-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathSngFile.py
311 lines (255 loc) · 11.9 KB
/
SngFile.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
"""This file is used to define SngFile class and somee helper methods related to it's usage."""
import json
import logging
import logging.config
import re
from itertools import chain
from pathlib import Path
import SNG_DEFAULTS
from sng_utils import generate_verse_marker_from_line, validate_suspicious_encoding_str
from SngFileHeaderValidationPart import SngFileHeaderValidation
from SngFileParserPart import SngFileParserPart
config_file = Path("logging_config.json")
with config_file.open(encoding="utf-8") as f_in:
logging_config = json.load(f_in)
logging.config.dictConfig(config=logging_config)
logger = logging.getLogger(__name__)
class SngFile(SngFileParserPart, SngFileHeaderValidation):
"""Main class that defines one single SongBeamer SNG file."""
def __init__(self, filename: str, songbook_prefix: str = "") -> None:
"""Default Construction for a SNG File and it's params.
Args:
filename: filename with optional directory which should be opened
songbook_prefix: prefix of songbook e.g. EG. Defaults to "".
"""
super().__init__(filename=filename, songbook_prefix=songbook_prefix)
def fix_intro_slide(self) -> None:
"""Checks if Intro Slide exists as content block.
and adds in case one is required
Also ensures that Intro is part of VerseOrder
"""
if "Intro" not in self.header["VerseOrder"]:
self.header["VerseOrder"].insert(0, "Intro")
self.update_editor_because_content_modified()
logger.debug("Added Intro to VerseOrder of (%s)", self.filename)
if "Intro" not in self.content:
intro = {"Intro": [["Intro"], []]}
self.content = {**intro, **self.content}
self.update_editor_because_content_modified()
logger.debug("Added Intro Block to (%s)", self.filename)
def validate_content_slides_number_of_lines(
self, number_of_lines: int = 4, fix: bool = False
) -> bool:
"""Method that checks if slides need to contain # (default 4) lines except for last block which can have less.
and optionally fixes it
Params:
number_of_lines max number of lines allowed per slide
fix: if it should be attempt to fix itself
Returns:
True if something was fixed
"""
for verse_label, verse_block in self.content.items(): # Iterate all blocks
# any slide which (except last one) which does not have the correct number of lines is wrong
has_issues = any(
len(slide) != number_of_lines for slide in verse_block[1:-1]
)
# any last slide which has more lines than desired is wrong
has_issues |= len(verse_block[-1]) > number_of_lines
if has_issues and fix:
logger.debug(
"Fixing block length %s in (%s) to %s lines",
verse_label,
self.filename,
number_of_lines,
)
all_lines = list(
chain(*verse_block[1:])
) # Merge list of all text lines
# Remove all old text lines except for Verse Marker
self.content[verse_label] = [verse_block[0]]
# Append 4 lines per slide as one list - last slide will exceed available lines but simply fill available
for i in range(0, len(all_lines), number_of_lines):
self.content[verse_label].append(all_lines[i : i + number_of_lines])
has_issues = False
self.update_editor_because_content_modified()
if not has_issues:
continue
return False
return True
def validate_verse_numbers(self, fix: bool = False) -> bool:
"""Method which checks Verse Numbers for numeric parts that are non-standard - e.g. 1b.
* tries to remove letters in 2nd part if fix is enabled
* merges consecutive similar parts
does only work if versemarker exists!
"""
all_verses_valid = True
new_content = {}
for key, verse_block in self.content.items():
# if block starts with a verse label and some additional information
verse_label_list = verse_block[0]
is_valid_verse_label_list = self.is_valid_verse_label_list(verse_label_list)
if not is_valid_verse_label_list and fix:
# fix verse label
old_key = " ".join(verse_label_list)
new_number = re.sub(r"\D+", "", verse_label_list[1])
new_label = [verse_label_list[0], new_number]
new_key = " ".join(new_label)
# check if new block name already exists
if new_key in new_content:
logger.debug(
"\t Appending %s to existing Verse label %s",
old_key,
new_key,
)
# if yes, append content and remove old label from verse order
new_content[new_key].extend(verse_block[1:])
# remove old key from verse order (replacement already exists)
self.header["VerseOrder"] = [
item for item in self.header["VerseOrder"] if item != old_key
]
# If it's a new label after fix
else:
logger.debug(
"New Verse label from %s to %s in (%s)",
old_key,
new_key,
self.filename,
)
# if no, rename block in verse order and dict
self.header["VerseOrder"] = [
new_key if item == old_key else item
for item in self.header["VerseOrder"]
]
new_content[new_key] = [new_label] + verse_block[1:]
self.update_editor_because_content_modified()
continue
# Any regular block w/o versemarker
if not is_valid_verse_label_list:
all_verses_valid &= is_valid_verse_label_list
logger.debug(
"Invalid verse label %s not fixed in (%s)",
verse_label_list,
self.filename,
)
# Keep content because either error was logged or it was valid content
new_content[key] = verse_block
self.content = new_content
return all_verses_valid
def is_valid_verse_label_list(self, verse_label_list: str) -> bool:
"""Checks if a list extracted from SNG content could be a valid verse label.
Args:
verse_label_list: list from content which should be checked
Returns:
whether the line is a valid verse_label
"""
result = verse_label_list[0] in SNG_DEFAULTS.VerseMarker
if len(verse_label_list) > 1:
result &= verse_label_list[1].isdigit()
return result
def validate_suspicious_encoding(self, fix: bool = False) -> bool:
"""Function that checks the SNG content for suspicious characters which might be result of previous encoding errors.
utf8_as_iso dict is used to check for common occurances of utf-8 german Umlaut when read as iso8895-1
Params:
fix: if method should try to fix the encoding issues
Returns:
if no suspicious encoding exists
"""
# Check headers
valid_header = self.validate_suspicious_encoding_header(fix=fix)
# Check content
valid_content = self.validate_suspicious_encoding_content(fix=fix)
return valid_header & valid_content
def validate_suspicious_encoding_content(self, fix: bool = False) -> bool:
"""Function that checks the SNG CONTENT for suspicious characters which might be result of previous encoding errors.
utf8_as_iso dict is used to check for common occurances of utf-8 german Umlaut when read as iso8895-1
Only lines upon first "non-fixed" line will be checked
Params:
fix: if method should try to fix the encoding issues
Returns:
if no suspicious encoding exists
"""
for verse in self.content.values():
text_slides = verse[1:] # skip verse marker
for slide_no, slide in enumerate(text_slides):
for line_no, line in enumerate(slide):
is_valid, checked_line = validate_suspicious_encoding_str(
line, fix=fix
)
if not is_valid:
logger.info(
"Found problematic encoding [%s] in %s %s slide line %s of %s",
checked_line,
verse[1],
slide_no,
line_no,
self.filename,
)
return False # if not fixed can abort on first error
return True
def get_id(self) -> int:
"""Helper function accessing ID in header mapping not existant to -1.
Returns:
id
"""
if "id" in self.header:
return int(self.header["id"])
return -1
def set_id(self, new_id: int) -> None:
"""Helper function for easy access to write header ID.
Args:
new_id: ID (ideally ChurchTools) which should be set for the specific song
"""
self.header["id"] = str(new_id)
self.update_editor_because_content_modified()
def generate_verses_from_unknown(self) -> dict | None:
"""Method used to split any "Unknown" Block into auto detected segments of numeric verses or chorus.
Changes the songs VerseOrder and content blocks if possible
Does not change parts of verses that already have a verse label !
Returns:
dict of blocks or None
"""
logger.info("Started generate_verses_from_unknown()")
old_block = self.content.get("Unknown")
if not old_block:
return None
current_block_name = "Unknown"
new_blocks = {"Unknown": []}
for slide in old_block:
first_line = slide[0]
new_block_name, new_text = generate_verse_marker_from_line(first_line)
# not new verse add to last block as next slide
if not new_block_name:
new_blocks[current_block_name].append(slide)
continue
current_block_name = " ".join(new_block_name).strip()
logger.debug(
"Detected new '%s' in 'Unknown' block of (%s)",
current_block_name,
self.filename,
)
# add remaining text lines from slide
new_slides = [new_text] + slide[1:]
new_blocks[current_block_name] = [new_block_name, new_slides]
# Cleanup Legacy if not used
if len(new_blocks["Unknown"]) == 1:
del new_blocks["Unknown"]
new_block_keys = list(new_blocks.keys())
# look for position of "Unknown" and replace
position_of_unknown = self.header["VerseOrder"].index("Unknown")
self.header["VerseOrder"][
position_of_unknown : position_of_unknown + 1
] = new_block_keys
logger.info(
"Added new '%s' in Verse Order of (%s)", new_block_keys, self.filename
)
self.content.pop("Unknown")
self.content.update(new_blocks)
# TODO (bensteUEM): check what happens if already exists
# https://github.com/bensteUEM/SongBeamerQS/issues/35
logger.info(
"Replaced 'Unknown' with '%s' in Verse Order of (%s)",
new_block_keys,
self.filename,
)
self.update_editor_because_content_modified()
return new_blocks