mirror of
https://github.com/NNTmux/newznab-tmux.git
synced 2026-08-29 01:08:56 +00:00
351 lines
10 KiB
PHP
351 lines
10 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use GuzzleHttp\Client;
|
|
use GuzzleHttp\Exception\GuzzleException;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
/**
|
|
* Custom iTunes Search API Service.
|
|
* Replaces the DariusIII/php-itunes-api package with a direct implementation.
|
|
* Supports music and ebook searches only (movies are not supported due to API issues).
|
|
*
|
|
* @see https://developer.apple.com/library/archive/documentation/AudioVideo/Conceptual/iTuneSearchAPI/index.html
|
|
*/
|
|
class ItunesService
|
|
{
|
|
protected const API_URL = 'https://itunes.apple.com/search';
|
|
|
|
protected const LOOKUP_URL = 'https://itunes.apple.com/lookup';
|
|
|
|
protected Client $client;
|
|
|
|
protected int $limit = 25;
|
|
|
|
protected string $country = 'US';
|
|
|
|
public function __construct()
|
|
{
|
|
$this->client = new Client([
|
|
'timeout' => 15,
|
|
'connect_timeout' => 10,
|
|
'http_errors' => false,
|
|
'headers' => [
|
|
'Accept' => 'application/json',
|
|
'User-Agent' => 'NNTmux/1.0',
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Set the result limit.
|
|
*/
|
|
public function limit(int $limit): self
|
|
{
|
|
$this->limit = min($limit, 200); // iTunes max is 200
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Set the country code for the search.
|
|
*/
|
|
public function country(string $country): self
|
|
{
|
|
$this->country = strtoupper($country);
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Search for music albums.
|
|
*/
|
|
public function searchAlbums(string $term): ?array
|
|
{
|
|
return $this->search($term, 'album', 'music');
|
|
}
|
|
|
|
/**
|
|
* Search for music tracks.
|
|
*/
|
|
public function searchTracks(string $term): ?array
|
|
{
|
|
return $this->search($term, 'song', 'music');
|
|
}
|
|
|
|
/**
|
|
* Search for artists.
|
|
*/
|
|
public function searchArtists(string $term): ?array
|
|
{
|
|
return $this->search($term, 'musicArtist', 'music');
|
|
}
|
|
|
|
/**
|
|
* Search for ebooks.
|
|
*/
|
|
public function searchEbooks(string $term): ?array
|
|
{
|
|
return $this->search($term, 'ebook', 'ebook');
|
|
}
|
|
|
|
/**
|
|
* Search for audiobooks.
|
|
*/
|
|
public function searchAudiobooks(string $term): ?array
|
|
{
|
|
return $this->search($term, 'audiobook', 'audiobook');
|
|
}
|
|
|
|
/**
|
|
* Lookup by iTunes ID.
|
|
*/
|
|
public function lookupById(int $id): ?array
|
|
{
|
|
$cacheKey = 'itunes_lookup_'.$id;
|
|
$cached = Cache::get($cacheKey);
|
|
|
|
if ($cached !== null) {
|
|
return $cached;
|
|
}
|
|
|
|
try {
|
|
$response = $this->client->get(self::LOOKUP_URL, [
|
|
'query' => [
|
|
'id' => $id,
|
|
'country' => $this->country,
|
|
],
|
|
]);
|
|
|
|
$statusCode = $response->getStatusCode();
|
|
if ($statusCode !== 200) {
|
|
Log::warning("iTunes API lookup returned status {$statusCode}");
|
|
|
|
return null;
|
|
}
|
|
|
|
$data = json_decode($response->getBody()->getContents(), true);
|
|
|
|
if (! isset($data['results']) || empty($data['results'])) {
|
|
return null;
|
|
}
|
|
|
|
Cache::put($cacheKey, $data['results'][0], now()->addDays(7));
|
|
|
|
return $data['results'][0];
|
|
} catch (GuzzleException $e) {
|
|
Log::error('iTunes API lookup error: '.$e->getMessage());
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Lookup artist by ID.
|
|
*/
|
|
public function lookupArtist(int $artistId): ?array
|
|
{
|
|
return $this->lookupById($artistId);
|
|
}
|
|
|
|
/**
|
|
* Get the first album result for a search term.
|
|
*
|
|
* @return array|null Normalized album data or null
|
|
*/
|
|
public function findAlbum(string $term): ?array
|
|
{
|
|
$results = $this->searchAlbums($term);
|
|
|
|
if (empty($results)) {
|
|
return null;
|
|
}
|
|
|
|
return $this->normalizeAlbumResult($results[0]);
|
|
}
|
|
|
|
/**
|
|
* Get the first track result for a search term.
|
|
*
|
|
* @return array|null Normalized track data or null
|
|
*/
|
|
public function findTrack(string $term): ?array
|
|
{
|
|
$results = $this->searchTracks($term);
|
|
|
|
if (empty($results)) {
|
|
return null;
|
|
}
|
|
|
|
return $this->normalizeTrackResult($results[0]);
|
|
}
|
|
|
|
/**
|
|
* Get the first ebook result for a search term.
|
|
*
|
|
* @return array|null Normalized ebook data or null
|
|
*/
|
|
public function findEbook(string $term): ?array
|
|
{
|
|
$results = $this->searchEbooks($term);
|
|
|
|
if (empty($results)) {
|
|
return null;
|
|
}
|
|
|
|
return $this->normalizeEbookResult($results[0]);
|
|
}
|
|
|
|
/**
|
|
* Perform a search request to iTunes API.
|
|
*/
|
|
protected function search(string $term, string $entity, string $media): ?array
|
|
{
|
|
$term = trim($term);
|
|
if (empty($term)) {
|
|
return null;
|
|
}
|
|
|
|
$cacheKey = 'itunes_search_'.md5($term.$entity.$media.$this->country.$this->limit);
|
|
$cached = Cache::get($cacheKey);
|
|
|
|
if ($cached !== null) {
|
|
return $cached;
|
|
}
|
|
|
|
try {
|
|
$response = $this->client->get(self::API_URL, [
|
|
'query' => [
|
|
'term' => $term,
|
|
'entity' => $entity,
|
|
'media' => $media,
|
|
'country' => $this->country,
|
|
'limit' => $this->limit,
|
|
],
|
|
]);
|
|
|
|
$statusCode = $response->getStatusCode();
|
|
if ($statusCode !== 200) {
|
|
Log::warning("iTunes API search returned status {$statusCode}");
|
|
|
|
return null;
|
|
}
|
|
|
|
$data = json_decode($response->getBody()->getContents(), true);
|
|
|
|
if (! isset($data['results'])) {
|
|
return null;
|
|
}
|
|
|
|
$results = $data['results'];
|
|
Cache::put($cacheKey, $results, now()->addHours(6));
|
|
|
|
return $results;
|
|
} catch (GuzzleException $e) {
|
|
Log::error('iTunes API search error: '.$e->getMessage());
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Normalize album result to consistent format.
|
|
*/
|
|
protected function normalizeAlbumResult(array $result): array
|
|
{
|
|
return [
|
|
'id' => $result['collectionId'] ?? null,
|
|
'name' => $result['collectionName'] ?? '',
|
|
'artist' => $result['artistName'] ?? '',
|
|
'artist_id' => $result['artistId'] ?? null,
|
|
'genre' => $result['primaryGenreName'] ?? '',
|
|
'release_date' => isset($result['releaseDate']) ? $this->parseDate($result['releaseDate']) : null,
|
|
'track_count' => $result['trackCount'] ?? 0,
|
|
'cover' => $this->getHighResCover($result['artworkUrl100'] ?? ''),
|
|
'store_url' => $result['collectionViewUrl'] ?? '',
|
|
'price' => $result['collectionPrice'] ?? null,
|
|
'currency' => $result['currency'] ?? 'USD',
|
|
'copyright' => $result['copyright'] ?? '',
|
|
'explicit' => ($result['collectionExplicitness'] ?? '') === 'explicit',
|
|
'raw' => $result,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Normalize track result to consistent format.
|
|
*/
|
|
protected function normalizeTrackResult(array $result): array
|
|
{
|
|
return [
|
|
'id' => $result['trackId'] ?? null,
|
|
'name' => $result['trackName'] ?? '',
|
|
'artist' => $result['artistName'] ?? '',
|
|
'artist_id' => $result['artistId'] ?? null,
|
|
'album' => $result['collectionName'] ?? '',
|
|
'album_id' => $result['collectionId'] ?? null,
|
|
'genre' => $result['primaryGenreName'] ?? '',
|
|
'release_date' => isset($result['releaseDate']) ? $this->parseDate($result['releaseDate']) : null,
|
|
'duration_ms' => $result['trackTimeMillis'] ?? 0,
|
|
'cover' => $this->getHighResCover($result['artworkUrl100'] ?? ''),
|
|
'preview_url' => $result['previewUrl'] ?? '',
|
|
'store_url' => $result['trackViewUrl'] ?? '',
|
|
'price' => $result['trackPrice'] ?? null,
|
|
'currency' => $result['currency'] ?? 'USD',
|
|
'explicit' => ($result['trackExplicitness'] ?? '') === 'explicit',
|
|
'raw' => $result,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Normalize ebook result to consistent format.
|
|
*/
|
|
protected function normalizeEbookResult(array $result): array
|
|
{
|
|
return [
|
|
'id' => $result['trackId'] ?? null,
|
|
'name' => $result['trackName'] ?? '',
|
|
'author' => $result['artistName'] ?? '',
|
|
'author_id' => $result['artistId'] ?? null,
|
|
'genres' => $result['genres'] ?? [],
|
|
'genre' => $result['primaryGenreName'] ?? ($result['genres'][0] ?? ''),
|
|
'release_date' => isset($result['releaseDate']) ? $this->parseDate($result['releaseDate']) : null,
|
|
'description' => $result['description'] ?? '',
|
|
'cover' => $this->getHighResCover($result['artworkUrl100'] ?? ''),
|
|
'store_url' => $result['trackViewUrl'] ?? '',
|
|
'price' => $result['price'] ?? null,
|
|
'currency' => $result['currency'] ?? 'USD',
|
|
'average_rating' => $result['averageUserRating'] ?? null,
|
|
'rating_count' => $result['userRatingCount'] ?? 0,
|
|
'raw' => $result,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Convert iTunes 100x100 artwork URL to higher resolution.
|
|
*/
|
|
protected function getHighResCover(string $url, int $size = 800): string
|
|
{
|
|
if (empty($url)) {
|
|
return '';
|
|
}
|
|
|
|
// iTunes artwork URLs typically end with /100x100bb.jpg
|
|
// We can replace it with larger sizes up to 3000x3000
|
|
return preg_replace('/\d+x\d+(bb)?\./', $size.'x'.$size.'bb.', $url) ?? $url;
|
|
}
|
|
|
|
/**
|
|
* Parse ISO 8601 date string.
|
|
*/
|
|
protected function parseDate(string $dateString): ?string
|
|
{
|
|
try {
|
|
return \Carbon\Carbon::parse($dateString)->format('Y-m-d');
|
|
} catch (\Exception $e) {
|
|
return null;
|
|
}
|
|
}
|
|
}
|