linkhandler (13560B)
1 #!/usr/bin/env perl 2 # vim: foldmethod=marker 3 use strict; 4 use warnings; 5 use 5.006; # checked with `perlver` 6 7 use constant EXIT_USER_CANCELLED => 130; 8 use constant MPD_HOST => 'localhost'; 9 use constant MPD_PORT => 6600; 10 11 sub urlize { 12 my ($rv) = @_; 13 $rv =~ s/([^A-Za-z0-9])/sprintf("%%%2.2X", ord($1))/ge; 14 return $rv; 15 } 16 17 sub un_urlize { 18 my ($rv) = @_; 19 $rv =~ s/\+/ /g; 20 $rv =~ s/%(..)/pack("c",hex($1))/ge; 21 return $rv; 22 } 23 24 sub untrack { 25 my ($orig_link) = @_; 26 if ($orig_link =~ /(https?(?::|%3A)(?:%2F%2F|\/\/).*)/) { 27 $orig_link = un_urlize($1); 28 } 29 $orig_link =~ s/https?(?::|%3A)(?:%2F%2F|\/\/).*(https?(?::|%3A)(?:%2F%2F|\/\/).*)/$1/; 30 $orig_link =~ s/utm_[^&]*&?//g; 31 $orig_link =~ s/\?$//; 32 33 if ($orig_link =~ /lnks\.gd\// or $orig_link =~ /url.*\.creators\.gumroad\.com/) { 34 $orig_link = `curl --silent --head --write-out '%{redirect_url}' --output /dev/null "$orig_link"`; 35 } 36 return $orig_link; 37 } 38 39 my $CHOOSER = $ENV{'CHOOSER'}; 40 unless (length $CHOOSER) { 41 die 'Must set CHOOSER in environment.'; 42 } 43 44 my $HOME = $ENV{'HOME'}; 45 my $TERM = $ENV{'TERM'}; 46 47 if ( @ARGV != 1 ) { 48 die 'Link necessary.'; 49 } 50 my ($LINK) = @ARGV; 51 $LINK = untrack($LINK); 52 53 =begin 54 This subroutine takes a subroutine reference and executes the dereferenced subroutine in a fork. 55 Basically a way to handle things async by just writing `detach sub { .... }` in code. 56 Using a fork is OK here because: "If a parent process terminates, then its 57 "zombie" children (if any) are adopted by init(8), which automatically performs 58 a wait to remove the zombies." 59 # https://linux.die.net/man/2/wait 60 (Also, what a sentence. Fun without the context.) 61 =cut 62 sub detach { 63 my $funcref = shift; 64 if ( fork() == 0 ) { 65 $funcref->(); 66 } 67 return; 68 } 69 70 # Runs arguments in surrounding terminal, or creates a new one. 71 sub launch_in_terminal { 72 my $args = shift; 73 if (($TERM eq 'dumb') or (not defined $TERM)) { 74 system(qq(alacritty -e $args)); 75 } 76 else { 77 system($args); 78 } 79 return; 80 } 81 82 # Send notification via my cross-platform-ish `notify` script 83 sub notify { 84 my ($title, $message) = @_; 85 system qq(notify '$title' '$message' linkhandler >/dev/null 2>&1); 86 return; 87 } 88 89 # String checking functions {{{1 90 # Checks if $str (includes|endswith|startswith) any of $substrs 91 sub checkstr { 92 my ($str, $mode, $substrs) = @_; 93 my @result; 94 if ($mode eq 'includes') { 95 @result = grep { index( $str, $_ ) != -1 } @{$substrs}; 96 } 97 elsif ($mode eq 'ends') { 98 @result = grep { $str =~ /\Q$_\E$/msx } @{$substrs}; 99 } 100 elsif ($mode eq 'starts') { 101 @result = grep { $str =~ /^\Q$_\E/msx } @{$substrs}; 102 } 103 return scalar @result; 104 } 105 106 sub is_bandcamp_track { 107 my $link = shift; 108 return (checkstr($link, 'includes', ['bandcamp.com', 'godisanastronaut.com']) and checkstr($link, 'includes', ['/track/'])); 109 } 110 111 sub is_bandcamp_album { 112 my $link = shift; 113 return (checkstr($link, 'includes', ['bandcamp.com', 'godisanastronaut.com']) and checkstr($link, 'includes', ['/album/'])); 114 } 115 116 sub is_video { 117 my $link = shift; 118 return (checkstr($link, 'ends', ['mkv', 'webm', 'mp4']) 119 or checkstr($link, 'includes', ['youtube.com/watch', 'youtube.com/playlist', 'yewtu.be', 'youtu.be', 120 'hooktube.com', 'bitchute.com', 'videos.lukesmith.xyz', 'v.redd.it', 'fb.watch', 'vimeo.com'])); 121 } 122 sub is_image { 123 my $link = shift; 124 return checkstr($link, 'ends', ['png', 'jpg', 'jpe', 'jpeg', 'gif']); 125 } 126 127 sub is_gifv { 128 my $link = shift; 129 return checkstr($link, 'ends', ['gifv']); 130 } 131 132 sub is_audio { 133 my $link = shift; 134 return (checkstr($link, 'ends', ['mp3', 'flac', 'opus', 'mp3?source']) 135 or checkstr($link, 'includes', ['soundcloud.com'])); 136 } 137 138 # Menu builders {{{1 139 # Choose from @options (passed by reference) via $CHOOSER 140 sub choose { 141 my $options_ref = shift; 142 my $options_str = join "\n", @{$options_ref}; 143 my $selected = `printf '$options_str\n' | "$CHOOSER"`; 144 $selected =~ s/\s+$//msx; 145 unless (length $selected) { 146 exit EXIT_USER_CANCELLED; # user interrupted 147 } 148 return $selected; 149 } 150 151 # Receives a dispatch table of strings to subroutines, as an array. 152 # Lets you select one of the strings via `choose`. 153 # Returns reference to corresponding subroutine. 154 sub menu { 155 my $tableref = shift; 156 157 # Global system open -- should be available for every link 158 159 push @$tableref, ['Open (system)', sub { 160 my ($link) = @_; 161 system('open', $link); 162 }]; 163 164 # Global copy -- should be available for every link 165 push @$tableref, ['Copy', sub { 166 my ($link) = @_; 167 system(qq(printf '%s' '$link' | clc)); 168 notify 'Copied to clipboard', $link; 169 }]; 170 171 push @$tableref, ['Save', sub { 172 my ($link) = @_; 173 detach sub { 174 system(qq(pocket save '$link')); 175 notify 'Saved to Pocket', $link; 176 } 177 }]; 178 179 push @$tableref, ['Archive', sub { 180 my ($link) = @_; 181 detach sub { 182 web_archive($link); 183 notify "Archived", "$link"; 184 } 185 }]; 186 187 push @$tableref, ['Archive (and copy archived)', sub { 188 my ($link) = @_; 189 detach sub { 190 my $location = web_archive($link); 191 system(qq(printf '%s' '$location' | clc)); 192 notify "Archived $link", "Archived location copied to clipboard: $location"; 193 } 194 }]; 195 196 push @$tableref, ['Send via KDEConnect', sub { 197 my ($link) = @_; 198 detach sub { 199 system(qq(kdeconnect-handler "$link")); 200 } 201 }]; 202 203 push @$tableref, ['Noecho News', sub { 204 my ($link) = @_; 205 detach sub { 206 notify 'Finding different perspectives...', "$link"; 207 launch_in_terminal(qq(article-noecho-news "$link")); 208 } 209 }]; 210 211 # Choose an option 212 my @options = map { $_->[0] } @$tableref; 213 my $choice = choose(\@options); 214 215 # Return corresponding the subroutine reference 216 my $choice_index = (grep { $options[$_] eq $choice } 0..$#options)[0]; 217 return $tableref->[$choice_index][1]; 218 } 219 220 # Save in the Internet Archive 221 sub web_archive { 222 my $link = shift; 223 my $location = `curl -sI 'https://web.archive.org/save/$link' | awk -F': ' '/^location/ { print \$2 }'`; 224 return $location; 225 } 226 227 228 # How to play {{{1 229 sub play_audio_mpd { 230 my $link = shift; 231 232 detach sub { 233 if (checkstr($link, 'includes', ['youtube.com/playlist'])) { 234 system(qq([ -d "$HOME/.cache/mpd" ] || mkdir -p "$HOME/.cache/mpd")); 235 open(my $playlist, '-|', qq(youtube-dl --flat-playlist -j '$link' | jq -r '.url')) or die "Couldn't read playlist $link"; 236 while (my $song = <$playlist>) { 237 chomp $song; 238 system(qq( 239 title=\$(youtube-dl --ignore-config --get-title '$song' 2>/dev/null); 240 printf "%b\n" "\$title\t$song" >> "$HOME/.cache/mpd/linkarchive"; 241 242 send_mpd_command() { 243 (printf '%s\n' "\$1"; sleep 1) | telnet ${\MPD_HOST} ${\MPD_PORT} 2>/dev/null 244 } 245 246 url="\$(youtube-dl -x -g "$song")" 247 res="\$(send_mpd_command "addid \$url")" 248 song_id="\$(printf '%s' "\$res" | awk -F': ' '/^Id: / { print \$2 }')" 249 send_mpd_command "\$(printf 'addtagid %s Artist "%s"\naddtagid %s Title "%s"' "\$song_id" "Youtube" "\$song_id" "\$title")" >/dev/null)); 250 } 251 close($playlist) or die "Could not close handle."; 252 253 notify 'Added playlist', $link; 254 } 255 else { 256 system(qq([ -d "$HOME/.cache/mpd" ] || mkdir -p "$HOME/.cache/mpd"; 257 title=\$(youtube-dl --ignore-config --get-title "$link" 2>/dev/null); 258 printf "%b\n" "\$title\t$link" >> "$HOME/.cache/mpd/linkarchive"; 259 260 send_mpd_command() { 261 (printf '%s\n' "\$1"; sleep 1) | telnet ${\MPD_HOST} ${\MPD_PORT} 2>/dev/null 262 } 263 264 url="\$(youtube-dl -x -g "$link")" 265 res="\$(send_mpd_command "addid \$url")" 266 song_id="\$(printf '%s' "\$res" | awk -F': ' '/^Id: / { print \$2 }')" 267 send_mpd_command "\$(printf 'addtagid %s Artist "%s"\naddtagid %s Title "%s"' "\$song_id" "Youtube" "\$song_id" "\$title")" >/dev/null)); 268 }; 269 } 270 } 271 272 sub play_audio_mpv { 273 my $link = shift; 274 system(qq(mpv --no-audio-display --no-video --volume=50 '$link')); 275 } 276 277 sub play_video_mpv { 278 my $link = shift; 279 detach sub { 280 system(qq(mpvq '$link' >/dev/null 2>&1)); 281 notify 'Starting mpv', "Opening $link..."; 282 } 283 } 284 285 # How to download {{{1 286 sub download_bandcamp { 287 my $link = shift; 288 detach sub { 289 my $download_dir = "$HOME/Downloads/songs/listen to"; 290 system(qq(mkdir -p "$download_dir")); 291 chdir($download_dir); 292 ( my $name = $link ) =~ s!^.*/!!; 293 ( my $artist = $link ) =~ s|https*://||; 294 $artist =~ s/\.bandcamp\.com.*//; 295 296 notify( "Downloading $name by $artist", "Downloading $link" ); 297 system(qq(mkdir -p $artist)) unless ( -d $artist ); 298 chdir($artist); 299 system(qq(mkdir -p $name)) unless ( -d $name ); 300 chdir($name); 301 system qq(youtube-dl -f mp3 -o "%(playlist_index)s %(title)s %(id)s.%(ext)s" '$link' >/dev/null 2>&1); 302 system(qq(printf "#EXTM3U\n#PLAYLIST:%s\n#EXTART:%s\n" "$name" "$artist" > "$name".m3u)); 303 system(qq(youtube-dl -f mp3 --get-filename -o "%(playlist_index)s %(title)s %(id)s.%(ext)s" '$link' >> "$name".m3u)); 304 notify( "Finished downloading $name by $artist", 305 "Downloaded $link" ); 306 }; 307 } 308 309 sub download_audio { 310 my $link = shift; 311 detach sub { 312 my $download_dir = "$HOME/Downloads/songs/listen to"; 313 notify 'Download (audio) started', "Downloading $link"; 314 system(qq(youtube-dl --add-metadata -xic -f bestaudio/best -o "$download_dir/%(title)s-%(creator)s.%(ext)s" --exec "notify 'Download finished' 'Downloaded $link.' linkhandler" '$link' >/dev/null 2>&1)); 315 }; 316 } 317 318 sub download_video { 319 my $link = shift; 320 my $download_dir = "$HOME/Downloads"; 321 322 my @choices = ( 323 ['Both', sub { 324 my $link = shift; 325 detach sub { 326 notify 'Download (av) started', "Downloading $link"; 327 system(qq(youtube-dl --add-metadata -ic --write-sub --embed-subs -o "$download_dir/%(title)s-%(creator)s.%(ext)s" --exec "notify 'Download finished' 'Downloaded $link.' linkhandler" '$link' >/dev/null 2>&1)); 328 }; 329 }], 330 ['Audio', \&download_audio], 331 ['Video', sub { 332 my $link = shift; 333 detach sub { 334 notify 'Download (video) started', "Downloading $link"; 335 system(qq(youtube-dl --add-metadata -ic -f bestvideo --write-sub --embed-subs -o "$download_dir/%(title)s-%(creator)s.%(ext)s" --exec "notify 'Download finished' 'Downloaded $link.' linkhandler" '$link' >/dev/null 2>&1)); 336 }; 337 }] 338 ); 339 my $selected_ref = menu(\@choices); 340 $selected_ref->($link); 341 } 342 343 # Main {{{1 344 if (is_bandcamp_track($LINK)) { 345 my @choices = ( 346 ['Download', \&download_bandcamp], 347 ['Play', sub { 348 my $link = shift; 349 my @choices = ( 350 ['Audio (queue in mpd)', \&play_audio_mpd], 351 ['Audio (mpv)', \&play_audio_mpv] 352 ); 353 354 my $selected_ref = menu(\@choices); 355 $selected_ref->($link); 356 }] 357 ); 358 my $selected_ref = menu(\@choices); 359 $selected_ref->($LINK); 360 } 361 elsif (is_bandcamp_album($LINK)) { 362 my @choices = ( 363 ['Download', \&download_bandcamp], 364 ['Play', sub { 365 my $link = shift; 366 my @choices = ( 367 # ['Audio (queue in mpd)', \&play_audio_mpd], 368 ['Audio (mpv)', \&play_audio_mpv] 369 ); 370 371 my $selected_ref = menu(\@choices); 372 $selected_ref->($link); 373 }] 374 ); 375 my $selected_ref = menu(\@choices); 376 $selected_ref->($LINK); 377 } 378 elsif (is_video($LINK)) { 379 my @choices = ( 380 ['Play', sub { 381 my $link = shift; 382 383 my @choices = ( 384 ['Video', \&play_video_mpv], 385 ['Audio (queue in mpd)', \&play_audio_mpd], 386 ['Audio (mpv)', \&play_audio_mpv] 387 ); 388 my $selected_ref = menu(\@choices); 389 $selected_ref->($LINK); 390 }], 391 ['Download', \&download_video] 392 ); 393 my $selected_ref = menu(\@choices); 394 $selected_ref->($LINK); 395 } 396 elsif (is_image($LINK)) { 397 my @choices = ( 398 ['View (nsxiv)', sub { 399 my $link = shift; 400 detach sub { 401 notify 'Starting image viewer', "Opening $link..."; 402 system(qq(curl -sL '$link' >"/tmp/\$(printf "%s" '$link' | sed "s/.*\\///")")); 403 system(qq(opener "/tmp/\$(printf "%s" '$link' | sed "s/.*\\///")" >/tmp/error 2>&1)); 404 } 405 }] 406 ); 407 my $selected_ref = menu(\@choices); 408 $selected_ref->($LINK); 409 } 410 elsif (is_gifv($LINK)) { 411 my @choices = ( 412 ['View (mpv)', sub { 413 my $link = shift; 414 detach sub { 415 system(qq(mpv --volume=50 '$link' >/dev/null 2>&1)); 416 }; 417 }] 418 ); 419 my $selected_ref = menu(\@choices); 420 $selected_ref->($LINK); 421 } 422 elsif (is_audio($LINK)) { 423 my @choices = ( 424 ['Download', \&download_audio], 425 ['Play', sub { 426 my $link = shift; 427 my @choices = ( 428 ['Audio (queue in mpd)', \&play_audio_mpd], 429 ['Audio (mpv)', \&play_audio_mpv] 430 ); 431 432 my $selected_ref = menu(\@choices); 433 $selected_ref->($link); 434 }] 435 ); 436 my $selected_ref = menu(\@choices); 437 $selected_ref->($LINK); 438 } 439 elsif (checkstr($LINK, 'includes', ['reddit.com'])) { 440 my @choices = ( 441 ['reddio', sub { 442 my $link = shift; 443 # Have to go via bash here to be able to pipe to `less` 444 launch_in_terminal(qq(bash -c 'reddio print -c always "comments/\$(printf "%s" '$link' | cut -d/ -f7)" | less -+F -+X')); 445 }] 446 ); 447 my $selected_ref = menu(\@choices); 448 $selected_ref->($LINK); 449 } 450 elsif (checkstr($LINK, 'starts', ['http://', 'https://'])) { 451 my @choices = ( 452 ['w3m', sub { 453 my $link = shift; 454 launch_in_terminal(qq(w3m -config ~/.config/w3m/config -T text/html '$link')); 455 }]); 456 my $selected_ref = menu(\@choices); 457 $selected_ref->($LINK); 458 } 459 else { 460 if ( -f $LINK ) { 461 system(qq(\${EDITOR:-vim} '$LINK')); 462 } 463 else { 464 system(qq(open '$LINK' >/dev/null 2>&1)); 465 } 466 }