fix(overlays): add 'does not contain' string operator

also adds templateData to hash comparison

fix #367
This commit is contained in:
Tom Wheeler
2026-01-21 05:26:50 +13:00
parent 1be54fdcf7
commit fad645485b
12 changed files with 54 additions and 13 deletions

View File

@@ -3304,6 +3304,7 @@ components:
'lte',
'in',
'contains',
'notContains',
'regex',
'begins',
'ends',

View File

@@ -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

View File

@@ -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;
}
}
}

View File

@@ -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>,
});

View File

@@ -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 {

View File

@@ -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
});
}

View File

@@ -16,6 +16,7 @@ const OPERATOR_LABELS: Record<string, string> = {
lte: '≤',
in: 'in',
contains: 'contains',
notContains: '!contains',
regex: 'regex',
begins: 'begins',
ends: 'ends',

View File

@@ -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>

View File

@@ -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',

View File

@@ -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

View File

@@ -45,6 +45,7 @@ const OPERATOR_LABELS: Record<string, string> = {
lte: '≤',
in: 'in',
contains: 'contains',
notContains: '!contains',
};
// Get field label from AVAILABLE_VARIABLES

View File

@@ -44,6 +44,7 @@ const OPERATOR_LABELS: Record<string, string> = {
lte: '≤',
in: 'in',
contains: 'contains',
notContains: '!contains',
};
// Get field label from AVAILABLE_VARIABLES