Coverage for an_website/soundboard/data.py: 87.255%

102 statements  

« prev     ^ index     » next       coverage.py v7.6.4, created at 2024-11-16 19: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"""Create the required stuff for the soundboard from the info in info.json.""" 

15 

16from __future__ import annotations 

17 

18import email.utils 

19from collections.abc import Callable 

20from dataclasses import dataclass, field 

21from enum import Enum 

22from typing import Final 

23 

24import orjson as json 

25import regex 

26 

27from .. import DIR as ROOT_DIR 

28from ..utils.static_file_handling import hash_file 

29from ..utils.utils import name_to_id, replace_umlauts, size_of_file 

30 

31DIR: Final = ROOT_DIR / "soundboard" 

32with (DIR / "info.json").open("r", encoding="UTF-8") as file: 

33 info = json.loads(file.read()) 

34 

35# {"muk": "Marc-Uwe Kling", ...} 

36person_dict: dict[str, str] = info["personen"] 

37Person = Enum("Person", {**person_dict}, module=__name__) # type: ignore[misc] 

38 

39PERSON_SHORTS: tuple[str, ...] = tuple(person_dict.keys()) 

40 

41books: list[str] = [] 

42chapters: list[str] = [] 

43for book in info["bücher"]: 

44 books.append(book["name"]) 

45 

46 for chapter in book["kapitel"]: 

47 chapters.append(chapter["name"]) 

48 

49 

50Book = Enum("Book", [*books], module=__name__) # type: ignore[misc] 

51Chapter = Enum("Chapter", [*chapters], module=__name__) # type: ignore[misc] 

52 

53 

54del books, chapters, person_dict 

55 

56 

57def mark_query(text: str, query: None | str) -> str: 

58 """Replace the instances of the query with itself in a div.""" 

59 if not query: 

60 return text 

61 

62 query = regex.sub("(ä|ae)", "(ä|ae)", query.lower()) 

63 query = regex.sub("(ö|oe)", "(ö|oe)", query) 

64 query = regex.sub("(ü|ue)", "(ü|ue)", query) 

65 query = regex.sub("(ü|ue)", "(ü|ue)", query) 

66 

67 for word in query.split(" "): 

68 text = regex.sub( 

69 word, 

70 lambda match: f'<div class="marked">{match[0]}</div>', 

71 text, 

72 regex.IGNORECASE, 

73 ) 

74 

75 return text 

76 

77 

78@dataclass(frozen=True, slots=True) 

79class Info: 

80 """Info class that is used as a base for HeaderInfo and SoundInfo.""" 

81 

82 text: str 

83 

84 def to_html( 

85 self, 

86 fix_url_func: Callable[ # pylint: disable=unused-argument 

87 [str], str 

88 ] = lambda url: url, 

89 query: None | str = None, 

90 ) -> str: 

91 """Return the text of the info and mark the query.""" 

92 return mark_query(self.text, query) 

93 

94 

95@dataclass(frozen=True, slots=True) 

96class HeaderInfo(Info): 

97 """A header with a tag and href to itself.""" 

98 

99 tag: str = "h1" 

100 type: type[Book | Chapter | Person] = Book 

101 

102 def to_html( 

103 self, 

104 fix_url_func: Callable[[str], str] = lambda url: url, 

105 query: None | str = None, 

106 ) -> str: 

107 """ 

108 Return an HTML element with the tag and the content of the HeaderInfo. 

109 

110 The HTML element gets an id and a href 

111 with a # to itself based on the text content. 

112 """ 

113 id_ = name_to_id(self.text) 

114 text = ( 

115 self.text # no need to mark query if type 

116 # is book as the book title is excluded from the search 

117 if self.type == Book 

118 else mark_query(self.text, query) 

119 ) 

120 return ( 

121 f"<{self.tag} id={id_!r}>" 

122 f"{text}<a no-dynload href='#{id_}' class='header-id-link'></a>" 

123 f"</{self.tag}>" 

124 ) 

125 

126 

127@dataclass(frozen=True, slots=True) 

128class SoundInfo(Info): 

129 """The information about a sound.""" 

130 

131 book: Book 

132 chapter: Chapter 

133 person: Person 

134 

135 filename: str = field(init=False) 

136 pub_date: str = field(init=False) 

137 

138 def __post_init__(self) -> None: 

139 """Init post.""" 

140 person_, timestamp, text = self.text.split("-", 2) 

141 object.__setattr__( 

142 self, 

143 "filename", 

144 regex.sub( 

145 r"[^a-z0-9_-]+", 

146 "", 

147 replace_umlauts( 

148 (person_ + "-" + text).lower().replace(" ", "_") 

149 ), 

150 ), 

151 ) 

152 object.__setattr__(self, "text", text) 

153 # convert seconds since epoch to readable timestamp 

154 object.__setattr__( 

155 self, "pub_date", email.utils.formatdate(float(timestamp), True) 

156 ) 

157 

158 def contains(self, string: None | str) -> bool: 

159 """Check whether this sound info contains a given string.""" 

160 if string is None: 

161 return False 

162 

163 content = " ".join([self.chapter.name, self.person.value, self.text]) 

164 content = replace_umlauts(content.lower()) 

165 

166 return not any( 

167 word not in content 

168 for word in replace_umlauts(string.lower()).split(" ") 

169 ) 

170 

171 def to_html( 

172 self, 

173 fix_url_func: Callable[[str], str] = lambda url: url, 

174 query: None | str = None, 

175 ) -> str: 

176 """Parse the info to a list element with an audio element.""" 

177 file = self.filename # pylint: disable=redefined-outer-name 

178 href = fix_url_func(f"/soundboard/{self.person.name}") 

179 path = f"files/{file}.mp3" 

180 file_url = f"/soundboard/{path}?v={hash_file(DIR / path)}" 

181 return ( 

182 f"<li id={file!r}>" 

183 f"<a href={href!r} class='a_hover'>" 

184 f"{mark_query(self.person.value, query)}" 

185 "</a>" 

186 ": »" 

187 f"<a no-dynload href={file_url!r} class='quote-a'>" 

188 f"{mark_query(self.text, query)}" 

189 "</a>" 

190 "«" 

191 "<br>" 

192 '<audio controls preload="none">' 

193 f"<source src={file_url!r} type='audio/mpeg'>" 

194 "</audio>" 

195 "</li>" 

196 ) 

197 

198 def to_rss(self, url: None | str) -> str: 

199 """Parse the info to a RSS item.""" 

200 filename = self.filename 

201 rel_path = f"files/{filename}.mp3" 

202 abs_path = DIR / rel_path 

203 file_size = size_of_file(abs_path) 

204 link = f"/soundboard/{rel_path}" 

205 text = self.text 

206 if url is not None: 

207 link = url + link 

208 return ( 

209 "<item>" 

210 "<title>" 

211 f"[{self.book.name} - {self.chapter.name}] " 

212 f"{self.person.value}: »{text}«" 

213 "</title>" 

214 f"<quote>{text}</quote>" 

215 f"<book>{self.book.name}</book>" 

216 f"<chapter>{self.chapter.name}</chapter>" 

217 f"<person>{self.person.value}</person>" 

218 f"<link>{link}</link>" 

219 f"<enclosure url={link!r} type='audio/mpeg'" 

220 f" length={str(file_size)!r}>" 

221 "</enclosure>" 

222 f"<guid>{filename}</guid>" 

223 f"<pubDate>{self.pub_date}</pubDate>" 

224 "</item>" 

225 ) 

226 

227 

228all_sounds: list[SoundInfo] = [] 

229main_page_info: list[Info] = [] 

230PERSON_SOUNDS: Final[dict[str, list[SoundInfo]]] = {} 

231 

232for book_info in info["bücher"]: 

233 book = Book[book_info["name"]] 

234 main_page_info.append(HeaderInfo(book.name, "h1", Book)) 

235 

236 for chapter_info in book_info["kapitel"]: 

237 chapter = Chapter[chapter_info["name"]] 

238 main_page_info.append(HeaderInfo(chapter.name, "h2", Chapter)) 

239 

240 for file_text in chapter_info["dateien"]: 

241 person_short = file_text.split("-")[0] 

242 person = Person[person_short] 

243 

244 sound_info = SoundInfo(file_text, book, chapter, person) 

245 all_sounds.append(sound_info) 

246 main_page_info.append(sound_info) 

247 PERSON_SOUNDS.setdefault(person_short, []).append(sound_info) 

248 

249# convert to tuple for immutability 

250ALL_SOUNDS: Final[tuple[SoundInfo, ...]] = tuple(all_sounds) 

251MAIN_PAGE_INFO: Final[tuple[Info, ...]] = tuple(main_page_info) 

252 

253del all_sounds, main_page_info