$ git clone http://tcclient.ion.nu/tc_client.git
commit 23205c603267895c449bf613500a099001d9729c
Author: Alicia <...>
Date:   Tue Apr 7 06:49:01 2015 +0200

    Version 0.28

diff --git a/ChangeLog b/ChangeLog
index d591221..99ac586 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,13 @@
+0.28:
+Delete streams when they stop, to allow reusing stream IDs.
+Added a /camdown command.
+camviewer: worked around the cam streaming issue.
+camviewer: switched to libavresample instead of libswresample (when available)
+modbot: added a !modstats command to see how often there are moderators present.
+modbot: moved the help-text into an HTML document.
+modbot: added a -v/--verbose option to print/log all incoming messages.
+modbot: pass on error messages from youtube-dl when requesting a video fails.
+cursedchat: added history (up/down arrows)
 0.27:
 Added some more compatibility hacks for android.
 Added support for video broadcasting (adding /camup and /video <length> followed by the encoded data for each frame)
diff --git a/Makefile b/Makefile
index 4480abe..72a37d8 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-VERSION=0.27
+VERSION=0.28
 CFLAGS=-g3 -Wall $(shell curl-config --cflags)
 LIBS=-g3 $(shell curl-config --libs)
 ifneq ($(wildcard config.mk),)
@@ -16,10 +16,13 @@ ifdef AVUTIL_LIBS
 ifdef SWSCALE_LIBS
   UTILS+=camviewer
   CFLAGS+=$(GTK_CFLAGS) $(AVCODEC_CFLAGS) $(AVUTIL_CFLAGS) $(SWSCALE_CFLAGS)
-  ifdef SWRESAMPLE_LIBS
   ifdef AO_LIBS
-    CFLAGS+=-DHAVE_SOUND $(SWRESAMPLE_CFLAGS) $(AO_CFLAGS)
-  endif
+    ifdef AVRESAMPLE_LIBS
+      CFLAGS+=-DHAVE_SOUND=avresample $(AVRESAMPLE_CFLAGS) $(AO_CFLAGS)
+    endif
+    ifdef SWRESAMPLE_LIBS
+      CFLAGS+=-DHAVE_SOUND=swresample $(SWRESAMPLE_CFLAGS) $(AO_CFLAGS)
+    endif
   endif
   ifdef LIBV4L2_LIBS
     CFLAGS+=-DHAVE_V4L2 $(LIBV4L2_CFLAGS)
@@ -47,7 +50,7 @@ modbot: $(MODBOT_OBJ)
  $(CC) $(LDFLAGS) $^ $(LIBS) -o $@
 
 camviewer: $(CAMVIEWER_OBJ)
- $(CC) $(LDFLAGS) $^ $(LIBS) $(GTK_LIBS) $(AVCODEC_LIBS) $(AVUTIL_LIBS) $(SWSCALE_LIBS) $(SWRESAMPLE_LIBS) $(AO_LIBS) $(LIBV4L2_LIBS) -o $@
+ $(CC) $(LDFLAGS) $^ $(LIBS) $(GTK_LIBS) $(AVCODEC_LIBS) $(AVUTIL_LIBS) $(SWSCALE_LIBS) $(AVRESAMPLE_LIBS) $(SWRESAMPLE_LIBS) $(AO_LIBS) $(LIBV4L2_LIBS) -o $@
 
 cursedchat: $(CURSEDCHAT_OBJ)
  $(CC) $(LDFLAGS) $^ $(LIBS) $(READLINE_LIBS) $(CURSES_LIBS) -o $@
@@ -56,4 +59,4 @@ clean:
  rm -f $(OBJ) $(IRCHACK_OBJ) $(MODBOT_OBJ) $(CAMVIEWER_OBJ) $(CURSEDCHAT_OBJ) tc_client irchack modbot camviewer cursedchat
 
 tarball:
- tar -cJf tc_client-$(VERSION).tar.xz --transform='s|^|tc_client-$(VERSION)/|' Makefile client.c amfparser.c rtmp.c numlist.c amfwriter.c idlist.c colors.c endian.c media.c amfparser.h rtmp.h numlist.h amfwriter.h idlist.h colors.h endian.h media.h LICENSE README ChangeLog crossbuild.sh utilities/irchack/irchack.c utilities/modbot/modbot.c utilities/modbot/queue.c utilities/modbot/queue.h utilities/camviewer/camviewer.c utilities/cursedchat/cursedchat.c utilities/cursedchat/buffer.c utilities/cursedchat/buffer.h utilities/compat.c utilities/compat.h utilities/list.c utilities/list.h configure
+ tar -cJf tc_client-$(VERSION).tar.xz --transform='s|^|tc_client-$(VERSION)/|' Makefile client.c amfparser.c rtmp.c numlist.c amfwriter.c idlist.c colors.c endian.c media.c amfparser.h rtmp.h numlist.h amfwriter.h idlist.h colors.h endian.h media.h LICENSE README ChangeLog crossbuild.sh utilities/irchack/irchack.c utilities/modbot/modbot.c utilities/modbot/queue.c utilities/modbot/queue.h utilities/modbot/commands.html utilities/camviewer/camviewer.c utilities/cursedchat/cursedchat.c utilities/cursedchat/buffer.c utilities/cursedchat/buffer.h utilities/compat.c utilities/compat.h utilities/list.c utilities/list.h configure
diff --git a/README b/README
index 438e4b5..b4aa87e 100644
--- a/README
+++ b/README
@@ -16,6 +16,7 @@ Commands supported by tc_client:
 /forgive <nick/ID> = unban someone
 /camup          = open an audio/video stream for broadcasting your video
 /video <length> = send a <length> bytes long encoded frame, send the frame data after this line
+/camdown        = close the audio/video stream
 /help           = list these commands at runtime
 
 Current commands sent by the TC servers that tc_client doesn't know how to handle:
diff --git a/client.c b/client.c
index 48af852..b259d22 100644
--- a/client.c
+++ b/client.c
@@ -434,6 +434,7 @@ int main(int argc, char** argv)
                  "/mute           = temporarily mutes all non-moderator broadcasts\n"
                  "/push2talk      = puts all non-operators in push2talk mode\n"
                  "/camup          = open an audio/video stream for broadcasting your video\n"
+                 "/camdown        = close the broadcasting stream\n"
                  "/video <length> = send a <length> bytes long encoded frame, send the frame data after this line\n");
           fflush(stdout);
         }
@@ -590,6 +591,11 @@ int main(int argc, char** argv)
           streamout_start(idlist_get(nickname), sock);
           continue;
         }
+        else if(!strcmp(buf, "/camdown"))
+        {
+          stream_stopvideo(sock);
+          continue;
+        }
         else if(!strncmp(buf, "/video ", 7)) // Send video data
         {
           size_t len=strtoul(&buf[7],0,10);
@@ -875,7 +881,7 @@ int main(int argc, char** argv)
     }
     else if(amfin->itemcount>0 && amfin->items[0].type==AMF_STRING && amf_comparestrings_c(&amfin->items[0].string, "onStatus"))
     {
-      stream_handlestatus(amfin);
+      stream_handlestatus(amfin, sock);
     }
     // else{printf("Unknown command...\n"); printamf(amfin);} // (Debugging)
     amf_free(amfin);
diff --git a/configure b/configure
index 5bff5c5..166d010 100755
--- a/configure
+++ b/configure
@@ -89,14 +89,23 @@ fi
 
 # TODO: Figure out how to mix sound sources so that this doesn't sound horrible with more than one person on mic, having it disabled by default until then
 if [ "x${ENABLE_MIC}" != "x" ]; then
-printf 'Checking for libswresample... '
-libs="`pkg-config --libs libswresample 2> /dev/null`"
+printf 'Checking for libavresample... '
+libs="`pkg-config --libs libavresample 2> /dev/null`"
 if [ "x$libs" != "x" ]; then
-  echo "SWRESAMPLE_LIBS=${libs}" >> config.mk
-  echo "SWRESAMPLE_CFLAGS=`pkg-config --cflags libswresample`" >> config.mk
+  echo "AVRESAMPLE_LIBS=${libs}" >> config.mk
+  echo "AVRESAMPLE_CFLAGS=`pkg-config --cflags libavresample`" >> config.mk
   echo yes
 else
   echo no
+  printf 'Checking for libswresample... '
+  libs="`pkg-config --libs libswresample 2> /dev/null`"
+  if [ "x$libs" != "x" ]; then
+    echo "SWRESAMPLE_LIBS=${libs}" >> config.mk
+    echo "SWRESAMPLE_CFLAGS=`pkg-config --cflags libswresample`" >> config.mk
+    echo yes
+  else
+    echo no
+  fi
 fi
 
 printf 'Checking for libao... '
diff --git a/media.c b/media.c
index 649c2f7..7e2e166 100644
--- a/media.c
+++ b/media.c
@@ -70,6 +70,7 @@ void streamout_start(unsigned int id, int sock) // called upon privmsg "/camup"
   amfnull(&amf);
   amfsend(&amf, sock);
   printf("Starting outgoing media stream\n");
+  fflush(stdout);
 }
 
 void stream_play(struct amf* amf, int sock) // called upon _result
@@ -115,7 +116,7 @@ void stream_handledata(struct rtmp* rtmp)
   printf("Received media data to unknown stream ID %u\n", rtmp->msgid);
 }
 
-void stream_handlestatus(struct amf* amf)
+void stream_handlestatus(struct amf* amf, int sock)
 {
   if(amf->itemcount<3 || amf->items[2].type!=AMF_OBJECT){return;}
   struct amfobject* obj=&amf->items[2].object;
@@ -132,7 +133,17 @@ void stream_handlestatus(struct amf* amf)
       if(streams[i].userid==id)
       {
         printf("VideoEnd: %u\n", streams[i].userid);
-        // Note: not removing it from the list because tinychat doesn't seem to handle reusing stream IDs
+        // Delete the stream
+        struct rtmp amf;
+        amfinit(&amf, 3);
+        amfstring(&amf, "deleteStream");
+        amfnum(&amf, 0);
+        amfnull(&amf);
+        amfnum(&amf, streams[i].streamid);
+        amfsend(&amf, sock);
+        // Remove from list of streams
+        --streamcount;
+        memmove(&streams[i], &streams[i+1], sizeof(struct stream)*(streamcount-i));
         return;
       }
     }
@@ -157,3 +168,33 @@ void stream_sendvideo(int sock, void* buf, size_t len)
     }
   }
 }
+
+void stream_stopvideo(int sock)
+{
+  unsigned int i;
+  for(i=0; i<streamcount; ++i)
+  {
+    if(streams[i].outgoing)
+    {
+      struct rtmp amf;
+      // Close the stream
+      amfinit(&amf, 8);
+      amfstring(&amf, "closeStream");
+      amfnum(&amf, 0);
+      amfnull(&amf);
+      amf.msgid=le32(streams[i].streamid);
+      amfsend(&amf, sock);
+      // Delete the stream
+      amfinit(&amf, 3);
+      amfstring(&amf, "deleteStream");
+      amfnum(&amf, 0);
+      amfnull(&amf);
+      amfnum(&amf, streams[i].streamid);
+      amfsend(&amf, sock);
+      // Remove from list of streams
+      --streamcount;
+      memmove(&streams[i], &streams[i+1], sizeof(struct stream)*(streamcount-i));
+      return;
+    }
+  }
+}
diff --git a/media.h b/media.h
index 71893cf..25195ab 100644
--- a/media.h
+++ b/media.h
@@ -30,5 +30,6 @@ extern void stream_start(const char* nick, int sock); // called upon privmsg "/o
 extern void streamout_start(unsigned int id, int sock); // called upon privmsg "/camup"
 extern void stream_play(struct amf* amf, int sock); // called upon _result
 extern void stream_handledata(struct rtmp* rtmp);
-extern void stream_handlestatus(struct amf* amf);
+extern void stream_handlestatus(struct amf* amf, int sock);
 extern void stream_sendvideo(int sock, void* buf, size_t len);
+extern void stream_stopvideo(int sock);
diff --git a/utilities/camviewer/camviewer.c b/utilities/camviewer/camviewer.c
index df8ad53..a11142b 100644
--- a/utilities/camviewer/camviewer.c
+++ b/utilities/camviewer/camviewer.c
@@ -24,11 +24,15 @@
 #include <libavutil/imgutils.h>
 #ifdef HAVE_SOUND
 // TODO: use libavresample instead if available
-  #include <libswresample/swresample.h>
+  #if HAVE_SOUND==avresample
+    #include <libavutil/opt.h>
+    #include <libavresample/avresample.h>
+  #else
+    #include <libswresample/swresample.h>
+  #endif
   #include <ao/ao.h>
 #endif
 #include <gtk/gtk.h>
-#undef HAVE_V4L2 // Not working yet, something is wrong with the frames making the encoding break (for keyframes in particular and I don't know why)
 #ifdef HAVE_V4L2
   #include <libv4l2.h>
   #include <linux/videodev2.h>
@@ -68,11 +72,15 @@ struct viddata
   unsigned int camcount;
   GtkWidget* box;
   AVCodec* vdecoder;
-//  AVCodec* vencoder;
+  AVCodec* vencoder;
   AVCodec* adecoder;
 #ifdef HAVE_SOUND
   int audiopipe;
-  SwrContext* swrctx;
+  #if HAVE_SOUND==avresample
+    AVAudioResampleContext* resamplectx;
+  #else
+    SwrContext* swrctx;
+  #endif
 #endif
 };
 
@@ -175,7 +183,6 @@ gboolean handledata(GIOChannel* channel, GIOCondition condition, gpointer datap)
     gtk_widget_show_all(cam->box);
     return 1;
   }
-/*
   if(!strcmp(buf, "Starting outgoing media stream"))
   {
     ++data->camcount;
@@ -195,7 +202,6 @@ gboolean handledata(GIOChannel* channel, GIOCondition condition, gpointer datap)
     gtk_widget_show_all(cam->box);
     return 1;
   }
-*/
   if(!strncmp(buf, "VideoEnd: ", 10))
   {
     for(i=0; i<data->camcount; ++i)
@@ -248,7 +254,11 @@ gboolean handledata(GIOChannel* channel, GIOCondition condition, gpointer datap)
     int gotframe;
     avcodec_decode_audio4(cam->actx, cam->frame, &gotframe, &pkt);
     if(!gotframe){return 1;}
-    int outlen=swr_convert(data->swrctx, cam->frame->data, cam->frame->nb_samples, (const uint8_t**)cam->frame->data, cam->frame->nb_samples);
+  #if HAVE_SOUND==avresample
+    int outlen=avresample_convert(data->resamplectx, cam->frame->data, cam->frame->linesize[0], cam->frame->nb_samples, cam->frame->data, cam->frame->linesize[0], cam->frame->nb_samples);
+  #else
+    int outlen=swr_convert(data->resamplectx, cam->frame->data, cam->frame->nb_samples, (const uint8_t**)cam->frame->data, cam->frame->nb_samples);
+  #endif
     camera_playsnd(data, cam, (short*)cam->frame->data[0], outlen);
 #endif
     return 1;
@@ -328,12 +338,23 @@ void audiothread(int fd)
 #endif
 
 #ifdef HAVE_V4L2
-void camup(GtkWidget* button, struct viddata* data)
+pid_t camproc=0;
+void togglecam(GtkButton* button, struct viddata* data)
 {
+  if(camproc)
+  {
+    kill(camproc, SIGINT);
+    camproc=0;
+    gtk_button_set_label(button, "Broadcast cam");
+    dprintf(tc_client_in[1], "/camdown\n");
+    dprintf(tc_client[1], "VideoEnd: out\n"); // Close our local display
+    return;
+  }
   dprintf(tc_client_in[1], "/camup\n");
-  gtk_widget_destroy(button); // Only once
+  gtk_button_set_label(button, "Stop broadcasting");
 // printf("Camming up!\n");
-  if(!fork())
+  camproc=fork();
+  if(!camproc)
   {
     prctl(PR_SET_PDEATHSIG, SIGHUP);
     unsigned int delay=500000;
@@ -387,9 +408,7 @@ void camup(GtkWidget* button, struct viddata* data)
       packet.data=0;
 packet.size=0;
       avcodec_encode_video2(ctx, &packet, dstframe, &gotpacket);
-      unsigned char frameinfo=0x20;
-// if(packet.flags&AV_PKT_FLAG_KEY){printf("Sending keyframe!\n");}
-      if(packet.flags&AV_PKT_FLAG_KEY){frameinfo|=0x01;}else{frameinfo|=0x02;}
+      unsigned char frameinfo=0x22; // Note: differentiating between keyframes and non-keyframes seems to break stuff, so let's just go with all being interframes (1=keyframe, 2=interframe, 3=disposable interframe)
       dprintf(tc_client_in[1], "/video %i\n", packet.size+1);
       write(tc_client_in[1], &frameinfo, 1);
       write(tc_client_in[1], packet.data, packet.size);
@@ -414,8 +433,20 @@ int main(int argc, char** argv)
   data.adecoder=avcodec_find_decoder(AV_CODEC_ID_NELLYMOSER);
 
 #ifdef HAVE_SOUND
-  data.swrctx=swr_alloc_set_opts(0, AV_CH_FRONT_CENTER, AV_SAMPLE_FMT_S16, 22050, AV_CH_FRONT_CENTER, AV_SAMPLE_FMT_FLT, 11025, 0, 0); // TODO: any way to get the sample rate from the frame/decoder? cam->frame->sample_rate seems to be 0
+  #if HAVE_SOUND==avresample
+  data.resamplectx=avresample_alloc_context();
+  av_opt_set_int(data.resamplectx, "in_channel_layout", AV_CH_FRONT_CENTER, 0);
+  av_opt_set_int(data.resamplectx, "in_sample_fmt", AV_SAMPLE_FMT_FLT, 0);
+  // TODO: any way to get the sample rate from the frame/decoder? cam->frame->sample_rate seems to be 0
+  av_opt_set_int(data.resamplectx, "in_sample_rate", 11025, 0);
+  av_opt_set_int(data.resamplectx, "out_channel_layout", AV_CH_FRONT_CENTER, 0);
+  av_opt_set_int(data.resamplectx, "out_sample_fmt", AV_SAMPLE_FMT_S16, 0);
+  av_opt_set_int(data.resamplectx, "out_sample_rate", 22050, 0);
+  avresample_open(data.resamplectx);
+  #else
+  data.resamplectx=swr_alloc_set_opts(0, AV_CH_FRONT_CENTER, AV_SAMPLE_FMT_S16, 22050, AV_CH_FRONT_CENTER, AV_SAMPLE_FMT_FLT, 11025, 0, 0);
   swr_init(data.swrctx);
+  #endif
   int audiopipe[2];
   pipe(audiopipe);
   data.audiopipe=audiopipe[1];
@@ -437,7 +468,7 @@ int main(int argc, char** argv)
 #ifdef HAVE_V4L2
   data.vencoder=avcodec_find_encoder(AV_CODEC_ID_FLV1);
   GtkWidget* cambutton=gtk_button_new_with_label("Broadcast cam");
-  g_signal_connect(cambutton, "clicked", G_CALLBACK(camup), &data);
+  g_signal_connect(cambutton, "clicked", G_CALLBACK(togglecam), &data);
   gtk_box_pack_start(GTK_BOX(data.box), cambutton, 0, 0, 0);
 #endif
   gtk_widget_show_all(w);
@@ -470,7 +501,11 @@ int main(int argc, char** argv)
     avcodec_free_context(&data.cams[i].vctx);
 #ifdef HAVE_SOUND
     avcodec_free_context(&data.cams[i].actx);
+  #if HAVE_SOUND==avresample
+    avresample_free(&data.resamplectx);
+  #else
     swr_free(&data.swrctx);
+  #endif
 #endif
     free(data.cams[i].id);
     free(data.cams[i].nick);
diff --git a/utilities/cursedchat/cursedchat.c b/utilities/cursedchat/cursedchat.c
index 8a30f75..c61bc98 100644
--- a/utilities/cursedchat/cursedchat.c
+++ b/utilities/cursedchat/cursedchat.c
@@ -23,6 +23,7 @@
 #include <locale.h>
 #include <curses.h>
 #include <readline/readline.h>
+#include <readline/history.h>
 #include "../compat.h"
 #include "../list.h"
 #include "buffer.h"
@@ -115,6 +116,7 @@ void drawtopic(void)
 void gotline(char* line)
 {
   if(!line){close(to_app); return;} // TODO: handle EOF on stdin better?
+  add_history(line);
   if(!strcmp(line, "/pm"))
   {
     currentbuf=0;
@@ -218,8 +220,8 @@ int escinput(int a, int byte)
   char buf[4];
   read(0, buf, 2);
   buf[2]=0;
-  if(!strcmp(buf, "[A")||!strcmp(buf, "OA")){return 0;} // TODO: history?
-  if(!strcmp(buf, "[B")||!strcmp(buf, "OB")){return 0;} // TODO: history?
+  if(!strcmp(buf, "[A")||!strcmp(buf, "OA")){rl_get_previous_history(1,27);return 0;}
+  if(!strcmp(buf, "[B")||!strcmp(buf, "OB")){rl_get_next_history(1,27);return 0;}
   if(!strcmp(buf, "[C")||!strcmp(buf, "OC")){rl_forward(1,27);return 0;}
   if(!strcmp(buf, "[D")||!strcmp(buf, "OD")){rl_backward(1,27);return 0;}
   if(!strcmp(buf, "[H")||!strcmp(buf, "OH")){rl_beg_of_line(1,27);return 0;}
diff --git a/utilities/modbot/commands.html b/utilities/modbot/commands.html
new file mode 100644
index 0000000..c9477b2
--- /dev/null
+++ b/utilities/modbot/commands.html
@@ -0,0 +1,30 @@
+<html>
+<head>
+  <title>modbot commands</title>
+  <style>
+  <!--
+  th{text-align:left;}
+  -->
+  </style>
+</head>
+<body>
+  modbot is a utility to queue videos to be played, with a list of approved videos that will automatically play when requested. Below is a list of supported commands.<br /><br />
+  <table cellspacing="1" border="0">
+    <tr><th>Command</th><th>Description</th></tr>
+    <tr><td>!request &lt;link/searchterm&gt;</td><td>request a video to be played</td></tr>
+    <tr><td>!queue</td><td>get the number of songs in queue and which (if any) need to  be approved</td></tr>
+    <tr><td>!wrongrequest</td><td>undo the last request you made</td></tr>
+    <tr><td>!requestedby</td><td>see who requested the current video</td></tr>
+    <tr><td>!modstats</td><td>get a percentage of how often there are mods in the channel (aside from modbot)</td></tr>
+    <tr><th colspan="2">Mod commands:</th></tr>
+    <tr><td>!playnext</td><td>play the next video in queue without approving it (to see if it's ok)</td></tr>
+    <tr><td>!approve</td><td>mark the currently playing video as good, or if none is playing the next in queue</td></tr>
+    <tr><td>!approve &lt;link/searchterm&gt;</td><td>mark the specified video as okay</td></tr>
+    <tr><td>!approve next</td><td>mark the next not yet approved video as okay</td></tr>
+    <tr><td>!approve entire queue</td><td>approve all videos in queue (for playlists)</td></tr>
+    <tr><td>!badvid</td><td>stop playing the current video and mark it as bad</td></tr>
+    <tr><td>!badvid &lt;link/searchterm&gt;</td><td>mark the specified video as bad, preventing it from ever being queued again</td></tr>
+  </table><br />
+  You can also just play videos manually/the good old way and they will be marked as good.
+</body>
+</html>
diff --git a/utilities/modbot/modbot.c b/utilities/modbot/modbot.c
index 3e84a77..86ed03b 100644
--- a/utilities/modbot/modbot.c
+++ b/utilities/modbot/modbot.c
@@ -38,6 +38,20 @@ char* requester=0;
 time_t started=0;
 int tc_client;
 
+time_t time_with_mods=0;
+time_t time_modcount;
+char havemods=0;
+void timemods(void)
+{
+  time_t now=time(0);
+  if(havemods)
+  {
+    time_with_mods+=now-time_modcount;
+  }
+  time_modcount=now;
+  havemods=(mods.itemcount>1); // Not counting modbot as a mod
+}
+
 void say(const char* pm, const char* fmt, ...)
 {
   va_list va;
@@ -57,32 +71,51 @@ void say(const char* pm, const char* fmt, ...)
   write(tc_client, buf, strlen(buf));
 }
 
-void getvidinfo(const char* vid, const char* type, char* buf, unsigned int len)
+void getvidinfo(const char* vid, const char* type, char* buf, char* errbuf, unsigned int len)
 {
   int out[2];
+  int err[2];
   pipe(out);
+  pipe(err);
   if(!fork())
   {
     close(out[0]);
+    close(err[0]);
     dup2(out[1], 1);
+    dup2(err[1], 2);
     execlp("youtube-dl", "youtube-dl", "--default-search", "auto", type, "--", vid, (char*)0);
     perror("execlp(youtube-dl)");
     _exit(1);
   }
   wait(0);
   close(out[1]);
-  len=read(out[0], buf, len-1);
-  if(len<0){len=0;}
-  while(len>0 && (buf[len-1]=='\r' || buf[len-1]=='\n')){--len;} // Strip newlines
-  buf[len]=0;
+  close(err[1]);
+  size_t r;
+  // Read output
+  r=read(out[0], buf, len-1);
+  if(r<0){r=0;}
+  while(r>0 && (buf[r-1]=='\r' || buf[r-1]=='\n')){--r;} // Strip newlines
+  buf[r]=0;
   close(out[0]);
+  // Read any error messages
+  if(errbuf)
+  {
+    r=read(err[0], errbuf, len-1);
+    if(r<0){r=0;}
+    while(r>0 && (errbuf[r-1]=='\r' || errbuf[r-1]=='\n')){--r;} // Strip newlines
+    errbuf[r]=0;
+    char* newline; // No need for newlines in error messages
+    while((newline=strchr(errbuf, '\n'))){newline[0]=' ';}
+    while((newline=strchr(errbuf, '\r'))){newline[0]=' ';}
+  }
+  close(err[0]);
 }
 
 unsigned int getduration(const char* vid)
 {
   char timebuf[128];
   timebuf[0]=':'; // Sacrifice 1 byte to avoid having to deal with a special case later on, where no ':' is found and we go from the start of the string, but only once
-  getvidinfo(vid, "--get-duration", &timebuf[1], 127);
+  getvidinfo(vid, "--get-duration", &timebuf[1], 0, 127);
   if(!timebuf[1]){printf("Failed to get video duration using youtube-dl, assuming 60s\n"); return 60;} // If using youtube-dl fails, assume videos are 1 minute long
   // youtube-dl prints it out in hh:mm:ss format, convert it to plain seconds
   unsigned int len;
@@ -153,6 +186,7 @@ int main(int argc, char** argv)
   // Handle arguments (-d, -l, -h additions, the rest are handled by tc_client)
   char daemon=0;
   char* logfile=0;
+  char verbose=0;
   unsigned int i;
   for(i=1; i<argc; ++i)
   {
@@ -174,11 +208,21 @@ int main(int argc, char** argv)
       argv[argc]=0;
       --i;
     }
+    else if(!strcmp(argv[i], "-v") || !strcmp(argv[i], "--verbose"))
+    {
+      verbose=1;
+      // Remove non-tc_client argument
+      --argc;
+      memmove(&argv[i], &argv[i+1], sizeof(char*)*(argc-i));
+      argv[argc]=0;
+      --i;
+    }
     else if(!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help"))
     {
       printf("Additional options for modbot:\n"
              "-d/--daemon     = daemonize after startup\n"
              "-l/--log <file> = log output into <file>\n"
+             "-v/--verbose    = print/log all incoming messages\n"
              "\n");
       execv("./tc_client", argv);
       return 1;
@@ -205,6 +249,8 @@ int main(int argc, char** argv)
   list_load(&badvids, "badvids.txt");
   char buf[1024];
   int len=0;
+  time_t sessionstart=time(0);
+  time_modcount=sessionstart;
   while(1)
   {
     if(read(out[0], &buf[len], 1)<1){break;}
@@ -237,7 +283,6 @@ int main(int argc, char** argv)
       memmove(esc, &esc[len+1], strlen(&esc[len]));
     }
     len=0;
-    // printf("Got line '%s'\n", buf);
     // Note: daemonizing and setting up logging here to avoid interfering with account password entry
     if(daemon)
     {
@@ -253,6 +298,7 @@ int main(int argc, char** argv)
       close(f);
       logfile=0;
     }
+    if(verbose){printf("Got line '%s'\n", buf); fflush(stdout);}
     char* space=strchr(buf, ' ');
     if(!space){continue;}
     if(!strcmp(space, " is a moderator."))
@@ -260,12 +306,14 @@ int main(int argc, char** argv)
       // If there are not-yet-approved videos in the queue when a mod joins, ask them to review them
       space[0]=0;
       list_add(&mods, buf);
+      timemods();
       continue;
     }
     if(!strcmp(space, " is no longer a moderator."))
     {
       space[0]=0;
       list_del(&mods, buf);
+      timemods();
       continue;
     }
     space[0]=0;
@@ -291,19 +339,24 @@ int main(int argc, char** argv)
         {
           char title[256];
           char vid[1024];
-          getvidinfo(&msg[9], "--get-id", vid, 1024);
-          if(!vid[0]){say(pm, "No video found, sorry\n"); continue;} // Nothing found
+          char viderr[1024];
+          getvidinfo(&msg[9], "--get-id", vid, viderr, 1024);
+          if(!vid[0]) // Nothing found
+          {
+            say(pm, "No video found, sorry (%s)\n", viderr);
+            continue;
+          }
           char* plist;
           for(plist=vid; plist[0] && plist[0]!='\r' && plist[0]!='\n'; plist=&plist[1]);
           if(plist[0]) // Link was a playlist, do some trickery to get the title of the first video (instead of getting nothing)
           {
             strcpy(title, "Playlist, starting with ");
             plist[0]=0;
-            getvidinfo(vid, "--get-title", &title[24], 256-24);
+            getvidinfo(vid, "--get-title", &title[24], 0, 256-24);
             plist[0]='\n';
           }else{
             plist=0;
-            getvidinfo(vid, "--get-title", title, 256);
+            getvidinfo(vid, "--get-title", title, 0, 256);
           }
           printf("Requested ID '%s' by '%s'\n", vid, nick);
           // Check if it's already queued and mention which spot it's in, or if it's marked as bad and shouldn't be queued
@@ -362,7 +415,7 @@ int main(int argc, char** argv)
         else if(!strncmp(msg, "!wrongrequest ", 14))
         {
           char vid[1024];
-          getvidinfo(&msg[14], "--get-id", vid, 1024);
+          getvidinfo(&msg[14], "--get-id", vid, 0, 1024);
           unsigned int i;
           for(i=0; i<queue.itemcount; ++i)
           {
@@ -421,33 +474,30 @@ int main(int argc, char** argv)
         }
         else if(!strcmp(msg, "!help"))
         {
-          say(nick, "The following commands can be used:\n");
-          usleep(500000);
-          say(nick, "!request <link> = request a video to be played\n");
-          usleep(500000);
-          say(nick, "!queue          = get the number of songs in queue and which (if any) need to  be approved\n");
-          usleep(500000);
-          say(nick, "Mod commands:\n"); // TODO: don't bother filling non-mods' chats with these?
-          usleep(500000);
-          say(nick, "!playnext       = play the next video in queue without approving it (to see if it's ok)\n");
-          usleep(500000);
-          say(nick, "!approve        = mark the currently playing video as good, or if none is playing the next in queue\n");
-          usleep(500000);
-          say(nick, "!approve <link> = mark the specified video as okay\n");
-          usleep(500000);
-          say(nick, "!approve next   = mark the next not yet approved video as okay\n");
-          usleep(500000);
-          say(nick, "!approve entire queue = approve all videos in queue (for playlists)\n");
-          usleep(500000);
-          say(nick, "!badvid         = stop playing the current video and mark it as bad\n");
-          usleep(500000);
-          say(nick, "!badvid <link>  = mark the specified video as bad, preventing it from ever being queued again\n");
-          usleep(500000);
-          say(nick, "!wrongrequest   = undo the last request you made\n");
-          usleep(500000);
-          say(nick, "!requestedby    = see who requested the current video\n");
-          usleep(500000);
-          say(nick, "You can also just play videos manually/the good old way and they will be marked as good.\n");
+          say(pm, "http://tc_client.ion.nu/misc/modbotcommands.html\n");
+        }
+        else if(!strcmp(msg, "!modstats"))
+        {
+          unsigned int session=time(0)-sessionstart;
+          timemods();
+          unsigned int hasmods=time_with_mods*100/session;
+          const char* timeformat="seconds";
+          if(session>=120)
+          {
+            session/=60;
+            timeformat="minutes";
+            if(session>=120)
+            {
+              session/=60;
+              timeformat="hours";
+              if(session>=48)
+              {
+                session/=24;
+                timeformat="days";
+              }
+            }
+          }
+          say(pm, "The channel has had mods %u%% of the time for the past %u %s\n", hasmods, session, timeformat);
         }
         else if(list_contains(&mods, nick)) // Mods-only commands
         {
@@ -514,7 +564,7 @@ int main(int argc, char** argv)
               }
               continue;
             }else{
-              getvidinfo(vid, "--get-id", vidbuf, 256);
+              getvidinfo(vid, "--get-id", vidbuf, 0, 256);
               vid=vidbuf;
             }
             list_add(&goodvids, vid);
@@ -530,7 +580,7 @@ int main(int argc, char** argv)
             char vid[1024];
             if(space && space[1])
             {
-              getvidinfo(&space[1], "--get-id", vid, 256);
+              getvidinfo(&space[1], "--get-id", vid, 0, 256);
             }else{strncpy(vid, playing, 1023); vid[1023]=0;}
             if(!vid[0]){say(pm, "Video not found, sorry\n"); continue;}
             queue_del(&queue, vid);
@@ -613,6 +663,12 @@ int main(int argc, char** argv)
             say(0, "/priv %s /mbs youTube %s %u\n", nick, playing, (time(0)-started)*1000);
           }
         }
+        else if(!strcmp(space, " left the channel"))
+        {
+          space[0]=0;
+          list_del(&mods, nick); // Absent people can't be mods
+          timemods();
+        }
       }
     }
   }