From fcdc9cfaed9a185831c769f258d97a3915722a9c Mon Sep 17 00:00:00 2001 From: Leonardo Segala Date: Fri, 1 Aug 2025 18:23:27 +0200 Subject: [PATCH 01/12] Add initial room classes & join API --- backend/src/app.py | 30 +++++++++++++++++++++++++++++- backend/src/classes.py | 9 +++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 backend/src/classes.py diff --git a/backend/src/app.py b/backend/src/app.py index cd551c2..8602d86 100644 --- a/backend/src/app.py +++ b/backend/src/app.py @@ -1,14 +1,42 @@ -from flask import Flask +from flask import Flask, request, Response, jsonify from flask_cors import CORS +from .classes import Room + app = Flask(__name__) CORS(app) +ROOMS: dict[int, Room] = {} # { room_id: room, ... } + + +def error(msg: str, status: int = 400) -> Response: + res = jsonify({"success": False, "error": msg}) + res.status_code = status + return res + + @app.route("/api") def index(): return "hello from flask" +@app.route("/api/join") +def join(): + room_id = request.args.get("room") + code = request.args.get("code") + + if room_id is None: + return error("Missing room id") + + if (room := ROOMS.get(int(room_id))) is None: + return error("Invalid room") + + if room.pin is not None and room.pin != code: + return error("Invalid code") + + return {"success": True, "ws": f"/ws/{room_id}"} + + if __name__ == "__main__": app.run(debug=True) diff --git a/backend/src/classes.py b/backend/src/classes.py new file mode 100644 index 0000000..c607883 --- /dev/null +++ b/backend/src/classes.py @@ -0,0 +1,9 @@ +from dataclasses import dataclass + + +@dataclass +class Room: + id: int + coord: ... + name: str + pin: int | None From f0defdd91871b2755d9796da137c21373567cecc Mon Sep 17 00:00:00 2001 From: Simone Tesini Date: Fri, 1 Aug 2025 18:46:38 +0200 Subject: [PATCH 02/12] add classes --- backend/src/app.py | 5 ++--- backend/src/classes.py | 9 --------- backend/src/room.py | 18 ++++++++++++++++++ backend/src/song.py | 11 +++++++++++ 4 files changed, 31 insertions(+), 12 deletions(-) delete mode 100644 backend/src/classes.py create mode 100644 backend/src/room.py create mode 100644 backend/src/song.py diff --git a/backend/src/app.py b/backend/src/app.py index 8602d86..2484d2c 100644 --- a/backend/src/app.py +++ b/backend/src/app.py @@ -1,7 +1,6 @@ -from flask import Flask, request, Response, jsonify +from flask import Flask, Response, jsonify, request from flask_cors import CORS - -from .classes import Room +from room import Room app = Flask(__name__) CORS(app) diff --git a/backend/src/classes.py b/backend/src/classes.py deleted file mode 100644 index c607883..0000000 --- a/backend/src/classes.py +++ /dev/null @@ -1,9 +0,0 @@ -from dataclasses import dataclass - - -@dataclass -class Room: - id: int - coord: ... - name: str - pin: int | None diff --git a/backend/src/room.py b/backend/src/room.py new file mode 100644 index 0000000..bdf4026 --- /dev/null +++ b/backend/src/room.py @@ -0,0 +1,18 @@ +from dataclasses import dataclass + +from song import Song + +type UserScoredSong = tuple[Song, int] + + +@dataclass +class Room: + id: int + coord: tuple[float, float] + name: str + pin: int | None + tags: set[str] + creative: bool + + playlist: set[UserScoredSong] + history: list[Song] diff --git a/backend/src/song.py b/backend/src/song.py new file mode 100644 index 0000000..5ad3115 --- /dev/null +++ b/backend/src/song.py @@ -0,0 +1,11 @@ +from dataclasses import dataclass + + +@dataclass +class Song: + mbid: str + title: str + artist: str + tags: list[str] + image_id: str + youtube_id: str From b0be25aafb41d9274f017d845312bb5d7e04869b Mon Sep 17 00:00:00 2001 From: panzerotto Date: Fri, 1 Aug 2025 19:02:50 +0200 Subject: [PATCH 03/12] add db --- backend/src/connect.py | 6 ++++++ backend/src/song.py | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 backend/src/connect.py diff --git a/backend/src/connect.py b/backend/src/connect.py new file mode 100644 index 0000000..12e53d8 --- /dev/null +++ b/backend/src/connect.py @@ -0,0 +1,6 @@ +import sqlite3 + + +def get_connection(): + conn = sqlite3.connect("jukebox.db") + return conn diff --git a/backend/src/song.py b/backend/src/song.py index 5ad3115..2260838 100644 --- a/backend/src/song.py +++ b/backend/src/song.py @@ -1,4 +1,22 @@ from dataclasses import dataclass +from connect import get_connection + + +def init_db(): + conn = get_connection() + cursor = conn.cursor() + cursor.execute(""" + CREATE TABLE IF NOT EXISTS songs ( + mbid TEXT PRIMARY KEY, + title TEXT NOT NULL, + artist TEXT NOT NULL, + tags TEXT NOT NULL, + lastfm_image_id TEXT NOT NULL, + youtube_id TEXT NOT NULL + ); + """) + conn.commit() + conn.close() @dataclass From a0e1662e1de0229b6b2359dd800945d3edfe026b Mon Sep 17 00:00:00 2001 From: Simone Tesini Date: Fri, 1 Aug 2025 19:13:09 +0200 Subject: [PATCH 04/12] pull --- backend/src/app.py | 2 +- backend/src/room.py | 33 +++++++++++++++++++++++++++++++-- ruff.toml | 1 + 3 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 ruff.toml diff --git a/backend/src/app.py b/backend/src/app.py index 2484d2c..39ff440 100644 --- a/backend/src/app.py +++ b/backend/src/app.py @@ -1,6 +1,6 @@ from flask import Flask, Response, jsonify, request from flask_cors import CORS -from room import Room +from src.room import Room app = Flask(__name__) CORS(app) diff --git a/backend/src/room.py b/backend/src/room.py index bdf4026..a9ddd7c 100644 --- a/backend/src/room.py +++ b/backend/src/room.py @@ -2,7 +2,16 @@ from dataclasses import dataclass from song import Song -type UserScoredSong = tuple[Song, int] +USER_SCORE_WEIGHT = 0.7 +GENRE_WEIGHT = 0.1 +RANDOM_WEIGHT = 0.1 +RECENT_PENALTY = 0.5 +RECENT_COUNT = 10 + + +@dataclass +class UserScoredSong(Song): + user_score: int @dataclass @@ -14,5 +23,25 @@ class Room: tags: set[str] creative: bool - playlist: set[UserScoredSong] + songs: dict[str, UserScoredSong] history: list[Song] + + def rank_song( + self, + song: UserScoredSong, + ) -> float: + rank = 0.0 + song_items = self.songs.items() + + lowest_score = min(song_items, key=lambda item: item[1].user_score)[1].user_score + highest_score = min(song_items, key=lambda item: item[1].user_score)[1].user_score + + rank += translate(song.user_score, lowest_score, highest_score, 0.0, USER_SCORE_WEIGHT) + + last10items = self.history[-10:] + + return rank + + +def translate(value: float, in_min: float, in_max: float, out_min: float, out_max: float): + return (value - in_min) / (in_max - in_min) * (out_max - out_min) + out_min diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..d461c74 --- /dev/null +++ b/ruff.toml @@ -0,0 +1 @@ +line-length = 200 From adce5d7c65e745014002a6dfdf023decb0cd6e81 Mon Sep 17 00:00:00 2001 From: Leonardo Segala Date: Fri, 1 Aug 2025 19:13:12 +0200 Subject: [PATCH 05/12] ops --- backend/src/app.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/backend/src/app.py b/backend/src/app.py index 8602d86..41ecd9d 100644 --- a/backend/src/app.py +++ b/backend/src/app.py @@ -1,7 +1,11 @@ from flask import Flask, request, Response, jsonify from flask_cors import CORS +import dotenv from .classes import Room +# from . import song_fetch + +dotenv.load_dotenv() app = Flask(__name__) CORS(app) From 4bb6254396e60f3aa2c22f36699e8a43777cb280 Mon Sep 17 00:00:00 2001 From: Simone Tesini Date: Fri, 1 Aug 2025 19:14:07 +0200 Subject: [PATCH 06/12] add basic ranking algo --- backend/src/room.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/room.py b/backend/src/room.py index a9ddd7c..4c405c0 100644 --- a/backend/src/room.py +++ b/backend/src/room.py @@ -38,7 +38,7 @@ class Room: rank += translate(song.user_score, lowest_score, highest_score, 0.0, USER_SCORE_WEIGHT) - last10items = self.history[-10:] + # last10items = self.history[-10:] return rank From 7f6a36439f54cc2be580a81b65b0206743807398 Mon Sep 17 00:00:00 2001 From: panzerotto Date: Fri, 1 Aug 2025 19:14:10 +0200 Subject: [PATCH 07/12] add db creation --- backend/src/app.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/src/app.py b/backend/src/app.py index 2484d2c..1326446 100644 --- a/backend/src/app.py +++ b/backend/src/app.py @@ -1,6 +1,7 @@ from flask import Flask, Response, jsonify, request from flask_cors import CORS from room import Room +from song import init_db app = Flask(__name__) CORS(app) @@ -38,4 +39,5 @@ def join(): if __name__ == "__main__": + init_db() app.run(debug=True) From 8bb582148df4a928e665ccd928468969bf3c2818 Mon Sep 17 00:00:00 2001 From: Leonardo Segala Date: Fri, 1 Aug 2025 19:16:20 +0200 Subject: [PATCH 08/12] Add song fetch test --- backend/Dockerfile | 4 +++ backend/requirements.txt | 2 ++ backend/src/app.py | 12 ++++---- backend/src/room.py | 2 +- backend/src/song.py | 2 +- backend/src/song_fetch.py | 63 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 76 insertions(+), 9 deletions(-) create mode 100644 backend/src/song_fetch.py diff --git a/backend/Dockerfile b/backend/Dockerfile index 56023cb..dc8f66f 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -2,6 +2,10 @@ FROM python:3.13.5-alpine WORKDIR /app +RUN apk update && apk add git + +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 diff --git a/backend/requirements.txt b/backend/requirements.txt index 4b83363..7623d9f 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,2 +1,4 @@ Flask==3.1.0 flask-cors +dotenv +requests \ No newline at end of file diff --git a/backend/src/app.py b/backend/src/app.py index 665603a..3fadaf6 100644 --- a/backend/src/app.py +++ b/backend/src/app.py @@ -1,15 +1,13 @@ from flask import Flask, Response, jsonify, request from flask_cors import CORS -<<<<<<< HEAD -import dotenv -from .classes import Room -# from . import song_fetch +import dotenv +from .room import Room + +from . import song_fetch dotenv.load_dotenv() -======= -from room import Room ->>>>>>> refs/remotes/origin/main + app = Flask(__name__) CORS(app) diff --git a/backend/src/room.py b/backend/src/room.py index bdf4026..996a5a1 100644 --- a/backend/src/room.py +++ b/backend/src/room.py @@ -1,6 +1,6 @@ from dataclasses import dataclass -from song import Song +from .song import Song type UserScoredSong = tuple[Song, int] diff --git a/backend/src/song.py b/backend/src/song.py index 2260838..8570033 100644 --- a/backend/src/song.py +++ b/backend/src/song.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from connect import get_connection +from .connect import get_connection def init_db(): diff --git a/backend/src/song_fetch.py b/backend/src/song_fetch.py new file mode 100644 index 0000000..96288e9 --- /dev/null +++ b/backend/src/song_fetch.py @@ -0,0 +1,63 @@ +import requests +import urllib.parse +import os.path +import os +import sys + +sys.path.append("/yt-dlp") +import yt_dlp + + +def lastfm_search(query: str) -> tuple[str, str]: + 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 + + track_info = response.json()["results"]["trackmatches"]["track"][0] + + return track_info["name"], track_info["artist"] + + +def lastfm_getinfo( + name: str, artist: str +) -> tuple[str, str, str, str, list[str]]: # ( id, 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_url = urllib.parse.urlparse(track_info["album"]["image"][0]["#text"]) + + return ( + track_info["mbid"], + [t["name"] for t in track_info["toptags"]["tag"]], + os.path.splitext(os.path.basename(image_url.path))[0], + ) + + +print(yt_dlp, flush=True) + +# # def get_yt_mp3link(name: str, artist: str) -> str: ... +# # os.popen("/yt-dlp ") + +# # /yt-dlp/yt-dlp.sh "ytsearch1:Never gonna give you up" --get-url -f "ba" + +# import json + +# print(json.dumps(lastfm_getinfo(*lastfm_search("money")), indent=2)) +# exit(1) + + +# # def + + +# ## query ==> lastfm ==> list of songs ==> take first ==> request song info ==> get YT link ==> save in DB ==> From 036b64b51efc9188ec7bee86c825e6840c45ba97 Mon Sep 17 00:00:00 2001 From: Simone Tesini Date: Fri, 1 Aug 2025 19:31:47 +0200 Subject: [PATCH 09/12] theoretically finish the algorithm --- backend/src/room.py | 42 ++++++++++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/backend/src/room.py b/backend/src/room.py index 92b0ee6..6332fd0 100644 --- a/backend/src/room.py +++ b/backend/src/room.py @@ -3,15 +3,14 @@ from dataclasses import dataclass from .song import Song USER_SCORE_WEIGHT = 0.7 -GENRE_WEIGHT = 0.1 +ARTIST_WEIGHT = 0.1 +TAG_WEIGHT = 0.1 RANDOM_WEIGHT = 0.1 RECENT_PENALTY = 0.5 RECENT_COUNT = 10 -@dataclass -class UserScoredSong(Song): - user_score: int +type UserScoredSong = tuple[Song, int] @dataclass @@ -26,19 +25,38 @@ class Room: songs: dict[str, UserScoredSong] history: list[Song] - def rank_song( - self, - song: UserScoredSong, - ) -> float: + def rank_song(self, song: Song, user_score: int) -> float: rank = 0.0 song_items = self.songs.items() - lowest_score = min(song_items, key=lambda item: item[1].user_score)[1].user_score - highest_score = min(song_items, key=lambda item: item[1].user_score)[1].user_score + lowest_score = min(song_items, key=lambda item: item[1][1])[1][1] + highest_score = min(song_items, key=lambda item: item[1][1])[1][1] - rank += translate(song.user_score, lowest_score, highest_score, 0.0, USER_SCORE_WEIGHT) + rank += translate(user_score, lowest_score, highest_score, 0.0, USER_SCORE_WEIGHT) - # last10items = self.history[-10:] + recent_songs = self.history[-RECENT_COUNT:] + + tag_counts = {} + artist_counts = {} + for song in recent_songs: + for tag in song.tags: + if tag not in tag_counts: + tag_counts[tag] = 0 + tag_counts[tag] += 1 + if song.artist not in artist_counts: + artist_counts[song.artist] = 0 + artist_counts[song.artist] += 1 + + tag_total = 0 + for tag in song.tags: + if tag in tag_counts: + tag_total += tag_counts[tag] + + rank += translate(tag_total, 0, RECENT_COUNT, 0, TAG_WEIGHT) + rank += translate(artist_counts[song.artist], 0, RECENT_COUNT, 0, ARTIST_WEIGHT) + + if song in recent_songs: + rank -= RECENT_PENALTY return rank From 41f6f5ba1af381a00722501a94361a5c388ae465 Mon Sep 17 00:00:00 2001 From: Simone Tesini Date: Fri, 1 Aug 2025 20:46:42 +0200 Subject: [PATCH 10/12] pull --- backend/src/room.py | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/room.py b/backend/src/room.py index 6332fd0..51b96e2 100644 --- a/backend/src/room.py +++ b/backend/src/room.py @@ -20,7 +20,6 @@ class Room: name: str pin: int | None tags: set[str] - creative: bool songs: dict[str, UserScoredSong] history: list[Song] From 248c9bc278d2e9491c7d5c0328fd835673374312 Mon Sep 17 00:00:00 2001 From: panzerotto Date: Fri, 1 Aug 2025 20:56:48 +0200 Subject: [PATCH 11/12] fix db creation --- backend/src/app.py | 2 +- backend/src/connect.py | 2 +- docker-compose.yml | 32 ++++++++++++++++---------------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/backend/src/app.py b/backend/src/app.py index 9dda54a..2df573d 100644 --- a/backend/src/app.py +++ b/backend/src/app.py @@ -45,6 +45,6 @@ def join(): return {"success": True, "ws": f"/ws/{room_id}"} +init_db() if __name__ == "__main__": - init_db() app.run(debug=True) diff --git a/backend/src/connect.py b/backend/src/connect.py index 12e53d8..962528c 100644 --- a/backend/src/connect.py +++ b/backend/src/connect.py @@ -2,5 +2,5 @@ import sqlite3 def get_connection(): - conn = sqlite3.connect("jukebox.db") + conn = sqlite3.connect(".data/jukebox.db") return conn diff --git a/docker-compose.yml b/docker-compose.yml index ea7e2ca..3b05616 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,18 +1,18 @@ services: - backend: - build: ./backend - ports: - - 5001:5000 - environment: - NODE_ENV: development - volumes: - - ./backend:/app - - ./.data/jukebox.sqlite3:/app/.data/jukebox.sqlite3 + backend: + build: ./backend + ports: + - 5001:5000 + environment: + NODE_ENV: development + volumes: + - ./backend:/app + - ./.data:/app/.data - frontend: - build: ./frontend - ports: - - 5173:5173 - volumes: - - ./frontend:/app - - /app/node_modules + frontend: + build: ./frontend + ports: + - 5173:5173 + volumes: + - ./frontend:/app + - /app/node_modules From 70f4b89d9e05ab608541ff4f8372c5296ca15ff3 Mon Sep 17 00:00:00 2001 From: Simone Tesini Date: Fri, 1 Aug 2025 20:59:09 +0200 Subject: [PATCH 12/12] pull --- backend/src/app.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/backend/src/app.py b/backend/src/app.py index 9dda54a..645b827 100644 --- a/backend/src/app.py +++ b/backend/src/app.py @@ -1,12 +1,10 @@ +import dotenv from flask import Flask, Response, jsonify, request from flask_cors import CORS -import dotenv from .room import Room from .song import init_db -from . import song_fetch - dotenv.load_dotenv()