false, 'Settings' => null, ]; $options += $defaults; $this->echooutput = ($options['Echo'] && config('nntmux.echocli')); $this->pdo = DB::connection()->getPdo(); $this->padb = new PaDb(['Echo' => $options['Echo']]); $this->colorCli = new ColorCLI(); $qty = (int) Settings::settingValue('..maxanidbprocessed'); $this->aniqty = $qty ?? 100; $this->status = 'NULL'; } /** * Queues anime releases for processing. * * @throws \Exception */ public function processAnimeReleases(): void { $results = DB::select( sprintf( ' SELECT searchname, id FROM releases WHERE nzbstatus = %d AND anidbid IS NULL AND categories_id = %d ORDER BY postdate DESC LIMIT %d', NZB::NZB_ADDED, Category::TV_ANIME, $this->aniqty ) ); if (\count($results) > 0) { $this->doRandomSleep(); foreach ($results as $release) { $matched = $this->matchAnimeRelease($release); if ($matched === false) { DB::update( sprintf( ' UPDATE releases SET anidbid = %d WHERE id = %d', $this->status, $release->id ) ); } } } else { $this->colorCli->info('No anidb releases to process.'); } } /** * Selects episode info for a local match. * * @param $anidbId * @param int $episode * @return \Illuminate\Database\Eloquent\Model|null|static */ private function checkAniDBInfo($anidbId, $episode = -1) { return AnidbEpisode::query()->where( [ 'anidbid' => $anidbId, 'episode_no' => $episode, ] )->first( [ 'anidbid', 'episode_no', 'airdate', 'episode_title', ] ); } /** * Sleeps between 10 and 15 seconds for AniDB API cooldown. * * * @throws \Exception */ private function doRandomSleep(): void { sleep(random_int(10, 15)); } /** * Extracts anime title and episode info from release searchname. * * @param string $cleanName * * @return array $matches */ private function extractTitleEpisode($cleanName = ''): array { $cleanName = str_replace('_', ' ', $cleanName); if (preg_match( '/(^|.*\")(\[[a-zA-Z\.\!?-]+\][\s_]*)?(\[BD\][\s_]*)?(\[\d{3,4}[ip]\][\s_]*)?(?P[\w\s_.+!?\'\-\(\)]+)(New Edit|(Blu-?ray)?( ?Box)?( ?Set)?)?([ _]\-[ _]|([ ._-]Epi?(sode)?[ ._-]?0?)?[ ._-]?|[ ._-]Vol\.|[ ._-]E)(?P<epno>\d{1,3}|Movie|OVA|Complete Series)(v\d|-\d+)?[\-_. ].*[\[\(\"]/i', $cleanName, $matches ) ) { $matches['epno'] = (int) $matches['epno']; if (\in_array($matches['epno'], ['Movie', 'OVA'], false)) { $matches['epno'] = 1; } } elseif (preg_match( '/^(\[[a-zA-Z\.\-!?]+\][\s_]*)?(\[BD\])?(\[\d{3,4}[ip]\])?(?P<title>[\w\s_.+!?\'\-\(\)]+)(New Edit|(Blu-?ray)?( ?Box)?( ?Set)?)?\s*[\(\[](BD|\d{3,4}[ipx])/i', $cleanName, $matches ) ) { $matches['epno'] = 1; } elseif (preg_match('#^(\[[a-zA-Z\.\-!?]+\][\s_]*)?(?P<title>[\w -]+)?\s+-\s+(?P<epno>\d+)\s*(\[\d+p\])?$#', $cleanName, $matches)) { $matches['epno'] = (int) $matches['epno']; } else { $this->status = self::PROC_EXTFAIL; } if (! empty($matches['title'])) { $matches['title'] = trim(str_replace(['_', '.'], ' ', $matches['title'])); } return $matches; } /** * Retrieves AniDB Info using a cleaned name. * * * @param string $searchName * * @return mixed */ private function getAnidbByName($searchName = '') { return DB::selectOne( sprintf( ' SELECT at.anidbid, at.title FROM anidb_titles AS at WHERE at.title LIKE %s', escapeString('%'.$searchName.'%') ) ); } /** * Matches the anime release to AniDB Info * If no info is available locally the AniDB API is invoked. * * * @param $release * * @return bool * @throws \Exception */ private function matchAnimeRelease($release): bool { $matched = false; $type = 'Local'; // clean up the release name to ensure we get a good chance at getting a valid title $cleanArr = $this->extractTitleEpisode($release->searchname); if (\is_array($cleanArr) && isset($cleanArr['title']) && is_numeric($cleanArr['epno'])) { $this->colorCli->header(PHP_EOL.'Looking Up: '). $this->colorCli->primary(' Title: '.$cleanArr['title'].PHP_EOL. ' Episode: '.$cleanArr['epno']); // get anidb number for the title of the name $anidbId = $this->getAnidbByName($cleanArr['title']); if ($anidbId === false) { $tmpName = preg_replace('/\s/', '%', $cleanArr['title']); $anidbId = $this->getAnidbByName($tmpName); } if (! empty($anidbId) && is_numeric($anidbId->anidbid) && $anidbId->anidbid > 0) { $updatedAni = $this->checkAniDBInfo($anidbId->anidbid, $cleanArr['epno']); if ($updatedAni === null) { if ($this->updateTimeCheck($anidbId->anidbid) !== null) { $this->padb->populateTable('info', $anidbId->anidbid); $this->doRandomSleep(); $updatedAni = $this->checkAniDBInfo($anidbId->anidbid); $type = 'Remote'; } else { echo PHP_EOL. $this->colorCli->info('This AniDB ID was not found to be accurate locally, but has been updated too recently to check AniDB.'). PHP_EOL; } } $episodeTitle = $updatedAni['episode_title'] ?? 'Unknown'; $this->updateRelease($anidbId->anidbid, $release->id); $this->colorCli->headerOver('Matched '.$type.' AniDB ID: '). $this->colorCli->primary($anidbId->anidbid). $this->colorCli->alternateOver(' Title: '). $this->colorCli->primary($anidbId->title). $this->colorCli->alternateOver(' Episode #: '). $this->colorCli->primary($cleanArr['epno']). $this->colorCli->alternateOver(' Episode Title: '). $this->colorCli->primary($episodeTitle); $matched = true; } else { $this->status = self::PROC_NOMATCH; } } return $matched; } /** * @param $anidbId * @param $relId */ private function updateRelease($anidbId, $relId): void { DB::update( sprintf( ' UPDATE releases SET anidbid = %d WHERE id = %d', $anidbId, $relId ) ); } /** * Checks a specific Anime title's last update time. * * * @param $anidbId * * @return mixed */ private function updateTimeCheck($anidbId) { return DB::selectOne( sprintf( ' SELECT anidbid FROM anidb_info ai WHERE ai.updated < (NOW() - INTERVAL 7 DAY) AND ai.anidbid = %d', $anidbId ) ); } }