mirror of
https://github.com/Dvorinka/SpotifyRecAlg.git
synced 2026-06-03 20:13:03 +00:00
191 lines
5.1 KiB
Python
191 lines
5.1 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
import shlex
|
|
import shutil
|
|
import subprocess
|
|
import time
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
|
|
SUPPORTED_AUDIO_EXTENSIONS = {
|
|
".flac",
|
|
".mp3",
|
|
".m4a",
|
|
".ogg",
|
|
".opus",
|
|
".wav",
|
|
".aac",
|
|
}
|
|
|
|
|
|
@dataclass
|
|
class AdapterDownloadResult:
|
|
file_path: str
|
|
codec: str
|
|
bitrate: int
|
|
provider: str
|
|
|
|
|
|
def _quality_to_bitrate(quality: str, codec: str) -> int:
|
|
quality = (quality or "high").lower()
|
|
codec = (codec or "mp3").lower()
|
|
|
|
if codec == "flac" or quality == "lossless":
|
|
return 1411
|
|
if quality == "high":
|
|
return 320
|
|
if quality == "medium":
|
|
return 192
|
|
return 128
|
|
|
|
|
|
class CommandFallbackAdapter:
|
|
"""
|
|
Generic command adapter used as fallback when the primary SpotiFLAC
|
|
provider is not available or fails.
|
|
|
|
Configure with:
|
|
- SWINGMUSIC_FALLBACK_DOWNLOAD_CMD
|
|
Default: disabled.
|
|
Example:
|
|
'{url}' -> source URL
|
|
'{output_dir}' -> destination directory
|
|
'{codec}' / '{quality}' / '{item_type}' / '{target_path}'
|
|
"""
|
|
|
|
def __init__(self) -> None:
|
|
self.name = os.getenv("SWINGMUSIC_FALLBACK_PROVIDER_NAME", "fallback-command")
|
|
self.command_template = os.getenv(
|
|
"SWINGMUSIC_FALLBACK_DOWNLOAD_CMD", ""
|
|
).strip()
|
|
self.timeout_seconds = int(
|
|
os.getenv("SWINGMUSIC_FALLBACK_TIMEOUT_SECONDS", "3600")
|
|
)
|
|
|
|
def is_available(self) -> bool:
|
|
if not self.command_template:
|
|
return False
|
|
|
|
try:
|
|
command = shlex.split(self.command_template)
|
|
except ValueError:
|
|
return False
|
|
|
|
if not command:
|
|
return False
|
|
|
|
executable = command[0]
|
|
return shutil.which(executable) is not None
|
|
|
|
@staticmethod
|
|
def _list_audio_files(output_dir: str) -> set[Path]:
|
|
directory = Path(output_dir)
|
|
if not directory.exists():
|
|
return set()
|
|
|
|
files: set[Path] = set()
|
|
for path in directory.rglob("*"):
|
|
if path.is_file() and path.suffix.lower() in SUPPORTED_AUDIO_EXTENSIONS:
|
|
files.add(path.resolve())
|
|
return files
|
|
|
|
def _build_command(
|
|
self,
|
|
*,
|
|
source_url: str,
|
|
output_dir: str,
|
|
codec: str,
|
|
quality: str,
|
|
item_type: str,
|
|
target_path: str | None,
|
|
) -> list[str]:
|
|
command = self.command_template.format(
|
|
url=source_url,
|
|
output_dir=output_dir,
|
|
codec=codec,
|
|
quality=quality,
|
|
item_type=item_type,
|
|
target_path=target_path or "",
|
|
)
|
|
return shlex.split(command)
|
|
|
|
def download(
|
|
self,
|
|
*,
|
|
source_url: str,
|
|
output_dir: str,
|
|
codec: str,
|
|
quality: str,
|
|
item_type: str,
|
|
target_path: str | None = None,
|
|
) -> AdapterDownloadResult:
|
|
if not source_url:
|
|
raise RuntimeError("Fallback adapter requires source_url")
|
|
|
|
if not self.is_available():
|
|
raise RuntimeError(
|
|
"Fallback adapter command is not configured or unavailable"
|
|
)
|
|
|
|
os.makedirs(output_dir, exist_ok=True)
|
|
before = self._list_audio_files(output_dir)
|
|
command = self._build_command(
|
|
source_url=source_url,
|
|
output_dir=output_dir,
|
|
codec=codec,
|
|
quality=quality,
|
|
item_type=item_type,
|
|
target_path=target_path,
|
|
)
|
|
|
|
process = subprocess.run(
|
|
command,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=self.timeout_seconds,
|
|
check=False,
|
|
)
|
|
if process.returncode != 0:
|
|
err = (
|
|
process.stderr.strip()
|
|
or process.stdout.strip()
|
|
or "Fallback command failed"
|
|
)
|
|
raise RuntimeError(err)
|
|
|
|
if target_path and Path(target_path).exists():
|
|
resolved = str(Path(target_path).resolve())
|
|
return AdapterDownloadResult(
|
|
file_path=resolved if item_type == "track" else output_dir,
|
|
codec=Path(resolved).suffix.lstrip(".") or codec,
|
|
bitrate=_quality_to_bitrate(quality, codec),
|
|
provider=self.name,
|
|
)
|
|
|
|
after = self._list_audio_files(output_dir)
|
|
new_files = list(after - before)
|
|
if not new_files:
|
|
new_files = list(after)
|
|
if not new_files:
|
|
raise RuntimeError(
|
|
"Fallback adapter finished without producing audio files"
|
|
)
|
|
|
|
newest = max(
|
|
new_files,
|
|
key=lambda path: path.stat().st_mtime if path.exists() else time.time(),
|
|
)
|
|
resolved = str(newest.resolve())
|
|
resolved_codec = newest.suffix.lstrip(".") or codec
|
|
|
|
return AdapterDownloadResult(
|
|
file_path=resolved if item_type == "track" else output_dir,
|
|
codec=resolved_codec,
|
|
bitrate=_quality_to_bitrate(quality, resolved_codec),
|
|
provider=self.name,
|
|
)
|
|
|
|
|
|
fallback_download_adapter = CommandFallbackAdapter()
|