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
217 lines
6.8 KiB
Python
217 lines
6.8 KiB
Python
"""Tests for organizer module."""
|
|
|
|
import pytest
|
|
|
|
from tvh2jellyfin.organizer import (
|
|
generate_destination_path,
|
|
generate_destination_path_with_episode,
|
|
sanitize_directory_name,
|
|
sanitize_filename,
|
|
)
|
|
from tvh2jellyfin.tvdb import EpisodeInfo
|
|
from tvh2jellyfin.tvheadend import Recording
|
|
|
|
|
|
class TestSanitizeFilename:
|
|
"""Tests for filename sanitization."""
|
|
|
|
def test_removes_invalid_characters(self):
|
|
assert sanitize_filename('Test: "Show"') == "Test Show"
|
|
# Multiple spaces get normalized to single space
|
|
assert sanitize_filename("Show <1> | Part 2") == "Show 1 Part 2"
|
|
|
|
def test_normalizes_spaces(self):
|
|
assert sanitize_filename("Test Show") == "Test Show"
|
|
assert sanitize_filename("Test_Show") == "Test Show"
|
|
|
|
def test_strips_whitespace(self):
|
|
assert sanitize_filename(" Test Show ") == "Test Show"
|
|
|
|
def test_empty_returns_unknown(self):
|
|
assert sanitize_filename("") == "Unknown"
|
|
assert sanitize_filename(" ") == "Unknown"
|
|
|
|
|
|
class TestSanitizeDirectoryName:
|
|
"""Tests for directory name sanitization."""
|
|
|
|
def test_removes_trailing_dots(self):
|
|
assert sanitize_directory_name("Test Show.") == "Test Show"
|
|
assert sanitize_directory_name("Test Show...") == "Test Show"
|
|
|
|
def test_handles_empty(self):
|
|
assert sanitize_directory_name("") == "Unknown"
|
|
|
|
|
|
class TestGenerateDestinationPath:
|
|
"""Tests for destination path generation."""
|
|
|
|
@pytest.fixture
|
|
def recording_with_subtitle(self):
|
|
return Recording(
|
|
uuid="123",
|
|
title="Test Show",
|
|
subtitle="Episode Title",
|
|
filename="/recordings/test.ts",
|
|
filesize=1000000,
|
|
channel="BBC One",
|
|
start=1705312800,
|
|
stop=1705316400,
|
|
status="Completed OK",
|
|
)
|
|
|
|
@pytest.fixture
|
|
def recording_without_subtitle(self):
|
|
return Recording(
|
|
uuid="456",
|
|
title="Another Show",
|
|
subtitle="",
|
|
filename="/recordings/another.ts",
|
|
filesize=2000000,
|
|
channel="ITV",
|
|
start=1705312800,
|
|
stop=1705316400,
|
|
status="Completed OK",
|
|
)
|
|
|
|
def test_with_subtitle(self, recording_with_subtitle):
|
|
dest_dir, filename = generate_destination_path(
|
|
recording_with_subtitle, "/media/tv"
|
|
)
|
|
assert dest_dir == "/media/tv/Test Show"
|
|
assert filename == "Test Show - Episode Title.ts"
|
|
|
|
def test_without_subtitle(self, recording_without_subtitle):
|
|
dest_dir, filename = generate_destination_path(
|
|
recording_without_subtitle, "/media/tv"
|
|
)
|
|
assert dest_dir == "/media/tv/Another Show"
|
|
# Should include timestamp
|
|
assert filename.startswith("Another Show - ")
|
|
assert filename.endswith(".ts")
|
|
|
|
|
|
class TestGenerateDestinationPathWithEpisode:
|
|
"""Tests for destination path generation with episode info."""
|
|
|
|
@pytest.fixture
|
|
def recording(self):
|
|
return Recording(
|
|
uuid="123",
|
|
title="Coronation Street",
|
|
subtitle="",
|
|
filename="/recordings/Coronation-Street/Coronation Street2026-01-06.ts",
|
|
filesize=1000000,
|
|
channel="ITV",
|
|
start=1767744000, # 2026-01-06
|
|
stop=1767747600,
|
|
status="Completed OK",
|
|
)
|
|
|
|
@pytest.fixture
|
|
def episode_info(self):
|
|
return EpisodeInfo(
|
|
series_id=456,
|
|
series_name="Coronation Street",
|
|
season=67,
|
|
episode=4,
|
|
episode_name="Episode 4",
|
|
air_date="2026-01-06",
|
|
)
|
|
|
|
def test_with_episode_info(self, recording, episode_info):
|
|
"""Test Jellyfin naming format with episode info."""
|
|
dest_dir, filename = generate_destination_path_with_episode(
|
|
recording, "/media/tv", episode_info
|
|
)
|
|
assert dest_dir == "/media/tv/Coronation Street/Season 67"
|
|
assert filename == "Coronation Street - S67E04.ts"
|
|
|
|
def test_without_episode_info(self, recording):
|
|
"""Test fallback when episode info not available."""
|
|
dest_dir, filename = generate_destination_path_with_episode(
|
|
recording, "/media/tv", None
|
|
)
|
|
assert dest_dir == "/media/tv/Coronation Street"
|
|
# Should use fallback logic (timestamp)
|
|
assert filename.startswith("Coronation Street - ")
|
|
assert filename.endswith(".ts")
|
|
|
|
def test_uses_canonical_series_name(self, recording):
|
|
"""Test that canonical series name from TVDB is used."""
|
|
episode_info = EpisodeInfo(
|
|
series_id=456,
|
|
series_name="Doctor Who (2005)", # TVDB canonical name
|
|
season=14,
|
|
episode=1,
|
|
episode_name="The Church on Ruby Road",
|
|
air_date="2023-12-25",
|
|
)
|
|
|
|
# Recording might have different name
|
|
recording_with_diff_name = Recording(
|
|
uuid="789",
|
|
title="Doctor Who", # TVHeadend name
|
|
subtitle="",
|
|
filename="/recordings/doctor-who.ts",
|
|
filesize=3000000,
|
|
channel="BBC One",
|
|
start=1703512800,
|
|
stop=1703516400,
|
|
status="Completed OK",
|
|
)
|
|
|
|
dest_dir, filename = generate_destination_path_with_episode(
|
|
recording_with_diff_name, "/media/tv", episode_info
|
|
)
|
|
|
|
# Should use TVDB canonical name with season folder
|
|
assert dest_dir == "/media/tv/Doctor Who (2005)/Season 14"
|
|
assert filename == "Doctor Who (2005) - S14E01.ts"
|
|
|
|
def test_large_episode_numbers(self, recording):
|
|
"""Test with season/episode numbers > 99."""
|
|
episode_info = EpisodeInfo(
|
|
series_id=456,
|
|
series_name="Long Running Show",
|
|
season=67,
|
|
episode=125,
|
|
episode_name="Episode",
|
|
air_date="2026-01-06",
|
|
)
|
|
|
|
dest_dir, filename = generate_destination_path_with_episode(
|
|
recording, "/media/tv", episode_info
|
|
)
|
|
|
|
assert filename == "Long Running Show - S67E125.ts"
|
|
|
|
def test_preserves_file_extension(self, recording):
|
|
"""Test that file extension is preserved."""
|
|
recording_mkv = Recording(
|
|
uuid="123",
|
|
title="Test Show",
|
|
subtitle="",
|
|
filename="/recordings/test.mkv",
|
|
filesize=1000000,
|
|
channel="BBC",
|
|
start=1705312800,
|
|
stop=1705316400,
|
|
status="Completed OK",
|
|
)
|
|
|
|
episode_info = EpisodeInfo(
|
|
series_id=456,
|
|
series_name="Test Show",
|
|
season=1,
|
|
episode=1,
|
|
episode_name="Pilot",
|
|
air_date="2024-01-15",
|
|
)
|
|
|
|
_, filename = generate_destination_path_with_episode(
|
|
recording_mkv, "/media/tv", episode_info
|
|
)
|
|
|
|
assert filename.endswith(".mkv")
|