using System.Text.RegularExpressions; using VideoDownloader.Core.Models; namespace VideoDownloader.Core.Hls; /// m3u8(마스터/미디어) 재생목록 파서. Python parse_m3u8 의 C# 포팅. public static class M3u8Parser { // KEY="VAL",KEY2=VAL 형태 속성 파싱 private static readonly Regex AttrRe = new(@"(?[A-Z0-9\-]+)=(\""(?[^""]*)\"")|(?[A-Z0-9\-]+)=(?[^,]*)", RegexOptions.Compiled); private static Dictionary ParseAttrs(string s) { var d = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (Match m in AttrRe.Matches(s)) { var key = m.Groups["k"].Success ? m.Groups["k"].Value : m.Groups["k2"].Value; var val = m.Groups["q"].Success ? m.Groups["q"].Value : m.Groups["v"].Value; d[key] = val; } return d; } private static string GetValue(this Dictionary d, string key, string def = "") => d.TryGetValue(key, out var v) ? v : def; public static ParsedPlaylist Parse(string text, Uri baseUrl) { var variants = new List(); var segments = new List(); Uri? init = null; bool isMaster = false; Dictionary? pendingVariant = null; KeyInfo? curKey = null; long seq = 0; int idx = 0; foreach (var raw in text.Split('\n')) { var line = raw.Trim(); if (string.IsNullOrEmpty(line)) continue; if (line.StartsWith("#EXT-X-STREAM-INF:")) { pendingVariant = ParseAttrs(line.Substring("#EXT-X-STREAM-INF:".Length)); continue; } if (line.StartsWith("#EXT-X-MEDIA-SEQUENCE:")) { if (long.TryParse(line.Split(':', 2)[1].Trim(), out var s)) seq = s; continue; } if (line.StartsWith("#EXT-X-KEY:")) { var a = ParseAttrs(line.Substring("#EXT-X-KEY:".Length)); var method = a.GetValue("METHOD", "NONE").ToUpperInvariant(); curKey = method == "AES-128" ? new KeyInfo( a.TryGetValue("URI", out var u) && !string.IsNullOrEmpty(u) ? new Uri(baseUrl, u) : null, a.GetValue("IV")) : null; continue; } if (line.StartsWith("#EXT-X-MAP:")) { var a = ParseAttrs(line.Substring("#EXT-X-MAP:".Length)); if (a.TryGetValue("URI", out var u) && !string.IsNullOrEmpty(u)) init = new Uri(baseUrl, u); continue; } if (line.StartsWith("#")) continue; // URL 라인 var abs = new Uri(baseUrl, line); if (pendingVariant != null) { int.TryParse(pendingVariant.GetValue("BANDWIDTH", "0"), out var bw); variants.Add(new Variant(abs, bw, pendingVariant.GetValue("RESOLUTION"), pendingVariant.GetValue("CODECS"))); pendingVariant = null; isMaster = true; } else { segments.Add(new Segment(abs, idx, seq, curKey)); idx++; seq++; } } return new ParsedPlaylist { Type = isMaster ? PlaylistType.Master : PlaylistType.Media, Variants = variants, Segments = segments, InitSegment = init, }; } }