├── .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 |
--------------------------------------------------------------------------------