From 51a859397d5bfcaf217ebca506fce465ad0f6ccf Mon Sep 17 00:00:00 2001 From: Fiatsoft Date: Mon, 21 Jul 2025 19:11:13 -0400 Subject: [PATCH 1/4] with session healing, device caching, logging, transfer play-back, go to album, add to queue, view top-tracks, User-name display, toasting, helper-oriented refactoring (for common track-list item context-menu), SpotifyListPage.cs file-name, etc --- CmdPal.Ext.Spotify.sln | 1 + CmdPal.Ext.Spotify/Assets/Dark/album.png | Bin 0 -> 1677 bytes CmdPal.Ext.Spotify/Assets/Dark/device.png | Bin 0 -> 343 bytes CmdPal.Ext.Spotify/Assets/Dark/speaker.png | Bin 0 -> 870 bytes CmdPal.Ext.Spotify/Assets/Light/album.png | Bin 0 -> 1677 bytes CmdPal.Ext.Spotify/Assets/Light/device.png | Bin 0 -> 356 bytes CmdPal.Ext.Spotify/Assets/Light/speaker.png | Bin 0 -> 870 bytes CmdPal.Ext.Spotify/CmdPal.Ext.Spotify.csproj | 10 +- .../Commands/AddToQueueCommand.cs | 87 +++---- CmdPal.Ext.Spotify/Commands/LoginCommand.cs | 23 +- CmdPal.Ext.Spotify/Commands/PlayerCommand.cs | 109 +++++---- .../Commands/TransferPlaybackCommand.cs | 39 +++ .../GenerateResourcesDesigner.bat | 1 + CmdPal.Ext.Spotify/Helpers/Album.cs | 23 ++ CmdPal.Ext.Spotify/Helpers/Artist.cs | 23 ++ CmdPal.Ext.Spotify/Helpers/Cache.cs | 60 +++++ CmdPal.Ext.Spotify/Helpers/Icons.cs | 6 +- CmdPal.Ext.Spotify/Helpers/Journal.cs | 50 ++++ CmdPal.Ext.Spotify/Helpers/Playlist.cs | 23 ++ CmdPal.Ext.Spotify/Helpers/Track.cs | 40 +++ CmdPal.Ext.Spotify/Pages/AlbumPage.cs | 85 +++++++ .../{SpotifListPage.cs => SpotifyListPage.cs} | 144 +++++++---- CmdPal.Ext.Spotify/Pages/TopTracksPage.cs | 44 ++++ CmdPal.Ext.Spotify/Program.cs | 3 +- ...Ext.Spotify.Properties.Resources.resources | Bin 2961 -> 4435 bytes .../Properties/Resources.Designer.cs | 231 ++++++++++++++---- CmdPal.Ext.Spotify/Properties/Resources.resx | 49 ++++ .../Properties/Resources.zh-CN.resx | 49 ++++ ....CmdPal.Ext.Spotify.Device.Select.Demo.png | Bin 0 -> 38765 bytes .../Fiatsoft.CmdPal.Ext.Spotify.Item.Demo.png | Bin 0 -> 39709 bytes ...iatsoft.CmdPal.Ext.Spotify.Search.Demo.png | Bin 0 -> 25028 bytes ...dPal.Ext.Spotify.Search.Demo.vaporwave.png | Bin 0 -> 22533 bytes Fiatsoft.README.md | 52 ++++ 33 files changed, 955 insertions(+), 197 deletions(-) create mode 100644 CmdPal.Ext.Spotify/Assets/Dark/album.png create mode 100644 CmdPal.Ext.Spotify/Assets/Dark/device.png create mode 100644 CmdPal.Ext.Spotify/Assets/Dark/speaker.png create mode 100644 CmdPal.Ext.Spotify/Assets/Light/album.png create mode 100644 CmdPal.Ext.Spotify/Assets/Light/device.png create mode 100644 CmdPal.Ext.Spotify/Assets/Light/speaker.png create mode 100644 CmdPal.Ext.Spotify/Commands/TransferPlaybackCommand.cs create mode 100644 CmdPal.Ext.Spotify/Helpers/Album.cs create mode 100644 CmdPal.Ext.Spotify/Helpers/Artist.cs create mode 100644 CmdPal.Ext.Spotify/Helpers/Cache.cs create mode 100644 CmdPal.Ext.Spotify/Helpers/Journal.cs create mode 100644 CmdPal.Ext.Spotify/Helpers/Playlist.cs create mode 100644 CmdPal.Ext.Spotify/Helpers/Track.cs create mode 100644 CmdPal.Ext.Spotify/Pages/AlbumPage.cs rename CmdPal.Ext.Spotify/Pages/{SpotifListPage.cs => SpotifyListPage.cs} (61%) create mode 100644 CmdPal.Ext.Spotify/Pages/TopTracksPage.cs create mode 100644 Documentation/Fiatsoft.CmdPal.Ext.Spotify.Device.Select.Demo.png create mode 100644 Documentation/Fiatsoft.CmdPal.Ext.Spotify.Item.Demo.png create mode 100644 Documentation/Fiatsoft.CmdPal.Ext.Spotify.Search.Demo.png create mode 100644 Documentation/Fiatsoft.CmdPal.Ext.Spotify.Search.Demo.vaporwave.png create mode 100644 Fiatsoft.README.md diff --git a/CmdPal.Ext.Spotify.sln b/CmdPal.Ext.Spotify.sln index 648210f..7f98d38 100644 --- a/CmdPal.Ext.Spotify.sln +++ b/CmdPal.Ext.Spotify.sln @@ -10,6 +10,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution .github\FUNDING.yml = .github\FUNDING.yml LICENSE = LICENSE README.md = README.md + Fiatsoft.README.md = Fiatsoft.README.md EndProjectSection EndProject Global diff --git a/CmdPal.Ext.Spotify/Assets/Dark/album.png b/CmdPal.Ext.Spotify/Assets/Dark/album.png new file mode 100644 index 0000000000000000000000000000000000000000..f1d1c7a557ce2fddd7e186a0e5f3902e55bee8f8 GIT binary patch literal 1677 zcmV;826Fj{P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!~g&e!~vBn4jTXf1N2EmK~!i%<(a>0 z6hRcnqoha~Aw`N5f+E+YDNR>kRGbzk#TXe*-^CrVBHjpQ7}G&!PSfImV8zCT*H3XyoDsjtsHJp7C5(#| zk?OWs5Mv^GE@D*h49})|C?-VoT!An%U`D(YUQfili0U_Cub?fj*J8Jb>gNbE6X^VB zVfrPGi>PidEDG8-eG{`Hs{5BP(}CIenP(vVnDuVZwx6u`Vp2qPHwZH`uv6^OM8m8` zRJWg`EB;J$eZRuYj3r@WX$Lv;Dg2fX%-(h&)^E)%C^RHurg;<8v1yi26q9~M#lGWH7-ean^SZiF`<4w9yJ&C7pRl!@}D zj+@_Bb7qv95Kfi}^TENYD;%~p;oFEr`KoB*em}@klxsg703RbJyvin8789Mz4R7*- z*dumiDfi}O;+ayvu6$p79}6brx2-0-4>oTqJM?&``~g+>ouEBaZd*@Rz%MKQd|lz= zn=XoOJJ%yYx#?h*R>{6AP|ozy0oXSz@()#ME=$q%EbS&=?w>1@pG@D!@EmBPnJh(o zjg`rl`{!CI9e|(p+(;|*DoYWcjb-xX{<$)Fv&mUTIsm^^O}05sOw*Ap?IvIDpDUA} z%-p+~4uH6gscI&@{5H2J<~x@KEeOg@kF&H&zAwIyg#i$BGWngQO86_pco30`hvK~8 zB_;Ym%Kd#oIimi}&SjRd(;K}aO!O^Rp1aBLW`vl5sNQCf_9bUNkh^iT`J@~Ktv)FM8*TGZ z-%on_sE45rtH8e!*jg2>0$V_om_VzEh6lM4wCy|3dC@wwXb>gl``E;g+kpfwd+E}@U;nKJ00Kcr09e5u)Zn*PPa z-%3m3gfOSf^hFRosQQZu$Bf8=1prJI2skC`M?=yP)r38ngQZ`ht#F^OV(9;7{qO(i z-g5OTDzRl6@&SyD`~l2O58Y6TbG`ro002ovPDHLkV1gCXkHV58-t>)VKfu002ov zPDHLkV1nU>CTz&oaa;i3fxFZMuILL5sAWR1p&_3@PR1&UB*6E+HkP)OeqgrlLkcd< zHH5IG6z@JMiX;%*R`Pe7Y(J;mwF8zuy1Tn=;AF<_Bq@rPK#y^v$=_{!tgWq?Qo0Th znS!<$92|7*8NPxn#z%^xPrwZ&lE2$zLayL)K?r^fvKW`8D24=FdO`Ac8!25_S&7Zj z)S$0m*pPpK8GjG56vdEq1jahSNye3;P|!y(VMzv`>cfX9W+L#~G~md!nlGGe6Fda3 zrA@HTTXIL3^Uhn|NAFW|3eyWmf)5C1FizVXn3_Y0-|16RPALjmIs@c;k- z07*qoM6N<$f|t+_$mhqxVVU@6Q?3ACbkVO-fq2zVrTQwRq*<=mDKrG-zS@*2KHr)6 z`BdBG>d-#3VrX5&hVP9A)u)gTS5tpMTyOje{f53P{?vM?--+J^sTJ9g;{6G{Ta5W1 XkXqP@iLime00000NkvXXu0mjfBa|N{ literal 0 HcmV?d00001 diff --git a/CmdPal.Ext.Spotify/Assets/Dark/device.png b/CmdPal.Ext.Spotify/Assets/Dark/device.png new file mode 100644 index 0000000000000000000000000000000000000000..f6b20872adebe606962b5fe7f787815c6211dcbc GIT binary patch literal 343 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 XkXqP@iLime00000NkvXXu0mjfcQ+=# literal 0 HcmV?d00001 diff --git a/CmdPal.Ext.Spotify/Assets/Light/device.png b/CmdPal.Ext.Spotify/Assets/Light/device.png new file mode 100644 index 0000000000000000000000000000000000000000..def523cf32d6d10b8e415edc78590f985fcb8aa7 GIT binary patch literal 356 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTC#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DinK$vl=HlH+5u))*CF(kwJ?R9Ui1_K_3i^qQ7x7_vO)c$}O zZqIT{mNzS2IdLZJ=B4$ik`BfVX$)r>qJB0UU+m)YAiejk?Be@!7EW9Il}o;Bx>x^> zaqj>rn8j$qoWXX1XMv=Hw)_Ks+qeIfO|0EJ$}F9XbLQ{eoco01;vFZjk}mcw+n6p3 zYq~ah!~-40z~JfX=d#Wzp$Y0XPmtR@yx*QPW$@HiyaCH&AT<@w#AS QOFSk=DE&XQa3Qi7086}pn*aa+ literal 0 HcmV?d00001 diff --git a/CmdPal.Ext.Spotify/Assets/Light/speaker.png b/CmdPal.Ext.Spotify/Assets/Light/speaker.png new file mode 100644 index 0000000000000000000000000000000000000000..2d294cd9da832a369772c7b1b006272055f42399 GIT binary patch literal 870 zcmV-s1DX7ZP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!~g&e!~vBn4jTXf0^>Ro`Qmsk|IBVK4n^d21Wh>krE{ZNHi2kP*6}Hfs{xf z9q;3_B{Mhs;l1o#R(G%TtaoR|UeB)W^-gB9Sv6+eh~;QC`UE-+p~+W~h zO#GRj4x*ppt@`$Rep#=s39MzJsBe>aTC3s&x-`0sO7AsD0*b$@;<30B3{~}A9INi< z`DML|9~KF;L0H22a-=kz=X=_xvDc%SsHiip9XAdKsW5Y+x zftc2`748Gp=iwSHO^r%){(bb*$$cA>#qGx!4Ci7YiRcShpNDI-G&L&mTr#gx<;imVclWV1a)HF z6JzHP$nACXUPj|m`BhxXb_-jHRG&HVOH9Q*wec<57w*-hW%fd>^XOwNi=@u|RgvnZ zwu-e(0_h`l9LqjRbyHi#+UV`{k65x2^;N3NR->OZ7F!`Kk=b^m?IPJqkeI~jqqdvb zIRuieiQaEOmD!k`im6r(mc&x=kHHFb8r$lpJexWQOBnmOKV;Fz#*C9bTZu!*&mmxa z9 - - - - - @@ -52,6 +47,11 @@ + + + + + true diff --git a/CmdPal.Ext.Spotify/Commands/AddToQueueCommand.cs b/CmdPal.Ext.Spotify/Commands/AddToQueueCommand.cs index c501f8b..de55db9 100644 --- a/CmdPal.Ext.Spotify/Commands/AddToQueueCommand.cs +++ b/CmdPal.Ext.Spotify/Commands/AddToQueueCommand.cs @@ -1,59 +1,50 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using CmdPal.Ext.Spotify.Helpers; +using CmdPal.Ext.Spotify.Helpers; using CmdPal.Ext.Spotify.Properties; +using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; using SpotifyAPI.Web; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Xml.Linq; -namespace CmdPal.Ext.Spotify.Commands; - -internal sealed partial class AddToQueueCommand : PlayerCommand +namespace CmdPal.Ext.Spotify.Commands { - private object _item; - - private AddToQueueCommand(SpotifyClient spotifyClient, object item) : base(spotifyClient, new PlayerAddToQueueRequest("spotify:track:xxxx")) - { - _item = item; - Name = Resources.ContextMenuResultAddToQueueTitle; - Icon = Icons.AddQueue; - } - - public AddToQueueCommand(SpotifyClient spotifyClient, FullTrack track) : this(spotifyClient, (object)track) + internal sealed partial class AddToQueueCommand : PlayerCommand { - } - - public AddToQueueCommand(SpotifyClient spotifyClient, SimpleAlbum album) : this(spotifyClient, (object)album) - { - } - - public override CommandResult Invoke() - { - // each track is queued sequentially to preserve ordering - // UI would be blocked for a while if we weret to wait for all those requests to finish - Task.Run(() => EnsureActiveDeviceAsync(InvokeAsync)); - return CommandResult.Hide(); - } - - protected override async Task InvokeAsync(IPlayerClient player, PlayerAddToQueueRequest requestParams) - { - List? uris = null; - switch (_item) + public AddToQueueCommand(SpotifyClient spotifyClient, PlayerAddToQueueRequest requestParams) : base(spotifyClient, requestParams) { - case FullTrack track: - uris = [track.Uri]; - break; - - case SimpleAlbum album: - var tracks = await spotifyClient.Albums.GetTracks(album.Id); - uris = (await spotifyClient.PaginateAll(tracks)).Select(track => track.Uri).ToList(); - break; + Name = Resources.ContextMenuResultAddToQueueTitle; + Icon = Icons.AddQueue; + } - default: throw new NotImplementedException("Item type not implemented"); - }; + public AddToQueueCommand(SpotifyClient spotifyClient, string uri) : this(spotifyClient, new PlayerAddToQueueRequest(uri)) + { + } - foreach (var uri in uris) - await player.AddToQueue(new PlayerAddToQueueRequest(uri)); + protected override async Task InvokeAsync(IPlayerClient player, PlayerAddToQueueRequest requestParams) + { + //await player.AddToQueue(requestParams); + try + { + if (await spotifyClient.Player.AddToQueue(requestParams)) + new ToastStatusMessage(new StatusMessage() { + Message = Resources.ContextMenuResultAddToQueueTitle, + State = MessageState.Success + }).Show(); + else + throw new InvalidOperationException(Resources.EmptyErrorTitle); + } + catch (Exception ex) + { + new ToastStatusMessage(new StatusMessage() { + Message = Resources.ErrorAddToQueueToast, + State = MessageState.Error + }).Show(); + Journal.Append($"Add to queue failed: {ex.Message}", label: Journal.Label.Error); + } + } } } \ No newline at end of file diff --git a/CmdPal.Ext.Spotify/Commands/LoginCommand.cs b/CmdPal.Ext.Spotify/Commands/LoginCommand.cs index 138c3cd..d16a08b 100644 --- a/CmdPal.Ext.Spotify/Commands/LoginCommand.cs +++ b/CmdPal.Ext.Spotify/Commands/LoginCommand.cs @@ -1,11 +1,14 @@ -using Microsoft.CommandPalette.Extensions.Toolkit; -using SpotifyAPI.Web.Auth; +using CmdPal.Ext.Spotify.Helpers; +using CmdPal.Ext.Spotify.Properties; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Newtonsoft.Json; using SpotifyAPI.Web; -using System.Collections.Generic; -using System.Threading.Tasks; +using SpotifyAPI.Web.Auth; using System; +using System.Collections.Generic; using System.IO; -using Newtonsoft.Json; +using System.Threading.Tasks; namespace CmdPal.Ext.Spotify.Commands; @@ -63,7 +66,10 @@ private async Task InvokeAsync() Scope = new List { Scopes.UserReadPlaybackState, - Scopes.UserModifyPlaybackState + Scopes.UserModifyPlaybackState, + Scopes.UserReadCurrentlyPlaying, + Scopes.UserTopRead, + Scopes.UserReadEmail } }; @@ -71,9 +77,10 @@ private async Task InvokeAsync() { BrowserUtil.Open(loginRequest.ToUri()); } - catch (Exception) + catch (Exception ex) { - // TODO: notify user somehow? + new ToastStatusMessage(new StatusMessage() { Message = Resources.ErrorLoginToast, State = MessageState.Error }).Show(); + Journal.Append($"Failed to login: {ex.Message}", label: Journal.Label.Error); return; } diff --git a/CmdPal.Ext.Spotify/Commands/PlayerCommand.cs b/CmdPal.Ext.Spotify/Commands/PlayerCommand.cs index c38b81f..cc95ce9 100644 --- a/CmdPal.Ext.Spotify/Commands/PlayerCommand.cs +++ b/CmdPal.Ext.Spotify/Commands/PlayerCommand.cs @@ -1,13 +1,18 @@ -using Microsoft.CommandPalette.Extensions.Toolkit; +using CmdPal.Ext.Spotify.Helpers; +using CmdPal.Ext.Spotify.Properties; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Newtonsoft.Json; using SpotifyAPI.Web; +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; -using System.Threading.Tasks; -using System.Threading; -using System; using System.Net; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; namespace CmdPal.Ext.Spotify.Commands; @@ -29,11 +34,32 @@ T requestParams public override CommandResult Invoke() { + Journal.Append(JsonConvert.SerializeObject(this)); EnsureActiveDeviceAsync(InvokeAsync).GetAwaiter().GetResult(); - return CommandResult.Hide(); + return CommandResult.KeepOpen(); } - protected abstract Task InvokeAsync(IPlayerClient player,T requestParams); + protected abstract Task InvokeAsync(IPlayerClient player, T requestParams); + + //prefer the host's app before web-player, and 'newer' web-player (with higher-index) than 'older'; ignore mobile players + private Device? SelectBestDevice(IList devices) + { + var hostname = Environment.MachineName; + + var matchHostname = devices.FirstOrDefault(d => + d.Type?.Equals("Computer", StringComparison.OrdinalIgnoreCase) == true && + d.Name?.Contains(hostname, StringComparison.OrdinalIgnoreCase) == true + ); + if (matchHostname != null) return matchHostname; + + var webPlayer = devices.LastOrDefault(d => + d.Type?.Equals("Web", StringComparison.OrdinalIgnoreCase) == true || + d.Name?.Contains("Web Player", StringComparison.OrdinalIgnoreCase) == true + ); + if (webPlayer != null) return webPlayer; + + return devices.LastOrDefault(); + } protected async Task EnsureActiveDeviceAsync(Func callback) { @@ -42,55 +68,52 @@ protected async Task EnsureActiveDeviceAsync(Func callba if (deviceIdProperty == null) throw new InvalidOperationException($"Request of type {requestType.Name} does not need an active device."); + bool attemptedWithCachedDevices = false; + try { await callback(spotifyClient.Player, requestParams); return; } - catch (APIException exception) + catch (APIException ex) when (ex.Response?.StatusCode == HttpStatusCode.NotFound) { - if (exception.Response?.StatusCode != HttpStatusCode.NotFound) - throw; - - var possiblePaths = new List - { - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Spotify", "Spotify.exe"), - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "Spotify", "Spotify.exe"), - }; - - var windowsAppsPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Microsoft", "WindowsApps"); + new ToastStatusMessage(new StatusMessage() { Message = Resources.PlayerCommandSessionHealingToast, State = MessageState.Info }).Show(); + Journal.Append($"Healing aged-session, due to: {ex.Message}: {JsonConvert.SerializeObject(this)}", label: Journal.Label.Information); + } - if (Directory.Exists(windowsAppsPath)) + // 🧊 Attempt to load from local cache + var cachedDevices = Cache.LoadDevices(); + if (cachedDevices != null && cachedDevices?.Count > 0) + { + var selected = SelectBestDevice(cachedDevices); + if (selected != null) { - var subDirectories = Directory.GetDirectories(windowsAppsPath, "SpotifyAB.SpotifyMusic_*"); - foreach (string subDirectory in subDirectories) + deviceIdProperty.SetValue(requestParams, selected.Id); + attemptedWithCachedDevices = true; + try { - var exePath = Path.Combine(subDirectory, "Spotify.exe"); - if (File.Exists(exePath)) - possiblePaths.Add(exePath); + await callback(spotifyClient.Player, requestParams); + return; + } + catch (KeyNotFoundException) + { + Journal.Append($"⚠ Cached deviceId no longer valid. Will re-query devices; {JsonConvert.SerializeObject(this)}"); } } + } - foreach (var path in possiblePaths) - { - if (!File.Exists(path)) - continue; - - if (Process.Start(path) == null) - throw new ApplicationException($"Failed to start process {path}"); - - Thread.Sleep(1000 * 10); // wait for Spotify to open - - var deviceResponse = await spotifyClient.Player.GetAvailableDevices(); - var device = deviceResponse.Devices.FirstOrDefault(x => x.Name == Environment.MachineName); - - deviceIdProperty.SetValue(requestParams, device?.Id); - - await callback(spotifyClient.Player, requestParams); - return; - } - - throw new ApplicationException("Could not find the Spotify executable"); + if (cachedDevices == null || attemptedWithCachedDevices) + { + var freshDevices = (await spotifyClient.Player.GetAvailableDevices()).Devices; + Cache.SaveDevices(freshDevices); + var selected = SelectBestDevice(freshDevices); + if (selected == null) + throw new InvalidOperationException(Resources.PlayerCommandDeviceSelectionFailedEx); + deviceIdProperty.SetValue(requestParams, selected.Id); + await callback(spotifyClient.Player, requestParams); + return; } + + throw new InvalidOperationException(Resources.PlayerCommandDeviceRetrievalFailedEx); } } \ No newline at end of file diff --git a/CmdPal.Ext.Spotify/Commands/TransferPlaybackCommand.cs b/CmdPal.Ext.Spotify/Commands/TransferPlaybackCommand.cs new file mode 100644 index 0000000..51ef5ad --- /dev/null +++ b/CmdPal.Ext.Spotify/Commands/TransferPlaybackCommand.cs @@ -0,0 +1,39 @@ +using CmdPal.Ext.Spotify.Helpers; +using CmdPal.Ext.Spotify.Properties; +using Microsoft.CommandPalette.Extensions.Toolkit; +using SpotifyAPI.Web; +using System.Threading.Tasks; + +namespace CmdPal.Ext.Spotify.Commands; + +internal sealed partial class TransferPlaybackCommand : PlayerCommand +{ + public TransferPlaybackCommand(SpotifyClient spotifyClient, string deviceId, string deviceName) + : base(spotifyClient, new PlayerTransferPlaybackRequest(deviceId)) + { + Name = string.Format(Resources.TransferPlaybackCommandName, deviceName); + Icon = Icons.Device; + } + + protected override async Task InvokeAsync(IPlayerClient player, PlayerTransferPlaybackRequest requestParams) + { + var transferRequest = new SpotifyAPI.Web.PlayerTransferPlaybackRequest(new[] { requestParams.DeviceId }) + { + Play = true //requestParams.Play + }; + + await player.TransferPlayback(transferRequest); + } +} + +public class PlayerTransferPlaybackRequest : RequestParams +{ + public string DeviceId { get; set; } + public bool Play { get; set; } = false; + + public PlayerTransferPlaybackRequest(string deviceId, bool play = false) + { + DeviceId = deviceId; + Play = play; + } +} diff --git a/CmdPal.Ext.Spotify/GenerateResourcesDesigner.bat b/CmdPal.Ext.Spotify/GenerateResourcesDesigner.bat index 586c4b6..84f14f3 100644 --- a/CmdPal.Ext.Spotify/GenerateResourcesDesigner.bat +++ b/CmdPal.Ext.Spotify/GenerateResourcesDesigner.bat @@ -1,2 +1,3 @@ + call "C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\Tools\VsDevCmd.bat" cd "%~dp0\Properties" resgen Resources.resx CmdPal.Ext.Spotify.Properties.Resources.resources /str:CSharp,CmdPal.Ext.Spotify.Properties,Resources,Resources.Designer.cs \ No newline at end of file diff --git a/CmdPal.Ext.Spotify/Helpers/Album.cs b/CmdPal.Ext.Spotify/Helpers/Album.cs new file mode 100644 index 0000000..a674275 --- /dev/null +++ b/CmdPal.Ext.Spotify/Helpers/Album.cs @@ -0,0 +1,23 @@ +using CmdPal.Ext.Spotify.Commands; +using CmdPal.Ext.Spotify.Properties; +using Microsoft.CommandPalette.Extensions.Toolkit; +using SpotifyAPI.Web; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace CmdPal.Ext.Spotify.Helpers +{ + internal class Album + { + internal static IEnumerable ListItems(List For, SpotifyClient _spotifyClient) + { + return For.Where(album => album != null).Select(album => + new ListItem(new ResumePlaybackCommand(_spotifyClient, album.Uri)) { + Title = album.Name, + Subtitle = Resources.ResultAlbumSubTitle, + Icon = new IconInfo(album.Images.OrderBy(x => x.Width * x.Height).FirstOrDefault()?.Url), + }); + } + } +} \ No newline at end of file diff --git a/CmdPal.Ext.Spotify/Helpers/Artist.cs b/CmdPal.Ext.Spotify/Helpers/Artist.cs new file mode 100644 index 0000000..d298b28 --- /dev/null +++ b/CmdPal.Ext.Spotify/Helpers/Artist.cs @@ -0,0 +1,23 @@ +using CmdPal.Ext.Spotify.Commands; +using CmdPal.Ext.Spotify.Properties; +using Microsoft.CommandPalette.Extensions.Toolkit; +using SpotifyAPI.Web; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace CmdPal.Ext.Spotify.Helpers +{ + internal class Artist + { + internal static IEnumerable ListItems(List For, SpotifyClient _spotifyClient) + { + return For.Where(artist => artist != null).Select(artist => + new ListItem(new ResumePlaybackCommand(_spotifyClient, artist.Uri)) { + Title = artist.Name, + Subtitle = Resources.ResultArtistSubTitle, + Icon = new IconInfo(artist.Images.OrderBy(x => x.Width * x.Height).FirstOrDefault()?.Url), + }); + } + } +} \ No newline at end of file diff --git a/CmdPal.Ext.Spotify/Helpers/Cache.cs b/CmdPal.Ext.Spotify/Helpers/Cache.cs new file mode 100644 index 0000000..8193b29 --- /dev/null +++ b/CmdPal.Ext.Spotify/Helpers/Cache.cs @@ -0,0 +1,60 @@ +//using System.Text.Json; +using Newtonsoft.Json; +using SpotifyAPI.Web; +using Swan.Formatters; +using System; +using System.Collections.Generic; +using System.IO; + +namespace CmdPal.Ext.Spotify.Helpers; + +internal class Cache +{ + public static List? LoadDevices(string? file = null) + { + try + { + if (string.IsNullOrEmpty(file)) + { + var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + file = Path.Combine(localAppData, "CmdPal.Ext.Spotify", "devices.json"); + } + + if (!File.Exists(file)) return null; + + var json = File.ReadAllText(file); + return JsonConvert.DeserializeObject>(json); + } + catch (Exception ex) + { + Journal.Append($"⚠ Could not load device list: {ex.Message}:", label: Journal.Label.Warning); + return null; + } + } + + + public static void SaveDevices(IList devices, string? file = null) + { + try + { + if (string.IsNullOrEmpty(file)) + { + var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + file = Path.Combine(localAppData, "CmdPal.Ext.Spotify", "devices.json"); + } + + var dir = Path.GetDirectoryName(file); + if (!Directory.Exists(dir)) + Directory.CreateDirectory(dir); + + File.WriteAllText(file, JsonConvert.SerializeObject(devices, Formatting.Indented)); + + Journal.Append($"💾 Cached {devices.Count} devices to {file}"); + } + catch (Exception ex) + { + Journal.Append($"⚠ Could not cache device list: {ex.Message}", label: Journal.Label.Error); + } + } + +} diff --git a/CmdPal.Ext.Spotify/Helpers/Icons.cs b/CmdPal.Ext.Spotify/Helpers/Icons.cs index 4dc45d2..93d0b94 100644 --- a/CmdPal.Ext.Spotify/Helpers/Icons.cs +++ b/CmdPal.Ext.Spotify/Helpers/Icons.cs @@ -14,5 +14,9 @@ internal sealed class Icons internal static IconInfo Repeat { get; } = FromRelativePath("repeat.png"); internal static IconInfo Shuffle { get; } = FromRelativePath("shuffle.png"); internal static IconInfo AddQueue { get; } = FromRelativePath("add-queue.png"); - internal static IconInfo Spotify { get; } = FromRelativePath("Spotify.png"); + internal static IconInfo Spotify { get; } = FromRelativePath("spotify.png"); + internal static IconInfo Device { get; } = FromRelativePath("device.png"); + internal static IconInfo Speaker { get; } = FromRelativePath("speaker.png"); + internal static IconInfo Album { get; } = FromRelativePath("album.png"); + } diff --git a/CmdPal.Ext.Spotify/Helpers/Journal.cs b/CmdPal.Ext.Spotify/Helpers/Journal.cs new file mode 100644 index 0000000..e5f555b --- /dev/null +++ b/CmdPal.Ext.Spotify/Helpers/Journal.cs @@ -0,0 +1,50 @@ +using CmdPal.Ext.Spotify.Properties; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Newtonsoft.Json; +using System; +using System.IO; +using System.Runtime.CompilerServices; + +namespace CmdPal.Ext.Spotify.Helpers +{ + public static class Journal + { + private static readonly string _dir = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "CmdPal.Ext.Spotify" + ); + private static readonly string _file = "session.log"; + private static readonly string _path = Path.Combine(_dir, _file); + private static bool errorCondition; + + public enum Label + { + Information, Warning, Error, Debug + } + + public static void Append(string message, object? source = null, Label label = Label.Information, [CallerMemberName] string memberName = "", [CallerFilePath] string filePath = "") + { + try + { + Directory.CreateDirectory(_dir); + if (source == null) + { + source = $"{Path.GetFileNameWithoutExtension(filePath)}.{memberName}"; + } + var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); + var entry = $"[{timestamp}] [{label.ToString().PadRight(11)}] [{source}] {message}"; //Journal.Label.Information.ToString().Length == 11 + File.AppendAllText(_path, entry + Environment.NewLine); + } + catch (Exception ex) + { + if (!errorCondition) + { + new ToastStatusMessage(new StatusMessage() { Message = $"{Resources.ErrorJournalAppend}: {ex.Message}", State = MessageState.Warning }).Show(); + errorCondition = true; + } + } + } + } + +} diff --git a/CmdPal.Ext.Spotify/Helpers/Playlist.cs b/CmdPal.Ext.Spotify/Helpers/Playlist.cs new file mode 100644 index 0000000..b0748bc --- /dev/null +++ b/CmdPal.Ext.Spotify/Helpers/Playlist.cs @@ -0,0 +1,23 @@ +using CmdPal.Ext.Spotify.Commands; +using CmdPal.Ext.Spotify.Properties; +using Microsoft.CommandPalette.Extensions.Toolkit; +using SpotifyAPI.Web; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace CmdPal.Ext.Spotify.Helpers +{ + internal class Playlist + { + internal static IEnumerable ListItems(List For, SpotifyClient _spotifyClient) + { + return For.Where(playlist => playlist != null).Select(playlist => + new ListItem(new ResumePlaybackCommand(_spotifyClient, playlist.Uri)) { + Title = playlist.Name, + Subtitle = Resources.ResultPlaylistSubTitle, + Icon = new IconInfo(playlist.Images.OrderBy(x => x.Width * x.Height).FirstOrDefault()?.Url), + }); + } + } +} \ No newline at end of file diff --git a/CmdPal.Ext.Spotify/Helpers/Track.cs b/CmdPal.Ext.Spotify/Helpers/Track.cs new file mode 100644 index 0000000..dab5559 --- /dev/null +++ b/CmdPal.Ext.Spotify/Helpers/Track.cs @@ -0,0 +1,40 @@ +using CmdPal.Ext.Spotify.Commands; +using CmdPal.Ext.Spotify.Pages; +using CmdPal.Ext.Spotify.Properties; +using Microsoft.CommandPalette.Extensions.Toolkit; +using SpotifyAPI.Web; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CmdPal.Ext.Spotify.Helpers +{ + internal class Track + { + public static List ListItems(IList For, SpotifyClient _spotifyClient, bool includeGoToAlbum = true) + { + return For.Where(track => track != null) + .Select(track => + { + var playCommand = new ResumePlaybackCommand(_spotifyClient, new PlayerResumePlaybackRequest() { Uris = [track.Uri] }); + var queueCommand = new AddToQueueCommand(_spotifyClient, new PlayerAddToQueueRequest(track.Uri)); + + var moreCommands = new List + { + new(queueCommand) + }; + + if (includeGoToAlbum) + moreCommands.Add(new CommandContextItem(new AlbumPage(_spotifyClient, track.Album.Id, track.Album.Name))); + return new ListItem(playCommand) { + Title = track.Name, + Subtitle = $"{Resources.ResultSongSubTitle}{(track.Explicit ? $" • {Resources.ResultSongExplicitSubTitle}" : "")} • {Resources.ResultSongBySubTitle} {string.Join(", ", track.Artists.Select(x => x.Name))}", + Icon = new IconInfo(track.Album.Images.OrderBy(x => x.Width * x.Height).FirstOrDefault()?.Url), + MoreCommands = moreCommands.ToArray() + }; + }).ToList(); + } + } +} diff --git a/CmdPal.Ext.Spotify/Pages/AlbumPage.cs b/CmdPal.Ext.Spotify/Pages/AlbumPage.cs new file mode 100644 index 0000000..1232fe7 --- /dev/null +++ b/CmdPal.Ext.Spotify/Pages/AlbumPage.cs @@ -0,0 +1,85 @@ +using CmdPal.Ext.Spotify.Commands; +using CmdPal.Ext.Spotify.Helpers; +using CmdPal.Ext.Spotify.Properties; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Microsoft.Win32; +using Newtonsoft.Json; +using SpotifyAPI.Web; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace CmdPal.Ext.Spotify.Pages; + +internal partial class AlbumPage : ListPage +{ + private SpotifyClient spotifyClient; + private string albumId; + public AlbumPage( + SpotifyClient spotifyClient, + string albumId, + string albumName = null + ) + { + this.spotifyClient = spotifyClient; + this.Name = !String.IsNullOrEmpty(albumName) ? $"{Resources.ContextMenuResultGoToAlbumTitle}: {albumName}" : Resources.ContextMenuResultGoToAlbumTitle; + this.Title = albumName; + this.albumId = albumId; + this.Icon = Icons.Album; + } + + public override IListItem[] GetItems() + { + string artwork = null; + try + { + using RegistryKey? key = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"); + var dark = key is not null && Convert.ToInt32(key.GetValue("AppsUseLightTheme", 1)) == 0; + artwork = new Uri(dark ? Icons.Play.Dark.Icon : Icons.Play.Light.Icon).AbsoluteUri; + } + catch (Exception ex) + { + Journal.Append("Failed to dervive fall-back Album artwork", label: Journal.Label.Error); + artwork = "https://cdn.jsdelivr.net/gh/waaverecords/CmdPal.Ext.Spotify@main/CmdPal.Ext.Spotify/Assets/Dark/play.png"; + } + + try + { + var album = spotifyClient.Albums.Get(albumId).GetAwaiter().GetResult(); + var firstTrack = album.Tracks.Items.FirstOrDefault(); + if (firstTrack != null) + { + var fullTrack = spotifyClient.Tracks.Get(firstTrack.Id).GetAwaiter().GetResult(); ; + artwork = fullTrack.Album?.Images? + .OrderBy(i => i.Width * i.Height) + .FirstOrDefault()?.Url + ?? artwork; + } + return CmdPal.Ext.Spotify.Helpers.Track.ListItems(album.Tracks.Items.Select(simpleTrack => new FullTrack + { + Id = simpleTrack.Id, + Name = simpleTrack.Name, + Uri = simpleTrack.Uri, + DurationMs = simpleTrack.DurationMs, + Explicit = simpleTrack.Explicit, + Artists = simpleTrack.Artists, + Album = new SimpleAlbum + { + Images = [new Image { Url = artwork }], + Id = album.Id, + Name = album.Name + } + }).ToList(), spotifyClient, includeGoToAlbum: false).ToArray(); + } + catch (Exception ex) + { + Journal.Append($"Could not get album items: ${ex}: {JsonConvert.SerializeObject(this)}", label: Journal.Label.Error); + return new ListItem[0]; + } + } + + + +} \ No newline at end of file diff --git a/CmdPal.Ext.Spotify/Pages/SpotifListPage.cs b/CmdPal.Ext.Spotify/Pages/SpotifyListPage.cs similarity index 61% rename from CmdPal.Ext.Spotify/Pages/SpotifListPage.cs rename to CmdPal.Ext.Spotify/Pages/SpotifyListPage.cs index 113d422..e2dc90e 100644 --- a/CmdPal.Ext.Spotify/Pages/SpotifListPage.cs +++ b/CmdPal.Ext.Spotify/Pages/SpotifyListPage.cs @@ -9,9 +9,10 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; -using System.Text.RegularExpressions; +using Windows.Media.Protection.PlayReady; namespace CmdPal.Ext.Spotify.Pages; @@ -23,7 +24,9 @@ internal sealed partial class SpotifyListPage : DynamicListPage private SpotifyClient _spotifyClient; private Timer _debounceTimer; private List _neutralTypeFilters = ["album", "artist", "playlist", "track"]; - private List _localTypeFilters = [Resources.SearchTypeAlbum, Resources.SearchTypeArtist, Resources.SearchTypePlaylist, Resources.SearchTypeSong]; + private List _localTypeFilters = [Resources.SearchTypeAlbum, Resources.SearchTypeArtist, Resources.SearchTypePlaylist, Resources.SearchTypeTrack]; + private Lazy> _cachedPlayerCommands; + private PrivateUser _profile; public SpotifyListPage(SettingsManager settingsManager) { @@ -37,17 +40,20 @@ public SpotifyListPage(SettingsManager settingsManager) var appDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "CmdPal.Ext.Spotify"); _credentialsPath = Path.Combine(appDataPath, "credentials.json"); + _cachedPlayerCommands = new Lazy>(GetPlayerCommands); _items = [.. GetItems(string.Empty).GetAwaiter().GetResult()]; EmptyContent = _defaultEmptyContent; } - private CommandItem _defaultEmptyContent => new(GetPlayerCommands()[0]) + private CommandItem _defaultEmptyContent => new(_cachedPlayerCommands.Value[0]) { Title = Resources.ExtensionDisplayName, - Subtitle = Resources.ExtensionDescription, + Subtitle = _profile != null + ? String.Format(Resources.ExtensionStatusDescription, _profile.DisplayName, _profile.Email) + : Resources.ExtensionDescription, Icon = Icons.Spotify, - MoreCommands = GetPlayerCommands().Skip(1).Select(command => new CommandContextItem(command)).ToArray(), + MoreCommands = _cachedPlayerCommands.Value.Skip(1).Select(command => new CommandContextItem(command)).ToArray(), }; public override IListItem[] GetItems() => [.. _items]; @@ -89,7 +95,30 @@ private async Task> GetItems(string search) if (!File.Exists(_credentialsPath)) { var loginCommand = new LoginCommand(clientId, _credentialsPath); - loginCommand.LoggedIn += (_, _) => SearchAsync(search); + loginCommand.LoggedIn += async (_, _) => + { + try + { + if (_spotifyClient == null) + _spotifyClient = await GetSpotifyClientAsync(clientId); + this._profile = await _spotifyClient.UserProfile.Current(); + new ToastStatusMessage(new StatusMessage() + { + Message = String.Format(Resources.LoginSuccessToast, _profile.DisplayName, _profile.Email), + State = MessageState.Success + }).Show(); + } + catch (Exception ex) + { + new ToastStatusMessage(new StatusMessage() + { + Message = Resources.LoginUserInfoEmptyToast, + State = MessageState.Warning + }).Show(); + Journal.Append($"{Resources.LoginUserInfoEmptyToast}: {ex.Message}: {JsonConvert.SerializeObject(this)}"); + } + RefreshCommandList(); + }; EmptyContent = new CommandItem(loginCommand) { Title = Resources.ResultLoginTitle, @@ -118,7 +147,7 @@ private async Task> GetItems(string search) try { - var results = await GetSearchItemsAsync(search, searchTypes); + var results = await GetSearchItemsAsync(search, searchTypes); if (results.Count == 0) EmptyContent = new CommandItem() { @@ -128,7 +157,7 @@ private async Task> GetItems(string search) } catch (Exception ex) { - EmptyContent = new CommandItem() { + EmptyContent = new CommandItem() { Title = Resources.EmptyErrorTitle, }; } @@ -151,7 +180,14 @@ private async Task GetSpotifyClientAsync(string clientId) public List GetPlayerCommands() { - return [ + var clientId = _settingsManager.ClientId; + if (_spotifyClient == null && !string.IsNullOrEmpty(clientId)) + _spotifyClient = Task.Run(() => GetSpotifyClientAsync(clientId)).Wait(TimeSpan.FromSeconds(3)) + ? GetSpotifyClientAsync(clientId).Result + : null; + + var commands = new List + { new TogglePlaybackCommand(_spotifyClient), new PausePlaybackCommand(_spotifyClient), new ResumePlaybackCommand(_spotifyClient), @@ -162,7 +198,53 @@ public List GetPlayerCommands() new SetRepeatCommand(_spotifyClient, new(PlayerSetRepeatRequest.State.Track)), new SetRepeatCommand(_spotifyClient, new(PlayerSetRepeatRequest.State.Context)), new SetRepeatCommand(_spotifyClient, new(PlayerSetRepeatRequest.State.Off)), - ]; + new TopTrackPage(_spotifyClient) + }; + + if (_spotifyClient != null) + { + try + { + var devices = _spotifyClient.Player.GetAvailableDevices().Result; + Cache.SaveDevices(devices.Devices); + foreach (var device in devices.Devices) + { + commands.Add(new TransferPlaybackCommand(_spotifyClient, device.Id, device.Name)); + } + new ToastStatusMessage(new StatusMessage() { Message = Resources.DeviceCacheSavedToast, State = MessageState.Info }).Show(); + } + catch (Exception ex) + { + new ToastStatusMessage(new StatusMessage() { Message = Resources.DeviceCacheErrorToast, State = MessageState.Error }).Show(); + Journal.Append($"Could not use devices cache: {ex.Message}", label: Journal.Label.Error); + + var cached = CmdPal.Ext.Spotify.Helpers.Cache.LoadDevices(); + foreach (var device in cached) + { + commands.Add(new TransferPlaybackCommand(_spotifyClient, device.Id, device.Name)); + } + } + } + return commands; + } + + private void RefreshCommandList() + { + _spotifyClient = null; // reset to allow re-auth'd client + _cachedPlayerCommands = new(() => GetPlayerCommands()); // refresh device commands + + // Reset top-level UI + EmptyContent = _defaultEmptyContent; + + // Re-run search if applicable + if (!string.IsNullOrWhiteSpace(SearchText)) + { + _ = Task.Run(() => SearchAsync(SearchText)); + } + else + { + RaiseItemsChanged(0); + } } private async Task> GetSearchItemsAsync(string search, SearchRequest.Types searchTypes) @@ -177,47 +259,13 @@ private async Task> GetSearchItemsAsync(string search, SearchRequ var searchResponse = await _spotifyClient.Search.Item(searchRequest); if (searchResponse.Tracks?.Items != null) - results.AddRange(searchResponse.Tracks.Items.Where(track => track != null).Select(track => - new ListItem(new ResumePlaybackCommand(_spotifyClient, new PlayerResumePlaybackRequest() { Uris = [track.Uri] })) - { - Title = track.Name, - Subtitle = $"{Resources.ResultSongSubTitle}{(track.Explicit ? $" • {Resources.ResultSongExplicitSubTitle}" : "")} • {Resources.ResultSongBySubTitle} {string.Join(", ", track.Artists.Select(x => x.Name))}", - Icon = new IconInfo(track.Album.Images.OrderBy(x => x.Width * x.Height).FirstOrDefault()?.Url), - MoreCommands = [new AddToQueueCommand(_spotifyClient, track).ToCommandContextItem()], - }) - ); - + results.AddRange(CmdPal.Ext.Spotify.Helpers.Track.ListItems(searchResponse.Tracks.Items, _spotifyClient)); if (searchResponse.Albums?.Items != null) - results.AddRange(searchResponse.Albums.Items.Where(album => album != null).Select(album => - new ListItem(new ResumePlaybackCommand(_spotifyClient, album.Uri)) - { - Title = album.Name, - Subtitle = Resources.ResultAlbumSubTitle, - Icon = new IconInfo(album.Images.OrderBy(x => x.Width * x.Height).FirstOrDefault()?.Url), - MoreCommands = [new AddToQueueCommand(_spotifyClient, album).ToCommandContextItem()], - }) - ); - - + results.AddRange(CmdPal.Ext.Spotify.Helpers.Album.ListItems(searchResponse.Albums.Items, _spotifyClient)); if (searchResponse.Artists?.Items != null) - results.AddRange(searchResponse.Artists.Items.Where(artist => artist != null).Select(artist => - new ListItem(new ResumePlaybackCommand(_spotifyClient, artist.Uri)) - { - Title = artist.Name, - Subtitle = Resources.ResultArtistSubTitle, - Icon = new IconInfo(artist.Images.OrderBy(x => x.Width * x.Height).FirstOrDefault()?.Url), - }) - ); - + results.AddRange(CmdPal.Ext.Spotify.Helpers.Artist.ListItems(searchResponse.Artists.Items, _spotifyClient)); if (searchResponse.Playlists?.Items != null) - results.AddRange(searchResponse.Playlists.Items.Where(playlist => playlist != null).Select(playlist => - new ListItem(new ResumePlaybackCommand(_spotifyClient, playlist.Uri)) - { - Title = playlist.Name, - Subtitle = Resources.ResultPlaylistSubTitle, - Icon = new IconInfo(playlist.Images.OrderBy(x => x.Width * x.Height).FirstOrDefault()?.Url), - }) - ); + results.AddRange(CmdPal.Ext.Spotify.Helpers.Playlist.ListItems(searchResponse.Playlists.Items, _spotifyClient)); return results; } diff --git a/CmdPal.Ext.Spotify/Pages/TopTracksPage.cs b/CmdPal.Ext.Spotify/Pages/TopTracksPage.cs new file mode 100644 index 0000000..3be5371 --- /dev/null +++ b/CmdPal.Ext.Spotify/Pages/TopTracksPage.cs @@ -0,0 +1,44 @@ +using CmdPal.Ext.Spotify.Commands; +using CmdPal.Ext.Spotify.Helpers; +using CmdPal.Ext.Spotify.Properties; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Newtonsoft.Json; +using SpotifyAPI.Web; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Windows.Media.Protection.PlayReady; + +namespace CmdPal.Ext.Spotify.Pages; + +internal sealed partial class TopTrackPage : ListPage +{ + private SpotifyClient spotifyClient; + private List? items; + + public TopTrackPage( + SpotifyClient spotifyClient + ) + { + this.spotifyClient = spotifyClient; + this.Title = Resources.TopTracksTitle; + Name = this.Title; + Icon = Icons.Spotify; + } + + public override IListItem[] GetItems() + { + try + { + this.items = spotifyClient.Personalization.GetTopTracks(new PersonalizationTopRequest()).GetAwaiter().GetResult().Items; + return CmdPal.Ext.Spotify.Helpers.Track.ListItems(this.items, spotifyClient).ToArray(); + } + catch (Exception ex) + { + Journal.Append($"Failed to Get Top Tracks: {ex.Message}: {JsonConvert.SerializeObject(this)}", label: Journal.Label.Error); + return new ListItem[0]; + } + } +} \ No newline at end of file diff --git a/CmdPal.Ext.Spotify/Program.cs b/CmdPal.Ext.Spotify/Program.cs index 0d86349..a490e15 100644 --- a/CmdPal.Ext.Spotify/Program.cs +++ b/CmdPal.Ext.Spotify/Program.cs @@ -1,3 +1,4 @@ +using CmdPal.Ext.Spotify.Helpers; using Microsoft.CommandPalette.Extensions; using Shmuelie.WinRTServer; using Shmuelie.WinRTServer.CsWinRT; @@ -25,7 +26,7 @@ public static async Task Main(string[] args) } else { - Console.WriteLine("Not being launched as a Extension... exiting."); + Journal.Append($"Not being launched as a Extension... exiting; args: {Newtonsoft.Json.JsonConvert.SerializeObject(args)}"); } } } diff --git a/CmdPal.Ext.Spotify/Properties/CmdPal.Ext.Spotify.Properties.Resources.resources b/CmdPal.Ext.Spotify/Properties/CmdPal.Ext.Spotify.Properties.Resources.resources index c099d0ad3d0127b6946a990627a86197e3c7bcbc..0bf465d5463aeb183c8349e158baa37019ee51c9 100644 GIT binary patch literal 4435 zcmbtYdyEy;89!&;U3p~}qLji4Jq1MEu9qva?4rf8?8Duzz_Ry(6>GxWnRDk(cJ7>U z9=mKQ7Bp(pHdbS6jhZ&3RuYLOK1!QDlr(A6)F>7;t(q8P2>e$F(%M(r{=S(z%)NK- z7MtGLGc)IWzwiBhXJ+>G-<^G#GsYgquyfOXAj_F?>8sGwrGN2mTxy2&xGTV^c(9Npg+Fi*@Jd>)1i02d*a^Z%dfj{H+$#)^N&9G zjjw(GrpHHn4n48&kN5oOv=$sWIr8W$=Fwjqe`#>POx_@m%LS zI}f~mYWTu$&kVo$vyV3Z{2w13{cHIzD-KM4JaFo*e=RY8({%EiXPX~dKYxDj-e<4) z^2f&(KlawEpMLy&+p@O1k1T6F{^yQo&i!!B`Col5J9_BF-W&Jav-w)qxn+mEd+2E4 z+e3?Pes5TBeq(fM;dgc|c;NZncdfsA&-&AyX4`SwY<~I$>z}uO%YOUHFO*O1@XJfz zKj%Jo$IE{2JjT8>m$Ak0cVG@P1rOMx*#dkN!B$UF?}2N+L*b_3=QK=vl!=K_Bx#!k?h zF#ieU><2IX&H!^BwD(}k{kWEKeGRlM)(1hq06EWKEW~&Uv?oD3v4F8H{Y){31x&Ji zIB&z%VIdpG)yLJwS|2lTR;G|@ z=lv=>UBK3+7B!AbV&y@ASEt}F!u$|!IIz%06kHq>YJ)un;D!``ciykb+1eIouB>E7BaW5k!Z2WaxioYHaLH5cT;>i5h}- zDtZY|Z;S2?c&%Y(usskk{{IMQY}W+Dq5$e6o`V${Mu<>0W;_PQm1z!hfa*9XQB)^M zC7uB+lfrg^NZySgt3`m)x=&e}vGWX%Rq{t*YVa}+4NOL4PSUH?ECCRdv`NryHiR<; zk=l^l%6@WTR13tYLu6cG^}Vxg+Z zjsd#17V1UD(N0tk{$;(~H0t%xUjr(t<)+ZI!~YG?jn$xSa3yxHQLohNXvN;Y;ghqc zdV9cF@y zp916RT7P0K^&0+E?*;8PCJyTN*+YF{CR_X%jiZ#qTS6Gci^Qa1Zo#WmO;P3nqo?l< zjMPpUJ!~f(3_FM z+9yWx4&Z~8FX%H25h!t2C_~BT-=b)u*@dvoSM{sVHiV-B;mM*W{gTioHOtltwiJe( zwD85lJH}&@G#U^W^*KU%p7Mm!b$sj?8Q!*C2@4-hqNqX#^G~HpYAoqU*QY!%2w(d2 z0VQl@@}()!RvOvw3VcNA6Fy&DPl}fpRWF;uax#2XPHLWB5<$sRp$Xp{L+Hv0JY|cV zs{*Syo#E}3yM)5$_S=?p0x>ecm*0-Bz65%WFErN`dQ7$8S^JP>2htO_S+=2Tp20^d z3saQ^p_epRN0`t@{1AB8ldgmvnkYuhT|#Eej93>XL?DizjZ?Gk(zegje6c@!K&;(= z{ef$F$GIOoctqT)OcRPMN7N8IKYa4Ol-4B*VIYb~bZnC;1G3KpTT$`ufhh4vhb{Gh z5>i1^2GWVD1|;|pEhZU- znhbA=I62}Oh-pe^C4nOY6r~y&$gnSi02cWf-jPVTPHeMKm@xAqC!e<+isORhZDSfK zc5Q9CfCA;sW4PmU=xmeeZDSq+szSelZ^!3+SgvANOr}v;Xb`EQI)rq-sCHQui~Ne( zrKllz(^hC3+~?w73s%6JbILJ!dmeRw9=>SQ!hGmC0y2bO3X4Ts0urq_7Z+OM!fVyy zh|kqR!%_vMCivyjwR~krK8NH*PBr@_HN{(E3)-VL$)iQ{aWA&x>i9)e6#PMn{s)C_ BPHO-F delta 757 zcmXw$ZAepL6vxl=Y}dQp&6jfN@^y-v`4Z`LCaJ_i%`j$V8JUoYYYDokb4g%|T7Jk# z?evC%WG_gXMi~Vq;WvAM#B`JfeMzi7grb&&GCH$*xaYa&_doyhaGraoqPv1S`%yQD1DFkGJ`aRodK|-Boi1&Z@1S7psSlzjT)R z0Cdop`vri9a)5SX#sLC}=ZO<3x5xlS$#b6a3I>ovc>~qH(>sc4pQvY@;y%^CQACg@ z!v`|JOs3OhI!2;Ic`KQ{>kA$3qHq%rkohq+YbhR(c9*o@3M}S(agQvL`C*yNz@pGD zPh`ooYjH%b5sK)LBxFJow*h+O`-EB&GD*kb002{>wz5+NX z#tG|W(c!QdgbSj?(-^LaSwa@gl8PQNUVRuUpbjd*3U-E0MFMYUcuSGOZ!mN#B6&Z< zUy2=kh+(WUg+F6hqBIHP)SZTt$_RX-)bfuE-zk&%0z)R51WsRI8}>;WERZxpFj49T zHAp%h%CT39<$8|el8Gm9^iV@4S?GqUAhf7LxP_xd9qezWiORqQRd9mJIy|V>Y%)VF zOwE5p-g52q_+x`G-tj6E~ diff --git a/CmdPal.Ext.Spotify/Properties/Resources.Designer.cs b/CmdPal.Ext.Spotify/Properties/Resources.Designer.cs index 40e0771..82e8688 100644 --- a/CmdPal.Ext.Spotify/Properties/Resources.Designer.cs +++ b/CmdPal.Ext.Spotify/Properties/Resources.Designer.cs @@ -19,10 +19,10 @@ namespace CmdPal.Ext.Spotify.Properties { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resources { + public class Resources { private static global::System.Resources.ResourceManager resourceMan; @@ -36,7 +36,7 @@ internal Resources() { /// Returns the cached ResourceManager instance used by this class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { + public static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("CmdPal.Ext.Spotify.Properties.Resources", typeof(Resources).Assembly); @@ -51,7 +51,7 @@ internal Resources() { /// resource lookups using this strongly typed resource class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { + public static global::System.Globalization.CultureInfo Culture { get { return resourceCulture; } @@ -63,16 +63,43 @@ internal Resources() { /// /// Looks up a localized string similar to Add to queue. /// - internal static string ContextMenuResultAddToQueueTitle { + public static string ContextMenuResultAddToQueueTitle { get { return ResourceManager.GetString("ContextMenuResultAddToQueueTitle", resourceCulture); } } + /// + /// Looks up a localized string similar to Go to album. + /// + public static string ContextMenuResultGoToAlbumTitle { + get { + return ResourceManager.GetString("ContextMenuResultGoToAlbumTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Could not refresh available devices. + /// + public static string DeviceCacheErrorToast { + get { + return ResourceManager.GetString("DeviceCacheErrorToast", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Refreshed available devices. + /// + public static string DeviceCacheSavedToast { + get { + return ResourceManager.GetString("DeviceCacheSavedToast", resourceCulture); + } + } + /// /// Looks up a localized string similar to An error occured.. /// - internal static string EmptyErrorTitle { + public static string EmptyErrorTitle { get { return ResourceManager.GetString("EmptyErrorTitle", resourceCulture); } @@ -81,16 +108,61 @@ internal static string EmptyErrorTitle { /// /// Looks up a localized string similar to No item found.. /// - internal static string EmptyResultsTitle { + public static string EmptyResultsTitle { get { return ResourceManager.GetString("EmptyResultsTitle", resourceCulture); } } + /// + /// Looks up a localized string similar to Could not Add to queue. + /// + public static string ErrorAddToQueueToast { + get { + return ResourceManager.GetString("ErrorAddToQueueToast", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Could not write session log. + /// + public static string ErrorJournalAppend { + get { + return ResourceManager.GetString("ErrorJournalAppend", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Could not load top tracks. + /// + public static string ErrorLoadingTopTracksTitle { + get { + return ResourceManager.GetString("ErrorLoadingTopTracksTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Could not load tracks. + /// + public static string ErrorLoadingTracksTitle { + get { + return ResourceManager.GetString("ErrorLoadingTracksTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Could not log in.. + /// + public static string ErrorLoginToast { + get { + return ResourceManager.GetString("ErrorLoginToast", resourceCulture); + } + } + /// /// Looks up a localized string similar to Search through and control Spotify.. /// - internal static string ExtensionDescription { + public static string ExtensionDescription { get { return ResourceManager.GetString("ExtensionDescription", resourceCulture); } @@ -99,7 +171,7 @@ internal static string ExtensionDescription { /// /// Looks up a localized string similar to Spotify control. /// - internal static string ExtensionDisplayName { + public static string ExtensionDisplayName { get { return ResourceManager.GetString("ExtensionDisplayName", resourceCulture); } @@ -108,7 +180,7 @@ internal static string ExtensionDisplayName { /// /// Looks up a localized string similar to Client ID. /// - internal static string ExtensionSettingClientId { + public static string ExtensionSettingClientId { get { return ResourceManager.GetString("ExtensionSettingClientId", resourceCulture); } @@ -117,7 +189,7 @@ internal static string ExtensionSettingClientId { /// /// Looks up a localized string similar to Your Spotify's app client id.. /// - internal static string ExtensionSettingClientIdDescription { + public static string ExtensionSettingClientIdDescription { get { return ResourceManager.GetString("ExtensionSettingClientIdDescription", resourceCulture); } @@ -126,7 +198,7 @@ internal static string ExtensionSettingClientIdDescription { /// /// Looks up a localized string similar to Filter Wildcard. /// - internal static string ExtensionSettingFilterWildcard { + public static string ExtensionSettingFilterWildcard { get { return ResourceManager.GetString("ExtensionSettingFilterWildcard", resourceCulture); } @@ -135,16 +207,71 @@ internal static string ExtensionSettingFilterWildcard { /// /// Looks up a localized string similar to Filter wildcard character used to prepend a filter, e.g. /album or !album. /// - internal static string ExtensionSettingFilterWildcardDescription { + public static string ExtensionSettingFilterWildcardDescription { get { return ResourceManager.GetString("ExtensionSettingFilterWildcardDescription", resourceCulture); } } + /// + /// Looks up a localized string similar to Search through and control Spotify + ///as {0} ({1}). + /// + public static string ExtensionStatusDescription { + get { + return ResourceManager.GetString("ExtensionStatusDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to 🎧 Logged in as {0} ({1}). + /// + public static string LoginSuccessToast { + get { + return ResourceManager.GetString("LoginSuccessToast", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ✅ Logged in, but failed to get user info. + /// + public static string LoginUserInfoEmptyToast { + get { + return ResourceManager.GetString("LoginUserInfoEmptyToast", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Device selection failed with both cache and API query. + /// + public static string PlayerCommandDeviceRetrievalFailedEx { + get { + return ResourceManager.GetString("PlayerCommandDeviceRetrievalFailedEx", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unable to select valid device from Spotify API. + /// + public static string PlayerCommandDeviceSelectionFailedEx { + get { + return ResourceManager.GetString("PlayerCommandDeviceSelectionFailedEx", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Refreshing expired session…. + /// + public static string PlayerCommandSessionHealingToast { + get { + return ResourceManager.GetString("PlayerCommandSessionHealingToast", resourceCulture); + } + } + /// /// Looks up a localized string similar to Album. /// - internal static string ResultAlbumSubTitle { + public static string ResultAlbumSubTitle { get { return ResourceManager.GetString("ResultAlbumSubTitle", resourceCulture); } @@ -153,7 +280,7 @@ internal static string ResultAlbumSubTitle { /// /// Looks up a localized string similar to Artist. /// - internal static string ResultArtistSubTitle { + public static string ResultArtistSubTitle { get { return ResourceManager.GetString("ResultArtistSubTitle", resourceCulture); } @@ -162,7 +289,7 @@ internal static string ResultArtistSubTitle { /// /// Looks up a localized string similar to Login to authorize the extension to use the Spotify API.. /// - internal static string ResultLoginSubTitle { + public static string ResultLoginSubTitle { get { return ResourceManager.GetString("ResultLoginSubTitle", resourceCulture); } @@ -171,7 +298,7 @@ internal static string ResultLoginSubTitle { /// /// Looks up a localized string similar to Login. /// - internal static string ResultLoginTitle { + public static string ResultLoginTitle { get { return ResourceManager.GetString("ResultLoginTitle", resourceCulture); } @@ -180,7 +307,7 @@ internal static string ResultLoginTitle { /// /// Looks up a localized string similar to Set your client ID in the extension's settings.. /// - internal static string ResultMissingClientIdSubTitle { + public static string ResultMissingClientIdSubTitle { get { return ResourceManager.GetString("ResultMissingClientIdSubTitle", resourceCulture); } @@ -189,7 +316,7 @@ internal static string ResultMissingClientIdSubTitle { /// /// Looks up a localized string similar to Spotify - Missing client ID. /// - internal static string ResultMissingClientIdTitle { + public static string ResultMissingClientIdTitle { get { return ResourceManager.GetString("ResultMissingClientIdTitle", resourceCulture); } @@ -198,7 +325,7 @@ internal static string ResultMissingClientIdTitle { /// /// Looks up a localized string similar to Next track. /// - internal static string ResultNextTrackTitle { + public static string ResultNextTrackTitle { get { return ResourceManager.GetString("ResultNextTrackTitle", resourceCulture); } @@ -207,7 +334,7 @@ internal static string ResultNextTrackTitle { /// /// Looks up a localized string similar to Pause playback. /// - internal static string ResultPausePlaybackTitle { + public static string ResultPausePlaybackTitle { get { return ResourceManager.GetString("ResultPausePlaybackTitle", resourceCulture); } @@ -216,7 +343,7 @@ internal static string ResultPausePlaybackTitle { /// /// Looks up a localized string similar to Playlist. /// - internal static string ResultPlaylistSubTitle { + public static string ResultPlaylistSubTitle { get { return ResourceManager.GetString("ResultPlaylistSubTitle", resourceCulture); } @@ -225,7 +352,7 @@ internal static string ResultPlaylistSubTitle { /// /// Looks up a localized string similar to Play. /// - internal static string ResultPlayName { + public static string ResultPlayName { get { return ResourceManager.GetString("ResultPlayName", resourceCulture); } @@ -234,7 +361,7 @@ internal static string ResultPlayName { /// /// Looks up a localized string similar to Previous track. /// - internal static string ResultPreviousTrackTitle { + public static string ResultPreviousTrackTitle { get { return ResourceManager.GetString("ResultPreviousTrackTitle", resourceCulture); } @@ -243,7 +370,7 @@ internal static string ResultPreviousTrackTitle { /// /// Looks up a localized string similar to Resume playback. /// - internal static string ResultResumePlaybackTitle { + public static string ResultResumePlaybackTitle { get { return ResourceManager.GetString("ResultResumePlaybackTitle", resourceCulture); } @@ -252,7 +379,7 @@ internal static string ResultResumePlaybackTitle { /// /// Looks up a localized string similar to Set repeat to context. /// - internal static string ResultSetRepeatContextTitle { + public static string ResultSetRepeatContextTitle { get { return ResourceManager.GetString("ResultSetRepeatContextTitle", resourceCulture); } @@ -261,7 +388,7 @@ internal static string ResultSetRepeatContextTitle { /// /// Looks up a localized string similar to Set repeat to off. /// - internal static string ResultSetRepeatOffTitle { + public static string ResultSetRepeatOffTitle { get { return ResourceManager.GetString("ResultSetRepeatOffTitle", resourceCulture); } @@ -270,7 +397,7 @@ internal static string ResultSetRepeatOffTitle { /// /// Looks up a localized string similar to Set repeat to track. /// - internal static string ResultSetRepeatTrackTitle { + public static string ResultSetRepeatTrackTitle { get { return ResourceManager.GetString("ResultSetRepeatTrackTitle", resourceCulture); } @@ -279,7 +406,7 @@ internal static string ResultSetRepeatTrackTitle { /// /// Looks up a localized string similar to By. /// - internal static string ResultSongBySubTitle { + public static string ResultSongBySubTitle { get { return ResourceManager.GetString("ResultSongBySubTitle", resourceCulture); } @@ -288,7 +415,7 @@ internal static string ResultSongBySubTitle { /// /// Looks up a localized string similar to Explicit. /// - internal static string ResultSongExplicitSubTitle { + public static string ResultSongExplicitSubTitle { get { return ResourceManager.GetString("ResultSongExplicitSubTitle", resourceCulture); } @@ -297,7 +424,7 @@ internal static string ResultSongExplicitSubTitle { /// /// Looks up a localized string similar to Song. /// - internal static string ResultSongSubTitle { + public static string ResultSongSubTitle { get { return ResourceManager.GetString("ResultSongSubTitle", resourceCulture); } @@ -306,7 +433,7 @@ internal static string ResultSongSubTitle { /// /// Looks up a localized string similar to Toggle playback. /// - internal static string ResultTogglePlaybackTitle { + public static string ResultTogglePlaybackTitle { get { return ResourceManager.GetString("ResultTogglePlaybackTitle", resourceCulture); } @@ -315,7 +442,7 @@ internal static string ResultTogglePlaybackTitle { /// /// Looks up a localized string similar to Turn off shuffle. /// - internal static string ResultTurnOffShuffleTitle { + public static string ResultTurnOffShuffleTitle { get { return ResourceManager.GetString("ResultTurnOffShuffleTitle", resourceCulture); } @@ -324,7 +451,7 @@ internal static string ResultTurnOffShuffleTitle { /// /// Looks up a localized string similar to Turn on shuffle. /// - internal static string ResultTurnOnShuffleTitle { + public static string ResultTurnOnShuffleTitle { get { return ResourceManager.GetString("ResultTurnOnShuffleTitle", resourceCulture); } @@ -333,7 +460,7 @@ internal static string ResultTurnOnShuffleTitle { /// /// Looks up a localized string similar to album. /// - internal static string SearchTypeAlbum { + public static string SearchTypeAlbum { get { return ResourceManager.GetString("SearchTypeAlbum", resourceCulture); } @@ -342,7 +469,7 @@ internal static string SearchTypeAlbum { /// /// Looks up a localized string similar to artist. /// - internal static string SearchTypeArtist { + public static string SearchTypeArtist { get { return ResourceManager.GetString("SearchTypeArtist", resourceCulture); } @@ -351,7 +478,7 @@ internal static string SearchTypeArtist { /// /// Looks up a localized string similar to audiobook. /// - internal static string SearchTypeAudiobook { + public static string SearchTypeAudiobook { get { return ResourceManager.GetString("SearchTypeAudiobook", resourceCulture); } @@ -360,7 +487,7 @@ internal static string SearchTypeAudiobook { /// /// Looks up a localized string similar to episode. /// - internal static string SearchTypeEpisode { + public static string SearchTypeEpisode { get { return ResourceManager.GetString("SearchTypeEpisode", resourceCulture); } @@ -369,7 +496,7 @@ internal static string SearchTypeEpisode { /// /// Looks up a localized string similar to playlist. /// - internal static string SearchTypePlaylist { + public static string SearchTypePlaylist { get { return ResourceManager.GetString("SearchTypePlaylist", resourceCulture); } @@ -378,18 +505,36 @@ internal static string SearchTypePlaylist { /// /// Looks up a localized string similar to show. /// - internal static string SearchTypeShow { + public static string SearchTypeShow { get { return ResourceManager.GetString("SearchTypeShow", resourceCulture); } } /// - /// Looks up a localized string similar to song. + /// Looks up a localized string similar to track. + /// + public static string SearchTypeTrack { + get { + return ResourceManager.GetString("SearchTypeTrack", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Your Top Tracks. + /// + public static string TopTracksTitle { + get { + return ResourceManager.GetString("TopTracksTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Transfer to {0}. /// - internal static string SearchTypeSong { + public static string TransferPlaybackCommandName { get { - return ResourceManager.GetString("SearchTypeSong", resourceCulture); + return ResourceManager.GetString("TransferPlaybackCommandName", resourceCulture); } } } diff --git a/CmdPal.Ext.Spotify/Properties/Resources.resx b/CmdPal.Ext.Spotify/Properties/Resources.resx index 2d3211c..8ca6a4d 100644 --- a/CmdPal.Ext.Spotify/Properties/Resources.resx +++ b/CmdPal.Ext.Spotify/Properties/Resources.resx @@ -228,4 +228,53 @@ Client ID + + Go to album + + + Could not refresh available devices + + + Refreshed available devices + + + Could not Add to queue + + + Could not write session log + + + Could not load top tracks + + + Could not load tracks + + + Could not log in. + + + Search through and control Spotify +as {0} ({1}) + + + 🎧 Logged in as {0} ({1}) + + + ✅ Logged in, but failed to get user info + + + Device selection failed with both cache and API query + + + Unable to select valid device from Spotify API + + + Refreshing expired session… + + + Your Top Tracks + + + Transfer to {0} + \ No newline at end of file diff --git a/CmdPal.Ext.Spotify/Properties/Resources.zh-CN.resx b/CmdPal.Ext.Spotify/Properties/Resources.zh-CN.resx index ab2324b..e834f30 100644 --- a/CmdPal.Ext.Spotify/Properties/Resources.zh-CN.resx +++ b/CmdPal.Ext.Spotify/Properties/Resources.zh-CN.resx @@ -228,4 +228,53 @@ Client ID + + 前往专辑 + + + 无法刷新可用设备 + + + 可用设备已刷新 + + + 无法添加到播放队列 + + + 无法写入会话日志 + + + 無法載入熱門曲目 + + + 无法加载曲目 + + + 无法登录 + + + 搜索歌曲与控制 Spotify +身份:{0}({1}) + + + 🎧 已登录为 {0}({1}) + + + ✅ 已登录,但获取用户信息失败 + + + 设备选择失败:缓存与 API 查询均无效 + + + 无法从 Spotify API 中选择有效设备 + + + 正在刷新过期的会话… + + + 您的熱門曲目 + + + 传输至 {0} + \ No newline at end of file diff --git a/Documentation/Fiatsoft.CmdPal.Ext.Spotify.Device.Select.Demo.png b/Documentation/Fiatsoft.CmdPal.Ext.Spotify.Device.Select.Demo.png new file mode 100644 index 0000000000000000000000000000000000000000..7808b17dac0fb460ff8602acba435492d9ed74e6 GIT binary patch literal 38765 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 NH1}vzpX@gE`9F7HeX#%l literal 0 HcmV?d00001 diff --git a/Documentation/Fiatsoft.CmdPal.Ext.Spotify.Item.Demo.png b/Documentation/Fiatsoft.CmdPal.Ext.Spotify.Item.Demo.png new file mode 100644 index 0000000000000000000000000000000000000000..aef949248b5eb3abe0e50c33a044a4ab39f5e1c3 GIT binary patch literal 39709 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°S{QDk4&McljPU%K~$U6hy^xdX1? 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

s{d(K5K~%04`?(Yz`n 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 J`M|lve*ybEy{Z5J literal 0 HcmV?d00001 diff --git a/Fiatsoft.README.md b/Fiatsoft.README.md new file mode 100644 index 0000000..bca1e0f --- /dev/null +++ b/Fiatsoft.README.md @@ -0,0 +1,52 @@ +# [Fiatsoft.CmdPal.Ext.Spotify](https://github.com/Fiatsoft/Fiatsoft.CmdPal.Ext.Spotify) + +This is a Windows [Spotify](https://spotify.com) controller for the [PowerToys](https://github.com/microsoft/PowerToys) [Command Palette](https://learn.microsoft.com/en-us/windows/powertoys/command-palette/overview) based on [CmdPal.Ext.Spotify](https://github.com/waaverecords/CmdPal.Ext.Spotify/) that adds the following features: + +- More reliable playback (for aged sessions) +- Transfer play-back (to other Spotify Devices) +- Go to album +- Add to queue +- View top-tracks +- User-name display +- Device caching (to `%USERPROFILE%\AppData\Local\Packages\CmdPal.Ext.Spotify_786n6zdm3r5tt\LocalCache\Local\CmdPal.Ext.Spotify\devices.json`) +- Logging (to `%USERPROFILE%\AppData\Local\Packages\CmdPal.Ext.Spotify_786n6zdm3r5tt\LocalCache\Local\CmdPal.Ext.Spotify\session.log`) + +

+ +

+ +

+ +

+ +This is a **community fork** of [CmdPal.Ext.Spotify](https://github.com/waaverecords/CmdPal.Ext.Spotify). All credit to the original author for the architecture and inspiration. + +🛠️ This version is intended for upstream contribution. If the pull request is accepted, this fork may be deprecated or merged back. + +## Installation (copied in-part from [CmdPal.Ext.Spotify](https://github.com/waaverecords/CmdPal.Ext.Spotify/README.md)) + +> [!IMPORTANT] +> Spotify Premium is necessary to control the player. + +1. Ensure you have the [latest version](https://github.com/microsoft/PowerToys/releases/latest) of PowerToys installed. +2. In a terminal run this command, to clone the project: git clone https://github.com/Fiatsoft/Fiatsoft.CmdPal.Ext.Spotify +3. Open the Solution in Visual Studio 2022 (load `Fiatsoft.CmdPal.Ext.Spotify\Fiatsoft.CmdPal.Ext.Spotify.sln` file). +4. Click `Build > Deploy` in the menu-bar. +5. Head to your Spotify [developer dashboard](https://developer.spotify.com/). +6. Create a new app with: + - `Redirect URI` set to `http://127.0.0.1:5543/callback` + - `Web API` and `Web Playback SDK` checked +7. Go to the settings of the newly created app and save somewhere the value of `Client ID`. It is needed later. +8. Open the Command Palette Settings and go to the Extensions section. Scroll down until you find the `Spotify control` section. +9. Set the value of `Client ID` with the value saved earlier. +10. Type `Spotify` in Command Palette. You should see `Spotify control`. Hit `enter` and go through the login process. + +## Work In Progress + +**The Microsoft.CommandPalette.Extensions.Toolkit is still evolving** + +If the context-menu vanishes (after moving back between pages), fetching search results will force the UI to refresh. + +## License + +This project is licensed under the [MIT License](LICENSE) From 2d2ebb3275a4484eeda3232966ae247d19618372 Mon Sep 17 00:00:00 2001 From: Fiatsoft Date: Fri, 1 Aug 2025 19:11:08 -0400 Subject: [PATCH 2/4] DRY logging, smart GenerateResourcesDesigner.bat init, log failed search, reset resource-string accessibility, edit ZH translation for session-healing (less technical) --- .../Commands/AddToQueueCommand.cs | 3 +- CmdPal.Ext.Spotify/Commands/LoginCommand.cs | 3 +- CmdPal.Ext.Spotify/Commands/PlayerCommand.cs | 3 +- .../GenerateResourcesDesigner.bat | 2 +- CmdPal.Ext.Spotify/InitVsEnv.bat | 30 ++++ CmdPal.Ext.Spotify/Pages/SpotifyListPage.cs | 138 ++++++++++-------- .../Properties/Resources.Designer.cs | 114 +++++++-------- .../Properties/Resources.zh-CN.resx | 2 +- 8 files changed, 169 insertions(+), 126 deletions(-) create mode 100644 CmdPal.Ext.Spotify/InitVsEnv.bat diff --git a/CmdPal.Ext.Spotify/Commands/AddToQueueCommand.cs b/CmdPal.Ext.Spotify/Commands/AddToQueueCommand.cs index de55db9..07b8ffd 100644 --- a/CmdPal.Ext.Spotify/Commands/AddToQueueCommand.cs +++ b/CmdPal.Ext.Spotify/Commands/AddToQueueCommand.cs @@ -5,6 +5,7 @@ using SpotifyAPI.Web; using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -43,7 +44,7 @@ protected override async Task InvokeAsync(IPlayerClient player, PlayerAddToQueue Message = Resources.ErrorAddToQueueToast, State = MessageState.Error }).Show(); - Journal.Append($"Add to queue failed: {ex.Message}", label: Journal.Label.Error); + Journal.Append($"{Resources.ResourceManager.GetString("ErrorAddToQueueToast", CultureInfo.InvariantCulture)}: {ex.Message}", label: Journal.Label.Error); } } } diff --git a/CmdPal.Ext.Spotify/Commands/LoginCommand.cs b/CmdPal.Ext.Spotify/Commands/LoginCommand.cs index d16a08b..c174e53 100644 --- a/CmdPal.Ext.Spotify/Commands/LoginCommand.cs +++ b/CmdPal.Ext.Spotify/Commands/LoginCommand.cs @@ -7,6 +7,7 @@ using SpotifyAPI.Web.Auth; using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Threading.Tasks; @@ -80,7 +81,7 @@ private async Task InvokeAsync() catch (Exception ex) { new ToastStatusMessage(new StatusMessage() { Message = Resources.ErrorLoginToast, State = MessageState.Error }).Show(); - Journal.Append($"Failed to login: {ex.Message}", label: Journal.Label.Error); + Journal.Append($"{Resources.ResourceManager.GetString("ErrorLoginToast", CultureInfo.InvariantCulture)}: {ex.Message}", label: Journal.Label.Error); return; } diff --git a/CmdPal.Ext.Spotify/Commands/PlayerCommand.cs b/CmdPal.Ext.Spotify/Commands/PlayerCommand.cs index cc95ce9..b4e9cbb 100644 --- a/CmdPal.Ext.Spotify/Commands/PlayerCommand.cs +++ b/CmdPal.Ext.Spotify/Commands/PlayerCommand.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Globalization; using System.IO; using System.Linq; using System.Net; @@ -78,7 +79,7 @@ protected async Task EnsureActiveDeviceAsync(Func callba catch (APIException ex) when (ex.Response?.StatusCode == HttpStatusCode.NotFound) { new ToastStatusMessage(new StatusMessage() { Message = Resources.PlayerCommandSessionHealingToast, State = MessageState.Info }).Show(); - Journal.Append($"Healing aged-session, due to: {ex.Message}: {JsonConvert.SerializeObject(this)}", label: Journal.Label.Information); + Journal.Append($"{Resources.ResourceManager.GetString("PlayerCommandSessionHealingToast", CultureInfo.InvariantCulture)} due to: {ex.Message}: {JsonConvert.SerializeObject(this)}", label: Journal.Label.Information); } // 🧊 Attempt to load from local cache diff --git a/CmdPal.Ext.Spotify/GenerateResourcesDesigner.bat b/CmdPal.Ext.Spotify/GenerateResourcesDesigner.bat index 84f14f3..79d82e2 100644 --- a/CmdPal.Ext.Spotify/GenerateResourcesDesigner.bat +++ b/CmdPal.Ext.Spotify/GenerateResourcesDesigner.bat @@ -1,3 +1,3 @@ - call "C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\Tools\VsDevCmd.bat" + call "InitVsEnv.bat" cd "%~dp0\Properties" resgen Resources.resx CmdPal.Ext.Spotify.Properties.Resources.resources /str:CSharp,CmdPal.Ext.Spotify.Properties,Resources,Resources.Designer.cs \ No newline at end of file diff --git a/CmdPal.Ext.Spotify/InitVsEnv.bat b/CmdPal.Ext.Spotify/InitVsEnv.bat new file mode 100644 index 0000000..4aafda9 --- /dev/null +++ b/CmdPal.Ext.Spotify/InitVsEnv.bat @@ -0,0 +1,30 @@ +@echo off + +:: Skip if cl.exe is already available +where cl >nul 2>nul && exit /b 0 + +:: Locate VS 2022 using vswhere +set "VSWHERE=%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" +if exist "%VSWHERE%" ( + for /f "usebackq tokens=*" %%i in (`"%VSWHERE%" -latest -products * -requires Microsoft.Component.MSBuild -property installationPath`) do ( + set "VSINSTALLDIR=%%i" + goto :found + ) +) + +:: Fallback: Try default install path +if exist "C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\Tools\VsDevCmd.bat" ( + set "VSINSTALLDIR=C:\Program Files\Microsoft Visual Studio\2022\Community" + goto :found +) + +echo Error: Visual Studio 2022 with MSBuild not found. +exit /b 1 + +:found +echo Found Visual Studio at: %VSINSTALLDIR% +call "%VSINSTALLDIR%\Common7\Tools\VsDevCmd.bat" +if errorlevel 1 ( + echo Failed to initialize Developer Command Prompt. + exit /b 1 +) diff --git a/CmdPal.Ext.Spotify/Pages/SpotifyListPage.cs b/CmdPal.Ext.Spotify/Pages/SpotifyListPage.cs index e2dc90e..9bc705d 100644 --- a/CmdPal.Ext.Spotify/Pages/SpotifyListPage.cs +++ b/CmdPal.Ext.Spotify/Pages/SpotifyListPage.cs @@ -7,6 +7,7 @@ using SpotifyAPI.Web; using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Text.RegularExpressions; @@ -80,88 +81,97 @@ private async void SearchAsync(string search) private async Task> GetItems(string search) { - var clientId = _settingsManager.ClientId; - - if (string.IsNullOrEmpty(clientId)) + try { - EmptyContent = new CommandItem() + var clientId = _settingsManager.ClientId; + + if (string.IsNullOrEmpty(clientId)) { - Title = Resources.ResultMissingClientIdTitle, - Subtitle = Resources.ResultMissingClientIdSubTitle, - }; - return []; - } + EmptyContent = new CommandItem() + { + Title = Resources.ResultMissingClientIdTitle, + Subtitle = Resources.ResultMissingClientIdSubTitle, + }; + return []; + } - if (!File.Exists(_credentialsPath)) - { - var loginCommand = new LoginCommand(clientId, _credentialsPath); - loginCommand.LoggedIn += async (_, _) => + if (!File.Exists(_credentialsPath)) { - try + var loginCommand = new LoginCommand(clientId, _credentialsPath); + loginCommand.LoggedIn += async (_, _) => { - if (_spotifyClient == null) - _spotifyClient = await GetSpotifyClientAsync(clientId); - this._profile = await _spotifyClient.UserProfile.Current(); - new ToastStatusMessage(new StatusMessage() + try { - Message = String.Format(Resources.LoginSuccessToast, _profile.DisplayName, _profile.Email), - State = MessageState.Success - }).Show(); - } - catch (Exception ex) - { - new ToastStatusMessage(new StatusMessage() + if (_spotifyClient == null) + _spotifyClient = await GetSpotifyClientAsync(clientId); + this._profile = await _spotifyClient.UserProfile.Current(); + new ToastStatusMessage(new StatusMessage() + { + Message = String.Format(Resources.LoginSuccessToast, _profile.DisplayName, _profile.Email), + State = MessageState.Success + }).Show(); + } + catch (Exception ex) { - Message = Resources.LoginUserInfoEmptyToast, - State = MessageState.Warning - }).Show(); - Journal.Append($"{Resources.LoginUserInfoEmptyToast}: {ex.Message}: {JsonConvert.SerializeObject(this)}"); - } - RefreshCommandList(); - }; - EmptyContent = new CommandItem(loginCommand) - { - Title = Resources.ResultLoginTitle, - Subtitle = Resources.ResultLoginSubTitle, - Icon = Icons.Spotify, - }; - return []; - } + new ToastStatusMessage(new StatusMessage() + { + Message = Resources.LoginUserInfoEmptyToast, + State = MessageState.Warning + }).Show(); + Journal.Append($"{Resources.LoginUserInfoEmptyToast}: {ex.Message}: {JsonConvert.SerializeObject(this)}"); + } + RefreshCommandList(); + }; + EmptyContent = new CommandItem(loginCommand) + { + Title = Resources.ResultLoginTitle, + Subtitle = Resources.ResultLoginSubTitle, + Icon = Icons.Spotify, + }; + return []; + } - if (_spotifyClient == null) - _spotifyClient = await GetSpotifyClientAsync(clientId); + if (_spotifyClient == null) + _spotifyClient = await GetSpotifyClientAsync(clientId); - if (string.IsNullOrEmpty(search.Trim())) - { - EmptyContent = _defaultEmptyContent; - return []; - } + if (string.IsNullOrEmpty(search.Trim())) + { + EmptyContent = _defaultEmptyContent; + return []; + } - (search, var searchTypes) = GetSearchTypes(search); + (search, var searchTypes) = GetSearchTypes(search); - if (string.IsNullOrEmpty(search.Trim())) - { - EmptyContent = _defaultEmptyContent; - return []; - } + if (string.IsNullOrEmpty(search.Trim())) + { + EmptyContent = _defaultEmptyContent; + return []; + } - try - { - var results = await GetSearchItemsAsync(search, searchTypes); - if (results.Count == 0) + try + { + var results = await GetSearchItemsAsync(search, searchTypes); + if (results.Count == 0) + EmptyContent = new CommandItem() + { + Title = Resources.EmptyResultsTitle, + }; + return results; + } + catch (Exception ex) + { EmptyContent = new CommandItem() { - Title = Resources.EmptyResultsTitle, + Title = Resources.EmptyErrorTitle, }; - return results; + } + return []; } catch (Exception ex) { - EmptyContent = new CommandItem() { - Title = Resources.EmptyErrorTitle, - }; + Journal.Append(ex.Message, label: Journal.Label.Error); + return []; } - return []; } private async Task GetSpotifyClientAsync(string clientId) @@ -216,7 +226,7 @@ public List GetPlayerCommands() catch (Exception ex) { new ToastStatusMessage(new StatusMessage() { Message = Resources.DeviceCacheErrorToast, State = MessageState.Error }).Show(); - Journal.Append($"Could not use devices cache: {ex.Message}", label: Journal.Label.Error); + Journal.Append($"{Resources.ResourceManager.GetString("DeviceCacheErrorToast", CultureInfo.InvariantCulture)}: {ex.Message}", label: Journal.Label.Error); var cached = CmdPal.Ext.Spotify.Helpers.Cache.LoadDevices(); foreach (var device in cached) diff --git a/CmdPal.Ext.Spotify/Properties/Resources.Designer.cs b/CmdPal.Ext.Spotify/Properties/Resources.Designer.cs index 82e8688..95e28a5 100644 --- a/CmdPal.Ext.Spotify/Properties/Resources.Designer.cs +++ b/CmdPal.Ext.Spotify/Properties/Resources.Designer.cs @@ -19,10 +19,10 @@ namespace CmdPal.Ext.Spotify.Properties { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - public class Resources { + internal class Resources { private static global::System.Resources.ResourceManager resourceMan; @@ -36,7 +36,7 @@ internal Resources() { /// Returns the cached ResourceManager instance used by this class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - public static global::System.Resources.ResourceManager ResourceManager { + internal static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("CmdPal.Ext.Spotify.Properties.Resources", typeof(Resources).Assembly); @@ -51,7 +51,7 @@ internal Resources() { /// resource lookups using this strongly typed resource class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - public static global::System.Globalization.CultureInfo Culture { + internal static global::System.Globalization.CultureInfo Culture { get { return resourceCulture; } @@ -63,7 +63,7 @@ internal Resources() { /// /// Looks up a localized string similar to Add to queue. /// - public static string ContextMenuResultAddToQueueTitle { + internal static string ContextMenuResultAddToQueueTitle { get { return ResourceManager.GetString("ContextMenuResultAddToQueueTitle", resourceCulture); } @@ -72,7 +72,7 @@ public static string ContextMenuResultAddToQueueTitle { /// /// Looks up a localized string similar to Go to album. /// - public static string ContextMenuResultGoToAlbumTitle { + internal static string ContextMenuResultGoToAlbumTitle { get { return ResourceManager.GetString("ContextMenuResultGoToAlbumTitle", resourceCulture); } @@ -81,7 +81,7 @@ public static string ContextMenuResultGoToAlbumTitle { /// /// Looks up a localized string similar to Could not refresh available devices. /// - public static string DeviceCacheErrorToast { + internal static string DeviceCacheErrorToast { get { return ResourceManager.GetString("DeviceCacheErrorToast", resourceCulture); } @@ -90,7 +90,7 @@ public static string DeviceCacheErrorToast { /// /// Looks up a localized string similar to Refreshed available devices. /// - public static string DeviceCacheSavedToast { + internal static string DeviceCacheSavedToast { get { return ResourceManager.GetString("DeviceCacheSavedToast", resourceCulture); } @@ -99,7 +99,7 @@ public static string DeviceCacheSavedToast { /// /// Looks up a localized string similar to An error occured.. /// - public static string EmptyErrorTitle { + internal static string EmptyErrorTitle { get { return ResourceManager.GetString("EmptyErrorTitle", resourceCulture); } @@ -108,7 +108,7 @@ public static string EmptyErrorTitle { /// /// Looks up a localized string similar to No item found.. /// - public static string EmptyResultsTitle { + internal static string EmptyResultsTitle { get { return ResourceManager.GetString("EmptyResultsTitle", resourceCulture); } @@ -117,7 +117,7 @@ public static string EmptyResultsTitle { /// /// Looks up a localized string similar to Could not Add to queue. /// - public static string ErrorAddToQueueToast { + internal static string ErrorAddToQueueToast { get { return ResourceManager.GetString("ErrorAddToQueueToast", resourceCulture); } @@ -126,7 +126,7 @@ public static string ErrorAddToQueueToast { /// /// Looks up a localized string similar to Could not write session log. /// - public static string ErrorJournalAppend { + internal static string ErrorJournalAppend { get { return ResourceManager.GetString("ErrorJournalAppend", resourceCulture); } @@ -135,7 +135,7 @@ public static string ErrorJournalAppend { /// /// Looks up a localized string similar to Could not load top tracks. /// - public static string ErrorLoadingTopTracksTitle { + internal static string ErrorLoadingTopTracksTitle { get { return ResourceManager.GetString("ErrorLoadingTopTracksTitle", resourceCulture); } @@ -144,7 +144,7 @@ public static string ErrorLoadingTopTracksTitle { /// /// Looks up a localized string similar to Could not load tracks. /// - public static string ErrorLoadingTracksTitle { + internal static string ErrorLoadingTracksTitle { get { return ResourceManager.GetString("ErrorLoadingTracksTitle", resourceCulture); } @@ -153,7 +153,7 @@ public static string ErrorLoadingTracksTitle { /// /// Looks up a localized string similar to Could not log in.. /// - public static string ErrorLoginToast { + internal static string ErrorLoginToast { get { return ResourceManager.GetString("ErrorLoginToast", resourceCulture); } @@ -162,7 +162,7 @@ public static string ErrorLoginToast { /// /// Looks up a localized string similar to Search through and control Spotify.. /// - public static string ExtensionDescription { + internal static string ExtensionDescription { get { return ResourceManager.GetString("ExtensionDescription", resourceCulture); } @@ -171,7 +171,7 @@ public static string ExtensionDescription { /// /// Looks up a localized string similar to Spotify control. /// - public static string ExtensionDisplayName { + internal static string ExtensionDisplayName { get { return ResourceManager.GetString("ExtensionDisplayName", resourceCulture); } @@ -180,7 +180,7 @@ public static string ExtensionDisplayName { /// /// Looks up a localized string similar to Client ID. /// - public static string ExtensionSettingClientId { + internal static string ExtensionSettingClientId { get { return ResourceManager.GetString("ExtensionSettingClientId", resourceCulture); } @@ -189,7 +189,7 @@ public static string ExtensionSettingClientId { /// /// Looks up a localized string similar to Your Spotify's app client id.. /// - public static string ExtensionSettingClientIdDescription { + internal static string ExtensionSettingClientIdDescription { get { return ResourceManager.GetString("ExtensionSettingClientIdDescription", resourceCulture); } @@ -198,7 +198,7 @@ public static string ExtensionSettingClientIdDescription { /// /// Looks up a localized string similar to Filter Wildcard. /// - public static string ExtensionSettingFilterWildcard { + internal static string ExtensionSettingFilterWildcard { get { return ResourceManager.GetString("ExtensionSettingFilterWildcard", resourceCulture); } @@ -207,7 +207,7 @@ public static string ExtensionSettingFilterWildcard { /// /// Looks up a localized string similar to Filter wildcard character used to prepend a filter, e.g. /album or !album. /// - public static string ExtensionSettingFilterWildcardDescription { + internal static string ExtensionSettingFilterWildcardDescription { get { return ResourceManager.GetString("ExtensionSettingFilterWildcardDescription", resourceCulture); } @@ -217,7 +217,7 @@ public static string ExtensionSettingFilterWildcardDescription { /// Looks up a localized string similar to Search through and control Spotify ///as {0} ({1}). /// - public static string ExtensionStatusDescription { + internal static string ExtensionStatusDescription { get { return ResourceManager.GetString("ExtensionStatusDescription", resourceCulture); } @@ -226,7 +226,7 @@ public static string ExtensionStatusDescription { /// /// Looks up a localized string similar to 🎧 Logged in as {0} ({1}). /// - public static string LoginSuccessToast { + internal static string LoginSuccessToast { get { return ResourceManager.GetString("LoginSuccessToast", resourceCulture); } @@ -235,7 +235,7 @@ public static string LoginSuccessToast { /// /// Looks up a localized string similar to ✅ Logged in, but failed to get user info. /// - public static string LoginUserInfoEmptyToast { + internal static string LoginUserInfoEmptyToast { get { return ResourceManager.GetString("LoginUserInfoEmptyToast", resourceCulture); } @@ -244,7 +244,7 @@ public static string LoginUserInfoEmptyToast { /// /// Looks up a localized string similar to Device selection failed with both cache and API query. /// - public static string PlayerCommandDeviceRetrievalFailedEx { + internal static string PlayerCommandDeviceRetrievalFailedEx { get { return ResourceManager.GetString("PlayerCommandDeviceRetrievalFailedEx", resourceCulture); } @@ -253,7 +253,7 @@ public static string PlayerCommandDeviceRetrievalFailedEx { /// /// Looks up a localized string similar to Unable to select valid device from Spotify API. /// - public static string PlayerCommandDeviceSelectionFailedEx { + internal static string PlayerCommandDeviceSelectionFailedEx { get { return ResourceManager.GetString("PlayerCommandDeviceSelectionFailedEx", resourceCulture); } @@ -262,7 +262,7 @@ public static string PlayerCommandDeviceSelectionFailedEx { /// /// Looks up a localized string similar to Refreshing expired session…. /// - public static string PlayerCommandSessionHealingToast { + internal static string PlayerCommandSessionHealingToast { get { return ResourceManager.GetString("PlayerCommandSessionHealingToast", resourceCulture); } @@ -271,7 +271,7 @@ public static string PlayerCommandSessionHealingToast { /// /// Looks up a localized string similar to Album. /// - public static string ResultAlbumSubTitle { + internal static string ResultAlbumSubTitle { get { return ResourceManager.GetString("ResultAlbumSubTitle", resourceCulture); } @@ -280,7 +280,7 @@ public static string ResultAlbumSubTitle { /// /// Looks up a localized string similar to Artist. /// - public static string ResultArtistSubTitle { + internal static string ResultArtistSubTitle { get { return ResourceManager.GetString("ResultArtistSubTitle", resourceCulture); } @@ -289,7 +289,7 @@ public static string ResultArtistSubTitle { /// /// Looks up a localized string similar to Login to authorize the extension to use the Spotify API.. /// - public static string ResultLoginSubTitle { + internal static string ResultLoginSubTitle { get { return ResourceManager.GetString("ResultLoginSubTitle", resourceCulture); } @@ -298,7 +298,7 @@ public static string ResultLoginSubTitle { /// /// Looks up a localized string similar to Login. /// - public static string ResultLoginTitle { + internal static string ResultLoginTitle { get { return ResourceManager.GetString("ResultLoginTitle", resourceCulture); } @@ -307,7 +307,7 @@ public static string ResultLoginTitle { /// /// Looks up a localized string similar to Set your client ID in the extension's settings.. /// - public static string ResultMissingClientIdSubTitle { + internal static string ResultMissingClientIdSubTitle { get { return ResourceManager.GetString("ResultMissingClientIdSubTitle", resourceCulture); } @@ -316,7 +316,7 @@ public static string ResultMissingClientIdSubTitle { /// /// Looks up a localized string similar to Spotify - Missing client ID. /// - public static string ResultMissingClientIdTitle { + internal static string ResultMissingClientIdTitle { get { return ResourceManager.GetString("ResultMissingClientIdTitle", resourceCulture); } @@ -325,7 +325,7 @@ public static string ResultMissingClientIdTitle { /// /// Looks up a localized string similar to Next track. /// - public static string ResultNextTrackTitle { + internal static string ResultNextTrackTitle { get { return ResourceManager.GetString("ResultNextTrackTitle", resourceCulture); } @@ -334,7 +334,7 @@ public static string ResultNextTrackTitle { /// /// Looks up a localized string similar to Pause playback. /// - public static string ResultPausePlaybackTitle { + internal static string ResultPausePlaybackTitle { get { return ResourceManager.GetString("ResultPausePlaybackTitle", resourceCulture); } @@ -343,7 +343,7 @@ public static string ResultPausePlaybackTitle { /// /// Looks up a localized string similar to Playlist. /// - public static string ResultPlaylistSubTitle { + internal static string ResultPlaylistSubTitle { get { return ResourceManager.GetString("ResultPlaylistSubTitle", resourceCulture); } @@ -352,7 +352,7 @@ public static string ResultPlaylistSubTitle { /// /// Looks up a localized string similar to Play. /// - public static string ResultPlayName { + internal static string ResultPlayName { get { return ResourceManager.GetString("ResultPlayName", resourceCulture); } @@ -361,7 +361,7 @@ public static string ResultPlayName { /// /// Looks up a localized string similar to Previous track. /// - public static string ResultPreviousTrackTitle { + internal static string ResultPreviousTrackTitle { get { return ResourceManager.GetString("ResultPreviousTrackTitle", resourceCulture); } @@ -370,7 +370,7 @@ public static string ResultPreviousTrackTitle { /// /// Looks up a localized string similar to Resume playback. /// - public static string ResultResumePlaybackTitle { + internal static string ResultResumePlaybackTitle { get { return ResourceManager.GetString("ResultResumePlaybackTitle", resourceCulture); } @@ -379,7 +379,7 @@ public static string ResultResumePlaybackTitle { /// /// Looks up a localized string similar to Set repeat to context. /// - public static string ResultSetRepeatContextTitle { + internal static string ResultSetRepeatContextTitle { get { return ResourceManager.GetString("ResultSetRepeatContextTitle", resourceCulture); } @@ -388,7 +388,7 @@ public static string ResultSetRepeatContextTitle { /// /// Looks up a localized string similar to Set repeat to off. /// - public static string ResultSetRepeatOffTitle { + internal static string ResultSetRepeatOffTitle { get { return ResourceManager.GetString("ResultSetRepeatOffTitle", resourceCulture); } @@ -397,7 +397,7 @@ public static string ResultSetRepeatOffTitle { /// /// Looks up a localized string similar to Set repeat to track. /// - public static string ResultSetRepeatTrackTitle { + internal static string ResultSetRepeatTrackTitle { get { return ResourceManager.GetString("ResultSetRepeatTrackTitle", resourceCulture); } @@ -406,7 +406,7 @@ public static string ResultSetRepeatTrackTitle { /// /// Looks up a localized string similar to By. /// - public static string ResultSongBySubTitle { + internal static string ResultSongBySubTitle { get { return ResourceManager.GetString("ResultSongBySubTitle", resourceCulture); } @@ -415,7 +415,7 @@ public static string ResultSongBySubTitle { /// /// Looks up a localized string similar to Explicit. /// - public static string ResultSongExplicitSubTitle { + internal static string ResultSongExplicitSubTitle { get { return ResourceManager.GetString("ResultSongExplicitSubTitle", resourceCulture); } @@ -424,7 +424,7 @@ public static string ResultSongExplicitSubTitle { /// /// Looks up a localized string similar to Song. /// - public static string ResultSongSubTitle { + internal static string ResultSongSubTitle { get { return ResourceManager.GetString("ResultSongSubTitle", resourceCulture); } @@ -433,7 +433,7 @@ public static string ResultSongSubTitle { /// /// Looks up a localized string similar to Toggle playback. /// - public static string ResultTogglePlaybackTitle { + internal static string ResultTogglePlaybackTitle { get { return ResourceManager.GetString("ResultTogglePlaybackTitle", resourceCulture); } @@ -442,7 +442,7 @@ public static string ResultTogglePlaybackTitle { /// /// Looks up a localized string similar to Turn off shuffle. /// - public static string ResultTurnOffShuffleTitle { + internal static string ResultTurnOffShuffleTitle { get { return ResourceManager.GetString("ResultTurnOffShuffleTitle", resourceCulture); } @@ -451,7 +451,7 @@ public static string ResultTurnOffShuffleTitle { /// /// Looks up a localized string similar to Turn on shuffle. /// - public static string ResultTurnOnShuffleTitle { + internal static string ResultTurnOnShuffleTitle { get { return ResourceManager.GetString("ResultTurnOnShuffleTitle", resourceCulture); } @@ -460,7 +460,7 @@ public static string ResultTurnOnShuffleTitle { /// /// Looks up a localized string similar to album. /// - public static string SearchTypeAlbum { + internal static string SearchTypeAlbum { get { return ResourceManager.GetString("SearchTypeAlbum", resourceCulture); } @@ -469,7 +469,7 @@ public static string SearchTypeAlbum { /// /// Looks up a localized string similar to artist. /// - public static string SearchTypeArtist { + internal static string SearchTypeArtist { get { return ResourceManager.GetString("SearchTypeArtist", resourceCulture); } @@ -478,7 +478,7 @@ public static string SearchTypeArtist { /// /// Looks up a localized string similar to audiobook. /// - public static string SearchTypeAudiobook { + internal static string SearchTypeAudiobook { get { return ResourceManager.GetString("SearchTypeAudiobook", resourceCulture); } @@ -487,7 +487,7 @@ public static string SearchTypeAudiobook { /// /// Looks up a localized string similar to episode. /// - public static string SearchTypeEpisode { + internal static string SearchTypeEpisode { get { return ResourceManager.GetString("SearchTypeEpisode", resourceCulture); } @@ -496,7 +496,7 @@ public static string SearchTypeEpisode { /// /// Looks up a localized string similar to playlist. /// - public static string SearchTypePlaylist { + internal static string SearchTypePlaylist { get { return ResourceManager.GetString("SearchTypePlaylist", resourceCulture); } @@ -505,7 +505,7 @@ public static string SearchTypePlaylist { /// /// Looks up a localized string similar to show. /// - public static string SearchTypeShow { + internal static string SearchTypeShow { get { return ResourceManager.GetString("SearchTypeShow", resourceCulture); } @@ -514,7 +514,7 @@ public static string SearchTypeShow { /// /// Looks up a localized string similar to track. /// - public static string SearchTypeTrack { + internal static string SearchTypeTrack { get { return ResourceManager.GetString("SearchTypeTrack", resourceCulture); } @@ -523,7 +523,7 @@ public static string SearchTypeTrack { /// /// Looks up a localized string similar to Your Top Tracks. /// - public static string TopTracksTitle { + internal static string TopTracksTitle { get { return ResourceManager.GetString("TopTracksTitle", resourceCulture); } @@ -532,7 +532,7 @@ public static string TopTracksTitle { /// /// Looks up a localized string similar to Transfer to {0}. /// - public static string TransferPlaybackCommandName { + internal static string TransferPlaybackCommandName { get { return ResourceManager.GetString("TransferPlaybackCommandName", resourceCulture); } diff --git a/CmdPal.Ext.Spotify/Properties/Resources.zh-CN.resx b/CmdPal.Ext.Spotify/Properties/Resources.zh-CN.resx index e834f30..1da72b3 100644 --- a/CmdPal.Ext.Spotify/Properties/Resources.zh-CN.resx +++ b/CmdPal.Ext.Spotify/Properties/Resources.zh-CN.resx @@ -269,7 +269,7 @@ 无法从 Spotify API 中选择有效设备 - 正在刷新过期的会话… + 正在更新会话状态… 您的熱門曲目 From 949d4297ef5e0b1b9de1472d708b12f0a53194e5 Mon Sep 17 00:00:00 2001 From: Fiatsoft Date: Mon, 4 Aug 2025 03:33:55 -0400 Subject: [PATCH 3/4] with [Hide]/KeepOpen/GoHome result-setting per PlayerCommand (by internal static SpotifyCommandsProvider.SettingsManager), track-list helper command exclusions by Type-list --- CmdPal.Ext.Spotify/Commands/PlayerCommand.cs | 22 ++- CmdPal.Ext.Spotify/Helpers/SettingsManager.cs | 38 ++++- CmdPal.Ext.Spotify/Helpers/Track.cs | 19 ++- CmdPal.Ext.Spotify/Pages/AlbumPage.cs | 2 +- ...Ext.Spotify.Properties.Resources.resources | Bin 4435 -> 6205 bytes .../Properties/Resources.Designer.cs | 135 ++++++++++++++++++ CmdPal.Ext.Spotify/Properties/Resources.resx | 45 ++++++ .../Properties/Resources.zh-CN.resx | 45 ++++++ CmdPal.Ext.Spotify/SpotifyCommandsProvider.cs | 6 +- 9 files changed, 295 insertions(+), 17 deletions(-) diff --git a/CmdPal.Ext.Spotify/Commands/PlayerCommand.cs b/CmdPal.Ext.Spotify/Commands/PlayerCommand.cs index b4e9cbb..9e9b450 100644 --- a/CmdPal.Ext.Spotify/Commands/PlayerCommand.cs +++ b/CmdPal.Ext.Spotify/Commands/PlayerCommand.cs @@ -1,7 +1,9 @@ using CmdPal.Ext.Spotify.Helpers; +using CmdPal.Ext.Spotify.Pages; using CmdPal.Ext.Spotify.Properties; using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; +using Microsoft.UI.Xaml; using Newtonsoft.Json; using SpotifyAPI.Web; using System; @@ -37,7 +39,25 @@ public override CommandResult Invoke() { Journal.Append(JsonConvert.SerializeObject(this)); EnsureActiveDeviceAsync(InvokeAsync).GetAwaiter().GetResult(); - return CommandResult.KeepOpen(); + return GetCommandResult(this); + } + + private CommandResult GetCommandResult(PlayerCommand playerCommand) + { + if (SpotifyCommandsProvider.SettingsManager.CommandResults.TryGetValue(playerCommand.GetType(), out var setting)) + return GetCommandResult(setting.Value); + return CommandResult.Hide(); + } + + private CommandResult GetCommandResult(string? value) + { + return value switch + { + "KeepOpen" => CommandResult.KeepOpen(), + "GoHome" => CommandResult.GoHome(), + "Hide" or null => CommandResult.Hide(), + _ => CommandResult.Hide() + }; } protected abstract Task InvokeAsync(IPlayerClient player, T requestParams); diff --git a/CmdPal.Ext.Spotify/Helpers/SettingsManager.cs b/CmdPal.Ext.Spotify/Helpers/SettingsManager.cs index b1ec2c0..c50429a 100644 --- a/CmdPal.Ext.Spotify/Helpers/SettingsManager.cs +++ b/CmdPal.Ext.Spotify/Helpers/SettingsManager.cs @@ -1,6 +1,11 @@ -using CmdPal.Ext.Spotify.Properties; +using CmdPal.Ext.Spotify.Commands; +using CmdPal.Ext.Spotify.Properties; using Microsoft.CommandPalette.Extensions.Toolkit; +using Microsoft.UI.Xaml.Controls; +using System; +using System.Collections.Generic; using System.IO; +using System.Linq; namespace CmdPal.Ext.Spotify.Helpers; @@ -36,6 +41,8 @@ internal static string SettingsJsonPath() public string FilterWildcard => _filterWildcard.Value; + public Dictionary CommandResults { get; } = new(); + public SettingsManager() { FilePath = SettingsJsonPath(); @@ -43,6 +50,35 @@ public SettingsManager() Settings.Add(_clientId); Settings.Add(_filterWildcard); + var choices = new List() { + new ChoiceSetSetting.Choice(Resources.ExtensionSettingCommandResultActionHide, "Hide"), + new ChoiceSetSetting.Choice(Resources.ExtensionSettingCommandResultActionKeepOpen, "KeepOpen"), + new ChoiceSetSetting.Choice(Resources.ExtensionSettingCommandResultActionGoHome, "GoHome") + }; + foreach (var type in new Type[] { + typeof(AddToQueueCommand), + typeof(LoginCommand), + typeof(PausePlaybackCommand), + typeof(ResumePlaybackCommand), + typeof(SetRepeatCommand), + typeof(SetShuffleCommand), + typeof(SkipNextCommand), + typeof(SkipPreviousCommand), + typeof(TogglePlaybackCommand), + typeof(TransferPlaybackCommand) + }) + { + CommandResults.Add(type, new ChoiceSetSetting( + key: type.Name, + label: string.Format(Resources.ExtensionSettingCommandResultSettingLabel, Resources.ResourceManager.GetString(type.Name)), + description: string.Format(Resources.ExtensionSettingCommandResultSettingDesc, type.Name), + choices: choices + ) + ); + } + foreach (var choiceSetSetting in CommandResults.Values) + Settings.Add(choiceSetSetting); + LoadSettings(); Settings.SettingsChanged += (s, a) => SaveSettings(); diff --git a/CmdPal.Ext.Spotify/Helpers/Track.cs b/CmdPal.Ext.Spotify/Helpers/Track.cs index dab5559..ccdd1c0 100644 --- a/CmdPal.Ext.Spotify/Helpers/Track.cs +++ b/CmdPal.Ext.Spotify/Helpers/Track.cs @@ -13,22 +13,19 @@ namespace CmdPal.Ext.Spotify.Helpers { internal class Track { - public static List ListItems(IList For, SpotifyClient _spotifyClient, bool includeGoToAlbum = true) + public static List ListItems(IList For, SpotifyClient _spotifyClient, IList Without = null) { + if (Without == null) + Without = new List(); return For.Where(track => track != null) .Select(track => { - var playCommand = new ResumePlaybackCommand(_spotifyClient, new PlayerResumePlaybackRequest() { Uris = [track.Uri] }); - var queueCommand = new AddToQueueCommand(_spotifyClient, new PlayerAddToQueueRequest(track.Uri)); - - var moreCommands = new List - { - new(queueCommand) - }; - - if (includeGoToAlbum) + var moreCommands = new List(); + if (!Without.Contains(typeof(ResumePlaybackCommand))) + moreCommands.Add(new CommandContextItem(new AddToQueueCommand(_spotifyClient, new PlayerAddToQueueRequest(track.Uri)))); + if (!Without.Contains(typeof(AlbumPage))) moreCommands.Add(new CommandContextItem(new AlbumPage(_spotifyClient, track.Album.Id, track.Album.Name))); - return new ListItem(playCommand) { + return new ListItem(new ResumePlaybackCommand(_spotifyClient, new PlayerResumePlaybackRequest() { Uris = [track.Uri] })) { Title = track.Name, Subtitle = $"{Resources.ResultSongSubTitle}{(track.Explicit ? $" • {Resources.ResultSongExplicitSubTitle}" : "")} • {Resources.ResultSongBySubTitle} {string.Join(", ", track.Artists.Select(x => x.Name))}", Icon = new IconInfo(track.Album.Images.OrderBy(x => x.Width * x.Height).FirstOrDefault()?.Url), diff --git a/CmdPal.Ext.Spotify/Pages/AlbumPage.cs b/CmdPal.Ext.Spotify/Pages/AlbumPage.cs index 1232fe7..145cefe 100644 --- a/CmdPal.Ext.Spotify/Pages/AlbumPage.cs +++ b/CmdPal.Ext.Spotify/Pages/AlbumPage.cs @@ -71,7 +71,7 @@ public override IListItem[] GetItems() Id = album.Id, Name = album.Name } - }).ToList(), spotifyClient, includeGoToAlbum: false).ToArray(); + }).ToList(), spotifyClient, Without: new List() {typeof(AlbumPage)}).ToArray(); } catch (Exception ex) { diff --git a/CmdPal.Ext.Spotify/Properties/CmdPal.Ext.Spotify.Properties.Resources.resources b/CmdPal.Ext.Spotify/Properties/CmdPal.Ext.Spotify.Properties.Resources.resources index 0bf465d5463aeb183c8349e158baa37019ee51c9..d987dc4d171be27433c4bf0ee0925eb8d2e32bdc 100644 GIT binary patch delta 1984 zcma)+du&rx9LIn6^xnQ!Hee41blYWdvW*bBM>i1&RtBqXj&5xb=Sb+*+lREhWo?lm zI8ZhS@CV~KBMBI%i;ob9WIhrc0*R4FqQ)VA%tWI`MPmZ$<}?l$zqcsFpmjYx=bm$Z z-_P%HdU`JxpSB$wSXf6lKQ2d|_7w_`?+n}7u21_vdirA3AG-?*7WX`3dbzjnY<~Xl zdm3Nsr_r~TT>5c%KpZQuQ@ND8moC{a!gl!VEou`k%__` zTYjzk`je?Sf%n=}XR2)Ml{Qh|0)2GRZSbsF~F&7Zuqu=Q5khpE5xrt^b5KL z4s+n}T|BSD!B=pw0QN!56G-F&9BqRG6W|T#<4EEVG;8o&h5fe?U>JJsQwo1SLt_P3 zgn+%6pFr}N&~1jF)zH|p@`-+cpc(xe7(WBthU)gt9%^MjF`dk5voZeE2cv_;lT&01JpW=S~?x70fa&^T8pkAeh9Qj zqig?*UW4p9k+ToYgLyNmP*Obs_CbwZoStn26+pI8+6SUlBMQQL7&VWU0P06Y4ZoQ3H8RP3Cvb_83VUo zN!%MfE^_KDR;K5*-zR{k?-5)^d&4`#~u0#X4mm^`Z9i0U(CGPw1GXRNTiM}DDt>*5$h583&tweC-N)CA~qoM z3F8v>ipUq5s@O@9x0&qHInXuyV^bl2!<5fPME;?vf_*P?!EBdqz*fo!%sG6mIY*j> zS3`AbwcRYUT!}w#UdW0he%@?nB@&+kPy>mL3mJL5DPtaUO1#N3C(D71+Hw6N^a#u{ zZ?@#ja^Pxcryj?+5xaM5yZmCtY$=FUv9`)yjjN|A)YJz4fU$*R3nNYIzv81Ff}MjOkNvPqC$?t z=aW6E+?-UBio8mVM7&X-Fr!fogrc{Wc`k1lipalg`er)lxSL1YlG464bnPfi3C*&S08A%B?&8L$wROI$?RLZCZ4dmuriYimG(Vj>R@XDOr)xOS z`!;-c*jJZa-}Oe^JD%AZK{P=7b+i?rxDg+|d$+9T3N zRK|)iiMv8e@DZf#cp=unQ;I{X!mvoCDo32ARe996<4eVW0SP0@2AG!cm$C(9z%11d zo|WXrC#p*X=;C7Zs7W}fronwc)$E1=K%Y4ih5%0xy#s7FuZMqt{pKPOL)Ycupg98# ziwS%jH(4^_0LR0YTsX?H&5{D=Ili!DLp{d{YBq9AuzI0`<6dil2-5gwtgt%qnN<_7 zh;pJdZneV)j&s&bnBeHtd}5NcR9vUo@vNr7497;zD;A>zbJ`jl)f^yXoY8VcBK-v? zhIKm@>L!s+WJIZ2S4B2aT9le~oA432qSU8bphU(YeWUPG(-Cd{(c?rV(dsg%%>+ke Y%&?_Ejf{R<0bG + /// Looks up a localized string similar to Go Home. + /// + internal static string ExtensionSettingCommandResultActionGoHome { + get { + return ResourceManager.GetString("ExtensionSettingCommandResultActionGoHome", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Hide. + /// + internal static string ExtensionSettingCommandResultActionHide { + get { + return ResourceManager.GetString("ExtensionSettingCommandResultActionHide", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Keep Open. + /// + internal static string ExtensionSettingCommandResultActionKeepOpen { + get { + return ResourceManager.GetString("ExtensionSettingCommandResultActionKeepOpen", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Add To Queue Command. + /// + internal static string ExtensionSettingCommandResultForAddToQueue { + get { + return ResourceManager.GetString("ExtensionSettingCommandResultForAddToQueue", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Login Command. + /// + internal static string ExtensionSettingCommandResultForLogin { + get { + return ResourceManager.GetString("ExtensionSettingCommandResultForLogin", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Pause Playback Command. + /// + internal static string ExtensionSettingCommandResultForPausePlayback { + get { + return ResourceManager.GetString("ExtensionSettingCommandResultForPausePlayback", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Resume Playback Command. + /// + internal static string ExtensionSettingCommandResultForResumePlayback { + get { + return ResourceManager.GetString("ExtensionSettingCommandResultForResumePlayback", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Set Repeat Command. + /// + internal static string ExtensionSettingCommandResultForSetRepeat { + get { + return ResourceManager.GetString("ExtensionSettingCommandResultForSetRepeat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Set Shuffle Command. + /// + internal static string ExtensionSettingCommandResultForSetShuffle { + get { + return ResourceManager.GetString("ExtensionSettingCommandResultForSetShuffle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Skip Next Command. + /// + internal static string ExtensionSettingCommandResultForSkipNext { + get { + return ResourceManager.GetString("ExtensionSettingCommandResultForSkipNext", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Skip Previous Command. + /// + internal static string ExtensionSettingCommandResultForSkipPrevious { + get { + return ResourceManager.GetString("ExtensionSettingCommandResultForSkipPrevious", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Toggle Playback Command. + /// + internal static string ExtensionSettingCommandResultForTogglePlayback { + get { + return ResourceManager.GetString("ExtensionSettingCommandResultForTogglePlayback", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Transfer Playback Command. + /// + internal static string ExtensionSettingCommandResultForTransferPlayback { + get { + return ResourceManager.GetString("ExtensionSettingCommandResultForTransferPlayback", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to What should the Spotify control do after {0}?. + /// + internal static string ExtensionSettingCommandResultSettingDesc { + get { + return ResourceManager.GetString("ExtensionSettingCommandResultSettingDesc", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to After {0}. + /// + internal static string ExtensionSettingCommandResultSettingLabel { + get { + return ResourceManager.GetString("ExtensionSettingCommandResultSettingLabel", resourceCulture); + } + } + /// /// Looks up a localized string similar to Filter Wildcard. /// diff --git a/CmdPal.Ext.Spotify/Properties/Resources.resx b/CmdPal.Ext.Spotify/Properties/Resources.resx index 8ca6a4d..a40a235 100644 --- a/CmdPal.Ext.Spotify/Properties/Resources.resx +++ b/CmdPal.Ext.Spotify/Properties/Resources.resx @@ -277,4 +277,49 @@ as {0} ({1}) Transfer to {0} + + Hide + + + Keep Open + + + Go Home + + + After {0} + + + What should the Spotify control do after {0}? + + + Add To Queue Command + + + Login Command + + + Pause Playback Command + + + Resume Playback Command + + + Set Repeat Command + + + Set Shuffle Command + + + Skip Next Command + + + Skip Previous Command + + + Toggle Playback Command + + + Transfer Playback Command + \ No newline at end of file diff --git a/CmdPal.Ext.Spotify/Properties/Resources.zh-CN.resx b/CmdPal.Ext.Spotify/Properties/Resources.zh-CN.resx index 1da72b3..cbf0bf2 100644 --- a/CmdPal.Ext.Spotify/Properties/Resources.zh-CN.resx +++ b/CmdPal.Ext.Spotify/Properties/Resources.zh-CN.resx @@ -277,4 +277,49 @@ 传输至 {0} + + 隐藏 + + + 保持打开 + + + 回到首页 + + + {0} 之后 + + + {0} 之后 Spotify 控件应执行什么操作? + + + 添加到队列命令 + + + 登录命令 + + + 暂停播放命令 + + + 恢复播放命令 + + + 设置重复命令 + + + 设置随机播放命令 + + + 跳过下一首命令 + + + 跳过上一首命令 + + + 切换播放状态命令 + + + 转移播放命令 + \ No newline at end of file diff --git a/CmdPal.Ext.Spotify/SpotifyCommandsProvider.cs b/CmdPal.Ext.Spotify/SpotifyCommandsProvider.cs index e80d4e0..6fafb6d 100644 --- a/CmdPal.Ext.Spotify/SpotifyCommandsProvider.cs +++ b/CmdPal.Ext.Spotify/SpotifyCommandsProvider.cs @@ -9,15 +9,15 @@ namespace CmdPal.Ext.Spotify; public partial class SpotifyCommandsProvider : CommandProvider { private readonly CommandItem _command; - private static readonly SettingsManager _settingsManager = new(); - private static readonly SpotifyListPage _spotifyExtensionPage = new(_settingsManager); + internal static readonly SettingsManager SettingsManager = new(); + private static readonly SpotifyListPage _spotifyExtensionPage = new(SettingsManager); public SpotifyCommandsProvider() { DisplayName = Resources.ExtensionDisplayName; Id = "Spotify"; Icon = Icons.Spotify; - Settings = _settingsManager.Settings; + Settings = SettingsManager.Settings; _command = new CommandItem(_spotifyExtensionPage) { From 7ddd265cba53b599c5adea5817101e50a10dd819 Mon Sep 17 00:00:00 2001 From: Fiatsoft Date: Tue, 5 Aug 2025 19:03:23 -0400 Subject: [PATCH 4/4] fix device-cache, with transfer-device page instead of root context-menu items, fix Helpers.Track.ListItems command-filtering, CommandResults settings/naming-scheme --- CmdPal.Ext.Spotify/Assets/Dark/refresh.png | Bin 0 -> 1677 bytes CmdPal.Ext.Spotify/Assets/Light/refresh.png | Bin 0 -> 1677 bytes CmdPal.Ext.Spotify/Commands/PlayerCommand.cs | 4 +- CmdPal.Ext.Spotify/Helpers/Cache.cs | 11 +- CmdPal.Ext.Spotify/Helpers/Icons.cs | 2 +- CmdPal.Ext.Spotify/Helpers/SettingsManager.cs | 32 ++--- CmdPal.Ext.Spotify/Helpers/Track.cs | 2 +- CmdPal.Ext.Spotify/Pages/DevicesPage.cs | 81 +++++++++++++ CmdPal.Ext.Spotify/Pages/SpotifyListPage.cs | 27 +---- ...Ext.Spotify.Properties.Resources.resources | Bin 6205 -> 5837 bytes .../Properties/Resources.Designer.cs | 113 ++++++++++-------- CmdPal.Ext.Spotify/Properties/Resources.resx | 33 ++--- .../Properties/Resources.zh-CN.resx | 33 ++--- 13 files changed, 207 insertions(+), 131 deletions(-) create mode 100644 CmdPal.Ext.Spotify/Assets/Dark/refresh.png create mode 100644 CmdPal.Ext.Spotify/Assets/Light/refresh.png create mode 100644 CmdPal.Ext.Spotify/Pages/DevicesPage.cs diff --git a/CmdPal.Ext.Spotify/Assets/Dark/refresh.png b/CmdPal.Ext.Spotify/Assets/Dark/refresh.png new file mode 100644 index 0000000000000000000000000000000000000000..b6cbc600e2433b725ed93cce5ae42f5d93ec61bc GIT binary patch literal 1677 zcmV;826Fj{P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!~g&e!~vBn4jTXf1KUYNK~!i%-I+0H z6hRb*?<7JBi!?%t2q}V9gcOz*_F4o)u&_v(B89DJ5wNgGV`q^fMG6Da&QvK3XlG?* zVL-t^1Q7xe6cQ4@f9}mWxBK?)a<`Y;%MU+q?(LhIe|F#P&c3amo}Si58JEnCEc&?u z%VvwPV;m^@SwN?bui^bhGvGG-2KV5x9A%)4D{vWJk7T^@dc7{c&}@J(5yuZOZ{Zek zvcKi;8CpP(oIE)>S%rUSr}$;|6Kuv4_YQv1fe7`5YcwZeI(FA|ogFvdmo^~T>{5xei*iR!8Ghhle9Iw|KYvR+%fycY!wi-L~TuEQ)o=-!)kTg zx`x%>eds{T6k5Jx53ow3=KDnU3Mf$GcdH^^kQUQ4!Oy9Nw}I>erp;DHe1fPcY7A&u z0IQs|W@Q(UXbi}ukl7z*tAwbjZ3>MXvI~f*({hXU8J1>&%jY(&DbtN%`r<_CXc0LfQy6rx({k*IB@{ysnCwicVf! zFVx*;8>_L)FRw>3aP|YoI?R6H=$h&a*I-PUfqvkK)E%MSCnm2?lZvs}Qa&;D^OaH$ z7CosS+EY6KpSB*_#ftIXt(OsPhqbHJ)*yh`zZgCBA7lOddsh?TPSU?IrHFc(^88@DFVGpr7!M=zJOZoAFj>ynoF3xSpWb4 z07*qoM6N<$f&q+-`~l2O58Y6TbG`ro002ovPDHLkV1gCXkHV58-t>)VKfu002ov zPDHLkV1nU>CTz&oaa;i3fxFZMuILL5sAWR1p&_3@PR1&UB*6E+HkP)OeqgrlLkcd< zHH5IG6z@JMiX;%*R`Pe7Y(J;mwF8zuy1Tn=;AF<_Bq@rPK#y^v$=_{!tgWq?Qo0Th znS!<$92|7*8NPxn#z%^xPrwZ&lE2$zLayL)K?r^fvKW`8D24=FdO`Ac8!25_S&7Zj z)S$0m*pPpK8GjG56vdEq1jahSNye3;P|!y(VMzv`>cfX9W+L#~G~md!nlGGe6Fda3 zrA@HTTXIL3^Uhn|NAFW|3eyWmf)5C1FizVXn3_Y0-|16RPALjmIs@c;k- z07*qoM6N<$f|t+_$mhqxVVU@6Q?3ACbkVO-fq2zVrTQwRq*<=mDKrG-zS@*2KHr)6 z`BdBG>d-#3VrX5&hVP9A)u)gTS5tpMTyOje{f53P{?vM?--+J^sTJ9g;{6G{Ta5W1 XkXqP@iLime00000NkvXXu0mjf_|6J{ literal 0 HcmV?d00001 diff --git a/CmdPal.Ext.Spotify/Assets/Light/refresh.png b/CmdPal.Ext.Spotify/Assets/Light/refresh.png new file mode 100644 index 0000000000000000000000000000000000000000..2f186ad8198b419a814971c2f5fbfd237fbcc179 GIT binary patch literal 1677 zcmV;826Fj{P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!~g&e!~vBn4jTXf1KdeOK~!i%-I=jZ z6j2byxeF!~6f`Cj6coe?6ADWUdo4(8C@d@~C}@oqBo-9@2^16*nh-mvC?wI&%F03# zEl4miMiWdlfpGr5W#)LdH+yi$p1bFle9U8R-pu!I-`>9Ww$W;}#zq-Uvm=XvuRx>G zShHpk_Q}0(?cqTK7=-G23Ac0c_uwoXPyN?r8CT#2yye6{;W@F5tO4+6PV7(24&3GQ z9r#QBfuUl95KJRx1O8T;gx}46NaKln13x8kE%k+)w5QY9ei%A?5lq1~7j7c<;4+-i zXY$KN{S{-e>T{ChSf@P|_36dXSwxD=!_OvAg}32c8UuO`Af@`kmlm&lNAOM*Tg1>A z5iFAY*b2SJ-Ytl`@&KF?Z8??u7%oL|5kqI5BwRI7DzJr}jN(Ml_W(pNPVNost2(Pu zT%MsbC0MkCC-7MmlM)Ox04Ft8RLgQKMlmTvr;|jQ^FD1Z6<-#qwQMyto#vw$XXq5c z6rHmh7P}S243pH%rjuzX{B;VQBFLr!33PK6&iP>x=$?2h4X|)J#?JGHls}-tUz>kH zupkYlC4AU*IYc`6G)Wu(YA|i&2cR93P#f_HT$5|e&@li;Ic?3_ z4j|E(p=%16{c5&Oa7{g@(8wV_0G~P?m#HQQr+3EyxXZ)KR1;*H5{()1SKy1;+71A? z#&hUl-~^-nAj5Hr(V>&jweF}8u0;o7(%44yE3|9&q1igXwYau6{=en41-!y)N&P%? zR*R=-8OKUf_c|AJ!+cD&%|hw2MAp--9Nz2IRL)%6%^FNPEYDb|IIUUqP0!NyZ(TT~j=o`SM%3@Q1$cSpXy;>`d6 z002ovPDHLkV1kT{`~l2O58Y6TbG`ro002ovPDHLkV1gCXkHV58-t>)VKfu002ov zPDHLkV1nU>CTz&oaa;i3fxFZMuILL5sAWR1p&_3@PR1&UB*6E+HkP)OeqgrlLkcd< zHH5IG6z@JMiX;%*R`Pe7Y(J;mwF8zuy1Tn=;AF<_Bq@rPK#y^v$=_{!tgWq?Qo0Th znS!<$92|7*8NPxn#z%^xPrwZ&lE2$zLayL)K?r^fvKW`8D24=FdO`Ac8!25_S&7Zj z)S$0m*pPpK8GjG56vdEq1jahSNye3;P|!y(VMzv`>cfX9W+L#~G~md!nlGGe6Fda3 zrA@HTTXIL3^Uhn|NAFW|3eyWmf)5C1FizVXn3_Y0-|16RPALjmIs@c;k- z07*qoM6N<$f|t+_$mhqxVVU@6Q?3ACbkVO-fq2zVrTQwRq*<=mDKrG-zS@*2KHr)6 z`BdBG>d-#3VrX5&hVP9A)u)gTS5tpMTyOje{f53P{?vM?--+J^sTJ9g;{6G{Ta5W1 XkXqP@iLime00000NkvXXu0mjf!@m+_ literal 0 HcmV?d00001 diff --git a/CmdPal.Ext.Spotify/Commands/PlayerCommand.cs b/CmdPal.Ext.Spotify/Commands/PlayerCommand.cs index 9e9b450..e6b728f 100644 --- a/CmdPal.Ext.Spotify/Commands/PlayerCommand.cs +++ b/CmdPal.Ext.Spotify/Commands/PlayerCommand.cs @@ -44,7 +44,7 @@ public override CommandResult Invoke() private CommandResult GetCommandResult(PlayerCommand playerCommand) { - if (SpotifyCommandsProvider.SettingsManager.CommandResults.TryGetValue(playerCommand.GetType(), out var setting)) + if (SpotifyCommandsProvider.SettingsManager.CommandResults.TryGetValue(playerCommand.GetType().Name, out var setting)) return GetCommandResult(setting.Value); return CommandResult.Hide(); } @@ -103,7 +103,7 @@ protected async Task EnsureActiveDeviceAsync(Func callba } // 🧊 Attempt to load from local cache - var cachedDevices = Cache.LoadDevices(); + var cachedDevices = Cache.GetDevices(); if (cachedDevices != null && cachedDevices?.Count > 0) { var selected = SelectBestDevice(cachedDevices); diff --git a/CmdPal.Ext.Spotify/Helpers/Cache.cs b/CmdPal.Ext.Spotify/Helpers/Cache.cs index 8193b29..a8b9a47 100644 --- a/CmdPal.Ext.Spotify/Helpers/Cache.cs +++ b/CmdPal.Ext.Spotify/Helpers/Cache.cs @@ -1,16 +1,20 @@ -//using System.Text.Json; -using Newtonsoft.Json; +using Newtonsoft.Json; using SpotifyAPI.Web; using Swan.Formatters; using System; using System.Collections.Generic; using System.IO; +using System.Threading.Tasks; namespace CmdPal.Ext.Spotify.Helpers; internal class Cache { - public static List? LoadDevices(string? file = null) + private static List? _cachedDevices = null; + private static DateTime _lastFetched = DateTime.MinValue; + private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(1); + + public static List? GetDevices(string? file = null) { try { @@ -32,7 +36,6 @@ internal class Cache } } - public static void SaveDevices(IList devices, string? file = null) { try diff --git a/CmdPal.Ext.Spotify/Helpers/Icons.cs b/CmdPal.Ext.Spotify/Helpers/Icons.cs index 93d0b94..94a3df2 100644 --- a/CmdPal.Ext.Spotify/Helpers/Icons.cs +++ b/CmdPal.Ext.Spotify/Helpers/Icons.cs @@ -18,5 +18,5 @@ internal sealed class Icons internal static IconInfo Device { get; } = FromRelativePath("device.png"); internal static IconInfo Speaker { get; } = FromRelativePath("speaker.png"); internal static IconInfo Album { get; } = FromRelativePath("album.png"); - + internal static IconInfo Refresh { get; } = FromRelativePath("refresh.png"); } diff --git a/CmdPal.Ext.Spotify/Helpers/SettingsManager.cs b/CmdPal.Ext.Spotify/Helpers/SettingsManager.cs index c50429a..1f16304 100644 --- a/CmdPal.Ext.Spotify/Helpers/SettingsManager.cs +++ b/CmdPal.Ext.Spotify/Helpers/SettingsManager.cs @@ -41,7 +41,7 @@ internal static string SettingsJsonPath() public string FilterWildcard => _filterWildcard.Value; - public Dictionary CommandResults { get; } = new(); + public Dictionary CommandResults { get; } = new(); public SettingsManager() { @@ -55,23 +55,23 @@ public SettingsManager() new ChoiceSetSetting.Choice(Resources.ExtensionSettingCommandResultActionKeepOpen, "KeepOpen"), new ChoiceSetSetting.Choice(Resources.ExtensionSettingCommandResultActionGoHome, "GoHome") }; - foreach (var type in new Type[] { - typeof(AddToQueueCommand), - typeof(LoginCommand), - typeof(PausePlaybackCommand), - typeof(ResumePlaybackCommand), - typeof(SetRepeatCommand), - typeof(SetShuffleCommand), - typeof(SkipNextCommand), - typeof(SkipPreviousCommand), - typeof(TogglePlaybackCommand), - typeof(TransferPlaybackCommand) + foreach (var commandName in new string[] { + nameof(AddToQueueCommand), + nameof(LoginCommand), + nameof(PausePlaybackCommand), + nameof(ResumePlaybackCommand), + nameof(SetRepeatCommand), + nameof(SetShuffleCommand), + nameof(SkipNextCommand), + nameof(SkipPreviousCommand), + nameof(TogglePlaybackCommand), + nameof(TransferPlaybackCommand) }) { - CommandResults.Add(type, new ChoiceSetSetting( - key: type.Name, - label: string.Format(Resources.ExtensionSettingCommandResultSettingLabel, Resources.ResourceManager.GetString(type.Name)), - description: string.Format(Resources.ExtensionSettingCommandResultSettingDesc, type.Name), + CommandResults.Add(commandName, new ChoiceSetSetting( + key: commandName, + label: string.Format(Resources.ExtensionSettingCommandResultLabel, Resources.ResourceManager.GetString($"Name{commandName}")), + description: string.Format(Resources.ExtensionSettingCommandResultDesc, Resources.ResourceManager.GetString($"Name{commandName}")), choices: choices ) ); diff --git a/CmdPal.Ext.Spotify/Helpers/Track.cs b/CmdPal.Ext.Spotify/Helpers/Track.cs index ccdd1c0..d39b0b6 100644 --- a/CmdPal.Ext.Spotify/Helpers/Track.cs +++ b/CmdPal.Ext.Spotify/Helpers/Track.cs @@ -21,7 +21,7 @@ public static List ListItems(IList For, SpotifyClient _spot .Select(track => { var moreCommands = new List(); - if (!Without.Contains(typeof(ResumePlaybackCommand))) + if (!Without.Contains(typeof(AddToQueueCommand))) moreCommands.Add(new CommandContextItem(new AddToQueueCommand(_spotifyClient, new PlayerAddToQueueRequest(track.Uri)))); if (!Without.Contains(typeof(AlbumPage))) moreCommands.Add(new CommandContextItem(new AlbumPage(_spotifyClient, track.Album.Id, track.Album.Name))); diff --git a/CmdPal.Ext.Spotify/Pages/DevicesPage.cs b/CmdPal.Ext.Spotify/Pages/DevicesPage.cs new file mode 100644 index 0000000..69acd43 --- /dev/null +++ b/CmdPal.Ext.Spotify/Pages/DevicesPage.cs @@ -0,0 +1,81 @@ +using CmdPal.Ext.Spotify.Commands; +using CmdPal.Ext.Spotify.Helpers; +using CmdPal.Ext.Spotify.Properties; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Newtonsoft.Json; +using SpotifyAPI.Web; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace CmdPal.Ext.Spotify.Pages +{ + internal partial class DevicesPage : ListPage + { + private readonly SpotifyClient _spotifyClient; + + public DevicesPage(SpotifyClient spotifyClient) + { + _spotifyClient = spotifyClient; + Name = "Devices"; + Title = Name; + Icon = Icons.Device; + } + + public override IListItem[] GetItems() + { + var commands = new List(); + try + { + var devices = Cache.GetDevices(); + if (devices == null || !devices.Any()) + { + try + { + devices = _spotifyClient.Player.GetAvailableDevices().Result.Devices.ToList(); + Cache.SaveDevices(devices); + new ToastStatusMessage(new StatusMessage() { Message = Resources.DeviceCacheSavedToast, State = MessageState.Info }).Show(); + } + catch (Exception ex) + { + new ToastStatusMessage(new StatusMessage { Message = Resources.DeviceCacheErrorToast, State = MessageState.Error }).Show(); + Journal.Append($"{Resources.DeviceCacheErrorToast}: {ex.Message}",label: Journal.Label.Error); + devices = Cache.GetDevices() ?? new List(); + } + } + + foreach (var device in devices) + { + commands.Add(new ListItem(new TransferPlaybackCommand(_spotifyClient, device.Id, device.Name))); + } + + commands.Add(new ListItem(new AnonymousCommand(() => + { + try + { + Cache.SaveDevices(_spotifyClient.Player.GetAvailableDevices().Result.Devices.ToList()); + new ToastStatusMessage(new StatusMessage() { Message = Resources.DeviceCacheSavedToast, State = MessageState.Info }).Show(); + } + catch (Exception ex) + { + Journal.Append($"Failed to refresh devices: {ex.Message}",label: Journal.Label.Error); + } + RaiseItemsChanged(); + }) + { + Icon = Icons.Refresh, + Name = Resources.DevicesRefreshCommandName, + Result = CommandResult.KeepOpen() + })); + } + catch (Exception ex) + { + Journal.Append( $"Problem getting devices: {ex}", label: Journal.Label.Error); + } + + return commands.ToArray(); + } + } +} diff --git a/CmdPal.Ext.Spotify/Pages/SpotifyListPage.cs b/CmdPal.Ext.Spotify/Pages/SpotifyListPage.cs index 9bc705d..66fa67e 100644 --- a/CmdPal.Ext.Spotify/Pages/SpotifyListPage.cs +++ b/CmdPal.Ext.Spotify/Pages/SpotifyListPage.cs @@ -208,33 +208,10 @@ public List GetPlayerCommands() new SetRepeatCommand(_spotifyClient, new(PlayerSetRepeatRequest.State.Track)), new SetRepeatCommand(_spotifyClient, new(PlayerSetRepeatRequest.State.Context)), new SetRepeatCommand(_spotifyClient, new(PlayerSetRepeatRequest.State.Off)), - new TopTrackPage(_spotifyClient) + new TopTrackPage(_spotifyClient), + new DevicesPage(_spotifyClient) }; - if (_spotifyClient != null) - { - try - { - var devices = _spotifyClient.Player.GetAvailableDevices().Result; - Cache.SaveDevices(devices.Devices); - foreach (var device in devices.Devices) - { - commands.Add(new TransferPlaybackCommand(_spotifyClient, device.Id, device.Name)); - } - new ToastStatusMessage(new StatusMessage() { Message = Resources.DeviceCacheSavedToast, State = MessageState.Info }).Show(); - } - catch (Exception ex) - { - new ToastStatusMessage(new StatusMessage() { Message = Resources.DeviceCacheErrorToast, State = MessageState.Error }).Show(); - Journal.Append($"{Resources.ResourceManager.GetString("DeviceCacheErrorToast", CultureInfo.InvariantCulture)}: {ex.Message}", label: Journal.Label.Error); - - var cached = CmdPal.Ext.Spotify.Helpers.Cache.LoadDevices(); - foreach (var device in cached) - { - commands.Add(new TransferPlaybackCommand(_spotifyClient, device.Id, device.Name)); - } - } - } return commands; } diff --git a/CmdPal.Ext.Spotify/Properties/CmdPal.Ext.Spotify.Properties.Resources.resources b/CmdPal.Ext.Spotify/Properties/CmdPal.Ext.Spotify.Properties.Resources.resources index d987dc4d171be27433c4bf0ee0925eb8d2e32bdc..a200681e002feb9350811d01f5eb61634ec77aff 100644 GIT binary patch delta 1438 zcmZWoeN2>f9RL1~djPq&=Z+&CFV}UzfpDw<=aEnkU*u-;Rk%Y00PYcc=Z}Mz4$VdvW?$*Q>l*m$ zZcrcJ>zb{+Z`!+OChO+a`Q>F772f@SDpw?^LR%dw)$vjy5c;U>s zh>ok5RvekuMrz+Y85uluFN)qMj{EA)3G3>{*o5ifWII=gI9dH(PkxOf{^jtw*jH{e z2oHZE%uRPRL^76JLG(~X^eWaG;GR&T5%?cUMD4IeAw-k#$AXAb;EzIcUx35=hzKTn zANE(|%r7HqL@g(*7rFr@Q4Dl8)KCDI!S6);XT%>5r6#ICQaTFkN5NW%t!S_X!A@`; zu-72IttQGv^QkbRE8sIwBpKR|VV6O(7dici^ZegQCh?-PbzH+m0+WkEbN>iIi*Yt z_Z-)RSoo+EC)_+FKz}rcJaP*tebqs>JP~D#Y3#HwWZygvu~-)e=z`MD~9~d?qA{s|;FoKS)P_ z)P=_Q$R;ny#6Y=RbgTEj^j+{g@c-NUHpGjgE{Jof97C4kuK3@mk07`HTYD!4-9N|S`M;?mf}BQDk?DMFO|ktSATa<68sl#Qe7+EPm9j*NS)eygpGn0500k&FMp>!$1qQOolLVj8_`W s;&Xn%kS@OE7K0~Dx4ERWwxq7yoYi~XU=qC(Mp^J&_;0y9>MCNO-rqciic` z)l>1_VVXL9|K#;iopXYn$@&;CL zSY)fEcS5yl&v0#vFQ<-&L`H|jwW@u$o#?1Wl%x>dHWGE&h^7`1T~Ue7Azor3sz6L6 z5T&CZk>CwPK@-tNC+q;;jl`DMHkS;cHve6^L zyvA4{I(dgNSM>1_W3d?Ji$JEUJm_E*kcvx9eQaHeyyAoB5I+rx5Nitw#&dPTB@_LrnFgm(wnKj@Hr^+#4{irsqLL zS{t~>;#^Xcs{qkN+u*VfYF%8F3o552q{^d+*T>^o(X1!o)(X!Z=&Puig0!19S;x@Zmx)E=P^QNdLU(hpT0K(nHoY8XH7G6pdm0__~Y^+*2WLo9^ zuU3IAZHC@Vop5*{X07tgMK0xa@L3r7lzxQ^%`5ToLuQZSH1GlQa#6%5Efpe&XC7x- zGi4u6<{JLMlERHvk2uf!ta(TnwHC;0P&}NdF6DWvuKZzOP0bQs_NlAIN^Vh$#a2F{ z7KkqXUiHgf{Cixy+~!EAL}`()GsuNDuQ_2K*s<8 diff --git a/CmdPal.Ext.Spotify/Properties/Resources.Designer.cs b/CmdPal.Ext.Spotify/Properties/Resources.Designer.cs index 747a90e..70e1326 100644 --- a/CmdPal.Ext.Spotify/Properties/Resources.Designer.cs +++ b/CmdPal.Ext.Spotify/Properties/Resources.Designer.cs @@ -96,6 +96,15 @@ internal static string DeviceCacheSavedToast { } } + /// + /// Looks up a localized string similar to Refresh Devices. + /// + internal static string DevicesRefreshCommandName { + get { + return ResourceManager.GetString("DevicesRefreshCommandName", resourceCulture); + } + } + /// /// Looks up a localized string similar to An error occured.. /// @@ -223,156 +232,156 @@ internal static string ExtensionSettingCommandResultActionKeepOpen { } /// - /// Looks up a localized string similar to Add To Queue Command. + /// Looks up a localized string similar to What should the Spotify control do after {0}?. /// - internal static string ExtensionSettingCommandResultForAddToQueue { + internal static string ExtensionSettingCommandResultDesc { get { - return ResourceManager.GetString("ExtensionSettingCommandResultForAddToQueue", resourceCulture); + return ResourceManager.GetString("ExtensionSettingCommandResultDesc", resourceCulture); } } /// - /// Looks up a localized string similar to Login Command. + /// Looks up a localized string similar to After {0}. /// - internal static string ExtensionSettingCommandResultForLogin { + internal static string ExtensionSettingCommandResultLabel { get { - return ResourceManager.GetString("ExtensionSettingCommandResultForLogin", resourceCulture); + return ResourceManager.GetString("ExtensionSettingCommandResultLabel", resourceCulture); } } /// - /// Looks up a localized string similar to Pause Playback Command. + /// Looks up a localized string similar to Filter Wildcard. /// - internal static string ExtensionSettingCommandResultForPausePlayback { + internal static string ExtensionSettingFilterWildcard { get { - return ResourceManager.GetString("ExtensionSettingCommandResultForPausePlayback", resourceCulture); + return ResourceManager.GetString("ExtensionSettingFilterWildcard", resourceCulture); } } /// - /// Looks up a localized string similar to Resume Playback Command. + /// Looks up a localized string similar to Filter wildcard character used to prepend a filter, e.g. /album or !album. /// - internal static string ExtensionSettingCommandResultForResumePlayback { + internal static string ExtensionSettingFilterWildcardDescription { get { - return ResourceManager.GetString("ExtensionSettingCommandResultForResumePlayback", resourceCulture); + return ResourceManager.GetString("ExtensionSettingFilterWildcardDescription", resourceCulture); } } /// - /// Looks up a localized string similar to Set Repeat Command. + /// Looks up a localized string similar to Search through and control Spotify + ///as {0} ({1}). /// - internal static string ExtensionSettingCommandResultForSetRepeat { + internal static string ExtensionStatusDescription { get { - return ResourceManager.GetString("ExtensionSettingCommandResultForSetRepeat", resourceCulture); + return ResourceManager.GetString("ExtensionStatusDescription", resourceCulture); } } /// - /// Looks up a localized string similar to Set Shuffle Command. + /// Looks up a localized string similar to 🎧 Logged in as {0} ({1}). /// - internal static string ExtensionSettingCommandResultForSetShuffle { + internal static string LoginSuccessToast { get { - return ResourceManager.GetString("ExtensionSettingCommandResultForSetShuffle", resourceCulture); + return ResourceManager.GetString("LoginSuccessToast", resourceCulture); } } /// - /// Looks up a localized string similar to Skip Next Command. + /// Looks up a localized string similar to ✅ Logged in, but failed to get user info. /// - internal static string ExtensionSettingCommandResultForSkipNext { + internal static string LoginUserInfoEmptyToast { get { - return ResourceManager.GetString("ExtensionSettingCommandResultForSkipNext", resourceCulture); + return ResourceManager.GetString("LoginUserInfoEmptyToast", resourceCulture); } } /// - /// Looks up a localized string similar to Skip Previous Command. + /// Looks up a localized string similar to Add To Queue Command. /// - internal static string ExtensionSettingCommandResultForSkipPrevious { + internal static string NameAddToQueueCommand { get { - return ResourceManager.GetString("ExtensionSettingCommandResultForSkipPrevious", resourceCulture); + return ResourceManager.GetString("NameAddToQueueCommand", resourceCulture); } } /// - /// Looks up a localized string similar to Toggle Playback Command. + /// Looks up a localized string similar to Login Command. /// - internal static string ExtensionSettingCommandResultForTogglePlayback { + internal static string NameLoginCommand { get { - return ResourceManager.GetString("ExtensionSettingCommandResultForTogglePlayback", resourceCulture); + return ResourceManager.GetString("NameLoginCommand", resourceCulture); } } /// - /// Looks up a localized string similar to Transfer Playback Command. + /// Looks up a localized string similar to Pause Playback Command. /// - internal static string ExtensionSettingCommandResultForTransferPlayback { + internal static string NamePausePlaybackCommand { get { - return ResourceManager.GetString("ExtensionSettingCommandResultForTransferPlayback", resourceCulture); + return ResourceManager.GetString("NamePausePlaybackCommand", resourceCulture); } } /// - /// Looks up a localized string similar to What should the Spotify control do after {0}?. + /// Looks up a localized string similar to Resume Playback Command. /// - internal static string ExtensionSettingCommandResultSettingDesc { + internal static string NameResumePlaybackCommand { get { - return ResourceManager.GetString("ExtensionSettingCommandResultSettingDesc", resourceCulture); + return ResourceManager.GetString("NameResumePlaybackCommand", resourceCulture); } } /// - /// Looks up a localized string similar to After {0}. + /// Looks up a localized string similar to Set Repeat Command. /// - internal static string ExtensionSettingCommandResultSettingLabel { + internal static string NameSetRepeatCommand { get { - return ResourceManager.GetString("ExtensionSettingCommandResultSettingLabel", resourceCulture); + return ResourceManager.GetString("NameSetRepeatCommand", resourceCulture); } } /// - /// Looks up a localized string similar to Filter Wildcard. + /// Looks up a localized string similar to Set Shuffle Command. /// - internal static string ExtensionSettingFilterWildcard { + internal static string NameSetShuffleCommand { get { - return ResourceManager.GetString("ExtensionSettingFilterWildcard", resourceCulture); + return ResourceManager.GetString("NameSetShuffleCommand", resourceCulture); } } /// - /// Looks up a localized string similar to Filter wildcard character used to prepend a filter, e.g. /album or !album. + /// Looks up a localized string similar to Skip Next Command. /// - internal static string ExtensionSettingFilterWildcardDescription { + internal static string NameSkipNextCommand { get { - return ResourceManager.GetString("ExtensionSettingFilterWildcardDescription", resourceCulture); + return ResourceManager.GetString("NameSkipNextCommand", resourceCulture); } } /// - /// Looks up a localized string similar to Search through and control Spotify - ///as {0} ({1}). + /// Looks up a localized string similar to Skip Previous Command. /// - internal static string ExtensionStatusDescription { + internal static string NameSkipPreviousCommand { get { - return ResourceManager.GetString("ExtensionStatusDescription", resourceCulture); + return ResourceManager.GetString("NameSkipPreviousCommand", resourceCulture); } } /// - /// Looks up a localized string similar to 🎧 Logged in as {0} ({1}). + /// Looks up a localized string similar to Toggle Playback Command. /// - internal static string LoginSuccessToast { + internal static string NameTogglePlaybackCommand { get { - return ResourceManager.GetString("LoginSuccessToast", resourceCulture); + return ResourceManager.GetString("NameTogglePlaybackCommand", resourceCulture); } } /// - /// Looks up a localized string similar to ✅ Logged in, but failed to get user info. + /// Looks up a localized string similar to Transfer Playback Command. /// - internal static string LoginUserInfoEmptyToast { + internal static string NameTransferPlaybackCommand { get { - return ResourceManager.GetString("LoginUserInfoEmptyToast", resourceCulture); + return ResourceManager.GetString("NameTransferPlaybackCommand", resourceCulture); } } diff --git a/CmdPal.Ext.Spotify/Properties/Resources.resx b/CmdPal.Ext.Spotify/Properties/Resources.resx index a40a235..56723fd 100644 --- a/CmdPal.Ext.Spotify/Properties/Resources.resx +++ b/CmdPal.Ext.Spotify/Properties/Resources.resx @@ -286,40 +286,43 @@ as {0} ({1}) Go Home - + After {0} - + What should the Spotify control do after {0}? - - Add To Queue Command - - - Login Command + + Refresh Devices - + Pause Playback Command - + Resume Playback Command - + Set Repeat Command - + Set Shuffle Command - + Skip Next Command - + Skip Previous Command - + Toggle Playback Command - + Transfer Playback Command + + Add To Queue Command + + + Login Command + \ No newline at end of file diff --git a/CmdPal.Ext.Spotify/Properties/Resources.zh-CN.resx b/CmdPal.Ext.Spotify/Properties/Resources.zh-CN.resx index cbf0bf2..fef54d5 100644 --- a/CmdPal.Ext.Spotify/Properties/Resources.zh-CN.resx +++ b/CmdPal.Ext.Spotify/Properties/Resources.zh-CN.resx @@ -286,40 +286,43 @@ 回到首页 - + {0} 之后 - + {0} 之后 Spotify 控件应执行什么操作? - - 添加到队列命令 - - - 登录命令 + + 刷新设备 - + 暂停播放命令 - + 恢复播放命令 - + 设置重复命令 - + 设置随机播放命令 - + 跳过下一首命令 - + 跳过上一首命令 - + 切换播放状态命令 - + 转移播放命令 + + 添加到队列命令 + + + 登录命令 + \ No newline at end of file