This commit is contained in:
@@ -11,7 +11,8 @@ use MIME::Base64 qw(decode_base64);
|
|||||||
|
|
||||||
use constant API_URL => 'https://music.youtube.com/youtubei/v1/';
|
use constant API_URL => 'https://music.youtube.com/youtubei/v1/';
|
||||||
use constant DEFAULT_CACHE_TTL => 24 * 3600;
|
use constant DEFAULT_CACHE_TTL => 24 * 3600;
|
||||||
use constant DEFAULT_API_KEY => 'QUl6YVN5Qi1wd1B0RGt4RjZKUW1BOHFxOWgxbWQ2ME15STVRNWlB';
|
use constant DEFAULT_API_KEY =>
|
||||||
|
'QUl6YVN5Qi1wd1B0RGt4RjZKUW1BOHFxOWgxbWQ2ME15STVRNWlB';
|
||||||
|
|
||||||
use Slim::Utils::Cache;
|
use Slim::Utils::Cache;
|
||||||
use Slim::Utils::Log;
|
use Slim::Utils::Log;
|
||||||
@@ -57,17 +58,23 @@ sub getCategories {
|
|||||||
sub getVideoDetails {
|
sub getVideoDetails {
|
||||||
my ( $class, $cb, $ids ) = @_;
|
my ( $class, $cb, $ids ) = @_;
|
||||||
|
|
||||||
_call('videos', {
|
_call(
|
||||||
|
'videos',
|
||||||
|
{
|
||||||
part => 'snippet,contentDetails',
|
part => 'snippet,contentDetails',
|
||||||
id => $ids,
|
id => $ids,
|
||||||
|
|
||||||
# cache video details a bit longer
|
# cache video details a bit longer
|
||||||
_cache_ttl => 7 * 86400,
|
_cache_ttl => 7 * 86400,
|
||||||
}, $cb);
|
},
|
||||||
|
$cb
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
sub _pagedCall {
|
sub _pagedCall {
|
||||||
my ( $method, $args, $cb ) = @_;
|
my ( $method, $args, $cb ) = @_;
|
||||||
my $wantedItems = $args->{_quantity} || $prefs->get('max_items');
|
my $wantedItems = $args->{_quantity} || $prefs->get('max_items');
|
||||||
|
|
||||||
# ignore $args->{_index} and let LMS handle offset
|
# ignore $args->{_index} and let LMS handle offset
|
||||||
my $wantedIndex = 0;
|
my $wantedIndex = 0;
|
||||||
my @items;
|
my @items;
|
||||||
@@ -78,16 +85,27 @@ sub _pagedCall {
|
|||||||
$wantedItems += $args->{_index} || 0;
|
$wantedItems += $args->{_index} || 0;
|
||||||
|
|
||||||
main::INFOLOG && $log->info("Searching by [$args->{order}]");
|
main::INFOLOG && $log->info("Searching by [$args->{order}]");
|
||||||
main::INFOLOG && $log->info("Querying [$args->{_quantity}] from [$args->{_index}] to [", $wantedItems-1, "] using [$method]");
|
main::INFOLOG && $log->info(
|
||||||
|
"Querying [$args->{_quantity}] from [$args->{_index}] to [",
|
||||||
|
$wantedItems - 1,
|
||||||
|
"] using [$method]"
|
||||||
|
);
|
||||||
|
|
||||||
# doing a search with a display order, so need to give precedence to 'query_size'
|
# doing a search with a display order, so need to give precedence to 'query_size'
|
||||||
if ($prefs->get('search_sort') || $prefs->get('channel_sort') || $prefs->get('playlist_sort')) {
|
if ( $prefs->get('search_sort')
|
||||||
$wantedItems = (int(($wantedItems - 1) / $prefs->get('query_size')) + 1) * $prefs->get('query_size');
|
|| $prefs->get('channel_sort')
|
||||||
main::INFOLOG && $log->info("Stretching quantity to [$wantedItems] due to sorting");
|
|| $prefs->get('playlist_sort') )
|
||||||
|
{
|
||||||
|
$wantedItems =
|
||||||
|
( int( ( $wantedItems - 1 ) / $prefs->get('query_size') ) + 1 ) *
|
||||||
|
$prefs->get('query_size');
|
||||||
|
main::INFOLOG
|
||||||
|
&& $log->info("Stretching quantity to [$wantedItems] due to sorting");
|
||||||
}
|
}
|
||||||
|
|
||||||
# that the maximum we'll get anyway
|
# that the maximum we'll get anyway
|
||||||
$wantedItems = $prefs->get('max_items') if $wantedItems > $prefs->get('max_items');
|
$wantedItems = $prefs->get('max_items')
|
||||||
|
if $wantedItems > $prefs->get('max_items');
|
||||||
|
|
||||||
$pagingCb = sub {
|
$pagingCb = sub {
|
||||||
my $results = shift;
|
my $results = shift;
|
||||||
@@ -101,16 +119,30 @@ sub _pagedCall {
|
|||||||
push @items, @{ $results->{items} };
|
push @items, @{ $results->{items} };
|
||||||
$pageIndex += scalar @{ $results->{items} };
|
$pageIndex += scalar @{ $results->{items} };
|
||||||
|
|
||||||
main::INFOLOG && $log->info("Want $wantedItems items from offset ", $wantedIndex, ", have " . scalar @items . " so far [acquired $pageIndex]");
|
main::INFOLOG && $log->info( "Want $wantedItems items from offset ",
|
||||||
|
$wantedIndex,
|
||||||
|
", have " . scalar @items . " so far [acquired $pageIndex]" );
|
||||||
|
|
||||||
if ( @items < $wantedItems && $results->{nextPageToken} ) {
|
if ( @items < $wantedItems && $results->{nextPageToken} ) {
|
||||||
$args->{pageToken} = $results->{nextPageToken};
|
$args->{pageToken} = $results->{nextPageToken};
|
||||||
main::INFOLOG && $log->info("Get next page using token " . $args->{pageToken});
|
main::INFOLOG
|
||||||
|
&& $log->info(
|
||||||
|
"Get next page using token " . $args->{pageToken} );
|
||||||
_call( $method, $args, $pagingCb );
|
_call( $method, $args, $pagingCb );
|
||||||
} else {
|
}
|
||||||
my $total = min($results->{'pageInfo'}->{'totalResults'} || $pageIndex, $prefs->get('max_items'));
|
else {
|
||||||
main::INFOLOG && $log->info("Got all we wanted, return " . scalar @items . "/$total. (YT total ", $results->{'pageInfo'}->{'totalResults'} || 'N/A', ")");
|
my $total =
|
||||||
$cb->( { items => \@items, offset => $wantedIndex, total => $total } );
|
min( $results->{'pageInfo'}->{'totalResults'} || $pageIndex,
|
||||||
|
$prefs->get('max_items') );
|
||||||
|
main::INFOLOG && $log->info(
|
||||||
|
"Got all we wanted, return "
|
||||||
|
. scalar @items
|
||||||
|
. "/$total. (YT total ",
|
||||||
|
$results->{'pageInfo'}->{'totalResults'} || 'N/A',
|
||||||
|
")"
|
||||||
|
);
|
||||||
|
$cb->(
|
||||||
|
{ items => \@items, offset => $wantedIndex, total => $total } );
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -120,7 +152,8 @@ sub _pagedCall {
|
|||||||
sub _call {
|
sub _call {
|
||||||
my ( $method, $args, $cb ) = @_;
|
my ( $method, $args, $cb ) = @_;
|
||||||
|
|
||||||
my $API_KEY = $prefs->get('APIkey') || MIME::Base64::decode_base64(DEFAULT_API_KEY);
|
my $API_KEY =
|
||||||
|
$prefs->get('APIkey') || MIME::Base64::decode_base64(DEFAULT_API_KEY);
|
||||||
my $url = '?' . ( $args->{_noKey} ? '' : 'key=' . $API_KEY . '&' );
|
my $url = '?' . ( $args->{_noKey} ? '' : 'key=' . $API_KEY . '&' );
|
||||||
|
|
||||||
$args->{regionCode} ||= $prefs->get('country') unless $args->{_noRegion};
|
$args->{regionCode} ||= $prefs->get('country') unless $args->{_noRegion};
|
||||||
@@ -129,7 +162,11 @@ sub _call {
|
|||||||
|
|
||||||
for my $k ( sort keys %{$args} ) {
|
for my $k ( sort keys %{$args} ) {
|
||||||
next if $k =~ /^_/;
|
next if $k =~ /^_/;
|
||||||
$url .= $k . '=' . URI::Escape::uri_escape_utf8( Encode::decode( 'utf8', $args->{$k} ) ) . '&';
|
$url .=
|
||||||
|
$k . '='
|
||||||
|
. URI::Escape::uri_escape_utf8(
|
||||||
|
Encode::decode( 'utf8', $args->{$k} ) )
|
||||||
|
. '&';
|
||||||
}
|
}
|
||||||
|
|
||||||
$url =~ s/&$//;
|
$url =~ s/&$//;
|
||||||
@@ -143,7 +180,11 @@ sub _call {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
main::INFOLOG && $log->info("Calling API (will cache for ", $args->{_cache_ttl} || DEFAULT_CACHE_TTL, "s): $url");
|
main::INFOLOG && $log->info(
|
||||||
|
"Calling API (will cache for ",
|
||||||
|
$args->{_cache_ttl} || DEFAULT_CACHE_TTL,
|
||||||
|
"s): $url"
|
||||||
|
);
|
||||||
|
|
||||||
Slim::Networking::SimpleAsyncHTTP->new(
|
Slim::Networking::SimpleAsyncHTTP->new(
|
||||||
sub {
|
sub {
|
||||||
@@ -151,14 +192,16 @@ sub _call {
|
|||||||
my $result = eval { from_json( $response->content ) };
|
my $result = eval { from_json( $response->content ) };
|
||||||
|
|
||||||
if ($@) {
|
if ($@) {
|
||||||
$log->error(Data::Dump::dump($response)) unless main::DEBUGLOG && $log->is_debug;
|
$log->error( Data::Dump::dump($response) )
|
||||||
|
unless main::DEBUGLOG && $log->is_debug;
|
||||||
$log->error($@);
|
$log->error($@);
|
||||||
}
|
}
|
||||||
|
|
||||||
main::DEBUGLOG && $log->is_debug && warn Data::Dump::dump($result);
|
main::DEBUGLOG && $log->is_debug && warn Data::Dump::dump($result);
|
||||||
|
|
||||||
$result ||= {};
|
$result ||= {};
|
||||||
$cache->set($cacheKey, $result, $args->{_cache_ttl} || DEFAULT_CACHE_TTL);
|
$cache->set( $cacheKey, $result,
|
||||||
|
$args->{_cache_ttl} || DEFAULT_CACHE_TTL );
|
||||||
|
|
||||||
$cb->($result);
|
$cb->($result);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,14 +8,18 @@ use Slim::Utils::Strings qw(string cstring);
|
|||||||
use JSON::XS::VersionOneAndTwo;
|
use JSON::XS::VersionOneAndTwo;
|
||||||
use Data::Dumper;
|
use Data::Dumper;
|
||||||
|
|
||||||
use constant CLIENT_ID => "861556708454-d6dlm3lh05idd8npek18k6be8ba3oc68.apps.googleusercontent.com";
|
use constant CLIENT_ID =>
|
||||||
|
"861556708454-d6dlm3lh05idd8npek18k6be8ba3oc68.apps.googleusercontent.com";
|
||||||
use constant CLIENT_SECRET => "SboVhoG9s0rNafixCSGGKXAT";
|
use constant CLIENT_SECRET => "SboVhoG9s0rNafixCSGGKXAT";
|
||||||
|
|
||||||
my $log = logger('plugin.youtubemusic');
|
my $log = logger('plugin.youtubemusic');
|
||||||
my $cache = Slim::Utils::Cache->new();
|
my $cache = Slim::Utils::Cache->new();
|
||||||
|
|
||||||
sub getDeviceCode {
|
sub getDeviceCode {
|
||||||
my $post = "client_id=" . CLIENT_ID . "&scope=https://www.googleapis.com/auth/youtube";
|
my $post =
|
||||||
|
"client_id="
|
||||||
|
. CLIENT_ID
|
||||||
|
. "&scope=https://www.googleapis.com/auth/youtube";
|
||||||
|
|
||||||
my $http = Slim::Networking::SimpleAsyncHTTP->new(
|
my $http = Slim::Networking::SimpleAsyncHTTP->new(
|
||||||
sub {
|
sub {
|
||||||
@@ -23,12 +27,17 @@ sub getDeviceCode {
|
|||||||
my $result = eval { from_json( $response->content ) };
|
my $result = eval { from_json( $response->content ) };
|
||||||
|
|
||||||
if ($@) {
|
if ($@) {
|
||||||
$log->error(Data::Dump::dump($response)) unless main::DEBUGLOG && $log->is_debug;
|
$log->error( Data::Dump::dump($response) )
|
||||||
|
unless main::DEBUGLOG && $log->is_debug;
|
||||||
$log->error($@);
|
$log->error($@);
|
||||||
} else {
|
}
|
||||||
$cache->set("yt:device_code", $result->{device_code}, $result->{expires_in});
|
else {
|
||||||
$cache->set("yt:verification_url", $result->{verification_url}, $result->{expires_in});
|
$cache->set( "yt:device_code", $result->{device_code},
|
||||||
$cache->set("yt:user_code", $result->{user_code}, $result->{expires_in});
|
$result->{expires_in} );
|
||||||
|
$cache->set( "yt:verification_url", $result->{verification_url},
|
||||||
|
$result->{expires_in} );
|
||||||
|
$cache->set( "yt:user_code", $result->{user_code},
|
||||||
|
$result->{expires_in} );
|
||||||
|
|
||||||
$log->debug( "content:", $response->content );
|
$log->debug( "content:", $response->content );
|
||||||
}
|
}
|
||||||
@@ -55,9 +64,14 @@ sub getToken {
|
|||||||
my $code = $cache->get('yt:device_code');
|
my $code = $cache->get('yt:device_code');
|
||||||
|
|
||||||
if ( defined $code ) {
|
if ( defined $code ) {
|
||||||
$post .= "&code=$code&grant_type=http://oauth.net/grant_type/device/1.0";
|
$post .=
|
||||||
} else {
|
"&code=$code&grant_type=http://oauth.net/grant_type/device/1.0";
|
||||||
$post .= "&refresh_token=" . $cache->get('yt:refresh_token') . "&grant_type=refresh_token";
|
}
|
||||||
|
else {
|
||||||
|
$post .=
|
||||||
|
"&refresh_token="
|
||||||
|
. $cache->get('yt:refresh_token')
|
||||||
|
. "&grant_type=refresh_token";
|
||||||
}
|
}
|
||||||
|
|
||||||
my $http = Slim::Networking::SimpleAsyncHTTP->new(
|
my $http = Slim::Networking::SimpleAsyncHTTP->new(
|
||||||
@@ -66,11 +80,18 @@ sub getToken {
|
|||||||
my $result = eval { from_json( $response->content ) };
|
my $result = eval { from_json( $response->content ) };
|
||||||
|
|
||||||
if ($@) {
|
if ($@) {
|
||||||
$log->error(Data::Dump::dump($response)) unless main::DEBUGLOG && $log->is_debug;
|
$log->error( Data::Dump::dump($response) )
|
||||||
|
unless main::DEBUGLOG && $log->is_debug;
|
||||||
$log->error($@);
|
$log->error($@);
|
||||||
} else {
|
}
|
||||||
$cache->set("yt:access_token", $result->{access_token}, $result->{expires_in} - 60);
|
else {
|
||||||
$cache->set('yt:refresh_token', $result->{refresh_token}) if $result->{refresh_token};
|
$cache->set(
|
||||||
|
"yt:access_token",
|
||||||
|
$result->{access_token},
|
||||||
|
$result->{expires_in} - 60
|
||||||
|
);
|
||||||
|
$cache->set( 'yt:refresh_token', $result->{refresh_token} )
|
||||||
|
if $result->{refresh_token};
|
||||||
|
|
||||||
$cache->remove('yt:user_code');
|
$cache->remove('yt:user_code');
|
||||||
$cache->remove('yt:verification_url');
|
$cache->remove('yt:verification_url');
|
||||||
|
|||||||
@@ -11,18 +11,22 @@ use Slim::Utils::Cache;
|
|||||||
|
|
||||||
use Plugins::YouTubeMusic::OAuth2;
|
use Plugins::YouTubeMusic::OAuth2;
|
||||||
|
|
||||||
my $log = Slim::Utils::Log->addLogCategory({
|
my $log = Slim::Utils::Log->addLogCategory(
|
||||||
|
{
|
||||||
'category' => 'plugin.youtubemusic',
|
'category' => 'plugin.youtubemusic',
|
||||||
'defaultLevel' => 'WARN',
|
'defaultLevel' => 'WARN',
|
||||||
'description' => 'PLUGIN_YOUTUBEMUSIC',
|
'description' => 'PLUGIN_YOUTUBEMUSIC',
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
my $prefs = preferences('plugin.youtubemusic');
|
my $prefs = preferences('plugin.youtubemusic');
|
||||||
my $cache = Slim::Utils::Cache->new();
|
my $cache = Slim::Utils::Cache->new();
|
||||||
|
|
||||||
$prefs->init({
|
$prefs->init(
|
||||||
|
{
|
||||||
testPref => 'test'
|
testPref => 'test'
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
sub initPlugin {
|
sub initPlugin {
|
||||||
my $class = shift;
|
my $class = shift;
|
||||||
@@ -50,12 +54,18 @@ sub getDisplayName { 'PLUGIN_YOUTUBEMUSIC' }
|
|||||||
sub mainMenu {
|
sub mainMenu {
|
||||||
my ( $client, $callback, $args ) = @_;
|
my ( $client, $callback, $args ) = @_;
|
||||||
|
|
||||||
$callback->([
|
$callback->(
|
||||||
{ name => cstring($client, 'PLUGIN_YOUTUBEMUSIC_MYPLAYLISTS'), type => 'url', url => \&myPlaylistHandler, passthrough => [ { count => 2 } ] },
|
[
|
||||||
]);
|
{
|
||||||
|
name => cstring( $client, 'PLUGIN_YOUTUBEMUSIC_MYPLAYLISTS' ),
|
||||||
|
type => 'url',
|
||||||
|
url => \&myPlaylistHandler,
|
||||||
|
passthrough => [ { count => 2 } ]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
sub myPlaylistHandler {
|
sub myPlaylistHandler {
|
||||||
my ( $client, $cb, $args, $params ) = @_;
|
my ( $client, $cb, $args, $params ) = @_;
|
||||||
|
|
||||||
@@ -71,13 +81,17 @@ sub myPlaylistHandler {
|
|||||||
access_token => $cache->get('yt:access_token'),
|
access_token => $cache->get('yt:access_token'),
|
||||||
};
|
};
|
||||||
|
|
||||||
Plugins::YouTubeMusic::API->searchDirect('playlists', sub {
|
Plugins::YouTubeMusic::API->searchDirect(
|
||||||
|
'playlists',
|
||||||
|
sub {
|
||||||
$cb->( _renderList( $_[0], 'title', $account ) );
|
$cb->( _renderList( $_[0], 'title', $account ) );
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
%$account,
|
%$account,
|
||||||
_index => $args->{index},
|
_index => $args->{index},
|
||||||
_quantity => $args->{quantity},
|
_quantity => $args->{quantity},
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
sub _renderList {
|
sub _renderList {
|
||||||
@@ -103,4 +117,3 @@ sub _renderList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
1;
|
1;
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ sub handler {
|
|||||||
my ( $class, $client, $params ) = @_;
|
my ( $class, $client, $params ) = @_;
|
||||||
|
|
||||||
if ( $params->{'saveSettings'} ) {
|
if ( $params->{'saveSettings'} ) {
|
||||||
preferences('plugin.youtubemusic')->set('testPref', $params->{'pref_testPref'});
|
preferences('plugin.youtubemusic')
|
||||||
|
->set( 'testPref', $params->{'pref_testPref'} );
|
||||||
}
|
}
|
||||||
|
|
||||||
Plugins::YouTubeMusic::OAuth2::getDeviceCode if $params->{get_device_code};
|
Plugins::YouTubeMusic::OAuth2::getDeviceCode if $params->{get_device_code};
|
||||||
@@ -39,4 +40,3 @@ sub handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
1;
|
1;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user