Browse Source

refactor(EditSong): Converted to composition API

Owen Diffey 2 years ago
parent
commit
552776ab13

+ 1 - 1
frontend/src/components/modals/EditSong/Tabs/Discogs.vue

@@ -205,7 +205,7 @@ onMounted(() => {
 					class="input"
 					type="text"
 					placeholder="Enter your Discogs query here..."
-					:ref="discogsInput"
+					ref="discogsInput"
 					v-model="discogsQuery"
 					@keyup.enter="searchDiscogsForPage(1)"
 					@change="onDiscogsQueryChange"

+ 1910 - 1963
frontend/src/components/modals/EditSong/index.vue

@@ -1,2067 +1,2014 @@
-<template>
-	<div>
-		<modal
-			:title="`${newSong ? 'Create' : 'Edit'} Song`"
-			class="song-modal"
-			:size="'wide'"
-			:split="true"
-			:intercept-close="true"
-			@close="onCloseModal"
-		>
-			<template #toggleMobileSidebar>
-				<slot name="toggleMobileSidebar" />
-			</template>
-			<template #sidebar>
-				<slot name="sidebar" />
-			</template>
-			<template #body>
-				<div v-if="!youtubeId && !newSong" class="notice-container">
-					<h4>No song has been selected</h4>
-				</div>
-				<div v-if="songDeleted" class="notice-container">
-					<h4>The song you were editing has been deleted</h4>
-				</div>
-				<div
-					v-if="
-						youtubeId &&
-						!songDataLoaded &&
-						!songNotFound &&
-						!newSong
-					"
-					class="notice-container"
-				>
-					<h4>Song hasn't loaded yet</h4>
-				</div>
-				<div
-					v-if="youtubeId && songNotFound && !newSong"
-					class="notice-container"
-				>
-					<h4>Song was not found</h4>
-				</div>
-				<div
-					class="left-section"
-					v-show="songDataLoaded && !songDeleted"
-				>
-					<div class="top-section">
-						<div class="player-section">
-							<div :id="`editSongPlayer-${modalUuid}`" />
+<script setup lang="ts">
+import { useStore } from "vuex";
+import {
+	defineAsyncComponent,
+	ref,
+	computed,
+	watch,
+	onMounted,
+	onBeforeUnmount
+} from "vue";
+import Toast from "toasters";
+import { useModalState, useModalActions } from "@/vuex_helpers";
+import aw from "@/aw";
+import ws from "@/ws";
+import validation from "@/validation";
+import keyboardShortcuts from "@/keyboardShortcuts";
 
-							<div v-show="youtubeError" class="player-error">
-								<h2>{{ youtubeErrorMessage }}</h2>
-							</div>
+const FloatingBox = defineAsyncComponent(
+	() => import("@/components/FloatingBox.vue")
+);
+const SaveButton = defineAsyncComponent(
+	() => import("@/components/SaveButton.vue")
+);
+const AutoSuggest = defineAsyncComponent(
+	() => import("@/components/AutoSuggest.vue")
+);
+const Discogs = defineAsyncComponent(() => import("./Tabs/Discogs.vue"));
+const ReportsTab = defineAsyncComponent(() => import("./Tabs/Reports.vue"));
+const Youtube = defineAsyncComponent(() => import("./Tabs/Youtube.vue"));
+const MusareSongs = defineAsyncComponent(() => import("./Tabs/Songs.vue"));
+
+const props = defineProps({
+	modalUuid: { type: String, default: "" },
+	modalModulePath: {
+		type: String,
+		default: "modals/editSong/MODAL_UUID"
+	},
+	discogsAlbum: { type: Object, default: null },
+	bulk: { type: Boolean, default: false },
+	flagged: { type: Boolean, default: false }
+});
+
+const emit = defineEmits([
+	"error",
+	"savedSuccess",
+	"savedError",
+	"flagSong",
+	"nextSong",
+	"close"
+]);
+
+const store = useStore();
+
+const { socket } = store.state.websockets;
+
+const modals = computed(() => store.state.modalVisibility.modals);
+const activeModals = computed(() => store.state.modalVisibility.activeModals);
+
+const modalState = useModalState(props.modalModulePath, {
+	modalUuid: props.modalUuid
+});
+const tab = computed(() => modalState.tab);
+const video = computed(() => modalState.video);
+const song = computed(() => modalState.song);
+const youtubeId = computed(() => modalState.youtubeId);
+const prefillData = computed(() => modalState.prefillData);
+const originalSong = computed(() => modalState.originalSong);
+const reports = computed(() => modalState.reports);
+const newSong = computed(() => modalState.newSong);
+
+const songDataLoaded = ref(false);
+const songDeleted = ref(false);
+const youtubeError = ref(false);
+const youtubeErrorMessage = ref("");
+const youtubeVideoDuration = ref("0.000");
+const youtubeVideoCurrentTime = ref(0);
+const youtubeVideoNote = ref("");
+const useHTTPS = ref(false);
+const muted = ref(false);
+const volumeSliderValue = ref(0);
+const artistInputValue = ref("");
+const genreInputValue = ref("");
+const tagInputValue = ref("");
+const activityWatchVideoDataInterval = ref(null);
+const activityWatchVideoLastStatus = ref("");
+const activityWatchVideoLastStartDuration = ref("");
+const recommendedGenres = ref([
+	"Blues",
+	"Country",
+	"Disco",
+	"Funk",
+	"Hip-Hop",
+	"Jazz",
+	"Metal",
+	"Oldies",
+	"Other",
+	"Pop",
+	"Rap",
+	"Reggae",
+	"Rock",
+	"Techno",
+	"Trance",
+	"Classical",
+	"Instrumental",
+	"House",
+	"Electronic",
+	"Christian Rap",
+	"Lo-Fi",
+	"Musical",
+	"Rock 'n' Roll",
+	"Opera",
+	"Drum & Bass",
+	"Club-House",
+	"Indie",
+	"Heavy Metal",
+	"Christian rock",
+	"Dubstep"
+]);
+const autosuggest = ref({
+	allItems: {
+		artists: [],
+		genres: [],
+		tags: []
+	}
+});
+const songNotFound = ref(false);
+const showRateDropdown = ref(false);
+const thumbnailElement = ref();
+const thumbnailNotSquare = ref(false);
+const thumbnailWidth = ref(null);
+const thumbnailHeight = ref(null);
+const thumbnailLoadError = ref(false);
+const tabs = ref([]);
+const inputs = ref([]);
+const playerReady = ref(true);
+const interval = ref();
+const saveButtonRefs = ref([]);
+const canvasElement = ref();
+const genreHelper = ref();
+
+const isYoutubeThumbnail = computed(
+	() =>
+		songDataLoaded.value &&
+		song.value.youtubeId &&
+		song.value.thumbnail &&
+		(song.value.thumbnail.lastIndexOf("i.ytimg.com") !== -1 ||
+			song.value.thumbnail.lastIndexOf("img.youtube.com") !== -1)
+);
+
+const {
+	stopVideo,
+	hardStopVideo,
+	loadVideoById,
+	pauseVideo,
+	setSong,
+	resetSong,
+	updateOriginalSong,
+	updateSongField,
+	updateReports,
+	setPlaybackRate
+} = useModalActions(
+	props.modalModulePath,
+	[
+		"stopVideo",
+		"hardStopVideo",
+		"loadVideoById",
+		"pauseVideo",
+		"setSong",
+		"resetSong",
+		"updateOriginalSong",
+		"updateSongField",
+		"updateReports",
+		"setPlaybackRate"
+	],
+	{
+		modalUuid: props.modalUuid
+	}
+);
 
-							<canvas
-								:ref="`durationCanvas-${modalUuid}`"
-								class="duration-canvas"
-								v-show="!youtubeError"
-								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="video.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="
-											() => {
-												showRateDropdown = true;
-											}
-										"
-										@hide="
-											() => {
-												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>
-											<button
-												class="button dropdown-toggle"
-											>
-												<i class="material-icons">
-													{{
-														showRateDropdown
-															? "expand_more"
-															: "expand_less"
-													}}
-												</i>
-											</button>
-										</div>
+const openModal = payload =>
+	store.dispatch("modalVisibility/openModal", payload);
 
-										<template #content>
-											<div class="nav-dropdown-items">
-												<button
-													class="nav-item button"
-													:class="{
-														active:
-															video.playbackRate ===
-															0.5
-													}"
-													title="0.5x"
-													@click="
-														setPlaybackRate(0.5)
-													"
-												>
-													<p>0.5x</p>
-												</button>
-												<button
-													class="nav-item button"
-													:class="{
-														active:
-															video.playbackRate ===
-															1
-													}"
-													title="1x"
-													@click="setPlaybackRate(1)"
-												>
-													<p>1x</p>
-												</button>
-												<button
-													class="nav-item button"
-													:class="{
-														active:
-															video.playbackRate ===
-															2
-													}"
-													title="2x"
-													@click="setPlaybackRate(2)"
-												>
-													<p>2x</p>
-												</button>
-											</div>
-										</template>
-									</tippy>
-								</div>
-								<div class="player-footer-center">
-									<span>
-										<span>
-											{{ youtubeVideoCurrentTime }}
-										</span>
-										/
-										<span>
-											{{ youtubeVideoDuration }}
-											{{ youtubeVideoNote }}
-										</span>
-									</span>
-								</div>
-								<div class="player-footer-right">
-									<p id="volume-control">
-										<i
-											class="material-icons"
-											@click="toggleMute()"
-											:content="`${
-												muted ? 'Unmute' : 'Mute'
-											}`"
-											v-tippy
-											>{{
-												muted
-													? "volume_mute"
-													: volumeSliderValue >= 50
-													? "volume_up"
-													: "volume_down"
-											}}</i
-										>
-										<input
-											v-model="volumeSliderValue"
-											type="range"
-											min="0"
-											max="100"
-											class="volume-slider active"
-											@change="changeVolume()"
-											@input="changeVolume()"
-										/>
-									</p>
-								</div>
-							</div>
-						</div>
-						<song-thumbnail
-							v-if="songDataLoaded && !songDeleted"
-							:song="song"
-							:fallback="false"
-							class="thumbnail-preview"
-							@loadError="onThumbnailLoadError"
-						/>
-						<img
-							v-if="
-								!isYoutubeThumbnail &&
-								songDataLoaded &&
-								!songDeleted
-							"
-							class="thumbnail-dummy"
-							:src="song.thumbnail"
-							ref="thumbnailElement"
-							@load="onThumbnailLoad"
-						/>
-					</div>
+const closeCurrentModal = () => {
+	if (props.bulk) emit("close");
+	else store.dispatch("modalVisibility/closeCurrentModal");
+};
 
-					<div
-						class="edit-section"
-						v-if="songDataLoaded && !songDeleted"
-					>
-						<div class="control is-grouped">
-							<div class="title-container">
-								<label class="label">Title</label>
-								<p class="control has-addons">
-									<input
-										class="input"
-										type="text"
-										ref="title-input"
-										v-model="song.title"
-										placeholder="Enter song title..."
-										@keyup.shift.enter="
-											getAlbumData('title')
-										"
-									/>
-									<button
-										class="button youtube-get-button"
-										@click="getYouTubeData('title')"
-									>
-										<div
-											class="youtube-icon"
-											v-tippy
-											content="Fill from YouTube"
-										></div>
-									</button>
-									<button
-										class="button album-get-button"
-										@click="getAlbumData('title')"
-									>
-										<i
-											class="material-icons"
-											v-tippy
-											content="Fill from Discogs"
-											>album</i
-										>
-									</button>
-								</p>
-							</div>
+const showTab = payload => {
+	if (tabs.value[`${payload}-tab`])
+		tabs.value[`${payload}-tab`].scrollIntoView({ block: "nearest" });
+	store.dispatch(
+		`${props.modalModulePath.replace(
+			"MODAL_UUID",
+			props.modalUuid
+		)}/showTab`,
+		payload
+	);
+};
 
-							<div class="duration-container">
-								<label class="label">Duration</label>
-								<p class="control has-addons">
-									<input
-										class="input"
-										type="text"
-										placeholder="Enter song duration..."
-										v-model.number="song.duration"
-										@keyup.shift.enter="fillDuration()"
-									/>
-									<button
-										class="button duration-fill-button"
-										@click="fillDuration()"
-									>
-										<i
-											class="material-icons"
-											v-tippy
-											content="Sync duration with YouTube"
-											>sync</i
-										>
-									</button>
-								</p>
-							</div>
+const onThumbnailLoad = () => {
+	if (thumbnailElement.value) {
+		const height = thumbnailElement.value.naturalHeight;
+		const width = thumbnailElement.value.naturalWidth;
+
+		thumbnailNotSquare.value = height !== width;
+		thumbnailHeight.value = height;
+		thumbnailWidth.value = width;
+	} else {
+		thumbnailNotSquare.value = false;
+		thumbnailHeight.value = null;
+		thumbnailWidth.value = null;
+	}
+};
 
-							<div class="skip-duration-container">
-								<label class="label">Skip duration</label>
-								<p class="control">
-									<input
-										class="input"
-										type="text"
-										placeholder="Enter skip duration..."
-										v-model.number="song.skipDuration"
-									/>
-								</p>
-							</div>
-						</div>
+const onThumbnailLoadError = error => {
+	thumbnailLoadError.value = error !== 0;
+};
 
-						<div class="control is-grouped">
-							<div class="album-art-container">
-								<label class="label">
-									Thumbnail
-									<i
-										v-if="
-											thumbnailNotSquare &&
-											!isYoutubeThumbnail
-										"
-										class="material-icons thumbnail-warning"
-										content="Thumbnail not square, it will be stretched"
-										v-tippy="{ theme: 'info' }"
-									>
-										warning
-									</i>
-									<i
-										v-if="
-											thumbnailLoadError &&
-											!isYoutubeThumbnail
-										"
-										class="material-icons thumbnail-warning"
-										content="Error loading thumbnail"
-										v-tippy="{ theme: 'info' }"
-									>
-										warning
-									</i>
-								</label>
+const unloadSong = (_youtubeId, songId) => {
+	songDataLoaded.value = false;
+	songDeleted.value = false;
+	stopVideo();
+	pauseVideo(true);
+	resetSong(_youtubeId);
+	thumbnailNotSquare.value = false;
+	thumbnailWidth.value = null;
+	thumbnailHeight.value = null;
+	youtubeVideoCurrentTime.value = "0.000";
+	youtubeVideoDuration.value = "0.000";
+	youtubeVideoNote.value = "";
+	if (songId) socket.dispatch("apis.leaveRoom", `edit-song.${songId}`);
+	if (saveButtonRefs.value.saveButton)
+		saveButtonRefs.value.saveButton.status = "default";
+};
 
-								<p class="control has-addons">
-									<input
-										class="input"
-										type="text"
-										v-model="song.thumbnail"
-										placeholder="Enter link to thumbnail..."
-										@keyup.shift.enter="
-											getAlbumData('albumArt')
-										"
-									/>
-									<button
-										class="button youtube-get-button"
-										@click="getYouTubeData('thumbnail')"
-									>
-										<div
-											class="youtube-icon"
-											v-tippy
-											content="Fill from YouTube"
-										></div>
-									</button>
-									<button
-										class="button album-get-button"
-										@click="getAlbumData('albumArt')"
-									>
-										<i
-											class="material-icons"
-											v-tippy
-											content="Fill from Discogs"
-											>album</i
-										>
-									</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 class="verified-container">
-								<label class="label">Verified</label>
-								<p class="is-expanded checkbox-control">
-									<label class="switch">
-										<input
-											type="checkbox"
-											id="verified"
-											v-model="song.verified"
-										/>
-										<span class="slider round"></span>
-									</label>
-								</p>
-							</div>
-						</div>
+const loadSong = _youtubeId => {
+	console.log(`LOAD SONG ${_youtubeId}`);
+	songNotFound.value = false;
+	socket.dispatch(`songs.getSongsFromYoutubeIds`, [_youtubeId], res => {
+		const { songs } = res.data;
+		if (res.status === "success" && songs.length > 0) {
+			let _song = songs[0];
+			_song = Object.assign(_song, prefillData.value);
 
-						<div class="control is-grouped">
-							<div class="artists-container">
-								<label class="label">Artists</label>
-								<p class="control has-addons">
-									<auto-suggest
-										v-model="artistInputValue"
-										ref="new-artist"
-										placeholder="Add artist..."
-										:all-items="
-											autosuggest.allItems.artists
-										"
-										@submitted="addTag('artists')"
-										@keyup.shift.enter="
-											getAlbumData('artists')
-										"
-									/>
-									<button
-										class="button youtube-get-button"
-										@click="getYouTubeData('author')"
-									>
-										<div
-											class="youtube-icon"
-											v-tippy
-											content="Fill from YouTube"
-										></div>
-									</button>
-									<button
-										class="button album-get-button"
-										@click="getAlbumData('artists')"
-									>
-										<i
-											class="material-icons"
-											v-tippy
-											content="Fill from Discogs"
-											>album</i
-										>
-									</button>
-									<button
-										class="button is-info add-button"
-										@click="addTag('artists')"
-									>
-										<i class="material-icons">add</i>
-									</button>
-								</p>
-								<div class="list-container">
-									<div
-										class="list-item"
-										v-for="artist in song.artists"
-										:key="artist"
-									>
-										<div
-											class="list-item-circle"
-											@click="
-												removeTag('artists', artist)
-											"
-										>
-											<i class="material-icons">close</i>
-										</div>
-										<p>{{ artist }}</p>
-									</div>
-								</div>
-							</div>
-							<div class="genres-container">
-								<label class="label">
-									<span>Genres</span>
-									<i
-										class="material-icons"
-										@click="toggleGenreHelper"
-										@dblclick="resetGenreHelper"
-										v-tippy
-										content="View list of genres"
-										>info</i
-									>
-								</label>
-								<p class="control has-addons">
-									<auto-suggest
-										v-model="genreInputValue"
-										ref="new-genre"
-										placeholder="Add genre..."
-										:all-items="autosuggest.allItems.genres"
-										@submitted="addTag('genres')"
-										@keyup.shift.enter="
-											getAlbumData('genres')
-										"
-									/>
-									<button
-										class="button album-get-button"
-										@click="getAlbumData('genres')"
-									>
-										<i
-											class="material-icons"
-											v-tippy
-											content="Fill from Discogs"
-											>album</i
-										>
-									</button>
-									<button
-										class="button is-info add-button"
-										@click="addTag('genres')"
-									>
-										<i class="material-icons">add</i>
-									</button>
-								</p>
-								<div class="list-container">
-									<div
-										class="list-item"
-										v-for="genre in song.genres"
-										:key="genre"
-									>
-										<div
-											class="list-item-circle"
-											@click="removeTag('genres', genre)"
-										>
-											<i class="material-icons">close</i>
-										</div>
-										<p>{{ genre }}</p>
-									</div>
-								</div>
-							</div>
-							<div class="tags-container">
-								<label class="label">Tags</label>
-								<p class="control has-addons">
-									<auto-suggest
-										v-model="tagInputValue"
-										ref="new-tag"
-										placeholder="Add tag..."
-										:all-items="autosuggest.allItems.tags"
-										@submitted="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>
-				</div>
-				<div
-					class="right-section"
-					v-if="songDataLoaded && !songDeleted"
-				>
-					<div id="tabs-container">
-						<div id="tab-selection">
-							<button
-								class="button is-default"
-								:class="{ selected: tab === 'discogs' }"
-								ref="discogs-tab"
-								@click="showTab('discogs')"
-							>
-								Discogs
-							</button>
-							<button
-								v-if="!newSong"
-								class="button is-default"
-								:class="{ selected: tab === 'reports' }"
-								ref="reports-tab"
-								@click="showTab('reports')"
-							>
-								Reports ({{ reports.length }})
-							</button>
-							<button
-								class="button is-default"
-								:class="{ selected: tab === 'youtube' }"
-								ref="youtube-tab"
-								@click="showTab('youtube')"
-							>
-								YouTube
-							</button>
-							<button
-								class="button is-default"
-								:class="{ selected: tab === 'musare-songs' }"
-								ref="musare-songs-tab"
-								@click="showTab('musare-songs')"
-							>
-								Songs
-							</button>
-						</div>
-						<discogs
-							class="tab"
-							v-show="tab === 'discogs'"
-							:bulk="bulk"
-							:modal-uuid="modalUuid"
-							:modal-module-path="modalModulePath"
-						/>
-						<reports
-							v-if="!newSong"
-							class="tab"
-							v-show="tab === 'reports'"
-							:modal-uuid="modalUuid"
-							:modal-module-path="modalModulePath"
-						/>
-						<youtube
-							class="tab"
-							v-show="tab === 'youtube'"
-							:modal-uuid="modalUuid"
-							:modal-module-path="modalModulePath"
-						/>
-						<musare-songs
-							class="tab"
-							v-show="tab === 'musare-songs'"
-							:modal-uuid="modalUuid"
-							:modal-module-path="modalModulePath"
-						/>
-					</div>
-				</div>
-			</template>
-			<template #footer>
-				<div v-if="bulk">
-					<button class="button is-primary" @click="editNextSong()">
-						Next
-					</button>
-					<button
-						class="button is-primary"
-						@click="toggleFlag()"
-						v-if="youtubeId && !songDeleted"
-					>
-						{{ flagged ? "Unflag" : "Flag" }}
-					</button>
-				</div>
-				<div v-if="!newSong && !songDeleted">
-					<save-button
-						ref="saveButton"
-						@clicked="save(song, false, 'saveButton')"
-					/>
-					<save-button
-						ref="saveAndCloseButton"
-						:default-message="
-							bulk ? `Save and next` : `Save and close`
-						"
-						@clicked="save(song, true, 'saveAndCloseButton')"
-					/>
+			setSong(_song);
 
-					<div class="right">
-						<button
-							class="button is-danger icon-with-button material-icons"
-							@click.prevent="
-								confirmAction({
-									message:
-										'Removing this song will remove it from all playlists and cause a ratings recalculation.',
-									action: 'remove',
-									params: song._id
-								})
-							"
-							content="Delete Song"
-							v-tippy
-						>
-							delete_forever
-						</button>
-					</div>
-				</div>
-				<div v-else-if="newSong">
-					<save-button
-						ref="createButton"
-						default-message="Create Song"
-						@clicked="save(song, false, 'createButton', true)"
-					/>
-					<save-button
-						ref="createAndCloseButton"
-						:default-message="
-							bulk ? `Create and next` : `Create and close`
-						"
-						@clicked="
-							save(song, true, 'createAndCloseButton', true)
-						"
-					/>
-				</div>
-			</template>
-		</modal>
-		<floating-box
-			id="genreHelper"
-			ref="genreHelper"
-			:column="false"
-			title="Song Genres List"
-		>
-			<template #body>
-				<span
-					v-for="item in autosuggest.allItems.genres"
-					:key="`genre-helper-${item}`"
-				>
-					{{ item }}
-				</span>
-			</template>
-		</floating-box>
-	</div>
-</template>
+			songDataLoaded.value = true;
 
-<script>
-import { mapState, mapGetters, mapActions } from "vuex";
-import Toast from "toasters";
+			if (_song._id)
+				socket.dispatch("apis.joinRoom", `edit-song.${_song._id}`);
 
-import { mapModalState, mapModalActions } from "@/vuex_helpers";
+			if (video.value.player && video.value.player.cueVideoById) {
+				video.value.player.cueVideoById(_youtubeId, _song.skipDuration);
+			}
+		} else {
+			new Toast("Song with that ID not found");
+			if (props.bulk) songNotFound.value = true;
+			if (!props.bulk) closeCurrentModal();
+		}
+	});
 
-import aw from "@/aw";
-import ws from "@/ws";
-import validation from "@/validation";
-import keyboardShortcuts from "@/keyboardShortcuts";
+	if (!newSong.value)
+		socket.dispatch("reports.getReportsForSong", song.value._id, res => {
+			updateReports(res.data.reports);
+		});
+};
 
-import FloatingBox from "../../FloatingBox.vue";
-import SaveButton from "../../SaveButton.vue";
-import AutoSuggest from "@/components/AutoSuggest.vue";
-
-import Discogs from "./Tabs/Discogs.vue";
-import Reports from "./Tabs/Reports.vue";
-import Youtube from "./Tabs/Youtube.vue";
-import MusareSongs from "./Tabs/Songs.vue";
-
-export default {
-	components: {
-		FloatingBox,
-		SaveButton,
-		AutoSuggest,
-		Discogs,
-		Reports,
-		Youtube,
-		MusareSongs
-	},
-	props: {
-		// songId: { type: String, default: null },
-		modalUuid: { type: String, default: "" },
-		modalModulePath: {
-			type: String,
-			default: "modals/editSong/MODAL_UUID"
-		},
-		discogsAlbum: { type: Object, default: null },
-		bulk: { type: Boolean, default: false },
-		flagged: { type: Boolean, default: false }
-	},
-	emits: [
-		"error",
-		"savedSuccess",
-		"savedError",
-		"flagSong",
-		"nextSong",
-		"close"
-	],
-	data() {
-		return {
-			songDataLoaded: false,
-			songDeleted: false,
-			youtubeError: false,
-			youtubeErrorMessage: "",
-			focusedElementBefore: null,
-			youtubeVideoDuration: "0.000",
-			youtubeVideoCurrentTime: 0,
-			youtubeVideoNote: "",
-			useHTTPS: false,
-			muted: false,
-			volumeSliderValue: 0,
-			artistInputValue: "",
-			genreInputValue: "",
-			tagInputValue: "",
-			activityWatchVideoDataInterval: null,
-			activityWatchVideoLastStatus: "",
-			activityWatchVideoLastStartDuration: "",
-			recommendedGenres: [
-				"Blues",
-				"Country",
-				"Disco",
-				"Funk",
-				"Hip-Hop",
-				"Jazz",
-				"Metal",
-				"Oldies",
-				"Other",
-				"Pop",
-				"Rap",
-				"Reggae",
-				"Rock",
-				"Techno",
-				"Trance",
-				"Classical",
-				"Instrumental",
-				"House",
-				"Electronic",
-				"Christian Rap",
-				"Lo-Fi",
-				"Musical",
-				"Rock 'n' Roll",
-				"Opera",
-				"Drum & Bass",
-				"Club-House",
-				"Indie",
-				"Heavy Metal",
-				"Christian rock",
-				"Dubstep"
-			],
-			autosuggest: {
-				allItems: {
-					artists: [],
-					genres: [],
-					tags: []
-				}
-			},
-			songNotFound: false,
-			showRateDropdown: false,
-			thumbnailNotSquare: false,
-			thumbnailWidth: null,
-			thumbnailHeight: null,
-			thumbnailLoadError: false
-		};
-	},
-	computed: {
-		isYoutubeThumbnail() {
-			return (
-				this.songDataLoaded &&
-				this.song.youtubeId &&
-				this.song.thumbnail &&
-				(this.song.thumbnail.lastIndexOf("i.ytimg.com") !== -1 ||
-					this.song.thumbnail.lastIndexOf("img.youtube.com") !== -1)
-			);
-		},
-		...mapModalState("MODAL_MODULE_PATH", {
-			tab: state => state.tab,
-			video: state => state.video,
-			song: state => state.song,
-			youtubeId: state => state.youtubeId,
-			prefillData: state => state.prefillData,
-			originalSong: state => state.originalSong,
-			reports: state => state.reports,
-			newSong: state => state.newSong
-		}),
-		...mapState("modalVisibility", {
-			activeModals: state => state.activeModals
-		}),
-		...mapGetters({
-			socket: "websockets/getSocket"
-		})
-	},
-	watch: {
-		/* eslint-disable */
-		"song.duration": function () {
-			this.drawCanvas();
-		},
-		"song.skipDuration": function () {
-			this.drawCanvas();
-		},
-		/* eslint-enable */
-		youtubeId(youtubeId, oldYoutubeId) {
-			console.log("NEW YOUTUBE ID", youtubeId);
-			this.unloadSong(oldYoutubeId);
-			this.loadSong(youtubeId);
-		}
-	},
-	beforeMount() {
-		console.log("EDITSONG BEFOREMOUNT");
-	},
-	async mounted() {
-		console.log("EDITSONG MOUNTED");
-		this.activityWatchVideoDataInterval = setInterval(() => {
-			this.sendActivityWatchVideoData();
-		}, 1000);
+const drawCanvas = () => {
+	if (!songDataLoaded.value || !canvasElement.value) return;
+	console.log(555, canvasElement.value);
+	const ctx = canvasElement.value.getContext("2d");
 
-		this.useHTTPS = await lofig.get("cookie.secure");
+	const videoDuration = Number(youtubeVideoDuration.value);
 
-		ws.onConnect(this.init);
+	const skipDuration = Number(song.value.skipDuration.value);
+	const duration = Number(song.value.duration.value);
+	const afterDuration = videoDuration - (skipDuration + duration);
 
-		let volume = parseFloat(localStorage.getItem("volume"));
-		volume =
-			typeof volume === "number" && !Number.isNaN(volume) ? volume : 20;
-		localStorage.setItem("volume", volume);
-		this.volumeSliderValue = volume;
+	const width = 530;
 
-		this.socket.on(
-			"event:admin.song.removed",
-			res => {
-				if (res.data.songId === this.song._id) {
-					this.songDeleted = true;
-				}
-			},
-			{ modalUuid: this.modalUuid }
-		);
+	const currentTime =
+		video.value.player && video.value.player.getCurrentTime
+			? video.value.player.getCurrentTime()
+			: 0;
 
-		keyboardShortcuts.registerShortcut("editSong.pauseResumeVideo", {
-			keyCode: 101,
-			preventDefault: true,
-			handler: () => {
-				if (this.video.paused) this.play();
-				else this.settings("pause");
-			}
-		});
+	const widthSkipDuration = (skipDuration / videoDuration) * width;
+	const widthDuration = (duration / videoDuration) * width;
+	const widthAfterDuration = (afterDuration / videoDuration) * width;
 
-		keyboardShortcuts.registerShortcut("editSong.stopVideo", {
-			keyCode: 101,
-			ctrl: true,
-			preventDefault: true,
-			handler: () => {
-				this.settings("stop");
-			}
-		});
+	const widthCurrentTime = (currentTime / videoDuration) * width;
 
-		keyboardShortcuts.registerShortcut("editSong.hardStopVideo", {
-			keyCode: 101,
-			ctrl: true,
-			shift: true,
-			preventDefault: true,
-			handler: () => {
-				this.settings("hardStop");
-			}
-		});
+	const skipDurationColor = "#F42003";
+	const durationColor = "#03A9F4";
+	const afterDurationColor = "#41E841";
+	const currentDurationColor = "#3b25e8";
 
-		keyboardShortcuts.registerShortcut("editSong.skipToLast10Secs", {
-			keyCode: 102,
-			preventDefault: true,
-			handler: () => {
-				this.settings("skipToLast10Secs");
-			}
-		});
+	ctx.fillStyle = skipDurationColor;
+	ctx.fillRect(0, 0, widthSkipDuration, 20);
+	ctx.fillStyle = durationColor;
+	ctx.fillRect(widthSkipDuration, 0, widthDuration, 20);
+	ctx.fillStyle = afterDurationColor;
+	ctx.fillRect(widthSkipDuration + widthDuration, 0, widthAfterDuration, 20);
 
-		keyboardShortcuts.registerShortcut("editSong.lowerVolumeLarge", {
-			keyCode: 98,
-			preventDefault: true,
-			handler: () => {
-				this.volumeSliderValue = Math.max(
-					0,
-					this.volumeSliderValue - 10
-				);
-				this.changeVolume();
-			}
-		});
+	ctx.fillStyle = currentDurationColor;
+	ctx.fillRect(widthCurrentTime, 0, 1, 20);
+};
 
-		keyboardShortcuts.registerShortcut("editSong.lowerVolumeSmall", {
-			keyCode: 98,
-			ctrl: true,
-			preventDefault: true,
-			handler: () => {
-				this.volumeSliderValue = Math.max(
-					0,
-					this.volumeSliderValue - 1
-				);
-				this.changeVolume();
-			}
-		});
+const seekTo = position => {
+	pauseVideo(false);
+	video.value.player.seekTo(position);
+};
 
-		keyboardShortcuts.registerShortcut("editSong.increaseVolumeLarge", {
-			keyCode: 104,
-			preventDefault: true,
-			handler: () => {
-				this.volumeSliderValue = Math.min(
-					100,
-					this.volumeSliderValue + 10
-				);
-				this.changeVolume();
-			}
+const init = () => {
+	if (newSong.value && !youtubeId.value && !props.bulk) {
+		setSong({
+			youtubeId: "",
+			title: "",
+			artists: [],
+			genres: [],
+			tags: [],
+			duration: 0,
+			skipDuration: 0,
+			thumbnail: "",
+			verified: false
 		});
+		songDataLoaded.value = true;
+		showTab("youtube");
+	} else if (youtubeId.value) loadSong(youtubeId.value);
+	else if (!props.bulk) {
+		new Toast("You can't open EditSong without editing a song");
+		return closeCurrentModal();
+	}
 
-		keyboardShortcuts.registerShortcut("editSong.increaseVolumeSmall", {
-			keyCode: 104,
-			ctrl: true,
-			preventDefault: true,
-			handler: () => {
-				this.volumeSliderValue = Math.min(
-					100,
-					this.volumeSliderValue + 1
-				);
-				this.changeVolume();
-			}
-		});
+	interval.value = setInterval(() => {
+		if (
+			song.value.duration !== -1 &&
+			video.value.paused === false &&
+			playerReady.value &&
+			(video.value.player.getCurrentTime() - song.value.skipDuration >
+				song.value.duration ||
+				(video.value.player.getCurrentTime() > 0 &&
+					video.value.player.getCurrentTime() >=
+						video.value.player.getDuration()))
+		) {
+			stopVideo();
+			pauseVideo(true);
+			drawCanvas();
+		}
+		if (
+			playerReady.value &&
+			video.value.player.getVideoData &&
+			video.value.player.getVideoData() &&
+			video.value.player.getVideoData().video_id === song.value.youtubeId
+		) {
+			const currentTime = video.value.player.getCurrentTime();
 
-		keyboardShortcuts.registerShortcut("editSong.save", {
-			keyCode: 83,
-			ctrl: true,
-			preventDefault: true,
-			handler: () => {
-				this.save(this.song, false, "saveButton");
-			}
-		});
+			if (currentTime !== undefined)
+				youtubeVideoCurrentTime.value = currentTime.toFixed(3);
 
-		keyboardShortcuts.registerShortcut("editSong.saveClose", {
-			keyCode: 83,
-			ctrl: true,
-			alt: true,
-			preventDefault: true,
-			handler: () => {
-				this.save(this.song, true, "saveAndCloseButton");
-			}
-		});
+			if (youtubeVideoDuration.value.indexOf(".000") !== -1) {
+				const duration = video.value.player.getDuration();
 
-		keyboardShortcuts.registerShortcut("editSong.focusTitle", {
-			keyCode: 36,
-			preventDefault: true,
-			handler: () => {
-				this.$refs["title-input"].focus();
-			}
-		});
+				if (duration !== undefined) {
+					if (
+						`${youtubeVideoDuration.value}` ===
+						`${Number(song.value.duration).toFixed(3)}`
+					)
+						song.value.duration = duration.toFixed(3);
 
-		keyboardShortcuts.registerShortcut("editSong.useAllDiscogs", {
-			keyCode: 68,
-			alt: true,
-			ctrl: true,
-			preventDefault: true,
-			handler: () => {
-				this.getAlbumData("title");
-				this.getAlbumData("albumArt");
-				this.getAlbumData("artists");
-				this.getAlbumData("genres");
-			}
-		});
+					youtubeVideoDuration.value = duration.toFixed(3);
+					if (youtubeVideoDuration.value.indexOf(".000") !== -1)
+						youtubeVideoNote.value = "(~)";
+					else youtubeVideoNote.value = "";
 
-		keyboardShortcuts.registerShortcut("editSong.closeModal", {
-			keyCode: 27,
-			handler: () => {
-				if (
-					this.modals[
-						this.activeModals[this.activeModals.length - 1]
-					] === "editSong" ||
-					this.modals[
-						this.activeModals[this.activeModals.length - 1]
-					] === "editSongs"
-				) {
-					this.onCloseModal();
+					drawCanvas();
 				}
 			}
-		});
-
-		/*
-
-		editSong.pauseResume - Num 5 - Pause/resume song
-		editSong.stopVideo - Ctrl - Num 5 - Stop
-		editSong.hardStopVideo - Shift - Ctrl - Num 5 - Stop
-		editSong.skipToLast10Secs - Num 6 - Skip to last 10 seconds
-
-		editSong.lowerVolumeLarge - Num 2 - Volume down by 10
-		editSong.lowerVolumeSmall - Ctrl - Num 2 - Volume down by 1
-		editSong.increaseVolumeLarge - Num 8 - Volume up by 10
-		editSong.increaseVolumeSmall - Ctrl - Num 8 - Volume up by 1
+		}
 
-		editSong.focusTitle - Home - Focus the title input
-		editSong.focusDicogs - End - Focus the discogs input
+		if (video.value.paused === false) drawCanvas();
+	}, 200);
+
+	if (window.YT && window.YT.Player) {
+		video.value.player = new window.YT.Player(
+			`editSongPlayer-${props.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: song.value.skipDuration,
+				events: {
+					onReady: () => {
+						let volume = parseFloat(localStorage.getItem("volume"));
+						volume = typeof volume === "number" ? volume : 20;
+						video.value.player.setVolume(volume);
+						if (volume > 0) video.value.player.unMute();
+
+						playerReady.value = true;
+
+						if (song.value && song.value.youtubeId)
+							video.value.player.cueVideoById(
+								song.value.youtubeId,
+								song.value.skipDuration
+							);
 
-		editSong.save - Ctrl - S - Saves song
-		editSong.save - Ctrl - Alt - S - Saves song and closes the modal
-		editSong.save - Ctrl - Alt - V - Saves song, verifies songs and then closes the modal
-		editSong.close - F4 - Closes modal without saving
+						setPlaybackRate(null);
 
-		editSong.useAllDiscogs - Ctrl - Alt - D - Sets all fields to the Discogs data
+						drawCanvas();
+					},
+					onStateChange: event => {
+						drawCanvas();
 
-		Inside Discogs inputs: Ctrl - D - Sets this field to the Discogs data
+						if (event.data === 1) {
+							video.value.paused = false;
+							let youtubeDuration =
+								video.value.player.getDuration();
+							const newYoutubeVideoDuration =
+								youtubeDuration.toFixed(3);
 
-		*/
-	},
-	beforeUnmount() {
-		console.log("EDITSONG BEFOREUNMOUNT");
-		this.unloadSong(this.youtubeId, this.song._id);
-
-		this.playerReady = false;
-		clearInterval(this.interval);
-		clearInterval(this.activityWatchVideoDataInterval);
-
-		const shortcutNames = [
-			"editSong.pauseResume",
-			"editSong.stopVideo",
-			"editSong.hardStopVideo",
-			"editSong.skipToLast10Secs",
-			"editSong.lowerVolumeLarge",
-			"editSong.lowerVolumeSmall",
-			"editSong.increaseVolumeLarge",
-			"editSong.increaseVolumeSmall",
-			"editSong.focusTitle",
-			"editSong.focusDicogs",
-			"editSong.save",
-			"editSong.saveClose",
-			"editSong.useAllDiscogs",
-			"editSong.closeModal"
-		];
-
-		shortcutNames.forEach(shortcutName => {
-			keyboardShortcuts.unregisterShortcut(shortcutName);
-		});
+							if (
+								youtubeVideoDuration.value.indexOf(".000") !==
+									-1 &&
+								`${youtubeVideoDuration.value}` !==
+									`${newYoutubeVideoDuration}`
+							) {
+								const songDurationNumber = Number(
+									song.value.duration
+								);
+								const songDurationNumber2 =
+									Number(song.value.duration) + 1;
+								const songDurationNumber3 =
+									Number(song.value.duration) - 1;
+								const fixedSongDuration =
+									songDurationNumber.toFixed(3);
+								const fixedSongDuration2 =
+									songDurationNumber2.toFixed(3);
+								const fixedSongDuration3 =
+									songDurationNumber3.toFixed(3);
+
+								if (
+									`${youtubeVideoDuration.value}` ===
+										`${Number(song.value.duration).toFixed(
+											3
+										)}` &&
+									(fixedSongDuration ===
+										youtubeVideoDuration.value ||
+										fixedSongDuration2 ===
+											youtubeVideoDuration.value ||
+										fixedSongDuration3 ===
+											youtubeVideoDuration.value)
+								)
+									song.value.duration =
+										newYoutubeVideoDuration;
+
+								youtubeVideoDuration.value =
+									newYoutubeVideoDuration;
+								if (
+									youtubeVideoDuration.value.indexOf(
+										".000"
+									) !== -1
+								)
+									youtubeVideoNote.value = "(~)";
+								else youtubeVideoNote.value = "";
+							}
 
-		if (!this.bulk) {
-			// Delete the VueX module that was created for this modal, after all other cleanup tasks are performed
-			this.$store.unregisterModule([
-				"modals",
-				"editSong",
-				this.modalUuid
-			]);
-		} else {
-			console.log("UNREGISTER EDITSONG");
-			this.$store.unregisterModule([
-				"modals",
-				"editSongs",
-				this.modalUuid,
-				"editSong"
-			]);
-		}
-	},
-	unmounted() {
-		console.log("EDITSONG UNMOUNTED");
-	},
-	methods: {
-		onThumbnailLoad() {
-			if (this.$refs.thumbnailElement) {
-				const thumbnailHeight =
-					this.$refs.thumbnailElement.naturalHeight;
-				const thumbnailWidth = this.$refs.thumbnailElement.naturalWidth;
-
-				this.thumbnailNotSquare = thumbnailHeight !== thumbnailWidth;
-				this.thumbnailHeight = thumbnailHeight;
-				this.thumbnailWidth = thumbnailWidth;
-			} else {
-				this.thumbnailNotSquare = false;
-				this.thumbnailHeight = null;
-				this.thumbnailWidth = null;
-			}
-		},
-		onThumbnailLoadError(error) {
-			this.thumbnailLoadError = error !== 0;
-		},
-		init() {
-			if (this.newSong && !this.youtubeId && !this.bulk) {
-				this.setSong({
-					youtubeId: "",
-					title: "",
-					artists: [],
-					genres: [],
-					tags: [],
-					duration: 0,
-					skipDuration: 0,
-					thumbnail: "",
-					verified: false
-				});
-				this.songDataLoaded = true;
-				this.showTab("youtube");
-			} else if (this.youtubeId) this.loadSong(this.youtubeId);
-			else if (!this.bulk) {
-				new Toast("You can't open EditSong without editing a song");
-				return this.closeModal("editSong");
-			}
+							if (song.value.duration === -1)
+								song.value.duration =
+									youtubeVideoDuration.value;
 
-			this.interval = setInterval(() => {
-				if (
-					this.song.duration !== -1 &&
-					this.video.paused === false &&
-					this.playerReady &&
-					(this.video.player.getCurrentTime() -
-						this.song.skipDuration >
-						this.song.duration ||
-						(this.video.player.getCurrentTime() > 0 &&
-							this.video.player.getCurrentTime() >=
-								this.video.player.getDuration()))
-				) {
-					this.stopVideo();
-					this.pauseVideo(true);
-					this.drawCanvas();
-				}
-				if (
-					this.playerReady &&
-					this.video.player.getVideoData &&
-					this.video.player.getVideoData() &&
-					this.video.player.getVideoData().video_id ===
-						this.song.youtubeId
-				) {
-					const currentTime = this.video.player.getCurrentTime();
-
-					if (currentTime !== undefined)
-						this.youtubeVideoCurrentTime = currentTime.toFixed(3);
-
-					if (this.youtubeVideoDuration.indexOf(".000") !== -1) {
-						const duration = this.video.player.getDuration();
-
-						if (duration !== undefined) {
-							if (
-								`${this.youtubeVideoDuration}` ===
-								`${Number(this.song.duration).toFixed(3)}`
-							)
-								this.song.duration = duration.toFixed(3);
+							youtubeDuration -= song.value.skipDuration;
+							if (song.value.duration > youtubeDuration + 1) {
+								stopVideo();
+								pauseVideo(true);
+								return new Toast(
+									"Video can't play. Specified duration is bigger than the YouTube song duration."
+								);
+							}
+							if (song.value.duration <= 0) {
+								stopVideo();
+								pauseVideo(true);
+								return new Toast(
+									"Video can't play. Specified duration has to be more than 0 seconds."
+								);
+							}
 
-							this.youtubeVideoDuration = duration.toFixed(3);
 							if (
-								this.youtubeVideoDuration.indexOf(".000") !== -1
-							)
-								this.youtubeVideoNote = "(~)";
-							else this.youtubeVideoNote = "";
+								video.value.player.getCurrentTime() <
+								song.value.skipDuration
+							) {
+								return seekTo(song.value.skipDuration);
+							}
 
-							this.drawCanvas();
+							setPlaybackRate(null);
+						} else if (event.data === 2) {
+							video.value.paused = true;
 						}
-					}
-				}
 
-				if (this.video.paused === false) this.drawCanvas();
-			}, 200);
-
-			if (window.YT && window.YT.Player) {
-				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.playerReady = true;
-
-								if (this.song && this.song.youtubeId)
-									this.video.player.cueVideoById(
-										this.song.youtubeId,
-										this.song.skipDuration
-									);
-
-								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);
-
-									if (
-										this.youtubeVideoDuration.indexOf(
-											".000"
-										) !== -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."
-										);
-									}
-
-									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;
-								}
-
-								return false;
-							}
-						}
+						return false;
 					}
-				);
-			} else {
-				this.youtubeError = true;
-				this.youtubeErrorMessage = "Player could not be loaded.";
+				}
 			}
+		);
+	} else {
+		youtubeError.value = true;
+		youtubeErrorMessage.value = "Player could not be loaded.";
+	}
 
-			["artists", "genres", "tags"].forEach(type => {
-				this.socket.dispatch(
-					`songs.get${type.charAt(0).toUpperCase()}${type.slice(1)}`,
-					res => {
-						if (res.status === "success") {
-							const { items } = res.data;
-							if (type === "genres")
-								this.autosuggest.allItems[type] = Array.from(
-									new Set([
-										...this.recommendedGenres,
-										...items
-									])
-								);
-							else this.autosuggest.allItems[type] = items;
-						} else {
-							new Toast(res.message);
-						}
-					}
-				);
-			});
+	["artists", "genres", "tags"].forEach(type => {
+		socket.dispatch(
+			`songs.get${type.charAt(0).toUpperCase()}${type.slice(1)}`,
+			res => {
+				if (res.status === "success") {
+					const { items } = res.data;
+					if (type === "genres")
+						autosuggest.value.allItems[type] = Array.from(
+							new Set([...recommendedGenres.value, ...items])
+						);
+					else autosuggest.value.allItems[type] = items;
+				} else {
+					new Toast(res.message);
+				}
+			}
+		);
+	});
 
-			return null;
-		},
-		unloadSong(youtubeId, songId) {
-			this.songDataLoaded = false;
-			this.songDeleted = false;
-			this.stopVideo();
-			this.pauseVideo(true);
-			this.resetSong(youtubeId);
-			this.thumbnailNotSquare = false;
-			this.thumbnailWidth = null;
-			this.thumbnailHeight = null;
-			this.youtubeVideoCurrentTime = "0.000";
-			this.youtubeVideoDuration = "0.000";
-			this.youtubeVideoNote = "";
-			if (songId)
-				this.socket.dispatch("apis.leaveRoom", `edit-song.${songId}`);
-			if (this.$refs.saveButton) this.$refs.saveButton.status = "default";
-		},
-		loadSong(youtubeId) {
-			console.log(`LOAD SONG ${youtubeId}`);
-			this.songNotFound = false;
-			this.socket.dispatch(
-				`songs.getSongsFromYoutubeIds`,
-				[youtubeId],
-				res => {
-					const { songs } = res.data;
-					if (res.status === "success" && songs.length > 0) {
-						let song = songs[0];
-						song = Object.assign(song, this.prefillData);
-
-						this.setSong(song);
-
-						this.songDataLoaded = true;
-
-						if (song._id)
-							this.socket.dispatch(
-								"apis.joinRoom",
-								`edit-song.${song._id}`
-							);
+	return null;
+};
 
-						if (
-							this.video.player &&
-							this.video.player.cueVideoById
-						) {
-							this.video.player.cueVideoById(
-								youtubeId,
-								song.skipDuration
-							);
-						}
-					} else {
-						new Toast("Song with that ID not found");
-						if (this.bulk) this.songNotFound = true;
-						if (!this.bulk) this.closeModal("editSong");
-					}
-				}
-			);
+const save = (songToCopy, closeOrNext, saveButtonRefName, _newSong = false) => {
+	const _song = JSON.parse(JSON.stringify(songToCopy));
 
-			if (!this.newSong)
-				this.socket.dispatch(
-					"reports.getReportsForSong",
-					this.song._id,
-					res => {
-						this.updateReports(res.data.reports);
-					}
-				);
-		},
-		importAlbum(result) {
-			this.selectDiscogsAlbum(result);
-			this.openModal("importAlbum");
-			this.closeModal("editSong");
-		},
-		save(songToCopy, closeOrNext, saveButtonRefName, newSong = false) {
-			const song = JSON.parse(JSON.stringify(songToCopy));
+	if (!newSong.value || props.bulk) emit("saving", _song.youtubeId);
 
-			if (!newSong || this.bulk) this.$emit("saving", song.youtubeId);
+	const saveButtonRef = saveButtonRefs.value[saveButtonRefName];
 
-			const saveButtonRef = this.$refs[saveButtonRefName];
+	if (!youtubeError.value && youtubeVideoDuration.value === "0.000") {
+		saveButtonRef.handleFailedSave();
+		if (!_newSong) emit("savedError", _song.youtubeId);
+		return new Toast("The video appears to not be working.");
+	}
 
-			if (!this.youtubeError && this.youtubeVideoDuration === "0.000") {
-				saveButtonRef.handleFailedSave();
-				if (!newSong) this.$emit("savedError", song.youtubeId);
-				return new Toast("The video appears to not be working.");
-			}
+	if (!_song.title) {
+		saveButtonRef.handleFailedSave();
+		if (!_newSong || props.bulk) emit("savedError", _song.youtubeId);
+		return new Toast("Please fill in all fields");
+	}
 
-			if (!song.title) {
-				saveButtonRef.handleFailedSave();
-				if (!newSong || this.bulk)
-					this.$emit("savedError", song.youtubeId);
-				return new Toast("Please fill in all fields");
-			}
+	if (!_song.thumbnail) {
+		saveButtonRef.handleFailedSave();
+		if (!_newSong || props.bulk) emit("savedError", _song.youtubeId);
+		return new Toast("Please fill in all fields");
+	}
 
-			if (!song.thumbnail) {
-				saveButtonRef.handleFailedSave();
-				if (!newSong || this.bulk)
-					this.$emit("savedError", song.youtubeId);
-				return new Toast("Please fill in all fields");
-			}
+	// const thumbnailHeight = thumbnailElement.value.naturalHeight;
+	// const thumbnailWidth = thumbnailElement.value.naturalWidth;
+
+	// if (thumbnailHeight < 80 || thumbnailWidth < 80) {
+	// 	saveButtonRef.handleFailedSave();
+	// 	return new Toast(
+	// 		"Thumbnail width and height must be at least 80px."
+	// 	);
+	// }
+
+	// if (thumbnailHeight > 4000 || thumbnailWidth > 4000) {
+	// 	saveButtonRef.handleFailedSave();
+	// 	return new Toast(
+	// 		"Thumbnail width and height must be less than 4000px."
+	// 	);
+	// }
+
+	// if (thumbnailHeight - thumbnailWidth > 5) {
+	// 	saveButtonRef.handleFailedSave();
+	// 	return new Toast("Thumbnail cannot be taller than it is wide.");
+	// }
+
+	// Youtube Id
+	if (
+		!_newSong &&
+		youtubeError.value &&
+		originalSong.value.youtubeId !== _song.youtubeId
+	) {
+		saveButtonRef.handleFailedSave();
+		if (!_newSong || props.bulk) emit("savedError", _song.youtubeId);
+		return new Toast(
+			"You're not allowed to change the YouTube id while the player is not working"
+		);
+	}
 
-			// const thumbnailHeight = this.$refs.thumbnailElement.naturalHeight;
-			// const thumbnailWidth = this.$refs.thumbnailElement.naturalWidth;
-
-			// if (thumbnailHeight < 80 || thumbnailWidth < 80) {
-			// 	saveButtonRef.handleFailedSave();
-			// 	return new Toast(
-			// 		"Thumbnail width and height must be at least 80px."
-			// 	);
-			// }
-
-			// if (thumbnailHeight > 4000 || thumbnailWidth > 4000) {
-			// 	saveButtonRef.handleFailedSave();
-			// 	return new Toast(
-			// 		"Thumbnail width and height must be less than 4000px."
-			// 	);
-			// }
-
-			// if (thumbnailHeight - thumbnailWidth > 5) {
-			// 	saveButtonRef.handleFailedSave();
-			// 	return new Toast("Thumbnail cannot be taller than it is wide.");
-			// }
-
-			// Youtube Id
-			if (
-				!newSong &&
-				this.youtubeError &&
-				this.originalSong.youtubeId !== song.youtubeId
-			) {
-				saveButtonRef.handleFailedSave();
-				if (!newSong || this.bulk)
-					this.$emit("savedError", song.youtubeId);
-				return new Toast(
-					"You're not allowed to change the YouTube id while the player is not working"
-				);
-			}
+	// Duration
+	if (
+		Number(_song.skipDuration) + Number(_song.duration) >
+			youtubeVideoDuration.value &&
+		(((!_newSong || props.bulk) && !youtubeError.value) ||
+			originalSong.value.duration !== _song.duration)
+	) {
+		saveButtonRef.handleFailedSave();
+		if (!_newSong || props.bulk) emit("savedError", _song.youtubeId);
+		return new Toast(
+			"Duration can't be higher than the length of the video"
+		);
+	}
 
-			// Duration
-			if (
-				Number(song.skipDuration) + Number(song.duration) >
-					this.youtubeVideoDuration &&
-				(((!newSong || this.bulk) && !this.youtubeError) ||
-					this.originalSong.duration !== song.duration)
-			) {
-				saveButtonRef.handleFailedSave();
-				if (!newSong || this.bulk)
-					this.$emit("savedError", song.youtubeId);
-				return new Toast(
-					"Duration can't be higher than the length of the video"
-				);
-			}
+	// Title
+	if (!validation.isLength(_song.title, 1, 100)) {
+		saveButtonRef.handleFailedSave();
+		if (!_newSong || props.bulk) emit("savedError", _song.youtubeId);
+		return new Toast("Title must have between 1 and 100 characters.");
+	}
 
-			// Title
-			if (!validation.isLength(song.title, 1, 100)) {
-				saveButtonRef.handleFailedSave();
-				if (!newSong || this.bulk)
-					this.$emit("savedError", song.youtubeId);
-				return new Toast(
-					"Title must have between 1 and 100 characters."
-				);
-			}
+	// Artists
+	if (
+		(_song.verified && _song.artists.length < 1) ||
+		_song.artists.length > 10
+	) {
+		saveButtonRef.handleFailedSave();
+		if (!_newSong || props.bulk) emit("savedError", _song.youtubeId);
+		return new Toast(
+			"Invalid artists. You must have at least 1 artist and a maximum of 10 artists."
+		);
+	}
 
-			// Artists
-			if (
-				(song.verified && song.artists.length < 1) ||
-				song.artists.length > 10
-			) {
-				saveButtonRef.handleFailedSave();
-				if (!newSong || this.bulk)
-					this.$emit("savedError", song.youtubeId);
-				return new Toast(
-					"Invalid artists. You must have at least 1 artist and a maximum of 10 artists."
-				);
-			}
+	let error;
+	_song.artists.forEach(artist => {
+		if (!validation.isLength(artist, 1, 64)) {
+			error = "Artist must have between 1 and 64 characters.";
+			return error;
+		}
+		if (artist === "NONE") {
+			error =
+				'Invalid artist format. Artists are not allowed to be named "NONE".';
+			return error;
+		}
 
-			let error;
-			song.artists.forEach(artist => {
-				if (!validation.isLength(artist, 1, 64)) {
-					error = "Artist must have between 1 and 64 characters.";
-					return error;
-				}
-				if (artist === "NONE") {
-					error =
-						'Invalid artist format. Artists are not allowed to be named "NONE".';
-					return error;
-				}
+		return false;
+	});
 
-				return false;
-			});
+	if (error) {
+		saveButtonRef.handleFailedSave();
+		if (!_newSong || props.bulk) emit("savedError", _song.youtubeId);
+		return new Toast(error);
+	}
 
-			if (error) {
-				saveButtonRef.handleFailedSave();
-				if (!newSong || this.bulk)
-					this.$emit("savedError", song.youtubeId);
-				return new Toast(error);
+	// Genres
+	error = undefined;
+	if (_song.verified && _song.genres.length < 1)
+		_song.genres.forEach(genre => {
+			if (!validation.isLength(genre, 1, 32)) {
+				error = "Genre must have between 1 and 32 characters.";
+				return error;
+			}
+			if (!validation.regex.ascii.test(genre)) {
+				error =
+					"Invalid genre format. Only ascii characters are allowed.";
+				return error;
 			}
 
-			// Genres
-			error = undefined;
-			if (song.verified && song.genres.length < 1)
-				song.genres.forEach(genre => {
-					if (!validation.isLength(genre, 1, 32)) {
-						error = "Genre must have between 1 and 32 characters.";
-						return error;
-					}
-					if (!validation.regex.ascii.test(genre)) {
-						error =
-							"Invalid genre format. Only ascii characters are allowed.";
-						return error;
-					}
+			return false;
+		});
 
-					return false;
-				});
+	if ((_song.verified && _song.genres.length < 1) || _song.genres.length > 16)
+		error = "You must have between 1 and 16 genres.";
 
-			if (
-				(song.verified && song.genres.length < 1) ||
-				song.genres.length > 16
+	if (error) {
+		saveButtonRef.handleFailedSave();
+		if (!_newSong || props.bulk) emit("savedError", _song.youtubeId);
+		return new Toast(error);
+	}
+
+	error = undefined;
+	_song.tags.forEach(tag => {
+		if (
+			!/^[a-zA-Z0-9_]{1,64}$|^[a-zA-Z0-9_]{1,64}\[[a-zA-Z0-9_]{1,64}\]$/.test(
+				tag
 			)
-				error = "You must have between 1 and 16 genres.";
+		) {
+			error = "Invalid tag format.";
+			return error;
+		}
 
-			if (error) {
-				saveButtonRef.handleFailedSave();
-				if (!newSong || this.bulk)
-					this.$emit("savedError", song.youtubeId);
-				return new Toast(error);
-			}
+		return false;
+	});
 
-			error = undefined;
-			song.tags.forEach(tag => {
-				if (
-					!/^[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;
-				}
+	if (error) {
+		saveButtonRef.handleFailedSave();
+		if (!_newSong || props.bulk) emit("savedError", _song.youtubeId);
+		return new Toast(error);
+	}
 
-				return false;
-			});
+	// Thumbnail
+	if (!validation.isLength(_song.thumbnail, 1, 256)) {
+		saveButtonRef.handleFailedSave();
+		if (!_newSong || props.bulk) emit("savedError", _song.youtubeId);
+		return new Toast("Thumbnail must have between 8 and 256 characters.");
+	}
+	if (useHTTPS.value && _song.thumbnail.indexOf("https://") !== 0) {
+		saveButtonRef.handleFailedSave();
+		if (!_newSong || props.bulk) emit("savedError", _song.youtubeId);
+		return new Toast('Thumbnail must start with "https://".');
+	}
 
-			if (error) {
-				saveButtonRef.handleFailedSave();
-				if (!newSong || this.bulk)
-					this.$emit("savedError", song.youtubeId);
-				return new Toast(error);
-			}
+	if (
+		!useHTTPS.value &&
+		_song.thumbnail.indexOf("http://") !== 0 &&
+		_song.thumbnail.indexOf("https://") !== 0
+	) {
+		saveButtonRef.handleFailedSave();
+		if (!_newSong || props.bulk) emit("savedError", _song.youtubeId);
+		return new Toast('Thumbnail must start with "http://".');
+	}
 
-			// Thumbnail
-			if (!validation.isLength(song.thumbnail, 1, 256)) {
-				saveButtonRef.handleFailedSave();
-				if (!newSong || this.bulk)
-					this.$emit("savedError", song.youtubeId);
-				return new Toast(
-					"Thumbnail must have between 8 and 256 characters."
-				);
-			}
-			if (this.useHTTPS && song.thumbnail.indexOf("https://") !== 0) {
+	saveButtonRef.status = "saving";
+
+	if (_newSong)
+		return socket.dispatch(`songs.create`, _song, res => {
+			new Toast(res.message);
+
+			if (res.status === "error") {
 				saveButtonRef.handleFailedSave();
-				if (!newSong || this.bulk)
-					this.$emit("savedError", song.youtubeId);
-				return new Toast('Thumbnail must start with "https://".');
+				emit("savedError", _song.youtubeId);
+				return;
 			}
 
-			if (
-				!this.useHTTPS &&
-				song.thumbnail.indexOf("http://") !== 0 &&
-				song.thumbnail.indexOf("https://") !== 0
-			) {
-				saveButtonRef.handleFailedSave();
-				if (!newSong || this.bulk)
-					this.$emit("savedError", song.youtubeId);
-				return new Toast('Thumbnail must start with "http://".');
+			saveButtonRef.handleSuccessfulSave();
+			emit("savedSuccess", _song.youtubeId);
+
+			if (!closeOrNext) {
+				loadSong(_song.youtubeId);
+				return;
 			}
 
-			saveButtonRef.status = "saving";
+			if (props.bulk) emit("nextSong");
+			else closeCurrentModal();
+		});
+	return socket.dispatch(`songs.update`, _song._id, _song, res => {
+		new Toast(res.message);
 
-			if (newSong)
-				return this.socket.dispatch(`songs.create`, song, res => {
-					new Toast(res.message);
+		if (res.status === "error") {
+			saveButtonRef.handleFailedSave();
+			emit("savedError", _song.youtubeId);
+			return;
+		}
 
-					if (res.status === "error") {
-						saveButtonRef.handleFailedSave();
-						this.$emit("savedError", song.youtubeId);
-						return;
-					}
+		updateOriginalSong(_song);
 
-					saveButtonRef.handleSuccessfulSave();
-					this.$emit("savedSuccess", song.youtubeId);
+		saveButtonRef.handleSuccessfulSave();
+		emit("savedSuccess", _song.youtubeId);
 
-					if (!closeOrNext) {
-						this.loadSong(song.youtubeId);
-						return;
-					}
+		if (!closeOrNext) return;
 
-					if (this.bulk) this.$emit("nextSong");
-					else this.closeModal("editSong");
-				});
-			return this.socket.dispatch(`songs.update`, song._id, song, res => {
-				new Toast(res.message);
+		if (props.bulk) emit("nextSong");
+		else closeCurrentModal();
+	});
+};
 
-				if (res.status === "error") {
-					saveButtonRef.handleFailedSave();
-					this.$emit("savedError", song.youtubeId);
-					return;
-				}
+const editNextSong = () => {
+	emit("nextSong");
+};
 
-				this.updateOriginalSong(song);
+const toggleFlag = () => {
+	emit("toggleFlag");
+};
 
-				saveButtonRef.handleSuccessfulSave();
-				this.$emit("savedSuccess", song.youtubeId);
+const getAlbumData = type => {
+	if (!song.value.discogs) return;
+	if (type === "title")
+		updateSongField({
+			field: "title",
+			value: song.value.discogs.track.title
+		});
+	if (type === "albumArt")
+		updateSongField({
+			field: "thumbnail",
+			value: song.value.discogs.album.albumArt
+		});
+	if (type === "genres")
+		updateSongField({
+			field: "genres",
+			value: JSON.parse(JSON.stringify(song.value.discogs.album.genres))
+		});
+	if (type === "artists")
+		updateSongField({
+			field: "artists",
+			value: JSON.parse(JSON.stringify(song.value.discogs.album.artists))
+		});
+};
 
-				if (!closeOrNext) return;
+const getYouTubeData = type => {
+	if (type === "title") {
+		try {
+			const { title } = video.value.player.getVideoData();
 
-				if (this.bulk) this.$emit("nextSong");
-				else this.closeModal("editSong");
-			});
-		},
-		editNextSong() {
-			this.$emit("nextSong");
-		},
-		toggleFlag() {
-			this.$emit("toggleFlag");
-		},
-		getAlbumData(type) {
-			if (!this.song.discogs) return;
-			if (type === "title")
-				this.updateSongField({
+			if (title)
+				updateSongField({
 					field: "title",
-					value: this.song.discogs.track.title
-				});
-			if (type === "albumArt")
-				this.updateSongField({
-					field: "thumbnail",
-					value: this.song.discogs.album.albumArt
-				});
-			if (type === "genres")
-				this.updateSongField({
-					field: "genres",
-					value: JSON.parse(
-						JSON.stringify(this.song.discogs.album.genres)
-					)
-				});
-			if (type === "artists")
-				this.updateSongField({
-					field: "artists",
-					value: JSON.parse(
-						JSON.stringify(this.song.discogs.album.artists)
-					)
-				});
-		},
-		getYouTubeData(type) {
-			if (type === "title") {
-				try {
-					const { title } = this.video.player.getVideoData();
-
-					if (title)
-						this.updateSongField({
-							field: "title",
-							value: title
-						});
-					else throw new Error("No title found");
-				} catch (e) {
-					new Toast(
-						"Unable to fetch YouTube video title. Try starting the video."
-					);
-				}
-			}
-			if (type === "thumbnail")
-				this.updateSongField({
-					field: "thumbnail",
-					value: `https://img.youtube.com/vi/${this.song.youtubeId}/mqdefault.jpg`
+					value: title
 				});
-			if (type === "author") {
-				try {
-					const { author } = this.video.player.getVideoData();
-
-					if (author) this.artistInputValue = author;
-					else throw new Error("No video author found");
-				} catch (e) {
-					new Toast(
-						"Unable to fetch YouTube video author. Try starting the video."
-					);
-				}
-			}
-		},
-		fillDuration() {
-			this.song.duration =
-				this.youtubeVideoDuration - this.song.skipDuration;
-		},
-		settings(type) {
-			switch (type) {
-				case "stop":
-					this.stopVideo();
-					this.pauseVideo(true);
-					break;
-				case "hardStop":
-					this.hardStopVideo();
-					this.pauseVideo(true);
-					break;
-				case "pause":
-					this.pauseVideo(true);
-					break;
-				case "play":
-					this.pauseVideo(false);
-					break;
-				case "skipToLast10Secs":
-					this.seekTo(
-						this.song.duration - 10 + this.song.skipDuration
-					);
-					break;
-				default:
-					break;
-			}
-		},
-		play() {
-			if (
-				this.video.player.getVideoData().video_id !==
-				this.song.youtubeId
-			) {
-				this.song.duration = -1;
-				this.loadVideoById(this.song.youtubeId, this.song.skipDuration);
-			}
-			this.settings("play");
-		},
-		seekTo(position) {
-			this.settings("play");
-			this.video.player.seekTo(position);
-		},
-		changeVolume() {
-			const volume = this.volumeSliderValue;
-			localStorage.setItem("volume", volume);
-			this.video.player.setVolume(volume);
-			if (volume > 0) {
-				this.video.player.unMute();
-				this.muted = false;
-			}
-		},
-		toggleMute() {
-			const previousVolume = parseFloat(localStorage.getItem("volume"));
-			const volume =
-				this.video.player.getVolume() <= 0 ? previousVolume : 0;
-			this.muted = !this.muted;
-			this.volumeSliderValue = volume;
-			this.video.player.setVolume(volume);
-			if (!this.muted) localStorage.setItem("volume", volume);
-		},
-		increaseVolume() {
-			const previousVolume = parseFloat(localStorage.getItem("volume"));
-			let volume = previousVolume + 5;
-			this.muted = false;
-			if (volume > 100) volume = 100;
-			this.volumeSliderValue = volume;
-			this.video.player.setVolume(volume);
-			localStorage.setItem("volume", volume);
-		},
-		addTag(type, value) {
-			if (type === "genres") {
-				const genre = value || this.genreInputValue.trim();
-
-				if (
-					this.song.genres
-						.map(genre => genre.toLowerCase())
-						.indexOf(genre.toLowerCase()) !== -1
-				)
-					return new Toast("Genre already exists");
-				if (genre) {
-					this.song.genres.push(genre);
-					this.genreInputValue = "";
-					return false;
-				}
+			else throw new Error("No title found");
+		} catch (e) {
+			new Toast(
+				"Unable to fetch YouTube video title. Try starting the video."
+			);
+		}
+	}
+	if (type === "thumbnail")
+		updateSongField({
+			field: "thumbnail",
+			value: `https://img.youtube.com/vi/${song.value.youtubeId}/mqdefault.jpg`
+		});
+	if (type === "author") {
+		try {
+			const { author } = video.value.player.getVideoData();
+
+			if (author) artistInputValue.value = author;
+			else throw new Error("No video author found");
+		} catch (e) {
+			new Toast(
+				"Unable to fetch YouTube video author. Try starting the video."
+			);
+		}
+	}
+};
 
-				return new Toast("Genre cannot be empty");
-			}
-			if (type === "artists") {
-				const artist = value || this.artistInputValue;
-				if (this.song.artists.indexOf(artist) !== -1)
-					return new Toast("Artist already exists");
-				if (artist !== "") {
-					this.song.artists.push(artist);
-					this.artistInputValue = "";
-					return false;
-				}
-				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");
-			}
+const fillDuration = () => {
+	song.value.duration = youtubeVideoDuration.value - song.value.skipDuration;
+};
+
+const settings = type => {
+	switch (type) {
+		case "stop":
+			stopVideo();
+			pauseVideo(true);
+			break;
+		case "hardStop":
+			hardStopVideo();
+			pauseVideo(true);
+			break;
+		case "pause":
+			pauseVideo(true);
+			break;
+		case "play":
+			pauseVideo(false);
+			break;
+		case "skipToLast10Secs":
+			seekTo(song.value.duration - 10 + song.value.skipDuration);
+			break;
+		default:
+			break;
+	}
+};
+
+const play = () => {
+	if (video.value.player.getVideoData().video_id !== song.value.youtubeId) {
+		song.value.duration = -1;
+		loadVideoById(song.value.youtubeId, song.value.skipDuration);
+	}
+	settings("play");
+};
+
+const changeVolume = () => {
+	const volume = volumeSliderValue.value;
+	localStorage.setItem("volume", volume);
+	video.value.player.setVolume(volume);
+	if (volume > 0) {
+		video.value.player.unMute();
+		muted.value = false;
+	}
+};
 
+const toggleMute = () => {
+	const previousVolume = parseFloat(localStorage.getItem("volume"));
+	const volume = video.value.player.getVolume() <= 0 ? previousVolume : 0;
+	muted.value = !muted.value;
+	volumeSliderValue.value = volume;
+	video.value.player.setVolume(volume);
+	if (!muted.value) localStorage.setItem("volume", volume);
+};
+
+const addTag = (type, value) => {
+	if (type === "genres") {
+		const genre = value || genreInputValue.value.trim();
+
+		if (
+			song.value.genres
+				.map(genre => genre.toLowerCase())
+				.indexOf(genre.toLowerCase()) !== -1
+		)
+			return new Toast("Genre already exists");
+		if (genre) {
+			song.value.genres.push(genre);
+			genreInputValue.value = "";
 			return false;
-		},
-		removeTag(type, value) {
-			if (type === "genres")
-				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-${this.modalUuid}`];
-			if (!this.songDataLoaded || !canvasElement) return;
-			const ctx = canvasElement.getContext("2d");
-
-			const videoDuration = Number(this.youtubeVideoDuration);
-
-			const skipDuration = Number(this.song.skipDuration);
-			const duration = Number(this.song.duration);
-			const afterDuration = videoDuration - (skipDuration + duration);
-
-			const width = 530;
-
-			const currentTime =
-				this.video.player && this.video.player.getCurrentTime
-					? this.video.player.getCurrentTime()
-					: 0;
-
-			const widthSkipDuration = (skipDuration / videoDuration) * width;
-			const widthDuration = (duration / videoDuration) * width;
-			const widthAfterDuration = (afterDuration / videoDuration) * width;
-
-			const widthCurrentTime = (currentTime / videoDuration) * width;
-
-			const skipDurationColor = "#F42003";
-			const durationColor = "#03A9F4";
-			const afterDurationColor = "#41E841";
-			const currentDurationColor = "#3b25e8";
-
-			ctx.fillStyle = skipDurationColor;
-			ctx.fillRect(0, 0, widthSkipDuration, 20);
-			ctx.fillStyle = durationColor;
-			ctx.fillRect(widthSkipDuration, 0, widthDuration, 20);
-			ctx.fillStyle = afterDurationColor;
-			ctx.fillRect(
-				widthSkipDuration + widthDuration,
-				0,
-				widthAfterDuration,
-				20
-			);
+		}
 
-			ctx.fillStyle = currentDurationColor;
-			ctx.fillRect(widthCurrentTime, 0, 1, 20);
-		},
-		setTrackPosition(event) {
-			this.seekTo(
-				Number(
-					Number(this.video.player.getDuration()) *
-						((event.pageX -
-							event.target.getBoundingClientRect().left) /
-							530)
-				)
-			);
-		},
-		toggleGenreHelper() {
-			this.$refs.genreHelper.toggleBox();
-		},
-		resetGenreHelper() {
-			this.$refs.genreHelper.resetBox();
-		},
-		sendActivityWatchVideoData() {
-			if (!this.video.paused) {
-				if (this.activityWatchVideoLastStatus !== "playing") {
-					this.activityWatchVideoLastStatus = "playing";
-					if (
-						this.song.skipDuration > 0 &&
-						parseFloat(this.youtubeVideoCurrentTime) === 0
-					) {
-						this.activityWatchVideoLastStartDuration = Math.floor(
-							this.song.skipDuration +
-								parseFloat(this.youtubeVideoCurrentTime)
-						);
-					} else {
-						this.activityWatchVideoLastStartDuration = Math.floor(
-							parseFloat(this.youtubeVideoCurrentTime)
-						);
-					}
-				}
+		return new Toast("Genre cannot be empty");
+	}
+	if (type === "artists") {
+		const artist = value || artistInputValue.value;
+		if (song.value.artists.indexOf(artist) !== -1)
+			return new Toast("Artist already exists");
+		if (artist !== "") {
+			song.value.artists.push(artist);
+			artistInputValue.value = "";
+			return false;
+		}
+		return new Toast("Artist cannot be empty");
+	}
+	if (type === "tags") {
+		const tag = value || tagInputValue.value;
+		if (song.value.tags.indexOf(tag) !== -1)
+			return new Toast("Tag already exists");
+		if (tag !== "") {
+			song.value.tags.push(tag);
+			tagInputValue.value = "";
+			return false;
+		}
+		return new Toast("Tag cannot be empty");
+	}
 
-				const videoData = {
-					title: this.song.title,
-					artists: this.song.artists
-						? this.song.artists.join(", ")
-						: null,
-					youtubeId: this.song.youtubeId,
-					muted: this.muted,
-					volume: this.volumeSliderValue,
-					startedDuration:
-						this.activityWatchVideoLastStartDuration <= 0
-							? 0
-							: this.activityWatchVideoLastStartDuration,
-					source: `editSong#${this.song.youtubeId}`,
-					hostname: window.location.hostname
-				};
-
-				aw.sendVideoData(videoData);
-			} else {
-				this.activityWatchVideoLastStatus = "not_playing";
-			}
-		},
-		remove(id) {
-			this.socket.dispatch("songs.remove", id, res => {
-				new Toast(res.message);
-			});
-		},
-		confirmAction({ message, action, params }) {
-			this.openModal({
-				modal: "confirm",
-				data: {
-					message,
-					action,
-					params,
-					onCompleted: this.handleConfirmed
-				}
-			});
-		},
-		handleConfirmed({ action, params }) {
-			if (typeof this[action] === "function") {
-				if (params) this[action](params);
-				else this[action]();
-			}
-		},
-		onCloseModal() {
-			const songStringified = JSON.stringify({
-				...this.song,
-				...{
-					duration: Number(this.song.duration).toFixed(3)
-				}
-			});
-			const originalSongStringified = JSON.stringify({
-				...this.originalSong,
-				...{
-					duration: Number(this.originalSong.duration).toFixed(3)
-				}
-			});
-			const unsavedChanges = songStringified !== originalSongStringified;
-
-			if (unsavedChanges) {
-				return this.confirmAction({
-					message:
-						"You have unsaved changes. Are you sure you want to discard unsaved changes?",
-					action: "closeThisModal",
-					params: null
-				});
-			}
+	return false;
+};
 
-			return this.closeThisModal();
-		},
-		closeThisModal() {
-			if (this.bulk) this.$emit("close");
-			else this.closeModal("editSong");
-		},
-		...mapActions("modals/importAlbum", ["selectDiscogsAlbum"]),
-		...mapActions({
-			showTab(dispatch, payload) {
-				if (this.$refs[`${payload}-tab`])
-					this.$refs[`${payload}-tab`].scrollIntoView({
-						block: "nearest"
-					});
-				return dispatch(
-					`${this.modalModulePath.replace(
-						"MODAL_UUID",
-						this.modalUuid
-					)}/showTab`,
-					payload
+const removeTag = (type, value) => {
+	if (type === "genres")
+		song.value.genres.splice(song.value.genres.indexOf(value), 1);
+	else if (type === "artists")
+		song.value.artists.splice(song.value.artists.indexOf(value), 1);
+	else if (type === "tags")
+		song.value.tags.splice(song.value.tags.indexOf(value), 1);
+};
+
+const setTrackPosition = event => {
+	seekTo(
+		Number(
+			Number(video.value.player.getDuration()) *
+				((event.pageX - event.target.getBoundingClientRect().left) /
+					530)
+		)
+	);
+};
+
+const toggleGenreHelper = () => {
+	genreHelper.value.toggleBox();
+};
+
+const resetGenreHelper = () => {
+	genreHelper.value.resetBox();
+};
+
+const sendActivityWatchVideoData = () => {
+	if (!video.value.paused) {
+		if (activityWatchVideoLastStatus.value !== "playing") {
+			activityWatchVideoLastStatus.value = "playing";
+			if (
+				song.value.skipDuration > 0 &&
+				parseFloat(youtubeVideoCurrentTime.value) === 0
+			) {
+				activityWatchVideoLastStartDuration.value = Math.floor(
+					song.value.skipDuration +
+						parseFloat(youtubeVideoCurrentTime.value)
+				);
+			} else {
+				activityWatchVideoLastStartDuration.value = Math.floor(
+					parseFloat(youtubeVideoCurrentTime.value)
 				);
 			}
-		}),
-		...mapModalActions("MODAL_MODULE_PATH", [
-			"stopVideo",
-			"hardStopVideo",
-			"loadVideoById",
-			"pauseVideo",
-			"getCurrentTime",
-			"setSong",
-			"resetSong",
-			"updateOriginalSong",
-			"updateSongField",
-			"updateReports",
-			"setPlaybackRate"
-		]),
-		...mapActions("modalVisibility", ["closeModal", "openModal"])
+		}
+
+		const videoData = {
+			title: song.value.title,
+			artists: song.value.artists ? song.value.artists.join(", ") : null,
+			youtubeId: song.value.youtubeId,
+			muted: muted.value,
+			volume: volumeSliderValue.value,
+			startedDuration:
+				activityWatchVideoLastStartDuration.value <= 0
+					? 0
+					: activityWatchVideoLastStartDuration.value,
+			source: `editSong#${song.value.youtubeId}`,
+			hostname: window.location.hostname
+		};
+
+		aw.sendVideoData(videoData);
+	} else {
+		activityWatchVideoLastStatus.value = "not_playing";
 	}
 };
-</script>
+
+const remove = id => {
+	socket.dispatch("songs.remove", id, res => {
+		new Toast(res.message);
+	});
+};
+
+const handleConfirmed = ({ action, params }) => {
+	if (typeof action === "function") {
+		if (params) action(params);
+		else action();
+	}
+};
+
+const confirmAction = ({ message, action, params }) => {
+	openModal({
+		modal: "confirm",
+		data: {
+			message,
+			action,
+			params,
+			onCompleted: handleConfirmed
+		}
+	});
+};
+
+const onCloseModal = () => {
+	const songStringified = JSON.stringify({
+		...song.value,
+		...{
+			duration: Number(song.value.duration).toFixed(3)
+		}
+	});
+	const originalSongStringified = JSON.stringify({
+		...originalSong.value,
+		...{
+			duration: Number(originalSong.value.duration).toFixed(3)
+		}
+	});
+	const unsavedChanges = songStringified !== originalSongStringified;
+
+	if (unsavedChanges) {
+		return confirmAction({
+			message:
+				"You have unsaved changes. Are you sure you want to discard unsaved changes?",
+			action: closeCurrentModal,
+			params: null
+		});
+	}
+
+	return closeCurrentModal();
+};
+
+watch(
+	() => song.value.duration,
+	() => drawCanvas()
+);
+watch(
+	() => song.value.skipDuration,
+	() => drawCanvas()
+);
+watch(
+	() => youtubeId.value,
+	(_youtubeId, _oldYoutubeId) => {
+		console.log("NEW YOUTUBE ID", _youtubeId);
+		unloadSong(_oldYoutubeId);
+		loadSong(_youtubeId);
+	}
+);
+
+onMounted(async () => {
+	activityWatchVideoDataInterval.value = setInterval(() => {
+		sendActivityWatchVideoData();
+	}, 1000);
+
+	useHTTPS.value = await lofig.get("cookie.secure");
+
+	ws.onConnect(init);
+
+	let volume = parseFloat(localStorage.getItem("volume"));
+	volume = typeof volume === "number" && !Number.isNaN(volume) ? volume : 20;
+	localStorage.setItem("volume", volume);
+	volumeSliderValue.value = volume;
+
+	socket.on(
+		"event:admin.song.removed",
+		res => {
+			if (res.data.songId === song.value._id) {
+				songDeleted.value = true;
+			}
+		},
+		{ modalUuid: props.modalUuid }
+	);
+
+	keyboardShortcuts.registerShortcut("editSong.pauseResumeVideo", {
+		keyCode: 101,
+		preventDefault: true,
+		handler: () => {
+			if (video.value.paused) play();
+			else settings("pause");
+		}
+	});
+
+	keyboardShortcuts.registerShortcut("editSong.stopVideo", {
+		keyCode: 101,
+		ctrl: true,
+		preventDefault: true,
+		handler: () => {
+			settings("stop");
+		}
+	});
+
+	keyboardShortcuts.registerShortcut("editSong.hardStopVideo", {
+		keyCode: 101,
+		ctrl: true,
+		shift: true,
+		preventDefault: true,
+		handler: () => {
+			settings("hardStop");
+		}
+	});
+
+	keyboardShortcuts.registerShortcut("editSong.skipToLast10Secs", {
+		keyCode: 102,
+		preventDefault: true,
+		handler: () => {
+			settings("skipToLast10Secs");
+		}
+	});
+
+	keyboardShortcuts.registerShortcut("editSong.lowerVolumeLarge", {
+		keyCode: 98,
+		preventDefault: true,
+		handler: () => {
+			volumeSliderValue.value = Math.max(0, volumeSliderValue.value - 10);
+			changeVolume();
+		}
+	});
+
+	keyboardShortcuts.registerShortcut("editSong.lowerVolumeSmall", {
+		keyCode: 98,
+		ctrl: true,
+		preventDefault: true,
+		handler: () => {
+			volumeSliderValue.value = Math.max(0, volumeSliderValue.value - 1);
+			changeVolume();
+		}
+	});
+
+	keyboardShortcuts.registerShortcut("editSong.increaseVolumeLarge", {
+		keyCode: 104,
+		preventDefault: true,
+		handler: () => {
+			volumeSliderValue.value = Math.min(
+				100,
+				volumeSliderValue.value + 10
+			);
+			changeVolume();
+		}
+	});
+
+	keyboardShortcuts.registerShortcut("editSong.increaseVolumeSmall", {
+		keyCode: 104,
+		ctrl: true,
+		preventDefault: true,
+		handler: () => {
+			volumeSliderValue.value = Math.min(
+				100,
+				volumeSliderValue.value + 1
+			);
+			changeVolume();
+		}
+	});
+
+	keyboardShortcuts.registerShortcut("editSong.save", {
+		keyCode: 83,
+		ctrl: true,
+		preventDefault: true,
+		handler: () => {
+			save(song.value, false, "saveButton");
+		}
+	});
+
+	keyboardShortcuts.registerShortcut("editSong.saveClose", {
+		keyCode: 83,
+		ctrl: true,
+		alt: true,
+		preventDefault: true,
+		handler: () => {
+			save(song.value, true, "saveAndCloseButton");
+		}
+	});
+
+	keyboardShortcuts.registerShortcut("editSong.focusTitle", {
+		keyCode: 36,
+		preventDefault: true,
+		handler: () => {
+			inputs.value["title-input"].focus();
+		}
+	});
+
+	keyboardShortcuts.registerShortcut("editSong.useAllDiscogs", {
+		keyCode: 68,
+		alt: true,
+		ctrl: true,
+		preventDefault: true,
+		handler: () => {
+			getAlbumData("title");
+			getAlbumData("albumArt");
+			getAlbumData("artists");
+			getAlbumData("genres");
+		}
+	});
+
+	keyboardShortcuts.registerShortcut("editSong.closeModal", {
+		keyCode: 27,
+		handler: () => {
+			if (
+				modals.value[
+					activeModals.value[activeModals.value.length - 1]
+				] === "editSong" ||
+				modals.value[
+					activeModals.value[activeModals.value.length - 1]
+				] === "editSongs"
+			) {
+				onCloseModal();
+			}
+		}
+	});
+
+	/*
+
+	editSong.pauseResume - Num 5 - Pause/resume song
+	editSong.stopVideo - Ctrl - Num 5 - Stop
+	editSong.hardStopVideo - Shift - Ctrl - Num 5 - Stop
+	editSong.skipToLast10Secs - Num 6 - Skip to last 10 seconds
+
+	editSong.lowerVolumeLarge - Num 2 - Volume down by 10
+	editSong.lowerVolumeSmall - Ctrl - Num 2 - Volume down by 1
+	editSong.increaseVolumeLarge - Num 8 - Volume up by 10
+	editSong.increaseVolumeSmall - Ctrl - Num 8 - Volume up by 1
+
+	editSong.focusTitle - Home - Focus the title input
+	editSong.focusDicogs - End - Focus the discogs input
+
+	editSong.save - Ctrl - S - Saves song
+	editSong.save - Ctrl - Alt - S - Saves song and closes the modal
+	editSong.save - Ctrl - Alt - V - Saves song, verifies songs and then closes the modal
+	editSong.close - F4 - Closes modal without saving
+
+	editSong.useAllDiscogs - Ctrl - Alt - D - Sets all fields to the Discogs data
+
+	Inside Discogs inputs: Ctrl - D - Sets this field to the Discogs data
+
+	*/
+});
+
+onBeforeUnmount(() => {
+	unloadSong(youtubeId.value, song.value._id);
+
+	playerReady.value = false;
+	clearInterval(interval.value);
+	clearInterval(activityWatchVideoDataInterval.value);
+
+	const shortcutNames = [
+		"editSong.pauseResume",
+		"editSong.stopVideo",
+		"editSong.hardStopVideo",
+		"editSong.skipToLast10Secs",
+		"editSong.lowerVolumeLarge",
+		"editSong.lowerVolumeSmall",
+		"editSong.increaseVolumeLarge",
+		"editSong.increaseVolumeSmall",
+		"editSong.focusTitle",
+		"editSong.focusDicogs",
+		"editSong.save",
+		"editSong.saveClose",
+		"editSong.useAllDiscogs",
+		"editSong.closeModal"
+	];
+
+	shortcutNames.forEach(shortcutName => {
+		keyboardShortcuts.unregisterShortcut(shortcutName);
+	});
+
+	if (!props.bulk) {
+		// Delete the VueX module that was created for this modal, after all other cleanup tasks are performed
+		store.unregisterModule(["modals", "editSong", props.modalUuid]);
+	} else {
+		store.unregisterModule([
+			"modals",
+			"editSongs",
+			props.modalUuid,
+			"editSong"
+		]);
+	}
+});
+</script>
+
+<template>
+	<div>
+		<modal
+			:title="`${newSong ? 'Create' : 'Edit'} Song`"
+			class="song-modal"
+			:size="'wide'"
+			:split="true"
+			:intercept-close="true"
+			@close="onCloseModal"
+		>
+			<template #toggleMobileSidebar>
+				<slot name="toggleMobileSidebar" />
+			</template>
+			<template #sidebar>
+				<slot name="sidebar" />
+			</template>
+			<template #body>
+				<div v-if="!youtubeId && !newSong" class="notice-container">
+					<h4>No song has been selected</h4>
+				</div>
+				<div v-if="songDeleted" class="notice-container">
+					<h4>The song you were editing has been deleted</h4>
+				</div>
+				<div
+					v-if="
+						youtubeId &&
+						!songDataLoaded &&
+						!songNotFound &&
+						!newSong
+					"
+					class="notice-container"
+				>
+					<h4>Song hasn't loaded yet</h4>
+				</div>
+				<div
+					v-if="youtubeId && songNotFound && !newSong"
+					class="notice-container"
+				>
+					<h4>Song was not found</h4>
+				</div>
+				<div
+					class="left-section"
+					v-show="songDataLoaded && !songDeleted"
+				>
+					<div class="top-section">
+						<div class="player-section">
+							<div :id="`editSongPlayer-${modalUuid}`" />
+
+							<div v-show="youtubeError" class="player-error">
+								<h2>{{ youtubeErrorMessage }}</h2>
+							</div>
+
+							<canvas
+								ref="canvasElement"
+								class="duration-canvas"
+								v-show="!youtubeError"
+								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="video.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="
+											() => {
+												showRateDropdown = true;
+											}
+										"
+										@hide="
+											() => {
+												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>
+											<button
+												class="button dropdown-toggle"
+											>
+												<i class="material-icons">
+													{{
+														showRateDropdown
+															? "expand_more"
+															: "expand_less"
+													}}
+												</i>
+											</button>
+										</div>
+
+										<template #content>
+											<div class="nav-dropdown-items">
+												<button
+													class="nav-item button"
+													:class="{
+														active:
+															video.playbackRate ===
+															0.5
+													}"
+													title="0.5x"
+													@click="
+														setPlaybackRate(0.5)
+													"
+												>
+													<p>0.5x</p>
+												</button>
+												<button
+													class="nav-item button"
+													:class="{
+														active:
+															video.playbackRate ===
+															1
+													}"
+													title="1x"
+													@click="setPlaybackRate(1)"
+												>
+													<p>1x</p>
+												</button>
+												<button
+													class="nav-item button"
+													:class="{
+														active:
+															video.playbackRate ===
+															2
+													}"
+													title="2x"
+													@click="setPlaybackRate(2)"
+												>
+													<p>2x</p>
+												</button>
+											</div>
+										</template>
+									</tippy>
+								</div>
+								<div class="player-footer-center">
+									<span>
+										<span>
+											{{ youtubeVideoCurrentTime }}
+										</span>
+										/
+										<span>
+											{{ youtubeVideoDuration }}
+											{{ youtubeVideoNote }}
+										</span>
+									</span>
+								</div>
+								<div class="player-footer-right">
+									<p id="volume-control">
+										<i
+											class="material-icons"
+											@click="toggleMute()"
+											:content="`${
+												muted ? 'Unmute' : 'Mute'
+											}`"
+											v-tippy
+											>{{
+												muted
+													? "volume_mute"
+													: volumeSliderValue >= 50
+													? "volume_up"
+													: "volume_down"
+											}}</i
+										>
+										<input
+											v-model="volumeSliderValue"
+											type="range"
+											min="0"
+											max="100"
+											class="volume-slider active"
+											@change="changeVolume()"
+											@input="changeVolume()"
+										/>
+									</p>
+								</div>
+							</div>
+						</div>
+						<song-thumbnail
+							v-if="songDataLoaded && !songDeleted"
+							:song="song"
+							:fallback="false"
+							class="thumbnail-preview"
+							@loadError="onThumbnailLoadError"
+						/>
+						<img
+							v-if="
+								!isYoutubeThumbnail &&
+								songDataLoaded &&
+								!songDeleted
+							"
+							class="thumbnail-dummy"
+							:src="song.thumbnail"
+							ref="thumbnailElement"
+							@load="onThumbnailLoad"
+						/>
+					</div>
+
+					<div
+						class="edit-section"
+						v-if="songDataLoaded && !songDeleted"
+					>
+						<div class="control is-grouped">
+							<div class="title-container">
+								<label class="label">Title</label>
+								<p class="control has-addons">
+									<input
+										class="input"
+										type="text"
+										:ref="
+											el => (inputs['title-input'] = el)
+										"
+										v-model="song.title"
+										placeholder="Enter song title..."
+										@keyup.shift.enter="
+											getAlbumData('title')
+										"
+									/>
+									<button
+										class="button youtube-get-button"
+										@click="getYouTubeData('title')"
+									>
+										<div
+											class="youtube-icon"
+											v-tippy
+											content="Fill from YouTube"
+										></div>
+									</button>
+									<button
+										class="button album-get-button"
+										@click="getAlbumData('title')"
+									>
+										<i
+											class="material-icons"
+											v-tippy
+											content="Fill from Discogs"
+											>album</i
+										>
+									</button>
+								</p>
+							</div>
+
+							<div class="duration-container">
+								<label class="label">Duration</label>
+								<p class="control has-addons">
+									<input
+										class="input"
+										type="text"
+										placeholder="Enter song duration..."
+										v-model.number="song.duration"
+										@keyup.shift.enter="fillDuration()"
+									/>
+									<button
+										class="button duration-fill-button"
+										@click="fillDuration()"
+									>
+										<i
+											class="material-icons"
+											v-tippy
+											content="Sync duration with YouTube"
+											>sync</i
+										>
+									</button>
+								</p>
+							</div>
+
+							<div class="skip-duration-container">
+								<label class="label">Skip duration</label>
+								<p class="control">
+									<input
+										class="input"
+										type="text"
+										placeholder="Enter skip duration..."
+										v-model.number="song.skipDuration"
+									/>
+								</p>
+							</div>
+						</div>
+
+						<div class="control is-grouped">
+							<div class="album-art-container">
+								<label class="label">
+									Thumbnail
+									<i
+										v-if="
+											thumbnailNotSquare &&
+											!isYoutubeThumbnail
+										"
+										class="material-icons thumbnail-warning"
+										content="Thumbnail not square, it will be stretched"
+										v-tippy="{ theme: 'info' }"
+									>
+										warning
+									</i>
+									<i
+										v-if="
+											thumbnailLoadError &&
+											!isYoutubeThumbnail
+										"
+										class="material-icons thumbnail-warning"
+										content="Error loading thumbnail"
+										v-tippy="{ theme: 'info' }"
+									>
+										warning
+									</i>
+								</label>
+
+								<p class="control has-addons">
+									<input
+										class="input"
+										type="text"
+										v-model="song.thumbnail"
+										placeholder="Enter link to thumbnail..."
+										@keyup.shift.enter="
+											getAlbumData('albumArt')
+										"
+									/>
+									<button
+										class="button youtube-get-button"
+										@click="getYouTubeData('thumbnail')"
+									>
+										<div
+											class="youtube-icon"
+											v-tippy
+											content="Fill from YouTube"
+										></div>
+									</button>
+									<button
+										class="button album-get-button"
+										@click="getAlbumData('albumArt')"
+									>
+										<i
+											class="material-icons"
+											v-tippy
+											content="Fill from Discogs"
+											>album</i
+										>
+									</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 class="verified-container">
+								<label class="label">Verified</label>
+								<p class="is-expanded checkbox-control">
+									<label class="switch">
+										<input
+											type="checkbox"
+											id="verified"
+											v-model="song.verified"
+										/>
+										<span class="slider round"></span>
+									</label>
+								</p>
+							</div>
+						</div>
+
+						<div class="control is-grouped">
+							<div class="artists-container">
+								<label class="label">Artists</label>
+								<p class="control has-addons">
+									<auto-suggest
+										v-model="artistInputValue"
+										ref="new-artist"
+										placeholder="Add artist..."
+										:all-items="
+											autosuggest.allItems.artists
+										"
+										@submitted="addTag('artists')"
+										@keyup.shift.enter="
+											getAlbumData('artists')
+										"
+									/>
+									<button
+										class="button youtube-get-button"
+										@click="getYouTubeData('author')"
+									>
+										<div
+											class="youtube-icon"
+											v-tippy
+											content="Fill from YouTube"
+										></div>
+									</button>
+									<button
+										class="button album-get-button"
+										@click="getAlbumData('artists')"
+									>
+										<i
+											class="material-icons"
+											v-tippy
+											content="Fill from Discogs"
+											>album</i
+										>
+									</button>
+									<button
+										class="button is-info add-button"
+										@click="addTag('artists')"
+									>
+										<i class="material-icons">add</i>
+									</button>
+								</p>
+								<div class="list-container">
+									<div
+										class="list-item"
+										v-for="artist in song.artists"
+										:key="artist"
+									>
+										<div
+											class="list-item-circle"
+											@click="
+												removeTag('artists', artist)
+											"
+										>
+											<i class="material-icons">close</i>
+										</div>
+										<p>{{ artist }}</p>
+									</div>
+								</div>
+							</div>
+							<div class="genres-container">
+								<label class="label">
+									<span>Genres</span>
+									<i
+										class="material-icons"
+										@click="toggleGenreHelper"
+										@dblclick="resetGenreHelper"
+										v-tippy
+										content="View list of genres"
+										>info</i
+									>
+								</label>
+								<p class="control has-addons">
+									<auto-suggest
+										v-model="genreInputValue"
+										ref="new-genre"
+										placeholder="Add genre..."
+										:all-items="autosuggest.allItems.genres"
+										@submitted="addTag('genres')"
+										@keyup.shift.enter="
+											getAlbumData('genres')
+										"
+									/>
+									<button
+										class="button album-get-button"
+										@click="getAlbumData('genres')"
+									>
+										<i
+											class="material-icons"
+											v-tippy
+											content="Fill from Discogs"
+											>album</i
+										>
+									</button>
+									<button
+										class="button is-info add-button"
+										@click="addTag('genres')"
+									>
+										<i class="material-icons">add</i>
+									</button>
+								</p>
+								<div class="list-container">
+									<div
+										class="list-item"
+										v-for="genre in song.genres"
+										:key="genre"
+									>
+										<div
+											class="list-item-circle"
+											@click="removeTag('genres', genre)"
+										>
+											<i class="material-icons">close</i>
+										</div>
+										<p>{{ genre }}</p>
+									</div>
+								</div>
+							</div>
+							<div class="tags-container">
+								<label class="label">Tags</label>
+								<p class="control has-addons">
+									<auto-suggest
+										v-model="tagInputValue"
+										ref="new-tag"
+										placeholder="Add tag..."
+										:all-items="autosuggest.allItems.tags"
+										@submitted="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>
+				</div>
+				<div
+					class="right-section"
+					v-if="songDataLoaded && !songDeleted"
+				>
+					<div id="tabs-container">
+						<div id="tab-selection">
+							<button
+								class="button is-default"
+								:class="{ selected: tab === 'discogs' }"
+								:ref="el => (tabs['discogs-tab'] = el)"
+								@click="showTab('discogs')"
+							>
+								Discogs
+							</button>
+							<button
+								v-if="!newSong"
+								class="button is-default"
+								:class="{ selected: tab === 'reports' }"
+								:ref="el => (tabs['reports-tab'] = el)"
+								@click="showTab('reports')"
+							>
+								Reports ({{ reports.length }})
+							</button>
+							<button
+								class="button is-default"
+								:class="{ selected: tab === 'youtube' }"
+								:ref="el => (tabs['youtube-tab'] = el)"
+								@click="showTab('youtube')"
+							>
+								YouTube
+							</button>
+							<button
+								class="button is-default"
+								:class="{ selected: tab === 'musare-songs' }"
+								:ref="el => (tabs['musare-songs-tab'] = el)"
+								@click="showTab('musare-songs')"
+							>
+								Songs
+							</button>
+						</div>
+						<discogs
+							class="tab"
+							v-show="tab === 'discogs'"
+							:bulk="bulk"
+							:modal-uuid="modalUuid"
+							:modal-module-path="modalModulePath"
+						/>
+						<reports-tab
+							v-if="!newSong"
+							class="tab"
+							v-show="tab === 'reports'"
+							:modal-uuid="modalUuid"
+							:modal-module-path="modalModulePath"
+						/>
+						<youtube
+							class="tab"
+							v-show="tab === 'youtube'"
+							:modal-uuid="modalUuid"
+							:modal-module-path="modalModulePath"
+						/>
+						<musare-songs
+							class="tab"
+							v-show="tab === 'musare-songs'"
+							:modal-uuid="modalUuid"
+							:modal-module-path="modalModulePath"
+						/>
+					</div>
+				</div>
+			</template>
+			<template #footer>
+				<div v-if="bulk">
+					<button class="button is-primary" @click="editNextSong()">
+						Next
+					</button>
+					<button
+						class="button is-primary"
+						@click="toggleFlag()"
+						v-if="youtubeId && !songDeleted"
+					>
+						{{ flagged ? "Unflag" : "Flag" }}
+					</button>
+				</div>
+				<div v-if="!newSong && !songDeleted">
+					<save-button
+						:ref="el => (saveButtonRefs['saveButton'] = el)"
+						@clicked="save(song, false, 'saveButton')"
+					/>
+					<save-button
+						:ref="el => (saveButtonRefs['saveAndCloseButton'] = el)"
+						:default-message="
+							bulk ? `Save and next` : `Save and close`
+						"
+						@clicked="save(song, true, 'saveAndCloseButton')"
+					/>
+
+					<div class="right">
+						<button
+							class="button is-danger icon-with-button material-icons"
+							@click.prevent="
+								confirmAction({
+									message:
+										'Removing this song will remove it from all playlists and cause a ratings recalculation.',
+									action: remove,
+									params: song._id
+								})
+							"
+							content="Delete Song"
+							v-tippy
+						>
+							delete_forever
+						</button>
+					</div>
+				</div>
+				<div v-else-if="newSong">
+					<save-button
+						:ref="el => (saveButtonRefs['createButton'] = el)"
+						default-message="Create Song"
+						@clicked="save(song, false, 'createButton', true)"
+					/>
+					<save-button
+						:ref="
+							el => (saveButtonRefs['createAndCloseButton'] = el)
+						"
+						:default-message="
+							bulk ? `Create and next` : `Create and close`
+						"
+						@clicked="
+							save(song, true, 'createAndCloseButton', true)
+						"
+					/>
+				</div>
+			</template>
+		</modal>
+		<floating-box
+			id="genreHelper"
+			ref="genreHelper"
+			:column="false"
+			title="Song Genres List"
+		>
+			<template #body>
+				<span
+					v-for="item in autosuggest.allItems.genres"
+					:key="`genre-helper-${item}`"
+				>
+					{{ item }}
+				</span>
+			</template>
+		</floating-box>
+	</div>
+</template>
 
 <style lang="less" scoped>
 .night-mode {