dotfiles

My personal shell configs and stuff
git clone git://git.alex.balgavy.eu/dotfiles.git
Log | Files | Refs | Submodules | README | LICENSE

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 }