summaryrefslogtreecommitdiff
path: root/youtube_dl/extractor/yandexmusic.py
blob: eb1062142ecbc6a4702f0dc7763c414934fd3645 (plain)
    1 # coding: utf-8
    2 from __future__ import unicode_literals
    3 
    4 import re
    5 import hashlib
    6 
    7 from .common import InfoExtractor
    8 from ..compat import compat_str
    9 from ..utils import (
   10     ExtractorError,
   11     int_or_none,
   12     float_or_none,
   13 )
   14 
   15 
   16 class YandexMusicBaseIE(InfoExtractor):
   17     @staticmethod
   18     def _handle_error(response):
   19         if isinstance(response, dict):
   20             error = response.get('error')
   21             if error:
   22                 raise ExtractorError(error, expected=True)
   23             if response.get('type') == 'captcha' or 'captcha' in response:
   24                 YandexMusicBaseIE._raise_captcha()
   25 
   26     @staticmethod
   27     def _raise_captcha():
   28         raise ExtractorError(
   29             'YandexMusic has considered youtube-dl requests automated and '
   30             'asks you to solve a CAPTCHA. You can either wait for some '
   31             'time until unblocked and optionally use --sleep-interval '
   32             'in future or alternatively you can go to https://music.yandex.ru/ '
   33             'solve CAPTCHA, then export cookies and pass cookie file to '
   34             'youtube-dl with --cookies',
   35             expected=True)
   36 
   37     def _download_webpage(self, *args, **kwargs):
   38         webpage = super(YandexMusicBaseIE, self)._download_webpage(*args, **kwargs)
   39         if 'Нам очень жаль, но запросы, поступившие с вашего IP-адреса, похожи на автоматические.' in webpage:
   40             self._raise_captcha()
   41         return webpage
   42 
   43     def _download_json(self, *args, **kwargs):
   44         response = super(YandexMusicBaseIE, self)._download_json(*args, **kwargs)
   45         self._handle_error(response)
   46         return response
   47 
   48 
   49 class YandexMusicTrackIE(YandexMusicBaseIE):
   50     IE_NAME = 'yandexmusic:track'
   51     IE_DESC = 'Яндекс.Музыка - Трек'
   52     _VALID_URL = r'https?://music\.yandex\.(?:ru|kz|ua|by)/album/(?P<album_id>\d+)/track/(?P<id>\d+)'
   53 
   54     _TEST = {
   55         'url': 'http://music.yandex.ru/album/540508/track/4878838',
   56         'md5': 'f496818aa2f60b6c0062980d2e00dc20',
   57         'info_dict': {
   58             'id': '4878838',
   59             'ext': 'mp3',
   60             'title': 'Carlo Ambrosio & Fabio Di Bari, Carlo Ambrosio - Gypsy Eyes 1',
   61             'filesize': 4628061,
   62             'duration': 193.04,
   63             'track': 'Gypsy Eyes 1',
   64             'album': 'Gypsy Soul',
   65             'album_artist': 'Carlo Ambrosio',
   66             'artist': 'Carlo Ambrosio & Fabio Di Bari, Carlo Ambrosio',
   67             'release_year': '2009',
   68         },
   69         'skip': 'Travis CI servers blocked by YandexMusic',
   70     }
   71 
   72     def _get_track_url(self, storage_dir, track_id):
   73         data = self._download_json(
   74             'http://music.yandex.ru/api/v1.5/handlers/api-jsonp.jsx?action=getTrackSrc&p=download-info/%s'
   75             % storage_dir,
   76             track_id, 'Downloading track location JSON')
   77 
   78         # Each string is now wrapped in a list, this is probably only temporarily thus
   79         # supporting both scenarios (see https://github.com/rg3/youtube-dl/issues/10193)
   80         for k, v in data.items():
   81             if v and isinstance(v, list):
   82                 data[k] = v[0]
   83 
   84         key = hashlib.md5(('XGRlBW9FXlekgbPrRHuSiA' + data['path'][1:] + data['s']).encode('utf-8')).hexdigest()
   85         storage = storage_dir.split('.')
   86 
   87         return ('http://%s/get-mp3/%s/%s?track-id=%s&from=service-10-track&similarities-experiment=default'
   88                 % (data['host'], key, data['ts'] + data['path'], storage[1]))
   89 
   90     def _get_track_info(self, track):
   91         thumbnail = None
   92         cover_uri = track.get('albums', [{}])[0].get('coverUri')
   93         if cover_uri:
   94             thumbnail = cover_uri.replace('%%', 'orig')
   95             if not thumbnail.startswith('http'):
   96                 thumbnail = 'http://' + thumbnail
   97 
   98         track_title = track['title']
   99         track_info = {
  100             'id': track['id'],
  101             'ext': 'mp3',
  102             'url': self._get_track_url(track['storageDir'], track['id']),
  103             'filesize': int_or_none(track.get('fileSize')),
  104             'duration': float_or_none(track.get('durationMs'), 1000),
  105             'thumbnail': thumbnail,
  106             'track': track_title,
  107         }
  108 
  109         def extract_artist(artist_list):
  110             if artist_list and isinstance(artist_list, list):
  111                 artists_names = [a['name'] for a in artist_list if a.get('name')]
  112                 if artists_names:
  113                     return ', '.join(artists_names)
  114 
  115         albums = track.get('albums')
  116         if albums and isinstance(albums, list):
  117             album = albums[0]
  118             if isinstance(album, dict):
  119                 year = album.get('year')
  120                 track_info.update({
  121                     'album': album.get('title'),
  122                     'album_artist': extract_artist(album.get('artists')),
  123                     'release_year': compat_str(year) if year else None,
  124                 })
  125 
  126         track_artist = extract_artist(track.get('artists'))
  127         if track_artist:
  128             track_info.update({
  129                 'artist': track_artist,
  130                 'title': '%s - %s' % (track_artist, track_title),
  131             })
  132         else:
  133             track_info['title'] = track_title
  134         return track_info
  135 
  136     def _real_extract(self, url):
  137         mobj = re.match(self._VALID_URL, url)
  138         album_id, track_id = mobj.group('album_id'), mobj.group('id')
  139 
  140         track = self._download_json(
  141             'http://music.yandex.ru/handlers/track.jsx?track=%s:%s' % (track_id, album_id),
  142             track_id, 'Downloading track JSON')['track']
  143 
  144         return self._get_track_info(track)
  145 
  146 
  147 class YandexMusicPlaylistBaseIE(YandexMusicBaseIE):
  148     def _build_playlist(self, tracks):
  149         return [
  150             self.url_result(
  151                 'http://music.yandex.ru/album/%s/track/%s' % (track['albums'][0]['id'], track['id']))
  152             for track in tracks if track.get('albums') and isinstance(track.get('albums'), list)]
  153 
  154 
  155 class YandexMusicAlbumIE(YandexMusicPlaylistBaseIE):
  156     IE_NAME = 'yandexmusic:album'
  157     IE_DESC = 'Яндекс.Музыка - Альбом'
  158     _VALID_URL = r'https?://music\.yandex\.(?:ru|kz|ua|by)/album/(?P<id>\d+)/?(\?|$)'
  159 
  160     _TEST = {
  161         'url': 'http://music.yandex.ru/album/540508',
  162         'info_dict': {
  163             'id': '540508',
  164             'title': 'Carlo Ambrosio - Gypsy Soul (2009)',
  165         },
  166         'playlist_count': 50,
  167         'skip': 'Travis CI servers blocked by YandexMusic',
  168     }
  169 
  170     def _real_extract(self, url):
  171         album_id = self._match_id(url)
  172 
  173         album = self._download_json(
  174             'http://music.yandex.ru/handlers/album.jsx?album=%s' % album_id,
  175             album_id, 'Downloading album JSON')
  176 
  177         entries = self._build_playlist(album['volumes'][0])
  178 
  179         title = '%s - %s' % (album['artists'][0]['name'], album['title'])
  180         year = album.get('year')
  181         if year:
  182             title += ' (%s)' % year
  183 
  184         return self.playlist_result(entries, compat_str(album['id']), title)
  185 
  186 
  187 class YandexMusicPlaylistIE(YandexMusicPlaylistBaseIE):
  188     IE_NAME = 'yandexmusic:playlist'
  189     IE_DESC = 'Яндекс.Музыка - Плейлист'
  190     _VALID_URL = r'https?://music\.yandex\.(?P<tld>ru|kz|ua|by)/users/(?P<user>[^/]+)/playlists/(?P<id>\d+)'
  191 
  192     _TESTS = [{
  193         'url': 'http://music.yandex.ru/users/music.partners/playlists/1245',
  194         'info_dict': {
  195             'id': '1245',
  196             'title': 'Что слушают Enter Shikari',
  197             'description': 'md5:3b9f27b0efbe53f2ee1e844d07155cc9',
  198         },
  199         'playlist_count': 6,
  200         'skip': 'Travis CI servers blocked by YandexMusic',
  201     }, {
  202         # playlist exceeding the limit of 150 tracks shipped with webpage (see
  203         # https://github.com/rg3/youtube-dl/issues/6666)
  204         'url': 'https://music.yandex.ru/users/ya.playlist/playlists/1036',
  205         'info_dict': {
  206             'id': '1036',
  207             'title': 'Музыка 90-х',
  208         },
  209         'playlist_mincount': 300,
  210         'skip': 'Travis CI servers blocked by YandexMusic',
  211     }]
  212 
  213     def _real_extract(self, url):
  214         mobj = re.match(self._VALID_URL, url)
  215         tld = mobj.group('tld')
  216         user = mobj.group('user')
  217         playlist_id = mobj.group('id')
  218 
  219         playlist = self._download_json(
  220             'https://music.yandex.%s/handlers/playlist.jsx' % tld,
  221             playlist_id, 'Downloading missing tracks JSON',
  222             fatal=False,
  223             headers={
  224                 'Referer': url,
  225                 'X-Requested-With': 'XMLHttpRequest',
  226                 'X-Retpath-Y': url,
  227             },
  228             query={
  229                 'owner': user,
  230                 'kinds': playlist_id,
  231                 'light': 'true',
  232                 'lang': tld,
  233                 'external-domain': 'music.yandex.%s' % tld,
  234                 'overembed': 'false',
  235             })['playlist']
  236 
  237         tracks = playlist['tracks']
  238         track_ids = [compat_str(track_id) for track_id in playlist['trackIds']]
  239 
  240         # tracks dictionary shipped with playlist.jsx API is limited to 150 tracks,
  241         # missing tracks should be retrieved manually.
  242         if len(tracks) < len(track_ids):
  243             present_track_ids = set([
  244                 compat_str(track['id'])
  245                 for track in tracks if track.get('id')])
  246             missing_track_ids = [
  247                 track_id for track_id in track_ids
  248                 if track_id not in present_track_ids]
  249             missing_tracks = self._download_json(
  250                 'https://music.yandex.%s/handlers/track-entries.jsx' % tld,
  251                 playlist_id, 'Downloading missing tracks JSON',
  252                 fatal=False,
  253                 headers={
  254                     'Referer': url,
  255                     'X-Requested-With': 'XMLHttpRequest',
  256                 },
  257                 query={
  258                     'entries': ','.join(missing_track_ids),
  259                     'lang': tld,
  260                     'external-domain': 'music.yandex.%s' % tld,
  261                     'overembed': 'false',
  262                     'strict': 'true',
  263                 })
  264             if missing_tracks:
  265                 tracks.extend(missing_tracks)
  266 
  267         return self.playlist_result(
  268             self._build_playlist(tracks),
  269             compat_str(playlist_id),
  270             playlist.get('title'), playlist.get('description'))

Generated by cgit