#!/usr/bin/perl # yttitle.pl # When someone says something with a youtube video link in it, get the # title and say it in the channel. # Uses yt-dlp to do the title lookups. Works much better than the # usual HTML-scraping, but unfortunately is *slow* (like, average 3 # sec per lookup). # Since lookups take so long, they're done asynchronously, via # fork(). This script owes a great debt to the dns.pl script # included with irssi, which shows how to use irssi's pidwait and # input_(add|remove). no strict; use POSIX '_exit'; use Irssi qw/ signal_add_first pidwait_add input_add input_remove /; our $VERSION = "0.1"; our %IRSSI = ( authors => 'Urchlay', contact => 'Urchlay on Libera', name => 'yttitle', description => 'get titles for youtube videos using yt-dlp', license => 'WTFPL', url => 'none', ); # video ID => title our %cache; # keep track of video IDs of in-progress lookups, so we don't spawn # a 2nd process for the same ID. our %in_progress; # TODO: the rest of these variables should be irssi settings. # how many jobs are currently running? our $jobcount = 0; # attempts to spawn more simultaneous jobs than this are just ignored. our $maxjobs = 10; # yt-dlp processes that don't respond within this many seconds # are terminated. our $job_timeout = 15; # passed to yt-dlp itself. our $socket_timeout = 10; # command to execute, %s replaced with video ID. # --socket-timeout arg is seconds, should be longer that $timeout. our $command_fmt = "yt-dlp -q --print '%%(title)s' --socket-timeout $socket_timeout -- %s 2>/dev/null"; our $DEBUG = 1; sub debug { Irssi::print(join "", @_) if $DEBUG; } sub debugf { Irssi::print(sprintf(@_)) if $DEBUG; } sub spawn_job { my ($server, $target, $video_id) = @_; debug("spawn_job() called, video_id $video_id"); if($jobcount > $maxjobs) { debug("spawn_job(): jobcount $jobcount > maxjobs $maxjobs, ignoring request"); return; } if($in_progress{$video_id}) { debug("spawn_job(): video_id $video_id already in queue"); return; } $in_progress{$video_id} = 1; my $cmd = sprintf($command_fmt, $video_id); debug("spawn_job() command is: $cmd"); my($read, $write); pipe($read, $write); my $pid = fork(); if(!defined($pid)) { debug("spawn_job(): video_id $video_id: fork() failed!"); close $read; close $write; return; } if($pid) { # parent $jobcount++; debug("spawn_job() forked, kid pid is $pid, jobcount now $jobcount"); close $write; my $pipe_args = [ $pid, $server, $target, $video_id, $read ]; pidwait_add($pid); $pipe_tags{$video_id} = input_add(fileno($read), INPUT_READ, \&job_done, $pipe_args); return; } else { # child, debug() and Irssi::print don't work, here. eval { local $SIG{ALRM} = sub { die "alarum\n" }; alarm $job_timeout; print $write `$cmd`; alarm 0; }; # don't bother to die() on error here, we're about to exit anyway. if($@ && ($@ eq "alarum\n")) { # print *something* if $cmd timed out print $write "\n"; } # not sure this needs to be wrapped in eval, but... eval { close $write; }; _exit(1); } } sub job_done { my $pipe_args = shift; my ($pid, $server, $target, $video_id, $read) = @$pipe_args; debug("job_done() pid $pid, target $target, video_id $video_id, jobcount was $jobcount"); my $title = <$read>; chomp $title; close $read; input_remove($pipe_tags{$video_id}); $jobcount--; delete $in_progress{$video_id}; $cache{$video_id} = $title; if(defined($title)) { debug("job_done(): video_id $video_id title is $title"); } else { debug("job_done(): video_id $video_id failed to get title"); return; } say_title($server, $target, $video_id, $title); } sub say_title { my ($server, $target, $video_id, $title) = @_; my $tag = "YouTube" . ($DEBUG ? "($video_id)" : ""); $server->command("msg $target $tag: $title"); } sub on_public_msg { my ($server, $msg, $nick, $address, $target) = @_; my $mynick = $server->{nick}; unless(length $target) { $target = $nick; $nick = $mynick; } if($target eq $mynick) { # private message... send response to sender $target = $nick; } for my $video_id ($msg =~ /(?:youtube\.com\S+(?:embed\/|v=)|youtu.be\/)([-0-9a-zA-Z_]{11})/g) { if($cache{$video_id}) { debug("video_id $video_id found in cache"); say_title($server, $target, $video_id, $cache{$video_id}); } else { debug("video_id $video_id NOT found in cache, queuing job"); spawn_job($server, $target, $video_id); } } } ### main() signal_add_first("message public", "on_public_msg");