2021-02-27 01:05:38 -07:00
#!/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 ;
2023-02-15 14:23:16 -07:00
use Data::Dumper ;
$ Data:: Dumper:: Indent = 1 ;
$ Data:: Dumper:: Maxdepth = 1 ;
2021-02-27 01:05:38 -07:00
2021-03-01 14:31:36 -07:00
my $ homeDir = "/home/bluesaxman/" ;
2022-07-08 13:06:02 -06:00
my $ cookiesFile = $ homeDir . "cookies.txt" ;
2021-03-01 14:31:36 -07:00
my $ cachedFile = $ homeDir . ".yt_cache" ;
my $ playlistFile = $ homeDir . ".yt_play" ;
my $ debugFile = $ homeDir . ".yt_last" ;
2021-02-27 01:05:38 -07:00
my @ watchlist = ( ) ;
my @ suggestArray = ( ) ;
2021-03-01 15:59:23 -07:00
my @ watchArray = ( ) ;
2021-02-27 01:05:38 -07:00
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 ;
2022-10-26 15:47:23 -06:00
my $ volume = 50 ;
2021-02-27 01:05:38 -07:00
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" ) ;
2021-03-01 14:31:36 -07:00
open ( WATCH , $ playlistFile ) or goto SKIPWATCHLOAD ;
2021-03-01 15:59:23 -07:00
my $ readFile = "" ;
while ( <WATCH> ) { $ readFile . = $ _ ; }
2021-02-27 01:05:38 -07:00
close ( WATCH ) ;
2021-03-03 15:32:44 -07:00
if ( $ readFile ) { @ watchlist = @ { decode_json ( $ readFile ) } ; }
2021-02-27 01:05:38 -07:00
SKIPWATCHLOAD:
2021-03-01 15:59:23 -07:00
my % tests = map { $ _ - > { id } = > 1 } @ watchlist ;
2021-02-27 01:05:38 -07:00
# 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 ;
2021-03-01 14:31:36 -07:00
my $ myProgress = shift ;
2021-02-27 01:05:38 -07:00
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 + + ;
2021-03-01 14:31:36 -07:00
$ myProgress - > pos ( $ recursionWatchdog ) ;
$ myProgress - > draw ( ) ;
unless ( $ recursionWatchdog > 100 ) { sleep 2 ; return additionalGet ( $ _ - > { 'continuationItemRenderer' } , $ myProgress ) ; }
2021-02-27 01:05:38 -07:00
}
}
}
sub firstGet {
2021-03-01 14:31:36 -07:00
my $ myProgress = shift ;
2021-02-27 01:05:38 -07:00
$ rawFile = `curl -s -b $cookiesFile https://www.youtube.com` ;
2021-03-01 14:31:36 -07:00
$ myProgress - > draw ( ) ;
2021-02-27 01:05:38 -07:00
$ rawFile =~ m/ytInitialData =([^<]*)/ ;
$ jsonDirty = $ 1 ;
chop $ jsonDirty ;
$ json = decode_json $ jsonDirty ;
$ visitorID = $ json - > { 'responseContext' } { 'webResponseContextExtensionData' } { 'ytConfigData' } { 'visitorData' } ;
$ visitorID =~ s/%3D/=/g ;
2021-03-01 14:31:36 -07:00
addVideos ( $ json - > { 'contents' } { 'twoColumnBrowseResultsRenderer' } { 'tabs' } [ 0 ] - > { 'tabRenderer' } { 'content' } { 'richGridRenderer' } { 'contents' } , $ myProgress ) ;
2021-02-27 01:05:38 -07:00
}
sub additionalGet {
2021-03-01 14:31:36 -07:00
# We need these tokens in order to get more suggestions.
2021-02-27 01:05:38 -07:00
my $ continuationItem = shift ;
2021-03-01 14:31:36 -07:00
my $ myProgress = shift ;
2021-02-27 01:05:38 -07:00
$ 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' } ) ) {
2021-03-01 14:31:36 -07:00
return addVideos ( $ json - > { 'onResponseReceivedActions' } [ 0 ] - > { 'appendContinuationItemsAction' } { 'continuationItems' } , $ myProgress ) ;
2021-02-27 01:05:38 -07:00
}
}
sub loadSugs {
my $ counter = 1 ;
2021-03-03 15:41:49 -07:00
splice ( @ suggestArray , 0 , scalar ( @ suggestArray ) ) ;
splice ( @ watchArray , 0 , scalar ( @ watchArray ) ) ;
2021-02-27 01:05:38 -07:00
for ( @ videos ) {
my $ title = $ _ - > { title } ;
my $ playtime = $ _ - > { runtime } ;
2021-03-01 14:31:36 -07:00
unless ( $ playtime ) { $ playtime = "Live" ; }
2021-02-27 01:05:38 -07:00
push ( @ suggestArray , sprintf ( '%3.3s.' , $ counter ) . sprintf ( '%8.8s ' , $ playtime ) . $ title ) ;
$ counter + + ;
}
2021-03-01 15:59:23 -07:00
for ( @ watchlist ) {
my $ title = $ _ - > { title } ;
my $ playtime = $ _ - > { runtime } ;
unless ( $ playtime ) { $ playtime = "Live" ; }
push ( @ watchArray , sprintf ( '%8.8s ' , $ playtime ) . $ title ) ;
}
2021-02-27 01:05:38 -07:00
}
2021-03-01 14:31:36 -07:00
open ( CACHED , $ cachedFile ) or goto SKIPCACHED ;
2021-03-01 15:59:23 -07:00
$ readFile = "" ;
2021-02-27 01:05:38 -07:00
while ( <CACHED> ) { $ readFile . = $ _ ; }
close ( CACHED ) ;
2021-03-03 15:32:44 -07:00
if ( $ readFile ) { @ videos = @ { decode_json ( $ readFile ) } ; }
2021-02-27 01:05:38 -07:00
SKIPCACHED:
2021-03-01 14:31:36 -07:00
`touch $debugFile` ;
`touch $cachedFile` ;
`touch $playlistFile` ;
open ( DEBUG , ">" , $ debugFile ) ;
open ( CACHING , ">" , $ cachedFile ) ;
open ( SAVE , ">" , $ playlistFile ) ;
2023-02-15 14:23:16 -07:00
close ( STDERR ) ;
open ( STDERR , ">>" , $ debugFile ) ;
select ( ( select ( DEBUG ) , $| = 1 ) [ 0 ] ) ;
2021-03-01 14:31:36 -07:00
sub sync2files {
2022-07-08 13:06:02 -06:00
truncate ( DEBUG , 0 ) ;
truncate ( CACHING , 0 ) ;
truncate ( SAVE , 0 ) ;
2021-03-01 14:31:36 -07:00
2022-07-08 13:06:02 -06:00
seek ( DEBUG , 0 , 0 ) ;
seek ( CACHING , 0 , 0 ) ;
seek ( SAVE , 0 , 0 ) ;
2021-03-01 14:31:36 -07:00
2022-07-08 13:06:02 -06:00
if ( $ rawFile ) { print DEBUG $ rawFile ; }
print CACHING encode_json ( \ @ videos ) ;
print SAVE encode_json ( \ @ watchlist ) ;
2021-03-01 14:31:36 -07:00
}
2021-02-27 01:05:38 -07:00
####################################################
# 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' ) ;
2023-02-15 14:23:16 -07:00
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" ) ;
2021-02-27 01:05:38 -07:00
loadSugs ( ) ;
$ cui - > set_binding ( sub { $ cui - > mainloopExit ( ) ; } , "q" ) ;
$ suggestions - > set_binding ( sub {
2021-03-02 08:40:09 -07:00
push ( @ watchArray , ( split ( ". " , splice ( @ suggestArray , $ suggestions - > get_active_id ( ) , 1 ) , 2 ) ) [ 1 ] ) ;
2021-03-01 15:59:23 -07:00
push ( @ watchlist , splice ( @ videos , $ suggestions - > get_active_id ( ) , 1 ) ) ;
2021-03-01 14:31:36 -07:00
sync2files ( ) ;
2021-02-27 01:05:38 -07:00
$ suggestions - > draw ( ) ;
$ wlist - > draw ( ) ;
} , KEY_ENTER ) ;
2022-10-25 16:21:43 -06:00
2021-03-01 15:59:23 -07:00
$ suggestions - > set_binding ( sub {
2023-02-14 14:48:04 -07:00
my $ info = ( split ( $ suggestArray [ $ suggestions - > get_active_id ( ) ] , 2 ) ) [ 1 ] ;
my $ video = $ videos [ $ suggestions - > get_active_id ( ) ] ;
2022-10-25 16:00:30 -06:00
unless ( $ video ) { return 0 ; }
2023-02-14 14:48:04 -07:00
$ cui - > status ( "Playing Video, controls will resume once video is compelte\n Currently Playing: " . $ info ) ;
if ( 0 == system ( "mpv --volume=" . $ volume . " --script-opts=ytdl_hook-ytdl_path=yt-dlp --ytdl-raw-options=cookies=$cookiesFile,buffer-size=200M,format='bestvideo[height<=720]+bestaudio/best[height<=720]' --terminal=no https://www.youtube.com/watch?v=$video->{id}" ) ) {
2022-07-08 13:06:02 -06:00
$ win - > draw ( ) ;
$ suggestions - > draw ( ) ;
$ wlist - > draw ( ) ;
2023-02-14 14:48:04 -07:00
$ cui - > status ( "Playback Complete" ) ;
2022-07-08 13:06:02 -06:00
} else {
2023-02-14 14:48:04 -07:00
$ cui - > status ( "PLAYBACK ERROR" ) ;
2022-07-08 13:06:02 -06:00
}
2021-02-27 01:05:38 -07:00
} , "p" ) ;
2022-10-25 16:21:43 -06:00
2021-03-03 15:32:44 -07:00
$ wlist - > set_binding ( sub {
2022-07-08 13:06:02 -06:00
my $ video = $ watchlist [ $ wlist - > get_active_id ( ) ] ;
2022-10-25 16:00:30 -06:00
unless ( $ video ) { return 0 ; }
2022-07-08 13:06:02 -06:00
$ cui - > status ( "Playing Video, controls will resume once video is compelte\n Currently Playing: " . $ watchArray [ $ wlist - > get_active_id ( ) ] ) ;
2022-10-26 15:47:23 -06:00
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}" ) ) {
2022-10-25 16:00:30 -06:00
splice ( @ watchArray , $ wlist - > get_active_id ( ) , 1 ) ;
2022-07-08 13:06:02 -06:00
splice ( @ watchlist , $ wlist - > get_active_id ( ) , 1 ) ;
sync2files ( ) ;
$ win - > draw ( ) ;
$ suggestions - > draw ( ) ;
$ wlist - > draw ( ) ;
2022-10-25 16:00:30 -06:00
$ cui - > status ( "Playback Complete, video cleared" ) ;
2022-07-08 13:06:02 -06:00
} else {
$ cui - > status ( "PLAYBACK ERROR: Video not removed for safety." ) ;
}
2021-03-03 15:32:44 -07:00
} , "p" ) ;
2022-10-25 16:21:43 -06:00
2023-02-14 14:48:04 -07:00
$ 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}" ) ) {
$ win - > draw ( ) ;
$ suggestions - > draw ( ) ;
$ wlist - > draw ( ) ;
$ cui - > status ( "Playback Complete" ) ;
} else {
$ cui - > status ( "PLAYBACK ERROR." ) ;
}
} , "w" ) ;
2021-03-01 15:59:23 -07:00
$ suggestions - > set_binding ( sub {
2021-02-27 01:05:38 -07:00
splice ( @ videos , 0 , scalar ( @ videos ) ) ;
splice ( @ suggestArray , 0 , scalar ( @ suggestArray ) ) ;
$ cui - > status ( "Refreshing from Youtube, please wait..." ) ;
2021-03-01 14:31:36 -07:00
$ recursionWatchdog = 0 ;
my $ progressBar = $ win - > add ( 'refresh' , 'Progressbar' , - max = > 50 , - pos = > 0 , - y = > $ suggestions - > height ( ) / 2 ) ;
$ progressBar - > focus ( ) ;
firstGet ( $ progressBar ) ;
2021-02-27 01:05:38 -07:00
loadSugs ( ) ;
2021-03-01 14:31:36 -07:00
sync2files ( ) ;
2021-02-27 01:05:38 -07:00
$ win - > draw ( ) ;
$ suggestions - > draw ( ) ;
$ wlist - > draw ( ) ;
} , "r" ) ;
2022-10-25 16:21:43 -06:00
2021-03-01 15:59:23 -07:00
$ suggestions - > set_binding ( sub {
2021-02-27 01:05:38 -07:00
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" ) ;
2022-10-25 16:21:43 -06:00
2021-03-03 15:32:44 -07:00
$ wlist - > set_binding ( sub {
2022-10-25 16:00:30 -06:00
my $ vid = $ watchlist [ $ wlist - > get_active_id ( ) ] ;
2021-03-03 15:32:44 -07:00
$ cui - > status ( "Video: " . $ vid - > { title } . "\nChannel: " . $ vid - > { channel } . "\nRuntime: " . $ vid - > { runtime } . "\nPublished: " . $ vid - > { published } . "\nURL: https://www.youtube.com/watch?v=" . $ vid - > { id } ) ;
} , "i" ) ;
2021-02-27 01:05:38 -07:00
2022-10-25 16:21:43 -06:00
$ wlist - > set_binding ( sub {
2022-10-26 15:47:23 -06:00
@ watchlist = sort { $ a - > { runtime } cmp $ b - > { runtime } } @ watchlist ;
sync2files ( ) ;
loadSugs ( ) ;
$ wlist - > draw ( ) ;
} , ">" ) ;
2022-10-25 16:21:43 -06:00
$ wlist - > set_binding ( sub {
2022-10-26 15:47:23 -06:00
@ watchlist = sort { $ b - > { runtime } cmp $ a - > { runtime } } @ watchlist ;
sync2files ( ) ;
loadSugs ( ) ;
$ wlist - > draw ( ) ;
} , "<" ) ;
2022-10-25 16:21:43 -06:00
2023-02-14 14:48:04 -07:00
$ wlist - > set_binding ( sub {
2023-02-15 14:23:16 -07:00
print DEBUG "Manually adding a video...\n" ;
2023-02-14 14:48:04 -07:00
$ cui - > status ( "Manual Adding not implimented at this time" ) ;
2023-02-15 14:23:16 -07:00
$ 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 ( ) ;
2023-02-14 14:48:04 -07:00
} , "m" ) ;
2022-10-25 16:21:43 -06:00
$ suggestions - > set_binding ( sub {
2023-02-14 14:48:04 -07:00
$ 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" ) ;
2022-10-25 16:21:43 -06:00
} , "?" ) ;
$ wlist - > set_binding ( sub {
2023-02-14 14:48:04 -07:00
$ 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" ) ;
2022-10-25 16:21:43 -06:00
} , "?" ) ;
2021-02-27 01:05:38 -07:00
$ suggestions - > focus ( ) ;
$ cui - > mainloop ( ) ;
#Save leftovers
2021-03-01 14:31:36 -07:00
sync2files ( ) ;
2023-02-15 14:23:16 -07:00
print DEBUG "Closeing Program\n" ;
2021-02-27 01:05:38 -07:00
close ( DEBUG ) ;
close ( CACHING ) ;
close ( SAVE ) ;