├── .gitignore ├── README.rst ├── setup.cfg ├── setup.py ├── tests ├── __init__.py └── yamusic_app_tests.py └── yamusic ├── __init__.py └── app.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pot 3 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Python YaMusic Readme 2 | ===================== 3 | 4 | This library for using yandex music in python with orm and cursor. 5 | 6 | Using ORM 7 | --------- 8 | 9 | Import models: 10 | >>> from yamusic.app import Artist, Album, Track 11 | 12 | Use filter for iterate result: 13 | >>> Track.objects.filter(title='this must be', artist__title='royksopp', album__title='junior') 14 | >>> Album.objects.filter(artist__title='royksopp', title='junior') 15 | >>> Artist.objects.filter(title='royksopp') 16 | 17 | For managing iterated filter result you can use: 18 | >>> Track.objects.filter(artist__title='unkle').all() 19 | >>> Album.objects.filter(artist__title='a place')[1:5:2] 20 | >>> Artist.objects.filter(title='the')[5] 21 | 22 | If you want to get single item use *get* instead *filter*: 23 | >>> Track.objects.get(title='this must be', artist__title='royksopp', album__title='junior') 24 | >>> Album.objects.get(artist__title='royksopp', title='junior') 25 | >>> Artist.objects.get(title='royksopp') 26 | 27 | You can get *Album* and *Artist* by *id*: 28 | >>> Artist.objects.get(id=49522) 29 | >>> Album.objects.get(id=34596) 30 | 31 | For getting track you need *id* and on of *album*, *album__id* or *album__title* 32 | >>> Track.objects.get(id=id, album__id=album__id) 33 | 34 | Or if you only need play use *id* and *storage_dir*: 35 | >>> Track.objects.get(id=id, storage_dir=storage_dir) 36 | 37 | Using cursor [deprecated] 38 | ------------------------- 39 | 40 | Import search app: 41 | >>> from yamusic.app import Search, cursor 42 | 43 | Cursor can search artists: 44 | >>> cursor.search(Search.TYPE_ARTISTS, 'query') 45 | 46 | Albums: 47 | >>> cursor.search(Search.TYPE_ALBUMS, 'query') 48 | 49 | And tracks: 50 | >>> cursor.search(Search.TYPE_TRACKS, 'query') 51 | 52 | If single=True, search return one item: 53 | >>> cursor.search(Search.TYPE_TRACKS, 'query', single=True) 54 | 55 | Else - return iterator. 56 | 57 | Work with search result 58 | ----------------------- 59 | 60 | For getting data from albums and artists use: 61 | >>> artist.get_albums() 62 | >>> artist.get_tracks() 63 | >>> album.get_tracks() 64 | 65 | For opening track like file use: 66 | >>> track.open() 67 | 68 | For fast getting data objects have: 69 | >>> track.artist 70 | >>> track.album 71 | >>> track.title 72 | >>> album.artist 73 | >>> album.title 74 | >>> artist.title 75 | 76 | Other you can find in source. 77 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [egg_info] 2 | tag_build = dev 3 | tag_svn_revision = true 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | import sys, os 3 | 4 | version = '63' 5 | 6 | 7 | setup(name='python-yamusic', 8 | version=version, 9 | description="Library for acessing yandex music from python", 10 | long_description="""\ 11 | """, 12 | classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers 13 | keywords='yandex music', 14 | author='Vladimir Yakovlev', 15 | author_email='nvbn.rm@gmail.com', 16 | url='http://github.com/nvbn/python-yamusic', 17 | license='LGPL', 18 | packages=find_packages(exclude=['ez_setup', 'examples', 'tests']), 19 | include_package_data=True, 20 | zip_safe=True, 21 | install_requires=requireds, 22 | entry_points=""" 23 | # -*- Entry points: -*- 24 | """, 25 | ) 26 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nvbn/python-yamusic/88a1de6ebc5e7fd216f256fcde7343eb32985e85/tests/__init__.py -------------------------------------------------------------------------------- /tests/yamusic_app_tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This program is free software: you can redistribute it and/or modify 3 | # it under the terms of the GNU Lesser General Public License 4 | # as published by the Free Software Foundation. 5 | # 6 | # This program is distributed in the hope that it will be useful, 7 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | # GNU General Public License for more details. 10 | # 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program. If not, see . 13 | 14 | import unittest 15 | import sys 16 | import os 17 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) 18 | from yamusic.app import cursor, Search, Artist, Album, Track 19 | from itertools import islice 20 | 21 | 22 | class CursorTestCase(unittest.TestCase): 23 | def setUp(self): 24 | self.artists = ( 25 | 'notfoundableartist', 26 | 'royksopp', 'liars', 'portishead' 27 | ) 28 | self.not_fount_artist = self.artists[0] 29 | self.albums = ( 30 | 'notofoundableshitalbum', 31 | 'junior', 'jade motel', 32 | ) 33 | self.not_found_album = self.albums[0] 34 | self.tracks = ( 35 | 'song 2', 'paranoid android', 'castle' 36 | ) 37 | 38 | def test_artist(self): 39 | for artist in self.artists: 40 | artist_list = list( 41 | islice(cursor.search(Search.TYPE_ARTISTS, artist), 1) 42 | ) 43 | if artist == self.not_fount_artist: 44 | self.assertEqual( 45 | len(artist_list), 0, 46 | 'Not found not work!' 47 | ) 48 | else: 49 | self.assertEqual( 50 | len(artist_list), 1, 51 | 'Search artist not work!' 52 | ) 53 | artist_obj = artist_list[0] 54 | albums = list(artist_obj.get_albums()) 55 | self.assertEqual( 56 | len(albums) > 0, True, 57 | 'get_album not work!' 58 | ) 59 | for album in albums: 60 | tracks = list(album.get_tracks()) 61 | self.assertEqual( 62 | len(tracks) > 0, True, 63 | 'get_tracks from album not work!' 64 | ) 65 | 66 | def test_albums(self): 67 | for album in self.albums: 68 | album_list = list( 69 | islice(cursor.search(Search.TYPE_ALBUMS, album), 1) 70 | ) 71 | if album == self.not_found_album: 72 | self.assertEqual( 73 | len(album_list), 0, 74 | 'Not found not work!' 75 | ) 76 | else: 77 | self.assertEqual( 78 | len(album_list), 1, 79 | 'Search album not work!' 80 | ) 81 | album_obj = album_list[0] 82 | tracks = list(album_obj.get_tracks()) 83 | self.assertEqual( 84 | len(tracks) > 0, True, 85 | 'get_tracks from album not work!' 86 | ) 87 | 88 | def test_tracks(self): 89 | for track in self.tracks: 90 | track = cursor.search(Search.TYPE_TRACKS, track, single=True) 91 | self.assertEqual( 92 | len(track.title) > 0, True, 93 | 'Track search not work!' 94 | ) 95 | self.assertEqual( 96 | len(track.open().read(200)), 200, 97 | 'Track downloading not work!' 98 | ) 99 | 100 | 101 | class ORMTestCase(unittest.TestCase): 102 | def setUp(self): 103 | self.artists = ( 104 | 'notfoundableartist', 105 | 'royksopp', 'liars', 'portishead' 106 | ) 107 | self.not_fount_artist = self.artists[0] 108 | self.albums = ( 109 | 'notofoundableshitalbum', 110 | 'junior', 'jade motel', 111 | ) 112 | self.not_found_album = self.albums[0] 113 | self.tracks = ( 114 | 'song 2', 'paranoid android', 'castle' 115 | ) 116 | 117 | def test_artist(self): 118 | for artist in self.artists: 119 | artist_list = Artist.objects.filter(title=artist).all()[0:1] 120 | if artist == self.not_fount_artist: 121 | self.assertEqual( 122 | len(artist_list), 0, 123 | 'Not found not work!' 124 | ) 125 | else: 126 | self.assertEqual( 127 | len(artist_list), 1, 128 | 'Search artist not work!' 129 | ) 130 | artist_obj = artist_list[0] 131 | albums = list(artist_obj.get_albums()) 132 | self.assertEqual( 133 | len(albums) > 0, True, 134 | 'get_album not work!' 135 | ) 136 | for album in albums: 137 | tracks = list(album.get_tracks()) 138 | self.assertEqual( 139 | len(tracks) > 0, True, 140 | 'get_tracks from album not work!' 141 | ) 142 | self.assertEqual(Artist.objects.get(id=24463).title, 'Archive') 143 | 144 | def test_albums(self): 145 | for album in self.albums: 146 | album_list = Album.objects.filter(title=album).all()[0:1] 147 | if album == self.not_found_album: 148 | self.assertEqual( 149 | len(album_list), 0, 150 | 'Not found not work!' 151 | ) 152 | else: 153 | self.assertEqual( 154 | len(album_list), 1, 155 | 'Search album not work!' 156 | ) 157 | album_obj = album_list[0] 158 | tracks = list(album_obj.get_tracks()) 159 | self.assertEqual( 160 | len(tracks) > 0, True, 161 | 'get_tracks from album not work!' 162 | ) 163 | self.assertEqual(Album.objects.get(id=298323).title, 'The Less You Know, The Better') 164 | 165 | def test_tracks(self): 166 | for track in self.tracks: 167 | track = Track.objects.get(title=track) 168 | self.assertEqual( 169 | len(track.title) > 0, True, 170 | 'Track search not work!' 171 | ) 172 | self.assertEqual( 173 | len(track.open().read(200)), 200, 174 | 'Track downloading not work!' 175 | ) 176 | self.assertEqual(Track.objects.get(id=1675302, album__id=166649).title, 'Karen') 177 | 178 | if __name__ == '__main__': 179 | unittest.main() 180 | -------------------------------------------------------------------------------- /yamusic/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nvbn/python-yamusic/88a1de6ebc5e7fd216f256fcde7343eb32985e85/yamusic/__init__.py -------------------------------------------------------------------------------- /yamusic/app.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This program is free software: you can redistribute it and/or modify 3 | # it under the terms of the GNU Lesser General Public License 4 | # as published by the Free Software Foundation. 5 | # 6 | # This program is distributed in the hope that it will be useful, 7 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | # GNU General Public License for more details. 10 | # 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program. If not, see . 13 | 14 | from BeautifulSoup import BeautifulSoup, BeautifulStoneSoup 15 | import urllib2 16 | import cookielib 17 | from itertools import imap, islice 18 | from hashlib import md5 19 | import json 20 | import re 21 | 22 | TYPE_TRACKS = 0 23 | TYPE_ALBUMS = 1 24 | TYPE_ARTISTS = 2 25 | 26 | 27 | def fix_json_single_quotes(text): 28 | def replace_quotes(match): 29 | if match.group(1)[0] == "'": 30 | return '"%s"'%(match.group(2).replace(r"\'","'").replace('"',r'\"')) 31 | else: 32 | return '"%s"'%(match.group(2)) 33 | 34 | return re.sub(r"""(["'])((?:\\?.)*?)\1""", replace_quotes, text); 35 | 36 | class Cached(object): 37 | """Simple cache for avoiding duplicates""" 38 | CACHE = '' 39 | 40 | @classmethod 41 | def get(cls, **kwargs): 42 | id = kwargs.get('id', -1) 43 | if id in getattr(Search, cls.CACHE): 44 | return getattr(Search, cls.CACHE)[id] 45 | result = cls(**kwargs) 46 | if result.id: 47 | getattr(Search, cls.CACHE)[id] = result 48 | return result 49 | 50 | def __unicode__(self): 51 | return '' 52 | 53 | def __repr__(self): 54 | return '<%s: %s>' % ( 55 | self.__class__.__name__, 56 | self.__unicode__() 57 | ) 58 | 59 | 60 | class Manager(object): 61 | 62 | def __init__(self, search_cls, _type, filter_fnc=None): 63 | self.search_cls = search_cls 64 | self.type = _type 65 | self._filter_fnc = filter_fnc 66 | 67 | @property 68 | def filter_result(self): 69 | if not self._filter_fnc: 70 | raise ValueError 71 | if not hasattr(self, '_filter_result'): 72 | self._filter_result = self._filter_fnc() 73 | return self._filter_result 74 | 75 | def _get_titles(self, *args, **kwargs): 76 | """Get title for search 77 | 78 | Keyword Arguments: 79 | *args -- classes for search 80 | **kwargs -- search fields 81 | 82 | Returns: str 83 | """ 84 | titles = [] 85 | for cls, cls_name in map( 86 | lambda arg: (arg, arg.__name__.lower()), args 87 | ): 88 | cls_dict = dict(filter( 89 | lambda (name, val): 90 | val and cls_name in name, kwargs.items() 91 | )) 92 | title = cls_dict.get(cls_name + '__title', '') 93 | id = cls_dict.get(cls_name + '__id', None) 94 | obj = cls_dict.get(cls_name) 95 | if not title: 96 | if id and not obj: 97 | obj = cls.objects.get(id=id) 98 | if obj: 99 | title = obj.title 100 | titles.append(title) 101 | return ' '.join(titles) 102 | 103 | def _search(self, single, title='', **kwargs): 104 | titles = self._get_titles(*self.search_cls, **kwargs) 105 | if title: 106 | titles = ' '.join([titles, title]) 107 | return cursor.search(self.type, titles, single=single) 108 | 109 | def all(self): 110 | return list(self.filter_result) 111 | 112 | def filter(self, title='', **kwargs): 113 | return Manager( 114 | self.search_cls, 115 | self.type, 116 | lambda: self._search(False, title, **kwargs) 117 | ) 118 | 119 | def get(self, title='', **kwargs): 120 | if 'id' in kwargs: 121 | raise ValueError 122 | return self._search(True, title, **kwargs) 123 | 124 | def __getitem__(self, item): 125 | if type(item) is slice: 126 | return list(islice( 127 | self.filter_result, item.start, item.stop, item.step 128 | )) 129 | else: 130 | return list(islice(self.filter_result, item, item + 1))[0] 131 | 132 | def __iter__(self): 133 | return self.filter_result 134 | 135 | def __len__(self): 136 | return len(self.all()) 137 | 138 | 139 | class ArtistManager(Manager): 140 | 141 | def __init__(self): 142 | super(ArtistManager, self).__init__([], TYPE_ARTISTS) 143 | 144 | def get(self, id=None, **kwargs): 145 | if id: 146 | artist = Artist(id=id) 147 | artist.get_data() 148 | return artist 149 | else: 150 | return super(ArtistManager, self).get(**kwargs) 151 | 152 | 153 | class Artist(Cached): 154 | """Artist item""" 155 | CACHE = 'ARTISTS_CACHE' 156 | objects = ArtistManager() 157 | 158 | def __init__(self, id=None, title=None): 159 | self.id = id 160 | self.title = title 161 | 162 | def __unicode__(self): 163 | return self.title 164 | 165 | def get_albums(self): 166 | """Lazy get artist albums""" 167 | if not hasattr(self, '_albums'): 168 | self.get_data() 169 | return self._albums 170 | 171 | def get_data(self): 172 | self._albums = [] 173 | data = cursor.open( 174 | 'http://music.yandex.ru/fragment/artist/%d/tracks' % 175 | int(self.id) 176 | ).read() 177 | soup = BeautifulSoup(data) 178 | self.title = cursor._remove_html( 179 | soup.find( 180 | 'h1', cursor._class_filter('b-title__title') 181 | ).__unicode__() 182 | ) 183 | for album in soup.findAll( 184 | 'div', cursor._class_filter('b-album-control') 185 | ): 186 | try: 187 | album_data = json.loads(fix_json_single_quotes(album['onclick'][7:])) 188 | album = Album.get( 189 | id=album_data.get('id'), 190 | title=album_data.get('title'), 191 | artist=self, 192 | cover=album_data.get('cover') 193 | ) 194 | self._albums.append(album) 195 | album.set_tracks(album_data.get('tracks')) 196 | except ValueError: 197 | pass 198 | 199 | def get_tracks(self): 200 | """Lazy get artist tracks""" 201 | if hasattr(self, '_tracks'): 202 | return self._tracks 203 | tracks = [] 204 | for album in self.get_albums(): 205 | tracks += album.get_tracks() 206 | self._tracks = tracks 207 | return self._tracks 208 | 209 | 210 | class AlbumManager(Manager): 211 | 212 | def __init__(self): 213 | super(AlbumManager, self).__init__([Artist], TYPE_ALBUMS) 214 | 215 | def get(self, id=None, **kwargs): 216 | if id: 217 | album = Album(id=id) 218 | album.get_data() 219 | return album 220 | else: 221 | return super(AlbumManager, self).get(**kwargs) 222 | 223 | 224 | class Album(Cached): 225 | """Album item""" 226 | CACHE = 'ALBUMS_CACHE' 227 | objects = AlbumManager() 228 | 229 | def __init__(self, id=None, title=None, cover=None, 230 | artist__id=None, artist__title=None, 231 | artist=None): 232 | self.id = id 233 | self.title = title 234 | self.cover = cover 235 | if artist__id or artist__title: 236 | self.artist = Artist.get( 237 | id=artist__id, 238 | title=artist__title, 239 | ) 240 | if artist: 241 | self.artist = artist 242 | 243 | def set_tracks(self, tracks): 244 | """Set tracks to album""" 245 | self._tracks = [] 246 | for track in tracks: 247 | self._tracks.append( 248 | Track.get( 249 | id=track['id'], 250 | title=track['title'], 251 | artist=self.artist, 252 | album=self, 253 | duration=track['duration'], 254 | storage_dir=track['storage_dir'], 255 | ) 256 | ) 257 | 258 | def get_tracks(self): 259 | """Lazy get album tracks""" 260 | if not hasattr(self, '_tracks'): 261 | self.get_data() 262 | return self._tracks 263 | 264 | def get_data(self): 265 | data = cursor.open( 266 | 'http://music.yandex.ru/fragment/album/%d' % 267 | int(self.id) 268 | ).read() 269 | soup = BeautifulSoup(data) 270 | self._tracks = [] 271 | artist_soup = soup.find( 272 | 'div', cursor._class_filter('b-title__artist') 273 | ).find('a') 274 | self.artist__id = artist_soup['href'].split('/')[-1] 275 | self.artist__title = cursor._remove_html(artist_soup.__unicode__()) 276 | self.artist = Artist.get( 277 | id=self.artist__id, 278 | title=self.artist__title, 279 | ) 280 | for track in soup.findAll('div', cursor._class_filter('b-track')): 281 | track_data = json.loads(track['onclick'][7:]) 282 | self._tracks.append(Track.get( 283 | id=track_data['id'], 284 | title=track_data['title'], 285 | artist=self.artist, 286 | album=self, 287 | storage_dir=track_data['storage_dir'], 288 | duration=track_data['duration'] 289 | )) 290 | self.title = cursor._remove_html( 291 | soup.find( 292 | 'h1', cursor._class_filter('b-title__title') 293 | ).__unicode__() 294 | ) 295 | 296 | def __unicode__(self): 297 | return u'%s - %s' % (self.artist, self.title) 298 | 299 | 300 | class TrackManager(Manager): 301 | 302 | def __init__(self): 303 | super(TrackManager, self).__init__((Artist, Album), TYPE_TRACKS) 304 | 305 | def get(self, id=None, storage_dir=None, **kwargs): 306 | album = kwargs.get('album', None) 307 | album__id = kwargs.get('album__id', None) 308 | album__title = kwargs.get('album__title', None) 309 | if not album: 310 | if album__id: 311 | album = Album.objects.get(id=album__id) 312 | elif album__title: 313 | album = Album.objects.get(title=album__title) 314 | if id and storage_dir: 315 | return Track(id, storage_dir, **kwargs) 316 | elif id and album: 317 | track = Track(id=id, album=album) 318 | track.get_data() 319 | return track 320 | else: 321 | return super(TrackManager, self).get(**kwargs) 322 | 323 | 324 | class Track(Cached): 325 | """Track item""" 326 | CACHE = "TRACKS_CACHE" 327 | objects = TrackManager() 328 | 329 | def __init__(self, id=None, title=None, artist__id=None, 330 | artist__title=None, album__id=None, album__title=None, 331 | album__cover=None, duration=None, storage_dir=None, 332 | artist=None, album=None): 333 | self.id = int(id) 334 | self.title = title 335 | if artist__id or artist__title: 336 | self.artist = Artist.get( 337 | id=artist__id, 338 | title=artist__title, 339 | ) 340 | if album__id or album__title or album__cover: 341 | self.album = Album.get( 342 | id=album__id, 343 | title=album__title, 344 | cover=album__cover, 345 | ) 346 | self.duration = duration 347 | self.storage_dir = storage_dir 348 | if artist: 349 | self.artist = artist 350 | if album: 351 | self.album = album 352 | 353 | def __unicode__(self): 354 | return u'%s - %s' % (self.artist, self.title) 355 | 356 | @property 357 | def url(self): 358 | """Calculate track url""" 359 | if not self.storage_dir: 360 | raise AttributeError('Storage dir required!') 361 | info_path_data = cursor.open( 362 | 'http://storage.music.yandex.ru/get/%s/2.xml' % self.storage_dir 363 | ).read() 364 | info_path_soup = BeautifulStoneSoup(info_path_data) 365 | file_path_data = cursor.open( 366 | 'http://storage.music.yandex.ru/download-info/%s/%s' % ( 367 | self.storage_dir, 368 | info_path_soup.find('track')['filename'], 369 | ) 370 | ).read() 371 | file_path_soup = BeautifulStoneSoup(file_path_data).find('download-info') 372 | path = file_path_soup.find('path').text 373 | return 'http://%s/get-mp3/%s/%s%s?track-id=%d®ion=225&from=service-search' % ( 374 | file_path_soup.find('host').text, 375 | cursor.get_key(path[1:] + file_path_soup.find('s').text), 376 | file_path_soup.find('ts').text, 377 | path, 378 | int(self.id), 379 | ) 380 | 381 | def get_data(self): 382 | self.artist = self.album.artist 383 | soup = BeautifulSoup( 384 | cursor.open( 385 | 'http://music.yandex.ru/fragment/track/%d/album/%d' % ( 386 | self.id, 387 | self.album.id, 388 | ) 389 | ).read() 390 | ) 391 | data = soup.find('div', cursor._class_filter('b-track b-track_type_track js-track')) 392 | for attr, val in cursor._parse_track(data).items(): 393 | setattr(self, attr, val) 394 | 395 | def open(self): 396 | """Open track like urlopen""" 397 | return cursor.open(self.url) 398 | 399 | 400 | class Search(object): 401 | """Main search class""" 402 | TYPE_TRACKS = TYPE_TRACKS 403 | TYPE_ALBUMS = TYPE_ALBUMS 404 | TYPE_ARTISTS = TYPE_ARTISTS 405 | TYPES = { 406 | TYPE_TRACKS: 'tracks', 407 | TYPE_ALBUMS: 'albums', 408 | TYPE_ARTISTS: 'artists', 409 | } 410 | URL = 'http://music.yandex.ru/fragment/search?text=%(text)s&type=%(type)s&page=%(page)d' 411 | TRACKS_CACHE = {} 412 | ALBUMS_CACHE = {} 413 | ARTISTS_CACHE = {} 414 | 415 | def __init__(self): 416 | self._opener = self._cookie_jar = None 417 | self.authenticated = False 418 | 419 | @property 420 | def cookie_jar(self): 421 | if not self._cookie_jar: 422 | self._cookie_jar = cookielib.CookieJar() 423 | return self._cookie_jar 424 | 425 | @property 426 | def opener(self): 427 | if not self._opener: 428 | self._opener = urllib2.build_opener( 429 | urllib2.HTTPCookieProcessor(self.cookie_jar) 430 | ) 431 | return self._opener 432 | 433 | def open(self, url): 434 | """Open with cookies""" 435 | return self.opener.open(url) 436 | 437 | def get_key(self, key): 438 | """Get secret key for track loading""" 439 | return md5('XGRlBW9FXlekgbPrRHuSiA' + key.replace('\r\n', '\n')).hexdigest() 440 | 441 | def _class_filter(self, cls_name): 442 | """Create BeautifulSoup class filter""" 443 | return {'class': re.compile(r'\b%s\b' % cls_name)} 444 | 445 | def _remove_html(self, data): 446 | p = re.compile(r'<.*?>') 447 | try: 448 | return p.sub('', data) 449 | except TypeError: 450 | return data 451 | 452 | def _parse_track(self, data): 453 | track = json.loads(data['onclick'][7:]) 454 | return { 455 | 'id': track['id'], 456 | 'title': track['title'], 457 | 'artist__id': track['artist_id'], 458 | 'artist__title': track['artist'], 459 | 'album__id': track['album_id'], 460 | 'album__title': track['album'], 461 | 'album__cover': track['cover'], 462 | 'storage_dir': track['storage_dir'] 463 | } 464 | 465 | def _get_tracks(self, soup): 466 | for track in soup.findAll('div', self._class_filter('b-track')): 467 | yield Track.get(**self._parse_track(track)) 468 | 469 | def _get_albums(self, soup): 470 | for album in soup.findAll('div', self._class_filter('b-albums')): 471 | cover_a = album.find('div', self._class_filter('b-albums__cover')).find('a') 472 | artist_a = album.find('a', 473 | self._class_filter('b-link_class_albums-title-link') 474 | ) 475 | yield Album.get( 476 | id=cover_a['href'].split('/')[-1], 477 | title=self._remove_html(album.find('a', 478 | self._class_filter('b-link_class_albums-title-link') 479 | ).__unicode__()), 480 | cover=cover_a.find('img')['src'], 481 | artist__id=artist_a['href'].split('/')[-1], 482 | artist__title=self._remove_html(artist_a.__unicode__()) 483 | ) 484 | 485 | def _get_artists(self, soup): 486 | for artist in imap( 487 | lambda obj: obj.find('a'), 488 | soup.findAll('div', self._class_filter('b-artist-group')) 489 | ): 490 | yield Artist.get( 491 | id=artist['href'].split('/')[-1], 492 | title=self._remove_html(artist.__unicode__()) 493 | ) 494 | 495 | def _get(self, type, soup): 496 | if type == self.TYPE_TRACKS: 497 | return self._get_tracks(soup) 498 | elif type == self.TYPE_ALBUMS: 499 | return self._get_albums(soup) 500 | elif type == self.TYPE_ARTISTS: 501 | return self._get_artists(soup) 502 | 503 | def _get_result(self, type, text): # start from 0! 504 | pages_count = 1 505 | current_page = 0 506 | while pages_count > current_page: 507 | result = self.open(self.URL % { 508 | 'text': text, 509 | 'type': self.TYPES[type], 510 | 'page': current_page, 511 | }).read() 512 | soup = BeautifulSoup(result) 513 | try: 514 | try: 515 | pages_count = int(soup.findAll('a', self._class_filter('b-pager__page'))[-1].text) # start form 1! 516 | except UnicodeEncodeError: # fix work with ... page 517 | pages_count = int(soup.findAll('a', self._class_filter('b-pager__page'))[-2].text) # start form 1! 518 | current_page = int(soup.find('b', self._class_filter('b-pager__current')).text) # start from 1! 519 | except IndexError: 520 | current_page += 1 # if only one page 521 | for obj in self._get(type, soup): 522 | yield obj 523 | 524 | def search(self, type, text, single=False): 525 | if type not in self.TYPES: 526 | raise AttributeError('Wrong type') 527 | result = self._get_result(type, text) 528 | if single: 529 | return list(islice(result, 1))[0] 530 | else: 531 | return result 532 | 533 | cursor = Search() 534 | --------------------------------------------------------------------------------