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

121
tests/test_tvmaze.py Normal file
View File

@@ -0,0 +1,121 @@
"""Tests for TVmaze client."""
from unittest.mock import Mock, patch
import pytest
from tvh2jellyfin.tvmaze import TVMazeClient, TVMazeConfig
class TestTVMazeClient:
"""Tests for TVmaze API client."""
@pytest.fixture
def config(self):
return TVMazeConfig(cache_ttl=3600, enabled=True)
@pytest.fixture
def client(self, config):
return TVMazeClient(config)
@patch("requests.Session.get")
def test_search_series_success(self, mock_get, client):
"""Test successful series search."""
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {
"id": 789,
"name": "Coronation Street",
}
mock_get.return_value.raise_for_status = Mock()
result = client.search_series("Coronation Street")
assert result is not None
assert result[0] == 789
assert result[1] == "Coronation Street"
@patch("requests.Session.get")
def test_search_series_not_found(self, mock_get, client):
"""Test series search with 404 response."""
mock_get.return_value.status_code = 404
result = client.search_series("Nonexistent Show")
assert result is None
@patch("requests.Session.get")
def test_get_episode_by_date_success(self, mock_get, client):
"""Test successful episode lookup by date."""
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = [
{"season": 67, "number": 4, "name": "Episode 4"}
]
mock_get.return_value.raise_for_status = Mock()
result = client.get_episode_by_date(789, "2026-01-06", "Coronation Street")
assert result is not None
assert result.season == 67
assert result.episode == 4
assert result.series_name == "Coronation Street"
@patch("requests.Session.get")
def test_get_episode_by_date_not_found(self, mock_get, client):
"""Test episode lookup with no results."""
mock_get.return_value.status_code = 404
result = client.get_episode_by_date(789, "2026-01-01", "Coronation Street")
assert result is None
@patch("requests.Session.get")
def test_lookup_episode_full_flow(self, mock_get, client):
"""Test full episode lookup flow."""
# Set up responses for search and episode lookup
mock_get.return_value.status_code = 200
mock_get.return_value.json.side_effect = [
{"id": 789, "name": "Coronation Street"}, # Search response
[{"season": 67, "number": 4, "name": "Episode 4"}], # Episode response
]
mock_get.return_value.raise_for_status = Mock()
# Timestamp for 2026-01-06
result = client.lookup_episode("Coronation Street", 1767744000)
assert result is not None
assert result.series_name == "Coronation Street"
assert result.formatted_episode == "S67E04"
def test_caching_series(self, client):
"""Test that series cache prevents duplicate calls."""
import time
# Manually populate cache
client._series_cache["coronation street"] = (789, "Coronation Street", time.time())
# Should use cache
result = client.search_series("coronation street")
assert result == (789, "Coronation Street")
def test_caching_episodes(self, client):
"""Test that episode cache prevents duplicate calls."""
import time
from tvh2jellyfin.tvdb import EpisodeInfo
cached_episode = EpisodeInfo(
series_id=789,
series_name="Coronation Street",
season=67,
episode=4,
episode_name="Episode 4",
air_date="2026-01-06",
)
client._episode_cache["789:2026-01-06"] = (cached_episode, time.time())
# Should use cache
result = client.get_episode_by_date(789, "2026-01-06", "Coronation Street")
assert result is not None
assert result.season == 67