Archived
Add watchlist filesystem reconciliation
This commit is contained in:
@@ -987,6 +987,14 @@ def watchlist_source_library_dir(item, config):
|
||||
return Path(str((config or {}).get("download_dir") or DEFAULT_CONFIG["download_dir"])).expanduser() / media_type / watchlist_download_library_name(item)
|
||||
|
||||
|
||||
def watchlist_source_season_dir(item, config):
|
||||
source_root = watchlist_source_library_dir(item, config)
|
||||
if normalize_media_type(item.get("media_type")) == "movie":
|
||||
return source_root
|
||||
season = normalize_season((item or {}).get("auto_download_series") or "1")
|
||||
return source_root / f"Season {int(season):02d}"
|
||||
|
||||
|
||||
def jellyfin_target_library_dir(item, config):
|
||||
media_type = normalize_media_type(item.get("media_type"))
|
||||
key = "jellyfin_movie_dir" if media_type == "movie" else "jellyfin_tv_dir"
|
||||
@@ -1043,6 +1051,62 @@ def watchlist_item_download_complete(item, mode=""):
|
||||
return True, "ready"
|
||||
|
||||
|
||||
def normalize_downloaded_episode_value(value):
|
||||
text = str(value or "").strip()
|
||||
if not text:
|
||||
return ""
|
||||
match = re.match(r"^0*([0-9]+)(.*)$", text)
|
||||
if not match:
|
||||
return text
|
||||
number = int(match.group(1) or "0", 10)
|
||||
suffix = str(match.group(2) or "")
|
||||
return f"{number}{suffix}" if number > 0 else text
|
||||
|
||||
|
||||
def reverse_episode_offset(value, offset):
|
||||
normalized_offset = normalize_episode_offset(offset)
|
||||
text = str(value or "").strip()
|
||||
if not normalized_offset or not text:
|
||||
return normalize_downloaded_episode_value(text)
|
||||
match = re.match(r"^0*([0-9]+)(.*)$", text)
|
||||
if not match:
|
||||
return normalize_downloaded_episode_value(text)
|
||||
number = int(match.group(1) or "0", 10)
|
||||
if number < 1:
|
||||
return normalize_downloaded_episode_value(text)
|
||||
logical_number = number - int(normalized_offset, 10) + 1
|
||||
if logical_number < 1:
|
||||
return ""
|
||||
suffix = str(match.group(2) or "")
|
||||
return f"{logical_number}{suffix}"
|
||||
|
||||
|
||||
def watchlist_downloaded_episode_values_from_filesystem(item, config, mode):
|
||||
normalized_mode = str(mode or "").strip().lower()
|
||||
if normalized_mode not in MODE_CHOICES:
|
||||
return []
|
||||
season_dir = watchlist_source_season_dir(item, config)
|
||||
if not season_dir.exists() or not season_dir.is_dir():
|
||||
return []
|
||||
season = normalize_season((item or {}).get("auto_download_series") or "1")
|
||||
pattern = re.compile(rf"\bS{int(season):02d}E([0-9][0-9A-Za-z.\-]*)\b", re.IGNORECASE)
|
||||
values = []
|
||||
seen = set()
|
||||
offset = (item or {}).get("auto_download_offset")
|
||||
for path in sorted(season_dir.rglob("*")):
|
||||
if not path.is_file():
|
||||
continue
|
||||
match = pattern.search(path.stem)
|
||||
if not match:
|
||||
continue
|
||||
logical_value = reverse_episode_offset(match.group(1), offset)
|
||||
normalized_value = normalize_downloaded_episode_value(logical_value)
|
||||
if normalized_value and normalized_value not in seen:
|
||||
seen.add(normalized_value)
|
||||
values.append(normalized_value)
|
||||
return values
|
||||
|
||||
|
||||
def _move_tree_contents(source_dir, target_dir, progress_fn=None):
|
||||
moved = []
|
||||
file_paths = [
|
||||
@@ -2095,6 +2159,81 @@ class WatchlistStore:
|
||||
)
|
||||
return self.schedule_refresh(existing["show_id"], source=source)
|
||||
|
||||
def reconcile_downloaded_filesystem(self, config=None):
|
||||
active_config = normalize_config(config or self.config_getter() or {})
|
||||
items = []
|
||||
changed = 0
|
||||
with self.lock, self._connect() as conn:
|
||||
rows = conn.execute("SELECT * FROM watchlist ORDER BY title COLLATE NOCASE ASC").fetchall()
|
||||
for row in rows:
|
||||
existing_item = self._row_to_item(row)
|
||||
media_type = normalize_media_type(existing_item.get("media_type"))
|
||||
if media_type == "movie":
|
||||
source_dir = watchlist_source_library_dir(existing_item, active_config)
|
||||
has_files = source_dir.exists() and any(path.is_file() for path in source_dir.rglob("*"))
|
||||
new_sub_values = []
|
||||
new_dub_values = []
|
||||
downloaded = has_files
|
||||
else:
|
||||
filesystem_values = watchlist_downloaded_episode_values_from_filesystem(existing_item, active_config, "dub")
|
||||
if filesystem_values:
|
||||
preferred_mode = watchlist_preferred_completion_mode(existing_item)
|
||||
new_sub_values = list(existing_item.get("downloaded_sub_episodes") or [])
|
||||
new_dub_values = list(existing_item.get("downloaded_dub_episodes") or [])
|
||||
if preferred_mode == "sub":
|
||||
new_sub_values = filesystem_values
|
||||
else:
|
||||
new_dub_values = filesystem_values
|
||||
downloaded = bool(new_sub_values or new_dub_values)
|
||||
else:
|
||||
new_sub_values = []
|
||||
new_dub_values = []
|
||||
downloaded = False
|
||||
old_sub_values = existing_item.get("downloaded_sub_episodes") or []
|
||||
old_dub_values = existing_item.get("downloaded_dub_episodes") or []
|
||||
sub_added = [value for value in new_sub_values if value not in old_sub_values]
|
||||
sub_removed = [value for value in old_sub_values if value not in new_sub_values]
|
||||
dub_added = [value for value in new_dub_values if value not in old_dub_values]
|
||||
dub_removed = [value for value in old_dub_values if value not in new_dub_values]
|
||||
downloaded_changed = bool(existing_item.get("downloaded")) != downloaded
|
||||
row_changed = bool(sub_added or sub_removed or dub_added or dub_removed or downloaded_changed)
|
||||
if row_changed:
|
||||
conn.execute(
|
||||
"""
|
||||
UPDATE watchlist
|
||||
SET downloaded = ?, downloaded_sub_episodes_json = ?, downloaded_dub_episodes_json = ?, updated_at = ?
|
||||
WHERE show_id = ?
|
||||
""",
|
||||
(
|
||||
1 if downloaded else 0,
|
||||
encode_episode_values(new_sub_values) if new_sub_values else None,
|
||||
encode_episode_values(new_dub_values) if new_dub_values else None,
|
||||
now_iso(),
|
||||
existing_item["show_id"],
|
||||
),
|
||||
)
|
||||
changed += 1
|
||||
items.append(
|
||||
{
|
||||
"show_id": existing_item.get("show_id"),
|
||||
"title": existing_item.get("title"),
|
||||
"media_type": media_type,
|
||||
"changed": row_changed,
|
||||
"downloaded": downloaded,
|
||||
"sub_added": sub_added,
|
||||
"sub_removed": sub_removed,
|
||||
"dub_added": dub_added,
|
||||
"dub_removed": dub_removed,
|
||||
}
|
||||
)
|
||||
return {
|
||||
"message": f"Reconciled downloaded watchlist files for {len(items)} entr{'y' if len(items) == 1 else 'ies'}."
|
||||
+ (f" Updated {changed}." if changed else " No changes were needed."),
|
||||
"total": len(items),
|
||||
"changed": changed,
|
||||
"items": items,
|
||||
}
|
||||
|
||||
def mark_downloaded(self, show_id, title="", mode="", episodes="", media_type=""):
|
||||
normalized_show_id = str(show_id or "").strip()
|
||||
if not normalized_show_id:
|
||||
@@ -2653,6 +2792,12 @@ def get_jellyfin_sync_status():
|
||||
return WATCHLIST_JELLYFIN_SYNC.status()
|
||||
|
||||
|
||||
def reconcile_watchlist_downloaded_files(config=None):
|
||||
runtime = ensure_runtime()
|
||||
active_config = normalize_config(config or runtime["config"] or {})
|
||||
return runtime["watchlist"].reconcile_downloaded_filesystem(active_config)
|
||||
|
||||
|
||||
def update_watchlist_category(show_id, category):
|
||||
ensure_runtime()
|
||||
item = WATCHLIST.update_category(show_id, category)
|
||||
@@ -2951,6 +3096,7 @@ Handler = build_handler_class(
|
||||
http_error=HttpError,
|
||||
index_html=INDEX_HTML,
|
||||
queue_html=QUEUE_HTML,
|
||||
reconcile_watchlist_downloaded_files=reconcile_watchlist_downloaded_files,
|
||||
remove_from_watchlist=remove_from_watchlist,
|
||||
run_jellyfin_sync=run_jellyfin_sync,
|
||||
runtime_state=runtime_state,
|
||||
|
||||
Reference in New Issue
Block a user