Fix custom playlist loading error

This commit is contained in:
2025-11-06 15:09:55 +01:00
parent 51f0b5639a
commit bd18636141
9 changed files with 470 additions and 10721 deletions

1
.gitignore vendored
View File

@@ -4,3 +4,4 @@ node_modules
apple_private_key.p8 apple_private_key.p8
musicplayerv2-server.zip musicplayerv2-server.zip
dist dist
package-lock.json

File diff suppressed because it is too large Load Diff

View File

@@ -19,7 +19,7 @@
"@rollup/plugin-inject": "^5.0.5", "@rollup/plugin-inject": "^5.0.5",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"colorthief": "^2.6.0", "colorthief": "^2.6.0",
"music-metadata-browser": "^2.5.10", "music-metadata": "^11.9.0",
"musickit-typescript": "^1.2.4", "musickit-typescript": "^1.2.4",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"socket.io-client": "^4.7.5", "socket.io-client": "^4.7.5",

View File

@@ -1,24 +1,33 @@
<template> <template>
<div> <div>
<h1>Library</h1> <h1>Library</h1>
<playlistsView :playlists="$props.playlists" @selected-playlist="( id ) => selectPlaylist( id )" :is-logged-in="$props.isLoggedIn" <playlistsView
@custom-playlist="( pl ) => selectCustomPlaylist( pl )"></playlistsView> :playlists="$props.playlists"
:is-logged-in="$props.isLoggedIn"
@selected-playlist="( id ) => selectPlaylist( id )"
@custom-playlist="( pl ) => selectCustomPlaylist( pl )"
/>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import playlistsView from '@/components/playlistsView.vue'; import playlistsView from '@/components/playlistsView.vue';
import type { ReadFile } from '@/scripts/song'; import type {
ReadFile
} from '@/scripts/song';
const emits = defineEmits( [ 'selected-playlist', 'custom-playlist' ] ); const emits = defineEmits( [
'selected-playlist',
'custom-playlist'
] );
const selectPlaylist = ( id: string ) => { const selectPlaylist = ( id: string ) => {
emits( 'selected-playlist', id ); emits( 'selected-playlist', id );
} };
const selectCustomPlaylist = ( playlist: ReadFile[] ) => { const selectCustomPlaylist = ( playlist: ReadFile[] ) => {
emits( 'custom-playlist', playlist ); emits( 'custom-playlist', playlist );
} };
defineProps( { defineProps( {
'playlists': { 'playlists': {

View File

@@ -3,14 +3,18 @@
<div id="notifications"> <div id="notifications">
<div class="message-box" :class="[ location, size ]" :style="'z-index: ' + ( messageType === 'hide' ? '-1' : '1000' )"> <div class="message-box" :class="[ location, size ]" :style="'z-index: ' + ( messageType === 'hide' ? '-1' : '1000' )">
<div class="message-container" :class="messageType"> <div class="message-container" :class="messageType">
<button @click="handleNotifications();" class="close-notification"><span class="material-symbols-outlined close-notification-icon">close</span></button> <button class="close-notification" @click="handleNotifications();">
<span class="material-symbols-outlined types hide" v-if="messageType == 'hide'">question_mark</span> <span class="material-symbols-outlined close-notification-icon">close</span>
<span class="material-symbols-outlined types" v-else-if="messageType == 'ok'" style="background-color: green;">done</span> </button>
<span class="material-symbols-outlined types" v-else-if="messageType == 'error'" style="background-color: red;">close</span> <span v-if="messageType == 'hide'" class="material-symbols-outlined types hide">question_mark</span>
<span class="material-symbols-outlined types progress-spinner" v-else-if="messageType == 'progress'" style="background-color: blue;">progress_activity</span> <span v-else-if="messageType == 'ok'" class="material-symbols-outlined types" style="background-color: green;">done</span>
<span class="material-symbols-outlined types" v-else-if="messageType == 'info'" style="background-color: lightblue;">info</span> <span v-else-if="messageType == 'error'" class="material-symbols-outlined types" style="background-color: red;">close</span>
<span class="material-symbols-outlined types" v-else-if="messageType == 'warning'" style="background-color: orangered;">warning</span> <span v-else-if="messageType == 'progress'" class="material-symbols-outlined types progress-spinner" style="background-color: blue;">progress_activity</span>
<p class="message" @click="notificationAction()">{{ notifications[ currentDID ] ? notifications[ currentDID ].message : '' }}</p> <span v-else-if="messageType == 'info'" class="material-symbols-outlined types" style="background-color: lightblue;">info</span>
<span v-else-if="messageType == 'warning'" class="material-symbols-outlined types" style="background-color: orangered;">warning</span>
<p class="message" @click="notificationAction()">
{{ notifications[ currentDID ] ? notifications[ currentDID ].message : '' }}
</p>
<div :class="'countdown countdown-' + messageType" :style="'width: ' + ( 100 - ( currentTime - notificationDisplayStartTime ) / ( notifications[ currentDID ] ? notifications[ currentDID ].showDuration : 1 ) / 10 ) + '%'"></div> <div :class="'countdown countdown-' + messageType" :style="'width: ' + ( 100 - ( currentTime - notificationDisplayStartTime ) / ( notifications[ currentDID ] ? notifications[ currentDID ].showDuration : 1 ) / 10 ) + '%'"></div>
</div> </div>
</div> </div>
@@ -19,28 +23,30 @@
<script setup lang="ts"> <script setup lang="ts">
import router from '@/router'; import router from '@/router';
import { onUnmounted, ref, type Ref } from 'vue'; import {
type Ref, onUnmounted, ref
} from 'vue';
defineProps( { defineProps( {
location: { 'location': {
type: String, 'type': String,
'default': 'topleft', 'default': 'topleft',
}, },
size: { 'size': {
type: String, 'type': String,
'default': 'default', 'default': 'default',
} }
// Size options: small, default (default option), big, bigger, huge // Size options: small, default (default option), big, bigger, huge
} ); } );
interface Notification { interface Notification {
message: string; 'message': string;
showDuration: number; 'showDuration': number;
messageType: string; 'messageType': string;
priority: string; 'priority': string;
id: number; 'id': number;
redirect?: string; 'redirect'?: string;
openInNewTab?: boolean; 'openInNewTab'?: boolean;
} }
interface NotificationList { interface NotificationList {
@@ -51,11 +57,17 @@
const queue: Ref<number[]> = ref( [] ); const queue: Ref<number[]> = ref( [] );
const currentDID: Ref<number> = ref( 0 ); const currentDID: Ref<number> = ref( 0 );
const messageType: Ref<string> = ref( 'hide' ); const messageType: Ref<string> = ref( 'hide' );
const currentID = ref( { 'critical': 0, 'medium': 1000, 'low': 10000 } ); const currentID = ref( {
'critical': 0,
'medium': 1000,
'low': 10000
} );
const notificationDisplayStartTime: Ref<number> = ref( 0 ); const notificationDisplayStartTime: Ref<number> = ref( 0 );
const currentTime: Ref<number> = ref( 0 ); const currentTime: Ref<number> = ref( 0 );
let progressBar = 0; let progressBar = 0;
let notificationTimeout = 0; let notificationTimeout = 0;
const notificationAction = () => { const notificationAction = () => {
if ( notifications.value[ currentDID.value ] ) { if ( notifications.value[ currentDID.value ] ) {
if ( notifications.value[ currentDID.value ].redirect ) { if ( notifications.value[ currentDID.value ].redirect ) {
@@ -76,7 +88,9 @@
* @param {string} priority The priority of the message: 'low', 'normal', 'critical' * @param {string} priority The priority of the message: 'low', 'normal', 'critical'
* @returns {number} * @returns {number}
*/ */
const createNotification = ( message: string, showDuration: number, msgType: string, priority: string, redirect?: string, openInNewTab?: boolean ): number => { const createNotification = (
message: string, showDuration: number, msgType: string, priority: string, redirect?: string, openInNewTab?: boolean
): number => {
/* /*
Takes a notification options array that contains: message, showDuration (in seconds), msgType (ok, error, progress, info) and priority (low, normal, critical). Takes a notification options array that contains: message, showDuration (in seconds), msgType (ok, error, progress, info) and priority (low, normal, critical).
Returns a notification ID which can be used to cancel the notification. The component will throttle notifications and display Returns a notification ID which can be used to cancel the notification. The component will throttle notifications and display
@@ -94,14 +108,25 @@
currentID.value[ 'low' ] += 1; currentID.value[ 'low' ] += 1;
id = currentID.value[ 'low' ]; id = currentID.value[ 'low' ];
} }
notifications.value[ id ] = { 'message': message, 'showDuration': showDuration, 'messageType': msgType, 'priority': priority, 'id': id, redirect: redirect, openInNewTab: openInNewTab };
notifications.value[ id ] = {
'message': message,
'showDuration': showDuration,
'messageType': msgType,
'priority': priority,
'id': id,
'redirect': redirect,
'openInNewTab': openInNewTab
};
queue.value.push( id ); queue.value.push( id );
console.log( 'scheduled notification: ' + id + ' (' + message + ')' ); console.log( 'scheduled notification: ' + id + ' (' + message + ')' );
if ( ( new Date().getTime() - notificationDisplayStartTime.value ) / 1000 >= ( notifications.value[ currentDID.value ] ? notifications.value[ currentDID.value ].showDuration : 0 ) || messageType.value === 'hide' ) { if ( ( new Date().getTime() - notificationDisplayStartTime.value ) / 1000 >= ( notifications.value[ currentDID.value ] ? notifications.value[ currentDID.value ].showDuration : 0 ) || messageType.value === 'hide' ) {
handleNotifications(); handleNotifications();
} }
return id; return id;
} };
/** /**
* Update a notification's message after creating it * Update a notification's message after creating it
@@ -113,7 +138,7 @@
if ( notifications.value[ id ] ) { if ( notifications.value[ id ] ) {
notifications.value[ id ].message = message; notifications.value[ id ].message = message;
} }
} };
/** /**
@@ -127,26 +152,31 @@
} catch ( error ) { } catch ( error ) {
console.log( 'notification to be deleted is nonexistent or currently being displayed' ); console.log( 'notification to be deleted is nonexistent or currently being displayed' );
} }
try { try {
queue.value.splice( queue.value.indexOf( id ), 1 ); queue.value.splice( queue.value.indexOf( id ), 1 );
} catch { } catch {
console.debug( 'queue empty' ); console.debug( 'queue empty' );
} }
if ( currentDID.value == id ) { if ( currentDID.value == id ) {
try { try {
clearTimeout( notificationTimeout ); clearTimeout( notificationTimeout );
} catch (err) { /* empty */ } } catch ( err ) { /* empty */ }
handleNotifications(); handleNotifications();
} }
} };
const handleNotifications = () => { const handleNotifications = () => {
notificationDisplayStartTime.value = new Date().getTime(); notificationDisplayStartTime.value = new Date().getTime();
queue.value.sort(); queue.value.sort();
if ( queue.value.length > 0 ) { if ( queue.value.length > 0 ) {
if ( currentDID.value !== 0 ) { if ( currentDID.value !== 0 ) {
delete notifications.value[ currentDID.value ]; delete notifications.value[ currentDID.value ];
} }
currentDID.value = notifications.value[ queue.value[ 0 ] ][ 'id' ]; currentDID.value = notifications.value[ queue.value[ 0 ] ][ 'id' ];
messageType.value = notifications.value[ queue.value[ 0 ] ].messageType; messageType.value = notifications.value[ queue.value[ 0 ] ].messageType;
queue.value.reverse(); queue.value.reverse();
@@ -158,22 +188,24 @@
} else { } else {
try { try {
clearInterval( progressBar ); clearInterval( progressBar );
} catch (err) { /* empty */ } } catch ( err ) { /* empty */ }
messageType.value = 'hide'; messageType.value = 'hide';
} }
} };
const progressBarHandler = () => { const progressBarHandler = () => {
currentTime.value = new Date().getTime(); currentTime.value = new Date().getTime();
} };
onUnmounted( () => { onUnmounted( () => {
try { try {
clearInterval( progressBar ); clearInterval( progressBar );
} catch (err) { /* empty */ } } catch ( err ) { /* empty */ }
try { try {
clearInterval( notificationTimeout ); clearInterval( notificationTimeout );
} catch (err) { /* empty */ } } catch ( err ) { /* empty */ }
} ); } );
defineExpose( { defineExpose( {

View File

@@ -4,51 +4,109 @@
<h3>WARNING!</h3> <h3>WARNING!</h3>
<p>A client display is being tampered with!</p> <p>A client display is being tampered with!</p>
<p>A desktop notification with a warning has already been dispatched.</p> <p>A desktop notification with a warning has already been dispatched.</p>
<button @click="dismissNotification()" class="simple-button">Ok</button> <button class="simple-button" @click="dismissNotification()">
Ok
</button>
<div class="flash"></div> <div class="flash"></div>
</div> </div>
<div class="player"> <div class="player">
<div :class="'main-player' + ( isShowingFullScreenPlayer ? ' full-screen' : '' )"> <div :class="'main-player' + ( isShowingFullScreenPlayer ? ' full-screen' : '' )">
<div :class="'song-name-wrapper' + ( isShowingFullScreenPlayer ? ' full-screen' : '' )" @click="controlUI( 'show' )"> <div :class="'song-name-wrapper' + ( isShowingFullScreenPlayer ? ' full-screen' : '' )" @click="controlUI( 'show' )">
<img src="https://github.com/simplePCBuilding/MusicPlayerV2/raw/master/assets/logo.png" alt="MusicPlayer Logo" class="logo-player" v-if="coverArt === ''"> <img
<img :src="coverArt" alt="MusicPlayer Logo" class="logo-player" v-else> v-if="coverArt === ''"
src="https://github.com/simplePCBuilding/MusicPlayerV2/raw/master/assets/logo.png"
alt="MusicPlayer Logo"
class="logo-player"
>
<img
v-else
:src="coverArt"
alt="MusicPlayer Logo"
class="logo-player"
>
<div class="name-time"> <div class="name-time">
<p class="song-name">{{ currentlyPlayingSongName }} <i v-if="currentlyPlayingSongArtist">by {{ currentlyPlayingSongArtist }}</i></p> <p class="song-name">
<div class="playback" v-if="!isShowingFullScreenPlayer"> {{ currentlyPlayingSongName }} <i v-if="currentlyPlayingSongArtist">by {{ currentlyPlayingSongArtist }}</i>
</p>
<div v-if="!isShowingFullScreenPlayer" class="playback">
<div class="playback-pos-wrapper"> <div class="playback-pos-wrapper">
<p class="playback-pos">{{ nicePlaybackPos }}</p> <p class="playback-pos">
{{ nicePlaybackPos }}
</p>
<p> / </p> <p> / </p>
<p class="playback-duration">{{ niceDuration }}</p> <p class="playback-duration">
{{ niceDuration }}
</p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div :class="'controls-wrapper' + ( isShowingFullScreenPlayer ? ' full-screen' : '' )" :style="playlist.length > 0 ? '' : 'pointer-events: none'"> <div :class="'controls-wrapper' + ( isShowingFullScreenPlayer ? ' full-screen' : '' )" :style="playlist.length > 0 ? '' : 'pointer-events: none'">
<div class="main-controls"> <div class="main-controls">
<span class="material-symbols-outlined controls next-previous" @click="control( 'previous' )" id="previous" v-if="isShowingFullScreenPlayer">skip_previous</span> <span
<span class="material-symbols-outlined controls forward-back" @click="control( 'back' )" :style="'rotate: -' + 360 * clickCountBack + 'deg;'" v-if="isShowingFullScreenPlayer">replay_10</span> v-if="isShowingFullScreenPlayer"
<span class="material-symbols-outlined controls" v-if="isPlaying" @click="playPause()" id="play-pause">pause</span> id="previous"
<span class="material-symbols-outlined controls" v-else @click="playPause()" id="play-pause">play_arrow</span> class="material-symbols-outlined controls next-previous"
<span class="material-symbols-outlined controls forward-back" @click="control( 'forward' )" :style="'rotate: ' + 360 * clickCountForward + 'deg;'" v-if="isShowingFullScreenPlayer">forward_10</span> @click="control( 'previous' )"
<span class="material-symbols-outlined controls next-previous" @click="control( 'next' )" id="next">skip_next</span> >skip_previous</span>
<span
v-if="isShowingFullScreenPlayer"
class="material-symbols-outlined controls forward-back"
:style="'rotate: -' + 360 * clickCountBack + 'deg;'"
@click="control( 'back' )"
>replay_10</span>
<span
v-if="isPlaying"
id="play-pause"
class="material-symbols-outlined controls"
@click="playPause()"
>pause</span>
<span
v-else
id="play-pause"
class="material-symbols-outlined controls"
@click="playPause()"
>play_arrow</span>
<span
v-if="isShowingFullScreenPlayer"
class="material-symbols-outlined controls forward-back"
:style="'rotate: ' + 360 * clickCountForward + 'deg;'"
@click="control( 'forward' )"
>forward_10</span>
<span id="next" class="material-symbols-outlined controls next-previous" @click="control( 'next' )">skip_next</span>
</div> </div>
<div class="slider-wrapper" v-if="isShowingFullScreenPlayer"> <div v-if="isShowingFullScreenPlayer" class="slider-wrapper">
<div class="slider-pb-pos"> <div class="slider-pb-pos">
<p class="playback-pos">{{ nicePlaybackPos }}</p> <p class="playback-pos">
<p class="playback-duration" @click="toggleRemaining()" title="Toggle between remaining time and song duration">{{ niceDuration }}</p> {{ nicePlaybackPos }}
</p>
<p class="playback-duration" title="Toggle between remaining time and song duration" @click="toggleRemaining()">
{{ niceDuration }}
</p>
</div> </div>
<sliderView :position="pos" :active="true" :duration="duration" name="main" @pos="( pos ) => goToPos( pos )"></sliderView> <sliderView
:position="pos"
:active="true"
:duration="duration"
name="main"
@pos="( pos ) => goToPos( pos )"
/>
</div> </div>
<div class="shuffle-repeat" v-if="isShowingFullScreenPlayer"> <div v-if="isShowingFullScreenPlayer" class="shuffle-repeat">
<span class="material-symbols-outlined controls" @click="control( 'repeat' )" style="margin-right: auto;">repeat{{ repeatMode }}</span> <span class="material-symbols-outlined controls" style="margin-right: auto;" @click="control( 'repeat' )">repeat{{ repeatMode }}</span>
<div style="margin-right: auto; pointer-events: all;"> <div style="margin-right: auto; pointer-events: all;">
<span class="material-symbols-outlined controls" @click="control( 'start-share' )" title="Share your playlist on a public playlist page (opens a configuration window)" v-if="!isConnectedToNotifier">share</span> <span
v-if="!isConnectedToNotifier"
class="material-symbols-outlined controls"
title="Share your playlist on a public playlist page (opens a configuration window)"
@click="control( 'start-share' )"
>share</span>
<div v-else> <div v-else>
<span class="material-symbols-outlined controls" @click="control( 'stop-share' )" title="Stop sharing your playlist on a public playlist page">close</span> <span class="material-symbols-outlined controls" title="Stop sharing your playlist on a public playlist page" @click="control( 'stop-share' )">close</span>
<span class="material-symbols-outlined controls" @click="control( 'show-share' )" title="Show information on the share, including URL to connect to">info</span> <span class="material-symbols-outlined controls" title="Show information on the share, including URL to connect to" @click="control( 'show-share' )">info</span>
</div> </div>
</div> </div>
<span class="material-symbols-outlined controls" @click="control( 'shuffle' )">shuffle{{ shuffleMode }}</span> <span class="material-symbols-outlined controls" @click="control( 'shuffle' )">shuffle{{ shuffleMode }}</span>
@@ -58,33 +116,49 @@
</div> </div>
<div :class="'playlist-view' + ( isShowingFullScreenPlayer ? '' : ' hidden' )"> <div :class="'playlist-view' + ( isShowingFullScreenPlayer ? '' : ' hidden' )">
<span class="material-symbols-outlined close-fullscreen" @click="controlUI( 'hide' )">close</span> <span class="material-symbols-outlined close-fullscreen" @click="controlUI( 'hide' )">close</span>
<playlistView :playlist="playlist" class="pl-wrapper" :currently-playing="currentlyPlayingSongIndex" :is-playing="isPlaying" :pos="pos" <playlistView
@control="( action ) => { control( action ) }" @play-song="( song ) => { playSong( song ) }" :playlist="playlist"
@add-new-songs="( songs ) => addNewSongs( songs )" @playlist-reorder="( move ) => moveSong( move )" class="pl-wrapper"
:currently-playing="currentlyPlayingSongIndex"
:is-playing="isPlaying"
:pos="pos"
:is-logged-into-apple-music="player.isLoggedIn" :is-logged-into-apple-music="player.isLoggedIn"
@control="( action ) => { control( action ) }"
@play-song="( song ) => { playSong( song ) }"
@add-new-songs="( songs ) => addNewSongs( songs )"
@playlist-reorder="( move ) => moveSong( move )"
@add-new-songs-apple-music="( song ) => addNewSongFromObject( song )" @add-new-songs-apple-music="( song ) => addNewSongFromObject( song )"
@delete-song="song => removeSongFromPlaylist( song )" @delete-song="song => removeSongFromPlaylist( song )"
@clear-playlist="() => clearPlaylist()" @clear-playlist="() => clearPlaylist()"
@send-additional-info="() => sendAdditionalInfo()"></playlistView> @send-additional-info="() => sendAdditionalInfo()"
/>
</div> </div>
<notificationsModule ref="notifications" location="bottomleft" size="bigger"></notificationsModule> <notificationsModule ref="notifications" location="bottomleft" size="bigger" />
<popupModule @update="( data ) => popupReturnHandler( data )" ref="popup"></popupModule> <popupModule ref="popup" @update="( data ) => popupReturnHandler( data )" />
<audio src="" id="local-audio" controls="false"></audio> <audio id="local-audio" src="" controls="false"></audio>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, type Ref } from 'vue'; import type {
import playlistView from '@/components/playlistView.vue'; ReadFile, Song, SongMove
} from '@/scripts/song';
import {
type Ref, ref
} from 'vue';
import MusicKitJSWrapper from '@/scripts/music-player'; import MusicKitJSWrapper from '@/scripts/music-player';
import sliderView from './sliderView.vue';
import type { ReadFile, Song, SongMove } from '@/scripts/song';
import { parseBlob } from 'music-metadata-browser';
import notificationsModule from './notificationsModule.vue';
import { useUserStore } from '@/stores/userStore';
import NotificationHandler from '@/scripts/notificationHandler'; import NotificationHandler from '@/scripts/notificationHandler';
import notificationsModule from './notificationsModule.vue';
import {
parseBlob
} from 'music-metadata';
import playlistView from '@/components/playlistView.vue';
import popupModule from './popupModule.vue'; import popupModule from './popupModule.vue';
import sliderView from './sliderView.vue';
import {
useUserStore
} from '@/stores/userStore';
const isPlaying = ref( false ); const isPlaying = ref( false );
const repeatMode = ref( '' ); const repeatMode = ref( '' );
@@ -100,7 +174,9 @@
const nicePlaybackPos = ref( '00:00' ); const nicePlaybackPos = ref( '00:00' );
const niceDuration = ref( '00:00' ); const niceDuration = ref( '00:00' );
const isShowingRemainingTime = ref( false ); const isShowingRemainingTime = ref( false );
let isShowingRemainingTimeBackend = false; let isShowingRemainingTimeBackend = false;
const currentlyPlayingSongArtist = ref( '' ); const currentlyPlayingSongArtist = ref( '' );
const pos = ref( 0 ); const pos = ref( 0 );
const duration = ref( 0 ); const duration = ref( 0 );
@@ -110,6 +186,7 @@
const popup = ref( popupModule ); const popup = ref( popupModule );
const roomName = ref( '' ); const roomName = ref( '' );
const isShowingWarning = ref( false ); const isShowingWarning = ref( false );
let currentlyOpenPopup = ''; let currentlyOpenPopup = '';
let logoutErrorNotification = -1; let logoutErrorNotification = -1;
@@ -118,19 +195,24 @@
document.addEventListener( 'musicplayer:autherror', () => { document.addEventListener( 'musicplayer:autherror', () => {
localStorage.setItem( 'close-tab', 'true' ); localStorage.setItem( 'close-tab', 'true' );
isConnectedToNotifier.value = false; isConnectedToNotifier.value = false;
logoutErrorNotification = notifications.value.createNotification( 'You appear to have been logged out. Click to log in again!', 600, 'error', 'critical', '/', true ); logoutErrorNotification = notifications.value.createNotification(
'You appear to have been logged out. Click to log in again!', 600, 'error', 'critical', '/', true
);
} ); } );
window.addEventListener( 'storage', () => { window.addEventListener( 'storage', () => {
if ( localStorage.getItem( 'login-ok' ) === 'true' ) { if ( localStorage.getItem( 'login-ok' ) === 'true' ) {
notifications.value.cancelNotification( logoutErrorNotification ); notifications.value.cancelNotification( logoutErrorNotification );
notifications.value.createNotification( 'Logged in again. You will have to reconnect to the share!', 20, 'ok', 'normal' ); notifications.value.createNotification(
'Logged in again. You will have to reconnect to the share!', 20, 'ok', 'normal'
);
localStorage.removeItem( 'login-ok' ); localStorage.removeItem( 'login-ok' );
} }
} ); } );
const playPause = () => { const playPause = () => {
isPlaying.value = !isPlaying.value; isPlaying.value = !isPlaying.value;
if ( isPlaying.value ) { if ( isPlaying.value ) {
player.control( 'play' ); player.control( 'play' );
startProgressTracker(); startProgressTracker();
@@ -138,17 +220,17 @@
player.control( 'pause' ); player.control( 'pause' );
stopProgressTracker(); stopProgressTracker();
} }
} };
const goToPos = ( position: number ) => { const goToPos = ( position: number ) => {
player.goToPos( position ); player.goToPos( position );
pos.value = position; pos.value = position;
notificationHandler.emit( 'playback-start-update', new Date().getTime() - pos.value * 1000 ); notificationHandler.emit( 'playback-start-update', new Date().getTime() - pos.value * 1000 );
} };
const toggleRemaining = () => { const toggleRemaining = () => {
isShowingRemainingTime.value = !isShowingRemainingTime.value; isShowingRemainingTime.value = !isShowingRemainingTime.value;
} };
const control = ( action: string ) => { const control = ( action: string ) => {
if ( action === 'pause' ) { if ( action === 'pause' ) {
@@ -184,11 +266,13 @@
getDetails(); getDetails();
notificationHandler.emit( 'playlist-update', playlist.value ); notificationHandler.emit( 'playlist-update', playlist.value );
} }
notificationHandler.emit( 'playlist-index-update', player.getQueueID() ); notificationHandler.emit( 'playlist-index-update', player.getQueueID() );
getDetails(); getDetails();
} else if ( action === 'forward' ) { } else if ( action === 'forward' ) {
clickCountForward.value += 1; clickCountForward.value += 1;
if( player.control( 'skip-10' ) ) {
if ( player.control( 'skip-10' ) ) {
startProgressTracker(); startProgressTracker();
} else { } else {
pos.value = player.getPlaybackPos(); pos.value = player.getPlaybackPos();
@@ -196,7 +280,8 @@
} }
} else if ( action === 'back' ) { } else if ( action === 'back' ) {
clickCountBack.value += 1; clickCountBack.value += 1;
if( player.control( 'back-10' ) ) {
if ( player.control( 'back-10' ) ) {
startProgressTracker(); startProgressTracker();
} else { } else {
pos.value = player.getPlaybackPos(); pos.value = player.getPlaybackPos();
@@ -218,19 +303,19 @@
startProgressTracker(); startProgressTracker();
} else if ( action === 'start-share' ) { } else if ( action === 'start-share' ) {
popup.value.openPopup( { popup.value.openPopup( {
title: 'Define a share name', 'title': 'Define a share name',
popupType: 'input', 'popupType': 'input',
subtitle: 'A share allows others to join your playlist and see the current song, the playback position and the upcoming songs. You can get the link to the page, once the share is set up. Please choose a name, which will then be part of the URL with which others can join the share. The anti tamper feature notifies you, whenever a user leaves the fancy view.', 'subtitle': 'A share allows others to join your playlist and see the current song, the playback position and the upcoming songs. You can get the link to the page, once the share is set up. Please choose a name, which will then be part of the URL with which others can join the share. The anti tamper feature notifies you, whenever a user leaves the fancy view.',
data: [ 'data': [
{ {
name: 'Share Name', 'name': 'Share Name',
dataType: 'text', 'dataType': 'text',
id: 'roomName' 'id': 'roomName'
}, },
{ {
name: 'Use Anti-Tamper?', 'name': 'Use Anti-Tamper?',
dataType: 'checkbox', 'dataType': 'checkbox',
id: 'useAntiTamper' 'id': 'useAntiTamper'
} }
] ]
} ); } );
@@ -239,19 +324,21 @@
if ( confirm( 'Do you really want to stop sharing?' ) ) { if ( confirm( 'Do you really want to stop sharing?' ) ) {
notificationHandler.disconnect(); notificationHandler.disconnect();
isConnectedToNotifier.value = false; isConnectedToNotifier.value = false;
notifications.value.createNotification( 'Disconnected successfully!', 5, 'ok', 'normal' ); notifications.value.createNotification(
'Disconnected successfully!', 5, 'ok', 'normal'
);
} }
} else if ( action === 'show-share' ) { } else if ( action === 'show-share' ) {
popup.value.openPopup( { popup.value.openPopup( {
title: 'Details on share', 'title': 'Details on share',
subtitle: 'You are currently connected to share "' + roomName.value 'subtitle': 'You are currently connected to share "' + roomName.value
+ '". \nYou can connect to it via <a href="https://music.janishutz.com/share/' + roomName.value + '" target="_blank">https://music.janishutz.com/share/' + roomName.value + '</a>' + '". \nYou can connect to it via <a href="https://music.janishutz.com/share/' + roomName.value + '" target="_blank">https://music.janishutz.com/share/' + roomName.value + '</a>'
+ '. \n\nYou can connect to the fancy showcase screen using this link: <a href="https://music.janishutz.com/fancy/' + roomName.value + '" target="_blank">https://music.janishutz.com/fancy/' + roomName.value + '</a>' + '. \n\nYou can connect to the fancy showcase screen using this link: <a href="https://music.janishutz.com/fancy/' + roomName.value + '" target="_blank">https://music.janishutz.com/fancy/' + roomName.value + '</a>'
+ '. Be aware that this one will use significantly more system AND network resources, so only use that for a screen that is front and center, not for a QR code to have all people connect to.' + '. Be aware that this one will use significantly more system AND network resources, so only use that for a screen that is front and center, not for a QR code to have all people connect to.'
} ); } );
currentlyOpenPopup = 'share-details'; currentlyOpenPopup = 'share-details';
} }
} };
const controlUI = ( action: string ) => { const controlUI = ( action: string ) => {
@@ -263,28 +350,30 @@
isShowingFullScreenPlayer.value = false; isShowingFullScreenPlayer.value = false;
isShowingRemainingTimeBackend = isShowingRemainingTime.value; isShowingRemainingTimeBackend = isShowingRemainingTime.value;
isShowingRemainingTime.value = false; isShowingRemainingTime.value = false;
try { try {
prepNiceDurationTime( player.getPlayingSong() ); prepNiceDurationTime( player.getPlayingSong() );
} catch ( err ) { /* empty */ } } catch ( err ) { /* empty */ }
emits( 'playerStateChange', 'hide' ); emits( 'playerStateChange', 'hide' );
} }
} };
const getPlaylists = ( cb: ( data: object ) => void ) => { const getPlaylists = ( cb: ( data: object ) => void ) => {
player.getUserPlaylists( cb ); player.getUserPlaylists( cb );
} };
const logIntoAppleMusic = () => { const logIntoAppleMusic = () => {
player.logIn(); player.logIn();
} };
const getAuth = (): boolean[] => { const getAuth = (): boolean[] => {
return player.getAuth(); return player.getAuth();
} };
const skipLogin = () => { const skipLogin = () => {
player.init(); player.init();
} };
const selectPlaylist = ( id: string ) => { const selectPlaylist = ( id: string ) => {
currentlyPlayingSongArtist.value = ''; currentlyPlayingSongArtist.value = '';
@@ -298,20 +387,26 @@
notificationHandler.emit( 'playlist-update', playlist.value ); notificationHandler.emit( 'playlist-update', playlist.value );
}, 2000 ); }, 2000 );
} ); } );
} };
const selectCustomPlaylist = async ( pl: ReadFile[] ) => { const selectCustomPlaylist = async ( pl: ReadFile[] ) => {
let n = notifications.value.createNotification( 'Analyzing playlist', 200, 'progress', 'normal' ); let n = notifications.value.createNotification(
'Analyzing playlist', 200, 'progress', 'normal'
);
playlist.value = []; playlist.value = [];
let plLoad: Song[] = []; const plLoad: Song[] = [];
for ( let element in pl ) { for ( let element in pl ) {
try { try {
plLoad.push( await fetchSongData( pl[ element ] ) ); plLoad.push( await fetchSongData( pl[ element ] ) );
} catch ( e ) { } catch ( e ) {
console.error( e ); console.error( e );
} }
notifications.value.updateNotification( n, `Analyzing playlist (${element}/${pl.length})` );
notifications.value.updateNotification( n, `Analyzing playlist (${ element }/${ pl.length })` );
} }
playlist.value = plLoad; playlist.value = plLoad;
player.setPlaylist( playlist.value ); player.setPlaylist( playlist.value );
player.prepare( 0 ); player.prepare( 0 );
@@ -322,82 +417,106 @@
notificationHandler.emit( 'playlist-update', playlist.value ); notificationHandler.emit( 'playlist-update', playlist.value );
}, 2000 ); }, 2000 );
notifications.value.cancelNotification( n ); notifications.value.cancelNotification( n );
notifications.value.createNotification( 'Playlist loaded', 10, 'ok', 'normal' ); notifications.value.createNotification(
} 'Playlist loaded', 10, 'ok', 'normal'
);
};
const fetchSongData = ( songDetails: ReadFile ): Promise<Song> => { const fetchSongData = ( songDetails: ReadFile ): Promise<Song> => {
return new Promise( ( resolve, reject ) => { return new Promise( ( resolve, reject ) => {
fetch( songDetails.url ).then( res => { console.info( 'Loading song', songDetails.filename );
if ( res.status === 200 ) { fetch( songDetails.url )
res.blob().then( blob => { .then( res => {
parseBlob( blob ).then( data => { if ( res.status === 200 ) {
try { res.blob().then( blob => {
player.findSongOnAppleMusic( data.common.title ?? songDetails.filename.split( '.' )[ 0 ] ).then( d => { console.info( 'Song loaded for processing' );
let url = d.data.results.songs.data[ 0 ].attributes.artwork.url; parseBlob( blob )
url = url.replace( '{w}', String( d.data.results.songs.data[ 0 ].attributes.artwork.width ) ); .then( data => {
url = url.replace( '{h}', String( d.data.results.songs.data[ 0 ].attributes.artwork.height ) ); console.info( 'Song metadata processing successful' );
const song: Song = {
artist: d.data.results.songs.data[ 0 ].attributes.artistName, try {
title: d.data.results.songs.data[ 0 ].attributes.name, player.findSongOnAppleMusic( data.common.title
duration: data.format.duration ?? ( d.data.results.songs.data[ 0 ].attributes.durationInMillis / 1000 ), ?? songDetails.filename.split( '.' )[ 0 ] )
id: songDetails.url, .then( d => {
origin: 'disk', console.info( 'Apple Music API lookup successful' );
cover: url 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
};
resolve( song );
} )
.catch( e => {
console.info( 'Apple Music API failed' );
console.error( e );
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': ''
};
resolve( song );
} );
} catch ( err ) {
console.error( err );
alert( 'One of your songs was not loadable. (finalization-error)' );
reject( err );
} }
resolve( song ); } )
} ).catch( e => { .catch( e => {
console.error( e ); console.error( e );
const song: Song = { alert( 'One of your songs was not loadable. (parser-error)' );
artist: data.common.artist ?? 'Unknown artist', reject( e );
title: data.common.title ?? 'Unknown song title',
duration: data.format.duration ?? 1000,
id: songDetails.url,
origin: 'disk',
cover: ''
}
resolve( song );
} ); } );
} catch ( err ) { } )
console.error( err ); .catch( e => {
alert( 'One of your songs was not loadable. (finalization-error)' ) console.error( e );
} alert( 'One of your songs was not loadable. (converter-error)' );
} ).catch( e => { reject( e );
console.error( e ); } );
alert( 'One of your songs was not loadable. (parser-error)' ); } else {
reject( e ); console.error( res.status );
} ); alert( 'One of your songs was not loadable. (invalid-response-code)' );
} ).catch( e => { reject( res.status );
console.error( e ); }
alert( 'One of your songs was not loadable. (converter-error)' ); } )
reject( e ); .catch( e => {
} ); console.error( e );
} else { alert( 'One of your songs was not loadable. (could-not-connect)' );
console.error( res.status ); reject( e );
alert( 'One of your songs was not loadable. (invalid-response-code)' ); } );
}
} ).catch( e => {
console.error( e );
alert( 'One of your songs was not loadable. (could-not-connect)' );
reject( e );
} );
} ); } );
} };
const getDetails = () => { const getDetails = () => {
const details = player.getPlayingSong(); const details = player.getPlayingSong();
currentlyPlayingSongName.value = details.title; currentlyPlayingSongName.value = details.title;
coverArt.value = details.cover; coverArt.value = details.cover;
currentlyPlayingSongIndex.value = player.getQueueID(); currentlyPlayingSongIndex.value = player.getQueueID();
playlist.value = player.getQueue(); playlist.value = player.getQueue();
currentlyPlayingSongArtist.value = details.artist; currentlyPlayingSongArtist.value = details.artist;
} };
const playSong = ( id: string ) => { const playSong = ( id: string ) => {
const p = player.getPlaylist(); const p = player.getPlaylist();
currentlyPlayingSongArtist.value = ''; currentlyPlayingSongArtist.value = '';
coverArt.value = ''; coverArt.value = '';
currentlyPlayingSongName.value = 'Loading...'; currentlyPlayingSongName.value = 'Loading...';
stopProgressTracker(); stopProgressTracker();
for ( const s in p ) { for ( const s in p ) {
if ( p[ s ].id === id ) { if ( p[ s ].id === id ) {
player.prepare( parseInt( s ) ); player.prepare( parseInt( s ) );
@@ -405,24 +524,29 @@
break; break;
} }
} }
} };
let progressTracker: ReturnType<typeof setInterval> = setInterval( () => {}, 1000 ); let progressTracker: ReturnType<typeof setInterval> = setInterval( () => {}, 1000 );
clearInterval( progressTracker ); clearInterval( progressTracker );
let hasReachedEnd = false; let hasReachedEnd = false;
let hasStarted = false; let hasStarted = false;
const startProgressTracker = () => { const startProgressTracker = () => {
hasReachedEnd = false; hasReachedEnd = false;
isPlaying.value = true; isPlaying.value = true;
let playingSong = player.getPlayingSong(); let playingSong = player.getPlayingSong();
hasStarted = false; hasStarted = false;
pos.value = 0; pos.value = 0;
progressTracker = setInterval( () => { progressTracker = setInterval( () => {
pos.value = player.getPlaybackPos(); pos.value = player.getPlaybackPos();
if ( pos.value > playingSong.duration - 1 && !hasReachedEnd ) { if ( pos.value > playingSong.duration - 1 && !hasReachedEnd ) {
stopProgressTracker(); stopProgressTracker();
hasReachedEnd = true; hasReachedEnd = true;
if ( repeatMode.value === '_one_on' ) { if ( repeatMode.value === '_one_on' ) {
player.goToPos( 0 ); player.goToPos( 0 );
setTimeout( () => { setTimeout( () => {
@@ -449,11 +573,15 @@
} }
const minuteCount = Math.floor( pos.value / 60 ); const minuteCount = Math.floor( pos.value / 60 );
nicePlaybackPos.value = minuteCount + ':'; nicePlaybackPos.value = minuteCount + ':';
if ( ( '' + minuteCount ).length === 1 ) { if ( ( '' + minuteCount ).length === 1 ) {
nicePlaybackPos.value = '0' + minuteCount + ':'; nicePlaybackPos.value = '0' + minuteCount + ':';
} }
const secondCount = Math.floor( pos.value - minuteCount * 60 ); const secondCount = Math.floor( pos.value - minuteCount * 60 );
if ( ( '' + secondCount ).length === 1 ) { if ( ( '' + secondCount ).length === 1 ) {
nicePlaybackPos.value += '0' + secondCount; nicePlaybackPos.value += '0' + secondCount;
} else { } else {
@@ -462,11 +590,15 @@
if ( isShowingRemainingTime.value ) { if ( isShowingRemainingTime.value ) {
const minuteCounts = Math.floor( ( playingSong.duration - pos.value ) / 60 ); const minuteCounts = Math.floor( ( playingSong.duration - pos.value ) / 60 );
niceDuration.value = '-' + String( minuteCounts ) + ':'; niceDuration.value = '-' + String( minuteCounts ) + ':';
if ( ( '' + minuteCounts ).length === 1 ) { if ( ( '' + minuteCounts ).length === 1 ) {
niceDuration.value = '-0' + minuteCounts + ':'; 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 ) { if ( ( '' + secondCounts ).length === 1 ) {
niceDuration.value += '0' + secondCounts; niceDuration.value += '0' + secondCounts;
} else { } else {
@@ -474,82 +606,101 @@
} }
} }
}, 100 ); }, 100 );
} };
const prepNiceDurationTime = ( playingSong: Song ) => { const prepNiceDurationTime = ( playingSong: Song ) => {
duration.value = playingSong.duration; duration.value = playingSong.duration;
const minuteCounts = Math.floor( ( playingSong.duration ) / 60 ); const minuteCounts = Math.floor( playingSong.duration / 60 );
niceDuration.value = String( minuteCounts ) + ':'; niceDuration.value = String( minuteCounts ) + ':';
if ( ( '' + minuteCounts ).length === 1 ) { if ( ( '' + minuteCounts ).length === 1 ) {
niceDuration.value = '0' + minuteCounts + ':'; niceDuration.value = '0' + minuteCounts + ':';
} }
const secondCounts = Math.floor( ( playingSong.duration ) - minuteCounts * 60 );
const secondCounts = Math.floor( playingSong.duration - minuteCounts * 60 );
if ( ( '' + secondCounts ).length === 1 ) { if ( ( '' + secondCounts ).length === 1 ) {
niceDuration.value += '0' + secondCounts; niceDuration.value += '0' + secondCounts;
} else { } else {
niceDuration.value += secondCounts; niceDuration.value += secondCounts;
} }
} };
const stopProgressTracker = () => { const stopProgressTracker = () => {
try { try {
clearInterval( progressTracker ); clearInterval( progressTracker );
} catch ( _ ) { /* empty */ } } catch ( _ ) { /* empty */ }
isPlaying.value = false; isPlaying.value = false;
notificationHandler.emit( 'playback-update', isPlaying.value ); notificationHandler.emit( 'playback-update', isPlaying.value );
} };
const moveSong = ( move: SongMove ) => { const moveSong = ( move: SongMove ) => {
player.moveSong( move ); player.moveSong( move );
getDetails(); getDetails();
notificationHandler.emit( 'playlist-update', playlist.value ); notificationHandler.emit( 'playlist-update', playlist.value );
} };
const addNewSongs = async ( songs: ReadFile[] ) => { const addNewSongs = async ( songs: ReadFile[] ) => {
let n = notifications.value.createNotification( 'Analyzing new songs', 200, 'progress', 'normal' ); let n = notifications.value.createNotification(
'Analyzing new songs', 200, 'progress', 'normal'
);
playlist.value = player.getQueue(); playlist.value = player.getQueue();
for ( let element in songs ) { for ( let element in songs ) {
try { try {
playlist.value.push( await fetchSongData( songs[ element ] ) ); playlist.value.push( await fetchSongData( songs[ element ] ) );
} catch ( e ) { } catch ( e ) {
console.error( e ); console.error( e );
} }
notifications.value.updateNotification( n, `Analyzing new songs (${element}/${songs.length})` );
notifications.value.updateNotification( n, `Analyzing new songs (${ element }/${ songs.length })` );
} }
player.setPlaylist( playlist.value ); player.setPlaylist( playlist.value );
if ( !isPlaying.value ) { if ( !isPlaying.value ) {
player.prepare( 0 ); player.prepare( 0 );
isPlaying.value = true; isPlaying.value = true;
startProgressTracker(); startProgressTracker();
} }
notifications.value.cancelNotification( n ); notifications.value.cancelNotification( n );
notifications.value.createNotification( 'New songs added', 10, 'ok', 'normal' ); notifications.value.createNotification(
'New songs added', 10, 'ok', 'normal'
);
notificationHandler.emit( 'playlist-update', playlist.value ); notificationHandler.emit( 'playlist-update', playlist.value );
} };
const addNewSongFromObject = ( song: Song ) => { const addNewSongFromObject = ( song: Song ) => {
playlist.value = player.getQueue(); playlist.value = player.getQueue();
playlist.value.push( song ); playlist.value.push( song );
player.setPlaylist( playlist.value ); player.setPlaylist( playlist.value );
if ( !isPlaying.value ) { if ( !isPlaying.value ) {
player.prepare( 0 ); player.prepare( 0 );
isPlaying.value = true; isPlaying.value = true;
startProgressTracker(); startProgressTracker();
} }
notificationHandler.emit( 'playlist-update', playlist.value ); notificationHandler.emit( 'playlist-update', playlist.value );
} };
const removeSongFromPlaylist = ( song: number ) => { const removeSongFromPlaylist = ( song: number ) => {
playlist.value = player.getQueue(); playlist.value = player.getQueue();
playlist.value.splice( song, 1 ); playlist.value.splice( song, 1 );
player.setPlaylist( playlist.value ); player.setPlaylist( playlist.value );
if ( !isPlaying.value ) { if ( !isPlaying.value ) {
player.prepare( 0 ); player.prepare( 0 );
isPlaying.value = true; isPlaying.value = true;
startProgressTracker(); startProgressTracker();
} }
notificationHandler.emit( 'playlist-update', playlist.value ); notificationHandler.emit( 'playlist-update', playlist.value );
} };
const clearPlaylist = () => { const clearPlaylist = () => {
playlist.value = []; playlist.value = [];
@@ -562,18 +713,20 @@
coverArt.value = ''; coverArt.value = '';
pos.value = 0; pos.value = 0;
notificationHandler.emit( 'playlist-update', playlist.value ); notificationHandler.emit( 'playlist-update', playlist.value );
} };
const sendAdditionalInfo = () => { const sendAdditionalInfo = () => {
notifications.value.createNotification( 'Additional song info transmitted', 5, 'ok', 'normal' ); notifications.value.createNotification(
'Additional song info transmitted', 5, 'ok', 'normal'
);
notificationHandler.emit( 'playlist-update', playlist.value ); notificationHandler.emit( 'playlist-update', playlist.value );
} };
emits( 'playerStateChange', isShowingFullScreenPlayer.value ? 'show' : 'hide' ); emits( 'playerStateChange', isShowingFullScreenPlayer.value ? 'show' : 'hide' );
const userStore = useUserStore(); const userStore = useUserStore();
document.addEventListener( 'keydown', ( e ) => { document.addEventListener( 'keydown', e => {
if ( !userStore.isUsingKeyboard ) { if ( !userStore.isUsingKeyboard ) {
if ( e.key === ' ' ) { if ( e.key === ' ' ) {
e.preventDefault(); e.preventDefault();
@@ -590,7 +743,7 @@
const dismissNotification = () => { const dismissNotification = () => {
isShowingWarning.value = false; isShowingWarning.value = false;
} };
const popupReturnHandler = ( data: any ) => { const popupReturnHandler = ( data: any ) => {
if ( currentlyOpenPopup === 'create-share' ) { if ( currentlyOpenPopup === 'create-share' ) {
@@ -601,26 +754,35 @@
notificationHandler.emit( 'playback-update', isPlaying.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 );
notificationHandler.emit( 'playlist-update', playlist.value ); notificationHandler.emit( 'playlist-update', playlist.value );
notifications.value.createNotification( 'Joined share "' + data.roomName + '"!', 5, 'ok', 'normal' ); notifications.value.createNotification(
'Joined share "' + data.roomName + '"!', 5, 'ok', 'normal'
);
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
notificationHandler.registerListener( 'tampering-msg', ( _ ) => { notificationHandler.registerListener( 'tampering-msg', _ => {
isShowingWarning.value = true; isShowingWarning.value = true;
} ); } );
} ).catch( e => { } )
if ( e === 'ERR_CONFLICT' ) { .catch( e => {
notifications.value.createNotification( 'A share with this name exists already!', 5, 'error', 'normal' ); if ( e === 'ERR_CONFLICT' ) {
control( 'start-share' ); notifications.value.createNotification(
} else if ( e === 'ERR_UNAUTHORIZED' ) { 'A share with this name exists already!', 5, 'error', 'normal'
console.error( e ); );
localStorage.setItem( 'close-tab', 'true' ); control( 'start-share' );
logoutErrorNotification = notifications.value.createNotification( 'You appear to have been logged out. Click to log in again!', 20, 'error', 'normal', '/', true ); } else if ( e === 'ERR_UNAUTHORIZED' ) {
} else { console.error( e );
console.error( e ); localStorage.setItem( 'close-tab', 'true' );
notifications.value.createNotification( 'Could not create share!', 5, 'error', 'normal' ); logoutErrorNotification = notifications.value.createNotification(
} 'You appear to have been logged out. Click to log in again!', 20, 'error', 'normal', '/', true
} ); );
} else {
console.error( e );
notifications.value.createNotification(
'Could not create share!', 5, 'error', 'normal'
);
}
} );
} }
} };
window.addEventListener( 'beforeunload', async () => { window.addEventListener( 'beforeunload', async () => {
await notificationHandler.disconnect(); await notificationHandler.disconnect();

View File

@@ -1,6 +1,8 @@
<template> <template>
<div class="playlists"> <div class="playlists">
<h3 style="width: fit-content;">Your playlists</h3> <h3 style="width: fit-content;">
Your playlists
</h3>
<div v-if="( $props.playlists ? $props.playlists.length < 1 : true ) && $props.isLoggedIn"> <div v-if="( $props.playlists ? $props.playlists.length < 1 : true ) && $props.isLoggedIn">
Loading... Loading...
<!-- TODO: Make prettier --> <!-- TODO: Make prettier -->
@@ -8,12 +10,27 @@
<div v-else-if="!$props.isLoggedIn" class="not-logged-in"> <div v-else-if="!$props.isLoggedIn" class="not-logged-in">
<p>You are not logged into Apple Music. We therefore can't show you your playlists. <a href="" title="Refreshes the page, allowing you to log in">Change that</a></p> <p>You are not logged into Apple Music. We therefore can't show you your playlists. <a href="" title="Refreshes the page, allowing you to log in">Change that</a></p>
<p>Use the button below to load songs from your local disk</p> <p>Use the button below to load songs from your local disk</p>
<input class="pl-loader-button" type="file" multiple="true" accept="audio/*" id="pl-loader"><br> <input
<button @click="loadPlaylistFromDisk()" class="pl-loader-button" id="load-button">Load</button> id="pl-loader"
<p v-if="!hasSelectedSongs">Please select at least one song to proceed!</p> class="pl-loader-button"
type="file"
multiple="true"
accept="audio/*"
><br>
<button id="load-button" class="pl-loader-button" @click="loadPlaylistFromDisk()">
Load
</button>
<p v-if="!hasSelectedSongs">
Please select at least one song to proceed!
</p>
</div> </div>
<div class="playlist-wrapper"> <div class="playlist-wrapper">
<div v-for="pl in $props.playlists" v-bind:key="pl.id" class="playlist" @click="selectPlaylist( pl.id )"> <div
v-for="pl in $props.playlists"
:key="pl.id"
class="playlist"
@click="selectPlaylist( pl.id )"
>
{{ pl.attributes.name }} {{ pl.attributes.name }}
</div> </div>
</div> </div>
@@ -21,8 +38,13 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { ReadFile } from '@/scripts/song'; import type {
import { ref } from 'vue'; ReadFile
} from '@/scripts/song';
import {
ref
} from 'vue';
const hasSelectedSongs = ref( true ); const hasSelectedSongs = ref( true );
defineProps( { defineProps( {
@@ -41,22 +63,31 @@
const loadPlaylistFromDisk = () => { const loadPlaylistFromDisk = () => {
const fileURLList: ReadFile[] = []; const fileURLList: ReadFile[] = [];
const allFiles = ( document.getElementById( 'pl-loader' ) as HTMLInputElement ).files ?? []; const allFiles = ( document.getElementById( 'pl-loader' ) as HTMLInputElement ).files ?? [];
if ( allFiles.length > 0 ) { if ( allFiles.length > 0 ) {
hasSelectedSongs.value = true; hasSelectedSongs.value = true;
for ( let file = 0; file < allFiles.length; file++ ) { 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( 'custom-playlist', fileURLList ); emits( 'custom-playlist', fileURLList );
} else { } else {
hasSelectedSongs.value = false; hasSelectedSongs.value = false;
} }
} };
const emits = defineEmits( [ 'selected-playlist', 'custom-playlist' ] ); const emits = defineEmits( [
'selected-playlist',
'custom-playlist'
] );
const selectPlaylist = ( id: string ) => { const selectPlaylist = ( id: string ) => {
emits( 'selected-playlist', id ); emits( 'selected-playlist', id );
} };
</script> </script>
<style scoped> <style scoped>

View File

@@ -555,9 +555,10 @@ class MusicKitJSWrapper {
'types': [ 'songs' ], 'types': [ 'songs' ],
}; };
this.musicKit.api.music( 'v1/catalog/ch/search', queryParameters ).then( results => { this.musicKit.api.music( 'v1/catalog/ch/search', queryParameters )
resolve( results ); .then( results => {
} ) resolve( results );
} )
.catch( e => { .catch( e => {
console.error( e ); console.error( e );
reject( e ); reject( e );

3671
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff