"""Download worker: argument construction, rejection classification, moves.""" import json from conftest import add_video from youtube_automate import config, download, videos class TestBuildArgs: def _row(self, conn, channel, **kwargs): add_video(conn, channel["id"], "vid1", **kwargs) return conn.execute( "SELECT v.*, c.dir_name, c.title AS channel_title, c.channel_id " "FROM video v JOIN channel c ON c.id = v.channel_pk " "WHERE v.video_id = 'vid1'" ).fetchone() def test_sort_puts_vcodec_before_res_and_res_before_acodec( self, conn, settings, channel ): """The original spec ordering selected 360p — see specs.md §6.""" args = download.build_args(settings, self._row(conn, channel)) sort = args[args.index("-S") + 1] assert sort == "vcodec:h264,res:720,acodec:aac" assert sort.index("vcodec") < sort.index("res") < sort.index("acodec") def test_format_selector_caps_height(self, conn, settings, channel): args = download.build_args(settings, self._row(conn, channel)) assert args[args.index("-f") + 1] == "bv*[height<=720]+ba/b[height<=720]" def test_max_height_setting_is_honoured(self, conn, settings, channel): settings.set("max_height", "480") args = download.build_args(settings, self._row(conn, channel)) assert "height<=480" in args[args.index("-f") + 1] assert "res:480" in args[args.index("-S") + 1] def test_merges_to_mp4(self, conn, settings, channel): args = download.build_args(settings, self._row(conn, channel)) assert args[args.index("--merge-output-format") + 1] == "mp4" def test_no_match_filter_for_uulf_rows(self, conn, settings, channel): args = download.build_args( settings, self._row(conn, channel, discovery_source=videos.SOURCE_UULF) ) assert "--match-filter" not in args def test_match_filter_applied_to_fallback_rows(self, conn, settings, channel): args = download.build_args( settings, self._row(conn, channel, discovery_source=videos.SOURCE_UC) ) assert "--match-filter" in args expression = args[args.index("--match-filter") + 1] assert "duration>?120" in expression # The `?` forms must be used so unknown values pass rather than reject. assert "live_status!=?is_live" in expression assert "live_status!=?is_upcoming" in expression assert "!was_live" in expression def test_min_duration_setting_flows_into_the_filter(self, conn, settings, channel): settings.set("min_duration_seconds", "60") args = download.build_args( settings, self._row(conn, channel, discovery_source=videos.SOURCE_UC) ) assert "duration>?60" in args[args.index("--match-filter") + 1] def test_subtitles_can_be_disabled(self, conn, settings, channel): settings.set("write_subs", "false") args = download.build_args(settings, self._row(conn, channel)) assert "--write-subs" not in args def test_sponsorblock_marks_rather_than_removes(self, conn, settings, channel): args = download.build_args(settings, self._row(conn, channel)) assert "--sponsorblock-mark" in args assert "--sponsorblock-remove" not in args assert "--embed-chapters" in args def test_targets_the_right_video(self, conn, settings, channel): args = download.build_args(settings, self._row(conn, channel)) assert args[-1] == "https://www.youtube.com/watch?v=vid1" class TestRejectionClassification: def test_not_a_rejection_when_no_marker(self): assert download._classify_rejection({}, "downloading", "") is None def test_upcoming_premiere_is_deferred_not_skipped(self): outcome = download._classify_rejection( {"live_status": "is_upcoming"}, "does not pass filter", "" ) assert outcome == videos.DEFERRED def test_live_is_skipped_permanently(self): assert ( download._classify_rejection( {"live_status": "is_live"}, "does not pass filter", "" ) == videos.SKIPPED_LIVE ) def test_past_livestream_is_skipped(self): assert ( download._classify_rejection( {"was_live": True}, "does not pass filter", "" ) == videos.SKIPPED_LIVE ) def test_otherwise_it_was_too_short(self): assert ( download._classify_rejection({"duration": 30}, "does not pass filter", "") == videos.SKIPPED_SHORT ) def test_missing_info_json_still_classifies(self): assert ( download._classify_rejection(None, "does not pass filter", "") == videos.SKIPPED_SHORT ) class TestSubtitleChoice: def test_prefers_plain_en(self, media_root): (config.WORK_DIR / "v.en.srt").write_text("a") (config.WORK_DIR / "v.en-orig.srt").write_text("b") assert download._choose_subtitle("v").name == "v.en.srt" def test_promotes_en_orig_when_alone(self, media_root): (config.WORK_DIR / "v.en-orig.srt").write_text("b") assert download._choose_subtitle("v").name == "v.en-orig.srt" def test_none_when_no_subtitles(self, media_root): assert download._choose_subtitle("v") is None class TestWorkDir: def test_cleanup_removes_only_that_video(self, media_root): (config.WORK_DIR / "keep.mp4").write_bytes(b"x") (config.WORK_DIR / "drop.mp4").write_bytes(b"x") (config.WORK_DIR / "drop.info.json").write_text("{}") download.cleanup_work("drop") assert (config.WORK_DIR / "keep.mp4").exists() assert not (config.WORK_DIR / "drop.mp4").exists() assert not (config.WORK_DIR / "drop.info.json").exists() def test_recover_orphans_clears_everything_but_the_ignore_marker(self, media_root): (config.WORK_DIR / "a.part").write_bytes(b"x") (config.WORK_DIR / "b.mp4").write_bytes(b"x") assert download.recover_orphans() == 2 assert [p.name for p in config.WORK_DIR.iterdir()] == [".ignore"] class TestMoveIntoPlace: def _row(self, conn, channel): return conn.execute( "SELECT v.*, c.dir_name, c.title AS channel_title, c.channel_id " "FROM video v JOIN channel c ON c.id = v.channel_pk " "WHERE v.video_id = 'vid1'" ).fetchone() def _artefacts(self, video_id="vid1"): (config.WORK_DIR / f"{video_id}.mp4").write_bytes(b"video-bytes") (config.WORK_DIR / f"{video_id}.info.json").write_text("{}") (config.WORK_DIR / f"{video_id}.jpg").write_bytes(b"jpg") (config.WORK_DIR / f"{video_id}.en.srt").write_text("1\n") def test_places_every_artefact_with_the_shared_stem( self, conn, channel, media_root ): add_video(conn, channel["id"], "vid1", upload_date="2026-08-11") self._artefacts() info = { "upload_date": "20260811", "title": "A Title: with colon", "description": "plot", "duration": 600, } rel_path, size = download._move_into_place(conn, self._row(conn, channel), info) placed = (media_root / rel_path).parent stem = "clabretro - S2026E8110 - A Title with colon [vid1]" assert {p.name for p in placed.iterdir()} == { f"{stem}.mp4", f"{stem}.nfo", f"{stem}.info.json", f"{stem}-thumb.jpg", f"{stem}.en.srt", } assert size == len(b"video-bytes") def test_work_dir_is_emptied_of_that_video(self, conn, channel, media_root): add_video(conn, channel["id"], "vid1", upload_date="2026-08-11") self._artefacts() download._move_into_place( conn, self._row(conn, channel), {"upload_date": "20260811", "title": "t"} ) assert list(config.WORK_DIR.glob("vid1.*")) == [] def test_database_row_records_the_result(self, conn, channel, media_root): add_video(conn, channel["id"], "vid1", upload_date="2026-08-11") self._artefacts() download._move_into_place( conn, self._row(conn, channel), {"upload_date": "20260811", "title": "t", "duration": 600}, ) row = videos.get(conn, "vid1") assert row["state"] == videos.DOWNLOADED assert row["season"] == 2026 assert row["episode"] == 8110 assert row["size_bytes"] == len(b"video-bytes") assert row["rel_path"].endswith(".mp4") def test_info_json_upload_date_beats_the_feed_date( self, conn, channel, media_root ): add_video(conn, channel["id"], "vid1", upload_date="2026-01-01") self._artefacts() download._move_into_place( conn, self._row(conn, channel), {"upload_date": "20260811", "title": "t"} ) row = videos.get(conn, "vid1") assert row["upload_date"] == "2026-08-11" assert row["episode"] == 8110 def test_missing_media_file_raises(self, conn, channel, media_root): add_video(conn, channel["id"], "vid1", upload_date="2026-08-11") (config.WORK_DIR / "vid1.info.json").write_text("{}") try: download._move_into_place( conn, self._row(conn, channel), {"upload_date": "20260811", "title": "t"} ) except FileNotFoundError: pass else: # pragma: no cover raise AssertionError("expected FileNotFoundError") def test_written_nfo_matches_the_episode_number(self, conn, channel, media_root): add_video(conn, channel["id"], "vid1", upload_date="2026-08-11") self._artefacts() rel_path, _ = download._move_into_place( conn, self._row(conn, channel), {"upload_date": "20260811", "title": "t", "description": "d", "duration": 60}, ) nfo_path = (media_root / rel_path).with_suffix(".nfo") content = nfo_path.read_text() assert "8110" in content assert "S2026E8110" in nfo_path.name class TestReadInfoJson: def test_returns_none_when_absent(self, media_root): assert download._read_info_json("nope") is None def test_returns_none_on_corrupt_json(self, media_root): (config.WORK_DIR / "v.info.json").write_text("{not json") assert download._read_info_json("v") is None def test_parses_valid_json(self, media_root): (config.WORK_DIR / "v.info.json").write_text(json.dumps({"title": "x"})) assert download._read_info_json("v")["title"] == "x"