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:
2026-01-07 17:43:09 +00:00
commit a8b247a83f
25 changed files with 2962 additions and 0 deletions

View File

@@ -0,0 +1,117 @@
"""Tests for episode resolver."""
from unittest.mock import Mock
import pytest
from tvh2jellyfin.episode_resolver import EpisodeResolver
from tvh2jellyfin.tvdb import EpisodeInfo
class TestEpisodeResolver:
"""Tests for EpisodeResolver."""
@pytest.fixture
def mock_episode_info(self):
return EpisodeInfo(
series_id=456,
series_name="Test Show",
season=1,
episode=5,
episode_name="Pilot",
air_date="2024-01-15",
)
def test_lookup_tvdb_success(self, mock_episode_info):
"""Test successful lookup from TVDB."""
mock_tvdb = Mock()
mock_tvdb.lookup_episode.return_value = mock_episode_info
resolver = EpisodeResolver(tvdb_client=mock_tvdb, tvmaze_client=None)
result, source = resolver.lookup("Test Show", 1705312800)
assert result is not None
assert result.season == 1
assert result.episode == 5
assert source == "tvdb"
def test_lookup_tvmaze_fallback(self, mock_episode_info):
"""Test fallback to TVmaze when TVDB fails."""
mock_tvdb = Mock()
mock_tvdb.lookup_episode.return_value = None
mock_tvmaze = Mock()
mock_tvmaze.lookup_episode.return_value = mock_episode_info
resolver = EpisodeResolver(tvdb_client=mock_tvdb, tvmaze_client=mock_tvmaze)
result, source = resolver.lookup("Test Show", 1705312800)
assert result is not None
assert source == "tvmaze"
def test_lookup_tvdb_exception_fallback(self, mock_episode_info):
"""Test fallback to TVmaze when TVDB raises exception."""
mock_tvdb = Mock()
mock_tvdb.lookup_episode.side_effect = Exception("API Error")
mock_tvmaze = Mock()
mock_tvmaze.lookup_episode.return_value = mock_episode_info
resolver = EpisodeResolver(tvdb_client=mock_tvdb, tvmaze_client=mock_tvmaze)
result, source = resolver.lookup("Test Show", 1705312800)
assert result is not None
assert source == "tvmaze"
def test_lookup_both_fail(self):
"""Test when both sources fail."""
mock_tvdb = Mock()
mock_tvdb.lookup_episode.return_value = None
mock_tvmaze = Mock()
mock_tvmaze.lookup_episode.return_value = None
resolver = EpisodeResolver(tvdb_client=mock_tvdb, tvmaze_client=mock_tvmaze)
result, source = resolver.lookup("Unknown Show", 1705312800)
assert result is None
assert source == "none"
def test_lookup_no_clients(self):
"""Test when no clients are available."""
resolver = EpisodeResolver(tvdb_client=None, tvmaze_client=None)
result, source = resolver.lookup("Test Show", 1705312800)
assert result is None
assert source == "none"
def test_lookup_tvmaze_only(self, mock_episode_info):
"""Test with only TVmaze client."""
mock_tvmaze = Mock()
mock_tvmaze.lookup_episode.return_value = mock_episode_info
resolver = EpisodeResolver(tvdb_client=None, tvmaze_client=mock_tvmaze)
result, source = resolver.lookup("Test Show", 1705312800)
assert result is not None
assert source == "tvmaze"
def test_is_available_with_tvdb(self):
"""Test is_available with TVDB client."""
resolver = EpisodeResolver(tvdb_client=Mock(), tvmaze_client=None)
assert resolver.is_available() is True
def test_is_available_with_tvmaze(self):
"""Test is_available with TVmaze client."""
resolver = EpisodeResolver(tvdb_client=None, tvmaze_client=Mock())
assert resolver.is_available() is True
def test_is_available_with_both(self):
"""Test is_available with both clients."""
resolver = EpisodeResolver(tvdb_client=Mock(), tvmaze_client=Mock())
assert resolver.is_available() is True
def test_is_available_with_none(self):
"""Test is_available with no clients."""
resolver = EpisodeResolver(tvdb_client=None, tvmaze_client=None)
assert resolver.is_available() is False