openm3u8 7.0.0__cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
openm3u8/parser.py ADDED
@@ -0,0 +1,786 @@
1
+ # Copyright 2014 Globo.com Player authors. All rights reserved.
2
+ # Modifications Copyright (c) 2026 Wurl.
3
+ # Use of this source code is governed by a MIT License
4
+ # license that can be found in the LICENSE file.
5
+
6
+ import itertools
7
+ import re
8
+ from datetime import datetime, timedelta
9
+
10
+ try:
11
+ from backports.datetime_fromisoformat import MonkeyPatch
12
+
13
+ MonkeyPatch.patch_fromisoformat()
14
+ except ImportError:
15
+ pass
16
+
17
+
18
+ from openm3u8 import protocol, version_matching
19
+
20
+ """
21
+ http://tools.ietf.org/html/draft-pantos-http-live-streaming-08#section-3.2
22
+ http://stackoverflow.com/questions/2785755/how-to-split-but-ignore-separators-in-quoted-strings-in-python
23
+ """
24
+ ATTRIBUTELISTPATTERN = re.compile(r"""((?:[^,"']|"[^"]*"|'[^']*')+)""")
25
+
26
+
27
+ def cast_date_time(value):
28
+ return datetime.fromisoformat(value)
29
+
30
+
31
+ def format_date_time(value, **kwargs):
32
+ return value.isoformat(**kwargs)
33
+
34
+
35
+ class ParseError(Exception):
36
+ def __init__(self, lineno, line):
37
+ self.lineno = lineno
38
+ self.line = line
39
+
40
+ def __str__(self):
41
+ return "Syntax error in manifest on line %d: %s" % (self.lineno, self.line)
42
+
43
+
44
+ def parse(content, strict=False, custom_tags_parser=None):
45
+ """
46
+ Given a M3U8 playlist content returns a dictionary with all data found
47
+ """
48
+ data = {
49
+ "media_sequence": 0,
50
+ "is_variant": False,
51
+ "is_endlist": False,
52
+ "is_i_frames_only": False,
53
+ "is_independent_segments": False,
54
+ "is_images_only": False,
55
+ "playlist_type": None,
56
+ "playlists": [],
57
+ "segments": [],
58
+ "iframe_playlists": [],
59
+ "image_playlists": [],
60
+ "tiles": [],
61
+ "media": [],
62
+ "keys": [],
63
+ "rendition_reports": [],
64
+ "skip": {},
65
+ "part_inf": {},
66
+ "session_data": [],
67
+ "session_keys": [],
68
+ "segment_map": [],
69
+ }
70
+
71
+ state = {
72
+ "expect_segment": False,
73
+ "expect_playlist": False,
74
+ "current_key": None,
75
+ "current_segment_map": None,
76
+ }
77
+
78
+ lines = string_to_lines(content)
79
+ if strict:
80
+ found_errors = version_matching.validate(lines)
81
+
82
+ if len(found_errors) > 0:
83
+ raise Exception(found_errors)
84
+
85
+ for lineno, line in enumerate(lines, 1):
86
+ line = line.strip()
87
+ parse_kwargs = {
88
+ "line": line,
89
+ "lineno": lineno,
90
+ "data": data,
91
+ "state": state,
92
+ "strict": strict,
93
+ }
94
+
95
+ # Blank lines are ignored.
96
+ if not line:
97
+ continue
98
+
99
+ # Call custom parser if needed
100
+ if line.startswith("#") and callable(custom_tags_parser):
101
+ go_to_next_line = custom_tags_parser(line, lineno, data, state)
102
+
103
+ # Do not try to parse other standard tags on this line if custom_tags_parser
104
+ # function returns `True`
105
+ if go_to_next_line:
106
+ continue
107
+
108
+ # Fast-path: dispatch based on tag token up to first ':' (or full tag if none)
109
+ if line.startswith("#"):
110
+ tag = line.split(":", 1)[0]
111
+ handler = DISPATCH.get(tag)
112
+ if handler is not None:
113
+ handler(**parse_kwargs)
114
+ continue
115
+ # #EXTM3U should be present; ignore if seen
116
+ if tag == protocol.ext_m3u:
117
+ continue
118
+ # In strict mode, unrecognized tags are illegal
119
+ if strict:
120
+ raise ParseError(lineno, line)
121
+ continue
122
+
123
+ # Lines that don't start with # are either segments or playlists.
124
+ if state["expect_segment"]:
125
+ _parse_ts_chunk(**parse_kwargs)
126
+ elif state["expect_playlist"]:
127
+ _parse_variant_playlist(**parse_kwargs)
128
+ # In strict mode, any other content is illegal
129
+ elif strict:
130
+ raise ParseError(lineno, line)
131
+
132
+ # Handle remaining partial segments.
133
+ if "segment" in state:
134
+ data["segments"].append(state.pop("segment"))
135
+
136
+ return data
137
+
138
+
139
+ def _parse_key(line, data, state, **kwargs):
140
+ params = ATTRIBUTELISTPATTERN.split(line.replace(protocol.ext_x_key + ":", ""))[
141
+ 1::2
142
+ ]
143
+ key = {}
144
+ for param in params:
145
+ name, value = param.split("=", 1)
146
+ key[normalize_attribute(name)] = remove_quotes(value)
147
+
148
+ state["current_key"] = key
149
+ if key not in data["keys"]:
150
+ data["keys"].append(key)
151
+
152
+
153
+ def _parse_extinf(line, state, lineno, strict, **kwargs):
154
+ chunks = line.replace(protocol.extinf + ":", "").split(",", 1)
155
+ if len(chunks) == 2:
156
+ duration, title = chunks
157
+ elif len(chunks) == 1:
158
+ if strict:
159
+ raise ParseError(lineno, line)
160
+ else:
161
+ duration = chunks[0]
162
+ title = ""
163
+ if "segment" not in state:
164
+ state["segment"] = {}
165
+ state["segment"]["duration"] = float(duration)
166
+ state["segment"]["title"] = title
167
+ state["expect_segment"] = True
168
+
169
+
170
+ def _parse_ts_chunk(line, data, state, **kwargs):
171
+ segment = state.pop("segment")
172
+ if state.get("program_date_time"):
173
+ segment["program_date_time"] = state.pop("program_date_time")
174
+ if state.get("current_program_date_time"):
175
+ segment["current_program_date_time"] = state["current_program_date_time"]
176
+ state["current_program_date_time"] += timedelta(seconds=segment["duration"])
177
+ segment["uri"] = line
178
+ segment["cue_in"] = state.pop("cue_in", False)
179
+ segment["cue_out"] = state.pop("cue_out", False)
180
+ segment["cue_out_start"] = state.pop("cue_out_start", False)
181
+ segment["cue_out_explicitly_duration"] = state.pop(
182
+ "cue_out_explicitly_duration", False
183
+ )
184
+
185
+ scte_op = state.get if segment["cue_out"] else state.pop
186
+ segment["scte35"] = scte_op("current_cue_out_scte35", None)
187
+ segment["oatcls_scte35"] = scte_op("current_cue_out_oatcls_scte35", None)
188
+ segment["scte35_duration"] = scte_op("current_cue_out_duration", None)
189
+ segment["scte35_elapsedtime"] = scte_op("current_cue_out_elapsedtime", None)
190
+ segment["asset_metadata"] = scte_op("asset_metadata", None)
191
+
192
+ segment["discontinuity"] = state.pop("discontinuity", False)
193
+ if state.get("current_key"):
194
+ segment["key"] = state["current_key"]
195
+ else:
196
+ # For unencrypted segments, the initial key would be None
197
+ if None not in data["keys"]:
198
+ data["keys"].append(None)
199
+ if state.get("current_segment_map"):
200
+ segment["init_section"] = state["current_segment_map"]
201
+ segment["dateranges"] = state.pop("dateranges", None)
202
+ segment["gap_tag"] = state.pop("gap", None)
203
+ segment["blackout"] = state.pop("blackout", None)
204
+ data["segments"].append(segment)
205
+ state["expect_segment"] = False
206
+
207
+
208
+ def _parse_attribute_list(prefix, line, attribute_parser, default_parser=None):
209
+ params = ATTRIBUTELISTPATTERN.split(line.replace(prefix + ":", ""))[1::2]
210
+
211
+ attributes = {}
212
+ if not line.startswith(prefix + ":"):
213
+ return attributes
214
+
215
+ for param in params:
216
+ param_parts = param.split("=", 1)
217
+ if len(param_parts) == 1:
218
+ name = ""
219
+ value = param_parts[0]
220
+ else:
221
+ name, value = param_parts
222
+
223
+ name = normalize_attribute(name)
224
+ if name in attribute_parser:
225
+ value = attribute_parser[name](value)
226
+ elif default_parser is not None:
227
+ value = default_parser(value)
228
+
229
+ attributes[name] = value
230
+
231
+ return attributes
232
+
233
+
234
+ def _parse_stream_inf(line, data, state, **kwargs):
235
+ state["expect_playlist"] = True
236
+ data["is_variant"] = True
237
+ data["media_sequence"] = None
238
+ state["stream_info"] = _parse_attribute_list(
239
+ protocol.ext_x_stream_inf, line, STREAM_INF_ATTRIBUTE_PARSER
240
+ )
241
+
242
+
243
+ def _parse_i_frame_stream_inf(line, data, **kwargs):
244
+ iframe_stream_info = _parse_attribute_list(
245
+ protocol.ext_x_i_frame_stream_inf, line, IFRAME_STREAM_INF_ATTRIBUTE_PARSER
246
+ )
247
+ iframe_playlist = {
248
+ "uri": iframe_stream_info.pop("uri"),
249
+ "iframe_stream_info": iframe_stream_info,
250
+ }
251
+
252
+ data["iframe_playlists"].append(iframe_playlist)
253
+
254
+
255
+ def _parse_image_stream_inf(line, data, **kwargs):
256
+ image_stream_info = _parse_attribute_list(
257
+ protocol.ext_x_image_stream_inf, line, IMAGE_STREAM_INF_ATTRIBUTE_PARSER
258
+ )
259
+ image_playlist = {
260
+ "uri": image_stream_info.pop("uri"),
261
+ "image_stream_info": image_stream_info,
262
+ }
263
+
264
+ data["image_playlists"].append(image_playlist)
265
+
266
+
267
+ def _parse_is_images_only(line, data, **kwargs):
268
+ data["is_images_only"] = True
269
+
270
+
271
+ def _parse_tiles(line, data, state, **kwargs):
272
+ tiles_info = _parse_attribute_list(
273
+ protocol.ext_x_tiles, line, TILES_ATTRIBUTE_PARSER
274
+ )
275
+ data["tiles"].append(tiles_info)
276
+
277
+
278
+ def _parse_media(line, data, **kwargs):
279
+ media = _parse_attribute_list(protocol.ext_x_media, line, MEDIA_ATTRIBUTE_PARSER)
280
+ data["media"].append(media)
281
+
282
+
283
+ def _parse_variant_playlist(line, data, state, **kwargs):
284
+ playlist = {"uri": line, "stream_info": state.pop("stream_info")}
285
+ data["playlists"].append(playlist)
286
+ state["expect_playlist"] = False
287
+
288
+
289
+ def _parse_bitrate(state, **kwargs):
290
+ if "segment" not in state:
291
+ state["segment"] = {}
292
+ state["segment"]["bitrate"] = _parse_simple_parameter(cast_to=int, **kwargs)
293
+
294
+
295
+ def _parse_byterange(line, state, **kwargs):
296
+ if "segment" not in state:
297
+ state["segment"] = {}
298
+ state["segment"]["byterange"] = line.replace(protocol.ext_x_byterange + ":", "")
299
+ state["expect_segment"] = True
300
+
301
+
302
+ def _parse_targetduration(**parse_kwargs):
303
+ return _parse_simple_parameter(cast_to=int, **parse_kwargs)
304
+
305
+
306
+ def _parse_media_sequence(**parse_kwargs):
307
+ return _parse_simple_parameter(cast_to=int, **parse_kwargs)
308
+
309
+
310
+ def _parse_discontinuity_sequence(**parse_kwargs):
311
+ return _parse_simple_parameter(cast_to=int, **parse_kwargs)
312
+
313
+
314
+ def _parse_program_date_time(line, state, data, **parse_kwargs):
315
+ _, program_date_time = _parse_simple_parameter_raw_value(
316
+ line, cast_to=cast_date_time, **parse_kwargs
317
+ )
318
+ if not data.get("program_date_time"):
319
+ data["program_date_time"] = program_date_time
320
+ state["current_program_date_time"] = program_date_time
321
+ state["program_date_time"] = program_date_time
322
+
323
+
324
+ def _parse_discontinuity(state, **parse_kwargs):
325
+ state["discontinuity"] = True
326
+
327
+
328
+ def _parse_cue_in(state, **parse_kwargs):
329
+ state["cue_in"] = True
330
+
331
+
332
+ def _parse_cue_span(state, **parse_kwargs):
333
+ state["cue_out"] = True
334
+
335
+
336
+ def _parse_version(**parse_kwargs):
337
+ return _parse_simple_parameter(cast_to=int, **parse_kwargs)
338
+
339
+
340
+ def _parse_allow_cache(**parse_kwargs):
341
+ return _parse_simple_parameter(cast_to=str, **parse_kwargs)
342
+
343
+
344
+ def _parse_playlist_type(line, data, **kwargs):
345
+ return _parse_simple_parameter(line, data)
346
+
347
+
348
+ def _parse_x_map(line, data, state, **kwargs):
349
+ segment_map_info = _parse_attribute_list(
350
+ protocol.ext_x_map, line, X_MAP_ATTRIBUTE_PARSER
351
+ )
352
+ state["current_segment_map"] = segment_map_info
353
+ data["segment_map"].append(segment_map_info)
354
+
355
+
356
+ def _parse_start(line, data, **kwargs):
357
+ start_info = _parse_attribute_list(
358
+ protocol.ext_x_start, line, START_ATTRIBUTE_PARSER
359
+ )
360
+ data["start"] = start_info
361
+
362
+
363
+ def _parse_gap(state, **kwargs):
364
+ state["gap"] = True
365
+
366
+
367
+ def _parse_blackout(line, state, **kwargs):
368
+ # Store the full tag content to pass through unmodified
369
+ # Extract everything after "#EXT-X-BLACKOUT"
370
+ if ":" in line:
371
+ # Tag has parameters: #EXT-X-BLACKOUT:params
372
+ blackout_data = line.split(":", 1)[1]
373
+ else:
374
+ # Tag has no parameters, just store True
375
+ blackout_data = True
376
+ state["blackout"] = blackout_data
377
+
378
+
379
+ def _parse_simple_parameter_raw_value(line, cast_to=str, normalize=False, **kwargs):
380
+ param, value = line.split(":", 1)
381
+ param = normalize_attribute(param.replace("#EXT-X-", ""))
382
+ if normalize:
383
+ value = value.strip().lower()
384
+ return param, cast_to(value)
385
+
386
+
387
+ def _parse_and_set_simple_parameter_raw_value(
388
+ line, data, cast_to=str, normalize=False, **kwargs
389
+ ):
390
+ param, value = _parse_simple_parameter_raw_value(line, cast_to, normalize)
391
+ data[param] = value
392
+ return data[param]
393
+
394
+
395
+ def _parse_simple_parameter(line, data, cast_to=str, **kwargs):
396
+ return _parse_and_set_simple_parameter_raw_value(line, data, cast_to, True)
397
+
398
+
399
+ def _parse_i_frames_only(data, **kwargs):
400
+ data["is_i_frames_only"] = True
401
+
402
+
403
+ def _parse_is_independent_segments(data, **kwargs):
404
+ data["is_independent_segments"] = True
405
+
406
+
407
+ def _parse_endlist(data, **kwargs):
408
+ data["is_endlist"] = True
409
+
410
+
411
+ def _parse_cueout_cont(line, state, **kwargs):
412
+ state["cue_out"] = True
413
+
414
+ elements = line.split(":", 1)
415
+ if len(elements) != 2:
416
+ return
417
+
418
+ # EXT-X-CUE-OUT-CONT:ElapsedTime=10,Duration=60,SCTE35=... style
419
+ cue_info = _parse_attribute_list(
420
+ protocol.ext_x_cue_out_cont,
421
+ line,
422
+ CUEOUT_CONT_ATTRIBUTE_PARSER,
423
+ )
424
+
425
+ # EXT-X-CUE-OUT-CONT:2.436/120 style
426
+ progress = cue_info.get("")
427
+ if progress:
428
+ progress_parts = progress.split("/", 1)
429
+ if len(progress_parts) == 1:
430
+ state["current_cue_out_duration"] = progress_parts[0]
431
+ else:
432
+ state["current_cue_out_elapsedtime"] = progress_parts[0]
433
+ state["current_cue_out_duration"] = progress_parts[1]
434
+
435
+ duration = cue_info.get("duration")
436
+ if duration:
437
+ state["current_cue_out_duration"] = duration
438
+
439
+ scte35 = cue_info.get("scte35")
440
+ if duration:
441
+ state["current_cue_out_scte35"] = scte35
442
+
443
+ elapsedtime = cue_info.get("elapsedtime")
444
+ if elapsedtime:
445
+ state["current_cue_out_elapsedtime"] = elapsedtime
446
+
447
+
448
+ def _parse_cueout(line, state, **kwargs):
449
+ state["cue_out_start"] = True
450
+ state["cue_out"] = True
451
+ if "DURATION" in line.upper():
452
+ state["cue_out_explicitly_duration"] = True
453
+
454
+ elements = line.split(":", 1)
455
+ if len(elements) != 2:
456
+ return
457
+
458
+ cue_info = _parse_attribute_list(
459
+ protocol.ext_x_cue_out,
460
+ line,
461
+ CUEOUT_ATTRIBUTE_PARSER,
462
+ )
463
+ cue_out_scte35 = cue_info.get("cue")
464
+ cue_out_duration = cue_info.get("duration") or cue_info.get("")
465
+
466
+ current_cue_out_scte35 = state.get("current_cue_out_scte35")
467
+ state["current_cue_out_scte35"] = cue_out_scte35 or current_cue_out_scte35
468
+ state["current_cue_out_duration"] = cue_out_duration
469
+
470
+
471
+ def _parse_server_control(line, data, **kwargs):
472
+ data["server_control"] = _parse_attribute_list(
473
+ protocol.ext_x_server_control, line, SERVER_CONTROL_ATTRIBUTE_PARSER
474
+ )
475
+
476
+
477
+ def _parse_part_inf(line, data, **kwargs):
478
+ data["part_inf"] = _parse_attribute_list(
479
+ protocol.ext_x_part_inf, line, PART_INF_ATTRIBUTE_PARSER
480
+ )
481
+
482
+
483
+ def _parse_rendition_report(line, data, **kwargs):
484
+ rendition_report = _parse_attribute_list(
485
+ protocol.ext_x_rendition_report, line, RENDITION_REPORT_ATTRIBUTE_PARSER
486
+ )
487
+
488
+ data["rendition_reports"].append(rendition_report)
489
+
490
+
491
+ def _parse_part(line, state, **kwargs):
492
+ part = _parse_attribute_list(protocol.ext_x_part, line, PART_ATTRIBUTE_PARSER)
493
+
494
+ # this should always be true according to spec
495
+ if state.get("current_program_date_time"):
496
+ part["program_date_time"] = state["current_program_date_time"]
497
+ state["current_program_date_time"] += timedelta(seconds=part["duration"])
498
+
499
+ part["dateranges"] = state.pop("dateranges", None)
500
+ part["gap_tag"] = state.pop("gap", None)
501
+
502
+ if "segment" not in state:
503
+ state["segment"] = {}
504
+ segment = state["segment"]
505
+ if "parts" not in segment:
506
+ segment["parts"] = []
507
+
508
+ segment["parts"].append(part)
509
+
510
+
511
+ def _parse_skip(line, data, **parse_kwargs):
512
+ data["skip"] = _parse_attribute_list(
513
+ protocol.ext_x_skip, line, SKIP_ATTRIBUTE_PARSER
514
+ )
515
+
516
+
517
+ def _parse_session_data(line, data, **kwargs):
518
+ session_data = _parse_attribute_list(
519
+ protocol.ext_x_session_data, line, SESSION_DATA_ATTRIBUTE_PARSER
520
+ )
521
+ data["session_data"].append(session_data)
522
+
523
+
524
+ def _parse_session_key(line, data, **kwargs):
525
+ params = ATTRIBUTELISTPATTERN.split(
526
+ line.replace(protocol.ext_x_session_key + ":", "")
527
+ )[1::2]
528
+ key = {}
529
+ for param in params:
530
+ name, value = param.split("=", 1)
531
+ key[normalize_attribute(name)] = remove_quotes(value)
532
+ data["session_keys"].append(key)
533
+
534
+
535
+ def _parse_preload_hint(line, data, **kwargs):
536
+ data["preload_hint"] = _parse_attribute_list(
537
+ protocol.ext_x_preload_hint, line, PRELOAD_HINT_ATTRIBUTE_PARSER
538
+ )
539
+
540
+
541
+ def _parse_daterange(line, state, **kwargs):
542
+ parsed = _parse_attribute_list(
543
+ protocol.ext_x_daterange, line, DATERANGE_ATTRIBUTE_PARSER
544
+ )
545
+
546
+ if "dateranges" not in state:
547
+ state["dateranges"] = []
548
+
549
+ state["dateranges"].append(parsed)
550
+
551
+
552
+ def _parse_content_steering(line, data, **kwargs):
553
+ data["content_steering"] = _parse_attribute_list(
554
+ protocol.ext_x_content_steering, line, CONTENT_STEERING_ATTRIBUTE_PARSER
555
+ )
556
+
557
+
558
+ def _parse_oatcls_scte35(line, state, **kwargs):
559
+ scte35_cue = line.split(":", 1)[1]
560
+ state["current_cue_out_oatcls_scte35"] = scte35_cue
561
+ if not state.get("current_cue_out_scte35"):
562
+ state["current_cue_out_scte35"] = scte35_cue
563
+
564
+
565
+ def _parse_asset(line, state, **kwargs):
566
+ # EXT-X-ASSET attribute values may or may not be quoted, and need to be URL-encoded.
567
+ # They are preserved as-is here to prevent loss of information.
568
+ state["asset_metadata"] = _parse_attribute_list(
569
+ protocol.ext_x_asset, line, {}, default_parser=str
570
+ )
571
+
572
+
573
+ def string_to_lines(string):
574
+ return string.strip().splitlines()
575
+
576
+
577
+ def remove_quotes_parser(*attrs):
578
+ return dict(zip(attrs, itertools.repeat(remove_quotes)))
579
+
580
+
581
+ def remove_quotes(string):
582
+ """
583
+ Remove quotes from string.
584
+
585
+ Ex.:
586
+ "foo" -> foo
587
+ 'foo' -> foo
588
+ 'foo -> 'foo
589
+
590
+ """
591
+ quotes = ('"', "'")
592
+ if string.startswith(quotes) and string.endswith(quotes):
593
+ return string[1:-1]
594
+ return string
595
+
596
+
597
+ def normalize_attribute(attribute):
598
+ return attribute.replace("-", "_").lower().strip()
599
+
600
+
601
+ def get_segment_custom_value(state, key, default=None):
602
+ """
603
+ Helper function for getting custom values for Segment
604
+ Are useful with custom_tags_parser
605
+ """
606
+ if "segment" not in state:
607
+ return default
608
+ if "custom_parser_values" not in state["segment"]:
609
+ return default
610
+ return state["segment"]["custom_parser_values"].get(key, default)
611
+
612
+
613
+ def save_segment_custom_value(state, key, value):
614
+ """
615
+ Helper function for saving custom values for Segment
616
+ Are useful with custom_tags_parser
617
+ """
618
+ if "segment" not in state:
619
+ state["segment"] = {}
620
+
621
+ if "custom_parser_values" not in state["segment"]:
622
+ state["segment"]["custom_parser_values"] = {}
623
+
624
+ state["segment"]["custom_parser_values"][key] = value
625
+
626
+
627
+ # Attribute parser constants (built once)
628
+ STREAM_INF_ATTRIBUTE_PARSER = remove_quotes_parser(
629
+ "codecs",
630
+ "audio",
631
+ "video",
632
+ "video_range",
633
+ "subtitles",
634
+ "pathway_id",
635
+ "stable_variant_id",
636
+ )
637
+ STREAM_INF_ATTRIBUTE_PARSER.update(
638
+ {
639
+ "program_id": int,
640
+ "bandwidth": lambda x: int(float(x)),
641
+ "average_bandwidth": int,
642
+ "frame_rate": float,
643
+ "hdcp_level": str,
644
+ }
645
+ )
646
+
647
+ IFRAME_STREAM_INF_ATTRIBUTE_PARSER = remove_quotes_parser(
648
+ "codecs", "uri", "pathway_id", "stable_variant_id"
649
+ )
650
+ IFRAME_STREAM_INF_ATTRIBUTE_PARSER.update(
651
+ {
652
+ "program_id": int,
653
+ "bandwidth": int,
654
+ "average_bandwidth": int,
655
+ "hdcp_level": str,
656
+ }
657
+ )
658
+
659
+ IMAGE_STREAM_INF_ATTRIBUTE_PARSER = remove_quotes_parser(
660
+ "codecs", "uri", "pathway_id", "stable_variant_id"
661
+ )
662
+ IMAGE_STREAM_INF_ATTRIBUTE_PARSER.update(
663
+ {
664
+ "program_id": int,
665
+ "bandwidth": int,
666
+ "average_bandwidth": int,
667
+ "resolution": str,
668
+ }
669
+ )
670
+
671
+ MEDIA_ATTRIBUTE_PARSER = remove_quotes_parser(
672
+ "uri",
673
+ "group_id",
674
+ "language",
675
+ "assoc_language",
676
+ "name",
677
+ "instream_id",
678
+ "characteristics",
679
+ "channels",
680
+ "stable_rendition_id",
681
+ "thumbnails",
682
+ "image",
683
+ )
684
+
685
+ X_MAP_ATTRIBUTE_PARSER = remove_quotes_parser("uri", "byterange")
686
+
687
+ START_ATTRIBUTE_PARSER = {"time_offset": lambda x: float(x)}
688
+
689
+ SERVER_CONTROL_ATTRIBUTE_PARSER = {
690
+ "can_block_reload": str,
691
+ "hold_back": lambda x: float(x),
692
+ "part_hold_back": lambda x: float(x),
693
+ "can_skip_until": lambda x: float(x),
694
+ "can_skip_dateranges": str,
695
+ }
696
+
697
+ PART_INF_ATTRIBUTE_PARSER = {"part_target": lambda x: float(x)}
698
+
699
+ RENDITION_REPORT_ATTRIBUTE_PARSER = remove_quotes_parser("uri")
700
+ RENDITION_REPORT_ATTRIBUTE_PARSER.update({"last_msn": int, "last_part": int})
701
+
702
+ PART_ATTRIBUTE_PARSER = remove_quotes_parser("uri")
703
+ PART_ATTRIBUTE_PARSER.update(
704
+ {"duration": lambda x: float(x), "independent": str, "gap": str, "byterange": str}
705
+ )
706
+
707
+ SKIP_ATTRIBUTE_PARSER = remove_quotes_parser("recently_removed_dateranges")
708
+ SKIP_ATTRIBUTE_PARSER.update({"skipped_segments": int})
709
+
710
+ SESSION_DATA_ATTRIBUTE_PARSER = remove_quotes_parser(
711
+ "data_id", "value", "uri", "language"
712
+ )
713
+
714
+ PRELOAD_HINT_ATTRIBUTE_PARSER = remove_quotes_parser("uri")
715
+ PRELOAD_HINT_ATTRIBUTE_PARSER.update(
716
+ {"type": str, "byterange_start": int, "byterange_length": int}
717
+ )
718
+
719
+ DATERANGE_ATTRIBUTE_PARSER = remove_quotes_parser(
720
+ "id", "class", "start_date", "end_date"
721
+ )
722
+ DATERANGE_ATTRIBUTE_PARSER.update(
723
+ {
724
+ "duration": float,
725
+ "planned_duration": float,
726
+ "end_on_next": str,
727
+ "scte35_cmd": str,
728
+ "scte35_out": str,
729
+ "scte35_in": str,
730
+ }
731
+ )
732
+
733
+ CONTENT_STEERING_ATTRIBUTE_PARSER = remove_quotes_parser("server_uri", "pathway_id")
734
+
735
+ CUEOUT_CONT_ATTRIBUTE_PARSER = remove_quotes_parser("duration", "elapsedtime", "scte35")
736
+
737
+ CUEOUT_ATTRIBUTE_PARSER = remove_quotes_parser("cue")
738
+
739
+ TILES_ATTRIBUTE_PARSER = remove_quotes_parser("uri")
740
+ TILES_ATTRIBUTE_PARSER.update({"resolution": str, "layout": str, "duration": float})
741
+
742
+
743
+ # Single token-to-handler dispatch to avoid a long startswith chain
744
+ DISPATCH = {
745
+ protocol.ext_x_byterange: _parse_byterange,
746
+ protocol.ext_x_bitrate: _parse_bitrate,
747
+ protocol.ext_x_targetduration: _parse_targetduration,
748
+ protocol.ext_x_media_sequence: _parse_media_sequence,
749
+ protocol.ext_x_discontinuity_sequence: _parse_discontinuity_sequence,
750
+ protocol.ext_x_program_date_time: _parse_program_date_time,
751
+ protocol.ext_x_discontinuity: _parse_discontinuity,
752
+ protocol.ext_x_cue_out_cont: _parse_cueout_cont,
753
+ protocol.ext_x_cue_out: _parse_cueout,
754
+ protocol.ext_oatcls_scte35: _parse_oatcls_scte35,
755
+ protocol.ext_x_asset: _parse_asset,
756
+ protocol.ext_x_cue_in: _parse_cue_in,
757
+ protocol.ext_x_cue_span: _parse_cue_span,
758
+ protocol.ext_x_version: _parse_version,
759
+ protocol.ext_x_allow_cache: _parse_allow_cache,
760
+ protocol.ext_x_key: _parse_key,
761
+ protocol.extinf: _parse_extinf,
762
+ protocol.ext_x_stream_inf: _parse_stream_inf,
763
+ protocol.ext_x_i_frame_stream_inf: _parse_i_frame_stream_inf,
764
+ protocol.ext_x_media: _parse_media,
765
+ protocol.ext_x_playlist_type: _parse_playlist_type,
766
+ protocol.ext_i_frames_only: _parse_i_frames_only,
767
+ protocol.ext_is_independent_segments: _parse_is_independent_segments,
768
+ protocol.ext_x_endlist: _parse_endlist,
769
+ protocol.ext_x_map: _parse_x_map,
770
+ protocol.ext_x_start: _parse_start,
771
+ protocol.ext_x_server_control: _parse_server_control,
772
+ protocol.ext_x_part_inf: _parse_part_inf,
773
+ protocol.ext_x_rendition_report: _parse_rendition_report,
774
+ protocol.ext_x_part: _parse_part,
775
+ protocol.ext_x_skip: _parse_skip,
776
+ protocol.ext_x_session_data: _parse_session_data,
777
+ protocol.ext_x_session_key: _parse_session_key,
778
+ protocol.ext_x_preload_hint: _parse_preload_hint,
779
+ protocol.ext_x_daterange: _parse_daterange,
780
+ protocol.ext_x_gap: _parse_gap,
781
+ protocol.ext_x_content_steering: _parse_content_steering,
782
+ protocol.ext_x_image_stream_inf: _parse_image_stream_inf,
783
+ protocol.ext_x_images_only: _parse_is_images_only,
784
+ protocol.ext_x_tiles: _parse_tiles,
785
+ protocol.ext_x_blackout: _parse_blackout,
786
+ }