Welcome to Xpo Music v2.0
n&&(n=l)});return new w(a,b,m,d,q,n,c)}function n(a,c){function b(a){var f=a+"1";a+="2";var v,d,m,e;d=0;for(k=c[f];k<=c[a];k++)if(y[k]>n/2){m=c.copy();e=c.copy();v=k-c[f];d=c[a]-k;for(v=v<=d?Math.min(c[a]-1,~~(k+d/2)):Math.max(c[f],~~(k-1-v/2));!y[v];)v++;for(d=s[v];!d&&y[v-1];)d=s[--v];m[a]=v;e[f]=m[a]+1;return[m,e]}}if(c.count()){var d=c.r2- //@ts-ignore c.r1+1,m=c.g2-c.g1+1,e=l.max([d,m,c.b2-c.b1+1]);if(1==c.count())return[c.copy()];var n=0,y=[],s=[],k,g,t,u,p;if(e==d)for(k=c.r1;k<=c.r2;k++){u=0;for(g=c.g1;g<=c.g2;g++)for(t=c.b1;t<=c.b2;t++)p=h(k,g,t),u+=a[p]||0;n+=u;y[k]=n}else if(e==m)for(k=c.g1;k<=c.g2;k++){u=0;for(g=c.r1;g<=c.r2;g++)for(t=c.b1;t<=c.b2;t++)p=h(g,k,t),u+=a[p]||0;n+=u;y[k]=n}else for(k=c.b1;k<=c.b2;k++){u=0;for(g=c.r1;g<=c.r2;g++)for(t=c.g1;t<=c.g2;t++)p=h(g,t,k),u+=a[p]||0;n+=u;y[k]=n}y.forEach(function(a,c){s[c]=n-a});return e== //@ts-ignore d?b("r"):e==m?b("g"):b("b")}}var d=5,e=8-d;w.prototype={volume:function(a){if(!this._volume||a)this._volume=(this.r2-this.r1+1)*(this.g2-this.g1+1)*(this.b2-this.b1+1);return this._volume},count:function(a){var c=this.histo;if(!this._count_set||a){a=0;var b,d,n;for(b=this.r1;b<=this.r2;b++)for(d=this.g1;d<=this.g2;d++)for(n=this.b1;n<=this.b2;n++)index=h(b,d,n),a+=c[index]||0;this._count=a;this._count_set=!0}return this._count},copy:function(){return new w(this.r1,this.r2,this.g1,this.g2,this.b1, //@ts-ignore this.b2,this.histo)},avg:function(a){var c=this.histo;if(!this._avg||a){a=0;var b=1<<8-d,n=0,e=0,g=0,q,l,s,k;for(l=this.r1;l<=this.r2;l++)for(s=this.g1;s<=this.g2;s++)for(k=this.b1;k<=this.b2;k++)q=h(l,s,k),q=c[q]||0,a+=q,n+=q*(l+0.5)*b,e+=q*(s+0.5)*b,g+=q*(k+0.5)*b;this._avg=a?[~~(n/a),~~(e/a),~~(g/a)]:[~~(b*(this.r1+this.r2+1)/2),~~(b*(this.g1+this.g2+1)/2),~~(b*(this.b1+this.b2+1)/2)]}return this._avg},contains:function(a){var c=a[0]>>e;gval=a[1]>>e;bval=a[2]>>e;return c>=this.r1&&c<=this.r2&& //@ts-ignore gval>=this.g1&&gval<=this.g2&&bval>=this.b1&&bval<=this.b2}};p.prototype={push:function(a){this.vboxes.push({vbox:a,color:a.avg()})},palette:function(){return this.vboxes.map(function(a){return a.color})},size:function(){return this.vboxes.size()},map:function(a){for(var c=this.vboxes,b=0;bb[0]&&5>b[1]&&5>b[2]&&(a[0].color=[0,0,0]);var b=a.length-1,n=a[b].color;251 d;)if(f=a.pop(),f.count()){var m=n(h,f);f=m[0];m=m[1];if(!f)break; //@ts-ignore a.push(f);m&&(a.push(m),c++);if(c>=b)break;if(1E3 c||256 this.yiq?"#fff":"#000"};b.prototype.getBodyTextColor=function(){this._ensureTextColors();return 150>this.yiq?"#fff":"#000"};b.prototype._ensureTextColors=function(){if(!this.yiq)return this.yiq=(299*this.rgb[0]+587*this.rgb[1]+114*this.rgb[2])/1E3};return b}();window.Vibrant=g=function(){function b(a,b,d){this.swatches=w(this.swatches,this);var e,f, c,g,p,m,r,q;"undefined"===typeof b&&(b=64);"undefined"===typeof d&&(d=5);p=new l(a);r=p.getImageData().data;m=p.getPixelCount();a=[];for(g=0;g =f&&s<=c&&m>=b&&m<=d&&!this.isAlreadySelected(k)&&(m=this.createComparisonValue(s,e,m,a, k.getPopulation(),this.HighestPopulation),void 0===l||m>q))l=k,q=m;return l};b.prototype.createComparisonValue=function(a,b,d,e,f,c){return this.weightedMean(this.invertDiff(a,b),this.WEIGHT_SATURATION,this.invertDiff(d,e),this.WEIGHT_LUMA,f/c,this.WEIGHT_POPULATION)};b.prototype.invertDiff=function(a,b){return 1-Math.abs(a-b)};b.prototype.weightedMean=function(){var a,b,d,e,f,c;f=1<=arguments.length?p.call(arguments,0):[];for(a=d=b=0;a c&&(c+=1);1 c?b:c<2/3?a+(b-a)*(2/3-c)*6:a};0===b?c=f=e=d:(b=0.5>d?d*(1+b):d+b-d*b,d=2*d-b,c=e(d,b,a+1/3),f=e(d,b,a),e=e(d,b,a-1/3));return[255*c,255*f,255*e]};return b}();window.CanvasImage=l=function(){function b(a){this.canvas= document.createElement("canvas");this.context=this.canvas.getContext("2d");document.body.appendChild(this.canvas);this.width=this.canvas.width=a.width;this.height=this.canvas.height=a.height;this.context.drawImage(a,0,0,this.width,this.height)}b.prototype.clear=function(){return this.context.clearRect(0,0,this.width,this.height)};b.prototype.update=function(a){return this.context.putImageData(a,0,0)};b.prototype.getPixelCount=function(){return this.width*this.height};b.prototype.getImageData=function(){return this.context.getImageData(0, 0,this.width,this.height)};b.prototype.removeCanvas=function(){return this.canvas.parentNode.removeChild(this.canvas)};return b}()}).call(this)},{quantize:1}]},{},[2]); } } ================================================ FILE: XpoMusic/Scripts/LightTheme/initScript-light.ts ================================================ /// /// namespace XpoMusicScript { document.getElementsByTagName('body')[0].setAttribute('data-xpotifyTheme', 'light'); var errors = ""; errors += Common.init(); errors += Light.PageOverlay.createPageOverlay(); if (errors.length > 0) { try { // @ts-ignore XpoMusic.initFailed(errors); } catch (ex) { } throw errors; } } ================================================ FILE: XpoMusic/Scripts/LightTheme/pageOverlay.ts ================================================ namespace XpoMusicScript.Light.PageOverlay { export function createPageOverlay() { try { var body = document.getElementsByTagName('body')[0]; var overlayDiv = document.createElement('div'); overlayDiv.classList.add("whole-page-overlay"); body.appendChild(overlayDiv); } catch (ex) { return "injectOverlayFailed,"; } return ""; } } ================================================ FILE: XpoMusic/Scripts/LightTheme/tsconfig.json ================================================ { "compileOnSave": true, "compilerOptions": { "noImplicitAny": false, "noEmitOnError": true, "removeComments": false, "sourceMap": true, "module": "system", "outFile": "../../InjectedAssets/init-light.js", "target": "es6" }, "include": [ "*.ts", "../Common/*.ts", "../Lib/*.ts", "../SpotifyApi/*.ts" ] } ================================================ FILE: XpoMusic/Scripts/SpotifyApi/apiBase.ts ================================================ namespace XpoMusicScript.SpotifyApi { declare var XpoMusic: any; var accessToken = "{{SPOTIFYACCESSTOKEN}}"; export abstract class ApiBase { protected async sendJsonRequestWithToken(uri, method, body = undefined): Promise { if (accessToken.length === 0) { accessToken = await XpoMusic.getNewAccessTokenAsync(); } return await this.sendJsonRequestWithTokenInternal(uri, method, body, true); } private async sendJsonRequestWithTokenInternal(uri, method, body, allowRefreshingToken): Promise { var response = await fetch(uri, { method: method, body: JSON.stringify(body), headers: { 'Authorization': 'Bearer ' + accessToken, }, }); XpoMusic.log("SpotifyApi: " + uri + " (" + method + ") -> result status = " + response.status); if (response.status == 401 && allowRefreshingToken) { // Refresh access token and retry XpoMusic.log("Will ask for new token."); accessToken = await XpoMusic.getNewAccessTokenAsync(); return await this.sendJsonRequestWithTokenInternal(uri, method, body, false); } return response; } } } ================================================ FILE: XpoMusic/Scripts/SpotifyApi/library.ts ================================================ /// namespace XpoMusicScript.SpotifyApi { export class Library extends ApiBase { public async isTrackSaved(trackId: string): Promise { return (await this.isTracksSaved([trackId]))[0]; } public async isTracksSaved(trackIds: string[]): Promise { var output = []; for (var i = 0; i < trackIds.length; i += 50) { var slice = trackIds.slice(i, i + 50); output = output.concat(await this.isTracksSavedInternal(slice)); } return output; } private async isTracksSavedInternal(trackIds: string[]): Promise { var url = 'https://api.spotify.com/v1/me/tracks/contains?ids=' + trackIds.join(','); var result = await this.sendJsonRequestWithToken(url, 'get'); var data = await result.json(); return data; } public async saveTrack(trackId: string): Promise { var url = "https://api.spotify.com/v1/me/tracks?ids=" + trackId; var result = await this.sendJsonRequestWithToken(url, 'put'); return result.status >= 200 && result.status <= 299; } public async removeTrack(trackId: string): Promise { var url = "https://api.spotify.com/v1/me/tracks?ids=" + trackId; var result = await this.sendJsonRequestWithToken(url, 'delete'); return result.status >= 200 && result.status <= 299; } } } ================================================ FILE: XpoMusic/Scripts/SpotifyApi/player.ts ================================================ /// namespace XpoMusicScript.SpotifyApi { export class Player extends ApiBase { public async getCurrentlyPlaying(): Promise { var url = 'https://api.spotify.com/v1/me/player'; var result = await this.sendJsonRequestWithToken(url, 'get'); var data = await result.json(); return data; } } } ================================================ FILE: XpoMusic/SpotifyApi/Album.cs ================================================ using XpoMusic.Helpers; using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Text; using System.Threading.Tasks; namespace XpoMusic.SpotifyApi { public class Album : ApiBase { public async Task GetAlbum(string albumId) { var result = await SendRequestWithTokenAsync($"https://api.spotify.com/v1/albums/{albumId}", HttpMethod.Get); var resultString = await result.Content.ReadAsStringAsync(); if (result.IsSuccessStatusCode == false) AnalyticsHelper.Log("api", "getalbum::" + result.StatusCode.ToString()); return JsonConvert.DeserializeObject (resultString); } } } ================================================ FILE: XpoMusic/SpotifyApi/ApiBase.cs ================================================ using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Threading.Tasks; using XpoMusic.SpotifyApi.Model; namespace XpoMusic.SpotifyApi { public class ApiBase { protected static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); public Task SendRequestWithTokenAsync(string url, HttpMethod httpMethod) { return SendRequestWithTokenAsync(url, httpMethod, new Dictionary (), canUseRefreshToken: true); } public Task SendRequestWithTokenAsync(string url, HttpMethod httpMethod, Dictionary data) { return SendRequestWithTokenAsync(url, httpMethod, data, canUseRefreshToken: true); } private async Task SendRequestWithTokenAsync(string url, HttpMethod httpMethod, Dictionary data, bool canUseRefreshToken) { try { var httpClient = new HttpClient(); HttpRequestMessage msg = new HttpRequestMessage(httpMethod, url); msg.Headers.Clear(); msg.Headers.Authorization = new AuthenticationHeaderValue("Bearer", TokenHelper.GetTokens().AccessToken); msg.Content = new FormUrlEncodedContent(data.Select(x => x).ToList()); var response = await httpClient.SendAsync(msg); if ((response.IsSuccessStatusCode == false) && (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)) throw new UnauthorizedAccessException(); if (response.IsSuccessStatusCode) logger.Info($"{httpMethod.Method} request to {url} returned with status code {response.StatusCode}."); else logger.Warn($"{httpMethod.Method} request to {url} returned with status code {response.StatusCode}."); return response; } catch (UnauthorizedAccessException) { if (!canUseRefreshToken) throw; await TokenHelper.GetAndSaveNewTokenAsync(); return await SendRequestWithTokenAsync(url, httpMethod, data, false); } } public Task SendJsonRequestWithTokenAsync(string url, HttpMethod httpMethod, string data) { return SendJsonRequestWithTokenAsync(url, httpMethod, data, canUseRefreshToken: true); } private async Task SendJsonRequestWithTokenAsync(string url, HttpMethod httpMethod, string data, bool canUseRefreshToken) { try { var httpClient = new HttpClient(); HttpRequestMessage msg = new HttpRequestMessage(httpMethod, url); msg.Headers.Clear(); msg.Headers.Authorization = new AuthenticationHeaderValue("Bearer", TokenHelper.GetTokens().AccessToken); msg.Content = new StringContent(data, Encoding.UTF8, "application/json"); var response = await httpClient.SendAsync(msg); if ((response.IsSuccessStatusCode == false) && (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)) throw new UnauthorizedAccessException(); if (response.IsSuccessStatusCode) logger.Info($"{httpMethod.Method} [json] request to {url} returned with status code {response.StatusCode}."); else logger.Warn($"{httpMethod.Method} [json] request to {url} returned with status code {response.StatusCode}."); return response; } catch (UnauthorizedAccessException) { if (!canUseRefreshToken) throw; await TokenHelper.GetAndSaveNewTokenAsync(); return await SendJsonRequestWithTokenAsync(url, httpMethod, data, false); } } public async Task > GetNextPage (Paging page) { if (!page.hasNext) return null; var result = await SendRequestWithTokenAsync( page.next, HttpMethod.Get); var resultString = await result.Content.ReadAsStringAsync(); return JsonConvert.DeserializeObject >(resultString); } public async Task > GetPrevPage (Paging page) { if (!page.hasPrev) return null; var result = await SendRequestWithTokenAsync( page.previous, HttpMethod.Get); var resultString = await result.Content.ReadAsStringAsync(); return JsonConvert.DeserializeObject >(resultString); } public async Task > GetNextPage(Paging page) { if (!page.hasNext) return null; var result = await SendRequestWithTokenAsync( page.next, HttpMethod.Get); var resultString = await result.Content.ReadAsStringAsync(); return JsonConvert.DeserializeObject (resultString).artists; } public async Task > GetPrevPage(Paging page) { if (!page.hasPrev) return null; var result = await SendRequestWithTokenAsync( page.previous, HttpMethod.Get); var resultString = await result.Content.ReadAsStringAsync(); return JsonConvert.DeserializeObject (resultString).artists; } } } ================================================ FILE: XpoMusic/SpotifyApi/Artist.cs ================================================ using XpoMusic.Helpers; using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Text; using System.Threading.Tasks; namespace XpoMusic.SpotifyApi { public class Artist : ApiBase { public async Task GetArtist(string artistId) { var result = await SendRequestWithTokenAsync($"https://api.spotify.com/v1/artists/{artistId}", HttpMethod.Get); var resultString = await result.Content.ReadAsStringAsync(); if (result.IsSuccessStatusCode == false) AnalyticsHelper.Log("api", "getartist::" + result.StatusCode.ToString()); return JsonConvert.DeserializeObject (resultString); } } } ================================================ FILE: XpoMusic/SpotifyApi/Authorization.cs ================================================ using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Threading.Tasks; using XpoMusic.Classes; namespace XpoMusic.SpotifyApi { public static class Authorization { internal static readonly string Scopes = "user-read-recently-played playlist-read-private playlist-read-collaborative playlist-modify-private playlist-modify-public user-read-email user-library-read user-library-modify user-read-playback-state user-modify-playback-state user-read-private user-read-currently-playing user-follow-read user-follow-modify streaming user-top-read app-remote-control"; internal static readonly string SpotifyLoginUri = "https://accounts.spotify.com/"; internal static readonly string FacebookLoginFinishRedirectUri = "https://accounts.spotify.com/api/facebook/oauth/access_token"; internal static readonly string RedirectUri = "https://xpomusic.ghiasi.net/login/redirect"; internal static readonly string FacebookLoginUri = "https://www.facebook.com/login.php"; public static string GetAuthorizationUrl(string state) { return "https://accounts.spotify.com/authorize?" + $"client_id={WebUtility.UrlEncode(Secrets.SpotifyClientId)}&" + $"response_type=code&" + $"redirect_uri={WebUtility.UrlEncode(RedirectUri)}&" + $"state={WebUtility.UrlEncode(state)}&" + $"scope={WebUtility.UrlEncode(Scopes)}&" + $"show_dialog=false"; } public static async Task RetrieveAndSaveTokensFromAuthCode(string code) { var httpClient = new HttpClient(); HttpRequestMessage msg = new HttpRequestMessage(HttpMethod.Post, "https://accounts.spotify.com/api/token"); msg.Headers.Clear(); msg.Content = new FormUrlEncodedContent(new KeyValuePair [] { new KeyValuePair ("grant_type", "authorization_code"), new KeyValuePair ("code", code), new KeyValuePair ("redirect_uri", RedirectUri), new KeyValuePair ("client_id", Secrets.SpotifyClientId), new KeyValuePair ("client_secret", Secrets.SpotifyClientSecret), }); var response = await httpClient.SendAsync(msg); var responseString = await response.Content.ReadAsStringAsync(); var responseData = JsonConvert.DeserializeObject >(responseString); var accessToken = responseData["access_token"].ToString(); var refreshToken = responseData["refresh_token"].ToString(); TokenHelper.SaveTokens(accessToken, refreshToken); LocalConfiguration.ApiTokenVersion = 2; } } } ================================================ FILE: XpoMusic/SpotifyApi/Library.cs ================================================ using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Text; using System.Threading.Tasks; using XpoMusic.SpotifyApi.Model; namespace XpoMusic.SpotifyApi { public class Library : ApiBase { public async Task IsTrackSaved(string trackId) { return (await IsTrackSaved(new[] { trackId }))[0]; } public async Task IsTrackSaved(string[] trackIds) { List output = new List (); for (int i = 0; i < trackIds.Length; i += 50) { var slice = trackIds.Skip(i).Take(50).ToArray(); output.AddRange(await IsTrackSavedInternal(slice)); } return output.ToArray(); } private async Task IsTrackSavedInternal(string[] trackIds) { trackIds = trackIds.Where(x => x != null && x.Length > 0).ToArray(); if (trackIds.Length == 0) return new bool[] { }; var result = await SendRequestWithTokenAsync( "https://api.spotify.com/v1/me/tracks/contains?" + $"ids={string.Join(',', trackIds)}", HttpMethod.Get); var resultString = await result.Content.ReadAsStringAsync(); return JsonConvert.DeserializeObject (resultString); } public async Task SaveTrackToLibrary(string trackId) { var result = await SendRequestWithTokenAsync( "https://api.spotify.com/v1/me/tracks?" + $"ids={trackId}", HttpMethod.Put); return result.IsSuccessStatusCode; } public async Task RemoveTrackFromLibrary(string trackId) { var result = await SendRequestWithTokenAsync( "https://api.spotify.com/v1/me/tracks?" + $"ids={trackId}", HttpMethod.Delete); return result.IsSuccessStatusCode; } public async Task > GetAlbums(int offset, int limit = 50) { var result = await SendRequestWithTokenAsync( $"https://api.spotify.com/v1/me/albums?limit={limit}&offset={offset}", HttpMethod.Get); var resultString = await result.Content.ReadAsStringAsync(); return JsonConvert.DeserializeObject