using System.IO; using System.IO.Compression; using System.Net.Http; using System.Net.Http.Headers; using System.Security.Policy; using System.Text; using System.Text.RegularExpressions; using System.Xml; namespace TV_Player { public class ProgramInfo : ObservableViewModelBase { private string _title; private int _durationValue; public string Title { get => _title; set => SetProperty(ref _title, value); } public DateTime StartTime { get; set; } public DateTime EndTime { get; set; } public int DurationValue { get => _durationValue; set => SetProperty(ref _durationValue, value); } } public class ProgramGuide { public string Id { get; set; } public string DisplayName { get; set; } public List Programs { get; set; } = new List(); } public static class M3UParser { private static string GetWritableAppDataFolder() { var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); if (!string.IsNullOrWhiteSpace(localAppData)) { return localAppData; } var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); if (!string.IsNullOrWhiteSpace(appData)) { return appData; } return AppContext.BaseDirectory; } public static async Task DownloadGuideFromWebAsync(string name, string url) { var fileName = name + "_guide.xml"; string programDataPath = GetWritableAppDataFolder(); string filePath = Path.Combine(programDataPath, "TVPlayer", fileName); if (File.Exists(filePath)) { DateTime creationTime = File.GetCreationTime(filePath); DateTime modificationTime = File.GetLastWriteTime(filePath); DateTime currentTime = DateTime.Now; if ((currentTime - creationTime).TotalDays < 3 || (currentTime - modificationTime).TotalDays < 3) { return; } } ; var channelsContent = string.Empty; if (url.Contains(".gz")) { channelsContent = await DownloadGzip(url); } else { channelsContent = await DownloadXMLProgram(url); } Directory.CreateDirectory(Path.GetDirectoryName(filePath)); await File.WriteAllTextAsync(filePath, channelsContent); } private static async Task DownloadXMLProgram(string url) { try { using (var client = new HttpClient()) using (var request = new HttpRequestMessage()) { client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/text")); request.Method = HttpMethod.Get; request.RequestUri = new Uri(url); var response = await client.GetAsync(url); response.EnsureSuccessStatusCode(); return await response.Content.ReadAsStringAsync(); } } catch (HttpRequestException ex) { System.Diagnostics.Debug.WriteLine($"Network error downloading XML from {url}: {ex.Message}"); throw; } catch (UriFormatException ex) { System.Diagnostics.Debug.WriteLine($"Invalid URL: {url} - {ex.Message}"); throw; } } public static async Task DownloadGzip(string url) { try { using HttpClient httpClient = new HttpClient(); byte[] compressedData = await httpClient.GetByteArrayAsync(url); using MemoryStream compressedStream = new MemoryStream(compressedData); using GZipStream decompressionStream = new GZipStream(compressedStream, CompressionMode.Decompress); using MemoryStream decompressedStream = new MemoryStream(); await decompressionStream.CopyToAsync(decompressedStream); string xmlContent = Encoding.UTF8.GetString(decompressedStream.ToArray()); return xmlContent; } catch (HttpRequestException ex) { System.Diagnostics.Debug.WriteLine($"Network error downloading gzip from {url}: {ex.Message}"); return null; } catch (InvalidOperationException ex) { System.Diagnostics.Debug.WriteLine($"Invalid gzip data: {ex.Message}"); return null; } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"Error decompressing gzip: {ex.Message}"); return null; } } public static async Task ParseEpg(string groupName, string channelId) { XmlReaderSettings settings = new XmlReaderSettings(); settings.IgnoreWhitespace = true; settings.IgnoreComments = true; settings.DtdProcessing = DtdProcessing.Parse; settings.Async = true; ProgramGuide channel = null; var fileName = groupName + "_guide.xml"; string programDataPath = GetWritableAppDataFolder(); string filePath = Path.Combine(programDataPath, "TVPlayer", fileName); if (!File.Exists(filePath)) { System.Diagnostics.Debug.WriteLine($"EPG file not found: {filePath}"); return null; } using (XmlReader reader = XmlReader.Create(filePath, settings)) { try { while (await reader.ReadAsync()) { if (reader.NodeType == XmlNodeType.Element && reader.Name == "channel") { var id = reader.GetAttribute("id"); if (id == channelId) { channel = new ProgramGuide { Id = id }; reader.Read(); channel.DisplayName = reader.ReadElementContentAsString(); } continue; } if (channel != null && reader.NodeType == XmlNodeType.Element && reader.Name == "programme") { ProgramInfo program = new ProgramInfo(); var id = reader.GetAttribute("channel"); if (id != channelId) continue; if (!DateTime.TryParseExact(reader.GetAttribute("start"), "yyyyMMddHHmmss zzz", System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out var startTime)) continue; if (!DateTime.TryParseExact(reader.GetAttribute("stop"), "yyyyMMddHHmmss zzz", System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out var endTime)) continue; program.StartTime = startTime; program.EndTime = endTime; reader.Read(); program.Title = reader.ReadElementContentAsString(); channel.Programs.Add(program); } } } catch (XmlException ex) { System.Diagnostics.Debug.WriteLine($"XML parsing error in EPG file: {ex.Message}"); } catch (FileNotFoundException ex) { System.Diagnostics.Debug.WriteLine($"EPG file not found: {ex.Message}"); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"Unexpected error parsing EPG: {ex.Message}"); } } return channel; } private static async Task ReadFile(string url) { try { using var client = new HttpClient(); using var request = new HttpRequestMessage(); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/text")); request.Method = HttpMethod.Get; request.RequestUri = new Uri(url); var response = await client.GetAsync(url); response.EnsureSuccessStatusCode(); return await response.Content.ReadAsStringAsync(); } catch (HttpRequestException ex) { System.Diagnostics.Debug.WriteLine($"Network error reading file from {url}: {ex.Message}"); throw; } catch (UriFormatException ex) { System.Diagnostics.Debug.WriteLine($"Invalid URL: {url} - {ex.Message}"); throw; } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"Error reading file from {url}: {ex.Message}"); throw; } } public static async Task<(List programList, string programGuide)> DownloadM3UFromWebAsync(string url) { string fileData; if (Uri.IsWellFormedUriString(url, UriKind.Absolute)) { fileData = await ReadFile(url); } else { fileData = File.ReadAllText(url); } return ParseM3UFromString(fileData); } private static string[] SplitStringBeforeSeparator(string input, string separator) { string[] parts = input.Split(separator); // Reconstruct the string until the separator is reached int separatorIndex = input.IndexOf(separator); if (separatorIndex != -1) { parts[0] = input.Substring(0, separatorIndex + 1); for (int i = 1; i < parts.Length; i++) { parts[i] = separator + parts[i]; } } return parts; } private static (List programList, string programGuide) ParseM3UFromString(string content) { List playlistItems = new List(); string programGuideLink = string.Empty; try { if (string.IsNullOrWhiteSpace(content)) { System.Diagnostics.Debug.WriteLine("[M3UParser] M3U content is empty"); return (playlistItems, programGuideLink); } System.Diagnostics.Debug.WriteLine($"[M3UParser] Starting parse of M3U content ({content.Length} bytes)"); var m3u = SplitStringBeforeSeparator(content, "#EXT"); foreach (var line in m3u) { if (line.StartsWith("#EXTINF:")) { if (TryParseM3ULine(line, out var m3uInfo)) { if (!string.IsNullOrEmpty(m3uInfo?.Url)) { playlistItems.Add(m3uInfo); System.Diagnostics.Debug.WriteLine($"[M3UParser] Parsed: {m3uInfo.Name} -> group='{m3uInfo.GroupTitle}'"); } } } if (line.StartsWith("#EXTM3U")) { programGuideLink = ExtractXtvgUrl(line); } } var groupSummary = playlistItems.GroupBy(p => p.GroupTitle).Select(g => $"{g.Key}({g.Count()})").ToList(); System.Diagnostics.Debug.WriteLine($"[M3UParser] Parse complete: {playlistItems.Count} programs in {groupSummary.Count} groups: {string.Join(", ", groupSummary)}"); } catch (ArgumentException ex) { System.Diagnostics.Debug.WriteLine($"Invalid M3U format: {ex.Message}"); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"Error parsing M3U file: {ex.Message}"); } return (playlistItems, programGuideLink); } private static bool TryParseM3ULine(string m3uLine, out M3UInfo? info) { info = null; string pattern = @"#EXTINF:(-?\d+)\s*?(?:timeshift=""(?.*?)""\s+)?(?:catchup-days=""(?.*?)""\s+)?(?:catchup-type=""(?.*?)""\s+)?(?:CUID=""(?.*?)""\s+)?(?:number=""(?.*?)""\s+)?(?:tvg-id=""(?.*?)""\s+)?(?:tvg-name=""(?.*?)""\s+)?(?:group-title=""(?.*?)""\s+)?(?:tvg-logo=""(?.*?)""\s*)?(?:group-title=""(?.*?)"")?(?:,(?.*?)\s*\r?\n(?.*))"; //string pattern = @"#EXTINF:(-?\d+)\s+?(?:timeshift=""(?.*?)""\s+)?(?:catchup-days=""(?.*?)""\s+)?(?:catchup-type=""(?.*?)""\s+)?(?:CUID=""(?.*?)""\s+)?(?:number=""(?.*?)""\s+)?(?:tvg-id=""(?.*?)""\s+)?(?:tvg-name=""(?.*?)""\s+)?(?:group-title=""(?.*?)""\s+)?(?:tvg-logo=""(?.*?)"")?,(?.*?)\s*\r?\n(?.*)"; Regex regex = new Regex(pattern, RegexOptions.IgnoreCase); Match match = regex.Match(m3uLine); if (match.Success) { info = new M3UInfo { CUID = match.Groups["CUID"].Value, Number = match.Groups["Number"].Value, TvgID = match.Groups["TvgID"].Value, TvgName = match.Groups["TvgName"].Value, GroupTitle = string.IsNullOrEmpty(match.Groups["GroupTitle"].Value) ? "undefined" : match.Groups["GroupTitle"].Value, Logo = match.Groups["Logo"].Value, Name = match.Groups["Name"].Value, Url = match.Groups["URL"].Value }; return true; } return false; } private static string ExtractXtvgUrl(string m3uEntry) { // Define a regular expression pattern to match x-tvg-url attribute string pattern = @"(x-tvg-url=""(.*?)"")?(url-tvg=""(.*?)"")"; // Use Regex.Match to find the first match Match match = Regex.Match(m3uEntry, pattern); // Check if a match is found and get the value from the capturing group if (match.Success && match.Groups.Count > 1) { return match.Groups[4].Value; } // Return null or an empty string if no match is found return string.Empty; } } }