From 01bdd928c72091b55ffb78d48669b9642d08d183 Mon Sep 17 00:00:00 2001 From: Kieran Bond Date: Mon, 20 Sep 2021 19:14:27 +0100 Subject: [PATCH] Update deps, add missed TinYard (#37) Updated the dependency, added the missing TinYard implementation.. whoops! --- MiniSpotify/MiniSpotify/MiniSpotify.csproj | 2 +- MiniSpotify/MiniSpotify/MiniSpotifyConfig.cs | 44 +++ .../MiniSpotify/Source/Impl/SpotifyService.cs | 315 ++++++++++++++++++ .../Source/Interfaces/ISpotifyService.cs | 27 ++ .../Source/VO/ContextualUpdateVO.cs | 30 ++ README.md | 4 +- 6 files changed, 420 insertions(+), 2 deletions(-) create mode 100644 MiniSpotify/MiniSpotify/MiniSpotifyConfig.cs create mode 100644 MiniSpotify/MiniSpotify/Source/Impl/SpotifyService.cs create mode 100644 MiniSpotify/MiniSpotify/Source/Interfaces/ISpotifyService.cs create mode 100644 MiniSpotify/MiniSpotify/Source/VO/ContextualUpdateVO.cs diff --git a/MiniSpotify/MiniSpotify/MiniSpotify.csproj b/MiniSpotify/MiniSpotify/MiniSpotify.csproj index fe4c3ad..bb40eb1 100644 --- a/MiniSpotify/MiniSpotify/MiniSpotify.csproj +++ b/MiniSpotify/MiniSpotify/MiniSpotify.csproj @@ -217,7 +217,7 @@ 13.0.1 - 6.1.0 + 6.2.1 1.3.1 diff --git a/MiniSpotify/MiniSpotify/MiniSpotifyConfig.cs b/MiniSpotify/MiniSpotify/MiniSpotifyConfig.cs new file mode 100644 index 0000000..ba9bb1b --- /dev/null +++ b/MiniSpotify/MiniSpotify/MiniSpotifyConfig.cs @@ -0,0 +1,44 @@ +using MiniSpotify.Source.Impl; +using MiniSpotify.Source.Interfaces; +using System; +using TinYard.API.Interfaces; +using TinYard.Framework.Impl.Attributes; + +namespace MiniSpotify +{ + public class MiniSpotifyConfig : IConfig + { + [Inject] + public IContext context; + + public object Environment => null; + + private SpotifyService _spotifyService; + + private string _clientID = "93f2598a9eaf4056b34f7b5ca254ff17"; + + private event Action _onServiceConnected; + + public void Configure() + { + _spotifyService = new SpotifyService(_clientID); + context.Mapper.Map().ToValue(_spotifyService); + + context.PostInitialize += OnContextInitialized; + } + + private async void OnContextInitialized() + { + await _spotifyService.Connect(); + _onServiceConnected.Invoke(); + _spotifyService.SetupUpdate(); + } + + public MiniSpotifyConfig OnServiceConnected(Action callback) + { + _onServiceConnected += callback; + + return this; + } + } +} diff --git a/MiniSpotify/MiniSpotify/Source/Impl/SpotifyService.cs b/MiniSpotify/MiniSpotify/Source/Impl/SpotifyService.cs new file mode 100644 index 0000000..9ca8ccb --- /dev/null +++ b/MiniSpotify/MiniSpotify/Source/Impl/SpotifyService.cs @@ -0,0 +1,315 @@ +using MiniSpotify.HelperScripts; +using MiniSpotify.Source.Interfaces; +using MiniSpotify.Source.VO; +using SpotifyAPI.Web; +using SpotifyAPI.Web.Auth; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using TinYard.Extensions.CallbackTimer.API.Services; +using TinYard.Framework.Impl.Attributes; + +namespace MiniSpotify.Source.Impl +{ + public class SpotifyService : ISpotifyService + { + [Inject] + public ICallbackTimer CallbackTimer { get; private set; } + + public event Action OnPlaybackUpdated; + + private SpotifyClient _spotifyClient; + private EmbedIOAuthServer _server; + private string _authToken; + private string _clientID; + + //https://developer.spotify.com/documentation/general/guides/scopes/ + private string[] _accessScopes = new string[] + { + Scopes.UserModifyPlaybackState, + Scopes.Streaming, + Scopes.UserReadRecentlyPlayed, + Scopes.UserReadCurrentlyPlaying, + Scopes.UserReadPlaybackState, + Scopes.UserLibraryModify, + Scopes.UserLibraryRead + }; + + private double _updateInterval = 0.5d; + + public SpotifyService(string clientID) + { + _clientID = clientID; + } + + public async Task Connect() + { + if (!string.IsNullOrWhiteSpace(_authToken)) + return false; + + // Information: + // https://github.com/JohnnyCrazy/SpotifyAPI-NET + // https://johnnycrazy.github.io/SpotifyAPI-NET/auth/implicit_grant.html + + int redirectPort = 4002; + string redirectURI = $"http://localhost:{redirectPort}"; + + _server = new EmbedIOAuthServer(new Uri(redirectURI), redirectPort); + await _server.Start(); + + var auth = new LoginRequest( + new Uri(redirectURI), + _clientID, + LoginRequest.ResponseType.Token) + { + Scope = _accessScopes, + }; + + BrowserUtil.Open(auth.ToUri()); + + await AwaitImplicitGrantReceived(); + + return true; + } + + public void SetupUpdate() + { + CallbackTimer.AddRecurringTimer(_updateInterval, Update); + } + + private async Task AwaitImplicitGrantReceived() + { + bool grantReceived = false; + + _server.ImplictGrantReceived += async (sender, response) => + { + await _server.Stop(); + + _authToken = response.AccessToken; + + _spotifyClient = new SpotifyClient(_authToken); + + grantReceived = true; + + // Trigger a UI catchup + UpdatePlayback(); + }; + + while (!grantReceived) + { + await Task.Delay(300); + } + } + + private void Update() + { + if (_spotifyClient == null) + return; + + UpdatePlayback(); + } + + public void Disconnect() + { + _server?.Dispose(); + } + + private async void UpdatePlayback() + { + var currentPlayback = await GetCurrentPlayback(); + + float songProgress = currentPlayback != null ? currentPlayback.ProgressMs : 0f; + FullTrack latestSong; + if (currentPlayback == null || currentPlayback.Item == null) + latestSong = await GetLastPlayedSong(); + else + latestSong = currentPlayback.Item as FullTrack; + + if (latestSong == null) + return; + + bool songLiked = await IsSongLiked(latestSong); + bool isSongPlaying = await IsSongPlaying(latestSong); + string artworkURL = GetSongArtworkUrl(latestSong); + string playingContext = await GetPlayingContext(); + songProgress = LerpEaser.GetLerpT(LerpEaser.EaseType.Linear, songProgress, latestSong.DurationMs);// Normalize + ContextualUpdateVO updateVO = new ContextualUpdateVO(latestSong, artworkURL, playingContext, songLiked, isSongPlaying, songProgress); + + OnPlaybackUpdated.Invoke(updateVO); + } + + private async Task GetPlayingContext() + { + var playbackContext = await GetCurrentPlayback(); + if (playbackContext == null || playbackContext.Context == null) + return string.Empty; + + string id = playbackContext.Context?.Uri.Split(':').Last(); + switch (playbackContext.Context.Type) + { + case "album": + return (await _spotifyClient.Albums.Get(id)).Name; + case "artist": + return (await _spotifyClient.Artists.Get(id)).Name; + case "playlist": + return (await _spotifyClient.Playlists.Get(id)).Name; + default: + return string.Empty; + } + } + + private async Task GetLastPlayedSong() + { + var currentlyPlaying = await GetCurrentPlayback(); + if(currentlyPlaying != null && currentlyPlaying.IsPlaying) + { + return currentlyPlaying.Item as FullTrack; + } + + var recentlyPlayed = await _spotifyClient.Player.GetRecentlyPlayed(); + var historyItem = recentlyPlayed.Items?[0]; + + if (historyItem == null) + return null; + + return await _spotifyClient.Tracks.Get(historyItem.Track.Id); + } + + public async Task GetCurrentPlayback() + { + CurrentlyPlayingContext currentPlayback; + try + { + currentPlayback = await _spotifyClient.Player.GetCurrentPlayback(); + } + catch + { + // TODO : Add error handling / logging + return null; + } + + if (currentPlayback == null || currentPlayback.Item == null) + return null; + + return currentPlayback; + } + + public async Task IsSongLiked(FullTrack song) + { + try + { + var likedTracks = await _spotifyClient.Library.GetTracks(); + var fullPlaylist = await _spotifyClient.PaginateAll(likedTracks); + + return fullPlaylist.Any(track => track.Track.Id == song.Id); + } + catch + { + return false; + } + } + + public async Task IsSongPlaying(FullTrack song) + { + var playback = await GetCurrentPlayback(); + if (playback == null || !playback.IsPlaying) + return false; + + if (!(playback.Item is FullTrack)) + return false; + + return (playback.Item as FullTrack).Id == song.Id; + } + + public string GetSongArtworkUrl(FullTrack song) + { + return song.Album.Images[0].Url; + } + + public async Task GetCurrentSongProgress() + { + var playback = await GetCurrentPlayback(); + if (playback == null) + return 0f; + + return playback.ProgressMs; + } + + public async Task ToggleLikeCurrentSong() + { + var currentSong = await GetLastPlayedSong(); + try + { + var tracksToModify = new List() { currentSong.Id }; + if(await IsSongLiked(currentSong)) + { + await _spotifyClient.Library.RemoveTracks(new LibraryRemoveTracksRequest(tracksToModify)); + } + else + { + await _spotifyClient.Library.SaveTracks(new LibrarySaveTracksRequest(tracksToModify)); + } + + return await IsSongLiked(currentSong); + } + catch + { + return false; + } + } + + public async Task TogglePlayingStatus() + { + var playbackContext = await GetCurrentPlayback(); + try + { + if (playbackContext.IsPlaying) + await _spotifyClient.Player.PausePlayback(); + else + await _spotifyClient.Player.ResumePlayback(); + return (await GetCurrentPlayback()).IsPlaying; + } + catch + { + return false; + } + } + + public async void PlayNextSong() + { + try + { + await _spotifyClient.Player.SkipNext(); + } + catch + { + // TODO : Log something + } + } + + public async void PlayPreviousSong() + { + try + { + await _spotifyClient.Player.SkipPrevious(); + } + catch + { + // TODO : Log something + } + } + + public async void RestartCurrentSong() + { + try + { + await _spotifyClient.Player.SeekTo(new PlayerSeekToRequest(0)); + } + catch + { + + } + } + } +} diff --git a/MiniSpotify/MiniSpotify/Source/Interfaces/ISpotifyService.cs b/MiniSpotify/MiniSpotify/Source/Interfaces/ISpotifyService.cs new file mode 100644 index 0000000..819f78a --- /dev/null +++ b/MiniSpotify/MiniSpotify/Source/Interfaces/ISpotifyService.cs @@ -0,0 +1,27 @@ +using MiniSpotify.Source.VO; +using SpotifyAPI.Web; +using System; +using System.Threading.Tasks; + +namespace MiniSpotify.Source.Interfaces +{ + public interface ISpotifyService + { + event Action OnPlaybackUpdated; + + Task Connect(); + void Disconnect(); + + Task GetCurrentPlayback(); + string GetSongArtworkUrl(FullTrack song); + Task GetCurrentSongProgress(); + Task IsSongLiked(FullTrack song); + Task IsSongPlaying(FullTrack song); + + Task ToggleLikeCurrentSong(); + Task TogglePlayingStatus(); + void PlayNextSong(); + void PlayPreviousSong(); + void RestartCurrentSong(); + } +} diff --git a/MiniSpotify/MiniSpotify/Source/VO/ContextualUpdateVO.cs b/MiniSpotify/MiniSpotify/Source/VO/ContextualUpdateVO.cs new file mode 100644 index 0000000..8e7bd36 --- /dev/null +++ b/MiniSpotify/MiniSpotify/Source/VO/ContextualUpdateVO.cs @@ -0,0 +1,30 @@ +using SpotifyAPI.Web; + +namespace MiniSpotify.Source.VO +{ + public class ContextualUpdateVO + { + public readonly FullTrack LatestSong; + public readonly string LatestSongArtworkURL; + public readonly string PlaybackContext; + public readonly bool IsSongLiked; + public readonly bool IsSongPlaying; + public readonly float latestSongProgress; + + public ContextualUpdateVO( + FullTrack latestSong, + string latestSongArtworkUrl, + string playbackContext, + bool isSongLiked, + bool isSongPlaying, + float latestSongProgress) + { + LatestSong = latestSong; + LatestSongArtworkURL = latestSongArtworkUrl; + PlaybackContext = playbackContext; + IsSongLiked = isSongLiked; + IsSongPlaying = isSongPlaying; + this.latestSongProgress = latestSongProgress; + } + } +} diff --git a/README.md b/README.md index 6ac5426..2c63d0c 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,9 @@ A mini-player/viewer for Spotify, that gives you the information you want but in This is just a little application, designed to pack spotify away neatly so that you can focus on the things that matter whilst still controlling your music. -Written in C#; Implements [Johnny Crazy's Spotify API](https://github.com/JohnnyCrazy/SpotifyAPI-NET); UI via WPF. +Written in C#. +Utilizes [Johnny Crazy's Spotify API](https://github.com/JohnnyCrazy/SpotifyAPI-NET). +UI via WPF. # Releases