fc5a1526ca
* [BandcampBridge] Add band and artist feeds This can return a limited number of the most recent releases by a band, or a single release/album. Each release may be given a unique article ID depending on its track list with the "Releases, new one when track track changes" option, which should make them show up as new articles when tracks are added or removed. Releases may also be split up to individual articles for each track with the "Individual tracks" option. This uses and undocumented API from the Bandcamp Android app. It's much faster than loading and parsing the website HTML, and seems to fail less often with more relaxed rate limits. It's still far from perfect in that regard though. The "Individual tracks" option generates requests for each individual track so that can quickly run into rate limits. The "Individual tracks" option also has a quirk where tracks released under e.g. a music label will have their artist set to the label instead of the actual artist of the track. This is a limitation of the API.
363 lines
10 KiB
PHP
363 lines
10 KiB
PHP
<?php
|
|
class BandcampBridge extends BridgeAbstract {
|
|
|
|
const MAINTAINER = 'sebsauvage, Roliga';
|
|
const NAME = 'Bandcamp Bridge';
|
|
const URI = 'https://bandcamp.com/';
|
|
const CACHE_TIMEOUT = 600; // 10min
|
|
const DESCRIPTION = 'New bandcamp releases by tag, band or album';
|
|
const PARAMETERS = array(
|
|
'By tag' => array(
|
|
'tag' => array(
|
|
'name' => 'tag',
|
|
'type' => 'text',
|
|
'required' => true
|
|
)
|
|
),
|
|
'By band' => array(
|
|
'band' => array(
|
|
'name' => 'band',
|
|
'type' => 'text',
|
|
'title' => 'Band name as seen in the band page URL',
|
|
'required' => true
|
|
),
|
|
'type' => array(
|
|
'name' => 'Articles are',
|
|
'type' => 'list',
|
|
'values' => array(
|
|
'Releases' => 'releases',
|
|
'Releases, new one when track list changes' => 'changes',
|
|
'Individual tracks' => 'tracks'
|
|
),
|
|
'defaultValue' => 'changes'
|
|
),
|
|
'limit' => array(
|
|
'name' => 'limit',
|
|
'type' => 'number',
|
|
'title' => 'Number of releases to return',
|
|
'defaultValue' => 5
|
|
)
|
|
),
|
|
'By album' => array(
|
|
'band' => array(
|
|
'name' => 'band',
|
|
'type' => 'text',
|
|
'title' => 'Band name as seen in the album page URL',
|
|
'required' => true
|
|
),
|
|
'album' => array(
|
|
'name' => 'album',
|
|
'type' => 'text',
|
|
'title' => 'Album name as seen in the album page URL',
|
|
'required' => true
|
|
),
|
|
'type' => array(
|
|
'name' => 'Articles are',
|
|
'type' => 'list',
|
|
'values' => array(
|
|
'Releases' => 'releases',
|
|
'Releases, new one when track list changes' => 'changes',
|
|
'Individual tracks' => 'tracks'
|
|
),
|
|
'defaultValue' => 'tracks'
|
|
)
|
|
)
|
|
);
|
|
const IMGURI = 'https://f4.bcbits.com/';
|
|
const IMGSIZE_300PX = 23;
|
|
const IMGSIZE_700PX = 16;
|
|
|
|
private $feedName;
|
|
|
|
public function getIcon() {
|
|
return 'https://s4.bcbits.com/img/bc_favicon.ico';
|
|
}
|
|
|
|
public function collectData(){
|
|
switch($this->queriedContext) {
|
|
case 'By tag':
|
|
$url = self::URI . 'api/hub/1/dig_deeper';
|
|
$data = $this->buildRequestJson();
|
|
$header = array(
|
|
'Content-Type: application/json',
|
|
'Content-Length: ' . strlen($data)
|
|
);
|
|
$opts = array(
|
|
CURLOPT_CUSTOMREQUEST => 'POST',
|
|
CURLOPT_POSTFIELDS => $data
|
|
);
|
|
$content = getContents($url, $header, $opts)
|
|
or returnServerError('Could not complete request to: ' . $url);
|
|
|
|
$json = json_decode($content);
|
|
|
|
if ($json->ok !== true) {
|
|
returnServerError('Invalid response');
|
|
}
|
|
|
|
foreach ($json->items as $entry) {
|
|
$url = $entry->tralbum_url;
|
|
$artist = $entry->artist;
|
|
$title = $entry->title;
|
|
// e.g. record label is the releaser, but not the artist
|
|
$releaser = $entry->band_name !== $entry->artist ? $entry->band_name : null;
|
|
|
|
$full_title = $artist . ' - ' . $title;
|
|
$full_artist = $artist;
|
|
if (isset($releaser)) {
|
|
$full_title .= ' (' . $releaser . ')';
|
|
$full_artist .= ' (' . $releaser . ')';
|
|
}
|
|
$small_img = $this->getImageUrl($entry->art_id, self::IMGSIZE_300PX);
|
|
$img = $this->getImageUrl($entry->art_id, self::IMGSIZE_700PX);
|
|
|
|
$item = array(
|
|
'uri' => $url,
|
|
'author' => $full_artist,
|
|
'title' => $full_title
|
|
);
|
|
$item['content'] = "<img src='$small_img' /><br/>$full_title";
|
|
$item['enclosures'] = array($img);
|
|
$this->items[] = $item;
|
|
}
|
|
break;
|
|
case 'By band':
|
|
case 'By album':
|
|
$html = getSimpleHTMLDOMCached($this->getURI(), 86400);
|
|
|
|
$titleElement = $html->find('head meta[name=title]', 0)
|
|
or returnServerError('Unable to find title on: ' . $this->getURI());
|
|
$this->feedName = $titleElement->content;
|
|
|
|
$regex = '/band_id=(\d+)/';
|
|
if(preg_match($regex, $html, $matches) == false)
|
|
returnServerError('Unable to find band ID on: ' . $this->getURI());
|
|
$band_id = $matches[1];
|
|
|
|
$tralbums = array();
|
|
switch($this->queriedContext) {
|
|
case 'By band':
|
|
$query_data = array(
|
|
'band_id' => $band_id
|
|
);
|
|
$band_data = $this->apiGet('mobile/22/band_details', $query_data);
|
|
|
|
$num_albums = min(count($band_data->discography), $this->getInput('limit'));
|
|
for($i = 0; $i < $num_albums; $i++) {
|
|
$album_basic_data = $band_data->discography[$i];
|
|
|
|
// 'a' or 't' for albums and individual tracks respectively
|
|
$tralbum_type = substr($album_basic_data->item_type, 0, 1);
|
|
|
|
$query_data = array(
|
|
'band_id' => $band_id,
|
|
'tralbum_type' => $tralbum_type,
|
|
'tralbum_id' => $album_basic_data->item_id
|
|
);
|
|
$tralbums[] = $this->apiGet('mobile/22/tralbum_details', $query_data);
|
|
}
|
|
break;
|
|
case 'By album':
|
|
$regex = '/album=(\d+)/';
|
|
if(preg_match($regex, $html, $matches) == false)
|
|
returnServerError('Unable to find album ID on: ' . $this->getURI());
|
|
$album_id = $matches[1];
|
|
|
|
$query_data = array(
|
|
'band_id' => $band_id,
|
|
'tralbum_type' => 'a',
|
|
'tralbum_id' => $album_id
|
|
);
|
|
$tralbums[] = $this->apiGet('mobile/22/tralbum_details', $query_data);
|
|
|
|
break;
|
|
}
|
|
|
|
foreach ($tralbums as $tralbum_data) {
|
|
if ($tralbum_data->type === 'a' && $this->getInput('type') === 'tracks') {
|
|
foreach ($tralbum_data->tracks as $track) {
|
|
$query_data = array(
|
|
'band_id' => $band_id,
|
|
'tralbum_type' => 't',
|
|
'tralbum_id' => $track->track_id
|
|
);
|
|
$track_data = $this->apiGet('mobile/22/tralbum_details', $query_data);
|
|
|
|
$this->items[] = $this->buildTralbumItem($track_data);
|
|
}
|
|
} else {
|
|
$this->items[] = $this->buildTralbumItem($tralbum_data);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
private function buildTralbumItem($tralbum_data){
|
|
$band_data = $tralbum_data->band;
|
|
|
|
// Format title like: ARTIST - ALBUM/TRACK (OPTIONAL RELEASER)
|
|
// Format artist/author like: ARTIST (OPTIONAL RELEASER)
|
|
//
|
|
// If the album/track is released under a label/a band other than the artist
|
|
// themselves, append that releaser name to the title and artist/author.
|
|
//
|
|
// This sadly doesn't always work right for individual tracks as the artist
|
|
// of the track is always set to the releaser.
|
|
$artist = $tralbum_data->tralbum_artist;
|
|
$full_title = $artist . ' - ' . $tralbum_data->title;
|
|
$full_artist = $artist;
|
|
if (isset($tralbum_data->label)) {
|
|
$full_title .= ' (' . $tralbum_data->label . ')';
|
|
$full_artist .= ' (' . $tralbum_data->label . ')';
|
|
} elseif ($band_data->name !== $artist) {
|
|
$full_title .= ' (' . $band_data->name . ')';
|
|
$full_artist .= ' (' . $band_data->name . ')';
|
|
}
|
|
|
|
$small_img = $this->getImageUrl($tralbum_data->art_id, self::IMGSIZE_300PX);
|
|
$img = $this->getImageUrl($tralbum_data->art_id, self::IMGSIZE_700PX);
|
|
|
|
$item = array(
|
|
'uri' => $tralbum_data->bandcamp_url,
|
|
'author' => $full_artist,
|
|
'title' => $full_title,
|
|
'enclosures' => array($img),
|
|
'timestamp' => $tralbum_data->release_date
|
|
);
|
|
|
|
$item['categories'] = array();
|
|
foreach ($tralbum_data->tags as $tag) {
|
|
$item['categories'][] = $tag->norm_name;
|
|
}
|
|
|
|
// Give articles a unique UID depending on its track list
|
|
// Releases should then show up as new articles when tracks are added
|
|
if ($this->getInput('type') === 'changes') {
|
|
$item['uid'] = "bandcamp/$band_data->band_id/$tralbum_data->id/";
|
|
foreach ($tralbum_data->tracks as $track) {
|
|
$item['uid'] .= $track->track_id;
|
|
}
|
|
}
|
|
|
|
$item['content'] = "<img src='$small_img' /><br/>$full_title<br/>";
|
|
if ($tralbum_data->type === 'a') {
|
|
$item['content'] .= '<ol>';
|
|
foreach ($tralbum_data->tracks as $track) {
|
|
$item['content'] .= "<li>$track->title</li>";
|
|
}
|
|
$item['content'] .= '</ol>';
|
|
}
|
|
if (!empty($tralbum_data->about)) {
|
|
$item['content'] .= '<p>'
|
|
. nl2br($tralbum_data->about)
|
|
. '</p>';
|
|
}
|
|
|
|
return $item;
|
|
}
|
|
|
|
private function buildRequestJson(){
|
|
$requestJson = array(
|
|
'tag' => $this->getInput('tag'),
|
|
'page' => 1,
|
|
'sort' => 'date'
|
|
);
|
|
return json_encode($requestJson);
|
|
}
|
|
|
|
private function getImageUrl($id, $size){
|
|
return self::IMGURI . 'img/a' . $id . '_' . $size . '.jpg';
|
|
}
|
|
|
|
private function apiGet($endpoint, $query_data) {
|
|
$url = self::URI . 'api/' . $endpoint . '?' . http_build_query($query_data);
|
|
$data = json_decode(getContents($url))
|
|
or returnServerError('API request to "' . $url . '" failed.');
|
|
return $data;
|
|
}
|
|
|
|
public function getURI(){
|
|
switch($this->queriedContext) {
|
|
case 'By tag':
|
|
if(!is_null($this->getInput('tag'))) {
|
|
return self::URI
|
|
. 'tag/'
|
|
. urlencode($this->getInput('tag'))
|
|
. '?sort_field=date';
|
|
}
|
|
break;
|
|
case 'By band':
|
|
if(!is_null($this->getInput('band'))) {
|
|
return 'https://'
|
|
. $this->getInput('band')
|
|
. '.bandcamp.com/music';
|
|
}
|
|
break;
|
|
case 'By album':
|
|
if(!is_null($this->getInput('band')) && !is_null($this->getInput('album'))) {
|
|
return 'https://'
|
|
. $this->getInput('band')
|
|
. '.bandcamp.com/album/'
|
|
. $this->getInput('album');
|
|
}
|
|
break;
|
|
}
|
|
|
|
return parent::getURI();
|
|
}
|
|
|
|
public function getName(){
|
|
switch($this->queriedContext) {
|
|
case 'By tag':
|
|
if(!is_null($this->getInput('tag'))) {
|
|
return $this->getInput('tag') . ' - Bandcamp Tag';
|
|
}
|
|
break;
|
|
case 'By band':
|
|
if(isset($this->feedName)) {
|
|
return $this->feedName . ' - Bandcamp Band';
|
|
} elseif(!is_null($this->getInput('band'))) {
|
|
return $this->getInput('band') . ' - Bandcamp Band';
|
|
}
|
|
break;
|
|
case 'By album':
|
|
if(isset($this->feedName)) {
|
|
return $this->feedName . ' - Bandcamp Album';
|
|
} elseif(!is_null($this->getInput('album'))) {
|
|
return $this->getInput('album') . ' - Bandcamp Album';
|
|
}
|
|
break;
|
|
}
|
|
|
|
return parent::getName();
|
|
}
|
|
|
|
public function detectParameters($url) {
|
|
$params = array();
|
|
|
|
// By tag
|
|
$regex = '/^(https?:\/\/)?bandcamp\.com\/tag\/([^\/.&?\n]+)/';
|
|
if(preg_match($regex, $url, $matches) > 0) {
|
|
$params['tag'] = urldecode($matches[2]);
|
|
return $params;
|
|
}
|
|
|
|
// By band
|
|
$regex = '/^(https?:\/\/)?([^\/.&?\n]+?)\.bandcamp\.com/';
|
|
if(preg_match($regex, $url, $matches) > 0) {
|
|
$params['band'] = urldecode($matches[2]);
|
|
return $params;
|
|
}
|
|
|
|
// By album
|
|
$regex = '/^(https?:\/\/)?([^\/.&?\n]+?)\.bandcamp\.com\/album\/([^\/.&?\n]+)/';
|
|
if(preg_match($regex, $url, $matches) > 0) {
|
|
$params['band'] = urldecode($matches[2]);
|
|
$params['album'] = urldecode($matches[3]);
|
|
return $params;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|