mirror of
https://github.com/RSS-Bridge/rss-bridge.git
synced 2024-11-23 07:37:35 +00:00
371 lines
12 KiB
PHP
371 lines
12 KiB
PHP
<?php
|
|
|
|
class SpotifyBridge extends BridgeAbstract
|
|
{
|
|
const NAME = 'Spotify';
|
|
const URI = 'https://spotify.com/';
|
|
const DESCRIPTION = 'Fetches the latest items from one or more artists, playlists or podcasts';
|
|
const MAINTAINER = 'Paroleen';
|
|
const CACHE_TIMEOUT = 3600;
|
|
const PARAMETERS = [
|
|
'By Spotify URIs' => [
|
|
'clientid' => [
|
|
'name' => 'Client ID',
|
|
'type' => 'text',
|
|
'required' => true
|
|
],
|
|
'clientsecret' => [
|
|
'name' => 'Client secret',
|
|
'type' => 'text',
|
|
'required' => true
|
|
],
|
|
'country' => [
|
|
'name' => 'Country/Market',
|
|
'type' => 'text',
|
|
'required' => false,
|
|
'exampleValue' => 'US',
|
|
'defaultValue' => 'US'
|
|
],
|
|
'limit' => [
|
|
'name' => 'Limit',
|
|
'type' => 'number',
|
|
'required' => false,
|
|
'exampleValue' => 10,
|
|
'defaultValue' => 10
|
|
],
|
|
'spotifyuri' => [
|
|
'name' => 'Spotify URIs',
|
|
'type' => 'text',
|
|
'required' => true,
|
|
|
|
// spotify:playlist:37i9dQZF1DXcBWIGoYBM5M
|
|
// spotify:show:6ShFMYxeDNMo15COLObDvC
|
|
'exampleValue' => 'spotify:artist:4lianjyuR1tqf6oUX8kjrZ',
|
|
],
|
|
'albumtype' => [
|
|
'name' => 'Album type',
|
|
'type' => 'text',
|
|
'required' => false,
|
|
'exampleValue' => 'album,single,appears_on,compilation',
|
|
'defaultValue' => 'album,single'
|
|
]
|
|
],
|
|
'By Spotify Search' => [
|
|
'clientid' => [
|
|
'name' => 'Client ID',
|
|
'type' => 'text',
|
|
'required' => true
|
|
],
|
|
'clientsecret' => [
|
|
'name' => 'Client secret',
|
|
'type' => 'text',
|
|
'required' => true
|
|
],
|
|
'market' => [
|
|
'name' => 'Market',
|
|
'type' => 'text',
|
|
'required' => false,
|
|
'exampleValue' => 'US',
|
|
'defaultValue' => 'US'
|
|
],
|
|
'limit' => [
|
|
'name' => 'Limit',
|
|
'type' => 'number',
|
|
'required' => false,
|
|
'exampleValue' => 10,
|
|
'defaultValue' => 10
|
|
],
|
|
'query' => [
|
|
'name' => 'Search query',
|
|
'type' => 'text',
|
|
'required' => true,
|
|
'exampleValue' => 'artist:The Beatles',
|
|
],
|
|
'type' => [
|
|
'name' => 'Type',
|
|
'type' => 'text',
|
|
'required' => true,
|
|
'exampleValue' => 'album,episode',
|
|
'defaultValue' => 'album,episode'
|
|
]
|
|
],
|
|
];
|
|
|
|
private $uri = '';
|
|
private $name = '';
|
|
private $token = '';
|
|
|
|
public function collectData()
|
|
{
|
|
/**
|
|
* https://developer.spotify.com/documentation/web-api/concepts/rate-limits
|
|
*/
|
|
$cacheKey = 'spotify_rate_limit';
|
|
|
|
try {
|
|
$this->collectDataInternal();
|
|
} catch (HttpException $e) {
|
|
if ($e->getCode() === 429) {
|
|
$retryAfter = $e->response->getHeader('Retry-After') ?? (60 * 5);
|
|
$this->cache->set($cacheKey, true, $retryAfter);
|
|
throw new RateLimitException(sprintf('Rate limited by spotify, try again in %s seconds', $retryAfter));
|
|
}
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
private function collectDataInternal()
|
|
{
|
|
$this->fetchAccessToken();
|
|
|
|
if ($this->queriedContext === 'By Spotify URIs') {
|
|
$entries = $this->getEntriesFromURIs();
|
|
} else {
|
|
$entries = $this->getEntriesFromQuery();
|
|
}
|
|
|
|
usort($entries, function ($entry1, $entry2) {
|
|
return $this->getDate($entry2) <=> $this->getDate($entry1);
|
|
});
|
|
|
|
foreach ($entries as $entry) {
|
|
if (! isset($entry['type'])) {
|
|
$item = $this->getTrackData($entry);
|
|
} elseif ($entry['type'] === 'album') {
|
|
$item = $this->getAlbumData($entry);
|
|
} elseif ($entry['type'] === 'episode') {
|
|
$item = $this->getEpisodeData($entry);
|
|
} else {
|
|
throw new \Exception('Spotify URI not supported');
|
|
}
|
|
|
|
$this->items[] = $item;
|
|
|
|
if ($this->getInput('limit') > 0 && count($this->items) >= $this->getInput('limit')) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private function fetchAccessToken()
|
|
{
|
|
$cacheKey = sprintf('SpotifyBridge:%s:%s', $this->getInput('clientid'), $this->getInput('clientsecret'));
|
|
|
|
$token = $this->cache->get($cacheKey);
|
|
if ($token) {
|
|
$this->token = $token;
|
|
} else {
|
|
$basicAuth = base64_encode(sprintf('%s:%s', $this->getInput('clientid'), $this->getInput('clientsecret')));
|
|
$json = getContents('https://accounts.spotify.com/api/token', [
|
|
"Authorization: Basic $basicAuth",
|
|
], [
|
|
CURLOPT_POSTFIELDS => 'grant_type=client_credentials',
|
|
]);
|
|
$data = Json::decode($json);
|
|
$this->token = $data['access_token'];
|
|
|
|
$this->cache->set($cacheKey, $this->token, 3600);
|
|
}
|
|
}
|
|
|
|
private function getEntriesFromQuery()
|
|
{
|
|
$entries = [];
|
|
|
|
$types = [
|
|
'albums',
|
|
'episodes',
|
|
];
|
|
|
|
$query = [
|
|
'q' => $this->getInput('query'),
|
|
'type' => $this->getInput('type'),
|
|
'market' => $this->getInput('market'),
|
|
'limit' => 50,
|
|
];
|
|
|
|
$hasItems = true;
|
|
$offset = 0;
|
|
|
|
while ($hasItems && $offset < 1000) {
|
|
$hasItems = false;
|
|
|
|
$query['offset'] = $offset;
|
|
$json = getContents('https://api.spotify.com/v1/search?' . http_build_query($query), ['Authorization: Bearer ' . $this->token]);
|
|
$partial = Json::decode($json);
|
|
|
|
foreach ($types as $type) {
|
|
if (isset($partial[$type]['items'])) {
|
|
$entries = array_merge($entries, $partial[$type]['items']);
|
|
$hasItems = true;
|
|
}
|
|
}
|
|
|
|
$offset += 50;
|
|
}
|
|
|
|
return $entries;
|
|
}
|
|
|
|
private function getEntriesFromURIs()
|
|
{
|
|
$entries = [];
|
|
$uris = explode(',', $this->getInput('spotifyuri'));
|
|
|
|
foreach ($uris as $uri) {
|
|
$type = explode(':', $uri)[1];
|
|
$spotifyId = explode(':', $uri)[2];
|
|
|
|
$types = [
|
|
'artist' => 'album',
|
|
'playlist' => 'track',
|
|
'show' => 'episode',
|
|
];
|
|
if (!isset($types[$type])) {
|
|
throw new \Exception(sprintf('Unsupported Spotify URI: %s', $uri));
|
|
}
|
|
$entry_type = $types[$type];
|
|
|
|
$url = 'https://api.spotify.com/v1/' . $type . 's/' . $spotifyId . '/' . $entry_type . 's';
|
|
$query = [
|
|
'limit' => 50,
|
|
];
|
|
|
|
if ($type === 'artist') {
|
|
$query['country'] = $this->getInput('country');
|
|
$query['include_groups'] = $this->getInput('albumtype');
|
|
} else {
|
|
$query['market'] = $this->getInput('country');
|
|
}
|
|
|
|
$offset = 0;
|
|
while (true) {
|
|
$query['offset'] = $offset;
|
|
$json = getContents($url . '?' . http_build_query($query), ['Authorization: Bearer ' . $this->token]);
|
|
$partial = Json::decode($json);
|
|
if (empty($partial['items'])) {
|
|
break;
|
|
}
|
|
$entries = array_merge($entries, $partial['items']);
|
|
$offset += 50;
|
|
}
|
|
}
|
|
return $entries;
|
|
}
|
|
|
|
private function getAlbumData($album)
|
|
{
|
|
$item = [];
|
|
$item['title'] = $album['name'];
|
|
$item['uri'] = $album['external_urls']['spotify'];
|
|
$item['timestamp'] = $this->getDate($album);
|
|
$item['author'] = $album['artists'][0]['name'];
|
|
$item['categories'] = [$album['album_type']];
|
|
$item['content'] = '<img style="width: 256px" src="' . $album['images'][0]['url'] . '">';
|
|
if ($album['total_tracks'] > 1) {
|
|
$item['content'] .= '<p>Total tracks: ' . $album['total_tracks'] . '</p>';
|
|
}
|
|
return $item;
|
|
}
|
|
|
|
private function getTrackData($track)
|
|
{
|
|
$item = [];
|
|
$item['title'] = $track['track']['name'];
|
|
$item['uri'] = $track['track']['external_urls']['spotify'];
|
|
$item['timestamp'] = $this->getDate($track);
|
|
$item['author'] = $track['track']['artists'][0]['name'];
|
|
$item['categories'] = ['track'];
|
|
$item['content'] = '<img style="width: 256px" src="' . $track['track']['album']['images'][0]['url'] . '">';
|
|
return $item;
|
|
}
|
|
|
|
private function getEpisodeData($episode)
|
|
{
|
|
$item = [];
|
|
$item['title'] = $episode['name'];
|
|
$item['uri'] = $episode['external_urls']['spotify'];
|
|
$item['timestamp'] = $this->getDate($episode);
|
|
$item['content'] = '<img style="width: 256px" src="' . $episode['images'][0]['url'] . '">';
|
|
if (isset($episode['description'])) {
|
|
$item['content'] = $item['content'] . '<p>' . $episode['description'] . '</p>';
|
|
}
|
|
if (isset($episode['audio_preview_url'])) {
|
|
$item['content'] = $item['content'] . '<audio controls src="' . $episode['audio_preview_url'] . '"></audio>';
|
|
}
|
|
return $item;
|
|
}
|
|
|
|
private function getDate($entry)
|
|
{
|
|
if (isset($entry['type'])) {
|
|
$type = 'release_date';
|
|
} else {
|
|
$type = 'added_at';
|
|
}
|
|
|
|
$date = $entry[$type];
|
|
|
|
if (strlen($date) == 4) {
|
|
$date .= '-01-01';
|
|
} elseif (strlen($date) == 7) {
|
|
$date .= '-01';
|
|
}
|
|
|
|
if (strlen($date) > 10) {
|
|
return DateTime::createFromFormat('Y-m-d\TH:i:s\Z', $date)->getTimestamp();
|
|
}
|
|
|
|
return DateTime::createFromFormat('Y-m-d', $date)->getTimestamp();
|
|
}
|
|
|
|
public function getURI()
|
|
{
|
|
if (empty($this->uri)) {
|
|
$this->getFirstEntry();
|
|
}
|
|
|
|
return $this->uri;
|
|
}
|
|
|
|
public function getName()
|
|
{
|
|
if (empty($this->name)) {
|
|
$this->getFirstEntry();
|
|
}
|
|
|
|
return $this->name;
|
|
}
|
|
|
|
private function getFirstEntry()
|
|
{
|
|
$spotifyUri = $this->getInput('spotifyuri');
|
|
|
|
if (!is_null($spotifyUri) && strpos($spotifyUri, ',') === false) {
|
|
$uris = explode(',', $spotifyUri);
|
|
$firstUri = $uris[0];
|
|
$type = explode(':', $firstUri)[1];
|
|
$spotifyId = explode(':', $firstUri)[2];
|
|
|
|
$uri = 'https://api.spotify.com/v1/' . $type . 's/' . $spotifyId;
|
|
$query = [];
|
|
if ($type === 'show') {
|
|
$query['market'] = $this->getInput('country');
|
|
}
|
|
|
|
$json = getContents($uri . '?' . http_build_query($query), ['Authorization: Bearer ' . $this->token]);
|
|
$item = Json::decode($json);
|
|
|
|
$this->uri = $item['external_urls']['spotify'];
|
|
$this->name = $item['name'] . ' - Spotify';
|
|
} else {
|
|
$this->uri = parent::getURI();
|
|
$this->name = parent::getName();
|
|
}
|
|
}
|
|
|
|
public function getIcon()
|
|
{
|
|
return 'https://www.scdn.co/i/_global/favicon.png';
|
|
}
|
|
} |