Skip to content

Commit

Permalink
feat: add new functionality including song search and additional sear…
Browse files Browse the repository at this point in the history
…ch parameters

- Added functionality for the song search feature (song_search and SongResult and a change in the link parsing to extract the song ID)
- Added additional search parameters for band_search and album_search such as release formats (there's a pull request for this by @pabe-github), additional notes etc.
- Integrated the "similar artists" feature from @msanlop's fork and pull request, including the README example
- Fixed the requests_cache.core import issue
- "Lyrical themes" has since been renamed to just "Themes"
- Added an other property to band_search results to allow access to additional search result parameters (issue by me Queries resulting in more than just the basic band information lcharlick#7)

Original pull request: lcharlick#17
  • Loading branch information
YisusChrist committed Feb 11, 2024
1 parent 901212b commit 6b7c164
Show file tree
Hide file tree
Showing 7 changed files with 310 additions and 8 deletions.
9 changes: 7 additions & 2 deletions metallum/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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())
85 changes: 82 additions & 3 deletions metallum/models/Results.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -17,8 +18,12 @@ def __init__(self, details):
super().__init__()
for detail in details:
if re.match("^<a href.*", detail):
d = PyQuery(detail)
self.append(d("a").text())
lyrics_link = re.search('id="lyricsLink_(\d+)"', detail)
if lyrics_link is not None:
self.append(lyrics_link[1])
else:
d = PyQuery(detail)
self.append(d("a").text())
else:
self.append(detail)

Expand Down Expand Up @@ -74,6 +79,10 @@ def country(self) -> str:
"""
return self[2]

@property
def other(self) -> str:
return self[3:]


class AlbumResult(SearchResult):

Expand Down Expand Up @@ -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("<div></div>")
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)
32 changes: 32 additions & 0 deletions metallum/models/SimilarArtists.py
Original file line number Diff line number Diff line change
@@ -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 "<SimilarArtists: None>"
names = list(map(similar_artist_str, self))
s = " | ".join(names)
return "<SimilarArtists: {0}>".format(s)
66 changes: 64 additions & 2 deletions metallum/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
<SimilarArtists: Megadeth (488) | Testament (420) | Exodus (212) | Evile (206) | Anthrax (182) | Death Angel (148) | Diamond Head (119) | Xentrix (115) | Annihilator (111) | Newsted (108) | Metal Church (105) | Heathen (105) | Flotsam and Jetsam (104) | Slayer (71) | Trivium (70) | Overkill (66) | Artillery (58) | Mortal Sin (58) | Volbeat (55) | Sacred Reich (48) | Paradox (44) | Slammer (34) | Pantera (33) | Corrosion of Conformity (30) | Am I Blood (30) | Alice in Chains (25) | Stone (25) | Motörhead (21) | Dark Angel (20) | Vio-lence (20) | Meliah Rage (19) | Machine Head (18) | Onslaught (18) | Tantara (18) | Kreator (17) | Outrage (17) | Blitzkrieg (16) | Znöwhite (16) | Forbidden (15) | Suicidal Tendencies (15) | Cyclone Temple (15) | Whiplash (15) | Havok (14) | Defiance (14) | Accu§er (13) | Deliverance (13) | Lȧȧz Rockit (13) | Reign of Fury (13) | Woslom (13) | Kat (12) | Iced Earth (12) | 4Arm (12) | Acrassicauda (11) | Wrathchild America (11) | Sepultura (11) | Power Trip (11) | Apocalypse (11) | Ellefson (11) | Metal Allegiance (11) | Alcoholica (11) | Wildhunt (11) | Mercyful Fate (10) | Железный Поток (10) | Destruction (10) | Sufosia (10) | Nuclear Assault (9) | Tourniquet (9) | Shah (9) | Faith No More (8) | Channel Zero (8) | Mantic Ritual (8) | Dust Bolt (8) | Hammerhedd (8) | Eradikator (8) | Ramp (8) | Earthquake (8) | Anesthesia (8) | Inciter (8) | Hirax (7) | Shadows Fall (7) | Faith or Fear (7) | Game Over (7) | Holocaust (7) | Sweet Savage (7) | Blackened (7) | Morgana Lefay (7) | Hellraiser (7) | Razor (7) | Posehn (7) | Eternal Decision (6) | Armored Saint (6) | Austrian Death Machine (6) | Hatriot (6) | Vindicator (6) | Dublin Death Patrol (6) | Sylosis (6) | Angelus Apatrida (6) | Addictive (6) | Hunter (6) | Meshiaak (6) | Vader (6) | Living Sacrifice (6) | Thrashback (6) | Venom Inc. (6) | Athena (6) | Prong (5) | Danzig (5) | Thrashsteel (5) | Fallen Angel (5) | Lost Society (5) | Ekhymosis (5) | Attomica (5) | Raven (5) | In.Si.Dia (5) | Turbo (5) | Act of Defiance (5) | Hellripper (5) | Suicidal Angels (5) | Phantom Lord (5) | Sanctity (5) | Wargasm (5) | Seducer (5) | Calipash (5) | Detritus (5) | Enforcer (5) | Mason (5) | Tyrant's Reign (5) | Braindamage (5) | Vortex (5) | Abandoned (5) | Arbitrater (5) | Bleak House (5) | Metallic Ass (5) | Modifidious (5) | Apocalyptica (4) | The Worshyp (4) | Airdash (4) | Mezzrow (4) | White Zombie (4) | DBC (4) | Diamond Plate (4) | Equinox (4) | Excel (4) | Acrophet (4) | Black Track (4) | Panic (4) | Adamantine (4) | Critical Solution (4) | Perzonal War (4) | Phantasm (4) | Shredhead (4) | Steel Fury (4) | Alexander Palitsin (4) | I.N.C. (4) | Wrath (4) | Fight (4) | Kazjurol (4) | King Diamond (4) | Manditory (4) | Allegiance (4) | Altitudes & Attitude (4) | Dead On (4) | Fallout (4) | Hermética (4) | Lethal (4) | Tonic Breed (4) | Wiplash (4) | Animator (4) | Astharoth (4) | Disciples of Power (4) | Extrema (4) | Face of Anger (4) | Fatality (4) | Insecurity (4) | Nasty Savage (4) | Pentagram (4) | Potential Threat (4) | Prophecy (4) | Algebra (4) | Criminal (4) | Nuclear Simphony (4) | Oil (4) | Planleft (4) | Practice to Deceive (4) | Razgate (4) | Revtend (4) | Ritual Servant (4) | Space Chaser (4) | Stormdeath (4) | Teronation (4) | Victim (4) | Vingador (4) | Vulture (4) | Legion (4) | Flacmans Port (4) | Four Noses (4) | Half-Lit (4) | Sabbat (3) | Ancesttral (3) | Target (3) | Симфония Разрушения (3) | Piranha (3) | Hatrix (3) | Souls at Zero (3) | Toranaga (3) | Bitter End (3) | Demolition Train (3) | Killers (3) | Legion (3) | Rhythm of Fear (3) | Sacrifice (3) | Wolfpack Unleashed (3) | Anihilated (3) | Dethrone (3) | Viking (3) | Acridity (3) | Dissolved (3) | Dogma (3) | Exciter (3) | Forced Entry (3) | Nihilist (3) | Quorthon (3) | Taurus (3) | Yosh (3) | Abaxial (3) | Battalion (3) | Disaster Area (3) | Hate FX (3) | Horcas (3) | LawShed (3) | Prowler (3) | Strip Mind (3) | The Force (3) | Glenn Tipton (3) | Next (3) | Against (3) | Deliverance (3) | Hatchet (3) | HI-GH (3) | In Malice's Wake (3) | King Gizzard & the Lizard Wizard (3) | Stone Vengeance (3) | Terror Empire (3) | Thrash Bombz (3) | Thresher (3) | Aleister (3) | Alpha Warhead (3) | Amboog-a-Lard (3) | Anesthesia (3) | Blatant Disarray (3) | Chronical Disturbance (3) | Coldsteel (3) | Cro-Mags (3) | Crossbones (3) | Darkness (3) | DesExult (3) | Disturbed (3) | Drünkards (3) | E.S.T. (3) | Eradicator (3) | F5 (3) | Fierce Allegiance (3) | Filter (3) | Hostile Rage (3) | Incursion Dementa (3) | Jesus Freaks (3) | Metalord (3) | Mystrez (3) | Necrosis (3) | Night Viper (3) | Nightfyre (3) | Serpentor (3) | Total Annihilation (3) | Violator (3)>
"""

url = "band/ajax-recommendations/id/" + self.id + "/showMoreSimilar/1"
return SimilarArtists(url, SimilarArtistsResult)


class Track(object):

Expand Down Expand Up @@ -309,6 +320,9 @@ class Album(MetallumEntity):
def __init__(self, url):
super().__init__(url)

def __repr__(self):
return "<Album: {0}>".format(self.title)

@property
def id(self) -> str:
"""
Expand Down Expand Up @@ -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 "<SimilarArtist: {0}>".format(s)

def get(self) -> "Metallum":
return self._resultType(self.url)
63 changes: 62 additions & 1 deletion metallum/operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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."""
Expand All @@ -42,6 +43,7 @@ def band_search(
"year_created_to": "yearCreationTo",
"status": "status[]",
"label": "bandLabelName",
"additional_notes": "bandNotes",
"page_start": "iDisplayStart",
},
)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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",
},
)
Expand All @@ -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)
Empty file added tests/__init__.py
Empty file.
Loading

0 comments on commit 6b7c164

Please sign in to comment.