Browse Source

feat: added basic tagging to songs

Kristian Vos 2 years ago
parent
commit
b5056570ff

+ 9 - 1
backend/logic/db/index.js

@@ -12,7 +12,7 @@ const REQUIRED_DOCUMENT_VERSIONS = {
 	punishment: 1,
 	queueSong: 1,
 	report: 5,
-	song: 5,
+	song: 6,
 	station: 6,
 	user: 3
 };
@@ -199,6 +199,14 @@ class _DBModule extends CoreClass {
 					};
 					this.schemas.song.path("genres").validate(songGenres, "Invalid genres.");
 
+					const songTags = tags => {
+						return (
+							tags.filter(tag => (new RegExp(/^[a-zA-Z0-9_]{1,64}$|^[a-zA-Z0-9_]{1,64}\[[a-zA-Z0-9_]{1,64}\]$/)).test(tag)).length ===
+							tags.length
+						);
+					};
+					this.schemas.song.path("tags").validate(songTags, "Invalid tags.");
+
 					const songThumbnail = thumbnail => {
 						if (!isLength(thumbnail, 1, 256)) return false;
 						if (config.get("cookie.secure") === true) return thumbnail.startsWith("https://");

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

@@ -3,6 +3,7 @@ export default {
 	title: { type: String, required: true },
 	artists: [{ type: String, default: [] }],
 	genres: [{ type: String, default: [] }],
+	tags: [{ type: String, default: [] }],
 	duration: { type: Number, min: 1, required: true },
 	skipDuration: { type: Number, required: true, default: 0 },
 	thumbnail: { type: String },
@@ -15,5 +16,5 @@ export default {
 	verifiedAt: { type: Date },
 	discogs: { type: Object },
 	status: { type: String, required: true, default: "hidden", enum: ["hidden", "unverified", "verified"] },
-	documentVersion: { type: Number, default: 5, required: true }
+	documentVersion: { type: Number, default: 6, required: true }
 };

+ 42 - 0
backend/logic/migration/migrations/migration17.js

@@ -0,0 +1,42 @@
+import async from "async";
+
+/**
+ * Migration 17
+ *
+ * Migration for songs to add tags property
+ *
+ * @param {object} MigrationModule - the MigrationModule
+ * @returns {Promise} - returns promise
+ */
+export default async function migrate(MigrationModule) {
+	const songModel = await MigrationModule.runJob("GET_MODEL", { modelName: "song" }, this);
+
+	return new Promise((resolve, reject) => {
+		async.waterfall(
+			[
+				next => {
+					this.log("INFO", `Migration 17. Finding songs with document version 5.`);
+					songModel.updateMany(
+						{ documentVersion: 5 },
+						{ $set: { documentVersion: 6, "tags": [] } },
+						(err, res) => {
+							if (err) next(err);
+							else {
+								console.log(res);
+								this.log(
+									"INFO",
+									`Migration 17. Matched: ${res.matchedCount}, modified: ${res.modifiedCount}.`
+								);
+								next();
+							}
+						}
+					);
+				}
+			],
+			err => {
+				if (err) reject(new Error(err));
+				else resolve();
+			}
+		);
+	});
+}

+ 80 - 7
frontend/src/components/modals/EditSong/index.vue

@@ -222,6 +222,17 @@
 									</button>
 								</p>
 							</div>
+							<div class="youtube-id-container">
+								<label class="label">YouTube ID</label>
+								<p class="control">
+									<input
+										class="input"
+										type="text"
+										placeholder="Enter YouTube ID..."
+										v-model="song.youtubeId"
+									/>
+								</p>
+							</div>
 						</div>
 
 						<div class="control is-grouped">
@@ -376,16 +387,39 @@
 									</div>
 								</div>
 							</div>
-							<div class="youtube-id-container">
-								<label class="label">YouTube ID</label>
-								<p class="control">
+							<div class="tags-container">
+								<label class="label">Tags</label>
+								<p class="control has-addons">
 									<input
 										class="input"
 										type="text"
-										placeholder="Enter YouTube ID..."
-										v-model="song.youtubeId"
+										ref="new-tag"
+										v-model="tagInputValue"
+										placeholder="Add tag..."
+										@keyup.exact.enter="addTag('tags')"
 									/>
+									<button
+										class="button is-info add-button"
+										@click="addTag('tags')"
+									>
+										<i class="material-icons">add</i>
+									</button>
 								</p>
+								<div class="list-container">
+									<div
+										class="list-item"
+										v-for="tag in song.tags"
+										:key="tag"
+									>
+										<div
+											class="list-item-circle"
+											@click="removeTag('tags', tag)"
+										>
+											<i class="material-icons">close</i>
+										</div>
+										<p>{{ tag }}</p>
+									</div>
+								</div>
 							</div>
 						</div>
 					</div>
@@ -605,6 +639,7 @@ export default {
 			skipToLast10SecsPressed: false,
 			artistInputValue: "",
 			genreInputValue: "",
+			tagInputValue: "",
 			artistInputFocussed: false,
 			genreInputFocussed: false,
 			genreAutosuggestContainerFocussed: false,
@@ -1266,6 +1301,25 @@ export default {
 				return new Toast(error);
 			}
 
+			error = undefined;
+			song.tags.forEach(tag => {
+				if (
+					!new RegExp(
+						/^[a-zA-Z0-9_]{1,64}$|^[a-zA-Z0-9_]{1,64}\[[a-zA-Z0-9_]{1,64}\]$/
+					).test(tag)
+				) {
+					error = "Invalid tag format.";
+					return error;
+				}
+
+				return false;
+			});
+
+			if (error) {
+				saveButtonRef.handleFailedSave();
+				return new Toast(error);
+			}
+
 			// Thumbnail
 			if (!validation.isLength(song.thumbnail, 1, 256)) {
 				saveButtonRef.handleFailedSave();
@@ -1464,6 +1518,17 @@ export default {
 				}
 				return new Toast("Artist cannot be empty");
 			}
+			if (type === "tags") {
+				const tag = value || this.tagInputValue;
+				if (this.song.tags.indexOf(tag) !== -1)
+					return new Toast("Tag already exists");
+				if (tag !== "") {
+					this.song.tags.push(tag);
+					this.tagInputValue = "";
+					return false;
+				}
+				return new Toast("Tag cannot be empty");
+			}
 
 			return false;
 		},
@@ -1472,6 +1537,8 @@ export default {
 				this.song.genres.splice(this.song.genres.indexOf(value), 1);
 			else if (type === "artists")
 				this.song.artists.splice(this.song.artists.indexOf(value), 1);
+			else if (type === "tags")
+				this.song.tags.splice(this.song.tags.indexOf(value), 1);
 		},
 		drawCanvas() {
 			const canvasElement = this.$refs.durationCanvas;
@@ -1938,7 +2005,12 @@ export default {
 		}
 
 		.album-art-container {
-			width: 100%;
+			margin-right: 16px;
+			width: calc((100% - 16px) / 3 * 2);
+		}
+
+		.youtube-id-container {
+			width: calc((100% - 16px) / 3);
 		}
 
 		.artists-container {
@@ -1969,8 +2041,9 @@ export default {
 			}
 		}
 
-		.youtube-id-container {
+		.tags-container {
 			width: calc((100% - 32px) / 3);
+			position: relative;
 		}
 
 		.list-item-circle {