diff --git a/src/lib/components/custom/PlayerControls.svelte b/src/lib/components/custom/PlayerControls.svelte index ef40d6b..fcaa5d9 100644 --- a/src/lib/components/custom/PlayerControls.svelte +++ b/src/lib/components/custom/PlayerControls.svelte @@ -3,10 +3,43 @@
- - - - - - - + + + + + + +
- +

Songs

Radios

Shares

diff --git a/src/lib/components/custom/QueueFrame.svelte b/src/lib/components/custom/QueueFrame.svelte index f2cd1da..d780db4 100644 --- a/src/lib/components/custom/QueueFrame.svelte +++ b/src/lib/components/custom/QueueFrame.svelte @@ -5,28 +5,54 @@ import {Button} from "$lib/components/ui/button"; import {timeFormat} from "$lib/time-format"; import LoadingSpinner from "$lib/components/custom/LoadingSpinner.svelte"; + import {queueState} from "$lib/states/play-queue.svelte"; + import {playbackState} from "$lib/states/playback-state.svelte"; - let { queue = [], fetchQueue, saveQueue, removeSongFromQueue, playSong, currentIndex } = $props(); let loading = $state(false); async function innerFetchQueue() { loading = true; - await fetchQueue() + await queueState.getPlayQueue() await new Promise(resolve => setTimeout(resolve, 100)); loading = false; } + + function removeSongFromQueue(idx: number) { + if (idx > -1) { + queueState.queue.splice(idx, 1); + } + } + + function playSong( songIndex: number) { + queueState.setSong(songIndex); + playbackState.pause(); + playbackState.newSong(queueState.getSong()); + playbackState.play(); + } +{#snippet songEntry(song, idx)} +

playSong(idx)}>{song.artist} - {song.title} ({timeFormat(song.duration)})

+ +{/snippet} + {#snippet playerQueue()} - {#each queue as song, idx} - {#if currentIndex == idx} -

playSong(song, idx)}>{song.artist} - {song.title} ({timeFormat(song.duration)})

+ {#each queueState.queue as song, idx} + {#if idx < queueState.currentIndex} +
+ {@render songEntry(song, idx)} +
+ {:else if idx === queueState.currentIndex} +
+ {@render songEntry(song, idx)} +
{:else} -

playSong(song, idx)}>{song.artist} - {song.title} ({timeFormat(song.duration)})

+
+ {@render songEntry(song, idx)} +
{/if} {/each} - - + {/snippet}
@@ -34,7 +60,7 @@

Current Queue

- +
{#if loading} diff --git a/src/lib/components/custom/Views/AlbumView.svelte b/src/lib/components/custom/Views/AlbumsView.svelte similarity index 78% rename from src/lib/components/custom/Views/AlbumView.svelte rename to src/lib/components/custom/Views/AlbumsView.svelte index 0b295ce..68261dd 100644 --- a/src/lib/components/custom/Views/AlbumView.svelte +++ b/src/lib/components/custom/Views/AlbumsView.svelte @@ -1,16 +1,14 @@ diff --git a/src/lib/components/custom/Views/PlaylistView.svelte b/src/lib/components/custom/Views/PlaylistView.svelte index e914080..ff75a51 100644 --- a/src/lib/components/custom/Views/PlaylistView.svelte +++ b/src/lib/components/custom/Views/PlaylistView.svelte @@ -3,9 +3,9 @@ diff --git a/src/lib/components/custom/Views/views.svelte.ts b/src/lib/components/custom/Views/views.svelte.ts index 2c5a765..70b58cc 100644 --- a/src/lib/components/custom/Views/views.svelte.ts +++ b/src/lib/components/custom/Views/views.svelte.ts @@ -1,4 +1,4 @@ -export enum AlbumViews { +export enum AlbumView { All = "alphabeticalByName", Random = "random", Favourites = "starred", @@ -8,10 +8,13 @@ export enum AlbumViews { MostPlayed = "frequent" } -export enum Views { +export enum View { Albums = "Albums", Artists = "Artists", Songs = "Songs", Radios = "Radios", - Shares = "Shares" + Shares = "Shares", + Playlist = "Playlist", + Artist = "Artist", + Album = "Album", } \ No newline at end of file diff --git a/src/lib/formatting.ts b/src/lib/formatting.ts new file mode 100644 index 0000000..1f8ed19 --- /dev/null +++ b/src/lib/formatting.ts @@ -0,0 +1,4 @@ +export function toFixedNumber(num: number, digits: number, base: number): number{ + const pow: number = Math.pow(base ?? 10, digits); + return Math.round(num*pow) / pow; +} \ No newline at end of file diff --git a/src/lib/states/play-queue.svelte.ts b/src/lib/states/play-queue.svelte.ts new file mode 100644 index 0000000..acaef3b --- /dev/null +++ b/src/lib/states/play-queue.svelte.ts @@ -0,0 +1,134 @@ +import { + type GetPlayQueueResponse, type NowPlayingEntry, + type NowPlayingResponse, + OpenSubsonic, + type Parameter, + type Song +} from "$lib/opensubsonic"; +import {playbackState} from "$lib/states/playback-state.svelte"; + +interface QueueState { + queue: Array, + currentIndex: number, + nextSong: () => Song, + setSong: (index: number) => void, + getSong: () => Song, + firstSong: () => Song, + findSong: (song: Song) => {found: boolean, index: number}, + addSong: (song: Song) => void, + getPlayQueue: (addNowPlaying: boolean) => Promise, + saveQueue: () => Promise, +} + +export const queueState: QueueState = $state({ + queue: new Array(), + currentIndex: 0, + nextSong(): Song { + this.currentIndex += 1; + return this.queue[this.currentIndex]; + }, + setSong(index: number): void { + this.currentIndex = index; + }, + getSong(): Song { + return this.queue[this.currentIndex]; + }, + firstSong(): Song { + this.currentIndex = 0; + return this.queue[this.currentIndex]; + }, + findSong(searchSong: Song): {found: boolean, index: number} { + const data = { + found: false, + index: 0, + } + for (const [index, song] of this.queue.entries()) { + if (song.id === searchSong.id) { + data.found = true; + data.index = index; + break; + } + } + return data; + }, + addSong(newSong: Song): void { + this.queue.push(newSong); + this.currentIndex = this.queue.length - 1; + }, + async getPlayQueue(addNowPlaying: boolean = false): Promise { + const queueData: GetPlayQueueResponse = await OpenSubsonic.get("getPlayQueue") + if (queueData && queueData.playQueue.entry) { + this.queue = queueData.playQueue.entry; + } + const nowPlayingData: NowPlayingResponse = await OpenSubsonic.get("getNowPlaying"); + + if (nowPlayingData && nowPlayingData.nowPlaying.entry) { + const userEntries: Array = []; + nowPlayingData.nowPlaying.entry.forEach((entry) => { + if (entry.username === OpenSubsonic.username) { + userEntries.push(entry); + } + }); + console.log(userEntries); + let localClientEntry = undefined; + userEntries.forEach((entry) => { + if (entry.playerName === OpenSubsonic.clientName) { + localClientEntry = entry; + } + }) + + if (typeof localClientEntry !== "undefined") { + if (this.findSong(localClientEntry).found) { + this.setSong(this.findSong(localClientEntry).index); + playbackState.newSong(this.getSong()); + } + else if (addNowPlaying) { + this.addSong(localClientEntry); + playbackState.newSong(this.getSong()); + } + else if (this.queue.length != 0) { + playbackState.newSong(this.firstSong()); + } + } + else { + for (const entry of userEntries) { + if (this.findSong(entry).found) { + this.setSong(this.findSong(entry).index); + playbackState.newSong(this.getSong()); + } + else if (addNowPlaying) { + this.addSong(entry); + playbackState.newSong(this.getSong()); + } + else if (this.queue.length != 0) { + playbackState.newSong(this.firstSong()); + } + } + } + + + } + }, + async saveQueue(): Promise { + const songs: Array = []; + + console.log(this.queue); + + this.queue.forEach((song: Song, idx: number): void => { + if (idx === 0) { + songs.push({key: "current", value: song.id}) + songs.push({key: "id", value: song.id}) + // Add Progress within current song + } + else { + songs.push({key: "id", value: song.id}) + } + }) + + const data = await OpenSubsonic.get("savePlayQueue", songs); + if (data) { + await this.getPlayQueue(); + } + } +}) + diff --git a/src/lib/states/playback-state.svelte.ts b/src/lib/states/playback-state.svelte.ts new file mode 100644 index 0000000..9c87d1b --- /dev/null +++ b/src/lib/states/playback-state.svelte.ts @@ -0,0 +1,98 @@ +import {PlaybackMode, PlaybackStateSvelte} from "$lib/player.svelte"; +import {OpenSubsonic, type Parameter, type Song} from "$lib/opensubsonic"; +import {queueState} from "$lib/states/play-queue.svelte"; + +interface PlaybackState { + mode: PlaybackStateSvelte, + song: HTMLAudioElement, + metaData: Song, + duration: number, + volume: number, + paused: boolean, + progress: number, + newSong: (song: Song) => void, + play: () => void, + pause: () => void, +} + +export const playbackState: PlaybackState = $state({ + mode: new PlaybackStateSvelte(), + song: {}, + metaData: {}, + duration: 0, + volume: 0.05, + paused: true, + progress: 0, + newSong(song: Song): void { + const parameters: Array = [ + { key: "id", value: song.id }, + //{ key: "maxBitRate", value: } // TODO + //{ key: "format", value: } // TODO + //{ key: "timeOffset", value: } // TODO? Only Video related + //{ key: "size", value: } // TODO? Only Video related + { key: "estimateContentLength", value: "true" }, + //{ key: "converted", value: } // TODO? Only Video related + ]; + const url = OpenSubsonic.getApiUrl("stream", parameters); + this.song = new Audio(url); // Assign new URL + this.metaData = song; + + // Reassign Event Handlers + this.song.onloadedmetadata = () => { + this.duration = this.song.duration; + }; + + this.song.onplay = () => { + this.song.volume = this.volume; + this.paused = this.song.paused; + const time: number = Date.now(); + const parameters: Array = [ + { key: "id", value: song.id }, + { key: "time", value: time.toString()}, + { key: "submission", value: `${false}`} + ]; + OpenSubsonic.get("scrobble", parameters); + } + + this.song.onpause = () => { + this.paused = this.song.paused; + } + + this.song.ontimeupdate = () => { + this.progress = this.song.currentTime; + } + + this.song.onended = () => { + switch (this.mode.current) { + case PlaybackMode.Linear: { + this.newSong(queueState.nextSong()); + this.play(); + break; + } + case PlaybackMode.LoopOne: { + this.play(); + break; + } + case PlaybackMode.LoopQueue: { + if (queueState.currentIndex === queueState.queue.length -1) { + this.newSong(queueState.firstSong()); + } + else { + this.newSong(queueState.nextSong()); + } + this.play(); + break; + } + } + } + + this.song.load(); + }, + play() { + this.song.play().catch((): void => {}); + }, + pause() { + console.log(this.song); + this.song.pause(); + } +}) \ No newline at end of file diff --git a/src/lib/states/view-state.svelte.ts b/src/lib/states/view-state.svelte.ts new file mode 100644 index 0000000..3077046 --- /dev/null +++ b/src/lib/states/view-state.svelte.ts @@ -0,0 +1,30 @@ +import {AlbumView, View} from "$lib/components/custom/Views/views.svelte"; + +interface ViewState { + current: ViewDetails, + previous?: ViewState, + parent?: ViewState, + setMode: (newMode: View) => void, + setSubMode: (newSubMode: AlbumView) => void, +} + +interface ViewDetails { + // sort + mode: View, + subMode: AlbumView, +} + +export const viewState: ViewState = $state({ + current: { + mode: View.Albums, + subMode: AlbumView.All, + }, + setSubMode(newSubMode: AlbumView): void { + this.previous = this.current; + this.current.subMode = newSubMode; + }, + setMode(newMode: View): void { + this.previous = this.current; + this.current.mode = newMode; + } +}) \ No newline at end of file diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index b50cebc..94f8872 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -2,14 +2,34 @@

Navbar

+ +
+ + {@render children()} + +
- {@render children()} +
+ +
\ No newline at end of file diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 47c8893..4579c7a 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,249 +1,30 @@ - Lydstyrke - {currentSong.data.title}({currentSong.data.artist}) + Lydstyrke - {playbackState.metaData.title}({playbackState.metaData.artist}) -{#snippet centerModule()} -
- {#if viewMode === Views.Albums} - - {:else if viewMode === Views.Artists} -

Get Fucked

- {/if} -
-{/snippet} - -{#snippet leftModule()} - -{/snippet} - -{#snippet rightModule()} - -{/snippet} - -
- {@render leftModule()} - {@render centerModule()} - {@render rightModule()} - -
-
-
-

Song: {currentSong.data.title}

-

Volume: {volume}

-

{displayTime(progress)}/{displayTime(duration)}

-

{progressPercent().toFixed(2)}%

- - - - {#if isPaused} - - {:else} - - {/if} - - -
+
+ {#if viewState.current.mode === View.Albums}es + + {:else if viewState.current.mode === View.Artists} +

Get Fucked

+ {/if}
+