Somewhat functional playback (not really)
This commit is contained in:
parent
59bedf73da
commit
d088472acb
11 changed files with 319 additions and 26 deletions
12
src/app.pcss
12
src/app.pcss
|
@ -50,6 +50,18 @@
|
|||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
|
1
src/lib/AudioState.svelte.ts
Normal file
1
src/lib/AudioState.svelte.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export let audioStore: HTMLAudioElement = $state(new Audio());
|
10
src/lib/components/custom/LoadingSpinner.svelte
Normal file
10
src/lib/components/custom/LoadingSpinner.svelte
Normal 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"
|
||||
/>
|
29
src/lib/components/custom/PlayerFrame.svelte
Normal file
29
src/lib/components/custom/PlayerFrame.svelte
Normal 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>
|
47
src/lib/components/custom/QueueFrame.svelte
Normal file
47
src/lib/components/custom/QueueFrame.svelte
Normal 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>
|
|
@ -6,13 +6,40 @@ export module OpenSubsonic {
|
|||
export let password = "";
|
||||
let token = "";
|
||||
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 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 === "") {
|
||||
let cookie = Cookies.get("subsonicUsername");
|
||||
const cookie = Cookies.get("subsonicUsername");
|
||||
if (typeof cookie !== "undefined") {
|
||||
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`;
|
||||
}
|
||||
|
||||
function generateToken() {
|
||||
const generateToken = () => {
|
||||
salt = "staticfornow";
|
||||
token = Md5.hashStr(password + salt);
|
||||
}
|
||||
|
||||
export function storeCredentials() {
|
||||
let options = {
|
||||
const options = {
|
||||
expires: 7,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
|
|
7
src/lib/time-format.ts
Normal file
7
src/lib/time-format.ts
Normal 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;
|
||||
}
|
|
@ -2,4 +2,9 @@
|
|||
import "../app.pcss";
|
||||
</script>
|
||||
|
||||
<slot></slot>
|
||||
<div class="h-full flex flex-col">
|
||||
<div class="border border-2 p-1">
|
||||
<h1>Navbar</h1>
|
||||
</div>
|
||||
<slot></slot>
|
||||
</div>
|
|
@ -1,2 +1,159 @@
|
|||
<h1>Welcome to SvelteKit</h1>
|
||||
<p>Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation</p>
|
||||
<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";
|
||||
|
||||
//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>
|
||||
|
||||
|
|
|
@ -11,10 +11,9 @@
|
|||
superForm,
|
||||
} from "sveltekit-superforms";
|
||||
import { zodClient } from "sveltekit-superforms/adapters";
|
||||
import {Circle2} from "svelte-loading-spinners";
|
||||
import {Md5} from "ts-md5";
|
||||
import Cookies from 'js-cookie'
|
||||
import {goto} from "$app/navigation";
|
||||
import LoadingSpinner from "$lib/components/custom/LoadingSpinner.svelte";
|
||||
|
||||
let { data }: SuperValidated<Infer<FormSchema>> = $props<{ data: SuperValidated<Infer<FormSchema>> }>();
|
||||
|
||||
|
@ -23,15 +22,19 @@
|
|||
onUpdated() {
|
||||
OpenSubsonic.username = previousForm.get("username");
|
||||
OpenSubsonic.password = previousForm.get("password");
|
||||
let path = OpenSubsonic.getApiPath("ping");
|
||||
fetch(path).then(async (data) => {
|
||||
let res = (await data.json())["subsonic-response"];
|
||||
if (res.status === "ok") {
|
||||
OpenSubsonic.storeCredentials();
|
||||
loading = false;
|
||||
let route = Cookies.get("preLoginRoute")
|
||||
Cookies.remove("preLoginRoute", { path: "/login" })
|
||||
goto(route);
|
||||
OpenSubsonic.get("ping").then((data) => {
|
||||
if (data) {
|
||||
if (data.status === "ok") {
|
||||
OpenSubsonic.storeCredentials();
|
||||
loading = false;
|
||||
let route = Cookies.get("preLoginRoute")
|
||||
Cookies.remove("preLoginRoute", { path: "/login" })
|
||||
goto(route);
|
||||
}
|
||||
else {
|
||||
error = true;
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
error = true;
|
||||
|
@ -69,12 +72,7 @@
|
|||
</Form.Field>
|
||||
<div class="flex flex-row justify-center pt-2">
|
||||
{#if loading}
|
||||
<Circle2
|
||||
size="40" unit="px"
|
||||
colorOuter="#0892B4" durationOuter="1s"
|
||||
colorCenter="white" durationCenter="3s"
|
||||
colorInner="#9cc1c9" durationInner="2s"
|
||||
/>
|
||||
<LoadingSpinner />
|
||||
{:else}
|
||||
<Form.Button>Login</Form.Button>
|
||||
{/if}
|
||||
|
|
|
@ -2186,7 +2186,7 @@ svelte-preprocess@^5.1.3:
|
|||
sorcery "^0.11.0"
|
||||
strip-indent "^3.0.0"
|
||||
|
||||
svelte@^5.0.0-next:
|
||||
svelte@^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"
|
||||
integrity sha512-RDeoTJtI7HRx1VPROJ2qeibL14OPTQGmNrjM8Lug/N4hJXtolmA7USKggcHL3RXl1loS5Zb1R0IV7stfS7GhQg==
|
||||
|
|
Loading…
Reference in a new issue