diff --git a/TV Player Avalonia/TV Player Avalonia.csproj b/TV Player Avalonia/TV Player Avalonia.csproj
index 473f389..966cac9 100644
--- a/TV Player Avalonia/TV Player Avalonia.csproj
+++ b/TV Player Avalonia/TV Player Avalonia.csproj
@@ -23,11 +23,6 @@
-
-
-
-
-
-
+
diff --git a/TV Player Core/GroupInfo.cs b/TV Player Core/GroupInfo.cs
new file mode 100644
index 0000000..f8a3da8
--- /dev/null
+++ b/TV Player Core/GroupInfo.cs
@@ -0,0 +1,9 @@
+namespace TV_Player
+{
+ [Serializable]
+ public class GroupInfo
+ {
+ public string Name { get; set; }
+ public int Count { get; set; }
+ }
+}
diff --git a/TV Player Core/M3UInfo.cs b/TV Player Core/M3UInfo.cs
new file mode 100644
index 0000000..c46ee17
--- /dev/null
+++ b/TV Player Core/M3UInfo.cs
@@ -0,0 +1,14 @@
+namespace TV_Player
+{
+ public class M3UInfo
+ {
+ public string CUID { get; set; }
+ public string Number { get; set; }
+ public string TvgID { get; set; }
+ public string TvgName { get; set; }
+ public string GroupTitle { get; set; }
+ public string Logo { get; set; }
+ public string Name{ get; set; }
+ public string Url { get; set; }
+ }
+}
diff --git a/TV Player Core/M3UParser.cs b/TV Player Core/M3UParser.cs
new file mode 100644
index 0000000..ad2d4ab
--- /dev/null
+++ b/TV Player Core/M3UParser.cs
@@ -0,0 +1,373 @@
+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;
+ }
+ }
+
+ 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;
+ }
+ }
+}
diff --git a/TV Player Core/ObservableViewModelBase.cs b/TV Player Core/ObservableViewModelBase.cs
new file mode 100644
index 0000000..f54f263
--- /dev/null
+++ b/TV Player Core/ObservableViewModelBase.cs
@@ -0,0 +1,28 @@
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+
+namespace TV_Player
+{
+ public abstract class ObservableViewModelBase : INotifyPropertyChanged
+ {
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ protected void RaisePropertyChanged(string propertyName)
+ => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+
+ ///
+ /// Set a property and raise a property changed event if it has changed
+ ///
+ protected bool SetProperty(ref T property, T value, [CallerMemberName] string propertyName = null)
+ {
+ if (EqualityComparer.Default.Equals(property, value))
+ {
+ return false;
+ }
+
+ property = value;
+ RaisePropertyChanged(propertyName);
+ return true;
+ }
+ }
+}
diff --git a/TV Player Core/ProgramsData.cs b/TV Player Core/ProgramsData.cs
new file mode 100644
index 0000000..b4c598a
--- /dev/null
+++ b/TV Player Core/ProgramsData.cs
@@ -0,0 +1,66 @@
+using System.Reactive;
+using System.Reactive.Subjects;
+
+namespace TV_Player
+{
+ public class ProgramsData
+ {
+ private readonly ReplaySubject> programsSubject = new ReplaySubject>();
+ private readonly ReplaySubject> groupsSubject = new ReplaySubject>();
+ private readonly ReplaySubject programGuideSubject = new ReplaySubject();
+ public IObservable> AllPrograms => programsSubject;
+ public IObservable> GroupsInformation => groupsSubject;
+ public IObservable ProgramGuideInfo => programGuideSubject;
+
+ private readonly string _programName;
+ public ProgramsData(string name,string playlistURL)
+ {
+ _programName = name;
+ Task.Run(() => GetPrograms(name,playlistURL));
+ }
+
+ private async Task GetPrograms(string name,string m3uLink)
+ {
+ System.Diagnostics.Debug.WriteLine($"[ProgramsData] Starting download of: {m3uLink}");
+ //string m3uLink = "http://pl.da-tv.vip/a71e77fa/835b3216/tv.m3u";
+ try
+ {
+ var result = await M3UParser.DownloadM3UFromWebAsync(m3uLink);
+ System.Diagnostics.Debug.WriteLine($"[ProgramsData] Downloaded {result.programList.Count} programs");
+
+ programsSubject.OnNext(result.programList);
+
+ var groupping = result.programList.GroupBy(item => item.GroupTitle)
+ .Select(group => new GroupInfo() { Name = group.Key, Count = group.Count() })
+ .OrderBy(g => g.Name)
+ .ToList();
+ System.Diagnostics.Debug.WriteLine($"[ProgramsData] Publishing {groupping.Count} groups: {string.Join(", ", groupping.Select(g => $"{g.Name}({g.Count})"))}");
+ if (groupping.Count == 0)
+ {
+ System.Diagnostics.Debug.WriteLine("[ProgramsData] WARNING: No groups found! Check if programs have 'group-title' metadata.");
+ }
+ groupsSubject.OnNext(groupping);
+
+ await Task.Run(() => GetProgramGuide(name, result.programGuide));
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"[ProgramsData] ERROR downloading programs: {ex.Message}");
+ throw;
+ }
+ }
+
+ public Task GetGuideByProgram(string channelID)
+ {
+ return M3UParser.ParseEpg(_programName,channelID);
+ }
+
+ private async Task GetProgramGuide(string name, string guideLink)
+ {
+ //guideLink = "http://epg.da-tv.vip/107-light.xml";
+ await M3UParser.DownloadGuideFromWebAsync(name,guideLink);
+ programGuideSubject.OnNext(Unit.Default);
+ }
+
+ }
+}
diff --git a/TV Player Core/SettingsModel.cs b/TV Player Core/SettingsModel.cs
new file mode 100644
index 0000000..079e791
--- /dev/null
+++ b/TV Player Core/SettingsModel.cs
@@ -0,0 +1,87 @@
+using Newtonsoft.Json;
+using System.IO;
+
+namespace TV_Player.ViewModels
+{
+ public static class SettingsModel
+ {
+ private static readonly string AppDataFolder = Path.Combine(GetWritableAppDataFolder(), "TVPlayer");
+ private static readonly string SettingsFilePath = Path.Combine(AppDataFolder, "settings.json");
+
+ 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 Dictionary Playlists { get; set; }
+ public static bool StartFullScreen { get; set; }
+ public static bool StartFromLastScreen { get; set; }
+ public static string LastScreen { get; set; }
+ public static GroupInfo Group { get; set; }
+ public static M3UInfo Program { get; set; }
+ public static string[] HiddenGroups { get; set; }
+
+ public static void SaveSetttings()
+ {
+ // Create an anonymous object to hold the properties
+ var dataToSerialize = new
+ {
+ Playlists,
+ StartFromLastScreen,
+ StartFullScreen,
+ LastScreen,
+ Group,
+ Program,
+ HiddenGroups,
+ };
+
+ // Serialize the object to JSON
+ string json = JsonConvert.SerializeObject(dataToSerialize, Formatting.Indented);
+
+ if (!Directory.Exists(AppDataFolder))
+ Directory.CreateDirectory(AppDataFolder);
+ // Save the JSON to a file
+ File.WriteAllText(SettingsFilePath, json);
+ }
+
+ public static void LoadSettings()
+ {
+ var loadedData = new
+ {
+ Playlists = default(Dictionary),
+ LastScreen = default(string),
+ Group = default(GroupInfo),
+ Program = default(M3UInfo),
+ StartFromLastScreen = default(bool),
+ StartFullScreen = default(bool),
+ HiddenGroups = default(string[])
+ };
+ if (File.Exists(SettingsFilePath))
+ {
+ // Read the JSON content from the file
+ string json = File.ReadAllText(SettingsFilePath);
+ loadedData = JsonConvert.DeserializeAnonymousType(json, loadedData);
+ }
+ // Assign the values to the properties
+ Playlists = loadedData.Playlists;
+ LastScreen = loadedData.LastScreen;
+ Group = loadedData.Group;
+ Program = loadedData.Program;
+ StartFromLastScreen = loadedData.StartFromLastScreen;
+ StartFullScreen = loadedData.StartFullScreen;
+ HiddenGroups = loadedData.HiddenGroups;
+ }
+ }
+}
diff --git a/TV Player Core/TV Player Core.csproj b/TV Player Core/TV Player Core.csproj
new file mode 100644
index 0000000..d88cec8
--- /dev/null
+++ b/TV Player Core/TV Player Core.csproj
@@ -0,0 +1,17 @@
+
+
+
+ Library
+ net8.0
+ TV_Player
+ TV Player Core
+ enable
+ enable
+
+
+
+
+
+
+
+
diff --git a/TV Player WPF/TV Player WPF.csproj b/TV Player WPF/TV Player WPF.csproj
index b0fac19..453ffbd 100644
--- a/TV Player WPF/TV Player WPF.csproj
+++ b/TV Player WPF/TV Player WPF.csproj
@@ -30,6 +30,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
Never
diff --git a/TV player.sln b/TV player.sln
index 73f5afc..b73b8ed 100644
--- a/TV player.sln
+++ b/TV player.sln
@@ -9,6 +9,8 @@ Project("{54435603-DBB4-11D2-8724-00A0C9A8B90C}") = "TVPlayerSetup", "TVPlayerSe
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TV Player Avalonia", "TV Player Avalonia\TV Player Avalonia.csproj", "{CDF6DAD3-4880-4E03-8EE3-7B6568442BFC}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TV Player Core", "TV Player Core\TV Player Core.csproj", "{AA32E630-977F-4494-84A6-5A7245C9F892}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -31,6 +33,10 @@ Global
{CDF6DAD3-4880-4E03-8EE3-7B6568442BFC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CDF6DAD3-4880-4E03-8EE3-7B6568442BFC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CDF6DAD3-4880-4E03-8EE3-7B6568442BFC}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AA32E630-977F-4494-84A6-5A7245C9F892}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AA32E630-977F-4494-84A6-5A7245C9F892}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AA32E630-977F-4494-84A6-5A7245C9F892}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AA32E630-977F-4494-84A6-5A7245C9F892}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE