Ver código fonte

refactor: Continued work on Youtube video modal and other tweaks

Owen Diffey 2 anos atrás
pai
commit
371f954c5b

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

@@ -326,8 +326,8 @@ export default {
 	 *
 	 * @returns {{status: string, data: object}}
 	 */
-	getVideo: isAdminRequired(function getVideo(session, identifier, cb) {
-		YouTubeModule.runJob("GET_VIDEO", { identifier }, this)
+	getVideo: isAdminRequired(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.`);
 

+ 1 - 0
backend/logic/songs.js

@@ -1263,6 +1263,7 @@ class _SongsModule extends CoreClass {
 							.then(response => next(null, user, response.song))
 							.catch(next);
 					},
+
 					(user, youtubeVideo, next) =>
 						YouTubeModule.runJob("CREATE_VIDEOS", { youtubeVideos: youtubeVideo }, this)
 							.then(() => {

+ 19 - 0
backend/logic/youtube.js

@@ -1065,6 +1065,7 @@ class _YouTubeModule extends CoreClass {
 	 *
 	 * @param {object} payload - an object containing the payload
 	 * @param {string} payload.identifier - the youtube video ObjectId or YouTube ID
+	 * @param {string} payload.createMissing - attempt to fetch and create video if not in db
 	 * @returns {Promise} - returns a promise (resolve, reject)
 	 */
 	 GET_VIDEO(payload) {
@@ -1077,6 +1078,24 @@ class _YouTubeModule extends CoreClass {
 							{ youtubeId: payload.identifier };
 
 						return YouTubeModule.youtubeVideoModel.findOne(query, next);
+					},
+
+					(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))
+							.catch(next);
+					},
+
+					(video, youtubeVideo, next) => {
+						if (video) return next(null, video);
+						return YouTubeModule.runJob("CREATE_VIDEOS", { youtubeVideos: youtubeVideo }, this)
+							.then(res => {
+								if (res.youtubeVideos.length === 1) next(null, res.youtubeVideos[0])
+								else next("YouTube video not found.")
+							})
+							.catch(next);
 					}
 				],
 				(err, video) => {

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

@@ -79,15 +79,21 @@
 
 					<template #content>
 						<div class="icons-group">
-							<a
+							<i
 								v-if="disabledActions.indexOf('youtube') === -1"
-								target="_blank"
-								:href="`https://www.youtube.com/watch?v=${song.youtubeId}`"
-								content="View on Youtube"
+								@click="
+									openModal({
+										modal: 'viewYoutubeVideo',
+										data: {
+											videoId: song.youtubeId
+										}
+									})
+								"
+								content="View YouTube Video"
 								v-tippy
 							>
 								<div class="youtube-icon"></div>
-							</a>
+							</i>
 							<i
 								v-if="disabledActions.indexOf('report') === -1"
 								class="material-icons report-icon"

+ 127 - 117
frontend/src/components/modals/EditSong/index.vue

@@ -41,15 +41,15 @@
 				>
 					<div class="top-section">
 						<div class="player-section">
-							<div id="editSongPlayer" />
+							<div :id="`editSongPlayer-${modalUuid}`" />
 
 							<div v-show="youtubeError" class="player-error">
 								<h2>{{ youtubeErrorMessage }}</h2>
 							</div>
 
 							<canvas
-								ref="durationCanvas"
-								id="durationCanvas"
+								:ref="`durationCanvas-${modalUuid}`"
+								class="duration-canvas"
 								v-show="!youtubeError"
 								height="20"
 								width="530"
@@ -1177,133 +1177,142 @@ export default {
 			}, 200);
 
 			if (window.YT && window.YT.Player) {
-				this.video.player = new window.YT.Player("editSongPlayer", {
-					height: 298,
-					width: 530,
-					videoId: null,
-					host: "https://www.youtube-nocookie.com",
-					playerVars: {
-						controls: 0,
-						iv_load_policy: 3,
-						rel: 0,
-						showinfo: 0,
-						autoplay: 0
-					},
-					startSeconds: this.song.skipDuration,
-					events: {
-						onReady: () => {
-							let volume = parseFloat(
-								localStorage.getItem("volume")
-							);
-							volume = typeof volume === "number" ? volume : 20;
-							this.video.player.setVolume(volume);
-							if (volume > 0) this.video.player.unMute();
-
-							this.playerReady = true;
-
-							if (this.song && this.song._id)
-								this.video.player.cueVideoById(
-									this.song.youtubeId,
-									this.song.skipDuration
+				this.video.player = new window.YT.Player(
+					`editSongPlayer-${this.modalUuid}`,
+					{
+						height: 298,
+						width: 530,
+						videoId: null,
+						host: "https://www.youtube-nocookie.com",
+						playerVars: {
+							controls: 0,
+							iv_load_policy: 3,
+							rel: 0,
+							showinfo: 0,
+							autoplay: 0
+						},
+						startSeconds: this.song.skipDuration,
+						events: {
+							onReady: () => {
+								let volume = parseFloat(
+									localStorage.getItem("volume")
 								);
+								volume =
+									typeof volume === "number" ? volume : 20;
+								this.video.player.setVolume(volume);
+								if (volume > 0) this.video.player.unMute();
 
-							this.setPlaybackRate(null);
+								this.playerReady = true;
 
-							this.drawCanvas();
-						},
-						onStateChange: event => {
-							this.drawCanvas();
-
-							if (event.data === 1) {
-								this.video.paused = false;
-								let youtubeDuration =
-									this.video.player.getDuration();
-								const newYoutubeVideoDuration =
-									youtubeDuration.toFixed(3);
-
-								if (
-									this.youtubeVideoDuration.indexOf(
-										".000"
-									) !== -1 &&
-									`${this.youtubeVideoDuration}` !==
-										`${newYoutubeVideoDuration}`
-								) {
-									const songDurationNumber = Number(
-										this.song.duration
+								if (this.song && this.song._id)
+									this.video.player.cueVideoById(
+										this.song.youtubeId,
+										this.song.skipDuration
 									);
-									const songDurationNumber2 =
-										Number(this.song.duration) + 1;
-									const songDurationNumber3 =
-										Number(this.song.duration) - 1;
-									const fixedSongDuration =
-										songDurationNumber.toFixed(3);
-									const fixedSongDuration2 =
-										songDurationNumber2.toFixed(3);
-									const fixedSongDuration3 =
-										songDurationNumber3.toFixed(3);
 
-									if (
-										`${this.youtubeVideoDuration}` ===
-											`${Number(
-												this.song.duration
-											).toFixed(3)}` &&
-										(fixedSongDuration ===
-											this.youtubeVideoDuration ||
-											fixedSongDuration2 ===
-												this.youtubeVideoDuration ||
-											fixedSongDuration3 ===
-												this.youtubeVideoDuration)
-									)
-										this.song.duration =
-											newYoutubeVideoDuration;
+								this.setPlaybackRate(null);
+
+								this.drawCanvas();
+							},
+							onStateChange: event => {
+								this.drawCanvas();
+
+								if (event.data === 1) {
+									this.video.paused = false;
+									let youtubeDuration =
+										this.video.player.getDuration();
+									const newYoutubeVideoDuration =
+										youtubeDuration.toFixed(3);
 
-									this.youtubeVideoDuration =
-										newYoutubeVideoDuration;
 									if (
 										this.youtubeVideoDuration.indexOf(
 											".000"
-										) !== -1
-									)
-										this.youtubeVideoNote = "(~)";
-									else this.youtubeVideoNote = "";
-								}
-
-								if (this.song.duration === -1)
-									this.song.duration =
-										this.youtubeVideoDuration;
+										) !== -1 &&
+										`${this.youtubeVideoDuration}` !==
+											`${newYoutubeVideoDuration}`
+									) {
+										const songDurationNumber = Number(
+											this.song.duration
+										);
+										const songDurationNumber2 =
+											Number(this.song.duration) + 1;
+										const songDurationNumber3 =
+											Number(this.song.duration) - 1;
+										const fixedSongDuration =
+											songDurationNumber.toFixed(3);
+										const fixedSongDuration2 =
+											songDurationNumber2.toFixed(3);
+										const fixedSongDuration3 =
+											songDurationNumber3.toFixed(3);
+
+										if (
+											`${this.youtubeVideoDuration}` ===
+												`${Number(
+													this.song.duration
+												).toFixed(3)}` &&
+											(fixedSongDuration ===
+												this.youtubeVideoDuration ||
+												fixedSongDuration2 ===
+													this.youtubeVideoDuration ||
+												fixedSongDuration3 ===
+													this.youtubeVideoDuration)
+										)
+											this.song.duration =
+												newYoutubeVideoDuration;
+
+										this.youtubeVideoDuration =
+											newYoutubeVideoDuration;
+										if (
+											this.youtubeVideoDuration.indexOf(
+												".000"
+											) !== -1
+										)
+											this.youtubeVideoNote = "(~)";
+										else this.youtubeVideoNote = "";
+									}
+
+									if (this.song.duration === -1)
+										this.song.duration =
+											this.youtubeVideoDuration;
 
-								youtubeDuration -= this.song.skipDuration;
-								if (this.song.duration > youtubeDuration + 1) {
-									this.stopVideo();
-									this.pauseVideo(true);
-									return new Toast(
-										"Video can't play. Specified duration is bigger than the YouTube song duration."
-									);
-								}
-								if (this.song.duration <= 0) {
-									this.stopVideo();
-									this.pauseVideo(true);
-									return new Toast(
-										"Video can't play. Specified duration has to be more than 0 seconds."
-									);
-								}
+									youtubeDuration -= this.song.skipDuration;
+									if (
+										this.song.duration >
+										youtubeDuration + 1
+									) {
+										this.stopVideo();
+										this.pauseVideo(true);
+										return new Toast(
+											"Video can't play. Specified duration is bigger than the YouTube song duration."
+										);
+									}
+									if (this.song.duration <= 0) {
+										this.stopVideo();
+										this.pauseVideo(true);
+										return new Toast(
+											"Video can't play. Specified duration has to be more than 0 seconds."
+										);
+									}
 
-								if (
-									this.video.player.getCurrentTime() <
-									this.song.skipDuration
-								) {
-									return this.seekTo(this.song.skipDuration);
+									if (
+										this.video.player.getCurrentTime() <
+										this.song.skipDuration
+									) {
+										return this.seekTo(
+											this.song.skipDuration
+										);
+									}
+
+									this.setPlaybackRate(null);
+								} else if (event.data === 2) {
+									this.video.paused = true;
 								}
 
-								this.setPlaybackRate(null);
-							} else if (event.data === 2) {
-								this.video.paused = true;
+								return false;
 							}
-
-							return false;
 						}
 					}
-				});
+				);
 			} else {
 				this.youtubeError = true;
 				this.youtubeErrorMessage = "Player could not be loaded.";
@@ -1797,7 +1806,8 @@ export default {
 		},
 		drawCanvas() {
 			if (!this.songDataLoaded) return;
-			const canvasElement = this.$refs.durationCanvas;
+			const canvasElement =
+				this.$refs[`durationCanvas-${this.modalUuid}`];
 			const ctx = canvasElement.getContext("2d");
 
 			const videoDuration = Number(this.youtubeVideoDuration);
@@ -2012,7 +2022,7 @@ export default {
 		}
 	}
 
-	#durationCanvas {
+	.duration-canvas {
 		background-color: var(--dark-grey-2) !important;
 	}
 }
@@ -2048,7 +2058,7 @@ export default {
 			border-radius: @border-radius;
 			overflow: hidden;
 
-			#durationCanvas {
+			.duration-canvas {
 				background-color: var(--light-grey-2);
 			}
 

+ 586 - 269
frontend/src/components/modals/ViewYoutubeVideo.vue

@@ -1,187 +1,213 @@
 <template>
 	<modal title="View YouTube Video">
 		<template #body>
-			<div v-if="!loaded" class="vertical-padding">
-				<p>Video hasn't loaded yet</p>
+			<div v-if="loaded" class="top-section">
+				<div class="left-section">
+					<p>
+						<strong>ID:</strong>
+						<span :title="video._id">{{ video._id }}</span>
+					</p>
+					<p>
+						<strong>YouTube ID:</strong>
+						<a
+							:href="
+								'https://www.youtube.com/watch?v=' +
+								`${video.youtubeId}`
+							"
+							target="_blank"
+						>
+							{{ video.youtubeId }}
+						</a>
+					</p>
+					<p>
+						<strong>Title:</strong>
+						<span :title="video.title">{{ video.title }}</span>
+					</p>
+					<p>
+						<strong>Author:</strong>
+						<span :title="video.author">{{ video.author }}</span>
+					</p>
+					<p>
+						<strong>Duration:</strong>
+						<span :title="video.duration">{{
+							video.duration
+						}}</span>
+					</p>
+				</div>
+				<div class="right-section">
+					<song-thumbnail :song="video" class="thumbnail-preview" />
+				</div>
 			</div>
-			<div v-else class="vertical-padding">
-				<div class="player-section">
-					<div id="viewYoutubeVideoPlayer" />
 
-					<div v-show="player.error" class="player-error">
-						<h2>{{ player.errorMessage }}</h2>
-					</div>
+			<div v-show="loaded" class="player-section">
+				<div class="player-container">
+					<div :id="`viewYoutubeVideoPlayer-${modalUuid}`" />
+				</div>
 
-					<canvas
-						ref="videoDurationCanvas"
-						id="videoDurationCanvas"
-						v-show="!player.error"
-						height="20"
-						width="530"
-						@click="setTrackPosition($event)"
-					/>
-					<div class="player-footer">
-						<div class="player-footer-left">
-							<button
-								class="button is-primary"
-								@click="play()"
-								@keyup.enter="play()"
-								v-if="player.paused"
-								content="Resume Playback"
-								v-tippy
-							>
-								<i class="material-icons">play_arrow</i>
-							</button>
-							<button
-								class="button is-primary"
-								@click="settings('pause')"
-								@keyup.enter="settings('pause')"
-								v-else
-								content="Pause Playback"
-								v-tippy
-							>
-								<i class="material-icons">pause</i>
-							</button>
-							<button
-								class="button is-danger"
-								@click.exact="settings('stop')"
-								@click.shift="settings('hardStop')"
-								@keyup.enter.exact="settings('stop')"
-								@keyup.shift.enter="settings('hardStop')"
-								content="Stop Playback"
+				<div v-show="player.error" class="player-error">
+					<h2>{{ player.errorMessage }}</h2>
+				</div>
+
+				<canvas
+					:ref="`durationCanvas-${modalUuid}`"
+					class="duration-canvas"
+					v-show="!player.error"
+					height="20"
+					:width="530"
+					@click="setTrackPosition($event)"
+				/>
+				<div class="player-footer">
+					<div class="player-footer-left">
+						<button
+							class="button is-primary"
+							@click="play()"
+							@keyup.enter="play()"
+							v-if="player.paused"
+							content="Resume Playback"
+							v-tippy
+						>
+							<i class="material-icons">play_arrow</i>
+						</button>
+						<button
+							class="button is-primary"
+							@click="settings('pause')"
+							@keyup.enter="settings('pause')"
+							v-else
+							content="Pause Playback"
+							v-tippy
+						>
+							<i class="material-icons">pause</i>
+						</button>
+						<button
+							class="button is-danger"
+							@click.exact="settings('stop')"
+							@click.shift="settings('hardStop')"
+							@keyup.enter.exact="settings('stop')"
+							@keyup.shift.enter="settings('hardStop')"
+							content="Stop Playback"
+							v-tippy
+						>
+							<i class="material-icons">stop</i>
+						</button>
+						<tippy
+							class="playerRateDropdown"
+							:touch="true"
+							:interactive="true"
+							placement="bottom"
+							theme="dropdown"
+							ref="dropdown"
+							trigger="click"
+							append-to="parent"
+							@show="
+								() => {
+									player.showRateDropdown = true;
+								}
+							"
+							@hide="
+								() => {
+									player.showRateDropdown = false;
+								}
+							"
+						>
+							<div
+								ref="trigger"
+								class="control has-addons"
+								content="Set Playback Rate"
 								v-tippy
 							>
-								<i class="material-icons">stop</i>
-							</button>
-							<tippy
-								class="playerRateDropdown"
-								:touch="true"
-								:interactive="true"
-								placement="bottom"
-								theme="dropdown"
-								ref="dropdown"
-								trigger="click"
-								append-to="parent"
-								@show="
-									() => {
-										player.showRateDropdown = true;
-									}
-								"
-								@hide="
-									() => {
-										player.showRateDropdown = false;
-									}
-								"
-							>
-								<div
-									ref="trigger"
-									class="control has-addons"
-									content="Set Playback Rate"
-									v-tippy
-								>
-									<button class="button is-primary">
-										<i class="material-icons"
-											>fast_forward</i
-										>
+								<button class="button is-primary">
+									<i class="material-icons">fast_forward</i>
+								</button>
+								<button class="button dropdown-toggle">
+									<i class="material-icons">
+										{{
+											player.showRateDropdown
+												? "expand_more"
+												: "expand_less"
+										}}
+									</i>
+								</button>
+							</div>
+
+							<template #content>
+								<div class="nav-dropdown-items">
+									<button
+										class="nav-item button"
+										:class="{
+											active: player.playbackRate === 0.5
+										}"
+										title="0.5x"
+										@click="setPlaybackRate(0.5)"
+									>
+										<p>0.5x</p>
 									</button>
-									<button class="button dropdown-toggle">
-										<i class="material-icons">
-											{{
-												player.showRateDropdown
-													? "expand_more"
-													: "expand_less"
-											}}
-										</i>
+									<button
+										class="nav-item button"
+										:class="{
+											active: player.playbackRate === 1
+										}"
+										title="1x"
+										@click="setPlaybackRate(1)"
+									>
+										<p>1x</p>
+									</button>
+									<button
+										class="nav-item button"
+										:class="{
+											active: player.playbackRate === 2
+										}"
+										title="2x"
+										@click="setPlaybackRate(2)"
+									>
+										<p>2x</p>
 									</button>
 								</div>
-
-								<template #content>
-									<div class="nav-dropdown-items">
-										<button
-											class="nav-item button"
-											:class="{
-												active:
-													player.playbackRate === 0.5
-											}"
-											title="0.5x"
-											@click="setPlaybackRate(0.5)"
-										>
-											<p>0.5x</p>
-										</button>
-										<button
-											class="nav-item button"
-											:class="{
-												active:
-													player.playbackRate === 1
-											}"
-											title="1x"
-											@click="setPlaybackRate(1)"
-										>
-											<p>1x</p>
-										</button>
-										<button
-											class="nav-item button"
-											:class="{
-												active:
-													player.playbackRate === 2
-											}"
-											title="2x"
-											@click="setPlaybackRate(2)"
-										>
-											<p>2x</p>
-										</button>
-									</div>
-								</template>
-							</tippy>
-						</div>
-						<div class="player-footer-center">
+							</template>
+						</tippy>
+					</div>
+					<div class="player-footer-center">
+						<span>
 							<span>
-								<span>
-									{{ player.currentTime }}
-								</span>
-								/
-								<span>
-									{{ player.duration }}
-									{{ player.videoNote }}
-								</span>
+								{{ player.currentTime }}
 							</span>
-						</div>
-						<div class="player-footer-right">
-							<p id="volume-control">
-								<i
-									class="material-icons"
-									@click="toggleMute()"
-									:content="`${
-										player.muted ? 'Unmute' : 'Mute'
-									}`"
-									v-tippy
-									>{{
-										player.muted
-											? "volume_mute"
-											: player.volume >= 50
-											? "volume_up"
-											: "volume_down"
-									}}</i
-								>
-								<input
-									v-model="player.volume"
-									type="range"
-									min="0"
-									max="100"
-									class="volume-slider active"
-									@change="changeVolume()"
-									@input="changeVolume()"
-								/>
-							</p>
-						</div>
+							/
+							<span>
+								{{ player.duration }}
+								{{ player.videoNote }}
+							</span>
+						</span>
+					</div>
+					<div class="player-footer-right">
+						<p id="volume-control">
+							<i
+								class="material-icons"
+								@click="toggleMute()"
+								:content="`${player.muted ? 'Unmute' : 'Mute'}`"
+								v-tippy
+								>{{
+									player.muted
+										? "volume_mute"
+										: player.volume >= 50
+										? "volume_up"
+										: "volume_down"
+								}}</i
+							>
+							<input
+								v-model="player.volume"
+								type="range"
+								min="0"
+								max="100"
+								class="volume-slider active"
+								@change="changeVolume()"
+								@input="changeVolume()"
+							/>
+						</p>
 					</div>
 				</div>
-				<p><strong>ID:</strong> {{ video._id }}</p>
-				<p><strong>YouTube ID:</strong> {{ video.youtubeId }}</p>
-				<p><strong>Title:</strong> {{ video.title }}</p>
-				<p><strong>Author:</strong> {{ video.author }}</p>
-				<p><strong>Duration:</strong> {{ video.duration }}</p>
-				<song-thumbnail :song="video" />
+			</div>
+
+			<div v-if="!loaded" class="vertical-padding">
+				<p>Video hasn't loaded yet</p>
 			</div>
 		</template>
 		<template #footer>
@@ -234,6 +260,15 @@ export default {
 		ws.onConnect(this.init);
 	},
 	beforeUnmount() {
+		this.stopVideo();
+		this.pauseVideo(true);
+		this.player.duration = "0.000";
+		this.player.currentTime = 0;
+		this.player.playerReady = false;
+		this.player.videoNote = "";
+		clearInterval(this.interval);
+		this.loaded = false;
+
 		this.socket.dispatch(
 			"apis.leaveRoom",
 			`view-youtube-video.${this.videoId}`,
@@ -250,76 +285,82 @@ export default {
 	methods: {
 		init() {
 			this.loaded = false;
-			this.socket.dispatch("youtube.getVideo", this.videoId, res => {
-				if (res.status === "success") {
-					const youtubeVideo = res.data;
-					this.viewYoutubeVideo(youtubeVideo);
-					this.loaded = true;
-
-					this.interval = setInterval(() => {
-						if (
-							this.video.duration !== -1 &&
-							this.player.paused === false &&
-							this.player.playerReady &&
-							(this.player.player.getCurrentTime() >
-								this.video.duration ||
-								(this.player.player.getCurrentTime() > 0 &&
-									this.player.player.getCurrentTime() >=
-										this.player.player.getDuration()))
-						) {
-							this.stopVideo();
-							this.pauseVideo(true);
-							this.drawCanvas();
-						}
-						if (
-							this.player.playerReady &&
-							this.player.player.getVideoData &&
-							this.player.player.getVideoData() &&
-							this.player.player.getVideoData().video_id ===
-								this.video.youtubeId
-						) {
-							const currentTime =
-								this.player.player.getCurrentTime();
-
-							if (currentTime !== undefined)
-								this.player.currentTime =
-									currentTime.toFixed(3);
-
-							if (this.player.duration.indexOf(".000") !== -1) {
-								const duration =
-									this.player.player.getDuration();
-
-								if (duration !== undefined) {
-									if (
-										`${this.player.duration}` ===
-										`${Number(this.video.duration).toFixed(
-											3
-										)}`
-									)
-										this.video.duration =
+			this.socket.dispatch(
+				"youtube.getVideo",
+				this.videoId,
+				true,
+				res => {
+					if (res.status === "success") {
+						const youtubeVideo = res.data;
+						this.viewYoutubeVideo(youtubeVideo);
+						this.loaded = true;
+
+						this.interval = setInterval(() => {
+							if (
+								this.video.duration !== -1 &&
+								this.player.paused === false &&
+								this.player.playerReady &&
+								(this.player.player.getCurrentTime() >
+									this.video.duration ||
+									(this.player.player.getCurrentTime() > 0 &&
+										this.player.player.getCurrentTime() >=
+											this.player.player.getDuration()))
+							) {
+								this.stopVideo();
+								this.pauseVideo(true);
+								this.drawCanvas();
+							}
+							if (
+								this.player.playerReady &&
+								this.player.player.getVideoData &&
+								this.player.player.getVideoData() &&
+								this.player.player.getVideoData().video_id ===
+									this.video.youtubeId
+							) {
+								const currentTime =
+									this.player.player.getCurrentTime();
+
+								if (currentTime !== undefined)
+									this.player.currentTime =
+										currentTime.toFixed(3);
+
+								if (
+									this.player.duration.indexOf(".000") !== -1
+								) {
+									const duration =
+										this.player.player.getDuration();
+
+									if (duration !== undefined) {
+										if (
+											`${this.player.duration}` ===
+											`${Number(
+												this.video.duration
+											).toFixed(3)}`
+										)
+											this.video.duration =
+												duration.toFixed(3);
+
+										this.player.duration =
 											duration.toFixed(3);
-
-									this.player.duration = duration.toFixed(3);
-									if (
-										this.player.duration.indexOf(".000") !==
-										-1
-									)
-										this.player.videoNote = "(~)";
-									else this.player.videoNote = "";
-
-									this.drawCanvas();
+										if (
+											this.player.duration.indexOf(
+												".000"
+											) !== -1
+										)
+											this.player.videoNote = "(~)";
+										else this.player.videoNote = "";
+
+										this.drawCanvas();
+									}
 								}
 							}
-						}
 
-						if (this.player.paused === false) this.drawCanvas();
-					}, 200);
+							if (this.player.paused === false) this.drawCanvas();
+						}, 200);
 
-					if (window.YT && window.YT.Player) {
-						console.log(111);
-						this.updatePlayer({
-							player: new window.YT.Player(
-								"viewYoutubeVideoPlayer",
+						if (window.YT && window.YT.Player) {
+							this.player.player = new window.YT.Player(
+								`viewYoutubeVideoPlayer-${this.modalUuid}`,
 								{
 									height: 298,
 									width: 530,
@@ -334,7 +375,6 @@ export default {
 									},
 									events: {
 										onReady: () => {
-											console.log(222);
 											let volume = parseFloat(
 												localStorage.getItem("volume")
 											);
@@ -465,42 +505,41 @@ export default {
 										}
 									}
 								}
-							)
-						});
+							);
+						} else {
+							this.updatePlayer({
+								error: true,
+								errorMessage: "Player could not be loaded."
+							});
+						}
+
+						let volume = parseFloat(localStorage.getItem("volume"));
+						volume =
+							typeof volume === "number" && !Number.isNaN(volume)
+								? volume
+								: 20;
+						localStorage.setItem("volume", volume);
+						this.updatePlayer({ volume });
+
+						this.socket.dispatch(
+							"apis.joinRoom",
+							`view-youtube-video.${this.videoId}`
+						);
+
+						this.socket.on(
+							"event:youtubeVideo.removed",
+							() => {
+								new Toast("This YouTube video was removed.");
+								this.closeCurrentModal();
+							},
+							{ modalUuid: this.modalUuid }
+						);
 					} else {
-						console.log(999);
-						this.updatePlayer({
-							error: true,
-							errorMessage: "Player could not be loaded."
-						});
+						new Toast("YouTube video with that ID not found");
+						this.closeCurrentModal();
 					}
-
-					let volume = parseFloat(localStorage.getItem("volume"));
-					volume =
-						typeof volume === "number" && !Number.isNaN(volume)
-							? volume
-							: 20;
-					localStorage.setItem("volume", volume);
-					this.updatePlayer({ volume });
-
-					this.socket.dispatch(
-						"apis.joinRoom",
-						`view-youtube-video.${this.videoId}`
-					);
-
-					this.socket.on(
-						"event:youtubeVideo.removed",
-						() => {
-							new Toast("This YouTube video was removed.");
-							this.closeCurrentModal();
-						},
-						{ modalUuid: this.modalUuid }
-					);
-				} else {
-					new Toast("YouTube video with that ID not found");
-					this.closeCurrentModal();
 				}
-			});
+			);
 		},
 		remove() {
 			this.socket.dispatch("youtube.removeVideos", this.videoId, res => {
@@ -591,7 +630,9 @@ export default {
 		},
 		drawCanvas() {
 			if (!this.loaded) return;
-			const canvasElement = this.$refs.videoDurationCanvas;
+			const canvasElement =
+				this.$refs[`durationCanvas-${this.modalUuid}`];
+			if (!canvasElement) return;
 			const ctx = canvasElement.getContext("2d");
 
 			const videoDuration = Number(this.player.duration);
@@ -645,3 +686,279 @@ export default {
 	}
 };
 </script>
+
+<style lang="less" scoped>
+.night-mode {
+	.player-section,
+	.top-section {
+		background-color: var(--dark-grey-3) !important;
+		border: 0 !important;
+
+		.duration-canvas {
+			background-color: var(--dark-grey-2) !important;
+		}
+	}
+}
+
+.top-section {
+	display: flex;
+	margin: 0 auto;
+	padding: 10px;
+	border: 1px solid var(--light-grey-3);
+	border-radius: @border-radius;
+
+	.left-section {
+		display: flex;
+		flex-direction: column;
+		flex-grow: 1;
+
+		p {
+			text-overflow: ellipsis;
+			white-space: nowrap;
+			overflow: hidden;
+
+			&:first-child {
+				margin-top: auto;
+			}
+
+			&:last-child {
+				margin-bottom: auto;
+			}
+
+			& > span,
+			& > a {
+				margin-left: 5px;
+			}
+		}
+	}
+
+	:deep(.right-section .thumbnail-preview) {
+		width: 120px;
+		height: 120px;
+		margin: 0;
+	}
+
+	@media (max-width: 600px) {
+		flex-direction: column-reverse;
+
+		.left-section {
+			margin-top: 10px;
+		}
+	}
+}
+
+.player-section {
+	display: flex;
+	flex-direction: column;
+	margin: 10px auto 0 auto;
+	border: 1px solid var(--light-grey-3);
+	border-radius: @border-radius;
+	overflow: hidden;
+
+	.player-container {
+		position: relative;
+		padding-bottom: 56.25%; /* proportion value to aspect ratio 16:9 (9 / 16 = 0.5625 or 56.25%) */
+		height: 0;
+		overflow: hidden;
+
+		:deep([id^="viewYoutubeVideoPlayer"]) {
+			position: absolute;
+			top: 0;
+			left: 0;
+			width: 100%;
+			height: 100%;
+			min-height: 200px;
+		}
+	}
+
+	.duration-canvas {
+		background-color: var(--light-grey-2);
+	}
+
+	.player-error {
+		display: flex;
+		height: 428px;
+		align-items: center;
+
+		* {
+			margin: 0;
+			flex: 1;
+			font-size: 30px;
+			text-align: center;
+		}
+	}
+
+	.player-footer {
+		display: flex;
+		justify-content: space-between;
+		height: 54px;
+		padding-left: 10px;
+		padding-right: 10px;
+
+		> * {
+			width: 33.3%;
+			display: flex;
+			align-items: center;
+		}
+
+		.player-footer-left {
+			flex: 1;
+
+			& > .button:not(:first-child) {
+				margin-left: 5px;
+			}
+
+			:deep(& > .playerRateDropdown) {
+				margin-left: 5px;
+				margin-bottom: unset !important;
+
+				.control.has-addons {
+					margin-bottom: unset !important;
+
+					& > .button {
+						font-size: 24px;
+					}
+				}
+			}
+
+			:deep(.tippy-box[data-theme~="dropdown"]) {
+				max-width: 100px !important;
+
+				.nav-dropdown-items .nav-item {
+					justify-content: center !important;
+					border-radius: @border-radius !important;
+
+					&.active {
+						background-color: var(--primary-color);
+						color: var(--white);
+					}
+				}
+			}
+		}
+
+		.player-footer-center {
+			justify-content: center;
+			align-items: center;
+			flex: 2;
+			font-size: 18px;
+			font-weight: 400;
+			width: 200px;
+			margin: 0 5px;
+
+			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;
+
+			#volume-control {
+				margin: 3px;
+				margin-top: 0;
+				display: flex;
+				align-items: center;
+				cursor: pointer;
+
+				.volume-slider {
+					width: 100%;
+					padding: 0 15px;
+					background: transparent;
+					min-width: 100px;
+				}
+
+				input[type="range"] {
+					-webkit-appearance: none;
+					margin: 7.3px 0;
+				}
+
+				input[type="range"]:focus {
+					outline: none;
+				}
+
+				input[type="range"]::-webkit-slider-runnable-track {
+					width: 100%;
+					height: 5.2px;
+					cursor: pointer;
+					box-shadow: 0;
+					background: var(--light-grey-3);
+					border-radius: @border-radius;
+					border: 0;
+				}
+
+				input[type="range"]::-webkit-slider-thumb {
+					box-shadow: 0;
+					border: 0;
+					height: 19px;
+					width: 19px;
+					border-radius: 100%;
+					background: var(--primary-color);
+					cursor: pointer;
+					-webkit-appearance: none;
+					margin-top: -6.5px;
+				}
+
+				input[type="range"]::-moz-range-track {
+					width: 100%;
+					height: 5.2px;
+					cursor: pointer;
+					box-shadow: 0;
+					background: var(--light-grey-3);
+					border-radius: @border-radius;
+					border: 0;
+				}
+
+				input[type="range"]::-moz-range-thumb {
+					box-shadow: 0;
+					border: 0;
+					height: 19px;
+					width: 19px;
+					border-radius: 100%;
+					background: var(--primary-color);
+					cursor: pointer;
+					-webkit-appearance: none;
+					margin-top: -6.5px;
+				}
+				input[type="range"]::-ms-track {
+					width: 100%;
+					height: 5.2px;
+					cursor: pointer;
+					box-shadow: 0;
+					background: var(--light-grey-3);
+					border-radius: @border-radius;
+				}
+
+				input[type="range"]::-ms-fill-lower {
+					background: var(--light-grey-3);
+					border: 0;
+					border-radius: 0;
+					box-shadow: 0;
+				}
+
+				input[type="range"]::-ms-fill-upper {
+					background: var(--light-grey-3);
+					border: 0;
+					border-radius: 0;
+					box-shadow: 0;
+				}
+
+				input[type="range"]::-ms-thumb {
+					box-shadow: 0;
+					border: 0;
+					height: 15px;
+					width: 15px;
+					border-radius: 100%;
+					background: var(--primary-color);
+					cursor: pointer;
+					-webkit-appearance: none;
+					margin-top: 1.5px;
+				}
+			}
+		}
+	}
+}
+</style>