├── LICENSE
├── Old_Version
└── d-fi.zip
├── README.md
├── assets
├── Albums.png
├── BeatOn_WebUI.jpg
├── Dashboard.jpg
├── Downloads.jpg
├── Downloads_BeatOn_Webui.png
├── Featured.jpg
├── Home_BeatOn_Webui.png
├── Search_BeatOn_Webui.png
├── Settings.png
├── Settings_BeatOn_Webui.png
└── downloads_with_urls.gif
├── d-fi.zip
└── src
├── AlbumCache.cs
├── App.config
├── App.xaml
├── App.xaml.cs
├── AssemblyInfo.cs
├── BeatOn.csproj
├── BeatOn.csproj.user
├── BeatOn.sln
├── BeatOn_Banner-removebg.png
├── BeatOn_Logo.ico
├── CountToVisibilityConverter.cs
├── DatabaseManager.cs
├── DeezerArlScraper.cs
├── DownloadItem.cs
├── DownloadManagerControl.xaml
├── DownloadManagerControl.xaml.cs
├── DownloadManagerWindow.xaml
├── DownloadManagerWindow.xaml.cs
├── FormatOptionsPopup.xaml
├── FormatOptionsPopup.xaml.cs
├── LidarrDownloader.cs
├── LidarrDownloaderStats.cs
├── Logo_BeatOn.ico
├── MainWindow.xaml
├── MainWindow.xaml.cs
├── OutputWindow.xaml
├── OutputWindow.xaml.cs
├── Properties
├── Resources.Designer.cs
├── Resources.resx
├── Settings.Designer.cs
└── Settings.settings
├── QobuzCredentialManager.cs
├── QobuzDownloader.cs
├── Qobuz_FormatOptionsPopup.xaml
├── Qobuz_FormatOptionsPopup.xaml.cs
├── SettingsWindow.xaml
├── SettingsWindow.xaml.cs
├── SubtractConverter.cs
├── SystemUsageStats.cs
├── URL.png
├── WebUIServer.cs
├── album_coverholder.png
├── download.png
├── downloads.db
├── filedownload.png
├── img
├── BeatOn_Banner-removebg.png
├── album_coverholder.png
├── download.png
├── filedownload.png
└── settings.png
├── settings.png
└── wwwroot
├── index.html
├── script.js
├── search.html
└── styles.css
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 jaylex32
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Old_Version/d-fi.zip:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # BeatOn
2 |
3 | BeatOn is the ultimate GUI for D-Fi, designed to enhance your download experience with Deezer and Qobuz. Built using C# and WPF, BeatOn allows you to effortlessly download albums, songs, and artist catalogs with just a few clicks.
4 |
5 | ## Features
6 |
7 | - Intuitive and visually pleasing interface
8 | - Easy downloading of albums, songs, and artist catalogs
9 | - Support for both Deezer and Qobuz platforms
10 | - Web UI for convenient access and control
11 |
12 | ## Screenshots
13 |
14 | ### Desktop Application
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | ### Web UI
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | ## Tutorials
39 |
40 | ### Download with URLs
41 | 
42 |
43 |
44 | ## License
45 |
46 | This project is licensed under the MIT License - see the LICENSE file for details.
47 |
48 |
49 | ---
50 |
51 | BeatOn - Simplifying your music download experience.
52 |
53 |
54 |
55 |
56 | ### If you like my work and want to buy me a coffee to support me, you can do so here: https://buymeacoffee.com/jayross
57 |
58 |
--------------------------------------------------------------------------------
/assets/Albums.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jaylex32/BeatOn/b1183ea5c68e056e044da4b205908d54079afb1d/assets/Albums.png
--------------------------------------------------------------------------------
/assets/BeatOn_WebUI.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jaylex32/BeatOn/b1183ea5c68e056e044da4b205908d54079afb1d/assets/BeatOn_WebUI.jpg
--------------------------------------------------------------------------------
/assets/Dashboard.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jaylex32/BeatOn/b1183ea5c68e056e044da4b205908d54079afb1d/assets/Dashboard.jpg
--------------------------------------------------------------------------------
/assets/Downloads.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jaylex32/BeatOn/b1183ea5c68e056e044da4b205908d54079afb1d/assets/Downloads.jpg
--------------------------------------------------------------------------------
/assets/Downloads_BeatOn_Webui.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jaylex32/BeatOn/b1183ea5c68e056e044da4b205908d54079afb1d/assets/Downloads_BeatOn_Webui.png
--------------------------------------------------------------------------------
/assets/Featured.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jaylex32/BeatOn/b1183ea5c68e056e044da4b205908d54079afb1d/assets/Featured.jpg
--------------------------------------------------------------------------------
/assets/Home_BeatOn_Webui.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jaylex32/BeatOn/b1183ea5c68e056e044da4b205908d54079afb1d/assets/Home_BeatOn_Webui.png
--------------------------------------------------------------------------------
/assets/Search_BeatOn_Webui.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jaylex32/BeatOn/b1183ea5c68e056e044da4b205908d54079afb1d/assets/Search_BeatOn_Webui.png
--------------------------------------------------------------------------------
/assets/Settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jaylex32/BeatOn/b1183ea5c68e056e044da4b205908d54079afb1d/assets/Settings.png
--------------------------------------------------------------------------------
/assets/Settings_BeatOn_Webui.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jaylex32/BeatOn/b1183ea5c68e056e044da4b205908d54079afb1d/assets/Settings_BeatOn_Webui.png
--------------------------------------------------------------------------------
/assets/downloads_with_urls.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jaylex32/BeatOn/b1183ea5c68e056e044da4b205908d54079afb1d/assets/downloads_with_urls.gif
--------------------------------------------------------------------------------
/d-fi.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jaylex32/BeatOn/b1183ea5c68e056e044da4b205908d54079afb1d/d-fi.zip
--------------------------------------------------------------------------------
/src/AlbumCache.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using System.Text;
6 | using System.Threading.Tasks;
7 |
8 | namespace WpfApp1
9 | {
10 | public class AlbumCache
11 | {
12 | private Dictionary cache = new Dictionary();
13 | private readonly TimeSpan cacheDuration = TimeSpan.FromHours(24); // Cache for 24 hours
14 | private readonly string cacheFilePath;
15 |
16 | public AlbumCache(string cacheFilePath)
17 | {
18 | this.cacheFilePath = cacheFilePath;
19 | LoadCache();
20 | }
21 |
22 | public bool IsAlbumProcessed(string artistName, string albumName)
23 | {
24 | string key = GetCacheKey(artistName, albumName);
25 | return cache.ContainsKey(key);
26 | }
27 |
28 | public void MarkAlbumAsProcessed(string artistName, string albumName)
29 | {
30 | string key = GetCacheKey(artistName, albumName);
31 | cache[key] = true;
32 | SaveCache();
33 | }
34 |
35 | private string GetCacheKey(string artistName, string albumName)
36 | {
37 | return $"{artistName}|{albumName}".ToLower();
38 | }
39 |
40 | private void LoadCache()
41 | {
42 | if (File.Exists(cacheFilePath))
43 | {
44 | var lines = File.ReadAllLines(cacheFilePath);
45 | foreach (var line in lines)
46 | {
47 | var parts = line.Split('|');
48 | if (parts.Length == 3 && DateTime.TryParse(parts[2], out DateTime timestamp))
49 | {
50 | if (DateTime.Now - timestamp <= cacheDuration)
51 | {
52 | cache[parts[0] + "|" + parts[1]] = true;
53 | }
54 | }
55 | }
56 | }
57 | }
58 |
59 | private void SaveCache()
60 | {
61 | var lines = cache.Select(kvp => $"{kvp.Key}|{DateTime.Now:O}");
62 | File.WriteAllLines(cacheFilePath, lines);
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/App.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/App.xaml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/App.xaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Reflection;
4 | using System.Windows;
5 | using System.Diagnostics;
6 |
7 | namespace WpfApp1
8 | {
9 | public partial class App : Application
10 | {
11 | protected override void OnStartup(StartupEventArgs e)
12 | {
13 | AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
14 | base.OnStartup(e);
15 | EnsureDatabaseFile();
16 | }
17 |
18 | private void EnsureDatabaseFile()
19 | {
20 | var dbFileName = "downloads.db";
21 | var dbFilePath = Path.Combine(AppContext.BaseDirectory, dbFileName);
22 |
23 | try
24 | {
25 | //Debug.WriteLine($"Database Path: {dbFilePath}");
26 |
27 | if (!File.Exists(dbFilePath))
28 | {
29 | //Debug.WriteLine("Database file not found. Attempting to extract from resources.");
30 | ExtractEmbeddedResource("BeatOn.downloads.db", dbFilePath);
31 | //Debug.WriteLine("Successfully extracted database file.");
32 | }
33 | else
34 | {
35 | //Debug.WriteLine("Database file already exists.");
36 | }
37 | }
38 | catch (Exception ex)
39 | {
40 | // Log the exception
41 | //Debug.WriteLine($"Failed to copy database file: {ex.Message}");
42 | MessageBox.Show($"Failed to initialize the database. Error: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
43 | Application.Current.Shutdown();
44 | }
45 | }
46 |
47 | private void ExtractEmbeddedResource(string resourceName, string outputPath)
48 | {
49 | var assembly = Assembly.GetExecutingAssembly();
50 | using (Stream stream = assembly.GetManifestResourceStream(resourceName))
51 | {
52 | if (stream == null)
53 | {
54 | throw new FileNotFoundException("Resource not found.", resourceName);
55 | }
56 |
57 | using (FileStream fileStream = new FileStream(outputPath, FileMode.Create, FileAccess.Write))
58 | {
59 | stream.CopyTo(fileStream);
60 | }
61 | }
62 | }
63 |
64 | private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
65 | {
66 |
67 | var exception = e.ExceptionObject as Exception;
68 | if (exception != null)
69 | {
70 | //Debug.WriteLine($"Unhandled exception: {exception.Message}");
71 | }
72 | else
73 | {
74 | //Debug.WriteLine("Unhandled exception: Unknown error");
75 | }
76 | }
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Windows;
2 |
3 | [assembly: ThemeInfo(
4 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
5 | //(used if a resource is not found in the page,
6 | // or application resource dictionaries)
7 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
8 | //(used if a resource is not found in the page,
9 | // app, or any theme specific resource dictionaries)
10 | )]
11 |
--------------------------------------------------------------------------------
/src/BeatOn.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | WinExe
5 | net8.0-windows
6 | enable
7 | true
8 | true
9 | true
10 | BeatOn
11 | BeatOn_Logo.ico
12 | AnyCPU;x64
13 | true
14 | win-x64
15 |
16 | true
17 | true
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | PreserveNewest
34 |
35 |
36 | PreserveNewest
37 |
38 |
39 | PreserveNewest
40 |
41 |
42 | PreserveNewest
43 |
44 |
45 | PreserveNewest
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 | True
84 | True
85 | Resources.resx
86 |
87 |
88 | True
89 | True
90 | Settings.settings
91 |
92 |
93 |
94 |
95 |
96 | PublicResXFileCodeGenerator
97 | Resources.Designer.cs
98 |
99 |
100 |
101 |
102 |
103 | SettingsSingleFileGenerator
104 | Settings.Designer.cs
105 |
106 |
107 | PreserveNewest
108 |
109 |
110 | PreserveNewest
111 |
112 |
113 | PreserveNewest
114 |
115 |
116 |
117 |
118 |
119 | PreserveNewest
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
--------------------------------------------------------------------------------
/src/BeatOn.csproj.user:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <_LastSelectedProfileId>E:\Programing Code Projects\C#\WPF\BeatOn\BeatOn\src\Properties\PublishProfiles\FolderProfile.pubxml
5 |
6 |
7 |
8 | Designer
9 |
10 |
11 |
12 |
13 | Code
14 |
15 |
16 | Code
17 |
18 |
19 | Code
20 |
21 |
22 | Code
23 |
24 |
25 | Code
26 |
27 |
28 | Code
29 |
30 |
31 |
32 |
33 | Designer
34 |
35 |
36 | Designer
37 |
38 |
39 | Designer
40 |
41 |
42 | Designer
43 |
44 |
45 | Designer
46 |
47 |
48 | Designer
49 |
50 |
51 |
--------------------------------------------------------------------------------
/src/BeatOn.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.10.35122.118
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BeatOn", "BeatOn.csproj", "{EE0C5594-C300-4EBC-B21F-C93C7D4C4824}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Debug|x64 = Debug|x64
12 | Release|Any CPU = Release|Any CPU
13 | Release|x64 = Release|x64
14 | EndGlobalSection
15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
16 | {EE0C5594-C300-4EBC-B21F-C93C7D4C4824}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
17 | {EE0C5594-C300-4EBC-B21F-C93C7D4C4824}.Debug|Any CPU.Build.0 = Debug|Any CPU
18 | {EE0C5594-C300-4EBC-B21F-C93C7D4C4824}.Debug|x64.ActiveCfg = Debug|x64
19 | {EE0C5594-C300-4EBC-B21F-C93C7D4C4824}.Debug|x64.Build.0 = Debug|x64
20 | {EE0C5594-C300-4EBC-B21F-C93C7D4C4824}.Release|Any CPU.ActiveCfg = Release|Any CPU
21 | {EE0C5594-C300-4EBC-B21F-C93C7D4C4824}.Release|Any CPU.Build.0 = Release|Any CPU
22 | {EE0C5594-C300-4EBC-B21F-C93C7D4C4824}.Release|x64.ActiveCfg = Release|x64
23 | {EE0C5594-C300-4EBC-B21F-C93C7D4C4824}.Release|x64.Build.0 = Release|x64
24 | EndGlobalSection
25 | GlobalSection(SolutionProperties) = preSolution
26 | HideSolutionNode = FALSE
27 | EndGlobalSection
28 | GlobalSection(ExtensibilityGlobals) = postSolution
29 | SolutionGuid = {18E9CB0E-8AAE-4C57-A4C1-25DE3DD1B192}
30 | EndGlobalSection
31 | EndGlobal
32 |
--------------------------------------------------------------------------------
/src/BeatOn_Banner-removebg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jaylex32/BeatOn/b1183ea5c68e056e044da4b205908d54079afb1d/src/BeatOn_Banner-removebg.png
--------------------------------------------------------------------------------
/src/BeatOn_Logo.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jaylex32/BeatOn/b1183ea5c68e056e044da4b205908d54079afb1d/src/BeatOn_Logo.ico
--------------------------------------------------------------------------------
/src/CountToVisibilityConverter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Globalization;
3 | using System.Windows;
4 | using System.Windows.Data;
5 | using System.Collections.Generic;
6 | using System.Linq;
7 | using System.Text;
8 | using System.Threading.Tasks;
9 |
10 | namespace WpfApp1
11 | {
12 | public class CountToVisibilityConverter : IValueConverter
13 | {
14 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
15 | {
16 | // Convert item count to visibility: 0 items results in Visible, more than 0 results in Collapsed.
17 | return (int)value > 0 ? Visibility.Collapsed : Visibility.Visible;
18 | }
19 |
20 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
21 | {
22 | throw new NotImplementedException(); // Not needed for one-way bindings
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/DatabaseManager.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Data.SQLite;
4 | using System.Diagnostics;
5 | using System.IO;
6 | using System.Linq;
7 |
8 | public class DatabaseManager
9 | {
10 | private string connectionString;
11 | private List downloadItems;
12 |
13 | public DatabaseManager()
14 | {
15 | var dbFileName = "downloads.db";
16 | var dbFilePath = Path.Combine(AppContext.BaseDirectory, dbFileName);
17 | connectionString = $"Data Source={dbFilePath};Version=3;";
18 | downloadItems = new List();
19 | InitializeDatabase();
20 | }
21 |
22 | private void InitializeDatabase()
23 | {
24 | try
25 | {
26 | using (SQLiteConnection conn = new SQLiteConnection(connectionString))
27 | {
28 | conn.Open();
29 |
30 | string sql = @"
31 | CREATE TABLE IF NOT EXISTS Downloads (
32 | Id INTEGER PRIMARY KEY AUTOINCREMENT,
33 | StartTime TEXT NOT NULL,
34 | EndTime TEXT,
35 | Url TEXT NOT NULL,
36 | Output TEXT,
37 | Status TEXT NOT NULL,
38 | ErrorMessage TEXT,
39 | Log TEXT
40 | )";
41 |
42 | using (SQLiteCommand command = new SQLiteCommand(sql, conn))
43 | {
44 | command.ExecuteNonQuery();
45 | }
46 |
47 | //Debug.WriteLine("Database initialized successfully.");
48 | }
49 | }
50 | catch (Exception ex)
51 | {
52 | // Log the exception
53 | //Debug.WriteLine($"Database initialization failed: {ex.Message}");
54 | }
55 | }
56 |
57 | public void SaveDownloadItem(DownloadItem downloadItem)
58 | {
59 | using (SQLiteConnection conn = new SQLiteConnection(connectionString))
60 | {
61 | conn.Open();
62 |
63 | string sql = @"
64 | INSERT INTO Downloads (StartTime, EndTime, Url, Output, Status, ErrorMessage)
65 | VALUES (@StartTime, @EndTime, @Url, @Output, @Status, @ErrorMessage)";
66 |
67 | using (SQLiteCommand command = new SQLiteCommand(sql, conn))
68 | {
69 | command.Parameters.AddWithValue("@StartTime", downloadItem.StartTime.ToString("yyyy-MM-dd HH:mm:ss"));
70 | command.Parameters.AddWithValue("@EndTime", downloadItem.EndTime?.ToString("yyyy-MM-dd HH:mm:ss"));
71 | command.Parameters.AddWithValue("@Url", downloadItem.Url);
72 | command.Parameters.AddWithValue("@Output", downloadItem.Output);
73 | command.Parameters.AddWithValue("@Status", downloadItem.Status);
74 | command.Parameters.AddWithValue("@ErrorMessage", downloadItem.ErrorMessage);
75 |
76 | command.ExecuteNonQuery();
77 | }
78 | }
79 | }
80 |
81 | // Add this method to get a download item based on artist and album name
82 | public DownloadItem GetDownloadItem(string artistName, string albumName)
83 | {
84 | return downloadItems.FirstOrDefault(item => item.ArtistName == artistName && item.Name == albumName);
85 | }
86 |
87 | public void UpdateDownloadItem(DownloadItem downloadItem)
88 | {
89 | using (SQLiteConnection conn = new SQLiteConnection(connectionString))
90 | {
91 | conn.Open();
92 |
93 | string sql = @"
94 | UPDATE Downloads
95 | SET EndTime = @EndTime, Output = @Output, Status = @Status, ErrorMessage = @ErrorMessage
96 | WHERE Id = @Id";
97 |
98 | using (SQLiteCommand command = new SQLiteCommand(sql, conn))
99 | {
100 | command.Parameters.AddWithValue("@EndTime", downloadItem.EndTime?.ToString("yyyy-MM-dd HH:mm:ss"));
101 | command.Parameters.AddWithValue("@Output", downloadItem.Output);
102 | command.Parameters.AddWithValue("@Status", downloadItem.Status);
103 | command.Parameters.AddWithValue("@ErrorMessage", downloadItem.ErrorMessage);
104 | command.Parameters.AddWithValue("@Id", downloadItem.Id);
105 |
106 | command.ExecuteNonQuery();
107 | }
108 | }
109 | }
110 |
111 | public DownloadItem GetDownloadItem(int id)
112 | {
113 | using (SQLiteConnection conn = new SQLiteConnection(connectionString))
114 | {
115 | conn.Open();
116 |
117 | string sql = "SELECT * FROM Downloads WHERE Id = @Id";
118 |
119 | using (SQLiteCommand command = new SQLiteCommand(sql, conn))
120 | {
121 | command.Parameters.AddWithValue("@Id", id);
122 |
123 | using (SQLiteDataReader reader = command.ExecuteReader())
124 | {
125 | if (reader.Read())
126 | {
127 | return new DownloadItem
128 | {
129 | Id = Convert.ToInt32(reader["Id"]),
130 | StartTime = Convert.ToDateTime(reader["StartTime"]),
131 | EndTime = reader["EndTime"] != DBNull.Value ? Convert.ToDateTime(reader["EndTime"]) : (DateTime?)null,
132 | Url = Convert.ToString(reader["Url"]),
133 | Output = Convert.ToString(reader["Output"]),
134 | Status = Convert.ToString(reader["Status"]),
135 | ErrorMessage = Convert.ToString(reader["ErrorMessage"]),
136 | Log = Convert.ToString(reader["Log"])
137 | };
138 | }
139 | }
140 | }
141 | }
142 |
143 | return null; // Return null if the download item with the specified id is not found
144 | }
145 |
146 | public void UpdateDownloadItemLog(DownloadItem downloadItem)
147 | {
148 | using (SQLiteConnection conn = new SQLiteConnection(connectionString))
149 | {
150 | conn.Open();
151 |
152 | string sql = @"
153 | UPDATE Downloads
154 | SET Log = @Log
155 | WHERE Id = @Id";
156 |
157 | using (SQLiteCommand command = new SQLiteCommand(sql, conn))
158 | {
159 | command.Parameters.AddWithValue("@Log", downloadItem.Log);
160 | command.Parameters.AddWithValue("@Id", downloadItem.Id);
161 |
162 | command.ExecuteNonQuery();
163 | }
164 | }
165 | }
166 | }
167 |
--------------------------------------------------------------------------------
/src/DeezerArlScraper.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Net.Http;
4 | using System.Text.RegularExpressions;
5 | using System.Threading.Tasks;
6 |
7 | public class DeezerArlScraper
8 | {
9 | private const string Url = "https://rentry.org/firehawk52";
10 |
11 | public async Task> GetAllArlsAsync()
12 | {
13 | using (HttpClient httpClient = new HttpClient())
14 | {
15 | string content = await httpClient.GetStringAsync(Url);
16 |
17 | // Find the start of the Deezer ARLs section
18 | int startIndex = content.IndexOf("Deezer ARLs");
19 | if (startIndex == -1) return new List();
20 |
21 | // Find the start of the table
22 | startIndex = content.IndexOf("", startIndex);
23 | if (startIndex == -1) return new List();
24 |
25 | // Find the end of the table
26 | int endIndex = content.IndexOf("
", startIndex);
27 | if (endIndex == -1) return new List();
28 |
29 | // Extract the table content
30 | string tableContent = content.Substring(startIndex, endIndex - startIndex);
31 |
32 | // Use regex to find all ARL codes
33 | var matches = Regex.Matches(tableContent, "(.*?)
");
34 |
35 | List arls = new List();
36 | foreach (Match match in matches)
37 | {
38 | if (match.Groups.Count > 1)
39 | {
40 | arls.Add(match.Groups[1].Value);
41 | }
42 | }
43 |
44 | return arls;
45 | }
46 | }
47 |
48 | public async Task GetRandomArlAsync()
49 | {
50 | var arls = await GetAllArlsAsync();
51 | if (arls.Count == 0) return null;
52 |
53 | Random random = new Random();
54 | int index = random.Next(arls.Count);
55 | return arls[index];
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/DownloadItem.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.ComponentModel;
3 | using System.Diagnostics;
4 |
5 | public class DownloadItem : INotifyPropertyChanged
6 | {
7 | public int Id { get; set; }
8 |
9 | // Property to store the path of the individual track
10 | public string DownloadPath { get; set; }
11 |
12 | // Property to store the path of the album
13 | public string AlbumDownloadPath { get; set; }
14 |
15 | public string SourceType { get; set; } // Add this property
16 | public Process DownloadProcess { get; set; }
17 | private string name;
18 | public string ArtistName { get; set; }
19 | private int progress;
20 | private string output;
21 | private string log;
22 | public bool IsDetailsVisible { get; set; } = false;
23 | public string Log
24 | {
25 | get { return log; }
26 | set
27 | {
28 | if (log != value)
29 | {
30 | log = value;
31 | OnPropertyChanged(nameof(Log));
32 | }
33 | }
34 | }
35 | private DateTime startTime;
36 | private DateTime? endTime;
37 | private string url;
38 | private string status;
39 | private string errorMessage;
40 |
41 | private string totalProgress;
42 | public string TotalProgress
43 | {
44 | get { return totalProgress; }
45 | set
46 | {
47 | if (totalProgress != value)
48 | {
49 | totalProgress = value;
50 | OnPropertyChanged(nameof(TotalProgress));
51 | }
52 | }
53 | }
54 |
55 | private double overallProgress;
56 | public double OverallProgress
57 | {
58 | get { return overallProgress; }
59 | set
60 | {
61 | if (overallProgress != value)
62 | {
63 | overallProgress = value;
64 | OnPropertyChanged(nameof(OverallProgress));
65 | }
66 | }
67 | }
68 |
69 | private double downloadProgress;
70 | public double DownloadProgress
71 | {
72 | get { return downloadProgress; }
73 | set
74 | {
75 | if (downloadProgress != value)
76 | {
77 | downloadProgress = value;
78 | OnPropertyChanged(nameof(DownloadProgress));
79 | OnPropertyChanged(nameof(IsCompleted)); // Raise PropertyChanged event for IsCompleted when DownloadProgress changes
80 | }
81 | }
82 | }
83 |
84 | private string _rawOutput;
85 |
86 | public string RawOutput
87 | {
88 | get { return _rawOutput; }
89 | set
90 | {
91 | _rawOutput = value;
92 | OnPropertyChanged(nameof(RawOutput));
93 | }
94 | }
95 |
96 | private bool isCompleted;
97 | public bool IsCompleted
98 | {
99 | get { return isCompleted; }
100 | set
101 | {
102 | if (isCompleted != value)
103 | {
104 | isCompleted = value;
105 | OnPropertyChanged(nameof(IsCompleted));
106 | OnPropertyChanged(nameof(DownloadProgress)); // Raise PropertyChanged event for DownloadProgress when IsCompleted changes
107 | }
108 | }
109 | }
110 |
111 | public string Name
112 | {
113 | get { return name; }
114 | set
115 | {
116 | if (name != value)
117 | {
118 | name = value;
119 | OnPropertyChanged(nameof(Name));
120 | }
121 | }
122 | }
123 |
124 | public int Progress
125 | {
126 | get { return progress; }
127 | set
128 | {
129 | if (progress != value)
130 | {
131 | progress = value;
132 | OnPropertyChanged(nameof(Progress));
133 | }
134 | }
135 | }
136 |
137 | public string Output
138 | {
139 | get { return output; }
140 | set
141 | {
142 | if (output != value)
143 | {
144 | output = value;
145 | OnPropertyChanged(nameof(Output));
146 | }
147 | }
148 | }
149 |
150 | public DateTime StartTime
151 | {
152 | get { return startTime; }
153 | set
154 | {
155 | if (startTime != value)
156 | {
157 | startTime = value;
158 | OnPropertyChanged(nameof(StartTime));
159 | }
160 | }
161 | }
162 |
163 | public DateTime? EndTime
164 | {
165 | get { return endTime; }
166 | set
167 | {
168 | if (endTime != value)
169 | {
170 | endTime = value;
171 | OnPropertyChanged(nameof(EndTime));
172 | }
173 | }
174 | }
175 |
176 | public string Url
177 | {
178 | get { return url; }
179 | set
180 | {
181 | if (url != value)
182 | {
183 | url = value;
184 | OnPropertyChanged(nameof(Url));
185 | }
186 | }
187 | }
188 |
189 | public string Status
190 | {
191 | get { return status; }
192 | set
193 | {
194 | if (status != value)
195 | {
196 | status = value;
197 | OnPropertyChanged(nameof(Status));
198 | }
199 | }
200 | }
201 |
202 | public string ErrorMessage
203 | {
204 | get { return errorMessage; }
205 | set
206 | {
207 | if (errorMessage != value)
208 | {
209 | errorMessage = value;
210 | OnPropertyChanged(nameof(ErrorMessage));
211 | }
212 | }
213 | }
214 |
215 | public event PropertyChangedEventHandler PropertyChanged;
216 |
217 | protected virtual void OnPropertyChanged(string propertyName)
218 | {
219 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
220 | }
221 | }
--------------------------------------------------------------------------------
/src/DownloadManagerControl.xaml:
--------------------------------------------------------------------------------
1 |
11 |
12 |
18 |
19 |
20 |
51 |
52 |
88 |
89 |
90 |
137 |
138 |
178 |
179 |
212 |
213 |
226 |
227 |
228 |
238 |
239 |
240 |
253 |
254 |
255 |
266 |
267 |
268 |
278 |
279 |
280 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
304 |
305 |
312 |
319 |
326 |
333 |
340 |
344 |
345 |
346 |
347 |
355 |
356 |
357 |
358 |
359 |
360 |
361 |
362 |
368 |
372 |
383 |
384 |
390 |
391 |
392 |
393 |
394 |
395 |
396 |
397 |
401 |
402 |
403 |
413 |
423 |
424 |
425 |
--------------------------------------------------------------------------------
/src/DownloadManagerControl.xaml.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Collections.ObjectModel;
5 | using System.ComponentModel;
6 | using System.Diagnostics;
7 | using System.Globalization;
8 | using System.IO;
9 | using System.Linq;
10 | using System.Text;
11 | using System.Threading.Tasks;
12 | using System.Windows;
13 | using System.Windows.Controls;
14 | using System.Windows.Data;
15 | using System.Windows.Documents;
16 | using System.Windows.Input;
17 | using System.Windows.Media;
18 | using System.Windows.Media.Imaging;
19 | using System.Windows.Navigation;
20 | using System.Windows.Shapes;
21 | using System.Windows.Threading;
22 |
23 | namespace WpfApp1
24 | {
25 | ///
26 | /// Interaction logic for DownloadManagerControl.xaml
27 | ///
28 |
29 | public partial class DownloadManagerControl : UserControl, INotifyPropertyChanged
30 | {
31 |
32 |
33 | private ObservableCollection downloadItems;
34 | private DispatcherTimer timer;
35 |
36 | private Settings settings;
37 |
38 | private DatabaseManager db;
39 |
40 | public DatabaseManager DbManager
41 | {
42 | get { return db; }
43 | set { db = value; }
44 | }
45 |
46 |
47 | public DownloadManagerControl()
48 | {
49 | InitializeComponent();
50 | DataContext = this;
51 | LoadSettings(); // Load settings here as well
52 | // Initialize the DownloadItems collection
53 | DownloadItems = new ObservableCollection();
54 | // Start the progress update timer
55 | StartProgressUpdateTimer();
56 | }
57 |
58 | public class TextTruncationConverter : IValueConverter
59 | {
60 | public int MaxLength { get; set; } = 20; // Default max length
61 |
62 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
63 | {
64 | if (value is string text && text.Length > MaxLength)
65 | {
66 | return text.Substring(0, MaxLength) + "...";
67 | }
68 | return value;
69 | }
70 |
71 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
72 | {
73 | throw new NotImplementedException();
74 | }
75 | }
76 | private void LoadSettings()
77 | {
78 | try
79 | {
80 |
81 | string json = File.ReadAllText("d-fi.config.json");
82 | settings = JsonConvert.DeserializeObject(json);
83 | }
84 | catch (Exception ex)
85 | {
86 | //MessageBox.Show($"Error reading settings file: {ex.Message}");
87 | //// Handle exceptions or provide a default settings object
88 | }
89 | }
90 |
91 | public void CancelDownload(DownloadItem downloadItem)
92 | {
93 | if (downloadItem?.DownloadProcess != null)
94 | {
95 | try
96 | {
97 | if (!downloadItem.DownloadProcess.HasExited)
98 | {
99 | downloadItem.DownloadProcess.Kill();
100 | downloadItem.Status = "Canceled";
101 | }
102 | }
103 | catch (InvalidOperationException)
104 | {
105 | // Handle the situation where the process is no longer available
106 | }
107 |
108 | downloadItem.DownloadProcess = null;
109 |
110 | // Update UI or database as needed
111 | db.UpdateDownloadItem(downloadItem);
112 |
113 | // Refresh the DataGrid
114 | RefreshDataGrid();
115 | }
116 | }
117 |
118 | private void OpenPathButton_Click(object sender, RoutedEventArgs e)
119 | {
120 | if (DownloadDataGrid.SelectedItem is DownloadItem selectedDownload)
121 | {
122 | string pathToOpen = selectedDownload.Status == "Completed" && !string.IsNullOrEmpty(selectedDownload.AlbumDownloadPath)
123 | ? selectedDownload.AlbumDownloadPath
124 | : System.IO.Path.GetDirectoryName(selectedDownload.DownloadPath);
125 |
126 | if (!string.IsNullOrEmpty(pathToOpen) && Directory.Exists(pathToOpen))
127 | {
128 | OpenFolderPath(pathToOpen);
129 | }
130 | else
131 | {
132 | MessageBox.Show("Path is not available or does not exist.", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
133 | }
134 | }
135 | }
136 | private void OpenFolderPath(string path)
137 | {
138 | // Check if the path is a file or directory
139 | if (File.Exists(path))
140 | {
141 | // If it's a file, open the file's location and select the file
142 | Process.Start("explorer.exe", $"/select, \"{path}\"");
143 | }
144 | else if (Directory.Exists(path))
145 | {
146 | // If it's a directory, open the directory
147 | Process.Start("explorer.exe", path);
148 | }
149 | else
150 | {
151 | MessageBox.Show("The path does not exist.", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
152 | }
153 | }
154 |
155 | private void DataGrid_Loaded(object sender, RoutedEventArgs e)
156 | {
157 | // Adjust the index values based on your DataGrid's columns
158 | // Assuming "Actions" column is initially at index 2 but should be at index 3
159 | if (DownloadDataGrid.Columns.Count > 5) // Check if you have enough columns
160 | {
161 | DownloadDataGrid.Columns[5].DisplayIndex = 2;
162 | }
163 | }
164 |
165 | private void RefreshDataGrid()
166 | {
167 | Dispatcher.Invoke(() =>
168 | {
169 | var currentItemsSource = DownloadDataGrid.ItemsSource;
170 | DownloadDataGrid.ItemsSource = null;
171 | DownloadDataGrid.ItemsSource = currentItemsSource;
172 | });
173 | }
174 | private void CancelButton_Click(object sender, RoutedEventArgs e)
175 | {
176 | Button cancelButton = sender as Button;
177 | DownloadItem downloadItem = cancelButton.DataContext as DownloadItem;
178 | CancelDownload(downloadItem);
179 | }
180 |
181 | private void LogTextBox_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
182 | {
183 | var textBox = sender as TextBox;
184 | if (textBox == null) return;
185 |
186 | int pos = textBox.GetCharacterIndexFromPoint(e.GetPosition(textBox), true);
187 | int lineIndex = textBox.GetLineIndexFromCharacterIndex(pos);
188 | int lineStart = textBox.GetCharacterIndexFromLineIndex(lineIndex);
189 | int lineLength = textBox.GetLineLength(lineIndex);
190 | string lineText = textBox.Text.Substring(lineStart, lineLength);
191 |
192 | // Now, determine if the clicked text is a path
193 | string path = ExtractPathFromLine(lineText);
194 | if (!string.IsNullOrEmpty(path) && File.Exists(path))
195 | {
196 | Process.Start(new ProcessStartInfo(path) { UseShellExecute = true });
197 | }
198 | }
199 |
200 | private string ExtractPathFromLine(string lineText)
201 | {
202 | // Implement logic to extract the path from the line text
203 | // For example, you can look for "✔ Path:" and extract the path after it
204 | if (lineText.Contains("Path:"))
205 | {
206 | int startIndex = lineText.IndexOf("Path:") + "Path:".Length;
207 | return lineText[startIndex..].Trim();
208 | }
209 | return null;
210 | }
211 |
212 | public void RaisePropertyChanged(string propertyName)
213 | {
214 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
215 | }
216 | public ObservableCollection DownloadItems
217 | {
218 | get { return downloadItems; }
219 | set
220 | {
221 | downloadItems = value;
222 | OnPropertyChanged(nameof(DownloadItems));
223 | }
224 | }
225 |
226 | private double overallProgress;
227 | public double OverallProgress
228 | {
229 | get { return overallProgress; }
230 | set
231 | {
232 | if (overallProgress != value)
233 | {
234 | overallProgress = value;
235 | OnPropertyChanged(nameof(OverallProgress));
236 | }
237 | }
238 | }
239 |
240 |
241 | private DispatcherTimer progressUpdateTimer;
242 |
243 | private void StartProgressUpdateTimer()
244 | {
245 | // Create a new timer
246 | progressUpdateTimer = new DispatcherTimer();
247 | progressUpdateTimer.Interval = TimeSpan.FromSeconds(1);
248 | progressUpdateTimer.Tick += ProgressUpdateTimer_Tick;
249 |
250 | // Start the timer
251 | progressUpdateTimer.Start();
252 | }
253 |
254 | private void ProgressUpdateTimer_Tick(object sender, EventArgs e)
255 | {
256 | // Check if there are any download items
257 | if (DownloadItems.Count == 0)
258 | {
259 | // Stop the progress update timer
260 | progressUpdateTimer.Stop();
261 | return;
262 | }
263 |
264 | // Calculate the overall progress
265 | double overallProgress = 0;
266 | int completedItemCount = 0;
267 |
268 | foreach (var downloadItem in DownloadItems)
269 | {
270 | // Skip items that are already completed
271 | if (downloadItem.IsCompleted)
272 | {
273 | completedItemCount++;
274 | continue;
275 | }
276 |
277 | if (downloadItem.Status == "Completed")
278 | {
279 | downloadItem.DownloadProgress = 100;
280 | downloadItem.IsCompleted = true;
281 | completedItemCount++;
282 | }
283 | else if (downloadItem.Status == "In progress" && downloadItem.Output != null)
284 | {
285 | // Try to extract the progress information from the output
286 | if (TryExtractProgress(downloadItem.Output, out int currentIndex, out int totalCount))
287 | {
288 | // Calculate the progress percentage
289 | double progressPercentage = (double)currentIndex / totalCount * 100;
290 |
291 | // Update the download item's progress
292 | downloadItem.DownloadProgress = progressPercentage;
293 |
294 | // Check if the download is completed
295 | if (currentIndex == totalCount)
296 | {
297 | downloadItem.IsCompleted = true;
298 | completedItemCount++;
299 | }
300 | }
301 | }
302 |
303 | overallProgress += downloadItem.DownloadProgress;
304 | }
305 |
306 | // Calculate the overall progress percentage
307 | overallProgress /= DownloadItems.Count - completedItemCount;
308 | OverallProgress = overallProgress;
309 |
310 | // Check if all downloads are completed
311 | if (completedItemCount == DownloadItems.Count)
312 | {
313 | // Stop the progress update timer
314 | progressUpdateTimer.Stop();
315 | }
316 | }
317 |
318 |
319 | private bool TryExtractProgress(string output, out int currentIndex, out int totalCount)
320 | {
321 | currentIndex = 0;
322 | totalCount = 0;
323 |
324 | int startIndex = output.IndexOf("(") + 1;
325 | int endIndex = output.IndexOf("/");
326 | if (startIndex >= 0 && endIndex >= 0 && endIndex > startIndex)
327 | {
328 | string progress = output.Substring(startIndex, endIndex - startIndex).Trim();
329 | string[] progressParts = progress.Split('/');
330 | if (progressParts.Length == 2 && int.TryParse(progressParts[0], out currentIndex) && int.TryParse(progressParts[1], out totalCount))
331 | {
332 | return true;
333 | }
334 | }
335 |
336 | return false;
337 | }
338 |
339 |
340 | // Implement INotifyPropertyChanged
341 | public event PropertyChangedEventHandler PropertyChanged;
342 |
343 | protected virtual void OnPropertyChanged(string propertyName)
344 | {
345 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
346 | }
347 |
348 | public void AddDownloadItem(DownloadItem downloadItem)
349 | {
350 | if (downloadItem != null && !string.IsNullOrEmpty(downloadItem.Name))
351 | {
352 | // Retrieve the download status from the database
353 | DownloadItem existingItem = db.GetDownloadItem(downloadItem.Id);
354 |
355 | if (existingItem != null)
356 | {
357 | // Update the status and other properties of the existing download item
358 | existingItem.Status = downloadItem.Status;
359 | existingItem.EndTime = downloadItem.EndTime;
360 | existingItem.Output = downloadItem.Output;
361 | existingItem.ErrorMessage = downloadItem.ErrorMessage;
362 |
363 | // Update the existing download item in the database
364 | db.UpdateDownloadItem(existingItem);
365 |
366 | // Find the index of the existing download item in the DownloadItems collection
367 | int index = DownloadItems.IndexOf(existingItem);
368 | downloadItem.DownloadProgress = 0; // Initialize the DownloadProgress property
369 | downloadItem.IsCompleted = false; // Initialize the IsCompleted property
370 | if (index >= 0)
371 | {
372 | // Update the existing download item in the DownloadItems collection
373 | DownloadItems[index] = existingItem;
374 | }
375 | }
376 | else
377 | {
378 | // Add the new download item to the DownloadItems collection
379 | DownloadItems.Add(downloadItem);
380 |
381 | // Save the download item to the database
382 | db.SaveDownloadItem(downloadItem);
383 | }
384 | }
385 | }
386 |
387 | public void RemoveDownloadItem(DownloadItem downloadItem)
388 | {
389 | DownloadItems.Remove(downloadItem);
390 | }
391 |
392 | private DownloadItem activeDownloadItem;
393 | public DownloadItem ActiveDownloadItem
394 | {
395 | get { return activeDownloadItem; }
396 | set
397 | {
398 | if (activeDownloadItem != value)
399 | {
400 | activeDownloadItem = value;
401 | OnPropertyChanged(nameof(ActiveDownloadItem));
402 | }
403 | }
404 | }
405 |
406 | private void DataGridRow_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
407 | {
408 | if (sender is DataGridRow row)
409 | {
410 | if (row.DetailsVisibility == Visibility.Collapsed)
411 | {
412 | row.DetailsVisibility = Visibility.Visible;
413 | }
414 | else
415 | {
416 | row.DetailsVisibility = Visibility.Collapsed;
417 | }
418 | }
419 | }
420 |
421 | public void UpdateDownloadItemProgress(DownloadItem downloadItem, int currentIndex, int totalCount)
422 | {
423 | // Update the progress of the download item
424 | downloadItem.Progress = currentIndex;
425 |
426 | // Update the overall progress of the download item
427 | downloadItem.OverallProgress = (double)currentIndex / totalCount;
428 |
429 | // Raise the PropertyChanged event for the updated properties
430 | OnPropertyChanged(nameof(DownloadItem.Progress));
431 | OnPropertyChanged(nameof(DownloadItem.OverallProgress));
432 | }
433 |
434 |
435 | private OutputWindow outputWindow;
436 |
437 | private Dictionary openOutputWindows = new Dictionary();
438 |
439 | private void OutputButton_Click(object sender, RoutedEventArgs e)
440 | {
441 | Button button = sender as Button;
442 | DownloadItem downloadItem = button.DataContext as DownloadItem;
443 |
444 | if (openOutputWindows.TryGetValue(downloadItem, out OutputWindow existingWindow))
445 | {
446 | // If the window is already open, bring it to the front
447 | existingWindow.Activate();
448 | }
449 | else
450 | {
451 | // If the window is not open, create and open a new OutputWindow
452 | OutputWindow outputWindow = new OutputWindow();
453 | outputWindow.DownloadItem = downloadItem; // Set the DownloadItem
454 | outputWindow.Closed += (s, args) =>
455 | {
456 | // Remove the window reference from the dictionary when it's closed
457 | openOutputWindows.Remove(downloadItem);
458 | };
459 | outputWindow.Show();
460 |
461 | // Store a reference to the open OutputWindow for this DownloadItem
462 | openOutputWindows[downloadItem] = outputWindow;
463 | }
464 | }
465 |
466 | public class StatusToVisibilityConverter : IValueConverter
467 | {
468 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
469 | {
470 | string status = value as string;
471 | string expectedStatus = parameter as string;
472 |
473 | if (status == expectedStatus)
474 | {
475 | return Visibility.Visible;
476 | }
477 |
478 | return Visibility.Collapsed;
479 | }
480 |
481 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
482 | {
483 | throw new NotImplementedException();
484 | }
485 | }
486 |
487 | private void OutputWindow_Closed(object sender, EventArgs e)
488 | {
489 | // Clear the output and reset the reference to null when the window is closed
490 | outputWindow.OutputText = string.Empty;
491 | outputWindow = null;
492 | }
493 |
494 | public void UpdateOutput(string output)
495 | {
496 | if (outputWindow != null && outputWindow.IsVisible)
497 | {
498 | outputWindow.Dispatcher.Invoke(() =>
499 | {
500 | outputWindow.OutputTextBox.AppendText(output + Environment.NewLine);
501 | outputWindow.OutputTextBox.ScrollToEnd(); // Scroll to the end to show the latest output
502 | });
503 | }
504 | }
505 | private void ClearButton_Click(object sender, RoutedEventArgs e)
506 | {
507 | for (int i = DownloadItems.Count - 1; i >= 0; i--)
508 | {
509 | var item = DownloadItems[i];
510 | if (item.Status == "Completed" || item.Status == "Failed")
511 | {
512 | DownloadItems.RemoveAt(i);
513 | }
514 | }
515 | }
516 |
517 | //private void CloseButton_Click(object sender, RoutedEventArgs e)
518 | //{
519 | // this.Hide();
520 | //}
521 |
522 | //private void Window_Closing(object sender, CancelEventArgs e)
523 | //{
524 | // e.Cancel = true;
525 | // this.Hide();
526 | //}
527 | }
528 | }
529 |
--------------------------------------------------------------------------------
/src/DownloadManagerWindow.xaml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
40 |
80 |
112 |
113 |
126 |
127 |
128 |
138 |
139 |
140 |
153 |
154 |
155 |
166 |
167 |
168 |
174 |
175 |
176 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
199 |
202 |
205 |
208 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
226 |
227 |
233 |
234 |
235 |
236 |
237 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
--------------------------------------------------------------------------------
/src/DownloadManagerWindow.xaml.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Collections.ObjectModel;
5 | using System.ComponentModel;
6 | using System.Diagnostics;
7 | using System.Globalization;
8 | using System.IO;
9 | using System.Threading;
10 | using System.Threading.Tasks;
11 | using System.Windows;
12 | using System.Windows.Controls;
13 | using System.Windows.Data;
14 | using System.Windows.Input;
15 | using System.Windows.Threading;
16 | using WpfApp1.Properties;
17 |
18 | namespace WpfApp1
19 | {
20 | public partial class DownloadManagerWindow : Window, INotifyPropertyChanged
21 | {
22 |
23 | private ObservableCollection downloadItems;
24 | private DatabaseManager db;
25 | private DispatcherTimer timer;
26 |
27 | private Settings settings;
28 |
29 | public DownloadManagerWindow(DatabaseManager databaseManager)
30 | {
31 | InitializeComponent();
32 | DataContext = this;
33 | Closing += Window_Closing;
34 | // Store the reference to the DatabaseManager
35 | db = databaseManager;
36 | LoadSettings(); // Load settings here as well
37 | // Initialize the DownloadItems collection
38 | DownloadItems = new ObservableCollection();
39 |
40 | // Start the progress update timer
41 | StartProgressUpdateTimer();
42 | }
43 |
44 | private void LoadSettings()
45 | {
46 | try
47 | {
48 |
49 | string json = File.ReadAllText("d-fi.config.json");
50 | settings = JsonConvert.DeserializeObject(json);
51 | }
52 | catch (Exception ex)
53 | {
54 | //MessageBox.Show($"Error reading settings file: {ex.Message}");
55 | //// Handle exceptions or provide a default settings object
56 | }
57 | }
58 |
59 | public void CancelDownload(DownloadItem downloadItem)
60 | {
61 | if (downloadItem?.DownloadProcess != null)
62 | {
63 | try
64 | {
65 | if (!downloadItem.DownloadProcess.HasExited)
66 | {
67 | downloadItem.DownloadProcess.Kill();
68 | downloadItem.Status = "Canceled";
69 | }
70 | }
71 | catch (InvalidOperationException)
72 | {
73 | // Handle the situation where the process is no longer available
74 | }
75 |
76 | downloadItem.DownloadProcess = null;
77 |
78 | // Update UI or database as needed
79 | db.UpdateDownloadItem(downloadItem);
80 |
81 | // Refresh the DataGrid
82 | RefreshDataGrid();
83 | }
84 | }
85 |
86 | private void OpenPathButton_Click(object sender, RoutedEventArgs e)
87 | {
88 | if (DownloadDataGrid.SelectedItem is DownloadItem selectedDownload)
89 | {
90 | // Check if settings is not null and SourceType is set
91 | if (settings != null && !string.IsNullOrEmpty(selectedDownload.SourceType))
92 | {
93 | string basePath = selectedDownload.SourceType == "Deezer"
94 | ? settings.DeezerFolderPath
95 | : settings.QobuzFolderPath;
96 |
97 | // Check if basePath is not null or empty
98 | if (!string.IsNullOrEmpty(basePath))
99 | {
100 | OpenFolderPath(basePath);
101 | }
102 | else
103 | {
104 | MessageBox.Show("Base path is not set.", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
105 | }
106 | }
107 | else
108 | {
109 | MessageBox.Show("Settings are not loaded or Source Type is missing.", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
110 | }
111 | }
112 | else
113 | {
114 | MessageBox.Show("No item selected.", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
115 | }
116 | }
117 |
118 | private void OpenFolderPath(string path)
119 | {
120 | if (Directory.Exists(path))
121 | {
122 | Process.Start(new ProcessStartInfo("explorer.exe", path));
123 | }
124 | else
125 | {
126 | MessageBox.Show($"Path does not exist: {path}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
127 | }
128 | }
129 |
130 | private void DataGrid_Loaded(object sender, RoutedEventArgs e)
131 | {
132 | // Adjust the index values based on your DataGrid's columns
133 | // Assuming "Actions" column is initially at index 2 but should be at index 3
134 | if (DownloadDataGrid.Columns.Count > 5) // Check if you have enough columns
135 | {
136 | DownloadDataGrid.Columns[5].DisplayIndex = 2;
137 | }
138 | }
139 |
140 | private void RefreshDataGrid()
141 | {
142 | Dispatcher.Invoke(() =>
143 | {
144 | var currentItemsSource = DownloadDataGrid.ItemsSource;
145 | DownloadDataGrid.ItemsSource = null;
146 | DownloadDataGrid.ItemsSource = currentItemsSource;
147 | });
148 | }
149 | private void CancelButton_Click(object sender, RoutedEventArgs e)
150 | {
151 | Button cancelButton = sender as Button;
152 | DownloadItem downloadItem = cancelButton.DataContext as DownloadItem;
153 | CancelDownload(downloadItem);
154 | }
155 |
156 | public void RaisePropertyChanged(string propertyName)
157 | {
158 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
159 | }
160 | public ObservableCollection DownloadItems
161 | {
162 | get { return downloadItems; }
163 | set
164 | {
165 | downloadItems = value;
166 | OnPropertyChanged(nameof(DownloadItems));
167 | }
168 | }
169 |
170 | private double overallProgress;
171 | public double OverallProgress
172 | {
173 | get { return overallProgress; }
174 | set
175 | {
176 | if (overallProgress != value)
177 | {
178 | overallProgress = value;
179 | OnPropertyChanged(nameof(OverallProgress));
180 | }
181 | }
182 | }
183 |
184 |
185 | private DispatcherTimer progressUpdateTimer;
186 |
187 | private void StartProgressUpdateTimer()
188 | {
189 | // Create a new timer
190 | progressUpdateTimer = new DispatcherTimer();
191 | progressUpdateTimer.Interval = TimeSpan.FromSeconds(1);
192 | progressUpdateTimer.Tick += ProgressUpdateTimer_Tick;
193 |
194 | // Start the timer
195 | progressUpdateTimer.Start();
196 | }
197 |
198 | private void ProgressUpdateTimer_Tick(object sender, EventArgs e)
199 | {
200 | // Check if there are any download items
201 | if (DownloadItems.Count == 0)
202 | {
203 | // Stop the progress update timer
204 | progressUpdateTimer.Stop();
205 | return;
206 | }
207 |
208 | // Calculate the overall progress
209 | double overallProgress = 0;
210 | int completedItemCount = 0;
211 |
212 | foreach (var downloadItem in DownloadItems)
213 | {
214 | // Skip items that are already completed
215 | if (downloadItem.IsCompleted)
216 | {
217 | completedItemCount++;
218 | continue;
219 | }
220 |
221 | if (downloadItem.Status == "Completed")
222 | {
223 | downloadItem.DownloadProgress = 100;
224 | downloadItem.IsCompleted = true;
225 | completedItemCount++;
226 | }
227 | else if (downloadItem.Status == "In progress" && downloadItem.Output != null)
228 | {
229 | // Try to extract the progress information from the output
230 | if (TryExtractProgress(downloadItem.Output, out int currentIndex, out int totalCount))
231 | {
232 | // Calculate the progress percentage
233 | double progressPercentage = (double)currentIndex / totalCount * 100;
234 |
235 | // Update the download item's progress
236 | downloadItem.DownloadProgress = progressPercentage;
237 |
238 | // Check if the download is completed
239 | if (currentIndex == totalCount)
240 | {
241 | downloadItem.IsCompleted = true;
242 | completedItemCount++;
243 | }
244 | }
245 | }
246 |
247 | overallProgress += downloadItem.DownloadProgress;
248 | }
249 |
250 | // Calculate the overall progress percentage
251 | overallProgress /= DownloadItems.Count - completedItemCount;
252 | OverallProgress = overallProgress;
253 |
254 | // Check if all downloads are completed
255 | if (completedItemCount == DownloadItems.Count)
256 | {
257 | // Stop the progress update timer
258 | progressUpdateTimer.Stop();
259 | }
260 | }
261 |
262 |
263 | private bool TryExtractProgress(string output, out int currentIndex, out int totalCount)
264 | {
265 | currentIndex = 0;
266 | totalCount = 0;
267 |
268 | int startIndex = output.IndexOf("(") + 1;
269 | int endIndex = output.IndexOf("/");
270 | if (startIndex >= 0 && endIndex >= 0 && endIndex > startIndex)
271 | {
272 | string progress = output.Substring(startIndex, endIndex - startIndex).Trim();
273 | string[] progressParts = progress.Split('/');
274 | if (progressParts.Length == 2 && int.TryParse(progressParts[0], out currentIndex) && int.TryParse(progressParts[1], out totalCount))
275 | {
276 | return true;
277 | }
278 | }
279 |
280 | return false;
281 | }
282 |
283 |
284 | // Implement INotifyPropertyChanged
285 | public event PropertyChangedEventHandler PropertyChanged;
286 |
287 | protected virtual void OnPropertyChanged(string propertyName)
288 | {
289 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
290 | }
291 |
292 | public void AddDownloadItem(DownloadItem downloadItem)
293 | {
294 | if (downloadItem != null && !string.IsNullOrEmpty(downloadItem.Name))
295 | {
296 | // Retrieve the download status from the database
297 | DownloadItem existingItem = db.GetDownloadItem(downloadItem.Id);
298 |
299 | if (existingItem != null)
300 | {
301 | // Update the status and other properties of the existing download item
302 | existingItem.Status = downloadItem.Status;
303 | existingItem.EndTime = downloadItem.EndTime;
304 | existingItem.Output = downloadItem.Output;
305 | existingItem.ErrorMessage = downloadItem.ErrorMessage;
306 |
307 | // Update the existing download item in the database
308 | db.UpdateDownloadItem(existingItem);
309 |
310 | // Find the index of the existing download item in the DownloadItems collection
311 | int index = DownloadItems.IndexOf(existingItem);
312 | downloadItem.DownloadProgress = 0; // Initialize the DownloadProgress property
313 | downloadItem.IsCompleted = false; // Initialize the IsCompleted property
314 | if (index >= 0)
315 | {
316 | // Update the existing download item in the DownloadItems collection
317 | DownloadItems[index] = existingItem;
318 | }
319 | }
320 | else
321 | {
322 | // Add the new download item to the DownloadItems collection
323 | DownloadItems.Add(downloadItem);
324 |
325 | // Save the download item to the database
326 | db.SaveDownloadItem(downloadItem);
327 | }
328 | }
329 | }
330 |
331 | public void RemoveDownloadItem(DownloadItem downloadItem)
332 | {
333 | DownloadItems.Remove(downloadItem);
334 | }
335 |
336 | private DownloadItem activeDownloadItem;
337 | public DownloadItem ActiveDownloadItem
338 | {
339 | get { return activeDownloadItem; }
340 | set
341 | {
342 | if (activeDownloadItem != value)
343 | {
344 | activeDownloadItem = value;
345 | OnPropertyChanged(nameof(ActiveDownloadItem));
346 | }
347 | }
348 | }
349 |
350 | private void DataGridRow_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
351 | {
352 | if (sender is DataGridRow row)
353 | {
354 | if (row.DetailsVisibility == Visibility.Collapsed)
355 | {
356 | row.DetailsVisibility = Visibility.Visible;
357 | }
358 | else
359 | {
360 | row.DetailsVisibility = Visibility.Collapsed;
361 | }
362 | }
363 | }
364 |
365 | public void UpdateDownloadItemProgress(DownloadItem downloadItem, int currentIndex, int totalCount)
366 | {
367 | // Update the progress of the download item
368 | downloadItem.Progress = currentIndex;
369 |
370 | // Update the overall progress of the download item
371 | downloadItem.OverallProgress = (double)currentIndex / totalCount;
372 |
373 | // Raise the PropertyChanged event for the updated properties
374 | OnPropertyChanged(nameof(DownloadItem.Progress));
375 | OnPropertyChanged(nameof(DownloadItem.OverallProgress));
376 | }
377 |
378 |
379 | private OutputWindow outputWindow;
380 |
381 | private Dictionary openOutputWindows = new Dictionary();
382 |
383 | private void OutputButton_Click(object sender, RoutedEventArgs e)
384 | {
385 | Button button = sender as Button;
386 | DownloadItem downloadItem = button.DataContext as DownloadItem;
387 |
388 | if (openOutputWindows.TryGetValue(downloadItem, out OutputWindow existingWindow))
389 | {
390 | // If the window is already open, bring it to the front
391 | existingWindow.Activate();
392 | }
393 | else
394 | {
395 | // If the window is not open, create and open a new OutputWindow
396 | OutputWindow outputWindow = new OutputWindow();
397 | outputWindow.DownloadItem = downloadItem; // Set the DownloadItem
398 | outputWindow.Closed += (s, args) =>
399 | {
400 | // Remove the window reference from the dictionary when it's closed
401 | openOutputWindows.Remove(downloadItem);
402 | };
403 | outputWindow.Show();
404 |
405 | // Store a reference to the open OutputWindow for this DownloadItem
406 | openOutputWindows[downloadItem] = outputWindow;
407 | }
408 | }
409 |
410 | public class StatusToVisibilityConverter : IValueConverter
411 | {
412 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
413 | {
414 | string status = value as string;
415 | string expectedStatus = parameter as string;
416 |
417 | if (status == expectedStatus)
418 | {
419 | return Visibility.Visible;
420 | }
421 |
422 | return Visibility.Collapsed;
423 | }
424 |
425 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
426 | {
427 | throw new NotImplementedException();
428 | }
429 | }
430 |
431 | private void OutputWindow_Closed(object sender, EventArgs e)
432 | {
433 | // Clear the output and reset the reference to null when the window is closed
434 | outputWindow.OutputText = string.Empty;
435 | outputWindow = null;
436 | }
437 |
438 | public void UpdateOutput(string output)
439 | {
440 | if (outputWindow != null && outputWindow.IsVisible)
441 | {
442 | outputWindow.Dispatcher.Invoke(() =>
443 | {
444 | outputWindow.OutputTextBox.AppendText(output + Environment.NewLine);
445 | outputWindow.OutputTextBox.ScrollToEnd(); // Scroll to the end to show the latest output
446 | });
447 | }
448 | }
449 | private void ClearButton_Click(object sender, RoutedEventArgs e)
450 | {
451 | for (int i = DownloadItems.Count - 1; i >= 0; i--)
452 | {
453 | var item = DownloadItems[i];
454 | if (item.Status == "Completed" || item.Status == "Failed")
455 | {
456 | DownloadItems.RemoveAt(i);
457 | }
458 | }
459 | }
460 |
461 | private void CloseButton_Click(object sender, RoutedEventArgs e)
462 | {
463 | this.Hide();
464 | }
465 |
466 | private void Window_Closing(object sender, CancelEventArgs e)
467 | {
468 | e.Cancel = true;
469 | this.Hide();
470 | }
471 | }
472 | }
--------------------------------------------------------------------------------
/src/FormatOptionsPopup.xaml:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
21 |
22 |
90 |
91 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
114 |
115 |
119 |
120 |
121 |
--------------------------------------------------------------------------------
/src/FormatOptionsPopup.xaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 | using System.Windows;
7 | using System.Windows.Controls;
8 | using System.Windows.Data;
9 | using System.Windows.Documents;
10 | using System.Windows.Input;
11 | using System.Windows.Media;
12 | using System.Windows.Media.Imaging;
13 | using System.Windows.Shapes;
14 |
15 | namespace WpfApp1
16 | {
17 | ///
18 | /// Interaction logic for FormatOptionsPopup.xaml
19 | ///
20 | public partial class FormatOptionsPopup : Window
21 | {
22 | public FormatOptionsPopup()
23 | {
24 | InitializeComponent();
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/LidarrDownloaderStats.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Diagnostics;
4 | using System.IO;
5 | using System.Linq;
6 | using System.Threading.Tasks;
7 |
8 | namespace WpfApp1
9 | {
10 | public class LidarrDownloaderStats
11 | {
12 | private List _recentDownloads = new List();
13 | private LidarrDownloader _lidarrDownloader;
14 |
15 | public LidarrDownloaderStats()
16 | {
17 | // No parameters needed in the constructor
18 | }
19 |
20 | public void SetLidarrDownloader(LidarrDownloader lidarrDownloader)
21 | {
22 | _lidarrDownloader = lidarrDownloader;
23 | }
24 |
25 | public int GetDownloadCount()
26 | {
27 | return _recentDownloads.Count;
28 | }
29 |
30 | public List GetRecentDownloads()
31 | {
32 | var completedDownloads = _recentDownloads
33 | .Where(d => d.Status == "Completed")
34 | .OrderByDescending(d => d.DownloadTime)
35 | .Take(10)
36 | .ToList();
37 |
38 | foreach (var download in completedDownloads)
39 | {
40 | //Console.WriteLine($"Download: {download.ArtistName} - {download.AlbumName}, Status: {download.Status}");
41 | }
42 |
43 | return completedDownloads;
44 | }
45 |
46 | public void UpdateDownloadProgress(string artistName, string albumName, int progress)
47 | {
48 | var download = _recentDownloads.FirstOrDefault(d => d.ArtistName == artistName && d.AlbumName == albumName);
49 | if (download != null)
50 | {
51 | download.Progress = progress;
52 | if (progress == 100)
53 | {
54 | download.Status = "Completed";
55 | }
56 | }
57 | }
58 |
59 | public void AddDownload(string artistName, string albumName, string status)
60 | {
61 | var existingDownload = _recentDownloads.FirstOrDefault(d => d.ArtistName == artistName && d.AlbumName == albumName);
62 | if (existingDownload != null)
63 | {
64 | existingDownload.Status = status;
65 | existingDownload.DownloadTime = DateTime.Now;
66 | }
67 | else
68 | {
69 | _recentDownloads.Add(new DownloadInfo
70 | {
71 | ArtistName = artistName,
72 | AlbumName = albumName,
73 | DownloadTime = DateTime.Now,
74 | Status = status
75 | });
76 | }
77 |
78 | if (_recentDownloads.Count > 100) // Keep only last 100 downloads
79 | {
80 | _recentDownloads.RemoveAt(0);
81 | }
82 | }
83 |
84 | public List GetActiveDownloads()
85 | {
86 | return _recentDownloads.Where(d => d.Status == "In progress" || d.Status == "Waiting" || d.Status == "Completed").ToList();
87 | }
88 | public List GetFailedDownloads()
89 | {
90 | return _recentDownloads
91 | .Where(d => d.Status == "Failed" || d.Status == "Error")
92 | .OrderByDescending(d => d.DownloadTime)
93 | .Take(10)
94 | .ToList();
95 | }
96 |
97 | public async Task GetStorageInfo()
98 | {
99 | DriveInfo drive = new DriveInfo(Path.GetPathRoot(Environment.CurrentDirectory));
100 | return new StorageInfo
101 | {
102 | TotalSpace = drive.TotalSize,
103 | FreeSpace = drive.AvailableFreeSpace
104 | };
105 | }
106 |
107 | public List GetRecentCompletedDownloads()
108 | {
109 | return _recentDownloads
110 | .Where(d => d.Status == "Completed")
111 | .OrderByDescending(d => d.DownloadTime)
112 | .Take(10)
113 | .ToList();
114 | }
115 | }
116 |
117 |
118 |
119 | public class DownloadInfo
120 | {
121 | public string ArtistName { get; set; }
122 | public string AlbumName { get; set; }
123 | public DateTime DownloadTime { get; set; }
124 | public string Status { get; set; }
125 | public int Progress { get; set; }
126 | }
127 |
128 | public class StorageInfo
129 | {
130 | public long TotalSpace { get; set; }
131 | public long FreeSpace { get; set; }
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/src/Logo_BeatOn.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jaylex32/BeatOn/b1183ea5c68e056e044da4b205908d54079afb1d/src/Logo_BeatOn.ico
--------------------------------------------------------------------------------
/src/OutputWindow.xaml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/OutputWindow.xaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Windows;
3 | using System.Windows.Threading;
4 |
5 | namespace WpfApp1
6 | {
7 | public partial class OutputWindow : Window
8 | {
9 | private DispatcherTimer timer;
10 | private DownloadItem downloadItem;
11 |
12 | // Add a new dependency property for OutputText
13 | public static readonly DependencyProperty OutputTextProperty = DependencyProperty.Register(
14 | "OutputText", typeof(string), typeof(OutputWindow), new PropertyMetadata(""));
15 |
16 | public string OutputText
17 | {
18 | get { return (string)GetValue(OutputTextProperty); }
19 | set { SetValue(OutputTextProperty, value); }
20 | }
21 |
22 | public OutputWindow()
23 | {
24 | InitializeComponent();
25 | DataContext = this;
26 |
27 | // Create the timer and set the interval to 3 seconds
28 | timer = new DispatcherTimer();
29 | timer.Interval = TimeSpan.FromSeconds(3);
30 | timer.Tick += Timer_Tick;
31 | }
32 |
33 | // Event handler for the timer tick event
34 | private void Timer_Tick(object sender, EventArgs e)
35 | {
36 | // Update the log text in the OutputWindow
37 | OutputText = downloadItem.Log;
38 | }
39 |
40 | // Property to set the DownloadItem and start the timer when the window is shown
41 | public DownloadItem DownloadItem
42 | {
43 | get { return downloadItem; }
44 | set
45 | {
46 | downloadItem = value;
47 | // Start the timer when the window is shown
48 | timer.Start();
49 | }
50 | }
51 |
52 | // Event handler for the Closed event of the OutputWindow to stop and dispose of the timer
53 | private void OutputWindow_Closed(object sender, EventArgs e)
54 | {
55 | timer.Stop();
56 | timer.Tick -= Timer_Tick; // Remove the event handler to avoid potential memory leaks
57 | }
58 | }
59 | }
60 |
61 |
--------------------------------------------------------------------------------
/src/Properties/Resources.Designer.cs:
--------------------------------------------------------------------------------
1 | //------------------------------------------------------------------------------
2 | //
3 | // This code was generated by a tool.
4 | // Runtime Version:4.0.30319.42000
5 | //
6 | // Changes to this file may cause incorrect behavior and will be lost if
7 | // the code is regenerated.
8 | //
9 | //------------------------------------------------------------------------------
10 |
11 | namespace WpfApp1.Properties {
12 | using System;
13 |
14 |
15 | ///
16 | /// A strongly-typed resource class, for looking up localized strings, etc.
17 | ///
18 | // This class was auto-generated by the StronglyTypedResourceBuilder
19 | // class via a tool like ResGen or Visual Studio.
20 | // To add or remove a member, edit your .ResX file then rerun ResGen
21 | // with the /str option, or rebuild your VS project.
22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
25 | public class Resources {
26 |
27 | private static global::System.Resources.ResourceManager resourceMan;
28 |
29 | private static global::System.Globalization.CultureInfo resourceCulture;
30 |
31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
32 | internal Resources() {
33 | }
34 |
35 | ///
36 | /// Returns the cached ResourceManager instance used by this class.
37 | ///
38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
39 | public static global::System.Resources.ResourceManager ResourceManager {
40 | get {
41 | if (object.ReferenceEquals(resourceMan, null)) {
42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("WpfApp1.Properties.Resources", typeof(Resources).Assembly);
43 | resourceMan = temp;
44 | }
45 | return resourceMan;
46 | }
47 | }
48 |
49 | ///
50 | /// Overrides the current thread's CurrentUICulture property for all
51 | /// resource lookups using this strongly typed resource class.
52 | ///
53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
54 | public static global::System.Globalization.CultureInfo Culture {
55 | get {
56 | return resourceCulture;
57 | }
58 | set {
59 | resourceCulture = value;
60 | }
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/Properties/Resources.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 | text/microsoft-resx
91 |
92 |
93 | 1.3
94 |
95 |
96 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
97 |
98 |
99 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
100 |
101 |
--------------------------------------------------------------------------------
/src/Properties/Settings.Designer.cs:
--------------------------------------------------------------------------------
1 | //------------------------------------------------------------------------------
2 | //
3 | // This code was generated by a tool.
4 | // Runtime Version:4.0.30319.42000
5 | //
6 | // Changes to this file may cause incorrect behavior and will be lost if
7 | // the code is regenerated.
8 | //
9 | //------------------------------------------------------------------------------
10 |
11 | namespace WpfApp1.Properties {
12 |
13 |
14 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
15 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.8.0.0")]
16 | internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
17 |
18 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
19 |
20 | public static Settings Default {
21 | get {
22 | return defaultInstance;
23 | }
24 | }
25 |
26 | [global::System.Configuration.UserScopedSettingAttribute()]
27 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
28 | [global::System.Configuration.DefaultSettingValueAttribute("")]
29 | public string LastOpenedSection {
30 | get {
31 | return ((string)(this["LastOpenedSection"]));
32 | }
33 | set {
34 | this["LastOpenedSection"] = value;
35 | }
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/Properties/Settings.settings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/QobuzCredentialManager.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Net.Http;
5 | using System.Text;
6 | using System.Text.RegularExpressions;
7 | using System.Threading.Tasks;
8 |
9 | namespace WpfApp1
10 | {
11 | public class QobuzCredentialManager
12 | {
13 | public async Task<(string appId, string appSecret)> GetQobuzCredentials(string userId, string userAuthToken)
14 | {
15 | try
16 | {
17 | var bundleUrl = await GetBundleJsUrl();
18 | var (appId, appSecret) = await GetAppIdAndSecret(bundleUrl);
19 |
20 | var isValid = await VerifyUserCredentials(userId, userAuthToken, appId);
21 | if (!isValid)
22 | {
23 | throw new Exception("Invalid user credentials");
24 | }
25 |
26 | return (appId, appSecret);
27 | }
28 | catch (Exception ex)
29 | {
30 | // Log the error
31 | Console.WriteLine($"Error: {ex.Message}");
32 | throw;
33 | }
34 | }
35 |
36 | private async Task GetBundleJsUrl()
37 | {
38 | using (var client = new HttpClient())
39 | {
40 | var bundleHTML = await client.GetStringAsync("https://play.qobuz.com/login");
41 | var match = Regex.Match(bundleHTML, "
11 |
63 |
64 |
65 |
66 |
67 |
BeatOn
68 |
69 |
70 |
71 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
Download Statistics
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
System Usage
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
Download Trend
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
Storage Information
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
Active Downloads
128 |
129 |
130 |
131 |
132 | Artist
133 | Album
134 | Status
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
Failed Downloads
147 |
148 |
149 |
150 |
151 | Artist
152 | Album
153 | Time
154 | Error
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
Recent Downloads
170 |
171 |
172 |
173 |
174 | Artist
175 | Album
176 | Time
177 | Status
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
349 |
350 |