Treat manual storage as Importarr-owned
Use SAB storage under the configured download root as ownership even when SAB reports category '*'.
This commit is contained in:
+3
-3
@@ -317,9 +317,9 @@ async def sync_queue() -> None:
|
|||||||
continue
|
continue
|
||||||
if readiness.ready and readiness.storage:
|
if readiness.ready and readiness.storage:
|
||||||
for video in scan_videos(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:
|
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]]:
|
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"],
|
"updated_at": item["updated_at"],
|
||||||
"completed_at": item["completed_at"],
|
"completed_at": item["completed_at"],
|
||||||
"sab_status": state_name if source_type == "sab" else None,
|
"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_run_now": state_name in {"ready", "manual_batch", "failed"},
|
||||||
"can_retry": state_name in {"failed", "skipped"},
|
"can_retry": state_name in {"failed", "skipped"},
|
||||||
"can_ignore": state_name not in {"imported", "skipped"},
|
"can_ignore": state_name not in {"imported", "skipped"},
|
||||||
|
|||||||
+14
-8
@@ -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:
|
if not force_status and nzo_id and nzo_id in active_nzo_ids:
|
||||||
return Readiness("processing", "SAB job is still present in queue")
|
return Readiness("processing", "SAB job is still present in queue")
|
||||||
item_category = str(item.get("category") or item.get("cat") or "")
|
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 "")
|
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":
|
if not force_status and status == "Failed":
|
||||||
return Readiness("failed", "SAB history reports failure")
|
return Readiness("failed", "SAB history reports failure")
|
||||||
if not force_status and (status in NOT_READY_STATUSES or status != "Completed"):
|
if not force_status and (status in NOT_READY_STATUSES or status != "Completed"):
|
||||||
return Readiness("processing", f"SAB status is {status or 'unknown'}")
|
return Readiness("processing", f"SAB status is {status or 'unknown'}")
|
||||||
storage_value = str(item.get("storage") or "")
|
if storage is None:
|
||||||
if not storage_value:
|
|
||||||
return Readiness("unknown", "SAB completed item has no final storage")
|
return Readiness("unknown", "SAB completed item has no final storage")
|
||||||
storage = Path(storage_value).resolve()
|
if not storage_in_root:
|
||||||
root = download_root.resolve()
|
|
||||||
if storage != root and root not in storage.parents:
|
|
||||||
return Readiness("ignored", "SAB storage is outside configured download root", storage)
|
return Readiness("ignored", "SAB storage is outside configured download root", storage)
|
||||||
if has_transient_part(storage):
|
if has_transient_part(storage):
|
||||||
return Readiness("processing", "SAB storage path contains transient unpack/admin marker", 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)
|
return Readiness("ready", reason, storage)
|
||||||
|
|||||||
+9
-3
@@ -46,6 +46,7 @@ class State:
|
|||||||
size integer not null default 0,
|
size integer not null default 0,
|
||||||
batch_id integer,
|
batch_id integer,
|
||||||
job_id text,
|
job_id text,
|
||||||
|
sab_category text,
|
||||||
first_seen_at text not null default current_timestamp,
|
first_seen_at text not null default current_timestamp,
|
||||||
updated_at text not null default current_timestamp,
|
updated_at text not null default current_timestamp,
|
||||||
completed_at text,
|
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()
|
self.conn.commit()
|
||||||
|
|
||||||
def get_app_state(self, key: str, default: str | None = None) -> str | None:
|
def get_app_state(self, key: str, default: str | None = None) -> str | None:
|
||||||
@@ -111,11 +115,12 @@ class State:
|
|||||||
size: int = 0,
|
size: int = 0,
|
||||||
batch_id: int | None = None,
|
batch_id: int | None = None,
|
||||||
job_id: str | None = None,
|
job_id: str | None = None,
|
||||||
|
sab_category: str | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
self.conn.execute(
|
self.conn.execute(
|
||||||
"""
|
"""
|
||||||
insert into import_queue_items(source_type, source_id, source_path, name, state, reason, relative_path, size, batch_id, job_id)
|
insert into import_queue_items(source_type, source_id, source_path, name, state, reason, relative_path, size, batch_id, job_id, sab_category)
|
||||||
values (?,?,?,?,?,?,?,?,?,?)
|
values (?,?,?,?,?,?,?,?,?,?,?)
|
||||||
on conflict(source_type, source_id) do update set
|
on conflict(source_type, source_id) do update set
|
||||||
source_path=excluded.source_path,
|
source_path=excluded.source_path,
|
||||||
name=excluded.name,
|
name=excluded.name,
|
||||||
@@ -125,10 +130,11 @@ class State:
|
|||||||
size=excluded.size,
|
size=excluded.size,
|
||||||
batch_id=excluded.batch_id,
|
batch_id=excluded.batch_id,
|
||||||
job_id=excluded.job_id,
|
job_id=excluded.job_id,
|
||||||
|
sab_category=excluded.sab_category,
|
||||||
updated_at=current_timestamp,
|
updated_at=current_timestamp,
|
||||||
completed_at=case when excluded.state in ('imported','failed','skipped') then current_timestamp else null end
|
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()
|
self.conn.commit()
|
||||||
row = self.conn.execute("select * from import_queue_items where source_type = ? and source_id = ?", (source_type, source_id)).fetchone()
|
row = self.conn.execute("select * from import_queue_items where source_type = ? and source_id = ?", (source_type, source_id)).fetchone()
|
||||||
|
|||||||
@@ -19,6 +19,12 @@ def test_completed_manual_is_ready():
|
|||||||
|
|
||||||
def test_wrong_category_ignored():
|
def test_wrong_category_ignored():
|
||||||
result = classify_history_item(item(category="*"), set(), "manual", ROOT)
|
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"
|
assert result.state == "ignored"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user