diff --git a/.editorconfig b/src/.editorconfig similarity index 94% rename from .editorconfig rename to src/.editorconfig index 3de7e81..8ec3771 100644 --- a/.editorconfig +++ b/src/.editorconfig @@ -207,21 +207,36 @@ dotnet_diagnostic.CA1308.severity = none dotnet_diagnostic.CA1716.severity = none dotnet_diagnostic.CA1720.severity = none dotnet_diagnostic.CA2101.severity = none +dotnet_diagnostic.CA2234.severity = none +dotnet_diagnostic.CA5350.severity = none dotnet_diagnostic.CA9998.severity = none dotnet_diagnostic.CS1591.severity = none dotnet_diagnostic.IDE0002.severity = none +dotnet_diagnostic.IDE0021.severity = warning +dotnet_diagnostic.IDE0022.severity = warning dotnet_diagnostic.IDE0058.severity = none dotnet_diagnostic.IDE0059.severity = none +dotnet_diagnostic.SA1108.severity = none +dotnet_diagnostic.SA1117.severity = none dotnet_diagnostic.SA1121.severity = none +dotnet_diagnostic.SA1122.severity = none dotnet_diagnostic.SA1131.severity = none dotnet_diagnostic.SA1201.severity = none dotnet_diagnostic.SA1202.severity = none +dotnet_diagnostic.SA1203.severity = none +dotnet_diagnostic.SA1204.severity = none +dotnet_diagnostic.SA1214.severity = none dotnet_diagnostic.SA1309.severity = none dotnet_diagnostic.SA1512.severity = none +dotnet_diagnostic.SA1513.severity = none dotnet_diagnostic.SA1600.severity = none dotnet_diagnostic.SA1601.severity = none +dotnet_diagnostic.SA1602.severity = none dotnet_diagnostic.SA1629.severity = none dotnet_diagnostic.SA1633.severity = none + +dotnet_diagnostic.SX1309.severity = warning +dotnet_diagnostic.SX1309S.severity = warning diff --git a/src/SpotifyPremiumPluginWin.sln b/src/SpotifyPremiumPlugin.sln similarity index 86% rename from src/SpotifyPremiumPluginWin.sln rename to src/SpotifyPremiumPlugin.sln index 364dea6..499b9e4 100644 --- a/src/SpotifyPremiumPluginWin.sln +++ b/src/SpotifyPremiumPlugin.sln @@ -2,7 +2,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.30128.74 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpotifyPremiumPlugin", "win\SpotifyPremiumPlugin.csproj", "{D7B32254-4248-4A43-B127-706810EFCAFA}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpotifyPlugin", "SpotifyPremiumPlugin\SpotifyPlugin.csproj", "{D7B32254-4248-4A43-B127-706810EFCAFA}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/src/Win/Adjustments/Playback/PlayAndNavigateAdjustment.cs b/src/SpotifyPremiumPlugin/Adjustments/Playback/PlayAndNavigateAdjustment.cs similarity index 62% rename from src/Win/Adjustments/Playback/PlayAndNavigateAdjustment.cs rename to src/SpotifyPremiumPlugin/Adjustments/Playback/PlayAndNavigateAdjustment.cs index cea2c85..7173560 100644 --- a/src/Win/Adjustments/Playback/PlayAndNavigateAdjustment.cs +++ b/src/SpotifyPremiumPlugin/Adjustments/Playback/PlayAndNavigateAdjustment.cs @@ -3,7 +3,6 @@ namespace Loupedeck.SpotifyPremiumPlugin { using System; - using SpotifyAPI.Web.Models; internal class PlayAndNavigateAdjustment : PluginDynamicAdjustment { @@ -24,11 +23,11 @@ protected override void ApplyAdjustment(String actionParameter, Int32 ticks) { if (ticks > 0) { - this.SpotifyPremiumPlugin.CheckSpotifyResponse(this.SkipPlaybackToNext); + this.SpotifyPremiumPlugin.Wrapper.SkipPlaybackToNext(); } else { - this.SpotifyPremiumPlugin.CheckSpotifyResponse(this.SkipPlaybackToPrevious); + this.SpotifyPremiumPlugin.Wrapper.SkipPlaybackToPrevious(); } } catch (Exception e) @@ -42,7 +41,7 @@ protected override void RunCommand(String actionParameter) { try { - this.SpotifyPremiumPlugin.CheckSpotifyResponse(this.TogglePlayback); + this.SpotifyPremiumPlugin.Wrapper.TogglePlayback(); } catch (Exception e) { @@ -56,17 +55,5 @@ protected override BitmapImage GetCommandImage(String actionParameter, PluginIma var bitmapImage = EmbeddedResources.ReadImage("Loupedeck.SpotifyPremiumPlugin.Icons.Width50.PlayAndNavigateTracks.png"); return bitmapImage; } - - public ErrorResponse SkipPlaybackToNext() => this.SpotifyPremiumPlugin.Api.SkipPlaybackToNext(this.SpotifyPremiumPlugin.CurrentDeviceId); - - public ErrorResponse SkipPlaybackToPrevious() => this.SpotifyPremiumPlugin.Api.SkipPlaybackToPrevious(this.SpotifyPremiumPlugin.CurrentDeviceId); - - public ErrorResponse TogglePlayback() - { - var playback = this.SpotifyPremiumPlugin.Api.GetPlayback(); - return playback.IsPlaying - ? this.SpotifyPremiumPlugin.Api.PausePlayback(this.SpotifyPremiumPlugin.CurrentDeviceId) - : this.SpotifyPremiumPlugin.Api.ResumePlayback(this.SpotifyPremiumPlugin.CurrentDeviceId, String.Empty, null, String.Empty, 0); - } } } diff --git a/src/SpotifyPremiumPlugin/Adjustments/Volume/SpotifyVolumeAdjustment.cs b/src/SpotifyPremiumPlugin/Adjustments/Volume/SpotifyVolumeAdjustment.cs new file mode 100644 index 0000000..fbbf12f --- /dev/null +++ b/src/SpotifyPremiumPlugin/Adjustments/Volume/SpotifyVolumeAdjustment.cs @@ -0,0 +1,28 @@ +// Copyright(c) Loupedeck.All rights reserved. + +namespace Loupedeck.SpotifyPremiumPlugin +{ + using System; + + internal class SpotifyVolumeAdjustment : PluginDynamicAdjustment + { + private SpotifyPremiumPlugin SpotifyPremiumPlugin => this.Plugin as SpotifyPremiumPlugin; + + public SpotifyVolumeAdjustment() + : base("Spotify Volume", "Spotify Volume description", "Spotify Volume", true) + { + } + + protected override void ApplyAdjustment(String actionParameter, Int32 ticks) => this.SpotifyPremiumPlugin.Wrapper.SetVolume(ticks); + + // Overwrite the RunCommand method that is called every time the user presses the encoder to which this command is assigned + protected override void RunCommand(String actionParameter) => this.SpotifyPremiumPlugin.Wrapper.TogglePlayback(); + + protected override BitmapImage GetCommandImage(String actionParameter, PluginImageSize imageSize) + { + // Dial strip 50px + var bitmapImage = EmbeddedResources.ReadImage("Loupedeck.SpotifyPremiumPlugin.Icons.Width50.Volume.png"); + return bitmapImage; + } + } +} diff --git a/src/Win/App.config b/src/SpotifyPremiumPlugin/App.config similarity index 100% rename from src/Win/App.config rename to src/SpotifyPremiumPlugin/App.config diff --git a/src/Win/CommandFolders/DeviceSelectorCommandFolder.cs b/src/SpotifyPremiumPlugin/CommandFolders/DeviceSelectorCommandFolder.cs similarity index 66% rename from src/Win/CommandFolders/DeviceSelectorCommandFolder.cs rename to src/SpotifyPremiumPlugin/CommandFolders/DeviceSelectorCommandFolder.cs index bdd7a50..7ce38f0 100644 --- a/src/Win/CommandFolders/DeviceSelectorCommandFolder.cs +++ b/src/SpotifyPremiumPlugin/CommandFolders/DeviceSelectorCommandFolder.cs @@ -6,7 +6,7 @@ namespace Loupedeck.SpotifyPremiumPlugin.CommandFolders using System.Collections.Generic; using System.Linq; - using SpotifyAPI.Web.Models; + using SpotifyAPI.Web; /// /// Dynamic folder (control center) for Spotify devices. https://developer.loupedeck.com/docs/Actions-taxonomy @@ -32,10 +32,9 @@ public override BitmapImage GetButtonImage(PluginImageSize imageSize) public override IEnumerable GetButtonPressActionNames() { - this._devices = this.SpotifyPremiumPlugin?.Api?.GetDevices()?.Devices; + this._devices = this.SpotifyPremiumPlugin.Wrapper.GetDevices(); if (this._devices != null && this._devices.Any()) { - this._devices.Add(new Device { Id = "activedevice", Name = "Active Device" }); return this._devices.Select(x => this.CreateCommandName(x.Id)); } @@ -54,29 +53,6 @@ public override String GetCommandDisplayName(String commandParameter, PluginImag return deviceDisplayName; } - public override void RunCommand(String commandParameter) - { - try - { - this.SpotifyPremiumPlugin.CheckSpotifyResponse(this.TransferPlayback, commandParameter); - } - catch (Exception e) - { - Tracer.Trace($"Spotify DeviceSelectorCommandFolder action obtain an error: ", e); - } - } - - public ErrorResponse TransferPlayback(String commandParameter) - { - if (commandParameter == "activedevice") - { - commandParameter = String.Empty; - } - - this.SpotifyPremiumPlugin.CurrentDeviceId = commandParameter; - this.SpotifyPremiumPlugin.SaveDeviceToCache(commandParameter); - - return this.SpotifyPremiumPlugin.Api.TransferPlayback(this.SpotifyPremiumPlugin.CurrentDeviceId, true); - } + public override void RunCommand(String commandParameter) => this.SpotifyPremiumPlugin.Wrapper.TransferPlayback(commandParameter); } } \ No newline at end of file diff --git a/src/SpotifyPremiumPlugin/Commands/Playback/ChangeRepeatStateCommand.cs b/src/SpotifyPremiumPlugin/Commands/Playback/ChangeRepeatStateCommand.cs new file mode 100644 index 0000000..31668ac --- /dev/null +++ b/src/SpotifyPremiumPlugin/Commands/Playback/ChangeRepeatStateCommand.cs @@ -0,0 +1,47 @@ +// Copyright(c) Loupedeck.All rights reserved. + +namespace Loupedeck.SpotifyPremiumPlugin +{ + using System; + + using SpotifyAPI.Web; + + internal class ChangeRepeatStateCommand : PluginDynamicCommand + { + private SpotifyPremiumPlugin SpotifyPremiumPlugin => this.Plugin as SpotifyPremiumPlugin; + + public ChangeRepeatStateCommand() + : base("Change Repeat State", "Change Repeat State description", "Playback") + { + } + + protected override void RunCommand(String actionParameter) => this.SpotifyPremiumPlugin.Wrapper.ChangeRepeatState(); + + protected override BitmapImage GetCommandImage(String actionParameter, PluginImageSize imageSize) + { + String icon; + switch (this.SpotifyPremiumPlugin.Wrapper.CachedRepeatState) + { + case PlayerSetRepeatRequest.State.Off: + icon = "Loupedeck.SpotifyPremiumPlugin.Icons.Width80.RepeatOff.png"; + break; + + case PlayerSetRepeatRequest.State.Context: + icon = "Loupedeck.SpotifyPremiumPlugin.Icons.Width80.RepeatList.png"; + break; + + case PlayerSetRepeatRequest.State.Track: + icon = "Loupedeck.SpotifyPremiumPlugin.Icons.Width80.Repeat.png"; + break; + + default: + // Set plugin status and message + icon = "Loupedeck.SpotifyPremiumPlugin.Icons.Width80.RepeatOff.png"; + break; + } + + var bitmapImage = EmbeddedResources.ReadImage(icon); + return bitmapImage; + } + } +} diff --git a/src/Win/Commands/Playback/NextTrackCommand.cs b/src/SpotifyPremiumPlugin/Commands/Playback/NextTrackCommand.cs similarity index 52% rename from src/Win/Commands/Playback/NextTrackCommand.cs rename to src/SpotifyPremiumPlugin/Commands/Playback/NextTrackCommand.cs index 9dd1bbd..669b8b2 100644 --- a/src/Win/Commands/Playback/NextTrackCommand.cs +++ b/src/SpotifyPremiumPlugin/Commands/Playback/NextTrackCommand.cs @@ -3,38 +3,22 @@ namespace Loupedeck.SpotifyPremiumPlugin { using System; - using SpotifyAPI.Web.Models; internal class NextTrackCommand : PluginDynamicCommand { private SpotifyPremiumPlugin SpotifyPremiumPlugin => this.Plugin as SpotifyPremiumPlugin; public NextTrackCommand() - : base( - "Next Track", - "Next Track description", - "Playback") + : base("Next Track", "Next Track description", "Playback") { } - protected override void RunCommand(String actionParameter) - { - try - { - this.SpotifyPremiumPlugin.CheckSpotifyResponse(this.SkipPlaybackToNext); - } - catch (Exception e) - { - Tracer.Trace($"Spotify NextTrackCommand action obtain an error: ", e); - } - } + protected override void RunCommand(String actionParameter) => this.SpotifyPremiumPlugin.Wrapper.SkipPlaybackToNext(); protected override BitmapImage GetCommandImage(String actionParameter, PluginImageSize imageSize) { var bitmapImage = EmbeddedResources.ReadImage("Loupedeck.SpotifyPremiumPlugin.Icons.Width80.NextTrack.png"); return bitmapImage; } - - public ErrorResponse SkipPlaybackToNext() => this.SpotifyPremiumPlugin.Api.SkipPlaybackToNext(this.SpotifyPremiumPlugin.CurrentDeviceId); } } diff --git a/src/Win/Commands/Playback/PreviousTrackCommand.cs b/src/SpotifyPremiumPlugin/Commands/Playback/PreviousTrackCommand.cs similarity index 52% rename from src/Win/Commands/Playback/PreviousTrackCommand.cs rename to src/SpotifyPremiumPlugin/Commands/Playback/PreviousTrackCommand.cs index 370516d..8053640 100644 --- a/src/Win/Commands/Playback/PreviousTrackCommand.cs +++ b/src/SpotifyPremiumPlugin/Commands/Playback/PreviousTrackCommand.cs @@ -3,38 +3,22 @@ namespace Loupedeck.SpotifyPremiumPlugin { using System; - using SpotifyAPI.Web.Models; internal class PreviousTrackCommand : PluginDynamicCommand { private SpotifyPremiumPlugin SpotifyPremiumPlugin => this.Plugin as SpotifyPremiumPlugin; public PreviousTrackCommand() - : base( - "Previous Track", - "Previous Track description", - "Playback") + : base("Previous Track", "Previous Track description", "Playback") { } - protected override void RunCommand(String actionParameter) - { - try - { - this.SpotifyPremiumPlugin.CheckSpotifyResponse(this.SkipPlaybackToPrevious); - } - catch (Exception e) - { - Tracer.Trace($"Spotify PreviousTrackCommand action obtain an error: ", e); - } - } + protected override void RunCommand(String actionParameter) => this.SpotifyPremiumPlugin.Wrapper.SkipPlaybackToPrevious(); protected override BitmapImage GetCommandImage(String actionParameter, PluginImageSize imageSize) { var bitmapImage = EmbeddedResources.ReadImage("Loupedeck.SpotifyPremiumPlugin.Icons.Width80.PreviousTrack.png"); return bitmapImage; } - - public ErrorResponse SkipPlaybackToPrevious() => this.SpotifyPremiumPlugin.Api.SkipPlaybackToPrevious(this.SpotifyPremiumPlugin.CurrentDeviceId); } } diff --git a/src/SpotifyPremiumPlugin/Commands/Playback/ShufflePlayCommand.cs b/src/SpotifyPremiumPlugin/Commands/Playback/ShufflePlayCommand.cs new file mode 100644 index 0000000..99ebdd1 --- /dev/null +++ b/src/SpotifyPremiumPlugin/Commands/Playback/ShufflePlayCommand.cs @@ -0,0 +1,25 @@ +// Copyright(c) Loupedeck.All rights reserved. + +namespace Loupedeck.SpotifyPremiumPlugin +{ + using System; + + internal class ShufflePlayCommand : PluginDynamicCommand + { + private SpotifyPremiumPlugin SpotifyPremiumPlugin => this.Plugin as SpotifyPremiumPlugin; + + public ShufflePlayCommand() + : base("Shuffle Play", "Shuffle Play description", "Playback") + { + } + + protected override void RunCommand(String actionParameter) => this.SpotifyPremiumPlugin.Wrapper.ShufflePlay(); + + protected override BitmapImage GetCommandImage(String actionParameter, PluginImageSize imageSize) + { + return this.SpotifyPremiumPlugin.Wrapper.CachedShuffleState ? + EmbeddedResources.ReadImage("Loupedeck.SpotifyPremiumPlugin.Icons.Width80.Shuffle.png") : + EmbeddedResources.ReadImage("Loupedeck.SpotifyPremiumPlugin.Icons.Width80.ShuffleOff.png"); + } + } +} diff --git a/src/SpotifyPremiumPlugin/Commands/Playback/TogglePlaybackCommand.cs b/src/SpotifyPremiumPlugin/Commands/Playback/TogglePlaybackCommand.cs new file mode 100644 index 0000000..c1b0c5b --- /dev/null +++ b/src/SpotifyPremiumPlugin/Commands/Playback/TogglePlaybackCommand.cs @@ -0,0 +1,25 @@ +// Copyright(c) Loupedeck.All rights reserved. + +namespace Loupedeck.SpotifyPremiumPlugin +{ + using System; + + internal class TogglePlaybackCommand : PluginDynamicCommand + { + private SpotifyPremiumPlugin SpotifyPremiumPlugin => this.Plugin as SpotifyPremiumPlugin; + + public TogglePlaybackCommand() + : base("Toggle Playback", "Toggles audio playback", "Playback") + { + } + + protected override void RunCommand(String actionParameter) => this.SpotifyPremiumPlugin.Wrapper.TogglePlayback(); + + protected override BitmapImage GetCommandImage(String actionParameter, PluginImageSize imageSize) + { + return this.SpotifyPremiumPlugin.Wrapper.CachedPlayingState ? + EmbeddedResources.ReadImage("Loupedeck.SpotifyPremiumPlugin.Icons.Width80.Play.png") : + EmbeddedResources.ReadImage("Loupedeck.SpotifyPremiumPlugin.Icons.Width80.Pause.png"); + } + } +} diff --git a/src/SpotifyPremiumPlugin/Commands/ToggleLikeCommand.cs b/src/SpotifyPremiumPlugin/Commands/ToggleLikeCommand.cs new file mode 100644 index 0000000..893761c --- /dev/null +++ b/src/SpotifyPremiumPlugin/Commands/ToggleLikeCommand.cs @@ -0,0 +1,25 @@ +// Copyright(c) Loupedeck.All rights reserved. + +namespace Loupedeck.SpotifyPremiumPlugin +{ + using System; + + internal class ToggleLikeCommand : PluginDynamicCommand + { + private SpotifyPremiumPlugin SpotifyPremiumPlugin => this.Plugin as SpotifyPremiumPlugin; + + public ToggleLikeCommand() + : base("Toggle Like", "Toggle Like", "Others") + { + } + + protected override void RunCommand(String actionParameter) => this.SpotifyPremiumPlugin.Wrapper.ToggleLike(); + + protected override BitmapImage GetCommandImage(String actionParameter, PluginImageSize imageSize) + { + return this.SpotifyPremiumPlugin.Wrapper.CachedLikeState ? + EmbeddedResources.ReadImage("Loupedeck.SpotifyPremiumPlugin.Icons.Width80.SongLike.png") : + EmbeddedResources.ReadImage("Loupedeck.SpotifyPremiumPlugin.Icons.Width80.SongDislike.png"); + } + } +} diff --git a/src/SpotifyPremiumPlugin/Commands/Volume/MuteCommand.cs b/src/SpotifyPremiumPlugin/Commands/Volume/MuteCommand.cs new file mode 100644 index 0000000..75ade7b --- /dev/null +++ b/src/SpotifyPremiumPlugin/Commands/Volume/MuteCommand.cs @@ -0,0 +1,24 @@ +// Copyright(c) Loupedeck.All rights reserved. + +namespace Loupedeck.SpotifyPremiumPlugin +{ + using System; + + internal class MuteCommand : PluginDynamicCommand + { + private SpotifyPremiumPlugin SpotifyPremiumPlugin => this.Plugin as SpotifyPremiumPlugin; + + public MuteCommand() + : base("Mute", "Mute description", "Spotify Volume") + { + } + + protected override void RunCommand(String actionParameter) => this.SpotifyPremiumPlugin.Wrapper.Mute(); + + protected override BitmapImage GetCommandImage(String actionParameter, PluginImageSize imageSize) + { + var bitmapImage = EmbeddedResources.ReadImage("Loupedeck.SpotifyPremiumPlugin.Icons.Width80.MuteVolume.png"); + return bitmapImage; + } + } +} diff --git a/src/SpotifyPremiumPlugin/Commands/Volume/ToggleMuteCommand.cs b/src/SpotifyPremiumPlugin/Commands/Volume/ToggleMuteCommand.cs new file mode 100644 index 0000000..0118ead --- /dev/null +++ b/src/SpotifyPremiumPlugin/Commands/Volume/ToggleMuteCommand.cs @@ -0,0 +1,29 @@ +// Copyright(c) Loupedeck.All rights reserved. + +namespace Loupedeck.SpotifyPremiumPlugin +{ + using System; + + internal class ToggleMuteCommand : PluginDynamicCommand + { + private SpotifyPremiumPlugin SpotifyPremiumPlugin => this.Plugin as SpotifyPremiumPlugin; + + public ToggleMuteCommand() + : base("Toggle Mute", "Toggles audio mute state", "Spotify Volume") + { + } + + protected override void RunCommand(String actionParameter) + { + this.SpotifyPremiumPlugin.Wrapper.ToggleMute(); + this.ActionImageChanged(); + } + + protected override BitmapImage GetCommandImage(String actionParameter, PluginImageSize imageSize) + { + return this.SpotifyPremiumPlugin.Wrapper.CachedMuteState ? + EmbeddedResources.ReadImage("Loupedeck.SpotifyPremiumPlugin.Icons.Width80.Volume.png") : + EmbeddedResources.ReadImage("Loupedeck.SpotifyPremiumPlugin.Icons.Width80.MuteVolume.png"); + } + } +} diff --git a/src/SpotifyPremiumPlugin/Commands/Volume/UnmuteCommand.cs b/src/SpotifyPremiumPlugin/Commands/Volume/UnmuteCommand.cs new file mode 100644 index 0000000..8ab92eb --- /dev/null +++ b/src/SpotifyPremiumPlugin/Commands/Volume/UnmuteCommand.cs @@ -0,0 +1,24 @@ +// Copyright(c) Loupedeck.All rights reserved. + +namespace Loupedeck.SpotifyPremiumPlugin +{ + using System; + + internal class UnmuteCommand : PluginDynamicCommand + { + private SpotifyPremiumPlugin SpotifyPremiumPlugin => this.Plugin as SpotifyPremiumPlugin; + + public UnmuteCommand() + : base("Unmute", "Unmute description", "Spotify Volume") + { + } + + protected override void RunCommand(String actionParameter) => this.SpotifyPremiumPlugin.Wrapper.Unmute(); + + protected override BitmapImage GetCommandImage(String actionParameter, PluginImageSize imageSize) + { + var bitmapImage = EmbeddedResources.ReadImage("Loupedeck.SpotifyPremiumPlugin.Icons.Width80.Volume.png"); + return bitmapImage; + } + } +} diff --git a/src/Win/Icons/PluginIcon16x16.png b/src/SpotifyPremiumPlugin/Icons/PluginIcon16x16.png similarity index 100% rename from src/Win/Icons/PluginIcon16x16.png rename to src/SpotifyPremiumPlugin/Icons/PluginIcon16x16.png diff --git a/src/Win/Icons/PluginIcon256x256.png b/src/SpotifyPremiumPlugin/Icons/PluginIcon256x256.png similarity index 100% rename from src/Win/Icons/PluginIcon256x256.png rename to src/SpotifyPremiumPlugin/Icons/PluginIcon256x256.png diff --git a/src/Win/Icons/PluginIcon32x32.png b/src/SpotifyPremiumPlugin/Icons/PluginIcon32x32.png similarity index 100% rename from src/Win/Icons/PluginIcon32x32.png rename to src/SpotifyPremiumPlugin/Icons/PluginIcon32x32.png diff --git a/src/Win/Icons/PluginIcon48x48.png b/src/SpotifyPremiumPlugin/Icons/PluginIcon48x48.png similarity index 100% rename from src/Win/Icons/PluginIcon48x48.png rename to src/SpotifyPremiumPlugin/Icons/PluginIcon48x48.png diff --git a/src/Win/Icons/Width50/Browse.png b/src/SpotifyPremiumPlugin/Icons/Width50/Browse.png similarity index 100% rename from src/Win/Icons/Width50/Browse.png rename to src/SpotifyPremiumPlugin/Icons/Width50/Browse.png diff --git a/src/Win/Icons/Width50/Devices.png b/src/SpotifyPremiumPlugin/Icons/Width50/Devices.png similarity index 100% rename from src/Win/Icons/Width50/Devices.png rename to src/SpotifyPremiumPlugin/Icons/Width50/Devices.png diff --git a/src/Win/Icons/Width50/Home2.png b/src/SpotifyPremiumPlugin/Icons/Width50/Home2.png similarity index 100% rename from src/Win/Icons/Width50/Home2.png rename to src/SpotifyPremiumPlugin/Icons/Width50/Home2.png diff --git a/src/Win/Icons/Width50/MuteVolume.png b/src/SpotifyPremiumPlugin/Icons/Width50/MuteVolume.png similarity index 100% rename from src/Win/Icons/Width50/MuteVolume.png rename to src/SpotifyPremiumPlugin/Icons/Width50/MuteVolume.png diff --git a/src/Win/Icons/Width50/NextTrack.png b/src/SpotifyPremiumPlugin/Icons/Width50/NextTrack.png similarity index 100% rename from src/Win/Icons/Width50/NextTrack.png rename to src/SpotifyPremiumPlugin/Icons/Width50/NextTrack.png diff --git a/src/Win/Icons/Width50/Pause.png b/src/SpotifyPremiumPlugin/Icons/Width50/Pause.png similarity index 100% rename from src/Win/Icons/Width50/Pause.png rename to src/SpotifyPremiumPlugin/Icons/Width50/Pause.png diff --git a/src/Win/Icons/Width50/Play.png b/src/SpotifyPremiumPlugin/Icons/Width50/Play.png similarity index 100% rename from src/Win/Icons/Width50/Play.png rename to src/SpotifyPremiumPlugin/Icons/Width50/Play.png diff --git a/src/Win/Icons/Width50/PlayAndNavigateTracks.png b/src/SpotifyPremiumPlugin/Icons/Width50/PlayAndNavigateTracks.png similarity index 100% rename from src/Win/Icons/Width50/PlayAndNavigateTracks.png rename to src/SpotifyPremiumPlugin/Icons/Width50/PlayAndNavigateTracks.png diff --git a/src/Win/Icons/Width50/PlayPause.png b/src/SpotifyPremiumPlugin/Icons/Width50/PlayPause.png similarity index 100% rename from src/Win/Icons/Width50/PlayPause.png rename to src/SpotifyPremiumPlugin/Icons/Width50/PlayPause.png diff --git a/src/Win/Icons/Width50/PlayStop.png b/src/SpotifyPremiumPlugin/Icons/Width50/PlayStop.png similarity index 100% rename from src/Win/Icons/Width50/PlayStop.png rename to src/SpotifyPremiumPlugin/Icons/Width50/PlayStop.png diff --git a/src/Win/Icons/Width50/Playlist.png b/src/SpotifyPremiumPlugin/Icons/Width50/Playlist.png similarity index 100% rename from src/Win/Icons/Width50/Playlist.png rename to src/SpotifyPremiumPlugin/Icons/Width50/Playlist.png diff --git a/src/Win/Icons/Width50/PreviousTrack.png b/src/SpotifyPremiumPlugin/Icons/Width50/PreviousTrack.png similarity index 100% rename from src/Win/Icons/Width50/PreviousTrack.png rename to src/SpotifyPremiumPlugin/Icons/Width50/PreviousTrack.png diff --git a/src/Win/Icons/Width50/Radio.png b/src/SpotifyPremiumPlugin/Icons/Width50/Radio.png similarity index 100% rename from src/Win/Icons/Width50/Radio.png rename to src/SpotifyPremiumPlugin/Icons/Width50/Radio.png diff --git a/src/Win/Icons/Width50/Repeat.png b/src/SpotifyPremiumPlugin/Icons/Width50/Repeat.png similarity index 100% rename from src/Win/Icons/Width50/Repeat.png rename to src/SpotifyPremiumPlugin/Icons/Width50/Repeat.png diff --git a/src/Win/Icons/Width50/RepeatList.png b/src/SpotifyPremiumPlugin/Icons/Width50/RepeatList.png similarity index 100% rename from src/Win/Icons/Width50/RepeatList.png rename to src/SpotifyPremiumPlugin/Icons/Width50/RepeatList.png diff --git a/src/Win/Icons/Width50/RepeatOff.png b/src/SpotifyPremiumPlugin/Icons/Width50/RepeatOff.png similarity index 100% rename from src/Win/Icons/Width50/RepeatOff.png rename to src/SpotifyPremiumPlugin/Icons/Width50/RepeatOff.png diff --git a/src/Win/Icons/Width50/Share.png b/src/SpotifyPremiumPlugin/Icons/Width50/Share.png similarity index 100% rename from src/Win/Icons/Width50/Share.png rename to src/SpotifyPremiumPlugin/Icons/Width50/Share.png diff --git a/src/Win/Icons/Width50/Shuffle.png b/src/SpotifyPremiumPlugin/Icons/Width50/Shuffle.png similarity index 100% rename from src/Win/Icons/Width50/Shuffle.png rename to src/SpotifyPremiumPlugin/Icons/Width50/Shuffle.png diff --git a/src/Win/Icons/Width50/ShuffleList.png b/src/SpotifyPremiumPlugin/Icons/Width50/ShuffleList.png similarity index 100% rename from src/Win/Icons/Width50/ShuffleList.png rename to src/SpotifyPremiumPlugin/Icons/Width50/ShuffleList.png diff --git a/src/Win/Icons/Width50/ShuffleOff.png b/src/SpotifyPremiumPlugin/Icons/Width50/ShuffleOff.png similarity index 100% rename from src/Win/Icons/Width50/ShuffleOff.png rename to src/SpotifyPremiumPlugin/Icons/Width50/ShuffleOff.png diff --git a/src/Win/Icons/Width50/SongDislike.png b/src/SpotifyPremiumPlugin/Icons/Width50/SongDislike.png similarity index 100% rename from src/Win/Icons/Width50/SongDislike.png rename to src/SpotifyPremiumPlugin/Icons/Width50/SongDislike.png diff --git a/src/Win/Icons/Width50/SongLike.png b/src/SpotifyPremiumPlugin/Icons/Width50/SongLike.png similarity index 100% rename from src/Win/Icons/Width50/SongLike.png rename to src/SpotifyPremiumPlugin/Icons/Width50/SongLike.png diff --git a/src/Win/Icons/Width50/Volume.png b/src/SpotifyPremiumPlugin/Icons/Width50/Volume.png similarity index 100% rename from src/Win/Icons/Width50/Volume.png rename to src/SpotifyPremiumPlugin/Icons/Width50/Volume.png diff --git a/src/Win/Icons/Width80/Browse.png b/src/SpotifyPremiumPlugin/Icons/Width80/Browse.png similarity index 100% rename from src/Win/Icons/Width80/Browse.png rename to src/SpotifyPremiumPlugin/Icons/Width80/Browse.png diff --git a/src/Win/Icons/Width80/Devices.png b/src/SpotifyPremiumPlugin/Icons/Width80/Devices.png similarity index 100% rename from src/Win/Icons/Width80/Devices.png rename to src/SpotifyPremiumPlugin/Icons/Width80/Devices.png diff --git a/src/Win/Icons/Width80/Home2.png b/src/SpotifyPremiumPlugin/Icons/Width80/Home2.png similarity index 100% rename from src/Win/Icons/Width80/Home2.png rename to src/SpotifyPremiumPlugin/Icons/Width80/Home2.png diff --git a/src/Win/Icons/Width80/MuteVolume.png b/src/SpotifyPremiumPlugin/Icons/Width80/MuteVolume.png similarity index 100% rename from src/Win/Icons/Width80/MuteVolume.png rename to src/SpotifyPremiumPlugin/Icons/Width80/MuteVolume.png diff --git a/src/Win/Icons/Width80/NextTrack.png b/src/SpotifyPremiumPlugin/Icons/Width80/NextTrack.png similarity index 100% rename from src/Win/Icons/Width80/NextTrack.png rename to src/SpotifyPremiumPlugin/Icons/Width80/NextTrack.png diff --git a/src/Win/Icons/Width80/Pause.png b/src/SpotifyPremiumPlugin/Icons/Width80/Pause.png similarity index 100% rename from src/Win/Icons/Width80/Pause.png rename to src/SpotifyPremiumPlugin/Icons/Width80/Pause.png diff --git a/src/Win/Icons/Width80/Play.png b/src/SpotifyPremiumPlugin/Icons/Width80/Play.png similarity index 100% rename from src/Win/Icons/Width80/Play.png rename to src/SpotifyPremiumPlugin/Icons/Width80/Play.png diff --git a/src/Win/Icons/Width80/PlayAndNavigateTracks.png b/src/SpotifyPremiumPlugin/Icons/Width80/PlayAndNavigateTracks.png similarity index 100% rename from src/Win/Icons/Width80/PlayAndNavigateTracks.png rename to src/SpotifyPremiumPlugin/Icons/Width80/PlayAndNavigateTracks.png diff --git a/src/Win/Icons/Width80/PlayPause.png b/src/SpotifyPremiumPlugin/Icons/Width80/PlayPause.png similarity index 100% rename from src/Win/Icons/Width80/PlayPause.png rename to src/SpotifyPremiumPlugin/Icons/Width80/PlayPause.png diff --git a/src/Win/Icons/Width80/PlayStop.png b/src/SpotifyPremiumPlugin/Icons/Width80/PlayStop.png similarity index 100% rename from src/Win/Icons/Width80/PlayStop.png rename to src/SpotifyPremiumPlugin/Icons/Width80/PlayStop.png diff --git a/src/Win/Icons/Width80/Playlist.png b/src/SpotifyPremiumPlugin/Icons/Width80/Playlist.png similarity index 100% rename from src/Win/Icons/Width80/Playlist.png rename to src/SpotifyPremiumPlugin/Icons/Width80/Playlist.png diff --git a/src/Win/Icons/Width80/PreviousTrack.png b/src/SpotifyPremiumPlugin/Icons/Width80/PreviousTrack.png similarity index 100% rename from src/Win/Icons/Width80/PreviousTrack.png rename to src/SpotifyPremiumPlugin/Icons/Width80/PreviousTrack.png diff --git a/src/Win/Icons/Width80/Radio.png b/src/SpotifyPremiumPlugin/Icons/Width80/Radio.png similarity index 100% rename from src/Win/Icons/Width80/Radio.png rename to src/SpotifyPremiumPlugin/Icons/Width80/Radio.png diff --git a/src/Win/Icons/Width80/Repeat.png b/src/SpotifyPremiumPlugin/Icons/Width80/Repeat.png similarity index 100% rename from src/Win/Icons/Width80/Repeat.png rename to src/SpotifyPremiumPlugin/Icons/Width80/Repeat.png diff --git a/src/Win/Icons/Width80/RepeatList.png b/src/SpotifyPremiumPlugin/Icons/Width80/RepeatList.png similarity index 100% rename from src/Win/Icons/Width80/RepeatList.png rename to src/SpotifyPremiumPlugin/Icons/Width80/RepeatList.png diff --git a/src/Win/Icons/Width80/RepeatOff.png b/src/SpotifyPremiumPlugin/Icons/Width80/RepeatOff.png similarity index 100% rename from src/Win/Icons/Width80/RepeatOff.png rename to src/SpotifyPremiumPlugin/Icons/Width80/RepeatOff.png diff --git a/src/Win/Icons/Width80/Share.png b/src/SpotifyPremiumPlugin/Icons/Width80/Share.png similarity index 100% rename from src/Win/Icons/Width80/Share.png rename to src/SpotifyPremiumPlugin/Icons/Width80/Share.png diff --git a/src/Win/Icons/Width80/Shuffle.png b/src/SpotifyPremiumPlugin/Icons/Width80/Shuffle.png similarity index 100% rename from src/Win/Icons/Width80/Shuffle.png rename to src/SpotifyPremiumPlugin/Icons/Width80/Shuffle.png diff --git a/src/Win/Icons/Width80/ShuffleList.png b/src/SpotifyPremiumPlugin/Icons/Width80/ShuffleList.png similarity index 100% rename from src/Win/Icons/Width80/ShuffleList.png rename to src/SpotifyPremiumPlugin/Icons/Width80/ShuffleList.png diff --git a/src/Win/Icons/Width80/ShuffleOff.png b/src/SpotifyPremiumPlugin/Icons/Width80/ShuffleOff.png similarity index 100% rename from src/Win/Icons/Width80/ShuffleOff.png rename to src/SpotifyPremiumPlugin/Icons/Width80/ShuffleOff.png diff --git a/src/Win/Icons/Width80/SongDislike.png b/src/SpotifyPremiumPlugin/Icons/Width80/SongDislike.png similarity index 100% rename from src/Win/Icons/Width80/SongDislike.png rename to src/SpotifyPremiumPlugin/Icons/Width80/SongDislike.png diff --git a/src/Win/Icons/Width80/SongLike.png b/src/SpotifyPremiumPlugin/Icons/Width80/SongLike.png similarity index 100% rename from src/Win/Icons/Width80/SongLike.png rename to src/SpotifyPremiumPlugin/Icons/Width80/SongLike.png diff --git a/src/Win/Icons/Width80/Volume.png b/src/SpotifyPremiumPlugin/Icons/Width80/Volume.png similarity index 100% rename from src/Win/Icons/Width80/Volume.png rename to src/SpotifyPremiumPlugin/Icons/Width80/Volume.png diff --git a/src/SpotifyPremiumPlugin/ParameterizedCommands/DirectVolumeCommand.cs b/src/SpotifyPremiumPlugin/ParameterizedCommands/DirectVolumeCommand.cs new file mode 100644 index 0000000..6551faf --- /dev/null +++ b/src/SpotifyPremiumPlugin/ParameterizedCommands/DirectVolumeCommand.cs @@ -0,0 +1,23 @@ +// Copyright(c) Loupedeck.All rights reserved. + +namespace Loupedeck.SpotifyPremiumPlugin.ParameterizedCommands +{ + using System; + + internal class DirectVolumeCommand : PluginDynamicCommand + { + private SpotifyPremiumPlugin SpotifyPremiumPlugin => this.Plugin as SpotifyPremiumPlugin; + + public DirectVolumeCommand() + : base() + { + // Profile actions do not belong to a group in the current UI, they are on the top level + this.DisplayName = "Direct Volume"; // so this will be shown as "group name" for parameterized commands + this.GroupName = "Not used"; + + this.MakeProfileAction("text;Enter volume level to set 0-100:"); + } + + protected override void RunCommand(String actionParameter) => this.SpotifyPremiumPlugin.Wrapper.SetVolume(actionParameter); + } +} diff --git a/src/Win/ParameterizedCommands/SaveToPlaylistCommand.cs b/src/SpotifyPremiumPlugin/ParameterizedCommands/SaveToPlaylistCommand.cs similarity index 52% rename from src/Win/ParameterizedCommands/SaveToPlaylistCommand.cs rename to src/SpotifyPremiumPlugin/ParameterizedCommands/SaveToPlaylistCommand.cs index 3198823..f49224b 100644 --- a/src/Win/ParameterizedCommands/SaveToPlaylistCommand.cs +++ b/src/SpotifyPremiumPlugin/ParameterizedCommands/SaveToPlaylistCommand.cs @@ -4,7 +4,6 @@ namespace Loupedeck.SpotifyPremiumPlugin.ParameterizedCommands { using System; using System.Linq; - using SpotifyAPI.Web.Models; internal class SaveToPlaylistCommand : PluginDynamicCommand { @@ -20,35 +19,12 @@ public SaveToPlaylistCommand() this.MakeProfileAction("list;Select playlist to save:"); } - protected override void RunCommand(String actionParameter) - { - try - { - this.SpotifyPremiumPlugin.CheckSpotifyResponse(this.SaveToPlaylist, actionParameter); - } - catch (Exception e) - { - Tracer.Trace($"Spotify SaveToPlaylistCommand action obtain an error: ", e); - } - } - - public ErrorResponse SaveToPlaylist(String playlistId) - { - var idWithUri = true; - if (idWithUri) - { - playlistId = playlistId.Replace("spotify:playlist:", String.Empty); - } - - var playback = this.SpotifyPremiumPlugin.Api.GetPlayback(); - var currentTrackUri = playback.Item.Uri; - return this.SpotifyPremiumPlugin.Api.AddPlaylistTrack(playlistId, currentTrackUri); - } + protected override void RunCommand(String actionParameter) => this.SpotifyPremiumPlugin.Wrapper.SaveToPlaylist(actionParameter); protected override PluginActionParameter[] GetParameters() { - var playlists = this.SpotifyPremiumPlugin.GetAllPlaylists(); - return playlists?.Items + var playlists = this.SpotifyPremiumPlugin.Wrapper.GetAllPlaylists(); + return playlists? .Select(x => new PluginActionParameter(x.Uri, x.Name, String.Empty)) .ToArray(); } diff --git a/src/Win/ParameterizedCommands/StartPlaylistCommand.cs b/src/SpotifyPremiumPlugin/ParameterizedCommands/StartPlaylistCommand.cs similarity index 59% rename from src/Win/ParameterizedCommands/StartPlaylistCommand.cs rename to src/SpotifyPremiumPlugin/ParameterizedCommands/StartPlaylistCommand.cs index 88e6598..bcf2595 100644 --- a/src/Win/ParameterizedCommands/StartPlaylistCommand.cs +++ b/src/SpotifyPremiumPlugin/ParameterizedCommands/StartPlaylistCommand.cs @@ -4,7 +4,6 @@ namespace Loupedeck.SpotifyPremiumPlugin.ParameterizedCommands { using System; using System.Linq; - using SpotifyAPI.Web.Models; internal class StartPlaylistCommand : PluginDynamicCommand { @@ -20,27 +19,12 @@ public StartPlaylistCommand() this.MakeProfileAction("list;Select playlist to play:"); } - protected override void RunCommand(String actionParameter) - { - try - { - this.SpotifyPremiumPlugin.CheckSpotifyResponse(this.StartPlaylist, actionParameter); - } - catch (Exception e) - { - Tracer.Trace($"Spotify StartPlaylistCommand action obtain an error: ", e); - } - } - - public ErrorResponse StartPlaylist(String contextUri) - { - return this.SpotifyPremiumPlugin.Api.ResumePlayback(this.SpotifyPremiumPlugin.CurrentDeviceId, contextUri, null, String.Empty); - } + protected override void RunCommand(String actionParameter) => this.SpotifyPremiumPlugin.Wrapper.StartPlaylist(actionParameter); protected override PluginActionParameter[] GetParameters() { - var playlists = this.SpotifyPremiumPlugin.GetAllPlaylists(); - return playlists?.Items + var playlists = this.SpotifyPremiumPlugin.Wrapper.GetAllPlaylists(); + return playlists? .Select(x => new PluginActionParameter(x.Uri, x.Name, String.Empty)) .ToArray(); } diff --git a/src/Win/PluginConfiguration.json b/src/SpotifyPremiumPlugin/PluginConfiguration.json similarity index 100% rename from src/Win/PluginConfiguration.json rename to src/SpotifyPremiumPlugin/PluginConfiguration.json diff --git a/src/Win/Properties/AssemblyInfo.cs b/src/SpotifyPremiumPlugin/Properties/AssemblyInfo.cs similarity index 95% rename from src/Win/Properties/AssemblyInfo.cs rename to src/SpotifyPremiumPlugin/Properties/AssemblyInfo.cs index c75ad5b..eb01828 100644 --- a/src/Win/Properties/AssemblyInfo.cs +++ b/src/SpotifyPremiumPlugin/Properties/AssemblyInfo.cs @@ -10,7 +10,7 @@ [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("LoupeDeck Oy")] [assembly: AssemblyProduct("Spotify Premium Plugin for Loupedeck Software")] -[assembly: AssemblyCopyright("Copyright (c) 2021 LoupeDeck Oy. All rights reserved.")] +[assembly: AssemblyCopyright("Copyright (c) 2022 LoupeDeck Oy. All rights reserved.")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/src/Win/Properties/Settings.Designer.cs b/src/SpotifyPremiumPlugin/Properties/Settings.Designer.cs similarity index 100% rename from src/Win/Properties/Settings.Designer.cs rename to src/SpotifyPremiumPlugin/Properties/Settings.Designer.cs diff --git a/src/Win/Properties/Settings.settings b/src/SpotifyPremiumPlugin/Properties/Settings.settings similarity index 100% rename from src/Win/Properties/Settings.settings rename to src/SpotifyPremiumPlugin/Properties/Settings.settings diff --git a/src/Win/SpotifyPremiumPlugin.csproj b/src/SpotifyPremiumPlugin/SpotifyPlugin.csproj similarity index 83% rename from src/Win/SpotifyPremiumPlugin.csproj rename to src/SpotifyPremiumPlugin/SpotifyPlugin.csproj index eaf7ed4..ea82ed3 100644 --- a/src/Win/SpotifyPremiumPlugin.csproj +++ b/src/SpotifyPremiumPlugin/SpotifyPlugin.csproj @@ -11,6 +11,8 @@ SpotifyPremiumPlugin v4.7.2 512 + + @@ -22,7 +24,7 @@ true full false - $(LocalAppData)\Loupedeck\Plugins\ + ..\..\..\..\Users\Loupedeck\AppData\Local\Loupedeck\Plugins\DenysSpotify\ DEBUG;TRACE prompt 4 @@ -35,6 +37,9 @@ 4 + + ..\packages\EmbedIO.3.4.3\lib\netstandard2.0\EmbedIO.dll + ..\packages\Newtonsoft.Json.13.0.1\lib\net45\Newtonsoft.Json.dll @@ -43,16 +48,20 @@ C:\Program Files (x86)\Loupedeck\Loupedeck2\PluginApi.dll - False - ..\..\..\..\..\Program Files (x86)\Loupedeck\Loupedeck2\SpotifyAPI.Web.dll + ..\packages\SpotifyAPI.Web.6.2.2\lib\netstandard2.0\SpotifyAPI.Web.dll - False - ..\..\..\..\..\Program Files (x86)\Loupedeck\Loupedeck2\SpotifyAPI.Web.Auth.dll + ..\packages\SpotifyAPI.Web.Auth.6.2.2\lib\netstandard2.0\SpotifyAPI.Web.Auth.dll + + + ..\packages\Unosquare.Swan.Lite.3.0.0\lib\net461\Swan.Lite.dll + + ..\packages\System.ValueTuple.4.5.0\lib\net47\System.ValueTuple.dll + @@ -75,11 +84,8 @@ - - - + - @@ -90,6 +96,13 @@ + + + + + + + @@ -153,12 +166,11 @@ - + - \ No newline at end of file diff --git a/src/Win/SpotifyPremiumApplication.cs b/src/SpotifyPremiumPlugin/SpotifyPremiumApplication.cs similarity index 100% rename from src/Win/SpotifyPremiumApplication.cs rename to src/SpotifyPremiumPlugin/SpotifyPremiumApplication.cs diff --git a/src/Win/SpotifyPremiumPlugin.Installer.cs b/src/SpotifyPremiumPlugin/SpotifyPremiumPlugin.Installer.cs similarity index 70% rename from src/Win/SpotifyPremiumPlugin.Installer.cs rename to src/SpotifyPremiumPlugin/SpotifyPremiumPlugin.Installer.cs index d4db094..e14d85d 100644 --- a/src/Win/SpotifyPremiumPlugin.Installer.cs +++ b/src/SpotifyPremiumPlugin/SpotifyPremiumPlugin.Installer.cs @@ -3,11 +3,10 @@ namespace Loupedeck.SpotifyPremiumPlugin { using System; + using System.IO; public partial class SpotifyPremiumPlugin : Plugin { - public String ClientConfigurationFilePath => System.IO.Path.Combine(this.GetPluginDataDirectory(), "spotify-client.txt"); - public override Boolean Install() { // Here we ensure the plugin data directory is there. @@ -20,12 +19,11 @@ public override Boolean Install() } // Now we put a template configuration file from resources - var filePath = System.IO.Path.Combine(pluginDataDirectory, this.ClientConfigurationFilePath); - - using (var streamWriter = new System.IO.StreamWriter(filePath)) + var filePath = SpotifyWrapper.GetClientConfigurationFilePath(pluginDataDirectory); + using (var streamWriter = new StreamWriter(filePath)) { // Write data - this.Assembly.ExtractFile("spotify-client-template.txt", this.ClientConfigurationFilePath); + this.Assembly.ExtractFile("spotify-client-template.txt", filePath); } return true; diff --git a/src/SpotifyPremiumPlugin/SpotifyPremiumPlugin.Statuses.cs b/src/SpotifyPremiumPlugin/SpotifyPremiumPlugin.Statuses.cs new file mode 100644 index 0000000..03e9efd --- /dev/null +++ b/src/SpotifyPremiumPlugin/SpotifyPremiumPlugin.Statuses.cs @@ -0,0 +1,37 @@ +// Copyright (c) Loupedeck. All rights reserved. + +namespace Loupedeck.SpotifyPremiumPlugin +{ + using System; + using Loupedeck; + + /// + /// Plugin: handle wrapper status and switch plugin status + /// + public partial class SpotifyPremiumPlugin : Plugin + { + internal void WrapperStatusParser(Object o, WrapperChangedEventArgs e) => + this.OnPluginStatusChanged(this.GetPluginStatus(e.WrapperStatus), e.Message, e.SupportUrl); + + private PluginStatus GetPluginStatus(WrapperStatus wrapperStatus) + { + switch (wrapperStatus) + { + case WrapperStatus.Unknown: + return Loupedeck.PluginStatus.Unknown; + + case WrapperStatus.Normal: + return Loupedeck.PluginStatus.Normal; + + case WrapperStatus.Warning: + return Loupedeck.PluginStatus.Warning; + + case WrapperStatus.Error: + return Loupedeck.PluginStatus.Error; + + default: + return Loupedeck.PluginStatus.Unknown; + } + } + } +} diff --git a/src/Win/SpotifyPremiumPlugin.cs b/src/SpotifyPremiumPlugin/SpotifyPremiumPlugin.cs similarity index 69% rename from src/Win/SpotifyPremiumPlugin.cs rename to src/SpotifyPremiumPlugin/SpotifyPremiumPlugin.cs index 4de6905..d050ae9 100644 --- a/src/Win/SpotifyPremiumPlugin.cs +++ b/src/SpotifyPremiumPlugin/SpotifyPremiumPlugin.cs @@ -16,20 +16,20 @@ public partial class SpotifyPremiumPlugin : Plugin // This plugin does not require an application (i.e. Spotify application installed on pc). public override Boolean HasNoApplication => true; + internal SpotifyWrapper Wrapper { get; private set; } + public override void Load() { this.LoadPluginIcons(); - // Set everything ready and connect to Spotify API - this.SpotifyConfiguration(); + this.Wrapper = new SpotifyWrapper(this.GetPluginDataDirectory()); + this.Wrapper.WrapperStatusChanged += this.WrapperStatusParser; + this.Wrapper.Init(); - // Get current (active) device id from internal cache - this.CurrentDeviceId = this.GetCachedDeviceID(); + this.ServiceEvents.UrlCallbackReceived += this.OnUrlCallbackReceived; } - public override void Unload() - { - } + public override void Unload() => this.Wrapper.WrapperStatusChanged -= this.WrapperStatusParser; public override void RunCommand(String commandName, String parameter) { @@ -47,5 +47,13 @@ private void LoadPluginIcons() this.Info.Icon48x48 = EmbeddedResources.ReadImage("Loupedeck.SpotifyPremiumPlugin.Icons.PluginIcon48x48.png"); this.Info.Icon256x256 = EmbeddedResources.ReadImage("Loupedeck.SpotifyPremiumPlugin.Icons.PluginIcon256x256.png"); } + + private void OnUrlCallbackReceived(Object sender, UrlCallbackReceivedEventArgs e) + { + if ((e.Uri != null) && e.Uri.LocalPath.Equals("login")) + { + this.Wrapper.StartLogin(); + } + } } } diff --git a/src/Win/SpotifyPremiumPluginApiCommands.cs b/src/SpotifyPremiumPlugin/SpotifyPremiumPluginApiCommands.cs similarity index 100% rename from src/Win/SpotifyPremiumPluginApiCommands.cs rename to src/SpotifyPremiumPlugin/SpotifyPremiumPluginApiCommands.cs diff --git a/src/SpotifyPremiumPlugin/Wrapper/Events/SpotifyWrapper.EventArgs.cs b/src/SpotifyPremiumPlugin/Wrapper/Events/SpotifyWrapper.EventArgs.cs new file mode 100644 index 0000000..3768fa9 --- /dev/null +++ b/src/SpotifyPremiumPlugin/Wrapper/Events/SpotifyWrapper.EventArgs.cs @@ -0,0 +1,28 @@ +namespace Loupedeck.SpotifyPremiumPlugin +{ + using System; + + public class WrapperChangedEventArgs : EventArgs + { + public WrapperStatus WrapperStatus { get; set; } + + public String Message { get; set; } + + public String SupportUrl { get; set; } + + public WrapperChangedEventArgs(WrapperStatus wrapperStatus, String message, String supportUrl) + { + this.WrapperStatus = wrapperStatus; + this.Message = message; + this.SupportUrl = supportUrl; + } + } + + public enum WrapperStatus + { + Unknown = 0, + Normal = 1, + Warning = 2, + Error = 3, + } +} diff --git a/src/SpotifyPremiumPlugin/Wrapper/Events/SpotifyWrapper.Events.cs b/src/SpotifyPremiumPlugin/Wrapper/Events/SpotifyWrapper.Events.cs new file mode 100644 index 0000000..ed92136 --- /dev/null +++ b/src/SpotifyPremiumPlugin/Wrapper/Events/SpotifyWrapper.Events.cs @@ -0,0 +1,17 @@ +namespace Loupedeck.SpotifyPremiumPlugin +{ + using System; + + public partial class SpotifyWrapper + { + public event EventHandler WrapperStatusChanged; + + public void OnWrapperStatusChanged(WrapperStatus wrapperStatus, String message, String supportUrl) + { + this.Status = wrapperStatus; + + var status = new WrapperChangedEventArgs(wrapperStatus, message, supportUrl); + this.WrapperStatusChanged?.Invoke(this, status); + } + } +} diff --git a/src/SpotifyPremiumPlugin/Wrapper/SpotifyWrapper.Authentification.cs b/src/SpotifyPremiumPlugin/Wrapper/SpotifyWrapper.Authentification.cs new file mode 100644 index 0000000..a929c1b --- /dev/null +++ b/src/SpotifyPremiumPlugin/Wrapper/SpotifyWrapper.Authentification.cs @@ -0,0 +1,192 @@ +namespace Loupedeck.SpotifyPremiumPlugin +{ + using System; + using System.IO; + using System.Security.Cryptography; + using System.Text; + + using Loupedeck.SpotifyPremiumPlugin.Wrapper; + + using Newtonsoft.Json; + + using SpotifyAPI.Web; + using SpotifyAPI.Web.Auth; + + public partial class SpotifyWrapper + { + private String SpotifyTokenFilePath => Path.Combine(this._cacheDirectory, "spotify.dat"); + + public void StartAuth() + { + if (!this.TryReadConfigurationFile(out var configurationModel)) + { + return; + } + + if (!File.Exists(this.SpotifyTokenFilePath)) + { + this.OnWrapperStatusChanged(WrapperStatus.Error, "Please login to Spotify. Click More Details below", "loupedeck:plugin/SpotifyPremium/callback/login"); + return; + } + + var token = this.ReadTokenFromLocalFile(); + if (token != null) + { + // Use the existing token + this.InitSpotifyClient(token, configurationModel); + } + } + + internal void StartLogin() + { + if (this.TryReadConfigurationFile(out var configurationModel)) + { + this.StartLogin(configurationModel); + } + } + + internal void StartLogin(WrapperConfigurationModel configurationModel) + { + if (!NetworkHelpers.TryGetFreeTcpPort(configurationModel.TcpPorts, out var selectedPort)) + { + Tracer.Error("No available ports for Spotify!"); + return; + } + + var server = new EmbedIOAuthServer(new Uri($"http://localhost:{selectedPort}/callback"), selectedPort); + + server.Start(); + + server.AuthorizationCodeReceived += (sender, response) => + { + AuthorizationCodeTokenResponse token = + new OAuthClient() + .RequestToken( + new AuthorizationCodeTokenRequest( + configurationModel.ClientId, + configurationModel.ClientSecret, + response.Code, + server.BaseUri)).Result; + + this.SaveTokenToLocalFile(token); + this.InitSpotifyClient(token, configurationModel); + + server.Stop(); + server.Dispose(); + return null; + }; + + server.ErrorReceived += (sender, error, state) => + { + server.Stop(); + server.Dispose(); + return null; + }; + + var request = new LoginRequest(server.BaseUri, configurationModel.ClientId, LoginRequest.ResponseType.Code) + { + Scope = new[] + { + Scopes.PlaylistReadPrivate, + Scopes.Streaming, + Scopes.UserReadCurrentlyPlaying, + Scopes.UserReadPlaybackState, + Scopes.UserLibraryRead, + Scopes.UserLibraryModify, + Scopes.UserReadPrivate, + Scopes.UserModifyPlaybackState, + Scopes.PlaylistReadCollaborative, + Scopes.PlaylistModifyPublic, + Scopes.PlaylistModifyPrivate, + Scopes.PlaylistReadPrivate, + Scopes.UserReadEmail, + }, + }; + + try + { + BrowserUtil.Open(request.ToUri()); + } + catch (Exception) + { + this.OnWrapperStatusChanged(WrapperStatus.Error, "Login to Spotify failed. Click More Details below", null); + + server.Stop(); + server.Dispose(); + } + } + + internal void InitSpotifyClient(IRefreshableToken refreshableToken, WrapperConfigurationModel configurationModel) + { + var localToken = refreshableToken as AuthorizationCodeTokenResponse; + + // Refreshes token automatically on demand + var authenticator = new AuthorizationCodeAuthenticator(configurationModel.ClientId, configurationModel.ClientSecret, localToken); + authenticator.TokenRefreshed += (sender, token) => this.SaveTokenToLocalFile(token); + + var config = SpotifyClientConfig.CreateDefault() + .WithAuthenticator(authenticator); + + this.Client = new SpotifyClient(config); + + this.OnWrapperStatusChanged(WrapperStatus.Normal, "Connected", null); + } + + private Boolean TryReadConfigurationFile(out WrapperConfigurationModel model) + { + model = null; + + // Get Spotify App configuration from spotify-client.yml file: client id and client secret + // Windows path: %LOCALAPPDATA%/Loupedeck/PluginData/SpotifyPremium/spotify-client.yml + var spotifyClientConfigurationFile = this.ClientConfigurationFilePath; + if (!File.Exists(spotifyClientConfigurationFile)) + { + this.OnWrapperStatusChanged(WrapperStatus.Error, $"Spotify configuration is missing. Click More Details below", null); + return false; + } + + if (!YamlHelpers.TryDeserializeFromFile(spotifyClientConfigurationFile, out var configurationModel)) + { + this.OnWrapperStatusChanged(WrapperStatus.Error, $"Check Spotify API app 'ClientId' / 'ClientSecret' and 'TcpPorts' in configuration file. Click More Details below", null); + return false; + } + + model = configurationModel; + + return true; + } + + private IRefreshableToken ReadTokenFromLocalFile() + { + IRefreshableToken localToken = null; + + try + { + var encryptedBase64Data = File.ReadAllText(this.SpotifyTokenFilePath); + var encryptedData = Convert.FromBase64String(encryptedBase64Data); + var unparsedToken = ProtectedData.Unprotect(encryptedData, null, DataProtectionScope.CurrentUser); + var encoding = new UTF8Encoding(); + + localToken = JsonConvert.DeserializeObject(encoding.GetString(unparsedToken)); + } + catch (Exception ex) + { + Tracer.Warning("Unable to read cached token!", ex); + } + + return localToken; + } + + private void SaveTokenToLocalFile(IRefreshableToken token) + { + var serializedToken = JsonHelpers.SerializeObject(token); + + var encoding = new UTF8Encoding(); + var plain = encoding.GetBytes(serializedToken); + var secret = ProtectedData.Protect(plain, null, DataProtectionScope.CurrentUser); + var encryptedData = Convert.ToBase64String(secret); + + File.WriteAllText(this.SpotifyTokenFilePath, encryptedData); + } + } +} diff --git a/src/Win/SpotifyPremiumPlugin.DevicesSelector.cs b/src/SpotifyPremiumPlugin/Wrapper/SpotifyWrapper.Cache.cs similarity index 71% rename from src/Win/SpotifyPremiumPlugin.DevicesSelector.cs rename to src/SpotifyPremiumPlugin/Wrapper/SpotifyWrapper.Cache.cs index 91f0348..901db51 100644 --- a/src/Win/SpotifyPremiumPlugin.DevicesSelector.cs +++ b/src/SpotifyPremiumPlugin/Wrapper/SpotifyWrapper.Cache.cs @@ -1,25 +1,27 @@ -// Copyright (c) Loupedeck. All rights reserved. - -namespace Loupedeck.SpotifyPremiumPlugin +namespace Loupedeck.SpotifyPremiumPlugin { using System; using System.IO; - /// - /// Plugin: Store Spotify devices locally - /// - public partial class SpotifyPremiumPlugin : Plugin + public partial class SpotifyWrapper { - private readonly String _deviceCacheFileName = "CachedDevice.txt"; + private readonly String _cacheDirectory; + + private readonly String _deviceCacheFileName = "CachedDevice"; private readonly Object _locker = new Object(); - private String GetCacheFilePath(String fileName) => Path.Combine(this.GetPluginDataDirectory(), "Cache", fileName); + public static String GetClientConfigurationFilePath(String cacheDirectory) => Path.Combine(cacheDirectory, ClientConfigurationFileName); + + public static String ClientConfigurationFileName => "spotify-client.yml"; + + public String ClientConfigurationFilePath => Path.Combine(this._cacheDirectory, ClientConfigurationFileName); + + private String GetCacheFilePath(String fileName) => Path.Combine(this._cacheDirectory, fileName); public void SaveDeviceToCache(String deviceId) { - var cacheDirectory = Path.Combine(this.GetPluginDataDirectory(), "Cache"); - + var cacheDirectory = this._cacheDirectory; if (!Directory.Exists(cacheDirectory)) { try diff --git a/src/SpotifyPremiumPlugin/Wrapper/SpotifyWrapper.ConfigurationModel.cs b/src/SpotifyPremiumPlugin/Wrapper/SpotifyWrapper.ConfigurationModel.cs new file mode 100644 index 0000000..59a0d23 --- /dev/null +++ b/src/SpotifyPremiumPlugin/Wrapper/SpotifyWrapper.ConfigurationModel.cs @@ -0,0 +1,14 @@ +namespace Loupedeck.SpotifyPremiumPlugin.Wrapper +{ + using System; + using System.Collections.Generic; + + internal class WrapperConfigurationModel + { + public String ClientId { get; set; } + + public String ClientSecret { get; set; } + + public List TcpPorts { get; set; } + } +} diff --git a/src/SpotifyPremiumPlugin/Wrapper/SpotifyWrapper.Responses.cs b/src/SpotifyPremiumPlugin/Wrapper/SpotifyWrapper.Responses.cs new file mode 100644 index 0000000..9220e82 --- /dev/null +++ b/src/SpotifyPremiumPlugin/Wrapper/SpotifyWrapper.Responses.cs @@ -0,0 +1,97 @@ +namespace Loupedeck.SpotifyPremiumPlugin +{ + using System; + using System.Net; + + using SpotifyAPI.Web; + + public partial class SpotifyWrapper + { + public WrapperStatus Status { get; private set; } + + public TResult CheckSpotifyResponse(Func apiMethod, T parameter) + { + try + { + return apiMethod(parameter); + } + catch (APIUnauthorizedException) + { + this.OnWrapperStatusChanged(WrapperStatus.Error, "Please login to Spotify. Click More Details below", "loupedeck:plugin/SpotifyPremium/callback/login"); + } + catch (APITooManyRequestsException) + { + this.OnWrapperStatusChanged(WrapperStatus.Error, "Too many requests!", null); + } + catch (APIException apiException) + { + this.CheckStatusCode(apiException.Response.StatusCode, apiException.Message); + } + + return default; + } + + public TResult CheckSpotifyResponse(Func apiMethod) + { + try + { + return apiMethod(); + } + catch (APIUnauthorizedException) + { + this.OnWrapperStatusChanged(WrapperStatus.Error, "Please login to Spotify. Click More Details below", "loupedeck:plugin/SpotifyPremium/callback/login"); + } + catch (APITooManyRequestsException) + { + this.OnWrapperStatusChanged(WrapperStatus.Error, "Too many requests!", null); + } + catch (APIException apiException) + { + this.CheckStatusCode(apiException.Response.StatusCode, apiException.Message); + } + + return default; + } + + internal void CheckStatusCode(HttpStatusCode statusCode, String spotifyApiMessage) + { + switch (statusCode) + { + case HttpStatusCode.Continue: + case HttpStatusCode.SwitchingProtocols: + case HttpStatusCode.OK: + case HttpStatusCode.Created: + case HttpStatusCode.Accepted: + case HttpStatusCode.NonAuthoritativeInformation: + case HttpStatusCode.NoContent: + case HttpStatusCode.ResetContent: + case HttpStatusCode.PartialContent: + + if (this.Status != WrapperStatus.Normal) + { + this.OnWrapperStatusChanged(WrapperStatus.Normal, null, null); + } + + break; + + case HttpStatusCode.Unauthorized: + // This should never happen? + this.OnWrapperStatusChanged(WrapperStatus.Error, "Login to Spotify", null); + break; + + case HttpStatusCode.NotFound: + // User doesn't have device set or some other Spotify related thing. User action needed. + this.OnWrapperStatusChanged(WrapperStatus.Warning, $"Spotify message: {spotifyApiMessage}", null); + break; + + default: + if (this.Status != WrapperStatus.Error) + { + this.OnWrapperStatusChanged(WrapperStatus.Error, spotifyApiMessage, null); + } + + break; + } + } + } +} diff --git a/src/SpotifyPremiumPlugin/Wrapper/SpotifyWrapper.cs b/src/SpotifyPremiumPlugin/Wrapper/SpotifyWrapper.cs new file mode 100644 index 0000000..eaf31d4 --- /dev/null +++ b/src/SpotifyPremiumPlugin/Wrapper/SpotifyWrapper.cs @@ -0,0 +1,335 @@ +namespace Loupedeck.SpotifyPremiumPlugin +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Timers; + + using SpotifyAPI.Web; + + public partial class SpotifyWrapper + { + public SpotifyClient Client { get; private set; } + + public PlayerSetRepeatRequest.State CachedRepeatState { get; private set; } + + public Boolean CachedShuffleState { get; private set; } + + public Boolean CachedPlayingState { get; private set; } + + public Boolean CachedMuteState { get; private set; } + + public Boolean CachedLikeState { get; private set; } + + internal String CurrentDeviceId + { + get => this._currentDeviceId; + set + { + this._currentDeviceId = value; + this.SaveDeviceToCache(value); + } + } + + public SpotifyWrapper(String cacheDirectory) => this._cacheDirectory = cacheDirectory; + + public void Init() + { + this.StartAuth(); + this.CurrentDeviceId = this.GetCachedDeviceID(); + } + + /// + /// Gets or sets Volume. Used when muting to remember the previous volume. Used for dials when + /// incrementing rapidly. + /// + internal Int32 PreviousVolume { get; set; } + + public void SetVolume(String volumeString) + { + if (Int32.TryParse(volumeString, out var volume)) + { + this.SetVolume(volume); + } + } + + public void SetVolume(Int32 percents) + { + if (percents > 100) + { + percents = 100; + } + + if (percents < 0) + { + percents = 0; + } + + this.InitVolumeBlockedTimer(); + + this.PreviousVolume = percents; + + var request = new PlayerVolumeRequest(percents) { DeviceId = this._currentDeviceId }; + this.CheckSpotifyResponse(this.Client.Player.SetVolume, request); + } + + private Boolean _volumeCallsBlocked; + private Timer _volumeBlockedTimer; + private String _currentDeviceId; + + public void AdjustVolume(Int32 ticks) + { + var modifiedVolume = 0; + + // Because this can be called in rapid succession with a dial turn, and it take Spotify a bit of time to register + // volume changes round trip to the api, we don't want to Get the current Volume from Spotify if we've very recently set it + // a few times. Thus, we have a 2 second buffer after the last volume set, before we try to get the actual current volume + // from Spotify. + if (this._volumeCallsBlocked) + { + modifiedVolume = this.PreviousVolume + ticks; + } + else + { + var playback = this.GetCurrentPlayback(); + if (playback?.Device == null) + { + // Set plugin status and message + this.OnWrapperStatusChanged(WrapperStatus.Warning, "Cannot adjust volume, no device", null); + return; + } + else + { + modifiedVolume = (Int32)playback.Device.VolumePercent + ticks; + } + } + + this.SetVolume(modifiedVolume); + } + + private void InitVolumeBlockedTimer() + { + if (this._volumeBlockedTimer == null) + { + this._volumeBlockedTimer = new Timer(2000); + this._volumeBlockedTimer.Elapsed += this.VolumeBlockExpired; + } + + this._volumeCallsBlocked = true; + if (this._volumeBlockedTimer.Enabled) + { + this._volumeBlockedTimer.Stop(); + } + + this._volumeBlockedTimer.Start(); + } + + private void VolumeBlockExpired(Object o, ElapsedEventArgs e) => this._volumeCallsBlocked = false; + + public void Mute() + { + var playback = this.GetCurrentPlayback(); + var volumePercent = playback?.Device?.VolumePercent; + if (volumePercent > 0) + { + this.PreviousVolume = (Int32)volumePercent; + } + + this.SetVolume(0); + } + + public void Unmute() + { + var unmuteVolume = this.PreviousVolume != 0 ? this.PreviousVolume : 50; + + this.SetVolume(unmuteVolume); + } + + /// + /// Toggle current Mute setting + /// + /// true if muted after this call + public Boolean ToggleMute() + { + var playback = this.GetCurrentPlayback(); + + if (playback?.Device.VolumePercent != 0) + { + this.Mute(); + return this.CachedMuteState = true; + } + else + { + this.Unmute(); + return this.CachedMuteState = false; + } + } + + public const String _activeDevice = "activedevice"; + + public void TransferPlayback(String commandParameter) + { + if (commandParameter == _activeDevice) + { + commandParameter = null; + } + + this.CurrentDeviceId = commandParameter; + + this.CheckSpotifyResponse(this.Client.Player.TransferPlayback, new PlayerTransferPlaybackRequest(new[] { this.CurrentDeviceId })); + } + + public void TogglePlayback() + { + var playback = this.Client.Player.GetCurrentPlayback().Result; + if (playback.IsPlaying) + { + this.Client.Player.PausePlayback(new PlayerPausePlaybackRequest() { DeviceId = this.CurrentDeviceId }); + } + else + { + this.Client.Player.ResumePlayback(new PlayerResumePlaybackRequest() { DeviceId = this.CurrentDeviceId }); + } + + this.CachedPlayingState = !playback.IsPlaying; // presume we switched it at this point. + } + + public void SkipPlaybackToNext() => this.CheckSpotifyResponse(this.Client.Player.SkipNext); + + public void SkipPlaybackToPrevious() => this.CheckSpotifyResponse(this.Client.Player.SkipPrevious); + + public PlayerSetRepeatRequest.State ChangeRepeatState() + { + var playback = this.GetCurrentPlayback(); + + var newRepeatState = PlayerSetRepeatRequest.State.Off; + + switch (playback.RepeatState.ToLower()) + { + case "off": + newRepeatState = PlayerSetRepeatRequest.State.Context; + break; + + case "context": + newRepeatState = PlayerSetRepeatRequest.State.Track; + break; + + case "track": + newRepeatState = PlayerSetRepeatRequest.State.Off; + break; + + default: + this.OnWrapperStatusChanged(WrapperStatus.Warning, "Not able to change repeat state (check device etc.)", null); + break; + } + + this.CachedRepeatState = newRepeatState; + + this.CheckSpotifyResponse(this.Client.Player.SetRepeat, new PlayerSetRepeatRequest(newRepeatState) { DeviceId = this.CurrentDeviceId }); + + return newRepeatState; + } + + public Boolean ShufflePlay() + { + var playback = this.GetCurrentPlayback(); + var shuffleState = !playback.ShuffleState; + + this.CheckSpotifyResponse(this.Client.Player.SetShuffle, new PlayerShuffleRequest(shuffleState) { DeviceId = this.CurrentDeviceId }); + + this.CachedShuffleState = shuffleState; + + return shuffleState; + } + + public void StartPlaylist(String contextUri) => + this.CheckSpotifyResponse(this.Client.Player.ResumePlayback, new PlayerResumePlaybackRequest() { ContextUri = contextUri, DeviceId = this.CurrentDeviceId }); + + public void SaveToPlaylist(String playlistId) + { + playlistId = playlistId.Replace("spotify:playlist:", String.Empty); + + var playbackResponse = this.GetCurrentPlayback(); + if (playbackResponse.Item is FullTrack track) + { + SnapshotResponse GetPlayistAddResponse() => this.Client.Playlists.AddItems(playlistId, new PlaylistAddItemsRequest(new[] { track.Uri })).Result; + this.CheckSpotifyResponse(GetPlayistAddResponse); + } + } + + public List GetAllPlaylists() + { + Paging playlists = this.GetUserPlaylists(); + if (playlists != null) + { + var totalPlaylistsCount = playlists.Total; + while (playlists.Items.Count < totalPlaylistsCount) + { + playlists.Items.AddRange(this.GetUserPlaylists(playlists.Items.Count).Items); + } + + return playlists.Items; + } + + return null; + } + + private Paging GetUserPlaylists(Int32 offset = 0) + { + var userProfile = this.CheckSpotifyResponse(this.Client.UserProfile.Current).Result; + Paging GetUsers() => this.Client.Playlists.GetUsers(userProfile.Id, new PlaylistGetUsersRequest() { Limit = 50, Offset = offset }).Result; + return this.CheckSpotifyResponse(GetUsers); + } + + public List GetDevices() + { + var devicesResponse = this.CheckSpotifyResponse(this.Client.Player.GetAvailableDevices).Result; + var devices = devicesResponse.Devices; + + if (devices?.Any() == true) + { + devices.Add(new Device { Id = _activeDevice, Name = "Active Device" }); + } + + return devices; + } + + public void ToggleLike() + { + var currentlyPlaying = this.GetCurrentPlayback(); + switch (currentlyPlaying.Item) + { + case FullTrack track: + var trackId = new[] { track.Id }; + var trackLikedResponse = this.CheckSpotifyResponse(this.Client.Library.CheckTracks, new LibraryCheckTracksRequest(trackId)); + var trackLiked = trackLikedResponse.Result.FirstOrDefault(); + if (trackLiked) + { + this.CheckSpotifyResponse(this.Client.Library.RemoveTracks, new LibraryRemoveTracksRequest(trackId)); + } + else + { + this.CheckSpotifyResponse(this.Client.Library.SaveTracks, new LibrarySaveTracksRequest(trackId)); + } + break; + + case FullEpisode episode: + var episodeId = new[] { episode.Id }; + var episodeLikedResponse = this.CheckSpotifyResponse(this.Client.Library.CheckEpisodes, new LibraryCheckEpisodesRequest(episodeId)); + var episodeLiked = episodeLikedResponse.Result.FirstOrDefault(); + if (episodeLiked) + { + this.CheckSpotifyResponse(this.Client.Library.RemoveEpisodes, new LibraryRemoveEpisodesRequest(episodeId)); + } + else + { + this.CheckSpotifyResponse(this.Client.Library.SaveEpisodes, new LibrarySaveEpisodesRequest(episodeId)); + } + break; + } + } + + public CurrentlyPlayingContext GetCurrentPlayback() => + this.CheckSpotifyResponse(this.Client.Player.GetCurrentPlayback).Result; + } +} \ No newline at end of file diff --git a/src/SpotifyPremiumPlugin/packages.config b/src/SpotifyPremiumPlugin/packages.config new file mode 100644 index 0000000..34138f8 --- /dev/null +++ b/src/SpotifyPremiumPlugin/packages.config @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/Win/spotify-client-template.txt b/src/SpotifyPremiumPlugin/spotify-client-template.yml similarity index 79% rename from src/Win/spotify-client-template.txt rename to src/SpotifyPremiumPlugin/spotify-client-template.yml index a86ce25..d4f8edb 100644 --- a/src/Win/spotify-client-template.txt +++ b/src/SpotifyPremiumPlugin/spotify-client-template.yml @@ -1,8 +1,9 @@ # This is a client configuration for Spotify Premium Service access / Spotify Web API # For the instructions on how to obtain the client ID and secret, please see https://developer.spotify.com/documentation/general/guides/authorization/app-settings/ -# Port(s) must correspond to that on the Spotify Redirect URIs. E.g. Redirect URIs: http://localhost:4500 for below configuration. +# Port(s) must correspond to that on the Spotify Redirect URIs. E.g. Redirect URIs: http://localhost:5000 for below configuration. # This file is stored in %LOCALAPPDATA%\Loupedeck\PluginData\SpotifyPremiumPlugin in Windows and # ~/.local/.... /Loupedeck/PluginData/SpotifyPremiumPlugin on Mac -ClientId=1234567890 -ClientSecret=1234567890 -TcpPorts=4500, +ClientId: 1234567890 +ClientSecret: 1234567890 +TcpPorts: +- 5000 \ No newline at end of file diff --git a/src/Win/Adjustments/Volume/SpotifyVolumeAdjustment.cs b/src/Win/Adjustments/Volume/SpotifyVolumeAdjustment.cs deleted file mode 100644 index 25cabdb..0000000 --- a/src/Win/Adjustments/Volume/SpotifyVolumeAdjustment.cs +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright(c) Loupedeck.All rights reserved. - -namespace Loupedeck.SpotifyPremiumPlugin -{ - using System; - using System.Timers; - using SpotifyAPI.Web.Models; - - internal class SpotifyVolumeAdjustment : PluginDynamicAdjustment - { - private SpotifyPremiumPlugin SpotifyPremiumPlugin => this.Plugin as SpotifyPremiumPlugin; - - private Boolean _volumeBlocked; - - private Timer _volumeBlockedTimer; - - public SpotifyVolumeAdjustment() - : base( - "Spotify Volume", - "Spotify Volume description", - "Spotify Volume", - true) - { - } - - protected override void ApplyAdjustment(String actionParameter, Int32 ticks) - { - try - { - var modifiedVolume = 0; - if (this._volumeBlocked) - { - modifiedVolume = this.SpotifyPremiumPlugin.PreviousVolume + ticks; - } - else - { - var playback = this.SpotifyPremiumPlugin.Api.GetPlayback(); - if (playback?.Device == null) - { - // Set plugin status and message - this.SpotifyPremiumPlugin.CheckStatusCode(System.Net.HttpStatusCode.NotFound, "Cannot adjust volume, no device"); - return; - } - else - { - this.InitVolumeBlockedTimer(); - modifiedVolume = playback.Device.VolumePercent + ticks; - } - } - - this.SpotifyPremiumPlugin.PreviousVolume = modifiedVolume; - this.SpotifyPremiumPlugin.CheckSpotifyResponse(this.SetVolume, modifiedVolume); - } - catch (Exception e) - { - Tracer.Trace($"Spotify SpotifyVolumeAdjustment action obtain an error: ", e); - } - } - - // Overwrite the RunCommand method that is called every time the user presses the encoder to which this command is assigned - protected override void RunCommand(String actionParameter) - { - try - { - this.SpotifyPremiumPlugin.CheckSpotifyResponse(this.TogglePlayback); - } - catch (Exception e) - { - Tracer.Trace($"Spotify SpotifyVolumeAdjustment action obtain an error: ", e); - } - } - - protected override BitmapImage GetCommandImage(String actionParameter, PluginImageSize imageSize) - { - // Dial strip 50px - var bitmapImage = EmbeddedResources.ReadImage("Loupedeck.SpotifyPremiumPlugin.Icons.Width50.Volume.png"); - return bitmapImage; - } - - public ErrorResponse TogglePlayback() - { - var playback = this.SpotifyPremiumPlugin.Api.GetPlayback(); - return playback.IsPlaying - ? this.SpotifyPremiumPlugin.Api.PausePlayback(this.SpotifyPremiumPlugin.CurrentDeviceId) - : this.SpotifyPremiumPlugin.Api.ResumePlayback(this.SpotifyPremiumPlugin.CurrentDeviceId, String.Empty, null, String.Empty, 0); - } - - private void InitVolumeBlockedTimer() - { - if (this._volumeBlockedTimer == null) - { - this._volumeBlockedTimer = new Timer(2000); - this._volumeBlockedTimer.Elapsed += this.VolumeBlockExpired; - } - - this._volumeBlocked = true; - if (this._volumeBlockedTimer.Enabled) - { - this._volumeBlockedTimer.Stop(); - } - - this._volumeBlockedTimer.Start(); - } - - public ErrorResponse SetVolume(Int32 percents) - { - if (percents > 100) - { - percents = 100; - } - - if (percents < 0) - { - percents = 0; - } - - var response = this.SpotifyPremiumPlugin.Api.SetVolume(percents, this.SpotifyPremiumPlugin.CurrentDeviceId); - return response; - } - - private void VolumeBlockExpired(Object o, ElapsedEventArgs e) => this._volumeBlocked = false; - } -} diff --git a/src/Win/Commands/LoginToSpotifyCommand.cs b/src/Win/Commands/LoginToSpotifyCommand.cs deleted file mode 100644 index c172429..0000000 --- a/src/Win/Commands/LoginToSpotifyCommand.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright(c) Loupedeck.All rights reserved. - -namespace Loupedeck.SpotifyPremiumPlugin -{ - using System; - using Loupedeck; - - internal class LoginToSpotifyCommand : PluginDynamicCommand - { - public LoginToSpotifyCommand() - : base( - "Login to Spotify", - "Premium user login to Spotify API", - "Login") - { - } - - protected override void RunCommand(String actionParameter) => (this.Plugin as SpotifyPremiumPlugin).LoginToSpotify(); - } -} diff --git a/src/Win/Commands/Playback/ChangeRepeatStateCommand.cs b/src/Win/Commands/Playback/ChangeRepeatStateCommand.cs deleted file mode 100644 index fbda683..0000000 --- a/src/Win/Commands/Playback/ChangeRepeatStateCommand.cs +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright(c) Loupedeck.All rights reserved. - -namespace Loupedeck.SpotifyPremiumPlugin -{ - using System; - using SpotifyAPI.Web.Enums; - using SpotifyAPI.Web.Models; - - internal class ChangeRepeatStateCommand : PluginDynamicCommand - { - private SpotifyPremiumPlugin SpotifyPremiumPlugin => this.Plugin as SpotifyPremiumPlugin; - - private RepeatState _repeatState; - - public ChangeRepeatStateCommand() - : base( - "Change Repeat State", - "Change Repeat State description", - "Playback") - { - } - - protected override void RunCommand(String actionParameter) - { - try - { - var playback = this.SpotifyPremiumPlugin.Api.GetPlayback(); - switch (playback.RepeatState) - { - case RepeatState.Off: - this._repeatState = RepeatState.Context; - this.SpotifyPremiumPlugin.CheckSpotifyResponse(this.ChangeRepeatState, this._repeatState); - break; - - case RepeatState.Context: - this._repeatState = RepeatState.Track; - this.SpotifyPremiumPlugin.CheckSpotifyResponse(this.ChangeRepeatState, this._repeatState); - break; - - case RepeatState.Track: - this._repeatState = RepeatState.Off; - this.SpotifyPremiumPlugin.CheckSpotifyResponse(this.ChangeRepeatState, this._repeatState); - break; - - default: - // Set plugin status and message - this.SpotifyPremiumPlugin.CheckStatusCode(System.Net.HttpStatusCode.NotFound, "Not able to change repeat state (check device etc.)"); - break; - } - } - catch (Exception e) - { - Tracer.Trace($"Spotify ChangeRepeatStateCommand action obtain an error: ", e); - } - } - - protected override BitmapImage GetCommandImage(String actionParameter, PluginImageSize imageSize) - { - String icon; - switch (this._repeatState) - { - case RepeatState.Off: - icon = "Loupedeck.SpotifyPremiumPlugin.Icons.Width80.RepeatOff.png"; - break; - - case RepeatState.Context: - icon = "Loupedeck.SpotifyPremiumPlugin.Icons.Width80.RepeatList.png"; - break; - - case RepeatState.Track: - icon = "Loupedeck.SpotifyPremiumPlugin.Icons.Width80.Repeat.png"; - break; - - default: - // Set plugin status and message - icon = "Loupedeck.SpotifyPremiumPlugin.Icons.Width80.RepeatOff.png"; - break; - } - - var bitmapImage = EmbeddedResources.ReadImage(icon); - return bitmapImage; - } - - public ErrorResponse ChangeRepeatState(RepeatState repeatState) - { - var response = this.SpotifyPremiumPlugin.Api.SetRepeatMode(repeatState, this.SpotifyPremiumPlugin.CurrentDeviceId); - - this.ActionImageChanged(); - - return response; - } - } -} diff --git a/src/Win/Commands/Playback/ShufflePlayCommand.cs b/src/Win/Commands/Playback/ShufflePlayCommand.cs deleted file mode 100644 index 137e173..0000000 --- a/src/Win/Commands/Playback/ShufflePlayCommand.cs +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright(c) Loupedeck.All rights reserved. - -namespace Loupedeck.SpotifyPremiumPlugin -{ - using System; - using SpotifyAPI.Web.Models; - - internal class ShufflePlayCommand : PluginDynamicCommand - { - private SpotifyPremiumPlugin SpotifyPremiumPlugin => this.Plugin as SpotifyPremiumPlugin; - - private Boolean _shuffleState; - - public ShufflePlayCommand() - : base( - "Shuffle Play", - "Shuffle Play description", - "Playback") - { - } - - protected override void RunCommand(String actionParameter) - { - try - { - this.SpotifyPremiumPlugin.CheckSpotifyResponse(this.ShufflePlay); - } - catch (Exception e) - { - Tracer.Trace($"Spotify ShufflePlayCommand action obtain an error: ", e); - } - } - - protected override BitmapImage GetCommandImage(String actionParameter, PluginImageSize imageSize) - { - return this._shuffleState ? - EmbeddedResources.ReadImage("Loupedeck.SpotifyPremiumPlugin.Icons.Width80.Shuffle.png") : - EmbeddedResources.ReadImage("Loupedeck.SpotifyPremiumPlugin.Icons.Width80.ShuffleOff.png"); - } - - public ErrorResponse ShufflePlay() - { - var playback = this.SpotifyPremiumPlugin.Api.GetPlayback(); - this._shuffleState = !playback.ShuffleState; - var response = this.SpotifyPremiumPlugin.Api.SetShuffle(this._shuffleState, this.SpotifyPremiumPlugin.CurrentDeviceId); - - this.ActionImageChanged(); - - return response; - } - } -} diff --git a/src/Win/Commands/Playback/TogglePlaybackCommand.cs b/src/Win/Commands/Playback/TogglePlaybackCommand.cs deleted file mode 100644 index faa4b7d..0000000 --- a/src/Win/Commands/Playback/TogglePlaybackCommand.cs +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright(c) Loupedeck.All rights reserved. - -namespace Loupedeck.SpotifyPremiumPlugin -{ - using System; - using SpotifyAPI.Web.Models; - - internal class TogglePlaybackCommand : PluginDynamicCommand - { - private SpotifyPremiumPlugin SpotifyPremiumPlugin => this.Plugin as SpotifyPremiumPlugin; - - private Boolean _isPlaying = true; - - public TogglePlaybackCommand() - : base( - "Toggle Playback", - "Toggles audio playback", - "Playback") - { - } - - protected override void RunCommand(String actionParameter) - { - try - { - this.SpotifyPremiumPlugin.CheckSpotifyResponse(this.TogglePlayback); - } - catch (Exception e) - { - Tracer.Trace($"Spotify TogglePlayback action obtain an error: ", e); - } - } - - protected override BitmapImage GetCommandImage(String actionParameter, PluginImageSize imageSize) - { - return this._isPlaying ? - EmbeddedResources.ReadImage("Loupedeck.SpotifyPremiumPlugin.Icons.Width80.Play.png") : - EmbeddedResources.ReadImage("Loupedeck.SpotifyPremiumPlugin.Icons.Width80.Pause.png"); - } - - public ErrorResponse TogglePlayback() - { - var playback = this.SpotifyPremiumPlugin.Api.GetPlayback(); - this._isPlaying = playback.IsPlaying; - - this.ActionImageChanged(); - - return playback.IsPlaying - ? this.SpotifyPremiumPlugin.Api.PausePlayback(this.SpotifyPremiumPlugin.CurrentDeviceId) - : this.SpotifyPremiumPlugin.Api.ResumePlayback(this.SpotifyPremiumPlugin.CurrentDeviceId, String.Empty, null, String.Empty, 0); - } - } -} diff --git a/src/Win/Commands/ToggleLikeCommand.cs b/src/Win/Commands/ToggleLikeCommand.cs deleted file mode 100644 index b049b59..0000000 --- a/src/Win/Commands/ToggleLikeCommand.cs +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright(c) Loupedeck.All rights reserved. - -namespace Loupedeck.SpotifyPremiumPlugin -{ - using System; - using System.Collections.Generic; - using System.Linq; - - internal class ToggleLikeCommand : PluginDynamicCommand - { - private SpotifyPremiumPlugin SpotifyPremiumPlugin => this.Plugin as SpotifyPremiumPlugin; - - private Boolean _isLiked = true; - - public ToggleLikeCommand() - : base( - "Toggle Like", - "Toggle Like", - "Others") - { - } - - protected override void RunCommand(String actionParameter) - { - try - { - var playback = this.SpotifyPremiumPlugin.Api.GetPlayback(); - var trackId = playback.Item?.Id; - if (String.IsNullOrEmpty(trackId)) - { - // Set plugin status and message - this.SpotifyPremiumPlugin.CheckStatusCode(System.Net.HttpStatusCode.NotFound, "No track"); - return; - } - - var trackItemId = new List { trackId }; - var tracksExist = this.SpotifyPremiumPlugin.Api.CheckSavedTracks(trackItemId); - if (tracksExist.List == null && tracksExist.Error != null) - { - // Set plugin status and message - this.SpotifyPremiumPlugin.CheckStatusCode(System.Net.HttpStatusCode.NotFound, "No track list"); - return; - } - - if (tracksExist.List.Any() && tracksExist.List.FirstOrDefault() == false) - { - this.SpotifyPremiumPlugin.CheckSpotifyResponse(this.SpotifyPremiumPlugin.Api.SaveTrack, trackId); - this._isLiked = true; - this.ActionImageChanged(); - } - else - { - this.SpotifyPremiumPlugin.CheckSpotifyResponse(this.SpotifyPremiumPlugin.Api.RemoveSavedTracks, trackItemId); - this._isLiked = false; - - this.ActionImageChanged(); - } - } - catch (Exception e) - { - Tracer.Trace($"Spotify Toggle Like action obtain an error: ", e); - } - } - - protected override BitmapImage GetCommandImage(String actionParameter, PluginImageSize imageSize) - { - return this._isLiked ? - EmbeddedResources.ReadImage("Loupedeck.SpotifyPremiumPlugin.Icons.Width80.SongLike.png") : - EmbeddedResources.ReadImage("Loupedeck.SpotifyPremiumPlugin.Icons.Width80.SongDislike.png"); - } - } -} diff --git a/src/Win/Commands/Volume/MuteCommand.cs b/src/Win/Commands/Volume/MuteCommand.cs deleted file mode 100644 index bd7ad26..0000000 --- a/src/Win/Commands/Volume/MuteCommand.cs +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright(c) Loupedeck.All rights reserved. - -namespace Loupedeck.SpotifyPremiumPlugin -{ - using System; - using SpotifyAPI.Web.Models; - - internal class MuteCommand : PluginDynamicCommand - { - private SpotifyPremiumPlugin SpotifyPremiumPlugin => this.Plugin as SpotifyPremiumPlugin; - - public MuteCommand() - : base( - "Mute", - "Mute description", - "Spotify Volume") - { - } - - protected override void RunCommand(String actionParameter) - { - try - { - this.SpotifyPremiumPlugin.CheckSpotifyResponse(this.Mute); - } - catch (Exception e) - { - Tracer.Trace($"Spotify MuteCommand action obtain an error: ", e); - } - } - - protected override BitmapImage GetCommandImage(String actionParameter, PluginImageSize imageSize) - { - var bitmapImage = EmbeddedResources.ReadImage("Loupedeck.SpotifyPremiumPlugin.Icons.Width80.MuteVolume.png"); - return bitmapImage; - } - - public ErrorResponse Mute() - { - var playback = this.SpotifyPremiumPlugin.Api.GetPlayback(); - if (playback?.Device != null) - { - this.SpotifyPremiumPlugin.PreviousVolume = playback.Device.VolumePercent; - } - - var result = this.SpotifyPremiumPlugin.Api.SetVolume(0, this.SpotifyPremiumPlugin.CurrentDeviceId); - - return result; - } - } -} diff --git a/src/Win/Commands/Volume/ToggleMuteCommand.cs b/src/Win/Commands/Volume/ToggleMuteCommand.cs deleted file mode 100644 index 8345673..0000000 --- a/src/Win/Commands/Volume/ToggleMuteCommand.cs +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright(c) Loupedeck.All rights reserved. - -namespace Loupedeck.SpotifyPremiumPlugin -{ - using System; - using SpotifyAPI.Web.Models; - - internal class ToggleMuteCommand : PluginDynamicCommand - { - private SpotifyPremiumPlugin SpotifyPremiumPlugin => this.Plugin as SpotifyPremiumPlugin; - - private Boolean _mute; - - public ToggleMuteCommand() - : base( - "Toggle Mute", - "Toggles audio mute state", - "Spotify Volume") - { - } - - protected override void RunCommand(String actionParameter) - { - try - { - this.SpotifyPremiumPlugin.CheckSpotifyResponse(this.ToggleMute); - } - catch (Exception e) - { - Tracer.Trace($"Spotify ToggleMuteCommand action obtain an error: ", e); - } - } - - - protected override BitmapImage GetCommandImage(String actionParameter, PluginImageSize imageSize) - { - return this._mute ? - EmbeddedResources.ReadImage("Loupedeck.SpotifyPremiumPlugin.Icons.Width80.Volume.png") : - EmbeddedResources.ReadImage("Loupedeck.SpotifyPremiumPlugin.Icons.Width80.MuteVolume.png"); - } - - public ErrorResponse ToggleMute() - { - this.ActionImageChanged(); - - var playback = this.SpotifyPremiumPlugin.Api.GetPlayback(); - return playback?.Device.VolumePercent != 0 ? this.Mute() : this.Unmute(); - } - - public ErrorResponse Unmute() - { - this._mute = false; - var unmuteVolume = this.SpotifyPremiumPlugin.PreviousVolume != 0 ? this.SpotifyPremiumPlugin.PreviousVolume : 50; - var result = this.SpotifyPremiumPlugin.Api.SetVolume(unmuteVolume, this.SpotifyPremiumPlugin.CurrentDeviceId); - return result; - } - - public ErrorResponse Mute() - { - this._mute = true; - var playback = this.SpotifyPremiumPlugin.Api.GetPlayback(); - if (playback?.Device != null) - { - this.SpotifyPremiumPlugin.PreviousVolume = playback.Device.VolumePercent; - } - - var result = this.SpotifyPremiumPlugin.Api.SetVolume(0, this.SpotifyPremiumPlugin.CurrentDeviceId); - - return result; - } - } -} diff --git a/src/Win/Commands/Volume/UnmuteCommand.cs b/src/Win/Commands/Volume/UnmuteCommand.cs deleted file mode 100644 index d3e9d32..0000000 --- a/src/Win/Commands/Volume/UnmuteCommand.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright(c) Loupedeck.All rights reserved. - -namespace Loupedeck.SpotifyPremiumPlugin -{ - using System; - using SpotifyAPI.Web.Models; - - internal class UnmuteCommand : PluginDynamicCommand - { - private SpotifyPremiumPlugin SpotifyPremiumPlugin => this.Plugin as SpotifyPremiumPlugin; - - public UnmuteCommand() - : base( - "Unmute", - "Unmute description", - "Spotify Volume") - { - } - - protected override void RunCommand(String actionParameter) - { - try - { - this.SpotifyPremiumPlugin.CheckSpotifyResponse(this.Unmute); - } - catch (Exception e) - { - Tracer.Trace($"Spotify UnmuteCommand action obtain an error: ", e); - } - } - - - protected override BitmapImage GetCommandImage(String actionParameter, PluginImageSize imageSize) - { - var bitmapImage = EmbeddedResources.ReadImage("Loupedeck.SpotifyPremiumPlugin.Icons.Width80.Volume.png"); - return bitmapImage; - } - - public ErrorResponse Unmute() - { - var unmuteVolume = this.SpotifyPremiumPlugin.PreviousVolume != 0 ? this.SpotifyPremiumPlugin.PreviousVolume : 50; - var result = this.SpotifyPremiumPlugin.Api.SetVolume(unmuteVolume, this.SpotifyPremiumPlugin.CurrentDeviceId); - return result; - } - } -} diff --git a/src/Win/ParameterizedCommands/DirectVolumeCommand.cs b/src/Win/ParameterizedCommands/DirectVolumeCommand.cs deleted file mode 100644 index c4dab53..0000000 --- a/src/Win/ParameterizedCommands/DirectVolumeCommand.cs +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright(c) Loupedeck.All rights reserved. - -namespace Loupedeck.SpotifyPremiumPlugin.ParameterizedCommands -{ - using System; - using SpotifyAPI.Web.Models; - - internal class DirectVolumeCommand : PluginDynamicCommand - { - private SpotifyPremiumPlugin SpotifyPremiumPlugin => this.Plugin as SpotifyPremiumPlugin; - - public DirectVolumeCommand() - : base() - { - // Profile actions do not belong to a group in the current UI, they are on the top level - this.DisplayName = "Direct Volume"; // so this will be shown as "group name" for parameterized commands - this.GroupName = "Not used"; - - this.MakeProfileAction("text;Enter volume level to set 0-100:"); - } - - protected override void RunCommand(String actionParameter) - { - try - { - this.SpotifyPremiumPlugin.CheckSpotifyResponse(this.SetVolume, actionParameter); - } - catch (Exception e) - { - Tracer.Trace($"Spotify DirectVolumeCommand action obtain an error: ", e); - } - } - - public ErrorResponse SetVolume(String percents) - { - var isConverted = Int32.TryParse(percents, out var volume); - return isConverted ? this.SetVolume(volume) : null; - } - - public ErrorResponse SetVolume(Int32 percents) - { - if (percents > 100) - { - percents = 100; - } - - if (percents < 0) - { - percents = 0; - } - - var response = this.SpotifyPremiumPlugin.Api.SetVolume(percents, this.SpotifyPremiumPlugin.CurrentDeviceId); - return response; - } - } -} diff --git a/src/Win/SpotifyPremiumPlugin.Api.cs b/src/Win/SpotifyPremiumPlugin.Api.cs deleted file mode 100644 index e73ce4d..0000000 --- a/src/Win/SpotifyPremiumPlugin.Api.cs +++ /dev/null @@ -1,331 +0,0 @@ -// Copyright (c) Loupedeck. All rights reserved. - -namespace Loupedeck.SpotifyPremiumPlugin -{ - using System; - using System.Collections.Generic; - using System.IO; - using System.Linq; - using System.Security.Cryptography; - using System.Text; - using Loupedeck; - using Newtonsoft.Json; - using SpotifyAPI.Web; - using SpotifyAPI.Web.Auth; - using SpotifyAPI.Web.Models; - - /// - /// Plugin Spotify API configuration and authorization - /// - public partial class SpotifyPremiumPlugin : Plugin - { - private const String _clientId = "ClientId"; - private const String _clientSecret = "ClientSecret"; - private const String _tcpPorts = "TcpPorts"; - - private static Token token = new Token(); - private static AuthorizationCodeAuth auth; - private static String spotifyTokenFilePath; - - private static Dictionary _spotifyConfiguration; - - private List tcpPorts = new List(); - - internal SpotifyWebAPI Api { get; set; } - - internal String CurrentDeviceId { get; set; } - - internal Int32 PreviousVolume { get; set; } - - public Boolean SpotifyApiConnectionOk() - { - if (this.Api == null) - { - // User not logged in -> Automatically start login - this.LoginToSpotify(); - - // and skip action for now - return false; - } - else if (token != null && DateTime.Now > token.CreateDate.AddSeconds(token.ExpiresIn) && !String.IsNullOrEmpty(token.RefreshToken)) - { - this.RefreshToken(token.RefreshToken); - } - - return true; - } - - private Boolean ReadConfigurationFile() - { - // Get Spotify App configuration from spotify-client.txt file: client id and client secret - // Windows path: %LOCALAPPDATA%/Loupedeck/PluginData/SpotifyPremium/spotify-client.txt - var spotifyClientConfigurationFile = this.ClientConfigurationFilePath; - if (!File.Exists(spotifyClientConfigurationFile)) - { - // Check path - Directory.CreateDirectory(Path.GetDirectoryName(spotifyClientConfigurationFile)); - - // Create the file - using (FileStream fs = File.Create(spotifyClientConfigurationFile)) - { - var info = new UTF8Encoding(true).GetBytes($"{_clientId}{Environment.NewLine}{_clientSecret}{Environment.NewLine}{_tcpPorts}"); - - // Add parameter titles to file. - fs.Write(info, 0, info.Length); - } - - this.OnPluginStatusChanged(Loupedeck.PluginStatus.Error, $"Spotify configuration is missing. Click More Details below", $"file:/{spotifyClientConfigurationFile}"); - return false; - } - - // Read configuration file, skip # comments, trim key and value - _spotifyConfiguration = File.ReadAllLines(spotifyClientConfigurationFile) - .Where(x => !x.StartsWith("#")) - .Select(x => x.Split('=')) - .ToDictionary(x => x[0].Trim(), x => x[1].Trim()); - - if (!(_spotifyConfiguration.ContainsKey(_clientId) && - _spotifyConfiguration.ContainsKey(_clientSecret) && - _spotifyConfiguration.ContainsKey(_tcpPorts))) - { - this.OnPluginStatusChanged(Loupedeck.PluginStatus.Error, $"Check Spotify API app 'ClientId' / 'ClientSecret' and 'TcpPorts' in configuration file. Click More Details below", $"file:/{spotifyClientConfigurationFile}"); - return false; - } - - // Check TCP Ports - this.tcpPorts = _spotifyConfiguration[_tcpPorts] - .Split(',') - .Select(x => new { valid = Int32.TryParse(x.Trim(), out var val), port = val }) - .Where(x => x.valid) - .Select(x => x.port) - .ToList(); - - if (this.tcpPorts.Count == 0) - { - this.OnPluginStatusChanged(Loupedeck.PluginStatus.Error, $"Check 'TcpPorts' values in configuration file. Click More Details below", $"file:/{spotifyClientConfigurationFile}"); - return false; - } - - return true; - } - - private void SpotifyConfiguration() - { - if (!this.ReadConfigurationFile()) - { - return; - } - - // Is there a token available - token = null; - spotifyTokenFilePath = System.IO.Path.Combine(this.GetPluginDataDirectory(), "spotify.json"); - if (File.Exists(spotifyTokenFilePath)) - { - token = this.ReadTokenFromLocalFile(); - } - - // Check token and the expiration datetime - if (token != null && DateTime.Now < token.CreateDate.AddSeconds(token.ExpiresIn)) - { - // Use the existing token - this.Api = new SpotifyWebAPI - { - AccessToken = token.AccessToken, - TokenType = "Bearer", - }; - this.OnPluginStatusChanged(Loupedeck.PluginStatus.Normal, "Connected", null); - } - else if (token != null && !String.IsNullOrEmpty(token.RefreshToken)) - { - // Get a new access token based on the Refresh Token - this.RefreshToken(token.RefreshToken); - } - else - { - // User has to login from Loupedeck application Plugin UI: Login - Login to Spotify. See LoginToSpotifyCommand.cs - this.OnPluginStatusChanged(Loupedeck.PluginStatus.Error, "Login to Spotify as Premium user", null); - } - } - - private Token ReadTokenFromLocalFile() - { - var json = File.ReadAllText(spotifyTokenFilePath); - var localToken = JsonConvert.DeserializeObject(json); - - // Decrypt refresh token - if (localToken != null && !String.IsNullOrEmpty(localToken.RefreshToken)) - { - var secret = Convert.FromBase64String(localToken.RefreshToken); - var plain = ProtectedData.Unprotect(secret, null, DataProtectionScope.CurrentUser); - var encoding = new UTF8Encoding(); - localToken.RefreshToken = encoding.GetString(plain); - } - - return localToken; - } - - private void SaveTokenToLocalFile(Token newToken, String refreshToken) - { - // Decrypt refresh token - var encoding = new UTF8Encoding(); - var plain = encoding.GetBytes(refreshToken); - var secret = ProtectedData.Protect(plain, null, DataProtectionScope.CurrentUser); - newToken.RefreshToken = Convert.ToBase64String(secret); - - File.WriteAllText(spotifyTokenFilePath, JsonConvert.SerializeObject(newToken)); - } - - public void RefreshToken(String refreshToken) - { - auth = this.GetAuthorizationCodeAuth(out var timeout); - - Token newToken = auth.RefreshToken(refreshToken).Result; - - if (!String.IsNullOrWhiteSpace(newToken.Error)) - { - Tracer.Error($"Error happened during refreshing Spotify account token: {newToken.Error}: {newToken.ErrorDescription}"); - this.OnPluginStatusChanged(Loupedeck.PluginStatus.Error, "Failed getting access to Spotify. Login as Premium user", null); - } - - if (this.Api == null) - { - this.Api = new SpotifyWebAPI - { - AccessToken = newToken.AccessToken, - TokenType = "Bearer", - }; - } - - this.OnPluginStatusChanged(Loupedeck.PluginStatus.Normal, "Connected", null); - - this.Api.AccessToken = newToken.AccessToken; - this.SaveTokenToLocalFile(newToken, refreshToken); - } - - private PrivateProfile _privateProfile; - - private Paging GetUserPlaylists(Int32 offset = 0) - { - if (this.Api != null) - { - try - { - if (this._privateProfile == null) - { - this._privateProfile = this.Api.GetPrivateProfile(); - } - - var profileId = this._privateProfile?.Id; - if (!String.IsNullOrEmpty(profileId)) - { - var playlists = this.Api.GetUserPlaylists(profileId, 50, offset); - if (playlists?.Items != null && playlists.Items.Any()) - { - return playlists; - } - } - } - catch (Exception e) - { - Tracer.Trace(e, "Spotify playlists obtaining error"); - } - } - - return new Paging - { - Items = new List(), - }; - } - - public Paging GetAllPlaylists() - { - Paging playlists = this.GetUserPlaylists(); - if (playlists != null) - { - var totalPlaylistsCount = playlists.Total; - while (playlists.Items.Count < totalPlaylistsCount) - { - playlists.Items.AddRange(this.GetUserPlaylists(playlists.Items.Count).Items); - } - - return playlists; - } - - return null; - } - - public void LoginToSpotify() - { - auth = this.GetAuthorizationCodeAuth(out var timeout); - - auth.AuthReceived += this.Auth_AuthReceived; - - auth.Start(); - auth.OpenBrowser(); - } - - private async void Auth_AuthReceived(Object sender, AuthorizationCode payload) - { - try - { - auth.Stop(); - - var previousToken = await auth.ExchangeCode(payload.Code); - if (!String.IsNullOrWhiteSpace(previousToken.Error)) - { - Tracer.Error($"Error happened during adding Spotify account: {previousToken.Error}: {previousToken.ErrorDescription}"); - return; - } - - this.Api = new SpotifyWebAPI - { - AccessToken = previousToken.AccessToken, - TokenType = previousToken.TokenType, - }; - - this.OnPluginStatusChanged(Loupedeck.PluginStatus.Normal, null, null); - - this.SaveTokenToLocalFile(previousToken, previousToken.RefreshToken); - } - catch (Exception ex) - { - Tracer.Error($"Error happened during Spotify authentication: {ex.Message}"); - } - } - - public AuthorizationCodeAuth GetAuthorizationCodeAuth(out Int32 timeout) - { - timeout = 240000; // ?!? - - if (!NetworkHelpers.TryGetFreeTcpPort(this.tcpPorts, out var selectedPort)) - { - Tracer.Error("No available ports for Spotify!"); - return null; - } - - var scopes = - SpotifyAPI.Web.Enums.Scope.PlaylistReadPrivate | - SpotifyAPI.Web.Enums.Scope.Streaming | - SpotifyAPI.Web.Enums.Scope.UserReadCurrentlyPlaying | - SpotifyAPI.Web.Enums.Scope.UserReadPlaybackState | - SpotifyAPI.Web.Enums.Scope.UserLibraryRead | - SpotifyAPI.Web.Enums.Scope.UserLibraryModify | - SpotifyAPI.Web.Enums.Scope.UserReadPrivate | - SpotifyAPI.Web.Enums.Scope.UserModifyPlaybackState | - SpotifyAPI.Web.Enums.Scope.PlaylistReadCollaborative | - SpotifyAPI.Web.Enums.Scope.PlaylistModifyPublic | - SpotifyAPI.Web.Enums.Scope.PlaylistModifyPrivate | - SpotifyAPI.Web.Enums.Scope.PlaylistReadPrivate | - SpotifyAPI.Web.Enums.Scope.UserReadEmail; - - return !this.ReadConfigurationFile() - ? null - : new AuthorizationCodeAuth( - _spotifyConfiguration[_clientId], // Spotify API Client Id - _spotifyConfiguration[_clientSecret], // Spotify API Client Secret - $"http://localhost:{selectedPort}", // selectedPort must correspond to that on the Spotify developers's configuration! - $"http://localhost:{selectedPort}", - scopes); - } - } -} diff --git a/src/Win/SpotifyPremiumPlugin.ApiResponse.cs b/src/Win/SpotifyPremiumPlugin.ApiResponse.cs deleted file mode 100644 index 9d1ff9f..0000000 --- a/src/Win/SpotifyPremiumPlugin.ApiResponse.cs +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright (c) Loupedeck. All rights reserved. - -namespace Loupedeck.SpotifyPremiumPlugin -{ - using System; - using System.Net; - using Loupedeck; - using SpotifyAPI.Web.Models; - - /// - /// Plugin: Check Spotify API responses - /// - public partial class SpotifyPremiumPlugin : Plugin - { - public void CheckSpotifyResponse(Func apiCall, T param) - { - if (!this.SpotifyApiConnectionOk()) - { - return; - } - - var response = apiCall(param); - - this.CheckStatusCode(response.StatusCode(), response.Error?.Message); - } - - public void CheckSpotifyResponse(Func apiCall) - { - if (!this.SpotifyApiConnectionOk()) - { - return; - } - - var response = apiCall(); - - this.CheckStatusCode(response.StatusCode(), response.Error?.Message); - } - - internal void CheckStatusCode(HttpStatusCode statusCode, String spotifyApiMessage) - { - switch (statusCode) - { - case HttpStatusCode.Continue: - case HttpStatusCode.SwitchingProtocols: - case HttpStatusCode.OK: - case HttpStatusCode.Created: - case HttpStatusCode.Accepted: - case HttpStatusCode.NonAuthoritativeInformation: - case HttpStatusCode.NoContent: - case HttpStatusCode.ResetContent: - case HttpStatusCode.PartialContent: - - if (this.PluginStatus.Status != Loupedeck.PluginStatus.Normal) - { - this.OnPluginStatusChanged(Loupedeck.PluginStatus.Normal, null, null); - } - - break; - - case HttpStatusCode.Unauthorized: - // This should never happen? - this.OnPluginStatusChanged(Loupedeck.PluginStatus.Error, "Login to Spotify", null); - break; - - case HttpStatusCode.NotFound: - // User doesn't have device set or some other Spotify related thing. User action needed. - this.OnPluginStatusChanged(Loupedeck.PluginStatus.Warning, $"Spotify message: {spotifyApiMessage}", null); - break; - - default: - if (this.PluginStatus.Status != Loupedeck.PluginStatus.Error) - { - this.OnPluginStatusChanged(Loupedeck.PluginStatus.Error, spotifyApiMessage, null); - } - - break; - } - } - } -} diff --git a/src/Win/packages.config b/src/Win/packages.config deleted file mode 100644 index 2547ac5..0000000 --- a/src/Win/packages.config +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file