diff --git a/Oauth2.pm b/Oauth2.pm
new file mode 100644
index 0000000..c6d1a41
--- /dev/null
+++ b/Oauth2.pm
@@ -0,0 +1,202 @@
+package Plugins::SqueezeCloud::Oauth2;
+
+# Plugin to stream audio from SoundCloud streams
+#
+# Released under GNU General Public License version 2 (GPLv2)
+#
+# Written by Daniel Vijge
+#
+# See file LICENSE for full license details
+
+use strict;
+
+use Slim::Utils::Prefs;
+use Slim::Utils::Log;
+use Slim::Utils::Cache;
+use Slim::Utils::Strings qw(string cstring);
+use JSON::XS::VersionOneAndTwo;
+use Plugins::SqueezeCloud::Random qw(random_regex);
+use Digest::SHA qw(sha256_base64);
+
+my $log = logger('plugin.squeezecloud');
+my $prefs = preferences('plugin.squeezecloud');
+my $cache = Slim::Utils::Cache->new();
+
+use constant CLIENT_ID => "112d35211af80d72c8ff470ab66400d8";
+use constant CLIENT_SECRET => "fc63200fee37d02bc3216cfeffe5f5ae";
+use constant REDIRECT_URI => "https%3A%2F%2Fdanielvijge.github.io%2FSqueezeCloud%2Fcallback.html";
+use constant META_CACHE_TTL => 86400 * 30; # 24 hours x 30 = 30 days
+
+sub isLoggedIn {
+ return(isRefreshTokenAvailable() || isApiKeyAvailable());
+}
+
+sub isApiKeyAvailable {
+ return ($prefs->get('apiKey') ne '');
+}
+
+sub isAccessTokenAvailable {
+ return ($cache->get('access_token') ne '');
+}
+
+sub isRefreshTokenAvailable {
+ return ($cache->get('refresh_token') ne '');
+}
+
+sub isAccessTokenExpired {
+ return 0 if isApiKeyAvailable(); # API key cannot expire
+ return 0 if isAccessTokenAvailable(); # Access token still valid
+ return 1 if isRefreshTokenAvailable(); # Access token expired, refresh token available
+ return 1; # This should not happen, equal to isLoggedIn() == false
+}
+
+sub getAccessToken {
+ $log->debug('getAccessToken started.');
+
+ if (!isRefreshTokenAvailable()) {
+ $log->error('No authentication available. Use the settings page to log in first.');
+ return;
+ }
+
+ if (!isAccessTokenAvailable()) {
+ $log->info('Access token has expired. Getting a new access token with the refresh token.');
+ getAccessTokenWithRefreshToken(\&getAccessToken, @_);
+ return;
+ }
+
+ $log->debug('Cached access token ' . $cache->get('access_token'));
+ return $cache->get('access_token');
+}
+
+sub getAuthorizationToken {
+ $log->debug('getAuthorizationToken started.');
+
+ my $code = shift;
+
+ if (!$cache->get('codeVerifier')) {
+ $log->error('No code verifier is available. Reload the page and try to authenticate again.');
+ return;
+ }
+
+ my $post = "grant_type=authorization_code" .
+ "&client_id=" . CLIENT_ID .
+ "&client_secret=" . CLIENT_SECRET .
+ "&redirect_uri=" . REDIRECT_URI .
+ "&code_verifier=" . $cache->get('codeVerifier') .
+ "&code=" . $code;
+
+ my $http = Slim::Networking::SimpleAsyncHTTP->new(
+ sub {
+ $log->debug('Successful request for authorization_code.');
+ my $response = shift;
+ my $result = eval { from_json($response->content) };
+
+ $cache->set('access_token', $result->{access_token}, 30);
+ $cache->set('refresh_token', $result->{refresh_token}, META_CACHE_TTL);
+ },
+ sub {
+ $log->error('Failed request for authorization_code.');
+ $log->error($_[1]);
+
+ my $response = shift;
+ my $result = eval { from_json($response->content) };
+ $log->error($result);
+ },
+ {
+ timeout => 15,
+ }
+ );
+ $log->debug($post);
+ $http->post(
+ "https://secure.soundcloud.com/oauth/token",
+ 'Content-Type' => 'application/x-www-form-urlencoded',
+ $post,
+ );
+}
+
+sub getAccessTokenWithRefreshToken {
+ $log->debug('getAccessTokenWithRefreshToken started.');
+
+ my $cb = shift;
+ my @params = @_;
+
+ if (!isRefreshTokenAvailable()) {
+ $log->error('No authentication available. Use the settings page to log in first.');
+ return;
+ }
+
+ if (isAccessTokenAvailable()) {
+ $log->debug('Still an access token available. No need for a refresh.');
+ return;
+ }
+
+ $log->debug('Cached refresh token ' . $cache->get('refresh_token'));
+ my $post = "grant_type=refresh_token" .
+ "&client_id=" . CLIENT_ID .
+ "&client_secret=" . CLIENT_SECRET .
+ "&refresh_token=" . $cache->get('refresh_token');
+
+ my $http = Slim::Networking::SimpleAsyncHTTP->new(
+ sub {
+ $log->debug('Successful request for refresh_token');
+ my $response = shift;
+ my $result = eval { from_json($response->content) };
+ $cache->set('access_token', $result->{access_token}, 30);
+ $cache->set('refresh_token', $result->{refresh_token}, META_CACHE_TTL);
+ $cb->(@params) if $cb;
+ },
+ sub {
+ $log->error('Failed request for refresh_token');
+ $log->error($_[1]);
+ $log->debug('Removing refresh_token for failed request. User is nog logged out.');
+ $cache->remove('refresh_token');
+ $cb->(@params) if $cb;
+ },
+ {
+ timeout => 15,
+ }
+ );
+ $log->debug($post);
+ $http->post(
+ "https://secure.soundcloud.com/oauth/token",
+ 'Content-Type' => 'application/x-www-form-urlencoded',
+ $post,
+ );
+}
+
+sub getAuthenticationHeaders {
+ $log->debug('getAuthenticationHeaders started.');
+ if (isApiKeyAvailable()) {
+ # If there is still an older API key, use this for authentication
+ $log->debug('Using old API key ' . $prefs->get('apiKey'));
+ return 'Authorization' => 'OAuth ' . $prefs->get('apiKey');
+ }
+ else {
+ $log->debug('Using bearer token for authorization');
+ return 'Authorization' => 'Bearer ' . getAccessToken();
+ }
+}
+
+sub getCodeChallenge {
+ $log->debug('getCodeChallenge started.');
+ if ($cache->get('codeChallenge')) {
+ $log->debug('Random string [cached]: '. $cache->get('codeVerifier'));
+ $log->debug('S256 [cached]: '.$cache->get('codeChallenge'));
+ return $cache->get('codeChallenge');
+ }
+
+ my $randomString = random_regex('[a-z0-9]{56}');
+ my $s256 = sha256_base64($randomString);
+ $s256 =~ s/\+/-/g;
+ $s256 =~ s/\//_/g;
+ $s256 =~ s/=$//g;
+
+ $log->debug('Random string: '.$randomString);
+ $log->debug('S256: '.$s256);
+
+ $cache->set('codeVerifier', $randomString, 60);
+ $cache->set('codeChallenge', $s256, 60);
+ return $s256;
+}
+
+1;
diff --git a/Plugin.pm b/Plugin.pm
index ac82eb1..7356636 100644
--- a/Plugin.pm
+++ b/Plugin.pm
@@ -29,7 +29,7 @@ use POSIX qw(strftime);
use Slim::Utils::Strings qw(string cstring);
use Slim::Utils::Prefs;
use Slim::Utils::Log;
-
+use Plugins::SqueezeCloud::Oauth2;
# Defines the timeout in seconds for a http request
use constant HTTP_TIMEOUT => 15;
@@ -154,11 +154,6 @@ sub defaultMeta {
$log->debug('defaultMeta ended.');
}
-sub getAuthenticationHeaders() {
- $log->debug('getAuthenticationHeaders started.');
- return 'Authorization' => 'OAuth ' . $prefs->get('apiKey');
-}
-
# Extracts the available metadata for a tracks from the JSON data. The data
# is cached and then returned to be presented to the user.
sub _makeMetadata {
@@ -346,9 +341,14 @@ sub likeTrack {
my $url = $method . "://api.soundcloud.com/likes/tracks/$id";
$log->debug("Liking: $url");
+ if (Plugins::SqueezeCloud::Oauth2::isAccessTokenExpired()) {
+ Plugins::SqueezeCloud::Oauth2::getAccessTokenWithRefreshToken(\&likeTrack, @_);
+ return;
+ }
+
my $fetch = sub {
my $ua = LWP::UserAgent->new;
- my $request = HTTP::Request::Common::POST($url, getAuthenticationHeaders());
+ my $request = HTTP::Request::Common::POST($url, Plugins::SqueezeCloud::Oauth2::getAuthenticationHeaders());
my $response = $ua->request($request);
if ( $response->is_success() ) {
@@ -376,10 +376,14 @@ sub unlikeTrack {
my $url = $method . "://api.soundcloud.com/likes/tracks/$id";
$log->debug("Unliking: $url");
-
+ if (Plugins::SqueezeCloud::Oauth2::isAccessTokenExpired()) {
+ Plugins::SqueezeCloud::Oauth2::getAccessTokenWithRefreshToken(\&unlikeTrack, @_);
+ return;
+ }
+
my $fetch = sub {
my $ua = LWP::UserAgent->new;
- my $request = HTTP::Request::Common::DELETE($url, getAuthenticationHeaders());
+ my $request = HTTP::Request::Common::DELETE($url, Plugins::SqueezeCloud::Oauth2::getAuthenticationHeaders());
my $response = $ua->request($request);
if ( $response->is_success() ) {
@@ -451,7 +455,7 @@ sub _gotMetadata {
requests_redirectable => [],
);
- my $res = $ua->get( getStreamURL($json), getAuthenticationHeaders() );
+ my $res = $ua->get( getStreamURL($json), Plugins::SqueezeCloud::Oauth2::getAuthenticationHeaders() );
my $stream = $res->header( 'location' );
@@ -487,6 +491,11 @@ sub fetchMetadata {
my $extras = "linked_partitioning=true&limit=1";
my $queryUrl = $method."://api.soundcloud.com/".$resource."?" . $extras . $params;
+ if (Plugins::SqueezeCloud::Oauth2::isAccessTokenExpired()) {
+ Plugins::SqueezeCloud::Oauth2::getAccessTokenWithRefreshToken(\&fetchMetadata, @_);
+ return;
+ }
+
# Call the server to fetch the data via the asynchronous http request.
# The methods are called when a response was received or an error
# occurred. Additional information to the http call is passed via
@@ -501,7 +510,7 @@ sub fetchMetadata {
},
);
- $http->get($queryUrl, getAuthenticationHeaders());
+ $http->get($queryUrl, Plugins::SqueezeCloud::Oauth2::getAuthenticationHeaders());
}
$log->debug('fetchMetadata ended.');
@@ -652,6 +661,11 @@ sub _getTracks {
$log->debug('_getTracks started.');
my ($client, $searchType, $index, $quantity, $queryUrl, $uid, $cursor, $parser, $callback, $menu) = @_;
+ if (Plugins::SqueezeCloud::Oauth2::isAccessTokenExpired()) {
+ Plugins::SqueezeCloud::Oauth2::getAccessTokenWithRefreshToken(\&_getTracks, @_);
+ return;
+ }
+
Slim::Networking::SimpleAsyncHTTP->new(
# Called when a response has been received for the request.
sub {
@@ -707,7 +721,7 @@ sub _getTracks {
$callback->([ { name => $_[1], type => 'text' } ]);
},
- )->get($queryUrl, getAuthenticationHeaders());
+ )->get($queryUrl, Plugins::SqueezeCloud::Oauth2::getAuthenticationHeaders());
$log->debug('_getTracks ended.');
}
@@ -788,6 +802,11 @@ sub urlHandler {
my $queryUrl = "https://api.soundcloud.com/resolve?url=$url";
$log->debug("fetching: $queryUrl");
+ if (Plugins::SqueezeCloud::Oauth2::isAccessTokenExpired()) {
+ Plugins::SqueezeCloud::Oauth2::getAccessTokenWithRefreshToken(\&urlHandler, @_);
+ return;
+ }
+
my $fetch = sub {
Slim::Networking::SimpleAsyncHTTP->new(
sub {
@@ -806,7 +825,7 @@ sub urlHandler {
$log->warn("error: $_[1]");
$callback->([ { name => $_[1], type => 'text' } ]);
},
- )->get($queryUrl, getAuthenticationHeaders());
+ )->get($queryUrl, Plugins::SqueezeCloud::Oauth2::getAuthenticationHeaders());
};
$fetch->();
@@ -867,9 +886,14 @@ sub likePlaylist {
my $url = $method . "://api.soundcloud.com/likes/playlists/$id";
$log->debug("Liking: $url");
+ if (Plugins::SqueezeCloud::Oauth2::isAccessTokenExpired()) {
+ Plugins::SqueezeCloud::Oauth2::getAccessTokenWithRefreshToken(\&likePlaylist, @_);
+ return;
+ }
+
my $fetch = sub {
my $ua = LWP::UserAgent->new;
- my $request = HTTP::Request::Common::POST($url, getAuthenticationHeaders());
+ my $request = HTTP::Request::Common::POST($url, Plugins::SqueezeCloud::Oauth2::getAuthenticationHeaders());
my $response = $ua->request($request);
if ( $response->is_success() ) {
@@ -897,10 +921,14 @@ sub unlikePlaylist {
my $url = $method . "://api.soundcloud.com/likes/playlists/$id";
$log->debug("Unliking: $url");
+ if (Plugins::SqueezeCloud::Oauth2::isAccessTokenExpired()) {
+ Plugins::SqueezeCloud::Oauth2::getAccessTokenWithRefreshToken(\&unlikePlaylist, @_);
+ return;
+ }
my $fetch = sub {
my $ua = LWP::UserAgent->new;
- my $request = HTTP::Request::Common::DELETE($url, getAuthenticationHeaders());
+ my $request = HTTP::Request::Common::DELETE($url, Plugins::SqueezeCloud::Oauth2::getAuthenticationHeaders());
my $response = $ua->request($request);
if ( $response->is_success() ) {
@@ -1070,9 +1098,14 @@ sub followFriend {
my $url = $method . "://api.soundcloud.com/me/followings/$uid";
$log->debug("Following: $url");
+ if (Plugins::SqueezeCloud::Oauth2::isAccessTokenExpired()) {
+ Plugins::SqueezeCloud::Oauth2::getAccessTokenWithRefreshToken(\&followFriend, @_);
+ return;
+ }
+
my $fetch = sub {
my $ua = LWP::UserAgent->new;
- my $request = HTTP::Request::Common::POST($url, getAuthenticationHeaders());
+ my $request = HTTP::Request::Common::POST($url, Plugins::SqueezeCloud::Oauth2::getAuthenticationHeaders());
my $response = $ua->request($request);
if ( $response->is_success() ) {
@@ -1099,10 +1132,14 @@ sub unfollowFriend {
my $url = $method . "://api.soundcloud.com/me/followings/$uid";
$log->debug("Unfollowing: $url");
-
+ if (Plugins::SqueezeCloud::Oauth2::isAccessTokenExpired()) {
+ Plugins::SqueezeCloud::Oauth2::getAccessTokenWithRefreshToken(\&unfollowFriend, @_);
+ return;
+ }
+
my $fetch = sub {
my $ua = LWP::UserAgent->new;
- my $request = HTTP::Request::Common::DELETE($url, getAuthenticationHeaders());
+ my $request = HTTP::Request::Common::DELETE($url, Plugins::SqueezeCloud::Oauth2::getAuthenticationHeaders());
my $response = $ua->request($request);
if ( $response->is_success() ) {
@@ -1249,7 +1286,7 @@ sub playerMenu { shift->can('nonSNApps') ? undef : 'RADIO' }
sub toplevel {
$log->debug('toplevel started.');
my ($client, $callback, $args) = @_;
-
+
# These are the available main menus. The variable type defines the menu
# type (search allows text input, link opens another menu), the url defines
# the method that shall be called when the user has selected the menu entry.
@@ -1257,8 +1294,8 @@ sub toplevel {
# method defined by the url variable.
my $callbacks = [];
- # Add the following menu items only when the user has specified an API key
- if ($prefs->get('apiKey') ne '') {
+ # Add the following menu items only when the user is logged in
+ if (Plugins::SqueezeCloud::Oauth2::isLoggedIn()) {
# Menu entry to show all activities (Stream)
push(@$callbacks,
@@ -1336,9 +1373,10 @@ sub toplevel {
push(@$callbacks,
{ name => string('PLUGIN_SQUEEZECLOUD_URL'), type => 'search', url => \&urlHandler, }
);
+
} else {
push(@$callbacks,
- { name => string('PLUGIN_SQUEEZECLOUD_SET_API_KEY'), type => 'text' }
+ { name => string('PLUGIN_SQUEEZECLOUD_LOGIN'), type => 'text' }
);
}
diff --git a/ProtocolHandler.pm b/ProtocolHandler.pm
index fb1f9da..75a13e5 100644
--- a/ProtocolHandler.pm
+++ b/ProtocolHandler.pm
@@ -28,6 +28,7 @@ use Slim::Utils::Prefs;
use Slim::Utils::Errno;
use Slim::Utils::Cache;
use Scalar::Util qw(blessed);
+use Plugins::SqueezeCloud::Oauth2;
my $log = logger('plugin.squeezecloud');
@@ -55,10 +56,6 @@ my $prefix = 'sc:';
sub canSeek { 0 }
-sub getAuthenticationHeaders() {
- return 'Authorization' => 'OAuth ' . $prefs->get('apiKey');
-}
-
sub _makeMetadata {
my ($json) = shift;
@@ -154,7 +151,7 @@ sub gotNextTrack {
requests_redirectable => [],
);
- my $res = $ua->get($stream, getAuthenticationHeaders() );
+ my $res = $ua->get($stream, Plugins::SqueezeCloud::Oauth2::getAuthenticationHeaders() );
my $redirector = $res->header( 'location' );
@@ -195,6 +192,11 @@ sub getNextTrack {
# Talk to SN and get the next track to play
my $trackURL = "https://api.soundcloud.com/tracks/" . $id;
+
+ if (Plugins::SqueezeCloud::Oauth2::isAccessTokenExpired()) {
+ Plugins::SqueezeCloud::Oauth2::getAccessTokenWithRefreshToken(\&getNextTrack, @_);
+ return;
+ }
my $http = Slim::Networking::SimpleAsyncHTTP->new(
\&gotNextTrack,
@@ -210,7 +212,7 @@ sub getNextTrack {
main::DEBUGLOG && $log->is_debug && $log->debug("Getting track from soundcloud for $id");
- $http->get( $trackURL, getAuthenticationHeaders() );
+ $http->get( $trackURL, Plugins::SqueezeCloud::Oauth2::getAuthenticationHeaders() );
}
# To support remote streaming (synced players, slimp3/SB1), we need to subclass Protocols::HTTP
diff --git a/Random.pm b/Random.pm
new file mode 100644
index 0000000..a30b2f8
--- /dev/null
+++ b/Random.pm
@@ -0,0 +1,559 @@
+# String::Random - Generates a random string from a pattern
+# Copyright (C) 1999-2006 Steven Pritchard
+#
+# This program is free software; you can redistribute it
+# and/or modify it under the same terms as Perl itself.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+#
+# $Id: Random.pm,v 1.4 2006/09/21 17:34:07 steve Exp $
+
+package Plugins::SqueezeCloud::Random;
+
+require 5.006_001;
+
+use strict;
+use warnings;
+
+use Carp;
+use parent qw(Exporter);
+
+our %EXPORT_TAGS = (
+ 'all' => [
+ qw(
+ &random_string
+ &random_regex
+ )
+ ]
+);
+our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
+
+# These are the various character sets.
+my @upper = ( 'A' .. 'Z' );
+my @lower = ( 'a' .. 'z' );
+my @digit = ( '0' .. '9' );
+my @punct = map {chr} ( 33 .. 47, 58 .. 64, 91 .. 96, 123 .. 126 );
+my @any = ( @upper, @lower, @digit, @punct );
+my @salt = ( @upper, @lower, @digit, '.', '/' );
+my @binary = map {chr} ( 0 .. 255 );
+
+# What's important is how they relate to the pattern characters.
+# These are the old patterns for randpattern/random_string.
+my %old_patterns = (
+ 'C' => [@upper],
+ 'c' => [@lower],
+ 'n' => [@digit],
+ '!' => [@punct],
+ '.' => [@any],
+ 's' => [@salt],
+ 'b' => [@binary],
+);
+
+# These are the regex-based patterns.
+my %patterns = (
+
+ # These are the regex-equivalents.
+ '.' => [@any],
+ '\d' => [@digit],
+ '\D' => [ @upper, @lower, @punct ],
+ '\w' => [ @upper, @lower, @digit, '_' ],
+ '\W' => [ grep { $_ ne '_' } @punct ],
+ '\s' => [ q{ }, "\t" ], # Would anything else make sense?
+ '\S' => [ @upper, @lower, @digit, @punct ],
+
+ # These are translated to their double quoted equivalents.
+ '\t' => ["\t"],
+ '\n' => ["\n"],
+ '\r' => ["\r"],
+ '\f' => ["\f"],
+ '\a' => ["\a"],
+ '\e' => ["\e"],
+);
+
+# This is used for cache of parsed range patterns in %regch
+my %parsed_range_patterns = ();
+
+# These characters are treated specially in randregex().
+my %regch = (
+ '\\' => sub {
+ my ( $self, $ch, $chars, $string ) = @_;
+ if ( @{$chars} ) {
+ my $tmp = shift( @{$chars} );
+ if ( $tmp eq 'x' ) {
+
+ # This is supposed to be a number in hex, so
+ # there had better be at least 2 characters left.
+ $tmp = shift( @{$chars} ) . shift( @{$chars} );
+ push( @{$string}, [ chr( hex($tmp) ) ] );
+ }
+ elsif ( $tmp =~ /[0-7]/ ) {
+ carp 'octal parsing not implemented. treating literally.';
+ push( @{$string}, [$tmp] );
+ }
+ elsif ( defined( $patterns{"\\$tmp"} ) ) {
+ $ch .= $tmp;
+ push( @{$string}, $patterns{$ch} );
+ }
+ else {
+ if ( $tmp =~ /\w/ ) {
+ carp "'\\$tmp' being treated as literal '$tmp'";
+ }
+ push( @{$string}, [$tmp] );
+ }
+ }
+ else {
+ croak 'regex not terminated';
+ }
+ },
+ '.' => sub {
+ my ( $self, $ch, $chars, $string ) = @_;
+ push( @{$string}, $patterns{$ch} );
+ },
+ '[' => sub {
+ my ( $self, $ch, $chars, $string ) = @_;
+ my @tmp;
+ while ( defined( $ch = shift( @{$chars} ) ) && ( $ch ne ']' ) ) {
+ if ( ( $ch eq '-' ) && @{$chars} && @tmp ) {
+ my $begin_ch = $tmp[-1];
+ $ch = shift( @{$chars} );
+ my $key = "$begin_ch-$ch";
+ if ( defined( $parsed_range_patterns{$key} ) ) {
+ push( @tmp, @{ $parsed_range_patterns{$key} } );
+ }
+ else {
+ my @chs;
+ for my $n ( ( ord($begin_ch) + 1 ) .. ord($ch) ) {
+ push @chs, chr($n);
+ }
+ $parsed_range_patterns{$key} = \@chs;
+ push @tmp, @chs;
+ }
+ }
+ else {
+ carp "'$ch' will be treated literally inside []"
+ if ( $ch =~ /\W/ );
+ push( @tmp, $ch );
+ }
+ }
+ croak 'unmatched []' if ( $ch ne ']' );
+ push( @{$string}, \@tmp );
+ },
+ '*' => sub {
+ my ( $self, $ch, $chars, $string ) = @_;
+ unshift( @{$chars}, split( //, '{0,}' ) );
+ },
+ '+' => sub {
+ my ( $self, $ch, $chars, $string ) = @_;
+ unshift( @{$chars}, split( //, '{1,}' ) );
+ },
+ '?' => sub {
+ my ( $self, $ch, $chars, $string ) = @_;
+ unshift( @{$chars}, split( //, '{0,1}' ) );
+ },
+ '{' => sub {
+ my ( $self, $ch, $chars, $string ) = @_;
+ my $closed;
+ CLOSED:
+ for my $c ( @{$chars} ) {
+ if ( $c eq '}' ) {
+ $closed = 1;
+ last CLOSED;
+ }
+ }
+ if ($closed) {
+ my $tmp;
+ while ( defined( $ch = shift( @{$chars} ) ) && ( $ch ne '}' ) ) {
+ croak "'$ch' inside {} not supported" if ( $ch !~ /[\d,]/ );
+ $tmp .= $ch;
+ }
+ if ( $tmp =~ /,/ ) {
+ if ( my ( $min, $max ) = $tmp =~ /^(\d*),(\d*)$/ ) {
+ if ( !length($min) ) { $min = 0 }
+ if ( !length($max) ) { $max = $self->{'_max'} }
+ croak "bad range {$tmp}" if ( $min > $max );
+ if ( $min == $max ) {
+ $tmp = $min;
+ }
+ else {
+ $tmp = $min + $self->{'_rand'}( $max - $min + 1 );
+ }
+ }
+ else {
+ croak "malformed range {$tmp}";
+ }
+ }
+ if ($tmp) {
+ my $prev_ch = $string->[-1];
+
+ push @{$string}, ( ($prev_ch) x ( $tmp - 1 ) );
+ }
+ else {
+ pop( @{$string} );
+ }
+ }
+ else {
+ # { isn't closed, so treat it literally.
+ push( @{$string}, [$ch] );
+ }
+ },
+);
+
+# Default rand function
+sub _rand {
+ my ($max) = @_;
+ return int rand $max;
+}
+
+sub new {
+ my ( $proto, @args ) = @_;
+ my $class = ref($proto) || $proto;
+ my $self;
+ $self = {%old_patterns}; # makes $self refer to a copy of %old_patterns
+ my %args = ();
+ if (@args) { %args = @args }
+ if ( defined( $args{'max'} ) ) {
+ $self->{'_max'} = $args{'max'};
+ }
+ else {
+ $self->{'_max'} = 10;
+ }
+ if ( defined( $args{'rand_gen'} ) ) {
+ $self->{'_rand'} = $args{'rand_gen'};
+ }
+ else {
+ $self->{'_rand'} = \&_rand;
+ }
+ return bless( $self, $class );
+}
+
+# Returns a random string for each regular expression given as an
+# argument, or the strings concatenated when used in a scalar context.
+sub randregex {
+ my $self = shift;
+ croak 'called without a reference' if ( !ref($self) );
+
+ my @strings = ();
+
+ while ( defined( my $pattern = shift ) ) {
+ my $ch;
+ my @string = ();
+ my $string = q{};
+
+ # Split the characters in the pattern
+ # up into a list for easier parsing.
+ my @chars = split( //, $pattern );
+
+ while ( defined( $ch = shift(@chars) ) ) {
+ if ( defined( $regch{$ch} ) ) {
+ $regch{$ch}->( $self, $ch, \@chars, \@string );
+ }
+ elsif ( $ch =~ /[\$\^\*\(\)\+\{\}\]\|\?]/ ) {
+
+ # At least some of these probably should have special meaning.
+ carp "'$ch' not implemented. treating literally.";
+ push( @string, [$ch] );
+ }
+ else {
+ push( @string, [$ch] );
+ }
+ }
+
+ foreach my $ch (@string) {
+ $string .= $ch->[ $self->{'_rand'}( scalar( @{$ch} ) ) ];
+ }
+
+ push( @strings, $string );
+ }
+
+ return wantarray ? @strings : join( q{}, @strings );
+}
+
+# For compatibility with an ancient version, please ignore...
+sub from_pattern {
+ my ( $self, @args ) = @_;
+ croak 'called without a reference' if ( !ref($self) );
+
+ return $self->randpattern(@args);
+}
+
+sub randpattern {
+ my $self = shift;
+ croak 'called without a reference' if ( !ref($self) );
+
+ my @strings = ();
+
+ while ( defined( my $pattern = shift ) ) {
+ my $string = q{};
+
+ for my $ch ( split( //, $pattern ) ) {
+ if ( defined( $self->{$ch} ) ) {
+ $string .= $self->{$ch}
+ ->[ $self->{'_rand'}( scalar( @{ $self->{$ch} } ) ) ];
+ }
+ else {
+ croak qq(Unknown pattern character "$ch"!);
+ }
+ }
+ push( @strings, $string );
+ }
+
+ return wantarray ? @strings : join( q{}, @strings );
+}
+
+sub get_pattern {
+ my ( $self, $name ) = @_;
+ return $self->{ $name };
+}
+
+sub set_pattern {
+ my ( $self, $name, $charset ) = @_;
+ $self->{ $name } = $charset;
+}
+
+sub random_regex {
+ my (@args) = @_;
+ my $foo = Plugins::SqueezeCloud::Random->new;
+ return $foo->randregex(@args);
+}
+
+sub random_string {
+ my ( $pattern, @list ) = @_;
+
+ my $foo = Plugins::SqueezeCloud->new;
+
+ for my $n ( 0 .. $#list ) {
+ $foo->{$n} = [ @{ $list[$n] } ];
+ }
+
+ return $foo->randpattern($pattern);
+}
+
+1;
+__END__
+
+=encoding utf8
+
+=head1 NAME
+
+String::Random - Perl module to generate random strings based on a pattern
+
+=head1 SYNOPSIS
+
+ use String::Random;
+ my $string_gen = String::Random->new;
+ print $string_gen->randregex('\d\d\d'); # Prints 3 random digits
+ # Prints 3 random printable characters
+ print $string_gen->randpattern("...");
+
+I
+
+ use String::Random qw(random_regex random_string);
+ print random_regex('\d\d\d'); # Also prints 3 random digits
+ print random_string("..."); # Also prints 3 random printable characters
+
+=head1 DESCRIPTION
+
+This module makes it trivial to generate random strings.
+
+As an example, let's say you are writing a script that needs to generate a
+random password for a user. The relevant code might look something like
+this:
+
+ use String::Random;
+ my $pass = String::Random->new;
+ print "Your password is ", $pass->randpattern("CCcc!ccn"), "\n";
+
+This would output something like this:
+
+ Your password is UDwp$tj5
+
+B: currently, C defaults to Perl's built-in predictable
+random number generator so the passwords generated by it are insecure. See the
+C option to C constructor to specify a more secure
+random number generator. There is no equivalent to this in the procedural
+interface, you must use the object-oriented interface to get this
+functionality.
+
+If you are more comfortable dealing with regular expressions, the following
+code would have a similar result:
+
+ use String::Random;
+ my $pass = String::Random->new;
+ print "Your password is ",
+ $pass->randregex('[A-Z]{2}[a-z]{2}.[a-z]{2}\d'), "\n";
+
+=head2 Patterns
+
+The pre-defined patterns (for use with C and C)
+are as follows:
+
+ c Any Latin lowercase character [a-z]
+ C Any Latin uppercase character [A-Z]
+ n Any digit [0-9]
+ ! A punctuation character [~`!@$%^&*()-_+={}[]|\:;"'.<>?/#,]
+ . Any of the above
+ s A "salt" character [A-Za-z0-9./]
+ b Any binary data
+
+These can be modified, but if you need a different pattern it is better to
+create another pattern, possibly using one of the pre-defined as a base.
+For example, if you wanted a pattern C that contained all upper and lower
+case letters (C<[A-Za-z]>), the following would work:
+
+ my $gen = String::Random->new;
+ $gen->{'A'} = [ 'A'..'Z', 'a'..'z' ];
+
+I
+
+ my $gen = String::Random->new;
+ $gen->{'A'} = [ @{$gen->{'C'}}, @{$gen->{'c'}} ];
+
+I
+
+ my $gen = String::Random->new;
+ $gen->set_pattern(A => [ 'A'..'Z', 'a'..'z' ]);
+
+The random_string function, described below, has an alternative interface
+for adding patterns.
+
+=head2 Methods
+
+=over 8
+
+=item new
+
+=item new max =E I
+
+=item new rand_gen =E I
+
+Create a new String::Random object.
+
+Optionally a parameter C can be included to specify the maximum number
+of characters to return for C<*> and other regular expression patterns that
+do not return a fixed number of characters.
+
+Optionally a parameter C can be included to specify a subroutine
+coderef for generating the random numbers used in this module. The coderef
+must accept one argument C and return an integer between 0 and C.
+The default rand_gen coderef is
+
+ sub {
+ my ($max) = @_;
+ return int rand $max;
+ }
+
+=item randpattern LIST
+
+The randpattern method returns a random string based on the concatenation
+of all the pattern strings in the list.
+
+It will return a list of random strings corresponding to the pattern
+strings when used in list context.
+
+=item randregex LIST
+
+The randregex method returns a random string that will match the regular
+expression passed in the list argument.
+
+Please note that the arguments to randregex are not real regular
+expressions. Only a small subset of regular expression syntax is actually
+supported. So far, the following regular expression elements are
+supported:
+
+ \w Alphanumeric + "_".
+ \d Digits.
+ \W Printable characters other than those in \w.
+ \D Printable characters other than those in \d.
+ . Printable characters.
+ [] Character classes.
+ {} Repetition.
+ * Same as {0,}.
+ ? Same as {0,1}.
+ + Same as {1,}.
+
+Regular expression support is still somewhat incomplete. Currently special
+characters inside [] are not supported (with the exception of "-" to denote
+ranges of characters). The parser doesn't care for spaces in the "regular
+expression" either.
+
+=item get_pattern STRING
+
+Return a pattern given a name.
+
+ my $gen = String::Random->new;
+ $gen->get_pattern('C');
+
+(Added in version 0.32.)
+
+=item set_pattern STRING ARRAYREF
+
+Add or redefine a pattern given a name and a character set.
+
+ my $gen = String::Random->new;
+ $gen->set_pattern(A => [ 'A'..'Z', 'a'..'z' ]);
+
+(Added in version 0.32.)
+
+=item from_pattern
+
+B - for compatibility with an old version. B
+
+=back
+
+=head2 Functions
+
+=over 8
+
+=item random_string PATTERN,LIST
+
+=item random_string PATTERN
+
+When called with a single scalar argument, random_string returns a random
+string using that scalar as a pattern. Optionally, references to lists
+containing other patterns can be passed to the function. Those lists will
+be used for 0 through 9 in the pattern (meaning the maximum number of lists
+that can be passed is 10). For example, the following code:
+
+ print random_string("0101",
+ ["a", "b", "c"],
+ ["d", "e", "f"]), "\n";
+
+would print something like this:
+
+ cebd
+
+=item random_regex REGEX_IN_STRING
+
+Prints a string for the regular expression given as the string. See the
+synposis for example.
+
+=back
+
+=head1 BUGS
+
+This is Bug Free™ code. (At least until somebody finds one…)
+
+Please report bugs here:
+
+L .
+
+=head1 AUTHOR
+
+Original Author: Steven Pritchard C<< steve@silug.org >>
+
+Now maintained by: Shlomi Fish ( L ).
+
+=head1 LICENSE
+
+This program is free software; you can redistribute it and/or modify it
+under the same terms as Perl itself.
+
+=head1 SEE ALSO
+
+perl(1).
+
+=cut
+
+# vi: set ai et:
diff --git a/Settings.pm b/Settings.pm
index 3e337dd..de7ae98 100644
--- a/Settings.pm
+++ b/Settings.pm
@@ -13,6 +13,14 @@ use strict;
use base qw(Slim::Web::Settings);
use Slim::Utils::Prefs;
+use Slim::Utils::Log;
+use Slim::Utils::Cache;
+
+use JSON::XS::VersionOneAndTwo;
+
+my $log = logger('plugin.squeezecloud');
+my $prefs = preferences('plugin.squeezecloud');
+my $cache = Slim::Utils::Cache->new();
# Returns the name of the plugin. The real
# string is specified in the strings.txt file.
@@ -32,5 +40,59 @@ sub prefs {
return (preferences('plugin.squeezecloud'), qw(apiKey playmethod));
}
+sub handler {
+ my ($class, $client, $params, $callback, @args) = @_;
+
+ if ($params->{code} && $params->{code} ne '') {
+ $log->debug('Getting access token and refresh token from code');
+ Plugins::SqueezeCloud::Oauth2::getAuthorizationToken($params->{code});
+ }
+ elsif ($params->{logout}) {
+ $log->debug('Logging out...');
+ $cache->remove('refresh_token');
+ $cache->remove('access_token');
+ $prefs->remove('apiKey');
+ }
+ elsif (!$cache->get('refresh_token')) {
+ $log->debug('Generating code and code challange');
+ my $codeChallenge = Plugins::SqueezeCloud::Oauth2::getCodeChallenge;
+ $params->{codeChallenge} = $codeChallenge;
+ $params->{hostName} = Slim::Utils::Misc::getLibraryName();
+ }
+
+ my $http = Slim::Networking::SimpleAsyncHTTP->new(
+ sub {
+ $log->debug('Successful request for user info.');
+ my $response = shift;
+ my $result = eval { from_json($response->content) };
+ $log->debug("User name: " . $result->{username});
+ $params->{username} = $result->{username};
+
+ $callback->($client, $params, $class->SUPER::handler($client, $params), @args);
+ },
+ sub {
+ $log->error('Failed request for user info.');
+ $log->error($_[1]);
+ $callback->($client, $params, $class->SUPER::handler($client, $params), @args);
+ },
+ {
+ timeout => 15,
+ }
+ );
+
+ if (Plugins::SqueezeCloud::Oauth2::isLoggedIn()) {
+
+ if (Plugins::SqueezeCloud::Oauth2::isAccessTokenExpired()) {
+ Plugins::SqueezeCloud::Oauth2::getAccessTokenWithRefreshToken(\&handler, @_);
+ return;
+ }
+
+ $http->get("https://api.soundcloud.com/me", Plugins::SqueezeCloud::Oauth2::getAuthenticationHeaders());
+ }
+ else {
+ $callback->($client, $params, $class->SUPER::handler($client, $params), @args);
+ }
+}
+
# Always end with a 1 to make Perl happy
1;
diff --git a/strings.txt b/strings.txt
index 6e13ac8..b3ca9a7 100644
--- a/strings.txt
+++ b/strings.txt
@@ -49,11 +49,11 @@ PLUGIN_SQUEEZECLOUD_PLAYLIST_SEARCH
PLUGIN_SQUEEZECLOUD_PLAYLIST_BROWSE
EN Playlists
-PLUGIN_SQUEEZECLOUD_APIKEY
- EN Soundcloud API Key
+PLUGIN_SQUEEZECLOUD_CODE
+ EN Soundcloud authorization code
-PLUGIN_SQUEEZECLOUD_APIKEY_DESC
- EN Paste your API key here, then save the settings
+PLUGIN_SQUEEZECLOUD_CODE_DESC
+ EN Paste your autorization code after clicking the button above, then press Save
PLUGIN_SQUEEZECLOUD_CONNECT_WITH_SOUNDCLOUD
EN Login to SoundCloud
@@ -88,8 +88,26 @@ PLUGIN_SQUEEZECLOUD_NO_INFO
PLUGIN_SQUEEZECLOUD_ERROR
EN SoundCloud error
-PLUGIN_SQUEEZECLOUD_SET_API_KEY
- EN Set your SoundCloud API key in Advanced settings
+PLUGIN_SQUEEZECLOUD_LOGIN
+ EN Log in to SoundCloud in Advanced settings
+
+PLUGIN_SQUEEZECLOUD_LOGGING_IN
+ EN Logging you in... (should be good. Press Apply again, or refresh this page to see the result.)
+
+PLUGIN_SQUEEZECLOUD_LOGGED_IN_AS
+ EN Logged in as
+
+PLUGIN_SQUEEZECLOUD_LOG_OUT
+ EN Log out
+
+PLUGIN_SQUEEZECLOUD_LOGIN_ERROR
+ EN Something went wrong. Try to refresh the page.
+
+PLUGIN_SQUEEZECLOUD_USE_STREAM_METHOD
+ EN Always use stream method
+
+PLUGIN_SQUEEZECLOUD_USE_DOWNLOAD_METHOD
+ EN Use download method if available
PLUGIN_SQUEEZECLOUD_FOLLOW
EN Follow