diff --git a/.github/workflows/build-dev.yml b/.github/workflows/build-dev.yml index 37dc5a7..da6a692 100644 --- a/.github/workflows/build-dev.yml +++ b/.github/workflows/build-dev.yml @@ -18,9 +18,9 @@ jobs: - name: Restore NuGet Packages run: nuget restore AMDiscordRPC.sln - name: Build - run: msbuild AMDiscordRPC.sln -property:Configuration=Release -property:platform="x64" + run: msbuild AMDiscordRPC.sln -property:Configuration=Debug -property:platform="x64" - name: Zip - run: powershell Compress-Archive -Path ./AMDiscordRPC/bin/x64/Release -DestinationPath Release.zip + run: powershell Compress-Archive -Path ./AMDiscordRPC/bin/x64/Debug -DestinationPath Dev-Release.zip - name: Release uses: softprops/action-gh-release@v2 with: @@ -31,4 +31,4 @@ jobs: Don't use this version if you want more stable experience. prerelease: true files: | - Release.zip \ No newline at end of file + Dev-Release.zip \ No newline at end of file diff --git a/AMDiscordRPC/AMDiscordRPC.cs b/AMDiscordRPC/AMDiscordRPC.cs index a807bb9..145ca3f 100644 --- a/AMDiscordRPC/AMDiscordRPC.cs +++ b/AMDiscordRPC/AMDiscordRPC.cs @@ -20,37 +20,44 @@ internal class AMDiscordRPC private static string oldAlbumnArtist; static void Main(string[] args) { + ConfigureLogger(); + // SetToken(); InitRegion(); CreateUI(); - ConfigureLogger(); - InitializeDiscordRPC(); - AttachToAppleMusic(); + InitDiscordRPC(); + AttachToAM(); + // ListBuckets(); 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.Equals(new WebSongResponse()) || CoverThread != 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; - } - }; + { + 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.Equals(new SQLRPCResponse()) || CoverThread != null) + { + httpRes = await GetCover( + new AppleMusicScrapedData( + x.ArtistandAlbumName.Split(new string[] { " — " }, StringSplitOptions.None)[0], + x.SongName, + x.ArtistandAlbumName.Split(new string[] { " — " }, StringSplitOptions.None)[1], + (x.isSingle) ? SecondaryType.Single : (x.IsMV) ? SecondaryType.MV : (x.ArtistandAlbumName.Contains(" - EP") ? SecondaryType.EP : SecondaryType.Album) + )); + log.Debug($"Set Cover: {((httpRes.coverURL != null) ? httpRes.coverURL : null)}"); + } + SetPresence(x, httpRes); + oldAlbumnArtist = x.ArtistandAlbumName; + } + }; CheckDatabaseIntegrity(); - InitDBCreds(); + ConfigureFromDB(); CheckFFmpeg(); InitS3(); AMEvent(); } - static void AMEvent() { using (var automation = new UIA3Automation()) @@ -88,7 +95,7 @@ static void AMEvent() { for (var i = 0; i < windows.Length; i++) { - if (windows[i].Name == "Apple Music") window = windows[i]; + if (windows[i].Name == "Apple Music" && windows[i].FindFirstChild().Name == "Non Client Input Sink Window") window = windows[i]; } } else if (windows.Length == 1) @@ -127,6 +134,7 @@ static void AMEvent() string previousSong = string.Empty; string previousArtistAlbum = string.Empty; string lastFetchedArtistAlbum = string.Empty; + string lastFetchedSong = string.Empty; AudioFormat format = AudioFormat.AAC; bool resetStatus = false; double oldValue = 0; @@ -137,14 +145,14 @@ static void AMEvent() { try { - var currentSong = listeningInfo[0].Name; + var currentSong = (listeningInfo[0].Properties.Name.IsSupported == true && listeningInfo[0].Name != "Connecting…") ? listeningInfo[0].Name : lastFetchedSong; var currentArtistAlbum = (listeningInfo[1].Properties.Name.IsSupported == true) ? listeningInfo[1].Name : lastFetchedArtistAlbum; var dashSplit = currentArtistAlbum.Split('-'); var subractThis = TimeSpan.FromSeconds(slider.AsSlider().Value + 1); if (oldValue == 0) oldValue = slider.AsSlider().Value; DateTime currentTime = DateTime.UtcNow; DateTime startTime = currentTime.Subtract(subractThis); - DateTime endTime = currentTime.AddSeconds(slider.AsSlider().Maximum).Subtract(subractThis); + DateTime endTime = startTime.AddSeconds(slider.AsSlider().Maximum); DateTime oldEndTime = DateTime.MinValue; DateTime oldStartTime = DateTime.MinValue; bool isSingle = dashSplit[dashSplit.Length - 1].Contains("Single"); @@ -172,7 +180,7 @@ static void AMEvent() oldValue = slider.AsSlider().Value; } - if (currentArtistAlbum != lastFetchedArtistAlbum) + if (currentArtistAlbum != lastFetchedArtistAlbum || currentSong != lastFetchedSong) { if (CoverThread != null) { @@ -181,15 +189,22 @@ static void AMEvent() } else log.Debug("Continue"); string idontknowwhatshouldinamethisbutitsaboutalbum = (isSingle) ? string.Join("-", dashSplit.Take(dashSplit.Length - 1).ToArray()) : string.Join("—", currentArtistAlbum.Split('—').Take(2).ToArray()); - CheckAndInsertAlbum(idontknowwhatshouldinamethisbutitsaboutalbum.Split('—')[1]); Task t = new Task(async () => { - 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)}"); + httpRes = await GetCover( + new AppleMusicScrapedData( + idontknowwhatshouldinamethisbutitsaboutalbum.Split(new string[] { " — " }, StringSplitOptions.None)[0], + currentSong, + idontknowwhatshouldinamethisbutitsaboutalbum.Split(new string[] { " — " }, StringSplitOptions.None)[1], + (isSingle) ? SecondaryType.Single : (idontknowwhatshouldinamethisbutitsaboutalbum.Split('—').Length <= 1) ? SecondaryType.MV : (idontknowwhatshouldinamethisbutitsaboutalbum.Contains(" - EP") ? SecondaryType.EP : SecondaryType.Album) + ) + ); + log.Debug($"Set Cover: {((httpRes.coverURL != null) ? httpRes.coverURL : null)}"); }); CoverThread = t; t.Start(); lastFetchedArtistAlbum = currentArtistAlbum; + lastFetchedSong = currentSong; } if (slider.AsSlider().Maximum != 0 && slider.AsSlider().Value != 0 && endTime != startTime && (currentSong != previousSong || currentArtistAlbum != previousArtistAlbum) && oldEndTime != endTime && oldStartTime != startTime) @@ -214,11 +229,9 @@ static void AMEvent() } 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, format)); + 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, isSingle)); } if (playButton?.Name != null && (localizedPlay != null && localizedPlay == playButton?.Name || localizedStop != null && localizedStop != playButton?.Name)) @@ -249,20 +262,20 @@ static void AMEvent() client.ClearPresence(); while (!AMAttached) { - AttachToAppleMusic(); + AttachToAM(); Thread.Sleep(1000); } AMEvent(); } Thread.Sleep(20); } - if (!AMAttached & AppleMusicProc.HasExited != true) + if (!AMAttached && AppleMusicProc.HasExited != true) { log.Info("Something happened which needs to reattach"); client.ClearPresence(); while (!AMAttached) { - AttachToAppleMusic(); + AttachToAM(); Thread.Sleep(1000); } AMEvent(); @@ -272,7 +285,7 @@ static void AMEvent() { while (!AMAttached) { - AttachToAppleMusic(); + AttachToAM(); Thread.Sleep(1000); } AMEvent(); diff --git a/AMDiscordRPC/AMDiscordRPC.csproj b/AMDiscordRPC/AMDiscordRPC.csproj index 4d4c56a..028c1cc 100644 --- a/AMDiscordRPC/AMDiscordRPC.csproj +++ b/AMDiscordRPC/AMDiscordRPC.csproj @@ -70,7 +70,7 @@ latest prompt true - 0 + 1 bin\x64\Release\ @@ -100,7 +100,11 @@ Resources\Logo Black.ico + + false + + ..\packages\AngleSharp.1.3.0\lib\net472\AngleSharp.dll @@ -110,8 +114,8 @@ ..\packages\AWSSDK.S3.4.0.6.2\lib\net472\AWSSDK.S3.dll - - ..\packages\DiscordRichPresence.1.5.0.51\lib\net45\DiscordRPC.dll + + ..\packages\DiscordRichPresence.1.6.1.70\lib\net45\DiscordRPC.dll ..\packages\EntityFramework.6.5.1\lib\net45\EntityFramework.dll @@ -138,6 +142,15 @@ ..\packages\Microsoft.Bcl.AsyncInterfaces.9.0.7\lib\net462\Microsoft.Bcl.AsyncInterfaces.dll + + ..\packages\Microsoft.Web.WebView2.1.0.3595.46\lib\net462\Microsoft.Web.WebView2.Core.dll + + + ..\packages\Microsoft.Web.WebView2.1.0.3595.46\lib\net462\Microsoft.Web.WebView2.WinForms.dll + + + ..\packages\Microsoft.Web.WebView2.1.0.3595.46\lib\net462\Microsoft.Web.WebView2.Wpf.dll + ..\packages\Microsoft.Win32.Registry.5.0.0\lib\net461\Microsoft.Win32.Registry.dll @@ -174,6 +187,7 @@ ..\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 @@ -189,6 +203,7 @@ ..\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 @@ -233,6 +248,7 @@ + @@ -250,6 +266,9 @@ InputWindow.xaml + + OptionsWindow.xaml + @@ -270,7 +289,7 @@ - ResXFileCodeGenerator + PublicResXFileCodeGenerator Resources.Designer.cs @@ -279,6 +298,10 @@ Designer MSBuild:Compile + + Designer + MSBuild:Compile + @@ -310,8 +333,10 @@ + + \ No newline at end of file diff --git a/AMDiscordRPC/App.config b/AMDiscordRPC/App.config index 2012abb..1da3d7b 100644 --- a/AMDiscordRPC/App.config +++ b/AMDiscordRPC/App.config @@ -49,6 +49,10 @@ + + + + @@ -65,4 +69,4 @@ - \ No newline at end of file + diff --git a/AMDiscordRPC/AppleMusic.cs b/AMDiscordRPC/AppleMusic.cs index 4eb1dd3..519fa23 100644 --- a/AMDiscordRPC/AppleMusic.cs +++ b/AMDiscordRPC/AppleMusic.cs @@ -6,13 +6,13 @@ namespace AMDiscordRPC { internal class AppleMusic { - public static void AttachToAppleMusic() + public static void AttachToAM() { try { AppleMusicProc = Application.Attach("AppleMusic.exe"); AMAttached = true; - log.Info($"Attached to Process Id: {AppleMusicProc.ProcessId}"); + log.Info($"Attached to PID: {AppleMusicProc.ProcessId}"); } catch (Exception e) { diff --git a/AMDiscordRPC/Cloudflare.cs b/AMDiscordRPC/Cloudflare.cs new file mode 100644 index 0000000..5f4098c --- /dev/null +++ b/AMDiscordRPC/Cloudflare.cs @@ -0,0 +1,39 @@ +using Newtonsoft.Json; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using static AMDiscordRPC.Globals; + +namespace AMDiscordRPC +{ + internal class Cloudflare : Globals.CloudflareTypes + { + public static HttpClient CfHttpClient = new HttpClient(); + + public static async void SetToken() + { + CfHttpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {CfAccountCredentials.api_token}"); + await VerifyToken(); + } + + private static async Task VerifyToken() + { + var response = await CfHttpClient.GetAsync($"{endpointV4}/accounts/{CfAccountCredentials.account_id}/tokens/verify"); + Response deserialized = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); + if (deserialized.result.status == "active") + log.Info("Token valid and active"); + else + log.Error("Token is not valid."); + } + + public static async Task ListBuckets() + { + CfHttpClient.DefaultRequestHeaders.Add("cf-r2-jurisdiction", "eu"); + var response = await CfHttpClient.GetAsync($"{endpointV4}/accounts/{CfAccountCredentials.account_id}/r2/buckets"); + Response deserialized = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); + if (deserialized.success) + deserialized.result = JsonConvert.DeserializeObject>(deserialized.result.buckets.ToString()); + return deserialized; + } + } +} diff --git a/AMDiscordRPC/Covers.cs b/AMDiscordRPC/Covers.cs index 5c69e07..a29be24 100644 --- a/AMDiscordRPC/Covers.cs +++ b/AMDiscordRPC/Covers.cs @@ -4,6 +4,7 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using AngleSharp.Dom; using static AMDiscordRPC.Database; using static AMDiscordRPC.Globals; using static AMDiscordRPC.Playlist; @@ -14,90 +15,120 @@ public class Covers { public static Task CoverThread; - private static async Task AsyncFetchiTunes(string album, string searchStr) + private static async Task AsyncFetchiTunes(AppleMusicScrapedData data) { 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 - HttpResponseMessage 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={data.GetSearchString()}&limit=1&entity=song&country={AMRegion}"); if (iTunesReq.IsSuccessStatusCode) { dynamic imageRes = JObject.Parse(await iTunesReq.Content.ReadAsStringAsync()); if (imageRes["resultCount"] != 0) { - WebSongResponse webRes = new WebSongResponse - ( - imageRes["results"][0]["artworkUrl100"].ToString(), - imageRes["results"][0]["trackViewUrl"].ToString(), - imageRes["results"][0]["collectionName"].ToString() + SQLSongResponse songData = new SQLSongResponse( + new SQLCoverData(0, imageRes["results"][0]["artworkUrl100"].ToString(), null, null, null), + new SQLAlbumData(0, data.AlbumName, imageRes["results"][0]["trackViewUrl"].ToString().Split(new string[] { "?i=" }, StringSplitOptions.None)[0], data.Type == SecondaryType.Single, 0, 0), + new SQLArtistData(0, data.ArtistName, imageRes["results"][0]["artistViewUrl"].ToString().Split(new string[] { "?uo=" }, StringSplitOptions.None)[0], await AsyncArtistProfileFetch(imageRes["results"][0]["artistViewUrl"].ToString().Split(new string[] { "?uo=" }, StringSplitOptions.None)[0])), + new SQLSongData(data.SongName, imageRes["results"][0]["trackViewUrl"].ToString().Split(new string[] { "&uo=" }, StringSplitOptions.None)[0], 0, 0) ); - Database.UpdateAlbum(new Database.SQLCoverResponse(album, webRes.artworkURL, webRes.trackURL)); + InsertNew(songData); CoverThread = null; - return webRes; + return songData; } else { log.Warn("iTunes no image found"); CoverThread = null; - return new WebSongResponse(); + return null; } } else { log.Warn("iTunes request failed"); CoverThread = null; - return new WebSongResponse(); + return null; } } catch (Exception e) { log.Error($"iTunes Exception {e.Message}"); CoverThread = null; - return new WebSongResponse(); + return null; } } - public static async Task AsyncAMFetch(string album, string searchStr) + public static async Task AsyncArtistProfileFetch(string url) { try { - HttpResponseMessage AMRequest = await hclient.GetAsync($"https://music.apple.com/{AMRegion.ToLower()}/search?term={searchStr}"); + HttpResponseMessage AMRequest = await hclient.GetAsync(url); 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") + if (document.QuerySelectorAll("div.artwork-component > picture > img")[1].GetAttribute("alt") == "") return null; + return document.QuerySelectorAll("div.artwork-component > picture > source")[1].GetAttribute("srcset").Split(',')[1].Split(' ')[0]; + } + else + { + log.Error($"Apple Music Artist request failed returned: {AMRequest.StatusCode}"); + } + } + catch (Exception e) + { + log.Error($"Apple Music Artist Request failed. {e}"); + } + return null; + } + + public static async Task AsyncAMFetch(AppleMusicScrapedData data) + { + log.Debug($"https://music.apple.com/{AMRegion.ToLower()}/search?term={data.GetSearchString()}"); + try + { + HttpResponseMessage AMRequest = await hclient.GetAsync($"https://music.apple.com/{AMRegion.ToLower()}/search?term={data.GetSearchString()}"); + if (AMRequest.IsSuccessStatusCode) + { + string DOMasAString = await AMRequest.Content.ReadAsStringAsync(); + IElement document = parser.ParseDocument(DOMasAString).DocumentElement; + string[] artistData = document.GetArtist(data); + SQLSongResponse songData = new SQLSongResponse( + new SQLCoverData(0, document.GetCover(data), null, null, null), + new SQLAlbumData(0, data.AlbumName, document.GetAlbum(data), data.Type == SecondaryType.Single, 0, 0), + new SQLArtistData(0, data.ArtistName, artistData[0], artistData[1]), + new SQLSongData(data.SongName, document.GetSong(data), 0, 0) ); + + InsertNew(songData); CoverThread = null; - Database.UpdateAlbum(new Database.SQLCoverResponse(album, webRes.artworkURL, webRes.trackURL)); - return webRes; + return songData; } else { log.Error($"Apple Music request failed returned: {AMRequest.StatusCode}"); - return await AsyncFetchiTunes(album, searchStr); + return await AsyncFetchiTunes(data); } } catch (Exception e) { log.Error($"Apple Music Request failed. {e}"); - return await AsyncFetchiTunes(album, searchStr); + return await AsyncFetchiTunes(data); } } - public static async Task CheckAnimatedCover(string album, string url, CancellationToken ct) + public static async Task CheckAnimatedCover(string albumUrl, CancellationToken ct) { try { - var appleMusicDom = await hclient.GetAsync(url); + var appleMusicDom = await hclient.GetAsync(albumUrl); + log.Debug($"Animated Cover Request: {albumUrl}"); if (appleMusicDom.IsSuccessStatusCode) { string DOMasAString = await appleMusicDom.Content.ReadAsStringAsync(); IHtmlDocument document = parser.ParseDocument(DOMasAString); - ConvertM3U8(album, document.DocumentElement.QuerySelector("div.video-artwork__container").InnerHtml.Split(new string[] { "src=\"" }, StringSplitOptions.None)[1].Split('"')[0], ct); + ConvertM3U8(albumUrl, document.DocumentElement.QuerySelector("div.video-artwork__container").InnerHtml.Split(new string[] { "src=\"" }, StringSplitOptions.None)[1].Split('"')[0], ct); } else { @@ -109,42 +140,52 @@ 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)); + UpdateAlbumCover(albumUrl, new SQLCoverData(0, null, false, null, null)); } } - public static async Task GetCover(string album, string searchStr) + public static async Task GetCover(AppleMusicScrapedData data) { try { - log.Debug($"https://music.apple.com/us/search?term={searchStr}"); - SQLCoverResponse cover = GetAlbumDataFromSQL(album); + SQLRPCResponse cover = GetSongFromDB(data.SongName, data.AlbumName, data.ArtistName); if (cover != null) { - 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; + return cover; } else { - return await AsyncAMFetch(album, searchStr); + SQLSongResponse songData = await AsyncAMFetch(data); + if (songData == null) return new SQLRPCResponse(); + return new SQLRPCResponse + { + coverURL = songData.cover.staticCoverURL, + artistRedirURL = songData.artist.artistRedirURL, + artistProfileSource = songData.artist.artistProfileSource, + albumURL = songData.album.albumURL, + songURL = songData.song.songURL + }; } } catch (Exception ex) { - return await AsyncAMFetch(album, searchStr); + SQLSongResponse songData = await AsyncAMFetch(data); + if (songData == null) return new SQLRPCResponse(); + return new SQLRPCResponse + { + coverURL = songData.cover.staticCoverURL, + artistRedirURL = songData.artist.artistRedirURL, + artistProfileSource = songData.artist.artistProfileSource, + albumURL = songData.album.albumURL, + songURL = songData.song.songURL + }; } } - - /* 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. + * probably not gonna add it since prefered source is now Apple Music and we have iTunes as a backup. public static async Task FetchImage(string ArtistAndAlbum, string lastFMAPIKey) { diff --git a/AMDiscordRPC/Database.cs b/AMDiscordRPC/Database.cs index 2594fb2..5b91868 100644 --- a/AMDiscordRPC/Database.cs +++ b/AMDiscordRPC/Database.cs @@ -6,14 +6,19 @@ namespace AMDiscordRPC { - internal class Database + public class Database { private static SQLiteConnection sqlite; 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, 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" } + {"coverTable", "album TEXT PRIMARY KEY NOT NULL, source TEXT, redirURL TEXT DEFAULT 'https://music.apple.com/home', artistRedirURL TEXT DEFAULT 'https://music.apple.com/home', artistSource TEXT, animated BOOLEAN CHECK (animated IN (0,1)) DEFAULT NULL, streamURL TEXT, animatedURL TEXT" }, // will be replaced + {"coverTableNew", "coverID INTEGER PRIMARY KEY AUTOINCREMENT, staticCoverURL TEXT NOT NULL UNIQUE, isAnimated BOOLEAN CHECK (isAnimated IN (0,1)) DEFAULT NULL, streamURL TEXT, animatedURL TEXT"}, + {"artistTable", "artistID INTEGER PRIMARY KEY AUTOINCREMENT, artistName TEXT NOT NULL, artistRedirURL TEXT DEFAULT 'https://music.apple.com/home', artistProfileSource TEXT"}, + {"albumTable", "albumID INTEGER PRIMARY KEY AUTOINCREMENT, albumName TEXT NOT NULL, albumURL TEXT UNIQUE, isSingle BOOLEAN CHECK (isSingle IN (0,1)), coverID INTEGER, artistID INTEGER, FOREIGN KEY (coverID) REFERENCES coverTableNew(coverID), FOREIGN KEY (artistID) REFERENCES artistTable(artistID)"}, + {"songTable", "songTitle TEXT, songURL TEXT UNIQUE, albumID INTEGER, artistID INTEGER, FOREIGN KEY (albumID) REFERENCES albumTable(albumID), FOREIGN KEY (artistID) REFERENCES artistTable(artistID)"}, + {"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, LastFMToken TEXT" }, + {"logs", "timestamp INTEGER, type TEXT, occuredAt TEXT, message TEXT" }, + {"clientSettings", "smallImage INTEGER"} }; private static void InitDatabase() @@ -38,7 +43,7 @@ public static void CheckDatabaseIntegrity() { try { - //CheckForeignKeys(); we don't have use case for relationships rn so no need to waste resources on this check + CheckForeignKeys(); CheckTables(); CheckColumns(); } @@ -51,9 +56,41 @@ public static void CheckDatabaseIntegrity() private static void CreateDatabase() { - foreach (var item in sqlMap) + using (var transaction = sqlite.BeginTransaction()) { - ExecuteNonQueryCommand($"CREATE TABLE IF NOT EXISTS {item.Key}({item.Value})"); + try + { + foreach (var item in sqlMap) + { + ExecuteNonQueryCommand($"CREATE TABLE IF NOT EXISTS {item.Key}({item.Value})"); + } + transaction.Commit(); + } + catch (Exception ex) + { + log.Error($"An error occured while creating database: {ex}"); + transaction.Rollback(); + } + } + CreateIndexes(); + } + + private static void CreateIndexes() + { + using (var transaction = sqlite.BeginTransaction()) + { + try + { + ExecuteNonQueryCommand($"CREATE INDEX IF NOT EXISTS idx_albumName ON albumTable(albumName)"); + ExecuteNonQueryCommand($"CREATE INDEX IF NOT EXISTS idx_songTitle ON songTable(songTitle)"); + ExecuteNonQueryCommand($"CREATE INDEX IF NOT EXISTS idx_artistName ON artistTable(artistName)"); + transaction.Commit(); + } + catch (Exception ex) + { + log.Error($"An error occured while creating indexes: {ex}"); + transaction.Rollback(); + } } } @@ -92,34 +129,129 @@ private static void CheckTables() { ExecuteNonQueryCommand($"CREATE TABLE IF NOT EXISTS {item}({sqlMap[item]})"); } + CreateIndexes(); } else log.Debug("No missing table found."); } - public static void UpdateAlbum(SQLCoverResponse data) + public static void UpdateAlbumCover(string albumURL, SQLCoverData data) + { + int rowsAffected = ExecuteNonQueryCommand($@"UPDATE coverTableNew SET ({string.Join(", ", data.GetNotNullKeys())}) = ({string.Join(", ", data.GetNotNullValues())}) FROM albumTable WHERE coverTableNew.coverID = albumTable.coverID AND albumURL = @albumURL;", new[] { new SQLiteParameter("@albumURL", albumURL) }); + if (rowsAffected == 0) + { + log.Warn($"This album is not present in database. Skipping {albumURL}"); + } + } + + public static int InsertAlbumNew(SQLAlbumData data) + { + using (SQLiteDataReader reader = ExecuteReaderCommand($"SELECT albumID FROM albumTable WHERE albumURL = @albumURL", new[] { new SQLiteParameter("@albumURL", data.albumURL) })) + { + if (!reader.HasRows) + { + return Convert.ToInt32(ExecuteScalarCommand($@"INSERT INTO albumTable(albumName, albumURL, isSingle, coverID, artistID) VALUES (@albumName, @albumURL, @isSingle, @coverID, @artistID); SELECT last_insert_rowid();", new[] { new SQLiteParameter("@albumName", data.albumName), new SQLiteParameter("@albumURL", data.albumURL), new SQLiteParameter("@isSingle", data.isSingle), new SQLiteParameter("@coverID", data.coverID), new SQLiteParameter("@artistID", data.artistID) })); + } + else + { + reader.Read(); + return reader.GetInt32(0); + } + } + } + + public static int InsertArtist(SQLArtistData data) + { + using (SQLiteDataReader reader = ExecuteReaderCommand($"SELECT artistID FROM artistTable WHERE artistRedirUrl = @artistRedirURL", new[] { new SQLiteParameter("@artistRedirURL", data.artistRedirURL) })) + { + if (!reader.HasRows) + { + return Convert.ToInt32(ExecuteScalarCommand($@"INSERT INTO artistTable(artistName, artistRedirURL, artistProfileSource) VALUES (@artistName, @artistRedirURL, @artistProfileSource); SELECT last_insert_rowid();", new[] { new SQLiteParameter("@artistName", data.artistName), new SQLiteParameter("@artistRedirURL", data.artistRedirURL), new SQLiteParameter("@artistProfileSource", data.artistProfileSource) })); + } + else + { + reader.Read(); + return reader.GetInt32(0); + } + } + } + + public static void InsertSong(SQLSongData data) { - ExecuteNonQueryCommand($"UPDATE coverTable SET ({string.Join(", ", data.GetNotNullKeys())}) = ({string.Join(", ", data.GetNotNullValues())}) WHERE album = '{data.album}'"); + if (ExecuteScalarCommand($"SELECT songTitle FROM songTable WHERE songURL = @songURL AND albumID = @albumID AND artistID = @artistID", new[] { new SQLiteParameter("@songURL", data.songURL), new SQLiteParameter("@albumID", data.albumID), new SQLiteParameter("@artistID", data.artistID) }) == null) + ExecuteNonQueryCommand($@"INSERT INTO songTable(songTitle, songURL, albumID, artistID) VALUES (@songTitle, @songURL, @albumID, @artistID)", new[] { new SQLiteParameter("@songTitle", data.songTitle), new SQLiteParameter("@songURL", data.songURL), new SQLiteParameter("@albumID", data.albumID), new SQLiteParameter("@artistID", data.artistID) }); } - public static void CheckAndInsertAlbum(string album) + public static int InsertCover(SQLCoverData data) { - if (ExecuteScalarCommand($"SELECT album from coverTable WHERE album = '{album}'") == null) - ExecuteNonQueryCommand($"INSERT INTO coverTable(album) VALUES ('{album}')"); + using (SQLiteDataReader reader = ExecuteReaderCommand($"SELECT coverID FROM coverTableNew WHERE staticCoverURL = @staticCoverURL", new[] { new SQLiteParameter("@staticCoverURL", data.staticCoverURL) })) + { + if (!reader.HasRows) + { + return Convert.ToInt32(ExecuteScalarCommand($@"INSERT INTO coverTableNew(staticCoverURL, isAnimated, streamURL, animatedURL) VALUES (@staticCoverURL, @isAnimated, @streamURL, @animatedURL); SELECT last_insert_rowid();", new[] { new SQLiteParameter("@staticCoverURL", data.staticCoverURL), new SQLiteParameter("@isAnimated", data.isAnimated), new SQLiteParameter("@streamURL", data.streamURL), new SQLiteParameter("@animatedURL", data.animatedURL) })); + } + else + { + reader.Read(); + return reader.GetInt32(0); + } + } } - public static SQLCoverResponse GetAlbumDataFromSQL(string album) + public static void InsertNew(SQLSongResponse SQLSongResponse) { - using (SQLiteDataReader reader = ExecuteReaderCommand($"SELECT * FROM coverTable WHERE album = '{album}' LIMIT 1")) + using (var transaction = sqlite.BeginTransaction()) { + try + { + int coverID = InsertCover(SQLSongResponse.cover); + int artistID = InsertArtist(SQLSongResponse.artist); + SQLSongResponse.album.coverID = coverID; + SQLSongResponse.album.artistID = artistID; + int albumID = InsertAlbumNew(SQLSongResponse.album); + SQLSongResponse.song.albumID = albumID; + SQLSongResponse.song.artistID = artistID; + InsertSong(SQLSongResponse.song); + transaction.Commit(); + } + catch (Exception ex) + { + log.Error($"An error occured while inserting data: {ex}"); + transaction.Rollback(); + } + } + } + + public static SQLRPCResponse GetSongFromDB(string song, string album, string artist) + { + string cmd = @" + SELECT + IIF(coverTableNew.isAnimated = 1, coverTableNew.animatedURL, coverTableNew.staticCoverURL) as coverURL, + artistTable.artistRedirURL as artistRedirURL, + artistTable.artistProfileSource as artistProfileSource, + albumTable.albumURL, + songTable.songURL + FROM songTable + INNER JOIN albumTable on albumTable.albumID = songTable.albumID + INNER JOIN artistTable on artistTable.artistID = albumTable.artistID + INNER JOIN coverTableNew on coverTableNew.coverID = albumTable.coverID + WHERE + artistTable.artistName = @artist + AND songTable.songTitle = @song + AND albumTable.albumName = @album + LIMIT 1; + "; + using (SQLiteDataReader reader = ExecuteReaderCommand(cmd, new[] { new SQLiteParameter("@album", album), new SQLiteParameter("@song", song), new SQLiteParameter("@artist", artist) })) + { + if (!reader.HasRows) return null; while (reader.Read()) { - return new SQLCoverResponse( - reader.GetString(0), - ((!reader.IsDBNull(1)) ? reader.GetString(1) : null), - reader.GetString(2), - ((!reader.IsDBNull(3)) ? reader.GetBoolean(3) : null), - ((!reader.IsDBNull(4)) ? reader.GetString(4) : null), - ((!reader.IsDBNull(5)) ? reader.GetString(5) : null)); + return new SQLRPCResponse( + reader.IsDBNull(0) ? null : reader.GetString(0), + reader.IsDBNull(1) ? null : reader.GetString(1), + reader.IsDBNull(2) ? null : reader.GetString(2), + reader.IsDBNull(3) ? null : reader.GetString(3), + reader.IsDBNull(4) ? null : reader.GetString(4) + ); } } return null; @@ -129,12 +261,21 @@ private static void CheckColumns() { foreach (var table in sqlMap.Keys) { - SQLiteDataReader data = ExecuteReaderCommand($"PRAGMA table_info({table})"); + SQLiteDataReader data = ExecuteReaderCommand($"SELECT * FROM sqlite_master"); Dictionary tableData = new Dictionary(); while (data.Read()) { - tableData.Add(data.GetString(1), new ColumnInfo(data.GetString(2), data.GetBoolean(3), (!data.IsDBNull(4)) ? data.GetString(4) : null, data.GetBoolean(5))); + if (data.GetString(0) == "table" && data.GetString(2) == table) + { + string sqlStr = string.Join("(", data.GetString(4).Split(new[] { "CREATE TABLE " }, StringSplitOptions.None)[1].Split('(').Skip(1)).TrimEnd(1); + var temp = ConvertSQLStringToColumnInfo(sqlStr); + foreach (var keyValuePair in temp) + { + //log.Debug($"{keyValuePair.Key}, autoIncrement: {keyValuePair.Value.isAutoIncrementing}, defaultValue: {keyValuePair.Value.defaultValue}, foreignKey: [Key: {keyValuePair.Value.foreignKey?.key}, refColumn: {keyValuePair.Value.foreignKey?.refColumn}, refTable: {keyValuePair.Value.foreignKey?.refTable}], nullCheck: {keyValuePair.Value.nullCheck}, primaryKey: {keyValuePair.Value.primaryKey}, type: {keyValuePair.Value.type}"); + tableData.Add(keyValuePair.Key, keyValuePair.Value); + } + } } foreach (var item in ConvertSQLStringToColumnInfo(sqlMap[table])) @@ -144,7 +285,7 @@ private static void CheckColumns() if (!item.Value.Equals(column) && column != null) { log.Debug($"Corrupted/Outdated column:{SQLInfo.Split(' ')[0]} found."); - if (!item.Value.primaryKey && ((item.Value.nullCheck && item.Value.defaultValue != null) || !item.Value.nullCheck)) + if (!item.Value.primaryKey && ((item.Value.nullCheck && item.Value.defaultValue != null) || !item.Value.nullCheck) && item.Value.foreignKey == null) { ExecuteNonQueryCommand($"ALTER TABLE {table} DROP COLUMN {SQLInfo.Split(' ')[0]}"); ExecuteNonQueryCommand($"ALTER TABLE {table} ADD COLUMN {SQLInfo}"); @@ -155,7 +296,7 @@ private static void CheckColumns() // Recovery functionality will be added next release. } } - else if (column == null) + else if (column == null && !item.Value.primaryKey) { ExecuteNonQueryCommand($"ALTER TABLE {table} ADD COLUMN {SQLInfo}"); } @@ -170,21 +311,25 @@ private static Dictionary ConvertSQLStringToColumnInfo(strin foreach (var column in columns) { string[] splitStr = column.Split(' '); + if (splitStr[0] == "FOREIGN") continue; columnsMap.Add(splitStr[0], new ColumnInfo( splitStr[1], column.Contains("NOT NULL"), (column.Contains("DEFAULT")) ? column.Split(new[] { "DEFAULT " }, StringSplitOptions.None)[1] : null, //This is not a proper way to do this but it works for now (DEFAULT value must be on the last section of the SQL Command) - column.Contains("PRIMARY KEY") + column.Contains("PRIMARY KEY"), + column.Contains("AUTOINCREMENT"), + (sqlStr.Contains($"FOREIGN KEY ({splitStr[0]})")) ? new ForeignKey(splitStr[0], columns.Where(a => a.Contains($"FOREIGN KEY ({splitStr[0]})")).First().Split(new[] { "REFERENCES " }, StringSplitOptions.None)[1].Split('(')[0], columns.Where(a => a.Contains($"FOREIGN KEY ({splitStr[0]})")).First().Split(new[] { "REFERENCES " }, StringSplitOptions.None)[1].Split('(')[1].Split(')')[0]) : null )); } return columnsMap; } - public static object ExecuteScalarCommand(string command) + public static object ExecuteScalarCommand(string command, SQLiteParameter[] parameters = null) { try { - SQLiteCommand cmd = new SQLiteCommand($@"{command}", sqlite); + SQLiteCommand cmd = new SQLiteCommand(command, sqlite); + if (parameters != null) cmd.Parameters.AddRange(parameters); return cmd.ExecuteScalar(); } catch (Exception ex) @@ -194,11 +339,12 @@ public static object ExecuteScalarCommand(string command) } } - public static SQLiteDataReader ExecuteReaderCommand(string command) + public static SQLiteDataReader ExecuteReaderCommand(string command, SQLiteParameter[] parameters = null) { try { - SQLiteCommand cmd = new SQLiteCommand($@"{command}", sqlite); + SQLiteCommand cmd = new SQLiteCommand(command, sqlite); + if (parameters != null) cmd.Parameters.AddRange(parameters); return cmd.ExecuteReader(); } catch (Exception ex) @@ -208,11 +354,12 @@ public static SQLiteDataReader ExecuteReaderCommand(string command) } } - public static int ExecuteNonQueryCommand(string command) + public static int ExecuteNonQueryCommand(string command, SQLiteParameter[] parameters = null) { try { - SQLiteCommand cmd = new SQLiteCommand($@"{command}", sqlite); + SQLiteCommand cmd = new SQLiteCommand(command, sqlite); + if (parameters != null) cmd.Parameters.AddRange(parameters); return cmd.ExecuteNonQuery(); } catch (Exception ex) @@ -228,13 +375,17 @@ private class ColumnInfo public bool nullCheck { get; set; } public string defaultValue { get; set; } public bool primaryKey { get; set; } + public bool isAutoIncrementing { get; set; } + public ForeignKey foreignKey { get; set; } - public ColumnInfo(string type, bool nullCheck, string defaultValue, bool primaryKey) + public ColumnInfo(string type, bool nullCheck, string defaultValue, bool primaryKey, bool isAutoIncrementing, ForeignKey foreignKey = null) { this.type = type; this.nullCheck = nullCheck; this.defaultValue = defaultValue; this.primaryKey = primaryKey; + this.isAutoIncrementing = isAutoIncrementing; + this.foreignKey = foreignKey; } public override bool Equals(object obj) @@ -243,39 +394,142 @@ public override bool Equals(object obj) type == other.type && nullCheck == other.nullCheck && defaultValue == other.defaultValue && - primaryKey == other.primaryKey; + primaryKey == other.primaryKey && + isAutoIncrementing == other.isAutoIncrementing && + foreignKey == other.foreignKey; } } - public class SQLCoverResponse + private class ForeignKey { - public string album { get; set; } - public string source { get; set; } - public string redirURL { get; set; } - public bool? animated { get; set; } - public string streamURL { get; set; } - public string animatedURL { get; set; } + public string key { get; set; } + public string refTable { get; set; } + public string refColumn { get; set; } - public SQLCoverResponse(string album = null, string source = null, string redirURL = null, bool? animated = null, string streamURL = null, string animatedURL = null) + public ForeignKey(string key, string refTable, string refColumn) { + this.key = key; + this.refTable = refTable; + this.refColumn = refColumn; + } + } + + public class SQLSongResponse + { + public SQLCoverData? cover; + public SQLAlbumData? album; + public SQLArtistData? artist; + public SQLSongData? song; + + public SQLSongResponse(SQLCoverData cover, SQLAlbumData album, SQLArtistData artist, SQLSongData song) + { + this.cover = cover; this.album = album; - this.source = source; - this.redirURL = redirURL; - this.animated = animated; + this.artist = artist; + this.song = song; + } + } + public class SQLRPCResponse + { + public string? coverURL { get; set; } + public string? artistRedirURL { get; set; } + public string? artistProfileSource { get; set; } + public string? albumURL { get; set; } + public string? songURL { get; set; } + + public SQLRPCResponse(string? coverURL = null, string? artistRedirURL = null, string? artistProfileSource = null, string? albumURL = null, string? songURL = null) + { + this.coverURL = coverURL; + this.artistRedirURL = artistRedirURL; + this.artistProfileSource = artistProfileSource; + this.albumURL = albumURL; + this.songURL = songURL; + } + } + + public class SQLCoverData + { + public int id { get; set; } + public string staticCoverURL { get; set; } + public bool? isAnimated { get; set; } + public string? streamURL { get; set; } + public string? animatedURL { get; set; } + + public SQLCoverData(int id, string staticCoverURL, bool? isAnimated, string? streamURL, string? animatedURL) + { + this.id = id; + this.staticCoverURL = staticCoverURL; + this.isAnimated = isAnimated; this.streamURL = streamURL; this.animatedURL = animatedURL; } - public List GetNotNullKeys() { - return GetType().GetProperties().Where(s => s.GetValue(this) != null && s.GetValue(this) != this.album).Select(p => p.Name).ToList(); + return GetType().GetProperties() + .Where(s => s.GetValue(this) != null && !s.GetValue(this).Equals(id) && s.GetValue(this) != staticCoverURL) + .Select(p => p.Name) + .ToList(); } 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(); + return GetType().GetProperties() + .Where(s => s.GetValue(this) != null && !s.GetValue(this).Equals(id) && s.GetValue(this) != staticCoverURL) + .Select(p => (p.PropertyType == typeof(string)) ? $"'{p.GetValue(this)}'" : p.GetValue(this)) + .ToList(); } + } + + public class SQLArtistData + { + public int id; + public string artistName; + public string artistRedirURL; + public string? artistProfileSource; + public SQLArtistData(int id, string artistName, string artistRedirURL, string? artistProfileSource) + { + this.id = id; + this.artistName = artistName; + this.artistRedirURL = artistRedirURL; + this.artistProfileSource = artistProfileSource; + } + } + + public class SQLAlbumData + { + public int id; + public string albumName; + public string albumURL; + public bool isSingle; + public int? coverID; // Foreign Key + public int? artistID; // Foreign Key + + public SQLAlbumData(int id, string albumName, string albumURL, bool isSingle, int coverID, int artistID) + { + this.id = id; + this.albumName = albumName; + this.albumURL = albumURL; + this.isSingle = isSingle; + this.coverID = coverID; + this.artistID = artistID; + } + } + + public class SQLSongData + { + public string songTitle; + public string songURL; + public int? albumID; // Foreign Key + public int? artistID; // Foreign Key + + public SQLSongData(string songTitle, string songURL, int albumID, int artistID) + { + this.songTitle = songTitle; + this.songURL = songURL; + this.albumID = albumID; + this.artistID = artistID; + } } } } diff --git a/AMDiscordRPC/Discord.cs b/AMDiscordRPC/Discord.cs index 124d731..c68bf54 100644 --- a/AMDiscordRPC/Discord.cs +++ b/AMDiscordRPC/Discord.cs @@ -2,8 +2,8 @@ using System; using System.Threading; using System.Threading.Tasks; -using System.Web; using static AMDiscordRPC.Covers; +using static AMDiscordRPC.Database; using static AMDiscordRPC.Globals; namespace AMDiscordRPC @@ -13,10 +13,11 @@ public class Discord private static Thread thread = null; public static CancellationTokenSource animatedCoverCts; - public static void InitializeDiscordRPC() + public static void InitDiscordRPC() { client = new DiscordRpcClient("1308911584164319282"); client.Initialize(); + log.Debug("Discord RPC initialized."); } public static void ChangeTimestamps(DateTime start = new DateTime(), DateTime end = new DateTime()) @@ -50,12 +51,14 @@ public static void SetPresence(SongData x) private static async Task AsyncSetButton(SongData x) { + /* 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.trackURL != null) ? resp.trackURL.Replace("https://", "music://") : "music://music.apple.com/home"} }; client.SetPresence(oldData); + */ thread = null; } @@ -66,7 +69,7 @@ public static async Task SetCover(string coverURL) animatedCoverCts = null; } - public static void SetPresence(SongData x, WebSongResponse resp) + public static void SetPresence(SongData x, SQLRPCResponse resp) { log.Debug($"Timestamps {x.StartTime}/{x.EndTime}"); if (thread != null) thread.Abort(); @@ -79,18 +82,22 @@ public static void SetPresence(SongData x, WebSongResponse resp) { Type = ActivityType.Listening, Details = ConvertToValidString(x.SongName), + DetailsUrl = resp.songURL, + StateUrl = resp.artistRedirURL, StatusDisplay = StatusDisplayType.State, State = (x.IsMV) ? x.ArtistandAlbumName : ConvertToValidString(x.ArtistandAlbumName.Split('—')[0]), Assets = new Assets() { - 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, + LargeImageKey = (resp.coverURL != null) ? resp.coverURL : "", + LargeImageUrl = resp.albumURL, + LargeImageText = (x.IsMV && x.SongName != null) ? x.SongName : ConvertToValidString(x.ArtistandAlbumName.Split('—')[1]), + SmallImageKey = (SelectedSmallImage == SmallImage.Artist) ? resp.artistProfileSource : (x.format == AudioFormat.Lossless) ? "lossless" : (x.format == AudioFormat.Dolby_Atmos || x.format == AudioFormat.Dolby_Audio) ? "dolbysimplified" : null, + SmallImageText = (SelectedSmallImage == SmallImage.Artist) ? ConvertToValidString(x.ArtistandAlbumName.Split('—')[0]) : (x.format == AudioFormat.Lossless) ? "Lossless" : (x.format == AudioFormat.Dolby_Atmos) ? "Dolby Atmos" : (x.format == AudioFormat.Dolby_Audio) ? "Dolby Audio" : null, + SmallImageUrl = (SelectedSmallImage == SmallImage.Artist) ? resp.artistRedirURL : null }, Buttons = new Button[] { - new Button() { Label = "Listen on Apple Music", Url = (resp.trackURL != null) ? resp.trackURL.Replace("https://", "music://") : "music://music.apple.com/home"} + new Button() { Label = "Listen on Apple Music", Url = (resp.songURL != null) ? resp.songURL.Replace("https://", "music://") : "music://music.apple.com/home"} }, Timestamps = new Timestamps() { @@ -98,11 +105,13 @@ public static void SetPresence(SongData x, WebSongResponse resp) End = x.EndTime, } }; + if (oldData.Assets.LargeImageText.Length == 1) + oldData.Assets.LargeImageText = $"{oldData.Assets.LargeImageText}‍"; // THIS HAS U+200D AT THE END OF STRING TO FIX '"large_text" length must be at least 2 characters long' ERROR client.SetPresence(oldData); - if (resp.artworkURL != null && !resp.artworkURL.Contains((S3_Credentials != null) ? (S3_Credentials.GetNullKeys().Count == 0) ? S3_Credentials.bucketURL : "" : "")) + if (resp.coverURL != null && !resp.coverURL.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.trackURL, animatedCoverCts.Token)); + Task t = new Task(() => CheckAnimatedCover(resp.albumURL, animatedCoverCts.Token)); t.Start(); } } diff --git a/AMDiscordRPC/Globals.cs b/AMDiscordRPC/Globals.cs index dad6c88..01c26c2 100644 --- a/AMDiscordRPC/Globals.cs +++ b/AMDiscordRPC/Globals.cs @@ -2,7 +2,14 @@ using DiscordRPC; using DiscordRPC.Helper; using log4net; +using log4net.Appender; using log4net.Config; +using log4net.Core; +using log4net.Filter; +using log4net.Layout; +using log4net.Repository.Hierarchy; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; using System; using System.Collections.Generic; using System.Data.SQLite; @@ -11,8 +18,10 @@ using System.Net; using System.Net.Http; using System.Reflection; +using System.Runtime.Serialization; using System.Text; using System.Text.RegularExpressions; +using AngleSharp.Dom; using static AMDiscordRPC.Database; using static AMDiscordRPC.UI; @@ -30,26 +39,18 @@ 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 WebSongResponse httpRes = new WebSongResponse(); + public static SQLRPCResponse httpRes = new SQLRPCResponse(); 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 SmallImage SelectedSmallImage = SmallImage.LossDolby; + public static CloudflareTypes.AccountCredentials CfAccountCredentials = new CloudflareTypes.AccountCredentials( + "", + "", + "" + ); public static void ConfigureLogger() { @@ -57,9 +58,36 @@ public static void ConfigureLogger() { XmlConfigurator.Configure(stream); } + + LevelRangeFilter lrf = new LevelRangeFilter + { + LevelMax = Level.Fatal, + LevelMin = Level.Info + }; +#if DEBUG + lrf.LevelMin = Level.Debug; +#endif + lrf.ActivateOptions(); + + PatternLayout pl = new PatternLayout + { + ConversionPattern = "[%date{HH:mm:ss.fff}] %level (%method:%line) - %message%newline" + }; + pl.ActivateOptions(); + + RollingFileAppender rfa = new RollingFileAppender + { + AppendToFile = false, + File = @"logs/latest.log", + Layout = pl, + }; + rfa.AddFilter(lrf); + rfa.ActivateOptions(); + + ((Hierarchy)LogManager.GetRepository()).Root.AddAppender(rfa); } - public static async void InitRegion() + public static void InitRegion() { HttpClientHandler HClientHandlerhandler = new HttpClientHandler(); CookieContainer cookies = new CookieContainer(); @@ -72,11 +100,12 @@ public static async void InitRegion() AMRegion = cookies.GetCookies(new Uri("https://music.apple.com/")).Cast() .Where(cookie => cookie.Name == "geo").ToList()[0].Value; + log.Info($"Region selected as: {AMRegion.ToLower()}"); } catch (Exception e) { log.Error($"Error happened while trying to select region, falling back to US Apple Music. Cause: {e}"); - AMRegion = "US"; + AMRegion = "us"; } } @@ -101,9 +130,9 @@ public static string ConvertToValidString(string data) return data; } - public static void InitDBCreds() + public static void ConfigureFromDB() { - using (SQLiteDataReader dbResp = Database.ExecuteReaderCommand($"SELECT {string.Join(", ", Regex.Matches(Database.sqlMap["creds"], @"S3_\w+").FilterRepeatMatches())} FROM creds LIMIT 1")) + using (SQLiteDataReader dbResp = ExecuteReaderCommand($"SELECT {string.Join(", ", Regex.Matches(sqlMap["creds"], @"S3_\w+").FilterRepeatMatches())} FROM creds LIMIT 1")) { while (dbResp.Read()) { @@ -116,6 +145,8 @@ public static void InitDBCreds() ((!dbResp.IsDBNull(5)) ? dbResp.GetBoolean(5) : null)); } } + + SelectedSmallImage = (SmallImage)Convert.ToInt32(ExecuteScalarCommand("SELECT smallImage FROM clientSettings")); } private static void StartFFmpegProcess(string filename) @@ -173,6 +204,135 @@ public static async void CheckFFmpeg() else FFmpegDialog(); } + public enum S3ConnectionStatus + { + Connected, + Disconnected, + Error + } + public enum AudioFormat + { + Lossless, + Dolby_Atmos, + Dolby_Audio, + AAC + } + + public enum SmallImage + { + LossDolby, + Artist, + None + } + + public enum SecondaryType + { + EP, + Album, + Single, + MV + } + + public enum GWLP + { + EXSTYLE = -20, + HINSTANCE = -6, + HWNDPARENT = -8, + ID = -12, + STYLE = -16, + USERDATA = -21, + WNDPROC = -4 + } + + public enum WS : long + { + BORDER = 0x00800000L, + CAPTION = 0x00C00000L, + CHILD = 0x40000000L, + CHILDWINDOW = 0x40000000L, + CLIPCHILDREN = 0x02000000L, + CLIPSIBLINGS = 0x04000000L, + DISABLED = 0x08000000L, + DLGFRAME = 0x00400000L, + GROUP = 0x00020000L, + HSCROLL = 0x00100000L, + ICONIC = 0x20000000L, + MAXIMIZE = 0x01000000L, + MAXIMIZEBOX = 0x00010000L, + MINIMIZE = 0x20000000L, + MINIMIZEBOX = 0x00020000L, + OVERLAPPED = 0x00000000L, + OVERLAPPEDWINDOW = (OVERLAPPED | CAPTION | SYSMENU | THICKFRAME | MINIMIZEBOX | MAXIMIZEBOX), + POPUP = 0x80000000L, + POPUPWINDOW = (POPUP | BORDER | SYSMENU), + SIZEBOX = 0x00040000L, + SYSMENU = 0x00080000L, + TABSTOP = 0x00010000L, + THICKFRAME = 0x00040000L, + TILED = 0x00000000L, + TILEDWINDOW = (OVERLAPPED | CAPTION | SYSMENU | THICKFRAME | MINIMIZEBOX | MAXIMIZEBOX), + VISIBLE = 0x10000000L, + VSCROLL = 0x00200000L + } + + public enum WS_EX : long + { + ACCEPTFILES = 0x00000010L, + APPWINDOW = 0x00040000L, + CLIENTEDGE = 0x00000200L, + COMPOSITED = 0x02000000L, + CONTEXTHELP = 0x00000400L, + CONTROLPARENT = 0x00010000L, + DLGMODALFRAME = 0x00000001L, + LAYERED = 0x00080000L, + LAYOUTRTL = 0x00400000L, + LEFT = 0x00000000L, + LEFTSCROLLBAR = 0x00004000L, + LTRREADING = 0x00000000L, + MDICHILD = 0x00000040L, + NOACTIVATE = 0x08000000L, + NOINHERITLAYOUT = 0x00100000L, + NOPARENTNOTIFY = 0x00000004L, + NOREDIRECTIONBITMAP = 0x00200000L, + OVERLAPPEDWINDOW = (WINDOWEDGE | CLIENTEDGE), + PALETTEWINDOW = (WINDOWEDGE | TOOLWINDOW | TOPMOST), + RIGHT = 0x00001000L, + RIGHTSCROLLBAR = 0x00000000L, + RTLREADING = 0x00002000L, + STATICEDGE = 0x00020000L, + TOOLWINDOW = 0x00000080L, + TOPMOST = 0x00000008L, + TRANSPARENT = 0x00000020L, + WINDOWEDGE = 0x00000100L + } + + public enum HWND + { + BOTTOM = 1, + NOTOPMOST = -2, + TOP = 0, + TOPMOST = -1 + } + + public enum SWP + { + ASYNCWINDOWPOS = 0x4000, + DEFERERASE = 0x2000, + DRAWFRAME = 0x0020, + FRAMECHANGED = 0x0020, + HIDEWINDOW = 0x0080, + NOACTIVATE = 0x0010, + NOCOPYBITS = 0x0100, + NOMOVE = 0x0002, + NOOWNERZORDER = 0x0200, + NOREDRAW = 0x0008, + NOREPOSITION = 0x0200, + NOSENDCHANGING = 0x0400, + NOSIZE = 0x0001, + NOZORDER = 0x0004, + SHOWWINDOW = 0x0040 + } + public class SongData : EventArgs { public string SongName { get; set; } @@ -182,7 +342,9 @@ public class SongData : EventArgs public DateTime EndTime { get; set; } public AudioFormat format { get; set; } - public SongData(string SongName, string ArtistandAlbumName, bool IsMV, DateTime StartTime, DateTime EndTime, AudioFormat format) + public bool isSingle { get; set; } + + public SongData(string SongName, string ArtistandAlbumName, bool IsMV, DateTime StartTime, DateTime EndTime, AudioFormat format, bool isSingle) { this.SongName = SongName; this.ArtistandAlbumName = ArtistandAlbumName; @@ -190,6 +352,7 @@ public SongData(string SongName, string ArtistandAlbumName, bool IsMV, DateTime this.StartTime = StartTime; this.EndTime = EndTime; this.format = format; + this.isSingle = isSingle; } } @@ -233,17 +396,41 @@ public List GetNotNullValues() } } + public class AppleMusicScrapedData + { + public string ArtistName { get; set; } + public string SongName { get; set; } + public string AlbumName { get; set; } + + public SecondaryType? Type { get; set; } + + public AppleMusicScrapedData(string ArtistName = null, string SongName = null, string AlbumName = null, SecondaryType? Type = null) + { + this.ArtistName = ArtistName; + this.SongName = SongName; + this.AlbumName = AlbumName; + this.Type = Type; + } + + public string GetSearchString() + { + return Uri.EscapeDataString($"{SongName} {ArtistName} — {AlbumName} - {Type.ToString()}"); + } + } + public class WebSongResponse { public string artworkURL { get; set; } public string trackURL { get; set; } public string trackName { get; set; } + public string artistURL { get; set; } - public WebSongResponse(string artworkURL = null, string trackURL = null, string trackName = null) + public WebSongResponse(string artworkURL = null, string trackURL = null, string trackName = null, string artistURL = null) { this.artworkURL = artworkURL; this.trackURL = trackURL; this.trackName = trackName; + this.artistURL = artistURL; } public override bool Equals(object obj) @@ -251,8 +438,201 @@ public override bool Equals(object obj) return obj is WebSongResponse other && artworkURL == other.artworkURL && trackURL == other.trackURL && - trackName == other.trackName; + trackName == other.trackName && + artistURL == other.artistURL; + } + } + + [JsonConverter(typeof(StringEnumConverter))] + public class CloudflareTypes + { + public static readonly string endpointV4 = "https://api.cloudflare.com/client/v4"; + + public enum Jurisdiction + { + [EnumMember(Value = "default")] + Default = 0, + + [EnumMember(Value = "eu")] + EU = 1, + + [EnumMember(Value = "fedramp")] + FedRAMP = 2 + } + + public enum Location + { + [EnumMember(Value = "apac")] + APAC, + + [EnumMember(Value = "eeur")] + EEUR, + + [EnumMember(Value = "enam")] + ENAM, + + [EnumMember(Value = "weur")] + WEUR, + + [EnumMember(Value = "wnam")] + WNAM, + + [EnumMember(Value = "oc")] + OC + } + + public enum StorageClass + { + [EnumMember(Value = "standard")] + Standard, + + [EnumMember(Value = "infrequent_access")] + InfrequentAccess + } + + public class ResponseInfo + { + public int code { get; set; } + public string message { get; set; } + public string documentation_url { get; set; } + public Source source { get; set; } + + public ResponseInfo(int code, string message, string documentation_url, Source source) + { + this.code = code; + this.message = message; + this.documentation_url = documentation_url; + this.source = source; + } + } + + public class Response + { + public List errors { get; set; } + public dynamic messages { get; set; } + public dynamic result { get; set; } + public bool success { get; set; } + + public Response(List errors, dynamic messages, dynamic result, bool success) + { + this.errors = errors; + this.messages = messages; + this.result = result; + this.success = success; + } + } + + public class Source + { + public string pointer { get; set; } + } + + public class Bucket + { + public DateTime creation_date { get; set; } + public Jurisdiction jurisdiction { get; set; } + public Location location { get; set; } + public string name { get; set; } + public StorageClass storage_class { get; set; } + + [JsonConstructor] + public Bucket(DateTime creation_date, Jurisdiction jurisdiction, Location location, string name, StorageClass storage_class) + { + this.creation_date = creation_date; + this.jurisdiction = jurisdiction; + this.location = location; + this.name = name; + this.storage_class = storage_class; + } + } + + public class AccountCredentials + { + public string api_token { get; set; } + public string zone_id { get; set; } + public string account_id { get; set; } + + public AccountCredentials(string api_token, string zone_id, string account_id) + { + this.api_token = api_token; + this.zone_id = zone_id; + this.account_id = account_id; + } + } + } + + public static String TrimEnd(this String str, int count) + { + return str.Substring(0, str.Length - count); + } + + public static String[] GetArtist(this IElement element, AppleMusicScrapedData data) + { + string[] returnData = new string[2]; + foreach (IElement innerElement in element.QuerySelectorAll("div.ellipse-lockup-wrapper")) + { + string title = innerElement.QuerySelector("h3.title").TextContent; + if (data.ArtistName == title) + { + returnData[0] = innerElement.QuerySelector("a.click-action").GetAttribute("href"); + returnData[1] = innerElement.QuerySelector("source[type=\"image/jpeg\"]").GetAttribute("srcset") + .Split(' ')[0]; + return returnData; + } + else if (returnData[0] == null && data.ArtistName.Contains(title)) + { + returnData[0] = innerElement.QuerySelector("a.click-action").GetAttribute("href"); + returnData[1] = innerElement.QuerySelector("source[type=\"image/jpeg\"]").GetAttribute("srcset") + .Split(' ')[0]; + } + } + return (returnData[0] == null) ? null : returnData; + + } + + public static String GetAlbum(this IElement element, AppleMusicScrapedData data) + { + foreach (IElement innerElement in element.QuerySelector(@"div[aria-label=""Albums""]").QuerySelectorAll("div > div > section > div > ul > li")) + { + IHtmlCollection texts = innerElement.QuerySelectorAll("span.multiline-clamp__text > a"); + if (texts[0].NormalizedText().Contains(data.AlbumName) && data.ArtistName.Contains(texts[1].NormalizedText())) + { + return texts[0].GetAttribute("href"); + } + } + return null; + } + + public static String NormalizedText(this INode node) + { + return node.TextContent.Replace(" ", " ").Trim(); + } + + public static String GetSong(this IElement element, AppleMusicScrapedData data) + { + foreach (IElement innerElement in element.QuerySelectorAll(@"ul.track-lockup__content")) + { + IHtmlCollection texts = innerElement.QuerySelectorAll(@"div.track-lockup__clamp-wrapper"); + if (texts[0].NormalizedText() == data.SongName && data.ArtistName.Contains(texts[1].QuerySelector("span").NormalizedText())) + { + return texts[0].QuerySelector("a").GetAttribute("href"); + } + } + return null; + } + + public static String GetCover(this IElement element, AppleMusicScrapedData data) + { + foreach (IElement innerElement in element.QuerySelectorAll("div.track-lockup")) + { + if (data.SongName.Contains(innerElement.QuerySelector("ul > li > div > a").NormalizedText()) && + data.ArtistName.Contains(innerElement.QuerySelector("ul > li > div > span > a > span").NormalizedText())) + { + return innerElement.QuerySelectorAll("div > div > div > picture > source")[1].GetAttribute("srcset") + .Split(',')[1].Split(' ')[0]; + } } + return null; } } } \ No newline at end of file diff --git a/AMDiscordRPC/Playlist.cs b/AMDiscordRPC/Playlist.cs index e3520bd..7411cd2 100644 --- a/AMDiscordRPC/Playlist.cs +++ b/AMDiscordRPC/Playlist.cs @@ -18,10 +18,10 @@ namespace AMDiscordRPC { internal class Playlist { - public static async Task ConvertM3U8(string album, string playlistUrl, CancellationToken ct) + public static async Task ConvertM3U8(string albumURL, 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)); + Database.UpdateAlbumCover(albumURL, new Database.SQLCoverData(0, null, true, playlistUrl, null)); StreamInf playlist = await FetchResolution(playlistUrl); if (!ct.IsCancellationRequested && playlist != null) { @@ -48,7 +48,7 @@ public static async Task ConvertM3U8(string album, string playlistUrl, Cancellat 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"); - Database.UpdateAlbum(new Database.SQLCoverResponse(album, null, null, null, null, servedPath)); + Database.UpdateAlbumCover(albumURL, new Database.SQLCoverData(0, null, null, null, servedPath)); if (ct.IsCancellationRequested) throw new Exception("Cancelled"); SetCover(servedPath); log.Debug("Set Animated Cover"); diff --git a/AMDiscordRPC/Properties/AssemblyInfo.cs b/AMDiscordRPC/Properties/AssemblyInfo.cs index 6cf49e4..e651e22 100644 --- a/AMDiscordRPC/Properties/AssemblyInfo.cs +++ b/AMDiscordRPC/Properties/AssemblyInfo.cs @@ -9,7 +9,7 @@ [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("CrawLeyYou")] [assembly: AssemblyProduct("AMDiscordRPC")] -[assembly: AssemblyCopyright("Copyright © 2024")] +[assembly: AssemblyCopyright("Copyright © 2026")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/AMDiscordRPC/Properties/Resources.Designer.cs b/AMDiscordRPC/Properties/Resources.Designer.cs index 91b7f95..a7ca0b1 100644 --- a/AMDiscordRPC/Properties/Resources.Designer.cs +++ b/AMDiscordRPC/Properties/Resources.Designer.cs @@ -19,10 +19,10 @@ namespace AMDiscordRPC.Properties { // 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.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resources { + public class Resources { private static global::System.Resources.ResourceManager resourceMan; @@ -36,7 +36,7 @@ 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 { + public 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); @@ -51,7 +51,7 @@ internal Resources() { /// resource lookups using this strongly typed resource class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { + public static global::System.Globalization.CultureInfo Culture { get { return resourceCulture; } @@ -63,7 +63,7 @@ internal Resources() { /// /// Looks up a localized resource of type System.Drawing.Bitmap. /// - internal static System.Drawing.Bitmap Logo_Black { + public static System.Drawing.Bitmap Logo_Black { get { object obj = ResourceManager.GetObject("Logo_Black", resourceCulture); return ((System.Drawing.Bitmap)(obj)); @@ -73,7 +73,7 @@ internal static System.Drawing.Bitmap Logo_Black { /// /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). /// - internal static System.Drawing.Icon Logo_Black_32 { + public static System.Drawing.Icon Logo_Black_32 { get { object obj = ResourceManager.GetObject("Logo_Black_32", resourceCulture); return ((System.Drawing.Icon)(obj)); @@ -83,7 +83,7 @@ internal static System.Drawing.Icon Logo_Black_32 { /// /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). /// - internal static System.Drawing.Icon Logo_Black1 { + public static System.Drawing.Icon Logo_Black1 { get { object obj = ResourceManager.GetObject("Logo_Black1", resourceCulture); return ((System.Drawing.Icon)(obj)); diff --git a/AMDiscordRPC/UI.cs b/AMDiscordRPC/UI.cs index 4af808e..52f3d85 100644 --- a/AMDiscordRPC/UI.cs +++ b/AMDiscordRPC/UI.cs @@ -1,8 +1,9 @@ using AMDiscordRPC.UIComponents; +using FlaUI.UIA3; using System; using System.Diagnostics; -using System.Drawing; using System.IO; +using System.Runtime.InteropServices; using System.Threading; using System.Windows; using System.Windows.Forms; @@ -10,15 +11,26 @@ using static AMDiscordRPC.Globals; using Application = System.Windows.Application; using OpenFileDialog = Microsoft.Win32.OpenFileDialog; +using Window = FlaUI.Core.AutomationElements.Window; namespace AMDiscordRPC { internal class UI { private static InputWindow inputWindow; + private static OptionsWindow optionsWindow; private static Application app; private static Thread mainThread = Thread.CurrentThread; + [DllImport("user32.dll")] + private static extern bool SetWindowPos(IntPtr hWnd, int hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags); + + [DllImport("user32.dll")] + private static extern long GetWindowLongPtrA(IntPtr hWnd, int nIndex); + + [DllImport("user32.dll")] + private static extern long SetWindowLongPtrA(IntPtr hWnd, int nIndex, long dwNewLong); + public static void CreateUI() { Thread thread = new Thread(() => @@ -31,6 +43,7 @@ public static void CreateUI() thread.SetApartmentState(ApartmentState.STA); thread.Start(); + log.Debug("Tray thread started."); } public static void FFmpegDialog() @@ -67,12 +80,43 @@ public static void FFmpegDialog() thread.Start(); } + public static void FullScreenTweak() + { + using (var automation = new UIA3Automation()) + { + Window lyricScreenWindow = null; + foreach (var window in AppleMusicProc.GetAllTopLevelWindows(automation)) + { + if (window.FindFirstChild().Name != "Non Client Input Sink Window" && window.Name == "Apple Music") lyricScreenWindow = window; + } + + if (lyricScreenWindow != null) + { + IntPtr lyricsScreenHandler = (IntPtr)lyricScreenWindow.Properties.NativeWindowHandle; + Screen lyricsScreenHandlerCurrentMonitor = Screen.FromHandle(lyricsScreenHandler); + long style = GetWindowLongPtrA(lyricsScreenHandler, (int)GWLP.STYLE); + style &= ~((long)WS.CAPTION | (long)WS.THICKFRAME); + SetWindowLongPtrA(lyricsScreenHandler, (int)GWLP.STYLE, style); + + long exStyle = GetWindowLongPtrA(lyricsScreenHandler, (int)GWLP.EXSTYLE); + exStyle &= ~((long)WS_EX.DLGMODALFRAME | (long)WS_EX.CLIENTEDGE | (long)WS_EX.STATICEDGE); + SetWindowLongPtrA(lyricsScreenHandler, (int)GWLP.EXSTYLE, exStyle); + + SetWindowPos(lyricsScreenHandler, (int)HWND.TOPMOST, lyricsScreenHandlerCurrentMonitor.Bounds.Left, + lyricsScreenHandlerCurrentMonitor.Bounds.Top, lyricsScreenHandlerCurrentMonitor.Bounds.Width, + lyricsScreenHandlerCurrentMonitor.Bounds.Height, + (uint)SWP.NOOWNERZORDER | (uint)SWP.FRAMECHANGED | (uint)SWP.SHOWWINDOW); + } + } + } + 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 MenuItem optionsMenu = new MenuItem(); public AMDiscordRPCTray() { @@ -91,11 +135,24 @@ public AMDiscordRPCTray() }); }); + optionsMenu.Text = "Options"; + optionsMenu.Index = 2; + optionsMenu.Click += new EventHandler((object sender, EventArgs e) => + { + app.Dispatcher.Invoke(() => + { + optionsWindow = new OptionsWindow(); + optionsWindow.Show(); + }); + }); + contextMenu.MenuItems.AddRange( new MenuItem[] { notifySongState, s3Menu, + optionsMenu, + new MenuItem("Fix Fullscreen", (s,e) => FullScreenTweak()), 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); }) } diff --git a/AMDiscordRPC/UIComponents/InputWindow.xaml.cs b/AMDiscordRPC/UIComponents/InputWindow.xaml.cs index 7ff2261..9d57c8a 100644 --- a/AMDiscordRPC/UIComponents/InputWindow.xaml.cs +++ b/AMDiscordRPC/UIComponents/InputWindow.xaml.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; + +using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using System.Windows; @@ -72,13 +73,15 @@ private void SetS3Button_Click(object sender, RoutedEventArgs e) } } - private static void PutValues(S3_Creds creds, ShowMode mode = ShowMode.Hide) + private void PutValues(S3_Creds creds, ShowMode mode = ShowMode.Hide) { List Instances = new List { Instance.AccessKeyIDBox, Instance.SecretKeyBox, Instance.EndpointBox, Instance.BucketNameBox, Instance.PublicBucketURLBox }; List Keys = new List() { creds.accessKey, creds.secretKey, creds.serviceURL, creds.bucketName, creds.bucketURL }; foreach (var (item, index) in Instances.Select((v, i) => (v, i))) { - PlaceholderAdorner adorner = Helpers.TextBoxHelper.GetPlaceholderAdorner(item); + PlaceholderAdorner adorner = GetPlaceholderAdorner(item); + if (Keys[index] == null) + return; item.Text = (mode == ShowMode.Show) ? Keys[index] : new string('*', Keys[index].Length); item.IsEnabled = (mode == ShowMode.Show) ? true : false; if (Keys[index].Length > 0) diff --git a/AMDiscordRPC/UIComponents/OptionsWindow.xaml b/AMDiscordRPC/UIComponents/OptionsWindow.xaml new file mode 100644 index 0000000..8c4a0af --- /dev/null +++ b/AMDiscordRPC/UIComponents/OptionsWindow.xaml @@ -0,0 +1,21 @@ + + + + + + + + + + diff --git a/AMDiscordRPC/UIComponents/OptionsWindow.xaml.cs b/AMDiscordRPC/UIComponents/OptionsWindow.xaml.cs new file mode 100644 index 0000000..87bd260 --- /dev/null +++ b/AMDiscordRPC/UIComponents/OptionsWindow.xaml.cs @@ -0,0 +1,32 @@ +using System.Windows; +using System.Windows.Controls; +using static AMDiscordRPC.Globals; + +namespace AMDiscordRPC.UIComponents +{ + /// + /// Interaction logic for OptionsWindow.xaml + /// + public partial class OptionsWindow : Window + { + private static OptionsWindow Instance; + public OptionsWindow() + { + InitializeComponent(); + Instance = this; + Instance.Loaded += (s, e) => + { + smallImage.SelectedIndex = (int)SelectedSmallImage; + }; + } + + private void SmallImage_OnSelectionChanged(object sender, SelectionChangedEventArgs e) + { + SelectedSmallImage = (SmallImage)smallImage.SelectedIndex; + if (Database.ExecuteScalarCommand("SELECT smallImage FROM clientSettings") == null) + Database.ExecuteNonQueryCommand($"INSERT INTO clientSettings (smallImage) VALUES ({smallImage.SelectedIndex})"); + else + Database.ExecuteNonQueryCommand($"UPDATE clientSettings SET (smallImage) = ({smallImage.SelectedIndex})"); + } + } +} diff --git a/AMDiscordRPC/log4netconf.xml b/AMDiscordRPC/log4netconf.xml index 30837d1..8f1ebc9 100644 --- a/AMDiscordRPC/log4netconf.xml +++ b/AMDiscordRPC/log4netconf.xml @@ -4,23 +4,8 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/AMDiscordRPC/packages.config b/AMDiscordRPC/packages.config index 5d6c8c1..e8d4ce6 100644 --- a/AMDiscordRPC/packages.config +++ b/AMDiscordRPC/packages.config @@ -3,7 +3,7 @@ - + @@ -11,6 +11,7 @@ + diff --git a/README.md b/README.md index 8e17c67..3b9cb6a 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@

AMDiscordRPC

An another Apple Music Discord RPC

+[Support Server](https://discord.gg/5trvjuqgm8) ## Usage [Download](https://github.com/CrawLeyYou/AMDiscordRPC/releases/latest) latest release and make sure you have [.NET Framework 4.7.2](https://dotnet.microsoft.com/en-us/download/dotnet-framework/net472).