diff --git a/.gitignore b/.gitignore index f8c97df..80e7d53 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ bin/ obj/ publish/ *.csproj.user +.vs/* +packages/* \ No newline at end of file diff --git a/Program.cs b/Program.cs index 81e6875..a0146b8 100644 --- a/Program.cs +++ b/Program.cs @@ -7,6 +7,7 @@ using System.ServiceModel; using System.Windows.Forms; using System.Runtime.InteropServices; +using System.IO; namespace Spoti15 { @@ -74,14 +75,18 @@ public static void LoadFonts() if (resKey == null || !resKey.StartsWith("font_")) continue; + var path = Path.Combine(Application.StartupPath, string.Format("lcdfonts\\{0}.ttf", resKey.Substring(5))); + pFonts.AddFontFile(path); + /* byte[] resVal = (byte[])entry.Value; - - IntPtr data = Marshal.AllocCoTaskMem(resVal.Length); - Marshal.Copy(resVal, 0, data, resVal.Length); - - pFonts.AddMemoryFont(data, resVal.Length); - - Marshal.FreeCoTaskMem(data); + unsafe + { + fixed(byte * pFontData = resVal) + { + pFonts.AddMemoryFont((System.IntPtr)pFontData, resVal.Length); + } + } + */ } } @@ -91,6 +96,7 @@ public static void LoadFonts() static void Main(string[] args) { + Application.SetCompatibleTextRenderingDefault(false); using (ChannelFactory spotFactory = new ChannelFactory(new NetNamedPipeBinding(), new EndpointAddress("net.pipe://localhost/Spoti15WCF"))) { try diff --git a/Properties/Resources.Designer.cs b/Properties/Resources.Designer.cs index 64c695b..2bb610a 100644 --- a/Properties/Resources.Designer.cs +++ b/Properties/Resources.Designer.cs @@ -19,7 +19,7 @@ namespace Spoti15.Properties { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Resources { diff --git a/Properties/Settings.Designer.cs b/Properties/Settings.Designer.cs index e6e0afe..53fd719 100644 --- a/Properties/Settings.Designer.cs +++ b/Properties/Settings.Designer.cs @@ -12,7 +12,7 @@ namespace Spoti15.Properties { [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "14.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "15.9.0.0")] internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); diff --git a/README.md b/README.md index 6270f56..bfc1e08 100644 --- a/README.md +++ b/README.md @@ -2,45 +2,77 @@ ### A Spotify Applet for the Logitech G15/G510S --- -![example gif](https://thumbs.gfycat.com/HalfSpanishDutchshepherddog-size_restricted.gif) ---- - The original developer has abandoned the project so I've kept this applet updated to work with the latest version of [SpotifyAPI-NET](https://github.com/JohnnyCrazy/SpotifyAPI-NET). + The original developer has abandoned the project so I've kept this applet updated to work with the latest version of [SpotifyAPI-NET](https://github.com/JohnnyCrazy/SpotifyAPI-NET) and updated it with new features. -### Features -1. Displays: Artist | Track Title | Album Title | Elapsed Time | Total Time | Play/Pause Status -1. Text scrolling -2. Sleek 11px Native font -3. Support for the latest version Spotify +## Features +1. Three display modes +2. Seeking and playback control (Spotify Premium members only) +3. Follow/Unfollow buttons +4. Text scrolling +5. Sleek display +6. Support for the latest version Spotify + +## Installation +1\. Download the Latest release of Spoti15 from [Here.](https://github.com/eezstreet/Spoti15/releases) + +2\. Create a new applet in the Spotify Developer Dashboard. Save the Client ID and Secret ID. + +3\. In the Applet page of the Spotify Developer Dashboard, under 'Edit Settings' enter the following URI to the 'Redirect URIs' whitelist: `http://localhost:4002`. Then click 'Add' and finally 'Save'. + +4\. Run Spoti15. Use the Client ID and Secret ID in the web browser window that pops up. + +5\. (OPTIONAL): Save the Client ID and Secret ID to environment variables (SPOTIFY_CLIENT_ID and SPOTIFY_SECRET_ID) to save them across sessions. + +6\. Ensure that the application is selected in your Logitech Gaming Software + +7\. Ensure that Spotify is Running in the background. + +8\. Done! -### Installation -1\. Download the Latest release of Spoti15 from [Here.](https://github.com/haidarn2/Spoti15/releases) +## Main Display +![Main display](https://i.imgur.com/359JN6p.png) -2\. Ensure that Spotify is Running in the background. -3\. Run Spoti15.exe! +### Controls -![Running spoti15](http://i.imgur.com/hbvBbMS.png) +Pressing the left button (button 0) at any time will either LIKE or UNLIKE the currently playing song. -4\. Done! +![Liked display](https://i.imgur.com/DfqLbRy.png) +Pressing and holding the left-middle button (button 1) while in the main display will show information about the currently playing playlist, album, or artist. -![example gif](http://gifimgs.com/res/1016/57f883e446259953890092.gif) +![Playlist Information](https://i.imgur.com/7r2hntK.png) -### Customization -Currently, two things can be toggled within Spoti15; album name display, and animated lines. +![Artist Information](https://i.imgur.com/8p9AgLf.png) -![controls](http://i.imgur.com/0euaQrH.png) +![Album Information](https://i.imgur.com/lmsVIOx.png) -### Toggling Album Name ON/OFF -![album on](http://i.imgur.com/b187cNt.png) -![album off](http://i.imgur.com/s2nsfy4.png) +## Seek Display +![Seek display](https://i.imgur.com/oBVqnxA.png) + +Pressing and holding the right-middle button (button 2) while in the main display will bring up the Seek Display. + +### Controls +Note that the Seek Display will only function if you have a Spotify Premium membership. + +Pressing the left-middle button (button 1) will seek left, and pressing the right button (button 3) will seek right. Press the left button (button 0) to go to that section of the song. + +## Up Next Display +![Up Next Display](https://i.imgur.com/QIOX18F.png) + +Pressing and holding the right button (button 3) while in the main display will bring up the Up Next Display. + +### Controls +Note that the Up Next Display will only function if you have a Spotify Premium membership. + +Pressing the left button (button 0) will go to the previous song. Pressing the left-middle button will go to the next song. Pressing the right-middle button will pause/play the current track. -### Toggling Animated Lines ON/OFF -![lines on](http://i.imgur.com/RcnTdDe.png) -![lines off](http://i.imgur.com/eElJoBx.png) ### *Changelog:* ``` +v2.0.0 [April 12 2020] + + Updated SpotifyAPI-NET, total rewrite of the software + v1.0.0.16 [April 28 2018] + Updated SpotifyAPI-NET to 2.18.1 + Support for 1.0.77.338.g758ebd78 diff --git a/Spoti15.cs b/Spoti15.cs index f4f3243..89ecd67 100644 --- a/Spoti15.cs +++ b/Spoti15.cs @@ -1,18 +1,18 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Windows.Forms; using System.Drawing; -using SpotifyAPI.Local; -using SpotifyAPI.Local.Enums; -using SpotifyAPI.Local.Models; +using System.Threading.Tasks; +using System.Collections.Generic; +using SpotifyAPI.Web; +using SpotifyAPI.Web.Auth; +using SpotifyAPI.Web.Enums; +using SpotifyAPI.Web.Models; +using System.Globalization; namespace Spoti15 { class Spoti15 { - private SpotifyLocalAPI api; private Exception initExcpt; private LogiLcd lcd; @@ -20,11 +20,37 @@ class Spoti15 private Timer spotTimer; private Timer lcdTimer; private Timer refreshTimer; + private Timer descFlipTimer; + private Timer inputTimer; + private Timer disableLikedSongNotificationTimer; + private Timer hideErrorTimer; private uint scrollStep = 0; + private bool descFlip = false; private bool showAlbum = true; private bool showAnimatedLines = true; + private bool showUpNext = false; + private AuthorizationCodeAuth auth; + private SpotifyWebAPI api; + + private string _clientId = ""; //""; + private string _secretId = ""; //""; + private string refreshToken = ""; + private bool authorized = false; + private bool cachedLikedTrack = false; + private bool likedSongNotification = false; + private bool unlikedSongNotification = false; + private bool showingError = false; + private string errorString = ""; + private FullTrack likedOrUnlikedSong; + private PlaylistTrack upNextPlaylistTrack; + private SimpleTrack upNextAlbumTrack; + private FullTrack upNextAlbumTrackFull; + private FullPlaylist cachedPlaylist; + private FullAlbum cachedAlbum; + private PlaybackContext cachedPlayback; + private FullArtist cachedArtist; public Spoti15() { @@ -32,10 +58,10 @@ public Spoti15() InitSpot(); - lcd = new LogiLcd("Spoti15"); + lcd = new LogiLcd("Spotify"); spotTimer = new Timer(); - spotTimer.Interval = 1000; + spotTimer.Interval = 500; spotTimer.Enabled = true; spotTimer.Tick += OnSpotTimer; @@ -45,48 +71,293 @@ public Spoti15() lcdTimer.Tick += OnLcdTimer; refreshTimer = new Timer(); - refreshTimer.Interval = 5000; + refreshTimer.Interval = 500; refreshTimer.Enabled = true; refreshTimer.Tick += OnRefreshTimer; + descFlipTimer = new Timer(); + descFlipTimer.Interval = 2000; + descFlipTimer.Enabled = true; + descFlipTimer.Tick += OnDescFlip; + + inputTimer = new Timer(); + inputTimer.Interval = 1; + inputTimer.Enabled = true; + inputTimer.Tick += CheckInput; + UpdateSpot(); UpdateLcd(); } + private void OnDescFlip(object source, EventArgs e) + { + descFlip = !descFlip; + } + private void OnSpotTimer(object source, EventArgs e) { UpdateSpot(); } private bool btn0Before = false; + private bool btn1Before = false; private bool btn2Before = false; private bool btn3Before = false; - private void OnLcdTimer(object source, EventArgs e) + private bool seeking = false; + private bool inControlMenu = false; + private double currentSeekPosition = 0.0f; + private static double seekSpeed = 0.003; + + private void CheckInput(object source, EventArgs e) { + seeking = lcd.IsButtonPressed(LogiLcd.LcdButton.Mono2); + + inControlMenu = lcd.IsButtonPressed(LogiLcd.LcdButton.Mono3); + if(inControlMenu && !seeking) + { + bool btn2InCtlNow = lcd.IsButtonPressed(LogiLcd.LcdButton.Mono2); + if(btn2InCtlNow && !btn2Before) + { + var error = api.PausePlayback(); + if(error.HasError()) + { + showingError = true; + errorString = error.Error.Message; + hideErrorTimer = new Timer(); + hideErrorTimer.Enabled = true; + hideErrorTimer.Interval = 3000; + hideErrorTimer.Tick += OnErrorHidden; + } + } + btn2Before = btn2InCtlNow; + + bool btn1InCtlNow = lcd.IsButtonPressed(LogiLcd.LcdButton.Mono1); + if(btn1InCtlNow && !btn1Before) + { + var error = api.SkipPlaybackToNext(); + if (error.HasError()) + { + showingError = true; + errorString = error.Error.Message; + hideErrorTimer = new Timer(); + hideErrorTimer.Enabled = true; + hideErrorTimer.Interval = 3000; + hideErrorTimer.Tick += OnErrorHidden; + } + } + btn1Before = btn1InCtlNow; + + bool btn0InCtlNow = lcd.IsButtonPressed(LogiLcd.LcdButton.Mono0); + if(btn0InCtlNow && !btn0Before) + { + var error = api.SkipPlaybackToPrevious(); + if (error.HasError()) + { + showingError = true; + errorString = error.Error.Message; + hideErrorTimer = new Timer(); + hideErrorTimer.Enabled = true; + hideErrorTimer.Interval = 3000; + hideErrorTimer.Tick += OnErrorHidden; + } + } + btn0Before = btn0InCtlNow; + return; + } + + if (seeking && !btn2Before && cachedPlayback != null && cachedPlayback.CurrentlyPlayingType != TrackType.Ad) + { + // set seek position to current + currentSeekPosition = (double)cachedPlayback.ProgressMs / cachedPlayback.Item.DurationMs; + } + + btn2Before = seeking; + + if(seeking) + { + bool btn0InSeekNow = lcd.IsButtonPressed(LogiLcd.LcdButton.Mono0); + if(btn0InSeekNow && !btn0Before) + { + int toMs = (int)(currentSeekPosition * cachedPlayback.Item.DurationMs); + var error = api.SeekPlayback(toMs); + if (error.HasError()) + { + showingError = true; + errorString = error.Error.Message; + hideErrorTimer = new Timer(); + hideErrorTimer.Enabled = true; + hideErrorTimer.Interval = 3000; + hideErrorTimer.Tick += OnErrorHidden; + } + seeking = false; + btn0Before = true; + return; + } + btn0Before = btn0InSeekNow; + + if(lcd.IsButtonPressed(LogiLcd.LcdButton.Mono1)) + { + currentSeekPosition -= seekSpeed; + } + if(lcd.IsButtonPressed(LogiLcd.LcdButton.Mono3)) + { + currentSeekPosition += seekSpeed; + } + + if(currentSeekPosition > 1.0) + { + currentSeekPosition = 1.0; + } + else if(currentSeekPosition < 0.0) + { + currentSeekPosition = 0.0; + } + + return; + } + bool btn0Now = lcd.IsButtonPressed(LogiLcd.LcdButton.Mono0); - if (btn0Now && !btn0Before) - InitSpot(); + if(btn0Now && !btn0Before) + { + var thisItem = cachedPlayback; + if(thisItem == null || thisItem.Item == null) + { + return; + } + + if(likedSongNotification || unlikedSongNotification) + { + likedSongNotification = unlikedSongNotification = false; + btn0Before = btn0Now; + disableLikedSongNotificationTimer.Enabled = false; + return; + } + + var ListedItem = new List(1); + + likedOrUnlikedSong = thisItem.Item; + ListedItem.Add(likedOrUnlikedSong.Id); + if(cachedLikedTrack) + { + api.RemoveSavedTracks(ListedItem); + likedSongNotification = false; + unlikedSongNotification = true; + } + else + { + api.SaveTrack(likedOrUnlikedSong.Id); + likedSongNotification = true; + unlikedSongNotification = false; + } + + disableLikedSongNotificationTimer = new Timer(); + disableLikedSongNotificationTimer.Enabled = true; + disableLikedSongNotificationTimer.Interval = 5000; + disableLikedSongNotificationTimer.Tick += OnLikedSongNotificationFinished; + } + btn0Before = btn0Now; + bool btn1Now = lcd.IsButtonPressed(LogiLcd.LcdButton.Mono1); + if(btn1Now) + { + var playback = cachedPlayback; + if(playback == null) + { + return; + } + + if(playback.CurrentlyPlayingType == TrackType.Ad) + { // doing calls later down here is bad + return; + } + + if(playback.Context == null) + { + + } + else if(playback.Context.Type == "playlist") + { + var playlist = cachedPlaylist; + if (playlist == null) + { + return; + } + + upNextPlaylistTrack = null; + for (int i = 0; i < playlist.Tracks.Items.Count; i++) + { + if (playlist.Tracks.Items[i].Track.Uri == playback.Item.Uri) + { + // next track is it + if (i == playlist.Tracks.Items.Count - 1) + { + upNextPlaylistTrack = playlist.Tracks.Items[0]; + } + else + { + upNextPlaylistTrack = playlist.Tracks.Items[i + 1]; + } + break; + } + } + if (upNextPlaylistTrack == null) + { + return; + } + } + else if(playback.Context.Type == "album") + { + upNextPlaylistTrack = null; + + var newAlbum = cachedAlbum; + if(newAlbum == null) + { + return; + } + + for(int i = 0; i < newAlbum.Tracks.Items.Count; i++) + { + if(playback.Item.Uri == newAlbum.Tracks.Items[i].Uri) + { + if(i == newAlbum.Tracks.Items.Count - 1) + { + upNextAlbumTrack = null; + } + else + { + upNextAlbumTrack = newAlbum.Tracks.Items[i + 1]; + } + } + } + } + + } + + showUpNext = btn1Now; + } + + private void OnErrorHidden(object source, EventArgs e) + { + showingError = false; + hideErrorTimer.Enabled = false; + } + + private void OnLikedSongNotificationFinished(object source, EventArgs e) + { + likedSongNotification = unlikedSongNotification = false; + disableLikedSongNotificationTimer.Enabled = false; + } + + private void OnLcdTimer(object source, EventArgs e) + { UpdateLcd(); scrollStep += 1; - - // toggle between "ARTIST - ALBUM" and "ALBUM" on line 1 - bool btn3Now = lcd.IsButtonPressed(LogiLcd.LcdButton.Mono3); - if (btn3Now && !btn3Before) - showAlbum = !showAlbum; - btn3Before = btn3Now; - - // toggle animated lines within progress bar - bool btn2Now = lcd.IsButtonPressed(LogiLcd.LcdButton.Mono2); - if (btn2Now && !btn2Before) - showAnimatedLines = !showAnimatedLines; - btn2Before = btn2Now; } private void OnRefreshTimer(object source, EventArgs e) { - InitSpot(); + //InitSpot(); } public void Dispose() @@ -106,17 +377,87 @@ public void Dispose() refreshTimer = null; initExcpt = null; + auth.Stop(0); + } + + private void UpdateAccessToken(Token token) + { + api = new SpotifyWebAPI + { + AccessToken = token.AccessToken, + TokenType = token.TokenType + }; + authorized = true; + refreshToken = token.RefreshToken; + Environment.SetEnvironmentVariable("SPOTIFY_REFRESH_TOKEN", refreshToken); + } + + private async void OnAuthReceived(object sender, AuthorizationCode payload) + { + try + { + auth.Stop(); + + var token = await auth.ExchangeCode(payload.Code); + UpdateAccessToken(token); + } + catch (Exception e) + { + initExcpt = e; + } + } + + private void Authorize() + { + var url = "http://localhost:4002"; + + System.Diagnostics.Debug.Write("Re-authorize\r\n"); + auth?.Stop(); + auth = new AuthorizationCodeAuth(_clientId, _secretId, url, url, Scope.UserReadCurrentlyPlaying | + Scope.UserReadPlaybackState | Scope.UserLibraryRead | Scope.UserLibraryModify | Scope.UserModifyPlaybackState); + + if (string.IsNullOrEmpty(refreshToken)) + { + auth.Start(); + auth.AuthReceived += OnAuthReceived; + auth.OpenBrowser(); + } + else + { + RefreshAccessToken(); + } + } + + private async Task RefreshAccessToken() + { + try + { + var token = await auth.RefreshToken(refreshToken); + if(token.HasError()) + { + refreshToken = null; + System.Diagnostics.Debug.Write("Bad token\r\n"); + Authorize(); + return; + } + UpdateAccessToken(token); + } + catch(Exception e) + { + initExcpt = e; + } } private void InitSpot() { try { - if (api == null) - api = new SpotifyLocalAPI(); - if (!api.Connect()) - throw new Exception ("Is Spotify Even Running?"); - initExcpt = null; + _clientId = string.IsNullOrEmpty(_clientId) ? Environment.GetEnvironmentVariable("SPOTIFY_CLIENT_ID") : _clientId; + _secretId = string.IsNullOrEmpty(_secretId) ? Environment.GetEnvironmentVariable("SPOTIFY_SECRET_ID") : _secretId; + refreshToken = string.IsNullOrEmpty(refreshToken) ? Environment.GetEnvironmentVariable("SPOTIFY_REFRESH_TOKEN") : refreshToken; + + System.Diagnostics.Debug.Write("InitSpot()\r\n"); + Authorize(); } catch (Exception e) { @@ -126,13 +467,128 @@ private void InitSpot() public void UpdateSpot() { - if(initExcpt != null) return; + + if (api == null) + return; + + try + { + initExcpt = null; + + var retrievedPlayback = api.GetPlayback(); + if (retrievedPlayback != null) + { + cachedPlayback = retrievedPlayback; + if (cachedPlayback.HasError() && cachedPlayback.Error.Message == "The access token expired" && authorized) + { + authorized = false; + Authorize(); + } + } + + if (cachedPlayback != null && cachedPlayback.Item != null) + { + var ListedItem = new List(1); + ListedItem.Add(cachedPlayback.Item.Id); + var response = api.CheckSavedTracks(ListedItem); + if (response.List == null) + { + if (response.HasError() && response.Error.Message == "The access token expired" && authorized) + { + authorized = false; + Authorize(); + } + } + else + { + cachedLikedTrack = response.List[0]; + } + + if (cachedPlayback.Context == null) + { + + } + else if (cachedPlayback.Context.Type == "playlist") + { + var split = cachedPlayback.Context.ExternalUrls["spotify"].Split('/'); + var newPlaylist = api.GetPlaylist(split[4]); + if (newPlaylist != null && !newPlaylist.HasError()) + { + cachedPlaylist = newPlaylist; + } + } + else if (cachedPlayback.Context.Type == "album") + { + var newAlbum = api.GetAlbum(cachedPlayback.Item.Album.Id); + if (newAlbum != null && !newAlbum.HasError()) + { + cachedAlbum = newAlbum; + } + + if (upNextAlbumTrack != null) + { + if (cachedPlayback.Item.TrackNumber + 1 > newAlbum.Tracks.Items.Count) + { // just go back to 0 i guess? + upNextAlbumTrackFull = api.GetTrack(newAlbum.Tracks.Items[0].Id); + } + else + { + upNextAlbumTrackFull = api.GetTrack(newAlbum.Tracks.Items[cachedPlayback.Item.TrackNumber + 1].Id); + } + } + else + { + upNextAlbumTrackFull = null; + } + } + else if (cachedPlayback.Context.Type == "artist") + { + var split = cachedPlayback.Context.ExternalUrls["spotify"].Split('/'); + var region = RegionInfo.CurrentRegion; + + upNextAlbumTrackFull = null; + + var artistTracks = api.GetArtistsTopTracks(split[4], region.TwoLetterISORegionName); + var thisArtist = api.GetArtist(split[4]); + + if (thisArtist != null) + { + cachedArtist = thisArtist; + } + + for (int i = 0; i < artistTracks.Tracks.Count; i++) + { + if (artistTracks.Tracks[i].Id == cachedPlayback.Item.Id) + { + if (i == artistTracks.Tracks.Count - 1) + { + upNextAlbumTrackFull = artistTracks.Tracks[0]; + break; + } + else + { + upNextAlbumTrackFull = artistTracks.Tracks[i + 1]; + break; + } + } + } + } + + } + } + catch(AggregateException /*e*/) + { + //initExcpt = e; // This is common if the task was dropped + } + } private Bitmap bgBitmap = new Bitmap(LogiLcd.MonoWidth, LogiLcd.MonoHeight); - private Font mainFont = new Font(Program.GetFontFamily("11pxbus"), 11, GraphicsUnit.Pixel); + private Font mainFont = new Font(Program.GetFontFamily("6pxbus"), 6, GraphicsUnit.Pixel); + private Font iconFont = new Font(Program.GetFontFamily("5px2bus"), 5, GraphicsUnit.Pixel); + private Font bigFont = new Font(Program.GetFontFamily("11px3bus"), 11, GraphicsUnit.Pixel); private Color bgColor = Color.Black; private Color fgColor = Color.White; private Brush bgBrush = Brushes.Black; @@ -149,15 +605,23 @@ private void SetupGraphics(Graphics g) g.Clear(bgColor); } - private void DrawText(Graphics g, int line, string text, Font fnt, int offset = 0) + private void DrawText(Graphics g, int line, string text, Font fnt, int offset = 0, int vertOffset = 0) { int x = offset; - int y = line * 10; + int y = (line * 6) + vertOffset; if (line == 0) y -= 1; // offset first line 3 pixels up TextRenderer.DrawText(g, text, fnt, new Point(x, y), fgColor, TextFormatFlags.NoPrefix); } + private void DrawTextWithinBounds(Graphics g, int line, string text, Font fnt, int x, int w) + { + int y = (line * 6); + if (line == 0) + y -= 1; + TextRenderer.DrawText(g, text, fnt, new Rectangle(x, y, w, 6), Color.White, TextFormatFlags.NoPrefix); + } + private void DrawTextScroll(Graphics g, int line, string text, Font fnt, bool center = true) { Size textSize = TextRenderer.MeasureText(text, fnt); @@ -208,6 +672,150 @@ private void DoRender() lcd.Update(); } + private string GetStringFromArtists(SimpleArtist[] artists) + { + string returnValue = ""; + + for (int i = 0; i < artists.Length; i++) + { + returnValue = string.Concat(returnValue, artists[i].Name); + if (i != artists.Length - 1) + { + returnValue = string.Concat(returnValue, ", "); + } + } + + return returnValue; + } + + private string GetStringFromGenres(string[] genres) + { + string returnValue = ""; + + for (int i = 0; i < genres.Length; i++) + { + returnValue = string.Concat(returnValue, genres[i]); + if (i != genres.Length - 1) + { + returnValue = string.Concat(returnValue, ", "); + } + } + + return returnValue; + } + + private void DrawPlaybackStatus(Graphics g, bool drawTime) + { + var playback = cachedPlayback; + + if(playback.HasError()) + { // if there's an error, don't bother + DrawTextScroll(g, 6, playback.Error.Message); + return; + } + + int len = playback.Item.DurationMs; + int pos = playback.ProgressMs; + double perc = pos / (double)len; + + // Draw following status + DrawText(g, 6, cachedLikedTrack ? "e" : "f", iconFont, 0, 1); + + // Draw progress bar + g.DrawRectangle(Pens.White, 8, LogiLcd.MonoHeight - 6, (LogiLcd.MonoWidth - 24), 4); + g.FillRectangle(Brushes.White, 8, LogiLcd.MonoHeight - 6, (int)((LogiLcd.MonoWidth - 24) * perc), 4); + + if (playback.IsPlaying) + { + g.FillPolygon(Brushes.White, new Point[] { + new Point((LogiLcd.MonoWidth - 14), LogiLcd.MonoHeight - 7), + new Point((LogiLcd.MonoWidth - 14), LogiLcd.MonoHeight - 1), + new Point((LogiLcd.MonoWidth - 9), LogiLcd.MonoHeight - 4) + }); + + if (lineTrack > 12) + lineTrack = 6; + else + lineTrack++; + for (int x = lineTrack; x < LogiLcd.MonoWidth - 22; x += 8) + g.DrawLine(Pens.Black, new Point(x, LogiLcd.MonoHeight - 4), new Point(x + 2, LogiLcd.MonoHeight - 4)); + } + else + { + g.FillRectangle(Brushes.White, new Rectangle((LogiLcd.MonoWidth - 10), LogiLcd.MonoHeight - 6, 2, 5)); + g.FillRectangle(Brushes.White, new Rectangle((LogiLcd.MonoWidth - 13), LogiLcd.MonoHeight - 6, 2, 5)); + } + + if (playback.ShuffleState) + { + DrawText(g, 6, "S", mainFont, LogiLcd.MonoWidth - 7, -1); + } + else if (playback.RepeatState == RepeatState.Context) + { + DrawText(g, 6, "h", iconFont, LogiLcd.MonoWidth - 7); + } + else if (playback.RepeatState == RepeatState.Track) + { + DrawText(g, 6, "g", iconFont, LogiLcd.MonoWidth - 7); + } + + if(drawTime) + { + DrawTextScroll(g, 5, String.Format("{0}:{1:D2} / {2}:{3:D2}", pos / 60000, (pos % 60000) / 1000, len / 60000, (len % 60000) / 1000)); + } + } + + private void DrawPlaylistStatus(Graphics g) + { + var playback = cachedPlayback; + + if (playback.Context == null) + { + + } + else if (playback.Context.Type == "album") + { + if (descFlip) + { + DrawText(g, 0, "Playing Album"); + } + else + { + DrawTextWithinBounds(g, 0, playback.Item.Album.Name, mainFont, 0, 110); + } + } + else if (playback.Context.Type == "playlist") + { + var playlist = cachedPlaylist; + if (playlist != null) + { + if (descFlip) + { + DrawTextWithinBounds(g, 0, playlist.Type, mainFont, 0, 110); + } + else + { + DrawTextWithinBounds(g, 0, playlist.Name, mainFont, 0, 110); + } + } + } + else if(playback.Context.Type == "artist") + { + if(descFlip || cachedArtist == null) + { + DrawText(g, 0, "Playing Artist"); + } + else if(cachedArtist != null) + { + DrawTextWithinBounds(g, 0, cachedArtist.Name, mainFont, 0, 110); + } + } + else + { + DrawText(g, 3, "Unknown"); + } + } + //private Byte[] emptyBg = new Byte[LogiLcd.MonoWidth * LogiLcd.MonoHeight]; private int lineTrack = 4; public void UpdateLcd() @@ -232,47 +840,263 @@ public void UpdateLcd() try { - StatusResponse status = api.GetStatus(); - int len = status.Track.Length; - int pos = (int)status.PlayingPosition; - double perc = status.PlayingPosition / status.Track.Length; - - String lineZero = status.Track.ArtistResource.Name; - if (showAlbum) - lineZero += " - " + status.Track.AlbumResource.Name; - DrawTextScroll(g, 0, lineZero); - DrawTextScroll(g, 1, status.Track.TrackResource.Name); - DrawTextScroll(g, 3, String.Format("{0}:{1:D2} / {2}:{3:D2}", pos / 60, pos % 60, len / 60, len % 60)); - - // draw progress bar - g.DrawRectangle(Pens.White, 3, 24, LogiLcd.MonoWidth - 6, 4); - g.FillRectangle(Brushes.White, 3, 24, (int)((LogiLcd.MonoWidth - 6) * perc), 4); - - // draw stylistic pattern lines within progress bar - if (showAnimatedLines) + if(api == null) + { + // TODO: draw spotify logo + g.Clear(bgColor); + DrawTextScroll(g, 2, "SPOTIFY"); + } + else if(showingError) + { + g.Clear(bgColor); + + DrawTextScroll(g, 1, "ERROR", bigFont); + DrawTextScroll(g, 3, errorString); + + DrawPlaybackStatus(g, true); + DrawPlaylistStatus(g); + } + else if(likedSongNotification || unlikedSongNotification) + { + g.Clear(bgColor); + + if (likedSongNotification) + { + DrawTextScroll(g, 1, "LIKED SONG!", bigFont); + } + else if (unlikedSongNotification) + { + DrawTextScroll(g, 1, "UNLIKED SONG!", bigFont); + } + + DrawTextScroll(g, 3, likedOrUnlikedSong.Name); + DrawTextScroll(g, 4, GetStringFromArtists(likedOrUnlikedSong.Artists.ToArray())); + DrawTextScroll(g, 5, likedOrUnlikedSong.Album.Name); + + DrawPlaybackStatus(g, false); + } + else if(seeking && cachedPlayback != null && cachedPlayback.Item != null) { - if (lineTrack > 8) - lineTrack = 4; + g.Clear(bgColor); + + DrawTextScroll(g, 1, "SEEKING"); + + var posInMs = (int)(cachedPlayback.Item.DurationMs * currentSeekPosition); + DrawTextScroll(g, 3, string.Format("{0:D2}:{1:D2}", posInMs / 60000, (posInMs % 60000) / 1000)); + + // Draw progress bar + g.DrawRectangle(Pens.White, 8, LogiLcd.MonoHeight - 16, (LogiLcd.MonoWidth - 24), 6); + g.FillRectangle(Brushes.White, 8, LogiLcd.MonoHeight - 16, (int)((LogiLcd.MonoWidth - 24) * currentSeekPosition), 6); + + g.FillPolygon(Brushes.White, new Point[] { + new Point((LogiLcd.MonoWidth - 19), LogiLcd.MonoHeight - 7), + new Point((LogiLcd.MonoWidth - 19), LogiLcd.MonoHeight - 1), + new Point((LogiLcd.MonoWidth - 14), LogiLcd.MonoHeight - 4) + }); + + g.FillPolygon(Brushes.White, new Point[] { + new Point(60, LogiLcd.MonoHeight - 7), + new Point(60, LogiLcd.MonoHeight - 1), + new Point(55, LogiLcd.MonoHeight - 4) + }); + + DrawText(g, 6, "OK", iconFont, 8); + DrawPlaylistStatus(g); + } + else if(inControlMenu && cachedPlayback != null) + { + g.Clear(bgColor); + + g.FillPolygon(Brushes.White, new Point[] { + new Point((LogiLcd.MonoWidth - 100), LogiLcd.MonoHeight - 7), + new Point((LogiLcd.MonoWidth - 100), LogiLcd.MonoHeight - 1), + new Point((LogiLcd.MonoWidth - 95), LogiLcd.MonoHeight - 4) + }); + + g.FillPolygon(Brushes.White, new Point[] { + new Point((LogiLcd.MonoWidth - 106), LogiLcd.MonoHeight - 7), + new Point((LogiLcd.MonoWidth - 106), LogiLcd.MonoHeight - 1), + new Point((LogiLcd.MonoWidth - 101), LogiLcd.MonoHeight - 4) + }); + + g.FillPolygon(Brushes.White, new Point[] { + new Point(17, LogiLcd.MonoHeight - 7), + new Point(17, LogiLcd.MonoHeight - 1), + new Point(12, LogiLcd.MonoHeight - 4) + }); + + g.FillPolygon(Brushes.White, new Point[] { + new Point(23, LogiLcd.MonoHeight - 7), + new Point(23, LogiLcd.MonoHeight - 1), + new Point(18, LogiLcd.MonoHeight - 4) + }); + + if (cachedPlayback.IsPlaying) + { + g.FillRectangle(Brushes.White, new Rectangle((LogiLcd.MonoWidth - 58), LogiLcd.MonoHeight - 6, 2, 5)); + g.FillRectangle(Brushes.White, new Rectangle((LogiLcd.MonoWidth - 61), LogiLcd.MonoHeight - 6, 2, 5)); + } else - lineTrack++; - for (int x = lineTrack; x < LogiLcd.MonoWidth - 6; x += 6) - g.DrawLine(Pens.Black, new Point(x, 26), new Point(x + 2, 26)); + { + g.FillPolygon(Brushes.White, new Point[] { + new Point((LogiLcd.MonoWidth - 64), LogiLcd.MonoHeight - 7), + new Point((LogiLcd.MonoWidth - 64), LogiLcd.MonoHeight - 1), + new Point((LogiLcd.MonoWidth - 59), LogiLcd.MonoHeight - 4) + }); + } + + DrawTextScroll(g, 1, "UP NEXT", bigFont); + + if(cachedPlayback.Context == null) + { + + } + else if (cachedPlayback.Context.Type == "playlist" && upNextPlaylistTrack != null) + { + DrawTextScroll(g, 3, upNextPlaylistTrack.Track.Name); + DrawTextScroll(g, 4, GetStringFromArtists(upNextPlaylistTrack.Track.Artists.ToArray())); + DrawTextScroll(g, 5, upNextPlaylistTrack.Track.Album.Name); + + DrawText(g, 0, string.Format("e {0}%", upNextPlaylistTrack.Track.Popularity), iconFont, 5, 5); + } + else if (cachedPlayback.Context.Type == "album" && upNextAlbumTrack != null) + { + DrawTextScroll(g, 3, upNextAlbumTrack.Name); + DrawTextScroll(g, 4, string.Format("Track {0}", upNextAlbumTrack.TrackNumber)); + + if (upNextAlbumTrackFull != null) + { + DrawText(g, 0, string.Format("e {0}%", upNextAlbumTrackFull.Popularity), iconFont, 5, 5); + } + } + else if(cachedPlayback.Context.Type == "artist" && upNextAlbumTrackFull != null) + { + DrawTextScroll(g, 3, upNextAlbumTrackFull.Name); + DrawTextScroll(g, 4, upNextAlbumTrackFull.Album.Name); + DrawText(g, 0, string.Format("e {0}%", upNextAlbumTrackFull.Popularity), iconFont, 5, 5); + } + else + { + DrawTextScroll(g, 3, "Unknown"); + } } - - if (status.Playing) + else if(showUpNext) { - g.FillPolygon(Brushes.White, new Point[] { new Point(3, 42), new Point(3, 32), new Point(8, 37) }); + g.Clear(bgColor); + + if(cachedPlayback.Context == null) + { + + } + else if(cachedPlayback.Context.Type == "playlist") + { + DrawTextScroll(g, 1, "PLAYLIST", bigFont); + DrawTextScroll(g, 3, cachedPlaylist.Name); + DrawTextScroll(g, 4, cachedPlaylist.Description); + + DrawText(g, 0, string.Format("e {0}", cachedPlaylist.Followers.Total), iconFont, 5, 5); + + DrawPlaybackStatus(g, true); + } + else if(cachedPlayback.Context.Type == "album") + { + DrawTextScroll(g, 1, "ALBUM", bigFont); + DrawTextScroll(g, 3, cachedPlayback.Item.Album.Name); + DrawTextScroll(g, 4, GetStringFromArtists(cachedPlayback.Item.Artists.ToArray())); + DrawTextScroll(g, 5, string.Format("Released {0}", cachedPlayback.Item.Album.ReleaseDate)); + + if(cachedAlbum != null) + { + string genres = GetStringFromGenres(cachedAlbum.Genres.ToArray()); + if(genres == "") + { + DrawText(g, 0, string.Format("e {0}", cachedAlbum.Popularity), iconFont, 5, 5); + } + else + { + DrawTextScroll(g, 6, string.Format("{0} - {1} followers", GetStringFromGenres(cachedAlbum.Genres.ToArray()), cachedAlbum.Popularity)); + } + } + + DrawPlaybackStatus(g, false); + } + else if(cachedPlayback.Context.Type == "artist" && cachedArtist != null) + { + DrawTextScroll(g, 1, "ARTIST", bigFont); + DrawTextScroll(g, 3, cachedArtist.Name); + DrawTextScroll(g, 4, GetStringFromGenres(cachedArtist.Genres.ToArray())); + + DrawText(g, 0, string.Format("e {0}", cachedArtist.Followers.Total), iconFont, 5, 5); + + DrawPlaybackStatus(g, true); + } + else + { + DrawTextScroll(g, 1, cachedPlayback.Context.Type); + } } else { - g.FillRectangle(Brushes.White, new Rectangle(3, 34, 2, 7)); - g.FillRectangle(Brushes.White, new Rectangle(6, 34, 2, 7)); + + var playback = cachedPlayback; + if (playback == null || playback.Item == null) + { + if (playback == null) + { + g.Clear(bgColor); + DrawTextScroll(g, 1, "ERROR"); + DrawTextScroll(g, 2, "SPOTIFY PLAYBACK NOT DETECTED"); + DoRender(); + return; + } + else if (playback.CurrentlyPlayingType == TrackType.Ad) + { + g.Clear(bgColor); + DrawTextScroll(g, 2, "Advertisement"); + DoRender(); + return; + } + else if (playback.HasError()) + { + g.Clear(bgColor); + DrawTextScroll(g, 1, "ERROR"); + DrawTextScroll(g, 2, playback.Error.Message); + } + else + { + return; + } + } + + + // Draw number of followers + if(cachedPlayback != null && cachedPlayback.Item != null) + { + DrawText(g, 5, string.Format("e {0}%", cachedPlayback.Item.Popularity), iconFont); + } + + if(playback != null && playback.Item != null) + { + DrawTextScroll(g, 2, GetStringFromArtists(playback.Item.Artists.ToArray())); + DrawTextScroll(g, 1, playback.Item.Name); + DrawTextScroll(g, 3, playback.Item.Album.Name); + } + + DrawPlaybackStatus(g, true); + DrawPlaylistStatus(g); } + + string currentTime = DateTime.Now.ToString("h:mm:ss tt"); + Size textSize = TextRenderer.MeasureText(currentTime, mainFont); + DrawText(g, 0, currentTime, LogiLcd.MonoWidth - textSize.Width); } - catch (NullReferenceException) + catch (Exception e) { g.Clear(bgColor); - DrawTextScroll(g, 1, "No track information available", false); + var split = e.StackTrace.Split('\\'); + DrawTextScroll(g, 1, string.Format("Exception: {0}", e.GetType())); + DrawTextScroll(g, 2, split[split.Length - 1]); + //DrawTextScroll(g, 1, "No track information available", false); } } diff --git a/Spoti15.csproj b/Spoti15.csproj index f9b8011..8b11ce9 100644 --- a/Spoti15.csproj +++ b/Spoti15.csproj @@ -1,5 +1,5 @@  - + Debug @@ -9,7 +9,7 @@ Properties Spoti15 Spoti15 - v4.5 + v4.6.1 512 @@ -45,6 +45,7 @@ prompt MinimumRecommendedRules.ruleset false + true bin\x86\Release\ @@ -73,13 +74,14 @@ false - - False - SpotifyLocalAPI\Newtonsoft.Json.dll + + packages\Newtonsoft.Json.12.0.3\lib\net45\Newtonsoft.Json.dll - - False - SpotifyLocalAPI\SpotifyAPI.dll + + packages\SpotifyAPI.Web.5.1.0\lib\netstandard2.0\SpotifyAPI.Web.dll + + + packages\SpotifyAPI.Web.Auth.5.1.0\lib\netstandard2.0\SpotifyAPI.Web.Auth.dll @@ -91,6 +93,12 @@ + + packages\EmbedIO.2.9.2\lib\netstandard2.0\Unosquare.Labs.EmbedIO.dll + + + packages\Unosquare.Swan.Lite.1.3.1\lib\net461\Unosquare.Swan.Lite.dll + @@ -111,24 +119,61 @@ - - - - - - - - - - - - - - - - - - + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + SettingsSingleFileGenerator Settings.Designer.cs diff --git a/app.config b/app.config index 88a4e17..b21ff98 100644 --- a/app.config +++ b/app.config @@ -1,15 +1,23 @@ - + -
+
- + False + + + + + + + + diff --git a/packages.config b/packages.config new file mode 100644 index 0000000..5f5ce7e --- /dev/null +++ b/packages.config @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file