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.

This commit is contained in:
Vladimir
2026-03-22 09:14:39 +02:00
parent 59d0ed1ab5
commit a6ec011e79
9 changed files with 270 additions and 181 deletions
+72 -16
View File
@@ -65,6 +65,8 @@ namespace TV_Player
} }
private static async Task<string> DownloadXMLProgram(string url) private static async Task<string> DownloadXMLProgram(string url)
{
try
{ {
using (var client = new HttpClient()) using (var client = new HttpClient())
using (var request = new HttpRequestMessage()) using (var request = new HttpRequestMessage())
@@ -74,8 +76,18 @@ namespace TV_Player
request.RequestUri = new Uri(url); request.RequestUri = new Uri(url);
var response = await client.GetAsync(url); var response = await client.GetAsync(url);
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
string responseBody = await response.Content.ReadAsStringAsync(); return await response.Content.ReadAsStringAsync();
return responseBody; }
}
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(); using MemoryStream decompressedStream = new MemoryStream();
await decompressionStream.CopyToAsync(decompressedStream); await decompressionStream.CopyToAsync(decompressedStream);
// Step 3: Get the inner XML as a string
string xmlContent = Encoding.UTF8.GetString(decompressedStream.ToArray()); string xmlContent = Encoding.UTF8.GetString(decompressedStream.ToArray());
// Output the XML content
return xmlContent; 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) catch (Exception ex)
{ {
Console.WriteLine($"An error occurred: {ex.Message}"); System.Diagnostics.Debug.WriteLine($"Error decompressing gzip: {ex.Message}");
return null; return null;
} }
} }
@@ -117,6 +136,12 @@ namespace TV_Player
string programDataPath = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData); string programDataPath = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
string filePath = Path.Combine(programDataPath, "TVPlayer", fileName); 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)) using (XmlReader reader = XmlReader.Create(filePath, settings))
{ {
try try
@@ -134,7 +159,6 @@ namespace TV_Player
}; };
reader.Read(); reader.Read();
channel.DisplayName = reader.ReadElementContentAsString(); channel.DisplayName = reader.ReadElementContentAsString();
} }
continue; continue;
} }
@@ -145,22 +169,33 @@ namespace TV_Player
var id = reader.GetAttribute("channel"); var id = reader.GetAttribute("channel");
if (id != channelId) continue; 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(); reader.Read();
program.Title = reader.ReadElementContentAsString(); program.Title = reader.ReadElementContentAsString();
channel.Programs.Add(program); channel.Programs.Add(program);
} }
else if (reader.NodeType == XmlNodeType.EndElement && reader.Name == "channel") }
}
catch (XmlException ex)
{ {
// break; 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) catch (Exception ex)
{ {
System.Diagnostics.Debug.WriteLine($"Unexpected error parsing EPG: {ex.Message}");
} }
} }
return channel; return channel;
@@ -168,7 +203,8 @@ namespace TV_Player
private static async Task<string> ReadFile(string url) private static async Task<string> ReadFile(string url)
{ {
string responseBody; try
{
using (var client = new HttpClient()) using (var client = new HttpClient())
using (var request = new HttpRequestMessage()) using (var request = new HttpRequestMessage())
{ {
@@ -177,9 +213,19 @@ namespace TV_Player
request.RequestUri = new Uri(url); request.RequestUri = new Uri(url);
var response = await client.GetAsync(url); var response = await client.GetAsync(url);
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
responseBody = await response.Content.ReadAsStringAsync(); 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<M3UInfo> programList, string programGuide)> DownloadM3UFromWebAsync(string url) public static async Task<(List<M3UInfo> programList, string programGuide)> DownloadM3UFromWebAsync(string url)
@@ -220,6 +266,12 @@ namespace TV_Player
string programGuideLink = string.Empty; string programGuideLink = string.Empty;
try try
{ {
if (string.IsNullOrWhiteSpace(content))
{
System.Diagnostics.Debug.WriteLine("M3U content is empty");
return (playlistItems, programGuideLink);
}
var m3u = SplitStringBeforeSeparator(content, "#EXT"); var m3u = SplitStringBeforeSeparator(content, "#EXT");
foreach (var line in m3u) 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) 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);
+8 -1
View File
@@ -69,7 +69,14 @@ namespace TV_Player
public void OnCloseAppButtonClick() 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() private void OnButtonBackClick()
+4 -72
View File
@@ -1,74 +1,6 @@
using Microsoft.Maui.Handlers; namespace TV_Player.MAUI
using static Microsoft.Maui.ApplicationModel.Permissions;
namespace TV_Player.MAUI
{ {
//public partial class MediaViewerHandler : ViewHandler<MediaViewer, VideoView> // AndroidHandler - Platform-specific implementation for Android video player
//{ // Currently using built-in media player implementation
// VideoView _videoView; // Reference: LibVLC implementation was considered but currently not in use
// 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)
// {
// }
// }
//}
} }
+3 -22
View File
@@ -1,24 +1,5 @@
using System; namespace TV_Player.MAUI
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace TV_Player.MAUI
{ {
// public partial class MediaViewerHandler // MediaViewerHandler is currently not implemented
// { // This is reserved for custom platform-specific media player implementations
// public static IPropertyMapper<MediaViewer, MediaViewerHandler> PropertyMapper =
// new PropertyMapper<MediaViewer, MediaViewerHandler>()
// {
// };
// public static CommandMapper<MediaViewer, MediaViewerHandler> CommandMapper = new(ViewCommandMapper)
// {
// };
// public MediaViewerHandler() : base(PropertyMapper, CommandMapper)
// {
// }
// }
} }
+58 -9
View File
@@ -28,7 +28,9 @@ namespace TV_Player.MAUI
public static async Task<List<ProgramGuide>> DownloadGuideFromWebAsync(string url) public static async Task<List<ProgramGuide>> DownloadGuideFromWebAsync(string url)
{ {
List<ProgramGuide> epgChannels = new List<ProgramGuide>(); ; List<ProgramGuide> epgChannels = new List<ProgramGuide>();
try
{
using (var client = new HttpClient()) using (var client = new HttpClient())
using (var request = new HttpRequestMessage()) using (var request = new HttpRequestMessage())
{ {
@@ -40,6 +42,19 @@ namespace TV_Player.MAUI
string responseBody = await response.Content.ReadAsStringAsync(); string responseBody = await response.Content.ReadAsStringAsync();
epgChannels = ParseEpg(responseBody); 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; return epgChannels;
} }
@@ -73,13 +88,19 @@ namespace TV_Player.MAUI
var id = reader.GetAttribute("channel"); var id = reader.GetAttribute("channel");
var channel = epgChannels.FirstOrDefault(x => x.Id == id); var channel = epgChannels.FirstOrDefault(x => x.Id == id);
program.StartTime = DateTime.ParseExact(reader.GetAttribute("start"), "yyyyMMddHHmmss zzz", null); if (channel == null) continue;
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(); reader.Read();
program.Title = reader.ReadElementContentAsString(); program.Title = reader.ReadElementContentAsString();
channel.Programs.Add(program); channel.Programs.Add(program);
} }
else if (reader.NodeType == XmlNodeType.EndElement && reader.Name == "channel") else if (reader.NodeType == XmlNodeType.EndElement && reader.Name == "channel")
@@ -88,14 +109,22 @@ 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; return epgChannels;
} }
private static async Task<string> ReadFile(string url) private static async Task<string> ReadFile(string url)
{ {
string responseBody; try
{
using (var client = new HttpClient()) using (var client = new HttpClient())
using (var request = new HttpRequestMessage()) using (var request = new HttpRequestMessage())
{ {
@@ -104,9 +133,19 @@ namespace TV_Player.MAUI
request.RequestUri = new Uri(url); request.RequestUri = new Uri(url);
var response = await client.GetAsync(url); var response = await client.GetAsync(url);
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
responseBody = await response.Content.ReadAsStringAsync(); 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<M3UInfo> programList, string programGuide)> DownloadM3UFromWebAsync(string url) public static async Task<(List<M3UInfo> programList, string programGuide)> DownloadM3UFromWebAsync(string url)
@@ -139,6 +178,12 @@ namespace TV_Player.MAUI
string programGuideLink = string.Empty; string programGuideLink = string.Empty;
try try
{ {
if (string.IsNullOrWhiteSpace(content))
{
System.Diagnostics.Debug.WriteLine("M3U content is empty");
return (playlistItems, programGuideLink);
}
var m3u = SplitStringBeforeSeparator(content, "#EXT"); var m3u = SplitStringBeforeSeparator(content, "#EXT");
foreach (var line in m3u) foreach (var line in m3u)
@@ -156,9 +201,13 @@ namespace TV_Player.MAUI
} }
} }
} }
catch (ArgumentException ex)
{
System.Diagnostics.Debug.WriteLine($"Invalid M3U format: {ex.Message}");
}
catch (Exception ex) 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);
+24 -8
View File
@@ -1,10 +1,11 @@
using System.Windows.Input; using System.Windows.Input;
using System.Reactive.Disposables;
namespace TV_Player.MAUI namespace TV_Player.MAUI
{ {
public class MainViewModel : ObservableViewModelBase public class MainViewModel : ObservableViewModelBase, IDisposable
{ {
private IDisposable _groupsSubscriber; private CompositeDisposable _disposables = new CompositeDisposable();
private List<GroupInfo> _programs; private List<GroupInfo> _programs;
public List<GroupInfo> Programs public List<GroupInfo> Programs
{ {
@@ -18,24 +19,39 @@ namespace TV_Player.MAUI
public MainViewModel() public MainViewModel()
{ {
ItemSelectedCommand = new Command(OnItemSelected); 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() private void OnItemSelected()
{ {
var navigation = (INavigation)Application.Current.MainPage.Navigation; try
{
if (Application.Current?.MainPage?.Navigation == null)
{
System.Diagnostics.Debug.WriteLine("Navigation context is not available");
return;
}
var programPageViewModel = new ProgramViewModel(SelectedItem); var programPageViewModel = new ProgramViewModel(SelectedItem);
// Create a new SecondPage and set its BindingContext to the ViewModel
var programPage = new ProgramPage var programPage = new ProgramPage
{ {
BindingContext = programPageViewModel BindingContext = programPageViewModel
}; };
// Navigate to the OtherPage
navigation.PushAsync(programPage);
_groupsSubscriber.Dispose(); Application.Current.MainPage.Navigation.PushAsync(programPage);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Navigation error: {ex.Message}");
}
}
public void Dispose()
{
_disposables?.Dispose();
} }
} }
} }
+10 -4
View File
@@ -38,11 +38,17 @@ namespace TV_Player.MAUI
{ {
try try
{ {
URLSource = _currentProgram.Url; if (string.IsNullOrWhiteSpace(url))
}
catch
{ {
// Handle exceptions System.Diagnostics.Debug.WriteLine("Invalid URL for playback");
return;
}
URLSource = url;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error playing M3U8: {ex.Message}");
} }
} }
} }
+43
View File
@@ -0,0 +1,43 @@
namespace TV_Player.MAUI
{
/// <summary>
/// Configuration settings for playlist and EPG sources
/// </summary>
public class PlaylistSettings
{
/// <summary>
/// URL to the M3U playlist file
/// </summary>
public string M3UUrl { get; set; }
/// <summary>
/// URL to the EPG (Electronic Program Guide) XML file
/// Can be overridden by x-tvg-url in M3U file
/// </summary>
public string EpgUrl { get; set; }
/// <summary>
/// Connection timeout in seconds
/// </summary>
public int TimeoutSeconds { get; set; } = 30;
/// <summary>
/// Whether to cache EPG data locally
/// </summary>
public bool CacheEpgLocally { get; set; } = true;
/// <summary>
/// Cache validity period in days
/// </summary>
public int CacheValidityDays { get; set; } = 3;
/// <summary>
/// Load default settings
/// </summary>
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
};
}
}
+10 -11
View File
@@ -7,21 +7,20 @@ namespace TV_Player.MAUI
public Action ButtonBackAction { get; set; } public Action ButtonBackAction { get; set; }
private static TVPlayerViewModel _instance; private static readonly Lazy<TVPlayerViewModel> _instance =
public static TVPlayerViewModel Instance new Lazy<TVPlayerViewModel>(
{ () => new TVPlayerViewModel(),
get LazyThreadSafetyMode.ExecutionAndPublication);
{
if (_instance == null) public static TVPlayerViewModel Instance => _instance.Value;
_instance = new TVPlayerViewModel();
return _instance;
}
}
public TVPlayerViewModel() public TVPlayerViewModel()
{ {
PlaylistData = new ProgramsData(); 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);
} }
} }