feat(client): initial implementation of jellyfin htpc client with yatse remote and youtube playback
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from src.config import AppConfig
|
||||
|
||||
|
||||
def test_default_config():
|
||||
cfg = AppConfig.load()
|
||||
assert cfg.jellyfin.client_name == "Jellyfin HTPC Client"
|
||||
assert cfg.player.hwdec == "auto"
|
||||
assert cfg.kodi_rpc.http_port == 8080
|
||||
assert cfg.web_api.port == 8097
|
||||
|
||||
|
||||
def test_custom_yaml_config():
|
||||
yaml_content = """
|
||||
jellyfin:
|
||||
server_url: "https://media.lan"
|
||||
username: "myuser"
|
||||
password: "mypassword"
|
||||
player:
|
||||
hwdec: "vaapi"
|
||||
fullscreen: false
|
||||
kodi_rpc:
|
||||
http_port: 8088
|
||||
"""
|
||||
with tempfile.NamedTemporaryFile("w", suffix=".yaml", delete=False) as f:
|
||||
f.write(yaml_content)
|
||||
f_path = f.name
|
||||
|
||||
try:
|
||||
cfg = AppConfig.load(f_path)
|
||||
assert cfg.jellyfin.server_url == "https://media.lan"
|
||||
assert cfg.jellyfin.username == "myuser"
|
||||
assert cfg.player.hwdec == "vaapi"
|
||||
assert cfg.player.fullscreen is False
|
||||
assert cfg.kodi_rpc.http_port == 8088
|
||||
finally:
|
||||
Path(f_path).unlink()
|
||||
@@ -0,0 +1,83 @@
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
from src.config import KodiRpcConfig
|
||||
from src.player.models import MediaMetadata, PlaybackState, PlayerStatus
|
||||
from src.player.mpv_controller import MpvController
|
||||
from src.remotes.kodi_rpc import KodiRpcServer
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_player():
|
||||
player = MagicMock(spec=MpvController)
|
||||
player.status = PlayerStatus(
|
||||
state=PlaybackState.PLAYING,
|
||||
current_time=65.5,
|
||||
duration=300.0,
|
||||
volume=85,
|
||||
muted=False,
|
||||
metadata=MediaMetadata(
|
||||
url="https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
||||
title="Never Gonna Give You Up",
|
||||
artist="Rick Astley",
|
||||
duration=300.0,
|
||||
)
|
||||
)
|
||||
player.play_url = AsyncMock()
|
||||
player.pause = AsyncMock()
|
||||
player.resume = AsyncMock()
|
||||
player.play_pause = AsyncMock()
|
||||
player.stop = AsyncMock()
|
||||
player.seek = AsyncMock()
|
||||
player.set_volume = AsyncMock()
|
||||
player.set_mute = AsyncMock()
|
||||
player.toggle_mute = AsyncMock()
|
||||
player.add_listener = MagicMock()
|
||||
return player
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_kodi_rpc_ping(mock_player):
|
||||
rpc = KodiRpcServer(KodiRpcConfig(), mock_player)
|
||||
resp = await rpc._dispatch({"jsonrpc": "2.0", "method": "JSONRPC.Ping", "id": 1})
|
||||
assert resp["result"] == "pong"
|
||||
assert resp["id"] == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_kodi_rpc_active_players(mock_player):
|
||||
rpc = KodiRpcServer(KodiRpcConfig(), mock_player)
|
||||
resp = await rpc._dispatch({"jsonrpc": "2.0", "method": "Player.GetActivePlayers", "id": 2})
|
||||
assert len(resp["result"]) == 1
|
||||
assert resp["result"][0]["playerid"] == 1
|
||||
assert resp["result"][0]["type"] == "video"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_kodi_rpc_player_properties(mock_player):
|
||||
rpc = KodiRpcServer(KodiRpcConfig(), mock_player)
|
||||
resp = await rpc._dispatch({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "Player.GetProperties",
|
||||
"params": {"playerid": 1, "properties": ["percentage", "time", "totaltime", "speed"]},
|
||||
"id": 3
|
||||
})
|
||||
result = resp["result"]
|
||||
assert result["speed"] == 1
|
||||
assert result["time"]["minutes"] == 1
|
||||
assert result["time"]["seconds"] == 5
|
||||
assert result["totaltime"]["minutes"] == 5
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_kodi_rpc_open_youtube(mock_player):
|
||||
rpc = KodiRpcServer(KodiRpcConfig(), mock_player)
|
||||
resp = await rpc._dispatch({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "Player.Open",
|
||||
"params": {"item": {"file": "plugin://plugin.video.youtube/play/?video_id=dQw4w9WgXcQ"}},
|
||||
"id": 4
|
||||
})
|
||||
assert resp["result"] == "OK"
|
||||
assert mock_player.play_url.called
|
||||
args, kwargs = mock_player.play_url.call_args
|
||||
assert "dQw4w9WgXcQ" in args[0]
|
||||
@@ -0,0 +1,42 @@
|
||||
import pytest
|
||||
from src.utils.youtube import extract_youtube_video_id, is_youtube_url, get_canonical_youtube_url
|
||||
|
||||
|
||||
def test_extract_standard_watch_url():
|
||||
url = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
|
||||
assert extract_youtube_video_id(url) == "dQw4w9WgXcQ"
|
||||
assert is_youtube_url(url) is True
|
||||
|
||||
|
||||
def test_extract_short_url():
|
||||
url = "https://youtu.be/dQw4w9WgXcQ?si=abcdef123456"
|
||||
assert extract_youtube_video_id(url) == "dQw4w9WgXcQ"
|
||||
assert is_youtube_url(url) is True
|
||||
|
||||
|
||||
def test_extract_shorts_url():
|
||||
url = "https://youtube.com/shorts/dQw4w9WgXcQ"
|
||||
assert extract_youtube_video_id(url) == "dQw4w9WgXcQ"
|
||||
assert is_youtube_url(url) is True
|
||||
|
||||
|
||||
def test_extract_kodi_plugin_url():
|
||||
url = "plugin://plugin.video.youtube/play/?video_id=dQw4w9WgXcQ"
|
||||
assert extract_youtube_video_id(url) == "dQw4w9WgXcQ"
|
||||
assert is_youtube_url(url) is True
|
||||
|
||||
|
||||
def test_extract_embed_url():
|
||||
url = "https://www.youtube.com/embed/dQw4w9WgXcQ"
|
||||
assert extract_youtube_video_id(url) == "dQw4w9WgXcQ"
|
||||
assert is_youtube_url(url) is True
|
||||
|
||||
|
||||
def test_non_youtube_url():
|
||||
url = "https://example.com/video.mp4"
|
||||
assert extract_youtube_video_id(url) is None
|
||||
assert is_youtube_url(url) is False
|
||||
|
||||
|
||||
def test_canonical_url():
|
||||
assert get_canonical_youtube_url("dQw4w9WgXcQ") == "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
|
||||
Reference in New Issue
Block a user