258 lines
7.9 KiB
Svelte
258 lines
7.9 KiB
Svelte
<svelte:options runes={true} />
|
|
|
|
<script lang="ts">
|
|
import {OpenSubsonic} from "$lib/opensubsonic";
|
|
import {onMount} from "svelte";
|
|
import QueueFrame from "$lib/components/custom/QueueFrame.svelte";
|
|
//import PlayerFrame from "$lib/components/custom/PlayerFrame.svelte";
|
|
import {Button} from "$lib/components/ui/button";
|
|
|
|
function toFixedNumber(num, digits, base){
|
|
const pow = Math.pow(base ?? 10, digits);
|
|
return Math.round(num*pow) / pow;
|
|
}
|
|
|
|
enum PlaybackMode {
|
|
Linear,
|
|
LoopOne,
|
|
LoopQueue,
|
|
}
|
|
|
|
class PlaybackState {
|
|
values = {
|
|
[PlaybackMode.Linear]: "1",
|
|
[PlaybackMode.LoopOne]: "0",
|
|
[PlaybackMode.LoopQueue]: "00",
|
|
}
|
|
|
|
current: PlaybackMode = $state(PlaybackMode.Linear);
|
|
|
|
next = function() {
|
|
console.log(this.current)
|
|
if (this.current == PlaybackMode.LoopQueue) {
|
|
this.current = -1;
|
|
}
|
|
return this.values[++this.current];
|
|
}
|
|
|
|
prev = function() {
|
|
if (this.current == PlaybackMode.Linear) {
|
|
this.current = this.values.length;
|
|
}
|
|
return this.values[--this.current];
|
|
}
|
|
|
|
get = function() {
|
|
return this.values[this.current];
|
|
}
|
|
}
|
|
|
|
let source: HTMLAudioElement = $state();
|
|
let mode = $state(new PlaybackState());
|
|
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 queue: Array<unknown> = $state([]);
|
|
|
|
async function fetchQueue() {
|
|
const data = await OpenSubsonic.get("getPlayQueue");
|
|
if (data) {
|
|
queue = [];
|
|
queue = queue.concat(data.playQueue.entry);
|
|
}
|
|
}
|
|
|
|
async function fetchNowPlaying() {
|
|
const data = 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 = [];
|
|
|
|
queue.forEach((song, idx) => {
|
|
if (idx === 0) {
|
|
songs.push({parameter: "current", value: song.id})
|
|
songs.push({parameter: "id", value: song.id})
|
|
// Add Progress within current song
|
|
}
|
|
else {
|
|
songs.push({parameter: "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: unknown, songIndex: number) {
|
|
//const chosenSong = queue[songIndex];
|
|
pause();
|
|
newSong(song);
|
|
currentSong.queueIndex = songIndex;
|
|
play();
|
|
}
|
|
|
|
function newSong(song: unknown) {
|
|
let parameters = [
|
|
{ parameter: "id", value: song.id },
|
|
//{ parameter: "maxBitRate", value: } // TODO
|
|
//{ parameter: "format", value: } // TODO
|
|
//{ parameter: "timeOffset", value: } // TODO? Only Video related
|
|
//{ parameter: "size", value: } // TODO? Only Video related
|
|
{ parameter: "estimateContentLength", value: "true" },
|
|
//{ parameter: "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 = [
|
|
{ parameter: "id", value: song.id },
|
|
{ parameter: "time", value: time},
|
|
{ parameter: "submission", value: false}
|
|
];
|
|
console.warn("Scrobble is disabled");
|
|
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 = rawSeconds.toFixed(0);
|
|
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(() => {
|
|
fetchQueue().then(() => {
|
|
fetchNowPlaying();
|
|
});
|
|
currentSong.source = new Audio();
|
|
})
|
|
</script>
|
|
|
|
<svelte:head>
|
|
<title>Lytter - {currentSong.data.title}({currentSong.data.artist})</title>
|
|
<meta name="robots" content="noindex nofollow" />
|
|
</svelte:head>
|
|
|
|
<div class="border border-2 flex-1 grid grid-cols-5">
|
|
<div class="border border-2 col-span-1">
|
|
<h1>Left Sidebar</h1>
|
|
</div>
|
|
<div class="border border-2 col-span-3">
|
|
<h1>Center</h1>
|
|
</div>
|
|
<QueueFrame {queue} {fetchQueue} {saveQueue} {removeSongFromQueue} {playSong} currentIndex={currentSong.queueIndex} />
|
|
</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>
|
|
|