Compare commits
106 commits
websockets
...
main
Author | SHA1 | Date | |
---|---|---|---|
![]() |
7443d600e7 | ||
![]() |
bf71a8103d | ||
f7351aecf8 | |||
436553809a | |||
fb49da2edd | |||
![]() |
9a12f39f3e | ||
![]() |
9ec601b263 | ||
![]() |
1763a6b96f | ||
360338ccb2 | |||
278a2d94a8 | |||
![]() |
c3d30a1cc8 | ||
![]() |
1063c239b6 | ||
![]() |
6a50dc4c86 | ||
![]() |
efcabd2ee4 | ||
746189862a | |||
01cf53c775 | |||
7587796934 | |||
a244ca30d4 | |||
f4a4c46fbc | |||
![]() |
85bb34328d | ||
![]() |
ba2b5f04c6 | ||
![]() |
45ccadb736 | ||
![]() |
350f44b194 | ||
a0360f821a | |||
2a63a527f0 | |||
![]() |
fb34d2e945 | ||
![]() |
cbf90d1d0a | ||
![]() |
7e52746b37 | ||
![]() |
ec36ea3feb | ||
![]() |
cd9d47ad8d | ||
![]() |
88f34c6257 | ||
![]() |
3ee764ad03 | ||
![]() |
a7374d6d94 | ||
![]() |
795a5a3a19 | ||
![]() |
5632209ace | ||
![]() |
e045e11023 | ||
![]() |
69cf462662 | ||
![]() |
543db8d25f | ||
![]() |
0c34586358 | ||
596061adfe | |||
2a8802a0b8 | |||
396c7b0cea | |||
656d1e40c7 | |||
124e426b21 | |||
d50979077a | |||
fe7c740573 | |||
![]() |
bb16817811 | ||
244e49d311 | |||
![]() |
964c226bde | ||
![]() |
2c1928822b | ||
cd5e52cb23 | |||
6bdbae1881 | |||
3a944767a8 | |||
b7aebadffe | |||
![]() |
f3101ad8ab | ||
![]() |
9cb9c2cf72 | ||
d337f747f3 | |||
4bf93b565b | |||
78f03dec5d | |||
adc362562e | |||
ca0b441229 | |||
ebf3d29fa8 | |||
![]() |
a8d6279ef7 | ||
![]() |
b8b4ae2480 | ||
![]() |
7170c7b663 | ||
![]() |
23c58b299c | ||
![]() |
f36441565c | ||
db31dda495 | |||
46b055c591 | |||
![]() |
9402ff7059 | ||
![]() |
2b94bfddfb | ||
f70d2e707c | |||
0066166765 | |||
![]() |
9faba7dbd3 | ||
![]() |
419fde1a10 | ||
![]() |
426a2706d8 | ||
![]() |
f05b5666db | ||
![]() |
0bf67061f8 | ||
6c5fe4930b | |||
97b8bac747 | |||
![]() |
e4be88db2d | ||
![]() |
bfcb29fc9f | ||
2a167ba8ad | |||
2a4a4c3caa | |||
a6a7eeb690 | |||
72ceb0f8dc | |||
e01d52f7f4 | |||
6f1e590e50 | |||
5caa19fd9f | |||
64941061ea | |||
![]() |
7020a334a2 | ||
![]() |
44e0d9f44c | ||
![]() |
a737a651ec | ||
![]() |
a5b1943030 | ||
![]() |
57e6acb1ee | ||
![]() |
1383c0fbed | ||
aa25c6075b | |||
6a460ea38d | |||
6194dfbf34 | |||
7a9d29a2da | |||
0d659beffb | |||
4a3c75cd51 | |||
ba77a1857c | |||
41b2f61f30 | |||
5a7d661629 | |||
![]() |
ab9bfa41c9 |
41
README.md
|
@ -1,3 +1,40 @@
|
||||||
# team-1
|
# ChillBox
|
||||||
|
|
||||||
Test
|
> *A project by Pausetta.org, Simone Tesini, Francesco De Carlo, Leonardo Segala, Matteo Peretto*
|
||||||
|
|
||||||
|
**ChillBox** is a web app that lets you create a shared radio station with a democratic voting system, so everyone gets to enjoy their favorite music together.
|
||||||
|
Perfect for venues like swimming pools, cafés, or even lively parties.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎵 Voting System
|
||||||
|
|
||||||
|
Joining a ChillBox room is easy: users can either scan the QR code displayed on the host screen or use GPS to find nearby rooms.
|
||||||
|
Hosts can set a location range, ensuring only people physically present can add or vote for songs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Ranking Algorithm
|
||||||
|
|
||||||
|
ChillBox uses a smart ranking algorithm to decide what plays next. The score of each song is based on:
|
||||||
|
|
||||||
|
* Votes from users
|
||||||
|
* How recently similar songs (same genre or artist) have been played (less = better)
|
||||||
|
* A bit of randomness to keep things interesting
|
||||||
|
* A strong penalty for songs played too recently
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 👐 Hands-Off Experience
|
||||||
|
|
||||||
|
ChillBox is designed to be almost entirely hands-free.
|
||||||
|
Once the host sets up a room and optionally connects a screen or projector
|
||||||
|
(to show the current track, QR code, etc.), ChillBox takes care of the rest.
|
||||||
|
|
||||||
|
ChillBox comes with built-in automatic moderation to keep the music fair and on-theme.
|
||||||
|
|
||||||
|
* Users can’t vote for the same song multiple times.
|
||||||
|
* A cooldown prevents users from spamming song requests.
|
||||||
|
* Hosts can define preferred genres and overall mood, so no one can hijack your chill beach vibes with unexpected death metal.
|
||||||
|
|
||||||
|
That said, hosts still have access to essential controls, like pause and skip, if needed.
|
||||||
|
|
15
SPEECH.md
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
# speech
|
||||||
|
|
||||||
|
## Home screen
|
||||||
|
We start here in the home page.
|
||||||
|
We can see this little radar animation, which means that the app is looking for nearby ChillBox rooms to join.
|
||||||
|
It uses GPS for this feature.
|
||||||
|
|
||||||
|
## Join room
|
||||||
|
When we join a room, the server checks our location and checks if it's within a specified range.
|
||||||
|
That way, you must physically be in the location to actually be able to add new songs
|
||||||
|
|
||||||
|
## Talk about the host
|
||||||
|
As you can see here (and hear) on the left, the host is already playing some music.
|
||||||
|
Now i will add a song on the client side and it will pop up in the list.
|
||||||
|
|
|
@ -12,5 +12,6 @@ RUN pip install --no-cache-dir -r requirements.txt
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
EXPOSE 5000
|
EXPOSE 5000
|
||||||
CMD ["flask", "--app", "src.app", "run", "--debug", "--host=0.0.0.0"]
|
# CMD ["flask", "--app", "src.app", "run", "--debug", "--host=0.0.0.0"]
|
||||||
|
CMD ["python3", "src/app.py"]
|
||||||
# flask --app src.app run --host=0.0.0.0 --port=5001 --debug
|
# flask --app src.app run --host=0.0.0.0 --port=5001 --debug
|
||||||
|
|
|
@ -4,4 +4,5 @@ flask-socketio
|
||||||
dotenv
|
dotenv
|
||||||
requests
|
requests
|
||||||
qrcode
|
qrcode
|
||||||
Pillow
|
Pillow
|
||||||
|
eventlet>=0.33
|
||||||
|
|
|
@ -1,14 +1,17 @@
|
||||||
|
import uuid
|
||||||
|
from dataclasses import asdict
|
||||||
|
|
||||||
import dotenv
|
import dotenv
|
||||||
|
from connect import get_connection
|
||||||
from flask import Flask, Response, jsonify, request
|
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 gps import Coordinates, distance_between_coords
|
||||||
from .connect import get_connection
|
from qrcode_gen import generate_qr
|
||||||
from .qrcode_gen import generate_qr
|
from room import Room
|
||||||
from .room import Room
|
from song import Song, add_song_in_db, get_song_by_title_artist, get_song_by_uuid, init_db
|
||||||
from .song import Song, add_song_in_db, get_song_by_title_artist, init_db
|
from song_fetch import query_search, yt_get_audio_url, yt_search_song
|
||||||
from .song_fetch import download_song_mp3, lastfm_query_search
|
from state import State
|
||||||
from .state import State
|
|
||||||
|
|
||||||
dotenv.load_dotenv()
|
dotenv.load_dotenv()
|
||||||
|
|
||||||
|
@ -25,13 +28,14 @@ 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="Lido Scena",
|
||||||
pin=None,
|
pin=1234,
|
||||||
tags=set(),
|
tags=set(["chill", "raggaetton", "spanish", "latino", "mexican", "rock"]),
|
||||||
|
range_size=150,
|
||||||
songs={},
|
songs={},
|
||||||
history=[],
|
history=[],
|
||||||
playing=[Song(uuid="<uuid>", title="<title>", artist="<artist>", tags=[], image_id="<img>", youtube_id="<yt>")],
|
playing=[],
|
||||||
playing_idx=0,
|
playing_idx=0,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -69,7 +73,7 @@ def on_leave(data):
|
||||||
@app.get("/api/join")
|
@app.get("/api/join")
|
||||||
def join():
|
def join():
|
||||||
room_id = request.args.get("room")
|
room_id = request.args.get("room")
|
||||||
code = request.args.get("code")
|
code = request.args.get("pin")
|
||||||
|
|
||||||
if room_id is None:
|
if room_id is None:
|
||||||
return error("Missing room id")
|
return error("Missing room id")
|
||||||
|
@ -77,8 +81,22 @@ def join():
|
||||||
if (room := state.rooms.get(int(room_id))) is None:
|
if (room := state.rooms.get(int(room_id))) is None:
|
||||||
return error("Invalid room")
|
return error("Invalid room")
|
||||||
|
|
||||||
if room.pin is not None and room.pin != code:
|
if room.pin is not None:
|
||||||
return error("Invalid code")
|
if code is None:
|
||||||
|
return error("Missing code")
|
||||||
|
if int(room.pin) != int(code):
|
||||||
|
return error("Invalid code")
|
||||||
|
|
||||||
|
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 {"success": True, "ws": f"/ws/{room_id}"}
|
return {"success": True, "ws": f"/ws/{room_id}"}
|
||||||
|
|
||||||
|
@ -106,13 +124,15 @@ def queue_next():
|
||||||
|
|
||||||
if room.playing_idx >= len(room.playing):
|
if room.playing_idx >= len(room.playing):
|
||||||
## queue ended
|
## queue ended
|
||||||
|
room.renew_queue()
|
||||||
|
ended = True
|
||||||
|
else:
|
||||||
|
ended = False
|
||||||
|
|
||||||
# room.renew_queue()
|
data = {"success": True, "ended": ended, "index": room.playing_idx, "queue": [asdict(s) for s in room.playing]}
|
||||||
state.socketio.emit("update_queue", {"songs": room.playing}, to=str(room.id))
|
state.socketio.emit("queue_update", data, to=str(room.id))
|
||||||
|
|
||||||
return {"success": True, "ended": True, "index": room.playing_idx, "queue": room.playing}
|
return data
|
||||||
|
|
||||||
return {"success": True, "ended": False, "index": room.playing_idx}
|
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/room/new")
|
@app.post("/api/room/new")
|
||||||
|
@ -123,6 +143,9 @@ def room_new():
|
||||||
if (room_cords := request.args.get("coords")) is None:
|
if (room_cords := request.args.get("coords")) is None:
|
||||||
return error("Missing room coords")
|
return error("Missing room coords")
|
||||||
|
|
||||||
|
if (room_range := request.args.get("range")) is None:
|
||||||
|
return error("Missing room range")
|
||||||
|
|
||||||
if room_pin := request.args.get("pin"):
|
if room_pin := request.args.get("pin"):
|
||||||
room_pin = int(room_pin)
|
room_pin = int(room_pin)
|
||||||
else:
|
else:
|
||||||
|
@ -131,8 +154,9 @@ 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),
|
||||||
name=room_name,
|
name=room_name,
|
||||||
pin=room_pin,
|
pin=room_pin,
|
||||||
tags=set([tag for tag in request.args.get("tags", "").split(",") if tag]),
|
tags=set([tag for tag in request.args.get("tags", "").split(",") if tag]),
|
||||||
|
@ -149,14 +173,25 @@ def room_new():
|
||||||
|
|
||||||
@app.get("/api/room")
|
@app.get("/api/room")
|
||||||
def room():
|
def room():
|
||||||
|
lat = request.args.get("lat")
|
||||||
|
lon = request.args.get("lon")
|
||||||
|
|
||||||
|
if lat and lon:
|
||||||
|
user_coords = Coordinates(latitude=float(lat), longitude=float(lon))
|
||||||
|
else:
|
||||||
|
return error("Missing user coordinates")
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
"id": room.id,
|
"id": room.id,
|
||||||
"name": room.name,
|
"name": room.name,
|
||||||
"private": room.pin is not None,
|
"private": room.pin is not None,
|
||||||
"coords": room.coord,
|
"coords": room.coord,
|
||||||
|
"range": room.range_size,
|
||||||
|
"distance": d,
|
||||||
}
|
}
|
||||||
for room in state.rooms.values()
|
for room in state.rooms.values()
|
||||||
|
if (d := distance_between_coords(user_coords, room.coord)) <= room.range_size
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -171,20 +206,75 @@ def add_song():
|
||||||
if (query := request.args.get("query")) is None:
|
if (query := request.args.get("query")) is None:
|
||||||
return error("Missing query")
|
return error("Missing query")
|
||||||
|
|
||||||
info = lastfm_query_search(query)
|
if (info := query_search(query)) is None:
|
||||||
|
return error("Search failed")
|
||||||
|
|
||||||
if (song := get_song_by_title_artist(info.title, info.artist)) is None:
|
if (song := get_song_by_title_artist(info.title, info.artist)) is None:
|
||||||
res = download_song_mp3(info.title, info.artist)
|
|
||||||
if res is None:
|
|
||||||
return error("Cannot get info from YT")
|
|
||||||
yt_id, _ = res
|
|
||||||
## song not found, downolad from YT
|
## song not found, downolad from YT
|
||||||
|
yt_video_id = yt_search_song(info.title, info.artist)
|
||||||
|
|
||||||
|
if yt_video_id is None:
|
||||||
|
return error("No video found on youtube")
|
||||||
|
|
||||||
## add in DB
|
## add in DB
|
||||||
add_song_in_db(info.artist, info.title, info.tags, info.img_id, yt_id)
|
song = Song(
|
||||||
|
uuid=str(uuid.uuid4()),
|
||||||
|
title=info.title,
|
||||||
|
artist=info.artist,
|
||||||
|
tags=info.tags,
|
||||||
|
image_id=info.img_id,
|
||||||
|
youtube_id=yt_video_id,
|
||||||
|
)
|
||||||
|
|
||||||
## HERE: add song in the room list
|
add_song_in_db(song)
|
||||||
|
|
||||||
return {"artist": info.artist, "title": info.title, "tags": info.tags, "image_id": info.img_id}
|
if len(room.tags) > 0:
|
||||||
|
tag_ok = False
|
||||||
|
for tag in song.tags:
|
||||||
|
if tag in room.tags:
|
||||||
|
tag_ok = True
|
||||||
|
|
||||||
|
if not tag_ok:
|
||||||
|
return error("Song genre does not belong to this room")
|
||||||
|
|
||||||
|
## 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) | {"upvote": 1}}, to=str(room.id))
|
||||||
|
|
||||||
|
return {"success": True, "song": song}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/room/suggestions")
|
||||||
|
def get_room_suggestion():
|
||||||
|
if (room_id := request.args.get("room")) is None:
|
||||||
|
return error("Missing room id")
|
||||||
|
|
||||||
|
if (room := state.rooms.get(int(room_id))) is None:
|
||||||
|
return error("Invalid room id")
|
||||||
|
|
||||||
|
return {"success": True, "songs": [asdict(song) | {"upvote": score} for song, score in room.songs.values()]}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/song/voting")
|
||||||
|
def post_song_vote():
|
||||||
|
if (room_id := request.args.get("room")) is None:
|
||||||
|
return error("Missing room id")
|
||||||
|
|
||||||
|
if (room := state.rooms.get(int(room_id))) is None:
|
||||||
|
return error("Invalid room id")
|
||||||
|
|
||||||
|
if (song_id := request.args.get("song")) is None:
|
||||||
|
return error("Missing song id")
|
||||||
|
|
||||||
|
if (song_info := room.songs.get(song_id)) is None:
|
||||||
|
return error("Invalid song id")
|
||||||
|
|
||||||
|
## 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]) | {"upvote": song_info[1]}})
|
||||||
|
|
||||||
|
return {"success": True}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/room/qrcode")
|
@app.get("/api/room/qrcode")
|
||||||
|
@ -204,5 +294,19 @@ def room_qrcode():
|
||||||
return Response(stream, content_type="image/jpeg")
|
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__":
|
if __name__ == "__main__":
|
||||||
socketio.run(app, debug=True)
|
socketio.run(app, host="0.0.0.0", port=5000, debug=True)
|
||||||
|
|
29
backend/src/gps.py
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import math
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Coordinates:
|
||||||
|
latitude: float
|
||||||
|
longitude: float
|
||||||
|
|
||||||
|
|
||||||
|
def distance_between_coords(lhs: Coordinates, rhs: Coordinates) -> float:
|
||||||
|
R = 6371000 # Earth radius in meters
|
||||||
|
|
||||||
|
def to_rad(deg):
|
||||||
|
return (deg * math.pi) / 180
|
||||||
|
|
||||||
|
d_lat = to_rad(rhs.latitude - lhs.latitude)
|
||||||
|
d_lon = to_rad(rhs.longitude - lhs.longitude)
|
||||||
|
|
||||||
|
a = (d_lat / 2) ** 2 + (to_rad(lhs.latitude) * to_rad(rhs.latitude)) * (d_lon / 2) ** 2
|
||||||
|
|
||||||
|
c = 2 * (a**0.5) / ((1 - a) ** 0.5)
|
||||||
|
|
||||||
|
return R * c # distance in meters
|
||||||
|
|
||||||
|
|
||||||
|
def is_within_range(my_coords: Coordinates, target_coords: Coordinates, max_range: float) -> bool:
|
||||||
|
distance = distance_between_coords(my_coords, target_coords)
|
||||||
|
return distance <= max_range
|
|
@ -1,7 +1,8 @@
|
||||||
import random
|
import random
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from .song import Song
|
from gps import Coordinates
|
||||||
|
from song import Song
|
||||||
|
|
||||||
USER_SCORE_WEIGHT = 0.7
|
USER_SCORE_WEIGHT = 0.7
|
||||||
ARTIST_WEIGHT = 0.1
|
ARTIST_WEIGHT = 0.1
|
||||||
|
@ -30,10 +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 ??
|
||||||
|
|
||||||
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
|
||||||
|
@ -125,6 +127,7 @@ def test_algo():
|
||||||
"test",
|
"test",
|
||||||
None,
|
None,
|
||||||
set(["rock", "rap"]),
|
set(["rock", "rap"]),
|
||||||
|
100,
|
||||||
{
|
{
|
||||||
"paulham": (songs[0], 7),
|
"paulham": (songs[0], 7),
|
||||||
"cisco": (songs[1], 5),
|
"cisco": (songs[1], 5),
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
import uuid
|
|
||||||
from sqlite3 import Cursor
|
from sqlite3 import Cursor
|
||||||
|
|
||||||
from .connect import get_connection
|
from connect import get_connection
|
||||||
|
|
||||||
|
|
||||||
def init_db(db: Cursor):
|
def init_db(db: Cursor):
|
||||||
|
@ -38,7 +37,7 @@ def get_song_by_uuid(uuid: str) -> Song | None:
|
||||||
if row is None:
|
if row is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
song = Song(uuid=row["uuid"], title=row["title"], artist=row["artist"], tags=row["tags"].split(","), image_id=row["lastfm_image_id"], youtube_id=row["youtube_id"])
|
song = Song(uuid=row["uuid"], title=row["title"], artist=row["artist"], tags=list(filter(None, row["tags"].split(","))), image_id=row["lastfm_image_id"], youtube_id=row["youtube_id"])
|
||||||
return song
|
return song
|
||||||
|
|
||||||
|
|
||||||
|
@ -52,11 +51,11 @@ def get_song_by_title_artist(title: str, artist: str) -> Song | None:
|
||||||
if row is None:
|
if row is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
song = Song(uuid=row["uuid"], title=row["title"], artist=row["artist"], tags=row["tags"].split(","), image_id=row["lastfm_image_id"], youtube_id=row["youtube_id"])
|
song = Song(uuid=row["uuid"], title=row["title"], artist=row["artist"], tags=list(filter(None, row["tags"].split(","))), image_id=row["lastfm_image_id"], youtube_id=row["youtube_id"])
|
||||||
return song
|
return song
|
||||||
|
|
||||||
|
|
||||||
def add_song_in_db(title: str, artist: str, tags: list[str], image_id: str, yt_id: str) -> None:
|
def add_song_in_db(song: Song) -> None:
|
||||||
conn = get_connection()
|
conn = get_connection()
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
@ -65,7 +64,8 @@ def add_song_in_db(title: str, artist: str, tags: list[str], image_id: str, yt_i
|
||||||
INSERT OR REPLACE INTO songs (uuid, title, artist, tags, lastfm_image_id, youtube_id)
|
INSERT OR REPLACE INTO songs (uuid, title, artist, tags, lastfm_image_id, youtube_id)
|
||||||
VALUES (?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(str(uuid.uuid4()), title, artist, ",".join(tags), image_id, yt_id),
|
(song.uuid, song.title, song.artist, ",".join(song.tags), song.image_id, song.youtube_id),
|
||||||
) # Updates song info if it already exists
|
) # Updates song info if it already exists
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import requests
|
|
||||||
import urllib.parse
|
|
||||||
import os.path
|
|
||||||
import os
|
import os
|
||||||
|
import os.path
|
||||||
import sys
|
import sys
|
||||||
|
import urllib.parse
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
sys.path.append("/yt-dlp")
|
sys.path.append("/yt-dlp")
|
||||||
import yt_dlp
|
import yt_dlp
|
||||||
|
|
||||||
|
@ -17,7 +18,7 @@ class SongInfo:
|
||||||
tags: list[str]
|
tags: list[str]
|
||||||
|
|
||||||
|
|
||||||
def _lastfm_search(query: str) -> tuple[str, str]:
|
def _lastfm_search(query: str) -> tuple[str, str] | None:
|
||||||
response = requests.get(
|
response = requests.get(
|
||||||
url="https://ws.audioscrobbler.com/2.0/?method=track.search&format=json",
|
url="https://ws.audioscrobbler.com/2.0/?method=track.search&format=json",
|
||||||
params={"limit": 5, "track": query, "api_key": os.environ["LASTFM_API_KEY"]},
|
params={"limit": 5, "track": query, "api_key": os.environ["LASTFM_API_KEY"]},
|
||||||
|
@ -25,7 +26,10 @@ def _lastfm_search(query: str) -> tuple[str, str]:
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
track_info = response.json()["results"]["trackmatches"]["track"][0]
|
tracks = response.json()["results"]["trackmatches"]["track"]
|
||||||
|
if len(tracks) == 0:
|
||||||
|
return None
|
||||||
|
track_info = tracks[0]
|
||||||
|
|
||||||
return track_info["name"], track_info["artist"]
|
return track_info["name"], track_info["artist"]
|
||||||
|
|
||||||
|
@ -42,24 +46,48 @@ def _lastfm_getinfo(name: str, artist: str) -> tuple[str, list[str]]: # ( image
|
||||||
|
|
||||||
track_info = response.json()["track"]
|
track_info = response.json()["track"]
|
||||||
|
|
||||||
image_url = urllib.parse.urlparse(track_info["album"]["image"][0]["#text"])
|
image_id = ""
|
||||||
|
if "album" in track_info:
|
||||||
|
image_url = urllib.parse.urlparse(track_info["album"]["image"][0]["#text"])
|
||||||
|
image_id = os.path.splitext(os.path.basename(image_url.path))[0]
|
||||||
|
else:
|
||||||
|
print("this song haas no image", flush=True)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
# track_info["mbid"],
|
# track_info["mbid"],
|
||||||
os.path.splitext(os.path.basename(image_url.path))[0],
|
image_id,
|
||||||
[t["name"] for t in track_info["toptags"]["tag"]],
|
[t["name"] for t in track_info["toptags"]["tag"]],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def lastfm_query_search(query: str) -> SongInfo:
|
def _yt_search(query: str) -> tuple[str, str]:
|
||||||
name, artist = _lastfm_search(query)
|
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(query, download=False)
|
||||||
|
|
||||||
|
first = info["entries"][0]
|
||||||
|
|
||||||
|
return first["track"], first["artists"][0]
|
||||||
|
|
||||||
|
|
||||||
|
def query_search(query: str) -> SongInfo | None:
|
||||||
|
res = _lastfm_search(query)
|
||||||
|
if res is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
name, artist = res
|
||||||
img_id, tags = _lastfm_getinfo(name, artist)
|
img_id, tags = _lastfm_getinfo(name, artist)
|
||||||
|
|
||||||
return SongInfo(artist=artist, title=name, img_id=img_id, tags=tags)
|
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 | None: # video id
|
||||||
ydl_opts = {
|
ydl_opts = {
|
||||||
"format": "bestaudio",
|
"format": "bestaudio",
|
||||||
"default_search": "ytsearch1",
|
"default_search": "ytsearch1",
|
||||||
|
@ -70,12 +98,30 @@ def download_song_mp3(name: str, artist: str) -> tuple[str, str] | None: # ( id
|
||||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||||
info = ydl.extract_info(f"{name!r} - {artist!r}", download=False)
|
info = ydl.extract_info(f"{name!r} - {artist!r}", download=False)
|
||||||
|
|
||||||
first_entry = info["entries"][0]
|
if len(info["entries"]) == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
video_id = first_entry["id"]
|
return info["entries"][0]["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"]:
|
for fmt in first_entry["formats"]:
|
||||||
if "acodec" in fmt and fmt["acodec"] != "none":
|
if "acodec" in fmt and fmt["acodec"] != "none":
|
||||||
return video_id, fmt["url"]
|
return fmt["url"]
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
|
@ -4,7 +4,7 @@ from sqlite3 import Cursor
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from flask_socketio import SocketIO
|
from flask_socketio import SocketIO
|
||||||
|
|
||||||
from .room import Room
|
from room import Room
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|
256
frontend/package-lock.json
generated
|
@ -8,8 +8,9 @@
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"socket.io": "^4.8.1",
|
"@lucide/svelte": "^0.536.0",
|
||||||
"socket.io-client": "^4.8.1"
|
"socket.io-client": "^4.8.1",
|
||||||
|
"zod": "^4.0.14"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-auto": "^6.0.0",
|
"@sveltejs/adapter-auto": "^6.0.0",
|
||||||
|
@ -30,7 +31,6 @@
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
|
||||||
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
|
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/gen-mapping": "^0.3.5",
|
"@jridgewell/gen-mapping": "^0.3.5",
|
||||||
|
@ -499,7 +499,6 @@
|
||||||
"version": "0.3.12",
|
"version": "0.3.12",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz",
|
||||||
"integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==",
|
"integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||||
|
@ -510,7 +509,6 @@
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
|
@ -520,20 +518,27 @@
|
||||||
"version": "1.5.4",
|
"version": "1.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz",
|
||||||
"integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==",
|
"integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/trace-mapping": {
|
"node_modules/@jridgewell/trace-mapping": {
|
||||||
"version": "0.3.29",
|
"version": "0.3.29",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz",
|
||||||
"integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==",
|
"integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/resolve-uri": "^3.1.0",
|
"@jridgewell/resolve-uri": "^3.1.0",
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@lucide/svelte": {
|
||||||
|
"version": "0.536.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@lucide/svelte/-/svelte-0.536.0.tgz",
|
||||||
|
"integrity": "sha512-YAeoWU+0B/RriFZZ3wHno1FMkbrVrFdityuo2B0YuphD0vtJWXStzZkWLGVhT3jMb7zhugmhayIg+gI4+AZu1g==",
|
||||||
|
"license": "ISC",
|
||||||
|
"peerDependencies": {
|
||||||
|
"svelte": "^5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@polka/url": {
|
"node_modules/@polka/url": {
|
||||||
"version": "1.0.0-next.29",
|
"version": "1.0.0-next.29",
|
||||||
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
|
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
|
||||||
|
@ -831,7 +836,6 @@
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.5.tgz",
|
||||||
"integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==",
|
"integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"acorn": "^8.9.0"
|
"acorn": "^8.9.0"
|
||||||
|
@ -1203,49 +1207,16 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/cors": {
|
|
||||||
"version": "2.8.19",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
|
|
||||||
"integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
|
||||||
"version": "24.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz",
|
|
||||||
"integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"undici-types": "~7.8.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/accepts": {
|
|
||||||
"version": "1.3.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
|
||||||
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"mime-types": "~2.1.34",
|
|
||||||
"negotiator": "0.6.3"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.15.0",
|
"version": "8.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
|
@ -1258,7 +1229,6 @@
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
|
||||||
"integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
|
"integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
|
@ -1268,21 +1238,11 @@
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
||||||
"integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
|
"integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/base64id": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": "^4.5.0 || >= 5.9"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/chokidar": {
|
"node_modules/chokidar": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||||
|
@ -1313,7 +1273,6 @@
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||||
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
|
@ -1329,19 +1288,6 @@
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/cors": {
|
|
||||||
"version": "2.8.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
|
|
||||||
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"object-assign": "^4",
|
|
||||||
"vary": "^1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.1",
|
"version": "4.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||||
|
@ -1387,26 +1333,6 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/engine.io": {
|
|
||||||
"version": "6.6.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz",
|
|
||||||
"integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/cors": "^2.8.12",
|
|
||||||
"@types/node": ">=10.0.0",
|
|
||||||
"accepts": "~1.3.4",
|
|
||||||
"base64id": "2.0.0",
|
|
||||||
"cookie": "~0.7.2",
|
|
||||||
"cors": "~2.8.5",
|
|
||||||
"debug": "~4.3.1",
|
|
||||||
"engine.io-parser": "~5.2.1",
|
|
||||||
"ws": "~8.17.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10.2.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/engine.io-client": {
|
"node_modules/engine.io-client": {
|
||||||
"version": "6.6.3",
|
"version": "6.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz",
|
||||||
|
@ -1446,32 +1372,6 @@
|
||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/engine.io/node_modules/cookie": {
|
|
||||||
"version": "0.7.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
|
||||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/engine.io/node_modules/debug": {
|
|
||||||
"version": "4.3.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
|
||||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"ms": "^2.1.3"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"supports-color": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/enhanced-resolve": {
|
"node_modules/enhanced-resolve": {
|
||||||
"version": "5.18.2",
|
"version": "5.18.2",
|
||||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz",
|
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz",
|
||||||
|
@ -1532,14 +1432,12 @@
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
|
||||||
"integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==",
|
"integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/esrap": {
|
"node_modules/esrap": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.0.tgz",
|
||||||
"integrity": "sha512-yzmPNpl7TBbMRC5Lj2JlJZNPml0tzqoqP5B1JXycNUwtqma9AKCO0M2wHrdgsHcy1WRW7S9rJknAMtByg3usgA==",
|
"integrity": "sha512-yzmPNpl7TBbMRC5Lj2JlJZNPml0tzqoqP5B1JXycNUwtqma9AKCO0M2wHrdgsHcy1WRW7S9rJknAMtByg3usgA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.15"
|
"@jridgewell/sourcemap-codec": "^1.4.15"
|
||||||
|
@ -1586,7 +1484,6 @@
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
|
||||||
"integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
|
"integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/estree": "^1.0.6"
|
"@types/estree": "^1.0.6"
|
||||||
|
@ -1855,40 +1752,17 @@
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
|
||||||
"integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==",
|
"integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/magic-string": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.30.17",
|
"version": "0.30.17",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
|
||||||
"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
|
"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mime-db": {
|
|
||||||
"version": "1.52.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
|
||||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/mime-types": {
|
|
||||||
"version": "2.1.35",
|
|
||||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
|
||||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"mime-db": "1.52.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/minipass": {
|
"node_modules/minipass": {
|
||||||
"version": "7.1.2",
|
"version": "7.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||||
|
@ -1973,24 +1847,6 @@
|
||||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/negotiator": {
|
|
||||||
"version": "0.6.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
|
||||||
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/object-assign": {
|
|
||||||
"version": "4.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
|
||||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
|
@ -2243,51 +2099,6 @@
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/socket.io": {
|
|
||||||
"version": "4.8.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz",
|
|
||||||
"integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"accepts": "~1.3.4",
|
|
||||||
"base64id": "~2.0.0",
|
|
||||||
"cors": "~2.8.5",
|
|
||||||
"debug": "~4.3.2",
|
|
||||||
"engine.io": "~6.6.0",
|
|
||||||
"socket.io-adapter": "~2.5.2",
|
|
||||||
"socket.io-parser": "~4.2.4"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10.2.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/socket.io-adapter": {
|
|
||||||
"version": "2.5.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz",
|
|
||||||
"integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"debug": "~4.3.4",
|
|
||||||
"ws": "~8.17.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/socket.io-adapter/node_modules/debug": {
|
|
||||||
"version": "4.3.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
|
||||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"ms": "^2.1.3"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"supports-color": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/socket.io-client": {
|
"node_modules/socket.io-client": {
|
||||||
"version": "4.8.1",
|
"version": "4.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
|
||||||
|
@ -2350,23 +2161,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/socket.io/node_modules/debug": {
|
|
||||||
"version": "4.3.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
|
||||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"ms": "^2.1.3"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"supports-color": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
|
@ -2381,7 +2175,6 @@
|
||||||
"version": "5.37.1",
|
"version": "5.37.1",
|
||||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.37.1.tgz",
|
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.37.1.tgz",
|
||||||
"integrity": "sha512-h8arWpQZ+3z8eahyBT5KkiBOUsG6xvI5Ykg0ozRr9xEdImgSMUPUlOFWRNkUsT7Ti0DSUCTEbPoped0aoxFyWA==",
|
"integrity": "sha512-h8arWpQZ+3z8eahyBT5KkiBOUsG6xvI5Ykg0ozRr9xEdImgSMUPUlOFWRNkUsT7Ti0DSUCTEbPoped0aoxFyWA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ampproject/remapping": "^2.3.0",
|
"@ampproject/remapping": "^2.3.0",
|
||||||
|
@ -2503,21 +2296,6 @@
|
||||||
"node": ">=14.17"
|
"node": ">=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/undici-types": {
|
|
||||||
"version": "7.8.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
|
|
||||||
"integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/vary": {
|
|
||||||
"version": "1.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
|
||||||
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.0.6.tgz",
|
||||||
|
@ -2656,8 +2434,16 @@
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz",
|
||||||
"integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==",
|
"integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/zod": {
|
||||||
|
"version": "4.0.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/zod/-/zod-4.0.14.tgz",
|
||||||
|
"integrity": "sha512-nGFJTnJN6cM2v9kXL+SOBq3AtjQby3Mv5ySGFof5UGRHrRioSJ5iG680cYNjE/yWk671nROcpPj4hAS8nyLhSw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,8 @@
|
||||||
"vite": "^7.0.4"
|
"vite": "^7.0.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"socket.io": "^4.8.1",
|
"@lucide/svelte": "^0.536.0",
|
||||||
"socket.io-client": "^4.8.1"
|
"socket.io-client": "^4.8.1",
|
||||||
|
"zod": "^4.0.14"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1,66 @@
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Lilita+One&display=swap');
|
||||||
@import 'tailwindcss';
|
@import 'tailwindcss';
|
||||||
|
|
||||||
|
@keyframes spin-slower {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.spin-slower {
|
||||||
|
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 {
|
||||||
|
--color-dark-pine-base: hsl(249deg, 22%, 12%);
|
||||||
|
--color-dark-pine-surface: hsl(247deg, 23%, 15%);
|
||||||
|
--color-dark-pine-overlay: hsl(248deg, 25%, 18%);
|
||||||
|
--color-dark-pine-muted: hsl(249deg, 12%, 47%);
|
||||||
|
--color-dark-pine-subtle: hsl(248deg, 15%, 61%);
|
||||||
|
--color-dark-pine-text: hsl(245deg, 50%, 91%);
|
||||||
|
--color-dark-pine-red: hsl(343deg, 76%, 68%);
|
||||||
|
--color-dark-pine-yellow: hsl(35deg, 88%, 72%);
|
||||||
|
--color-dark-pine-pink: hsl(2deg, 55%, 83%);
|
||||||
|
--color-dark-pine-blue: hsl(197deg, 49%, 38%);
|
||||||
|
--color-dark-pine-green: hsl(189deg, 43%, 73%);
|
||||||
|
--color-dark-pine-purple: hsl(267deg, 57%, 78%);
|
||||||
|
--color-dark-pine-highlight-low: hsl(244deg, 18%, 15%);
|
||||||
|
--color-dark-pine-highlight-med: hsl(249deg, 15%, 28%);
|
||||||
|
--color-dark-pine-highlight-high: hsl(248deg, 13%, 36%);
|
||||||
|
}
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--color-light-pine-base: hsl(32deg, 57%, 95%);
|
||||||
|
--color-light-pine-surface: hsl(35deg, 100%, 98%);
|
||||||
|
--color-light-pine-overlay: hsl(33deg, 43%, 91%);
|
||||||
|
--color-light-pine-muted: hsl(257deg, 9%, 61%);
|
||||||
|
--color-light-pine-subtle: hsl(248deg, 12%, 52%);
|
||||||
|
--color-light-pine-text: hsl(248deg, 19%, 40%);
|
||||||
|
--color-light-pine-red: hsl(343deg, 35%, 55%);
|
||||||
|
--color-light-pine-yellow: hsl(35deg, 81%, 56%);
|
||||||
|
--color-light-pine-pink: hsl(3deg, 53%, 67%);
|
||||||
|
--color-light-pine-blue: hsl(197deg, 53%, 34%);
|
||||||
|
--color-light-pine-green: hsl(189deg, 30%, 48%);
|
||||||
|
--color-light-pine-purple: hsl(268deg, 21%, 57%);
|
||||||
|
--color-light-pine-highlight-low: hsl(25deg, 35%, 93%);
|
||||||
|
--color-light-pine-highlight-med: hsl(10deg, 9%, 86%);
|
||||||
|
--color-light-pine-highlight-high: hsl(315deg, 4%, 80%);
|
||||||
|
}
|
||||||
|
|
|
@ -1,15 +1,13 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
<head>
|
||||||
<head>
|
<meta charset="utf-8" />
|
||||||
<meta charset="utf-8" />
|
<link rel="icon" href="%sveltekit.assets%/favicon.ico" />
|
||||||
<link rel="icon" href="%sveltekit.assets%/favicon.svg" />
|
<link rel="manifest" href="manifest.json" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
</head>
|
</head>
|
||||||
|
<body data-sveltekit-preload-data="hover" class="h-max">
|
||||||
<body data-sveltekit-preload-data="hover" class="h-max">
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
<div style="display: contents">%sveltekit.body%</div>
|
</body>
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
10
frontend/src/lib/components/Error.svelte
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { FetchError } from "$lib/types"
|
||||||
|
|
||||||
|
let { returnError }: { returnError: FetchError } = $props()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex h-screen w-full flex-col items-center justify-center">
|
||||||
|
<h1 class="p-2 text-xl">Error {returnError.code}</h1>
|
||||||
|
<p>{returnError.message}</p>
|
||||||
|
</div>
|
|
@ -1,30 +1,44 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
let { songs, playing } = $props()
|
import { type Song, createEmptySong } from "$lib/types"
|
||||||
|
|
||||||
let displaySongs = $derived([
|
let { queueSongs, playingIndex } = $props()
|
||||||
playing > 0 && playing < songs.length ? songs[playing - 1] : { name: "", image: "" },
|
|
||||||
songs[playing],
|
let displaySongs = $derived<Song[]>([
|
||||||
playing == songs.length - 1 ? { name: "", image: "" } : songs[playing + 1],
|
playingIndex > 0 ? queueSongs[playingIndex - 1] : createEmptySong(),
|
||||||
|
queueSongs[playingIndex],
|
||||||
|
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">
|
||||||
<div class="flex w-fit flex-row gap-4">
|
<div class="flex w-fit flex-row gap-4">
|
||||||
{#each displaySongs as song, i}
|
{#each displaySongs as song, i}
|
||||||
{#if song.name != ""}
|
{#if song?.title && song.title != ""}
|
||||||
<div class={`relative flex flex-col items-center transition-all duration-300 ${i === 1 ? "z-10" : "z-0 scale-85 opacity-50"}`}>
|
<div class={`relative flex flex-col items-center transition-all duration-300 ${i === 1 ? "z-10" : "z-0 scale-85 opacity-50"}`}>
|
||||||
<img
|
<div
|
||||||
class="h-[60vw] max-h-[250px] w-[60vw]
|
class={`flex h-[60vw] max-h-[250px] w-[60vw] max-w-[250px] items-center justify-center ${i === 1 ? "spin-slower rounded-full border-2 border-black" : "rounded"} object-cover`}
|
||||||
max-w-[250px] rounded object-cover transition-all duration-300"
|
>
|
||||||
src={song.image}
|
{#if i === 1}
|
||||||
alt="Song cover"
|
<div class="absolute z-20 h-16 w-16 rounded-full border-2 border-black bg-light-pine-base dark:bg-dark-pine-base"></div>
|
||||||
/>
|
{/if}
|
||||||
|
<img
|
||||||
|
class={`h-full overflow-hidden ${i === 1 ? "rounded-full" : "rounded"}`}
|
||||||
|
src={song.image_id != ""
|
||||||
|
? `https://lastfm.freetls.fastly.net/i/u/174s/${song.image_id}.png`
|
||||||
|
: "https://s2.qwant.com/thumbr/474x474/f/6/b50687db1ebb262ac78b98a8f3c56a1e62235aaeebe0346dd27d4fbf1edec8/OIP.kXN41HyriW5dLTkjm0QQoAHaHa.jpg?u=https%3A%2F%2Ftse.mm.bing.net%2Fth%2Fid%2FOIP.kXN41HyriW5dLTkjm0QQoAHaHa%3Fpid%3DApi&q=0&b=1&p=0&a=0"}
|
||||||
|
alt="Song cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
{#if i === 1}
|
{#if i === 1}
|
||||||
<h1 class="mt-2">{song.name}</h1>
|
<h1 class="mt-5">{song.title} - {song.artist}</h1>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="h-[60vw] max-h-[250px] w-[60vw] max-w-[250px]"></div>
|
<div class="flex h-[60vw] max-h-[250px] w-[60vw] max-w-[250px] items-center justify-center"></div>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
45
frontend/src/lib/components/RoomComponent.svelte
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { type Room } from "$lib/types"
|
||||||
|
let { room }: { room: Room } = $props()
|
||||||
|
let showPinModal: boolean = $state(false)
|
||||||
|
let pin: number = $state()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex gap-2 w-82 cursor-pointer flex-col 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"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="flex flex-row items-center"
|
||||||
|
onclick={() => {
|
||||||
|
if (!room.private) {
|
||||||
|
window.location.href = "/room/" + room.id
|
||||||
|
return
|
||||||
|
}
|
||||||
|
showPinModal = !showPinModal
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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">{Math.round(room.distance)}m</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>
|
||||||
|
{#if showPinModal}
|
||||||
|
<input
|
||||||
|
placeholder="PIN (requied)"
|
||||||
|
class="p-2 text-xl rounded-md border-dark-pine-muted bg-light-pine-overlay dark:bg-dark-pine-base hover:dark:bg-light-pine-base/20 duration-100 outline-none focus:ring-2"
|
||||||
|
type="number"
|
||||||
|
bind:value={pin}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onclick={() => {
|
||||||
|
window.location.href = `/room/${room.id}?pin=${pin}`
|
||||||
|
}}
|
||||||
|
class="p-2 text-xl rounded-md border-dark-pine-muted bg-light-pine-overlay dark:bg-dark-pine-base hover:dark:bg-light-pine-base/20 duration-100 outline-none focus:ring-2">JOIN</button
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
</div>
|
|
@ -1,7 +1,69 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { LoaderCircle } from "@lucide/svelte"
|
||||||
|
|
||||||
|
const COOLDOWN_SECS = 10
|
||||||
|
|
||||||
|
let { roomId } = $props()
|
||||||
|
let input = $state("")
|
||||||
|
let loading: boolean = $state(false)
|
||||||
|
let cooldowned: boolean = $state(false)
|
||||||
|
let errorMsg: string = $state()
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
console.log("cooldowned is now", cooldowned)
|
||||||
|
})
|
||||||
|
|
||||||
|
async function sendSong() {
|
||||||
|
loading = true
|
||||||
|
const res = await fetch(`/api/addsong?room=${roomId}&query=${input}`, { method: "POST" })
|
||||||
|
const json = await res.json()
|
||||||
|
input = ""
|
||||||
|
loading = false
|
||||||
|
|
||||||
|
if (!json.success) {
|
||||||
|
errorMsg = json.error
|
||||||
|
}
|
||||||
|
|
||||||
|
cooldowned = true
|
||||||
|
setTimeout(() => {
|
||||||
|
cooldowned = false
|
||||||
|
console.log("unset cooldown")
|
||||||
|
}, COOLDOWN_SECS * 1000)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex h-full w-full flex-row items-center gap-2 rounded border-2 border-black p-4">
|
<div
|
||||||
<input type="text" placeholder="Song & Artist" class="h-full w-3/4 rounded" />
|
class={`flex h-full w-full flex-row items-center gap-2 rounded-md border-dark-pine-muted bg-light-pine-overlay hover:bg-dark-pine-base/20 dark:bg-dark-pine-overlay hover:dark:bg-light-pine-base/20 ${loading ? "disabled" : ""}`}
|
||||||
<button class="w-1/4 rounded">Send</button>
|
>
|
||||||
|
<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) => {
|
||||||
|
errorMsg = null
|
||||||
|
if (e.key == "Enter") {
|
||||||
|
sendSong()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
{#if loading}
|
||||||
|
<span class="animate-spin">
|
||||||
|
<LoaderCircle />
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<button
|
||||||
|
disabled={cooldowned}
|
||||||
|
class="i-lucide-check h-[40px] w-1/4 rounded font-semibold shadow-xl duration-100 active:scale-90 {!cooldowned
|
||||||
|
? 'cursor-pointer bg-light-pine-blue hover:scale-105 dark:bg-dark-pine-blue '
|
||||||
|
: 'bg-light-pine-muted dark:bg-light-pine-muted'}"
|
||||||
|
onclick={sendSong}>Add</button
|
||||||
|
>
|
||||||
|
<span class="i-lucide-chevrons-left"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<p class="text-red-500 font-semibold">
|
||||||
|
{errorMsg}
|
||||||
|
</p>
|
||||||
|
|
|
@ -1,32 +1,62 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Song } from "$lib/types"
|
import type { Suggestion } from "$lib/types"
|
||||||
let { suggestions }: { suggestions: Song[] } = $props()
|
import { ThumbsUp, ThumbsDown } from "@lucide/svelte"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
let reactiveSugg = $derived(
|
let { suggestions = $bindable(), roomId }: { suggestions: Suggestion[]; roomId: string } = $props()
|
||||||
[...suggestions].sort((a, b) => {
|
|
||||||
return b.points - a.points
|
let picked_suggestions: string[] = $state([])
|
||||||
})
|
|
||||||
)
|
async function vote(amount: number, songId: string) {
|
||||||
|
if (picked_suggestions.includes(songId)) return console.log("rejecting vote")
|
||||||
|
await fetch(`/api/song/voting?room=${roomId}&song=${songId}&increment=${amount}`, { method: "POST" })
|
||||||
|
picked_suggestions.push(songId)
|
||||||
|
console.log("accepted vote")
|
||||||
|
sessionStorage.setItem("picked_suggestions", JSON.stringify(picked_suggestions))
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
picked_suggestions = JSON.parse(sessionStorage.getItem("picked_suggestions") ?? "[]")
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex h-full w-full flex-col items-center gap-2 overflow-y-auto">
|
<div class="flex h-full w-full flex-col items-center gap-2 overflow-y-auto">
|
||||||
{#each reactiveSugg as sug}
|
{#if suggestions.length == 0}
|
||||||
<div class="flex h-[80px] w-full flex-row gap-2 rounded border-2 border-black p-2">
|
<p>No suggestions yet! Try to add a new one using the Add button</p>
|
||||||
<div class="flex w-3/4 flex-row gap-2">
|
{/if}
|
||||||
<img class="w-[60px] min-w-[60px] rounded" src={sug.image} alt="Song cover" />
|
|
||||||
<h1>{sug.name}</h1>
|
{#each suggestions as sug}
|
||||||
|
<div
|
||||||
|
class="flex h-[80px] w-full flex-row gap-2 rounded-md border-dark-pine-muted bg-light-pine-overlay p-2 shadow-md duration-100 hover:bg-dark-pine-base/20 dark:bg-dark-pine-overlay hover:dark:bg-light-pine-base/20"
|
||||||
|
>
|
||||||
|
<div class="flex w-3/4 flex-row items-center gap-2">
|
||||||
|
<img
|
||||||
|
class="w-[60px] min-w-[60px] rounded"
|
||||||
|
alt="Song cover"
|
||||||
|
src={sug.image_id != ""
|
||||||
|
? `https://lastfm.freetls.fastly.net/i/u/174s/${sug.image_id}.png`
|
||||||
|
: "https://s2.qwant.com/thumbr/474x474/f/6/b50687db1ebb262ac78b98a8f3c56a1e62235aaeebe0346dd27d4fbf1edec8/OIP.kXN41HyriW5dLTkjm0QQoAHaHa.jpg?u=https%3A%2F%2Ftse.mm.bing.net%2Fth%2Fid%2FOIP.kXN41HyriW5dLTkjm0QQoAHaHa%3Fpid%3DApi&q=0&b=1&p=0&a=0"}
|
||||||
|
/>
|
||||||
|
<div class="h-fit w-fit flex-col text-white">
|
||||||
|
<b>{sug.title}</b>
|
||||||
|
<p>{sug.artist}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex w-1/4 flex-row items-center justify-center gap-2">
|
<div class="flex w-1/4 flex-row items-center justify-center gap-2">
|
||||||
<button
|
<button
|
||||||
onclick={() => {
|
class={!picked_suggestions.includes(sug.uuid) ? "text-light-pine-green duration-100 hover:scale-150 dark:text-dark-pine-green" : "text-light-pine-muted dark:text-dark-pine-muted"}
|
||||||
sug.points += 1
|
disabled={!!picked_suggestions.includes(sug.uuid)}
|
||||||
}}>🔥</button
|
onclick={async () => {
|
||||||
|
await vote(1, sug.uuid)
|
||||||
|
}}><ThumbsUp /></button
|
||||||
>
|
>
|
||||||
<p>{sug.points}</p>
|
<p class="font-semibold text-light-pine-text dark:text-dark-pine-text">{sug.upvote}</p>
|
||||||
<button
|
<button
|
||||||
onclick={() => {
|
class={!picked_suggestions.includes(sug.uuid) ? "text-light-pine-red duration-100 hover:scale-150 dark:text-dark-pine-red" : "text-light-pine-muted dark:text-dark-pine-muted"}
|
||||||
sug.points--
|
disabled={!!picked_suggestions.includes(sug.uuid)}
|
||||||
}}>💩</button
|
onclick={async () => {
|
||||||
|
await vote(-1, sug.uuid)
|
||||||
|
}}><ThumbsDown /></button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
36
frontend/src/lib/gps.ts
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
export type Coordinates = {
|
||||||
|
latitude: number
|
||||||
|
longitude: number
|
||||||
|
}
|
||||||
|
|
||||||
|
function geolocation_to_simple_coords(coordinates: GeolocationCoordinates): Coordinates {
|
||||||
|
return { latitude: coordinates.latitude, longitude: coordinates.longitude }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function get_coords(): Promise<{ coords: Coordinates | null; error: string | null }> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (!navigator.geolocation) {
|
||||||
|
resolve({ coords: null, error: "Geolocation is not supported by your browser" })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const error_callback = (gps_error: GeolocationPositionError) => {
|
||||||
|
console.log(gps_error)
|
||||||
|
resolve({
|
||||||
|
coords: null,
|
||||||
|
error: `Unable to retrieve your location: (${gps_error.message})`,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const success_callback = (gps_position: GeolocationPosition) => {
|
||||||
|
resolve({
|
||||||
|
coords: geolocation_to_simple_coords(gps_position.coords),
|
||||||
|
error: null,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
navigator.geolocation.getCurrentPosition(success_callback, error_callback)
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,9 +1,57 @@
|
||||||
import * as z from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
export const SongSchema = z.object({
|
const SongSchema = z.object({
|
||||||
name: z.string(),
|
uuid: z.string(),
|
||||||
image: z.string(),
|
title: z.string(),
|
||||||
points: z.number().optional().default(0),
|
artist: z.string(),
|
||||||
|
tags: z.array(z.string()),
|
||||||
|
image_id: z.string(),
|
||||||
|
youtube_id: z.string(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export type Song = z.infer<typeof SongSchema>
|
export type Song = z.infer<typeof SongSchema>
|
||||||
|
|
||||||
|
export const parseSong = async function (song: any): Promise<Song> {
|
||||||
|
let resp = await SongSchema.parseAsync(song)
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createEmptySong = function (): Song {
|
||||||
|
return {
|
||||||
|
uuid: "-1",
|
||||||
|
title: "",
|
||||||
|
artist: "",
|
||||||
|
tags: [""],
|
||||||
|
image_id: "",
|
||||||
|
youtube_id: "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const SuggestionSchema = SongSchema.extend({
|
||||||
|
upvote: z.number(),
|
||||||
|
})
|
||||||
|
export type Suggestion = z.infer<typeof SuggestionSchema>
|
||||||
|
|
||||||
|
export const parseSuggestion = async function (sugg: any): Promise<Suggestion> {
|
||||||
|
let resp = await SuggestionSchema.parseAsync(sugg)
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
const RoomSchema = z.object({
|
||||||
|
id: z.number(),
|
||||||
|
name: z.string(),
|
||||||
|
private: z.boolean(),
|
||||||
|
coords: z.object({ latitude: z.number(), longitude: z.number() }),
|
||||||
|
range: z.number().int(),
|
||||||
|
distance: z.number()
|
||||||
|
})
|
||||||
|
export type Room = z.infer<typeof RoomSchema>
|
||||||
|
|
||||||
|
export const parseRoom = async function (room: any): Promise<Room> {
|
||||||
|
let resp = await RoomSchema.parseAsync(room)
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FetchError = {
|
||||||
|
code: number
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
76
frontend/src/lib/utils.ts
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
import { type Coordinates } from "./gps"
|
||||||
|
import { parseSong, parseSuggestion, type FetchError, type Song, type Suggestion } from "./types"
|
||||||
|
|
||||||
|
export const joinRoom = async function (roomId: string, coords: Coordinates, pin: string): Promise<[FetchError | null, string]> {
|
||||||
|
let res = await fetch(`/api/join?room=${roomId}&lat=${coords.latitude}&lon=${coords.longitude}&pin=${pin}`)
|
||||||
|
|
||||||
|
if (res.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[] = []
|
||||||
|
|
||||||
|
json["queue"].forEach(async (i: any) => {
|
||||||
|
songs.push(await parseSong(i))
|
||||||
|
})
|
||||||
|
return [null, songs, json["index"]]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getStreamingUrl = async function (uuid: string) {
|
||||||
|
let resp = await fetch("/api/song/audio?song=" + uuid)
|
||||||
|
|
||||||
|
let json = await resp.json()
|
||||||
|
|
||||||
|
return json["url"]
|
||||||
|
}
|
|
@ -4,4 +4,6 @@
|
||||||
let { children } = $props()
|
let { children } = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{@render children()}
|
<div class="min-h-screen w-full bg-light-pine-base px-1 text-light-pine-text sm:px-20 md:px-40 lg:px-80 dark:bg-dark-pine-base dark:text-dark-pine-text">
|
||||||
|
{@render children()}
|
||||||
|
</div>
|
||||||
|
|
|
@ -1,21 +1,44 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import RoomComponent from "$lib/components/RoomComponent.svelte"
|
||||||
|
import { get_coords } from "$lib/gps"
|
||||||
|
import { parseRoom, type Room } from "$lib/types"
|
||||||
|
import { Plus } from "@lucide/svelte"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
let pos = $state()
|
let rooms: Room[] = $state([])
|
||||||
|
|
||||||
function onGPS(t: GeolocationPosition) {
|
onMount(async () => {
|
||||||
pos = t.coords
|
let { coords, error } = await get_coords()
|
||||||
}
|
if (error != null || coords == null) coords = { latitude: 46.6769043, longitude: 11.1851585 }
|
||||||
|
|
||||||
function failGPS(t: GeolocationPositionError) {
|
let res = await fetch(`/api/room?lat=${coords.latitude}&lon=${coords.longitude}`)
|
||||||
console.log(t)
|
let json = await res.json()
|
||||||
}
|
for (let room of json) rooms.push(await parseRoom(room))
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
navigator.geolocation.getCurrentPosition(onGPS, failGPS)
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex h-full w-full items-center justify-center">
|
<div class="flex flex-col items-center gap-2">
|
||||||
<div></div>
|
<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="/radar_bonus.gif" alt="radar" class="h-64 w-64" />
|
||||||
|
<span class="animate-pulse text-sm italic">Scanning for rooms near you...</span>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onclick={() => {
|
||||||
|
window.location.href = "/room/create"
|
||||||
|
}}
|
||||||
|
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">
|
||||||
|
{#each rooms as room}
|
||||||
|
<RoomComponent {room} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
118
frontend/src/routes/admin/[id]/+page.svelte
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import QueueSlider from "$lib/components/QueueSlider.svelte"
|
||||||
|
import { type Song } from "$lib/types"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
import type { FetchError } from "$lib/types"
|
||||||
|
import { getQueueSongs, getStreamingUrl, triggerPlayNext } from "$lib/utils.js"
|
||||||
|
import Error from "$lib/components/Error.svelte"
|
||||||
|
|
||||||
|
let { data } = $props()
|
||||||
|
|
||||||
|
let queueSongs = $state<Song[]>([])
|
||||||
|
let playingIndex = $state<number>(0)
|
||||||
|
let returnError = $state<FetchError | null>()
|
||||||
|
|
||||||
|
let currentPlaying = $derived<Song>(queueSongs[playingIndex])
|
||||||
|
let audioController = $state<HTMLAudioElement>()
|
||||||
|
|
||||||
|
let playerInfo = $state({
|
||||||
|
playing: false,
|
||||||
|
currentTime: 0,
|
||||||
|
duration: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
async function playOnEnd() {
|
||||||
|
let url = await getStreamingUrl(currentPlaying.uuid)
|
||||||
|
if (audioController) {
|
||||||
|
audioController.src = url
|
||||||
|
await audioController.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (audioController) {
|
||||||
|
audioController.ontimeupdate = () => {
|
||||||
|
playerInfo.currentTime = audioController?.currentTime || 0
|
||||||
|
}
|
||||||
|
audioController.onloadedmetadata = () => {
|
||||||
|
playerInfo.duration = audioController?.duration || 0
|
||||||
|
}
|
||||||
|
audioController.onplay = () => {
|
||||||
|
playerInfo.playing = true
|
||||||
|
}
|
||||||
|
audioController.onpause = () => {
|
||||||
|
playerInfo.playing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
let songs, index
|
||||||
|
;[returnError, songs, index] = await getQueueSongs(data.roomId)
|
||||||
|
|
||||||
|
queueSongs = songs
|
||||||
|
playingIndex = index
|
||||||
|
|
||||||
|
if (queueSongs.length == 0) {
|
||||||
|
playNext()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
playOnEnd()
|
||||||
|
})
|
||||||
|
|
||||||
|
const formatTime = (t: number) => {
|
||||||
|
const min = Math.floor(t / 60)
|
||||||
|
const sec = Math.floor(t % 60)
|
||||||
|
return `${min}:${sec.toString().padStart(2, "0")}`
|
||||||
|
}
|
||||||
|
|
||||||
|
async function playNext() {
|
||||||
|
let songs, index
|
||||||
|
;[returnError, songs, index] = await triggerPlayNext(data.roomId)
|
||||||
|
|
||||||
|
if (returnError) return
|
||||||
|
|
||||||
|
queueSongs = songs
|
||||||
|
playingIndex = index
|
||||||
|
|
||||||
|
if (audioController) {
|
||||||
|
audioController.pause()
|
||||||
|
audioController.currentTime = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function seek(e: Event) {
|
||||||
|
const target = e.target as HTMLInputElement
|
||||||
|
const seekTime = parseFloat(target.value)
|
||||||
|
playerInfo.currentTime = seekTime
|
||||||
|
if (audioController) {
|
||||||
|
audioController.currentTime = seekTime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if returnError}
|
||||||
|
<Error {returnError} />
|
||||||
|
{:else}
|
||||||
|
<div class="flex w-full flex-col items-center justify-center p-4 lg:p-10">
|
||||||
|
<QueueSlider {queueSongs} {playingIndex} />
|
||||||
|
|
||||||
|
<audio autoplay bind:this={audioController} hidden onended={playNext}></audio>
|
||||||
|
|
||||||
|
<div class="flex w-full flex-col items-start justify-start gap-4 p-2 lg:w-[30vw]">
|
||||||
|
<p>{formatTime(playerInfo.currentTime)} - {formatTime(playerInfo.duration)}</p>
|
||||||
|
<input type="range" min="0" max={playerInfo.duration} step="0.1" value={playerInfo.currentTime} class="w-full accent-blue-500" oninput={seek} />
|
||||||
|
<div class="flex w-full flex-row items-center justify-center gap-6">
|
||||||
|
<button
|
||||||
|
class="rounded-md border border-dark-pine-muted p-2 hover:scale-105 active:scale-90 dark:bg-dark-pine-blue"
|
||||||
|
onclick={() => (playerInfo.playing ? audioController?.pause() : audioController?.play())}>{playerInfo.playing ? "Pause" : "Unpause"}</button
|
||||||
|
>
|
||||||
|
<button class="rounded-md border border-dark-pine-muted p-2 hover:scale-105 active:scale-90 dark:bg-dark-pine-blue" onclick={playNext}>Next</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<img class="absolute right-1 bottom-1" src="/api/room/qrcode?room=1000&pin=1234" />
|
||||||
|
<!-- @PERETTO fix here pls -->
|
||||||
|
</div>
|
||||||
|
{/if}
|
11
frontend/src/routes/admin/[id]/+page.ts
Normal file
|
@ -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")
|
||||||
|
}
|
|
@ -1,45 +1,88 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import QueueSlider from "$lib/components/QueueSlider.svelte"
|
import QueueSlider from "$lib/components/QueueSlider.svelte"
|
||||||
import SuggestionInput from "$lib/components/SuggestionInput.svelte"
|
import SuggestionInput from "$lib/components/SuggestionInput.svelte"
|
||||||
|
import Error from "$lib/components/Error.svelte"
|
||||||
|
import { type Suggestion, type Song, parseSong, parseSuggestion } from "$lib/types.js"
|
||||||
|
import { onDestroy, onMount } from "svelte"
|
||||||
import SuggestionList from "$lib/components/SuggestionList.svelte"
|
import SuggestionList from "$lib/components/SuggestionList.svelte"
|
||||||
|
import { getQueueSongs, getSuggestions, joinRoom } from "$lib/utils.js"
|
||||||
|
import type { FetchError } from "$lib/types.js"
|
||||||
|
import { io, Socket } from "socket.io-client"
|
||||||
|
import { get_coords } from "$lib/gps.js"
|
||||||
|
|
||||||
let songs = $state([
|
let { data } = $props()
|
||||||
{
|
|
||||||
name: "Cisco PT - Cantarex",
|
|
||||||
image: "https://s2.qwant.com/thumbr/474x474/5/9/bcbd0c0aeb1838f6916bf452c557251d7be985a13449e49fccb567a3374d4e/OIP.pmqEiKWv47zViDGgPgbbQAHaHa.jpg?u=https%3A%2F%2Ftse.mm.bing.net%2Fth%2Fid%2FOIP.pmqEiKWv47zViDGgPgbbQAHaHa%3Fr%3D0%26pid%3DApi&q=0&b=1&p=0&a=0",
|
|
||||||
points: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Io e i miei banchi - Paul Ham",
|
|
||||||
image: "https://i.prcdn.co/img?regionKey=RbtvKb5E1Cv4j1VWm2uGrw%3D%3D",
|
|
||||||
points: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Ragatthi - Bersatetthi",
|
|
||||||
image: "https://s2.qwant.com/thumbr/474x474/5/9/bcbd0c0aeb1838f6916bf452c557251d7be985a13449e49fccb567a3374d4e/OIP.pmqEiKWv47zViDGgPgbbQAHaHa.jpg?u=https%3A%2F%2Ftse.mm.bing.net%2Fth%2Fid%2FOIP.pmqEiKWv47zViDGgPgbbQAHaHa%3Fr%3D0%26pid%3DApi&q=0&b=1&p=0&a=0",
|
|
||||||
points: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Quarta",
|
|
||||||
image: "https://s2.qwant.com/thumbr/474x474/5/9/bcbd0c0aeb1838f6916bf452c557251d7be985a13449e49fccb567a3374d4e/OIP.pmqEiKWv47zViDGgPgbbQAHaHa.jpg?u=https%3A%2F%2Ftse.mm.bing.net%2Fth%2Fid%2FOIP.pmqEiKWv47zViDGgPgbbQAHaHa%3Fr%3D0%26pid%3DApi&q=0&b=1&p=0&a=0",
|
|
||||||
points: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Quinta",
|
|
||||||
image: "https://s2.qwant.com/thumbr/474x474/5/9/bcbd0c0aeb1838f6916bf452c557251d7be985a13449e49fccb567a3374d4e/OIP.pmqEiKWv47zViDGgPgbbQAHaHa.jpg?u=https%3A%2F%2Ftse.mm.bing.net%2Fth%2Fid%2FOIP.pmqEiKWv47zViDGgPgbbQAHaHa%3Fr%3D0%26pid%3DApi&q=0&b=1&p=0&a=0",
|
|
||||||
points: 0,
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|
||||||
let playing = $state(1)
|
let queueSongs = $state<Song[]>([])
|
||||||
|
let playingIndex = $state(0)
|
||||||
|
|
||||||
|
let suggestions = $state<Suggestion[]>([])
|
||||||
|
|
||||||
|
let returnError = $state<FetchError | null>()
|
||||||
|
|
||||||
|
let socket: Socket
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
// 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
|
||||||
|
;[returnError] = await joinRoom(data.roomId, coords, data.pin)
|
||||||
|
if (returnError) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
;[returnError, sugg] = await getSuggestions(data.roomId)
|
||||||
|
if (returnError) return
|
||||||
|
;[returnError, queue, index] = await getQueueSongs(data.roomId)
|
||||||
|
if (returnError) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
queueSongs = queue
|
||||||
|
suggestions = sugg
|
||||||
|
playingIndex = index
|
||||||
|
|
||||||
|
// Setup websocket connection
|
||||||
|
socket = io("/", { path: "/ws", transports: ["websocket"] })
|
||||||
|
await socket.emitWithAck("join_room", { id: data.roomId })
|
||||||
|
|
||||||
|
socket.on("queue_update", async (d) => {
|
||||||
|
const songs = await Promise.all(d.queue.map(parseSong))
|
||||||
|
queueSongs = songs
|
||||||
|
playingIndex = d.index
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.on("new_vote", async (d) => {
|
||||||
|
const updated = await parseSuggestion(d.song)
|
||||||
|
suggestions = suggestions.map((s) => (s.uuid === updated.uuid ? updated : s))
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.on("new_song", async (d) => {
|
||||||
|
const song = await parseSuggestion(d.song)
|
||||||
|
suggestions = [...suggestions, song]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
onDestroy(() => {
|
||||||
|
if (socket) socket.disconnect()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex w-full flex-col items-center justify-center p-4 lg:p-10">
|
<!-- Check if the room exists -->
|
||||||
<QueueSlider {songs} {playing} />
|
{#if returnError}
|
||||||
<div class="w-full py-6 lg:w-[30vw]">
|
<Error {returnError} />
|
||||||
<SuggestionInput />
|
{:else}
|
||||||
|
<div class="flex w-full flex-col items-center justify-center px-2 py-4 lg:p-10">
|
||||||
|
<QueueSlider {queueSongs} {playingIndex} />
|
||||||
|
<div class="w-full py-6 lg:w-[30vw]">
|
||||||
|
<SuggestionInput roomId={data.roomId} />
|
||||||
|
</div>
|
||||||
|
<div class="w-full py-6 lg:w-[30vw]">
|
||||||
|
<SuggestionList bind:suggestions roomId={data.roomId} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full py-6 lg:w-[30vw]">
|
{/if}
|
||||||
<SuggestionList suggestions={songs} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
8
frontend/src/routes/room/[id]/+page.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import type { PageLoad } from "./$types"
|
||||||
|
|
||||||
|
export const load: PageLoad = ({ params, url }) => {
|
||||||
|
return {
|
||||||
|
roomId: params.id || "",
|
||||||
|
pin: url.searchParams.get("pin") || "",
|
||||||
|
}
|
||||||
|
}
|
83
frontend/src/routes/room/create/+page.svelte
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { get_coords } from "$lib/gps"
|
||||||
|
import { X, Check, Plus } from "@lucide/svelte"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
|
let privateRoom: boolean = $state(true)
|
||||||
|
let coord = $state({ latitude: 0, longitude: 0 })
|
||||||
|
let creating: boolean = $state(false)
|
||||||
|
|
||||||
|
let name: string = $state()
|
||||||
|
let range: number = $state()
|
||||||
|
let pin: number = $state()
|
||||||
|
|
||||||
|
async function createRoom() {
|
||||||
|
if (creating) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
creating = true
|
||||||
|
|
||||||
|
const res = await fetch(
|
||||||
|
`/api/room/new?name=${encodeURIComponent(name)}&coords=${coord.latitude},${coord.longitude}&range=${encodeURIComponent(range ?? "100")}&pin=${encodeURIComponent(pin ?? "")}`,
|
||||||
|
{ method: "POST" }
|
||||||
|
)
|
||||||
|
|
||||||
|
const json = await res.json()
|
||||||
|
|
||||||
|
window.location.href = `/admin/${json.room_id}`
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
const res = await get_coords()
|
||||||
|
coord = res.coords ?? { latitude: 46.6769043, longitude: 11.1851585 }
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form class="flex flex-col items-center">
|
||||||
|
<h1 class="text-4xl my-10">Create Room</h1>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-3 w-1/2">
|
||||||
|
<input
|
||||||
|
bind:value={name}
|
||||||
|
placeholder="Room name (Required)"
|
||||||
|
class="{name
|
||||||
|
? ''
|
||||||
|
: 'border-2 border-red-500'} p-2 text-xl rounded-md bg-light-pine-overlay hover:bg-dark-pine-base/20 dark:bg-dark-pine-overlay hover:dark:bg-light-pine-base/20 duration-100 outline-none focus:ring-2"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<input
|
||||||
|
bind:value={range}
|
||||||
|
type="number"
|
||||||
|
min="10"
|
||||||
|
placeholder="Range (in meters)"
|
||||||
|
class="p-2 text-xl rounded-md border-dark-pine-muted bg-light-pine-overlay hover:bg-dark-pine-base/20 dark:bg-dark-pine-overlay hover:dark:bg-light-pine-base/20 duration-100 outline-none focus:ring-2"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Room Coordinates:
|
||||||
|
<span>{coord.latitude},{coord.longitude}</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<input
|
||||||
|
bind:value={pin}
|
||||||
|
type="number"
|
||||||
|
max="9999"
|
||||||
|
placeholder="PIN (none if public)"
|
||||||
|
class="p-2 text-xl rounded-md border-dark-pine-muted bg-light-pine-overlay hover:bg-dark-pine-base/20 dark:bg-dark-pine-overlay hover:dark:bg-light-pine-base/20 duration-100 outline-none focus:ring-2"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="cursor-pointer flex flex-row items-center justify-center gap-3 border border-dark-pine-muted bg-light-pine-overlay hover:bg-dark-pine-base/20 dark:bg-dark-pine-overlay hover:dark:bg-light-pine-base/20 duration-100 rounded-lg h-10 font-bold"
|
||||||
|
onclick={createRoom}
|
||||||
|
>
|
||||||
|
CREA
|
||||||
|
<Plus />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
17
frontend/src/routes/zesty/gps/+page.svelte
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
import { get_coords, type Coordinates } from "$lib/gps"
|
||||||
|
|
||||||
|
let lido_schenna_coords: Coordinates = { latitude: 46.6769043, longitude: 11.1851585 }
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
let { coords, error } = await get_coords()
|
||||||
|
console.log(coords)
|
||||||
|
if (error != null) {
|
||||||
|
return console.log(error)
|
||||||
|
}
|
||||||
|
if (coords == null) return
|
||||||
|
|
||||||
|
// console.log(is_within_range(coords, lido_schenna_coords, 103))
|
||||||
|
})
|
||||||
|
</script>
|
7
frontend/src/routes/zesty/icons/+page.svelte
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { Skull, Accessibility, Anvil } from "@lucide/svelte"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Skull />
|
||||||
|
<Accessibility />
|
||||||
|
<Anvil />
|
3
frontend/static/CHILLBOX.svg
Normal file
After Width: | Height: | Size: 5.2 KiB |
BIN
frontend/static/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
frontend/static/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 142 KiB |
BIN
frontend/static/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
frontend/static/favicon-16x16.png
Normal file
After Width: | Height: | Size: 768 B |
BIN
frontend/static/favicon-32x32.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
frontend/static/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>
|
|
Before Width: | Height: | Size: 1.5 KiB |
BIN
frontend/static/icon_small.png
Normal file
After Width: | Height: | Size: 17 KiB |
21
frontend/static/manifest.json
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"name": "Chillbox Music Player",
|
||||||
|
"short_name": "Chillbox",
|
||||||
|
"start_url": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#334155",
|
||||||
|
"theme_color": "#334155",
|
||||||
|
"orientation": "portrait-primary",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/android-chrome-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/android-chrome-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
BIN
frontend/static/radar.gif
Normal file
After Width: | Height: | Size: 420 KiB |
BIN
frontend/static/radar_bonus.gif
Normal file
After Width: | Height: | Size: 446 KiB |
|
@ -1,21 +1,22 @@
|
||||||
import tailwindcss from '@tailwindcss/vite';
|
import tailwindcss from "@tailwindcss/vite"
|
||||||
import { sveltekit } from '@sveltejs/kit/vite';
|
import { sveltekit } from "@sveltejs/kit/vite"
|
||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from "vite"
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [tailwindcss(), sveltekit()],
|
plugins: [tailwindcss(), sveltekit()],
|
||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
"/api": {
|
||||||
target: "http://backend:5000",
|
target: "http://backend:5000",
|
||||||
changeOrigin: false,
|
changeOrigin: false,
|
||||||
secure: false
|
secure: false,
|
||||||
},
|
},
|
||||||
'/ws': {
|
"/ws": {
|
||||||
target: "http://backend:5000",
|
target: "http://backend:5000",
|
||||||
changeOrigin: false,
|
changeOrigin: false,
|
||||||
secure: false
|
secure: false,
|
||||||
}
|
ws: true,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
},
|
||||||
|
})
|
||||||
|
|