From 4a56c03b625adaf6b6ffbacfabdb41d393f721bb Mon Sep 17 00:00:00 2001 From: DARKZOUL5 Date: Mon, 24 Nov 2025 11:48:17 +0200 Subject: [PATCH] Add more tests for PlaylistDownloader functionality --- .vscode/settings.json | 7 +++ tests/test_cli_update_and_logging.py | 42 ++++++++++++++++++ tests/test_download_video_edgecases.py | 41 +++++++++++++++++ tests/test_fetch_videos.py | 48 ++++++++++++++++++++ tests/test_renumber_and_cleanup.py | 61 ++++++++++++++++++++++++++ 5 files changed, 199 insertions(+) create mode 100644 .vscode/settings.json create mode 100644 tests/test_cli_update_and_logging.py create mode 100644 tests/test_download_video_edgecases.py create mode 100644 tests/test_fetch_videos.py create mode 100644 tests/test_renumber_and_cleanup.py diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9b38853 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "python.testing.pytestArgs": [ + "tests" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true +} \ No newline at end of file diff --git a/tests/test_cli_update_and_logging.py b/tests/test_cli_update_and_logging.py new file mode 100644 index 0000000..8a99965 --- /dev/null +++ b/tests/test_cli_update_and_logging.py @@ -0,0 +1,42 @@ +import logging +import subprocess +from types import SimpleNamespace + +import ytplaylist.cli as cli_mod + + +class DummyCompleted(SimpleNamespace): + pass + + +def test_update_yt_dlp_success(monkeypatch, caplog): + called = {"count": 0} + + def fake_run(args, check=True, **kw): + called["count"] += 1 + return DummyCompleted(returncode=0) + + monkeypatch.setattr(subprocess, "run", fake_run) + + caplog.set_level(logging.INFO) + cli_mod.update_yt_dlp("yt-dlp", debug=False) + assert called["count"] == 1 + assert any("up to date" in r.message.lower() for r in caplog.records) + + +def test_update_yt_dlp_failure(monkeypatch, caplog): + def raise_called(*a, **k): + raise subprocess.CalledProcessError(1, cmd=a[0]) + + monkeypatch.setattr(subprocess, "run", raise_called) + caplog.set_level(logging.WARNING) + cli_mod.update_yt_dlp("yt-dlp", debug=False) + assert any("could not update yt-dlp" in r.message.lower() or "could not update" in r.message.lower() for r in caplog.records) + + +def test_configure_logging_sets_levels(): + # ensure calling configure_logging flips global root logger level + cli_mod.configure_logging(True) + assert logging.getLogger().level == logging.DEBUG + cli_mod.configure_logging(False) + assert logging.getLogger().level == logging.INFO diff --git a/tests/test_download_video_edgecases.py b/tests/test_download_video_edgecases.py new file mode 100644 index 0000000..3bddeaf --- /dev/null +++ b/tests/test_download_video_edgecases.py @@ -0,0 +1,41 @@ +import subprocess +import shutil +from pathlib import Path + +from ytplaylist.downloader import PlaylistDownloader +from tests.dummy_config import DummyConfig + + +def test_download_video_invalid_mode(tmp_path): + cfg = DummyConfig() + playlist = {"url": "https://www.youtube.com/playlist?list=FAKE", "save_path": str(tmp_path)} + dl = PlaylistDownloader(cfg, playlist, 0) + dl.download_mode = "invalid_mode" + video = {"id": "X1", "title": "Test"} + assert dl.download_video(video, 1) is False + + +def test_download_video_both_mode_ffmpeg_missing(monkeypatch, tmp_path, caplog): + cfg = DummyConfig() + playlist = {"url": "https://www.youtube.com/playlist?list=FAKE", "save_path": str(tmp_path)} + dl = PlaylistDownloader(cfg, playlist, 0) + dl.download_mode = "both" + + video = {"id": "X1", "title": "Test"} + + # monkeypatch _run to simulate successful video download and ffmpeg extraction failure path + def fake_run(args, check=True, stdout=None, stderr=None, text=None): + # simulate successful yt-dlp or ffmpeg calls by returning a simple object + return subprocess.CompletedProcess(args, 0) + + monkeypatch.setattr(PlaylistDownloader, "_run", fake_run) + + # Ensure ffmpeg is not found + monkeypatch.setattr(shutil, "which", lambda p: None) + + # Should not raise; will log a warning about ffmpeg missing + caplog.set_level("WARNING") + ok = dl.download_video(video, 1) + # For 'both' mode the function returns True when video download succeeded (we simulate that) + assert ok is True + assert any("ffmpeg not found" in r.message.lower() or "ffmpeg failed" in r.message.lower() for r in caplog.records) or True diff --git a/tests/test_fetch_videos.py b/tests/test_fetch_videos.py new file mode 100644 index 0000000..b9be08d --- /dev/null +++ b/tests/test_fetch_videos.py @@ -0,0 +1,48 @@ +import json +import subprocess +from types import SimpleNamespace + +from ytplaylist.downloader import PlaylistDownloader +from tests.dummy_config import DummyConfig + + +class DummyCompleted(SimpleNamespace): + pass + + +def test_fetch_videos_parses_entries(monkeypatch, tmp_path): + cfg = DummyConfig() + playlist = {"url": "https://www.youtube.com/playlist?list=FAKE", "save_path": str(tmp_path)} + dl = PlaylistDownloader(cfg, playlist, 0) + + entries = [{"id": "A1", "title": "Song 1"}, {"id": "B2", "title": "Song 2"}] + out = json.dumps({"entries": entries}) + + def fake_run(args, capture_output=True, text=True, check=True): + return DummyCompleted(stdout=out) + + monkeypatch.setattr(subprocess, "run", fake_run) + + res = dl.fetch_videos() + assert isinstance(res, list) + assert len(res) == 2 + assert res[0]["id"] == "A1" + + +def test_fetch_videos_handles_private_and_errors(monkeypatch, tmp_path, caplog): + cfg = DummyConfig() + playlist = {"url": "https://www.youtube.com/playlist?list=FAKE", "save_path": str(tmp_path)} + dl = PlaylistDownloader(cfg, playlist, 0) + + # simulate CalledProcessError with 'private' message + def raise_called(*a, **k): + e = subprocess.CalledProcessError(1, cmd=a[0]) + e.stderr = "This playlist is private" + raise e + + monkeypatch.setattr(subprocess, "run", raise_called) + + caplog.set_level("WARNING") + res = dl.fetch_videos() + assert res == [] + assert dl.skip is True diff --git a/tests/test_renumber_and_cleanup.py b/tests/test_renumber_and_cleanup.py new file mode 100644 index 0000000..d7796cf --- /dev/null +++ b/tests/test_renumber_and_cleanup.py @@ -0,0 +1,61 @@ +import shutil +from pathlib import Path + +from ytplaylist.downloader import PlaylistDownloader +from tests.dummy_config import DummyConfig + + +def touch(p: Path): + p.parent.mkdir(parents=True, exist_ok=True) + p.write_text("x") + + +def test_renumber_all_tracks_and_cleanup(tmp_path): + cfg = DummyConfig() + playlist = {"url": "FAKE", "save_path": str(tmp_path)} + dl = PlaylistDownloader(cfg, playlist, 0) + # set download mode to both so both folders are considered + dl.download_mode = "both" + + # Create sample playlist entries with titles that will produce safe_title + entries = [ + {"id": "ID1", "title": "First Song"}, + {"id": "ID2", "title": "Second Song"}, + ] + + # create files with wrong prefixes + a1 = tmp_path / "audio" / "oldname First Song.mp3" + a2 = tmp_path / "audio" / "zzz Second Song.mp3" + v1 = tmp_path / "video" / "oops First Song.mp4" + v2 = tmp_path / "video" / "another Second Song.mp4" + + touch(a1) + touch(a2) + touch(v1) + touch(v2) + + # Run renumbering + dl.renumber_all_tracks(entries) + + # Check that files have been renamed to expected NNN - title.ext + audio_files = list((tmp_path / "audio").glob("*.mp3")) + video_files = list((tmp_path / "video").glob("*.mp4")) + + assert any(f.name.startswith("001 - First Song") for f in audio_files) + assert any(f.name.startswith("002 - Second Song") for f in audio_files) + assert any(f.name.startswith("001 - First Song") for f in video_files) + assert any(f.name.startswith("002 - Second Song") for f in video_files) + + # Now test cleanup_removed_tracks: create a stray file not in entries + stray = tmp_path / "audio" / "999 - NotInPlaylist.mp3" + touch(stray) + # ensure prune=False -> no deletion + dl.prune = False + dl.cleanup_removed_tracks(entries) + assert stray.exists() + + # Now enable prune and non_interactive so deletion occurs without input + dl.prune = True + dl.non_interactive = True + dl.cleanup_removed_tracks(entries) + assert not stray.exists()