mirror of
https://github.com/Dvorinka/SpotifyRecAlg.git
synced 2026-06-04 20:43:04 +00:00
first commit
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user