1 from __future__ 
import unicode_literals
 
   7 from .common 
import InfoExtractor
 
  28 class BandcampIE(InfoExtractor
): 
  29     _VALID_URL 
= r
'https?://[^/]+\.bandcamp\.com/track/(?P<title>[^/?#&]+)' 
  31         'url': 'http://youtube-dl.bandcamp.com/track/youtube-dl-test-song', 
  32         'md5': 'c557841d5e50261777a6585648adf439', 
  36             'title': "youtube-dl  \"'/\\\u00e4\u21ad - youtube-dl test song \"'/\\\u00e4\u21ad", 
  39         '_skip': 'There is a limit of 200 free downloads / month for the test song' 
  42         'url': 'http://benprunty.bandcamp.com/track/lanius-battle', 
  43         'md5': '853e35bf34aa1d6fe2615ae612564b36', 
  47             'title': 'Ben Prunty - Lanius (Battle)', 
  48             'thumbnail': r
're:^https?://.*\.jpg$', 
  49             'uploader': 'Ben Prunty', 
  50             'timestamp': 1396508491, 
  51             'upload_date': '20140403', 
  52             'release_date': '20140403', 
  54             'track': 'Lanius (Battle)', 
  56             'track_id': '2650410135', 
  57             'artist': 'Ben Prunty', 
  58             'album': 'FTL: Advanced Edition Soundtrack', 
  61         # no free download, mp3 128 
  62         'url': 'https://relapsealumni.bandcamp.com/track/hail-to-fire', 
  63         'md5': 'fec12ff55e804bb7f7ebeb77a800c8b7', 
  67             'title': 'Mastodon - Hail to Fire', 
  68             'thumbnail': r
're:^https?://.*\.jpg$', 
  69             'uploader': 'Mastodon', 
  70             'timestamp': 1322005399, 
  71             'upload_date': '20111122', 
  72             'release_date': '20040207', 
  74             'track': 'Hail to Fire', 
  76             'track_id': '2584466013', 
  78             'album': 'Call of the Mastodon', 
  82     def _real_extract(self
, url
): 
  83         mobj 
= re
.match(self
._VALID
_URL
, url
) 
  84         title 
= mobj
.group('title') 
  85         webpage 
= self
._download
_webpage
(url
, title
) 
  86         thumbnail 
= self
._html
_search
_meta
('og:image', webpage
, default
=None) 
  94         track_info 
= self
._parse
_json
( 
  96                 r
'trackinfo\s*:\s*\[\s*({.+?})\s*\]\s*,\s*?\n', 
  97                 webpage
, 'track info', default
='{}'), title
) 
  99             file_ 
= track_info
.get('file') 
 100             if isinstance(file_
, dict): 
 101                 for format_id
, format_url 
in file_
.items(): 
 102                     if not url_or_none(format_url
): 
 104                     ext
, abr_str 
= format_id
.split('-', 1) 
 106                         'format_id': format_id
, 
 107                         'url': self
._proto
_relative
_url
(format_url
, 'http:'), 
 111                         'abr': int_or_none(abr_str
), 
 113             track 
= track_info
.get('title') 
 114             track_id 
= str_or_none(track_info
.get('track_id') or track_info
.get('id')) 
 115             track_number 
= int_or_none(track_info
.get('track_num')) 
 116             duration 
= float_or_none(track_info
.get('duration')) 
 119             return self
._search
_regex
( 
 120                 r
'\b%s\s*["\']?\s
*:\s
*(["\'])(?P<value>(?:(?!\1).)+)\1' % key, 
 121                 webpage, key, default=None, group='value') 
 123         artist = extract('artist') 
 124         album = extract('album_title') 
 125         timestamp = unified_timestamp( 
 126             extract('publish_date') or extract('album_publish_date')) 
 127         release_date = unified_strdate(extract('album_release_date')) 
 129         download_link = self._search_regex( 
 130             r'freeDownloadPage\s*:\s*(["\'])(?P
<url
>(?
:(?
!\
1).)+)\
1', webpage, 
 131             'download link
', default=None, group='url
') 
 133             track_id = self._search_regex( 
 134                 r'(?ms
)var TralbumData 
= .*?
[{,]\s
*id: (?P
<id>\d
+),?$
', 
 137             download_webpage = self._download_webpage( 
 138                 download_link, track_id, 'Downloading free downloads page
') 
 140             blob = self._parse_json( 
 142                     r'data
-blob
=(["\'])(?P<blob>{.+?})\1', download_webpage, 
 143                     'blob', group='blob'), 
 144                 track_id, transform_source=unescapeHTML) 
 147                 blob, (lambda x: x['digital_items'][0], 
 148                        lambda x: x['download_items'][0]), dict) 
 150                 downloads = info.get('downloads') 
 151                 if isinstance(downloads, dict): 
 153                         track = info.get('title') 
 155                         artist = info.get('artist') 
 157                         thumbnail = info.get('thumb_url') 
 159                     download_formats = {} 
 160                     download_formats_list = blob.get('download_formats') 
 161                     if isinstance(download_formats_list, list): 
 162                         for f in blob['download_formats']: 
 163                             name, ext = f.get('name'), f.get('file_extension') 
 164                             if all(isinstance(x, compat_str) for x in (name, ext)): 
 165                                 download_formats[name] = ext.strip('.') 
 167                     for format_id, f in downloads.items(): 
 168                         format_url = f.get('url') 
 171                         # Stat URL generation algorithm is reverse engineered from 
 172                         # download_*_bundle_*.js 
 173                         stat_url = update_url_query( 
 174                             format_url.replace('/download/', '/statdownload/'), { 
 175                                 '.rand': int(time.time() * 1000 * random.random()), 
 177                         format_id = f.get('encoding_name') or format_id 
 178                         stat = self._download_json( 
 179                             stat_url, track_id, 'Downloading %s JSON' % format_id, 
 180                             transform_source=lambda s: s[s.index('{'):s.rindex('}') + 1], 
 184                         retry_url = url_or_none(stat.get('retry_url')) 
 188                             'url': self._proto_relative_url(retry_url, 'http:'), 
 189                             'ext': download_formats.get(format_id), 
 190                             'format_id': format_id, 
 191                             'format_note': f.get('description'), 
 192                             'filesize': parse_filesize(f.get('size_mb')), 
 196         self._sort_formats(formats) 
 198         title = '%s - %s' % (artist, track) if artist else track 
 201             duration = float_or_none(self._html_search_meta( 
 202                 'duration', webpage, default=None)) 
 207             'thumbnail': thumbnail, 
 209             'timestamp': timestamp, 
 210             'release_date': release_date, 
 211             'duration': duration, 
 213             'track_number': track_number, 
 214             'track_id': track_id, 
 221 class BandcampAlbumIE(InfoExtractor): 
 222     IE_NAME = 'Bandcamp:album' 
 223     _VALID_URL = r'https?://(?:(?P<subdomain>[^.]+)\.)?bandcamp\.com(?:/album/(?P<album_id>[^/?#&]+))?' 
 226         'url': 'http://blazo.bandcamp.com/album/jazz-format-mixtape-vol-1', 
 229                 'md5': '39bc1eded3476e927c724321ddf116cf', 
 237                 'md5': '1a2c32e2691474643e912cc6cd4bffaa', 
 241                     'title': 'Kero One - Keep It Alive (Blazo remix)', 
 246             'title': 'Jazz Format Mixtape vol.1', 
 247             'id': 'jazz-format-mixtape-vol-1', 
 248             'uploader_id': 'blazo', 
 253         'skip': 'Bandcamp imposes download limits.' 
 255         'url': 'http://nightbringer.bandcamp.com/album/hierophany-of-the-open-grave', 
 257             'title': 'Hierophany of the Open Grave', 
 258             'uploader_id': 'nightbringer', 
 259             'id': 'hierophany-of-the-open-grave', 
 261         'playlist_mincount': 9, 
 263         'url': 'http://dotscale.bandcamp.com', 
 267             'uploader_id': 'dotscale', 
 269         'playlist_mincount': 7, 
 271         # with escaped quote in title 
 272         'url': 'https://jstrecords.bandcamp.com/album/entropy-ep', 
 274             'title': '"Entropy
" EP', 
 275             'uploader_id': 'jstrecords', 
 278         'playlist_mincount': 3, 
 280         # not all tracks have songs 
 281         'url': 'https://insulters.bandcamp.com/album/we-are-the-plague', 
 283             'id': 'we-are-the-plague', 
 284             'title': 'WE ARE THE PLAGUE', 
 285             'uploader_id': 'insulters', 
 291     def suitable(cls, url): 
 293                 if BandcampWeeklyIE.suitable(url) or BandcampIE.suitable(url) 
 294                 else super(BandcampAlbumIE, cls).suitable(url)) 
 296     def _real_extract(self, url): 
 297         mobj = re.match(self._VALID_URL, url) 
 298         uploader_id = mobj.group('subdomain') 
 299         album_id = mobj.group('album_id') 
 300         playlist_id = album_id or uploader_id 
 301         webpage = self._download_webpage(url, playlist_id) 
 302         track_elements = re.findall( 
 303             r'(?s)<div[^>]*>(.*?<a[^>]+href="([^
"]+?)"[^
>]+itemprop
="url"[^
>]*>.*?
)</div
>', webpage) 
 304         if not track_elements: 
 305             raise ExtractorError('The page doesn
\'t contain any tracks
') 
 306         # Only tracks with duration info have songs 
 309                 compat_urlparse.urljoin(url, t_path), 
 310                 ie=BandcampIE.ie_key(), 
 311                 video_title=self._search_regex( 
 312                     r'<span
\b[^
>]+\bitemprop
=["\']name["\'][^
>]*>([^
<]+)', 
 313                     elem_content, 'track title
', fatal=False)) 
 314             for elem_content, t_path in track_elements 
 315             if self._html_search_meta('duration
', elem_content, default=None)] 
 317         title = self._html_search_regex( 
 318             r'album_title\s
*:\s
*"((?:\\.|[^"\\])+?
)"', 
 319             webpage, 'title', fatal=False) 
 321             title = title.replace(r'\"', '"') 
 324             'uploader_id
': uploader_id, 
 331 class BandcampWeeklyIE(InfoExtractor): 
 332     IE_NAME = 'Bandcamp
:weekly
' 
 333     _VALID_URL = r'https?
://(?
:www\
.)?bandcamp\
.com
/?
\?(?
:.*?
&)?show
=(?P
<id>\d
+)' 
 335         'url
': 'https
://bandcamp
.com
/?show
=224', 
 336         'md5
': 'b00df799c733cf7e0c567ed187dea0fd
', 
 340             'title
': 'BC Weekly April 
4th 
2017 - Magic Moments
', 
 341             'description
': 'md5
:5d48150916e8e02d030623a48512c874
', 
 343             'release_date
': '20170404', 
 344             'series
': 'Bandcamp Weekly
', 
 345             'episode
': 'Magic Moments
', 
 346             'episode_number
': 208, 
 350         'url
': 'https
://bandcamp
.com
/?blah
/blah
@&show
=228', 
 351         'only_matching
': True 
 354     def _real_extract(self, url): 
 355         video_id = self._match_id(url) 
 356         webpage = self._download_webpage(url, video_id) 
 358         blob = self._parse_json( 
 360                 r'data
-blob
=(["\'])(?P<blob>{.+?})\1', webpage, 
 361                 'blob', group='blob'), 
 362             video_id, transform_source=unescapeHTML) 
 364         show = blob['bcw_show'] 
 366         # This is desired because any invalid show id redirects to `bandcamp.com` 
 367         # which happens to expose the latest Bandcamp Weekly episode. 
 368         show_id = int_or_none(show.get('show_id')) or int_or_none(video_id) 
 371         for format_id, format_url in show['audio_stream'].items(): 
 372             if not url_or_none(format_url): 
 374             for known_ext in KNOWN_EXTENSIONS: 
 375                 if known_ext in format_id: 
 381                 'format_id': format_id, 
 386         self._sort_formats(formats) 
 388         title = show.get('audio_title') or 'Bandcamp Weekly' 
 389         subtitle = show.get('subtitle') 
 391             title += ' - %s' % subtitle 
 393         episode_number = None 
 394         seq = blob.get('bcw_seq') 
 396         if seq and isinstance(seq, list): 
 398                 episode_number = next( 
 399                     int_or_none(e.get('episode_number')) 
 401                     if isinstance(e, dict) and int_or_none(e.get('id')) == show_id) 
 402             except StopIteration: 
 408             'description': show.get('desc') or show.get('short_desc'), 
 409             'duration': float_or_none(show.get('audio_duration')), 
 411             'release_date': unified_strdate(show.get('published_date')), 
 412             'series': 'Bandcamp Weekly', 
 413             'episode': show.get('subtitle'), 
 414             'episode_number': episode_number, 
 415             'episode_id': compat_str(video_id),