Skip to content

Commit e0bde42

Browse files
committed
feat: add fetch_lyrics param to search_album
Adds fetch_lyrics: bool = True to search_album(). When False, lyrics scraping is skipped for all tracks, returning only metadata. Reduces time for a 10-track album from ~12s to ~2s. Fixes #217
1 parent 3538bb1 commit e0bde42

4 files changed

Lines changed: 102 additions & 4 deletions

File tree

lyricsgenius/genius.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,7 @@ def search_album(
352352
album_id: int | None = None,
353353
get_full_info: bool = True,
354354
text_format: TextFormatT | None = None,
355+
fetch_lyrics: bool = True,
355356
) -> Album | None:
356357
"""Searches for a specific album and gets its songs.
357358
@@ -365,6 +366,10 @@ def search_album(
365366
for the album (slower if no album_id present).
366367
text_format (:obj:`str`, optional): Text format of the results
367368
('dom', 'html', 'markdown' or 'plain').
369+
fetch_lyrics (:obj:`bool`, optional): If `True` (default), scrapes
370+
lyrics for each track. Set to `False` to skip lyrics fetching
371+
and return only track metadata — significantly faster for large
372+
albums.
368373
369374
Returns:
370375
:class:`Album <types.Album>` \\| :obj:`None`: On success,
@@ -431,8 +436,10 @@ def search_album(
431436
for track_data in tracks_list_response["tracks"]:
432437
song_info = track_data["song"]
433438
song_lyrics = None
434-
if song_info["lyrics_state"] == "complete" and not song_info.get(
435-
"instrumental"
439+
if (
440+
fetch_lyrics
441+
and song_info["lyrics_state"] == "complete"
442+
and not song_info.get("instrumental")
436443
):
437444
song_lyrics = self.lyrics(song_url=song_info["url"])
438445

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "lyricsgenius"
7-
version = "3.9.0"
7+
version = "3.9.1"
88
dependencies = ["beautifulsoup4>=4.12.3", "requests>=2.27.1"]
99
requires-python = ">=3.11"
1010
authors = [{ name = "John W. R. Miller", email = "john.w.millr+lg@gmail.com" }]

tests/test_album.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import pytest
88

9+
from lyricsgenius import Genius
910
from lyricsgenius.types import Album, Artist, Song
1011
from lyricsgenius.utils import sanitize_filename
1112

@@ -213,3 +214,93 @@ def test_saving_txt_file(album_object: Album, tmp_path: Path) -> None:
213214
content_after_overwrite = expected_filepath.read_text()
214215
assert "Overwritten TXT Test" in content_after_overwrite, content_after_overwrite
215216
album_object.tracks[0][1].lyrics = original_lyrics
217+
218+
219+
@pytest.fixture
220+
def genius_client() -> Genius:
221+
return Genius("dummy_access_token", verbose=False, sleep_time=0)
222+
223+
224+
def _make_search_all_response(album_data: dict[str, Any]) -> dict[str, Any]:
225+
hit = {"index": "album", "result": album_data}
226+
return {
227+
"sections": [
228+
{"type": "top_hits", "hits": [hit]},
229+
{"type": "album", "hits": [hit]},
230+
]
231+
}
232+
233+
234+
def _make_album_tracks_response(songs: list[dict[str, Any]]) -> dict[str, Any]:
235+
return {
236+
"tracks": [{"song": s, "number": i + 1} for i, s in enumerate(songs)],
237+
"next_page": None,
238+
}
239+
240+
241+
def test_fetch_lyrics_true_calls_lyrics(
242+
genius_client: Genius,
243+
mock_album_data: dict[str, Any],
244+
mock_track_data_list: list[dict[str, Any]],
245+
) -> None:
246+
"""When fetch_lyrics=True (default), lyrics() should be called for each track."""
247+
with (
248+
mock.patch.object(
249+
genius_client,
250+
"search_all",
251+
return_value=_make_search_all_response(mock_album_data),
252+
),
253+
mock.patch.object(
254+
genius_client, "album", return_value={"album": mock_album_data}
255+
),
256+
mock.patch.object(
257+
genius_client,
258+
"album_tracks",
259+
return_value=_make_album_tracks_response(mock_track_data_list),
260+
),
261+
mock.patch.object(
262+
genius_client, "lyrics", return_value="some lyrics"
263+
) as mock_lyrics,
264+
):
265+
result = genius_client.search_album(
266+
name=mock_album_data["name"], fetch_lyrics=True
267+
)
268+
269+
assert result is not None
270+
assert mock_lyrics.call_count == len(mock_track_data_list)
271+
for _, track in result.tracks:
272+
assert track.lyrics == "some lyrics"
273+
274+
275+
def test_fetch_lyrics_false_skips_lyrics(
276+
genius_client: Genius,
277+
mock_album_data: dict[str, Any],
278+
mock_track_data_list: list[dict[str, Any]],
279+
) -> None:
280+
"""When fetch_lyrics=False, lyrics() is not called and tracks have empty lyrics."""
281+
with (
282+
mock.patch.object(
283+
genius_client,
284+
"search_all",
285+
return_value=_make_search_all_response(mock_album_data),
286+
),
287+
mock.patch.object(
288+
genius_client, "album", return_value={"album": mock_album_data}
289+
),
290+
mock.patch.object(
291+
genius_client,
292+
"album_tracks",
293+
return_value=_make_album_tracks_response(mock_track_data_list),
294+
),
295+
mock.patch.object(
296+
genius_client, "lyrics", return_value="some lyrics"
297+
) as mock_lyrics,
298+
):
299+
result = genius_client.search_album(
300+
name=mock_album_data["name"], fetch_lyrics=False
301+
)
302+
303+
assert result is not None
304+
assert mock_lyrics.call_count == 0
305+
for _, track in result.tracks:
306+
assert track.lyrics == ""

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)