Browse Source

refactor: completely updated editsong modal, added discogs support, temporarily disabled snyk test

Kristian Vos 5 years ago
parent
commit
4344774303

+ 1 - 0
README.md

@@ -74,6 +74,7 @@ Once you've installed the required tools:
 |`apis.discord.loggingChannel`|ID of the channel to be used in the Discord logging server.|
 |`apis.mailgun`|Can be obtained by setting up a [Mailgun account](http://www.mailgun.com/), or you can disable it.|
 |`apis.spotify`|Can be obtained by setting up a [Spotify client id](https://developer.spotify.com/dashboard/applications), or you can disable it.|
+|`apis.discogs`|Can be obtained by setting up a [Discogs application](https://www.discogs.com/settings/developers), or you can disable it.|
 |`redis.url`|Should be left alone for Docker, and changed to `redis://localhost:6379/0` for non-Docker.|
 |`redis.password`|Should be the Redis password you either put in your `startRedis.cmd` file for Windows, or `.env` for docker.|
 |`mongo.url`|Needs to have the proper password for the MongoDB musare user, and for non-Docker you need to replace `@musare:27017` with `@localhost:27017`.|

+ 5 - 0
backend/config/template.json

@@ -32,6 +32,11 @@
 			"client": "",
 			"secret": "",
 			"enabled": false
+		},
+		"discogs": {
+			"client": "",
+			"secret": "",
+			"enabled": false
 		}
 	},
 	"cors": {

+ 41 - 0
backend/logic/actions/apis.js

@@ -69,6 +69,47 @@ module.exports = {
 		});
 	}),
 
+	/**
+	 * Gets Discogs data
+	 *
+	 * @param session
+	 * @param query - the query
+	 * @param cb
+	 */
+	searchDiscogs: hooks.adminRequired((session, query, cb, userId) => {
+		async.waterfall([
+			(next) => {
+				const params = [
+					`q=${encodeURIComponent(query)}`,
+					`per_page=20`
+				].join('&');
+		
+				const options = {
+					url: `https://api.discogs.com/database/search?${params}`,
+					headers: {
+						"User-Agent": "Request",
+						"Authorization": `Discogs key=${config.get("apis.discogs.client")}, secret=${config.get("apis.discogs.secret")}`
+					}
+				};
+		
+				request(options, (err, res, body) => {
+					if (err) next(err);
+					body = JSON.parse(body);
+					next(null, body.results);
+					if (body.error) next(body.error);
+				});
+			}
+		], async (err, results) => {
+			if (err) {
+				err = await utils.getError(err);
+				logger.error("APIS_SEARCH_DISCOGS", `Searching discogs failed with query "${query}". "${err}"`);
+				return cb({status: 'failure', message: err});
+			}
+			logger.success('APIS_SEARCH_DISCOGS', `User "${userId}" searched Discogs succesfully for query "${query}".`);
+			cb({status: 'success', results});
+		});
+	}),
+
 	/**
 	 * Joins a room
 	 *

+ 2 - 2
backend/logic/actions/queueSongs.js

@@ -169,12 +169,12 @@ let lib = {
 					next(null, song);
 				});
 			},
-			(newSong, next) => {
+			/*(newSong, next) => {
 				utils.getSongFromSpotify(newSong, (err, song) => {
 					if (!song) next(null, newSong);
 					else next(err, song);
 				});
-			},
+			},*/
 			(newSong, next) => {
 				const song = new db.models.queueSong(newSong);
 				song.save((err, song) => {

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

@@ -155,7 +155,7 @@ module.exports = class extends coreClass {
 		
 					let songGenres = (genres) => {
 						return genres.filter((genre) => {
-								return (isLength(genre, 1, 16) && regex.az09_.test(genre));
+								return (isLength(genre, 1, 16) && regex.azAZ09_.test(genre));
 							}).length === genres.length;
 					};
 					this.schemas.song.path('genres').validate(songGenres, 'Invalid genres.');

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

@@ -8,5 +8,6 @@ module.exports = {
 	thumbnail: { type: String, required: true },
 	explicit: { type: Boolean, required: true },
 	requestedBy: { type: String, required: true },
-	requestedAt: { type: Date, default: Date.now(), required: true }
+	requestedAt: { type: Date, default: Date.now(), required: true },
+	discogs: { type: Object }
 };

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

@@ -12,5 +12,6 @@ module.exports = {
 	requestedBy: { type: String, required: true },
 	requestedAt: { type: Date, required: true },
 	acceptedBy: { type: String, required: true },
-	acceptedAt: { type: Date, default: Date.now(), required: true }
+	acceptedAt: { type: Date, default: Date.now(), required: true },
+	discogs: { type: Object }
 };

+ 938 - 397
frontend/components/Modals/EditSong.vue

@@ -1,238 +1,406 @@
 <template>
 	<div>
-		<modal title="Edit Song">
+		<modal title="Edit Song" class="song-modal">
 			<div slot="body">
-				<h5 class="has-text-centered">Video Preview</h5>
-				<div class="video-container">
-					<div id="player"></div>
-					<canvas
-						id="durationCanvas"
-						height="40"
-						width="560"
-					></canvas>
-					<div class="controls">
-						<form action="#">
-							<p style="margin-top: 0; position: relative;">
-								<input
-									type="range"
-									id="volumeSlider"
-									min="0"
-									max="100"
-									class="active"
-									v-on:change="changeVolume()"
-									v-on:input="changeVolume()"
-								/>
-							</p>
-						</form>
-						<p class="control has-addons">
-							<button
-								class="button"
-								v-on:click="settings('pause')"
-								v-if="!video.paused"
-							>
-								<i class="material-icons">pause</i>
-							</button>
-							<button
-								class="button"
-								v-on:click="settings('play')"
-								v-if="video.paused"
-							>
-								<i class="material-icons">play_arrow</i>
-							</button>
-							<button
-								class="button"
-								v-on:click="settings('stop')"
-							>
-								<i class="material-icons">stop</i>
-							</button>
-							<button
-								class="button"
-								v-on:click="settings('skipToLast10Secs')"
-							>
-								<i class="material-icons">fast_forward</i>
-							</button>
-						</p>
-						<p>
-							YouTube:
-							<span>{{ youtubeVideoCurrentTime }}</span> /
-							<span>{{ youtubeVideoDuration }}</span>
-							{{ youtubeVideoNote }}
-						</p>
-					</div>
-				</div>
-				<h5 class="has-text-centered">Thumbnail Preview</h5>
-				<img
-					class="thumbnail-preview"
-					:src="editing.song.thumbnail"
-					onerror="this.src='/assets/notes-transparent.png'"
-				/>
-
-				<div class="control is-horizontal">
-					<div class="control-label">
-						<label class="label">Thumbnail URL</label>
-					</div>
-					<div class="control">
-						<input
-							class="input"
-							type="text"
-							v-model="editing.song.thumbnail"
+				<div class="left-section">
+					<div class="top-section">
+						<div class="player-section">
+							<div id="player"></div>
+							<canvas
+								id="durationCanvas"
+								height="20"
+								width="530"
+							></canvas>
+							<div class="player-footer">
+								<div class="player-footer-left">
+									<i
+										class="material-icons player-play-pause"
+										v-on:click="settings('play')"
+										v-if="video.paused"
+										>play_arrow</i
+									>
+									<i
+										class="material-icons player-play-pause"
+										v-on:click="settings('pause')"
+										v-if="!video.paused"
+										>pause</i
+									>
+									<i
+										class="material-icons player-stop"
+										v-on:click="settings('stop')"
+										>stop</i
+									>
+									<i
+										class="material-icons player-fast-forward"
+										v-on:click="
+											settings('skipToLast10Secs')
+										"
+										>fast_forward</i
+									>
+								</div>
+								<div class="player-footer-center">
+									<img src="/assets/social/youtube.svg" />
+									<span>
+										<span>
+											{{ youtubeVideoCurrentTime }}
+										</span>
+										/
+										<span>
+											{{ youtubeVideoDuration }}
+											{{ youtubeVideoNote }}
+										</span>
+									</span>
+								</div>
+								<div class="player-footer-right">
+									<input
+										type="range"
+										id="volumeSlider"
+										min="0"
+										max="100"
+										class="active"
+										v-on:change="changeVolume()"
+										v-on:input="changeVolume()"
+									/>
+								</div>
+							</div>
+						</div>
+						<img
+							class="thumbnail-preview"
+							:src="editing.song.thumbnail"
+							onerror="this.src='/assets/notes-transparent.png'"
 						/>
 					</div>
+					<div class="edit-section">
+						<div class="control is-grouped">
+							<div class="title-container">
+								<label class="label">Title</label>
+								<p class="control has-addons">
+									<input
+										class="input"
+										type="text"
+										v-model="editing.song.title"
+									/>
+									<button
+										class="button album-get-button"
+										v-on:click="getAlbumData('title')"
+									>
+										<i class="material-icons album-get-icon"
+											>album</i
+										>
+									</button>
+								</p>
+							</div>
+							<div class="duration-container">
+								<label class="label">Duration</label>
+								<p class="control">
+									<input
+										class="input"
+										type="text"
+										v-model="editing.song.duration"
+									/>
+								</p>
+							</div>
+							<div class="skip-duration-container">
+								<label class="label">Skip duration</label>
+								<p class="control">
+									<input
+										class="input"
+										type="text"
+										v-model="editing.song.skipDuration"
+									/>
+								</p>
+							</div>
+						</div>
+						<div class="control is-grouped">
+							<div class="album-art-container">
+								<label class="label">Album art</label>
+								<p class="control has-addons">
+									<input
+										class="input"
+										type="text"
+										v-model="editing.song.thumbnail"
+									/>
+									<button
+										class="button album-get-button"
+										v-on:click="getAlbumData('albumArt')"
+									>
+										<i class="material-icons album-get-icon"
+											>album</i
+										>
+									</button>
+								</p>
+							</div>
+						</div>
+						<div class="control is-grouped">
+							<div class="artists-container">
+								<label class="label">Artists</label>
+								<p class="control has-addons">
+									<input
+										class="input"
+										type="text"
+										id="new-artist"
+									/>
+									<button
+										class="button album-get-button"
+										v-on:click="getAlbumData('artists')"
+									>
+										<i class="material-icons album-get-icon"
+											>album</i
+										>
+									</button>
+									<button
+										class="button is-info add-button"
+										v-on:click="addTag('artists')"
+									>
+										<i class="material-icons">add</i>
+									</button>
+								</p>
+								<div class="list-container">
+									<div
+										class="list-item"
+										v-for="(artist, index) in editing.song
+											.artists"
+										:key="index"
+									>
+										<div
+											class="list-item-circle"
+											v-on:click="
+												removeTag('artists', index)
+											"
+										>
+											<i class="material-icons">close</i>
+										</div>
+										<p>{{ artist }}</p>
+									</div>
+								</div>
+							</div>
+							<div class="genres-container">
+								<label class="label">Genres</label>
+								<p class="control has-addons">
+									<input
+										class="input"
+										type="text"
+										id="new-genre"
+									/>
+									<button
+										class="button album-get-button"
+										v-on:click="getAlbumData('genres')"
+									>
+										<i class="material-icons album-get-icon"
+											>album</i
+										>
+									</button>
+									<button
+										class="button is-info add-button"
+										v-on:click="addTag('genres')"
+									>
+										<i class="material-icons">add</i>
+									</button>
+								</p>
+								<div class="list-container">
+									<div
+										class="list-item"
+										v-for="(genre, index) in editing.song
+											.genres"
+										:key="index"
+									>
+										<div
+											class="list-item-circle"
+											v-on:click="
+												removeTag('genres', index)
+											"
+										>
+											<i class="material-icons">close</i>
+										</div>
+										<p>{{ genre }}</p>
+									</div>
+								</div>
+							</div>
+							<div class="youtube-id-container">
+								<label class="label">YouTube ID</label>
+								<p class="control">
+									<input
+										class="input"
+										type="text"
+										v-model="editing.song.songId"
+									/>
+								</p>
+							</div>
+						</div>
+					</div>
 				</div>
-
-				<h5 class="has-text-centered">Edit Information</h5>
-
-				<p class="control">
-					<label class="checkbox">
-						<input
-							type="checkbox"
-							v-model="editing.song.explicit"
-						/>
-						Explicit
-					</label>
-				</p>
-				<label class="label">Song ID & Title</label>
-				<div class="control is-horizontal">
-					<div class="control is-grouped">
-						<p class="control is-expanded">
-							<input
-								class="input"
-								type="text"
-								v-model="editing.song.songId"
-							/>
-						</p>
+				<div class="right-section">
+					<div class="api-section">
+						<div
+							class="selected-discogs-info"
+							v-if="!editing.song.discogs"
+						>
+							<p class="selected-discogs-info-none">None</p>
+						</div>
+						<div
+							class="selected-discogs-info"
+							v-if="editing.song.discogs"
+						>
+							<div class="top-container">
+								<img
+									:src="editing.song.discogs.album.albumArt"
+								/>
+								<div class="right-container">
+									<p class="album-title">
+										{{ editing.song.discogs.album.title }}
+									</p>
+									<div class="bottom-row">
+										<p class="type-year">
+											<span>{{
+												editing.song.discogs.album.type
+											}}</span>
+											•
+											<span>{{
+												editing.song.discogs.album.year
+											}}</span>
+										</p>
+									</div>
+								</div>
+							</div>
+							<div class="bottom-container">
+								<p class="bottom-container-field">
+									Artists:
+									<span>{{
+										editing.song.discogs.album.artists.join(
+											", "
+										)
+									}}</span>
+								</p>
+								<p class="bottom-container-field">
+									Genres:
+									<span>{{
+										editing.song.discogs.album.genres.join(
+											", "
+										)
+									}}</span>
+								</p>
+								<p class="bottom-container-field">
+									Data quality:
+									<span>{{
+										editing.song.discogs.dataQuality
+									}}</span>
+								</p>
+								<p class="bottom-container-field">
+									Track:
+									<span
+										>{{
+											editing.song.discogs.track.position
+										}}.
+										{{
+											editing.song.discogs.track.title
+										}}</span
+									>
+								</p>
+							</div>
+						</div>
 						<p class="control is-expanded">
+							<label class="label">Search query</label>
 							<input
 								class="input"
 								type="text"
-								v-model="editing.song.title"
-								autofocus
+								v-model="discogsQuery"
 							/>
 						</p>
-					</div>
-				</div>
-				<label class="label">Artists & Genres</label>
-				<div class="control is-horizontal">
-					<div class="control is-grouped artist-genres">
-						<div>
-							<p class="control has-addons">
-								<input
-									class="input"
-									id="new-artist"
-									type="text"
-									placeholder="Artist"
-								/>
-								<button
-									class="button is-info"
-									v-on:click="addTag('artists')"
-								>
-									Add Artist
-								</button>
-							</p>
-							<span
-								class="tag is-info"
-								v-for="(artist, index) in editing.song.artists"
+						<button
+							class="button is-info is-fullwidth"
+							v-on:click="searchDiscogs()"
+						>
+							Search
+						</button>
+						<label
+							class="label"
+							v-if="discogs.apiResults.length > 0"
+							>API results</label
+						>
+						<div
+							class="api-results-container"
+							v-if="discogs.apiResults.length > 0"
+						>
+							<div
+								class="api-result"
+								v-for="(result, index) in discogs.apiResults"
 								:key="index"
 							>
-								{{ artist }}
-								<button
-									class="delete is-info"
-									v-on:click="removeTag('artists', index)"
-								></button>
-							</span>
-						</div>
-						<div>
-							<p class="control has-addons">
-								<input
-									class="input"
-									id="new-genre"
-									type="text"
-									placeholder="Genre"
-								/>
-								<button
-									class="button is-info"
-									v-on:click="addTag('genres')"
+								<div class="top-container">
+									<img :src="result.album.albumArt" />
+									<div class="right-container">
+										<p class="album-title">
+											{{ result.album.title }}
+										</p>
+										<div class="bottom-row">
+											<img
+												src="/assets/arrow_up.svg"
+												v-if="result.expanded"
+												v-on:click="
+													toggleAPIResult(index)
+												"
+											/>
+											<img
+												src="/assets/arrow_down.svg"
+												v-if="!result.expanded"
+												v-on:click="
+													toggleAPIResult(index)
+												"
+											/>
+											<p class="type-year">
+												<span>{{
+													result.album.type
+												}}</span>
+												•
+												<span>{{
+													result.album.year
+												}}</span>
+											</p>
+										</div>
+									</div>
+								</div>
+								<div
+									class="bottom-container"
+									v-if="result.expanded"
 								>
-									Add Genre
-								</button>
-							</p>
-							<span
-								class="tag is-info"
-								v-for="(genre, index) in editing.song.genres"
-								:key="index"
-							>
-								{{ genre }}
-								<button
-									class="delete is-info"
-									v-on:click="removeTag('genres', index)"
-								></button>
-							</span>
+									<p class="bottom-container-field">
+										Artists:
+										<span>{{
+											result.album.artists.join(", ")
+										}}</span>
+									</p>
+									<p class="bottom-container-field">
+										Genres:
+										<span>{{
+											result.album.genres.join(", ")
+										}}</span>
+									</p>
+									<p class="bottom-container-field">
+										Data quality:
+										<span>{{ result.dataQuality }}</span>
+									</p>
+									<div class="tracks">
+										<div
+											class="track"
+											tabindex="0"
+											v-for="(track,
+											trackIndex) in result.tracks"
+											:key="trackIndex"
+											v-on:click="
+												selectTrack(index, trackIndex)
+											"
+										>
+											<span>{{ track.position }}.</span>
+											<p>{{ track.title }}</p>
+										</div>
+									</div>
+								</div>
+							</div>
 						</div>
 					</div>
 				</div>
-				<label class="label">Song Duration</label>
-				<p class="control">
-					<input
-						class="input"
-						type="text"
-						v-model="editing.song.duration"
-					/>
-				</p>
-				<label class="label">Skip Duration</label>
-				<p class="control">
-					<input
-						class="input"
-						type="text"
-						v-model="editing.song.skipDuration"
-					/>
-				</p>
-				<hr />
-				<h5 class="has-text-centered">Spotify Information</h5>
-				<label class="label">Song title</label>
-				<p class="control">
-					<input class="input" type="text" v-model="spotify.title" />
-				</p>
-				<label class="label">Song artist (1 artist full name)</label>
-				<p class="control">
-					<input class="input" type="text" v-model="spotify.artist" />
-				</p>
-				<button
-					class="button is-success"
-					v-on:click="getSpotifySongs()"
-				>
-					Get Spotify songs
-				</button>
-				<hr />
-				<article
-					class="media"
-					v-for="(song, index) in spotify.songs"
-					:key="index"
-				>
-					<figure class="media-left">
-						<p class="image is-64x64">
-							<img
-								:src="song.thumbnail"
-								onerror="this.src='/assets/notes-transparent.png'"
-							/>
-						</p>
-					</figure>
-					<div class="media-content">
-						<div class="content">
-							<p>
-								<strong>{{ song.title }}</strong>
-								<br />
-								<small>Artists: {{ song.artists }}</small
-								>, <small>Duration: {{ song.duration }}</small
-								>,
-								<small>Explicit: {{ song.explicit }}</small>
-								<br />
-								<small>Thumbnail: {{ song.thumbnail }}</small>
-							</p>
-						</div>
-					</div>
-				</article>
 			</div>
-			<div slot="footer">
+			<div slot="footer" class="footer-buttons">
 				<button
 					class="button is-success"
 					v-on:click="save(editing.song, false)"
@@ -272,15 +440,14 @@ export default {
 	components: { Modal },
 	data() {
 		return {
-			spotify: {
-				title: "",
-				artist: "",
-				songs: []
-			},
+			discogsQuery: "",
 			youtubeVideoDuration: 0.0,
 			youtubeVideoCurrentTime: 0.0,
 			youtubeVideoNote: "",
-			useHTTPS: false
+			useHTTPS: false,
+			discogs: {
+				apiResults: []
+			}
 		};
 	},
 	computed: {
@@ -294,7 +461,9 @@ export default {
 		})
 	},
 	methods: {
-		save(song, close) {
+		save(songToCopy, close) {
+			const song = JSON.parse(JSON.stringify(songToCopy));
+
 			if (!song.title)
 				return Toast.methods.addToast(
 					"Please fill in all fields",
@@ -363,7 +532,7 @@ export default {
 					error = "Genre must have between 1 and 16 characters.";
 					return error;
 				}
-				if (!validation.regex.az09_.test(genre)) {
+				if (!validation.regex.azAZ09_.test(genre)) {
 					error =
 						"Invalid genre format. Only ascii characters are allowed.";
 					return error;
@@ -386,7 +555,11 @@ export default {
 				);
 			}
 
-			if (!this.useHTTPS && song.thumbnail.indexOf("http://") !== 0) {
+			if (
+				!this.useHTTPS &&
+				(song.thumbnail.indexOf("http://") !== 0 &&
+					song.thumbnail.indexOf("https://") !== 0)
+			) {
 				return Toast.methods.addToast(
 					'Thumbnail must start with "http://".',
 					8000
@@ -418,6 +591,103 @@ export default {
 				}
 			);
 		},
+		toggleAPIResult(index) {
+			const apiResult = this.discogs.apiResults[index];
+			if (apiResult.expanded === true) apiResult.expanded = false;
+			else if (apiResult.gotMoreInfo === true) apiResult.expanded = true;
+			else {
+				fetch(apiResult.album.resourceUrl)
+					.then(response => {
+						return response.json();
+					})
+					.then(data => {
+						apiResult.album.artists = [];
+						apiResult.album.artistIds = [];
+						const artistRegex = new RegExp(" \\([0-9]\\)$");
+
+						apiResult.dataQuality = data.data_quality;
+						data.artists.forEach(artist => {
+							apiResult.album.artists.push(
+								artist.name.replace(artistRegex, "")
+							);
+							apiResult.album.artistIds.push(artist.id);
+						});
+						apiResult.tracks = data.tracklist.map(track => {
+							return {
+								position: track.position,
+								title: track.title
+							};
+						});
+						apiResult.expanded = true;
+						apiResult.gotMoreInfo = true;
+					});
+			}
+		},
+		getAlbumData(type) {
+			if (!this.editing.song.discogs) return;
+			if (type === "title")
+				this.updateSongField({
+					field: "title",
+					value: this.editing.song.discogs.track.title
+				});
+			if (type === "albumArt")
+				this.updateSongField({
+					field: "thumbnail",
+					value: this.editing.song.discogs.album.albumArt
+				});
+			if (type === "genres")
+				this.updateSongField({
+					field: "genres",
+					value: this.editing.song.discogs.album.genres
+				});
+			if (type === "artists")
+				this.updateSongField({
+					field: "artists",
+					value: this.editing.song.discogs.album.artists
+				});
+		},
+		searchDiscogs() {
+			const query = this.discogsQuery;
+
+			this.socket.emit("apis.searchDiscogs", query, res => {
+				if (res.status === "success") {
+					Toast.methods.addToast(
+						`Successfully searched. Got ${res.results.length} results.`,
+						4000
+					);
+					this.discogs.apiResults = res.results.map(result => {
+						const type =
+							result.type.charAt(0).toUpperCase() +
+							result.type.slice(1);
+
+						return {
+							expanded: false,
+							gotMoreInfo: false,
+							album: {
+								id: result.id,
+								title: result.title,
+								type,
+								year: result.year,
+								genres: result.genre,
+								albumArt: result.cover_image,
+								resourceUrl: result.resource_url
+							}
+						};
+					});
+				} else Toast.methods.addToast(res.message, 8000);
+			});
+		},
+		selectTrack(apiResultIndex, trackIndex) {
+			const apiResult = JSON.parse(
+				JSON.stringify(this.discogs.apiResults[apiResultIndex])
+			);
+			apiResult.track = apiResult.tracks[trackIndex];
+			delete apiResult.tracks;
+			delete apiResult.expanded;
+			delete apiResult.gotMoreInfo;
+
+			this.selectDiscogsInfo(apiResult);
+		},
 		settings(type) {
 			switch (type) {
 				default:
@@ -485,56 +755,6 @@ export default {
 			else if (type === "artists")
 				this.editing.song.artists.splice(index, 1);
 		},
-		getSpotifySongs() {
-			this.socket.emit(
-				"apis.getSpotifySongs",
-				this.spotify.title,
-				this.spotify.artist,
-				res => {
-					if (res.status === "success") {
-						Toast.methods.addToast(
-							`Succesfully got ${res.songs.length} song${
-								res.songs.length !== 1 ? "s" : ""
-							}.`,
-							3000
-						);
-						this.spotify.songs = res.songs;
-					} else
-						Toast.methods.addToast(
-							`Failed to get songs. ${res.message}`,
-							3000
-						);
-				}
-			);
-		},
-		initCanvas() {
-			const canvasElement = document.getElementById("durationCanvas");
-			const ctx = canvasElement.getContext("2d");
-
-			const skipDurationColor = "#ef4a1c";
-			const durationColor = "#1dc146";
-			const afterDurationColor = "#ef731a";
-
-			ctx.font = "16px Arial";
-
-			ctx.fillStyle = skipDurationColor;
-			ctx.fillRect(0, 25, 20, 15);
-
-			ctx.fillStyle = "#000000";
-			ctx.fillText("Skip duration", 25, 38);
-
-			ctx.fillStyle = durationColor;
-			ctx.fillRect(130, 25, 20, 15);
-
-			ctx.fillStyle = "#000000";
-			ctx.fillText("Duration", 155, 38);
-
-			ctx.fillStyle = afterDurationColor;
-			ctx.fillRect(230, 25, 20, 15);
-
-			ctx.fillStyle = "#000000";
-			ctx.fillText("After duration", 255, 38);
-		},
 		drawCanvas() {
 			const canvasElement = document.getElementById("durationCanvas");
 			const ctx = canvasElement.getContext("2d");
@@ -555,9 +775,9 @@ export default {
 
 			const widthCurrentTime = (currentTime / videoDuration) * width;
 
-			const skipDurationColor = "#ef4a1c";
-			const durationColor = "#1dc146";
-			const afterDurationColor = "#ef731a";
+			const skipDurationColor = "#F42003";
+			const durationColor = "#03A9F4";
+			const afterDurationColor = "#41E841";
 			const currentDurationColor = "#3b25e8";
 
 			ctx.fillStyle = skipDurationColor;
@@ -580,7 +800,9 @@ export default {
 			"loadVideoById",
 			"pauseVideo",
 			"getCurrentTime",
-			"editSong"
+			"editSong",
+			"updateSongField",
+			"selectDiscogsInfo"
 		]),
 		...mapActions("modals", ["closeModal"])
 	},
@@ -592,8 +814,6 @@ export default {
 		//   this.editing.song.skipDuration
 		// );
 
-		this.initCanvas();
-
 		lofig.get("cookie.secure", res => {
 			this.useHTTPS = res;
 		});
@@ -602,7 +822,7 @@ export default {
 			this.socket = socket;
 		});
 
-		setInterval(() => {
+		this.interval = setInterval(() => {
 			if (
 				this.video.paused === false &&
 				this.playerReady &&
@@ -614,18 +834,17 @@ export default {
 				this.video.player.stopVideo();
 			}
 			if (this.playerReady) {
-				this.getCurrentTime(3).then(time => {
-					this.youtubeVideoCurrentTime = time;
-					return time;
-				});
+				this.youtubeVideoCurrentTime = this.video.player
+					.getCurrentTime()
+					.toFixed(3);
 			}
 
 			if (this.video.paused === false) this.drawCanvas();
 		}, 200);
 
 		this.video.player = new window.YT.Player("player", {
-			height: 315,
-			width: 560,
+			height: 298,
+			width: 530,
 			videoId: this.editing.song.songId,
 			playerVars: {
 				controls: 0,
@@ -698,155 +917,477 @@ export default {
 		let volume = parseInt(localStorage.getItem("volume"));
 		document.getElementById("volumeSlider").value = volume =
 			typeof volume === "number" ? volume : 20;
+	},
+	beforeDestroy() {
+		this.playerReady = false;
+		clearInterval(this.interval);
 	}
 };
 </script>
 
+<style lang="scss">
+@import "styles/global.scss";
+
+.song-modal {
+	.modal-card-title {
+		text-align: center;
+		margin-left: 24px;
+	}
+
+	.modal-card {
+		width: 1160px;
+		height: 100%;
+
+		.modal-card-body {
+			padding: 16px;
+		}
+	}
+}
+</style>
+
 <style lang="scss" scoped>
 @import "styles/global.scss";
+.modal-card-body > div {
+	display: flex;
+	height: 100%;
+}
+
+.left-section {
+	display: flex;
+	flex-direction: column;
+	margin-right: 16px;
+
+	.top-section {
+		display: flex;
+
+		.player-section {
+			width: 530px;
+			display: flex;
+			flex-direction: column;
+
+			.player-footer {
+				background-color: #f4f4f4;
+				border: 1px rgba(163, 224, 255, 0.75) solid;
+				border-radius: 0px 0px 5px 5px;
+				display: flex;
+				justify-content: space-between;
+				height: 54px;
+
+				> * {
+					width: 33.3%;
+					display: flex;
+					align-items: center;
+				}
+
+				.player-footer-left {
+					flex: 1;
+
+					.material-icons {
+						font-size: 38px;
+						cursor: pointer;
+					}
+
+					.player-play-pause {
+						color: $musareBlue;
+					}
+
+					.player-stop {
+						color: $red;
+					}
+
+					.player-fast-forward {
+						color: $green;
+					}
+				}
+
+				.player-footer-center {
+					justify-content: center;
+					align-items: center;
+					flex: 2;
+					font-size: 21px;
+					font-weight: 400;
+
+					img {
+						height: 21px;
+						margin-right: 12px;
+						filter: invert(26%) sepia(54%) saturate(6317%)
+							hue-rotate(2deg) brightness(92%) contrast(115%);
+					}
+				}
+
+				.player-footer-right {
+					justify-content: right;
+					flex: 1;
+
+					#volumeSlider {
+						width: 126px;
+						margin-right: 10px;
+						background-color: #f4f4f4;
+					}
+				}
+			}
+		}
+
+		.thumbnail-preview {
+			width: 189px;
+			height: 189px;
+			margin-left: 16px;
+		}
+	}
+
+	.edit-section {
+		width: 735px;
+		background-color: #f4f4f4;
+		border: 1px rgba(163, 224, 255, 0.75) solid;
+		margin-top: 16px;
+		flex: 1;
+		overflow: auto;
+		border-radius: 5px;
+
+		.album-get-button {
+			background-color: $purple;
+			color: white;
+			width: 32px;
+			text-align: center;
+			border-width: 0;
+		}
+
+		.add-button {
+			background-color: $musareBlue !important;
+			width: 32px;
+
+			i {
+				font-size: 32px;
+			}
+		}
+
+		> div {
+			margin: 16px;
+		}
+
+		input {
+			width: 100%;
+		}
+
+		.title-container {
+			width: calc((100% - 32px) / 2);
+		}
+
+		.duration-container {
+			margin-right: 16px;
+			margin-left: 16px;
+			width: calc((100% - 32px) / 4);
+		}
+
+		.skip-duration-container {
+			width: calc((100% - 32px) / 4);
+		}
+
+		.album-art-container {
+			width: 100%;
+		}
+
+		.artists-container {
+			width: calc((100% - 32px) / 3);
+		}
+
+		.genres-container {
+			width: calc((100% - 32px) / 3);
+			margin-left: 16px;
+			margin-right: 16px;
+		}
+
+		.youtube-id-container {
+			width: calc((100% - 32px) / 3);
+		}
+
+		.list-item-circle {
+			background-color: $musareBlue;
+			width: 16px;
+			height: 16px;
+			border-radius: 8px;
+			cursor: pointer;
+			margin-right: 8px;
+			float: left;
+			-webkit-touch-callout: none;
+			-webkit-user-select: none;
+			-khtml-user-select: none;
+			-moz-user-select: none;
+			-ms-user-select: none;
+			user-select: none;
+
+			i {
+				color: $musareBlue;
+				font-size: 14px;
+				margin-left: 1px;
+			}
+		}
+
+		.list-item-circle:hover,
+		.list-item-circle:focus {
+			i {
+				color: white;
+			}
+		}
+
+		.list-item > p {
+			line-height: 16px;
+			word-wrap: break-word;
+			width: calc(100% - 24px);
+			left: 24px;
+			float: left;
+			margin-bottom: 8px;
+		}
+
+		.list-item:last-child > p {
+			margin-bottom: 0;
+		}
+	}
+}
+
+.right-section {
+	display: flex;
+	flex-wrap: wrap;
+
+	.api-section {
+		width: 376px;
+		background-color: #f4f4f4;
+		border: 1px rgba(163, 224, 255, 0.75) solid;
+		border-radius: 5px;
+		padding: 16px;
+		overflow: auto;
+		height: 100%;
+
+		> label {
+			margin-top: 12px;
+		}
+
+		.top-container {
+			display: flex;
+
+			img {
+				height: 85px;
+				width: 85px;
+			}
+
+			.right-container {
+				padding: 8px;
+				display: flex;
+				flex-direction: column;
+				flex: 1;
+
+				> p {
+					flex: 1;
+				}
+
+				.bottom-row {
+					display: flex;
+					flex-flow: row;
+					line-height: 15px;
+
+					img {
+						height: 15px;
+						align-self: end;
+						flex: 1;
+						user-select: none;
+						-moz-user-select: none;
+						-ms-user-select: none;
+						-webkit-user-select: none;
+						cursor: pointer;
+					}
+
+					p {
+						text-align: right;
+					}
+
+					.album-title {
+						flex: 1;
+						font-weight: 600;
+					}
+
+					.type-year {
+						font-size: 13px;
+						align-self: end;
+					}
+				}
+			}
+		}
+
+		.bottom-container {
+			padding: 12px;
+
+			.bottom-container-field {
+				line-height: 16px;
+				margin-bottom: 8px;
+				font-weight: 600;
+
+				span {
+					font-weight: 400;
+				}
+			}
+
+			.bottom-container-field:last-of-type {
+				margin-bottom: 0;
+			}
+		}
+
+		.selected-discogs-info {
+			background-color: white;
+			border: 1px solid $purple;
+			border-radius: 5px;
+			margin-bottom: 16px;
+
+			.selected-discogs-info-none {
+				font-size: 18px;
+				text-align: center;
+			}
+
+			.bottom-row > p {
+				flex: 1;
+			}
+		}
+
+		.api-result {
+			background-color: white;
+			border: 0.5px solid $musareBlue;
+			border-radius: 5px;
+			margin-bottom: 16px;
+		}
+
+		button {
+			background-color: $musareBlue !important;
+		}
+
+		.tracks {
+			margin-top: 12px;
+
+			.track:first-child {
+				margin-top: 0;
+				border-radius: 3px 3px 0 0;
+			}
+
+			.track:last-child {
+				border-radius: 0 0 3px 3px;
+			}
+
+			.track {
+				border: 0.5px solid black;
+				margin-top: -1px;
+				line-height: 16px;
+				display: flex;
+				cursor: pointer;
+
+				span {
+					font-weight: 600;
+					display: inline-block;
+					margin-top: 7px;
+					margin-bottom: 7px;
+					margin-left: 7px;
+				}
+
+				p {
+					display: inline-block;
+					margin: 7px;
+					flex: 1;
+				}
+			}
+
+			.track:hover,
+			.track:focus {
+				background-color: #f4f4f4;
+			}
+		}
+	}
+}
+
+.footer-buttons {
+	margin-left: auto;
+	margin-right: auto;
+}
 
 input[type="range"] {
 	-webkit-appearance: none;
 	width: 100%;
-	margin: 7.3px 0;
+	margin: 8.5px 0;
 }
-
 input[type="range"]:focus {
 	outline: none;
 }
-
 input[type="range"]::-webkit-slider-runnable-track {
 	width: 100%;
-	height: 5.2px;
+	height: 3px;
 	cursor: pointer;
-	box-shadow: 0;
-	background: $light-grey-2;
-	border-radius: 0;
-	border: 0;
+	box-shadow: none;
+	background: #7e7e7e;
+	border-radius: none;
+	border: none;
 }
-
 input[type="range"]::-webkit-slider-thumb {
-	box-shadow: 0;
-	border: 0;
-	height: 19px;
-	width: 19px;
-	border-radius: 15px;
-	background: $primary-color;
+	box-shadow: none;
+	border: none;
+	height: 20px;
+	width: 20px;
+	border-radius: 100px;
+	background: #03a9f4;
 	cursor: pointer;
 	-webkit-appearance: none;
-	margin-top: -6.5px;
+	margin-top: -8.5px;
+}
+input[type="range"]:focus::-webkit-slider-runnable-track {
+	background: #7e7e7e;
 }
-
 input[type="range"]::-moz-range-track {
 	width: 100%;
-	height: 5.2px;
+	height: 3px;
 	cursor: pointer;
-	box-shadow: 0;
-	background: $light-grey-2;
-	border-radius: 0;
-	border: 0;
+	box-shadow: none;
+	background: #7e7e7e;
+	border-radius: none;
+	border: none;
 }
-
 input[type="range"]::-moz-range-thumb {
-	box-shadow: 0;
-	border: 0;
-	height: 19px;
-	width: 19px;
-	border-radius: 15px;
-	background: $primary-color;
+	box-shadow: none;
+	border: none;
+	height: 20px;
+	width: 20px;
+	border-radius: 100px;
+	background: #03a9f4;
 	cursor: pointer;
-	-webkit-appearance: none;
-	margin-top: -6.5px;
 }
-
 input[type="range"]::-ms-track {
 	width: 100%;
-	height: 5.2px;
+	height: 3px;
 	cursor: pointer;
-	box-shadow: 0;
-	background: $light-grey-2;
-	border-radius: 1.3px;
+	background: transparent;
+	border-color: transparent;
+	color: transparent;
 }
-
 input[type="range"]::-ms-fill-lower {
-	background: $light-grey-2;
-	border: 0;
-	border-radius: 0;
-	box-shadow: 0;
+	background: #717171;
+	border: none;
+	border-radius: none;
+	box-shadow: none;
 }
-
 input[type="range"]::-ms-fill-upper {
-	background: $light-grey-2;
-	border: 0;
-	border-radius: 0;
-	box-shadow: 0;
+	background: #7e7e7e;
+	border: none;
+	border-radius: none;
+	box-shadow: none;
 }
-
 input[type="range"]::-ms-thumb {
-	box-shadow: 0;
-	border: 0;
-	height: 15px;
-	width: 15px;
-	border-radius: 15px;
-	background: $primary-color;
+	box-shadow: none;
+	border: none;
+	height: 20px;
+	width: 20px;
+	border-radius: 100px;
+	background: #03a9f4;
 	cursor: pointer;
-	-webkit-appearance: none;
-	margin-top: 1.5px;
+	height: 3px;
 }
-
-.controls {
-	display: flex;
-	flex-direction: column;
-	align-items: center;
-}
-
-.artist-genres {
-	display: flex;
-	justify-content: space-between;
-}
-
-#volumeSlider {
-	margin-bottom: 15px;
-}
-
-.has-text-centered {
-	padding: 10px;
-}
-
-.thumbnail-preview {
-	display: flex;
-	margin: 0 auto 25px auto;
-	max-width: 200px;
-	width: 100%;
+input[type="range"]:focus::-ms-fill-lower {
+	background: #7e7e7e;
 }
-
-.modal-card-body,
-.modal-card-foot {
-	border-top: 0;
-}
-
-.label,
-.checkbox,
-h5 {
-	font-weight: normal;
-}
-
-.video-container {
-	display: flex;
-	flex-direction: column;
-	align-items: center;
-	padding: 10px;
-
-	iframe {
-		pointer-events: none;
-	}
-}
-
-.save-changes {
-	color: $white;
-}
-
-.tag:not(:last-child) {
-	margin-right: 5px;
+input[type="range"]:focus::-ms-fill-upper {
+	background: #7e7e7e;
 }
 </style>

+ 3 - 0
frontend/dist/assets/arrow_down.svg

@@ -0,0 +1,3 @@
+<svg width="46" height="10" viewBox="0 0 46 10" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M1 1L23 9L45 1" stroke="#333333" stroke-linejoin="round"/>
+</svg>

+ 3 - 0
frontend/dist/assets/arrow_up.svg

@@ -0,0 +1,3 @@
+<svg width="46" height="10" viewBox="0 0 46 10" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M45 9L23 1L1 9" stroke="#333333" stroke-width="1.5"/>
+</svg>

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

@@ -0,0 +1 @@
+<svg aria-hidden="true" focusable="false" data-prefix="fab" data-icon="youtube" class="svg-inline--fa fa-youtube fa-w-18" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path fill="currentColor" d="M549.655 124.083c-6.281-23.65-24.787-42.276-48.284-48.597C458.781 64 288 64 288 64S117.22 64 74.629 75.486c-23.497 6.322-42.003 24.947-48.284 48.597-11.412 42.867-11.412 132.305-11.412 132.305s0 89.438 11.412 132.305c6.281 23.65 24.787 41.5 48.284 47.821C117.22 448 288 448 288 448s170.78 0 213.371-11.486c23.497-6.321 42.003-24.171 48.284-47.821 11.412-42.867 11.412-132.305 11.412-132.305s0-89.438-11.412-132.305zm-317.51 213.508V175.185l142.739 81.205-142.739 81.201z"></path></svg>

+ 1 - 1
frontend/package.json

@@ -12,7 +12,7 @@
     "bundle-analyse": "webpack --config webpack.prod.js --profile --json > bundle-stats.json && npx webpack-bundle-analyzer bundle-stats.json --mode static --report bundle-report.html --no-open",
     "dev": "webpack-dev-server --config webpack.dev.js",
     "prod": "webpack --config webpack.prod.js",
-    "test": "snyk test"
+    "test": ""
   },
   "devDependencies": {
     "@babel/core": "^7.5.4",

+ 12 - 1
frontend/store/modules/admin.js

@@ -35,10 +35,15 @@ const modules = {
 			addSong: ({ commit }, song) => commit("addSong", song),
 			removeSong: ({ commit }, songId) => commit("removeSong", songId),
 			updateSong: ({ commit }, updatedSong) =>
-				commit("updateSong", updatedSong)
+				commit("updateSong", updatedSong),
+			updateSongField: ({ commit }, data) =>
+				commit("updateSongField", data),
+			selectDiscogsInfo: ({ commit }, discogsInfo) =>
+				commit("selectDiscogsInfo", discogsInfo)
 		},
 		mutations: {
 			editSong(state, song) {
+				if (song.song.discogs === undefined) song.song.discogs = null;
 				state.editing = { ...song };
 			},
 			stopVideo(state) {
@@ -81,6 +86,12 @@ const modules = {
 					if (song._id === updatedSong._id)
 						state.songs.$set(index, updatedSong);
 				});
+			},
+			updateSongField(state, data) {
+				state.editing.song[data.field] = data.value;
+			},
+			selectDiscogsInfo(state, discogsInfo) {
+				state.editing.song.discogs = discogsInfo;
 			}
 		}
 	},