#!/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 () { $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() { $readFile .= $_; } close(WATCH); if ($readFile) { @watchlist = @{decode_json($readFile)}; } SKIPWATCHLOAD: my %tests = map { $_->{id} => 1 } @watchlist; # 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 () { $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); 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", -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", -fg => "green", -bg => "black", -height => $win->height() / 2, -y => $suggestions->height(), -values => \@watchArray); 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 $video = $watchlist[0]; unless($video) { return 0; } $cui->status("Playing Video, controls will resume once video is compelte\n Currently Playing: ".$watchArray[0]); if (0 == system("mpv --volume=".$volume." --script-opts=ytdl_hook-ytdl_path=yt-dlp --ytdl-raw-options=cookies=$cookiesFile,mark-watched=,buffer-size=200M,format='bestvideo[height<=720]+bestaudio/best[height<=720]' --terminal=no https://www.youtube.com/watch?v=$video->{id}")) { shift(@watchArray); shift(@watchlist); 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()]); if (0 == system("mpv --volume=".$volume." --script-opts=ytdl_hook-ytdl_path=yt-dlp --ytdl-raw-options=cookies=$cookiesFile,mark-watched=,buffer-size=200M,format='bestvideo[height<=720]+bestaudio/best[height<=720]' --terminal=no https://www.youtube.com/watch?v=$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"); $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(); }, "<"); $suggestions->set_binding( sub { $cui->status("Controls:\nEnter\tAdd a video to the playlist\np\tPlay currently selected video\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\ni\tInfo about currently selected video\n?\tShow controls"); }, "?"); $suggestions->focus(); $cui->mainloop(); #Save leftovers sync2files(); close(DEBUG); close(CACHING); close(SAVE);