Parcourir la source

feat: worked on convert spotify songs modal functionality

Kristian Vos il y a 1 an
Parent
commit
1059dda98e

+ 23 - 0
backend/logic/actions/media.js

@@ -913,5 +913,28 @@ export default {
 				this.log("ERROR", "MEDIA_REMOVE_IMPORT_JOBS", `Removing import jobs failed. "${err}"`);
 				return cb({ status: "error", message: err });
 			});
+	}),
+
+	/**
+	 * Gets an array of media from media sources
+	 *
+	 * @returns {{status: string, data: object}}
+	 */
+	getMediaFromMediaSources: isLoginRequired(function getMediaFromMediaSources(session, mediaSources, cb) {
+		MediaModule.runJob("GET_MEDIA_FROM_MEDIA_SOURCES", { mediaSources }, this)
+			.then(songMap => {
+				this.log("SUCCESS", "MEDIA_GET_MEDIA_FROM_MEDIA_SOURCES", `GET_MEDIA_FROM_MEDIA_SOURCES successful.`);
+
+				return cb({ status: "success", message: "GET_MEDIA_FROM_MEDIA_SOURCES success", data: { songMap } });
+			})
+			.catch(async err => {
+				err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+				this.log(
+					"ERROR",
+					"MEDIA_GET_MEDIA_FROM_MEDIA_SOURCES",
+					`GET_MEDIA_FROM_MEDIA_SOURCES failed. "${err}"`
+				);
+				return cb({ status: "error", message: err });
+			});
 	})
 };

+ 6 - 0
backend/logic/db/schemas/soundcloudTrack.js

@@ -16,6 +16,12 @@ export default {
 	userPermalink: { type: String },
 	trackFormat: { type: String },
 	permalink: { type: String },
+	monetizationModel: { type: String },
+	policy: { type: String },
+	streamable: { type: Boolean },
+	sharing: { type: String },
+	state: { type: String },
+	embeddableBy: { type: String },
 	createdAt: { type: Date, default: Date.now, required: true },
 	documentVersion: { type: Number, default: 1, required: true }
 };

+ 90 - 0
backend/logic/media.js

@@ -480,6 +480,96 @@ class _MediaModule extends CoreClass {
 		});
 	}
 
+	/**
+	 * Gets media from media sources
+	 *
+	 * @param {object} payload - an object containing the payload
+	 * @param {string} payload.mediaSources - the media sources
+	 * @returns {Promise} - returns a promise (resolve, reject)
+	 */
+	GET_MEDIA_FROM_MEDIA_SOURCES(payload) {
+		return new Promise((resolve, reject) => {
+			const songMap = {};
+			const youtubeMediaSources = payload.mediaSources.filter(mediaSource => mediaSource.startsWith("youtube:"));
+			const soundcloudMediaSources = payload.mediaSources.filter(mediaSource =>
+				mediaSource.startsWith("soundcloud:")
+			);
+
+			async.waterfall(
+				[
+					next => {
+						const allPromises = [];
+
+						youtubeMediaSources.forEach(mediaSource => {
+							const youtubeId = mediaSource.split(":")[1];
+
+							const promise = YouTubeModule.runJob(
+								"GET_VIDEO",
+								{ identifier: youtubeId, createMissing: true },
+								this
+							)
+								.then(response => {
+									const { youtubeId, title, author, duration } = response.video;
+									songMap[mediaSource] = {
+										mediaSource: `youtube:${youtubeId}`,
+										title,
+										artists: [author],
+										duration
+									};
+								})
+								.catch(err => {
+									MediaModule.log(
+										"ERROR",
+										`Failed to get media in GET_MEDIA_FROM_MEDIA_SOURCES with mediaSource ${mediaSource} and error`,
+										typeof err === "string" ? err : err.message
+									);
+								});
+
+							allPromises.push(promise);
+						});
+
+						soundcloudMediaSources.forEach(mediaSource => {
+							const trackId = mediaSource.split(":")[1];
+
+							const promise = SoundCloudModule.runJob(
+								"GET_TRACK",
+								{ identifier: trackId, createMissing: true },
+								this
+							)
+								.then(response => {
+									const { trackId, title, username, artworkUrl, duration } = response.track;
+									songMap[mediaSource] = {
+										mediaSource: `soundcloud:${trackId}`,
+										title,
+										artists: [username],
+										thumbnail: artworkUrl,
+										duration
+									};
+								})
+								.catch(err => {
+									MediaModule.log(
+										"ERROR",
+										`Failed to get media in GET_MEDIA_FROM_MEDIA_SOURCES with mediaSource ${mediaSource} and error`,
+										typeof err === "string" ? err : err.message
+									);
+								});
+
+							allPromises.push(promise);
+						});
+
+						Promise.allSettled(allPromises).then(() => {
+							next();
+						});
+					}
+				],
+				err => {
+					if (err && err !== true) return reject(new Error(err));
+					return resolve(songMap);
+				}
+			);
+		});
+	}
+
 	/**
 	 * Remove import job by id from Mongo
 	 *

+ 16 - 2
backend/logic/soundcloud.js

@@ -30,7 +30,13 @@ const soundcloudTrackObjectToMusareTrackObject = soundcloudTrackObject => {
 		user_id: userId,
 		user,
 		track_format: trackFormat,
-		permalink
+		permalink,
+		monetization_model: monetizationModel,
+		policy: policy,
+		streamable,
+		sharing,
+		state,
+		embeddable_by: embeddableBy
 	} = soundcloudTrackObject;
 
 	return {
@@ -50,7 +56,13 @@ const soundcloudTrackObjectToMusareTrackObject = soundcloudTrackObject => {
 		username: user.username,
 		userPermalink: user.permalink,
 		trackFormat,
-		permalink
+		permalink,
+		monetizationModel,
+		policy,
+		streamable,
+		sharing,
+		state,
+		embeddableBy
 	};
 };
 
@@ -372,6 +384,7 @@ class _SoundCloudModule extends CoreClass {
 				],
 				(err, track, existing) => {
 					if (err) reject(new Error(err));
+					else if (track.policy === "SNIP") reject(new Error("Track is premium-only."));
 					else resolve({ track, existing });
 				}
 			);
@@ -438,6 +451,7 @@ class _SoundCloudModule extends CoreClass {
 				],
 				(err, track, existing) => {
 					if (err) reject(new Error(err));
+					else if (track.policy === "SNIP") reject(new Error("Track is premium-only."));
 					else resolve({ track, existing });
 				}
 			);

+ 43 - 2
backend/logic/spotify.js

@@ -9,6 +9,7 @@ import url from "url";
 import CoreClass from "../core";
 
 let SpotifyModule;
+let SoundcloudModule;
 let DBModule;
 let CacheModule;
 let MediaModule;
@@ -83,6 +84,7 @@ class _SpotifyModule extends CoreClass {
 		CacheModule = this.moduleManager.modules.cache;
 		MediaModule = this.moduleManager.modules.media;
 		MusicBrainzModule = this.moduleManager.modules.musicbrainz;
+		SoundcloudModule = this.moduleManager.modules.soundcloud;
 
 		// this.youtubeApiRequestModel = this.YoutubeApiRequestModel = await DBModule.runJob("GET_MODEL", {
 		// 	modelName: "youtubeApiRequest"
@@ -573,6 +575,8 @@ class _SpotifyModule extends CoreClass {
 		const mediaSources = new Set();
 		const mediaSourcesOrigins = {};
 
+		const jobsToRun = [];
+
 		ISRCApiResponse.recordings.forEach(recording => {
 			recording.relations.forEach(relation => {
 				if (relation["target-type"] === "url" && relation.url) {
@@ -580,7 +584,42 @@ class _SpotifyModule extends CoreClass {
 					const { resource } = relation.url;
 
 					if (resource.indexOf("soundcloud.com") !== -1) {
-						throw new Error(`Unable to parse SoundCloud resource ${resource}.`);
+						// throw new Error(`Unable to parse SoundCloud resource ${resource}.`);
+
+						const promise = new Promise(resolve => {
+							SoundcloudModule.runJob(
+								"GET_TRACK_FROM_URL",
+								{ identifier: resource, createMissing: true },
+								this
+							)
+								.then(response => {
+									const { trackId } = response.track;
+									const mediaSource = `soundcloud:${trackId}`;
+
+									const mediaSourceOrigins = [
+										`Spotify track ${spotifyTrackId}`,
+										`ISRC ${ISRC}`,
+										`MusicBrainz recordings`,
+										`MusicBrainz recording ${recording.id}`,
+										`MusicBrainz relations`,
+										`MusicBrainz relation target-type url`,
+										`MusicBrainz relation resource ${resource}`,
+										`SoundCloud ID ${trackId}`
+									];
+
+									mediaSources.add(mediaSource);
+									if (!mediaSourcesOrigins[mediaSource]) mediaSourcesOrigins[mediaSource] = [];
+
+									mediaSourcesOrigins[mediaSource].push(mediaSourceOrigins);
+
+									resolve();
+								})
+								.catch(() => {
+									resolve();
+								});
+						});
+
+						jobsToRun.push(promise);
 
 						return;
 					}
@@ -607,7 +646,7 @@ class _SpotifyModule extends CoreClass {
 						mediaSources.add(mediaSource);
 						if (!mediaSourcesOrigins[mediaSource]) mediaSourcesOrigins[mediaSource] = [];
 
-						mediaSourcesOrigins[mediaSource].push([mediaSourceOrigins]);
+						mediaSourcesOrigins[mediaSource].push(mediaSourceOrigins);
 
 						return;
 					}
@@ -621,6 +660,8 @@ class _SpotifyModule extends CoreClass {
 			});
 		});
 
+		await Promise.allSettled(jobsToRun);
+
 		return {
 			mediaSources: Array.from(mediaSources),
 			mediaSourcesOrigins

Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
frontend/dist/assets/social/soundcloud.svg


+ 1 - 0
frontend/dist/assets/social/spotify.svg

@@ -0,0 +1 @@
+<svg aria-hidden="true" focusable="false" data-icon="spotify" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 168 168"><path fill="currentColor" d="m83.996 0.277c-46.249 0-83.743 37.493-83.743 83.742 0 46.251 37.494 83.741 83.743 83.741 46.254 0 83.744-37.49 83.744-83.741 0-46.246-37.49-83.738-83.745-83.738l0.001-0.004zm38.404 120.78c-1.5 2.46-4.72 3.24-7.18 1.73-19.662-12.01-44.414-14.73-73.564-8.07-2.809 0.64-5.609-1.12-6.249-3.93-0.643-2.81 1.11-5.61 3.926-6.25 31.9-7.291 59.263-4.15 81.337 9.34 2.46 1.51 3.24 4.72 1.73 7.18zm10.25-22.805c-1.89 3.075-5.91 4.045-8.98 2.155-22.51-13.839-56.823-17.846-83.448-9.764-3.453 1.043-7.1-0.903-8.148-4.35-1.04-3.453 0.907-7.093 4.354-8.143 30.413-9.228 68.222-4.758 94.072 11.127 3.07 1.89 4.04 5.91 2.15 8.976v-0.001zm0.88-23.744c-26.99-16.031-71.52-17.505-97.289-9.684-4.138 1.255-8.514-1.081-9.768-5.219-1.254-4.14 1.08-8.513 5.221-9.771 29.581-8.98 78.756-7.245 109.83 11.202 3.73 2.209 4.95 7.016 2.74 10.733-2.2 3.722-7.02 4.949-10.73 2.739z"/></svg>

+ 24 - 9
frontend/src/App.vue

@@ -317,7 +317,8 @@ onMounted(async () => {
 	--dark-grey-3: rgb(34, 34, 34);
 	--dark-grey-4: rgb(26, 26, 26);
 	--youtube: rgb(189, 46, 46);
-	--soundcloud: var(242, 111, 35);
+	--soundcloud: rgb(242, 111, 35);
+	--spotify: rgb(30, 215, 96);
 }
 
 .night-mode {
@@ -845,7 +846,9 @@ img {
 				color: var(--white);
 			}
 
-			.youtube-icon {
+			.youtube-icon,
+			.spotify-icon,
+			.soundcloud-icon {
 				background-color: var(--white);
 			}
 		}
@@ -1774,21 +1777,33 @@ h4.section-title {
 	opacity: 0;
 }
 
-.youtube-icon {
-	margin-right: 3px;
+.youtube-icon,
+.spotify-icon,
+.soundcloud-icon {
 	height: 20px;
+	min-height: 20px;
+	max-height: 20px;
 	width: 20px;
+	min-width: 20px;
+	max-width: 20px;
+}
+
+.youtube-icon {
+	margin-right: 3px;
 	-webkit-mask: url("/assets/social/youtube.svg") no-repeat center;
 	mask: url("/assets/social/youtube.svg") no-repeat center;
 	background-color: var(--youtube);
 }
 
+.spotify-icon {
+	-webkit-mask: url("/assets/social/spotify.svg") no-repeat center;
+	mask: url("/assets/social/spotify.svg") no-repeat center;
+	background-color: var(--spotify);
+}
+
 .soundcloud-icon {
-	margin-right: 3px;
-	height: 20px;
-	width: 20px;
-	-webkit-mask: url("/assets/social/youtube.svg") no-repeat center;
-	mask: url("/assets/social/youtube.svg") no-repeat center;
+	-webkit-mask: url("/assets/social/soundcloud.svg") no-repeat center;
+	mask: url("/assets/social/soundcloud.svg") no-repeat center;
 	background-color: var(--soundcloud);
 }
 

+ 21 - 4
frontend/src/components/SongItem.vue

@@ -164,8 +164,8 @@ onUnmounted(() => {
 		v-if="song"
 	>
 		<div class="thumbnail-and-info">
-			<slot v-if="$slots.leftIcon" name="leftIcon" />
 			<song-thumbnail :song="song" v-if="thumbnail" />
+			<slot v-if="$slots.leftIcon" name="leftIcon" />
 			<div class="song-info">
 				<h6 v-if="header">{{ header }}</h6>
 				<div class="song-title">
@@ -279,6 +279,17 @@ onUnmounted(() => {
 							>
 								<div class="youtube-icon"></div>
 							</i>
+							<!-- <i
+								v-if="
+									disabledActions.indexOf('youtube') === -1 &&
+									songMediaType === 'spotify'
+								"
+								@click="viewYoutubeVideo(songMediaValue)"
+								content="View Spotify Video"
+								v-tippy
+							>
+								<div class="spotify-icon"></div>
+							</i> -->
 							<i
 								v-if="
 									song._id &&
@@ -415,15 +426,21 @@ onUnmounted(() => {
 		position: absolute;
 	}
 
+	:deep(.left-icon) {
+		margin-left: 70px;
+	}
+
+	.song-info:not(:nth-child(3)) {
+		margin-left: 70px;
+	}
+
 	.song-info {
 		display: flex;
 		flex-direction: column;
 		justify-content: center;
-		// margin-left: 20px;
+		margin-left: 10px;
 		min-width: 0;
 
-		margin-left: 70px;
-
 		*:not(i) {
 			margin: 0;
 			font-family: Karla, Arial, sans-serif;

+ 234 - 25
frontend/src/components/modals/ConvertSpotifySongs.vue

@@ -33,6 +33,9 @@ const currentConvertType = ref("track");
 const sortBy = ref("track_count_des");
 
 const spotifyArtists = ref({});
+const alternativeSongsMap = ref(new Map());
+
+const showExtra = ref(false);
 
 // const ISRCMap = ref(new Map());
 // const WikidataSpotifyTrackMap = ref(new Map());
@@ -99,7 +102,78 @@ const getAlternativeMediaSourcesForTrack = mediaSource => {
 		res => {
 			console.log("KRIS111133", res);
 			if (res.status === "success") {
-				AlternativeSourcesForTrackMap.value.set(mediaSource, res.data);
+				AlternativeSourcesForTrackMap.value.set(
+					mediaSource,
+					res.data.alternativeMediaSources
+				);
+				console.log(32211, AlternativeSourcesForTrackMap.value);
+				getMissingAlternativeSongs();
+				// ISRCMap.value.set(ISRC, res.data);
+				// WikidataMusicBrainzWorkMap.value.set(workId, res.data.response);
+			}
+		}
+	);
+};
+
+const loadingMediaSourcesMap = ref(new Map());
+const failedToLoadMediaSourcesMap = ref(new Map());
+
+const getMissingAlternativeSongs = () => {
+	const allAlternativeMediaSources = Array.from(
+		new Set(
+			Array.from(AlternativeSourcesForTrackMap.value.values())
+				.map(t => t.mediaSources)
+				.flat()
+		)
+	);
+	const filteredMediaSources = allAlternativeMediaSources.filter(
+		mediaSource => {
+			const alreadyExists = alternativeSongsMap.value.has(mediaSource);
+			if (alreadyExists) return false;
+			const loading = loadingMediaSourcesMap.value.get(mediaSource);
+			if (loading) return false;
+			const failedToLoad =
+				failedToLoadMediaSourcesMap.value.get(mediaSource);
+			if (failedToLoad) return false;
+			return true;
+		}
+	);
+	console.log(
+		321111145668778,
+		allAlternativeMediaSources,
+		filteredMediaSources
+	);
+	filteredMediaSources.forEach(mediaSource => {
+		loadingMediaSourcesMap.value.set(mediaSource, true);
+	});
+
+	socket.dispatch(
+		"media.getMediaFromMediaSources",
+		filteredMediaSources,
+		res => {
+			console.log("KRIS111136663", res);
+			if (res.status === "success") {
+				const { songMap } = res.data;
+				filteredMediaSources.forEach(mediaSource => {
+					if (songMap[mediaSource]) {
+						alternativeSongsMap.value.set(
+							mediaSource,
+							songMap[mediaSource]
+						);
+					} else {
+						failedToLoadMediaSourcesMap.value.set(
+							mediaSource,
+							true
+						);
+					}
+					loadingMediaSourcesMap.value.delete(mediaSource);
+				});
+				// console.log(657567, );
+				// AlternativeSourcesForTrackMap.value.set(
+				// 	mediaSource,
+				// 	res.data.alternativeMediaSources
+				// );
+				// console.log(32211, AlternativeSourcesForTrackMap.value);
 				// ISRCMap.value.set(ISRC, res.data);
 				// WikidataMusicBrainzWorkMap.value.set(workId, res.data.response);
 			}
@@ -184,6 +258,12 @@ onMounted(() => {
 		>
 			<template #body>
 				<p>Converting by {{ currentConvertType }}</p>
+				<button
+					class="button is-primary"
+					@click="showExtra = !showExtra"
+				>
+					Toggle show extra
+				</button>
 				<!-- <p>Sorting by {{ sortBy }}</p> -->
 
 				<br />
@@ -204,31 +284,37 @@ onMounted(() => {
 						:key="spotifyTrackMediaSource"
 					>
 						<div class="left">
+							<song-item
+								:song="{
+									title: allSongs[spotifyTrackMediaSource]
+										.track.name,
+									duration:
+										allSongs[spotifyTrackMediaSource].track
+											.duration,
+									artists:
+										allSongs[spotifyTrackMediaSource].track
+											.artists,
+									thumbnail:
+										allSongs[spotifyTrackMediaSource].track
+											.albumImageUrl
+								}"
+							>
+								<template #leftIcon>
+									<a
+										:href="`https://open.spotify.com/track/${
+											spotifyTrackMediaSource.split(
+												':'
+											)[1]
+										}`"
+										target="_blank"
+									>
+										<div
+											class="spotify-icon left-icon"
+										></div>
+									</a>
+								</template>
+							</song-item>
 							<p>Media source: {{ spotifyTrackMediaSource }}</p>
-							<p>
-								Name:
-								{{
-									allSongs[spotifyTrackMediaSource].track.name
-								}}
-							</p>
-							<p>Artists:</p>
-							<ul>
-								<li
-									v-for="artist in allSongs[
-										spotifyTrackMediaSource
-									].track.artists"
-									:key="artist"
-								>
-									- {{ artist }}
-								</li>
-							</ul>
-							<p>
-								Duration:
-								{{
-									allSongs[spotifyTrackMediaSource].track
-										.duration
-								}}
-							</p>
 							<p>
 								ISRC:
 								{{
@@ -239,6 +325,11 @@ onMounted(() => {
 						</div>
 						<div class="right">
 							<button
+								v-if="
+									!AlternativeSourcesForTrackMap.has(
+										spotifyTrackMediaSource
+									)
+								"
 								class="button"
 								@click="
 									getAlternativeMediaSourcesForTrack(
@@ -248,6 +339,115 @@ onMounted(() => {
 							>
 								Get alternative media sources
 							</button>
+							<template v-else>
+								<div
+									v-for="[
+										alternativeMediaSource,
+										alternativeMediaSourceOrigins
+									] in Object.entries(
+										AlternativeSourcesForTrackMap.get(
+											spotifyTrackMediaSource
+										).mediaSourcesOrigins
+									)"
+									:key="alternativeMediaSource"
+								>
+									<p
+										v-if="
+											loadingMediaSourcesMap.has(
+												alternativeMediaSource
+											)
+										"
+									>
+										Song {{ alternativeMediaSource }} is
+										loading
+									</p>
+									<p
+										v-else-if="
+											failedToLoadMediaSourcesMap.has(
+												alternativeMediaSource
+											)
+										"
+									>
+										Song {{ alternativeMediaSource }} failed
+										to load
+									</p>
+									<p
+										v-else-if="
+											!alternativeSongsMap.has(
+												alternativeMediaSource
+											)
+										"
+									>
+										Song {{ alternativeMediaSource }} not
+										loaded/found
+									</p>
+									<song-item
+										v-else
+										:song="
+											alternativeSongsMap.get(
+												alternativeMediaSource
+											)
+										"
+									>
+										<template #leftIcon>
+											<a
+												v-if="
+													alternativeMediaSource.split(
+														':'
+													)[0] === 'youtube'
+												"
+												:href="`https://youtu.be/${
+													alternativeMediaSource.split(
+														':'
+													)[1]
+												}`"
+												target="_blank"
+											>
+												<div
+													class="youtube-icon left-icon"
+												></div>
+											</a>
+											<a
+												v-if="
+													alternativeMediaSource.split(
+														':'
+													)[0] === 'soundcloud'
+												"
+												target="_blank"
+											>
+												<div
+													class="soundcloud-icon left-icon"
+												></div>
+											</a>
+										</template>
+									</song-item>
+									<ul v-if="showExtra">
+										<li
+											v-for="origin in alternativeMediaSourceOrigins"
+											:key="
+												spotifyTrackMediaSource +
+												alternativeMediaSource +
+												origin
+											"
+										>
+											=
+											<ul>
+												<li
+													v-for="originItem in origin"
+													:key="
+														spotifyTrackMediaSource +
+														alternativeMediaSource +
+														origin +
+														originItem
+													"
+												>
+													+ {{ originItem }}
+												</li>
+											</ul>
+										</li>
+									</ul>
+								</div>
+							</template>
 							<!-- <button
 								class="button"
 								v-if="
@@ -408,6 +608,12 @@ onMounted(() => {
 </template>
 
 <style lang="less" scoped>
+:deep(.song-item) {
+	.left-icon {
+		cursor: pointer;
+	}
+}
+
 .tracks {
 	display: flex;
 	flex-direction: column;
@@ -418,6 +624,9 @@ onMounted(() => {
 			padding: 8px;
 			width: 50%;
 			box-shadow: inset 0px 0px 1px white;
+			display: flex;
+			flex-direction: column;
+			row-gap: 8px;
 		}
 	}
 }

+ 2 - 1
frontend/src/components/modals/EditSong/index.vue

@@ -3176,7 +3176,8 @@ onBeforeUnmount(() => {
 			padding-right: 4px;
 
 			.youtube-icon,
-			.soundcloud-icon {
+			.soundcloud-icon,
+			.spotify-icon {
 				background: var(--white);
 			}
 		}

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff