Passed
Push — master ( 747121...cd5352 )
by Darko
11:38 queued 01:03
created

RefreshAnimeData::extractTitleFromSearchname()   B

Complexity

Conditions 8
Paths 13

Size

Total Lines 54
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 26
c 1
b 0
f 0
dl 0
loc 54
rs 8.4444
cc 8
nc 13
nop 1

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace App\Console\Commands;
4
5
use App\Models\AnidbInfo;
6
use App\Models\Category;
7
use App\Models\Release;
8
use Blacklight\PopulateAniList;
9
use Illuminate\Console\Command;
10
use Illuminate\Support\Facades\DB;
11
12
class RefreshAnimeData extends Command
13
{
14
    /**
15
     * The name and signature of the console command.
16
     *
17
     * @var string
18
     */
19
    protected $signature = 'anime:refresh
20
                            {--limit=0 : Maximum number of releases to process (0 = all)}
21
                            {--chunk=100 : Process releases in chunks of this size}
22
                            {--missing-only : Only refresh releases missing AniList data (no anilist_id)}
23
                            {--force : Force refresh even if data exists}';
24
25
    /**
26
     * The console command description.
27
     *
28
     * @var string
29
     */
30
    protected $description = 'Fetch and refresh AniList data for existing anime releases in TV->Anime category by matching release searchname';
31
32
    /**
33
     * Execute the console command.
34
     */
35
    public function handle(): int
36
    {
37
        $limit = (int) $this->option('limit');
38
        $chunkSize = (int) $this->option('chunk');
39
        $missingOnly = $this->option('missing-only');
40
        $force = $this->option('force');
41
42
        $this->info('Starting AniList data refresh for anime releases...');
43
        $this->info('Matching releases by searchname to AniList API...');
44
        $this->newLine();
45
46
        // Build query for releases in TV_ANIME category
47
        $query = Release::query()
48
            ->select(['releases.id', 'releases.anidbid', 'releases.searchname'])
49
            ->where('categories_id', Category::TV_ANIME);
50
51
        // If missing-only, only get releases without anilist_id
52
        if ($missingOnly) {
53
            $query->leftJoin('anidb_info as ai', 'ai.anidbid', '=', 'releases.anidbid')
54
                ->whereNull('ai.anilist_id');
55
        }
56
57
        // Get releases (not distinct anidbids, since we're matching by searchname)
58
        $releases = $query->orderBy('releases.id')
0 ignored issues
show
Bug introduced by
'releases.id' of type string is incompatible with the type Closure|Illuminate\Datab...\Database\Query\Builder expected by parameter $column of Illuminate\Database\Query\Builder::orderBy(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

58
        $releases = $query->orderBy(/** @scrutinizer ignore-type */ 'releases.id')
Loading history...
59
            ->get();
60
61
        $totalCount = $releases->count();
62
63
        if ($totalCount === 0) {
64
            $this->warn('No anime releases found to process.');
65
            return self::SUCCESS;
66
        }
67
68
        $this->info("Found {$totalCount} anime releases to process.");
69
        
70
        if ($limit > 0) {
71
            $releases = $releases->take($limit);
72
            $totalCount = $releases->count();
73
            $this->info("Processing {$totalCount} releases (limited).");
74
        }
75
76
        $this->newLine();
77
78
        $populateAniList = new PopulateAniList;
79
        $processed = 0;
80
        $successful = 0;
81
        $failed = 0;
82
        $skipped = 0;
83
        $notFound = 0;
84
85
        // Process in chunks
86
        $chunks = $releases->chunk($chunkSize);
87
        $progressBar = $this->output->createProgressBar($totalCount);
88
        $progressBar->setFormat(' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s% -- %message%');
89
        $progressBar->setMessage('Starting...');
90
        $progressBar->start();
91
92
        foreach ($chunks as $chunk) {
93
            foreach ($chunk as $release) {
94
                $searchname = $release->searchname ?? '';
95
                $progressBar->setMessage("Processing: " . substr($searchname, 0, 50) . "...");
96
97
                try {
98
                    // Extract clean title from searchname
99
                    $titleData = $this->extractTitleFromSearchname($searchname);
100
                    
101
                    if (empty($titleData) || empty($titleData['title'])) {
102
                        $notFound++;
103
                        $processed++;
104
                        $progressBar->advance();
105
                        continue;
106
                    }
107
108
                    $cleanTitle = $titleData['title'];
109
110
                    // Check if we should skip (if not forcing and data exists)
111
                    if (! $force && ! $missingOnly) {
112
                        // Check if release already has complete AniList data
113
                        if ($release->anidbid > 0) {
114
                            $anidbInfo = DB::table('anidb_info')
115
                                ->where('anidbid', $release->anidbid)
116
                                ->whereNotNull('anilist_id')
117
                                ->whereNotNull('country')
118
                                ->whereNotNull('media_type')
119
                                ->first();
120
121
                            if ($anidbInfo) {
122
                                $skipped++;
123
                                $processed++;
124
                                $progressBar->advance();
125
                                continue;
126
                            }
127
                        }
128
                    }
129
130
                    // Search AniList for this title
131
                    $searchResults = $populateAniList->searchAnime($cleanTitle, 1);
132
                    
133
                    if (! $searchResults || empty($searchResults)) {
134
                        // Try with spaces replaced for broader matching
135
                        $altTitle = preg_replace('/\s+/', ' ', $cleanTitle);
136
                        if ($altTitle !== $cleanTitle) {
137
                            $searchResults = $populateAniList->searchAnime($altTitle, 1);
138
                        }
139
                    }
140
141
                    if (! $searchResults || empty($searchResults)) {
142
                        $notFound++;
143
                        $processed++;
144
                        $progressBar->advance();
145
                        continue;
146
                    }
147
148
                    $anilistData = $searchResults[0];
149
                    $anilistId = $anilistData['id'] ?? null;
150
151
                    if (! $anilistId) {
152
                        $notFound++;
153
                        $processed++;
154
                        $progressBar->advance();
155
                        continue;
156
                    }
157
158
                    // Fetch full data from AniList and insert/update
159
                    // This will create/update anidb_info entry using anilist_id as anidbid if needed
160
                    $populateAniList->populateTable('info', $anilistId);
161
162
                    // Get the anidbid that was created/updated (it uses anilist_id as anidbid)
163
                    $anidbid = AnidbInfo::query()
164
                        ->where('anilist_id', $anilistId)
165
                        ->value('anidbid');
166
167
                    if (! $anidbid) {
168
                        // Fallback: use anilist_id as anidbid
169
                        $anidbid = (int) $anilistId;
170
                    }
171
172
                    // Update release with the anidbid
173
                    Release::query()
174
                        ->where('id', $release->id)
175
                        ->update(['anidbid' => $anidbid]);
176
177
                    $successful++;
178
                } catch (\Exception $e) {
179
                    $failed++;
180
                    if ($this->getOutput()->isVerbose()) {
181
                        $this->newLine();
182
                        $this->error("Error processing release ID {$release->id}: " . $e->getMessage());
183
                    }
184
                }
185
186
                $processed++;
187
                $progressBar->advance();
188
            }
189
        }
190
191
        $progressBar->setMessage('Complete!');
192
        $progressBar->finish();
193
        $this->newLine(2);
194
195
        // Summary
196
        $this->info('Summary:');
197
        $this->table(
198
            ['Status', 'Count'],
199
            [
200
                ['Total Processed', $processed],
201
                ['Successful', $successful],
202
                ['Failed', $failed],
203
                ['Not Found', $notFound],
204
                ['Skipped', $skipped],
205
            ]
206
        );
207
208
        return self::SUCCESS;
209
    }
210
211
    /**
212
     * Extract clean anime title from release searchname.
213
     * Similar to extractTitleEpisode in AniDB.php but simplified.
214
     *
215
     * @return array{title: string}|array{}
216
     */
217
    private function extractTitleFromSearchname(string $searchname): array
218
    {
219
        if (empty($searchname)) {
220
            return [];
221
        }
222
223
        // Normalize common separators
224
        $s = str_replace(['_', '.'], ' ', $searchname);
225
        $s = preg_replace('/\s+/', ' ', $s);
226
        $s = trim($s);
227
228
        // Strip leading group tags like [Group]
229
        $s = preg_replace('/^(?:\[[^\]]+\]\s*)+/', '', $s);
230
        $s = trim($s);
231
232
        // Remove language codes and tags
233
        $s = preg_replace('/\[(?:ENG|JAP|JPN|SUB|DUB|MULTI|RAW|HARDSUB|SOFTSUB|HARDDUB|SOFTDUB|ITA|SPA|FRE|GER|RUS|CHI|KOR)\]/i', ' ', $s);
234
        $s = preg_replace('/\((?:ENG|JAP|JPN|SUB|DUB|MULTI|RAW|HARDSUB|SOFTSUB|HARDDUB|SOFTDUB|ITA|SPA|FRE|GER|RUS|CHI|KOR)\)/i', ' ', $s);
235
        
236
        // Extract title by removing episode patterns
237
        $title = '';
238
239
        // Try to extract title by removing episode patterns
240
        // 1) Look for " - NNN" and extract title before it
241
        if (preg_match('/\s-\s*(\d{1,3})\b/', $s, $m, PREG_OFFSET_CAPTURE)) {
242
            $title = substr($s, 0, (int) $m[0][1]);
243
        }
244
        // 2) If not found, look for " E0*NNN" or " Ep NNN"
245
        elseif (preg_match('/\sE(?:p(?:isode)?)?\s*0*(\d{1,3})\b/i', $s, $m, PREG_OFFSET_CAPTURE)) {
246
            $title = substr($s, 0, (int) $m[0][1]);
247
        }
248
        // 3) Look for " S01E01" or " S1E1" pattern
249
        elseif (preg_match('/\sS\d+E\d+/i', $s, $m, PREG_OFFSET_CAPTURE)) {
250
            $title = substr($s, 0, (int) $m[0][1]);
251
        }
252
        // 4) Keywords Movie/OVA/Complete Series
253
        elseif (preg_match('/\b(Movie|OVA|Complete Series|Complete|Full Series)\b/i', $s, $m, PREG_OFFSET_CAPTURE)) {
254
            $title = substr($s, 0, (int) $m[0][1]);
255
        }
256
        // 5) BD/resolution releases: pick title before next bracket token
257
        elseif (preg_match('/\[(?:BD|BDRip|BluRay|Blu-Ray|\d{3,4}[ipx]|HEVC|x264|x265|H264|H265)\]/i', $s, $m, PREG_OFFSET_CAPTURE)) {
258
            $title = substr($s, 0, (int) $m[0][1]);
259
        } else {
260
            // No episode pattern found, use the whole string as title
261
            $title = $s;
262
        }
263
264
        $title = $this->cleanTitle($title);
265
266
        if ($title === '') {
267
            return [];
268
        }
269
270
        return ['title' => $title];
271
    }
272
273
    /**
274
     * Strip stray separators, language codes, episode numbers, and other release tags from title.
275
     */
276
    private function cleanTitle(string $title): string
277
    {
278
        // Remove all bracketed tags (language, quality, etc.)
279
        $title = preg_replace('/\[[^\]]+\]/', ' ', $title);
280
        
281
        // Remove all parenthesized tags
282
        $title = preg_replace('/\([^)]+\)/', ' ', $title);
283
        
284
        // Remove language codes (standalone or with separators)
285
        $title = preg_replace('/\b(ENG|JAP|JPN|SUB|DUB|MULTI|RAW|HARDSUB|SOFTSUB|HARDDUB|SOFTDUB|ITA|SPA|FRE|GER|RUS|CHI|KOR)\b/i', ' ', $title);
286
        
287
        // Remove episode patterns
288
        $title = preg_replace('/\s*-\s*\d{1,4}\s*$/i', '', $title);
289
        $title = preg_replace('/\s*-\s*$/i', '', $title);
290
        $title = preg_replace('/\s+E(?:p(?:isode)?)?\s*0*\d{1,4}\s*$/i', '', $title);
291
        $title = preg_replace('/\s+S\d+E\d+\s*$/i', '', $title);
292
        
293
        // Remove quality/resolution tags
294
        $title = preg_replace('/\b(480p|720p|1080p|2160p|4K|BD|BDRip|BluRay|Blu-Ray|HEVC|x264|x265|H264|H265|WEB|WEBRip|DVDRip|TVRip)\b/i', ' ', $title);
295
        
296
        // Remove common release tags
297
        $title = preg_replace('/\b(PROPER|REPACK|RIP|ISO|CRACK|BETA|ALPHA|FINAL|COMPLETE|FULL)\b/i', ' ', $title);
298
        
299
        // Remove volume/chapter markers
300
        $title = preg_replace('/\s+Vol\.?\s*\d*\s*$/i', '', $title);
301
        $title = preg_replace('/\s+Ch\.?\s*\d*\s*$/i', '', $title);
302
        
303
        // Remove trailing dashes and separators
304
        $title = preg_replace('/\s*[-_]\s*$/', '', $title);
305
        
306
        // Normalize whitespace
307
        $title = preg_replace('/\s+/', ' ', $title);
308
        
309
        return trim($title);
310
    }
311
}
312
313