Initial import: Music_Server, MusicFree, catalog-sync
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import tempfile
|
||||
import zipfile
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Iterable
|
||||
from uuid import uuid4
|
||||
|
||||
from .runtime import sanitize_path_component
|
||||
|
||||
INTERNAL_BUNDLE_NAME_SEPARATOR = "--"
|
||||
|
||||
|
||||
def default_bundle_root() -> Path:
|
||||
root = Path(tempfile.gettempdir()) / "musicdl-catalogsync" / "bundles"
|
||||
root.mkdir(parents=True, exist_ok=True)
|
||||
return root
|
||||
|
||||
|
||||
def build_single_playlist_bundle_filename(
|
||||
*,
|
||||
platform: str,
|
||||
playlist_id: int,
|
||||
playlist_name: str,
|
||||
) -> str:
|
||||
safe_platform = sanitize_path_component(str(platform or ""), "unknown")
|
||||
safe_name = sanitize_path_component(str(playlist_name or ""), f"playlist-{int(playlist_id)}")
|
||||
return f"playlist-{safe_platform}-{int(playlist_id)}-{safe_name}.zip"
|
||||
|
||||
|
||||
def build_multi_playlist_bundle_filename(*, created_at: datetime | None = None) -> str:
|
||||
now = created_at or datetime.now()
|
||||
return "playlists-export-" + now.strftime("%Y%m%d-%H%M%S") + ".zip"
|
||||
|
||||
|
||||
def bundle_download_filename(bundle_path_or_name: str | Path) -> str:
|
||||
filename = Path(bundle_path_or_name).name
|
||||
if INTERNAL_BUNDLE_NAME_SEPARATOR not in filename:
|
||||
return filename
|
||||
return filename.split(INTERNAL_BUNDLE_NAME_SEPARATOR, 1)[1]
|
||||
|
||||
|
||||
def resolve_bundle_download_path(bundle_root: Path, bundle_name: str) -> Path | None:
|
||||
normalized_name = str(bundle_name or "").strip()
|
||||
if not normalized_name:
|
||||
return None
|
||||
safe_name = sanitize_path_component(normalized_name, "")
|
||||
if not safe_name or safe_name != normalized_name:
|
||||
return None
|
||||
return Path(bundle_root) / f"{normalized_name}.zip"
|
||||
|
||||
|
||||
def create_single_playlist_bundle(
|
||||
*,
|
||||
playlist_dir: Path,
|
||||
bundle_root: Path,
|
||||
platform: str,
|
||||
playlist_id: int,
|
||||
playlist_name: str,
|
||||
) -> Path:
|
||||
source_dir = Path(playlist_dir)
|
||||
if not source_dir.exists() or not source_dir.is_dir():
|
||||
raise FileNotFoundError(f"playlist directory not found: {source_dir}")
|
||||
root = Path(bundle_root)
|
||||
root.mkdir(parents=True, exist_ok=True)
|
||||
bundle_path = root / build_single_playlist_bundle_filename(
|
||||
platform=platform,
|
||||
playlist_id=playlist_id,
|
||||
playlist_name=playlist_name,
|
||||
)
|
||||
_write_zip_from_directories(bundle_path, [(source_dir, source_dir.name)])
|
||||
return bundle_path
|
||||
|
||||
|
||||
def create_multi_playlist_bundle(
|
||||
*,
|
||||
playlist_dirs: Iterable[Path],
|
||||
bundle_root: Path,
|
||||
created_at: datetime | None = None,
|
||||
) -> Path:
|
||||
resolved_dirs: list[Path] = []
|
||||
for item in playlist_dirs:
|
||||
playlist_dir = Path(item)
|
||||
if not playlist_dir.exists() or not playlist_dir.is_dir():
|
||||
raise FileNotFoundError(f"playlist directory not found: {playlist_dir}")
|
||||
resolved_dirs.append(playlist_dir)
|
||||
if not resolved_dirs:
|
||||
raise ValueError("playlist_dirs is required")
|
||||
|
||||
root = Path(bundle_root)
|
||||
root.mkdir(parents=True, exist_ok=True)
|
||||
friendly_name = build_multi_playlist_bundle_filename(created_at=created_at)
|
||||
unique_storage_name = (
|
||||
datetime.now().strftime("%Y%m%d%H%M%S%f")
|
||||
+ "-"
|
||||
+ uuid4().hex[:8]
|
||||
+ INTERNAL_BUNDLE_NAME_SEPARATOR
|
||||
+ friendly_name
|
||||
)
|
||||
bundle_path = root / unique_storage_name
|
||||
_write_zip_from_directories(
|
||||
bundle_path,
|
||||
[(playlist_dir, f"playlists/{playlist_dir.name}") for playlist_dir in resolved_dirs],
|
||||
)
|
||||
return bundle_path
|
||||
|
||||
|
||||
def _write_zip_from_directories(bundle_path: Path, directories: list[tuple[Path, str]]) -> None:
|
||||
if bundle_path.exists():
|
||||
bundle_path.unlink()
|
||||
with zipfile.ZipFile(bundle_path, mode="w", compression=zipfile.ZIP_DEFLATED) as archive:
|
||||
for source_dir, zip_root in directories:
|
||||
for child in sorted(source_dir.rglob("*")):
|
||||
if not child.is_file():
|
||||
continue
|
||||
relative_path = child.relative_to(source_dir).as_posix()
|
||||
archive.write(child, arcname=f"{zip_root}/{relative_path}")
|
||||
Reference in New Issue
Block a user