Compare commits

...

3 Commits

4 changed files with 214 additions and 92 deletions

View File

@@ -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;

View File

@@ -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;

View File

@@ -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"

View File

@@ -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';