diff --git a/unshackle/core/api/handlers.py b/unshackle/core/api/handlers.py index 3c5158bc..167b4bfb 100644 --- a/unshackle/core/api/handlers.py +++ b/unshackle/core/api/handlers.py @@ -4198,9 +4198,6 @@ async def session_tracks_handler( except (Exception, SystemExit): session.chapters_by_title[str(title_id)] = [] - video_tracks = sorted(tracks.videos, key=lambda t: t.bitrate or 0, reverse=True) - audio_tracks = sorted(tracks.audio, key=lambda t: t.bitrate or 0, reverse=True) - manifests, track_manifests = await asyncio.to_thread( lambda: (extract_manifests(tracks), extract_track_manifests(tracks)) ) @@ -4238,27 +4235,21 @@ async def session_tracks_handler( else: server_cdm_type = "playready" - def serialize() -> tuple[list, list, list, list]: + def serialize() -> Dict[str, list]: """The track and attachment lists, off the event loop: an attachment can be a 20 MB file.""" lists = serialize_tracks(tracks, title, include_url=True) for d in lists["video"] + lists["audio"] + lists["subtitles"]: d["data"] = scrub_track_data(d.get("data"), session) - attachments = [a for a in map(serialize_attachment, tracks.attachments) if a] - return lists["video"], lists["audio"], lists["subtitles"], attachments - - video, audio, subtitles, attachments = await asyncio.to_thread(serialize) + return {**lists, "attachments": [a for a in map(serialize_attachment, tracks.attachments) if a]} return web.json_response( { "title": stamp_service_flags(serialize_title(title), service_instance), - "video": video, - "audio": audio, - "subtitles": subtitles, + **await asyncio.to_thread(serialize), "chapters": [ {"timestamp": ch.timestamp, "name": ch.name} for ch in session.chapters_by_title.get(str(title_id), []) ], - "attachments": attachments, "manifests": manifests, "track_manifests": track_manifests, "session_headers": session_headers, @@ -4267,7 +4258,7 @@ async def session_tracks_handler( "server_cdm": session.server_cdm, "server_cdm_type": server_cdm_type, "sample_decrypt_tracks": [ - str(t.id) for t in [*video_tracks, *audio_tracks] if service_instance.decrypts_samples(t) + str(t.id) for t in [*tracks.videos, *tracks.audio] if service_instance.decrypts_samples(t) ], } ) @@ -4547,8 +4538,7 @@ async def get_validated_session(session_id: str, request: Optional[web.Request]) details={"session_id": session_id}, ) if session.owner_key is not None and request is not None: - caller_key = request.headers.get("X-Secret-Key", "anonymous") - if not hmac.compare_digest(caller_key, session.owner_key): + if not hmac.compare_digest(caller_key(request), session.owner_key): raise APIError( APIErrorCode.FORBIDDEN, "Session access denied", diff --git a/unshackle/core/api/routes.py b/unshackle/core/api/routes.py index a8b36d16..d62ee827 100644 --- a/unshackle/core/api/routes.py +++ b/unshackle/core/api/routes.py @@ -207,16 +207,15 @@ def heartbeat(handler: Handler) -> Handler: UPDATE_CHECK_TTL = 300.0 -UPDATE_CHECK: dict[str, tuple[float, Optional[str]]] = {} +UPDATE_CHECK: list[tuple[float, Optional[str]]] = [] -def latest_release() -> Optional[str]: +async def latest_release() -> Optional[str]: """The newer release the update check reports, kept for ``UPDATE_CHECK_TTL`` seconds: clients poll health.""" - hit = UPDATE_CHECK.get(__version__) - if hit and time.monotonic() - hit[0] < UPDATE_CHECK_TTL: - return hit[1] - latest = UpdateChecker.check_for_updates_sync(__version__) - UPDATE_CHECK[__version__] = (time.monotonic(), latest) + if UPDATE_CHECK and time.monotonic() - UPDATE_CHECK[0][0] < UPDATE_CHECK_TTL: + return UPDATE_CHECK[0][1] + latest = await asyncio.to_thread(UpdateChecker.check_for_updates_sync, __version__) + UPDATE_CHECK[:] = [(time.monotonic(), latest)] return latest @@ -261,7 +260,7 @@ async def health(request: web.Request) -> web.Response: nullable: true """ try: - latest_version = await asyncio.to_thread(latest_release) + latest_version = await latest_release() update_info = { "update_available": latest_version is not None, "current_version": __version__, diff --git a/unshackle/core/api/session_store.py b/unshackle/core/api/session_store.py index fda282fc..08fde75e 100644 --- a/unshackle/core/api/session_store.py +++ b/unshackle/core/api/session_store.py @@ -75,7 +75,7 @@ class SessionEntry: """ return self.lock.locked() - def summary(self) -> Dict[str, Any]: + def summary(self, include_actions: bool = True) -> Dict[str, Any]: """Dashboard view of the session: no service instance, no raw owner key.""" from unshackle.core.api.stats import mask_key @@ -100,7 +100,7 @@ class SessionEntry: "server_cdm_max_height": self.server_cdm_max_height, "log_seq": self.log_buffer.last_seq if self.log_buffer else 0, "client": self.client, - "actions": list(self.actions), + **({"actions": list(self.actions)} if include_actions else {}), "created_at": self.created_at.isoformat(), "last_accessed": self.last_accessed.isoformat(), "created_ts": self.created_at.timestamp(), @@ -121,11 +121,10 @@ def _publish( action: str, entry: SessionEntry, reason: Optional[str] = None, action_entry: Optional[Dict[str, Any]] = None ) -> None: """Tell dashboard listeners about a remote session; ``action_entry`` replaces the action log with that one action.""" - data = {"action": action, **entry.summary()} + data = {"action": action, **entry.summary(include_actions=action_entry is None)} if reason: data["reason"] = reason if action_entry is not None: - del data["actions"] data["action_entry"] = action_entry bus.publish("session", data) @@ -301,9 +300,8 @@ class SessionStore: @classmethod async def remove_cache_dirs(cls, cache_tags: List[Optional[str]]) -> None: """Remove the cache directories of ended remote sessions on a worker thread, outside the store lock.""" - for tag in cache_tags: - if tag: - await asyncio.to_thread(cls.cleanup_cache_dir, tag) + if any(cache_tags): + await asyncio.to_thread(lambda: [cls.cleanup_cache_dir(tag) for tag in cache_tags]) @staticmethod def cleanup_cache_dir(cache_tag: Optional[str]) -> None: diff --git a/unshackle/core/api/stats.py b/unshackle/core/api/stats.py index 71fb1782..1d09ce19 100644 --- a/unshackle/core/api/stats.py +++ b/unshackle/core/api/stats.py @@ -72,7 +72,6 @@ def mask_key(key: Optional[str]) -> str: return key[:4] + "…" -@lru_cache(maxsize=1024) def key_id(key: Optional[str]) -> str: """A stable id for an API key. A hash prefix, so two unnamed keys never collide the way ``mask_key`` does, and the id carries no key material."""