Как распарсить такое? (M3U, Twitch)

Здравствуйте.
как распарсить такое?

#EXTM3U
#EXT-X-TWITCH-INFO:NODE="video-edge-c685ec.arn03",MANIFEST-NODE-TYPE="weaver_cluster",MANIFEST-NODE="video-weaver.fra05",SUPPRESS="true",SERVER-TIME="1610277986.44",TRANSCODESTACK="2017TranscodeQS_V2",USER-IP="46.175.35.158",SERVING-ID="1550fbfb04504907aff64a3e037f0f0c",CLUSTER="arn03",ABS="false",VIDEO-SESSION-ID="5663013854168568271",BROADCAST-ID="40128701277",STREAM-TIME="5165.443388",FUTURE="true",B="false",USER-COUNTRY="RU",MANIFEST-CLUSTER="fra05",ORIGIN="cmh01",C="aHR0cHM6Ly92aWRlby1lZGdlLTFiY2MxNi5wZHgwMS5hYnMuaGxzLnR0dm53Lm5ldC92MS9zZWdtZW50L0N2TUJTVUFaQW1Fb1JhNmxKZllyd0Vra3psT0gwYlR5ZE1hR3VPNjNMZHozSkl5dEE5SUd6WmQ2QUFGRlhUYXZQdXQ0b0JueC1LRFg2YmJraFZuaWFKNTZaUHZCYmFNNEstSWo5N2pkWWhBZWxkX2IxTzBwTmNIWk9BeTNXemU5Vm9JMEtoVHVGLVQ4aFJ1alhqVjNkUS1RZC1QaDZkQWFfb180NUEwa0NvYXhGVjJWUElwa1kxVzFtQk83YWVSNTI1YTVic0JsdjRqeFlWLVNSY3lqdXQ1QS1wb0dYdkJ0cGRoVnVtMzJ3U1h4MXpZd3VPam82UGpvblVwNjVfVEQzbmJsNVB5TDZhZ1JPUEZpaGw5aXhndXJNdmZvUnQ0bkFxZzFKV1JUeVYzaG5HQTA3SzcwcUhpbzhnR0NZTS1fVURBb095N2dtSUp2X2VVQm9ueUY4LXZoLW5aemhuR1YwUDgxd2IzT09hQmdjX1JRNTgtd2NENWNjdWNaWGkxT25HUWNSM3FNdmprelQtOFY0QWc5eXBlVTdHczJlVEhTMENHZXVORExXUm54RVVrTFRHNXJBa09iY2l3d0stMTY4Qk1MUVQ5UEFJMWVlc1NnSHNhazFuMVFzQ1hBLVh5YW95N0ZidGNxX0tfZU9VWmc1cVA5dkVhVnNDTEhxVlJxZkhkRktZdEl2alJ1aGlNV2gtMl9qUHFzamtiSXpFOWNoV2h0Y1ltYmRreXpmcy1tWURLcTZ3OWUtdTdTWFg4VUhFTnpDdDZraDJxZ3M0cUduZUVhelh5S0lndTkwQnNsTnlQWE1XeGxmNzdTUnhza0hOTnhSWHEtRjh2dG85SDIxQWxOMHU1V3ZocjRvczhGbGxlQWdwSDhScndmSXFnNExpRUxlemZuUTNvSGVEdjkyZGZIcjlQZmVNZnNtb3BPWXgyS2xmMkZCRWZnN0g4Y0FRbHQxdjZVSzJfNEVDVml1N3Y0ZElaNk9KTmN5cExUV0YtVlJFdlNsd1pHcnoudHM",D="false"
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="chunked",NAME="1080p60 (source)",AUTOSELECT=YES,DEFAULT=YES
#EXT-X-STREAM-INF:BANDWIDTH=5813280,RESOLUTION=1920x1080,CODECS="avc1.64002A,mp4a.40.2",VIDEO="chunked",FRAME-RATE=60.000
<very long url>
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="720p60",NAME="720p60",AUTOSELECT=YES,DEFAULT=YES
#EXT-X-STREAM-INF:BANDWIDTH=3428223,RESOLUTION=1280x720,CODECS="avc1.4D401F,mp4a.40.2",VIDEO="720p60",FRAME-RATE=60.000
<very long url>
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="480p30",NAME="480p",AUTOSELECT=YES,DEFAULT=YES
#EXT-X-STREAM-INF:BANDWIDTH=1433223,RESOLUTION=852x480,CODECS="avc1.4D401F,mp4a.40.2",VIDEO="480p30",FRAME-RATE=30.000
<very long url>
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="360p30",NAME="360p",AUTOSELECT=YES,DEFAULT=YES
#EXT-X-STREAM-INF:BANDWIDTH=630000,RESOLUTION=640x360,CODECS="avc1.4D401F,mp4a.40.2",VIDEO="360p30",FRAME-RATE=30.000
<very long url>
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="160p30",NAME="160p",AUTOSELECT=YES,DEFAULT=YES
#EXT-X-STREAM-INF:BANDWIDTH=230000,RESOLUTION=284x160,CODECS="avc1.4D401F,mp4a.40.2",VIDEO="160p30",FRAME-RATE=30.000
<very long url>

Я уже писал для этого корявый парсер на Delphi. Ничего не мешает сделать это и на C-Sharp’е. Но может уже есть что-то готовое?

Это M3U вроде. google.com/search?q=m3u+parser+c#

Ну или может что-то именно для твича есть. Там еще вроде есть какие-то API, возможно лучше через них.

Есть разные виды M3U

API твича? Это и так их API выдало.

Какое именно?

Обычно есть более удобные обертки типа этой

не докумментированное

Получилось что-то типа этого (в стиле Гоши Дударя :rofl: ):

namespace TwitchWatcher
{
    class TwitchPlaylistsManifestParser
    {
        private string[] medias;
        //private string[] streamInfos;
        private string[] urls;
        private int elementsCount;

        public TwitchPlaylistsManifestParser(string inputManifestString)
        {
            string[] strings = inputManifestString.Split('\n');
            elementsCount = (strings.Length - 2) / 3;
            medias = new string[elementsCount];
            //streamInfos = new string[elementsCount];
            urls = new string[elementsCount];
            for (int i = 2; i < strings.Length; i += 3)
            {
                int id = (i - 2) / 3;
                medias[id] = strings[i];
                //streamInfos[id] = strings[i + 1];
                urls[id] = strings[i + 2];
            }
        }

        public string GetGroupId(int n)
        {
            string t = medias[n];
            int j = t.IndexOf("GROUP-ID=\"");
            t = t.Remove(0, j + 10);
            j = t.IndexOf("\"");
            t = t.Substring(0, j);
            return t;
        }

        public string GetUrl(int n)
        {
            return urls[n];
        }

        public int GetCount()
        {
            return elementsCount;
        }
    }
}

Мне надо было достать только GROUP-ID и урлы. На удивление, как-то совсем просто получилось.
Знаю, что можно сделать и лучше, но пока не знаю как и сейчас не до этого.

Лучше список каких-нибудь объектов возвращать (или Dictionary), иначе с результатом неудобно работать.

И наверно надежнее отфильтровать нужные строки, чем так высчитывать индексы, потому что в M3U могут быть другие строки между ними и т.д.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;

public class StreamInfo
{
    public string GroupId;
    public string Url;
}

public class Program
{
    public static List<StreamInfo> Parse(string m3u)
    {
        var lines = m3u.Split('\n').Select(s => s.Trim());
        var usefulLines = lines.Where(line => line.StartsWith("http") || line.Contains("GROUP-ID")).ToList();

        var regex = new Regex("GROUP-ID=\"(.+?)\"");

        var result = new List<StreamInfo>();
        for (int i = 0; i < usefulLines.Count - 1; i += 2)
        {
            string info = usefulLines[i];
            string url = usefulLines[i + 1];

            var match = regex.Match(info);
            if (!match.Success || !url.StartsWith("http"))
            {
                throw new Exception($"Invalid input: {info}\\n{url}");
            }

            result.Add(new StreamInfo { GroupId = match.Groups[1].Value, Url = url });
        }

        return result;
    }

    public static void Main()
    {
        string input = @"#EXTM3U
#EXT-X-TWITCH-INFO:NODE=""video-edge-c685ec.arn03"",MANIFEST-NODE-TYPE=""weaver_cluster"",MANIFEST-NODE=""video-weaver.fra05"",SUPPRESS=""true"",SERVER-TIME=""1610277986.44"",TRANSCODESTACK=""2017TranscodeQS_V2"",USER-IP=""46.175.35.158"",SERVING-ID=""1550fbfb04504907aff64a3e037f0f0c"",CLUSTER=""arn03"",ABS=""false"",VIDEO-SESSION-ID=""5663013854168568271"",BROADCAST-ID=""40128701277"",STREAM-TIME=""5165.443388"",FUTURE=""true"",B=""false"",USER-COUNTRY=""RU"",MANIFEST-CLUSTER=""fra05"",ORIGIN=""cmh01"",C=""aHR0cHM6Ly92aWRlby1lZGdlLTFiY2MxNi5wZHgwMS5hYnMuaGxzLnR0dm53Lm5ldC92MS9zZWdtZW50L0N2TUJTVUFaQW1Fb1JhNmxKZllyd0Vra3psT0gwYlR5ZE1hR3VPNjNMZHozSkl5dEE5SUd6WmQ2QUFGRlhUYXZQdXQ0b0JueC1LRFg2YmJraFZuaWFKNTZaUHZCYmFNNEstSWo5N2pkWWhBZWxkX2IxTzBwTmNIWk9BeTNXemU5Vm9JMEtoVHVGLVQ4aFJ1alhqVjNkUS1RZC1QaDZkQWFfb180NUEwa0NvYXhGVjJWUElwa1kxVzFtQk83YWVSNTI1YTVic0JsdjRqeFlWLVNSY3lqdXQ1QS1wb0dYdkJ0cGRoVnVtMzJ3U1h4MXpZd3VPam82UGpvblVwNjVfVEQzbmJsNVB5TDZhZ1JPUEZpaGw5aXhndXJNdmZvUnQ0bkFxZzFKV1JUeVYzaG5HQTA3SzcwcUhpbzhnR0NZTS1fVURBb095N2dtSUp2X2VVQm9ueUY4LXZoLW5aemhuR1YwUDgxd2IzT09hQmdjX1JRNTgtd2NENWNjdWNaWGkxT25HUWNSM3FNdmprelQtOFY0QWc5eXBlVTdHczJlVEhTMENHZXVORExXUm54RVVrTFRHNXJBa09iY2l3d0stMTY4Qk1MUVQ5UEFJMWVlc1NnSHNhazFuMVFzQ1hBLVh5YW95N0ZidGNxX0tfZU9VWmc1cVA5dkVhVnNDTEhxVlJxZkhkRktZdEl2alJ1aGlNV2gtMl9qUHFzamtiSXpFOWNoV2h0Y1ltYmRreXpmcy1tWURLcTZ3OWUtdTdTWFg4VUhFTnpDdDZraDJxZ3M0cUduZUVhelh5S0lndTkwQnNsTnlQWE1XeGxmNzdTUnhza0hOTnhSWHEtRjh2dG85SDIxQWxOMHU1V3ZocjRvczhGbGxlQWdwSDhScndmSXFnNExpRUxlemZuUTNvSGVEdjkyZGZIcjlQZmVNZnNtb3BPWXgyS2xmMkZCRWZnN0g4Y0FRbHQxdjZVSzJfNEVDVml1N3Y0ZElaNk9KTmN5cExUV0YtVlJFdlNsd1pHcnoudHM"",D=""false""
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=""chunked"",NAME=""1080p60 (source)"",AUTOSELECT=YES,DEFAULT=YES
#EXT-X-STREAM-INF:BANDWIDTH=5813280,RESOLUTION=1920x1080,CODECS=""avc1.64002A,mp4a.40.2"",VIDEO=""chunked"",FRAME-RATE=60.000
https://twitch1
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=""720p60"",NAME=""720p60"",AUTOSELECT=YES,DEFAULT=YES
#EXT-X-STREAM-INF:BANDWIDTH=3428223,RESOLUTION=1280x720,CODECS=""avc1.4D401F,mp4a.40.2"",VIDEO=""720p60"",FRAME-RATE=60.000
https://twitch2
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=""480p30"",NAME=""480p"",AUTOSELECT=YES,DEFAULT=YES
#EXT-X-STREAM-INF:BANDWIDTH=1433223,RESOLUTION=852x480,CODECS=""avc1.4D401F,mp4a.40.2"",VIDEO=""480p30"",FRAME-RATE=30.000
https://twitch3
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=""360p30"",NAME=""360p"",AUTOSELECT=YES,DEFAULT=YES
#EXT-X-STREAM-INF:BANDWIDTH=630000,RESOLUTION=640x360,CODECS=""avc1.4D401F,mp4a.40.2"",VIDEO=""360p30"",FRAME-RATE=30.000
https://twitch4
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=""160p30"",NAME=""160p"",AUTOSELECT=YES,DEFAULT=YES
#EXT-X-STREAM-INF:BANDWIDTH=230000,RESOLUTION=284x160,CODECS=""avc1.4D401F,mp4a.40.2"",VIDEO=""160p30"",FRAME-RATE=30.000
https://twitch5";

        var streams = Parse(input);
        foreach (var stream in streams)
        {
            Console.WriteLine($"{stream.GroupId} - {stream.Url}");
        }
    }
}