瀏覽代碼

refactor: Continue on Youtube videos and surrounding areas

Owen Diffey 3 年之前
父節點
當前提交
26ab23063f

+ 7 - 4
backend/logic/actions/playlists.js

@@ -1163,7 +1163,7 @@ export default {
 					)
 						.then(response => {
 							const { song } = response;
-							const { _id, title, artists, thumbnail, duration, status } = song;
+							const { _id, title, artists, thumbnail, duration, verified } = song;
 							next(null, {
 								_id,
 								youtubeId,
@@ -1171,7 +1171,7 @@ export default {
 								artists,
 								thumbnail,
 								duration,
-								status
+								verified
 							});
 						})
 						.catch(next);
@@ -1542,8 +1542,11 @@ export default {
 							})
 						)
 						.catch(() => {
-							YouTubeModule.runJob("GET_SONG", { youtubeId }, this)
-								.then(response => next(null, playlist, response.song))
+							YouTubeModule.runJob("GET_VIDEO", { identifier: youtubeId, createMissing: true }, this)
+								.then(response => {
+									const { youtubeId, title, author, duration } = response.video;
+									next(null, playlist, { youtubeId, title, artists: [author], duration });
+								})
 								.catch(next);
 						});
 				},

+ 0 - 134
backend/logic/actions/songs.js

@@ -11,7 +11,6 @@ const WSModule = moduleManager.modules.ws;
 const CacheModule = moduleManager.modules.cache;
 const SongsModule = moduleManager.modules.songs;
 const ActivitiesModule = moduleManager.modules.activities;
-const YouTubeModule = moduleManager.modules.youtube;
 const PlaylistsModule = moduleManager.modules.playlists;
 const StationsModule = moduleManager.modules.stations;
 
@@ -831,39 +830,6 @@ export default {
 		);
 	}),
 
-	/**
-	 * Requests a song
-	 *
-	 * @param {object} session - the session object automatically added by the websocket
-	 * @param {string} youtubeId - the youtube id of the song that gets requested
-	 * @param {string} returnSong - returns the simple song
-	 * @param {Function} cb - gets called with the result
-	 */
-	request: isLoginRequired(async function add(session, youtubeId, returnSong, cb) {
-		SongsModule.runJob("REQUEST_SONG", { youtubeId, userId: session.userId }, this)
-			.then(response => {
-				this.log(
-					"SUCCESS",
-					"SONGS_REQUEST",
-					`User "${session.userId}" successfully requested song "${youtubeId}".`
-				);
-				return cb({
-					status: "success",
-					message: "Successfully requested that song",
-					song: returnSong ? response.song : null
-				});
-			})
-			.catch(async _err => {
-				const err = await UtilsModule.runJob("GET_ERROR", { error: _err }, this);
-				this.log(
-					"ERROR",
-					"SONGS_REQUEST",
-					`Requesting song "${youtubeId}" failed for user ${session.userId}. "${err}"`
-				);
-				return cb({ status: "error", message: err, song: returnSong && _err.data ? _err.data.song : null });
-			});
-	}),
-
 	/**
 	 * Verifies a song
 	 *
@@ -1140,106 +1106,6 @@ export default {
 		);
 	}),
 
-	/**
-	 * Requests a set of songs
-	 *
-	 * @param {object} session - the session object automatically added by the websocket
-	 * @param {string} url - the url of the the YouTube playlist
-	 * @param {boolean} musicOnly - whether to only get music from the playlist
-	 * @param {Function} cb - gets called with the result
-	 */
-	requestSet: isLoginRequired(function requestSet(session, url, musicOnly, returnSongs, cb) {
-		async.waterfall(
-			[
-				next => {
-					const playlistRegex = /[\\?&]list=([^&#]*)/;
-					const channelRegex =
-						/\.[\w]+\/(?:(?:channel\/(UC[0-9A-Za-z_-]{21}[AQgw]))|(?:user\/?([\w-]+))|(?:c\/?([\w-]+))|(?:\/?([\w-]+)))/;
-					if (playlistRegex.exec(url) || channelRegex.exec(url))
-						YouTubeModule.runJob(
-							playlistRegex.exec(url) ? "GET_PLAYLIST" : "GET_CHANNEL",
-							{
-								url,
-								musicOnly
-							},
-							this
-						)
-							.then(res => {
-								next(null, res.songs);
-							})
-							.catch(next);
-					else next("Invalid YouTube URL.");
-				},
-				(youtubeIds, next) => {
-					let successful = 0;
-					let songs = {};
-					let failed = 0;
-					let alreadyInDatabase = 0;
-
-					if (youtubeIds.length === 0) next();
-
-					async.eachOfLimit(
-						youtubeIds,
-						1,
-						(youtubeId, index, next) => {
-							WSModule.runJob(
-								"RUN_ACTION2",
-								{
-									session,
-									namespace: "songs",
-									action: "request",
-									args: [youtubeId, returnSongs]
-								},
-								this
-							)
-								.then(res => {
-									if (res.status === "success") successful += 1;
-									else failed += 1;
-									if (res.message === "This song is already in the database.") alreadyInDatabase += 1;
-									if (res.song) songs[index] = res.song;
-								})
-								.catch(() => {
-									failed += 1;
-								})
-								.finally(() => {
-									next();
-								});
-						},
-						() => {
-							if (returnSongs)
-								songs = Object.keys(songs)
-									.sort()
-									.map(key => songs[key]);
-
-							next(null, { successful, failed, alreadyInDatabase, songs });
-						}
-					);
-				}
-			],
-			async (err, response) => {
-				if (err) {
-					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
-					this.log(
-						"ERROR",
-						"REQUEST_SET",
-						`Importing a YouTube playlist to be requested failed for user "${session.userId}". "${err}"`
-					);
-					return cb({ status: "error", message: err });
-				}
-				this.log(
-					"SUCCESS",
-					"REQUEST_SET",
-					`Successfully imported a YouTube playlist to be requested for user "${session.userId}".`
-				);
-				return cb({
-					status: "success",
-					message: `Playlist is done importing. ${response.successful} were added succesfully, ${response.failed} failed (${response.alreadyInDatabase} were already in database)`,
-					songs: returnSongs ? response.songs : null
-				});
-			}
-		);
-	}),
-
 	/**
 	 * Likes a song
 	 *

+ 1 - 3
backend/logic/actions/stations.js

@@ -1962,9 +1962,7 @@ export default {
 							[]
 						);
 
-					if (
-						blacklistedSongs.find(blacklistedSong => blacklistedSong._id.toString() === song._id.toString())
-					)
+					if (blacklistedSongs.find(blacklistedSong => blacklistedSong.youtubeId === song.youtubeId))
 						next("That song is in an blacklisted playlist and cannot be played.");
 					else next(null, song, station);
 				},

+ 36 - 2
backend/logic/actions/youtube.js

@@ -1,7 +1,7 @@
 import mongoose from "mongoose";
 import async from "async";
 
-import { isAdminRequired } from "./hooks";
+import { isAdminRequired, isLoginRequired } from "./hooks";
 
 // eslint-disable-next-line
 import moduleManager from "../../index";
@@ -326,7 +326,7 @@ export default {
 	 *
 	 * @returns {{status: string, data: object}}
 	 */
-	getVideo: isAdminRequired(function getVideo(session, identifier, createMissing, cb) {
+	getVideo: isLoginRequired(function getVideo(session, identifier, createMissing, cb) {
 		YouTubeModule.runJob("GET_VIDEO", { identifier, createMissing }, this)
 			.then(res => {
 				this.log("SUCCESS", "YOUTUBE_GET_VIDEO", `Fetching video was successful.`);
@@ -362,5 +362,39 @@ export default {
 				this.log("ERROR", "YOUTUBE_REMOVE_VIDEOS", `Removing videos failed. "${err}"`);
 				return cb({ status: "error", message: err });
 			});
+	}),
+
+	/**
+	 * Requests a set of YouTube videos
+	 *
+	 * @param {object} session - the session object automatically added by the websocket
+	 * @param {string} url - the url of the the YouTube playlist
+	 * @param {boolean} musicOnly - whether to only get music from the playlist
+	 * @param {boolean} musicOnly - whether to return videos
+	 * @param {Function} cb - gets called with the result
+	 */
+	requestSet: isLoginRequired(function requestSet(session, url, musicOnly, returnVideos, cb) {
+		YouTubeModule.runJob("REQUEST_SET", { url, musicOnly, returnVideos }, this)
+			.then(response => {
+				this.log(
+					"SUCCESS",
+					"REQUEST_SET",
+					`Successfully imported a YouTube playlist to be requested for user "${session.userId}".`
+				);
+				return cb({
+					status: "success",
+					message: `Playlist is done importing. ${response.successful} were added succesfully, ${response.failed} failed (${response.alreadyInDatabase} were already in database)`,
+					videos: returnVideos ? response.videos : null
+				});
+			})
+			.catch(async err => {
+				err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+				this.log(
+					"ERROR",
+					"REQUEST_SET",
+					`Importing a YouTube playlist to be requested failed for user "${session.userId}". "${err}"`
+				);
+				return cb({ status: "error", message: err });
+			});
 	})
 };

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

@@ -4,7 +4,7 @@ export default {
 	displayName: { type: String, min: 2, max: 32, trim: true, required: true },
 	songs: [
 		{
-			_id: { type: mongoose.Schema.Types.ObjectId, required: true },
+			_id: { type: mongoose.Schema.Types.ObjectId },
 			youtubeId: { type: String, required: true },
 			title: { type: String },
 			artists: [{ type: String }],

+ 7 - 0
backend/logic/db/schemas/song.js

@@ -16,5 +16,12 @@ export default {
 	verifiedBy: { type: String },
 	verifiedAt: { type: Date },
 	discogs: { type: Object },
+	// sources: [
+	// 	{
+	// 		type: { type: String, enum: ["youtube"], required: true },
+	// 		youtube: { type: mongoose.Schema.Types.ObjectId, ref: "youtubevideos" }
+	// 	}
+	// ],
 	documentVersion: { type: Number, default: 8, required: true }
+	// documentVersion: { type: Number, default: 9, required: true }
 };

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

@@ -27,7 +27,7 @@ export default {
 	privacy: { type: String, enum: ["public", "unlisted", "private"], default: "private" },
 	queue: [
 		{
-			_id: { type: mongoose.Schema.Types.ObjectId, required: true },
+			_id: { type: mongoose.Schema.Types.ObjectId },
 			youtubeId: { type: String, required: true },
 			title: { type: String },
 			artists: [{ type: String }],

+ 16 - 128
backend/logic/songs.js

@@ -10,17 +10,6 @@ let YouTubeModule;
 let StationsModule;
 let PlaylistsModule;
 
-class ErrorWithData extends Error {
-	/**
-	 * @param {string} message - the error message
-	 * @param {object} data - the error data
-	 */
-	constructor(message, data) {
-		super(message);
-		this.data = data;
-	}
-}
-
 class _SongsModule extends CoreClass {
 	// eslint-disable-next-line require-jsdoc
 	constructor() {
@@ -225,30 +214,33 @@ class _SongsModule extends CoreClass {
 					(song, next) => {
 						if (song && song.duration > 0) next(true, song);
 						else {
-							YouTubeModule.runJob("GET_SONG", { youtubeId: payload.youtubeId }, this)
+							YouTubeModule.runJob(
+								"GET_VIDEO",
+								{ identifier: payload.youtubeId, createMissing: true },
+								this
+							)
 								.then(response => {
-									next(null, song, response.song);
+									const { youtubeId, title, author, duration } = response.video;
+									next(null, song, { youtubeId, title, artists: [author], duration });
 								})
 								.catch(next);
 						}
 					},
 
-					(song, youtubeSong, next) => {
+					(song, youtubeVideo, next) => {
 						if (song && song.duration <= 0) {
-							song.duration = youtubeSong.duration;
+							song.duration = youtubeVideo.duration;
 							song.save({ validateBeforeSave: true }, err => {
-								if (err) return next(err, song);
-								return next(null, song);
+								if (err) next(err, song);
+								next(null, song);
 							});
 						} else {
-							const song = new SongsModule.SongModel({
-								...youtubeSong,
+							next(null, {
+								...youtubeVideo,
+								skipDuration: 0,
 								requestedBy: payload.userId,
-								requestedAt: Date.now()
-							});
-							song.save({ validateBeforeSave: true }, err => {
-								if (err) return next(err, song);
-								return next(null, song);
+								requestedAt: Date.now(),
+								verified: false
 							});
 						}
 					}
@@ -1227,110 +1219,6 @@ class _SongsModule extends CoreClass {
 		});
 	}
 
-	/**
-	 * Requests a song, adding it to the DB
-	 *
-	 * @param {object} payload - The payload
-	 * @param {string} payload.youtubeId - The YouTube song id of the song
-	 * @param {string} payload.userId - The user id of the person requesting the song
-	 * @returns {Promise} - returns promise (reject, resolve)
-	 */
-	REQUEST_SONG(payload) {
-		return new Promise((resolve, reject) => {
-			const { youtubeId, userId } = payload;
-			const requestedAt = Date.now();
-
-			async.waterfall(
-				[
-					next => {
-						DBModule.runJob("GET_MODEL", { modelName: "user" }, this)
-							.then(UserModel => {
-								UserModel.findOne({ _id: userId }, { "preferences.anonymousSongRequests": 1 }, next);
-							})
-							.catch(next);
-					},
-
-					(user, next) => {
-						SongsModule.SongModel.findOne({ youtubeId }, (err, song) => next(err, user, song));
-					},
-
-					// Get YouTube data from id
-					(user, song, next) => {
-						if (song) return next("This song is already in the database.", song);
-						// TODO Add err object as first param of callback
-
-						return YouTubeModule.runJob("GET_SONG", { youtubeId }, this)
-							.then(response => next(null, user, response.song))
-							.catch(next);
-					},
-
-					(user, youtubeVideo, next) =>
-						YouTubeModule.runJob("CREATE_VIDEOS", { youtubeVideos: youtubeVideo }, this)
-							.then(() => {
-								const song = youtubeVideo;
-								delete song.author;
-								song.artists = [];
-								song.genres = [];
-								song.skipDuration = 0;
-								song.explicit = false;
-								song.requestedBy = user.preferences.anonymousSongRequests ? null : userId;
-								song.requestedAt = requestedAt;
-								song.verified = false;
-								next(null, song);
-							})
-							.catch(next),
-					(newSong, next) => {
-						const song = new SongsModule.SongModel(newSong);
-						song.save({ validateBeforeSave: false }, err => {
-							if (err) return next(err, song);
-							return next(null, song);
-						});
-					},
-					(song, next) => {
-						DBModule.runJob("GET_MODEL", { modelName: "user" }, this)
-							.then(UserModel => {
-								UserModel.findOne({ _id: userId }, (err, user) => {
-									if (err) return next(err);
-									if (!user) return next(null, song);
-
-									user.statistics.songsRequested += 1;
-
-									return user.save(err => {
-										if (err) return next(err);
-										return next(null, song);
-									});
-								});
-							})
-							.catch(next);
-					}
-				],
-				async (err, song) => {
-					if (err && err !== "This song is already in the database.") return reject(err);
-
-					const { _id, youtubeId, title, artists, thumbnail, duration, verified } = song;
-					const trimmedSong = {
-						_id,
-						youtubeId,
-						title,
-						artists,
-						thumbnail,
-						duration,
-						verified
-					};
-
-					if (err && err === "This song is already in the database.")
-						return reject(new ErrorWithData(err, { song: trimmedSong }));
-
-					SongsModule.runJob("UPDATE_SONG", { songId: song._id });
-
-					return resolve({ song: trimmedSong });
-				}
-			);
-		});
-	}
-
-	// runjob songs REQUEST_ORPHANED_PLAYLIST_SONGS {}
-
 	/**
 	 * Requests all orphaned playlist songs, adding them to the database
 	 *

+ 28 - 29
backend/logic/stations.js

@@ -624,36 +624,35 @@ class _StationsModule extends CoreClass {
 					},
 
 					(queueSong, next) => {
-						if (!queueSong._id) next(null, queueSong);
-						else
-							SongsModule.runJob("GET_SONG", { songId: queueSong._id }, this)
-								.then(response => {
-									const { song } = response;
-
-									if (song) {
-										const newSong = {
-											_id: song._id,
-											youtubeId: song.youtubeId,
-											title: song.title,
-											artists: song.artists,
-											duration: song.duration,
-											skipDuration: song.skipDuration,
-											thumbnail: song.thumbnail,
-											requestedAt: queueSong.requestedAt,
-											requestedBy: queueSong.requestedBy,
-											likes: song.likes,
-											dislikes: song.dislikes,
-											verified: song.verified
-										};
-
-										return next(null, newSong);
-									}
-
-									return next(null, song);
-								})
-								.catch(err => {
-									next(err);
+						SongsModule.runJob(
+							"ENSURE_SONG_EXISTS_BY_YOUTUBE_ID",
+							{
+								youtubeId: queueSong.youtubeId,
+								userId: null,
+								automaticallyRequested: true
+							},
+							this
+						)
+							.then(response => {
+								const { song } = response;
+								const { _id, youtubeId, title, skipDuration, artists, thumbnail, duration, verified } =
+									song;
+								next(null, {
+									_id,
+									youtubeId,
+									title,
+									skipDuration,
+									artists,
+									thumbnail,
+									duration,
+									verified,
+									requestedAt: queueSong.requestedAt,
+									requestedBy: queueSong.requestedBy,
+									likes: song.likes || 0,
+									dislikes: song.dislikes || 0
 								});
+							})
+							.catch(next);
 					}
 				],
 				(err, song) => {

+ 129 - 69
backend/logic/youtube.js

@@ -225,69 +225,6 @@ class _YouTubeModule extends CoreClass {
 		});
 	}
 
-	/**
-	 * Gets the details of a song using the YouTube API
-	 *
-	 * @param {object} payload - object that contains the payload
-	 * @param {string} payload.youtubeId - the YouTube API id of the song
-	 * @returns {Promise} - returns promise (reject, resolve)
-	 */
-	GET_SONG(payload) {
-		return new Promise((resolve, reject) => {
-			const params = {
-				part: "snippet,contentDetails,statistics,status",
-				id: payload.youtubeId
-			};
-
-			YouTubeModule.runJob("API_GET_VIDEOS", { params }, this)
-				.then(({ response }) => {
-					const { data } = response;
-					if (data.items[0] === undefined)
-						return reject(new Error("The specified video does not exist or cannot be publicly accessed."));
-
-					// TODO Clean up duration converter
-					let dur = data.items[0].contentDetails.duration;
-
-					dur = dur.replace("PT", "");
-
-					let duration = 0;
-
-					dur = dur.replace(/([\d]*)H/, (v, v2) => {
-						v2 = Number(v2);
-						duration = v2 * 60 * 60;
-						return "";
-					});
-
-					dur = dur.replace(/([\d]*)M/, (v, v2) => {
-						v2 = Number(v2);
-						duration += v2 * 60;
-						return "";
-					});
-
-					// eslint-disable-next-line no-unused-vars
-					dur = dur.replace(/([\d]*)S/, (v, v2) => {
-						v2 = Number(v2);
-						duration += v2;
-						return "";
-					});
-
-					const song = {
-						youtubeId: data.items[0].id,
-						title: data.items[0].snippet.title,
-						author: data.items[0].snippet.channelTitle,
-						thumbnail: data.items[0].snippet.thumbnails.default.url,
-						duration
-					};
-
-					return resolve({ song });
-				})
-				.catch(err => {
-					YouTubeModule.log("ERROR", "GET_SONG", `${err.message}`);
-					return reject(new Error("An error has occured. Please try again later."));
-				});
-		});
-	}
-
 	/**
 	 * Gets the id of the channel upload playlist
 	 *
@@ -1083,24 +1020,70 @@ class _YouTubeModule extends CoreClass {
 					(video, next) => {
 						if (video) return next(null, video, false);
 						if (mongoose.Types.ObjectId.isValid(payload.identifier) || !payload.createMissing) return next("YouTube video not found.");
-						return YouTubeModule.runJob("GET_SONG", { youtubeId: payload.identifier }, this)
-							.then(response => next(null, false, response.song))
+
+						const params = {
+							part: "snippet,contentDetails,statistics,status",
+							id: payload.identifier
+						};
+				
+						return YouTubeModule.runJob("API_GET_VIDEOS", { params }, this)
+							.then(({ response }) => {
+								const { data } = response;
+								if (data.items[0] === undefined)
+									return next("The specified video does not exist or cannot be publicly accessed.");
+			
+								// TODO Clean up duration converter
+								let dur = data.items[0].contentDetails.duration;
+			
+								dur = dur.replace("PT", "");
+			
+								let duration = 0;
+			
+								dur = dur.replace(/([\d]*)H/, (v, v2) => {
+									v2 = Number(v2);
+									duration = v2 * 60 * 60;
+									return "";
+								});
+			
+								dur = dur.replace(/([\d]*)M/, (v, v2) => {
+									v2 = Number(v2);
+									duration += v2 * 60;
+									return "";
+								});
+			
+								// eslint-disable-next-line no-unused-vars
+								dur = dur.replace(/([\d]*)S/, (v, v2) => {
+									v2 = Number(v2);
+									duration += v2;
+									return "";
+								});
+			
+								const youtubeVideo = {
+									youtubeId: data.items[0].id,
+									title: data.items[0].snippet.title,
+									author: data.items[0].snippet.channelTitle,
+									thumbnail: data.items[0].snippet.thumbnails.default.url,
+									duration
+								};
+			
+								return next(null, false, youtubeVideo);
+							})
 							.catch(next);
 					},
 
 					(video, youtubeVideo, next) => {
-						if (video) return next(null, video);
+						if (video) return next(null, video, true);
 						return YouTubeModule.runJob("CREATE_VIDEOS", { youtubeVideos: youtubeVideo }, this)
 							.then(res => {
-								if (res.youtubeVideos.length === 1) next(null, res.youtubeVideos[0])
+								if (res.youtubeVideos.length === 1) next(null, res.youtubeVideos[0], false)
 								else next("YouTube video not found.")
 							})
 							.catch(next);
 					}
 				],
-				(err, video) => {
+				(err, video, existing) => {
 					if (err) reject(new Error(err));
-					else resolve({ video });
+					else resolve({ video, existing });
 				}
 			)
 		});
@@ -1134,6 +1117,83 @@ class _YouTubeModule extends CoreClass {
 			)
 		});
 	}
+
+	/**
+	 * Request a set of YouTube videos
+	 *
+	 * @param {object} payload - an object containing the payload
+	 * @param {string} payload.url - the url of the the YouTube playlist or channel
+	 * @param {boolean} payload.musicOnly - whether to only get music from the playlist/channel
+	 * @param {boolean} payload.returnVideos - whether to return videos
+	 * @returns {Promise} - returns a promise (resolve, reject)
+	 */
+	 REQUEST_SET(payload) {
+		return new Promise((resolve, reject) => {
+			async.waterfall(
+				[
+					next => {
+						const playlistRegex = /[\\?&]list=([^&#]*)/;
+						const channelRegex =
+							/\.[\w]+\/(?:(?:channel\/(UC[0-9A-Za-z_-]{21}[AQgw]))|(?:user\/?([\w-]+))|(?:c\/?([\w-]+))|(?:\/?([\w-]+)))/;
+						if (playlistRegex.exec(payload.url) || channelRegex.exec(payload.url))
+							YouTubeModule.runJob(
+								playlistRegex.exec(payload.url) ? "GET_PLAYLIST" : "GET_CHANNEL",
+								{
+									url: payload.url,
+									musicOnly: payload.musicOnly
+								},
+								this
+							)
+								.then(res => {
+									next(null, res.songs);
+								})
+								.catch(next);
+						else next("Invalid YouTube URL.");
+					},
+
+					(youtubeIds, next) => {
+						let successful = 0;
+						let videos = {};
+						let failed = 0;
+						let alreadyInDatabase = 0;
+	
+						if (youtubeIds.length === 0) next();
+	
+						async.eachOfLimit(
+							youtubeIds,
+							1,
+							(youtubeId, index, next2) => {
+								YouTubeModule.runJob("GET_VIDEO", { identifier: youtubeId, createMissing: true }, this)
+									.then(res => {
+										successful += 1;
+										if (res.existing) alreadyInDatabase += 1;
+										if (res.video) videos[index] = res.video;
+									})
+									.catch(() => {
+										failed += 1;
+									})
+									.finally(() => {
+										next2();
+									});
+							},
+							() => {
+								if (payload.returnVideos)
+									videos = Object.keys(videos)
+										.sort()
+										.map(key => videos[key]);
+	
+								next(null, { successful, failed, alreadyInDatabase, videos });
+							}
+						);
+					}
+				],
+				(err, response) => {
+					if (err) reject(new Error(err));
+					else resolve(response);
+				}
+			)
+		});
+	}
 }
 
 export default new _YouTubeModule();

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

@@ -24,7 +24,6 @@ export default {
 			createStation: "CreateStation.vue",
 			editNews: "EditNews.vue",
 			manageStation: "ManageStation/index.vue",
-			importPlaylist: "ImportPlaylist.vue",
 			editPlaylist: "EditPlaylist/index.vue",
 			createPlaylist: "CreatePlaylist.vue",
 			report: "Report.vue",

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

@@ -95,7 +95,10 @@
 								<div class="youtube-icon"></div>
 							</i>
 							<i
-								v-if="disabledActions.indexOf('report') === -1"
+								v-if="
+									song._id &&
+									disabledActions.indexOf('report') === -1
+								"
 								class="material-icons report-icon"
 								@click="report(song)"
 								content="Report Song"
@@ -123,6 +126,7 @@
 							<i
 								v-if="
 									loggedIn &&
+									song._id &&
 									userRole === 'admin' &&
 									disabledActions.indexOf('edit') === -1
 								"

+ 0 - 164
frontend/src/components/modals/ImportPlaylist.vue

@@ -1,164 +0,0 @@
-<template>
-	<modal title="Import Playlist">
-		<template #body>
-			<div class="vertical-padding">
-				<p class="section-description">
-					Import a playlist by using a link from YouTube
-				</p>
-
-				<div class="control is-grouped">
-					<p class="control is-expanded">
-						<input
-							class="input"
-							type="text"
-							placeholder="YouTube Playlist URL"
-							v-model="youtubeSearch.playlist.query"
-							@keyup.enter="importPlaylist()"
-						/>
-					</p>
-					<p id="playlist-import-type" class="control select">
-						<select
-							v-model="
-								youtubeSearch.playlist.isImportingOnlyMusic
-							"
-						>
-							<option :value="false">Import all</option>
-							<option :value="true">Import only music</option>
-						</select>
-					</p>
-					<p class="control">
-						<button
-							class="button is-info"
-							@click.prevent="importPlaylist()"
-						>
-							<i class="material-icons icon-with-button"
-								>publish</i
-							>Import
-						</button>
-					</p>
-				</div>
-			</div>
-		</template>
-		<template #footer>
-			<p class="is-expanded checkbox-control">
-				<label class="switch">
-					<input
-						type="checkbox"
-						id="edit-imported-songs"
-						v-model="localEditSongs"
-					/>
-					<span class="slider round"></span>
-				</label>
-
-				<label for="edit-imported-songs">
-					<p>Edit Songs</p>
-				</label>
-			</p>
-		</template>
-	</modal>
-</template>
-
-<script>
-import { mapActions, mapState, mapGetters } from "vuex";
-
-import Toast from "toasters";
-
-import SearchYoutube from "@/mixins/SearchYoutube.vue";
-
-export default {
-	mixins: [SearchYoutube],
-	props: {
-		modalUuid: { type: String, default: "" }
-	},
-	computed: {
-		localEditSongs: {
-			get() {
-				return this.$store.state.modals.importPlaylist[this.modalUuid]
-					.editImportedSongs;
-			},
-			set(editImportedSongs) {
-				this.$store.commit(
-					`modals/importPlaylist/${this.modalUuid}/updateEditImportedSongs`,
-					editImportedSongs
-				);
-			}
-		},
-		...mapState({
-			loggedIn: state => state.user.auth.loggedIn
-		}),
-		...mapGetters({
-			socket: "websockets/getSocket"
-		})
-	},
-	beforeUnmount() {
-		// Delete the VueX module that was created for this modal, after all other cleanup tasks are performed
-		this.$store.unregisterModule([
-			"modals",
-			"importPlaylist",
-			this.modalUuid
-		]);
-	},
-	methods: {
-		importPlaylist() {
-			let isImportingPlaylist = true;
-
-			// import query is blank
-			if (!this.youtubeSearch.playlist.query)
-				return new Toast("Please enter a YouTube playlist URL.");
-
-			const regex = /[\\?&]list=([^&#]*)/;
-			const splitQuery = regex.exec(this.youtubeSearch.playlist.query);
-
-			if (!splitQuery) {
-				return new Toast({
-					content: "Please enter a valid YouTube playlist URL.",
-					timeout: 4000
-				});
-			}
-
-			// don't give starting import message instantly in case of instant error
-			setTimeout(() => {
-				if (isImportingPlaylist) {
-					new Toast(
-						"Starting to import your playlist. This can take some time to do."
-					);
-				}
-			}, 750);
-
-			return this.socket.dispatch(
-				"songs.requestSet",
-				this.youtubeSearch.playlist.query,
-				this.youtubeSearch.playlist.isImportingOnlyMusic,
-				true,
-				res => {
-					isImportingPlaylist = false;
-
-					if (
-						this.localEditSongs &&
-						res.status === "success" &&
-						res.songs &&
-						res.songs.length > 0
-					) {
-						this.openModal({
-							modal: "editSongs",
-							data: {
-								songs: res.songs.map(song => ({
-									...song,
-									songId: song._id
-								}))
-							}
-						});
-					}
-
-					this.closeCurrentModal();
-					return new Toast({
-						content: res.message,
-						timeout: 20000
-					});
-				}
-			);
-		},
-		...mapActions("modalVisibility", ["openModal", "closeCurrentModal"])
-	}
-};
-</script>

+ 46 - 4
frontend/src/components/modals/ViewYoutubeVideo.vue

@@ -53,7 +53,7 @@
 					class="duration-canvas"
 					v-show="!player.error"
 					height="20"
-					:width="530"
+					:width="canvasWidth"
 					@click="setTrackPosition($event)"
 				/>
 				<div class="player-footer">
@@ -234,6 +234,7 @@ import { mapActions, mapGetters } from "vuex";
 
 import Toast from "toasters";
 
+import aw from "@/aw";
 import ws from "@/ws";
 import { mapModalState, mapModalActions } from "@/vuex_helpers";
 
@@ -243,7 +244,11 @@ export default {
 	},
 	data() {
 		return {
-			loaded: false
+			loaded: false,
+			canvasWidth: 760,
+			activityWatchVideoDataInterval: null,
+			activityWatchVideoLastStatus: "",
+			activityWatchVideoLastStartDuration: ""
 		};
 	},
 	computed: {
@@ -267,6 +272,7 @@ export default {
 		this.player.playerReady = false;
 		this.player.videoNote = "";
 		clearInterval(this.interval);
+		clearInterval(this.activityWatchVideoDataInterval);
 		this.loaded = false;
 
 		this.socket.dispatch(
@@ -358,6 +364,13 @@ export default {
 							if (this.player.paused === false) this.drawCanvas();
 						}, 200);
 
+						this.activityWatchVideoDataInterval = setInterval(
+							() => {
+								this.sendActivityWatchVideoData();
+							},
+							1000
+						);
+
 						if (window.YT && window.YT.Player) {
 							this.player.player = new window.YT.Player(
 								`viewYoutubeVideoPlayer-${this.modalUuid}`,
@@ -640,7 +653,8 @@ export default {
 			const duration = Number(this.video.duration);
 			const afterDuration = videoDuration - duration;
 
-			const width = 530;
+			this.canvasWidth = Math.min(document.body.clientWidth - 40, 760);
+			const width = this.canvasWidth;
 
 			const currentTime =
 				this.player.player && this.player.player.getCurrentTime
@@ -670,10 +684,38 @@ export default {
 					Number(this.player.player.getDuration()) *
 						((event.pageX -
 							event.target.getBoundingClientRect().left) /
-							530)
+							this.canvasWidth)
 				)
 			);
 		},
+		sendActivityWatchVideoData() {
+			if (!this.player.paused) {
+				if (this.activityWatchVideoLastStatus !== "playing") {
+					this.activityWatchVideoLastStatus = "playing";
+					this.activityWatchVideoLastStartDuration = Math.floor(
+						parseFloat(this.player.currentTime)
+					);
+				}
+
+				const videoData = {
+					title: this.video.title,
+					artists: this.video.author,
+					youtubeId: this.video.youtubeId,
+					muted: this.player.muted,
+					volume: this.player.volume,
+					startedDuration:
+						this.activityWatchVideoLastStartDuration <= 0
+							? 0
+							: this.activityWatchVideoLastStartDuration,
+					source: `viewYoutubeVideo#${this.video.youtubeId}`,
+					hostname: window.location.hostname
+				};
+
+				aw.sendVideoData(videoData);
+			} else {
+				this.activityWatchVideoLastStatus = "not_playing";
+			}
+		},
 		...mapModalActions("modals/viewYoutubeVideo/MODAL_UUID", [
 			"updatePlayer",
 			"stopVideo",

+ 1 - 1
frontend/src/pages/Admin/Songs/Import.vue

@@ -184,7 +184,7 @@ export default {
 			}, 750);
 
 			return this.socket.dispatch(
-				"songs.requestSet",
+				"youtube.requestSet",
 				this.createImport.youtubeUrl,
 				this.createImport.isImportingOnlyMusic,
 				true,

+ 0 - 6
frontend/src/pages/Admin/Songs/index.vue

@@ -10,12 +10,6 @@
 				<button class="button is-primary" @click="create()">
 					Create song
 				</button>
-				<button
-					class="button is-primary"
-					@click="openModal('importPlaylist')"
-				>
-					Import playlist
-				</button>
 				<button
 					class="button is-primary"
 					@click="openModal('importAlbum')"

+ 0 - 1
frontend/src/store/index.js

@@ -27,7 +27,6 @@ export default createStore({
 				editSong: emptyModule,
 				editSongs: emptyModule,
 				importAlbum: emptyModule,
-				importPlaylist: emptyModule,
 				editPlaylist: emptyModule,
 				manageStation: emptyModule,
 				editUser: emptyModule,

+ 0 - 2
frontend/src/store/modules/modalVisibility.js

@@ -6,7 +6,6 @@ import whatIsNew from "./modals/whatIsNew";
 import createStation from "./modals/createStation";
 import editNews from "./modals/editNews";
 import manageStation from "./modals/manageStation";
-import importPlaylist from "./modals/importPlaylist";
 import editPlaylist from "./modals/editPlaylist";
 import report from "./modals/report";
 import viewReport from "./modals/viewReport";
@@ -30,7 +29,6 @@ const modalModules = {
 	createStation,
 	editNews,
 	manageStation,
-	importPlaylist,
 	editPlaylist,
 	report,
 	viewReport,

+ 0 - 3
frontend/src/store/modules/modals/viewYoutubeVideo.js

@@ -40,10 +40,7 @@ export default {
 			state.video = video;
 		},
 		updatePlayer(state, player) {
-			console.log(1212, player, state.player);
-			// state.player = player;
 			state.player = Object.assign(state.player, player);
-			console.log(1313, state.player);
 		},
 		stopVideo(state) {
 			if (state.player.player && state.player.player.pauseVideo) {