video sources: add refresh_sections() — tell the server to rescan VIDEO sections

The foundation for the 'Scan Video Library' automation (the video twin of music's
Scan Library). PlexVideoSource.refresh_sections() triggers a Plex scan on the selected
movie/TV sections (section.update()); JellyfinVideoSource.refresh_sections() POSTs
/Items/{id}/Refresh per selected video view (the GET-only _make_request can't POST).
Module helper refresh_video_server_sections() gets the active source and refreshes —
scoped to the user's chosen video libraries (Settings), so it scans the CORRECT media,
not music. Video-only, additive; isolation guard + (my code) ruff clean.
This commit is contained in:
BoulderBadgeDad 2026-06-19 19:03:51 -07:00
parent 3698ed1ec0
commit 13b30a5997

View file

@ -266,6 +266,22 @@ def get_active_video_source():
return _build_source(sel.get("movies") or None, sel.get("tv") or None)
def refresh_video_server_sections():
"""Tell the active media server to rescan its selected VIDEO sections (so newly
downloaded files get indexed) the video twin of music's 'Scan Library'. Returns
{ok, sections} or {ok: False, error}."""
src = get_active_video_source()
if src is None:
return {"ok": False, "error": "No video server configured"}
if not hasattr(src, "refresh_sections"):
return {"ok": False, "error": "This server doesn't support a scan trigger"}
try:
return src.refresh_sections()
except Exception as e: # noqa: BLE001 - surface any server error to the automation
logger.exception("video sources: refresh failed")
return {"ok": False, "error": str(e)}
def list_video_libraries():
"""Discover the active server's video libraries for the mapping UI:
{'server', 'movies': [{'title'}], 'tv': [{'title'}]} or None."""
@ -324,6 +340,19 @@ class PlexVideoSource:
except Exception:
logger.exception("Plex: skipping show %s", getattr(sh, "title", "?"))
def refresh_sections(self) -> dict:
"""Tell Plex to rescan the selected video sections so freshly-downloaded files
get indexed. (plexapi ``section.update()`` triggers the library scan.)"""
n = 0
for kind, name in (("movie", self._movies_lib), ("show", self._tv_lib)):
for s in self._sections(kind, name):
try:
s.update()
n += 1
except Exception:
logger.exception("Plex: refresh failed for section %s", getattr(s, "title", "?"))
return {"ok": n > 0, "sections": n}
@staticmethod
def _part_file(obj):
try:
@ -485,6 +514,29 @@ class JellyfinVideoSource:
"tv": [{"title": v.get("Name")} for v in self._views("tvshows")],
}
def refresh_sections(self) -> dict:
"""Ask Jellyfin to rescan the selected video libraries (POST /Items/{id}/Refresh)
so freshly-downloaded files get indexed. _make_request is GET-only, so POST direct."""
import requests
base = (self._c.base_url or "").rstrip("/")
if not base:
return {"ok": False, "sections": 0}
headers = {"X-Emby-Token": self._c.api_key or ""}
views = list(self._views("movies", self._movies_lib)) + list(self._views("tvshows", self._tv_lib))
n = 0
for v in views:
vid = v.get("Id")
if not vid:
continue
try:
requests.post(base + "/Items/" + str(vid) + "/Refresh", headers=headers,
params={"Recursive": "true", "MetadataRefreshMode": "Default",
"ImageRefreshMode": "Default"}, timeout=15)
n += 1
except Exception:
logger.exception("Jellyfin: refresh failed for view %s", vid)
return {"ok": n > 0, "sections": n}
def counts(self, incremental=False) -> dict:
def total(view, itype):
resp = self._req(f"/Users/{self.uid}/Items", {