mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-07-20 04:02:03 +08:00
feat: add support for syncing matching subtitle and audio files with main media during transfer
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user