mirror of
https://github.com/Dictionarry-Hub/profilarr.git
synced 2026-01-22 10:51:02 +01:00
refactor: moved upgrade/filter info into a seperate page on upgrades/info
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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}`;
|
||||
</script>
|
||||
|
||||
<button {type} {disabled} class={classes} on:click on:mouseenter on:mouseleave>
|
||||
{#if icon && iconPosition === 'left'}
|
||||
<svelte:component this={icon} size={effectiveSize === 'xs' ? 12 : effectiveSize === 'sm' ? 14 : 16} class={baseIconColor} />
|
||||
{/if}
|
||||
{#if text}
|
||||
<span class={baseTextColor}>{text}</span>
|
||||
{/if}
|
||||
<slot />
|
||||
{#if icon && iconPosition === 'right'}
|
||||
<svelte:component this={icon} size={effectiveSize === 'xs' ? 12 : effectiveSize === 'sm' ? 14 : 16} class={baseIconColor} />
|
||||
{/if}
|
||||
</button>
|
||||
{#if href}
|
||||
<a {href} class={classes} on:click on:mouseenter on:mouseleave>
|
||||
{#if icon && iconPosition === 'left'}
|
||||
<svelte:component this={icon} size={effectiveSize === 'xs' ? 12 : effectiveSize === 'sm' ? 14 : 16} class={baseIconColor} />
|
||||
{/if}
|
||||
{#if text}
|
||||
<span class={baseTextColor}>{text}</span>
|
||||
{/if}
|
||||
<slot />
|
||||
{#if icon && iconPosition === 'right'}
|
||||
<svelte:component this={icon} size={effectiveSize === 'xs' ? 12 : effectiveSize === 'sm' ? 14 : 16} class={baseIconColor} />
|
||||
{/if}
|
||||
</a>
|
||||
{:else}
|
||||
<button {type} {disabled} class={classes} on:click on:mouseenter on:mouseleave>
|
||||
{#if icon && iconPosition === 'left'}
|
||||
<svelte:component this={icon} size={effectiveSize === 'xs' ? 12 : effectiveSize === 'sm' ? 14 : 16} class={baseIconColor} />
|
||||
{/if}
|
||||
{#if text}
|
||||
<span class={baseTextColor}>{text}</span>
|
||||
{/if}
|
||||
<slot />
|
||||
{#if icon && iconPosition === 'right'}
|
||||
<svelte:component this={icon} size={effectiveSize === 'xs' ? 12 : effectiveSize === 'sm' ? 14 : 16} class={baseIconColor} />
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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 @@
|
||||
</p>
|
||||
</div>
|
||||
<div slot="right" class="flex items-center gap-2">
|
||||
<Button text="How it works" icon={Info} on:click={() => (showInfoModal = true)} />
|
||||
<Button text="Info" icon={Info} href="/arr/{data.instance.id}/upgrades/info" />
|
||||
{#if !isNewConfig && data.config?.dryRun}
|
||||
<Button
|
||||
text={clearing ? 'Clearing...' : 'Reset Cache'}
|
||||
@@ -236,5 +234,4 @@
|
||||
></form>
|
||||
{/if}
|
||||
|
||||
<UpgradesInfoModal bind:open={showInfoModal} />
|
||||
<DirtyModal />
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
Plus,
|
||||
Pencil,
|
||||
Check,
|
||||
Info,
|
||||
Power,
|
||||
Copy,
|
||||
ClipboardCopy,
|
||||
@@ -16,7 +15,6 @@
|
||||
import { createSearchStore } from '$lib/client/stores/search';
|
||||
import FilterGroupComponent from './FilterGroup.svelte';
|
||||
import NumberInput from '$ui/form/NumberInput.svelte';
|
||||
import FiltersInfoModal from './FiltersInfoModal.svelte';
|
||||
import DropdownSelect from '$ui/dropdown/DropdownSelect.svelte';
|
||||
import ActionsBar from '$ui/actions/ActionsBar.svelte';
|
||||
import ActionButton from '$ui/actions/ActionButton.svelte';
|
||||
@@ -37,8 +35,6 @@
|
||||
onFiltersChange?.(filters);
|
||||
}
|
||||
|
||||
let showInfoModal = false;
|
||||
|
||||
// Filter the filters list based on search
|
||||
$: filteredFilters = filterByName(filters, $debouncedQuery);
|
||||
|
||||
@@ -186,7 +182,6 @@
|
||||
<div class="mb-4">
|
||||
<ActionsBar>
|
||||
<SearchAction {searchStore} placeholder="Search filters..." />
|
||||
<ActionButton icon={Info} title="Fields" on:click={() => (showInfoModal = true)} />
|
||||
<ActionButton icon={Plus} title="Add filter" on:click={addFilter} />
|
||||
</ActionsBar>
|
||||
</div>
|
||||
@@ -390,8 +385,6 @@
|
||||
</ExpandableTable>
|
||||
</div>
|
||||
|
||||
<FiltersInfoModal bind:open={showInfoModal} />
|
||||
|
||||
<Modal
|
||||
bind:open={deleteModalOpen}
|
||||
header="Delete Filter"
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
<script lang="ts">
|
||||
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');
|
||||
</script>
|
||||
|
||||
<InfoModal bind:open header="Available Filters">
|
||||
<div class="space-y-5 text-sm text-neutral-600 dark:text-neutral-400">
|
||||
<p>
|
||||
Use these fields to build filter rules. Combine multiple rules with AND/OR logic to create
|
||||
complex filters.
|
||||
</p>
|
||||
|
||||
{#if booleanFields.length > 0}
|
||||
<div>
|
||||
<div class="mb-2 font-medium text-neutral-900 dark:text-neutral-100">Yes/No Fields</div>
|
||||
<div class="space-y-1.5">
|
||||
{#each booleanFields as field}
|
||||
<div class="flex items-start gap-2">
|
||||
<span class="font-medium text-neutral-700 dark:text-neutral-300">{field.label}:</span>
|
||||
<span>
|
||||
{#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}
|
||||
</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if selectFields.length > 0}
|
||||
<div>
|
||||
<div class="mb-2 font-medium text-neutral-900 dark:text-neutral-100">Selection Fields</div>
|
||||
<div class="space-y-1.5">
|
||||
{#each selectFields as field}
|
||||
<div class="flex items-start gap-2">
|
||||
<span class="font-medium text-neutral-700 dark:text-neutral-300">{field.label}:</span>
|
||||
<span>
|
||||
{#if field.id === 'minimum_availability'}
|
||||
The minimum availability status ({field.values?.map((v) => v.label).join(', ')})
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if textFields.length > 0}
|
||||
<div>
|
||||
<div class="mb-2 font-medium text-neutral-900 dark:text-neutral-100">Text Fields</div>
|
||||
<p class="mb-2 text-xs">All text comparisons are case-insensitive.</p>
|
||||
<div class="space-y-1.5">
|
||||
{#each textFields as field}
|
||||
<div class="flex items-start gap-2">
|
||||
<span class="font-medium text-neutral-700 dark:text-neutral-300">{field.label}:</span>
|
||||
<span>
|
||||
{#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}
|
||||
</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if numberFields.length > 0}
|
||||
<div>
|
||||
<div class="mb-2 font-medium text-neutral-900 dark:text-neutral-100">Number Fields</div>
|
||||
<div class="space-y-1.5">
|
||||
{#each numberFields as field}
|
||||
<div class="flex items-start gap-2">
|
||||
<span class="font-medium text-neutral-700 dark:text-neutral-300">{field.label}:</span>
|
||||
<span>
|
||||
{#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}
|
||||
</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if dateFields.length > 0}
|
||||
<div>
|
||||
<div class="mb-2 font-medium text-neutral-900 dark:text-neutral-100">Date Fields</div>
|
||||
<div class="space-y-1.5">
|
||||
{#each dateFields as field}
|
||||
<div class="flex items-start gap-2">
|
||||
<span class="font-medium text-neutral-700 dark:text-neutral-300">{field.label}:</span>
|
||||
<span>
|
||||
{#if field.id === 'date_added'}
|
||||
When the movie was added to your library
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<div class="mb-2 font-medium text-neutral-900 dark:text-neutral-100">Filter Settings</div>
|
||||
<div class="space-y-1.5">
|
||||
<div class="flex items-start gap-2">
|
||||
<span class="font-medium text-neutral-700 dark:text-neutral-300">Cutoff %:</span>
|
||||
<span>Score threshold (0-100%) for the "Cutoff Met" filter</span>
|
||||
</div>
|
||||
<div class="flex items-start gap-2">
|
||||
<span class="font-medium text-neutral-700 dark:text-neutral-300">Cooldown:</span>
|
||||
<span>Skip items that were searched within this many hours</span>
|
||||
</div>
|
||||
<div class="flex items-start gap-2">
|
||||
<span class="font-medium text-neutral-700 dark:text-neutral-300">Method:</span>
|
||||
<span>How to select items from the filtered results</span>
|
||||
</div>
|
||||
<div class="flex items-start gap-2">
|
||||
<span class="font-medium text-neutral-700 dark:text-neutral-300">Count:</span>
|
||||
<span>Number of items to search per run</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</InfoModal>
|
||||
@@ -1,66 +0,0 @@
|
||||
<script lang="ts">
|
||||
import InfoModal from '$ui/modal/InfoModal.svelte';
|
||||
|
||||
export let open: boolean = false;
|
||||
</script>
|
||||
|
||||
<InfoModal bind:open header="How Upgrades Work">
|
||||
<div class="space-y-4 text-sm text-neutral-600 dark:text-neutral-400">
|
||||
<div>
|
||||
<div class="font-medium text-neutral-900 dark:text-neutral-100">Why is this needed?</div>
|
||||
<div class="mt-1 space-y-2">
|
||||
<p>
|
||||
*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.
|
||||
</p>
|
||||
<p>
|
||||
This module lets you define exactly what to upgrade, when, and how — all within Profilarr.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="font-medium text-neutral-900 dark:text-neutral-100">Core Settings</div>
|
||||
<div class="mt-1">
|
||||
Configure the basic behavior: set how often it runs and choose how to cycle through your
|
||||
filters (round robin or random shuffle).
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="font-medium text-neutral-900 dark:text-neutral-100">Filters</div>
|
||||
<div class="mt-1">
|
||||
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.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="font-medium text-neutral-900 dark:text-neutral-100">Filter Mode</div>
|
||||
<div class="mt-1 space-y-1">
|
||||
<p><strong>Round Robin:</strong> Cycle through filters in order, one per run.</p>
|
||||
<p>
|
||||
<strong>Random Shuffle:</strong> Randomly shuffle filters, use each once before repeating.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="font-medium text-neutral-900 dark:text-neutral-100">Selection</div>
|
||||
<div class="mt-1">
|
||||
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.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="font-medium text-neutral-900 dark:text-neutral-100">How it runs</div>
|
||||
<div class="mt-1">
|
||||
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.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</InfoModal>
|
||||
21
src/routes/arr/[id]/upgrades/info/+page.server.ts
Normal file
21
src/routes/arr/[id]/upgrades/info/+page.server.ts
Normal file
@@ -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
|
||||
};
|
||||
};
|
||||
179
src/routes/arr/[id]/upgrades/info/+page.svelte
Normal file
179
src/routes/arr/[id]/upgrades/info/+page.svelte
Normal file
@@ -0,0 +1,179 @@
|
||||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
import { filterFields, filterModes } from '$lib/shared/filters';
|
||||
import { selectors } from '$lib/shared/selectors';
|
||||
import { ArrowLeft } from 'lucide-svelte';
|
||||
import StickyCard from '$ui/card/StickyCard.svelte';
|
||||
import Button from '$ui/button/Button.svelte';
|
||||
import ExpandableTable from '$ui/table/ExpandableTable.svelte';
|
||||
import Table from '$ui/table/Table.svelte';
|
||||
import type { Column } from '$ui/table/types';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
// Filter fields with type labels
|
||||
const typeLabels: Record<string, string> = {
|
||||
boolean: 'Yes/No',
|
||||
select: 'Selection',
|
||||
text: 'Text',
|
||||
number: 'Number',
|
||||
date: 'Date'
|
||||
};
|
||||
|
||||
const badgeBase = 'inline-flex items-center rounded font-medium px-1.5 py-0.5 text-[10px]';
|
||||
const badgeAccent = 'bg-accent-100 text-accent-800 dark:bg-accent-900 dark:text-accent-200';
|
||||
const badgeNeutral = 'bg-neutral-100 text-neutral-700 dark:bg-neutral-800 dark:text-neutral-300';
|
||||
|
||||
const fieldColumns: Column<(typeof filterFields)[0]>[] = [
|
||||
{ key: 'label', header: 'Field', sortable: false },
|
||||
{
|
||||
key: 'valueType',
|
||||
header: 'Type',
|
||||
sortable: false,
|
||||
cell: (row) => ({
|
||||
html: `<span class="${badgeBase} ${badgeAccent}">${typeLabels[row.valueType] || row.valueType}</span>`
|
||||
})
|
||||
},
|
||||
{ key: 'description', header: 'Description', sortable: false },
|
||||
{
|
||||
key: 'operators',
|
||||
header: 'Operators',
|
||||
sortable: false,
|
||||
cell: (row) => ({
|
||||
html: row.operators
|
||||
.map((op) => `<span class="${badgeBase} ${badgeNeutral}">${op.label}</span>`)
|
||||
.join(' ')
|
||||
})
|
||||
}
|
||||
];
|
||||
|
||||
// Selectors table
|
||||
const selectorColumns: Column<(typeof selectors)[0]>[] = [
|
||||
{ key: 'label', header: 'Selector', sortable: false },
|
||||
{ key: 'description', header: 'Description', sortable: false }
|
||||
];
|
||||
|
||||
// Filter modes table
|
||||
const modeColumns: Column<(typeof filterModes)[0]>[] = [
|
||||
{ key: 'label', header: 'Mode', sortable: false },
|
||||
{ key: 'description', header: 'Description', sortable: false }
|
||||
];
|
||||
|
||||
// Concepts for the main explanation
|
||||
const concepts = [
|
||||
{
|
||||
id: 'filters',
|
||||
name: 'Filters',
|
||||
summary: 'Rules that define what qualifies for upgrade',
|
||||
details:
|
||||
'Combine conditions with AND/OR logic. Example: monitored = true AND cutoff met = false AND (popularity > 30 OR year >= 2020). Rules can be nested into groups for complex logic.'
|
||||
},
|
||||
{
|
||||
id: 'selectors',
|
||||
name: 'Selectors',
|
||||
summary: 'How to prioritize from filtered results',
|
||||
details:
|
||||
"After filtering narrows the pool, the selector picks which items actually get searched. Random spreads searches evenly over time. Oldest/Newest prioritizes by when items were added. Lowest Score targets items most in need of upgrades. Most/Least Popular lets you prioritize based on TMDb popularity."
|
||||
},
|
||||
{
|
||||
id: 'count-cooldown',
|
||||
name: 'Count & Cooldown',
|
||||
summary: 'Batch size and search throttling',
|
||||
details:
|
||||
"Count limits how many items get searched per run - prevents overwhelming your indexers. Cooldown (in hours) prevents re-searching the same item too soon. Items are tagged with the search date so they're skipped until the cooldown expires."
|
||||
},
|
||||
{
|
||||
id: 'cutoff',
|
||||
name: 'Cutoff %',
|
||||
summary: 'Quality score threshold for the Cutoff Met field',
|
||||
details:
|
||||
'The Cutoff Met filter field checks if an item\'s custom format score has reached this percentage of the profile\'s cutoff score. Set to 80% means items below 80% of their cutoff will have "Cutoff Met = false".'
|
||||
},
|
||||
{
|
||||
id: 'multiple-filters',
|
||||
name: 'Multiple Filters',
|
||||
summary: 'Different strategies for different content',
|
||||
details:
|
||||
'Create separate filters for different upgrade strategies. A "High Priority" filter for popular recent content, a "Backlog" filter for older items. The filter mode controls which runs each cycle - round robin cycles through in order, random shuffle picks one at random.'
|
||||
},
|
||||
{
|
||||
id: 'dry-run',
|
||||
name: 'Dry Run',
|
||||
summary: 'Test without triggering searches',
|
||||
details:
|
||||
"Enable dry run mode to test your filters without triggering actual searches. The full filter/select pipeline runs, but no searches are sent to your arr instance. Check the logs to see what would have been searched."
|
||||
}
|
||||
];
|
||||
|
||||
const conceptColumns: Column<(typeof concepts)[0]>[] = [
|
||||
{ key: 'name', header: 'Concept', sortable: false },
|
||||
{ key: 'summary', header: 'Summary', sortable: false }
|
||||
];
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{data.instance.name} - Upgrades Info - Profilarr</title>
|
||||
</svelte:head>
|
||||
|
||||
<StickyCard position="top">
|
||||
<div slot="left">
|
||||
<h1 class="text-xl font-semibold text-neutral-900 dark:text-neutral-50">How Upgrades Work</h1>
|
||||
</div>
|
||||
<div slot="right">
|
||||
<Button text="Back" icon={ArrowLeft} href="/arr/{data.instance.id}/upgrades" />
|
||||
</div>
|
||||
</StickyCard>
|
||||
|
||||
<div class="mt-6 space-y-8 px-4">
|
||||
<!-- Intro -->
|
||||
<div class="text-neutral-600 dark:text-neutral-400">
|
||||
<p>
|
||||
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: <span class="font-medium text-neutral-700 dark:text-neutral-300"
|
||||
>Filter</span
|
||||
>
|
||||
your library,
|
||||
<span class="font-medium text-neutral-700 dark:text-neutral-300">Select</span> items to search,
|
||||
then
|
||||
<span class="font-medium text-neutral-700 dark:text-neutral-300">Search</span> for better releases.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Concepts -->
|
||||
<section>
|
||||
<h2 class="mb-3 text-lg font-semibold text-neutral-900 dark:text-neutral-100">Concepts</h2>
|
||||
<ExpandableTable
|
||||
columns={conceptColumns}
|
||||
data={concepts}
|
||||
getRowId={(row) => row.id}
|
||||
emptyMessage="No concepts"
|
||||
chevronPosition="right"
|
||||
flushExpanded
|
||||
>
|
||||
<svelte:fragment slot="expanded" let:row>
|
||||
<div class="px-6 py-4">
|
||||
<p class="text-sm text-neutral-600 dark:text-neutral-400">{row.details}</p>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</ExpandableTable>
|
||||
</section>
|
||||
|
||||
<!-- Selectors Reference -->
|
||||
<section>
|
||||
<h2 class="mb-3 text-lg font-semibold text-neutral-900 dark:text-neutral-100">Selectors</h2>
|
||||
<Table columns={selectorColumns} data={selectors} emptyMessage="No selectors" />
|
||||
</section>
|
||||
|
||||
<!-- Filter Modes Reference -->
|
||||
<section>
|
||||
<h2 class="mb-3 text-lg font-semibold text-neutral-900 dark:text-neutral-100">Filter Modes</h2>
|
||||
<Table columns={modeColumns} data={filterModes} emptyMessage="No modes" />
|
||||
</section>
|
||||
|
||||
<!-- Filter Fields Reference -->
|
||||
<section>
|
||||
<h2 class="mb-3 text-lg font-semibold text-neutral-900 dark:text-neutral-100">Filter Fields</h2>
|
||||
<Table columns={fieldColumns} data={filterFields} emptyMessage="No fields" />
|
||||
</section>
|
||||
</div>
|
||||
Reference in New Issue
Block a user