First Commit

This commit is contained in:
Evann Regnault 2024-04-02 03:11:40 +02:00
commit 279f208c2d
15 changed files with 949 additions and 0 deletions

20
.drone.yml Normal file
View 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
View file

@ -0,0 +1,3 @@
bin/
obj/
.vs/

25
AppleMusicRPC.sln Normal file
View 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

View 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>

View 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
View 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
View 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());
}
}
}

View 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));
}
}
}
}

View 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
View 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
View 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;
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

157
AppleMusicRPC/Scrapper.cs Normal file
View 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;
}
}
}

View 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();
}
}
}

View 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
};
}
}
}