diff --git a/docs/todo/scratchpad.md b/docs/todo/scratchpad.md index 6ac4e88..0307052 100644 --- a/docs/todo/scratchpad.md +++ b/docs/todo/scratchpad.md @@ -14,10 +14,7 @@ - maybe move langauges to general tab or put info directly below it to fill space - adding a database requires double click??? im not running into this personally -- default delay profiles upon adding an arr. different for radarr/sonarr - - wait for seraphys to give default profiles - - maybe need to get feedback and update over time - - on by default, turn off in settings > general +- pageinate the profilarr log pages # Adaptive Backoff diff --git a/src/lib/client/ui/button/Button.svelte b/src/lib/client/ui/button/Button.svelte index 828571a..7b5ba9f 100644 --- a/src/lib/client/ui/button/Button.svelte +++ b/src/lib/client/ui/button/Button.svelte @@ -14,6 +14,8 @@ // Responsive: auto-switch to xs on smaller screens (< 1280px) export let responsive: boolean = false; export let fullWidth: boolean = false; + // Optional href - renders as anchor instead of button + export let href: string | undefined = undefined; let isSmallScreen = false; let mediaQuery: MediaQueryList | null = null; @@ -64,15 +66,30 @@ $: classes = `${baseClasses} ${sizeClasses[effectiveSize]} ${variantClasses[variant]} ${widthClass}`; - +{#if href} + + {#if icon && iconPosition === 'left'} + + {/if} + {#if text} + {text} + {/if} + + {#if icon && iconPosition === 'right'} + + {/if} + +{:else} + +{/if} diff --git a/src/lib/shared/filters.ts b/src/lib/shared/filters.ts index fef3011..54b84b3 100644 --- a/src/lib/shared/filters.ts +++ b/src/lib/shared/filters.ts @@ -8,6 +8,7 @@ import { uuid } from './uuid.ts'; export interface FilterOperator { id: string; label: string; + description?: string; } export type FilterValueType = string | number | boolean | null; @@ -20,6 +21,7 @@ export interface FilterValue { export interface FilterField { id: string; label: string; + description: string; operators: FilterOperator[]; valueType: 'boolean' | 'select' | 'text' | 'number' | 'date'; values?: FilterValue[]; @@ -82,42 +84,42 @@ export const filterModes: { id: FilterMode; label: string; description: string } * Common operator sets */ const booleanOperators: FilterOperator[] = [ - { id: 'is', label: 'is' }, - { id: 'is_not', label: 'is not' } + { id: 'is', label: 'is', description: 'Exact match' }, + { id: 'is_not', label: 'is not', description: 'Does not match' } ]; const numberOperators: FilterOperator[] = [ - { id: 'eq', label: 'equals' }, - { id: 'neq', label: 'does not equal' }, - { id: 'gt', label: 'is greater than' }, - { id: 'gte', label: 'is greater than or equal' }, - { id: 'lt', label: 'is less than' }, - { id: 'lte', label: 'is less than or equal' } + { id: 'eq', label: 'equals', description: 'Exactly equals the value' }, + { id: 'neq', label: 'does not equal', description: 'Does not equal the value' }, + { id: 'gt', label: 'is greater than', description: 'Greater than the value' }, + { id: 'gte', label: 'is greater than or equal', description: 'Greater than or equal to the value' }, + { id: 'lt', label: 'is less than', description: 'Less than the value' }, + { id: 'lte', label: 'is less than or equal', description: 'Less than or equal to the value' } ]; const textOperators: FilterOperator[] = [ - { id: 'contains', label: 'contains' }, - { id: 'not_contains', label: 'does not contain' }, - { id: 'starts_with', label: 'starts with' }, - { id: 'ends_with', label: 'ends with' }, - { id: 'eq', label: 'equals' }, - { id: 'neq', label: 'does not equal' } + { id: 'contains', label: 'contains', description: 'Contains the text (case-insensitive)' }, + { id: 'not_contains', label: 'does not contain', description: 'Does not contain the text' }, + { id: 'starts_with', label: 'starts with', description: 'Starts with the text' }, + { id: 'ends_with', label: 'ends with', description: 'Ends with the text' }, + { id: 'eq', label: 'equals', description: 'Exactly equals the text (case-insensitive)' }, + { id: 'neq', label: 'does not equal', description: 'Does not equal the text' } ]; const dateOperators: FilterOperator[] = [ - { id: 'before', label: 'is before' }, - { id: 'after', label: 'is after' }, - { id: 'in_last', label: 'in the last' }, - { id: 'not_in_last', label: 'not in the last' } + { id: 'before', label: 'is before', description: 'The date is before the specified date' }, + { id: 'after', label: 'is after', description: 'The date is after the specified date' }, + { id: 'in_last', label: 'in the last', description: 'Within the last N days' }, + { id: 'not_in_last', label: 'not in the last', description: 'Not within the last N days' } ]; const ordinalOperators: FilterOperator[] = [ - { id: 'eq', label: 'is exactly' }, - { id: 'neq', label: 'is not' }, - { id: 'gte', label: 'has reached' }, - { id: 'lte', label: "hasn't passed" }, - { id: 'gt', label: 'is past' }, - { id: 'lt', label: 'is before' } + { id: 'eq', label: 'is exactly', description: 'Exactly matches the status' }, + { id: 'neq', label: 'is not', description: 'Does not match the status' }, + { id: 'gte', label: 'has reached', description: 'Has reached this status or further in the progression' }, + { id: 'lte', label: "hasn't passed", description: 'Has not passed this status in the progression' }, + { id: 'gt', label: 'is past', description: 'Is past this status (further along)' }, + { id: 'lt', label: 'is before', description: 'Is before this status (not yet reached)' } ]; /** @@ -139,6 +141,7 @@ export const filterFields: FilterField[] = [ { id: 'monitored', label: 'Monitored', + description: 'Whether the item is being monitored for upgrades', operators: booleanOperators, valueType: 'boolean', values: [ @@ -149,6 +152,7 @@ export const filterFields: FilterField[] = [ { id: 'cutoff_met', label: 'Cutoff Met', + description: "Whether the item's quality score meets the filter's cutoff percentage", operators: booleanOperators, valueType: 'boolean', values: [ @@ -161,6 +165,7 @@ export const filterFields: FilterField[] = [ { id: 'minimum_availability', label: 'Minimum Availability', + description: 'The minimum availability status set in Radarr. Progresses: TBA → Announced → In Cinemas → Released', operators: ordinalOperators, valueType: 'select', values: [ @@ -175,48 +180,56 @@ export const filterFields: FilterField[] = [ { id: 'title', label: 'Title', + description: 'The title of the movie', operators: textOperators, valueType: 'text' }, { id: 'quality_profile', label: 'Quality Profile', + description: 'The assigned quality profile name', operators: textOperators, valueType: 'text' }, { id: 'collection', label: 'Collection', + description: 'The collection the movie belongs to (e.g., "Marvel Cinematic Universe")', operators: textOperators, valueType: 'text' }, { id: 'studio', label: 'Studio', + description: 'The production studio', operators: textOperators, valueType: 'text' }, { id: 'original_language', label: 'Original Language', + description: 'The original language of the movie (e.g., "en", "ja")', operators: textOperators, valueType: 'text' }, { id: 'genres', label: 'Genres', + description: 'Movie genres (Action, Comedy, Drama, etc.)', operators: textOperators, valueType: 'text' }, { id: 'keywords', label: 'Keywords', + description: 'TMDb keywords associated with the movie', operators: textOperators, valueType: 'text' }, { id: 'release_group', label: 'Release Group', + description: 'The release group of the current file', operators: textOperators, valueType: 'text' }, @@ -225,48 +238,56 @@ export const filterFields: FilterField[] = [ { id: 'year', label: 'Year', + description: 'The release year', operators: numberOperators, valueType: 'number' }, { id: 'popularity', label: 'Popularity', + description: 'TMDb popularity score', operators: numberOperators, valueType: 'number' }, { id: 'runtime', - label: 'Runtime (minutes)', + label: 'Runtime', + description: 'Movie runtime in minutes', operators: numberOperators, valueType: 'number' }, { id: 'size_on_disk', - label: 'Size on Disk (GB)', + label: 'Size on Disk', + description: 'Current file size in GB', operators: numberOperators, valueType: 'number' }, { id: 'tmdb_rating', label: 'TMDb Rating', + description: 'TMDb rating (0-10)', operators: numberOperators, valueType: 'number' }, { id: 'imdb_rating', label: 'IMDb Rating', + description: 'IMDb rating (0-10)', operators: numberOperators, valueType: 'number' }, { id: 'tomato_rating', - label: 'Rotten Tomatoes Rating', + label: 'Rotten Tomatoes', + description: 'Rotten Tomatoes score (0-100)', operators: numberOperators, valueType: 'number' }, { id: 'trakt_rating', label: 'Trakt Rating', + description: 'Trakt rating (0-100)', operators: numberOperators, valueType: 'number' }, @@ -275,18 +296,21 @@ export const filterFields: FilterField[] = [ { id: 'date_added', label: 'Date Added', + description: 'When the movie was added to your library', operators: dateOperators, valueType: 'date' }, { id: 'digital_release', label: 'Digital Release', + description: 'The digital release date from TMDb', operators: dateOperators, valueType: 'date' }, { id: 'physical_release', label: 'Physical Release', + description: 'The physical release date from TMDb', operators: dateOperators, valueType: 'date' } diff --git a/src/routes/arr/[id]/upgrades/+page.svelte b/src/routes/arr/[id]/upgrades/+page.svelte index 5ceb363..601d128 100644 --- a/src/routes/arr/[id]/upgrades/+page.svelte +++ b/src/routes/arr/[id]/upgrades/+page.svelte @@ -9,7 +9,6 @@ import CoreSettings from './components/CoreSettings.svelte'; import FilterSettings from './components/FilterSettings.svelte'; import RunHistory from './components/RunHistory.svelte'; - import UpgradesInfoModal from './components/UpgradesInfoModal.svelte'; import DirtyModal from '$ui/modal/DirtyModal.svelte'; import StickyCard from '$ui/card/StickyCard.svelte'; import Button from '$ui/button/Button.svelte'; @@ -37,7 +36,6 @@ // Dev mode check - use VITE_CHANNEL which is explicitly set in dev mode const isDev = import.meta.env.VITE_CHANNEL === 'dev'; - let showInfoModal = false; let saving = false; let running = false; let clearing = false; @@ -86,7 +84,7 @@

-
- - - import InfoModal from '$ui/modal/InfoModal.svelte'; - import { filterFields } from '$lib/shared/filters'; - - export let open: boolean = false; - - // Group fields by type for better organization - const booleanFields = filterFields.filter((f) => f.valueType === 'boolean'); - const selectFields = filterFields.filter((f) => f.valueType === 'select'); - const textFields = filterFields.filter((f) => f.valueType === 'text'); - const numberFields = filterFields.filter((f) => f.valueType === 'number'); - const dateFields = filterFields.filter((f) => f.valueType === 'date'); - - - -
-

- Use these fields to build filter rules. Combine multiple rules with AND/OR logic to create - complex filters. -

- - {#if booleanFields.length > 0} -
-
Yes/No Fields
-
- {#each booleanFields as field} -
- {field.label}: - - {#if field.id === 'monitored'} - Whether the item is being monitored for upgrades - {:else if field.id === 'cutoff_met'} - Whether the item's score meets the filter's cutoff percentage - {/if} - -
- {/each} -
-
- {/if} - - {#if selectFields.length > 0} -
-
Selection Fields
-
- {#each selectFields as field} -
- {field.label}: - - {#if field.id === 'minimum_availability'} - The minimum availability status ({field.values?.map((v) => v.label).join(', ')}) - {/if} - -
- {/each} -
-
- {/if} - - {#if textFields.length > 0} -
-
Text Fields
-

All text comparisons are case-insensitive.

-
- {#each textFields as field} -
- {field.label}: - - {#if field.id === 'title'} - The title of the movie - {:else if field.id === 'quality_profile'} - The assigned quality profile name - {:else if field.id === 'collection'} - The collection the movie belongs to (e.g., "Marvel Cinematic Universe") - {:else if field.id === 'studio'} - The production studio - {:else if field.id === 'original_language'} - The original language of the movie - {:else if field.id === 'genres'} - Movie genres (Action, Comedy, Drama, etc.) - {:else if field.id === 'keywords'} - TMDb keywords associated with the movie - {:else if field.id === 'release_group'} - The release group of the current file - {/if} - -
- {/each} -
-
- {/if} - - {#if numberFields.length > 0} -
-
Number Fields
-
- {#each numberFields as field} -
- {field.label}: - - {#if field.id === 'year'} - The release year - {:else if field.id === 'popularity'} - TMDb popularity score - {:else if field.id === 'runtime'} - Movie runtime in minutes - {:else if field.id === 'size_on_disk'} - Current file size in GB - {:else if field.id === 'tmdb_rating'} - TMDb rating (0-10) - {:else if field.id === 'imdb_rating'} - IMDb rating (0-10) - {:else if field.id === 'tomato_rating'} - Rotten Tomatoes score (0-100) - {:else if field.id === 'trakt_rating'} - Trakt rating (0-100) - {/if} - -
- {/each} -
-
- {/if} - - {#if dateFields.length > 0} -
-
Date Fields
-
- {#each dateFields as field} -
- {field.label}: - - {#if field.id === 'date_added'} - When the movie was added to your library - {/if} - -
- {/each} -
-
- {/if} - -
-
Filter Settings
-
-
- Cutoff %: - Score threshold (0-100%) for the "Cutoff Met" filter -
-
- Cooldown: - Skip items that were searched within this many hours -
-
- Method: - How to select items from the filtered results -
-
- Count: - Number of items to search per run -
-
-
-
-
diff --git a/src/routes/arr/[id]/upgrades/components/UpgradesInfoModal.svelte b/src/routes/arr/[id]/upgrades/components/UpgradesInfoModal.svelte deleted file mode 100644 index b4f9208..0000000 --- a/src/routes/arr/[id]/upgrades/components/UpgradesInfoModal.svelte +++ /dev/null @@ -1,66 +0,0 @@ - - - -
-
-
Why is this needed?
-
-

- *arr apps only upgrade via RSS feeds, which are incremental and only show what's new. RSS - doesn't search for the best available release — it just grabs whatever comes through. -

-

- This module lets you define exactly what to upgrade, when, and how — all within Profilarr. -

-
-
- -
-
Core Settings
-
- Configure the basic behavior: set how often it runs and choose how to cycle through your - filters (round robin or random shuffle). -
-
- -
-
Filters
-
- Define which items to consider for upgrades. Create multiple named filters with different - criteria (e.g., "Marvel Movies", "2024 Releases", "Low Quality"). Each filter can have - complex rules with AND/OR logic and nested groups. -
-
- -
-
Filter Mode
-
-

Round Robin: Cycle through filters in order, one per run.

-

- Random Shuffle: Randomly shuffle filters, use each once before repeating. -

-
-
- -
-
Selection
-
- Each filter has its own selection settings. Choose how to pick items for upgrade (e.g., - random) and how many items to process per run. This prevents overwhelming your indexers with - too many searches at once. -
-
- -
-
How it runs
-
- On each scheduled run: pick a filter (based on mode) → apply filter rules to your library → - select items (based on selection method) → trigger searches in your *arr app. -
-
-
-
diff --git a/src/routes/arr/[id]/upgrades/info/+page.server.ts b/src/routes/arr/[id]/upgrades/info/+page.server.ts new file mode 100644 index 0000000..8c8ae41 --- /dev/null +++ b/src/routes/arr/[id]/upgrades/info/+page.server.ts @@ -0,0 +1,21 @@ +import { error } from '@sveltejs/kit'; +import type { ServerLoad } from '@sveltejs/kit'; +import { arrInstancesQueries } from '$db/queries/arrInstances.ts'; + +export const load: ServerLoad = ({ params }) => { + const id = parseInt(params.id || '', 10); + + if (isNaN(id)) { + error(404, `Invalid instance ID: ${params.id}`); + } + + const instance = arrInstancesQueries.getById(id); + + if (!instance) { + error(404, `Instance not found: ${id}`); + } + + return { + instance + }; +}; diff --git a/src/routes/arr/[id]/upgrades/info/+page.svelte b/src/routes/arr/[id]/upgrades/info/+page.svelte new file mode 100644 index 0000000..01c65fa --- /dev/null +++ b/src/routes/arr/[id]/upgrades/info/+page.svelte @@ -0,0 +1,179 @@ + + + + {data.instance.name} - Upgrades Info - Profilarr + + + +
+

How Upgrades Work

+
+
+
+
+ +
+ +
+

+ Radarr and Sonarr don't search for the best release. They monitor RSS feeds and grab the + first thing that qualifies as an upgrade. To get optimal releases, you need manual searches. + This module automates that: Filter + your library, + Select items to search, + then + Search for better releases. +

+
+ + +
+

Concepts

+ row.id} + emptyMessage="No concepts" + chevronPosition="right" + flushExpanded + > + +
+

{row.details}

+
+
+
+
+ + +
+

Selectors

+ + + + +
+

Filter Modes

+
+ + + +
+

Filter Fields

+
+ +