Updated `Updater.cs` to improve functionality for retrieving the currently playing track in Spotify. Added new methods for window title retrieval and utilized the `EnumWindows` function to filter and identify relevant Spotify track titles. Included P/Invoke declarations for Windows API functions to support these enhancements, along with a new method `GetWindowTitle` for better code organization.
326 lines
10 KiB
C#
326 lines
10 KiB
C#
using Dalamud.Plugin;
|
|
using Dalamud.Plugin.Ipc;
|
|
using Dalamud.Plugin.Services;
|
|
using Dalamud.Utility;
|
|
using Newtonsoft.Json;
|
|
using Scriban;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
using System.Timers;
|
|
|
|
namespace SpotifyHonorific.Updaters;
|
|
|
|
public class Updater : IDisposable
|
|
{
|
|
private Config Config { get; init; }
|
|
private IFramework Framework { get; init; }
|
|
private IDalamudPluginInterface PluginInterface { get; init; }
|
|
private IPluginLog PluginLog { get; init; }
|
|
private ICallGateSubscriber<int, string, object> SetCharacterTitleSubscriber { get; init; }
|
|
private ICallGateSubscriber<int, object> ClearCharacterTitleSubscriber { get; init; }
|
|
|
|
private Action? UpdateTitle { get; set; }
|
|
private string? UpdatedTitleJson { get; set; }
|
|
private UpdaterContext UpdaterContext { get; init; } = new();
|
|
|
|
private Timer MediaCheckTimer { get; init; }
|
|
private MediaActivity? CurrentMediaActivity { get; set; }
|
|
|
|
public Updater(Config config, IFramework framwork, IDalamudPluginInterface pluginInterface, IPluginLog pluginLog)
|
|
{
|
|
Config = config;
|
|
Framework = framwork;
|
|
PluginInterface = pluginInterface;
|
|
PluginLog = pluginLog;
|
|
|
|
SetCharacterTitleSubscriber = PluginInterface.GetIpcSubscriber<int, string, object>("Honorific.SetCharacterTitle");
|
|
ClearCharacterTitleSubscriber = PluginInterface.GetIpcSubscriber<int, object>("Honorific.ClearCharacterTitle");
|
|
|
|
MediaCheckTimer = new Timer(2000);
|
|
MediaCheckTimer.Elapsed += CheckMediaActivity;
|
|
MediaCheckTimer.AutoReset = true;
|
|
|
|
if (Config.Enabled)
|
|
{
|
|
Start();
|
|
}
|
|
|
|
Framework.Update += OnFrameworkUpdate;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
Framework.Update -= OnFrameworkUpdate;
|
|
MediaCheckTimer?.Stop();
|
|
MediaCheckTimer?.Dispose();
|
|
Framework.RunOnFrameworkThread(() =>
|
|
{
|
|
ClearCharacterTitleSubscriber.InvokeAction(0);
|
|
});
|
|
}
|
|
|
|
public Task Enable(bool value)
|
|
{
|
|
return value ? Start() : Stop();
|
|
}
|
|
|
|
public Task Restart()
|
|
{
|
|
return Stop().ContinueWith(t => Start());
|
|
}
|
|
|
|
public Task Start()
|
|
{
|
|
if (Config.Enabled)
|
|
{
|
|
MediaCheckTimer.Start();
|
|
PluginLog.Info("Media activity monitoring started");
|
|
}
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task Stop()
|
|
{
|
|
MediaCheckTimer.Stop();
|
|
Framework.RunOnFrameworkThread(() =>
|
|
{
|
|
ClearCharacterTitleSubscriber.InvokeAction(0);
|
|
});
|
|
PluginLog.Info("Media activity monitoring stopped");
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public string State()
|
|
{
|
|
return MediaCheckTimer.Enabled ? "Running" : "Stopped";
|
|
}
|
|
|
|
private void CheckMediaActivity(object sender, ElapsedEventArgs e)
|
|
{
|
|
try
|
|
{
|
|
var mediaActivity = GetCurrentMediaActivity();
|
|
|
|
if (!MediaActivitiesEqual(CurrentMediaActivity, mediaActivity))
|
|
{
|
|
CurrentMediaActivity = mediaActivity;
|
|
MediaActivityUpdated(mediaActivity);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
PluginLog.Error($"Error checking media activity: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private MediaActivity? GetCurrentMediaActivity()
|
|
{
|
|
var spotifyTrack = GetSpotifyTrack();
|
|
if (!string.IsNullOrEmpty(spotifyTrack))
|
|
{
|
|
string artist = "";
|
|
string songName = spotifyTrack;
|
|
var parts = spotifyTrack.Split(new[] { " - " }, 2, StringSplitOptions.None);
|
|
if (parts.Length == 2)
|
|
{
|
|
artist = parts[0];
|
|
songName = parts[1];
|
|
}
|
|
|
|
return new MediaActivity
|
|
{
|
|
Name = "Spotify (V1)",
|
|
SongName = songName,
|
|
Artist = artist,
|
|
Type = ActivityType.Listening
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private string? GetSpotifyTrack()
|
|
{
|
|
try
|
|
{
|
|
var spotifyProcesses = Process.GetProcessesByName("Spotify");
|
|
var trackTitles = new List<string>();
|
|
|
|
foreach (var process in spotifyProcesses)
|
|
{
|
|
EnumWindows((hWnd, lParam) =>
|
|
{
|
|
uint processId;
|
|
GetWindowThreadProcessId(hWnd, out processId);
|
|
|
|
if (processId == process.Id)
|
|
{
|
|
var title = GetWindowTitle(hWnd);
|
|
if (!string.IsNullOrEmpty(title) &&
|
|
title != "Spotify" &&
|
|
title != "Spotify Premium" &&
|
|
title != "Spotify Free" &&
|
|
title.Contains(" - "))
|
|
{
|
|
trackTitles.Add(title);
|
|
}
|
|
}
|
|
return true;
|
|
}, IntPtr.Zero);
|
|
}
|
|
|
|
return trackTitles.OrderByDescending(t => t.Length).FirstOrDefault();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
PluginLog.Debug($"Error getting Spotify track: {ex.Message}");
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private string GetWindowTitle(IntPtr hWnd)
|
|
{
|
|
const int nChars = 256;
|
|
var buff = new StringBuilder(nChars);
|
|
if (GetWindowTextW(hWnd, buff, nChars) > 0)
|
|
{
|
|
return buff.ToString();
|
|
}
|
|
return string.Empty;
|
|
}
|
|
|
|
|
|
[DllImport("user32.dll")]
|
|
private static extern bool EnumWindows(EnumWindowsProc enumProc, IntPtr lParam);
|
|
|
|
[DllImport("user32.dll")]
|
|
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
|
|
|
|
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
|
|
private static extern int GetWindowTextW(IntPtr hWnd, StringBuilder text, int count);
|
|
|
|
private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
|
|
|
|
private bool MediaActivitiesEqual(MediaActivity? a, MediaActivity? b)
|
|
{
|
|
if (a == null && b == null) return true;
|
|
if (a == null || b == null) return false;
|
|
return a.Name == b.Name && a.SongName == b.SongName && a.Type == b.Type;
|
|
}
|
|
|
|
private void MediaActivityUpdated(MediaActivity? mediaActivity)
|
|
{
|
|
PluginLog.Verbose($"MediaActivityUpdated: {JsonConvert.SerializeObject(mediaActivity, Formatting.Indented)}");
|
|
|
|
if (mediaActivity != null)
|
|
{
|
|
foreach (var activityConfig in Config.ActivityConfigs.Where(c => c.Enabled).OrderByDescending(c => c.Priority))
|
|
{
|
|
var matchesType =
|
|
(mediaActivity.Type == ActivityType.Listening);
|
|
PluginLog.Verbose($"Checking activity config '{activityConfig.Name}' for match: {matchesType}");
|
|
if (matchesType)
|
|
{
|
|
var matchFilter = true;
|
|
if (!activityConfig.FilterTemplate.IsNullOrWhitespace())
|
|
{
|
|
var filterTemplate = Template.Parse(activityConfig.FilterTemplate);
|
|
var filter = filterTemplate.Render(new { Activity = mediaActivity, Context = UpdaterContext }, member => member.Name);
|
|
|
|
if (bool.TryParse(filter, out var parsedFilter))
|
|
{
|
|
matchFilter = parsedFilter;
|
|
}
|
|
else
|
|
{
|
|
PluginLog.Error($"Unable to parse filter '{filter}' as boolean, skipping result");
|
|
}
|
|
}
|
|
|
|
if (matchFilter)
|
|
{
|
|
UpdaterContext.SecsElapsed = 0;
|
|
UpdateTitle = () =>
|
|
{
|
|
if (Config.Enabled && activityConfig.Enabled)
|
|
{
|
|
var titleTemplate = Template.Parse(activityConfig.TitleTemplate);
|
|
var title = titleTemplate.Render(new { Activity = mediaActivity, Context = UpdaterContext }, member => member.Name);
|
|
|
|
var data = new Dictionary<string, object>() {
|
|
{"Title", title},
|
|
{"IsPrefix", activityConfig.IsPrefix},
|
|
{"Color", activityConfig.Color!},
|
|
{"Glow", activityConfig.Glow!}
|
|
};
|
|
|
|
var serializedData = JsonConvert.SerializeObject(data, Formatting.Indented);
|
|
if (serializedData != UpdatedTitleJson)
|
|
{
|
|
PluginLog.Verbose($"Call Honorific SetCharacterTitle IPC with:\n{serializedData}");
|
|
SetCharacterTitleSubscriber.InvokeAction(0, serializedData);
|
|
UpdatedTitleJson = serializedData;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ClearTitle();
|
|
}
|
|
};
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (UpdateTitle != null || UpdatedTitleJson != null)
|
|
{
|
|
ClearTitle();
|
|
}
|
|
}
|
|
|
|
private void OnFrameworkUpdate(IFramework framework)
|
|
{
|
|
if (Config.Enabled && UpdateTitle != null)
|
|
{
|
|
UpdateTitle.Invoke();
|
|
UpdaterContext.SecsElapsed += framework.UpdateDelta.TotalSeconds;
|
|
}
|
|
}
|
|
|
|
private void ClearTitle()
|
|
{
|
|
PluginLog.Verbose("Call Honorific ClearCharacterTitle IPC");
|
|
Framework.RunOnFrameworkThread(() =>
|
|
{
|
|
ClearCharacterTitleSubscriber.InvokeAction(0);
|
|
});
|
|
UpdaterContext.SecsElapsed = 0;
|
|
UpdateTitle = null;
|
|
UpdatedTitleJson = null;
|
|
}
|
|
}
|
|
|
|
public class MediaActivity
|
|
{
|
|
public string Name { get; set; } = "";
|
|
public string SongName { get; set; } = "";
|
|
public string Artist { get; set; } = "";
|
|
public ActivityType Type { get; set; }
|
|
}
|
|
|
|
public enum ActivityType
|
|
{
|
|
Playing,
|
|
Streaming,
|
|
Listening,
|
|
Watching,
|
|
Custom,
|
|
Competing
|
|
}
|