linkhandler (13894B)
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 204 push @$tableref, ['Create QR code', sub { 205 my ($link) = @_; 206 detach sub { 207 system(qq(qrencode -d 300 -t png "$link" -o /tmp/qr.png)); 208 system(qq(nsxiv /tmp/qr.png)); 209 } 210 }]; 211 212 push @$tableref, ['Noecho News', sub { 213 my ($link) = @_; 214 detach sub { 215 notify 'Finding different perspectives...', "$link"; 216 launch_in_terminal(qq(article-noecho-news "$link")); 217 } 218 }]; 219 220 push @$tableref, ['Quicksilver', sub { 221 my ($link) = @_; 222 detach sub { 223 system(qq(printf '%s' '$link' | qs)); 224 } 225 }]; 226 227 # Choose an option 228 my @options = map { $_->[0] } @$tableref; 229 my $choice = choose(\@options); 230 231 # Return corresponding the subroutine reference 232 my $choice_index = (grep { $options[$_] eq $choice } 0..$#options)[0]; 233 return $tableref->[$choice_index][1]; 234 } 235 236 # Save in the Internet Archive 237 sub web_archive { 238 my $link = shift; 239 my $location = `curl -sI 'https://web.archive.org/save/$link' | awk -F': ' '/^location/ { print \$2 }'`; 240 return $location; 241 } 242 243 244 # How to play {{{1 245 sub play_audio_mpd { 246 my $link = shift; 247 248 detach sub { 249 if (checkstr($link, 'includes', ['youtube.com/playlist'])) { 250 system(qq([ -d "$HOME/.cache/mpd" ] || mkdir -p "$HOME/.cache/mpd")); 251 open(my $playlist, '-|', qq(youtube-dl --flat-playlist -j '$link' | jq -r '.url')) or die "Couldn't read playlist $link"; 252 while (my $song = <$playlist>) { 253 chomp $song; 254 system(qq( 255 title=\$(youtube-dl --ignore-config --get-title '$song' 2>/dev/null); 256 printf "%b\n" "\$title\t$song" >> "$HOME/.cache/mpd/linkarchive"; 257 258 send_mpd_command() { 259 (printf '%s\n' "\$1"; sleep 1) | telnet ${\MPD_HOST} ${\MPD_PORT} 2>/dev/null 260 } 261 262 url="\$(youtube-dl -x -g "$song")" 263 res="\$(send_mpd_command "addid \$url")" 264 song_id="\$(printf '%s' "\$res" | awk -F': ' '/^Id: / { print \$2 }')" 265 send_mpd_command "\$(printf 'addtagid %s Artist "%s"\naddtagid %s Title "%s"' "\$song_id" "Youtube" "\$song_id" "\$title")" >/dev/null)); 266 } 267 close($playlist) or die "Could not close handle."; 268 269 notify 'Added playlist', $link; 270 } 271 else { 272 system(qq([ -d "$HOME/.cache/mpd" ] || mkdir -p "$HOME/.cache/mpd"; 273 title=\$(youtube-dl --ignore-config --get-title "$link" 2>/dev/null); 274 printf "%b\n" "\$title\t$link" >> "$HOME/.cache/mpd/linkarchive"; 275 276 send_mpd_command() { 277 (printf '%s\n' "\$1"; sleep 1) | telnet ${\MPD_HOST} ${\MPD_PORT} 2>/dev/null 278 } 279 280 url="\$(youtube-dl -x -g "$link")" 281 res="\$(send_mpd_command "addid \$url")" 282 song_id="\$(printf '%s' "\$res" | awk -F': ' '/^Id: / { print \$2 }')" 283 send_mpd_command "\$(printf 'addtagid %s Artist "%s"\naddtagid %s Title "%s"' "\$song_id" "Youtube" "\$song_id" "\$title")" >/dev/null)); 284 }; 285 } 286 } 287 288 sub play_audio_mpv { 289 my $link = shift; 290 system(qq(mpv --no-audio-display --no-video --volume=50 '$link')); 291 } 292 293 sub play_video_mpv { 294 my $link = shift; 295 detach sub { 296 system(qq(mpvq '$link' >/dev/null 2>&1)); 297 notify 'Starting mpv', "Opening $link..."; 298 } 299 } 300 301 # How to download {{{1 302 sub download_bandcamp { 303 my $link = shift; 304 detach sub { 305 my $download_dir = "$HOME/Downloads/songs/listen to"; 306 system(qq(mkdir -p "$download_dir")); 307 chdir($download_dir); 308 ( my $name = $link ) =~ s!^.*/!!; 309 ( my $artist = $link ) =~ s|https*://||; 310 $artist =~ s/\.bandcamp\.com.*//; 311 312 notify( "Downloading $name by $artist", "Downloading $link" ); 313 system(qq(mkdir -p $artist)) unless ( -d $artist ); 314 chdir($artist); 315 system(qq(mkdir -p $name)) unless ( -d $name ); 316 chdir($name); 317 system qq(youtube-dl -f mp3 -o "%(playlist_index)s %(title)s %(id)s.%(ext)s" '$link' >/dev/null 2>&1); 318 system(qq(printf "#EXTM3U\n#PLAYLIST:%s\n#EXTART:%s\n" "$name" "$artist" > "$name".m3u)); 319 system(qq(youtube-dl -f mp3 --get-filename -o "%(playlist_index)s %(title)s %(id)s.%(ext)s" '$link' >> "$name".m3u)); 320 notify( "Finished downloading $name by $artist", 321 "Downloaded $link" ); 322 }; 323 } 324 325 sub download_audio { 326 my $link = shift; 327 detach sub { 328 my $download_dir = "$HOME/Downloads/songs/listen to"; 329 notify 'Download (audio) started', "Downloading $link"; 330 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)); 331 }; 332 } 333 334 sub download_video { 335 my $link = shift; 336 my $download_dir = "$HOME/Downloads"; 337 338 my @choices = ( 339 ['Both', sub { 340 my $link = shift; 341 detach sub { 342 notify 'Download (av) started', "Downloading $link"; 343 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)); 344 }; 345 }], 346 ['Audio', \&download_audio], 347 ['Video', sub { 348 my $link = shift; 349 detach sub { 350 notify 'Download (video) started', "Downloading $link"; 351 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)); 352 }; 353 }] 354 ); 355 my $selected_ref = menu(\@choices); 356 $selected_ref->($link); 357 } 358 359 # Main {{{1 360 if (is_bandcamp_track($LINK)) { 361 my @choices = ( 362 ['Download', \&download_bandcamp], 363 ['Play', sub { 364 my $link = shift; 365 my @choices = ( 366 ['Audio (queue in mpd)', \&play_audio_mpd], 367 ['Audio (mpv)', \&play_audio_mpv] 368 ); 369 370 my $selected_ref = menu(\@choices); 371 $selected_ref->($link); 372 }] 373 ); 374 my $selected_ref = menu(\@choices); 375 $selected_ref->($LINK); 376 } 377 elsif (is_bandcamp_album($LINK)) { 378 my @choices = ( 379 ['Download', \&download_bandcamp], 380 ['Play', sub { 381 my $link = shift; 382 my @choices = ( 383 # ['Audio (queue in mpd)', \&play_audio_mpd], 384 ['Audio (mpv)', \&play_audio_mpv] 385 ); 386 387 my $selected_ref = menu(\@choices); 388 $selected_ref->($link); 389 }] 390 ); 391 my $selected_ref = menu(\@choices); 392 $selected_ref->($LINK); 393 } 394 elsif (is_video($LINK)) { 395 my @choices = ( 396 ['Play', sub { 397 my $link = shift; 398 399 my @choices = ( 400 ['Video', \&play_video_mpv], 401 ['Audio (queue in mpd)', \&play_audio_mpd], 402 ['Audio (mpv)', \&play_audio_mpv] 403 ); 404 my $selected_ref = menu(\@choices); 405 $selected_ref->($LINK); 406 }], 407 ['Download', \&download_video] 408 ); 409 my $selected_ref = menu(\@choices); 410 $selected_ref->($LINK); 411 } 412 elsif (is_image($LINK)) { 413 my @choices = ( 414 ['View (nsxiv)', sub { 415 my $link = shift; 416 detach sub { 417 notify 'Starting image viewer', "Opening $link..."; 418 system(qq(curl -sL '$link' >"/tmp/\$(printf "%s" '$link' | sed "s/.*\\///")")); 419 system(qq(opener "/tmp/\$(printf "%s" '$link' | sed "s/.*\\///")" >/tmp/error 2>&1)); 420 } 421 }] 422 ); 423 my $selected_ref = menu(\@choices); 424 $selected_ref->($LINK); 425 } 426 elsif (is_gifv($LINK)) { 427 my @choices = ( 428 ['View (mpv)', sub { 429 my $link = shift; 430 detach sub { 431 system(qq(mpv --volume=50 '$link' >/dev/null 2>&1)); 432 }; 433 }] 434 ); 435 my $selected_ref = menu(\@choices); 436 $selected_ref->($LINK); 437 } 438 elsif (is_audio($LINK)) { 439 my @choices = ( 440 ['Download', \&download_audio], 441 ['Play', sub { 442 my $link = shift; 443 my @choices = ( 444 ['Audio (queue in mpd)', \&play_audio_mpd], 445 ['Audio (mpv)', \&play_audio_mpv] 446 ); 447 448 my $selected_ref = menu(\@choices); 449 $selected_ref->($link); 450 }] 451 ); 452 my $selected_ref = menu(\@choices); 453 $selected_ref->($LINK); 454 } 455 elsif (checkstr($LINK, 'includes', ['reddit.com'])) { 456 my @choices = ( 457 ['reddio', sub { 458 my $link = shift; 459 # Have to go via bash here to be able to pipe to `less` 460 launch_in_terminal(qq(bash -c 'reddio print -c always "comments/\$(printf "%s" '$link' | cut -d/ -f7)" | less -+F -+X')); 461 }] 462 ); 463 my $selected_ref = menu(\@choices); 464 $selected_ref->($LINK); 465 } 466 elsif (checkstr($LINK, 'starts', ['http://', 'https://'])) { 467 my @choices = ( 468 ['w3m', sub { 469 my $link = shift; 470 launch_in_terminal(qq(w3m -config ~/.config/w3m/config -T text/html '$link')); 471 }]); 472 my $selected_ref = menu(\@choices); 473 $selected_ref->($LINK); 474 } 475 else { 476 if ( -f $LINK ) { 477 system(qq(\${EDITOR:-vim} '$LINK')); 478 } 479 else { 480 system(qq(open '$LINK' >/dev/null 2>&1)); 481 } 482 }