ConvertSpotifySongs.vue 24 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037
  1. <script setup lang="ts">
  2. import {
  3. defineProps,
  4. defineAsyncComponent,
  5. onMounted,
  6. ref,
  7. reactive,
  8. computed
  9. } from "vue";
  10. import Toast from "toasters";
  11. import { useModalsStore } from "@/stores/modals";
  12. import { useWebsocketsStore } from "@/stores/websockets";
  13. const Modal = defineAsyncComponent(() => import("@/components/Modal.vue"));
  14. const SongItem = defineAsyncComponent(
  15. () => import("@/components/SongItem.vue")
  16. );
  17. const QuickConfirm = defineAsyncComponent(
  18. () => import("@/components/QuickConfirm.vue")
  19. );
  20. const { openModal, closeCurrentModal } = useModalsStore();
  21. const { socket } = useWebsocketsStore();
  22. const TAG = "CSS";
  23. const props = defineProps({
  24. modalUuid: { type: String, required: true },
  25. playlistId: { type: String, default: null }
  26. });
  27. const playlist = ref(null);
  28. const spotifySongs = ref([]);
  29. const spotifyTracks = reactive({});
  30. const spotifyArtists = reactive({});
  31. const loadingPlaylist = ref(false);
  32. const loadedPlaylist = ref(false);
  33. const loadingSpotifyTracks = ref(false);
  34. const loadedSpotifyTracks = ref(false);
  35. const gettingAllAlternativeMediaPerTrack = ref(false);
  36. const gotAllAlternativeMediaPerTrack = ref(false);
  37. const alternativeMediaPerTrack = reactive({});
  38. const alternativeMediaMap = reactive({});
  39. const alternativeMediaFailedMap = reactive({});
  40. const gettingMissingAlternativeMedia = ref(false);
  41. const replacingAllSpotifySongs = ref(false);
  42. const currentConvertType = ref("track");
  43. const showReplaceButtonPerAlternative = ref(false);
  44. const hideSpotifySongsWithNoAlternativesFound = ref(false);
  45. const preferredAlternativeSongMode = ref<
  46. "FIRST" | "LYRICS" | "TOPIC" | "LYRICS_TOPIC" | "TOPIC_LYRICS"
  47. >("FIRST");
  48. // const singleMode = ref(false);
  49. const showExtra = ref(false);
  50. const missingMediaSources = computed(() => {
  51. const missingMediaSources = [];
  52. Object.values(alternativeMediaPerTrack).forEach(({ mediaSources }) => {
  53. mediaSources.forEach(mediaSource => {
  54. if (
  55. !alternativeMediaMap[mediaSource] &&
  56. !alternativeMediaFailedMap[mediaSource] &&
  57. missingMediaSources.indexOf(mediaSource) === -1
  58. )
  59. missingMediaSources.push(mediaSource);
  60. });
  61. });
  62. return missingMediaSources;
  63. });
  64. const preferredAlternativeSongPerTrack = computed(() => {
  65. const returnObject = {};
  66. Object.entries(alternativeMediaPerTrack).forEach(
  67. ([spotifyMediaSource, { mediaSources }]) => {
  68. returnObject[spotifyMediaSource] = null;
  69. if (mediaSources.length === 0) return;
  70. let sortFn = (mediaSourceA, mediaSourceB) => {
  71. if (preferredAlternativeSongMode.value === "FIRST") return 0;
  72. const aHasLyrics =
  73. alternativeMediaMap[mediaSourceA].title
  74. .toLowerCase()
  75. .indexOf("lyric") !== -1;
  76. const aHasTopic =
  77. alternativeMediaMap[mediaSourceA].artists[0]
  78. .toLowerCase()
  79. .indexOf("topic") !== -1;
  80. const bHasLyrics =
  81. alternativeMediaMap[mediaSourceB].title
  82. .toLowerCase()
  83. .indexOf("lyric") !== -1;
  84. const bHasTopic =
  85. alternativeMediaMap[mediaSourceB].artists[0]
  86. .toLowerCase()
  87. .indexOf("topic") !== -1;
  88. if (preferredAlternativeSongMode.value === "LYRICS") {
  89. if (aHasLyrics && bHasLyrics) return 0;
  90. if (aHasLyrics && !bHasLyrics) return -1;
  91. if (!aHasLyrics && bHasLyrics) return 1;
  92. return 0;
  93. }
  94. if (preferredAlternativeSongMode.value === "TOPIC") {
  95. if (aHasTopic && bHasTopic) return 0;
  96. if (aHasTopic && !bHasTopic) return -1;
  97. if (!aHasTopic && bHasTopic) return 1;
  98. return 0;
  99. }
  100. if (preferredAlternativeSongMode.value === "LYRICS_TOPIC") {
  101. if (aHasLyrics && bHasLyrics) return 0;
  102. if (aHasLyrics && !bHasLyrics) return -1;
  103. if (!aHasLyrics && bHasLyrics) return 1;
  104. if (aHasTopic && bHasTopic) return 0;
  105. if (aHasTopic && !bHasTopic) return -1;
  106. if (!aHasTopic && bHasTopic) return 1;
  107. return 0;
  108. }
  109. if (preferredAlternativeSongMode.value === "TOPIC_LYRICS") {
  110. if (aHasTopic && bHasTopic) return 0;
  111. if (aHasTopic && !bHasTopic) return -1;
  112. if (!aHasTopic && bHasTopic) return 1;
  113. if (aHasLyrics && bHasLyrics) return 0;
  114. if (aHasLyrics && !bHasLyrics) return -1;
  115. if (!aHasLyrics && bHasLyrics) return 1;
  116. return 0;
  117. }
  118. };
  119. if (
  120. mediaSources.length === 1 ||
  121. preferredAlternativeSongMode.value === "FIRST"
  122. )
  123. sortFn = () => 0;
  124. else if (preferredAlternativeSongMode.value === "LYRICS")
  125. sortFn = mediaSourceA => {
  126. if (!alternativeMediaMap[mediaSourceA]) return 0;
  127. if (
  128. alternativeMediaMap[mediaSourceA].title
  129. .toLowerCase()
  130. .indexOf("lyric") !== -1
  131. )
  132. return -1;
  133. return 1;
  134. };
  135. else if (preferredAlternativeSongMode.value === "TOPIC")
  136. sortFn = mediaSourceA => {
  137. if (!alternativeMediaMap[mediaSourceA]) return 0;
  138. if (
  139. alternativeMediaMap[mediaSourceA].artists[0]
  140. .toLowerCase()
  141. .indexOf("topic") !== -1
  142. )
  143. return -1;
  144. return 1;
  145. };
  146. const [firstMediaSource] = mediaSources
  147. .slice()
  148. .filter(mediaSource => !!alternativeMediaMap[mediaSource])
  149. .sort(sortFn);
  150. returnObject[spotifyMediaSource] = firstMediaSource;
  151. }
  152. );
  153. return returnObject;
  154. });
  155. const replaceAllSpotifySongs = async () => {
  156. if (replacingAllSpotifySongs.value) return;
  157. replacingAllSpotifySongs.value = true;
  158. const replaceArray = [];
  159. spotifySongs.value.forEach(spotifySong => {
  160. const spotifyMediaSource = spotifySong.mediaSource;
  161. const replacementMediaSource =
  162. preferredAlternativeSongPerTrack.value[spotifyMediaSource];
  163. if (!spotifyMediaSource || !replacementMediaSource) return;
  164. replaceArray.push([spotifyMediaSource, replacementMediaSource]);
  165. });
  166. const promises = replaceArray.map(
  167. ([spotifyMediaSource, replacementMediaSource]) =>
  168. new Promise<void>(resolve => {
  169. socket.dispatch(
  170. "playlists.replaceSongInPlaylist",
  171. spotifyMediaSource,
  172. replacementMediaSource,
  173. props.playlistId,
  174. res => {
  175. console.log(
  176. "playlists.replaceSongInPlaylist response",
  177. res
  178. );
  179. resolve();
  180. }
  181. );
  182. })
  183. );
  184. Promise.allSettled(promises).finally(() => {
  185. replacingAllSpotifySongs.value = false;
  186. });
  187. };
  188. const replaceSpotifySong = (oldMediaSource, newMediaSource) => {
  189. socket.dispatch(
  190. "playlists.replaceSongInPlaylist",
  191. oldMediaSource,
  192. newMediaSource,
  193. props.playlistId,
  194. res => {
  195. console.log("playlists.replaceSongInPlaylist response", res);
  196. }
  197. );
  198. };
  199. const getMissingAlternativeMedia = () => {
  200. if (gettingMissingAlternativeMedia.value) return;
  201. gettingMissingAlternativeMedia.value = true;
  202. const _missingMediaSources = missingMediaSources.value;
  203. console.log("Getting missing", _missingMediaSources);
  204. socket.dispatch(
  205. "media.getMediaFromMediaSources",
  206. _missingMediaSources,
  207. res => {
  208. if (res.status === "success") {
  209. const { songMap } = res.data;
  210. _missingMediaSources.forEach(missingMediaSource => {
  211. if (songMap[missingMediaSource])
  212. alternativeMediaMap[missingMediaSource] =
  213. songMap[missingMediaSource];
  214. else alternativeMediaFailedMap[missingMediaSource] = true;
  215. });
  216. }
  217. gettingMissingAlternativeMedia.value = false;
  218. }
  219. );
  220. };
  221. const getAlternativeMedia = () => {
  222. if (
  223. gettingAllAlternativeMediaPerTrack.value ||
  224. gotAllAlternativeMediaPerTrack.value
  225. )
  226. return;
  227. gettingAllAlternativeMediaPerTrack.value = true;
  228. const mediaSources = spotifySongs.value.map(song => song.mediaSource);
  229. socket.dispatch("apis.getAlternativeMediaSourcesForTracks", mediaSources, {
  230. cb: res => {
  231. console.log(
  232. "apis.getAlternativeMediaSourcesForTracks response",
  233. res
  234. );
  235. },
  236. onProgress: data => {
  237. console.log(
  238. "apis.getAlternativeMediaSourcesForTracks onProgress",
  239. data
  240. );
  241. if (data.status === "working") {
  242. if (data.data.status === "success") {
  243. const { mediaSource, result } = data.data;
  244. if (!spotifyTracks[mediaSource]) return;
  245. alternativeMediaPerTrack[mediaSource] = result;
  246. }
  247. } else if (data.status === "finished") {
  248. gotAllAlternativeMediaPerTrack.value = true;
  249. gettingAllAlternativeMediaPerTrack.value = false;
  250. }
  251. }
  252. });
  253. };
  254. const loadSpotifyTracks = () =>
  255. new Promise<void>(resolve => {
  256. console.debug(TAG, "Loading Spotify tracks");
  257. loadingSpotifyTracks.value = true;
  258. const mediaSources = spotifySongs.value.map(song => song.mediaSource);
  259. socket.dispatch(
  260. "spotify.getTracksFromMediaSources",
  261. mediaSources,
  262. res => {
  263. console.debug(TAG, "Get tracks response", res);
  264. if (res.status !== "success") {
  265. new Toast(res.message);
  266. closeCurrentModal();
  267. return;
  268. }
  269. const { tracks } = res.data;
  270. Object.entries(tracks).forEach(([mediaSource, track]) => {
  271. spotifyTracks[mediaSource] = track;
  272. track.artistIds.forEach((artistId, artistIndex) => {
  273. if (!spotifyArtists[artistId]) {
  274. spotifyArtists[artistId] = {
  275. name: track.artists[artistIndex],
  276. songs: [],
  277. expanded: false
  278. };
  279. }
  280. spotifyArtists[artistId].songs.push(mediaSource);
  281. });
  282. });
  283. console.debug(TAG, "Loaded Spotify tracks");
  284. loadedSpotifyTracks.value = true;
  285. loadingSpotifyTracks.value = false;
  286. resolve();
  287. }
  288. );
  289. });
  290. const loadPlaylist = () =>
  291. new Promise<void>(resolve => {
  292. console.debug(TAG, `Loading playlist ${props.playlistId}`);
  293. loadingPlaylist.value = true;
  294. socket.dispatch("playlists.getPlaylist", props.playlistId, res => {
  295. console.debug(TAG, "Get playlist response", res);
  296. if (res.status !== "success") {
  297. new Toast(res.message);
  298. closeCurrentModal();
  299. return;
  300. }
  301. playlist.value = res.data.playlist;
  302. spotifySongs.value = playlist.value.songs.filter(song =>
  303. song.mediaSource.startsWith("spotify:")
  304. );
  305. console.debug(TAG, `Loaded playlist ${props.playlistId}`);
  306. loadedPlaylist.value = true;
  307. loadingPlaylist.value = false;
  308. resolve();
  309. });
  310. });
  311. const removeSpotifyTrack = mediaSource => {
  312. const spotifyTrack = spotifyTracks[mediaSource];
  313. if (spotifyTrack) {
  314. delete spotifyTracks[mediaSource];
  315. spotifyTrack.artistIds.forEach(artistId => {
  316. const spotifyArtist = spotifyArtists[artistId];
  317. if (spotifyArtist) {
  318. if (spotifyArtist.songs.length === 1)
  319. delete spotifyArtists[artistId];
  320. else
  321. spotifyArtists[artistId].songs = spotifyArtists[
  322. artistId
  323. ].songs.filter(
  324. _mediaSource => _mediaSource !== mediaSource
  325. );
  326. }
  327. });
  328. }
  329. };
  330. onMounted(() => {
  331. console.debug(TAG, "On mounted start");
  332. loadPlaylist().then(loadSpotifyTracks);
  333. socket.on(
  334. "event:playlist.song.removed",
  335. res => {
  336. console.log("SONG REMOVED", res);
  337. if (
  338. loadedPlaylist.value &&
  339. playlist.value._id === res.data.playlistId
  340. ) {
  341. const { oldMediaSource } = res.data;
  342. // remove song
  343. playlist.value.songs = playlist.value.songs.filter(
  344. song => song.mediaSource !== oldMediaSource
  345. );
  346. spotifySongs.value = spotifySongs.value.filter(
  347. song => song.mediaSource !== oldMediaSource
  348. );
  349. removeSpotifyTrack(oldMediaSource);
  350. delete alternativeMediaMap[oldMediaSource];
  351. delete alternativeMediaFailedMap[oldMediaSource];
  352. }
  353. },
  354. { modalUuid: props.modalUuid }
  355. );
  356. socket.on(
  357. "event:playlist.song.replaced",
  358. res => {
  359. console.log(
  360. "SONG REPLACED",
  361. res,
  362. playlist.value._id === res.data.playlistId
  363. );
  364. if (
  365. loadedPlaylist.value &&
  366. playlist.value._id === res.data.playlistId
  367. ) {
  368. const { oldMediaSource } = res.data;
  369. // remove song
  370. playlist.value.songs = playlist.value.songs.filter(
  371. song => song.mediaSource !== oldMediaSource
  372. );
  373. spotifySongs.value = spotifySongs.value.filter(
  374. song => song.mediaSource !== oldMediaSource
  375. );
  376. removeSpotifyTrack(oldMediaSource);
  377. delete alternativeMediaMap[oldMediaSource];
  378. delete alternativeMediaFailedMap[oldMediaSource];
  379. }
  380. },
  381. { modalUuid: props.modalUuid }
  382. );
  383. console.debug(TAG, "On mounted end");
  384. });
  385. </script>
  386. <template>
  387. <div>
  388. <modal
  389. title="Convert Spotify Songs"
  390. class="convert-spotify-songs-modal"
  391. size="wide"
  392. @closed="closeCurrentModal()"
  393. >
  394. <template #body>
  395. <template v-if="loadedPlaylist && spotifySongs.length === 0">
  396. <h2>All Spotify songs have been converted</h2>
  397. <button
  398. class="button is-primary is-fullwidth"
  399. @click="closeCurrentModal()"
  400. >
  401. Close modal
  402. </button>
  403. </template>
  404. <template v-else>
  405. <div class="buttons-options-info-row">
  406. <div class="buttons">
  407. <quick-confirm
  408. v-if="
  409. gotAllAlternativeMediaPerTrack &&
  410. missingMediaSources.length === 0 &&
  411. !replacingAllSpotifySongs
  412. "
  413. placement="top"
  414. @confirm="replaceAllSpotifySongs()"
  415. >
  416. <button class="button is-primary is-fullwidth">
  417. Replace all available songs with provided
  418. prefer settings
  419. </button>
  420. </quick-confirm>
  421. <button
  422. v-if="
  423. loadedSpotifyTracks &&
  424. !gettingAllAlternativeMediaPerTrack &&
  425. !gotAllAlternativeMediaPerTrack
  426. "
  427. class="button is-primary"
  428. @click="getAlternativeMedia()"
  429. >
  430. Get alternative media
  431. </button>
  432. <button
  433. v-if="
  434. gotAllAlternativeMediaPerTrack &&
  435. !gettingMissingAlternativeMedia &&
  436. missingMediaSources.length > 0
  437. "
  438. class="button is-primary"
  439. @click="getMissingAlternativeMedia()"
  440. >
  441. Get missing alternative media
  442. </button>
  443. </div>
  444. <div class="options">
  445. <p class="is-expanded checkbox-control">
  446. <label class="switch">
  447. <input
  448. type="checkbox"
  449. id="show-extra"
  450. v-model="showExtra"
  451. />
  452. <span class="slider round"></span>
  453. </label>
  454. <label for="show-extra">
  455. <p>Show extra info</p>
  456. </label>
  457. </p>
  458. <p class="is-expanded checkbox-control">
  459. <label class="switch">
  460. <input
  461. type="checkbox"
  462. id="show-replace-button-per-alternative"
  463. v-model="
  464. showReplaceButtonPerAlternative
  465. "
  466. />
  467. <span class="slider round"></span>
  468. </label>
  469. <label
  470. for="show-replace-button-per-alternative"
  471. >
  472. <p>Show replace button per alternative</p>
  473. </label>
  474. </p>
  475. <p class="is-expanded checkbox-control">
  476. <label class="switch">
  477. <input
  478. type="checkbox"
  479. id="hide-spotify-songs-with-no-alternatives-found"
  480. v-model="
  481. hideSpotifySongsWithNoAlternativesFound
  482. "
  483. />
  484. <span class="slider round"></span>
  485. </label>
  486. <label
  487. for="hide-spotify-songs-with-no-alternatives-found"
  488. >
  489. <p>
  490. Hide Spotify songs with no alternatives
  491. found
  492. </p>
  493. </label>
  494. </p>
  495. <div class="control">
  496. <label class="label"
  497. >Get alternatives per</label
  498. >
  499. <p class="control is-expanded select">
  500. <select
  501. v-model="currentConvertType"
  502. :disabled="
  503. gettingAllAlternativeMediaPerTrack
  504. "
  505. >
  506. <option value="track">Track</option>
  507. <option value="artist">Artist</option>
  508. <option value="album">Album</option>
  509. </select>
  510. </p>
  511. </div>
  512. <div class="control">
  513. <label class="label"
  514. >Preferred track mode</label
  515. >
  516. <p class="control is-expanded select">
  517. <select
  518. v-model="preferredAlternativeSongMode"
  519. :disabled="false"
  520. >
  521. <option value="FIRST">
  522. First song
  523. </option>
  524. <option value="LYRICS">
  525. First song with lyrics in title
  526. </option>
  527. <option value="TOPIC">
  528. First song from topic channel
  529. (YouTube only)
  530. </option>
  531. <option value="LYRICS_TOPIC">
  532. First song with lyrics in title, or
  533. from topic channel (YouTube only)
  534. </option>
  535. <option value="TOPIC_LYRICS">
  536. First song from topic channel
  537. (YouTube only), or with lyrics in
  538. title
  539. </option>
  540. </select>
  541. </p>
  542. </div>
  543. </div>
  544. <div class="info">
  545. <h6>Status</h6>
  546. <p>Loading playlist: {{ loadingPlaylist }}</p>
  547. <p>Loaded playlist: {{ loadedPlaylist }}</p>
  548. <p>
  549. Spotify songs in playlist:
  550. {{ spotifySongs.length }}
  551. </p>
  552. <p>Converting by {{ currentConvertType }}</p>
  553. <hr />
  554. <p>
  555. Loading Spotify tracks:
  556. {{ loadingSpotifyTracks }}
  557. </p>
  558. <p>
  559. Loaded Spotify tracks: {{ loadedSpotifyTracks }}
  560. </p>
  561. <p>
  562. Spotify tracks loaded:
  563. {{ Object.keys(spotifyTracks).length }}
  564. </p>
  565. <p>
  566. Spotify artists:
  567. {{ Object.keys(spotifyArtists).length }}
  568. </p>
  569. <p>
  570. Getting missing alternative media:
  571. {{ gettingMissingAlternativeMedia }}
  572. </p>
  573. <p>
  574. Getting all alternative media per track:
  575. {{ gettingAllAlternativeMediaPerTrack }}
  576. </p>
  577. <p>
  578. Got all alternative media per track:
  579. {{ gotAllAlternativeMediaPerTrack }}
  580. </p>
  581. <hr />
  582. <p>
  583. Alternative media loaded:
  584. {{ Object.keys(alternativeMediaMap).length }}
  585. </p>
  586. <p>
  587. Alternative media that failed to load:
  588. {{
  589. Object.keys(alternativeMediaFailedMap)
  590. .length
  591. }}
  592. </p>
  593. <hr />
  594. <p>
  595. Replacing all Spotify songs:
  596. {{ replacingAllSpotifySongs }}
  597. </p>
  598. </div>
  599. </div>
  600. <br />
  601. <hr />
  602. <div
  603. class="convert-table convert-song-by-track"
  604. v-if="currentConvertType === 'track'"
  605. >
  606. <h4>Spotify songs</h4>
  607. <h4>Alternative songs</h4>
  608. <template
  609. v-for="spotifySong in spotifySongs"
  610. :key="spotifySong.mediaSource"
  611. >
  612. <div
  613. class="convert-table-cell convert-table-cell-left"
  614. >
  615. <song-item :song="spotifySong">
  616. <template #leftIcon>
  617. <a
  618. :href="`https://open.spotify.com/track/${
  619. spotifySong.mediaSource.split(
  620. ':'
  621. )[1]
  622. }`"
  623. target="_blank"
  624. >
  625. <div
  626. class="spotify-icon left-icon"
  627. ></div>
  628. </a>
  629. </template>
  630. </song-item>
  631. <p>
  632. Media source: {{ spotifySong.mediaSource }}
  633. </p>
  634. <p v-if="loadedSpotifyTracks">
  635. ISRC:
  636. {{
  637. spotifyTracks[spotifySong.mediaSource]
  638. .externalIds.isrc
  639. }}
  640. </p>
  641. </div>
  642. <div
  643. class="convert-table-cell convert-table-cell-right"
  644. >
  645. <p
  646. v-if="
  647. !alternativeMediaPerTrack[
  648. spotifySong.mediaSource
  649. ]
  650. "
  651. >
  652. Alternatives not loaded yet
  653. </p>
  654. <template v-else>
  655. <div class="alternative-media-items">
  656. <div
  657. class="alternative-media-item"
  658. :class="{
  659. 'selected-alternative-song':
  660. preferredAlternativeSongPerTrack[
  661. spotifySong.mediaSource
  662. ] ===
  663. alternativeMediaSource &&
  664. missingMediaSources.length ===
  665. 0
  666. }"
  667. v-for="alternativeMediaSource in alternativeMediaPerTrack[
  668. spotifySong.mediaSource
  669. ].mediaSources"
  670. :key="
  671. spotifySong.mediaSource +
  672. alternativeMediaSource
  673. "
  674. >
  675. <p
  676. v-if="
  677. alternativeMediaFailedMap[
  678. alternativeMediaSource
  679. ]
  680. "
  681. >
  682. Song
  683. {{ alternativeMediaSource }}
  684. failed to load
  685. </p>
  686. <p
  687. v-else-if="
  688. !alternativeMediaMap[
  689. alternativeMediaSource
  690. ]
  691. "
  692. >
  693. Song
  694. {{ alternativeMediaSource }}
  695. hasn't been loaded yet
  696. </p>
  697. <template v-else>
  698. <div>
  699. <song-item
  700. :song="
  701. alternativeMediaMap[
  702. alternativeMediaSource
  703. ]
  704. "
  705. >
  706. <template #leftIcon>
  707. <a
  708. v-if="
  709. alternativeMediaSource.split(
  710. ':'
  711. )[0] ===
  712. 'youtube'
  713. "
  714. :href="`https://youtu.be/${
  715. alternativeMediaSource.split(
  716. ':'
  717. )[1]
  718. }`"
  719. target="_blank"
  720. >
  721. <div
  722. class="youtube-icon left-icon"
  723. ></div>
  724. </a>
  725. <a
  726. v-if="
  727. alternativeMediaSource.split(
  728. ':'
  729. )[0] ===
  730. 'soundcloud'
  731. "
  732. target="_blank"
  733. >
  734. <div
  735. class="soundcloud-icon left-icon"
  736. ></div>
  737. </a>
  738. </template>
  739. </song-item>
  740. <quick-confirm
  741. v-if="
  742. showReplaceButtonPerAlternative
  743. "
  744. placement="top"
  745. @confirm="
  746. replaceSpotifySong(
  747. spotifySong.mediaSource,
  748. alternativeMediaSource
  749. )
  750. "
  751. >
  752. <button
  753. class="button is-primary is-fullwidth"
  754. >
  755. Replace Spotify song
  756. with this song
  757. </button>
  758. </quick-confirm>
  759. </div>
  760. <ul v-if="showExtra">
  761. <li
  762. v-for="origin in alternativeMediaPerTrack[
  763. spotifySong
  764. .mediaSource
  765. ].mediaSourcesOrigins[
  766. alternativeMediaSource
  767. ]"
  768. :key="
  769. spotifySong.mediaSource +
  770. alternativeMediaSource +
  771. origin
  772. "
  773. >
  774. <hr />
  775. <ul>
  776. <li
  777. v-for="originItem in origin"
  778. :key="
  779. spotifySong.mediaSource +
  780. alternativeMediaSource +
  781. origin +
  782. originItem
  783. "
  784. >
  785. +
  786. {{ originItem }}
  787. </li>
  788. </ul>
  789. </li>
  790. </ul>
  791. </template>
  792. </div>
  793. </div>
  794. </template>
  795. </div>
  796. </template>
  797. </div>
  798. </template>
  799. </template>
  800. </modal>
  801. </div>
  802. </template>
  803. <style lang="less" scoped>
  804. :deep(.song-item) {
  805. .left-icon {
  806. cursor: pointer;
  807. }
  808. }
  809. .tracks {
  810. display: flex;
  811. flex-direction: column;
  812. .track-row {
  813. .left,
  814. .right {
  815. padding: 8px;
  816. width: 50%;
  817. box-shadow: inset 0px 0px 1px white;
  818. display: flex;
  819. flex-direction: column;
  820. row-gap: 8px;
  821. }
  822. }
  823. }
  824. .alternative-media-items {
  825. display: flex;
  826. flex-direction: column;
  827. row-gap: 12px;
  828. }
  829. .convert-table {
  830. display: grid;
  831. grid-template-columns: 50% 50%;
  832. gap: 1px;
  833. .convert-table-cell {
  834. outline: 1px solid white;
  835. padding: 4px;
  836. }
  837. }
  838. .selected-alternative-song {
  839. // outline: 4px solid red;
  840. border-left: 12px solid var(--primary-color);
  841. padding: 4px;
  842. }
  843. .buttons-options-info-row {
  844. display: grid;
  845. grid-template-columns: 33.3% 33.3% 33.3%;
  846. gap: 8px;
  847. .buttons,
  848. .options {
  849. display: flex;
  850. flex-direction: column;
  851. row-gap: 8px;
  852. > .control {
  853. margin-bottom: 0 !important;
  854. }
  855. }
  856. }
  857. // .column-headers {
  858. // display: flex;
  859. // flex-direction: row;
  860. // .column-header {
  861. // flex: 1;
  862. // }
  863. // }
  864. // .artists {
  865. // display: flex;
  866. // flex-direction: column;
  867. // .artist-item {
  868. // display: flex;
  869. // flex-direction: column;
  870. // row-gap: 8px;
  871. // box-shadow: inset 0px 0px 1px white;
  872. // width: 50%;
  873. // position: relative;
  874. // .spotify-section {
  875. // display: flex;
  876. // flex-direction: column;
  877. // row-gap: 8px;
  878. // padding: 8px 12px;
  879. // .spotify-songs {
  880. // display: flex;
  881. // flex-direction: column;
  882. // row-gap: 4px;
  883. // }
  884. // }
  885. // .soundcloud-section {
  886. // position: absolute;
  887. // left: 100%;
  888. // top: 0;
  889. // width: 100%;
  890. // height: 100%;
  891. // overflow: hidden;
  892. // box-shadow: inset 0px 0px 1px white;
  893. // padding: 8px 12px;
  894. // }
  895. // }
  896. // }
  897. </style>