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 SetCharacterTitleSubscriber { get; init; } private ICallGateSubscriber 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("Honorific.SetCharacterTitle"); ClearCharacterTitleSubscriber = PluginInterface.GetIpcSubscriber("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(); 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() { {"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 }