瀏覽代碼

Temporary artist playlists

(special characters in artist name will prevent the playlist from being created atm)
Owen Diffey 3 年之前
父節點
當前提交
4f327efdd4

+ 171 - 0
backend/logic/actions/playlists.js

@@ -239,6 +239,7 @@ export default {
 						query,
 						includeUser: true,
 						includeGenre: true,
+						includeArtist: true,
 						includeOwn: true,
 						includeSongs: true,
 						userId: session.userId,
@@ -1648,6 +1649,41 @@ export default {
 		);
 	}),
 
+	/**
+	 * Deletes all orphaned artist playlists
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {Function} cb - gets called with the result
+	 */
+	deleteOrphanedArtistPlaylists: isAdminRequired(async function index(session, cb) {
+		async.waterfall(
+			[
+				next => {
+					PlaylistsModule.runJob("DELETE_ORPHANED_ARTIST_PLAYLISTS", {}, this)
+						.then(() => next())
+						.catch(next);
+				}
+			],
+			async err => {
+				if (err) {
+					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+					this.log(
+						"ERROR",
+						"PLAYLISTS_DELETE_ORPHANED_ARTIST_PLAYLISTS",
+						`Deleting orphaned artist playlists failed. "${err}"`
+					);
+					return cb({ status: "error", message: err });
+				}
+				this.log(
+					"SUCCESS",
+					"PLAYLISTS_DELETE_ORPHANED_ARTIST_PLAYLISTS",
+					"Deleting orphaned artist playlists successful."
+				);
+				return cb({ status: "success", message: "Successfully deleted orphaned artist playlists." });
+			}
+		);
+	}),
+
 	/**
 	 * Requests orpahned playlist songs
 	 *
@@ -1783,6 +1819,56 @@ export default {
 		);
 	}),
 
+	/**
+	 * Clears and refills a artist playlist
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {string} playlistId - the id of the playlist we are clearing and refilling
+	 * @param {Function} cb - gets called with the result
+	 */
+	clearAndRefillArtistPlaylist: isAdminRequired(async function index(session, playlistId, cb) {
+		async.waterfall(
+			[
+				next => {
+					if (!playlistId) next("Please specify a playlist id");
+					else {
+						PlaylistsModule.runJob("CLEAR_AND_REFILL_ARTIST_PLAYLIST", { playlistId }, this)
+							.then(() => {
+								next();
+							})
+							.catch(err => {
+								next(err);
+							});
+					}
+				}
+			],
+			async err => {
+				if (err) {
+					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+
+					this.log(
+						"ERROR",
+						"PLAYLIST_CLEAR_AND_REFILL_ARTIST_PLAYLIST",
+						`Clearing and refilling artist playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
+					);
+
+					return cb({ status: "error", message: err });
+				}
+
+				this.log(
+					"SUCCESS",
+					"PLAYLIST_CLEAR_AND_REFILL_ARTIST_PLAYLIST",
+					`Successfully cleared and refilled artist playlist "${playlistId}" for user "${session.userId}".`
+				);
+
+				return cb({
+					status: "success",
+					message: "Playlist has been successfully cleared and refilled"
+				});
+			}
+		);
+	}),
+
 	/**
 	 * Clears and refills all station playlists
 	 *
@@ -1909,6 +1995,91 @@ export default {
 					`Successfully cleared and refilled all genre playlists for user "${session.userId}".`
 				);
 
+				return cb({
+					status: "success",
+					message: "Playlists have been successfully cleared and refilled"
+				});
+			}
+		);
+	}),
+
+	/**
+	 * Clears and refills all artist playlists
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {Function} cb - gets called with the result
+	 */
+	clearAndRefillAllArtistPlaylists: isAdminRequired(async function index(session, cb) {
+		async.waterfall(
+			[
+				next => {
+					PlaylistsModule.runJob("GET_ALL_ARTIST_PLAYLISTS", {}, this)
+						.then(response => {
+							next(null, response.playlists);
+						})
+						.catch(err => {
+							next(err);
+						});
+				},
+
+				(playlists, next) => {
+					async.eachLimit(
+						playlists,
+						1,
+						(playlist, next) => {
+							PlaylistsModule.runJob(
+								"CLEAR_AND_REFILL_ARTIST_PLAYLIST",
+								{ playlistId: playlist._id },
+								this
+							)
+								.then(() => {
+									next();
+								})
+								.catch(err => {
+									next(err);
+								});
+						},
+						next
+					);
+				}
+				// next => {
+				// 	// PlaylistsModule.runJob("CREATE_MISSING_ARTIST_PLAYLISTS", {}, null)
+				// 	// 	.then()
+				// 	// 	.catch()
+				// 	// 	.finally(() => {
+				// 	// 		SongsModule.runJob("GET_ALL_ARTISTS", {}, null)
+				// 	// 			.then(response => {
+				// 	// 				const { artists } = response;
+				// 	// 				artists.forEach(artist => {
+				// 	// 					PlaylistsModule.runJob("AUTOFILL_ARTIST_PLAYLIST", { artist }, null).then().catch();
+				// 	// 				});
+				// 	// 			})
+				// 	// 			.catch();
+				// 	// 	});
+				// 	PlaylistsModule.runJob("GET_MISSING_ARTIST_PLAYLISTS", {}, this).then(response => {
+				// 		console.log(response);
+				// 	});
+				// }
+			],
+			async err => {
+				if (err) {
+					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+
+					this.log(
+						"ERROR",
+						"PLAYLIST_CLEAR_AND_REFILL_ALL_ARTIST_PLAYLISTS",
+						`Clearing and refilling all artist playlists failed for user "${session.userId}". "${err}"`
+					);
+
+					return cb({ status: "error", message: err });
+				}
+
+				this.log(
+					"SUCCESS",
+					"PLAYLIST_CLEAR_AND_REFILL_ALL_ARTIST_PLAYLISTS",
+					`Successfully cleared and refilled all artist playlists for user "${session.userId}".`
+				);
+
 				return cb({
 					status: "success",
 					message: "Playlists have been successfully cleared and refilled"

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

@@ -405,6 +405,15 @@ export default {
 										.catch(() => {});
 								});
 
+							existingSong.artists
+								.concat(song.artists)
+								.filter((value, index, self) => self.indexOf(value) === index)
+								.forEach(artist => {
+									PlaylistsModule.runJob("AUTOFILL_ARTIST_PLAYLIST", { artist })
+										.then(() => {})
+										.catch(() => {});
+								});
+
 							next(null, song);
 						})
 						.catch(next);
@@ -684,6 +693,12 @@ export default {
 							.catch(() => {});
 					});
 
+					song.artists.forEach(artist => {
+						PlaylistsModule.runJob("AUTOFILL_ARTIST_PLAYLIST", { artist })
+							.then(() => {})
+							.catch(() => {});
+					});
+
 					SongsModule.runJob("UPDATE_SONG", { songId: song._id });
 					next(null, song, oldStatus);
 				}
@@ -756,6 +771,12 @@ export default {
 							.catch(() => {});
 					});
 
+					song.artists.forEach(artist => {
+						PlaylistsModule.runJob("AUTOFILL_ARTIST_PLAYLIST", { artist })
+							.then(() => {})
+							.catch(() => {});
+					});
+
 					SongsModule.runJob("UPDATE_SONG", { songId });
 
 					next(null);

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

@@ -18,6 +18,6 @@ export default {
 	createdAt: { type: Date, default: Date.now, required: true },
 	createdFor: { type: String },
 	privacy: { type: String, enum: ["public", "private"], default: "private" },
-	type: { type: String, enum: ["user", "genre", "station"], required: true },
+	type: { type: String, enum: ["user", "genre", "station", "artist"], required: true },
 	documentVersion: { type: Number, default: 4, required: true }
 };

+ 380 - 0
backend/logic/playlists.js

@@ -210,6 +210,42 @@ class _PlaylistsModule extends CoreClass {
 		});
 	}
 
+	/**
+	 * Creates a playlist that contains all songs of a specific artist
+	 *
+	 * @param {object} payload - object that contains the payload
+	 * @param {string} payload.artist - the artist
+	 * @returns {Promise} - returns promise (reject, resolve)
+	 */
+	CREATE_ARTIST_PLAYLIST(payload) {
+		return new Promise((resolve, reject) => {
+			PlaylistsModule.runJob("GET_ARTIST_PLAYLIST", { artist: payload.artist.toLowerCase() }, this)
+				.then(() => {
+					reject(new Error("Playlist already exists"));
+				})
+				.catch(err => {
+					if (err.message === "Playlist not found") {
+						PlaylistsModule.playlistModel.create(
+							{
+								isUserModifiable: false,
+								displayName: `Artist - ${payload.artist}`,
+								songs: [],
+								createdBy: "Musare",
+								createdFor: `${payload.artist.toLowerCase()}`,
+								createdAt: Date.now(),
+								type: "artist",
+								privacy: "public"
+							},
+							(err, playlist) => {
+								if (err) return reject(new Error(err));
+								return resolve(playlist._id);
+							}
+						);
+					} else reject(new Error(err));
+				});
+		});
+	}
+
 	/**
 	 * Gets all genre playlists
 	 *
@@ -227,6 +263,23 @@ class _PlaylistsModule extends CoreClass {
 		});
 	}
 
+	/**
+	 * Gets all artist playlists
+	 *
+	 * @param {object} payload - object that contains the payload
+	 * @param {string} payload.includeSongs - include the songs
+	 * @returns {Promise} - returns promise (reject, resolve)
+	 */
+	GET_ALL_ARTIST_PLAYLISTS(payload) {
+		return new Promise((resolve, reject) => {
+			const includeObject = payload.includeSongs ? null : { songs: false };
+			PlaylistsModule.playlistModel.find({ type: "artist" }, includeObject, (err, playlists) => {
+				if (err) reject(new Error(err));
+				else resolve({ playlists });
+			});
+		});
+	}
+
 	/**
 	 * Gets all station playlists
 	 *
@@ -343,6 +396,105 @@ class _PlaylistsModule extends CoreClass {
 		});
 	}
 
+	/**
+	 * Gets a artist playlist
+	 *
+	 * @param {object} payload - object that contains the payload
+	 * @param {string} payload.artist - the artist
+	 * @param {string} payload.includeSongs - include the songs
+	 * @returns {Promise} - returns promise (reject, resolve)
+	 */
+	GET_ARTIST_PLAYLIST(payload) {
+		return new Promise((resolve, reject) => {
+			const includeObject = payload.includeSongs ? null : { songs: false };
+			PlaylistsModule.playlistModel.findOne(
+				{ type: "artist", createdFor: payload.artist },
+				includeObject,
+				(err, playlist) => {
+					if (err) reject(new Error(err));
+					else if (!playlist) reject(new Error("Playlist not found"));
+					else resolve({ playlist });
+				}
+			);
+		});
+	}
+
+	/**
+	 * Gets all missing artist playlists
+	 *
+	 * @returns {Promise} - returns promise (reject, resolve)
+	 */
+	GET_MISSING_ARTIST_PLAYLISTS() {
+		return new Promise((resolve, reject) => {
+			SongsModule.runJob("GET_ALL_ARTISTS", {}, this)
+				.then(response => {
+					const { artists } = response;
+					const missingArtists = [];
+					async.eachLimit(
+						artists,
+						1,
+						(artist, next) => {
+							PlaylistsModule.runJob(
+								"GET_ARTIST_PLAYLIST",
+								{ artist: artist.toLowerCase(), includeSongs: false },
+								this
+							)
+								.then(() => {
+									next();
+								})
+								.catch(err => {
+									if (err.message === "Playlist not found") {
+										missingArtists.push(artist);
+										next();
+									} else next(err);
+								});
+						},
+						err => {
+							if (err) reject(err);
+							else resolve({ artists: missingArtists });
+						}
+					);
+				})
+				.catch(err => {
+					reject(err);
+				});
+		});
+	}
+
+	/**
+	 * Creates all missing artist playlists
+	 *
+	 * @returns {Promise} - returns promise (reject, resolve)
+	 */
+	CREATE_MISSING_ARTIST_PLAYLISTS() {
+		return new Promise((resolve, reject) => {
+			PlaylistsModule.runJob("GET_MISSING_ARTIST_PLAYLISTS", {}, this)
+				.then(response => {
+					const { artists } = response;
+					async.eachLimit(
+						artists,
+						1,
+						(artist, next) => {
+							PlaylistsModule.runJob("CREATE_ARTIST_PLAYLIST", { artist }, this)
+								.then(() => {
+									next();
+								})
+								.catch(err => {
+									next(err);
+								});
+						},
+						err => {
+							if (err) reject(err);
+							else resolve();
+						}
+					);
+				})
+				.catch(err => {
+					reject(err);
+				});
+		});
+	}
+
 	/**
 	 * Gets a station playlist
 	 *
@@ -663,6 +815,187 @@ class _PlaylistsModule extends CoreClass {
 		});
 	}
 
+	/**
+	 * Fills a artist playlist with songs
+	 *
+	 * @param {object} payload - object that contains the payload
+	 * @param {string} payload.artist - the artist
+	 * @returns {Promise} - returns promise (reject, resolve)
+	 */
+	AUTOFILL_ARTIST_PLAYLIST(payload) {
+		return new Promise((resolve, reject) => {
+			async.waterfall(
+				[
+					next => {
+						PlaylistsModule.runJob(
+							"GET_ARTIST_PLAYLIST",
+							{ artist: payload.artist.toLowerCase(), includeSongs: true },
+							this
+						)
+							.then(response => {
+								next(null, response.playlist._id);
+							})
+							.catch(err => {
+								if (err.message === "Playlist not found") {
+									PlaylistsModule.runJob("CREATE_ARTIST_PLAYLIST", { artist: payload.artist }, this)
+										.then(playlistId => {
+											next(null, playlistId);
+										})
+										.catch(err => {
+											next(err);
+										});
+								} else next(err);
+							});
+					},
+
+					(playlistId, next) => {
+						SongsModule.runJob("GET_ALL_SONGS_WITH_ARTIST", { artist: payload.artist }, this)
+							.then(response => {
+								next(null, playlistId, response.songs);
+							})
+							.catch(err => {
+								console.log(err);
+								next(err);
+							});
+					},
+
+					(playlistId, _songs, next) => {
+						const songs = _songs.map(song => {
+							const { _id, youtubeId, title, artists, thumbnail, duration, status } = song;
+							return {
+								_id,
+								youtubeId,
+								title,
+								artists,
+								thumbnail,
+								duration,
+								status
+							};
+						});
+
+						PlaylistsModule.playlistModel.updateOne({ _id: playlistId }, { $set: { songs } }, err => {
+							next(err, playlistId);
+						});
+					},
+
+					(playlistId, next) => {
+						PlaylistsModule.runJob("UPDATE_PLAYLIST", { playlistId }, this)
+							.then(() => {
+								next(null, playlistId);
+							})
+							.catch(next);
+					},
+
+					(playlistId, next) => {
+						StationsModule.runJob("GET_STATIONS_THAT_INCLUDE_OR_EXCLUDE_PLAYLIST", { playlistId }, this)
+							.then(response => {
+								async.eachLimit(
+									response.stationIds,
+									1,
+									(stationId, next) => {
+										PlaylistsModule.runJob("AUTOFILL_STATION_PLAYLIST", { stationId }, this)
+											.then(() => {
+												next();
+											})
+											.catch(err => {
+												next(err);
+											});
+									},
+									err => {
+										if (err) next(err);
+										else next();
+									}
+								);
+							})
+							.catch(err => {
+								next(err);
+							});
+					}
+				],
+				err => {
+					if (err && err !== true) return reject(new Error(err));
+					return resolve({});
+				}
+			);
+		});
+	}
+
+	/**
+	 * Gets orphaned artist playlists
+	 *
+	 * @returns {Promise} - returns promise (reject, resolve)
+	 */
+	GET_ORPHANED_ARTIST_PLAYLISTS() {
+		return new Promise((resolve, reject) => {
+			PlaylistsModule.playlistModel.find({ type: "artist" }, { songs: false }, (err, playlists) => {
+				if (err) reject(new Error(err));
+				else {
+					const orphanedPlaylists = [];
+					async.eachLimit(
+						playlists,
+						1,
+						(playlist, next) => {
+							SongsModule.runJob("GET_ALL_SONGS_WITH_ARTIST", { artist: playlist.createdFor }, this)
+								.then(response => {
+									if (response.songs.length === 0) {
+										StationsModule.runJob(
+											"GET_STATIONS_THAT_INCLUDE_OR_EXCLUDE_PLAYLIST",
+											{ playlistId: playlist._id },
+											this
+										)
+											.then(response => {
+												if (response.stationIds.length === 0) orphanedPlaylists.push(playlist);
+												next();
+											})
+											.catch(next);
+									} else next();
+								})
+								.catch(next);
+						},
+						err => {
+							if (err) reject(new Error(err));
+							else resolve({ playlists: orphanedPlaylists });
+						}
+					);
+				}
+			});
+		});
+	}
+
+	/**
+	 * Deletes all orphaned artist playlists
+	 *
+	 * @returns {Promise} - returns promise (reject, resolve)
+	 */
+	DELETE_ORPHANED_ARTIST_PLAYLISTS() {
+		return new Promise((resolve, reject) => {
+			PlaylistsModule.runJob("GET_ORPHANED_ARTIST_PLAYLISTS", {}, this)
+				.then(response => {
+					async.eachLimit(
+						response.playlists,
+						1,
+						(playlist, next) => {
+							PlaylistsModule.runJob("DELETE_PLAYLIST", { playlistId: playlist._id }, this)
+								.then(() => {
+									this.log("INFO", "Deleting orphaned artist playlist");
+									next();
+								})
+								.catch(err => {
+									next(err);
+								});
+						},
+						err => {
+							if (err) reject(new Error(err));
+							else resolve({});
+						}
+					);
+				})
+				.catch(err => {
+					reject(new Error(err));
+				});
+		});
+	}
+
 	/**
 	 * Gets a orphaned station playlists
 	 *
@@ -1026,6 +1359,7 @@ class _PlaylistsModule extends CoreClass {
 	 * @param {string} payload.includeStation - include station playlists
 	 * @param {string} payload.includeUser - include user playlists
 	 * @param {string} payload.includeGenre - include genre playlists
+	 * @param {string} payload.includeArtist - include artist playlists
 	 * @param {string} payload.includeOwn - include own user playlists
 	 * @param {string} payload.userId - the user id of the person requesting
 	 * @param {string} payload.includeSongs - include songs
@@ -1041,6 +1375,7 @@ class _PlaylistsModule extends CoreClass {
 						if (payload.includeStation) types.push("station");
 						if (payload.includeUser) types.push("user");
 						if (payload.includeGenre) types.push("genre");
+						if (payload.includeArtist) types.push("artist");
 						if (types.length === 0 && !payload.includeOwn) return next("No types have been included.");
 
 						const privacies = ["public"];
@@ -1195,6 +1530,51 @@ class _PlaylistsModule extends CoreClass {
 			);
 		});
 	}
+
+	/**
+	 * Clears and refills a artist playlist
+	 *
+	 * @param {object} payload - object that contains the payload
+	 * @param {string} payload.playlistId - the id of the playlist we are trying to clear and refill
+	 * @returns {Promise} - returns promise (reject, resolve)
+	 */
+	CLEAR_AND_REFILL_ARTIST_PLAYLIST(payload) {
+		return new Promise((resolve, reject) => {
+			const { playlistId } = payload;
+			async.waterfall(
+				[
+					next => {
+						PlaylistsModule.runJob("GET_PLAYLIST", { playlistId }, this)
+							.then(playlist => {
+								next(null, playlist);
+							})
+							.catch(err => {
+								next(err);
+							});
+					},
+
+					(playlist, next) => {
+						if (playlist.type !== "artist") next("This playlist is not a artist playlist.");
+						else next(null, playlist.createdFor);
+					},
+
+					(artist, next) => {
+						PlaylistsModule.runJob("AUTOFILL_ARTIST_PLAYLIST", { artist }, this)
+							.then(() => {
+								next();
+							})
+							.catch(err => {
+								next(err);
+							});
+					}
+				],
+				err => {
+					if (err && err !== true) return reject(new Error(err));
+					return resolve();
+				}
+			);
+		});
+	}
 }
 
 export default new _PlaylistsModule();

+ 78 - 0
backend/logic/songs.js

@@ -499,6 +499,20 @@ class _SongsModule extends CoreClass {
 								next(err, song);
 							}
 						);
+						async.eachLimit(
+							song.artists,
+							1,
+							(artist, next) => {
+								PlaylistsModule.runJob("AUTOFILL_ARTIST_PLAYLIST", { artist }, this)
+									.then(() => {
+										next();
+									})
+									.catch(err => next(err));
+							},
+							err => {
+								next(err, song);
+							}
+						);
 					}
 				],
 				(err, song) => {
@@ -816,6 +830,41 @@ class _SongsModule extends CoreClass {
 		);
 	}
 
+	/**
+	 * Gets an array of all artists
+	 *
+	 * @returns {Promise} - returns a promise (resolve, reject)
+	 */
+	GET_ALL_ARTISTS() {
+		return new Promise((resolve, reject) =>
+			async.waterfall(
+				[
+					next => {
+						SongsModule.SongModel.find({ status: "verified" }, { artists: 1, _id: false }, next);
+					},
+
+					(songs, next) => {
+						let allArtists = [];
+						songs.forEach(song => {
+							allArtists = allArtists.concat(song.artists);
+						});
+
+						const lowerCaseArtists = allArtists.map(artist => artist.toLowerCase());
+						const uniqueArtists = lowerCaseArtists.filter(
+							(value, index, self) => self.indexOf(value) === index
+						);
+
+						next(null, uniqueArtists);
+					}
+				],
+				(err, artists) => {
+					if (err && err !== true) return reject(new Error(err));
+					return resolve({ artists });
+				}
+			)
+		);
+	}
+
 	/**
 	 * Gets an array of all songs with a specific genre
 	 *
@@ -845,6 +894,35 @@ class _SongsModule extends CoreClass {
 		);
 	}
 
+	/**
+	 * Gets an array of all songs with a specific artist
+	 *
+	 * @param {object} payload - returns an object containing the payload
+	 * @param {string} payload.artist - the artist
+	 * @returns {Promise} - returns a promise (resolve, reject)
+	 */
+	GET_ALL_SONGS_WITH_ARTIST(payload) {
+		return new Promise((resolve, reject) =>
+			async.waterfall(
+				[
+					next => {
+						SongsModule.SongModel.find(
+							{
+								status: "verified",
+								artists: { $regex: new RegExp(`^${payload.artist.toLowerCase()}$`, "i") }
+							},
+							next
+						);
+					}
+				],
+				(err, songs) => {
+					if (err && err !== true) return reject(new Error(err));
+					return resolve({ songs });
+				}
+			)
+		);
+	}
+
 	// runjob songs GET_ORPHANED_PLAYLIST_SONGS {}
 
 	/**

+ 4 - 2
frontend/src/components/modals/EditPlaylist/Tabs/Settings.vue

@@ -26,7 +26,8 @@
 		<div
 			v-if="
 				userId === playlist.createdBy ||
-				(playlist.type === 'genre' && isAdmin())
+				((playlist.type === 'genre' || playlist.type === 'artist') &&
+					isAdmin())
 			"
 		>
 			<label class="label"> Change privacy </label>
@@ -108,7 +109,8 @@ export default {
 			const { privacy } = this.playlist;
 			if (privacy === "public" || privacy === "private") {
 				this.socket.dispatch(
-					this.playlist.type === "genre"
+					this.playlist.type === "genre" ||
+						this.playlist.type === "artist"
 						? "playlists.updatePrivacyAdmin"
 						: "playlists.updatePrivacy",
 					this.playlist._id,

+ 28 - 2
frontend/src/components/modals/EditPlaylist/index.vue

@@ -29,7 +29,9 @@
 								v-if="
 									userId === playlist.createdBy ||
 									isEditable() ||
-									(playlist.type === 'genre' && isAdmin())
+									((playlist.type === 'genre' ||
+										playlist.type === 'artist') &&
+										isAdmin())
 								"
 							>
 								Settings
@@ -61,7 +63,9 @@
 							v-if="
 								userId === playlist.createdBy ||
 								isEditable() ||
-								(playlist.type === 'genre' && isAdmin())
+								((playlist.type === 'genre' ||
+									playlist.type === 'artist') &&
+									isAdmin())
 							"
 						/>
 						<add-songs
@@ -239,6 +243,14 @@
 						Clear and refill genre playlist
 					</a>
 				</confirm>
+				<confirm
+					v-if="playlist.type === 'artist'"
+					@confirm="clearAndRefillArtistPlaylist()"
+				>
+					<a class="button is-danger">
+						Clear and refill artist playlist
+					</a>
+				</confirm>
 				<confirm v-if="isEditable()" @confirm="removePlaylist()">
 					<a class="button is-danger"> Remove Playlist </a>
 				</confirm>
@@ -566,6 +578,20 @@ export default {
 				}
 			);
 		},
+		clearAndRefillArtistPlaylist() {
+			this.socket.dispatch(
+				"playlists.clearAndRefillArtistPlaylist",
+				this.playlist._id,
+				data => {
+					if (data.status !== "success")
+						new Toast({
+							content: `Error: ${data.message}`,
+							timeout: 8000
+						});
+					else new Toast({ content: data.message, timeout: 4000 });
+				}
+			);
+		},
 		...mapActions({
 			showTab(dispatch, payload) {
 				this.$refs[`${payload}-tab`].scrollIntoView();

+ 35 - 0
frontend/src/pages/Admin/tabs/Playlists.vue

@@ -14,6 +14,12 @@
 			>
 				Delete orphaned genre playlists
 			</button>
+			<button
+				class="button is-primary"
+				@click="deleteOrphanedArtistPlaylists()"
+			>
+				Delete orphaned artist playlists
+			</button>
 			<button
 				class="button is-primary"
 				@click="requestOrphanedPlaylistSongs()"
@@ -32,6 +38,12 @@
 			>
 				Clear and refill all genre playlists
 			</button>
+			<button
+				class="button is-primary"
+				@click="clearAndRefillAllArtistPlaylists()"
+			>
+				Clear and refill all artist playlists
+			</button>
 			<br />
 			<br />
 			<table class="table is-striped">
@@ -184,6 +196,15 @@ export default {
 				}
 			);
 		},
+		deleteOrphanedArtistPlaylists() {
+			this.socket.dispatch(
+				"playlists.deleteOrphanedArtistPlaylists",
+				res => {
+					if (res.status === "success") new Toast(res.message);
+					else new Toast(`Error: ${res.message}`);
+				}
+			);
+		},
 		requestOrphanedPlaylistSongs() {
 			this.socket.dispatch(
 				"playlists.requestOrphanedPlaylistSongs",
@@ -221,6 +242,20 @@ export default {
 				}
 			);
 		},
+		clearAndRefillAllArtistPlaylists() {
+			this.socket.dispatch(
+				"playlists.clearAndRefillAllArtistPlaylists",
+				res => {
+					if (res.status === "success")
+						new Toast({ content: res.message, timeout: 4000 });
+					else
+						new Toast({
+							content: `Error: ${res.message}`,
+							timeout: 4000
+						});
+				}
+			);
+		},
 		...mapActions("modalVisibility", ["openModal"]),
 		...mapActions("user/playlists", ["editPlaylist"])
 	}