diff --git a/metallum/__init__.py b/metallum/__init__.py index 835ce9e..9e52f17 100644 --- a/metallum/__init__.py +++ b/metallum/__init__.py @@ -3,11 +3,11 @@ """Python interface for www.metal-archives.com""" import requests_cache -from requests_cache.core import remove_expired_responses +from requests_cache import remove_expired_responses from metallum.consts import CACHE_FILE from metallum.models.AlbumTypes import AlbumTypes -from metallum.operations import album_for_id, band_search +from metallum.operations import album_for_id, band_search, song_search requests_cache.install_cache(cache_name=CACHE_FILE, expire_after=300) remove_expired_responses() @@ -29,4 +29,9 @@ # 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()) diff --git a/metallum/models/Results.py b/metallum/models/Results.py index f371ea9..b2412d3 100644 --- a/metallum/models/Results.py +++ b/metallum/models/Results.py @@ -3,7 +3,8 @@ from pyquery import PyQuery -from metallum.models import AlbumWrapper, Band +from metallum.models import Album, AlbumWrapper, Band +from metallum.models.Lyrics import Lyrics from metallum.models.Metallum import Metallum from metallum.utils import split_genres @@ -17,8 +18,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): @@ -112,3 +121,73 @@ def bands(self) -> List["Band"]: @property 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) diff --git a/metallum/models/SimilarArtists.py b/metallum/models/SimilarArtists.py new file mode 100644 index 0000000..e7d6121 --- /dev/null +++ b/metallum/models/SimilarArtists.py @@ -0,0 +1,32 @@ +from pyquery import PyQuery + +from metallum.models.Metallum import Metallum + + +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) diff --git a/metallum/models/__init__.py b/metallum/models/__init__.py index 96fde0e..44fe28d 100644 --- a/metallum/models/__init__.py +++ b/metallum/models/__init__.py @@ -10,6 +10,7 @@ from metallum.models.Metallum import Metallum from metallum.models.MetallumCollection import MetallumCollection from metallum.models.MetallumEntity import MetallumEntity +from metallum.models.SimilarArtists import SimilarArtists from metallum.utils import offset_time, parse_duration, split_genres @@ -131,9 +132,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: @@ -177,6 +178,16 @@ 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 Track(object): @@ -309,6 +320,9 @@ class Album(MetallumEntity): def __init__(self, url): super().__init__(url) + def __repr__(self): + return "".format(self.title) + @property def id(self) -> str: """ @@ -593,3 +607,51 @@ def year(self) -> int: 1986 """ return int(self._elem("td").eq(2).text()) + + +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) diff --git a/metallum/operations.py b/metallum/operations.py index 0165213..809367d 100644 --- a/metallum/operations.py +++ b/metallum/operations.py @@ -2,7 +2,7 @@ from metallum.models import AlbumWrapper, Band from metallum.models.Lyrics import Lyrics -from metallum.models.Results import AlbumResult, BandResult +from metallum.models.Results import AlbumResult, BandResult, SongResult from metallum.models.Search import Search from metallum.utils import map_params @@ -22,6 +22,7 @@ def band_search( themes=None, location=None, label=None, + additional_notes=None, page_start=0, ) -> "Search": """Perform an advanced band search.""" @@ -42,6 +43,7 @@ def band_search( "year_created_to": "yearCreationTo", "status": "status[]", "label": "bandLabelName", + "additional_notes": "bandNotes", "page_start": "iDisplayStart", }, ) @@ -70,8 +72,14 @@ def album_search( 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 @@ -103,7 +111,13 @@ def album_search( "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", }, ) @@ -114,5 +128,52 @@ def album_search( 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) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_metallum_operations.py b/tests/test_metallum_operations.py new file mode 100644 index 0000000..f9b7345 --- /dev/null +++ b/tests/test_metallum_operations.py @@ -0,0 +1,63 @@ +import pytest +from metallum.models.AlbumTypes import AlbumTypes +from metallum.operations import album_for_id, band_search, song_search + + +@pytest.fixture +def band(): + search_results = band_search("metallica") + return search_results[0].get() + + +@pytest.fixture +def album(band): + return band.albums.search(type=AlbumTypes.FULL_LENGTH)[2] + + +@pytest.fixture +def track(album): + return album.tracks[0] + + +@pytest.fixture +def split_album(): + return album_for_id("42682") + + +@pytest.fixture +def split_album_track(split_album): + return split_album.tracks[2] + + +@pytest.fixture +def multi_disc_album(): + return album_for_id("338756") + + +@pytest.fixture +def song(): + return song_search( + "Fear of the Dark", band="Iron Maiden", release="Fear of the Dark" + )[0] + + +def test_band_search(band): + assert band.name == "Metallica" + + +def test_album_search(album): + assert album.title == "Master of Puppets" + + +def test_split_album(split_album): + assert split_album.title == "Paysage d'Hiver / Lunar Aurora" + + +def test_multi_disc_album(multi_disc_album): + assert multi_disc_album.title == "Blood Geometry" + + +def test_song_search(song): + assert song.title == "Fear of the Dark" + assert song.bands[0].name == "Iron Maiden" + assert song.album.title == "Fear of the Dark"