Reorganize leaves a cover.jpg (and other leftovers) behind when it moves an album, and the Empty Folder Cleaner is too conservative to sweep them. Two complementary fixes over one shared 'residual file' predicate (core/library/residual_files.py: junk + cover/scan images + lyric/metadata sidecars), so both features agree on what a dead folder is. 1. Reorganize (proactive): _delete_album_sidecars now sweeps ALL residual files when a source dir has no audio left — previously only a fixed list of cover NAMES, so back.jpg / disc.png / .webp survived and kept the folder un-prunable. 2. Empty Folder Cleaner (the request): new opt-in 'Remove Residual Files' setting (default off) treats a folder holding only images/sidecars/junk as removable — cleans the existing backlog + arbitrary names. Auto-renders as a UI toggle. Safe by construction: whitelist-only (a booklet.pdf / video / .txt is real content and kept), reorganize sweep gated on no-audio, cleaner re-checks at apply time. 20 new tests; 272 reorganize/repair/empty-folder green.
53 lines
1.9 KiB
Python
53 lines
1.9 KiB
Python
"""What counts as a *residual* file — a leftover with no value once the audio it
|
|
accompanied is gone: OS junk, cover/scan images, and lyric/metadata sidecars.
|
|
|
|
Single source of truth shared by:
|
|
* the **Reorganize** cleanup, which strips these from a source dir after every
|
|
track has moved out (so the empty-dir pruner can take the folder), and
|
|
* the **Empty Folder Cleaner** job, which can optionally treat a folder holding
|
|
ONLY residual files as removable (#891).
|
|
|
|
Defining "disposable" in one place keeps the two features agreeing on what a "dead
|
|
folder" is. Pure predicates — no filesystem access — so they're unit-tested in
|
|
isolation. The whitelist is deliberately conservative: anything NOT recognized here
|
|
(a booklet ``.pdf``, a video, a ``.txt`` note) is treated as real content and kept.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
|
|
# OS / tooling junk.
|
|
JUNK_FILES = {'.ds_store', 'thumbs.db', 'desktop.ini', '.directory', 'album.nfo~'}
|
|
# Cover art + booklet scans.
|
|
IMAGE_EXTS = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.tiff', '.tif'}
|
|
# Lyric / metadata / playlist sidecars that are worthless without their audio.
|
|
SIDECAR_EXTS = {'.lrc', '.nfo', '.cue', '.m3u', '.m3u8'}
|
|
|
|
|
|
def _ext(name: str) -> str:
|
|
return os.path.splitext(name or '')[1].lower()
|
|
|
|
|
|
def is_junk(name: str) -> bool:
|
|
return (name or '').lower() in JUNK_FILES
|
|
|
|
|
|
def is_image(name: str) -> bool:
|
|
return _ext(name) in IMAGE_EXTS
|
|
|
|
|
|
def is_sidecar(name: str) -> bool:
|
|
return _ext(name) in SIDECAR_EXTS
|
|
|
|
|
|
def is_disposable(name: str) -> bool:
|
|
"""True if this file is junk, a cover/scan image, or a lyric/metadata sidecar —
|
|
i.e. safe to delete from a folder that has no audio left."""
|
|
return is_junk(name) or is_image(name) or is_sidecar(name)
|
|
|
|
|
|
__all__ = [
|
|
'JUNK_FILES', 'IMAGE_EXTS', 'SIDECAR_EXTS',
|
|
'is_junk', 'is_image', 'is_sidecar', 'is_disposable',
|
|
]
|