Issues (867)

app/Http/Controllers/SeriesController.php (6 issues)

1
<?php
2
3
namespace App\Http\Controllers;
4
5
use App\Models\TvEpisode;
6
use App\Models\UserSerie;
7
use App\Models\Video;
8
use App\Services\Releases\ReleaseSearchService;
9
use Carbon\Carbon;
10
use Illuminate\Http\Request;
11
use Illuminate\Support\Arr;
12
13
class SeriesController extends BasePageController
14
{
15
    private ReleaseSearchService $releaseSearchService;
16
17
    public function __construct(ReleaseSearchService $releaseSearchService)
18
    {
19
        parent::__construct();
20
        $this->releaseSearchService = $releaseSearchService;
21
    }
22
23
    /**
24
     * @throws \Exception
25
     */
26
    public function index(Request $request, string $id = '')
27
    {
28
29
        if ($id && ctype_digit($id)) {
30
            $category = -1;
31
            if ($request->has('t') && ctype_digit($request->input('t'))) {
32
                $category = $request->input('t');
33
            }
34
35
            $catarray = [];
36
            $catarray[] = $category;
37
38
            $seriesLimit = (int) config('nntmux.series_view_limit', 200);
39
            $page = $request->has('page') && is_numeric($request->input('page')) ? (int) $request->input('page') : 1;
40
            $page = max($page, 1);
41
            $offset = $seriesLimit > 0 ? ($page - 1) * $seriesLimit : 0;
42
43
            $rel = $this->releaseSearchService->tvSearch(['id' => $id], '', '', '', $offset, $seriesLimit, '', $catarray, -1);
44
45
            $show = Video::getByVideoID($id);
46
47
            $nodata = '';
48
            $seasons = [];
49
            $myshows = null;
50
            $seriestitles = '';
51
            $seriessummary = '';
52
            $seriescountry = '';
53
54
            if (! $show) {
55
                $nodata = 'No video information for this series.';
56
            } elseif (! $rel) {
57
                $nodata = 'No releases for this series.';
58
            } else {
59
                $myshows = UserSerie::getShow($this->userdata->id, $show['id']);
60
61
                // Hydrate missing season/episode numbers if tv_episodes_id is set but series/episode missing or zero.
62
                $episodeMeta = collect();
63
                $episodeIds = collect($rel)->pluck('tv_episodes_id')->filter(fn ($v) => $v > 0)->unique()->values();
64
                if ($episodeIds->isNotEmpty()) {
65
                    $episodeMeta = TvEpisode::whereIn('id', $episodeIds)->get()->keyBy('id');
66
                }
67
68
                foreach ($rel as $r) {
69
                    if ($r->tv_episodes_id > 0 && $episodeMeta->has($r->tv_episodes_id)) {
70
                        $meta = $episodeMeta[$r->tv_episodes_id];
71
                        if (empty($r->series) || (int) $r->series === 0) {
72
                            $r->series = (int) $meta->series;
73
                        }
74
                        if (empty($r->episode) || (int) $r->episode === 0) {
75
                            $r->episode = (int) $meta->episode;
76
                        }
77
                        if ((! $meta->series || (int) $meta->series === 0) && ! empty($meta->firstaired)) {
78
                            if (empty($r->series) || (int) $r->series === 0) {
79
                                $r->series = (int) Carbon::parse($meta->firstaired)->format('Y');
80
                            }
81
                            if (empty($r->episode) || (int) $r->episode === 0) {
82
                                $r->episode = (int) Carbon::parse($meta->firstaired)->format('md');
83
                            }
84
                        }
85
                    } elseif (! empty($r->firstaired)) {
86
                        if (empty($r->series) || (int) $r->series === 0) {
87
                            $r->series = (int) Carbon::parse($r->firstaired)->format('Y');
88
                        }
89
                        if (empty($r->episode) || (int) $r->episode === 0) {
90
                            $r->episode = (int) Carbon::parse($r->firstaired)->format('md');
91
                        }
92
                    }
93
94
                    if ((empty($r->series) || (int) $r->series === 0 || empty($r->episode) || (int) $r->episode === 0) && ! empty($r->searchname)) {
95
                        $matched = false;
96
97
                        if (! $matched && preg_match('/\bS(\d{1,2})E(\d{1,3})\b/i', $r->searchname, $m)) {
98
                            if (empty($r->series) || (int) $r->series === 0) {
99
                                $r->series = (int) $m[1];
100
                            }
101
                            if (empty($r->episode) || (int) $r->episode === 0) {
102
                                $r->episode = (int) $m[2];
103
                            }
104
                            $matched = true;
105
                        }
106
107
                        if (! $matched && preg_match('/\b(\d{1,2})x(\d{1,3})\b/i', $r->searchname, $m)) {
108
                            if (empty($r->series) || (int) $r->series === 0) {
109
                                $r->series = (int) $m[1];
110
                            }
111
                            if (empty($r->episode) || (int) $r->episode === 0) {
112
                                $r->episode = (int) $m[2];
113
                            }
114
                            $matched = true;
115
                        }
116
117
                        if (! $matched && preg_match('/\bSeason[\s._-]*(\d{1,2})[\s._-]*Episode[\s._-]*(\d{1,3})\b/i', $r->searchname, $m)) {
118
                            if (empty($r->series) || (int) $r->series === 0) {
119
                                $r->series = (int) $m[1];
120
                            }
121
                            if (empty($r->episode) || (int) $r->episode === 0) {
122
                                $r->episode = (int) $m[2];
123
                            }
124
                            $matched = true;
125
                        }
126
127
                        if (! $matched && preg_match('/\b(\d)(0[1-9]|[1-9]\d)\b/', $r->searchname, $m)) {
128
                            if ((empty($r->series) || (int) $r->series === 0) && (empty($r->episode) || (int) $r->episode === 0)) {
129
                                $r->series = (int) $m[1];
130
                                $r->episode = (int) $m[2];
131
                                $matched = true;
132
                            }
133
                        }
134
135
                        if (! $matched && preg_match('/\b(\d{4})[._-](\d{2})[._-](\d{2})\b/', $r->searchname, $m)) {
136
                            if (empty($r->series) || (int) $r->series === 0) {
137
                                $r->series = (int) $m[1];
138
                            }
139
                            if (empty($r->episode) || (int) $r->episode === 0) {
140
                                $r->episode = (int) ($m[2].$m[3]);
141
                            }
142
                            $matched = true;
143
                        }
144
145
                        if (! $matched && preg_match('/\b(?:Part|Pt)[\s._-]*(\d{1,3})\b/i', $r->searchname, $m)) {
146
                            if (empty($r->episode) || (int) $r->episode === 0) {
147
                                $r->episode = (int) $m[1];
148
                                if (empty($r->series) || (int) $r->series === 0) {
149
                                    $r->series = 1;
150
                                }
151
                                $matched = true;
152
                            }
153
                        }
154
155
                        if (! $matched && (empty($r->episode) || (int) $r->episode === 0) && preg_match('/\bEp?[\s._-]*(\d{1,3})\b/i', $r->searchname, $m)) {
156
                            $r->episode = (int) $m[1];
157
                            if (empty($r->series) || (int) $r->series === 0) {
158
                                $r->series = 1;
159
                            }
160
                        }
161
                    }
162
                }
163
164
                // Sort releases by season, episode, date posted.
165
                $series = $episode = $posted = [];
166
                foreach ($rel as $rlk => $rlv) {
167
                    $series[$rlk] = $rlv->series;
168
                    $episode[$rlk] = $rlv->episode;
169
                    $posted[$rlk] = $rlv->postdate;
170
                }
171
                Arr::sort($series, [[$episode, false], [$posted, false], $rel]);
172
173
                $series = [];
174
                foreach ($rel as $r) {
175
                    $series[$r->series][$r->episode][] = $r;
176
                }
177
178
                $seasons = Arr::sortRecursive($series);
179
180
                // get series name(s), description, country and genre
181
                $seriestitlesArray = $seriessummaryArray = $seriescountryArray = [];
182
                $seriestitlesArray[] = $show['title'];
183
184
                if (! empty($show['summary'])) {
185
                    $seriessummaryArray[] = $show['summary'];
186
                }
187
188
                if (! empty($show['countries_id'])) {
189
                    $seriescountryArray[] = $show['countries_id'];
190
                }
191
192
                $seriestitles = implode('/', array_map('trim', $seriestitlesArray));
193
                $seriessummary = $seriessummaryArray ? array_shift($seriessummaryArray) : '';
194
                $seriescountry = $seriescountryArray ? array_shift($seriescountryArray) : '';
195
            }
196
197
            // Calculate statistics
198
            $episodeCount = 0;
199
            $seasonCount = count($seasons);
200
            $totalSeasonsAvailable = $seasonCount;
201
202
            // Get first and last aired dates from TV episodes
203
            $firstEpisodeAired = null;
204
            $lastEpisodeAired = null;
205
            $totalSeasonsAired = 0;
206
            $totalEpisodesAired = 0;
207
208
            if (! empty($show['id'])) {
209
                $episodeStats = \App\Models\TvEpisode::query()
210
                    ->where('videos_id', $show['id'])
211
                    ->whereNotNull('firstaired')
212
                    ->where('firstaired', '!=', '')
213
                    ->selectRaw('MIN(firstaired) as first_aired, MAX(firstaired) as last_aired, COUNT(DISTINCT series) as total_seasons, COUNT(*) as total_episodes')
214
                    ->first();
215
216
                if ($episodeStats) {
217
                    if (! empty($episodeStats->first_aired) && $episodeStats->first_aired != '0000-00-00') {
0 ignored issues
show
The property first_aired does not exist on App\Models\TvEpisode. Did you mean firstaired?
Loading history...
218
                        $firstEpisodeAired = \Carbon\Carbon::parse($episodeStats->first_aired);
219
                    }
220
                    if (! empty($episodeStats->last_aired) && $episodeStats->last_aired != '0000-00-00') {
0 ignored issues
show
The property last_aired does not seem to exist on App\Models\TvEpisode. Are you sure there is no database migration missing?

Checks if undeclared accessed properties appear in database migrations and if the creating migration is correct.

Loading history...
221
                        $lastEpisodeAired = \Carbon\Carbon::parse($episodeStats->last_aired);
222
                    }
223
                    $totalSeasonsAired = $episodeStats->total_seasons ?? 0;
0 ignored issues
show
The property total_seasons does not seem to exist on App\Models\TvEpisode. Are you sure there is no database migration missing?

Checks if undeclared accessed properties appear in database migrations and if the creating migration is correct.

Loading history...
224
                    $totalEpisodesAired = $episodeStats->total_episodes ?? 0;
0 ignored issues
show
The property total_episodes does not exist on App\Models\TvEpisode. Did you mean episode?
Loading history...
225
                }
226
            }
227
228
            foreach ($seasons as $seasonNum => $episodes) {
229
                $episodeCount += count($episodes);
230
            }
231
232
            $catid = $category !== -1 ? $category : '';
233
            $totalRows = ($rel && $rel->count() > 0) ? ($rel[0]->_totalrows ?? $rel->count()) : 0;
234
            $totalPages = $seriesLimit > 0 ? (int) ceil(max($totalRows, 1) / $seriesLimit) : 1;
235
236
            $this->viewData = array_merge($this->viewData, [
237
                'seasons' => $seasons,
238
                'show' => $show,
239
                'myshows' => $myshows,
240
                'seriestitles' => $seriestitles,
241
                'seriessummary' => $seriessummary,
242
                'seriescountry' => $seriescountry,
243
                'category' => $catid,
244
                'nodata' => $nodata,
245
                'episodeCount' => $episodeCount,
246
                'seasonCount' => $seasonCount,
247
                'firstEpisodeAired' => $firstEpisodeAired,
248
                'lastEpisodeAired' => $lastEpisodeAired,
249
                'totalSeasonsAvailable' => $totalSeasonsAvailable,
250
                'totalSeasonsAired' => $totalSeasonsAired,
251
                'totalEpisodesAired' => $totalEpisodesAired,
252
                'pagination' => [
253
                    'per_page' => $seriesLimit,
254
                    'current_page' => $page,
255
                    'total_pages' => $totalPages,
256
                    'total_rows' => $totalRows,
257
                ],
258
                'meta_title' => 'View TV Series',
259
                'meta_keywords' => 'view,series,tv,show,description,details',
260
                'meta_description' => 'View TV Series',
261
            ]);
262
263
            return view('series.viewseries', $this->viewData);
264
        } else {
265
            $letter = ($id && preg_match('/^(0-9|[A-Z])$/i', $id)) ? $id : '0-9';
266
267
            $showname = ($request->has('title') && ! empty($request->input('title'))) ? $request->input('title') : '';
268
269
            if ($showname !== '' && ! $id) {
270
                $letter = '';
271
            }
272
273
            $masterserieslist = Video::getSeriesList($this->userdata->id, $letter, $showname);
274
275
            $serieslist = [];
276
            foreach ($masterserieslist as $s) {
277
                if (preg_match('/^[0-9]/', $s['title'])) {
278
                    $thisrange = '0-9';
279
                } else {
280
                    preg_match('/([A-Z]).*/i', $s['title'], $hits);
281
                    $thisrange = strtoupper($hits[1]);
282
                }
283
                $serieslist[$thisrange][] = $s;
284
            }
285
            ksort($serieslist);
286
287
            $this->viewData = array_merge($this->viewData, [
288
                'serieslist' => $serieslist,
289
                'seriesrange' => range('A', 'Z'),
290
                'seriesletter' => $letter,
291
                'showname' => $showname,
292
                'meta_title' => 'View Series List',
293
                'meta_keywords' => 'view,series,tv,show,description,details',
294
                'meta_description' => 'View Series List',
295
            ]);
296
297
            return view('series.viewserieslist', $this->viewData);
298
        }
299
    }
300
301
    /**
302
     * Show trending TV shows (top 15 most downloaded in last 48 hours)
303
     *
304
     * @throws \Exception
305
     */
306
    public function showTrending(Request $request)
0 ignored issues
show
The parameter $request is not used and could be removed. ( Ignorable by Annotation )

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

306
    public function showTrending(/** @scrutinizer ignore-unused */ Request $request)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
307
    {
308
        // Cache key for trending TV shows (48 hours)
309
        $cacheKey = 'trending_tv_top_15_48h';
310
311
        // Get trending TV shows from cache or calculate (refresh every hour)
312
        $trendingShows = \Illuminate\Support\Facades\Cache::remember($cacheKey, 3600, function () {
313
            // Calculate timestamp for 48 hours ago
314
            $fortyEightHoursAgo = \Illuminate\Support\Carbon::now()->subHours(48);
315
316
            // Get TV shows with their download counts from last 48 hours
317
            // Join with user_downloads to get actual download timestamps
318
            $query = \Illuminate\Support\Facades\DB::table('videos as v')
319
                ->join('tv_info as ti', 'v.id', '=', 'ti.videos_id')
320
                ->join('releases as r', 'v.id', '=', 'r.videos_id')
321
                ->leftJoin('user_downloads as ud', 'r.id', '=', 'ud.releases_id')
322
                ->select([
323
                    'v.id',
324
                    'v.title',
325
                    'v.started',
326
                    'v.tvdb',
327
                    'v.tvmaze',
328
                    'v.trakt',
329
                    'v.tmdb',
330
                    'v.countries_id',
331
                    'ti.summary',
332
                    'ti.image',
333
                    \Illuminate\Support\Facades\DB::raw('COUNT(DISTINCT ud.id) as total_downloads'),
334
                    \Illuminate\Support\Facades\DB::raw('COUNT(DISTINCT r.id) as release_count'),
335
                ])
336
                ->where('v.type', 0) // 0 = TV
337
                ->where('v.title', '!=', '')
338
                ->where('ud.timestamp', '>=', $fortyEightHoursAgo)
339
                ->groupBy('v.id', 'v.title', 'v.started', 'v.tvdb', 'v.tvmaze', 'v.trakt', 'v.tmdb', 'v.countries_id', 'ti.summary', 'ti.image')
340
                ->havingRaw('COUNT(DISTINCT ud.id) > 0')
341
                ->orderByDesc('total_downloads')
0 ignored issues
show
'total_downloads' of type string is incompatible with the type Closure|Illuminate\Datab...\Database\Query\Builder expected by parameter $column of Illuminate\Database\Query\Builder::orderByDesc(). ( Ignorable by Annotation )

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

341
                ->orderByDesc(/** @scrutinizer ignore-type */ 'total_downloads')
Loading history...
342
                ->limit(15)
343
                ->get();
344
345
            return $query;
346
        });
347
348
        $this->viewData = array_merge($this->viewData, [
349
            'trendingShows' => $trendingShows,
350
            'meta_title' => 'Trending TV Shows - Last 48 Hours',
351
            'meta_keywords' => 'trending,tv,shows,series,popular,downloads,recent',
352
            'meta_description' => 'Browse the most popular and downloaded TV shows in the last 48 hours',
353
        ]);
354
355
        return view('series.trending', $this->viewData);
356
    }
357
}
358