Move playback and queue to state files; Move various components to layout; Begin moving view to state file;

This commit is contained in:
Neshura 2024-04-30 02:27:10 +02:00
parent 65f0e6776e
commit 99b534e9b5
Signed by: Neshura
GPG key ID: B6983AAA6B9A7A6C
12 changed files with 422 additions and 284 deletions

View file

@ -3,10 +3,43 @@
<script lang="ts"> <script lang="ts">
import {Button} from "$lib/components/ui/button"; import {Button} from "$lib/components/ui/button";
import {onMount} from "svelte"; import {onMount} from "svelte";
let { audio, pause, play } = $props(); import {playbackState} from "$lib/states/playback-state.svelte";
import {toFixedNumber} from "$lib/formatting";
let mounted = $state(false); let mounted = $state(false);
$inspect(audio); function changeVolume(change: number) {
if (playbackState.volume + change > 1) {
playbackState.volume = 1;
}
else if (playbackState.volume + change < 0) {
playbackState.volume = 0;
}
else {
playbackState.volume = toFixedNumber(playbackState.volume + change, 2, 10) ;
}
if (playbackState.song) {
playbackState.song.volume = playbackState.volume;
}
}
function progressPercent() {
return playbackState.progress / playbackState.duration * 100 || 0;
}
function displayTime(rawSeconds: number) {
const intSeconds = Math.round(rawSeconds);
const seconds = intSeconds % 60;
const minutes = Math.floor((intSeconds / 60)) % 60;
const hours = Math.floor((intSeconds / 3600));
if (hours == 0) {
return `${minutes.toString()}:${seconds.toString().padStart(2, 0)}`
}
else {
return `${hours}:${minutes.toString().padStart(2, 0)}:${seconds.toString().padStart(2, 0)}`
}
}
onMount(() => { onMount(() => {
mounted = true; mounted = true;
@ -15,14 +48,22 @@
<div class="flex flex-row gap-2"> <div class="flex flex-row gap-2">
{#if mounted} {#if mounted}
<p>Paused: {audio.paused}</p> <div class="flex flex-row gap-2">
<p>Volume: {audio.volume}</p> <p class="border p-2">Song: {playbackState.metaData.title}</p>
<p>Duration: {audio.duration}</p> <p class="border p-2">Volume: {playbackState.volume}</p>
{#if audio.paused} <p class="border p-2">{displayTime(playbackState.progress)}/{displayTime(playbackState.duration)}</p>
<Button onclick={play}>Play</Button> <p class="border p-2">{progressPercent().toFixed(2)}%</p>
<Button class="border p-2" onclick={() => changeVolume(-0.05)}>-</Button>
<Button class="border p-2" onclick={() => changeVolume(0.05)}>+</Button>
<Button class="border p-2" onclick={() => playbackState.song.currentTime -= 5}>{"<<"}</Button>
{#if playbackState.paused}
<Button onclick={() => playbackState.play()}>Play</Button>
{:else} {:else}
<Button onclick={pause}>Pause</Button> <Button onclick={() => playbackState.pause()}>Pause</Button>
{/if} {/if}
<Button class="border p-2" onclick={() => playbackState.song.currentTime += 5}>{">>"}</Button>
<Button class="border p-2" onclick={() => playbackState.mode.next()}>{playbackState.mode.get()}</Button>
</div>
{:else} {:else}
<p>Nothing going on here</p> <p>Nothing going on here</p>
{/if} {/if}

View file

@ -3,28 +3,28 @@
<script lang="ts"> <script lang="ts">
import {Separator} from "$lib/components/ui/separator"; import {Separator} from "$lib/components/ui/separator";
import {Button} from "$lib/components/ui/button"; import {Button} from "$lib/components/ui/button";
import {Views} from "$lib/components/custom/Views/views.svelte"; import {View} from "$lib/components/custom/Views/views.svelte";
import {AlbumViews} from "$lib/components/custom/Views/views.svelte.js"; import {AlbumView} from "$lib/components/custom/Views/views.svelte.js";
import {viewState} from "$lib/states/view-state.svelte";
let { view = $bindable(), subView = $bindable() } = $props();
let playlists = $state([]); let playlists = $state([]);
</script> </script>
<div class="border border-2 col-span-1 flex flex-col gap-1 p-2"> <div class="border border-2 col-span-1 flex flex-col gap-1 p-2">
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<Button onclick={() => view=Views.Albums} class="flex flex-col h-fit">Albums <Button onclick={() => viewState.setMode(View.Albums)} class="flex flex-col h-fit">Albums
<Button variant="secondary" onclick={() => subView=AlbumViews.All}>All</Button> <Button variant="secondary" onclick={() => viewState.setSubMode(AlbumView.All)}>All</Button>
<Button variant="secondary" onclick={() => subView=AlbumViews.Random}>Random</Button> <Button variant="secondary" onclick={() => viewState.setSubMode(AlbumView.Random)}>Random</Button>
<Button variant="secondary" onclick={() => subView=AlbumViews.Favourites}>Favourites</Button> <Button variant="secondary" onclick={() => viewState.setSubMode(AlbumView.Favourites)}>Favourites</Button>
<Button variant="secondary" onclick={() => subView=AlbumViews.TopRated}>Top Rated</Button> <Button variant="secondary" onclick={() => viewState.setSubMode(AlbumView.TopRated)}>Top Rated</Button>
<Button variant="secondary" onclick={() => subView=AlbumViews.RecentlyAdded}>Recently Added</Button> <Button variant="secondary" onclick={() => viewState.setSubMode(AlbumView.RecentlyAdded)}>Recently Added</Button>
<Button variant="secondary" onclick={() => subView=AlbumViews.RecentlyPlayed}>Recently Played</Button> <Button variant="secondary" onclick={() => viewState.setSubMode(AlbumView.RecentlyPlayed)}>Recently Played</Button>
<Button variant="secondary" onclick={() => subView=AlbumViews.MostPlayed}>Most Played</Button> <Button variant="secondary" onclick={() => viewState.setSubMode(AlbumView.MostPlayed)}>Most Played</Button>
</Button> </Button>
</div> </div>
<Separator /> <Separator />
<Button onclick={() => view=Views.Artists}>Artists</Button> <Button onclick={() => viewState.setMode(View.Artists)}>Artists</Button>
<h1>Songs</h1> <h1>Songs</h1>
<h1>Radios</h1> <h1>Radios</h1>
<h1>Shares</h1> <h1>Shares</h1>

View file

@ -5,28 +5,54 @@
import {Button} from "$lib/components/ui/button"; import {Button} from "$lib/components/ui/button";
import {timeFormat} from "$lib/time-format"; import {timeFormat} from "$lib/time-format";
import LoadingSpinner from "$lib/components/custom/LoadingSpinner.svelte"; 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); let loading = $state(false);
async function innerFetchQueue() { async function innerFetchQueue() {
loading = true; loading = true;
await fetchQueue() await queueState.getPlayQueue()
await new Promise(resolve => setTimeout(resolve, 100)); await new Promise(resolve => setTimeout(resolve, 100));
loading = false; 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();
}
</script> </script>
{#snippet songEntry(song, idx)}
<p onclick={() => playSong(idx)}>{song.artist} - {song.title} ({timeFormat(song.duration)})</p>
<Button onclick={() => removeSongFromQueue(idx)}>X</Button>
{/snippet}
{#snippet playerQueue()} {#snippet playerQueue()}
{#each queue as song, idx} {#each queueState.queue as song, idx}
{#if currentIndex == idx} {#if idx < queueState.currentIndex}
<p class="bg-secondary" onclick={() => playSong(song, idx)}>{song.artist} - {song.title} ({timeFormat(song.duration)})<Button onclick={() => removeSongFromQueue(idx)}>X</Button></p> <div class="flex flex-row gap-2 text-muted">
{@render songEntry(song, idx)}
</div>
{:else if idx === queueState.currentIndex}
<div class="flex flex-row gap-2 bg-secondary">
{@render songEntry(song, idx)}
</div>
{:else} {:else}
<p onclick={() => playSong(song, idx)}>{song.artist} - {song.title} ({timeFormat(song.duration)})<Button onclick={() => removeSongFromQueue(idx)}>X</Button></p> <div class="flex flex-row gap-2">
{@render songEntry(song, idx)}
</div>
{/if} {/if}
{/each} {/each}
<Button onclick={() => queueState.queue.push(queueState.queue[0])}>Add</Button>
<Button onclick={() => queue.push(queue[0])}>Add</Button>
{/snippet} {/snippet}
<div class="border border-2 col-span-1 flex flex-col gap-1 p-2"> <div class="border border-2 col-span-1 flex flex-col gap-1 p-2">
@ -34,7 +60,7 @@
<h1>Current Queue</h1> <h1>Current Queue</h1>
<div class="flex-grow"></div> <div class="flex-grow"></div>
<Button variant="outline" class="h-8 w-12" onclick={innerFetchQueue}>Fetch</Button> <Button variant="outline" class="h-8 w-12" onclick={innerFetchQueue}>Fetch</Button>
<Button variant="outline" class="h-8 w-12" onclick={saveQueue}>Upload</Button> <Button variant="outline" class="h-8 w-12" onclick={() => queueState.saveQueue()}>Upload</Button>
</div> </div>
<Separator /> <Separator />
{#if loading} {#if loading}

View file

@ -1,16 +1,14 @@
<svelte:options runes={true} /> <svelte:options runes={true} />
<script lang="ts"> <script lang="ts">
import {AlbumViews} from "$lib/components/custom/Views/views.svelte"; import {AlbumView} from "$lib/components/custom/Views/views.svelte";
import {viewState} from "$lib/states/view-state.svelte";
import {onMount} from "svelte"; import {onMount} from "svelte";
import LoadingSpinner from "$lib/components/custom/LoadingSpinner.svelte";
import ViewCard from "$lib/components/custom/Views/ViewCard.svelte"; import ViewCard from "$lib/components/custom/Views/ViewCard.svelte";
import {type AlbumID3, type GetAlbumList2Response, OpenSubsonic, type Parameter} from "$lib/opensubsonic"; import {type AlbumID3, type GetAlbumList2Response, OpenSubsonic, type Parameter} from "$lib/opensubsonic";
import {Skeleton} from "$lib/components/ui/skeleton"; import {Skeleton} from "$lib/components/ui/skeleton";
import {goto} from "$app/navigation"; import {goto} from "$app/navigation";
let { viewMode = $bindable() }: { viewMode: AlbumViews } = $props();
let previousView = $state(viewMode);
let albums: Array<AlbumID3> = $state([]); let albums: Array<AlbumID3> = $state([]);
let loading = $state(true); let loading = $state(true);
let paginating = $state(false); let paginating = $state(false);
@ -22,7 +20,7 @@
async function fetchAlbums() { async function fetchAlbums() {
let parameters: Array<Parameter> = [ let parameters: Array<Parameter> = [
{key: "type", value: viewMode}, {key: "type", value: viewState.current.subMode},
{key: "size", value: paginationIncrement}, {key: "size", value: paginationIncrement},
{key: "offset", value: pagination}, {key: "offset", value: pagination},
]; ];
@ -50,14 +48,17 @@
}) })
$effect(() => { $effect(() => {
console.log(viewMode); console.log(viewState.current.subMode);
if (viewMode !== previousView) { if (viewState.previous) {
if (viewState.current.subMode !== viewState.previous.subMode) {
loading = true; loading = true;
albums = []; albums = [];
pagination = 0; pagination = 0;
fetchAlbums(); fetchAlbums();
previousView = viewMode viewState.previous.mode = viewState.current.mode
} }
}
}) })
function selectAlbum(albumId) { function selectAlbum(albumId) {
@ -67,7 +68,7 @@
onMount(() => { onMount(() => {
fetchAlbums(); fetchAlbums();
previousView = viewMode; viewState.previous = viewState.current;
}) })
</script> </script>

View file

@ -3,9 +3,9 @@
<script lang="ts"> <script lang="ts">
import {Separator} from "$lib/components/ui/separator"; import {Separator} from "$lib/components/ui/separator";
import {Button} from "$lib/components/ui/button"; import {Button} from "$lib/components/ui/button";
import {AlbumViews} from "$lib/components/custom/Views/views.svelte"; import {AlbumView} from "$lib/components/custom/Views/views.svelte";
let { viewMode = $bindable() }: { viewMode: AlbumViews } = $props(); let { viewMode = $bindable() }: { viewMode: AlbumView } = $props();
let range = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,] let range = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,]
</script> </script>

View file

@ -1,4 +1,4 @@
export enum AlbumViews { export enum AlbumView {
All = "alphabeticalByName", All = "alphabeticalByName",
Random = "random", Random = "random",
Favourites = "starred", Favourites = "starred",
@ -8,10 +8,13 @@ export enum AlbumViews {
MostPlayed = "frequent" MostPlayed = "frequent"
} }
export enum Views { export enum View {
Albums = "Albums", Albums = "Albums",
Artists = "Artists", Artists = "Artists",
Songs = "Songs", Songs = "Songs",
Radios = "Radios", Radios = "Radios",
Shares = "Shares" Shares = "Shares",
Playlist = "Playlist",
Artist = "Artist",
Album = "Album",
} }

4
src/lib/formatting.ts Normal file
View file

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

View file

@ -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<Song>,
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<void>,
saveQueue: () => Promise<void>,
}
export const queueState: QueueState = $state({
queue: new Array<Song>(),
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<void> {
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<NowPlayingEntry> = [];
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<void> {
const songs: Array<Parameter> = [];
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();
}
}
})

View file

@ -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<Parameter> = [
{ 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<Parameter> = [
{ 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();
}
})

View file

@ -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<ViewState>({
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;
}
})

View file

@ -2,14 +2,34 @@
<script> <script>
import "../app.pcss"; import "../app.pcss";
import QueueFrame from "$lib/components/custom/QueueFrame.svelte";
import {onMount} from "svelte";
import PlayerControls from "$lib/components/custom/PlayerControls.svelte";
import {AlbumView, View} from "$lib/components/custom/Views/views.svelte";
import PlayerSidebar from "$lib/components/custom/PlayerSidebar.svelte";
let { children } = $props(); let { children } = $props();
let viewMode = $state(View.Albums);
let subViewMode = $state(AlbumView.All);
onMount(() => {
})
</script> </script>
<div class="h-full flex flex-col"> <div class="h-full flex flex-col">
<div class="border border-2 p-1"> <div class="border border-2 p-1">
<h1>Navbar</h1> <h1>Navbar</h1>
</div> </div>
<!--<QueueFrame queue={queueState} {fetchQueue} {saveQueue} {removeSongFromQueue} {playSong} currentIndex={currentSong.queueIndex} />-->
<div class="border border-2 flex-1 grid grid-cols-5 h-1 min-h-fit">
<PlayerSidebar />
{@render children()} {@render children()}
<QueueFrame />
</div>
<div class="border border-2 min-h-24 h-24">
<PlayerControls />
</div>
</div> </div>

View file

@ -1,249 +1,30 @@
<svelte:options runes={true} /> <svelte:options runes={true} />
<script lang="ts"> <script lang="ts">
import {
type Parameter, type Song, type NowPlayingResponse, type GetPlayQueueResponse,
OpenSubsonic
} from "$lib/opensubsonic";
import {onMount} from "svelte"; import {onMount} from "svelte";
import QueueFrame from "$lib/components/custom/QueueFrame.svelte"; import AlbumsView from "$lib/components/custom/Views/AlbumsView.svelte";
import {Button} from "$lib/components/ui/button"; import {View} from "$lib/components/custom/Views/views.svelte";
import {PlaybackMode, PlaybackStateSvelte} from "$lib/player.svelte.js"; import {queueState} from "$lib/states/play-queue.svelte";
import PlayerSidebar from "$lib/components/custom/PlayerSidebar.svelte"; import {playbackState} from "$lib/states/playback-state.svelte";
import AlbumView from "$lib/components/custom/Views/AlbumView.svelte"; import {viewState} from "$lib/states/view-state.svelte";
import {AlbumViews, Views} from "$lib/components/custom/Views/views.svelte";
function toFixedNumber(num, digits, base){
const pow = Math.pow(base ?? 10, digits);
return Math.round(num*pow) / pow;
}
let source: HTMLAudioElement = $state();
let mode = $state(new PlaybackStateSvelte());
let title = $state("");
let artist = $state("");
let queueIndex = $state(0);
let currentSong = $state({
data: {
title,
artist
},
source,
queueIndex
});
let isPaused = $state(true);
let volume = $state(0.2);
let progress = $state(0);
let duration = $state(0);
let viewMode = $state(Views.Albums);
let subViewMode = $state(AlbumViews.All);
let queue: Array<Song> = $state([]);
async function fetchQueue() {
const data: GetPlayQueueResponse = await OpenSubsonic.get("getPlayQueue");
if (data && data.playQueue.entry) {
queue = [];
queue = queue.concat(data.playQueue.entry);
}
}
async function fetchNowPlaying() {
const data: NowPlayingResponse = await OpenSubsonic.get("getNowPlaying");
let foundInNowPlaying = false;
if (data && data.nowPlaying.entry) {
data.nowPlaying.entry.forEach((entry) => {
if (entry.username == OpenSubsonic.username) {
newSong(entry);
foundInNowPlaying = true;
}
})
}
if (!foundInNowPlaying && queue.length != 0) {
newSong(queue[0])
currentSong.queueIndex = 0;
}
}
async function saveQueue() {
let songs: Array<Parameter> = [];
if (queue.length === 0) {
songs.push({key: "id", value: ""})
}
else {
queue.forEach((song, idx) => {
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 fetchQueue();
}
}
function removeSongFromQueue(idx: number) {
if (idx > -1) {
queue.splice(idx, 1);
}
}
function playSong(song: Song, songIndex: number) {
pause();
newSong(song);
currentSong.queueIndex = songIndex;
play();
}
function newSong(song: Song) {
let parameters: Array<Parameter> = [
{ 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
];
let url = OpenSubsonic.getApiUrl("stream", parameters);
currentSong.source = new Audio(url); // Assign new URL
currentSong.data = song;
// Reassign Event Handlers
currentSong.source.onloadedmetadata = () => {
duration = currentSong.source.duration;
};
currentSong.source.onplay = () => {
currentSong.source.volume = volume;
isPaused = currentSong.source.paused;
let time: number = Date.now();
let parameters: Array<Parameter> = [
{ key: "id", value: song.id },
{ key: "time", value: time},
{ key: "submission", value: false}
];
OpenSubsonic.get("scrobble", parameters);
}
currentSong.source.onpause = () => {
isPaused = currentSong.source.paused;
}
currentSong.source.ontimeupdate = () => {
progress = currentSong.source.currentTime;
}
currentSong.source.onended = () => {
if (mode.current == PlaybackMode.LoopOne) {
currentSong.source.play();
}
}
currentSong.source.load();
}
function play() {
currentSong.source.play().catch(() => {});
}
function pause() {
currentSong.source.pause();
}
function changeVolume(change: number) {
if (volume + change > 1) {
volume = 1;
}
else if (volume + change < 0) {
volume = 0;
}
else {
volume = toFixedNumber(volume + change, 2, 10) ;
}
if (currentSong.source) {
currentSong.source.volume = volume;
}
}
function progressPercent() {
return progress / duration * 100 || 0;
}
function displayTime(rawSeconds: number) {
const intSeconds = Math.round(rawSeconds);
const seconds = intSeconds % 60;
const minutes = Math.floor((intSeconds / 60)) % 60;
const hours = Math.floor((intSeconds / 3600));
if (hours == 0) {
return `${minutes.toString()}:${seconds.toString().padStart(2, 0)}`
}
else {
return `${hours}:${minutes.toString().padStart(2, 0)}:${seconds.toString().padStart(2, 0)}`
}
}
onMount(() => { onMount(() => {
fetchQueue().then(() => { queueState.getPlayQueue(true);
fetchNowPlaying(); playbackState.song = new Audio(); // needed so source is not undefined
});
currentSong.source = new Audio(); // needed so source is not undefined
}) })
</script> </script>
<svelte:head> <svelte:head>
<title>Lydstyrke - {currentSong.data.title}({currentSong.data.artist})</title> <title>Lydstyrke - {playbackState.metaData.title}({playbackState.metaData.artist})</title>
<meta name="robots" content="noindex nofollow" /> <meta name="robots" content="noindex nofollow" />
</svelte:head> </svelte:head>
{#snippet centerModule()} <div class="border border-2 col-span-3 overflow-hidden">
<div class="border border-2 col-span-3 overflow-hidden"> {#if viewState.current.mode === View.Albums}es
{#if viewMode === Views.Albums} <AlbumsView />
<AlbumView viewMode={subViewMode}/> {:else if viewState.current.mode === View.Artists}
{:else if viewMode === Views.Artists}
<p>Get Fucked</p> <p>Get Fucked</p>
{/if} {/if}
</div>
{/snippet}
{#snippet leftModule()}
<PlayerSidebar bind:view={viewMode} bind:subView={subViewMode} />
{/snippet}
{#snippet rightModule()}
<QueueFrame {queue} {fetchQueue} {saveQueue} {removeSongFromQueue} {playSong} currentIndex={currentSong.queueIndex} />
{/snippet}
<div class="border border-2 flex-1 grid grid-cols-5 h-1 min-h-fit">
{@render leftModule()}
{@render centerModule()}
{@render rightModule()}
</div>
<div class="border border-2 min-h-24 h-24">
<div class="flex flex-row gap-2">
<p class="border p-2">Song: {currentSong.data.title}</p>
<p class="border p-2">Volume: {volume}</p>
<p class="border p-2">{displayTime(progress)}/{displayTime(duration)}</p>
<p class="border p-2">{progressPercent().toFixed(2)}%</p>
<Button class="border p-2" onclick={() => changeVolume(-0.05)}>-</Button>
<Button class="border p-2" onclick={() => changeVolume(0.05)}>+</Button>
<Button class="border p-2" onclick={() => currentSong.source.currentTime -= 5}>{"<<"}</Button>
{#if isPaused}
<Button onclick={play}>Play</Button>
{:else}
<Button onclick={pause}>Pause</Button>
{/if}
<Button class="border p-2" onclick={() => currentSong.source.currentTime += 5}>{">>"}</Button>
<Button class="border p-2" onclick={() => mode.next()}>{mode.get()}</Button>
</div>
</div> </div>