server/src/rules/types.ts
Rama main · réplica de lectura del repositorio original.
import type { MediaItem, Rule } from '../types';
// ============================================================================
// V2 Rule Condition Tree Types
// ============================================================================
/**
* Logic operators for grouping condition nodes in v2 schema.
*/
export type GroupLogic = 'AND' | 'OR' | 'NOT';
/**
* A single condition evaluating one field against an operator/value pair.
* Optional `params` carry operator-specific metadata (e.g. username for
* watched_by_user).
*/
export interface ConditionLeaf {
kind: 'condition';
field: string;
operator: string;
value: unknown;
params?: Record<string, unknown>;
}
/**
* A group node combining child nodes under a logical operator.
* `NOT` groups negate the conjunction of their children (i.e. NOT(AND(children))).
*/
export interface ConditionGroupNode {
kind: 'group';
logic: GroupLogic;
children: ConditionNode[];
}
export type ConditionNode = ConditionLeaf | ConditionGroupNode;
/**
* Legacy (v1) condition shape preserved for backward compatibility.
*/
export interface LegacyCondition {
field: string;
operator: string;
value: unknown;
}
/**
* Versioned rule conditions payload. Persisted as JSON inside `rules.conditions`.
*/
export type RuleConditions =
| { version: 1; conditions: LegacyCondition[]; logic: 'AND' | 'OR' }
| { version: 2; root: ConditionNode };
// ============================================================================
// Rule Condition Types (Legacy)
// ============================================================================
/**
* Condition types supported by the rules engine
*/
export type ConditionType =
| 'not_watched_days'
| 'play_count_less_than'
| 'file_size_greater_than'
| 'resolution_less_than'
| 'added_days_ago'
| 'genre_is'
| 'genre_is_not'
| 'year_before'
| 'year_after'
| 'rating_less_than'
| 'type_is';
/**
* Logic operators for combining conditions
*/
export type LogicOperator = 'AND' | 'OR';
/**
* Extended condition with support for custom condition types
*/
export interface ExtendedRuleCondition {
type: ConditionType;
value: string | number | boolean | string[];
operator?: 'equals' | 'not_equals' | 'greater_than' | 'less_than' | 'contains' | 'not_contains';
}
/**
* Condition group for complex rule logic
*/
export interface ConditionGroup {
logic: LogicOperator;
conditions: ExtendedRuleCondition[];
}
// ============================================================================
// Action Types
// ============================================================================
/**
* Actions that can be taken on media items
*/
export enum Action {
MARK_FOR_DELETION = 'mark_for_deletion',
PROTECT = 'protect',
IGNORE = 'ignore',
}
/**
* Deletion actions specifying how to handle the deletion in Sonarr/Radarr
*/
export enum DeletionAction {
/** Only unmonitor the item, keep files and metadata */
UNMONITOR_ONLY = 'unmonitor_only',
/** Delete files but keep the item in Sonarr/Radarr (unmonitored) */
DELETE_FILES_ONLY = 'delete_files_only',
/** Unmonitor and delete files */
UNMONITOR_AND_DELETE = 'unmonitor_and_delete',
/** Completely remove from Sonarr/Radarr including metadata */
FULL_REMOVAL = 'full_removal',
}
/**
* Human-readable labels for deletion actions
*/
export const DELETION_ACTION_LABELS: Record<DeletionAction, string> = {
[DeletionAction.UNMONITOR_ONLY]: 'Unmonitor Only (keep files)',
[DeletionAction.DELETE_FILES_ONLY]: 'Delete Files Only',
[DeletionAction.UNMONITOR_AND_DELETE]: 'Unmonitor & Delete Files',
[DeletionAction.FULL_REMOVAL]: 'Full Removal (delete everything)',
};
/**
* Descriptions for deletion actions
*/
export const DELETION_ACTION_DESCRIPTIONS: Record<DeletionAction, string> = {
[DeletionAction.UNMONITOR_ONLY]: 'Stop monitoring the item but keep all files and metadata intact',
[DeletionAction.DELETE_FILES_ONLY]: 'Delete the media files but keep the item in Sonarr/Radarr for re-download',
[DeletionAction.UNMONITOR_AND_DELETE]: 'Unmonitor and delete files, but keep metadata in Sonarr/Radarr',
[DeletionAction.FULL_REMOVAL]: 'Completely remove from Sonarr/Radarr including all metadata',
};
// ============================================================================
// Evaluation Result Types
// ============================================================================
/**
* Result of evaluating a single item against a rule
*/
export interface ItemEvaluationResult {
matched: boolean;
rule?: Rule;
action?: Action;
matchedConditions?: ExtendedRuleCondition[];
}
/**
* Result of evaluating an item against all rules
*/
export interface EvaluationResult {
item: MediaItem;
matched: boolean;
matchedRule?: Rule;
action?: Action;
isProtected: boolean;
protectionReason?: string;
evaluatedAt: Date;
}
/**
* Summary of a full evaluation run
*/
export interface EvaluationSummary {
itemsEvaluated: number;
itemsFlagged: number;
itemsProtected: number;
itemsIgnored: number;
results: EvaluationResult[];
startedAt: Date;
completedAt: Date;
durationMs: number;
}
// ============================================================================
// Protection Rule Types
// ============================================================================
/**
* Result of protection rule evaluation
*/
export interface ProtectionResult {
isProtected: boolean;
reason?: string;
}
/**
* Configuration for protection rules
*/
export interface ProtectionConfig {
protectRecentlyAdded: boolean;
recentlyAddedDays: number;
protectRecentlyWatched: boolean;
recentlyWatchedDays: number;
protectInProgress: boolean;
protectedGenres: string[];
protectedTags: string[];
protectedRatings: number; // Protect items rated above this threshold
}
// ============================================================================
// Queue Types
// ============================================================================
/**
* Item in the deletion queue
*/
export interface QueueItem {
id: number;
mediaItem: MediaItem;
markedAt: Date;
deleteAfter: Date;
ruleId?: number;
ruleName?: string;
daysRemaining: number;
action: DeletionAction;
/** Whether to reset the item in Overseerr after deletion */
resetOverseerr: boolean;
/** Who originally requested this content (from Overseerr) */
requestedBy?: string;
}
/**
* Result of a deletion operation
*/
export interface DeletionResult {
success: boolean;
itemId: number;
title: string;
action: DeletionAction;
fileSizeFreed?: number;
error?: string;
deletedAt?: Date;
/** Whether the item was reset in Overseerr */
overseerrReset?: boolean;
/** Error when trying to reset in Overseerr (non-fatal) */
overseerrError?: string;
}
// ============================================================================
// Extended Media Item with additional metadata
// ============================================================================
/**
* Extended media item with parsed metadata for rule evaluation
*/
export interface ExtendedMediaItem extends Omit<MediaItem, 'genres' | 'tags'> {
genres?: string[] | null;
tags?: string[] | null;
rating?: number;
inProgress?: boolean;
daysSinceLastWatched?: number;
daysSinceAdded?: number;
fileSizeGB?: number;
resolutionValue?: number; // Numeric representation (720, 1080, 2160)
}
// ============================================================================
// Rule Configuration Types
// ============================================================================
/**
* Parsed rule with conditions ready for evaluation
*/
export interface ParsedRule extends Omit<Rule, 'conditions'> {
conditions: ExtendedRuleCondition[];
conditionLogic: LogicOperator;
gracePeriodDays: number;
deletionAction: DeletionAction;
}
/**
* Default configuration for rules
*/
export interface RulesConfig {
defaultGracePeriodDays: number;
defaultDeletionAction: DeletionAction;
defaultConditionLogic: LogicOperator;
enableDryRun: boolean;
maxItemsPerRun: number;
}
export const DEFAULT_RULES_CONFIG: RulesConfig = {
defaultGracePeriodDays: 7,
defaultDeletionAction: DeletionAction.UNMONITOR_AND_DELETE,
defaultConditionLogic: 'AND',
enableDryRun: false,
maxItemsPerRun: 1000,
};