Files
tvh2jellyfin/tests/test_organizer.py
Dan Head a8b247a83f 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
2026-04-03 09:31:04 +01:00

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")