Somewhat functional playback (not really)

This commit is contained in:
Neshura 2024-04-21 23:00:45 +02:00
parent 59bedf73da
commit d088472acb
Signed by: Neshura
GPG key ID: B6983AAA6B9A7A6C
11 changed files with 319 additions and 26 deletions

View file

@ -50,6 +50,18 @@
} }
@layer base { @layer base {
h1 {
@apply text-2xl;
@apply font-bold;
}
h2 {
@apply text-xl;
@apply font-bold;
}
h3 {
@apply text-lg;
@apply font-bold;
}
* { * {
@apply border-border; @apply border-border;
} }

View file

@ -0,0 +1 @@
export let audioStore: HTMLAudioElement = $state(new Audio());

View file

@ -0,0 +1,10 @@
<script>
import {Circle2} from "svelte-loading-spinners";
</script>
<Circle2
size="40" unit="px"
colorOuter="#0892B4" durationOuter="1s"
colorCenter="white" durationCenter="3s"
colorInner="#9cc1c9" durationInner="2s"
/>

View file

@ -0,0 +1,29 @@
<svelte:options runes={true} />
<script lang="ts">
import {Button} from "$lib/components/ui/button";
import {onMount} from "svelte";
let { audio, pause, play } = $props();
let mounted = $state(false);
$inspect(audio);
onMount(() => {
mounted = true;
})
</script>
<div class="flex flex-row gap-2">
{#if mounted}
<p>Paused: {audio.paused}</p>
<p>Volume: {audio.volume}</p>
<p>Duration: {audio.duration}</p>
{#if audio.paused}
<Button onclick={play}>Play</Button>
{:else}
<Button onclick={pause}>Pause</Button>
{/if}
{:else}
<p>Nothing going on here</p>
{/if}
</div>

View file

@ -0,0 +1,47 @@
<svelte:options runes={true} />
<script lang="ts">
import {Separator} from "$lib/components/ui/separator";
import {Button} from "$lib/components/ui/button";
import {timeFormat} from "$lib/time-format";
import LoadingSpinner from "$lib/components/custom/LoadingSpinner.svelte";
let { queue = [], fetchQueue, saveQueue, removeSongFromQueue, playSong } = $props();
let loading = $state(false);
async function innerFetchQueue() {
loading = true;
await fetchQueue()
await new Promise(resolve => setTimeout(resolve, 100));
loading = false;
}
</script>
{#snippet playerQueue()}
{#each queue as song, idx}
<p onclick={() => playSong(idx)}>{song.artist} - {song.title} ({timeFormat(song.duration)})<Button onclick={() => removeSongFromQueue(idx)}>X</Button></p>
{/each}
<Button onclick={() => queue.push(queue[0])}>Add</Button>
{/snippet}
<div class="border border-2 col-span-1 flex flex-col gap-1 p-2">
<div class="flex gap-2">
<h1>Current Queue</h1>
<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={saveQueue}>Upload</Button>
</div>
<Separator />
{#if loading}
<div class="relative flex flex-col gap-1">
<div class="absolute w-full h-full z-40 bg-opacity-60 bg-background flex flex-col items-center justify-center gap-2">
<h2>Fetching Play Queue</h2>
<LoadingSpinner />
</div>
{@render playerQueue()}
</div>
{:else}
{@render playerQueue()}
{/if}
</div>

View file

@ -6,13 +6,40 @@ export module OpenSubsonic {
export let password = ""; export let password = "";
let token = ""; let token = "";
let salt = ""; let salt = "";
export let base = "https://navidrome.neshweb.net"; export let base = "https://music.neshweb.net";
const apiVer = "1.16.1"; // Version supported by Navidrome. Variable for easier updating const apiVer = "1.16.1"; // Version supported by Navidrome. Variable for easier updating
const clientName = "Lytter"; const clientName = "Lytter";
export function getApiPath(path: string) { export async function get(path: string, parameters: {parameter: string, value: string}[] = []) {
const apiPath = getApiUrl(path, parameters);
const res = (await fetch(apiPath));
if (res.ok) {
switch (res.headers.get("content-type")) {
case("application/json"): {
return (await res.json())["subsonic-response"];
}
case("audio/mp4"): {
return res;
}
}
}
else {
return false;
}
}
export function getApiUrl(path: string, parameters: {parameter: string, value: string}[] = []) {
let apiPath = generateBasePath(path);
parameters.forEach(({parameter, value}) => {
apiPath = apiPath + `&${parameter}=${value}`;
})
return apiPath;
}
const generateBasePath = (path: string) => {
if (username === "") { if (username === "") {
let cookie = Cookies.get("subsonicUsername"); const cookie = Cookies.get("subsonicUsername");
if (typeof cookie !== "undefined") { if (typeof cookie !== "undefined") {
username = cookie; username = cookie;
} }
@ -34,13 +61,13 @@ export module OpenSubsonic {
return `${base}/rest/${path}?u=${username}&s=${salt}&t=${token}&v=${apiVer}&c=${clientName}&f=json`; return `${base}/rest/${path}?u=${username}&s=${salt}&t=${token}&v=${apiVer}&c=${clientName}&f=json`;
} }
function generateToken() { const generateToken = () => {
salt = "staticfornow"; salt = "staticfornow";
token = Md5.hashStr(password + salt); token = Md5.hashStr(password + salt);
} }
export function storeCredentials() { export function storeCredentials() {
let options = { const options = {
expires: 7, expires: 7,
path: "/", path: "/",
sameSite: "strict", sameSite: "strict",

7
src/lib/time-format.ts Normal file
View file

@ -0,0 +1,7 @@
export function timeFormat(time: number) {
const minutes = (time / 60).toFixed(0).toString();
const seconds = time % 60;
const secondsString = (seconds < 10) ? '0' + seconds.toString() : seconds.toString()
return minutes + ":" + secondsString;
}

View file

@ -2,4 +2,9 @@
import "../app.pcss"; import "../app.pcss";
</script> </script>
<slot></slot> <div class="h-full flex flex-col">
<div class="border border-2 p-1">
<h1>Navbar</h1>
</div>
<slot></slot>
</div>

View file

@ -1,2 +1,159 @@
<h1>Welcome to SvelteKit</h1> <svelte:options runes={true} />
<p>Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation</p>
<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";
//let audioSource: HTMLAudioElement = $state(new Audio());
//let paused: boolean = $derived(audioSource.paused);
//let volume: number = $derived(audioSource.volume);
let source: HTMLAudioElement = $state();
let audio = $state({
get paused() {
if (source) {
return source.paused;
}
else {
return true;
}
},
get volume() {
if (source) {
return source.volume;
}
else {
return 0;
}
},
set volume(volume) {
source.volume = volume;
},
get duration() {
if (source) {
return source.duration;
}
else {
return 0;
}
},
get currentTime() {
if (source) {
return source.currentTime;
}
else {
return 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 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);
}
}
async function playSong(songIndex: number) {
console.log(queue[songIndex].title);
const chosenSong = queue[songIndex];
let parameters = [
{ parameter: "id", value: chosenSong.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);
await pause();
source = new Audio(url);
await play();
}
async function play() {
await source.play().catch(() => {});
await new Promise(resolve => {setTimeout(resolve, 50)});
audio.volume = 0.2;
forceUpdate();
}
async function pause() {
source.pause();
await new Promise(resolve => {setTimeout(resolve, 50)});
forceUpdate();
//audio.paused = audio.source.paused;
}
function forceUpdate() {
let tmp = audio;
audio = {};
audio = tmp;
}
onMount(() => {
fetchQueue();
source = new Audio();
})
</script>
<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>
<button onclick={() => audio.volume += 0.1}>Louder</button>
<button onclick={() => audio.volume -= 0.1}>Quieter</button>
{#if typeof audio !== "undefined"}
<p>{audio.currentTime}</p>
{/if}
</div>
<QueueFrame {queue} {fetchQueue} {saveQueue} {removeSongFromQueue} {playSong} />
</div>
<div class="border border-2 min-h-24 h-24">
{#if typeof audio !== "undefined"}
<PlayerFrame
audio={audio}
pause={pause}
play={play}
/>
{/if}
</div>

View file

@ -11,10 +11,9 @@
superForm, superForm,
} from "sveltekit-superforms"; } from "sveltekit-superforms";
import { zodClient } from "sveltekit-superforms/adapters"; import { zodClient } from "sveltekit-superforms/adapters";
import {Circle2} from "svelte-loading-spinners";
import {Md5} from "ts-md5";
import Cookies from 'js-cookie' import Cookies from 'js-cookie'
import {goto} from "$app/navigation"; import {goto} from "$app/navigation";
import LoadingSpinner from "$lib/components/custom/LoadingSpinner.svelte";
let { data }: SuperValidated<Infer<FormSchema>> = $props<{ data: SuperValidated<Infer<FormSchema>> }>(); let { data }: SuperValidated<Infer<FormSchema>> = $props<{ data: SuperValidated<Infer<FormSchema>> }>();
@ -23,10 +22,9 @@
onUpdated() { onUpdated() {
OpenSubsonic.username = previousForm.get("username"); OpenSubsonic.username = previousForm.get("username");
OpenSubsonic.password = previousForm.get("password"); OpenSubsonic.password = previousForm.get("password");
let path = OpenSubsonic.getApiPath("ping"); OpenSubsonic.get("ping").then((data) => {
fetch(path).then(async (data) => { if (data) {
let res = (await data.json())["subsonic-response"]; if (data.status === "ok") {
if (res.status === "ok") {
OpenSubsonic.storeCredentials(); OpenSubsonic.storeCredentials();
loading = false; loading = false;
let route = Cookies.get("preLoginRoute") let route = Cookies.get("preLoginRoute")
@ -37,6 +35,11 @@
error = true; error = true;
loading = false; loading = false;
} }
}
else {
error = true;
loading = false;
}
}); });
}, },
onSubmit({ formData }) { onSubmit({ formData }) {
@ -69,12 +72,7 @@
</Form.Field> </Form.Field>
<div class="flex flex-row justify-center pt-2"> <div class="flex flex-row justify-center pt-2">
{#if loading} {#if loading}
<Circle2 <LoadingSpinner />
size="40" unit="px"
colorOuter="#0892B4" durationOuter="1s"
colorCenter="white" durationCenter="3s"
colorInner="#9cc1c9" durationInner="2s"
/>
{:else} {:else}
<Form.Button>Login</Form.Button> <Form.Button>Login</Form.Button>
{/if} {/if}

View file

@ -2186,7 +2186,7 @@ svelte-preprocess@^5.1.3:
sorcery "^0.11.0" sorcery "^0.11.0"
strip-indent "^3.0.0" strip-indent "^3.0.0"
svelte@^5.0.0-next: svelte@^5.0.0-next.110:
version "5.0.0-next.110" version "5.0.0-next.110"
resolved "https://registry.yarnpkg.com/svelte/-/svelte-5.0.0-next.110.tgz#90ccb7500fc257b21f311f28bd82110585c2f58c" resolved "https://registry.yarnpkg.com/svelte/-/svelte-5.0.0-next.110.tgz#90ccb7500fc257b21f311f28bd82110585c2f58c"
integrity sha512-RDeoTJtI7HRx1VPROJ2qeibL14OPTQGmNrjM8Lug/N4hJXtolmA7USKggcHL3RXl1loS5Zb1R0IV7stfS7GhQg== integrity sha512-RDeoTJtI7HRx1VPROJ2qeibL14OPTQGmNrjM8Lug/N4hJXtolmA7USKggcHL3RXl1loS5Zb1R0IV7stfS7GhQg==