mirror of
https://github.com/NNTmux/newznab-tmux.git
synced 2026-08-29 01:08:56 +00:00
Replace anidb with anilist
This commit is contained in:
+81
-16
@@ -15,20 +15,19 @@ class AniDB
|
||||
public function __construct() {}
|
||||
|
||||
/**
|
||||
* Updates stored AniDB entries in the database.
|
||||
* Updates stored AniList entries in the database.
|
||||
* Note: AniList doesn't support episodes, so episode-related parameters are ignored.
|
||||
*/
|
||||
public function updateTitle(int $anidbID, string $title, string $type, string $startdate, string $enddate, string $related, string $similar, string $creators, string $description, string $rating, string $categories, string $characters, string $epnos, string $airdates, string $episodetitles): void
|
||||
public function updateTitle(int $anidbID, string $title, string $type, string $startdate, string $enddate, string $related, string $similar, string $creators, string $description, string $rating, string $categories, string $characters, string $epnos = '', string $airdates = '', string $episodetitles = ''): void
|
||||
{
|
||||
DB::update(
|
||||
sprintf(
|
||||
'
|
||||
UPDATE anidb_titles at
|
||||
INNER JOIN anidb_info ai ON ai.anidbid = at.anidbid
|
||||
INNER JOIN anidb_episodes ae ON ae.anidbid = at.anidbid
|
||||
SET title = %s, type = %s, startdate = %s, enddate = %s,
|
||||
related = %s, similar = %s, creators = %s, description = %s, rating = %s,
|
||||
categories = %s, characters = %s, epnos = %s, airdates = %s,
|
||||
episodetitles = %s WHERE anidbid = %d',
|
||||
categories = %s, characters = %s WHERE anidbid = %d',
|
||||
escapeString($title),
|
||||
escapeString($type),
|
||||
escapeString($startdate),
|
||||
@@ -40,9 +39,6 @@ class AniDB
|
||||
escapeString($rating),
|
||||
escapeString($categories),
|
||||
escapeString($characters),
|
||||
escapeString($epnos),
|
||||
escapeString($airdates),
|
||||
escapeString($episodetitles),
|
||||
$anidbID
|
||||
)
|
||||
);
|
||||
@@ -57,10 +53,9 @@ class AniDB
|
||||
DB::delete(
|
||||
sprintf(
|
||||
'
|
||||
DELETE at, ai, ae
|
||||
DELETE at, ai
|
||||
FROM anidb_titles AS at
|
||||
LEFT OUTER JOIN anidb_info ai USING (anidbid)
|
||||
LEFT OUTER JOIN anidb_episodes ae USING (anidbid)
|
||||
WHERE anidbid = %d',
|
||||
$anidbID
|
||||
)
|
||||
@@ -125,25 +120,95 @@ class AniDB
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all info for a specific AniDB ID.
|
||||
* Retrieves all info for a specific AniList ID.
|
||||
* Note: AniList doesn't support episodes, so episode data is not included.
|
||||
*/
|
||||
public function getAnimeInfo(int $anidbID): mixed
|
||||
{
|
||||
// Get main info with primary title (prefer English, fallback to any)
|
||||
$animeInfo = DB::select(
|
||||
sprintf(
|
||||
'
|
||||
SELECT at.anidbid, at.lang, at.title,
|
||||
ai.anilist_id, ai.mal_id, ai.country, ai.media_type, ai.episodes, ai.duration, ai.status, ai.source, ai.hashtag,
|
||||
ai.startdate, ai.enddate, ai.updated, ai.related, ai.creators, ai.description,
|
||||
ai.rating, ai.picture, ai.categories, ai.characters, ai.type, ai.similar, ae.episodeid, ae
|
||||
.episode_title, ae.episode_no, ae.airdate
|
||||
ai.rating, ai.picture, ai.categories, ai.characters, ai.type, ai.similar
|
||||
FROM anidb_titles AS at
|
||||
LEFT JOIN anidb_info AS ai USING (anidbid)
|
||||
LEFT JOIN anidb_episodes ae USING (anidbid)
|
||||
WHERE at.anidbid = %d',
|
||||
WHERE at.anidbid = %d
|
||||
ORDER BY CASE
|
||||
WHEN at.lang = "en" THEN 1
|
||||
WHEN at.lang = "x-jat" THEN 2
|
||||
ELSE 3
|
||||
END
|
||||
LIMIT 1',
|
||||
$anidbID
|
||||
)
|
||||
);
|
||||
|
||||
return $animeInfo[0] ?? false;
|
||||
$result = $animeInfo[0] ?? false;
|
||||
|
||||
if ($result) {
|
||||
// Get English title separately
|
||||
$englishTitle = DB::selectOne(
|
||||
sprintf(
|
||||
'
|
||||
SELECT title
|
||||
FROM anidb_titles
|
||||
WHERE anidbid = %d AND lang = "en"
|
||||
LIMIT 1',
|
||||
$anidbID
|
||||
)
|
||||
);
|
||||
|
||||
// Get native title (prefer ja, then x-jat, then any non-English title)
|
||||
$nativeTitle = DB::selectOne(
|
||||
sprintf(
|
||||
'
|
||||
SELECT title, lang
|
||||
FROM anidb_titles
|
||||
WHERE anidbid = %d AND lang NOT IN ("en")
|
||||
ORDER BY CASE
|
||||
WHEN lang = "ja" THEN 1
|
||||
WHEN lang = "x-jat" THEN 2
|
||||
ELSE 3
|
||||
END
|
||||
LIMIT 1',
|
||||
$anidbID
|
||||
)
|
||||
);
|
||||
|
||||
// If we found a native title, use it; otherwise try to get romaji
|
||||
$originalTitle = $nativeTitle;
|
||||
if (!$originalTitle) {
|
||||
$originalTitle = DB::selectOne(
|
||||
sprintf(
|
||||
'
|
||||
SELECT title, lang
|
||||
FROM anidb_titles
|
||||
WHERE anidbid = %d AND lang = "x-jat"
|
||||
LIMIT 1',
|
||||
$anidbID
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Add titles to result
|
||||
if ($englishTitle && isset($englishTitle->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;
|
||||
} else {
|
||||
$result->original_title = null;
|
||||
$result->original_lang = null;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,6 +316,7 @@ class Categorize
|
||||
|
||||
public function isAnimeTV(): bool
|
||||
{
|
||||
|
||||
if ($this->hasAdultMarkers()) {
|
||||
return false;
|
||||
}
|
||||
@@ -325,7 +326,12 @@ class Categorize
|
||||
|
||||
return true;
|
||||
}
|
||||
if (preg_match('/(ANiHLS|HaiKU|ANiURL)/i', $this->releaseName)) {
|
||||
if (preg_match('/((\[[a-fA-F0-9]{8}\])|(\[[a-z0-9]{8}\])|ANiHLS|HaiKU|ANiURL|SkyAnime|2jzgte|^shiteater|(Erai|New)\-raws)|(LostYears|Vodes)$/i', $this->releaseName)) {
|
||||
$this->tmpCat = Category::TV_ANIME;
|
||||
|
||||
return true;
|
||||
}
|
||||
if (preg_match('/Sokudo|Ninja Kamui|Synduality Noir|Komyo|Yoohoo/i', $this->releaseName)) {
|
||||
$this->tmpCat = Category::TV_ANIME;
|
||||
|
||||
return true;
|
||||
|
||||
@@ -0,0 +1,640 @@
|
||||
<?php
|
||||
|
||||
namespace Blacklight;
|
||||
|
||||
use App\Models\AnidbInfo;
|
||||
use App\Models\AnidbTitle;
|
||||
use App\Models\Settings;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
class PopulateAniList
|
||||
{
|
||||
/**
|
||||
* AniList GraphQL API endpoint
|
||||
*/
|
||||
private const API_URL = 'https://graphql.anilist.co';
|
||||
|
||||
/**
|
||||
* Rate limit: 90 requests per minute
|
||||
*/
|
||||
private const RATE_LIMIT_PER_MINUTE = 90;
|
||||
|
||||
/**
|
||||
* Whether to echo message output.
|
||||
*/
|
||||
public bool $echooutput;
|
||||
|
||||
/**
|
||||
* The directory to store anime covers.
|
||||
*/
|
||||
public string $imgSavePath;
|
||||
|
||||
/**
|
||||
* HTTP client for API requests
|
||||
*/
|
||||
protected Client $client;
|
||||
|
||||
/**
|
||||
* Rate limiting: track requests and timestamps
|
||||
*/
|
||||
private array $rateLimitQueue = [];
|
||||
|
||||
protected ColorCLI $colorCli;
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->echooutput = config('nntmux.echocli');
|
||||
$this->colorCli = new ColorCLI;
|
||||
|
||||
// Use storage_path directly to match CoverController expectations
|
||||
$this->imgSavePath = storage_path('covers/anime/');
|
||||
$this->client = new Client([
|
||||
'base_uri' => self::API_URL,
|
||||
'timeout' => 30,
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/json',
|
||||
'Accept' => 'application/json',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main switch that initiates AniList table population.
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function populateTable(string $type = '', int|string $anilistId = ''): void
|
||||
{
|
||||
switch ($type) {
|
||||
case 'info':
|
||||
$this->populateInfoTable($anilistId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for anime by title using AniList GraphQL API.
|
||||
*
|
||||
* @return array|false
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function searchAnime(string $title, int $limit = 10)
|
||||
{
|
||||
$query = '
|
||||
query ($search: String, $perPage: Int) {
|
||||
Page (perPage: $perPage) {
|
||||
media (search: $search, type: ANIME) {
|
||||
id
|
||||
idMal
|
||||
title {
|
||||
romaji
|
||||
english
|
||||
native
|
||||
}
|
||||
type
|
||||
format
|
||||
status
|
||||
countryOfOrigin
|
||||
episodes
|
||||
duration
|
||||
source
|
||||
popularity
|
||||
favourites
|
||||
startDate {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
endDate {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
description
|
||||
averageScore
|
||||
hashtag
|
||||
coverImage {
|
||||
large
|
||||
}
|
||||
genres
|
||||
studios {
|
||||
nodes {
|
||||
name
|
||||
}
|
||||
}
|
||||
characters {
|
||||
nodes {
|
||||
name {
|
||||
full
|
||||
}
|
||||
}
|
||||
}
|
||||
relations {
|
||||
edges {
|
||||
relationType
|
||||
node {
|
||||
id
|
||||
title {
|
||||
romaji
|
||||
english
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
';
|
||||
|
||||
$variables = [
|
||||
'search' => $title,
|
||||
'perPage' => $limit,
|
||||
];
|
||||
|
||||
$response = $this->makeGraphQLRequest($query, $variables);
|
||||
|
||||
if ($response === false || ! isset($response['data']['Page']['media'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $response['data']['Page']['media'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get anime by AniList ID.
|
||||
*
|
||||
* @return array|false
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getAnimeById(int $anilistId)
|
||||
{
|
||||
$query = '
|
||||
query ($id: Int) {
|
||||
Media (id: $id, type: ANIME) {
|
||||
id
|
||||
idMal
|
||||
title {
|
||||
romaji
|
||||
english
|
||||
native
|
||||
}
|
||||
type
|
||||
format
|
||||
status
|
||||
countryOfOrigin
|
||||
episodes
|
||||
duration
|
||||
source
|
||||
popularity
|
||||
favourites
|
||||
startDate {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
endDate {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
description
|
||||
averageScore
|
||||
hashtag
|
||||
coverImage {
|
||||
large
|
||||
}
|
||||
genres
|
||||
studios {
|
||||
nodes {
|
||||
name
|
||||
}
|
||||
}
|
||||
characters {
|
||||
nodes {
|
||||
name {
|
||||
full
|
||||
}
|
||||
}
|
||||
}
|
||||
relations {
|
||||
edges {
|
||||
relationType
|
||||
node {
|
||||
id
|
||||
title {
|
||||
romaji
|
||||
english
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
';
|
||||
|
||||
$variables = ['id' => $anilistId];
|
||||
|
||||
$response = $this->makeGraphQLRequest($query, $variables);
|
||||
|
||||
if ($response === false || ! isset($response['data']['Media'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $response['data']['Media'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a GraphQL request to AniList API with rate limiting.
|
||||
*
|
||||
* @return array|false
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function makeGraphQLRequest(string $query, array $variables = [])
|
||||
{
|
||||
// Enforce rate limiting
|
||||
$this->enforceRateLimit();
|
||||
|
||||
try {
|
||||
$response = $this->client->post('', [
|
||||
'json' => [
|
||||
'query' => $query,
|
||||
'variables' => $variables,
|
||||
],
|
||||
]);
|
||||
|
||||
$statusCode = $response->getStatusCode();
|
||||
$body = json_decode($response->getBody()->getContents(), true);
|
||||
|
||||
// Track rate limit from headers
|
||||
$remaining = (int) ($response->getHeader('X-RateLimit-Remaining')[0] ?? self::RATE_LIMIT_PER_MINUTE);
|
||||
$resetAt = (int) ($response->getHeader('X-RateLimit-Reset')[0] ?? time() + 60);
|
||||
|
||||
// Record this request
|
||||
$this->rateLimitQueue[] = [
|
||||
'timestamp' => time(),
|
||||
'remaining' => $remaining,
|
||||
'resetAt' => $resetAt,
|
||||
];
|
||||
|
||||
// Clean old entries (older than 1 minute)
|
||||
$this->rateLimitQueue = array_filter($this->rateLimitQueue, function ($entry) {
|
||||
return $entry['timestamp'] > (time() - 60);
|
||||
});
|
||||
|
||||
if ($statusCode === 200 && isset($body['data'])) {
|
||||
return $body;
|
||||
}
|
||||
|
||||
if (isset($body['errors'])) {
|
||||
if ($this->echooutput) {
|
||||
$this->colorCli->error('AniList API Error: '.json_encode($body['errors']));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (GuzzleException $e) {
|
||||
if ($this->echooutput) {
|
||||
$this->colorCli->error('AniList API Request Failed: '.$e->getMessage());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforce rate limiting: 90 requests per minute.
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function enforceRateLimit(): void
|
||||
{
|
||||
// Count requests in the last minute
|
||||
$recentRequests = array_filter($this->rateLimitQueue, function ($entry) {
|
||||
return $entry['timestamp'] > (time() - 60);
|
||||
});
|
||||
|
||||
$requestCount = count($recentRequests);
|
||||
|
||||
// If we're approaching the limit, wait
|
||||
if ($requestCount >= (self::RATE_LIMIT_PER_MINUTE - 5)) {
|
||||
// Wait until the oldest request is more than 1 minute old
|
||||
if (! empty($this->rateLimitQueue)) {
|
||||
$oldestRequest = min(array_column($this->rateLimitQueue, 'timestamp'));
|
||||
$waitTime = 60 - (time() - $oldestRequest) + 1;
|
||||
|
||||
if ($waitTime > 0 && $waitTime <= 60) {
|
||||
if ($this->echooutput) {
|
||||
$this->colorCli->warning("Rate limit approaching. Waiting {$waitTime} seconds...");
|
||||
}
|
||||
sleep($waitTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert or update AniList title data.
|
||||
*/
|
||||
private function insertAniListTitle(int $anidbid, string $type, string $lang, string $title): void
|
||||
{
|
||||
$check = AnidbTitle::query()->where([
|
||||
'anidbid' => $anidbid,
|
||||
'type' => $type,
|
||||
'lang' => $lang,
|
||||
'title' => $title,
|
||||
])->first();
|
||||
|
||||
if ($check === null) {
|
||||
AnidbTitle::insertOrIgnore([
|
||||
'anidbid' => $anidbid,
|
||||
'type' => $type,
|
||||
'lang' => $lang,
|
||||
'title' => $title,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert or update AniList info data.
|
||||
*/
|
||||
private function insertAniListInfo(int $anidbid, array $anilistData): void
|
||||
{
|
||||
// Extract data from AniList response
|
||||
$startDate = null;
|
||||
if (isset($anilistData['startDate']['year'], $anilistData['startDate']['month'], $anilistData['startDate']['day'])) {
|
||||
$startDate = sprintf(
|
||||
'%04d-%02d-%02d',
|
||||
$anilistData['startDate']['year'],
|
||||
$anilistData['startDate']['month'] ?? 1,
|
||||
$anilistData['startDate']['day'] ?? 1
|
||||
);
|
||||
}
|
||||
|
||||
$endDate = null;
|
||||
if (isset($anilistData['endDate']['year'], $anilistData['endDate']['month'], $anilistData['endDate']['day'])) {
|
||||
$endDate = sprintf(
|
||||
'%04d-%02d-%02d',
|
||||
$anilistData['endDate']['year'],
|
||||
$anilistData['endDate']['month'] ?? 1,
|
||||
$anilistData['endDate']['day'] ?? 1
|
||||
);
|
||||
}
|
||||
|
||||
$description = $anilistData['description'] ?? '';
|
||||
// Remove HTML tags from description
|
||||
$description = strip_tags($description);
|
||||
$description = html_entity_decode($description, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
|
||||
$rating = isset($anilistData['averageScore']) ? (string) $anilistData['averageScore'] : null;
|
||||
|
||||
$picture = null;
|
||||
if (isset($anilistData['coverImage']['large'])) {
|
||||
$picture = basename(parse_url($anilistData['coverImage']['large'], PHP_URL_PATH));
|
||||
}
|
||||
|
||||
$genres = '';
|
||||
if (isset($anilistData['genres']) && is_array($anilistData['genres'])) {
|
||||
$genres = implode(', ', $anilistData['genres']);
|
||||
}
|
||||
|
||||
$creators = '';
|
||||
if (isset($anilistData['studios']['nodes']) && is_array($anilistData['studios']['nodes'])) {
|
||||
$studioNames = array_map(function ($studio) {
|
||||
return $studio['name'] ?? '';
|
||||
}, $anilistData['studios']['nodes']);
|
||||
$creators = implode(', ', array_filter($studioNames));
|
||||
}
|
||||
|
||||
$characters = '';
|
||||
if (isset($anilistData['characters']['nodes']) && is_array($anilistData['characters']['nodes'])) {
|
||||
$characterNames = array_map(function ($character) {
|
||||
return $character['name']['full'] ?? '';
|
||||
}, array_slice($anilistData['characters']['nodes'], 0, 10)); // Limit to 10 characters
|
||||
$characters = implode(', ', array_filter($characterNames));
|
||||
}
|
||||
|
||||
$related = '';
|
||||
$similar = '';
|
||||
if (isset($anilistData['relations']['edges']) && is_array($anilistData['relations']['edges'])) {
|
||||
$relatedItems = [];
|
||||
$similarItems = [];
|
||||
foreach ($anilistData['relations']['edges'] as $edge) {
|
||||
$relationType = $edge['relationType'] ?? '';
|
||||
$title = $edge['node']['title']['english'] ?? $edge['node']['title']['romaji'] ?? '';
|
||||
$id = $edge['node']['id'] ?? '';
|
||||
|
||||
if (in_array($relationType, ['SEQUEL', 'PREQUEL', 'SIDE_STORY', 'PARENT', 'SPIN_OFF'])) {
|
||||
$relatedItems[] = $title.' ('.$id.')';
|
||||
} elseif (in_array($relationType, ['ALTERNATIVE', 'CHARACTER'])) {
|
||||
$similarItems[] = $title.' ('.$id.')';
|
||||
}
|
||||
}
|
||||
$related = implode(', ', array_slice($relatedItems, 0, 20));
|
||||
$similar = implode(', ', array_slice($similarItems, 0, 20));
|
||||
}
|
||||
|
||||
$anilistId = $anilistData['id'] ?? null;
|
||||
$malId = $anilistData['idMal'] ?? null;
|
||||
$country = $anilistData['countryOfOrigin'] ?? null;
|
||||
$mediaType = $anilistData['type'] ?? null; // ANIME or MANGA
|
||||
$episodes = isset($anilistData['episodes']) ? (int) $anilistData['episodes'] : null;
|
||||
$duration = isset($anilistData['duration']) ? (int) $anilistData['duration'] : null;
|
||||
$status = $anilistData['status'] ?? null;
|
||||
$source = $anilistData['source'] ?? null;
|
||||
$hashtag = $anilistData['hashtag'] ?? null;
|
||||
|
||||
$check = AnidbInfo::query()->where('anidbid', $anidbid)->first();
|
||||
|
||||
if ($check === null) {
|
||||
AnidbInfo::insert([
|
||||
'anidbid' => $anidbid,
|
||||
'anilist_id' => $anilistId,
|
||||
'mal_id' => $malId,
|
||||
'country' => $country,
|
||||
'media_type' => $mediaType,
|
||||
'type' => $anilistData['format'] ?? null,
|
||||
'episodes' => $episodes,
|
||||
'duration' => $duration,
|
||||
'status' => $status,
|
||||
'source' => $source,
|
||||
'hashtag' => $hashtag,
|
||||
'startdate' => $startDate,
|
||||
'enddate' => $endDate,
|
||||
'description' => $description,
|
||||
'rating' => $rating,
|
||||
'picture' => $picture,
|
||||
'categories' => $genres,
|
||||
'characters' => $characters,
|
||||
'creators' => $creators,
|
||||
'related' => $related,
|
||||
'similar' => $similar,
|
||||
'updated' => now(),
|
||||
]);
|
||||
} else {
|
||||
AnidbInfo::query()
|
||||
->where('anidbid', $anidbid)
|
||||
->update([
|
||||
'anilist_id' => $anilistId ?? $check->anilist_id,
|
||||
'mal_id' => $malId ?? $check->mal_id,
|
||||
'country' => $country ?? $check->country,
|
||||
'media_type' => $mediaType ?? $check->media_type,
|
||||
'type' => $anilistData['format'] ?? $anilistData['type'] ?? $check->type,
|
||||
'episodes' => $episodes ?? $check->episodes,
|
||||
'duration' => $duration ?? $check->duration,
|
||||
'status' => $status ?? $check->status,
|
||||
'source' => $source ?? $check->source,
|
||||
'hashtag' => $hashtag ?? $check->hashtag,
|
||||
'startdate' => $startDate ?? $check->startdate,
|
||||
'enddate' => $endDate ?? $check->enddate,
|
||||
'description' => $description ?: $check->description,
|
||||
'rating' => $rating ?? $check->rating,
|
||||
'picture' => $picture ?? $check->picture,
|
||||
'categories' => $genres ?: $check->categories,
|
||||
'characters' => $characters ?: $check->characters,
|
||||
'creators' => $creators ?: $check->creators,
|
||||
'related' => $related ?: $check->related,
|
||||
'similar' => $similar ?: $check->similar,
|
||||
'updated' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
// Insert titles
|
||||
if (isset($anilistData['title']['romaji']) && ! empty($anilistData['title']['romaji'])) {
|
||||
$this->insertAniListTitle($anidbid, 'main', 'x-jat', $anilistData['title']['romaji']);
|
||||
}
|
||||
if (isset($anilistData['title']['english']) && ! empty($anilistData['title']['english'])) {
|
||||
$this->insertAniListTitle($anidbid, 'official', 'en', $anilistData['title']['english']);
|
||||
}
|
||||
if (isset($anilistData['title']['native']) && ! empty($anilistData['title']['native'])) {
|
||||
$this->insertAniListTitle($anidbid, 'main', 'ja', $anilistData['title']['native']);
|
||||
}
|
||||
|
||||
// Download cover image if available
|
||||
if (! empty($picture) && isset($anilistData['coverImage']['large'])) {
|
||||
$this->downloadCoverImage($anidbid, $anilistData['coverImage']['large']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download cover image from AniList.
|
||||
*/
|
||||
private function downloadCoverImage(int $anidbid, string $imageUrl): void
|
||||
{
|
||||
// Use the format expected by getReleaseCover: {id}-cover.jpg
|
||||
// This matches the format used by movies: {id}-cover.jpg
|
||||
$coverFilename = $anidbid.'-cover.jpg';
|
||||
$coverPath = $this->imgSavePath.$coverFilename;
|
||||
|
||||
if (file_exists($coverPath)) {
|
||||
return; // Already exists
|
||||
}
|
||||
|
||||
try {
|
||||
// Ensure directory exists with proper permissions
|
||||
if (! is_dir($this->imgSavePath)) {
|
||||
if (! mkdir($this->imgSavePath, 0755, true) && ! is_dir($this->imgSavePath)) {
|
||||
throw new \RuntimeException(sprintf('Directory "%s" was not created', $this->imgSavePath));
|
||||
}
|
||||
}
|
||||
|
||||
$response = $this->client->get($imageUrl);
|
||||
$imageData = $response->getBody()->getContents();
|
||||
|
||||
// Write the image file
|
||||
$bytesWritten = file_put_contents($coverPath, $imageData);
|
||||
|
||||
if ($bytesWritten === false) {
|
||||
throw new \RuntimeException("Failed to write cover image to {$coverPath}");
|
||||
}
|
||||
|
||||
// Set proper file permissions
|
||||
chmod($coverPath, 0644);
|
||||
|
||||
if ($this->echooutput) {
|
||||
$this->colorCli->info("Downloaded cover image for ID {$anidbid} from AniList to {$coverPath}");
|
||||
}
|
||||
} catch (GuzzleException $e) {
|
||||
if ($this->echooutput) {
|
||||
$this->colorCli->warning("Failed to download cover image for ID {$anidbid}: ".$e->getMessage());
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
if ($this->echooutput) {
|
||||
$this->colorCli->error("Error saving cover image for ID {$anidbid}: ".$e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Directs flow for populating the AniList Info table.
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function populateInfoTable(string $anilistId = ''): void
|
||||
{
|
||||
if (empty($anilistId)) {
|
||||
// Get titles that need updating (missing anilist_id or stale)
|
||||
$anidbIds = AnidbTitle::query()
|
||||
->selectRaw('DISTINCT anidb_titles.anidbid')
|
||||
->leftJoin('anidb_info as ai', 'ai.anidbid', '=', 'anidb_titles.anidbid')
|
||||
->where(function ($query) {
|
||||
$query->whereNull('ai.anilist_id')
|
||||
->orWhere('ai.updated', '<', now()->subWeek());
|
||||
})
|
||||
->limit(100) // Process in batches
|
||||
->get();
|
||||
|
||||
foreach ($anidbIds as $anidb) {
|
||||
// Try to find by title first
|
||||
$title = AnidbTitle::query()
|
||||
->where('anidbid', $anidb->anidbid)
|
||||
->where('lang', 'en')
|
||||
->value('title');
|
||||
|
||||
if ($title) {
|
||||
$searchResults = $this->searchAnime($title, 1);
|
||||
if ($searchResults && ! empty($searchResults)) {
|
||||
$anilistData = $searchResults[0];
|
||||
$this->insertAniListInfo($anidb->anidbid, $anilistData);
|
||||
// Rate limiting is handled in makeGraphQLRequest
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Get specific anime by AniList ID
|
||||
$anilistData = $this->getAnimeById((int) $anilistId);
|
||||
|
||||
if ($anilistData === false) {
|
||||
if ($this->echooutput) {
|
||||
$this->colorCli->info("Anime with AniList ID: {$anilistId} not found.");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// We need to find or create an anidbid for this
|
||||
// For now, use anilist_id as anidbid if not found
|
||||
$anidbid = AnidbInfo::query()
|
||||
->where('anilist_id', $anilistId)
|
||||
->value('anidbid');
|
||||
|
||||
if (! $anidbid) {
|
||||
// Create a new entry using anilist_id as anidbid
|
||||
$anidbid = (int) $anilistId;
|
||||
}
|
||||
|
||||
$this->insertAniListInfo($anidbid, $anilistData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,25 +4,24 @@ declare(strict_types=1);
|
||||
|
||||
namespace Blacklight\processing\post;
|
||||
|
||||
use App\Models\AnidbEpisode;
|
||||
use App\Models\AnidbInfo;
|
||||
use App\Models\AnidbTitle;
|
||||
use App\Models\Category;
|
||||
use App\Models\Release;
|
||||
use App\Models\Settings;
|
||||
use Blacklight\ColorCLI;
|
||||
use Blacklight\PopulateAniDB as PaDb;
|
||||
use Blacklight\PopulateAniList as PaList;
|
||||
|
||||
class AniDB
|
||||
{
|
||||
private const PROC_EXTFAIL = -1; // Release Anime title/episode # could not be extracted from searchname
|
||||
|
||||
private const PROC_NOMATCH = -2; // AniDB ID was not found in anidb table using extracted title/episode #
|
||||
private const PROC_NOMATCH = -2; // AniList ID was not found in anidb table using extracted title
|
||||
|
||||
/** @var bool Whether to echo messages to CLI */
|
||||
public bool $echooutput;
|
||||
|
||||
public PaDb $padb;
|
||||
public PaList $palist;
|
||||
|
||||
/** @var int number of AniDB releases to process */
|
||||
private int $aniqty;
|
||||
@@ -39,13 +38,20 @@ class AniDB
|
||||
*/
|
||||
private array $titleCache = [];
|
||||
|
||||
/**
|
||||
* Simple cache of looked up titles -> anilist_id to reduce repeat queries within one run.
|
||||
*
|
||||
* @var array<string,int>
|
||||
*/
|
||||
private array $anilistIdCache = [];
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->echooutput = (bool) config('nntmux.echocli');
|
||||
$this->padb = new PaDb;
|
||||
$this->palist = new PaList;
|
||||
$this->colorCli = new ColorCLI;
|
||||
|
||||
$quantity = (int) Settings::settingValue('maxanidbprocessed');
|
||||
@@ -80,8 +86,7 @@ class AniDB
|
||||
->get();
|
||||
|
||||
if ($results->count() > 0) {
|
||||
// Honor AniDB API cooldown before starting a batch.
|
||||
$this->doRandomSleep();
|
||||
// AniList rate limiting is handled internally in PopulateAniList
|
||||
|
||||
foreach ($results as $release) {
|
||||
$matched = $this->matchAnimeRelease($release);
|
||||
@@ -96,32 +101,19 @@ class AniDB
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves episode info if present.
|
||||
* AniList doesn't support episode lookups, so this method is deprecated.
|
||||
* Kept for compatibility but returns empty array.
|
||||
*/
|
||||
private function checkAniDBInfo(int $anidbId, int $episode = -1): array
|
||||
{
|
||||
$q = AnidbEpisode::query()->where(['anidbid' => $anidbId]);
|
||||
if ($episode >= 0) {
|
||||
$q->where('episode_no', $episode);
|
||||
}
|
||||
$result = $q->select(['anidbid', 'episode_no', 'airdate', 'episode_title'])->first();
|
||||
|
||||
return $result ? $result->toArray() : [];
|
||||
// AniList doesn't support episode lookups
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Returns ['title' => string, 'epno' => int] on success else empty array.
|
||||
* Extracts anime title from release searchname.
|
||||
* Returns ['title' => string] on success else empty array.
|
||||
* Note: AniList doesn't support episode lookups, so we only extract the title.
|
||||
*/
|
||||
private function extractTitleEpisode(string $cleanName = ''): array
|
||||
{
|
||||
@@ -134,68 +126,92 @@ class AniDB
|
||||
$s = preg_replace('/^(?:\[[^\]]+\]\s*)+/', '', $s);
|
||||
$s = trim((string) $s);
|
||||
|
||||
// Remove language codes and tags (before extracting title)
|
||||
// Common language tags: [ENG], [JAP], [SUB], [DUB], [MULTI], etc.
|
||||
$s = preg_replace('/\[(?:ENG|JAP|JPN|SUB|DUB|MULTI|RAW|HARDSUB|SOFTSUB|HARDDUB|SOFTDUB|ITA|SPA|FRE|GER|RUS|CHI|KOR)\]/i', ' ', $s);
|
||||
$s = preg_replace('/\((?:ENG|JAP|JPN|SUB|DUB|MULTI|RAW|HARDSUB|SOFTSUB|HARDDUB|SOFTDUB|ITA|SPA|FRE|GER|RUS|CHI|KOR)\)/i', ' ', $s);
|
||||
|
||||
// Remove episode patterns and extract title
|
||||
$title = '';
|
||||
$ep = -1;
|
||||
|
||||
// 1) Look for " - NNN" first
|
||||
// Try to extract title by removing episode patterns
|
||||
// 1) Look for " - NNN" and extract title before it
|
||||
if (preg_match('/\s-\s*(\d{1,3})\b/', $s, $m, PREG_OFFSET_CAPTURE)) {
|
||||
$ep = (int) $m[1][0];
|
||||
$title = substr($s, 0, (int) $m[0][1]);
|
||||
}
|
||||
|
||||
// 2) If not found, look for " E0*NNN"
|
||||
if ($ep < 0 && preg_match('/\sE0*(\d{1,3})\b/i', $s, $m, PREG_OFFSET_CAPTURE)) {
|
||||
$ep = (int) $m[1][0];
|
||||
// 2) If not found, look for " E0*NNN" or " Ep NNN"
|
||||
elseif (preg_match('/\sE(?:p(?:isode)?)?\s*0*(\d{1,3})\b/i', $s, $m, PREG_OFFSET_CAPTURE)) {
|
||||
$title = substr($s, 0, (int) $m[0][1]);
|
||||
}
|
||||
|
||||
// 3) Keywords Movie/OVA/Complete Series
|
||||
if ($ep < 0 && preg_match('/\b(Movie|OVA|Complete Series)\b/i', $s, $m, PREG_OFFSET_CAPTURE)) {
|
||||
$kind = strtolower($m[1][0]);
|
||||
$ep = match ($kind) {
|
||||
'movie', 'ova' => 1,
|
||||
'complete series' => 0,
|
||||
default => -1,
|
||||
};
|
||||
// 3) Look for " S01E01" or " S1E1" pattern
|
||||
elseif (preg_match('/\sS\d+E\d+/i', $s, $m, PREG_OFFSET_CAPTURE)) {
|
||||
$title = substr($s, 0, (int) $m[0][1]);
|
||||
}
|
||||
|
||||
// 4) BD/resolution releases: pick title before next bracket token
|
||||
if ($ep < 0 && preg_match('/\[(?:BD|\d{3,4}[ipx])\]/i', $s, $m, PREG_OFFSET_CAPTURE)) {
|
||||
$ep = 1;
|
||||
// 4) Keywords Movie/OVA/Complete Series
|
||||
elseif (preg_match('/\b(Movie|OVA|Complete Series|Complete|Full Series)\b/i', $s, $m, PREG_OFFSET_CAPTURE)) {
|
||||
$title = substr($s, 0, (int) $m[0][1]);
|
||||
}
|
||||
// 5) BD/resolution releases: pick title before next bracket token
|
||||
elseif (preg_match('/\[(?:BD|BDRip|BluRay|Blu-Ray|\d{3,4}[ipx]|HEVC|x264|x265|H264|H265)\]/i', $s, $m, PREG_OFFSET_CAPTURE)) {
|
||||
$title = substr($s, 0, (int) $m[0][1]);
|
||||
} else {
|
||||
// No episode pattern found, use the whole string as title
|
||||
$title = $s;
|
||||
}
|
||||
|
||||
$title = $this->cleanTitle((string) $title);
|
||||
|
||||
if ($title === '' || $ep < 0) {
|
||||
if ($title === '') {
|
||||
$this->status = self::PROC_EXTFAIL;
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
return ['title' => $title, 'epno' => $ep];
|
||||
return ['title' => $title];
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip stray separators or tokens accidentally captured at the end of title.
|
||||
* Strip stray separators, language codes, episode numbers, and other release tags from title.
|
||||
*/
|
||||
private function cleanTitle(string $title): string
|
||||
{
|
||||
// Remove trailing "- 123", trailing dash, trailing E/E0/Episode tokens, and trailing Vol.
|
||||
$patterns = [
|
||||
'/\s*-\s*\d+\s*$/i',
|
||||
'/\s*-\s*$/',
|
||||
'/\s+E(?:pi(?:sode)?)?\s*0?\s*$/i',
|
||||
'/\s+Vol\.?\s*$/i',
|
||||
];
|
||||
$title = preg_replace($patterns, '', $title);
|
||||
|
||||
// Remove all bracketed tags (language, quality, etc.)
|
||||
$title = preg_replace('/\[[^\]]+\]/', ' ', $title);
|
||||
|
||||
// Remove all parenthesized tags
|
||||
$title = preg_replace('/\([^)]+\)/', ' ', $title);
|
||||
|
||||
// Remove language codes (standalone or with separators)
|
||||
$title = preg_replace('/\b(ENG|JAP|JPN|SUB|DUB|MULTI|RAW|HARDSUB|SOFTSUB|HARDDUB|SOFTDUB|ITA|SPA|FRE|GER|RUS|CHI|KOR)\b/i', ' ', $title);
|
||||
|
||||
// Remove episode patterns
|
||||
$title = preg_replace('/\s*-\s*\d{1,4}\s*$/i', '', $title);
|
||||
$title = preg_replace('/\s*-\s*$/i', '', $title);
|
||||
$title = preg_replace('/\s+E(?:p(?:isode)?)?\s*0*\d{1,4}\s*$/i', '', $title);
|
||||
$title = preg_replace('/\s+S\d+E\d+\s*$/i', '', $title);
|
||||
|
||||
// Remove quality/resolution tags
|
||||
$title = preg_replace('/\b(480p|720p|1080p|2160p|4K|BD|BDRip|BluRay|Blu-Ray|HEVC|x264|x265|H264|H265|WEB|WEBRip|DVDRip|TVRip)\b/i', ' ', $title);
|
||||
|
||||
// Remove common release tags
|
||||
$title = preg_replace('/\b(PROPER|REPACK|RIP|ISO|CRACK|BETA|ALPHA|FINAL|COMPLETE|FULL)\b/i', ' ', $title);
|
||||
|
||||
// Remove volume/chapter markers
|
||||
$title = preg_replace('/\s+Vol\.?\s*\d*\s*$/i', '', $title);
|
||||
$title = preg_replace('/\s+Ch\.?\s*\d*\s*$/i', '', $title);
|
||||
|
||||
// Remove trailing dashes and separators
|
||||
$title = preg_replace('/\s*[-_]\s*$/', '', $title);
|
||||
|
||||
// Normalize whitespace
|
||||
$title = preg_replace('/\s+/', ' ', $title);
|
||||
|
||||
return trim((string) $title);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve AniDB title row (id + title) by name attempt exact then partial.
|
||||
* Retrieve AniList anime by searching for title.
|
||||
* First checks local database, then searches AniList API if not found.
|
||||
*/
|
||||
private function getAnidbByName(string $searchName = ''): ?AnidbTitle
|
||||
{
|
||||
@@ -204,29 +220,70 @@ class AniDB
|
||||
}
|
||||
|
||||
$key = strtolower($searchName);
|
||||
|
||||
// Check cache first
|
||||
if (isset($this->titleCache[$key])) {
|
||||
return AnidbTitle::query()->select(['anidbid', 'title'])->where('anidbid', $this->titleCache[$key])->first();
|
||||
}
|
||||
|
||||
// Exact (case-insensitive) first
|
||||
// Try exact match in local database first
|
||||
$exact = AnidbTitle::query()->whereRaw('LOWER(title) = ?', [$key])->select(['anidbid', 'title'])->first();
|
||||
if ($exact) {
|
||||
$this->titleCache[$key] = (int) $exact->anidbid;
|
||||
|
||||
return $exact;
|
||||
}
|
||||
|
||||
// Partial fallback
|
||||
// Try partial match in local database
|
||||
$partial = AnidbTitle::query()->where('title', 'like', '%'.$searchName.'%')->select(['anidbid', 'title'])->first();
|
||||
if ($partial) {
|
||||
$this->titleCache[$key] = (int) $partial->anidbid;
|
||||
return $partial;
|
||||
}
|
||||
|
||||
return $partial;
|
||||
// Not found locally, search AniList API
|
||||
try {
|
||||
$searchResults = $this->palist->searchAnime($searchName, 1);
|
||||
if ($searchResults && !empty($searchResults)) {
|
||||
$anilistData = $searchResults[0];
|
||||
$anilistId = $anilistData['id'] ?? null;
|
||||
|
||||
if ($anilistId) {
|
||||
// Use anilist_id as anidbid for new entries
|
||||
$anidbid = AnidbInfo::query()->where('anilist_id', $anilistId)->value('anidbid');
|
||||
|
||||
if (!$anidbid) {
|
||||
// Create new entry using anilist_id as anidbid
|
||||
$anidbid = (int) $anilistId;
|
||||
$this->palist->populateTable('info', $anilistId);
|
||||
}
|
||||
|
||||
// Get the title from database after insertion
|
||||
$title = AnidbTitle::query()
|
||||
->where('anidbid', $anidbid)
|
||||
->where('lang', 'en')
|
||||
->value('title');
|
||||
|
||||
if ($title) {
|
||||
$this->titleCache[$key] = $anidbid;
|
||||
return AnidbTitle::query()
|
||||
->where('anidbid', $anidbid)
|
||||
->where('title', $title)
|
||||
->first();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
if ($this->echooutput) {
|
||||
$this->colorCli->error('AniList search failed: '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches the anime release to AniDB Info; fetches remotely if needed.
|
||||
* Matches the anime release to AniList Info; fetches remotely if needed.
|
||||
* Note: AniList doesn't support episode lookups, so we only match by title.
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
@@ -241,11 +298,9 @@ class AniDB
|
||||
}
|
||||
|
||||
$title = $cleanArr['title'];
|
||||
$epno = $cleanArr['epno'];
|
||||
|
||||
// We ignore episode number 0 (Complete Series) for matching episodes but still link the title.
|
||||
if ($this->echooutput) {
|
||||
$this->colorCli->info('Looking Up: Title: '.$title.' Episode: '.$epno);
|
||||
$this->colorCli->info('Looking Up: Title: '.$title);
|
||||
}
|
||||
|
||||
$anidbTitle = $this->getAnidbByName($title);
|
||||
@@ -258,29 +313,40 @@ class AniDB
|
||||
if ($anidbTitle && is_numeric($anidbTitle->anidbid) && (int) $anidbTitle->anidbid > 0) {
|
||||
$anidbId = (int) $anidbTitle->anidbid;
|
||||
|
||||
$episodeInfo = ($epno > 0) ? $this->checkAniDBInfo($anidbId, $epno) : [];
|
||||
// Check if we need to update info from AniList
|
||||
$info = AnidbInfo::query()->where('anidbid', $anidbId)->first();
|
||||
if (! $info || $this->shouldUpdateInfo($anidbId)) {
|
||||
// Try to get anilist_id if we have it
|
||||
$anilistId = $info->anilist_id ?? null;
|
||||
|
||||
if (! $anilistId) {
|
||||
// Search AniList for this title
|
||||
try {
|
||||
$searchResults = $this->palist->searchAnime($title, 1);
|
||||
if ($searchResults && !empty($searchResults)) {
|
||||
$anilistId = $searchResults[0]['id'] ?? null;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
if ($this->echooutput) {
|
||||
$this->colorCli->warning('AniList search failed: '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($episodeInfo) && $this->shouldUpdateInfo($anidbId)) {
|
||||
// Fetch remote info
|
||||
$this->padb->populateTable('info', $anidbId);
|
||||
$this->doRandomSleep();
|
||||
$episodeInfo = ($epno > 0) ? $this->checkAniDBInfo($anidbId, $epno) : [];
|
||||
$type = 'Remote';
|
||||
if ($anilistId) {
|
||||
// Fetch remote info from AniList
|
||||
$this->palist->populateTable('info', $anilistId);
|
||||
$type = 'Remote';
|
||||
}
|
||||
}
|
||||
|
||||
$episodeTitle = $episodeInfo['episode_title'] ?? 'Unknown';
|
||||
$this->updateRelease($anidbId, (int) $release->id);
|
||||
|
||||
if ($this->echooutput) {
|
||||
// Break chained calls into sequential ones; methods return void and are not chainable.
|
||||
$this->colorCli->headerOver('Matched '.$type.' AniDB ID: ');
|
||||
$this->colorCli->headerOver('Matched '.$type.' AniList ID: ');
|
||||
$this->colorCli->primary((string) $anidbId);
|
||||
$this->colorCli->alternateOver(' Title: ');
|
||||
$this->colorCli->primary($anidbTitle->title);
|
||||
$this->colorCli->alternateOver(' Episode #: ');
|
||||
$this->colorCli->primary((string) $epno);
|
||||
$this->colorCli->alternateOver(' Episode Title: ');
|
||||
$this->colorCli->primary($episodeTitle);
|
||||
}
|
||||
$matched = true;
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
class MigrateAnimeCovers extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'anime:migrate-covers {--dry-run : Show what would be renamed without actually renaming}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Migrate anime cover files from old format ({id}.jpg) to new format ({id}-cover.jpg)';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$animeCoverPath = storage_path('covers/anime/');
|
||||
$dryRun = $this->option('dry-run');
|
||||
|
||||
if (!is_dir($animeCoverPath)) {
|
||||
$this->error("Anime covers directory does not exist: {$animeCoverPath}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
$this->info("Scanning anime covers directory: {$animeCoverPath}");
|
||||
|
||||
$files = File::files($animeCoverPath);
|
||||
$renamed = 0;
|
||||
$skipped = 0;
|
||||
|
||||
foreach ($files as $file) {
|
||||
$filename = $file->getFilename();
|
||||
|
||||
// Check if file matches old format: {id}.jpg (where id is numeric)
|
||||
if (preg_match('/^(\d+)\.jpg$/', $filename, $matches)) {
|
||||
$anidbid = $matches[1];
|
||||
$newFilename = "{$anidbid}-cover.jpg";
|
||||
$newPath = $animeCoverPath . $newFilename;
|
||||
|
||||
// Skip if new format already exists
|
||||
if (file_exists($newPath)) {
|
||||
$this->warn("Skipping {$filename} - {$newFilename} already exists");
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($dryRun) {
|
||||
$this->line("Would rename: {$filename} -> {$newFilename}");
|
||||
$renamed++;
|
||||
} else {
|
||||
if (rename($file->getPathname(), $newPath)) {
|
||||
$this->info("Renamed: {$filename} -> {$newFilename}");
|
||||
$renamed++;
|
||||
} else {
|
||||
$this->error("Failed to rename: {$filename}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($dryRun) {
|
||||
$this->info("\nDry run complete. Would rename {$renamed} file(s), skipped {$skipped}.");
|
||||
$this->info("Run without --dry-run to perform the migration.");
|
||||
} else {
|
||||
$this->info("\nMigration complete. Renamed {$renamed} file(s), skipped {$skipped}.");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Blacklight\PopulateAniDB;
|
||||
use Blacklight\PopulateAniList;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class NntmuxPopulateAniDB extends Command
|
||||
@@ -13,29 +13,32 @@ class NntmuxPopulateAniDB extends Command
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'nntmux:populate-anidb
|
||||
{--full : Populate full data}
|
||||
{--info : Populate info only}
|
||||
{--anidbid : Populate tables for specific anidbid}';
|
||||
{--anilistid : Populate tables for specific AniList ID}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Populate AniDB table';
|
||||
protected $description = 'Populate AniList table (replaces AniDB)';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
if ($this->option('full')) {
|
||||
(new PopulateAniDB(['Echo' => true]))->populateTable('full');
|
||||
} elseif ($this->option('info') && is_numeric($this->option('anidbid'))) {
|
||||
(new PopulateAniDB(['Echo' => true]))->populateTable('info', $this->option('anidbid'));
|
||||
$palist = new PopulateAniList;
|
||||
|
||||
if ($this->option('info') && is_numeric($this->option('anilistid'))) {
|
||||
$palist->populateTable('info', $this->option('anilistid'));
|
||||
} elseif ($this->option('info')) {
|
||||
(new PopulateAniDB(['Echo' => true]))->populateTable('info');
|
||||
$palist->populateTable('info');
|
||||
} else {
|
||||
$this->error('Please specify --info option');
|
||||
return;
|
||||
}
|
||||
$this->info('AniDB tables populated with requested data');
|
||||
|
||||
$this->info('AniList tables populated with requested data');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +82,6 @@ class NntmuxResetDb extends Command
|
||||
'releases',
|
||||
'anidb_titles',
|
||||
'anidb_info',
|
||||
'anidb_episodes',
|
||||
'releases_groups',
|
||||
];
|
||||
foreach ($arr as &$value) {
|
||||
|
||||
@@ -37,6 +37,17 @@ class CoverController extends Controller
|
||||
} else {
|
||||
$filePath = storage_path("covers/{$type}/{$filename}");
|
||||
}
|
||||
} elseif ($type === 'anime') {
|
||||
// For anime, try the requested filename first, then fall back to old format (without -cover)
|
||||
$filePath = storage_path("covers/{$type}/{$filename}");
|
||||
|
||||
// If file doesn't exist and filename ends with -cover.jpg, try old format
|
||||
if (!file_exists($filePath) && preg_match('/^(\d+)-cover\.jpg$/', $filename, $matches)) {
|
||||
$oldFormatPath = storage_path("covers/{$type}/{$matches[1]}.jpg");
|
||||
if (file_exists($oldFormatPath)) {
|
||||
$filePath = $oldFormatPath;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$filePath = storage_path("covers/{$type}/{$filename}");
|
||||
}
|
||||
|
||||
@@ -121,6 +121,22 @@ class DetailsController extends BasePageController
|
||||
$AniDBAPIArray = '';
|
||||
if ($data['anidbid'] > 0) {
|
||||
$AniDBAPIArray = (new AniDB)->getAnimeInfo($data['anidbid']);
|
||||
|
||||
// If we have anilist_id but missing details, fetch from AniList
|
||||
if ($AniDBAPIArray && !empty($AniDBAPIArray->anilist_id)) {
|
||||
$anilistId = is_object($AniDBAPIArray) ? $AniDBAPIArray->anilist_id : ($AniDBAPIArray['anilist_id'] ?? null);
|
||||
if ($anilistId && (empty($AniDBAPIArray->country) && empty($AniDBAPIArray->media_type))) {
|
||||
// Fetch fresh data from AniList if country/media_type is missing
|
||||
try {
|
||||
$palist = new \Blacklight\PopulateAniList;
|
||||
$palist->populateTable('info', $anilistId);
|
||||
// Refresh the data
|
||||
$AniDBAPIArray = (new AniDB)->getAnimeInfo($data['anidbid']);
|
||||
} catch (\Exception $e) {
|
||||
// Silently fail, use existing data
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$pre = Predb::getForRelease($data['predb_id']);
|
||||
|
||||
@@ -8,7 +8,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
/**
|
||||
* App\Models\AnidbInfo.
|
||||
*
|
||||
* @property-read AnidbEpisode $episode
|
||||
* @property-read AnidbTitle $title
|
||||
*
|
||||
* @mixin \Eloquent
|
||||
@@ -26,6 +25,15 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
* @property string|null $picture
|
||||
* @property string|null $categories
|
||||
* @property string|null $characters
|
||||
* @property int|null $anilist_id ID from AniList
|
||||
* @property int|null $mal_id ID from MyAnimeList
|
||||
* @property string|null $country ISO 3166-1 alpha-2 country code
|
||||
* @property string|null $media_type ANIME or MANGA
|
||||
* @property int|null $episodes Number of episodes
|
||||
* @property int|null $duration Duration in minutes
|
||||
* @property string|null $status Media status (FINISHED, RELEASING, etc.)
|
||||
* @property string|null $source Original source (MANGA, ORIGINAL, etc.)
|
||||
* @property string|null $hashtag AniList hashtag
|
||||
*
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\AnidbInfo newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\AnidbInfo newQuery()
|
||||
@@ -81,8 +89,4 @@ class AnidbInfo extends Model
|
||||
return $this->belongsTo(AnidbTitle::class, 'anidbid');
|
||||
}
|
||||
|
||||
public function episode(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(AnidbEpisode::class, 'anidbid');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
* @property string $type type of title.
|
||||
* @property string $lang
|
||||
* @property string $title
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\AnidbEpisode[] $episode
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\AnidbInfo[] $info
|
||||
*
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\AnidbTitle whereAnidbid($value)
|
||||
@@ -53,11 +52,6 @@ class AnidbTitle extends Model
|
||||
*/
|
||||
protected $guarded = [];
|
||||
|
||||
public function episode(): HasMany
|
||||
{
|
||||
return $this->hasMany(AnidbEpisode::class, 'anidbid');
|
||||
}
|
||||
|
||||
public function info(): HasMany
|
||||
{
|
||||
return $this->hasMany(AnidbInfo::class, 'anidbid');
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
// Drop anidb_episodes table (AniList doesn't support episodes)
|
||||
Schema::dropIfExists('anidb_episodes');
|
||||
|
||||
// Add anilist_id, mal_id, country, and media_type columns to anidb_info
|
||||
Schema::table('anidb_info', function (Blueprint $table) {
|
||||
if (! Schema::hasColumn('anidb_info', 'anilist_id')) {
|
||||
$table->unsignedInteger('anilist_id')->nullable()->after('anidbid');
|
||||
}
|
||||
if (! Schema::hasColumn('anidb_info', 'mal_id')) {
|
||||
$table->unsignedInteger('mal_id')->nullable()->after('anilist_id');
|
||||
}
|
||||
if (! Schema::hasColumn('anidb_info', 'country')) {
|
||||
$table->string('country', 2)->nullable()->after('mal_id')->comment('ISO 3166-1 alpha-2 country code');
|
||||
}
|
||||
if (! Schema::hasColumn('anidb_info', 'media_type')) {
|
||||
$table->string('media_type', 10)->nullable()->after('country')->comment('ANIME or MANGA');
|
||||
}
|
||||
if (! Schema::hasColumn('anidb_info', 'episodes')) {
|
||||
$table->unsignedInteger('episodes')->nullable()->after('media_type');
|
||||
}
|
||||
if (! Schema::hasColumn('anidb_info', 'duration')) {
|
||||
$table->unsignedInteger('duration')->nullable()->after('episodes')->comment('Duration in minutes');
|
||||
}
|
||||
if (! Schema::hasColumn('anidb_info', 'status')) {
|
||||
$table->string('status', 20)->nullable()->after('duration')->comment('Media status (FINISHED, RELEASING, etc.)');
|
||||
}
|
||||
if (! Schema::hasColumn('anidb_info', 'source')) {
|
||||
$table->string('source', 20)->nullable()->after('status')->comment('Original source (MANGA, ORIGINAL, etc.)');
|
||||
}
|
||||
if (! Schema::hasColumn('anidb_info', 'hashtag')) {
|
||||
$table->string('hashtag', 255)->nullable()->after('source')->comment('AniList hashtag');
|
||||
}
|
||||
});
|
||||
|
||||
// Add indexes for faster lookups
|
||||
Schema::table('anidb_info', function (Blueprint $table) {
|
||||
if (! $this->indexExists('anidb_info', 'ix_anidb_info_anilist_id')) {
|
||||
$table->index('anilist_id', 'ix_anidb_info_anilist_id');
|
||||
}
|
||||
if (! $this->indexExists('anidb_info', 'ix_anidb_info_mal_id')) {
|
||||
$table->index('mal_id', 'ix_anidb_info_mal_id');
|
||||
}
|
||||
if (! $this->indexExists('anidb_info', 'ix_anidb_info_country')) {
|
||||
$table->index('country', 'ix_anidb_info_country');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
// Remove indexes
|
||||
Schema::table('anidb_info', function (Blueprint $table) {
|
||||
if ($this->indexExists('anidb_info', 'ix_anidb_info_anilist_id')) {
|
||||
$table->dropIndex('ix_anidb_info_anilist_id');
|
||||
}
|
||||
if ($this->indexExists('anidb_info', 'ix_anidb_info_mal_id')) {
|
||||
$table->dropIndex('ix_anidb_info_mal_id');
|
||||
}
|
||||
if ($this->indexExists('anidb_info', 'ix_anidb_info_country')) {
|
||||
$table->dropIndex('ix_anidb_info_country');
|
||||
}
|
||||
});
|
||||
|
||||
// Remove columns
|
||||
Schema::table('anidb_info', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('anidb_info', 'anilist_id')) {
|
||||
$table->dropColumn('anilist_id');
|
||||
}
|
||||
if (Schema::hasColumn('anidb_info', 'mal_id')) {
|
||||
$table->dropColumn('mal_id');
|
||||
}
|
||||
if (Schema::hasColumn('anidb_info', 'country')) {
|
||||
$table->dropColumn('country');
|
||||
}
|
||||
if (Schema::hasColumn('anidb_info', 'media_type')) {
|
||||
$table->dropColumn('media_type');
|
||||
}
|
||||
if (Schema::hasColumn('anidb_info', 'episodes')) {
|
||||
$table->dropColumn('episodes');
|
||||
}
|
||||
if (Schema::hasColumn('anidb_info', 'duration')) {
|
||||
$table->dropColumn('duration');
|
||||
}
|
||||
if (Schema::hasColumn('anidb_info', 'status')) {
|
||||
$table->dropColumn('status');
|
||||
}
|
||||
if (Schema::hasColumn('anidb_info', 'source')) {
|
||||
$table->dropColumn('source');
|
||||
}
|
||||
if (Schema::hasColumn('anidb_info', 'hashtag')) {
|
||||
$table->dropColumn('hashtag');
|
||||
}
|
||||
});
|
||||
|
||||
// Recreate anidb_episodes table (if needed for rollback)
|
||||
if (! Schema::hasTable('anidb_episodes')) {
|
||||
Schema::create('anidb_episodes', function (Blueprint $table) {
|
||||
$table->unsignedInteger('anidbid')->comment('ID of title from AniDB');
|
||||
$table->unsignedInteger('episodeid')->default(0)->comment('anidb id for this episode');
|
||||
$table->unsignedSmallInteger('episode_no')->comment('Numeric version of episode (leave 0 for combined episodes).');
|
||||
$table->string('episode_title', 255)->comment('Title of the episode (en, x-jat)');
|
||||
$table->date('airdate');
|
||||
$table->primary(['anidbid', 'episodeid']);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an index exists on a table.
|
||||
*/
|
||||
private function indexExists(string $table, string $index): bool
|
||||
{
|
||||
$connection = Schema::getConnection();
|
||||
$database = $connection->getDatabaseName();
|
||||
$result = $connection->select(
|
||||
"SELECT COUNT(*) as count FROM information_schema.statistics
|
||||
WHERE table_schema = ? AND table_name = ? AND index_name = ?",
|
||||
[$database, $table, $index]
|
||||
);
|
||||
|
||||
return $result[0]->count > 0;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
DROP TABLE IF EXISTS `anidb_episodes`;
|
||||
|
||||
CREATE TABLE `anidb_episodes` (
|
||||
`anidbid` int(10) unsigned NOT NULL COMMENT 'ID of title from AniDB',
|
||||
`episodeid` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'anidb id for this episode',
|
||||
`episode_no` smallint(5) unsigned NOT NULL COMMENT 'Numeric version of episode (leave 0 for combined episodes).',
|
||||
`episode_title` varchar(255) NOT NULL COMMENT 'Title of the episode (en, x-jat)',
|
||||
`airdate` date NOT NULL,
|
||||
PRIMARY KEY (`anidbid`,`episodeid`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
|
||||
|
||||
DROP TABLE IF EXISTS `anidb_info`;
|
||||
|
||||
CREATE TABLE `anidb_info` (
|
||||
`anidbid` int(10) unsigned NOT NULL COMMENT 'ID of title from AniDB',
|
||||
`anilist_id` int(10) unsigned DEFAULT NULL COMMENT 'ID from AniList',
|
||||
`mal_id` int(10) unsigned DEFAULT NULL COMMENT 'ID from MyAnimeList',
|
||||
`country` char(2) DEFAULT NULL COMMENT 'ISO 3166-1 alpha-2 country code',
|
||||
`media_type` varchar(10) DEFAULT NULL COMMENT 'ANIME or MANGA',
|
||||
`type` varchar(32) DEFAULT NULL,
|
||||
`episodes` int(10) unsigned DEFAULT NULL,
|
||||
`duration` int(10) unsigned DEFAULT NULL COMMENT 'Duration in minutes',
|
||||
`status` varchar(20) DEFAULT NULL COMMENT 'Media status (FINISHED, RELEASING, etc.)',
|
||||
`source` varchar(20) DEFAULT NULL COMMENT 'Original source (MANGA, ORIGINAL, etc.)',
|
||||
`hashtag` varchar(255) DEFAULT NULL COMMENT 'AniList hashtag',
|
||||
`startdate` date DEFAULT NULL,
|
||||
`enddate` date DEFAULT NULL,
|
||||
`updated` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||
@@ -27,7 +26,10 @@ CREATE TABLE `anidb_info` (
|
||||
`categories` varchar(1024) DEFAULT NULL,
|
||||
`characters` varchar(1024) DEFAULT NULL,
|
||||
PRIMARY KEY (`anidbid`),
|
||||
KEY `ix_anidb_info_datetime` (`startdate`,`enddate`,`updated`)
|
||||
KEY `ix_anidb_info_datetime` (`startdate`,`enddate`,`updated`),
|
||||
KEY `ix_anidb_info_anilist_id` (`anilist_id`),
|
||||
KEY `ix_anidb_info_mal_id` (`mal_id`),
|
||||
KEY `ix_anidb_info_country` (`country`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
|
||||
|
||||
DROP TABLE IF EXISTS `anidb_titles`;
|
||||
|
||||
@@ -29,7 +29,6 @@ if (isset($argv[1]) && $argv[1] === 'all' && isset($argv[2]) && $argv[2] === 'tr
|
||||
DB::select('TRUNCATE TABLE tv_info');
|
||||
DB::select('TRUNCATE TABLE tv_episodes');
|
||||
DB::select('TRUNCATE TABLE anidb_info');
|
||||
DB::select('TRUNCATE TABLE anidb_episodes');
|
||||
}
|
||||
$colorCli->header('Resetting all postprocessing');
|
||||
$qry = DB::select('SELECT id FROM releases');
|
||||
@@ -209,7 +208,6 @@ if (isset($argv[1]) && ($argv[1] === 'anime' || $argv[1] === 'all')) {
|
||||
$ran = true;
|
||||
if (isset($argv[3]) && $argv[3] === 'truncate') {
|
||||
DB::select('TRUNCATE TABLE anidb_info');
|
||||
DB::select('TRUNCATE TABLE anidb_episodes');
|
||||
}
|
||||
if (isset($argv[2]) && $argv[2] === 'true') {
|
||||
$colorCli->header('Resetting all Anime postprocessing');
|
||||
|
||||
@@ -125,11 +125,11 @@
|
||||
</label>
|
||||
<div class="border border-gray-300 dark:border-gray-600 rounded-lg p-4 bg-gray-50 dark:bg-gray-900">
|
||||
@php
|
||||
$coverPath = public_path('covers/anime/' . $anime['anidbid'] . '.jpg');
|
||||
$coverPath = storage_path('covers/anime/' . $anime['anidbid'] . '-cover.jpg');
|
||||
$hasCover = file_exists($coverPath);
|
||||
@endphp
|
||||
@if($hasCover)
|
||||
<img src="{{ asset('covers/anime/' . $anime['anidbid'] . '.jpg') }}"
|
||||
<img src="{{ url('/covers/anime/' . $anime['anidbid'] . '-cover.jpg') }}"
|
||||
alt="{{ $anime['title'] }}"
|
||||
class="max-w-full h-auto mx-auto rounded shadow-lg"
|
||||
style="max-height: 400px;">
|
||||
@@ -137,10 +137,36 @@
|
||||
<div class="flex flex-col items-center justify-center py-12 text-gray-400 dark:text-gray-500">
|
||||
<i class="fas fa-dragon text-6xl mb-3"></i>
|
||||
<p>No cover image available</p>
|
||||
<p class="text-xs mt-2">Cover will be downloaded from AniList when data is populated</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- External Links -->
|
||||
@php
|
||||
$anilistId = $anime['anilist_id'] ?? null;
|
||||
$malId = $anime['mal_id'] ?? null;
|
||||
@endphp
|
||||
@if(!empty($anilistId) || !empty($malId))
|
||||
<div class="mt-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
External Links
|
||||
</label>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@if(!empty($anilistId))
|
||||
<a href="{{ $site['dereferrer_link'] ?? '' }}https://anilist.co/anime/{{ $anilistId }}" target="_blank" class="inline-flex items-center px-3 py-2 bg-blue-100 text-blue-800 rounded-lg hover:bg-blue-200 transition text-sm">
|
||||
<i class="fas fa-external-link-alt mr-2"></i> AniList
|
||||
</a>
|
||||
@endif
|
||||
@if(!empty($malId))
|
||||
<a href="{{ $site['dereferrer_link'] ?? '' }}https://myanimelist.net/anime/{{ $malId }}" target="_blank" class="inline-flex items-center px-3 py-2 bg-yellow-100 text-yellow-800 rounded-lg hover:bg-yellow-200 transition text-sm">
|
||||
<i class="fas fa-external-link-alt mr-2"></i> MyAnimeList
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Description -->
|
||||
<div>
|
||||
|
||||
@@ -61,11 +61,11 @@
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
@php
|
||||
$coverPath = public_path('covers/anime/' . $anime->anidbid . '.jpg');
|
||||
$coverPath = storage_path('covers/anime/' . $anime->anidbid . '-cover.jpg');
|
||||
$hasCover = file_exists($coverPath);
|
||||
@endphp
|
||||
@if($hasCover)
|
||||
<img src="{{ asset('covers/anime/' . $anime->anidbid . '.jpg') }}"
|
||||
<img src="{{ url('/covers/anime/' . $anime->anidbid . '-cover.jpg') }}"
|
||||
alt="{{ $anime->title }}"
|
||||
class="h-16 w-12 object-cover rounded shadow"
|
||||
loading="lazy">
|
||||
|
||||
@@ -454,26 +454,166 @@
|
||||
@php
|
||||
$anidbData = is_object($anidb) ? get_object_vars($anidb) : $anidb;
|
||||
$anidbTitle = $anidbData['title'] ?? ($anidb->title ?? null);
|
||||
$anidbEnglishTitle = $anidbData['english_title'] ?? ($anidb->english_title ?? null);
|
||||
$anidbOriginalTitle = $anidbData['original_title'] ?? ($anidb->original_title ?? null);
|
||||
$anidbOriginalLang = $anidbData['original_lang'] ?? ($anidb->original_lang ?? null);
|
||||
$anidbHashtag = $anidbData['hashtag'] ?? ($anidb->hashtag ?? null);
|
||||
$anidbType = $anidbData['type'] ?? ($anidb->type ?? null);
|
||||
$anidbMediaType = $anidbData['media_type'] ?? ($anidb->media_type ?? null);
|
||||
$anidbCountry = $anidbData['country'] ?? ($anidb->country ?? null);
|
||||
$anidbEpisodes = $anidbData['episodes'] ?? ($anidb->episodes ?? null);
|
||||
$anidbDuration = $anidbData['duration'] ?? ($anidb->duration ?? null);
|
||||
$anidbStatus = $anidbData['status'] ?? ($anidb->status ?? null);
|
||||
$anidbSource = $anidbData['source'] ?? ($anidb->source ?? null);
|
||||
$anidbStartDate = $anidbData['startdate'] ?? ($anidb->startdate ?? null);
|
||||
$anidbEndDate = $anidbData['enddate'] ?? ($anidb->enddate ?? null);
|
||||
$anidbRating = $anidbData['rating'] ?? ($anidb->rating ?? null);
|
||||
$anidbDescription = $anidbData['description'] ?? ($anidb->description ?? null);
|
||||
$anidbCategories = $anidbData['categories'] ?? ($anidb->categories ?? null);
|
||||
$anidbCreators = $anidbData['creators'] ?? ($anidb->creators ?? null);
|
||||
|
||||
// Get country name from country code
|
||||
$countryName = null;
|
||||
if (!empty($anidbCountry)) {
|
||||
$country = \App\Models\Country::where('iso_3166_2', $anidbCountry)->first();
|
||||
$countryName = $country->name ?? $anidbCountry;
|
||||
}
|
||||
|
||||
// Format status
|
||||
$statusLabels = [
|
||||
'FINISHED' => 'Finished',
|
||||
'RELEASING' => 'Releasing',
|
||||
'NOT_YET_RELEASED' => 'Not Yet Released',
|
||||
'CANCELLED' => 'Cancelled',
|
||||
'HIATUS' => 'Hiatus',
|
||||
];
|
||||
$anidbStatusLabel = $statusLabels[$anidbStatus] ?? $anidbStatus;
|
||||
|
||||
// Format source
|
||||
$sourceLabels = [
|
||||
'ORIGINAL' => 'Original',
|
||||
'MANGA' => 'Manga',
|
||||
'LIGHT_NOVEL' => 'Light Novel',
|
||||
'VISUAL_NOVEL' => 'Visual Novel',
|
||||
'VIDEO_GAME' => 'Video Game',
|
||||
'OTHER' => 'Other',
|
||||
'NOVEL' => 'Novel',
|
||||
'DOUJINSHI' => 'Doujinshi',
|
||||
'ANIME' => 'Anime',
|
||||
'WEB_MANGA' => 'Web Manga',
|
||||
'MUSIC' => 'Music',
|
||||
'4_KOMA_MANGA' => '4-Koma Manga',
|
||||
];
|
||||
$anidbSourceLabel = $sourceLabels[$anidbSource] ?? $anidbSource;
|
||||
@endphp
|
||||
<div class="bg-gradient-to-r from-pink-50 to-purple-50 dark:from-pink-900 dark:to-purple-900 rounded-lg p-6 border border-pink-100 dark:border-pink-800">
|
||||
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-4 flex items-center">
|
||||
<i class="fas fa-dragon mr-2 text-pink-600 dark:text-pink-400"></i> Anime Information
|
||||
<i class="fas fa-dragon mr-2 text-pink-600 dark:text-pink-400"></i>
|
||||
@if($anidbMediaType === 'MANGA')
|
||||
Manga Information
|
||||
@else
|
||||
Anime Information
|
||||
@endif
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
@if(!empty($anidbTitle))
|
||||
@if(!empty($anidbEnglishTitle) || !empty($anidbOriginalTitle) || !empty($anidbHashtag))
|
||||
<div class="md:col-span-2">
|
||||
<dt class="text-sm font-medium text-gray-600 dark:text-gray-400 mb-2">Titles</dt>
|
||||
<dd class="mt-1 space-y-2">
|
||||
@if(!empty($anidbEnglishTitle))
|
||||
<div class="text-sm text-gray-900 dark:text-gray-100 font-semibold">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400 font-normal mr-2">English:</span>
|
||||
{{ $anidbEnglishTitle }}
|
||||
</div>
|
||||
@endif
|
||||
@if(!empty($anidbOriginalTitle))
|
||||
<div class="text-sm text-gray-900 dark:text-gray-100 font-semibold">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400 font-normal mr-2">
|
||||
@if($anidbOriginalLang === 'ja')
|
||||
Native:
|
||||
@elseif($anidbOriginalLang === 'x-jat')
|
||||
Romaji:
|
||||
@else
|
||||
Original:
|
||||
@endif
|
||||
</span>
|
||||
{{ $anidbOriginalTitle }}
|
||||
</div>
|
||||
@endif
|
||||
@if(!empty($anidbHashtag))
|
||||
<div class="text-sm text-gray-900 dark:text-gray-100">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400 font-normal mr-2">Hashtag:</span>
|
||||
<span class="font-mono text-blue-600 dark:text-blue-400">{{ $anidbHashtag }}</span>
|
||||
</div>
|
||||
@endif
|
||||
@if(empty($anidbEnglishTitle) && empty($anidbOriginalTitle) && !empty($anidbTitle))
|
||||
<div class="text-sm text-gray-900 dark:text-gray-100 font-semibold">
|
||||
{{ $anidbTitle }}
|
||||
</div>
|
||||
@endif
|
||||
</dd>
|
||||
</div>
|
||||
@elseif(!empty($anidbTitle))
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-600 dark:text-gray-400">Title</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100 font-semibold">{{ $anidbTitle }}</dd>
|
||||
</div>
|
||||
@endif
|
||||
@if(!empty($anidbMediaType))
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-600 dark:text-gray-400">Media Type</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">
|
||||
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium
|
||||
{{ $anidbMediaType === 'ANIME' ? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200' : 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200' }}">
|
||||
{{ $anidbMediaType === 'ANIME' ? 'Anime' : 'Manga' }}
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
@endif
|
||||
@if(!empty($anidbType))
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-600 dark:text-gray-400">Type</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ $anidbType }}</dd>
|
||||
<dt class="text-sm font-medium text-gray-600 dark:text-gray-400">Format</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ ucfirst(str_replace('_', ' ', strtolower($anidbType))) }}</dd>
|
||||
</div>
|
||||
@endif
|
||||
@if(!empty($countryName))
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-600 dark:text-gray-400">Country</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">
|
||||
<span class="inline-flex items-center">
|
||||
@if(!empty($country))
|
||||
<img src="{{ asset('assets/images/flags/' . strtolower($anidbCountry) . '.png') }}"
|
||||
alt="{{ $countryName }}"
|
||||
class="w-4 h-3 mr-1"
|
||||
onerror="this.style.display='none'">
|
||||
@endif
|
||||
{{ $countryName }}
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
@endif
|
||||
@if(!empty($anidbStatus))
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-600 dark:text-gray-400">Status</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ $anidbStatusLabel }}</dd>
|
||||
</div>
|
||||
@endif
|
||||
@if(!empty($anidbSource))
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-600 dark:text-gray-400">Source</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ $anidbSourceLabel }}</dd>
|
||||
</div>
|
||||
@endif
|
||||
@if(!empty($anidbEpisodes))
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-600 dark:text-gray-400">Episodes</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ number_format($anidbEpisodes) }}</dd>
|
||||
</div>
|
||||
@endif
|
||||
@if(!empty($anidbDuration))
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-600 dark:text-gray-400">Duration</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ $anidbDuration }} minutes</dd>
|
||||
</div>
|
||||
@endif
|
||||
@if(!empty($anidbStartDate))
|
||||
@@ -482,17 +622,40 @@
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ $anidbStartDate }}</dd>
|
||||
</div>
|
||||
@endif
|
||||
@if(!empty($anidbEndDate))
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-600 dark:text-gray-400">End Date</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ $anidbEndDate }}</dd>
|
||||
</div>
|
||||
@endif
|
||||
@if(!empty($anidbRating))
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-600 dark:text-gray-400">Rating</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">
|
||||
<span class="inline-flex items-center">
|
||||
<i class="fas fa-star text-yellow-500 dark:text-yellow-400 mr-1"></i>
|
||||
{{ $anidbRating }}
|
||||
@php
|
||||
$ratingValue = is_numeric($anidbRating) ? (float)$anidbRating : 0;
|
||||
// AniList rating is out of 100, convert to /10
|
||||
$displayRating = $ratingValue > 10 ? number_format($ratingValue / 10, 1) : number_format($ratingValue, 1);
|
||||
@endphp
|
||||
{{ $displayRating }} / 10
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
@endif
|
||||
@if(!empty($anidbCategories))
|
||||
<div class="md:col-span-2">
|
||||
<dt class="text-sm font-medium text-gray-600 dark:text-gray-400">Genres</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ $anidbCategories }}</dd>
|
||||
</div>
|
||||
@endif
|
||||
@if(!empty($anidbCreators))
|
||||
<div class="md:col-span-2">
|
||||
<dt class="text-sm font-medium text-gray-600 dark:text-gray-400">Studios</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ $anidbCreators }}</dd>
|
||||
</div>
|
||||
@endif
|
||||
@if(!empty($anidbDescription))
|
||||
<div class="md:col-span-2">
|
||||
<dt class="text-sm font-medium text-gray-600 dark:text-gray-400">Description</dt>
|
||||
@@ -500,6 +663,29 @@
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- External Links -->
|
||||
@php
|
||||
$anilistId = $anidbData['anilist_id'] ?? ($anidb->anilist_id ?? null);
|
||||
$malId = $anidbData['mal_id'] ?? ($anidb->mal_id ?? null);
|
||||
@endphp
|
||||
@if(!empty($anilistId) || !empty($malId))
|
||||
<div class="mt-4 pt-4 border-t border-pink-200 dark:border-pink-700">
|
||||
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">External Links</h4>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
@if(!empty($anilistId))
|
||||
<a href="{{ $site['dereferrer_link'] ?? '' }}https://anilist.co/anime/{{ $anilistId }}" target="_blank" class="inline-flex items-center px-4 py-2 bg-blue-100 text-blue-800 rounded-lg hover:bg-blue-200 transition">
|
||||
<i class="fas fa-external-link-alt mr-2"></i> View on AniList
|
||||
</a>
|
||||
@endif
|
||||
@if(!empty($malId))
|
||||
<a href="{{ $site['dereferrer_link'] ?? '' }}https://myanimelist.net/anime/{{ $malId }}" target="_blank" class="inline-flex items-center px-4 py-2 bg-yellow-100 text-yellow-800 rounded-lg hover:bg-yellow-200 transition">
|
||||
<i class="fas fa-external-link-alt mr-2"></i> View on MyAnimeList
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
|
||||
Reference in New Issue
Block a user