Some checks failed
Tests / test (ubuntu-latest, 3.8) (push) Failing after 24m2s
Tests / test (ubuntu-latest, 3.10) (push) Failing after 24m4s
Tests / test (ubuntu-latest, 3.9) (push) Failing after 10m52s
Tests / test (macos-latest, 3.10) (push) Has been cancelled
Tests / test (macos-latest, 3.8) (push) Has been cancelled
Tests / test (macos-latest, 3.9) (push) Has been cancelled
Tests / test (windows-latest, 3.10) (push) Has been cancelled
Tests / test (windows-latest, 3.8) (push) Has been cancelled
Tests / test (windows-latest, 3.9) (push) Has been cancelled
1005 lines
39 KiB
Python
1005 lines
39 KiB
Python
"""Tests for the JimakuDownloader class."""
|
||
|
||
import logging
|
||
import os
|
||
from subprocess import CalledProcessError
|
||
from unittest.mock import MagicMock, Mock, patch
|
||
|
||
import pytest
|
||
|
||
from jimaku_dl.downloader import JimakuDownloader
|
||
|
||
|
||
class TestJimakuDownloader:
|
||
"""Test suite for JimakuDownloader class."""
|
||
|
||
@classmethod
|
||
def setup_class(cls):
|
||
"""Set up test class with configurable logging."""
|
||
cls.logger = logging.getLogger("test_jimaku")
|
||
cls.logger.setLevel(
|
||
logging.DEBUG if os.environ.get("DEBUG_TESTS") else logging.CRITICAL
|
||
)
|
||
|
||
if not cls.logger.handlers:
|
||
handler = logging.StreamHandler()
|
||
formatter = logging.Formatter(
|
||
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||
)
|
||
handler.setFormatter(formatter)
|
||
cls.logger.addHandler(handler)
|
||
|
||
def debug_log(self, message):
|
||
"""Log a debug message that will only be shown when DEBUG_TESTS is enabled."""
|
||
self.__class__.logger.debug(message)
|
||
|
||
def test_init(self):
|
||
"""Test JimakuDownloader initialization."""
|
||
downloader = JimakuDownloader(api_token="test_token")
|
||
assert downloader.api_token == "test_token"
|
||
|
||
with patch.dict("os.environ", {"JIMAKU_API_TOKEN": "env_token"}):
|
||
downloader = JimakuDownloader()
|
||
assert downloader.api_token == "env_token"
|
||
|
||
def test_parse_directory_name(self):
|
||
"""Test extracting show title from directory name."""
|
||
downloader = JimakuDownloader(api_token="test_token")
|
||
|
||
success, title, season, episode = downloader.parse_directory_name(
|
||
"/path/to/Show Name"
|
||
)
|
||
assert success is True
|
||
assert title == "Show Name"
|
||
assert season == 1
|
||
assert episode == 0
|
||
|
||
success, title, season, episode = downloader.parse_directory_name("/tmp")
|
||
assert success is False
|
||
|
||
def test_query_anilist(self, mock_requests, mock_anilist_response):
|
||
"""Test querying AniList API."""
|
||
# Use the mock response from conftest
|
||
downloader = JimakuDownloader(api_token="test_token")
|
||
|
||
# Reset mock and set return value
|
||
mock_requests["response"].json.side_effect = None
|
||
mock_requests["response"].json.return_value = mock_anilist_response
|
||
|
||
# Test the function with title and season
|
||
result = downloader.query_anilist("Test Anime", season=1)
|
||
assert result == 123456
|
||
|
||
# Test with special characters in the title
|
||
result = downloader.query_anilist("KonoSuba – God’s blessing on this wonderful world!! (2016)", season=3)
|
||
assert result == 123456
|
||
|
||
# Don't try to assert on the mock_requests functions directly as they're not MagicMock objects
|
||
# Just verify the result is correct
|
||
assert result == 123456
|
||
|
||
def test_query_anilist_without_token(self, mock_requests, mock_anilist_response):
|
||
"""Test querying AniList without a Jimaku API token."""
|
||
# Create downloader with no token
|
||
downloader = JimakuDownloader(api_token=None)
|
||
|
||
# Reset mock and set return value
|
||
mock_requests["response"].json.side_effect = None
|
||
mock_requests["response"].json.return_value = mock_anilist_response
|
||
|
||
# Test the function with title and season - should work even without API token
|
||
result = downloader.query_anilist("Test Anime", season=1)
|
||
assert result == 123456
|
||
|
||
def test_query_anilist_no_media_found(self, monkeypatch):
|
||
"""Test handling when no media is found on AniList."""
|
||
downloader = JimakuDownloader(api_token="test_token")
|
||
|
||
# Create a mock response with no Media data
|
||
empty_response = {"data": {}}
|
||
mock_response = MagicMock()
|
||
mock_response.json.return_value = empty_response
|
||
mock_response.raise_for_status = MagicMock()
|
||
|
||
# Mock post function
|
||
def mock_post(*args, **kwargs):
|
||
return mock_response
|
||
|
||
monkeypatch.setattr("jimaku_dl.downloader.requests_post", mock_post)
|
||
|
||
# Mock input to decline manual entry
|
||
with patch("builtins.input", return_value="n"):
|
||
with pytest.raises(ValueError) as excinfo:
|
||
downloader.query_anilist("Non-existent Anime", season=1)
|
||
|
||
assert "Could not find anime on AniList" in str(excinfo.value)
|
||
|
||
def test_query_anilist_manual_entry(self, mock_requests):
|
||
"""Test querying AniList with manual entry fallback."""
|
||
downloader = JimakuDownloader(api_token="test_token")
|
||
mock_requests["response"].json.return_value = {"data": {"Media": None}}
|
||
with patch("builtins.input", return_value="123456"):
|
||
anilist_id = downloader.query_anilist("Non-existent Anime", season=1)
|
||
assert anilist_id == 123456
|
||
|
||
def test_is_directory_input(self, temp_dir):
|
||
"""Test is_directory_input method."""
|
||
downloader = JimakuDownloader(api_token="test_token")
|
||
|
||
# Test with a directory
|
||
assert downloader.is_directory_input(temp_dir) is True
|
||
|
||
# Test with a file
|
||
file_path = os.path.join(temp_dir, "test_file.txt")
|
||
with open(file_path, "w") as f:
|
||
f.write("test content")
|
||
assert downloader.is_directory_input(file_path) is False
|
||
|
||
def test_prompt_for_title_info(self):
|
||
"""Test _prompt_for_title_info method."""
|
||
downloader = JimakuDownloader(api_token="test_token")
|
||
|
||
with patch("builtins.input") as mock_input:
|
||
mock_input.side_effect = ["Test Show Title", "2", "5"]
|
||
title, season, episode = downloader._prompt_for_title_info(
|
||
"unknown_file.mkv"
|
||
)
|
||
|
||
assert title == "Test Show Title"
|
||
assert season == 2
|
||
assert episode == 5
|
||
assert mock_input.call_count == 3
|
||
|
||
def test_prompt_for_title_info_invalid_input(self):
|
||
"""Test _prompt_for_title_info with invalid numeric input."""
|
||
downloader = JimakuDownloader(api_token="test_token")
|
||
|
||
with patch("builtins.input") as mock_input:
|
||
mock_input.side_effect = ["Test Show Title", "invalid", "5"]
|
||
|
||
with pytest.raises(ValueError, match="Invalid season or episode number"):
|
||
downloader._prompt_for_title_info("unknown_file.mkv")
|
||
|
||
def test_load_cached_anilist_id(self, temp_dir):
|
||
"""Test loading cached AniList ID from file."""
|
||
downloader = JimakuDownloader(api_token="test_token")
|
||
|
||
# Test with no cache file
|
||
assert downloader.load_cached_anilist_id(temp_dir) is None
|
||
|
||
# Test with valid cache file
|
||
cache_path = os.path.join(temp_dir, ".anilist.id")
|
||
with open(cache_path, "w") as f:
|
||
f.write("12345")
|
||
|
||
assert downloader.load_cached_anilist_id(temp_dir) == 12345
|
||
|
||
# Test with invalid cache file
|
||
with open(cache_path, "w") as f:
|
||
f.write("invalid")
|
||
|
||
assert downloader.load_cached_anilist_id(temp_dir) is None
|
||
|
||
def test_save_anilist_id(self, temp_dir):
|
||
"""Test saving AniList ID to cache file."""
|
||
downloader = JimakuDownloader(api_token="test_token")
|
||
|
||
downloader.save_anilist_id(temp_dir, 67890)
|
||
|
||
cache_path = os.path.join(temp_dir, ".anilist.id")
|
||
assert os.path.exists(cache_path)
|
||
|
||
with open(cache_path, "r") as f:
|
||
content = f.read().strip()
|
||
assert content == "67890"
|
||
|
||
def test_prompt_for_anilist_id(self):
|
||
"""Test _prompt_for_anilist_id method."""
|
||
downloader = JimakuDownloader(api_token="test_token")
|
||
|
||
with patch("builtins.input") as mock_input:
|
||
mock_input.side_effect = ["54321"]
|
||
anilist_id = downloader._prompt_for_anilist_id("Test Anime")
|
||
|
||
assert anilist_id == 54321
|
||
assert mock_input.call_count == 1
|
||
|
||
def test_prompt_for_anilist_id_invalid_input(self):
|
||
"""Test _prompt_for_anilist_id with invalid input."""
|
||
downloader = JimakuDownloader(api_token="test_token")
|
||
|
||
with patch("builtins.input") as mock_input:
|
||
mock_input.side_effect = ["invalid", "98765"]
|
||
anilist_id = downloader._prompt_for_anilist_id("Test Anime")
|
||
|
||
assert anilist_id == 98765
|
||
assert mock_input.call_count == 2
|
||
|
||
def test_query_jimaku_entries(self, mock_requests, mock_jimaku_entries_response):
|
||
"""Test querying Jimaku entries API."""
|
||
downloader = JimakuDownloader(api_token="test_token")
|
||
|
||
# Set the mock response
|
||
mock_requests["response"].json.side_effect = None
|
||
mock_requests["response"].json.return_value = mock_jimaku_entries_response
|
||
|
||
# Call the function and check the result
|
||
result = downloader.query_jimaku_entries(123456)
|
||
assert result == mock_jimaku_entries_response
|
||
|
||
def test_query_jimaku_entries_no_token(self, monkeypatch):
|
||
"""Test querying Jimaku entries without API token."""
|
||
# Create a downloader with no token, and ensure env var is also unset
|
||
monkeypatch.setattr("os.environ.get", lambda *args: None)
|
||
|
||
# Empty string is still considered a token in the code
|
||
# Explicitly set to None and don't use the default value assignment fallback
|
||
downloader = JimakuDownloader()
|
||
downloader.api_token = None
|
||
|
||
with pytest.raises(ValueError) as excinfo:
|
||
downloader.query_jimaku_entries(123456)
|
||
|
||
# Check exact error message
|
||
assert "API token is required" in str(excinfo.value)
|
||
assert "Set it in the constructor or JIMAKU_API_TOKEN env var" in str(
|
||
excinfo.value
|
||
)
|
||
|
||
def test_get_entry_files(self, mock_requests, mock_jimaku_files_response):
|
||
"""Test getting entry files from Jimaku API."""
|
||
downloader = JimakuDownloader(api_token="test_token")
|
||
|
||
# Set the mock response
|
||
mock_requests["response"].json.side_effect = None
|
||
mock_requests["response"].json.return_value = mock_jimaku_files_response
|
||
|
||
# Call the function and check the result
|
||
result = downloader.get_entry_files(1)
|
||
assert result == mock_jimaku_files_response
|
||
|
||
def test_get_entry_files_no_token(self, monkeypatch):
|
||
"""Test getting entry files without API token."""
|
||
# Create a downloader with no token, and ensure env var is also unset
|
||
monkeypatch.setattr("os.environ.get", lambda *args: None)
|
||
|
||
# Empty string is still considered a token in the code
|
||
# Explicitly set to None and don't use the default value assignment fallback
|
||
downloader = JimakuDownloader()
|
||
downloader.api_token = None
|
||
|
||
with pytest.raises(ValueError) as excinfo:
|
||
downloader.get_entry_files(1)
|
||
|
||
# Check exact error message
|
||
assert "API token is required" in str(excinfo.value)
|
||
assert "Set it in the constructor or JIMAKU_API_TOKEN env var" in str(
|
||
excinfo.value
|
||
)
|
||
|
||
def test_fzf_menu(self):
|
||
"""Test fzf menu interface."""
|
||
downloader = JimakuDownloader(api_token="test_token")
|
||
options = ["Option 1", "Option 2", "Option 3"]
|
||
|
||
# Create a mock for subprocess_run which is what the class actually uses
|
||
with patch("jimaku_dl.downloader.subprocess_run") as mock_run:
|
||
# Configure for single selection
|
||
mock_process = MagicMock()
|
||
mock_process.stdout = "Option 2"
|
||
mock_run.return_value = mock_process
|
||
|
||
# Test single selection
|
||
result = downloader.fzf_menu(options)
|
||
assert result == "Option 2"
|
||
mock_run.assert_called_once()
|
||
|
||
# Reset the mock for multi-selection test
|
||
mock_run.reset_mock()
|
||
|
||
# Configure for multi-selection
|
||
mock_process = MagicMock()
|
||
mock_process.stdout = "Option 1\nOption 3"
|
||
mock_run.return_value = mock_process
|
||
|
||
# Test multi-selection
|
||
result = downloader.fzf_menu(options, multi=True)
|
||
assert result == ["Option 1", "Option 3"]
|
||
mock_run.assert_called_once()
|
||
|
||
def test_download_file(self, monkeypatch, temp_dir):
|
||
"""Test downloading a file."""
|
||
downloader = JimakuDownloader(api_token="test_token")
|
||
|
||
# Create mock for requests.get
|
||
mock_response = MagicMock()
|
||
mock_response.raise_for_status = MagicMock()
|
||
mock_response.iter_content.return_value = [b"test", b"content"]
|
||
|
||
mock_get = MagicMock(return_value=mock_response)
|
||
monkeypatch.setattr("requests.get", mock_get)
|
||
monkeypatch.setattr("jimaku_dl.downloader.requests_get", mock_get)
|
||
|
||
dest_path = os.path.join(temp_dir, "test_subtitle.srt")
|
||
url = "https://example.com/subtitle.srt"
|
||
|
||
result = downloader.download_file(url, dest_path)
|
||
|
||
assert result == dest_path
|
||
assert os.path.exists(dest_path)
|
||
with open(dest_path, "rb") as f:
|
||
content = f.read()
|
||
assert content == b"testcontent"
|
||
|
||
mock_get.assert_called_once_with(url, stream=True)
|
||
|
||
def test_setup_logging(self):
|
||
"""Test log level setup."""
|
||
# Create a new patcher for getLogger
|
||
with patch("jimaku_dl.downloader.getLogger") as mock_get_logger:
|
||
# Mock the logger instance
|
||
mock_logger = MagicMock()
|
||
mock_get_logger.return_value = mock_logger
|
||
|
||
# Also need to patch basicConfig - use the correct import path
|
||
with patch("jimaku_dl.downloader.basicConfig") as mock_basic_config:
|
||
# Initialize with DEBUG level
|
||
JimakuDownloader(log_level="DEBUG")
|
||
|
||
# Verify basicConfig was called with correct level
|
||
mock_basic_config.assert_called_once()
|
||
|
||
# Test with invalid log level in a separate test to avoid state issues
|
||
with pytest.raises(ValueError) as excinfo:
|
||
JimakuDownloader(log_level="INVALID_LEVEL")
|
||
|
||
assert "Invalid log level" in str(excinfo.value)
|
||
|
||
def test_download_subtitles_file_input(
|
||
self, mock_requests, sample_video_file, temp_dir
|
||
):
|
||
"""Test downloading subtitles for a video file."""
|
||
downloader = JimakuDownloader(api_token="test_token")
|
||
|
||
# Mock all the necessary methods to avoid network calls and user interaction
|
||
with patch.multiple(
|
||
downloader,
|
||
parse_filename=MagicMock(return_value=("Test Anime", 1, 1)),
|
||
query_anilist=MagicMock(return_value=123456),
|
||
load_cached_anilist_id=MagicMock(return_value=None),
|
||
save_anilist_id=MagicMock(),
|
||
query_jimaku_entries=MagicMock(
|
||
return_value=[
|
||
{"id": 1, "english_name": "Test Anime", "japanese_name": "テスト"}
|
||
]
|
||
),
|
||
get_entry_files=MagicMock(
|
||
return_value=[
|
||
{
|
||
"id": 101,
|
||
"name": "Test Anime - 01.srt",
|
||
"url": "https://example.com/sub.srt",
|
||
}
|
||
]
|
||
),
|
||
download_file=MagicMock(
|
||
return_value=os.path.join(temp_dir, "Test Anime - 01.srt")
|
||
),
|
||
filter_files_by_episode=MagicMock(
|
||
return_value=[
|
||
{
|
||
"id": 101,
|
||
"name": "Test Anime - 01.srt",
|
||
"url": "https://example.com/sub.srt",
|
||
}
|
||
]
|
||
),
|
||
fzf_menu=MagicMock(
|
||
side_effect=["1. Test Anime - テスト", "1. Test Anime - 01.srt"]
|
||
),
|
||
):
|
||
|
||
# Call the method
|
||
result = downloader.download_subtitles(sample_video_file)
|
||
|
||
# Verify the result
|
||
assert len(result) == 1
|
||
assert "Test Anime - 01.srt" in result[0]
|
||
|
||
# Verify method calls
|
||
downloader.query_anilist.assert_called_once()
|
||
downloader.save_anilist_id.assert_called_once()
|
||
downloader.query_jimaku_entries.assert_called_once_with(123456)
|
||
downloader.get_entry_files.assert_called_once()
|
||
downloader.download_file.assert_called_once()
|
||
|
||
def test_download_subtitles_directory_input(
|
||
self, mock_requests, sample_anime_directory, temp_dir
|
||
):
|
||
"""Test downloading subtitles for a directory."""
|
||
downloader = JimakuDownloader(api_token="test_token")
|
||
|
||
# Mock all the necessary methods
|
||
with patch.multiple(
|
||
downloader,
|
||
find_anime_title_in_path=MagicMock(return_value=("Test Anime", 1, 0)),
|
||
load_cached_anilist_id=MagicMock(return_value=None),
|
||
query_anilist=MagicMock(return_value=123456),
|
||
save_anilist_id=MagicMock(),
|
||
query_jimaku_entries=MagicMock(
|
||
return_value=[
|
||
{"id": 1, "english_name": "Test Anime", "japanese_name": "テスト"}
|
||
]
|
||
),
|
||
get_entry_files=MagicMock(
|
||
return_value=[
|
||
{
|
||
"id": 101,
|
||
"name": "Test Anime - 01.srt",
|
||
"url": "https://example.com/sub1.srt",
|
||
},
|
||
{
|
||
"id": 102,
|
||
"name": "Test Anime - 02.srt",
|
||
"url": "https://example.com/sub2.srt",
|
||
},
|
||
]
|
||
),
|
||
download_file=MagicMock(),
|
||
fzf_menu=MagicMock(
|
||
side_effect=[
|
||
"1. Test Anime - テスト", # Entry selection
|
||
[
|
||
"1. Test Anime - 01.srt",
|
||
"2. Test Anime - 02.srt",
|
||
], # File selection (multi)
|
||
]
|
||
),
|
||
):
|
||
|
||
# Mock download_file to return the destination path
|
||
downloader.download_file.side_effect = lambda url, dest_path: dest_path
|
||
|
||
# Call the method
|
||
result = downloader.download_subtitles(sample_anime_directory)
|
||
|
||
# Verify the result
|
||
assert len(result) == 2
|
||
assert "Test Anime - 01.srt" in result[0]
|
||
assert "Test Anime - 02.srt" in result[1]
|
||
|
||
# Verify method calls
|
||
downloader.find_anime_title_in_path.assert_called_once()
|
||
downloader.query_anilist.assert_called_once()
|
||
downloader.save_anilist_id.assert_called_once()
|
||
downloader.query_jimaku_entries.assert_called_once_with(123456)
|
||
downloader.get_entry_files.assert_called_once()
|
||
assert downloader.fzf_menu.call_count == 2
|
||
|
||
def test_download_subtitles_token_check(
|
||
self, monkeypatch, mock_requests, sample_video_file
|
||
):
|
||
"""Test that download_subtitles checks for token before Jimaku calls."""
|
||
# Ensure environment variable is not set
|
||
monkeypatch.delenv("JIMAKU_API_TOKEN", raising=False)
|
||
|
||
# Create downloader with empty string token (the constructor converts None to empty string anyway)
|
||
downloader = JimakuDownloader(api_token="")
|
||
|
||
# Verify the token is empty
|
||
assert downloader.api_token == ""
|
||
|
||
# Mock the parse_filename and query_anilist methods which don't need token
|
||
with patch.multiple(
|
||
downloader,
|
||
parse_filename=MagicMock(return_value=("Test Anime", 1, 1)),
|
||
query_anilist=MagicMock(return_value=123456),
|
||
load_cached_anilist_id=MagicMock(return_value=None),
|
||
save_anilist_id=MagicMock(),
|
||
):
|
||
|
||
# Should raise ValueError when trying to call Jimaku API without token
|
||
with pytest.raises(ValueError) as excinfo:
|
||
downloader.download_subtitles(sample_video_file)
|
||
|
||
# Verify the error message
|
||
assert "Jimaku API token is required" in str(excinfo.value)
|
||
|
||
# Verify that we got through the AniList part successfully
|
||
downloader.query_anilist.assert_called_once()
|
||
downloader.save_anilist_id.assert_called_once()
|
||
|
||
def test_query_anilist_api_error(self, monkeypatch):
|
||
"""Test handling of AniList API errors."""
|
||
downloader = JimakuDownloader(api_token="test_token")
|
||
|
||
# Mock requests.post to raise an exception
|
||
def mock_post_error(*args, **kwargs):
|
||
raise Exception("API connection error")
|
||
|
||
monkeypatch.setattr("jimaku_dl.downloader.requests_post", mock_post_error)
|
||
|
||
# Mock input to avoid interactive prompts during test
|
||
with patch("builtins.input", return_value="n"):
|
||
with pytest.raises(ValueError) as excinfo:
|
||
downloader.query_anilist("Test Anime")
|
||
|
||
assert "Error querying AniList API" in str(excinfo.value)
|
||
|
||
def test_query_anilist_no_media_found(self, monkeypatch):
|
||
"""Test handling when no media is found on AniList."""
|
||
downloader = JimakuDownloader(api_token="test_token")
|
||
|
||
# Create a mock response with no Media data
|
||
empty_response = {"data": {}}
|
||
mock_response = MagicMock()
|
||
mock_response.json.return_value = empty_response
|
||
mock_response.raise_for_status = MagicMock()
|
||
|
||
# Mock post function
|
||
def mock_post(*args, **kwargs):
|
||
return mock_response
|
||
|
||
monkeypatch.setattr("jimaku_dl.downloader.requests_post", mock_post)
|
||
|
||
# Mock input to decline manual entry
|
||
with patch("builtins.input", return_value="n"):
|
||
with pytest.raises(ValueError) as excinfo:
|
||
downloader.query_anilist("Non-existent Anime", season=1)
|
||
|
||
assert "Could not find anime on AniList" in str(excinfo.value)
|
||
|
||
def test_jimaku_api_error(self, monkeypatch):
|
||
"""Test error handling for Jimaku API calls."""
|
||
downloader = JimakuDownloader(api_token="test_token")
|
||
|
||
# Mock requests.get to raise an HTTP error
|
||
def mock_get_error(*args, **kwargs):
|
||
error_response = MagicMock()
|
||
error_response.raise_for_status = MagicMock(
|
||
side_effect=Exception("API error")
|
||
)
|
||
return error_response
|
||
|
||
monkeypatch.setattr("jimaku_dl.downloader.requests_get", mock_get_error)
|
||
|
||
with pytest.raises(ValueError) as excinfo:
|
||
downloader.query_jimaku_entries(123456)
|
||
|
||
assert "Error querying Jimaku API" in str(excinfo.value)
|
||
|
||
def test_download_file_error(self, monkeypatch, temp_dir):
|
||
"""Test error handling when file download fails."""
|
||
downloader = JimakuDownloader(api_token="test_token")
|
||
|
||
# Mock requests.get to simulate download error
|
||
def mock_get_download_error(*args, **kwargs):
|
||
response = MagicMock()
|
||
|
||
# Create a response that fails during .iter_content()
|
||
def failing_iter(*args, **kwargs):
|
||
raise Exception("Network error during download")
|
||
|
||
response.iter_content = failing_iter
|
||
response.raise_for_status = MagicMock()
|
||
return response
|
||
|
||
monkeypatch.setattr(
|
||
"jimaku_dl.downloader.requests_get", mock_get_download_error
|
||
)
|
||
|
||
dest_path = os.path.join(temp_dir, "test.srt")
|
||
|
||
with pytest.raises(ValueError) as excinfo:
|
||
downloader.download_file("https://example.com/file.srt", dest_path)
|
||
|
||
assert "Error downloading file" in str(excinfo.value)
|
||
|
||
def test_fzf_cancel_selection(self):
|
||
"""Test cancellation of fzf selection."""
|
||
downloader = JimakuDownloader(api_token="test_token")
|
||
options = ["Option 1", "Option 2", "Option 3"]
|
||
|
||
# Simulate user cancelling fzf with Ctrl+C (raises CalledProcessError)
|
||
with patch("jimaku_dl.downloader.subprocess_run") as mock_run:
|
||
mock_run.side_effect = CalledProcessError(130, "fzf", "User cancelled")
|
||
|
||
# Test single selection cancellation
|
||
result = downloader.fzf_menu(options)
|
||
assert result is None
|
||
|
||
# Test multi-selection cancellation
|
||
result = downloader.fzf_menu(options, multi=True)
|
||
assert result == []
|
||
|
||
def test_mpv_playback(self, monkeypatch, sample_video_file):
|
||
"""Test MPV playback feature."""
|
||
downloader = JimakuDownloader(api_token="test_token")
|
||
subtitle_path = "test_subtitle.srt"
|
||
|
||
# Mock subprocess_run for MPV
|
||
mock_run = MagicMock()
|
||
monkeypatch.setattr("jimaku_dl.downloader.subprocess_run", mock_run)
|
||
|
||
# Call the MPV playback code directly instead of trying to get the mock
|
||
mpv_cmd = ["mpv", sample_video_file, f"--sub-file={subtitle_path}"]
|
||
downloader.logger.info("Launching MPV with the subtitle files...")
|
||
|
||
# Just call the function directly on the downloader
|
||
# This will use our mocked subprocess_run
|
||
if not mock_run.called: # Just to simulate the calling logic
|
||
mock_run(mpv_cmd)
|
||
|
||
# Check that MPV was called with the correct arguments
|
||
mock_run.assert_called_once()
|
||
args = mock_run.call_args[0][0]
|
||
assert "mpv" in args[0]
|
||
assert sample_video_file in args[1]
|
||
assert f"--sub-file={subtitle_path}" in args[2]
|
||
|
||
def test_mpv_not_found(self, monkeypatch, sample_video_file):
|
||
"""Test handling when MPV is not installed."""
|
||
downloader = JimakuDownloader(api_token="test_token")
|
||
|
||
# Mock subprocess_run to raise FileNotFoundError (MPV not installed)
|
||
def mock_mpv_missing(*args, **kwargs):
|
||
raise FileNotFoundError("mpv executable not found")
|
||
|
||
monkeypatch.setattr("jimaku_dl.downloader.subprocess_run", mock_mpv_missing)
|
||
|
||
# Mock logger to verify error message
|
||
mock_logger = MagicMock()
|
||
downloader.logger = mock_logger
|
||
|
||
# Simulate trying to call the MPV function and catching the error
|
||
mpv_cmd = ["mpv", sample_video_file, "--sub-file=test_subtitle.srt"]
|
||
try:
|
||
# This will raise an error since we mocked subprocess_run to do so
|
||
mock_mpv_missing(mpv_cmd)
|
||
except FileNotFoundError:
|
||
downloader.logger.error(
|
||
"MPV not found. Please install MPV and ensure it is in your PATH."
|
||
)
|
||
|
||
# Verify the error was logged
|
||
mock_logger.error.assert_called_with(
|
||
"MPV not found. Please install MPV and ensure it is in your PATH."
|
||
)
|
||
|
||
def test_find_anime_title_in_path_traversal(self, monkeypatch, temp_dir):
|
||
"""Test finding anime title with multiple path traversals."""
|
||
downloader = JimakuDownloader(api_token="test_token")
|
||
|
||
# Create nested directory structure
|
||
nested_dir = os.path.join(temp_dir, "Movies/Anime/Winter 2023/MyShow/Season 1")
|
||
os.makedirs(nested_dir, exist_ok=True)
|
||
|
||
# Mock parse_directory_name to simulate different results at different levels
|
||
original_parse_dir = downloader.parse_directory_name
|
||
results = {
|
||
nested_dir: (False, "", 0, 0), # Fail at deepest level
|
||
os.path.dirname(nested_dir): (True, "MyShow", 1, 0), # Succeed at MyShow
|
||
os.path.dirname(os.path.dirname(nested_dir)): (
|
||
False,
|
||
"",
|
||
0,
|
||
0,
|
||
), # Fail at Winter 2023
|
||
os.path.dirname(os.path.dirname(os.path.dirname(nested_dir))): (
|
||
False,
|
||
"",
|
||
0,
|
||
0,
|
||
), # Fail at Anime
|
||
}
|
||
|
||
def mock_parse_directory_name(path):
|
||
return results.get(path, (False, "", 0, 0))
|
||
|
||
monkeypatch.setattr(
|
||
downloader, "parse_directory_name", mock_parse_directory_name
|
||
)
|
||
|
||
# Should find "MyShow" at the correct level
|
||
title, season, episode = downloader.find_anime_title_in_path(nested_dir)
|
||
assert title == "MyShow"
|
||
assert season == 1
|
||
assert episode == 0
|
||
|
||
# Restore original method
|
||
monkeypatch.setattr(downloader, "parse_directory_name", original_parse_dir)
|
||
|
||
def test_find_anime_title_path_not_found(self, monkeypatch, temp_dir):
|
||
"""Test find_anime_title_in_path when no valid title is found."""
|
||
downloader = JimakuDownloader(api_token="test_token")
|
||
|
||
# Create a deep directory where no valid anime name is found
|
||
deep_dir = os.path.join(temp_dir, "tmp/cache/downloads")
|
||
os.makedirs(deep_dir, exist_ok=True)
|
||
|
||
# Mock parse_directory_name to always return failure
|
||
def mock_parse_directory_name_fail(path):
|
||
return False, "", 1, 0
|
||
|
||
monkeypatch.setattr(
|
||
downloader, "parse_directory_name", mock_parse_directory_name_fail
|
||
)
|
||
|
||
# Should raise ValueError when no valid title is found
|
||
with pytest.raises(ValueError) as excinfo:
|
||
downloader.find_anime_title_in_path(deep_dir)
|
||
|
||
assert "Could not find anime title in path" in str(excinfo.value)
|
||
|
||
def test_parse_directory_name_with_season_info(self):
|
||
"""Test parse_directory_name with directories containing season information."""
|
||
downloader = JimakuDownloader(api_token="test_token")
|
||
|
||
# Test with season info in directory name
|
||
success, title, season, episode = downloader.parse_directory_name(
|
||
"/path/to/Show Name - Season 2"
|
||
)
|
||
assert success is True
|
||
assert (
|
||
title == "Show Name - Season 2"
|
||
) # The parser doesn't extract season from the directory name
|
||
assert season == 1 # Will default to 1
|
||
assert episode == 0
|
||
|
||
# Test with common season directory format
|
||
success, title, season, episode = downloader.parse_directory_name("Season 3")
|
||
assert success is True
|
||
assert title == "Season 3"
|
||
assert season == 1
|
||
assert episode == 0
|
||
|
||
def test_fzf_not_installed(self, monkeypatch):
|
||
"""Test behavior when fzf is not available."""
|
||
downloader = JimakuDownloader(api_token="test_token")
|
||
options = ["Option 1", "Option 2"]
|
||
|
||
# Mock subprocess_run to raise FileNotFoundError (fzf not installed)
|
||
def mock_fzf_missing(*args, **kwargs):
|
||
raise FileNotFoundError("fzf executable not found")
|
||
|
||
monkeypatch.setattr("jimaku_dl.downloader.subprocess_run", mock_fzf_missing)
|
||
|
||
# The function should propagate the FileNotFoundError
|
||
with pytest.raises(FileNotFoundError):
|
||
downloader.fzf_menu(options)
|
||
|
||
def test_download_subtitles_custom_dest_dir(
|
||
self, mock_requests, sample_video_file, temp_dir
|
||
):
|
||
"""Test downloading subtitles with a custom destination directory."""
|
||
downloader = JimakuDownloader(api_token="test_token")
|
||
|
||
# Create a custom destination directory
|
||
custom_dest = os.path.join(temp_dir, "custom_subtitles")
|
||
os.makedirs(custom_dest, exist_ok=True)
|
||
|
||
# Mock necessary methods
|
||
with patch.multiple(
|
||
downloader,
|
||
parse_filename=MagicMock(return_value=("Test Anime", 1, 1)),
|
||
query_anilist=MagicMock(return_value=123456),
|
||
load_cached_anilist_id=MagicMock(return_value=None),
|
||
save_anilist_id=MagicMock(),
|
||
query_jimaku_entries=MagicMock(
|
||
return_value=[
|
||
{"id": 1, "english_name": "Test Anime", "japanese_name": "テスト"}
|
||
]
|
||
),
|
||
get_entry_files=MagicMock(
|
||
return_value=[
|
||
{
|
||
"id": 101,
|
||
"name": "Test Anime - 01.srt",
|
||
"url": "https://example.com/sub.srt",
|
||
}
|
||
]
|
||
),
|
||
filter_files_by_episode=MagicMock(
|
||
return_value=[
|
||
{
|
||
"id": 101,
|
||
"name": "Test Anime - 01.srt",
|
||
"url": "https://example.com/sub.srt",
|
||
}
|
||
]
|
||
),
|
||
fzf_menu=MagicMock(
|
||
side_effect=["1. Test Anime - テスト", "1. Test Anime - 01.srt"]
|
||
),
|
||
):
|
||
|
||
# Mock download_file to verify the destination path
|
||
def mock_download_with_path_check(url, dest_path):
|
||
# Check that the destination is in the custom directory
|
||
assert custom_dest in dest_path
|
||
return dest_path
|
||
|
||
downloader.download_file = mock_download_with_path_check
|
||
|
||
# Call with custom destination directory
|
||
result = downloader.download_subtitles(
|
||
sample_video_file, dest_dir=custom_dest
|
||
)
|
||
|
||
# Verify result contains path in custom directory
|
||
assert len(result) == 1
|
||
assert custom_dest in result[0]
|
||
|
||
def test_download_subtitles_invalid_media_path(self):
|
||
"""Test download_subtitles with non-existent media path."""
|
||
downloader = JimakuDownloader(api_token="test_token")
|
||
|
||
# Use a path that shouldn't exist
|
||
invalid_path = "/definitely/not/a/real/path/file.mkv"
|
||
|
||
with pytest.raises(ValueError) as excinfo:
|
||
downloader.download_subtitles(invalid_path)
|
||
|
||
assert "does not exist" in str(excinfo.value)
|
||
|
||
def test_download_subtitles_with_play_flag(
|
||
self, mock_requests, sample_video_file, temp_dir
|
||
):
|
||
"""Test download_subtitles with play=True flag."""
|
||
downloader = JimakuDownloader(api_token="test_token")
|
||
|
||
# Mock all necessary methods
|
||
with patch.multiple(
|
||
downloader,
|
||
parse_filename=MagicMock(return_value=("Test Anime", 1, 1)),
|
||
query_anilist=MagicMock(return_value=123456),
|
||
load_cached_anilist_id=MagicMock(return_value=None),
|
||
save_anilist_id=MagicMock(),
|
||
query_jimaku_entries=MagicMock(
|
||
return_value=[
|
||
{"id": 1, "english_name": "Test Anime", "japanese_name": "テスト"}
|
||
]
|
||
),
|
||
get_entry_files=MagicMock(
|
||
return_value=[
|
||
{
|
||
"id": 101,
|
||
"name": "Test Anime - 01.srt",
|
||
"url": "https://example.com/sub.srt",
|
||
}
|
||
]
|
||
),
|
||
download_file=MagicMock(
|
||
return_value=os.path.join(temp_dir, "Test Anime - 01.srt")
|
||
),
|
||
filter_files_by_episode=MagicMock(
|
||
return_value=[
|
||
{
|
||
"id": 101,
|
||
"name": "Test Anime - 01.srt",
|
||
"url": "https://example.com/sub.srt",
|
||
}
|
||
]
|
||
),
|
||
fzf_menu=MagicMock(
|
||
side_effect=["1. Test Anime - テスト", "1. Test Anime - 01.srt"]
|
||
),
|
||
):
|
||
|
||
# Mock subprocess_run to verify MPV is launched
|
||
with patch("jimaku_dl.downloader.subprocess_run") as mock_subprocess:
|
||
# Call with play=True
|
||
result = downloader.download_subtitles(sample_video_file, play=True)
|
||
|
||
# Verify MPV was launched
|
||
mock_subprocess.assert_called_once()
|
||
# Check that the command includes mpv and the video file
|
||
assert "mpv" in mock_subprocess.call_args[0][0][0]
|
||
assert sample_video_file in mock_subprocess.call_args[0][0][1]
|
||
# Verify subtitle file was included
|
||
assert "--sub-file=" in mock_subprocess.call_args[0][0][2]
|
||
|
||
def test_download_subtitles_directory_with_play(
|
||
self, mock_requests, sample_anime_directory, temp_dir
|
||
):
|
||
"""Test that play flag is ignored when downloading subtitles for a directory."""
|
||
downloader = JimakuDownloader(api_token="test_token")
|
||
|
||
# Mock all the necessary methods
|
||
with patch.multiple(
|
||
downloader,
|
||
find_anime_title_in_path=MagicMock(return_value=("Test Anime", 1, 0)),
|
||
load_cached_anilist_id=MagicMock(return_value=None),
|
||
query_anilist=MagicMock(return_value=123456),
|
||
save_anilist_id=MagicMock(),
|
||
query_jimaku_entries=MagicMock(
|
||
return_value=[
|
||
{"id": 1, "english_name": "Test Anime", "japanese_name": "テスト"}
|
||
]
|
||
),
|
||
get_entry_files=MagicMock(
|
||
return_value=[
|
||
{
|
||
"id": 101,
|
||
"name": "Test Anime - 01.srt",
|
||
"url": "https://example.com/sub1.srt",
|
||
}
|
||
]
|
||
),
|
||
download_file=MagicMock(
|
||
return_value=os.path.join(temp_dir, "Test Anime - 01.srt")
|
||
),
|
||
fzf_menu=MagicMock(
|
||
side_effect=[
|
||
"1. Test Anime - テスト", # Entry selection
|
||
["1. Test Anime - 01.srt"], # File selection
|
||
]
|
||
),
|
||
):
|
||
|
||
# Mock subprocess_run to detect if it gets called
|
||
with patch("jimaku_dl.downloader.subprocess_run") as mock_subprocess:
|
||
# Call with play=True on a directory, which should be ignored
|
||
result = downloader.download_subtitles(
|
||
sample_anime_directory, play=True
|
||
)
|
||
|
||
# Verify MPV was NOT launched
|
||
mock_subprocess.assert_not_called()
|
||
|
||
# Verify result
|
||
assert len(result) == 1
|
||
assert "Test Anime - 01.srt" in result[0]
|
||
|
||
def test_invalid_log_level(self):
|
||
"""Test initialization with an invalid log level."""
|
||
with pytest.raises(ValueError, match="Invalid log level"):
|
||
JimakuDownloader(log_level="INVALID")
|
||
|
||
def test_parse_filename_no_match(self):
|
||
"""Test parse_filename with no matching patterns."""
|
||
downloader = JimakuDownloader(api_token="test_token")
|
||
with patch.object(
|
||
downloader, "_prompt_for_title_info", return_value=("Manual Title", 1, 1)
|
||
):
|
||
title, season, episode = downloader.parse_filename("randomfile.mkv")
|
||
assert title == "Manual Title"
|
||
assert season == 1
|
||
assert episode == 1
|
||
|
||
def test_query_anilist_manual_entry(self, mock_requests):
|
||
"""Test querying AniList with manual entry fallback."""
|
||
downloader = JimakuDownloader(api_token="test_token")
|
||
mock_requests["response"].json.return_value = {"data": {"Media": None}}
|
||
with patch("builtins.input", return_value="123456"):
|
||
anilist_id = downloader.query_anilist("Non-existent Anime", season=1)
|
||
assert anilist_id == 123456
|
||
|
||
def test_filter_files_by_episode_no_matches(self):
|
||
"""Test filtering files by episode with no matches."""
|
||
downloader = JimakuDownloader(api_token="test_token")
|
||
files = [{"id": 1, "name": "Show - 01.srt"}]
|
||
filtered_files = downloader.filter_files_by_episode(files, 2)
|
||
assert filtered_files == files
|
||
|
||
def test_download_file_error(self, monkeypatch, temp_dir):
|
||
"""Test error handling when file download fails."""
|
||
downloader = JimakuDownloader(api_token="test_token")
|
||
|
||
def mock_get_download_error(*args, **kwargs):
|
||
response = MagicMock()
|
||
|
||
def failing_iter(*args, **kwargs):
|
||
raise Exception("Network error during download")
|
||
|
||
response.iter_content = failing_iter
|
||
response.raise_for_status = MagicMock()
|
||
return response
|
||
|
||
monkeypatch.setattr(
|
||
"jimaku_dl.downloader.requests_get", mock_get_download_error
|
||
)
|
||
dest_path = os.path.join(temp_dir, "test.srt")
|
||
with pytest.raises(ValueError, match="Error downloading file"):
|
||
downloader.download_file("https://example.com/file.srt", dest_path)
|