From 9eb1c0f55330b1e69481b89c3c03d527f46e9b0d Mon Sep 17 00:00:00 2001 From: Seraph91P Date: Sun, 12 Apr 2026 13:02:09 +0200 Subject: [PATCH 1/3] feat: add per-run options and unmatched channel reporting - Add overwrite_existing, skip_vod, ignore_cache fields to enrich_logos action - Action fields override global settings for manual runs - Bypass match cache when ignore_cache is enabled - Log and report unmatched channel names in result data --- Plugin.php | 57 +++++++++++++++++++++++++++++++++++++++++------------ plugin.json | 18 +++++++++++++++++ 2 files changed, 62 insertions(+), 13 deletions(-) diff --git a/Plugin.php b/Plugin.php index 8a6de5e..4c91a76 100644 --- a/Plugin.php +++ b/Plugin.php @@ -157,6 +157,10 @@ private function healthCheck(PluginExecutionContext $context): PluginActionResul /** * Entry point for the manual enrich_logos action. + * + * Accepts optional overrides for overwrite_existing, skip_vod, and + * ignore_cache so the user can control these per run without changing + * the global plugin settings. */ private function enrichFromAction(array $payload, PluginExecutionContext $context): PluginActionResult { @@ -166,19 +170,36 @@ private function enrichFromAction(array $payload, PluginExecutionContext $contex return PluginActionResult::failure('Missing playlist_id in action payload.'); } - return $this->processPlaylist($playlistId, $context); + $overrides = []; + + if (array_key_exists('overwrite_existing', $payload)) { + $overrides['overwrite_existing'] = (bool) $payload['overwrite_existing']; + } + + if (array_key_exists('skip_vod', $payload)) { + $overrides['skip_vod'] = (bool) $payload['skip_vod']; + } + + if (array_key_exists('ignore_cache', $payload)) { + $overrides['ignore_cache'] = (bool) $payload['ignore_cache']; + } + + return $this->processPlaylist($playlistId, $context, $overrides); } /** * Core enrichment logic — queries channels for the given playlist and attempts * to match each one against a logo from the tv-logo/tv-logos CDN. + * + * @param array{overwrite_existing?: bool, skip_vod?: bool, ignore_cache?: bool} $overrides */ - private function processPlaylist(int $playlistId, PluginExecutionContext $context): PluginActionResult + private function processPlaylist(int $playlistId, PluginExecutionContext $context, array $overrides = []): PluginActionResult { $settings = $context->settings; $countryCode = strtolower(trim((string) ($settings['country_code'] ?? 'us'))); - $overwriteExisting = (bool) ($settings['overwrite_existing'] ?? false); - $skipVod = (bool) ($settings['skip_vod'] ?? true); + $overwriteExisting = (bool) ($overrides['overwrite_existing'] ?? $settings['overwrite_existing'] ?? false); + $skipVod = (bool) ($overrides['skip_vod'] ?? $settings['skip_vod'] ?? true); + $ignoreCache = (bool) ($overrides['ignore_cache'] ?? false); $cacheTtlDays = (int) ($settings['cache_ttl_days'] ?? 7); $isDryRun = $context->dryRun; @@ -247,6 +268,7 @@ private function processPlaylist(int $playlistId, PluginExecutionContext $contex $matched = 0; $cacheHits = 0; $cacheMisses = 0; + $unmatched = []; foreach ($channels as $i => $channel) { $displayName = trim((string) ($channel->title_custom ?? $channel->title ?? $channel->name_custom ?? $channel->name ?? '')); @@ -257,7 +279,7 @@ private function processPlaylist(int $playlistId, PluginExecutionContext $contex $cacheKey = $countryCode.':'.mb_strtolower($displayName, 'UTF-8'); - if (array_key_exists($cacheKey, $cache['matches'])) { + if (! $ignoreCache && array_key_exists($cacheKey, $cache['matches'])) { $logoUrl = $cache['matches'][$cacheKey] ?: null; $cacheHits++; } else { @@ -274,6 +296,9 @@ private function processPlaylist(int $playlistId, PluginExecutionContext $contex if (! $isDryRun) { Channel::where('id', $channel->id)->update(['logo' => $logoUrl]); } + } else { + $unmatched[] = $displayName; + $context->info("Unmatched: \"{$displayName}\""); } if (($i + 1) % 20 === 0) { @@ -285,16 +310,22 @@ private function processPlaylist(int $playlistId, PluginExecutionContext $contex $this->saveCache($cache); } + $resultData = [ + 'matched' => $matched, + 'total' => $total, + 'cache_hits' => $cacheHits, + 'cache_misses' => $cacheMisses, + 'country_code' => $countryCode, + 'dry_run' => $isDryRun, + ]; + + if ($unmatched !== []) { + $resultData['unmatched'] = $unmatched; + } + return PluginActionResult::success( sprintf('%d of %d channel(s) matched%s.', $matched, $total, $isDryRun ? ' (dry run — no changes written)' : ''), - [ - 'matched' => $matched, - 'total' => $total, - 'cache_hits' => $cacheHits, - 'cache_misses' => $cacheMisses, - 'country_code' => $countryCode, - 'dry_run' => $isDryRun, - ] + $resultData ); } diff --git a/plugin.json b/plugin.json index 92e0cae..e057ae2 100644 --- a/plugin.json +++ b/plugin.json @@ -98,6 +98,24 @@ "label_attribute": "name", "scope": "owned", "required": true + }, + { + "id": "overwrite_existing", + "label": "Overwrite channels that already have a logo", + "type": "boolean", + "default": false + }, + { + "id": "skip_vod", + "label": "Skip VOD channels", + "type": "boolean", + "default": true + }, + { + "id": "ignore_cache", + "label": "Ignore cache (force fresh lookups)", + "type": "boolean", + "default": false } ] } From 05b318660d3fd1e85cbd591c31406b408293bea5 Mon Sep 17 00:00:00 2001 From: Seraph91P Date: Sun, 12 Apr 2026 17:29:42 +0200 Subject: [PATCH 2/3] feat: add modular channel name normalization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add configurable normalization rules that clean up channel display names before slug generation and logo matching: - normalize_channel_names: master toggle to enable/disable normalization - strip_unicode: convert superscripts (²), subscripts, small-caps (ʀᴀᴡ) to ASCII - strip_raw: remove 'raw' suffix from quality tags (HDraw → HD) - strip_provider_info: remove Cable, Sat, Kabel, Astra, (VF), (UM), (HEVC), etc. - strip_quality_extras: remove Low/High suffixes after quality tags - custom_patterns: user-defined regex patterns (one per line) for additional cleanup Each rule can be toggled independently. Normalization runs before slugify() so the matching pipeline receives cleaner input without requiring filename hacks in the logo repository. --- Plugin.php | 103 +++++++++++++++++++++++++++++++++++++++++++++++++++- plugin.json | 42 +++++++++++++++++++++ 2 files changed, 143 insertions(+), 2 deletions(-) diff --git a/Plugin.php b/Plugin.php index 4c91a76..0f32cc3 100644 --- a/Plugin.php +++ b/Plugin.php @@ -202,6 +202,7 @@ private function processPlaylist(int $playlistId, PluginExecutionContext $contex $ignoreCache = (bool) ($overrides['ignore_cache'] ?? false); $cacheTtlDays = (int) ($settings['cache_ttl_days'] ?? 7); $isDryRun = $context->dryRun; + $normConfig = $this->buildNormalizationConfig($settings); $repo = trim((string) ($settings['github_repo'] ?? self::DEFAULT_GITHUB_REPO)); if ($repo === '') { @@ -277,13 +278,14 @@ private function processPlaylist(int $playlistId, PluginExecutionContext $contex continue; } - $cacheKey = $countryCode.':'.mb_strtolower($displayName, 'UTF-8'); + $normalizedName = $this->normalizeChannelName($displayName, $normConfig); + $cacheKey = $countryCode.':'.mb_strtolower($normalizedName, 'UTF-8'); if (! $ignoreCache && array_key_exists($cacheKey, $cache['matches'])) { $logoUrl = $cache['matches'][$cacheKey] ?: null; $cacheHits++; } else { - $logoUrl = $this->resolveLogoUrl($displayName, $countryCode, $countryFolder, $index); + $logoUrl = $this->resolveLogoUrl($normalizedName, $countryCode, $countryFolder, $index); $cache['matches'][$cacheKey] = $logoUrl ?? ''; $cacheChanged = true; $cacheMisses++; @@ -644,4 +646,101 @@ private function saveCache(array $cache): void // Non-fatal — a missing cache means the next run re-checks the CDN. } } + + /** + * Build the normalization configuration array from plugin settings. + * + * @param array $settings + * @return array{enabled: bool, strip_unicode: bool, strip_raw: bool, strip_provider_info: bool, strip_quality_extras: bool, custom_patterns: list} + */ + private function buildNormalizationConfig(array $settings): array + { + $enabled = (bool) ($settings['normalize_channel_names'] ?? false); + + $customPatterns = []; + $rawPatterns = trim((string) ($settings['normalize_custom_patterns'] ?? '')); + + if ($rawPatterns !== '') { + foreach (explode("\n", $rawPatterns) as $line) { + $line = trim($line); + if ($line !== '' && @preg_match($line, '') !== false) { + $customPatterns[] = $line; + } + } + } + + return [ + 'enabled' => $enabled, + 'strip_unicode' => $enabled && (bool) ($settings['normalize_strip_unicode'] ?? true), + 'strip_raw' => $enabled && (bool) ($settings['normalize_strip_raw'] ?? true), + 'strip_provider_info' => $enabled && (bool) ($settings['normalize_strip_provider_info'] ?? true), + 'strip_quality_extras' => $enabled && (bool) ($settings['normalize_strip_quality_extras'] ?? true), + 'custom_patterns' => $customPatterns, + ]; + } + + /** + * Normalize a channel display name before slug generation. + * + * Applies enabled normalization rules in a fixed order to produce + * a cleaner name that maps more reliably to logo filenames. + */ + private function normalizeChannelName(string $name, array $config): string + { + if (! $config['enabled']) { + return $name; + } + + // 1. Unicode → ASCII transliteration (superscripts, subscripts, small-caps) + if ($config['strip_unicode']) { + $unicodeMap = [ + // Superscripts + '⁰' => '0', '¹' => '1', '²' => '2', '³' => '3', '⁴' => '4', + '⁵' => '5', '⁶' => '6', '⁷' => '7', '⁸' => '8', '⁹' => '9', + '⁺' => '+', '⁻' => '-', + // Subscripts + '₀' => '0', '₁' => '1', '₂' => '2', '₃' => '3', '₄' => '4', + '₅' => '5', '₆' => '6', '₇' => '7', '₈' => '8', '₉' => '9', + // Small-caps Latin + 'ᴀ' => 'A', 'ʙ' => 'B', 'ᴄ' => 'C', 'ᴅ' => 'D', 'ᴇ' => 'E', + 'ꜰ' => 'F', 'ɢ' => 'G', 'ʜ' => 'H', 'ɪ' => 'I', 'ᴊ' => 'J', + 'ᴋ' => 'K', 'ʟ' => 'L', 'ᴍ' => 'M', 'ɴ' => 'N', 'ᴏ' => 'O', + 'ᴘ' => 'P', 'ꞯ' => 'Q', 'ʀ' => 'R', 'ꜱ' => 'S', 'ᴛ' => 'T', + 'ᴜ' => 'U', 'ᴠ' => 'V', 'ᴡ' => 'W', 'ʏ' => 'Y', 'ᴢ' => 'Z', + ]; + + $name = strtr($name, $unicodeMap); + } + + // 2. Strip "raw" / "RAW" appended to quality tags (e.g. "HDraw" → "HD") + if ($config['strip_raw']) { + $name = (string) preg_replace('/\b(HD|FHD|UHD|SD)\s*raw\b/iu', '$1', $name); + } + + // 3. Strip provider / transport info (Cable, Sat, Kabel, Astra, Satellit, etc.) + if ($config['strip_provider_info']) { + $name = (string) preg_replace('/\b(Cable|Sat|Kabel|Astra|Satellit|Terrestrial|DVB-[TCSA]2?|IPTV)\b/iu', '', $name); + // Also remove common provider tags in parentheses: (VF), (UM), (KD), (HEVC), etc. + $name = (string) preg_replace('/\(\s*(VF|UM|KD|KBW|HEVC|H\.?265)\s*\)/iu', '', $name); + } + + // 4. Strip extra quality descriptors that follow a quality tag + if ($config['strip_quality_extras']) { + // "HD Low" → "HD", "FHD+" → "FHD", "HD Plus" when it's a suffix not a channel name + $name = (string) preg_replace('/\b(HD|FHD|UHD|SD)\s*(Low|High)\b/iu', '$1', $name); + } + + // 5. Apply user-defined custom regex patterns (each pattern replaces match with empty string) + foreach ($config['custom_patterns'] as $pattern) { + $result = @preg_replace($pattern, '', $name); + if ($result !== null) { + $name = $result; + } + } + + // Final cleanup: collapse whitespace, trim + $name = trim((string) preg_replace('/\s{2,}/', ' ', $name)); + + return $name; + } } diff --git a/plugin.json b/plugin.json index e057ae2..880c03a 100644 --- a/plugin.json +++ b/plugin.json @@ -76,6 +76,48 @@ "label": "Match cache TTL in days (0 = never expire)", "type": "number", "default": 7 + }, + { + "id": "normalize_channel_names", + "label": "Normalize channel names before matching", + "type": "boolean", + "default": false, + "helper_text": "Enable to clean up channel names before logo matching. Configure individual rules below." + }, + { + "id": "normalize_strip_unicode", + "label": "Strip unicode special characters (², ³, ʀᴀᴡ, etc.)", + "type": "boolean", + "default": true, + "helper_text": "Remove superscripts, subscripts, and small-caps unicode characters." + }, + { + "id": "normalize_strip_raw", + "label": "Strip 'raw' from quality tags (HDraw → HD)", + "type": "boolean", + "default": true, + "helper_text": "Remove 'raw' suffix from quality indicators like HDraw, FHDraw, UHDraw." + }, + { + "id": "normalize_strip_provider_info", + "label": "Strip provider-specific info (Cable, Sat, etc.)", + "type": "boolean", + "default": true, + "helper_text": "Remove provider identifiers like Cable, Sat, Kabel that are not part of the channel name." + }, + { + "id": "normalize_strip_quality_extras", + "label": "Strip extra quality descriptors (Low, FHD+, etc.)", + "type": "boolean", + "default": true, + "helper_text": "Remove extra quality descriptors like Low, Plus (when after a quality tag), FHD+." + }, + { + "id": "normalize_custom_patterns", + "label": "Custom normalization patterns (one regex per line)", + "type": "textarea", + "default": null, + "helper_text": "Advanced: custom regex patterns to remove from channel names. One pattern per line, applied in order." } ], "actions": [ From 617a0d979288f36d68c4ff94e6dfd5fd8845787c Mon Sep 17 00:00:00 2001 From: Shaun Parkison Date: Mon, 13 Apr 2026 08:40:38 -0600 Subject: [PATCH 3/3] chore: Simplify --- Plugin.php | 27 ++++++++++++++++++++------- plugin.json | 15 +++++++++++---- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/Plugin.php b/Plugin.php index 0f32cc3..65b2c54 100644 --- a/Plugin.php +++ b/Plugin.php @@ -319,6 +319,7 @@ private function processPlaylist(int $playlistId, PluginExecutionContext $contex 'cache_misses' => $cacheMisses, 'country_code' => $countryCode, 'dry_run' => $isDryRun, + 'ignore_cache' => $ignoreCache, ]; if ($unmatched !== []) { @@ -651,12 +652,24 @@ private function saveCache(array $cache): void * Build the normalization configuration array from plugin settings. * * @param array $settings - * @return array{enabled: bool, strip_unicode: bool, strip_raw: bool, strip_provider_info: bool, strip_quality_extras: bool, custom_patterns: list} + * @return array{enabled: bool, strip_unicode: bool, strip_raw: bool, strip_provider_info: bool, provider_terms: list, strip_quality_extras: bool, custom_patterns: list} */ private function buildNormalizationConfig(array $settings): array { $enabled = (bool) ($settings['normalize_channel_names'] ?? false); + $providerTerms = []; + $rawProviderTerms = trim((string) ($settings['normalize_provider_terms'] ?? '')); + + if ($rawProviderTerms !== '') { + foreach (explode("\n", $rawProviderTerms) as $line) { + $line = trim($line); + if ($line !== '') { + $providerTerms[] = $line; + } + } + } + $customPatterns = []; $rawPatterns = trim((string) ($settings['normalize_custom_patterns'] ?? '')); @@ -674,6 +687,7 @@ private function buildNormalizationConfig(array $settings): array 'strip_unicode' => $enabled && (bool) ($settings['normalize_strip_unicode'] ?? true), 'strip_raw' => $enabled && (bool) ($settings['normalize_strip_raw'] ?? true), 'strip_provider_info' => $enabled && (bool) ($settings['normalize_strip_provider_info'] ?? true), + 'provider_terms' => $providerTerms, 'strip_quality_extras' => $enabled && (bool) ($settings['normalize_strip_quality_extras'] ?? true), 'custom_patterns' => $customPatterns, ]; @@ -717,16 +731,15 @@ private function normalizeChannelName(string $name, array $config): string $name = (string) preg_replace('/\b(HD|FHD|UHD|SD)\s*raw\b/iu', '$1', $name); } - // 3. Strip provider / transport info (Cable, Sat, Kabel, Astra, Satellit, etc.) - if ($config['strip_provider_info']) { - $name = (string) preg_replace('/\b(Cable|Sat|Kabel|Astra|Satellit|Terrestrial|DVB-[TCSA]2?|IPTV)\b/iu', '', $name); - // Also remove common provider tags in parentheses: (VF), (UM), (KD), (HEVC), etc. - $name = (string) preg_replace('/\(\s*(VF|UM|KD|KBW|HEVC|H\.?265)\s*\)/iu', '', $name); + // 3. Strip provider / transport terms (configurable list, one term per line in settings) + if ($config['strip_provider_info'] && $config['provider_terms'] !== []) { + $escapedTerms = array_map(fn (string $t): string => preg_quote($t, '/'), $config['provider_terms']); + $name = (string) preg_replace('/\b(' . implode('|', $escapedTerms) . ')\b/iu', '', $name); } // 4. Strip extra quality descriptors that follow a quality tag if ($config['strip_quality_extras']) { - // "HD Low" → "HD", "FHD+" → "FHD", "HD Plus" when it's a suffix not a channel name + // "HD Low" → "HD", "HD High" → "HD" $name = (string) preg_replace('/\b(HD|FHD|UHD|SD)\s*(Low|High)\b/iu', '$1', $name); } diff --git a/plugin.json b/plugin.json index 880c03a..ab149e8 100644 --- a/plugin.json +++ b/plugin.json @@ -100,17 +100,24 @@ }, { "id": "normalize_strip_provider_info", - "label": "Strip provider-specific info (Cable, Sat, etc.)", + "label": "Strip provider/transport terms from channel names", "type": "boolean", "default": true, - "helper_text": "Remove provider identifiers like Cable, Sat, Kabel that are not part of the channel name." + "helper_text": "Remove provider or transport identifiers (e.g. Cable, IPTV) that are not part of the channel name. Configure the terms to strip in the field below." + }, + { + "id": "normalize_provider_terms", + "label": "Provider/transport terms to strip (one per line)", + "type": "textarea", + "default": "Cable\nIPTV\nTerrestrial\nDVB-T\nDVB-T2\nDVB-C\nDVB-S\nDVB-S2", + "helper_text": "Terms matched as whole words (case-insensitive) and removed from channel names. Add provider-specific terms for your setup, e.g. Sat, Kabel, Astra — but check first that they don't clash with actual channel names." }, { "id": "normalize_strip_quality_extras", - "label": "Strip extra quality descriptors (Low, FHD+, etc.)", + "label": "Strip extra quality descriptors (Low, High, etc.)", "type": "boolean", "default": true, - "helper_text": "Remove extra quality descriptors like Low, Plus (when after a quality tag), FHD+." + "helper_text": "Remove Low/High when appended to a quality tag (e.g. HD Low → HD, UHD High → UHD)." }, { "id": "normalize_custom_patterns",