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()