Merge branch 'main' of https://repos.hackathon.bz.it/2025-summer/team-1
This commit is contained in:
commit
25a6b4e82c
9 changed files with 220 additions and 17 deletions
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -1,2 +1,4 @@
|
||||||
Flask==3.1.0
|
Flask==3.1.0
|
||||||
flask-cors
|
flask-cors
|
||||||
|
dotenv
|
||||||
|
requests
|
|
@ -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
6
backend/src/connect.py
Normal 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
64
backend/src/room.py
Normal 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
29
backend/src/song.py
Normal 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
63
backend/src/song_fetch.py
Normal 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 ==>
|
|
@ -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
1
ruff.toml
Normal file
|
@ -0,0 +1 @@
|
||||||
|
line-length = 200
|
Loading…
Add table
Add a link
Reference in a new issue