|
| 1 | +<?php |
| 2 | +/** |
| 3 | + * Fix remaining ~72 TV Episodes that TMDB couldn't match by IMDb ID. |
| 4 | + * Matches by episode title against TMDB season/episode data. |
| 5 | + * |
| 6 | + * Usage: php artisan tinker scripts/fix-remaining-episodes.php |
| 7 | + * OR: cd /home/dotmavriq/Code/TEAL && php artisan tinker < scripts/fix-remaining-episodes.php |
| 8 | + */ |
| 9 | + |
| 10 | +use App\Models\Movie; |
| 11 | +use App\Services\TmdbService; |
| 12 | +use Illuminate\Support\Str; |
| 13 | + |
| 14 | +$tmdb = app(TmdbService::class); |
| 15 | + |
| 16 | +// Shows with known TMDB IDs and the seasons we need |
| 17 | +$shows = [ |
| 18 | + ['name' => 'Cardinal', 'tmdb_id' => 67743, 'seasons' => [4]], |
| 19 | + ['name' => 'Glória', 'tmdb_id' => 109369, 'seasons' => [1]], |
| 20 | + ['name' => 'Reyka', 'tmdb_id' => 132629, 'seasons' => [1]], |
| 21 | + ['name' => 'Our Girl', 'tmdb_id' => 61517, 'seasons' => [3]], |
| 22 | + ['name' => 'Älskade Samir', 'tmdb_id' => 289318, 'seasons' => [1]], |
| 23 | + ['name' => 'Once Upon a Time in Londongrad', 'tmdb_id' => 203226, 'seasons' => [1]], |
| 24 | + ['name' => 'Black Market: Dispatches', 'tmdb_id' => 67583, 'seasons' => [1]], |
| 25 | + ['name' => 'The Power of Nightmares', 'tmdb_id' => 6132, 'seasons' => [1]], |
| 26 | + ['name' => 'Evil Con Carne', 'tmdb_id' => 4246, 'seasons' => [1, 2]], |
| 27 | + ['name' => 'NileCity 105.6', 'tmdb_id' => 15232, 'seasons' => [1]], |
| 28 | + ['name' => 'Uppdrag granskning', 'tmdb_id' => 5848, 'seasons' => [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]], |
| 29 | +]; |
| 30 | + |
| 31 | +$totalFixed = 0; |
| 32 | +$totalSkipped = 0; |
| 33 | + |
| 34 | +foreach ($shows as $show) { |
| 35 | + $showName = $show['name']; |
| 36 | + $tmdbId = $show['tmdb_id']; |
| 37 | + |
| 38 | + // Find our unmatched episodes for this show |
| 39 | + $episodes = Movie::where('title_type', 'TV Episode') |
| 40 | + ->where(function ($q) use ($showName) { |
| 41 | + $q->where('show_name', $showName) |
| 42 | + ->orWhere('show_name', 'like', $showName . '%') |
| 43 | + ->orWhere('primary_title', 'like', '%' . $showName . '%') |
| 44 | + ->orWhere('primary_title', 'like', '%' . Str::ascii($showName) . '%'); |
| 45 | + }) |
| 46 | + ->where(function ($q) { |
| 47 | + $q->whereNull('season_number') |
| 48 | + ->orWhereNull('episode_number'); |
| 49 | + }) |
| 50 | + ->get(); |
| 51 | + |
| 52 | + if ($episodes->isEmpty()) { |
| 53 | + echo "[$showName] No unmatched episodes found in DB, skipping.\n"; |
| 54 | + continue; |
| 55 | + } |
| 56 | + |
| 57 | + echo "[$showName] Found {$episodes->count()} unmatched episodes in DB.\n"; |
| 58 | + |
| 59 | + // Fetch all TMDB episodes for relevant seasons |
| 60 | + $tmdbEpisodes = []; |
| 61 | + foreach ($show['seasons'] as $seasonNum) { |
| 62 | + try { |
| 63 | + $seasonData = $tmdb->fetchTVSeasonEpisodes($tmdbId, $seasonNum); |
| 64 | + if (!empty($seasonData['episodes'])) { |
| 65 | + foreach ($seasonData['episodes'] as $ep) { |
| 66 | + $tmdbEpisodes[] = [ |
| 67 | + 'name' => $ep['name'] ?? '', |
| 68 | + 'season' => $seasonNum, |
| 69 | + 'episode' => $ep['episode_number'] ?? null, |
| 70 | + ]; |
| 71 | + } |
| 72 | + } |
| 73 | + usleep(300000); // rate limit |
| 74 | + } catch (\Exception $e) { |
| 75 | + echo " Warning: Could not fetch S{$seasonNum}: {$e->getMessage()}\n"; |
| 76 | + } |
| 77 | + } |
| 78 | + |
| 79 | + if (empty($tmdbEpisodes)) { |
| 80 | + echo " No TMDB episodes fetched, skipping.\n"; |
| 81 | + continue; |
| 82 | + } |
| 83 | + |
| 84 | + echo " Fetched " . count($tmdbEpisodes) . " TMDB episodes across " . count($show['seasons']) . " season(s).\n"; |
| 85 | + |
| 86 | + $fixed = 0; |
| 87 | + foreach ($episodes as $ep) { |
| 88 | + $ourTitle = trim($ep->primary_title); |
| 89 | + $matched = null; |
| 90 | + |
| 91 | + // Strategy 1: Exact title match |
| 92 | + foreach ($tmdbEpisodes as $te) { |
| 93 | + if (strcasecmp($ourTitle, $te['name']) === 0) { |
| 94 | + $matched = $te; |
| 95 | + break; |
| 96 | + } |
| 97 | + } |
| 98 | + |
| 99 | + // Strategy 2: Our title contains TMDB title or vice versa |
| 100 | + if (!$matched) { |
| 101 | + foreach ($tmdbEpisodes as $te) { |
| 102 | + if (empty($te['name'])) continue; |
| 103 | + if (Str::contains(strtolower($ourTitle), strtolower($te['name'])) || |
| 104 | + Str::contains(strtolower($te['name']), strtolower($ourTitle))) { |
| 105 | + $matched = $te; |
| 106 | + break; |
| 107 | + } |
| 108 | + } |
| 109 | + } |
| 110 | + |
| 111 | + // Strategy 3: Parse "Episode #X.Y" format from our title |
| 112 | + if (!$matched && preg_match('/Episode\s*#?(\d+)\.(\d+)/i', $ourTitle, $m)) { |
| 113 | + $parsedSeason = (int)$m[1]; |
| 114 | + $parsedEp = (int)$m[2]; |
| 115 | + foreach ($tmdbEpisodes as $te) { |
| 116 | + if ($te['season'] === $parsedSeason && $te['episode'] === $parsedEp) { |
| 117 | + $matched = $te; |
| 118 | + break; |
| 119 | + } |
| 120 | + } |
| 121 | + } |
| 122 | + |
| 123 | + // Strategy 4: Levenshtein distance (fuzzy match, threshold 3) |
| 124 | + if (!$matched) { |
| 125 | + $bestDist = 999; |
| 126 | + $bestMatch = null; |
| 127 | + foreach ($tmdbEpisodes as $te) { |
| 128 | + if (empty($te['name'])) continue; |
| 129 | + $dist = levenshtein(strtolower($ourTitle), strtolower($te['name'])); |
| 130 | + if ($dist < $bestDist && $dist <= 3) { |
| 131 | + $bestDist = $dist; |
| 132 | + $bestMatch = $te; |
| 133 | + } |
| 134 | + } |
| 135 | + if ($bestMatch) { |
| 136 | + $matched = $bestMatch; |
| 137 | + } |
| 138 | + } |
| 139 | + |
| 140 | + if ($matched) { |
| 141 | + $ep->season_number = $matched['season']; |
| 142 | + $ep->episode_number = $matched['episode']; |
| 143 | + if (empty($ep->show_name)) { |
| 144 | + $ep->show_name = $showName; |
| 145 | + } |
| 146 | + $ep->save(); |
| 147 | + $fixed++; |
| 148 | + echo " FIXED: \"{$ourTitle}\" -> S{$matched['season']}E{$matched['episode']}\n"; |
| 149 | + } else { |
| 150 | + $totalSkipped++; |
| 151 | + echo " SKIP: \"{$ourTitle}\" - no TMDB match\n"; |
| 152 | + } |
| 153 | + } |
| 154 | + |
| 155 | + $totalFixed += $fixed; |
| 156 | + echo " => Fixed {$fixed}/{$episodes->count()} episodes.\n\n"; |
| 157 | +} |
| 158 | + |
| 159 | +// Also fix episodes with no show_name at all |
| 160 | +echo "--- Checking for episodes with NULL show_name ---\n"; |
| 161 | +$noShowName = Movie::where('title_type', 'TV Episode') |
| 162 | + ->whereNull('show_name') |
| 163 | + ->where(function ($q) { |
| 164 | + $q->whereNull('season_number') |
| 165 | + ->orWhereNull('episode_number'); |
| 166 | + }) |
| 167 | + ->get(); |
| 168 | + |
| 169 | +if ($noShowName->isNotEmpty()) { |
| 170 | + echo "Found {$noShowName->count()} episodes with no show_name:\n"; |
| 171 | + foreach ($noShowName as $ep) { |
| 172 | + echo " ID={$ep->id} \"{$ep->primary_title}\" (IMDb: {$ep->imdb_id})\n"; |
| 173 | + } |
| 174 | +} else { |
| 175 | + echo "No orphan episodes with NULL show_name remain.\n"; |
| 176 | +} |
| 177 | + |
| 178 | +echo "\n=== SUMMARY ===\n"; |
| 179 | +echo "Total fixed: {$totalFixed}\n"; |
| 180 | +echo "Total skipped: {$totalSkipped}\n"; |
0 commit comments