Skip to content

Commit 6c06132

Browse files
cenodudePazzie van den Berg
andauthored
Refactor: standardize adapters across media servers and trackers (#194)
* Refactor: standardize provider metadata, user-agent setup, and write skip handling * Change: align scoped state writes with capture-mode safe behavior * Change: standardize resolve, cache merge, and write logging semantics * Refactor: standardize module write/index logging and provider result reporting * Refactor: centralize scoped state, snapshot, restore, and unresolved helpers * Refactor: move shared state handling into common helpers and standardize logging * Refactor: reuse common state helpers and normalize ratings persistence flow * Refactor: centralize snapshot and restore handling through common utilities * Refactor: standardize module metadata, user-agent headers, and read-only write handling * Change: standardize index and read-only write logging semantics * Change: align scoped state writes with capture-mode safe behavior * Refactor: standardize module metadata, user-agent handling, and provider write logging * Change: align scoped state writes with capture-mode safe behavior * Change: standardize index and write logging for ratings sync * Change: standardize index and write logging for watchlist sync * Refactor: standardize provider capabilities logging and shared module wiring * Change: centralize shared logging, provider index, and resolver helpers * Change: switch to common logger helpers and normalize history event logging * Change: reuse common logging/time helpers and standardize write result events * Change: reuse common logger helpers and standardize progress sync events * Change: align helper behavior with updated shared provider conventions * Change: move to shared logger helpers and normalize watchlist write logging * Refactor: standardize provider internals logging and capability metadata * Change: centralize shared logging, provider index, and resolver helpers * Change: move to common logger helpers and normalize history sync logging * Change: reuse common logger helpers and standardize progress sync logging * Change: reuse common logging/time helpers and standardize write result event * Change: align helper behavior with updated shared provider conventions * Change: switch to shared logger helpers and normalize watchlist sync events * Refactor: centralize provider behavior and normalize capability reporting * Change: centralize shared logging, token, guid, and resolver helpers * Refactor: move shared guid, cache, and write helpers into common utilities * Refactor: reuse shared resolver and token helpers and normalize write result logging * Refactor: switch to shared token, config, and logging helpers * Refactor: align progress sync with shared logger and resolver helpers * Change: align utility headers and helper behavior with shared plex common settings --------- Co-authored-by: Pazzie van den Berg <[email protected]>
1 parent ac45bce commit 6c06132

36 files changed

Lines changed: 1227 additions & 1667 deletions

providers/sync/_mod_ANILIST.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@ def _confirmed_keys(key_of, items: Iterable[Mapping[str, Any]], unresolved: Any)
5454
seen.add(k)
5555
return out
5656

57-
__VERSION__ = "1.0.0"
57+
__VERSION__ = "1.0"
58+
os.environ.setdefault("CW_ANILIST_VERSION", __VERSION__)
59+
os.environ.setdefault("CW_ANILIST_UA", f"CrossWatch/{__VERSION__} (AniList)")
5860
__all__ = ["get_manifest", "ANILISTModule", "OPS"]
5961

6062
def _health(status: str, ok: bool, latency_ms: int) -> None:
@@ -86,7 +88,7 @@ def emit(self, *args: Any, **kwargs: Any) -> None:
8688

8789

8890
GQL_URL = "https://graphql.anilist.co"
89-
UA = "CrossWatch/1.0"
91+
UA = os.environ.get("CW_ANILIST_UA") or os.environ.get("CW_UA") or f"CrossWatch/{__VERSION__} (AniList)"
9092

9193

9294
class ANILISTError(RuntimeError):
@@ -378,6 +380,7 @@ def feature_names(self) -> tuple[str, ...]:
378380

379381
def build_index(self, feature: str, **kwargs: Any) -> dict[str, dict[str, Any]]:
380382
if feature != "watchlist" or not feat_watchlist:
383+
_info("index_skipped", feature=feature, reason="disabled_or_missing")
381384
return {}
382385
return feat_watchlist.build_index(self)
383386

@@ -394,6 +397,7 @@ def add(
394397
if dry_run:
395398
return {"ok": True, "count": len(lst), "dry_run": True}
396399
if feature != "watchlist" or not feat_watchlist:
400+
_info("write_skipped", op="add", feature=feature, reason="disabled_or_missing")
397401
return {"ok": True, "count": 0, "unresolved": []}
398402
if hasattr(feat_watchlist, "add_detailed"):
399403
res = feat_watchlist.add_detailed(self, lst) # type: ignore[attr-defined]
@@ -425,6 +429,7 @@ def remove(
425429
if dry_run:
426430
return {"ok": True, "count": len(lst), "dry_run": True}
427431
if feature != "watchlist" or not feat_watchlist:
432+
_info("write_skipped", op="remove", feature=feature, reason="disabled_or_missing")
428433
return {"ok": True, "count": 0, "unresolved": []}
429434
count, unresolved = feat_watchlist.remove(self, lst)
430435
confirmed_keys = _confirmed_keys(self.key_of, lst, unresolved)

providers/sync/_mod_CROSSWATCH.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ def cw_log(provider: str, feature: str, level: str, msg: str, **fields: Any) ->
9292
except Exception:
9393
make_snapshot_progress = None # type: ignore[assignment]
9494

95-
__VERSION__ = "1.0.0"
95+
__VERSION__ = "1.0"
9696
__all__ = ["get_manifest", "CROSSWATCHModule", "OPS"]
9797

9898
_FEATURES: dict[str, Any] = {}
@@ -261,7 +261,7 @@ def build_index(self, feature: str, **kwargs: Any) -> dict[str, dict[str, Any]]:
261261
return {}
262262
mod = _FEATURES.get(feature)
263263
if not mod:
264-
_warn(feature, "index_skipped", reason="module_missing")
264+
_info(feature, "index_skipped", reason="module_missing")
265265
return {}
266266
started = time.perf_counter()
267267
out = mod.build_index(self, **kwargs)
@@ -285,12 +285,12 @@ def add(
285285
return {"ok": True, "count": len(lst), "dry_run": True}
286286
mod = _FEATURES.get(feature)
287287
if not mod:
288-
_warn(feature, "write_skipped", op="add", reason="module_missing")
288+
_info(feature, "write_skipped", op="add", reason="module_missing")
289289
return {"ok": True, "count": 0, "unresolved": []}
290290
try:
291291
started = time.perf_counter()
292292
cnt, unresolved = mod.add(self, lst)
293-
_info(feature, "write_done", op="add", count=int(cnt), unresolved=len(unresolved), dur_ms=int((time.perf_counter() - started) * 1000))
293+
_info(feature, "write_done", op="add", ok=len(unresolved) == 0, applied=int(cnt), unresolved=len(unresolved), dur_ms=int((time.perf_counter() - started) * 1000))
294294
confirmed_keys = _confirmed_keys(_crosswatch_key_of, lst, unresolved)
295295
return {"ok": True, "count": int(cnt), "unresolved": unresolved, "confirmed_keys": confirmed_keys}
296296
except Exception as e:
@@ -314,12 +314,12 @@ def remove(
314314
return {"ok": True, "count": len(lst), "dry_run": True}
315315
mod = _FEATURES.get(feature)
316316
if not mod:
317-
_warn(feature, "write_skipped", op="remove", reason="module_missing")
317+
_info(feature, "write_skipped", op="remove", reason="module_missing")
318318
return {"ok": True, "count": 0, "unresolved": []}
319319
try:
320320
started = time.perf_counter()
321321
cnt, unresolved = mod.remove(self, lst)
322-
_info(feature, "write_done", op="remove", count=int(cnt), unresolved=len(unresolved), dur_ms=int((time.perf_counter() - started) * 1000))
322+
_info(feature, "write_done", op="remove", ok=len(unresolved) == 0, applied=int(cnt), unresolved=len(unresolved), dur_ms=int((time.perf_counter() - started) * 1000))
323323
confirmed_keys = _confirmed_keys(_crosswatch_key_of, lst, unresolved)
324324
return {"ok": True, "count": int(cnt), "unresolved": unresolved, "confirmed_keys": confirmed_keys}
325325
except Exception as e:

providers/sync/_mod_EMBY.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from ._log import log as cw_log
1717

1818
from .emby._common import normalize as emby_normalize, key_of as emby_key_of
19-
from .emby._common import _pair_scope as _emby_pair_scope, state_file as _emby_state_file
19+
from .emby._common import _pair_scope as _emby_pair_scope, state_file as _emby_state_file, _is_capture_mode as _emby_capture_mode
2020
from .emby import _watchlist as feat_watchlist
2121
from .emby import _history as feat_history
2222
from .emby import _ratings as feat_ratings
@@ -76,10 +76,12 @@ def _confirmed_keys(key_of, items: Iterable[Mapping[str, Any]], unresolved: Any)
7676
except Exception:
7777
ctx = None # type: ignore[assignment]
7878

79-
__VERSION__ = "3.3.0"
79+
__VERSION__ = "1.0"
80+
os.environ.setdefault("CW_EMBY_VERSION", __VERSION__)
81+
os.environ.setdefault("CW_EMBY_UA", f"CrossWatch/{__VERSION__} (Emby)")
8082
__all__ = ["get_manifest", "EMBYModule", "OPS"]
8183

82-
_DEF_UA = os.environ.get("CW_UA", f"CrossWatch/{__VERSION__} (Emby)")
84+
_DEF_UA = os.environ.get("CW_EMBY_UA") or os.environ.get("CW_UA") or f"CrossWatch/{__VERSION__} (Emby)"
8385

8486

8587
def _pick_instance_id(provider: str) -> str:
@@ -149,7 +151,7 @@ def _present_flags() -> dict[str, bool]:
149151

150152

151153
def _save_health_shadow(payload: Mapping[str, Any]) -> None:
152-
if _emby_pair_scope() is None:
154+
if _emby_pair_scope() is None or _emby_capture_mode():
153155
return
154156
try:
155157
path = _emby_state_file(_HEALTH_SHADOW_NAME)
@@ -172,7 +174,7 @@ def get_manifest() -> Mapping[str, Any]:
172174
"features": {
173175
"watchlist": True,
174176
"history": True,
175-
"ratings": False,
177+
"ratings": True,
176178
"playlists": False,
177179
"progress": True,
178180
},
@@ -399,7 +401,7 @@ def manifest(self) -> Mapping[str, Any]:
399401

400402
@staticmethod
401403
def supported_features() -> dict[str, bool]:
402-
toggles = {"watchlist": True, "history": True, "ratings": False, "playlists": False, "progress": True}
404+
toggles = {"watchlist": True, "history": True, "ratings": True, "playlists": False, "progress": True}
403405
present = _present_flags()
404406
return {k: bool(toggles.get(k, False) and present.get(k, False)) for k in toggles.keys()}
405407

@@ -545,7 +547,7 @@ def feature_names(self) -> tuple[str, ...]:
545547
def build_index(self, feature: str, **kwargs: Any) -> Mapping[str, dict[str, Any]]:
546548
f = (feature or "watchlist").lower()
547549
if not self._is_enabled(f):
548-
_dbg("feature_disabled", op="build_index", feature=f)
550+
_info("index_skipped", feature=f, reason="feature_disabled")
549551
return {}
550552
mod = _FEATURES.get(f)
551553
if not mod:
@@ -565,7 +567,7 @@ def add(
565567
) -> Mapping[str, Any]:
566568
f = (feature or "watchlist").lower()
567569
if not self._is_enabled(f):
568-
_dbg("feature_disabled", op="add", feature=f)
570+
_info("write_skipped", op="add", feature=f, reason="feature_disabled")
569571
return {"ok": True, "count": 0, "unresolved": []}
570572
if dry_run:
571573
return self._dry_result(items)
@@ -592,7 +594,7 @@ def remove(
592594
) -> Mapping[str, Any]:
593595
f = (feature or "watchlist").lower()
594596
if not self._is_enabled(f):
595-
_dbg("feature_disabled", op="remove", feature=f)
597+
_info("write_skipped", op="remove", feature=f, reason="feature_disabled")
596598
return {"ok": True, "count": 0, "unresolved": []}
597599
if dry_run:
598600
return self._dry_result(items)
@@ -676,4 +678,4 @@ def remove(
676678
def health(self, cfg: Mapping[str, Any]) -> Mapping[str, Any]:
677679
return self._adapter(cfg).health()
678680

679-
OPS = _EmbyOPS()
681+
OPS = _EmbyOPS()

providers/sync/_mod_JELLYFIN.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
from ._log import log as cw_log
1717

18-
from .jellyfin._common import normalize as jelly_normalize, key_of as jelly_key_of, _pair_scope as _jf_pair_scope, state_file as _jf_state_file
18+
from .jellyfin._common import normalize as jelly_normalize, key_of as jelly_key_of, _pair_scope as _jf_pair_scope, state_file as _jf_state_file, _is_capture_mode as _jf_capture_mode
1919
from .jellyfin import _watchlist as feat_watchlist
2020
from .jellyfin import _history as feat_history
2121
from .jellyfin import _ratings as feat_ratings
@@ -74,10 +74,12 @@ def _confirmed_keys(key_of, items: Iterable[Mapping[str, Any]], unresolved: Any)
7474
except Exception:
7575
ctx = None # type: ignore[assignment]
7676

77-
__VERSION__ = "3.3.0"
77+
__VERSION__ = "1.0"
78+
os.environ.setdefault("CW_JELLYFIN_VERSION", __VERSION__)
79+
os.environ.setdefault("CW_JELLYFIN_UA", f"CrossWatch/{__VERSION__} (Jellyfin)")
7880
__all__ = ["get_manifest", "JELLYFINModule", "OPS"]
7981

80-
_DEF_UA = os.environ.get("CW_UA", f"CrossWatch/{__VERSION__} (Jellyfin)")
82+
_DEF_UA = os.environ.get("CW_JELLYFIN_UA") or os.environ.get("CW_UA") or f"CrossWatch/{__VERSION__} (Jellyfin)"
8183

8284

8385
def _pick_instance_id(provider: str) -> str:
@@ -140,7 +142,7 @@ def _error(feature: str, msg: str, **fields: Any) -> None:
140142

141143

142144
def _save_health_shadow(payload: Mapping[str, Any]) -> None:
143-
if _jf_pair_scope() is None:
145+
if _jf_pair_scope() is None or _jf_capture_mode():
144146
return
145147
try:
146148
path = str(_jf_state_file(_HEALTH_SHADOW_NAME))
@@ -167,7 +169,7 @@ def get_manifest() -> Mapping[str, Any]:
167169
"features": {
168170
"watchlist": True,
169171
"history": True,
170-
"ratings": False,
172+
"ratings": True,
171173
"playlists": False,
172174
"progress": True,
173175
},
@@ -366,7 +368,7 @@ def manifest(self) -> Mapping[str, Any]:
366368

367369
@staticmethod
368370
def supported_features() -> dict[str, bool]:
369-
toggles = {"watchlist": True, "history": True, "ratings": False, "playlists": False, "progress": True}
371+
toggles = {"watchlist": True, "history": True, "ratings": True, "playlists": False, "progress": True}
370372
present = _present_flags()
371373
return {k: bool(toggles.get(k, False) and present.get(k, False)) for k in toggles.keys()}
372374

@@ -511,7 +513,7 @@ def feature_names(self) -> tuple[str, ...]:
511513
def build_index(self, feature: str, **kwargs: Any) -> Mapping[str, dict[str, Any]]:
512514
f = (feature or "watchlist").lower()
513515
if not self._is_enabled(f):
514-
_dbg(f, "build index skipped", reason="feature disabled")
516+
_info(f, "index_skipped", reason="feature_disabled")
515517
return {}
516518
mod = _FEATURES.get(f)
517519
if not mod:
@@ -531,7 +533,7 @@ def add(
531533
) -> Mapping[str, Any]:
532534
f = (feature or "watchlist").lower()
533535
if not self._is_enabled(f):
534-
_dbg(f, "add skipped", reason="feature disabled")
536+
_info(f, "write_skipped", op="add", reason="feature_disabled")
535537
return {"ok": True, "count": 0, "unresolved": []}
536538
if dry_run:
537539
return self._dry_result(items)
@@ -558,7 +560,7 @@ def remove(
558560
) -> Mapping[str, Any]:
559561
f = (feature or "watchlist").lower()
560562
if not self._is_enabled(f):
561-
_dbg(f, "remove skipped", reason="feature disabled")
563+
_info(f, "write_skipped", op="remove", reason="feature_disabled")
562564
return {"ok": True, "count": 0, "unresolved": []}
563565
if dry_run:
564566
return self._dry_result(items)
@@ -644,4 +646,4 @@ def remove(
644646
def health(self, cfg: Mapping[str, Any]) -> Mapping[str, Any]:
645647
return self._adapter(cfg).health()
646648

647-
OPS = _JellyfinOPS()
649+
OPS = _JellyfinOPS()

providers/sync/_mod_PLEX.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@ def _error(event: str, **fields: Any) -> None:
3535
def _log(msg: str) -> None:
3636
_dbg(msg)
3737

38-
__VERSION__ = "5.2.1"
38+
__VERSION__ = "1.0"
39+
os.environ.setdefault("CW_PLEX_VERSION", __VERSION__)
40+
os.environ.setdefault("CW_PLEX_UA", f"CrossWatch/{__VERSION__} (Plex)")
3941
__all__ = ["get_manifest", "PLEXModule", "PLEXClient", "PLEXError", "PLEXAuthError", "PLEXNotFound", "OPS"]
4042

4143
try:
@@ -1047,7 +1049,7 @@ def feature_names(self) -> tuple[str, ...]:
10471049

10481050
def build_index(self, feature: str, **kwargs) -> dict[str, dict[str, Any]]:
10491051
if not self._is_enabled(feature) or feature not in _FEATURES:
1050-
_info("feature_skipped", op="build_index", feature=feature, reason="disabled_or_missing")
1052+
_info("index_skipped", feature=feature, reason="disabled_or_missing")
10511053
return {}
10521054
mod = _FEATURES.get(feature)
10531055
return mod.build_index(self, **kwargs) if mod else {}
@@ -1063,13 +1065,13 @@ def add(
10631065
if not lst:
10641066
return {"ok": True, "count": 0}
10651067
if not self._is_enabled(feature) or feature not in _FEATURES:
1066-
_info("feature_skipped", op="add", feature=feature, reason="disabled_or_missing")
1068+
_info("write_skipped", op="add", feature=feature, reason="disabled_or_missing")
10671069
return {"ok": True, "count": 0, "unresolved": []}
10681070
if dry_run:
10691071
return {"ok": True, "count": len(lst), "dry_run": True}
10701072
mod = _FEATURES.get(feature)
10711073
if not mod:
1072-
_warn("feature_missing", op="add", feature=feature)
1074+
_warn("write_skipped", op="add", feature=feature, reason="missing_feature")
10731075
return {"ok": True, "count": 0, "unresolved": []}
10741076
try:
10751077
cnt, unresolved = mod.add(self, lst)
@@ -1115,13 +1117,13 @@ def remove(
11151117
if not lst:
11161118
return {"ok": True, "count": 0}
11171119
if not self._is_enabled(feature) or feature not in _FEATURES:
1118-
_info("feature_skipped", op="remove", feature=feature, reason="disabled_or_missing")
1120+
_info("write_skipped", op="remove", feature=feature, reason="disabled_or_missing")
11191121
return {"ok": True, "count": 0, "unresolved": []}
11201122
if dry_run:
11211123
return {"ok": True, "count": len(lst), "dry_run": True}
11221124
mod = _FEATURES.get(feature)
11231125
if not mod:
1124-
_warn("feature_missing", op="remove", feature=feature)
1126+
_warn("write_skipped", op="remove", feature=feature, reason="missing_feature")
11251127
return {"ok": True, "count": 0, "unresolved": []}
11261128
try:
11271129
cnt, unresolved = mod.remove(self, lst)

providers/sync/_mod_TAUTULLI.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from collections.abc import Iterable, Mapping
77
from dataclasses import dataclass
88
from typing import Any
9+
import os
910
import time
1011

1112
from ._log import log as cw_log
@@ -16,7 +17,9 @@
1617
except Exception:
1718
ctx = None # type: ignore[assignment]
1819

19-
__VERSION__ = "1.0.0"
20+
__VERSION__ = "1.0"
21+
os.environ.setdefault("CW_TAUTULLI_VERSION", __VERSION__)
22+
os.environ.setdefault("CW_TAUTULLI_UA", f"CrossWatch/{__VERSION__} (Tautulli)")
2023
__all__ = ["get_manifest", "OPS"]
2124

2225
def _health(status: str, ok: bool, latency_ms: int) -> None:
@@ -106,6 +109,11 @@ def __init__(self, cfg: TAUTULLIConfig, raw_cfg: Mapping[str, Any]):
106109
self.cfg = cfg
107110
self.raw_cfg = raw_cfg
108111
self.session = build_session("TAUTULLI", ctx, feature_label=_label)
112+
try:
113+
self.session.headers.setdefault("User-Agent", os.environ.get("CW_TAUTULLI_UA") or f"CrossWatch/{__VERSION__} (Tautulli)")
114+
self.session.headers.setdefault("Accept", "application/json")
115+
except Exception:
116+
pass
109117

110118
def _url(self) -> str:
111119
return f"{self.cfg.server_url}/api/v2"
@@ -130,12 +138,12 @@ def call(self, cmd: str, **params: Any) -> Any:
130138
if isinstance(resp, dict):
131139
if str(resp.get("result") or "").lower() != "success":
132140
msg = str(resp.get("message") or "unknown error")
133-
_warn("api result not success", cmd=cmd, message=msg)
141+
_warn("http_failed", op=cmd, message=msg)
134142
raise RuntimeError(msg)
135143
return resp.get("data")
136144

137145
if r.status_code >= 400:
138-
_warn("http non ok", cmd=cmd, status=r.status_code)
146+
_warn("http_failed", op=cmd, status=r.status_code)
139147
raise RuntimeError(f"HTTP {r.status_code}")
140148
return j
141149

@@ -185,24 +193,28 @@ def activities(self) -> Mapping[str, Any]:
185193
ts = rows[0].get("date") or rows[0].get("started")
186194
return {"history": str(ts or "0"), "updated_at": str(ts or "0")}
187195
except Exception as e:
188-
_dbg("activities failed", error=str(e))
196+
_dbg("activities_failed", error=str(e))
189197
return {"updated_at": "0"}
190198

191199
def build_index(self, feature: str) -> Mapping[str, dict[str, Any]]:
192200
if feature != "history":
193-
_dbg("unsupported feature", requested=feature)
201+
_info("index_skipped", feature=feature, reason="disabled_or_missing")
194202
return {}
195203
from .tautulli import _history
196204

197205
adapter = _HistoryAdapter(cfg=self.client.raw_cfg, client=self.client)
198206
return _history.build_index(adapter)
199207

200208
def add(self, feature: str, items: Iterable[Mapping[str, Any]], *, dry_run: bool = False) -> dict[str, Any]:
209+
if feature != "history":
210+
return {"ok": True, "count": 0, "unresolved": [], "reason": "disabled_or_missing"}
201211
from .tautulli import _history
202212

203213
return _history.add(self, items, dry_run=dry_run)
204214

205215
def remove(self, feature: str, items: Iterable[Mapping[str, Any]], *, dry_run: bool = False) -> dict[str, Any]:
216+
if feature != "history":
217+
return {"ok": True, "count": 0, "unresolved": [], "reason": "disabled_or_missing"}
206218
from .tautulli import _history
207219

208220
return _history.remove(self, items, dry_run=dry_run)

0 commit comments

Comments
 (0)