#917: 'I have this' reuses the album's existing folder year instead of dropping it

The import rebuilds the destination path from album metadata. When the albums row has no year,
release_date is empty, the path template drops $year, and the copied file lands in a NEW yearless
directory instead of the album's existing 'Album (YYYY)' folder. (The code logically forces this:
the year only drops when album.year is empty.)

Fix: when album.year is empty, recover it from a sibling track — its own year column, else a
(YYYY)/[YYYY] in the album folder name — so the rebuilt path matches the existing directory.
No-op when album.year is already set.

Tests: _existing_album_year_from_sibling covers year-column, paren folder, bracket folder, no-signal,
and target-slot exclusion.
This commit is contained in:
BoulderBadgeDad 2026-06-23 16:18:00 -07:00
parent 2934903874
commit 600a744f7f
2 changed files with 103 additions and 0 deletions

View file

@ -10,6 +10,7 @@ from __future__ import annotations
from dataclasses import dataclass
import logging
import os
import re
import shutil
import uuid
from typing import Any, Callable, Dict, Optional
@ -121,6 +122,20 @@ def import_existing_track_for_album_slot(album_id: str, payload: dict, deps: Mis
if album_data.get("server_source") and source_track.get("server_source") and album_data["server_source"] != source_track["server_source"]:
raise MissingTrackImportError("Selected track belongs to a different library source", 400)
# #917: "I have this" rebuilds the destination path from album metadata. When the album row
# has no year, the rebuilt path drops the $year and the copied file lands in a NEW, yearless
# directory instead of the album's existing folder. Recover the year from a sibling track so
# the import reuses the same directory.
if not album_data.get("year"):
recovered_year = _existing_album_year_from_sibling(
database, album_id, deps.resolve_library_file_path_fn,
int(expected.get("disc_number") or 1), int(expected.get("track_number") or 1),
)
if recovered_year:
album_data["year"] = recovered_year
logger.info("[I Have This] recovered album year %s from existing folder for album %s",
recovered_year, album_id)
source_path = deps.resolve_library_file_path_fn(source_track.get("file_path"))
if not source_path:
raise MissingTrackImportError(_file_not_found_message(source_track.get("file_path")), 404)
@ -426,6 +441,50 @@ def _sync_imported_track(deps: MissingTrackImportDeps, track_id, expected_title:
logger.debug("Existing-track import server sync skipped/failed: %s", sync_err)
def _existing_album_year_from_sibling(
database,
album_id: str,
resolve_library_file_path_fn: Callable[[Optional[str]], Optional[str]],
target_disc: int,
target_track: int,
) -> Optional[str]:
"""Find the release year already baked into this album's on-disk folder (#917).
Read from a sibling track its own ``year`` column first, else a ``(YYYY)`` /
``[YYYY]`` in the album folder name so an "I have this" import reuses the album's
existing directory instead of rebuilding a yearless one. Returns the 4-digit year
string, or None when no signal exists.
"""
try:
with database._get_connection() as conn:
cursor = conn.cursor()
cursor.execute(
"""
SELECT file_path, year FROM tracks
WHERE album_id = ?
AND file_path IS NOT NULL AND file_path != ''
AND NOT (COALESCE(disc_number, 1) = ? AND track_number = ?)
ORDER BY COALESCE(disc_number, 1), track_number
LIMIT 12
""",
(album_id, target_disc, target_track),
)
rows = cursor.fetchall()
for row in rows:
year = row["year"]
if year is not None and str(year).strip()[:4].isdigit():
return str(year).strip()[:4]
resolved = resolve_library_file_path_fn(row["file_path"])
if resolved:
folder = os.path.basename(os.path.dirname(resolved))
match = re.search(r"[(\[](\d{4})[)\]]", folder) # "Album (2019)" / "Album [2019]"
if match:
return match.group(1)
except Exception as exc:
logger.debug("Could not recover album year from sibling for %s: %s", album_id, exc)
return None
def copy_album_identity_from_target_sibling(
database,
album_id: str,

View file

@ -258,3 +258,47 @@ def test_import_rejects_missing_expected_track_context(tmp_path):
assert exc.value.status_code == 400
assert "expected_track" in str(exc.value)
# ── #917: recover album year from existing folder so "I have this" reuses the dir ──
def _year_db(rows):
conn = sqlite3.connect(":memory:")
conn.row_factory = sqlite3.Row
conn.execute("CREATE TABLE tracks (album_id TEXT, disc_number INTEGER, track_number INTEGER, file_path TEXT, year INTEGER)")
conn.executemany(
"INSERT INTO tracks (album_id, disc_number, track_number, file_path, year) VALUES (?,?,?,?,?)",
[(r.get("album_id", "A1"), r.get("disc_number", 1), r.get("track_number", 1),
r.get("file_path"), r.get("year")) for r in rows],
)
conn.commit()
return _FakeDB(conn)
def _ident(p):
return p
def test_year_recovered_from_sibling_year_column():
db = _year_db([{"track_number": 3, "file_path": "/m/Artist/Album/03.flac", "year": 2024}])
assert mti._existing_album_year_from_sibling(db, "A1", _ident, 1, 5) == "2024"
def test_year_recovered_from_paren_folder_name():
db = _year_db([{"track_number": 3, "file_path": "/music/Artist/Album (2019)/03 - Song.flac", "year": None}])
assert mti._existing_album_year_from_sibling(db, "A1", _ident, 1, 5) == "2019"
def test_year_recovered_from_bracket_folder_name():
db = _year_db([{"track_number": 3, "file_path": "/music/Artist/Album [2008]/03.flac", "year": None}])
assert mti._existing_album_year_from_sibling(db, "A1", _ident, 1, 5) == "2008"
def test_year_none_when_no_signal():
db = _year_db([{"track_number": 3, "file_path": "/music/Artist/Album/03.flac", "year": None}])
assert mti._existing_album_year_from_sibling(db, "A1", _ident, 1, 5) is None
def test_year_ignores_the_target_slot_itself():
# The only sibling row IS the slot being imported -> excluded -> no year.
db = _year_db([{"disc_number": 1, "track_number": 5, "file_path": "/m/Album (2030)/05.flac", "year": 2030}])
assert mti._existing_album_year_from_sibling(db, "A1", _ident, 1, 5) is None