Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented automatic album art #48

Merged
merged 9 commits into from
Sep 29, 2023
2 changes: 2 additions & 0 deletions RockSniffer/Configuration/RPCSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ namespace RockSniffer.Configuration
public class RPCSettings
{
public bool enabled = false;
public uint updatePeriodMs = 1000;
public string client_id = "573253140682375193";
public bool enableCoverArt = true;
}
}
120 changes: 120 additions & 0 deletions RockSniffer/RPC/AlbumArtResolver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
using Newtonsoft.Json;
using RockSnifferLib.Logging;
using RockSnifferLib.Sniffing;
using System;
using System.Collections.Concurrent;
using System.Collections.Specialized;
using System.IO;
using System.Net;
using System.Net.Http;

namespace RockSniffer.RPC
{
public class AlbumArtResolver
{
[Serializable]
private class AppleResult
{
public string artistName;
public string collectionName;
public string trackName;
public string artworkUrl100;
}

[Serializable]
private class AppleRequestResponse
{
public int resultCount;
public AppleResult[] results;
}

private HttpClient httpClient = new HttpClient();
private ConcurrentDictionary<string, (string URL, string DisplayText)?> cache = new ConcurrentDictionary<string, (string URL, string DisplayText)?>();


public AlbumArtResolver() {
httpClient.DefaultRequestHeaders.Add("User-Agent", "RockSniffer");
}

/// <summary>
/// Attempt to obtain album cover
/// </summary>
/// <param name="songInfo">Details about the given song</param>
public (string URL, string DisplayText)? Get(SongDetails songInfo)
{
string key = $"{songInfo.artistName}|{songInfo.albumName}";

cache.AddOrUpdate(key, (key) => GetFromAppleMusic(songInfo), (key, value) => value);

return cache[key];
}

/// <summary>
/// Attempt to obtain album cover from Apple Music
/// </summary>
/// <param name="songInfo">Details about the given song</param>
protected (string URL, string DisplayText)? GetFromAppleMusic(SongDetails songInfo)
{
string baseUrl = "https://itunes.apple.com/search";
NameValueCollection queryString = System.Web.HttpUtility.ParseQueryString(string.Empty);
queryString.Add("media", "music");
queryString.Add("limit", "25");
queryString.Add("term", $"{songInfo.artistName} {songInfo.songName}");

var webRequest = new HttpRequestMessage(HttpMethod.Get, baseUrl + "?" + queryString.ToString());
sdasda7777 marked this conversation as resolved.
Show resolved Hide resolved

HttpResponseMessage responseMessage = httpClient.Send(webRequest);

if (responseMessage.StatusCode != HttpStatusCode.OK)
return null;

using (var reader = new StreamReader(responseMessage.Content.ReadAsStream()))
{
// Attempt deserialization, return null on failure
AppleRequestResponse? responseOpt = null;
try
{
responseOpt = JsonConvert.DeserializeObject<AppleRequestResponse>(reader.ReadToEnd());
}
catch (Exception ex)
{
Logger.LogException(ex);
return null;
}

if (responseOpt is AppleRequestResponse response)
{
AppleResult? bestResult = null;

// Attempt to find a full match, or at least valid result
foreach (AppleResult appleResult in response.results)
{
if (appleResult.artistName != null && appleResult.collectionName != null && appleResult.trackName != null)
{
if (bestResult == null)
bestResult = appleResult;

if ((appleResult.artistName.Contains(songInfo.artistName, StringComparison.OrdinalIgnoreCase)
|| songInfo.artistName.Contains(appleResult.artistName, StringComparison.OrdinalIgnoreCase))
&& (appleResult.collectionName.Contains(songInfo.albumName, StringComparison.OrdinalIgnoreCase)
|| songInfo.albumName.Contains(appleResult.collectionName, StringComparison.OrdinalIgnoreCase)))
{
bestResult = appleResult;
break;
}
}
}

// If bestResult isn't null (is valid result, perhaps even full match), return the URL and description
if (bestResult is AppleResult bestResultOk)
{
return (bestResultOk.artworkUrl100, $"{bestResultOk.artistName} - {bestResultOk.collectionName} (art from Apple Music)");
}
}

// Return null in case response had no data or so
return null;
};
}
}
}
54 changes: 48 additions & 6 deletions RockSniffer/RPC/DiscordRPCHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Timers;

namespace RockSniffer.RPC
{
Expand All @@ -16,6 +17,10 @@ public class DiscordRPCHandler : IDisposable
private RSMemoryReadout readout;
private SnifferState state = SnifferState.NONE;
private SongDetails songdetails;
private AlbumArtResolver? albumArtResolver;

private System.Timers.Timer timer;
private readonly object membersLock = new object();

private readonly Dictionary<string, string> gcadeGames = new Dictionary<string, string>()
{
Expand All @@ -38,6 +43,11 @@ public DiscordRPCHandler(Sniffer sniffer)
{
client = new DiscordRpcClient(Program.config.rpcSettings.client_id);

if (Program.config.rpcSettings.enableCoverArt)
{
albumArtResolver = new AlbumArtResolver();
}

//Set the logger
client.Logger = new ConsoleLogger() { Level = LogLevel.Warning };

Expand All @@ -57,6 +67,19 @@ public DiscordRPCHandler(Sniffer sniffer)
sniffer.OnStateChanged += Sniffer_OnStateChanged;
sniffer.OnMemoryReadout += Sniffer_OnMemoryReadout;
sniffer.OnSongChanged += Sniffer_OnSongChanged;

// Set up presence update timer
timer = new System.Timers.Timer(Math.Max(250, Program.config.rpcSettings.updatePeriodMs));
timer.Elapsed += CondUpdatePresence;
timer.AutoReset = true;
timer.Enabled = true;
}

internal void CondUpdatePresence(Object source, ElapsedEventArgs e) {
lock (membersLock)
{
UpdatePresence();
}
}

internal void UpdatePresence()
Expand All @@ -70,6 +93,17 @@ internal void UpdatePresence()
//If we have a valid song and are playing a song
if ((songdetails != null && readout != null) && (state == SnifferState.SONG_STARTING || state == SnifferState.SONG_PLAYING || state == SnifferState.SONG_ENDING))
{
try
{
sdasda7777 marked this conversation as resolved.
Show resolved Hide resolved
// Get the appropriate album cover
if (albumArtResolver != null && albumArtResolver.Get(songdetails) is (string resURL, string resDisplayText) resultTuple)
{
rp.Assets.LargeImageKey = resURL;
rp.Assets.LargeImageText = resDisplayText.Substring(0, Math.Min(resDisplayText.Length, 128));
}
}
catch (Exception ex) { Logger.LogException(ex); }

//Get the arrangement based on the arrangement id
var arrangement = songdetails.arrangements.FirstOrDefault(x => x.arrangementID == readout.arrangementID);

Expand Down Expand Up @@ -118,6 +152,9 @@ internal void UpdatePresence()
rp.Assets.SmallImageText = $"{section.name} | {rp.Assets.SmallImageText}";
}
}

if (string.IsNullOrEmpty(rp.Assets.SmallImageKey) && rp.Assets.LargeImageKey != "rocksmith")
rp.Assets.SmallImageKey = "rocksmith";
}
else
{
Expand Down Expand Up @@ -188,20 +225,25 @@ internal void UpdatePresence()

private void Sniffer_OnSongChanged(object sender, RockSnifferLib.Events.OnSongChangedArgs e)
{
songdetails = e.songDetails;
UpdatePresence();
lock (membersLock) {
songdetails = e.songDetails;
}
}

private void Sniffer_OnMemoryReadout(object sender, RockSnifferLib.Events.OnMemoryReadoutArgs e)
{
readout = e.memoryReadout;
UpdatePresence();
lock (membersLock)
{
readout = e.memoryReadout;
}
}

private void Sniffer_OnStateChanged(object sender, RockSnifferLib.Events.OnStateChangedArgs e)
{
state = e.newState;
UpdatePresence();
lock (membersLock)
{
state = e.newState;
}
}

public void Dispose()
Expand Down
6 changes: 3 additions & 3 deletions RockSniffer/RockSniffer.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@
<ProjectReference Include="..\RockSnifferLib\RockSnifferLib.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="DiscordRichPresence" Version="1.0.175" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
<PackageReference Include="System.Data.SQLite.Core" Version="1.0.115.5" />
<PackageReference Include="System.Data.SQLite.Core" Version="1.0.118" />
</ItemGroup>
<Import Project="..\packages\System.Data.SQLite.Core.1.0.113.1\build\net46\System.Data.SQLite.Core.targets" Condition="Exists('..\packages\System.Data.SQLite.Core.1.0.113.1\build\net46\System.Data.SQLite.Core.targets')" />
</Project>