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
« 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/>.
14"""Create the required stuff for the soundboard from the info in info.json."""
16from __future__ import annotations
18import email.utils
19from collections.abc import Callable
20from dataclasses import dataclass, field
21from enum import Enum
22from typing import Final
24import orjson as json
25import regex
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
31DIR: Final = ROOT_DIR / "soundboard"
32with (DIR / "info.json").open("r", encoding="UTF-8") as file:
33 info = json.loads(file.read())
35# {"muk": "Marc-Uwe Kling", ...}
36person_dict: dict[str, str] = info["personen"]
37Person = Enum("Person", {**person_dict}, module=__name__) # type: ignore[misc]
39PERSON_SHORTS: tuple[str, ...] = tuple(person_dict.keys())
41books: list[str] = []
42chapters: list[str] = []
43for book in info["bücher"]:
44 books.append(book["name"])
46 for chapter in book["kapitel"]:
47 chapters.append(chapter["name"])
50Book = Enum("Book", [*books], module=__name__) # type: ignore[misc]
51Chapter = Enum("Chapter", [*chapters], module=__name__) # type: ignore[misc]
54del books, chapters, person_dict
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
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)
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 )
75 return text
78@dataclass(frozen=True, slots=True)
79class Info:
80 """Info class that is used as a base for HeaderInfo and SoundInfo."""
82 text: str
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)
95@dataclass(frozen=True, slots=True)
96class HeaderInfo(Info):
97 """A header with a tag and href to itself."""
99 tag: str = "h1"
100 type: type[Book | Chapter | Person] = Book
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.
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 )
127@dataclass(frozen=True, slots=True)
128class SoundInfo(Info):
129 """The information about a sound."""
131 book: Book
132 chapter: Chapter
133 person: Person
135 filename: str = field(init=False)
136 pub_date: str = field(init=False)
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 )
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
163 content = " ".join([self.chapter.name, self.person.value, self.text])
164 content = replace_umlauts(content.lower())
166 return not any(
167 word not in content
168 for word in replace_umlauts(string.lower()).split(" ")
169 )
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 )
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 )
228all_sounds: list[SoundInfo] = []
229main_page_info: list[Info] = []
230PERSON_SOUNDS: Final[dict[str, list[SoundInfo]]] = {}
232for book_info in info["bücher"]:
233 book = Book[book_info["name"]]
234 main_page_info.append(HeaderInfo(book.name, "h1", Book))
236 for chapter_info in book_info["kapitel"]:
237 chapter = Chapter[chapter_info["name"]]
238 main_page_info.append(HeaderInfo(chapter.name, "h2", Chapter))
240 for file_text in chapter_info["dateien"]:
241 person_short = file_text.split("-")[0]
242 person = Person[person_short]
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)
249# convert to tuple for immutability
250ALL_SOUNDS: Final[tuple[SoundInfo, ...]] = tuple(all_sounds)
251MAIN_PAGE_INFO: Final[tuple[Info, ...]] = tuple(main_page_info)
253del all_sounds, main_page_info