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

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/>. 

13 

14"""The soundboard of the website.""" 

15 

16from collections.abc import Callable, Iterable 

17from functools import cache 

18from typing import ClassVar 

19 

20from tornado.web import HTTPError 

21 

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) 

33 

34 

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

40 

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 ) 

50 

51 

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) 

80 

81 # pylint: disable=while-used 

82 while len(found) > 0 and isinstance(found[-1], HeaderInfo): 

83 del found[-1] 

84 

85 return found 

86 

87 

88class SoundboardHTMLHandler(HTMLRequestHandler): 

89 """The request handler for the HTML pages.""" 

90 

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

95 

96 parsed_info = await self.parse_path(path) 

97 if parsed_info is None: 

98 raise HTTPError(404, reason="Page not found") 

99 

100 if head: 

101 return 

102 

103 self.update_title_and_desc(path) 

104 

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 ) 

117 

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 

124 

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 

131 

132 if path in {"search", "suche"}: 

133 query = self.get_argument("q", "") 

134 if not query: 

135 return MAIN_PAGE_INFO, query 

136 

137 return ( 

138 await search_main_page_info(lambda info: info.contains(query)), 

139 query, 

140 ) 

141 

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 ) 

148 

149 return None 

150 

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 

165 

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 ) 

172 

173 

174class SoundboardRSSHandler(SoundboardHTMLHandler): 

175 """The request handler for the RSS feeds.""" 

176 

177 POSSIBLE_CONTENT_TYPES: ClassVar[tuple[str, ...]] = ( 

178 "application/rss+xml", 

179 "application/xml", 

180 ) 

181 

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 ) 

187 

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="")