feat(ads/seo/release): v1.2.0 with live Google Ads, isolated ad profile, ads.txt, and SEO/AEO/GEO optimization

This commit is contained in:
Yun Chan 2026-09-14 00:39:44 +09:00
parent a88aafa1e5
commit 0e568f1f0a
975 changed files with 130593 additions and 16783 deletions

View file

@ -0,0 +1,116 @@
using System.Text.RegularExpressions;
using Paca.Core.Models;
namespace Paca.Core.Hls;
/// <summary>m3u8(마스터/미디어) 재생목록 파서. Python parse_m3u8 의 C# 포팅.</summary>
public static class M3u8Parser
{
// KEY="VAL",KEY2=VAL 형태 속성 파싱 (소문자/언더스코어 포함)
private static readonly Regex AttrRe =
new(@"(?<k>[A-Za-z0-9_\-]+)=(\""(?<q>[^""]*)\"")|(?<k2>[A-Za-z0-9_\-]+)=(?<v>[^,]*)",
RegexOptions.Compiled);
private static Dictionary<string, string> ParseAttrs(string s)
{
var d = new Dictionary<string, string>(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<string, string> 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<Variant>();
var segments = new List<Segment>();
Uri? init = null;
bool isMaster = false;
if (string.IsNullOrWhiteSpace(text) || !text.Contains("#EXTM3U", StringComparison.OrdinalIgnoreCase))
{
return new ParsedPlaylist
{
Type = PlaylistType.Media,
Variants = variants,
Segments = segments,
InitSegment = init,
};
}
Dictionary<string, string>? pendingVariant = null;
KeyInfo? curKey = null;
long seq = 0;
int idx = 0;
using var reader = new StringReader(text);
string? raw;
while ((raw = reader.ReadLine()) != null)
{
var line = raw.Trim();
if (string.IsNullOrEmpty(line)) continue;
if (line.StartsWith("#EXT-X-STREAM-INF:", StringComparison.OrdinalIgnoreCase))
{
pendingVariant = ParseAttrs(line.Substring("#EXT-X-STREAM-INF:".Length));
continue;
}
if (line.StartsWith("#EXT-X-MEDIA-SEQUENCE:", StringComparison.OrdinalIgnoreCase))
{
var parts = line.Split(':', 2);
if (parts.Length > 1 && long.TryParse(parts[1].Trim(), out var s)) seq = s;
continue;
}
if (line.StartsWith("#EXT-X-KEY:", StringComparison.OrdinalIgnoreCase))
{
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) && Uri.TryCreate(baseUrl, u.Trim('"'), out var ku) ? ku : null,
a.GetValue("IV"))
: null;
continue;
}
if (line.StartsWith("#EXT-X-MAP:", StringComparison.OrdinalIgnoreCase))
{
var a = ParseAttrs(line.Substring("#EXT-X-MAP:".Length));
if (a.TryGetValue("URI", out var u) && !string.IsNullOrEmpty(u) && Uri.TryCreate(baseUrl, u.Trim('"'), out var mu))
init = mu;
continue;
}
if (line.StartsWith("#")) continue;
// URL 라인
if (!Uri.TryCreate(baseUrl, line, out var abs)) continue;
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,
};
}
}