359 lines
13 KiB
Perl
Executable File
359 lines
13 KiB
Perl
Executable File
#!/usr/bin/perl
|
|
|
|
use strict;
|
|
use warnings;
|
|
use JSON;
|
|
use Time::HiRes qw( gettimeofday );
|
|
use Digest::SHA1 qw( sha1_hex );
|
|
use Curses;
|
|
use Curses::UI;
|
|
|
|
use Data::Dumper;
|
|
$Data::Dumper::Indent = 1;
|
|
$Data::Dumper::Maxdepth = 1;
|
|
|
|
my $homeDir = "/home/bluesaxman/";
|
|
my $cookiesFile = $homeDir."cookies.txt";
|
|
my $cachedFile = $homeDir.".yt_cache";
|
|
my $playlistFile = $homeDir.".yt_play";
|
|
my $debugFile = $homeDir.".yt_last";
|
|
|
|
my @watchlist = ();
|
|
my @suggestArray = ();
|
|
my @watchArray = ();
|
|
my $continuation;
|
|
my $visitorID;
|
|
my $pageID;
|
|
my $ytclient;
|
|
my $ytclientVersion;
|
|
my $apiKey;
|
|
my $sapisidhash;
|
|
my $json;
|
|
my $jsonDirty;
|
|
my $rawFile;
|
|
my @videos = ();
|
|
my $recursionWatchdog = 0;
|
|
my $volume = 50;
|
|
|
|
my $cookieCrisp = "";
|
|
open (COOKIE, $cookiesFile) or die "I CANNOT FUNCTION WITHOUT COOKIES!!!\n";
|
|
while (<COOKIE>) { $cookieCrisp .= $_ }
|
|
close (COOKIE);
|
|
|
|
$cookieCrisp =~ m/\.youtube\.com[^\n]*SAPISID\s([^\n]*)/;
|
|
#sha1(new Date().getTime() + " " + SAPISID + " " + origin)
|
|
$sapisidhash = (gettimeofday())[0]."_".sha1_hex((gettimeofday())[0]." ".$1." https://www.youtube.com");
|
|
|
|
open(WATCH, $playlistFile) or goto SKIPWATCHLOAD;
|
|
my $readFile = "";
|
|
while(<WATCH>) { $readFile .= $_; }
|
|
close(WATCH);
|
|
if ($readFile) { @watchlist = @{decode_json($readFile)}; }
|
|
SKIPWATCHLOAD:
|
|
my %tests = map { $_->{id} => 1 } @watchlist;
|
|
|
|
sub MPV_base {
|
|
my $string = "";
|
|
$string .= "mpv --volume=".$volume;
|
|
# MPV play nice please
|
|
$string .= " --ytdl=no --scripts-pre=".$homeDir.".config/mpv/scripts/ytdl_hook.lua";
|
|
|
|
$string .= " --script-opts=ytdl_hook-ytdl_path=yt-dlp --ytdl-raw-options=";
|
|
if ( grep( /^c$/, @_ )) { $string .= "cookies=$cookiesFile,"; }
|
|
if ( grep( /^m$/, @_ )) { $string .= "mark-watched=,"; }
|
|
$string .= "buffer-size=200M,format='bestvideo[height<=720]+bestaudio/best[height<=720]' --terminal=no ";
|
|
$string .= "https://www.youtube.com/watch?v=";
|
|
return $string;
|
|
}
|
|
|
|
# Currently we read from a file that was rerived from
|
|
# curl -b ~/.scripts/cookies.txt https://www.youtube.com
|
|
# So we don't make Youtube mad at us. eventually we will
|
|
# replace the open with this.
|
|
|
|
sub addVideos {
|
|
my @contents = shift;
|
|
my $myProgress = shift;
|
|
|
|
for (@{$contents[0]}) {
|
|
if (exists($_->{'richItemRenderer'})) {
|
|
if ($_->{'richItemRenderer'}{'content'}{'videoRenderer'}{'thumbnailOverlays'}[0]{'thumbnailOverlayResumePlaybackRenderer'}) { next }
|
|
my $idToken = $_->{'richItemRenderer'}{'content'}{'videoRenderer'}{'videoId'};
|
|
unless ($idToken) { next }
|
|
if (exists($tests{$idToken})) { next }
|
|
push @videos, { id => $_->{'richItemRenderer'}{'content'}{'videoRenderer'}{'videoId'},
|
|
title => $_->{'richItemRenderer'}{'content'}{'videoRenderer'}{'title'}{'runs'}[0]->{'text'},
|
|
published => $_->{'richItemRenderer'}{'content'}{'videoRenderer'}{'publishedTimeText'}{'simpleText'},
|
|
runtime => $_->{'richItemRenderer'}{'content'}{'videoRenderer'}{'lengthText'}{'simpleText'},
|
|
channel => $_->{'richItemRenderer'}{'content'}{'videoRenderer'}{'ownerText'}{'runs'}[0]->{'text'}
|
|
};
|
|
} elsif (exists($_->{'continuationItemRenderer'})) {
|
|
$recursionWatchdog++;
|
|
$myProgress->pos($recursionWatchdog);
|
|
$myProgress->draw();
|
|
unless($recursionWatchdog > 100) { sleep 2; return additionalGet($_->{'continuationItemRenderer'},$myProgress); }
|
|
}
|
|
}
|
|
}
|
|
|
|
sub firstGet {
|
|
my $myProgress = shift;
|
|
$rawFile = `curl -s -b $cookiesFile https://www.youtube.com`;
|
|
$myProgress->draw();
|
|
$rawFile =~ m/ytInitialData =([^<]*)/;
|
|
$jsonDirty = $1;
|
|
chop $jsonDirty;
|
|
$json = decode_json $jsonDirty;
|
|
$visitorID = $json->{'responseContext'}{'webResponseContextExtensionData'}{'ytConfigData'}{'visitorData'};
|
|
$visitorID =~ s/%3D/=/g;
|
|
addVideos($json->{'contents'}{'twoColumnBrowseResultsRenderer'}{'tabs'}[0]->{'tabRenderer'}{'content'}{'richGridRenderer'}{'contents'},$myProgress);
|
|
}
|
|
|
|
sub additionalGet {
|
|
# We need these tokens in order to get more suggestions.
|
|
my $continuationItem = shift;
|
|
my $myProgress = shift;
|
|
$continuation = $continuationItem->{'continuationEndpoint'}{'continuationCommand'}{'token'};
|
|
$pageID = (split(/\|/, $json->{'responseContext'}{'mainAppWebResponseContext'}{'datasyncId'}))[0];
|
|
$ytclient = $json->{'responseContext'}{'serviceTrackingParams'}[2]->{'params'}[1]->{'value'};
|
|
$ytclientVersion = $json->{'responseContext'}{'serviceTrackingParams'}[2]->{'params'}[2]->{'value'};
|
|
|
|
# hopefully I can find this in the json
|
|
# for now this will have to do
|
|
$rawFile =~ m/INNERTUBE_API_KEY":"([^"]*)/;
|
|
$apiKey = $1;
|
|
|
|
my $requestJson = encode_json({
|
|
context => {
|
|
client => {
|
|
hl => "en",
|
|
gl => "US",
|
|
visitorData => $visitorID,
|
|
clientName => $ytclient,
|
|
clientVersion => $ytclientVersion
|
|
},
|
|
user => { lockedSafetyMode => 'false' },
|
|
request => { useSsl => 'true' }
|
|
},
|
|
continuation => $continuation
|
|
});
|
|
|
|
# Not the curl the world wants, but the curl the world needs
|
|
|
|
$rawFile = `curl -s -X POST -H 'content-type: application/json' -H 'authorization: SAPISIDHASH $sapisidhash' -H 'referer: https://www.youtube.com/' -H 'x-goog-authuser: 0' -H 'x-goog-pageid: $pageID' -H 'x-goog-visitor-id: $visitorID' -H 'x-origin: https://www.youtube.com' -H 'x-youtube-client-name: 1' -H 'x-youtube-client-version: $ytclientVersion' -b $cookiesFile -d '$requestJson' https://www.youtube.com/youtubei/v1/browse?key=$apiKey`;
|
|
$json = decode_json $rawFile;
|
|
if (exists($json->{'onResponseReceivedActions'})) {
|
|
return addVideos($json->{'onResponseReceivedActions'}[0]->{'appendContinuationItemsAction'}{'continuationItems'},$myProgress);
|
|
}
|
|
}
|
|
|
|
sub loadSugs {
|
|
my $counter = 1;
|
|
splice(@suggestArray,0,scalar(@suggestArray));
|
|
splice(@watchArray,0,scalar(@watchArray));
|
|
for (@videos) {
|
|
my $title = $_->{title};
|
|
my $playtime = $_->{runtime};
|
|
unless($playtime) { $playtime = "Live"; }
|
|
push(@suggestArray, sprintf('%3.3s.',$counter).sprintf('%8.8s ',$playtime).$title);
|
|
$counter ++;
|
|
}
|
|
for (@watchlist) {
|
|
my $title = $_->{title};
|
|
my $playtime = $_->{runtime};
|
|
unless($playtime) { $playtime = "Live"; }
|
|
push(@watchArray, sprintf('%8.8s ',$playtime).$title);
|
|
}
|
|
}
|
|
|
|
open(CACHED, $cachedFile) or goto SKIPCACHED;
|
|
$readFile = "";
|
|
while (<CACHED>) { $readFile .= $_; }
|
|
close(CACHED);
|
|
if ($readFile) { @videos = @{decode_json($readFile)}; }
|
|
SKIPCACHED:
|
|
|
|
`touch $debugFile`;
|
|
`touch $cachedFile`;
|
|
`touch $playlistFile`;
|
|
open(DEBUG, ">", $debugFile);
|
|
open(CACHING, ">", $cachedFile);
|
|
open(SAVE, ">", $playlistFile);
|
|
|
|
close(STDERR);
|
|
open(STDERR, ">>", $debugFile);
|
|
|
|
select( (select(DEBUG), $| = 1)[0] );
|
|
|
|
sub sync2files {
|
|
truncate(DEBUG,0);
|
|
truncate(CACHING,0);
|
|
truncate(SAVE,0);
|
|
|
|
seek(DEBUG,0,0);
|
|
seek(CACHING,0,0);
|
|
seek(SAVE,0,0);
|
|
|
|
if ($rawFile) { print DEBUG $rawFile; }
|
|
print CACHING encode_json(\@videos);
|
|
print SAVE encode_json(\@watchlist);
|
|
}
|
|
|
|
####################################################
|
|
# Now that we have our data from Youtube, lets make
|
|
# it easy to look at and select.
|
|
|
|
my $cui = new Curses::UI( -clear_on_exit => 1, -color_support => 1 );
|
|
my $win = $cui->add( 'main', 'Window');
|
|
my $suggestions = $win->add( 'suggestions', 'Listbox', -pad => 1, -ipad => 1, -border => 1, -title => "Suggestions", -vscrollbar => 'right', -fg => "blue", -bg => "white", -height => $win->height() / 2 , -values => \@suggestArray);
|
|
my $wlist = $win->add( 'watchlist', 'Listbox', -pad => 1, -ipad => 1, -border => 1, -title => "Watch list", -vscrollbar => 'right', -fg => "green", -bg => "black", -height => $win->height() / 2, -y => $suggestions->height(), -values => \@watchArray);
|
|
my $textEntry = $win->add( 'mytextentry', 'TextEntry', -y => $win->height() / 2, -title => "Manual Video Add");
|
|
loadSugs();
|
|
|
|
$cui->set_binding( sub { $cui->mainloopExit(); }, "q" );
|
|
$suggestions->set_binding( sub {
|
|
push(@watchArray, (split(". ", splice(@suggestArray,$suggestions->get_active_id(),1),2))[1] );
|
|
push(@watchlist, splice(@videos,$suggestions->get_active_id(),1));
|
|
sync2files();
|
|
$suggestions->draw();
|
|
$wlist->draw();
|
|
}, KEY_ENTER);
|
|
|
|
$suggestions->set_binding( sub {
|
|
my $info = (split($suggestArray[$suggestions->get_active_id()],2))[1];
|
|
my $video = $videos[$suggestions->get_active_id()];
|
|
unless($video) { return 0; }
|
|
$cui->status("Playing Video, controls will resume once video is compelte\n Currently Playing: ".$info);
|
|
print DEBUG MPV_base("c").$video->{id}."\n";
|
|
if (0 == system(MPV_base("c").$video->{id})) {
|
|
$win->draw();
|
|
$suggestions->draw();
|
|
$wlist->draw();
|
|
$cui->status("Playback Complete");
|
|
} else {
|
|
$cui->status("PLAYBACK ERROR");
|
|
}
|
|
}, "p");
|
|
|
|
$wlist->set_binding( sub {
|
|
my $video = $watchlist[$wlist->get_active_id()];
|
|
unless($video) { return 0; }
|
|
$cui->status("Playing Video, controls will resume once video is compelte\n Currently Playing: ".$watchArray[$wlist->get_active_id()]);
|
|
print DEBUG MPV_base("c","m").$video->{id}."\n";
|
|
if (0 == system(MPV_base("c","m").$video->{id})) {
|
|
splice(@watchArray,$wlist->get_active_id(),1);
|
|
splice(@watchlist,$wlist->get_active_id(),1);
|
|
sync2files();
|
|
$win->draw();
|
|
$suggestions->draw();
|
|
$wlist->draw();
|
|
$cui->status("Playback Complete, video cleared");
|
|
} else {
|
|
$cui->status("PLAYBACK ERROR: Video not removed for safety.");
|
|
}
|
|
} , "p");
|
|
|
|
$wlist->set_binding( sub {
|
|
my $video = $watchlist[$wlist->get_active_id()];
|
|
unless($video) { return 0; }
|
|
$cui->status("Playing Video, controls will resume once video is compelte\n Currently Playing: ".$watchArray[$wlist->get_active_id()]);
|
|
print DEBUG MPV_base("c").$video->{id}."\n";
|
|
if (0 == system(MPV_base("c").$video->{id})) {
|
|
$win->draw();
|
|
$suggestions->draw();
|
|
$wlist->draw();
|
|
$cui->status("Playback Complete");
|
|
} else {
|
|
$cui->status("PLAYBACK ERROR.");
|
|
}
|
|
} , "w");
|
|
|
|
$suggestions->set_binding( sub {
|
|
splice(@videos,0,scalar(@videos));
|
|
splice(@suggestArray,0,scalar(@suggestArray));
|
|
$cui->status("Refreshing from Youtube, please wait...");
|
|
$recursionWatchdog = 0;
|
|
my $progressBar = $win->add('refresh', 'Progressbar', -max => 50, -pos => 0, -y => $suggestions->height() / 2 );
|
|
$progressBar->focus();
|
|
firstGet($progressBar);
|
|
loadSugs();
|
|
sync2files();
|
|
$win->draw();
|
|
$suggestions->draw();
|
|
$wlist->draw();
|
|
}, "r");
|
|
|
|
$suggestions->set_binding( sub {
|
|
my $vid = $videos[$suggestions->get_active_id()];
|
|
$cui->status("Video: ".$vid->{title}."\nChannel: ".$vid->{channel}."\nRuntime: ".$vid->{runtime}."\nPublished: ".$vid->{published}."\nURL: https://www.youtube.com/watch?v=".$vid->{id});
|
|
}, "i");
|
|
|
|
$wlist->set_binding( sub {
|
|
my $vid = $watchlist[$wlist->get_active_id()];
|
|
$cui->status("Video: ".$vid->{title}."\nChannel: ".$vid->{channel}."\nRuntime: ".$vid->{runtime}."\nPublished: ".$vid->{published}."\nURL: https://www.youtube.com/watch?v=".$vid->{id});
|
|
} , "i");
|
|
|
|
$wlist->set_binding( sub {
|
|
@watchlist = sort { $a->{runtime} cmp $b->{runtime} } @watchlist;
|
|
sync2files();
|
|
loadSugs();
|
|
$wlist->draw();
|
|
} , ">");
|
|
|
|
$wlist->set_binding( sub {
|
|
@watchlist = sort { $b->{runtime} cmp $a->{runtime} } @watchlist;
|
|
sync2files();
|
|
loadSugs();
|
|
$wlist->draw();
|
|
}, "<");
|
|
|
|
$wlist->set_binding( sub {
|
|
print DEBUG "Manually adding a video...\n";
|
|
$cui->status("Manual Adding not implimented at this time");
|
|
$textEntry->text("");
|
|
$textEntry->set_binding( sub {
|
|
my $text = $textEntry->get();
|
|
if($text =~ m/youtu(?:\.be\/|be.com\/\S*(?:watch|embed)(?:(?:(?=\/[-a-zA-Z0-9_]{11,}(?!\S))\/)|(?:\S*v=|v\/)))([-a-zA-Z0-9_]{11,})/) {
|
|
print DEBUG $1."\n";
|
|
my $videoJsonString = `./info.sh "https://www.youtube.com/watch?v=$1"`;
|
|
print DEBUG $videoJsonString."\n";
|
|
my $video = \%{decode_json($videoJsonString)};
|
|
print DEBUG "Confirming with user\n";
|
|
print DEBUG Dumper($video)."\n";
|
|
my $confirm = $cui->dialog(-message => "Here is what we know about that:\nTitle: ".$video->{title}."\nChannel: ".$video->{channel}."\nRuntime: ".$video->{runtime}."\nPublished: ".$video->{published}, -buttons => [{ -label => "Add Video", -value => 1, -shortcut => "a" },'cancel']);
|
|
print DEBUG $confirm."\n";
|
|
if ($confirm) {
|
|
print DEBUG "Attempting to add...\n";
|
|
push(@watchArray, $video->{runtime}>" ".$video->{title});
|
|
push(@watchlist, $video);
|
|
print DEBUG Dumper($video);
|
|
sync2files();
|
|
$suggestions->draw();
|
|
$wlist->draw();
|
|
}
|
|
}
|
|
$wlist->focus();
|
|
}, KEY_ENTER);
|
|
$textEntry->focus();
|
|
}, "m");
|
|
|
|
$suggestions->set_binding( sub {
|
|
$cui->status("Controls:\nEnter\tAdd a video to the playlist\np\tPlay currently selected video from suggestions\ni\tInfo about currently selected video\nr\tRefresh list from youtube\n?\tShow controls");
|
|
}, "?");
|
|
$wlist->set_binding( sub {
|
|
$cui->status("Controls:\n<\tSort from shortest to longest\n>\tSort from longest to shortest\np\tPlay currently selected video from playlist, then remove it\nw\tPlay currently selected video from playlist without removing it\ni\tInfo about currently selected video\nm\tManually Add a video by URL\n?\tShow controls");
|
|
}, "?");
|
|
|
|
$suggestions->focus();
|
|
|
|
$cui->mainloop();
|
|
|
|
#Save leftovers
|
|
sync2files();
|
|
|
|
print DEBUG "Closeing Program\n";
|
|
|
|
close(DEBUG);
|
|
close(CACHING);
|
|
close(SAVE);
|