Compare commits
2 Commits
bdbb9073db
...
5e85b74224
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e85b74224 | ||
|
|
4f0402d71b |
50
.gitignore
vendored
Normal file
50
.gitignore
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
!Build/
|
||||
.last_cover_stats
|
||||
/META.yml
|
||||
/META.json
|
||||
/MYMETA.*
|
||||
*.o
|
||||
*.pm.tdy
|
||||
*.bs
|
||||
|
||||
# Devel::Cover
|
||||
cover_db/
|
||||
|
||||
# Devel::NYTProf
|
||||
nytprof.out
|
||||
|
||||
# Dist::Zilla
|
||||
/.build/
|
||||
|
||||
# Module::Build
|
||||
_build/
|
||||
Build
|
||||
Build.bat
|
||||
|
||||
# Module::Install
|
||||
inc/
|
||||
|
||||
# ExtUtils::MakeMaker
|
||||
/blib/
|
||||
/_eumm/
|
||||
/*.gz
|
||||
/Makefile
|
||||
/Makefile.old
|
||||
/MANIFEST.bak
|
||||
/pm_to_blib
|
||||
/*.zip
|
||||
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
!.vscode/*.code-snippets
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
# Built Visual Studio Code Extensions
|
||||
*.vsix
|
||||
|
||||
**.json
|
||||
179
YouTubeMusic/API.pm
Normal file
179
YouTubeMusic/API.pm
Normal file
@@ -0,0 +1,179 @@
|
||||
package Plugins::YouTubeMusic::API;
|
||||
|
||||
use strict;
|
||||
|
||||
use Digest::MD5 qw(md5_hex);
|
||||
use File::Spec::Functions qw(catdir);
|
||||
use JSON::XS::VersionOneAndTwo;
|
||||
use List::Util qw(min max);
|
||||
use URI::Escape qw(uri_escape uri_escape_utf8);
|
||||
use MIME::Base64 qw(decode_base64);
|
||||
|
||||
use constant API_URL => 'https://music.youtube.com/youtubei/v1/';
|
||||
use constant DEFAULT_CACHE_TTL => 24 * 3600;
|
||||
use constant DEFAULT_API_KEY => 'QUl6YVN5Qi1wd1B0RGt4RjZKUW1BOHFxOWgxbWQ2ME15STVRNWlB';
|
||||
|
||||
use Slim::Utils::Cache;
|
||||
use Slim::Utils::Log;
|
||||
use Slim::Utils::Prefs;
|
||||
|
||||
my $prefs = preferences('plugin.youtubemusic');
|
||||
my $log = logger('plugin.youtubemusic');
|
||||
my $cache = Slim::Utils::Cache->new();
|
||||
|
||||
sub flushCache { $cache->cleanup(); }
|
||||
|
||||
sub search {
|
||||
my ( $class, $cb, $args ) = @_;
|
||||
|
||||
$args ||= {};
|
||||
$args->{part} = 'snippet';
|
||||
$args->{type} ||= 'video';
|
||||
$args->{order} ||= $prefs->get('search_rank');
|
||||
$args->{relevanceLanguage} = Slim::Utils::Strings::getLanguage();
|
||||
|
||||
_pagedCall('search', $args, $cb);
|
||||
}
|
||||
|
||||
sub searchDirect {
|
||||
my ( $class, $type, $cb, $args ) = @_;
|
||||
|
||||
$args ||= {};
|
||||
$args->{_noRegion} = 1;
|
||||
|
||||
_pagedCall( $type, $args, $cb);
|
||||
}
|
||||
|
||||
sub getCategories {
|
||||
my ( $class, $type, $cb, $args ) = @_;
|
||||
|
||||
$args ||= {};
|
||||
$args->{hl} = Slim::Utils::Strings::getLanguage();
|
||||
$args->{_cache_ttl} = 7 * 86400;
|
||||
|
||||
_pagedCall($type, $args, $cb);
|
||||
}
|
||||
|
||||
sub getVideoDetails {
|
||||
my ( $class, $cb, $ids ) = @_;
|
||||
|
||||
_call('videos', {
|
||||
part => 'snippet,contentDetails',
|
||||
id => $ids,
|
||||
# cache video details a bit longer
|
||||
_cache_ttl => 7 * 86400,
|
||||
}, $cb);
|
||||
}
|
||||
|
||||
sub _pagedCall {
|
||||
my ( $method, $args, $cb ) = @_;
|
||||
my $wantedItems = $args->{_quantity} || $prefs->get('max_items');
|
||||
# ignore $args->{_index} and let LMS handle offset
|
||||
my $wantedIndex = 0;
|
||||
my @items;
|
||||
my $pagingCb;
|
||||
my $pageIndex = 0;
|
||||
|
||||
# we want them starting from current index
|
||||
$wantedItems += $args->{_index} || 0;
|
||||
|
||||
main::INFOLOG && $log->info("Searching by [$args->{order}]");
|
||||
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'
|
||||
if ($prefs->get('search_sort') || $prefs->get('channel_sort') || $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
|
||||
$wantedItems = $prefs->get('max_items') if $wantedItems > $prefs->get('max_items');
|
||||
|
||||
$pagingCb = sub {
|
||||
my $results = shift;
|
||||
|
||||
if ( $results->{error} || !$results->{items} ) {
|
||||
$log->error("no results");
|
||||
$cb->( { items => undef, total => 0 } ) if ( $results->{error} );
|
||||
return;
|
||||
}
|
||||
|
||||
push @items, @{$results->{items}};
|
||||
$pageIndex += scalar @{$results->{items}};
|
||||
|
||||
main::INFOLOG && $log->info("Want $wantedItems items from offset ", $wantedIndex, ", have " . scalar @items . " so far [acquired $pageIndex]");
|
||||
|
||||
if (@items < $wantedItems && $results->{nextPageToken}) {
|
||||
$args->{pageToken} = $results->{nextPageToken};
|
||||
main::INFOLOG && $log->info("Get next page using token " . $args->{pageToken});
|
||||
_call($method, $args, $pagingCb);
|
||||
} else {
|
||||
my $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 } );
|
||||
}
|
||||
};
|
||||
|
||||
_call($method, $args, $pagingCb);
|
||||
}
|
||||
|
||||
sub _call {
|
||||
my ( $method, $args, $cb ) = @_;
|
||||
|
||||
my $API_KEY = $prefs->get('APIkey') || MIME::Base64::decode_base64(DEFAULT_API_KEY);
|
||||
my $url = '?' . ($args->{_noKey} ? '' : 'key=' . $API_KEY . '&');
|
||||
|
||||
$args->{regionCode} ||= $prefs->get('country') unless $args->{_noRegion};
|
||||
$args->{part} ||= 'snippet' unless $args->{_noPart};
|
||||
$args->{maxResults} ||= 50;
|
||||
|
||||
for my $k ( sort keys %{$args} ) {
|
||||
next if $k =~ /^_/;
|
||||
$url .= $k . '=' . URI::Escape::uri_escape_utf8( Encode::decode( 'utf8', $args->{$k} ) ) . '&';
|
||||
}
|
||||
|
||||
$url =~ s/&$//;
|
||||
$url = API_URL . $method . $url;
|
||||
|
||||
my $cacheKey = $args->{_noCache} ? '' : md5_hex($url);
|
||||
|
||||
if ( $cacheKey && (my $cached = $cache->get($cacheKey)) ) {
|
||||
main::INFOLOG && $log->info("Returning cached data for: $url");
|
||||
$cb->($cached);
|
||||
return;
|
||||
}
|
||||
|
||||
main::INFOLOG && $log->info("Calling API (will cache for ", $args->{_cache_ttl} || DEFAULT_CACHE_TTL, "s): $url");
|
||||
|
||||
Slim::Networking::SimpleAsyncHTTP->new(
|
||||
sub {
|
||||
my $response = shift;
|
||||
my $result = eval { from_json($response->content) };
|
||||
|
||||
if ($@) {
|
||||
$log->error(Data::Dump::dump($response)) unless main::DEBUGLOG && $log->is_debug;
|
||||
$log->error($@);
|
||||
}
|
||||
|
||||
main::DEBUGLOG && $log->is_debug && warn Data::Dump::dump($result);
|
||||
|
||||
$result ||= {};
|
||||
$cache->set($cacheKey, $result, $args->{_cache_ttl} || DEFAULT_CACHE_TTL);
|
||||
|
||||
$cb->($result);
|
||||
},
|
||||
|
||||
sub {
|
||||
warn Data::Dump::dump(@_);
|
||||
$log->error($_[1]);
|
||||
$cb->( { error => $_[1] } );
|
||||
},
|
||||
|
||||
{
|
||||
timeout => 15,
|
||||
}
|
||||
|
||||
)->get($url);
|
||||
}
|
||||
|
||||
1;
|
||||
@@ -2,7 +2,7 @@ package Plugins::YouTubeMusic::Plugin;
|
||||
|
||||
use strict;
|
||||
|
||||
use base qw(Slim::Plugin::Base);
|
||||
use base qw(Slim::Plugin::OPMLBased);
|
||||
|
||||
use Slim::Utils::Strings qw(string);
|
||||
use Slim::Utils::Prefs;
|
||||
@@ -25,9 +25,10 @@ $prefs->init({
|
||||
sub initPlugin {
|
||||
my $class = shift;
|
||||
|
||||
$class->SUPER::initPlugin;
|
||||
|
||||
$log->info("Hello, World!");
|
||||
$class->SUPER::initPlugin(
|
||||
feed => \&mainMenu,
|
||||
is_app => 1
|
||||
);
|
||||
|
||||
if (main::WEBUI) {
|
||||
require Plugins::YouTubeMusic::Settings;
|
||||
@@ -41,5 +42,60 @@ sub shutdownPlugin {
|
||||
|
||||
sub getDisplayName { 'PLUGIN_YOUTUBEMUSIC' }
|
||||
|
||||
sub mainMenu {
|
||||
my ($client, $callback, $args) = @_;
|
||||
|
||||
$callback->([
|
||||
{ name => cstring($client, 'PLUGIN_YOUTUBEMUSIC_MYPLAYLISTS'), type => 'url', url => \&myPlaylistHandler, passthrough => [ { count => 2 } ] },
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
sub myPlaylistHandler {
|
||||
my ($client, $cb, $args, $params) = @_;
|
||||
|
||||
if (!$cache->get('yt:access_token')) {
|
||||
Plugins::YouTubeMusic::OAuth2::getToken(\&myPlaylistHandler, @_);
|
||||
return;
|
||||
}
|
||||
|
||||
my $account = {
|
||||
_cache_ttl => 60,
|
||||
_noKey => 1,
|
||||
mine => 'true',
|
||||
access_token => $cache->get('yt:access_token'),
|
||||
};
|
||||
|
||||
Plugins::YouTubeMusic::API->searchDirect('playlists', sub {
|
||||
$cb->(_renderList($_[0], 'title', $account));
|
||||
}, {
|
||||
%$account,
|
||||
_index => $args->{index},
|
||||
_quantity => $args->{quantity},
|
||||
});
|
||||
}
|
||||
|
||||
sub _renderList {
|
||||
my ($list, $sort, $passthrough, $tags) = @_;
|
||||
my $sortedList = $list->{items};
|
||||
my @items;
|
||||
|
||||
for my $entry (@$sortedList) {
|
||||
my $snippet = $entry->{snippet} || next;
|
||||
my $title = $snippet->{title} || next;
|
||||
|
||||
my $item = {
|
||||
name => $title,
|
||||
type => 'playlist',
|
||||
};
|
||||
|
||||
push @items, $item;
|
||||
}
|
||||
|
||||
$list->{items} = \@items;
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
|
||||
@@ -22,3 +22,6 @@ PLUGIN_YOUTUBEMUSIC_START_AUTH
|
||||
PLUGIN_YOUTUBEMUSIC_VERIFICATIONURL
|
||||
EN Login
|
||||
|
||||
PLUGIN_YOUTUBEMUSIC_MYPLAYLISTS
|
||||
EN My Playlists
|
||||
|
||||
|
||||
Reference in New Issue
Block a user