204 lines
5.4 KiB
PHP
204 lines
5.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Service;
|
|
|
|
use App\Entity\Interfaces\SongInterface;
|
|
use App\Entity\StationMedia;
|
|
use App\Lock\LockFactory;
|
|
use App\Version;
|
|
use GuzzleHttp\Client;
|
|
use GuzzleHttp\Psr7\UriResolver;
|
|
use GuzzleHttp\Psr7\Utils;
|
|
use GuzzleHttp\RequestOptions;
|
|
use JsonException;
|
|
use Psr\Http\Message\UriInterface;
|
|
use RuntimeException;
|
|
use Symfony\Component\Lock\Exception\LockConflictedException;
|
|
|
|
final class MusicBrainz
|
|
{
|
|
public const API_BASE_URL = 'https://musicbrainz.org/ws/2/';
|
|
|
|
public const COVER_ART_ARCHIVE_BASE_URL = 'https://coverartarchive.org/';
|
|
|
|
public function __construct(
|
|
private readonly Client $httpClient,
|
|
private readonly LockFactory $lockFactory
|
|
) {
|
|
}
|
|
|
|
/**
|
|
* @param string|UriInterface $uri
|
|
* @param mixed[] $query Query string parameters to supplement defaults.
|
|
*
|
|
* @return mixed[] The decoded JSON response.
|
|
*/
|
|
public function makeRequest(
|
|
UriInterface|string $uri,
|
|
array $query = []
|
|
): array {
|
|
$rateLimitLock = $this->lockFactory->createLock(
|
|
'api_musicbrainz',
|
|
1,
|
|
false
|
|
);
|
|
|
|
try {
|
|
$rateLimitLock->acquire(true);
|
|
} catch (LockConflictedException) {
|
|
throw new RuntimeException('Could not acquire rate limiting lock.');
|
|
}
|
|
|
|
$query['fmt'] = 'json';
|
|
|
|
$uri = UriResolver::resolve(
|
|
Utils::uriFor(self::API_BASE_URL),
|
|
Utils::uriFor($uri)
|
|
);
|
|
|
|
$response = $this->httpClient->request(
|
|
'GET',
|
|
$uri,
|
|
[
|
|
RequestOptions::TIMEOUT => 7,
|
|
RequestOptions::HTTP_ERRORS => true,
|
|
RequestOptions::HEADERS => [
|
|
'User-Agent' => 'AzuraCast ' . Version::STABLE_VERSION,
|
|
'Accept' => 'application/json',
|
|
],
|
|
RequestOptions::QUERY => $query,
|
|
]
|
|
);
|
|
|
|
$responseBody = (string)$response->getBody();
|
|
return json_decode($responseBody, true, 512, JSON_THROW_ON_ERROR);
|
|
}
|
|
|
|
/**
|
|
* @return mixed[]
|
|
*/
|
|
public function findRecordingsForSong(
|
|
SongInterface $song,
|
|
string $include = 'releases'
|
|
): array {
|
|
$query = [];
|
|
|
|
if (!empty($song->getTitle())) {
|
|
$query[] = $this->quoteQuery($song->getTitle());
|
|
}
|
|
|
|
if (!empty($song->getArtist())) {
|
|
$query[] = 'artist:' . $this->quoteQuery($song->getArtist());
|
|
}
|
|
|
|
if ($song instanceof StationMedia) {
|
|
$advancedQuery = $query;
|
|
|
|
if (!empty($song->getAlbum())) {
|
|
$advancedQuery[] = 'release:' . $this->quoteQuery($song->getAlbum());
|
|
}
|
|
if (!empty($song->getIsrc())) {
|
|
$advancedQuery[] = 'isrc:' . $this->quoteQuery($song->getIsrc());
|
|
}
|
|
|
|
if (count($advancedQuery) > count($query)) {
|
|
$response = $this->makeRequest(
|
|
'recording/',
|
|
[
|
|
'query' => implode(' AND ', $advancedQuery),
|
|
'inc' => $include,
|
|
'limit' => 5,
|
|
]
|
|
);
|
|
|
|
if (!empty($response['recordings'])) {
|
|
return $response['recordings'];
|
|
}
|
|
}
|
|
}
|
|
|
|
if (empty($query)) {
|
|
return [];
|
|
}
|
|
|
|
$response = $this->makeRequest(
|
|
'recording/',
|
|
[
|
|
'query' => implode(' AND ', $query),
|
|
'inc' => $include,
|
|
'limit' => 5,
|
|
]
|
|
);
|
|
|
|
return $response['recordings'];
|
|
}
|
|
|
|
private function quoteQuery(string $query): string
|
|
{
|
|
return '"' . str_replace('"', '\'', $query) . '"';
|
|
}
|
|
|
|
public function getCoverArt(
|
|
string $recordType,
|
|
string $mbid
|
|
): ?string {
|
|
$uri = '/' . $recordType . '/' . $mbid;
|
|
|
|
$uri = UriResolver::resolve(
|
|
Utils::uriFor(self::COVER_ART_ARCHIVE_BASE_URL),
|
|
Utils::uriFor($uri)
|
|
);
|
|
|
|
$response = $this->httpClient->request(
|
|
'GET',
|
|
$uri,
|
|
[
|
|
RequestOptions::ALLOW_REDIRECTS => true,
|
|
RequestOptions::HEADERS => [
|
|
'User-Agent' => 'AzuraCast ' . Version::STABLE_VERSION,
|
|
'Accept' => 'application/json',
|
|
],
|
|
]
|
|
);
|
|
|
|
if (200 !== $response->getStatusCode()) {
|
|
return null;
|
|
}
|
|
|
|
$responseBody = (string)$response->getBody();
|
|
if (empty($responseBody)) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
$jsonBody = json_decode($responseBody, true, 512, JSON_THROW_ON_ERROR);
|
|
} catch (JsonException) {
|
|
return null;
|
|
}
|
|
|
|
if (empty($jsonBody['images'])) {
|
|
return null; // API returned no "images".
|
|
}
|
|
|
|
foreach ($jsonBody['images'] as $image) {
|
|
if (!$image['front']) {
|
|
continue;
|
|
}
|
|
|
|
$imageUrl = $image['thumbnails'][1200]
|
|
?? $image['thumbnails']['large']
|
|
?? $image['image']
|
|
?? null;
|
|
|
|
if (!empty($imageUrl)) {
|
|
$imageUri = Utils::uriFor($imageUrl);
|
|
return (string)($imageUri->withScheme('https'));
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|