feat: intial commits
This commit is contained in:
+41
-30
@@ -1,41 +1,52 @@
|
|||||||
# Sonarr
|
# =================================
|
||||||
|
# NFOGuard - Simple Configuration
|
||||||
|
# =================================
|
||||||
|
|
||||||
|
# --- Required: Media Library Paths ---
|
||||||
|
# Comma-separated paths to your media libraries
|
||||||
|
TV_PATHS=/media/TV/tv, /media/TV/tv6
|
||||||
|
MOVIE_PATHS=/media/Movies/movies, /media/Movies/movies6
|
||||||
|
|
||||||
|
# --- Required: Sonarr/Radarr Connection ---
|
||||||
SONARR_URL=http://sonarr:8989
|
SONARR_URL=http://sonarr:8989
|
||||||
SONARR_API_KEY=
|
SONARR_API_KEY=
|
||||||
SONARR_TIMEOUT=45
|
|
||||||
SONARR_RETRIES=3
|
|
||||||
|
|
||||||
RADARR_URL=http://radarr:7878
|
RADARR_URL=http://radarr:7878
|
||||||
RADARR_API_KEY=
|
RADARR_API_KEY=
|
||||||
RADARR_TIMEOUT=45
|
|
||||||
RADARR_RETRIES=3
|
|
||||||
|
|
||||||
|
# --- Required: TMDB for Release Dates ---
|
||||||
|
# Get free API key from https://www.themoviedb.org/settings/api
|
||||||
|
TMDB_API_KEY=
|
||||||
|
TMDB_COUNTRY=US
|
||||||
|
|
||||||
# Gotify (optional)
|
# --- Behavior Settings ---
|
||||||
GOTIFY_URL=
|
# When to update existing dates: always, missing_only, never
|
||||||
GOTIFY_TOKEN=-s
|
UPDATE_MODE=always
|
||||||
|
|
||||||
|
# Update file modification times to match chosen date: update, leave_alone
|
||||||
|
MTIME_BEHAVIOR=update
|
||||||
|
|
||||||
#TMDB_API_KEY=
|
# Lock metadata in NFO files to prevent overwrites: true, false
|
||||||
#TMDB_PRIMARY_COUNTRY=US
|
|
||||||
#TMDB_TIMEOUT=45
|
|
||||||
#TMDB_RETRIES=3
|
|
||||||
|
|
||||||
|
|
||||||
#OMDB_API_KEY=
|
|
||||||
TVDB_API_KEY=
|
|
||||||
# Batching configuration
|
|
||||||
BATCH_DELAY=5.0 # Seconds to wait for more webhooks
|
|
||||||
MAX_CONCURRENT_SERIES=3 # Max series proceossing simultaneously
|
|
||||||
|
|
||||||
TV_PATHS=/media/TV/tv,/media/TV/tv6
|
|
||||||
MOVIE_PATHS=/media/Movies/movies,/media/Movies/movies6
|
|
||||||
|
|
||||||
# Batching (optional)
|
|
||||||
#BATCH_DELAY=5.0
|
|
||||||
#MAX_CONCURRENT_SERIES=3
|
|
||||||
|
|
||||||
# Features (optional)
|
|
||||||
MANAGE_NFO=true
|
|
||||||
FIX_DIR_MTIMES=true
|
|
||||||
LOCK_METADATA=true
|
LOCK_METADATA=true
|
||||||
|
|
||||||
|
# Brand name in NFO comments
|
||||||
|
MANAGER_BRAND=NFOGuard
|
||||||
|
|
||||||
|
# --- Optional: Path Mapping for Containers ---
|
||||||
|
# If your Radarr/Sonarr containers see different paths than your webhook server
|
||||||
|
# Format: container_path->host_path,another_container_path->another_host_path
|
||||||
|
RADARR_PATH_MAP=/mnt/unionfs/Media/Movies->/media/Movies/movies,/mnt/unionfs/Media/Movies6->/media/Movies/movies6
|
||||||
|
SONARR_PATH_MAP=/mnt/unionfs/Media/TV->/media/TV/tv->/media/TV/tv6
|
||||||
|
|
||||||
|
# --- Optional: Additional Digital Release Sources ---
|
||||||
|
#OMDB_API_KEY=your_omdb_api_key_here
|
||||||
|
JELLYSEERR_URL=http://jellyseerr:5055
|
||||||
|
JELLYSEERR_API_KEY=
|
||||||
|
|
||||||
|
# --- Optional: Advanced Settings ---
|
||||||
|
BATCH_DELAY=5.0
|
||||||
|
MAX_CONCURRENT_SERIES=3
|
||||||
|
TIMEOUT_SECONDS=45
|
||||||
DEBUG=false
|
DEBUG=false
|
||||||
|
DB_PATH=/app/data/media_dates.db
|
||||||
|
PORT=8080
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
services:
|
||||||
|
NFOguard:
|
||||||
|
build: .
|
||||||
|
container_name: NFOguard
|
||||||
|
restart: unless-stopped
|
||||||
|
user: "1000:1000"
|
||||||
|
networks:
|
||||||
|
- saltbox
|
||||||
|
ports:
|
||||||
|
- "8085:8080"
|
||||||
|
volumes:
|
||||||
|
- /mnt/unionfs/Media/:/media:rw
|
||||||
|
- ./data:/app/data:rw
|
||||||
|
- ./.env:/app/.env:ro
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
||||||
|
|
||||||
|
networks:
|
||||||
|
saltbox:
|
||||||
|
external: true
|
||||||
+99
-17
@@ -312,8 +312,11 @@ class RadarrClient:
|
|||||||
"sortKey": "date", "sortDirection": "ascending"
|
"sortKey": "date", "sortDirection": "ascending"
|
||||||
})
|
})
|
||||||
if not data:
|
if not data:
|
||||||
|
_log("DEBUG", f"No history data returned for movie_id {movie_id}")
|
||||||
return None
|
return None
|
||||||
items = data if isinstance(data, list) else data.get("records") or []
|
items = data if isinstance(data, list) else data.get("records") or []
|
||||||
|
_log("DEBUG", f"Found {len(items)} history items for movie_id {movie_id}")
|
||||||
|
|
||||||
first_grab = None
|
first_grab = None
|
||||||
for it in items:
|
for it in items:
|
||||||
ev = (it.get("eventType") or it.get("type") or "").lower()
|
ev = (it.get("eventType") or it.get("type") or "").lower()
|
||||||
@@ -323,13 +326,27 @@ class RadarrClient:
|
|||||||
date_iso = datetime.fromisoformat(it["date"].replace("Z", "+00:00")).astimezone(timezone.utc).isoformat(timespec="seconds")
|
date_iso = datetime.fromisoformat(it["date"].replace("Z", "+00:00")).astimezone(timezone.utc).isoformat(timespec="seconds")
|
||||||
except Exception:
|
except Exception:
|
||||||
date_iso = None
|
date_iso = None
|
||||||
if ev in ("downloadfolderimported", "moviefileimported"):
|
|
||||||
|
_log("DEBUG", f"History event: type='{ev}', date={date_iso}, data={it.get('data', {})}")
|
||||||
|
|
||||||
|
# Expanded event types for Radarr v4/v5 compatibility
|
||||||
|
import_events = {
|
||||||
|
"downloadfolderimported", "moviefileimported", "imported",
|
||||||
|
"downloadimported", "fileimported", "movieimported"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ev in import_events:
|
||||||
if date_iso:
|
if date_iso:
|
||||||
|
_log("INFO", f"Found import event '{ev}' at {date_iso} for movie_id {movie_id}")
|
||||||
return date_iso
|
return date_iso
|
||||||
if ev == "grabbed" and date_iso and first_grab is None:
|
if ev == "grabbed" and date_iso and first_grab is None:
|
||||||
first_grab = date_iso
|
first_grab = date_iso
|
||||||
|
|
||||||
if first_grab:
|
if first_grab:
|
||||||
|
_log("INFO", f"Using grab date {first_grab} as fallback for movie_id {movie_id}")
|
||||||
return first_grab
|
return first_grab
|
||||||
|
|
||||||
|
_log("WARNING", f"No usable import/grab events found for movie_id {movie_id}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# ---------------------------
|
# ---------------------------
|
||||||
@@ -523,7 +540,11 @@ def upsert_movie_nfo(
|
|||||||
_ensure_child_text(root, "year", ymd.split("-")[0], overwrite=False)
|
_ensure_child_text(root, "year", ymd.split("-")[0], overwrite=False)
|
||||||
|
|
||||||
if dateadded:
|
if dateadded:
|
||||||
_ensure_child_text(root, "dateadded", dateadded, overwrite=True)
|
if dateadded == "MANUAL_REVIEW_NEEDED":
|
||||||
|
_ensure_child_text(root, "dateadded", "MANUAL_REVIEW_NEEDED", overwrite=True)
|
||||||
|
_log("WARNING", f"Set MANUAL_REVIEW_NEEDED marker in NFO: {movie_dir}/movie.nfo")
|
||||||
|
else:
|
||||||
|
_ensure_child_text(root, "dateadded", dateadded, overwrite=True)
|
||||||
|
|
||||||
if lock_metadata:
|
if lock_metadata:
|
||||||
_ensure_child_text(root, "lockdata", "true", overwrite=True)
|
_ensure_child_text(root, "lockdata", "true", overwrite=True)
|
||||||
@@ -552,9 +573,16 @@ def _parse_date_to_iso(d: str) -> Optional[str]:
|
|||||||
try:
|
try:
|
||||||
if re.fullmatch(r"\d{4}-\d{2}-\d{2}", d):
|
if re.fullmatch(r"\d{4}-\d{2}-\d{2}", d):
|
||||||
dt = datetime.fromisoformat(d).replace(tzinfo=timezone.utc)
|
dt = datetime.fromisoformat(d).replace(tzinfo=timezone.utc)
|
||||||
return dt.isoformat(timespec="seconds")
|
else:
|
||||||
dt = datetime.fromisoformat(d.replace("Z", "+00:00"))
|
dt = datetime.fromisoformat(d.replace("Z", "+00:00")).astimezone(timezone.utc)
|
||||||
return dt.astimezone(timezone.utc).isoformat(timespec="seconds")
|
|
||||||
|
# Reject dates that are too recent (prevent bogus timestamps)
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
if (now - dt).days < 1:
|
||||||
|
_log("WARNING", f"Rejecting recent timestamp (less than 1 day old): {d} -> {dt.isoformat()}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
return dt.isoformat(timespec="seconds")
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -587,21 +615,32 @@ def _strategy_movie_date_from_movie_obj(movie_obj: Dict[str, Any], which: str) -
|
|||||||
|
|
||||||
def _collect_digital_candidates(imdb_id: str, movie_obj: Optional[Dict[str, Any]]) -> List[Tuple[str, str]]:
|
def _collect_digital_candidates(imdb_id: str, movie_obj: Optional[Dict[str, Any]]) -> List[Tuple[str, str]]:
|
||||||
cands: List[Tuple[str, str]] = []
|
cands: List[Tuple[str, str]] = []
|
||||||
|
_log("DEBUG", f"Collecting digital dates for {imdb_id}")
|
||||||
|
|
||||||
if movie_obj:
|
if movie_obj:
|
||||||
iso = _strategy_movie_date_from_movie_obj(movie_obj, "digital")
|
iso = _strategy_movie_date_from_movie_obj(movie_obj, "digital")
|
||||||
if iso:
|
if iso:
|
||||||
cands.append((iso, "radarr:digitalRelease"))
|
cands.append((iso, "radarr:digitalRelease"))
|
||||||
|
_log("DEBUG", f"Found Radarr digital date: {iso}")
|
||||||
|
|
||||||
iso = tmdb_digital_date_from_imdb(imdb_id)
|
iso = tmdb_digital_date_from_imdb(imdb_id)
|
||||||
if iso:
|
if iso:
|
||||||
cands.append((iso, "tmdb:digital"))
|
cands.append((iso, "tmdb:digital"))
|
||||||
|
_log("DEBUG", f"Found TMDB digital date: {iso}")
|
||||||
|
|
||||||
tmdb_id = _tmdb_id_from_imdb(imdb_id)
|
tmdb_id = _tmdb_id_from_imdb(imdb_id)
|
||||||
if tmdb_id:
|
if tmdb_id:
|
||||||
iso = jellyseerr_digital_date_from_tmdb(tmdb_id)
|
iso = jellyseerr_digital_date_from_tmdb(tmdb_id)
|
||||||
if iso:
|
if iso:
|
||||||
cands.append((iso, "jellyseerr:digital"))
|
cands.append((iso, "jellyseerr:digital"))
|
||||||
|
_log("DEBUG", f"Found Jellyseerr digital date: {iso}")
|
||||||
|
|
||||||
iso = omdb_dvd_date(imdb_id)
|
iso = omdb_dvd_date(imdb_id)
|
||||||
if iso:
|
if iso:
|
||||||
cands.append((iso, "omdb:dvd"))
|
cands.append((iso, "omdb:dvd"))
|
||||||
|
_log("DEBUG", f"Found OMDb DVD date: {iso}")
|
||||||
|
|
||||||
|
_log("INFO", f"Found {len(cands)} digital date candidates: {[f'{src}={date}' for date, src in cands]}")
|
||||||
return cands
|
return cands
|
||||||
|
|
||||||
def _pick_earliest(cands: List[Tuple[str, str]]) -> Optional[Tuple[str, str]]:
|
def _pick_earliest(cands: List[Tuple[str, str]]) -> Optional[Tuple[str, str]]:
|
||||||
@@ -654,15 +693,24 @@ def _decide_movie_date(imdb_id: str, movie_dir: Path, poll_external: bool) -> Tu
|
|||||||
|
|
||||||
def from_import_history() -> Tuple[Optional[str], str]:
|
def from_import_history() -> Tuple[Optional[str], str]:
|
||||||
ensure_movie()
|
ensure_movie()
|
||||||
if poll_external and movie_id:
|
if not (poll_external and movie_id):
|
||||||
iso = _radarr_earliest_import_history_iso(movie_id)
|
_log("DEBUG", f"Skipping import history: poll_external={poll_external}, movie_id={movie_id}")
|
||||||
if iso:
|
return None, "radarr:history.skipped"
|
||||||
return iso, "radarr:history.import"
|
|
||||||
if poll_external and movie_id:
|
# Try import history events first (most reliable)
|
||||||
iso2 = _radarr_earliest_file_dateadded_iso(movie_id)
|
iso = _radarr_earliest_import_history_iso(movie_id)
|
||||||
if iso2:
|
if iso:
|
||||||
return iso2, "radarr:moviefile.dateAdded"
|
_log("INFO", f"Using Radarr import history date: {iso}")
|
||||||
return None, "radarr:history"
|
return iso, "radarr:history.import"
|
||||||
|
|
||||||
|
# Fallback to moviefile dateAdded
|
||||||
|
iso2 = _radarr_earliest_file_dateadded_iso(movie_id)
|
||||||
|
if iso2:
|
||||||
|
_log("INFO", f"Using Radarr moviefile dateAdded: {iso2}")
|
||||||
|
return iso2, "radarr:moviefile.dateAdded"
|
||||||
|
|
||||||
|
_log("WARNING", f"No import dates found for movie_id {movie_id}")
|
||||||
|
return None, "radarr:history.none"
|
||||||
|
|
||||||
def from_radarr_field(which: str) -> Tuple[Optional[str], str]:
|
def from_radarr_field(which: str) -> Tuple[Optional[str], str]:
|
||||||
ensure_movie()
|
ensure_movie()
|
||||||
@@ -715,33 +763,67 @@ def _decide_movie_date(imdb_id: str, movie_dir: Path, poll_external: bool) -> Tu
|
|||||||
return digi_iso, "radarr:digitalRelease"
|
return digi_iso, "radarr:digitalRelease"
|
||||||
|
|
||||||
# Priority flows
|
# Priority flows
|
||||||
|
_log("INFO", f"Date selection for {imdb_id}: priority={config.movie_priority}, poll_external={poll_external}")
|
||||||
|
|
||||||
if config.movie_priority == "import_then_digital":
|
if config.movie_priority == "import_then_digital":
|
||||||
|
_log("DEBUG", "Trying import history first (import_then_digital mode)")
|
||||||
imp_iso, imp_src = from_import_history()
|
imp_iso, imp_src = from_import_history()
|
||||||
if imp_iso:
|
if imp_iso:
|
||||||
|
_log("INFO", f"Selected import date: {imp_iso} ({imp_src})")
|
||||||
return imp_iso, imp_src, released_iso
|
return imp_iso, imp_src, released_iso
|
||||||
|
|
||||||
|
_log("DEBUG", "No import date found, trying digital release dates")
|
||||||
digi_iso, digi_src = from_external_digital_bundle()
|
digi_iso, digi_src = from_external_digital_bundle()
|
||||||
digi_iso, digi_src = maybe_sanitize_digital(digi_iso)
|
digi_iso, digi_src = maybe_sanitize_digital(digi_iso)
|
||||||
if digi_iso:
|
if digi_iso:
|
||||||
|
_log("INFO", f"Selected digital date: {digi_iso} ({digi_src})")
|
||||||
return digi_iso, digi_src, released_iso
|
return digi_iso, digi_src, released_iso
|
||||||
|
|
||||||
|
_log("DEBUG", "No digital date found, trying cinema release")
|
||||||
cine_iso, cine_src = from_radarr_field("cinema")
|
cine_iso, cine_src = from_radarr_field("cinema")
|
||||||
if cine_iso:
|
if cine_iso:
|
||||||
|
_log("INFO", f"Selected cinema date: {cine_iso} ({cine_src})")
|
||||||
return cine_iso, cine_src, released_iso or cine_iso
|
return cine_iso, cine_src, released_iso or cine_iso
|
||||||
|
|
||||||
|
_log("DEBUG", "No external dates found, falling back to file mtime")
|
||||||
mtime_iso, mtime_src = from_file_mtime()
|
mtime_iso, mtime_src = from_file_mtime()
|
||||||
return mtime_iso, mtime_src, released_iso
|
if mtime_iso:
|
||||||
|
_log("WARNING", f"Using file mtime as last resort: {mtime_iso} ({mtime_src})")
|
||||||
|
return mtime_iso, mtime_src, released_iso
|
||||||
|
|
||||||
|
# No valid date found - create manual review marker
|
||||||
|
_log("ERROR", f"No valid dateadded found for {imdb_id} - marking for MANUAL_REVIEW_NEEDED")
|
||||||
|
return "MANUAL_REVIEW_NEEDED", "manual_review_required", released_iso
|
||||||
|
|
||||||
# digital_then_import
|
# digital_then_import
|
||||||
|
_log("DEBUG", "Trying digital release dates first (digital_then_import mode)")
|
||||||
digi_iso, digi_src = from_external_digital_bundle()
|
digi_iso, digi_src = from_external_digital_bundle()
|
||||||
digi_iso, digi_src = maybe_sanitize_digital(digi_iso)
|
digi_iso, digi_src = maybe_sanitize_digital(digi_iso)
|
||||||
if digi_iso:
|
if digi_iso:
|
||||||
|
_log("INFO", f"Selected digital date: {digi_iso} ({digi_src})")
|
||||||
return digi_iso, digi_src, released_iso
|
return digi_iso, digi_src, released_iso
|
||||||
|
|
||||||
|
_log("DEBUG", "No digital date found, trying import history")
|
||||||
imp_iso, imp_src = from_import_history()
|
imp_iso, imp_src = from_import_history()
|
||||||
if imp_iso:
|
if imp_iso:
|
||||||
|
_log("INFO", f"Selected import date: {imp_iso} ({imp_src})")
|
||||||
return imp_iso, imp_src, released_iso
|
return imp_iso, imp_src, released_iso
|
||||||
|
|
||||||
|
_log("DEBUG", "No import date found, trying cinema release")
|
||||||
cine_iso, cine_src = from_radarr_field("cinema")
|
cine_iso, cine_src = from_radarr_field("cinema")
|
||||||
if cine_iso:
|
if cine_iso:
|
||||||
|
_log("INFO", f"Selected cinema date: {cine_iso} ({cine_src})")
|
||||||
return cine_iso, cine_src, released_iso or cine_iso
|
return cine_iso, cine_src, released_iso or cine_iso
|
||||||
|
|
||||||
|
_log("DEBUG", "No external dates found, falling back to file mtime")
|
||||||
mtime_iso, mtime_src = from_file_mtime()
|
mtime_iso, mtime_src = from_file_mtime()
|
||||||
return mtime_iso, mtime_src, released_iso
|
if mtime_iso:
|
||||||
|
_log("WARNING", f"Using file mtime as last resort: {mtime_iso} ({mtime_src})")
|
||||||
|
return mtime_iso, mtime_src, released_iso
|
||||||
|
|
||||||
|
# No valid date found - create manual review marker
|
||||||
|
_log("ERROR", f"No valid dateadded found for {imdb_id} - marking for MANUAL_REVIEW_NEEDED")
|
||||||
|
return "MANUAL_REVIEW_NEEDED", "manual_review_required", released_iso
|
||||||
|
|
||||||
# ---------------------------
|
# ---------------------------
|
||||||
# Movie apply
|
# Movie apply
|
||||||
@@ -854,7 +936,7 @@ def apply_movie(movie_dir: Path, conn: sqlite3.Connection):
|
|||||||
brand=config.manager_brand,
|
brand=config.manager_brand,
|
||||||
)
|
)
|
||||||
|
|
||||||
if config.fix_dir_mtimes and dateadded_iso:
|
if config.fix_dir_mtimes and dateadded_iso and dateadded_iso != "MANUAL_REVIEW_NEEDED":
|
||||||
set_movie_file_mtime(movie_dir, dateadded_iso)
|
set_movie_file_mtime(movie_dir, dateadded_iso)
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|||||||
Reference in New Issue
Block a user