diff --git a/README.md b/README.md index 762c294..0696159 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,10 @@ bands[0].name # Fetch band page band = bands[0].get() +# Fetch band's similiar artists +band.similar_artists +# -> + # Get all albums band.albums # -> [, , ...] @@ -63,5 +67,28 @@ metallum.album_search('seventh', band='iron maiden', strict=False) ``` +Song search + +```python +import metallum + +# Search songs matching term +metallum.song_search('fear of the') +# -> [] + +# Search songs containing term +metallum.album_search('fear of the', strict=False) +# -> [, ...] + +# Search songs by band +metallum.song_search('fear of the', band='iron maiden', strict=False) +# -> [, ...] + +# Search songs by release +metallum.song_search('fear of the', release='fear of the dark', strict=False) +# -> [, ...] + +``` + Refer to source and doctests for detailed usage diff --git a/metallum.py b/metallum.py index 945818d..202e6d9 100755 --- a/metallum.py +++ b/metallum.py @@ -15,7 +15,7 @@ import requests_cache from dateutil import parser as date_parser from pyquery import PyQuery -from requests_cache.core import remove_expired_responses +from requests_cache import remove_expired_responses CACHE_FILE = os.path.join(tempfile.gettempdir(), 'metallum_cache') requests_cache.install_cache(cache_name=CACHE_FILE, expire_after=300) @@ -49,8 +49,8 @@ def band_for_id(id: str) -> 'Band': def band_search(name, strict=True, genre=None, countries=[], year_created_from=None, - year_created_to=None, status=[], themes=None, location=None, label=None, - page_start=0) -> 'Search': + year_created_to=None, status=[], themes=None, location=None, + label=None, additional_notes=None, page_start=0) -> 'Search': """Perform an advanced band search. """ # Create a dict from the method arguments @@ -68,6 +68,7 @@ def band_search(name, strict=True, genre=None, countries=[], year_created_from=N 'year_created_to': 'yearCreationTo', 'status': 'status[]', 'label': 'bandLabelName', + 'additional_notes': 'bandNotes', 'page_start': 'iDisplayStart' }) @@ -82,8 +83,11 @@ def album_for_id(id: str) -> 'AlbumWrapper': def album_search(title, strict=True, band=None, band_strict=True, year_from=None, - year_to=None, month_from=None, month_to=None, countries=[], location=None, label=None, - indie_label=False, genre=None, types=[], page_start=0) -> 'Search': + year_to=None, month_from=None, month_to=None, countries=[], + location=None, label=None, indie_label=False, genre=None, + catalog_number=None, identifiers=None, recording_info=None, + version_description=None, additional_notes=None, types=[], + page_start=0, formats=[]) -> 'Search': """Perform an advanced album search """ # Create a dict from the method arguments @@ -113,7 +117,13 @@ def album_search(title, strict=True, band=None, band_strict=True, year_from=None 'countries': 'country[]', 'label': 'releaseLabelName', 'indie_label': 'indieLabel', + 'catalog_number': 'releaseCatalogNumber', + 'identifiers': 'releaseIdentifiers', + 'recording_info': 'releaseRecordingInfo', + 'version_description': 'releaseDescription', + 'additional_notes': 'releaseNotes', 'types': 'releaseType[]', + 'formats': 'releaseFormat[]', 'page_start': 'iDisplayStart' }) @@ -123,6 +133,42 @@ def album_search(title, strict=True, band=None, band_strict=True, year_from=None return Search(url, AlbumResult) +def song_search(title, strict=True, band=None, band_strict=True, release=None, + release_strict=True, lyrics=None, genre=None, types=[], + page_start=0) -> 'Search': + """Perform an advanced song search + """ + # Create a dict from the method arguments + params = locals() + + # Convert boolean value to integer + params['strict'] = str(int(params['strict'])) + params['band_strict'] = str(int(params['band_strict'])) + params['release_strict'] = str(int(params['release_strict'])) + + # Set genre as '*' if none is given to make sure + # that the correct number of parameters will be returned + if params['genre'] is None or len(params['genre'].strip()) == 0: + params['genre'] = '*' + + # Map method arguments to their url query string counterparts + params = map_params(params, { + 'title': 'songTitle', + 'strict': 'exactSongMatch', + 'band': 'bandName', + 'band_strict': 'exactBandMatch', + 'release': 'releaseTitle', + 'release_strict': 'exactReleaseMatch', + 'types': 'releaseType[]', + 'page_start': 'iDisplayStart' + }) + + # Build the search URL + url = 'search/ajax-advanced/searching/songs/?' + urlencode(params, True) + + return Search(url, SongResult) + + def lyrics_for_id(id: int) -> 'Lyrics': return Lyrics(id) @@ -232,12 +278,10 @@ def _dd_element_for_label(self, label: str) -> Optional[PyQuery]: """Data on entity pages are stored in
/
pairs """ labels = list(self._page('dt').contents()) - try: index = labels.index(label) except ValueError: return None - return self._page('dd').eq(index) def _dd_text_for_label(self, label: str) -> str: @@ -294,8 +338,12 @@ def __init__(self, details): super().__init__() for detail in details: if re.match('^ str: """ return self[2] + @property + def other(self) -> str: + return self[3:] + class AlbumResult(SearchResult): @@ -391,6 +443,76 @@ def band_name(self) -> str: return self[0] +class SongResult(SearchResult): + + def __init__(self, details): + super().__init__(details) + self._details = details + self._resultType = None + + def get(self) -> 'SongResult': + return self + + @property + def id(self) -> str: + """ + >>> song.id + '3449' + """ + return re.search(r'(\d+)', self[5]).group(0) + + @property + def title(self) -> str: + return self[3] + + @property + def type(self) -> str: + return self[2] + + @property + def bands(self) -> List['Band']: + bands = [] + el = PyQuery(self._details[0]).wrap('
') + for a in el.find('a'): + url = PyQuery(a).attr('href') + id = re.search(r'\d+$', url).group(0) + bands.append(Band('bands/_/{0}'.format(id))) + return bands + + @property + def band_name(self) -> str: + return self[0] + + @property + def album(self) -> 'Album': + url = PyQuery(self._details[1]).attr('href') + id = re.search('\d+$', url).group(0) + return Album('albums/_/_/{0}'.format(id)) + + @property + def album_name(self) -> str: + return self[1] + + @property + def genres(self) -> List[str]: + """ + >>> song.genres + ['Heavy Metal', 'NWOBHM'] + """ + genres = [] + for genre in self[4].split(' | '): + genres.extend(split_genres(genre.strip())) + return genres + + @property + def lyrics(self) -> 'Lyrics': + """ + >>> str(song.lyrics).split('\\n')[0] + 'I am a man who walks alone' + """ + return Lyrics(self.id) + + class Band(MetallumEntity): def __init__(self, url): @@ -488,9 +610,9 @@ def genres(self) -> List[str]: def themes(self) -> List[str]: """ >>> band.themes - ['Corruption', 'Death', 'Life', 'Internal struggles', 'Anger'] + ['Introspection', 'Anger', 'Corruption', 'Deceit', 'Death', 'Life', 'Metal', 'Literature', 'Films'] """ - return self._dd_text_for_label('Lyrical themes:').split(', ') + return self._dd_text_for_label('Themes:').split(', ') @property def label(self) -> str: @@ -503,8 +625,8 @@ def label(self) -> str: @property def logo(self) -> Optional[str]: """ - >>> band.logo - 'https://www.metal-archives.com/images/1/2/5/125_logo.png' + >>> band.logo[:-3] + 'https://www.metal-archives.com/images/1/2/5/125_logo.' """ url = self._page('#logo').attr('href') if not url: @@ -534,6 +656,95 @@ def albums(self) -> List['AlbumCollection']: url = 'band/discography/id/{0}/tab/all'.format(self.id) return AlbumCollection(url) + @property + def similar_artists(self) -> 'SimilarArtists': + """ + >>> band.similar_artists + """ + + url = 'band/ajax-recommendations/id/' + self.id + '/showMoreSimilar/1' + return SimilarArtists(url, SimilarArtistsResult) + + +class SimilarArtists(Metallum, list): + """Entries in the similar artists tab + """ + + def __init__(self, url, result_handler): + super().__init__(url) + data = self._content + + links_list = PyQuery(data)('a') + values_list = PyQuery(data)('tr') + + # assert(len(links_list) == len(values_list) - 1) + for i in range(0, len(links_list) -1): + details = [links_list[i].attrib.get('href')] + details.extend(values_list[i+1].text_content().split('\n')[1:-1]) + self.append(result_handler(details)) + self.result_count = i + + def __repr__(self): + + def similar_artist_str(SimilarArtistsResult): + return f'{SimilarArtistsResult.name} ({SimilarArtistsResult.score})' + if not self: + return '' + names = list(map(similar_artist_str, self)) + s = ' | '.join(names) + return ''.format(s) + + +class SimilarArtistsResult(list): + """Represents a entry in the similar artists tab + """ + _resultType = Band + + def __init__(self, details): + super().__init__() + self._details = details + for d in details: + self.append(d) + + @property + def id(self) -> str: + # url = PyQuery(self._details[0])('a').attr('href') + return re.search(r'\d+$', self[0]).group(0) + + @property + def url(self) -> str: + return 'bands/_/{0}'.format(self.id) + + @property + def name(self) -> str: + return self[1] + + @property + def country(self) -> str: + """ + >>> search_results[0].country + 'United States' + """ + return self[2] + + @property + def genres(self) -> List[str]: + return split_genres(self[3]) + + @property + def score(self) -> int: + return int(self[4]) + + + def __repr__(self): + s = ' | '.join(self[1:]) + return ''.format(s) + + def get(self) -> 'Metallum': + return self._resultType(self.url) + + + class AlbumCollection(MetallumCollection): @@ -605,6 +816,9 @@ class Album(MetallumEntity): def __init__(self, url): super().__init__(url) + def __repr__(self): + return ''.format(self.title) + @property def id(self) -> str: """ @@ -731,7 +945,7 @@ def _review_element(self) -> Optional[PyQuery]: def score(self) -> Optional[int]: """ >>> album.score - 79 + 81 >>> split_album.score 94 @@ -997,4 +1211,7 @@ def __str__(self): # Objects for multi-disc album testing multi_disc_album = album_for_id('338756') + # Objects for song search testing + song = song_search('Fear of the Dark', band='Iron Maiden', release='Fear of the Dark')[0] + doctest.testmod(globs=locals())