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