This commit is contained in:
Mat12143 2025-08-01 21:10:14 +02:00
commit 25a6b4e82c
9 changed files with 220 additions and 17 deletions

View file

@ -2,6 +2,10 @@ FROM python:3.13.5-alpine
WORKDIR /app 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 ./ COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt

View file

@ -1,2 +1,4 @@
Flask==3.1.0 Flask==3.1.0
flask-cors flask-cors
dotenv
requests

View file

@ -1,14 +1,48 @@
from flask import Flask import dotenv
from flask import Flask, Response, jsonify, request
from flask_cors import CORS from flask_cors import CORS
from .room import Room
from .song import init_db
dotenv.load_dotenv()
app = Flask(__name__) app = Flask(__name__)
CORS(app) 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") @app.route("/api")
def index(): def index():
return "hello from flask" 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__": if __name__ == "__main__":
app.run(debug=True) app.run(debug=True)

6
backend/src/connect.py Normal file
View file

@ -0,0 +1,6 @@
import sqlite3
def get_connection():
conn = sqlite3.connect(".data/jukebox.db")
return conn

64
backend/src/room.py Normal file
View file

@ -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

29
backend/src/song.py Normal file
View file

@ -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

63
backend/src/song_fetch.py Normal file
View file

@ -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 ==>

View file

@ -1,18 +1,18 @@
services: services:
backend: backend:
build: ./backend build: ./backend
ports: ports:
- 5001:5000 - 5001:5000
environment: environment:
NODE_ENV: development NODE_ENV: development
volumes: volumes:
- ./backend:/app - ./backend:/app
- ./.data/jukebox.sqlite3:/app/.data/jukebox.sqlite3 - ./.data:/app/.data
frontend: frontend:
build: ./frontend build: ./frontend
ports: ports:
- 5173:5173 - 5173:5173
volumes: volumes:
- ./frontend:/app - ./frontend:/app
- /app/node_modules - /app/node_modules

1
ruff.toml Normal file
View file

@ -0,0 +1 @@
line-length = 200