The dev-nightly build runs `ruff check .` before "Build and push to GHCR" in the same job, so the three S110 (try/except/pass) errors introduced since the last green build (ce6ce4d) failed the lint step and SKIPPED the image push entirely — every dev-nightly since #704 went red, so the dev image was never rebuilt and none of the recent fixes (incl. the #852 WebSocket login-bypass fix) ever shipped to the image users pull. All three are deliberate best-effort swallows; annotate them with the repo's existing `# noqa: S110 — <reason>` convention rather than adding dead logging: - relocate.py: tag write is best-effort (re-import re-derives tags) - acoustid_scanner.py: verification-status tag is optional context - web_server.py: audio-duration probe falls through to 0 ruff check . + compileall now clean; pytest already passed in CI atce6ce4d.
68 lines
2.6 KiB
Python
68 lines
2.6 KiB
Python
"""Relocate an AcoustID-mismatched file into Staging for clean re-import (#704).
|
|
|
|
The existing 'retag' fix corrects a mismatched file's tags + DB record but leaves
|
|
the file in the WRONG artist/album folder on disk — so the library shows the right
|
|
title while the file sits under the previous track's artist/album. AcoustID only
|
|
yields a title + artist (not a reliable album), so an *in-place* move has no
|
|
trustworthy target.
|
|
|
|
Instead: retag the file, move it into the staging folder, and drop the stale
|
|
``tracks`` row. The auto-import worker (which watches staging) then re-identifies
|
|
the file with full metadata and files it in the correct artist/album/track path —
|
|
reusing the battle-tested import pipeline rather than guessing a destination here.
|
|
|
|
Side effects are injected so the orchestration is a pure, unit-testable seam.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from typing import Any, Callable, Dict, Optional
|
|
|
|
|
|
def staging_destination(staging_dir: str, filename: str,
|
|
exists: Callable[[str], bool]) -> str:
|
|
"""A non-colliding path for ``filename`` inside ``staging_dir``.
|
|
|
|
If the name is already taken, suffix it ``' (1)'``, ``' (2)'``, … before the
|
|
extension — never overwrite an unrelated file already waiting in staging.
|
|
"""
|
|
base, ext = os.path.splitext(filename)
|
|
dest = os.path.join(staging_dir, filename)
|
|
n = 1
|
|
while exists(dest):
|
|
dest = os.path.join(staging_dir, f"{base} ({n}){ext}")
|
|
n += 1
|
|
return dest
|
|
|
|
|
|
def relocate_mismatch_to_staging(
|
|
resolved_path: str,
|
|
staging_dir: str,
|
|
tag_updates: Optional[Dict[str, Any]],
|
|
*,
|
|
write_tags: Callable[[str, Dict[str, Any]], Any],
|
|
move_file: Callable[[str, str], Any],
|
|
drop_db_row: Callable[[], Any],
|
|
exists: Callable[[str], bool],
|
|
) -> str:
|
|
"""Retag (best-effort) → move into staging → drop the stale DB row.
|
|
|
|
Returns the staging destination path. Order matters: the DB row is dropped
|
|
only AFTER a successful move, so a failed move (which raises) leaves the
|
|
library entry intact rather than orphaning it.
|
|
"""
|
|
if tag_updates:
|
|
try:
|
|
write_tags(resolved_path, tag_updates)
|
|
except Exception: # noqa: S110 — tags are best-effort; re-import re-derives them
|
|
# The relocation itself is the point, so don't abort over a tag write.
|
|
pass
|
|
|
|
dest = staging_destination(staging_dir, os.path.basename(resolved_path), exists)
|
|
move_file(resolved_path, dest) # may raise → row NOT dropped (intentional)
|
|
drop_db_row()
|
|
return dest
|
|
|
|
|
|
__all__ = ["staging_destination", "relocate_mismatch_to_staging"]
|