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:
216
tests/test_organizer.py
Normal file
216
tests/test_organizer.py
Normal file
@@ -0,0 +1,216 @@
|
||||
"""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")
|
||||
Reference in New Issue
Block a user