video scan: harden Jellyfin adapter (paging + per-item resilience)
- Paginate Jellyfin movie/series listings (StartIndex/Limit, 500/page) so large libraries aren't truncated on full/deep scans; incremental stays a single capped page. - Per-item try/except in iter_movies/iter_shows (matches Plex) so one bad item doesn't abort the scan. - Skip Jellyfin episodes with no IndexNumber (consistency with Plex; avoids ep-0 collisions). All three modes now solid for Plex + Jellyfin.
This commit is contained in:
parent
fba47e9665
commit
9974845470
1 changed files with 37 additions and 7 deletions
|
|
@ -235,6 +235,21 @@ class JellyfinVideoSource:
|
||||||
"tv": [{"title": v.get("Name")} for v in self._views("tvshows")],
|
"tv": [{"title": v.get("Name")} for v in self._views("tvshows")],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def _paged(self, path, params, page_size=500):
|
||||||
|
"""Yield items across pages so large libraries aren't capped/truncated."""
|
||||||
|
start = 0
|
||||||
|
while True:
|
||||||
|
p = dict(params)
|
||||||
|
p.update({"StartIndex": str(start), "Limit": str(page_size)})
|
||||||
|
resp = self._req(path, p) or {}
|
||||||
|
batch = resp.get("Items", [])
|
||||||
|
for it in batch:
|
||||||
|
yield it
|
||||||
|
start += len(batch)
|
||||||
|
total = resp.get("TotalRecordCount")
|
||||||
|
if not batch or len(batch) < page_size or (total is not None and start >= total):
|
||||||
|
break
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _ticks_to_seconds(ticks):
|
def _ticks_to_seconds(ticks):
|
||||||
return int(ticks / 10_000_000) if ticks else None
|
return int(ticks / 10_000_000) if ticks else None
|
||||||
|
|
@ -259,14 +274,20 @@ class JellyfinVideoSource:
|
||||||
}
|
}
|
||||||
|
|
||||||
def iter_movies(self, incremental=False):
|
def iter_movies(self, incremental=False):
|
||||||
|
path = f"/Users/{self.uid}/Items"
|
||||||
for view in self._views("movies", self._movies_lib):
|
for view in self._views("movies", self._movies_lib):
|
||||||
params = {"ParentId": view["Id"], "IncludeItemTypes": "Movie",
|
params = {"ParentId": view["Id"], "IncludeItemTypes": "Movie",
|
||||||
"Recursive": "true", "Fields": _JF_MOVIE_FIELDS}
|
"Recursive": "true", "Fields": _JF_MOVIE_FIELDS}
|
||||||
if incremental:
|
if incremental:
|
||||||
params.update({"SortBy": "DateCreated", "SortOrder": "Descending", "Limit": "100"})
|
params.update({"SortBy": "DateCreated", "SortOrder": "Descending", "Limit": "100"})
|
||||||
resp = self._req(f"/Users/{self.uid}/Items", params) or {}
|
items = (self._req(path, params) or {}).get("Items", [])
|
||||||
for it in resp.get("Items", []):
|
else:
|
||||||
yield self._movie(it)
|
items = self._paged(path, params)
|
||||||
|
for it in items:
|
||||||
|
try:
|
||||||
|
yield self._movie(it)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Jellyfin: skipping movie %s", it.get("Name", "?"))
|
||||||
|
|
||||||
def _movie(self, it) -> dict:
|
def _movie(self, it) -> dict:
|
||||||
studios = it.get("Studios") or []
|
studios = it.get("Studios") or []
|
||||||
|
|
@ -284,14 +305,20 @@ class JellyfinVideoSource:
|
||||||
}
|
}
|
||||||
|
|
||||||
def iter_shows(self, incremental=False):
|
def iter_shows(self, incremental=False):
|
||||||
|
path = f"/Users/{self.uid}/Items"
|
||||||
for view in self._views("tvshows", self._tv_lib):
|
for view in self._views("tvshows", self._tv_lib):
|
||||||
params = {"ParentId": view["Id"], "IncludeItemTypes": "Series",
|
params = {"ParentId": view["Id"], "IncludeItemTypes": "Series",
|
||||||
"Recursive": "true", "Fields": "Overview,ProductionYear,OfficialRating"}
|
"Recursive": "true", "Fields": "Overview,ProductionYear,OfficialRating"}
|
||||||
if incremental:
|
if incremental:
|
||||||
params.update({"SortBy": "DateCreated", "SortOrder": "Descending", "Limit": "50"})
|
params.update({"SortBy": "DateCreated", "SortOrder": "Descending", "Limit": "50"})
|
||||||
resp = self._req(f"/Users/{self.uid}/Items", params) or {}
|
items = (self._req(path, params) or {}).get("Items", [])
|
||||||
for it in resp.get("Items", []):
|
else:
|
||||||
yield self._show(it)
|
items = self._paged(path, params)
|
||||||
|
for it in items:
|
||||||
|
try:
|
||||||
|
yield self._show(it)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Jellyfin: skipping show %s", it.get("Name", "?"))
|
||||||
|
|
||||||
def _show(self, it) -> dict:
|
def _show(self, it) -> dict:
|
||||||
series_id = str(it["Id"])
|
series_id = str(it["Id"])
|
||||||
|
|
@ -301,13 +328,16 @@ class JellyfinVideoSource:
|
||||||
"UserId": self.uid, "Fields": _JF_EP_FIELDS}) or {}
|
"UserId": self.uid, "Fields": _JF_EP_FIELDS}) or {}
|
||||||
by_season: dict[int, list] = {}
|
by_season: dict[int, list] = {}
|
||||||
for ep in eps_resp.get("Items", []):
|
for ep in eps_resp.get("Items", []):
|
||||||
|
enum = ep.get("IndexNumber")
|
||||||
|
if enum is None:
|
||||||
|
continue # unnumbered/special — can't key it
|
||||||
snum = ep.get("ParentIndexNumber") or 0
|
snum = ep.get("ParentIndexNumber") or 0
|
||||||
aired = ep.get("PremiereDate")
|
aired = ep.get("PremiereDate")
|
||||||
ticks = ep.get("RunTimeTicks")
|
ticks = ep.get("RunTimeTicks")
|
||||||
by_season.setdefault(snum, []).append({
|
by_season.setdefault(snum, []).append({
|
||||||
"server_id": str(ep["Id"]),
|
"server_id": str(ep["Id"]),
|
||||||
"season_number": snum,
|
"season_number": snum,
|
||||||
"episode_number": ep.get("IndexNumber") or 0,
|
"episode_number": enum,
|
||||||
"title": ep.get("Name"),
|
"title": ep.get("Name"),
|
||||||
"overview": ep.get("Overview"),
|
"overview": ep.get("Overview"),
|
||||||
"air_date": aired[:10] if aired else None,
|
"air_date": aired[:10] if aired else None,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue