From a6ec011e7992ff72a98fd1688a8daecbcbfe94f8 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Sun, 22 Mar 2026 09:14:39 +0200 Subject: [PATCH] Enhance error handling and logging in M3U and XML downloading processes; implement proper application shutdown in MainViewModel; introduce PlaylistSettings for configurable URLs and timeout settings. --- TV Player WPF/PlaylistWorker/M3UParser.cs | 118 ++++++++++++++++------ TV Player WPF/ViewModels/MainViewModel.cs | 9 +- TV Player/Handlers/AndroidHandler.cs | 76 +------------- TV Player/Handlers/MediaViewerHandler.cs | 25 +---- TV Player/ViewModels/M3UParser.cs | 103 ++++++++++++++----- TV Player/ViewModels/MainViewModel.cs | 44 +++++--- TV Player/ViewModels/PlayerViewModel.cs | 12 ++- TV Player/ViewModels/PlaylistSettings.cs | 43 ++++++++ TV Player/ViewModels/TVPlayerViewModel.cs | 21 ++-- 9 files changed, 270 insertions(+), 181 deletions(-) create mode 100644 TV Player/ViewModels/PlaylistSettings.cs diff --git a/TV Player WPF/PlaylistWorker/M3UParser.cs b/TV Player WPF/PlaylistWorker/M3UParser.cs index fff9939..0a2350e 100644 --- a/TV Player WPF/PlaylistWorker/M3UParser.cs +++ b/TV Player WPF/PlaylistWorker/M3UParser.cs @@ -66,16 +66,28 @@ namespace TV_Player private static async Task DownloadXMLProgram(string url) { - using (var client = new HttpClient()) - using (var request = new HttpRequestMessage()) + try { - 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(); - string responseBody = await response.Content.ReadAsStringAsync(); - return responseBody; + 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; } } @@ -91,15 +103,22 @@ namespace TV_Player using MemoryStream decompressedStream = new MemoryStream(); await decompressionStream.CopyToAsync(decompressedStream); - // Step 3: Get the inner XML as a string string xmlContent = Encoding.UTF8.GetString(decompressedStream.ToArray()); - - // Output the XML content 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) { - Console.WriteLine($"An error occurred: {ex.Message}"); + System.Diagnostics.Debug.WriteLine($"Error decompressing gzip: {ex.Message}"); return null; } } @@ -117,6 +136,12 @@ namespace TV_Player string programDataPath = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData); 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 @@ -134,7 +159,6 @@ namespace TV_Player }; reader.Read(); channel.DisplayName = reader.ReadElementContentAsString(); - } continue; } @@ -145,22 +169,33 @@ namespace TV_Player var id = reader.GetAttribute("channel"); if (id != channelId) continue; - program.StartTime = DateTime.ParseExact(reader.GetAttribute("start"), "yyyyMMddHHmmss zzz", null); - program.EndTime = DateTime.ParseExact(reader.GetAttribute("stop"), "yyyyMMddHHmmss zzz", null); + + 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); } - else if (reader.NodeType == XmlNodeType.EndElement && reader.Name == "channel") - { - // break; - } } } + 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; @@ -168,18 +203,29 @@ namespace TV_Player private static async Task ReadFile(string url) { - string responseBody; - using (var client = new HttpClient()) - using (var request = new HttpRequestMessage()) + try { - 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(); - responseBody = await response.Content.ReadAsStringAsync(); + 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; } - return responseBody; } public static async Task<(List programList, string programGuide)> DownloadM3UFromWebAsync(string url) @@ -220,6 +266,12 @@ namespace TV_Player string programGuideLink = string.Empty; try { + if (string.IsNullOrWhiteSpace(content)) + { + System.Diagnostics.Debug.WriteLine("M3U content is empty"); + return (playlistItems, programGuideLink); + } + var m3u = SplitStringBeforeSeparator(content, "#EXT"); foreach (var line in m3u) @@ -238,9 +290,13 @@ namespace TV_Player } } } + catch (ArgumentException ex) + { + System.Diagnostics.Debug.WriteLine($"Invalid M3U format: {ex.Message}"); + } catch (Exception ex) { - Console.WriteLine("Error reading M3U file: " + ex.Message); + System.Diagnostics.Debug.WriteLine($"Error parsing M3U file: {ex.Message}"); } return (playlistItems, programGuideLink); diff --git a/TV Player WPF/ViewModels/MainViewModel.cs b/TV Player WPF/ViewModels/MainViewModel.cs index dcd3094..1e2537d 100644 --- a/TV Player WPF/ViewModels/MainViewModel.cs +++ b/TV Player WPF/ViewModels/MainViewModel.cs @@ -69,7 +69,14 @@ namespace TV_Player public void OnCloseAppButtonClick() { - Environment.Exit(0); + if (Application.Current?.MainWindow is Window window) + { + window.Close(); // Allows proper shutdown sequence + } + else + { + Environment.Exit(0); // Fallback if window not available + } } private void OnButtonBackClick() diff --git a/TV Player/Handlers/AndroidHandler.cs b/TV Player/Handlers/AndroidHandler.cs index 6c684a0..af4197c 100644 --- a/TV Player/Handlers/AndroidHandler.cs +++ b/TV Player/Handlers/AndroidHandler.cs @@ -1,74 +1,6 @@ -using Microsoft.Maui.Handlers; -using static Microsoft.Maui.ApplicationModel.Permissions; - -namespace TV_Player.MAUI +namespace TV_Player.MAUI { - //public partial class MediaViewerHandler : ViewHandler - //{ - // VideoView _videoView; - // LibVLC _libVLC; - // LibVLCSharp.Shared.MediaPlayer _mediaPlayer; - - // protected override VideoView CreatePlatformView() => new VideoView(Context); - - // protected override void ConnectHandler(VideoView nativeView) - // { - // base.ConnectHandler(nativeView); - - // _libVLC = new LibVLC(enableDebugLogs: true); - // _mediaPlayer = new LibVLCSharp.Shared.MediaPlayer(_libVLC) - // { - // EnableHardwareDecoding = true - // }; - - // _videoView = nativeView ?? new VideoView(Context); - // _videoView.MediaPlayer = _mediaPlayer; - - // HandleUrl(VirtualView.StreamUrl); - - // base.ConnectHandler(nativeView); - // } - - // protected override void DisconnectHandler(VideoView nativeView) - // { - // nativeView.Dispose(); - // base.DisconnectHandler(nativeView); - // } - - // private void HandleUrl(string url) - // { - // try - // { - - // if (url.EndsWith("/")) - // { - // url = url.TrimEnd('/'); - // } - - // //url = "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"; - - // if (!string.IsNullOrEmpty(url)) - // { - // var media = new Media(_libVLC, url, FromType.FromLocation); - - // _mediaPlayer.NetworkCaching = 1500; - - // if (_mediaPlayer.Media != null) - // { - // _mediaPlayer.Stop(); - // _mediaPlayer.Media.Dispose(); - // } - - // _mediaPlayer.Media = media; - // _mediaPlayer.Mute = true; - - // _videoView.MediaPlayer.Play(); - // } - // } - // catch (Exception ex) - // { - // } - // } - - //} + // AndroidHandler - Platform-specific implementation for Android video player + // Currently using built-in media player implementation + // Reference: LibVLC implementation was considered but currently not in use } diff --git a/TV Player/Handlers/MediaViewerHandler.cs b/TV Player/Handlers/MediaViewerHandler.cs index d563361..a49ece6 100644 --- a/TV Player/Handlers/MediaViewerHandler.cs +++ b/TV Player/Handlers/MediaViewerHandler.cs @@ -1,24 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace TV_Player.MAUI +namespace TV_Player.MAUI { -// public partial class MediaViewerHandler -// { -// public static IPropertyMapper PropertyMapper = -// new PropertyMapper() -// { -// }; - -// public static CommandMapper CommandMapper = new(ViewCommandMapper) -// { -// }; - -// public MediaViewerHandler() : base(PropertyMapper, CommandMapper) -// { -// } -// } + // MediaViewerHandler is currently not implemented + // This is reserved for custom platform-specific media player implementations } diff --git a/TV Player/ViewModels/M3UParser.cs b/TV Player/ViewModels/M3UParser.cs index 67d71dc..4eab95c 100644 --- a/TV Player/ViewModels/M3UParser.cs +++ b/TV Player/ViewModels/M3UParser.cs @@ -28,17 +28,32 @@ namespace TV_Player.MAUI public static async Task> DownloadGuideFromWebAsync(string url) { - List epgChannels = new List(); ; - using (var client = new HttpClient()) - using (var request = new HttpRequestMessage()) + List epgChannels = new List(); + try { - 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(); - string responseBody = await response.Content.ReadAsStringAsync(); - epgChannels = ParseEpg(responseBody); + 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(); + string responseBody = await response.Content.ReadAsStringAsync(); + epgChannels = ParseEpg(responseBody); + } + } + catch (HttpRequestException ex) + { + System.Diagnostics.Debug.WriteLine($"Network error downloading EPG from {url}: {ex.Message}"); + } + catch (UriFormatException ex) + { + System.Diagnostics.Debug.WriteLine($"Invalid EPG URL: {url} - {ex.Message}"); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Unexpected error downloading EPG: {ex.Message}"); } return epgChannels; } @@ -73,13 +88,19 @@ namespace TV_Player.MAUI var id = reader.GetAttribute("channel"); var channel = epgChannels.FirstOrDefault(x => x.Id == id); - program.StartTime = DateTime.ParseExact(reader.GetAttribute("start"), "yyyyMMddHHmmss zzz", null); - program.EndTime = DateTime.ParseExact(reader.GetAttribute("stop"), "yyyyMMddHHmmss zzz", null); + if (channel == null) 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); } else if (reader.NodeType == XmlNodeType.EndElement && reader.Name == "channel") @@ -88,25 +109,43 @@ namespace TV_Player.MAUI } } } - catch{} + catch (XmlException ex) + { + System.Diagnostics.Debug.WriteLine($"XML parsing error: {ex.Message}"); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Unexpected error parsing EPG: {ex.Message}"); + } } return epgChannels; } private static async Task ReadFile(string url) { - string responseBody; - using (var client = new HttpClient()) - using (var request = new HttpRequestMessage()) + try { - 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(); - responseBody = await response.Content.ReadAsStringAsync(); + 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 from {url}: {ex.Message}"); + throw; + } + catch (UriFormatException ex) + { + System.Diagnostics.Debug.WriteLine($"Invalid URL: {url} - {ex.Message}"); + throw; } - return responseBody; } public static async Task<(List programList, string programGuide)> DownloadM3UFromWebAsync(string url) @@ -139,6 +178,12 @@ namespace TV_Player.MAUI string programGuideLink = string.Empty; try { + if (string.IsNullOrWhiteSpace(content)) + { + System.Diagnostics.Debug.WriteLine("M3U content is empty"); + return (playlistItems, programGuideLink); + } + var m3u = SplitStringBeforeSeparator(content, "#EXT"); foreach (var line in m3u) @@ -152,16 +197,20 @@ namespace TV_Player.MAUI } if (line.StartsWith("#EXTM3U")) { - programGuideLink=ExtractXtvgUrl(line); + programGuideLink = ExtractXtvgUrl(line); } } } + catch (ArgumentException ex) + { + System.Diagnostics.Debug.WriteLine($"Invalid M3U format: {ex.Message}"); + } catch (Exception ex) { - Console.WriteLine("Error reading M3U file: " + ex.Message); + System.Diagnostics.Debug.WriteLine($"Error parsing M3U file: {ex.Message}"); } - return (playlistItems,programGuideLink); + return (playlistItems, programGuideLink); } private static bool TryParseM3ULine(string m3uLine, out M3UInfo? info) diff --git a/TV Player/ViewModels/MainViewModel.cs b/TV Player/ViewModels/MainViewModel.cs index 9e2acfc..ac1d598 100644 --- a/TV Player/ViewModels/MainViewModel.cs +++ b/TV Player/ViewModels/MainViewModel.cs @@ -1,10 +1,11 @@ using System.Windows.Input; +using System.Reactive.Disposables; namespace TV_Player.MAUI { - public class MainViewModel : ObservableViewModelBase + public class MainViewModel : ObservableViewModelBase, IDisposable { - private IDisposable _groupsSubscriber; + private CompositeDisposable _disposables = new CompositeDisposable(); private List _programs; public List Programs { @@ -18,24 +19,39 @@ namespace TV_Player.MAUI public MainViewModel() { ItemSelectedCommand = new Command(OnItemSelected); - _groupsSubscriber = TVPlayerViewModel.Instance.PlaylistData.GroupsInformation.Subscribe(x => Programs = x); + _disposables.Add( + TVPlayerViewModel.Instance.PlaylistData.GroupsInformation.Subscribe(x => Programs = x) + ); } private void OnItemSelected() { - var navigation = (INavigation)Application.Current.MainPage.Navigation; - - var programPageViewModel = new ProgramViewModel(SelectedItem); - - // Create a new SecondPage and set its BindingContext to the ViewModel - var programPage = new ProgramPage + try { - BindingContext = programPageViewModel - }; - // Navigate to the OtherPage - navigation.PushAsync(programPage); + if (Application.Current?.MainPage?.Navigation == null) + { + System.Diagnostics.Debug.WriteLine("Navigation context is not available"); + return; + } - _groupsSubscriber.Dispose(); + var programPageViewModel = new ProgramViewModel(SelectedItem); + + var programPage = new ProgramPage + { + BindingContext = programPageViewModel + }; + + Application.Current.MainPage.Navigation.PushAsync(programPage); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Navigation error: {ex.Message}"); + } + } + + public void Dispose() + { + _disposables?.Dispose(); } } } diff --git a/TV Player/ViewModels/PlayerViewModel.cs b/TV Player/ViewModels/PlayerViewModel.cs index b599a65..6440402 100644 --- a/TV Player/ViewModels/PlayerViewModel.cs +++ b/TV Player/ViewModels/PlayerViewModel.cs @@ -38,11 +38,17 @@ namespace TV_Player.MAUI { try { - URLSource = _currentProgram.Url; + if (string.IsNullOrWhiteSpace(url)) + { + System.Diagnostics.Debug.WriteLine("Invalid URL for playback"); + return; + } + + URLSource = url; } - catch + catch (Exception ex) { - // Handle exceptions + System.Diagnostics.Debug.WriteLine($"Error playing M3U8: {ex.Message}"); } } } diff --git a/TV Player/ViewModels/PlaylistSettings.cs b/TV Player/ViewModels/PlaylistSettings.cs new file mode 100644 index 0000000..c4023e7 --- /dev/null +++ b/TV Player/ViewModels/PlaylistSettings.cs @@ -0,0 +1,43 @@ +namespace TV_Player.MAUI +{ + /// + /// Configuration settings for playlist and EPG sources + /// + public class PlaylistSettings + { + /// + /// URL to the M3U playlist file + /// + public string M3UUrl { get; set; } + + /// + /// URL to the EPG (Electronic Program Guide) XML file + /// Can be overridden by x-tvg-url in M3U file + /// + public string EpgUrl { get; set; } + + /// + /// Connection timeout in seconds + /// + public int TimeoutSeconds { get; set; } = 30; + + /// + /// Whether to cache EPG data locally + /// + public bool CacheEpgLocally { get; set; } = true; + + /// + /// Cache validity period in days + /// + public int CacheValidityDays { get; set; } = 3; + + /// + /// Load default settings + /// + public static PlaylistSettings Default => new PlaylistSettings + { + M3UUrl = "http://pl.da-tv.vip/a71e77fa/835b3216/tv.m3u", + EpgUrl = string.Empty // Will be extracted from M3U file + }; + } +} diff --git a/TV Player/ViewModels/TVPlayerViewModel.cs b/TV Player/ViewModels/TVPlayerViewModel.cs index aaf5fcf..a80b46f 100644 --- a/TV Player/ViewModels/TVPlayerViewModel.cs +++ b/TV Player/ViewModels/TVPlayerViewModel.cs @@ -7,21 +7,20 @@ namespace TV_Player.MAUI public Action ButtonBackAction { get; set; } - private static TVPlayerViewModel _instance; - public static TVPlayerViewModel Instance - { - get - { - if (_instance == null) - _instance = new TVPlayerViewModel(); - return _instance; - } - } + private static readonly Lazy _instance = + new Lazy( + () => new TVPlayerViewModel(), + LazyThreadSafetyMode.ExecutionAndPublication); + + public static TVPlayerViewModel Instance => _instance.Value; public TVPlayerViewModel() { PlaylistData = new ProgramsData(); - PlaylistData.GetData("http://pl.da-tv.vip/a71e77fa/835b3216/tv.m3u"); + + // Load settings - can be overridden with custom configuration + var settings = PlaylistSettings.Default; + PlaylistData.GetData(settings.M3UUrl); } }