swords 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
swords-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.3
2
+ Name: swords
3
+ Version: 0.1.0
4
+ Summary: Add your description here
5
+ Requires-Dist: click>=8.3.1
6
+ Requires-Dist: requests>=2.33.1
7
+ Requires-Dist: selectolax>=0.4.7
8
+ Requires-Python: >=3.14
9
+ Description-Content-Type: text/markdown
10
+
swords-0.1.0/README.md ADDED
File without changes
@@ -0,0 +1,23 @@
1
+ [project]
2
+ name = "swords"
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ requires-python = ">=3.14"
7
+ dependencies = [
8
+ "click>=8.3.1",
9
+ "requests>=2.33.1",
10
+ "selectolax>=0.4.7",
11
+ ]
12
+
13
+ [dependency-groups]
14
+ dev = [
15
+ "ruff>=0.15.9",
16
+ ]
17
+
18
+ [build-system]
19
+ requires = ["uv_build>=0.11.3,<0.12"]
20
+ build-backend = "uv_build"
21
+
22
+ [project.scripts]
23
+ swords = "swords.main:search"
File without changes
@@ -0,0 +1,107 @@
1
+ """A lightweight script to search Genius for song lyrics."""
2
+
3
+ from dataclasses import dataclass, fields
4
+ import re
5
+ from typing import Any
6
+ from urllib.parse import quote_plus
7
+
8
+ import click
9
+ from selectolax.lexbor import LexborHTMLParser
10
+ import requests
11
+
12
+
13
+ @dataclass
14
+ class Song:
15
+ """Represents song data from Genius."""
16
+
17
+ primary_artist_names: str
18
+ title: str
19
+ url: str
20
+
21
+ @classmethod
22
+ def from_dict(cls, data: dict[str, Any]) -> "Song":
23
+ class_fields = {field.name for field in fields(cls)}
24
+ filtered_data = {
25
+ key: value for key, value in data.items() if key in class_fields
26
+ }
27
+ return cls(**filtered_data)
28
+
29
+
30
+ def get_song_choices(query: str) -> list[Song]:
31
+ """Query Genius for songs that match the given search."""
32
+ response = requests.get(
33
+ "https://genius.com/api/search/multi",
34
+ params={"per_page": 5, "q": quote_plus(query)},
35
+ )
36
+ response.raise_for_status()
37
+ data = response.json()
38
+ results = []
39
+ for section in data["response"]["sections"]:
40
+ if section["type"] == "song":
41
+ for hit in section["hits"]:
42
+ song = Song.from_dict(hit["result"])
43
+ results.append(song)
44
+ return results
45
+
46
+
47
+ def get_lyrics(song: Song) -> str:
48
+ """Given a song, get Genius's corresponding lyrics using selectolax (Lexbor)."""
49
+ response = requests.get(song.url)
50
+ response.raise_for_status()
51
+
52
+ parser = LexborHTMLParser(response.text)
53
+ lyrics_list = []
54
+
55
+ # Find all lyric container divs
56
+ for section in parser.css('div[class^="Lyrics__Container"]'):
57
+ # Manually find and decompose the header containers
58
+ for header in section.css('div[class^="LyricsHeader__Container"]'):
59
+ header.decompose()
60
+
61
+ # Get text with a newline separator
62
+ # strip=True removes leading/trailing whitespace from the resulting string
63
+ text = section.text(separator="\n", strip=True)
64
+ if text:
65
+ lyrics_list.append(text)
66
+
67
+ return "\n".join(lyrics_list)
68
+
69
+
70
+ @click.command
71
+ @click.argument("query")
72
+ def search(query: str) -> None:
73
+ """
74
+ Takes a search query, finds up to 5 matches, and displays lyrics for a
75
+ chosen match.
76
+ """
77
+ click.echo("Searching...")
78
+ click.echo("")
79
+
80
+ song_choices = get_song_choices(query)
81
+ click.echo("Choices:")
82
+ for i, song in enumerate(song_choices):
83
+ click.echo(f"{i}. {song.title} - {song.primary_artist_names}")
84
+ click.echo("")
85
+
86
+ choice_id = click.prompt(
87
+ "Select an option",
88
+ type=click.IntRange(0, len(song_choices) - 1),
89
+ )
90
+ choice = song_choices[choice_id]
91
+ lyrics = get_lyrics(choice)
92
+
93
+ # Assume anything in brackets is a section header and format accordingly.
94
+ formatted_lyrics = re.sub(
95
+ r"(\[.*?\])",
96
+ lambda m: "\n" + click.style(m.group(0), bold=True),
97
+ lyrics,
98
+ )
99
+
100
+ click.echo("=" * 80)
101
+ click.echo("")
102
+ click.echo(f"{formatted_lyrics.strip()}")
103
+ click.echo("")
104
+
105
+
106
+ if __name__ == "__main__":
107
+ search()