Files
jellyfin-scene-search/tests/test_jellyfin.py
T
2026-10-06 15:59:12 +01:00

143 lines
4.8 KiB
Python

from __future__ import annotations
import io
import tempfile
import unittest
from pathlib import Path
from unittest.mock import Mock
from PIL import Image
from scene_search.jellyfin import (
JellyfinClient,
TrickplaySelection,
caption_context_from_tags,
route_override_from_tags,
)
class JellyfinIntegrationTests(unittest.TestCase):
def test_item_detail_uses_items_query_not_direct_item_route(self) -> None:
client = JellyfinClient("http://jellyfin.test", "token")
response = Mock()
response.ok = True
response.json.return_value = {
"Items": [
{
"Id": "item-1",
"Name": "Example",
"Tags": ["Tag"],
"MediaSources": [],
"Trickplay": {},
"Etag": "etag-1",
}
]
}
client._get = Mock(return_value=response)
result = client.item("item-1")
self.assertEqual(result["Id"], "item-1")
call = client._get.call_args
self.assertEqual(call.args[0], "/Items")
self.assertEqual(call.kwargs["params"]["Ids"], "item-1")
self.assertIn("Tags", call.kwargs["params"]["Fields"])
self.assertIn("MediaSources", call.kwargs["params"]["Fields"])
self.assertIn("Trickplay", call.kwargs["params"]["Fields"])
self.assertIn("Etag", call.kwargs["params"]["Fields"])
def test_select_trickplay_prefers_nearest_width(self) -> None:
item = {
"Trickplay": {
"source-1": {
"160": {
"Width": 160,
"Height": 90,
"Interval": 10000,
"ThumbnailCount": 20,
"TileWidth": 5,
"TileHeight": 4,
},
"320": {
"Width": 320,
"Height": 180,
"Interval": 10000,
"ThumbnailCount": 20,
"TileWidth": 5,
"TileHeight": 4,
},
}
}
}
selected = JellyfinClient.select_trickplay(item, preferred_width=300)
self.assertIsNotNone(selected)
assert selected is not None
self.assertEqual(selected.media_source_id, "source-1")
self.assertEqual(selected.width, 320)
self.assertEqual(selected.interval_ms, 10000)
self.assertEqual(selected.thumbnails_per_sheet, 20)
def test_download_trickplay_frames_splits_sheet(self) -> None:
client = JellyfinClient("http://jellyfin.test", "token")
selection = TrickplaySelection(
media_source_id="source-1",
width=10,
height=5,
interval_ms=10000,
thumbnail_count=4,
tile_columns=2,
tile_rows=2,
)
sheet = Image.new("RGB", (20, 10), "white")
payload = io.BytesIO()
sheet.save(payload, "JPEG")
response = Mock()
response.ok = True
response.content = payload.getvalue()
client._get = Mock(return_value=response)
with tempfile.TemporaryDirectory() as tmp:
paths = client.download_trickplay_frames(
"item-1",
selection,
Path(tmp),
)
self.assertEqual(len(paths), 4)
self.assertTrue(all(path.is_file() for path in paths))
call = client._get.call_args
self.assertIn("/Videos/item-1/Trickplay/10/0.jpg", call.args[0])
self.assertEqual(call.kwargs["params"]["MediaSourceId"], "source-1")
def test_jellyfin_tags_can_force_specialist_route(self) -> None:
override = route_override_from_tags(
["Archive", "scene-search:specialist"],
force_specialist_tag="scene-search:specialist",
force_general_tag="scene-search:general",
specialist_tags=(),
)
self.assertEqual(override, ("specialist", "jellyfin-force-tag"))
def test_configured_specialist_tag_routes_to_specialist(self) -> None:
override = route_override_from_tags(
["Custom Category"],
force_specialist_tag="scene-search:specialist",
force_general_tag="scene-search:general",
specialist_tags=("custom category",),
)
self.assertEqual(override, ("specialist", "jellyfin-specialist-tag"))
def test_control_tags_are_not_added_to_caption_context(self) -> None:
context = caption_context_from_tags(
["holiday", "scene-search:specialist", "beach"],
control_tags=("scene-search:specialist",),
)
self.assertEqual(context, "Jellyfin tags: holiday, beach")
if __name__ == "__main__":
unittest.main()