diff --git a/importarr/main.py b/importarr/main.py index 3124ae2..fef4903 100644 --- a/importarr/main.py +++ b/importarr/main.py @@ -317,9 +317,9 @@ async def sync_queue() -> None: continue if readiness.ready and readiness.storage: for video in scan_videos(readiness.storage): - state.upsert_queue_item(source_type="sab", source_id=str(video.path), source_path=video.path, name=video.path.name, state="ready", reason=readiness.reason, relative_path=str(video.relative_path), size=video.size, job_id=job_id) + state.upsert_queue_item(source_type="sab", source_id=str(video.path), source_path=video.path, name=video.path.name, state="ready", reason=readiness.reason, relative_path=str(video.relative_path), size=video.size, job_id=job_id, sab_category=str(item.get("category") or item.get("cat") or "")) else: - state.upsert_queue_item(source_type="sab", source_id=job_id, source_path=readiness.storage, name=str(item.get("name") or job_id), state=readiness.state, reason=readiness.reason, job_id=job_id) + state.upsert_queue_item(source_type="sab", source_id=job_id, source_path=readiness.storage, name=str(item.get("name") or job_id), state=readiness.state, reason=readiness.reason, job_id=job_id, sab_category=str(item.get("category") or item.get("cat") or "")) def queue_jobs() -> list[dict[str, object]]: @@ -346,7 +346,7 @@ def serialize_queue_item(item: dict[str, object]) -> dict[str, object]: "updated_at": item["updated_at"], "completed_at": item["completed_at"], "sab_status": state_name if source_type == "sab" else None, - "sab_category": settings.sab_category if source_type == "sab" else None, + "sab_category": item.get("sab_category") if source_type == "sab" else None, "can_run_now": state_name in {"ready", "manual_batch", "failed"}, "can_retry": state_name in {"failed", "skipped"}, "can_ignore": state_name not in {"imported", "skipped"}, diff --git a/importarr/readiness.py b/importarr/readiness.py index fe0031e..ccd865a 100644 --- a/importarr/readiness.py +++ b/importarr/readiness.py @@ -31,21 +31,27 @@ def classify_history_item(item: dict[str, Any], active_nzo_ids: set[str], catego if not force_status and nzo_id and nzo_id in active_nzo_ids: return Readiness("processing", "SAB job is still present in queue") item_category = str(item.get("category") or item.get("cat") or "") - if item_category != category: - return Readiness("ignored", "SAB category is not owned by Importarr") status = str(item.get("status") or "") + storage_value = str(item.get("storage") or "") + storage = Path(storage_value).resolve() if storage_value else None + root = download_root.resolve() + storage_in_root = bool(storage and (storage == root or root in storage.parents)) + if item_category != category and not storage_in_root: + return Readiness("ignored", "SAB category/storage is not owned by Importarr", storage) if not force_status and status == "Failed": return Readiness("failed", "SAB history reports failure") if not force_status and (status in NOT_READY_STATUSES or status != "Completed"): return Readiness("processing", f"SAB status is {status or 'unknown'}") - storage_value = str(item.get("storage") or "") - if not storage_value: + if storage is None: return Readiness("unknown", "SAB completed item has no final storage") - storage = Path(storage_value).resolve() - root = download_root.resolve() - if storage != root and root not in storage.parents: + if not storage_in_root: return Readiness("ignored", "SAB storage is outside configured download root", storage) if has_transient_part(storage): return Readiness("processing", "SAB storage path contains transient unpack/admin marker", storage) - reason = "forced despite SAB status" if force_status and status != "Completed" else "SAB completed in owned category with final storage" + if force_status and status != "Completed": + reason = "forced despite SAB status" + elif item_category != category: + reason = "SAB completed inside Importarr download root" + else: + reason = "SAB completed in owned category with final storage" return Readiness("ready", reason, storage) diff --git a/importarr/state.py b/importarr/state.py index fa152d4..66402ec 100644 --- a/importarr/state.py +++ b/importarr/state.py @@ -46,6 +46,7 @@ class State: size integer not null default 0, batch_id integer, job_id text, + sab_category text, first_seen_at text not null default current_timestamp, updated_at text not null default current_timestamp, completed_at text, @@ -53,6 +54,9 @@ class State: ); """ ) + columns = {row["name"] for row in self.conn.execute("pragma table_info(import_queue_items)")} + if "sab_category" not in columns: + self.conn.execute("alter table import_queue_items add column sab_category text") self.conn.commit() def get_app_state(self, key: str, default: str | None = None) -> str | None: @@ -111,11 +115,12 @@ class State: size: int = 0, batch_id: int | None = None, job_id: str | None = None, + sab_category: str | None = None, ) -> dict[str, Any]: self.conn.execute( """ - insert into import_queue_items(source_type, source_id, source_path, name, state, reason, relative_path, size, batch_id, job_id) - values (?,?,?,?,?,?,?,?,?,?) + insert into import_queue_items(source_type, source_id, source_path, name, state, reason, relative_path, size, batch_id, job_id, sab_category) + values (?,?,?,?,?,?,?,?,?,?,?) on conflict(source_type, source_id) do update set source_path=excluded.source_path, name=excluded.name, @@ -125,10 +130,11 @@ class State: size=excluded.size, batch_id=excluded.batch_id, job_id=excluded.job_id, + sab_category=excluded.sab_category, updated_at=current_timestamp, completed_at=case when excluded.state in ('imported','failed','skipped') then current_timestamp else null end """, - (source_type, source_id, str(source_path) if source_path else None, name, state, reason, relative_path, size, batch_id, job_id), + (source_type, source_id, str(source_path) if source_path else None, name, state, reason, relative_path, size, batch_id, job_id, sab_category), ) self.conn.commit() row = self.conn.execute("select * from import_queue_items where source_type = ? and source_id = ?", (source_type, source_id)).fetchone() diff --git a/tests/test_sab_readiness.py b/tests/test_sab_readiness.py index e12856d..619c3ce 100644 --- a/tests/test_sab_readiness.py +++ b/tests/test_sab_readiness.py @@ -19,6 +19,12 @@ def test_completed_manual_is_ready(): def test_wrong_category_ignored(): result = classify_history_item(item(category="*"), set(), "manual", ROOT) + assert result.ready + assert result.reason == "SAB completed inside Importarr download root" + + +def test_wrong_category_outside_root_ignored(): + result = classify_history_item(item(category="*", storage="/tmp/other/Movie"), set(), "manual", ROOT) assert result.state == "ignored"