From fc5a1526ca1b5d22e2e436ab9729f522ecf648a4 Mon Sep 17 00:00:00 2001 From: Roliga Date: Wed, 16 Oct 2019 21:37:25 +0200 Subject: [PATCH] [BandcampBridge] Add band and album feeds (#1317) * [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. --- bridges/BandcampBridge.php | 378 +++++++++++++++++++++++++++++++------ 1 file changed, 325 insertions(+), 53 deletions(-) diff --git a/bridges/BandcampBridge.php b/bridges/BandcampBridge.php index 6c75ed5e..fa071465 100644 --- a/bridges/BandcampBridge.php +++ b/bridges/BandcampBridge.php @@ -1,73 +1,262 @@ array( - 'name' => 'tag', - 'type' => 'text', - 'required' => true + 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(){ - $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 + 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) ); - $item['content'] = "
$full_title"; - $item['enclosures'] = array($img); - $this->items[] = $item; + $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'] = "
$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'] = "
$full_title
"; + if ($tralbum_data->type === 'a') { + $item['content'] .= '
    '; + foreach ($tralbum_data->tracks as $track) { + $item['content'] .= "
  1. $track->title
  2. "; + } + $item['content'] .= '
'; + } + if (!empty($tralbum_data->about)) { + $item['content'] .= '

' + . nl2br($tralbum_data->about) + . '

'; + } + + return $item; + } + private function buildRequestJson(){ $requestJson = array( 'tag' => $this->getInput('tag'), @@ -81,11 +270,94 @@ class BandcampBridge extends BridgeAbstract { 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(){ - if(!is_null($this->getInput('tag'))) { - return $this->getInput('tag') . ' - Bandcamp Tag'; + 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; + } }