diff --git a/app/Http/Controllers/SeriesController.php b/app/Http/Controllers/SeriesController.php index bf8108fb9..c01d979f1 100644 --- a/app/Http/Controllers/SeriesController.php +++ b/app/Http/Controllers/SeriesController.php @@ -7,26 +7,20 @@ namespace App\Http\Controllers; use App\Models\TvEpisode; use App\Models\UserSerie; use App\Models\Video; -use App\Services\EpisodeHydrationService; -use App\Services\Releases\ReleaseSearchService; +use App\Services\SeriesReleaseService; use Carbon\Carbon; -use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Http\Request; -use Illuminate\Support\Arr; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\DB; class SeriesController extends BasePageController { - private ReleaseSearchService $releaseSearchService; + private SeriesReleaseService $seriesReleaseService; - private EpisodeHydrationService $episodeHydrationService; - - public function __construct(ReleaseSearchService $releaseSearchService, EpisodeHydrationService $episodeHydrationService) + public function __construct(SeriesReleaseService $seriesReleaseService) { parent::__construct(); - $this->releaseSearchService = $releaseSearchService; - $this->episodeHydrationService = $episodeHydrationService; + $this->seriesReleaseService = $seriesReleaseService; } /** @@ -45,49 +39,58 @@ class SeriesController extends BasePageController $catarray = []; $catarray[] = $category; - $seriesLimit = (int) config('nntmux.series_view_limit', 200); + $seriesLimit = (int) config('nntmux.series_view_limit', config('nntmux.items_per_page', 50)); $page = $this->resolvePage($request); $offset = $seriesLimit > 0 ? $this->paginationOffset($page, $seriesLimit) : 0; - $rel = $this->releaseSearchService->tvSearch(['id' => $id], '', '', '', $offset, $seriesLimit, '', $catarray, -1); - $show = Video::getByVideoID($id); $nodata = ''; $seasons = []; + $seasonTabs = []; + $selectedSeason = null; $myshows = null; $seriestitles = ''; $seriessummary = ''; $seriescountry = ''; + $totalRows = 0; if (! $show) { $nodata = 'No video information for this series.'; - } elseif (! $rel) { - $nodata = 'No releases for this series.'; } else { $myshows = UserSerie::getShow($this->userdata->id, $show['id']); + $categoryIds = $this->seriesReleaseService->categoryIds($catarray); + $seasonCounts = $this->seriesReleaseService->seasonCounts((int) $show['id'], $categoryIds); - $this->episodeHydrationService->hydrateEpisodeMetadata($rel); - if ($rel instanceof EloquentCollection && $rel->isNotEmpty()) { - $rel->loadCount('failed'); + if ($seasonCounts === []) { + $nodata = 'No releases for this series.'; + } else { + $requestedSeason = $this->resolveSeason($request); + $selectedSeason = $requestedSeason !== null && array_key_exists($requestedSeason, $seasonCounts) + ? $requestedSeason + : array_key_first($seasonCounts); + + $seasonReleaseResult = $this->seriesReleaseService->releasesForSeason( + (int) $show['id'], + (int) $selectedSeason, + $offset, + $seriesLimit, + $categoryIds + ); + + $series = []; + foreach ($seasonReleaseResult['releases'] as $release) { + $series[(int) $selectedSeason][(int) $release->getAttribute('episode')][] = $release; + } + if (isset($series[(int) $selectedSeason])) { + ksort($series[(int) $selectedSeason], SORT_NUMERIC); + } + + $seasons = $series; + $totalRows = $seasonReleaseResult['total']; + $seasonTabs = $this->buildSeasonTabs($request, $seasonCounts, (int) $selectedSeason); } - // Sort releases by season, episode, date posted. - $series = $episode = $posted = []; - foreach ($rel as $rlk => $rlv) { - $series[$rlk] = $rlv->series; - $episode[$rlk] = $rlv->episode; - $posted[$rlk] = $rlv->postdate; - } - Arr::sort($series, [[$episode, false], [$posted, false], $rel]); - - $series = []; - foreach ($rel as $r) { - $series[$r->series][$r->episode][] = $r; - } - - $seasons = Arr::sortRecursive($series); - // get series name(s), description, country and genre $seriestitlesArray = $seriessummaryArray = $seriescountryArray = []; $seriestitlesArray[] = $show['title']; @@ -107,7 +110,7 @@ class SeriesController extends BasePageController // Calculate statistics $episodeCount = 0; - $seasonCount = count($seasons); + $seasonCount = count($seasonTabs); $totalSeasonsAvailable = $seasonCount; // Get first and last aired dates from TV episodes @@ -141,11 +144,12 @@ class SeriesController extends BasePageController } $catid = $category !== -1 ? $category : ''; - $totalRows = ($rel && $rel->count() > 0) ? ($rel[0]->_totalrows ?? $rel->count()) : 0; $totalPages = $seriesLimit > 0 ? (int) ceil(max($totalRows, 1) / $seriesLimit) : 1; $this->viewData = array_merge($this->viewData, [ 'seasons' => $seasons, + 'seasonTabs' => $seasonTabs, + 'selectedSeason' => $selectedSeason, 'show' => $show, 'myshows' => $myshows, 'seriestitles' => $seriestitles, @@ -171,6 +175,17 @@ class SeriesController extends BasePageController 'meta_description' => 'View TV Series', ]); + if ($this->scalarInput($request, '_fragment') === 'season') { + return response()->json([ + 'selectedSeason' => $selectedSeason, + 'contentHtml' => view('series.partials.season-content', $this->viewData)->render(), + 'paginationHtml' => view('series.partials.season-pagination', $this->viewData)->render(), + 'url' => $selectedSeason !== null + ? $this->seriesUrlWithQuery($request, ['season' => (int) $selectedSeason, 'page' => $page]) + : $this->seriesUrlWithQuery($request), + ]); + } + return view('series.viewseries', $this->viewData); } else { $letter = ($id && preg_match('/^(0-9|[A-Z])$/i', $id)) ? $id : '0-9'; @@ -211,6 +226,52 @@ class SeriesController extends BasePageController } } + private function resolveSeason(Request $request): ?int + { + $seasonInput = $this->scalarInput($request, 'season'); + if ($seasonInput === '' || preg_match('/^\d+$/', $seasonInput) !== 1) { + return null; + } + + return (int) $seasonInput; + } + + /** + * @param array $seasonCounts + * @return array + */ + private function buildSeasonTabs(Request $request, array $seasonCounts, int $selectedSeason): array + { + $tabs = []; + foreach ($seasonCounts as $season => $count) { + $tabs[] = [ + 'season' => (int) $season, + 'count' => (int) $count, + 'url' => $this->seriesUrlWithQuery($request, [ + 'season' => (int) $season, + 'page' => 1, + ]).'#series-episodes', + 'active' => (int) $season === $selectedSeason, + ]; + } + + return $tabs; + } + + /** + * @param array $query + */ + private function seriesUrlWithQuery(Request $request, array $query = []): string + { + $currentQuery = $request->query(); + unset($currentQuery['_fragment']); + + $merged = array_merge($currentQuery, $query); + $queryString = http_build_query($merged); + + return url()->current().($queryString !== '' ? '?'.$queryString : ''); + } + /** * Show trending TV shows (top 15 most downloaded in last 48 hours) * diff --git a/resources/js/alpine/lazy-loader.js b/resources/js/alpine/lazy-loader.js index 94c49c6a0..196999500 100644 --- a/resources/js/alpine/lazy-loader.js +++ b/resources/js/alpine/lazy-loader.js @@ -39,6 +39,7 @@ const lazyComponentMap = { 'copyToClipboard': () => import('./components/profile-edit.js'), // same file 'cartPage': () => import('./components/cart-page.js'), 'releaseMultiOps': () => import('./components/cart-page.js'), // same file + 'seriesSeasonLoader': () => import('./components/series-season-loader.js'), 'authPage': () => import('./components/auth-page.js'), 'otpInput': () => import('./components/auth-page.js'), // same file 'loginMode': () => import('./components/login-mode.js'), diff --git a/resources/views/series/viewseries.blade.php b/resources/views/series/viewseries.blade.php index 26cfb4fda..e6570df48 100644 --- a/resources/views/series/viewseries.blade.php +++ b/resources/views/series/viewseries.blade.php @@ -198,9 +198,10 @@ - @if(!empty($seasons)) + @if(!empty($seasonTabs)) +
-
+
@@ -233,125 +234,35 @@
-
- @foreach($seasons as $seasonNumber => $episodes) -
- @foreach($episodes as $episodeNumber => $releases) -
-
- Episode {{ $episodeNumber }} -
-
- @foreach($releases as $release) -
-
- -
-
-
- - {{ $release->searchname }} - - @if(($release->failed_count ?? 0) > 0) - - Failed ({{ $release->failed_count }}) - - @endif -
-
- - {{ formatBytes($release->size) }} - - - Added: {{ userDateDiffForHumans($release->adddate) }} - - @if(!empty($release->postdate)) - - Posted: {{ userDate($release->postdate, 'M d, Y H:i') }} - - @endif - @if(!empty($release->fromname)) - - {{ $release->fromname }} - - @endif -
-
- -
- -{{-- Season switcher functionality moved to csp-safe.js --}} - @endforeach -
-
- @endforeach -
- @endforeach -
+ @include('series.partials.season-content')
+
+ @include('series.partials.season-pagination') +
+
@else
No episodes/releases found for this series.
@endif - @if(!empty($pagination['per_page']) && $pagination['per_page'] > 0 && ($pagination['total_pages'] ?? 1) > 1) -
-
- Page {{ $pagination['current_page'] }} of {{ $pagination['total_pages'] }} - ({{ number_format($pagination['total_rows']) }} total releases) -
-
- @php - $prevPage = max($pagination['current_page'] - 1, 1); - $nextPage = min($pagination['current_page'] + 1, $pagination['total_pages']); - @endphp - - Previous - - - Next - -
-
- @endif @endif
@@ -360,4 +271,3 @@ @push('scripts') @include('partials.cart-script') @endpush -