A Docker-based Python application that transfers completed recordings from TVHeadend to Jellyfin media storage via SCP. Features: - Monitors TVHeadend for completed recordings via JSON API - Transfers files via SCP with SSH key authentication - Organizes files by show name - Deletes recordings from TVHeadend after successful transfer - Tracks transferred files to avoid duplicates - Dry run mode for testing without making changes
191 lines
6.2 KiB
Python
191 lines
6.2 KiB
Python
"""Tests for TheTVDB client."""
|
|
|
|
from unittest.mock import Mock, patch
|
|
|
|
import pytest
|
|
|
|
from tvh2jellyfin.tvdb import EpisodeInfo, TVDBClient, TVDBConfig
|
|
|
|
|
|
class TestEpisodeInfo:
|
|
"""Tests for EpisodeInfo dataclass."""
|
|
|
|
def test_formatted_episode_single_digit(self):
|
|
"""Test SxxExx formatting with single digit numbers."""
|
|
info = EpisodeInfo(
|
|
series_id=123,
|
|
series_name="Test Show",
|
|
season=1,
|
|
episode=5,
|
|
episode_name="Pilot",
|
|
air_date="2024-01-15",
|
|
)
|
|
assert info.formatted_episode == "S01E05"
|
|
|
|
def test_formatted_episode_double_digit(self):
|
|
"""Test SxxExx formatting with double digit numbers."""
|
|
info = EpisodeInfo(
|
|
series_id=123,
|
|
series_name="Test Show",
|
|
season=12,
|
|
episode=25,
|
|
episode_name="Episode",
|
|
air_date="2024-01-15",
|
|
)
|
|
assert info.formatted_episode == "S12E25"
|
|
|
|
def test_formatted_episode_large_numbers(self):
|
|
"""Test formatting with season/episode > 99."""
|
|
info = EpisodeInfo(
|
|
series_id=123,
|
|
series_name="Long Running Show",
|
|
season=67,
|
|
episode=125,
|
|
episode_name="Episode",
|
|
air_date="2024-01-15",
|
|
)
|
|
assert info.formatted_episode == "S67E125"
|
|
|
|
|
|
class TestTVDBClient:
|
|
"""Tests for TVDB API client."""
|
|
|
|
@pytest.fixture
|
|
def config(self):
|
|
return TVDBConfig(
|
|
api_key="test_key",
|
|
pin="",
|
|
cache_ttl=3600,
|
|
show_mapping_file=None,
|
|
enabled=True,
|
|
)
|
|
|
|
@pytest.fixture
|
|
def client(self, config):
|
|
with patch.object(TVDBClient, "_authenticate"):
|
|
return TVDBClient(config)
|
|
|
|
def test_cache_valid(self, client):
|
|
"""Test cache validity check."""
|
|
import time
|
|
|
|
# Recent timestamp should be valid
|
|
assert client._is_cache_valid(time.time() - 100)
|
|
|
|
# Old timestamp should be invalid
|
|
assert not client._is_cache_valid(time.time() - 7200)
|
|
|
|
@patch("requests.Session.post")
|
|
def test_authenticate(self, mock_post, config):
|
|
"""Test authentication flow."""
|
|
mock_post.return_value.json.return_value = {"data": {"token": "test_token"}}
|
|
mock_post.return_value.raise_for_status = Mock()
|
|
|
|
client = TVDBClient(config)
|
|
client._authenticate()
|
|
|
|
assert client._token == "test_token"
|
|
mock_post.assert_called_once()
|
|
|
|
@patch("requests.Session.get")
|
|
@patch("requests.Session.post")
|
|
def test_search_series_success(self, mock_post, mock_get, config):
|
|
"""Test successful series search."""
|
|
mock_post.return_value.json.return_value = {"data": {"token": "test_token"}}
|
|
mock_post.return_value.raise_for_status = Mock()
|
|
|
|
mock_get.return_value.json.return_value = {
|
|
"data": [{"tvdb_id": 456, "name": "The Test Show"}]
|
|
}
|
|
mock_get.return_value.raise_for_status = Mock()
|
|
|
|
client = TVDBClient(config)
|
|
result = client.search_series("Test Show")
|
|
|
|
assert result is not None
|
|
assert result[0] == 456
|
|
assert result[1] == "The Test Show"
|
|
|
|
@patch("requests.Session.get")
|
|
@patch("requests.Session.post")
|
|
def test_search_series_not_found(self, mock_post, mock_get, config):
|
|
"""Test series search with no results."""
|
|
mock_post.return_value.json.return_value = {"data": {"token": "test_token"}}
|
|
mock_post.return_value.raise_for_status = Mock()
|
|
|
|
mock_get.return_value.json.return_value = {"data": []}
|
|
mock_get.return_value.raise_for_status = Mock()
|
|
|
|
client = TVDBClient(config)
|
|
result = client.search_series("Nonexistent Show")
|
|
|
|
assert result is None
|
|
|
|
@patch("tvh2jellyfin.tvdb.requests.Session")
|
|
def test_lookup_episode_success(self, mock_session_class, config):
|
|
"""Test successful episode lookup."""
|
|
from datetime import datetime
|
|
|
|
mock_session = Mock()
|
|
mock_session_class.return_value = mock_session
|
|
|
|
# Use a timestamp and derive the expected air date from it
|
|
timestamp = 1767744000
|
|
expected_air_date = datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d")
|
|
|
|
# Mock authentication
|
|
mock_session.post.return_value.json.return_value = {"data": {"token": "test_token"}}
|
|
mock_session.post.return_value.raise_for_status = Mock()
|
|
|
|
# Mock search and episode responses
|
|
mock_session.get.return_value.raise_for_status = Mock()
|
|
mock_session.get.return_value.json.side_effect = [
|
|
{"data": [{"tvdb_id": 456, "name": "Coronation Street"}]},
|
|
{
|
|
"data": {
|
|
"name": "Coronation Street",
|
|
"episodes": [
|
|
{
|
|
"seasonNumber": 67,
|
|
"number": 4,
|
|
"name": "Episode 4",
|
|
"aired": expected_air_date,
|
|
}
|
|
],
|
|
},
|
|
"links": {},
|
|
},
|
|
]
|
|
|
|
client = TVDBClient(config)
|
|
result = client.lookup_episode("Coronation Street", timestamp)
|
|
|
|
assert result is not None
|
|
assert result.season == 67
|
|
assert result.episode == 4
|
|
assert result.formatted_episode == "S67E04"
|
|
|
|
def test_show_mapping_file(self, tmp_path, config):
|
|
"""Test show name mapping from file."""
|
|
mapping_file = tmp_path / "mapping.json"
|
|
mapping_file.write_text('{"Doctor Who": "Doctor Who (2005)"}')
|
|
|
|
config.show_mapping_file = str(mapping_file)
|
|
|
|
with patch.object(TVDBClient, "_authenticate"):
|
|
client = TVDBClient(config)
|
|
|
|
assert client._show_mapping.get("Doctor Who") == "Doctor Who (2005)"
|
|
|
|
def test_caching_prevents_duplicate_calls(self, client):
|
|
"""Test that cache prevents duplicate API calls."""
|
|
import time
|
|
|
|
# Manually populate cache
|
|
client._series_cache["test show"] = (123, "Test Show", time.time())
|
|
|
|
# Search should use cache, not make API call
|
|
result = client.search_series("test show")
|
|
|
|
assert result == (123, "Test Show")
|