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