diff --git a/app/Console/Commands/FetchMovieByImdb.php b/app/Console/Commands/FetchMovieByImdb.php index d5239566a..7f7bcd088 100644 --- a/app/Console/Commands/FetchMovieByImdb.php +++ b/app/Console/Commands/FetchMovieByImdb.php @@ -2,7 +2,7 @@ namespace App\Console\Commands; -use Blacklight\Movie; +use App\Services\MovieService; use Illuminate\Console\Command; class FetchMovieByImdb extends Command @@ -32,7 +32,7 @@ class FetchMovieByImdb extends Command return self::FAILURE; } - $movie = app(Movie::class); + $movie = app(MovieService::class); $movie->echooutput = true; $movie->service = 'console'; diff --git a/app/Http/Controllers/Admin/AdminMovieController.php b/app/Http/Controllers/Admin/AdminMovieController.php index 60b566edb..0e1d8d645 100644 --- a/app/Http/Controllers/Admin/AdminMovieController.php +++ b/app/Http/Controllers/Admin/AdminMovieController.php @@ -5,11 +5,19 @@ namespace App\Http\Controllers\Admin; use App\Http\Controllers\BasePageController; use App\Models\MovieInfo; use App\Models\Release; -use Blacklight\Movie; +use App\Services\MovieService; use Illuminate\Http\Request; class AdminMovieController extends BasePageController { + protected MovieService $movieService; + + public function __construct(MovieService $movieService) + { + parent::__construct(); + $this->movieService = $movieService; + } + /** * @throws \Exception */ @@ -56,8 +64,7 @@ class AdminMovieController extends BasePageController } // Check if movie already exists - $movie = new Movie(['Settings' => null]); - $movCheck = $movie->getMovieInfo($id); + $movCheck = $this->movieService->getMovieInfo($id); if ($movCheck !== null) { return redirect()->to('/admin/movie-edit?id='.$id) @@ -66,7 +73,7 @@ class AdminMovieController extends BasePageController // Try to fetch and add the movie from TMDB try { - $movieInfo = $movie->updateMovieInfo($id); + $movieInfo = $this->movieService->updateMovieInfo($id); if ($movieInfo) { // Link any existing releases to this movie @@ -101,7 +108,6 @@ class AdminMovieController extends BasePageController */ public function edit(Request $request) { - $movie = new Movie; $title = 'Movie Edit'; // Check if ID is provided @@ -111,7 +117,7 @@ class AdminMovieController extends BasePageController } $id = $request->input('id'); - $mov = $movie->getMovieInfo($id); + $mov = $this->movieService->getMovieInfo($id); if ($mov === null) { return redirect()->to('admin/movie-list') @@ -125,7 +131,7 @@ class AdminMovieController extends BasePageController \define('STDOUT', fopen('php://stdout', 'wb')); } - $movieInfo = $movie->updateMovieInfo($id); + $movieInfo = $this->movieService->updateMovieInfo($id); if ($movieInfo) { return redirect()->back() @@ -168,7 +174,7 @@ class AdminMovieController extends BasePageController $request->merge(['cover' => file_exists($coverLoc) ? 1 : 0]); $request->merge(['backdrop' => file_exists($backdropLoc) ? 1 : 0]); - $movie->update([ + $this->movieService->update([ 'actors' => $request->input('actors'), 'backdrop' => $request->input('backdrop'), 'cover' => $request->input('cover'), diff --git a/app/Http/Controllers/DetailsController.php b/app/Http/Controllers/DetailsController.php index 4cb3b27cc..21eefd0cd 100644 --- a/app/Http/Controllers/DetailsController.php +++ b/app/Http/Controllers/DetailsController.php @@ -12,12 +12,12 @@ use App\Models\ReleaseRegex; use App\Models\Settings; use App\Models\UserDownload; use App\Models\Video; +use App\Services\MovieService; use App\Services\Releases\ReleaseSearchService; use Blacklight\AniDB; use Blacklight\Books; use Blacklight\Console; use Blacklight\Games; -use Blacklight\Movie; use Blacklight\Music; use Blacklight\ReleaseExtra; use Blacklight\XXX; @@ -27,10 +27,13 @@ class DetailsController extends BasePageController { private ReleaseSearchService $releaseSearchService; - public function __construct(ReleaseSearchService $releaseSearchService) + private MovieService $movieService; + + public function __construct(ReleaseSearchService $releaseSearchService, MovieService $movieService) { parent::__construct(); $this->releaseSearchService = $releaseSearchService; + $this->movieService = $movieService; } public function show(Request $request, string $guid) @@ -70,15 +73,14 @@ class DetailsController extends BasePageController $mov = ''; if ($data['imdbid'] !== '' && $data['imdbid'] !== 0000000) { - $movie = new Movie(['Settings' => $this->settings]); - $mov = $movie->getMovieInfo($data['imdbid']); + $mov = $this->movieService->getMovieInfo($data['imdbid']); if (! empty($mov['title'])) { $mov['title'] = str_replace(['/', '\\'], '', $mov['title']); $mov['actors'] = makeFieldLinks($mov, 'actors', 'movies'); $mov['genre'] = makeFieldLinks($mov, 'genre', 'movies'); $mov['director'] = makeFieldLinks($mov, 'director', 'movies'); if (Settings::settingValue('trailers_display')) { - $trailer = empty($mov['trailer']) || $mov['trailer'] === '' ? $movie->getTrailer($data['imdbid']) : $mov['trailer']; + $trailer = empty($mov['trailer']) || $mov['trailer'] === '' ? $this->movieService->getTrailer($data['imdbid']) : $mov['trailer']; if ($trailer) { $mov['trailer'] = sprintf('', Settings::settingValue('trailers_size_x'), Settings::settingValue('trailers_size_y'), $trailer); } diff --git a/app/Http/Controllers/MovieController.php b/app/Http/Controllers/MovieController.php index 37ca1379b..07f2fcbb2 100644 --- a/app/Http/Controllers/MovieController.php +++ b/app/Http/Controllers/MovieController.php @@ -3,18 +3,28 @@ namespace App\Http\Controllers; use App\Models\Category; -use Blacklight\Movie; +use App\Services\MovieBrowseService; +use App\Services\MovieService; use Illuminate\Http\Request; class MovieController extends BasePageController { + protected MovieBrowseService $movieBrowseService; + + protected MovieService $movieService; + + public function __construct(MovieBrowseService $movieBrowseService, MovieService $movieService) + { + parent::__construct(); + $this->movieBrowseService = $movieBrowseService; + $this->movieService = $movieService; + } + /** * @throws \Exception */ public function showMovies(Request $request, string $id = '') { - $movie = new Movie(['Settings' => $this->settings]); - $moviecats = Category::getChildren(Category::MOVIE_ROOT)->map(function ($mcat) { return ['id' => $mcat->id, 'title' => $mcat->title]; }); @@ -31,12 +41,12 @@ class MovieController extends BasePageController $offset = ($page - 1) * config('nntmux.items_per_cover_page'); $orderby = $request->input('ob', ''); - $ordering = $movie->getMovieOrdering(); + $ordering = $this->movieBrowseService->getMovieOrdering(); if (! in_array($orderby, $ordering, false)) { $orderby = ''; } - $rslt = $movie->getMovieRange($page, $catarray, $offset, config('nntmux.items_per_cover_page'), $orderby, -1, $this->userdata->categoryexclusions); + $rslt = $this->movieBrowseService->getMovieRange($page, $catarray, $offset, config('nntmux.items_per_cover_page'), $orderby, -1, $this->userdata->categoryexclusions); $results = $this->paginate($rslt ?? [], $rslt[0]->_totalcount ?? 0, config('nntmux.items_per_cover_page'), $page, $request->url(), $request->query()); $movies = $results->map(function ($result) { @@ -67,7 +77,7 @@ class MovieController extends BasePageController 'director' => stripslashes($request->input('director', '')), 'ratings' => range(1, 9), 'rating' => $request->input('rating', ''), - 'genres' => $movie->getGenres(), + 'genres' => $this->movieBrowseService->getGenres(), 'genre' => $request->input('genre', ''), 'years' => $years, 'year' => $request->input('year', ''), @@ -94,17 +104,15 @@ class MovieController extends BasePageController */ public function showMovie(Request $request, string $imdbid) { - $movie = new Movie(['Settings' => $this->settings]); - // Get movie info - $movieInfo = $movie->getMovieInfo($imdbid); + $movieInfo = $this->movieService->getMovieInfo($imdbid); if (! $movieInfo) { return redirect()->route('Movies')->with('error', 'Movie not found'); } // Get all releases for this movie - $rslt = $movie->getMovieRange(1, [], 0, 1000, '', -1, $this->userdata->categoryexclusions); + $rslt = $this->movieBrowseService->getMovieRange(1, [], 0, 1000, '', -1, $this->userdata->categoryexclusions); // Filter to only this movie's IMDB ID $movieData = collect($rslt)->firstWhere('imdbid', $imdbid); @@ -183,10 +191,8 @@ class MovieController extends BasePageController */ public function showTrailer(Request $request) { - $movie = new Movie; - if ($request->has('id') && ctype_digit($request->input('id'))) { - $mov = $movie->getMovieInfo($request->input('id')); + $mov = $this->movieService->getMovieInfo($request->input('id')); if (! $mov) { return response()->json(['message' => 'There is no trailer for this movie.'], 404); @@ -224,7 +230,6 @@ class MovieController extends BasePageController */ public function showTrending(Request $request) { - $movie = new Movie(['Settings' => $this->settings]); // Cache key for trending movies (48 hours) $cacheKey = 'trending_movies_top_15_48h'; diff --git a/app/Http/Controllers/MyMoviesController.php b/app/Http/Controllers/MyMoviesController.php index d4db47fc1..cacea2d4c 100644 --- a/app/Http/Controllers/MyMoviesController.php +++ b/app/Http/Controllers/MyMoviesController.php @@ -5,24 +5,32 @@ namespace App\Http\Controllers; use App\Models\Category; use App\Models\Settings; use App\Models\UserMovie; +use App\Services\MovieBrowseService; +use App\Services\MovieService; use App\Services\Releases\ReleaseBrowseService; -use Blacklight\Movie; use Illuminate\Http\Request; class MyMoviesController extends BasePageController { private ReleaseBrowseService $releaseBrowseService; - public function __construct(ReleaseBrowseService $releaseBrowseService) - { + private MovieService $movieService; + + private MovieBrowseService $movieBrowseService; + + public function __construct( + ReleaseBrowseService $releaseBrowseService, + MovieService $movieService, + MovieBrowseService $movieBrowseService + ) { parent::__construct(); $this->releaseBrowseService = $releaseBrowseService; + $this->movieService = $movieService; + $this->movieBrowseService = $movieBrowseService; } public function show(Request $request) { - $mv = new Movie; - $action = $request->input('id') ?? ''; $imdbid = $request->input('imdb') ?? ''; @@ -53,7 +61,7 @@ class MyMoviesController extends BasePageController return redirect()->to('/mymovies'); } - $movie = $mv->getMovieInfo($imdbid); + $movie = $this->movieService->getMovieInfo($imdbid); if (! $movie) { return redirect()->to('/mymovies'); } @@ -149,11 +157,11 @@ class MyMoviesController extends BasePageController } } - $ordering = $this->releaseBrowseService->getBrowseOrdering(); + $ordering = $request->input('ob', ''); $page = $request->has('page') && is_numeric($request->input('page')) ? $request->input('page') : 1; - $results = $mv->getMovieRange($page, $movie['categoryNames'], $offset, config('nntmux.items_per_cover_page'), $ordering, -1, $this->userdata->categoryexclusions); + $results = $this->movieBrowseService->getMovieRange($page, [], $offset, config('nntmux.items_per_cover_page'), $ordering, -1, $this->userdata->categoryexclusions); $this->viewData['covgroup'] = ''; diff --git a/app/Http/Resources/MovieCollection.php b/app/Http/Resources/MovieCollection.php new file mode 100644 index 000000000..14b0515aa --- /dev/null +++ b/app/Http/Resources/MovieCollection.php @@ -0,0 +1,128 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'data' => $this->collection, + 'meta' => [ + 'total' => $this->total(), + 'per_page' => $this->perPage(), + 'current_page' => $this->currentPage(), + 'last_page' => $this->lastPage(), + ], + 'links' => [ + 'first' => $this->url(1), + 'last' => $this->url($this->lastPage()), + 'prev' => $this->previousPageUrl(), + 'next' => $this->nextPageUrl(), + ], + ]; + } + + /** + * Get total items count when available. + */ + protected function total(): int + { + if (method_exists($this->resource, 'total')) { + return $this->resource->total(); + } + + return $this->collection->count(); + } + + /** + * Get items per page when available. + */ + protected function perPage(): int + { + if (method_exists($this->resource, 'perPage')) { + return $this->resource->perPage(); + } + + return $this->collection->count(); + } + + /** + * Get current page when available. + */ + protected function currentPage(): int + { + if (method_exists($this->resource, 'currentPage')) { + return $this->resource->currentPage(); + } + + return 1; + } + + /** + * Get last page when available. + */ + protected function lastPage(): int + { + if (method_exists($this->resource, 'lastPage')) { + return $this->resource->lastPage(); + } + + return 1; + } + + /** + * Get URL for a specific page. + */ + protected function url(int $page): ?string + { + if (method_exists($this->resource, 'url')) { + return $this->resource->url($page); + } + + return null; + } + + /** + * Get previous page URL. + */ + protected function previousPageUrl(): ?string + { + if (method_exists($this->resource, 'previousPageUrl')) { + return $this->resource->previousPageUrl(); + } + + return null; + } + + /** + * Get next page URL. + */ + protected function nextPageUrl(): ?string + { + if (method_exists($this->resource, 'nextPageUrl')) { + return $this->resource->nextPageUrl(); + } + + return null; + } +} + diff --git a/app/Http/Resources/MovieResource.php b/app/Http/Resources/MovieResource.php new file mode 100644 index 000000000..f6beb0273 --- /dev/null +++ b/app/Http/Resources/MovieResource.php @@ -0,0 +1,71 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'imdbid' => $this->imdbid, + 'tmdbid' => $this->tmdbid, + 'traktid' => $this->traktid, + 'title' => $this->title, + 'year' => $this->year, + 'rating' => $this->rating, + 'rtrating' => $this->rtrating, + 'tagline' => $this->tagline, + 'plot' => $this->plot, + 'genre' => $this->genre, + 'type' => $this->type, + 'director' => $this->director, + 'actors' => $this->actors, + 'language' => $this->language, + 'cover' => $this->cover ? true : false, + 'backdrop' => $this->backdrop ? true : false, + 'trailer' => $this->trailer, + 'cover_url' => $this->getCoverUrl(), + 'backdrop_url' => $this->getBackdropUrl(), + 'imdb_url' => $this->imdbid ? 'https://www.imdb.com/title/tt'.$this->imdbid.'/' : null, + 'created_at' => $this->created_at?->toIso8601String(), + 'updated_at' => $this->updated_at?->toIso8601String(), + ]; + } + + /** + * Get the cover image URL. + */ + protected function getCoverUrl(): ?string + { + if (! $this->cover || ! $this->imdbid) { + return null; + } + + return url('/covers/movies/'.$this->imdbid.'-cover.jpg'); + } + + /** + * Get the backdrop image URL. + */ + protected function getBackdropUrl(): ?string + { + if (! $this->backdrop || ! $this->imdbid) { + return null; + } + + return url('/covers/movies/'.$this->imdbid.'-backdrop.jpg'); + } +} + diff --git a/app/Services/MovieBrowseService.php b/app/Services/MovieBrowseService.php new file mode 100644 index 000000000..66243766f --- /dev/null +++ b/app/Services/MovieBrowseService.php @@ -0,0 +1,184 @@ +showPasswords = app(\App\Services\Releases\ReleaseBrowseService::class)->showPasswords(); + } + + /** + * Get movie releases with covers for movie browse page. + */ + public function getMovieRange(int $page, array $cat, int $start, int $num, string $orderBy, int $maxAge = -1, array $excludedCats = []): mixed + { + $page = max(1, $page); + $start = max(0, $start); + $catsrch = ''; + if (count($cat) > 0 && $cat[0] !== -1) { + $catsrch = Category::getCategorySearch($cat); + } + $order = $this->getMovieOrder($orderBy); + $expiresAt = now()->addMinutes(config('nntmux.cache_expiry_medium')); + $whereAge = $maxAge > 0 ? 'AND r.postdate > NOW() - INTERVAL '.$maxAge.' DAY ' : ''; + $whereExcluded = count($excludedCats) > 0 ? ' AND r.categories_id NOT IN ('.implode(',', $excludedCats).')' : ''; + $limitClause = $start === false ? '' : ' LIMIT '.$num.' OFFSET '.$start; + $moviesSql = "SELECT SQL_CALC_FOUND_ROWS m.imdbid, GROUP_CONCAT(r.id ORDER BY r.postdate DESC SEPARATOR ',' ) AS grp_release_id " + ."FROM movieinfo m LEFT JOIN releases r USING (imdbid) WHERE m.title != '' AND m.imdbid != '0000000' " + ."AND r.passwordstatus {$this->showPasswords} " + .$this->getBrowseBy().' ' + .(! empty($catsrch) ? $catsrch.' ' : '') + .$whereAge + .$whereExcluded.' ' + ."GROUP BY m.imdbid ORDER BY {$order[0]} {$order[1]} {$limitClause}"; + $movieCache = Cache::get(md5($moviesSql.$page)); + if ($movieCache !== null) { + $movies = $movieCache; + } else { + $data = MovieInfo::fromQuery($moviesSql); + $movies = ['total' => DB::select('SELECT FOUND_ROWS() AS total'), 'result' => $data]; + Cache::put(md5($moviesSql.$page), $movies, $expiresAt); + } + $movieIDs = $releaseIDs = []; + if (! empty($movies['result'])) { + foreach ($movies['result'] as $id) { + $movieIDs[] = $id->imdbid; + $releaseIDs[] = $id->grp_release_id; + } + } + $inMovieIds = (is_array($movieIDs) && ! empty($movieIDs)) ? implode(',', $movieIDs) : -1; + $inReleaseIds = (is_array($releaseIDs) && ! empty($releaseIDs)) ? implode(',', $releaseIDs) : -1; + $sql = 'SELECT ' + ."GROUP_CONCAT(r.id ORDER BY r.postdate DESC SEPARATOR ',' ) AS grp_release_id, " + ."GROUP_CONCAT(r.rarinnerfilecount ORDER BY r.postdate DESC SEPARATOR ',' ) AS grp_rarinnerfilecount, " + ."GROUP_CONCAT(r.haspreview ORDER BY r.postdate DESC SEPARATOR ',' ) AS grp_haspreview, " + ."GROUP_CONCAT(r.passwordstatus ORDER BY r.postdate DESC SEPARATOR ',' ) AS grp_release_password, " + ."GROUP_CONCAT(r.guid ORDER BY r.postdate DESC SEPARATOR ',' ) AS grp_release_guid, " + ."GROUP_CONCAT(rn.releases_id ORDER BY r.postdate DESC SEPARATOR ',' ) AS grp_release_nfoid, " + ."GROUP_CONCAT(g.name ORDER BY r.postdate DESC SEPARATOR ',' ) AS grp_release_grpname, " + ."GROUP_CONCAT(r.searchname ORDER BY r.postdate DESC SEPARATOR '#') AS grp_release_name, " + ."GROUP_CONCAT(r.postdate ORDER BY r.postdate DESC SEPARATOR ',' ) AS grp_release_postdate, " + ."GROUP_CONCAT(r.adddate ORDER BY r.postdate DESC SEPARATOR ',' ) AS grp_release_adddate, " + ."GROUP_CONCAT(r.size ORDER BY r.postdate DESC SEPARATOR ',' ) AS grp_release_size, " + ."GROUP_CONCAT(r.totalpart ORDER BY r.postdate DESC SEPARATOR ',' ) AS grp_release_totalparts, " + ."GROUP_CONCAT(r.comments ORDER BY r.postdate DESC SEPARATOR ',' ) AS grp_release_comments, " + ."GROUP_CONCAT(r.grabs ORDER BY r.postdate DESC SEPARATOR ',' ) AS grp_release_grabs, " + ."GROUP_CONCAT(df.failed ORDER BY r.postdate DESC SEPARATOR ',' ) AS grp_release_failed, " + ."GROUP_CONCAT(cp.title, ' > ', c.title ORDER BY r.postdate DESC SEPARATOR ',' ) AS grp_release_catname, " + .'m.*, g.name AS group_name, rn.releases_id AS nfoid FROM releases r ' + .'LEFT OUTER JOIN usenet_groups g ON g.id = r.groups_id ' + .'LEFT OUTER JOIN release_nfos rn ON rn.releases_id = r.id ' + .'LEFT OUTER JOIN dnzb_failures df ON df.release_id = r.id ' + .'LEFT OUTER JOIN categories c ON c.id = r.categories_id ' + .'LEFT OUTER JOIN root_categories cp ON cp.id = c.root_categories_id ' + .'INNER JOIN movieinfo m ON m.imdbid = r.imdbid ' + ."WHERE m.imdbid IN ($inMovieIds) AND r.id IN ($inReleaseIds) " + .(! empty($catsrch) ? $catsrch.' ' : '') + ."GROUP BY m.imdbid ORDER BY {$order[0]} {$order[1]}"; + $return = Cache::get(md5($sql.$page)); + if ($return !== null) { + return $return; + } + $return = Release::fromQuery($sql); + if (count($return) > 0) { + $return[0]->_totalcount = $movies['total'][0]->total ?? 0; + } + Cache::put(md5($sql.$page), $return, $expiresAt); + + return $return; + } + + /** + * Get the order type the user requested on the movies page. + */ + protected function getMovieOrder(string $orderBy): array + { + $orderArr = explode('_', (($orderBy === '') ? 'MAX(r.postdate)' : $orderBy)); + $orderField = match ($orderArr[0]) { + 'title' => 'm.title', + 'year' => 'm.year', + 'rating' => 'm.rating', + default => 'MAX(r.postdate)', + }; + + return [$orderField, isset($orderArr[1]) && preg_match('/^asc|desc$/i', $orderArr[1]) ? $orderArr[1] : 'desc']; + } + + /** + * Order types for movies page. + */ + public function getMovieOrdering(): array + { + return ['title_asc', 'title_desc', 'year_asc', 'year_desc', 'rating_asc', 'rating_desc']; + } + + protected function getBrowseBy(): string + { + $browseBy = ' '; + $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)); + if ($bb === 'rating') { + $bbv .= '.'; + } + if ($bb === 'imdb') { + $browseBy .= sprintf(' AND m.imdbid = %d', $bbv); + } else { + $browseBy .= ' AND m.'.$bb.' '.'LIKE '.escapeString('%'.$bbv.'%'); + } + } + } + + return $browseBy; + } + + /** + * Get IMDB genres. + */ + public function getGenres(): array + { + return [ + 'Action', + 'Adventure', + 'Animation', + 'Biography', + 'Comedy', + 'Crime', + 'Documentary', + 'Drama', + 'Family', + 'Fantasy', + 'Film-Noir', + 'Game-Show', + 'History', + 'Horror', + 'Music', + 'Musical', + 'Mystery', + 'News', + 'Reality-TV', + 'Romance', + 'Sci-Fi', + 'Sport', + 'Talk-Show', + 'Thriller', + 'War', + 'Western', + ]; + } +} + diff --git a/Blacklight/Movie.php b/app/Services/MovieService.php old mode 100755 new mode 100644 similarity index 73% rename from Blacklight/Movie.php rename to app/Services/MovieService.php index 0697f4008..98abc37a0 --- a/Blacklight/Movie.php +++ b/app/Services/MovieService.php @@ -1,56 +1,36 @@ releaseImage = new ReleaseImage; $this->colorCli = new ColorCLI; $this->traktcheck = config('nntmux_api.trakttv_api_key'); @@ -125,7 +89,6 @@ class Movie if (! File::isDirectory($cacheDir)) { File::makeDirectory($cacheDir, 0777, false, true); } - // $this->config->cachedir = $cacheDir; // removed imdbphp config $this->imdburl = (int) Settings::settingValue('imdburl') !== 0; $this->movieqty = Settings::settingValue('maximdbprocessed') !== '' ? (int) Settings::settingValue('maximdbprocessed') : 100; @@ -137,151 +100,20 @@ class Movie } /** - * @return Builder|Model|null|object + * Get movie info by IMDB ID. */ - public function getMovieInfo($imdbId) + public function getMovieInfo(string $imdbId): ?MovieInfo { return MovieInfo::query()->where('imdbid', $imdbId)->first(); } - /** - * Get movie releases with covers for movie browse page. - * - * - * @return array|mixed - */ - public function getMovieRange($page, $cat, $start, $num, $orderBy, int $maxAge = -1, array $excludedCats = []) - { - $page = max(1, $page); - $start = max(0, $start); - $catsrch = ''; - if (count($cat) > 0 && $cat[0] !== -1) { - $catsrch = Category::getCategorySearch($cat); - } - $order = $this->getMovieOrder($orderBy); - $expiresAt = now()->addMinutes(config('nntmux.cache_expiry_medium')); - $whereAge = $maxAge > 0 ? 'AND r.postdate > NOW() - INTERVAL '.$maxAge.' DAY ' : ''; - $whereExcluded = count($excludedCats) > 0 ? ' AND r.categories_id NOT IN ('.implode(',', $excludedCats).')' : ''; - $limitClause = $start === false ? '' : ' LIMIT '.$num.' OFFSET '.$start; - $moviesSql = "SELECT SQL_CALC_FOUND_ROWS m.imdbid, GROUP_CONCAT(r.id ORDER BY r.postdate DESC SEPARATOR ',' ) AS grp_release_id " - ."FROM movieinfo m LEFT JOIN releases r USING (imdbid) WHERE m.title != '' AND m.imdbid != '0000000' " - ."AND r.passwordstatus {$this->showPasswords} " - .$this->getBrowseBy().' ' - .(! empty($catsrch) ? $catsrch.' ' : '') - .$whereAge - .$whereExcluded.' ' - ."GROUP BY m.imdbid ORDER BY {$order[0]} {$order[1]} {$limitClause}"; - $movieCache = Cache::get(md5($moviesSql.$page)); - if ($movieCache !== null) { - $movies = $movieCache; - } else { - $data = MovieInfo::fromQuery($moviesSql); - $movies = ['total' => DB::select('SELECT FOUND_ROWS() AS total'), 'result' => $data]; - Cache::put(md5($moviesSql.$page), $movies, $expiresAt); - } - $movieIDs = $releaseIDs = []; - if (! empty($movies['result'])) { - foreach ($movies['result'] as $id) { - $movieIDs[] = $id->imdbid; - $releaseIDs[] = $id->grp_release_id; - } - } - $inMovieIds = (is_array($movieIDs) && ! empty($movieIDs)) ? implode(',', $movieIDs) : -1; - $inReleaseIds = (is_array($releaseIDs) && ! empty($releaseIDs)) ? implode(',', $releaseIDs) : -1; - $sql = 'SELECT ' - ."GROUP_CONCAT(r.id ORDER BY r.postdate DESC SEPARATOR ',' ) AS grp_release_id, " - ."GROUP_CONCAT(r.rarinnerfilecount ORDER BY r.postdate DESC SEPARATOR ',' ) AS grp_rarinnerfilecount, " - ."GROUP_CONCAT(r.haspreview ORDER BY r.postdate DESC SEPARATOR ',' ) AS grp_haspreview, " - ."GROUP_CONCAT(r.passwordstatus ORDER BY r.postdate DESC SEPARATOR ',' ) AS grp_release_password, " - ."GROUP_CONCAT(r.guid ORDER BY r.postdate DESC SEPARATOR ',' ) AS grp_release_guid, " - ."GROUP_CONCAT(rn.releases_id ORDER BY r.postdate DESC SEPARATOR ',' ) AS grp_release_nfoid, " - ."GROUP_CONCAT(g.name ORDER BY r.postdate DESC SEPARATOR ',' ) AS grp_release_grpname, " - ."GROUP_CONCAT(r.searchname ORDER BY r.postdate DESC SEPARATOR '#') AS grp_release_name, " - ."GROUP_CONCAT(r.postdate ORDER BY r.postdate DESC SEPARATOR ',' ) AS grp_release_postdate, " - ."GROUP_CONCAT(r.adddate ORDER BY r.postdate DESC SEPARATOR ',' ) AS grp_release_adddate, " - ."GROUP_CONCAT(r.size ORDER BY r.postdate DESC SEPARATOR ',' ) AS grp_release_size, " - ."GROUP_CONCAT(r.totalpart ORDER BY r.postdate DESC SEPARATOR ',' ) AS grp_release_totalparts, " - ."GROUP_CONCAT(r.comments ORDER BY r.postdate DESC SEPARATOR ',' ) AS grp_release_comments, " - ."GROUP_CONCAT(r.grabs ORDER BY r.postdate DESC SEPARATOR ',' ) AS grp_release_grabs, " - ."GROUP_CONCAT(df.failed ORDER BY r.postdate DESC SEPARATOR ',' ) AS grp_release_failed, " - ."GROUP_CONCAT(cp.title, ' > ', c.title ORDER BY r.postdate DESC SEPARATOR ',' ) AS grp_release_catname, " - .'m.*, g.name AS group_name, rn.releases_id AS nfoid FROM releases r ' - .'LEFT OUTER JOIN usenet_groups g ON g.id = r.groups_id ' - .'LEFT OUTER JOIN release_nfos rn ON rn.releases_id = r.id ' - .'LEFT OUTER JOIN dnzb_failures df ON df.release_id = r.id ' - .'LEFT OUTER JOIN categories c ON c.id = r.categories_id ' - .'LEFT OUTER JOIN root_categories cp ON cp.id = c.root_categories_id ' - .'INNER JOIN movieinfo m ON m.imdbid = r.imdbid ' - ."WHERE m.imdbid IN ($inMovieIds) AND r.id IN ($inReleaseIds) " - .(! empty($catsrch) ? $catsrch.' ' : '') - ."GROUP BY m.imdbid ORDER BY {$order[0]} {$order[1]}"; - $return = Cache::get(md5($sql.$page)); - if ($return !== null) { - return $return; - } - $return = Release::fromQuery($sql); - if (count($return) > 0) { - $return[0]->_totalcount = $movies['total'][0]->total ?? 0; - } - Cache::put(md5($sql.$page), $return, $expiresAt); - - return $return; - } - - /** - * Get the order type the user requested on the movies page. - */ - protected function getMovieOrder($orderBy): array - { - $orderArr = explode('_', (($orderBy === '') ? 'MAX(r.postdate)' : $orderBy)); - $orderField = match ($orderArr[0]) { - 'title' => 'm.title', - 'year' => 'm.year', - 'rating' => 'm.rating', - default => 'MAX(r.postdate)', - }; - - return [$orderField, isset($orderArr[1]) && preg_match('/^asc|desc$/i', $orderArr[1]) ? $orderArr[1] : 'desc']; - } - - /** - * Order types for movies page. - */ - public function getMovieOrdering(): array - { - return ['title_asc', 'title_desc', 'year_asc', 'year_desc', 'rating_asc', 'rating_desc']; - } - - protected function getBrowseBy(): string - { - $browseBy = ' '; - $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)); - if ($bb === 'rating') { - $bbv .= '.'; - } - if ($bb === 'imdb') { - $browseBy .= sprintf(' AND m.imdbid = %d', $bbv); - } else { - $browseBy .= ' AND m.'.$bb.' '.'LIKE '.escapeString('%'.$bbv.'%'); - } - } - } - - return $browseBy; - } - /** * Get trailer using IMDB Id. * - * @return bool|string - * * @throws \Exception * @throws GuzzleException */ - public function getTrailer(int $imdbId) + public function getTrailer(int $imdbId): string|false { $trailer = MovieInfo::query()->where('imdbid', $imdbId)->where('trailer', '<>', '')->first(['trailer']); if ($trailer !== null) { @@ -307,10 +139,8 @@ class Movie /** * Parse trakt info, insert into DB. - * - * @return mixed */ - public function parseTraktTv(array &$data) + public function parseTraktTv(array &$data): mixed { if (empty($data['ids']['imdb'])) { return false; @@ -325,7 +155,6 @@ class Movie } $imdbId = (str_starts_with($data['ids']['imdb'], 'tt')) ? substr($data['ids']['imdb'], 2) : $data['ids']['imdb']; $cover = 0; - // Fix incorrect file existence check (parentheses and concatenation) if (File::isFile($this->imgSavePath.$imdbId.'-cover.jpg')) { $cover = 1; } @@ -346,10 +175,7 @@ class Movie ]); } - /** - * @return mixed|string - */ - private function checkTraktValue($value) + private function checkTraktValue(mixed $value): mixed { if (\is_array($value) && ! empty($value)) { $temp = ''; @@ -377,10 +203,8 @@ class Movie /** * Choose the first non-empty variable from up to five inputs. - * - * @return array|string */ - protected function setVariables(string|array $variable1, string|array $variable2, string|array $variable3, string|array $variable4, string|array $variable5 = '') + protected function setVariables(string|array $variable1, string|array $variable2, string|array $variable3, string|array $variable4, string|array $variable5 = ''): array|string { if (! empty($variable1)) { return $variable1; @@ -403,8 +227,6 @@ class Movie /** * Update movie on movie-edit page. - * - * @param array $values Array of keys/values to update. See $validKeys */ public function update(array $values): bool { @@ -451,10 +273,9 @@ class Movie /** * Fetch IMDB/TMDB/TRAKT/OMDB/iTunes info for the movie. * - * * @throws \Exception */ - public function updateMovieInfo($imdbId): bool + public function updateMovieInfo(string $imdbId): bool { if ($this->echooutput && $this->service !== '') { $this->colorCli->primary('Fetching IMDB info from TMDB/IMDB/Trakt/OMDB/iTunes using IMDB id: '.$imdbId); @@ -656,7 +477,7 @@ class Movie /** * Fetch FanArt.tv backdrop / cover / title. */ - protected function fetchFanartTVProperties($imdbId): false|array + protected function fetchFanartTVProperties(string $imdbId): false|array { if (! $this->fanart->isConfigured()) { return false; @@ -681,21 +502,14 @@ class Movie /** * Fetch movie information from TMDB using an IMDB ID. - * - * @param string $imdbId The IMDB ID to look up - * @param bool $text Whether the ID is already in text format - * @return array|false Movie data array or false if not found/matched */ public function fetchTMDBProperties(string $imdbId, bool $text = false): array|false { - // Format the lookup ID correctly with 'tt' prefix if needed $lookupId = $text === false && (strlen($imdbId) === 7 || strlen($imdbId) === 8) ? 'tt'.$imdbId : $imdbId; - // Create a cache key for this request $cacheKey = 'tmdb_movie_'.md5($lookupId); - $expiresAt = now()->addDays(7); // Cache for 7 days since movie data rarely changes + $expiresAt = now()->addDays(7); - // Check if we have this cached if (Cache::has($cacheKey)) { return Cache::get($cacheKey); } @@ -707,7 +521,6 @@ class Movie return false; } - // Fetch movie data from TMDB $tmdbLookup = $tmdbClient->getMovie($lookupId, ['credits']); if ($tmdbLookup === null || empty($tmdbLookup)) { @@ -716,7 +529,6 @@ class Movie return false; } - // Title similarity check $title = TmdbClient::getString($tmdbLookup, 'title'); if ($this->currentTitle !== '' && ! empty($title)) { similar_text($this->currentTitle, $title, $percent); @@ -727,7 +539,6 @@ class Movie } } - // Year similarity check $releaseDate = TmdbClient::getString($tmdbLookup, 'release_date'); if ($this->currentYear !== '' && ! empty($releaseDate)) { $tmdbYear = Carbon::parse($releaseDate)->year; @@ -740,7 +551,6 @@ class Movie } } - // Build the return array with proper null handling $imdbIdFromResponse = TmdbClient::getString($tmdbLookup, 'imdb_id'); $ret = [ 'title' => $title, @@ -757,13 +567,11 @@ class Movie 'backdrop' => '', ]; - // Rating $vote = TmdbClient::getFloat($tmdbLookup, 'vote_average'); if ($vote > 0) { $ret['rating'] = $vote; } - // Actors $credits = TmdbClient::getArray($tmdbLookup, 'credits'); $cast = TmdbClient::getArray($credits, 'cast'); if (! empty($cast)) { @@ -778,7 +586,6 @@ class Movie } } - // Director - get first director only $crew = TmdbClient::getArray($credits, 'crew'); foreach ($crew as $crewMember) { if (! is_array($crewMember)) { @@ -792,12 +599,10 @@ class Movie } } - // Year if (! empty($releaseDate)) { $ret['year'] = Carbon::parse($releaseDate)->year; } - // Genres $genresa = TmdbClient::getArray($tmdbLookup, 'genres'); if (! empty($genresa)) { $genres = []; @@ -811,7 +616,6 @@ class Movie } } - // Cover and backdrop $posterPath = TmdbClient::getString($tmdbLookup, 'poster_path'); if (! empty($posterPath)) { $ret['cover'] = 'https://image.tmdb.org/t/p/original'.$posterPath; @@ -822,21 +626,16 @@ class Movie $ret['backdrop'] = 'https://image.tmdb.org/t/p/original'.$backdropPath; } - // Log success if ($this->echooutput) { $this->colorCli->info('TMDb found '.$ret['title']); } - // Cache the result Cache::put($cacheKey, $ret, $expiresAt); return $ret; } catch (\Throwable $e) { - // Log the error Log::warning('TMDB API error for '.$lookupId.': '.$e->getMessage()); - - // Cache the failure but for shorter time Cache::put($cacheKey, false, now()->addHours(6)); return false; @@ -845,9 +644,6 @@ class Movie /** * Fetch movie information from IMDB. - * - * @param string $imdbId The IMDB ID to look up - * @return array|false Movie data array or false if not found/matched */ public function fetchIMDBProperties(string $imdbId): array|false { @@ -897,39 +693,30 @@ class Movie /** * Fetch movie information from Trakt.tv using IMDB ID. * - * @param string $imdbId The IMDB ID without the 'tt' prefix - * @return array|false Movie data array or false if not found/matched - * - * @throws GuzzleException Only if unhandled HTTP errors occur + * @throws GuzzleException */ public function fetchTraktTVProperties(string $imdbId): array|false { - // Skip if Trakt API key isn't configured if ($this->traktcheck === null) { return false; } - // Create a cache key for this request $cacheKey = 'trakt_movie_'.md5($imdbId); - $expiresAt = now()->addDays(7); // Cache for 7 days since movie data rarely changes + $expiresAt = now()->addDays(7); - // Check if we have this cached if (Cache::has($cacheKey)) { return Cache::get($cacheKey); } try { - // Fetch movie data from Trakt.tv $resp = $this->traktTv->client->getMovieSummary('tt'.$imdbId, 'full'); - // If no result or no title, cache the failure and return if ($resp === false || empty($resp['title'])) { Cache::put($cacheKey, false, now()->addHours(6)); return false; } - // Title similarity check if (! empty($this->currentTitle)) { similar_text($this->currentTitle, $resp['title'], $percent); if ($percent < self::MATCH_PERCENT) { @@ -939,7 +726,6 @@ class Movie } } - // Year similarity check if (! empty($this->currentYear) && ! empty($resp['year'])) { similar_text($this->currentYear, $resp['year'], $percent); if ($percent < self::YEAR_MATCH_PERCENT) { @@ -949,7 +735,6 @@ class Movie } } - // Build the return data $movieData = [ 'id' => $resp['ids']['trakt'] ?? null, 'title' => $resp['title'], @@ -964,21 +749,16 @@ class Movie 'trailer' => $resp['trailer'] ?? '', ]; - // Log success if ($this->echooutput) { $this->colorCli->info('Trakt found '.$movieData['title']); } - // Cache the successful result Cache::put($cacheKey, $movieData, $expiresAt); return $movieData; } catch (\Throwable $e) { - // Log the error Log::warning('Trakt API error for '.$imdbId.': '.$e->getMessage()); - - // Cache the failure but for shorter time Cache::put($cacheKey, false, now()->addHours(6)); return false; @@ -987,31 +767,23 @@ class Movie /** * Fetch movie information from OMDB API using IMDB ID. - * - * @param string $imdbId The IMDB ID without the 'tt' prefix - * @return array|false Movie data array or false if not found/matched */ public function fetchOmdbAPIProperties(string $imdbId): array|false { - // Skip if OMDB API key isn't configured if ($this->omdbapikey === null) { return false; } - // Create a cache key for this request $cacheKey = 'omdb_movie_'.md5($imdbId); - $expiresAt = now()->addDays(7); // Cache for 7 days since movie data rarely changes + $expiresAt = now()->addDays(7); - // Check if we have this cached if (Cache::has($cacheKey)) { return Cache::get($cacheKey); } try { - // Fetch movie data from OMDB $resp = $this->omdbApi->fetch('i', 'tt'.$imdbId); - // Validate the response if (! is_object($resp) || $resp->message !== 'OK' || Str::contains($resp->data->Response, 'Error:') || @@ -1022,7 +794,6 @@ class Movie return false; } - // Title similarity check when we have a title to compare against if (! empty($this->currentTitle)) { similar_text($this->currentTitle, $resp->data->Title, $percent); if ($percent < self::MATCH_PERCENT) { @@ -1031,7 +802,6 @@ class Movie return false; } - // Year similarity check if (! empty($this->currentYear)) { similar_text($this->currentYear, $resp->data->Year, $percent); if ($percent < self::YEAR_MATCH_PERCENT) { @@ -1042,13 +812,11 @@ class Movie } } - // Safely extract the Rotten Tomatoes rating $rtRating = ''; if (isset($resp->data->Ratings) && is_array($resp->data->Ratings) && count($resp->data->Ratings) > 1) { $rtRating = $resp->data->Ratings[1]->Value ?? ''; } - // Build the movie data array $movieData = [ 'title' => $resp->data->Title ?? '', 'cover' => $resp->data->Poster ?? '', @@ -1064,21 +832,16 @@ class Movie 'boxOffice' => $resp->data->BoxOffice ?? '', ]; - // Log success if ($this->echooutput) { $this->colorCli->info('OMDbAPI Found '.$movieData['title']); } - // Cache the successful result Cache::put($cacheKey, $movieData, $expiresAt); return $movieData; } catch (\Throwable $e) { - // Log the error Log::warning('OMDB API error for '.$imdbId.': '.$e->getMessage()); - - // Cache the failure but for shorter time Cache::put($cacheKey, false, now()->addHours(6)); return false; @@ -1088,23 +851,15 @@ class Movie /** * Update a release with an IMDB ID and related movie information. * - * @param string $buffer Data to parse an IMDB ID from - * @param string $service Method that called this method - * @param int $id ID of the release - * @param int $processImdb Whether to fetch movie info (1) or not (0) - * @return string|false IMDB ID or false if not found - * * @throws \Exception */ public function doMovieUpdate(string $buffer, string $service, int $id, int $processImdb = 1): string|false { - // First check if this release already has an IMDB ID to avoid duplicate processing $existingImdbId = Release::query()->where('id', $id)->value('imdbid'); if ($existingImdbId !== null && $existingImdbId !== '' && $existingImdbId !== '0000000') { return $existingImdbId; } - // Extract IMDB ID using regex $imdbId = false; if (preg_match('/(?:imdb.*?)?(?:tt|Title\?)(?P\d{5,8})/i', $buffer, $hits)) { $imdbId = $hits['imdbid']; @@ -1117,21 +872,17 @@ class Movie $this->colorCli->info($this->service.' found IMDBid: tt'.$imdbId); } - // Get movie info ID $movieInfoId = MovieInfo::query()->where('imdbid', $imdbId)->first(['id']); - // Update release with IMDB ID Release::query()->where('id', $id)->update([ 'imdbid' => $imdbId, 'movieinfo_id' => $movieInfoId !== null ? $movieInfoId['id'] : null, ]); - // If set, scan for IMDB info if ($processImdb === 1) { $movCheck = $this->getMovieInfo($imdbId); - $thirtyDaysInSeconds = 30 * 24 * 60 * 60; // 30 days in seconds + $thirtyDaysInSeconds = 30 * 24 * 60 * 60; - // Check if movie info is missing or outdated if ($movCheck === null || (isset($movCheck['updated_at']) && (time() - strtotime($movCheck['updated_at'])) > $thirtyDaysInSeconds)) { @@ -1139,13 +890,10 @@ class Movie $info = $this->updateMovieInfo($imdbId); if ($info === false) { - // Update failed, mark with invalid IMDB ID Release::query()->where('id', $id)->update(['imdbid' => '0000000']); } elseif ($info === true) { - // Get fresh movie info ID after update $freshMovieInfo = MovieInfo::query()->where('imdbid', $imdbId)->first(['id']); - // Update release with movie info ID Release::query()->where('id', $id)->update([ 'movieinfo_id' => $freshMovieInfo !== null ? $freshMovieInfo['id'] : null, ]); @@ -1155,7 +903,6 @@ class Movie return $imdbId; } catch (\Exception $e) { - // Log the error Log::error('Error updating movie information: '.$e->getMessage()); return false; @@ -1168,35 +915,20 @@ class Movie /** * Process releases with no IMDB IDs by looking up movie information from various sources. * - * Searches for IMDB IDs using multiple services in this order: - * 1. Local database - * 2. IMDb API - * 3. OMDb API - * 4. Trakt.tv - * 5. The Movie Database (TMDB) - * - * @param string $groupID Optional group ID to filter by - * @param string $guidChar Optional first character of GUID to filter by - * @param int $lookupIMDB 0: Skip lookup, 1: Process all, 2: Only renamed releases - * * @throws \Exception * @throws GuzzleException */ public function processMovieReleases(string $groupID = '', string $guidChar = '', int $lookupIMDB = 1): void { - // Skip processing if lookup is disabled if ($lookupIMDB === 0) { return; } - // Always query fresh data to avoid processing already-processed releases - // Build query to get releases without IMDB IDs $query = Release::query() ->select(['searchname', 'id']) ->whereBetween('categories_id', [Category::MOVIE_ROOT, Category::MOVIE_OTHER]) ->whereNull('imdbid'); - // Apply filters if provided if ($groupID !== '') { $query->where('groups_id', $groupID); } @@ -1209,22 +941,17 @@ class Movie $query->where('isrenamed', '=', 1); } - // Execute the query with limit, ordering by latest releases first $res = $query->orderByDesc('id')->limit($this->movieqty)->get(); - $movieCount = count($res); - $failedIDs = []; // Track IDs that need to be marked as unidentifiable + $failedIDs = []; if ($movieCount > 0) { - // Log the start of processing if multiple movies if ($this->echooutput && $movieCount > 1) { $this->colorCli->header('Processing '.$movieCount.' movie releases.'); } - // Loop over releases foreach ($res as $arr) { - // Try to extract movie name and year if (! $this->parseMovieSearchName($arr['searchname'])) { $failedIDs[] = $arr['id']; @@ -1234,28 +961,23 @@ class Movie $this->currentRelID = $arr['id']; $movieName = $this->formatMovieName(); - // Log current lookup if output is enabled if ($this->echooutput) { $this->colorCli->info('Looking up: '.$movieName); } - // Try all available sources to find IMDB ID $foundIMDB = $this->searchLocalDatabase($arr['id']) || $this->searchIMDb($arr['id']) || $this->searchOMDbAPI($arr['id']) || $this->searchTraktTV($arr['id'], $movieName) || $this->searchTMDB($arr['id']); - // Double-check if we actually got an IMDB ID if ($foundIMDB) { - // Movie was successfully updated by one of the services if ($this->echooutput) { $this->colorCli->primary('Successfully updated release with IMDB ID'); } continue; } else { - // Verify the release wasn't actually updated $releaseCheck = Release::query()->where('id', $arr['id'])->whereNotNull('imdbid')->exists(); if ($releaseCheck) { if ($this->echooutput) { @@ -1266,13 +988,10 @@ class Movie } } - // If we get here, all searches failed $failedIDs[] = $arr['id']; } - // Batch update all failed releases at once if (! empty($failedIDs)) { - // Get searchnames for failed releases to show in output if ($this->echooutput) { $failedReleases = Release::query() ->select(['id', 'searchname']) @@ -1285,7 +1004,6 @@ class Movie } } - // Use chunk to avoid huge queries for many IDs foreach (array_chunk($failedIDs, 100) as $chunk) { Release::query()->whereIn('id', $chunk)->update(['imdbid' => '0000000']); } @@ -1293,9 +1011,6 @@ class Movie } } - /** - * Format current movie name with year if available. - */ private function formatMovieName(): string { $movieName = $this->currentTitle; @@ -1306,9 +1021,6 @@ class Movie return $movieName; } - /** - * Search local database for movie. - */ private function searchLocalDatabase(int $releaseId): bool { $getIMDBid = $this->localIMDBSearch(); @@ -1321,12 +1033,8 @@ class Movie return $imdbId !== false; } - /** - * Search IMDb for movie. - */ private function searchIMDb(int $releaseId): bool { - // Simplified scraper-only search try { $scraper = app(ImdbScraper::class); $matches = $scraper->search($this->currentTitle); @@ -1357,9 +1065,6 @@ class Movie return false; } - /** - * Search OMDb API for movie. - */ private function searchOMDbAPI(int $releaseId): bool { if ($this->omdbapikey === null) { @@ -1369,7 +1074,6 @@ class Movie $omdbTitle = strtolower(str_replace(' ', '_', $this->currentTitle)); try { - // Search with year if available, otherwise search without year $buffer = $this->currentYear !== '' ? $this->omdbApi->search($omdbTitle, 'movie', $this->currentYear) : $this->omdbApi->search($omdbTitle, 'movie'); @@ -1388,16 +1092,12 @@ class Movie return $imdbId !== false; } catch (\Exception $e) { - // Log error but continue processing Log::error('OMDb API error: '.$e->getMessage()); return false; } } - /** - * Search Trakt.tv for movie. - */ private function searchTraktTV(int $releaseId, string $movieName): bool { if ($this->traktcheck === null) { @@ -1422,9 +1122,6 @@ class Movie } } - /** - * Search TMDB for movie. - */ private function searchTMDB(int $releaseId): bool { try { @@ -1446,7 +1143,6 @@ class Movie continue; } - // Skip results without ID or release date $resultId = TmdbClient::getInt($result, 'id'); $releaseDate = TmdbClient::getString($result, 'release_date'); @@ -1454,7 +1150,6 @@ class Movie continue; } - // Compare release years similar_text( $this->currentYear, (string) Carbon::parse($releaseDate)->year, @@ -1465,7 +1160,6 @@ class Movie continue; } - // Try to get IMDB ID from TMDB $ret = $this->fetchTMDBProperties((string) $resultId, true); if ($ret === false || empty($ret['imdbid'])) { continue; @@ -1484,54 +1178,40 @@ class Movie return false; } - /** - * Search for a movie in the local database by title and year. - * - * @return string|false IMDB ID without 'tt' prefix if found, false otherwise - */ protected function localIMDBSearch(): string|false { - // Skip processing if title is empty if (empty($this->currentTitle)) { return false; } - // Create a cache key for this search $cacheKey = 'local_imdb_'.md5($this->currentTitle.$this->currentYear); - // Check cache first if (Cache::has($cacheKey)) { return Cache::get($cacheKey); } - // Build the base query $query = MovieInfo::query() ->select(['imdbid', 'title']) ->where('title', 'like', '%'.$this->currentTitle.'%'); - // Add year range filter if we have a year if (! empty($this->currentYear)) { $start = Carbon::createFromFormat('Y', $this->currentYear)->subYears(2)->year; $end = Carbon::createFromFormat('Y', $this->currentYear)->addYears(2)->year; $query->whereBetween('year', [$start, $end]); } - // Get potential matches in a single query $potentialMatches = $query->get(); - // If no matches found, cache the failure and return false if ($potentialMatches->isEmpty()) { Cache::put($cacheKey, false, now()->addHours(6)); return false; } - // Check each potential match for title similarity foreach ($potentialMatches as $match) { similar_text($this->currentTitle, $match['title'], $percent); if ($percent >= self::MATCH_PERCENT) { - // Found a good match, cache it and return Cache::put($cacheKey, $match['imdbid'], now()->addDays(7)); if ($this->echooutput) { @@ -1542,32 +1222,19 @@ class Movie } } - // No good matches found, cache the failure Cache::put($cacheKey, false, now()->addHours(6)); return false; } - /** - * Parse a movie title and year from a release search name. - * - * This method attempts to extract a clean movie title and year from scene release names - * which often contain quality indicators, source information, and other metadata. - * - * @param string $releaseName The raw release name to parse - * @return bool True if parsing was successful, false otherwise - */ protected function parseMovieSearchName(string $releaseName): bool { - // Skip empty release names if (empty(trim($releaseName))) { return false; } - // Create a cache key for this parsing operation $cacheKey = 'parse_movie_'.md5($releaseName); - // Check if we have a cached result if (Cache::has($cacheKey)) { $result = Cache::get($cacheKey); if (is_array($result)) { @@ -1582,55 +1249,35 @@ class Movie $name = $year = ''; - // Common movie quality, format, and release group patterns to identify boundaries $followingList = '[^\w]((1080|480|720|2160)p|AC3D|Directors([^\w]CUT)?|DD5\.1|(DVD|BD|BR|UHD)(Rip)?|' .'BluRay|divx|HDTV|iNTERNAL|LiMiTED|(Real\.)?PROPER|RE(pack|Rip)|Sub\.?(fix|pack)|' .'Unrated|WEB-?DL|WEBRip|(x|H|HEVC)[ ._-]?26[45]|xvid|AAC|REMUX)[^\w]'; - // First attempt: Find pattern with year - most reliable method if (preg_match('/(?P[\w. -]+)[^\w](?P(19|20)\d\d)/i', $releaseName, $hits)) { $name = $hits['name']; $year = $hits['year']; - } - // Second attempt: Look for title followed by common release identifiers - elseif (preg_match('/([^\w]{2,})?(?P[\w .-]+?)'.$followingList.'/i', $releaseName, $hits)) { + } elseif (preg_match('/([^\w]{2,})?(?P[\w .-]+?)'.$followingList.'/i', $releaseName, $hits)) { $name = $hits['name']; - } - // Third attempt: Try to match the start of the string up to a pattern - elseif (preg_match('/^(?P[\w .-]+?)'.$followingList.'/i', $releaseName, $hits)) { + } elseif (preg_match('/^(?P[\w .-]+?)'.$followingList.'/i', $releaseName, $hits)) { $name = $hits['name']; - } - // Fourth attempt: Check if the whole string might be a title (for simple releases) - elseif (strlen($releaseName) <= 100 && ! preg_match('/\.(rar|zip|avi|mkv|mp4)$/i', $releaseName)) { + } elseif (strlen($releaseName) <= 100 && ! preg_match('/\.(rar|zip|avi|mkv|mp4)$/i', $releaseName)) { $name = $releaseName; } - // Process the name if we have one if (! empty($name)) { - // Clean up the name by removing common patterns - // 1. Remove any common movie flags like 1080p, BluRay, etc. $name = preg_replace('/'.$followingList.'/i', ' ', $name); - - // 2. Remove content in parentheses, brackets, and periods/underscores without complex alternation $name = preg_replace('/\([^)]*\)/i', ' ', $name); - // Remove [ ... ] blocks without regex escapes to satisfy linter while (($openPos = strpos($name, '[')) !== false && ($closePos = strpos($name, ']', $openPos)) !== false) { $name = substr($name, 0, $openPos).' '.substr($name, $closePos + 1); } $name = str_replace(['.', '_'], ' ', $name); - - // 3. Remove scene group names (typically after a hyphen) $name = preg_replace('/-[A-Z0-9].*$/i', '', $name); - - // 4. Clean up multiple spaces $name = trim(preg_replace('/\s{2,}/', ' ', $name)); - // Validate the extracted name (at least 2 characters, not just numbers) if (strlen($name) > 2 && ! preg_match('/^\d+$/', $name)) { $this->currentTitle = $name; $this->currentYear = $year; - // Cache the successful result Cache::put($cacheKey, [ 'title' => $name, 'year' => $year, @@ -1640,7 +1287,6 @@ class Movie } } - // Cache the failed result but with shorter expiration Cache::put($cacheKey, false, now()->addHours(24)); return false; @@ -1683,7 +1329,6 @@ class Movie protected function hasCover(string $imdbId): bool { - // Checks both DB flag and physical file existence. $record = MovieInfo::query()->select('cover')->where('imdbid', $imdbId)->first(); $dbHas = $record !== null && (int) $record->cover === 1; $filePath = $this->imgSavePath.$imdbId.'-cover.jpg'; @@ -1694,8 +1339,6 @@ class Movie protected function fetchAndSaveCoverOnly(string $imdbId): bool { - // Attempt sources in priority order without pulling full metadata again if possible. - // Fanart try { $fanart = $this->fetchFanartTVProperties($imdbId); if (! empty($fanart['cover'])) { @@ -1706,7 +1349,7 @@ class Movie } catch (\Throwable $e) { Log::debug('Fanart cover fetch failed for '.$imdbId.': '.$e->getMessage()); } - // TMDB + try { $tmdb = $this->fetchTMDBProperties($imdbId); if (! empty($tmdb['cover'])) { @@ -1717,7 +1360,7 @@ class Movie } catch (\Throwable $e) { Log::debug('TMDB cover fetch failed for '.$imdbId.': '.$e->getMessage()); } - // IMDB + try { $imdb = $this->fetchIMDBProperties($imdbId); if (! empty($imdb['cover'])) { @@ -1728,7 +1371,7 @@ class Movie } catch (\Throwable $e) { Log::debug('IMDB cover fetch failed for '.$imdbId.': '.$e->getMessage()); } - // OMDB + try { $omdb = $this->fetchOmdbAPIProperties($imdbId); if (! empty($omdb['cover'])) { @@ -1743,3 +1386,4 @@ class Movie return false; } } + diff --git a/app/Services/MoviesProcessor.php b/app/Services/MoviesProcessor.php index 7ba302519..e9ecc1f2c 100644 --- a/app/Services/MoviesProcessor.php +++ b/app/Services/MoviesProcessor.php @@ -3,7 +3,6 @@ namespace App\Services; use App\Models\Settings; -use Blacklight\Movie; use GuzzleHttp\Exception\GuzzleException; class MoviesProcessor @@ -24,7 +23,7 @@ class MoviesProcessor { $processMovies = (is_numeric($processMovies) ? $processMovies : Settings::settingValue('lookupimdb')); if ($processMovies > 0) { - (new Movie)->processMovieReleases($groupID, $guidChar, $processMovies); + (new MovieService)->processMovieReleases($groupID, $guidChar, $processMovies); } } } diff --git a/misc/testing/PostProc/check_covers.php b/misc/testing/PostProc/check_covers.php index 34b1f714d..c3730cd3b 100644 --- a/misc/testing/PostProc/check_covers.php +++ b/misc/testing/PostProc/check_covers.php @@ -5,11 +5,12 @@ // -------------------------------------------------------------- require_once dirname(__DIR__, 3).DIRECTORY_SEPARATOR.'bootstrap/autoload.php'; +use App\Services\MovieService; use Blacklight\ColorCLI; -use Blacklight\Movie; use Illuminate\Support\Facades\DB; -$movie = new Movie(['Echo' => true]); +$movie = new MovieService(); +$movie->echooutput = true; $colorCli = new ColorCLI; $path2cover = storage_path('covers/movies/'); @@ -49,7 +50,7 @@ if (isset($argv[1]) && ($argv[1] === 'true' || $argv[1] === 'check')) { ."Releases without covers may be reset for post-processing, thus regenerating them and related meta data.\n\n" ."Useful for recovery after filesystem corruption, or as an alternative re-postprocessing tool.\n\n" ."Optional LIMIT parameter restricts number of releases to be reset.\n\n" - ."php $argv[0] check [LIMIT] ...: Dry run, displays missing covers.\n" - ."php $argv[0] true [LIMIT] ...: Re-process releases missing covers.\n"); - exit(); + ."php $argv[0] true ...:Sets all releases missing covers to be reset for post-processing.\n" + ."php $argv[0] check ...:Checks and displays all releases missing covers.\n" + ."php $argv[0] true 500 ...:Sets only 500 releases missing covers to be reset for post-processing.\n"); } diff --git a/misc/testing/PostProc/getImdb.php b/misc/testing/PostProc/getImdb.php index 1dc0d7776..c7e5192ed 100644 --- a/misc/testing/PostProc/getImdb.php +++ b/misc/testing/PostProc/getImdb.php @@ -3,12 +3,13 @@ // This script will update all records in the movieinfo table require_once dirname(__DIR__, 3).DIRECTORY_SEPARATOR.'bootstrap/autoload.php'; +use App\Services\MovieService; use Blacklight\ColorCLI; -use Blacklight\Movie; use Illuminate\Support\Facades\DB; $pdo = DB::connection()->getPdo(); -$movie = new Movie(['Echo' => true]); +$movie = new MovieService(); +$movie->echooutput = true; $colorCli = new ColorCLI; $movies = $pdo->query('SELECT imdbid FROM movieinfo WHERE tmdbid = 0 ORDER BY id ASC'); diff --git a/misc/testing/PostProc/getMovieCovers.php b/misc/testing/PostProc/getMovieCovers.php index 85d331b46..e31418e82 100644 --- a/misc/testing/PostProc/getMovieCovers.php +++ b/misc/testing/PostProc/getMovieCovers.php @@ -4,10 +4,11 @@ require_once dirname(__DIR__, 3).DIRECTORY_SEPARATOR.'bootstrap/autoload.php'; use App\Models\MovieInfo; +use App\Services\MovieService; use Blacklight\ColorCLI; -use Blacklight\Movie; -$movie = new Movie(['Echo' => true]); +$movie = new MovieService(); +$movie->echooutput = true; $colorCli = new ColorCLI; $movies = MovieInfo::query()->where('cover', '=', 0)->orderBy('year')->orderByDesc('id')->get(['imdbid']); diff --git a/misc/testing/PostProc/getTraktData.php b/misc/testing/PostProc/getTraktData.php index 03b95bd6a..a8856a993 100644 --- a/misc/testing/PostProc/getTraktData.php +++ b/misc/testing/PostProc/getTraktData.php @@ -4,11 +4,12 @@ require_once dirname(__DIR__, 3).DIRECTORY_SEPARATOR.'bootstrap/autoload.php'; use App\Models\MovieInfo; +use App\Services\MovieService; +use App\Services\TvProcessing\Providers\TraktProvider; use Blacklight\ColorCLI; -use Blacklight\Movie; -use Blacklight\processing\tv\TraktTv; -$movie = new Movie(['Echo' => true]); +$movie = new MovieService(); +$movie->echooutput = true; $colorCli = new ColorCLI; $movies = MovieInfo::query()->where('imdbid', '<>', 0)->where('traktid', '=', 0)->get(['imdbid']); @@ -16,7 +17,7 @@ $count = $movies->count(); if ($count > 0) { $colorCli->primary('Updating '.number_format($count).' movies for TraktTV id.'); foreach ($movies as $mov) { - $traktTv = new TraktTv(['Settings' => null]); + $traktTv = new TraktProvider(); $traktmovie = $traktTv->client->getMovieSummary('tt'.$mov['imdbid'], 'full'); if ($traktmovie !== false) { $colorCli->info('Updating IMDb id: tt'.$mov['imdbid']);