feat: add support for syncing matching subtitle and audio files with main media during transfer

This commit is contained in:
jxxghp
2026-05-14 21:12:02 +08:00
parent 23784f614b
commit f50773711e
2 changed files with 493 additions and 31 deletions

View File

@@ -101,6 +101,22 @@ def make_transfer_chain() -> TransferChain:
return chain
def make_fileitem(path: str, size: int = 1024) -> FileItem:
file_path = path
name = file_path.rsplit("/", 1)[-1]
suffix = name.rsplit(".", 1)[-1] if "." in name else ""
basename = name[: -(len(suffix) + 1)] if suffix else name
return FileItem(
storage="local",
path=file_path,
type="file",
name=name,
basename=basename,
extension=suffix,
size=size,
)
def migrate_to_media_job(jobview: JobManager, task: TransferTask):
task.mediainfo = FakeMedia()
jobview.migrate_task(task)
@@ -345,6 +361,177 @@ class TransferJobManagerTest(unittest.TestCase):
self.assertEqual([("abc123", "qbittorrent")], completed)
self.assertEqual([], chain.jobview.list_jobs())
def test_do_transfer_does_not_sync_extra_files_by_default(self):
chain = make_transfer_chain()
planned = []
main_fileitem = make_fileitem(
"/downloads/Test Show (2026)/Test.Show.S01E01.2026.mkv"
)
subtitle_fileitem = make_fileitem(
"/downloads/Test Show (2026)/Test.Show.S01E01.2026.zh-cn.srt"
)
chain._TransferChain__get_trans_fileitems = lambda fileitem, predicate: [
(main_fileitem, False)
]
chain._TransferChain__put_to_jobview = lambda task: True
chain._TransferChain__register_scrape_batch_task = lambda task: None
chain._TransferChain__close_scrape_batch = lambda batch_id: None
def fake_handle_transfer(task, callback=None):
planned.append(task.fileitem.path)
return True, ""
chain._TransferChain__handle_transfer = fake_handle_transfer
transfer_history_oper = SimpleNamespace(get_by_src=lambda src, storage=None: None)
download_history_oper = SimpleNamespace(
get_by_hash=lambda download_hash: None,
get_file_by_fullpath=lambda fullpath: None,
get_files_by_savepath=lambda savepath: [],
get_by_path=lambda path: None,
)
system_config_oper = SimpleNamespace(get=lambda key: None)
storage_chain = SimpleNamespace(
get_parent_item=lambda fileitem: FileItem(
storage="local",
path="/downloads/Test Show (2026)/",
type="dir",
name="Test Show (2026)",
),
list_files=lambda fileitem, recursion=False: [
main_fileitem,
subtitle_fileitem,
],
)
with patch(
"app.chain.transfer.TransferHistoryOper",
return_value=transfer_history_oper,
), patch(
"app.chain.transfer.DownloadHistoryOper",
return_value=download_history_oper,
), patch(
"app.chain.transfer.SystemConfigOper",
return_value=system_config_oper,
), patch(
"app.chain.transfer.StorageChain",
return_value=storage_chain,
):
state, errmsg = TransferChain.do_transfer(
chain,
fileitem=main_fileitem,
background=False,
)
self.assertTrue(state)
self.assertEqual("", errmsg)
self.assertEqual([main_fileitem.path], planned)
def test_do_transfer_syncs_matching_extra_files_for_each_main_video(self):
chain = make_transfer_chain()
planned = []
main_ep1_fileitem = make_fileitem(
"/downloads/Test Show (2026)/Test.Show.S01E01.2026.mkv"
)
main_ep2_fileitem = make_fileitem(
"/downloads/Test Show (2026)/Test.Show.S01E02.2026.mkv"
)
ep1_subtitle_fileitem = make_fileitem(
"/downloads/Test Show (2026)/Test.Show.S01E01.2026.zh-cn.srt"
)
ep1_audio_fileitem = make_fileitem(
"/downloads/Test Show (2026)/Test.Show.S01E01.2026.commentary.mka"
)
ep2_subtitle_fileitem = make_fileitem(
"/downloads/Test Show (2026)/Test.Show.S01E02.2026.zh-cn.srt"
)
other_title_fileitem = make_fileitem(
"/downloads/Test Show (2026)/Other.Show.S01E01.2026.zh-cn.srt"
)
parent_fileitem = FileItem(
storage="local",
path="/downloads/Test Show (2026)/",
type="dir",
name="Test Show (2026)",
)
chain._TransferChain__get_trans_fileitems = lambda fileitem, predicate: [
(main_ep1_fileitem, False),
(main_ep2_fileitem, False),
(ep1_subtitle_fileitem, False),
(ep1_audio_fileitem, False),
(ep2_subtitle_fileitem, False),
(other_title_fileitem, False),
]
chain._TransferChain__put_to_jobview = lambda task: True
chain._TransferChain__register_scrape_batch_task = lambda task: None
chain._TransferChain__close_scrape_batch = lambda batch_id: None
def fake_handle_transfer(task, callback=None):
planned.append((task.fileitem.path, task.meta.begin_episode))
return True, ""
chain._TransferChain__handle_transfer = fake_handle_transfer
transfer_history_oper = SimpleNamespace(get_by_src=lambda src, storage=None: None)
download_history_oper = SimpleNamespace(
get_by_hash=lambda download_hash: None,
get_file_by_fullpath=lambda fullpath: None,
get_files_by_savepath=lambda savepath: [],
get_by_path=lambda path: None,
)
system_config_oper = SimpleNamespace(get=lambda key: None)
list_files_calls = []
def fake_list_files(fileitem, recursion=False):
list_files_calls.append((fileitem.path, recursion))
return [
main_ep1_fileitem,
main_ep2_fileitem,
ep1_subtitle_fileitem,
ep1_audio_fileitem,
ep2_subtitle_fileitem,
other_title_fileitem,
]
storage_chain = SimpleNamespace(
get_parent_item=lambda fileitem: parent_fileitem,
list_files=fake_list_files,
)
with patch(
"app.chain.transfer.TransferHistoryOper",
return_value=transfer_history_oper,
), patch(
"app.chain.transfer.DownloadHistoryOper",
return_value=download_history_oper,
), patch(
"app.chain.transfer.SystemConfigOper",
return_value=system_config_oper,
), patch(
"app.chain.transfer.StorageChain",
return_value=storage_chain,
):
state, errmsg = TransferChain.do_transfer(
chain,
fileitem=parent_fileitem,
background=False,
sync_extra_files=True,
)
self.assertTrue(state)
self.assertEqual("", errmsg)
self.assertEqual(
[
(main_ep1_fileitem.path, 1),
(main_ep2_fileitem.path, 2),
(ep1_subtitle_fileitem.path, 1),
(ep1_audio_fileitem.path, 1),
(ep2_subtitle_fileitem.path, 2),
],
planned,
)
self.assertEqual([], list_files_calls)
def test_scrape_event_is_aggregated_by_transfer_batch_across_seasons(self):
chain = make_transfer_chain()
chain.eventmanager = MagicMock()