#!/usr/bin/env perl use strict; use warnings; use IPC::Open2 qw; 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; exec("mpv $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/&/&/gr; } sub parse_duration { my $duration = shift; my $hours = ($duration =~ /(\d+)H/) ? $1 : 0; my $minutes = ($duration =~ /(\d+)M/) ? $1 : 0; my $seconds = ($duration =~ /(\d+)S/) ? $1 : 0; return $hours == 0 ? "${minutes}m ${seconds}s" : "${hours}h ${minutes}m"; } 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("[$chan->{title}] $chan->{description}\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 $duration = parse_duration($video->{contentDetails}{duration}); 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("[$duration] ($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"); }