dotfiles

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

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 }