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 cd551c2..2b636c3 100644 --- a/backend/src/app.py +++ b/backend/src/app.py @@ -1,14 +1,48 @@ -from flask import Flask +import dotenv +from flask import Flask, Response, jsonify, request from flask_cors import CORS +from .room import Room +from .song import init_db + +dotenv.load_dotenv() + + 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}"} + + +init_db() if __name__ == "__main__": app.run(debug=True) diff --git a/backend/src/connect.py b/backend/src/connect.py new file mode 100644 index 0000000..962528c --- /dev/null +++ b/backend/src/connect.py @@ -0,0 +1,6 @@ +import sqlite3 + + +def get_connection(): + conn = sqlite3.connect(".data/jukebox.db") + return conn diff --git a/backend/src/room.py b/backend/src/room.py new file mode 100644 index 0000000..51b96e2 --- /dev/null +++ b/backend/src/room.py @@ -0,0 +1,64 @@ +from dataclasses import dataclass + +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 + + +type UserScoredSong = tuple[Song, int] + + +@dataclass +class Room: + id: int + coord: tuple[float, float] + name: str + pin: int | None + tags: set[str] + + songs: dict[str, UserScoredSong] + history: list[Song] + + 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][1])[1][1] + highest_score = min(song_items, key=lambda item: item[1][1])[1][1] + + rank += translate(user_score, lowest_score, highest_score, 0.0, USER_SCORE_WEIGHT) + + 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 + + +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/backend/src/song.py b/backend/src/song.py new file mode 100644 index 0000000..8570033 --- /dev/null +++ b/backend/src/song.py @@ -0,0 +1,29 @@ +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 +class Song: + mbid: str + title: str + artist: str + tags: list[str] + image_id: str + youtube_id: str 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 ==> 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 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