diff --git a/NetEaseMusic-DiscordRPC/MemoryUtil.cs b/NetEaseMusic-DiscordRPC/MemoryUtil.cs index 3c637f7..066e1f6 100644 --- a/NetEaseMusic-DiscordRPC/MemoryUtil.cs +++ b/NetEaseMusic-DiscordRPC/MemoryUtil.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; +using System.Text; namespace NetEaseMusic_DiscordRPC { @@ -15,8 +16,10 @@ static class MemoryUtil public static List Offsets; - public static void LoadMemory(int pid, ref double rate, ref double lens) + public static void LoadNetEaseMemory(int pid, ref double rate, ref double lens, ref string title, ref string album, ref string artists, ref string cover, ref string url, out bool extra) { + extra = false; + if (ProcessId != pid) { EntryPoint = OpenProcess(0x10, IntPtr.Zero, pid); @@ -24,33 +27,20 @@ public static void LoadMemory(int pid, ref double rate, ref double lens) using var process = Process.GetProcessById(pid); - //Debug.Print($"Process Hadnle {process.Id}"); - foreach (ProcessModule module in process.Modules) { - //Debug.Print($"Find module {module.ModuleName}"); if ("cloudmusic.dll".Equals(module.ModuleName)) { BaseAddress = module.BaseAddress; - - //Debug.Print($"Match module address {module.BaseAddress}"); break; } } Version = process.MainModule?.FileVersionInfo.ProductVersion; - //Debug.Print($"Match application version {Version}"); - } - - if (EntryPoint == IntPtr.Zero || BaseAddress == IntPtr.Zero) - { - //Debug.Print($"Null handle"); - return; } - if (string.IsNullOrEmpty(Version)) + if (EntryPoint == IntPtr.Zero || BaseAddress == IntPtr.Zero || string.IsNullOrEmpty(Version)) { - //Debug.Print($"Null version"); return; } @@ -59,20 +49,12 @@ public static void LoadMemory(int pid, ref double rate, ref double lens) var offset = Offsets.FirstOrDefault(x => x.Version == Version); if (offset == null) { - ///Debug.Print($"Offset not found"); return; } - //Debug.Print($"Offset -> {offset.Offsets.Length} | {offset.Offsets.Schedule}"); + // create buffer + var buffer = new byte[64]; - var buffer = new byte[sizeof(double) + 1]; - - // offset 2.7.1 -> 0x8ADA70 - // offset 2.7.3 -> 0x8BDAD0 - // offset 2.7.6 -> 0x8BEAD8 - // offset 2.8.0 -> 0x939B50 - // offset 2.9.2 -> 0x93EB38 - // 0ffset 2.9.5 -> 0x955F60 if (!ReadProcessMemory(EntryPoint, BaseAddress + offset.Offsets.Schedule, buffer, sizeof(double), IntPtr.Zero)) { Debug.Print($"Failed to load memory at 0x{(BaseAddress + offset.Offsets.Schedule).ToString("X")}"); @@ -80,24 +62,66 @@ public static void LoadMemory(int pid, ref double rate, ref double lens) } var current = BitConverter.ToDouble(buffer, 0); - // offset 2.7.1 -> 0x8CDF88 - // offset 2.7.3 -> 0x8DEB98 - // offset 2.7.6 -> 0x8DFC080 - // offset 2.8.0 -> 0x961D98 - // offset 2.9.2 -> 0x967DA8 - // offset 2.9.5 -> 0x97F588 if (!ReadProcessMemory(EntryPoint, BaseAddress + offset.Offsets.Length, buffer, sizeof(double), IntPtr.Zero)) { Debug.Print($"Failed to load memory at 0x{(BaseAddress + offset.Offsets.Length).ToString("X")}"); return; } - var maxlens = BitConverter.ToDouble(buffer, 0); - - //Debug.Print($"Current value {current} | {maxlens}"); + var length = BitConverter.ToDouble(buffer, 0); rate = current; - lens = maxlens; - //text = process.MainWindowTitle; + lens = length; + + if (offset.Offsets.CachePointer > 0) + { + try + { + // offset +8 (28 8f 0b050043 04 88) + // 28 8f 0b050043 04 88 31 00 39 00 39 00 38 00 36 00 39 00 36 00 34 00 34 00 33 00 5f 00 31 00 5f 00 38 00 34 00 33 00 35 00360039 00 36 00 31 00 37 00 33 00 00 + // op code + // cloudmusic.dll+709868: + // 7A1A985A - 0F82 68030000 - jb cloudmusic.dll + 709BC8 + // 7A1A9860 - 0FBA 25 284F547A 01 - bt[cloudmusic.dll + AA4F28],01 + // 7A1A9868 - 73 07 - jae cloudmusic.dll + 709871 << + // 7A1A986A - F3 A4 - repe movsb + // 7A1A986C - E9 17030000 - jmp cloudmusic.dll + 709B88 + if (!ReadProcessMemory(EntryPoint, BaseAddress + offset.Offsets.CachePointer, buffer, sizeof(uint), IntPtr.Zero)) + { + Debug.Print("Error read cache array pointer for %LocalAppData%/NetEase/CloudMusic/webdata/file"); + return; + } + + var add = BitConverter.ToUInt32(buffer, 0); + var ptr = new IntPtr(add); + if (!ReadProcessMemory(EntryPoint, ptr, buffer, int.MaxValue.ToString().Length * 2, IntPtr.Zero)) + { + Debug.Print("Error read pointer for %LocalAppData%/NetEase/CloudMusic/webdata/file"); + return; + } + + /* {tid}_{fid}_{data} */ + var id = Encoding.Unicode.GetString(buffer).Trim().Split('_')[0]; + + var sound = NetEaseCacheManager.GetSoundInfo(int.Parse(id)); + if (sound == null) + { + Debug.Print($"Sound {id} not found in cache"); + return; + } + + title = sound.Track.Name; + album = sound.Track.Album.Name; + artists = string.Join(", ", sound.Track.Artists.Select(x => x.Name)); + cover = sound.Track.Album.Cover; + lens = sound.Track.Duration * 0.001; + url = $"https://music.163.com/song?id={sound.Id}"; + extra = true; + } + catch (Exception e) + { + Debug.Print(e.ToString()); + } + } } [DllImport("kernel32", SetLastError = true)] diff --git a/NetEaseMusic-DiscordRPC/NetEaseMusic-DiscordRPC.csproj b/NetEaseMusic-DiscordRPC/NetEaseMusic-DiscordRPC.csproj index 019d9c1..e3f63e3 100644 --- a/NetEaseMusic-DiscordRPC/NetEaseMusic-DiscordRPC.csproj +++ b/NetEaseMusic-DiscordRPC/NetEaseMusic-DiscordRPC.csproj @@ -77,7 +77,7 @@ - + diff --git a/NetEaseMusic-DiscordRPC/Program.cs b/NetEaseMusic-DiscordRPC/Program.cs index f1b9fdb..25ba189 100644 --- a/NetEaseMusic-DiscordRPC/Program.cs +++ b/NetEaseMusic-DiscordRPC/Program.cs @@ -8,10 +8,11 @@ using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; +using Button = DiscordRPC.Button; namespace NetEaseMusic_DiscordRPC { - static class Program + internal static class Program { private const string NeteaseAppId = "481562643958595594"; private const string TencentAppId = "903485504899665990"; @@ -63,7 +64,7 @@ private static async Task Main() ContextMenu = notifyMenu, Text = "NetEase Cloud Music DiscordRPC", Icon = Properties.Resources.icon, - Visible = true + Visible = true, }; exitButton.Click += (sender, args) => @@ -76,9 +77,13 @@ private static async Task Main() { var x = AutoStart.Check(); if (x) + { AutoStart.Remove(); + } else + { AutoStart.Set(); + } autoButton.Text = "AutoStart" + " " + (AutoStart.Check() ? "√" : "✘"); }; @@ -89,10 +94,13 @@ private static async Task Main() private static async Task UpdateThread(DiscordRpcClient neteaseRpc, DiscordRpcClient tencentRpc) { - var playerState = false; var currentSong = string.Empty; var currentSing = string.Empty; + var currentHash = string.Empty; + var currentImgX = string.Empty; + var currentKeyX = string.Empty; + var currentCurl = string.Empty; var currentRate = 0.0; var maxSongLens = 0.0; var lastPlaying = -1; @@ -101,14 +109,11 @@ private static async Task UpdateThread(DiscordRpcClient neteaseRpc, DiscordRpcCl { try { - string title; - int pid; - var lastRate = currentRate; var lastLens = maxSongLens; var skipThis = false; - if (!Win32Api.User32.GetWindowTitle("OrpheusBrowserHost", out title, out pid) && + if (!Win32Api.User32.GetWindowTitle("OrpheusBrowserHost", out var title, out var pid) && !Win32Api.User32.GetWindowTitle("QQMusic_Daemon_Wnd", out title)) { Debug.Print($"player is not running"); @@ -116,10 +121,16 @@ private static async Task UpdateThread(DiscordRpcClient neteaseRpc, DiscordRpcCl goto update; } + // NetEase if (pid > 0) { // load memory - MemoryUtil.LoadMemory(pid, ref currentRate, ref maxSongLens); + var artists = string.Empty; + var album = string.Empty; + var cover = string.Empty; + var url = string.Empty; + MemoryUtil.LoadNetEaseMemory(pid, ref currentRate, ref maxSongLens, ref title, ref album, ref artists, + ref cover, ref url, out var extra); var diffRate = currentRate - lastRate; @@ -128,7 +139,7 @@ private static async Task UpdateThread(DiscordRpcClient neteaseRpc, DiscordRpcCl Debug.Print($"invalid? {currentRate} | {lastRate} | {diffRate}"); playerState = false; } - // magic hacks? //currentRate != 0.109 && + // magic hacks? //currentRate != 0.109 && else if ((currentRate > 0.109 || currentRate == 0) && diffRate < 0.001 && diffRate >= 0 && maxSongLens == lastLens) //currentRate.Equals(lastRate) { @@ -138,16 +149,41 @@ private static async Task UpdateThread(DiscordRpcClient neteaseRpc, DiscordRpcCl } else if (!playerState || !maxSongLens.Equals(lastLens)) { - var match = title.Replace("\r", "").Replace("\n", "").Replace(" - ", "\t").Split('\t'); - if (match.Length > 1) + if (extra) { - currentSong = match[0]; - currentSing = match[1]; // like spotify + // force refresh if cache loaded + var hash = $"{title}-{artists}-{cover}"; + if (hash != currentHash) + { + skipThis = false; + } + + currentHash = hash; + + currentSong = title; + currentSing = artists; + + currentImgX = cover; + currentKeyX = album; + currentCurl = url; } else { - currentSong = title; - currentSing = string.Empty; + var match = title.Replace("\r", "").Replace("\n", "").Replace(" - ", "\t").Split('\t'); + if (match.Length > 1) + { + currentSong = match[0]; + currentSing = match[1]; // like spotify + } + else + { + currentSong = title; + currentSing = string.Empty; + } + + currentImgX = "timg"; + currentKeyX = "Netease Cloud Music"; + currentCurl = "https://music.163.com/#/search/m/?s=" + currentSong; } playerState = true; @@ -160,12 +196,19 @@ private static async Task UpdateThread(DiscordRpcClient neteaseRpc, DiscordRpcCl skipThis = true; } } + // Tencent else if (pid == 0) { // mark as playing and always update playerState = true; skipThis = false; + // TODO + // soundId = QQMusic.dll+B661C8 + // soundLen = QQMusic.dll+B661DC + // soundInfo = ptr + 0x19B09528 + // https://y.qq.com/n/ryqq/songDetail/ + var match = title.Replace(" - ", "\t").Split('\t'); if (match.Length > 1) { @@ -189,13 +232,13 @@ private static async Task UpdateThread(DiscordRpcClient neteaseRpc, DiscordRpcCl lastPlaying = pid; - update: + update: // update #if DEBUG if (!playerState) #else - if (Win32Api.User32.IsFullscreenAppRunning() || Win32Api.User32.IsWhitelistAppRunning() || - !playerState) + if (Win32Api.User32.IsFullscreenAppRunning() || Win32Api.User32.IsWhitelistAppRunning() || + !playerState) #endif { Debug.Print( @@ -216,16 +259,18 @@ private static async Task UpdateThread(DiscordRpcClient neteaseRpc, DiscordRpcCl } if (skipThis) - // skip + // skip + { continue; + } if (pid > 0) { tencentRpc.ClearPresence(); neteaseRpc.SetPresence(new RichPresence { - Details = $"🎵 {currentSong}", - State = $"🎤 {currentSing}", + Details = $"🎵 {currentSong}", + State = $"🎤 {currentSing}", Timestamps = new Timestamps( DateTime.UtcNow.Subtract(TimeSpan.FromSeconds(currentRate)), @@ -234,9 +279,21 @@ private static async Task UpdateThread(DiscordRpcClient neteaseRpc, DiscordRpcCl Assets = new Assets { - LargeImageKey = "timg", - LargeImageText = "Netease Cloud Music" - } + LargeImageKey = currentImgX, + LargeImageText = currentKeyX, + SmallImageKey = "timg", + SmallImageText = "Netease Cloud Music", + }, + + Buttons = new[] + { + new Button + { + Label = "🎧 Listen", + Url = currentCurl, + // Url = "https://music.163.com/" + }, + }, }); } else if (pid == 0) @@ -250,8 +307,8 @@ private static async Task UpdateThread(DiscordRpcClient neteaseRpc, DiscordRpcCl Assets = new Assets { LargeImageKey = "qimg", - LargeImageText = "QQMusic" - } + LargeImageText = "QQMusic", + }, }); } @@ -280,12 +337,14 @@ private static async Task GetOffsetsAsync() { using var client = new HttpClient() { - Timeout = TimeSpan.FromMinutes(1) + Timeout = TimeSpan.FromMinutes(1), }; client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - var response = await client.GetAsync("https://github.com/Kxnrl/NetEase-Cloud-Music-DiscordRPC/raw/master/offset/offset.json"); + var response = + await client.GetAsync( + "https://github.com/Kxnrl/NetEase-Cloud-Music-DiscordRPC/raw/master/offset/offset.json"); response.EnsureSuccessStatusCode(); @@ -298,7 +357,9 @@ private static async Task GetOffsetsAsync() var r = MessageBox.Show(e.Message, "Failed to get offsets", MessageBoxButtons.RetryCancel, MessageBoxIcon.Error); if (r == DialogResult.Retry) + { goto retry; + } #if !DEBUG Environment.Exit(-1); diff --git a/NetEaseMusic-DiscordRPC/Properties/AssemblyInfo.cs b/NetEaseMusic-DiscordRPC/Properties/AssemblyInfo.cs index 113d465..4313b37 100644 --- a/NetEaseMusic-DiscordRPC/Properties/AssemblyInfo.cs +++ b/NetEaseMusic-DiscordRPC/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ // 可以指定所有值,也可以使用以下所示的 "*" 预置版本号和修订号 // 方法是按如下所示使用“*”: : // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("2.2.1")] -[assembly: AssemblyFileVersion("2.2.1")] +[assembly: AssemblyVersion("2.3.0")] +[assembly: AssemblyFileVersion("2.3.0")] diff --git a/NetEaseMusic-DiscordRPC/json.cs b/NetEaseMusic-DiscordRPC/json.cs index 4b3b577..8471b6d 100644 --- a/NetEaseMusic-DiscordRPC/json.cs +++ b/NetEaseMusic-DiscordRPC/json.cs @@ -1,4 +1,9 @@ using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; namespace NetEaseMusic_DiscordRPC { @@ -16,6 +21,76 @@ public struct Offset public int Length { get; set; } [JsonProperty("schedule")] public int Schedule { get; set; } + [JsonProperty("pointer")] + public int CachePointer { get; set; } } + public class NetEaseBaseModel + { + [JsonProperty("id")] + public uint Id { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + } + + public class NetEaseTrackModel : NetEaseBaseModel + { + public class NetEaseTrackAlbumModel : NetEaseBaseModel + { + [JsonProperty("picUrl")] + public string Cover { get; set; } + } + + public class NetEaseTrackArtists : NetEaseBaseModel + { + + } + + [JsonProperty("album")] + public NetEaseTrackAlbumModel Album { get; set; } + + [JsonProperty("artists")] + public List Artists { get; set; } + + /// + /// Duration in milliseconds + /// + [JsonProperty("duration")] + public uint Duration { get; set; } + } + + public class NetEaseSoundModel + { + [JsonProperty("track")] + public NetEaseTrackModel Track { get; set; } + + [JsonProperty("tid")] + public uint Id { get; set; } + } + + public static class NetEaseCacheManager + { + private static string[] Files = { "queue", "queue_backup", "history" }; + + public static NetEaseSoundModel GetSoundInfo(int tid) + { + foreach (var f in Files) + { + var path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "NetEase", "CloudMusic", "webdata", "file", f); + if (!File.Exists(path)) + { + continue; + } + var data = File.ReadAllText(path, Encoding.UTF8); + var json = JsonConvert.DeserializeObject(data); + var find = json.SingleOrDefault(x => x.Id == tid); + if (find != null) + { + return find; + } + } + + return null; + } + } } diff --git a/NetEaseMusic-DiscordRPC/packages.config b/NetEaseMusic-DiscordRPC/packages.config index 3e79058..4843af4 100644 --- a/NetEaseMusic-DiscordRPC/packages.config +++ b/NetEaseMusic-DiscordRPC/packages.config @@ -1,5 +1,5 @@  - + \ No newline at end of file diff --git a/README.md b/README.md index f3150fd..6d960af 100644 --- a/README.md +++ b/README.md @@ -20,10 +20,13 @@ Enables Discord [Rich Presence](https://discordapp.com/rich-presence) For Neteas ### Screenshot - + ### Changes log +#### 2.3 +- 新增网易云封面显示 +- 新增网易云听歌按钮 #### 2.2 - 添加QQ音乐Rpc #### 2.1 diff --git a/offset/offset.json b/offset/offset.json index 3c35340..9648537 100644 --- a/offset/offset.json +++ b/offset/offset.json @@ -61,6 +61,6 @@ }, { "version": "2.10.11.201538", - "offsets": { "length": 11717272, "schedule": 10986880 } + "offsets": { "length": 11717272, "schedule": 10986880, "pointer": 11713712 } } ]