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\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[\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[\w -]+)?\s+-\s+(?P\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
)
);
}
}