diff --git a/Blacklight/PopulateAniDB.php b/Blacklight/PopulateAniDB.php deleted file mode 100644 index 702bb4868..000000000 --- a/Blacklight/PopulateAniDB.php +++ /dev/null @@ -1,440 +0,0 @@ -echooutput = config('nntmux.echocli'); - $this->colorCli = new ColorCLI; - - $anidbupdint = Settings::settingValue('max_update_frequency'); - $lastupdated = Settings::settingValue('last_full_update'); - - $this->imgSavePath = config('nntmux_settings.covers_path').'/anime/'; - $this->apiKey = config('nntmux_api.anidb_api_key'); - - $this->updateInterval = $anidbupdint ?? '7'; - $this->lastUpdate = $lastupdated ?? '0'; - $this->banned = false; - } - - /** - * Main switch that initiates AniDB table population. - * - * - * @throws \Exception - */ - public function populateTable(string $type = '', int|string $aniDbId = ''): void - { - switch ($type) { - case 'full': - $this->populateMainTable(); - break; - case 'info': - $this->populateInfoTable($aniDbId); - break; - } - } - - /** - * @return array|false - * - * @throws \Exception - */ - private function getAniDbAPI($aniDbId) - { - $timestamp = Settings::settingValue('banned') + 90000; - if ($timestamp > time()) { - echo 'Banned from AniDB lookups until '.date('Y-m-d H:i:s', $timestamp).PHP_EOL; - - return false; - } - $apiResponse = $this->getAniDbResponse($aniDbId); - - $AniDBAPIArray = []; - - if ($apiResponse === false) { - echo 'AniDB: Error getting response.'.PHP_EOL; - } elseif (preg_match('/\Banned\<\/error\>/', $apiResponse)) { - $this->banned = true; - Settings::query()->where(['name' => 'banned'])->update(['value' => time()]); - } elseif (preg_match('/\Anime not found\<\/error\>/', $apiResponse)) { - echo "AniDB : Anime not yet on site. Remove until next update.\n"; - } elseif ($AniDBAPIXML = new \SimpleXMLElement($apiResponse)) { - $AniDBAPIArray['similar'] = $this->processAPIResponseElement($AniDBAPIXML->similaranime, 'anime', false); - $AniDBAPIArray['related'] = $this->processAPIResponseElement($AniDBAPIXML->relatedanime, 'anime', false); - $AniDBAPIArray['creators'] = $this->processAPIResponseElement($AniDBAPIXML->creators, null, false); - $AniDBAPIArray['characters'] = $this->processAPIResponseElement($AniDBAPIXML->characters, null, true); - $AniDBAPIArray['categories'] = $this->processAPIResponseElement($AniDBAPIXML->categories, null, true); - - $episodeArray = []; - if ($AniDBAPIXML->episodes && $AniDBAPIXML->episodes->episode[0]->attributes()) { - $i = 1; - foreach ($AniDBAPIXML->episodes->episode as $episode) { - $titleArray = []; - - $episodeArray[$i]['episode_id'] = (int) $episode->attributes()->id; - $episodeArray[$i]['episode_no'] = (int) $episode->epno; - $episodeArray[$i]['airdate'] = (string) $episode->airdate; - - if (! empty($episode->title)) { - foreach ($episode->title as $title) { - $xmlAttribs = $title->attributes('xml', true); - // only english, x-jat imploded episode titles for now - if (\in_array($xmlAttribs->lang, ['en', 'x-jat'], false)) { - $titleArray[] = $title[0]; - } - } - } - - $episodeArray[$i]['episode_title'] = empty($titleArray) ? '' : implode(', ', $titleArray); - $i++; - } - } - - // start and end date come from AniDB API as date strings -- no manipulation needed - $AniDBAPIArray['startdate'] = $AniDBAPIXML->startdate ?? '0000-00-00'; - $AniDBAPIArray['enddate'] = $AniDBAPIXML->enddate ?? '0000-00-00'; - - if (isset($AniDBAPIXML->ratings->permanent)) { - $AniDBAPIArray['rating'] = $AniDBAPIXML->ratings->permanent; - } else { - $AniDBAPIArray['rating'] = $AniDBAPIXML->ratings->temporary ?? ''; - } - - $AniDBAPIArray += [ - 'type' => isset($AniDBAPIXML->type[0]) ? (string) $AniDBAPIXML->type : '', - 'description' => isset($AniDBAPIXML->description) ? (string) $AniDBAPIXML->description : '', - 'picture' => isset($AniDBAPIXML->picture[0]) ? (string) $AniDBAPIXML->picture : '', - 'epsarr' => $episodeArray, - ]; - - return $AniDBAPIArray; - } - - return false; - } - - private function processAPIResponseElement(\SimpleXMLElement $element, ?string $property = null, bool $children = false): string - { - $property = $property ?? 'name'; - $temp = ''; - - if (count($element) !== 0) { - $result = $children === true ? $element->children() : $element; - foreach ($result as $entry) { - $temp .= $entry->$property.', '; - } - } - - return empty($temp) ? '' : substr($temp, 0, -2); - } - - /** - * Requests and returns the API data from AniDB. - */ - private function getAniDbResponse($aniDbId): string - { - $curlString = sprintf( - 'http://api.anidb.net:9001/httpapi?request=anime&client=%s&clientver=%d&protover=1&aid=%d', - $this->apiKey, - self::CLIENT_VERSION, - $aniDbId - ); - - $ch = curl_init($curlString); - - $curlOpts = [ - CURLOPT_RETURNTRANSFER => 1, - CURLOPT_HEADER => 0, - CURLOPT_FAILONERROR => 1, - CURLOPT_ENCODING => 'gzip', - ]; - - curl_setopt_array($ch, $curlOpts); - $apiResponse = curl_exec($ch); - curl_close($ch); - - return $apiResponse; - } - - /** - * Inserts new anime info from AniDB to anidb table. - * - * @param int $id The AniDB ID to be inserted - * @param string $type The title type - * @param string $lang The title language - * @param string $title The title of the Anime - */ - private function insertAniDb(int $id, string $type, string $lang, string $title): void - { - $check = AnidbTitle::query()->where(['anidbid' => $id, 'type' => $type, 'lang' => $lang, 'title' => $title])->first(); - - if ($check === null) { - AnidbTitle::insertOrIgnore(['anidbid' => $id, 'type' => $type, 'lang' => $lang, 'title' => $title]); - } else { - $this->colorCli->warning("Duplicate: $id"); - } - } - - private function insertAniDBInfoEps($aniDbId, array $AniDBInfoArray = []): string - { - AnidbInfo::query() - ->insert( - [ - 'anidbid' => $aniDbId, - 'type' => $AniDBInfoArray['type'], - 'startdate' => $AniDBInfoArray['startdate'], - 'enddate' => $AniDBInfoArray['enddate'], - 'related' => $AniDBInfoArray['enddate'], - 'similar' => $AniDBInfoArray['similar'], - 'creators' => $AniDBInfoArray['creators'], - 'description' => $AniDBInfoArray['description'], - 'rating' => $AniDBInfoArray['rating'], - 'picture' => $AniDBInfoArray['picture'], - 'categories' => $AniDBInfoArray['categories'], - 'characters' => $AniDBInfoArray['characters'], - 'updated' => now(), - ] - ); - if (! empty($AniDBInfoArray['epsarr'])) { - $this->insertAniDBEpisodes($aniDbId, $AniDBInfoArray['epsarr']); - } - - return $AniDBInfoArray['picture']; - } - - private function insertAniDBEpisodes($aniDbId, array $episodeArr = []): void - { - if (! empty($episodeArr)) { - foreach ($episodeArr as $episode) { - AnidbEpisode::insertOrIgnore( - [ - 'anidbid' => $aniDbId, - 'episodeid' => $episode['episode_id'], - 'episode_no' => $episode['episode_no'], - 'episode_title' => $episode['episode_title'], - 'airdate' => $episode['airdate'], - ] - ); - } - } - } - - /** - * Grabs AniDB Full Dump XML and inserts it into anidb table. - */ - private function populateMainTable(): void - { - $lastUpdate = Carbon::createFromTimestamp($this->lastUpdate, date_default_timezone_get()); - $current = now(); - - if ($current->diff($lastUpdate)->format('%d') > $this->updateInterval) { - if ($this->echooutput) { - $this->colorCli->header('Updating anime titles by grabbing full data AniDB dump.'); - } - - $animeTitles = new \SimpleXMLElement('compress.zlib://http://anidb.net/api/anime-titles.xml.gz', null, true); - - // Even if the update process fails, - // we must mark the last update time or risk ban - $this->setLastUpdated(); - - if ($animeTitles) { - $count = $animeTitles->count(); - if ($this->echooutput) { - $this->colorCli->header( - 'Total of '.number_format($count).' titles to add.'.PHP_EOL - ); - } - - foreach ($animeTitles as $anime) { - echo "Remaining: $count \r"; - foreach ($anime->title as $title) { - $xmlAttribs = $title->attributes('xml', true); - $this->insertAniDb( - (string) $anime['aid'], - (string) $title['type'], - (string) $xmlAttribs->lang, - (string) $title[0] - ); - $this->colorCli->primary( - sprintf( - 'Inserting: %d, %s, %s, %s', - $anime['aid'], - $title['type'], - $xmlAttribs->lang, - $title[0] - ) - ); - } - $count--; - } - } else { - echo PHP_EOL. - $this->colorCli->error('Error retrieving XML data from AniDB. Please try again later.'). - PHP_EOL; - } - } else { - $this->colorCli->info( - 'AniDB has been updated within the past '.$this->updateInterval.' days. '. - 'Either set this value lower in Site Edit (at your own risk of being banned) or try again later.' - ); - } - } - - /** - * Directs flow for populating the AniDB Info/Episodes table. - * - * - * @throws \Exception - */ - private function populateInfoTable(string $aniDbId = ''): void - { - if (empty($aniDbId)) { - $anidbIds = AnidbTitle::query() - ->selectRaw('DISTINCT anidb_titles.anidbid') - ->leftJoin('anidb_info as ai', 'ai.anidbid', '=', 'anidb_titles.anidbid') - ->whereNull('ai.updated') - ->get(); - - foreach ($anidbIds as $anidb) { - $AniDBAPIArray = $this->getAniDbAPI($anidb['anidbid']); - - if ($this->banned) { - $this->colorCli->error( - 'AniDB Banned, import will fail, please wait 24 hours before retrying.' - ); - exit; - } - - if ($AniDBAPIArray === false && $this->echooutput) { - $this->colorCli->info( - 'Anime ID: '.$anidb['anidbid'].' not available for update yet.' - ); - } else { - $this->updateAniChildTables($anidb['anidbid'], $AniDBAPIArray); - } - sleep(random_int(120, 240)); - } - } else { - $AniDBAPIArray = $this->getAniDbAPI($aniDbId); - - if ($this->banned) { - $this->colorCli->error( - 'AniDB Banned, import will fail, please wait 24 hours before retrying.' - ); - exit; - } - - if ($AniDBAPIArray === false && $this->echooutput) { - $this->colorCli->info( - 'Anime ID: '.$aniDbId.' not available for update yet.' - ); - } else { - $this->updateAniChildTables($aniDbId, $AniDBAPIArray); - } - } - } - - /** - * Sets the database time for last full AniDB update. - */ - private function setLastUpdated(): void - { - Settings::query()->where(['name' => 'last_full_update'])->update(['value' => time()]); - } - - private function updateAniDBInfoEps($aniDbId, array $AniDBInfoArray = []): string - { - AnidbInfo::query() - ->where('anidbid', $aniDbId) - ->update( - [ - 'type' => $AniDBInfoArray['type'], - 'startdate' => $AniDBInfoArray['startdate'], - 'enddate' => $AniDBInfoArray['enddate'], - 'related' => $AniDBInfoArray['enddate'], - 'similar' => $AniDBInfoArray['similar'], - 'creators' => $AniDBInfoArray['creators'], - 'description' => $AniDBInfoArray['description'], - 'rating' => $AniDBInfoArray['rating'], - 'picture' => $AniDBInfoArray['picture'], - 'categories' => $AniDBInfoArray['categories'], - 'characters' => $AniDBInfoArray['characters'], - 'updated' => now(), - ] - ); - if (! empty($AniDBInfoArray['epsarr'])) { - $this->insertAniDBEpisodes($aniDbId, $AniDBInfoArray['epsarr']); - } - - return $AniDBInfoArray['picture']; - } - - private function updateAniChildTables($aniDbId, array $AniDBInfoArray = []): void - { - $check = AnidbInfo::query()->where('anidbid', $aniDbId)->first(['anidbid']); - - if ($check === null) { - $picture = $this->insertAniDBInfoEps($aniDbId, $AniDBInfoArray); - } else { - $picture = $this->updateAniDBInfoEps($aniDbId, $AniDBInfoArray); - } - - if (! empty($picture) && ! file_exists($this->imgSavePath.$aniDbId.'.jpg')) { - (new ReleaseImage)->saveImage( - $aniDbId, - 'http://img7.anidb.net/pics/anime/'.$picture, - $this->imgSavePath - ); - } - } -} diff --git a/app/Http/Controllers/Admin/AdminAnidbController.php b/app/Http/Controllers/Admin/AdminAnidbController.php index 6569322f0..39122e496 100644 --- a/app/Http/Controllers/Admin/AdminAnidbController.php +++ b/app/Http/Controllers/Admin/AdminAnidbController.php @@ -4,7 +4,7 @@ namespace App\Http\Controllers\Admin; use App\Http\Controllers\BasePageController; use App\Models\Release; -use Blacklight\AniDB; +use App\Services\AnidbService; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\View\View; @@ -18,11 +18,11 @@ class AdminAnidbController extends BasePageController { $this->setAdminPrefs(); - $AniDB = new AniDB; + $anidbService = new AnidbService; $title = $meta_title = 'AniDB List'; $animetitle = $request->input('animetitle', ''); - $anidblist = $AniDB->getAnimeRange($animetitle); + $anidblist = $anidbService->getAnimeRange($animetitle); return view('admin.anidb.index', compact('anidblist', 'animetitle', 'title', 'meta_title')); } @@ -34,14 +34,14 @@ class AdminAnidbController extends BasePageController { $this->setAdminPrefs(); - $AniDB = new AniDB; + $anidbService = new AnidbService; // Set the current action. $action = $request->input('action', 'view'); switch ($action) { case 'submit': - $AniDB->updateTitle( + $anidbService->updateTitle( $request->input('anidbid'), $request->input('title'), $request->input('type'), @@ -65,7 +65,7 @@ class AdminAnidbController extends BasePageController default: if (! empty($id)) { $title = $meta_title = 'AniDB Edit'; - $anime = $AniDB->getAnimeInfo($id); + $anime = $anidbService->getAnimeInfo($id); return view('admin.anidb.edit', compact('anime', 'title', 'meta_title')); } diff --git a/app/Http/Controllers/DetailsController.php b/app/Http/Controllers/DetailsController.php index e47cc2d7c..50fe79873 100644 --- a/app/Http/Controllers/DetailsController.php +++ b/app/Http/Controllers/DetailsController.php @@ -15,7 +15,7 @@ use App\Models\Video; use App\Services\MovieService; use App\Services\Releases\ReleaseSearchService; use App\Services\BookService; -use Blacklight\AniDB; +use App\Services\AnidbService; use Blacklight\Console; use Blacklight\Games; use Blacklight\Music; @@ -129,7 +129,7 @@ class DetailsController extends BasePageController $AniDBAPIArray = ''; if ($data['anidbid'] > 0) { - $AniDBAPIArray = (new AniDB)->getAnimeInfo($data['anidbid']); + $AniDBAPIArray = (new AnidbService)->getAnimeInfo($data['anidbid']); // If we have anilist_id but missing details, fetch from AniList if ($AniDBAPIArray && !empty($AniDBAPIArray->anilist_id)) { @@ -140,7 +140,7 @@ class DetailsController extends BasePageController $palist = new \Blacklight\PopulateAniList; $palist->populateTable('info', $anilistId); // Refresh the data - $AniDBAPIArray = (new AniDB)->getAnimeInfo($data['anidbid']); + $AniDBAPIArray = (new AnidbService)->getAnimeInfo($data['anidbid']); } catch (\Exception $e) { // Silently fail, use existing data } diff --git a/app/Http/Resources/AnidbCollection.php b/app/Http/Resources/AnidbCollection.php new file mode 100644 index 000000000..cbcbfd3d7 --- /dev/null +++ b/app/Http/Resources/AnidbCollection.php @@ -0,0 +1,32 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'data' => $this->collection, + ]; + } +} + diff --git a/app/Http/Resources/AnidbResource.php b/app/Http/Resources/AnidbResource.php new file mode 100644 index 000000000..426038bd3 --- /dev/null +++ b/app/Http/Resources/AnidbResource.php @@ -0,0 +1,74 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'anidbid' => $this->anidbid, + 'anilist_id' => $this->anilist_id, + 'mal_id' => $this->mal_id, + 'title' => $this->title, + 'english_title' => $this->english_title ?? null, + 'original_title' => $this->original_title ?? null, + 'romaji_title' => $this->romaji_title ?? null, + 'type' => $this->type, + 'media_type' => $this->media_type, + 'country' => $this->country, + 'startdate' => $this->startdate, + 'enddate' => $this->enddate, + 'episodes' => $this->episodes, + 'duration' => $this->duration, + 'status' => $this->status, + 'source' => $this->source, + 'rating' => $this->rating, + 'description' => $this->description, + 'picture' => $this->picture, + 'categories' => $this->categories, + 'characters' => $this->characters, + 'creators' => $this->creators, + 'related' => $this->related, + 'similar' => $this->similar, + 'hashtag' => $this->hashtag, + 'updated' => $this->updated, + 'picture_url' => $this->getPictureUrl(), + ]; + } + + /** + * Get the picture URL. + */ + protected function getPictureUrl(): ?string + { + if (empty($this->picture)) { + return null; + } + + // If picture is already a URL, return it + if (filter_var($this->picture, FILTER_VALIDATE_URL)) { + return $this->picture; + } + + // Otherwise construct the local path + $picturePath = storage_path('covers/anime/'.$this->anidbid.'.jpg'); + if (file_exists($picturePath)) { + return url('/covers/anime/'.$this->anidbid.'.jpg'); + } + + return null; + } +} + diff --git a/app/Models/AnidbInfo.php b/app/Models/AnidbInfo.php index ba832d1fe..d579f4e1a 100644 --- a/app/Models/AnidbInfo.php +++ b/app/Models/AnidbInfo.php @@ -4,6 +4,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\HasMany; /** * App\Models\AnidbInfo. @@ -84,9 +85,81 @@ class AnidbInfo extends Model */ protected $table = 'anidb_info'; + /** + * Get the title associated with this info. + */ public function title(): BelongsTo { return $this->belongsTo(AnidbTitle::class, 'anidbid'); } + /** + * Get the releases associated with this anime. + */ + public function releases(): HasMany + { + return $this->hasMany(Release::class, 'anidbid', 'anidbid'); + } + + /** + * Get the picture path. + */ + public function getPicturePath(): string + { + return storage_path('covers/anime/'.$this->anidbid.'.jpg'); + } + + /** + * Check if picture image exists locally. + */ + public function hasPictureImage(): bool + { + return file_exists($this->getPicturePath()); + } + + /** + * Get the picture URL. + */ + public function getPictureUrl(): ?string + { + if (empty($this->picture)) { + return null; + } + + // If picture is already a URL, return it + if (filter_var($this->picture, FILTER_VALIDATE_URL)) { + return $this->picture; + } + + // Otherwise construct the local path + if ($this->hasPictureImage()) { + return url('/covers/anime/'.$this->anidbid.'.jpg'); + } + + return null; + } + + /** + * Get AniList URL if anilist_id exists. + */ + public function getAnilistUrl(): ?string + { + if (empty($this->anilist_id)) { + return null; + } + + return 'https://anilist.co/anime/'.$this->anilist_id; + } + + /** + * Get MyAnimeList URL if mal_id exists. + */ + public function getMalUrl(): ?string + { + if (empty($this->mal_id)) { + return null; + } + + return 'https://myanimelist.net/anime/'.$this->mal_id; + } } diff --git a/Blacklight/AniDB.php b/app/Services/AnidbService.php old mode 100755 new mode 100644 similarity index 87% rename from Blacklight/AniDB.php rename to app/Services/AnidbService.php index 54e9e4982..a3ec9c5f0 --- a/Blacklight/AniDB.php +++ b/app/Services/AnidbService.php @@ -1,25 +1,39 @@ title)) { $result->english_title = $englishTitle->title; } else { $result->english_title = null; } - + if ($originalTitle && isset($originalTitle->title)) { $result->original_title = $originalTitle->title; $result->original_lang = $originalTitle->lang; @@ -210,7 +230,7 @@ class AniDB $result->original_title = null; $result->original_lang = null; } - + // Add romaji title separately (even if it's also the original) if ($romajiTitle && isset($romajiTitle->title)) { $result->romaji_title = $romajiTitle->title; @@ -222,3 +242,4 @@ class AniDB return $result; } } +