Browse Source

feat: started working on convert spotify songs modal

Kristian Vos 2 years ago
parent
commit
8a6992e9b1

+ 2 - 0
backend/logic/actions/index.js

@@ -11,6 +11,7 @@ import punishments from "./punishments";
 import utils from "./utils";
 import youtube from "./youtube";
 import soundcloud from "./soundcloud";
+import spotify from "./spotify";
 import media from "./media";
 
 export default {
@@ -27,5 +28,6 @@ export default {
 	utils,
 	youtube,
 	soundcloud,
+	spotify,
 	media
 };

+ 44 - 0
backend/logic/actions/spotify.js

@@ -0,0 +1,44 @@
+import mongoose from "mongoose";
+import async from "async";
+
+import isLoginRequired from "../hooks/loginRequired";
+import { useHasPermission } from "../hooks/hasPermission";
+
+// eslint-disable-next-line
+import moduleManager from "../../index";
+
+const DBModule = moduleManager.modules.db;
+const UtilsModule = moduleManager.modules.utils;
+const SoundcloudModule = moduleManager.modules.soundcloud;
+const SpotifyModule = moduleManager.modules.spotify;
+
+export default {
+	/**
+	 * Fetches new SoundCloud API key
+	 *
+	 * @returns {{status: string, data: object}}
+	 */
+	getTracksFromMediaSources: useHasPermission(
+		"admin.view.spotify",
+		function getTracksFromMediaSources(session, mediaSources, cb) {
+			SpotifyModule.runJob("GET_TRACKS_FROM_MEDIA_SOURCES", { mediaSources }, this)
+				.then(response => {
+					this.log(
+						"SUCCESS",
+						"SPOTIFY_GET_TRACKS_FROM_MEDIA_SOURCES",
+						`Getting tracks from media sources was successful.`
+					);
+					return cb({ status: "success", data: { tracks: response.tracks } });
+				})
+				.catch(async err => {
+					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+					this.log(
+						"ERROR",
+						"SPOTIFY_GET_TRACKS_FROM_MEDIA_SOURCES",
+						`Getting tracks from media sources failed. "${err}"`
+					);
+					return cb({ status: "error", message: err });
+				});
+		}
+	)
+};

+ 1 - 0
backend/logic/hooks/hasPermission.js

@@ -73,6 +73,7 @@ permissions.admin = {
 	"admin.view.statistics": true,
 	"admin.view.youtube": true,
 	"admin.view.soundcloud": true,
+	"admin.view.spotify": true,
 	"dataRequests.resolve": true,
 	"media.recalculateAllRatings": true,
 	"media.removeImportJobs": true,

+ 47 - 0
backend/logic/spotify.js

@@ -7,6 +7,7 @@ import axios from "axios";
 import url from "url";
 
 import CoreClass from "../core";
+import { resolve } from "path";
 
 let SpotifyModule;
 let DBModule;
@@ -322,6 +323,52 @@ class _SpotifyModule extends CoreClass {
 		});
 	}
 
+	/**
+	 * Gets tracks from media sources
+	 *
+	 * @param {object} payload
+	 * @returns {Promise}
+	 */
+	async GET_TRACKS_FROM_MEDIA_SOURCES(payload) {
+		return new Promise((resolve, reject) => {
+			const { mediaSources } = payload;
+
+			const responses = {};
+
+			const promises = [];
+
+			mediaSources.forEach(mediaSource => {
+				promises.push(
+					new Promise(resolve => {
+						const trackId = mediaSource.split(":")[1];
+						SpotifyModule.runJob("GET_TRACK", { identifier: trackId, createMissing: true }, this)
+							.then(({ track }) => {
+								responses[mediaSource] = track;
+							})
+							.catch(err => {
+								SpotifyModule.log(
+									"ERROR",
+									`Getting tracked with media source ${mediaSource} failed.`,
+									typeof err === "string" ? err : err.message
+								);
+								responses[mediaSource] = typeof err === "string" ? err : err.message;
+							})
+							.finally(() => {
+								resolve();
+							});
+					})
+				);
+			});
+
+			Promise.all(promises)
+				.then(() => {
+					SpotifyModule.log("SUCCESS", `Got all tracks.`);
+					resolve({ tracks: responses });
+				})
+				.catch(reject);
+		});
+	}
+
 	/**
 	 * Get Spotify track
 	 *

+ 2 - 1
frontend/src/components/ModalManager.vue

@@ -27,7 +27,8 @@ const modalComponents = shallowRef(
 		confirm: "Confirm.vue",
 		editSong: "EditSong/index.vue",
 		viewYoutubeVideo: "ViewYoutubeVideo.vue",
-		bulkEditPlaylist: "BulkEditPlaylist.vue"
+		bulkEditPlaylist: "BulkEditPlaylist.vue",
+		convertSpotifySongs: "ConvertSpotifySongs.vue"
 	})
 );
 </script>

+ 260 - 0
frontend/src/components/modals/ConvertSpotifySongs.vue

@@ -0,0 +1,260 @@
+<script setup lang="ts">
+import {
+	defineProps,
+	defineAsyncComponent,
+	onMounted,
+	ref,
+	computed
+} from "vue";
+import Toast from "toasters";
+import { useModalsStore } from "@/stores/modals";
+import { useWebsocketsStore } from "@/stores/websockets";
+
+const Modal = defineAsyncComponent(() => import("@/components/Modal.vue"));
+
+const SongItem = defineAsyncComponent(
+	() => import("@/components/SongItem.vue")
+);
+
+const { openModal, closeCurrentModal } = useModalsStore();
+const { socket } = useWebsocketsStore();
+
+const TAG = "CSS";
+
+const props = defineProps({
+	modalUuid: { type: String, required: true },
+	playlistId: { type: String, default: null }
+});
+
+const playlist = ref(null);
+const allSongs = ref(null);
+const loaded = ref(false);
+const currentConvertType = ref("artist");
+const sortBy = ref("track_count_des");
+
+const spotifyArtists = ref({});
+
+const spotifyArtistsArray = computed(() =>
+	Object.entries(spotifyArtists.value)
+		.map(([spotifyArtistId, spotifyArtist]) => ({
+			artistId: spotifyArtistId,
+			...spotifyArtist
+		}))
+		.sort((a, b) => {
+			if (sortBy.value === "track_count_des")
+				return b.songs.length - a.songs.length;
+			if (sortBy.value === "track_count_asc")
+				return a.songs.length - b.songs.length;
+		})
+);
+
+const toggleSpotifyArtistExpanded = spotifyArtistId => {
+	spotifyArtists.value[spotifyArtistId].expanded =
+		!spotifyArtists.value[spotifyArtistId].expanded;
+};
+
+onMounted(() => {
+	console.debug(TAG, "On mounted start");
+
+	console.debug(TAG, "Getting playlist", props);
+	socket.dispatch("playlists.getPlaylist", props.playlistId, res => {
+		console.debug(TAG, "Get playlist response", res);
+
+		if (res.status !== "success") {
+			new Toast(res.message);
+			closeCurrentModal();
+			return;
+		}
+
+		playlist.value = res.data.playlist;
+		allSongs.value = {};
+
+		playlist.value.songs
+			.filter(song => song.mediaSource.startsWith("spotify:"))
+			.forEach(song => {
+				allSongs.value[song.mediaSource] = {
+					song,
+					track: null
+				};
+			});
+
+		const mediaSources = Object.keys(allSongs.value);
+
+		console.debug(TAG, "getTracksFromMediaSources start", mediaSources);
+		socket.dispatch(
+			"spotify.getTracksFromMediaSources",
+			mediaSources,
+			res => {
+				console.debug(TAG, "getTracksFromMediaSources response", res);
+				if (res.status !== "success") {
+					new Toast(res.message);
+					closeCurrentModal();
+					return;
+				}
+
+				const { tracks } = res.data;
+
+				Object.entries(tracks).forEach(([mediaSource, track]) => {
+					allSongs.value[mediaSource].track = track;
+
+					track.artistIds.forEach((artistId, artistIndex) => {
+						if (!spotifyArtists.value[artistId]) {
+							spotifyArtists.value[artistId] = {
+								name: track.artists[artistIndex],
+								songs: [mediaSource],
+								expanded: false
+							};
+						} else
+							spotifyArtists.value[artistId].songs.push(
+								mediaSource
+							);
+					});
+				});
+
+				loaded.value = true;
+			}
+		);
+	});
+
+	console.debug(TAG, "On mounted end");
+});
+</script>
+
+<template>
+	<div>
+		<modal
+			title="Convert Spotify Songs"
+			class="convert-spotify-songs-modal"
+			size="wide"
+			@closed="closeCurrentModal()"
+		>
+			<template #body>
+				<p>Converting by {{ currentConvertType }}</p>
+				<p>Sorting by {{ sortBy }}</p>
+
+				<br />
+
+				<div class="column-headers">
+					<div class="spotify-column-header column-header">
+						<h3>Spotify</h3>
+					</div>
+					<div class="soumdcloud-column-header column-header">
+						<h3>Soundcloud</h3>
+					</div>
+				</div>
+
+				<div class="artists">
+					<div
+						v-for="spotifyArtist in spotifyArtistsArray"
+						:key="spotifyArtist.artistId"
+						class="artist-item"
+					>
+						<div class="spotify-section">
+							<p
+								@click="
+									toggleSpotifyArtistExpanded(
+										spotifyArtist.artistId
+									)
+								"
+							>
+								{{ spotifyArtist.name }} ({{
+									spotifyArtist.songs.length
+								}}
+								songs)
+							</p>
+
+							<div
+								class="spotify-songs"
+								v-if="spotifyArtist.expanded"
+							>
+								<div
+									v-for="mediaSource in spotifyArtist.songs"
+									:key="`${spotifyArtist.artistId}-${mediaSource}`"
+									class="spotify-song"
+								>
+									<song-item
+										:song="{
+											title: allSongs[mediaSource].track
+												.name,
+											duration:
+												allSongs[mediaSource].track
+													.duration,
+											artists:
+												allSongs[mediaSource].track
+													.artists,
+											thumbnail:
+												allSongs[mediaSource].track
+													.albumImageUrl
+										}"
+										:disabled-actions="[
+											'youtube',
+											'report',
+											'addToPlaylist',
+											'edit'
+										]"
+									></song-item>
+								</div>
+							</div>
+						</div>
+						<div class="soundcloud-section">
+							<p>Not found</p>
+							<div v-if="spotifyArtist.expanded">
+								<button class="button">Get artist</button>
+							</div>
+						</div>
+					</div>
+				</div>
+			</template>
+		</modal>
+	</div>
+</template>
+
+<style lang="less" scoped>
+.column-headers {
+	display: flex;
+	flex-direction: row;
+
+	.column-header {
+		flex: 1;
+	}
+}
+
+.artists {
+	display: flex;
+	flex-direction: column;
+
+	.artist-item {
+		display: flex;
+		flex-direction: column;
+		row-gap: 8px;
+		box-shadow: inset 0px 0px 1px white;
+		width: 50%;
+
+		position: relative;
+
+		.spotify-section {
+			display: flex;
+			flex-direction: column;
+			row-gap: 8px;
+			padding: 8px 12px;
+
+			.spotify-songs {
+				display: flex;
+				flex-direction: column;
+				row-gap: 4px;
+			}
+		}
+
+		.soundcloud-section {
+			position: absolute;
+			left: 100%;
+			top: 0;
+			width: 100%;
+			height: 100%;
+			overflow: hidden;
+			box-shadow: inset 0px 0px 1px white;
+			padding: 8px 12px;
+		}
+	}
+}
+</style>

+ 20 - 1
frontend/src/components/modals/EditPlaylist/index.vue

@@ -55,11 +55,18 @@ const playlistSongs = computed({
 	}
 });
 
+const containsSpotifySongs = computed(
+	() =>
+		playlistSongs.value
+			.map(playlistSong => playlistSong.mediaSource.split(":")[0])
+			.indexOf("spotify") !== -1
+);
+
 const { tab, playlist } = storeToRefs(editPlaylistStore);
 const { setPlaylist, clearPlaylist, addSong, removeSong, repositionedSong } =
 	editPlaylistStore;
 
-const { closeCurrentModal } = useModalsStore();
+const { closeCurrentModal, openModal } = useModalsStore();
 
 const showTab = payload => {
 	if (tabs.value[`${payload}-tab`])
@@ -521,6 +528,18 @@ onBeforeUnmount(() => {
 			>
 				Download Playlist
 			</button>
+			<button
+				class="button is-default"
+				v-if="isOwner() && containsSpotifySongs"
+				@click="
+					openModal({
+						modal: 'convertSpotifySongs',
+						props: { playlistId: playlist._id }
+					})
+				"
+			>
+				Convert Spotify Songs
+			</button>
 			<div class="right">
 				<quick-confirm
 					v-if="