From 160b8ad2662589dfe1c3f6ec55e44708664d60cb Mon Sep 17 00:00:00 2001 From: Serph91P Date: Wed, 15 Apr 2026 09:22:25 +0200 Subject: [PATCH 1/2] fix: improve logo URL resolution across nested subfolders - Refactor resolveLogoUrl() with new resolveFromIndex() method that searches ALL subfolders via basename lookup instead of only hd/ and root - Fix slugify() to strip HDraw, FHD Low, and IPTV transport terms (Cable, Sat, Terrestrial, DVB*, IPTV, OTT, FTA, Stream, Linear) - Prevent numeric shortening (dazn-1 no longer shortened to dazn) - Prevent quality suffix shortening (sky-sport-1-hd kept intact) - Sky Sport HD channels now correctly resolve to sky-sport/hd/ paths - DAZN HD variants resolve to hd/, FHD/HDraw fall back to SD --- .gitignore | 2 + Plugin.php | 117 +++++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 98 insertions(+), 21 deletions(-) diff --git a/.gitignore b/.gitignore index 14a7cdf..f8cecae 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,5 @@ # PHP deps (plugins ship no Composer deps) /vendor/ composer.lock + +*.log diff --git a/Plugin.php b/Plugin.php index e44853f..d2fca8c 100644 --- a/Plugin.php +++ b/Plugin.php @@ -335,14 +335,11 @@ private function processPlaylist(int $playlistId, PluginExecutionContext $contex /** * Attempt to resolve a CDN logo URL for the given channel name. * - * When an index is available (fetched once per run from the GitHub Contents - * API), resolution is a pure O(1) array lookup — no HTTP requests per channel. + * When an index is available, performs a comprehensive filename-based search + * across ALL subfolders (hd/, sky-sport/hd/, custom/, etc.), preferring + * HD subfolders for HD-hinted channels. * Falls back to sequential CDN HEAD checks only when the index is unavailable. * - * Tries candidate slugs with and without quality tokens (when present), then - * probes country root and subfolders (for example `hd/`) in a quality-aware - * order so channels like "Das Erste HD" can resolve to HD-specific assets. - * * @param array $index Filename → true map; empty array triggers HEAD fallback. */ private function resolveLogoUrl(string $channelName, string $countryCode, string $countryFolder, array $index): ?string @@ -358,25 +355,92 @@ private function resolveLogoUrl(string $channelName, string $countryCode, string $filenames = $this->buildFilenamesForSlugs($slugs, $countryCode); + // When an index is available, search all subfolders by basename for best match. + if ($index !== []) { + $result = $this->resolveFromIndex($filenames, $channelName, $countryFolder, $index); + + if ($result !== null) { + return $result; + } + + return $this->compactIndexMatch($slugs, $countryCode, $countryFolder, $channelName, $index); + } + + // HEAD fallback when index is unavailable. foreach ($this->preferredQualityFolders($channelName) as $folder) { foreach ($filenames as $filename) { $relativePath = $folder === '' ? $filename : "{$folder}/{$filename}"; $url = $this->cdnBase."/{$countryFolder}/{$relativePath}"; - $exists = $index !== [] - ? isset($index[strtolower($relativePath)]) - : $this->urlExists($url); - - if ($exists) { + if ($this->urlExists($url)) { return $url; } } } - // Compact matching fallback — strips all hyphens from both sides so that - // minor hyphenation differences (e.g. "sport1" vs "sport-1") still match. - if ($index !== []) { - return $this->compactIndexMatch($slugs, $countryCode, $countryFolder, $channelName, $index); + return null; + } + + /** + * Resolve a logo URL by searching the pre-fetched index across ALL subfolders. + * + * Builds a basename lookup from the index so that files in nested subfolders + * like sky-sport/hd/ or custom/hd/ are found regardless of folder structure. + * When multiple paths match the same filename, prefers HD subfolders for + * HD-hinted channels. + * + * @param array $filenames + * @param array $index + */ + private function resolveFromIndex(array $filenames, string $channelName, string $countryFolder, array $index): ?string + { + $hdPreferred = (bool) preg_match('/\b(hd|fhd|uhd|4k|8k|1080[pi]|720p)\b/iu', $channelName); + + // Build a basename → [relativePaths…] lookup for efficient searching. + $byBasename = []; + + foreach ($index as $relativePath => $_) { + $bn = strtolower(basename($relativePath)); + $byBasename[$bn][] = $relativePath; + } + + foreach ($filenames as $filename) { + $lowFilename = strtolower($filename); + + if (! isset($byBasename[$lowFilename])) { + continue; + } + + $paths = $byBasename[$lowFilename]; + + // Single match — return immediately. + if (count($paths) === 1) { + return $this->cdnBase."/{$countryFolder}/{$paths[0]}"; + } + + // Multiple matches — pick the best based on quality preference. + $hdMatch = null; + $rootMatch = null; + + foreach ($paths as $path) { + $inHd = str_contains($path, '/hd/') || str_starts_with($path, 'hd/'); + + if ($inHd) { + $hdMatch ??= $path; + } elseif (! str_contains($path, '/')) { + $rootMatch ??= $path; + } + } + + if ($hdPreferred && $hdMatch !== null) { + return $this->cdnBase."/{$countryFolder}/{$hdMatch}"; + } + + if ($rootMatch !== null) { + return $this->cdnBase."/{$countryFolder}/{$rootMatch}"; + } + + return $this->cdnBase."/{$countryFolder}/{$paths[0]}"; } return null; @@ -397,7 +461,9 @@ private function buildFilenamesForSlugs(array $slugs, string $countryCode): arra $filenames[] = "{$slug}.png"; $parts = explode('-', $slug); - if (count($parts) > 1) { + $lastPart = end($parts); + $qualitySuffixes = ['hd', 'fhd', 'uhd', 'sd', '4k', '8k']; + if (count($parts) > 1 && ! ctype_digit($lastPart) && ! in_array($lastPart, $qualitySuffixes, true)) { $shortened = implode('-', array_slice($parts, 0, -1)); if ($shortened !== '') { $filenames[] = "{$shortened}-{$countryCode}.png"; @@ -570,10 +636,14 @@ private function slugify(string $name, bool $stripQualityTags = true): string $name = mb_strtolower($name, 'UTF-8'); if ($stripQualityTags) { - // Strip quality suffixes (hd, fhd, 4k, etc.) - $name = preg_replace('/\b(hd|fhd|uhd|4k|8k|sd|1080[pi]|720p|hevc|h\.?264|h\.?265)\b/iu', '', $name) ?? $name; + // Strip quality suffixes and optional trailing modifiers (raw, low, high) + // e.g. "HDraw" → "", "FHD Low" → "", "HEVC" → "" + $name = preg_replace('/\b(hd|fhd|uhd|4k|8k|sd|1080[pi]|720p|hevc|h\.?264|h\.?265)\s*(raw|low|high)?\b/iu', '', $name) ?? $name; } + // Strip common IPTV transport / source terms (always, regardless of quality tag stripping) + $name = preg_replace('/\b(cable|sat(?:ellite)?|terrestrial|dvb[tcsh]?|iptv|ott|fta|stream|linear)\b/iu', '', $name) ?? $name; + // Remove content inside any bracket type $name = preg_replace('/[\(\[\{][^\)\]\}]*[\)\]\}]/', '', $name) ?? $name; @@ -731,19 +801,24 @@ 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 terms (configurable list, one term per line in settings) + // 3. Strip common IPTV transport / source terms that are never part of a channel name + if ($config['strip_provider_info']) { + $name = (string) preg_replace('/\b(Cable|Sat|Satellite|Terrestrial|DVB[TCSH]?|IPTV|OTT|FTA|Stream|Linear)\b/iu', '', $name); + } + + // 4. Strip user-configured provider terms (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 + // 5. Strip extra quality descriptors that follow a quality tag if ($config['strip_quality_extras']) { // "HD Low" → "HD", "HD High" → "HD" $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) + // 6. 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) { From 2a847fc6f81447eb5a89711446312e01a700eadf Mon Sep 17 00:00:00 2001 From: Serph91P Date: Thu, 16 Apr 2026 14:18:09 +0200 Subject: [PATCH 2/2] fix: remove per-channel activity spam, support nested hd/ subfolders in compactIndexMatch --- Plugin.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Plugin.php b/Plugin.php index d2fca8c..b5ff378 100644 --- a/Plugin.php +++ b/Plugin.php @@ -293,14 +293,12 @@ private function processPlaylist(int $playlistId, PluginExecutionContext $contex if ($logoUrl !== null) { $matched++; - $context->info("Matched: \"{$displayName}\" → {$logoUrl}"); if (! $isDryRun) { Channel::where('id', $channel->id)->update(['logo' => $logoUrl]); } } else { $unmatched[] = $displayName; - $context->info("Unmatched: \"{$displayName}\""); } if (($i + 1) % 20 === 0) { @@ -519,7 +517,10 @@ private function compactIndexMatch(array $slugs, string $countryCode, string $co $folder = dirname($relativePath); $folder = $folder === '.' ? '' : $folder; - if ($folder !== $preferredFolder) { + $isHdPath = $folder === 'hd' || str_ends_with($folder, '/hd'); + $wantsHd = $preferredFolder === 'hd'; + + if ($wantsHd !== $isHdPath) { continue; }