From 192ffdb527d85664f03683a129719b6df8fd7800 Mon Sep 17 00:00:00 2001 From: bluesaxman Date: Sat, 27 Feb 2021 01:05:38 -0700 Subject: [PATCH] Initial Commit --- README.md | 12 +++ ytbrowser.pl | 214 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 226 insertions(+) create mode 100644 README.md create mode 100755 ytbrowser.pl diff --git a/README.md b/README.md new file mode 100644 index 0000000..8ffc24e --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +### ytbrowser +This script lets you browse your suggestions on youtube without having to load all the overhead. It also lets you watch videos with mpv from its TUI. + +- Requirents: + - Perl Moduels + - Time::HiRes + - Digest::SHA1 + - Curses + - Curses::UI + - A cookies text file (with your current google/youtube seesion data) + - yt-download + - mpv diff --git a/ytbrowser.pl b/ytbrowser.pl new file mode 100755 index 0000000..97a53a6 --- /dev/null +++ b/ytbrowser.pl @@ -0,0 +1,214 @@ +#!/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 $cookiesFile = "/home/bluesaxman/.scripts/cookies.txt"; + +my @watchlist = (); +my @suggestArray = (); +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 $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, "./watchlist.txt") or goto SKIPWATCHLOAD; +while() { + my $item = $_; + chomp $item; + push(@watchlist, $item); +} +close(WATCH); +SKIPWATCHLOAD: +my %tests = map { (split("=",$_))[1] => 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; + + 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++; + unless($recursionWatchdog > 100) { sleep 2; return additionalGet($_->{'continuationItemRenderer'}); } + } + } +} + +sub firstGet { +# open(FH, "./ytsample"); +# $rawFile = ""; +# while () { $rawFile .= $_ } +# close(FH); + + $rawFile = `curl -s -b $cookiesFile https://www.youtube.com`; + + $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'}); +} + +sub additionalGet { +# We need this token in order to get more suggestions. + my $continuationItem = 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 + + # Our test sample data for a continuation +# open(FH2, "./ytsample_continuation"); +# $rawFile = ""; +# while () { $rawFile .= $_; } +# close(FH2); + + $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'}); + } +} + +sub loadSugs { + my $counter = 1; + for (@videos) { + my $title = $_->{title}; + my $playtime = $_->{runtime}; + push(@suggestArray, sprintf('%3.3s.',$counter).sprintf('%8.8s ',$playtime).$title); + $counter ++; + } +} + +open(CACHED, "./cachedSug") or goto SKIPCACHED; +my $readFile = ""; +while () { $readFile .= $_; } +close(CACHED); +@videos = @{decode_json($readFile)}; +SKIPCACHED: + +#################################################### +# 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 => \@watchlist); +loadSugs(); + +$cui->set_binding( sub { $cui->mainloopExit(); }, "q" ); +$suggestions->set_binding( sub { + my $vid = splice(@videos,$suggestions->get_active_id(),1); + splice(@suggestArray,$suggestions->get_active_id(),1); + push(@watchlist, "https://www.youtube.com/watch?v=".$vid->{id}); +# $cui->status("https://www.youtube.com/watch?v=".$vid->{id}."\nhas been added to the watchlist. Watchlist is ".(scalar(@watchlist) + 1)." long."); + $suggestions->draw(); + $wlist->draw(); +}, KEY_ENTER); +$cui->set_binding( sub { + my $video = shift(@watchlist); + $cui->status("Playing Video, controls will resume once video is compelte"); + `mpv --ytdl-raw-options=cookies=$cookiesFile,mark-watched= --terminal=no $video`; + $win->draw(); + $suggestions->draw(); + $wlist->draw(); +}, "p"); +$cui->set_binding( sub { + splice(@videos,0,scalar(@videos)); + splice(@suggestArray,0,scalar(@suggestArray)); + $cui->status("Refreshing from Youtube, please wait..."); + firstGet(); + loadSugs(); + $win->draw(); + $suggestions->draw(); + $wlist->draw(); + +}, "r"); +$cui->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"); + +$suggestions->focus(); + +$cui->mainloop(); + +#Save leftovers +`touch ./lastPull`; +open(DEBUG, ">", "./lastPull"); +print DEBUG $rawFile; +close(DEBUG); +`touch ./cachedSug`; +open(CACHING, ">", "./cachedSug"); +print CACHING encode_json(\@videos); +close(CACHING); +`touch ./watchlist.txt`; +open(SAVE, ">", "./watchlist.txt"); +for (@watchlist) { print SAVE $_."\n"; } +close(SAVE);