From 01526034ce788e8245eb1e20861e5cac8ef905ec Mon Sep 17 00:00:00 2001 From: Janis Hutz Date: Tue, 1 Sep 2026 18:16:11 +0200 Subject: [PATCH] feat: redesign main page, get player working (mostly) with Apple Music source --- backend/package.json | 10 +- backend/src/app.ts | 7 +- web/package.json | 2 +- web/src/App.vue | 23 +-- web/src/components/AddSong.vue | 48 ++++- web/src/components/CurrentSong.vue | 51 +++++ web/src/components/PlayerControls.vue | 19 +- web/src/components/PopupElement.vue | 7 +- web/src/components/ProgressBar.vue | 13 +- web/src/components/QueueViewer.vue | 42 ++-- web/src/composables/SearchView.vue | 7 +- web/src/router/index.ts | 17 +- web/src/scss/components/player.scss | 17 +- web/src/scss/components/queue.scss | 193 +++++++----------- web/src/scss/components/sortablelist.scss | 1 + web/src/stores/authstore.ts | 2 +- web/src/ts/player/index.ts | 24 ++- web/src/ts/player/playlists/add.ts | 13 +- web/src/ts/player/playlists/index.ts | 13 +- web/src/ts/player/plugins/interface.d.ts | 7 +- web/src/ts/player/plugins/local/index.ts | 5 +- web/src/ts/player/plugins/musickit/index.ts | 2 +- .../ts/player/plugins/musickit/playback.ts | 15 +- .../player/plugins/musickit/search/dtype.d.ts | 12 +- .../plugins/musickit/search/playlists.ts | 30 ++- .../player/plugins/musickit/search/songs.ts | 14 +- web/src/ts/player/status-tracking.ts | 8 +- web/src/ts/util/time.ts | 8 +- web/src/views/AppView.vue | 34 ++- 29 files changed, 425 insertions(+), 219 deletions(-) create mode 100644 web/src/components/CurrentSong.vue diff --git a/backend/package.json b/backend/package.json index 89df566..3b1a23c 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,17 +1,19 @@ { "dependencies": { + "@types/cors": "^2.8.19", + "cors": "^2.8.6", "express": "^5.2.1", "express-session": "^1.19.0", "jsonwebtoken": "^9.0.3" }, "devDependencies": { - "eslint": "^10.9.0", - "@types/express": "^5.0.6", - "@types/express-session": "^1.19.0", - "@types/node": "^26.2.0", "@eslint/js": "^10.0.1", "@stylistic/eslint-plugin": "^5.10.0", + "@types/express": "^5.0.6", + "@types/express-session": "^1.19.0", "@types/jsonwebtoken": "^9.0.10", + "@types/node": "^26.2.0", + "eslint": "^10.9.0", "eslint-plugin-vue": "^10.10.0", "globals": "^17.11.0", "typescript-eslint": "^8.67.0" diff --git a/backend/src/app.ts b/backend/src/app.ts index a0488e8..9b4a449 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -1,3 +1,4 @@ +import cors from 'cors'; import express from 'express'; import expressSession from 'express-session'; import fs from 'fs'; @@ -29,7 +30,11 @@ const run = () => { __dirname, '/config/apple-music-api.config.secret.json' ) ).toString() ); - app.get( '/dev-token', ( _request: express.Request, response: express.Response ) => { + + app.get( '/dev-token', cors( { + 'origin': 'http://localhost:8081', + 'credentials': true + } ), ( _request: express.Request, response: express.Response ) => { // sign dev token const now = new Date().getTime(); const tomorrow = now + ( 24 * 3600 * 1000 ); diff --git a/web/package.json b/web/package.json index 6d132c4..c36c46c 100644 --- a/web/package.json +++ b/web/package.json @@ -30,7 +30,7 @@ "@vue/tsconfig": "^0.9.1", "eslint-plugin-vue": "^10.10.0", "globals": "^17.11.0", - "musickitjs-v3-types": "^1.1.1", + "musickitjs-v3-types": "^1.2.1", "npm-run-all2": "^9.0.2", "sass-embedded": "^1.103.1", "typescript": "~6.0.0", diff --git a/web/src/App.vue b/web/src/App.vue index 72561e6..5fab23e 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -6,38 +6,37 @@ ref } from 'vue'; - const theme = ref( '☼' ); + const theme = ref( 'dark' ); const changeTheme = () => { - if ( theme.value === '☽' ) { + if ( theme.value === 'moon' ) { document.documentElement.classList.remove( 'dark' ); document.documentElement.classList.add( 'light' ); - localStorage.setItem( 'theme', '☼' ); - theme.value = '☼'; - } else if ( theme.value === '☼' ) { + localStorage.setItem( 'theme', 'light' ); + theme.value = 'sun'; + } else if ( theme.value === 'sun' ) { document.documentElement.classList.remove( 'light' ); document.documentElement.classList.add( 'dark' ); - localStorage.setItem( 'theme', '☽' ); - theme.value = '☽'; + localStorage.setItem( 'theme', 'dark' ); + theme.value = 'moon'; } }; theme.value = localStorage.getItem( 'theme' ) ?? ''; - if ( window.matchMedia( '(prefers-color-scheme: dark)' ).matches || theme.value === '☽' ) { + if ( window.matchMedia( '(prefers-color-scheme: dark)' ).matches || theme.value === 'dark' ) { document.documentElement.classList.add( 'dark' ); - theme.value = '☽'; + theme.value = 'moon'; } else { document.documentElement.classList.add( 'light' ); - theme.value = '☼'; + theme.value = 'sun'; } diff --git a/web/src/components/PlayerControls.vue b/web/src/components/PlayerControls.vue index efeff84..fc581dc 100644 --- a/web/src/components/PlayerControls.vue +++ b/web/src/components/PlayerControls.vue @@ -3,10 +3,12 @@ import { beautifyTime } from '@/ts/util/time'; + import { + computed + } from 'vue'; import player from '@/ts/player'; const playbackPercentage = player.playbackPercentage; - const duration = player.duration; const repeatMode = player.repeat; const shuffleMode = player.shuffle; @@ -31,6 +33,14 @@ alert( 'Share menu not yet implemented' ); }; + const current = computed( () => { + return beautifyTime( player.playbackPercentage.value * player.duration.value ); + } ); + const duration = computed( () => { + return beautifyTime( player.duration.value ); + } ); + + // TODO: Button availability @@ -39,7 +49,7 @@
-
+
@@ -48,12 +58,11 @@
-

- {{ beautifyTime( playbackPercentage * duration ) }} + {{ current }}

- {{ beautifyTime( duration ) }} + {{ duration }}

diff --git a/web/src/components/PopupElement.vue b/web/src/components/PopupElement.vue index 4ab41ff..89faca0 100644 --- a/web/src/components/PopupElement.vue +++ b/web/src/components/PopupElement.vue @@ -33,6 +33,7 @@ align-items: center; z-index: 1000; transition-delay: 0; + overflow: hidden; .popup-backdrop { width: 100vw; @@ -42,7 +43,7 @@ top: 0; left: 0; overflow: hidden; - transition: all 0.5s; + transition: all 0.25s; } .popup-main { @@ -53,7 +54,7 @@ border-radius: 20px; transform: scale(1); position: relative; - overflow-y: scroll; + overflow: hidden; display: block; transition: transform 0.25s ease, opacity 0.25s ease; @@ -69,7 +70,7 @@ &.hidden { top: -100vh; - transition-delay: 0.5s; + transition-delay: 0.25s; .popup-main { transform: scale(1.25); diff --git a/web/src/components/ProgressBar.vue b/web/src/components/ProgressBar.vue index e8a233a..62d167d 100644 --- a/web/src/components/ProgressBar.vue +++ b/web/src/components/ProgressBar.vue @@ -9,22 +9,27 @@ 'default': 0.5 } ); const offset = ref( -1 ); + const isMoving = ref( false ); const bar = useTemplateRef( 'bar' ); const start = ( ev: MouseEvent ) => { - offset.value = ev.x - bar.value!.offsetLeft; - val.value = offset.value / bar.value!.clientWidth; + offset.value = bar.value!.getBoundingClientRect().x; + isMoving.value = true; + val.value = ( ev.x - offset.value ) / bar.value!.clientWidth; emit( 'move-start' ); }; const move = ( ev: MouseEvent ) => { - if ( offset.value > -1 ) { - val.value = Math.max( 0, Math.min( ev.x / bar.value!.clientWidth, 1 ) ); + if ( isMoving.value ) { + val.value = Math.max( 0, Math.min( ( ev.x - offset.value ) / bar.value!.clientWidth, 1 ) ); } }; const end = () => { + if ( !isMoving.value ) return; + offset.value = -1; + isMoving.value = false; emit( 'move-end' ); }; diff --git a/web/src/components/QueueViewer.vue b/web/src/components/QueueViewer.vue index 3251607..0354e99 100644 --- a/web/src/components/QueueViewer.vue +++ b/web/src/components/QueueViewer.vue @@ -15,16 +15,18 @@ beautifyTime } from '@/ts/util/time'; import player from '@/ts/player'; + import { + queueIdx + } from '@/ts/player/state'; const queue: WritableComputedRef = computed( { get () { - return player.queue.value.slice( player.queueIdx.value ); + return player.queue.value.slice( player.queueIdx.value + 1 ); }, set ( val ) { - player.queue.value = player.queue.value.slice( 0, player.queueIdx.value ).concat( val ); + player.queue.value = player.queue.value.slice( 0, player.queueIdx.value + 1 ).concat( val ); } } ); - const isPlaying = player.isPlaying; const showAddSong = ref( false ); const showEditSong = ref( false ); const editingSong: Ref = ref( null ); @@ -35,16 +37,16 @@ const editSong = ( idx: number ) => { showEditSong.value = true; - editingSong.value = player.queue.value[ idx ]!; + editingSong.value = player.queue.value[ idx + queueIdx.value + 1 ]!; }; const deleteSong = ( idx: number ) => { - player.removeSong( idx ); + player.removeSong( idx + queueIdx.value + 1 ); }; diff --git a/web/src/composables/SearchView.vue b/web/src/composables/SearchView.vue index a547b6f..3963ea7 100644 --- a/web/src/composables/SearchView.vue +++ b/web/src/composables/SearchView.vue @@ -70,9 +70,9 @@ placeholder="Search..." @keypress="search" > -
+
- +

{{ result.name }}

{{ result.artist }}

@@ -80,6 +80,9 @@
+
+ No results found +
Searching...
diff --git a/web/src/router/index.ts b/web/src/router/index.ts index 956d739..2bdd70c 100644 --- a/web/src/router/index.ts +++ b/web/src/router/index.ts @@ -1,9 +1,10 @@ +import { + createRouter, + createWebHistory +} from 'vue-router'; import { useAuthStore } from '@/stores/authstore'; -import { - createRouter, createWebHistory -} from 'vue-router'; const router = createRouter( { 'history': createWebHistory( import.meta.env.BASE_URL ), @@ -38,16 +39,16 @@ const router = createRouter( { ] } ); -router.beforeEach( ( to, from ) => { +router.beforeEach( to => { const store = useAuthStore(); - if ( to.meta.authRequired && !store.isAuth ) { + if ( to.meta.auth && !store.isAuth ) { return { - 'name': 'login' + 'name': 'home' }; - } else if ( ( to.name === 'login' && store.isAuth ) || ( to.name === 'signup' && store.isAuth ) ) { + } else if ( to.name === 'home' && store.isAuth ) { return { - 'name': 'app-home' + 'name': 'app' }; } } ); diff --git a/web/src/scss/components/player.scss b/web/src/scss/components/player.scss index 2fd0a7f..eb8aeb0 100644 --- a/web/src/scss/components/player.scss +++ b/web/src/scss/components/player.scss @@ -1,6 +1,5 @@ .mp-player { width: 100%; - height: 100%; display: flex; flex-direction: column; @@ -19,10 +18,15 @@ display: flex; align-items: center; justify-content: center; + height: 3rem; >.play-pause { cursor: pointer; + >.fa-play { + display: none; + } + &.paused { >.fa-pause { display: none; @@ -60,6 +64,11 @@ margin-left: auto; margin-right: auto; + >p { + margin-top: 0px; + margin-bottom: 5px; + } + >.duration { margin-left: auto; } @@ -70,6 +79,12 @@ } >.bottom-bar { + display: flex; + align-items: center; + justify-content: center; + height: 3rem; + margin-top: 15px; + >.fa-shuffle { &.active { background-color: var(--primary-color); diff --git a/web/src/scss/components/queue.scss b/web/src/scss/components/queue.scss index 99e2e20..c925c4c 100644 --- a/web/src/scss/components/queue.scss +++ b/web/src/scss/components/queue.scss @@ -1,127 +1,92 @@ -.song-list-element { - display: flex; - width: 100%; +.queue-viewer { + height: 100%; + width: 90%; + overflow: hidden; - >.song-cover-wrapper { - height: 10rem; - width: 10rem; - position: relative; - margin-right: 20px; + >.queue-container { + display: flex; + width: 100%; + height: 90%; + flex-direction: column; + justify-content: center; + align-items: center; - .song-cover, - .fa-solid { - height: 10rem; - width: 10rem; - font-size: 7rem; - line-height: 100%; + .song-list-element { display: flex; justify-content: center; align-items: center; - } - - >.play-overlay { - background-color: var(--overlay-color); - position: absolute; - top: 0; - left: 0; width: 100%; - height: 100%; - cursor: pointer; - &.hover { - display: none; + >.song-cover-wrapper { + height: 7rem; + width: 7rem; + position: relative; + margin-right: 20px; + + .song-cover, + .fa-solid { + height: 100%; + width: 100%; + font-size: 6rem; + line-height: 100%; + display: flex; + justify-content: center; + align-items: center; + } + + >.play-overlay { + background-color: var(--overlay-color); + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + cursor: pointer; + + &.hover { + display: none; + } + } + + &:hover { + >.play-overlay.hover { + display: block; + } + } } - } - &:hover { - >.play-overlay.hover { - display: block; + >.song-details { + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: flex-start; + + >* { + margin: 5px; + } + + >h3 { + font-size: 1.2rem; + } + } + + >.song-actions { + display: flex; + flex-direction: row; + justify-content: flex-end; + align-items: center; + margin-left: auto; + margin-right: 10px; + + >* { + margin: 5px; + } + + >.fa-solid { + font-size: 1.25rem; + cursor: pointer; + } } } } - - >.song-details { - display: flex; - flex-direction: column; - justify-content: flex-start; - align-items: flex-start; - - >* { - margin: 5px; - } - - >h3 { - font-size: 1.8rem; - } - } - - >.song-actions { - display: flex; - flex-direction: row; - justify-content: flex-end; - align-items: center; - margin-left: auto; - margin-right: 10px; - - >* { - margin: 5px; - } - - >.fa-solid { - font-size: 1.25rem; - cursor: pointer; - } - } -} - -.playing-symbols { - position: absolute; - left: 0; - right: 0; - display: flex; - justify-content: center; - align-items: center; - flex-direction: row; - margin: 0; - padding-left: 20%; - padding-right: 20%; - width: 60%; - height: 100%; - background-color: rgba(0, 0, 0, 0.6); - - .playing-bar { - height: 40%; - background-color: white; - width: 12%; - border-radius: 50px; - margin: auto; - } - - #bar-1 { - animation: music-playing 0.9s infinite ease-in-out; - } - - #bar-2 { - animation: music-playing 0.9s infinite ease-in-out; - animation-delay: 0.3s; - } - - #bar-3 { - animation: music-playing 0.9s infinite ease-in-out; - animation-delay: 0.6s; - } - - @keyframes music-playing { - 0% { - transform: scaleY(1); - } - - 50% { - transform: scaleY(0.5); - } - - 100% { - transform: scaleY(1); - } - } } diff --git a/web/src/scss/components/sortablelist.scss b/web/src/scss/components/sortablelist.scss index 2af4a32..9f82471 100644 --- a/web/src/scss/components/sortablelist.scss +++ b/web/src/scss/components/sortablelist.scss @@ -2,6 +2,7 @@ overflow-y: scroll; overflow-x: hidden; height: 100%; + width: 100%; position: relative; >.grip-target { diff --git a/web/src/stores/authstore.ts b/web/src/stores/authstore.ts index 14c8803..b182c9d 100644 --- a/web/src/stores/authstore.ts +++ b/web/src/stores/authstore.ts @@ -6,7 +6,7 @@ import { } from 'vue'; export const useAuthStore = defineStore( 'authstore', () => { - const isAuth = ref( false ); + const isAuth = ref( true ); return { isAuth diff --git a/web/src/ts/player/index.ts b/web/src/ts/player/index.ts index aa6d9ec..06bfd1e 100644 --- a/web/src/ts/player/index.ts +++ b/web/src/ts/player/index.ts @@ -23,6 +23,9 @@ import { import type { RepeatMode } from '../dtype/player'; +import type { + Song +} from '../dtype/playlist'; import { playIndex } from './playlists'; @@ -118,12 +121,29 @@ const getSources = (): string[] => { return Object.keys( sources ); }; +const addToSongList = ( songs: Song[] ) => { + addSongList( songs ); + + if ( currentSource.value === '' ) { + playIndex( 0 ); + } +}; + /** * Add songs to the playlist from given source * @param source - The ID of the source to add from */ -const addSongFromSource = async ( source: string ) => { - sources[source]!.addSongsFromThisSource( addSongList ); +const addSongFromSource = async ( source: string ): Promise => { + if ( sources[source]!.authorized.value ) { + sources[source]!.addSongsFromThisSource( addToSongList ); + } else { + // TODO: Make this interact with user + sources[source]!.login!(); + + return false; + } + + return true; }; export default { diff --git a/web/src/ts/player/playlists/add.ts b/web/src/ts/player/playlists/add.ts index f7e70a1..3f3e1e3 100644 --- a/web/src/ts/player/playlists/add.ts +++ b/web/src/ts/player/playlists/add.ts @@ -1,9 +1,16 @@ import { + currentSource, + isPlaying, queue, queueIdx, rawQueue, - shuffle + shuffle, + sources } from '../state'; +import { + duration, + playbackPercentage +} from '../status-tracking'; import type { Song } from '@/ts/dtype/playlist'; @@ -23,6 +30,10 @@ export const addSongList = ( songs: Song[], first: boolean = false ) => { export const clearQueue = () => { queue.value = []; rawQueue.value = []; + sources[currentSource.value]?.stop(); + duration.value = -1; + playbackPercentage.value = 1; + isPlaying.value = false; }; /** diff --git a/web/src/ts/player/playlists/index.ts b/web/src/ts/player/playlists/index.ts index 4d24860..ee22a5e 100644 --- a/web/src/ts/player/playlists/index.ts +++ b/web/src/ts/player/playlists/index.ts @@ -1,5 +1,6 @@ import { currentSource, + isPlaying, queue, queueIdx, repeat, @@ -21,9 +22,15 @@ export const playIndex = async ( idx: number ) => { idx = repeat.value === 'all' ? queue.value.length - 1 : -1; } - if ( idx < 0 ) return false; - sources[currentSource.value]?.stop(); + + if ( idx < 0 ) { + isPlaying.value = false; + currentSource.value = ''; + + return false; + } + stopTracking(); const nextSong = queue.value[ idx ]!; @@ -33,4 +40,6 @@ export const playIndex = async ( idx: number ) => { sources[currentSource.value]?.playSong( nextSong.identifier ); startTracking(); + + isPlaying.value = true; }; diff --git a/web/src/ts/player/plugins/interface.d.ts b/web/src/ts/player/plugins/interface.d.ts index a49ca3f..b876ec3 100644 --- a/web/src/ts/player/plugins/interface.d.ts +++ b/web/src/ts/player/plugins/interface.d.ts @@ -1,3 +1,6 @@ +import type { + Ref +} from 'vue'; import type { Song } from '@/ts/dtype/playlist'; @@ -91,9 +94,9 @@ export interface PlayerSourcePlugin { 'getDuration': () => number; /** - * Check if source is available for playback + * Check if source is authorized */ - 'available': () => boolean; + 'authorized': Ref; /** * Called when user adds another song to the playlist via this source. diff --git a/web/src/ts/player/plugins/local/index.ts b/web/src/ts/player/plugins/local/index.ts index 637061f..519bb74 100644 --- a/web/src/ts/player/plugins/local/index.ts +++ b/web/src/ts/player/plugins/local/index.ts @@ -2,6 +2,9 @@ import type { PlayerSourcePlugin, PlayerSourcePluginInitializer } from '../interface'; +import { + ref +} from 'vue'; export const useLocalPlayer: PlayerSourcePluginInitializer = async (): Promise => { const player = document.createElement( 'audio' ); @@ -12,7 +15,7 @@ export const useLocalPlayer: PlayerSourcePluginInitializer = async (): Promise

true, + 'authorized': ref( true ), 'id': 'local', 'name': 'Local Disk', 'play': player.play, diff --git a/web/src/ts/player/plugins/musickit/index.ts b/web/src/ts/player/plugins/musickit/index.ts index 650ae8a..ff4dc36 100644 --- a/web/src/ts/player/plugins/musickit/index.ts +++ b/web/src/ts/player/plugins/musickit/index.ts @@ -43,7 +43,7 @@ export const useMusicKit: PlayerSourcePluginInitializer = ( storefront: string = }; return { - 'available': controls.getLoggedIn, + 'authorized': controls.loggedIn, 'play': controls.play, 'pause': controls.pause, 'stop': controls.stop, diff --git a/web/src/ts/player/plugins/musickit/playback.ts b/web/src/ts/player/plugins/musickit/playback.ts index 3e16961..c727d41 100644 --- a/web/src/ts/player/plugins/musickit/playback.ts +++ b/web/src/ts/player/plugins/musickit/playback.ts @@ -1,6 +1,9 @@ import type { MusicKitInstance } from 'musickitjs-v3-types/MusicKitInstance'; +import { + ref +} from 'vue'; export const musicKitPlayback = ( musickitInstance: MusicKitInstance ) => { const play = () => { @@ -27,16 +30,18 @@ export const musicKitPlayback = ( musickitInstance: MusicKitInstance ) => { }; const getPlaybackPos = (): number => { - return musickitInstance.currentPlaybackProgress; + return musickitInstance.currentPlaybackTime / musickitInstance.currentPlaybackDuration; }; const getDuration = (): number => { return musickitInstance.currentPlaybackDuration; }; - const getLoggedIn = (): boolean => { - return musickitInstance.isAuthorized; - }; + const loggedIn = ref( musickitInstance.isAuthorized ); + + musickitInstance.addEventListener( 'authorizationStatusDidChange', () => { + loggedIn.value = musickitInstance.isAuthorized; + } ); return { play, @@ -46,6 +51,6 @@ export const musicKitPlayback = ( musickitInstance: MusicKitInstance ) => { stop, getPlaybackPos, getDuration, - getLoggedIn + loggedIn }; }; diff --git a/web/src/ts/player/plugins/musickit/search/dtype.d.ts b/web/src/ts/player/plugins/musickit/search/dtype.d.ts index d0ec258..8e2f2c5 100644 --- a/web/src/ts/player/plugins/musickit/search/dtype.d.ts +++ b/web/src/ts/player/plugins/musickit/search/dtype.d.ts @@ -18,9 +18,19 @@ export interface AppleMusicSongData { 'attributes': { 'albumName': string; 'artistName': string; - 'artwork': Artwork, + 'artwork': Artwork; 'name': string; 'genreNames': string[]; 'durationInMillis': number; } } + +export interface AppleMusicPlaylistData { + 'id': string; + 'type': string; + 'href': string; + 'attributes': { + 'artwork': Artwork; + 'name': string; + } +} diff --git a/web/src/ts/player/plugins/musickit/search/playlists.ts b/web/src/ts/player/plugins/musickit/search/playlists.ts index 6fe0dc3..2b56d7f 100644 --- a/web/src/ts/player/plugins/musickit/search/playlists.ts +++ b/web/src/ts/player/plugins/musickit/search/playlists.ts @@ -1,3 +1,6 @@ +import type { + AppleMusicPlaylistData +} from './dtype'; import type { MusicKitInstance } from 'musickitjs-v3-types/MusicKitInstance'; @@ -6,10 +9,33 @@ import type { } from '@/ts/dtype/playlist'; export const searchPlaylists = async ( instance: MusicKitInstance, cb: ( songs: Song[] ) => void ) => { - // TODO: Get all playlists + let playlists: Song[] = []; + let filtered: Song[] = []; const search = async ( term: string ): Promise => { - return []; + if ( playlists.length === 0 ) { + playlists = ( ( await instance.api.music( '/v1/me/library/playlists', { + 'limit': 100 + } ) ).data as { + 'data': AppleMusicPlaylistData[] + } ).data.map( val => { + return { + 'artist': 'Playlist', + 'artwork': val.attributes.artwork ? window.MusicKit.formatArtworkURL( val.attributes.artwork, 1000, 100 ) : '', + 'duration': 0, + 'identifier': val.id, + 'additional-info': '', + 'name': val.attributes.name, + 'source': 'applemusic' + }; + } ); + } + + filtered = playlists.filter( val => { + return val.name.includes( term ); + } ); + + return filtered; }; const addSelected = async ( idx: number ) => { diff --git a/web/src/ts/player/plugins/musickit/search/songs.ts b/web/src/ts/player/plugins/musickit/search/songs.ts index 6d592ce..cb7f92b 100644 --- a/web/src/ts/player/plugins/musickit/search/songs.ts +++ b/web/src/ts/player/plugins/musickit/search/songs.ts @@ -1,5 +1,6 @@ import type { - AppleMusicApiSearchResult + AppleMusicApiSearchResult, + AppleMusicSongData } from './dtype'; import type { MusicKitInstance @@ -16,7 +17,16 @@ export const searchSongs = async ( instance: MusicKitInstance, cb: ( songs: Song 'term': term, 'types': [ 'songs' ] }; - const results = ( ( await instance.api.music( '/v1/catalog/{{storefrontId}}/search', params ) ).data as AppleMusicApiSearchResult ).results.songs.data; + + let results: AppleMusicSongData[] = []; + + try { + results = ( ( await instance.api.music( '/v1/catalog/{{storefrontId}}/search', params ) ).data as AppleMusicApiSearchResult ).results.songs.data; + } catch { + console.debug( 'Failed results: got', results ); + + return []; + } songs = []; diff --git a/web/src/ts/player/status-tracking.ts b/web/src/ts/player/status-tracking.ts index d9773d6..d9dbc18 100644 --- a/web/src/ts/player/status-tracking.ts +++ b/web/src/ts/player/status-tracking.ts @@ -24,12 +24,15 @@ export const startTracking = () => { } duration.value = sources[currentSource.value]?.getDuration() ?? -1; + setTimeout( () => { + duration.value = sources[currentSource.value]?.getDuration() ?? -1; + }, 2000 ); }; const tracker = () => { playbackPercentage.value = sources[currentSource.value]?.getPlaybackPos() ?? -1; - if ( playbackPercentage.value > duration.value ) { + if ( playbackPercentage.value > 0.995 && duration.value > 0 ) { if ( repeat.value === 'one' ) { sources[currentSource.value]?.seekTo( 0 ); } else { @@ -42,8 +45,9 @@ const tracker = () => { export const stopTracking = () => { try { clearInterval( interval ); - interval = -1; } catch { // empty } + + interval = -1; }; diff --git a/web/src/ts/util/time.ts b/web/src/ts/util/time.ts index 68f207e..7c461e4 100644 --- a/web/src/ts/util/time.ts +++ b/web/src/ts/util/time.ts @@ -10,13 +10,13 @@ export const beautifyTime = ( time: number ): string => { else if ( Math.floor( t / 60 ) > 0 ) return `${ helper( Math.floor( t / 60 ), depth + 1 ) }:${ t % 60 < 10 ? '0' : '' }${ Math.floor( t % 60 ) }`; else - return `${ t % 60 < 10 ? '0' : '' }${ Math.floor( t % 60 ) }`; + return `${ depth === 1 ? '00:' : '' }${ t % 60 < 10 ? '0' : '' }${ Math.floor( t % 60 ) }`; }; - if ( time < 0 ) { - return '-:--'; + if ( time < 0 || isNaN( time ) ) { + return '--:--'; } else if ( time == 0 ) { - return '0:00'; + return '00:00'; } return helper( Math.round( time ), 1 ); diff --git a/web/src/views/AppView.vue b/web/src/views/AppView.vue index fc3cc9c..217ecb3 100644 --- a/web/src/views/AppView.vue +++ b/web/src/views/AppView.vue @@ -1,14 +1,26 @@