Initial commit: TVH2Jellyfin application
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
This commit is contained in:
190
tests/test_tvdb.py
Normal file
190
tests/test_tvdb.py
Normal file
@@ -0,0 +1,190 @@
|
||||
"""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")
|
||||
Reference in New Issue
Block a user