[ 'tag' => [ 'name' => 'tag', 'type' => 'text', 'required' => true, 'exampleValue' => 'hip-hop-rap' ] ], 'By band' => [ 'band' => [ 'name' => 'band', 'type' => 'text', 'title' => 'Band name as seen in the band page URL', 'required' => true, 'exampleValue' => 'aesoprock' ], 'type' => [ 'name' => 'Articles are', 'type' => 'list', 'values' => [ 'Releases' => 'releases', 'Releases, new one when track list changes' => 'changes', 'Individual tracks' => 'tracks' ], 'defaultValue' => 'changes' ], 'limit' => [ 'name' => 'limit', 'type' => 'number', 'required' => true, 'title' => 'Number of releases to return', 'defaultValue' => 5 ] ], 'By label' => [ 'label' => [ 'name' => 'label', 'type' => 'text', 'title' => 'label name as seen in the label page URL', 'required' => true ], 'type' => [ 'name' => 'Articles are', 'type' => 'list', 'values' => [ 'Releases' => 'releases', 'Releases, new one when track list changes' => 'changes', 'Individual tracks' => 'tracks' ], 'defaultValue' => 'changes' ], 'limit' => [ 'name' => 'limit', 'type' => 'number', 'title' => 'Number of releases to return', 'defaultValue' => 5 ] ], 'By album' => [ 'band' => [ 'name' => 'band', 'type' => 'text', 'title' => 'Band name as seen in the album page URL', 'required' => true, 'exampleValue' => 'aesoprock' ], 'album' => [ 'name' => 'album', 'type' => 'text', 'title' => 'Album name as seen in the album page URL', 'required' => true, 'exampleValue' => 'appleseed' ], 'type' => [ 'name' => 'Articles are', 'type' => 'list', 'values' => [ '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 = [ 'Content-Type: application/json', 'Content-Length: ' . strlen($data), ]; $opts = [ CURLOPT_CUSTOMREQUEST => 'POST', CURLOPT_POSTFIELDS => $data, ]; $content = getContents($url, $header, $opts); $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 = [ 'uri' => $url, 'author' => $full_artist, 'title' => $full_title ]; $item['content'] = "
$full_title"; $item['enclosures'] = [$img]; $this->items[] = $item; } break; case 'By band': case 'By label': case 'By album': $html = getSimpleHTMLDOMCached($this->getURI(), 86400); if ($html->find('meta[name=title]', 0)) { $this->feedName = $html->find('meta[name=title]', 0)->content; } else { $this->feedName = str_replace('Music | ', '', $html->find('title', 0)->plaintext); } $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 = []; switch ($this->queriedContext) { case 'By band': case 'By label': $query_data = [ '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 = [ '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 = [ '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 = [ '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 = [ 'uri' => $tralbum_data->bandcamp_url, 'author' => $full_artist, 'title' => $full_title, 'enclosures' => [$img], 'timestamp' => $tralbum_data->release_date ]; $item['categories'] = []; 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 = [ '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); // todo: 429 Too Many Requests happens a lot $response = getContents($url); $data = json_decode($response); 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 label': if (!is_null($this->getInput('label'))) { return 'https://' . $this->getInput('label') . '.bandcamp.com/music'; } 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 label': if (isset($this->feedName)) { return $this->feedName . ' - Bandcamp Label'; } elseif (!is_null($this->getInput('label'))) { return $this->getInput('label') . ' - Bandcamp Label'; } 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 = []; // By tag $regex = '/^(https?:\/\/)?bandcamp\.com\/tag\/([^\/.&?\n]+)/'; if (preg_match($regex, $url, $matches) > 0) { $params['context'] = 'By tag'; $params['tag'] = urldecode($matches[2]); return $params; } // By band $regex = '/^(https?:\/\/)?([^\/.&?\n]+?)\.bandcamp\.com/'; if (preg_match($regex, $url, $matches) > 0) { $params['context'] = 'By band'; $params['band'] = urldecode($matches[2]); return $params; } // By album $regex = '/^(https?:\/\/)?([^\/.&?\n]+?)\.bandcamp\.com\/album\/([^\/.&?\n]+)/'; if (preg_match($regex, $url, $matches) > 0) { $params['context'] = 'By album'; $params['band'] = urldecode($matches[2]); $params['album'] = urldecode($matches[3]); return $params; } return null; } }