Browse Source

refactor: changed youtubeId to mediaSource in most places

Kristian Vos 1 year ago
parent
commit
654f736bfa
51 changed files with 1059 additions and 921 deletions
  1. 69 69
      backend/logic/actions/media.js
  2. 81 72
      backend/logic/actions/playlists.js
  3. 8 8
      backend/logic/actions/reports.js
  4. 49 39
      backend/logic/actions/songs.js
  5. 18 18
      backend/logic/actions/stations.js
  6. 8 8
      backend/logic/actions/users.js
  7. 3 3
      backend/logic/actions/youtube.js
  8. 6 6
      backend/logic/activities.js
  9. 12 7
      backend/logic/db/index.js
  10. 2 2
      backend/logic/db/schemas/activity.js
  11. 2 2
      backend/logic/db/schemas/playlist.js
  12. 2 0
      backend/logic/db/schemas/queueSong.js
  13. 2 2
      backend/logic/db/schemas/ratings.js
  14. 2 2
      backend/logic/db/schemas/report.js
  15. 2 2
      backend/logic/db/schemas/song.js
  16. 3 3
      backend/logic/db/schemas/station.js
  17. 2 2
      backend/logic/db/schemas/stationHistory.js
  18. 54 40
      backend/logic/media.js
  19. 47 47
      backend/logic/playlists.js
  20. 60 40
      backend/logic/songs.js
  21. 51 40
      backend/logic/stations.js
  22. 5 4
      backend/logic/youtube.js
  23. 4 4
      frontend/src/components/ActivityItem.vue
  24. 6 5
      frontend/src/components/AddToPlaylistDropdown.vue
  25. 7 7
      frontend/src/components/PlaylistTabBase.vue
  26. 5 5
      frontend/src/components/Queue.vue
  27. 9 7
      frontend/src/components/Request.vue
  28. 5 5
      frontend/src/components/SongItem.vue
  29. 13 3
      frontend/src/components/SongThumbnail.vue
  30. 3 3
      frontend/src/components/modals/BulkEditPlaylist.vue
  31. 9 8
      frontend/src/components/modals/EditPlaylist/Tabs/ImportPlaylists.vue
  32. 7 7
      frontend/src/components/modals/EditPlaylist/index.vue
  33. 84 67
      frontend/src/components/modals/EditSong/index.vue
  34. 32 24
      frontend/src/components/modals/ImportAlbum.vue
  35. 4 3
      frontend/src/components/modals/ManageStation/index.vue
  36. 1 1
      frontend/src/components/modals/Report.vue
  37. 212 223
      frontend/src/components/modals/ViewYoutubeVideo.vue
  38. 1 1
      frontend/src/composables/useSortablePlaylists.ts
  39. 1 1
      frontend/src/composables/useYoutubeDirect.ts
  40. 5 5
      frontend/src/pages/Admin/Reports.vue
  41. 14 7
      frontend/src/pages/Admin/Songs/Import.vue
  42. 13 13
      frontend/src/pages/Admin/Songs/index.vue
  43. 5 4
      frontend/src/pages/Admin/YouTube/Videos.vue
  44. 7 7
      frontend/src/pages/Station/Sidebar/History.vue
  45. 99 73
      frontend/src/pages/Station/index.vue
  46. 4 4
      frontend/src/stores/editPlaylist.ts
  47. 16 14
      frontend/src/stores/editSong.ts
  48. 2 1
      frontend/src/stores/manageStation.ts
  49. 1 1
      frontend/src/stores/station.ts
  50. 1 1
      frontend/src/types/song.ts
  51. 1 1
      types/models/Playlist.ts

+ 69 - 69
backend/logic/actions/media.js

@@ -18,11 +18,11 @@ CacheModule.runJob("SUB", {
 	channel: "ratings.like",
 	cb: data => {
 		WSModule.runJob("EMIT_TO_ROOM", {
-			room: `song.${data.youtubeId}`,
+			room: `song.${data.mediaSource}`,
 			args: [
 				"event:ratings.liked",
 				{
-					data: { youtubeId: data.youtubeId, likes: data.likes, dislikes: data.dislikes }
+					data: { mediaSource: data.mediaSource, likes: data.likes, dislikes: data.dislikes }
 				}
 			]
 		});
@@ -31,7 +31,7 @@ CacheModule.runJob("SUB", {
 			sockets.forEach(socket => {
 				socket.dispatch("event:ratings.updated", {
 					data: {
-						youtubeId: data.youtubeId,
+						mediaSource: data.mediaSource,
 						liked: true,
 						disliked: false
 					}
@@ -45,11 +45,11 @@ CacheModule.runJob("SUB", {
 	channel: "ratings.dislike",
 	cb: data => {
 		WSModule.runJob("EMIT_TO_ROOM", {
-			room: `song.${data.youtubeId}`,
+			room: `song.${data.mediaSource}`,
 			args: [
 				"event:ratings.disliked",
 				{
-					data: { youtubeId: data.youtubeId, likes: data.likes, dislikes: data.dislikes }
+					data: { mediaSource: data.mediaSource, likes: data.likes, dislikes: data.dislikes }
 				}
 			]
 		});
@@ -58,7 +58,7 @@ CacheModule.runJob("SUB", {
 			sockets.forEach(socket => {
 				socket.dispatch("event:ratings.updated", {
 					data: {
-						youtubeId: data.youtubeId,
+						mediaSource: data.mediaSource,
 						liked: false,
 						disliked: true
 					}
@@ -72,11 +72,11 @@ CacheModule.runJob("SUB", {
 	channel: "ratings.unlike",
 	cb: data => {
 		WSModule.runJob("EMIT_TO_ROOM", {
-			room: `song.${data.youtubeId}`,
+			room: `song.${data.mediaSource}`,
 			args: [
 				"event:ratings.unliked",
 				{
-					data: { youtubeId: data.youtubeId, likes: data.likes, dislikes: data.dislikes }
+					data: { mediaSource: data.mediaSource, likes: data.likes, dislikes: data.dislikes }
 				}
 			]
 		});
@@ -85,7 +85,7 @@ CacheModule.runJob("SUB", {
 			sockets.forEach(socket => {
 				socket.dispatch("event:ratings.updated", {
 					data: {
-						youtubeId: data.youtubeId,
+						mediaSource: data.mediaSource,
 						liked: false,
 						disliked: false
 					}
@@ -99,11 +99,11 @@ CacheModule.runJob("SUB", {
 	channel: "ratings.undislike",
 	cb: data => {
 		WSModule.runJob("EMIT_TO_ROOM", {
-			room: `song.${data.youtubeId}`,
+			room: `song.${data.mediaSource}`,
 			args: [
 				"event:ratings.undisliked",
 				{
-					data: { youtubeId: data.youtubeId, likes: data.likes, dislikes: data.dislikes }
+					data: { mediaSource: data.mediaSource, likes: data.likes, dislikes: data.dislikes }
 				}
 			]
 		});
@@ -112,7 +112,7 @@ CacheModule.runJob("SUB", {
 			sockets.forEach(socket => {
 				socket.dispatch("event:ratings.updated", {
 					data: {
-						youtubeId: data.youtubeId,
+						mediaSource: data.mediaSource,
 						liked: false,
 						disliked: false
 					}
@@ -190,10 +190,10 @@ export default {
 	 * Like
 	 *
 	 * @param session
-	 * @param youtubeId - the youtube id
+	 * @param mediaSource - the media source
 	 * @param cb
 	 */
-	like: isLoginRequired(async function like(session, youtubeId, cb) {
+	like: isLoginRequired(async function like(session, mediaSource, cb) {
 		const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
 
 		async.waterfall(
@@ -202,7 +202,7 @@ export default {
 					MediaModule.runJob(
 						"GET_MEDIA",
 						{
-							youtubeId
+							mediaSource
 						},
 						this
 					)
@@ -211,7 +211,7 @@ export default {
 							const { _id, title, artists, thumbnail, duration, verified } = song;
 							next(null, {
 								_id,
-								youtubeId,
+								mediaSource,
 								title,
 								artists,
 								thumbnail,
@@ -234,7 +234,7 @@ export default {
 								session,
 								namespace: "playlists",
 								action: "removeSongFromPlaylist",
-								args: [youtubeId, user.dislikedSongsPlaylist]
+								args: [mediaSource, user.dislikedSongsPlaylist]
 							},
 							this
 						)
@@ -254,7 +254,7 @@ export default {
 								session,
 								namespace: "playlists",
 								action: "addSongToPlaylist",
-								args: [false, youtubeId, likedSongsPlaylist]
+								args: [false, mediaSource, likedSongsPlaylist]
 							},
 							this
 						)
@@ -266,7 +266,7 @@ export default {
 						}),
 
 				(song, next) => {
-					MediaModule.runJob("RECALCULATE_RATINGS", { youtubeId })
+					MediaModule.runJob("RECALCULATE_RATINGS", { mediaSource })
 						.then(ratings => next(null, song, ratings))
 						.catch(err => next(err));
 				}
@@ -277,7 +277,7 @@ export default {
 					this.log(
 						"ERROR",
 						"MEDIA_RATINGS_LIKE",
-						`User "${session.userId}" failed to like song ${youtubeId}. "${err}"`
+						`User "${session.userId}" failed to like song ${mediaSource}. "${err}"`
 					);
 					return cb({ status: "error", message: err });
 				}
@@ -289,7 +289,7 @@ export default {
 				CacheModule.runJob("PUB", {
 					channel: "ratings.like",
 					value: JSON.stringify({
-						youtubeId,
+						mediaSource,
 						userId: session.userId,
 						likes,
 						dislikes
@@ -300,8 +300,8 @@ export default {
 					userId: session.userId,
 					type: "song__like",
 					payload: {
-						message: `Liked song <youtubeId>${song.title} by ${song.artists.join(", ")}</youtubeId>`,
-						youtubeId,
+						message: `Liked song <mediaSource>${song.title} by ${song.artists.join(", ")}</mediaSource>`,
+						mediaSource,
 						thumbnail: song.thumbnail
 					}
 				});
@@ -318,10 +318,10 @@ export default {
 	 * Dislike
 	 *
 	 * @param session
-	 * @param youtubeId - the youtube id
+	 * @param mediaSource - the media source
 	 * @param cb
 	 */
-	dislike: isLoginRequired(async function dislike(session, youtubeId, cb) {
+	dislike: isLoginRequired(async function dislike(session, mediaSource, cb) {
 		const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
 
 		async.waterfall(
@@ -330,7 +330,7 @@ export default {
 					MediaModule.runJob(
 						"GET_MEDIA",
 						{
-							youtubeId
+							mediaSource
 						},
 						this
 					)
@@ -339,7 +339,7 @@ export default {
 							const { _id, title, artists, thumbnail, duration, verified } = song;
 							next(null, {
 								_id,
-								youtubeId,
+								mediaSource,
 								title,
 								artists,
 								thumbnail,
@@ -362,7 +362,7 @@ export default {
 								session,
 								namespace: "playlists",
 								action: "removeSongFromPlaylist",
-								args: [youtubeId, user.likedSongsPlaylist]
+								args: [mediaSource, user.likedSongsPlaylist]
 							},
 							this
 						)
@@ -382,7 +382,7 @@ export default {
 								session,
 								namespace: "playlists",
 								action: "addSongToPlaylist",
-								args: [false, youtubeId, dislikedSongsPlaylist]
+								args: [false, mediaSource, dislikedSongsPlaylist]
 							},
 							this
 						)
@@ -394,7 +394,7 @@ export default {
 						}),
 
 				(song, next) => {
-					MediaModule.runJob("RECALCULATE_RATINGS", { youtubeId })
+					MediaModule.runJob("RECALCULATE_RATINGS", { mediaSource })
 						.then(ratings => next(null, song, ratings))
 						.catch(err => next(err));
 				}
@@ -405,7 +405,7 @@ export default {
 					this.log(
 						"ERROR",
 						"MEDIA_RATINGS_DISLIKE",
-						`User "${session.userId}" failed to dislike song ${youtubeId}. "${err}"`
+						`User "${session.userId}" failed to dislike song ${mediaSource}. "${err}"`
 					);
 					return cb({ status: "error", message: err });
 				}
@@ -417,7 +417,7 @@ export default {
 				CacheModule.runJob("PUB", {
 					channel: "ratings.dislike",
 					value: JSON.stringify({
-						youtubeId,
+						mediaSource,
 						userId: session.userId,
 						likes,
 						dislikes
@@ -428,8 +428,8 @@ export default {
 					userId: session.userId,
 					type: "song__dislike",
 					payload: {
-						message: `Disliked song <youtubeId>${song.title} by ${song.artists.join(", ")}</youtubeId>`,
-						youtubeId,
+						message: `Disliked song <mediaSource>${song.title} by ${song.artists.join(", ")}</mediaSource>`,
+						mediaSource,
 						thumbnail: song.thumbnail
 					}
 				});
@@ -446,10 +446,10 @@ export default {
 	 * Undislike
 	 *
 	 * @param session
-	 * @param youtubeId - the youtube id
+	 * @param mediaSource - the media source
 	 * @param cb
 	 */
-	undislike: isLoginRequired(async function undislike(session, youtubeId, cb) {
+	undislike: isLoginRequired(async function undislike(session, mediaSource, cb) {
 		const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
 
 		async.waterfall(
@@ -458,7 +458,7 @@ export default {
 					MediaModule.runJob(
 						"GET_MEDIA",
 						{
-							youtubeId
+							mediaSource
 						},
 						this
 					)
@@ -467,7 +467,7 @@ export default {
 							const { _id, title, artists, thumbnail, duration, verified } = song;
 							next(null, {
 								_id,
-								youtubeId,
+								mediaSource,
 								title,
 								artists,
 								thumbnail,
@@ -490,7 +490,7 @@ export default {
 								session,
 								namespace: "playlists",
 								action: "removeSongFromPlaylist",
-								args: [youtubeId, user.dislikedSongsPlaylist]
+								args: [mediaSource, user.dislikedSongsPlaylist]
 							},
 							this
 						)
@@ -510,7 +510,7 @@ export default {
 								session,
 								namespace: "playlists",
 								action: "removeSongFromPlaylist",
-								args: [youtubeId, likedSongsPlaylist]
+								args: [mediaSource, likedSongsPlaylist]
 							},
 							this
 						)
@@ -523,7 +523,7 @@ export default {
 				},
 
 				(song, next) => {
-					MediaModule.runJob("RECALCULATE_RATINGS", { youtubeId })
+					MediaModule.runJob("RECALCULATE_RATINGS", { mediaSource })
 						.then(ratings => next(null, song, ratings))
 						.catch(err => next(err));
 				}
@@ -534,7 +534,7 @@ export default {
 					this.log(
 						"ERROR",
 						"MEDIA_RATINGS_UNDISLIKE",
-						`User "${session.userId}" failed to undislike song ${youtubeId}. "${err}"`
+						`User "${session.userId}" failed to undislike song ${mediaSource}. "${err}"`
 					);
 					return cb({ status: "error", message: err });
 				}
@@ -546,7 +546,7 @@ export default {
 				CacheModule.runJob("PUB", {
 					channel: "ratings.undislike",
 					value: JSON.stringify({
-						youtubeId,
+						mediaSource,
 						userId: session.userId,
 						likes,
 						dislikes
@@ -557,10 +557,10 @@ export default {
 					userId: session.userId,
 					type: "song__undislike",
 					payload: {
-						message: `Removed <youtubeId>${song.title} by ${song.artists.join(
+						message: `Removed <mediaSource>${song.title} by ${song.artists.join(
 							", "
-						)}</youtubeId> from your Disliked Songs`,
-						youtubeId,
+						)}</mediaSource> from your Disliked Songs`,
+						mediaSource,
 						thumbnail: song.thumbnail
 					}
 				});
@@ -577,10 +577,10 @@ export default {
 	 * Unlike
 	 *
 	 * @param session
-	 * @param youtubeId - the youtube id
+	 * @param mediaSource - the media source
 	 * @param cb
 	 */
-	unlike: isLoginRequired(async function unlike(session, youtubeId, cb) {
+	unlike: isLoginRequired(async function unlike(session, mediaSource, cb) {
 		const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
 
 		async.waterfall(
@@ -589,7 +589,7 @@ export default {
 					MediaModule.runJob(
 						"GET_MEDIA",
 						{
-							youtubeId
+							mediaSource
 						},
 						this
 					)
@@ -598,7 +598,7 @@ export default {
 							const { _id, title, artists, thumbnail, duration, verified } = song;
 							next(null, {
 								_id,
-								youtubeId,
+								mediaSource,
 								title,
 								artists,
 								thumbnail,
@@ -621,7 +621,7 @@ export default {
 								session,
 								namespace: "playlists",
 								action: "removeSongFromPlaylist",
-								args: [youtubeId, user.dislikedSongsPlaylist]
+								args: [mediaSource, user.dislikedSongsPlaylist]
 							},
 							this
 						)
@@ -641,7 +641,7 @@ export default {
 								session,
 								namespace: "playlists",
 								action: "removeSongFromPlaylist",
-								args: [youtubeId, likedSongsPlaylist]
+								args: [mediaSource, likedSongsPlaylist]
 							},
 							this
 						)
@@ -654,7 +654,7 @@ export default {
 				},
 
 				(song, next) => {
-					MediaModule.runJob("RECALCULATE_RATINGS", { youtubeId })
+					MediaModule.runJob("RECALCULATE_RATINGS", { mediaSource })
 						.then(ratings => next(null, song, ratings))
 						.catch(err => next(err));
 				}
@@ -665,7 +665,7 @@ export default {
 					this.log(
 						"ERROR",
 						"MEDIA_RATINGS_UNLIKE",
-						`User "${session.userId}" failed to unlike song ${youtubeId}. "${err}"`
+						`User "${session.userId}" failed to unlike song ${mediaSource}. "${err}"`
 					);
 					return cb({ status: "error", message: err });
 				}
@@ -677,7 +677,7 @@ export default {
 				CacheModule.runJob("PUB", {
 					channel: "ratings.unlike",
 					value: JSON.stringify({
-						youtubeId,
+						mediaSource,
 						userId: session.userId,
 						likes,
 						dislikes
@@ -688,10 +688,10 @@ export default {
 					userId: session.userId,
 					type: "song__unlike",
 					payload: {
-						message: `Removed <youtubeId>${song.title} by ${song.artists.join(
+						message: `Removed <mediaSource>${song.title} by ${song.artists.join(
 							", "
-						)}</youtubeId> from your Liked Songs`,
-						youtubeId,
+						)}</mediaSource> from your Liked Songs`,
+						mediaSource,
 						thumbnail: song.thumbnail
 					}
 				});
@@ -708,15 +708,15 @@ export default {
 	 * Get ratings
 	 *
 	 * @param session
-	 * @param youtubeId - the youtube id
+	 * @param mediaSource - the media source
 	 * @param cb
 	 */
 
-	async getRatings(session, youtubeId, cb) {
+	async getRatings(session, mediaSource, cb) {
 		async.waterfall(
 			[
 				next => {
-					MediaModule.runJob("GET_RATINGS", { youtubeId, createMissing: true }, this)
+					MediaModule.runJob("GET_RATINGS", { mediaSource, createMissing: true }, this)
 						.then(res => next(null, res.ratings))
 						.catch(next);
 				},
@@ -734,7 +734,7 @@ export default {
 					this.log(
 						"ERROR",
 						"MEDIA_GET_RATINGS",
-						`User "${session.userId}" failed to get ratings for ${youtubeId}. "${err}"`
+						`User "${session.userId}" failed to get ratings for ${mediaSource}. "${err}"`
 					);
 					return cb({ status: "error", message: err });
 				}
@@ -756,10 +756,10 @@ export default {
 	 * Gets user's own ratings
 	 *
 	 * @param session
-	 * @param youtubeId - the youtube id
+	 * @param mediaSource - the media source
 	 * @param cb
 	 */
-	getOwnRatings: isLoginRequired(async function getOwnRatings(session, youtubeId, cb) {
+	getOwnRatings: isLoginRequired(async function getOwnRatings(session, mediaSource, cb) {
 		const playlistModel = await DBModule.runJob("GET_MODEL", { modelName: "playlist" }, this);
 
 		async.waterfall(
@@ -768,7 +768,7 @@ export default {
 					MediaModule.runJob(
 						"GET_MEDIA",
 						{
-							youtubeId
+							mediaSource
 						},
 						this
 					)
@@ -787,7 +787,7 @@ export default {
 
 							Object.values(playlist.songs).forEach(song => {
 								// song is found in 'liked songs' playlist
-								if (song.youtubeId === youtubeId) isLiked = true;
+								if (song.mediaSource === mediaSource) isLiked = true;
 							});
 
 							return next(null, isLiked);
@@ -805,7 +805,7 @@ export default {
 
 							Object.values(playlist.songs).forEach(song => {
 								// song is found in 'disliked songs' playlist
-								if (song.youtubeId === youtubeId) ratings.isDisliked = true;
+								if (song.mediaSource === mediaSource) ratings.isDisliked = true;
 							});
 
 							return next(null, ratings);
@@ -818,7 +818,7 @@ export default {
 					this.log(
 						"ERROR",
 						"MEDIA_GET_OWN_RATINGS",
-						`User "${session.userId}" failed to get ratings for ${youtubeId}. "${err}"`
+						`User "${session.userId}" failed to get ratings for ${mediaSource}. "${err}"`
 					);
 					return cb({ status: "error", message: err });
 				}
@@ -828,7 +828,7 @@ export default {
 				return cb({
 					status: "success",
 					data: {
-						youtubeId,
+						mediaSource,
 						liked: isLiked,
 						disliked: isDisliked
 					}

+ 81 - 72
backend/logic/actions/playlists.js

@@ -125,7 +125,7 @@ CacheModule.runJob("SUB", {
 					socket.dispatch("event:playlist.song.removed", {
 						data: {
 							playlistId: res.playlistId,
-							youtubeId: res.youtubeId
+							mediaSource: res.mediaSource
 						}
 					});
 				});
@@ -139,7 +139,7 @@ CacheModule.runJob("SUB", {
 						{
 							data: {
 								playlistId: res.playlistId,
-								youtubeId: res.youtubeId
+								mediaSource: res.mediaSource
 							}
 						}
 					]
@@ -150,7 +150,7 @@ CacheModule.runJob("SUB", {
 			room: "admin.playlists",
 			args: [
 				"event:admin.playlist.song.removed",
-				{ data: { playlistId: res.playlistId, youtubeId: res.youtubeId } }
+				{ data: { playlistId: res.playlistId, mediaSource: res.mediaSource } }
 			]
 		});
 	}
@@ -1046,7 +1046,7 @@ export default {
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} playlistId - the id of the playlist we are targeting
 	 * @param {object} song - the song to be repositioned
-	 * @param {string} song.youtubeId - the youtube id of the song being repositioned
+	 * @param {string} song.mediaSource - the media source of the song being repositioned
 	 * @param {string} song.newIndex - the new position of the song in the playlist
 	 * @param {...any} song.args - any other elements that would be included with a song item in a playlist
 	 * @param {Function} cb - gets called with the result
@@ -1058,7 +1058,7 @@ export default {
 			[
 				next => {
 					if (!playlistId) return next("Please provide a playlist.");
-					if (!song || !song.youtubeId) return next("You must provide a song to reposition.");
+					if (!song || !song.mediaSource) return next("You must provide a song to reposition.");
 					return next();
 				},
 
@@ -1079,7 +1079,7 @@ export default {
 				next => {
 					playlistModel.updateOne(
 						{ _id: playlistId },
-						{ $pull: { songs: { youtubeId: song.youtubeId } } },
+						{ $pull: { songs: { mediaSource: song.mediaSource } } },
 						next
 					);
 				},
@@ -1107,7 +1107,7 @@ export default {
 					this.log(
 						"ERROR",
 						"PLAYLIST_REPOSITION_SONG",
-						`Repositioning song ${song.youtubeId}  for private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
+						`Repositioning song ${song.mediaSource}  for private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
 					);
 
 					return cb({ status: "error", message: err });
@@ -1116,7 +1116,7 @@ export default {
 				this.log(
 					"SUCCESS",
 					"PLAYLIST_REPOSITION_SONG",
-					`Successfully repositioned song ${song.youtubeId} for private playlist "${playlistId}" for user "${session.userId}".`
+					`Successfully repositioned song ${song.mediaSource} for private playlist "${playlistId}" for user "${session.userId}".`
 				);
 
 				CacheModule.runJob("PUB", {
@@ -1141,11 +1141,11 @@ export default {
 	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {boolean} isSet - is the song part of a set of songs to be added
-	 * @param {string} youtubeId - the youtube id of the song we are trying to add
+	 * @param {string} mediaSource - the media source of the song we are trying to add
 	 * @param {string} playlistId - the id of the playlist we are adding the song to
 	 * @param {Function} cb - gets called with the result
 	 */
-	addSongToPlaylist: isLoginRequired(async function addSongToPlaylist(session, isSet, youtubeId, playlistId, cb) {
+	addSongToPlaylist: isLoginRequired(async function addSongToPlaylist(session, isSet, mediaSource, playlistId, cb) {
 		const playlistModel = await DBModule.runJob("GET_MODEL", { modelName: "playlist" }, this);
 
 		async.waterfall(
@@ -1168,7 +1168,7 @@ export default {
 						const oppositeType = playlist.type === "user-liked" ? "user-disliked" : "user-liked";
 						const oppositePlaylistName = oppositeType === "user-liked" ? "Liked Songs" : "Disliked Songs";
 						playlistModel.count(
-							{ type: oppositeType, createdBy: session.userId, "songs.youtubeId": youtubeId },
+							{ type: oppositeType, createdBy: session.userId, "songs.mediaSource": mediaSource },
 							(err, results) => {
 								if (err) next(err);
 								else if (results > 0)
@@ -1182,7 +1182,7 @@ export default {
 				},
 
 				next => {
-					PlaylistsModule.runJob("ADD_SONG_TO_PLAYLIST", { playlistId, youtubeId }, this)
+					PlaylistsModule.runJob("ADD_SONG_TO_PLAYLIST", { playlistId, mediaSource }, this)
 						.then(res => {
 							const { playlist, song, ratings } = res;
 							next(null, playlist, song, ratings);
@@ -1196,7 +1196,7 @@ export default {
 					this.log(
 						"ERROR",
 						"PLAYLIST_ADD_SONG",
-						`Adding song "${youtubeId}" to private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
+						`Adding song "${mediaSource}" to private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
 					);
 					return cb({ status: "error", message: err });
 				}
@@ -1204,7 +1204,7 @@ export default {
 				this.log(
 					"SUCCESS",
 					"PLAYLIST_ADD_SONG",
-					`Successfully added song "${youtubeId}" to private playlist "${playlistId}" for user "${session.userId}".`
+					`Successfully added song "${mediaSource}" to private playlist "${playlistId}" for user "${session.userId}".`
 				);
 
 				if (!isSet && playlist.type === "user" && playlist.privacy === "public") {
@@ -1216,10 +1216,10 @@ export default {
 						userId: session.userId,
 						type: "playlist__add_song",
 						payload: {
-							message: `Added <youtubeId>${songName}</youtubeId> to playlist <playlistId>${playlist.displayName}</playlistId>`,
+							message: `Added <mediaSource>${songName}</mediaSource> to playlist <playlistId>${playlist.displayName}</playlistId>`,
 							thumbnail: newSong.thumbnail,
 							playlistId,
-							youtubeId
+							mediaSource
 						}
 					});
 				}
@@ -1240,7 +1240,7 @@ export default {
 				});
 
 				if (ratings && (playlist.type === "user-liked" || playlist.type === "user-disliked")) {
-					const { _id, youtubeId, title, artists, thumbnail } = newSong;
+					const { _id, mediaSource, title, artists, thumbnail } = newSong;
 					const { likes, dislikes } = ratings;
 
 					if (_id) SongsModule.runJob("UPDATE_SONG", { songId: _id });
@@ -1249,7 +1249,7 @@ export default {
 						CacheModule.runJob("PUB", {
 							channel: "ratings.like",
 							value: JSON.stringify({
-								youtubeId,
+								mediaSource,
 								userId: session.userId,
 								likes,
 								dislikes
@@ -1260,8 +1260,8 @@ export default {
 							userId: session.userId,
 							type: "song__like",
 							payload: {
-								message: `Liked song <youtubeId>${title} by ${artists.join(", ")}</youtubeId>`,
-								youtubeId,
+								message: `Liked song <mediaSource>${title} by ${artists.join(", ")}</mediaSource>`,
+								mediaSource,
 								thumbnail
 							}
 						});
@@ -1269,7 +1269,7 @@ export default {
 						CacheModule.runJob("PUB", {
 							channel: "ratings.dislike",
 							value: JSON.stringify({
-								youtubeId,
+								mediaSource,
 								userId: session.userId,
 								likes,
 								dislikes
@@ -1280,8 +1280,10 @@ export default {
 							userId: session.userId,
 							type: "song__dislike",
 							payload: {
-								message: `Disliked song <youtubeId>${title} by ${artists.join(", ")}</youtubeId>`,
-								youtubeId,
+								message: `Disliked song <mediaSource>${title} by ${artists.join(
+									mediaSource
+								)}</mediaSource>`,
+								mediaSource,
 								thumbnail
 							}
 						});
@@ -1302,10 +1304,10 @@ export default {
 	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} playlistId - the id of the playlist we are adding the songs to
-	 * @param {Array} youtubeIds - the YouTube ids of the songs we are trying to add
+	 * @param {Array} mediaSources - the media sources of the songs we are trying to add
 	 * @param {Function} cb - gets called with the result
 	 */
-	addSongsToPlaylist: isLoginRequired(async function addSongsToPlaylist(session, playlistId, youtubeIds, cb) {
+	addSongsToPlaylist: isLoginRequired(async function addSongsToPlaylist(session, playlistId, mediaSources, cb) {
 		const successful = [];
 		const existing = [];
 		const failed = {};
@@ -1358,24 +1360,24 @@ export default {
 
 				(nothing, next) => {
 					async.eachLimit(
-						youtubeIds,
+						mediaSources,
 						1,
-						(youtubeId, next) => {
-							this.publishProgress({ status: "update", message: `Adding song "${youtubeId}"` });
-							PlaylistsModule.runJob("ADD_SONG_TO_PLAYLIST", { playlistId, youtubeId }, this)
+						(mediaSource, next) => {
+							this.publishProgress({ status: "update", message: `Adding song "${mediaSource}"` });
+							PlaylistsModule.runJob("ADD_SONG_TO_PLAYLIST", { playlistId, mediaSource }, this)
 								.then(({ song }) => {
-									successful.push(youtubeId);
+									successful.push(mediaSource);
 									songsAdded.push(song);
 									next();
 								})
 								.catch(async err => {
 									err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
 									if (err === "That song is already in the playlist.") {
-										existing.push(youtubeId);
+										existing.push(mediaSource);
 										next();
 									} else {
 										addError(err);
-										failed[youtubeId] = err;
+										failed[mediaSource] = err;
 										next();
 									}
 								});
@@ -1406,7 +1408,9 @@ export default {
 							session.userId
 						}". "${err}". Stats: successful:${successful.length}, existing:${existing.length}, failed:${
 							Object.keys(failed).length
-						}, last youtubeId:${lastYoutubeId}, youtubeIds length:${youtubeIds ? youtubeIds.length : null}`
+						}, last mediaSource:${lastYoutubeId}, mediaSources length:${
+							mediaSources ? mediaSources.length : null
+						}`
 					);
 					return cb({
 						status: "error",
@@ -1429,7 +1433,7 @@ export default {
 						session.userId
 					}". Stats: successful:${successful.length}, existing:${existing.length}, failed:${
 						Object.keys(failed).length
-					}, youtubeIds length:${youtubeIds ? youtubeIds.length : null}`
+					}, mediaSources length:${mediaSources ? mediaSources.length : null}`
 				);
 
 				await async.eachLimit(songsAdded, 1, (song, next) => {
@@ -1481,12 +1485,12 @@ export default {
 	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} playlistId - the id of the playlist we are removing the songs from
-	 * @param {Array} youtubeIds - the YouTube ids of the songs we are trying to remove
+	 * @param {Array} mediaSources - the media sources of the songs we are trying to remove
 	 * @param {Function} cb - gets called with the result
 	 */
 	removeSongsFromPlaylist: useHasPermission(
 		"playlists.songs.remove",
-		async function removeSongsFromPlaylist(session, playlistId, youtubeIds, cb) {
+		async function removeSongsFromPlaylist(session, playlistId, mediaSources, cb) {
 			const successful = [];
 			const notInPlaylist = [];
 			const failed = {};
@@ -1533,23 +1537,23 @@ export default {
 
 					next => {
 						async.eachLimit(
-							youtubeIds,
+							mediaSources,
 							1,
-							(youtubeId, next) => {
-								this.publishProgress({ status: "update", message: `Removing song "${youtubeId}"` });
-								PlaylistsModule.runJob("REMOVE_FROM_PLAYLIST", { playlistId, youtubeId }, this)
+							(mediaSource, next) => {
+								this.publishProgress({ status: "update", message: `Removing song "${mediaSource}"` });
+								PlaylistsModule.runJob("REMOVE_FROM_PLAYLIST", { playlistId, mediaSource }, this)
 									.then(() => {
-										successful.push(youtubeId);
+										successful.push(mediaSource);
 										next();
 									})
 									.catch(async err => {
 										err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
 										if (err === "That song is not currently in the playlist.") {
-											notInPlaylist.push(youtubeId);
+											notInPlaylist.push(mediaSource);
 											next();
 										} else {
 											addError(err);
-											failed[youtubeId] = err;
+											failed[mediaSource] = err;
 											next();
 										}
 									});
@@ -1582,8 +1586,8 @@ export default {
 								notInPlaylist.length
 							}, failed:${
 								Object.keys(failed).length
-							}, last youtubeId:${lastYoutubeId}, youtubeIds length:${
-								youtubeIds ? youtubeIds.length : null
+							}, last mediaSource:${lastYoutubeId}, mediaSources length:${
+								mediaSources ? mediaSources.length : null
 							}`
 						);
 						return cb({
@@ -1607,7 +1611,7 @@ export default {
 							session.userId
 						}". Stats: successful:${successful.length}, notInPlaylist:${notInPlaylist.length}, failed:${
 							Object.keys(failed).length
-						}, youtubeIds length:${youtubeIds ? youtubeIds.length : null}`
+						}, mediaSources length:${mediaSources ? mediaSources.length : null}`
 					);
 
 					CacheModule.runJob("PUB", {
@@ -1714,7 +1718,7 @@ export default {
 							.catch(next);
 					else next("Invalid YouTube URL.");
 				},
-				(youtubeIds, next) => {
+				(mediaSources, next) => {
 					this.publishProgress({ status: "update", message: `Importing YouTube playlist (stage 2)` });
 					let successful = 0;
 					let failed = 0;
@@ -1722,26 +1726,26 @@ export default {
 					let alreadyInLikedPlaylist = 0;
 					let alreadyInDislikedPlaylist = 0;
 
-					if (youtubeIds.length === 0) next();
+					if (mediaSources.length === 0) next();
 
 					async.eachLimit(
-						youtubeIds,
+						mediaSources,
 						1,
-						(youtubeId, next) => {
+						(mediaSource, next) => {
 							WSModule.runJob(
 								"RUN_ACTION2",
 								{
 									session,
 									namespace: "playlists",
 									action: "addSongToPlaylist",
-									args: [true, youtubeId, playlistId]
+									args: [true, mediaSource, playlistId]
 								},
 								this
 							)
 								.then(res => {
 									if (res.status === "success") {
 										successful += 1;
-										addedSongs.push(youtubeId);
+										addedSongs.push(mediaSource);
 									} else failed += 1;
 									if (res.message === "That song is already in the playlist") alreadyInPlaylist += 1;
 									else if (
@@ -1849,16 +1853,21 @@ export default {
 	 * Removes a song from a private playlist
 	 *
 	 * @param {object} session - the session object automatically added by the websocket
-	 * @param {string} youtubeId - the youtube id of the song we are removing from the private playlist
+	 * @param {string} mediaSource - the media source of the song we are removing from the private playlist
 	 * @param {string} playlistId - the id of the playlist we are removing the song from
 	 * @param {Function} cb - gets called with the result
 	 */
-	removeSongFromPlaylist: isLoginRequired(async function removeSongFromPlaylist(session, youtubeId, playlistId, cb) {
+	removeSongFromPlaylist: isLoginRequired(async function removeSongFromPlaylist(
+		session,
+		mediaSource,
+		playlistId,
+		cb
+	) {
 		async.waterfall(
 			[
 				next => {
-					if (!youtubeId || typeof youtubeId !== "string") return next("Invalid song id.");
-					if (!playlistId || typeof youtubeId !== "string") return next("Invalid playlist id.");
+					if (!mediaSource || typeof mediaSource !== "string") return next("Invalid song id.");
+					if (!playlistId || typeof mediaSource !== "string") return next("Invalid playlist id.");
 					return next();
 				},
 
@@ -1876,21 +1885,21 @@ export default {
 				},
 
 				(playlist, next) => {
-					MediaModule.runJob("GET_MEDIA", { youtubeId }, this)
+					MediaModule.runJob("GET_MEDIA", { mediaSource }, this)
 						.then(res =>
 							next(null, playlist, {
 								_id: res.song._id,
 								title: res.song.title,
 								thumbnail: res.song.thumbnail,
 								artists: res.song.artists,
-								youtubeId: res.song.youtubeId
+								mediaSource: res.song.mediaSource
 							})
 						)
 						.catch(next);
 				},
 
 				(playlist, newSong, next) => {
-					PlaylistsModule.runJob("REMOVE_FROM_PLAYLIST", { playlistId, youtubeId }, this)
+					PlaylistsModule.runJob("REMOVE_FROM_PLAYLIST", { playlistId, mediaSource }, this)
 						.then(res => {
 							const { ratings } = res;
 							next(null, playlist, newSong, ratings);
@@ -1907,10 +1916,10 @@ export default {
 							userId: session.userId,
 							type: "playlist__remove_song",
 							payload: {
-								message: `Removed <youtubeId>${songName}</youtubeId> from playlist <playlistId>${playlist.displayName}</playlistId>`,
+								message: `Removed <mediaSource>${songName}</mediaSource> from playlist <playlistId>${playlist.displayName}</playlistId>`,
 								thumbnail,
 								playlistId,
-								youtubeId: newSong.youtubeId
+								mediaSource: newSong.mediaSource
 							}
 						});
 					}
@@ -1924,7 +1933,7 @@ export default {
 							CacheModule.runJob("PUB", {
 								channel: "ratings.unlike",
 								value: JSON.stringify({
-									youtubeId: newSong.youtubeId,
+									mediaSource: newSong.mediaSource,
 									userId: session.userId,
 									likes,
 									dislikes
@@ -1935,10 +1944,10 @@ export default {
 								userId: session.userId,
 								type: "song__unlike",
 								payload: {
-									message: `Removed <youtubeId>${title} by ${artists.join(
+									message: `Removed <mediaSource>${title} by ${artists.join(
 										", "
-									)}</youtubeId> from your Liked Songs`,
-									youtubeId: newSong.youtubeId,
+									)}</mediaSource> from your Liked Songs`,
+									mediaSource: newSong.mediaSource,
 									thumbnail
 								}
 							});
@@ -1946,7 +1955,7 @@ export default {
 							CacheModule.runJob("PUB", {
 								channel: "ratings.undislike",
 								value: JSON.stringify({
-									youtubeId: newSong.youtubeId,
+									mediaSource: newSong.mediaSource,
 									userId: session.userId,
 									likes,
 									dislikes
@@ -1957,10 +1966,10 @@ export default {
 								userId: session.userId,
 								type: "song__undislike",
 								payload: {
-									message: `Removed <youtubeId>${title} by ${artists.join(
+									message: `Removed <mediaSource>${title} by ${artists.join(
 										", "
-									)}</youtubeId> from your Disliked Songs`,
-									youtubeId: newSong.youtubeId,
+									)}</mediaSource> from your Disliked Songs`,
+									mediaSource: newSong.mediaSource,
 									thumbnail
 								}
 							});
@@ -1976,7 +1985,7 @@ export default {
 					this.log(
 						"ERROR",
 						"PLAYLIST_REMOVE_SONG",
-						`Removing song "${youtubeId}" from private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
+						`Removing song "${mediaSource}" from private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
 					);
 					return cb({ status: "error", message: err });
 				}
@@ -1984,14 +1993,14 @@ export default {
 				this.log(
 					"SUCCESS",
 					"PLAYLIST_REMOVE_SONG",
-					`Successfully removed song "${youtubeId}" from private playlist "${playlistId}" for user "${session.userId}".`
+					`Successfully removed song "${mediaSource}" from private playlist "${playlistId}" for user "${session.userId}".`
 				);
 
 				CacheModule.runJob("PUB", {
 					channel: "playlist.removeSong",
 					value: {
 						playlistId: playlist._id,
-						youtubeId,
+						mediaSource,
 						createdBy: playlist.createdBy,
 						privacy: playlist.privacy
 					}

+ 8 - 8
backend/logic/actions/reports.js

@@ -512,7 +512,7 @@ export default {
 	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {object} report - the object of the report data
-	 * @param {string} report.youtubeId - the youtube id of the song that is being reported
+	 * @param {string} report.mediaSource - the media source of the song that is being reported
 	 * @param {Array} report.issues - all issues reported (custom or defined)
 	 * @param {Function} cb - gets called with the result
 	 */
@@ -520,11 +520,11 @@ export default {
 		const reportModel = await DBModule.runJob("GET_MODEL", { modelName: "report" }, this);
 		const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
 
-		const { youtubeId } = report;
+		const { mediaSource } = report;
 
 		async.waterfall(
 			[
-				next => songModel.findOne({ youtubeId }).exec(next),
+				next => songModel.findOne({ mediaSource }).exec(next),
 
 				(song, next) => {
 					if (!song) return next("Song not found.");
@@ -537,10 +537,10 @@ export default {
 				(song, next) => {
 					if (!song) return next("Song not found.");
 
-					delete report.youtubeId;
+					delete report.mediaSource;
 					report.song = {
 						_id: song._id,
-						youtubeId: song.youtubeId
+						mediaSource: song.mediaSource
 					};
 
 					return next(null, { title: song.title, artists: song.artists, thumbnail: song.thumbnail });
@@ -571,8 +571,8 @@ export default {
 					userId: session.userId,
 					type: "song__report",
 					payload: {
-						message: `Created a <reportId>${report._id}</reportId> for song <youtubeId>${song.title}</youtubeId>`,
-						youtubeId: report.song.youtubeId,
+						message: `Created a <reportId>${report._id}</reportId> for song <mediaSource>${song.title}</mediaSource>`,
+						mediaSource: report.song.mediaSource,
 						reportId: report._id,
 						thumbnail: song.thumbnail
 					}
@@ -583,7 +583,7 @@ export default {
 					value: report
 				});
 
-				this.log("SUCCESS", "REPORTS_CREATE", `User "${session.userId}" created report for "${youtubeId}".`);
+				this.log("SUCCESS", "REPORTS_CREATE", `User "${session.userId}" created report for "${mediaSource}".`);
 
 				return cb({
 					status: "success",

+ 49 - 39
backend/logic/actions/songs.js

@@ -296,45 +296,50 @@ export default {
 	 * At this time only used in bulk EditSong
 	 *
 	 * @param {object} session - the session object automatically added by the websocket
-	 * @param {Array} youtubeIds - the song ids
+	 * @param {Array} mediaSources - the song media sources
 	 * @param {Function} cb
 	 */
-	getSongsFromYoutubeIds: useHasPermission("songs.get", function getSongsFromYoutubeIds(session, youtubeIds, cb) {
-		async.waterfall(
-			[
-				next => {
-					SongsModule.runJob(
-						"GET_SONGS",
-						{
-							youtubeIds,
-							properties: [
-								"youtubeId",
-								"title",
-								"artists",
-								"thumbnail",
-								"duration",
-								"verified",
-								"_id",
-								"youtubeVideoId"
-							]
-						},
-						this
-					)
-						.then(response => next(null, response.songs))
-						.catch(err => next(err));
-				}
-			],
-			async (err, songs) => {
-				if (err) {
-					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
-					this.log("ERROR", "SONGS_GET_SONGS_FROM_MUSARE_IDS", `Failed to get songs. "${err}"`);
-					return cb({ status: "error", message: err });
+	getSongsFromMediaSources: useHasPermission(
+		"songs.get",
+		function getSongsFromMediaSources(session, mediaSources, cb) {
+			async.waterfall(
+				[
+					next => {
+						console.log(687889, mediaSources);
+
+						SongsModule.runJob(
+							"GET_SONGS",
+							{
+								mediaSources,
+								properties: [
+									"mediaSource",
+									"title",
+									"artists",
+									"thumbnail",
+									"duration",
+									"verified",
+									"_id",
+									"youtubeVideoId"
+								]
+							},
+							this
+						)
+							.then(response => next(null, response.songs))
+							.catch(err => next(err));
+					}
+				],
+				async (err, songs) => {
+					if (err) {
+						err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+						this.log("ERROR", "SONGS_GET_SONGS_FROM_MUSARE_IDS", `Failed to get songs. "${err}"`);
+						return cb({ status: "error", message: err });
+					}
+					this.log("SUCCESS", "SONGS_GET_SONGS_FROM_MUSARE_IDS", `Got songs successfully.`);
+					return cb({ status: "success", data: { songs } });
 				}
-				this.log("SUCCESS", "SONGS_GET_SONGS_FROM_MUSARE_IDS", `Got songs successfully.`);
-				return cb({ status: "success", data: { songs } });
-			}
-		);
-	}),
+			);
+		}
+	),
 
 	/**
 	 * Creates a song
@@ -468,7 +473,12 @@ export default {
 				},
 
 				(song, next) => {
-					YouTubeModule.runJob("GET_VIDEO", { identifier: song.youtubeId, createMissing: true }, this)
+					// TODO replace for spotify support
+					YouTubeModule.runJob(
+						"GET_VIDEO",
+						{ identifier: song.mediaSource.split(":")[1], createMissing: true },
+						this
+					)
 						.then(res => next(null, song, res.video))
 						.catch(() => next(null, song, false));
 				},
@@ -524,7 +534,7 @@ export default {
 													session,
 													namespace: "playlists",
 													action: "removeSongFromPlaylist",
-													args: [song.youtubeId, playlistId]
+													args: [song.mediaSource, playlistId]
 												},
 												this
 											)
@@ -627,7 +637,7 @@ export default {
 							if (!youtubeVideo)
 								StationsModule.runJob(
 									"REMOVE_FROM_QUEUE",
-									{ stationId, youtubeId: song.youtubeId },
+									{ stationId, mediaSource: song.mediaSource },
 									this
 								)
 									.then(() => next())

+ 18 - 18
backend/logic/actions/stations.js

@@ -864,7 +864,7 @@ export default {
 
 					WSModule.runJob("SOCKET_JOIN_SONG_ROOM", {
 						socketId: session.socketId,
-						room: `song.${data.currentSong.youtubeId}`
+						room: `song.${data.currentSong.mediaSource}`
 					});
 
 					data.currentSong.skipVotes = data.currentSong.skipVotes.length;
@@ -1888,10 +1888,10 @@ export default {
 	 *
 	 * @param session
 	 * @param stationId - the station id
-	 * @param youtubeId - the song id
+	 * @param mediaSource the song id
 	 * @param cb
 	 */
-	addToQueue: isLoginRequired(async function addToQueue(session, stationId, youtubeId, requestType, cb) {
+	addToQueue: isLoginRequired(async function addToQueue(session, stationId, mediaSource, requestType, cb) {
 		async.waterfall(
 			[
 				next => {
@@ -1938,7 +1938,7 @@ export default {
 						"ADD_TO_QUEUE",
 						{
 							stationId,
-							youtubeId,
+							mediaSource,
 							requestUser: session.userId,
 							requestType
 						},
@@ -1953,7 +1953,7 @@ export default {
 					this.log(
 						"ERROR",
 						"STATIONS_ADD_SONG_TO_QUEUE",
-						`Adding song "${youtubeId}" to station "${stationId}" queue failed. "${err}"`
+						`Adding song "${mediaSource}" to station "${stationId}" queue failed. "${err}"`
 					);
 					return cb({ status: "error", message: err });
 				}
@@ -1961,7 +1961,7 @@ export default {
 				this.log(
 					"SUCCESS",
 					"STATIONS_ADD_SONG_TO_QUEUE",
-					`Added song "${youtubeId}" to station "${stationId}" successfully.`
+					`Added song "${mediaSource}" to station "${stationId}" successfully.`
 				);
 
 				return cb({
@@ -1977,10 +1977,10 @@ export default {
 	 *
 	 * @param {object} session - user session
 	 * @param {string} stationId - the station id
-	 * @param {string} youtubeId - the youtube id
+	 * @param {string} mediaSource - the media source
 	 * @param {Function} cb - callback
 	 */
-	async removeFromQueue(session, stationId, youtubeId, cb) {
+	async removeFromQueue(session, stationId, mediaSource, cb) {
 		async.waterfall(
 			[
 				next => {
@@ -1990,8 +1990,8 @@ export default {
 				},
 
 				next => {
-					if (!youtubeId) return next("Invalid youtube id.");
-					return StationsModule.runJob("REMOVE_FROM_QUEUE", { stationId, youtubeId }, this)
+					if (!mediaSource) return next("Invalid media source.");
+					return StationsModule.runJob("REMOVE_FROM_QUEUE", { stationId, mediaSource }, this)
 						.then(() => next())
 						.catch(next);
 				}
@@ -2002,7 +2002,7 @@ export default {
 					this.log(
 						"ERROR",
 						"STATIONS_REMOVE_SONG_TO_QUEUE",
-						`Removing song "${youtubeId}" from station "${stationId}" queue failed. "${err}"`
+						`Removing song "${mediaSource}" from station "${stationId}" queue failed. "${err}"`
 					);
 					return cb({ status: "error", message: err });
 				}
@@ -2010,7 +2010,7 @@ export default {
 				this.log(
 					"SUCCESS",
 					"STATIONS_REMOVE_SONG_TO_QUEUE",
-					`Removed song "${youtubeId}" from station "${stationId}" successfully.`
+					`Removed song "${mediaSource}" from station "${stationId}" successfully.`
 				);
 
 				return cb({
@@ -2081,7 +2081,7 @@ export default {
 	 * @param {object} session - user session
 	 * @param {string} stationId - the station id
 	 * @param {object} song - contains details about the song that is to be repositioned
-	 * @param {string} song.youtubeId - the youtube id of the song
+	 * @param {string} song.mediaSource - the media source of the song
 	 * @param {number} song.newIndex - the new position for the song in the queue
 	 * @param {number} song.oldIndex - the old position of the song in the queue
 	 * @param {Function} cb - callback
@@ -2098,7 +2098,7 @@ export default {
 				},
 
 				next => {
-					if (!song || !song.youtubeId) return next("You must provide a song to reposition.");
+					if (!song || !song.mediaSource) return next("You must provide a song to reposition.");
 					return next();
 				},
 
@@ -2106,7 +2106,7 @@ export default {
 				next => {
 					stationModel.updateOne(
 						{ _id: stationId },
-						{ $pull: { queue: { youtubeId: song.youtubeId } } },
+						{ $pull: { queue: { mediaSource: song.mediaSource } } },
 						next
 					);
 				},
@@ -2133,7 +2133,7 @@ export default {
 					this.log(
 						"ERROR",
 						"STATIONS_REPOSITION_SONG_IN_QUEUE",
-						`Repositioning song ${song.youtubeId} in queue of station "${stationId}" failed. "${err}"`
+						`Repositioning song ${song.mediaSource} in queue of station "${stationId}" failed. "${err}"`
 					);
 					return cb({ status: "error", message: err });
 				}
@@ -2141,14 +2141,14 @@ export default {
 				this.log(
 					"SUCCESS",
 					"STATIONS_REPOSITION_SONG_IN_QUEUE",
-					`Repositioned song ${song.youtubeId} in queue of station "${stationId}" successfully.`
+					`Repositioned song ${song.mediaSource} in queue of station "${stationId}" successfully.`
 				);
 
 				CacheModule.runJob("PUB", {
 					channel: "station.repositionSongInQueue",
 					value: {
 						song: {
-							youtubeId: song.youtubeId,
+							mediaSource: song.mediaSource,
 							oldIndex: song.oldIndex,
 							newIndex: song.newIndex
 						},

+ 8 - 8
backend/logic/actions/users.js

@@ -394,7 +394,7 @@ export default {
 					if (!playlist) return next();
 
 					playlist.songs.forEach(song =>
-						songsToAdjustRatings.push({ songId: song._id, youtubeId: song.youtubeId })
+						songsToAdjustRatings.push({ songId: song._id, mediaSource: song.mediaSource })
 					);
 
 					return next();
@@ -408,7 +408,7 @@ export default {
 				(playlist, next) => {
 					if (!playlist) return next();
 
-					playlist.songs.forEach(song => songsToAdjustRatings.push({ youtubeId: song.youtubeId }));
+					playlist.songs.forEach(song => songsToAdjustRatings.push({ mediaSource: song.mediaSource }));
 
 					return next();
 				},
@@ -422,9 +422,9 @@ export default {
 					async.each(
 						songsToAdjustRatings,
 						(song, next) => {
-							const { youtubeId } = song;
+							const { mediaSource } = song;
 
-							MediaModule.runJob("RECALCULATE_RATINGS", { youtubeId })
+							MediaModule.runJob("RECALCULATE_RATINGS", { mediaSource })
 								.then(() => next())
 								.catch(next);
 						},
@@ -625,7 +625,7 @@ export default {
 					if (!playlist) return next();
 
 					playlist.songs.forEach(song =>
-						songsToAdjustRatings.push({ songId: song._id, youtubeId: song.youtubeId })
+						songsToAdjustRatings.push({ songId: song._id, mediaSource: song.mediaSource })
 					);
 
 					return next();
@@ -639,7 +639,7 @@ export default {
 				(playlist, next) => {
 					if (!playlist) return next();
 
-					playlist.songs.forEach(song => songsToAdjustRatings.push({ youtubeId: song.youtubeId }));
+					playlist.songs.forEach(song => songsToAdjustRatings.push({ mediaSource: song.mediaSource }));
 
 					return next();
 				},
@@ -653,9 +653,9 @@ export default {
 					async.each(
 						songsToAdjustRatings,
 						(song, next) => {
-							const { youtubeId } = song;
+							const { mediaSource } = song;
 
-							MediaModule.runJob("RECALCULATE_RATINGS", { youtubeId })
+							MediaModule.runJob("RECALCULATE_RATINGS", { mediaSource })
 								.then(() => next())
 								.catch(next);
 						},

+ 3 - 3
backend/logic/actions/youtube.js

@@ -265,10 +265,10 @@ export default {
 								blacklistedProperties: [],
 								specialProperties: {
 									songId: [
-										// Fetch songs from songs collection with a matching youtubeId
+										// Fetch songs from songs collection with a matching mediaSource
 										{
 											$lookup: {
-												from: "songs",
+												from: "songs", // TODO fix this to support mediasource, so start with youtube:, so add a new pipeline steps
 												localField: "youtubeId",
 												foreignField: "youtubeId",
 												as: "song"
@@ -376,7 +376,7 @@ export default {
 	 * @returns {{status: string, data: object}}
 	 */
 	getVideo: isLoginRequired(function getVideo(session, identifier, createMissing, cb) {
-		YouTubeModule.runJob("GET_VIDEO", { identifier, createMissing }, this)
+		return YouTubeModule.runJob("GET_VIDEO", { identifier, createMissing }, this)
 			.then(res => {
 				this.log("SUCCESS", "YOUTUBE_GET_VIDEO", `Fetching video was successful.`);
 

+ 6 - 6
backend/logic/activities.js

@@ -43,7 +43,7 @@ class _ActivitiesModule extends CoreClass {
 	 * @param {object} payload.payload - the details of the activity e.g. an array of songs that were added
 	 * @param {string} payload.payload.message - the main message describing the activity e.g. 50 songs added to playlist 'playlist name'
 	 * @param {string} payload.payload.thumbnail - url to a thumbnail e.g. song album art to be used when display an activity
-	 * @param {string} payload.payload.youtubeId - (optional) if relevant, the youtube id of the song related to the activity
+	 * @param {string} payload.payload.mediaSource - (optional) if relevant, the media source of the song related to the activity
 	 * @param {string} payload.payload.reportId - (optional) if relevant, the id of the report related to the activity
 	 * @param {string} payload.payload.playlistId - (optional) if relevant, the id of the playlist related to the activity
 	 * @param {string} payload.payload.stationId - (optional) if relevant, the id of the station related to the activity
@@ -287,11 +287,11 @@ class _ActivitiesModule extends CoreClass {
 	 * Removes any references to a station, playlist or song in activities
 	 *
 	 * @param {object} payload - object that contains the payload
-	 * @param {string} payload.type - type of reference. enum: ["youtubeId", "stationId", "playlistId", "playlistId"]
+	 * @param {string} payload.type - type of reference. enum: ["mediaSource", "stationId", "playlistId", "playlistId"]
 	 * @param {string} payload.stationId - (optional) the id of a station
 	 * @param {string} payload.reportId - (optional) the id of a report
 	 * @param {string} payload.playlistId - (optional) the id of a playlist
-	 * @param {string} payload.youtubeId - (optional) the id of a song
+	 * @param {string} payload.mediaSource - (optional) the id of a song
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	async REMOVE_ACTIVITY_REFERENCES(payload) {
@@ -302,7 +302,7 @@ class _ActivitiesModule extends CoreClass {
 				[
 					next => {
 						if (
-							(payload.type !== "youtubeId" &&
+							(payload.type !== "mediaSource" &&
 								payload.type !== "stationId" &&
 								payload.type !== "reportId" &&
 								payload.type !== "playlistId") ||
@@ -333,9 +333,9 @@ class _ActivitiesModule extends CoreClass {
 							(activity, next) => {
 								// remove the reference tags
 
-								if (payload.youtubeId) {
+								if (payload.mediaSource) {
 									activity.payload.message = activity.payload.message.replace(
-										/<youtubeId>(.*)<\/youtubeId>/g,
+										/<mediaSource>(.*)<\/mediaSource>/g,
 										"$1"
 									);
 								}

+ 12 - 7
backend/logic/db/index.js

@@ -6,20 +6,20 @@ import async from "async";
 import CoreClass from "../../core";
 
 const REQUIRED_DOCUMENT_VERSIONS = {
-	activity: 2,
+	activity: 3,
 	news: 3,
-	playlist: 6,
+	playlist: 7,
 	punishment: 1,
 	queueSong: 1,
-	report: 6,
-	song: 9,
-	station: 9,
+	report: 7,
+	song: 10,
+	station: 10,
 	user: 4,
 	youtubeApiRequest: 1,
 	youtubeVideo: 1,
-	ratings: 1,
+	ratings: 2,
 	importJob: 1,
-	stationHistory: 1
+	stationHistory: 2
 };
 
 const regex = {
@@ -199,6 +199,11 @@ class _DBModule extends CoreClass {
 					});
 
 					// Song
+					this.schemas.song.path("mediaSource").validate(mediaSource => {
+						if (mediaSource.startsWith("youtube:")) return true;
+						return false;
+					});
+
 					const songTitle = title => isLength(title, 1, 100);
 					this.schemas.song.path("title").validate(songTitle, "Invalid title.");
 

+ 2 - 2
backend/logic/db/schemas/activity.js

@@ -48,10 +48,10 @@ export default {
 	payload: {
 		message: { type: String, default: "", required: true },
 		thumbnail: { type: String, required: false },
-		youtubeId: { type: String, required: false },
+		mediaSource: { type: String, required: false },
 		stationId: { type: String, required: false },
 		playlistId: { type: String, required: false },
 		reportId: { type: String, required: false }
 	},
-	documentVersion: { type: Number, default: 2, required: true }
+	documentVersion: { type: Number, default: 3, required: true }
 };

+ 2 - 2
backend/logic/db/schemas/playlist.js

@@ -5,7 +5,7 @@ export default {
 	songs: [
 		{
 			_id: { type: mongoose.Schema.Types.ObjectId },
-			youtubeId: { type: String, required: true },
+			mediaSource: { type: String, required: true },
 			title: { type: String },
 			artists: [{ type: String }],
 			duration: { type: Number },
@@ -19,5 +19,5 @@ export default {
 	createdFor: { type: String },
 	privacy: { type: String, enum: ["public", "private"], default: "private" },
 	type: { type: String, enum: ["user", "user-liked", "user-disliked", "genre", "station", "admin"], required: true },
-	documentVersion: { type: Number, default: 6, required: true }
+	documentVersion: { type: Number, default: 7, required: true }
 };

+ 2 - 0
backend/logic/db/schemas/queueSong.js

@@ -1,3 +1,5 @@
+// Legacy file, not used atm, just exists for migration module
+
 export default {
 	songId: { type: String, min: 11, max: 11, required: true, index: true },
 	title: { type: String, required: true },

+ 2 - 2
backend/logic/db/schemas/ratings.js

@@ -1,6 +1,6 @@
 export default {
-	youtubeId: { type: String, min: 11, max: 11, required: true, index: true, unique: true },
+	mediaSource: { type: String, min: 11, max: 11, required: true, index: true, unique: true },
 	likes: { type: Number, default: 0, required: true },
 	dislikes: { type: Number, default: 0, required: true },
-	documentVersion: { type: Number, default: 1, required: true }
+	documentVersion: { type: Number, default: 2, required: true }
 };

+ 2 - 2
backend/logic/db/schemas/report.js

@@ -2,7 +2,7 @@ export default {
 	resolved: { type: Boolean, default: false, required: true },
 	song: {
 		_id: { type: String, required: true },
-		youtubeId: { type: String, required: true }
+		mediaSource: { type: String, required: true }
 	},
 	issues: [
 		{
@@ -18,5 +18,5 @@ export default {
 	],
 	createdBy: { type: String, required: true },
 	createdAt: { type: Date, default: Date.now, required: true },
-	documentVersion: { type: Number, default: 6, required: true }
+	documentVersion: { type: Number, default: 7, required: true }
 };

+ 2 - 2
backend/logic/db/schemas/song.js

@@ -1,5 +1,5 @@
 export default {
-	youtubeId: { type: String, min: 11, max: 11, required: true, index: true, unique: true },
+	mediaSource: { type: String, min: 11, max: 11, required: true, index: true, unique: true },
 	title: { type: String, trim: true, required: true },
 	artists: [{ type: String, trim: true, default: [] }],
 	genres: [{ type: String, trim: true, default: [] }],
@@ -14,5 +14,5 @@ export default {
 	verifiedBy: { type: String },
 	verifiedAt: { type: Date },
 	discogs: { type: Object },
-	documentVersion: { type: Number, default: 9, required: true }
+	documentVersion: { type: Number, default: 10, required: true }
 };

+ 3 - 3
backend/logic/db/schemas/station.js

@@ -8,7 +8,7 @@ export default {
 	paused: { type: Boolean, default: false, required: true },
 	currentSong: {
 		_id: { type: mongoose.Schema.Types.ObjectId },
-		youtubeId: { type: String },
+		mediaSource: { type: String },
 		title: { type: String },
 		artists: [{ type: String }],
 		duration: { type: Number },
@@ -29,7 +29,7 @@ export default {
 	queue: [
 		{
 			_id: { type: mongoose.Schema.Types.ObjectId },
-			youtubeId: { type: String, required: true },
+			mediaSource: { type: String, required: true },
 			title: { type: String },
 			artists: [{ type: String }],
 			duration: { type: Number },
@@ -61,5 +61,5 @@ export default {
 	blacklist: [{ type: mongoose.Schema.Types.ObjectId, ref: "playlists" }],
 	djs: [{ type: mongoose.Schema.Types.ObjectId, ref: "users" }],
 	skipVoteThreshold: { type: Number, min: 0, max: 100, default: 50, required: true },
-	documentVersion: { type: Number, default: 9, required: true }
+	documentVersion: { type: Number, default: 10, required: true }
 };

+ 2 - 2
backend/logic/db/schemas/stationHistory.js

@@ -6,7 +6,7 @@ export default {
 	payload: {
 		song: {
 			_id: { type: mongoose.Schema.Types.ObjectId },
-			youtubeId: { type: String, min: 11, max: 11, required: true },
+			mediaSource: { type: String, min: 11, max: 11, required: true },
 			title: { type: String, trim: true, required: true },
 			artists: [{ type: String, trim: true, default: [] }],
 			duration: { type: Number },
@@ -18,5 +18,5 @@ export default {
 		skippedAt: { type: Date },
 		skipReason: { type: String, enum: ["natural", "force_skip", "vote_skip", "other"] }
 	},
-	documentVersion: { type: Number, default: 1, required: true }
+	documentVersion: { type: Number, default: 2, required: true }
 };

+ 54 - 40
backend/logic/media.js

@@ -75,17 +75,17 @@ class _MediaModule extends CoreClass {
 
 						if (!ratings) return next();
 
-						const youtubeIds = Object.keys(ratings);
+						const mediaSources = Object.keys(ratings);
 
 						return async.each(
-							youtubeIds,
-							(youtubeId, next) => {
-								MediaModule.RatingsModel.findOne({ youtubeId }, (err, rating) => {
+							mediaSources,
+							(mediaSource, next) => {
+								MediaModule.RatingsModel.findOne({ mediaSource }, (err, rating) => {
 									if (err) next(err);
 									else if (!rating)
 										CacheModule.runJob("HDEL", {
 											table: "ratings",
-											key: youtubeId
+											key: mediaSource
 										})
 											.then(() => next())
 											.catch(next);
@@ -108,7 +108,7 @@ class _MediaModule extends CoreClass {
 							(rating, next) => {
 								CacheModule.runJob("HSET", {
 									table: "ratings",
-									key: rating.youtubeId,
+									key: rating.mediaSource,
 									value: MediaModule.RatingsSchemaCache(rating)
 								})
 									.then(() => next())
@@ -120,6 +120,7 @@ class _MediaModule extends CoreClass {
 				],
 				async err => {
 					if (err) {
+						console.log(345345, err);
 						err = await UtilsModule.runJob("GET_ERROR", { error: err });
 						reject(new Error(err));
 					} else resolve();
@@ -132,7 +133,7 @@ class _MediaModule extends CoreClass {
 	 * Recalculates dislikes and likes
 	 *
 	 * @param {object} payload - returns an object containing the payload
-	 * @param {string} payload.youtubeId - the youtube id
+	 * @param {string} payload.mediaSource - the media source
 	 * @returns {Promise} - returns a promise (resolve, reject)
 	 */
 	async RECALCULATE_RATINGS(payload) {
@@ -143,7 +144,7 @@ class _MediaModule extends CoreClass {
 				[
 					next => {
 						playlistModel.countDocuments(
-							{ songs: { $elemMatch: { youtubeId: payload.youtubeId } }, type: "user-liked" },
+							{ songs: { $elemMatch: { mediaSource: payload.mediaSource } }, type: "user-liked" },
 							(err, likes) => {
 								if (err) return next(err);
 								return next(null, likes);
@@ -153,7 +154,7 @@ class _MediaModule extends CoreClass {
 
 					(likes, next) => {
 						playlistModel.countDocuments(
-							{ songs: { $elemMatch: { youtubeId: payload.youtubeId } }, type: "user-disliked" },
+							{ songs: { $elemMatch: { mediaSource: payload.mediaSource } }, type: "user-disliked" },
 							(err, dislikes) => {
 								if (err) return next(err);
 								return next(err, { likes, dislikes });
@@ -163,7 +164,7 @@ class _MediaModule extends CoreClass {
 
 					({ likes, dislikes }, next) => {
 						MediaModule.RatingsModel.findOneAndUpdate(
-							{ youtubeId: payload.youtubeId },
+							{ mediaSource: payload.mediaSource },
 							{
 								$set: {
 									likes,
@@ -180,7 +181,7 @@ class _MediaModule extends CoreClass {
 							"HSET",
 							{
 								table: "ratings",
-								key: payload.youtubeId,
+								key: payload.mediaSource,
 								value: ratings
 							},
 							this
@@ -207,30 +208,31 @@ class _MediaModule extends CoreClass {
 			async.waterfall(
 				[
 					next => {
-						SongsModule.SongModel.find({}, { youtubeId: true }, next);
+						SongsModule.SongModel.find({}, { mediaSource: true }, next);
 					},
 
 					(songs, next) => {
+						// TODO support spotify
 						YouTubeModule.youtubeVideoModel.find({}, { youtubeId: true }, (err, videos) => {
 							if (err) next(err);
 							else
 								next(null, [
-									...songs.map(song => song.youtubeId),
-									...videos.map(video => video.youtubeId)
+									...songs.map(song => song.mediaSource),
+									...videos.map(video => `youtube:${video.youtubeId}`)
 								]);
 						});
 					},
 
-					(youtubeIds, next) => {
+					(mediaSources, next) => {
 						async.eachLimit(
-							youtubeIds,
+							mediaSources,
 							2,
-							(youtubeId, next) => {
+							(mediaSource, next) => {
 								this.publishProgress({
 									status: "update",
-									message: `Recalculating ratings for ${youtubeId}`
+									message: `Recalculating ratings for ${mediaSource}`
 								});
-								MediaModule.runJob("RECALCULATE_RATINGS", { youtubeId }, this)
+								MediaModule.runJob("RECALCULATE_RATINGS", { mediaSource }, this)
 									.then(() => {
 										next();
 									})
@@ -256,7 +258,7 @@ class _MediaModule extends CoreClass {
 	 * Gets ratings by id from the cache or Mongo, and if it isn't in the cache yet, adds it the cache
 	 *
 	 * @param {object} payload - object containing the payload
-	 * @param {string} payload.youtubeId - the youtube id
+	 * @param {string} payload.mediaSource - the media source
 	 * @param {string} payload.createMissing - whether to create missing ratings
 	 * @returns {Promise} - returns a promise (resolve, reject)
 	 */
@@ -265,13 +267,13 @@ class _MediaModule extends CoreClass {
 			async.waterfall(
 				[
 					next =>
-						CacheModule.runJob("HGET", { table: "ratings", key: payload.youtubeId }, this)
+						CacheModule.runJob("HGET", { table: "ratings", key: payload.mediaSource }, this)
 							.then(ratings => next(null, ratings))
 							.catch(next),
 
 					(ratings, next) => {
 						if (ratings) return next(true, ratings);
-						return MediaModule.RatingsModel.findOne({ youtubeId: payload.youtubeId }, next);
+						return MediaModule.RatingsModel.findOne({ mediaSource: payload.mediaSource }, next);
 					},
 
 					(ratings, next) => {
@@ -280,7 +282,7 @@ class _MediaModule extends CoreClass {
 								"HSET",
 								{
 									table: "ratings",
-									key: payload.youtubeId,
+									key: payload.mediaSource,
 									value: ratings
 								},
 								this
@@ -288,13 +290,13 @@ class _MediaModule extends CoreClass {
 
 						if (!payload.createMissing) return next("Ratings not found.");
 
-						return MediaModule.runJob("RECALCULATE_RATINGS", { youtubeId: payload.youtubeId }, this)
+						return MediaModule.runJob("RECALCULATE_RATINGS", { mediaSource: payload.mediaSource }, this)
 							.then(() => next())
 							.catch(next);
 					},
 
 					next =>
-						MediaModule.runJob("GET_RATINGS", { youtubeId: payload.youtubeId }, this)
+						MediaModule.runJob("GET_RATINGS", { mediaSource: payload.mediaSource }, this)
 							.then(res => next(null, res.ratings))
 							.catch(next)
 				],
@@ -310,29 +312,29 @@ class _MediaModule extends CoreClass {
 	 * Remove ratings by id from the cache and Mongo
 	 *
 	 * @param {object} payload - object containing the payload
-	 * @param {string} payload.youtubeIds - the youtube id
+	 * @param {string} payload.mediaSources - the media source
 	 * @returns {Promise} - returns a promise (resolve, reject)
 	 */
 	REMOVE_RATINGS(payload) {
 		return new Promise((resolve, reject) => {
-			let { youtubeIds } = payload;
-			if (!Array.isArray(youtubeIds)) youtubeIds = [youtubeIds];
+			let { mediaSources } = payload;
+			if (!Array.isArray(mediaSources)) mediaSources = [mediaSources];
 
 			async.eachLimit(
-				youtubeIds,
+				mediaSources,
 				1,
-				(youtubeId, next) => {
+				(mediaSource, next) => {
 					async.waterfall(
 						[
 							next => {
-								MediaModule.RatingsModel.deleteOne({ youtubeId }, err => {
+								MediaModule.RatingsModel.deleteOne({ mediaSource }, err => {
 									if (err) next(err);
 									else next();
 								});
 							},
 
 							next => {
-								CacheModule.runJob("HDEL", { table: "ratings", key: youtubeId }, this)
+								CacheModule.runJob("HDEL", { table: "ratings", key: mediaSource }, this)
 									.then(() => {
 										next();
 									})
@@ -351,10 +353,10 @@ class _MediaModule extends CoreClass {
 	}
 
 	/**
-	 * Get song or youtube video by youtubeId
+	 * Get song or youtube video by mediaSource
 	 *
 	 * @param {object} payload - an object containing the payload
-	 * @param {string} payload.youtubeId - the youtube id of the song/video
+	 * @param {string} payload.mediaSource - the media source of the song/video
 	 * @param {string} payload.userId - the user id
 	 * @returns {Promise} - returns a promise (resolve, reject)
 	 */
@@ -363,23 +365,35 @@ class _MediaModule extends CoreClass {
 			async.waterfall(
 				[
 					next => {
-						SongsModule.SongModel.findOne({ youtubeId: payload.youtubeId }, next);
+						SongsModule.SongModel.findOne({ mediaSource: payload.mediaSource }, next);
 					},
 
 					(song, next) => {
-						if (song && song.duration > 0) next(true, song);
-						else {
-							YouTubeModule.runJob(
+						if (song && song.duration > 0) return next(true, song);
+
+						if (payload.mediaSource.startsWith("youtube:")) {
+							const youtubeId = payload.mediaSource.split(":")[1];
+
+							return YouTubeModule.runJob(
 								"GET_VIDEO",
-								{ identifier: payload.youtubeId, createMissing: true },
+								{ identifier: youtubeId, createMissing: true },
 								this
 							)
 								.then(response => {
 									const { youtubeId, title, author, duration } = response.video;
-									next(null, song, { youtubeId, title, artists: [author], duration });
+									next(null, song, {
+										mediaSource: `youtube:${youtubeId}`,
+										title,
+										artists: [author],
+										duration
+									});
 								})
 								.catch(next);
 						}
+
+						// TODO handle Spotify here
+
+						return next("Invalid media source provided.");
 					},
 
 					(song, youtubeVideo, next) => {

+ 47 - 47
backend/logic/playlists.js

@@ -159,33 +159,33 @@ class _PlaylistsModule extends CoreClass {
 		});
 	}
 
-	/**
-	 * Returns a list of youtube ids in all user playlists of the specified user
-	 *
-	 * @param {object} payload - object that contains the payload
-	 * @param {string} payload.userId - the user id
-	 * @returns {Promise} - returns promise (reject, resolve)
-	 */
-	GET_SONG_YOUTUBE_IDS_FROM_PLAYLISTS(payload) {
-		return new Promise((resolve, reject) => {
-			PlaylistsModule.playlistModel.find({ createdBy: payload.userId }, (err, playlists) => {
-				const youtubeIds = new Set();
-
-				if (err) reject(err);
-				else {
-					playlists.forEach(playlist => {
-						playlist.songs.forEach(song => {
-							youtubeIds.add(song.youtubeId);
-						});
-					});
-
-					console.log(Array.from(youtubeIds));
-
-					resolve({ youtubeIds: Array.from(youtubeIds) });
-				}
-			});
-		});
-	}
+	// /**
+	//  * Returns a list of youtube ids in all user playlists of the specified user
+	//  *
+	//  * @param {object} payload - object that contains the payload
+	//  * @param {string} payload.userId - the user id
+	//  * @returns {Promise} - returns promise (reject, resolve)
+	//  */
+	// GET_SONG_YOUTUBE_IDS_FROM_PLAYLISTS(payload) {
+	// 	return new Promise((resolve, reject) => {
+	// 		PlaylistsModule.playlistModel.find({ createdBy: payload.userId }, (err, playlists) => {
+	// 			const youtubeIds = new Set();
+
+	// 			if (err) reject(err);
+	// 			else {
+	// 				playlists.forEach(playlist => {
+	// 					playlist.songs.forEach(song => {
+	// 						youtubeIds.add(song.youtubeId);
+	// 					});
+	// 				});
+
+	// 				console.log(Array.from(youtubeIds));
+
+	// 				resolve({ youtubeIds: Array.from(youtubeIds) });
+	// 			}
+	// 		});
+	// 	});
+	// }
 
 	/**
 	 * Creates a playlist owned by a user
@@ -410,12 +410,12 @@ class _PlaylistsModule extends CoreClass {
 	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.playlistId - the playlist id
-	 * @param {string} payload.youtubeId - the youtube id
+	 * @param {string} payload.mediaSource - the media source
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
 	ADD_SONG_TO_PLAYLIST(payload) {
 		return new Promise((resolve, reject) => {
-			const { playlistId, youtubeId } = payload;
+			const { playlistId, mediaSource } = payload;
 
 			async.waterfall(
 				[
@@ -429,19 +429,19 @@ class _PlaylistsModule extends CoreClass {
 
 					(playlist, next) => {
 						if (!playlist) return next("Playlist not found.");
-						if (playlist.songs.find(song => song.youtubeId === youtubeId))
+						if (playlist.songs.find(song => song.mediaSource === mediaSource))
 							return next("That song is already in the playlist.");
 						return next();
 					},
 
 					next => {
-						MediaModule.runJob("GET_MEDIA", { youtubeId }, this)
+						MediaModule.runJob("GET_MEDIA", { mediaSource }, this)
 							.then(response => {
 								const { song } = response;
 								const { _id, title, artists, thumbnail, duration, verified } = song;
 								next(null, {
 									_id,
-									youtubeId,
+									mediaSource,
 									title,
 									artists,
 									thumbnail,
@@ -489,7 +489,7 @@ class _PlaylistsModule extends CoreClass {
 					(playlist, newSong, next) => {
 						if (playlist.type === "user-liked" || playlist.type === "user-disliked") {
 							MediaModule.runJob("RECALCULATE_RATINGS", {
-								youtubeId: newSong.youtubeId
+								mediaSource: newSong.mediaSource
 							})
 								.then(ratings => next(null, playlist, newSong, ratings))
 								.catch(next);
@@ -511,12 +511,12 @@ class _PlaylistsModule extends CoreClass {
 	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.playlistId - the playlist id
-	 * @param {string} payload.youtubeId - the youtube id
+	 * @param {string} payload.mediaSource - the media source
 	 * @returns {Promise} - returns a promise (resolve, reject)
 	 */
 	REMOVE_FROM_PLAYLIST(payload) {
 		return new Promise((resolve, reject) => {
-			const { playlistId, youtubeId } = payload;
+			const { playlistId, mediaSource } = payload;
 			async.waterfall(
 				[
 					next => {
@@ -529,12 +529,12 @@ class _PlaylistsModule extends CoreClass {
 
 					(playlist, next) => {
 						if (!playlist) return next("Playlist not found.");
-						if (!playlist.songs.find(song => song.youtubeId === youtubeId))
+						if (!playlist.songs.find(song => song.mediaSource === mediaSource))
 							return next("That song is not currently in the playlist.");
 
 						return PlaylistsModule.playlistModel.updateOne(
 							{ _id: playlistId },
-							{ $pull: { songs: { youtubeId } } },
+							{ $pull: { songs: { mediaSource } } },
 							next
 						);
 					},
@@ -567,7 +567,7 @@ class _PlaylistsModule extends CoreClass {
 
 					(playlist, next) => {
 						if (playlist.type === "user-liked" || playlist.type === "user-disliked") {
-							MediaModule.runJob("RECALCULATE_RATINGS", { youtubeId })
+							MediaModule.runJob("RECALCULATE_RATINGS", { mediaSource })
 								.then(ratings => next(null, playlist, ratings))
 								.catch(next);
 						} else next(null, playlist, null);
@@ -594,18 +594,18 @@ class _PlaylistsModule extends CoreClass {
 	}
 
 	/**
-	 * Deletes a song from a playlist based on the youtube id
+	 * Deletes a song from a playlist based on the media source
 	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.playlistId - the playlist id
-	 * @param {string} payload.youtubeId - the youtube id
+	 * @param {string} payload.mediaSource - the media source
 	 * @returns {Promise} - returns promise (reject, resolve)
 	 */
-	DELETE_SONG_FROM_PLAYLIST_BY_YOUTUBE_ID(payload) {
+	DELETE_SONG_FROM_PLAYLIST_BY_MEDIA_SOURCE_ID(payload) {
 		return new Promise((resolve, reject) => {
 			PlaylistsModule.playlistModel.updateOne(
 				{ _id: payload.playlistId },
-				{ $pull: { songs: { youtubeId: payload.youtubeId } } },
+				{ $pull: { songs: { mediaSource: payload.mediaSource } } },
 				err => {
 					if (err) reject(new Error(err));
 					else {
@@ -668,10 +668,10 @@ class _PlaylistsModule extends CoreClass {
 
 					(playlistId, _songs, next) => {
 						const songs = _songs.map(song => {
-							const { _id, youtubeId, title, artists, thumbnail, duration, verified } = song;
+							const { _id, mediaSource, title, artists, thumbnail, duration, verified } = song;
 							return {
 								_id,
-								youtubeId,
+								mediaSource,
 								title,
 								artists,
 								thumbnail,
@@ -957,17 +957,17 @@ class _PlaylistsModule extends CoreClass {
 							.flatMap(blacklistedPlaylist => blacklistedPlaylist.songs)
 							.reduce(
 								(items, item) =>
-									items.find(x => x.youtubeId === item.youtubeId) ? [...items] : [...items, item],
+									items.find(x => x.mediaSource === item.mediaSource) ? [...items] : [...items, item],
 								[]
 							);
 						const includedSongs = playlists
 							.flatMap(playlist => playlist.songs)
 							.reduce(
 								(songs, song) =>
-									songs.find(x => x.youtubeId === song.youtubeId) ? [...songs] : [...songs, song],
+									songs.find(x => x.mediaSource === song.mediaSource) ? [...songs] : [...songs, song],
 								[]
 							)
-							.filter(song => !blacklistedSongs.find(x => x.youtubeId === song.youtubeId));
+							.filter(song => !blacklistedSongs.find(x => x.mediaSource === song.mediaSource));
 
 						next(null, station, includedSongs);
 					},

+ 60 - 40
backend/logic/songs.js

@@ -68,17 +68,17 @@ class _SongsModule extends CoreClass {
 
 						if (!songs) return next();
 
-						const youtubeIds = Object.keys(songs);
+						const mediaSources = Object.keys(songs);
 
 						return async.each(
-							youtubeIds,
-							(youtubeId, next) => {
-								SongsModule.SongModel.findOne({ youtubeId }, (err, song) => {
+							mediaSources,
+							(mediaSource, next) => {
+								SongsModule.SongModel.findOne({ mediaSource }, (err, song) => {
 									if (err) next(err);
 									else if (!song)
 										CacheModule.runJob("HDEL", {
 											table: "songs",
-											key: youtubeId
+											key: mediaSource
 										})
 											.then(() => next())
 											.catch(next);
@@ -101,7 +101,7 @@ class _SongsModule extends CoreClass {
 							(song, next) => {
 								CacheModule.runJob("HSET", {
 									table: "songs",
-									key: song.youtubeId,
+									key: song.mediaSource,
 									value: SongsModule.SongSchemaCache(song)
 								})
 									.then(() => next())
@@ -171,28 +171,38 @@ class _SongsModule extends CoreClass {
 	 * Gets songs by id from Mongo
 	 *
 	 * @param {object} payload - object containing the payload
-	 * @param {string} payload.youtubeIds - the youtube ids of the songs we are trying to get
+	 * @param {string} payload.mediaSources - the media sources of the songs we are trying to get
 	 * @returns {Promise} - returns a promise (resolve, reject)
 	 */
 	GET_SONGS(payload) {
 		return new Promise((resolve, reject) => {
 			async.waterfall(
 				[
-					next => SongsModule.SongModel.find({ youtubeId: { $in: payload.youtubeIds } }, next),
+					next => SongsModule.SongModel.find({ mediaSource: { $in: payload.mediaSources } }, next),
 
 					(songs, next) => {
-						const youtubeIds = payload.youtubeIds.filter(
-							youtubeId => !songs.find(song => song.youtubeId === youtubeId)
+						const mediaSources = payload.mediaSources.filter(
+							mediaSource => !songs.find(song => song.mediaSource === mediaSource)
 						);
+
+						console.log(536546, songs, payload, mediaSources);
+
+						// TODO support spotify here
 						return YouTubeModule.youtubeVideoModel.find(
-							{ youtubeId: { $in: youtubeIds } },
+							{
+								youtubeId: {
+									$in: mediaSources
+										.filter(mediaSource => mediaSource.startsWith("youtube:"))
+										.map(mediaSource => mediaSource.split(":")[1])
+								}
+							},
 							(err, videos) => {
 								if (err) next(err);
 								else {
 									const youtubeVideos = videos.map(video => {
 										const { youtubeId, title, author, duration, thumbnail } = video;
 										return {
-											youtubeId,
+											mediaSource: `youtube:${youtubeId}`,
 											title,
 											artists: [author],
 											genres: [],
@@ -209,11 +219,11 @@ class _SongsModule extends CoreClass {
 									});
 									next(
 										null,
-										payload.youtubeIds
+										payload.mediaSources
 											.map(
-												youtubeId =>
-													songs.find(song => song.youtubeId === youtubeId) ||
-													youtubeVideos.find(video => video.youtubeId === youtubeId)
+												mediaSource =>
+													songs.find(song => song.mediaSource === mediaSource) ||
+													youtubeVideos.find(video => video.mediaSource === mediaSource)
 											)
 											.filter(song => !!song)
 									);
@@ -276,7 +286,7 @@ class _SongsModule extends CoreClass {
 					},
 
 					(song, next) => {
-						MediaModule.runJob("RECALCULATE_RATINGS", { youtubeId: song.youtubeId }, this)
+						MediaModule.runJob("RECALCULATE_RATINGS", { mediaSource: song.mediaSource }, this)
 							.then(() => next(null, song))
 							.catch(next);
 					},
@@ -336,11 +346,19 @@ class _SongsModule extends CoreClass {
 							this
 						)
 							.then(() => {
-								const { _id, youtubeId, title, artists, thumbnail, duration, skipDuration, verified } =
-									song;
+								const {
+									_id,
+									mediaSource,
+									title,
+									artists,
+									thumbnail,
+									duration,
+									skipDuration,
+									verified
+								} = song;
 								next(null, {
 									_id,
-									youtubeId,
+									mediaSource,
 									title,
 									artists,
 									thumbnail,
@@ -361,7 +379,7 @@ class _SongsModule extends CoreClass {
 
 					(song, next) => {
 						playlistModel.updateMany(
-							{ "songs.youtubeId": song.youtubeId },
+							{ "songs.mediaSource": song.mediaSource },
 							{ $set: { "songs.$": song } },
 							err => {
 								if (err) next(err);
@@ -410,7 +428,7 @@ class _SongsModule extends CoreClass {
 
 					(song, next) => {
 						stationModel.updateMany(
-							{ "queue.youtubeId": song.youtubeId },
+							{ "queue.mediaSource": song.mediaSource },
 							{ $set: { "queue.$": song } },
 							err => {
 								if (err) next(err);
@@ -563,10 +581,10 @@ class _SongsModule extends CoreClass {
 						this.publishProgress({ status: "update", message: `Updating songs (stage 4)` });
 
 						const trimmedSongs = songs.map(song => {
-							const { _id, youtubeId, title, artists, thumbnail, duration, verified } = song;
+							const { _id, mediaSource, title, artists, thumbnail, duration, verified } = song;
 							return {
 								_id,
-								youtubeId,
+								mediaSource,
 								title,
 								artists,
 								thumbnail,
@@ -653,12 +671,12 @@ class _SongsModule extends CoreClass {
 								async.waterfall(
 									[
 										next => {
-											const { youtubeId, title, artists, thumbnail, duration, verified } = song;
+											const { mediaSource, title, artists, thumbnail, duration, verified } = song;
 											stationModel.updateMany(
 												{ "queue._id": song._id },
 												{
 													$set: {
-														"queue.$.youtubeId": youtubeId,
+														"queue.$.mediaSource": mediaSource,
 														"queue.$.title": title,
 														"queue.$.artists": artists,
 														"queue.$.thumbnail": thumbnail,
@@ -976,10 +994,10 @@ class _SongsModule extends CoreClass {
 						else if (payload.trimmed) {
 							next(null, {
 								songs: data.songs.map(song => {
-									const { _id, youtubeId, title, artists, thumbnail, duration, verified } = song;
+									const { _id, mediaSource, title, artists, thumbnail, duration, verified } = song;
 									return {
 										_id,
-										youtubeId,
+										mediaSource,
 										title,
 										artists,
 										thumbnail,
@@ -1077,7 +1095,7 @@ class _SongsModule extends CoreClass {
 				playlistModel.find({}, (err, playlists) => {
 					if (err) reject(new Error(err));
 					else {
-						SongsModule.SongModel.find({}, { _id: true, youtubeId: true }, (err, songs) => {
+						SongsModule.SongModel.find({}, { _id: true, mediaSource: true }, (err, songs) => {
 							if (err) reject(new Error(err));
 							else {
 								const songIds = songs.map(song => song._id.toString());
@@ -1089,9 +1107,9 @@ class _SongsModule extends CoreClass {
 										playlist.songs.forEach(song => {
 											if (
 												(!song._id || songIds.indexOf(song._id.toString() === -1)) &&
-												!orphanedYoutubeIds.has(song.youtubeId)
+												!orphanedYoutubeIds.has(song.mediaSource)
 											) {
-												orphanedYoutubeIds.add(song.youtubeId);
+												orphanedYoutubeIds.add(song.mediaSource);
 											}
 										});
 										next();
@@ -1118,29 +1136,31 @@ class _SongsModule extends CoreClass {
 			DBModule.runJob("GET_MODEL", { modelName: "playlist" })
 				.then(playlistModel => {
 					SongsModule.runJob("GET_ORPHANED_PLAYLIST_SONGS", {}, this).then(response => {
-						const { youtubeIds } = response;
+						const { mediaSources } = response;
 						const playlistsToUpdate = new Set();
 
 						async.eachLimit(
-							youtubeIds,
+							mediaSources,
 							1,
-							(youtubeId, next) => {
+							(mediaSource, next) => {
 								async.waterfall(
 									[
 										next => {
 											this.publishProgress({
 												status: "update",
-												message: `Requesting "${youtubeId}"`
+												message: `Requesting "${mediaSource}"`
 											});
 											console.log(
-												youtubeId,
-												`this is song ${youtubeIds.indexOf(youtubeId) + 1}/${youtubeIds.length}`
+												mediaSource,
+												`this is song ${mediaSources.indexOf(mediaSource) + 1}/${
+													mediaSources.length
+												}`
 											);
 											setTimeout(next, 150);
 										},
 
 										next => {
-											MediaModule.runJob("GET_MEDIA", { youtubeId }, this)
+											MediaModule.runJob("GET_MEDIA", { mediaSource }, this)
 												.then(res => next(null, res.song))
 												.catch(next);
 										},
@@ -1149,7 +1169,7 @@ class _SongsModule extends CoreClass {
 											const { _id, title, artists, thumbnail, duration, verified } = song;
 											const trimmedSong = {
 												_id,
-												youtubeId,
+												mediaSource,
 												title,
 												artists,
 												thumbnail,
@@ -1157,7 +1177,7 @@ class _SongsModule extends CoreClass {
 												verified
 											};
 											playlistModel.updateMany(
-												{ "songs.youtubeId": song.youtubeId },
+												{ "songs.mediaSource": song.mediaSource },
 												{ $set: { "songs.$": trimmedSong } },
 												err => {
 													next(err, song);

+ 51 - 40
backend/logic/stations.js

@@ -557,12 +557,12 @@ class _StationsModule extends CoreClass {
 						const currentRequests = station.queue.filter(song => !song.requestedBy).length;
 						const songsStillNeeded = station.autofill.limit - currentRequests;
 						const currentSongs = station.queue;
-						let currentYoutubeIds = station.queue.map(song => song.youtubeId);
+						let currentYoutubeIds = station.queue.map(song => song.mediaSource);
 						const songsToAdd = [];
 						let lastSongAdded = null;
 
-						if (station.currentSong && station.currentSong.youtubeId)
-							currentYoutubeIds.push(station.currentSong.youtubeId);
+						if (station.currentSong && station.currentSong.mediaSource)
+							currentYoutubeIds.push(station.currentSong.mediaSource);
 
 						// Block for experiment: queue_autofill_skip_last_x_played
 						if (config.has(`experimental.queue_autofill_skip_last_x_played.${stationId}`)) {
@@ -583,10 +583,12 @@ class _StationsModule extends CoreClass {
 									: config.get(`experimental.weight_stations.${stationId}`);
 							const weightMap = {};
 							const getYoutubeIds = playlistSongs
-								.map(playlistSong => playlistSong.youtubeId)
-								.filter(youtubeId => currentYoutubeIds.indexOf(youtubeId) === -1);
+								.map(playlistSong => playlistSong.mediaSource)
+								.filter(mediaSource => currentYoutubeIds.indexOf(mediaSource) === -1);
 
-							const { songs } = await SongsModule.runJob("GET_SONGS", { youtubeIds: getYoutubeIds });
+							console.log(4343, getYoutubeIds);
+
+							const { songs } = await SongsModule.runJob("GET_SONGS", { mediaSources: getYoutubeIds });
 
 							const weightRegex = new RegExp(`${weightTagName}\\[(\\d+)\\]`);
 
@@ -603,13 +605,13 @@ class _StationsModule extends CoreClass {
 								weight = Math.max(1, weight);
 								weight = Math.min(10000, weight);
 
-								weightMap[song.youtubeId] = weight;
+								weightMap[song.mediaSource] = weight;
 							});
 
 							const adjustedPlaylistSongs = [];
 
 							playlistSongs.forEach(playlistSong => {
-								Array.from({ length: weightMap[playlistSong.youtubeId] }).forEach(() => {
+								Array.from({ length: weightMap[playlistSong.mediaSource] }).forEach(() => {
 									adjustedPlaylistSongs.push(playlistSong);
 								});
 							});
@@ -626,8 +628,8 @@ class _StationsModule extends CoreClass {
 						playlistSongs.every(song => {
 							if (
 								songsToAdd.length < songsStillNeeded &&
-								currentYoutubeIds.indexOf(song.youtubeId) === -1 &&
-								!songsToAdd.find(songToAdd => songToAdd.youtubeId === song.youtubeId)
+								currentYoutubeIds.indexOf(song.mediaSource) === -1 &&
+								!songsToAdd.find(songToAdd => songToAdd.mediaSource === song.mediaSource)
 							) {
 								lastSongAdded = song;
 								songsToAdd.push(song);
@@ -641,8 +643,8 @@ class _StationsModule extends CoreClass {
 
 						if (station.autofill.mode === "sequential" && lastSongAdded) {
 							const indexOfLastSong = _playlistSongs
-								.map(song => song.youtubeId)
-								.indexOf(lastSongAdded.youtubeId);
+								.map(song => song.mediaSource)
+								.indexOf(lastSongAdded.mediaSource);
 
 							if (indexOfLastSong !== -1) currentSongIndex = indexOfLastSong;
 						}
@@ -653,17 +655,17 @@ class _StationsModule extends CoreClass {
 					({ currentSongs, songsToAdd, currentSongIndex }, next) => {
 						const songs = [];
 						async.eachLimit(
-							songsToAdd.map(song => song.youtubeId),
+							songsToAdd.map(song => song.mediaSource),
 							2,
-							(youtubeId, next) => {
-								MediaModule.runJob("GET_MEDIA", { youtubeId }, this)
+							(mediaSource, next) => {
+								MediaModule.runJob("GET_MEDIA", { mediaSource }, this)
 									.then(response => {
 										const { song } = response;
 										const { _id, title, artists, thumbnail, duration, skipDuration, verified } =
 											song;
 										songs.push({
 											_id,
-											youtubeId,
+											mediaSource,
 											title,
 											artists,
 											thumbnail,
@@ -679,7 +681,7 @@ class _StationsModule extends CoreClass {
 								if (err) next(err);
 								else {
 									const newSongsToAdd = songsToAdd.map(song =>
-										songs.find(newSong => newSong.youtubeId === song.youtubeId)
+										songs.find(newSong => newSong.mediaSource === song.mediaSource)
 									);
 									next(null, currentSongs, newSongsToAdd, currentSongIndex);
 								}
@@ -760,17 +762,25 @@ class _StationsModule extends CoreClass {
 						MediaModule.runJob(
 							"GET_MEDIA",
 							{
-								youtubeId: queueSong.youtubeId
+								mediaSource: queueSong.mediaSource
 							},
 							this
 						)
 							.then(response => {
 								const { song } = response;
-								const { _id, youtubeId, title, skipDuration, artists, thumbnail, duration, verified } =
-									song;
+								const {
+									_id,
+									mediaSource,
+									title,
+									skipDuration,
+									artists,
+									thumbnail,
+									duration,
+									verified
+								} = song;
 								next(null, {
 									_id,
-									youtubeId,
+									mediaSource,
 									title,
 									skipDuration,
 									artists,
@@ -1083,19 +1093,19 @@ class _StationsModule extends CoreClass {
 									config.get(`experimental.queue_autofill_skip_last_x_played.${payload.stationId}`)
 								);
 
-								// Add youtubeId to list for this station in Redis list
+								// Add mediaSource to list for this station in Redis list
 								await CacheModule.runJob(
 									"LPUSH",
 									{
 										key: `experimental:queue_autofill_skip_last_x_played:${payload.stationId}`,
-										value: song.youtubeId
+										value: song.mediaSource
 									},
 									this
 								);
 
 								const currentListLength = await CacheModule.runJob("LLEN", { key: redisList }, this);
 
-								// Removes oldest youtubeId from list for this station in Redis list
+								// Removes oldest mediaSource from list for this station in Redis list
 								if (currentListLength > maxListLength) {
 									const amount = currentListLength - maxListLength;
 									const promises = Array.from({ length: amount }).map(() =>
@@ -1113,7 +1123,7 @@ class _StationsModule extends CoreClass {
 
 							$set.currentSong = {
 								_id: song._id,
-								youtubeId: song.youtubeId,
+								mediaSource: song.mediaSource,
 								title: song.title,
 								artists: song.artists,
 								duration: song.duration,
@@ -1145,7 +1155,7 @@ class _StationsModule extends CoreClass {
 					},
 
 					(station, next) => {
-						if (station.currentSong !== null && station.currentSong.youtubeId !== undefined) {
+						if (station.currentSong !== null && station.currentSong.mediaSource !== undefined) {
 							station.currentSong.skipVotes = 0;
 						}
 						next(null, station);
@@ -1251,10 +1261,10 @@ class _StationsModule extends CoreClass {
 					}
 
 					WSModule.runJob("GET_SOCKETS_FOR_ROOM", { room: `station.${station._id}` }).then(sockets => {
-						if (station.currentSong !== null && station.currentSong.youtubeId !== undefined) {
+						if (station.currentSong !== null && station.currentSong.mediaSource !== undefined) {
 							WSModule.runJob("SOCKETS_JOIN_SONG_ROOM", {
 								sockets,
-								room: `song.${station.currentSong.youtubeId}`
+								room: `song.${station.currentSong.mediaSource}`
 							});
 							if (!station.paused) {
 								NotificationsModule.runJob("SCHEDULE", {
@@ -1893,14 +1903,15 @@ class _StationsModule extends CoreClass {
 	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.stationId - the station id
-	 * @param {string} payload.youtubeId - the youtube id
+	 * @param {string} payload.mediaSource - the media source
 	 * @param {string} payload.requestUser - the requesting user id
 	 * @param {string} payload.requestType - the request type
 	 * @returns {Promise} - returns a promise (resolve, reject)
 	 */
 	ADD_TO_QUEUE(payload) {
 		return new Promise((resolve, reject) => {
-			const { stationId, youtubeId, requestUser, requestType } = payload;
+			const { stationId, mediaSource, requestUser, requestType } = payload;
+			console.log(45436546, mediaSource);
 			async.waterfall(
 				[
 					next => {
@@ -1914,16 +1925,16 @@ class _StationsModule extends CoreClass {
 					(station, next) => {
 						if (!station) return next("Station not found.");
 						if (!station.requests.enabled) return next("Requests are disabled in this station.");
-						if (station.currentSong && station.currentSong.youtubeId === youtubeId)
+						if (station.currentSong && station.currentSong.mediaSource === mediaSource)
 							return next("That song is currently playing.");
-						if (station.queue.find(song => song.youtubeId === youtubeId))
+						if (station.queue.find(song => song.mediaSource === mediaSource))
 							return next("That song is already in the queue.");
 
 						return next(null, station);
 					},
 
 					(station, next) => {
-						MediaModule.runJob("GET_MEDIA", { youtubeId }, this)
+						MediaModule.runJob("GET_MEDIA", { mediaSource }, this)
 							.then(response => {
 								const { song } = response;
 								const { _id, title, skipDuration, artists, thumbnail, duration, verified } = song;
@@ -1931,7 +1942,7 @@ class _StationsModule extends CoreClass {
 									null,
 									{
 										_id,
-										youtubeId,
+										mediaSource,
 										title,
 										skipDuration,
 										artists,
@@ -1969,11 +1980,11 @@ class _StationsModule extends CoreClass {
 							.flatMap(blacklistedPlaylist => blacklistedPlaylist.songs)
 							.reduce(
 								(items, item) =>
-									items.find(x => x.youtubeId === item.youtubeId) ? [...items] : [...items, item],
+									items.find(x => x.mediaSource === item.mediaSource) ? [...items] : [...items, item],
 								[]
 							);
 
-						if (blacklistedSongs.find(blacklistedSong => blacklistedSong.youtubeId === song.youtubeId))
+						if (blacklistedSongs.find(blacklistedSong => blacklistedSong.mediaSource === song.mediaSource))
 							next("That song is in an blacklisted playlist and cannot be played.");
 						else next(null, song, station);
 					},
@@ -2112,12 +2123,12 @@ class _StationsModule extends CoreClass {
 	 *
 	 * @param {object} payload - object that contains the payload
 	 * @param {string} payload.stationId - the station id
-	 * @param {string} payload.youtubeId - the youtube id
+	 * @param {string} payload.mediaSource - the media source
 	 * @returns {Promise} - returns a promise (resolve, reject)
 	 */
 	REMOVE_FROM_QUEUE(payload) {
 		return new Promise((resolve, reject) => {
-			const { stationId, youtubeId } = payload;
+			const { stationId, mediaSource } = payload;
 			async.waterfall(
 				[
 					next => {
@@ -2130,12 +2141,12 @@ class _StationsModule extends CoreClass {
 
 					(station, next) => {
 						if (!station) return next("Station not found.");
-						if (!station.queue.find(song => song.youtubeId === youtubeId))
+						if (!station.queue.find(song => song.mediaSource === mediaSource))
 							return next("That song is not currently in the queue.");
 
 						return StationsModule.stationModel.updateOne(
 							{ _id: stationId },
-							{ $pull: { queue: { youtubeId } } },
+							{ $pull: { queue: { mediaSource } } },
 							next
 						);
 					},

+ 5 - 4
backend/logic/youtube.js

@@ -1344,12 +1344,13 @@ class _YouTubeModule extends CoreClass {
 					},
 
 					(youtubeVideos, next) => {
-						const youtubeIds = youtubeVideos.map(video => video.youtubeId);
+						// TODO support spotify here
+						const mediaSources = youtubeVideos.map(video => `youtube:${video.youtubeId}`);
 						async.eachLimit(
-							youtubeIds,
+							mediaSources,
 							2,
-							(youtubeId, next) => {
-								MediaModule.runJob("RECALCULATE_RATINGS", { youtubeId }, this)
+							(mediaSource, next) => {
+								MediaModule.runJob("RECALCULATE_RATINGS", { mediaSource }, this)
 									.then(() => next())
 									.catch(next);
 							},

+ 4 - 4
frontend/src/components/ActivityItem.vue

@@ -17,7 +17,7 @@ const { openModal } = useModalsStore();
 const messageParts = computed(() => {
 	const { message } = props.activity.payload;
 	const messageParts = message.split(
-		/((?:<youtubeId>.*<\/youtubeId>)|(?:<reportId>.*<\/reportId>)|(?:<playlistId>.*<\/playlistId>)|(?:<stationId>.*<\/stationId>))/g
+		/((?:<mediaSource>.*<\/mediaSource>)|(?:<reportId>.*<\/reportId>)|(?:<playlistId>.*<\/playlistId>)|(?:<stationId>.*<\/stationId>))/g
 	);
 
 	return messageParts;
@@ -26,7 +26,7 @@ const messageStripped = computed(() => {
 	let { message } = props.activity.payload;
 
 	message = message.replace(/<reportId>(.*)<\/reportId>/g, "report");
-	message = message.replace(/<youtubeId>(.*)<\/youtubeId>/g, "$1");
+	message = message.replace(/<mediaSource>(.*)<\/mediaSource/g, "$1");
 	message = message.replace(/<playlistId>(.*)<\/playlistId>/g, `$1`);
 	message = message.replace(/<stationId>(.*)<\/stationId>/g, `$1`);
 
@@ -40,7 +40,7 @@ const getMessagePartText = messagePart => {
 	let message = messagePart;
 
 	message = message.replace(/<reportId>(.*)<\/reportId>/g, "report");
-	message = message.replace(/<youtubeId>(.*)<\/youtubeId>/g, "$1");
+	message = message.replace(/<mediaSource>(.*)<\/mediaSource>/g, "$1");
 	message = message.replace(/<playlistId>(.*)<\/playlistId>/g, `$1`);
 	message = message.replace(/<stationId>(.*)<\/stationId>/g, `$1`);
 
@@ -115,7 +115,7 @@ onMounted(() => {
 			<p :title="messageStripped" class="item-title">
 				<span v-for="messagePart in messageParts" :key="messagePart">
 					<span
-						v-if="getMessagePartType(messagePart) === 'youtubeId'"
+						v-if="getMessagePartType(messagePart) === 'mediaSource'"
 						>{{ getMessagePartText(messagePart) }}</span
 					>
 					<a

+ 6 - 5
frontend/src/components/AddToPlaylistDropdown.vue

@@ -35,22 +35,23 @@ const { setPlaylists, addPlaylist, removePlaylist } = userPlaylistsStore;
 const { openModal } = useModalsStore();
 
 const hasSong = playlist =>
-	playlist.songs.map(song => song.youtubeId).indexOf(props.song.youtubeId) !==
-	-1;
+	playlist.songs
+		.map(song => song.mediaSource)
+		.indexOf(props.song.mediaSource) !== -1;
 const toggleSongInPlaylist = playlistIndex => {
 	const playlist = playlists.value[playlistIndex];
 	if (!hasSong(playlist)) {
 		socket.dispatch(
 			"playlists.addSongToPlaylist",
 			false,
-			props.song.youtubeId,
+			props.song.mediaSource,
 			playlist._id,
 			(res: AddSongToPlaylistResponse) => new Toast(res.message)
 		);
 	} else {
 		socket.dispatch(
 			"playlists.removeSongFromPlaylist",
-			props.song.youtubeId,
+			props.song.mediaSource,
 			playlist._id,
 			(res: RemoveSongFromPlaylistResponse) => new Toast(res.message)
 		);
@@ -118,7 +119,7 @@ onMounted(() => {
 				if (playlist._id === res.data.playlistId) {
 					playlists.value[playlistIndex].songs.forEach(
 						(song, songIndex) => {
-							if (song.youtubeId === res.data.youtubeId) {
+							if (song.mediaSource === res.data.mediaSource) {
 								playlists.value[playlistIndex].songs.splice(
 									songIndex,
 									1

+ 7 - 7
frontend/src/components/PlaylistTabBase.vue

@@ -104,26 +104,26 @@ const excludedYoutubeIds = computed(() => {
 		autorequestDisallowRecentlyPlayedNumber
 	} = station.value.requests;
 
-	const youtubeIds = new Set();
+	const mediaSources = new Set();
 
 	if (autorequestDisallowRecentlyPlayedEnabled) {
 		history.value.forEach((historyItem, index) => {
 			if (index < autorequestDisallowRecentlyPlayedNumber)
-				youtubeIds.add(historyItem.payload.song.youtubeId);
+				mediaSources.add(historyItem.payload.song.mediaSource);
 		});
 	}
 
 	if (songsList.value) {
 		songsList.value.forEach(song => {
-			youtubeIds.add(song.youtubeId);
+			mediaSources.add(song.mediaSource);
 		});
 	}
 
 	if (station.value.currentSong) {
-		youtubeIds.add(station.value.currentSong.youtubeId);
+		mediaSources.add(station.value.currentSong.mediaSource);
 	}
 
-	return Array.from(youtubeIds);
+	return Array.from(mediaSources);
 });
 
 const totalUniqueAutorequestableYoutubeIds = computed(() => {
@@ -133,7 +133,7 @@ const totalUniqueAutorequestableYoutubeIds = computed(() => {
 
 	autoRequest.value.forEach(playlist => {
 		playlist.songs.forEach(song => {
-			uniqueYoutubeIds.add(song.youtubeId);
+			uniqueYoutubeIds.add(song.mediaSource);
 		});
 	});
 
@@ -143,7 +143,7 @@ const totalUniqueAutorequestableYoutubeIds = computed(() => {
 const actuallyAutorequestingYoutubeIds = computed(() => {
 	const excluded = excludedYoutubeIds.value;
 	const remaining = totalUniqueAutorequestableYoutubeIds.value.filter(
-		youtubeId => excluded.indexOf(youtubeId) === -1
+		mediaSource => excluded.indexOf(mediaSource) === -1
 	);
 	return remaining;
 });

+ 5 - 5
frontend/src/components/Queue.vue

@@ -72,11 +72,11 @@ const canRequest = () =>
 		(station.value.requests.access === "owner" &&
 			hasPermission("stations.request")));
 
-const removeFromQueue = youtubeId => {
+const removeFromQueue = mediaSource => {
 	socket.dispatch(
 		"stations.removeFromQueue",
 		station.value._id,
-		youtubeId,
+		mediaSource,
 		res => {
 			if (res.status === "success")
 				new Toast("Successfully removed song from the queue.");
@@ -158,7 +158,7 @@ defineEmits(["onChangeTab"]);
 			>
 				<draggable-list
 					v-model:list="queue"
-					item-key="youtubeId"
+					item-key="mediaSource"
 					@start="drag = true"
 					@end="drag = false"
 					@update="repositionSongInQueue"
@@ -171,7 +171,7 @@ defineEmits(["onChangeTab"]);
 							:requested-type="true"
 							:disabled-actions="[]"
 							:ref="el => (songItems[`song-item-${index}`] = el)"
-							:key="`queue-song-item-${element.youtubeId}`"
+							:key="`queue-song-item-${element.mediaSource}`"
 						>
 							<template
 								v-if="
@@ -185,7 +185,7 @@ defineEmits(["onChangeTab"]);
 									"
 									placement="left"
 									@confirm="
-										removeFromQueue(element.youtubeId)
+										removeFromQueue(element.mediaSource)
 									"
 								>
 									<i

+ 9 - 7
frontend/src/components/Request.vue

@@ -82,9 +82,9 @@ const nextPageMusareResultsCount = computed(() =>
 const songsInQueue = computed(() => {
 	if (station.value.currentSong)
 		return songsList.value
-			.map(song => song.youtubeId)
-			.concat(station.value.currentSong.youtubeId);
-	return songsList.value.map(song => song.youtubeId);
+			.map(song => song.mediaSource)
+			.concat(station.value.currentSong.mediaSource);
+	return songsList.value.map(song => song.mediaSource);
 });
 // const currentUserQueueSongs = computed(
 // 	() =>
@@ -103,11 +103,11 @@ const showTab = _tab => {
 	tab.value = _tab;
 };
 
-const addSongToQueue = (youtubeId: string, index?: number) => {
+const addSongToQueue = (mediaSource: string, index?: number) => {
 	socket.dispatch(
 		"stations.addToQueue",
 		station.value._id,
-		youtubeId,
+		mediaSource,
 		"manual",
 		res => {
 			if (res.status !== "success") new Toast(`Error: ${res.message}`);
@@ -232,7 +232,7 @@ onMounted(async () => {
 									<i
 										v-if="
 											songsInQueue.indexOf(
-												song.youtubeId
+												song.mediaSource
 											) !== -1
 										"
 										class="material-icons added-to-playlist-icon"
@@ -243,7 +243,9 @@ onMounted(async () => {
 									<i
 										v-else
 										class="material-icons add-to-queue-icon"
-										@click="addSongToQueue(song.youtubeId)"
+										@click="
+											addSongToQueue(song.mediaSource)
+										"
 										content="Add Song to Queue"
 										v-tippy
 										>queue</i

+ 5 - 5
frontend/src/components/SongItem.vue

@@ -95,12 +95,12 @@ const hoverTippy = () => {
 	hoveredTippy.value = true;
 };
 
-const viewYoutubeVideo = youtubeId => {
+const viewYoutubeVideo = mediaSource => {
 	hideTippyElements();
 	openModal({
 		modal: "viewYoutubeVideo",
 		props: {
-			youtubeId
+			mediaSource: mediaSource.split(":")[1]
 		}
 	});
 };
@@ -179,7 +179,7 @@ onUnmounted(() => {
 					<strong>
 						<user-link
 							v-if="song.requestedBy"
-							:key="song.youtubeId"
+							:key="song.mediaSource"
 							:user-id="song.requestedBy"
 						/>
 						<span v-else>station</span>
@@ -205,7 +205,7 @@ onUnmounted(() => {
 						<strong>
 							<user-link
 								v-if="song.requestedBy"
-								:key="song.youtubeId"
+								:key="song.mediaSource"
 								:user-id="song.requestedBy"
 							/>
 							<span v-else>station</span>
@@ -245,7 +245,7 @@ onUnmounted(() => {
 						<div class="icons-group">
 							<i
 								v-if="disabledActions.indexOf('youtube') === -1"
-								@click="viewYoutubeVideo(song.youtubeId)"
+								@click="viewYoutubeVideo(song.mediaSource)"
 								content="View YouTube Video"
 								v-tippy
 							>

+ 13 - 3
frontend/src/components/SongThumbnail.vue

@@ -12,7 +12,9 @@ const loadError = ref(0);
 
 const isYoutubeThumbnail = computed(
 	() =>
-		props.song.youtubeId &&
+		((props.song.mediaSource &&
+			props.song.mediaSource.startsWith("youtube:")) ||
+			props.song.youtubeId) &&
 		((props.song.thumbnail &&
 			(props.song.thumbnail.lastIndexOf("i.ytimg.com") !== -1 ||
 				props.song.thumbnail.lastIndexOf("img.youtube.com") !== -1)) ||
@@ -65,7 +67,11 @@ watch(
 			:style="{
 				'background-image':
 					'url(' +
-					`https://img.youtube.com/vi/${song.youtubeId}/mqdefault.jpg` +
+					`https://img.youtube.com/vi/${
+						song.mediaSource
+							? song.mediaSource.split(':')[1]
+							: song.youtubeId
+					}/mqdefault.jpg` +
 					')'
 			}"
 		></div>
@@ -79,7 +85,11 @@ watch(
 		<img
 			v-if="-1 < loadError && loadError < 2 && isYoutubeThumbnail"
 			loading="lazy"
-			:src="`https://img.youtube.com/vi/${song.youtubeId}/mqdefault.jpg`"
+			:src="`https://img.youtube.com/vi/${
+				song.mediaSource
+					? song.mediaSource.split(':')[1]
+					: song.youtubeId
+			}/mqdefault.jpg`"
 			@error="onLoadError"
 		/>
 		<img

+ 3 - 3
frontend/src/components/modals/BulkEditPlaylist.vue

@@ -15,7 +15,7 @@ const QuickConfirm = defineAsyncComponent(
 
 const props = defineProps({
 	modalUuid: { type: String, required: true },
-	youtubeIds: { type: Array, required: true }
+	mediaSources: { type: Array, required: true }
 });
 
 const { closeCurrentModal } = useModalsStore();
@@ -82,7 +82,7 @@ const addSongsToPlaylist = playlistId => {
 	socket.dispatch(
 		"playlists.addSongsToPlaylist",
 		playlistId,
-		props.youtubeIds,
+		props.mediaSources,
 		{
 			cb: () => {},
 			onProgress: res => {
@@ -110,7 +110,7 @@ const removeSongsFromPlaylist = playlistId => {
 	socket.dispatch(
 		"playlists.removeSongsFromPlaylist",
 		playlistId,
-		props.youtubeIds,
+		props.mediaSources,
 		{
 			cb: data => {
 				console.log("FINISHED", data);

+ 9 - 8
frontend/src/components/modals/EditPlaylist/Tabs/ImportPlaylists.vue

@@ -105,27 +105,28 @@ const importMusarePlaylistFile = () => {
 	let id;
 	let title;
 
-	let youtubeIds = [];
+	let mediaSources = [];
 
 	if (!importMusarePlaylistFileContents.value)
 		return new Toast("Please choose a Musare playlist file first.");
 
 	if (importMusarePlaylistFileContents.value.playlist) {
-		youtubeIds = importMusarePlaylistFileContents.value.playlist.songs.map(
-			song => song.youtubeId
-		);
+		mediaSources =
+			importMusarePlaylistFileContents.value.playlist.songs.map(
+				song => `youtube:${song.youtubeId}`
+			);
 	} else if (importMusarePlaylistFileContents.value.songs) {
-		youtubeIds = importMusarePlaylistFileContents.value.songs.map(
-			song => song.youtubeId
+		mediaSources = importMusarePlaylistFileContents.value.songs.map(
+			song => `youtube:${song.youtubeId}`
 		);
 	}
 
-	if (youtubeIds.length === 0) return new Toast("No songs to import.");
+	if (mediaSources.length === 0) return new Toast("No songs to import.");
 
 	return socket.dispatch(
 		"playlists.addSongsToPlaylist",
 		playlist.value._id,
-		youtubeIds,
+		mediaSources,
 		{
 			cb: res => {
 				new Toast(res.message);

+ 7 - 7
frontend/src/components/modals/EditPlaylist/index.vue

@@ -202,11 +202,11 @@ const downloadPlaylist = async () => {
 		.catch(() => new Toast("Failed to export and download playlist."));
 };
 
-const addSongToQueue = youtubeId => {
+const addSongToQueue = mediaSource => {
 	socket.dispatch(
 		"stations.addToQueue",
 		station.value._id,
-		youtubeId,
+		mediaSource,
 		"manual",
 		data => {
 			if (data.status !== "success")
@@ -274,7 +274,7 @@ onMounted(() => {
 		res => {
 			if (playlist.value._id === res.data.playlistId) {
 				// remove song from array of playlists
-				removeSong(res.data.youtubeId);
+				removeSong(res.data.mediaSource);
 			}
 		},
 		{ modalUuid: props.modalUuid }
@@ -406,7 +406,7 @@ onBeforeUnmount(() => {
 						<draggable-list
 							v-if="playlistSongs.length > 0"
 							v-model:list="playlistSongs"
-							item-key="youtubeId"
+							item-key="mediaSource"
 							@start="drag = true"
 							@end="drag = false"
 							@update="repositionSong"
@@ -422,7 +422,7 @@ onBeforeUnmount(() => {
 											(songItems[`song-item-${index}`] =
 												el)
 									"
-									:key="`playlist-song-${element.youtubeId}`"
+									:key="`playlist-song-${element.mediaSource}`"
 								>
 									<template #tippyActions>
 										<i
@@ -441,7 +441,7 @@ onBeforeUnmount(() => {
 											"
 											@click="
 												addSongToQueue(
-													element.youtubeId
+													element.mediaSource
 												)
 											"
 											content="Add Song to Queue"
@@ -458,7 +458,7 @@ onBeforeUnmount(() => {
 											placement="left"
 											@confirm="
 												removeSongFromPlaylist(
-													element.youtubeId
+													element.mediaSource
 												)
 											"
 										>

+ 84 - 67
frontend/src/components/modals/EditSong/index.vue

@@ -67,12 +67,12 @@ const {
 	tab,
 	video,
 	song,
-	youtubeId,
+	mediaSource,
 	prefillData,
 	reports,
 	newSong,
 	bulk,
-	youtubeIds,
+	mediaSources,
 	songPrefillData
 } = storeToRefs(editSongStore);
 
@@ -151,7 +151,7 @@ const songItems = ref([]);
 
 const editingItemIndex = computed(() =>
 	items.value.findIndex(
-		item => item.song.youtubeId === currentSong.value.youtubeId
+		item => item.song.mediaSource === currentSong.value.mediaSource
 	)
 );
 const filteredItems = computed({
@@ -159,20 +159,20 @@ const filteredItems = computed({
 		items.value.filter(item => (flagFilter.value ? item.flagged : true)),
 	set: (newItem: any) => {
 		const index = items.value.findIndex(
-			item => item.song.youtubeId === newItem.youtubeId
+			item => item.song.mediaSource === newItem.mediaSource
 		);
 		items.value[index] = newItem;
 	}
 });
 const filteredEditingItemIndex = computed(() =>
 	filteredItems.value.findIndex(
-		item => item.song.youtubeId === currentSong.value.youtubeId
+		item => item.song.mediaSource === currentSong.value.mediaSource
 	)
 );
 const currentSongFlagged = computed(
 	() =>
 		items.value.find(
-			item => item.song.youtubeId === currentSong.value.youtubeId
+			item => item.song.mediaSource === currentSong.value.mediaSource
 		)?.flagged
 );
 // EditSongs end
@@ -191,13 +191,13 @@ const {
 
 const { updateMediaModalPlayingAudio } = stationStore;
 
-const unloadSong = (_youtubeId, songId?) => {
+const unloadSong = (_mediaSource, songId?) => {
 	songDataLoaded.value = false;
 	songDeleted.value = false;
 	stopVideo();
 	pauseVideo(true);
 
-	resetSong(_youtubeId);
+	resetSong(_mediaSource);
 	thumbnailNotSquare.value = false;
 	thumbnailWidth.value = null;
 	thumbnailHeight.value = null;
@@ -209,9 +209,10 @@ const unloadSong = (_youtubeId, songId?) => {
 		saveButtonRefs.value.saveButton.status = "default";
 };
 
-const loadSong = (_youtubeId: string, reset?: boolean) => {
+const loadSong = (_mediaSource: string, reset?: boolean) => {
 	songNotFound.value = false;
-	socket.dispatch(`songs.getSongsFromYoutubeIds`, [_youtubeId], res => {
+	console.log(58890, _mediaSource);
+	socket.dispatch(`songs.getSongsFromMediaSources`, [_mediaSource], res => {
 		const { songs } = res.data;
 		if (res.status === "success" && songs.length > 0) {
 			let _song = songs[0];
@@ -241,9 +242,9 @@ const loadSong = (_youtubeId: string, reset?: boolean) => {
 	});
 };
 
-const onSavedSuccess = youtubeId => {
+const onSavedSuccess = mediaSource => {
 	const itemIndex = items.value.findIndex(
-		item => item.song.youtubeId === youtubeId
+		item => item.song.mediaSource === mediaSource
 	);
 	if (itemIndex > -1) {
 		items.value[itemIndex].status = "done";
@@ -251,16 +252,16 @@ const onSavedSuccess = youtubeId => {
 	}
 };
 
-const onSavedError = youtubeId => {
+const onSavedError = mediaSource => {
 	const itemIndex = items.value.findIndex(
-		item => item.song.youtubeId === youtubeId
+		item => item.song.mediaSource === mediaSource
 	);
 	if (itemIndex > -1) items.value[itemIndex].status = "error";
 };
 
-const onSaving = youtubeId => {
+const onSaving = mediaSource => {
 	const itemIndex = items.value.findIndex(
-		item => item.song.youtubeId === youtubeId
+		item => item.song.mediaSource === mediaSource
 	);
 	if (itemIndex > -1) items.value[itemIndex].status = "saving";
 };
@@ -316,15 +317,15 @@ const { inputs, unsavedChanges, save, setValue, setOriginalValue } = useForm(
 				return true;
 			}
 		},
-		youtubeId: {
+		mediaSource: {
 			value: "",
 			validate: value => {
 				if (
 					!newSong.value &&
 					youtubeError.value &&
-					inputs.value.youtubeId.originalValue !== value
+					inputs.value.mediaSource.originalValue !== value
 				)
-					return "You're not allowed to change the YouTube id while the player is not working";
+					return "You're not allowed to change the media source while the player is not working";
 				return true;
 			}
 		},
@@ -415,8 +416,8 @@ const { inputs, unsavedChanges, save, setValue, setOriginalValue } = useForm(
 				}
 				new Toast(res.message);
 				saveButtonRef.handleSuccessfulSave();
-				onSavedSuccess(values.youtubeId);
-				if (newSong.value) loadSong(values.youtubeId, true);
+				onSavedSuccess(values.mediaSource);
+				if (newSong.value) loadSong(values.mediaSource, true);
 				else setSong(mergedValues);
 				resolve();
 			};
@@ -433,13 +434,13 @@ const { inputs, unsavedChanges, save, setValue, setOriginalValue } = useForm(
 			if (status === "unchanged") {
 				new Toast(messages.unchanged);
 				saveButtonRef.handleSuccessfulSave();
-				onSavedSuccess(values.youtubeId);
+				onSavedSuccess(values.mediaSource);
 			} else {
 				Object.values(messages).forEach(message => {
 					new Toast({ content: message, timeout: 8000 });
 				});
 				saveButtonRef.handleFailedSave();
-				onSavedError(values.youtubeId);
+				onSavedError(values.mediaSource);
 			}
 			resolve();
 		}
@@ -515,13 +516,13 @@ const onCloseOrNext = (next?: boolean): Promise<void> =>
 const pickSong = song => {
 	onCloseOrNext(true).then(() => {
 		editSong({
-			youtubeId: song.youtubeId,
-			prefill: songPrefillData.value[song.youtubeId]
+			mediaSource: song.mediaSource,
+			prefill: songPrefillData.value[song.mediaSource]
 		});
 		currentSong.value = song;
-		if (songItems.value[`edit-songs-item-${song.youtubeId}`])
+		if (songItems.value[`edit-songs-item-${song.mediaSource}`])
 			songItems.value[
-				`edit-songs-item-${song.youtubeId}`
+				`edit-songs-item-${song.mediaSource}`
 			].scrollIntoView();
 	});
 };
@@ -549,7 +550,7 @@ const editNextSong = () => {
 
 const saveSong = (refName: string, closeOrNext?: boolean) => {
 	saveButtonRefName.value = refName;
-	onSaving(inputs.value.youtubeId.value);
+	onSaving(inputs.value.mediaSource.value);
 	save(() => {
 		if (closeOrNext && bulk.value) editNextSong();
 		else if (closeOrNext) closeCurrentModal();
@@ -600,7 +601,8 @@ const onThumbnailLoadError = error => {
 const isYoutubeThumbnail = computed(
 	() =>
 		songDataLoaded.value &&
-		inputs.value.youtubeId.value &&
+		inputs.value.mediaSource.value &&
+		inputs.value.mediaSource.value.startsWith("youtube:") &&
 		inputs.value.thumbnail.value &&
 		(inputs.value.thumbnail.value.lastIndexOf("i.ytimg.com") !== -1 ||
 			inputs.value.thumbnail.value.lastIndexOf("img.youtube.com") !== -1)
@@ -686,7 +688,9 @@ const getYouTubeData = type => {
 	}
 	if (type === "thumbnail")
 		setValue({
-			thumbnail: `https://img.youtube.com/vi/${inputs.value.youtubeId.value}/mqdefault.jpg`
+			thumbnail: `https://img.youtube.com/vi/${
+				inputs.value.mediaSource.value.split(":")[1]
+			}/mqdefault.jpg`
 		});
 	if (type === "author") {
 		try {
@@ -744,12 +748,12 @@ const settings = type => {
 
 const play = () => {
 	if (
-		video.value.player.getVideoData().video_id !==
-		inputs.value.youtubeId.value
+		inputs.value.mediaSource.value !==
+		`youtube:${video.value.player.getVideoData().video_id}`
 	) {
 		setValue({ duration: -1 });
 		loadVideoById(
-			inputs.value.youtubeId.value,
+			inputs.value.mediaSource.value.split(":")[1],
 			inputs.value.skipDuration.value
 		);
 	}
@@ -882,14 +886,14 @@ const sendActivityWatchVideoData = () => {
 			artists: inputs.value.artists.value
 				? inputs.value.artists.value.join(", ")
 				: null,
-			youtubeId: inputs.value.youtubeId.value,
+			mediaSource: inputs.value.mediaSource.value,
 			muted: muted.value,
 			volume: volumeSliderValue.value,
 			startedDuration:
 				activityWatchVideoLastStartDuration.value <= 0
 					? 0
 					: activityWatchVideoLastStartDuration.value,
-			source: `editSong#${inputs.value.youtubeId.value}`,
+			source: `editSong#${inputs.value.mediaSource.value}`,
 			hostname: window.location.hostname,
 			playerState: Object.keys(window.YT.PlayerState).find(
 				key =>
@@ -915,16 +919,20 @@ watch(
 	[() => inputs.value.duration.value, () => inputs.value.skipDuration.value],
 	() => drawCanvas()
 );
-watch(youtubeId, (_youtubeId, _oldYoutubeId) => {
-	if (_oldYoutubeId) unloadSong(_oldYoutubeId);
-	if (_youtubeId) loadSong(_youtubeId, true);
+watch(mediaSource, (_mediaSource, _oldMediaSource) => {
+	if (_oldMediaSource) unloadSong(_oldMediaSource);
+	if (_mediaSource) loadSong(_mediaSource, true);
 });
 watch(
-	() => inputs.value.youtubeId.value,
+	() => inputs.value.mediaSource.value,
 	value => {
-		if (video.value.player && video.value.player.cueVideoById)
+		if (
+			video.value.player &&
+			video.value.player.cueVideoById &&
+			value.startsWith("youtube:")
+		)
 			video.value.player.cueVideoById(
-				value,
+				value.split(":")[1],
 				inputs.value.skipDuration.value
 			);
 	}
@@ -955,9 +963,9 @@ onMounted(async () => {
 	useHTTPS.value = await lofig.get("cookie.secure");
 
 	socket.onConnect(() => {
-		if (newSong.value && !youtubeId.value && !bulk.value) {
+		if (newSong.value && !mediaSource.value && !bulk.value) {
 			setSong({
-				youtubeId: "",
+				mediaSource: "",
 				title: "",
 				artists: [],
 				genres: [],
@@ -969,7 +977,7 @@ onMounted(async () => {
 			});
 			songDataLoaded.value = true;
 			showTab("youtube");
-		} else if (youtubeId.value) loadSong(youtubeId.value);
+		} else if (mediaSource.value) loadSong(mediaSource.value);
 		else if (!bulk.value) {
 			new Toast("You can't open EditSong without editing a song");
 			return closeCurrentModal();
@@ -996,8 +1004,8 @@ onMounted(async () => {
 				playerReady.value &&
 				video.value.player.getVideoData &&
 				video.value.player.getVideoData() &&
-				video.value.player.getVideoData().video_id ===
-					inputs.value.youtubeId.value
+				`youtube:${video.value.player.getVideoData().video_id}` ===
+					inputs.value.mediaSource.value
 			) {
 				const currentTime = video.value.player.getCurrentTime();
 
@@ -1054,9 +1062,16 @@ onMounted(async () => {
 
 							playerReady.value = true;
 
-							if (inputs.value.youtubeId.value)
+							if (
+								inputs.value.mediaSource.value &&
+								inputs.value.mediaSource.value.startsWith(
+									"youtube:"
+								)
+							)
 								video.value.player.cueVideoById(
-									inputs.value.youtubeId.value,
+									inputs.value.mediaSource.value.split(
+										":"
+									)[1],
 									inputs.value.skipDuration.value
 								);
 
@@ -1198,9 +1213,11 @@ onMounted(async () => {
 		if (bulk.value) {
 			socket.dispatch("apis.joinRoom", "edit-songs");
 
+			console.log(68768, mediaSources.value);
+
 			socket.dispatch(
-				"songs.getSongsFromYoutubeIds",
-				youtubeIds.value,
+				"songs.getSongsFromMediaSources",
+				mediaSources.value,
 				res => {
 					if (res.data.songs.length === 0) {
 						closeCurrentModal();
@@ -1229,7 +1246,7 @@ onMounted(async () => {
 					duration: res.data.song.duration,
 					skipDuration: res.data.song.skipDuration,
 					thumbnail: res.data.song.thumbnail,
-					youtubeId: res.data.song.youtubeId,
+					mediaSource: res.data.song.mediaSource,
 					verified: res.data.song.verified,
 					artists: res.data.song.artists,
 					genres: res.data.song.genres,
@@ -1238,8 +1255,8 @@ onMounted(async () => {
 				});
 			if (bulk.value) {
 				const index = items.value
-					.map(item => item.song.youtubeId)
-					.indexOf(res.data.song.youtubeId);
+					.map(item => item.song.mediaSource)
+					.indexOf(res.data.song.mediaSource);
 				if (index >= 0)
 					items.value[index].song = {
 						...items.value[index].song,
@@ -1267,8 +1284,8 @@ onMounted(async () => {
 			`event:admin.song.created`,
 			res => {
 				const index = items.value
-					.map(item => item.song.youtubeId)
-					.indexOf(res.data.song.youtubeId);
+					.map(item => item.song.mediaSource)
+					.indexOf(res.data.song.mediaSource);
 				if (index >= 0)
 					items.value[index].song = {
 						...items.value[index].song,
@@ -1459,7 +1476,7 @@ onBeforeUnmount(() => {
 		socket.dispatch("apis.leaveRoom", "edit-songs");
 	}
 
-	unloadSong(youtubeId.value, song.value._id);
+	unloadSong(mediaSource.value, song.value._id);
 
 	updateMediaModalPlayingAudio(false);
 
@@ -1540,7 +1557,7 @@ onBeforeUnmount(() => {
 								:ref="
 									el =>
 										(songItems[
-											`edit-songs-item-${data.song.youtubeId}`
+											`edit-songs-item-${data.song.mediaSource}`
 										] = el)
 								"
 							>
@@ -1561,8 +1578,8 @@ onBeforeUnmount(() => {
 									<template #leftIcon>
 										<i
 											v-if="
-												currentSong.youtubeId ===
-													data.song.youtubeId &&
+												currentSong.mediaSource ===
+													data.song.mediaSource &&
 												!data.song.removed
 											"
 											class="material-icons item-icon editing-icon"
@@ -1675,7 +1692,7 @@ onBeforeUnmount(() => {
 				></div>
 			</template>
 			<template #body>
-				<div v-if="!youtubeId && !newSong" class="notice-container">
+				<div v-if="!mediaSource && !newSong" class="notice-container">
 					<h4>No song has been selected</h4>
 				</div>
 				<div v-if="songDeleted" class="notice-container">
@@ -1683,7 +1700,7 @@ onBeforeUnmount(() => {
 				</div>
 				<div
 					v-if="
-						youtubeId &&
+						mediaSource &&
 						!songDataLoaded &&
 						!songNotFound &&
 						!newSong
@@ -1693,7 +1710,7 @@ onBeforeUnmount(() => {
 					<h4>Song hasn't loaded yet</h4>
 				</div>
 				<div
-					v-if="youtubeId && songNotFound && !newSong"
+					v-if="mediaSource && songNotFound && !newSong"
 					class="notice-container"
 				>
 					<h4>Song was not found</h4>
@@ -1886,7 +1903,7 @@ onBeforeUnmount(() => {
 						<song-thumbnail
 							v-if="songDataLoaded && !songDeleted"
 							:song="{
-								youtubeId: inputs['youtubeId'].value,
+								mediaSource: inputs['mediaSource'].value,
 								thumbnail: inputs['thumbnail'].value
 							}"
 							:fallback="false"
@@ -2051,13 +2068,13 @@ onBeforeUnmount(() => {
 								</p>
 							</div>
 							<div class="youtube-id-container">
-								<label class="label">YouTube ID</label>
+								<label class="label">Media source</label>
 								<p class="control">
 									<input
 										class="input"
 										type="text"
-										placeholder="Enter YouTube ID..."
-										v-model="inputs['youtubeId'].value"
+										placeholder="Enter Media source..."
+										v-model="inputs['mediaSource'].value"
 									/>
 								</p>
 							</div>
@@ -2309,7 +2326,7 @@ onBeforeUnmount(() => {
 					<button
 						class="button is-primary"
 						@click="toggleFlag()"
-						v-if="youtubeId && !songDeleted"
+						v-if="mediaSource && !songDeleted"
 					>
 						{{ currentSongFlagged ? "Unflag" : "Flag" }}
 					</button>

+ 32 - 24
frontend/src/components/modals/ImportAlbum.vue

@@ -83,7 +83,7 @@ const startEditingSongs = () => {
 			delete album.gotMoreInfo;
 
 			const songToEdit: {
-				youtubeId: string;
+				mediaSource: string;
 				prefill: {
 					discogs: typeof album;
 					title?: string;
@@ -92,7 +92,7 @@ const startEditingSongs = () => {
 					artists?: string[];
 				};
 			} = {
-				youtubeId: song.youtubeId,
+				mediaSource: song.mediaSource,
 				prefill: {
 					discogs: album
 				}
@@ -179,27 +179,35 @@ const importPlaylist = () => {
 		true,
 		res => {
 			isImportingPlaylist.value = false;
-			const youtubeIds = res.videos.map(video => video.youtubeId);
-
-			socket.dispatch("songs.getSongsFromYoutubeIds", youtubeIds, res => {
-				if (res.status === "success") {
-					const songs = res.data.songs.filter(song => !song.verified);
-					const songsAlreadyVerified =
-						res.data.songs.length - songs.length;
-					setPlaylistSongs(songs);
-					if (discogsAlbum.value.tracks) {
-						trackSongs.value = discogsAlbum.value.tracks.map(
-							() => []
+			const mediaSources = res.videos.map(
+				video => `youtube:${video.youtubeId}`
+			);
+
+			socket.dispatch(
+				"songs.getSongsFromMediaSources",
+				mediaSources,
+				res => {
+					if (res.status === "success") {
+						const songs = res.data.songs.filter(
+							song => !song.verified
 						);
-						tryToAutoMove();
+						const songsAlreadyVerified =
+							res.data.songs.length - songs.length;
+						setPlaylistSongs(songs);
+						if (discogsAlbum.value.tracks) {
+							trackSongs.value = discogsAlbum.value.tracks.map(
+								() => []
+							);
+							tryToAutoMove();
+						}
+						if (songsAlreadyVerified > 0)
+							new Toast(
+								`${songsAlreadyVerified} songs were already verified, skipping those.`
+							);
 					}
-					if (songsAlreadyVerified > 0)
-						new Toast(
-							`${songsAlreadyVerified} songs were already verified, skipping those.`
-						);
+					new Toast("Could not get songs.");
 				}
-				new Toast("Could not get songs.");
-			});
+			);
 
 			return new Toast({ content: res.message, timeout: 20000 });
 		}
@@ -628,12 +636,12 @@ onBeforeUnmount(() => {
 					<draggable-list
 						v-if="playlistSongs.length > 0"
 						v-model:list="playlistSongs"
-						item-key="youtubeId"
+						item-key="mediaSource"
 						:group="`import-album-${modalUuid}-songs`"
 					>
 						<template #item="{ element }">
 							<song-item
-								:key="`playlist-song-${element.youtubeId}`"
+								:key="`playlist-song-${element.mediaSource}`"
 								:song="element"
 							>
 							</song-item>
@@ -657,12 +665,12 @@ onBeforeUnmount(() => {
 						<div class="track-box-songs-drag-area">
 							<draggable-list
 								v-model:list="trackSongs[index]"
-								item-key="youtubeId"
+								item-key="mediaSource"
 								:group="`import-album-${modalUuid}-songs`"
 							>
 								<template #item="{ element }">
 									<song-item
-										:key="`track-song-${element.youtubeId}`"
+										:key="`track-song-${element.mediaSource}`"
 										:song="element"
 									>
 									</song-item>

+ 4 - 3
frontend/src/components/modals/ManageStation/index.vue

@@ -392,7 +392,7 @@ onMounted(() => {
 					if (stationPlaylist.value._id === res.data.playlistId) {
 						// remove song from array of playlists
 						stationPlaylist.value.songs.forEach((song, index) => {
-							if (song.youtubeId === res.data.youtubeId)
+							if (song.mediaSource === res.data.mediaSource)
 								stationPlaylist.value.songs.splice(index, 1);
 						});
 					}
@@ -412,7 +412,8 @@ onMounted(() => {
 								(song, index) => {
 									// find song locally
 									if (
-										song.youtubeId === changedSong.youtubeId
+										song.mediaSource ===
+										changedSong.mediaSource
 									) {
 										// change song position attribute
 										stationPlaylist.value.songs[
@@ -592,7 +593,7 @@ onBeforeUnmount(() => {
 					</div>
 					<hr class="section-horizontal-rule" />
 					<song-item
-						v-if="currentSong.youtubeId"
+						v-if="currentSong.mediaSource"
 						:song="currentSong"
 						:requested-by="true"
 						header="Currently Playing.."

+ 1 - 1
frontend/src/components/modals/Report.vue

@@ -180,7 +180,7 @@ const { inputs, save } = useForm(
 					"reports.create",
 					{
 						issues,
-						youtubeId: props.song.youtubeId
+						mediaSource: props.song.mediaSource
 					},
 					res => {
 						if (res.status === "success") {

+ 212 - 223
frontend/src/components/modals/ViewYoutubeVideo.vue

@@ -1,5 +1,11 @@
 <script setup lang="ts">
-import { defineAsyncComponent, onMounted, onBeforeUnmount, ref } from "vue";
+import {
+	defineAsyncComponent,
+	onMounted,
+	onBeforeUnmount,
+	ref,
+	computed
+} from "vue";
 import Toast from "toasters";
 import { storeToRefs } from "pinia";
 import aw from "@/aw";
@@ -53,6 +59,14 @@ const { socket } = useWebsocketsStore();
 const userAuthStore = useUserAuthStore();
 const { hasPermission } = userAuthStore;
 
+const youtubeId = computed(() => {
+	if (props.videoId && props.videoId.startsWith("youtube:"))
+		return props.videoId.split(":")[1];
+	if (props.youtubeId && props.youtubeId.startsWith("youtube:"))
+		return props.youtubeId.split(":")[1];
+	return props.videoId || props.youtubeId;
+});
+
 const remove = () => {
 	socket.dispatch("youtube.removeVideos", video.value._id, res => {
 		if (res.status === "success") {
@@ -213,251 +227,226 @@ const sendActivityWatchVideoData = () => {
 onMounted(() => {
 	socket.onConnect(() => {
 		loaded.value = false;
-		socket.dispatch(
-			"youtube.getVideo",
-			props.videoId || props.youtubeId,
-			true,
-			res => {
-				if (res.status === "success") {
-					const youtubeVideo = res.data;
-					viewYoutubeVideo(youtubeVideo);
-					loaded.value = true;
-
-					interval.value = setInterval(() => {
-						if (
-							video.value.duration !== -1 &&
-							player.value.paused === false &&
-							player.value.playerReady &&
-							(player.value.player.getCurrentTime() >
-								video.value.duration ||
-								(player.value.player.getCurrentTime() > 0 &&
-									player.value.player.getCurrentTime() >=
-										player.value.player.getDuration()))
-						) {
-							stopVideo();
-							pauseVideo(true);
-							drawCanvas();
-						}
-						if (
-							player.value.playerReady &&
-							player.value.player.getVideoData &&
-							player.value.player.getVideoData() &&
-							player.value.player.getVideoData().video_id ===
-								video.value.youtubeId
-						) {
-							const currentTime =
-								player.value.player.getCurrentTime();
-
-							if (currentTime !== undefined)
-								player.value.currentTime =
-									currentTime.toFixed(3);
-
-							if (player.value.duration.indexOf(".000") !== -1) {
-								const duration =
-									player.value.player.getDuration();
-
-								if (duration !== undefined) {
-									if (
-										`${player.value.duration}` ===
-										`${Number(video.value.duration).toFixed(
-											3
-										)}`
-									)
-										video.value.duration =
-											duration.toFixed(3);
-
-									player.value.duration = duration.toFixed(3);
-									if (
-										player.value.duration.indexOf(
-											".000"
-										) !== -1
-									)
-										player.value.videoNote = "(~)";
-									else player.value.videoNote = "";
-
-									drawCanvas();
-								}
+		socket.dispatch("youtube.getVideo", youtubeId.value, true, res => {
+			if (res.status === "success") {
+				const youtubeVideo = res.data;
+				viewYoutubeVideo(youtubeVideo);
+				loaded.value = true;
+
+				interval.value = setInterval(() => {
+					if (
+						video.value.duration !== -1 &&
+						player.value.paused === false &&
+						player.value.playerReady &&
+						(player.value.player.getCurrentTime() >
+							video.value.duration ||
+							(player.value.player.getCurrentTime() > 0 &&
+								player.value.player.getCurrentTime() >=
+									player.value.player.getDuration()))
+					) {
+						stopVideo();
+						pauseVideo(true);
+						drawCanvas();
+					}
+					if (
+						player.value.playerReady &&
+						player.value.player.getVideoData &&
+						player.value.player.getVideoData() &&
+						player.value.player.getVideoData().video_id ===
+							video.value.youtubeId
+					) {
+						const currentTime =
+							player.value.player.getCurrentTime();
+
+						if (currentTime !== undefined)
+							player.value.currentTime = currentTime.toFixed(3);
+
+						if (player.value.duration.indexOf(".000") !== -1) {
+							const duration = player.value.player.getDuration();
+
+							if (duration !== undefined) {
+								if (
+									`${player.value.duration}` ===
+									`${Number(video.value.duration).toFixed(3)}`
+								)
+									video.value.duration = duration.toFixed(3);
+
+								player.value.duration = duration.toFixed(3);
+								if (
+									player.value.duration.indexOf(".000") !== -1
+								)
+									player.value.videoNote = "(~)";
+								else player.value.videoNote = "";
+
+								drawCanvas();
 							}
 						}
+					}
 
-						if (player.value.paused === false) drawCanvas();
-					}, 200);
-
-					activityWatchVideoDataInterval.value = setInterval(() => {
-						sendActivityWatchVideoData();
-					}, 1000);
-
-					if (window.YT && window.YT.Player) {
-						player.value.player = new window.YT.Player(
-							`viewYoutubeVideoPlayer-${props.modalUuid}`,
-							{
-								height: 298,
-								width: 530,
-								videoId: null,
-								host: "https://www.youtube-nocookie.com",
-								playerVars: {
-									controls: 0,
-									iv_load_policy: 3,
-									rel: 0,
-									showinfo: 0,
-									autoplay: 0
-								},
-								events: {
-									onReady: () => {
-										let volume = parseFloat(
-											localStorage.getItem("volume")
+					if (player.value.paused === false) drawCanvas();
+				}, 200);
+
+				activityWatchVideoDataInterval.value = setInterval(() => {
+					sendActivityWatchVideoData();
+				}, 1000);
+
+				if (window.YT && window.YT.Player) {
+					player.value.player = new window.YT.Player(
+						`viewYoutubeVideoPlayer-${props.modalUuid}`,
+						{
+							height: 298,
+							width: 530,
+							videoId: null,
+							host: "https://www.youtube-nocookie.com",
+							playerVars: {
+								controls: 0,
+								iv_load_policy: 3,
+								rel: 0,
+								showinfo: 0,
+								autoplay: 0
+							},
+							events: {
+								onReady: () => {
+									let volume = parseFloat(
+										localStorage.getItem("volume")
+									);
+									volume =
+										typeof volume === "number"
+											? volume
+											: 20;
+									player.value.player.setVolume(volume);
+									if (volume > 0)
+										player.value.player.unMute();
+
+									player.value.playerReady = true;
+
+									if (video.value && video.value._id)
+										player.value.player.cueVideoById(
+											video.value.youtubeId
 										);
-										volume =
-											typeof volume === "number"
-												? volume
-												: 20;
-										player.value.player.setVolume(volume);
-										if (volume > 0)
-											player.value.player.unMute();
-
-										player.value.playerReady = true;
-
-										if (video.value && video.value._id)
-											player.value.player.cueVideoById(
-												video.value.youtubeId
-											);
 
-										setPlaybackRate();
+									setPlaybackRate();
 
-										drawCanvas();
-									},
-									onStateChange: event => {
-										drawCanvas();
+									drawCanvas();
+								},
+								onStateChange: event => {
+									drawCanvas();
 
-										if (event.data === 1) {
-											player.value.paused = false;
-											updateMediaModalPlayingAudio(true);
-											const youtubeDuration =
-												player.value.player.getDuration();
-											const newYoutubeVideoDuration =
-												youtubeDuration.toFixed(3);
+									if (event.data === 1) {
+										player.value.paused = false;
+										updateMediaModalPlayingAudio(true);
+										const youtubeDuration =
+											player.value.player.getDuration();
+										const newYoutubeVideoDuration =
+											youtubeDuration.toFixed(3);
+
+										if (
+											player.value.duration.indexOf(
+												".000"
+											) !== -1 &&
+											`${player.value.duration}` !==
+												`${newYoutubeVideoDuration}`
+										) {
+											const songDurationNumber = Number(
+												video.value.duration
+											);
+											const songDurationNumber2 =
+												Number(video.value.duration) +
+												1;
+											const songDurationNumber3 =
+												Number(video.value.duration) -
+												1;
+											const fixedSongDuration =
+												songDurationNumber.toFixed(3);
+											const fixedSongDuration2 =
+												songDurationNumber2.toFixed(3);
+											const fixedSongDuration3 =
+												songDurationNumber3.toFixed(3);
 
 											if (
-												player.value.duration.indexOf(
-													".000"
-												) !== -1 &&
-												`${player.value.duration}` !==
-													`${newYoutubeVideoDuration}`
-											) {
-												const songDurationNumber =
-													Number(
+												`${player.value.duration}` ===
+													`${Number(
 														video.value.duration
-													);
-												const songDurationNumber2 =
-													Number(
-														video.value.duration
-													) + 1;
-												const songDurationNumber3 =
-													Number(
-														video.value.duration
-													) - 1;
-												const fixedSongDuration =
-													songDurationNumber.toFixed(
-														3
-													);
-												const fixedSongDuration2 =
-													songDurationNumber2.toFixed(
-														3
-													);
-												const fixedSongDuration3 =
-													songDurationNumber3.toFixed(
-														3
-													);
-
-												if (
-													`${player.value.duration}` ===
-														`${Number(
-															video.value.duration
-														).toFixed(3)}` &&
-													(fixedSongDuration ===
+													).toFixed(3)}` &&
+												(fixedSongDuration ===
+													player.value.duration ||
+													fixedSongDuration2 ===
 														player.value.duration ||
-														fixedSongDuration2 ===
-															player.value
-																.duration ||
-														fixedSongDuration3 ===
-															player.value
-																.duration)
-												)
-													video.value.duration =
-														newYoutubeVideoDuration;
-
-												player.value.duration =
+													fixedSongDuration3 ===
+														player.value.duration)
+											)
+												video.value.duration =
 													newYoutubeVideoDuration;
-												if (
-													player.value.duration.indexOf(
-														".000"
-													) !== -1
-												)
-													player.value.videoNote =
-														"(~)";
-												else
-													player.value.videoNote = "";
-											}
-
-											if (video.value.duration === -1)
-												video.value.duration = Number(
-													player.value.duration
-												);
 
+											player.value.duration =
+												newYoutubeVideoDuration;
 											if (
-												video.value.duration >
-												youtubeDuration + 1
-											) {
-												stopVideo();
-												pauseVideo(true);
-												return new Toast(
-													"Video can't play. Specified duration is bigger than the YouTube song duration."
-												);
-											}
-											if (video.value.duration <= 0) {
-												stopVideo();
-												pauseVideo(true);
-												return new Toast(
-													"Video can't play. Specified duration has to be more than 0 seconds."
-												);
-											}
-
-											setPlaybackRate();
-										} else if (event.data === 2) {
-											player.value.paused = true;
-											updateMediaModalPlayingAudio(false);
+												player.value.duration.indexOf(
+													".000"
+												) !== -1
+											)
+												player.value.videoNote = "(~)";
+											else player.value.videoNote = "";
 										}
 
-										return false;
+										if (video.value.duration === -1)
+											video.value.duration = Number(
+												player.value.duration
+											);
+
+										if (
+											video.value.duration >
+											youtubeDuration + 1
+										) {
+											stopVideo();
+											pauseVideo(true);
+											return new Toast(
+												"Video can't play. Specified duration is bigger than the YouTube song duration."
+											);
+										}
+										if (video.value.duration <= 0) {
+											stopVideo();
+											pauseVideo(true);
+											return new Toast(
+												"Video can't play. Specified duration has to be more than 0 seconds."
+											);
+										}
+
+										setPlaybackRate();
+									} else if (event.data === 2) {
+										player.value.paused = true;
+										updateMediaModalPlayingAudio(false);
 									}
+
+									return false;
 								}
 							}
-						);
-					} else {
-						updatePlayer({
-							error: true,
-							errorMessage: "Player could not be loaded."
-						});
-					}
-
-					let volume = parseFloat(localStorage.getItem("volume"));
-					volume =
-						typeof volume === "number" && !Number.isNaN(volume)
-							? volume
-							: 20;
-					localStorage.setItem("volume", volume.toString());
-					updatePlayer({ volume });
-
-					socket.dispatch(
-						"apis.joinRoom",
-						`view-youtube-video.${video.value._id}`
+						}
 					);
 				} else {
-					new Toast("YouTube video with that ID not found");
-					closeCurrentModal();
+					updatePlayer({
+						error: true,
+						errorMessage: "Player could not be loaded."
+					});
 				}
+
+				let volume = parseFloat(localStorage.getItem("volume"));
+				volume =
+					typeof volume === "number" && !Number.isNaN(volume)
+						? volume
+						: 20;
+				localStorage.setItem("volume", volume.toString());
+				updatePlayer({ volume });
+
+				socket.dispatch(
+					"apis.joinRoom",
+					`view-youtube-video.${video.value._id}`
+				);
+			} else {
+				new Toast("YouTube video with that ID not found");
+				closeCurrentModal();
 			}
-		);
+		});
 	});
 
 	socket.on(

+ 1 - 1
frontend/src/composables/useSortablePlaylists.ts

@@ -105,7 +105,7 @@ export const useSortablePlaylists = () => {
 					if (playlist._id === res.data.playlistId) {
 						playlists.value[playlistIndex].songs.forEach(
 							(song, songIndex) => {
-								if (song.youtubeId === res.data.youtubeId) {
+								if (song.mediaSource === res.data.mediaSource) {
 									playlists.value[playlistIndex].songs.splice(
 										songIndex,
 										1

+ 1 - 1
frontend/src/composables/useYoutubeDirect.ts

@@ -68,7 +68,7 @@ export const useYoutubeDirect = () => {
 			socket.dispatch(
 				"stations.addToQueue",
 				stationId,
-				youtubeVideoId,
+				`youtube:${youtubeVideoId}`,
 				"manual",
 				res => {
 					if (res.status !== "success")

+ 5 - 5
frontend/src/pages/Admin/Reports.vue

@@ -52,9 +52,9 @@ const columns = ref<TableColumn[]>([
 	},
 	{
 		name: "songYoutubeId",
-		displayName: "Song YouTube ID",
+		displayName: "Song media source",
 		properties: ["song"],
-		sortProperty: "song.youtubeId",
+		sortProperty: "song.mediaSource",
 		minWidth: 165,
 		defaultWidth: 165
 	},
@@ -102,8 +102,8 @@ const filters = ref<TableFilter[]>([
 	},
 	{
 		name: "songYoutubeId",
-		displayName: "Song YouTube ID",
-		property: "song.youtubeId",
+		displayName: "Song media source",
+		property: "song.mediaSource",
 		filterTypes: ["contains", "exact", "regex"],
 		defaultFilterType: "contains"
 	},
@@ -244,7 +244,7 @@ const resolve = (reportId, value) =>
 					"
 					target="_blank"
 				>
-					{{ slotProps.item.song.youtubeId }}
+					{{ slotProps.item.song.mediaSource }}
 				</a>
 			</template>
 			<template #column-resolved="slotProps">

+ 14 - 7
frontend/src/pages/Admin/Songs/Import.vue

@@ -9,6 +9,8 @@ import { useUserAuthStore } from "@/stores/userAuth";
 import { TableColumn, TableFilter, TableEvents } from "@/types/advancedTable";
 import utils from "@/utils";
 
+// TODO make this page support Spotify
+
 const AdvancedTable = defineAsyncComponent(
 	() => import("@/components/AdvancedTable.vue")
 );
@@ -340,15 +342,19 @@ const submitCreateImport = stage => {
 // 	if (stage === 2) createImport.value.stage = 1;
 // };
 
-const editSongs = videos => {
-	const songs = videos.map(youtubeId => ({ youtubeId }));
-	if (songs.length === 1)
-		openModal({ modal: "editSong", props: { song: songs[0] } });
-	else openModal({ modal: "editSong", props: { songs } });
+const editSongs = youtubeIds => {
+	const mediaSources = youtubeIds.map(youtubeId => ({
+		mediaSource: `youtube:${youtubeId}`
+	}));
+	console.log(59685486, youtubeIds, mediaSources);
+	if (mediaSources.length === 1)
+		openModal({ modal: "editSong", props: { song: mediaSources[0] } });
+	else openModal({ modal: "editSong", props: { songs: mediaSources } });
 };
 
 const importAlbum = youtubeIds => {
-	socket.dispatch("songs.getSongsFromYoutubeIds", youtubeIds, res => {
+	const mediaSources = youtubeIds.map(youtubeId => `youtube:${youtubeId}`);
+	socket.dispatch("songs.getSongsFromMediaSources", mediaSources, res => {
 		if (res.status === "success") {
 			openModal({
 				modal: "importAlbum",
@@ -359,10 +365,11 @@ const importAlbum = youtubeIds => {
 };
 
 const bulkEditPlaylist = youtubeIds => {
+	const mediaSources = youtubeIds.map(youtubeId => `youtube:${youtubeId}`);
 	openModal({
 		modal: "bulkEditPlaylist",
 		props: {
-			youtubeIds
+			mediaSources
 		}
 	});
 };

+ 13 - 13
frontend/src/pages/Admin/Songs/index.vue

@@ -46,7 +46,7 @@ const columns = ref<TableColumn[]>([
 	{
 		name: "options",
 		displayName: "Options",
-		properties: ["_id", "verified", "youtubeId"],
+		properties: ["_id", "verified", "mediaSource"],
 		sortable: false,
 		hidable: false,
 		resizable: false,
@@ -106,10 +106,10 @@ const columns = ref<TableColumn[]>([
 		defaultWidth: 215
 	},
 	{
-		name: "youtubeId",
-		displayName: "YouTube ID",
-		properties: ["youtubeId"],
-		sortProperty: "youtubeId",
+		name: "mediaSource",
+		displayName: "Media source",
+		properties: ["mediaSource"],
+		sortProperty: "mediaSource",
 		minWidth: 120,
 		defaultWidth: 120
 	},
@@ -186,9 +186,9 @@ const filters = ref<TableFilter[]>([
 		defaultFilterType: "exact"
 	},
 	{
-		name: "youtubeId",
-		displayName: "YouTube ID",
-		property: "youtubeId",
+		name: "mediaSource",
+		displayName: "Media source",
+		property: "mediaSource",
 		filterTypes: ["contains", "exact", "regex"],
 		defaultFilterType: "contains"
 	},
@@ -339,7 +339,7 @@ const editMany = selectedRows => {
 	if (selectedRows.length === 1) editOne(selectedRows[0]);
 	else {
 		const songs = selectedRows.map(row => ({
-			youtubeId: row.youtubeId
+			mediaSource: row.mediaSource
 		}));
 		openModal({ modal: "editSong", props: { songs } });
 	}
@@ -410,7 +410,7 @@ const unverifyMany = selectedRows => {
 };
 
 const importAlbum = selectedRows => {
-	const youtubeIds = selectedRows.map(({ youtubeId }) => youtubeId);
+	const youtubeIds = selectedRows.map(({ mediaSource }) => mediaSource);
 	socket.dispatch("songs.getSongsFromYoutubeIds", youtubeIds, res => {
 		if (res.status === "success") {
 			openModal({
@@ -473,7 +473,7 @@ const bulkEditPlaylist = selectedRows => {
 	openModal({
 		modal: "bulkEditPlaylist",
 		props: {
-			youtubeIds: selectedRows.map(row => row.youtubeId)
+			youtubeIds: selectedRows.map(row => row.mediaSource)
 		}
 	});
 };
@@ -650,7 +650,7 @@ onMounted(() => {
 					slotProps.item._id
 				}}</span>
 			</template>
-			<template #column-youtubeId="slotProps">
+			<template #column-mediaSource="slotProps">
 				<a
 					:href="
 						'https://www.youtube.com/watch?v=' +
@@ -658,7 +658,7 @@ onMounted(() => {
 					"
 					target="_blank"
 				>
-					{{ slotProps.item.youtubeId }}
+					{{ slotProps.item.mediaSource }}
 				</a>
 			</template>
 			<template #column-verified="slotProps">

+ 5 - 4
frontend/src/pages/Admin/YouTube/Videos.vue

@@ -239,8 +239,9 @@ const editMany = selectedRows => {
 };
 
 const importAlbum = selectedRows => {
-	const youtubeIds = selectedRows.map(({ youtubeId }) => youtubeId);
-	socket.dispatch("songs.getSongsFromYoutubeIds", youtubeIds, res => {
+	const mediaSources = selectedRows.map(({ youtubeId }) => youtubeId);
+	console.log(77988, mediaSources);
+	socket.dispatch("songs.getSongsFromMediaSources", mediaSources, res => {
 		if (res.status === "success") {
 			openModal({
 				modal: "importAlbum",
@@ -254,7 +255,7 @@ const bulkEditPlaylist = selectedRows => {
 	openModal({
 		modal: "bulkEditPlaylist",
 		props: {
-			youtubeIds: selectedRows.map(row => row.youtubeId)
+			mediaSources: selectedRows.map(row => row.youtubeId)
 		}
 	});
 };
@@ -312,7 +313,7 @@ const removeVideos = videoIds => {
 							openModal({
 								modal: 'viewYoutubeVideo',
 								props: {
-									videoId: slotProps.item._id
+									videoId: slotProps.item.youtubeId
 								}
 							})
 						"

+ 7 - 7
frontend/src/pages/Station/Sidebar/History.vue

@@ -33,9 +33,9 @@ const songsList = computed({
 const songsInQueue = computed(() => {
 	if (station.value.currentSong)
 		return songsList.value
-			.map(song => song.youtubeId)
-			.concat(station.value.currentSong.youtubeId);
-	return songsList.value.map(song => song.youtubeId);
+			.map(song => song.mediaSource)
+			.concat(station.value.currentSong.mediaSource);
+	return songsList.value.map(song => song.mediaSource);
 });
 
 const formatDate = dateString => {
@@ -69,11 +69,11 @@ const formatSkipReason = skipReason => {
 	return "";
 };
 
-const addSongToQueue = (youtubeId: string) => {
+const addSongToQueue = (mediaSource: string) => {
 	socket.dispatch(
 		"stations.addToQueue",
 		station.value._id,
-		youtubeId,
+		mediaSource,
 		"manual",
 		res => {
 			if (res.status !== "success") new Toast(`Error: ${res.message}`);
@@ -105,7 +105,7 @@ onMounted(async () => {});
 						<i
 							v-if="
 								songsInQueue.indexOf(
-									historyItem.payload.song.youtubeId
+									historyItem.payload.song.mediaSource
 								) !== -1
 							"
 							class="material-icons disabled"
@@ -118,7 +118,7 @@ onMounted(async () => {});
 							class="material-icons add-to-queue-icon"
 							@click="
 								addSongToQueue(
-									historyItem.payload.song.youtubeId
+									historyItem.payload.song.mediaSource
 								)
 							"
 							content="Add Song to Queue"

+ 99 - 73
frontend/src/pages/Station/index.vue

@@ -174,6 +174,24 @@ const stationState = computed(() => {
 	return "unknown";
 });
 
+const currentYoutubeId = computed(() => {
+	if (
+		!currentSong.value ||
+		!currentSong.value.mediaSource.startsWith("youtube:")
+	)
+		return null;
+	return currentSong.value.mediaSource.split(":")[1];
+});
+
+const currentSongIsYoutube = computed(() => {
+	if (
+		!currentSong.value ||
+		!currentSong.value.mediaSource.startsWith("youtube:")
+	)
+		return false;
+	return true;
+});
+
 const {
 	joinStation,
 	leaveStation,
@@ -208,13 +226,13 @@ const {
 // 	store.dispatch("modals/editSong/stopVideo", payload);
 
 const recentlyPlayedYoutubeIds = (max: number) => {
-	const youtubeIds = new Set();
+	const mediaSources = new Set();
 
 	history.value.forEach((historyItem, index) => {
-		if (index < max) youtubeIds.add(historyItem.payload.song.youtubeId);
+		if (index < max) mediaSources.add(historyItem.payload.song.mediaSource);
 	});
 
-	return Array.from(youtubeIds);
+	return Array.from(mediaSources);
 };
 
 const updateMediaSessionData = song => {
@@ -258,32 +276,32 @@ const autoRequestSong = () => {
 
 	if (songsList.value) {
 		songsList.value.forEach(song => {
-			excludedYoutubeIds.push(song.youtubeId);
+			excludedYoutubeIds.push(song.mediaSource);
 		});
 	}
 
 	if (!noSong.value) {
-		excludedYoutubeIds.push(currentSong.value.youtubeId);
+		excludedYoutubeIds.push(currentSong.value.mediaSource);
 	}
 
 	const uniqueYoutubeIds = new Set();
 
 	autoRequest.value.forEach(playlist => {
 		playlist.songs.forEach(song => {
-			if (excludedYoutubeIds.indexOf(song.youtubeId) === -1)
-				uniqueYoutubeIds.add(song.youtubeId);
+			if (excludedYoutubeIds.indexOf(song.mediaSource) === -1)
+				uniqueYoutubeIds.add(song.mediaSource);
 		});
 	});
 
 	if (uniqueYoutubeIds.size > 0) {
-		const youtubeId = Array.from(uniqueYoutubeIds.values())[
+		const mediaSource = Array.from(uniqueYoutubeIds.values())[
 			Math.floor(Math.random() * uniqueYoutubeIds.size)
 		];
 		updateAutoRequestLock(true);
 		socket.dispatch(
 			"stations.addToQueue",
 			station.value._id,
-			youtubeId,
+			mediaSource,
 			"autorequest",
 			data => {
 				updateAutoRequestLock(false);
@@ -322,8 +340,8 @@ const skipSong = () => {
 		const _songsList = songsList.value.concat([]);
 		if (
 			_songsList.length > 0 &&
-			_songsList[0].youtubeId ===
-				nextCurrentSong.value.currentSong.youtubeId
+			_songsList[0].mediaSource ===
+				nextCurrentSong.value.currentSong.mediaSource
 		) {
 			_songsList.splice(0, 1);
 			updateSongsList(_songsList);
@@ -448,10 +466,10 @@ const calculateTimeElapsed = () => {
 			typeof duration === "number" ? utils.formatTime(duration) : "0";
 };
 const playVideo = () => {
-	if (playerReady.value) {
+	if (playerReady.value && currentSongIsYoutube.value) {
 		videoLoading.value = true;
 		player.value.loadVideoById(
-			currentSong.value.youtubeId,
+			currentYoutubeId.value,
 			getTimeElapsed() / 1000 + currentSong.value.skipDuration
 		);
 
@@ -480,7 +498,8 @@ const youtubeReady = () => {
 		player.value = new window.YT.Player("stationPlayer", {
 			height: 270,
 			width: 480,
-			videoId: currentSong.value.youtubeId,
+			// TODO CHECK TYPE
+			videoId: currentYoutubeId.value,
 			host: "https://www.youtube-nocookie.com",
 			startSeconds:
 				getTimeElapsed() / 1000 + currentSong.value.skipDuration,
@@ -544,13 +563,13 @@ const youtubeReady = () => {
 						});
 
 						// save current song id
-						const erroredYoutubeId = currentSong.value.youtubeId;
+						const erroredYoutubeId = currentSong.value.mediaSource;
 
 						persistentToasts.value.push({
 							toast: persistentToast,
 							checkIfCanRemove: () => {
 								if (
-									currentSong.value.youtubeId !==
+									currentSong.value.mediaSource ===
 									erroredYoutubeId
 								) {
 									persistentToast.destroy();
@@ -654,7 +673,7 @@ const setCurrentSong = data => {
 
 	let nextSong = null;
 	if (songsList.value[0])
-		nextSong = songsList.value[0].youtubeId ? songsList.value[0] : null;
+		nextSong = songsList.value[0].mediaSource ? songsList.value[0] : null;
 
 	updateNextSong(nextSong);
 	setNextCurrentSong(
@@ -738,8 +757,8 @@ const setCurrentSong = data => {
 			}
 		);
 
-		socket.dispatch("media.getRatings", _currentSong.youtubeId, res => {
-			if (_currentSong.youtubeId === currentSong.value.youtubeId) {
+		socket.dispatch("media.getRatings", _currentSong.mediaSource, res => {
+			if (_currentSong.mediaSource === currentSong.value.mediaSource) {
 				const { likes, dislikes } = res.data;
 				updateCurrentSongRatings({ likes, dislikes });
 			}
@@ -748,12 +767,12 @@ const setCurrentSong = data => {
 		if (loggedIn.value) {
 			socket.dispatch(
 				"media.getOwnRatings",
-				_currentSong.youtubeId,
+				_currentSong.mediaSource,
 				res => {
 					console.log("getOwnSongRatings", res);
 					if (
 						res.status === "success" &&
-						currentSong.value.youtubeId === res.data.youtubeId
+						currentSong.value.mediaSource === res.data.mediaSource
 					) {
 						updateOwnCurrentSongRatings(res.data);
 
@@ -864,11 +883,11 @@ const increaseVolume = () => {
 };
 const toggleLike = () => {
 	if (currentSong.value.liked)
-		socket.dispatch("media.unlike", currentSong.value.youtubeId, res => {
+		socket.dispatch("media.unlike", currentSong.value.mediaSource, res => {
 			if (res.status !== "success") new Toast(`Error: ${res.message}`);
 		});
 	else
-		socket.dispatch("media.like", currentSong.value.youtubeId, res => {
+		socket.dispatch("media.like", currentSong.value.mediaSource, res => {
 			if (res.status !== "success") new Toast(`Error: ${res.message}`);
 		});
 };
@@ -876,7 +895,7 @@ const toggleDislike = () => {
 	if (currentSong.value.disliked)
 		return socket.dispatch(
 			"media.undislike",
-			currentSong.value.youtubeId,
+			currentSong.value.mediaSource,
 			res => {
 				if (res.status !== "success")
 					new Toast(`Error: ${res.message}`);
@@ -885,7 +904,7 @@ const toggleDislike = () => {
 
 	return socket.dispatch(
 		"media.dislike",
-		currentSong.value.youtubeId,
+		currentSong.value.mediaSource,
 		res => {
 			if (res.status !== "success") new Toast(`Error: ${res.message}`);
 		}
@@ -920,9 +939,10 @@ const sendActivityWatchVideoData = () => {
 
 		if (
 			activityWatchVideoLastYouTubeId.value !==
-			currentSong.value.youtubeId
+			currentSong.value.mediaSource
 		) {
-			activityWatchVideoLastYouTubeId.value = currentSong.value.youtubeId;
+			activityWatchVideoLastYouTubeId.value =
+				currentSong.value.mediaSource;
 			activityWatchVideoLastStartDuration.value =
 				currentSong.value.skipDuration + getTimeElapsed();
 		}
@@ -933,7 +953,7 @@ const sendActivityWatchVideoData = () => {
 				currentSong.value && currentSong.value.artists
 					? currentSong.value.artists.join(", ")
 					: null,
-			youtubeId: currentSong.value.youtubeId,
+			mediaSource: currentSong.value.mediaSource,
 			muted: muted.value,
 			volume: volumeSliderValue.value,
 			startedDuration:
@@ -1390,7 +1410,7 @@ onMounted(async () => {
 
 	socket.on("event:ratings.liked", res => {
 		if (!noSong.value) {
-			if (res.data.youtubeId === currentSong.value.youtubeId) {
+			if (res.data.mediaSource === currentSong.value.mediaSource) {
 				updateCurrentSongRatings(res.data);
 			}
 		}
@@ -1398,7 +1418,7 @@ onMounted(async () => {
 
 	socket.on("event:ratings.disliked", res => {
 		if (!noSong.value) {
-			if (res.data.youtubeId === currentSong.value.youtubeId) {
+			if (res.data.mediaSource === currentSong.value.mediaSource) {
 				updateCurrentSongRatings(res.data);
 			}
 		}
@@ -1406,7 +1426,7 @@ onMounted(async () => {
 
 	socket.on("event:ratings.unliked", res => {
 		if (!noSong.value) {
-			if (res.data.youtubeId === currentSong.value.youtubeId) {
+			if (res.data.mediaSource === currentSong.value.mediaSource) {
 				updateCurrentSongRatings(res.data);
 			}
 		}
@@ -1414,7 +1434,7 @@ onMounted(async () => {
 
 	socket.on("event:ratings.undisliked", res => {
 		if (!noSong.value) {
-			if (res.data.youtubeId === currentSong.value.youtubeId) {
+			if (res.data.mediaSource === currentSong.value.mediaSource) {
 				updateCurrentSongRatings(res.data);
 			}
 		}
@@ -1422,7 +1442,7 @@ onMounted(async () => {
 
 	socket.on("event:ratings.updated", res => {
 		if (!noSong.value) {
-			if (res.data.youtubeId === currentSong.value.youtubeId) {
+			if (res.data.mediaSource === currentSong.value.mediaSource) {
 				updateOwnCurrentSongRatings(res.data);
 			}
 		}
@@ -1433,7 +1453,9 @@ onMounted(async () => {
 
 		let nextSong = null;
 		if (songsList.value[0])
-			nextSong = songsList.value[0].youtubeId ? songsList.value[0] : null;
+			nextSong = songsList.value[0].mediaSource
+				? songsList.value[0]
+				: null;
 
 		updateNextSong(nextSong);
 
@@ -1445,7 +1467,9 @@ onMounted(async () => {
 
 		let nextSong = null;
 		if (songsList.value[0])
-			nextSong = songsList.value[0].youtubeId ? songsList.value[0] : null;
+			nextSong = songsList.value[0].mediaSource
+				? songsList.value[0]
+				: null;
 
 		updateNextSong(nextSong);
 	});
@@ -1708,10 +1732,10 @@ onBeforeUnmount(() => {
 		<ul
 			v-if="
 				currentSong &&
-				(currentSong.youtubeId === 'l9PxOanFjxQ' ||
-					currentSong.youtubeId === 'xKVcVSYmesU' ||
-					currentSong.youtubeId === '60ItHLz5WEA' ||
-					currentSong.youtubeId === 'e6vkFbtSGm0')
+				(currentSong.mediaSource === 'l9PxOanFjxQ' ||
+					currentSong.mediaSource === 'xKVcVSYmesU' ||
+					currentSong.mediaSource === '60ItHLz5WEA' ||
+					currentSong.mediaSource === 'e6vkFbtSGm0')
 			"
 			class="bg-bubbles"
 		>
@@ -1954,8 +1978,8 @@ onBeforeUnmount(() => {
 										'christmas-seeker': christmas,
 										nyan:
 											currentSong &&
-											currentSong.youtubeId ===
-												'QH2-TGUlwu4'
+											currentSong.mediaSource ===
+												'youtube:QH2-TGUlwu4'
 									}"
 								/>
 								<div
@@ -1967,7 +1991,8 @@ onBeforeUnmount(() => {
 								<img
 									v-if="
 										currentSong &&
-										currentSong.youtubeId === 'QH2-TGUlwu4'
+										currentSong.mediaSource ===
+											'youtube:QH2-TGUlwu4'
 									"
 									src="https://freepngimg.com/thumb/nyan_cat/1-2-nyan-cat-free-download-png.png"
 									:style="{
@@ -1980,14 +2005,14 @@ onBeforeUnmount(() => {
 								<img
 									v-if="
 										currentSong &&
-										(currentSong.youtubeId ===
-											'DtVBCG6ThDk' ||
-											currentSong.youtubeId ===
-												'sI66hcu9fIs' ||
-											currentSong.youtubeId ===
-												'iYYRH4apXDo' ||
-											currentSong.youtubeId ===
-												'tRcPA7Fzebw')
+										(currentSong.mediaSource ===
+											'youtube:DtVBCG6ThDk' ||
+											currentSong.mediaSource ===
+												'youtube:sI66hcu9fIs' ||
+											currentSong.mediaSource ===
+												'youtube:iYYRH4apXDo' ||
+											currentSong.mediaSource ===
+												'youtube:tRcPA7Fzebw')
 									"
 									src="/assets/rocket.svg"
 									:style="{
@@ -2001,7 +2026,8 @@ onBeforeUnmount(() => {
 								<img
 									v-if="
 										currentSong &&
-										currentSong.youtubeId === 'jofNR_WkoCE'
+										currentSong.mediaSource ===
+											'youtube:jofNR_WkoCE'
 									"
 									src="/assets/fox.svg"
 									:style="{
@@ -2016,14 +2042,14 @@ onBeforeUnmount(() => {
 								<img
 									v-if="
 										currentSong &&
-										(currentSong.youtubeId ===
-											'l9PxOanFjxQ' ||
-											currentSong.youtubeId ===
-												'xKVcVSYmesU' ||
-											currentSong.youtubeId ===
-												'60ItHLz5WEA' ||
-											currentSong.youtubeId ===
-												'e6vkFbtSGm0')
+										(currentSong.mediaSource ===
+											'youtube:l9PxOanFjxQ' ||
+											currentSong.mediaSource ===
+												'youtube:xKVcVSYmesU' ||
+											currentSong.mediaSource ===
+												'youtube:60ItHLz5WEA' ||
+											currentSong.mediaSource ===
+												'youtube:e6vkFbtSGm0')
 									"
 									src="/assets/old_logo.png"
 									:style="{
@@ -2041,17 +2067,17 @@ onBeforeUnmount(() => {
 										christmas &&
 										currentSong &&
 										![
-											'QH2-TGUlwu4',
-											'DtVBCG6ThDk',
-											'sI66hcu9fIs',
-											'iYYRH4apXDo',
-											'tRcPA7Fzebw',
-											'jofNR_WkoCE',
-											'l9PxOanFjxQ',
-											'xKVcVSYmesU',
-											'60ItHLz5WEA',
-											'e6vkFbtSGm0'
-										].includes(currentSong.youtubeId)
+											'youtube:QH2-TGUlwu4',
+											'youtube:DtVBCG6ThDk',
+											'youtube:sI66hcu9fIs',
+											'youtube:iYYRH4apXDo',
+											'youtube:tRcPA7Fzebw',
+											'youtube:jofNR_WkoCE',
+											'youtube:l9PxOanFjxQ',
+											'youtube:xKVcVSYmesU',
+											'youtube:60ItHLz5WEA',
+											'youtube:e6vkFbtSGm0'
+										].includes(currentSong.mediaSource)
 									"
 									src="/assets/santa.png"
 									:style="{
@@ -2404,7 +2430,7 @@ onBeforeUnmount(() => {
 								:class="{ 'no-currently-playing': noSong }"
 							>
 								<song-item
-									:key="`songItem-currentSong-${currentSong.youtubeId}`"
+									:key="`songItem-currentSong-${currentSong.mediaSource}`"
 									:song="currentSong"
 									:duration="false"
 									:requested-by="true"
@@ -2418,7 +2444,7 @@ onBeforeUnmount(() => {
 								class="quadrant"
 							>
 								<song-item
-									:key="`songItem-nextSong-${nextSong.youtubeId}`"
+									:key="`songItem-nextSong-${nextSong.mediaSource}`"
 									:song="nextSong"
 									:duration="false"
 									:requested-by="true"
@@ -2442,7 +2468,7 @@ onBeforeUnmount(() => {
 			<template #body>
 				<span><b>No song</b>: {{ noSong }}</span>
 				<span><b>Song id</b>: {{ currentSong._id }}</span>
-				<span><b>YouTube id</b>: {{ currentSong.youtubeId }}</span>
+				<span><b>Media source</b>: {{ currentSong.mediaSource }}</span>
 				<span><b>Duration</b>: {{ currentSong.duration }}</span>
 				<span
 					><b>Skip duration</b>: {{ currentSong.skipDuration }}</span

+ 4 - 4
frontend/src/stores/editPlaylist.ts

@@ -24,9 +24,9 @@ export const useEditPlaylistStore = ({ modalUuid }: { modalUuid: string }) =>
 			addSong(song) {
 				this.playlist.songs.push(song);
 			},
-			removeSong(youtubeId) {
+			removeSong(mediaSource) {
 				this.playlist.songs = this.playlist.songs.filter(
-					song => song.youtubeId !== youtubeId
+					song => song.mediaSource !== mediaSource
 				);
 			},
 			updatePlaylistSongs(playlistSongs) {
@@ -35,8 +35,8 @@ export const useEditPlaylistStore = ({ modalUuid }: { modalUuid: string }) =>
 			repositionedSong(song) {
 				if (
 					this.playlist.songs[song.newIndex] &&
-					this.playlist.songs[song.newIndex].youtubeId ===
-						song.youtubeId
+					this.playlist.songs[song.newIndex].mediaSource ===
+						song.mediaSource
 				)
 					return;
 

+ 16 - 14
frontend/src/stores/editSong.ts

@@ -14,14 +14,14 @@ export const useEditSongStore = ({ modalUuid }: { modalUuid: string }) =>
 				currentTime: number;
 				playbackRate: 0.5 | 1 | 2;
 			};
-			youtubeId: string;
+			mediaSource: string;
 			song: Song;
 			reports: Report[];
 			tab: "discogs" | "reports" | "youtube" | "musare-songs";
 			newSong: boolean;
 			prefillData: any;
 			bulk: boolean;
-			youtubeIds: string[];
+			mediaSources: string[];
 			songPrefillData: any;
 			form: {
 				inputs: Ref<
@@ -54,25 +54,26 @@ export const useEditSongStore = ({ modalUuid }: { modalUuid: string }) =>
 				currentTime: 0,
 				playbackRate: 1
 			},
-			youtubeId: null,
+			mediaSource: null,
 			song: {},
 			reports: [],
 			tab: "discogs",
 			newSong: false,
 			prefillData: {},
 			bulk: false,
-			youtubeIds: [],
+			mediaSources: [],
 			songPrefillData: {},
 			form: {}
 		}),
 		actions: {
 			init({ song, songs }) {
+				console.log(12357878, song, songs);
 				if (songs) {
 					this.bulk = true;
-					this.youtubeIds = songs.map(song => song.youtubeId);
+					this.mediaSources = songs.map(song => song.mediaSource);
 					this.songPrefillData = Object.fromEntries(
 						songs.map(song => [
-							song.youtubeId,
+							song.mediaSource,
 							song.prefill ? song.prefill : {}
 						])
 					);
@@ -83,20 +84,20 @@ export const useEditSongStore = ({ modalUuid }: { modalUuid: string }) =>
 			},
 			editSong(song) {
 				this.newSong = !!song.newSong || !song._id;
-				this.youtubeId = song.youtubeId || null;
+				this.mediaSource = song.mediaSource || null;
 				this.prefillData = song.prefill ? song.prefill : {};
 			},
 			setSong(song, reset?: boolean) {
 				if (song.discogs === undefined) song.discogs = null;
 				this.song = JSON.parse(JSON.stringify(song));
 				this.newSong = !song._id;
-				this.youtubeId = song.youtubeId;
+				this.mediaSource = song.mediaSource;
 				const formSong = {
 					title: song.title,
 					duration: song.duration,
 					skipDuration: song.skipDuration,
 					thumbnail: song.thumbnail,
-					youtubeId: song.youtubeId,
+					mediaSource: song.mediaSource,
 					verified: song.verified,
 					addArtist: "",
 					artists: song.artists,
@@ -109,9 +110,9 @@ export const useEditSongStore = ({ modalUuid }: { modalUuid: string }) =>
 				if (reset) this.form.setValue(formSong, true);
 				else this.form.setOriginalValue(formSong);
 			},
-			resetSong(youtubeId) {
-				if (this.youtubeId === youtubeId) this.youtubeId = "";
-				if (this.song && this.song.youtubeId === youtubeId) {
+			resetSong(mediaSource) {
+				if (this.mediaSource === mediaSource) this.mediaSource = "";
+				if (this.song && this.song.mediaSource === mediaSource) {
 					this.song = {};
 					if (this.form.setValue)
 						this.form.setValue(
@@ -120,7 +121,7 @@ export const useEditSongStore = ({ modalUuid }: { modalUuid: string }) =>
 								duration: 0,
 								skipDuration: 0,
 								thumbnail: "",
-								youtubeId: "",
+								mediaSource: "",
 								verified: false,
 								addArtist: "",
 								artists: [],
@@ -171,7 +172,8 @@ export const useEditSongStore = ({ modalUuid }: { modalUuid: string }) =>
 				);
 			},
 			updateYoutubeId(youtubeId) {
-				this.form.setValue({ youtubeId });
+				this.form.setValue({ mediaSource: `youtube:${youtubeId}` });
+				// TODO support spotify
 				this.loadVideoById(youtubeId, 0);
 			},
 			setPlaybackRate(rate) {

+ 2 - 1
frontend/src/stores/manageStation.ts

@@ -67,7 +67,8 @@ export const useManageStationStore = ({ modalUuid }: { modalUuid: string }) =>
 			repositionSongInList(song) {
 				if (
 					this.songsList[song.newIndex] &&
-					this.songsList[song.newIndex].youtubeId === song.youtubeId
+					this.songsList[song.newIndex].mediaSource ===
+						song.mediaSource
 				)
 					return;
 

+ 1 - 1
frontend/src/stores/station.ts

@@ -95,7 +95,7 @@ export const useStationStore = defineStore("station", {
 		repositionSongInList(song) {
 			if (
 				this.songsList[song.newIndex] &&
-				this.songsList[song.newIndex].youtubeId === song.youtubeId
+				this.songsList[song.newIndex].mediaSource === song.mediaSource
 			)
 				return;
 

+ 1 - 1
frontend/src/types/song.ts

@@ -1,6 +1,6 @@
 export interface Song {
 	_id: string;
-	youtubeId: string;
+	mediaSource: string;
 	title: string;
 	artists: string[];
 	genres: string[];

+ 1 - 1
types/models/Playlist.ts

@@ -1,7 +1,7 @@
 // TODO check if all of these properties are always present
 export type PlaylistSong = {
 	_id: string;
-	youtubeId: string;
+	mediaSource: string;
 	title: string;
 	artists: string[];
 	duration: number;