--- /dev/null
+#
+# Usage:
+# * /set twitch_channels "channel1 channel2 channel3"
+# * /twitch_online to see which channels are online
+#
+# On load:
+# * refreshes online channel list every minute
+# * prints changes to the (status) window
+#
+
+use strict;
+use warnings;
+
+use English qw( -no_match_vars );
+use HTTP::Tiny;
+use IO::Handle;
+use IPC::Open3;
+use Irssi;
+use JSON::PP;
+use POSIX ();
+
+our $VERSION = '0.0.1';
+our %IRSSI = (
+ authors => 'leocp1',
+ name => 'twitch_notify',
+ description => 'Notify when a twitch channel comes online'
+ . 'Uses Twitch v5 API',
+ license => 'Public Domain',
+);
+
+## no critic (ProhibitConstantPragma) since Readonly is not in core perl
+
+# youtube-dl's Client ID
+use constant CLIENTID => 'kimne78kx3ncx6brgo4mv6wki5h1ko';
+
+# values for channel map
+use constant {
+ OFFLINE => 0,
+ ONLINE => 1,
+ WASONLINE => 2,
+};
+
+# How long between refreshing live channels in ms
+use constant UPDATETIMEOUT => 60_000;
+## use critic
+
+our %CHANNELS;
+our $READHANDLE;
+our $WRITEHANDLE;
+our $REFRESHTAG;
+our $UPDATETAG;
+
+## no critic (RequireArgUnpacking)
+sub uniq {
+ my %seen;
+ return grep { !$seen{$_}++ } @_;
+}
+## use critic
+
+sub get_chan_names {
+ return uniq( map {lc}
+ ( split q{ }, Irssi::settings_get_str('twitch_channels') ) );
+}
+
+sub call_api {
+ my ($resource) = @_;
+ my $endpointprefix = 'https://api.twitch.tv/kraken/';
+ my $jsonresp = undef;
+ use Symbol 'gensym';
+ if ( not HTTP::Tiny::can_ssl() ) {
+ my $pid = open3(
+ my $curl_in, my $curl_out, my $curl_err = gensym,
+ 'curl', '-f', '-L',
+ '--request', 'GET',
+ '-H', 'Accept:application/vnd.twitchtv.v5+json; charset=UTF-8',
+ '-H', 'Client-ID:' . CLIENTID,
+ $endpointprefix . $resource
+ );
+ waitpid $pid, 0;
+ if ( $CHILD_ERROR == 0 ) {
+ $jsonresp = do {
+ local $INPUT_RECORD_SEPARATOR = undef;
+ <$curl_out>;
+ };
+ }
+ }
+ else {
+ my $http = HTTP::Tiny->new;
+ my $resp = $http->get(
+ "$endpointprefix" . $resource,
+ { headers => {
+ 'Client-ID' => CLIENTID,
+ 'Accept' =>
+ 'application/vnd.twitchtv.v5+json; charset=UTF-8'
+ }
+ }
+ );
+ if ( $resp->{success} ) {
+ $jsonresp = $resp->{content};
+ }
+ }
+ return ($jsonresp);
+}
+
+## no critic (ProhibitConstantPragma)
+sub live_channels {
+ my (@names) = @_;
+ my @channels = ();
+ use constant MAXUSERS => 100;
+ use constant MAXSTREAMS => 25;
+ while ( my @name_slice = splice @names, 0, MAXUSERS ) {
+ my $users = join q{,}, @name_slice;
+ my @ids = eval {
+ my ($users_resp) = call_api( 'users?login=' . $users );
+ map { $_->{_id} } @{ decode_json($users_resp)->{users} };
+ };
+ while ( my @id_slice = splice @ids, 0, MAXSTREAMS ) {
+ my $streams = join q{,}, @id_slice;
+ my @live_chans = eval {
+ my ($streams_resp)
+ = call_api( 'streams?channel=' . $streams );
+ map { $_->{channel}->{name} }
+ @{ decode_json($streams_resp)->{streams} };
+ };
+ push @channels, @live_chans;
+ }
+ }
+ return @channels;
+}
+## use critic
+
+sub update {
+ my $pid = fork;
+ if ( not defined $pid ) {
+ return;
+ }
+ if ( $pid > 0 ) {
+ Irssi::pidwait_add($pid);
+ return;
+ }
+
+ my @live_chan_names = eval { live_channels( get_chan_names() ); };
+
+ foreach (@live_chan_names) {
+ say {$WRITEHANDLE} $_
+ or _msg_warn("Error writing to twitch_notify pipe: $ERRNO");
+ }
+
+ say {$WRITEHANDLE} q{+}
+ or _msg_warn("Error writing to twitch_notify pipe: $ERRNO");
+
+ close $READHANDLE
+ or _msg_warn("Error closing twitch_notify read pipe: $ERRNO");
+ close $WRITEHANDLE
+ or _msg_warn("Error closing twitch_notify write pipe: $ERRNO");
+ POSIX::_exit(1);
+ return;
+}
+
+sub notify {
+ for my $chan ( keys %CHANNELS ) {
+ if ( $CHANNELS{$chan} == ONLINE ) {
+ $CHANNELS{$chan} = WASONLINE;
+ }
+ }
+ while ( chomp( my $chan = <$READHANDLE> ) ) {
+ last if $chan eq q{+};
+ if ( not defined $CHANNELS{$chan} or $CHANNELS{$chan} != WASONLINE ) {
+ _msg_status("https://twitch.tv/$chan is now online.");
+ }
+ $CHANNELS{$chan} = ONLINE;
+ }
+
+ for my $chan ( keys %CHANNELS ) {
+ if ( $CHANNELS{$chan} == WASONLINE ) {
+ $CHANNELS{$chan} = OFFLINE;
+ _msg_status("https://twitch.tv/$chan is now offline.");
+ }
+ }
+ return;
+}
+
+sub online {
+ my @chans = get_chan_names();
+ _msg('The following channels are online:');
+ foreach my $chan (@chans) {
+ if ( defined $CHANNELS{$chan} and $CHANNELS{$chan} == ONLINE ) {
+ _msg("* https://twitch.tv/$chan");
+ }
+ }
+ return;
+}
+
+sub _msg {
+ my ($msg) = @_;
+ my $win = Irssi::active_win();
+ $win->print( $msg, Irssi::MSGLEVEL_CLIENTCRAP );
+ return;
+}
+
+sub _msg_status {
+ my ($msg) = @_;
+ my $win = Irssi::window_find_name('(status)');
+ $win->print( $msg, Irssi::MSGLEVEL_CLIENTCRAP );
+ return;
+}
+
+sub _msg_warn {
+ my ($msg) = @_;
+ my $win = Irssi::active_win();
+ $win->print( $msg, Irssi::MSGLEVEL_CLIENTERROR );
+ return;
+}
+
+sub load {
+ Irssi::settings_add_str( 'twitch_channel_notification',
+ 'twitch_channels', q{} );
+ pipe $READHANDLE, $WRITEHANDLE;
+ $WRITEHANDLE->autoflush();
+ $REFRESHTAG
+ = Irssi::input_add( fileno($READHANDLE), Irssi::INPUT_READ, \¬ify,
+ q{} );
+ update();
+ $UPDATETAG = Irssi::timeout_add( UPDATETIMEOUT, \&update, q{} );
+ Irssi::command_bind( 'twitch_online', \&online );
+ return;
+}
+
+sub UNLOAD {
+ Irssi::command_unbind( 'twitch_online', \&online );
+ close $READHANDLE
+ or _msg_warn("Error closing twitch_notify pipe: $ERRNO");
+ close $WRITEHANDLE
+ or _msg_warn("Error closing twitch_notify pipe: $ERRNO");
+ input_remove($REFRESHTAG);
+ timeout_remove($UPDATETAG);
+ return;
+}
+
+load();
+1;