mirror of
https://github.com/janishutz/MusicPlayerV2.git
synced 2025-11-25 13:04:23 +00:00
Compare commits
3 Commits
d63df5898b
...
669cc620bf
| Author | SHA1 | Date | |
|---|---|---|---|
| 669cc620bf | |||
| 64d086dec4 | |||
| 1d714da494 |
@@ -431,23 +431,46 @@
|
||||
parseBlob( blob )
|
||||
.then( data => {
|
||||
try {
|
||||
player.findSongOnAppleMusic( data.common.title
|
||||
?? songDetails.filename.split( '.' )[ 0 ] )
|
||||
const searchTerm = data.common.title
|
||||
? data.common.title + ( data.common.artist ? ' ' + data.common.artist : '' )
|
||||
: songDetails.filename.split( '.' )[ 0 ].replace( '_', ' ' );
|
||||
|
||||
|
||||
player.findSongOnAppleMusic( searchTerm )
|
||||
.then( d => {
|
||||
let url = d.data.results.songs.data[ 0 ].attributes.artwork.url;
|
||||
if ( d.data.results.songs ) {
|
||||
let url = d.data.results.songs.data[ 0 ].attributes.artwork.url;
|
||||
|
||||
url = url.replace( '{w}', String( d.data.results.songs.data[ 0 ].attributes.artwork.width ) );
|
||||
url = url.replace( '{h}', String( d.data.results.songs.data[ 0 ].attributes.artwork.height ) );
|
||||
const song: Song = {
|
||||
'artist': d.data.results.songs.data[ 0 ].attributes.artistName,
|
||||
'title': d.data.results.songs.data[ 0 ].attributes.name,
|
||||
'duration': data.format.duration ?? ( d.data.results.songs.data[ 0 ].attributes.durationInMillis / 1000 ),
|
||||
'id': songDetails.url,
|
||||
'origin': 'disk',
|
||||
'cover': url
|
||||
};
|
||||
console.debug(
|
||||
'Result used for', searchTerm, 'is', d.data.results.songs.data[0]
|
||||
);
|
||||
|
||||
resolve( song );
|
||||
url = url.replace( '{w}', String( d.data.results.songs.data[ 0 ].attributes.artwork.width ) );
|
||||
url = url.replace( '{h}', String( d.data.results.songs.data[ 0 ].attributes.artwork.height ) );
|
||||
const song: Song = {
|
||||
'artist': d.data.results.songs.data[ 0 ].attributes.artistName,
|
||||
'title': d.data.results.songs.data[ 0 ].attributes.name,
|
||||
'duration': data.format.duration ?? ( d.data.results.songs.data[ 0 ].attributes.durationInMillis / 1000 ),
|
||||
'id': songDetails.url,
|
||||
'origin': 'disk',
|
||||
'cover': url
|
||||
};
|
||||
|
||||
resolve( song );
|
||||
} else {
|
||||
const song: Song = {
|
||||
'artist': data.common.artist ?? 'Unknown artist',
|
||||
'title': data.common.title ?? 'Unknown song title',
|
||||
'duration': data.format.duration ?? 1000,
|
||||
'id': songDetails.url,
|
||||
'origin': 'disk',
|
||||
'cover': ''
|
||||
};
|
||||
|
||||
console.warn( 'No results found for', searchTerm );
|
||||
|
||||
resolve( song );
|
||||
}
|
||||
} )
|
||||
.catch( e => {
|
||||
console.error( e );
|
||||
@@ -560,7 +583,7 @@
|
||||
prepNiceDurationTime( playingSong );
|
||||
notificationHandler.emit( 'playlist-index-update', currentlyPlayingSongIndex.value );
|
||||
notificationHandler.emit( 'playback-update', isPlaying.value );
|
||||
notificationHandler.emit( 'playback-start-update', new Date().getTime() - pos.value * 1000 );
|
||||
notificationHandler.emit( 'playback-start-update', new Date().getTime() - ( pos.value * 1000 ) );
|
||||
hasStarted = true;
|
||||
}, 2000 );
|
||||
}
|
||||
@@ -574,7 +597,7 @@
|
||||
nicePlaybackPos.value = '0' + minuteCount + ':';
|
||||
}
|
||||
|
||||
const secondCount = Math.floor( pos.value - minuteCount * 60 );
|
||||
const secondCount = Math.floor( pos.value - ( minuteCount * 60 ) );
|
||||
|
||||
if ( ( '' + secondCount ).length === 1 ) {
|
||||
nicePlaybackPos.value += '0' + secondCount;
|
||||
@@ -591,7 +614,7 @@
|
||||
niceDuration.value = '-0' + minuteCounts + ':';
|
||||
}
|
||||
|
||||
const secondCounts = Math.floor( ( playingSong.duration - pos.value ) - minuteCounts * 60 );
|
||||
const secondCounts = Math.floor( ( playingSong.duration - pos.value ) - ( minuteCounts * 60 ) );
|
||||
|
||||
if ( ( '' + secondCounts ).length === 1 ) {
|
||||
niceDuration.value += '0' + secondCounts;
|
||||
|
||||
@@ -1,88 +1,153 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>Queue</h1>
|
||||
<input type="file" multiple accept="audio/*" id="more-songs" class="small-buttons">
|
||||
<button @click="addNewSongs()" class="small-buttons" title="Load selected files"><span class="material-symbols-outlined">upload</span></button>
|
||||
<button @click="openSearch()" v-if="$props.isLoggedIntoAppleMusic" class="small-buttons" title="Search Apple Music for the song"><span class="material-symbols-outlined">search</span></button>
|
||||
<button @click="clearPlaylist()" class="small-buttons" title="Clear the playlist"><span class="material-symbols-outlined">delete</span></button>
|
||||
<button title="Transmit additional information" class="small-buttons" @click="sendAdditionalInfo()"><span class="material-symbols-outlined">send</span></button>
|
||||
<p v-if="!hasSelectedSongs">Please select at least one song to proceed</p>
|
||||
<div class="playlist-box" id="pl-box">
|
||||
<input
|
||||
id="more-songs"
|
||||
type="file"
|
||||
multiple
|
||||
accept="audio/*"
|
||||
class="small-buttons"
|
||||
>
|
||||
<button class="small-buttons" title="Load selected files" @click="addNewSongs()">
|
||||
<span class="material-symbols-outlined">upload</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="$props.isLoggedIntoAppleMusic"
|
||||
class="small-buttons"
|
||||
title="Search Apple Music for the song"
|
||||
@click="openSearch()"
|
||||
>
|
||||
<span class="material-symbols-outlined">search</span>
|
||||
</button>
|
||||
<button class="small-buttons" title="Clear the playlist" @click="clearPlaylist()">
|
||||
<span class="material-symbols-outlined">delete</span>
|
||||
</button>
|
||||
<button title="Transmit additional information" class="small-buttons" @click="sendAdditionalInfo()">
|
||||
<span class="material-symbols-outlined">send</span>
|
||||
</button>
|
||||
<p v-if="!hasSelectedSongs">
|
||||
Please select at least one song to proceed
|
||||
</p>
|
||||
<div id="pl-box" class="playlist-box">
|
||||
<!-- TODO: Allow editing additionalInfo. Think also how to make it persist over reloads... Export to JSON and then best-guess add them? Very easy for Apple Music 'cause ID, but how for local songs? Maybe using retrieved ID from Apple Music? -->
|
||||
<!-- TODO: Handle long AppleMusic Playlists, as AppleMusic doesn't automatically load all songs of a playlist -->
|
||||
<div class="song" v-for="song in computedPlaylist" v-bind:key="song.id"
|
||||
<div
|
||||
v-for="song in computedPlaylist"
|
||||
:key="song.id"
|
||||
class="song"
|
||||
:class="( song.id === ( $props.playlist ? $props.playlist [ $props.currentlyPlaying ?? 0 ].id : '' ) && isPlaying ? 'playing' : ' not-playing' )
|
||||
+ ( ( !isPlaying && ( song.id === ( $props.playlist ? $props.playlist [ $props.currentlyPlaying ?? 0 ].id : '' ) ) ) ? ' active-song' : '' )">
|
||||
<img :src="song.cover" alt="Song cover" class="song-cover">
|
||||
+ ( ( !isPlaying && ( song.id === ( $props.playlist ? $props.playlist [ $props.currentlyPlaying ?? 0 ].id : '' ) ) ) ? ' active-song' : '' )"
|
||||
>
|
||||
<img
|
||||
v-if="song.cover"
|
||||
:src="song.cover"
|
||||
alt="Song cover"
|
||||
class="song-cover"
|
||||
>
|
||||
<span v-else class="material-symbols-outlined song-cover">music_note</span>
|
||||
<div v-if="song.id === ( $props.playlist ? $props.playlist [ $props.currentlyPlaying ?? 0 ].id : '' ) && $props.isPlaying" class="playing-symbols">
|
||||
<div class="playing-symbols-wrapper">
|
||||
<div class="playing-bar" id="bar-1"></div>
|
||||
<div class="playing-bar" id="bar-2"></div>
|
||||
<div class="playing-bar" id="bar-3"></div>
|
||||
<div id="bar-1" class="playing-bar"></div>
|
||||
<div id="bar-2" class="playing-bar"></div>
|
||||
<div id="bar-3" class="playing-bar"></div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="material-symbols-outlined play-icon" @click="control( 'play' )" v-if="song.id === ( $props.playlist ? $props.playlist [ $props.currentlyPlaying ?? 0 ].id : '' )">play_arrow</span>
|
||||
<span class="material-symbols-outlined play-icon" @click="play( song.id )" v-else>play_arrow</span>
|
||||
<span v-if="song.id === ( $props.playlist ? $props.playlist [ $props.currentlyPlaying ?? 0 ].id : '' )" class="material-symbols-outlined play-icon" @click="control( 'play' )">play_arrow</span>
|
||||
<span v-else class="material-symbols-outlined play-icon" @click="play( song.id )">play_arrow</span>
|
||||
<span class="material-symbols-outlined pause-icon" @click="control( 'pause' )">pause</span>
|
||||
<span class="material-symbols-outlined move-icon" @click="moveSong( song.id, 'up' )" title="Move song up" v-if="canBeMoved( 'up', song.id )">arrow_upward</span>
|
||||
<span class="material-symbols-outlined move-icon" @click="moveSong( song.id, 'down' )" title="Move song down" v-if="canBeMoved( 'down', song.id )">arrow_downward</span>
|
||||
<h3 class="song-title">{{ song.title }}</h3>
|
||||
<span
|
||||
v-if="canBeMoved( 'up', song.id )"
|
||||
class="material-symbols-outlined move-icon"
|
||||
title="Move song up"
|
||||
@click="moveSong( song.id, 'up' )"
|
||||
>arrow_upward</span>
|
||||
<span
|
||||
v-if="canBeMoved( 'down', song.id )"
|
||||
class="material-symbols-outlined move-icon"
|
||||
title="Move song down"
|
||||
@click="moveSong( song.id, 'down' )"
|
||||
>arrow_downward</span>
|
||||
<h3 class="song-title">
|
||||
{{ song.title }}
|
||||
</h3>
|
||||
<div>
|
||||
<input type="text" placeholder="Additional information for remote display" title="Additional information for remote display" v-model="song.additionalInfo" @focusin="kbControl( 'on' )" @focusout="kbControl( 'off' )">
|
||||
<p class="playing-in">{{ getTimeUntil( song ) }}</p>
|
||||
<input
|
||||
v-model="song.additionalInfo"
|
||||
type="text"
|
||||
placeholder="Additional information for remote display"
|
||||
title="Additional information for remote display"
|
||||
@focusin="kbControl( 'on' )"
|
||||
@focusout="kbControl( 'off' )"
|
||||
>
|
||||
<p class="playing-in">
|
||||
{{ getTimeUntil( song ) }}
|
||||
</p>
|
||||
</div>
|
||||
<button @click="deleteSong( song.id )" class="small-buttons" title="Remove this song from the queue" v-if="canBeMoved( 'down', song.id ) || canBeMoved( 'up', song.id )"><span class="material-symbols-outlined">delete</span></button>
|
||||
<button
|
||||
v-if="canBeMoved( 'down', song.id ) || canBeMoved( 'up', song.id )"
|
||||
class="small-buttons"
|
||||
title="Remove this song from the queue"
|
||||
@click="deleteSong( song.id )"
|
||||
>
|
||||
<span class="material-symbols-outlined">delete</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<searchView ref="search" @selected-song="( song ) => { addNewSongsAppleMusic( song ) }"></searchView>
|
||||
<searchView ref="search" @selected-song="( song ) => { addNewSongsAppleMusic( song ) }" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// TODO: Add logout button
|
||||
import type { AppleMusicSongData, ReadFile, Song } from '@/scripts/song';
|
||||
import { computed, ref } from 'vue';
|
||||
import type {
|
||||
AppleMusicSongData, ReadFile, Song
|
||||
} from '@/scripts/song';
|
||||
import {
|
||||
computed, ref
|
||||
} from 'vue';
|
||||
import searchView from './searchView.vue';
|
||||
import { useUserStore } from '@/stores/userStore';
|
||||
import {
|
||||
useUserStore
|
||||
} from '@/stores/userStore';
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
const search = ref( searchView );
|
||||
const props = defineProps( {
|
||||
'playlist': {
|
||||
default: [],
|
||||
required: true,
|
||||
type: Array<Song>
|
||||
'default': [],
|
||||
'required': true,
|
||||
'type': Array<Song>
|
||||
},
|
||||
'currentlyPlaying': {
|
||||
default: 0,
|
||||
required: true,
|
||||
type: Number,
|
||||
'default': 0,
|
||||
'required': true,
|
||||
'type': Number,
|
||||
},
|
||||
'isPlaying': {
|
||||
default: true,
|
||||
required: true,
|
||||
type: Boolean,
|
||||
'default': true,
|
||||
'required': true,
|
||||
'type': Boolean,
|
||||
},
|
||||
'pos': {
|
||||
default: 0,
|
||||
required: false,
|
||||
type: Number,
|
||||
'default': 0,
|
||||
'required': false,
|
||||
'type': Number,
|
||||
},
|
||||
'isLoggedIntoAppleMusic': {
|
||||
default: false,
|
||||
required: true,
|
||||
type: Boolean,
|
||||
'default': false,
|
||||
'required': true,
|
||||
'type': Boolean,
|
||||
}
|
||||
} );
|
||||
const hasSelectedSongs = ref( true );
|
||||
|
||||
const computedPlaylist = computed( () => {
|
||||
let pl: Song[] = [];
|
||||
|
||||
// ( document.getElementById( 'pl-box' ) as HTMLDivElement ).scrollTo( { behavior: 'smooth', top: 0 } );
|
||||
for ( let i = props.currentlyPlaying; i < props.playlist.length; i++ ) {
|
||||
pl.push( props.playlist[ i ] );
|
||||
}
|
||||
|
||||
return pl;
|
||||
} );
|
||||
|
||||
@@ -92,60 +157,66 @@
|
||||
} else {
|
||||
userStore.setKeyboardUsageStatus( true );
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const openSearch = () => {
|
||||
if ( search.value ) {
|
||||
search.value.controlSearch( 'show' );
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const canBeMoved = computed( () => {
|
||||
return ( direction: movementDirection, songID: string ): boolean => {
|
||||
let id = 0;
|
||||
|
||||
for ( let song in props.playlist ) {
|
||||
if ( props.playlist[ song ].id === songID ) {
|
||||
id = parseInt( song );
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( direction === 'up' ) {
|
||||
if ( props.currentlyPlaying + 1 === id || props.currentlyPlaying === id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} else {
|
||||
if ( id === props.playlist.length - 1 || props.currentlyPlaying === id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} )
|
||||
|
||||
};
|
||||
} );
|
||||
const getTimeUntil = computed( () => {
|
||||
return ( song: Song ) => {
|
||||
let timeRemaining = 0;
|
||||
|
||||
for ( let i = props.currentlyPlaying; i < Object.keys( props.playlist ).length; i++ ) {
|
||||
if ( props.playlist[ i ] == song ) {
|
||||
break;
|
||||
}
|
||||
|
||||
timeRemaining += props.playlist[ i ].duration;
|
||||
}
|
||||
|
||||
if ( props.isPlaying ) {
|
||||
if ( timeRemaining === 0 ) {
|
||||
return 'Currently playing';
|
||||
} else {
|
||||
return 'Playing in less than ' + Math.ceil( timeRemaining / 60 - props.pos / 60 ) + 'min';
|
||||
return 'Playing in less than ' + Math.ceil( timeRemaining / 60 - props.pos / 60 ) + 'min';
|
||||
}
|
||||
} else {
|
||||
if ( timeRemaining === 0 ) {
|
||||
return 'Plays next';
|
||||
} else {
|
||||
return 'Playing less than ' + Math.ceil( timeRemaining / 60 - props.pos / 60 ) + 'min after starting to play';
|
||||
return 'Playing less than ' + Math.ceil( timeRemaining / 60 - props.pos / 60 ) + 'min after starting to play';
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
} );
|
||||
|
||||
const deleteSong = ( songID: string ) => {
|
||||
@@ -154,69 +225,92 @@
|
||||
emits( 'delete-song', parseInt( song ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const clearPlaylist = () => {
|
||||
emits( 'clear-playlist', '' );
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const control = ( action: string ) => {
|
||||
emits( 'control', action );
|
||||
}
|
||||
};
|
||||
|
||||
const play = ( song: string ) => {
|
||||
emits( 'play-song', song );
|
||||
}
|
||||
};
|
||||
|
||||
const addNewSongs = () => {
|
||||
const fileURLList: ReadFile[] = [];
|
||||
const allFiles = ( document.getElementById( 'more-songs' ) as HTMLInputElement ).files ?? [];
|
||||
|
||||
if ( allFiles.length > 0 ) {
|
||||
hasSelectedSongs.value = true;
|
||||
|
||||
for ( let file = 0; file < allFiles.length; file++ ) {
|
||||
fileURLList.push( { 'url': URL.createObjectURL( allFiles[ file ] ), 'filename': allFiles[ file ].name } );
|
||||
fileURLList.push( {
|
||||
'url': URL.createObjectURL( allFiles[ file ] ),
|
||||
'filename': allFiles[ file ].name
|
||||
} );
|
||||
}
|
||||
|
||||
emits( 'add-new-songs', fileURLList );
|
||||
} else {
|
||||
hasSelectedSongs.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const addNewSongsAppleMusic = ( songData: AppleMusicSongData ) => {
|
||||
const song: Song = {
|
||||
artist: songData.attributes.artistName,
|
||||
cover: songData.attributes.artwork.url.replace( '{w}', String( songData.attributes.artwork.width ) ).replace( '{h}', String( songData.attributes.artwork.height ) ),
|
||||
duration: songData.attributes.durationInMillis / 1000,
|
||||
id: songData.id,
|
||||
origin: 'apple-music',
|
||||
title: songData.attributes.name
|
||||
}
|
||||
'artist': songData.attributes.artistName,
|
||||
'cover': songData.attributes.artwork.url.replace( '{w}', String( songData.attributes.artwork.width ) ).replace( '{h}', String( songData.attributes.artwork.height ) ),
|
||||
'duration': songData.attributes.durationInMillis / 1000,
|
||||
'id': songData.id,
|
||||
'origin': 'apple-music',
|
||||
'title': songData.attributes.name
|
||||
};
|
||||
|
||||
emits( 'add-new-songs-apple-music', song );
|
||||
}
|
||||
};
|
||||
|
||||
type movementDirection = 'up' | 'down';
|
||||
|
||||
const moveSong = ( songID: string, direction: movementDirection ) => {
|
||||
let newSongPos = 0;
|
||||
let hasFoundSongToMove = false;
|
||||
|
||||
for ( let el in props.playlist ) {
|
||||
if ( props.playlist[ el ].id === songID ) {
|
||||
const currPos = parseInt( el );
|
||||
|
||||
newSongPos = currPos + ( direction === 'up' ? -1 : 1 );
|
||||
hasFoundSongToMove = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( hasFoundSongToMove ) {
|
||||
emits( 'playlist-reorder', { 'songID': songID, 'newPos': newSongPos } );
|
||||
emits( 'playlist-reorder', {
|
||||
'songID': songID,
|
||||
'newPos': newSongPos
|
||||
} );
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const sendAdditionalInfo = () => {
|
||||
emits( 'send-additional-info' );
|
||||
}
|
||||
};
|
||||
|
||||
const emits = defineEmits( [ 'play-song', 'control', 'playlist-reorder', 'add-new-songs', 'add-new-songs-apple-music', 'delete-song', 'clear-playlist', 'send-additional-info' ] );
|
||||
const emits = defineEmits( [
|
||||
'play-song',
|
||||
'control',
|
||||
'playlist-reorder',
|
||||
'add-new-songs',
|
||||
'add-new-songs-apple-music',
|
||||
'delete-song',
|
||||
'clear-playlist',
|
||||
'send-additional-info'
|
||||
] );
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -249,6 +343,10 @@
|
||||
font-size: 6rem;
|
||||
}
|
||||
|
||||
.song img.song-cover {
|
||||
font-size: unset;
|
||||
}
|
||||
|
||||
.song-title {
|
||||
margin-left: 10px;
|
||||
margin-right: auto;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div v-if="hasLoaded && !showCouldNotFindRoom" style="width: 100%">
|
||||
<div class="current-song-wrapper">
|
||||
<img
|
||||
v-if="playlist[ playingSong ]"
|
||||
v-if="playlist[ playingSong ] && playlist[ playingSong ].cover"
|
||||
id="current-image"
|
||||
:src="playlist[ playingSong ].cover"
|
||||
class="fancy-view-song-art"
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<div v-if="hasLoaded && !showCouldNotFindRoom" class="showcase-wrapper">
|
||||
<div class="current-song-wrapper">
|
||||
<img
|
||||
v-if="playlist[ playingSong ]"
|
||||
v-if="playlist[ playingSong ] && playlist[ playingSong ].cover"
|
||||
id="current-image"
|
||||
:src="playlist[ playingSong ].cover"
|
||||
class="fancy-view-song-art"
|
||||
@@ -52,7 +52,8 @@
|
||||
</div>
|
||||
<div class="song-list-wrapper">
|
||||
<div v-for="song in songQueue" :key="song.id" class="song-list">
|
||||
<img :src="song.cover" class="song-image">
|
||||
<img v-if="song.cover" :src="song.cover" class="song-image">
|
||||
<span v-else class="material-symbols-outlined song-cover">music_note</span>
|
||||
<div
|
||||
v-if="( playlist[ playingSong ] ? playlist[ playingSong ].id : '' ) === song.id && isPlaying"
|
||||
class="playing-symbols"
|
||||
@@ -97,7 +98,7 @@
|
||||
Song
|
||||
} from '@/scripts/song';
|
||||
import {
|
||||
computed, ref, type Ref
|
||||
type Ref, computed, ref
|
||||
} from 'vue';
|
||||
import bizualizer from '@/scripts/bizualizer';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user