diff --git a/README.md b/README.md index 84edd62..65f5fc8 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,3 @@ -# ChillBox +# team-1 -> *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. +Test diff --git a/SPEECH.md b/SPEECH.md deleted file mode 100644 index 2ab507f..0000000 --- a/SPEECH.md +++ /dev/null @@ -1,15 +0,0 @@ -# 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. - diff --git a/backend/Dockerfile b/backend/Dockerfile index 3390030..56023cb 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -2,16 +2,11 @@ FROM python:3.13.5-alpine WORKDIR /app -RUN apk update && apk add git ffmpeg - -RUN git clone --depth 1 'https://github.com/yt-dlp/yt-dlp.git' /yt-dlp - COPY requirements.txt ./ RUN pip install --no-cache-dir -r requirements.txt COPY . . EXPOSE 5000 -# CMD ["flask", "--app", "src.app", "run", "--debug", "--host=0.0.0.0"] -CMD ["python3", "src/app.py"] +CMD ["flask", "--app", "src.app", "run", "--debug", "--host=0.0.0.0"] # flask --app src.app run --host=0.0.0.0 --port=5001 --debug diff --git a/backend/requirements.txt b/backend/requirements.txt index cb18a70..4b83363 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,8 +1,2 @@ Flask==3.1.0 flask-cors -flask-socketio -dotenv -requests -qrcode -Pillow -eventlet>=0.33 diff --git a/backend/src/app.py b/backend/src/app.py index 28c66e9..2484d2c 100644 --- a/backend/src/app.py +++ b/backend/src/app.py @@ -1,43 +1,12 @@ -import uuid -from dataclasses import asdict - -import dotenv -from connect import get_connection from flask import Flask, Response, jsonify, request from flask_cors import CORS -from flask_socketio import SocketIO, join_room, leave_room -from gps import Coordinates, distance_between_coords -from qrcode_gen import generate_qr 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_fetch import query_search, yt_get_audio_url, yt_search_song -from state import State - -dotenv.load_dotenv() - app = Flask(__name__) -app.config["SECRET_KEY"] = "your_secret_key" -socketio = SocketIO(app, cors_allowed_origins="*", path="/ws") CORS(app) -db_conn = get_connection() -state = State(app, socketio, db_conn.cursor()) -init_db(state.db) - -state.rooms[1000] = Room( - id=1000, - coord=Coordinates(46.6769043, 11.1851585), - name="Lido Scena", - pin=1234, - tags=set(["chill", "raggaetton", "spanish", "latino", "mexican", "rock"]), - range_size=150, - songs={}, - history=[], - playing=[], - playing_idx=0, -) +ROOMS: dict[int, Room] = {} # { room_id: room, ... } def error(msg: str, status: int = 400) -> Response: @@ -46,267 +15,27 @@ def error(msg: str, status: int = 400) -> Response: return res -@socketio.on("connect") -def handle_connection(): - print("somebody connected to socket.io", flush=True) +@app.route("/api") +def index(): + return "hello from flask" -@socketio.on("disconnect") -def handle_disconnection(): - print("somebody disconnected from socket.io", flush=True) - - -@socketio.on("join_room") -def on_join(data): - room = data["id"] - join_room(room) - print(f"somebody joined {room=}", flush=True) - - -@socketio.on("leave_room") -def on_leave(data): - room = data["id"] - leave_room(room) - print(f"somebody left {room=}", flush=True) - - -@app.get("/api/join") +@app.route("/api/join") def join(): room_id = request.args.get("room") - code = request.args.get("pin") + code = request.args.get("code") if room_id is None: return error("Missing room id") - if (room := state.rooms.get(int(room_id))) is None: + if (room := ROOMS.get(int(room_id))) is None: return error("Invalid room") - if room.pin is not None: - 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") + if room.pin is not None and room.pin != code: + return error("Invalid code") return {"success": True, "ws": f"/ws/{room_id}"} -@app.get("/api/queue") -def queue(): - 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") - - return {"success": True, "queue": room.playing, "index": room.playing_idx} - - -@app.post("/api/queue/next") -def queue_next(): - 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") - - room.playing_idx += 1 - - if room.playing_idx >= len(room.playing): - ## queue ended - room.renew_queue() - ended = True - else: - ended = False - - data = {"success": True, "ended": ended, "index": room.playing_idx, "queue": [asdict(s) for s in room.playing]} - state.socketio.emit("queue_update", data, to=str(room.id)) - - return data - - -@app.post("/api/room/new") -def room_new(): - if (room_name := request.args.get("name")) is None: - return error("Missing room name") - - if (room_cords := request.args.get("coords")) is None: - 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"): - room_pin = int(room_pin) - else: - room_pin = None - - lat, lon = room_cords.split(",") - - room = Room( - id=max(state.rooms or [0]) + 1, - coord=Coordinates(float(lat), float(lon)), - range_size=int(room_range), - name=room_name, - pin=room_pin, - tags=set([tag for tag in request.args.get("tags", "").split(",") if tag]), - songs={}, - history=[], - playing=[], - playing_idx=-1, - ) - - state.rooms[room.id] = room - - return {"success": True, "room_id": room.id} - - -@app.get("/api/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 [ - { - "id": room.id, - "name": room.name, - "private": room.pin is not None, - "coords": room.coord, - "range": room.range_size, - "distance": d, - } - for room in state.rooms.values() - if (d := distance_between_coords(user_coords, room.coord)) <= room.range_size - ] - - -@app.post("/api/addsong") -def add_song(): - 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") - - if (query := request.args.get("query")) is None: - return error("Missing 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: - ## 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 - 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, - ) - - add_song_in_db(song) - - 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") -def room_qrcode(): - if (room_id := request.args.get("room")) is None: - return error("Missing room id") - - if (pin := request.args.get("pin")) is not None: - pin = int(pin) - - stream = generate_qr( - base_uri="https://chillbox.leoinvents.com", - room_id=int(room_id), - pin=pin, - ) - - return Response(stream, content_type="image/jpeg") - - -@app.get("/api/song/audio") -def get_audio_url(): - if (song_uuid := request.args.get("song")) is None: - return error("Missing song id") - - if (song := get_song_by_uuid(song_uuid)) is None: - return error("Song not found") - - if (url := yt_get_audio_url(song.youtube_id)) is None: - return error("Cannot get audio url") - - return {"success": True, "url": url} - - if __name__ == "__main__": - socketio.run(app, host="0.0.0.0", port=5000, debug=True) + app.run(debug=True) diff --git a/backend/src/connect.py b/backend/src/connect.py index c07af1b..12e53d8 100644 --- a/backend/src/connect.py +++ b/backend/src/connect.py @@ -2,7 +2,5 @@ import sqlite3 def get_connection(): - conn = sqlite3.connect(".data/jukebox.db") - conn.row_factory = sqlite3.Row - conn.autocommit = True + conn = sqlite3.connect("jukebox.db") return conn diff --git a/backend/src/gps.py b/backend/src/gps.py deleted file mode 100644 index 98bda0c..0000000 --- a/backend/src/gps.py +++ /dev/null @@ -1,29 +0,0 @@ -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 diff --git a/backend/src/qrcode_gen.py b/backend/src/qrcode_gen.py deleted file mode 100644 index e4caffe..0000000 --- a/backend/src/qrcode_gen.py +++ /dev/null @@ -1,32 +0,0 @@ -import qrcode -import urllib.parse -from io import BytesIO - - -def create_login_url(base_uri: str, room_id: int, pin: int | None) -> str: - parsed = urllib.parse.urlparse(base_uri) - - params = { - "room": room_id, - } - - if pin is not None: - params["pin"] = pin - - parsed = parsed._replace(path="join", query=urllib.parse.urlencode(params)) - - return urllib.parse.urlunparse(parsed) - - -def generate_qr(base_uri: str, room_id: int, pin: int | None) -> BytesIO: - url = create_login_url(base_uri, room_id, pin) - - qr = qrcode.make(url) - - out = BytesIO() - - qr.save(out, format="jpeg") - - out.seek(0) - - return out diff --git a/backend/src/room.py b/backend/src/room.py index cb775b6..bdf4026 100644 --- a/backend/src/room.py +++ b/backend/src/room.py @@ -1,146 +1,18 @@ -import random from dataclasses import dataclass -from gps import Coordinates from song import Song -USER_SCORE_WEIGHT = 0.7 -ARTIST_WEIGHT = 0.1 -TAG_WEIGHT = 0.1 -RANDOM_WEIGHT = 0.1 -RECENT_PENALTY = 0.5 -RECENT_COUNT = 10 -QUEUE_SIZE = 3 - - type UserScoredSong = tuple[Song, int] -@dataclass -class Rank: - user_score: float - tag: float - artist: float - random: float - recent_penalty: float - - def total(self) -> float: - return self.user_score + self.tag + self.artist + self.random - self.recent_penalty - - @dataclass class Room: id: int - coord: Coordinates + coord: tuple[float, float] name: str pin: int | None tags: set[str] - range_size: int # in meters ?? + creative: bool - songs: dict[str, UserScoredSong] # all songs + user score (the playlist) - history: list[Song] # all songs previously played - - ## playing queue info - playing: list[Song] - playing_idx: int - - def renew_queue(self): - for song in self.playing: - self.history.append(song) - self.playing.clear() - self.playing_idx = 0 - - rankings: dict[str, float] = {} - for id, (song, user_score) in self.songs.items(): - rankings[id] = self.rank_song(song, user_score).total() - - # sort dict items by their values and pick the highest 3 - top_items = sorted(rankings.items(), key=lambda item: item[1], reverse=True)[:QUEUE_SIZE] # [:3] - self.playing = list(map(lambda x: self.songs[x[0]][0], top_items)) # remove the ranking and take only the songs - - def rank_song_from_id(self, id: str) -> Rank: - scored = self.songs[id] - return self.rank_song(scored[0], scored[1]) - - def rank_song(self, song: Song, user_score: int) -> Rank: - song_items = self.songs.items() - - lowest_score = min(song_items, key=lambda item: item[1][1])[1][1] - highest_score = max(song_items, key=lambda item: item[1][1])[1][1] - - score_rank = translate(user_score, lowest_score, highest_score, 0.0, USER_SCORE_WEIGHT) - - recent_songs = self.history[-RECENT_COUNT:] - - tag_counts = {} - artist_counts = {} - for recent_song in recent_songs: - for tag in recent_song.tags: - if tag not in tag_counts: - tag_counts[tag] = 0 - tag_counts[tag] += 1 - if recent_song.artist not in artist_counts: - artist_counts[recent_song.artist] = 0 - artist_counts[recent_song.artist] += 1 - - tag_total = 0 - for tag in song.tags: - if tag in tag_counts: - tag_total += tag_counts[tag] - - artist_total = artist_counts[song.artist] if song.artist in artist_counts else 0 - - tag_value = min(RECENT_COUNT, len(self.history)) - tag_total - artist_value = min(RECENT_COUNT, len(self.history)) - artist_total - - tag_rank = translate(tag_value, 0, min(RECENT_COUNT, len(self.history)), 0, TAG_WEIGHT) - artist_rank = translate(artist_value, 0, min(RECENT_COUNT, len(self.history)), 0, ARTIST_WEIGHT) - - random_rank = translate(random.random(), 0, 1, 0.0, RANDOM_WEIGHT) - - recent_penalty = RECENT_PENALTY if song in recent_songs else 0 - - return Rank(score_rank, tag_rank, artist_rank, random_rank, recent_penalty) - - -def translate(value: float, in_min: float, in_max: float, out_min: float, out_max: float): - if in_max == in_min: - return out_min - return (value - in_min) / (in_max - in_min) * (out_max - out_min) + out_min - - -def test_algo(): - songs = [ - Song("paulham", "Io e i miei banchi", "Paul Ham", ["pop"], "", ""), - Song("cisco", "CiscoPT", "Cantarex", ["rap"], "", ""), - Song("vpn", "VPN", "Cantarex", ["rap"], "", ""), - Song("gang", "Gang Gang Gang", "Cantarex", ["rap"], "", ""), - Song("bertha1", "Rindondantha", "Berthanetti", ["rock"], "", ""), - Song("bertha2", "Ragatthi", "Berthanetti", ["rock"], "", ""), - Song("bertha3", "Tranthathione", "Berthanetti", ["rock"], "", ""), - Song("cexx", "Spritz", "Cex", ["kpop"], "", ""), - ] - - room = Room( - 123, - (0.0, 0.0), - "test", - None, - set(["rock", "rap"]), - 100, - { - "paulham": (songs[0], 7), - "cisco": (songs[1], 5), - "vpn": (songs[2], 11), - "gang": (songs[3], 10), - "bertha1": (songs[4], 4), - "bertha2": (songs[5], 5), - "bertha3": (songs[6], -4), - "cexx": (songs[7], 12), - }, - [], - [songs[2], songs[0], songs[1]], - 1, - ) - room.renew_queue() - print(room.playing) + playlist: set[UserScoredSong] + history: list[Song] diff --git a/backend/src/song.py b/backend/src/song.py index f95609b..2260838 100644 --- a/backend/src/song.py +++ b/backend/src/song.py @@ -1,13 +1,13 @@ from dataclasses import dataclass -from sqlite3 import Cursor - from connect import get_connection -def init_db(db: Cursor): - db.execute(""" +def init_db(): + conn = get_connection() + cursor = conn.cursor() + cursor.execute(""" CREATE TABLE IF NOT EXISTS songs ( - uuid TEXT PRIMARY KEY, + mbid TEXT PRIMARY KEY, title TEXT NOT NULL, artist TEXT NOT NULL, tags TEXT NOT NULL, @@ -15,57 +15,15 @@ def init_db(db: Cursor): youtube_id TEXT NOT NULL ); """) + conn.commit() + conn.close() -@dataclass(frozen=True, slots=True, kw_only=True) +@dataclass class Song: - uuid: str + mbid: str title: str artist: str tags: list[str] image_id: str youtube_id: str - - -def get_song_by_uuid(uuid: str) -> Song | None: - conn = get_connection() - cursor = conn.cursor() - cursor.execute("SELECT * FROM songs WHERE uuid = ?", (uuid,)) - row = cursor.fetchone() - conn.close() - - if row is None: - return None - - 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 - - -def get_song_by_title_artist(title: str, artist: str) -> Song | None: - conn = get_connection() - cursor = conn.cursor() - cursor.execute("SELECT * FROM songs WHERE title = ? AND artist = ?", (title, artist)) - row = cursor.fetchone() - conn.close() - - if row is None: - return None - - 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 - - -def add_song_in_db(song: Song) -> None: - conn = get_connection() - cursor = conn.cursor() - - cursor.execute( - """ - INSERT OR REPLACE INTO songs (uuid, title, artist, tags, lastfm_image_id, youtube_id) - VALUES (?, ?, ?, ?, ?, ?) - """, - (song.uuid, song.title, song.artist, ",".join(song.tags), song.image_id, song.youtube_id), - ) # Updates song info if it already exists - - conn.commit() - conn.close() diff --git a/backend/src/song_fetch.py b/backend/src/song_fetch.py deleted file mode 100644 index 905d126..0000000 --- a/backend/src/song_fetch.py +++ /dev/null @@ -1,127 +0,0 @@ -import os -import os.path -import sys -import urllib.parse -from dataclasses import dataclass - -import requests - -sys.path.append("/yt-dlp") -import yt_dlp - - -@dataclass -class SongInfo: - artist: str - title: str - img_id: str - tags: list[str] - - -def _lastfm_search(query: str) -> tuple[str, str] | None: - response = requests.get( - url="https://ws.audioscrobbler.com/2.0/?method=track.search&format=json", - params={"limit": 5, "track": query, "api_key": os.environ["LASTFM_API_KEY"]}, - ) - - assert response.status_code == 200 - - tracks = response.json()["results"]["trackmatches"]["track"] - if len(tracks) == 0: - return None - track_info = tracks[0] - - return track_info["name"], track_info["artist"] - - -def _lastfm_getinfo(name: str, artist: str) -> tuple[str, list[str]]: # ( image_id, tags ) - response = requests.get( - url="https://ws.audioscrobbler.com/2.0/?method=track.getInfo&format=json", - params={ - "track": name, - "artist": artist, - "api_key": os.environ["LASTFM_API_KEY"], - }, - ) - - track_info = response.json()["track"] - - 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 ( - # track_info["mbid"], - image_id, - [t["name"] for t in track_info["toptags"]["tag"]], - ) - - -def _yt_search(query: str) -> tuple[str, str]: - 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) - - return SongInfo(artist=artist, title=name, img_id=img_id, tags=tags) - - -def yt_search_song(name: str, artist: str) -> str | None: # video id - 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(f"{name!r} - {artist!r}", download=False) - - if len(info["entries"]) == 0: - return None - - 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"]: - if "acodec" in fmt and fmt["acodec"] != "none": - return fmt["url"] - - return None diff --git a/backend/src/state.py b/backend/src/state.py deleted file mode 100644 index 73d05d2..0000000 --- a/backend/src/state.py +++ /dev/null @@ -1,15 +0,0 @@ -from dataclasses import dataclass, field -from sqlite3 import Cursor - -from flask import Flask -from flask_socketio import SocketIO - -from room import Room - - -@dataclass -class State: - app: Flask - socketio: SocketIO - db: Cursor - rooms: dict[int, Room] = field(default_factory=dict) # { room_id: room, ... } diff --git a/docker-compose.yml b/docker-compose.yml index 6e6c7ec..ea7e2ca 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,20 +1,18 @@ services: - backend: - build: ./backend - ports: - - 5001:5000 - environment: - NODE_ENV: development - volumes: - - ./backend:/app - - ./.data:/app/.data - env_file: - - .env + backend: + build: ./backend + ports: + - 5001:5000 + environment: + NODE_ENV: development + volumes: + - ./backend:/app + - ./.data/jukebox.sqlite3:/app/.data/jukebox.sqlite3 - frontend: - build: ./frontend - ports: - - 5173:5173 - volumes: - - ./frontend:/app - - /app/node_modules + frontend: + build: ./frontend + ports: + - 5173:5173 + volumes: + - ./frontend:/app + - /app/node_modules diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a620e77..b3ecdec 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -7,11 +7,6 @@ "": { "name": "frontend", "version": "0.0.1", - "dependencies": { - "@lucide/svelte": "^0.536.0", - "socket.io-client": "^4.8.1", - "zod": "^4.0.14" - }, "devDependencies": { "@sveltejs/adapter-auto": "^6.0.0", "@sveltejs/kit": "^2.22.0", @@ -31,6 +26,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, "license": "Apache-2.0", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -499,6 +495,7 @@ "version": "0.3.12", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -509,6 +506,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -518,27 +516,20 @@ "version": "1.5.4", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.29", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@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": { "version": "1.0.0-next.29", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", @@ -826,16 +817,11 @@ "win32" ] }, - "node_modules/@socket.io/component-emitter": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", - "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", - "license": "MIT" - }, "node_modules/@sveltejs/acorn-typescript": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.5.tgz", "integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==", + "dev": true, "license": "MIT", "peerDependencies": { "acorn": "^8.9.0" @@ -1211,12 +1197,14 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, "license": "MIT" }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -1229,6 +1217,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, "license": "Apache-2.0", "engines": { "node": ">= 0.4" @@ -1238,6 +1227,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, "license": "Apache-2.0", "engines": { "node": ">= 0.4" @@ -1273,6 +1263,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -1333,45 +1324,6 @@ "dev": true, "license": "MIT" }, - "node_modules/engine.io-client": { - "version": "6.6.3", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", - "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.17.1", - "xmlhttprequest-ssl": "~2.1.1" - } - }, - "node_modules/engine.io-client/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/engine.io-parser": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", - "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/enhanced-resolve": { "version": "5.18.2", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz", @@ -1432,12 +1384,14 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "dev": true, "license": "MIT" }, "node_modules/esrap": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.0.tgz", "integrity": "sha512-yzmPNpl7TBbMRC5Lj2JlJZNPml0tzqoqP5B1JXycNUwtqma9AKCO0M2wHrdgsHcy1WRW7S9rJknAMtByg3usgA==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" @@ -1484,6 +1438,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "dev": true, "license": "MIT", "dependencies": { "@types/estree": "^1.0.6" @@ -1752,12 +1707,14 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "dev": true, "license": "MIT" }, "node_modules/magic-string": { "version": "0.30.17", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" @@ -1826,6 +1783,7 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -2099,68 +2057,6 @@ "node": ">=18" } }, - "node_modules/socket.io-client": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", - "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.2", - "engine.io-client": "~6.6.1", - "socket.io-parser": "~4.2.4" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/socket.io-client/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-parser": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", - "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/socket.io-parser/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": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -2175,6 +2071,7 @@ "version": "5.37.1", "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.37.1.tgz", "integrity": "sha512-h8arWpQZ+3z8eahyBT5KkiBOUsG6xvI5Ykg0ozRr9xEdImgSMUPUlOFWRNkUsT7Ti0DSUCTEbPoped0aoxFyWA==", + "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.3.0", @@ -2391,35 +2288,6 @@ } } }, - "node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/xmlhttprequest-ssl": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", - "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/yallist": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", @@ -2434,16 +2302,8 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz", "integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==", + "dev": true, "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" - } } } } diff --git a/frontend/package.json b/frontend/package.json index f639403..cee9931 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -26,10 +26,5 @@ "tailwindcss": "^4.0.0", "typescript": "^5.0.0", "vite": "^7.0.4" - }, - "dependencies": { - "@lucide/svelte": "^0.536.0", - "socket.io-client": "^4.8.1", - "zod": "^4.0.14" } } diff --git a/frontend/src/app.css b/frontend/src/app.css index cb4fb29..d4b5078 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -1,66 +1 @@ -@import url('https://fonts.googleapis.com/css2?family=Lilita+One&display=swap'); @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%); -} diff --git a/frontend/src/app.html b/frontend/src/app.html index 0e6ef01..7af2f6d 100644 --- a/frontend/src/app.html +++ b/frontend/src/app.html @@ -1,13 +1,15 @@ -
- - - - - %sveltekit.head% - - -{returnError.message}
-- {errorMsg} -
diff --git a/frontend/src/lib/components/SuggestionList.svelte b/frontend/src/lib/components/SuggestionList.svelte deleted file mode 100644 index 6c667a3..0000000 --- a/frontend/src/lib/components/SuggestionList.svelte +++ /dev/null @@ -1,64 +0,0 @@ - - -No suggestions yet! Try to add a new one using the Add button
- {/if} - - {#each suggestions as sug} - - {/each} -{text}
diff --git a/frontend/src/routes/admin/[id]/+page.svelte b/frontend/src/routes/admin/[id]/+page.svelte deleted file mode 100644 index 02f5f08..0000000 --- a/frontend/src/routes/admin/[id]/+page.svelte +++ /dev/null @@ -1,118 +0,0 @@ - - -{#if returnError} -{formatTime(playerInfo.currentTime)} - {formatTime(playerInfo.duration)}
- -