├── .gitignore ├── Debugger ├── App.config ├── Debugger.csproj ├── Program.cs └── Properties │ └── AssemblyInfo.cs ├── DllExporter └── DllExporter.exe ├── README.md ├── SpotifyPlugin.sln └── SpotifyPlugin ├── AlbumArt.cs ├── FodyWeavers.xml ├── Out.cs ├── Parent.cs ├── Properties ├── AssemblyInfo.cs ├── Settings.Designer.cs └── Settings.settings ├── RainmeterAPI.cs ├── SpotifyPlugin.cs ├── SpotifyPlugin.csproj ├── app.config └── packages.config /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io 2 | 3 | ### VisualStudio ### 4 | ## Ignore Visual Studio temporary files, build results, and 5 | ## files generated by popular Visual Studio add-ons. 6 | 7 | # User-specific files 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | build/ 24 | bld/ 25 | [Bb]in/ 26 | [Oo]bj/ 27 | 28 | # Visual Studo 2015 cache/options directory 29 | .vs/ 30 | 31 | # MSTest test Results 32 | [Tt]est[Rr]esult*/ 33 | [Bb]uild[Ll]og.* 34 | 35 | # NUNIT 36 | *.VisualState.xml 37 | TestResult.xml 38 | 39 | # Build Results of an ATL Project 40 | [Dd]ebugPS/ 41 | [Rr]eleasePS/ 42 | dlldata.c 43 | 44 | *_i.c 45 | *_p.c 46 | *_i.h 47 | *.ilk 48 | *.meta 49 | *.obj 50 | *.pch 51 | *.pdb 52 | *.pgc 53 | *.pgd 54 | *.rsp 55 | *.sbr 56 | *.tlb 57 | *.tli 58 | *.tlh 59 | *.tmp 60 | *.tmp_proj 61 | *.log 62 | *.vspscc 63 | *.vssscc 64 | .builds 65 | *.pidb 66 | *.svclog 67 | *.scc 68 | 69 | # Chutzpah Test files 70 | _Chutzpah* 71 | 72 | # Visual C++ cache files 73 | ipch/ 74 | *.aps 75 | *.ncb 76 | *.opensdf 77 | *.sdf 78 | *.cachefile 79 | 80 | # Visual Studio profiler 81 | *.psess 82 | *.vsp 83 | *.vspx 84 | 85 | # TFS 2012 Local Workspace 86 | $tf/ 87 | 88 | # Guidance Automation Toolkit 89 | *.gpState 90 | 91 | # ReSharper is a .NET coding add-in 92 | _ReSharper*/ 93 | *.[Rr]e[Ss]harper 94 | *.DotSettings.user 95 | 96 | # JustCode is a .NET coding addin-in 97 | .JustCode 98 | 99 | # TeamCity is a build add-in 100 | _TeamCity* 101 | 102 | # DotCover is a Code Coverage Tool 103 | *.dotCover 104 | 105 | # NCrunch 106 | _NCrunch_* 107 | .*crunch*.local.xml 108 | 109 | # MightyMoose 110 | *.mm.* 111 | AutoTest.Net/ 112 | 113 | # Web workbench (sass) 114 | .sass-cache/ 115 | 116 | # Installshield output folder 117 | [Ee]xpress/ 118 | 119 | # DocProject is a documentation generator add-in 120 | DocProject/buildhelp/ 121 | DocProject/Help/*.HxT 122 | DocProject/Help/*.HxC 123 | DocProject/Help/*.hhc 124 | DocProject/Help/*.hhk 125 | DocProject/Help/*.hhp 126 | DocProject/Help/Html2 127 | DocProject/Help/html 128 | 129 | # Click-Once directory 130 | publish/ 131 | 132 | # Publish Web Output 133 | *.[Pp]ublish.xml 134 | *.azurePubxml 135 | # TODO: Comment the next line if you want to checkin your web deploy settings 136 | # but database connection strings (with potential passwords) will be unencrypted 137 | *.pubxml 138 | *.publishproj 139 | 140 | # NuGet Packages 141 | *.nupkg 142 | # The packages folder can be ignored because of Package Restore 143 | **/packages/* 144 | # except build/, which is used as an MSBuild target. 145 | !**/packages/build/ 146 | # Uncomment if necessary however generally it will be regenerated when needed 147 | #!**/packages/repositories.config 148 | 149 | # Windows Azure Build Output 150 | csx/ 151 | *.build.csdef 152 | 153 | # Windows Store app package directory 154 | AppPackages/ 155 | 156 | # Others 157 | *.[Cc]ache 158 | ClientBin/ 159 | [Ss]tyle[Cc]op.* 160 | ~$* 161 | *~ 162 | *.dbmdl 163 | *.dbproj.schemaview 164 | *.pfx 165 | *.publishsettings 166 | node_modules/ 167 | bower_components/ 168 | 169 | # RIA/Silverlight projects 170 | Generated_Code/ 171 | 172 | # Backup & report files from converting an old project file 173 | # to a newer Visual Studio version. Backup files are not needed, 174 | # because we have git ;-) 175 | _UpgradeReport_Files/ 176 | Backup*/ 177 | UpgradeLog*.XML 178 | UpgradeLog*.htm 179 | 180 | # SQL Server files 181 | *.mdf 182 | *.ldf 183 | 184 | # Business Intelligence projects 185 | *.rdl.data 186 | *.bim.layout 187 | *.bim_*.settings 188 | 189 | # Microsoft Fakes 190 | FakesAssemblies/ 191 | 192 | # Node.js Tools for Visual Studio 193 | .ntvs_analysis.dat 194 | 195 | # Visual Studio 6 build log 196 | *.plg 197 | 198 | # Visual Studio 6 workspace options file 199 | *.opt 200 | /SpotifyPlugin_backup 201 | /SpotifyWebApi 202 | /SpotifyPlugin 2 203 | SpotifyPlugin/APIKeys.cs 204 | -------------------------------------------------------------------------------- /Debugger/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Debugger/Debugger.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {C48DF75B-BB61-4EA9-A9A0-472031639BBD} 8 | Exe 9 | Properties 10 | Debugger 11 | Debugger 12 | v4.5 13 | 512 14 | 15 | 16 | 17 | true 18 | bin\x86\Debug\ 19 | DEBUG;TRACE 20 | full 21 | x86 22 | prompt 23 | MinimumRecommendedRules.ruleset 24 | true 25 | 26 | 27 | bin\x86\Release\ 28 | TRACE 29 | true 30 | pdbonly 31 | x86 32 | prompt 33 | MinimumRecommendedRules.ruleset 34 | true 35 | 36 | 37 | true 38 | bin\x64\Debug\ 39 | DEBUG;TRACE 40 | full 41 | x64 42 | prompt 43 | MinimumRecommendedRules.ruleset 44 | true 45 | 46 | 47 | bin\x64\Release\ 48 | TRACE 49 | true 50 | pdbonly 51 | x64 52 | prompt 53 | MinimumRecommendedRules.ruleset 54 | true 55 | 56 | 57 | Debugger.Program 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | {1743630a-663e-4c0b-8142-da51ab941550} 74 | SpotifyPlugin 75 | 76 | 77 | 78 | 85 | -------------------------------------------------------------------------------- /Debugger/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using SpotifyPlugin; 3 | using System.Threading; 4 | 5 | namespace Debugger 6 | { 7 | class Program 8 | { 9 | static void Main(string[] args) 10 | { 11 | string token = ""; 12 | int timeout = 1000; 13 | 14 | 15 | int numArgs = 2; 16 | if (args.Length % 2 != 0 && args.Length > numArgs * 2) 17 | { 18 | Console.WriteLine("Incorrect number of arguments"); 19 | } 20 | 21 | //Measure data = new Measure(); 22 | 23 | while (true) 24 | { 25 | // Setup 26 | StatusControl.timeout = timeout; 27 | StatusControl.Current_Status.token = token; 28 | 29 | Console.WriteLine("{0} - {1} - uri: {2}", StatusControl.Current_Status.track.track_resource.name, StatusControl.Current_Status.track.artist_resource.name, StatusControl.Current_Status.track.track_resource.uri); 30 | Thread.Sleep(2000); 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Debugger/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("Debugger")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("Debugger")] 13 | [assembly: AssemblyCopyright("Copyright © 2015")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("98cc0019-99df-417e-8d30-7345c4ed711b")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /DllExporter/DllExporter.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobertFrydenlund/SpotifyPlugin/1c5f2eae1ecfc589b533d09bbc90d41336c0ebc0/DllExporter/DllExporter.exe -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## This plugin is no longer being actively maintained. 2 | For a functional replacement, I recommend https://github.com/khanhas/Spicetify. 3 | 4 | 5 | 6 | # SpotifyPlugin 7 | 8 | Spotify plugin for [Rainmeter](http://rainmeter.net/). Forum discussion can be found [here](http://rainmeter.net/forum/viewtopic.php?f=18&t=17077/). 9 | 10 | ## Example 11 | ```ini 12 | [MeasureCover] 13 | Measure=Plugin 14 | Plugin=SpotifyPlugin 15 | Type=AlbumArt 16 | Res=300 17 | DefaultPath=#@#Default.png 18 | CoverPath=#@#Cover.png 19 | 20 | [MeasureProgress] 21 | Measure=Plugin 22 | Plugin=SpotifyPlugin 23 | Type=Progress 24 | 25 | [MeterCover] 26 | Meter=Image 27 | ImageName=[MeasureCover] 28 | LeftMouseUpAction=[!CommandMeasure "MeasureProgress" "PlayPause"] 29 | X=0 30 | Y=0 31 | W=300 32 | H=300 33 | DynamicVariables=1 34 | ``` 35 | 36 | 37 | ## Offline API 38 | |Measure |Description | Alias 39 | |-----------|-----------------------------------|------| 40 | |TrackName |Returns track name | Track 41 | |AlbumName |Returns album name | Album 42 | |ArtistName |Returns artist name | Artist 43 | |TrackURI |Returns spotify URI for the track 44 | |AlbumURI |Returns spotify URI for the album 45 | |ArtistURI |Returns spotify URI for the artist 46 | |AlbumArt |Path to album image | Cover 47 | |volume|Current volume 48 | |repeat|1 if enabled 49 | |shuffle|1 if enabled 50 | |position|Current position 51 | |playing|1 if playing 52 | |length|Song length| duration 53 | |progress|Song progress (0.0-1.0) 54 | --- 55 | 56 | ## Online API 57 | |Command | Description |Argument| 58 | |-----------|------------------------ |-------------------- 59 | |playpause |Pauses or resumes playback | 60 | |play |Starts playback | 61 | |pause |Pauses playback | 62 | |next |Next song | 63 | |previous |Previous song | 64 | |volume |Changes active device volume |```0``` to ```100``` 65 | |seek |Seek to positon (ms) |```0``` to *```length```* 66 | |seekpercent *or* setposition|Seek to position (percent) |```0``` to ```100``` 67 | |shuffle *or* setshuffle|Change shuffle state |```0``` or ```false```, ```1``` or ```true``` , ```-1``` to toggle. 68 | |repeat *or* setrepeat |Change repeat |```0``` or ```off```, ```1``` or ```track```, ```2``` or ```context```, ```-1``` to toggle. 69 | -------------------------------------------------------------------------------- /SpotifyPlugin.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26730.12 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpotifyPlugin", "SpotifyPlugin\SpotifyPlugin.csproj", "{1743630A-663E-4C0B-8142-DA51AB941550}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Debug", "Debug\Debug.csproj", "{3D041462-02A1-4C60-A673-17DE5403F691}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Debug|x64 = Debug|x64 14 | Debug|x86 = Debug|x86 15 | Release|Any CPU = Release|Any CPU 16 | Release|x64 = Release|x64 17 | Release|x86 = Release|x86 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {1743630A-663E-4C0B-8142-DA51AB941550}.Debug|Any CPU.ActiveCfg = Debug|x64 21 | {1743630A-663E-4C0B-8142-DA51AB941550}.Debug|Any CPU.Build.0 = Debug|x64 22 | {1743630A-663E-4C0B-8142-DA51AB941550}.Debug|x64.ActiveCfg = Debug|x64 23 | {1743630A-663E-4C0B-8142-DA51AB941550}.Debug|x64.Build.0 = Debug|x64 24 | {1743630A-663E-4C0B-8142-DA51AB941550}.Debug|x86.ActiveCfg = Debug|x86 25 | {1743630A-663E-4C0B-8142-DA51AB941550}.Debug|x86.Build.0 = Debug|x86 26 | {1743630A-663E-4C0B-8142-DA51AB941550}.Release|Any CPU.ActiveCfg = Release|x86 27 | {1743630A-663E-4C0B-8142-DA51AB941550}.Release|x64.ActiveCfg = Release|x64 28 | {1743630A-663E-4C0B-8142-DA51AB941550}.Release|x64.Build.0 = Release|x64 29 | {1743630A-663E-4C0B-8142-DA51AB941550}.Release|x86.ActiveCfg = Release|x86 30 | {1743630A-663E-4C0B-8142-DA51AB941550}.Release|x86.Build.0 = Release|x86 31 | {3D041462-02A1-4C60-A673-17DE5403F691}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {3D041462-02A1-4C60-A673-17DE5403F691}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {3D041462-02A1-4C60-A673-17DE5403F691}.Debug|x64.ActiveCfg = Debug|x64 34 | {3D041462-02A1-4C60-A673-17DE5403F691}.Debug|x64.Build.0 = Debug|x64 35 | {3D041462-02A1-4C60-A673-17DE5403F691}.Debug|x86.ActiveCfg = Debug|x86 36 | {3D041462-02A1-4C60-A673-17DE5403F691}.Debug|x86.Build.0 = Debug|x86 37 | {3D041462-02A1-4C60-A673-17DE5403F691}.Release|Any CPU.ActiveCfg = Release|Any CPU 38 | {3D041462-02A1-4C60-A673-17DE5403F691}.Release|Any CPU.Build.0 = Release|Any CPU 39 | {3D041462-02A1-4C60-A673-17DE5403F691}.Release|x64.ActiveCfg = Release|x64 40 | {3D041462-02A1-4C60-A673-17DE5403F691}.Release|x64.Build.0 = Release|x64 41 | {3D041462-02A1-4C60-A673-17DE5403F691}.Release|x86.ActiveCfg = Release|x86 42 | {3D041462-02A1-4C60-A673-17DE5403F691}.Release|x86.Build.0 = Release|x86 43 | EndGlobalSection 44 | GlobalSection(SolutionProperties) = preSolution 45 | HideSolutionNode = FALSE 46 | EndGlobalSection 47 | GlobalSection(ExtensibilityGlobals) = postSolution 48 | SolutionGuid = {A67B5E13-48BB-4A7C-8E73-38AD91E06FD2} 49 | EndGlobalSection 50 | EndGlobal 51 | -------------------------------------------------------------------------------- /SpotifyPlugin/AlbumArt.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Linq; 2 | using System; 3 | using System.IO; 4 | using System.Net; 5 | using System.Threading; 6 | 7 | namespace SpotifyPlugin 8 | { 9 | // TODO dont need this, SpotifyAPI already has this implemented. Fix before 2.0.0 10 | class AlbumArt 11 | { 12 | private static bool useCover; 13 | 14 | public static string CoverPath { get; private set; } 15 | public static string AlbumUri { get; private set; } 16 | 17 | public static string getArt(string albumUri, int resolution, string defaultPath, string coverPath) 18 | { 19 | // Image changed 20 | if (AlbumUri != albumUri) 21 | { 22 | if(resolution != 60 && resolution != 85 && resolution != 120 && resolution != 300 && resolution != 640) 23 | { 24 | 25 | Out.Log(Rainmeter.API.LogType.Warning, "Invalid resolution specified"); 26 | resolution = 300; 27 | } 28 | 29 | Out.Log(Rainmeter.API.LogType.Notice, "Artwork change detected"); 30 | // Update URI 31 | AlbumUri = albumUri; 32 | // Default image 33 | useCover = false; 34 | // Get image in separate thread 35 | Thread t = new Thread(() => GetAlbumImage(resolution, coverPath)); 36 | t.Start(); 37 | } 38 | return useCover ? coverPath : defaultPath; 39 | } 40 | 41 | private static byte[] ReadStream(Stream input) 42 | { 43 | byte[] buffer = new byte[1024]; 44 | using (MemoryStream ms = new MemoryStream()) 45 | { 46 | int read; 47 | while ((read = input.Read(buffer, 0, buffer.Length)) > 0) 48 | { 49 | ms.Write(buffer, 0, read); 50 | } 51 | return ms.ToArray(); 52 | } 53 | } 54 | 55 | public static void GetImageFromUrl(string url, string filePath) 56 | { 57 | // Create http request 58 | HttpWebRequest httpWebRequest = (HttpWebRequest)HttpWebRequest.Create(url); 59 | using (HttpWebResponse httpWebReponse = (HttpWebResponse)httpWebRequest.GetResponse()) 60 | { 61 | 62 | // Read as stream 63 | using (Stream stream = httpWebReponse.GetResponseStream()) 64 | { 65 | Byte[] buffer = ReadStream(stream); 66 | // Make sure the path folder exists 67 | System.IO.Directory.CreateDirectory(Path.GetDirectoryName(filePath)); 68 | // Write stream to file 69 | File.WriteAllBytes(filePath, buffer); 70 | } 71 | } 72 | // Change back to cover image 73 | useCover = true; 74 | CoverPath = url; 75 | //Out.Log(API.LogType.Debug, "Artwork updated"); 76 | } 77 | 78 | public static void GetAlbumImage(int resolution, string filePath) 79 | { 80 | try 81 | { 82 | string rawData; 83 | using (var webpage = new WebClient()) 84 | { 85 | // Request gets ignored if not called from a proper browser 86 | // webpage.Headers[HttpRequestHeader.UserAgent] = "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/15.0.874.121 Safari/535.2"; 87 | webpage.Headers[HttpRequestHeader.UserAgent] = String.Format("SpotifyPlugin {0}", System.Reflection.Assembly.GetCallingAssembly().GetName().Version.ToString()); 88 | 89 | //Out.Log(API.LogType.Debug, "Downloading embed page: {0}", status.track.album_resource.uri); 90 | rawData = webpage.DownloadString("https://embed.spotify.com/oembed/?url=" + AlbumUri); 91 | } 92 | 93 | JObject jo = JObject.Parse(rawData); 94 | // Retrieve cover url 95 | string imgUrl = jo.GetValue("thumbnail_url").ToString(); 96 | 97 | // Specify album resolution 98 | imgUrl = imgUrl.Replace("cover", resolution.ToString()); 99 | 100 | //Out.Log(API.LogType.Debug, "Artwork found, downloading image..."); 101 | 102 | GetImageFromUrl(imgUrl, filePath); 103 | 104 | } 105 | catch (Exception e) 106 | { 107 | Out.ChrashDump(e); 108 | } 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /SpotifyPlugin/FodyWeavers.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /SpotifyPlugin/Out.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Diagnostics.CodeAnalysis; 4 | using System.Globalization; 5 | using System.IO; 6 | using Rainmeter; 7 | 8 | namespace SpotifyPlugin 9 | { 10 | class Out 11 | { 12 | //private static string lastPrint = ""; 13 | private static Stopwatch sw; 14 | 15 | public static void Log(API.LogType verbosity, string value) 16 | { 17 | #pragma warning disable 0162 18 | #if DEBUG 19 | Console.WriteLine(value); 20 | return; 21 | #endif 22 | API.Log(Plugin.Rainmeter, verbosity, value); 23 | #pragma warning restore 0162 24 | } 25 | 26 | public static void Log(API.LogType verbosity, string format, params object[] arg0) 27 | { 28 | Log(verbosity, string.Format(format, arg0)); 29 | } 30 | 31 | public static void Start() 32 | { 33 | sw = new Stopwatch(); 34 | sw.Start(); 35 | } 36 | 37 | public long Stop() 38 | { 39 | sw.Stop(); 40 | return sw.ElapsedMilliseconds; 41 | } 42 | 43 | public static void ChrashDump(Exception e) 44 | { 45 | string chrash = String.Format("\n{0} {1}", DateTime.Now.ToLongTimeString(), DateTime.Now.ToLongDateString()); 46 | chrash += String.Format("\nSpotifyPlugin version {0}", System.Reflection.Assembly.GetCallingAssembly().GetName().Version.ToString()); 47 | chrash += String.Format("\nCulture: {0}", CultureInfo.InstalledUICulture.ToString()); 48 | chrash += String.Format("\nOSVersion: {0}", Environment.OSVersion.ToString()); 49 | chrash += String.Format("\n----"); 50 | chrash += String.Format("\n {0}", e.Message); 51 | chrash += String.Format("\n----"); 52 | chrash += String.Format("\n {0}", e.StackTrace); 53 | Log(API.LogType.Error, chrash); 54 | } 55 | 56 | } 57 | } -------------------------------------------------------------------------------- /SpotifyPlugin/Parent.cs: -------------------------------------------------------------------------------- 1 | using Rainmeter; 2 | using SpotifyAPI.Web; 3 | using SpotifyAPI.Web.Auth; 4 | using SpotifyAPI.Web.Enums; 5 | using SpotifyAPI.Web.Models; 6 | using System; 7 | using System.Threading; 8 | 9 | namespace SpotifyPlugin 10 | { 11 | public class Parent 12 | { 13 | private int _refreshRate = 500; 14 | public int RefreshRate 15 | { 16 | get => _refreshRate; 17 | set 18 | { 19 | _refreshRate = value; 20 | if (_timer != null) 21 | { 22 | _timer.Interval = value; 23 | } 24 | } 25 | } 26 | 27 | private readonly System.Timers.Timer _timer; 28 | 29 | public PlaybackContext Status { get; private set; } 30 | public SpotifyWebAPI WebApi; 31 | 32 | private readonly string _clientSecret = APIKeys.ClientSecret; 33 | private readonly string _clientId = APIKeys.ClientId; 34 | private readonly Scope _scope = Scope.UserReadPlaybackState | Scope.UserModifyPlaybackState; 35 | 36 | private readonly int _timeout = 20; 37 | 38 | private bool _authenticating; 39 | 40 | private Token _token; 41 | public double SecondsToExpiration => (_token?.ExpiresIn - (DateTime.Now - _token?.CreateDate).GetValueOrDefault().TotalSeconds).GetValueOrDefault(); 42 | 43 | public Parent() 44 | { 45 | CheckAuthentication(); 46 | 47 | _timer = new System.Timers.Timer(RefreshRate); 48 | _timer.Elapsed += Timer_Elapsed; 49 | _timer.AutoReset = true; 50 | _timer.Start(); 51 | } 52 | 53 | private void Timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) 54 | { 55 | if (SecondsToExpiration < 60) 56 | { 57 | Out.Log(API.LogType.Notice, "Token expires soon."); 58 | CheckAuthentication(); 59 | return; 60 | } 61 | try 62 | { 63 | Status = WebApi.GetPlayback(); 64 | } 65 | catch 66 | { 67 | CheckAuthentication(); 68 | } 69 | } 70 | 71 | 72 | public void CheckAuthentication() 73 | { 74 | if (_authenticating) return; 75 | _authenticating = true; 76 | new Thread(Authenticate).Start(); 77 | 78 | } 79 | 80 | public void Authenticate() 81 | { 82 | AutorizationCodeAuth authentication = new AutorizationCodeAuth 83 | { 84 | RedirectUri = new UriBuilder("http://127.0.0.1") { Port = 7476 }.Uri.OriginalString.TrimEnd('/'), 85 | ClientId = _clientId, 86 | Scope = _scope, 87 | State = "XSS" 88 | }; 89 | 90 | // Try refreshing 91 | try 92 | { 93 | Out.Log(API.LogType.Notice, "Refreshing token."); 94 | _token = authentication.RefreshToken(Properties.Settings.Default.RefreshToken, _clientSecret); 95 | if (_token.Error == null) 96 | { 97 | WebApi = ApiFromToken(_token); 98 | _authenticating = false; 99 | return; 100 | } 101 | } 102 | catch 103 | { 104 | Thread.Sleep(1000); 105 | _authenticating = false; 106 | return; 107 | } 108 | 109 | Out.Log(API.LogType.Notice, "Token refresh failed, opening authentication window."); 110 | AutoResetEvent authenticationWaitFlag = new AutoResetEvent(false); 111 | WebApi = null; 112 | authentication.OnResponseReceivedEvent += (response) => 113 | { 114 | WebApi = HandleSpotifyResponse(response, authentication); 115 | authenticationWaitFlag.Set(); 116 | }; 117 | 118 | try 119 | { 120 | authentication.StartHttpServer(7476); 121 | 122 | authentication.DoAuth(); 123 | 124 | authenticationWaitFlag.WaitOne(TimeSpan.FromSeconds(_timeout)); 125 | if (WebApi == null) 126 | throw new TimeoutException($"No valid response received for the last {_timeout} seconds"); 127 | } 128 | finally 129 | { 130 | authentication.StopHttpServer(); 131 | } 132 | _authenticating = false; 133 | } 134 | 135 | private SpotifyWebAPI HandleSpotifyResponse(AutorizationCodeAuthResponse response, 136 | AutorizationCodeAuth authentication) 137 | { 138 | if (response.State != "XSS") 139 | throw new SpotifyWebApiException($"Wrong state '{response.State}' received."); 140 | 141 | if (response.Error != null) 142 | throw new SpotifyWebApiException($"Error: {response.Error}"); 143 | 144 | var code = response.Code; 145 | 146 | _token = authentication.ExchangeAuthCode(code, _clientSecret); 147 | 148 | Properties.Settings.Default.RefreshToken = _token.RefreshToken; 149 | Properties.Settings.Default.Save(); 150 | 151 | return ApiFromToken(_token); 152 | } 153 | 154 | private static SpotifyWebAPI ApiFromToken(Token token) 155 | { 156 | var spotifyWebApi = new SpotifyWebAPI() 157 | { 158 | UseAuth = true, 159 | AccessToken = token.AccessToken, 160 | TokenType = token.TokenType 161 | }; 162 | return spotifyWebApi; 163 | } 164 | 165 | public void PlayPause() 166 | { 167 | if (Status.IsPlaying) 168 | { 169 | Pause(); 170 | } 171 | else 172 | { 173 | Play(); 174 | } 175 | } 176 | 177 | public void Play() 178 | { 179 | if (WebApi == null) return; 180 | ErrorResponse er = WebApi.ResumePlayback(); 181 | if (CorrectResponse(er)) return; 182 | Out.Log(API.LogType.Warning, er.Error.Message); 183 | } 184 | 185 | public void Pause() 186 | { 187 | if (WebApi == null) return; 188 | ErrorResponse er = WebApi.PausePlayback(); 189 | if (CorrectResponse(er)) return; 190 | Out.Log(API.LogType.Warning, er.Error.Message); 191 | } 192 | 193 | public void Next() 194 | { 195 | if (WebApi == null) return; 196 | ErrorResponse er = WebApi.SkipPlaybackToNext(); 197 | if (CorrectResponse(er)) return; 198 | } 199 | 200 | public void Previous(double skipThreshold) 201 | { 202 | if (WebApi == null) return; 203 | double playingPosition = 1.0 * (Status?.ProgressMs).GetValueOrDefault() / 1000; 204 | if (playingPosition < skipThreshold) 205 | { 206 | ErrorResponse er = WebApi.SkipPlaybackToPrevious(); 207 | if (CorrectResponse(er)) return; 208 | } 209 | else 210 | { 211 | Seek(0); 212 | ErrorResponse er = WebApi.SkipPlaybackToPrevious(); 213 | if (CorrectResponse(er)) return; 214 | } 215 | } 216 | 217 | public void Seek(int positionMs) 218 | { 219 | if (WebApi == null) return; 220 | ErrorResponse er = WebApi.SeekPlayback(positionMs); 221 | if (CorrectResponse(er)) return; 222 | } 223 | 224 | public void SetVolume(int volume) 225 | { 226 | if (WebApi == null) return; 227 | ErrorResponse er = WebApi.SetVolume(volume); 228 | if (CorrectResponse(er)) return; 229 | } 230 | 231 | public void SetShuffle(bool shuffle) 232 | { 233 | if (WebApi == null) return; 234 | ErrorResponse er = WebApi.SetShuffle(shuffle); 235 | if (CorrectResponse(er)) return; 236 | } 237 | 238 | public void SetRepeat(RepeatState repeat) 239 | { 240 | if (WebApi == null) return; 241 | ErrorResponse er = WebApi.SetRepeatMode(repeat); 242 | if (CorrectResponse(er)) return; 243 | } 244 | 245 | private static bool CorrectResponse(ErrorResponse er) 246 | { 247 | if (!er.HasError()) return true; 248 | Out.Log(API.LogType.Warning, $"Error {er.Error.Status}: {er.Error.Message}"); 249 | if (er.Error.Status == 401) 250 | { 251 | //CheckAuthentication(); 252 | } 253 | return false; 254 | } 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /SpotifyPlugin/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("SpotifyPlugin")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("Rainmeter")] 13 | [assembly: AssemblyCopyright("© 2014 - Robert Frydenlund")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("84266a71-d1fc-41b0-9295-0d929989c774")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("2.1.6")] 36 | [assembly: AssemblyFileVersion("2.1.6")] 37 | -------------------------------------------------------------------------------- /SpotifyPlugin/Properties/Settings.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace SpotifyPlugin.Properties { 12 | 13 | 14 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 15 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "15.3.0.0")] 16 | internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { 17 | 18 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); 19 | 20 | public static Settings Default { 21 | get { 22 | return defaultInstance; 23 | } 24 | } 25 | 26 | [global::System.Configuration.UserScopedSettingAttribute()] 27 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 28 | [global::System.Configuration.DefaultSettingValueAttribute("")] 29 | public string RefreshToken { 30 | get { 31 | return ((string)(this["RefreshToken"])); 32 | } 33 | set { 34 | this["RefreshToken"] = value; 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /SpotifyPlugin/Properties/Settings.settings: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SpotifyPlugin/RainmeterAPI.cs: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2011 Rainmeter Project Developers 2 | * 3 | * This Source Code Form is subject to the terms of the GNU General Public 4 | * License; either version 2 of the License, or (at your option) any later 5 | * version. If a copy of the GPL was not distributed with this file, You can 6 | * obtain one at . */ 7 | 8 | using System; 9 | using System.Runtime.InteropServices; 10 | 11 | namespace Rainmeter 12 | { 13 | /// 14 | /// Wrapper around the Rainmeter C API. 15 | /// 16 | public class API 17 | { 18 | private IntPtr m_Rm; 19 | 20 | public API(IntPtr rm) 21 | { 22 | m_Rm = rm; 23 | } 24 | 25 | static public implicit operator API(IntPtr rm) 26 | { 27 | return new Rainmeter.API(rm); 28 | } 29 | 30 | [DllImport("Rainmeter.dll", CharSet = CharSet.Unicode)] 31 | private extern static IntPtr RmReadString(IntPtr rm, string option, string defValue, bool replaceMeasures); 32 | 33 | [DllImport("Rainmeter.dll", CharSet = CharSet.Unicode)] 34 | private extern static double RmReadFormula(IntPtr rm, string option, double defValue); 35 | 36 | [DllImport("Rainmeter.dll", CharSet = CharSet.Unicode)] 37 | private extern static IntPtr RmReplaceVariables(IntPtr rm, string str); 38 | 39 | [DllImport("Rainmeter.dll", CharSet = CharSet.Unicode)] 40 | private extern static IntPtr RmPathToAbsolute(IntPtr rm, string relativePath); 41 | 42 | /// 43 | /// Executes a command 44 | /// 45 | /// Pointer to current skin (See API.GetSkin) 46 | /// Bang to execute 47 | /// No return type 48 | /// 49 | /// 50 | /// [DllExport] 51 | /// internal double Update(IntPtr data) 52 | /// { 53 | /// Measure measure = (Measure)data; 54 | /// Rainmeter.API.Execute(measure->skin, "!SetVariable SomeVar 10"); // 'measure->skin' stored previously in the Initialize function 55 | /// return 0.0; 56 | /// } 57 | /// 58 | /// 59 | [DllImport("Rainmeter.dll", EntryPoint = "RmExecute", CharSet = CharSet.Unicode)] 60 | public extern static void Execute(IntPtr skin, string command); 61 | 62 | [DllImport("Rainmeter.dll")] 63 | private extern static IntPtr RmGet(IntPtr rm, RmGetType type); 64 | 65 | [DllImport("Rainmeter.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.Cdecl)] 66 | private extern static int LSLog(int type, string unused, string message); 67 | 68 | [DllImport("Rainmeter.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] 69 | private extern static int RmLog(IntPtr rm, LogType type, string message); 70 | 71 | private enum RmGetType 72 | { 73 | MeasureName = 0, 74 | Skin = 1, 75 | SettingsFile = 2, 76 | SkinName = 3, 77 | SkinWindowHandle = 4 78 | } 79 | 80 | public enum LogType 81 | { 82 | Error = 1, 83 | Warning = 2, 84 | Notice = 3, 85 | Debug = 4 86 | } 87 | 88 | /// 89 | /// Retrieves the option defined in the skin file 90 | /// 91 | /// Option name to be read from skin 92 | /// Default value for the option if it is not found or invalid 93 | /// If true, replaces section variables in the returned string 94 | /// Returns the option value as a string 95 | /// 96 | /// 97 | /// [DllExport] 98 | /// public static void Reload(IntPtr data, IntPtr rm, ref double maxValue) 99 | /// { 100 | /// Measure measure = (Measure)data; 101 | /// Rainmeter.API api = (Rainmeter.API)rm; 102 | /// string value = api.ReadString("Value", "DefaultValue"); 103 | /// string action = api.ReadString("Action", "", false); // [MeasureNames] will be parsed/replaced when the action is executed with RmExecute 104 | /// } 105 | /// 106 | /// 107 | public string ReadString(string option, string defValue, bool replaceMeasures = true) 108 | { 109 | return Marshal.PtrToStringUni(RmReadString(m_Rm, option, defValue, replaceMeasures)); 110 | } 111 | 112 | /// 113 | /// Retrieves the option defined in the skin file and converts a relative path to a absolute path 114 | /// 115 | /// Option name to be read from skin 116 | /// Default value for the option if it is not found or invalid 117 | /// Returns the absolute path of the option value as a string 118 | /// 119 | /// 120 | /// [DllExport] 121 | /// public static void Reload(IntPtr data, IntPtr rm, ref double maxValue) 122 | /// { 123 | /// Measure measure = (Measure)data; 124 | /// Rainmeter.API api = (Rainmeter.API)rm; 125 | /// string path = api.ReadPath("MyPath", "C:\\"); 126 | /// } 127 | /// 128 | /// 129 | public string ReadPath(string option, string defValue) 130 | { 131 | return Marshal.PtrToStringUni(RmPathToAbsolute(m_Rm, ReadString(option, defValue))); 132 | } 133 | 134 | /// 135 | /// Retrieves the option defined in the skin file and converts it to a double 136 | /// 137 | /// If the option is a formula, the returned value will be the result of the parsed formula 138 | /// Option name to read from skin 139 | /// Default value for the option if it is not found, invalid, or a formula could not be parsed 140 | /// Returns the option value as a double 141 | /// 142 | /// 143 | /// [DllExport] 144 | /// public static void Reload(IntPtr data, IntPtr rm, ref double maxValue) 145 | /// { 146 | /// Measure measure = (Measure)data; 147 | /// Rainmeter.API api = (Rainmeter.API)rm; 148 | /// double value = api.ReadDouble("Value", 20.0); 149 | /// } 150 | /// 151 | /// 152 | public double ReadDouble(string option, double defValue) 153 | { 154 | return RmReadFormula(m_Rm, option, defValue); 155 | } 156 | 157 | /// 158 | /// Retrieves the option defined in the skin file and converts it to an integer 159 | /// 160 | /// If the option is a formula, the returned value will be the result of the parsed formula 161 | /// Option name to be read from skin 162 | /// Default value for the option if it is not found, invalid, or a formula could not be parsed 163 | /// Returns the option value as an integer 164 | /// 165 | /// 166 | /// [DllExport] 167 | /// public static void Reload(IntPtr data, IntPtr rm, ref double maxValue) 168 | /// { 169 | /// Measure measure = (Measure)data; 170 | /// Rainmeter.API api = (Rainmeter.API)rm; 171 | /// int value = api.ReadInt("Value", 20); 172 | /// } 173 | /// 174 | /// 175 | public int ReadInt(string option, int defValue) 176 | { 177 | return (int)RmReadFormula(m_Rm, option, defValue); 178 | } 179 | 180 | /// 181 | /// Returns a string, replacing any variables (or section variables) within the inputted string 182 | /// 183 | /// String with unresolved variables 184 | /// Returns a string replacing any variables in the 'str' 185 | /// 186 | /// 187 | /// [DllExport] 188 | /// public static double Update(IntPtr data) 189 | /// { 190 | /// Measure measure = (Measure)data; 191 | /// string myVar = measure.api.ReplaceVariables("#MyVar#").ToUpperInvariant(); // 'measure.api' stored previously in the Initialize function 192 | /// if (myVar == "SOMETHING") { return 1.0; } 193 | /// return 0.0; 194 | /// } 195 | /// 196 | /// 197 | public string ReplaceVariables(string str) 198 | { 199 | return Marshal.PtrToStringUni(RmReplaceVariables(m_Rm, str)); 200 | } 201 | 202 | /// 203 | /// Retrieves the name of the measure 204 | /// 205 | /// Call GetMeasureName() in the Initialize function and store the results for later use 206 | /// Returns the current measure name as a string 207 | /// 208 | /// 209 | /// [DllExport] 210 | /// public static void Initialize(ref IntPtr data, IntPtr rm) 211 | /// { 212 | /// Measure measure = new Measure(); 213 | /// Rainmeter.API api = (Rainmeter.API)rm; 214 | /// measure.myName = api.GetMeasureName(); // declare 'myName' as a string in measure class 215 | /// data = GCHandle.ToIntPtr(GCHandle.Alloc(measure)); 216 | /// } 217 | /// 218 | /// 219 | public string GetMeasureName() 220 | { 221 | return Marshal.PtrToStringUni(RmGet(m_Rm, RmGetType.MeasureName)); 222 | } 223 | 224 | /// 225 | /// Retrieves an internal pointer to the current skin 226 | /// 227 | /// Call GetSkin() in the Initialize function and store the results for later use 228 | /// Returns an IntPtr to the current skin 229 | /// 230 | /// 231 | /// [DllExport] 232 | /// public static void Initialize(ref IntPtr data, IntPtr rm) 233 | /// { 234 | /// Measure measure = new Measure(); 235 | /// Rainmeter.API api = (Rainmeter.API)rm; 236 | /// measure.mySkin = api.GetSkin(); // declare 'mySkin' as a IntPtr in measure class 237 | /// data = GCHandle.ToIntPtr(GCHandle.Alloc(measure)); 238 | /// } 239 | /// 240 | /// 241 | public IntPtr GetSkin() 242 | { 243 | return RmGet(m_Rm, RmGetType.Skin); 244 | } 245 | 246 | /// 247 | /// Retrieves a path to the Rainmeter data file (Rainmeter.data) 248 | /// 249 | /// Call GetSettingsFile() in the Initialize function and store the results for later use 250 | /// Returns the path and filename of the Rainmeter data file as a string 251 | /// 252 | /// 253 | /// public static void Initialize(ref IntPtr data, IntPtr rm) 254 | /// { 255 | /// data = GCHandle.ToIntPtr(GCHandle.Alloc(new Measure())); 256 | /// Rainmeter.API api = (Rainmeter.API)rm; 257 | /// if (rmDataFile == null) { rmDataFile = API.GetSettingsFile(); } // declare 'rmDataFile' as a string in global scope 258 | /// } 259 | /// 260 | /// 261 | public static string GetSettingsFile() 262 | { 263 | return Marshal.PtrToStringUni(RmGet(IntPtr.Zero, RmGetType.SettingsFile)); 264 | } 265 | 266 | /// 267 | /// Retrieves full path and name of the skin 268 | /// 269 | /// Call GetSkinName() in the Initialize function and store the results for later use 270 | /// Returns the path and filename of the skin as a string 271 | /// 272 | /// 273 | /// [DllExport] 274 | /// public static void Initialize(ref IntPtr data, IntPtr rm) 275 | /// { 276 | /// Measure measure = new Measure(); 277 | /// Rainmeter.API api = (Rainmeter.API)rm; 278 | /// measure.skinName = api.GetSkinName(); } // declare 'skinName' as a string in measure class 279 | /// data = GCHandle.ToIntPtr(GCHandle.Alloc(measure)); 280 | /// } 281 | /// 282 | /// 283 | public string GetSkinName() 284 | { 285 | return Marshal.PtrToStringUni(RmGet(m_Rm, RmGetType.SkinName)); 286 | } 287 | 288 | /// 289 | /// Executes a command auto getting the skin reference 290 | /// 291 | /// Bang to execute 292 | /// No return type 293 | /// 294 | /// 295 | /// [DllExport] 296 | /// public static double Update(IntPtr data) 297 | /// { 298 | /// Measure measure = (Measure)data; 299 | /// measure.api.Execute("!SetVariable SomeVar 10"); // 'measure.api' stored previously in the Initialize function 300 | /// return 0.0; 301 | /// } 302 | /// 303 | /// 304 | public void Execute(string command) 305 | { 306 | Execute(this.GetSkin(), command); 307 | } 308 | 309 | /// 310 | /// Returns a pointer to the handle of the skin window (HWND) 311 | /// 312 | /// Call GetSkinWindow() in the Initialize function and store the results for later use 313 | /// Returns a handle to the skin window as a IntPtr 314 | /// 315 | /// 316 | /// [DllExport] 317 | /// internal void Initialize(Rainmeter.API rm) 318 | /// { 319 | /// Measure measure = new Measure(); 320 | /// Rainmeter.API api = (Rainmeter.API)rm; 321 | /// measure.skinWindow = api.GetSkinWindow(); } // declare 'skinWindow' as a IntPtr in measure class 322 | /// data = GCHandle.ToIntPtr(GCHandle.Alloc(measure)); 323 | /// } 324 | /// 325 | /// 326 | public IntPtr GetSkinWindow() 327 | { 328 | return RmGet(m_Rm, RmGetType.SkinWindowHandle); 329 | } 330 | 331 | /// 332 | /// DEPRECATED: Save your rm or api reference and use Log(rm, type, message). Sends a message to the Rainmeter log with no source. 333 | /// 334 | public static void Log(int type, string message) 335 | { 336 | LSLog(type, null, message); 337 | } 338 | 339 | /// 340 | /// Sends a message to the Rainmeter log with source 341 | /// 342 | /// LOG_DEBUG messages are logged only when Rainmeter is in debug mode 343 | /// Pointer to the plugin measure 344 | /// Log type, use API.LogType enum (Error, Warning, Notice, or Debug) 345 | /// Message to be logged 346 | /// No return type 347 | /// 348 | /// 349 | /// Rainmeter.API.Log(rm, API.LogType.Notice, "I am a 'notice' log message with a source"); 350 | /// 351 | /// 352 | public static void Log(IntPtr rm, LogType type, string message) 353 | { 354 | RmLog(rm, type, message); 355 | } 356 | 357 | /// 358 | /// 359 | /// Sends a formatted message to the Rainmeter log 360 | /// 361 | /// LOG_DEBUG messages are logged only when Rainmeter is in debug mode 362 | /// Pointer to the plugin measure 363 | /// Log type, use API.LogType enum (Error, Warning, Notice, or Debug) 364 | /// Formatted message to be logged, follows string.Format syntax 365 | /// Comma separated list of args referenced in the formatted message 366 | /// No return type 367 | /// 368 | /// 369 | /// [DllExport] 370 | /// public static double Update(IntPtr data) 371 | /// { 372 | /// Measure measure = (Measure)data; 373 | /// string notice = "notice"; 374 | /// measure.api.LogF(measure.rm, API.LogType.Notice, "I am a '{0}' log message with a source", notice); // 'measure.rm' stored previously in the Initialize function 375 | /// 376 | /// return 0.0; 377 | /// } 378 | /// 379 | /// 380 | public static void LogF(IntPtr rm, LogType type, string format, params Object[] args) 381 | { 382 | RmLog(rm, type, string.Format(format, args)); 383 | } 384 | 385 | /// 386 | /// Sends a message to the Rainmeter log with source 387 | /// 388 | /// LOG_DEBUG messages are logged only when Rainmeter is in debug mode 389 | /// Log type, use API.LogType enum (Error, Warning, Notice, or Debug) 390 | /// Message to be logged 391 | /// No return type 392 | /// 393 | /// 394 | /// [DllExport] 395 | /// public static double Update(IntPtr data) 396 | /// { 397 | /// Measure measure = (Measure)data; 398 | /// measure.api.Log(api, API.LogType.Notice, "I am a 'notice' log message with a source"); // 'measure.api' stored previously in the Initialize function 399 | /// 400 | /// return 0.0; 401 | /// } 402 | /// 403 | /// 404 | public void Log(LogType type, string message) 405 | { 406 | RmLog(this.m_Rm, type, message); 407 | } 408 | 409 | /// 410 | /// Sends a formatted message to the Rainmeter log 411 | /// 412 | /// LOG_DEBUG messages are logged only when Rainmeter is in debug mode 413 | /// Log type, use API.LogType enum (Error, Warning, Notice, or Debug) 414 | /// Formatted message to be logged, follows string.Format syntax 415 | /// Comma separated list of args referenced in the formatted message 416 | /// No return type 417 | /// 418 | /// 419 | /// [DllExport] 420 | /// public static double Update(IntPtr data) 421 | /// { 422 | /// Measure measure = (Measure)data; 423 | /// string notice = "notice"; 424 | /// measure.api.LogF(API.LogType.Notice, "I am a '{0}' log message with a source", notice); // 'measure.api' stored previously in the Initialize function 425 | /// 426 | /// return 0.0; 427 | /// } 428 | /// 429 | /// 430 | public void LogF(LogType type, string format, params Object[] args) 431 | { 432 | RmLog(this.m_Rm, type, string.Format(format, args)); 433 | } 434 | } 435 | /// 436 | /// Dummy attribute to mark method as exported for DllExporter.exe. 437 | /// 438 | [AttributeUsage(AttributeTargets.Method)] 439 | public class DllExport : Attribute 440 | { 441 | public DllExport() 442 | { 443 | 444 | } 445 | } 446 | } -------------------------------------------------------------------------------- /SpotifyPlugin/SpotifyPlugin.cs: -------------------------------------------------------------------------------- 1 | using Rainmeter; 2 | using System; 3 | using System.Runtime.InteropServices; 4 | using System.Text.RegularExpressions; 5 | using System.Threading; 6 | using SpotifyAPI.Web.Enums; 7 | using SpotifyAPI.Web.Models; 8 | 9 | namespace SpotifyPlugin 10 | { 11 | public class Measure 12 | { 13 | /// 14 | /// Cover art save path. 15 | /// 16 | public string coverPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + @"\SpotifyPlugin\cover.png"; 17 | 18 | /// 19 | /// Default path. 20 | /// 21 | public string defaultPath = ""; 22 | 23 | /// 24 | /// Measure type. 25 | /// 26 | public string measureType = ""; 27 | 28 | /// 29 | /// Cover image resolution. 30 | /// 31 | public int artResolution = 300; 32 | 33 | /// 34 | /// Manages actually talking to spotify. 35 | /// 36 | private Parent parent; 37 | 38 | /// 39 | /// If playing position is larger than this value, ExecuteBang("previous") will start song over instead of skipping to previous. 40 | /// 41 | public double skipThreshold = 4; 42 | 43 | public Measure(Parent parent) 44 | { 45 | this.parent = parent; 46 | } 47 | 48 | public void Reload(Rainmeter.API rm, ref double maxValue) 49 | { 50 | measureType = rm.ReadString("Type", "").ToLowerInvariant(); 51 | } 52 | 53 | #if DEBUG 54 | public string GetString() 55 | #else 56 | internal string GetString() 57 | #endif 58 | { 59 | switch (measureType) 60 | { 61 | case "trackname": 62 | case "track": 63 | return parent.Status?.Item?.Name ?? ""; 64 | 65 | case "artistname": 66 | case "artist": 67 | var artists = parent.Status?.Item?.Artists; 68 | if (artists == null) return ""; 69 | string result = ""; 70 | foreach (SimpleArtist artist in artists) 71 | { 72 | if (result.Length != 0) 73 | { 74 | result += ", "; 75 | } 76 | result += artist.Name; 77 | } 78 | return result; 79 | 80 | case "albumname": 81 | case "album": 82 | return parent.Status?.Item?.Album?.Name ?? ""; 83 | 84 | case "trackuri": 85 | return parent.Status?.Item?.Uri ?? ""; 86 | 87 | case "albumuri": 88 | return parent.Status?.Item?.Album.Uri ?? ""; 89 | 90 | case "artisturi": 91 | // TODO 92 | //return parent.Status?.Track?.ArtistResource?.Uri ?? ""; 93 | return "not implemented yet"; 94 | 95 | case "position": 96 | TimeSpan position = TimeSpan.FromMilliseconds((parent.Status?.ProgressMs).GetValueOrDefault()); 97 | return position.ToString(@"mm\:ss"); 98 | 99 | case "duration": 100 | case "length": 101 | TimeSpan duration = TimeSpan.FromMilliseconds((parent.Status?.Item?.DurationMs).GetValueOrDefault()); 102 | return duration.ToString(@"mm\:ss"); 103 | 104 | // TODO 105 | case "albumart": 106 | case "cover": 107 | return AlbumArt.getArt(parent.Status?.Item?.Album?.Uri, artResolution, defaultPath, coverPath); 108 | 109 | } 110 | // MeasureType.Major, MeasureType.Minor, and MeasureType.Number are 111 | // numbers. Therefore, null is returned here for them. This is to 112 | // inform Rainmeter that it can treat those types as numbers. 113 | 114 | return null; 115 | } 116 | 117 | #if DEBUG 118 | public double Update() 119 | #else 120 | internal double Update() 121 | #endif 122 | { 123 | switch (measureType) 124 | { 125 | case "volume": 126 | return (parent.Status?.Device?.VolumePercent).GetValueOrDefault(); 127 | 128 | case "repeat": 129 | return (int)(parent.Status?.RepeatState).GetValueOrDefault(); 130 | 131 | case "shuffle": 132 | return (parent.Status?.ShuffleState).GetValueOrDefault() ? 1 : 0; 133 | 134 | case "position": 135 | return (parent.Status?.ProgressMs).GetValueOrDefault(); 136 | 137 | case "playing": 138 | return (parent.Status?.IsPlaying).GetValueOrDefault() ? 1 : 0; 139 | 140 | case "length": 141 | return (parent.Status?.Item?.DurationMs).GetValueOrDefault(); 142 | 143 | case "progress": 144 | double? o = parent.Status?.ProgressMs / parent.Status?.Item?.DurationMs; 145 | return o.GetValueOrDefault(); 146 | } 147 | //API.Log(API.LogType.Error, "SpotifyPlugin: Type=" + measureType + " not valid"); 148 | return 0.0; 149 | } 150 | 151 | internal void ExecuteBang(string arg) 152 | { 153 | Thread t = new Thread(() => Execute(arg)); 154 | t.Start(); 155 | } 156 | 157 | public void Execute(string arg) 158 | { 159 | if(parent.Status == null) return; 160 | string[] args = Regex.Split(arg.ToLowerInvariant(), " "); 161 | if (args.Length == 0) { Out.Log(API.LogType.Warning, $"No command given"); return; } 162 | switch (args[0]) 163 | { 164 | // Single commands 165 | case "playpause": 166 | parent.PlayPause(); 167 | return; 168 | case "play": 169 | parent.Play(); 170 | return; 171 | case "pause": 172 | parent.Pause(); 173 | return; 174 | case "next": 175 | parent.Next(); 176 | return; 177 | case "previous": 178 | parent.Previous(skipThreshold); 179 | return; 180 | } 181 | 182 | if (args.Length < 2) {Out.Log(API.LogType.Warning, $"Invalid amount of arguments for {args[9]}"); return;} 183 | switch (args[0]) 184 | { 185 | // Double commands 186 | case "volume": 187 | if (!Int32.TryParse(args[1], out int volume) && volume > 100 && volume < 0) 188 | { 189 | Out.Log(API.LogType.Warning, $"Invalid arguments for command: {args[0]}. {args[1]} should be an integer between 0 and 100."); 190 | return; 191 | } 192 | parent.SetVolume(volume); 193 | return; 194 | case "seek": 195 | if (!Int32.TryParse(args[1], out int positionMs)) 196 | { 197 | Out.Log(API.LogType.Warning, $"Invalid arguments for command: {args[0]}. {args[1]} should be an integer."); 198 | return; 199 | } 200 | parent.Seek(positionMs); 201 | return; 202 | case "seekpercent": 203 | case "setposition": 204 | if (!float.TryParse(args[1], out float position)) 205 | { 206 | Out.Log(API.LogType.Warning, $"Invalid arguments for command: {args[0]}. {args[1]} should be a number from 0 to 100."); 207 | return; 208 | } 209 | // TODO probably not correct 210 | parent.Seek((int)(parent.Status.Item.DurationMs * position) / 100); 211 | return; 212 | case "shuffle": 213 | case "setshuffle": 214 | if (!ShuffleTryParse(args[1], out bool shuffle)) 215 | { 216 | Out.Log(API.LogType.Warning, $"Invalid arguments for command: {args[0]}. {args[1]} should be either -1, 0, 1, True or False"); 217 | return; 218 | } 219 | parent.SetShuffle(shuffle); 220 | return; 221 | case "repeat": 222 | case "setrepeat": 223 | if (!RepeatTryParse(args[1], out RepeatState repeat)) 224 | { 225 | Out.Log(API.LogType.Warning, $"Invalid arguments for command: {args[0]}. {args[1]} should be either Off, Track, Context, -1, 0, 1 or 2"); 226 | return; 227 | } 228 | parent.SetRepeat(repeat); 229 | return; 230 | default: 231 | Out.Log(API.LogType.Warning, $"Unknown command: {arg}"); 232 | break; 233 | } 234 | 235 | } 236 | 237 | private bool RepeatTryParse(string value, out RepeatState repeat) 238 | { 239 | switch (value) 240 | { 241 | case null: 242 | repeat = RepeatState.Off; 243 | return false; 244 | case "-1": 245 | PlaybackContext pc = parent.WebApi.GetPlayback(); 246 | RepeatState repeatState = pc.RepeatState; 247 | switch (repeatState) 248 | { 249 | case RepeatState.Track: 250 | repeat = RepeatState.Context; 251 | break; 252 | case RepeatState.Context: 253 | repeat = RepeatState.Off; 254 | break; 255 | case RepeatState.Off: 256 | repeat = RepeatState.Track; 257 | break; 258 | default: 259 | repeat = RepeatState.Off; 260 | return false; 261 | } 262 | return true; 263 | case "0": 264 | repeat = RepeatState.Off; 265 | return true; 266 | default: 267 | return Enum.TryParse(value, out repeat); 268 | } 269 | } 270 | 271 | private bool ShuffleTryParse(string value, out bool shuffle) 272 | { 273 | switch (value) 274 | { 275 | case null: 276 | shuffle = false; 277 | return false; 278 | case "-1": 279 | shuffle = !(parent.Status?.ShuffleState).GetValueOrDefault(); 280 | return true; 281 | case "0": 282 | shuffle = false; 283 | return true; 284 | case "1": 285 | shuffle = true; 286 | return true; 287 | default: 288 | return bool.TryParse(value, out shuffle); 289 | } 290 | } 291 | } 292 | 293 | public static class Plugin 294 | { 295 | static IntPtr StringBuffer = IntPtr.Zero; 296 | public static IntPtr Rainmeter; 297 | 298 | static Parent parent; 299 | 300 | [DllExport] 301 | public static void Initialize(ref IntPtr data, IntPtr rm) 302 | { 303 | Rainmeter = rm; 304 | if (parent == null) { parent = new Parent(); } 305 | data = GCHandle.ToIntPtr(GCHandle.Alloc(new Measure(parent))); 306 | } 307 | 308 | [DllExport] 309 | public static void Finalize(IntPtr data) 310 | { 311 | GCHandle.FromIntPtr(data).Free(); 312 | 313 | if (StringBuffer != IntPtr.Zero) 314 | { 315 | Marshal.FreeHGlobal(StringBuffer); 316 | StringBuffer = IntPtr.Zero; 317 | } 318 | } 319 | 320 | [DllExport] 321 | public static void Reload(IntPtr data, IntPtr rm, ref double maxValue) 322 | { 323 | Measure measure = (Measure)GCHandle.FromIntPtr(data).Target; 324 | measure.Reload(new Rainmeter.API(rm), ref maxValue); 325 | } 326 | 327 | [DllExport] 328 | public static double Update(IntPtr data) 329 | { 330 | Measure measure = (Measure)GCHandle.FromIntPtr(data).Target; 331 | return measure.Update(); 332 | } 333 | 334 | [DllExport] 335 | public static IntPtr GetString(IntPtr data) 336 | { 337 | Measure measure = (Measure)GCHandle.FromIntPtr(data).Target; 338 | if (StringBuffer != IntPtr.Zero) 339 | { 340 | Marshal.FreeHGlobal(StringBuffer); 341 | StringBuffer = IntPtr.Zero; 342 | } 343 | 344 | string stringValue = measure.GetString(); 345 | if (stringValue != null) 346 | { 347 | StringBuffer = Marshal.StringToHGlobalUni(stringValue); 348 | } 349 | 350 | return StringBuffer; 351 | } 352 | 353 | [DllExport] 354 | public static void ExecuteBang(IntPtr data, IntPtr args) 355 | { 356 | Measure measure = (Measure)GCHandle.FromIntPtr(data).Target; 357 | measure.ExecuteBang(Marshal.PtrToStringUni(args)); 358 | } 359 | } 360 | } 361 | -------------------------------------------------------------------------------- /SpotifyPlugin/SpotifyPlugin.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Debug 5 | x86 6 | 8.0.30703 7 | 2.0 8 | {1743630A-663E-4C0B-8142-DA51AB941550} 9 | Library 10 | Properties 11 | SpotifyPlugin 12 | SpotifyPlugin 13 | v4.5 14 | 15 | 16 | 512 17 | publish\ 18 | true 19 | Disk 20 | false 21 | Foreground 22 | 7 23 | Days 24 | false 25 | false 26 | true 27 | 0 28 | 1.0.0.%2a 29 | false 30 | false 31 | true 32 | 33 | 34 | 35 | 36 | x86 37 | true 38 | full 39 | false 40 | bin\Debug\ 41 | DEBUG;TRACE 42 | prompt 43 | 4 44 | false 45 | false 46 | 47 | 48 | x86 49 | pdbonly 50 | true 51 | bin\Release\ 52 | TRACE 53 | prompt 54 | 4 55 | false 56 | false 57 | 58 | 59 | 60 | 61 | 62 | x64 63 | bin\x64\Debug\ 64 | true 65 | DEBUG 66 | false 67 | 68 | 69 | x64 70 | bin\x64\Release\ 71 | true 72 | TRACE;X64 73 | true 74 | MinimumRecommendedRules.ruleset 75 | false 76 | 77 | 78 | 79 | ..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll 80 | 81 | 82 | ..\packages\SpotifyAPI-NET.2.19.0\lib\SpotifyAPI.dll 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | True 95 | True 96 | Settings.settings 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | Designer 105 | 106 | 107 | Designer 108 | 109 | 110 | SettingsSingleFileGenerator 111 | Settings.Designer.cs 112 | 113 | 114 | 115 | 116 | False 117 | .NET Framework 3.5 SP1 Client Profile 118 | false 119 | 120 | 121 | False 122 | .NET Framework 3.5 SP1 123 | true 124 | 125 | 126 | False 127 | Windows Installer 3.1 128 | true 129 | 130 | 131 | 132 | 133 | Designer 134 | 135 | 136 | 137 | 138 | if $(ConfigurationName) == Release "$(SolutionDir)\DllExporter\DllExporter.exe" "$(ConfigurationName)" "$(PlatformName)" "$(TargetDir)\" "$(TargetFileName)" 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. 148 | 149 | 150 | 151 | 152 | 153 | 160 | -------------------------------------------------------------------------------- /SpotifyPlugin/app.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /SpotifyPlugin/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | --------------------------------------------------------------------------------