ConvertSpotifySongs.vue 52 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161
  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 loadingSpotifyArtists = ref(false);
  39. const loadedSpotifyArtists = ref(false);
  40. const gettingAllAlternativeMediaPerTrack = ref(false);
  41. const gotAllAlternativeMediaPerTrack = ref(false);
  42. const alternativeMediaPerTrack = reactive({});
  43. const gettingAllAlternativeAlbums = ref(false);
  44. const gotAllAlternativeAlbums = ref(false);
  45. const alternativeAlbumsPerAlbum = reactive({});
  46. const gettingAllAlternativeArtists = ref(false);
  47. const gotAllAlternativeArtists = ref(false);
  48. const alternativeArtistsPerArtist = reactive({});
  49. const alternativeMediaMap = reactive({});
  50. const alternativeMediaFailedMap = reactive({});
  51. const gettingMissingAlternativeMedia = ref(false);
  52. const replacingAllSpotifySongs = ref(false);
  53. const currentConvertType = ref<"track" | "album" | "artist">("track");
  54. const showReplaceButtonPerAlternative = ref(true);
  55. const hideSpotifySongsWithNoAlternativesFound = ref(false);
  56. const preferredAlternativeSongMode = ref<
  57. "FIRST" | "LYRICS" | "TOPIC" | "LYRICS_TOPIC" | "TOPIC_LYRICS"
  58. >("FIRST");
  59. // const singleMode = ref(false);
  60. const showExtra = ref(false);
  61. const collectAlternativeMediaSourcesOrigins = ref(false);
  62. const minimumSongsPerAlbum = ref(2);
  63. const minimumSongsPerArtist = ref(2);
  64. const sortAlbumMode = ref<
  65. "SONG_COUNT_ASC" | "SONG_COUNT_DESC" | "NAME_DESC" | "NAME_ASC"
  66. >("SONG_COUNT_ASC");
  67. const sortArtistMode = ref<
  68. "SONG_COUNT_ASC" | "SONG_COUNT_DESC" | "NAME_DESC" | "NAME_ASC"
  69. >("SONG_COUNT_ASC");
  70. const showDontConvertButton = ref(true);
  71. const replaceSongUrlMap = reactive({});
  72. const showReplacementInputs = ref(false);
  73. const youtubeVideoUrlRegex =
  74. /^(https?:\/\/)?(www\.)?(m\.)?(music\.)?(youtube\.com|youtu\.be)\/(watch\?v=)?(?<youtubeId>[\w-]{11})((&([A-Za-z0-9]+)?)*)?$/;
  75. const youtubeVideoIdRegex = /^([\w-]{11})$/;
  76. const youtubePlaylistUrlRegex = /[\\?&]list=([^&#]*)/;
  77. const youtubeChannelUrlRegex = /channel\/([A-Za-z0-9]+)\/?/;
  78. const filteredSpotifySongs = computed(() =>
  79. hideSpotifySongsWithNoAlternativesFound.value
  80. ? spotifySongs.value.filter(
  81. spotifySong =>
  82. (!gettingAllAlternativeMediaPerTrack.value &&
  83. !gotAllAlternativeMediaPerTrack.value) ||
  84. (alternativeMediaPerTrack[spotifySong.mediaSource] &&
  85. alternativeMediaPerTrack[spotifySong.mediaSource]
  86. .mediaSources.length > 0)
  87. )
  88. : spotifySongs.value
  89. );
  90. const filteredSpotifyArtists = computed(() => {
  91. let artists = Object.values(spotifyArtists);
  92. artists = artists.filter(
  93. artist => artist.songs.length >= minimumSongsPerArtist.value
  94. );
  95. let sortFn = null;
  96. if (sortArtistMode.value === "SONG_COUNT_ASC")
  97. sortFn = (artistA, artistB) =>
  98. artistA.songs.length - artistB.songs.length;
  99. else if (sortArtistMode.value === "SONG_COUNT_DESC")
  100. sortFn = (artistA, artistB) =>
  101. artistB.songs.length - artistA.songs.length;
  102. else if (loadedSpotifyArtists.value && sortArtistMode.value === "NAME_ASC")
  103. sortFn = (artistA, artistB) => {
  104. const nameA = artistA.rawData?.name?.toLowerCase();
  105. const nameB = artistB.rawData?.name?.toLowerCase();
  106. if (nameA === nameB) return 0;
  107. if (nameA < nameB) return -1;
  108. if (nameA > nameB) return 1;
  109. };
  110. else if (loadedSpotifyArtists.value && sortArtistMode.value === "NAME_DESC")
  111. sortFn = (artistA, artistB) => {
  112. const nameA = artistA.rawData?.name?.toLowerCase();
  113. const nameB = artistB.rawData?.name?.toLowerCase();
  114. if (nameA === nameB) return 0;
  115. if (nameA > nameB) return -1;
  116. if (nameA < nameB) return 1;
  117. };
  118. if (sortFn) artists = artists.sort(sortFn);
  119. return artists;
  120. });
  121. const filteredSpotifyAlbums = computed(() => {
  122. let albums = Object.values(spotifyAlbums);
  123. albums = albums.filter(
  124. album => album.songs.length >= minimumSongsPerAlbum.value
  125. );
  126. let sortFn = null;
  127. if (sortAlbumMode.value === "SONG_COUNT_ASC")
  128. sortFn = (albumA, albumB) => albumA.songs.length - albumB.songs.length;
  129. else if (sortAlbumMode.value === "SONG_COUNT_DESC")
  130. sortFn = (albumA, albumB) => albumB.songs.length - albumA.songs.length;
  131. else if (loadedSpotifyAlbums.value && sortAlbumMode.value === "NAME_ASC")
  132. sortFn = (albumA, albumB) => {
  133. const nameA = albumA.rawData?.name?.toLowerCase();
  134. const nameB = albumB.rawData?.name?.toLowerCase();
  135. if (nameA === nameB) return 0;
  136. if (nameA < nameB) return -1;
  137. if (nameA > nameB) return 1;
  138. };
  139. else if (loadedSpotifyAlbums.value && sortAlbumMode.value === "NAME_DESC")
  140. sortFn = (albumA, albumB) => {
  141. const nameA = albumA.rawData?.name?.toLowerCase();
  142. const nameB = albumB.rawData?.name?.toLowerCase();
  143. if (nameA === nameB) return 0;
  144. if (nameA > nameB) return -1;
  145. if (nameA < nameB) return 1;
  146. };
  147. if (sortFn) albums = albums.sort(sortFn);
  148. return albums;
  149. });
  150. const missingMediaSources = computed(() => {
  151. const missingMediaSources = [];
  152. Object.values(alternativeMediaPerTrack).forEach(({ mediaSources }) => {
  153. mediaSources.forEach(mediaSource => {
  154. if (
  155. !alternativeMediaMap[mediaSource] &&
  156. !alternativeMediaFailedMap[mediaSource] &&
  157. missingMediaSources.indexOf(mediaSource) === -1
  158. )
  159. missingMediaSources.push(mediaSource);
  160. });
  161. });
  162. return missingMediaSources;
  163. });
  164. const preferredAlternativeSongPerTrack = computed(() => {
  165. const returnObject = {};
  166. Object.entries(alternativeMediaPerTrack).forEach(
  167. ([spotifyMediaSource, { mediaSources }]) => {
  168. returnObject[spotifyMediaSource] = null;
  169. if (mediaSources.length === 0) return;
  170. let sortFn = (mediaSourceA, mediaSourceB) => {
  171. if (preferredAlternativeSongMode.value === "FIRST") return 0;
  172. const aHasLyrics =
  173. alternativeMediaMap[mediaSourceA].title
  174. .toLowerCase()
  175. .indexOf("lyric") !== -1;
  176. const aHasTopic =
  177. alternativeMediaMap[mediaSourceA].artists[0]
  178. .toLowerCase()
  179. .indexOf("topic") !== -1;
  180. const bHasLyrics =
  181. alternativeMediaMap[mediaSourceB].title
  182. .toLowerCase()
  183. .indexOf("lyric") !== -1;
  184. const bHasTopic =
  185. alternativeMediaMap[mediaSourceB].artists[0]
  186. .toLowerCase()
  187. .indexOf("topic") !== -1;
  188. if (preferredAlternativeSongMode.value === "LYRICS") {
  189. if (aHasLyrics && bHasLyrics) return 0;
  190. if (aHasLyrics && !bHasLyrics) return -1;
  191. if (!aHasLyrics && bHasLyrics) return 1;
  192. return 0;
  193. }
  194. if (preferredAlternativeSongMode.value === "TOPIC") {
  195. if (aHasTopic && bHasTopic) return 0;
  196. if (aHasTopic && !bHasTopic) return -1;
  197. if (!aHasTopic && bHasTopic) return 1;
  198. return 0;
  199. }
  200. if (preferredAlternativeSongMode.value === "LYRICS_TOPIC") {
  201. if (aHasLyrics && bHasLyrics) return 0;
  202. if (aHasLyrics && !bHasLyrics) return -1;
  203. if (!aHasLyrics && bHasLyrics) return 1;
  204. if (aHasTopic && bHasTopic) return 0;
  205. if (aHasTopic && !bHasTopic) return -1;
  206. if (!aHasTopic && bHasTopic) return 1;
  207. return 0;
  208. }
  209. if (preferredAlternativeSongMode.value === "TOPIC_LYRICS") {
  210. if (aHasTopic && bHasTopic) return 0;
  211. if (aHasTopic && !bHasTopic) return -1;
  212. if (!aHasTopic && bHasTopic) return 1;
  213. if (aHasLyrics && bHasLyrics) return 0;
  214. if (aHasLyrics && !bHasLyrics) return -1;
  215. if (!aHasLyrics && bHasLyrics) return 1;
  216. return 0;
  217. }
  218. };
  219. if (
  220. mediaSources.length === 1 ||
  221. preferredAlternativeSongMode.value === "FIRST"
  222. )
  223. sortFn = () => 0;
  224. else if (preferredAlternativeSongMode.value === "LYRICS")
  225. sortFn = mediaSourceA => {
  226. if (!alternativeMediaMap[mediaSourceA]) return 0;
  227. if (
  228. alternativeMediaMap[mediaSourceA].title
  229. .toLowerCase()
  230. .indexOf("lyric") !== -1
  231. )
  232. return -1;
  233. return 1;
  234. };
  235. else if (preferredAlternativeSongMode.value === "TOPIC")
  236. sortFn = mediaSourceA => {
  237. if (!alternativeMediaMap[mediaSourceA]) return 0;
  238. if (
  239. alternativeMediaMap[mediaSourceA].artists[0]
  240. .toLowerCase()
  241. .indexOf("topic") !== -1
  242. )
  243. return -1;
  244. return 1;
  245. };
  246. const [firstMediaSource] = mediaSources
  247. .slice()
  248. .filter(mediaSource => !!alternativeMediaMap[mediaSource])
  249. .sort(sortFn);
  250. returnObject[spotifyMediaSource] = firstMediaSource;
  251. }
  252. );
  253. return returnObject;
  254. });
  255. const replaceAllSpotifySongs = async () => {
  256. if (replacingAllSpotifySongs.value) return;
  257. replacingAllSpotifySongs.value = true;
  258. const replaceArray = [];
  259. spotifySongs.value.forEach(spotifySong => {
  260. const spotifyMediaSource = spotifySong.mediaSource;
  261. const replacementMediaSource =
  262. preferredAlternativeSongPerTrack.value[spotifyMediaSource];
  263. if (!spotifyMediaSource || !replacementMediaSource) return;
  264. replaceArray.push([spotifyMediaSource, replacementMediaSource]);
  265. });
  266. const promises = replaceArray.map(
  267. ([spotifyMediaSource, replacementMediaSource]) =>
  268. new Promise<void>(resolve => {
  269. socket.dispatch(
  270. "playlists.replaceSongInPlaylist",
  271. spotifyMediaSource,
  272. replacementMediaSource,
  273. props.playlistId,
  274. res => {
  275. console.log(
  276. "playlists.replaceSongInPlaylist response",
  277. res
  278. );
  279. resolve();
  280. }
  281. );
  282. })
  283. );
  284. Promise.allSettled(promises).finally(() => {
  285. replacingAllSpotifySongs.value = false;
  286. });
  287. };
  288. const replaceSpotifySong = (oldMediaSource, newMediaSource) => {
  289. socket.dispatch(
  290. "playlists.replaceSongInPlaylist",
  291. oldMediaSource,
  292. newMediaSource,
  293. props.playlistId,
  294. res => {
  295. console.log("playlists.replaceSongInPlaylist response", res);
  296. }
  297. );
  298. };
  299. const openReplaceAlbumModal = (spotifyAlbumId, youtubePlaylistId) => {
  300. console.log(spotifyAlbumId, youtubePlaylistId);
  301. if (
  302. !spotifyAlbums[spotifyAlbumId] ||
  303. !spotifyAlbums[spotifyAlbumId].rawData
  304. )
  305. return new Toast("Album hasn't loaded yet.");
  306. openModal({
  307. modal: "replaceSpotifySongs",
  308. props: {
  309. playlistId: props.playlistId,
  310. youtubePlaylistId,
  311. spotifyTracks: spotifyAlbums[spotifyAlbumId].songs.map(
  312. mediaSource => spotifyTracks[mediaSource]
  313. )
  314. }
  315. });
  316. };
  317. const openReplaceAlbumModalFromUrl = spotifyAlbumId => {
  318. const replacementUrl = replaceSongUrlMap[`album:${spotifyAlbumId}`];
  319. console.log(spotifyAlbumId, replacementUrl);
  320. let youtubePlaylistId = null;
  321. const youtubePlaylistUrlRegexMatches =
  322. youtubePlaylistUrlRegex.exec(replacementUrl);
  323. if (youtubePlaylistUrlRegexMatches)
  324. youtubePlaylistId = youtubePlaylistUrlRegexMatches[0];
  325. console.log("Open modal for ", youtubePlaylistId);
  326. openReplaceAlbumModal(spotifyAlbumId, youtubePlaylistId);
  327. };
  328. const openReplaceArtistModal = (spotifyArtistId, youtubeChannelUrl) => {
  329. console.log(spotifyArtistId, youtubeChannelUrl);
  330. if (
  331. !spotifyArtists[spotifyArtistId] ||
  332. !spotifyArtists[spotifyArtistId].rawData
  333. )
  334. return new Toast("Artist hasn't loaded yet.");
  335. openModal({
  336. modal: "replaceSpotifySongs",
  337. props: {
  338. playlistId: props.playlistId,
  339. youtubeChannelUrl,
  340. spotifyTracks: spotifyArtists[spotifyArtistId].songs.map(
  341. mediaSource => spotifyTracks[mediaSource]
  342. )
  343. }
  344. });
  345. };
  346. const openReplaceArtistModalFromUrl = spotifyArtistId => {
  347. const replacementUrl = replaceSongUrlMap[`artist:${spotifyArtistId}`];
  348. console.log(spotifyArtistId, replacementUrl);
  349. // let youtubeChannelId = null;
  350. // const youtubeChannelUrlRegexMatches =
  351. // youtubeChannelUrlRegex.exec(replacementUrl);
  352. // if (youtubeChannelUrlRegexMatches)
  353. // youtubeChannelId = youtubeChannelUrlRegexMatches[0];
  354. console.log("Open modal for ", replacementUrl);
  355. openReplaceArtistModal(spotifyArtistId, replacementUrl);
  356. };
  357. const replaceSongFromUrl = spotifyMediaSource => {
  358. const replacementUrl = replaceSongUrlMap[spotifyMediaSource];
  359. console.log(spotifyMediaSource, replacementUrl);
  360. let newMediaSource = null;
  361. const youtubeVideoUrlRegexMatches =
  362. youtubeVideoUrlRegex.exec(replacementUrl);
  363. console.log(youtubeVideoUrlRegexMatches);
  364. const youtubeVideoIdRegexMatches = youtubeVideoIdRegex.exec(replacementUrl);
  365. console.log(youtubeVideoIdRegexMatches);
  366. if (youtubeVideoUrlRegexMatches)
  367. newMediaSource = `youtube:${youtubeVideoUrlRegexMatches.groups.youtubeId}`;
  368. if (youtubeVideoIdRegexMatches)
  369. newMediaSource = `youtube:${youtubeVideoIdRegexMatches[0]}`;
  370. if (!newMediaSource) return new Toast("Invalid URL/identifier specified.");
  371. replaceSpotifySong(spotifyMediaSource, newMediaSource);
  372. };
  373. const getMissingAlternativeMedia = () => {
  374. if (gettingMissingAlternativeMedia.value) return;
  375. gettingMissingAlternativeMedia.value = true;
  376. const _missingMediaSources = missingMediaSources.value;
  377. console.log("Getting missing", _missingMediaSources);
  378. socket.dispatch(
  379. "media.getMediaFromMediaSources",
  380. _missingMediaSources,
  381. res => {
  382. if (res.status === "success") {
  383. const { songMap } = res.data;
  384. _missingMediaSources.forEach(missingMediaSource => {
  385. if (songMap[missingMediaSource])
  386. alternativeMediaMap[missingMediaSource] =
  387. songMap[missingMediaSource];
  388. else alternativeMediaFailedMap[missingMediaSource] = true;
  389. });
  390. }
  391. gettingMissingAlternativeMedia.value = false;
  392. }
  393. );
  394. };
  395. const getAlternativeArtists = () => {
  396. if (gettingAllAlternativeArtists.value || gotAllAlternativeArtists.value)
  397. return;
  398. gettingAllAlternativeArtists.value = true;
  399. const artistIds = filteredSpotifyArtists.value.map(
  400. artist => artist.artistId
  401. );
  402. socket.dispatch(
  403. "apis.getAlternativeArtistSourcesForArtists",
  404. artistIds,
  405. collectAlternativeMediaSourcesOrigins.value,
  406. {
  407. cb: res => {
  408. console.log(
  409. "apis.getAlternativeArtistSourcesForArtists response",
  410. res
  411. );
  412. },
  413. onProgress: data => {
  414. console.log(
  415. "apis.getAlternativeArtistSourcesForArtists onProgress",
  416. data
  417. );
  418. if (data.status === "working") {
  419. if (data.data.status === "success") {
  420. const { artistId, result } = data.data;
  421. if (!spotifyArtists[artistId]) return;
  422. alternativeArtistsPerArtist[artistId] = {
  423. youtubeChannelIds: result
  424. };
  425. }
  426. } else if (data.status === "finished") {
  427. gotAllAlternativeArtists.value = true;
  428. gettingAllAlternativeArtists.value = false;
  429. }
  430. }
  431. }
  432. );
  433. };
  434. const getAlternativeAlbums = () => {
  435. if (gettingAllAlternativeAlbums.value || gotAllAlternativeAlbums.value)
  436. return;
  437. gettingAllAlternativeAlbums.value = true;
  438. const albumIds = filteredSpotifyAlbums.value.map(album => album.albumId);
  439. socket.dispatch(
  440. "apis.getAlternativeAlbumSourcesForAlbums",
  441. albumIds,
  442. collectAlternativeMediaSourcesOrigins.value,
  443. {
  444. cb: res => {
  445. console.log(
  446. "apis.getAlternativeAlbumSourcesForAlbums response",
  447. res
  448. );
  449. },
  450. onProgress: data => {
  451. console.log(
  452. "apis.getAlternativeAlbumSourcesForAlbums onProgress",
  453. data
  454. );
  455. if (data.status === "working") {
  456. if (data.data.status === "success") {
  457. const { albumId, result } = data.data;
  458. if (!spotifyAlbums[albumId]) return;
  459. alternativeAlbumsPerAlbum[albumId] = {
  460. youtubePlaylistIds: result
  461. };
  462. }
  463. } else if (data.status === "finished") {
  464. gotAllAlternativeAlbums.value = true;
  465. gettingAllAlternativeAlbums.value = false;
  466. }
  467. }
  468. }
  469. );
  470. };
  471. const getAlternativeMedia = () => {
  472. if (
  473. gettingAllAlternativeMediaPerTrack.value ||
  474. gotAllAlternativeMediaPerTrack.value
  475. )
  476. return;
  477. gettingAllAlternativeMediaPerTrack.value = true;
  478. const mediaSources = spotifySongs.value.map(song => song.mediaSource);
  479. socket.dispatch(
  480. "apis.getAlternativeMediaSourcesForTracks",
  481. mediaSources,
  482. collectAlternativeMediaSourcesOrigins.value,
  483. {
  484. cb: res => {
  485. console.log(
  486. "apis.getAlternativeMediaSourcesForTracks response",
  487. res
  488. );
  489. },
  490. onProgress: data => {
  491. console.log(
  492. "apis.getAlternativeMediaSourcesForTracks onProgress",
  493. data
  494. );
  495. if (data.status === "working") {
  496. if (data.data.status === "success") {
  497. const { mediaSource, result } = data.data;
  498. if (!spotifyTracks[mediaSource]) return;
  499. alternativeMediaPerTrack[mediaSource] = result;
  500. }
  501. } else if (data.status === "finished") {
  502. gotAllAlternativeMediaPerTrack.value = true;
  503. gettingAllAlternativeMediaPerTrack.value = false;
  504. getMissingAlternativeMedia();
  505. }
  506. }
  507. }
  508. );
  509. };
  510. const loadSpotifyArtists = () =>
  511. new Promise<void>(resolve => {
  512. console.debug(TAG, "Loading Spotify artists");
  513. loadingSpotifyArtists.value = true;
  514. const artistIds = filteredSpotifyArtists.value.map(
  515. artist => artist.artistId
  516. );
  517. socket.dispatch("spotify.getArtistsFromIds", artistIds, res => {
  518. console.debug(TAG, "Get artists response", res);
  519. if (res.status !== "success") {
  520. new Toast(res.message);
  521. closeCurrentModal();
  522. return;
  523. }
  524. const { artists } = res.data;
  525. artists.forEach(artist => {
  526. spotifyArtists[artist.artistId].rawData = artist.rawData;
  527. });
  528. console.debug(TAG, "Loaded Spotify artists");
  529. loadedSpotifyArtists.value = true;
  530. loadingSpotifyArtists.value = false;
  531. resolve();
  532. });
  533. });
  534. const loadSpotifyAlbums = () =>
  535. new Promise<void>(resolve => {
  536. console.debug(TAG, "Loading Spotify albums");
  537. loadingSpotifyAlbums.value = true;
  538. const albumIds = filteredSpotifyAlbums.value.map(
  539. album => album.albumId
  540. );
  541. socket.dispatch("spotify.getAlbumsFromIds", albumIds, res => {
  542. console.debug(TAG, "Get albums response", res);
  543. if (res.status !== "success") {
  544. new Toast(res.message);
  545. closeCurrentModal();
  546. return;
  547. }
  548. const { albums } = res.data;
  549. albums.forEach(album => {
  550. spotifyAlbums[album.albumId].rawData = album.rawData;
  551. });
  552. console.debug(TAG, "Loaded Spotify albums");
  553. loadedSpotifyAlbums.value = true;
  554. loadingSpotifyAlbums.value = false;
  555. resolve();
  556. });
  557. });
  558. const loadSpotifyTracks = () =>
  559. new Promise<void>(resolve => {
  560. console.debug(TAG, "Loading Spotify tracks");
  561. loadingSpotifyTracks.value = true;
  562. const mediaSources = spotifySongs.value.map(song => song.mediaSource);
  563. socket.dispatch(
  564. "spotify.getTracksFromMediaSources",
  565. mediaSources,
  566. res => {
  567. console.debug(TAG, "Get tracks response", res);
  568. if (res.status !== "success") {
  569. new Toast(res.message);
  570. closeCurrentModal();
  571. return;
  572. }
  573. const { tracks } = res.data;
  574. Object.entries(tracks).forEach(([mediaSource, track]) => {
  575. spotifyTracks[mediaSource] = track;
  576. const { albumId, albumImageUrl, artistIds, artists } =
  577. track;
  578. if (albumId) {
  579. if (!spotifyAlbums[albumId])
  580. spotifyAlbums[albumId] = {
  581. albumId,
  582. albumImageUrl,
  583. songs: []
  584. };
  585. spotifyAlbums[albumId].songs.push(mediaSource);
  586. }
  587. artistIds.forEach((artistId, artistIndex) => {
  588. if (!spotifyArtists[artistId]) {
  589. spotifyArtists[artistId] = {
  590. artistId,
  591. name: artists[artistIndex],
  592. songs: [],
  593. expanded: false
  594. };
  595. }
  596. spotifyArtists[artistId].songs.push(mediaSource);
  597. });
  598. });
  599. console.debug(TAG, "Loaded Spotify tracks");
  600. loadedSpotifyTracks.value = true;
  601. loadingSpotifyTracks.value = false;
  602. resolve();
  603. }
  604. );
  605. });
  606. const loadPlaylist = () =>
  607. new Promise<void>(resolve => {
  608. console.debug(TAG, `Loading playlist ${props.playlistId}`);
  609. loadingPlaylist.value = true;
  610. socket.dispatch("playlists.getPlaylist", props.playlistId, res => {
  611. console.debug(TAG, "Get playlist response", res);
  612. if (res.status !== "success") {
  613. new Toast(res.message);
  614. closeCurrentModal();
  615. return;
  616. }
  617. playlist.value = res.data.playlist;
  618. spotifySongs.value = playlist.value.songs.filter(song =>
  619. song.mediaSource.startsWith("spotify:")
  620. );
  621. console.debug(TAG, `Loaded playlist ${props.playlistId}`);
  622. loadedPlaylist.value = true;
  623. loadingPlaylist.value = false;
  624. resolve();
  625. });
  626. });
  627. const removeAlternativeTrack = (spotifyMediaSource, alternativeMediaSource) => {
  628. alternativeMediaPerTrack[spotifyMediaSource].mediaSources =
  629. alternativeMediaPerTrack[spotifyMediaSource].mediaSources.filter(
  630. mediaSource => mediaSource !== alternativeMediaSource
  631. );
  632. };
  633. const removeSpotifyTrack = mediaSource => {
  634. const spotifyTrack = spotifyTracks[mediaSource];
  635. if (spotifyTrack) {
  636. delete spotifyTracks[mediaSource];
  637. spotifyTrack.artistIds.forEach(artistId => {
  638. const spotifyArtist = spotifyArtists[artistId];
  639. if (spotifyArtist) {
  640. if (spotifyArtist.songs.length === 1)
  641. delete spotifyArtists[artistId];
  642. else
  643. spotifyArtists[artistId].songs = spotifyArtists[
  644. artistId
  645. ].songs.filter(
  646. _mediaSource => _mediaSource !== mediaSource
  647. );
  648. }
  649. });
  650. const spotifyAlbum = spotifyAlbums[spotifyTrack.albumId];
  651. if (spotifyAlbum) {
  652. if (spotifyAlbum.songs.length === 1)
  653. delete spotifyAlbums[spotifyTrack.albumId];
  654. else
  655. spotifyAlbums[spotifyTrack.albumId].songs = spotifyAlbums[
  656. spotifyTrack.albumId
  657. ].songs.filter(_mediaSource => _mediaSource !== mediaSource);
  658. }
  659. }
  660. };
  661. const removeSpotifySong = mediaSource => {
  662. // remove song
  663. playlist.value.songs = playlist.value.songs.filter(
  664. song => song.mediaSource !== mediaSource
  665. );
  666. spotifySongs.value = spotifySongs.value.filter(
  667. song => song.mediaSource !== mediaSource
  668. );
  669. removeSpotifyTrack(mediaSource);
  670. delete alternativeMediaMap[mediaSource];
  671. delete alternativeMediaFailedMap[mediaSource];
  672. };
  673. onMounted(() => {
  674. console.debug(TAG, "On mounted start");
  675. loadPlaylist().then(loadSpotifyTracks);
  676. socket.on(
  677. "event:playlist.song.removed",
  678. res => {
  679. console.log("SONG REMOVED", res);
  680. if (
  681. loadedPlaylist.value &&
  682. playlist.value._id === res.data.playlistId
  683. ) {
  684. const { oldMediaSource } = res.data;
  685. removeSpotifySong(oldMediaSource);
  686. }
  687. },
  688. { modalUuid: props.modalUuid }
  689. );
  690. socket.on(
  691. "event:playlist.song.replaced",
  692. res => {
  693. console.log(
  694. "SONG REPLACED",
  695. res,
  696. playlist.value._id === res.data.playlistId
  697. );
  698. if (
  699. loadedPlaylist.value &&
  700. playlist.value._id === res.data.playlistId
  701. ) {
  702. const { oldMediaSource } = res.data;
  703. removeSpotifySong(oldMediaSource);
  704. }
  705. },
  706. { modalUuid: props.modalUuid }
  707. );
  708. console.debug(TAG, "On mounted end");
  709. });
  710. </script>
  711. <template>
  712. <div>
  713. <modal
  714. title="Convert Spotify Songs"
  715. class="convert-spotify-songs-modal"
  716. size="wide"
  717. @closed="closeCurrentModal()"
  718. >
  719. <template #body>
  720. <template v-if="loadedPlaylist && spotifySongs.length === 0">
  721. <h2>All Spotify songs have been converted</h2>
  722. <button
  723. class="button is-primary is-fullwidth"
  724. @click="closeCurrentModal()"
  725. >
  726. Close modal
  727. </button>
  728. </template>
  729. <template v-else>
  730. <div class="buttons-options-info-row">
  731. <div class="buttons">
  732. <quick-confirm
  733. v-if="
  734. gotAllAlternativeMediaPerTrack &&
  735. missingMediaSources.length === 0 &&
  736. !replacingAllSpotifySongs
  737. "
  738. placement="top"
  739. @confirm="replaceAllSpotifySongs()"
  740. >
  741. <button class="button is-primary is-fullwidth">
  742. Replace all available songs with provided
  743. prefer settings
  744. </button>
  745. </quick-confirm>
  746. <button
  747. v-if="
  748. loadedSpotifyTracks &&
  749. !gettingAllAlternativeMediaPerTrack &&
  750. !gotAllAlternativeMediaPerTrack &&
  751. currentConvertType === 'track'
  752. "
  753. class="button is-primary"
  754. @click="getAlternativeMedia()"
  755. >
  756. Get alternative media
  757. </button>
  758. <button
  759. v-if="
  760. currentConvertType === 'track' &&
  761. gotAllAlternativeMediaPerTrack &&
  762. !gettingMissingAlternativeMedia &&
  763. missingMediaSources.length > 0
  764. "
  765. class="button is-primary"
  766. @click="getMissingAlternativeMedia()"
  767. >
  768. Get missing alternative media
  769. </button>
  770. <button
  771. v-if="
  772. loadedSpotifyTracks &&
  773. !loadingSpotifyAlbums &&
  774. !loadedSpotifyAlbums &&
  775. currentConvertType === 'album'
  776. "
  777. class="button is-primary"
  778. @click="loadSpotifyAlbums()"
  779. >
  780. Get Spotify albums
  781. </button>
  782. <button
  783. v-if="
  784. loadedSpotifyTracks &&
  785. loadedSpotifyAlbums &&
  786. !gettingAllAlternativeAlbums &&
  787. !gotAllAlternativeAlbums &&
  788. currentConvertType === 'album'
  789. "
  790. class="button is-primary"
  791. @click="getAlternativeAlbums()"
  792. >
  793. Get alternative albums
  794. </button>
  795. <button
  796. v-if="
  797. loadedSpotifyTracks &&
  798. !loadingSpotifyArtists &&
  799. !loadedSpotifyArtists &&
  800. currentConvertType === 'artist'
  801. "
  802. class="button is-primary"
  803. @click="loadSpotifyArtists()"
  804. >
  805. Get Spotify artists
  806. </button>
  807. <button
  808. v-if="
  809. loadedSpotifyTracks &&
  810. loadedSpotifyArtists &&
  811. !gettingAllAlternativeArtists &&
  812. !gotAllAlternativeArtists &&
  813. currentConvertType === 'artist'
  814. "
  815. class="button is-primary"
  816. @click="getAlternativeArtists()"
  817. >
  818. Get alternative artists
  819. </button>
  820. </div>
  821. <div class="options">
  822. <p class="is-expanded checkbox-control">
  823. <label class="switch">
  824. <input
  825. type="checkbox"
  826. id="show-extra"
  827. v-model="showExtra"
  828. />
  829. <span class="slider round"></span>
  830. </label>
  831. <label for="show-extra">
  832. <p>Show extra info</p>
  833. </label>
  834. </p>
  835. <p class="is-expanded checkbox-control">
  836. <label class="switch">
  837. <input
  838. type="checkbox"
  839. id="collect-alternative-media-sources-origins"
  840. v-model="
  841. collectAlternativeMediaSourcesOrigins
  842. "
  843. />
  844. <span class="slider round"></span>
  845. </label>
  846. <label
  847. for="collect-alternative-media-sources-origins"
  848. >
  849. <p>
  850. Collect alternative media sources
  851. origins
  852. </p>
  853. </label>
  854. </p>
  855. <p class="is-expanded checkbox-control">
  856. <label class="switch">
  857. <input
  858. type="checkbox"
  859. id="show-replace-button-per-alternative"
  860. v-model="
  861. showReplaceButtonPerAlternative
  862. "
  863. />
  864. <span class="slider round"></span>
  865. </label>
  866. <label
  867. for="show-replace-button-per-alternative"
  868. >
  869. <p>Show replace button per alternative</p>
  870. </label>
  871. </p>
  872. <p class="is-expanded checkbox-control">
  873. <label class="switch">
  874. <input
  875. type="checkbox"
  876. id="showDontConvertButton"
  877. v-model="showDontConvertButton"
  878. />
  879. <span class="slider round"></span>
  880. </label>
  881. <label for="showDontConvertButton">
  882. <p>Show don't convert buttons</p>
  883. </label>
  884. </p>
  885. <p class="is-expanded checkbox-control">
  886. <label class="switch">
  887. <input
  888. type="checkbox"
  889. id="showReplacementInputs"
  890. v-model="showReplacementInputs"
  891. />
  892. <span class="slider round"></span>
  893. </label>
  894. <label for="showReplacementInputs">
  895. <p>Show replacement inputs</p>
  896. </label>
  897. </p>
  898. <p class="is-expanded checkbox-control">
  899. <label class="switch">
  900. <input
  901. type="checkbox"
  902. id="hide-spotify-songs-with-no-alternatives-found"
  903. v-model="
  904. hideSpotifySongsWithNoAlternativesFound
  905. "
  906. />
  907. <span class="slider round"></span>
  908. </label>
  909. <label
  910. for="hide-spotify-songs-with-no-alternatives-found"
  911. >
  912. <p>
  913. Hide Spotify songs with no alternatives
  914. found
  915. </p>
  916. </label>
  917. </p>
  918. <div class="control">
  919. <label class="label"
  920. >Get alternatives per</label
  921. >
  922. <p class="control is-expanded select">
  923. <select
  924. v-model="currentConvertType"
  925. :disabled="
  926. gettingAllAlternativeMediaPerTrack
  927. "
  928. >
  929. <option value="track">Track</option>
  930. <option value="artist">Artist</option>
  931. <option value="album">Album</option>
  932. </select>
  933. </p>
  934. </div>
  935. <div
  936. class="control"
  937. v-if="currentConvertType === 'track'"
  938. >
  939. <label class="label"
  940. >Preferred track mode</label
  941. >
  942. <p class="control is-expanded select">
  943. <select
  944. v-model="preferredAlternativeSongMode"
  945. :disabled="false"
  946. >
  947. <option value="FIRST">
  948. First song
  949. </option>
  950. <option value="LYRICS">
  951. First song with lyrics in title
  952. </option>
  953. <option value="TOPIC">
  954. First song from topic channel
  955. (YouTube only)
  956. </option>
  957. <option value="LYRICS_TOPIC">
  958. First song with lyrics in title, or
  959. from topic channel (YouTube only)
  960. </option>
  961. <option value="TOPIC_LYRICS">
  962. First song from topic channel
  963. (YouTube only), or with lyrics in
  964. title
  965. </option>
  966. </select>
  967. </p>
  968. </div>
  969. <div
  970. class="small-section"
  971. v-if="currentConvertType === 'album'"
  972. >
  973. <label class="label"
  974. >Minimum songs per album</label
  975. >
  976. <div class="control is-expanded">
  977. <input
  978. class="input"
  979. type="number"
  980. min="1"
  981. v-model="minimumSongsPerAlbum"
  982. />
  983. </div>
  984. </div>
  985. <div
  986. class="small-section"
  987. v-if="currentConvertType === 'artist'"
  988. >
  989. <label class="label"
  990. >Minimum songs per artist</label
  991. >
  992. <div class="control is-expanded">
  993. <input
  994. class="input"
  995. type="number"
  996. min="1"
  997. v-model="minimumSongsPerArtist"
  998. />
  999. </div>
  1000. </div>
  1001. <div
  1002. class="control"
  1003. v-if="currentConvertType === 'album'"
  1004. >
  1005. <label class="label">Sort album mode</label>
  1006. <p class="control is-expanded select">
  1007. <select v-model="sortAlbumMode">
  1008. <option value="SONG_COUNT_ASC">
  1009. Song count (ascending)
  1010. </option>
  1011. <option value="SONG_COUNT_DESC">
  1012. Song count (descending)
  1013. </option>
  1014. <option value="NAME_ASC">
  1015. Name (ascending)
  1016. </option>
  1017. <option value="NAME_DESC">
  1018. Name (descending)
  1019. </option>
  1020. </select>
  1021. </p>
  1022. </div>
  1023. <div
  1024. class="control"
  1025. v-if="currentConvertType === 'artist'"
  1026. >
  1027. <label class="label">Sort artist mode</label>
  1028. <p class="control is-expanded select">
  1029. <select v-model="sortArtistMode">
  1030. <option value="SONG_COUNT_ASC">
  1031. Song count (ascending)
  1032. </option>
  1033. <option value="SONG_COUNT_DESC">
  1034. Song count (descending)
  1035. </option>
  1036. <option value="NAME_ASC">
  1037. Name (ascending)
  1038. </option>
  1039. <option value="NAME_DESC">
  1040. Name (descending)
  1041. </option>
  1042. </select>
  1043. </p>
  1044. </div>
  1045. </div>
  1046. <div class="info">
  1047. <h6>Status</h6>
  1048. <p>Loading playlist: {{ loadingPlaylist }}</p>
  1049. <p>Loaded playlist: {{ loadedPlaylist }}</p>
  1050. <p>
  1051. Spotify songs in playlist:
  1052. {{ spotifySongs.length }}
  1053. </p>
  1054. <p>Converting by {{ currentConvertType }}</p>
  1055. <hr />
  1056. <p>
  1057. Loading Spotify tracks:
  1058. {{ loadingSpotifyTracks }}
  1059. </p>
  1060. <p>
  1061. Loaded Spotify tracks: {{ loadedSpotifyTracks }}
  1062. </p>
  1063. <p>
  1064. Spotify tracks loaded:
  1065. {{ Object.keys(spotifyTracks).length }}
  1066. </p>
  1067. <p>
  1068. Loading Spotify albums:
  1069. {{ loadingSpotifyAlbums }}
  1070. </p>
  1071. <p>
  1072. Loaded Spotify albums: {{ loadedSpotifyAlbums }}
  1073. </p>
  1074. <p>
  1075. Spotify albums:
  1076. {{ Object.keys(spotifyAlbums).length }}
  1077. </p>
  1078. <p>
  1079. Spotify artists:
  1080. {{ Object.keys(spotifyArtists).length }}
  1081. </p>
  1082. <p>
  1083. Getting missing alternative media:
  1084. {{ gettingMissingAlternativeMedia }}
  1085. </p>
  1086. <p>
  1087. Getting all alternative media per track:
  1088. {{ gettingAllAlternativeMediaPerTrack }}
  1089. </p>
  1090. <p>
  1091. Got all alternative media per track:
  1092. {{ gotAllAlternativeMediaPerTrack }}
  1093. </p>
  1094. <hr />
  1095. <p>
  1096. Alternative media loaded:
  1097. {{ Object.keys(alternativeMediaMap).length }}
  1098. </p>
  1099. <p>
  1100. Alternative media that failed to load:
  1101. {{
  1102. Object.keys(alternativeMediaFailedMap)
  1103. .length
  1104. }}
  1105. </p>
  1106. <hr />
  1107. <p>
  1108. Replacing all Spotify songs:
  1109. {{ replacingAllSpotifySongs }}
  1110. </p>
  1111. </div>
  1112. </div>
  1113. <br />
  1114. <hr />
  1115. <div
  1116. class="convert-table convert-song-by-track"
  1117. v-if="currentConvertType === 'track'"
  1118. >
  1119. <h4>Spotify songs</h4>
  1120. <h4>Alternative songs</h4>
  1121. <template
  1122. v-for="spotifySong in filteredSpotifySongs"
  1123. :key="spotifySong.mediaSource"
  1124. >
  1125. <div
  1126. class="convert-table-cell convert-table-cell-left"
  1127. >
  1128. <song-item :song="spotifySong">
  1129. <template #leftIcon>
  1130. <a
  1131. :href="`https://open.spotify.com/track/${
  1132. spotifySong.mediaSource.split(
  1133. ':'
  1134. )[1]
  1135. }`"
  1136. target="_blank"
  1137. >
  1138. <div
  1139. class="spotify-icon left-icon"
  1140. ></div>
  1141. </a>
  1142. </template>
  1143. </song-item>
  1144. <template v-if="showExtra">
  1145. <p>
  1146. Media source:
  1147. {{ spotifySong.mediaSource }}
  1148. </p>
  1149. <p v-if="loadedSpotifyTracks">
  1150. ISRC:
  1151. {{
  1152. spotifyTracks[
  1153. spotifySong.mediaSource
  1154. ].externalIds.isrc
  1155. }}
  1156. </p>
  1157. </template>
  1158. <button
  1159. v-if="showDontConvertButton"
  1160. class="button is-primary is-fullwidth"
  1161. @click="
  1162. removeSpotifySong(
  1163. spotifySong.mediaSource
  1164. )
  1165. "
  1166. >
  1167. Don't convert this song
  1168. </button>
  1169. </div>
  1170. <div
  1171. class="convert-table-cell convert-table-cell-right"
  1172. >
  1173. <p
  1174. v-if="
  1175. !alternativeMediaPerTrack[
  1176. spotifySong.mediaSource
  1177. ]
  1178. "
  1179. >
  1180. Alternatives not loaded yet
  1181. </p>
  1182. <template v-else>
  1183. <div class="alternative-media-items">
  1184. <div
  1185. class="alternative-media-item"
  1186. :class="{
  1187. 'selected-alternative-song':
  1188. preferredAlternativeSongPerTrack[
  1189. spotifySong.mediaSource
  1190. ] ===
  1191. alternativeMediaSource &&
  1192. missingMediaSources.length ===
  1193. 0
  1194. }"
  1195. v-for="alternativeMediaSource in alternativeMediaPerTrack[
  1196. spotifySong.mediaSource
  1197. ].mediaSources"
  1198. :key="
  1199. spotifySong.mediaSource +
  1200. alternativeMediaSource
  1201. "
  1202. >
  1203. <p
  1204. v-if="
  1205. alternativeMediaFailedMap[
  1206. alternativeMediaSource
  1207. ]
  1208. "
  1209. >
  1210. Song
  1211. {{ alternativeMediaSource }}
  1212. failed to load
  1213. </p>
  1214. <p
  1215. v-else-if="
  1216. !alternativeMediaMap[
  1217. alternativeMediaSource
  1218. ]
  1219. "
  1220. >
  1221. Song
  1222. {{ alternativeMediaSource }}
  1223. hasn't been loaded yet
  1224. </p>
  1225. <template v-else>
  1226. <div
  1227. class="alternative-song-container"
  1228. >
  1229. <song-item
  1230. :song="
  1231. alternativeMediaMap[
  1232. alternativeMediaSource
  1233. ]
  1234. "
  1235. >
  1236. <template #leftIcon>
  1237. <a
  1238. v-if="
  1239. alternativeMediaSource.split(
  1240. ':'
  1241. )[0] ===
  1242. 'youtube'
  1243. "
  1244. :href="`https://youtu.be/${
  1245. alternativeMediaSource.split(
  1246. ':'
  1247. )[1]
  1248. }`"
  1249. target="_blank"
  1250. >
  1251. <div
  1252. class="youtube-icon left-icon"
  1253. ></div>
  1254. </a>
  1255. <a
  1256. v-if="
  1257. alternativeMediaSource.split(
  1258. ':'
  1259. )[0] ===
  1260. 'soundcloud'
  1261. "
  1262. target="_blank"
  1263. >
  1264. <div
  1265. class="soundcloud-icon left-icon"
  1266. ></div>
  1267. </a>
  1268. </template>
  1269. </song-item>
  1270. <quick-confirm
  1271. v-if="
  1272. showReplaceButtonPerAlternative
  1273. "
  1274. placement="top"
  1275. @confirm="
  1276. replaceSpotifySong(
  1277. spotifySong.mediaSource,
  1278. alternativeMediaSource
  1279. )
  1280. "
  1281. >
  1282. <button
  1283. class="button is-primary is-fullwidth"
  1284. >
  1285. Use this alternative
  1286. </button>
  1287. </quick-confirm>
  1288. <button
  1289. v-if="
  1290. showDontConvertButton
  1291. "
  1292. class="button is-primary is-fullwidth"
  1293. @click="
  1294. removeAlternativeTrack(
  1295. spotifySong.mediaSource,
  1296. alternativeMediaSource
  1297. )
  1298. "
  1299. >
  1300. Remove this alternative
  1301. </button>
  1302. </div>
  1303. <ul v-if="showExtra">
  1304. <li
  1305. v-for="origin in alternativeMediaPerTrack[
  1306. spotifySong
  1307. .mediaSource
  1308. ].mediaSourcesOrigins[
  1309. alternativeMediaSource
  1310. ]"
  1311. :key="
  1312. spotifySong.mediaSource +
  1313. alternativeMediaSource +
  1314. origin
  1315. "
  1316. >
  1317. <hr />
  1318. <ul>
  1319. <li
  1320. v-for="originItem in origin"
  1321. :key="
  1322. spotifySong.mediaSource +
  1323. alternativeMediaSource +
  1324. origin +
  1325. originItem
  1326. "
  1327. >
  1328. +
  1329. {{ originItem }}
  1330. </li>
  1331. </ul>
  1332. </li>
  1333. </ul>
  1334. </template>
  1335. </div>
  1336. </div>
  1337. <p
  1338. v-if="
  1339. alternativeMediaPerTrack[
  1340. spotifySong.mediaSource
  1341. ].mediaSources.length === 0
  1342. "
  1343. >
  1344. No alternative media sources found
  1345. </p>
  1346. </template>
  1347. <div
  1348. v-if="
  1349. showReplacementInputs ||
  1350. (alternativeMediaPerTrack[
  1351. spotifySong.mediaSource
  1352. ] &&
  1353. alternativeMediaPerTrack[
  1354. spotifySong.mediaSource
  1355. ].mediaSources.length === 0)
  1356. "
  1357. >
  1358. <div>
  1359. <label class="label">
  1360. Enter replacement song from URL
  1361. </label>
  1362. <div
  1363. class="control is-grouped input-with-button"
  1364. >
  1365. <p class="control is-expanded">
  1366. <input
  1367. class="input"
  1368. type="text"
  1369. placeholder="Enter your song URL here..."
  1370. v-model="
  1371. replaceSongUrlMap[
  1372. spotifySong
  1373. .mediaSource
  1374. ]
  1375. "
  1376. @keyup.enter="
  1377. replaceSongFromUrl(
  1378. spotifySong.mediaSource
  1379. )
  1380. "
  1381. />
  1382. </p>
  1383. <p class="control">
  1384. <a
  1385. class="button is-info"
  1386. @click="
  1387. replaceSongFromUrl(
  1388. spotifySong.mediaSource
  1389. )
  1390. "
  1391. >Replace song</a
  1392. >
  1393. </p>
  1394. </div>
  1395. </div>
  1396. </div>
  1397. </div>
  1398. </template>
  1399. </div>
  1400. <div
  1401. class="convert-table convert-song-by-album"
  1402. v-if="currentConvertType === 'album'"
  1403. >
  1404. <h4>Spotify albums</h4>
  1405. <h4>Alternative albums (playlists)</h4>
  1406. <template
  1407. v-for="spotifyAlbum in filteredSpotifyAlbums"
  1408. :key="spotifyAlbum"
  1409. >
  1410. <div
  1411. class="convert-table-cell convert-table-cell-left"
  1412. >
  1413. <p>Album ID: {{ spotifyAlbum.albumId }}</p>
  1414. <p v-if="loadingSpotifyAlbums">
  1415. Loading album info...
  1416. </p>
  1417. <p
  1418. v-else-if="
  1419. loadedSpotifyAlbums &&
  1420. !spotifyAlbum.rawData
  1421. "
  1422. >
  1423. Failed to load album info...
  1424. </p>
  1425. <template v-else-if="loadedSpotifyAlbums">
  1426. <p>Name: {{ spotifyAlbum.rawData.name }}</p>
  1427. <p>
  1428. Label: {{ spotifyAlbum.rawData.label }}
  1429. </p>
  1430. <p>
  1431. Popularity:
  1432. {{ spotifyAlbum.rawData.popularity }}
  1433. </p>
  1434. <p>
  1435. Release date:
  1436. {{ spotifyAlbum.rawData.release_date }}
  1437. </p>
  1438. <p>
  1439. Artists:
  1440. {{
  1441. spotifyAlbum.rawData.artists
  1442. .map(artist => artist.name)
  1443. .join(", ")
  1444. }}
  1445. </p>
  1446. <p>
  1447. UPC:
  1448. {{
  1449. spotifyAlbum.rawData.external_ids
  1450. .upc
  1451. }}
  1452. </p>
  1453. </template>
  1454. <song-item
  1455. v-for="spotifyMediaSource in spotifyAlbum.songs"
  1456. :key="
  1457. spotifyAlbum.albumId +
  1458. spotifyMediaSource
  1459. "
  1460. :song="{
  1461. mediaSource: spotifyMediaSource,
  1462. title: spotifyTracks[spotifyMediaSource]
  1463. .name,
  1464. artists:
  1465. spotifyTracks[spotifyMediaSource]
  1466. .artists,
  1467. duration:
  1468. spotifyTracks[spotifyMediaSource]
  1469. .duration,
  1470. thumbnail:
  1471. spotifyTracks[spotifyMediaSource]
  1472. .albumImageUrl
  1473. }"
  1474. >
  1475. <template #leftIcon>
  1476. <a
  1477. :href="`https://open.spotify.com/track/${
  1478. spotifyMediaSource.split(':')[1]
  1479. }`"
  1480. target="_blank"
  1481. >
  1482. <div
  1483. class="spotify-icon left-icon"
  1484. ></div>
  1485. </a>
  1486. </template>
  1487. </song-item>
  1488. </div>
  1489. <div
  1490. class="convert-table-cell convert-table-cell-right"
  1491. >
  1492. <p
  1493. v-if="
  1494. !alternativeAlbumsPerAlbum[
  1495. spotifyAlbum.albumId
  1496. ]
  1497. "
  1498. >
  1499. No alternatives loaded
  1500. </p>
  1501. <div
  1502. class="alternative-album-items"
  1503. v-if="
  1504. alternativeAlbumsPerAlbum[
  1505. spotifyAlbum.albumId
  1506. ]
  1507. "
  1508. >
  1509. <p
  1510. v-if="
  1511. alternativeAlbumsPerAlbum[
  1512. spotifyAlbum.albumId
  1513. ].youtubePlaylistIds.length === 0
  1514. "
  1515. >
  1516. No alternative playlists were found
  1517. </p>
  1518. <div
  1519. class="alternative-album-item"
  1520. v-for="youtubePlaylistId in alternativeAlbumsPerAlbum[
  1521. spotifyAlbum.albumId
  1522. ].youtubePlaylistIds"
  1523. :key="
  1524. spotifyAlbum.albumId +
  1525. youtubePlaylistId
  1526. "
  1527. >
  1528. <p>
  1529. YouTube Playlist
  1530. {{ youtubePlaylistId }} has been
  1531. automatically found
  1532. </p>
  1533. <button
  1534. class="button is-primary is-fullwidth"
  1535. @click="
  1536. openReplaceAlbumModal(
  1537. spotifyAlbum.albumId,
  1538. youtubePlaylistId
  1539. )
  1540. "
  1541. >
  1542. Open replace modal
  1543. </button>
  1544. </div>
  1545. </div>
  1546. <div
  1547. v-if="
  1548. showReplacementInputs ||
  1549. (alternativeAlbumsPerAlbum[
  1550. spotifyAlbum.albumId
  1551. ] &&
  1552. alternativeAlbumsPerAlbum[
  1553. spotifyAlbum.albumId
  1554. ].youtubePlaylistIds.length === 0)
  1555. "
  1556. >
  1557. <div>
  1558. <label class="label">
  1559. Enter replacement playlist URL
  1560. </label>
  1561. <div
  1562. class="control is-grouped input-with-button"
  1563. >
  1564. <p class="control is-expanded">
  1565. <input
  1566. class="input"
  1567. type="text"
  1568. placeholder="Enter your playlist URL here..."
  1569. v-model="
  1570. replaceSongUrlMap[
  1571. `album:${spotifyAlbum.albumId}`
  1572. ]
  1573. "
  1574. @keyup.enter="
  1575. openReplaceAlbumModalFromUrl(
  1576. spotifyAlbum.albumId
  1577. )
  1578. "
  1579. />
  1580. </p>
  1581. <p class="control">
  1582. <a
  1583. class="button is-info"
  1584. @click="
  1585. openReplaceAlbumModalFromUrl(
  1586. spotifyAlbum.albumId
  1587. )
  1588. "
  1589. >Open replace modal</a
  1590. >
  1591. </p>
  1592. </div>
  1593. </div>
  1594. </div>
  1595. </div>
  1596. </template>
  1597. </div>
  1598. <div
  1599. class="convert-table convert-song-by-artist"
  1600. v-if="currentConvertType === 'artist'"
  1601. >
  1602. <h4>Spotify artists</h4>
  1603. <h4>Alternative artists (channels)</h4>
  1604. <template
  1605. v-for="spotifyArtist in filteredSpotifyArtists"
  1606. :key="spotifyArtist"
  1607. >
  1608. <div
  1609. class="convert-table-cell convert-table-cell-left"
  1610. >
  1611. <p>Artist ID: {{ spotifyArtist.artistId }}</p>
  1612. <p v-if="loadingSpotifyArtists">
  1613. Loading artist info...
  1614. </p>
  1615. <p
  1616. v-else-if="
  1617. loadedSpotifyArtists &&
  1618. !spotifyArtist.rawData
  1619. "
  1620. >
  1621. Failed to load artist info...
  1622. </p>
  1623. <template v-else-if="loadedSpotifyArtists">
  1624. <p>
  1625. Name: {{ spotifyArtist.rawData.name }}
  1626. </p>
  1627. <!-- <p>
  1628. Label: {{ spotifyArtist.rawData.label }}
  1629. </p>
  1630. <p>
  1631. Popularity:
  1632. {{ spotifyArtist.rawData.popularity }}
  1633. </p>
  1634. <p>
  1635. Release date:
  1636. {{ spotifyArtist.rawData.release_date }}
  1637. </p>
  1638. <p>
  1639. Artists:
  1640. {{
  1641. spotifyArtist.rawData.artists
  1642. .map(artist => artist.name)
  1643. .join(", ")
  1644. }}
  1645. </p>
  1646. <p>
  1647. UPC:
  1648. {{
  1649. spotifyArtist.rawData.external_ids
  1650. .upc
  1651. }}
  1652. </p> -->
  1653. </template>
  1654. <song-item
  1655. v-for="spotifyMediaSource in spotifyArtist.songs"
  1656. :key="
  1657. spotifyArtist.artistId +
  1658. spotifyMediaSource
  1659. "
  1660. :song="{
  1661. mediaSource: spotifyMediaSource,
  1662. title: spotifyTracks[spotifyMediaSource]
  1663. .name,
  1664. artists:
  1665. spotifyTracks[spotifyMediaSource]
  1666. .artists,
  1667. duration:
  1668. spotifyTracks[spotifyMediaSource]
  1669. .duration,
  1670. thumbnail:
  1671. spotifyTracks[spotifyMediaSource]
  1672. .albumImageUrl
  1673. }"
  1674. >
  1675. <template #leftIcon>
  1676. <a
  1677. :href="`https://open.spotify.com/track/${
  1678. spotifyMediaSource.split(':')[1]
  1679. }`"
  1680. target="_blank"
  1681. >
  1682. <div
  1683. class="spotify-icon left-icon"
  1684. ></div>
  1685. </a>
  1686. </template>
  1687. </song-item>
  1688. </div>
  1689. <div
  1690. class="convert-table-cell convert-table-cell-right"
  1691. >
  1692. <p
  1693. v-if="
  1694. !alternativeArtistsPerArtist[
  1695. spotifyArtist.artistId
  1696. ]
  1697. "
  1698. >
  1699. No alternatives loaded
  1700. </p>
  1701. <div
  1702. class="alternative-artist-items"
  1703. v-if="
  1704. alternativeArtistsPerArtist[
  1705. spotifyArtist.artistId
  1706. ]
  1707. "
  1708. >
  1709. <p
  1710. v-if="
  1711. alternativeArtistsPerArtist[
  1712. spotifyArtist.artistId
  1713. ].youtubeChannelIds.length === 0
  1714. "
  1715. >
  1716. No alternative channels were found
  1717. </p>
  1718. <div
  1719. class="alternative-artist-item"
  1720. v-for="youtubeChannelId in alternativeArtistsPerArtist[
  1721. spotifyArtist.artistId
  1722. ].youtubeChannelIds"
  1723. :key="
  1724. spotifyArtist.artistId +
  1725. youtubeChannelId
  1726. "
  1727. >
  1728. <p>
  1729. YouTube channel
  1730. {{ youtubeChannelId }} has been
  1731. automatically found
  1732. </p>
  1733. <button
  1734. class="button is-primary is-fullwidth"
  1735. @click="
  1736. openReplaceArtistModal(
  1737. spotifyArtist.artistId,
  1738. `https://youtube.com/channel/${youtubeChannelId}`
  1739. )
  1740. "
  1741. >
  1742. Open replace modal
  1743. </button>
  1744. </div>
  1745. </div>
  1746. <div
  1747. v-if="
  1748. showReplacementInputs ||
  1749. (alternativeArtistsPerArtist[
  1750. spotifyArtist.artistId
  1751. ] &&
  1752. alternativeArtistsPerArtist[
  1753. spotifyArtist.artistId
  1754. ].youtubeChannelIds.length === 0)
  1755. "
  1756. >
  1757. <div>
  1758. <label class="label">
  1759. Enter replacement YouTube channel
  1760. URL
  1761. </label>
  1762. <div
  1763. class="control is-grouped input-with-button"
  1764. >
  1765. <p class="control is-expanded">
  1766. <input
  1767. class="input"
  1768. type="text"
  1769. placeholder="Enter your channel URL here..."
  1770. v-model="
  1771. replaceSongUrlMap[
  1772. `artist:${spotifyArtist.artistId}`
  1773. ]
  1774. "
  1775. @keyup.enter="
  1776. openReplaceArtistModalFromUrl(
  1777. spotifyArtist.artistId
  1778. )
  1779. "
  1780. />
  1781. </p>
  1782. <p class="control">
  1783. <a
  1784. class="button is-info"
  1785. @click="
  1786. openReplaceArtistModalFromUrl(
  1787. spotifyArtist.artistId
  1788. )
  1789. "
  1790. >Open replace modal</a
  1791. >
  1792. </p>
  1793. </div>
  1794. </div>
  1795. </div>
  1796. </div>
  1797. </template>
  1798. </div>
  1799. </template>
  1800. </template>
  1801. </modal>
  1802. </div>
  1803. </template>
  1804. <style lang="less" scoped>
  1805. :deep(.song-item) {
  1806. .left-icon {
  1807. cursor: pointer;
  1808. }
  1809. }
  1810. .tracks {
  1811. display: flex;
  1812. flex-direction: column;
  1813. .track-row {
  1814. .left,
  1815. .right {
  1816. padding: 8px;
  1817. width: 50%;
  1818. box-shadow: inset 0px 0px 1px white;
  1819. display: flex;
  1820. flex-direction: column;
  1821. row-gap: 8px;
  1822. }
  1823. }
  1824. }
  1825. .alternative-media-items {
  1826. display: flex;
  1827. flex-direction: column;
  1828. row-gap: 12px;
  1829. }
  1830. .alternative-song-container,
  1831. .convert-table-cell-left {
  1832. display: flex;
  1833. flex-direction: column;
  1834. row-gap: 12px;
  1835. > * {
  1836. flex-grow: 0;
  1837. }
  1838. }
  1839. .convert-table {
  1840. display: grid;
  1841. grid-template-columns: 50% 50%;
  1842. gap: 1px;
  1843. .convert-table-cell {
  1844. outline: 1px solid white;
  1845. padding: 4px;
  1846. }
  1847. }
  1848. .selected-alternative-song {
  1849. // outline: 4px solid red;
  1850. border-left: 12px solid var(--primary-color);
  1851. padding: 4px;
  1852. }
  1853. .buttons-options-info-row {
  1854. display: grid;
  1855. grid-template-columns: 33.3% 33.3% 33.3%;
  1856. gap: 8px;
  1857. .buttons,
  1858. .options {
  1859. display: flex;
  1860. flex-direction: column;
  1861. row-gap: 8px;
  1862. > .control {
  1863. margin-bottom: 0 !important;
  1864. }
  1865. }
  1866. }
  1867. // .column-headers {
  1868. // display: flex;
  1869. // flex-direction: row;
  1870. // .column-header {
  1871. // flex: 1;
  1872. // }
  1873. // }
  1874. // .artists {
  1875. // display: flex;
  1876. // flex-direction: column;
  1877. // .artist-item {
  1878. // display: flex;
  1879. // flex-direction: column;
  1880. // row-gap: 8px;
  1881. // box-shadow: inset 0px 0px 1px white;
  1882. // width: 50%;
  1883. // position: relative;
  1884. // .spotify-section {
  1885. // display: flex;
  1886. // flex-direction: column;
  1887. // row-gap: 8px;
  1888. // padding: 8px 12px;
  1889. // .spotify-songs {
  1890. // display: flex;
  1891. // flex-direction: column;
  1892. // row-gap: 4px;
  1893. // }
  1894. // }
  1895. // .soundcloud-section {
  1896. // position: absolute;
  1897. // left: 100%;
  1898. // top: 0;
  1899. // width: 100%;
  1900. // height: 100%;
  1901. // overflow: hidden;
  1902. // box-shadow: inset 0px 0px 1px white;
  1903. // padding: 8px 12px;
  1904. // }
  1905. // }
  1906. // }
  1907. </style>