|
@@ -63,7 +63,7 @@ const gettingMissingAlternativeMedia = ref(false);
|
|
|
|
|
|
const replacingAllSpotifySongs = ref(false);
|
|
const replacingAllSpotifySongs = ref(false);
|
|
|
|
|
|
-const currentConvertType = ref<"track" | "album" | "artist">("album");
|
|
|
|
|
|
+const currentConvertType = ref<"track" | "album" | "artist">("track");
|
|
const showReplaceButtonPerAlternative = ref(false);
|
|
const showReplaceButtonPerAlternative = ref(false);
|
|
const hideSpotifySongsWithNoAlternativesFound = ref(false);
|
|
const hideSpotifySongsWithNoAlternativesFound = ref(false);
|
|
|
|
|
|
@@ -81,6 +81,16 @@ const sortAlbumMode = ref<
|
|
"SONG_COUNT_ASC" | "SONG_COUNT_DESC" | "NAME_DESC" | "NAME_ASC"
|
|
"SONG_COUNT_ASC" | "SONG_COUNT_DESC" | "NAME_DESC" | "NAME_ASC"
|
|
>("SONG_COUNT_ASC");
|
|
>("SONG_COUNT_ASC");
|
|
|
|
|
|
|
|
+const showDontConvertButton = ref(true);
|
|
|
|
+
|
|
|
|
+const replaceSongUrlMap = reactive({});
|
|
|
|
+
|
|
|
|
+const showReplacementInputs = ref(false);
|
|
|
|
+
|
|
|
|
+const youtubeVideoUrlRegex =
|
|
|
|
+ /^(https?:\/\/)?(www\.)?(m\.)?(music\.)?(youtube\.com|youtu\.be)\/(watch\?v=)?(?<youtubeId>[\w-]{11})((&([A-Za-z0-9]+)?)*)?$/;
|
|
|
|
+const youtubeVideoIdRegex = /^([\w-]{11})$/;
|
|
|
|
+
|
|
const filteredSpotifySongs = computed(() =>
|
|
const filteredSpotifySongs = computed(() =>
|
|
hideSpotifySongsWithNoAlternativesFound.value
|
|
hideSpotifySongsWithNoAlternativesFound.value
|
|
? spotifySongs.value.filter(
|
|
? spotifySongs.value.filter(
|
|
@@ -304,6 +314,30 @@ const replaceSpotifySong = (oldMediaSource, newMediaSource) => {
|
|
);
|
|
);
|
|
};
|
|
};
|
|
|
|
|
|
|
|
+const replaceSongFromUrl = spotifyMediaSource => {
|
|
|
|
+ const replacementUrl = replaceSongUrlMap[spotifyMediaSource];
|
|
|
|
+
|
|
|
|
+ console.log(spotifyMediaSource, replacementUrl);
|
|
|
|
+
|
|
|
|
+ let newMediaSource = null;
|
|
|
|
+
|
|
|
|
+ const youtubeVideoUrlRegexMatches =
|
|
|
|
+ youtubeVideoUrlRegex.exec(replacementUrl);
|
|
|
|
+ console.log(youtubeVideoUrlRegexMatches);
|
|
|
|
+
|
|
|
|
+ const youtubeVideoIdRegexMatches = youtubeVideoIdRegex.exec(replacementUrl);
|
|
|
|
+ console.log(youtubeVideoIdRegexMatches);
|
|
|
|
+
|
|
|
|
+ if (youtubeVideoUrlRegexMatches)
|
|
|
|
+ newMediaSource = `youtube:${youtubeVideoUrlRegexMatches.groups.youtubeId}`;
|
|
|
|
+ if (youtubeVideoIdRegexMatches)
|
|
|
|
+ newMediaSource = `youtube:${youtubeVideoIdRegexMatches[0]}`;
|
|
|
|
+
|
|
|
|
+ if (!newMediaSource) return new Toast("Invalid URL/identifier specified.");
|
|
|
|
+
|
|
|
|
+ replaceSpotifySong(spotifyMediaSource, newMediaSource);
|
|
|
|
+};
|
|
|
|
+
|
|
const getMissingAlternativeMedia = () => {
|
|
const getMissingAlternativeMedia = () => {
|
|
if (gettingMissingAlternativeMedia.value) return;
|
|
if (gettingMissingAlternativeMedia.value) return;
|
|
|
|
|
|
@@ -549,6 +583,13 @@ const loadPlaylist = () =>
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
|
|
|
|
+const removeAlternativeTrack = (spotifyMediaSource, alternativeMediaSource) => {
|
|
|
|
+ alternativeMediaPerTrack[spotifyMediaSource].mediaSources =
|
|
|
|
+ alternativeMediaPerTrack[spotifyMediaSource].mediaSources.filter(
|
|
|
|
+ mediaSource => mediaSource !== alternativeMediaSource
|
|
|
|
+ );
|
|
|
|
+};
|
|
|
|
+
|
|
const removeSpotifyTrack = mediaSource => {
|
|
const removeSpotifyTrack = mediaSource => {
|
|
const spotifyTrack = spotifyTracks[mediaSource];
|
|
const spotifyTrack = spotifyTracks[mediaSource];
|
|
if (spotifyTrack) {
|
|
if (spotifyTrack) {
|
|
@@ -568,9 +609,36 @@ const removeSpotifyTrack = mediaSource => {
|
|
);
|
|
);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
+
|
|
|
|
+ const spotifyAlbum = spotifyAlbums[spotifyTrack.albumId];
|
|
|
|
+
|
|
|
|
+ if (spotifyAlbum) {
|
|
|
|
+ if (spotifyAlbum.songs.length === 1)
|
|
|
|
+ delete spotifyAlbums[spotifyTrack.albumId];
|
|
|
|
+ else
|
|
|
|
+ spotifyAlbums[spotifyTrack.albumId].songs = spotifyAlbums[
|
|
|
|
+ spotifyTrack.albumId
|
|
|
|
+ ].songs.filter(_mediaSource => _mediaSource !== mediaSource);
|
|
|
|
+ }
|
|
}
|
|
}
|
|
};
|
|
};
|
|
|
|
|
|
|
|
+const removeSpotifySong = mediaSource => {
|
|
|
|
+ // remove song
|
|
|
|
+ playlist.value.songs = playlist.value.songs.filter(
|
|
|
|
+ song => song.mediaSource !== mediaSource
|
|
|
|
+ );
|
|
|
|
+
|
|
|
|
+ spotifySongs.value = spotifySongs.value.filter(
|
|
|
|
+ song => song.mediaSource !== mediaSource
|
|
|
|
+ );
|
|
|
|
+
|
|
|
|
+ removeSpotifyTrack(mediaSource);
|
|
|
|
+
|
|
|
|
+ delete alternativeMediaMap[mediaSource];
|
|
|
|
+ delete alternativeMediaFailedMap[mediaSource];
|
|
|
|
+};
|
|
|
|
+
|
|
onMounted(() => {
|
|
onMounted(() => {
|
|
console.debug(TAG, "On mounted start");
|
|
console.debug(TAG, "On mounted start");
|
|
|
|
|
|
@@ -587,19 +655,7 @@ onMounted(() => {
|
|
) {
|
|
) {
|
|
const { oldMediaSource } = res.data;
|
|
const { oldMediaSource } = res.data;
|
|
|
|
|
|
- // remove song
|
|
|
|
- playlist.value.songs = playlist.value.songs.filter(
|
|
|
|
- song => song.mediaSource !== oldMediaSource
|
|
|
|
- );
|
|
|
|
-
|
|
|
|
- spotifySongs.value = spotifySongs.value.filter(
|
|
|
|
- song => song.mediaSource !== oldMediaSource
|
|
|
|
- );
|
|
|
|
-
|
|
|
|
- removeSpotifyTrack(oldMediaSource);
|
|
|
|
-
|
|
|
|
- delete alternativeMediaMap[oldMediaSource];
|
|
|
|
- delete alternativeMediaFailedMap[oldMediaSource];
|
|
|
|
|
|
+ removeSpotifySong(oldMediaSource);
|
|
}
|
|
}
|
|
},
|
|
},
|
|
{ modalUuid: props.modalUuid }
|
|
{ modalUuid: props.modalUuid }
|
|
@@ -620,19 +676,7 @@ onMounted(() => {
|
|
) {
|
|
) {
|
|
const { oldMediaSource } = res.data;
|
|
const { oldMediaSource } = res.data;
|
|
|
|
|
|
- // remove song
|
|
|
|
- playlist.value.songs = playlist.value.songs.filter(
|
|
|
|
- song => song.mediaSource !== oldMediaSource
|
|
|
|
- );
|
|
|
|
-
|
|
|
|
- spotifySongs.value = spotifySongs.value.filter(
|
|
|
|
- song => song.mediaSource !== oldMediaSource
|
|
|
|
- );
|
|
|
|
-
|
|
|
|
- removeSpotifyTrack(oldMediaSource);
|
|
|
|
-
|
|
|
|
- delete alternativeMediaMap[oldMediaSource];
|
|
|
|
- delete alternativeMediaFailedMap[oldMediaSource];
|
|
|
|
|
|
+ removeSpotifySong(oldMediaSource);
|
|
}
|
|
}
|
|
},
|
|
},
|
|
{ modalUuid: props.modalUuid }
|
|
{ modalUuid: props.modalUuid }
|
|
@@ -786,6 +830,36 @@ onMounted(() => {
|
|
</label>
|
|
</label>
|
|
</p>
|
|
</p>
|
|
|
|
|
|
|
|
+ <p class="is-expanded checkbox-control">
|
|
|
|
+ <label class="switch">
|
|
|
|
+ <input
|
|
|
|
+ type="checkbox"
|
|
|
|
+ id="showDontConvertButton"
|
|
|
|
+ v-model="showDontConvertButton"
|
|
|
|
+ />
|
|
|
|
+ <span class="slider round"></span>
|
|
|
|
+ </label>
|
|
|
|
+
|
|
|
|
+ <label for="showDontConvertButton">
|
|
|
|
+ <p>Show don't convert buttons</p>
|
|
|
|
+ </label>
|
|
|
|
+ </p>
|
|
|
|
+
|
|
|
|
+ <p class="is-expanded checkbox-control">
|
|
|
|
+ <label class="switch">
|
|
|
|
+ <input
|
|
|
|
+ type="checkbox"
|
|
|
|
+ id="showReplacementInputs"
|
|
|
|
+ v-model="showReplacementInputs"
|
|
|
|
+ />
|
|
|
|
+ <span class="slider round"></span>
|
|
|
|
+ </label>
|
|
|
|
+
|
|
|
|
+ <label for="showReplacementInputs">
|
|
|
|
+ <p>Show replacement inputs</p>
|
|
|
|
+ </label>
|
|
|
|
+ </p>
|
|
|
|
+
|
|
<p class="is-expanded checkbox-control">
|
|
<p class="is-expanded checkbox-control">
|
|
<label class="switch">
|
|
<label class="switch">
|
|
<input
|
|
<input
|
|
@@ -1016,16 +1090,31 @@ onMounted(() => {
|
|
</a>
|
|
</a>
|
|
</template>
|
|
</template>
|
|
</song-item>
|
|
</song-item>
|
|
- <p>
|
|
|
|
- Media source: {{ spotifySong.mediaSource }}
|
|
|
|
- </p>
|
|
|
|
- <p v-if="loadedSpotifyTracks">
|
|
|
|
- ISRC:
|
|
|
|
- {{
|
|
|
|
- spotifyTracks[spotifySong.mediaSource]
|
|
|
|
- .externalIds.isrc
|
|
|
|
- }}
|
|
|
|
- </p>
|
|
|
|
|
|
+ <template v-if="showExtra">
|
|
|
|
+ <p>
|
|
|
|
+ Media source:
|
|
|
|
+ {{ spotifySong.mediaSource }}
|
|
|
|
+ </p>
|
|
|
|
+ <p v-if="loadedSpotifyTracks">
|
|
|
|
+ ISRC:
|
|
|
|
+ {{
|
|
|
|
+ spotifyTracks[
|
|
|
|
+ spotifySong.mediaSource
|
|
|
|
+ ].externalIds.isrc
|
|
|
|
+ }}
|
|
|
|
+ </p>
|
|
|
|
+ </template>
|
|
|
|
+ <button
|
|
|
|
+ v-if="showDontConvertButton"
|
|
|
|
+ class="button is-primary is-fullwidth"
|
|
|
|
+ @click="
|
|
|
|
+ removeSpotifySong(
|
|
|
|
+ spotifySong.mediaSource
|
|
|
|
+ )
|
|
|
|
+ "
|
|
|
|
+ >
|
|
|
|
+ Don't convert this song
|
|
|
|
+ </button>
|
|
</div>
|
|
</div>
|
|
<div
|
|
<div
|
|
class="convert-table-cell convert-table-cell-right"
|
|
class="convert-table-cell convert-table-cell-right"
|
|
@@ -1083,7 +1172,9 @@ onMounted(() => {
|
|
hasn't been loaded yet
|
|
hasn't been loaded yet
|
|
</p>
|
|
</p>
|
|
<template v-else>
|
|
<template v-else>
|
|
- <div>
|
|
|
|
|
|
+ <div
|
|
|
|
+ class="alternative-song-container"
|
|
|
|
+ >
|
|
<song-item
|
|
<song-item
|
|
:song="
|
|
:song="
|
|
alternativeMediaMap[
|
|
alternativeMediaMap[
|
|
@@ -1140,10 +1231,23 @@ onMounted(() => {
|
|
<button
|
|
<button
|
|
class="button is-primary is-fullwidth"
|
|
class="button is-primary is-fullwidth"
|
|
>
|
|
>
|
|
- Replace Spotify song
|
|
|
|
- with this song
|
|
|
|
|
|
+ Use this alternative
|
|
</button>
|
|
</button>
|
|
</quick-confirm>
|
|
</quick-confirm>
|
|
|
|
+ <button
|
|
|
|
+ v-if="
|
|
|
|
+ showDontConvertButton
|
|
|
|
+ "
|
|
|
|
+ class="button is-primary is-fullwidth"
|
|
|
|
+ @click="
|
|
|
|
+ removeAlternativeTrack(
|
|
|
|
+ spotifySong.mediaSource,
|
|
|
|
+ alternativeMediaSource
|
|
|
|
+ )
|
|
|
|
+ "
|
|
|
|
+ >
|
|
|
|
+ Remove this alternative
|
|
|
|
+ </button>
|
|
</div>
|
|
</div>
|
|
<ul v-if="showExtra">
|
|
<ul v-if="showExtra">
|
|
<li
|
|
<li
|
|
@@ -1179,7 +1283,66 @@ onMounted(() => {
|
|
</template>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
+ <p
|
|
|
|
+ v-if="
|
|
|
|
+ alternativeMediaPerTrack[
|
|
|
|
+ spotifySong.mediaSource
|
|
|
|
+ ].mediaSources.length === 0
|
|
|
|
+ "
|
|
|
|
+ >
|
|
|
|
+ No alternative media sources found
|
|
|
|
+ </p>
|
|
</template>
|
|
</template>
|
|
|
|
+ <div
|
|
|
|
+ v-if="
|
|
|
|
+ showReplacementInputs ||
|
|
|
|
+ (alternativeMediaPerTrack[
|
|
|
|
+ spotifySong.mediaSource
|
|
|
|
+ ] &&
|
|
|
|
+ alternativeMediaPerTrack[
|
|
|
|
+ spotifySong.mediaSource
|
|
|
|
+ ].mediaSources.length === 0)
|
|
|
|
+ "
|
|
|
|
+ >
|
|
|
|
+ <div>
|
|
|
|
+ <label class="label">
|
|
|
|
+ Enter replacement song from URL
|
|
|
|
+ </label>
|
|
|
|
+ <div
|
|
|
|
+ class="control is-grouped input-with-button"
|
|
|
|
+ >
|
|
|
|
+ <p class="control is-expanded">
|
|
|
|
+ <input
|
|
|
|
+ class="input"
|
|
|
|
+ type="text"
|
|
|
|
+ placeholder="Enter your song URL here..."
|
|
|
|
+ v-model="
|
|
|
|
+ replaceSongUrlMap[
|
|
|
|
+ spotifySong
|
|
|
|
+ .mediaSource
|
|
|
|
+ ]
|
|
|
|
+ "
|
|
|
|
+ @keyup.enter="
|
|
|
|
+ replaceSongFromUrl(
|
|
|
|
+ spotifySong.mediaSource
|
|
|
|
+ )
|
|
|
|
+ "
|
|
|
|
+ />
|
|
|
|
+ </p>
|
|
|
|
+ <p class="control">
|
|
|
|
+ <a
|
|
|
|
+ class="button is-info"
|
|
|
|
+ @click="
|
|
|
|
+ replaceSongFromUrl(
|
|
|
|
+ spotifySong.mediaSource
|
|
|
|
+ )
|
|
|
|
+ "
|
|
|
|
+ >Replace song</a
|
|
|
|
+ >
|
|
|
|
+ </p>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
@@ -1345,6 +1508,17 @@ onMounted(() => {
|
|
row-gap: 12px;
|
|
row-gap: 12px;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+.alternative-song-container,
|
|
|
|
+.convert-table-cell-left {
|
|
|
|
+ display: flex;
|
|
|
|
+ flex-direction: column;
|
|
|
|
+ row-gap: 12px;
|
|
|
|
+
|
|
|
|
+ > * {
|
|
|
|
+ flex-grow: 0;
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
.convert-table {
|
|
.convert-table {
|
|
display: grid;
|
|
display: grid;
|
|
grid-template-columns: 50% 50%;
|
|
grid-template-columns: 50% 50%;
|