diff --git a/AMWin-RichPresence/App.config b/AMWin-RichPresence/App.config index 633c498..e2d38d8 100644 --- a/AMWin-RichPresence/App.config +++ b/AMWin-RichPresence/App.config @@ -40,6 +40,9 @@ <setting name="ShowRPWhenMusicPaused" serializeAs="String"> <value>False</value> </setting> + <setting name="ClassicalComposerAsArtist" serializeAs="String"> + <value>True</value> + </setting> </AMWin_RichPresence.Properties.Settings> </userSettings> </configuration> \ No newline at end of file diff --git a/AMWin-RichPresence/App.xaml.cs b/AMWin-RichPresence/App.xaml.cs index 653e4b0..c2070ce 100644 --- a/AMWin-RichPresence/App.xaml.cs +++ b/AMWin-RichPresence/App.xaml.cs @@ -26,6 +26,7 @@ public App() { // start Discord RPC var subtitleOptions = (AppleMusicDiscordClient.RPSubtitleDisplayOptions)AMWin_RichPresence.Properties.Settings.Default.RPSubtitleChoice; + var classicalComposerAsArtist = AMWin_RichPresence.Properties.Settings.Default.ClassicalComposerAsArtist; discordClient = new(Constants.DiscordClientID, enabled: false, subtitleOptions: subtitleOptions); // start Last.FM scrobbler @@ -33,7 +34,7 @@ public App() { scrobblerClient.init(lastFmCredentials); // start Apple Music scraper - amScraper = new(AMWin_RichPresence.Properties.Settings.Default.LastfmAPIKey, Constants.RefreshPeriod, (newInfo) => { + amScraper = new(AMWin_RichPresence.Properties.Settings.Default.LastfmAPIKey, Constants.RefreshPeriod, classicalComposerAsArtist, (newInfo) => { // don't update scraper if Apple Music is paused or not open if (newInfo != null && newInfo != null && (AMWin_RichPresence.Properties.Settings.Default.ShowRPWhenMusicPaused || !newInfo.IsPaused)) { @@ -74,5 +75,8 @@ internal void UpdateRPSubtitleDisplay(AppleMusicDiscordClient.RPSubtitleDisplayO internal void UpdateLastfmCreds(bool showMessageBoxOnSuccess) { scrobblerClient.UpdateCreds(lastFmCredentials, showMessageBoxOnSuccess); } + internal void UpdateScraperPreferences(bool composerAsArtist) { + amScraper.composerAsArtist = composerAsArtist; + } } } diff --git a/AMWin-RichPresence/AppleMusicClientScraper.cs b/AMWin-RichPresence/AppleMusicClientScraper.cs index 042580d..6ec9b02 100644 --- a/AMWin-RichPresence/AppleMusicClientScraper.cs +++ b/AMWin-RichPresence/AppleMusicClientScraper.cs @@ -3,6 +3,8 @@ using System.Timers; using System.Windows.Automation; using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; namespace AMWin_RichPresence { @@ -51,14 +53,16 @@ internal class AppleMusicClientScraper { Timer timer; RefreshHandler refreshHandler; AppleMusicInfo? currentSong; + public bool composerAsArtist; // for classical music, treat composer (not performer) as artist - public AppleMusicClientScraper(string lastFmApiKey, int refreshPeriodInSec, RefreshHandler refreshHandler) { + public AppleMusicClientScraper(string lastFmApiKey, int refreshPeriodInSec, bool composerAsArtist, RefreshHandler refreshHandler) { this.refreshHandler = refreshHandler; this.lastFmApiKey = lastFmApiKey; timer = new Timer(refreshPeriodInSec * 1000); timer.Elapsed += Refresh; Refresh(this, null); timer.Start(); + this.composerAsArtist = composerAsArtist; } public void Refresh(object? source, ElapsedEventArgs? e) { @@ -129,24 +133,42 @@ public void Refresh(object? source, ElapsedEventArgs? e) { var songName = songNameElement.Current.Name; var songAlbumArtist = songAlbumArtistElement.Current.Name; - string songArtist; + string songArtist; string songAlbum; + + // some classical songs add "By " before the composer's name + string songComposer = null; + string songPerformer = null; + var composerPerformerRegex = new Regex(@"By\s.*?\s\u2014"); + var songComposerPerformer = composerPerformerRegex.Matches(songAlbumArtist); try { - // U+2014 is the emdash, not the standard "-" character on the keyboard! - songArtist = songAlbumArtist.Split(" \u2014 ")[0]; - songAlbum = songAlbumArtist.Split(" \u2014 ")[1]; + if (songComposerPerformer.Count > 0) { + songComposer = songAlbumArtist.Split(" \u2014 ")[0].Remove(0, 3); + songPerformer = songAlbumArtist.Split(" \u2014 ")[1]; + songArtist = composerAsArtist ? songComposer : songPerformer; + songAlbum = songAlbumArtist.Split(" \u2014 ")[2]; + } else { + // U+2014 is the emdash used by the Apple Music app, not the standard "-" character on the keyboard! + songArtist = songAlbumArtist.Split(" \u2014 ")[0]; + songAlbum = songAlbumArtist.Split(" \u2014 ")[1]; + } } catch { Trace.WriteLine($"Could not parse '{songAlbumArtist}' into artist and album."); songArtist = ""; songAlbum = ""; } + + // when searching for song info, use the performer as the artist instead of composer + string songSearchArtist = songPerformer ?? songArtist; + // if this is a new song, clear out the current song - if (currentSong == null || currentSong?.SongName != songName || currentSong?.SongSubTitle != songAlbumArtist) { + if (currentSong == null || currentSong?.SongName != songName || currentSong?.SongArtist != songArtist || currentSong?.SongSubTitle != songAlbumArtist) { currentSong = new AppleMusicInfo(songName, songAlbumArtist, songAlbum, songArtist); } - - if (currentSong.ArtistList == null) { - AppleMusicWebScraper.GetArtistList(songName, songAlbum, songArtist).ContinueWith(t => { + + // find artist list... unless it's a classical song + if (currentSong.ArtistList == null && songComposer == null) { + AppleMusicWebScraper.GetArtistList(songName, songAlbum, songSearchArtist).ContinueWith(t => { currentSong.ArtistList = t.Result; if (currentSong.ArtistList.Count == 0) { currentSong.ArtistList = null; @@ -176,7 +198,7 @@ public void Refresh(object? source, ElapsedEventArgs? e) { // try to get song duration if we don't have it if (currentSong.SongDuration == null) { - AppleMusicWebScraper.GetSongDuration(lastFmApiKey, songName, songAlbum, songArtist).ContinueWith(t => { + AppleMusicWebScraper.GetSongDuration(lastFmApiKey, songName, songAlbum, songSearchArtist).ContinueWith(t => { string? dur = t.Result; currentSong.SongDuration = dur == null ? null : ParseTimeString(dur); }); @@ -214,7 +236,7 @@ public void Refresh(object? source, ElapsedEventArgs? e) { // ------------------------------------------------ if (currentSong.CoverArtUrl == null) { - AppleMusicWebScraper.GetAlbumArtUrl(lastFmApiKey, songName, songAlbum, songArtist).ContinueWith(t => { + AppleMusicWebScraper.GetAlbumArtUrl(lastFmApiKey, songName, songAlbum, songSearchArtist).ContinueWith(t => { currentSong.CoverArtUrl = t.Result; }); } diff --git a/AMWin-RichPresence/AppleMusicDiscordClient.cs b/AMWin-RichPresence/AppleMusicDiscordClient.cs index d41ff50..cff0e9f 100644 --- a/AMWin-RichPresence/AppleMusicDiscordClient.cs +++ b/AMWin-RichPresence/AppleMusicDiscordClient.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics; +using System.Text; using System.Linq; using AMWin_RichPresence; using DiscordRPC; @@ -65,6 +66,10 @@ public void SetPresence(AppleMusicInfo amInfo, bool showSmallImage) { subtitle = songAlbum; break; } + if (ASCIIEncoding.Unicode.GetByteCount(subtitle) > 128) { + // TODO fix this to account for multibyte unicode characters + subtitle = subtitle.Substring(0, 60) + "..."; + } try { var rp = new RichPresence() { Details = songName, diff --git a/AMWin-RichPresence/AppleMusicWebScraper.cs b/AMWin-RichPresence/AppleMusicWebScraper.cs index 133b94a..7ab0f14 100644 --- a/AMWin-RichPresence/AppleMusicWebScraper.cs +++ b/AMWin-RichPresence/AppleMusicWebScraper.cs @@ -33,7 +33,8 @@ private async static Task<JsonDocument> GetURLJson(string url) { // Apple Music web search functions private async static Task<HtmlNode?> SearchTopResults(string songName, string songAlbum, string songArtist) { // search on the Apple Music website for the song - var url = $"https://music.apple.com/us/search?term={songName} {songAlbum} {songArtist}"; + var searchTerm = Uri.EscapeDataString($"{songName} {songAlbum} {songArtist}"); + var url = $"https://music.apple.com/us/search?term={searchTerm}"; HtmlDocument doc = await GetURL(url); try { @@ -76,7 +77,8 @@ private async static Task<JsonDocument> GetURLJson(string url) { private async static Task<HtmlNode?> SearchSongs(string songName, string songAlbum, string songArtist) { // search on the Apple Music website for the song - var url = $"https://music.apple.com/us/search?term={songName} {songAlbum} {songArtist}"; + var searchTerm = Uri.EscapeDataString($"{songName} {songAlbum} {songArtist}"); + var url = $"https://music.apple.com/us/search?term={searchTerm}"; HtmlDocument doc = await GetURL(url); try { @@ -223,8 +225,13 @@ private static string GetLargestImageUrl(HtmlNode nodeWithSource) { public async static Task<string?> GetSongDurationLastFm(string apiKey, string songName, string songArtist) { var url = $"http://ws.audioscrobbler.com/2.0/?method=track.getinfo&api_key={apiKey}&artist={Uri.EscapeDataString(songArtist)}&track={Uri.EscapeDataString(songName)}&format=json"; var j = await GetURLJson(url); - var dur = int.Parse(j.RootElement.GetProperty("track").GetProperty("duration").ToString())/1000; - return dur == 0 ? null : $"{dur / 60}:{$"{dur % 60}".PadLeft(2, '0')}"; + var track = j.RootElement.GetProperty("track"); + try { + var dur = int.Parse(track.GetProperty("duration").ToString()) / 1000; + return dur == 0 ? null : $"{dur / 60}:{$"{dur % 60}".PadLeft(2, '0')}"; + } catch { + return null; + } } public async static Task<string?> GetSongDurationAppleMusic(string songName, string songAlbum, string songArtist) { try { diff --git a/AMWin-RichPresence/Properties/Settings.Designer.cs b/AMWin-RichPresence/Properties/Settings.Designer.cs index c6cc88e..40d6cb0 100644 --- a/AMWin-RichPresence/Properties/Settings.Designer.cs +++ b/AMWin-RichPresence/Properties/Settings.Designer.cs @@ -154,5 +154,17 @@ public bool ShowRPWhenMusicPaused { this["ShowRPWhenMusicPaused"] = value; } } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("True")] + public bool ClassicalComposerAsArtist { + get { + return ((bool)(this["ClassicalComposerAsArtist"])); + } + set { + this["ClassicalComposerAsArtist"] = value; + } + } } } diff --git a/AMWin-RichPresence/Properties/Settings.settings b/AMWin-RichPresence/Properties/Settings.settings index ae78387..8f9a88d 100644 --- a/AMWin-RichPresence/Properties/Settings.settings +++ b/AMWin-RichPresence/Properties/Settings.settings @@ -35,5 +35,8 @@ <Setting Name="ShowRPWhenMusicPaused" Type="System.Boolean" Scope="User"> <Value Profile="(Default)">False</Value> </Setting> + <Setting Name="ClassicalComposerAsArtist" Type="System.Boolean" Scope="User"> + <Value Profile="(Default)">True</Value> + </Setting> </Settings> </SettingsFile> \ No newline at end of file diff --git a/AMWin-RichPresence/SettingsWindow.xaml b/AMWin-RichPresence/SettingsWindow.xaml index b1edf13..e8b2f92 100644 --- a/AMWin-RichPresence/SettingsWindow.xaml +++ b/AMWin-RichPresence/SettingsWindow.xaml @@ -7,7 +7,7 @@ xmlns:properties="clr-namespace:AMWin_RichPresence.Properties" mc:Ignorable="d" Icon="/Resources/AMWinRP.ico" - Title="AMWin-RichPresence" Height="510" Width="440" ResizeMode="NoResize" WindowStartupLocation="CenterScreen"> + Title="AMWin-RichPresence" Height="535" Width="440" ResizeMode="NoResize" WindowStartupLocation="CenterScreen"> <Grid Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"> <DockPanel> <Border DockPanel.Dock="Top" Height="64" Padding="10" Background="{DynamicResource {x:Static SystemColors.ControlLightBrushKey}}" BorderBrush="{DynamicResource {x:Static SystemColors.ControlDarkBrushKey}}" BorderThickness="0 0 0 1.5"> @@ -33,24 +33,28 @@ <RowDefinition Height="25"/> <RowDefinition Height="25"/> <RowDefinition Height="25"/> + <RowDefinition Height="25"/> </Grid.RowDefinitions> <TextBlock Grid.Row="0" Grid.Column="0">Run when Windows starts</TextBlock> <CheckBox Grid.Row="0" Grid.Column="1" x:Name="CheckBox_RunOnStartup" Margin="0 2 0 0" IsChecked="{Binding Source={x:Static properties:Settings.Default}, Path=RunOnStartup, Mode=TwoWay}" Click="CheckBox_RunOnStartup_Click"/> - <TextBlock Grid.Row="1" VerticalAlignment="Top" FontWeight="Bold" Padding="0 3 0 0">Discord settings</TextBlock> + <TextBlock Grid.Row="1" Grid.Column="0">Treat composer as artist</TextBlock> + <CheckBox Grid.Row="1" Grid.Column="1" x:Name="CheckBox_ClassicalComposerAsArtist" Margin="0 2 0 0" IsChecked="{Binding Source={x:Static properties:Settings.Default}, Path=ClassicalComposerAsArtist, Mode=TwoWay}" Click="CheckBox_ClassicalComposerAsArtist_Click"/> + + <TextBlock Grid.Row="2" VerticalAlignment="Top" FontWeight="Bold" Padding="0 3 0 0">Discord settings</TextBlock> - <TextBlock Grid.Row="2" Grid.Column="0" Padding="10 0 0 0">Enable Discord RP</TextBlock> - <CheckBox Grid.Row="2" Grid.Column="1" x:Name="CheckBox_EnableDiscordRP" Margin="0,2,0,0" IsChecked="{Binding Source={x:Static properties:Settings.Default}, Path=EnableDiscordRP, Mode=TwoWay}" Click="CheckBox_EnableDiscordRP_Click" HorizontalAlignment="Right" Width="120"/> + <TextBlock Grid.Row="3" Grid.Column="0" Padding="10 0 0 0">Enable Discord RP</TextBlock> + <CheckBox Grid.Row="3" Grid.Column="1" x:Name="CheckBox_EnableDiscordRP" Margin="0,2,0,0" IsChecked="{Binding Source={x:Static properties:Settings.Default}, Path=EnableDiscordRP, Mode=TwoWay}" Click="CheckBox_EnableDiscordRP_Click" HorizontalAlignment="Right" Width="120"/> - <TextBlock Grid.Row="3" Grid.Column="0" Padding="10 0 0 0">RP when music paused</TextBlock> - <CheckBox Grid.Row="3" Grid.Column="1" x:Name="CheckBox_ShowRPWhenMusicPaused" Margin="0 2 0 0" IsChecked="{Binding Source={x:Static properties:Settings.Default}, Path=ShowRPWhenMusicPaused, Mode=TwoWay}" Click="CheckBox_ShowRPWhenMusicPaused_Click"></CheckBox> + <TextBlock Grid.Row="4" Grid.Column="0" Padding="10 0 0 0">RP when music paused</TextBlock> + <CheckBox Grid.Row="4" Grid.Column="1" x:Name="CheckBox_ShowRPWhenMusicPaused" Margin="0 2 0 0" IsChecked="{Binding Source={x:Static properties:Settings.Default}, Path=ShowRPWhenMusicPaused, Mode=TwoWay}" Click="CheckBox_ShowRPWhenMusicPaused_Click"></CheckBox> - <TextBlock Grid.Row="4" Grid.Column="0" Padding="10 0 0 0">Apple Music icon in status</TextBlock> - <CheckBox Grid.Row="4" Grid.Column="1" x:Name="CheckBox_ShowAppleMusicIcon" Margin="0 2 0 0" IsChecked="{Binding Source={x:Static properties:Settings.Default}, Path=ShowAppleMusicIcon, Mode=TwoWay}" Click="CheckBox_ShowAppleMusicIcon_Click"></CheckBox> + <TextBlock Grid.Row="5" Grid.Column="0" Padding="10 0 0 0">Apple Music icon in status</TextBlock> + <CheckBox Grid.Row="5" Grid.Column="1" x:Name="CheckBox_ShowAppleMusicIcon" Margin="0 2 0 0" IsChecked="{Binding Source={x:Static properties:Settings.Default}, Path=ShowAppleMusicIcon, Mode=TwoWay}" Click="CheckBox_ShowAppleMusicIcon_Click"></CheckBox> - <TextBlock Grid.Row="5" Grid.Column="0" VerticalAlignment="Center" Margin="0 0 0 2" Padding="10 0 0 0">Rich Presence subtitle</TextBlock> - <ComboBox Grid.Row="5" Grid.Column="1" x:Name="ComboBox_RPSubtitleChoice" SelectedIndex="{Binding Source={x:Static properties:Settings.Default}, Path=RPSubtitleChoice, Mode=TwoWay}" Height="22" SelectionChanged="ComboBox_RPSubtitleChoice_SelectionChanged"> + <TextBlock Grid.Row="6" Grid.Column="0" VerticalAlignment="Center" Margin="0 0 0 2" Padding="10 0 0 0">Rich Presence subtitle</TextBlock> + <ComboBox Grid.Row="6" Grid.Column="1" x:Name="ComboBox_RPSubtitleChoice" SelectedIndex="{Binding Source={x:Static properties:Settings.Default}, Path=RPSubtitleChoice, Mode=TwoWay}" Height="22" SelectionChanged="ComboBox_RPSubtitleChoice_SelectionChanged"> <!-- The order of these items matters! (check the enum in AppleMusicDiscordClient.cs) --> <ComboBoxItem>Artist and album</ComboBoxItem> <ComboBoxItem>Artist only</ComboBoxItem> diff --git a/AMWin-RichPresence/SettingsWindow.xaml.cs b/AMWin-RichPresence/SettingsWindow.xaml.cs index 42723ef..013b6c9 100644 --- a/AMWin-RichPresence/SettingsWindow.xaml.cs +++ b/AMWin-RichPresence/SettingsWindow.xaml.cs @@ -29,6 +29,10 @@ private void CheckBox_RunOnStartup_Click(object sender, RoutedEventArgs e) { SaveSettings(); } + private void CheckBox_ClassicalComposerAsArtist_Click(object sender, RoutedEventArgs e) { + ((App)Application.Current).UpdateScraperPreferences(CheckBox_ClassicalComposerAsArtist.IsChecked == true); + SaveSettings(); + } private void CheckBox_EnableDiscordRP_Click(object sender, RoutedEventArgs e) { SaveSettings(); }