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..9aa03e6 100644 --- a/AMDiscordRPC/AMDiscordRPC.cs +++ b/AMDiscordRPC/AMDiscordRPC.cs @@ -20,13 +20,13 @@ internal class AMDiscordRPC private static string oldAlbumnArtist; static void Main(string[] args) { + ConfigureLogger(); InitRegion(); CreateUI(); - ConfigureLogger(); - InitializeDiscordRPC(); - AttachToAppleMusic(); + InitDiscordRPC(); + AttachToAM(); 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) @@ -43,9 +43,9 @@ static void Main(string[] args) SetPresence(x, httpRes); oldAlbumnArtist = x.ArtistandAlbumName; } - }; + }; CheckDatabaseIntegrity(); - InitDBCreds(); + ConfigureFromDB(); CheckFFmpeg(); InitS3(); AMEvent(); @@ -88,7 +88,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) @@ -144,7 +144,7 @@ static void AMEvent() 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"); @@ -181,7 +181,6 @@ 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}")); @@ -214,8 +213,6 @@ 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)); @@ -249,20 +246,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 +269,7 @@ static void AMEvent() { while (!AMAttached) { - AttachToAppleMusic(); + AttachToAM(); Thread.Sleep(1000); } AMEvent(); diff --git a/AMDiscordRPC/AMDiscordRPC.csproj b/AMDiscordRPC/AMDiscordRPC.csproj index 4d4c56a..1178e86 100644 --- a/AMDiscordRPC/AMDiscordRPC.csproj +++ b/AMDiscordRPC/AMDiscordRPC.csproj @@ -100,6 +100,9 @@ Resources\Logo Black.ico + + false + ..\packages\AngleSharp.1.3.0\lib\net472\AngleSharp.dll @@ -138,6 +141,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 @@ -250,6 +262,9 @@ InputWindow.xaml + + OptionsWindow.xaml + @@ -279,6 +294,10 @@ Designer MSBuild:Compile + + Designer + MSBuild:Compile + @@ -310,8 +329,10 @@ + + \ No newline at end of file diff --git a/AMDiscordRPC/App.config b/AMDiscordRPC/App.config index 2012abb..64a9a5f 100644 --- a/AMDiscordRPC/App.config +++ b/AMDiscordRPC/App.config @@ -49,6 +49,10 @@ + + + + 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/Covers.cs b/AMDiscordRPC/Covers.cs index 5c69e07..294c511 100644 --- a/AMDiscordRPC/Covers.cs +++ b/AMDiscordRPC/Covers.cs @@ -1,4 +1,5 @@ -using AngleSharp.Html.Dom; +using AngleSharp.Dom; +using AngleSharp.Html.Dom; using Newtonsoft.Json.Linq; using System; using System.Net.Http; @@ -30,9 +31,10 @@ private static async Task AsyncFetchiTunes(string album, string ( imageRes["results"][0]["artworkUrl100"].ToString(), imageRes["results"][0]["trackViewUrl"].ToString(), - imageRes["results"][0]["collectionName"].ToString() + imageRes["results"][0]["collectionName"].ToString(), + imageRes["results"][0]["artistViewUrl"].ToString() ); - Database.UpdateAlbum(new Database.SQLCoverResponse(album, webRes.artworkURL, webRes.trackURL)); + InsertAlbum(new SQLCoverResponse(album, webRes.artworkURL, webRes.trackURL, null, null, null, webRes.artistURL)); CoverThread = null; return webRes; } @@ -58,8 +60,14 @@ private static async Task AsyncFetchiTunes(string album, string } } + public static async Task AsyncArtistProfileFetch(string url) + { + return null; + } + public static async Task AsyncAMFetch(string album, string searchStr) { + log.Debug($"https://music.apple.com/{AMRegion.ToLower()}/search?term={searchStr}"); try { HttpResponseMessage AMRequest = await hclient.GetAsync($"https://music.apple.com/{AMRegion.ToLower()}/search?term={searchStr}"); @@ -67,12 +75,15 @@ public static async Task AsyncAMFetch(string album, string sear { 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") + document.DocumentElement.QuerySelectorAll("div.track-lockup__artwork-wrapper > div > picture > source")[1].GetAttribute("srcset").Split(',')[1].Split(' ')[0], + document.DocumentElement.QuerySelectorAll("div.track-lockup__clamp-wrapper > a")[0].GetAttribute("href"), + null, + document.DocumentElement.QuerySelectorAll("div.track-lockup__clamp-wrapper > span > a")[0].GetAttribute("href") ); CoverThread = null; - Database.UpdateAlbum(new Database.SQLCoverResponse(album, webRes.artworkURL, webRes.trackURL)); + InsertAlbum(new Database.SQLCoverResponse(album, webRes.artworkURL, webRes.trackURL, null, null, null, webRes.artistURL)); return webRes; } else @@ -93,6 +104,7 @@ public static async Task CheckAnimatedCover(string album, string url, Cancellati try { var appleMusicDom = await hclient.GetAsync(url); + log.Debug($"Animated Cover Request: {url}"); if (appleMusicDom.IsSuccessStatusCode) { string DOMasAString = await appleMusicDom.Content.ReadAsStringAsync(); @@ -117,7 +129,7 @@ public static async Task GetCover(string album, string searchSt { try { - log.Debug($"https://music.apple.com/us/search?term={searchStr}"); + log.Debug($"https://music.apple.com/{AMRegion.ToLower()}/search?term={searchStr}"); SQLCoverResponse cover = GetAlbumDataFromSQL(album); if (cover != null) { diff --git a/AMDiscordRPC/Database.cs b/AMDiscordRPC/Database.cs index 2594fb2..03ec51a 100644 --- a/AMDiscordRPC/Database.cs +++ b/AMDiscordRPC/Database.cs @@ -11,9 +11,14 @@ internal 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, 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, 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(); } @@ -57,6 +62,7 @@ private static void CreateDatabase() } } + // Note: source, streamurl, animated, animatedUrl can be stored in external table so we decrease the file size of database private static void CheckForeignKeys() { ExecuteNonQueryCommand("PRAGMA foreign_keys = on"); @@ -98,18 +104,18 @@ private static void CheckTables() public static void UpdateAlbum(SQLCoverResponse data) { - ExecuteNonQueryCommand($"UPDATE coverTable SET ({string.Join(", ", data.GetNotNullKeys())}) = ({string.Join(", ", data.GetNotNullValues())}) WHERE album = '{data.album}'"); + ExecuteNonQueryCommand($"UPDATE coverTable SET ({string.Join(", ", data.GetNotNullKeys())}) = ({string.Join(", ", data.GetNotNullValues())}) WHERE album = @album", new[] { new SQLiteParameter("@album", data.album)}); } - public static void CheckAndInsertAlbum(string album) + public static void InsertAlbum(SQLCoverResponse data) { - if (ExecuteScalarCommand($"SELECT album from coverTable WHERE album = '{album}'") == null) - ExecuteNonQueryCommand($"INSERT INTO coverTable(album) VALUES ('{album}')"); + if (ExecuteScalarCommand($"SELECT album from coverTable WHERE album = @album", new[] { new SQLiteParameter("@album", data.album) }) == null) + ExecuteNonQueryCommand($@"INSERT INTO coverTable(album, {string.Join(", ", data.GetNotNullKeys())}) VALUES (@album, {string.Join(", ", data.GetNotNullValues())})", new[] {new SQLiteParameter("@album", data.album)}); } public static SQLCoverResponse GetAlbumDataFromSQL(string album) { - using (SQLiteDataReader reader = ExecuteReaderCommand($"SELECT * FROM coverTable WHERE album = '{album}' LIMIT 1")) + using (SQLiteDataReader reader = ExecuteReaderCommand($"SELECT * FROM coverTable WHERE album = @album LIMIT 1", new[] { new SQLiteParameter("@album", album) })) { while (reader.Read()) { @@ -119,22 +125,59 @@ public static SQLCoverResponse GetAlbumDataFromSQL(string album) reader.GetString(2), ((!reader.IsDBNull(3)) ? reader.GetBoolean(3) : null), ((!reader.IsDBNull(4)) ? reader.GetString(4) : null), - ((!reader.IsDBNull(5)) ? reader.GetString(5) : null)); + ((!reader.IsDBNull(5)) ? reader.GetString(5) : null), + reader.GetString(6) + ); } } return null; } + public static SQLSongResponse 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; + "; + using (SQLiteDataReader reader = ExecuteReaderCommand(cmd, new[] { new SQLiteParameter("@album", album), new SQLiteParameter("@song", song), new SQLiteParameter("@artist", artist) })) + { + + } + + return null; + } + 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 +187,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 +198,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 +213,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 +241,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 +256,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 +277,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,7 +296,23 @@ 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; + } + } + + private class ForeignKey + { + public string key { get; set; } + public string refTable { get; set; } + public string refColumn { get; set; } + + public ForeignKey(string key, string refTable, string refColumn) + { + this.key = key; + this.refTable = refTable; + this.refColumn = refColumn; } } @@ -255,8 +324,10 @@ public class SQLCoverResponse public bool? animated { get; set; } public string streamURL { get; set; } public string animatedURL { get; set; } + public string artistRedirURL { get; set; } - public SQLCoverResponse(string album = null, string source = null, string redirURL = null, bool? animated = null, string streamURL = null, string animatedURL = null) + + public SQLCoverResponse(string album = null, string source = null, string redirURL = null, bool? animated = null, string streamURL = null, string animatedURL = null, string artistRedirURL = null) { this.album = album; this.source = source; @@ -264,6 +335,7 @@ public SQLCoverResponse(string album = null, string source = null, string redirU this.animated = animated; this.streamURL = streamURL; this.animatedURL = animatedURL; + this.artistRedirURL = artistRedirURL; } public List GetNotNullKeys() @@ -277,5 +349,91 @@ public List GetNotNullValues() } } + + 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.artist = artist; + this.song = song; + } + } + + public class SQLCoverData + { + public int id; + public string staticCoverURL; + public bool? isAnimated; + public string? streamURL; + public string? animatedURL; + + 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 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..715c973 100644 --- a/AMDiscordRPC/Discord.cs +++ b/AMDiscordRPC/Discord.cs @@ -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()) @@ -79,6 +80,7 @@ public static void SetPresence(SongData x, WebSongResponse resp) { Type = ActivityType.Listening, Details = ConvertToValidString(x.SongName), + StateUrl = resp.artistURL, StatusDisplay = StatusDisplayType.State, State = (x.IsMV) ? x.ArtistandAlbumName : ConvertToValidString(x.ArtistandAlbumName.Split('—')[0]), Assets = new Assets() @@ -98,6 +100,8 @@ 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 : "" : "")) { diff --git a/AMDiscordRPC/Globals.cs b/AMDiscordRPC/Globals.cs index dad6c88..ee644c5 100644 --- a/AMDiscordRPC/Globals.cs +++ b/AMDiscordRPC/Globals.cs @@ -2,7 +2,12 @@ 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 System; using System.Collections.Generic; using System.Data.SQLite; @@ -34,22 +39,9 @@ public static class Globals 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 void ConfigureLogger() { @@ -57,9 +49,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,6 +91,7 @@ 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) { @@ -101,9 +121,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 +136,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 +195,126 @@ 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 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; } @@ -238,12 +380,14 @@ 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 +395,13 @@ 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; } } + public static String TrimEnd(this String str, int count) + { + return str.Substring(0, str.Length - count); + } } } \ No newline at end of file diff --git a/AMDiscordRPC/UI.cs b/AMDiscordRPC/UI.cs index 4af808e..2b4a651 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..eb3c5de --- /dev/null +++ b/AMDiscordRPC/UIComponents/OptionsWindow.xaml.cs @@ -0,0 +1,36 @@ + +using System; +using System.Management.Instrumentation; +using System.Text.RegularExpressions; +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..79da35e 100644 --- a/AMDiscordRPC/packages.config +++ b/AMDiscordRPC/packages.config @@ -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).