diff --git a/DiscordInterface.cs b/DiscordInterface.cs index 1686d31..3c2621d 100644 --- a/DiscordInterface.cs +++ b/DiscordInterface.cs @@ -1,28 +1,134 @@ -using System.Runtime.InteropServices; +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Text; namespace DiscordInterface { - public class DiscordRPC + // stolen from https://github.com/discord/discord-rpc/blob/master/examples/button-clicker/Assets/DiscordRpc.cs + public class DiscordRpc { - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - public delegate void ReadyCallback(); + public static void ReadyCallback(ref DiscordUser connectedUser) { Callbacks.readyCallback(ref connectedUser); } + public delegate void OnReadyInfo(ref DiscordUser connectedUser); - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - public delegate void DisconnectedCallback(int errorCode, string message); + public static void DisconnectedCallback(int errorCode, string message) { Callbacks.disconnectedCallback(errorCode, message); } + public delegate void OnDisconnectedInfo(int errorCode, string message); - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - public delegate void ErrorCallback(int errorCode, string message); + public static void ErrorCallback(int errorCode, string message) { Callbacks.errorCallback(errorCode, message); } + public delegate void OnErrorInfo(int errorCode, string message); - public struct DiscordEventHandlers + public static void JoinCallback(string secret) { Callbacks.joinCallback(secret); } + public delegate void OnJoinInfo(string secret); + + public static void SpectateCallback(string secret) { Callbacks.spectateCallback(secret); } + public delegate void OnSpectateInfo(string secret); + + public static void RequestCallback(ref DiscordUser request) { Callbacks.requestCallback(ref request); } + public delegate void OnRequestInfo(ref DiscordUser request); + + static EventHandlers Callbacks { get; set; } + + public struct EventHandlers + { + public OnReadyInfo readyCallback; + public OnDisconnectedInfo disconnectedCallback; + public OnErrorInfo errorCallback; + public OnJoinInfo joinCallback; + public OnSpectateInfo spectateCallback; + public OnRequestInfo requestCallback; + } + + [Serializable, StructLayout(LayoutKind.Sequential)] + public struct RichPresenceStruct { - public ReadyCallback readyCallback; - public DisconnectedCallback disconnectedCallback; - public ErrorCallback errorCallback; + public IntPtr state; /* max 128 bytes */ + public IntPtr details; /* max 128 bytes */ + public long startTimestamp; + public long endTimestamp; + public IntPtr largeImageKey; /* max 32 bytes */ + public IntPtr largeImageText; /* max 128 bytes */ + public IntPtr smallImageKey; /* max 32 bytes */ + public IntPtr smallImageText; /* max 128 bytes */ + public IntPtr partyId; /* max 128 bytes */ + public int partySize; + public int partyMax; + public int partyPrivacy; + public IntPtr matchSecret; /* max 128 bytes */ + public IntPtr joinSecret; /* max 128 bytes */ + public IntPtr spectateSecret; /* max 128 bytes */ + public bool instance; } - [System.Serializable] - public struct RichPresence + [Serializable] + public struct DiscordUser { + public string userId; + public string username; + public string discriminator; + public string avatar; + } + + public enum Reply + { + No = 0, + Yes = 1, + Ignore = 2 + } + + public enum PartyPrivacy + { + Private = 0, + Public = 1 + } + + public static void Initialize(string applicationId, ref EventHandlers handlers, bool autoRegister, string optionalSteamId) + { + Callbacks = handlers; + + EventHandlers staticEventHandlers = new EventHandlers(); + staticEventHandlers.readyCallback += DiscordRpc.ReadyCallback; + staticEventHandlers.disconnectedCallback += DiscordRpc.DisconnectedCallback; + staticEventHandlers.errorCallback += DiscordRpc.ErrorCallback; + staticEventHandlers.joinCallback += DiscordRpc.JoinCallback; + staticEventHandlers.spectateCallback += DiscordRpc.SpectateCallback; + staticEventHandlers.requestCallback += DiscordRpc.RequestCallback; + + InitializeInternal(applicationId, ref staticEventHandlers, autoRegister, optionalSteamId); + } + + [DllImport("win32-discord-rpc", EntryPoint = "Discord_Initialize", CallingConvention = CallingConvention.Cdecl)] + static extern void InitializeInternal(string applicationId, ref EventHandlers handlers, bool autoRegister, string optionalSteamId); + + [DllImport("win32-discord-rpc", EntryPoint = "Discord_Shutdown", CallingConvention = CallingConvention.Cdecl)] + public static extern void Shutdown(); + + [DllImport("win32-discord-rpc", EntryPoint = "Discord_RunCallbacks", CallingConvention = CallingConvention.Cdecl)] + public static extern void RunCallbacks(); + + [DllImport("win32-discord-rpc", EntryPoint = "Discord_UpdatePresence", CallingConvention = CallingConvention.Cdecl)] + private static extern void UpdatePresenceNative(ref RichPresenceStruct presence); + + [DllImport("win32-discord-rpc", EntryPoint = "Discord_ClearPresence", CallingConvention = CallingConvention.Cdecl)] + public static extern void ClearPresence(); + + [DllImport("win32-discord-rpc", EntryPoint = "Discord_Respond", CallingConvention = CallingConvention.Cdecl)] + public static extern void Respond(string userId, Reply reply); + + [DllImport("win32-discord-rpc", EntryPoint = "Discord_UpdateHandlers", CallingConvention = CallingConvention.Cdecl)] + public static extern void UpdateHandlers(ref EventHandlers handlers); + + public static void UpdatePresence(RichPresence presence) + { + var presencestruct = presence.GetStruct(); + UpdatePresenceNative(ref presencestruct); + presence.FreeMem(); + } + + public class RichPresence + { + private RichPresenceStruct _presence; + private readonly List _buffers = new List(10); + public string state; /* max 128 bytes */ public string details; /* max 128 bytes */ public long startTimestamp; @@ -34,22 +140,89 @@ public struct RichPresence public string partyId; /* max 128 bytes */ public int partySize; public int partyMax; + public PartyPrivacy partyPrivacy; public string matchSecret; /* max 128 bytes */ public string joinSecret; /* max 128 bytes */ public string spectateSecret; /* max 128 bytes */ public bool instance; - } - [DllImport("win32-discord-rpc", EntryPoint = "Discord_Initialize", CallingConvention = CallingConvention.Cdecl)] - public static extern void Initialize(string applicationId, ref DiscordEventHandlers handlers, bool autoRegister, string optionalSteamId); + /// + /// Get the reprensentation of this instance + /// + /// reprensentation of this instance + internal RichPresenceStruct GetStruct() + { + if (_buffers.Count > 0) + { + FreeMem(); + } - [DllImport("win32-discord-rpc", EntryPoint = "Discord_UpdatePresence", CallingConvention = CallingConvention.Cdecl)] - public static extern void UpdatePresence(ref RichPresence presence); + _presence.state = StrToPtr(state); + _presence.details = StrToPtr(details); + _presence.startTimestamp = startTimestamp; + _presence.endTimestamp = endTimestamp; + _presence.largeImageKey = StrToPtr(largeImageKey); + _presence.largeImageText = StrToPtr(largeImageText); + _presence.smallImageKey = StrToPtr(smallImageKey); + _presence.smallImageText = StrToPtr(smallImageText); + _presence.partyId = StrToPtr(partyId); + _presence.partySize = partySize; + _presence.partyMax = partyMax; + _presence.partyPrivacy = (int)partyPrivacy; + _presence.matchSecret = StrToPtr(matchSecret); + _presence.joinSecret = StrToPtr(joinSecret); + _presence.spectateSecret = StrToPtr(spectateSecret); + _presence.instance = instance; - [DllImport("win32-discord-rpc", EntryPoint = "Discord_RunCallbacks", CallingConvention = CallingConvention.Cdecl)] - public static extern void RunCallbacks(); + return _presence; + } - [DllImport("win32-discord-rpc", EntryPoint = "Discord_Shutdown", CallingConvention = CallingConvention.Cdecl)] - public static extern void Shutdown(); + /// + /// Returns a pointer to a representation of the given string with a size of maxbytes + /// + /// String to convert + /// Pointer to the UTF-8 representation of + private IntPtr StrToPtr(string input) + { + if (string.IsNullOrEmpty(input)) return IntPtr.Zero; + var convbytecnt = Encoding.UTF8.GetByteCount(input); + var buffer = Marshal.AllocHGlobal(convbytecnt + 1); + for (int i = 0; i < convbytecnt + 1; i++) + { + Marshal.WriteByte(buffer, i, 0); + } + _buffers.Add(buffer); + Marshal.Copy(Encoding.UTF8.GetBytes(input), 0, buffer, convbytecnt); + return buffer; + } + + /// + /// Convert string to UTF-8 and add null termination + /// + /// string to convert + /// UTF-8 representation of with added null termination + private static string StrToUtf8NullTerm(string toconv) + { + var str = toconv.Trim(); + var bytes = Encoding.Default.GetBytes(str); + if (bytes.Length > 0 && bytes[bytes.Length - 1] != 0) + { + str += "\0\0"; + } + return Encoding.UTF8.GetString(Encoding.UTF8.GetBytes(str)); + } + + /// + /// Free the allocated memory for conversion to + /// + internal void FreeMem() + { + for (var i = _buffers.Count - 1; i >= 0; i--) + { + Marshal.FreeHGlobal(_buffers[i]); + _buffers.RemoveAt(i); + } + } + } } -} \ No newline at end of file +} diff --git a/Util.cs b/Util.cs deleted file mode 100644 index 3f5ab6d..0000000 --- a/Util.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using System.Text; - -namespace Util -{ - public class Utility - { - [DllImport("kernel32.dll")] - private static extern Int32 WideCharToMultiByte(UInt32 CodePage, UInt32 dwFlags, [MarshalAs(UnmanagedType.LPWStr)] String lpWideCharStr, Int32 cchWideChar, [Out, MarshalAs(UnmanagedType.LPStr)] StringBuilder lpMultiByteStr, Int32 cbMultiByte, IntPtr lpDefaultChar, IntPtr lpUsedDefaultChar); - - public static string Utf16ToUtf8(string utf16String) - { - Int32 iNewDataLen = WideCharToMultiByte(Convert.ToUInt32(Encoding.UTF8.CodePage), 0, utf16String, utf16String.Length, null, 0, IntPtr.Zero, IntPtr.Zero); - if (iNewDataLen > 1) - { - StringBuilder utf8String = new StringBuilder(iNewDataLen); - WideCharToMultiByte(Convert.ToUInt32(Encoding.UTF8.CodePage), 0, utf16String, -1, utf8String, utf8String.Capacity, IntPtr.Zero, IntPtr.Zero); - - return utf8String.ToString(); - } - else - { - return String.Empty; - } - } - } -} diff --git a/mb_DiscordRichPresence.cs b/mb_DiscordRichPresence.cs index c5f3a64..c33e0ec 100644 --- a/mb_DiscordRichPresence.cs +++ b/mb_DiscordRichPresence.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using DiscordInterface; -using Util; namespace MusicBeePlugin { @@ -13,6 +12,7 @@ public partial class Plugin { private MusicBeeApiInterface mbApiInterface; private PluginInfo about = new PluginInfo(); + private DiscordRpc.RichPresence presence = new DiscordRpc.RichPresence(); public PluginInfo Initialise(IntPtr apiInterfacePtr) { @@ -33,81 +33,54 @@ public PluginInfo Initialise(IntPtr apiInterfacePtr) about.ConfigurationPanelHeight = 0; // height in pixels that musicbee should reserve in a panel for config settings. When set, a handle to an empty panel will be passed to the Configure function InitialiseDiscord(); - + return about; } private void InitialiseDiscord() { - DiscordRPC.DiscordEventHandlers handlers = new DiscordRPC.DiscordEventHandlers(); - handlers.readyCallback = HandleReadyCallback; - handlers.errorCallback = HandleErrorCallback; - handlers.disconnectedCallback = HandleDisconnectedCallback; - // Kuunikal's dev app client ID - DiscordRPC.Initialize("519949979176140821", ref handlers, true, null); + var handlers = new DiscordRpc.EventHandlers(); + + handlers.readyCallback += HandleReadyCallback; + handlers.errorCallback += HandleErrorCallback; + handlers.disconnectedCallback += HandleDisconnectedCallback; + + DiscordRpc.Initialize("519949979176140821", ref handlers, true, null); + } - private void HandleReadyCallback() { } + private void HandleReadyCallback(ref DiscordRpc.DiscordUser user) { } private void HandleErrorCallback(int errorCode, string message) { } private void HandleDisconnectedCallback(int errorCode, string message) { } private void UpdatePresence(string artist, string track, string album, Boolean playing, int index, int totalTracks) { - DiscordRPC.RichPresence presence = new DiscordRPC.RichPresence(); - string bitrate = mbApiInterface.NowPlaying_GetFileProperty(FilePropertyType.Bitrate); - string codec = this.mbApiInterface.NowPlaying_GetFileProperty(Plugin.FilePropertyType.Kind); - - - /* Discord RPC doesn't like strings that are only one character long, so I - add a space after each track to make sure it's over 1 character long */ - track = Utility.Utf16ToUtf8(track + " "); - artist = Utility.Utf16ToUtf8("by " + artist); // Next line, shows the artist - // There are characters at the end of each line which Discord renders poorly - // (side-effect of Utf8, I guess?) so we need touse a substring instead - presence.state = artist.Substring(0, artist.Length - 1); - presence.details = track.Substring(0, track.Length - 1) + "[" + mbApiInterface.NowPlaying_GetFileProperty(FilePropertyType.Duration) + "]"; - - // Hovering over the image presents the album name - presence.largeImageText = album; - - /* Next block is fetching the album image from Discord's - server. They don't allow spaces in their file names, so - we need to convert them into underscores. */ - - char[] albumArray = album.ToCharArray(); // Create a char array because we can't edit strings - - // Search album string for spaces - for (int i = 0; i < album.Length; i++) - { - // If the current character is a space, turn it into an underscore - if (album[i] == ' ') albumArray[i] = '_'; - // Otherwise, just continue on - else albumArray[i] = album[i]; - } - // Create a string from the array, in lowercase - string newAlbum = new String(albumArray).ToLower(); - // Set the album art to the manipulated album string. - presence.largeImageKey = "albumart"; + string codec = mbApiInterface.NowPlaying_GetFileProperty(Plugin.FilePropertyType.Kind); - // Set the small image to the playback status. + // Discord RPC doesn't like strings that are only one character long + // NOTE(yui): unsure if this ^ was talking about the old interface or the discord client, leaving it in just in case + if (track.Length <= 1) track += " "; + if (artist.Length <= 1) artist += " "; + presence.state = $"by {artist}"; + presence.details = $"{track} [{mbApiInterface.NowPlaying_GetFileProperty(FilePropertyType.Duration)}]"; + + // Hovering over the image presents the album name + presence.largeImageText = album; + + // Set the album art to the manipulated album string. + presence.largeImageKey = "albumart"; + + // Set the small image to the playback status. if (playing) { presence.smallImageKey = "playing"; - } - if (playing) - { presence.smallImageText = bitrate + "bps [" + codec + "]"; } - bool flag2 = !playing; - if (flag2) + else { presence.smallImageKey = "paused"; - } - bool flag3 = !playing; - if (flag3) - { presence.smallImageText = "Paused"; } @@ -117,7 +90,9 @@ we need to convert them into underscores. */ long now = (long)DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1)).TotalSeconds; long duration = this.mbApiInterface.NowPlaying_GetDuration() / 1000; long end = now + duration; + TimeSpan t = DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1)); + if (playing) { long pos = (this.mbApiInterface.Player_GetPosition() / 1000); @@ -127,9 +102,13 @@ we need to convert them into underscores. */ presence.endTimestamp = end - pos; } } + else + { + presence.startTimestamp = 0; + presence.endTimestamp = 0; + } - - DiscordRPC.UpdatePresence(ref presence); + DiscordRpc.UpdatePresence(presence); } public bool Configure(IntPtr panelHandle) @@ -152,7 +131,7 @@ public bool Configure(IntPtr panelHandle) } return false; } - + // called by MusicBee when the user clicks Apply or Save in the MusicBee Preferences screen. // its up to you to figure out whether anything has changed and needs updating public void SaveSettings() @@ -164,7 +143,7 @@ public void SaveSettings() // MusicBee is closing the plugin (plugin is being disabled by user or MusicBee is shutting down) public void Close(PluginCloseReason reason) { - DiscordRPC.Shutdown(); + DiscordRpc.Shutdown(); } // uninstall this plugin - clean up any persisted files @@ -181,13 +160,13 @@ public void ReceiveNotification(string sourceFileUrl, NotificationType type) string albumArtist = mbApiInterface.NowPlaying_GetFileTag(MetaDataType.AlbumArtist); string trackTitle = mbApiInterface.NowPlaying_GetFileTag(MetaDataType.TrackTitle); string album = mbApiInterface.NowPlaying_GetFileTag(MetaDataType.Album); - int position = mbApiInterface.Player_GetPosition(); - - string[] tracks = null; + int position = mbApiInterface.Player_GetPosition(); + + string[] tracks = null; mbApiInterface.NowPlayingList_QueryFilesEx(null, ref tracks); int index = Array.IndexOf(tracks, mbApiInterface.NowPlaying_GetFileUrl()); - - // Check if there isn't an artist for the current song. If so, replace it with "(unknown artist)". + + // Check if there isn't an artist for the current song. If so, replace it with "(unknown artist)". if (string.IsNullOrEmpty(artist)) { if (!string.IsNullOrEmpty(albumArtist)) @@ -224,6 +203,6 @@ public void ReceiveNotification(string sourceFileUrl, NotificationType type) UpdatePresence(artist, trackTitle, album, mbApiInterface.Player_GetPlayState() == PlayState.Playing ? true : false, index + 1, tracks.Length); break; } - } + } } -} \ No newline at end of file +} diff --git a/mb_DiscordRichPresence.csproj b/mb_DiscordRichPresence.csproj index 6e9815b..76ff29c 100644 --- a/mb_DiscordRichPresence.csproj +++ b/mb_DiscordRichPresence.csproj @@ -81,7 +81,6 @@ - @@ -120,4 +119,4 @@ --> - \ No newline at end of file +