Preserve server source during album fill
- derive the destination server_source from the target album context - write it on copied rows and retarget moved rows too - cover the copy branch with a regression test
This commit is contained in:
parent
e7e32652f5
commit
8603cd6680
2 changed files with 57 additions and 17 deletions
|
|
@ -12,6 +12,7 @@ import os
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
|
import sqlite3
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
|
|
@ -2231,6 +2232,46 @@ class RepairWorker:
|
||||||
album_folder, filename_pattern, download_folder):
|
album_folder, filename_pattern, download_folder):
|
||||||
"""Move or copy a candidate track into the album folder and update DB."""
|
"""Move or copy a candidate track into the album folder and update DB."""
|
||||||
try:
|
try:
|
||||||
|
def _fallback_server_source():
|
||||||
|
if getattr(candidate, 'server_source', None):
|
||||||
|
return candidate.server_source
|
||||||
|
if self._config_manager:
|
||||||
|
getter = getattr(self._config_manager, 'get_active_media_server', None)
|
||||||
|
if callable(getter):
|
||||||
|
return getter() or 'plex'
|
||||||
|
return self._config_manager.get('active_media_server', 'plex')
|
||||||
|
return 'plex'
|
||||||
|
|
||||||
|
def _resolve_target_context(cursor):
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT artist_id, server_source
|
||||||
|
FROM tracks
|
||||||
|
WHERE album_id = ?
|
||||||
|
ORDER BY track_number, title
|
||||||
|
LIMIT 1
|
||||||
|
""",
|
||||||
|
(album_id,),
|
||||||
|
)
|
||||||
|
row = cursor.fetchone()
|
||||||
|
if row:
|
||||||
|
return row[0] or candidate.artist_id, row[1] or _fallback_server_source()
|
||||||
|
|
||||||
|
try:
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT artist_id, server_source FROM albums WHERE id = ? LIMIT 1",
|
||||||
|
(album_id,),
|
||||||
|
)
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
row = None
|
||||||
|
else:
|
||||||
|
row = cursor.fetchone()
|
||||||
|
|
||||||
|
if row:
|
||||||
|
return row[0] or candidate.artist_id, row[1] or _fallback_server_source()
|
||||||
|
|
||||||
|
return candidate.artist_id, _fallback_server_source()
|
||||||
|
|
||||||
# Resolve source file
|
# Resolve source file
|
||||||
src_path = _resolve_file_path(candidate.file_path, self.transfer_folder, download_folder, config_manager=self._config_manager)
|
src_path = _resolve_file_path(candidate.file_path, self.transfer_folder, download_folder, config_manager=self._config_manager)
|
||||||
if not src_path or not os.path.exists(src_path):
|
if not src_path or not os.path.exists(src_path):
|
||||||
|
|
@ -2264,18 +2305,15 @@ class RepairWorker:
|
||||||
# Update existing DB record to point to new album and path
|
# Update existing DB record to point to new album and path
|
||||||
conn = self.db._get_connection()
|
conn = self.db._get_connection()
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
# Get the target album's artist_id for consistency
|
target_artist_id, target_server_source = _resolve_target_context(cursor)
|
||||||
cursor.execute("SELECT artist_id FROM tracks WHERE album_id = ? LIMIT 1", (album_id,))
|
|
||||||
artist_row = cursor.fetchone()
|
|
||||||
target_artist_id = artist_row[0] if artist_row else candidate.artist_id
|
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
UPDATE tracks
|
UPDATE tracks
|
||||||
SET album_id = ?, artist_id = ?, title = ?,
|
SET album_id = ?, artist_id = ?, title = ?,
|
||||||
file_path = ?, track_number = ?,
|
file_path = ?, track_number = ?, server_source = ?,
|
||||||
updated_at = CURRENT_TIMESTAMP
|
updated_at = CURRENT_TIMESTAMP
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
""", (album_id, target_artist_id, track_name,
|
""", (album_id, target_artist_id, track_name,
|
||||||
target_path, track_number, candidate.id))
|
target_path, track_number, target_server_source, candidate.id))
|
||||||
|
|
||||||
# Clean up the source single's album if it's now empty
|
# Clean up the source single's album if it's now empty
|
||||||
cursor.execute("SELECT COUNT(*) FROM tracks WHERE album_id = ?", (candidate.album_id,))
|
cursor.execute("SELECT COUNT(*) FROM tracks WHERE album_id = ?", (candidate.album_id,))
|
||||||
|
|
@ -2300,17 +2338,14 @@ class RepairWorker:
|
||||||
|
|
||||||
conn = self.db._get_connection()
|
conn = self.db._get_connection()
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
# Get artist_id from existing album tracks
|
target_artist_id, target_server_source = _resolve_target_context(cursor)
|
||||||
cursor.execute("SELECT artist_id FROM tracks WHERE album_id = ? LIMIT 1", (album_id,))
|
|
||||||
artist_row = cursor.fetchone()
|
|
||||||
target_artist_id = artist_row[0] if artist_row else candidate.artist_id
|
|
||||||
|
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
INSERT INTO tracks (id, album_id, artist_id, title, track_number, duration,
|
INSERT INTO tracks (id, album_id, artist_id, title, track_number, duration,
|
||||||
file_path, bitrate, created_at, updated_at)
|
file_path, bitrate, server_source, created_at, updated_at)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||||
""", (new_track_id, album_id, target_artist_id, track_name, track_number,
|
""", (new_track_id, album_id, target_artist_id, track_name, track_number,
|
||||||
candidate.duration, target_path, candidate.bitrate))
|
candidate.duration, target_path, candidate.bitrate, target_server_source))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,7 @@ def test_perform_album_fill_copy_branch_generates_track_id(tmp_path, monkeypatch
|
||||||
duration INTEGER,
|
duration INTEGER,
|
||||||
file_path TEXT,
|
file_path TEXT,
|
||||||
bitrate INTEGER,
|
bitrate INTEGER,
|
||||||
|
server_source TEXT,
|
||||||
created_at TEXT,
|
created_at TEXT,
|
||||||
updated_at TEXT
|
updated_at TEXT
|
||||||
)
|
)
|
||||||
|
|
@ -69,10 +70,10 @@ def test_perform_album_fill_copy_branch_generates_track_id(tmp_path, monkeypatch
|
||||||
)
|
)
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO tracks (id, album_id, artist_id, title, track_number, duration, file_path, bitrate, created_at, updated_at)
|
INSERT INTO tracks (id, album_id, artist_id, title, track_number, duration, file_path, bitrate, server_source, created_at, updated_at)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||||
""",
|
""",
|
||||||
("target-track-1", "target-album", "target-artist", "Existing Track", 1, 180000, str(src_path), 320),
|
("target-track-1", "target-album", "target-artist", "Existing Track", 1, 180000, str(src_path), 320, "navidrome"),
|
||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
|
|
@ -89,6 +90,7 @@ def test_perform_album_fill_copy_branch_generates_track_id(tmp_path, monkeypatch
|
||||||
duration=180000,
|
duration=180000,
|
||||||
file_path=str(src_path),
|
file_path=str(src_path),
|
||||||
bitrate=320,
|
bitrate=320,
|
||||||
|
server_source="soulsync",
|
||||||
),
|
),
|
||||||
SimpleNamespace(
|
SimpleNamespace(
|
||||||
id="source-track-2",
|
id="source-track-2",
|
||||||
|
|
@ -97,6 +99,7 @@ def test_perform_album_fill_copy_branch_generates_track_id(tmp_path, monkeypatch
|
||||||
duration=181000,
|
duration=181000,
|
||||||
file_path=str(src_path),
|
file_path=str(src_path),
|
||||||
bitrate=320,
|
bitrate=320,
|
||||||
|
server_source="soulsync",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -120,6 +123,7 @@ def test_perform_album_fill_copy_branch_generates_track_id(tmp_path, monkeypatch
|
||||||
duration=180000,
|
duration=180000,
|
||||||
file_path=str(src_path),
|
file_path=str(src_path),
|
||||||
bitrate=320,
|
bitrate=320,
|
||||||
|
server_source="soulsync",
|
||||||
),
|
),
|
||||||
album_id="target-album",
|
album_id="target-album",
|
||||||
album_title="Target Album",
|
album_title="Target Album",
|
||||||
|
|
@ -137,7 +141,7 @@ def test_perform_album_fill_copy_branch_generates_track_id(tmp_path, monkeypatch
|
||||||
|
|
||||||
with sqlite3.connect(db_path) as verify_conn:
|
with sqlite3.connect(db_path) as verify_conn:
|
||||||
row = verify_conn.execute(
|
row = verify_conn.execute(
|
||||||
"SELECT id, title, file_path FROM tracks WHERE title = ?",
|
"SELECT id, title, file_path, server_source FROM tracks WHERE title = ?",
|
||||||
("New Track",),
|
("New Track",),
|
||||||
).fetchone()
|
).fetchone()
|
||||||
assert row is not None
|
assert row is not None
|
||||||
|
|
@ -145,4 +149,5 @@ def test_perform_album_fill_copy_branch_generates_track_id(tmp_path, monkeypatch
|
||||||
assert row[0].startswith("album_fill_source-track-1_deadbeef")
|
assert row[0].startswith("album_fill_source-track-1_deadbeef")
|
||||||
assert row[1] == "New Track"
|
assert row[1] == "New Track"
|
||||||
assert Path(row[2]).exists()
|
assert Path(row[2]).exists()
|
||||||
|
assert row[3] == "navidrome"
|
||||||
assert verify_conn.execute("SELECT COUNT(*) FROM tracks WHERE id IS NULL").fetchone()[0] == 0
|
assert verify_conn.execute("SELECT COUNT(*) FROM tracks WHERE id IS NULL").fetchone()[0] == 0
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue