ReplaceSpotifySongs.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505
  1. <script setup lang="ts">
  2. import { defineAsyncComponent, ref, onMounted, onBeforeUnmount } from "vue";
  3. import Toast from "toasters";
  4. import { DraggableList } from "vue-draggable-list";
  5. import { useWebsocketsStore } from "@/stores/websockets";
  6. import { useModalsStore } from "@/stores/modals";
  7. const Modal = defineAsyncComponent(() => import("@/components/Modal.vue"));
  8. const MediaItem = defineAsyncComponent(
  9. () => import("@/components/MediaItem.vue")
  10. );
  11. const props = defineProps({
  12. modalUuid: { type: String, required: true },
  13. spotifyAlbum: { type: Object, default: () => ({}) },
  14. spotifyTracks: { type: Array, default: () => [] },
  15. playlistId: { type: String, required: true },
  16. youtubePlaylistId: { type: String, default: null },
  17. youtubeChannelUrl: { type: String, default: null }
  18. });
  19. const { socket } = useWebsocketsStore();
  20. const { closeCurrentModal } = useModalsStore();
  21. const isImportingPlaylist = ref(false);
  22. const hasImportedPlaylist = ref(false);
  23. const trackSongs = ref([]);
  24. const playlistSongs = ref([]);
  25. const localSpotifyTracks = ref([]);
  26. const replacingAllSpotifySongs = ref(false);
  27. const replaceAllSpotifySongs = async () => {
  28. if (replacingAllSpotifySongs.value) return;
  29. replacingAllSpotifySongs.value = true;
  30. const replaceArray = [];
  31. localSpotifyTracks.value.forEach((spotifyTrack, index) => {
  32. const spotifyMediaSource = `spotify:${spotifyTrack.trackId}`;
  33. if (trackSongs.value[index].length === 1) {
  34. const replacementMediaSource =
  35. trackSongs.value[index][0].mediaSource;
  36. if (!spotifyMediaSource || !replacementMediaSource) return;
  37. replaceArray.push([spotifyMediaSource, replacementMediaSource]);
  38. }
  39. });
  40. if (replaceArray.length === 0) {
  41. new Toast(
  42. "No songs can be replaced, please make sure each track has one song dragged into the box"
  43. );
  44. return;
  45. }
  46. const promises = replaceArray.map(
  47. ([spotifyMediaSource, replacementMediaSource]) =>
  48. new Promise<void>(resolve => {
  49. socket.dispatch(
  50. "playlists.replaceSongInPlaylist",
  51. spotifyMediaSource,
  52. replacementMediaSource,
  53. props.playlistId,
  54. res => {
  55. console.log(
  56. "playlists.replaceSongInPlaylist response",
  57. res
  58. );
  59. if (res.status === "success") {
  60. const spotifyTrackId =
  61. spotifyMediaSource.split(":")[1];
  62. const trackIndex =
  63. localSpotifyTracks.value.findIndex(
  64. spotifyTrack =>
  65. spotifyTrack.trackId === spotifyTrackId
  66. );
  67. localSpotifyTracks.value.splice(trackIndex, 1);
  68. trackSongs.value.splice(trackIndex, 1);
  69. }
  70. resolve();
  71. }
  72. );
  73. })
  74. );
  75. Promise.allSettled(promises).finally(() => {
  76. replacingAllSpotifySongs.value = false;
  77. if (localSpotifyTracks.value.length === 0) closeCurrentModal();
  78. });
  79. };
  80. const replaceSpotifySong = (oldMediaSource, newMediaSource) => {
  81. socket.dispatch(
  82. "playlists.replaceSongInPlaylist",
  83. oldMediaSource,
  84. newMediaSource,
  85. props.playlistId,
  86. res => {
  87. console.log("playlists.replaceSongInPlaylist response", res);
  88. if (res.status === "success") {
  89. const spotifyTrackId = oldMediaSource.split(":")[1];
  90. const trackIndex = localSpotifyTracks.value.findIndex(
  91. spotifyTrack => spotifyTrack.trackId === spotifyTrackId
  92. );
  93. localSpotifyTracks.value.splice(trackIndex, 1);
  94. trackSongs.value.splice(trackIndex, 1);
  95. if (localSpotifyTracks.value.length === 0) closeCurrentModal();
  96. }
  97. }
  98. );
  99. };
  100. const tryToAutoMove = () => {
  101. const songs = playlistSongs.value;
  102. localSpotifyTracks.value.forEach((spotifyTrack, index) => {
  103. songs.forEach(playlistSong => {
  104. if (
  105. playlistSong.title
  106. .toLowerCase()
  107. .trim()
  108. .indexOf(spotifyTrack.name.toLowerCase().trim()) !== -1
  109. ) {
  110. songs.splice(songs.indexOf(playlistSong), 1);
  111. trackSongs.value[index].push(playlistSong);
  112. }
  113. });
  114. });
  115. };
  116. const importChannel = () => {
  117. if (hasImportedPlaylist.value)
  118. return new Toast("A playlist has already imported.");
  119. if (isImportingPlaylist.value)
  120. return new Toast("A playlist is already importing.");
  121. isImportingPlaylist.value = true;
  122. // don't give starting import message instantly in case of instant error
  123. setTimeout(() => {
  124. if (isImportingPlaylist.value) {
  125. new Toast(
  126. "Starting to import your channel. This can take some time to do."
  127. );
  128. }
  129. }, 750);
  130. return socket.dispatch(
  131. "youtube.requestSet",
  132. props.youtubeChannelUrl,
  133. false,
  134. true,
  135. res => {
  136. const mediaSources = res.videos.map(
  137. video => `youtube:${video.youtubeId}`
  138. );
  139. socket.dispatch(
  140. "songs.getSongsFromMediaSources",
  141. mediaSources,
  142. res => {
  143. if (res.status === "success") {
  144. const { songs } = res.data;
  145. playlistSongs.value = songs;
  146. songs.forEach(() => {
  147. trackSongs.value.push([]);
  148. });
  149. hasImportedPlaylist.value = true;
  150. isImportingPlaylist.value = false;
  151. tryToAutoMove();
  152. return;
  153. }
  154. new Toast("Could not get songs.");
  155. }
  156. );
  157. return new Toast({ content: res.message, timeout: 20000 });
  158. }
  159. );
  160. };
  161. const importPlaylist = () => {
  162. if (hasImportedPlaylist.value)
  163. return new Toast("A playlist has already imported.");
  164. if (isImportingPlaylist.value)
  165. return new Toast("A playlist is already importing.");
  166. isImportingPlaylist.value = true;
  167. // don't give starting import message instantly in case of instant error
  168. setTimeout(() => {
  169. if (isImportingPlaylist.value) {
  170. new Toast(
  171. "Starting to import your playlist. This can take some time to do."
  172. );
  173. }
  174. }, 750);
  175. return socket.dispatch(
  176. "youtube.requestSet",
  177. `https://youtube.com/playlist?list=${props.youtubePlaylistId}`,
  178. false,
  179. true,
  180. res => {
  181. const mediaSources = res.videos.map(
  182. video => `youtube:${video.youtubeId}`
  183. );
  184. socket.dispatch(
  185. "songs.getSongsFromMediaSources",
  186. mediaSources,
  187. res => {
  188. if (res.status === "success") {
  189. const { songs } = res.data;
  190. playlistSongs.value = songs;
  191. songs.forEach(() => {
  192. trackSongs.value.push([]);
  193. });
  194. hasImportedPlaylist.value = true;
  195. isImportingPlaylist.value = false;
  196. tryToAutoMove();
  197. return;
  198. }
  199. new Toast("Could not get songs.");
  200. }
  201. );
  202. return new Toast({ content: res.message, timeout: 20000 });
  203. }
  204. );
  205. };
  206. onMounted(() => {
  207. localSpotifyTracks.value = props.spotifyTracks;
  208. if (props.youtubePlaylistId) importPlaylist();
  209. else if (props.youtubeChannelUrl) importChannel();
  210. });
  211. onBeforeUnmount(() => {});
  212. </script>
  213. <template>
  214. <div>
  215. <modal
  216. title="Replace Spotify Songs"
  217. class="replace-spotify-songs-modal"
  218. size="wide"
  219. >
  220. <template #body>
  221. <div class="playlist-songs">
  222. <h4>YouTube songs</h4>
  223. <p v-if="isImportingPlaylist">Importing playlist...</p>
  224. <draggable-list
  225. v-if="playlistSongs.length > 0"
  226. v-model:list="playlistSongs"
  227. item-key="mediaSource"
  228. :group="`replace-spotify-album-${modalUuid}-songs`"
  229. >
  230. <template #item="{ element }">
  231. <media-item
  232. :key="`playlist-song-${element.mediaSource}`"
  233. :song="element"
  234. >
  235. </media-item>
  236. </template>
  237. </draggable-list>
  238. </div>
  239. <div class="track-boxes">
  240. <div
  241. class="track-box"
  242. v-for="(track, index) in localSpotifyTracks"
  243. :key="track.trackId"
  244. >
  245. <div class="track-position-title">
  246. <p>
  247. {{ track.name }} -
  248. {{ track.artists.join(", ") }}
  249. </p>
  250. </div>
  251. <!-- :data-track-index="index" -->
  252. <div class="track-box-songs-drag-area">
  253. <draggable-list
  254. v-model:list="trackSongs[index]"
  255. item-key="mediaSource"
  256. :group="`replace-spotify-album-${modalUuid}-songs`"
  257. >
  258. <template #item="{ element }">
  259. <media-item
  260. :key="`track-song-${element.mediaSource}`"
  261. :song="element"
  262. >
  263. </media-item>
  264. <button
  265. class="button is-primary is-fullwidth"
  266. @click="
  267. replaceSpotifySong(
  268. `spotify:${track.trackId}`,
  269. element.mediaSource
  270. )
  271. "
  272. >
  273. Replace Spotify song with this song
  274. </button>
  275. </template>
  276. </draggable-list>
  277. </div>
  278. </div>
  279. </div>
  280. </template>
  281. <template #footer>
  282. <button class="button is-primary" @click="tryToAutoMove()">
  283. Try to auto move
  284. </button>
  285. <button
  286. class="button is-primary"
  287. @click="replaceAllSpotifySongs()"
  288. >
  289. Replace all songs
  290. </button>
  291. </template>
  292. </modal>
  293. </div>
  294. </template>
  295. <style lang="less">
  296. .night-mode {
  297. .spotify-album-container,
  298. .playlist-songs,
  299. .track-boxes {
  300. background-color: var(--dark-grey-3) !important;
  301. border: 0 !important;
  302. .tab {
  303. border: 0 !important;
  304. }
  305. }
  306. .api-result {
  307. background-color: var(--dark-grey-3) !important;
  308. }
  309. .api-result .tracks .track:hover,
  310. .api-result .tracks .track:focus,
  311. .discogs-album .tracks .track:hover,
  312. .discogs-album .tracks .track:focus {
  313. background-color: var(--dark-grey-2) !important;
  314. }
  315. .api-result .bottom-row img,
  316. .discogs-album .bottom-row img {
  317. filter: invert(100%);
  318. }
  319. .label,
  320. p,
  321. strong {
  322. color: var(--light-grey-2);
  323. }
  324. }
  325. .replace-spotify-songs-modal {
  326. .modal-card-title {
  327. text-align: center;
  328. margin-left: 24px;
  329. }
  330. .modal-card {
  331. width: 100%;
  332. height: 100%;
  333. .modal-card-body {
  334. padding: 16px;
  335. display: flex;
  336. flex-direction: row;
  337. flex-wrap: wrap;
  338. justify-content: space-evenly;
  339. }
  340. .modal-card-foot {
  341. .button {
  342. margin: 0;
  343. &:not(:first-of-type) {
  344. margin-left: 5px;
  345. }
  346. }
  347. div div {
  348. margin-right: 5px;
  349. }
  350. .right {
  351. display: flex;
  352. margin-left: auto;
  353. margin-right: 0;
  354. }
  355. }
  356. }
  357. }
  358. </style>
  359. <style lang="less" scoped>
  360. .break {
  361. flex-basis: 100%;
  362. height: 0;
  363. border: 1px solid var(--dark-grey);
  364. margin-top: 16px;
  365. margin-bottom: 16px;
  366. }
  367. .spotify-album-container,
  368. .playlist-songs {
  369. width: 500px;
  370. background-color: var(--light-grey);
  371. border: 1px rgba(163, 224, 255, 0.75) solid;
  372. border-radius: @border-radius;
  373. padding: 16px;
  374. overflow: auto;
  375. height: 100%;
  376. h4 {
  377. margin: 0;
  378. margin-bottom: 16px;
  379. }
  380. button {
  381. margin: 5px 0;
  382. }
  383. }
  384. .track-boxes {
  385. width: 500px;
  386. background-color: var(--light-grey);
  387. border: 1px rgba(163, 224, 255, 0.75) solid;
  388. border-radius: @border-radius;
  389. padding: 16px;
  390. overflow: auto;
  391. height: 100%;
  392. .track-box:first-child {
  393. margin-top: 0;
  394. border-radius: @border-radius @border-radius 0 0;
  395. }
  396. .track-box:last-child {
  397. border-radius: 0 0 @border-radius @border-radius;
  398. }
  399. .track-box {
  400. border: 0.5px solid var(--black);
  401. margin-top: -1px;
  402. line-height: 16px;
  403. display: flex;
  404. flex-flow: column;
  405. .track-position-title {
  406. display: flex;
  407. span {
  408. font-weight: 600;
  409. display: inline-block;
  410. margin-top: 7px;
  411. margin-bottom: 7px;
  412. margin-left: 7px;
  413. }
  414. p {
  415. display: inline-block;
  416. margin: 7px;
  417. flex: 1;
  418. }
  419. }
  420. .track-box-songs-drag-area {
  421. flex: 1;
  422. min-height: 100px;
  423. display: flex;
  424. flex-direction: column;
  425. }
  426. }
  427. }
  428. </style>