First Commit
This commit is contained in:
commit
279f208c2d
15 changed files with 949 additions and 0 deletions
20
.drone.yml
Normal file
20
.drone.yml
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
kind: pipeline
|
||||||
|
type: docker
|
||||||
|
name: build
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Build dotnet image
|
||||||
|
image: mcr.microsoft.com/dotnet/sdk:8.0
|
||||||
|
commands:
|
||||||
|
- apt-get update && apt-get install -y zip
|
||||||
|
- dotnet build . -r Release
|
||||||
|
- zip -r build.zip AppleMusicRPC/bin/Release/net8.0-windows10.0.22000.0
|
||||||
|
|
||||||
|
|
||||||
|
- name: gitea_release
|
||||||
|
image: plugins/gitea-release
|
||||||
|
settings:
|
||||||
|
api_key:
|
||||||
|
from_secret: RELEASE_KEY
|
||||||
|
base_url: https://forgejo.regnault.dev
|
||||||
|
files: build.zip
|
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
.vs/
|
25
AppleMusicRPC.sln
Normal file
25
AppleMusicRPC.sln
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio Version 17
|
||||||
|
VisualStudioVersion = 17.9.34723.18
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AppleMusicRPC", "AppleMusicRPC\AppleMusicRPC.csproj", "{10830284-9AF1-40E0-8FE7-0A51F222A402}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{10830284-9AF1-40E0-8FE7-0A51F222A402}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{10830284-9AF1-40E0-8FE7-0A51F222A402}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{10830284-9AF1-40E0-8FE7-0A51F222A402}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{10830284-9AF1-40E0-8FE7-0A51F222A402}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
|
SolutionGuid = {94D0E4DD-4A9E-4BEE-B135-3AE648854766}
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
36
AppleMusicRPC/AppleMusicRPC.csproj
Normal file
36
AppleMusicRPC/AppleMusicRPC.csproj
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>WinExe</OutputType>
|
||||||
|
<TargetFramework>net8.0-windows10.0.22000.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<UseWPF>true</UseWPF>
|
||||||
|
<EnableWindowsTargeting>true</EnableWindowsTargeting>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
|
||||||
|
<PackageReference Include="FlaUI.UIA3" Version="4.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="8.0.3" />
|
||||||
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
|
<PackageReference Include="System.Drawing.Common" Version="8.0.3" />
|
||||||
|
<PackageReference Include="WebSocketSharp.Standard" Version="1.0.3" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Update="Properties\Resources.Designer.cs">
|
||||||
|
<DesignTime>True</DesignTime>
|
||||||
|
<AutoGen>True</AutoGen>
|
||||||
|
<DependentUpon>Resources.resx</DependentUpon>
|
||||||
|
</Compile>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<EmbeddedResource Update="Properties\Resources.resx">
|
||||||
|
<Generator>PublicResXFileCodeGenerator</Generator>
|
||||||
|
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
||||||
|
</EmbeddedResource>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
4
AppleMusicRPC/AppleMusicRPC.csproj.user
Normal file
4
AppleMusicRPC/AppleMusicRPC.csproj.user
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<PropertyGroup />
|
||||||
|
</Project>
|
84
AppleMusicRPC/Payload.cs
Normal file
84
AppleMusicRPC/Payload.cs
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
|
||||||
|
namespace AppleMusicRPC
|
||||||
|
{
|
||||||
|
public class Payload
|
||||||
|
{
|
||||||
|
private string? _playerStateValue;
|
||||||
|
private double _endTimeValue = -1;
|
||||||
|
private double _duration = -1;
|
||||||
|
|
||||||
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
|
public static class ResponseTypes
|
||||||
|
{
|
||||||
|
public const string Response = "res";
|
||||||
|
public const string Event = "event";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class PlayingStatuses
|
||||||
|
{
|
||||||
|
public const string Playing = "playing";
|
||||||
|
public const string NotStarted = "not_started";
|
||||||
|
public const string Paused = "paused";
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? title { get; set; }
|
||||||
|
public string? album { get; set; }
|
||||||
|
public string? artist { get; set; }
|
||||||
|
public string? thumbnailPath { get; set; }
|
||||||
|
public string? type { get; set; }
|
||||||
|
|
||||||
|
public string? playerState
|
||||||
|
{
|
||||||
|
get => _playerStateValue;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value == _playerStateValue) return;
|
||||||
|
_playerStateValue = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public double endTime
|
||||||
|
{
|
||||||
|
get => _endTimeValue;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value == _endTimeValue) return;
|
||||||
|
_endTimeValue = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public double duration
|
||||||
|
{
|
||||||
|
get => _duration;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value == _duration) return;
|
||||||
|
_duration = value;
|
||||||
|
NotifyPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
|
||||||
|
{
|
||||||
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResetToInitialState()
|
||||||
|
{
|
||||||
|
title = null;
|
||||||
|
artist = null;
|
||||||
|
album = null;
|
||||||
|
thumbnailPath = null;
|
||||||
|
playerState = PlayingStatuses.NotStarted;
|
||||||
|
endTime = -1;
|
||||||
|
duration = -1;
|
||||||
|
type = ResponseTypes.Event;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
AppleMusicRPC/Program.cs
Normal file
26
AppleMusicRPC/Program.cs
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
|
||||||
|
namespace AppleMusicRPC
|
||||||
|
{
|
||||||
|
internal class Program
|
||||||
|
{
|
||||||
|
|
||||||
|
|
||||||
|
[STAThread]
|
||||||
|
private static void Main()
|
||||||
|
{
|
||||||
|
|
||||||
|
if (System.Diagnostics.Process.GetProcessesByName(System.IO.Path.GetFileNameWithoutExtension(System.Reflection.Assembly.GetEntryAssembly()?.Location)).Length > 1) return;
|
||||||
|
|
||||||
|
_ = new Provider();
|
||||||
|
|
||||||
|
Application.Run(new ServiceApplicationContext());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
73
AppleMusicRPC/Properties/Resources.Designer.cs
generated
Normal file
73
AppleMusicRPC/Properties/Resources.Designer.cs
generated
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// <auto-generated>
|
||||||
|
// Ce code a été généré par un outil.
|
||||||
|
// Version du runtime :4.0.30319.42000
|
||||||
|
//
|
||||||
|
// Les modifications apportées à ce fichier peuvent provoquer un comportement incorrect et seront perdues si
|
||||||
|
// le code est régénéré.
|
||||||
|
// </auto-generated>
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
namespace AppleMusicRPC.Properties {
|
||||||
|
using System;
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Une classe de ressource fortement typée destinée, entre autres, à la consultation des chaînes localisées.
|
||||||
|
/// </summary>
|
||||||
|
// Cette classe a été générée automatiquement par la classe StronglyTypedResourceBuilder
|
||||||
|
// à l'aide d'un outil, tel que ResGen ou Visual Studio.
|
||||||
|
// Pour ajouter ou supprimer un membre, modifiez votre fichier .ResX, puis réexécutez ResGen
|
||||||
|
// avec l'option /str ou régénérez votre projet VS.
|
||||||
|
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
|
||||||
|
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||||
|
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||||
|
public class Resources {
|
||||||
|
|
||||||
|
private static global::System.Resources.ResourceManager resourceMan;
|
||||||
|
|
||||||
|
private static global::System.Globalization.CultureInfo resourceCulture;
|
||||||
|
|
||||||
|
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
|
||||||
|
internal Resources() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retourne l'instance ResourceManager mise en cache utilisée par cette classe.
|
||||||
|
/// </summary>
|
||||||
|
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||||
|
public static global::System.Resources.ResourceManager ResourceManager {
|
||||||
|
get {
|
||||||
|
if (object.ReferenceEquals(resourceMan, null)) {
|
||||||
|
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("AppleMusicRPC.Properties.Resources", typeof(Resources).Assembly);
|
||||||
|
resourceMan = temp;
|
||||||
|
}
|
||||||
|
return resourceMan;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Remplace la propriété CurrentUICulture du thread actuel pour toutes
|
||||||
|
/// les recherches de ressources à l'aide de cette classe de ressource fortement typée.
|
||||||
|
/// </summary>
|
||||||
|
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||||
|
public static global::System.Globalization.CultureInfo Culture {
|
||||||
|
get {
|
||||||
|
return resourceCulture;
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
resourceCulture = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Recherche une ressource localisée de type System.Drawing.Icon semblable à (Icône).
|
||||||
|
/// </summary>
|
||||||
|
public static System.Drawing.Icon TrayIcon {
|
||||||
|
get {
|
||||||
|
object obj = ResourceManager.GetObject("TrayIcon", resourceCulture);
|
||||||
|
return ((System.Drawing.Icon)(obj));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
124
AppleMusicRPC/Properties/Resources.resx
Normal file
124
AppleMusicRPC/Properties/Resources.resx
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<root>
|
||||||
|
<!--
|
||||||
|
Microsoft ResX Schema
|
||||||
|
|
||||||
|
Version 2.0
|
||||||
|
|
||||||
|
The primary goals of this format is to allow a simple XML format
|
||||||
|
that is mostly human readable. The generation and parsing of the
|
||||||
|
various data types are done through the TypeConverter classes
|
||||||
|
associated with the data types.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
... ado.net/XML headers & schema ...
|
||||||
|
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||||
|
<resheader name="version">2.0</resheader>
|
||||||
|
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||||
|
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||||
|
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||||
|
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||||
|
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||||
|
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||||
|
</data>
|
||||||
|
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||||
|
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||||
|
<comment>This is a comment</comment>
|
||||||
|
</data>
|
||||||
|
|
||||||
|
There are any number of "resheader" rows that contain simple
|
||||||
|
name/value pairs.
|
||||||
|
|
||||||
|
Each data row contains a name, and value. The row also contains a
|
||||||
|
type or mimetype. Type corresponds to a .NET class that support
|
||||||
|
text/value conversion through the TypeConverter architecture.
|
||||||
|
Classes that don't support this are serialized and stored with the
|
||||||
|
mimetype set.
|
||||||
|
|
||||||
|
The mimetype is used for serialized objects, and tells the
|
||||||
|
ResXResourceReader how to depersist the object. This is currently not
|
||||||
|
extensible. For a given mimetype the value must be set accordingly:
|
||||||
|
|
||||||
|
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||||
|
that the ResXResourceWriter will generate, however the reader can
|
||||||
|
read any of the formats listed below.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.binary.base64
|
||||||
|
value : The object must be serialized with
|
||||||
|
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.soap.base64
|
||||||
|
value : The object must be serialized with
|
||||||
|
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||||
|
value : The object must be serialized into a byte array
|
||||||
|
: using a System.ComponentModel.TypeConverter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
-->
|
||||||
|
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||||
|
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||||
|
<xsd:element name="root" msdata:IsDataSet="true">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:choice maxOccurs="unbounded">
|
||||||
|
<xsd:element name="metadata">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="assembly">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:attribute name="alias" type="xsd:string" />
|
||||||
|
<xsd:attribute name="name" type="xsd:string" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="data">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="resheader">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:choice>
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:schema>
|
||||||
|
<resheader name="resmimetype">
|
||||||
|
<value>text/microsoft-resx</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="version">
|
||||||
|
<value>2.0</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="reader">
|
||||||
|
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="writer">
|
||||||
|
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
|
||||||
|
<data name="TrayIcon" type="System.Resources.ResXFileRef, System.Windows.Forms">
|
||||||
|
<value>..\Resources\TrayIcon.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
|
||||||
|
</data>
|
||||||
|
</root>
|
154
AppleMusicRPC/Provider.cs
Normal file
154
AppleMusicRPC/Provider.cs
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Windows.Media.Control;
|
||||||
|
|
||||||
|
namespace AppleMusicRPC
|
||||||
|
{
|
||||||
|
internal class Provider
|
||||||
|
{
|
||||||
|
private GlobalSystemMediaTransportControlsSessionManager? _sessionManager;
|
||||||
|
private GlobalSystemMediaTransportControlsSession? _ampSession;
|
||||||
|
|
||||||
|
private const string AMPModelId = "AppleInc.AppleMusicWin";
|
||||||
|
|
||||||
|
private readonly Payload _payload;
|
||||||
|
private SemaphoreSlim semaphore = new SemaphoreSlim(1);
|
||||||
|
|
||||||
|
public Provider()
|
||||||
|
{
|
||||||
|
_payload = new Payload();
|
||||||
|
|
||||||
|
UpdateSessionManager();
|
||||||
|
UpdateAMPSession();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void UpdateSessionManager()
|
||||||
|
{
|
||||||
|
if (_sessionManager != null) return;
|
||||||
|
|
||||||
|
_sessionManager = await GlobalSystemMediaTransportControlsSessionManager.RequestAsync();
|
||||||
|
_sessionManager.SessionsChanged += OnSessionsChanged;
|
||||||
|
GetAMPSession();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateAMPSession()
|
||||||
|
{
|
||||||
|
UpdateSessionManager();
|
||||||
|
GetAMPSession();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GetAMPSession()
|
||||||
|
{
|
||||||
|
if (_sessionManager != null)
|
||||||
|
{
|
||||||
|
GlobalSystemMediaTransportControlsSession newSession = FindAMPSession();
|
||||||
|
SetAMPSession(newSession);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_ampSession != null)
|
||||||
|
{
|
||||||
|
_ampSession.MediaPropertiesChanged += OnMediaPropertiesChanged;
|
||||||
|
_ampSession.PlaybackInfoChanged += OnPlaybackInfoChanged;
|
||||||
|
OnMediaPropertiesChanged(_ampSession, null);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_payload.ResetToInitialState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPlaybackInfoChanged(GlobalSystemMediaTransportControlsSession? sender, PlaybackInfoChangedEventArgs? args)
|
||||||
|
{
|
||||||
|
Thread.Sleep(1000);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_ampSession == null)
|
||||||
|
{
|
||||||
|
_payload.ResetToInitialState();
|
||||||
|
RPCManager.SetActivity(_payload);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
var playbackInfo = _ampSession.GetPlaybackInfo();
|
||||||
|
var timelineProperties = _ampSession.GetTimelineProperties();
|
||||||
|
_payload.playerState = playbackInfo.PlaybackStatus.ToString().ToLower() == Payload.PlayingStatuses.Playing
|
||||||
|
? Payload.PlayingStatuses.Playing : Payload.PlayingStatuses.Paused;
|
||||||
|
|
||||||
|
_payload.endTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() +
|
||||||
|
(timelineProperties.EndTime.TotalMilliseconds - timelineProperties.Position.TotalMilliseconds);
|
||||||
|
_payload.duration = timelineProperties.EndTime.TotalSeconds - timelineProperties.StartTime.TotalSeconds;
|
||||||
|
|
||||||
|
RPCManager.SetActivity(_payload);
|
||||||
|
}
|
||||||
|
catch (Exception _)
|
||||||
|
{
|
||||||
|
_payload.ResetToInitialState();
|
||||||
|
RPCManager.SetActivity(_payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnMediaPropertiesChanged(GlobalSystemMediaTransportControlsSession? sender, MediaPropertiesChangedEventArgs? args)
|
||||||
|
{
|
||||||
|
await semaphore.WaitAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await sender?.TryGetMediaPropertiesAsync();
|
||||||
|
}
|
||||||
|
catch (Exception _)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
var songInfos = ClientScrapper.GetInfos();
|
||||||
|
if (songInfos != null)
|
||||||
|
{
|
||||||
|
_payload.artist = songInfos.SongArtist;
|
||||||
|
_payload.album = songInfos.SongAlbum;
|
||||||
|
_payload.title = songInfos.SongName;
|
||||||
|
OnPlaybackInfoChanged(null, null);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_payload.ResetToInitialState();
|
||||||
|
RPCManager.SetActivity(_payload);
|
||||||
|
}
|
||||||
|
semaphore.Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetAMPSession(GlobalSystemMediaTransportControlsSession newSession)
|
||||||
|
{
|
||||||
|
if (newSession == null && _ampSession != null)
|
||||||
|
{
|
||||||
|
_ampSession = null;
|
||||||
|
}
|
||||||
|
else if (_ampSession == null)
|
||||||
|
{
|
||||||
|
_ampSession = newSession;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private GlobalSystemMediaTransportControlsSession FindAMPSession()
|
||||||
|
{
|
||||||
|
IReadOnlyList<GlobalSystemMediaTransportControlsSession> sessions = _sessionManager.GetSessions();
|
||||||
|
foreach (GlobalSystemMediaTransportControlsSession session in sessions)
|
||||||
|
{
|
||||||
|
if (session.SourceAppUserModelId.Contains(AMPModelId))
|
||||||
|
{
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSessionsChanged(object sender, SessionsChangedEventArgs e)
|
||||||
|
{
|
||||||
|
UpdateAMPSession();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<GlobalSystemMediaTransportControlsSessionMediaProperties>? GetMediaProperties(GlobalSystemMediaTransportControlsSession AMPSession) =>
|
||||||
|
AMPSession == null ? null : await AMPSession.TryGetMediaPropertiesAsync();
|
||||||
|
}
|
||||||
|
}
|
130
AppleMusicRPC/RPCManager.cs
Normal file
130
AppleMusicRPC/RPCManager.cs
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using DiscordRPC;
|
||||||
|
|
||||||
|
namespace AppleMusicRPC
|
||||||
|
{
|
||||||
|
internal class RPCManager
|
||||||
|
{
|
||||||
|
|
||||||
|
private static DiscordRpcClient rpcClient { get; set; }
|
||||||
|
|
||||||
|
private static DiscordRpcClient GetClient()
|
||||||
|
{
|
||||||
|
if (rpcClient == null)
|
||||||
|
{
|
||||||
|
rpcClient = new DiscordRpcClient("773825528921849856");
|
||||||
|
rpcClient.Initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
return rpcClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Assets BuildAssetsFromPayload(Payload payload, TrackExtras extras)
|
||||||
|
{
|
||||||
|
return new Assets
|
||||||
|
{
|
||||||
|
LargeImageKey = extras.ArtworkUrl ?? "appicon",
|
||||||
|
LargeImageText = payload.album
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Button[] BuildButtonsFromPayload(Payload payload, TrackExtras extras)
|
||||||
|
{
|
||||||
|
var Buttons = new List<Button>();
|
||||||
|
|
||||||
|
|
||||||
|
// Apple Music
|
||||||
|
if (extras.ItunesUrl != null)
|
||||||
|
{
|
||||||
|
Buttons.Add(new Button
|
||||||
|
{
|
||||||
|
Label = "Play on Apple Music",
|
||||||
|
Url = extras.ItunesUrl,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Uri uri;
|
||||||
|
string query;
|
||||||
|
|
||||||
|
////Spotify
|
||||||
|
// query = $"{payload.artist} {payload.title}";
|
||||||
|
// uri = new Uri($"https://open.spotify.com/search/{query}?si");
|
||||||
|
// if (uri.AbsolutePath.Length <= 512)
|
||||||
|
// {
|
||||||
|
// Buttons.Add(new Button
|
||||||
|
// {
|
||||||
|
// Label = "Search on Spotify",
|
||||||
|
// Url = uri.AbsoluteUri,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
//Youtube
|
||||||
|
query = $"{payload.artist.Replace("#", "%23").Replace("&", "%26")} {payload.title.Replace("#", "%23").Replace("&", "%26")}";
|
||||||
|
uri = new Uri($"https://music.youtube.com/search?q={query}");
|
||||||
|
if (uri.AbsolutePath.Length <= 512)
|
||||||
|
{
|
||||||
|
Buttons.Add(new Button
|
||||||
|
{
|
||||||
|
Label = "Search on Youtube",
|
||||||
|
Url = uri.AbsoluteUri,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Buttons.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RichPresence BuildPlayingPresenceFromPayload(Payload payload, TrackExtras extras)
|
||||||
|
{
|
||||||
|
var assets = BuildAssetsFromPayload(payload, extras);
|
||||||
|
var buttons = BuildButtonsFromPayload(payload, extras);
|
||||||
|
|
||||||
|
return new RichPresence
|
||||||
|
{
|
||||||
|
Buttons = buttons,
|
||||||
|
Assets = assets,
|
||||||
|
State = $"{payload.artist}",
|
||||||
|
Details = $"{payload.title}",
|
||||||
|
Timestamps = new Timestamps { EndUnixMilliseconds = (ulong?)payload.endTime }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RichPresence BuildPausedPresenceFromPayload(Payload payload, TrackExtras extras)
|
||||||
|
{
|
||||||
|
var assets = BuildAssetsFromPayload(payload, extras);
|
||||||
|
var buttons = BuildButtonsFromPayload(payload, extras);
|
||||||
|
|
||||||
|
return new RichPresence
|
||||||
|
{
|
||||||
|
Buttons = buttons,
|
||||||
|
Assets = assets,
|
||||||
|
State = $"{payload.artist} - {payload.title}",
|
||||||
|
Details = $"Paused"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task SetActivity(Payload? payload)
|
||||||
|
{
|
||||||
|
if (payload == null) return;
|
||||||
|
|
||||||
|
TrackExtras extras;
|
||||||
|
switch (payload.playerState)
|
||||||
|
{
|
||||||
|
case "playing":
|
||||||
|
extras = await TrackExtras.GetTrackExtras(payload.title, payload.artist, payload.album);
|
||||||
|
|
||||||
|
GetClient().SetPresence(BuildPlayingPresenceFromPayload(payload, extras));
|
||||||
|
break;
|
||||||
|
case "paused":
|
||||||
|
extras = await TrackExtras.GetTrackExtras(payload.title, payload.artist, payload.album);
|
||||||
|
GetClient().SetPresence(BuildPausedPresenceFromPayload(payload, extras));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
GetClient().ClearPresence();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
BIN
AppleMusicRPC/Resources/TrayIcon.ico
Normal file
BIN
AppleMusicRPC/Resources/TrayIcon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.7 KiB |
157
AppleMusicRPC/Scrapper.cs
Normal file
157
AppleMusicRPC/Scrapper.cs
Normal file
|
@ -0,0 +1,157 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using FlaUI.UIA3;
|
||||||
|
using FlaUI.Core.Conditions;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using FlaUI.Core.AutomationElements;
|
||||||
|
|
||||||
|
namespace AppleMusicRPC
|
||||||
|
{
|
||||||
|
|
||||||
|
internal class SongInfos
|
||||||
|
{
|
||||||
|
public string SongName { get; private set; }
|
||||||
|
public string SongArtist { get; private set; }
|
||||||
|
public string SongAlbum { get; private set; }
|
||||||
|
public string SongPerformer { get; private set; }
|
||||||
|
|
||||||
|
public SongInfos(string SongName, string SongArtist, string SongAlbum, string SongPerformer)
|
||||||
|
{
|
||||||
|
this.SongName = SongName;
|
||||||
|
this.SongArtist = SongArtist;
|
||||||
|
this.SongAlbum = SongAlbum;
|
||||||
|
this.SongPerformer = SongPerformer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class ClientScrapper
|
||||||
|
{
|
||||||
|
private static readonly Regex ComposerPerformerRegex = new Regex(@"By\s.*?\s\u2014", RegexOptions.Compiled);
|
||||||
|
|
||||||
|
public static SongInfos GetInfos()
|
||||||
|
{
|
||||||
|
var amProcesses = Process.GetProcessesByName("AppleMusic");
|
||||||
|
if (amProcesses.Length == 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var app = FlaUI.Core.Application.Attach(amProcesses[0].Id);
|
||||||
|
using (var automation = new UIA3Automation())
|
||||||
|
{
|
||||||
|
var window = app.GetMainWindow(automation);
|
||||||
|
var amWinTransportBar = FindFirstDescendantWithAutomationId(window, "TransportBar");
|
||||||
|
if (amWinTransportBar == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var amWinLCD = amWinTransportBar.FindFirstChild("LCD");
|
||||||
|
|
||||||
|
// song panel not initialised
|
||||||
|
if (amWinLCD == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var songFields = amWinLCD.FindAllChildren(new ConditionFactory(new UIA3PropertyLibrary()).ByAutomationId("myScrollViewer"));
|
||||||
|
|
||||||
|
if (songFields.Length != 2)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var songNameElement = songFields[0];
|
||||||
|
var songAlbumArtistElement = songFields[1];
|
||||||
|
|
||||||
|
if (songNameElement.BoundingRectangle.Bottom > songAlbumArtistElement.BoundingRectangle.Bottom)
|
||||||
|
{
|
||||||
|
songNameElement = songFields[1];
|
||||||
|
songAlbumArtistElement = songFields[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
string songName;
|
||||||
|
string songAlbumArtist;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
songName = songNameElement.Name;
|
||||||
|
songAlbumArtist = songAlbumArtistElement.Name;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
string songArtist = "";
|
||||||
|
string songAlbum = "";
|
||||||
|
string songPerformer = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var songInfo = ParseSongAlbumArtist(songAlbumArtist, false);
|
||||||
|
songArtist = songInfo.Item1;
|
||||||
|
songAlbum = songInfo.Item2;
|
||||||
|
songPerformer = songInfo.Item3;
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
return new SongInfos(songName, songArtist, songAlbum, songPerformer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static Tuple<string, string, string> ParseSongAlbumArtist(string songAlbumArtist, bool composerAsArtist)
|
||||||
|
{
|
||||||
|
string songArtist;
|
||||||
|
string songAlbum;
|
||||||
|
string songPerformer = null;
|
||||||
|
|
||||||
|
// some classical songs add "By " before the composer's name
|
||||||
|
var songComposerPerformer = ComposerPerformerRegex.Matches(songAlbumArtist);
|
||||||
|
if (songComposerPerformer.Count > 0)
|
||||||
|
{
|
||||||
|
var splitted = Regex.Split(songAlbumArtist, @" \u2014 ");
|
||||||
|
var songComposer = splitted[0].Remove(0, 3);
|
||||||
|
songPerformer = splitted[1];
|
||||||
|
songArtist = composerAsArtist ? songComposer : songPerformer;
|
||||||
|
songAlbum = splitted[2];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// U+2014 is the emdash used by the Apple Music app, not the standard "-" character on the keyboard!
|
||||||
|
var songSplit = Regex.Split(songAlbumArtist, @" \u2014 ");
|
||||||
|
if (songSplit.Length > 1)
|
||||||
|
{
|
||||||
|
songArtist = songSplit[0];
|
||||||
|
songAlbum = songSplit[1];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{ // no emdash, probably custom music
|
||||||
|
// TODO find a better way to handle this?
|
||||||
|
songArtist = songSplit[0];
|
||||||
|
songAlbum = songSplit[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new Tuple<string, string, string>(songArtist, songAlbum, songPerformer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AutomationElement FindFirstDescendantWithAutomationId(AutomationElement baseElement, string id)
|
||||||
|
{
|
||||||
|
List<AutomationElement> nodes = new List<AutomationElement>() { baseElement };
|
||||||
|
for (var i = 0; i < nodes.Count; i++)
|
||||||
|
{
|
||||||
|
var node = nodes[i];
|
||||||
|
if (node.Properties.AutomationId.IsSupported && node.AutomationId == id)
|
||||||
|
{
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
nodes.AddRange(node.FindAllChildren());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
31
AppleMusicRPC/ServiceApplicationContext.cs
Normal file
31
AppleMusicRPC/ServiceApplicationContext.cs
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
using System;
|
||||||
|
using System.Resources;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
using AppleMusicRPC.Properties;
|
||||||
|
|
||||||
|
namespace AppleMusicRPC
|
||||||
|
{
|
||||||
|
internal class ServiceApplicationContext : ApplicationContext
|
||||||
|
{
|
||||||
|
private NotifyIcon trayIcon;
|
||||||
|
|
||||||
|
public ServiceApplicationContext()
|
||||||
|
{
|
||||||
|
trayIcon = new NotifyIcon()
|
||||||
|
{
|
||||||
|
Icon = Resources.TrayIcon,
|
||||||
|
ContextMenuStrip = new ContextMenuStrip()
|
||||||
|
{
|
||||||
|
Items = { new ToolStripMenuItem("Exit", null, Exit) }
|
||||||
|
},
|
||||||
|
Visible = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
void Exit(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
trayIcon.Visible = false;
|
||||||
|
Application.Exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
82
AppleMusicRPC/TrackExtras.cs
Normal file
82
AppleMusicRPC/TrackExtras.cs
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Microsoft.AspNetCore.WebUtilities;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Serialization;
|
||||||
|
|
||||||
|
namespace AppleMusicRPC
|
||||||
|
{
|
||||||
|
internal class TrackExtras
|
||||||
|
{
|
||||||
|
|
||||||
|
protected class iTunesSearchResponse
|
||||||
|
{
|
||||||
|
[JsonProperty("resultCount")]
|
||||||
|
public int ResultCount { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("results")]
|
||||||
|
public ItunesSearchResult[] Results { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
protected class ItunesSearchResult
|
||||||
|
{
|
||||||
|
[JsonProperty("trackName")]
|
||||||
|
public string TrackName { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("collectionName")]
|
||||||
|
public string CollectionName { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("artworkUrl100")]
|
||||||
|
public string ArtworkUrl { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("trackViewUrl")]
|
||||||
|
public string TrackViewUrl { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public string ArtworkUrl { get; private set; }
|
||||||
|
public string ItunesUrl { get; private set; }
|
||||||
|
|
||||||
|
public static async Task<TrackExtras> GetTrackExtras(string? song, string? artist, string? album)
|
||||||
|
{
|
||||||
|
// GET JSON
|
||||||
|
var searchQuery = $"{song} {artist} {album}".Replace("*", "");
|
||||||
|
HttpClient httpClient = new HttpClient();
|
||||||
|
|
||||||
|
var query = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["media"] = "music",
|
||||||
|
["entity"] = "song",
|
||||||
|
["term"] = searchQuery.Replace("#", "%23").Replace("&", "%26")
|
||||||
|
};
|
||||||
|
|
||||||
|
var data = await httpClient.GetAsync(QueryHelpers.AddQueryString("https://itunes.apple.com/search", query!));
|
||||||
|
var response = JsonConvert.DeserializeObject<iTunesSearchResponse>(await data.Content.ReadAsStringAsync());
|
||||||
|
|
||||||
|
|
||||||
|
// Get Track
|
||||||
|
ItunesSearchResult? result = null;
|
||||||
|
if (response.ResultCount == 1)
|
||||||
|
{
|
||||||
|
result = response.Results[0];
|
||||||
|
}
|
||||||
|
else if (response.ResultCount > 1)
|
||||||
|
{
|
||||||
|
result = response.Results.FirstOrDefault(x =>
|
||||||
|
x.CollectionName.ToLower().Contains(album.ToLower())
|
||||||
|
&& x.TrackName.ToLower().Contains(song.ToLower())
|
||||||
|
) ?? response.Results[0];
|
||||||
|
}
|
||||||
|
else if (Regex.Match(album, @"\(.*\)").Success)
|
||||||
|
{
|
||||||
|
return await GetTrackExtras(song, artist, Regex.Replace(album, @"\(.*\)", ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TrackExtras
|
||||||
|
{
|
||||||
|
ArtworkUrl = result?.ArtworkUrl,
|
||||||
|
ItunesUrl = result?.TrackViewUrl
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue