diff --git a/app/Console/Commands/NntmuxResetPostProcessing.php b/app/Console/Commands/NntmuxResetPostProcessing.php index 12bb62552..fdc0616e5 100644 --- a/app/Console/Commands/NntmuxResetPostProcessing.php +++ b/app/Console/Commands/NntmuxResetPostProcessing.php @@ -79,7 +79,6 @@ class NntmuxResetPostProcessing extends Command 'bookinfo_id' => null, 'videos_id' => 0, 'tv_episodes_id' => 0, - 'xxxinfo_id' => 0, 'passwordstatus' => -1, 'haspreview' => -1, 'jpgstatus' => 0, @@ -137,7 +136,6 @@ class NntmuxResetPostProcessing extends Command $this->resetMusic(); break; case 'adult': - $this->resetAdult(); break; case 'tv': $this->resetTv(); @@ -323,29 +321,6 @@ class NntmuxResetPostProcessing extends Command } } - private function resetAdult(): void - { - $qry = Release::query()->whereNotNull('xxxinfo_id')->whereBetween('categories_id', [Category::XXX_ROOT, Category::XXX_OTHER])->get(); - $total = $qry->count(); - if ($total > 0) { - $bar = $this->output->createProgressBar($total); - $bar->setOverwrite(true); // Terminal needs to support ANSI Encoding for this? - $bar->start(); - foreach ($qry as $releases) { - Release::query()->where('id', $releases->id)->update( - [ - 'xxxinfo_id' => null, - ]); - $bar->advance(); - } - $bar->finish(); - $this->newLine(); - $this->info(number_format($total).' xxxinfo_id\'s reset.'); - } else { - $this->info('No releases to reset'); - } - } - private function resetTv(): void { $qry = Release::query()->where('videos_id', '!=', 0)->where('tv_episodes_id', '!=', 0)->whereBetween('categories_id', [Category::TV_ROOT, Category::TV_OTHER])->get(); diff --git a/app/Console/Commands/ProcessAdultMovies.php b/app/Console/Commands/ProcessAdultMovies.php deleted file mode 100644 index 169da9c76..000000000 --- a/app/Console/Commands/ProcessAdultMovies.php +++ /dev/null @@ -1,114 +0,0 @@ -error('Adult movie lookup is disabled in settings. Enable "lookupxxx" to use this command.'); - - return Command::FAILURE; - } - - $debug = $this->option('debug'); - $title = $this->option('title'); - $limit = $this->option('limit'); - - $pipeline = new AdultProcessingPipeline([], true); - - if ($title) { - // Process a single title - $this->info("Looking up: {$title}"); - - $result = $pipeline->processMovie($title, $debug); - - if ($result['status'] === 'matched') { - $this->info("Match found on {$result['provider']}!"); - $title_display = $result['movieData']['title'] ?? 'N/A'; - $synopsis_display = substr($result['movieData']['synopsis'] ?? 'N/A', 0, 200); - $this->line("Title: {$title_display}"); - $this->line("Synopsis: {$synopsis_display}..."); - - if (! empty($result['movieData']['boxcover'])) { - $cover_url = $result['movieData']['boxcover']; - $this->line("Cover: {$cover_url}"); - } - - if ($debug && ! empty($result['debug'])) { - $this->newLine(); - $this->line('Debug Info:'); - $this->line(json_encode($result['debug'], JSON_PRETTY_PRINT)); - } - } else { - $this->warn("No match found for: {$title}"); - - if ($debug && ! empty($result['debug'])) { - $this->newLine(); - $this->line('Debug Info:'); - $this->line(json_encode($result['debug'], JSON_PRETTY_PRINT)); - } - } - } else { - // Process all pending releases - $this->info('Processing adult movie releases using pipeline...'); - - if ($limit) { - $this->info("Limited to {$limit} releases"); - } - - $pipeline->processXXXReleases(); - - $stats = $pipeline->getStats(); - - $this->newLine(); - $this->table( - ['Metric', 'Value'], - [ - ['Processed', $stats['processed']], - ['Matched', $stats['matched']], - ['Failed', $stats['failed']], - ['Skipped', $stats['skipped']], - ['Duration', sprintf('%.2f seconds', $stats['duration'])], - ] - ); - - if (! empty($stats['providers'])) { - $this->newLine(); - $this->info('Provider Statistics:'); - $providerData = []; - foreach ($stats['providers'] as $provider => $count) { - $providerData[] = [$provider, $count]; - } - $this->table(['Provider', 'Matches'], $providerData); - } - } - - return Command::SUCCESS; - } -} diff --git a/app/Console/Commands/RecategorizeReleases.php b/app/Console/Commands/RecategorizeReleases.php index d68b7e646..e9b57b94b 100644 --- a/app/Console/Commands/RecategorizeReleases.php +++ b/app/Console/Commands/RecategorizeReleases.php @@ -87,7 +87,6 @@ class RecategorizeReleases extends Command 'gamesinfo_id' => 0, 'bookinfo_id' => 0, 'anidbid' => null, - 'xxxinfo_id' => 0, 'categories_id' => $catId['categories_id'], ]); diff --git a/app/Console/Commands/TmuxMonitor.php b/app/Console/Commands/TmuxMonitor.php index 625f3178c..7b277cb86 100644 --- a/app/Console/Commands/TmuxMonitor.php +++ b/app/Console/Commands/TmuxMonitor.php @@ -245,6 +245,5 @@ class TmuxMonitor extends Command $this->taskRunner->runPaneTask('tv', [], $runVar); $this->taskRunner->runPaneTask('movies', [], $runVar); $this->taskRunner->runPaneTask('amazon', [], $runVar); - $this->taskRunner->runPaneTask('xxx', [], $runVar); } } diff --git a/app/Console/Commands/UpdatePostProcess.php b/app/Console/Commands/UpdatePostProcess.php index 764dd22ed..7f96ea361 100644 --- a/app/Console/Commands/UpdatePostProcess.php +++ b/app/Console/Commands/UpdatePostProcess.php @@ -16,7 +16,7 @@ class UpdatePostProcess extends Command * @var string */ protected $signature = 'update:postprocess - {type : Type (all, nfo, movies, tv, music, console, games, book, anime, xxx, additional, amazon)} + {type : Type (all, nfo, movies, tv, music, console, games, book, anime, additional, amazon)} {echo? : Echo output (true/false, default: true)}'; /** @@ -43,7 +43,6 @@ class UpdatePostProcess extends Command 'music' => false, 'nfo' => true, 'tv' => false, - 'xxx' => false, ]; public function __construct( @@ -80,7 +79,6 @@ class UpdatePostProcess extends Command 'book' => $this->postProcessService->processBooks(), 'anime' => $this->postProcessService->processAnime(), 'tv' => $this->postProcessService->processTv(), - 'xxx' => $this->postProcessService->processXXX(), 'additional' => $this->postProcessService->processAdditional(), }; @@ -108,13 +106,12 @@ class UpdatePostProcess extends Command $this->line(' book - Processes books'); $this->line(' anime - Processes anime'); $this->line(' tv - Processes tv'); - $this->line(' xxx - Processes xxx'); $this->line(' additional - Processes previews/mediainfo/etc...'); - $this->line(' amazon - Processes books, music, console, games, and xxx'); + $this->line(' amazon - Processes books, music, console, and games'); } /** - * Process amazon types (books, music, console, games, xxx). + * Process amazon types (books, music, console, games). */ private function processAmazon(): void { @@ -122,7 +119,6 @@ class UpdatePostProcess extends Command $this->postProcessService->processMusic(); $this->postProcessService->processConsoles(); $this->postProcessService->processGames(); - $this->postProcessService->processXXX(); } /** diff --git a/app/Extensions/helper/age_verification_helpers.php b/app/Extensions/helper/age_verification_helpers.php deleted file mode 100644 index 5a4c3c802..000000000 --- a/app/Extensions/helper/age_verification_helpers.php +++ /dev/null @@ -1,186 +0,0 @@ -|false - */ - function getRawHtmlWithAgeVerification($url, $cookie = false, $postData = null) - { - static $ageVerificationManager = null; - - // Initialize manager on first use - if ($ageVerificationManager === null) { - $ageVerificationManager = new AgeVerificationManager; - } - - // Check if this is an adult site that needs age verification - $adultSites = [ - 'adultdvdempire.com', - 'adultdvdmarketplace.com', - 'aebn.net', - 'hotmovies.com', - 'popporn.com', - ]; - - $isAdultSite = false; - foreach ($adultSites as $site) { - if (stripos($url, $site) !== false) { - $isAdultSite = true; - break; - } - } - - // For adult sites, use the age verification manager - if ($isAdultSite) { - try { - $options = []; - - // Handle POST data if provided - if ($postData !== null) { - $options['form_params'] = $postData; - $cookieJar = $ageVerificationManager->getCookieJar($url); - - $client = new Client([ - 'cookies' => $cookieJar, - 'headers' => [ - 'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', - 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', - ], - ]); - - $response = $client->post($url, $options); - $html = $response->getBody()->getContents(); - } else { - $html = $ageVerificationManager->makeRequest($url, $options); - } - - // Try to decode as JSON (some APIs return JSON) - if ($html !== false) { - $jsonResponse = json_decode($html, true); - if (json_last_error() === JSON_ERROR_NONE) { - return $jsonResponse; - } - } - - return $html; - - } catch (RequestException $e) { - if (config('app.debug') === true) { - Log::error('getRawHtmlWithAgeVerification: '.$e->getMessage()); - } - - return false; - } catch (\Exception $e) { - if (config('app.debug') === true) { - Log::error('getRawHtmlWithAgeVerification: '.$e->getMessage()); - } - - return false; - } - } - - // For non-adult sites, use standard approach - return getRawHtml($url, $cookie, $postData); - } -} - -if (! function_exists('initializeAdultSiteCookies')) { - /** - * Initialize age verification cookies for all adult sites - * Run this once during application setup - * - * @return array Statistics about initialized cookies - */ - function initializeAdultSiteCookies(): array - { - $manager = new AgeVerificationManager; - - $sites = [ - 'https://www.adultdvdempire.com', - 'https://www.adultdvdmarketplace.com', - 'https://straight.theater.aebn.net', - 'https://www.hotmovies.com', - 'https://www.popporn.com', - ]; - - $results = [ - 'initialized' => 0, - 'failed' => 0, - 'sites' => [], - ]; - - foreach ($sites as $url) { - try { - // Making a request will automatically set up cookies - $cookieJar = $manager->getCookieJar($url); - $domain = parse_url($url, PHP_URL_HOST); - - $results['sites'][$domain] = [ - 'status' => 'initialized', - 'cookies' => count($cookieJar), - ]; - $results['initialized']++; - - } catch (\Exception $e) { - $domain = parse_url($url, PHP_URL_HOST); - $results['sites'][$domain] = [ - 'status' => 'failed', - 'error' => $e->getMessage(), - ]; - $results['failed']++; - } - } - - return $results; - } -} - -if (! function_exists('getAdultSiteCookieStats')) { - /** - * Get statistics about stored adult site cookies - * - * @return array Cookie statistics - */ - function getAdultSiteCookieStats(): array - { - $manager = new AgeVerificationManager; - - return $manager->getCookieStats(); - } -} - -if (! function_exists('clearAdultSiteCookies')) { - /** - * Clear all stored adult site cookies - * - * @param string|null $domain Optional specific domain to clear - * @return int|bool Number of cookies cleared (or bool for specific domain) - */ - function clearAdultSiteCookies(?string $domain = null) - { - $manager = new AgeVerificationManager; - - if ($domain !== null) { - return $manager->clearCookies($domain); - } - - return $manager->clearAllCookies(); - } -} diff --git a/app/Extensions/helper/helpers.php b/app/Extensions/helper/helpers.php index 754d96df1..0060ba6ed 100644 --- a/app/Extensions/helper/helpers.php +++ b/app/Extensions/helper/helpers.php @@ -2,7 +2,6 @@ use App\Models\Country as CountryModel; use App\Models\Release; -use App\Models\XxxInfo; use App\Services\Nzb\NzbService; use GuzzleHttp\Client; use GuzzleHttp\Cookie\CookieJar; @@ -39,45 +38,7 @@ if (! function_exists('getRawHtml')) { } } - // For adult sites, use age verification manager if available - if ($isAdultSite && class_exists('\App\Services\AdultProcessing\AgeVerificationManager')) { - try { - static $ageVerificationManager = null; - if ($ageVerificationManager === null) { - $ageVerificationManager = new \App\Services\AdultProcessing\AgeVerificationManager; - } - - if ($postData !== null) { - // Handle POST requests - $cookieJar = $ageVerificationManager->getCookieJar($url); - $client = new Client([ - 'cookies' => $cookieJar, - 'headers' => ['User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'], - ]); - - $response = $client->post($url, ['form_params' => $postData]); - $response = $response->getBody()->getContents(); - } else { - $response = $ageVerificationManager->makeRequest($url); - } - - if ($response !== false) { - $jsonResponse = json_decode($response, true); - if (json_last_error() === JSON_ERROR_NONE) { - return $jsonResponse; - } - - return $response; - } - } catch (\Exception $e) { - // Fall through to standard method - if (function_exists('config') && config('app.debug') === true) { - Log::error('Age verification failed, falling back to standard method: '.$e->getMessage()); - } - } - } - - // Standard method for non-adult sites or if age verification fails + // Standard method $cookieJar = new CookieJar; $client = new Client(['headers' => ['User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.246']]); if ($cookie !== false && $cookie !== null && $cookie !== '') { @@ -121,10 +82,6 @@ if (! function_exists('makeFieldLinks')) { if (trim($ta) === '') { continue; } - if ($type === 'xxx' && $field === 'genre') { - $ta = XxxInfo::getGenres(true, (int) $ta); - $ta = $ta['title'] ?? ''; - } if ($i > 7) { break; } @@ -492,7 +449,6 @@ if (! function_exists('getReleaseCover')) { $consoleinfo_id = $getValue($release, 'consoleinfo_id'); $bookinfo_id = $getValue($release, 'bookinfo_id'); $gamesinfo_id = $getValue($release, 'gamesinfo_id'); - $xxxinfo_id = $getValue($release, 'xxxinfo_id'); $anidbid = $getValue($release, 'anidbid'); if (! empty($imdbid) && $imdbid > 0) { @@ -510,9 +466,6 @@ if (! function_exists('getReleaseCover')) { } elseif (! empty($gamesinfo_id)) { $coverType = 'games'; $coverId = $gamesinfo_id; - } elseif (! empty($xxxinfo_id)) { - $coverType = 'xxx'; - $coverId = $xxxinfo_id; } elseif (! empty($anidbid) && $anidbid > 0) { $coverType = 'anime'; $coverId = $anidbid; @@ -970,9 +923,6 @@ if (! function_exists('getRange')) { function getRange(mixed $tableName): \Illuminate\Contracts\Pagination\LengthAwarePaginator // @phpstan-ignore missingType.generics { $range = \Illuminate\Support\Facades\DB::table($tableName); - if ($tableName === 'xxxinfo') { - $range->selectRaw('UNCOMPRESS(plot) AS plot'); - } return $range->orderByDesc('created_at')->paginate(config('nntmux.items_per_page')); } diff --git a/app/Http/Controllers/Admin/AdminUserController.php b/app/Http/Controllers/Admin/AdminUserController.php index 6dfbb5b7c..d7a74b75d 100644 --- a/app/Http/Controllers/Admin/AdminUserController.php +++ b/app/Http/Controllers/Admin/AdminUserController.php @@ -132,7 +132,6 @@ class AdminUserController extends BasePageController 'notes' => '', 'invites' => $defaultInvites, 'movieview' => 0, - 'xxxview' => 0, 'musicview' => 0, 'consoleview' => 0, 'gameview' => 0, @@ -236,7 +235,7 @@ class AdminUserController extends BasePageController ($request->has('movieview') ? 1 : 0), ($request->has('musicview') ? 1 : 0), ($request->has('gameview') ? 1 : 0), - ($request->has('xxxview') ? 1 : 0), + ($editedUser->xxxview ? 1 : 0), ($request->has('consoleview') ? 1 : 0), ($request->has('bookview') ? 1 : 0) ); diff --git a/app/Http/Controllers/AdultController.php b/app/Http/Controllers/AdultController.php index 51dd03e4f..6f5910e40 100644 --- a/app/Http/Controllers/AdultController.php +++ b/app/Http/Controllers/AdultController.php @@ -3,19 +3,18 @@ namespace App\Http\Controllers; use App\Models\Category; -use App\Models\XxxInfo; -use App\Services\XxxBrowseService; +use App\Services\Releases\ReleaseBrowseService; use Illuminate\Http\Request; use Illuminate\Support\Arr; class AdultController extends BasePageController { - private XxxBrowseService $xxxBrowseService; + private ReleaseBrowseService $releaseBrowseService; - public function __construct(XxxBrowseService $xxxBrowseService) + public function __construct(ReleaseBrowseService $releaseBrowseService) { parent::__construct(); - $this->xxxBrowseService = $xxxBrowseService; + $this->releaseBrowseService = $releaseBrowseService; } /** @@ -26,11 +25,10 @@ class AdultController extends BasePageController $moviecats = Category::getChildren(Category::XXX_ROOT); $mtmp = []; foreach ($moviecats as $mcat) { - $mtmp[] = - [ - 'id' => $mcat->id, - 'title' => $mcat->title, - ]; + $mtmp[] = [ + 'id' => $mcat->id, + 'title' => $mcat->title, + ]; } $category = $request->has('t') ? $request->input('t') : Category::XXX_ROOT; if ($id && \in_array($id, Arr::pluck($mtmp, 'title'), false)) { @@ -43,40 +41,24 @@ class AdultController extends BasePageController $catarray = []; $catarray[] = $category; - $ordering = $this->xxxBrowseService->getXXXOrdering(); + $ordering = $this->releaseBrowseService->getBrowseOrdering(); $orderby = $request->has('ob') && \in_array($request->input('ob'), $ordering, false) ? $request->input('ob') : ''; $page = $request->has('page') && is_numeric($request->input('page')) ? $request->input('page') : 1; $offset = ($page - 1) * config('nntmux.items_per_page'); - $rslt = $this->xxxBrowseService->getXXXRange($page, $catarray, $offset, config('nntmux.items_per_page'), $orderby, -1, $this->userdata['categoryexclusions']); // @phpstan-ignore argument.type - $results = $this->paginate($rslt, $rslt[0]->_totalcount ?? 0, config('nntmux.items_per_page'), $page, $request->url(), $request->query()); - - $title = ($request->has('title') && ! empty($request->input('title'))) ? stripslashes($request->input('title')) : ''; - - $actors = ($request->has('actors') && ! empty($request->input('actors'))) ? stripslashes($request->input('actors')) : ''; - - $director = ($request->has('director') && ! empty($request->input('director'))) ? stripslashes($request->input('director')) : ''; - - $genres = XxxInfo::getAllGenres(true); - $genre = ($request->has('genre') && \in_array($request->input('genre'), $genres, false)) ? $request->input('genre') : ''; - - $browseby_link = '&title='.$title.'&actors='.$actors.'&director='.$director.'&genre='.$genre; + $rslt = $this->releaseBrowseService->getBrowseRange($page, $catarray, $offset, config('nntmux.items_per_page'), $orderby, -1, (array) ($this->userdata->categoryexclusions ?? []), -1); + $results = $this->paginate($rslt ?? [], isset($rslt[0]->_totalcount) ? $rslt[0]->_totalcount : 0, config('nntmux.items_per_page'), $page, $request->url(), $request->query()); if ((int) $category === -1) { $catname = 'All'; } else { $cdata = Category::find($category); - if ($cdata !== null) { - $catname = $cdata->title; - } else { - $catname = 'All'; - } + $catname = $cdata !== null ? $cdata->title : 'All'; } - // Build order by URLs $orderByUrls = []; foreach ($ordering as $ordertype) { - $orderByUrls['orderby'.$ordertype] = url('/XXX/'.($id ?: 'All').'?t='.$category.$browseby_link.'&ob='.$ordertype.'&offset=0'); + $orderByUrls['orderby'.$ordertype] = url('/XXX/'.($id ?: 'All').'?t='.$category.'&ob='.$ordertype.'&offset=0'); } $this->viewData = array_merge($this->viewData, [ @@ -84,11 +66,7 @@ class AdultController extends BasePageController 'category' => $category, 'categorytitle' => $id, 'catname' => $catname, - 'title' => stripslashes($title), - 'actors' => $actors, - 'director' => $director, - 'genres' => $genres, - 'genre' => $genre, + 'ordering' => $ordering, 'results' => $results, 'lastvisit' => $this->userdata['lastlogin'] ?? null, 'meta_title' => 'Browse XXX', diff --git a/app/Http/Controllers/CoverController.php b/app/Http/Controllers/CoverController.php index b187a21f9..bbff32d8e 100644 --- a/app/Http/Controllers/CoverController.php +++ b/app/Http/Controllers/CoverController.php @@ -18,7 +18,7 @@ class CoverController extends Controller public function show(string $type, string $filename) { // Validate cover type - $validTypes = ['anime', 'audio', 'audiosample', 'book', 'console', 'games', 'movies', 'music', 'preview', 'sample', 'tvrage', 'video', 'xxx', 'tvshows']; + $validTypes = ['anime', 'audio', 'audiosample', 'book', 'console', 'games', 'movies', 'music', 'preview', 'sample', 'tvrage', 'video', 'tvshows']; if (! in_array($type, $validTypes)) { abort(404); diff --git a/app/Http/Controllers/DetailsController.php b/app/Http/Controllers/DetailsController.php index 174f9e77f..f76775dbc 100644 --- a/app/Http/Controllers/DetailsController.php +++ b/app/Http/Controllers/DetailsController.php @@ -13,7 +13,6 @@ use App\Models\ReleaseReport; use App\Models\Settings; use App\Models\UserDownload; use App\Models\Video; -use App\Models\XxxInfo; use App\Services\AnidbService; use App\Services\BookService; use App\Services\ConsoleService; @@ -22,7 +21,6 @@ use App\Services\MovieService; use App\Services\MusicService; use App\Services\ReleaseExtraService; use App\Services\Releases\ReleaseSearchService; -use App\Services\XxxBrowseService; use Illuminate\Http\Request; class DetailsController extends BasePageController @@ -31,20 +29,16 @@ class DetailsController extends BasePageController private MovieService $movieService; - private XxxBrowseService $xxxBrowseService; - private ReleaseExtraService $releaseExtraService; public function __construct( ReleaseSearchService $releaseSearchService, MovieService $movieService, - XxxBrowseService $xxxBrowseService, ReleaseExtraService $releaseExtraService ) { parent::__construct(); $this->releaseSearchService = $releaseSearchService; $this->movieService = $movieService; - $this->xxxBrowseService = $xxxBrowseService; $this->releaseExtraService = $releaseExtraService; } @@ -104,24 +98,6 @@ class DetailsController extends BasePageController } } - $xxx = ''; - if ($data['xxxinfo_id'] !== '' && $data['xxxinfo_id'] !== 0) { - $xxx = XxxInfo::getXXXInfo($data['xxxinfo_id']); - - if (isset($xxx['trailers'])) { - $xxx['trailers'] = $this->xxxBrowseService->insertSwf($xxx['classused'], $xxx['trailers']); - } - - if ($xxx && isset($xxx['title'])) { - $xxx['title'] = str_replace(['/', '\\'], '', $xxx['title']); - $xxx['actors'] = makeFieldLinks($xxx, 'actors', 'xxx'); - $xxx['genre'] = makeFieldLinks($xxx, 'genre', 'xxx'); - $xxx['director'] = makeFieldLinks($xxx, 'director', 'xxx'); - } else { - $xxx = false; - } - } - $game = ''; if (! empty($data['gamesinfo_id'])) { $game = (new GamesService)->getGamesInfoById($data['gamesinfo_id']); @@ -176,7 +152,6 @@ class DetailsController extends BasePageController 'nfo' => $nfo, 'show' => $showInfo, 'movie' => $mov, - 'xxx' => $xxx, 'anidb' => $AniDBAPIArray, 'music' => $mus, 'con' => $con, diff --git a/app/Http/Controllers/ProfileController.php b/app/Http/Controllers/ProfileController.php index f11b05715..7fceb95d7 100644 --- a/app/Http/Controllers/ProfileController.php +++ b/app/Http/Controllers/ProfileController.php @@ -149,7 +149,7 @@ class ProfileController extends BasePageController $request->has('movieview') ? 1 : 0, $request->has('musicview') ? 1 : 0, $request->has('gameview') ? 1 : 0, - $request->has('xxxview') ? 1 : 0, + $this->userdata->xxxview ? 1 : 0, $request->has('consoleview') ? 1 : 0, $request->has('bookview') ? 1 : 0, 'None', diff --git a/app/Models/XxxInfo.php b/app/Models/XxxInfo.php deleted file mode 100644 index 29f506350..000000000 --- a/app/Models/XxxInfo.php +++ /dev/null @@ -1,231 +0,0 @@ - - */ - protected $guarded = []; - - /** - * Get releases associated with this XXX info. - * - * @return \Illuminate\Database\Eloquent\Relations\HasMany<\App\Models\Release, $this> - */ - public function releases(): HasMany - { - return $this->hasMany(Release::class, 'xxxinfo_id'); - } - - /** - * Get info for a xxx id with decompressed plot. - */ - public static function getXXXInfo(int $xxxid): ?self - { - return self::query() - ->where('id', $xxxid) - ->selectRaw('*, UNCOMPRESS(plot) as plot') - ->first(); - } - - /** - * Update XXX Information. - */ - public static function updateXxxInfo( - string $id, - string $title = '', - string $tagLine = '', - string $plot = '', - string $genre = '', - string $director = '', - string $actors = '', - string $extras = '', - string $productInfo = '', - string $trailers = '', - string $directUrl = '', - string $classUsed = '', - string $cover = '', - string $backdrop = '' - ): void { - if (! empty($id)) { - self::query()->where('id', $id)->update([ - 'title' => $title, - 'tagline' => $tagLine, - 'plot' => "\x1f\x8b\x08\x00".gzcompress($plot), - 'genre' => substr($genre, 0, 64), - 'director' => $director, - 'actors' => $actors, - 'extras' => $extras, - 'productinfo' => $productInfo, - 'trailers' => $trailers, - 'directurl' => $directUrl, - 'classused' => $classUsed, - 'cover' => empty($cover) ? 0 : $cover, - 'backdrop' => empty($backdrop) ? 0 : $backdrop, - ]); - } - } - - /** - * Get all genres for search-filter.tpl. - * - * @return array - */ - public static function getAllGenres(bool $activeOnly = false): array - { - $ret = []; - if ($activeOnly) { - $res = Genre::query() - ->where(['disabled' => 0, 'type' => Category::XXX_ROOT]) - ->orderBy('title') - ->get(['title']) - ->toArray(); - } else { - $res = Genre::query() - ->where(['type' => Category::XXX_ROOT]) - ->orderBy('title') - ->get(['title']) - ->toArray(); - } - - return array_column($res, 'title'); - } - - /** - * Get a specific genre. - * - * @return list - * @return array - */ - public static function getGenres(bool $activeOnly = false, ?int $gid = null): mixed - { - if ($activeOnly) { - return Genre::query() - ->where(['disabled' => 0, 'type' => Category::XXX_ROOT]) - ->when($gid !== null, fn ($query) => $query->where('id', $gid)) - ->orderBy('title') - ->first(['title']); - } - - return Genre::query() - ->where(['type' => Category::XXX_ROOT]) - ->when($gid !== null, fn ($query) => $query->where('id', $gid)) - ->orderBy('title') - ->first(['title']); - } - - /** - * Get Genre id's of the title. - * - * @param array|string $arr - Array or String - * @return string - If array .. 1,2,3,4 if string .. 1 - */ - public static function getGenreID(array|string $arr): string - { - $ret = null; - - if (! \is_array($arr)) { - $res = Genre::query()->where('title', $arr)->first(['id']); - if ($res !== null) { - return (string) $res['id']; - } - - return ''; - } - - foreach ($arr as $key => $value) { - $res = Genre::query()->where('title', $value)->first(['id']); - if ($res !== null) { - $ret .= ','.$res['id']; - } else { - $ret .= ','.self::insertGenre($value); - } - } - - $ret = ltrim($ret, ','); - - return $ret; - } - - /** - * Inserts Genre and returns last affected row (Genre ID). - */ - private static function insertGenre(?string $genre): int|string - { - $res = ''; - if ($genre !== null) { - $res = Genre::query()->insertGetId([ - 'title' => $genre, - 'type' => Category::XXX_ROOT, - 'disabled' => 0, - ]); - } - - return $res; - } - - /** - * Checks xxxinfo to make sure releases exist. - */ - public static function checkXXXInfoExists(string $releaseName): ?self - { - return self::query() - ->where('title', 'like', '%'.$releaseName.'%') - ->first(['id', 'title']); - } -} diff --git a/app/Providers/AdultProcessingServiceProvider.php b/app/Providers/AdultProcessingServiceProvider.php deleted file mode 100644 index d1fa09aca..000000000 --- a/app/Providers/AdultProcessingServiceProvider.php +++ /dev/null @@ -1,52 +0,0 @@ -app->singleton(AebnPipe::class, fn () => new AebnPipe); - $this->app->singleton(IafdPipe::class, fn () => new IafdPipe); - $this->app->singleton(Data18Pipe::class, fn () => new Data18Pipe); - $this->app->singleton(PoppornPipe::class, fn () => new PoppornPipe); - $this->app->singleton(AdmPipe::class, fn () => new AdmPipe); - $this->app->singleton(AdePipe::class, fn () => new AdePipe); - $this->app->singleton(HotmoviesPipe::class, fn () => new HotmoviesPipe); - - // Register the pipeline with all default pipes - $this->app->singleton(AdultProcessingPipeline::class, function ($app) { - return new AdultProcessingPipeline([ - $app->make(AebnPipe::class), - $app->make(IafdPipe::class), - $app->make(Data18Pipe::class), - $app->make(PoppornPipe::class), - $app->make(AdmPipe::class), - $app->make(AdePipe::class), - $app->make(HotmoviesPipe::class), - ]); - }); - } - - /** - * Bootstrap services. - */ - public function boot(): void - { - // - } -} diff --git a/app/Services/AdultProcessing/AdultProcessingPassable.php b/app/Services/AdultProcessing/AdultProcessingPassable.php deleted file mode 100644 index 1918256c1..000000000 --- a/app/Services/AdultProcessing/AdultProcessingPassable.php +++ /dev/null @@ -1,99 +0,0 @@ - - */ - public array $providerResults = []; - - public ?string $cookie = null; - - public function __construct(AdultReleaseContext $context, bool $debug = false, ?string $cookie = null) - { - $this->context = $context; - $this->debug = $debug; - $this->cookie = $cookie; - $this->result = AdultProcessingResult::pending(); - } - - /** - * Check if we should stop processing (match found). - */ - public function shouldStopProcessing(): bool - { - return $this->result->isMatched(); - } - - /** - * Update the result from a provider. - */ - public function updateResult(AdultProcessingResult $result, string $providerName): void - { - if ($this->debug) { - $this->providerResults[$providerName] = [ - 'status' => $result->status, - 'title' => $result->title, - 'debug' => $result->debug, - ]; - } - - // Update if we got a match - if ($result->isMatched()) { - $this->result = $result; - } - } - - /** - * Get the clean title for searching. - */ - public function getCleanTitle(): string - { - return $this->context->cleanTitle; - } - - /** - * Get the cookie string. - */ - public function getCookie(): ?string - { - return $this->cookie; - } - - /** - * Build the final result array. - * - * @return array - */ - public function toArray(): array - { - $returnValue = [ - 'status' => $this->result->status, - 'title' => $this->result->title, - 'provider' => $this->result->providerName, - 'movieData' => $this->result->movieData, - ]; - - if ($this->debug) { - $returnValue['debug'] = [ - 'release_id' => $this->context->releaseId, - 'search_name' => $this->context->searchName, - 'clean_title' => $this->context->cleanTitle, - 'provider_results' => $this->providerResults, - ]; - } - - return $returnValue; - } -} diff --git a/app/Services/AdultProcessing/AdultProcessingPipeline.php b/app/Services/AdultProcessing/AdultProcessingPipeline.php deleted file mode 100644 index 945783d86..000000000 --- a/app/Services/AdultProcessing/AdultProcessingPipeline.php +++ /dev/null @@ -1,792 +0,0 @@ - - */ - protected Collection $pipes; // @phpstan-ignore missingType.generics - - protected int $movieQty; - - protected bool $echoOutput; - - protected ReleaseImageService $releaseImage; - - protected string $imgSavePath; - - protected string $cookie; - - protected string $showPasswords; - - /** - * Processing statistics. - * - * @var array - */ - protected array $stats = [ - 'processed' => 0, - 'matched' => 0, - 'failed' => 0, - 'skipped' => 0, - 'duration' => 0.0, - 'providers' => [], - ]; - - /** - * @param iterable $pipes - */ - public function __construct(iterable $pipes = [], bool $echoOutput = true) - { - /** @phpstan-ignore argument.templateType */ - $this->pipes = collect($pipes); - - if ($this->pipes->isEmpty()) { - $this->pipes = $this->getDefaultPipes(); - } - - $this->pipes = $this->pipes->sortBy(fn (AbstractAdultProviderPipe $p) => $p->getPriority()); - - // Try to get settings from database, but handle failures gracefully (e.g., in child processes) - try { - $this->movieQty = (int) (Settings::settingValue('maxxxxprocessed') ?? 100); - $this->showPasswords = app(\App\Services\Releases\ReleaseBrowseService::class)->showPasswords(); - } catch (\Exception $e) { - // Fallback values for child processes where DB might not be available - $this->movieQty = 100; - $this->showPasswords = ''; - } - - $this->echoOutput = $echoOutput; - $this->releaseImage = new ReleaseImageService; - $this->imgSavePath = storage_path('covers/xxx/'); - $this->cookie = resource_path('tmp/xxx.cookie'); - } - - /** - * Create a lightweight instance for child process use. - * This avoids database calls that may fail in forked processes. - */ - protected static function createForChildProcess(string $cookie, bool $echoOutput, string $imgSavePath): self - { - $instance = new self([], $echoOutput); - $instance->cookie = $cookie; - $instance->imgSavePath = $imgSavePath; - - return $instance; - } - - /** - * Get the default pipes in priority order. - * - * @return Collection - */ - protected function getDefaultPipes(): Collection - { - return collect([ - new AebnPipe, - new IafdPipe, - new Data18Pipe, - new PoppornPipe, - new AdmPipe, - new AdePipe, - new HotmoviesPipe, - ]); - } - - /** - * Add a provider pipe to the pipeline. - */ - public function addPipe(AbstractAdultProviderPipe $pipe): self - { - $this->pipes->push($pipe); - $this->pipes = $this->pipes->sortBy(fn (AbstractAdultProviderPipe $p) => $p->getPriority()); - - return $this; - } - - /** - * Process a single movie title through the pipeline. - * - * @param string $movie Movie title to search for - * @param bool $debug Whether to include debug information - * @return array Processing result - */ - public function processMovie(string $movie, bool $debug = false): array - { - $context = AdultReleaseContext::fromTitle($movie); - $passable = new AdultProcessingPassable($context, $debug, $this->cookie); - - // Set echo output on all pipes - foreach ($this->pipes as $pipe) { - $pipe->setEchoOutput($this->echoOutput); - } - - /** @var AdultProcessingPassable $result */ - $result = app(Pipeline::class) - ->send($passable) - ->through($this->pipes->values()->all()) - ->thenReturn(); - - return $result->toArray(); - } - - /** - * Process a single release through the pipeline. - * - * @param array|object $release Release data - * @param string $cleanTitle Cleaned movie title - * @param bool $debug Whether to include debug information - * @return array Processing result - */ - public function processRelease(array|object $release, string $cleanTitle, bool $debug = false): array - { - $context = AdultReleaseContext::fromRelease($release, $cleanTitle); - $passable = new AdultProcessingPassable($context, $debug, $this->cookie); - - // Set echo output on all pipes - foreach ($this->pipes as $pipe) { - $pipe->setEchoOutput($this->echoOutput); - } - - /** @var AdultProcessingPassable $result */ - $result = app(Pipeline::class) - ->send($passable) - ->through($this->pipes->values()->all()) - ->thenReturn(); - - return $result->toArray(); - } - - /** - * Process all XXX releases where xxxinfo_id is 0. - * - * Uses Laravel's native Concurrency facade for parallel processing when possible. - */ - public function processXXXReleases(): void - { - $startTime = microtime(true); - $this->resetStats(); - - try { - $releases = $this->getReleasesToProcess(); - $releaseCount = count($releases); - - if ($releaseCount === 0) { - if ($this->echoOutput) { - cli()->header('No XXX releases to process.'); - } - - return; - } - - if ($this->echoOutput) { - cli()->header('Processing '.$releaseCount.' XXX releases using pipeline.'); - } - - // Process releases in batches for parallel processing - $batchSize = min(5, $releaseCount); // Process up to 5 at a time - $batches = array_chunk($releases->toArray(), $batchSize); - - foreach ($batches as $batchIndex => $batch) { - if ($this->echoOutput) { - cli()->info('Processing batch '.($batchIndex + 1).' of '.count($batches)); - } - $this->processBatch($batch); // @phpstan-ignore argument.type - } - } catch (\Throwable $e) { - Log::error('processXXXReleases failed: '.$e->getMessage(), [ - 'exception' => get_class($e), - 'file' => $e->getFile(), - 'line' => $e->getLine(), - 'trace' => $e->getTraceAsString(), - ]); - - if ($this->echoOutput) { - cli()->error('Error during processing: '.$e->getMessage()); - } - } - - $this->stats['duration'] = microtime(true) - $startTime; - - if ($this->echoOutput) { - $this->outputStats(); - } - } - - /** - * Process a batch of releases using Laravel's native Concurrency for parallel execution. - * - * Note: Due to serialization limitations with DOMDocument and HtmlDomParser, - * we process releases sequentially within the batch but can process multiple - * batches concurrently using async tasks that create fresh instances. - * - * @param array $batch - */ - protected function processBatch(array $batch): void - { - // Check if we can use async processing - // For now, disable async to avoid child process issues with database connections - // TODO: Re-enable async when database connection pooling is properly configured - $useAsync = $this->canUseAsync() && config('nntmux.adult_processing_async', false); - - if (! $useAsync) { - // Fall back to sequential processing (more reliable) - foreach ($batch as $release) { - $releaseId = (int) ($release['id'] ?? 0); - - if ($releaseId <= 0) { - Log::warning('Invalid release ID in batch: '.json_encode($release)); - $this->stats['failed']++; - - continue; - } - - try { - $this->processReleaseItem($release); - } catch (\Throwable $e) { - // processReleaseItem should handle its own exceptions, but this is a fallback - Log::error('Unexpected error in processBatch for release '.$releaseId.': '.$e->getMessage()); - $this->stats['failed']++; - - // Ensure the release is marked as processed to avoid infinite loop - try { - $this->updateReleaseXxxId($releaseId, -2); - } catch (\Throwable $updateError) { - Log::error('Failed to update release '.$releaseId.' with error code: '.$updateError->getMessage()); - } - } - } - - return; - } - - // For async processing using Laravel's native Concurrency facade - // We create independent tasks with only serializable data - $cookie = $this->cookie; - $echoOutput = $this->echoOutput; - $imgSavePath = $this->imgSavePath; - - $tasks = []; - foreach ($batch as $idx => $release) { - // Extract only the serializable data needed - $releaseData = [ - 'id' => $release['id'], - 'searchname' => $release['searchname'], - ]; - - $tasks[$idx] = fn () => self::processReleaseInChildProcess($releaseData, $cookie, $echoOutput, $imgSavePath); - } - - try { - $results = Concurrency::run($tasks); - - // Update stats based on results - foreach ($results as $result) { - if (is_array($result)) { - if ($result['matched']) { - $this->stats['matched']++; - } - if (isset($result['provider'])) { - $this->stats['providers'][$result['provider']] = - ($this->stats['providers'][$result['provider']] ?? 0) + 1; - } - } - $this->stats['processed']++; - } - } catch (\Throwable $e) { - Log::error('Async batch processing failed: '.$e->getMessage()); - - // Mark all releases in batch as processed with error to avoid infinite loop - foreach ($batch as $release) { - $this->stats['failed']++; - try { - Release::query()->where('id', $release['id'])->update(['xxxinfo_id' => -2]); - } catch (\Throwable $updateError) { - Log::error('Failed to mark release '.$release['id'].' as processed: '.$updateError->getMessage()); - } - } - } - } - - /** - * Process a release in a child process with fresh instances. - * This is a static method to avoid serializing $this. - * - * @param array $releaseData - * @return array - */ - protected static function processReleaseInChildProcess( - array $releaseData, - string $cookie, - bool $echoOutput, - string $imgSavePath - ): array { - // Create fresh pipeline instance in child process - $pipeline = new self([], $echoOutput); - $pipeline->cookie = $cookie; - $pipeline->imgSavePath = $imgSavePath; - - $result = [ - 'id' => $releaseData['id'], - 'xxxinfo_id' => -2, - 'matched' => false, - 'provider' => null, - ]; - - $cleanTitle = $pipeline->parseXXXSearchName($releaseData['searchname']); - - if ($cleanTitle === false) { - Release::query()->where('id', $releaseData['id'])->update(['xxxinfo_id' => -2]); - - return $result; - } - - // Check if we already have this movie in the database - $existingInfo = $pipeline->checkXXXInfoExists($cleanTitle); - - if ($existingInfo !== null) { - $result['xxxinfo_id'] = (int) $existingInfo['id']; - $result['matched'] = true; - } else { - $xxxId = $pipeline->updateXXXInfo($cleanTitle, $releaseData); - $result['xxxinfo_id'] = $xxxId; - $result['matched'] = $xxxId > 0; - - // Get the provider from the result - if ($xxxId > 0) { - $info = XxxInfo::find($xxxId); - if ($info) { - $result['provider'] = $info->classused; - } - } - } - - Release::query()->where('id', $releaseData['id'])->update(['xxxinfo_id' => $result['xxxinfo_id']]); - - return $result; - } - - /** - * Process a single release item. - * - * @param array $release - * @return int XXX info ID or error code - */ - protected function processReleaseItem(array $release): int - { - $idCheck = -2; - $releaseId = (int) ($release['id'] ?? 0); - - if ($releaseId <= 0) { - Log::warning('Invalid release ID in processReleaseItem'); - $this->stats['failed']++; - - return $idCheck; - } - - try { - $cleanTitle = $this->parseXXXSearchName($release['searchname'] ?? ''); - - if ($cleanTitle === false) { - if ($this->echoOutput) { - cli()->primary('.'); - } - $this->updateReleaseXxxId($releaseId, $idCheck); - $this->stats['skipped']++; - - return $idCheck; - } - - // Check if we already have this movie in the database - $existingInfo = $this->checkXXXInfoExists($cleanTitle); - - if ($existingInfo !== null) { - if ($this->echoOutput) { - cli()->info('Local match found for XXX Movie: '.$cleanTitle); - } - $idCheck = (int) $existingInfo['id']; - } else { - if ($this->echoOutput) { - cli()->info('Looking up: '.$cleanTitle); - cli()->info('Local match not found, checking web!'); - } - - $idCheck = $this->updateXXXInfo($cleanTitle, $release); - - // If updateXXXInfo returned false, treat as -2 - if ($idCheck === false) { - $idCheck = -2; - } - } - - $this->updateReleaseXxxId($releaseId, $idCheck); - $this->stats['processed']++; - - if ($idCheck > 0) { - $this->stats['matched']++; - } - } catch (\Throwable $e) { - Log::error('Processing failed for release '.$releaseId.': '.$e->getMessage(), [ - 'exception' => get_class($e), - 'file' => $e->getFile(), - 'line' => $e->getLine(), - 'trace' => $e->getTraceAsString(), - ]); - - // Still mark as processed with error code to avoid infinite loop - try { - $this->updateReleaseXxxId($releaseId, -2); - } catch (\Throwable $updateError) { - Log::error('Failed to update release '.$releaseId.' after processing error: '.$updateError->getMessage()); - } - $this->stats['failed']++; - } - - return $idCheck; - } - - /** - * Update XXX information from pipeline results. - * - * @param array $release - * @return int|false XXX info ID or false on failure - */ - public function updateXXXInfo(string $movie, ?array $release = null): int|false - { - $result = $this->processMovie($movie); - - if ($result['status'] !== AdultProcessingResult::STATUS_MATCHED) { - return -2; - } - - $providerName = $result['provider']; - $movieData = $result['movieData']; - - if ($this->echoOutput) { - $fromStr = match ($providerName) { - 'aebn' => 'Adult Entertainment Broadcast Network', - 'ade' => 'Adult DVD Empire', - 'pop' => 'PopPorn', - 'adm' => 'Adult DVD Marketplace', - 'hotm' => 'HotMovies', - default => $providerName, - }; - cli()->primary('Fetching XXX info from: '.$fromStr); - } - - // Track provider usage - $this->stats['providers'][$providerName] = ($this->stats['providers'][$providerName] ?? 0) + 1; - - // Prepare the movie data for database storage - $cast = ! empty($movieData['cast']) ? implode(',', (array) $movieData['cast']) : ''; - $genres = ! empty($movieData['genres']) ? $this->getGenreID($movieData['genres']) : ''; - - $mov = [ - 'trailers' => ! empty($movieData['trailers']) ? serialize($movieData['trailers']) : '', - 'extras' => ! empty($movieData['extras']) ? serialize($movieData['extras']) : '', - 'productinfo' => ! empty($movieData['productinfo']) ? serialize($movieData['productinfo']) : '', - 'backdrop' => ! empty($movieData['backcover']) ? $movieData['backcover'] : 0, - 'cover' => ! empty($movieData['boxcover']) ? $movieData['boxcover'] : 0, - 'title' => ! empty($movieData['title']) ? html_entity_decode($movieData['title'], ENT_QUOTES, 'UTF-8') : '', - 'plot' => ! empty($movieData['synopsis']) ? html_entity_decode($movieData['synopsis'], ENT_QUOTES, 'UTF-8') : '', - 'tagline' => ! empty($movieData['tagline']) ? html_entity_decode($movieData['tagline'], ENT_QUOTES, 'UTF-8') : '', - 'genre' => ! empty($genres) ? html_entity_decode($genres, ENT_QUOTES, 'UTF-8') : '', - 'director' => ! empty($movieData['director']) ? html_entity_decode($movieData['director'], ENT_QUOTES, 'UTF-8') : '', - 'actors' => ! empty($cast) ? html_entity_decode($cast, ENT_QUOTES, 'UTF-8') : '', - 'directurl' => ! empty($movieData['directurl']) ? html_entity_decode($movieData['directurl'], ENT_QUOTES, 'UTF-8') : '', - 'classused' => $providerName, - ]; - - $cover = 0; - $backdrop = 0; - $xxxID = false; - - // Check if this movie already exists - $check = XxxInfo::query()->where('title', $mov['title'])->first(['id']); - - if ($check !== null && $check['id'] > 0) { - $xxxID = $check['id']; - - // Update BoxCover - if (! empty($mov['cover'])) { - $cover = $this->releaseImage->saveImage($xxxID.'-cover', $mov['cover'], $this->imgSavePath); - } - - // BackCover - if (! empty($mov['backdrop'])) { - $backdrop = $this->releaseImage->saveImage($xxxID.'-backdrop', $mov['backdrop'], $this->imgSavePath, 1920, 1024); - } - - // Update existing record - XxxInfo::query()->where('id', $check['id'])->update([ - 'title' => $mov['title'], - 'tagline' => $mov['tagline'], - 'plot' => "\x1f\x8b\x08\x00".gzcompress($mov['plot']), - 'genre' => substr($mov['genre'], 0, 64), - 'director' => $mov['director'], - 'actors' => $mov['actors'], - 'extras' => $mov['extras'], - 'productinfo' => $mov['productinfo'], - 'trailers' => $mov['trailers'], - 'directurl' => $mov['directurl'], - 'classused' => $mov['classused'], - 'cover' => empty($cover) ? 0 : $cover, - 'backdrop' => empty($backdrop) ? 0 : $backdrop, - ]); - } else { - // Insert new record - $xxxID = XxxInfo::query()->insertGetId([ - 'title' => $mov['title'], - 'tagline' => $mov['tagline'], - 'plot' => "\x1f\x8b\x08\x00".gzcompress($mov['plot']), - 'genre' => substr($mov['genre'], 0, 64), - 'director' => $mov['director'], - 'actors' => $mov['actors'], - 'extras' => $mov['extras'], - 'productinfo' => $mov['productinfo'], - 'trailers' => $mov['trailers'], - 'directurl' => $mov['directurl'], - 'classused' => $mov['classused'], - 'created_at' => now(), - 'updated_at' => now(), - ]); - - // Update BoxCover - if (! empty($mov['cover'])) { - $cover = $this->releaseImage->saveImage($xxxID.'-cover', $mov['cover'], $this->imgSavePath); - } - - // BackCover - if (! empty($mov['backdrop'])) { - $backdrop = $this->releaseImage->saveImage($xxxID.'-backdrop', $mov['backdrop'], $this->imgSavePath, 1920, 1024); - } - - XxxInfo::whereId($xxxID)->update(['cover' => $cover, 'backdrop' => $backdrop]); - } - - if ($this->echoOutput) { - cli()->primary( - ($xxxID !== false ? 'Added/updated XXX movie: '.$mov['title'] : 'Nothing to update for XXX movie: '.$mov['title']), - true - ); - } - - return $xxxID; - } - - /** - * Get releases to process. - * - * @return Collection - */ - protected function getReleasesToProcess(): \Illuminate\Database\Eloquent\Collection // @phpstan-ignore missingType.generics, return.phpDocType - { - return Release::query() - ->where(['xxxinfo_id' => 0]) - ->whereIn('categories_id', [ - Category::XXX_DVD, - Category::XXX_WMV, - Category::XXX_XVID, - Category::XXX_X264, - Category::XXX_SD, - Category::XXX_CLIPHD, - Category::XXX_CLIPSD, - Category::XXX_WEBDL, - Category::XXX_UHD, - Category::XXX_VR, - ]) - ->limit($this->movieQty) - ->get(['searchname', 'id']); - } - - /** - * Check if async processing can be used. - */ - protected function canUseAsync(): bool - { - return class_exists('Illuminate\Support\Facades\Concurrency') && - function_exists('pcntl_fork') && - ! defined('HHVM_VERSION'); - } - - /** - * Check if XXX info already exists in database. - * - * @return array - */ - protected function checkXXXInfoExists(string $releaseName): ?array - { - $result = XxxInfo::query()->where('title', 'like', '%'.$releaseName.'%')->first(['id', 'title']); - - return $result ? $result->toArray() : null; - } - - /** - * Update release with XXX info ID. - */ - protected function updateReleaseXxxId(int $releaseId, int $xxxInfoId): void - { - Release::query()->where('id', $releaseId)->update(['xxxinfo_id' => $xxxInfoId]); - } - - /** - * Get Genre ID from genre names. - * - * @param array $arr - */ - protected function getGenreID(array|string $arr): string - { - $ret = null; - - if (! is_array($arr)) { - $res = Genre::query()->where('title', $arr)->first(['id']); - if ($res !== null) { - return (string) $res['id']; - } - - return ''; - } - - foreach ($arr as $value) { - $res = Genre::query()->where('title', $value)->first(['id']); - if ($res !== null) { - $ret .= ','.$res['id']; - } else { - $ret .= ','.$this->insertGenre($value); - } - } - - return ltrim($ret, ','); - } - - /** - * Insert a new genre. - */ - protected function insertGenre(string $genre): int|string - { - return Genre::query()->insertGetId([ - 'title' => $genre, - 'type' => Category::XXX_ROOT, - 'disabled' => 0, - ]); - } - - /** - * Parse XXX search name from release name. - * - * @return string|false Cleaned title or false if couldn't parse - */ - protected function parseXXXSearchName(string $releaseName): string|false - { - $name = ''; - $followingList = '[^\w]((2160|1080|480|720)(p|i)|AC3D|Directors([^\w]CUT)?|DD5\.1|(DVD|BD|BR)(Rip)?|BluRay|divx|HDTV|iNTERNAL|LiMiTED|(Real\.)?Proper|RE(pack|Rip)|Sub\.?(fix|pack)|Unrated|WEB-DL|(x|H)[ ._-]?264|xvid|[Dd][Ii][Ss][Cc](\d+|\s*\d+|\.\d+)|XXX|BTS|DirFix|Trailer|WEBRiP|NFO|(19|20)\d\d)[^\w]'; - - if (preg_match('/([^\w]{2,})?(?P[\w .-]+?)'.$followingList.'/i', $releaseName, $hits)) { - $name = $hits['name']; - } - - if ($name !== '') { - $name = preg_replace('/'.$followingList.'/i', ' ', $name); - $name = preg_replace('/\(.*?\)|[._-]/i', ' ', $name); - $name = trim(preg_replace('/\s{2,}/', ' ', $name)); - $name = trim(preg_replace('/^Private\s(Specials|Blockbusters|Blockbuster|Sports|Gold|Lesbian|Movies|Classics|Castings|Fetish|Stars|Pictures|XXX|Private|Black\sLabel|Black)\s\d+/i', '', $name)); - $name = trim(preg_replace('/(brazilian|chinese|croatian|danish|deutsch|dutch|estonian|flemish|finnish|french|german|greek|hebrew|icelandic|italian|latin|nordic|norwegian|polish|portuguese|japenese|japanese|russian|serbian|slovenian|spanish|spanisch|swedish|thai|turkish)$/i', '', $name)); - - if (strlen($name) > 5 && - ! preg_match('/^\d+$/', $name) && - ! preg_match('/( File \d+ of \d+|\d+.\d+.\d+)/', $name) && - ! preg_match('/(E\d+)/', $name) && - ! preg_match('/\d\d\.\d\d.\d\d/', $name) - ) { - return $name; - } - } - - return false; - } - - /** - * Reset processing statistics. - */ - protected function resetStats(): void - { - $this->stats = [ - 'processed' => 0, - 'matched' => 0, - 'failed' => 0, - 'skipped' => 0, - 'duration' => 0.0, - 'providers' => [], - ]; - } - - /** - * Output processing statistics. - */ - protected function outputStats(): void - { - cli()->header("\n=== Adult Processing Statistics ==="); - cli()->primary('Processed: '.$this->stats['processed']); - cli()->primary('Matched: '.$this->stats['matched']); - cli()->primary('Failed: '.$this->stats['failed']); - cli()->primary('Skipped: '.$this->stats['skipped']); - cli()->primary(sprintf('Duration: %.2f seconds', $this->stats['duration'])); - - if (! empty($this->stats['providers'])) { - cli()->header("\nProvider Statistics:"); - foreach ($this->stats['providers'] as $provider => $count) { - cli()->primary(" {$provider}: {$count} matches"); - } - } - } - - /** - * Get processing statistics. - * - * @return array - */ - public function getStats(): array - { - return $this->stats; - } - - /** - * Get all registered pipes. - * - * @return Collection - */ - public function getPipes(): Collection - { - return $this->pipes; - } -} diff --git a/app/Services/AdultProcessing/AdultProcessingResult.php b/app/Services/AdultProcessing/AdultProcessingResult.php deleted file mode 100644 index d6c4fc0a3..000000000 --- a/app/Services/AdultProcessing/AdultProcessingResult.php +++ /dev/null @@ -1,146 +0,0 @@ - $debug - * @param array $movieData - */ - public function __construct( - public readonly string $status, - public readonly ?string $title = null, - public readonly ?string $providerName = null, - public readonly array $movieData = [], - public readonly array $debug = [], - ) {} - - /** - * Create a successful match result. - * - * @param array $debug - * @param array $movieData - */ - public static function matched( - string $title, - string $providerName, - array $movieData, - array $debug = [] - ): self { - return new self( - status: self::STATUS_MATCHED, - title: $title, - providerName: $providerName, - movieData: $movieData, - debug: $debug, - ); - } - - /** - * Create a not found result. - * - * @param array $debug - */ - public static function notFound(?string $providerName = null, array $debug = []): self - { - return new self( - status: self::STATUS_NOT_FOUND, - providerName: $providerName, - debug: $debug, - ); - } - - /** - * Create a failed result. - */ - public static function failed(string $reason = '', ?string $providerName = null): self - { - return new self( - status: self::STATUS_FAILED, - providerName: $providerName, - debug: ['reason' => $reason], - ); - } - - /** - * Create a skipped result. - */ - public static function skipped(string $reason = '', ?string $providerName = null): self - { - return new self( - status: self::STATUS_SKIPPED, - providerName: $providerName, - debug: ['reason' => $reason], - ); - } - - /** - * Create a pending result. - */ - public static function pending(): self - { - return new self(status: self::STATUS_PENDING); - } - - /** - * Check if the result is a successful match. - */ - public function isMatched(): bool - { - return $this->status === self::STATUS_MATCHED; - } - - /** - * Check if processing should continue to the next provider. - */ - public function shouldContinueProcessing(): bool - { - return ! $this->isMatched(); - } - - /** - * Get the box cover URL if available. - */ - public function getBoxCover(): ?string - { - return $this->movieData['boxcover'] ?? null; - } - - /** - * Get the back cover URL if available. - */ - public function getBackCover(): ?string - { - return $this->movieData['backcover'] ?? null; - } - - /** - * Get the synopsis if available. - */ - public function getSynopsis(): ?string - { - return $this->movieData['synopsis'] ?? null; - } - - /** - * Get the direct URL if available. - */ - public function getDirectUrl(): ?string - { - return $this->movieData['directurl'] ?? null; - } -} diff --git a/app/Services/AdultProcessing/AdultReleaseContext.php b/app/Services/AdultProcessing/AdultReleaseContext.php deleted file mode 100644 index 98818a209..000000000 --- a/app/Services/AdultProcessing/AdultReleaseContext.php +++ /dev/null @@ -1,46 +0,0 @@ - $release - */ - public static function fromRelease(array|object $release, string $cleanTitle): self - { - $data = is_array($release) ? $release : (array) $release; - - return new self( - releaseId: (int) ($data['id'] ?? 0), - searchName: $data['searchname'] ?? '', - cleanTitle: $cleanTitle, - guid: $data['guid'] ?? null, - ); - } - - /** - * Create from a movie title string. - */ - public static function fromTitle(string $title): self - { - return new self( - releaseId: 0, - searchName: $title, - cleanTitle: $title, - guid: null, - ); - } -} diff --git a/app/Services/AdultProcessing/AgeVerificationManager.php b/app/Services/AdultProcessing/AgeVerificationManager.php deleted file mode 100644 index 9353e5d39..000000000 --- a/app/Services/AdultProcessing/AgeVerificationManager.php +++ /dev/null @@ -1,397 +0,0 @@ - - */ - private array $cookieJars = []; - - /** - * Site-specific age verification configurations - * - * @var array - */ - private array $siteConfigs = [ - 'adultdvdempire.com' => [ - 'cookies' => [ - ['name' => 'age_verified', 'value' => '1'], - ['name' => 'age_gate_passed', 'value' => 'true'], - ['name' => 'over18', 'value' => '1'], - ['name' => 'ageConfirmed', 'value' => 'true'], - ['name' => 'isAdult', 'value' => 'true'], - ], - 'detection' => ['ageConfirmationButton', 'age-confirmation', 'confirm you are over'], - ], - 'adultdvdmarketplace.com' => [ - 'cookies' => [ - ['name' => 'age_verified', 'value' => '1'], - ['name' => 'over18', 'value' => 'yes'], - ['name' => 'ageConfirmed', 'value' => 'true'], - ], - 'detection' => ['age verification', 'are you 18'], - ], - 'straight.theater.aebn.net' => [ - 'cookies' => [ - ['name' => 'age_verified', 'value' => '1'], - ['name' => 'aebn_age_check', 'value' => 'passed'], - ['name' => 'over18', 'value' => 'true'], - ], - 'detection' => ['age verification', 'over 18'], - ], - 'hotmovies.com' => [ - 'cookies' => [ - ['name' => 'age_verified', 'value' => '1'], - ['name' => 'over18', 'value' => 'true'], - ['name' => 'ageConfirmed', 'value' => 'true'], - ['name' => 'hmAgeVerified', 'value' => '1'], - ], - 'detection' => ['age verification', 'enter'], - ], - 'popporn.com' => [ - 'cookies' => [ - ['name' => 'age_verified', 'value' => '1'], - ['name' => 'over_18', 'value' => 'yes'], - ['name' => 'ageVerified', 'value' => 'true'], - ['name' => 'popAgeConfirmed', 'value' => '1'], - ['name' => 'adultConsent', 'value' => 'true'], - // Note: etoken is dynamically set by the server, we can't pre-set it - ], - 'detection' => ['age verification', 'over 18', 'AgeConfirmation'], - 'redirectBypass' => true, // Indicates this site uses redirect-based verification - ], - ]; - - /** - * Constructor - */ - public function __construct(?string $cookieDir = null) - { - if ($cookieDir !== null) { - $this->cookieDir = $cookieDir; - } else { - $this->cookieDir = storage_path('app/cookies/adult_sites'); - } - - // Create cookie directory if it doesn't exist - if (! is_dir($this->cookieDir)) { - mkdir($this->cookieDir, 0755, true); - } - } - - /** - * Get cookie jar for a specific site - * Loads existing cookies or creates new jar - */ - public function getCookieJar(string $url): FileCookieJar - { - $domain = $this->extractDomain($url); - - if (! isset($this->cookieJars[$domain])) { - $cookieFile = $this->getCookieFilePath($domain); - $this->cookieJars[$domain] = new FileCookieJar($cookieFile, true); - - // If cookie file is new or empty, set age verification cookies - if (! file_exists($cookieFile) || filesize($cookieFile) < 10) { - $this->setAgeVerificationCookies($domain, $this->cookieJars[$domain]); - } - } - - return $this->cookieJars[$domain]; - } - - /** - * Make HTTP request with automatic age verification handling - * - * @param array $options - */ - public function makeRequest(string $url, array $options = []): string|false - { - $domain = $this->extractDomain($url); - $cookieJar = $this->getCookieJar($url); - - $client = new Client([ - 'cookies' => $cookieJar, - 'headers' => [ - 'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', - 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', - 'Accept-Language' => 'en-US,en;q=0.5', - 'Accept-Encoding' => 'gzip, deflate, br', - 'DNT' => '1', - 'Connection' => 'keep-alive', - 'Upgrade-Insecure-Requests' => '1', - ], - 'allow_redirects' => true, - 'verify' => false, // For development; set to true in production - ]); - - try { - $response = $client->get($url, $options); - $html = $response->getBody()->getContents(); - - // Check if we got an age verification page - if ($this->isAgeVerificationPage($html, $domain)) { - // Set age verification cookies and retry - $this->setAgeVerificationCookies($domain, $cookieJar); - $cookieJar->save($this->getCookieFilePath($domain)); - - // Retry request with new cookies - $response = $client->get($url, $options); - $html = $response->getBody()->getContents(); - } - - return $html; - - } catch (\Exception $e) { - error_log("Age Verification Manager: Request failed for {$url}: ".$e->getMessage()); - - return false; - } - } - - /** - * Check if response is an age verification page - */ - private function isAgeVerificationPage(string $html, string $domain): bool - { - if (! isset($this->siteConfigs[$domain])) { - return false; - } - - $detection = $this->siteConfigs[$domain]['detection']; - $htmlLower = strtolower($html); - - foreach ($detection as $keyword) { - if (stripos($htmlLower, strtolower($keyword)) !== false) { - return true; - } - } - - return false; - } - - /** - * Set age verification cookies for a domain - */ - private function setAgeVerificationCookies(string $domain, CookieJar $cookieJar): void - { - if (! isset($this->siteConfigs[$domain])) { - return; - } - - $cookies = $this->siteConfigs[$domain]['cookies']; - $expiry = time() + (365 * 24 * 60 * 60); // 1 year - - foreach ($cookies as $cookieData) { - $cookie = new SetCookie([ - 'Name' => $cookieData['name'], - 'Value' => $cookieData['value'], - 'Domain' => '.'.$domain, - 'Path' => '/', - 'Expires' => $expiry, - 'Secure' => true, - 'HttpOnly' => false, - ]); - - $cookieJar->setCookie($cookie); - } - - // Save cookies to file - if ($cookieJar instanceof FileCookieJar) { - $cookieJar->save($this->getCookieFilePath($domain)); - } - } - - /** - * Extract domain from URL - */ - private function extractDomain(string $url): string - { - $parsed = parse_url($url); - $host = $parsed['host'] ?? ''; - - // Remove www. prefix if present - $host = preg_replace('/^www\./', '', $host); - - // Map known subdomains to main domains - $domainMap = [ - 'straight.theater.aebn.net' => 'straight.theater.aebn.net', - 'adultdvdempire.com' => 'adultdvdempire.com', - 'adultdvdmarketplace.com' => 'adultdvdmarketplace.com', - 'hotmovies.com' => 'hotmovies.com', - 'popporn.com' => 'popporn.com', - ]; - - return $domainMap[$host] ?? $host; - } - - /** - * Force refresh cookies for a domain. - */ - public function refreshCookies(string $url): FileCookieJar - { - $domain = $this->extractDomain($url); - $cookieFile = $this->getCookieFilePath($domain); - - // Clear existing cookie jar - if (isset($this->cookieJars[$domain])) { - unset($this->cookieJars[$domain]); - } - - // Delete old cookie file if exists - if (file_exists($cookieFile)) { - unlink($cookieFile); - } - - // Create new jar with fresh cookies - $this->cookieJars[$domain] = new FileCookieJar($cookieFile, true); - $this->setAgeVerificationCookies($domain, $this->cookieJars[$domain]); - - return $this->cookieJars[$domain]; - } - - /** - * Get cookie file path for a domain - */ - private function getCookieFilePath(string $domain): string - { - $safeName = preg_replace('/[^a-z0-9_-]/', '_', strtolower($domain)); - - return $this->cookieDir.'/'.$safeName.'_cookies.json'; - } - - /** - * Clear cookies for a specific domain - */ - public function clearCookies(string $domain): bool - { - $cookieFile = $this->getCookieFilePath($domain); - - if (file_exists($cookieFile)) { - unlink($cookieFile); - unset($this->cookieJars[$domain]); - - return true; - } - - return false; - } - - /** - * Clear all stored cookies - */ - public function clearAllCookies(): int - { - $cleared = 0; - $files = glob($this->cookieDir.'/*_cookies.json'); - - foreach ($files as $file) { - if (unlink($file)) { - $cleared++; - } - } - - $this->cookieJars = []; - - return $cleared; - } - - /** - * Get list of domains with stored cookies - * - * @return array - */ - public function getStoredDomains(): array - { - $domains = []; - $files = glob($this->cookieDir.'/*_cookies.json'); - - foreach ($files as $file) { - $basename = basename($file, '_cookies.json'); - $domain = str_replace('_', '.', $basename); - $domains[] = $domain; - } - - return $domains; - } - - /** - * Check if cookies exist for a domain - * - * @return list - */ - public function hasCookies(string $domain): bool - { - $cookieFile = $this->getCookieFilePath($domain); - - return file_exists($cookieFile) && filesize($cookieFile) > 10; - } - - /** - * Get cookie statistics - * - * @return array - */ - public function getCookieStats(): array - { - $stats = [ - 'total_domains' => 0, - 'total_cookies' => 0, - 'domains' => [], - ]; - - foreach ($this->siteConfigs as $domain => $config) { - if ($this->hasCookies($domain)) { - $cookieJar = $this->getCookieJar('https://'.$domain); - $cookieCount = count($cookieJar); - - $stats['domains'][$domain] = [ - 'has_cookies' => true, - 'cookie_count' => $cookieCount, - 'file' => $this->getCookieFilePath($domain), - ]; - - $stats['total_domains']++; - $stats['total_cookies'] += $cookieCount; - } else { - $stats['domains'][$domain] = [ - 'has_cookies' => false, - 'cookie_count' => 0, - ]; - } - } - - return $stats; - } - - /** - * Get the cookie directory path - */ - public function getCookieDirectory(): string - { - return $this->cookieDir; - } -} diff --git a/app/Services/AdultProcessing/Pipes/AbstractAdultProviderPipe.php b/app/Services/AdultProcessing/Pipes/AbstractAdultProviderPipe.php deleted file mode 100644 index b00b07369..000000000 --- a/app/Services/AdultProcessing/Pipes/AbstractAdultProviderPipe.php +++ /dev/null @@ -1,913 +0,0 @@ - - */ - protected static array $lastRequestTime = []; - - /** - * Cache duration for search results in minutes. - */ - protected int $cacheDuration = 60; - - /** - * Whether to use caching for this provider. - */ - protected bool $useCache = true; - - public function __construct() - { - // Lazy load HtmlDomParser to avoid serialization issues - } - - /** - * Get the HtmlDomParser instance (lazy loaded). - */ - protected function getHtmlParser(): HtmlDomParser - { - if ($this->html === null) { - $this->html = new HtmlDomParser; - } - - return $this->html; - } - - /** - * Handle the adult movie processing request. - */ - public function handle(AdultProcessingPassable $passable, Closure $next): AdultProcessingPassable - { - // If we already have a match, skip processing - if ($passable->shouldStopProcessing()) { - return $next($passable); - } - - // Set the cookie from passable - $this->cookie = $passable->getCookie(); - - // Skip if this provider shouldn't process - if ($this->shouldSkip($passable)) { - $passable->updateResult( - AdultProcessingResult::skipped('Provider skipped', $this->getName()), - $this->getName() - ); - - return $next($passable); - } - - // Output processing message - if ($this->echoOutput) { - cli()->info('Checking '.$this->getDisplayName().' for movie info'); - } - - try { - // Apply rate limiting - $this->applyRateLimit(); - - // Attempt to process with this provider - $result = $this->process($passable); - - // Update the result - $passable->updateResult($result, $this->getName()); - } catch (\Exception $e) { - Log::error('Adult provider '.$this->getName().' failed: '.$e->getMessage(), [ - 'provider' => $this->getName(), - 'title' => $passable->getCleanTitle(), - 'exception' => get_class($e), - ]); - - $passable->updateResult( - AdultProcessingResult::failed($e->getMessage(), $this->getName()), - $this->getName() - ); - } - - return $next($passable); - } - - /** - * Apply rate limiting between requests to the same provider. - */ - protected function applyRateLimit(): void - { - $providerName = $this->getName(); - $now = microtime(true) * 1000; - - if (isset(self::$lastRequestTime[$providerName])) { - $elapsed = $now - self::$lastRequestTime[$providerName]; - if ($elapsed < $this->rateLimitDelay) { - usleep((int) (($this->rateLimitDelay - $elapsed) * 1000)); - } - } - - self::$lastRequestTime[$providerName] = microtime(true) * 1000; - } - - /** - * Get the priority of this provider (lower = higher priority). - */ - public function getPriority(): int - { - return $this->priority; - } - - /** - * Get the internal name of this provider. - */ - abstract public function getName(): string; - - /** - * Get the display name for user-facing output. - */ - abstract public function getDisplayName(): string; - - /** - * Get the base URL for the provider. - */ - abstract protected function getBaseUrl(): string; - - /** - * Attempt to process the movie through this provider. - */ - abstract protected function process(AdultProcessingPassable $passable): AdultProcessingResult; - - /** - * Search for a movie on this provider. - * - * @return array|false Returns array with 'title' and 'url' keys on success, false on failure - */ - abstract protected function search(string $movie): array|false; - - /** - * Get all movie information from the provider. - * - * @return array - */ - abstract protected function getMovieInfo(): array|false; - - /** - * Check if this provider should be skipped for the given passable. - */ - protected function shouldSkip(AdultProcessingPassable $passable): bool - { - return empty($passable->getCleanTitle()); - } - - /** - * Set echo output flag. - */ - public function setEchoOutput(bool $echo): self - { - $this->echoOutput = $echo; - - return $this; - } - - /** - * Get cached search result if available. - * - * @return array - */ - protected function getCachedSearch(string $movie): array|false|null - { - if (! $this->useCache) { - return null; - } - - $cacheKey = 'adult_search_'.$this->getName().'_'.md5(strtolower($movie)); - $cached = Cache::get($cacheKey); - - if ($cached !== null) { - if ($this->echoOutput) { - cli()->info('Using cached result for: '.$movie); - } - - return $cached; - } - - return null; - } - - /** - * Cache a search result. - * - * @param array $result - */ - protected function cacheSearchResult(string $movie, array|false $result): void - { - if (! $this->useCache) { - return; - } - - $cacheKey = 'adult_search_'.$this->getName().'_'.md5(strtolower($movie)); - Cache::put($cacheKey, $result, now()->addMinutes($this->cacheDuration)); - } - - /** - * Fetch raw HTML from a URL with retry support. - * - * @param array $postData - */ - protected function fetchHtml(string $url, ?string $cookie = null, ?array $postData = null): string|false - { - $attempt = 0; - $lastException = null; - $ageVerificationAttempted = false; - - while ($attempt < $this->maxRetries) { - try { - $attempt++; - $client = $this->getHttpClient(); - - $options = [ - 'headers' => $this->getDefaultHeaders(), - ]; - - // Add custom cookie if provided - if ($cookie) { - $options['headers']['Cookie'] = $cookie; - } - - // Handle POST data - if ($postData !== null) { - $options['form_params'] = $postData; - $response = $client->post($url, $options); - } else { - $response = $client->get($url, $options); - } - - $body = $response->getBody()->getContents(); - - // Check if we were redirected to an age verification page - $finalUrl = $response->getHeaderLine('X-Guzzle-Redirect-History'); - if (empty($finalUrl)) { - // Use the effective URI if available - $effectiveUri = $response->getHeader('X-Guzzle-Redirect-History'); - if (! empty($effectiveUri)) { - $finalUrl = end($effectiveUri); - } - } - - // Check for common error pages - if ($this->isErrorPage($body)) { - Log::warning('Received error page from '.$this->getName().': '.$url); - if ($attempt < $this->maxRetries) { - usleep($this->retryDelay * 1000); - - continue; - } - - return false; - } - - // Check for age verification requirement - if ($this->requiresAgeVerification($body)) { - // If we haven't tried age verification yet, refresh cookies and retry - if (! $ageVerificationAttempted) { - $ageVerificationAttempted = true; - - // Refresh cookies using the manager - $this->getAgeVerificationManager()->refreshCookies($this->getBaseUrl()); - - // Reset HTTP client to pick up new cookies - $this->httpClient = null; - $this->cookieJar = null; - - Log::info('Refreshed age verification cookies for '.$this->getName().', retrying...'); - - continue; - } - - $body = $this->handleAgeVerification($url, $body); - if ($body === false) { - return false; - } - } - - return $body; - - } catch (ConnectException $e) { - $lastException = $e; - Log::warning('Connection failed for '.$this->getName().' (attempt '.$attempt.'): '.$e->getMessage()); - - if ($attempt < $this->maxRetries) { - usleep($this->retryDelay * 1000 * $attempt); // Exponential backoff - } - } catch (RequestException $e) { - $lastException = $e; - $statusCode = $e->hasResponse() ? $e->getResponse()->getStatusCode() : 0; - - // Don't retry on 4xx client errors (except 429 rate limit) - if ($statusCode >= 400 && $statusCode < 500 && $statusCode !== 429) { - Log::error('HTTP '.$statusCode.' for '.$this->getName().': '.$url); - - return false; - } - - Log::warning('Request failed for '.$this->getName().' (attempt '.$attempt.'): '.$e->getMessage()); - - if ($attempt < $this->maxRetries) { - // Longer delay for rate limit errors - $delay = $statusCode === 429 ? $this->retryDelay * 5 : $this->retryDelay * $attempt; - usleep($delay * 1000); - } - } catch (\Exception $e) { - $lastException = $e; - Log::error('Unexpected error for '.$this->getName().': '.$e->getMessage()); - - if ($attempt < $this->maxRetries) { - usleep($this->retryDelay * 1000); - } - } - } - - if ($lastException) { - Log::error('All retry attempts failed for '.$this->getName().': '.$lastException->getMessage()); - } - - return false; - } - - /** - * Get default HTTP headers. - * - * @return array - */ - protected function getDefaultHeaders(): array - { - return [ - 'User-Agent' => $this->getRandomUserAgent(), - 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8', - 'Accept-Language' => 'en-US,en;q=0.9', - 'Accept-Encoding' => 'gzip, deflate, br', - 'Cache-Control' => 'no-cache', - 'Pragma' => 'no-cache', - 'Sec-Fetch-Dest' => 'document', - 'Sec-Fetch-Mode' => 'navigate', - 'Sec-Fetch-Site' => 'none', - 'Sec-Fetch-User' => '?1', - 'Upgrade-Insecure-Requests' => '1', - ]; - } - - /** - * Get a random user agent string. - */ - protected function getRandomUserAgent(): string - { - $userAgents = [ - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36', - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0', - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15', - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0', - ]; - - return $userAgents[array_rand($userAgents)]; - } - - /** - * Check if the response is an error page. - */ - protected function isErrorPage(string $html): bool - { - $errorPatterns = [ - 'Access Denied', - 'Service Unavailable', - '503 Service', - '502 Bad Gateway', - 'temporarily unavailable', - 'maintenance mode', - 'rate limit exceeded', - ]; - - foreach ($errorPatterns as $pattern) { - if (stripos($html, $pattern) !== false) { - return true; - } - } - - return false; - } - - /** - * Check if the page requires age verification. - */ - protected function requiresAgeVerification(string $html): bool - { - // First check if this looks like a proper content page - // Content pages have actual movie info, cast, etc. - $contentIndicators = [ - '.*?DVD.*?', - 'product-info', - 'movie-details', - 'cast-list', - 'genre-list', - '"@type":\s*"Movie"', - '"@type":\s*"Product"', - ]; - - foreach ($contentIndicators as $pattern) { - // Note: Using # as delimiter to avoid issues with / in patterns like - if (preg_match('#'.$pattern.'#is', $html)) { - return false; // This is a content page, not an age verification page - } - } - - // Check for short page that might just be a redirect/age gate - if (strlen($html) < 500) { - return true; // Very short response likely means we got redirected - } - - // Now check for explicit age verification indicators - $agePatterns = [ - 'age verification', - 'are you 18', - 'are you over 18', - 'confirm your age', - 'enter your age', - 'must be 18', - 'age-gate', - 'ageGate', - 'AgeConfirmation', // PopPorn specific - 'ageConfirmationButton', // ADE specific - 'age-confirmation', // Generic - 'verify your age', - 'adult content warning', - 'I am 18 or older', - 'I am over 18', - 'this site contains adult', - ]; - - // Count how many patterns match - if multiple match on a short page, it's likely age verification - $matchCount = 0; - foreach ($agePatterns as $pattern) { - if (stripos($html, $pattern) !== false) { - $matchCount++; - // If the page is relatively short and has an age pattern, it's probably an age gate - if (strlen($html) < 10000) { - return true; - } - } - } - - // If multiple patterns match, it's likely an age verification page - return $matchCount >= 2; - } - - /** - * Handle age verification requirement. - */ - protected function handleAgeVerification(string $url, string $html): string|false - { - // First, try to use site-specific cookies from the AgeVerificationManager - $manager = $this->getAgeVerificationManager(); - $domain = parse_url($this->getBaseUrl(), PHP_URL_HOST); - $domain = preg_replace('/^www\./', '', $domain); - - // Re-initialize cookies from the manager and retry - if ($this->cookieJar) { - // The manager already handles setting cookies, but let's ensure they're fresh - Log::info('Attempting to handle age verification for '.$this->getName().' with domain: '.$domain); - } - - // Try to find and submit age verification form - $this->getHtmlParser()->loadHtml($html); - - // Look for common age verification form patterns - $forms = $this->getHtmlParser()->find('form'); - foreach ($forms as $form) { - $action = $form->action ?? ''; - $method = strtoupper($form->method ?? 'GET'); - - // Check if this looks like an age verification form - $formHtml = $form->innerHtml ?? ''; - if (stripos($formHtml, 'age') !== false || stripos($formHtml, '18') !== false || - stripos($formHtml, 'adult') !== false || stripos($formHtml, 'enter') !== false || - stripos($formHtml, 'confirm') !== false) { - // Try to submit the form with age confirmation - $postData = $this->extractAgeVerificationFormData($form); - - if (! empty($postData)) { - $submitUrl = $action; - if (! str_starts_with($submitUrl, 'http')) { - $submitUrl = $this->getBaseUrl().'/'.ltrim($submitUrl, '/'); - } - - // Submit the age verification - try { - $response = $this->getHttpClient()->request($method, $submitUrl, [ - 'form_params' => $postData, - 'headers' => $this->getDefaultHeaders(), - ]); - - $body = $response->getBody()->getContents(); - - // Check if we still get age verification after submit - if (! $this->requiresAgeVerification($body)) { - return $body; - } - } catch (\Exception $e) { - Log::warning('Age verification submission failed for '.$this->getName().': '.$e->getMessage()); - } - } - } - } - - // Look for JavaScript-based age verification (click to enter) - if (preg_match('/onclick\s*=\s*["\'].*?(enter|agree|confirm|over18|adult).*?["\']/i', $html) || - preg_match('/]*href\s*=\s*["\']([^"\']*)["\'][^>]*>(Enter|I am over 18|Agree|Enter Site|I Agree)/i', $html, $matches)) { - // Try to follow the link or simulate the click - if (! empty($matches[1])) { - $enterUrl = $matches[1]; - if (! str_starts_with($enterUrl, 'http')) { - $enterUrl = $this->getBaseUrl().'/'.ltrim($enterUrl, '/'); - } - - try { - $response = $this->getHttpClient()->get($enterUrl, [ - 'headers' => $this->getDefaultHeaders(), - ]); - $body = $response->getBody()->getContents(); - - if (! $this->requiresAgeVerification($body)) { - return $body; - } - } catch (\Exception $e) { - Log::warning('Age verification link follow failed for '.$this->getName().': '.$e->getMessage()); - } - } - } - - // If all else fails, try to just refetch the original URL - // (sometimes the cookies from previous attempts work) - try { - $response = $this->getHttpClient()->get($url, [ - 'headers' => $this->getDefaultHeaders(), - ]); - $body = $response->getBody()->getContents(); - - if (! $this->requiresAgeVerification($body)) { - return $body; - } - } catch (\Exception $e) { - Log::warning('Age verification retry failed for '.$this->getName().': '.$e->getMessage()); - } - - // If we couldn't handle age verification, log and return false - Log::warning('Could not handle age verification for '.$this->getName().': '.$url); - - return false; - } - - /** - * Extract form data for age verification submission. - * - * @return array - */ - protected function extractAgeVerificationFormData(mixed $form): array - { - $data = []; - - // Get all input fields - foreach ($form->find('input') as $input) { - $name = $input->name ?? ''; - $type = strtolower($input->type ?? 'text'); - $value = $input->value ?? ''; - - if (empty($name)) { - continue; - } - - // Handle different input types - switch ($type) { - case 'hidden': - $data[$name] = $value; - break; - case 'checkbox': - // Usually age verification checkboxes need to be checked - if (stripos($name, 'age') !== false || stripos($name, 'agree') !== false || stripos($name, 'confirm') !== false) { - $data[$name] = $value ?: '1'; - } - break; - case 'submit': - // Include submit button value if it has a name - if (! empty($value)) { - $data[$name] = $value; - } - break; - default: - // For text inputs that might be age/birthdate - if (stripos($name, 'age') !== false || stripos($name, 'year') !== false) { - $data[$name] = '1990'; // Default to a valid birth year - } - } - } - - // Handle select elements (for birthdate selection) - foreach ($form->find('select') as $select) { - $name = $select->name ?? ''; - if (empty($name)) { - continue; - } - - if (stripos($name, 'year') !== false) { - $data[$name] = '1990'; - } elseif (stripos($name, 'month') !== false) { - $data[$name] = '01'; - } elseif (stripos($name, 'day') !== false) { - $data[$name] = '01'; - } - } - - return $data; - } - - /** - * Get the age verification manager instance. - */ - protected function getAgeVerificationManager(): AgeVerificationManager - { - if ($this->ageVerificationManager === null) { - $this->ageVerificationManager = new AgeVerificationManager; - } - - return $this->ageVerificationManager; - } - - /** - * Get or create HTTP client with retry middleware. - */ - protected function getHttpClient(): Client - { - if ($this->httpClient === null) { - // Use the AgeVerificationManager to get proper cookie jar with age verification cookies - $this->cookieJar = $this->getAgeVerificationManager()->getCookieJar($this->getBaseUrl()); - - $this->httpClient = new Client([ - 'timeout' => 30, - 'connect_timeout' => 15, - 'verify' => false, - 'cookies' => $this->cookieJar, - 'allow_redirects' => [ - 'max' => 5, - 'strict' => false, - 'referer' => true, - 'track_redirects' => true, - ], - 'http_errors' => true, - ]); - } - - return $this->httpClient; - } - - /** - * Calculate similarity between two strings using multiple algorithms. - */ - protected function calculateSimilarity(string $searchTerm, string $resultTitle): float - { - // Clean up both strings for comparison - $cleanSearch = $this->cleanTitleForComparison($searchTerm); - $cleanResult = $this->cleanTitleForComparison($resultTitle); - - // Calculate similarity using multiple methods - similar_text($cleanSearch, $cleanResult, $similarTextPercent); - - // Also calculate Levenshtein distance based similarity - $maxLen = max(strlen($cleanSearch), strlen($cleanResult)); - if ($maxLen > 0) { - $levenshtein = levenshtein($cleanSearch, $cleanResult); - $levenshteinPercent = (1 - ($levenshtein / $maxLen)) * 100; - } else { - $levenshteinPercent = 0; - } - - // Use the higher of the two similarity scores - return max($similarTextPercent, $levenshteinPercent); - } - - /** - * Clean a title for comparison purposes. - */ - protected function cleanTitleForComparison(string $title): string - { - $title = strtolower($title); - $title = str_replace('/XXX/', '', $title); - - // Remove common adult movie prefixes/suffixes - $removePatterns = [ - '/\b(xxx|adult|porn|erotic|hd|4k|1080p|720p|dvdrip|webrip|bluray)\b/i', - '/\(.*?\)/', - '/\[.*?\]/', - '/[._-]+/', - '/\s+/', - ]; - - foreach ($removePatterns as $pattern) { - $title = preg_replace($pattern, ' ', $title); - } - - return trim($title); - } - - /** - * Extract movie information from the loaded HTML. - * - * @return array - */ - protected function extractCovers(): array - { - return []; - } - - /** - * @return array - */ - protected function extractSynopsis(): array - { - return []; - } - - /** - * @return array - */ - protected function extractCast(): array - { - return []; - } - - /** - * @return array - */ - protected function extractGenres(): array - { - return []; - } - - /** - * @return array - */ - protected function extractProductInfo(bool $extras = false): array - { - return []; - } - - /** - * @return array - */ - protected function extractTrailers(): array - { - return []; - } - - /** - * Output match success message. - */ - protected function outputMatch(string $title): void - { - if (! $this->echoOutput) { - return; - } - - cli()->primary('Found match on '.$this->getDisplayName().': '.$title); - } - - /** - * Output failure message. - */ - protected function outputNotFound(): void - { - if (! $this->echoOutput) { - return; - } - - cli()->notice('No match found on '.$this->getDisplayName()); - } - - /** - * Parse JSON-LD structured data from HTML. - * - * @return array - */ - protected function extractJsonLd(string $html): ?array - { - // Note: Using # as delimiter because pattern contains / in - if (preg_match_all('#]*type=["\']application/ld\+json["\'][^>]*>(.*?)#si', $html, $matches)) { - foreach ($matches[1] as $json) { - $data = json_decode(trim($json), true); - if (json_last_error() === JSON_ERROR_NONE && is_array($data)) { - // Handle both single object and array of objects - if (isset($data['@type'])) { - return $data; - } elseif (isset($data[0]['@type'])) { - return $data[0]; - } - } - } - } - - return null; - } - - /** - * Extract Open Graph meta data from HTML. - * - * @return array - */ - protected function extractOpenGraph(string $html): array - { - $og = []; - $this->getHtmlParser()->loadHtml($html); - - $metaTags = [ - 'og:title' => 'title', - 'og:description' => 'description', - 'og:image' => 'image', - 'og:url' => 'url', - ]; - - foreach ($metaTags as $property => $key) { - $meta = $this->getHtmlParser()->findOne('meta[property="'.$property.'"]'); - if ($meta && isset($meta->content)) { // @phpstan-ignore booleanAnd.leftAlwaysTrue - $og[$key] = trim($meta->content); - } - } - - return $og; - } -} diff --git a/app/Services/AdultProcessing/Pipes/AdePipe.php b/app/Services/AdultProcessing/Pipes/AdePipe.php deleted file mode 100644 index 920de35e9..000000000 --- a/app/Services/AdultProcessing/Pipes/AdePipe.php +++ /dev/null @@ -1,398 +0,0 @@ -getCleanTitle(); - - $searchResult = $this->search($movie); - - if ($searchResult === false) { - $this->outputNotFound(); - - return AdultProcessingResult::notFound($this->getName()); - } - - $this->title = $searchResult['title']; - $this->directUrl = $searchResult['url']; - - // Fetch the movie details page - $this->response = $this->fetchHtml($this->directUrl, $this->cookie); - - if ($this->response === false) { // @phpstan-ignore identical.alwaysFalse - return AdultProcessingResult::failed('Failed to fetch movie details page', $this->getName()); - } - - $this->getHtmlParser()->loadHtml($this->response); - - $movieInfo = $this->getMovieInfo(); - - if ($movieInfo === false) { - return AdultProcessingResult::notFound($this->getName()); - } - - $this->outputMatch($this->title); - - return AdultProcessingResult::matched( - $this->title, - $this->getName(), - $movieInfo - ); - } - - /** - * @return array - */ - protected function search(string $movie): array|false - { - if (empty($movie)) { - return false; - } - - // Initialize session with age verification cookies - $this->initializeSession(); - - $searchUrl = self::BASE_URL.self::SEARCH_URL.rawurlencode($movie); - $response = $this->fetchHtml($searchUrl, $this->cookie); - - if ($response === false) { - return false; - } - - $this->getHtmlParser()->loadHtml($response); - - // Try multiple search result selectors - $resultSelectors = [ - 'a[class=fancybox-button]', - 'div.card a.boxcover-link', - 'a[href*="/item/"]', - ]; - - $bestMatch = null; - $highestSimilarity = 0; - - foreach ($resultSelectors as $selector) { - $res = $this->getHtmlParser()->find($selector); - if (! empty($res)) { - foreach ($res as $ret) { - $title = $ret->title ?? $ret->getAttribute('title') ?? trim($ret->plaintext); - $url = trim($ret->href ?? ''); - - if (empty($title) || empty($url)) { - continue; - } - - $similarity = $this->calculateSimilarity($movie, $title); - - if ($similarity > $highestSimilarity) { - $highestSimilarity = $similarity; - $bestMatch = [ - 'title' => trim($title), - 'url' => str_starts_with($url, 'http') ? $url : self::BASE_URL.$url, - ]; - } - } - - // If we found results with this selector, don't try others - if ($bestMatch !== null) { - break; - } - } - } - - if ($bestMatch !== null && $highestSimilarity >= $this->minimumSimilarity) { - return $bestMatch; - } - - return false; - } - - /** - * @return array - */ - protected function getMovieInfo(): array|false - { - $results = []; - - if (! empty($this->directUrl)) { - if (! empty($this->title)) { - $results['title'] = $this->title; - } - $results['directurl'] = $this->directUrl; - } - - // Get all the movie data - $results = array_merge($results, $this->extractSynopsis()); - $results = array_merge($results, $this->extractProductInfo(true)); - $results = array_merge($results, $this->extractCast()); - $results = array_merge($results, $this->extractGenres()); - $results = array_merge($results, $this->extractCovers()); - $results = array_merge($results, $this->extractTrailers()); - - if (empty($results)) { - return false; - } - - return $results; - } - - /** - * @return array - */ - protected function extractTrailers(): array - { - $res = []; - - $trailersUrl = str_replace('/item/', '/item/trailers/', $this->directUrl); - $trailersResponse = $this->fetchHtml($trailersUrl, $this->cookie); - - if ($trailersResponse !== false) { - if (preg_match("/([\"|'])(?P[^\"']+.swf)([\"|'])/i", $trailersResponse, $hits)) { - $res['trailers']['url'] = self::BASE_URL.trim(trim($hits['swf']), '"'); - - if (preg_match('#(?:streamID:\s\")(?P[0-9A-Z]+)(?:\")#', $trailersResponse, $hits)) { - $res['trailers']['streamid'] = trim($hits['streamid']); - } - - if (preg_match('#(?:BaseStreamingUrl:\s\")(?P[\d]+\.[\d]+\.[\d]+\.[\d]+)(?:\")#', $trailersResponse, $hits)) { - $res['trailers']['baseurl'] = $hits['baseurl']; - } - } - } - - return $res; - } - - /** - * @return array - */ - protected function extractCovers(): array - { - $res = []; - - // Try multiple selectors for better reliability - $selectors = [ - 'div#Boxcover img[itemprop=image]', - 'img[itemprop=image]', - 'div#Boxcover img', - 'div.boxcover img', - ]; - - foreach ($selectors as $selector) { - $ret = $this->getHtmlParser()->findOne($selector); - if ($ret && isset($ret->src)) { // @phpstan-ignore booleanAnd.leftAlwaysTrue - // Get high-resolution covers - $res['boxcover'] = preg_replace('/[ms]\.jpg$/', 'h.jpg', $ret->src); - $res['backcover'] = preg_replace('/[ms]\.jpg$/', 'bh.jpg', $ret->src); - - return $res; - } - } - - return $res; - } - - /** - * @return array - */ - protected function extractSynopsis(): array - { - $res = []; - - // Try multiple selectors in priority order - $selectors = [ - 'meta[property="og:description"]' => 'content', - 'meta[name="description"]' => 'content', - 'div[itemprop="description"]' => 'plaintext', - 'p.synopsis' => 'plaintext', - ]; - - foreach ($selectors as $selector => $property) { - $meta = $this->getHtmlParser()->findOne($selector); - if ($meta && isset($meta->$property) && $meta->$property !== false && ! empty(trim($meta->$property))) { // @phpstan-ignore booleanAnd.leftAlwaysTrue - $res['synopsis'] = trim($meta->$property); - - return $res; - } - } - - return $res; - } - - /** - * @return array - */ - protected function extractCast(): array - { - $res = []; - $cast = []; - - // Try multiple selector strategies - $selectors = [ - 'div[itemprop="actor"] span[itemprop="name"]', - 'div.performer-list a', - 'a[href*="/performer/"]', - ]; - - foreach ($selectors as $selector) { - $elements = $this->getHtmlParser()->find($selector); - if (! empty($elements)) { - foreach ($elements as $a) { - if ($a->plaintext !== false && ! empty(trim($a->plaintext))) { - $cast[] = trim($a->plaintext); - } - } - - if (! empty($cast)) { - break; - } - } - } - - $res['cast'] = array_values(array_unique($cast)); - - return $res; - } - - /** - * @return array - */ - protected function extractGenres(): array - { - $res = []; - $genres = []; - - // Try multiple selector strategies - $selectors = [ - 'a[Label="Category"]', - 'div.categories a', - 'a[href*="/category/"]', - 'span[itemprop="genre"]', - ]; - - foreach ($selectors as $selector) { - $elements = $this->getHtmlParser()->find($selector); - if (! empty($elements)) { - foreach ($elements as $a) { - if ($a->plaintext !== false && ! empty(trim($a->plaintext))) { - $genres[] = trim($a->plaintext); - } - } - - if (! empty($genres)) { - break; - } - } - } - - $res['genres'] = array_values(array_unique($genres)); - - return $res; - } - - /** - * @return array - */ - protected function extractProductInfo(bool $extras = false): array - { - $res = []; - $dofeature = null; - - $tmpResponse = str_ireplace('Section ProductInfo', 'spdinfo', $this->response); - $tmpHtml = new \voku\helper\HtmlDomParser; - $tmpHtml->loadHtml($tmpResponse); - - if ($ret = $tmpHtml->findOne('div[class=spdinfo]')) { // @phpstan-ignore if.alwaysTrue - $productinfo = []; - $extrasData = []; - - $tmpResponse = trim($ret->outertext); - $ret = $tmpHtml->loadHtml($tmpResponse); - - foreach ($ret->find('text') as $strong) { - if (trim($strong->innertext) === 'Features') { - $dofeature = true; - } - if ($dofeature !== true) { - if (trim($strong->innertext) !== ' ') { - $productinfo[] = trim($strong->innertext); - } - } else { - if ($extras === true) { - $extrasData[] = trim($strong->innertext); - } - } - } - - array_shift($productinfo); - array_shift($productinfo); - $res['productinfo'] = array_chunk($productinfo, 2, false); - - if (! empty($extrasData)) { - $res['extras'] = $extrasData; - } - } - - return $res; - } - - /** - * Initialize session by visiting the site to establish cookies. - * ADE uses JavaScript-based age verification with cookies. - */ - protected function initializeSession(): void - { - try { - $client = $this->getHttpClient(); - - // Visit the homepage first to establish a session - $client->get(self::BASE_URL, [ - 'headers' => $this->getDefaultHeaders(), - 'allow_redirects' => true, - ]); - - usleep(300000); // 300ms delay - - } catch (\Exception $e) { - \Illuminate\Support\Facades\Log::debug('ADE session initialization: '.$e->getMessage()); - } - } -} diff --git a/app/Services/AdultProcessing/Pipes/AdmPipe.php b/app/Services/AdultProcessing/Pipes/AdmPipe.php deleted file mode 100644 index bfc07ed0f..000000000 --- a/app/Services/AdultProcessing/Pipes/AdmPipe.php +++ /dev/null @@ -1,299 +0,0 @@ -getCleanTitle(); - - $searchResult = $this->search($movie); - - if ($searchResult === false) { - $this->outputNotFound(); - - return AdultProcessingResult::notFound($this->getName()); - } - - $this->title = $searchResult['title']; - $this->directUrl = $searchResult['url']; - - // Fetch the movie details page - $this->response = $this->fetchHtml($this->directUrl, $this->cookie); - - if ($this->response === false) { // @phpstan-ignore identical.alwaysFalse - return AdultProcessingResult::failed('Failed to fetch movie details page', $this->getName()); - } - - $this->getHtmlParser()->loadHtml($this->response); - - $movieInfo = $this->getMovieInfo(); - - if ($movieInfo === false) { - return AdultProcessingResult::notFound($this->getName()); - } - - $this->outputMatch($this->title); - - return AdultProcessingResult::matched( - $this->title, - $this->getName(), - $movieInfo - ); - } - - /** - * @return array - */ - protected function search(string $movie): array|false - { - if (empty($movie)) { - return false; - } - - $searchUrl = self::BASE_URL.self::SEARCH_URL.urlencode($movie); - $response = $this->fetchHtml($searchUrl, $this->cookie); - - if ($response === false) { - return false; - } - - $this->getHtmlParser()->loadHtml($response); - $check = $this->getHtmlParser()->find('img[rel=license]'); - - if (empty($check)) { - return false; - } - - $bestMatch = null; - $highestSimilarity = 0; - - foreach ($check as $ret) { - if (! isset($ret->alt) || ! isset($ret->src)) { - continue; - } - - $title = trim($ret->alt, '"'); - $title = str_replace('/XXX/', '', $title); - - $similarity = $this->calculateSimilarity($movie, $title); - - if ($similarity > $highestSimilarity && preg_match('/\/(?\d+)\.jpg$/i', $ret->src, $hits)) { - $highestSimilarity = $similarity; - $bestMatch = [ - 'title' => trim($title), - 'sku' => $hits['sku'], - ]; - } - } - - if ($bestMatch !== null && $highestSimilarity >= $this->minimumSimilarity) { - return [ - 'title' => $bestMatch['title'], - 'url' => self::BASE_URL.'/dvd_view_'.$bestMatch['sku'].'.html', - ]; - } - - return false; - } - - /** - * @return array - */ - protected function getMovieInfo(): array|false - { - $results = []; - - if (! empty($this->directUrl)) { - if (! empty($this->title)) { - $results['title'] = $this->title; - } - $results['directurl'] = $this->directUrl; - } - - // Get all the movie data - $results = array_merge($results, $this->extractSynopsis()); - $results = array_merge($results, $this->extractProductInfo(true)); - $results = array_merge($results, $this->extractCast()); - $results = array_merge($results, $this->extractGenres()); - $results = array_merge($results, $this->extractCovers()); - - if (empty($results)) { - return false; - } - - return $results; - } - - /** - * @return array - */ - protected function extractCovers(): array - { - $res = []; - $baseUrl = 'https://www.adultdvdmarketplace.com/'; - - // Try fancybox link first - if ($ret = $this->getHtmlParser()->findOne('a[rel=fancybox-button]')) { // @phpstan-ignore if.alwaysTrue - if (isset($ret->href) && preg_match('/images\/.*[\d]+\.jpg$/i', $ret->href, $hits)) { - $res['boxcover'] = str_starts_with($hits[0], 'http') - ? $hits[0] - : $baseUrl.$hits[0]; - $res['backcover'] = str_ireplace('/front/', '/back/', $res['boxcover']); - - return $res; - } - } - - // Try license image - if ($ret = $this->getHtmlParser()->findOne('img[rel=license]')) { // @phpstan-ignore if.alwaysTrue - if (isset($ret->src) && preg_match('/images\/.*[\d]+\.jpg$/i', $ret->src, $hits)) { - $res['boxcover'] = str_starts_with($hits[0], 'http') - ? $hits[0] - : $baseUrl.$hits[0]; - - return $res; - } - } - - return $res; - } - - /** - * @return array - */ - protected function extractSynopsis(): array - { - $res = []; - $res['synopsis'] = 'N/A'; - - // Try to find Description heading - foreach ($this->getHtmlParser()->find('h3') as $heading) { - if (trim($heading->plaintext) === 'Description') { - $nextElement = $heading->next_sibling(); - if ($nextElement && ! empty(trim($nextElement->plaintext))) { - $res['synopsis'] = trim($nextElement->plaintext); - - return $res; - } - } - } - - // Fallback: Try meta description - $meta = $this->getHtmlParser()->findOne('meta[name="description"]'); - if ($meta && isset($meta->content) && ! empty(trim($meta->content))) { // @phpstan-ignore booleanAnd.leftAlwaysTrue - $res['synopsis'] = trim($meta->content); - } - - return $res; - } - - /** - * @return array - */ - protected function extractProductInfo(bool $extras = false): array - { - $res = []; - - foreach ($this->getHtmlParser()->find('ul.list-unstyled li') as $li) { - $category = explode(':', $li->plaintext); - switch (trim($category[0])) { - case 'Director': - $res['director'] = trim($category[1]); - break; - case 'Format': - case 'Studio': - case 'Released': - case 'SKU': - $res['productinfo'][trim($category[0])] = trim($category[1]); - } - } - - return $res; - } - - /** - * @return array - */ - protected function extractCast(): array - { - $res = []; - $cast = []; - - foreach ($this->getHtmlParser()->find('h3') as $heading) { - if (trim($heading->plaintext) === 'Cast') { - $next = $heading->nextSibling(); - while ($next) { - if ($next->nodeName === 'h3') { // @phpstan-ignore property.notFound - break; - } - if (isset($next->href) && preg_match('/search_performerid/', $next->href)) { - $cast[] = trim($next->plaintext); - } - $next = $next->nextSibling(); - } - } - } - - $res['cast'] = array_unique($cast); - - return $res; - } - - /** - * @return array - */ - protected function extractGenres(): array - { - $res = []; - $genres = []; - - foreach ($this->getHtmlParser()->find('ul.list-unstyled') as $li) { - $category = explode(':', $li->plaintext); - if (trim($category[0]) === 'Category') { - foreach (explode(',', $category[1]) as $genre) { - $genres[] = trim($genre); - } - $res['genres'] = $genres; - } - } - - return $res; - } -} diff --git a/app/Services/AdultProcessing/Pipes/AebnPipe.php b/app/Services/AdultProcessing/Pipes/AebnPipe.php deleted file mode 100644 index 52faf4495..000000000 --- a/app/Services/AdultProcessing/Pipes/AebnPipe.php +++ /dev/null @@ -1,576 +0,0 @@ - - */ - protected array $res = []; - - protected string $response = ''; - - /** - * @var array - */ - protected ?array $jsonLdData = null; - - public function getName(): string - { - return 'aebn'; - } - - public function getDisplayName(): string - { - return 'Adult Entertainment Broadcast Network'; - } - - protected function getBaseUrl(): string - { - return self::BASE_URL; - } - - protected function process(AdultProcessingPassable $passable): AdultProcessingResult - { - $movie = $passable->getCleanTitle(); - - // Check cache first - $cached = $this->getCachedSearch($movie); - if ($cached !== null) { - if ($cached === false) { - return AdultProcessingResult::notFound($this->getName()); - } - - return AdultProcessingResult::matched( - $cached['title'] ?? $movie, - $this->getName(), - $cached - ); - } - - $searchResult = $this->search($movie); - - if ($searchResult === false) { - $this->cacheSearchResult($movie, false); - $this->outputNotFound(); - - return AdultProcessingResult::notFound($this->getName()); - } - - $this->title = $searchResult['title']; - $this->directUrl = $searchResult['url']; - - // Fetch the movie details page - $this->response = $this->fetchHtml($this->directUrl, $this->cookie); - - if ($this->response === false) { // @phpstan-ignore identical.alwaysFalse - return AdultProcessingResult::failed('Failed to fetch movie details page', $this->getName()); - } - - // Try to extract JSON-LD data first - $this->jsonLdData = $this->extractJsonLd($this->response); - - $this->getHtmlParser()->loadHtml($this->response); - - $movieInfo = $this->getMovieInfo(); - - if ($movieInfo === false) { - $this->cacheSearchResult($movie, false); - - return AdultProcessingResult::notFound($this->getName()); - } - - // Cache the successful result - $this->cacheSearchResult($movie, $movieInfo); - - $this->outputMatch($this->title); - - return AdultProcessingResult::matched( - $this->title, - $this->getName(), - $movieInfo - ); - } - - /** - * @return array - */ - protected function search(string $movie): array|false - { - if (empty($movie)) { - return false; - } - - $searchUrl = self::BASE_URL.self::SEARCH_URL.urlencode($movie); - $response = $this->fetchHtml($searchUrl, $this->cookie); - - if ($response === false) { - return false; - } - - $this->getHtmlParser()->loadHtml($response); - - $bestMatch = null; - $highestSimilarity = 0; - $i = 1; - - // Try multiple container selectors for search results - $containerSelectors = [ - 'div.movie', - 'div.search-result', - 'div[class*="movie"]', - 'article.movie', - ]; - - $movies = []; - foreach ($containerSelectors as $containerSelector) { - $movies = $this->getHtmlParser()->find($containerSelector); - if (! empty($movies) && (is_countable($movies) ? count($movies) > 0 : true)) { - break; - } - } - - foreach ($movies as $mov) { - // Try multiple selector patterns for the title link - $selectors = [ - 'a#FTSMovieSearch_link_title_detail_'.$i, - 'a.title-link', - 'a[href*="/movie/"]', - 'h3 a', - 'a[title]', - ]; - - $ret = null; - foreach ($selectors as $selector) { - $ret = $mov->findOne($selector); - if ($ret && isset($ret->href)) { - break; - } - } - - if ($ret && isset($ret->href)) { - $title = $ret->title ?? trim($ret->plaintext ?? ''); - - if (! empty($title)) { - $similarity = $this->calculateSimilarity($movie, $title); - - if ($similarity > $highestSimilarity) { - $highestSimilarity = $similarity; - $bestMatch = [ - 'title' => trim($title), - 'url' => self::BASE_URL.html_entity_decode($ret->href), - 'similarity' => $similarity, - ]; - } - } - } - $i++; - } - - if ($bestMatch !== null && $highestSimilarity >= $this->minimumSimilarity) { - return $bestMatch; - } - - return false; - } - - /** - * @return array - */ - protected function getMovieInfo(): array|false - { - $results = []; - - if (! empty($this->directUrl)) { - if (! empty($this->title)) { - $results['title'] = $this->title; - } - $results['directurl'] = $this->directUrl; - } - - // Try to get data from JSON-LD first (most reliable) - if ($this->jsonLdData !== null) { - $results = array_merge($results, $this->extractFromJsonLd()); - } - - // Get all the movie data (HTML fallback) - $synopsis = $this->extractSynopsis(); - if (! empty($synopsis)) { - $results = array_merge($results, $synopsis); - } - - $productInfo = $this->extractProductInfo(true); - if (! empty($productInfo)) { - $results = array_merge($results, $productInfo); - } - - $cast = $this->extractCast(); - if (! empty($cast)) { - $results = array_merge($results, $cast); - } - - $genres = $this->extractGenres(); - if (! empty($genres)) { - $results = array_merge($results, $genres); - } - - $covers = $this->extractCovers(); - if (! empty($covers)) { - $results = array_merge($results, $covers); - } - - $trailers = $this->extractTrailers(); - if (! empty($trailers)) { - $results = array_merge($results, $trailers); - } - - if (empty($results) || (empty($results['title'] ?? '') && empty($results['boxcover'] ?? ''))) { - return false; - } - - return $results; - } - - /** - * Extract data from JSON-LD structured data. - * - * @return array - */ - protected function extractFromJsonLd(): array - { - $results = []; - - if ($this->jsonLdData === null) { - return $results; - } - - // Title - if (! empty($this->jsonLdData['name'])) { - $results['title'] = $this->jsonLdData['name']; - } - - // Synopsis/Description - if (! empty($this->jsonLdData['description'])) { - $results['synopsis'] = $this->jsonLdData['description']; - } - - // Image/Cover - if (! empty($this->jsonLdData['image'])) { - $image = is_array($this->jsonLdData['image']) ? ($this->jsonLdData['image'][0] ?? '') : $this->jsonLdData['image']; - if (! empty($image)) { - $results['boxcover'] = $image; - $results['backcover'] = str_ireplace(['xlf.jpg', 'front'], ['xlb.jpg', 'back'], $image); - } - } - - // Duration - if (! empty($this->jsonLdData['duration'])) { - $results['duration'] = $this->jsonLdData['duration']; - } - - // Director - if (! empty($this->jsonLdData['director'])) { - $director = $this->jsonLdData['director']; - if (is_array($director)) { - $results['director'] = $director['name'] ?? ($director[0]['name'] ?? ''); - } else { - $results['director'] = $director; - } - } - - // Actors - if (! empty($this->jsonLdData['actor'])) { - $actors = $this->jsonLdData['actor']; - $cast = []; - if (is_array($actors)) { - foreach ($actors as $actor) { - if (is_array($actor) && ! empty($actor['name'])) { - $cast[] = $actor['name']; - } elseif (is_string($actor)) { - $cast[] = $actor; - } - } - } - if (! empty($cast)) { - $results['cast'] = $cast; - } - } - - // Genre - if (! empty($this->jsonLdData['genre'])) { - $genres = $this->jsonLdData['genre']; - if (is_array($genres)) { - $results['genres'] = $genres; - } else { - $results['genres'] = [$genres]; - } - } - - return $results; - } - - /** - * @return array - */ - protected function extractTrailers(): array - { - $res = []; - - // Try multiple selectors - $selectors = [ - 'a[itemprop=trailer]', - 'a[href*="previewPlayer"]', - 'a.trailer-link', - ]; - - foreach ($selectors as $selector) { - $ret = $this->getHtmlParser()->find($selector, 0); - if ($ret && isset($ret->href) && preg_match('/movieId=(?\d+)/', trim($ret->href), $hits)) { - $movieid = $hits['movieid']; - $res['trailers']['url'] = self::BASE_URL.self::TRAILER_URL.$movieid; - - return $res; - } - } - - return $res; - } - - /** - * @return array - */ - protected function extractCovers(): array - { - $res = []; - - // Try multiple selectors - $selectors = [ - 'img[itemprop=thumbnailUrl]', - 'img[itemprop=image]', - 'div#md-boxCover img', - 'div.boxcover img', - 'img.boxcover', - 'img[src*="boxcover"]', - 'meta[property="og:image"]', - ]; - - foreach ($selectors as $selector) { - $ret = $this->getHtmlParser()->findOne($selector); - if ($ret) { // @phpstan-ignore if.alwaysTrue - $coverUrl = $ret->src ?? $ret->content ?? null; - - if (! empty($coverUrl)) { - // Ensure URL has protocol - if (str_starts_with($coverUrl, '//')) { - $coverUrl = 'https:'.$coverUrl; - } - - // Get high-resolution versions - $res['boxcover'] = str_ireplace(['160w.jpg', '120w.jpg', '_small', '_thumb'], ['xlf.jpg', 'xlf.jpg', '', ''], $coverUrl); - $res['backcover'] = str_ireplace(['160w.jpg', '120w.jpg', 'xlf.jpg', 'front'], ['xlb.jpg', 'xlb.jpg', 'xlb.jpg', 'back'], $res['boxcover']); - - return $res; - } - } - } - - return $res; - } - - /** - * @return array - */ - protected function extractGenres(): array - { - $res = []; - $genres = []; - - // Try multiple selectors - $selectors = [ - 'div.md-detailsCategories a[itemprop=genre]', - 'a[itemprop=genre]', - 'span[itemprop=genre]', - 'div.categories a', - 'a[href*="/category/"]', - ]; - - foreach ($selectors as $selector) { - $elements = $this->getHtmlParser()->find($selector); - if (! empty($elements)) { - foreach ($elements as $genre) { - $text = trim($genre->plaintext ?? ''); - if (! empty($text)) { - $genres[] = $text; - } - } - if (! empty($genres)) { - break; - } - } - } - - if (! empty($genres)) { - $res['genres'] = array_unique($genres); - } - - return $res; - } - - /** - * @return array - */ - protected function extractCast(): array - { - $res = []; - $cast = []; - - // Try multiple selectors - $selectors = [ - 'div.starsFull span[itemprop=name]', - 'span[itemprop=name]', - 'a[href*="/stars/"]', - 'div.cast a', - 'div.performers a', - ]; - - foreach ($selectors as $selector) { - $elements = $this->getHtmlParser()->find($selector); - if (! empty($elements)) { - foreach ($elements as $star) { - $text = trim($star->plaintext ?? ''); - if (! empty($text) && strlen($text) > 2) { - $cast[] = $text; - } - } - if (! empty($cast)) { - break; - } - } - } - - // Fallback: try detailsLink div - if (empty($cast)) { - $ret = $this->getHtmlParser()->findOne('div.detailsLink'); - if ($ret && ! ($ret instanceof \voku\helper\SimpleHtmlDomNodeBlank)) { // @phpstan-ignore booleanAnd.leftAlwaysTrue - foreach ($ret->find('span') as $star) { - $text = $star->plaintext ?? ''; - if (str_contains($text, '/More/') && str_contains($text, '/Stars/')) { - $cast[] = trim($text); - } - } - } - } - - if (! empty($cast)) { - $res['cast'] = array_unique($cast); - } - - return $res; - } - - /** - * @return array - */ - protected function extractProductInfo(bool $extras = false): array - { - $res = []; - - // Try multiple selectors - $selectors = [ - 'div#md-detailsLeft', - 'div.movie-details', - 'div.product-info', - ]; - - foreach ($selectors as $selector) { - $ret = $this->getHtmlParser()->find($selector, 0); - if ($ret) { - $productinfo = []; - foreach ($ret->find('div') as $div) { - foreach ($div->find('span') as $span) { - $text = rawurldecode($span->plaintext ?? ''); - $text = preg_replace('/ /', '', $text); - $text = trim($text); - if (! empty($text)) { - $productinfo[] = $text; - } - } - } - - if (! empty($productinfo)) { - if (false !== $key = array_search('Running Time:', $productinfo, false)) { - unset($productinfo[$key + 2]); - } - - if (false !== $key = array_search('Director:', $productinfo, false)) { - $res['director'] = $productinfo[$key + 1] ?? ''; - unset($productinfo[$key], $productinfo[$key + 1]); - } - - $res['productinfo'] = array_chunk(array_values($productinfo), 2, false); - break; - } - } - } - - return $res; - } - - /** - * @return array - */ - protected function extractSynopsis(): array - { - $res = []; - - // Try multiple selectors in order of preference - $selectors = [ - 'span[itemprop=about]', - 'span[itemprop=description]', - 'div[itemprop=description]', - 'div.movieDetailDescription', - 'div.synopsis', - 'p.description', - 'meta[name="description"]', - 'meta[property="og:description"]', - ]; - - foreach ($selectors as $selector) { - $ret = $this->getHtmlParser()->findOne($selector); - if ($ret) { // @phpstan-ignore if.alwaysTrue - $text = $ret->plaintext ?? $ret->content ?? ''; - if (! empty(trim($text))) { - $text = trim($text); - $text = preg_replace('/^Description:\s*/i', '', $text); - $res['synopsis'] = $text; - - return $res; - } - } - } - - return $res; - } -} diff --git a/app/Services/AdultProcessing/Pipes/Data18Pipe.php b/app/Services/AdultProcessing/Pipes/Data18Pipe.php deleted file mode 100644 index 40b464ec6..000000000 --- a/app/Services/AdultProcessing/Pipes/Data18Pipe.php +++ /dev/null @@ -1,525 +0,0 @@ - - */ - protected ?array $jsonLdData = null; - - public function getName(): string - { - return 'data18'; - } - - public function getDisplayName(): string - { - return 'Data18'; - } - - protected function getBaseUrl(): string - { - return self::BASE_URL; - } - - protected function process(AdultProcessingPassable $passable): AdultProcessingResult - { - $movie = $passable->getCleanTitle(); - - // Check cache first - $cached = $this->getCachedSearch($movie); - if ($cached !== null) { - if ($cached === false) { - return AdultProcessingResult::notFound($this->getName()); - } - - return AdultProcessingResult::matched( - $cached['title'] ?? $movie, - $this->getName(), - $cached - ); - } - - $searchResult = $this->search($movie); - - if ($searchResult === false) { - $this->cacheSearchResult($movie, false); - $this->outputNotFound(); - - return AdultProcessingResult::notFound($this->getName()); - } - - $this->title = $searchResult['title']; - $this->directUrl = $searchResult['url']; - - // Fetch the movie details page - $this->response = $this->fetchHtml($this->directUrl, $this->cookie); - - if ($this->response === false) { // @phpstan-ignore identical.alwaysFalse - return AdultProcessingResult::failed('Failed to fetch movie details page', $this->getName()); - } - - // Try to extract JSON-LD data first - $this->jsonLdData = $this->extractJsonLd($this->response); - - $this->getHtmlParser()->loadHtml($this->response); - - $movieInfo = $this->getMovieInfo(); - - if ($movieInfo === false) { - $this->cacheSearchResult($movie, false); - - return AdultProcessingResult::notFound($this->getName()); - } - - // Cache the successful result - $this->cacheSearchResult($movie, $movieInfo); - - $this->outputMatch($this->title); - - return AdultProcessingResult::matched( - $this->title, - $this->getName(), - $movieInfo - ); - } - - /** - * @return array - */ - protected function search(string $movie): array|false - { - if (empty($movie)) { - return false; - } - - $searchUrl = self::BASE_URL.self::SEARCH_URL.urlencode($movie); - $response = $this->fetchHtml($searchUrl, $this->cookie); - - if ($response === false) { - return false; - } - - $this->getHtmlParser()->loadHtml($response); - - $bestMatch = null; - $highestSimilarity = 0; - - // Try multiple container selectors for search results - $containerSelectors = [ - 'div.gen12', - 'div.movie-card', - 'div[class*="result"]', - 'a[href*="/movies/"]', - ]; - - foreach ($containerSelectors as $containerSelector) { - $results = $this->getHtmlParser()->find($containerSelector); - - if (! empty($results)) { - foreach ($results as $result) { - $link = null; - $title = ''; - - // If the container is a link itself - if (isset($result->href) && str_contains($result->href, '/movies/')) { - $link = $result; - $title = $result->title ?? trim($result->plaintext ?? ''); - } else { - // Find link within container - $link = $result->findOne('a[href*="/movies/"]'); - if ($link) { - $title = $link->title ?? trim($link->plaintext ?? ''); - } - } - - if ($link && isset($link->href) && ! empty($title)) { - $similarity = $this->calculateSimilarity($movie, $title); - - if ($similarity > $highestSimilarity) { - $highestSimilarity = $similarity; - $url = $link->href; - if (! str_starts_with($url, 'http')) { - $url = self::BASE_URL.$url; - } - $bestMatch = [ - 'title' => trim($title), - 'url' => $url, - 'similarity' => $similarity, - ]; - } - } - } - - if ($bestMatch !== null) { - break; - } - } - } - - if ($bestMatch !== null && $highestSimilarity >= $this->minimumSimilarity) { - return $bestMatch; - } - - return false; - } - - /** - * @return array - */ - protected function getMovieInfo(): array|false - { - $results = []; - - if (! empty($this->directUrl)) { - if (! empty($this->title)) { - $results['title'] = $this->title; - } - $results['directurl'] = $this->directUrl; - } - - // Try to get data from JSON-LD first (most reliable) - if ($this->jsonLdData !== null) { - $results = array_merge($results, $this->extractFromJsonLd()); - } - - // Get all the movie data (HTML fallback) - $synopsis = $this->extractSynopsis(); - if (! empty($synopsis)) { - $results = array_merge($results, $synopsis); - } - - $productInfo = $this->extractProductInfo(true); - if (! empty($productInfo)) { - $results = array_merge($results, $productInfo); - } - - $cast = $this->extractCast(); - if (! empty($cast)) { - $results = array_merge($results, $cast); - } - - $genres = $this->extractGenres(); - if (! empty($genres)) { - $results = array_merge($results, $genres); - } - - $covers = $this->extractCovers(); - if (! empty($covers)) { - $results = array_merge($results, $covers); - } - - if (empty($results) || (empty($results['title'] ?? '') && empty($results['boxcover'] ?? ''))) { - return false; - } - - return $results; - } - - /** - * Extract data from JSON-LD structured data. - * - * @return array - */ - protected function extractFromJsonLd(): array - { - $results = []; - - if ($this->jsonLdData === null) { - return $results; - } - - // Title - if (! empty($this->jsonLdData['name'])) { - $results['title'] = $this->jsonLdData['name']; - } - - // Synopsis/Description - if (! empty($this->jsonLdData['description'])) { - $results['synopsis'] = $this->jsonLdData['description']; - } - - // Image/Cover - if (! empty($this->jsonLdData['image'])) { - $image = is_array($this->jsonLdData['image']) ? ($this->jsonLdData['image'][0] ?? '') : $this->jsonLdData['image']; - if (! empty($image)) { - $results['boxcover'] = $image; - } - } - - // Duration - if (! empty($this->jsonLdData['duration'])) { - $results['duration'] = $this->jsonLdData['duration']; - } - - // Director - if (! empty($this->jsonLdData['director'])) { - $director = $this->jsonLdData['director']; - if (is_array($director)) { - $results['director'] = $director['name'] ?? ($director[0]['name'] ?? ''); - } else { - $results['director'] = $director; - } - } - - // Actors - if (! empty($this->jsonLdData['actor'])) { - $actors = $this->jsonLdData['actor']; - $cast = []; - if (is_array($actors)) { - foreach ($actors as $actor) { - if (is_array($actor) && ! empty($actor['name'])) { - $cast[] = $actor['name']; - } elseif (is_string($actor)) { - $cast[] = $actor; - } - } - } - if (! empty($cast)) { - $results['cast'] = $cast; - } - } - - // Genre - if (! empty($this->jsonLdData['genre'])) { - $genres = $this->jsonLdData['genre']; - if (is_array($genres)) { - $results['genres'] = $genres; - } else { - $results['genres'] = [$genres]; - } - } - - return $results; - } - - /** - * @return array - */ - protected function extractCovers(): array - { - $res = []; - - // Try multiple selectors - $selectors = [ - 'img[itemprop=image]', - 'img.cover', - 'img[src*="cover"]', - 'div.cover img', - 'a.cover img', - 'meta[property="og:image"]', - ]; - - foreach ($selectors as $selector) { - $ret = $this->getHtmlParser()->findOne($selector); - if ($ret) { // @phpstan-ignore if.alwaysTrue - $coverUrl = $ret->src ?? $ret->content ?? null; - - if (! empty($coverUrl)) { - if (str_starts_with($coverUrl, '//')) { - $coverUrl = 'https:'.$coverUrl; - } elseif (! str_starts_with($coverUrl, 'http')) { - $coverUrl = self::BASE_URL.'/'.ltrim($coverUrl, '/'); - } - - $res['boxcover'] = $coverUrl; - - // Try to find back cover - $backCover = str_replace(['front', '_f.', '_1.'], ['back', '_b.', '_2.'], $coverUrl); - if ($backCover !== $coverUrl) { - $res['backcover'] = $backCover; - } - - return $res; - } - } - } - - return $res; - } - - /** - * @return array - */ - protected function extractSynopsis(): array - { - $res = []; - - $selectors = [ - 'div[itemprop=description]', - 'span[itemprop=description]', - 'div.synopsis', - 'div.description', - 'p.synopsis', - 'meta[name="description"]', - 'meta[property="og:description"]', - ]; - - foreach ($selectors as $selector) { - $ret = $this->getHtmlParser()->findOne($selector); - if ($ret) { // @phpstan-ignore if.alwaysTrue - $text = $ret->plaintext ?? $ret->content ?? ''; - if (! empty(trim($text))) { - $res['synopsis'] = trim($text); - - return $res; - } - } - } - - return $res; - } - - /** - * @return array - */ - protected function extractCast(): array - { - $res = []; - $cast = []; - - $selectors = [ - 'a[href*="/name/"]', - 'a[href*="/pornstars/"]', - 'span[itemprop=actor] a', - 'div.cast a', - 'div.performers a', - ]; - - foreach ($selectors as $selector) { - $elements = $this->getHtmlParser()->find($selector); - if (! empty($elements)) { - foreach ($elements as $element) { - $name = trim($element->plaintext ?? ''); - if (! empty($name) && strlen($name) > 2) { - $cast[] = $name; - } - } - if (! empty($cast)) { - break; - } - } - } - - if (! empty($cast)) { - $res['cast'] = array_unique($cast); - } - - return $res; - } - - /** - * @return array - */ - protected function extractGenres(): array - { - $res = []; - $genres = []; - - $selectors = [ - 'a[href*="/category/"]', - 'a[href*="/genre/"]', - 'span[itemprop=genre]', - 'div.categories a', - 'div.tags a', - ]; - - foreach ($selectors as $selector) { - $elements = $this->getHtmlParser()->find($selector); - if (! empty($elements)) { - foreach ($elements as $element) { - $text = trim($element->plaintext ?? ''); - if (! empty($text) && strlen($text) > 1) { - $genres[] = $text; - } - } - if (! empty($genres)) { - break; - } - } - } - - if (! empty($genres)) { - $res['genres'] = array_unique($genres); - } - - return $res; - } - - /** - * @return array - */ - protected function extractProductInfo(bool $extras = false): array - { - $res = []; - - // Look for studio - $studioSelectors = [ - 'a[href*="/studio/"]', - 'a[href*="/studios/"]', - 'span.studio a', - ]; - - foreach ($studioSelectors as $selector) { - $studio = $this->getHtmlParser()->findOne($selector); - if ($studio) { // @phpstan-ignore if.alwaysTrue - $res['studio'] = trim($studio->plaintext ?? ''); - break; - } - } - - // Look for release date - $dateSelectors = [ - 'span[itemprop=datePublished]', - 'time[datetime]', - 'span.date', - ]; - - foreach ($dateSelectors as $selector) { - $date = $this->getHtmlParser()->findOne($selector); - if ($date) { // @phpstan-ignore if.alwaysTrue - $res['releasedate'] = $date->datetime ?? trim($date->plaintext ?? ''); - break; - } - } - - // Look for director - $directorSelectors = [ - 'a[href*="/director/"]', - 'span.director a', - ]; - - foreach ($directorSelectors as $selector) { - $director = $this->getHtmlParser()->findOne($selector); - if ($director) { // @phpstan-ignore if.alwaysTrue - $res['director'] = trim($director->plaintext ?? ''); - break; - } - } - - return $res; - } -} diff --git a/app/Services/AdultProcessing/Pipes/HotmoviesPipe.php b/app/Services/AdultProcessing/Pipes/HotmoviesPipe.php deleted file mode 100644 index 8a4059d09..000000000 --- a/app/Services/AdultProcessing/Pipes/HotmoviesPipe.php +++ /dev/null @@ -1,364 +0,0 @@ -getCleanTitle(); - - $searchResult = $this->search($movie); - - if ($searchResult === false) { - $this->outputNotFound(); - - return AdultProcessingResult::notFound($this->getName()); - } - - $this->title = $searchResult['title']; - $this->directUrl = $searchResult['url']; - - // Fetch the movie details page - $this->response = $this->fetchHtml($this->directUrl, $this->cookie); - - if ($this->response === false) { // @phpstan-ignore identical.alwaysFalse - return AdultProcessingResult::failed('Failed to fetch movie details page', $this->getName()); - } - - $this->getHtmlParser()->loadHtml($this->response); - - $movieInfo = $this->getMovieInfo(); - - if ($movieInfo === false) { - return AdultProcessingResult::notFound($this->getName()); - } - - $this->outputMatch($this->title); - - return AdultProcessingResult::matched( - $this->title, - $this->getName(), - $movieInfo - ); - } - - /** - * @return array - */ - protected function search(string $movie): array|false - { - if (empty($movie)) { - return false; - } - - // Initialize session with age verification - $this->initializeSession(); - - $searchUrl = self::BASE_URL.self::SEARCH_URL.urlencode($movie).self::EXTRA_SEARCH; - $response = $this->fetchHtml($searchUrl, $this->cookie); - - if ($response === false) { - return false; - } - - $this->getHtmlParser()->loadHtml($response); - - // Try multiple result selectors - $resultSelectors = [ - 'h3[class=title] a[title]', - 'h3.title a', - 'div.movie-title a', - ]; - - $bestMatch = null; - $highestSimilarity = 0; - - foreach ($resultSelectors as $selector) { - $elements = $this->getHtmlParser()->find($selector); - if (! empty($elements)) { - foreach ($elements as $ret) { - $title = $ret->title ?? $ret->plaintext ?? ''; - $url = $ret->href ?? ''; - - if (empty($title) || empty($url)) { - continue; - } - - $similarity = $this->calculateSimilarity($movie, $title); - - if ($similarity > $highestSimilarity) { - $highestSimilarity = $similarity; - $bestMatch = [ - 'title' => trim($title), - 'url' => str_starts_with($url, 'http') ? $url : self::BASE_URL.$url, - ]; - } - } - - // If we found results with this selector, don't try others - if ($bestMatch !== null) { - break; - } - } - } - - if ($bestMatch !== null && $highestSimilarity >= $this->minimumSimilarity) { - return $bestMatch; - } - - return false; - } - - /** - * @return array - */ - protected function getMovieInfo(): array|false - { - $results = []; - - if (! empty($this->directUrl)) { - if (! empty($this->title)) { - $results['title'] = $this->title; - } - $results['directurl'] = $this->directUrl; - } - - // Get all the movie data - $results = array_merge($results, $this->extractSynopsis()); - $results = array_merge($results, $this->extractProductInfo(true)); - $results = array_merge($results, $this->extractCast()); - $results = array_merge($results, $this->extractGenres()); - $results = array_merge($results, $this->extractCovers()); - - if (empty($results)) { - return false; - } - - return $results; - } - - /** - * @return array - */ - protected function extractSynopsis(): array - { - $res = []; - $res['synopsis'] = 'N/A'; - - // Try multiple selectors - $selectors = [ - '.video_description', - 'div.description', - 'div.synopsis', - 'meta[name="description"]', - ]; - - foreach ($selectors as $selector) { - $ret = $this->getHtmlParser()->findOne($selector); - if ($ret) { // @phpstan-ignore if.alwaysTrue - $text = $ret->innerText ?? $ret->plaintext ?? $ret->content ?? ''; - if (! empty(trim($text))) { - $res['synopsis'] = trim($text); - - return $res; - } - } - } - - return $res; - } - - /** - * @return array - */ - protected function extractProductInfo(bool $extras = false): array - { - $res = []; - $studio = false; - $director = false; - - if (($ret = $this->getHtmlParser()->find('div.page_video_info')) && ! empty($ret->find('text'))) { - $productinfo = []; - - foreach ($ret->find('text') as $e) { - $e = trim($e->plaintext); - $rArray = [',', '...', ' :']; - $e = str_replace($rArray, '', $e); - - if (stripos($e, 'Studio:') !== false) { - $studio = true; - } - - if (str_contains($e, 'Director:')) { - $director = true; - $e = null; - } - - if ($studio === true) { - if ((stripos($e, 'Custodian of Records') === false) && stripos($e, 'Description') === false) { - if ($director === true && ! empty($e)) { - $res['director'] = $e; - $e = null; - $director = false; - } - if (! empty($e)) { - $productinfo[] = $e; - } - } else { - break; - } - } - } - - $res['productinfo'] = array_chunk($productinfo, 2, false); - } - - return $res; - } - - /** - * @return array - */ - protected function extractCast(): array - { - $res = []; - $cast = []; - - // Prefer scoped search within stars container to avoid unrelated links - if ($container = $this->getHtmlParser()->findOne('.stars')) { // @phpstan-ignore if.alwaysTrue - foreach ($container->find('a[title]') as $e) { - $name = trim($e->title); // @phpstan-ignore property.notFound - $name = preg_replace('/\((.*)\)/', '', $name); - $name = trim($name); - if ($name !== '') { - $cast[] = $name; - } - } - } - - // Fallback: anchors that look like performer links - if (empty($cast)) { - foreach ($this->getHtmlParser()->find('a[href*="/performers/"]') as $e) { - $name = trim($e->plaintext); - if ($name !== '') { - $cast[] = $name; - } - } - } - - if (! empty($cast)) { - $res['cast'] = array_values(array_unique($cast)); - } - - return $res; - } - - /** - * @return array - */ - protected function extractGenres(): array - { - $res = []; - $genres = []; - - if ($ret = $this->getHtmlParser()->findOne('div.categories')) { // @phpstan-ignore if.alwaysTrue - foreach ($ret->find('a') as $e) { - if (str_contains($e->title, ' -> ')) { // @phpstan-ignore property.notFound - $e = explode(' -> ', $e->plaintext); - $genres[] = trim($e[1]); - } - } - $res['genres'] = $genres; - } - - return $res; - } - - /** - * @return array - */ - protected function extractCovers(): array - { - $res = []; - - // Try multiple selectors - $selectors = [ - 'img#cover', - 'div#large_cover img', - 'img.boxcover', - 'div.product-image img', - ]; - - foreach ($selectors as $selector) { - $ret = $this->getHtmlParser()->findOne($selector); - if ($ret && isset($ret->src)) { // @phpstan-ignore booleanAnd.leftAlwaysTrue - $res['boxcover'] = trim($ret->src); - $res['backcover'] = str_ireplace(['.cover', 'front'], ['.back', 'back'], trim($ret->src)); - - return $res; - } - } - - return $res; - } - - /** - * Initialize session by visiting the site to establish cookies. - * HotMovies uses cookie-based age verification. - */ - protected function initializeSession(): void - { - try { - $client = $this->getHttpClient(); - - // Visit the homepage first to establish a session - $client->get(self::BASE_URL, [ - 'headers' => $this->getDefaultHeaders(), - 'allow_redirects' => true, - ]); - - usleep(300000); // 300ms delay - - } catch (\Exception $e) { - \Illuminate\Support\Facades\Log::debug('HotMovies session initialization: '.$e->getMessage()); - } - } -} diff --git a/app/Services/AdultProcessing/Pipes/IafdPipe.php b/app/Services/AdultProcessing/Pipes/IafdPipe.php deleted file mode 100644 index 5fb92e5ba..000000000 --- a/app/Services/AdultProcessing/Pipes/IafdPipe.php +++ /dev/null @@ -1,518 +0,0 @@ - - */ - protected ?array $jsonLdData = null; - - public function getName(): string - { - return 'iafd'; - } - - public function getDisplayName(): string - { - return 'Internet Adult Film Database'; - } - - protected function getBaseUrl(): string - { - return self::BASE_URL; - } - - protected function process(AdultProcessingPassable $passable): AdultProcessingResult - { - $movie = $passable->getCleanTitle(); - - // Check cache first - $cached = $this->getCachedSearch($movie); - if ($cached !== null) { - if ($cached === false) { - return AdultProcessingResult::notFound($this->getName()); - } - - return AdultProcessingResult::matched( - $cached['title'] ?? $movie, - $this->getName(), - $cached - ); - } - - $searchResult = $this->search($movie); - - if ($searchResult === false) { - $this->cacheSearchResult($movie, false); - $this->outputNotFound(); - - return AdultProcessingResult::notFound($this->getName()); - } - - $this->title = $searchResult['title']; - $this->directUrl = $searchResult['url']; - - // Fetch the movie details page - $this->response = $this->fetchHtml($this->directUrl, $this->cookie); - - if ($this->response === false) { // @phpstan-ignore identical.alwaysFalse - return AdultProcessingResult::failed('Failed to fetch movie details page', $this->getName()); - } - - // Try to extract JSON-LD data first - $this->jsonLdData = $this->extractJsonLd($this->response); - - $this->getHtmlParser()->loadHtml($this->response); - - $movieInfo = $this->getMovieInfo(); - - if ($movieInfo === false) { - $this->cacheSearchResult($movie, false); - - return AdultProcessingResult::notFound($this->getName()); - } - - // Cache the successful result - $this->cacheSearchResult($movie, $movieInfo); - - $this->outputMatch($this->title); - - return AdultProcessingResult::matched( - $this->title, - $this->getName(), - $movieInfo - ); - } - - /** - * @return array - */ - protected function search(string $movie): array|false - { - if (empty($movie)) { - return false; - } - - $searchUrl = self::BASE_URL.self::SEARCH_URL.urlencode($movie); - $response = $this->fetchHtml($searchUrl, $this->cookie); - - if ($response === false) { - return false; - } - - $this->getHtmlParser()->loadHtml($response); - - $bestMatch = null; - $highestSimilarity = 0; - - // Look for movie results table - $movieResults = $this->getHtmlParser()->find('table#titleresult tr'); - - if (empty($movieResults)) { - // Try alternative selectors - $movieResults = $this->getHtmlParser()->find('div.panel-body a[href*="/title.rme"]'); - } - - foreach ($movieResults as $result) { - $link = null; - $title = ''; - - // Check if this is a table row - if ($result->tag === 'tr') { - $link = $result->findOne('a[href*="/title.rme"]'); - if ($link) { - $title = $link->title ?? trim($link->plaintext ?? ''); - } - } else { - // Direct link element - $link = $result; - $title = $link->title ?? trim($link->plaintext ?? ''); - } - - if ($link && isset($link->href) && ! empty($title)) { - $similarity = $this->calculateSimilarity($movie, $title); - - if ($similarity > $highestSimilarity) { - $highestSimilarity = $similarity; - $url = $link->href; - if (! str_starts_with($url, 'http')) { - $url = self::BASE_URL.'/'.ltrim($url, '/'); - } - $bestMatch = [ - 'title' => trim($title), - 'url' => $url, - 'similarity' => $similarity, - ]; - } - } - } - - if ($bestMatch !== null && $highestSimilarity >= $this->minimumSimilarity) { - return $bestMatch; - } - - return false; - } - - /** - * @return array - */ - protected function getMovieInfo(): array|false - { - $results = []; - - if (! empty($this->directUrl)) { - if (! empty($this->title)) { - $results['title'] = $this->title; - } - $results['directurl'] = $this->directUrl; - } - - // Try to get data from JSON-LD first (most reliable) - if ($this->jsonLdData !== null) { - $results = array_merge($results, $this->extractFromJsonLd()); - } - - // Get all the movie data (HTML fallback) - $synopsis = $this->extractSynopsis(); - if (! empty($synopsis)) { - $results = array_merge($results, $synopsis); - } - - $productInfo = $this->extractProductInfo(true); - if (! empty($productInfo)) { - $results = array_merge($results, $productInfo); - } - - $cast = $this->extractCast(); - if (! empty($cast)) { - $results = array_merge($results, $cast); - } - - $genres = $this->extractGenres(); - if (! empty($genres)) { - $results = array_merge($results, $genres); - } - - $covers = $this->extractCovers(); - if (! empty($covers)) { - $results = array_merge($results, $covers); - } - - if (empty($results) || (empty($results['title'] ?? '') && empty($results['boxcover'] ?? ''))) { - return false; - } - - return $results; - } - - /** - * Extract data from JSON-LD structured data. - * - * @return array - */ - protected function extractFromJsonLd(): array - { - $results = []; - - if ($this->jsonLdData === null) { - return $results; - } - - // Standard JSON-LD extraction - if (! empty($this->jsonLdData['name'])) { - $results['title'] = $this->jsonLdData['name']; - } - - if (! empty($this->jsonLdData['description'])) { - $results['synopsis'] = $this->jsonLdData['description']; - } - - if (! empty($this->jsonLdData['image'])) { - $image = is_array($this->jsonLdData['image']) ? ($this->jsonLdData['image'][0] ?? '') : $this->jsonLdData['image']; - if (! empty($image)) { - $results['boxcover'] = $image; - } - } - - if (! empty($this->jsonLdData['director'])) { - $director = $this->jsonLdData['director']; - if (is_array($director)) { - $results['director'] = $director['name'] ?? ($director[0]['name'] ?? ''); - } else { - $results['director'] = $director; - } - } - - if (! empty($this->jsonLdData['actor'])) { - $actors = $this->jsonLdData['actor']; - $cast = []; - if (is_array($actors)) { - foreach ($actors as $actor) { - if (is_array($actor) && ! empty($actor['name'])) { - $cast[] = $actor['name']; - } elseif (is_string($actor)) { - $cast[] = $actor; - } - } - } - if (! empty($cast)) { - $results['cast'] = $cast; - } - } - - if (! empty($this->jsonLdData['genre'])) { - $genres = $this->jsonLdData['genre']; - if (is_array($genres)) { - $results['genres'] = $genres; - } else { - $results['genres'] = [$genres]; - } - } - - return $results; - } - - /** - * @return array - */ - protected function extractCovers(): array - { - $res = []; - - // IAFD typically uses specific cover image selectors - $selectors = [ - 'div#titlecover img', - 'img#titlecover', - 'div.coverbox img', - 'img[src*="cover"]', - 'meta[property="og:image"]', - ]; - - foreach ($selectors as $selector) { - $ret = $this->getHtmlParser()->findOne($selector); - if ($ret) { // @phpstan-ignore if.alwaysTrue - $coverUrl = $ret->src ?? $ret->content ?? null; - - if (! empty($coverUrl)) { - if (str_starts_with($coverUrl, '//')) { - $coverUrl = 'https:'.$coverUrl; - } elseif (! str_starts_with($coverUrl, 'http')) { - $coverUrl = self::BASE_URL.'/'.ltrim($coverUrl, '/'); - } - - $res['boxcover'] = $coverUrl; - - return $res; - } - } - } - - return $res; - } - - /** - * @return array - */ - protected function extractSynopsis(): array - { - $res = []; - - $selectors = [ - 'div#synopsis', - 'div.synopsis', - 'td.syno', - 'p[class*="synopsis"]', - 'meta[name="description"]', - ]; - - foreach ($selectors as $selector) { - $ret = $this->getHtmlParser()->findOne($selector); - if ($ret) { // @phpstan-ignore if.alwaysTrue - $text = $ret->plaintext ?? $ret->content ?? ''; - if (! empty(trim($text))) { - $res['synopsis'] = trim($text); - - return $res; - } - } - } - - return $res; - } - - /** - * @return array - */ - protected function extractCast(): array - { - $res = []; - $cast = []; - - // IAFD has a specific cast table structure - $castTable = $this->getHtmlParser()->findOne('table#perfcast, div#perfcast'); - - if ($castTable) { // @phpstan-ignore if.alwaysTrue - $performers = $castTable->find('a[href*="/person.rme"]'); - foreach ($performers as $performer) { - $name = trim($performer->plaintext ?? ''); - if (! empty($name) && strlen($name) > 2) { - $cast[] = $name; - } - } - } - - // Fallback to general performer links - if (empty($cast)) { - $selectors = [ - 'a[href*="/person.rme"]', - 'a[href*="/performer/"]', - ]; - - foreach ($selectors as $selector) { - $elements = $this->getHtmlParser()->find($selector); - if (! empty($elements)) { - foreach ($elements as $element) { - $name = trim($element->plaintext ?? ''); - if (! empty($name) && strlen($name) > 2 && ! str_contains(strtolower($name), 'director')) { - $cast[] = $name; - } - } - if (! empty($cast)) { - break; - } - } - } - } - - if (! empty($cast)) { - $res['cast'] = array_unique($cast); - } - - return $res; - } - - /** - * @return array - */ - protected function extractGenres(): array - { - $res = []; - $genres = []; - - $selectors = [ - 'a[href*="/genre.rme"]', - 'div.genres a', - 'span.genre a', - ]; - - foreach ($selectors as $selector) { - $elements = $this->getHtmlParser()->find($selector); - if (! empty($elements)) { - foreach ($elements as $element) { - $text = trim($element->plaintext ?? ''); - if (! empty($text) && strlen($text) > 1) { - $genres[] = $text; - } - } - if (! empty($genres)) { - break; - } - } - } - - if (! empty($genres)) { - $res['genres'] = array_unique($genres); - } - - return $res; - } - - /** - * @return array - */ - protected function extractProductInfo(bool $extras = false): array - { - $res = []; - - // Look for studio - $studio = $this->getHtmlParser()->findOne('a[href*="/studio.rme"]'); - if ($studio) { // @phpstan-ignore if.alwaysTrue - $res['studio'] = trim($studio->plaintext ?? ''); - } - - // Look for distributor - $distributor = $this->getHtmlParser()->findOne('a[href*="/distrib.rme"]'); - if ($distributor) { // @phpstan-ignore if.alwaysTrue - $res['distributor'] = trim($distributor->plaintext ?? ''); - } - - // Look for release year - if (preg_match('/\((\d{4})\)/', $this->response, $yearMatch)) { - $res['year'] = $yearMatch[1]; - } - - // Look for director - $directorSelectors = [ - 'a[href*="/person.rme"][title*="director"]', - 'p:contains("Director") a', - ]; - - foreach ($directorSelectors as $selector) { - $director = $this->getHtmlParser()->findOne($selector); - if ($director) { // @phpstan-ignore if.alwaysTrue - $res['director'] = trim($director->plaintext ?? ''); - break; - } - } - - // Try to extract from info table - $infoRows = $this->getHtmlParser()->find('table.biodata tr, div.biodata p'); - foreach ($infoRows as $row) { - $text = $row->plaintext ?? ''; - if (stripos($text, 'Director:') !== false) { - $parts = explode(':', $text, 2); - if (count($parts) === 2) { - $res['director'] = trim($parts[1]); - } - } - if (stripos($text, 'Studio:') !== false) { - $parts = explode(':', $text, 2); - if (count($parts) === 2 && empty($res['studio'])) { - $res['studio'] = trim($parts[1]); - } - } - if (stripos($text, 'Minutes:') !== false || stripos($text, 'Runtime:') !== false) { - $parts = explode(':', $text, 2); - if (count($parts) === 2) { - $res['runtime'] = trim($parts[1]); - } - } - } - - return $res; - } -} diff --git a/app/Services/AdultProcessing/Pipes/PoppornPipe.php b/app/Services/AdultProcessing/Pipes/PoppornPipe.php deleted file mode 100644 index 2d6fc6253..000000000 --- a/app/Services/AdultProcessing/Pipes/PoppornPipe.php +++ /dev/null @@ -1,534 +0,0 @@ -getCleanTitle(); - - $searchResult = $this->search($movie); - - if ($searchResult === false) { - $this->outputNotFound(); - - return AdultProcessingResult::notFound($this->getName()); - } - - $this->title = $searchResult['title']; - $this->directUrl = $searchResult['url']; - - // Fetch the movie details page - $this->response = $this->fetchHtml($this->directUrl, $this->cookie); - - if ($this->response === false) { // @phpstan-ignore identical.alwaysFalse - return AdultProcessingResult::failed('Failed to fetch movie details page', $this->getName()); - } - - $this->getHtmlParser()->loadHtml($this->response); - - $movieInfo = $this->getMovieInfo(); - - if ($movieInfo === false) { - return AdultProcessingResult::notFound($this->getName()); - } - - $this->outputMatch($this->title); - - return AdultProcessingResult::matched( - $this->title, - $this->getName(), - $movieInfo - ); - } - - /** - * @return array - */ - protected function search(string $movie): array|false - { - if (empty($movie)) { - return false; - } - - // First, establish a session by visiting the AgeConfirmation endpoint to set cookies - $this->acceptAgeVerification(); - - $searchUrl = self::BASE_URL.self::SEARCH_ENDPOINT.urlencode($movie); - $response = $this->fetchHtml($searchUrl, $this->cookie); - - if (empty($response)) { - return false; - } - - $this->getHtmlParser()->loadHtml($response); - - $bestMatch = null; - $highestSimilarity = 0; - - // Try multiple selector patterns for search results - $resultSelectors = [ - 'div.product-info a, div.title a', - 'div.product-title a', - 'h3.product-title a', - ]; - - foreach ($resultSelectors as $selector) { - $results = $this->getHtmlParser()->find($selector); - - if (! empty($results)) { - foreach ($results as $result) { - $title = $result->title ?? $result->plaintext; - $url = $result->href; // @phpstan-ignore property.notFound - - if (! empty($title)) { - $similarity = $this->calculateSimilarity($movie, $title); - - if ($similarity > $highestSimilarity) { - $highestSimilarity = $similarity; - $bestMatch = [ - 'title' => trim($title), - 'url' => str_starts_with($url, 'http') ? $url : self::BASE_URL.$url, - ]; - } - } - } - - break; - } - } - - if ($bestMatch !== null && $highestSimilarity >= $this->minimumSimilarity) { - return $bestMatch; - } - - return false; - } - - /** - * @return array - */ - protected function getMovieInfo(): array|false - { - $results = []; - - if (! empty($this->directUrl)) { - if (! empty($this->title)) { - $results['title'] = $this->title; - } - $results['directurl'] = $this->directUrl; - } - - // Get all the movie data - $results = array_merge($results, $this->extractSynopsis()); - $results = array_merge($results, $this->extractProductInfo(true)); - $results = array_merge($results, $this->extractCast()); - $results = array_merge($results, $this->extractGenres()); - $results = array_merge($results, $this->extractCovers()); - $results = array_merge($results, $this->extractTrailers()); - - if (empty($results)) { - return false; - } - - return $results; - } - - /** - * @return array - */ - protected function extractCovers(): array - { - $res = []; - - // Method 1: Try structured data - if (preg_match('/"image":\s*"(.*?)"/is', $this->response, $match)) { - $res['boxcover'] = trim($match[1]); - // Try to determine backcover from boxcover pattern - if (stripos(trim($match[1]), '_aa') !== false) { - $res['backcover'] = str_ireplace('_aa', '_bb', trim($match[1])); - } else { - $res['backcover'] = str_ireplace('.jpg', '_b.jpg', trim($match[1])); - } - - return $res; - } - - // Method 2: Try multiple selectors - $selectors = [ - 'div[id=box-art], a[rel=box-art]', - 'img.front', - 'div.box-cover img', - 'div.product-image img', - ]; - - foreach ($selectors as $selector) { - if ($ret = $this->getHtmlParser()->findOne($selector)) { // @phpstan-ignore if.alwaysTrue - $res['boxcover'] = $ret->href ?? $ret->src; // @phpstan-ignore property.notFound - - // Try to determine backcover - if (stripos($res['boxcover'], '_aa') !== false) { - $res['backcover'] = str_ireplace('_aa', '_bb', $res['boxcover']); - } else { - $res['backcover'] = str_ireplace('.jpg', '_b.jpg', $res['boxcover']); - } - - return $res; - } - } - - return $res; // @phpstan-ignore deadCode.unreachable - } - - /** - * @return array - */ - protected function extractSynopsis(): array - { - $res = []; - - // Method 1: Try structured data - if (preg_match('/"description":\s*"(.*?)"/is', $this->response, $match)) { - $res['synopsis'] = trim(html_entity_decode(str_replace('\\u', '\\u', $match[1]))); - - return $res; - } - - // Method 2: Try multiple selectors - $selectors = [ - 'div[id=product-info] h3[class=highlight] + *', - 'div.product-description', - 'div.synopsis', - 'meta[name="description"]', - ]; - - foreach ($selectors as $selector) { - if ($ret = $this->getHtmlParser()->findOne($selector)) { // @phpstan-ignore if.alwaysTrue - $text = $ret->plaintext ?? $ret->content; // @phpstan-ignore property.notFound - - // Filter out "POPPORN EXCLUSIVE" text - if (stripos(trim($text), 'POPPORN EXCLUSIVE') !== false) { - if ($ret->next_sibling()) { - $text = trim($ret->next_sibling()->plaintext); - } - } - - if (! empty($text)) { - $res['synopsis'] = trim($text); - - return $res; - } - } - } - - return $res; - } - - /** - * @return array - */ - protected function extractTrailers(): array - { - $res = []; - - // Method 1: Try structured data - if (preg_match('/"contentUrl":\s*"(.*?)"/is', $this->response, $match)) { - $url = trim($match[1]); - if (! empty($url)) { - $res['trailers']['url'] = $url; - - return $res; - } - } - - // Method 2: Modern video embeds - $videoSelectors = [ - 'video source', - 'iframe[src*="trailer"]', - 'video[src]', - ]; - - foreach ($videoSelectors as $selector) { - $ret = $this->getHtmlParser()->findOne($selector); - if ($ret && isset($ret->src) && ! empty(trim($ret->src))) { // @phpstan-ignore booleanAnd.leftAlwaysTrue - $res['trailers']['url'] = trim($ret->src); - - return $res; - } - } - - return $res; - } - - /** - * @return array - */ - protected function extractProductInfo(bool $extras = false): array - { - $res = []; - $productInfo = []; - $director = ''; - - // Method 1: Try structured data - if (preg_match('/"director":\s*{[^}]*"name":\s*"(.*?)"/is', $this->response, $match)) { - $director = trim($match[1]); - } - - // Method 2: Look for product details in various formats - $selectors = [ - 'div#lside', - 'div.product-details', - 'div.product-info', - ]; - - foreach ($selectors as $selector) { - if ($ret = $this->getHtmlParser()->findOne($selector)) { // @phpstan-ignore if.alwaysTrue - // Extract country information - $country = false; - $rawInfo = []; - - foreach ($ret->find('text') as $e) { - $e = trim($e->innertext); - $e = str_replace([', ', '...', ' '], '', $e); - - if (stripos($e, 'Country:') !== false) { - $country = true; - } - - if ($country === true) { - if (stripos($e, 'addthis_config') === false) { - if (! empty($e)) { - $rawInfo[] = $e; - } - } else { - break; - } - } - } - - if (! empty($rawInfo)) { - $productInfo = array_chunk($rawInfo, 2, false); - break; - } - } - } - - $res['productinfo'] = $productInfo; - $res['director'] = $director; - - // Get extras if requested - if ($extras === true) { - $features = false; - $extrasData = []; - - $featureSelectors = [ - 'ul.stock-information', - 'div.features', - 'div.extras', - ]; - - foreach ($featureSelectors as $selector) { - if ($ret = $this->getHtmlParser()->findOne($selector)) { // @phpstan-ignore if.alwaysTrue - foreach ($ret->find('li') as $e) { - $text = trim($e->plaintext); - if ($text === 'Features:') { - $features = true; - - continue; - } - - if ($features === true && ! empty($text)) { - $extrasData[] = $text; - } - } - - if (! empty($extrasData)) { - $res['extras'] = $extrasData; - break; - } - } - } - } - - return $res; - } - - /** - * @return array - */ - protected function extractCast(): array - { - $res = []; - $cast = []; - $director = ''; - - // Method 1: Try structured data - if (preg_match_all('/"actor":\s*{[^}]*"name":\s*"(.*?)"/is', $this->response, $matches)) { - foreach ($matches[1] as $actor) { - $cast[] = trim($actor); - } - } - - if (preg_match('/"director":\s*{[^}]*"name":\s*"(.*?)"/is', $this->response, $match)) { - $director = trim($match[1]); - } - - // Method 2: Try multiple selectors - if (empty($cast)) { - $castSelectors = [ - 'div.cast a', - 'div.stars a', - 'div.performers a', - ]; - - foreach ($castSelectors as $selector) { - $elements = $this->getHtmlParser()->find($selector); - if (! empty($elements)) { - foreach ($elements as $element) { - $cast[] = trim($element->plaintext); - } - break; - } - } - } - - $res['cast'] = array_unique(array_filter($cast)); - $res['director'] = $director; - - return $res; - } - - /** - * @return array - */ - protected function extractGenres(): array - { - $res = []; - $genres = []; - - // Method 1: Try structured data - if (preg_match_all('/"genre":\s*"(.*?)"/is', $this->response, $matches)) { - foreach ($matches[1] as $genre) { - $genres[] = trim($genre); - } - } - - // Method 2: Try multiple selectors - if (empty($genres)) { - $selectors = [ - 'div[id=thekeywords] a', - 'p[class=keywords] a', - 'div.categories a', - 'div.tags a', - ]; - - foreach ($selectors as $selector) { - $elements = $this->getHtmlParser()->find($selector); - if (! empty($elements)) { - foreach ($elements as $e) { - $genres[] = trim($e->plaintext); - } - break; - } - } - } - - $res['genres'] = array_unique(array_filter($genres)); - - return $res; - } - - /** - * Accept age verification by visiting the confirmation endpoint. - * PopPorn uses a redirect-based age verification system. - */ - protected function acceptAgeVerification(): void - { - try { - // PopPorn requires visiting the AgeConfirmation endpoint with the URL you want to go to - // The confirmation sets a cookie that allows access - $client = $this->getHttpClient(); - - // PopPorn redirects to /AgeConfirmation?url2=/ on first visit - // We need to visit that page and follow through to set the etoken cookie - - // First, make a request that disables redirects to see where we're being sent - try { - $response = $client->get(self::BASE_URL.'/', [ - 'headers' => $this->getDefaultHeaders(), - 'allow_redirects' => false, - 'http_errors' => false, - ]); - - // Check for redirect to age confirmation - $statusCode = $response->getStatusCode(); - if ($statusCode === 302 || $statusCode === 301) { - $location = $response->getHeaderLine('Location'); - - // If redirected to AgeConfirmation, follow the flow - if (stripos($location, 'AgeConfirmation') !== false) { - // The redirect URL includes ?url2= parameter, we need to visit it - $ageConfirmUrl = $location; - if (! str_starts_with($ageConfirmUrl, 'http')) { - $ageConfirmUrl = self::BASE_URL.$ageConfirmUrl; - } - - // Visit the age confirmation page - $ageResponse = $client->get($ageConfirmUrl, [ - 'headers' => $this->getDefaultHeaders(), - 'allow_redirects' => true, - 'http_errors' => false, - ]); - - \Illuminate\Support\Facades\Log::debug('PopPorn age confirmation visited: '.$ageConfirmUrl); - } - } - } catch (\Exception $e) { - // Ignore redirect errors - } - - // Wait a moment to simulate human behavior - usleep(500000); // 500ms - - } catch (\Exception $e) { - // Log but don't fail - we'll try the search anyway - \Illuminate\Support\Facades\Log::debug('PopPorn age verification setup: '.$e->getMessage()); - } - } -} diff --git a/app/Services/PostProcessService.php b/app/Services/PostProcessService.php index 3e85f0f9e..30e64e8de 100644 --- a/app/Services/PostProcessService.php +++ b/app/Services/PostProcessService.php @@ -16,7 +16,6 @@ use dariusiii\rarinfo\Par2Info; * - NFO processing * - Movie/TV/Anime lookups * - Music/Books/Games/Console processing - * - XXX content processing * - Additional processing (RAR/ZIP contents, samples, etc.) */ final class PostProcessService @@ -51,8 +50,6 @@ final class PostProcessService private readonly AnimeProcessor $animeProcessor; - private readonly XXXProcessor $xxxProcessor; - public function __construct( ?NameFixingService $nameFixingService = null, ?Par2Info $par2Info = null, @@ -66,7 +63,6 @@ final class PostProcessService ?ConsolesProcessor $consolesProcessor = null, ?GamesProcessor $gamesProcessor = null, ?AnimeProcessor $animeProcessor = null, - ?XXXProcessor $xxxProcessor = null, ) { $this->echoOutput = (bool) config('nntmux.echocli'); $this->addPar2 = (bool) config('nntmux_settings.add_par2'); @@ -92,7 +88,6 @@ final class PostProcessService $this->consolesProcessor = $consolesProcessor ?? new ConsolesProcessor($this->echoOutput); $this->gamesProcessor = $gamesProcessor ?? new GamesProcessor($this->echoOutput); $this->animeProcessor = $animeProcessor ?? new AnimeProcessor($this->echoOutput); - $this->xxxProcessor = $xxxProcessor ?? new XXXProcessor($this->echoOutput); } /** @@ -110,7 +105,6 @@ final class PostProcessService $this->processGames(); $this->processAnime(); $this->processTv(); - $this->processXXX(); $this->processBooks(); } @@ -230,16 +224,6 @@ final class PostProcessService } } - /** - * Process XXX releases. - * - * @throws \Exception - */ - public function processXXX(): void - { - $this->xxxProcessor->process(); - } - /** * Process additional release data (RAR/ZIP contents, samples, media info). * diff --git a/app/Services/ReleaseRemoverService.php b/app/Services/ReleaseRemoverService.php index 044ae55fc..eb0d821db 100644 --- a/app/Services/ReleaseRemoverService.php +++ b/app/Services/ReleaseRemoverService.php @@ -835,7 +835,7 @@ class ReleaseRemoverService FROM releases r LEFT JOIN release_files rf ON r.id = rf.releases_id WHERE r.categories_id IN ({$categories}) - AND (r.imdbid NOT IN ('0000000', 0) OR xxxinfo_id > 0) + AND (r.imdbid NOT IN ('0000000', 0) OR r.categories_id BETWEEN ".Category::XXX_ROOT.' AND '.Category::XXX_OTHER.") AND r.nfostatus = 1 AND r.haspreview = 0 AND r.jpgstatus = 0 diff --git a/app/Services/Tmux/Tmux.php b/app/Services/Tmux/Tmux.php index 202937cb1..7c473ace7 100644 --- a/app/Services/Tmux/Tmux.php +++ b/app/Services/Tmux/Tmux.php @@ -191,7 +191,6 @@ class Tmux 'lookupbooks' => 'processbooks', 'lookupmusic' => 'processmusic', 'lookupgames' => 'processgames', - 'lookupxxx' => 'processxxx', 'lookupimdb' => 'processmovies', 'lookuptv' => 'processtvrage', 'lookupanidb' => 'processanime', @@ -311,12 +310,11 @@ class Tmux SUM(IF(categories_id BETWEEN %d AND %d AND consoleinfo_id IS NULL,1,0)) AS processconsole, SUM(IF(categories_id IN (%s) AND bookinfo_id IS NULL,1,0)) AS processbooks, SUM(IF(categories_id = %d AND gamesinfo_id = 0,1,0)) AS processgames, - SUM(IF(categories_id BETWEEN %d AND %d AND xxxinfo_id = 0,1,0)) AS processxxx, SUM(IF(1=1 %s,1,0)) AS processnfo, SUM(IF(isrenamed = %d AND predb_id = 0 AND passwordstatus >= 0 AND nfostatus > %d AND ((nfostatus = %d AND proc_nfo = %d) OR proc_files = %d OR proc_par2 = %d) AND categories_id IN (%s),1,0)) AS processrenames, SUM(IF(isrenamed = %d,1,0)) AS renamed, - SUM(IF(nfostatus = %20$d,1,0)) AS nfo, + SUM(IF(nfostatus = %19$d,1,0)) AS nfo, SUM(IF(predb_id > 0,1,0)) AS predb_matched, COUNT(DISTINCT(predb_id)) AS distinct_predb_matched FROM releases r', @@ -333,8 +331,6 @@ class Tmux Category::GAME_OTHER, $bookreqids, Category::PC_GAMES, - Category::XXX_ROOT, - Category::XXX_X264, NfoService::NfoQueryString(), NameFixingService::IS_RENAMED_NONE, NfoService::NFO_UNPROC, diff --git a/app/Services/Tmux/TmuxLayoutBuilder.php b/app/Services/Tmux/TmuxLayoutBuilder.php index 9258e4cfc..f73cf0c50 100644 --- a/app/Services/Tmux/TmuxLayoutBuilder.php +++ b/app/Services/Tmux/TmuxLayoutBuilder.php @@ -40,7 +40,6 @@ class TmuxLayoutBuilder 'postprocessing_tv' => '󰟴 TV/Anime', 'postprocessing_amazon' => ' Amazon', 'postprocessing_movies' => ' Movies', - 'postprocessing_xxx' => '󰞋 XXX', // IRC 'scrapeIRC' => '󰻞 IRC Scraper', @@ -125,7 +124,6 @@ class TmuxLayoutBuilder // Right side: After left splits, right side is now 2.3 $this->paneManager->selectPane('2.3'); $this->paneManager->setPaneTitle('2.3', $this->getPaneDisplayName('postprocessing_movies')); - $this->paneManager->splitVertical('2', '50%', $this->getPaneDisplayName('postprocessing_xxx')); // Window 3: IRC Scraper $this->createIRCScraperWindow(); @@ -171,7 +169,6 @@ class TmuxLayoutBuilder // Right side: After left splits, right side is now 2.3 $this->paneManager->selectPane('2.3'); $this->paneManager->setPaneTitle('2.3', $this->getPaneDisplayName('postprocessing_movies')); - $this->paneManager->splitVertical('2', '50%', $this->getPaneDisplayName('postprocessing_xxx')); // Window 3: IRC Scraper $this->createIRCScraperWindow(); diff --git a/app/Services/Tmux/TmuxOutput.php b/app/Services/Tmux/TmuxOutput.php index fdbf11185..f056b484e 100644 --- a/app/Services/Tmux/TmuxOutput.php +++ b/app/Services/Tmux/TmuxOutput.php @@ -359,16 +359,15 @@ class TmuxOutput extends Tmux 'XXX', sprintf( '%s(%s)', - number_format($this->runVar['counts']['now']['processxxx']), - $this->runVar['counts']['diff']['processxxx'] + '0', + '0' ), sprintf( '%s(%d%%)', - number_format($this->runVar['counts']['now']['xxx']), - $this->runVar['counts']['percent']['xxx'] + number_format($this->runVar['counts']['now']['xxx'] ?? 0), + $this->runVar['counts']['percent']['xxx'] ?? 0 ) ); - $buffer .= $this->_getSeparator(); $buffer .= sprintf( diff --git a/app/Services/Tmux/TmuxTaskRunner.php b/app/Services/Tmux/TmuxTaskRunner.php index f0593b474..c0f5304f4 100644 --- a/app/Services/Tmux/TmuxTaskRunner.php +++ b/app/Services/Tmux/TmuxTaskRunner.php @@ -319,7 +319,6 @@ class TmuxTaskRunner 'tv' => $this->runTvTask($runVar), 'movies' => $this->runMoviesTask($runVar), 'amazon' => $this->runAmazonTask($runVar), - 'xxx' => $this->runXXXTask($runVar), 'scraper' => $this->runIRCScraper($runVar), // Legacy mapping for backward compatibility 'nonamazon' => $this->runTvTask($runVar), @@ -779,32 +778,4 @@ class TmuxTaskRunner return $this->paneManager->respawnPane($pane, $fullCommand); } - - /** - * Run XXX post-processing - * - * @param array $runVar - */ - protected function runXXXTask(array $runVar): bool - { - $enabled = (int) ($runVar['settings']['processxxx'] ?? 0); - $pane = '2.4'; - - if ($enabled !== 1) { - return $this->disablePane($pane, 'Post-process XXX', 'disabled in settings'); - } - - $hasWork = (int) ($runVar['counts']['now']['processxxx'] ?? 0) > 0; - - if (! $hasWork) { - return $this->disablePane($pane, 'Post-process XXX', 'no XXX releases to process'); - } - - $niceness = Settings::settingValue('niceness') ?? 2; - $command = "nice -n{$niceness} ".PHP_BINARY.' artisan update:postprocess xxx true'; - $sleep = (int) ($runVar['settings']['post_timer_amazon'] ?? 300); - $command = $this->buildCommand($command, ['log_pane' => 'post_xxx', 'sleep' => $sleep]); - - return $this->paneManager->respawnPane($pane, $command); - } } diff --git a/app/Services/XXXProcessor.php b/app/Services/XXXProcessor.php deleted file mode 100644 index 4c3b0b5ab..000000000 --- a/app/Services/XXXProcessor.php +++ /dev/null @@ -1,38 +0,0 @@ -echooutput = $echooutput; - } - - public function process(): void - { - if ((int) Settings::settingValue('lookupxxx') === 1) { - $pipeline = new AdultProcessingPipeline([], $this->echooutput); - $pipeline->processXXXReleases(); - } - } - - /** - * Process a single movie title and return the result. - * - * @param string $movie Movie title to look up - * @param bool $debug Whether to include debug information - * @return array Processing result - */ - public function lookupMovie(string $movie, bool $debug = false): array - { - $pipeline = new AdultProcessingPipeline([], $this->echooutput); - - return $pipeline->processMovie($movie, $debug); - } -} diff --git a/app/Services/XxxBrowseService.php b/app/Services/XxxBrowseService.php deleted file mode 100644 index ba45b8aa1..000000000 --- a/app/Services/XxxBrowseService.php +++ /dev/null @@ -1,215 +0,0 @@ -showPasswords = app(ReleaseBrowseService::class)->showPasswords(); - } - - /** - * Get XXX releases with covers for xxx browse page. - * - * Uses three separate queries instead of GROUP_CONCAT: - * 1. COUNT query for total results (replaces SQL_CALC_FOUND_ROWS) - * 2. Paginated XXX entity list with only needed columns - * 3. Top 2 releases per XXX entity using ROW_NUMBER() window function - * - * @param array $cat - * @param array $excludedCats - */ - public function getXXXRange(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) { // @phpstan-ignore offsetAccess.notFound - $catSrch = Category::getCategorySearch($cat); - } - $order = $this->getXXXOrder($orderBy); - $expiresAt = now()->addMinutes(config('nntmux.cache_expiry_medium')); - - $browseBy = $this->getBrowseBy(); - $whereAge = $maxAge > 0 ? 'AND r.postdate > NOW() - INTERVAL '.$maxAge.' DAY ' : ''; - $whereExcluded = \count($excludedCats) > 0 ? ' AND r.categories_id NOT IN ('.implode(',', $excludedCats).')' : ''; - - $baseWhere = "xxx.title != '' " - ."AND r.passwordstatus {$this->showPasswords} " - .$browseBy.' ' - .$catSrch.' ' - .$whereAge - .$whereExcluded; - - $cacheKey = md5('xxx_range_'.$baseWhere.$order[0].$order[1].$start.$num.$page); // @phpstan-ignore offsetAccess.notFound - - $cached = Cache::get($cacheKey); - if ($cached !== null) { - return $cached; - } - - // Step 1: Count total distinct XXX entities matching filters - $countSql = 'SELECT COUNT(DISTINCT xxx.id) AS total ' - .'FROM xxxinfo xxx ' - .'INNER JOIN releases r ON xxx.id = r.xxxinfo_id ' - .'WHERE '.$baseWhere; - - $totalResult = DB::select($countSql); - $totalCount = $totalResult[0]->total ?? 0; - - if ($totalCount === 0) { - return collect(); - } - - // Step 2: Get paginated XXX entity list with only needed columns - $xxxSql = 'SELECT xxx.id, xxx.title, xxx.tagline, xxx.cover, xxx.genre, xxx.director, xxx.actors, ' - .'UNCOMPRESS(xxx.plot) AS plot, ' - .'MAX(r.postdate) AS latest_postdate, ' - .'COUNT(r.id) AS total_releases ' - .'FROM xxxinfo xxx ' - .'INNER JOIN releases r ON xxx.id = r.xxxinfo_id ' - .'WHERE '.$baseWhere.' ' - .'GROUP BY xxx.id, xxx.title, xxx.tagline, xxx.cover, xxx.genre, xxx.director, xxx.actors, xxx.plot ' - ."ORDER BY {$order[0]} {$order[1]} " // @phpstan-ignore offsetAccess.notFound - ."LIMIT {$num} OFFSET {$start}"; - - $xxxEntities = XxxInfo::fromQuery($xxxSql); - - if ($xxxEntities->isEmpty()) { - return collect(); - } - - // Build list of XXX IDs for release query - $xxxIds = $xxxEntities->pluck('id')->toArray(); - $inXxxIds = implode(',', array_map('intval', $xxxIds)); - - // Step 3: Get top 2 releases per XXX entity using ROW_NUMBER() - $releasesSql = 'SELECT ranked.id, ranked.xxxinfo_id, ranked.guid, ranked.searchname, ' - .'ranked.size, ranked.postdate, ranked.adddate, ranked.haspreview, ranked.grabs, ' - .'ranked.comments, ranked.totalpart, ranked.group_name, ranked.nfoid, ranked.failed_count ' - .'FROM ( ' - .'SELECT r.id, r.xxxinfo_id, r.guid, r.searchname, r.size, r.postdate, r.adddate, ' - .'r.haspreview, r.grabs, r.comments, r.totalpart, g.name AS group_name, ' - .'rn.releases_id AS nfoid, df.failed AS failed_count, ' - .'ROW_NUMBER() OVER (PARTITION BY r.xxxinfo_id ORDER BY r.postdate DESC) AS rn ' - .'FROM releases r ' - .'LEFT JOIN usenet_groups g ON g.id = r.groups_id ' - .'LEFT JOIN release_nfos rn ON rn.releases_id = r.id ' - .'LEFT JOIN dnzb_failures df ON df.release_id = r.id ' - ."WHERE r.xxxinfo_id IN ({$inXxxIds}) " - ."AND r.passwordstatus {$this->showPasswords} " - .$catSrch.' ' - .$whereAge - .$whereExcluded - .') ranked ' - .'WHERE ranked.rn <= 2 ' - .'ORDER BY ranked.xxxinfo_id, ranked.postdate DESC'; - - $releases = DB::select($releasesSql); - - // Group releases by xxxinfo_id for fast lookup - $releasesByXxx = []; - foreach ($releases as $release) { - $releasesByXxx[$release->xxxinfo_id][] = $release; - } - - // Attach releases to each XXX entity - foreach ($xxxEntities as $xxx) { - $xxx->releases = $releasesByXxx[$xxx->id] ?? []; // @phpstan-ignore assign.propertyReadOnly - } - - // Set total count on first item (matches existing pattern used by controllers) - if ($xxxEntities->isNotEmpty()) { - $xxxEntities[0]->_totalcount = $totalCount; // @phpstan-ignore property.notFound - } - - Cache::put($cacheKey, $xxxEntities, $expiresAt); - - return $xxxEntities; - } - - /** - * Get the order type the user requested on the xxx page. - * - * @return array - */ - protected function getXXXOrder(string $orderBy): array - { - $orderArr = explode('_', (($orderBy === '') ? 'r.postdate' : $orderBy)); - $orderField = match ($orderArr[0]) { - 'title' => 'xxx.title', - default => 'r.postdate', - }; - - return [$orderField, isset($orderArr[1]) && preg_match('/^asc|desc$/i', $orderArr[1]) ? $orderArr[1] : 'desc']; - } - - /** - * Order types for xxx page. - * - * @return array - */ - public function getXXXOrdering(): array - { - return ['title_asc', 'title_desc', 'name_asc', 'name_desc', 'size_asc', 'size_desc', 'posted_asc', 'posted_desc', 'cat_asc', 'cat_desc']; - } - - protected function getBrowseBy(): string - { - $browseBy = ' '; - foreach (['title', 'director', 'actors', 'genre', 'id'] as $bb) { - if (! empty($_REQUEST[$bb])) { - $bbv = stripslashes($_REQUEST[$bb]); - if ($bb === 'genre') { - $bbv = XxxInfo::getGenreID($bbv); - } - if ($bb === 'id') { - $browseBy .= ' AND xxx.'.$bb.'='.$bbv; - } else { - $browseBy .= ' AND xxx.'.$bb.' '.'LIKE '.escapeString('%'.$bbv.'%'); - } - } - } - - return $browseBy; - } - - /** - * Inserts Trailer Code by Class. - */ - public function insertSwf(string $whichClass, ?string $res): string - { - $ret = ''; - if (($whichClass === 'ade') && ! empty($res)) { - $trailers = unserialize($res, ['allowed_classes' => false]); - $ret .= ""; - $ret .= ""; - $ret .= ''; - - return $ret; - } - if (($whichClass === 'pop') && ! empty($res)) { - $trailers = unserialize($res, ['allowed_classes' => false]); - $ret .= ""; - - return $ret; - } - - return $ret; - } -} diff --git a/bootstrap/providers.php b/bootstrap/providers.php index 8c66860f3..a06762558 100644 --- a/bootstrap/providers.php +++ b/bootstrap/providers.php @@ -2,7 +2,6 @@ return [ App\Providers\AdditionalProcessingServiceProvider::class, - App\Providers\AdultProcessingServiceProvider::class, App\Providers\AppServiceProvider::class, App\Providers\CategorizationServiceProvider::class, App\Providers\ForumServiceProvider::class, diff --git a/database/migrations/2026_02_13_000000_drop_xxxinfo_and_releases_xxxinfo_id.php b/database/migrations/2026_02_13_000000_drop_xxxinfo_and_releases_xxxinfo_id.php new file mode 100644 index 000000000..c3549a593 --- /dev/null +++ b/database/migrations/2026_02_13_000000_drop_xxxinfo_and_releases_xxxinfo_id.php @@ -0,0 +1,51 @@ +dropIndex('ix_releases_xxxinfo_id'); + $table->dropColumn('xxxinfo_id'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('releases', function (Blueprint $table) { + $table->integer('xxxinfo_id')->default(0)->after('imdbid'); + $table->index('xxxinfo_id', 'ix_releases_xxxinfo_id'); + }); + + Schema::create('xxxinfo', function (Blueprint $table) { + $table->increments('id'); + $table->string('title', 1024); + $table->string('tagline', 1024); + $table->binary('plot')->nullable(); + $table->string('genre', 255); + $table->string('director', 255)->nullable(); + $table->string('actors', 2500); + $table->text('extras')->nullable(); + $table->text('productinfo')->nullable(); + $table->text('trailers')->nullable(); + $table->string('directurl', 2000); + $table->string('classused', 20)->default(''); + $table->boolean('cover')->default(false); + $table->boolean('backdrop')->default(false); + $table->timestamps(); + $table->unique('title', 'ix_xxxinfo_title'); + }); + } +}; diff --git a/database/schema/mariadb-schema.sql b/database/schema/mariadb-schema.sql index e2a3b777f..393e57439 100644 --- a/database/schema/mariadb-schema.sql +++ b/database/schema/mariadb-schema.sql @@ -893,7 +893,6 @@ CREATE TABLE `releases` ( `videos_id` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'FK to videos.id of the parent series.', `tv_episodes_id` int(11) NOT NULL DEFAULT 0 COMMENT 'FK to tv_episodes.id for the episode.', `imdbid` varchar(100) DEFAULT NULL, - `xxxinfo_id` int(11) NOT NULL DEFAULT 0, `musicinfo_id` int(11) DEFAULT NULL COMMENT 'FK to musicinfo.id', `consoleinfo_id` int(11) DEFAULT NULL COMMENT 'FK to consoleinfo.id', `gamesinfo_id` int(11) NOT NULL DEFAULT 0, @@ -938,7 +937,6 @@ CREATE TABLE `releases` ( KEY `ix_releases_videos_id` (`videos_id`), KEY `ix_releases_tv_episodes_id` (`tv_episodes_id`), KEY `ix_releases_imdbid` (`imdbid`), - KEY `ix_releases_xxxinfo_id` (`xxxinfo_id`), KEY `ix_releases_consoleinfo_id` (`consoleinfo_id`), KEY `ix_releases_gamesinfo_id` (`gamesinfo_id`), KEY `ix_releases_bookinfo_id` (`bookinfo_id`), @@ -1290,29 +1288,6 @@ CREATE TABLE `videos_aliases` ( PRIMARY KEY (`videos_id`,`title`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci; -DROP TABLE IF EXISTS `xxxinfo`; - -CREATE TABLE `xxxinfo` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `title` varchar(1024) NOT NULL, - `tagline` varchar(1024) NOT NULL, - `plot` blob DEFAULT NULL, - `genre` varchar(255) NOT NULL, - `director` varchar(255) DEFAULT NULL, - `actors` varchar(2500) NOT NULL, - `extras` text DEFAULT NULL, - `productinfo` text DEFAULT NULL, - `trailers` text DEFAULT NULL, - `directurl` varchar(2000) NOT NULL, - `classused` varchar(20) NOT NULL DEFAULT '', - `cover` tinyint(1) NOT NULL DEFAULT 0, - `backdrop` tinyint(1) NOT NULL DEFAULT 0, - `created_at` timestamp NULL DEFAULT NULL, - `updated_at` timestamp NULL DEFAULT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `ix_xxxinfo_title` (`title`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci; - INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (1,'2014_01_16_195548_create_users_table',1); INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (2,'2014_02_01_311070_create_firewall_table',1); INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (3,'2017_11_29_223842_create_countries_table',1); @@ -1366,7 +1341,6 @@ INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (52,'2018_01_20_200 INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (53,'2018_01_20_200346_create_video_data_table',1); INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (54,'2018_01_20_200353_create_videos_table',1); INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (55,'2018_01_20_200403_create_videos_aliases_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (56,'2018_01_20_200417_create_xxxinfo_table',1); INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (58,'2018_04_24_132758_create_cache_table',1); INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (59,'2018_08_08_100000_create_telescope_entries_table',1); INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (60,'2018_09_13_070520_add_verification_to_user_table',1); diff --git a/database/schema/mysql-schema.sql b/database/schema/mysql-schema.sql index e2a3b777f..393e57439 100644 --- a/database/schema/mysql-schema.sql +++ b/database/schema/mysql-schema.sql @@ -893,7 +893,6 @@ CREATE TABLE `releases` ( `videos_id` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'FK to videos.id of the parent series.', `tv_episodes_id` int(11) NOT NULL DEFAULT 0 COMMENT 'FK to tv_episodes.id for the episode.', `imdbid` varchar(100) DEFAULT NULL, - `xxxinfo_id` int(11) NOT NULL DEFAULT 0, `musicinfo_id` int(11) DEFAULT NULL COMMENT 'FK to musicinfo.id', `consoleinfo_id` int(11) DEFAULT NULL COMMENT 'FK to consoleinfo.id', `gamesinfo_id` int(11) NOT NULL DEFAULT 0, @@ -938,7 +937,6 @@ CREATE TABLE `releases` ( KEY `ix_releases_videos_id` (`videos_id`), KEY `ix_releases_tv_episodes_id` (`tv_episodes_id`), KEY `ix_releases_imdbid` (`imdbid`), - KEY `ix_releases_xxxinfo_id` (`xxxinfo_id`), KEY `ix_releases_consoleinfo_id` (`consoleinfo_id`), KEY `ix_releases_gamesinfo_id` (`gamesinfo_id`), KEY `ix_releases_bookinfo_id` (`bookinfo_id`), @@ -1290,29 +1288,6 @@ CREATE TABLE `videos_aliases` ( PRIMARY KEY (`videos_id`,`title`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci; -DROP TABLE IF EXISTS `xxxinfo`; - -CREATE TABLE `xxxinfo` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `title` varchar(1024) NOT NULL, - `tagline` varchar(1024) NOT NULL, - `plot` blob DEFAULT NULL, - `genre` varchar(255) NOT NULL, - `director` varchar(255) DEFAULT NULL, - `actors` varchar(2500) NOT NULL, - `extras` text DEFAULT NULL, - `productinfo` text DEFAULT NULL, - `trailers` text DEFAULT NULL, - `directurl` varchar(2000) NOT NULL, - `classused` varchar(20) NOT NULL DEFAULT '', - `cover` tinyint(1) NOT NULL DEFAULT 0, - `backdrop` tinyint(1) NOT NULL DEFAULT 0, - `created_at` timestamp NULL DEFAULT NULL, - `updated_at` timestamp NULL DEFAULT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `ix_xxxinfo_title` (`title`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci; - INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (1,'2014_01_16_195548_create_users_table',1); INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (2,'2014_02_01_311070_create_firewall_table',1); INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (3,'2017_11_29_223842_create_countries_table',1); @@ -1366,7 +1341,6 @@ INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (52,'2018_01_20_200 INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (53,'2018_01_20_200346_create_video_data_table',1); INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (54,'2018_01_20_200353_create_videos_table',1); INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (55,'2018_01_20_200403_create_videos_aliases_table',1); -INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (56,'2018_01_20_200417_create_xxxinfo_table',1); INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (58,'2018_04_24_132758_create_cache_table',1); INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (59,'2018_08_08_100000_create_telescope_entries_table',1); INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (60,'2018_09_13_070520_add_verification_to_user_table',1); diff --git a/manage_adult_cookies.php b/manage_adult_cookies.php deleted file mode 100644 index 5e94d13f7..000000000 --- a/manage_adult_cookies.php +++ /dev/null @@ -1,169 +0,0 @@ -#!/usr/bin/env php - 'Adult DVD Empire', - 'https://www.adultdvdmarketplace.com' => 'Adult DVD Marketplace', - 'https://straight.theater.aebn.net' => 'AEBN Theater', - 'https://www.hotmovies.com' => 'HotMovies', - 'https://www.popporn.com' => 'Popporn', - ]; - - foreach ($sites as $url => $name) { - echo "Setting up {$name}...\n"; - try { - $cookieJar = $manager->getCookieJar($url); - $count = count($cookieJar); - echo " ✓ Cookies initialized ({$count} cookies)\n"; - } catch (Exception $e) { - echo " ❌ Failed: {$e->getMessage()}\n"; - } - echo "\n"; - } - - echo 'Done! Cookies are saved to: '.$manager->getCookieDirectory()."\n"; - break; - - case 'stats': - echo "================================================================================\n"; - echo "Adult Site Cookie Statistics\n"; - echo "================================================================================\n\n"; - - $stats = $manager->getCookieStats(); - - echo "Total domains with cookies: {$stats['total_domains']}\n"; - echo "Total cookies stored: {$stats['total_cookies']}\n\n"; - - echo "Domain Details:\n"; - echo str_repeat('-', 80)."\n"; - - foreach ($stats['domains'] as $domain => $info) { - $status = $info['has_cookies'] ? '✓' : '❌'; - $cookies = $info['cookie_count']; - echo sprintf(" %s %-40s %d cookies\n", $status, $domain, $cookies); - - if ($info['has_cookies']) { - echo " File: {$info['file']}\n"; - } - } - - echo "\n"; - break; - - case 'clear': - if ($argument) { - echo "Clearing cookies for: {$argument}\n"; - if ($manager->clearCookies($argument)) { - echo "✓ Cookies cleared\n"; - } else { - echo "❌ No cookies found for this domain\n"; - } - } else { - echo "Clearing all adult site cookies...\n"; - $cleared = $manager->clearAllCookies(); - echo "✓ Cleared {$cleared} cookie file(s)\n"; - } - break; - - case 'test': - echo "================================================================================\n"; - echo "Testing Adult Site Access\n"; - echo "================================================================================\n\n"; - - $testSites = [ - 'https://www.adultdvdempire.com/dvd/search?q=test' => 'Adult DVD Empire', - 'https://www.adultdvdmarketplace.com' => 'Adult DVD Marketplace', - 'https://straight.theater.aebn.net' => 'AEBN Theater', - 'https://www.hotmovies.com' => 'HotMovies', - 'https://www.popporn.com' => 'Popporn', - ]; - - if ($argument) { - // Test specific site - $testSites = array_filter($testSites, function ($name, $url) use ($argument) { - return stripos($url, $argument) !== false; - }, ARRAY_FILTER_USE_BOTH); - } - - foreach ($testSites as $url => $name) { - echo "Testing {$name}...\n"; - try { - $response = $manager->makeRequest($url); - - if ($response === false) { - echo " ❌ Request failed\n"; - } else { - $responseLength = strlen($response); - echo " ✓ Response received ({$responseLength} bytes)\n"; - - // Check for age verification indicators - if (stripos($response, 'age') !== false && - (stripos($response, 'verification') !== false || - stripos($response, 'confirm') !== false)) { - echo " ⚠️ Age verification page detected\n"; - } else { - echo " ✓ Successfully bypassed age gate\n"; - } - } - } catch (Exception $e) { - echo " ❌ Error: {$e->getMessage()}\n"; - } - echo "\n"; - } - break; - - case 'help': - default: - echo "================================================================================\n"; - echo "Adult Site Cookie Manager\n"; - echo "================================================================================\n\n"; - - echo "Usage:\n"; - echo " php manage_adult_cookies.php [arguments]\n\n"; - - echo "Commands:\n"; - echo " init Initialize age verification cookies for all sites\n"; - echo " stats Show statistics about stored cookies\n"; - echo " clear [domain] Clear cookies (all or specific domain)\n"; - echo " test [domain] Test if cookies work for sites\n"; - echo " help Show this help message\n\n"; - - echo "Examples:\n"; - echo " php manage_adult_cookies.php init\n"; - echo " php manage_adult_cookies.php stats\n"; - echo " php manage_adult_cookies.php clear adultdvdempire.com\n"; - echo " php manage_adult_cookies.php test\n\n"; - - echo "Cookie Storage:\n"; - echo " Cookies are stored in: storage/app/cookies/adult_sites/\n"; - echo " Format: JSON files, one per domain\n"; - echo " Expiry: 1 year from creation\n\n"; - break; -} diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index a52621b86..d387ff2e8 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -390,18 +390,6 @@ parameters: count: 1 path: app/Models/UserActivityStat.php - - - message: '#^Method App\\Models\\XxxInfo\:\:getAllGenres\(\) should return array\ but returns list\.$#' - identifier: return.type - count: 1 - path: app/Models/XxxInfo.php - - - - message: '#^Method App\\Models\\XxxInfo\:\:getGenres\(\) should return array\ but returns App\\Models\\Genre\|null\.$#' - identifier: return.type - count: 2 - path: app/Models/XxxInfo.php - - message: '#^Method App\\Services\\AdditionalProcessing\\ArchiveExtractionService\:\:extractViaSevenZip\(\) should return list\ but returns array\{success\: false, files\: array\{\}, hasPassword\: false, passwordStatus\: 0\}\.$#' identifier: return.type @@ -462,18 +450,6 @@ parameters: count: 1 path: app/Services/AdditionalProcessing/NzbContentParser.php - - - message: '#^Method App\\Services\\AdultProcessing\\AgeVerificationManager\:\:getStoredDomains\(\) should return array\ but returns list\\.$#' - identifier: return.type - count: 1 - path: app/Services/AdultProcessing/AgeVerificationManager.php - - - - message: '#^PHPDoc tag @return with type list\ is incompatible with native type bool\.$#' - identifier: return.phpDocType - count: 1 - path: app/Services/AdultProcessing/AgeVerificationManager.php - - message: '#^Method App\\Services\\Archives\\SevenZipPartialParser\:\:getFilesByExtension\(\) should return array\\> but returns array\\.$#' identifier: return.type @@ -780,12 +756,6 @@ parameters: count: 1 path: app/Services/XrefService.php - - - message: '#^Method App\\Services\\XxxBrowseService\:\:getXXXOrder\(\) should return array\ but returns array\\.$#' - identifier: return.type - count: 1 - path: app/Services/XxxBrowseService.php - - message: '#^Method App\\Support\\UpdatePerformanceHelper\:\:checkSystemResources\(\) should return array\ but returns list\\.$#' identifier: return.type diff --git a/resources/views/admin/site/edit.blade.php b/resources/views/admin/site/edit.blade.php index 135c9ed4d..f89b51ab2 100644 --- a/resources/views/admin/site/edit.blade.php +++ b/resources/views/admin/site/edit.blade.php @@ -431,20 +431,6 @@

Whether to attempt to lookup game information from Amazon.

- -
- - -

Whether to attempt to lookup XXX information when processing binaries.

-
@@ -878,15 +864,6 @@

The maximum amount of books to process with amazon per run. This does not use an NNTP connection

-
- - -

The maximum amount of XXX to process per run.

-
-
-
- xxxview ?? 0)) ? 'checked' : '' }} - class="h-4 w-4 text-blue-600 dark:text-blue-400 focus:ring-blue-500 border-gray-300 dark:border-gray-600 rounded"> - -
@endif diff --git a/resources/views/details/index.blade.php b/resources/views/details/index.blade.php index de3305ebd..20a0de9f4 100644 --- a/resources/views/details/index.blade.php +++ b/resources/views/details/index.blade.php @@ -253,41 +253,6 @@ @endif - - @if(!empty($xxx)) - @php - $xxxData = is_object($xxx) ? get_object_vars($xxx) : $xxx; - $xxxTitle = $xxxData['title'] ?? ($xxx->title ?? null); - $xxxGenre = $xxxData['genre'] ?? ($xxx->genre ?? null); - $xxxActors = $xxxData['actors'] ?? ($xxx->actors ?? null); - @endphp -
-

- Adult Content Information -

-
- @if(!empty($xxxTitle)) -
-
Title
-
{{ $xxxTitle }}
-
- @endif - @if(!empty($xxxGenre)) -
-
Genre
-
{!! $xxxGenre !!}
-
- @endif - @if(!empty($xxxActors)) -
-
Actors
-
{!! $xxxActors !!}
-
- @endif -
-
- @endif - @if(!empty($music)) @php diff --git a/resources/views/profile/edit.blade.php b/resources/views/profile/edit.blade.php index 779808269..9dff4b5a3 100644 --- a/resources/views/profile/edit.blade.php +++ b/resources/views/profile/edit.blade.php @@ -183,11 +183,6 @@ class="rounded border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-400 focus:ring-blue-500 mr-2"> Book Covers - diff --git a/resources/views/profile/index.blade.php b/resources/views/profile/index.blade.php index f36a61133..309d7a9f4 100644 --- a/resources/views/profile/index.blade.php +++ b/resources/views/profile/index.blade.php @@ -325,10 +325,6 @@ Book Covers -
- - XXX Covers -
diff --git a/resources/views/xxx/index.blade.php b/resources/views/xxx/index.blade.php index 4ebbe2b9d..35516ab40 100644 --- a/resources/views/xxx/index.blade.php +++ b/resources/views/xxx/index.blade.php @@ -7,276 +7,125 @@ @section('content')
-
+
- -
-
-
- -
- - -
- - -
- - -
- - -
- - -
- - -
- - -
-
- -
- - - Clear + + - - - @if(count($results) > 0) -
-
-

- - {{ $catname ?? 'All' }} XXX -

- -
- - {{ $results->total() }} results found - -
- - -
- @foreach($results as $result) - @php - $releases = $result->releases ?? []; - $totalReleases = $result->total_releases ?? count($releases); - $maxReleases = 2; - $displayReleases = array_slice($releases, 0, $maxReleases); - $guid = !empty($displayReleases) ? $displayReleases[0]->guid : null; - $totalFailed = collect($releases)->sum(fn($r) => (int)($r->failed_count ?? 0)); - @endphp - -
-
- - - - -
-
-
-

{{ $result->title }}

- - @if($totalFailed > 0) -
- - {{ $totalFailed }} failed report{{ $totalFailed > 1 ? 's' : '' }} -
- @endif - -
- @if(!empty($result->genre)) -
- Genre: {!! makeFieldLinks((array) $result, 'genre', 'xxx') !!} -
- @endif - @if(!empty($result->actors)) -
- Actors: {!! makeFieldLinks((array) $result, 'actors', 'xxx') !!} -
- @endif - @if(!empty($result->director)) -
- Director: {!! makeFieldLinks((array) $result, 'director', 'xxx') !!} -
- @endif -
- - @if(isset($result->plot) && $result->plot) -

{{ $result->plot }}

- @endif -
-
- - - @if(!empty($displayReleases)) -
-

- Available Releases - @if($totalReleases > $maxReleases) - (Showing {{ $maxReleases }} of {{ $totalReleases }}) - @endif -

-
- @foreach($displayReleases as $release) - @if($release->searchname) -
-
- - - {{ $release->searchname }} - - - -
- @if(isset($release->size)) - - {{ number_format($release->size / 1073741824, 2) }} GB - - @endif - @if(isset($release->postdate)) - - {{ date('M d, Y H:i', strtotime($release->postdate)) }} - - @endif - @if(isset($release->grabs) && $release->grabs > 0) - - {{ $release->grabs }} grabs - - @endif - @if(isset($release->nfoid) && !empty($release->nfoid)) - - @endif - @if(isset($release->haspreview) && $release->haspreview == 1) - - @endif -
- - - -
-
- @endif - @endforeach -
-
- @endif -
-
-
@endforeach
+
+ Order by: + @foreach($ordering ?? [] as $ob) + + {{ str_replace('_', ' ', $ob) }} + + @endforeach +
+
+ + @if($results->count() > 0) +
+ {{ $results->total() }} results found +
+ +
+ + + + + + + + + + + + @foreach($results as $result) + + + + + + + + @endforeach + +
NameCategoryAddedSizeAction
+
+
+ +
+
+ + {{ $result->searchname }} + + @if(!empty($result->failed_count) && $result->failed_count > 0) + + Failed ({{ $result->failed_count }}) + + @endif + @if(!empty($result->group_name)) + {{ $result->group_name }} + @endif +
+
+
+ + {{ $result->category_name ?? 'Other' }} + + + {{ userDateDiffForHumans($result->adddate ?? null) }} + + {{ isset($result->size) ? number_format($result->size / 1073741824, 2) . ' GB' : '-' }} + + +
+
-
{{ $results->links() }}
@else - -
- +
+

No content found

-

Try adjusting your search filters or browse all content.

- +

Try a different category or browse all XXX.

+
Browse All XXX
@endif
- -{{-- All modals (NFO, preview, etc.) are included globally via layouts.main --}} @endsection - diff --git a/routes/web.php b/routes/web.php index 71d9189a1..298d31308 100644 --- a/routes/web.php +++ b/routes/web.php @@ -74,7 +74,7 @@ use App\Http\Controllers\TermsController; // Serve cover images from storage - Must be public (no auth required) Route::get('/covers/{type}/{filename}', [App\Http\Controllers\CoverController::class, 'show']) - ->where('type', 'anime|audio|audiosample|book|console|games|movies|music|preview|sample|tvrage|video|xxx') + ->where('type', 'anime|audio|audiosample|book|console|games|movies|music|preview|sample|tvrage|video') ->where('filename', '.*') ->name('covers.show'); diff --git a/storage/covers/xxx/.gitignore b/storage/covers/xxx/.gitignore deleted file mode 100644 index ba2606d59..000000000 --- a/storage/covers/xxx/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -.gitignore -!.gitignore -!no-cover.jpg -!no-backdrop.jpg diff --git a/storage/covers/xxx/no-backdrop.jpg b/storage/covers/xxx/no-backdrop.jpg deleted file mode 100644 index efd5e95f2..000000000 Binary files a/storage/covers/xxx/no-backdrop.jpg and /dev/null differ diff --git a/storage/covers/xxx/no-cover.jpg b/storage/covers/xxx/no-cover.jpg deleted file mode 100644 index 158955870..000000000 Binary files a/storage/covers/xxx/no-cover.jpg and /dev/null differ diff --git a/tests/Feature/AdultProcessingPipelineTest.php b/tests/Feature/AdultProcessingPipelineTest.php deleted file mode 100644 index 208fa3260..000000000 --- a/tests/Feature/AdultProcessingPipelineTest.php +++ /dev/null @@ -1,317 +0,0 @@ -assertInstanceOf(AdultProcessingPipeline::class, $pipeline); - $this->assertCount(7, $pipeline->getPipes()); - } - - /** - * Test that pipes are ordered by priority. - */ - public function test_pipes_are_ordered_by_priority(): void - { - $pipeline = new AdultProcessingPipeline; - $pipes = $pipeline->getPipes()->values(); - - // AEBN should be first (priority 10) - $this->assertInstanceOf(AebnPipe::class, $pipes[0]); - // IAFD should be second (priority 15) - $this->assertInstanceOf(IafdPipe::class, $pipes[1]); - // Data18 should be third (priority 18) - $this->assertInstanceOf(Data18Pipe::class, $pipes[2]); - // PopPorn should be fourth (priority 20) - $this->assertInstanceOf(PoppornPipe::class, $pipes[3]); - // ADM should be fifth (priority 30) - $this->assertInstanceOf(AdmPipe::class, $pipes[4]); - // ADE should be sixth (priority 40) - $this->assertInstanceOf(AdePipe::class, $pipes[5]); - // HotMovies should be last (priority 50) - $this->assertInstanceOf(HotmoviesPipe::class, $pipes[6]); - } - - /** - * Test that a pipe can be added to the pipeline. - */ - public function test_can_add_pipe_to_pipeline(): void - { - $pipeline = new AdultProcessingPipeline([]); - - $this->assertCount(7, $pipeline->getPipes()); // Default pipes - - // Create a custom pipe with high priority - $customPipe = new class extends AbstractAdultProviderPipe - { - protected int $priority = 5; - - public function getName(): string - { - return 'custom'; - } - - public function getDisplayName(): string - { - return 'Custom Provider'; - } - - protected function getBaseUrl(): string - { - return 'https://example.com'; - } - - protected function process(AdultProcessingPassable $passable): AdultProcessingResult - { - return AdultProcessingResult::notFound($this->getName()); - } - - protected function search(string $movie): array|false - { - return false; - } - - protected function getMovieInfo(): array|false - { - return false; - } - }; - - $pipeline->addPipe($customPipe); - - $this->assertCount(8, $pipeline->getPipes()); - } - - /** - * Test AdultReleaseContext creation from title. - */ - public function test_release_context_from_title(): void - { - $context = AdultReleaseContext::fromTitle('Test Movie Title'); - - $this->assertEquals(0, $context->releaseId); - $this->assertEquals('Test Movie Title', $context->searchName); - $this->assertEquals('Test Movie Title', $context->cleanTitle); - $this->assertNull($context->guid); - } - - /** - * Test AdultReleaseContext creation from release array. - */ - public function test_release_context_from_release(): void - { - $release = [ - 'id' => 123, - 'searchname' => 'Test.Movie.Title.XXX.2023', - 'guid' => 'abc123', - ]; - - $context = AdultReleaseContext::fromRelease($release, 'Test Movie Title'); - - $this->assertEquals(123, $context->releaseId); - $this->assertEquals('Test.Movie.Title.XXX.2023', $context->searchName); - $this->assertEquals('Test Movie Title', $context->cleanTitle); - $this->assertEquals('abc123', $context->guid); - } - - /** - * Test AdultProcessingResult creation methods. - */ - public function test_processing_result_creation(): void - { - // Test matched result - $matchedResult = AdultProcessingResult::matched( - 'Test Movie', - 'aebn', - ['boxcover' => 'http://example.com/cover.jpg'] - ); - $this->assertEquals(AdultProcessingResult::STATUS_MATCHED, $matchedResult->status); - $this->assertTrue($matchedResult->isMatched()); - $this->assertFalse($matchedResult->shouldContinueProcessing()); - $this->assertEquals('http://example.com/cover.jpg', $matchedResult->getBoxCover()); - - // Test not found result - $notFoundResult = AdultProcessingResult::notFound('aebn'); - $this->assertEquals(AdultProcessingResult::STATUS_NOT_FOUND, $notFoundResult->status); - $this->assertFalse($notFoundResult->isMatched()); - $this->assertTrue($notFoundResult->shouldContinueProcessing()); - - // Test skipped result - $skippedResult = AdultProcessingResult::skipped('Provider unavailable', 'aebn'); - $this->assertEquals(AdultProcessingResult::STATUS_SKIPPED, $skippedResult->status); - $this->assertFalse($skippedResult->isMatched()); - - // Test pending result - $pendingResult = AdultProcessingResult::pending(); - $this->assertEquals(AdultProcessingResult::STATUS_PENDING, $pendingResult->status); - $this->assertFalse($pendingResult->isMatched()); - } - - /** - * Test AdultProcessingPassable behavior. - */ - public function test_processing_passable_behavior(): void - { - $context = AdultReleaseContext::fromTitle('Test Movie'); - $passable = new AdultProcessingPassable($context, true, 'test_cookie'); - - $this->assertEquals('Test Movie', $passable->getCleanTitle()); - $this->assertEquals('test_cookie', $passable->getCookie()); - $this->assertFalse($passable->shouldStopProcessing()); - - // Update with a matched result - $result = AdultProcessingResult::matched('Test Movie', 'aebn', []); - $passable->updateResult($result, 'aebn'); - - $this->assertTrue($passable->shouldStopProcessing()); - $this->assertEquals('aebn', $passable->result->providerName); - } - - /** - * Test individual pipe name and priority. - */ - public function test_pipe_names_and_priorities(): void - { - $aebn = new AebnPipe; - $this->assertEquals('aebn', $aebn->getName()); - $this->assertEquals('Adult Entertainment Broadcast Network', $aebn->getDisplayName()); - $this->assertEquals(10, $aebn->getPriority()); - - $popporn = new PoppornPipe; - $this->assertEquals('pop', $popporn->getName()); - $this->assertEquals('PopPorn', $popporn->getDisplayName()); - $this->assertEquals(20, $popporn->getPriority()); - - $adm = new AdmPipe; - $this->assertEquals('adm', $adm->getName()); - $this->assertEquals('Adult DVD Marketplace', $adm->getDisplayName()); - $this->assertEquals(30, $adm->getPriority()); - - $ade = new AdePipe; - $this->assertEquals('ade', $ade->getName()); - $this->assertEquals('Adult DVD Empire', $ade->getDisplayName()); - $this->assertEquals(40, $ade->getPriority()); - - $hotmovies = new HotmoviesPipe; - $this->assertEquals('hotm', $hotmovies->getName()); - $this->assertEquals('HotMovies', $hotmovies->getDisplayName()); - $this->assertEquals(50, $hotmovies->getPriority()); - } - - /** - * Test passable to array conversion with debug info. - */ - public function test_passable_to_array_with_debug(): void - { - $context = AdultReleaseContext::fromRelease([ - 'id' => 123, - 'searchname' => 'Test.Movie.XXX', - ], 'Test Movie'); - - $passable = new AdultProcessingPassable($context, true); - - $result = AdultProcessingResult::matched('Test Movie', 'aebn', [ - 'boxcover' => 'http://example.com/cover.jpg', - ]); - $passable->updateResult($result, 'aebn'); - - $array = $passable->toArray(); - - $this->assertEquals('matched', $array['status']); - $this->assertEquals('Test Movie', $array['title']); - $this->assertEquals('aebn', $array['provider']); - $this->assertArrayHasKey('debug', $array); - $this->assertEquals(123, $array['debug']['release_id']); - $this->assertEquals('Test.Movie.XXX', $array['debug']['search_name']); - $this->assertEquals('Test Movie', $array['debug']['clean_title']); - } - - /** - * Test that echo output can be disabled on pipes. - */ - public function test_pipe_echo_output_can_be_disabled(): void - { - $pipe = new AebnPipe; - $pipe->setEchoOutput(false); - - // This should not throw any exceptions - $this->assertInstanceOf(AebnPipe::class, $pipe); - } - - /** - * Test similarity calculation for title matching. - */ - public function test_title_similarity_calculation(): void - { - $pipe = new class extends AbstractAdultProviderPipe - { - public function getName(): string - { - return 'test'; - } - - public function getDisplayName(): string - { - return 'Test'; - } - - protected function getBaseUrl(): string - { - return ''; - } - - protected function process(AdultProcessingPassable $passable): AdultProcessingResult - { - return AdultProcessingResult::pending(); - } - - protected function search(string $movie): array|false - { - return false; - } - - protected function getMovieInfo(): array|false - { - return false; - } - - public function publicCalculateSimilarity(string $search, string $result): float - { - return $this->calculateSimilarity($search, $result); - } - }; - - // Exact match should be 100% - $similarity = $pipe->publicCalculateSimilarity('Test Movie', 'Test Movie'); - $this->assertGreaterThan(99, $similarity); - - // Similar titles should have high similarity - $similarity = $pipe->publicCalculateSimilarity('Test Movie', 'Test Movie 2023'); - $this->assertGreaterThanOrEqual(80, $similarity); - - // Different titles should have lower similarity - $similarity = $pipe->publicCalculateSimilarity('Test Movie', 'Completely Different Title'); - $this->assertLessThan(50, $similarity); - } -}