import importlib
import re
import sys
from datetime import date
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import MagicMock, call
import pytest
PLAYER_PATH = Path(__file__).resolve().parents[2] / "lib" / "player.py"
def test_monitor_finally_uses_non_destructive_cleanup():
source = PLAYER_PATH.read_text()
monitor_match = re.search(
r"def monitor\(self\):(?P<body>.*?)def handle_subtitles", source, re.S
)
assert monitor_match is not None
monitor_body = monitor_match.group("body")
finally_match = re.search(r"finally:\n(?P<body>(?:\s+.*\n)+)", monitor_body)
assert finally_match is not None
finally_body = finally_match.group("body")
assert "self.cancel_playback()" not in finally_body
def test_cancel_playback_signals_kodi_and_stops_player():
source = PLAYER_PATH.read_text()
cancel_match = re.search(
r"def cancel_playback\(self\):(?P<body>.*?)def _cleanup_playback_session",
source,
re.S,
)
assert cancel_match is not None
cancel_body = cancel_match.group("body")
assert "self._cleanup_playback_session()" in cancel_body
# Must signal Kodi to avoid spinners (setResolvedUrl with False)
assert "setResolvedUrl(ADDON_HANDLE, False, ListItem(offscreen=True))" in cancel_body
# Must actually stop playback started by Player.play()
assert "self.stop()" in cancel_body
def test_play_video_always_uses_player_play_not_resolved_url(monkeypatch):
player_module = _player_module(monkeypatch)
test_player = _player_for_episode(player_module)
test_player.url = "http://stream"
test_player.play = MagicMock()
test_player.monitor = MagicMock()
test_player.cancel_playback = MagicMock()
test_player._check_volume = MagicMock(return_value=True)
test_player._handle_trakt_scrobble = MagicMock()
test_player.handle_subtitles = MagicMock()
list_item = MagicMock()
test_player.play_video(list_item)
test_player.play.assert_called_once_with("http://stream", list_item)
test_player.monitor.assert_called_once_with()
def test_run_does_not_add_next_episode_to_kodi_playlist():
source = PLAYER_PATH.read_text()
run_match = re.search(r"def run\(self, data=None\):(?P<body>.*?)def _drain_nextep_queue", source, re.S)
assert run_match is not None
run_body = run_match.group("body")
assert "self.build_playlist()" not in run_body
def _episode(number, name="Episode", air_date="2020-01-01"):
return SimpleNamespace(episode_number=number, name=name, air_date=air_date)
def _player_module(monkeypatch):
class FakeXbmcPlayer:
pass
monkeypatch.setattr(sys.modules["xbmc"], "Player", FakeXbmcPlayer)
from lib import player as player_module
return importlib.reload(player_module)
def _player_for_episode(player_module, season=1, episode=1):
JacktookPLayer = player_module.JacktookPLayer
player = JacktookPLayer.__new__(JacktookPLayer)
player.data = {
"mode": "tv",
"title": "Test Show",
"ids": {"tmdb_id": 123, "imdb_id": "tt123"},
"tv_data": {"season": season, "episode": episode},
}
return player
def _player_instance():
from lib.player import JacktookPLayer
player = object.__new__(JacktookPLayer)
player.data = {
"mode": "tv",
"title": "Show",
"ids": {"tmdb_id": "123"},
"tv_data": {"season": 1, "episode": 1},
}
player.playnext = MagicMock()
player.play = MagicMock()
player.run = MagicMock()
player.PLAYLIST = MagicMock()
return player
class FakeThread:
def __init__(self, *args, **kwargs):
self.target = kwargs.get("target")
self.args = kwargs.get("args", ())
self.daemon = False
def start(self):
pass
def test_get_next_episode_data_same_season(monkeypatch):
player_module = _player_module(monkeypatch)
test_player = _player_for_episode(player_module, season=1, episode=1)
details = SimpleNamespace(number_of_seasons=1)
season_details = SimpleNamespace(episodes=[_episode(1, "Pilot"), _episode(2, "Second")])
def fake_tmdb_get(path, params=None):
if path == "tv_details":
return details
if path == "season_details":
return season_details
return None
monkeypatch.setattr(player_module, "tmdb_get", fake_tmdb_get)
assert test_player._get_next_episode_data() == {
"name": "Second",
"episode": 2,
"season": 1,
}
def test_get_next_episode_data_season_boundary(monkeypatch):
player_module = _player_module(monkeypatch)
test_player = _player_for_episode(player_module, season=1, episode=10)
details = SimpleNamespace(number_of_seasons=2)
season_one = SimpleNamespace(episodes=[_episode(number) for number in range(1, 11)])
season_two = SimpleNamespace(episodes=[_episode(1, "Season Two Premiere")])
def fake_tmdb_get(path, params=None):
if path == "tv_details":
return details
if path == "season_details" and params["season"] == 1:
return season_one
if path == "season_details" and params["season"] == 2:
return season_two
return None
monkeypatch.setattr(player_module, "tmdb_get", fake_tmdb_get)
assert test_player._get_next_episode_data() == {
"name": "Season Two Premiere",
"episode": 1,
"season": 2,
}
def test_get_next_episode_data_accepts_string_season_episode(monkeypatch):
player_module = _player_module(monkeypatch)
test_player = _player_for_episode(player_module, season="1", episode="1")
details = SimpleNamespace(number_of_seasons=1)
season_details = SimpleNamespace(episodes=[_episode(1, "Pilot"), _episode(2, "Second")])
def fake_tmdb_get(path, params=None):
if path == "tv_details":
return details
if path == "season_details":
assert params["season"] == 1
return season_details
return None
monkeypatch.setattr(player_module, "tmdb_get", fake_tmdb_get)
assert test_player._get_next_episode_data() == {
"name": "Second",
"episode": 2,
"season": 1,
}
def _make_fake_date(fixed_today):
"""Create a fake date module with a fixed today() for deterministic testing."""
class FakeDate(date):
@classmethod
def today(cls):
return fixed_today
# Replace date's __new__ to return FakeDate instances for specific dates
# while keeping date() construction working normally
original_new = date.__new__
class PatchedDate(FakeDate):
def __new__(cls, year, month, day):
return original_new(cls, year, month, day)
return PatchedDate
def test_get_next_episode_data_future_air_date_returns_none(monkeypatch):
player_module = _player_module(monkeypatch)
test_player = _player_for_episode(player_module, season=1, episode=1)
details = SimpleNamespace(number_of_seasons=1)
season_details = SimpleNamespace(
episodes=[_episode(1, "Pilot"), _episode(2, "Future", "2999-01-01")]
)
def fake_tmdb_get(path, params=None):
if path == "tv_details":
return details
if path == "season_details":
return season_details
return None
monkeypatch.setattr(player_module, "tmdb_get", fake_tmdb_get)
monkeypatch.setattr(player_module, "date", _make_fake_date(date(2020, 6, 15)))
assert test_player._get_next_episode_data() is None
def test_get_next_episode_data_past_air_date_returns_episode(monkeypatch):
"""Task 2.6: Past air date returns episode with mocked today()."""
player_module = _player_module(monkeypatch)
test_player = _player_for_episode(player_module, season=1, episode=1)
details = SimpleNamespace(number_of_seasons=1)
season_details = SimpleNamespace(
episodes=[_episode(1, "Pilot", "2020-01-01"), _episode(2, "Aired", "2020-01-15")]
)
def fake_tmdb_get(path, params=None):
if path == "tv_details":
return details
if path == "season_details":
return season_details
return None
monkeypatch.setattr(player_module, "tmdb_get", fake_tmdb_get)
monkeypatch.setattr(player_module, "date", _make_fake_date(date(2020, 6, 15)))
assert test_player._get_next_episode_data() == {
"name": "Aired",
"episode": 2,
"season": 1,
}
def test_queue_from_autoscrape_cache_missing_id_returns_false(monkeypatch):
"""Missing id_value returns False early."""
player_module = _player_module(monkeypatch)
test_player = _player_for_episode(player_module)
ids = {}
next_tv_data = {"season": 1, "episode": 2}
result = test_player._queue_from_autoscrape_cache(next_tv_data, ids)
assert result is False
def test_queue_from_autoscrape_cache_cache_miss_returns_false(monkeypatch):
"""Cache miss returns False."""
player_module = _player_module(monkeypatch)
test_player = _player_for_episode(player_module)
ids = {"tmdb_id": 123}
next_tv_data = {"season": 1, "episode": 2}
monkeypatch.setattr(player_module.cache, "get", lambda key: None)
result = test_player._queue_from_autoscrape_cache(next_tv_data, ids)
assert result is False
def test_queue_from_autoscrape_cache_cache_hit_autoplay_enabled(monkeypatch):
"""Cache hit with autoplay enabled: queues data, stops player, returns True."""
player_module = _player_module(monkeypatch)
player_module.JacktookPLayer._nextep_queue.clear()
test_player = _player_for_episode(player_module)
ids = {"tmdb_id": 123}
next_tv_data = {"season": 1, "episode": 2}
cached_data = {"url": "http://cached", "title": "Cached Ep"}
test_player.stop = MagicMock()
monkeypatch.setattr(player_module.cache, "get", lambda key: cached_data)
monkeypatch.setattr("lib.utils.kodi.settings.auto_play_enabled", lambda: True)
clear_mock = MagicMock()
monkeypatch.setattr(player_module, "clear_property", clear_mock)
result = test_player._queue_from_autoscrape_cache(next_tv_data, ids)
assert result is True
assert len(player_module.JacktookPLayer._nextep_queue) == 1
entry = player_module.JacktookPLayer._nextep_queue[0]
assert entry["data"]["autoplay"] is True
assert entry["data"]["playnext_context"] is True
test_player.stop.assert_called_once_with()
clear_mock.assert_called_once_with("jacktook_next_dialog_action")
player_module.JacktookPLayer._nextep_queue.clear()
def test_queue_from_autoscrape_cache_cache_hit_autoplay_disabled(monkeypatch):
"""Cache hit with autoplay disabled: queues with results, stops player, returns True."""
player_module = _player_module(monkeypatch)
player_module.JacktookPLayer._nextep_queue.clear()
test_player = _player_for_episode(player_module)
ids = {"tmdb_id": 123}
next_tv_data = {"season": 1, "episode": 2}
cached_data = {"url": "http://cached", "title": "Cached Ep"}
test_player.stop = MagicMock()
monkeypatch.setattr(player_module.cache, "get", lambda key: cached_data)
monkeypatch.setattr("lib.utils.kodi.settings.auto_play_enabled", lambda: False)
clear_mock = MagicMock()
monkeypatch.setattr(player_module, "clear_property", clear_mock)
result = test_player._queue_from_autoscrape_cache(next_tv_data, ids)
assert result is True
assert len(player_module.JacktookPLayer._nextep_queue) == 1
test_player.stop.assert_called_once_with()
clear_mock.assert_called_once_with("jacktook_next_dialog_action")
player_module.JacktookPLayer._nextep_queue.clear()
def test_drain_nextep_queue_clears_empty_queue(monkeypatch):
"""Empty queue is handled without error."""
player_module = _player_module(monkeypatch)
player_module.JacktookPLayer._nextep_queue.clear()
test_player = _player_for_episode(player_module)
test_player._drain_nextep_queue() # Should not raise or hang
def test_check_still_watching_threshold_disabled(monkeypatch):
"""Task 2.2: Threshold is zero skips dialog immediately."""
from lib.player import JacktookPLayer
player = _player_instance()
monkeypatch.setattr("lib.player.get_setting", lambda key, default=None: 0)
result = JacktookPLayer._check_still_watching_threshold(player)
assert result is False
def test_check_still_watching_threshold_below_threshold(monkeypatch):
"""Task 2.2: Count below threshold increments counter."""
from lib.player import JacktookPLayer
player = _player_instance()
set_property_mock = MagicMock()
monkeypatch.setattr("lib.player.get_setting", lambda key, default=None: 3)
monkeypatch.setattr("lib.player.get_property", lambda key: "1")
monkeypatch.setattr("lib.player.set_property", set_property_mock)
result = JacktookPLayer._check_still_watching_threshold(player)
assert result is False
set_property_mock.assert_called_once_with("jacktook_consecutive_autoplays", "2")
def test_check_still_watching_threshold_reached_user_continues(monkeypatch):
"""Task 2.2: Count reaches threshold, user continues, resets to 1."""
import xbmcgui
from lib.player import JacktookPLayer
player = _player_instance()
set_property_mock = MagicMock()
clear_property_mock = MagicMock()
dialog = MagicMock()
dialog.yesno.return_value = True
monkeypatch.setattr("lib.player.get_setting", lambda key, default=None: 3)
monkeypatch.setattr("lib.player.get_property", lambda key: "3")
monkeypatch.setattr("lib.player.set_property", set_property_mock)
monkeypatch.setattr("lib.player.clear_property", clear_property_mock)
monkeypatch.setattr(xbmcgui, "Dialog", lambda: dialog)
result = JacktookPLayer._check_still_watching_threshold(player)
assert result is False
set_property_mock.assert_called_once_with("jacktook_consecutive_autoplays", "1")
def test_check_still_watching_threshold_reached_user_stops(monkeypatch):
"""Task 2.2: Count reaches threshold, user stops, returns True."""
import xbmcgui
from lib.player import JacktookPLayer
player = _player_instance()
set_property_mock = MagicMock()
clear_property_mock = MagicMock()
dialog = MagicMock()
dialog.yesno.return_value = False
monkeypatch.setattr("lib.player.get_setting", lambda key, default=None: 3)
monkeypatch.setattr("lib.player.get_property", lambda key: "3")
monkeypatch.setattr("lib.player.set_property", set_property_mock)
monkeypatch.setattr("lib.player.clear_property", clear_property_mock)
monkeypatch.setattr(xbmcgui, "Dialog", lambda: dialog)
result = JacktookPLayer._check_still_watching_threshold(player)
assert result is True
set_property_mock.assert_not_called()
assert clear_property_mock.call_args_list == [
call("jacktook_consecutive_autoplays"),
call("jacktook_next_dialog_action"),
]
def test_check_still_watching_threshold_invalid_count_str(monkeypatch):
"""Task 2.2: Invalid count_str defaults to 1, increments correctly."""
from lib.player import JacktookPLayer
player = _player_instance()
set_property_mock = MagicMock()
monkeypatch.setattr("lib.player.get_setting", lambda key, default=None: 2)
monkeypatch.setattr("lib.player.get_property", lambda key: "abc")
monkeypatch.setattr("lib.player.set_property", set_property_mock)
result = JacktookPLayer._check_still_watching_threshold(player)
assert result is False
set_property_mock.assert_called_once_with("jacktook_consecutive_autoplays", "2")
def test_build_playlist_adds_next_episode(monkeypatch):
"""Task 2.3: Has next episode data adds to PLAYLIST."""
player_module = _player_module(monkeypatch)
test_player = _player_for_episode(player_module)
test_player.preferred_group = "GRP"
next_data = {"name": "Next Ep", "episode": 2, "season": 1}
monkeypatch.setattr(test_player, "_get_next_episode_data", lambda: next_data)
monkeypatch.setattr(
player_module,
"get_setting",
lambda key, default=None: True if key == "autoscrape_next_episode" else default,
)
monkeypatch.setattr(
player_module,
"build_url",
lambda action, **kw: "plugin://play_autoscraped",
)
playlist = MagicMock()
playlist.size.return_value = 0
test_player.PLAYLIST = playlist
test_player.build_playlist()
playlist.add.assert_called_once()
call_url = playlist.add.call_args[1]["url"]
assert call_url == "plugin://play_autoscraped"
def test_build_playlist_no_next_episode(monkeypatch):
"""Task 2.3: No next episode data returns early."""
player_module = _player_module(monkeypatch)
test_player = _player_for_episode(player_module)
test_player.preferred_group = "GRP"
monkeypatch.setattr(test_player, "_get_next_episode_data", lambda: None)
playlist = MagicMock()
test_player.PLAYLIST = playlist
test_player.build_playlist()
playlist.add.assert_not_called()
def test_build_playlist_deduplicates(monkeypatch):
"""Task 2.3: Duplicate URL in playlist is skipped."""
player_module = _player_module(monkeypatch)
test_player = _player_for_episode(player_module)
test_player.preferred_group = "GRP"
next_data = {"name": "Next Ep", "episode": 2, "season": 1}
monkeypatch.setattr(test_player, "_get_next_episode_data", lambda: next_data)
monkeypatch.setattr(
player_module,
"get_setting",
lambda key, default=None: True if key == "autoscrape_next_episode" else default,
)
monkeypatch.setattr(
player_module,
"build_url",
lambda action, **kw: "plugin://play_autoscraped",
)
# Playlist already has the URL
mock_item = MagicMock()
mock_item.getPath.return_value = "plugin://play_autoscraped"
playlist = MagicMock()
playlist.size.return_value = 1
playlist.__getitem__.return_value = mock_item
test_player.PLAYLIST = playlist
test_player.build_playlist()
playlist.add.assert_not_called()
def test_build_playlist_search_fallback(monkeypatch):
"""Task 2.3: Uses search URL format when autoscrape disabled."""
player_module = _player_module(monkeypatch)
test_player = _player_for_episode(player_module)
test_player.preferred_group = "GRP"
next_data = {"name": "Next Ep", "episode": 2, "season": 1}
monkeypatch.setattr(test_player, "_get_next_episode_data", lambda: next_data)
monkeypatch.setattr(
player_module,
"get_setting",
lambda key, default=None: False if key == "autoscrape_next_episode" else default,
)
monkeypatch.setattr(
player_module,
"build_url",
lambda action, **kw: "plugin://search",
)
playlist = MagicMock()
playlist.size.return_value = 0
test_player.PLAYLIST = playlist
test_player.build_playlist()
playlist.add.assert_called_once()
call_url = playlist.add.call_args[1]["url"]
assert call_url == "plugin://search"
def test_handle_subtitles_skips_on_autoplay(monkeypatch):
"""Task 2.4: Autoplay flag set skips subtitle handling."""
player_module = _player_module(monkeypatch)
test_player = _player_for_episode(player_module)
test_player.data["autoplay"] = True
test_player.showSubtitles = MagicMock()
list_item = MagicMock()
test_player.handle_subtitles(list_item)
test_player.showSubtitles.assert_not_called()
def test_advance_across_season_boundary_series_end(monkeypatch):
"""Task 2.5: At series end (last ep of last season) returns None."""
player_module = _player_module(monkeypatch)
test_player = _player_for_episode(player_module)
episodes = [SimpleNamespace(episode_number=10)]
tv_details = SimpleNamespace(number_of_seasons=3)
result = test_player._advance_across_season_boundary(
"123", season=3, episode=10, episodes=episodes, tv_details=tv_details
)
assert result is None
def test_advance_across_season_boundary_single_season(monkeypatch):
"""Task 2.5: Single season show advances within season."""
player_module = _player_module(monkeypatch)
test_player = _player_for_episode(player_module)
episodes = [
SimpleNamespace(episode_number=1),
SimpleNamespace(episode_number=2),
SimpleNamespace(episode_number=3),
]
tv_details = SimpleNamespace(number_of_seasons=1)
result = test_player._advance_across_season_boundary(
"123", season=1, episode=1, episodes=episodes, tv_details=tv_details
)
assert result == (1, 2, episodes)
def test_advance_across_season_boundary_crosses_season(monkeypatch):
"""Task 2.5: Season boundary crossing fetches next season episodes."""
player_module = _player_module(monkeypatch)
test_player = _player_for_episode(player_module)
current_episodes = [SimpleNamespace(episode_number=8)]
next_episodes = [SimpleNamespace(episode_number=1)]
tv_details = SimpleNamespace(number_of_seasons=2)
monkeypatch.setattr(
test_player,
"_fetch_season_episodes",
lambda tmdb_id, season: next_episodes,
)
result = test_player._advance_across_season_boundary(
"123",
season=1,
episode=8,
episodes=current_episodes,
tv_details=tv_details,
)
assert result == (2, 1, next_episodes)
def test_advance_across_season_boundary_fetch_fails(monkeypatch):
"""Task 2.5: Next season episodes fetch fails returns None."""
player_module = _player_module(monkeypatch)
test_player = _player_for_episode(player_module)
current_episodes = [SimpleNamespace(episode_number=8)]
tv_details = SimpleNamespace(number_of_seasons=2)
monkeypatch.setattr(
test_player,
"_fetch_season_episodes",
lambda tmdb_id, season: None,
)
result = test_player._advance_across_season_boundary(
"123",
season=1,
episode=8,
episodes=current_episodes,
tv_details=tv_details,
)
assert result is None
def test_handle_next_dialog_action_fallback_uses_season_boundary_episode(monkeypatch):
"""Without cache, fires background search thread with season-crossing TV data."""
player_module = _player_module(monkeypatch)
player_module.JacktookPLayer._nextep_queue.clear()
class FakePlaylist:
def size(self):
return -1
def getposition(self):
return -1
def clear(self):
self.cleared = True
test_player = _player_for_episode(player_module, season=1, episode=10)
test_player.PLAYLIST = FakePlaylist()
threads = []
class CapturingThread(FakeThread):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
threads.append(self)
monkeypatch.setattr(player_module, "Thread", CapturingThread)
def fake_get_setting(key, default=None):
if key == "playnext_threshold":
return 0
return default
monkeypatch.setattr(player_module, "get_setting", fake_get_setting)
monkeypatch.setattr(player_module, "get_property", lambda key: "")
monkeypatch.setattr(player_module, "clear_property", lambda key: None)
monkeypatch.setattr(player_module.cache, "get", lambda key: None)
monkeypatch.setattr(
test_player,
"_get_next_episode_data",
lambda: {"name": "Season Two Premiere", "episode": 1, "season": 2},
)
test_player._handle_next_dialog_action()
assert len(threads) == 1
target, args = threads[0].target, threads[0].args
assert target == test_player._background_search_and_queue
item_data, next_tv_data = args
assert item_data["mode"] == "tv"
assert next_tv_data == {"name": "Season Two Premiere", "episode": 1, "season": 2}
# ---------------------------------------- live-stream subtitles (1.18.0)
# vsembed syncs a live stream's English to the stream's own audio in ~30-60 s.
# The player starts on wyzie's raw tracks and swaps the synced one in.
LIVE_URL = "http://stortron.local:7220/vidcore/movie/tmdb:1058424.m3u8?h=1080"
def _live_player(monkeypatch, url=LIVE_URL, settings=None, **attrs):
player_module = _player_module(monkeypatch)
values = {"wyzie_subtitle_enabled": True}
values.update(settings or {})
monkeypatch.setattr(player_module, "get_setting",
lambda key, *a, **k: values.get(key, False))
player = player_module.JacktookPLayer.__new__(player_module.JacktookPLayer)
player.data = {"mode": "movies", "url": url}
player.notification = MagicMock()
player.kodi_monitor = MagicMock()
player.kodi_monitor.waitForAbort.return_value = False
player.isPlayingVideo = MagicMock(return_value=True)
player.setSubtitles = MagicMock()
player.showSubtitles = MagicMock()
for k, v in attrs.items():
setattr(player, k, v)
return player_module, player
def test_a_live_vsembed_stream_wants_synced_subtitles(monkeypatch):
_, player = _live_player(monkeypatch)
assert player._wants_live_subtitles() is True
@pytest.mark.parametrize("url,settings,attrs", [
("https://cdn.example/x.m3u8", None, {}),
(LIVE_URL, {"wyzie_subtitle_enabled": False}, {}),
(LIVE_URL, None, {"_subs_from_library": True}),
])
def test_other_cases_do_not(monkeypatch, url, settings, attrs):
_, player = _live_player(monkeypatch, url, settings, **attrs)
assert player._wants_live_subtitles() is False
def test_autoplay_does_not(monkeypatch):
_, player = _live_player(monkeypatch)
player.data["autoplay"] = True
assert player._wants_live_subtitles() is False
def test_the_synced_track_is_swapped_in(monkeypatch):
player_module, player = _live_player(monkeypatch)
import lib.clients.subtitle.library as library
monkeypatch.setattr(library, "watch_live", lambda *a, **k: ["/s/en.synced.srt"])
player._watch_live_subtitles()
player.setSubtitles.assert_called_once_with("/s/en.synced.srt")
player.showSubtitles.assert_called_with(True)
def test_nothing_is_swapped_after_playback_stopped(monkeypatch):
player_module, player = _live_player(monkeypatch)
import lib.clients.subtitle.library as library
monkeypatch.setattr(library, "watch_live", lambda *a, **k: ["/s/en.synced.srt"])
player.isPlayingVideo.return_value = False
player._watch_live_subtitles()
player.setSubtitles.assert_not_called()
def test_no_answer_changes_nothing(monkeypatch):
player_module, player = _live_player(monkeypatch)
import lib.clients.subtitle.library as library
monkeypatch.setattr(library, "watch_live", lambda *a, **k: None)
player._watch_live_subtitles()
player.setSubtitles.assert_not_called()