Coverage for an_website/soundboard/soundboard.py: 95.294%
85 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-10 18:56 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-10 18:56 +0000
1# This program is free software: you can redistribute it and/or modify
2# it under the terms of the GNU Affero General Public License as
3# published by the Free Software Foundation, either version 3 of the
4# License, or (at your option) any later version.
5#
6# This program is distributed in the hope that it will be useful,
7# but WITHOUT ANY WARRANTY; without even the implied warranty of
8# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9# GNU Affero General Public License for more details.
10#
11# You should have received a copy of the GNU Affero General Public License
12# along with this program. If not, see <https://www.gnu.org/licenses/>.
14"""The soundboard of the website."""
16from collections.abc import Callable, Iterable
17from functools import cache
18from typing import ClassVar
20from tornado.web import HTTPError
22from ..utils.request_handler import HTMLRequestHandler
23from .data import (
24 ALL_SOUNDS,
25 MAIN_PAGE_INFO,
26 PERSON_SHORTS,
27 PERSON_SOUNDS,
28 HeaderInfo,
29 Info,
30 Person,
31 SoundInfo,
32)
35@cache
36def get_rss_str(path: str, protocol_and_host: str) -> None | str:
37 """Return the RSS string for the given path."""
38 if path is not None:
39 path = path.lower()
41 if path in {None, "/", ""}:
42 _infos: Iterable[SoundInfo] = ALL_SOUNDS
43 elif path in PERSON_SOUNDS:
44 _infos = PERSON_SOUNDS[path]
45 else:
46 return None
47 return "\n".join(
48 sound_info.to_rss(protocol_and_host) for sound_info in _infos
49 )
52async def search_main_page_info(
53 check_func: Callable[[SoundInfo], bool],
54 info_list: Iterable[Info] = MAIN_PAGE_INFO,
55) -> list[Info]:
56 # pylint: disable=confusing-consecutive-elif
57 """Get an info list based on the query and the check_func and return it."""
58 found: list[Info] = []
59 for info in info_list:
60 if isinstance(info, SoundInfo):
61 if check_func(info):
62 found.append(info)
63 elif isinstance(info, HeaderInfo):
64 tag = info.tag
65 while ( # pylint: disable=while-used
66 len(found) > 0
67 and isinstance(last := found[-1], HeaderInfo)
68 and (
69 tag
70 in (
71 "h1", # if it gets to h3 this doesn't work as
72 # then this should also be done for h2 when the ones
73 # before are h3
74 last.tag,
75 )
76 )
77 ):
78 del found[-1]
79 found.append(info)
81 # pylint: disable=while-used
82 while len(found) > 0 and isinstance(found[-1], HeaderInfo):
83 del found[-1]
85 return found
88class SoundboardHTMLHandler(HTMLRequestHandler):
89 """The request handler for the HTML pages."""
91 async def get(self, path: str = "/", *, head: bool = False) -> None:
92 """Handle GET requests and generate the page content."""
93 if path is not None:
94 path = path.lower()
96 parsed_info = await self.parse_path(path)
97 if parsed_info is None:
98 raise HTTPError(404, reason="Page not found")
100 if head:
101 return
103 self.update_title_and_desc(path)
105 await self.render(
106 "pages/soundboard.html",
107 sound_info_list=parsed_info[0],
108 query=parsed_info[1],
109 feed_url=self.fix_url(
110 (
111 f"/soundboard/{path.strip('/')}/feed"
112 if path and path != "/" and path != "personen"
113 else "/soundboard/feed"
114 ),
115 ),
116 )
118 async def parse_path(
119 self, path: None | str
120 ) -> None | tuple[Iterable[Info], None | str]:
121 """Get an info list based on the path and return it with the query."""
122 if path in {None, "", "index", "/"}:
123 return MAIN_PAGE_INFO, None
125 if path in {"persons", "personen"}:
126 persons_list: list[Info] = []
127 for key, person_sounds in PERSON_SOUNDS.items():
128 persons_list.append(HeaderInfo(Person[key].value, type=Person))
129 persons_list += person_sounds
130 return persons_list, None
132 if path in {"search", "suche"}:
133 query = self.get_argument("q", "")
134 if not query:
135 return MAIN_PAGE_INFO, query
137 return (
138 await search_main_page_info(lambda info: info.contains(query)),
139 query,
140 )
142 if path in PERSON_SHORTS:
143 person = Person[path]
144 return (
145 await search_main_page_info(lambda info: info.person == person),
146 None,
147 )
149 return None
151 def update_title_and_desc(self, path: str) -> None:
152 """Update the title and description of the page."""
153 if path not in PERSON_SHORTS:
154 return
155 name = Person[path].value
156 if name.startswith("Das ") or name.startswith("Der "):
157 von_name = f"dem{name[3:]}"
158 no_article_name = name[4:]
159 elif name.startswith("Die "):
160 von_name = f"der{name[3:]}"
161 no_article_name = name[4:]
162 else:
163 von_name = name
164 no_article_name = name
166 self.short_title = f"Soundboard ({path.upper()})"
167 self.title = f"{no_article_name.replace(' ', '-')}-Soundboard"
168 self.description = (
169 "Ein Soundboard mit coolen Sprüchen und Sounds von "
170 f"{von_name} aus den Känguru-Chroniken"
171 )
174class SoundboardRSSHandler(SoundboardHTMLHandler):
175 """The request handler for the RSS feeds."""
177 POSSIBLE_CONTENT_TYPES: ClassVar[tuple[str, ...]] = (
178 "application/rss+xml",
179 "application/xml",
180 )
182 async def get(self, path: str = "/", *, head: bool = False) -> None:
183 """Handle GET requests and generate the feed content."""
184 rss_str = get_rss_str(
185 path, f"{self.request.protocol}://{self.request.host}"
186 )
188 if rss_str is not None:
189 if head:
190 return
191 self.update_title_and_desc(path)
192 return await self.render(
193 "rss/soundboard.xml",
194 found=True,
195 rss_str=rss_str,
196 )
197 self.set_status(404, reason="Feed not found")
198 return await self.render("rss/soundboard.xml", found=False, rss_str="")