This commit is contained in:
Francesco De Carlo 2025-08-02 09:50:17 +02:00
commit 656d1e40c7
15 changed files with 137 additions and 60 deletions

View file

@ -5,13 +5,13 @@ from flask import Flask, Response, jsonify, request
from flask_cors import CORS from flask_cors import CORS
from flask_socketio import SocketIO, join_room, leave_room from flask_socketio import SocketIO, join_room, leave_room
from .state import State from state import State
from .connect import get_connection from connect import get_connection
from .room import Room from room import Room
from .song import Song, init_db, get_song_by_title_artist, add_song_in_db, get_song_by_uuid from song import Song, init_db, get_song_by_title_artist, add_song_in_db, get_song_by_uuid
from .song_fetch import query_search, yt_get_audio_url, yt_search_song from song_fetch import query_search, yt_get_audio_url, yt_search_song
from .qrcode_gen import generate_qr from qrcode_gen import generate_qr
from .gps import is_within_range, distance_between_coords, Coordinates from gps import is_within_range, distance_between_coords, Coordinates
dotenv.load_dotenv() dotenv.load_dotenv()
@ -29,7 +29,7 @@ init_db(state.db)
state.rooms[1000] = Room( state.rooms[1000] = Room(
id=1000, id=1000,
coord=(1.0, 5.5), coord=Coordinates(46.6769043, 11.1851585),
name="Test Room", name="Test Room",
pin=None, pin=None,
tags=set(), tags=set(),
@ -85,7 +85,15 @@ def join():
if room.pin is not None and room.pin != code: if room.pin is not None and room.pin != code:
return error("Invalid code") return error("Invalid code")
if room.distance > room.range_size: distance = distance_between_coords(
lhs=room.coord,
rhs=Coordinates(
latitude=float(request.args["lat"]),
longitude=float(request.args["lon"]),
),
)
if distance > room.range_size:
return error("You are not within the room range") return error("You are not within the room range")
return {"success": True, "ws": f"/ws/{room_id}"} return {"success": True, "ws": f"/ws/{room_id}"}
@ -144,8 +152,8 @@ def room_new():
lat, lon = room_cords.split(",") lat, lon = room_cords.split(",")
room = Room( room = Room(
id=max(state.rooms or [0]) + 1, # id=max(state.rooms or [0]) + 1,
coord=(float(lat), float(lon)), coord=Coordinates(float(lat), float(lon)),
range_size=int(room_range), range_size=int(room_range),
name=room_name, name=room_name,
pin=room_pin, pin=room_pin,
@ -165,12 +173,12 @@ def room_new():
def room(): def room():
lat = request.args.get("lat") lat = request.args.get("lat")
lon = request.args.get("lon") lon = request.args.get("lon")
if lat and lon: if lat and lon:
user_coords = Coordinates(latitude=float(lat), longitude=float(lon)) user_coords = Coordinates(latitude=float(lat), longitude=float(lon))
else: else:
return error("Missing user coordinates") return error("Missing user coordinates")
distance = distance_between_coords(user_coords, room.coord)
return [ return [
{ {
"id": room.id, "id": room.id,
@ -178,10 +186,10 @@ def room():
"private": room.pin is not None, "private": room.pin is not None,
"coords": room.coord, "coords": room.coord,
"range": room.range_size, "range": room.range_size,
"distance": distance, "distance": d,
} }
for room in state.rooms.values() for room in state.rooms.values()
if distance <= room.range_size if (d := distance_between_coords(user_coords, room.coord)) <= room.range_size
] ]

View file

@ -1,9 +1,11 @@
import math import math
from dataclasses import dataclass
@dataclass
class Coordinates: class Coordinates:
latitude: int latitude: float
longitude: int longitude: float
def distance_between_coords(lhs: Coordinates, rhs: Coordinates) -> float: def distance_between_coords(lhs: Coordinates, rhs: Coordinates) -> float:

View file

@ -1,6 +1,7 @@
import random import random
from dataclasses import dataclass from dataclasses import dataclass
from gps import Coordinates
from song import Song from song import Song
USER_SCORE_WEIGHT = 0.7 USER_SCORE_WEIGHT = 0.7
@ -30,12 +31,11 @@ class Rank:
@dataclass @dataclass
class Room: class Room:
id: int id: int
coord: tuple[float, float] coord: Coordinates
name: str name: str
pin: int | None pin: int | None
tags: set[str] tags: set[str]
range_size: int # in meters ?? range_size: int # in meters ??
distance: float
songs: dict[str, UserScoredSong] # all songs + user score (the playlist) songs: dict[str, UserScoredSong] # all songs + user score (the playlist)
history: list[Song] # all songs previously played history: list[Song] # all songs previously played

View file

@ -1,3 +1,4 @@
@import url('https://fonts.googleapis.com/css2?family=Lilita+One&display=swap');
@import 'tailwindcss'; @import 'tailwindcss';
@keyframes spin-slower { @keyframes spin-slower {
@ -14,6 +15,20 @@
animation: spin-slower 15s linear infinite; animation: spin-slower 15s linear infinite;
} }
html,
body {
@apply bg-light-pine-base dark:bg-dark-pine-base;
/* or use your color class here */
height: 100%;
margin: 0;
}
.lilita-one-regular {
font-family: "Lilita One", sans-serif;
font-weight: 400;
font-style: normal;
}
@theme { @theme {
--color-dark-pine-base: hsl(249deg, 22%, 12%); --color-dark-pine-base: hsl(249deg, 22%, 12%);
--color-dark-pine-surface: hsl(247deg, 23%, 15%); --color-dark-pine-surface: hsl(247deg, 23%, 15%);

View file

@ -8,6 +8,10 @@
queueSongs[playingIndex], queueSongs[playingIndex],
playingIndex == queueSongs.length - 1 ? createEmptySong() : queueSongs[playingIndex + 1], playingIndex == queueSongs.length - 1 ? createEmptySong() : queueSongs[playingIndex + 1],
]) ])
$effect(() => {
console.log(displaySongs)
})
</script> </script>
<div class="relative flex w-full justify-center overflow-hidden"> <div class="relative flex w-full justify-center overflow-hidden">
@ -28,11 +32,7 @@
{/if} {/if}
</div> </div>
{:else} {:else}
<div class="flex h-[60vw] max-h-[250px] w-[60vw] max-w-[250px] items-center justify-center"> <div class="flex h-[60vw] max-h-[250px] w-[60vw] max-w-[250px] items-center justify-center"></div>
{#if i === 1}
<p>No song in queue</p>
{/if}
</div>
{/if} {/if}
{/each} {/each}
</div> </div>

View file

@ -1,16 +0,0 @@
<script lang="ts">
import { type Room } from "$lib/types"
let { room }: { room: Room } = $props()
</script>
<div class="room flex flex-col items-start gap-2 rounded-lg bg-white p-6 shadow-md">
<h2 class="flex items-center gap-2 text-xl font-semibold">
{room.name}
{#if room.private}
<span class="text-gray-500">🔒</span>
{/if}
</h2>
<!-- <span class="text-sm text-gray-600"> -->
<!-- {participants} participant{participants === 1 ? "" : "s"} -->
<!-- </span> -->
</div>

View file

@ -0,0 +1,18 @@
<script lang="ts">
import { type Room } from "$lib/types"
let { room }: { room: Room } = $props()
</script>
<button
class="flex w-82 cursor-pointer flex-row items-center rounded-md border border-dark-pine-muted bg-light-pine-overlay p-3 hover:bg-dark-pine-base/20 dark:bg-dark-pine-overlay hover:dark:bg-light-pine-base/20"
>
<div class="flex flex-row">
{room.name}
{room.private ? "🔒" : ""}
</div>
<div class="grow"></div>
<div class="flex flex-row items-center gap-2">
<div class="font-mono">64m</div>
<div class="rounded bg-light-pine-blue px-2 py-0.5 text-dark-pine-text dark:bg-dark-pine-blue">Join</div>
</div>
</button>

View file

@ -4,15 +4,25 @@
let input = $state("") let input = $state("")
async function sendSong() { async function sendSong() {
let resp = await fetch(`/api/addsong?room=${roomId}&query=${input}`, { method: "POST" }) await fetch(`/api/addsong?room=${roomId}&query=${input}`, { method: "POST" })
input = "" input = ""
} }
</script> </script>
<div class="bg-lime-500 flex h-full w-full flex-row items-center gap-2 rounded border-2 border-lime-600"> <div class="flex h-full w-full flex-row items-center gap-2 rounded border-2 border-lime-600 bg-lime-500">
<input type="text" placeholder="Song & Artist" class="font-bold outline-none text-white h-[50px] px-4 w-3/4 rounded" bind:value={input} /> <input
type="text"
placeholder="Song & Artist"
class="h-[50px] w-3/4 rounded px-4 font-bold text-white outline-none"
bind:value={input}
onkeydown={(e) => {
if (e.key == "Enter") {
sendSong()
}
}}
/>
<button <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" class="i-lucide-check h-[40px] w-1/4 cursor-pointer rounded border-2 border-lime-700 bg-lime-600 font-semibold text-white shadow-xl duration-100 hover:scale-105 active:scale-90"
onclick={sendSong}>Add</button onclick={sendSong}>Add</button
> >
<span class="i-lucide-chevrons-left"></span> <span class="i-lucide-chevrons-left"></span>

View file

@ -10,19 +10,19 @@ const SongSchema = z.object({
}) })
export type Song = z.infer<typeof SongSchema> export type Song = z.infer<typeof SongSchema>
export const parseSong = async function(song: any): Promise<Song> { export const parseSong = async function (song: any): Promise<Song> {
let resp = await SongSchema.parseAsync(song) let resp = await SongSchema.parseAsync(song)
return resp return resp
} }
export const createEmptySong = function(): Song { export const createEmptySong = function (): Song {
return { return {
uuid: "-1", uuid: "-1",
title: "", title: "",
artist: "", artist: "",
tags: [""], tags: [""],
image_id: "", image_id: "",
youtube_id: 0, youtube_id: "",
} }
} }
@ -31,7 +31,7 @@ const SuggestionSchema = SongSchema.extend({
}) })
export type Suggestion = z.infer<typeof SuggestionSchema> export type Suggestion = z.infer<typeof SuggestionSchema>
export const parseSuggestion = async function(sugg: any): Promise<Suggestion> { export const parseSuggestion = async function (sugg: any): Promise<Suggestion> {
let resp = await SuggestionSchema.parseAsync(sugg) let resp = await SuggestionSchema.parseAsync(sugg)
return resp return resp
} }
@ -41,11 +41,11 @@ const RoomSchema = z.object({
name: z.string(), name: z.string(),
private: z.boolean(), private: z.boolean(),
coords: z.tuple([z.number(), z.number()]), coords: z.tuple([z.number(), z.number()]),
range: z.number().int() range: z.number().int(),
}) })
export type Room = z.infer<typeof RoomSchema> export type Room = z.infer<typeof RoomSchema>
export const parseRoom = async function(room: any): Promise<Room> { export const parseRoom = async function (room: any): Promise<Room> {
let resp = await RoomSchema.parseAsync(room) let resp = await RoomSchema.parseAsync(room)
return resp return resp
} }

View file

@ -1,7 +1,7 @@
import { parseSong, parseSuggestion, type FetchError, type Song, type Suggestion } from "./types" import { parseSong, parseSuggestion, type FetchError, type Song, type Suggestion } from "./types"
export const joinRoom = async function (roomId: string): Promise<[FetchError | null, string]> { export const joinRoom = async function (roomId: string, lat: number, lon: number): Promise<[FetchError | null, string]> {
let resp = await fetch("/api/join?room=" + roomId) let resp = await fetch(`/api/join?room=${roomId}&lat=${lat}&lon=${lon}`)
if (resp.status != 200) { if (resp.status != 200) {
return [{ code: 400, message: "Cannot join the room" }, ""] return [{ code: 400, message: "Cannot join the room" }, ""]

View file

@ -1,10 +1,40 @@
<script lang="ts"> <script lang="ts">
import RoomComponent from "$lib/components/Room.svelte" import RoomComponent from "$lib/components/RoomComponent.svelte"
import { parseRoom, type Room } from "$lib/types" import { parseRoom, type Room } from "$lib/types"
import { Plus } from "@lucide/svelte"
let room: Room = { id: "asd", coords: [0.123, 0.456], name: "scatolame party", private: true, range: 124 } let room: Room = { id: "asd", coords: [0.123, 0.456], name: "scatolame party", private: true, range: 124 }
</script> </script>
<div class="flex flex-col items-center gap-2">
<div class="flex flex-row items-center gap-4 py-4">
<img src="icon_small.png" class="h-12 w-12" alt="chillbox icon" />
<span class="lilita-one-regular text-6xl font-bold">ChillBox</span>
</div>
<img src="/smerdo_radar_bonus.gif" alt="radar" class="h-64 w-64" />
<span class="animate-pulse text-sm italic">Scanning for rooms near you...</span>
<button class="fixed right-4 bottom-4 flex flex-row gap-1 rounded-xl bg-light-pine-blue p-2 text-dark-pine-text sm:right-20 md:right-40 lg:right-80 dark:bg-dark-pine-blue">
<span> New Room </span>
<Plus />
</button>
<div class="flex flex-col gap-2">
<RoomComponent {room}></RoomComponent>
<RoomComponent {room}></RoomComponent>
<RoomComponent {room}></RoomComponent>
<RoomComponent {room}></RoomComponent>
<RoomComponent {room}></RoomComponent>
<RoomComponent {room}></RoomComponent>
<RoomComponent {room}></RoomComponent>
<RoomComponent {room}></RoomComponent>
<RoomComponent {room}></RoomComponent>
<RoomComponent {room}></RoomComponent>
<RoomComponent {room}></RoomComponent>
<RoomComponent {room}></RoomComponent>
</div>
</div>
<!-- <div class="h-full w-full flex-row justify-center p-4"> --> <!-- <div class="h-full w-full flex-row justify-center p-4"> -->
<!-- <div class="relative min-h-screen justify-center justify-items-center"> --> <!-- <div class="relative min-h-screen justify-center justify-items-center"> -->
<!-- <h1>Scan your nearby rooms</h1> --> <!-- <h1>Scan your nearby rooms</h1> -->

View file

@ -19,7 +19,7 @@
let url = await getStreamingUrl(currentPlaying.uuid) let url = await getStreamingUrl(currentPlaying.uuid)
if (audioController) { if (audioController) {
audioController.src = url audioController.src = url
audioController.play() await audioController.play()
} }
} }
@ -29,11 +29,10 @@
queueSongs = songs queueSongs = songs
playingIndex = index playingIndex = index
})
if (audioController) { $effect(() => {
audioController.src = await getStreamingUrl(currentPlaying.uuid) playOnEnd()
audioController.play()
}
}) })
async function playNext() { async function playNext() {
@ -53,6 +52,6 @@
<div class="flex w-full flex-col items-center justify-center p-4 lg:p-10"> <div class="flex w-full flex-col items-center justify-center p-4 lg:p-10">
<QueueSlider {queueSongs} {playingIndex} /> <QueueSlider {queueSongs} {playingIndex} />
<button onclick={playNext}>Next</button> <button onclick={playNext}>Next</button>
<audio controls autoplay bind:this={audioController} onended={playOnEnd}></audio> <audio controls autoplay bind:this={audioController} onended={playNext}></audio>
</div> </div>
{/if} {/if}

View file

@ -8,6 +8,8 @@
import { getQueueSongs, getSuggestions, joinRoom } from "$lib/utils.js" import { getQueueSongs, getSuggestions, joinRoom } from "$lib/utils.js"
import type { FetchError } from "$lib/types.js" import type { FetchError } from "$lib/types.js"
import { io, Socket } from "socket.io-client" import { io, Socket } from "socket.io-client"
import { get_coords } from "$lib/gps.js"
import type { Coordinates } from "$lib/gps.js"
let { data } = $props() let { data } = $props()
@ -23,8 +25,14 @@
onMount(async () => { onMount(async () => {
// Join the room // Join the room
let { coords, error } = await get_coords()
if (error || coords == null) {
// Default to Lido
coords = { latitude: 46.6769043, longitude: 11.1851585 }
}
let sugg, queue, index let sugg, queue, index
;[returnError] = await joinRoom(data.roomId) ;[returnError] = await joinRoom(data.roomId, coords.latitude, coords.longitude)
if (returnError) { if (returnError) {
return return
} }
@ -69,7 +77,7 @@
{#if returnError} {#if returnError}
<Error {returnError} /> <Error {returnError} />
{:else} {:else}
<div class="flex w-full flex-col items-center justify-center py-4 px-2 lg:p-10"> <div class="flex w-full flex-col items-center justify-center px-2 py-4 lg:p-10">
<QueueSlider {queueSongs} {playingIndex} /> <QueueSlider {queueSongs} {playingIndex} />
<div class="w-full py-6 lg:w-[30vw]"> <div class="w-full py-6 lg:w-[30vw]">
<SuggestionInput roomId={data.roomId} /> <SuggestionInput roomId={data.roomId} />

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB