ConvertSpotifySongs.vue 32 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339
  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 spotifyAlbums = reactive({});
  31. const spotifyArtists = reactive({});
  32. const loadingPlaylist = ref(false);
  33. const loadedPlaylist = ref(false);
  34. const loadingSpotifyTracks = ref(false);
  35. const loadedSpotifyTracks = ref(false);
  36. const loadingSpotifyAlbums = ref(false);
  37. const loadedSpotifyAlbums = ref(false);
  38. const gettingAllAlternativeMediaPerTrack = ref(false);
  39. const gotAllAlternativeMediaPerTrack = ref(false);
  40. const alternativeMediaPerTrack = reactive({});
  41. const alternativeMediaMap = reactive({});
  42. const alternativeMediaFailedMap = reactive({});
  43. const gettingMissingAlternativeMedia = ref(false);
  44. const replacingAllSpotifySongs = ref(false);
  45. const currentConvertType = ref<"track" | "album" | "artist">("album");
  46. const showReplaceButtonPerAlternative = ref(false);
  47. const hideSpotifySongsWithNoAlternativesFound = ref(false);
  48. const preferredAlternativeSongMode = ref<
  49. "FIRST" | "LYRICS" | "TOPIC" | "LYRICS_TOPIC" | "TOPIC_LYRICS"
  50. >("FIRST");
  51. // const singleMode = ref(false);
  52. const showExtra = ref(false);
  53. const collectAlternativeMediaSourcesOrigins = ref(false);
  54. const minimumSongsPerAlbum = ref(2);
  55. const sortAlbumMode = ref<
  56. "SONG_COUNT_ASC" | "SONG_COUNT_DESC" | "NAME_DESC" | "NAME_ASC"
  57. >("SONG_COUNT_ASC");
  58. const filteredSpotifySongs = computed(() =>
  59. hideSpotifySongsWithNoAlternativesFound.value
  60. ? spotifySongs.value.filter(
  61. spotifySong =>
  62. (!gettingAllAlternativeMediaPerTrack.value &&
  63. !gotAllAlternativeMediaPerTrack.value) ||
  64. (alternativeMediaPerTrack[spotifySong.mediaSource] &&
  65. alternativeMediaPerTrack[spotifySong.mediaSource]
  66. .mediaSources.length > 0)
  67. )
  68. : spotifySongs.value
  69. );
  70. const filteredSpotifyAlbums = computed(() => {
  71. let albums = Object.values(spotifyAlbums);
  72. albums = albums.filter(
  73. album => album.songs.length >= minimumSongsPerAlbum.value
  74. );
  75. let sortFn = null;
  76. if (sortAlbumMode.value === "SONG_COUNT_ASC")
  77. sortFn = (albumA, albumB) => albumA.songs.length - albumB.songs.length;
  78. else if (sortAlbumMode.value === "SONG_COUNT_DESC")
  79. sortFn = (albumA, albumB) => albumB.songs.length - albumA.songs.length;
  80. else if (loadedSpotifyAlbums.value && sortAlbumMode.value === "NAME_ASC")
  81. sortFn = (albumA, albumB) => {
  82. const nameA = albumA.rawData?.name?.toLowerCase();
  83. const nameB = albumB.rawData?.name?.toLowerCase();
  84. if (nameA === nameB) return 0;
  85. if (nameA < nameB) return -1;
  86. if (nameA > nameB) return 1;
  87. };
  88. else if (loadedSpotifyAlbums.value && sortAlbumMode.value === "NAME_DESC")
  89. sortFn = (albumA, albumB) => {
  90. const nameA = albumA.rawData?.name?.toLowerCase();
  91. const nameB = albumB.rawData?.name?.toLowerCase();
  92. if (nameA === nameB) return 0;
  93. if (nameA > nameB) return -1;
  94. if (nameA < nameB) return 1;
  95. };
  96. if (sortFn) albums = albums.sort(sortFn);
  97. return albums;
  98. });
  99. const missingMediaSources = computed(() => {
  100. const missingMediaSources = [];
  101. Object.values(alternativeMediaPerTrack).forEach(({ mediaSources }) => {
  102. mediaSources.forEach(mediaSource => {
  103. if (
  104. !alternativeMediaMap[mediaSource] &&
  105. !alternativeMediaFailedMap[mediaSource] &&
  106. missingMediaSources.indexOf(mediaSource) === -1
  107. )
  108. missingMediaSources.push(mediaSource);
  109. });
  110. });
  111. return missingMediaSources;
  112. });
  113. const preferredAlternativeSongPerTrack = computed(() => {
  114. const returnObject = {};
  115. Object.entries(alternativeMediaPerTrack).forEach(
  116. ([spotifyMediaSource, { mediaSources }]) => {
  117. returnObject[spotifyMediaSource] = null;
  118. if (mediaSources.length === 0) return;
  119. let sortFn = (mediaSourceA, mediaSourceB) => {
  120. if (preferredAlternativeSongMode.value === "FIRST") return 0;
  121. const aHasLyrics =
  122. alternativeMediaMap[mediaSourceA].title
  123. .toLowerCase()
  124. .indexOf("lyric") !== -1;
  125. const aHasTopic =
  126. alternativeMediaMap[mediaSourceA].artists[0]
  127. .toLowerCase()
  128. .indexOf("topic") !== -1;
  129. const bHasLyrics =
  130. alternativeMediaMap[mediaSourceB].title
  131. .toLowerCase()
  132. .indexOf("lyric") !== -1;
  133. const bHasTopic =
  134. alternativeMediaMap[mediaSourceB].artists[0]
  135. .toLowerCase()
  136. .indexOf("topic") !== -1;
  137. if (preferredAlternativeSongMode.value === "LYRICS") {
  138. if (aHasLyrics && bHasLyrics) return 0;
  139. if (aHasLyrics && !bHasLyrics) return -1;
  140. if (!aHasLyrics && bHasLyrics) return 1;
  141. return 0;
  142. }
  143. if (preferredAlternativeSongMode.value === "TOPIC") {
  144. if (aHasTopic && bHasTopic) return 0;
  145. if (aHasTopic && !bHasTopic) return -1;
  146. if (!aHasTopic && bHasTopic) return 1;
  147. return 0;
  148. }
  149. if (preferredAlternativeSongMode.value === "LYRICS_TOPIC") {
  150. if (aHasLyrics && bHasLyrics) return 0;
  151. if (aHasLyrics && !bHasLyrics) return -1;
  152. if (!aHasLyrics && bHasLyrics) return 1;
  153. if (aHasTopic && bHasTopic) return 0;
  154. if (aHasTopic && !bHasTopic) return -1;
  155. if (!aHasTopic && bHasTopic) return 1;
  156. return 0;
  157. }
  158. if (preferredAlternativeSongMode.value === "TOPIC_LYRICS") {
  159. if (aHasTopic && bHasTopic) return 0;
  160. if (aHasTopic && !bHasTopic) return -1;
  161. if (!aHasTopic && bHasTopic) return 1;
  162. if (aHasLyrics && bHasLyrics) return 0;
  163. if (aHasLyrics && !bHasLyrics) return -1;
  164. if (!aHasLyrics && bHasLyrics) return 1;
  165. return 0;
  166. }
  167. };
  168. if (
  169. mediaSources.length === 1 ||
  170. preferredAlternativeSongMode.value === "FIRST"
  171. )
  172. sortFn = () => 0;
  173. else if (preferredAlternativeSongMode.value === "LYRICS")
  174. sortFn = mediaSourceA => {
  175. if (!alternativeMediaMap[mediaSourceA]) return 0;
  176. if (
  177. alternativeMediaMap[mediaSourceA].title
  178. .toLowerCase()
  179. .indexOf("lyric") !== -1
  180. )
  181. return -1;
  182. return 1;
  183. };
  184. else if (preferredAlternativeSongMode.value === "TOPIC")
  185. sortFn = mediaSourceA => {
  186. if (!alternativeMediaMap[mediaSourceA]) return 0;
  187. if (
  188. alternativeMediaMap[mediaSourceA].artists[0]
  189. .toLowerCase()
  190. .indexOf("topic") !== -1
  191. )
  192. return -1;
  193. return 1;
  194. };
  195. const [firstMediaSource] = mediaSources
  196. .slice()
  197. .filter(mediaSource => !!alternativeMediaMap[mediaSource])
  198. .sort(sortFn);
  199. returnObject[spotifyMediaSource] = firstMediaSource;
  200. }
  201. );
  202. return returnObject;
  203. });
  204. const replaceAllSpotifySongs = async () => {
  205. if (replacingAllSpotifySongs.value) return;
  206. replacingAllSpotifySongs.value = true;
  207. const replaceArray = [];
  208. spotifySongs.value.forEach(spotifySong => {
  209. const spotifyMediaSource = spotifySong.mediaSource;
  210. const replacementMediaSource =
  211. preferredAlternativeSongPerTrack.value[spotifyMediaSource];
  212. if (!spotifyMediaSource || !replacementMediaSource) return;
  213. replaceArray.push([spotifyMediaSource, replacementMediaSource]);
  214. });
  215. const promises = replaceArray.map(
  216. ([spotifyMediaSource, replacementMediaSource]) =>
  217. new Promise<void>(resolve => {
  218. socket.dispatch(
  219. "playlists.replaceSongInPlaylist",
  220. spotifyMediaSource,
  221. replacementMediaSource,
  222. props.playlistId,
  223. res => {
  224. console.log(
  225. "playlists.replaceSongInPlaylist response",
  226. res
  227. );
  228. resolve();
  229. }
  230. );
  231. })
  232. );
  233. Promise.allSettled(promises).finally(() => {
  234. replacingAllSpotifySongs.value = false;
  235. });
  236. };
  237. const replaceSpotifySong = (oldMediaSource, newMediaSource) => {
  238. socket.dispatch(
  239. "playlists.replaceSongInPlaylist",
  240. oldMediaSource,
  241. newMediaSource,
  242. props.playlistId,
  243. res => {
  244. console.log("playlists.replaceSongInPlaylist response", res);
  245. }
  246. );
  247. };
  248. const getMissingAlternativeMedia = () => {
  249. if (gettingMissingAlternativeMedia.value) return;
  250. gettingMissingAlternativeMedia.value = true;
  251. const _missingMediaSources = missingMediaSources.value;
  252. console.log("Getting missing", _missingMediaSources);
  253. socket.dispatch(
  254. "media.getMediaFromMediaSources",
  255. _missingMediaSources,
  256. res => {
  257. if (res.status === "success") {
  258. const { songMap } = res.data;
  259. _missingMediaSources.forEach(missingMediaSource => {
  260. if (songMap[missingMediaSource])
  261. alternativeMediaMap[missingMediaSource] =
  262. songMap[missingMediaSource];
  263. else alternativeMediaFailedMap[missingMediaSource] = true;
  264. });
  265. }
  266. gettingMissingAlternativeMedia.value = false;
  267. }
  268. );
  269. };
  270. const getAlternativeMedia = () => {
  271. if (
  272. gettingAllAlternativeMediaPerTrack.value ||
  273. gotAllAlternativeMediaPerTrack.value
  274. )
  275. return;
  276. gettingAllAlternativeMediaPerTrack.value = true;
  277. const mediaSources = spotifySongs.value.map(song => song.mediaSource);
  278. socket.dispatch(
  279. "apis.getAlternativeMediaSourcesForTracks",
  280. mediaSources,
  281. collectAlternativeMediaSourcesOrigins.value,
  282. {
  283. cb: res => {
  284. console.log(
  285. "apis.getAlternativeMediaSourcesForTracks response",
  286. res
  287. );
  288. },
  289. onProgress: data => {
  290. console.log(
  291. "apis.getAlternativeMediaSourcesForTracks onProgress",
  292. data
  293. );
  294. if (data.status === "working") {
  295. if (data.data.status === "success") {
  296. const { mediaSource, result } = data.data;
  297. if (!spotifyTracks[mediaSource]) return;
  298. alternativeMediaPerTrack[mediaSource] = result;
  299. }
  300. } else if (data.status === "finished") {
  301. gotAllAlternativeMediaPerTrack.value = true;
  302. gettingAllAlternativeMediaPerTrack.value = false;
  303. }
  304. }
  305. }
  306. );
  307. };
  308. const loadSpotifyAlbums = () =>
  309. new Promise<void>(resolve => {
  310. console.debug(TAG, "Loading Spotify albums");
  311. loadingSpotifyAlbums.value = true;
  312. const albumIds = filteredSpotifyAlbums.value.map(
  313. album => album.albumId
  314. );
  315. socket.dispatch("spotify.getAlbumsFromIds", albumIds, res => {
  316. console.debug(TAG, "Get albums response", res);
  317. if (res.status !== "success") {
  318. new Toast(res.message);
  319. closeCurrentModal();
  320. return;
  321. }
  322. const { albums } = res.data;
  323. albums.forEach(album => {
  324. spotifyAlbums[album.albumId].rawData = album.rawData;
  325. });
  326. console.debug(TAG, "Loaded Spotify albums");
  327. loadedSpotifyAlbums.value = true;
  328. loadingSpotifyAlbums.value = false;
  329. resolve();
  330. });
  331. });
  332. const loadSpotifyTracks = () =>
  333. new Promise<void>(resolve => {
  334. console.debug(TAG, "Loading Spotify tracks");
  335. loadingSpotifyTracks.value = true;
  336. const mediaSources = spotifySongs.value.map(song => song.mediaSource);
  337. socket.dispatch(
  338. "spotify.getTracksFromMediaSources",
  339. mediaSources,
  340. res => {
  341. console.debug(TAG, "Get tracks response", res);
  342. if (res.status !== "success") {
  343. new Toast(res.message);
  344. closeCurrentModal();
  345. return;
  346. }
  347. const { tracks } = res.data;
  348. Object.entries(tracks).forEach(([mediaSource, track]) => {
  349. spotifyTracks[mediaSource] = track;
  350. const { albumId, albumImageUrl, artistIds, artists } =
  351. track;
  352. if (albumId) {
  353. if (!spotifyAlbums[albumId])
  354. spotifyAlbums[albumId] = {
  355. albumId,
  356. albumImageUrl,
  357. songs: []
  358. };
  359. spotifyAlbums[albumId].songs.push(mediaSource);
  360. }
  361. artistIds.forEach((artistId, artistIndex) => {
  362. if (!spotifyArtists[artistId]) {
  363. spotifyArtists[artistId] = {
  364. artistId,
  365. name: artists[artistIndex],
  366. songs: [],
  367. expanded: false
  368. };
  369. }
  370. spotifyArtists[artistId].songs.push(mediaSource);
  371. });
  372. });
  373. console.debug(TAG, "Loaded Spotify tracks");
  374. loadedSpotifyTracks.value = true;
  375. loadingSpotifyTracks.value = false;
  376. resolve();
  377. }
  378. );
  379. });
  380. const loadPlaylist = () =>
  381. new Promise<void>(resolve => {
  382. console.debug(TAG, `Loading playlist ${props.playlistId}`);
  383. loadingPlaylist.value = true;
  384. socket.dispatch("playlists.getPlaylist", props.playlistId, res => {
  385. console.debug(TAG, "Get playlist response", res);
  386. if (res.status !== "success") {
  387. new Toast(res.message);
  388. closeCurrentModal();
  389. return;
  390. }
  391. playlist.value = res.data.playlist;
  392. spotifySongs.value = playlist.value.songs.filter(song =>
  393. song.mediaSource.startsWith("spotify:")
  394. );
  395. console.debug(TAG, `Loaded playlist ${props.playlistId}`);
  396. loadedPlaylist.value = true;
  397. loadingPlaylist.value = false;
  398. resolve();
  399. });
  400. });
  401. const removeSpotifyTrack = mediaSource => {
  402. const spotifyTrack = spotifyTracks[mediaSource];
  403. if (spotifyTrack) {
  404. delete spotifyTracks[mediaSource];
  405. spotifyTrack.artistIds.forEach(artistId => {
  406. const spotifyArtist = spotifyArtists[artistId];
  407. if (spotifyArtist) {
  408. if (spotifyArtist.songs.length === 1)
  409. delete spotifyArtists[artistId];
  410. else
  411. spotifyArtists[artistId].songs = spotifyArtists[
  412. artistId
  413. ].songs.filter(
  414. _mediaSource => _mediaSource !== mediaSource
  415. );
  416. }
  417. });
  418. }
  419. };
  420. onMounted(() => {
  421. console.debug(TAG, "On mounted start");
  422. loadPlaylist().then(loadSpotifyTracks);
  423. socket.on(
  424. "event:playlist.song.removed",
  425. res => {
  426. console.log("SONG REMOVED", res);
  427. if (
  428. loadedPlaylist.value &&
  429. playlist.value._id === res.data.playlistId
  430. ) {
  431. const { oldMediaSource } = res.data;
  432. // remove song
  433. playlist.value.songs = playlist.value.songs.filter(
  434. song => song.mediaSource !== oldMediaSource
  435. );
  436. spotifySongs.value = spotifySongs.value.filter(
  437. song => song.mediaSource !== oldMediaSource
  438. );
  439. removeSpotifyTrack(oldMediaSource);
  440. delete alternativeMediaMap[oldMediaSource];
  441. delete alternativeMediaFailedMap[oldMediaSource];
  442. }
  443. },
  444. { modalUuid: props.modalUuid }
  445. );
  446. socket.on(
  447. "event:playlist.song.replaced",
  448. res => {
  449. console.log(
  450. "SONG REPLACED",
  451. res,
  452. playlist.value._id === res.data.playlistId
  453. );
  454. if (
  455. loadedPlaylist.value &&
  456. playlist.value._id === res.data.playlistId
  457. ) {
  458. const { oldMediaSource } = res.data;
  459. // remove song
  460. playlist.value.songs = playlist.value.songs.filter(
  461. song => song.mediaSource !== oldMediaSource
  462. );
  463. spotifySongs.value = spotifySongs.value.filter(
  464. song => song.mediaSource !== oldMediaSource
  465. );
  466. removeSpotifyTrack(oldMediaSource);
  467. delete alternativeMediaMap[oldMediaSource];
  468. delete alternativeMediaFailedMap[oldMediaSource];
  469. }
  470. },
  471. { modalUuid: props.modalUuid }
  472. );
  473. console.debug(TAG, "On mounted end");
  474. });
  475. </script>
  476. <template>
  477. <div>
  478. <modal
  479. title="Convert Spotify Songs"
  480. class="convert-spotify-songs-modal"
  481. size="wide"
  482. @closed="closeCurrentModal()"
  483. >
  484. <template #body>
  485. <template v-if="loadedPlaylist && spotifySongs.length === 0">
  486. <h2>All Spotify songs have been converted</h2>
  487. <button
  488. class="button is-primary is-fullwidth"
  489. @click="closeCurrentModal()"
  490. >
  491. Close modal
  492. </button>
  493. </template>
  494. <template v-else>
  495. <div class="buttons-options-info-row">
  496. <div class="buttons">
  497. <quick-confirm
  498. v-if="
  499. gotAllAlternativeMediaPerTrack &&
  500. missingMediaSources.length === 0 &&
  501. !replacingAllSpotifySongs
  502. "
  503. placement="top"
  504. @confirm="replaceAllSpotifySongs()"
  505. >
  506. <button class="button is-primary is-fullwidth">
  507. Replace all available songs with provided
  508. prefer settings
  509. </button>
  510. </quick-confirm>
  511. <button
  512. v-if="
  513. loadedSpotifyTracks &&
  514. !gettingAllAlternativeMediaPerTrack &&
  515. !gotAllAlternativeMediaPerTrack &&
  516. currentConvertType === 'track'
  517. "
  518. class="button is-primary"
  519. @click="getAlternativeMedia()"
  520. >
  521. Get alternative media
  522. </button>
  523. <button
  524. v-if="
  525. currentConvertType === 'track' &&
  526. gotAllAlternativeMediaPerTrack &&
  527. !gettingMissingAlternativeMedia &&
  528. missingMediaSources.length > 0
  529. "
  530. class="button is-primary"
  531. @click="getMissingAlternativeMedia()"
  532. >
  533. Get missing alternative media
  534. </button>
  535. <button
  536. v-if="
  537. loadedSpotifyTracks &&
  538. !loadingSpotifyAlbums &&
  539. !loadedSpotifyAlbums &&
  540. currentConvertType === 'album'
  541. "
  542. class="button is-primary"
  543. @click="loadSpotifyAlbums()"
  544. >
  545. Get Spotify albums
  546. </button>
  547. </div>
  548. <div class="options">
  549. <p class="is-expanded checkbox-control">
  550. <label class="switch">
  551. <input
  552. type="checkbox"
  553. id="show-extra"
  554. v-model="showExtra"
  555. />
  556. <span class="slider round"></span>
  557. </label>
  558. <label for="show-extra">
  559. <p>Show extra info</p>
  560. </label>
  561. </p>
  562. <p class="is-expanded checkbox-control">
  563. <label class="switch">
  564. <input
  565. type="checkbox"
  566. id="collect-alternative-media-sources-origins"
  567. v-model="
  568. collectAlternativeMediaSourcesOrigins
  569. "
  570. />
  571. <span class="slider round"></span>
  572. </label>
  573. <label
  574. for="collect-alternative-media-sources-origins"
  575. >
  576. <p>
  577. Collect alternative media sources
  578. origins
  579. </p>
  580. </label>
  581. </p>
  582. <p class="is-expanded checkbox-control">
  583. <label class="switch">
  584. <input
  585. type="checkbox"
  586. id="show-replace-button-per-alternative"
  587. v-model="
  588. showReplaceButtonPerAlternative
  589. "
  590. />
  591. <span class="slider round"></span>
  592. </label>
  593. <label
  594. for="show-replace-button-per-alternative"
  595. >
  596. <p>Show replace button per alternative</p>
  597. </label>
  598. </p>
  599. <p class="is-expanded checkbox-control">
  600. <label class="switch">
  601. <input
  602. type="checkbox"
  603. id="hide-spotify-songs-with-no-alternatives-found"
  604. v-model="
  605. hideSpotifySongsWithNoAlternativesFound
  606. "
  607. />
  608. <span class="slider round"></span>
  609. </label>
  610. <label
  611. for="hide-spotify-songs-with-no-alternatives-found"
  612. >
  613. <p>
  614. Hide Spotify songs with no alternatives
  615. found
  616. </p>
  617. </label>
  618. </p>
  619. <div class="control">
  620. <label class="label"
  621. >Get alternatives per</label
  622. >
  623. <p class="control is-expanded select">
  624. <select
  625. v-model="currentConvertType"
  626. :disabled="
  627. gettingAllAlternativeMediaPerTrack
  628. "
  629. >
  630. <option value="track">Track</option>
  631. <option value="artist">Artist</option>
  632. <option value="album">Album</option>
  633. </select>
  634. </p>
  635. </div>
  636. <div
  637. class="control"
  638. v-if="currentConvertType === 'track'"
  639. >
  640. <label class="label"
  641. >Preferred track mode</label
  642. >
  643. <p class="control is-expanded select">
  644. <select
  645. v-model="preferredAlternativeSongMode"
  646. :disabled="false"
  647. >
  648. <option value="FIRST">
  649. First song
  650. </option>
  651. <option value="LYRICS">
  652. First song with lyrics in title
  653. </option>
  654. <option value="TOPIC">
  655. First song from topic channel
  656. (YouTube only)
  657. </option>
  658. <option value="LYRICS_TOPIC">
  659. First song with lyrics in title, or
  660. from topic channel (YouTube only)
  661. </option>
  662. <option value="TOPIC_LYRICS">
  663. First song from topic channel
  664. (YouTube only), or with lyrics in
  665. title
  666. </option>
  667. </select>
  668. </p>
  669. </div>
  670. <div
  671. class="small-section"
  672. v-if="currentConvertType === 'album'"
  673. >
  674. <label class="label"
  675. >Minimum songs per album</label
  676. >
  677. <div class="control is-expanded">
  678. <input
  679. class="input"
  680. type="number"
  681. min="1"
  682. v-model="minimumSongsPerAlbum"
  683. />
  684. </div>
  685. </div>
  686. <div class="control">
  687. <label class="label">Sort album mode</label>
  688. <p class="control is-expanded select">
  689. <select v-model="sortAlbumMode">
  690. <option value="SONG_COUNT_ASC">
  691. Song count (ascending)
  692. </option>
  693. <option value="SONG_COUNT_DESC">
  694. Song count (descending)
  695. </option>
  696. <option value="NAME_ASC">
  697. Name (ascending)
  698. </option>
  699. <option value="NAME_DESC">
  700. Name (descending)
  701. </option>
  702. </select>
  703. </p>
  704. </div>
  705. </div>
  706. <div class="info">
  707. <h6>Status</h6>
  708. <p>Loading playlist: {{ loadingPlaylist }}</p>
  709. <p>Loaded playlist: {{ loadedPlaylist }}</p>
  710. <p>
  711. Spotify songs in playlist:
  712. {{ spotifySongs.length }}
  713. </p>
  714. <p>Converting by {{ currentConvertType }}</p>
  715. <hr />
  716. <p>
  717. Loading Spotify tracks:
  718. {{ loadingSpotifyTracks }}
  719. </p>
  720. <p>
  721. Loaded Spotify tracks: {{ loadedSpotifyTracks }}
  722. </p>
  723. <p>
  724. Spotify tracks loaded:
  725. {{ Object.keys(spotifyTracks).length }}
  726. </p>
  727. <p>
  728. Loading Spotify albums:
  729. {{ loadingSpotifyAlbums }}
  730. </p>
  731. <p>
  732. Loaded Spotify albums: {{ loadedSpotifyAlbums }}
  733. </p>
  734. <p>
  735. Spotify albums:
  736. {{ Object.keys(spotifyAlbums).length }}
  737. </p>
  738. <p>
  739. Spotify artists:
  740. {{ Object.keys(spotifyArtists).length }}
  741. </p>
  742. <p>
  743. Getting missing alternative media:
  744. {{ gettingMissingAlternativeMedia }}
  745. </p>
  746. <p>
  747. Getting all alternative media per track:
  748. {{ gettingAllAlternativeMediaPerTrack }}
  749. </p>
  750. <p>
  751. Got all alternative media per track:
  752. {{ gotAllAlternativeMediaPerTrack }}
  753. </p>
  754. <hr />
  755. <p>
  756. Alternative media loaded:
  757. {{ Object.keys(alternativeMediaMap).length }}
  758. </p>
  759. <p>
  760. Alternative media that failed to load:
  761. {{
  762. Object.keys(alternativeMediaFailedMap)
  763. .length
  764. }}
  765. </p>
  766. <hr />
  767. <p>
  768. Replacing all Spotify songs:
  769. {{ replacingAllSpotifySongs }}
  770. </p>
  771. </div>
  772. </div>
  773. <br />
  774. <hr />
  775. <div
  776. class="convert-table convert-song-by-track"
  777. v-if="currentConvertType === 'track'"
  778. >
  779. <h4>Spotify songs</h4>
  780. <h4>Alternative songs</h4>
  781. <template
  782. v-for="spotifySong in filteredSpotifySongs"
  783. :key="spotifySong.mediaSource"
  784. >
  785. <div
  786. class="convert-table-cell convert-table-cell-left"
  787. >
  788. <song-item :song="spotifySong">
  789. <template #leftIcon>
  790. <a
  791. :href="`https://open.spotify.com/track/${
  792. spotifySong.mediaSource.split(
  793. ':'
  794. )[1]
  795. }`"
  796. target="_blank"
  797. >
  798. <div
  799. class="spotify-icon left-icon"
  800. ></div>
  801. </a>
  802. </template>
  803. </song-item>
  804. <p>
  805. Media source: {{ spotifySong.mediaSource }}
  806. </p>
  807. <p v-if="loadedSpotifyTracks">
  808. ISRC:
  809. {{
  810. spotifyTracks[spotifySong.mediaSource]
  811. .externalIds.isrc
  812. }}
  813. </p>
  814. </div>
  815. <div
  816. class="convert-table-cell convert-table-cell-right"
  817. >
  818. <p
  819. v-if="
  820. !alternativeMediaPerTrack[
  821. spotifySong.mediaSource
  822. ]
  823. "
  824. >
  825. Alternatives not loaded yet
  826. </p>
  827. <template v-else>
  828. <div class="alternative-media-items">
  829. <div
  830. class="alternative-media-item"
  831. :class="{
  832. 'selected-alternative-song':
  833. preferredAlternativeSongPerTrack[
  834. spotifySong.mediaSource
  835. ] ===
  836. alternativeMediaSource &&
  837. missingMediaSources.length ===
  838. 0
  839. }"
  840. v-for="alternativeMediaSource in alternativeMediaPerTrack[
  841. spotifySong.mediaSource
  842. ].mediaSources"
  843. :key="
  844. spotifySong.mediaSource +
  845. alternativeMediaSource
  846. "
  847. >
  848. <p
  849. v-if="
  850. alternativeMediaFailedMap[
  851. alternativeMediaSource
  852. ]
  853. "
  854. >
  855. Song
  856. {{ alternativeMediaSource }}
  857. failed to load
  858. </p>
  859. <p
  860. v-else-if="
  861. !alternativeMediaMap[
  862. alternativeMediaSource
  863. ]
  864. "
  865. >
  866. Song
  867. {{ alternativeMediaSource }}
  868. hasn't been loaded yet
  869. </p>
  870. <template v-else>
  871. <div>
  872. <song-item
  873. :song="
  874. alternativeMediaMap[
  875. alternativeMediaSource
  876. ]
  877. "
  878. >
  879. <template #leftIcon>
  880. <a
  881. v-if="
  882. alternativeMediaSource.split(
  883. ':'
  884. )[0] ===
  885. 'youtube'
  886. "
  887. :href="`https://youtu.be/${
  888. alternativeMediaSource.split(
  889. ':'
  890. )[1]
  891. }`"
  892. target="_blank"
  893. >
  894. <div
  895. class="youtube-icon left-icon"
  896. ></div>
  897. </a>
  898. <a
  899. v-if="
  900. alternativeMediaSource.split(
  901. ':'
  902. )[0] ===
  903. 'soundcloud'
  904. "
  905. target="_blank"
  906. >
  907. <div
  908. class="soundcloud-icon left-icon"
  909. ></div>
  910. </a>
  911. </template>
  912. </song-item>
  913. <quick-confirm
  914. v-if="
  915. showReplaceButtonPerAlternative
  916. "
  917. placement="top"
  918. @confirm="
  919. replaceSpotifySong(
  920. spotifySong.mediaSource,
  921. alternativeMediaSource
  922. )
  923. "
  924. >
  925. <button
  926. class="button is-primary is-fullwidth"
  927. >
  928. Replace Spotify song
  929. with this song
  930. </button>
  931. </quick-confirm>
  932. </div>
  933. <ul v-if="showExtra">
  934. <li
  935. v-for="origin in alternativeMediaPerTrack[
  936. spotifySong
  937. .mediaSource
  938. ].mediaSourcesOrigins[
  939. alternativeMediaSource
  940. ]"
  941. :key="
  942. spotifySong.mediaSource +
  943. alternativeMediaSource +
  944. origin
  945. "
  946. >
  947. <hr />
  948. <ul>
  949. <li
  950. v-for="originItem in origin"
  951. :key="
  952. spotifySong.mediaSource +
  953. alternativeMediaSource +
  954. origin +
  955. originItem
  956. "
  957. >
  958. +
  959. {{ originItem }}
  960. </li>
  961. </ul>
  962. </li>
  963. </ul>
  964. </template>
  965. </div>
  966. </div>
  967. </template>
  968. </div>
  969. </template>
  970. </div>
  971. <div
  972. class="convert-table convert-song-by-album"
  973. v-if="currentConvertType === 'album'"
  974. >
  975. <h4>Spotify albums</h4>
  976. <h4>Alternative songs</h4>
  977. <template
  978. v-for="spotifyAlbum in filteredSpotifyAlbums"
  979. :key="spotifyAlbum"
  980. >
  981. <div
  982. class="convert-table-cell convert-table-cell-left"
  983. >
  984. <p>Album ID: {{ spotifyAlbum.albumId }}</p>
  985. <p v-if="loadingSpotifyAlbums">
  986. Loading album info...
  987. </p>
  988. <p
  989. v-else-if="
  990. loadedSpotifyAlbums &&
  991. !spotifyAlbum.rawData
  992. "
  993. >
  994. Failed to load album info...
  995. </p>
  996. <template v-else-if="loadedSpotifyAlbums">
  997. <p>Name: {{ spotifyAlbum.rawData.name }}</p>
  998. <p>
  999. Label: {{ spotifyAlbum.rawData.label }}
  1000. </p>
  1001. <p>
  1002. Popularity:
  1003. {{ spotifyAlbum.rawData.popularity }}
  1004. </p>
  1005. <p>
  1006. Release date:
  1007. {{ spotifyAlbum.rawData.release_date }}
  1008. </p>
  1009. <p>
  1010. Artists:
  1011. {{
  1012. spotifyAlbum.rawData.artists
  1013. .map(artist => artist.name)
  1014. .join(", ")
  1015. }}
  1016. </p>
  1017. <p>
  1018. UPC:
  1019. {{
  1020. spotifyAlbum.rawData.external_ids
  1021. .upc
  1022. }}
  1023. </p>
  1024. </template>
  1025. <song-item
  1026. v-for="spotifyMediaSource in spotifyAlbum.songs"
  1027. :key="
  1028. spotifyAlbum.albumId +
  1029. spotifyMediaSource
  1030. "
  1031. :song="{
  1032. title: spotifyTracks[spotifyMediaSource]
  1033. .name,
  1034. artists:
  1035. spotifyTracks[spotifyMediaSource]
  1036. .artists,
  1037. duration:
  1038. spotifyTracks[spotifyMediaSource]
  1039. .duration,
  1040. thumbnail:
  1041. spotifyTracks[spotifyMediaSource]
  1042. .albumImageUrl
  1043. }"
  1044. >
  1045. <template #leftIcon>
  1046. <a
  1047. :href="`https://open.spotify.com/track/${
  1048. spotifyMediaSource.split(':')[1]
  1049. }`"
  1050. target="_blank"
  1051. >
  1052. <div
  1053. class="spotify-icon left-icon"
  1054. ></div>
  1055. </a>
  1056. </template>
  1057. </song-item>
  1058. </div>
  1059. <div
  1060. class="convert-table-cell convert-table-cell-right"
  1061. >
  1062. <p>Test</p>
  1063. </div>
  1064. </template>
  1065. </div>
  1066. </template>
  1067. </template>
  1068. </modal>
  1069. </div>
  1070. </template>
  1071. <style lang="less" scoped>
  1072. :deep(.song-item) {
  1073. .left-icon {
  1074. cursor: pointer;
  1075. }
  1076. }
  1077. .tracks {
  1078. display: flex;
  1079. flex-direction: column;
  1080. .track-row {
  1081. .left,
  1082. .right {
  1083. padding: 8px;
  1084. width: 50%;
  1085. box-shadow: inset 0px 0px 1px white;
  1086. display: flex;
  1087. flex-direction: column;
  1088. row-gap: 8px;
  1089. }
  1090. }
  1091. }
  1092. .alternative-media-items {
  1093. display: flex;
  1094. flex-direction: column;
  1095. row-gap: 12px;
  1096. }
  1097. .convert-table {
  1098. display: grid;
  1099. grid-template-columns: 50% 50%;
  1100. gap: 1px;
  1101. .convert-table-cell {
  1102. outline: 1px solid white;
  1103. padding: 4px;
  1104. }
  1105. }
  1106. .selected-alternative-song {
  1107. // outline: 4px solid red;
  1108. border-left: 12px solid var(--primary-color);
  1109. padding: 4px;
  1110. }
  1111. .buttons-options-info-row {
  1112. display: grid;
  1113. grid-template-columns: 33.3% 33.3% 33.3%;
  1114. gap: 8px;
  1115. .buttons,
  1116. .options {
  1117. display: flex;
  1118. flex-direction: column;
  1119. row-gap: 8px;
  1120. > .control {
  1121. margin-bottom: 0 !important;
  1122. }
  1123. }
  1124. }
  1125. // .column-headers {
  1126. // display: flex;
  1127. // flex-direction: row;
  1128. // .column-header {
  1129. // flex: 1;
  1130. // }
  1131. // }
  1132. // .artists {
  1133. // display: flex;
  1134. // flex-direction: column;
  1135. // .artist-item {
  1136. // display: flex;
  1137. // flex-direction: column;
  1138. // row-gap: 8px;
  1139. // box-shadow: inset 0px 0px 1px white;
  1140. // width: 50%;
  1141. // position: relative;
  1142. // .spotify-section {
  1143. // display: flex;
  1144. // flex-direction: column;
  1145. // row-gap: 8px;
  1146. // padding: 8px 12px;
  1147. // .spotify-songs {
  1148. // display: flex;
  1149. // flex-direction: column;
  1150. // row-gap: 4px;
  1151. // }
  1152. // }
  1153. // .soundcloud-section {
  1154. // position: absolute;
  1155. // left: 100%;
  1156. // top: 0;
  1157. // width: 100%;
  1158. // height: 100%;
  1159. // overflow: hidden;
  1160. // box-shadow: inset 0px 0px 1px white;
  1161. // padding: 8px 12px;
  1162. // }
  1163. // }
  1164. // }
  1165. </style>