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

    Version 0.22

diff --git a/ChangeLog b/ChangeLog
index a403f50..424a400 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,12 @@
+0.22:
+Cleaned up media.c/h which I forgot to clean up before releasing 0.21.
+Detect when someone cams down.
+Added a /names command to list who is online.
+camviewer: fall back to GTK+2 if GTK+3 is not available.
+camviewer: show multiple cameras (cams to view are now picked up from who is on cam when joining and whenever someone cams up instead of from argv[1])
+modbot: list at most 5 not yet approved videos for !queue to avoid sending messages which are too large.
+modbot: fixed a bug where removing a non-approved video from queue using !wrongrequest and that video was the first in queue, the second video in queue wouldn't be played.
+modbot: added !whorequested to see who requested the currently playing video.
 0.21:
 Renamed streamids to chunkids to better match the specification.
 Added a /close command for mods to close cam/mic streams.
diff --git a/Makefile b/Makefile
index 7cdc521..f943ec4 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-VERSION=0.21
+VERSION=0.22
 CFLAGS=-g3 -Wall $(shell curl-config --cflags)
 LIBS=-g3 $(shell curl-config --libs)
 ifneq ($(wildcard config.mk),)
diff --git a/amfparser.c b/amfparser.c
index 8dfaf51..35e9a76 100644
--- a/amfparser.c
+++ b/amfparser.c
@@ -177,6 +177,16 @@ void amf_free(struct amf* amf)
   free(amf);
 }
 
+struct amfitem* amf_getobjmember(struct amfobject* obj, const char* name)
+{
+  unsigned int i;
+  for(i=0; i<obj->membercount; ++i)
+  {
+    if(!strcmp(obj->members[i].name, name)){return &obj->members[i].value;}
+  }
+  return 0;
+}
+
 void printamfobject(struct amfobject* obj, int indent)
 {
   int i, j;
diff --git a/amfparser.h b/amfparser.h
index fda86f0..96d9a1c 100644
--- a/amfparser.h
+++ b/amfparser.h
@@ -65,5 +65,6 @@ extern char amf_compareitems(struct amfitem* a, struct amfitem* b);
 
 extern struct amf* amf_parse(const unsigned char* buf, int len);
 extern void amf_free(struct amf* amf);
+extern struct amfitem* amf_getobjmember(struct amfobject* obj, const char* name);
 
 extern void printamf(struct amf* amf);
diff --git a/client.c b/client.c
index bb2c397..e7f8824 100644
--- a/client.c
+++ b/client.c
@@ -400,7 +400,8 @@ int main(int argc, char** argv)
                  "/close <nick>   = close someone's cam/mic stream (as a mod)\n"
                  "/ban <nick>     = ban someone\n"
                  "/banlist        = list who is banned\n"
-                 "/forgive <nick/ID> = unban someone\n");
+                 "/forgive <nick/ID> = unban someone\n"
+                 "/names          = list everyone that is online\n");
           fflush(stdout);
         }
         else if(!strncmp(buf, "/color", 6) && (!buf[6]||buf[6]==' '))
@@ -510,6 +511,17 @@ int main(int argc, char** argv)
           amfsend(&amf, sock);
           continue;
         }
+        else if(!strcmp(buf, "/names"))
+        {
+          printf("Currently online: ");
+          for(i=0; i<idlistlen; ++i)
+          {
+            printf("%s%s", (i?", ":""), idlist[i].name);
+          }
+          printf("\n");
+          fflush(stdout);
+          continue;
+        }
       }
       char* msg=tonumlist(buf, len);
       amfinit(&amf, 3);
@@ -742,7 +754,7 @@ int main(int argc, char** argv)
         for(;len<10; ++len){printf(" ");}
         printf(" %s\n", amfin->items[i+1].string.string);
       }
-      printf("Use /forgive <ID> to unban someone\n");
+      printf("Use /forgive <ID/nick> to unban someone\n");
       fflush(stdout);
     }
     // "avons", 0, "ID1", "nick1", "IDn", "nickn"...
@@ -775,6 +787,10 @@ int main(int argc, char** argv)
       // Creating a new stream worked, now play media (cam/mic) on it (if that's what the result was for)
       stream_play(amfin, sock);
     }
+    else if(amfin->itemcount>0 && amfin->items[0].type==AMF_STRING && amf_comparestrings_c(&amfin->items[0].string, "onStatus"))
+    {
+      stream_handlestatus(amfin);
+    }
     // else{printf("Unknown command...\n"); printamf(amfin);} // (Debugging)
     amf_free(amfin);
   }
diff --git a/configure b/configure
index 517c8ec..955fecc 100755
--- a/configure
+++ b/configure
@@ -39,17 +39,26 @@ fi
 rm -f iconvtest iconvtest.c
 
 printf 'Checking for gtk+-3.0... '
-libs="`pkg-config --libs gtk+-3.0`"
+libs="`pkg-config --libs gtk+-3.0 2> /dev/null`"
 if [ "x$libs" != "x" ]; then
   echo "GTK_LIBS=${libs}" >> config.mk
   echo "GTK_CFLAGS=`pkg-config --cflags gtk+-3.0`" >> config.mk
   echo yes
 else
   echo no
+  printf 'Checking for gtk+-2.0... '
+  libs="`pkg-config --libs gtk+-2.0 2> /dev/null`"
+  if [ "x$libs" != "x" ]; then
+    echo "GTK_LIBS=${libs}" >> config.mk
+    echo "GTK_CFLAGS=`pkg-config --cflags gtk+-2.0`" >> config.mk
+    echo yes
+  else
+    echo no
+  fi
 fi
 
 printf 'Checking for libavcodec... '
-libs="`pkg-config --libs libavcodec`"
+libs="`pkg-config --libs libavcodec 2> /dev/null`"
 if [ "x$libs" != "x" ]; then
   echo "AVCODEC_LIBS=${libs}" >> config.mk
   echo "AVCODEC_CFLAGS=`pkg-config --cflags libavcodec`" >> config.mk
@@ -59,7 +68,7 @@ else
 fi
 
 printf 'Checking for libswscale... '
-libs="`pkg-config --libs libswscale`"
+libs="`pkg-config --libs libswscale 2> /dev/null`"
 if [ "x$libs" != "x" ]; then
   echo "SWSCALE_LIBS=${libs}" >> config.mk
   echo "SWSCALE_CFLAGS=`pkg-config --cflags libswscale`" >> config.mk
@@ -69,7 +78,7 @@ else
 fi
 
 printf 'Checking for libavutil... '
-libs="`pkg-config --libs libavutil`"
+libs="`pkg-config --libs libavutil 2> /dev/null`"
 if [ "x$libs" != "x" ]; then
   echo "AVUTIL_LIBS=${libs}" >> config.mk
   echo "AVUTIL_CFLAGS=`pkg-config --cflags libavutil`" >> config.mk
diff --git a/media.c b/media.c
index 71d54e9..18157be 100644
--- a/media.c
+++ b/media.c
@@ -16,19 +16,11 @@
 */
 #include <stdio.h>
 #include <stdlib.h>
-//#include "rtmp.h"
-//#include "amfparser.h"
+#include <string.h>
 #include "endian.h"
 #include "media.h"
 #include "amfwriter.h"
 #include "idlist.h"
-/*
-struct stream
-{
-  unsigned int streamid;
-  unsigned int userid;
-};
-*/
 
 struct stream* streams=0;
 unsigned int streamcount=0;
@@ -43,14 +35,9 @@ char stream_idtaken(unsigned int id)
   return 0;
 }
 
-void stream_start(const char* nick, int sock) // called upon privmsg "/cam ..."
+void stream_start(const char* nick, int sock) // called upon privmsg "/opencam ..."
 {
   unsigned int userid=idlist_get(nick);
-/*
-  unsigned int id=idlist_get((char*)&buf[5]);
-  camid=malloc(128);
-  sprintf(camid, "%u", id);
-*/
   unsigned int streamid=1;
   while(stream_idtaken(streamid)){++streamid;}
   ++streamcount;
@@ -90,16 +77,40 @@ void stream_play(struct amf* amf, int sock) // called upon _result
 
 void stream_handledata(struct rtmp* rtmp)
 {
-rtmp->msgid=1;
   unsigned int i;
   for(i=0; i<streamcount; ++i)
   {
     if(streams[i].streamid!=rtmp->msgid){continue;}
-    printf("Video: %u %u\n", streams[i].userid, rtmp->length); // TODO: if this becomes permanent we will have to specify a nick or ID or something
-//  write(1, rtmp.buf, rtmp.length);
+// fprintf(stderr, "Chunk: chunkid: %u, streamid: %u, userid: %u\n", rtmp->chunkid, rtmp->msgid, streams[i].userid);
+    printf("Video: %u %u\n", streams[i].userid, rtmp->length);
     fwrite(rtmp->buf, rtmp->length, 1, stdout);
     fflush(stdout);
     return;
   }
   printf("Received media data to unknown stream ID %u\n", rtmp->msgid);
 }
+
+void stream_handlestatus(struct amf* amf)
+{
+  if(amf->itemcount<3 || amf->items[2].type!=AMF_OBJECT){return;}
+  struct amfobject* obj=&amf->items[2].object;
+  struct amfitem* code=amf_getobjmember(obj, "code");
+  struct amfitem* details=amf_getobjmember(obj, "details");
+  if(!code || !details){return;}
+  if(code->type!=AMF_STRING || details->type!=AMF_STRING){return;}
+  if(!strcmp(code->string.string, "NetStream.Play.Stop"))
+  {
+    unsigned int id=strtoul(details->string.string, 0, 0);
+    unsigned int i;
+    for(i=0; i<streamcount; ++i)
+    {
+      if(streams[i].userid==id)
+      {
+        printf("VideoEnd: %u\n", streams[i].userid);
+// TODO: remove stream from array
+// TODO: destroyStream or something?
+        return;
+      }
+    }
+  }
+}
diff --git a/media.h b/media.h
index 58850d4..728c3dd 100644
--- a/media.h
+++ b/media.h
@@ -25,6 +25,7 @@ struct stream
 extern struct stream* streams;
 extern unsigned int streamcount;
 
-extern void stream_start(const char* nick, int sock); // called upon privmsg "/cam ..."
+extern void stream_start(const char* nick, int sock); // called upon privmsg "/opencam ..."
 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);
diff --git a/rtmp.c b/rtmp.c
index 3c5c82b..ab2c29f 100644
--- a/rtmp.c
+++ b/rtmp.c
@@ -49,6 +49,7 @@ struct chunk* chunk_get(unsigned int id)
   chunks[i].buf=0;
   chunks[i].timestamp=0;
   chunks[i].length=0;
+  chunks[i].type=0;
   return &chunks[i];
 }
 
@@ -101,7 +102,7 @@ char rtmp_get(int sock, struct rtmp* rtmp)
   unsigned int rsize=((chunk->length-chunk->pos>127)?128:(chunk->length-chunk->pos));
   while(rsize>0)
   {
-    size_t r=read(sock, chunk->buf+chunk->pos, rsize);;
+    size_t r=read(sock, chunk->buf+chunk->pos, rsize);
     if(r<1){return 0;}
     rsize-=r;
     chunk->pos+=r;
diff --git a/utilities/camviewer/camviewer.c b/utilities/camviewer/camviewer.c
index 51e6738..dca0f35 100644
--- a/utilities/camviewer/camviewer.c
+++ b/utilities/camviewer/camviewer.c
@@ -20,13 +20,26 @@
 #include <libswscale/swscale.h>
 #include <gtk/gtk.h>
 
-struct viddata
+#if GTK_MAJOR_VERSION==2
+  #define gtk_box_new(orientation, spacing) gtk_hbox_new(1, spacing)
+#endif
+
+struct camera
 {
   AVFrame* frame;
   AVFrame* dstframe;
   GtkWidget* cam;
   AVCodecContext* ctx;
-  char* camnick;
+  char* id;
+  char* nick;
+};
+
+struct viddata
+{
+  struct camera* cams;
+  unsigned int camcount;
+  GtkWidget* box;
+  AVCodec* decoder;
 };
 
 int tc_client[2];
@@ -36,68 +49,138 @@ char buf[1024];
 gboolean handledata(GIOChannel* channel, GIOCondition condition, gpointer datap)
 {
   struct viddata* data=datap;
-  int i;
+  unsigned int i;
   for(i=0; i<1023; ++i)
   {
     if(read(tc_client[0], &buf[i], 1)<1){printf("No more data\n"); gtk_main_quit(); return 0;}
     if(buf[i]=='\r'||buf[i]=='\n'){break;}
   }
   buf[i]=0;
-  // Start stream once we're properly connected
-  if(!strncmp(buf, "Connection ID: ", 10)){dprintf(tc_client_in[1], "/opencam %s\n", data->camnick); return 1;}
-  if(strncmp(buf, "Video: ", 7)){printf("Got '%s'\n", buf); return 1;} // Ignore anything that isn't video
+  // Start streams once we're properly connected
+  if(!strncmp(buf, "Currently on cam: ", 18))
+  {
+    char* next=&buf[16];
+    while(next)
+    {
+      char* user=&next[2];
+      next=strstr(user, ", ");
+      if(next){next[0]=0;}
+      dprintf(tc_client_in[1], "/opencam %s\n", user);
+    }
+    return 1;
+  }
+  char* space=strchr(buf, ' ');
+  // Start a stream when someone cams up
+  if(space && !strcmp(space, " cammed up"))
+  {
+    space[0]=0;
+    dprintf(tc_client_in[1], "/opencam %s\n", buf);
+    return 1;
+  }
+  if(!strncmp(buf, "Starting media stream for ", 26))
+  {
+    char* nick=&buf[26];
+    char* id=strstr(nick, " (");
+    if(!id){return 1;}
+    id[0]=0;
+    id=&id[2];
+    char* idend=strchr(id, ')');
+    if(!idend){return 1;}
+    idend[0]=0;
+    ++data->camcount;
+    data->cams=realloc(data->cams, sizeof(struct camera)*data->camcount);
+    struct camera* cam=&data->cams[data->camcount-1];
+    cam->frame=av_frame_alloc();
+    cam->dstframe=av_frame_alloc();
+    cam->nick=strdup(nick);
+    cam->id=strdup(id);
+    cam->ctx=avcodec_alloc_context3(data->decoder);
+    avcodec_open2(cam->ctx, data->decoder, 0);
+    cam->cam=gtk_image_new();
+    gtk_box_pack_start(GTK_BOX(data->box), cam->cam, 0, 0, 0);
+    gtk_widget_show(cam->cam);
+    return 1;
+  }
+  if(!strncmp(buf, "VideoEnd: ", 10))
+  {
+    for(i=0; i<data->camcount; ++i)
+    {
+      if(!strcmp(data->cams[i].id, &buf[10]))
+      {
+        gtk_widget_destroy(data->cams[i].cam);
+        av_frame_free(&data->cams[i].frame);
+        avcodec_free_context(&data->cams[i].ctx);
+        free(data->cams[i].id);
+        free(data->cams[i].nick);
+        --data->camcount;
+        memmove(&data->cams[i], &data->cams[i+1], (data->camcount-i)*sizeof(struct camera));
+        break;
+      }
+    }
+    return 1;
+  }
+  if(strncmp(buf, "Video: ", 7)){printf("Got '%s'\n", buf); return 1;} // Ignore anything else that isn't video
   char* sizestr=strchr(&buf[7], ' ');
+  if(!sizestr){return 1;}
+  sizestr[0]=0;
+  // Find the camera representation for the given ID
+  struct camera* cam=0;
+  for(i=0; i<data->camcount; ++i)
+  {
+    if(!strcmp(data->cams[i].id, &buf[7])){cam=&data->cams[i]; break;}
+  }
   unsigned int size=strtoul(&sizestr[1], 0, 0);
-  // Mostly ignore the first byte (contains frame type (e.g. keyframe etc.) in 4 bits and codec in the other 4, but we assume FLV1)
+  // Mostly ignore the first byte (contains frame type (e.g. keyframe etc.) in 4 bits and codec in the other 4)
   --size;
   AVPacket pkt;
   av_init_packet(&pkt);
-  pkt.data=malloc(size);
-  read(tc_client[0], pkt.data, 1); // Skip
-// printf("Frametype-frame: %x\n", ((unsigned int)pkt.data[0]&0xf0)/16);
-// printf("Frametype-codec: %x\n", (unsigned int)pkt.data[0]&0xf);
-  if((pkt.data[0]&0xf)!=2) // Not FLV1, get data but discard it
+  unsigned char databuf[size+4];
+  pkt.data=databuf;
+  unsigned char frameinfo;
+  read(tc_client[0], &frameinfo, 1);
+// printf("Frametype-frame: %x\n", ((unsigned int)frameinfo&0xf0)/16);
+// printf("Frametype-codec: %x\n", (unsigned int)frameinfo&0xf);
+  unsigned int pos=0;
+  while(pos<size)
   {
-    read(tc_client[0], pkt.data, size);
-    return 1;
+    pos+=read(tc_client[0], pkt.data+pos, size-pos);
   }
-  read(tc_client[0], pkt.data, size);
+  if((frameinfo&0xf)!=2){return 1;} // Not FLV1, get data but discard it
+  if(!cam){printf("No cam found with ID '%s'\n", &buf[7]); free(pkt.data); return 1;}
   pkt.size=size;
   int gotframe;
-  avcodec_decode_video2(data->ctx, data->frame, &gotframe, &pkt);
-  free(pkt.data);
+  avcodec_decode_video2(cam->ctx, cam->frame, &gotframe, &pkt);
   if(!gotframe){return 1;}
 
   // Convert to RGB24 format
-  unsigned int bufsize=avpicture_get_size(PIX_FMT_RGB24, data->frame->width, data->frame->height);
+  unsigned int bufsize=avpicture_get_size(PIX_FMT_RGB24, cam->frame->width, cam->frame->height);
   unsigned char buf[bufsize];
-  data->dstframe->data[0]=buf;
-  data->dstframe->linesize[0]=data->frame->width*3;
-  struct SwsContext* swsctx=sws_getContext(data->frame->width, data->frame->height, data->frame->format, data->frame->width, data->frame->height, AV_PIX_FMT_RGB24, 0, 0, 0, 0);
-  sws_scale(swsctx, (const uint8_t*const*)data->frame->data, data->frame->linesize, 0, data->frame->height, data->dstframe->data, data->dstframe->linesize);
+  cam->dstframe->data[0]=buf;
+  cam->dstframe->linesize[0]=cam->frame->width*3;
+  struct SwsContext* swsctx=sws_getContext(cam->frame->width, cam->frame->height, cam->frame->format, cam->frame->width, cam->frame->height, AV_PIX_FMT_RGB24, 0, 0, 0, 0);
+  sws_scale(swsctx, (const uint8_t*const*)cam->frame->data, cam->frame->linesize, 0, cam->frame->height, cam->dstframe->data, cam->dstframe->linesize);
+  sws_freeContext(swsctx);
 
-  GdkPixbuf* gdkframe=gdk_pixbuf_new_from_data(data->dstframe->data[0], GDK_COLORSPACE_RGB, 0, 8, data->frame->width, data->frame->height, data->dstframe->linesize[0], 0, 0);
-  gtk_image_set_from_pixbuf(GTK_IMAGE(data->cam), gdkframe);
-  g_object_unref(gdkframe);
+  GdkPixbuf* gdkframe=gdk_pixbuf_new_from_data(cam->dstframe->data[0], GDK_COLORSPACE_RGB, 0, 8, cam->frame->width, cam->frame->height, cam->dstframe->linesize[0], 0, 0);
+  gtk_image_set_from_pixbuf(GTK_IMAGE(cam->cam), gdkframe);
+  // Make sure it gets redrawn in time
+  gdk_window_process_updates(gtk_widget_get_window(cam->cam), 1);
 
+  g_object_unref(gdkframe);
   return 1;
 }
 
 int main(int argc, char** argv)
 {
-  struct viddata data={av_frame_alloc(),av_frame_alloc()};
-  data.camnick=argv[1];
-  // Init the decoder
+  struct viddata data={0,0,0,0};
   avcodec_register_all();
-  AVCodec* decoder=avcodec_find_decoder(AV_CODEC_ID_FLV1);
-  data.ctx=avcodec_alloc_context3(decoder);
-  avcodec_open2(data.ctx, decoder, 0);
+  data.decoder=avcodec_find_decoder(AV_CODEC_ID_FLV1);
 
   gtk_init(&argc, &argv);
   GtkWidget* w=gtk_window_new(GTK_WINDOW_TOPLEVEL);
   g_signal_connect(w, "destroy", gtk_main_quit, 0);
-  data.cam=gtk_image_new();
-  gtk_container_add(GTK_CONTAINER(w), data.cam);
+  data.box=gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+  gtk_container_add(GTK_CONTAINER(w), data.box);
   gtk_widget_show_all(w);
 
   pipe(tc_client);
@@ -108,18 +191,27 @@ int main(int argc, char** argv)
     close(tc_client_in[1]);
     dup2(tc_client[1], 1);
     dup2(tc_client_in[0], 0);
-    argv[1]="./tc_client";
-    execv("./tc_client", &argv[1]);
+    argv[0]="./tc_client";
+    execv("./tc_client", argv);
   }
   close(tc_client[1]);
   close(tc_client_in[0]);
   GIOChannel* tcchannel=g_io_channel_unix_new(tc_client[0]);
   g_io_channel_set_encoding(tcchannel, 0, 0);
-  g_io_add_watch(tcchannel, G_IO_IN, handledata, &data);
+  unsigned int channel_id=g_io_add_watch(tcchannel, G_IO_IN, handledata, &data);
 
   gtk_main();
 
-  free(data.frame->data[0]);
-  av_frame_free(&data.frame);
+  g_source_remove(channel_id);
+  g_io_channel_shutdown(tcchannel, 0, 0);
+  unsigned int i;
+  for(i=0; i<data.camcount; ++i)
+  {
+    av_frame_free(&data.cams[i].frame);
+    avcodec_free_context(&data.cams[i].ctx);
+    free(data.cams[i].id);
+    free(data.cams[i].nick);
+  }
+  free(data.cams);
   return 0;
 }
diff --git a/utilities/modbot/modbot.c b/utilities/modbot/modbot.c
index 87155b9..90eec60 100644
--- a/utilities/modbot/modbot.c
+++ b/utilities/modbot/modbot.c
@@ -34,6 +34,7 @@ struct queue queue={0,0};
 struct list goodvids={0,0}; // pre-approved videos
 struct list badvids={0,0}; // not allowed, essentially banned
 char* playing=0;
+char* requester=0;
 time_t started=0;
 int tc_client;
 
@@ -105,7 +106,7 @@ void playnextvid()
 {
   waitskip=0;
   playing=queue.items[0].video;
-  free(queue.items[0].requester);
+  requester=queue.items[0].requester;
   free(queue.items[0].title);
   --queue.itemcount;
   memmove(queue.items, &queue.items[1], sizeof(struct queueitem)*queue.itemcount);
@@ -118,7 +119,9 @@ void playnextvid()
 void playnext(int x)
 {
   free(playing);
+  free(requester);
   playing=0;
+  requester=0;
   if(queue.itemcount<1){alarm(0); printf("Nothing more to play\n"); return;} // Nothing more to play
   if(!list_contains(&goodvids, queue.items[0].video))
   {
@@ -297,6 +300,7 @@ int main(int argc, char** argv)
             if(!strcmp(queue.items[i].requester, nick))
             {
               queue_del(&queue, queue.items[i].video);
+              if(!playing && i==0){playnext(0);}
               break;
             }
           }
@@ -332,22 +336,29 @@ int main(int argc, char** argv)
           {
             char buf[len];
             buf[0]=0;
+            unsigned int listed=0;
             for(i=0; i<queue.itemcount; ++i)
             {
-              if(!list_contains(&goodvids, queue.items[i].video))
+              if(listed<5 && !list_contains(&goodvids, queue.items[i].video))
               {
                 if(buf[0]){strcat(buf, ", ");}
                 strcat(buf, queue.items[i].video);
                 strcat(buf, " (");
                 strcat(buf, queue.items[i].title);
                 strcat(buf, ")");
+                ++listed;
               }
             }
-            say(pm, "%u videos in queue, %u of which are not yet approved by mods (%s)\n", queue.itemcount, notapproved, buf);
+            say(pm, "%u videos in queue, %u of which are not yet approved by mods (%s%s)\n", queue.itemcount, notapproved, buf, (listed<notapproved)?", etc.":"");
           }else{
             say(pm, "%u videos in queue\n", queue.itemcount);
           }
         }
+        else if(!strcmp(msg, "!whorequested"))
+        {
+          if(!playing){say(pm, "Nothing is playing\n");}
+          else{say(pm, "%s requested %s\n", requester, playing);}
+        }
         else if(!strcmp(msg, "!time")) // Debugging
         {
           unsigned int remaining=alarm(0);