119 lines
3.9 KiB
Python
119 lines
3.9 KiB
Python
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}")
|