diff --git a/app/Extensions/helper/helpers.php b/app/Extensions/helper/helpers.php index 0060ba6ed..13e8d4371 100644 --- a/app/Extensions/helper/helpers.php +++ b/app/Extensions/helper/helpers.php @@ -453,7 +453,7 @@ if (! function_exists('getReleaseCover')) { if (! empty($imdbid) && $imdbid > 0) { $coverType = 'movies'; - $coverId = str_pad($imdbid, 7, '0', STR_PAD_LEFT); + $coverId = str_pad($imdbid, 8, '0', STR_PAD_LEFT); } elseif (! empty($musicinfo_id)) { $coverType = 'music'; $coverId = $musicinfo_id; diff --git a/app/Services/MovieBrowseService.php b/app/Services/MovieBrowseService.php index 6d2ea3fc6..40bf99fd9 100644 --- a/app/Services/MovieBrowseService.php +++ b/app/Services/MovieBrowseService.php @@ -82,11 +82,11 @@ class MovieBrowseService $totalCount = Cache::get($countCacheKey); if ($totalCount === null) { - // Use LPAD to convert movieinfo.imdbid (int) to the zero-padded varchar format - // stored in releases.imdbid, so the index on r.imdbid can be used for the join. + // Normalize both sides so we match whether imdbid is stored with or without + // leading zeros (e.g. "0099348" vs "99348") in movieinfo or releases. $countSql = 'SELECT COUNT(DISTINCT m.imdbid) AS total ' .'FROM movieinfo m ' - .'INNER JOIN releases r ON r.imdbid = LPAD(m.imdbid, 7, \'0\') ' + .'INNER JOIN releases r ON LPAD(TRIM(r.imdbid), 8, \'0\') = LPAD(TRIM(m.imdbid), 8, \'0\') ' .'WHERE '.$baseWhere; $totalResult = DB::select($countSql); @@ -122,7 +122,7 @@ class MovieBrowseService .'FROM (' .'SELECT m.imdbid, MAX(r.postdate) AS latest_postdate, COUNT(r.id) AS total_releases ' .'FROM movieinfo m ' - .'INNER JOIN releases r ON r.imdbid = LPAD(m.imdbid, 7, \'0\') ' + .'INNER JOIN releases r ON LPAD(TRIM(r.imdbid), 8, \'0\') = LPAD(TRIM(m.imdbid), 8, \'0\') ' .'WHERE '.$baseWhere.' ' .'GROUP BY m.imdbid'.$innerExtraGroupBy.' ' ."ORDER BY {$innerOrderBy} {$order[1]} " // @phpstan-ignore offsetAccess.notFound @@ -141,18 +141,16 @@ class MovieBrowseService $movieImdbIds = $movies->pluck('imdbid')->toArray(); // Step 3: Get top 2 releases per movie using UNION ALL with LIMIT 2 per imdbid. - // Each subquery does an index lookup on imdbid and stops after 2 rows, - // avoiding the full-table-scan + ROW_NUMBER() materialization of millions of rows. - // IMDB IDs are quoted as strings with leading-zero padding to match the - // varchar format stored in releases.imdbid (e.g. '0099348'), preventing - // implicit type casts that would bypass index usage. + // Match releases by normalized 8-char imdbid so we find them whether stored + // with or without leading zeros (IMDb IDs are 7–8 digits). $unionParts = []; foreach ($movieImdbIds as $id) { - $quotedId = "'".str_pad((string) intval($id), 7, '0', STR_PAD_LEFT)."'"; + $paddedId = str_pad((string) (int) $id, 8, '0', STR_PAD_LEFT); + $quotedId = "'".$paddedId."'"; $unionParts[] = '(SELECT r.id, r.imdbid, r.guid, r.searchname, ' .'r.size, r.postdate, r.adddate, r.haspreview ' .'FROM releases r ' - ."WHERE r.imdbid = {$quotedId} " + .'WHERE LPAD(TRIM(r.imdbid), 8, \'0\') = '.$quotedId.' ' ."AND r.passwordstatus {$this->showPasswords} " .$catFilter .$whereExcluded @@ -163,15 +161,17 @@ class MovieBrowseService $releasesSql = implode(' UNION ALL ', $unionParts); $releases = DB::select($releasesSql); - // Group releases by imdbid for fast lookup + // Group releases by normalized imdbid so lookup works regardless of stored format $releasesByMovie = []; foreach ($releases as $release) { - $releasesByMovie[$release->imdbid][] = $release; + $normId = str_pad((string) (int) $release->imdbid, 8, '0', STR_PAD_LEFT); + $releasesByMovie[$normId][] = $release; } // Attach releases to each movie object foreach ($movies as $movie) { - $movie->releases = $releasesByMovie[$movie->imdbid] ?? []; // @phpstan-ignore assign.propertyReadOnly + $normMovieId = str_pad((string) (int) $movie->imdbid, 8, '0', STR_PAD_LEFT); + $movie->releases = $releasesByMovie[$normMovieId] ?? []; // @phpstan-ignore assign.propertyReadOnly } // Set total count on first item (matches existing pattern used by controllers) @@ -193,10 +193,11 @@ class MovieBrowseService public function getMovieReleases(string $imdbid, array $excludedCats = []): array { $whereExcluded = count($excludedCats) > 0 ? ' AND r.categories_id NOT IN ('.implode(',', $excludedCats).')' : ''; + $paddedId = "'".str_pad((string) (int) $imdbid, 8, '0', STR_PAD_LEFT)."'"; $sql = 'SELECT r.id, r.guid, r.searchname, r.size, r.postdate, r.adddate, r.haspreview ' .'FROM releases r ' - ."WHERE r.imdbid = '".str_pad((string) intval($imdbid), 7, '0', STR_PAD_LEFT)."' " + .'WHERE LPAD(TRIM(r.imdbid), 8, \'0\') = '.$paddedId.' ' ."AND r.passwordstatus {$this->showPasswords} " .$whereExcluded.' ' .'ORDER BY r.postdate DESC'; @@ -238,7 +239,18 @@ class MovieBrowseService $browseByArr = ['title', 'director', 'actors', 'genre', 'rating', 'year', 'imdb']; foreach ($browseByArr as $bb) { if (request()->has($bb) && ! empty(request()->input($bb))) { - $bbv = stripslashes(request()->input($bb)); + $bbv = request()->input($bb); + if (is_array($bbv)) { + continue; + } + $bbv = stripslashes((string) $bbv); + if ($bb === 'year') { + if (preg_match('/^(19|20)\d{2}$/', $bbv)) { + $browseBy .= ' AND m.year = '.escapeString($bbv); + } + + continue; + } if ($bb === 'rating') { $bbv .= '.'; } diff --git a/app/Services/TmdbClient.php b/app/Services/TmdbClient.php index eadc8b36d..2a1672040 100644 --- a/app/Services/TmdbClient.php +++ b/app/Services/TmdbClient.php @@ -356,7 +356,7 @@ class TmdbClient if (! empty($imdbId)) { // Format IMDB ID with tt prefix if it's numeric $imdbFormatted = is_numeric($imdbId) - ? 'tt'.str_pad((string) $imdbId, 7, '0', STR_PAD_LEFT) + ? 'tt'.str_pad((string) $imdbId, 8, '0', STR_PAD_LEFT) : (string) $imdbId; $show = $this->findTvByExternalId($imdbFormatted, 'imdb_id'); @@ -396,7 +396,7 @@ class TmdbClient $show = $this->findTvByExternalId((string) $id, 'tvdb_id'); } elseif ($source === 'imdb') { $imdbFormatted = is_numeric($id) - ? 'tt'.str_pad((string) $id, 7, '0', STR_PAD_LEFT) + ? 'tt'.str_pad((string) $id, 8, '0', STR_PAD_LEFT) : (string) $id; $show = $this->findTvByExternalId($imdbFormatted, 'imdb_id'); } @@ -417,7 +417,7 @@ class TmdbClient // Parse IMDB ID to numeric $imdbId = 0; if (! empty($externalIds['imdb_id'])) { - preg_match('/tt(?P\d{6,7})$/i', $externalIds['imdb_id'], $imdb); + preg_match('/tt(?P\d{6,8})$/i', $externalIds['imdb_id'], $imdb); $imdbId = (int) ($imdb['imdbid'] ?? 0); } diff --git a/app/Services/TraktService.php b/app/Services/TraktService.php index 335e701bc..c5d230555 100644 --- a/app/Services/TraktService.php +++ b/app/Services/TraktService.php @@ -233,7 +233,7 @@ class TraktService } return match ($idType) { - 'imdb' => is_numeric($id) ? 'tt'.str_pad((string) $id, 7, '0', STR_PAD_LEFT) : (string) $id, + 'imdb' => is_numeric($id) ? 'tt'.str_pad((string) $id, 8, '0', STR_PAD_LEFT) : (string) $id, 'trakt', 'tmdb', 'tvdb' => (string) $id, }; } diff --git a/app/Services/TvProcessing/Providers/TmdbProvider.php b/app/Services/TvProcessing/Providers/TmdbProvider.php index 70169776b..58078454b 100644 --- a/app/Services/TvProcessing/Providers/TmdbProvider.php +++ b/app/Services/TvProcessing/Providers/TmdbProvider.php @@ -494,7 +494,7 @@ class TmdbProvider extends AbstractTvProvider $imdbId = 0; $externalIds = TmdbClient::getArray($show, 'external_ids'); if (! empty($externalIds['imdb_id'])) { - preg_match('/tt(?P\d{6,7})$/i', $externalIds['imdb_id'], $imdb); + preg_match('/tt(?P\d{6,8})$/i', $externalIds['imdb_id'], $imdb); $imdbId = $imdb['imdbid'] ?? 0; } diff --git a/app/Services/TvProcessing/Providers/TraktProvider.php b/app/Services/TvProcessing/Providers/TraktProvider.php index 2ef73ea96..23809a7f1 100644 --- a/app/Services/TvProcessing/Providers/TraktProvider.php +++ b/app/Services/TvProcessing/Providers/TraktProvider.php @@ -383,7 +383,7 @@ class TraktProvider extends AbstractTvProvider */ public function formatShowInfo(mixed $show): array { - preg_match('/tt(?P\d{6,7})$/i', $show['ids']['imdb'], $imdb); + preg_match('/tt(?P\d{6,8})$/i', $show['ids']['imdb'], $imdb); $this->posterUrl = $show['images']['poster']['thumb'] ?? ''; $this->fanartUrl = $show['images']['fanart']['thumb'] ?? ''; $this->localizedTZ = $show['airs']['timezone'] ?? ''; @@ -435,7 +435,7 @@ class TraktProvider extends AbstractTvProvider // Try IMDB ID as fallback if (! empty($imdbId) && $imdbId > 0) { - $imdbFormatted = 'tt'.str_pad((string) $imdbId, 7, '0', STR_PAD_LEFT); + $imdbFormatted = 'tt'.str_pad((string) $imdbId, 8, '0', STR_PAD_LEFT); $result = $tvmazeClient->getShowBySiteID('imdb', $imdbFormatted); if ($result !== null && isset($result->id)) { return (int) $result->id; diff --git a/resources/views/series/viewseries.blade.php b/resources/views/series/viewseries.blade.php index ed2ffa40d..ddfe69198 100644 --- a/resources/views/series/viewseries.blade.php +++ b/resources/views/series/viewseries.blade.php @@ -176,7 +176,7 @@ @if(!empty($show['imdb']) && $show['imdb'] > 0) IMDb