-
Notifications
You must be signed in to change notification settings - Fork 0
[WIP] Add --badData argument to find jellyfin media entries with bad data #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -54,17 +54,23 @@ async def main(cfg: argparse.Namespace) -> None: | |
| else: | ||
| log.info("Using cached data from %s", cfg.db) | ||
|
|
||
| # --- Find targets across all requested paths --- | ||
| if not cfg.target_paths: | ||
| # --- Find targets --- | ||
| if not cfg.bad_data and not cfg.target_paths: | ||
| log.error( | ||
| "No target paths specified. " | ||
| "Pass paths as positional arguments or use --target-path." | ||
| ) | ||
| sys.exit(1) | ||
|
|
||
| log.info("Target paths: %s", cfg.target_paths) | ||
| targets = db.get_pending_targets(cfg.target_paths) | ||
| log.info("Found %d pending/failed items across all target paths", len(targets)) | ||
| if cfg.bad_data: | ||
| if cfg.target_paths: | ||
| log.info("--badData/--bad-data set; ignoring provided target paths.") | ||
| targets = db.get_bad_data_targets() | ||
| log.info("Found %d pending/failed items with bad metadata", len(targets)) | ||
| else: | ||
| log.info("Target paths: %s", cfg.target_paths) | ||
| targets = db.get_pending_targets(cfg.target_paths) | ||
| log.info("Found %d pending/failed items across all target paths", len(targets)) | ||
|
Comment on lines
+65
to
+73
|
||
|
|
||
| if not targets: | ||
| log.info("Nothing to delete.") | ||
|
|
@@ -73,14 +79,23 @@ async def main(cfg: argparse.Namespace) -> None: | |
|
|
||
| # --- Preview --- | ||
| print(f"\nItems to delete ({len(targets)}):") | ||
| for tp in cfg.target_paths: | ||
| group = [r for r in targets if r["path"].startswith(tp)] | ||
| if group: | ||
| print(f"\n [{tp}] ({len(group)} items)") | ||
| for row in group[:10]: | ||
| print(f" [{row['type']:12}] {row['name']}") | ||
| if len(group) > 10: | ||
| print(f" ... and {len(group) - 10} more") | ||
| if cfg.bad_data: | ||
| for row in targets[:30]: | ||
| print( | ||
| f" [{row['type']:12}] {row['name']} ({row['bad_reason']})" | ||
| f" - {row['path'] or '<no path>'}" | ||
| ) | ||
| if len(targets) > 30: | ||
| print(f" ... and {len(targets) - 30} more") | ||
| else: | ||
| for tp in cfg.target_paths: | ||
| group = [r for r in targets if r["path"].startswith(tp)] | ||
| if group: | ||
| print(f"\n [{tp}] ({len(group)} items)") | ||
| for row in group[:10]: | ||
| print(f" [{row['type']:12}] {row['name']}") | ||
| if len(group) > 10: | ||
| print(f" ... and {len(group) - 10} more") | ||
|
|
||
| if cfg.dry_run: | ||
| log.info("Dry run — nothing deleted.") | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -16,19 +16,7 @@ def __init__(self, path: str) -> None: | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._conn.row_factory = sqlite3.Row | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._conn.execute("PRAGMA journal_mode=WAL") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._conn.execute("PRAGMA synchronous=NORMAL") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._conn.execute(""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| CREATE TABLE IF NOT EXISTS items ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| id TEXT PRIMARY KEY, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name TEXT NOT NULL, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type TEXT, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| path TEXT, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| scraped_at TEXT NOT NULL, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| delete_status TEXT DEFAULT 'pending', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| -- 'pending' | 'deleted' | 'not_found' | 'failed' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| delete_attempted_at TEXT, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| delete_error TEXT | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _initialize_items_table(self._conn) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._conn.commit() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # -- context-manager support ------------------------------------------ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -64,13 +52,34 @@ def upsert_items(self, items: list[dict], scraped_at: str) -> None: | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| with self._cursor() as cur: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cur.executemany( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| INSERT INTO items (id, name, type, path, scraped_at) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| VALUES (:id, :name, :type, :path, :scraped_at) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| INSERT INTO items ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| path, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| scraped_at, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| index_number, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| parent_index_number, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| media_source_count | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| VALUES ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| :id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| :name, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| :type, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| :path, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| :scraped_at, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| :index_number, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| :parent_index_number, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| :media_source_count | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ON CONFLICT(id) DO UPDATE SET | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name = excluded.name, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type = excluded.type, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| path = excluded.path, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| scraped_at = excluded.scraped_at | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name = excluded.name, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type = excluded.type, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| path = excluded.path, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| scraped_at = excluded.scraped_at, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| index_number = excluded.index_number, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| parent_index_number = excluded.parent_index_number, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| media_source_count = excluded.media_source_count | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -79,6 +88,9 @@ def upsert_items(self, items: list[dict], scraped_at: str) -> None: | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "type": item.get("Type", ""), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "path": item.get("Path", ""), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "scraped_at": scraped_at, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "index_number": item.get("IndexNumber"), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "parent_index_number": item.get("ParentIndexNumber"), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "media_source_count": _media_source_count(item), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for item in items | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -127,6 +139,36 @@ def mark_failed(self, item_ids: list[str], error: str) -> None: | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [(now, error, iid) for iid in item_ids], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def get_bad_data_targets(self) -> list[sqlite3.Row]: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return self._conn.execute( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| SELECT | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| path, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| CASE | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| WHEN type='Episode' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| AND (parent_index_number IS NULL OR index_number IS NULL) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| THEN 'missing season or episode number' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| WHEN type='Season' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| AND index_number IS NULL | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| THEN 'missing season number' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| WHEN type IN ('Episode', 'Movie', 'Video') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| AND COALESCE(media_source_count, 0) = 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| THEN 'no media versions' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| END AS bad_reason | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| FROM items | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| WHERE delete_status IN ('pending', 'failed') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| AND ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (type='Episode' AND (parent_index_number IS NULL OR index_number IS NULL)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| OR (type='Season' AND index_number IS NULL) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| OR (type IN ('Episode', 'Movie', 'Video') AND COALESCE(media_source_count, 0) = 0) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+152
to
+166
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| AND (parent_index_number IS NULL OR index_number IS NULL) | |
| THEN 'missing season or episode number' | |
| WHEN type='Season' | |
| AND index_number IS NULL | |
| THEN 'missing season number' | |
| WHEN type IN ('Episode', 'Movie', 'Video') | |
| AND COALESCE(media_source_count, 0) = 0 | |
| THEN 'no media versions' | |
| END AS bad_reason | |
| FROM items | |
| WHERE delete_status IN ('pending', 'failed') | |
| AND ( | |
| (type='Episode' AND (parent_index_number IS NULL OR index_number IS NULL)) | |
| OR (type='Season' AND index_number IS NULL) | |
| OR (type IN ('Episode', 'Movie', 'Video') AND COALESCE(media_source_count, 0) = 0) | |
| AND ( | |
| (parent_index_number IS NULL AND index_number IS NOT NULL) | |
| OR (parent_index_number IS NOT NULL AND index_number IS NULL) | |
| ) | |
| THEN 'missing season or episode number' | |
| WHEN type IN ('Episode', 'Movie', 'Video') | |
| AND media_source_count = 0 | |
| THEN 'no media versions' | |
| END AS bad_reason | |
| FROM items | |
| WHERE delete_status IN ('pending', 'failed') | |
| AND ( | |
| ( | |
| type='Episode' | |
| AND ( | |
| (parent_index_number IS NULL AND index_number IS NOT NULL) | |
| OR (parent_index_number IS NOT NULL AND index_number IS NULL) | |
| ) | |
| ) | |
| OR (type IN ('Episode', 'Movie', 'Video') AND media_source_count = 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
--badDatahelp text doesn’t mention that it ignores any provided PATH/--target-path filters (the program does this in core). Consider updating the flag description to explicitly state that path filters are ignored in this mode so users don’t think both filters apply.