Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 15 additions & 8 deletions config.sample.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -219,15 +219,22 @@ arrs:
lidarr_instances: [] # Lidarr instances (configured via UI)
readarr_instances: [] # Readarr instances (configured via UI)
whisparr_instances: [] # Whisparr instances (configured via UI)
# Queue Cleanup Configuration
queue_cleanup_enabled: true # Enable automatic queue cleanup for failed imports (default: true)
queue_cleanup_interval_seconds: 10 # Interval in seconds to check for failed imports (default: 10)
cleanup_automatic_import_failure: false # Enable cleanup of "Automatic import is not possible" errors (default: false)
queue_cleanup_allowlist: [] # List of additional error messages to treat as safe for cleanup
# Queue Cleanup Configuration (covers ghost/empty-folder removal + message rules for
# all *arr types: radarr/sonarr/whisparr/lidarr/readarr/sportarr)
queue_cleanup_enabled: true # Enable automatic queue cleanup for failed/stuck imports (default: true)
queue_cleanup_interval_seconds: 10 # Interval in seconds to check the *arr queues (default: 10)
queue_cleanup_grace_period_minutes: 5 # Minutes an item must stay stuck before cleanup acts (default: 5)
# Message rules: when a stuck import's *arr status message matches a rule, run its
# action (action: remove | blocklist | blocklist_search). Manage these in the web UI.
queue_cleanup_rules: []
# Example:
# queue_cleanup_allowlist:
# - "Not a Custom Format upgrade"
# - "Ignored Message 2"
# queue_cleanup_rules:
# - message: "Sample"
# enabled: true
# action: blocklist_search
# - message: "automatic import is not possible"
# enabled: false
# action: remove
# Example instance configuration (use the web UI instead):
# radarr_instances:
# - name: "radarr-main"
Expand Down
37 changes: 23 additions & 14 deletions docs/docs/3. Configuration/integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,31 +149,40 @@ With this layout, both containers see the same paths, so `import_dir: /mnt/symli

## Step 6: Configure Queue Cleanup

AltMount can automatically monitor ARR queues and remove failed imports to keep things tidy:
AltMount can automatically monitor ARR queues and clean up stuck or failed imports to keep
things tidy. One pass covers all *arr types (Radarr, Sonarr, Whisparr, Lidarr, Readarr,
Sportarr): it removes ghost/empty-folder entries (already imported, or the source path is
gone) and applies your message rules to stuck imports. Only queue items owned by AltMount's
download client are ever touched.

```yaml
arrs:
enabled: true
webhook_base_url: "" # Base URL for webhook callbacks (defaults to http://<host>:<port>)
queue_cleanup_enabled: true
queue_cleanup_interval_seconds: 300
queue_cleanup_grace_period_minutes: 10 # Wait before cleaning up (default: 10)
cleanup_automatic_import_failure: true
queue_cleanup_allowlist:
- message: "Not a Custom Format upgrade"
queue_cleanup_grace_period_minutes: 5 # Wait before cleaning up (default: 5)
# Message rules: when a stuck import's *arr status message matches a rule, run its
# action (remove | blocklist | blocklist_search). Manage these in the web UI.
queue_cleanup_rules:
- message: "Sample"
enabled: true
- message: "No files found are eligible"
action: blocklist_search
- message: "Not a Custom Format upgrade"
enabled: true
action: remove
- message: "automatic import is not possible"
enabled: false
action: remove
```

| Field | Description |
| ------------------------------------ | ------------------------------------------------------------------------------------------------------- |
| `queue_cleanup_enabled` | Enable or disable automatic queue cleanup |
| `queue_cleanup_interval_seconds` | How often to check ARR queues (in seconds) |
| `queue_cleanup_grace_period_minutes` | Minimum age (in minutes) before a failed item is cleaned up (default: 10) |
| `cleanup_automatic_import_failure` | Clean up items with "Automatic import is not possible" errors |
| `webhook_base_url` | Base URL ARRs use to reach AltMount for webhooks (default: `http://<host>:<port>`) |
| `queue_cleanup_allowlist` | Error messages to treat as safe for cleanup. Each entry has a `message` string and an `enabled` boolean |
| Field | Description |
| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `queue_cleanup_enabled` | Enable or disable automatic queue cleanup |
| `queue_cleanup_interval_seconds` | How often to check ARR queues (in seconds) |
| `queue_cleanup_grace_period_minutes` | Minimum age (in minutes) a stuck/failed item must persist before it is cleaned up (default: 5). Ghost/empty-folder removal is immediate. |
| `webhook_base_url` | Base URL ARRs use to reach AltMount for webhooks (default: `http://<host>:<port>`) |
| `queue_cleanup_rules` | Message rules for stuck imports. Each rule has a `message` substring (case-insensitive), an `enabled` boolean, and an `action`: `remove`, `blocklist`, or `blocklist_search`. |

## Verifying the Setup

Expand Down
6 changes: 0 additions & 6 deletions frontend/bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 1 addition & 3 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,9 @@
"lucide-react": "^0.540.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-hook-form": "^7.62.0",
"react-router-dom": "^7.8.1",
"recharts": "^3.1.2",
"webdav": "^5.8.0",
"zod": "^4.0.17"
"webdav": "^5.8.0"
},
"devDependencies": {
"@biomejs/biome": "^2.2.2",
Expand Down
62 changes: 0 additions & 62 deletions frontend/src/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import type {
QueueHistoricalStatsResponse,
QueueItem,
QueueStats,
SABnzbdAddResponse,
ScanStatusResponse,
SystemBrowseResponse,
UploadNZBLnkResponse,
Expand Down Expand Up @@ -248,10 +247,6 @@ export class APIClient {
return this.requestWithMeta<QueueItem[]>(`/queue${query ? `?${query}` : ""}`);
}

async getQueueItem(id: number) {
return this.request<QueueItem>(`/queue/${id}`);
}

async deleteQueueItem(id: number) {
return this.request<QueueItem>(`/queue/${id}`, { method: "DELETE" });
}
Expand Down Expand Up @@ -385,10 +380,6 @@ export class APIClient {
return this.requestWithMeta<FileHealth[]>(`/health${query ? `?${query}` : ""}`);
}

async getHealthItem(id: string) {
return this.request<FileHealth>(`/health/${encodeURIComponent(id)}`);
}

async deleteHealthItem(id: number, options?: { deleteMeta?: boolean; deleteSymlink?: boolean }) {
const searchParams = new URLSearchParams();
if (options?.deleteMeta) searchParams.set("delete_meta", "true");
Expand Down Expand Up @@ -444,29 +435,13 @@ export class APIClient {
});
}

async retryHealthItem(id: string, resetStatus?: boolean) {
return this.request<FileHealth>(`/health/${encodeURIComponent(id)}/retry`, {
method: "POST",
body: JSON.stringify({ reset_status: resetStatus }),
});
}

async repairHealthItem(id: number, resetRepairRetryCount?: boolean) {
return this.request<FileHealth>(`/health/${id}/repair`, {
method: "POST",
body: JSON.stringify({ reset_repair_retry_count: resetRepairRetryCount }),
});
}

async getCorruptedFiles(params?: { limit?: number; offset?: number }) {
const searchParams = new URLSearchParams();
if (params?.limit) searchParams.set("limit", params.limit.toString());
if (params?.offset) searchParams.set("offset", params.offset.toString());

const query = searchParams.toString();
return this.request<FileHealth[]>(`/health/corrupted${query ? `?${query}` : ""}`);
}

async getHealthStats() {
return this.request<HealthStats>("/health/stats");
}
Expand Down Expand Up @@ -686,10 +661,6 @@ export class APIClient {
});
}

async getArrsHealth() {
return this.request<Record<string, unknown>>("/arrs/health");
}

async registerArrsWebhooks() {
return this.request<{ message: string }>("/arrs/webhook/register", {
method: "POST",
Expand Down Expand Up @@ -742,13 +713,6 @@ export class APIClient {
return this.request<ConfigResponse>("/config");
}

async updateConfig(config: ConfigUpdateRequest) {
return this.request<ConfigResponse>("/config", {
method: "PUT",
body: JSON.stringify(config),
});
}

async updateConfigSection(section: ConfigSection, config: ConfigUpdateRequest) {
return this.request<ConfigResponse>(`/config/${section}`, {
method: "PATCH",
Expand Down Expand Up @@ -941,32 +905,6 @@ export class APIClient {
});
}

// SABnzbd file upload endpoint
async uploadNzbFile(file: File, apiKey: string): Promise<SABnzbdAddResponse> {
const formData = new FormData();
formData.append("nzbfile", file);

const url = `/sabnzbd?mode=addfile&apikey=${encodeURIComponent(apiKey)}`;

const response = await fetch(url, {
method: "POST",
body: formData,
credentials: "include", // Include cookies for Safari compatibility
});

if (!response.ok) {
throw new APIError(response.status, `Upload failed: ${response.statusText}`, "");
}

const data = await response.json();
if (!data.status) {
const err = data as APIError;
throw new APIError(response.status, err.message || "Upload failed", err.details || "");
}

return data;
}

// Native upload endpoint using JWT authentication
async uploadToQueue(
file: File,
Expand Down
87 changes: 0 additions & 87 deletions frontend/src/components/charts/HealthChart.tsx

This file was deleted.

49 changes: 0 additions & 49 deletions frontend/src/components/charts/QueueChart.tsx

This file was deleted.

Loading
Loading