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.
- {unit3dprep-1.1.2/unit3dprep.egg-info → unit3dprep-1.1.4}/PKG-INFO +1 -1
- {unit3dprep-1.1.2 → unit3dprep-1.1.4}/pyproject.toml +1 -1
- {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/core.py +38 -0
- {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/api/library.py +65 -1
- {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/api/tmdb.py +10 -2
- unit3dprep-1.1.4/unit3dprep/web/dist/assets/index-BV3cCeEq.js +255 -0
- {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/dist/index.html +1 -1
- {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/tmdb_cache.py +17 -0
- {unit3dprep-1.1.2 → unit3dprep-1.1.4/unit3dprep.egg-info}/PKG-INFO +1 -1
- {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep.egg-info/SOURCES.txt +1 -1
- unit3dprep-1.1.2/unit3dprep/web/dist/assets/index-C7oz5Og_.js +0 -255
- {unit3dprep-1.1.2 → unit3dprep-1.1.4}/LICENSE +0 -0
- {unit3dprep-1.1.2 → unit3dprep-1.1.4}/MANIFEST.in +0 -0
- {unit3dprep-1.1.2 → unit3dprep-1.1.4}/README.md +0 -0
- {unit3dprep-1.1.2 → unit3dprep-1.1.4}/setup.cfg +0 -0
- {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/__init__.py +0 -0
- {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/cli.py +0 -0
- {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/i18n.py +0 -0
- {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/media.py +0 -0
- {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/upload.py +0 -0
- {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/__init__.py +0 -0
- {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/_env.py +0 -0
- {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/api/__init__.py +0 -0
- {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/api/auth.py +0 -0
- {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/api/fs.py +0 -0
- {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/api/logs.py +0 -0
- {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/api/queue.py +0 -0
- {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/api/quickupload.py +0 -0
- {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/api/reseed.py +0 -0
- {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/api/search.py +0 -0
- {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/api/settings.py +0 -0
- {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/api/trackers.py +0 -0
- {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/api/uploaded.py +0 -0
- {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/api/version.py +0 -0
- {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/api/webup.py +0 -0
- {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/api/wizard.py +0 -0
- {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/app.py +0 -0
- {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/auth.py +0 -0
- {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/clients.py +0 -0
- {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/config.py +0 -0
- {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/db.py +0 -0
- {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/dist/assets/JetBrainsMono-Italic-VariableFont_wght-CZO9PUqx.ttf +0 -0
- {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/dist/assets/JetBrainsMono-VariableFont_wght-BrlcHZ7m.ttf +0 -0
- {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/dist/assets/SpaceGrotesk-VariableFont_wght-DIScfSlK.ttf +0 -0
- {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/dist/assets/index-FFoOmpDN.css +0 -0
- {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/duplicate_check.py +0 -0
- {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/lang_cache.py +0 -0
- {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/logbuf.py +0 -0
- {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/reseed.py +0 -0
- {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/trackers.py +0 -0
- {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/webup_client.py +0 -0
- {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/webup_job_fix.py +0 -0
- {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/webup_logclass.py +0 -0
- {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/webup_orchestrator.py +0 -0
- {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep/web/webup_ws.py +0 -0
- {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep.egg-info/dependency_links.txt +0 -0
- {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep.egg-info/entry_points.txt +0 -0
- {unit3dprep-1.1.2 → unit3dprep-1.1.4}/unit3dprep.egg-info/requires.txt +0 -0
- {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.
|
|
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.
|
|
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
|
|