This commit is contained in:
2025-09-12 22:39:59 -04:00
parent f6d4ed57f3
commit 3b74333b96
5 changed files with 108 additions and 25 deletions

3
.gitignore vendored
View File

@@ -25,5 +25,8 @@ endpoints/auth.py
endpoints/radio2 endpoints/radio2
endpoints/radio2/** endpoints/radio2/**
hash_password.py hash_password.py
up.py
job_review.py
check_missing.py
**/auth/* **/auth/*
.gitignore .gitignore

View File

@@ -45,8 +45,8 @@ class Genius:
Optional[LyricsResult]: The result, if found - None otherwise. Optional[LyricsResult]: The result, if found - None otherwise.
""" """
try: try:
artist: str = artist.strip().lower() artist = artist.strip().lower()
song: str = song.strip().lower() song = song.strip().lower()
time_start: float = time.time() time_start: float = time.time()
logging.info("Searching %s - %s on %s", artist, song, self.label) logging.info("Searching %s - %s on %s", artist, song, self.label)
search_term: str = f"{artist}%20{song}" search_term: str = f"{artist}%20{song}"
@@ -56,7 +56,6 @@ class Genius:
f"{self.genius_search_url}{search_term}", f"{self.genius_search_url}{search_term}",
timeout=self.timeout, timeout=self.timeout,
headers=self.headers, headers=self.headers,
verify_ssl=False,
proxy=private.GENIUS_PROXY, proxy=private.GENIUS_PROXY,
) as request: ) as request:
request.raise_for_status() request.raise_for_status()
@@ -113,7 +112,6 @@ class Genius:
scrape_url, scrape_url,
timeout=self.timeout, timeout=self.timeout,
headers=self.headers, headers=self.headers,
verify_ssl=False,
proxy=private.GENIUS_PROXY, proxy=private.GENIUS_PROXY,
) as scrape_request: ) as scrape_request:
scrape_request.raise_for_status() scrape_request.raise_for_status()

View File

@@ -19,8 +19,8 @@ from utils.sr_wrapper import SRUtil
# ---------- Config ---------- # ---------- Config ----------
ROOT_DIR = Path("/storage/music2") ROOT_DIR = Path("/storage/music2")
MAX_RETRIES = 5 MAX_RETRIES = 5
THROTTLE_MIN = 1.7 THROTTLE_MIN = 1.0
THROTTLE_MAX = 10.0 THROTTLE_MAX = 3.5
HEADERS = { HEADERS = {
"User-Agent": ( "User-Agent": (
@@ -259,7 +259,6 @@ def bulk_download(track_list: list, quality: str = "FLAC"):
await asyncio.sleep(random.uniform(THROTTLE_MIN, THROTTLE_MAX)) await asyncio.sleep(random.uniform(THROTTLE_MIN, THROTTLE_MAX))
finally: finally:
try: try:
await session.close()
if tmp_file and tmp_file.exists(): if tmp_file and tmp_file.exists():
tmp_file.unlink() tmp_file.unlink()
except Exception: except Exception:
@@ -304,8 +303,13 @@ def bulk_download(track_list: list, quality: str = "FLAC"):
top_artist = "Unknown Artist" top_artist = "Unknown Artist"
combined_artist = sanitize_filename(top_artist) combined_artist = sanitize_filename(top_artist)
short_id = uuid.uuid4().hex[:8] staged_tarball = staging_root / f"{combined_artist}.tar.gz"
staged_tarball = staging_root / f"{combined_artist}_{short_id}.tar.gz" # Ensure uniqueness (Windows-style padding) within the parent folder
counter = 1
base_name = staged_tarball.stem
while staged_tarball.exists():
counter += 1
staged_tarball = staging_root / f"{base_name} ({counter}).tar.gz"
final_tarball = ROOT_DIR / "completed" / quality / staged_tarball.name final_tarball = ROOT_DIR / "completed" / quality / staged_tarball.name
final_tarball.parent.mkdir(parents=True, exist_ok=True) final_tarball.parent.mkdir(parents=True, exist_ok=True)

View File

@@ -3,9 +3,11 @@ from uuid import uuid4
from urllib.parse import urlparse from urllib.parse import urlparse
import hashlib import hashlib
import logging import logging
import random
import asyncio import asyncio
import os import os
import aiohttp import aiohttp
import time
from streamrip.client import TidalClient # type: ignore from streamrip.client import TidalClient # type: ignore
from streamrip.config import Config as StreamripConfig # type: ignore from streamrip.config import Config as StreamripConfig # type: ignore
from dotenv import load_dotenv from dotenv import load_dotenv
@@ -44,9 +46,24 @@ class SRUtil:
) )
self.streamrip_config self.streamrip_config
self.streamrip_client = TidalClient(self.streamrip_config) self.streamrip_client = TidalClient(self.streamrip_config)
self.MAX_CONCURRENT_METADATA_REQUESTS = 2
self.METADATA_RATE_LIMIT = 1.25
self.METADATA_SEMAPHORE = asyncio.Semaphore(self.MAX_CONCURRENT_METADATA_REQUESTS)
self.LAST_METADATA_REQUEST = 0
self.MAX_METADATA_RETRIES = 5 self.MAX_METADATA_RETRIES = 5
self.METADATA_ALBUM_CACHE: dict[str, dict] = {}
self.RETRY_DELAY = 1.0 # seconds between retries self.RETRY_DELAY = 1.0 # seconds between retries
async def rate_limited_request(self, func, *args, **kwargs):
async with self.METADATA_SEMAPHORE:
now = time.time()
elapsed = now - self.LAST_METADATA_REQUEST
if elapsed < self.METADATA_RATE_LIMIT:
await asyncio.sleep(self.METADATA_RATE_LIMIT - elapsed)
result = await func(*args, **kwargs)
self.last_request_time = time.time()
return result
def dedupe_by_key(self, key: str, entries: list[dict]) -> list[dict]: def dedupe_by_key(self, key: str, entries: list[dict]) -> list[dict]:
deduped = {} deduped = {}
for entry in entries: for entry in entries:
@@ -62,12 +79,14 @@ class SRUtil:
return f"{m}:{s:02}" return f"{m}:{s:02}"
def combine_album_track_metadata( def combine_album_track_metadata(
self, album_json: dict[str, Any], track_json: dict[str, Any] self, album_json: dict | None, track_json: dict
) -> dict[str, Any]: ) -> dict:
""" """
Combine album-level and track-level metadata into a unified tag dictionary. Combine album-level and track-level metadata into a unified tag dictionary.
If track_json comes from album_json['tracks'], it will override album-level values where relevant. Track-level metadata overrides album-level where relevant.
""" """
album_json = album_json or {}
# Album-level # Album-level
combined = { combined = {
"album": album_json.get("title"), "album": album_json.get("title"),
@@ -99,17 +118,18 @@ class SRUtil:
"peak": track_json.get("peak"), "peak": track_json.get("peak"),
"lyrics": track_json.get("lyrics"), "lyrics": track_json.get("lyrics"),
"track_copyright": track_json.get("copyright"), "track_copyright": track_json.get("copyright"),
"cover_id": track_json.get("album", {}).get( "cover_id": track_json.get("album", {}).get("cover") or album_json.get("cover"),
"cover", album_json.get("cover") "cover_url": (
f"https://resources.tidal.com/images/{track_json.get('album', {}).get('cover', album_json.get('cover'))}/1280x1280.jpg"
if (track_json.get("album", {}).get("cover") or album_json.get("cover"))
else None
), ),
"cover_url": f"https://resources.tidal.com/images/{track_json.get('album', {}).get('cover', album_json.get('cover'))}/1280x1280.jpg"
if (track_json.get("album", {}).get("cover") or album_json.get("cover"))
else None,
} }
) )
return combined return combined
def combine_album_with_all_tracks( def combine_album_with_all_tracks(
self, album_json: dict[str, Any] self, album_json: dict[str, Any]
) -> list[dict[str, Any]]: ) -> list[dict[str, Any]]:
@@ -282,22 +302,40 @@ class SRUtil:
async def get_metadata_by_track_id(self, track_id: int) -> Optional[dict]: async def get_metadata_by_track_id(self, track_id: int) -> Optional[dict]:
""" """
Fetch track + album metadata with retries. Fetch track + album metadata with retries, caching album data.
Returns combined metadata dict or None after exhausting retries. Returns combined metadata dict or None after exhausting retries.
""" """
for attempt in range(1, self.MAX_METADATA_RETRIES + 1): for attempt in range(1, self.MAX_METADATA_RETRIES + 1):
try: try:
await self.streamrip_client.login() await self.streamrip_client.login()
metadata = await self.streamrip_client.get_metadata(
str(track_id), "track" # Track metadata
metadata = await self.rate_limited_request(
self.streamrip_client.get_metadata, str(track_id), "track"
) )
album_id = metadata.get("album", {}).get("id") album_id = metadata.get("album", {}).get("id")
album_metadata = await self.streamrip_client.get_metadata( album_metadata = None
album_id, "album"
) if album_id:
# Check cache first
if album_id in self.METADATA_ALBUM_CACHE:
album_metadata = self.METADATA_ALBUM_CACHE[album_id]
else:
album_metadata = await self.rate_limited_request(
self.streamrip_client.get_metadata, album_id, "album"
)
if not album_metadata:
return None
self.METADATA_ALBUM_CACHE[album_id] = album_metadata
# Combine track + album metadata
if not album_metadata:
return None
combined_metadata: dict = self.combine_album_track_metadata( combined_metadata: dict = self.combine_album_track_metadata(
album_metadata, metadata album_metadata, metadata
) )
logging.info( logging.info(
"Combined metadata for track ID %s (attempt %d): %s", "Combined metadata for track ID %s (attempt %d): %s",
track_id, track_id,
@@ -305,16 +343,20 @@ class SRUtil:
combined_metadata, combined_metadata,
) )
return combined_metadata return combined_metadata
except Exception as e: except Exception as e:
# Exponential backoff with jitter for 429 or other errors
delay = self.RETRY_DELAY * (2 ** (attempt - 1)) + random.uniform(0, 0.5)
logging.warning( logging.warning(
"Metadata fetch failed for track %s (attempt %d/%d): %s", "Metadata fetch failed for track %s (attempt %d/%d): %s. Retrying in %.2fs",
track_id, track_id,
attempt, attempt,
self.MAX_METADATA_RETRIES, self.MAX_METADATA_RETRIES,
str(e), str(e),
delay,
) )
if attempt < self.MAX_METADATA_RETRIES: if attempt < self.MAX_METADATA_RETRIES:
await asyncio.sleep(self.RETRY_DELAY) await asyncio.sleep(delay)
else: else:
logging.error( logging.error(
"Metadata fetch failed permanently for track %s after %d attempts", "Metadata fetch failed permanently for track %s after %d attempts",
@@ -323,6 +365,7 @@ class SRUtil:
) )
return None return None
async def download(self, track_id: int, quality: str = "LOSSLESS") -> bool | str: async def download(self, track_id: int, quality: str = "LOSSLESS") -> bool | str:
"""Download track """Download track
Args: Args:

35
utils/test.conf Normal file
View File

@@ -0,0 +1,35 @@
# -----------------------
# /m/m2/ PHP handler
location ~ ^/m/m2/(.+\.php)$ {
alias /storage/music2/completed/;
include fastcgi_params;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME /storage/music2/completed/$1;
fastcgi_param DOCUMENT_ROOT /storage/music2/completed;
fastcgi_param SCRIPT_NAME /m/m2/$1;
}
# /m/m2/ static files
location /m/m2/ {
alias /storage/music2/completed/;
index index.php;
try_files $uri $uri/ /index.php$is_args$args;
}
# -----------------------
# /m/ PHP handler
location ~ ^/m/(.+\.php)$ {
root /var/www/codey.lol/new/public;
include fastcgi_params;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root/$1;
fastcgi_param DOCUMENT_ROOT $document_root;
fastcgi_param SCRIPT_NAME /m/$1;
}
# /m/ static files
location /m/ {
root /var/www/codey.lol/new/public;
index index.php;
try_files $uri $uri/ /m/index.php$is_args$args;
}