aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xbm231
-rwxr-xr-xdownload_yt_playlist28
-rwxr-xr-xdups56
-rwxr-xr-xmdk69
-rwxr-xr-xmultiplayerctl57
-rwxr-xr-xplayingnow32
-rwxr-xr-xyt144
7 files changed, 617 insertions, 0 deletions
diff --git a/bm b/bm
new file mode 100755
index 0000000..4573135
--- /dev/null
+++ b/bm
@@ -0,0 +1,231 @@
+#!/usr/bin/env perl
+
+use strict;
+use warnings;
+
+use File::Basename qw<basename dirname>;
+use File::Find qw<find>;
+use File::Glob qw<:bsd_glob :nocase>;
+use File::Path qw<make_path>;
+use File::Which qw<which>;
+use IPC::Open2 qw<open2>;
+use List::Util qw<all>;
+
+use JSON qw<decode_json>;
+
+my $BMDIR = "$ENV{HOME}/.bm";
+mkdir $BMDIR || die $!;
+
+my $YT = which('yt');
+my $GIT = "git -C '$BMDIR'" if which('git') && -d "$BMDIR/.git";
+
+sub is_uri {
+ my $text = shift;
+ return $text =~ /^http/;
+}
+
+sub all_contents {
+ my @files;
+ my $pre_len = length $BMDIR;
+ find({wanted => sub {
+ if (substr($_, 0, 1) eq '.' && $File::Find::name ne $BMDIR) {
+ # Ignore hidden files/dirs (except $BMDIR)
+ $File::Find::prune = 1;
+ } elsif (-f) {
+ push @files, $File::Find::name;
+ }
+ }},
+ $BMDIR);
+ @files = sort { $a cmp $b } @files;
+ return ([], \@files);
+}
+
+sub dir_contents {
+ my $dir = shift;
+
+ my @dirs = $dir eq $BMDIR ? () : ('..');
+ my @files;
+
+ foreach (glob "$dir/*") {
+ if (-d) {
+ push @dirs, $_;
+ } elsif (-f) {
+ push @files, $_;
+ }
+ }
+
+ return (\@dirs, \@files);
+}
+
+sub read_url {
+ my $file = shift;
+ open(my $fh, '<', $file) || die $!;
+ my $url = <$fh>;
+ return unless $url && is_uri($url);
+ $url =~ s/\s*$//g;
+ return $url;
+}
+
+sub open_bm {
+ my $url = shift;
+ if ($YT && $url =~ /youtube\.com\/watch/) {
+ `yt $url`;
+ return;
+ }
+ my $focused_name = `xdotool getwindowfocus getwindowname`;
+ if ($focused_name =~ / (Firefox|Chrom(e|ium))$/) {
+ `xdotool - <<<"key ctrl+t\ntype --delay 1ms $url\nsleep 0.2\nkey Return Tab"`;
+ } else {
+ `xdg-open $url`;
+ }
+}
+
+sub url_domain {
+ my $url = shift;
+ my ($domain) = $url =~ /^[a-z]+:\/\/([^\/]+)/;
+ return $domain;
+}
+
+sub notify {
+ my ($urgency, $text) = @_;
+ `notify-send -u '$urgency' '$text'`;
+}
+
+sub save_bm {
+ my ($basedir, $text) = @_;
+ my ($uri, $title);
+ if (is_uri($text)) {
+ $uri = $text;
+ $title = `rofi -dmenu -p "Set title" -no-fixed-num-lines` or return;
+ $title =~ s/\n//g;
+ } else {
+ $uri = `rofi -dmenu -p "Set URL" -no-fixed-num-lines` or return;
+ $uri =~ s/\n//g;
+ if (is_uri($uri)) {
+ $title = $text;
+ } else {
+ notify('critical', 'Not an URL; skipping');
+ return;
+ }
+ }
+ my $path = "$basedir/$title";
+ make_path(dirname($path));
+ open(my $fh, '>',$path);
+ print $fh "$uri\n";
+ if ($GIT) {
+ `$GIT add '$path'`;
+ my $basename = basename($path);
+ `$GIT commit -m "Add bookmark '$basename'"`;
+ }
+ notify('normal', "Bookmark saved");
+}
+
+sub sanitize_rofi {
+ return shift =~ s/&/&amp;/gr;
+}
+
+sub draw_dir_entry {
+ my $dir = shift;
+ return "<b>" . sanitize_rofi(basename($dir)) . "</b>";
+}
+
+sub draw_file_entry {
+ my $file = shift;
+ my $url = read_url($file);
+ unless ($url) {
+ print "$file does not seem to contain a valid URL; skipping\n";
+ return;
+ }
+ return sanitize_rofi(basename($file)) . " <u><tt>" . url_domain($url) . "</tt></u>";
+}
+
+sub select_bm {
+ my $basedir = $BMDIR;
+ my $recursive = 0;
+ while (-d $basedir) {
+ my ($dirs, $files) = $recursive ? all_contents() : dir_contents($basedir);
+
+ my $msg = $recursive ? "All bookmarks" : "Open bookmark";
+ open2(my $outh, my $inh, "rofi -sync -p '$msg' -dmenu -multi-select -i -markup-rows -lines 40 -width 80 -format 'i,f'");
+ print $inh "$_\n" foreach (map { draw_dir_entry($_) } @$dirs);
+ print $inh "$_\n" foreach (map { draw_file_entry($_) } @$files);
+ close $inh;
+
+ # Get all output lines from rofi
+ my @out = map { [/^(-?\d+),(.*)$/] } <$outh> or die 'cancelled';
+
+ # If there are sereval selections, all of them must be files
+ if (all { $_->[0] > $#$dirs } @out) {
+ foreach my $out (@out) {
+ my ($idx, $input) = @$out;
+ my $url = read_url($files->[$idx - @$dirs]);
+ open_bm($url);
+ }
+ return;
+ } elsif (@out == 1) {
+ my ($idx, $input) = @{$out[0]};
+ if ($idx == -1) {
+ # User entered something which was not among the options
+ if ($input) {
+ # If there was something in the search bar, save it
+ save_bm($basedir, $input);
+ return;
+ } else {
+ # Otherwise, take it as the recursive toggle (C-Ret)
+ $recursive = ! $recursive;
+ $basedir = $BMDIR;
+ }
+ } elsif ($idx <= $#$dirs) {
+ # User selected a directory
+ my $dir = $dirs->[$idx];
+ if ($dir eq '..') {
+ $basedir = dirname($basedir);
+ } else {
+ $basedir .= '/' . basename($dir);
+ }
+ }
+ }
+ }
+}
+
+sub sanitize_path {
+ my $path = shift;
+ $path =~ s/\//-/g;
+ $path = substr($path, 0, 100);
+ #$path =~ s/\(/-/g;
+ return $path;
+}
+
+sub import_firefox {
+ my ($basedir, $json) = @_;
+ if ($json->{type} eq "text/x-moz-place-container") {
+ my $title = sanitize_path($json->{title});
+ my $newdir = "$basedir/$title";
+ mkdir $newdir;
+ import_firefox($newdir, $_) foreach (@{$json->{children}});
+ } elsif ($json->{type} eq "text/x-moz-place") {
+ my $uri = $json->{uri};
+ my $title = sanitize_path($json->{title});
+ my $newfile = "$basedir/$title";
+ open(my $fh, '>', $newfile) || print "Unable to open $newfile\n";
+ print $fh $uri;
+ }
+}
+
+if (@ARGV == 0) {
+ select_bm();
+} elsif ($ARGV[0] eq "-h") {
+ die "Usage: $0 [--import-firefox <bookmarks.json>] [git <git command ...>]\n";
+} elsif ($ARGV[0] eq "git") {
+ if ($GIT) {
+ shift;
+ exec 'git', '-C', $BMDIR, @ARGV;
+ } else {
+ die which('git') ? "$BMDIR is not a git repository\n" : "git is not installed\n";
+ }
+} elsif ($ARGV[0] eq "--import-firefox") {
+ open(my $fh, '<', $ARGV[1]);
+ my $json = decode_json(<$fh>);
+ close($fh);
+ import_firefox($BMDIR, $json);
+}
diff --git a/download_yt_playlist b/download_yt_playlist
new file mode 100755
index 0000000..32b50eb
--- /dev/null
+++ b/download_yt_playlist
@@ -0,0 +1,28 @@
+#!/usr/bin/env perl
+
+use strict;
+use warnings;
+
+use JSON;
+
+my $ENDPOINT = "https://www.googleapis.com/youtube/v3";
+my $YT_API_KEY = $ENV{'YOUTUBE_API_KEY'}
+ || die "Error: You need to define the env variable 'YOUTUBE_API_KEY'\n";
+
+sub decode_json_utf8 {
+ my $json = join "", @_;
+ utf8::encode($json);
+ return decode_json($json);
+}
+
+sub api_playlist_videos {
+ my $id = shift;
+ my $videos = (decode_json_utf8 `curl -s '$ENDPOINT/playlistItems?part=snippet&maxResults=1000&playlistId=$id&key=$YT_API_KEY'`)->{items};
+ return map { $_->{snippet}{resourceId}{videoId} } @$videos;
+}
+
+@ARGV == 1 or die "Usage: $0 <playlist_id>\n";
+my $pl_id = $ARGV[0];
+my @videos = map { "https://www.youtube.com/watch?v=" . $_ } api_playlist_videos($pl_id);
+
+`mkdir $pl_id && cd $pl_id && youtube-dl @videos\n`;
diff --git a/dups b/dups
new file mode 100755
index 0000000..bbf357e
--- /dev/null
+++ b/dups
@@ -0,0 +1,56 @@
+#!/usr/bin/env perl
+
+use strict;
+use warnings;
+
+use File::Find qw<find>;
+use IPC::System::Simple qw<capture>;
+
+@ARGV ge 1 || die "Usage: $0 <dir1> [<dir2> ...]\n";
+
+my %sizes;
+my %md5s;
+
+sub fill_sizes {
+ return if -d "$_"; # Skip directories
+
+ my $size = capture("stat", "--printf=%s", $_);
+ $sizes{$size} ||= [];
+ push @{$sizes{$size}}, $File::Find::name;
+}
+
+print STDERR "Collecting file sizes...\n";
+find(\&fill_sizes, $_) foreach (@ARGV);
+
+print STDERR "Computing md5s of files with same size...\n";
+my @progress = (0, 0, scalar(keys(%sizes)));
+foreach my $size (keys(%sizes)) {
+ $progress[0]++;
+ $progress[1] = 0;
+ my @same_size_files = @{$sizes{$size}};
+ next unless @same_size_files gt 1; # Discard unique sizes
+
+
+ foreach my $file (@same_size_files) {
+ $progress[1]++;
+ print STDERR "$progress[0].$progress[1] / $progress[2]\r";
+ STDERR->flush();
+
+ my ($md5) = capture("md5sum", "-z", $file) =~ m/^([^ ]+)/;
+ $md5s{$md5} ||= [];
+ push @{$md5s{$md5}}, $file;
+ }
+}
+
+foreach my $md5 (keys(%md5s)) {
+ my @same_md5_files = @{$md5s{$md5}};
+ next unless @same_md5_files gt 1; # Discard unique hashes
+
+ print "Found duplicate files:\n";
+ foreach my $file (@same_md5_files) {
+ print "\t$file\n";
+ }
+ printf "\n";
+}
+
+printf STDERR "Done!\n";
diff --git a/mdk b/mdk
new file mode 100755
index 0000000..301a35a
--- /dev/null
+++ b/mdk
@@ -0,0 +1,69 @@
+#!/bin/bash
+#
+# MDK (Move / Delete / Keep)
+################################################################################
+
+if [ "$#" -ge 2 ]; then
+ cmd=$1
+ shift
+else
+ echo "Usage: $0 <command> <file1> [<file2> ...]";
+ exit 0;
+fi
+
+
+move() {
+ file="$1"
+
+ read -e -p "Where? " movedir
+ mkdir -p "$movedir"
+
+ mv "$file" "$movedir/" || exit 1
+
+ echo "[+] Moved to '$movedir'"
+}
+
+delete() {
+ file="$1"
+
+ mkdir -p .trash
+ echo ".trash generated, delete it manually if you want"
+ mv "$file" .trash/ || exit 1
+
+ echo "[+] Deleted"
+}
+
+keep() {
+ echo "[+] Kept"
+}
+
+for file in "$@"; do
+ key=""
+ while [ -z "$key" ]; do
+ echo -e "==============================\n[+] Opening '$file'..."
+ $cmd "$file"
+ echo -e "\n=============================="
+ prompt="[M]ove / [D]elete / [K]eep / [A]gain (${file}) > "
+ read -n1 -p "$prompt" key
+ while [[ ! "$key" =~ m|d|k|a ]]; do
+ echo -e "\nWhat?"
+ read -n1 -p "$prompt" key
+ done
+ echo
+ case "$key" in
+ m)
+ move "$file"
+ ;;
+ d)
+ delete "$file"
+ ;;
+ k)
+ keep
+ ;;
+ a)
+ key=""
+ ;;
+ esac
+ done
+done
+
diff --git a/multiplayerctl b/multiplayerctl
new file mode 100755
index 0000000..011c7bc
--- /dev/null
+++ b/multiplayerctl
@@ -0,0 +1,57 @@
+#!/usr/bin/env perl
+
+my $TMPFILE = '/tmp/multiplayerctl';
+
+sub players {
+ my $players = {};
+ for my $playerline (`bash -c "paste -d '|' <(playerctl -l) <(playerctl -a status)"`) {
+ chomp $playerline;
+ my ($player, $status) = split /\|/, $playerline;
+ $players->{$status} = [] unless $players->{$status};
+ push @{$players->{$status}}, $player;
+ }
+ return $players;
+}
+
+sub wrap {
+ my $player = shift;
+
+ print " player=$player\n";
+
+ open(my $fd, '>', $TMPFILE) or die $?;
+ print $fd $player;
+
+ my @args;
+ push @playerargs, ('-p', $player) if ($player);
+ exec 'playerctl', @playerargs, @ARGV;
+}
+
+my $players = players();
+my @playing = @{$players->{'Playing'}};
+my @paused = @{$players->{'Paused'}};
+
+print "Playing=(@playing) Paused=(@paused)";
+
+my $last;
+if (open(my $fd, '<', $TMPFILE)) {
+ $last = <$fd>;
+}
+print " last=$last";
+
+if (grep /^$last$/, @playing) {
+ # If the last known player is playing, stop it first
+ wrap($last);
+} elsif (@playing) {
+ # Take the first of the playing players
+ wrap($playing[0]);
+} else {
+ # Nothing is currently playing
+ if (grep /^$last$/, @paused) {
+ # The last known player is paused; take it
+ print " matched=$last";
+ wrap($last);
+ } elsif (@paused) {
+ # Take the first of the paused players
+ wrap($paused[0]);
+ }
+}
diff --git a/playingnow b/playingnow
new file mode 100755
index 0000000..ea6c07e
--- /dev/null
+++ b/playingnow
@@ -0,0 +1,32 @@
+#!/usr/bin/env perl
+
+sub show_player_meta {
+ my $player = shift;
+
+ my ($artist, $album, $title, $url) = split /\|/,
+ `playerctl -p $player metadata --format '{{xesam:artist}}|{{xesam:album}}|{{xesam:title}}|{{xesam:url}}'`;
+
+ $url = "" if $url =~ /^file:|spotify/;
+
+ my $song = $album ? "$title - <i>$album</i>" : $title;
+
+ my @lines;
+ push @lines, $artist if $artist;
+ push @lines, $song if $song;
+ push @lines, $url if $url;
+
+ my @args = qw"-t 4000 -u low";
+ push @args, shift @lines;
+ push @args, join("\n", @lines);
+
+ system "notify-send", @args;
+}
+
+sub players {
+ map { chomp $_; [split /\|/, $_] } `bash -c "paste -d '|' <(playerctl -l) <(playerctl -a status)"`
+}
+
+for my $playerline (players()) {
+ my ($player, $status) = @$playerline;
+ show_player_meta($player) if ($status eq "Playing");
+}
diff --git a/yt b/yt
new file mode 100755
index 0000000..b246b07
--- /dev/null
+++ b/yt
@@ -0,0 +1,144 @@
+#!/usr/bin/env perl
+
+use strict;
+use warnings;
+
+use IPC::Open2 qw<open2>;
+use JSON;
+
+my $ENDPOINT = "https://www.googleapis.com/youtube/v3";
+my $YT_API_KEY = $ENV{'YOUTUBE_API_KEY'}
+ || die "Error: You need to define the env variable 'YOUTUBE_API_KEY'\n";
+
+my $MAX_RESULTS = 30;
+
+# Currently unused
+my $graphical;
+
+sub decode_json_utf8 {
+ my $json = join "", @_;
+ utf8::encode($json);
+ return decode_json($json);
+}
+
+sub api_search {
+ my $query = shift;
+ # TODO sanitize query properly...
+ $query =~ s/'/'\\''/g;
+ return (decode_json_utf8 `curl -s '$ENDPOINT/search?part=snippet&q=$query&maxResults=$MAX_RESULTS&key=$YT_API_KEY'`)->{items};
+}
+
+sub api_videos {
+ my $ids = join(',', @_);
+ return (decode_json_utf8 `curl -s '$ENDPOINT/videos?part=snippet,statistics,contentDetails&id=$ids&key=$YT_API_KEY'`)->{items};
+}
+
+sub api_channel_videos {
+ use Data::Dumper;
+ my $id = shift;
+ my $uploads = (decode_json_utf8 `curl -s '$ENDPOINT/channels?part=contentDetails&id=$id&key=$YT_API_KEY'`)->{items}[0]{contentDetails}{relatedPlaylists}{uploads};
+ my $videos = (decode_json_utf8 `curl -s '$ENDPOINT/playlistItems?part=snippet&maxResults=$MAX_RESULTS&playlistId=$uploads&key=$YT_API_KEY'`)->{items};
+ return map { $_->{snippet}{resourceId}{videoId} } @$videos;
+}
+
+sub play {
+ my $url = shift;
+ `mpv --really-quiet $url`;
+}
+
+sub ask_query {
+ my $query = `rofi -dmenu -no-fixed-num-lines -p "Youtube search" | sed 's: :+:g'`;
+ $query =~ s/\s*$//g;
+ $graphical = 1;
+ return $query;
+}
+
+sub sanitize_rofi {
+ return shift =~ s/&/&amp;/gr;
+}
+
+sub select_entry {
+ my ($channels, $video_ids) = @_;
+
+ my @entries;
+
+ open2(my $outh, my $inh, 'rofi -i -markup-rows -dmenu -format i -p "Select video"');
+ foreach my $chan (@$channels) {
+ my $id = $chan->{channelId};
+ push @entries, $id;
+
+ print $inh sanitize_rofi("<b>[$chan->{title}] $chan->{description}</b>\n");
+ }
+
+ foreach my $video (@{api_videos(@$video_ids)}) {
+ my $id = $video->{id};
+ push @entries, $id;
+
+ my $publishedAt = $video->{snippet}{publishedAt};
+ my ($y, $mo, $d, $h, $mi) = $publishedAt =~ /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):\d{2}Z$/;
+ my (undef, $cmi, $ch, $cd, $cmo, $cy) = gmtime();
+ $cy += 1900;
+ $cmo++;
+ my $published;
+ if ($y < $cy-1) {
+ $published = $cy - $y . " years ago";
+ } elsif ($mo != $cmo || $y == $cy-1) {
+ $published = (12 * ($cy-$y) + $cmo) - $mo . " months ago";
+ } elsif ($cd != $d) {
+ $published = $cd - $d . " days ago";
+ } elsif ($ch != $h) {
+ $published = $ch - $h . " hours ago";
+ } elsif ($cmi != $mi) {
+ $published = $cmi - $mi . " minutes ago";
+ }
+
+ my $stats = $video->{statistics};
+ my $likes = $stats->{likeCount};
+ my $dislikes = $stats->{dislikeCount};
+ my $rating = "";
+ if ($likes && $dislikes) {
+ my $total = $likes + $dislikes;
+ $rating = "*" x (6 * $likes / $total);
+ }
+
+ print $inh sanitize_rofi("($published) [$video->{snippet}{channelTitle}] $video->{snippet}{title} $rating\n");
+ }
+ close $inh;
+
+ my $idx = <$outh> || die "cancelled\n";
+ if ($idx < @$channels) {
+ return ('channel', $entries[$idx]);
+ } else {
+ return ('video', $entries[$idx]);
+ }
+}
+
+
+## MAIN
+
+my $query = join("+", @ARGV) || ask_query();
+die "empty query\n" unless $query;
+
+if ($query =~ /^http/) {
+ play($query);
+ exit 0;
+}
+
+my @channels;
+my @video_ids;
+foreach my $entry (@{api_search($query)}) {
+ if ($entry->{id}{kind} eq 'youtube#channel') {
+ push @channels, $entry->{snippet};
+ } elsif ($entry->{id}{kind} eq 'youtube#video') {
+ push @video_ids, $entry->{id}{videoId};
+ }
+}
+
+my ($type, $entry) = select_entry(\@channels, \@video_ids);
+if ($type eq 'channel') {
+ my @videos = api_channel_videos($entry);
+ my (undef, $entry) = select_entry([], \@videos);
+ play("https://youtu.be/$entry");
+} elsif ($type eq 'video') {
+ play("https://youtu.be/$entry");
+}