Skip to content

Commit 1cc6172

Browse files
authored
Merge pull request #560 from arabcoders/dev
2 parents 0473c86 + 5844290 commit 1cc6172

File tree

2 files changed

+181
-4
lines changed

2 files changed

+181
-4
lines changed

app/library/downloads/core.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -393,7 +393,7 @@ def __getstate__(self) -> dict[str, Any]:
393393
"""
394394
state: dict[str, Any] = self.__dict__.copy()
395395

396-
excluded_keys: tuple[str, ...] = ("_notify",)
396+
excluded_keys: tuple[str, ...] = ("_notify", "_status_tracker")
397397
for key in excluded_keys:
398398
if key in state:
399399
state[key] = None

app/tests/test_download.py

Lines changed: 180 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import logging
22
import os
33
import signal
4+
from multiprocessing.reduction import ForkingPickler
45
from pathlib import Path
5-
from typing import Any
6+
from typing import Any, cast
67
from unittest.mock import MagicMock, Mock, patch
78

89
import pytest
910

11+
from app.library.Events import EventBus, Events
1012
from app.library.downloads import Download, NestedLogger, Terminator
1113
from app.library.downloads.hooks import HookHandlers
1214
from app.library.downloads.process_manager import ProcessManager
@@ -95,7 +97,7 @@ def emit(*_args, **_kwargs):
9597
def test_progress_hook_filters_fields(self) -> None:
9698
d = Download(make_item())
9799
q = DummyQueue()
98-
hooks = HookHandlers(d.id, q, d.logger, d.debug)
100+
hooks = HookHandlers(d.id, cast(Any, q), d.logger, d.debug)
99101

100102
payload = {
101103
"tmpfilename": "t",
@@ -131,7 +133,7 @@ def test_progress_hook_filters_fields(self) -> None:
131133
def test_post_hooks_pushes_filename(self) -> None:
132134
d = Download(make_item())
133135
q = DummyQueue()
134-
hooks = HookHandlers(d.id, q, d.logger, d.debug)
136+
hooks = HookHandlers(d.id, cast(Any, q), d.logger, d.debug)
135137
hooks.post_hook("name.ext")
136138
assert 1 == len(q.items), "Should have 1 item when filename is provided"
137139
assert q.items[0]["final_name"] == "name.ext", "Filename should match"
@@ -269,6 +271,181 @@ async def mock_executor(*args):
269271
mock_start.assert_called_once()
270272

271273

274+
class TestDownloadFlow:
275+
@pytest.mark.asyncio
276+
async def test_download_flow_inline_process(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
277+
class Cfg:
278+
debug = False
279+
ytdlp_debug = False
280+
max_workers = 1
281+
temp_keep = False
282+
temp_disabled = True
283+
download_info_expires = 3600
284+
285+
@staticmethod
286+
def get_instance():
287+
return Cfg
288+
289+
@staticmethod
290+
def get_manager():
291+
class DummyManager:
292+
def Queue(self):
293+
return DummyQueue()
294+
295+
return DummyManager()
296+
297+
monkeypatch.setattr("app.library.downloads.core.Config", Cfg)
298+
299+
class EB:
300+
@staticmethod
301+
def get_instance():
302+
return EB
303+
304+
@staticmethod
305+
def emit(*_args, **_kwargs):
306+
return None
307+
308+
monkeypatch.setattr("app.library.downloads.core.EventBus", EB)
309+
310+
item = ItemDTO(
311+
id="id1",
312+
title="T",
313+
url="http://u",
314+
folder="f",
315+
download_dir=str(tmp_path),
316+
temp_dir=str(tmp_path),
317+
)
318+
download = Download(info=item)
319+
320+
def fake_download():
321+
queue = download.status_queue
322+
assert queue is not None
323+
queue = cast(Any, queue)
324+
queue.put(
325+
{
326+
"id": download.id,
327+
"status": "downloading",
328+
"downloaded_bytes": 10,
329+
"total_bytes": 10,
330+
}
331+
)
332+
download._status_tracker = StatusTracker(
333+
info=download.info,
334+
download_id=download.id,
335+
download_dir=str(tmp_path),
336+
temp_path=None,
337+
status_queue=queue,
338+
logger=download.logger,
339+
debug=False,
340+
)
341+
queue.put(
342+
{
343+
"id": download.id,
344+
"status": "finished",
345+
"final_name": str(tmp_path / "video.mp4"),
346+
}
347+
)
348+
queue.put(Terminator())
349+
350+
download._download = fake_download
351+
352+
class InlineProcess:
353+
def __init__(self, target):
354+
self._target = target
355+
self.pid = 12345
356+
self.ident = 12345
357+
358+
def start(self):
359+
self._target()
360+
361+
def join(self):
362+
return 0
363+
364+
def is_alive(self):
365+
return False
366+
367+
def terminate(self):
368+
return None
369+
370+
def kill(self):
371+
return None
372+
373+
def close(self):
374+
return None
375+
376+
def create_process(target):
377+
inline_proc = InlineProcess(target)
378+
download._process_manager.proc = cast(Any, inline_proc)
379+
return download._process_manager.proc
380+
381+
def start_process():
382+
assert download._process_manager.proc is not None
383+
download._process_manager.proc.start()
384+
385+
monkeypatch.setattr(download._process_manager, "create_process", create_process)
386+
monkeypatch.setattr(download._process_manager, "start", start_process)
387+
388+
await download.start()
389+
390+
assert download.info.status == "finished", "Download should finish via inline process"
391+
assert download.info.filename == "video.mp4", "Final filename should be set from status update"
392+
393+
394+
class TestDownloadSpawnPickling:
395+
def setup_method(self):
396+
EventBus._reset_singleton()
397+
398+
def test_spawn_pickling_ignores_local_event_listener(
399+
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
400+
) -> None:
401+
class Cfg:
402+
debug = False
403+
ytdlp_debug = False
404+
max_workers = 1
405+
temp_keep = False
406+
temp_disabled = True
407+
download_info_expires = 3600
408+
409+
@staticmethod
410+
def get_instance():
411+
return Cfg
412+
413+
monkeypatch.setattr("app.library.downloads.core.Config", Cfg)
414+
415+
bus = EventBus.get_instance()
416+
417+
def local_event_handler(_event, _name, **_kwargs):
418+
return None
419+
420+
bus.subscribe(Events.LOG_INFO, local_event_handler, "local-event-handler")
421+
422+
item = ItemDTO(
423+
id="id1",
424+
title="T",
425+
url="http://u",
426+
folder="f",
427+
download_dir=str(tmp_path),
428+
temp_dir=str(tmp_path),
429+
)
430+
download = Download(info=item)
431+
download.status_queue = cast(Any, DummyQueue())
432+
assert download.status_queue is not None
433+
download._status_tracker = StatusTracker(
434+
info=item,
435+
download_id=download.id,
436+
download_dir=str(tmp_path),
437+
temp_path=None,
438+
status_queue=cast(Any, download.status_queue),
439+
logger=download.logger,
440+
debug=False,
441+
)
442+
443+
state = download.__getstate__()
444+
assert state.get("_status_tracker") is None, "StatusTracker should be excluded from pickled state"
445+
446+
ForkingPickler.dumps(download._download)
447+
448+
272449
class TestTempManager:
273450
def test_create_temp_path_when_disabled(self) -> None:
274451
info = make_item()

0 commit comments

Comments
 (0)