unit3dprep 1.1.2__tar.gz → 1.1.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. {unit3dprep-1.1.2/unit3dprep.egg-info → unit3dprep-1.1.4}/PKG-INFO +1 -1
  2. {unit3dprep-1.1.2 → unit3dprep-1.1.4}/pyproject.toml +1 -1
  3. {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/core.py +38 -0
  4. {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/api/library.py +65 -1
  5. {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/api/tmdb.py +10 -2
  6. unit3dprep-1.1.4/unit3dprep/web/dist/assets/index-BV3cCeEq.js +255 -0
  7. {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/dist/index.html +1 -1
  8. {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/tmdb_cache.py +17 -0
  9. {unit3dprep-1.1.2 → unit3dprep-1.1.4/unit3dprep.egg-info}/PKG-INFO +1 -1
  10. {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep.egg-info/SOURCES.txt +1 -1
  11. unit3dprep-1.1.2/unit3dprep/web/dist/assets/index-C7oz5Og_.js +0 -255
  12. {unit3dprep-1.1.2 → unit3dprep-1.1.4}/LICENSE +0 -0
  13. {unit3dprep-1.1.2 → unit3dprep-1.1.4}/MANIFEST.in +0 -0
  14. {unit3dprep-1.1.2 → unit3dprep-1.1.4}/README.md +0 -0
  15. {unit3dprep-1.1.2 → unit3dprep-1.1.4}/setup.cfg +0 -0
  16. {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/__init__.py +0 -0
  17. {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/cli.py +0 -0
  18. {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/i18n.py +0 -0
  19. {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/media.py +0 -0
  20. {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/upload.py +0 -0
  21. {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/__init__.py +0 -0
  22. {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/_env.py +0 -0
  23. {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/api/__init__.py +0 -0
  24. {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/api/auth.py +0 -0
  25. {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/api/fs.py +0 -0
  26. {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/api/logs.py +0 -0
  27. {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/api/queue.py +0 -0
  28. {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/api/quickupload.py +0 -0
  29. {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/api/reseed.py +0 -0
  30. {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/api/search.py +0 -0
  31. {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/api/settings.py +0 -0
  32. {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/api/trackers.py +0 -0
  33. {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/api/uploaded.py +0 -0
  34. {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/api/version.py +0 -0
  35. {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/api/webup.py +0 -0
  36. {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/api/wizard.py +0 -0
  37. {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/app.py +0 -0
  38. {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/auth.py +0 -0
  39. {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/clients.py +0 -0
  40. {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/config.py +0 -0
  41. {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/db.py +0 -0
  42. {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/dist/assets/JetBrainsMono-Italic-VariableFont_wght-CZO9PUqx.ttf +0 -0
  43. {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/dist/assets/JetBrainsMono-VariableFont_wght-BrlcHZ7m.ttf +0 -0
  44. {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/dist/assets/SpaceGrotesk-VariableFont_wght-DIScfSlK.ttf +0 -0
  45. {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/dist/assets/index-FFoOmpDN.css +0 -0
  46. {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/duplicate_check.py +0 -0
  47. {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/lang_cache.py +0 -0
  48. {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/logbuf.py +0 -0
  49. {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/reseed.py +0 -0
  50. {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/trackers.py +0 -0
  51. {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/webup_client.py +0 -0
  52. {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/webup_job_fix.py +0 -0
  53. {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/webup_logclass.py +0 -0
  54. {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/webup_orchestrator.py +0 -0
  55. {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/webup_ws.py +0 -0
  56. {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep.egg-info/dependency_links.txt +0 -0
  57. {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep.egg-info/entry_points.txt +0 -0
  58. {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep.egg-info/requires.txt +0 -0
  59. {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: unit3dprep
3
- Version: 1.1.2
3
+ Version: 1.1.4
4
4
  Summary: Web UI + CLI di pre-flight per tracker Unit3D, companion di Unit3DWebUp (audio ITA, nomenclatura ItaTorrents, hardlink, upload)
5
5
  Author: Davide Sidoti
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "unit3dprep"
7
- version = "1.1.2"
7
+ version = "1.1.4"
8
8
  description = "Web UI + CLI di pre-flight per tracker Unit3D, companion di Unit3DWebUp (audio ITA, nomenclatura ItaTorrents, hardlink, upload)"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -434,6 +434,44 @@ def tmdb_search(kind: str, query: str, year: str, api_key: str, language: str |
434
434
  return normalized
435
435
 
436
436
 
437
+ def tv_season_status(data: dict) -> dict:
438
+ """Compute per-season completion from a TMDB ``/tv/{id}`` object.
439
+
440
+ Returns::
441
+
442
+ {
443
+ "show_status": str, # raw TMDB `status` (e.g. "Ended", "Returning Series")
444
+ "seasons": { # keyed by season_number as a string
445
+ "1": {"episode_count": int, "complete": bool, "next_air_date": str | None},
446
+ ...
447
+ },
448
+ }
449
+
450
+ A season is ``complete`` (no further episodes expected) when the show has
451
+ ended/been canceled, when no next episode is scheduled, or when the next
452
+ episode to air belongs to a different season. The single season still
453
+ receiving episodes (``next_episode_to_air.season_number``) is the only one
454
+ marked not complete.
455
+ """
456
+ show_status = (data.get("status") or "").strip()
457
+ ended = show_status in ("Ended", "Canceled")
458
+ next_ep = data.get("next_episode_to_air") or {}
459
+ next_season = next_ep.get("season_number")
460
+ next_air_date = next_ep.get("air_date")
461
+ seasons: dict[str, dict] = {}
462
+ for s in data.get("seasons") or []:
463
+ num = s.get("season_number")
464
+ if num is None:
465
+ continue
466
+ ongoing = (not ended) and next_season is not None and next_season == num
467
+ seasons[str(num)] = {
468
+ "episode_count": s.get("episode_count") or 0,
469
+ "complete": not ongoing,
470
+ "next_air_date": next_air_date if ongoing else None,
471
+ }
472
+ return {"show_status": show_status, "seasons": seasons}
473
+
474
+
437
475
  # ---------------------------------------------------------------------------
438
476
  # Name builder
439
477
  # ---------------------------------------------------------------------------
@@ -7,6 +7,7 @@ from __future__ import annotations
7
7
  import asyncio
8
8
  import json
9
9
  import os
10
+ import time
10
11
  from pathlib import Path
11
12
  from typing import Any, AsyncGenerator
12
13
 
@@ -17,10 +18,12 @@ from sse_starlette.sse import EventSourceResponse
17
18
 
18
19
  from ...core import (
19
20
  audio_languages,
21
+ tmdb_fetch,
20
22
  tmdb_fetch_bilingual,
21
23
  tmdb_poster_url,
22
24
  tmdb_search,
23
25
  tmdb_year,
26
+ tv_season_status,
24
27
  )
25
28
  from ...media import (
26
29
  MediaItem,
@@ -33,7 +36,7 @@ from ...media import (
33
36
  from ...i18n import get_request_lang, t as _i18n_t
34
37
  from ..db import list_uploads, record_upload
35
38
  from ..lang_cache import get_many_langs, set_lang
36
- from ..tmdb_cache import get_cache, get_many, set_cache
39
+ from ..tmdb_cache import get_cache, get_many, set_cache, set_series_status
37
40
 
38
41
  TMDB_API_KEY = os.environ.get("TMDB_API_KEY", "")
39
42
 
@@ -242,6 +245,67 @@ async def library_categories():
242
245
  })
243
246
 
244
247
 
248
+ _STATUS_TTL_SECONDS = 12 * 3600
249
+
250
+
251
+ def _series_status_fresh(fetched_at: Any, show_status: str) -> bool:
252
+ """Cached season status is fresh when the show is finished (immutable) or
253
+ the last fetch is within the TTL."""
254
+ if show_status in ("Ended", "Canceled"):
255
+ return True
256
+ if not isinstance(fetched_at, (int, float)):
257
+ return False
258
+ return (time.time() - fetched_at) < _STATUS_TTL_SECONDS
259
+
260
+
261
+ @router.get("/library/series-status")
262
+ async def library_series_status(path: str):
263
+ """Per-season completion (complete vs ongoing) + TMDB episode count for a
264
+ series. Resolves the TMDB id from cache, refetches ``/tv/{id}`` when the
265
+ cached status is missing/stale, then caches it. On-demand only — never hit
266
+ during the library scan, so it costs at most one TMDB call per panel open.
267
+ """
268
+ record = await get_cache(path)
269
+ tmdb_id = (record or {}).get("tmdb_id", "")
270
+ if not tmdb_id:
271
+ return JSONResponse({"tmdb_id": "", "show_status": "", "seasons": {}})
272
+
273
+ cached_meta = (record or {}).get("seasons_meta")
274
+ cached_status = (record or {}).get("show_status", "")
275
+ if cached_meta is not None and _series_status_fresh(
276
+ (record or {}).get("status_fetched_at"), cached_status
277
+ ):
278
+ return JSONResponse(
279
+ {"tmdb_id": tmdb_id, "show_status": cached_status, "seasons": cached_meta}
280
+ )
281
+
282
+ if not TMDB_API_KEY:
283
+ return JSONResponse(
284
+ {"tmdb_id": tmdb_id, "show_status": cached_status, "seasons": cached_meta or {}}
285
+ )
286
+
287
+ loop = asyncio.get_event_loop()
288
+ try:
289
+ data = await loop.run_in_executor(None, tmdb_fetch, "tv", tmdb_id, TMDB_API_KEY)
290
+ except Exception:
291
+ # TMDB hiccup: serve stale cache if present, else report unknown.
292
+ if cached_meta is not None:
293
+ return JSONResponse(
294
+ {"tmdb_id": tmdb_id, "show_status": cached_status, "seasons": cached_meta}
295
+ )
296
+ return JSONResponse({"tmdb_id": tmdb_id, "show_status": "", "seasons": {}})
297
+
298
+ status_obj = tv_season_status(data)
299
+ await set_series_status(path, status_obj["show_status"], status_obj["seasons"])
300
+ return JSONResponse(
301
+ {
302
+ "tmdb_id": tmdb_id,
303
+ "show_status": status_obj["show_status"],
304
+ "seasons": status_obj["seasons"],
305
+ }
306
+ )
307
+
308
+
245
309
  @router.get("/library/{category}")
246
310
  async def library_list(request: Request, category: str):
247
311
  if category not in discover_categories():
@@ -8,9 +8,9 @@ from fastapi import APIRouter, HTTPException, Request
8
8
  from fastapi.responses import JSONResponse
9
9
  from pydantic import BaseModel
10
10
 
11
- from ...core import tmdb_fetch_bilingual, tmdb_poster_url, tmdb_search, tmdb_year
11
+ from ...core import tmdb_fetch_bilingual, tmdb_poster_url, tmdb_search, tmdb_year, tv_season_status
12
12
  from ...i18n import get_request_lang, t
13
- from ..tmdb_cache import delete_cache, set_cache
13
+ from ..tmdb_cache import delete_cache, set_cache, set_series_status
14
14
 
15
15
  TMDB_API_KEY = os.environ.get("TMDB_API_KEY", "")
16
16
 
@@ -64,6 +64,14 @@ async def set_manual(request: Request, body: SetBody):
64
64
  overview=(data.get("overview") or "")[:300],
65
65
  overview_en=(data.get("overview_en") or "")[:300],
66
66
  )
67
+ if body.tmdb_kind == "tv":
68
+ # `data` already holds the full /tv/{id} object → derive season status
69
+ # with no extra TMDB call. set_cache above replaced the record, so this
70
+ # merge must run after it.
71
+ status_obj = tv_season_status(data)
72
+ await set_series_status(
73
+ body.source_path, status_obj["show_status"], status_obj["seasons"]
74
+ )
67
75
  return JSONResponse({"ok": True, "title": title, "year": year, "poster": poster})
68
76
 
69
77