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 +10 -0
- swords-0.1.0/README.md +0 -0
- swords-0.1.0/pyproject.toml +23 -0
- swords-0.1.0/src/swords/__init__.py +0 -0
- swords-0.1.0/src/swords/main.py +107 -0
swords-0.1.0/PKG-INFO
ADDED
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()
|