mirror of
https://github.com/agregarr/agregarr.git
synced 2026-01-25 03:48:48 -06:00
fix(overlays): add 'does not contain' string operator
also adds templateData to hash comparison fix #367
This commit is contained in:
@@ -3304,6 +3304,7 @@ components:
|
||||
'lte',
|
||||
'in',
|
||||
'contains',
|
||||
'notContains',
|
||||
'regex',
|
||||
'begins',
|
||||
'ends',
|
||||
|
||||
@@ -195,6 +195,7 @@ export interface ConditionRule {
|
||||
| 'lte' // less than or equal
|
||||
| 'in' // value in array
|
||||
| 'contains' // string contains
|
||||
| 'notContains' // string does not contain
|
||||
| 'regex' // regex match
|
||||
| 'begins' // string begins with
|
||||
| 'ends' // string ends with
|
||||
|
||||
@@ -405,8 +405,8 @@ export async function buildRenderContext(
|
||||
}
|
||||
}
|
||||
|
||||
// Plex-specific metadata from Media (skip for placeholder items)
|
||||
if (!isPlaceholder && item.Media?.[0]) {
|
||||
// Plex-specific metadata from Media (extract if available, even for placeholders)
|
||||
if (item.Media?.[0]) {
|
||||
const media = item.Media[0];
|
||||
|
||||
// Resolution - use raw value from Plex (e.g., "720", "1080", "4k")
|
||||
@@ -432,6 +432,16 @@ export async function buildRenderContext(
|
||||
context.container = media.container;
|
||||
context.bitrate = media.bitrate;
|
||||
|
||||
// Extract file path and size from Part (independent of Stream data)
|
||||
if (media.Part?.[0]) {
|
||||
if (media.Part[0].file) {
|
||||
context.filePath = media.Part[0].file;
|
||||
}
|
||||
if (media.Part[0].size) {
|
||||
context.fileSize = media.Part[0].size;
|
||||
}
|
||||
}
|
||||
|
||||
// Extract detailed info from Streams
|
||||
if (media.Part?.[0]?.Stream) {
|
||||
const streams = media.Part[0].Stream;
|
||||
@@ -478,15 +488,6 @@ export async function buildRenderContext(
|
||||
context.audioChannels = audioStream.channels;
|
||||
}
|
||||
}
|
||||
|
||||
// Get file path from Part
|
||||
if (media.Part[0].file) {
|
||||
context.filePath = media.Part[0].file;
|
||||
}
|
||||
// Get file size
|
||||
if (media.Part[0].size) {
|
||||
context.fileSize = media.Part[0].size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -756,6 +756,7 @@ class OverlayLibraryService {
|
||||
|
||||
const overlayInputHash = calculateOverlayInputHash({
|
||||
templateIds: matchingTemplates.map((t) => t.id).sort(),
|
||||
templateData: templateDataArray,
|
||||
usedFields: usedFields,
|
||||
context: context as Record<string, unknown>,
|
||||
});
|
||||
|
||||
@@ -185,6 +185,11 @@ function evaluateRule(
|
||||
if (rule.operator === 'neq') {
|
||||
return conditionValue !== undefined && conditionValue !== null;
|
||||
}
|
||||
// For 'notContains', missing/null doesn't contain anything, so return true
|
||||
// e.g., "filePath does not contain '2005'" should match when filePath is undefined
|
||||
if (rule.operator === 'notContains') {
|
||||
return true;
|
||||
}
|
||||
// For 'exists', we need to evaluate based on the presence/absence of value
|
||||
if (rule.operator === 'exists') {
|
||||
// value is null/undefined, so field does NOT exist
|
||||
@@ -276,6 +281,20 @@ function evaluateRule(
|
||||
typeof conditionValue === 'string' &&
|
||||
value.toLowerCase().includes(conditionValue.toLowerCase())
|
||||
);
|
||||
case 'notContains':
|
||||
// For array fields, check if array does NOT contain the value
|
||||
if (Array.isArray(value) && typeof conditionValue === 'string') {
|
||||
return !value.some(
|
||||
(item) =>
|
||||
typeof item === 'string' &&
|
||||
item.toLowerCase().includes(conditionValue.toLowerCase())
|
||||
);
|
||||
}
|
||||
return (
|
||||
typeof value === 'string' &&
|
||||
typeof conditionValue === 'string' &&
|
||||
!value.toLowerCase().includes(conditionValue.toLowerCase())
|
||||
);
|
||||
case 'regex':
|
||||
if (typeof value === 'string' && typeof conditionValue === 'string') {
|
||||
try {
|
||||
|
||||
@@ -164,11 +164,19 @@ export function calculateThemeInputHash(filename: string): string {
|
||||
|
||||
/**
|
||||
* Calculate hash for overlay inputs
|
||||
* Only includes context fields that are actually used by the templates
|
||||
* This prevents unnecessary regeneration when unused fields change
|
||||
* Includes:
|
||||
* - Template IDs (which templates are applied)
|
||||
* - Template data (design: positions, colors, icon/image paths, etc.)
|
||||
* - Context fields actually used by the templates
|
||||
*
|
||||
* This ensures regeneration when:
|
||||
* - Different templates match
|
||||
* - Template design changes (including icon/image path changes)
|
||||
* - Context values change
|
||||
*/
|
||||
export function calculateOverlayInputHash(config: {
|
||||
templateIds: number[];
|
||||
templateData: OverlayTemplateData[];
|
||||
usedFields: Set<string>;
|
||||
context: Record<string, unknown>;
|
||||
}): string {
|
||||
@@ -180,6 +188,7 @@ export function calculateOverlayInputHash(config: {
|
||||
|
||||
return calculateInputHash({
|
||||
templateIds: [...config.templateIds].sort(), // Ensure sorted for consistency
|
||||
templateData: config.templateData, // Include template design (positions, colors, icon paths)
|
||||
context: relevantContext, // Only include fields actually used by templates
|
||||
});
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ const OPERATOR_LABELS: Record<string, string> = {
|
||||
lte: '≤',
|
||||
in: 'in',
|
||||
contains: 'contains',
|
||||
notContains: '!contains',
|
||||
regex: 'regex',
|
||||
begins: 'begins',
|
||||
ends: 'ends',
|
||||
|
||||
@@ -53,6 +53,7 @@ const messages = defineMessages({
|
||||
opLessThan: 'less than',
|
||||
opLessOrEqual: 'less than or equal',
|
||||
opContains: 'contains',
|
||||
opNotContains: 'does not contain',
|
||||
opRegex: 'regex',
|
||||
opBegins: 'begins with',
|
||||
opEnds: 'ends with',
|
||||
@@ -288,6 +289,9 @@ const RuleItem: React.FC<RuleItemProps> = ({
|
||||
<option value="contains">
|
||||
{intl.formatMessage(messages.opContains)}
|
||||
</option>
|
||||
<option value="notContains">
|
||||
{intl.formatMessage(messages.opNotContains)}
|
||||
</option>
|
||||
<option value="regex">
|
||||
{intl.formatMessage(messages.opRegex)}
|
||||
</option>
|
||||
|
||||
@@ -110,6 +110,7 @@ const messages = defineMessages({
|
||||
opLessThan: 'less than',
|
||||
opLessOrEqual: 'at most',
|
||||
opContains: 'contains',
|
||||
opNotContains: 'does not contain',
|
||||
opRegex: 'matches regex',
|
||||
opBegins: 'begins with',
|
||||
opEnds: 'ends with',
|
||||
|
||||
@@ -137,6 +137,7 @@ export interface ConditionRule {
|
||||
| 'lte' // less than or equal
|
||||
| 'in' // value in array
|
||||
| 'contains' // string contains
|
||||
| 'notContains' // string does not contain
|
||||
| 'regex' // regex match
|
||||
| 'begins' // string begins with
|
||||
| 'ends' // string ends with
|
||||
|
||||
@@ -45,6 +45,7 @@ const OPERATOR_LABELS: Record<string, string> = {
|
||||
lte: '≤',
|
||||
in: 'in',
|
||||
contains: 'contains',
|
||||
notContains: '!contains',
|
||||
};
|
||||
|
||||
// Get field label from AVAILABLE_VARIABLES
|
||||
|
||||
@@ -44,6 +44,7 @@ const OPERATOR_LABELS: Record<string, string> = {
|
||||
lte: '≤',
|
||||
in: 'in',
|
||||
contains: 'contains',
|
||||
notContains: '!contains',
|
||||
};
|
||||
|
||||
// Get field label from AVAILABLE_VARIABLES
|
||||
|
||||
Reference in New Issue
Block a user