From bbf0f1bf0aea3722555ec7ba3b8aa6f0f93d317e Mon Sep 17 00:00:00 2001 From: Martin Date: Sun, 9 Aug 2026 12:19:14 +0200 Subject: [PATCH] Add historical radioeins parser support --- radioeins_top100.py | 45 +++++++++++++++++++++++++++ tests/fixtures/andreas-loeffler.html | 18 +++++++++++ tests/fixtures/andreas-ulrich.html | 19 ++++++++++++ tests/fixtures/aydo-abay.html | 19 ++++++++++++ tests/test_radioeins_top100.py | 46 ++++++++++++++++++++++++---- 5 files changed, 141 insertions(+), 6 deletions(-) create mode 100644 tests/fixtures/andreas-loeffler.html create mode 100644 tests/fixtures/andreas-ulrich.html create mode 100644 tests/fixtures/aydo-abay.html diff --git a/radioeins_top100.py b/radioeins_top100.py index 8cbbdf2..c237e52 100644 --- a/radioeins_top100.py +++ b/radioeins_top100.py @@ -160,6 +160,47 @@ def extract_votes_from_table( return [rows_by_rank[rank] for rank in range(1, 11)] +def extract_votes_from_historical_text( + soup, + juror: str, + source_url: str, +) -> list[Vote] | None: + rows_by_rank: dict[int, Vote] = {} + + for element in soup.find_all(["tr", "li", "p"]): + text = clean(element.get_text(" ", strip=True)) + match = re.fullmatch(r"(10|[1-9])\.\s*(.+)", text) + if not match: + continue + + rank = int(match.group(1)) + entry = match.group(2) + artist, separator, title = entry.partition(":") + if not separator: + artist, separator, title = entry.partition(" - ") + if not separator: + continue + + artist = clean(artist) + title = clean(title) + if not artist or not title: + continue + + rows_by_rank[rank] = Vote( + juror=juror, + rank=rank, + points=POINTS[rank], + artist=artist, + title=title, + source_url=source_url, + ) + + if set(rows_by_rank) != set(range(1, 11)): + return None + + return [rows_by_rank[rank] for rank in range(1, 11)] + + def parse_jury_page( html: str, fallback_name: str, @@ -173,6 +214,10 @@ def parse_jury_page( if votes is not None: return votes + votes = extract_votes_from_historical_text(soup, juror, source_url) + if votes is not None: + return votes + raise RuntimeError("Keine vollständige Top-10-Tabelle gefunden") diff --git a/tests/fixtures/andreas-loeffler.html b/tests/fixtures/andreas-loeffler.html new file mode 100644 index 0000000..b1cef29 --- /dev/null +++ b/tests/fixtures/andreas-loeffler.html @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + +
1. Die Ärzte: Schrei nach Liebe  
2. Peter Fox: Haus am See  
3. Fettes Brot: Jein  
4. Die Toten Hosen: Hier kommt Alex  
5. Rammstein: Engel  
6. Marteria: OMG  
7. Herbert Grönemeyer: Mensch  
8. Sportfreunde Stiller: Ein Kompliment  
9. Falco: Der Kommissar  
10. Rio Reiser: Junimond  
+ + diff --git a/tests/fixtures/andreas-ulrich.html b/tests/fixtures/andreas-ulrich.html new file mode 100644 index 0000000..681a0b7 --- /dev/null +++ b/tests/fixtures/andreas-ulrich.html @@ -0,0 +1,19 @@ + + + + +

Meine Top 10

+ + + + + + + + + + + +
1. City: Am Fenster  
2. Rosenstolz: Königin  
3. Marteria: Kids ( 2 Finger am Kopf)  
4. Ton Steine Scherben: Macht kaputt was euch kaputt macht  
5. Heinz Rudolf Kunze: Wunderkinder  
6. Annenmaykantrereit: Pocahontas
  
7. Die Toten Hosen: Tage wie diese
  
8. Pankow: Langeweile
  
9. Rio Reiser: Junimond
  
10. Wir Sind Helden: Müssen nur wollen  
+ + diff --git a/tests/fixtures/aydo-abay.html b/tests/fixtures/aydo-abay.html new file mode 100644 index 0000000..3ac4df4 --- /dev/null +++ b/tests/fixtures/aydo-abay.html @@ -0,0 +1,19 @@ + + + + +

Meine Top 10

+ + + + + + + + + + + +
1. Kraftwerk: Radioaktivität  
2. Trio: Los Paul  
3. Udo Lindenberg: Leider nur ein Vakuum  
4. Dauerfisch: So gut  
5. D.A.F. : Der Mussolini  
6. Element of Crime: Am Ende denk ich immer nur an dich  
7. Porf: Schönes Bild Wenn  
8. Boxhmasters - Mogli  
9. All Diese Gewalt: Jeder Traum eine Falle  
10. Udo Jürgens: Merci Chérie  
+ + diff --git a/tests/test_radioeins_top100.py b/tests/test_radioeins_top100.py index 6351c4c..baba79e 100644 --- a/tests/test_radioeins_top100.py +++ b/tests/test_radioeins_top100.py @@ -5,6 +5,14 @@ from radioeins_top100 import POINTS, derive_output_dir, parse_jury_page class RadioeinsTop100Tests(unittest.TestCase): + def parse_fixture(self, name): + fixture = Path(__file__).parent / "fixtures" / name + return parse_jury_page( + fixture.read_text(encoding="utf-8"), + fallback_name="Fallback", + source_url=f"https://example.test/{name}", + ) + def test_output_directory_is_derived_from_url_path(self): base = "https://www.radioeins.de/musik/top_100/2026" self.assertEqual(derive_output_dir(f"{base}/zeit/"), Path("output_zeit")) @@ -20,12 +28,7 @@ class RadioeinsTop100Tests(unittest.TestCase): ) def test_parse_existing_jury_fixture(self): - fixture = Path(__file__).parent / "fixtures" / "aditya-sharma.html" - votes = parse_jury_page( - fixture.read_text(encoding="utf-8"), - fallback_name="Fallback", - source_url="https://example.test/aditya-sharma.html", - ) + votes = self.parse_fixture("aditya-sharma.html") self.assertEqual(len(votes), 10) self.assertEqual(votes[0].juror, "Aditya Sharma") @@ -33,6 +36,37 @@ class RadioeinsTop100Tests(unittest.TestCase): self.assertEqual((votes[1].artist, votes[1].title), ("Samy Deluxe", "Weck Mich Auf")) self.assertEqual((votes[-1].rank, votes[-1].points), (10, 1)) + def test_parse_historical_page_without_heading(self): + votes = self.parse_fixture("andreas-loeffler.html") + + self.assertEqual(len(votes), 10) + self.assertEqual(votes[0].juror, "Andreas Löffler") + self.assertEqual((votes[0].artist, votes[0].title), ("Die Ärzte", "Schrei nach Liebe")) + self.assertEqual((votes[-1].rank, votes[-1].points), (10, 1)) + + def test_parse_historical_page_with_html_spacing(self): + votes = self.parse_fixture("andreas-ulrich.html") + + self.assertEqual(len(votes), 10) + self.assertEqual(votes[5].title, "Pocahontas") + self.assertEqual(votes[-1].artist, "Wir Sind Helden") + + def test_historical_title_preserves_additional_colons(self): + fixture = Path(__file__).parent / "fixtures" / "andreas-loeffler.html" + html = fixture.read_text(encoding="utf-8").replace( + "Schrei nach Liebe", "Live: Schrei nach Liebe" + ) + votes = parse_jury_page(html, "Fallback", "https://example.test/list.html") + + self.assertEqual(votes[0].title, "Live: Schrei nach Liebe") + + def test_historical_page_accepts_spaced_hyphen_separator(self): + votes = self.parse_fixture("aydo-abay.html") + + self.assertEqual(len(votes), 10) + self.assertEqual(votes[7].artist, "Boxhmasters") + self.assertEqual(votes[7].title, "Mogli") + if __name__ == "__main__": unittest.main()