diff --git a/.github/workflows/build-dev.yml b/.github/workflows/build-dev.yml new file mode 100644 index 0000000..37dc5a7 --- /dev/null +++ b/.github/workflows/build-dev.yml @@ -0,0 +1,34 @@ +name: Build .NET Framework (Dev Branch) + +on: + push: + branches: [ "dev" ] + +jobs: + build: + runs-on: windows-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + - name: Setup MSBuild + uses: microsoft/setup-msbuild@v2 + - name: Setup NuGet + uses: nuget/setup-nuget@v2 + - name: Restore NuGet Packages + run: nuget restore AMDiscordRPC.sln + - name: Build + run: msbuild AMDiscordRPC.sln -property:Configuration=Release -property:platform="x64" + - name: Zip + run: powershell Compress-Archive -Path ./AMDiscordRPC/bin/x64/Release -DestinationPath Release.zip + - name: Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ github.run_id }} + name: Dev-Release BuildID-${{ github.run_id }} + body: | + This is a pre-release build from the dev branch. + Don't use this version if you want more stable experience. + prerelease: true + files: | + Release.zip \ No newline at end of file diff --git a/AMDiscordRPC/AMDiscordRPC.cs b/AMDiscordRPC/AMDiscordRPC.cs index b038ea0..a807bb9 100644 --- a/AMDiscordRPC/AMDiscordRPC.cs +++ b/AMDiscordRPC/AMDiscordRPC.cs @@ -5,13 +5,13 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using System.Web; using static AMDiscordRPC.AppleMusic; using static AMDiscordRPC.Covers; using static AMDiscordRPC.Database; using static AMDiscordRPC.Discord; using static AMDiscordRPC.Globals; using static AMDiscordRPC.S3; +using static AMDiscordRPC.UI; namespace AMDiscordRPC { @@ -20,22 +20,25 @@ internal class AMDiscordRPC private static string oldAlbumnArtist; static void Main(string[] args) { + InitRegion(); + CreateUI(); ConfigureLogger(); InitializeDiscordRPC(); AttachToAppleMusic(); AMSongDataEvent.SongChanged += async (sender, x) => { log.Info($"Song: {x.SongName} \\ Artist and Album: {x.ArtistandAlbumName}"); + AMDiscordRPCTray.ChangeSongState($"{x.ArtistandAlbumName.Split('—')[0]} - {x.SongName}"); if (x.ArtistandAlbumName == oldAlbumnArtist && oldData.Assets.LargeImageKey != null) { SetPresence(x); } else { - if (httpRes == Array.Empty() || CoverThread != null) + if (httpRes.Equals(new WebSongResponse()) || CoverThread != null) { - httpRes = await GetCover(x.ArtistandAlbumName.Split('—')[1], HttpUtility.UrlEncode(ConvertToValidString(x.ArtistandAlbumName) + $" {ConvertToValidString(x.SongName)}")); - log.Debug($"Set Cover: {((httpRes.Length > 0) ? httpRes[0] : null)}"); + httpRes = await GetCover(x.ArtistandAlbumName.Split('—')[1], Uri.EscapeDataString(x.ArtistandAlbumName + $" {x.SongName}")); + log.Debug($"Set Cover: {((httpRes.artworkURL != null) ? httpRes.artworkURL : null)}"); } SetPresence(x, httpRes); oldAlbumnArtist = x.ArtistandAlbumName; @@ -43,7 +46,7 @@ static void Main(string[] args) }; CheckDatabaseIntegrity(); InitDBCreds(); - CheckFFMpeg(); + CheckFFmpeg(); InitS3(); AMEvent(); } @@ -124,7 +127,7 @@ static void AMEvent() string previousSong = string.Empty; string previousArtistAlbum = string.Empty; string lastFetchedArtistAlbum = string.Empty; - int audioStatus = 3; + AudioFormat format = AudioFormat.AAC; bool resetStatus = false; double oldValue = 0; @@ -164,6 +167,7 @@ static void AMEvent() } else if (resetStatus == false && slider.AsSlider().Maximum != 0 && oldValue != 0 && currentSong == previousSong && currentArtistAlbum == previousArtistAlbum && startTime != endTime) { + AMDiscordRPCTray.ChangeSongState($"{((isSingle) ? string.Join("-", dashSplit.Take(dashSplit.Length - 1).ToArray()) : string.Join("—", currentArtistAlbum.Split('—').Take(2).ToArray())).Split('—')[0]} - {currentSong}"); ChangeTimestamps(startTime, endTime); oldValue = slider.AsSlider().Value; } @@ -180,8 +184,8 @@ static void AMEvent() CheckAndInsertAlbum(idontknowwhatshouldinamethisbutitsaboutalbum.Split('—')[1]); Task t = new Task(async () => { - httpRes = await GetCover(idontknowwhatshouldinamethisbutitsaboutalbum.Split('—')[1], HttpUtility.UrlEncode(ConvertToValidString((isSingle) ? string.Join("-", dashSplit.Take(dashSplit.Length - 1).ToArray()) : string.Join("—", currentArtistAlbum.Split('—').Take(2).ToArray())) + $" {ConvertToValidString(currentSong)}")); - log.Debug($"Set Cover: {((httpRes.Length > 0) ? httpRes[0] : null)}"); + httpRes = await GetCover(idontknowwhatshouldinamethisbutitsaboutalbum.Split('—')[1], Uri.EscapeDataString((isSingle) ? string.Join("-", dashSplit.Take(dashSplit.Length - 1).ToArray()) : string.Join("—", currentArtistAlbum.Split('—').Take(2).ToArray()) + $" {currentSong}")); + log.Debug($"Set Cover: {((httpRes.artworkURL != null) ? httpRes.artworkURL : null)}"); }); CoverThread = t; t.Start(); @@ -198,31 +202,35 @@ static void AMEvent() switch (audioBadge?.Name) { case "Dolby Atmos": - audioStatus = 1; + format = AudioFormat.Dolby_Atmos; break; case "Dolby Audio": - audioStatus = 2; + format = AudioFormat.Dolby_Audio; break; default: - audioStatus = 0; + format = AudioFormat.Lossless; break; } } - else audioStatus = 3; + else format = AudioFormat.AAC; oldValue = 0; + startTime = currentTime.Subtract(subractThis); + endTime = currentTime.AddSeconds(slider.AsSlider().Maximum).Subtract(subractThis); oldStartTime = startTime; oldEndTime = endTime; - AMSongDataEvent.ChangeSong(new SongData(currentSong, (isSingle) ? string.Join("-", dashSplit.Take(dashSplit.Length - 1).ToArray()) : string.Join("—", currentArtistAlbum.Split('—').Take(2).ToArray()), currentArtistAlbum.Split('—').Length <= 1, startTime, endTime, audioStatus)); + AMSongDataEvent.ChangeSong(new SongData(currentSong, (isSingle) ? string.Join("-", dashSplit.Take(dashSplit.Length - 1).ToArray()) : string.Join("—", currentArtistAlbum.Split('—').Take(2).ToArray()), currentArtistAlbum.Split('—').Length <= 1, startTime, endTime, format)); } if (playButton?.Name != null && (localizedPlay != null && localizedPlay == playButton?.Name || localizedStop != null && localizedStop != playButton?.Name)) { localizedPlay = playButton.Name; + AMDiscordRPCTray.ChangeSongState("AMDiscordRPC"); client.ClearPresence(); resetStatus = true; } else if (resetStatus == true && playButton?.Name != null && localizedPlay != null && localizedPlay != playButton?.Name && slider.AsSlider().Maximum != 0) { + AMDiscordRPCTray.ChangeSongState($"{((isSingle) ? string.Join("-", dashSplit.Take(dashSplit.Length - 1).ToArray()) : string.Join("—", currentArtistAlbum.Split('—').Take(2).ToArray())).Split('—')[0]} - {currentSong}"); ChangeTimestamps(startTime, endTime); resetStatus = false; } diff --git a/AMDiscordRPC/AMDiscordRPC.csproj b/AMDiscordRPC/AMDiscordRPC.csproj index 7f27849..4d4c56a 100644 --- a/AMDiscordRPC/AMDiscordRPC.csproj +++ b/AMDiscordRPC/AMDiscordRPC.csproj @@ -1,6 +1,6 @@  - + Debug @@ -96,24 +96,28 @@ + + Resources\Logo Black.ico + + - - ..\packages\AngleSharp.1.2.0\lib\net472\AngleSharp.dll + + ..\packages\AngleSharp.1.3.0\lib\net472\AngleSharp.dll - - ..\packages\AWSSDK.Core.3.7.402.43\lib\net45\AWSSDK.Core.dll + + ..\packages\AWSSDK.Core.4.0.0.19\lib\net472\AWSSDK.Core.dll - - ..\packages\AWSSDK.S3.3.7.416.13\lib\net45\AWSSDK.S3.dll + + ..\packages\AWSSDK.S3.4.0.6.2\lib\net472\AWSSDK.S3.dll - - ..\packages\DiscordRichPresence.1.3.0.28\lib\net45\DiscordRPC.dll + + ..\packages\DiscordRichPresence.1.5.0.51\lib\net45\DiscordRPC.dll - ..\packages\EntityFramework.6.4.4\lib\net45\EntityFramework.dll + ..\packages\EntityFramework.6.5.1\lib\net45\EntityFramework.dll - ..\packages\EntityFramework.6.4.4\lib\net45\EntityFramework.SqlServer.dll + ..\packages\EntityFramework.6.5.1\lib\net45\EntityFramework.SqlServer.dll ..\packages\FlaUI.Core.4.0.0\lib\netstandard2.0\FlaUI.Core.dll @@ -125,24 +129,29 @@ ..\packages\Interop.UIAutomationClient.10.19041.0\lib\net45\Interop.UIAutomationClient.dll False - - ..\packages\log4net.3.0.3\lib\net462\log4net.dll + + ..\packages\log4net.3.1.0\lib\net462\log4net.dll ..\packages\M3U8Parser.2.0.0\lib\netstandard2.0\M3U8Parser.dll + + ..\packages\Microsoft.Bcl.AsyncInterfaces.9.0.7\lib\net462\Microsoft.Bcl.AsyncInterfaces.dll + ..\packages\Microsoft.Win32.Registry.5.0.0\lib\net461\Microsoft.Win32.Registry.dll ..\packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll + + - - ..\packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll + + ..\packages\System.Buffers.4.6.1\lib\net462\System.Buffers.dll - - ..\packages\System.CodeDom.9.0.0\lib\net462\System.CodeDom.dll + + ..\packages\System.CodeDom.9.0.7\lib\net462\System.CodeDom.dll @@ -157,45 +166,58 @@ ..\packages\System.Data.SQLite.Linq.1.0.119.0\lib\net46\System.Data.SQLite.Linq.dll - - ..\packages\System.Diagnostics.PerformanceCounter.9.0.0\lib\net462\System.Diagnostics.PerformanceCounter.dll + + ..\packages\System.Diagnostics.PerformanceCounter.9.0.7\lib\net462\System.Diagnostics.PerformanceCounter.dll - ..\packages\System.Drawing.Common.9.0.0\lib\net462\System.Drawing.Common.dll + ..\packages\System.Drawing.Common.9.0.7\lib\net462\System.Drawing.Common.dll + + + ..\packages\System.IO.Pipelines.9.0.7\lib\net462\System.IO.Pipelines.dll - - ..\packages\System.Memory.4.5.5\lib\net461\System.Memory.dll + + ..\packages\System.Memory.4.6.3\lib\net462\System.Memory.dll - - ..\packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll + + ..\packages\System.Numerics.Vectors.4.6.1\lib\net462\System.Numerics.Vectors.dll - - ..\packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll + + ..\packages\System.Runtime.CompilerServices.Unsafe.6.1.2\lib\net462\System.Runtime.CompilerServices.Unsafe.dll ..\packages\System.Security.AccessControl.6.0.1\lib\net461\System.Security.AccessControl.dll - - ..\packages\System.Security.Permissions.9.0.0\lib\net462\System.Security.Permissions.dll + + ..\packages\System.Security.Permissions.9.0.7\lib\net462\System.Security.Permissions.dll ..\packages\System.Security.Principal.Windows.5.0.0\lib\net461\System.Security.Principal.Windows.dll - - ..\packages\System.Text.Encoding.CodePages.8.0.0\lib\net462\System.Text.Encoding.CodePages.dll + + ..\packages\System.Text.Encoding.CodePages.9.0.7\lib\net462\System.Text.Encoding.CodePages.dll + + + ..\packages\System.Text.Encodings.Web.9.0.7\lib\net462\System.Text.Encodings.Web.dll + + + ..\packages\System.Text.Json.9.0.7\lib\net462\System.Text.Json.dll + + + ..\packages\System.Threading.Tasks.Extensions.4.6.3\lib\net462\System.Threading.Tasks.Extensions.dll + @@ -215,9 +237,19 @@ + + + True + True + Resources.resx + + + + InputWindow.xaml + @@ -237,10 +269,35 @@ + + ResXFileCodeGenerator + Resources.Designer.cs + + + + + Designer + MSBuild:Compile + + + + + + + + + Always + + + + + Always + - - + + Always + @@ -249,10 +306,12 @@ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - + + + - + + \ No newline at end of file diff --git a/AMDiscordRPC/App.config b/AMDiscordRPC/App.config index 1e05619..2012abb 100644 --- a/AMDiscordRPC/App.config +++ b/AMDiscordRPC/App.config @@ -11,7 +11,7 @@ - + @@ -19,7 +19,35 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -33,6 +61,8 @@ - + + + \ No newline at end of file diff --git a/AMDiscordRPC/Covers.cs b/AMDiscordRPC/Covers.cs index ab5a7e1..5c69e07 100644 --- a/AMDiscordRPC/Covers.cs +++ b/AMDiscordRPC/Covers.cs @@ -1,8 +1,10 @@ using AngleSharp.Html.Dom; using Newtonsoft.Json.Linq; using System; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using static AMDiscordRPC.Database; using static AMDiscordRPC.Globals; using static AMDiscordRPC.Playlist; @@ -12,42 +14,77 @@ public class Covers { public static Task CoverThread; - public static async Task AsyncFetchiTunes(string album, string searchStr) + private static async Task AsyncFetchiTunes(string album, string searchStr) { try { //idk why but sometimes when you search as "Artist - Album Track" and if Album and Track named same it returns random song from album //ex: "Poppy - Negative Spaces negative spaces" Returns "Poppy - New Way Out" as a track link - var iTunesReq = await hclient.GetAsync($"https://itunes.apple.com/search?term={searchStr}&limit=1&entity=song"); + HttpResponseMessage iTunesReq = await hclient.GetAsync($"https://itunes.apple.com/search?term={searchStr}&limit=1&entity=song"); if (iTunesReq.IsSuccessStatusCode) { dynamic imageRes = JObject.Parse(await iTunesReq.Content.ReadAsStringAsync()); if (imageRes["resultCount"] != 0) { - string[] res = { imageRes["results"][0]["artworkUrl100"].ToString(), imageRes["results"][0]["trackViewUrl"].ToString(), imageRes["results"][0]["collectionName"].ToString() }; - Database.UpdateAlbum(new Database.SQLCoverResponse(album, res[0], res[1], null, null, null)); + WebSongResponse webRes = new WebSongResponse + ( + imageRes["results"][0]["artworkUrl100"].ToString(), + imageRes["results"][0]["trackViewUrl"].ToString(), + imageRes["results"][0]["collectionName"].ToString() + ); + Database.UpdateAlbum(new Database.SQLCoverResponse(album, webRes.artworkURL, webRes.trackURL)); CoverThread = null; - return res; + return webRes; } else { log.Warn("iTunes no image found"); CoverThread = null; - return Array.Empty(); + return new WebSongResponse(); } } else { log.Warn("iTunes request failed"); CoverThread = null; - return Array.Empty(); + return new WebSongResponse(); } } catch (Exception e) { log.Error($"iTunes Exception {e.Message}"); CoverThread = null; - return Array.Empty(); + return new WebSongResponse(); + } + } + + public static async Task AsyncAMFetch(string album, string searchStr) + { + try + { + HttpResponseMessage AMRequest = await hclient.GetAsync($"https://music.apple.com/{AMRegion.ToLower()}/search?term={searchStr}"); + if (AMRequest.IsSuccessStatusCode) + { + string DOMasAString = await AMRequest.Content.ReadAsStringAsync(); + IHtmlDocument document = parser.ParseDocument(DOMasAString); + WebSongResponse webRes = new WebSongResponse( + document.DocumentElement.QuerySelectorAll("div.top-search-lockup__artwork > div > picture > source")[1].GetAttribute("srcset").Split(' ')[0], + document.DocumentElement.QuerySelector("div.top-search-lockup__action > a").GetAttribute("href") + ); + CoverThread = null; + Database.UpdateAlbum(new Database.SQLCoverResponse(album, webRes.artworkURL, webRes.trackURL)); + return webRes; + } + else + { + log.Error($"Apple Music request failed returned: {AMRequest.StatusCode}"); + return await AsyncFetchiTunes(album, searchStr); + } + } + catch (Exception e) + { + log.Error($"Apple Music Request failed. {e}"); + return await AsyncFetchiTunes(album, searchStr); } } @@ -72,28 +109,40 @@ public static async Task CheckAnimatedCover(string album, string url, Cancellati { log.Error($"Apple Music animatedCover exception: {e.Message}"); Discord.animatedCoverCts = null; - Database.UpdateAlbum(new Database.SQLCoverResponse(album, null, null, false, null, null)); + Database.UpdateAlbum(new Database.SQLCoverResponse(album, null, null, false)); } } - public static async Task GetCover(string album, string searchStr) + public static async Task GetCover(string album, string searchStr) { try { - Database.SQLCoverResponse cover = Database.GetAlbumDataFromSQL(album); + log.Debug($"https://music.apple.com/us/search?term={searchStr}"); + SQLCoverResponse cover = GetAlbumDataFromSQL(album); if (cover != null) { - string[] res = { (cover.animated == true && cover.animatedURL != null) ? cover.animatedURL : (cover.source != null) ? cover.source : throw new Exception("Source not found."), cover.redirURL, album }; + WebSongResponse res = new WebSongResponse + ( + (cover.animated == true && cover.animatedURL != null) ? cover.animatedURL : (cover.source != null) ? cover.source : throw new Exception("Source not found."), + cover.redirURL, + album + ); + CoverThread = null; return res; } - else return await AsyncFetchiTunes(album, searchStr); + else + { + return await AsyncAMFetch(album, searchStr); + } } catch (Exception ex) { - return await AsyncFetchiTunes(album, searchStr); + return await AsyncAMFetch(album, searchStr); } } + + /* I realized we don't need Last.fm API to be here, bc we are making Apple Music RPC aren't we? so i decided to just use iTunes and go on. * might add later for the situation where iTunes api is down. diff --git a/AMDiscordRPC/Database.cs b/AMDiscordRPC/Database.cs index fda3e3b..2594fb2 100644 --- a/AMDiscordRPC/Database.cs +++ b/AMDiscordRPC/Database.cs @@ -2,8 +2,6 @@ using System.Collections.Generic; using System.Data.SQLite; using System.Linq; -using System.Reflection; -using System.Threading.Tasks; using static AMDiscordRPC.Globals; namespace AMDiscordRPC @@ -11,10 +9,10 @@ namespace AMDiscordRPC internal class Database { private static SQLiteConnection sqlite; - private static readonly Dictionary sqlMap = new Dictionary() + public static readonly Dictionary sqlMap = new Dictionary() { {"coverTable", "album TEXT PRIMARY KEY NOT NULL, source TEXT, redirURL TEXT DEFAULT 'https://music.apple.com/home', animated BOOLEAN CHECK (animated IN (0,1)) DEFAULT NULL, streamURL TEXT, animatedURL TEXT" }, - {"creds", "S3_accessKey TEXT NOT NULL, S3_secretKey TEXT NOT NULL, S3_serviceURL TEXT NOT NULL, S3_bucketName TEXT NOT NULL, S3_bucketURL TEXT NOT NULL, S3_isSpecificKey BOOLEAN NOT NULL CHECK (S3_isSpecificKey IN (0,1))" }, + {"creds", "S3_accessKey TEXT, S3_secretKey TEXT, S3_serviceURL TEXT, S3_bucketName TEXT, S3_bucketURL TEXT, S3_isSpecificKey BOOLEAN CHECK (S3_isSpecificKey IN (0,1)), FFmpegPath TEXT" }, {"logs", "timestamp INTEGER, type TEXT, occuredAt TEXT, message TEXT" } }; @@ -258,7 +256,7 @@ public class SQLCoverResponse public string streamURL { get; set; } public string animatedURL { get; set; } - public SQLCoverResponse(string album, string source, string redirURL, bool? animated, string streamURL, string animatedURL) + public SQLCoverResponse(string album = null, string source = null, string redirURL = null, bool? animated = null, string streamURL = null, string animatedURL = null) { this.album = album; this.source = source; @@ -277,6 +275,7 @@ public List GetNotNullValues() { return GetType().GetProperties().Where(s => s.GetValue(this) != null && s.GetValue(this) != this.album).Select(p => (p.PropertyType == typeof(string)) ? $"'{p.GetValue(this)}'" : p.GetValue(this)).ToList(); } + } } } diff --git a/AMDiscordRPC/Discord.cs b/AMDiscordRPC/Discord.cs index 474ae35..124d731 100644 --- a/AMDiscordRPC/Discord.cs +++ b/AMDiscordRPC/Discord.cs @@ -5,7 +5,6 @@ using System.Web; using static AMDiscordRPC.Covers; using static AMDiscordRPC.Globals; -using static AMDiscordRPC.Playlist; namespace AMDiscordRPC { @@ -51,10 +50,10 @@ public static void SetPresence(SongData x) private static async Task AsyncSetButton(SongData x) { - string[] resp = await GetCover(x.ArtistandAlbumName.Split('—')[1], HttpUtility.UrlEncode(ConvertToValidString(x.ArtistandAlbumName) + $" {ConvertToValidString(x.SongName)}")); + WebSongResponse resp = await GetCover(x.ArtistandAlbumName.Split('—')[1], HttpUtility.UrlEncode(x.ArtistandAlbumName + $" {x.SongName}")); oldData.Buttons = new Button[] { - new Button() { Label = "Listen on Apple Music", Url = (resp.Length > 0) ? resp[1].Replace("https://", "music://") : "music://music.apple.com/home"} + new Button() { Label = "Listen on Apple Music", Url = (resp.trackURL != null) ? resp.trackURL.Replace("https://", "music://") : "music://music.apple.com/home"} }; client.SetPresence(oldData); thread = null; @@ -67,7 +66,7 @@ public static async Task SetCover(string coverURL) animatedCoverCts = null; } - public static void SetPresence(SongData x, string[] resp) + public static void SetPresence(SongData x, WebSongResponse resp) { log.Debug($"Timestamps {x.StartTime}/{x.EndTime}"); if (thread != null) thread.Abort(); @@ -80,17 +79,18 @@ public static void SetPresence(SongData x, string[] resp) { Type = ActivityType.Listening, Details = ConvertToValidString(x.SongName), - State = $"by {((x.IsMV) ? x.ArtistandAlbumName : ConvertToValidString(x.ArtistandAlbumName.Split('—')[0]))}", + StatusDisplay = StatusDisplayType.State, + State = (x.IsMV) ? x.ArtistandAlbumName : ConvertToValidString(x.ArtistandAlbumName.Split('—')[0]), Assets = new Assets() { - LargeImageKey = (resp.Length > 0) ? resp[0] : "", - LargeImageText = (x.IsMV) ? resp[2] : ConvertToValidString(x.ArtistandAlbumName.Split('—')[1]), - SmallImageKey = (x.AudioDetail == 0) ? "lossless" : (x.AudioDetail == 1 || x.AudioDetail == 2) ? "dolbysimplified" : null, - SmallImageText = (x.AudioDetail == 0) ? "Lossless" : (x.AudioDetail == 1) ? "Dolby Atmos" : (x.AudioDetail == 2) ? "Dolby Audio" : null, + LargeImageKey = (resp.artworkURL != null) ? resp.artworkURL : "", + LargeImageText = (x.IsMV && resp.trackName != null) ? resp.trackName : ConvertToValidString(x.ArtistandAlbumName.Split('—')[1]), + SmallImageKey = (x.format == AudioFormat.Lossless) ? "lossless" : (x.format == AudioFormat.Dolby_Atmos || x.format == AudioFormat.Dolby_Audio) ? "dolbysimplified" : null, + SmallImageText = (x.format == AudioFormat.Lossless) ? "Lossless" : (x.format == AudioFormat.Dolby_Atmos) ? "Dolby Atmos" : (x.format == AudioFormat.Dolby_Audio) ? "Dolby Audio" : null, }, Buttons = new Button[] { - new Button() { Label = "Listen on Apple Music", Url = (resp.Length > 0) ? resp[1].Replace("https://", "music://") : "music://music.apple.com/home"} + new Button() { Label = "Listen on Apple Music", Url = (resp.trackURL != null) ? resp.trackURL.Replace("https://", "music://") : "music://music.apple.com/home"} }, Timestamps = new Timestamps() { @@ -99,10 +99,10 @@ public static void SetPresence(SongData x, string[] resp) } }; client.SetPresence(oldData); - if (resp != null && resp.Length > 0 && !string.IsNullOrEmpty(resp[0]) && !resp[0].Contains((S3_Credentials != null) ? S3_Credentials.bucketURL : "")) + if (resp.artworkURL != null && !resp.artworkURL.Contains((S3_Credentials != null) ? (S3_Credentials.GetNullKeys().Count == 0) ? S3_Credentials.bucketURL : "" : "")) { animatedCoverCts = new CancellationTokenSource(); - Task t = new Task(() => CheckAnimatedCover(ConvertToValidString(x.ArtistandAlbumName.Split('—')[1]), resp[1], animatedCoverCts.Token)); + Task t = new Task(() => CheckAnimatedCover(ConvertToValidString(x.ArtistandAlbumName.Split('—')[1]), resp.trackURL, animatedCoverCts.Token)); t.Start(); } } diff --git a/AMDiscordRPC/Globals.cs b/AMDiscordRPC/Globals.cs index 3a0b0e8..dad6c88 100644 --- a/AMDiscordRPC/Globals.cs +++ b/AMDiscordRPC/Globals.cs @@ -3,16 +3,18 @@ using DiscordRPC.Helper; using log4net; using log4net.Config; -using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Data.SQLite; using System.Diagnostics; using System.Linq; +using System.Net; using System.Net.Http; using System.Reflection; using System.Text; -using System.Threading.Tasks; +using System.Text.RegularExpressions; +using static AMDiscordRPC.Database; +using static AMDiscordRPC.UI; namespace AMDiscordRPC { @@ -28,9 +30,26 @@ public static class Globals public static readonly Assembly assembly = Assembly.GetExecutingAssembly(); public static HtmlParser parser = new HtmlParser(); public static RichPresence oldData = new RichPresence(); - public static string[] httpRes = Array.Empty(); + public static WebSongResponse httpRes = new WebSongResponse(); public static string ffmpegPath; public static S3_Creds S3_Credentials; + private static List newMatchesArr; + public enum S3ConnectionStatus + { + Connected, + Disconnected, + Error + } + public enum AudioFormat + { + Lossless, + Dolby_Atmos, + Dolby_Audio, + AAC + } + public static S3ConnectionStatus S3Status = S3ConnectionStatus.Disconnected; + public static string AMRegion; + public static void ConfigureLogger() { @@ -40,6 +59,27 @@ public static void ConfigureLogger() } } + public static async void InitRegion() + { + HttpClientHandler HClientHandlerhandler = new HttpClientHandler(); + CookieContainer cookies = new CookieContainer(); + HClientHandlerhandler.CookieContainer = cookies; + HttpClient httpClient = new HttpClient(HClientHandlerhandler); + + try + { + _ = httpClient.GetAsync("https://music.apple.com/").Result; + + AMRegion = cookies.GetCookies(new Uri("https://music.apple.com/")).Cast() + .Where(cookie => cookie.Name == "geo").ToList()[0].Value; + } + catch (Exception e) + { + log.Error($"Error happened while trying to select region, falling back to US Apple Music. Cause: {e}"); + AMRegion = "US"; + } + } + public class AMSongDataEvent { public static event EventHandler SongChanged; @@ -51,6 +91,7 @@ public static void ChangeSong(SongData e) public static string ConvertToValidString(string data) { + //We dont need byte validation anymore because of this fix https://github.com/Lachee/discord-rpc-csharp/pull/259. Going to change this method soon. if (!data.WithinLength(125, Encoding.UTF8)) { byte[] byteArr = Encoding.UTF8.GetBytes(data); @@ -62,7 +103,7 @@ public static string ConvertToValidString(string data) public static void InitDBCreds() { - using (SQLiteDataReader dbResp = Database.ExecuteReaderCommand("SELECT * FROM creds LIMIT 1")) + using (SQLiteDataReader dbResp = Database.ExecuteReaderCommand($"SELECT {string.Join(", ", Regex.Matches(Database.sqlMap["creds"], @"S3_\w+").FilterRepeatMatches())} FROM creds LIMIT 1")) { while (dbResp.Read()) { @@ -77,7 +118,7 @@ public static void InitDBCreds() } } - private static void StartFFMpegProcess(string filename) + private static void StartFFmpegProcess(string filename) { try { @@ -104,16 +145,22 @@ private static void StartFFMpegProcess(string filename) } catch (Exception ex) { - log.Error($"FFMpeg Check error: {ex}"); + log.Error($"FFmpeg Check error: {ex}"); } } - public static async void CheckFFMpeg() + public static async void CheckFFmpeg() { List paths = Environment.GetEnvironmentVariable("PATH").Split(';').Where(v => v.Contains("ffmpeg")).Select(s => $@"{s}\ffmpeg.exe").Prepend("ffmpeg").ToList(); - foreach (var item in paths) + object SQLQueryRes = ExecuteScalarCommand($"SELECT FFmpegPath from creds"); + + if (SQLQueryRes != null) + { + paths.Add(SQLQueryRes.ToString() + "\\ffmpeg.exe"); + } + foreach (string item in paths) { - StartFFMpegProcess(item); + StartFFmpegProcess(item); if (ffmpegPath != null) { break; @@ -123,7 +170,7 @@ public static async void CheckFFMpeg() { log.Info($"Found ffmpeg"); } - else log.Warn("FFmpeg not found"); + else FFmpegDialog(); } public class SongData : EventArgs @@ -133,19 +180,24 @@ public class SongData : EventArgs public bool IsMV { get; set; } public DateTime StartTime { get; set; } public DateTime EndTime { get; set; } - public int AudioDetail { get; set; } + public AudioFormat format { get; set; } - public SongData(string SongName, string ArtistandAlbumName, bool IsMV, DateTime StartTime, DateTime EndTime, int AudioDetail) + public SongData(string SongName, string ArtistandAlbumName, bool IsMV, DateTime StartTime, DateTime EndTime, AudioFormat format) { this.SongName = SongName; this.ArtistandAlbumName = ArtistandAlbumName; this.IsMV = IsMV; this.StartTime = StartTime; this.EndTime = EndTime; - this.AudioDetail = AudioDetail; + this.format = format; } } + public static List FilterRepeatMatches(this MatchCollection matches) + { + return matches.Cast().Select(m => m.Value).Distinct().ToList(); + } + public class S3_Creds { public string accessKey { get; set; } @@ -169,6 +221,38 @@ public List GetNullKeys() { return GetType().GetProperties().Where(s => s.GetValue(this) == null).Select(p => p.Name).ToList(); } + + public List GetNotNullKeys() + { + return GetType().GetProperties().Where(s => s.GetValue(this) != null).Select(p => $"S3_{p.Name}").ToList(); + } + + public List GetNotNullValues() + { + return GetType().GetProperties().Where(s => s.GetValue(this) != null).Select(p => (p.PropertyType == typeof(string)) ? $"'{p.GetValue(this)}'" : p.GetValue(this)).ToList(); + } + } + + public class WebSongResponse + { + public string artworkURL { get; set; } + public string trackURL { get; set; } + public string trackName { get; set; } + + public WebSongResponse(string artworkURL = null, string trackURL = null, string trackName = null) + { + this.artworkURL = artworkURL; + this.trackURL = trackURL; + this.trackName = trackName; + } + + public override bool Equals(object obj) + { + return obj is WebSongResponse other && + artworkURL == other.artworkURL && + trackURL == other.trackURL && + trackName == other.trackName; + } } } -} +} \ No newline at end of file diff --git a/AMDiscordRPC/Helpers/TextBoxHelper.cs b/AMDiscordRPC/Helpers/TextBoxHelper.cs new file mode 100644 index 0000000..c3789b8 --- /dev/null +++ b/AMDiscordRPC/Helpers/TextBoxHelper.cs @@ -0,0 +1,154 @@ +using System.Linq; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Documents; +using System.Windows.Media; + +namespace AMDiscordRPC.Helpers +{ + // Provided by Microsoft https://learn.microsoft.com/en-us/dotnet/desktop/wpf/controls/how-to-add-a-watermark-to-a-textbox + public static class TextBoxHelper + { + public static string GetPlaceholder(DependencyObject obj) => + (string)obj.GetValue(PlaceholderProperty); + + public static void SetPlaceholder(DependencyObject obj, string value) => + obj.SetValue(PlaceholderProperty, value); + + public static PlaceholderAdorner GetPlaceholderAdorner(TextBox obj) + { + GetOrCreateAdorner(obj, out PlaceholderAdorner adorner); + return adorner; + } + + public static readonly DependencyProperty PlaceholderProperty = + DependencyProperty.RegisterAttached( + "Placeholder", + typeof(string), + typeof(TextBoxHelper), + new FrameworkPropertyMetadata( + defaultValue: null, + propertyChangedCallback: OnPlaceholderChanged) + ); + + private static void OnPlaceholderChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is TextBox textBoxControl) + { + if (!textBoxControl.IsLoaded) + { + // Ensure that the events are not added multiple times + textBoxControl.Loaded -= TextBoxControl_Loaded; + textBoxControl.Loaded += TextBoxControl_Loaded; + } + + textBoxControl.TextChanged -= TextBoxControl_TextChanged; + textBoxControl.TextChanged += TextBoxControl_TextChanged; + + // If the adorner exists, invalidate it to draw the current text + if (GetOrCreateAdorner(textBoxControl, out PlaceholderAdorner adorner)) + adorner.InvalidateVisual(); + } + } + + private static void TextBoxControl_Loaded(object sender, RoutedEventArgs e) + { + if (sender is TextBox textBoxControl) + { + textBoxControl.Loaded -= TextBoxControl_Loaded; + GetOrCreateAdorner(textBoxControl, out _); + } + } + + private static void TextBoxControl_TextChanged(object sender, TextChangedEventArgs e) + { + if (sender is TextBox textBoxControl + && GetOrCreateAdorner(textBoxControl, out PlaceholderAdorner adorner)) + { + // Control has text. Hide the adorner. + if (textBoxControl.Text.Length > 0) + adorner.Visibility = Visibility.Hidden; + + // Control has no text. Show the adorner. + else + adorner.Visibility = Visibility.Visible; + } + } + + private static bool GetOrCreateAdorner(TextBox textBoxControl, out PlaceholderAdorner adorner) + { + // Get the adorner layer + AdornerLayer layer = AdornerLayer.GetAdornerLayer(textBoxControl); + + // If null, it doesn't exist or the control's template isn't loaded + if (layer == null) + { + adorner = null; + return false; + } + + // Layer exists, try to find the adorner + adorner = layer.GetAdorners(textBoxControl)?.OfType().FirstOrDefault(); + + // Adorner never added to control, so add it + if (adorner == null) + { + adorner = new PlaceholderAdorner(textBoxControl); + layer.Add(adorner); + } + + return true; + } + + public class PlaceholderAdorner : Adorner + { + public PlaceholderAdorner(TextBox textBox) : base(textBox) + { + IsHitTestVisible = false; + } + + protected override void OnRender(DrawingContext drawingContext) + { + TextBox textBoxControl = (TextBox)AdornedElement; + + string placeholderValue = TextBoxHelper.GetPlaceholder(textBoxControl); + + if (string.IsNullOrEmpty(placeholderValue)) + return; + + // Create the formatted text object + FormattedText text = new FormattedText( + placeholderValue, + System.Globalization.CultureInfo.CurrentCulture, + textBoxControl.FlowDirection, + new Typeface(textBoxControl.FontFamily, + textBoxControl.FontStyle, + textBoxControl.FontWeight, + textBoxControl.FontStretch), + textBoxControl.FontSize, + SystemColors.GrayTextBrush, + VisualTreeHelper.GetDpi(textBoxControl).PixelsPerDip); + + text.MaxTextWidth = System.Math.Max(textBoxControl.ActualWidth - textBoxControl.Padding.Left - textBoxControl.Padding.Right, 10); + text.MaxTextHeight = System.Math.Max(textBoxControl.ActualHeight, 10); + + // Render based on padding of the control, to try and match where the textbox places text + Point renderingOffset = new Point(textBoxControl.Padding.Left, textBoxControl.Padding.Top); + + // Template contains the content part; adjust sizes to try and align the text + if (textBoxControl.Template.FindName("PART_ContentHost", textBoxControl) is FrameworkElement part) + { + Point partPosition = part.TransformToAncestor(textBoxControl).Transform(new Point(0, 0)); + renderingOffset.X += partPosition.X + 2; + renderingOffset.Y += partPosition.Y; + + text.MaxTextWidth = System.Math.Max(part.ActualWidth - renderingOffset.X, 10); + text.MaxTextHeight = System.Math.Max(part.ActualHeight, 10); + } + + // Draw the text + drawingContext.DrawText(text, renderingOffset); + } + } + } +} \ No newline at end of file diff --git a/AMDiscordRPC/Playlist.cs b/AMDiscordRPC/Playlist.cs index 56c0d7f..e3520bd 100644 --- a/AMDiscordRPC/Playlist.cs +++ b/AMDiscordRPC/Playlist.cs @@ -1,6 +1,4 @@ -using AngleSharp.Text; -using FlaUI.Core.Tools; -using M3U8Parser; +using M3U8Parser; using M3U8Parser.Tags.MultivariantPlaylist; using System; using System.Collections.Generic; @@ -9,7 +7,6 @@ using System.Linq; using System.Net; using System.Net.Http; -using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; @@ -24,7 +21,7 @@ internal class Playlist public static async Task ConvertM3U8(string album, string playlistUrl, CancellationToken ct) { // ^I thought storing Master Playlist would be better for in case of bucket changes and Apple's codec changes on lowest quality. - Database.UpdateAlbum(new Database.SQLCoverResponse(album, null, null, true, playlistUrl, null)); + Database.UpdateAlbum(new Database.SQLCoverResponse(album, null, null, true, playlistUrl)); StreamInf playlist = await FetchResolution(playlistUrl); if (!ct.IsCancellationRequested && playlist != null) { @@ -48,10 +45,9 @@ public static async Task ConvertM3U8(string album, string playlistUrl, Cancellat else throw new Exception("FFmpeg not found"); log.Debug($"Converted to GIF. Path: {gifPath}"); if (ct.IsCancellationRequested) throw new Exception("Cancelled"); - if (S3_Credentials != null && S3_Credentials.GetNullKeys().Count == 0) servedPath = await PutGIF(gifPath, fileName.Replace(".mp4", ".gif")); + if (S3Status == S3ConnectionStatus.Connected) servedPath = await PutGIF(gifPath, fileName.Replace(".mp4", ".gif")); else throw new Exception("S3 is not properly configured."); log.Debug("Put S3 Bucket"); - if (ct.IsCancellationRequested) throw new Exception("Cancelled"); Database.UpdateAlbum(new Database.SQLCoverResponse(album, null, null, null, null, servedPath)); if (ct.IsCancellationRequested) throw new Exception("Cancelled"); SetCover(servedPath); diff --git a/AMDiscordRPC/Properties/AssemblyInfo.cs b/AMDiscordRPC/Properties/AssemblyInfo.cs index b3771ac..6cf49e4 100644 --- a/AMDiscordRPC/Properties/AssemblyInfo.cs +++ b/AMDiscordRPC/Properties/AssemblyInfo.cs @@ -5,9 +5,9 @@ // set of attributes. Change these attribute values to modify the information // associated with an assembly. [assembly: AssemblyTitle("AMDiscordRPC")] -[assembly: AssemblyDescription("")] +[assembly: AssemblyDescription("Apple Music Discord Rich Presence")] [assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] +[assembly: AssemblyCompany("CrawLeyYou")] [assembly: AssemblyProduct("AMDiscordRPC")] [assembly: AssemblyCopyright("Copyright © 2024")] [assembly: AssemblyTrademark("")] @@ -31,5 +31,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.2.0.0")] -[assembly: AssemblyFileVersion("1.2.0.0")] +[assembly: AssemblyVersion("1.3.5.0")] +[assembly: AssemblyFileVersion("1.3.5.0")] diff --git a/AMDiscordRPC/Properties/Resources.Designer.cs b/AMDiscordRPC/Properties/Resources.Designer.cs new file mode 100644 index 0000000..91b7f95 --- /dev/null +++ b/AMDiscordRPC/Properties/Resources.Designer.cs @@ -0,0 +1,93 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace AMDiscordRPC.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("AMDiscordRPC.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap Logo_Black { + get { + object obj = ResourceManager.GetObject("Logo_Black", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). + /// + internal static System.Drawing.Icon Logo_Black_32 { + get { + object obj = ResourceManager.GetObject("Logo_Black_32", resourceCulture); + return ((System.Drawing.Icon)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). + /// + internal static System.Drawing.Icon Logo_Black1 { + get { + object obj = ResourceManager.GetObject("Logo_Black1", resourceCulture); + return ((System.Drawing.Icon)(obj)); + } + } + } +} diff --git a/AMDiscordRPC/Properties/Resources.resx b/AMDiscordRPC/Properties/Resources.resx new file mode 100644 index 0000000..f1faf63 --- /dev/null +++ b/AMDiscordRPC/Properties/Resources.resx @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + ..\Resources\Logo Black.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\Logo Black.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\Logo Black 32.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + \ No newline at end of file diff --git a/AMDiscordRPC/Resources/Logo Black 32.ico b/AMDiscordRPC/Resources/Logo Black 32.ico new file mode 100644 index 0000000..ef316f6 Binary files /dev/null and b/AMDiscordRPC/Resources/Logo Black 32.ico differ diff --git a/AMDiscordRPC/Resources/Logo Black.ico b/AMDiscordRPC/Resources/Logo Black.ico new file mode 100644 index 0000000..ff1a4af Binary files /dev/null and b/AMDiscordRPC/Resources/Logo Black.ico differ diff --git a/AMDiscordRPC/Resources/Logo Black.png b/AMDiscordRPC/Resources/Logo Black.png new file mode 100644 index 0000000..9433ee3 Binary files /dev/null and b/AMDiscordRPC/Resources/Logo Black.png differ diff --git a/AMDiscordRPC/S3.cs b/AMDiscordRPC/S3.cs index f27ca2c..47fd5be 100644 --- a/AMDiscordRPC/S3.cs +++ b/AMDiscordRPC/S3.cs @@ -1,11 +1,8 @@ using Amazon.Runtime; using Amazon.S3; using Amazon.S3.Model; +using AMDiscordRPC.UIComponents; using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Text; using System.Threading.Tasks; using static AMDiscordRPC.Globals; @@ -17,15 +14,31 @@ internal class S3 public static void InitS3() { - if (S3_Credentials != null && S3_Credentials.GetNullKeys().Count == 0) + try { - BasicAWSCredentials credentials = new BasicAWSCredentials(S3_Credentials.accessKey, S3_Credentials.secretKey); - s3Client = new AmazonS3Client(credentials, new AmazonS3Config + if (S3_Credentials != null && S3_Credentials.GetNullKeys().Count == 0) { - ServiceURL = S3_Credentials.serviceURL, - RequestChecksumCalculation = RequestChecksumCalculation.WHEN_REQUIRED, - ResponseChecksumValidation = ResponseChecksumValidation.WHEN_REQUIRED - }); + BasicAWSCredentials credentials = new BasicAWSCredentials(S3_Credentials.accessKey, S3_Credentials.secretKey); + s3Client = new AmazonS3Client(credentials, new AmazonS3Config + { + ServiceURL = S3_Credentials.serviceURL, + RequestChecksumCalculation = RequestChecksumCalculation.WHEN_REQUIRED, + ResponseChecksumValidation = ResponseChecksumValidation.WHEN_REQUIRED + }); + S3Status = S3ConnectionStatus.Connected; + InputWindow.ChangeS3Status(S3ConnectionStatus.Connected); + } + else + { + S3Status = S3ConnectionStatus.Disconnected; + InputWindow.ChangeS3Status(S3ConnectionStatus.Disconnected); + } + } + catch (Exception e) + { + S3Status = S3ConnectionStatus.Error; + InputWindow.ChangeS3Status(S3ConnectionStatus.Error); + log.Error(e); } } diff --git a/AMDiscordRPC/UI.cs b/AMDiscordRPC/UI.cs new file mode 100644 index 0000000..4af808e --- /dev/null +++ b/AMDiscordRPC/UI.cs @@ -0,0 +1,119 @@ +using AMDiscordRPC.UIComponents; +using System; +using System.Diagnostics; +using System.Drawing; +using System.IO; +using System.Threading; +using System.Windows; +using System.Windows.Forms; +using static AMDiscordRPC.Database; +using static AMDiscordRPC.Globals; +using Application = System.Windows.Application; +using OpenFileDialog = Microsoft.Win32.OpenFileDialog; + +namespace AMDiscordRPC +{ + internal class UI + { + private static InputWindow inputWindow; + private static Application app; + private static Thread mainThread = Thread.CurrentThread; + + public static void CreateUI() + { + Thread thread = new Thread(() => + { + new AMDiscordRPCTray(); + app = new Application(); + app.ShutdownMode = ShutdownMode.OnExplicitShutdown; + app.Run(); + }); + + thread.SetApartmentState(ApartmentState.STA); + thread.Start(); + } + + public static void FFmpegDialog() + { + log.Warn("FFmpeg not found"); + Thread thread = new Thread(() => + { + if (System.Windows.MessageBox.Show("FFmpeg not found. Do you want to specify the path?", "FFmpeg not found.", MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.Yes) == MessageBoxResult.Yes) + { + log.Debug("Yes"); + OpenFileDialog openFileDialog = new OpenFileDialog(); + openFileDialog.FileName = "ffmpeg"; + openFileDialog.DefaultExt = ".exe"; + openFileDialog.Filter = "Executables (.exe)|*.exe"; + openFileDialog.Multiselect = false; + bool? openFileDialogRes = openFileDialog.ShowDialog(); + + if (openFileDialogRes == true) + { + ffmpegPath = Path.GetDirectoryName(openFileDialog.FileName); + if (ExecuteScalarCommand($"SELECT FFmpegPath from creds") != null) + ExecuteNonQueryCommand($"UPDATE creds SET FFmpegPath = '{ffmpegPath}'"); + else + ExecuteNonQueryCommand($"INSERT INTO creds (FFmpegPath) VALUES ('{ffmpegPath}')"); + } + } + else log.Debug("No"); + }); + /* + * Dude why we need STA for simple OpenFileDialog + * Update: Oh it was because Microsoft is lazy ass company that still uses COM which designed in 1990 and dont want to implement special support for older softwares and instead they force us to use STA :D + */ + thread.SetApartmentState(ApartmentState.STA); + thread.Start(); + } + + public class AMDiscordRPCTray + { + private static NotifyIcon notifyIcon = new NotifyIcon(); + private static ContextMenu contextMenu = new ContextMenu(); + public static MenuItem notifySongState = new MenuItem(); + public MenuItem s3Menu = new MenuItem(); + + public AMDiscordRPCTray() + { + notifySongState.Text = "AMDiscordRPC"; + notifySongState.Index = 0; + notifySongState.Enabled = false; + + s3Menu.Text = "S3 Credentials"; + s3Menu.Index = 1; + s3Menu.Click += new EventHandler((object sender, EventArgs e) => + { + app.Dispatcher.Invoke(() => + { + inputWindow = new InputWindow(); + inputWindow.Show(); + }); + }); + + contextMenu.MenuItems.AddRange( + new MenuItem[] + { + notifySongState, + s3Menu, + new MenuItem("Show Latest Log", (s,e) => { Process.Start("notepad", $"{Path.Combine(Directory.GetCurrentDirectory(), @"logs\latest.log")}"); }), + new MenuItem("Exit", (s, e) => { Environment.Exit(0); }) + } + ); + + notifyIcon.Icon = Properties.Resources.Logo_Black_32; + notifyIcon.ContextMenu = contextMenu; + notifyIcon.Text = "AMDiscordRPC"; + notifyIcon.Visible = true; + } + + public static void ChangeSongState(string stateText) + { + notifySongState.Text = stateText; + + notifyIcon.ContextMenu = null; + notifyIcon.ContextMenu = contextMenu; + } + } + } +} diff --git a/AMDiscordRPC/UIComponents/InputWindow.xaml b/AMDiscordRPC/UIComponents/InputWindow.xaml new file mode 100644 index 0000000..3471e8e --- /dev/null +++ b/AMDiscordRPC/UIComponents/InputWindow.xaml @@ -0,0 +1,22 @@ + + + + + + + + + + +