v1.0
This commit is contained in:
1 parent
0246cbb6ba
commit
0ca4b4bf7a
89 files changed
+7042
-2480
No files matched your search
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
from urllib.parse import quote
|
||||
|
||||
import httpx
|
||||
|
||||
@@ -11,6 +12,19 @@ from .config import Settings
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# Fields Emby only returns on request.
|
||||
_LIST_FIELDS = "ProductionYear,ParentId,PremiereDate"
|
||||
_DETAIL_FIELDS = (
|
||||
"Overview,Genres,ProductionYear,PremiereDate,People,Studios,Taglines,"
|
||||
"ProviderIds,ParentId,MediaSources,Path,CommunityRating,OfficialRating"
|
||||
)
|
||||
|
||||
|
||||
def _seg(value: Any) -> str:
|
||||
"""One URL path segment: IDs come from the LLM and must not add path parts
|
||||
(e.g. ``123/Delete`` or ``../System/Restart``)."""
|
||||
return quote(str(value), safe="")
|
||||
|
||||
|
||||
class EmbyClient:
|
||||
"""Thin async wrapper around Emby's REST API."""
|
||||
@@ -48,59 +62,65 @@ class EmbyClient:
|
||||
return None
|
||||
return None
|
||||
|
||||
async def _delete(self, path: str) -> Any:
|
||||
r = await self._http.delete(path)
|
||||
r.raise_for_status()
|
||||
if r.content:
|
||||
try:
|
||||
return r.json()
|
||||
except ValueError:
|
||||
return None
|
||||
return None
|
||||
|
||||
# ---- API: Items (media library) ---------------------------------------
|
||||
|
||||
async def list_items(
|
||||
self,
|
||||
parent_id: str | None = None,
|
||||
include_related: bool = False,
|
||||
recursive: bool = True,
|
||||
sort_by: str | None = "Name",
|
||||
sort_by: str | None = "SortName",
|
||||
sort_order: str = "Ascending",
|
||||
limit: int | None = None,
|
||||
search_term: str | None = None,
|
||||
item_types: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""List items from the media library."""
|
||||
"""List or search items from the media library (server-side filtering)."""
|
||||
params: dict[str, Any] = {
|
||||
"IncludeRelated": str(include_related).lower(),
|
||||
"Recursive": str(recursive).lower(),
|
||||
"SortBy": sort_by,
|
||||
"SortOrder": sort_order,
|
||||
"Fields": _LIST_FIELDS,
|
||||
}
|
||||
if sort_by:
|
||||
params["SortBy"] = sort_by
|
||||
if parent_id:
|
||||
params["ParentId"] = parent_id
|
||||
if limit:
|
||||
params["Limit"] = limit
|
||||
if search_term:
|
||||
params["SearchTerm"] = search_term
|
||||
if item_types:
|
||||
params["IncludeItemTypes"] = item_types
|
||||
return await self._get("/Items", params=params)
|
||||
|
||||
async def get_item(self, item_id: str) -> dict[str, Any]:
|
||||
"""Get details for a specific item."""
|
||||
return await self._get(f"/Items/{item_id}")
|
||||
"""Get details for a specific item (``GET /Items/{id}`` does not exist in Emby)."""
|
||||
raw = await self._get(
|
||||
"/Items", params={"Ids": item_id, "Recursive": "true", "Fields": _DETAIL_FIELDS}
|
||||
)
|
||||
items = raw.get("Items") if isinstance(raw, dict) else None
|
||||
if not items:
|
||||
raise LookupError(f"No Emby item with id {item_id!r}. Use search_items to find it.")
|
||||
return items[0]
|
||||
|
||||
# ---- API: Users -------------------------------------------------------
|
||||
|
||||
async def list_users(self) -> dict[str, Any]:
|
||||
async def list_users(self) -> list[dict[str, Any]]:
|
||||
"""List all users on the server."""
|
||||
return await self._get("/Users")
|
||||
raw = await self._get("/Users/Query")
|
||||
if isinstance(raw, dict):
|
||||
return [u for u in raw.get("Items", []) if isinstance(u, dict)]
|
||||
return raw if isinstance(raw, list) else []
|
||||
|
||||
async def get_user(self, user_id: str) -> dict[str, Any]:
|
||||
"""Get details for a specific user."""
|
||||
return await self._get(f"/Users/{user_id}")
|
||||
return await self._get(f"/Users/{_seg(user_id)}")
|
||||
|
||||
# ---- API: Sessions ----------------------------------------------------
|
||||
|
||||
async def list_sessions(self) -> list[dict[str, Any]]:
|
||||
"""List active sessions (connected devices/apps)."""
|
||||
return await self._get("/Sessions")
|
||||
raw = await self._get("/Sessions")
|
||||
return raw if isinstance(raw, list) else []
|
||||
|
||||
async def get_system_info(self) -> dict[str, Any]:
|
||||
"""Get server system info."""
|
||||
@@ -108,27 +128,27 @@ class EmbyClient:
|
||||
|
||||
# ---- API: Playback ----------------------------------------------------
|
||||
|
||||
async def play_item(
|
||||
self, item_id: str, user_id: str | None = None, autoplay: bool = True
|
||||
async def play_on_session(
|
||||
self, session_id: str, item_ids: list[str], play_command: str = "PlayNow"
|
||||
) -> Any:
|
||||
"""Start playback of an item."""
|
||||
params: dict[str, Any] = {"UserId": user_id or "null", "Autostart": autoplay}
|
||||
return await self._post(f"/Items/{item_id}/Play", params=params)
|
||||
"""Remote-control a client session to play items."""
|
||||
return await self._post(
|
||||
f"/Sessions/{_seg(session_id)}/Playing",
|
||||
params={"ItemIds": ",".join(item_ids), "PlayCommand": play_command},
|
||||
)
|
||||
|
||||
async def get_playback_info(self, item_id: str, user_id: str | None = None) -> dict[str, Any]:
|
||||
"""Get playback info for an item (transcoding options, etc.)."""
|
||||
params = {}
|
||||
if user_id:
|
||||
params["UserId"] = user_id
|
||||
return await self._get(f"/Items/{item_id}/PlaybackInfo", params=params)
|
||||
|
||||
async def get_playback_url(self, item_id: str) -> dict[str, Any]:
|
||||
"""Get a stream URL for direct playback."""
|
||||
return await self._get(f"/Items/{item_id}/StreamUrl")
|
||||
return await self._get(f"/Items/{_seg(item_id)}/PlaybackInfo", params=params)
|
||||
|
||||
# ---- API: Device Info -------------------------------------------------
|
||||
|
||||
async def list_devices(self) -> list[dict[str, Any]]:
|
||||
"""List registered devices."""
|
||||
r = await self._get("/Devices")
|
||||
return r if isinstance(r, list) else []
|
||||
"""List registered devices (Emby wraps them in ``{"Items": [...]}``)."""
|
||||
raw = await self._get("/Devices")
|
||||
if isinstance(raw, dict):
|
||||
raw = raw.get("Items", [])
|
||||
return [d for d in raw if isinstance(d, dict)] if isinstance(raw, list) else []
|
||||
Reference in new issue
Block a user