#!/usr/bin/perl no strict; use POSIX ":sys_wait_h"; use Irssi qw/ signal_add_first signal_continue signal_register command_bind timeout_add_once get_irssi_dir /; 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; our $tmpdir = get_irssi_dir . "/yttitle.tmp"; # pid => [server, target, video ID] our %jobs; # video ID keys (values meaningless; only care about key presence) our %job_videos; # attempts to spawn more jobs than this are just ignored. our $maxjobs = 10; # milliseconds: how often to check %jobs to see if any jobs are done. our $queue_time = 1000; # command to execute, 1st %s replaced with video ID, 2nd with tmp filename. # --socket-timeout arg is seconds, should be longer that $timeout. our $command_fmt = "yt-dlp -q --print '%%(title)s' --socket-timeout 10 -- %s >%s 2>/dev/null"; our $DEBUG = 1; sub debug { Irssi::print(join "", @_) if $DEBUG; } sub debugf { Irssi::print(sprintf(@_)) if $DEBUG; } sub start_timer { timeout_add_once($queue_time, "check_jobs", undef); } sub get_tmp_filename { my $id = shift; return $tmpdir . "/" . $id; } sub read_tmp_file { my $file = shift; open my $fh, "<:encoding(UTF-8)", $file; if(!$fh) { debug("read_tmp_file() failed to open $file: $!"); return; } my $result = <$fh>; close $fh; chomp $result if defined $result; return length($result) ? $result : undef; } sub spawn_job { my ($server, $target, $video_id) = @_; my $jobcount = keys %jobs; debug("spawn_job() called, video_id $video_id"); if($jobcount > $maxjobs) { debug("spawn_job(): jobcount $jobcount > maxjobs $maxjobs, ignoring request"); return; } if($job_videos{$video_id}) { debug("spawn_job(): video_id $video_id already in queue"); return; } my $cmd = sprintf($command_fmt, $video_id, get_tmp_filename($video_id)); debug("spawn_job() command is: $cmd"); my $pid = fork(); if($pid) { # parent debug("spawn_job() forked, kid pid is $pid"); start_timer unless keys %jobs; $jobs{$pid} = [ $server, $target, $video_id ]; $job_videos{$video_id} = 1; debug("spawn_job(): job pids: " . join(",", keys %jobs)); } else { # child, debug() and Irssi::print don't work, here. exec $cmd; } } sub job_done { my $pid = shift; my ($server, $target, $video_id) = @{$jobs{$pid}}; debug("job_done() pid $pid, target $target, video_id $video_id"); my $file = get_tmp_filename($video_id); my $title = read_tmp_file($file); unlink $file; $cache{$video_id} = $title; delete $job_videos{$video_id}; if(!defined($title)) { debug("job_done(): video_id $video_id failed to get title"); return; } say_title($server, $target, $video_id, $title); } sub check_jobs { my @k = keys %jobs; debug("check_jobs(): " . @k . " jobs"); return unless @k; for my $jobpid (@k) { debug("check_jobs(): about to waitpid($jobpid, WNOHANG)"); $pid = waitpid($jobpid, WNOHANG); debug("check_jobs(): jobpid $jobpid, pid $pid"); next if $pid == 0; # still running, let it run job_done($jobpid) if $pid > 0; # -1 is "no such pid" delete $jobs{$jobpid}; } start_timer if keys %jobs; } 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() mkdir $tmpdir; signal_add_first("message public", "on_public_msg");