From 44e0d9f44ca3ba48c624292c0d770c3db2c47dd3 Mon Sep 17 00:00:00 2001 From: Mat12143 Date: Sat, 2 Aug 2025 03:54:26 +0200 Subject: [PATCH 1/8] feat: refactoring api logic --- frontend/src/lib/components/Error.svelte | 8 ++- .../src/lib/components/QueueSlider.svelte | 8 +-- frontend/src/lib/types.ts | 5 ++ frontend/src/lib/utils.ts | 69 +++++++++++++++++++ frontend/src/routes/admin/[id]/+page.svelte | 64 +++++++++-------- frontend/src/routes/admin/[id]/+page.ts | 11 +++ frontend/src/routes/room/[id]/+page.svelte | 59 ++++++---------- 7 files changed, 150 insertions(+), 74 deletions(-) create mode 100644 frontend/src/lib/utils.ts create mode 100644 frontend/src/routes/admin/[id]/+page.ts diff --git a/frontend/src/lib/components/Error.svelte b/frontend/src/lib/components/Error.svelte index 47e6a19..414657e 100644 --- a/frontend/src/lib/components/Error.svelte +++ b/frontend/src/lib/components/Error.svelte @@ -1,8 +1,10 @@
-

Error {code}

-

{message}

+

Error {returnError.code}

+

{returnError.message}

diff --git a/frontend/src/lib/components/QueueSlider.svelte b/frontend/src/lib/components/QueueSlider.svelte index 430a9bc..ec5a647 100644 --- a/frontend/src/lib/components/QueueSlider.svelte +++ b/frontend/src/lib/components/QueueSlider.svelte @@ -1,12 +1,12 @@ diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index 3f10d61..7de4960 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -35,3 +35,8 @@ export const parseSuggestion = async function (sugg: any): Promise { let resp = await SuggestionSchema.parseAsync(sugg) return resp } + +export type FetchError = { + code: number + message: string +} diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts new file mode 100644 index 0000000..1babeb9 --- /dev/null +++ b/frontend/src/lib/utils.ts @@ -0,0 +1,69 @@ +import { parseSong, parseSuggestion, type FetchError, type Song, type Suggestion } from "./types" + +export const joinRoom = async function (roomId: string): Promise<[FetchError | null, string]> { + let resp = await fetch("/api/join?room=" + roomId) + + if (resp.status != 200) { + return [{ code: 400, message: "Cannot join the room" }, ""] + } + + return [null, "test"] +} + +export const getSuggestions = async function (roomId: string): Promise<[FetchError | null, Suggestion[]]> { + let resp = await fetch("/api/room/suggestions?room=" + roomId) + + if (resp.status != 200) { + return [{ code: 400, message: "Failed to retrieve suggestions" }, []] + } + + let json = await resp.json() + let suggestions: Suggestion[] = [] + + json["songs"].forEach(async (i: any) => { + suggestions.push(await parseSuggestion(i)) + }) + + suggestions = suggestions.sort((a, b) => { + return a.upvote - b.upvote + }) + + return [null, suggestions] +} + +export const getQueueSongs = async function (roomId: string): Promise<[FetchError | null, Song[], number]> { + let resp = await fetch("/api/queue?room=" + roomId) + if (resp.status != 200) { + return [{ code: 400, message: "Failed to load queue songs" }, [], 0] + } + + let json = await resp.json() + let songs: Song[] = [] + + json["queue"].forEach(async (i: any) => { + songs.push(await parseSong(i)) + }) + + let playingId = json["index"] + + return [null, songs, playingId] +} + +export const triggerPlayNext = async function (roomId: string): Promise<[FetchError | null, Song[], number]> { + let resp = await fetch("/api/queue/next?room=" + roomId, { method: "POST" }) + + if (resp.status != 200) { + return [{ code: 400, message: "Failed to trigger next song playback" }, [], 0] + } + + let json = await resp.json() + + let songs: Song[] = [] + + if (json["ended"]) { + json["queue"].forEach(async (i: any) => { + songs.push(await parseSong(i)) + }) + } + return [null, songs, json["index"]] +} diff --git a/frontend/src/routes/admin/[id]/+page.svelte b/frontend/src/routes/admin/[id]/+page.svelte index 7573aa2..55d93a5 100644 --- a/frontend/src/routes/admin/[id]/+page.svelte +++ b/frontend/src/routes/admin/[id]/+page.svelte @@ -1,37 +1,43 @@ +{returnError} +
- + +
diff --git a/frontend/src/routes/admin/[id]/+page.ts b/frontend/src/routes/admin/[id]/+page.ts new file mode 100644 index 0000000..216a08b --- /dev/null +++ b/frontend/src/routes/admin/[id]/+page.ts @@ -0,0 +1,11 @@ +import { error } from "@sveltejs/kit" +import type { PageLoad } from "../../room/[id]/$types" + +export const load: PageLoad = function ({ params }) { + if (params.id) { + return { + roomId: params.id, + } + } + error(400, "Please provide a room id") +} diff --git a/frontend/src/routes/room/[id]/+page.svelte b/frontend/src/routes/room/[id]/+page.svelte index f3905e6..b238e40 100644 --- a/frontend/src/routes/room/[id]/+page.svelte +++ b/frontend/src/routes/room/[id]/+page.svelte @@ -2,71 +2,54 @@ import QueueSlider from "$lib/components/QueueSlider.svelte" import SuggestionInput from "$lib/components/SuggestionInput.svelte" import Error from "$lib/components/Error.svelte" - import { parseSong, parseSuggestion, type Suggestion, type Song } from "$lib/types.js" + import { type Suggestion, type Song } from "$lib/types.js" import { onMount } from "svelte" import SuggestionList from "$lib/components/SuggestionList.svelte" + import { getQueueSongs, getSuggestions, joinRoom } from "$lib/utils.js" + import type { FetchError } from "$lib/types.js" let { data } = $props() - let songs = $state([]) - let playing = $state(0) + let queueSongs = $state([]) + let playingIndex = $state(0) let suggestions = $state([]) - let error = $state({ code: 0, message: "" }) + let returnError = $state() + + let wsUrl = "" onMount(async () => { // Join the room - let resp = await fetch("/api/join?room=" + data.roomId) - if (resp.status != 200) { - error = { code: 400, message: "Failed to join the room. Maybe wrong code or location?" } + let sugg, queue, index + ;[returnError, wsUrl] = await joinRoom(data.roomId) + if (returnError) { return } - // Setup websocket connection - - // Get room suggestions - - resp = await fetch("/api/room/suggestions?room=" + data.roomId) - - if (resp.status != 200) { - error = { code: 500, message: "Failed to retrive suggestions" } + ;[returnError, sugg] = await getSuggestions(data.roomId) + if (returnError) { return } - let json = await resp.json() - - json["songs"].forEach(async (i: any) => { - suggestions.push(await parseSuggestion(i)) - }) - - suggestions = suggestions.sort((a, b) => { - return a.upvote - b.upvote - }) - - // Get the room queue - - resp = await fetch("/api/queue?room=" + data.roomId) - if (resp.status != 200) { - error = { code: 404, message: "Room not found" } + ;[returnError, queue, index] = await getQueueSongs(data.roomId) + if (returnError) { return } - json = await resp.json() - json["queue"].forEach(async (i: any) => { - songs.push(await parseSong(i)) - }) - console.log(songs) + queueSongs = queue + suggestions = sugg + playingIndex = index }) -{#if error.code != 0} - +{#if returnError} + {:else}
- +
From 64941061ea9508609978da83d69af5263c2667b1 Mon Sep 17 00:00:00 2001 From: Leonardo Segala Date: Sat, 2 Aug 2025 04:09:01 +0200 Subject: [PATCH 2/8] Add endpoit to get audio url --- backend/src/app.py | 23 ++++++++++++++++++----- backend/src/song_fetch.py | 23 +++++++++++++++++++---- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/backend/src/app.py b/backend/src/app.py index 2066572..a484a46 100644 --- a/backend/src/app.py +++ b/backend/src/app.py @@ -8,8 +8,8 @@ from dataclasses import asdict from .state import State from .connect import get_connection from .room import Room -from .song import Song, init_db, get_song_by_title_artist, add_song_in_db -from .song_fetch import lastfm_query_search, download_song_mp3 +from .song import Song, init_db, get_song_by_title_artist, add_song_in_db, get_song_by_uuid +from .song_fetch import lastfm_query_search, yt_get_audio_url, yt_search_song from .qrcode_gen import generate_qr from typing import Any @@ -171,8 +171,7 @@ def add_song(): if (song := get_song_by_title_artist(info.title, info.artist)) is None: ## song not found, downolad from YT - if (res := download_song_mp3(info.title, info.artist)) is None: - return error("Cannot get info from YT") + yt_video_id = yt_search_song(info.title, info.artist) ## add in DB song = Song( @@ -181,7 +180,7 @@ def add_song(): artist=info.artist, tags=info.tags, image_id=info.img_id, - youtube_id=res[0], + youtube_id=yt_video_id, ) add_song_in_db(song) @@ -241,5 +240,19 @@ def room_qrcode(): return Response(stream, content_type="image/jpeg") +@app.get("/api/song/audio") +def get_audio_url(): + if (song_uuid := request.args.get("song")) is None: + return error("Missing song id") + + if (song := get_song_by_uuid(song_uuid)) is None: + return error("Song not found") + + if (url := yt_get_audio_url(song.youtube_id)) is None: + return error("Cannot get audio url") + + return {"success": True, "url": url} + + if __name__ == "__main__": socketio.run(app, debug=True) diff --git a/backend/src/song_fetch.py b/backend/src/song_fetch.py index 561e18d..ec15485 100644 --- a/backend/src/song_fetch.py +++ b/backend/src/song_fetch.py @@ -59,7 +59,7 @@ def lastfm_query_search(query: str) -> SongInfo: return SongInfo(artist=artist, title=name, img_id=img_id, tags=tags) -def download_song_mp3(name: str, artist: str) -> tuple[str, str] | None: # ( id, audio ) +def yt_search_song(name: str, artist: str) -> str: # video id ydl_opts = { "format": "bestaudio", "default_search": "ytsearch1", @@ -70,12 +70,27 @@ def download_song_mp3(name: str, artist: str) -> tuple[str, str] | None: # ( id with yt_dlp.YoutubeDL(ydl_opts) as ydl: info = ydl.extract_info(f"{name!r} - {artist!r}", download=False) - first_entry = info["entries"][0] + return info["entries"][0]["id"] - video_id = first_entry["id"] + +def yt_get_audio_url(video_id) -> str | None: # audio url + ydl_opts = { + "format": "bestaudio", + "default_search": "ytsearch1", + "outtmpl": "%(title)s.%(ext)s", + "skip_download": True, + } + + with yt_dlp.YoutubeDL(ydl_opts) as ydl: + info = ydl.extract_info(video_id, download=False) + + if "entries" not in info: + return info["url"] + + first_entry = info["entries"][0] for fmt in first_entry["formats"]: if "acodec" in fmt and fmt["acodec"] != "none": - return video_id, fmt["url"] + return fmt["url"] return None From 6f1e590e50d767b8fade0f670d35ef1367c8b58e Mon Sep 17 00:00:00 2001 From: Leonardo Segala Date: Sat, 2 Aug 2025 04:11:11 +0200 Subject: [PATCH 3/8] Remove dummy song --- backend/src/app.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/backend/src/app.py b/backend/src/app.py index 02e6594..d2682a7 100644 --- a/backend/src/app.py +++ b/backend/src/app.py @@ -26,18 +26,18 @@ db_conn = get_connection() state = State(app, socketio, db_conn.cursor()) init_db(state.db) -state.rooms[1000] = Room( - id=1000, - coord=(1.0, 5.5), - name="Test Room", - pin=None, - tags=set(), - range_size=100, - songs={"b": (Song(uuid="b", title="title", artist="art", tags=["a", "B"], image_id="img", youtube_id="yt"), 1)}, - history=[], - playing=[Song(uuid="", title="", artist="<artist>", tags=[], image_id="<img>", youtube_id="<yt>")], - playing_idx=0, -) +# state.rooms[1000] = Room( +# id=1000, +# coord=(1.0, 5.5), +# name="Test Room", +# pin=None, +# tags=set(), +# range_size=100, +# songs={"b": (Song(uuid="b", title="title", artist="art", tags=["a", "B"], image_id="img", youtube_id="yt"), 1)}, +# history=[], +# playing=[Song(uuid="<uuid>", title="<title>", artist="<artist>", tags=[], image_id="<img>", youtube_id="<yt>")], +# playing_idx=0, +# ) def error(msg: str, status: int = 400) -> Response: From e01d52f7f4dc76d4d498f97090cfaa71bcfc929e Mon Sep 17 00:00:00 2001 From: Leonardo Segala <leonardosegala2006@gmail.com> Date: Sat, 2 Aug 2025 04:11:54 +0200 Subject: [PATCH 4/8] Remove dummy song --- backend/src/app.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/backend/src/app.py b/backend/src/app.py index d2682a7..1d74893 100644 --- a/backend/src/app.py +++ b/backend/src/app.py @@ -26,18 +26,18 @@ db_conn = get_connection() state = State(app, socketio, db_conn.cursor()) init_db(state.db) -# state.rooms[1000] = Room( -# id=1000, -# coord=(1.0, 5.5), -# name="Test Room", -# pin=None, -# tags=set(), -# range_size=100, -# songs={"b": (Song(uuid="b", title="title", artist="art", tags=["a", "B"], image_id="img", youtube_id="yt"), 1)}, -# history=[], -# playing=[Song(uuid="<uuid>", title="<title>", artist="<artist>", tags=[], image_id="<img>", youtube_id="<yt>")], -# playing_idx=0, -# ) +state.rooms[1000] = Room( + id=1000, + coord=(1.0, 5.5), + name="Test Room", + pin=None, + tags=set(), + range_size=100, + songs={}, + history=[], + playing=[], + playing_idx=0, +) def error(msg: str, status: int = 400) -> Response: From 72ceb0f8dc3f45cdd9774bee685c20a6eac5d83f Mon Sep 17 00:00:00 2001 From: Leonardo Segala <leonardosegala2006@gmail.com> Date: Sat, 2 Aug 2025 04:17:20 +0200 Subject: [PATCH 5/8] Fix testini's smarcio --- backend/src/app.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/src/app.py b/backend/src/app.py index 1d74893..709118e 100644 --- a/backend/src/app.py +++ b/backend/src/app.py @@ -111,8 +111,8 @@ def queue_next(): if room.playing_idx >= len(room.playing): ## queue ended - # room.renew_queue() - data = {"success": True, "ended": True, "index": room.playing_idx, "queue": room.playing} + room.renew_queue() + data = {"success": True, "ended": True, "index": room.playing_idx, "queue": [asdict(s) for s in room.playing]} state.socketio.emit("new_queue", data, to=str(room.id)) return data @@ -202,7 +202,7 @@ def add_song(): ## add the song in the room if does not exists if song.uuid not in room.songs: room.songs[song.uuid] = (song, 1) # start with one vote - socketio.emit("new_song", {"song": song, "user_score": 1}, to=str(room.id)) + socketio.emit("new_song", {"song": asdict(song), "user_score": 1}, to=str(room.id)) return {"success": True, "song": song} @@ -234,7 +234,7 @@ def post_song_vote(): ## update the song room.songs[song_id] = (song_info[0], song_info[1] + int(request.args["increment"])) - socketio.emit("new_vote", {"song": song_info[0], "user_score": song_info[1]}) + socketio.emit("new_vote", {"song": asdict(song_info[0]), "user_score": song_info[1]}) return {"success": True} From a6a7eeb690bfd2d72445f1ba395a81fdd60e93db Mon Sep 17 00:00:00 2001 From: Leonardo Segala <leonardosegala2006@gmail.com> Date: Sat, 2 Aug 2025 04:34:07 +0200 Subject: [PATCH 6/8] Always send queue & event --- backend/src/app.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/backend/src/app.py b/backend/src/app.py index 709118e..e43184e 100644 --- a/backend/src/app.py +++ b/backend/src/app.py @@ -110,14 +110,15 @@ def queue_next(): if room.playing_idx >= len(room.playing): ## queue ended - room.renew_queue() - data = {"success": True, "ended": True, "index": room.playing_idx, "queue": [asdict(s) for s in room.playing]} - state.socketio.emit("new_queue", data, to=str(room.id)) + ended = True + else: + ended = False - return data + data = {"success": True, "ended": ended, "index": room.playing_idx, "queue": [asdict(s) for s in room.playing]} + state.socketio.emit("queue_update", data, to=str(room.id)) - return {"success": True, "ended": False, "index": room.playing_idx} + return data @app.post("/api/room/new") From 2a4a4c3caafbc1d08161815fc8047f8954900b8e Mon Sep 17 00:00:00 2001 From: Leonardo Segala <leonardosegala2006@gmail.com> Date: Sat, 2 Aug 2025 04:34:45 +0200 Subject: [PATCH 7/8] Add minimal styling --- .../src/lib/components/SuggestionInput.svelte | 10 +++++-- .../src/lib/components/SuggestionList.svelte | 28 +++++++++++++------ frontend/tailwind.config.ts | 22 +++++++++++++++ 3 files changed, 48 insertions(+), 12 deletions(-) create mode 100644 frontend/tailwind.config.ts diff --git a/frontend/src/lib/components/SuggestionInput.svelte b/frontend/src/lib/components/SuggestionInput.svelte index de8e437..09982fd 100644 --- a/frontend/src/lib/components/SuggestionInput.svelte +++ b/frontend/src/lib/components/SuggestionInput.svelte @@ -9,7 +9,11 @@ } </script> -<div class="flex h-full w-full flex-row items-center gap-2 rounded border-2 border-black p-4"> - <input type="text" placeholder="Song & Artist" class="h-full w-3/4 rounded" bind:value={input} /> - <button class="w-1/4 rounded" onclick={sendSong}>Send</button> +<div class="bg-lime-500 flex h-full w-full flex-row items-center gap-2 rounded border-2 border-lime-600"> + <input type="text" placeholder="Song & Artist" class="font-bold outline-none text-white h-[50px] px-4 w-3/4 rounded" bind:value={input} /> + <button + class="shadow-xl hover:scale-105 w-1/4 h-[40px] cursor-pointer bg-lime-600 border-lime-700 font-semibold text-white border-2 i-lucide-check rounded active:scale-90 duration-100" + onclick={sendSong}>Add</button + > + <span class="i-lucide-chevrons-left"></span> </div> diff --git a/frontend/src/lib/components/SuggestionList.svelte b/frontend/src/lib/components/SuggestionList.svelte index 16cc08c..9c3d650 100644 --- a/frontend/src/lib/components/SuggestionList.svelte +++ b/frontend/src/lib/components/SuggestionList.svelte @@ -3,29 +3,39 @@ let { suggestions, roomId }: { suggestions: Suggestion[]; roomId: string } = $props() - async function vote(amount: number, songId: string) { + async function vote(idx: number, amount: number, songId: string) { + suggestions[idx].upvote += amount await fetch(`/api/song/voting?room=${roomId}&song=${songId}&increment=${amount}`, { method: "POST" }) } </script> <div class="flex h-full w-full flex-col items-center gap-2 overflow-y-auto"> - {#each suggestions as sug} - <div class="flex h-[80px] w-full flex-row gap-2 rounded border-2 border-black p-2"> + {#if suggestions.length == 0} + <p>No suggestions yet! Try to add a new one using the <b>Add</b> button</p> + {/if} + + {#each suggestions as sug, idx} + <div class="shadow-md hover:bg-indigo-400 duration-100 bg-indigo-500 flex h-[80px] w-full flex-row gap-2 rounded border-2 border-indigo-600 p-2"> <div class="flex w-3/4 flex-row gap-2"> <img class="w-[60px] min-w-[60px] rounded" src={`https://lastfm.freetls.fastly.net/i/u/174s/${sug.image_id}.png`} alt="Song cover" /> - <h1>{sug.title} - {sug.artist}</h1> + <div class="text-white"> + <p>{sug.title}</p> + <p>{sug.artist}</p> + </div> </div> <div class="flex w-1/4 flex-row items-center justify-center gap-2"> <button + class="grayscale" onclick={() => { - vote(1, sug.uuid) - }}>+1</button + vote(idx, 1, sug.uuid) + }}>👍</button > - <p>{sug.upvote}</p> + <p class="text-white font-semibold">{sug.upvote}</p> <button + class="hover:scale-150 duration-100" onclick={() => { - vote(-1, sug.uuid) - }}>-1</button + vote(idx, -1, sug.uuid) + }}><div class="rotate-180">👍</div></button > </div> </div> diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts new file mode 100644 index 0000000..16951d0 --- /dev/null +++ b/frontend/tailwind.config.ts @@ -0,0 +1,22 @@ +import type { Config } from "tailwindcss" +const { iconsPlugin, getIconCollections } = require("@egoist/tailwindcss-icons") + +export default { + content: ["./src/**/*.{html,js,svelte,ts}"], + + theme: { + extend: {}, + }, + + plugins: [ + iconsPlugin({ + // Select the icon collections you want to use + // You can also ignore this option to automatically discover all individual icon packages you have installed + // If you install @iconify/json, you should explicitly specify the collections you want to use, like this: + collections: getIconCollections(["lucide"]), + // If you want to use all icons from @iconify/json, you can do this: + // collections: getIconCollections("all"), + // and the more recommended way is to use `dynamicIconsPlugin`, see below. + }), + ], +} satisfies Config From 2a167ba8ad9d2d8a4ba3a11f0ec9be64befebf5b Mon Sep 17 00:00:00 2001 From: Leonardo Segala <leonardosegala2006@gmail.com> Date: Sat, 2 Aug 2025 04:47:04 +0200 Subject: [PATCH 8/8] Add upvote field --- backend/src/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/app.py b/backend/src/app.py index e43184e..247ba5f 100644 --- a/backend/src/app.py +++ b/backend/src/app.py @@ -203,7 +203,7 @@ def add_song(): ## add the song in the room if does not exists if song.uuid not in room.songs: room.songs[song.uuid] = (song, 1) # start with one vote - socketio.emit("new_song", {"song": asdict(song), "user_score": 1}, to=str(room.id)) + socketio.emit("new_song", {"song": asdict(song) | {"upvote": 1}}, to=str(room.id)) return {"success": True, "song": song} @@ -235,7 +235,7 @@ def post_song_vote(): ## update the song room.songs[song_id] = (song_info[0], song_info[1] + int(request.args["increment"])) - socketio.emit("new_vote", {"song": asdict(song_info[0]), "user_score": song_info[1]}) + socketio.emit("new_vote", {"song": asdict(song_info[0]) | {"upvote": song_info[1]}}) return {"success": True}