]> granicus.if.org Git - rtmpdump/commitdiff
Import rtmpdump v1.3d.
authordiego <diego@400ebc74-4327-4243-bc38-086b20814532>
Fri, 9 Oct 2009 18:44:40 +0000 (18:44 +0000)
committerdiego <diego@400ebc74-4327-4243-bc38-086b20814532>
Fri, 9 Oct 2009 18:44:40 +0000 (18:44 +0000)
git-svn-id: svn://svn.mplayerhq.hu/rtmpdump@1 400ebc74-4327-4243-bc38-086b20814532

16 files changed:
AMFObject.cpp [new file with mode: 0644]
AMFObject.h [new file with mode: 0644]
COPYING [new file with mode: 0644]
ChangeLog [new file with mode: 0644]
Makefile [new file with mode: 0644]
README [new file with mode: 0644]
get_hulu [new file with mode: 0644]
get_iplayer [new file with mode: 0644]
get_iplayer_old [new file with mode: 0644]
log.cpp [new file with mode: 0644]
log.h [new file with mode: 0644]
rtmp.cpp [new file with mode: 0644]
rtmp.h [new file with mode: 0644]
rtmpdump.cpp [new file with mode: 0644]
rtmppacket.cpp [new file with mode: 0644]
rtmppacket.h [new file with mode: 0644]

diff --git a/AMFObject.cpp b/AMFObject.cpp
new file mode 100644 (file)
index 0000000..b41de3b
--- /dev/null
@@ -0,0 +1,502 @@
+/*
+ *      Copyright (C) 2005-2008 Team XBMC
+ *      http://www.xbmc.org
+ *      Copyright (C) 2008-2009 Andrej Stepanchuk
+ *
+ *  This Program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2, or (at your option)
+ *  any later version.
+ *
+ *  This Program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with RTMPDump; see the file COPYING.  If not, write to
+ *  the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
+ *  http://www.gnu.org/copyleft/gpl.html
+ *
+ */
+
+#include <string.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <time.h>
+#include <arpa/inet.h>
+
+#include "AMFObject.h"
+#include "log.h"
+#include "rtmp.h"
+
+RTMP_LIB::AMFObjectProperty RTMP_LIB::AMFObject::m_invalidProp;
+
+RTMP_LIB::AMFObjectProperty::AMFObjectProperty()
+{
+  Reset();
+}
+
+RTMP_LIB::AMFObjectProperty::AMFObjectProperty(const std::string & strName, double dValue)
+{
+  Reset();
+}
+
+RTMP_LIB::AMFObjectProperty::AMFObjectProperty(const std::string & strName, bool bValue)
+{
+  Reset();
+}
+
+RTMP_LIB::AMFObjectProperty::AMFObjectProperty(const std::string & strName, const std::string & strValue)
+{
+  Reset();
+}
+
+RTMP_LIB::AMFObjectProperty::AMFObjectProperty(const std::string & strName, const AMFObject & objValue)
+{
+  Reset();
+}
+
+RTMP_LIB::AMFObjectProperty::~ AMFObjectProperty()
+{
+}
+
+const std::string &RTMP_LIB::AMFObjectProperty::GetPropName() const
+{
+  return m_strName;
+}
+
+RTMP_LIB::AMFDataType RTMP_LIB::AMFObjectProperty::GetType() const
+{
+  return m_type;
+}
+
+double RTMP_LIB::AMFObjectProperty::GetNumber() const
+{
+  return m_dNumVal;
+}
+
+bool RTMP_LIB::AMFObjectProperty::GetBoolean() const
+{
+  return m_dNumVal != 0;
+}
+
+const std::string &RTMP_LIB::AMFObjectProperty::GetString() const
+{
+  return m_strVal;
+}
+
+const RTMP_LIB::AMFObject &RTMP_LIB::AMFObjectProperty::GetObject() const
+{
+  return m_objVal;
+}
+
+bool RTMP_LIB::AMFObjectProperty::IsValid() const
+{
+  return (m_type != AMF_INVALID);
+}
+
+int RTMP_LIB::AMFObjectProperty::Encode(char * pBuffer, int nSize) const
+{
+  int nBytes = 0;
+  
+  if (m_type == AMF_INVALID)
+    return -1;
+
+  if (m_type != AMF_NULL && nSize < (int)m_strName.size() + (int)sizeof(short) + 1)
+    return -1;
+
+  if (m_type != AMF_NULL && !m_strName.empty())
+  {
+    nBytes += EncodeName(pBuffer);
+    pBuffer += nBytes;
+    nSize -= nBytes;
+  }
+
+  switch (m_type)
+  {
+    case AMF_NUMBER:
+      if (nSize < 9)
+        return -1;
+      nBytes += RTMP_LIB::CRTMP::EncodeNumber(pBuffer, GetNumber());
+      break;
+
+    case AMF_BOOLEAN:
+      if (nSize < 2)
+        return -1;
+      nBytes += RTMP_LIB::CRTMP::EncodeBoolean(pBuffer, GetBoolean());
+      break;
+
+    case AMF_STRING:
+      if (nSize < (int)m_strVal.size() + (int)sizeof(short))
+        return -1;
+      nBytes += RTMP_LIB::CRTMP::EncodeString(pBuffer, GetString());
+      break;
+
+    case AMF_NULL:
+      if (nSize < 1)
+        return -1;
+      *pBuffer = 0x05;
+      nBytes += 1;
+      break;
+
+    case AMF_OBJECT:
+    {
+      int nRes = m_objVal.Encode(pBuffer, nSize);
+      if (nRes == -1)
+        return -1;
+
+      nBytes += nRes;
+      break;
+    }
+    default:
+      Log(LOGERROR,"%s, invalid type. %d", __FUNCTION__, m_type);
+      return -1;
+  };  
+
+  return nBytes;
+}
+
+int RTMP_LIB::AMFObjectProperty::Decode(const char * pBuffer, int nSize, bool bDecodeName) 
+{
+  int nOriginalSize = nSize;
+
+  if (nSize == 0 || !pBuffer) {
+    Log(LOGDEBUG,"empty buffer/no buffer pointer!");
+    return -1;
+  }
+  
+  //if (*pBuffer == 0x05 /* AMF_NULL */ || *pBuffer == 0x06 /* AMF_UNDEFINED */ || *pBuffer == 0x0D /* AMF_UNSUPPORTED */)
+  //{
+  //  m_type = AMF_NULL;
+  //  return 1;
+  //}
+
+  if (bDecodeName && nSize < 4) { // at least name (length + at least 1 byte) and 1 byte of data
+    Log(LOGDEBUG,"Not enough data for decoding with name, less then 4 bytes!");
+    return -1;
+  }
+
+  if (bDecodeName)
+  {
+    short nNameSize = RTMP_LIB::CRTMP::ReadInt16(pBuffer);
+    if (nNameSize > nSize - (short)sizeof(short)) {
+      Log(LOGDEBUG,"Name size out of range: namesize (%d) > len (%d) - 2", nNameSize, nSize);
+      return -1;
+    }
+
+    m_strName = RTMP_LIB::CRTMP::ReadString(pBuffer);
+    nSize -= sizeof(short) + m_strName.size();
+    pBuffer += sizeof(short) + m_strName.size();
+  }
+
+  if (nSize == 0) {
+    return -1;
+  }
+
+  nSize--;
+
+  switch (*pBuffer)
+  {
+    case 0x00: //AMF_NUMBER:
+      if (nSize < (int)sizeof(double))
+        return -1;
+      m_dNumVal = RTMP_LIB::CRTMP::ReadNumber(pBuffer+1);
+      nSize -= sizeof(double);
+      m_type = AMF_NUMBER;
+      break;
+    case 0x01: //AMF_BOOLEAN:
+      if (nSize < 1)
+        return -1;
+      m_dNumVal = (double)RTMP_LIB::CRTMP::ReadBool(pBuffer+1);
+      nSize--;
+      m_type = AMF_BOOLEAN;
+      break;
+    case 0x02: //AMF_STRING:
+    {
+      short nStringSize = RTMP_LIB::CRTMP::ReadInt16(pBuffer+1);
+      if (nSize < nStringSize + (int)sizeof(short))
+        return -1;
+      m_strVal = RTMP_LIB::CRTMP::ReadString(pBuffer+1);
+      nSize -= (sizeof(short) + nStringSize);
+      m_type = AMF_STRING;
+      break;
+    }
+    case 0x03: //AMF_OBJECT:
+    {
+      int nRes = m_objVal.Decode(pBuffer+1, nSize, true);
+      if (nRes == -1)
+        return -1;
+      nSize -= nRes;
+      m_type = AMF_OBJECT;
+      break;
+    }
+    case 0x0A: //AMF_ARRAY
+    {
+      int nArrayLen = RTMP_LIB::CRTMP::ReadInt32(pBuffer+1);
+      nSize -= 4;
+      
+      int nRes = m_objVal.DecodeArray(pBuffer+5, nSize, nArrayLen, false);
+      if (nRes == -1)
+        return -1;
+      nSize -= nRes;
+      m_type = AMF_OBJECT; 
+      break;
+    }
+    case 0x08: //AMF_MIXEDARRAY
+    {
+      //int nMaxIndex = RTMP_LIB::CRTMP::ReadInt32(pBuffer+1); // can be zero for unlimited
+      nSize -= 4;
+
+      // next comes the rest, mixed array has a final 0x000009 mark and names, so its an object
+      int nRes = m_objVal.Decode(pBuffer+5, nSize, true);
+      if (nRes == -1)
+       return -1;
+      nSize -= nRes;
+      m_type = AMF_OBJECT; 
+      break;
+    }
+    case 0x05: /* AMF_NULL */
+    case 0x06: /* AMF_UNDEFINED */
+    case 0x0D: /* AMF_UNSUPPORTED */
+        m_type = AMF_NULL;
+       break;
+    case 0x0B: // AMF_DATE
+    {
+      if (nSize < 10)
+              return -1;
+
+      m_dNumVal = RTMP_LIB::CRTMP::ReadNumber(pBuffer+1);
+      m_nUTCOffset = RTMP_LIB::CRTMP::ReadInt16(pBuffer+9);
+
+      m_type = AMF_DATE;
+      nSize -= 10;
+      break;
+    }
+    default:
+      Log(LOGDEBUG,"%s - unknown datatype 0x%02x, @0x%08X", __FUNCTION__, (unsigned char)(*pBuffer), pBuffer);
+      return -1;
+  }
+
+  return nOriginalSize - nSize;
+}
+
+void RTMP_LIB::AMFObjectProperty::Dump() const
+{
+  if (m_type == AMF_INVALID)
+  {
+    Log(LOGDEBUG,"Property: INVALID");
+    return;
+  }
+
+  if (m_type == AMF_NULL)
+  {
+    Log(LOGDEBUG,"Property: NULL");
+    return;
+  }
+
+  if (m_type == AMF_OBJECT)
+  {
+    Log(LOGDEBUG,"Property: <Name: %25s, OBJECT>", m_strName.empty() ? "no-name." : m_strName.c_str());
+    m_objVal.Dump();
+    return;
+  }
+
+  char strRes[256]="";
+  snprintf(strRes, 255, "Name: %25s, ", m_strName.empty()? "no-name.":m_strName.c_str());
+
+  char str[256]="";
+  switch(m_type)
+  {
+    case AMF_NUMBER:
+      snprintf(str, 255, "NUMBER:\t%.2f", m_dNumVal);
+      break;
+    case AMF_BOOLEAN:
+      snprintf(str, 255, "BOOLEAN:\t%s", m_dNumVal == 1.?"TRUE":"FALSE");
+      break;
+    case AMF_STRING:
+      snprintf(str, 255, "STRING:\t%s", m_strVal.c_str());
+      break;
+    case AMF_DATE:
+      snprintf(str, 255, "DATE:\ttimestamp: %.2f, UTC offset: %d", m_dNumVal, m_nUTCOffset);
+      break;
+    default:
+      snprintf(str, 255, "INVALID TYPE 0x%02x", (unsigned char)m_type);
+  }
+
+  Log(LOGDEBUG,"Property: <%s%s>", strRes, str);
+}
+
+void RTMP_LIB::AMFObjectProperty::Reset()
+{
+  m_dNumVal = 0.;
+  m_strVal.clear();
+  m_objVal.Reset();
+  m_type = AMF_INVALID;
+}
+
+int RTMP_LIB::AMFObjectProperty::EncodeName(char *pBuffer) const
+{
+  short length = htons(m_strName.size());
+  memcpy(pBuffer, &length, sizeof(short));
+  pBuffer += sizeof(short);
+
+  memcpy(pBuffer, m_strName.c_str(), m_strName.size());
+  return m_strName.size() + sizeof(short);
+}
+
+
+// AMFObject
+
+RTMP_LIB::AMFObject::AMFObject()
+{
+  Reset();
+}
+
+RTMP_LIB::AMFObject::~ AMFObject()
+{
+  Reset();
+}
+
+int RTMP_LIB::AMFObject::Encode(char * pBuffer, int nSize) const
+{
+  if (nSize < 4)
+    return -1;
+
+  *pBuffer = 0x03; // object
+
+  int nOriginalSize = nSize;
+  for (size_t i=0; i<m_properties.size(); i++)
+  {
+    int nRes = m_properties[i].Encode(pBuffer, nSize);
+    if (nRes == -1)
+    {
+      Log(LOGERROR,"AMFObject::Encode - failed to encode property in index %d", i);
+    }
+    else
+    {
+      nSize -= nRes;
+      pBuffer += nRes;
+    }
+  }
+
+  if (nSize < 3)
+    return -1; // no room for the end marker
+
+  RTMP_LIB::CRTMP::EncodeInt24(pBuffer, 0x000009);
+  nSize -= 3;
+
+  return nOriginalSize - nSize;
+}
+
+int RTMP_LIB::AMFObject::DecodeArray(const char * pBuffer, int nSize, int nArrayLen, bool bDecodeName)
+{
+  int nOriginalSize = nSize;
+  bool bError = false;
+
+  while(nArrayLen > 0)
+  {
+    nArrayLen--;
+
+    RTMP_LIB::AMFObjectProperty prop;
+    int nRes = prop.Decode(pBuffer, nSize, bDecodeName);
+    if (nRes == -1)
+      bError = true;
+    else
+    {
+      nSize -= nRes;
+      pBuffer += nRes;
+      m_properties.push_back(prop);
+    }
+  }
+  if (bError)
+    return -1;
+
+  return nOriginalSize - nSize;
+}
+
+int RTMP_LIB::AMFObject::Decode(const char * pBuffer, int nSize, bool bDecodeName)
+{
+  int nOriginalSize = nSize;
+  bool bError = false; // if there is an error while decoding - try to at least find the end mark 0x000009
+
+  while (nSize >= 3)
+  {
+    if (RTMP_LIB::CRTMP::ReadInt24(pBuffer) == 0x00000009)
+    {
+      nSize -= 3;
+      bError = false;
+      break;
+    }
+
+    if (bError)
+    {
+      Log(LOGDEBUG,"DECODING ERROR, IGNORING BYTES UNTIL NEXT KNOWN PATTERN!");
+      nSize--;
+      pBuffer++;
+      continue;
+    }
+
+    RTMP_LIB::AMFObjectProperty prop;
+    int nRes = prop.Decode(pBuffer, nSize, bDecodeName);
+    if (nRes == -1)
+      bError = true;
+    else
+    {
+      nSize -= nRes;
+      pBuffer += nRes;
+      m_properties.push_back(prop);
+    }
+  }
+
+  if (bError)
+    return -1;
+
+  return nOriginalSize - nSize;
+}
+
+void RTMP_LIB::AMFObject::AddProperty(const AMFObjectProperty & prop)
+{
+  m_properties.push_back(prop);
+}
+
+int RTMP_LIB::AMFObject::GetPropertyCount() const
+{
+  return m_properties.size();
+}
+
+const RTMP_LIB::AMFObjectProperty & RTMP_LIB::AMFObject::GetProperty(const std::string & strName) const
+{
+  for (size_t n=0; n<m_properties.size(); n++)
+  {
+    if (m_properties[n].GetPropName() == strName)
+      return m_properties[n];
+  }
+
+  return m_invalidProp;
+}
+
+const RTMP_LIB::AMFObjectProperty & RTMP_LIB::AMFObject::GetProperty(size_t nIndex) const
+{
+  if (nIndex >= m_properties.size())
+    return m_invalidProp;
+
+  return m_properties[nIndex];
+}
+
+void RTMP_LIB::AMFObject::Dump() const
+{
+  //Log(LOGDEBUG,"START AMF Object Dump:");
+  
+  for (size_t n=0; n<m_properties.size(); n++) {
+    m_properties[n].Dump();
+  }
+
+  //Log(LOGDEBUG,"END AMF Object Dump");
+}
+
+void RTMP_LIB::AMFObject::Reset()
+{
+  m_properties.clear();
+}
+
diff --git a/AMFObject.h b/AMFObject.h
new file mode 100644 (file)
index 0000000..e954701
--- /dev/null
@@ -0,0 +1,97 @@
+#ifndef __AMF_OBJECT__H__
+#define __AMF_OBJECT__H__
+/*
+ *      Copyright (C) 2005-2008 Team XBMC
+ *      http://www.xbmc.org
+ *     Copyright (C) 2008-2009 Andrej Stepanchuk
+ *
+ *  This Program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2, or (at your option)
+ *  any later version.
+ *
+ *  This Program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with RTMPDump; see the file COPYING.  If not, write to
+ *  the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
+ *  http://www.gnu.org/copyleft/gpl.html
+ *
+ */
+
+#include <string>
+#include <vector>
+
+namespace RTMP_LIB
+{
+  typedef enum {AMF_INVALID, AMF_NUMBER, AMF_BOOLEAN, AMF_STRING, AMF_OBJECT, AMF_NULL, AMF_MIXEDARRAY, AMF_ARRAY, AMF_DATE } AMFDataType;
+
+  class AMFObjectProperty;
+  class AMFObject
+  {
+    public:
+      AMFObject();
+      virtual ~AMFObject();
+
+      int Encode(char *pBuffer, int nSize) const;
+      int Decode(const char *pBuffer, int nSize, bool bDecodeName=false);
+      int DecodeArray(const char * pBuffer, int nSize, int nArrayLen, bool bDecodeName=false);
+
+      void AddProperty(const AMFObjectProperty &prop);
+
+      int GetPropertyCount() const;
+      const AMFObjectProperty &GetProperty(const std::string &strName) const;
+      const AMFObjectProperty &GetProperty(size_t nIndex) const;
+
+      void Dump() const;
+      void Reset();
+    protected:
+      static AMFObjectProperty m_invalidProp; // returned when no prop matches
+      std::vector<AMFObjectProperty> m_properties;
+  };
+
+  class AMFObjectProperty
+  {
+    public:
+      AMFObjectProperty();
+      AMFObjectProperty(const std::string &strName, double dValue);
+      AMFObjectProperty(const std::string &strName, bool   bValue);
+      AMFObjectProperty(const std::string &strName, const std::string &strValue);
+      AMFObjectProperty(const std::string &strName, const AMFObject &objValue);
+     
+      virtual ~AMFObjectProperty();
+
+      const std::string &GetPropName() const;
+
+      AMFDataType GetType() const;
+
+      bool IsValid() const;
+
+      double GetNumber() const;
+      bool   GetBoolean() const;
+      const std::string &GetString() const;
+      const AMFObject &GetObject() const;
+
+      int Encode(char *pBuffer, int nSize) const;
+      int Decode(const char *pBuffer, int nSize, bool bDecodeName);
+
+      void Reset();
+      void Dump() const;
+    protected:
+      int EncodeName(char *pBuffer) const;
+
+      std::string m_strName;
+
+      AMFDataType m_type;
+      double      m_dNumVal;
+      int16_t    m_nUTCOffset;
+      AMFObject   m_objVal;
+      std::string m_strVal;
+  };
+
+};
+
+#endif
diff --git a/COPYING b/COPYING
new file mode 100644 (file)
index 0000000..d511905
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,339 @@
+                   GNU GENERAL PUBLIC LICENSE
+                      Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                           Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                   GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                           NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+                    END OF TERMS AND CONDITIONS
+
+           How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License along
+    with this program; if not, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/ChangeLog b/ChangeLog
new file mode 100644 (file)
index 0000000..89f2897
--- /dev/null
+++ b/ChangeLog
@@ -0,0 +1,69 @@
+RTMPDump
+Copyright 2008-2009 Andrej Stepanchuk; Distributed under the GPL v2
+
+04 Feb 2009, v1.3d
+
+- fixed missing terminating character in url parsing
+
+19 Jan 2009, v1.3b
+
+- fixed segfault on Mac OS/BSDdue to times(0)
+
+- Makefile rewritten
+
+16 Jan 2009, v1.3a
+
+- fixed a bug introduced in v1.3 (wrong report bytes count), downloads won't
+hang anymore
+
+10 Jan 2009, v1.3
+
+- fixed audio only streams (rtmpdump now recognizes the stream and writes a
+correct tag, audio, video, audio+video)
+
+- improved resume function to wait till a the seek is executed by the server.
+The server might send playback data before seeking, so we ignore up to e.g. 50
+frames and keep waiting for a keyframe with a timestamp of zero.
+
+- nevertheless resuming does not always work since the server sometimes
+doesn't resend the keyframe, seeking in flash is unreliable
+
+02 Jan 2009, v1.2a
+
+- fixed non-standard rtmp urls (including characters + < > ; )
+
+- added small script get_hulu which can download hulu.com streams (US only)
+(many thanks to Richard Ablewhite for the help with hulu.com)
+
+01 Jan 2009, v1.2:
+
+- fixed FLV streams (support for resuming extended)
+
+- fixed hanging download at the end
+
+- several minor bugfixes
+
+- changed parameter behaviour: not supplied parameters are omitted from the
+connect packet, --auth is introduced (was automatically obtained from url
+before, but it is possible to have an auth in the tcurl/rtmp url only without
+an additional encoded string in the connect packet)
+
+28 Dec 2008, v1.1a:
+
+- fixed warnings, added -Wall to Makefile
+
+28 Dec 2008, v1.1:
+
+- fixed stucking downloads (the buffer time is set to the duration now,
+  so the server doesn't wait till the buffer is emptied
+
+ - added a --resume option to coninue incomplete downloads
+
+- added support for AMF_DATE (experimental, no stream to test so far)
+
+- fixed AMF parsing and several small bugs (works on 64bit platforms now)
+
+24 Dec 2008, v1.0:
+
+- First release
+
diff --git a/Makefile b/Makefile
new file mode 100644 (file)
index 0000000..d218c21
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,18 @@
+CFLAGS=-Wall
+CXXFLAGS=-Wall
+LDFLAGS=-Wall
+
+all: rtmpdump
+
+clean:
+       rm -f rtmpdump *.o
+
+rtmpdump: log.o rtmp.o AMFObject.o rtmppacket.o rtmpdump.o
+       g++ $(LDFLAGS) $^ -o $@ -lboost_regex
+
+log.o: log.cpp log.h Makefile
+rtmp.o: rtmp.cpp rtmp.h log.h AMFObject.h Makefile
+AMFObject.o: AMFObject.cpp AMFObject.h log.h rtmp.h Makefile
+rtmppacket.o: rtmppacket.cpp rtmppacket.h log.h Makefile
+rtmpdump.o: rtmpdump.cpp rtmp.h log.h AMFObject.h Makefile
+
diff --git a/README b/README
new file mode 100644 (file)
index 0000000..488d823
--- /dev/null
+++ b/README
@@ -0,0 +1,21 @@
+RTMP Dumper v1.3d
+(C) 2009 Andrej Stepanchuk
+License: GPLv2
+
+To compile you need the boost libraries, just type
+
+  $ make
+
+To download an rtmp stream use
+
+  $ ./get_iplayer --get 1 --vmode flashhigh --rtmp --rtmpdump "./rtmpdump"
+
+Or for hulu.com (US only)
+
+  $ ./get_hulu url quality
+
+The url is just a programme url like "http://www.hulu.com/watch/48466/30-rock-christmas-special"
+and quality is a 0, 1 or 2 to select one of the three available streams, use 1 for high quality rtmp.
+
+Credit goes to team boxee for the XBMC RTMP code used in RTMPDumper.
+
diff --git a/get_hulu b/get_hulu
new file mode 100644 (file)
index 0000000..66a6ead
--- /dev/null
+++ b/get_hulu
@@ -0,0 +1,165 @@
+#!/usr/bin/perl
+# (C) 2009 Andrej Stepanchuk
+# License: GPLv3
+my $version = 1.08;
+
+use Env qw[@PATH];
+use Fcntl;
+use File::Copy;
+use File::Path;
+use File::stat;
+use Getopt::Long;
+use HTML::Entities;
+use HTTP::Cookies;
+use HTTP::Headers;
+use IO::Seekable;
+use IO::Socket;
+use LWP::ConnCache;
+#use LWP::Debug qw(+);
+use LWP::UserAgent;
+use POSIX qw(mkfifo);
+use strict;
+#use warnings;
+use Time::Local;
+use URI;
+
+use XML::Simple;
+use Data::Dumper;
+
+my $progurl = shift;
+my $quality = shift;
+
+if(not defined($quality) or $progurl eq "" or not ($quality==0 or $quality==1 or $quality==2)) {
+       print "Error: please pass a content url and a quality (0,1,2)\n";
+       exit 1;
+}
+
+my $ua = LWP::UserAgent->new;
+
+my $h = new HTTP::Headers(
+                        #'User-Agent'    => $user_agent{coremedia},
+                        'Accept'        => '*/*',
+                        'Range'         => 'bytes=0-',
+                );
+
+my $req = HTTP::Request->new ('GET', $progurl, $h);
+my $res = $ua->request($req);
+my $content = $res->content;
+
+#print "Content: $content\n\n";
+
+my $cid;
+if($content =~ /.*UserHistory.add_watched_history\((\d+)\).*/) {
+       $cid = $1;
+       print "Found CID: $cid\n";
+}
+
+unless(defined($cid)) {
+       print "Couldn't get CID!\n";
+       exit 1;
+}
+
+my $sidurl = "http://r.hulu.com/videos?content_id=$cid";
+
+my $req = HTTP::Request->new ('GET', $sidurl, $h);
+my $res = $ua->request($req);
+my $content = $res->content;
+
+#print "Content: $content\n\n";
+
+my $xml = new XML::Simple;
+
+# read XML file
+my $data = $xml->XMLin($content);
+print Dumper($data);
+my $pid = $data->{video}->{pid};
+
+unless(defined($pid)) {
+       printf "Couldn't get PID!\n";
+       exit 1;
+}
+
+print "Found PID: $pid\n";
+
+my $xmlfile = "http://releasegeo.hulu.com/content.select?pid=$pid&mbr=true&format=smil";
+
+my $req = HTTP::Request->new ('GET', $xmlfile, $h);
+my $res = $ua->request($req);
+my $content = $res->content;
+
+my $xml = new XML::Simple;
+
+# read XML file
+my $data = $xml->XMLin($content);
+
+# access XML data
+#print "$data->{name} is $data->{age} years old and works in the $data->{department} section\n";
+print Dumper($data->{body}->{switch});
+
+#--app "ondemand?_fcs_vhost=${server_hostname}&auth=${auth}&aifp=NS20070910&slist=${playpath}"
+#--flashVer "WIN 10,0,1.2,36"
+#--swfUrl "http://www.hulu.com/player.swf"
+#--tcUrl "rtmp://${serverip}:1935/${app}"
+#--pageUrl "http://www.hulu.com/watch/2711/family-guy-padre-de-familia"
+
+#--app "ondemand?_fcs_vhost=${server_hostname}&auth=${auth}&aifp=sll02152008&slist=${playpath};.international=false"
+#--tcUrl "rtmp://${serverip}:1935/${app}"
+
+my $rtmpurl = $data->{body}->{switch}[1]->{video}[$quality]->{src};
+
+unless(defined($rtmpurl)) {
+       print "Couldn't get RTMP url!\n";
+       exit 1;
+}
+
+print "\nRTMP URL: $rtmpurl\n\n";
+if($rtmpurl =~ /^(.*)<break>.*$/) {
+       $rtmpurl= $1;
+}
+# don't strip off the .international=false, it is actually sent!
+#if($rtmpurl =~ /^(.*)\;\.international.*$/) {
+#        $rtmpurl= $1;
+#}
+
+#print "clean RTMP URL: $rtmpurl\n";
+
+my $servername;
+my $parameters;
+my $playpath;
+if($rtmpurl =~ /^rtmp:\/\/(.*)\/.*ondemand.*(auth.*)$/) {
+       #print "1: $1\n2: $2\n3: $3";
+       $servername = $1;
+       $parameters = $2;
+}
+
+print "Host: $servername\n";
+my $ip = inet_ntoa(inet_aton($servername));
+print "IP: $ip\n";
+
+# get playpath if necessary
+my $playpath;
+if($rtmpurl =~ /^rtmp:\/\/.*ondemand\/(.*)\?.*$/) {
+       $playpath = $1;
+}
+if($playpath) {
+       $rtmpurl    = "rtmp://$servername:1935/ondemand?_fcs_vhost=$servername&slist=$playpath";        
+} else {
+       $rtmpurl    = "rtmp://$servername:1935/ondemand?_fcs_vhost=$servername&$parameters";
+       
+       # we have to filter out the .international from the RTMP url since otherwise rtmpdump will parse slist="*.international=flse"
+       # and the play path must not include that string!
+       if($rtmpurl =~ /^(.*)\;\.international.*$/) {
+               $rtmpurl= $1;
+       }
+}
+my $app     = "ondemand?_fcs_vhost=$servername&$parameters";
+my $tcurl   = "rtmp://$ip:1935/ondemand?_fcs_vhost=$servername&$parameters";
+
+#print "RTMP: $rtmpurl\n";
+
+
+my $cmd = "./rtmpdump --rtmp \"$rtmpurl\" --pageUrl \"$progurl\" --flashVer \"WIN 10,0,1.2,36\" --swfUrl \"http://www.hulu.com/player.swf\" --tcUrl \"$tcurl\" --app \"$app\" -o test.flv";
+print "\nCOMMAND1: $cmd\n";
+system($cmd);
+exit 0;
+
diff --git a/get_iplayer b/get_iplayer
new file mode 100644 (file)
index 0000000..ea45e0d
--- /dev/null
@@ -0,0 +1,5017 @@
+#!/usr/bin/perl
+#
+# get_iplayer
+#
+# Lists and downloads BBC iPlayer audio and video streams
+# + Downloads ITVplayer Catch-Up video streams
+#
+# Author: Phil Lewis
+# Email: iplayer (at sign) linuxcentre.net
+# Web: http://linuxcentre.net/iplayer
+# License: GPLv3 (see LICENSE.txt)
+#
+# Other credits:
+#  RTMP additions: Andrej Stepanchuk
+#
+my $version = 1.19;
+#
+# Help:
+#      ./get_iplayer --help
+#
+# Changelog:
+#      http://linuxcentre.net/get_iplayer/CHANGELOG.txt
+#
+# Example Usage and Documentation:
+#      http://linuxcentre.net/getiplayer/documentation
+#
+# Todo:
+# * Use non-shell tee?
+# * Fix non-uk detection - iphone auth?
+# * Index/Download live radio streams w/schedule feeds to assist timing
+# * Podcasts for 'local' stations are missing (only a handful). They use a number of different station ids which will involve reading html to determine rss feed. 
+# * Remove all rtsp/mplayer/lame/tee dross when realaudio streams become obselete (not quite yet)
+# * Stdout mode with rtmp
+# * Do subtitle downloading after programme download so that rtmp auth doesn't timeout
+# * Playlist creation for multipart itv downloads
+
+# Known Issues:
+# * In ActivePerl/windows downloaded iPhone video files do not get renamed (remain with .partial.mov)
+# * vlc does not quit after downloading an rtsp N95 video stream (ctrl-c is required) - need a --play-and-quit option if such a thing exists
+# * rtmpdump (v1.2) of flashaudio fails at end of stream => non-zero exit code
+# * Some rtmpdump downloads always give a non-zero exit code regardless of success (using a min-filesize workaround for now)
+# * resuming a flashaudio download fails
+# * rtmpdump-v1.3 doesn't work correctly for flashhigh mode - keeps hanging on download
+use Env qw[@PATH];
+use Fcntl;
+use File::Copy;
+use File::Path;
+use File::stat;
+use Getopt::Long;
+use HTML::Entities;
+use HTTP::Cookies;
+use HTTP::Headers;
+use IO::Seekable;
+use IO::Socket;
+use LWP::ConnCache;
+#use LWP::Debug qw(+);
+use LWP::UserAgent;
+use POSIX qw(mkfifo);
+use strict;
+#use warnings;
+use Time::Local;
+use URI;
+
+$|=1;
+my %opt = ();
+my %opt_cmdline = (); # a hash of which options came from the cmdline rather than the options files
+my %opt_file = (); # a hash of which options came from the options files rather than the cmdline
+
+# Print to STDERR/STDOUT if not quiet unless verbose or debug
+sub logger(@) {
+       # Make sure quiet can be overridden by verbose and debug options
+       if ( $opt{verbose} || $opt{debug} || ! $opt{quiet} ) {
+               # Only send messages to STDERR if pvr or stdout options are being used.
+               if ( $opt{stdout} || $opt{pvr} || $opt{stderr} ) {
+                       print STDERR $_[0];
+               } else {
+                       print STDOUT $_[0];
+               }
+       }
+}
+
+sub usage {
+       logger <<EOF;
+get_iplayer v$version, Usage ( Also see http://linuxcentre.net/iplayer ):
+Search Programmes:  get_iplayer [<search options>] [<regex|index|pid|pidurl> ...]
+Download files:     get_iplayer --get [<search options>] <regex|index|pid|pidurl> ...
+                    get_iplayer --pid <pid|pidurl> [<options>]
+Stream Downloads:   get_iplayer --stdout [<options>] <regex|index|pid|pidurl> | mplayer -cache 3072 -
+Update get_iplayer: get_iplayer --update
+
+Search Options:
+ <regex|index|pid|url>         Search programme names based on given pattern
+ -l, --long                    Additionally search in long programme descriptions / episode names
+ --channel <regex>             Narrow search to matched channel(s)
+ --category <regex>            Narrow search to matched categories
+ --versions <regex>            Narrow search to matched programme version(s)
+ --exclude <regex>             Narrow search to exclude matched programme names
+ --exclude-channel <regex>     Narrow search to exclude matched channel(s)
+ --exclude-category <regex>    Narrow search to exclude matched catogories
+ --type <type>                 Only search in these types of programmes: radio, tv, podcast, all, itv (tv is default)
+ --since <hours>               Limit search to programmes added to the cache in the last N hours
+Display Options:
+ -l, --long                    Display long programme descriptions / episode names and other data
+ --terse                       Only show terse programme info (does not affect searching)
+ --tree                        Display Programme listings in a tree view
+ -i, --info                    Show full programme metadata (only if number of matches < 50)
+ --list <categories|channel>   Show a list of available categories/channels for the selected type and exit
+ --hide                        Hide previously downloaded programmes
+ --streaminfo                  Returns all of the media stream urls of the programme(s)
+
+Download Options:
+ -g, --get                     Download matching programmes
+ -x, --stdout                  Additionally stream to STDOUT (so you can pipe output to a player)
+ -p, --proxy <url>             Web proxy URL spec
+ --partial-proxy               Works around for some broken web proxies (try this extra option if your proxy fails)
+ --pid <pid|url>               Download an arbitrary pid that does not appear in the index (itv:<pid> for itv programmes)
+ --force-download              Ignore download history (unsets --hide option also)
+ --amode <mode>,<mode>,...     Audio Download mode(s): iphone,flashaudio,realaudio (default: iphone,flashaudio,realaudio)
+ --vmode <mode>,<mode>,...     Video Download mode(s): iphone,rtmp,flashhigh,flashnormal,flashwii,n95_wifi (default: iphone,flashhigh,flashnormal)
+ --wav                         In radio realaudio mode output as wav and don't transcode to mp3
+ --raw                         Don't transcode or change the downloaded stream in any way (i.e. radio/realaudio, rtmp/flv, iphone/mov)
+ --bandwidth                   In radio realaudio mode specify the link bandwidth in bps for rtsp streaming (default 512000)
+ --subtitles                   In TV mode, download subtitles into srt/SubRip format if available
+ --suboffset <offset>          Offset the subtitle timestamps by the specified number of milliseconds
+ --version-list <versions>     Override the version of programme to download (e.g. '--version-list signed,default')
+ -t, --test                    Test only - no download (will show programme type)
+
+PVR Options:
+ --pvr                         Runs the PVR download using all saved PVR searches (intended to be run every hour from cron etc)
+ --pvradd <search name>        Add the current search terms to the named PVR search
+ --pvrdel <search name>        Remove the named search from the PVR searches
+ --pvr-enable <search name>    Enable a previously disabled named PVR search
+ --pvr-disable <search name>   Disable (not delete) a named PVR search
+ --pvrlist                     Show the PVR search list
+
+Output Options:
+ -o, --output <dir>            Default Download output directory for all downloads
+ --outputradio <dir>           Download output directory for radio
+ --outputtv <dir>              Download output directory for tv
+ --outputpodcast <dir>         Download output directory for podcasts
+ --file-prefix <format>        The filename prefix (excluding dir and extension) using formatting fields. e.g. '<name>-<episode>-<pid>'
+ -s, --subdir                  Downloaded files into Programme name subdirectory
+ -n, --nowrite                 No writing of file to disk (use with -x to prevent a copy being stored on disk)
+ -w, --whitespace              Keep whitespace (and escape chars) in filenames
+ -q, --quiet                   No logging output
+ -c, --command <command>       Run user command after successful download using args such as <pid>, <name> etc
+
+Config Options:
+ -f, --flush, --refresh        Refresh cache
+ -e, --expiry <secs>           Cache expiry in seconds (default 4hrs)
+ --symlink <file>              Create symlink to <file> once we have the header of the download
+ --fxd <file>                  Create Freevo FXD XML of matching programmes in specified file
+ --mythtv <file>               Create Mythtv streams XML of matching programmes in specified file
+ --xml-channels                Create freevo/Mythtv menu of channels -> programme names -> episodes
+ --xml-names                   Create freevo/Mythtv menu of programme names -> episodes
+ --xml-alpha                   Create freevo/Mythtv menu sorted alphabetically by programme name
+ --html <file>                 Create basic HTML index of matching programmes in specified file
+ --mplayer <path>              Location of mplayer binary
+ --ffmpeg <path>               Location of ffmpeg binary
+ --lame <path>                 Location of lame binary
+ --id3v2 <path>                Location of id3v2 binary
+ --rtmpdump <path>             Location of rtmpdump binary
+ --vlc <path>                  Location of vlc or cvlc binary
+ -v, --verbose                 Verbose
+ -u, --update                  Update get_iplayer if a newer one exists
+ -h, --help                    Help
+ --save                        Save specified options as default in .get_iplayer/config
+EOF
+       exit 1;
+}
+
+# Get cmdline params
+my $save;
+# This is where all profile data/caches/cookies etc goes
+my $profile_dir;
+# This is where system-wide default options are specified
+my $optfile_system;
+# Options on unix-like systems
+if ( defined $ENV{HOME} ) {
+       $profile_dir = $ENV{HOME}.'/.get_iplayer';
+       $optfile_system = '/etc/get_iplayer/options';
+
+# Otherwise look for windows style file locations
+} elsif ( defined $ENV{USERPROFILE} ) {
+       $profile_dir = $ENV{USERPROFILE}.'/.get_iplayer';
+       $optfile_system = $ENV{ALLUSERSPROFILE}.'/get_iplayer/options';
+}
+# Make profile dir if it doesnt exist
+mkpath $profile_dir if ! -d $profile_dir;
+# Personal options go here
+my $optfile = "${profile_dir}/options";
+# PVR Lockfile location
+my $lockfile;
+# Parse options if we're not saving options (system-wide options are overridden by personal options)
+if ( ! grep /\-\-save/, @ARGV ) {
+       $opt{debug} = 1 if grep /\-\-debug/, @ARGV;
+       read_options_file($optfile_system);
+       read_options_file($optfile);
+}
+
+# Allow bundling of single char options
+Getopt::Long::Configure ("bundling");
+# cmdline opts take precedence
+GetOptions(
+       "amode=s"                       => \$opt_cmdline{amode},
+       "bandwidth=n"                   => \$opt_cmdline{bandwidth},
+       "category=s"                    => \$opt_cmdline{category},
+       "channel=s"                     => \$opt_cmdline{channel},
+       "c|command=s"                   => \$opt_cmdline{command},
+       "debug"                         => \$opt_cmdline{debug},
+       "exclude=s"                     => \$opt_cmdline{exclude},
+       "exclude-category=s"            => \$opt_cmdline{excludecategory},
+       "exclude-channel=s"             => \$opt_cmdline{excludechannel},
+       "expiry|e=n"                    => \$opt_cmdline{expiry},
+       "ffmpeg=s"                      => \$opt_cmdline{ffmpeg},
+       "file-prefix|fileprefix=s"      => \$opt_cmdline{fileprefix},
+       "flush|refresh|f"               => \$opt_cmdline{flush},
+       "force-download"                => \$opt_cmdline{forcedownload},
+       "fxd=s"                         => \$opt_cmdline{fxd},
+       "get|g"                         => \$opt_cmdline{get},
+       "help|h"                        => \$opt_cmdline{help},
+       "hide"                          => \$opt_cmdline{hide},
+       "html=s"                        => \$opt_cmdline{html},
+       "id3v2=s"                       => \$opt_cmdline{id3v2},
+       "i|info"                        => \$opt_cmdline{info},
+       "lame=s"                        => \$opt_cmdline{lame},
+       "list=s"                        => \$opt_cmdline{list},
+       "long|l"                        => \$opt_cmdline{long},
+       "mp3audio"                      => \$opt_cmdline{mp3audio},
+       "mplayer=s"                     => \$opt_cmdline{mplayer},
+       "mythtv=s"                      => \$opt_cmdline{mythtv},
+       "n95"                           => \$opt_cmdline{n95},
+       "no-write|nowrite|n"            => \$opt_cmdline{nowrite},
+       "output|o=s"                    => \$opt_cmdline{output},
+       "outputpodcast=s"               => \$opt_cmdline{outputpodcast},
+       "outputradio=s"                 => \$opt_cmdline{outputradio},
+       "outputtv=s"                    => \$opt_cmdline{outputtv},
+       "partial-proxy"                 => \$opt_cmdline{partialproxy},
+       "pid=s"                         => \$opt_cmdline{pid},
+       "proxy|p=s"                     => \$opt_cmdline{proxy},
+       "pvr"                           => \$opt_cmdline{pvr},
+       "pvradd|pvr-add=s"              => \$opt_cmdline{pvradd},
+       "pvrdel|pvr-del=s"              => \$opt_cmdline{pvrdel},
+       "pvrdisable|pvr-disable=s"      => \$opt_cmdline{pvrdisable},
+       "pvrenable|pvr-enable=s"        => \$opt_cmdline{pvrenable},
+       "pvrlist|pvr-list"              => \$opt_cmdline{pvrlist},
+       "q|quiet"                       => \$opt_cmdline{quiet},
+       "raw"                           => \$opt_cmdline{raw},
+       "realaudio"                     => \$opt_cmdline{realaudio},
+       "rtmp"                          => \$opt_cmdline{rtmp},
+       "rtmpdump=s"                    => \$opt_cmdline{rtmpdump},
+       "save"                          => \$save,
+       "since=n"                       => \$opt_cmdline{since},
+       "stdout|x"                      => \$opt_cmdline{stdout},
+       "streaminfo"                    => \$opt_cmdline{streaminfo},
+       "subdirs|subdir|s"              => \$opt_cmdline{subdir},
+       "suboffset=n"                   => \$opt_cmdline{suboffset},
+       "subtitles"                     => \$opt_cmdline{subtitles},
+       "symlink|freevo=s"              => \$opt_cmdline{symlink},
+       "test|t"                        => \$opt_cmdline{test},
+       "terse"                         => \$opt_cmdline{terse},
+       "tree"                          => \$opt_cmdline{tree},
+       "type=s"                        => \$opt_cmdline{type},
+       "update|u"                      => \$opt_cmdline{update},
+       "versionlist|version-list=s"    => \$opt_cmdline{versionlist},
+       "versions=s"                    => \$opt_cmdline{versions},
+       "verbose|v"                     => \$opt_cmdline{verbose},
+       "vlc=s"                         => \$opt_cmdline{vlc},
+       "vmode=s"                       => \$opt_cmdline{vmode},
+       "wav"                           => \$opt_cmdline{wav},
+       "whitespace|ws|w"               => \$opt_cmdline{whitespace},
+       "xml-channels|fxd-channels"     => \$opt_cmdline{xmlchannels},
+       "xml-names|fxd-names"           => \$opt_cmdline{xmlnames},
+       "xml-alpha|fxd-alpha"           => \$opt_cmdline{xmlalpha},
+) || die usage();
+usage() if $opt_cmdline{help};
+
+# Merge cmdline options into %opt
+for ( keys %opt_cmdline ) {
+       $opt{$_} = $opt_cmdline{$_} if defined $opt_cmdline{$_};
+}
+# Save opts if specified
+save_options_file( $optfile ) if $save;
+
+
+### Global vars ###
+
+# Programme data structure
+# $prog{$pid} = {
+#      'index'         => <index number>,
+#      'name'          => <programme short name>,
+#      'episode'       => <Episode info>,
+#      'desc'          => <Long Description>,
+#      'available'     => <Date/Time made available or remaining>,
+#      'duration'      => <duration in HH:MM:SS>
+#      'versions'      => <comma separated list of versions, e.g default, signed>
+#      'thumbnail'     => <programme thumbnail url>
+#      'channel        => <channel>
+#      'categories'    => <Comma separated list of categories>
+#      'type'          => <Type: tv, radio, itv or podcast>
+#      'timeadded'     => <timestamp when programme was added to cache>
+#      'longname'      => <Long name (only parsed in stage 1 download)>,
+#      'version'       => <selected version e.g default, signed, etc - only set before d/load>
+#      'filename'      => <Path and Filename of saved file - set only while downloading>
+#      'dir'           => <Filename Directory of saved file - set only while downloading>
+#      'fileprefix'    => <Filename Prefix of saved file - set only while downloading>
+#      'ext'           => <Filename Extension of saved file - set only while downloading>
+#};
+
+# Define cache file format
+my @cache_format = qw/index type name pid available episode versions duration desc channel categories thumbnail timeadded guidance web/;
+
+# List of all types
+my @all_prog_types = qw/ tv radio podcast itv /;
+
+# Ranges of numbers used in the indicies for each programme type
+my %index_range;
+$index_range{tv}{min}          = 1;
+$index_range{tv}{max}          = 9999;
+$index_range{radio}{min}       = 10001;
+$index_range{radio}{max}       = 19999;
+$index_range{podcast}{min}     = 20001;
+$index_range{podcast}{max}     = 29999;
+$index_range{itv}{min}                 = 100001;
+$index_range{itv}{max}                 = 199999;
+# Set maximun index number
+my $max_index;
+for (@all_prog_types) {
+       $max_index = $index_range{$_}{max} if $index_range{$_}{max} > $max_index;
+}
+my %prog;
+my %type;
+my %pids_history;
+my %index_pid; # Hash to obtain pid given an index
+my $now;
+my $childpid;
+my $min_download_size = 1000000;
+
+# Static URLs
+my $channel_feed_url           = 'http://feeds.bbc.co.uk/iplayer'; # /$channel/list/limit/400
+my $prog_feed_url              = 'http://feeds.bbc.co.uk/iplayer/episode/'; # $pid
+my $prog_iplayer_metadata      = 'http://www.bbc.co.uk/iplayer/playlist/'; # $pid
+my $media_stream_data_prefix   = 'http://www.bbc.co.uk/mediaselector/4/mtis/stream/'; # $verpid
+my $iphone_download_prefix     = 'http://www.bbc.co.uk/mediaselector/3/auth/iplayer_streaming_http_mp4';
+my $bbc_prog_page_prefix       = 'http://www.bbc.co.uk/programmes'; # /$pid
+my $itv_catchup_page_prefix    = 'http://www.itv.com/CatchUp/Video/default.html?ViewType=5&amp;Filter='; # $pid
+my $thumbnail_prefix           = 'http://www.bbc.co.uk/iplayer/images/episode';
+my $metadata_xml_prefix                = 'http://www.bbc.co.uk/iplayer/metafiles/episode'; # /${pid}.xml
+my $metadata_mobile_prefix     = 'http://www.bbc.co.uk/iplayer/widget/episodedetail/episode'; # /${pid}/template/mobile/service_type/tv/
+my $podcast_index_feed_url     = 'http://downloads.bbc.co.uk/podcasts/ppg.xml';
+my $version_url                        = 'http://linuxcentre.net/get_iplayer/VERSION-get_iplayer';
+my $update_url                 = 'http://linuxcentre.net/get_iplayer/get_iplayer'; # disabled since this is a modified version
+
+# Static hash definitions
+my %channels;
+$channels{tv} = {
+       'bbc_one'                               => 'tv|BBC One',
+       'bbc_two'                               => 'tv|BBC Two',
+       'bbc_three'                             => 'tv|BBC Three',
+       'bbc_four'                              => 'tv|BBC Four',
+       'cbbc'                                  => 'tv|CBBC',
+       'cbeebies'                              => 'tv|CBeebies',
+       'bbc_news24'                            => 'tv|BBC News 24',
+       'bbc_parliament'                        => 'tv|BBC Parliament',
+       'bbc_one_northern_ireland'              => 'tv|BBC One Northern Ireland',
+       'bbc_one_scotland'                      => 'tv|BBC One Scotland',
+       'bbc_one_wales'                         => 'tv|BBC One Wales',
+       'bbc_webonly'                           => 'tv|BBC Web Only',
+       'bbc_hd'                                => 'tv|BBC HD',
+       'bbc_alba'                              => 'tv|BBC Alba',
+       'categories/news/tv'                    => 'tv|BBC News',
+       'categories/sport/tv'                   => 'tv|BBC Sport',
+#      'categories/tv'                         => 'tv|All',
+       'categories/signed'                     => 'tv|Signed',
+};
+
+$channels{radio} = {
+       'bbc_1xtra'                             => 'radio|BBC 1Xtra',
+       'bbc_radio_one'                         => 'radio|BBC Radio 1',
+       'bbc_radio_two'                         => 'radio|BBC Radio 2',
+       'bbc_radio_three'                       => 'radio|BBC Radio 3',
+       'bbc_radio_four'                        => 'radio|BBC Radio 4',
+       'bbc_radio_five_live'                   => 'radio|BBC Radio 5 live',
+       'bbc_radio_five_live_sports_extra'      => 'radio|BBC 5 live Sports Extra',
+       'bbc_6music'                            => 'radio|BBC 6 Music',
+       'bbc_7'                                 => 'radio|BBC 7',
+       'bbc_asian_network'                     => 'radio|BBC Asian Network',
+       'bbc_radio_foyle'                       => 'radio|BBC Radio Foyle',
+       'bbc_radio_scotland'                    => 'radio|BBC Radio Scotland',
+       'bbc_radio_nan_gaidheal'                => 'radio|BBC Radio Nan Gaidheal',
+       'bbc_radio_ulster'                      => 'radio|BBC Radio Ulster',
+       'bbc_radio_wales'                       => 'radio|BBC Radio Wales',
+       'bbc_radio_cymru'                       => 'radio|BBC Radio Cymru',
+       'bbc_world_service'                     => 'radio|BBC World Service',
+#      'categories/radio'                      => 'radio|All',
+       'bbc_radio_cumbria'                     => 'radio|BBC Cumbria',
+       'bbc_radio_newcastle'                   => 'radio|BBC Newcastle',
+       'bbc_tees'                              => 'radio|BBC Tees',
+       'bbc_radio_lancashire'                  => 'radio|BBC Lancashire',
+       'bbc_radio_merseyside'                  => 'radio|BBC Merseyside',
+       'bbc_radio_manchester'                  => 'radio|BBC Manchester',
+       'bbc_radio_leeds'                       => 'radio|BBC Leeds',
+       'bbc_radio_sheffield'                   => 'radio|BBC Sheffield',
+       'bbc_radio_york'                        => 'radio|BBC York',
+       'bbc_radio_humberside'                  => 'radio|BBC Humberside',
+       'bbc_radio_lincolnshire'                => 'radio|BBC Lincolnshire',
+       'bbc_radio_nottingham'                  => 'radio|BBC Nottingham',
+       'bbc_radio_leicester'                   => 'radio|BBC Leicester',
+       'bbc_radio_derby'                       => 'radio|BBC Derby',
+       'bbc_radio_stoke'                       => 'radio|BBC Stoke',
+       'bbc_radio_shropshire'                  => 'radio|BBC Shropshire',
+       'bbc_wm'                                => 'radio|BBC WM',
+       'bbc_radio_coventry_warwickshire'       => 'radio|BBC Coventry &amp; Warwickshire',
+       'bbc_radio_hereford_worcester'          => 'radio|BBC Hereford &amp; Worcester',
+       'bbc_radio_northampton'                 => 'radio|BBC Northampton',
+       'bbc_three_counties_radio'              => 'radio|BBC Three Counties',
+       'bbc_radio_cambridge'                   => 'radio|BBC Cambridgeshire',
+       'bbc_radio_norfolk'                     => 'radio|BBC Norfolk',
+       'bbc_radio_suffolk'                     => 'radio|BBC Suffolk',
+       'bbc_radio_essex'                       => 'radio|BBC Essex',
+       'bbc_london'                            => 'radio|BBC London',
+       'bbc_radio_kent'                        => 'radio|BBC Kent',
+       'bbc_southern_counties_radio'           => 'radio|BBC Southern Counties',
+       'bbc_radio_oxford'                      => 'radio|BBC Oxford',
+       'bbc_radio_berkshire'                   => 'radio|BBC Berkshire',
+       'bbc_radio_solent'                      => 'radio|BBC Solent',
+       'bbc_radio_gloucestershire'             => 'radio|BBC Gloucestershire',
+       'bbc_radio_swindon'                     => 'radio|BBC Swindon',
+       'bbc_radio_wiltshire'                   => 'radio|BBC Wiltshire',
+       'bbc_radio_bristol'                     => 'radio|BBC Bristol',
+       'bbc_radio_somerset_sound'              => 'radio|BBC Somerset',
+       'bbc_radio_devon'                       => 'radio|BBC Devon',
+       'bbc_radio_cornwall'                    => 'radio|BBC Cornwall',
+       'bbc_radio_guernsey'                    => 'radio|BBC Guernsey',
+       'bbc_radio_jersey'                      => 'radio|BBC Jersey',
+};
+
+$channels{itv} = {
+       'crime'                                 => 'itv|TV Classics Crime Drama',
+       'perioddrama'                           => 'itv|TV Classics Period Drama',
+       'familydrama'                           => 'itv|TV Classics Family Drama',
+       'documentary'                           => 'itv|TV Classics Documentaries',
+       'comedy'                                => 'itv|TV Classics Comedy',
+       'kids'                                  => 'itv|TV Classics Children\'s TV',
+       'soaps'                                 => 'itv|TV Classics Soaps',
+       '/'                                     => 'itv|TV Classics',
+};
+
+
+# User Agents
+my %user_agent = (
+       coremedia       => 'Apple iPhone v1.1.1 CoreMedia v1.0.0.3A110a',
+       safari          => 'Mozilla/5.0 (iPhone; U; CPU like Mac OS X; en) AppleWebKit/420.1 (KHTML, like Gecko) Version/3.0 Mobile/3A110a Safari/419.3',
+       update          => "get_iplayer updater (v${version} - $^O)",
+       desktop         => 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-GB; rv:1.9) Gecko/2008052906 Firefox/3.0',
+       get_iplayer     => "get_iplayer/$version $^O",
+);
+
+# Setup signal handlers
+$SIG{INT} = $SIG{PIPE} =\&cleanup;
+
+# Other Non-option dependant vars
+my %cachefile = (
+       'itv'           => "${profile_dir}/itv.cache",
+       'tv'            => "${profile_dir}/tv.cache",
+       'radio'         => "${profile_dir}/radio.cache",
+       'podcast'       => "${profile_dir}/podcast.cache",
+);
+my $get_iplayer_stream = 'get_iplayer_freevo_wrapper'; # Location of wrapper script for streaming with mplayer/xine on freevo
+my $historyfile                = "${profile_dir}/download_history";
+my $pvr_dir            = "${profile_dir}/pvr/";
+my $cookiejar          = "${profile_dir}/cookies";
+my $namedpipe          = "${profile_dir}/namedpipe.$$";
+my $lwp_request_timeout        = 20;
+my $info_limit         = 40;
+my $iphone_block_size  = 0x2000000; # 32MB
+
+
+# Option dependant var definitions
+my %download_dir;
+my $cache_secs;
+my $mplayer;
+#my $mencoder;
+my $ffmpeg;
+my $ffmpeg_opts;
+my $rtmpdump;
+my $mplayer_opts;
+my $lame;
+my $lame_opts;
+my $vlc;
+my $vlc_opts;
+my $id3v2;
+my $tee;
+my $bandwidth;
+my @version_search_list;
+my $proxy_url;
+my @search_args = @ARGV;
+# Assume search term is '.*' if nothing is specified - i.e. lists all programmes
+push @search_args, '.*' if ! $search_args[0];
+
+
+# PVR functions
+my %pvrsearches; # $pvrsearches{searchname}{<option>} = <value>;
+if ( $opt{pvradd} ) {
+       pvr_add( $opt{pvradd}, @search_args );
+       exit 0;
+
+} elsif ( $opt{pvrdel} ) {
+       pvr_del( $opt{pvrdel} );
+       exit 0;
+
+} elsif ( $opt{pvrdisable} ) {
+       pvr_disable( $opt{pvrdisable} );
+       exit 0;
+
+} elsif ( $opt{pvrenable} ) {
+       pvr_enable( $opt{pvrenable} );
+       exit 0;
+
+} elsif ( $opt{pvrlist} ) {
+       pvr_display_list();
+       exit 0;
+
+# Load all PVR searches and run one-by-one
+} elsif ( $opt{pvr} ) {
+       # PVR Lockfile detection (with 12 hrs stale lockfile check)
+       $lockfile = "${profile_dir}/pvr_lock";
+       lockfile(43200) if ! $opt{test};
+       # Don't attempt to download pids in download history
+       %pids_history = load_download_history();
+       # Load all PVR searches
+       pvr_load_list();
+       # Display default options
+       display_default_options();
+       # For each PVR search
+       for my $name ( sort {lc $a cmp lc $b} keys %pvrsearches ) {
+               # Ignore if this search is disabled
+               if ( $pvrsearches{$name}{disable} ) {
+                       logger "\nSkipping disabled PVR Search '$name'\n" if $opt{verbose};
+                       next;
+               }
+               logger "\nRunning PVR Search '$name'\n";
+               # Clear then Load options for specified pvr search name
+               pvr_load_options($name);
+               # Switch on --hide option
+               $opt{hide} = 1;
+               # Dont allow --flush with --pvr
+               $opt{flush} = '';
+               # Do the downloads (force --get option)
+               $opt{get} = 1 if ! $opt{test};
+               process_matches( @search_args );
+       }
+
+# Else just process command line args
+} else {
+       %pids_history = load_download_history() if $opt{hide};
+       process_matches( @search_args );
+}
+exit 0;
+
+
+
+# Use the specified options to process the matches in specified array
+sub process_matches {
+       my @search_args = @_;
+       # Show options
+       display_current_options() if $opt{verbose};
+       # Clear prog hash
+       %prog = ();
+       logger "INFO: Search args: '".(join "','", @search_args)."'\n" if $opt{verbose};
+
+       # Option dependant vars
+       %download_dir   = (
+               'tv'            => $opt{outputtv} || $opt{output} || $ENV{IPLAYER_OUTDIR} || '.',
+               'itv'           => $opt{outputtv} || $opt{output} || $ENV{IPLAYER_OUTDIR} || '.',
+               'radio'         => $opt{outputradio} || $opt{output} || $ENV{IPLAYER_OUTDIR} || '.',
+               'podcast'       => $opt{outputpodcast} || $opt{output} || $ENV{IPLAYER_OUTDIR} || '.',
+       );
+
+       # Ensure lowercase
+       $opt{type}              = lc( $opt{type} );
+       # Expand 'all' to comma separated list all prog types
+       $opt{type}              = join(',', @all_prog_types) if $opt{type} =~ /(all|any)/i;
+       # Hash to store specified prog types
+       %type = ();
+       $type{$_} = 1 for split /,/, $opt{type};
+       # Default to type=tv if no type option is set
+       $type{tv}               = 1 if keys %type == 0;
+       $cache_secs             = $opt{expiry} || 14400;
+       $mplayer                = $opt{mplayer} || 'mplayer';
+       $mplayer_opts           = '-nolirc';
+       $mplayer_opts           .= ' -v' if $opt{debug};
+       $mplayer_opts           .= ' -really-quiet' if $opt{quiet};
+       $ffmpeg                 = $opt{ffmpeg} || 'ffmpeg';
+       $ffmpeg_opts            = '';
+       $lame                   = $opt{lame} || 'lame';
+       $lame_opts              = '-f';
+       $lame_opts              .= ' --quiet ' if $opt{quiet};
+       $vlc                    = $opt{vlc} || 'cvlc';
+       $vlc_opts               = '-vv' if $opt{verbose} || $opt{debug};
+       $id3v2                  = $opt{id3v2} || 'id3v2';
+       $tee                    = 'tee';
+       $rtmpdump               = $opt{rtmpdump} || 'rtmpdump';
+       $bandwidth              = $opt{bandwidth} || 512000; # Download bandwidth bps used for rtsp streams
+       # Order with which to search for programme versions (can be overridden by --versionlist option)
+       @version_search_list    = qw/ default original signed audiodescribed opensubtitled shortened lengthened other /;
+       @version_search_list    = split /,/, $opt{versionlist} if $opt{versionlist};
+       # Set quiet, test and get options if we're asked for streaminfo
+       if ( $opt{streaminfo} ) {
+               $opt{test}      = 1;
+               $opt{get}       = 1;
+               $opt{quiet}     = 1;
+       }
+
+       # Sanity check some conflicting options
+       if ($opt{nowrite} && (!$opt{stdout})) {
+               logger "ERROR: Cannot download to nowhere\n";
+               exit 1;
+       }
+
+       # Backward compatability options - to be removed eventually
+       $opt{vmode} = 'rtmp' if $opt{rtmp};
+       $opt{vmode} = 'n95_wifi' if $opt{n95};
+       $opt{amode} = 'realaudio' if $opt{realaudio};
+       $opt{amode} = 'iphone' if $opt{mp3audio};
+
+       # Web proxy
+       $proxy_url = $opt{proxy} || $ENV{HTTP_PROXY} || $ENV{http_proxy} || '';
+       logger "INFO: Using Proxy $proxy_url\n" if $proxy_url;
+
+       # Update this script if required
+       if ($opt{update}) {
+               update_script();
+       }
+
+       # Check for valid dload dirs or create them
+       for ( keys %download_dir ) {
+               if ( ! -d $download_dir{$_} ) {
+                       logger "INFO: Created directory $download_dir{$_}\n";
+                       mkpath($download_dir{$_});
+               }
+       }
+
+       # Get prog by arbitrary pid (then exit)
+       if ( $opt{pid} ) {
+
+               # Temporary hack to get 'ITV Catch-up' downloads specified as --pid itv:<pid>
+               $type{itv} = 1 if $opt{pid} =~ m{^itv:(.+?)$};
+               if ( $type{itv} ) {
+                       exit 1 if ( ! $opt{streaminfo} ) && check_download_history( $opt{pid} );
+                       # Remove leading itv: tag (backwards compat)
+                       $opt{pid} =~ s/^itv:(.+?)$/$1/ig;
+                       # Force prog type to itv
+                       $prog{$opt{pid}}{type} = 'itv';
+                       download_programme( $opt{pid} );
+                       exit 0;
+               }
+
+               # Remove any url parts from the pid
+               $opt{pid} =~ s/^.*(b0[a-z,0-9]{6}).*$/$1/g;
+               # Retry loop
+               my $count;
+               my $retries = 3;
+               my $retcode;
+               exit 1 if ( ! $opt{streaminfo} ) && check_download_history( $opt{pid} );
+               for ($count = 1; $count <= $retries; $count++) {
+                       $retcode = download_programme( $opt{pid} );
+                       return 0 if $retcode eq 'skip';
+                       if ( $retcode eq 'retry' && $count < $retries ) {
+                               logger "WARNING: Retrying download for PID $opt{pid}\n";
+                       } else {
+                               $retcode = 1 if $retcode eq 'retry';
+                               last;
+                       }
+               }
+               # Add to history, tag and Run post download command if download was successful
+               if ($retcode == 0) {
+                       add_to_download_history( $opt{pid} );
+                       tag_file( $opt{pid} );
+                       run_user_command( $opt{pid}, $opt{command} ) if $opt{command};
+               } elsif (! $opt{test}) {
+                       logger "ERROR: Failed to download PID $opt{pid}\n";
+               }
+               exit 0;
+       }
+
+       # Get stream links from BBC iplayer site or from cache (also populates all hashes) specified in --type option
+       get_links( $_ ) for keys %type;
+
+       # List elements (i.e. 'channel' 'categories') if required and exit
+       if ( $opt{list} ) {
+               list_unique_element_counts( $opt{list} );
+               exit 0;
+       }
+
+       # Parse remaining args
+       my @match_list;
+       for ( @search_args ) {
+               chomp();
+
+               # If Numerical value < $max_index
+               if ( /^[\d]+$/ && $_ <= $max_index) {
+                       push @match_list, $_;
+       
+               # If PID then find matching programmes with this PID
+               } elsif ( /^.*b0[a-z,0-9]{6}.*$/ ) {
+                       s/^.*(b0[a-z,0-9]{6}).*$/$1/g;
+                       push @match_list, get_regex_matches( $1 );
+       
+               # Else assume this is a programme name regex
+               } else {
+                       push @match_list, get_regex_matches( $_ );
+               }
+       }
+       
+       # De-dup matches and retain order
+       my %seen = ();
+       my @unique = grep { ! $seen{ $_ }++ } @match_list;
+       @match_list = @unique;
+       
+       # Go get the cached data for other programme types if the index numbers require it
+       my %require;
+       for ( @match_list ) {
+               for my $types ( @all_prog_types ) {
+                       $require{$types} = 1 if $_ >= $index_range{$types}{min} && $_ <= $index_range{$types}{max} && ( ! $require{$types} ) && ( ! $type{$types} );
+               }
+       }
+
+       # Get extra required programme caches
+       logger "INFO: Additionally getting cached programme data for ".(join ', ', keys %require)."\n" if %require > 0;
+       # Get stream links from BBC iplayer site or from cache (also populates all hashes)
+       for (keys %require) {
+               # Get $_ stream links
+               get_links( $_ );
+               # Add new prog types to the type list
+               $type{$_} = 1;
+       }
+       # Display list for download
+       logger "Matches:\n" if @match_list;
+       @match_list = list_progs( @match_list );
+
+       # Write HTML and XML files if required (with search options applied)
+       create_html( @match_list ) if $opt{html};
+       create_xml( $opt{fxd}, @match_list ) if $opt{fxd};
+       create_xml( $opt{mythtv}, @match_list ) if $opt{mythtv};
+
+       # Do the downloads based on list of index numbers if required
+       if ( $opt{get} || $opt{stdout} ) {
+               for (@match_list) {
+                       # Retry loop
+                       my $count = 0;
+                       my $retries = 3;
+                       my $retcode;
+                       my $pid = $index_pid{$_};
+                       next if ( ! $opt{streaminfo} ) && check_download_history( $pid );
+                       # Skip and warn if there is no pid
+                       if ( ! $pid ) {
+                               logger "ERROR: No PID for index $_ (try using --type option ?)\n";
+                               next;
+                       }
+                       for ($count = 1; $count <= $retries; $count++) {
+                               $retcode = download_programme( $pid );
+                               last if $retcode eq 'skip';
+                               if ( $retcode eq 'retry' && $count < $retries ) {
+                                       logger "WARNING: Retrying download for '$prog{$pid}{name} - $prog{$pid}{episode}'\n";
+                               } else {
+                                       $retcode = 1 if $retcode eq 'retry';
+                                       last;
+                               }
+                       }
+                       # Add to history, tag file, and run post download command if download was successful
+                       if ($retcode == 0) {
+                               add_to_download_history( $pid );
+                               tag_file( $pid );
+                               run_user_command( $pid, $opt{command} ) if $opt{command};
+                               pvr_report( $pid ) if $opt{pvr};
+                       # Next match if 'skip' was returned
+                       } elsif ( $retcode eq 'skip' ) {
+                               last;
+                       } elsif (! $opt{test}) {
+                               logger "ERROR: Failed to download '$prog{$pid}{name} - $prog{$pid}{episode}'\n";
+                       }
+               }
+       }
+
+       return 0;
+}
+
+
+
+# Lists progs given an array of index numbers, also returns an array with non-existent entries removed
+sub list_progs {
+       my $ua = create_ua('desktop');
+       my @checked;
+       my %names;
+       # Setup user agent for a persistent connection to get programme metadata
+       if ( $opt{info} ) {
+               # Truncate array if were lisiting info and > $info_limit entries are requested - be nice to the beeb!
+               if ( $#_ >= $info_limit ) {
+                       $#_ = $info_limit - 1;
+                       logger "WARNING: Only processing the first $info_limit matches\n";
+               }
+       }
+       for (@_) {
+               my $pid = $index_pid{$_};
+               # Skip if pid isn't in index
+               next if ! $pid;
+               # Skip if already downloaded and --hide option is specified
+               next if $opt{hide} && $pids_history{$pid};
+               if (! defined $names{ $prog{$pid}{name} }) {
+                       list_prog_entry( $pid, '' );
+               } else {
+                       list_prog_entry( $pid, '', 1 );
+               }
+               $names{ $prog{$pid}{name} } = 1;
+               push @checked, $_;
+               if ( $opt{info} ) {
+                       my %metadata = get_pid_metadata( $ua, $pid );
+                       display_metadata( \%metadata, qw/ pid index type duration channel available expiry versions guidance categories desc player / );
+               }
+       }
+       logger "\n";
+
+       logger "INFO: ".($#checked + 1)." Matching Programmes\n";
+       return @checked;
+}
+
+
+
+# Display a line containing programme info (using long, terse, and type options)
+sub list_prog_entry {
+       my ( $pid, $prefix, $tree ) = ( @_ );
+       my $prog_type = '';
+       # Show the type field if >1 type has been specified
+       $prog_type = "$prog{$pid}{type}, " if keys %type > 1;
+       my $name;
+       # If tree view
+       if ( $opt{tree} ) {
+               $prefix = '  '.$prefix;
+               $name = '';
+       } else {
+               $name = "$prog{$pid}{name} - ";
+       }
+
+       # Add some info depending on prog_type
+       my %optional = (
+               tv      => ", <channel>, <categories>, <guidance>, <versions>",
+               itv     => ", <channel>, <categories>, <guidance>",
+               radio   => ", <channel>, <categories>",
+               podcast => ", <available>, <channel>, <categories>",
+       );
+       $optional{$_} = substitute_fields( $pid, $optional{$_}, 2 ) for keys %optional;
+
+       logger "\n${prog_type}$prog{$pid}{name}\n" if $opt{tree} && ! $tree;
+       # Display based on output options
+       if ( $opt{long} ) {
+               my @time = gmtime( time() - $prog{$pid}{timeadded} );
+               logger "${prefix}$prog{$pid}{index}:\t${prog_type}${name}$prog{$pid}{episode}$optional{ $prog{$pid}{type} }, $time[7] days $time[2] hours ago - $prog{$pid}{desc}\n";
+       } elsif ( $opt{terse} ) {
+               logger "${prefix}$prog{$pid}{index}:\t${prog_type}${name}$prog{$pid}{episode}\n";
+       } else {
+               logger "${prefix}$prog{$pid}{index}:\t${prog_type}${name}$prog{$pid}{episode}$optional{ $prog{$pid}{type} }\n";
+       }
+       return 0;
+}
+
+
+# Get matching programme index numbers using supplied regex
+sub get_regex_matches {
+       my $download_regex = shift;
+       my %download_hash;
+       my $channel_regex = $opt{channel} || '.*';
+       my $category_regex = $opt{category} || '.*';
+       my $versions_regex = $opt{versions} || '.*';
+       my $exclude_regex = $opt{exclude} || '^ROGUE$';
+       my $channel_exclude_regex = $opt{excludechannel} || '^ROGUE$';
+       my $category_exclude_regex = $opt{excludecategory} || '^ROGUE$';
+       my $since = $opt{since} || 99999;
+       my $now = time();
+               
+       for (keys %index_pid) {
+               my $pid = $index_pid{$_};
+
+               # Only include programmes matching channels and category regexes
+               if ( $prog{$pid}{channel} =~ /$channel_regex/i
+                 && $prog{$pid}{categories} =~ /$category_regex/i
+                 && $prog{$pid}{versions} =~ /$versions_regex/i
+                 && $prog{$pid}{channel} !~ /$channel_exclude_regex/i
+                 && $prog{$pid}{name} !~ /$exclude_regex/i
+                 && $prog{$pid}{categories} !~ /$category_exclude_regex/i
+                 && $prog{$pid}{timeadded} >= $now - ($since * 3600)
+               ) {
+
+                       # Search prognames/pids while excluding channel_regex and category_regex
+                       $download_hash{$_} = 1 if (
+                               $prog{$pid}{name} =~ /$download_regex/i
+                               || ( $pid =~ /$download_regex/i && $download_regex =~ /b00/ )
+                               || ( $pid =~ /$download_regex/i && $download_regex =~ /b00/ )
+                       );
+                       # Also search long descriptions and episode data if -l is specified
+                       $download_hash{$_} = 1 if (
+                               $opt{long} 
+                               && 
+                               ( $prog{$pid}{desc} =~ /$download_regex/i
+                                 || $prog{$pid}{episode} =~ /$download_regex/i
+                               )
+                       );
+               }
+       }
+
+       return sort {$a <=> $b} keys %download_hash;
+}
+
+
+# get_links_bbciplayer (%channels)
+sub get_links_bbciplayer {
+       my $prog_type = shift;
+       my %channels = %{$_[0]};
+
+       my $xml;
+       my $feed_data;
+       my $res;
+       logger "INFO: Getting $prog_type Index Feeds\n";
+       # Setup User agent
+       my $ua = create_ua('desktop');
+
+       # Download index feed
+       # Sort feeds so that category based feeds are done last - this makes sure that the channels get defined correctly if there are dups
+       my @channel_list;
+       push @channel_list, grep !/categor/, keys %channels;
+       push @channel_list, grep  /categor/, keys %channels;
+       for ( @channel_list ) {
+
+               my $url = "${channel_feed_url}/$_/list/limit/400";
+               logger "DEBUG: Getting feed $url\n" if $opt{verbose};
+               $xml = request_url_retry($ua, $url, 3, '.', "WARNING: Failed to get programme index feed for $_ from iplayer site\n");
+               logger "INFO: Got ".(grep /<entry/, split /\n/, $xml)." programmes\n" if $opt{verbose};
+               decode_entities($xml);  
+               
+               # Feed as of August 2008
+               #        <entry>
+               #          <title type="text">Bargain Hunt: Series 18: Oswestry</title>
+               #          <id>tag:feeds.bbc.co.uk,2008:PIPS:b0088jgs</id>
+               #          <updated>2008-07-22T00:23:50Z</updated>
+               #          <content type="html">
+               #            &lt;p&gt;
+               #              &lt;a href=&quot;http://www.bbc.co.uk/iplayer/episode/b0088jgs?src=a_syn30&quot;&gt;
+               #                &lt;img src=&quot;http://www.bbc.co.uk/iplayer/images/episode/b0088jgs_150_84.jpg&quot; alt=&quot;Bargain Hunt: Series 18: Oswestry&quot; /&gt;
+               #              &lt;/a&gt;
+               #            &lt;/p&gt;
+               #            &lt;p&gt;
+               #              The teams are at an antiques fair in Oswestry showground. Hosted by Tim Wonnacott.
+               #            &lt;/p&gt;
+               #          </content>
+               #          <category term="Factual" />
+               #          <category term="TV" />
+               #          <link rel="via" href="http://www.bbc.co.uk/iplayer/episode/b0088jgs?src=a_syn30" type="text/html" title="Bargain Hunt: Series 18: Oswestry" />
+               #       </entry>
+               #
+
+               ### New Feed
+               #  <entry>
+               #    <title type="text">House of Lords: 02/07/2008</title>
+               #    <id>tag:bbc.co.uk,2008:PIPS:b00cd5p7</id>
+               #    <updated>2008-06-24T00:15:11Z</updated>
+               #    <content type="html">
+               #      <p>
+               #       <a href="http://www.bbc.co.uk/iplayer/episode/b00cd5p7?src=a_syn30">
+               #         <img src="http://www.bbc.co.uk/iplayer/images/episode/b00cd5p7_150_84.jpg" alt="House of Lords: 02/07/2008" />
+               #       </a>
+               #      </p>
+               #      <p>
+               #       House of Lords, including the third reading of the Health and Social Care Bill. 1 July.
+               #      </p>
+               #    </content>
+               #    <category term="Factual" scheme="urn:bbciplayer:category" />
+               #    <link rel="via" href="http://www.bbc.co.uk/iplayer/episode/b00cd5p7?src=a_syn30" type="application/atom+xml" title="House of Lords: 02/07/2008">
+               #    </link>
+               #  </entry>
+
+               # Parse XML
+
+               # get list of entries within <entry> </entry> tags
+               my @entries = split /<entry>/, $xml;
+               # Discard first element == header
+               shift @entries;
+
+               my ( $name, $episode, $desc, $pid, $available, $channel, $duration, $thumbnail, $prog_type, $versions );
+               foreach my $entry (@entries) {
+
+                       my $entry_flat = $entry;
+                       $entry_flat =~ s/\n/ /g;
+
+                       # <id>tag:bbc.co.uk,2008:PIPS:b008pj3w</id>
+                       $pid = $1 if $entry =~ m{<id>.*PIPS:(.+?)</id>};
+
+                       # parse name: episode, e.g. Take a Bow: Street Feet on the Farm
+                       $name = $1 if $entry =~ m{<title\s*.*?>\s*(.*?)\s*</title>};
+                       $episode = $name;
+                       $name =~ s/^(.*): .*$/$1/g;
+                       $episode =~ s/^.*: (.*)$/$1/g;
+
+                       # This is not the availability!
+                       # <updated>2008-06-22T05:01:49Z</updated>
+                       #$available = get_available_time_string( $1 ) if $entry =~ m{<updated>(\d{4}\-\d\d\-\d\dT\d\d:\d\d:\d\d.).*?</updated>};
+
+                       #<p>    House of Lords, including the third reading of the Health and Social Care Bill. 1 July.   </p>    </content>
+                       $desc = $1 if $entry =~ m{<p>\s*(.*?)\s*</p>\s*</content>};
+
+                       # Parse the categories into hash
+                       # <category term="Factual" />
+                       my @category;
+                       for my $line ( grep /<category/, (split /\n/, $entry) ) {
+                               push @category, $1 if $line =~ m{<category\s+term="(.+?)"};
+                       }
+
+                       # Extract channel and type
+                       ($prog_type, $channel) = (split /\|/, $channels{$_})[0,1];
+
+                       logger "DEBUG: '$pid, $name - $episode, $channel'\n" if $opt{debug};
+
+                       # Merge and Skip if this pid is a duplicate
+                       if ( defined $prog{$pid} ) {
+                               logger "WARNING: '$pid, $prog{$pid}{name} - $prog{$pid}{episode}, $prog{$pid}{channel}' already exists (this channel = $channel)\n" if $opt{verbose};
+                               # Since we use the 'Signed' channel to get sign zone data, merge the categories from this entry to the existing entry
+                               if ( $prog{$pid}{categories} ne join(',', @category) ) {
+                                       my %cats;
+                                       $cats{$_} = 1 for ( split /,/, $prog{$pid}{categories} );
+                                       $cats{$_} = 1 for ( @category );
+                                       logger "INFO: Merged categories for $pid from $prog{$pid}{categories} to ".join(',', sort keys %cats)."\n" if $opt{verbose};
+                                       $prog{$pid}{categories} = join(',', sort keys %cats);
+                               }
+                               # If this is a dupicate pid and the channel is now Signed then both versions are available
+                               $prog{$pid}{versions} = 'default,signed' if $channel eq 'Signed';
+                               next;
+                       }
+
+                       # Check for signed-only version from Channel
+                       if ($channel eq 'Signed') {
+                               $versions = 'signed';
+                       # Else if not channel 'Signed' then this must also have both versions available
+                       } elsif ( grep /Sign Zone/, @category ) {
+                               $versions = 'default,signed';
+                       } else {
+                               $versions = 'default';
+                       }
+                       
+                       # build data structure
+                       $prog{$pid} = {
+                               'name'          => $name,
+                               'versions'      => $versions,
+                               'episode'       => $episode,
+                               'desc'          => $desc,
+                               'available'     => 'Unknown',
+                               'duration'      => 'Unknown',
+                               'thumbnail'     => "${thumbnail_prefix}/${pid}_150_84.jpg",
+                               'channel'       => $channel,
+                               'categories'    => join(',', @category),
+                               'type'          => $prog_type,
+                               'web'           => "${bbc_prog_page_prefix}/${pid}.html",
+                       };
+               }
+       }
+       logger "\n";
+       return 0;
+}
+
+
+
+# Populates the index field of the prog hash as well as creating the %index_pid hash
+# Should be run after getting any link lists
+sub sort_indexes {
+
+       # Add index field based on alphabetical sorting by prog name
+       my %index;
+       # Start index counter at 'min' for each prog type
+       $index{$_} = $index_range{$_}{min} for @all_prog_types;
+
+       my @prog_pid;
+
+       # Create unique array of '<progname|pid>'
+       push @prog_pid, "$prog{$_}{name}|$_" for (keys %prog);
+
+       # Sort by progname and index 
+       for (sort @prog_pid) {
+               # Extract pid
+               my $pid = (split /\|/)[1];
+               my $prog_type = $prog{$pid}{type};
+               $index_pid{ $index{$prog_type} } = $pid;
+               $prog{$pid}{index} = $index{$prog_type};
+               $index{$prog_type}++;
+       }
+       return 0;
+}
+
+
+
+# Uses: $podcast_index_feed_url
+# get_links_bbcpodcast ()
+sub get_links_bbcpodcast {
+
+       my $xml;
+       my $res;
+       logger "INFO: Getting podcast Index Feeds\n";
+       # Setup User agent
+       my $ua = create_ua('get_iplayer');
+       
+       # Method
+       # $podcast_index_feed_url (gets list of rss feeds for each podcast prog) =>
+       # http://downloads.bbc.co.uk/podcasts/$channel/$name/rss.xml =>
+       
+       # Download index feed
+       my $xmlindex = request_url_retry($ua, $podcast_index_feed_url, 3, '.', "WARNING: Failed to get prodcast index from site\n");
+       $xmlindex =~ s/\n/ /g;
+
+       # Every RSS feed has an extry like below (all in a text block - not formatted like below)
+       #  <program xmlns="" language="en-gb" typicalDuration="P30M" active="true" public="true" bbcFlavour="Programme Highlights" region="all" wwpid="0">
+       #    <title>Best of Chris Moyles</title>
+       #    <shortTitle>moyles</shortTitle>
+       #    <description>Weekly highlights from the award-winning Chris Moyles breakfast show, as broadcast by Chris and team every morning from 6.30am to 10am.</description>
+       #    <network id="radio1" name="BBC Radio 1" />
+       #    <image use="itunes" url="http://www.bbc.co.uk/radio/podcasts/moyles/assets/_300x300.jpg" />
+       #    <link target="homepage" url="http://www.bbc.co.uk/radio1/chrismoyles/" />
+       #    <link target="feed" url="http://downloads.bbc.co.uk/podcasts/radio1/moyles/rss.xml" />
+       #    <link target="currentItem" url="http://downloads.bbc.co.uk/podcasts/radio1/moyles/moyles_20080926-0630a.mp3">
+       #      <title>Moyles: Guestfest. 26 Sep 08</title>
+       #      <description>Rihanna, Ross Kemp, Jack Osbourne, John
+       #      Barrowman, Cheggars, the legend that is Roy Walker and more,
+       #      all join the team in a celeb laden bundle of mirth and
+       #      merriment. It&#226;&#8364;&#8482;s all the best bits of the
+       #      week from The Chris Moyles Show on BBC Radio 1.</description>
+       #      <publishDate>2008-09-26T06:30:00+01:00</publishDate>
+       #    </link>
+       #    <bbcGenre id="entertainment" name="Entertainment" />
+       #    <systemRef systemId="podcast" key="42" />
+       #    <systemRef systemId="pid.brand" key="b006wkqb" />
+       #    <feed mimeType="audio/mpeg" content="audio" audioCodec="mp3" audioProfile="cbr" />
+       #  </program>
+       for ( split /<program/, $xmlindex ) {
+               # Extract channel name, rss feed data
+               my ($channel, $url, $web);
+
+               # <network id="radio1" name="BBC Radio 1" />
+               $channel = $1 if m{<network\s+id=".*?"\s+name="(.*?)"\s*\/>};
+
+               # <link target="feed" url="http://downloads.bbc.co.uk/podcasts/radio1/moyles/rss.xml" />
+               $url = $1 if m{<link\s+target="feed"\s+url="(.*?)"\s*\/>};
+
+               # <link target="homepage" url="http://www.bbc.co.uk/radio1/chrismoyles/" />
+               $web = $1 if m{<link\s+target="homepage"\s+url="(.*?)"\s*\/>};
+
+               # Skip if there is no feed data for channel
+               next if ! ($channel || $url);
+
+               my ( $name, $episode, $desc, $pid, $available, $duration, $thumbnail );
+
+               # Get RSS feeds for each podcast programme
+               logger "DEBUG: Getting podcast feed $url\n" if $opt{verbose};
+               $xml = request_url_retry($ua, $url, 3, '.', "WARNING: Failed to get podcast feed for $channel / $_ from iplayer site\n") if $opt{verbose};
+               $xml = request_url_retry($ua, $url, 3, '.', '') if ! $opt{verbose};
+               # skip if no data
+               next if ! $xml;
+
+               logger "INFO: Got ".(grep /<media:content/, split /<item>/, $xml)." programmes\n" if $opt{verbose};
+               decode_entities($xml);
+       
+               # First entry is channel data
+               # <?xml version="1.0" encoding="utf-8"?>
+               #<rss xmlns:media="http://search.yahoo.com/mrss/"
+               #xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd"
+               #version="2.0">
+               #  <channel>
+               #    <title>Stuart Maconie's Freak Zone</title>
+               #    <link>http://www.bbc.co.uk/6music/shows/freakzone/</link>
+               #    <description>Weekly highlights from Stuart Maconie's
+               #    ...podcast is only available in the UK.</description>
+               #    <itunes:summary>Weekly highlights from Stuart Maconie's
+               #    ...podcast is only available in the UK.</itunes:summary>
+               #    <itunes:author>BBC 6 Music</itunes:author>
+               #    <itunes:owner>
+               #      <itunes:name>BBC</itunes:name>
+               #      <itunes:email>podcast.support@bbc.co.uk</itunes:email>
+               #    </itunes:owner>
+               #    <language>en</language>
+               #    <ttl>720</ttl>
+               #    <image>
+               #      <url>
+               #      http://www.bbc.co.uk/radio/podcasts/freakzone/assets/_300x300.jpg</url>
+               #      <title>Stuart Maconie's Freak Zone</title>
+               #      <link>http://www.bbc.co.uk/6music/shows/freakzone/</link>
+               #    </image>
+               #    <itunes:image href="http://www.bbc.co.uk/radio/podcasts/freakzone/assets/_300x300.jpg" />
+               #    <copyright>(C) BBC 2008</copyright>
+               #    <pubDate>Sun, 06 Jul 2008 20:00:05 +0100</pubDate>
+               #    <itunes:category text="Music" />
+               #    <itunes:keywords>Stewart Maconie, Macconie, freekzone,
+               #    freakzone, macoonie</itunes:keywords>
+               #    <media:keywords>Stewart Maconie, Macconie, freekzone,
+               #    freakzone, macoonie</media:keywords>
+               #   <itunes:explicit>no</itunes:explicit>
+               #    <media:rating scheme="urn:simple">nonadult</media:rating>
+
+               # Parse XML
+
+               # get list of entries within <entry> </entry> tags
+               my @entries = split /<item>/, $xml;
+               # first element == <channel> header
+               my $header = shift @entries;
+
+               # Get podcast name
+               $name = $1 if $header =~ m{<title>\s*(.+?)\s*</title>};
+       
+               # Parse the categories into hash
+               # <itunes:category text="Music" />
+               my @category;
+               for my $line ( grep /<itunes:category/, (split /\n/, $header) ) {
+                       push @category, $1 if $line =~ m{<itunes:category\s+text="\s*(.+?)\s*"};
+               }
+       
+               # Get thumbnail from header
+               # <itunes:image href="http://www.bbc.co.uk/radio/podcasts/freakzone/assets/_300x300.jpg" />
+               $thumbnail = $1 if $header =~ m{<itunes:image href="\s*(.+?)\s*"};
+
+               # Followed by items:
+               #    <item>
+               #      <title>FreakZone: C'est Stuart avec le Professeur Spear et le
+               #      pop francais?</title>
+               #      <description>Stuart and Justin discuss the sub-genre of
+               #      French 'cold wave' in this week's module.</description>
+               #      <itunes:subtitle>Stuart and Justin discuss the sub-genre of
+               #      French 'cold wave' in this week's
+               #      module....</itunes:subtitle>
+               #      <itunes:summary>Stuart and Justin discuss the sub-genre of
+               #      French 'cold wave' in this week's module.</itunes:summary>
+               #      <pubDate>Sun, 06 Jul 2008 20:00:00 +0100</pubDate>
+               #      <itunes:duration>14:23</itunes:duration>
+               #      <enclosure url="http://downloads.bbc.co.uk/podcasts/6music/freakzone/freakzone_20080706-2000.mp3"
+               #      length="13891916" type="audio/mpeg" />
+               #      <guid isPermaLink="false">
+               #      http://downloads.bbc.co.uk/podcasts/6music/freakzone/freakzone_20080706-2000.mp3</guid>
+               #      <link>
+               #      http://downloads.bbc.co.uk/podcasts/6music/freakzone/freakzone_20080706-2000.mp3</link>
+               #      <media:content url="http://downloads.bbc.co.uk/podcasts/6music/freakzone/freakzone_20080706-2000.mp3"
+               #      fileSize="13891916" type="audio/mpeg" medium="audio"
+               #      expression="full" duration="863" />
+               #      <itunes:author>BBC 6 Music</itunes:author>
+               #    </item>
+       
+               foreach my $entry (@entries) {
+
+                       my $entry_flat = $entry;
+                       $entry_flat =~ s/\n/ /g;
+       
+                       # Use the link as a guid
+                       # <link>   http://downloads.bbc.co.uk/podcasts/6music/freakzone/freakzone_20080706-2000.mp3</link>
+                       $pid = $1 if $entry =~ m{<link>\s*(.+?)</link>};
+       
+                       # Skip if this pid is a duplicate
+                       if ( defined $prog{$pid} ) {
+                               logger "WARNING: '$pid, $prog{$pid}{name} - $prog{$pid}{episode}, $prog{$pid}{channel}' already exists (this channel = $_)\n" if $opt{verbose};
+                               next;
+                       }
+       
+                       # parse episode
+                       # <title>FreakZone: C'est Stuart avec le Professeur Spear et le pop francais?</title>
+                       $episode = $1 if $entry =~ m{<title>\s*(.*?)\s*</title>};
+       
+                       # <pubDate>Sun, 06 Jul 2008 20:00:00 +0100</pubDate>
+                       $available = $1 if $entry =~ m{<pubDate>\s*(.*?)\s*</pubDate>};
+       
+                       # <description>Stuart and Justin discuss the sub-genre of French 'cold wave' in this week's module.</description>
+                       $desc = $1 if $entry =~ m{<description>\s*(.*?)\s*</description>};
+       
+                       # Duration
+                       $duration = $1 if $entry =~ m{<itunes:duration>\s*(.*?)\s*</itunes:duration>};
+       
+                       # build data structure
+                       $prog{$pid} = {
+                               'name'          => $name,
+                               'versions'      => 'default',
+                               'episode'       => $episode,
+                               'desc'          => $desc,
+                               'available'     => $available,
+                               'duration'      => $duration,
+                               'thumbnail'     => $thumbnail,
+                               'channel'       => $channel,
+                               'categories'    => join(',', @category),
+                               'type'          => 'podcast',
+                               'web'           => $web,
+                       };
+               }
+       }
+       logger "\n";
+       return 0;
+}
+
+
+
+# Uses:
+# get_links_itv ()
+sub get_links_itv {
+       my %channels = %{$_[0]};
+       my $xml;
+       my $res;
+       my %series_pid;
+       my %episode_pid;
+       logger "INFO: Getting itv Index Feeds\n";
+       # Setup User agent
+       my $ua = create_ua('desktop');
+
+       # Method
+       # http://www.itv.com/_data/xml/CatchUpData/CatchUp360/CatchUpMenu.xml (gets list of urls for each prog series) =>
+       #  =>
+       
+       # Download index feed
+       my $itv_index_shows_url = 'http://www.itv.com/ClassicTVshows/'; # $channel/default.html
+       my $itv_index_feed_url = 'http://www.itv.com/_data/xml/CatchUpData/CatchUp360/CatchUpMenu.xml';
+
+       # Sort feeds so that pages are done last - this makes sure that the channels get defined correctly if there are dups
+       my @channel_list;
+       push @channel_list, grep !/\//, keys %channels;
+       push @channel_list, grep  /\//, keys %channels;
+       # ITV ClassicShows parsing
+       for my $channel ( @channel_list ) {
+               # <li class="first-child"><a href="http://www.itv.com/ClassicTVshows/comedy/ABitofaDo/default.html">A Bit of a Do</a><br></li>
+               # <li><a href="http://www.itv.com/ClassicTVshows/familydrama/achristmascarol/default.html">A Christmas Carol</a><br></li>
+               # Get page, search for relevent lines which contain series links and loop through each matching line
+               for my $s_line ( grep /(<li><a\s+href=".+?"><img\s+src=".+?"\s+alt=".+?"><\/a><h4>|<li.*?><a href=".+?">.+?<\/a><br><\/li>)/, ( split /\n/, request_url_retry($ua, $itv_index_shows_url.${channel}.'/default.html', 3, '.', "WARNING: Failed to get itv ${channel} index from site\n") ) ) {
+                       my ($url, $name);
+                       # Extract series url + series description
+                       ($url, $name) = ($1, $2) if $s_line =~ m{<li><a\s+href="\s*(.+?)\s*"><img\s+src=".+?"\s+alt="\s*(.+?)\s*"><\/a><h4>};
+                       ($url, $name) = ($1, $2) if $s_line =~ m{<li.*?><a href="\s*(.+?)\s*">\s*(.+?)\s*<\/a><br><\/li>};
+                       chomp($url);
+                       chomp($name);
+                       next if ! ($url && $name);
+                       logger "DEBUG: Channel: '$channel' Series: '$name' URL: '$url'\n" if $opt{verbose};
+
+                       # Get list of episodes for this series
+                       # e.g. <li class="first-child"><a title="Play" href="?vodcrid=crid://itv.com/993&amp;DF=0">Episode one</a><br>The Sun in a Bottle</li>
+                       #      <li><a title="Play" href="?vodcrid=crid://itv.com/994&amp;DF=0">Episode two</a><br>Castle Saburac</li>
+                       #      <li class="first-child"><a class="nsat" title="This programme contains strong language and violence      Â " href="?vodcrid=crid://itv.com/588&amp;G=10&amp;DF=0">Episode one</a><br>The Dead of Jericho</li>
+                       #
+                       # e.g. <li><a class="playVideo" title="Play" href="?vodcrid=crid://itv.com/1232&amp;DF=0"><img src="img/60x45/Crossroads-Rosemary-shoots-David-efeef7cd-8d41-416c-9e30-26ce1b3d625c.jpg" alt="Crossroads: Rosemary shoots David"><span>Play</span></a><h4>
+                       #      vodcrid=crid://itv.com/971&amp;DF=0"><img src="img/60x45/9d20fd47-5d4b-44f5-9188-856505de0d0f.jpg" alt="Emmerdale  2002 Louise kills Ray"
+                       #
+                       # e.g. <a class="playVideo" title="Play" href="?vodcrid=crid://itv.com/1854&amp;DF=1"><img src="img/157x104/140c456c-d8bd-49d5-90f8-f7cc6d86f132.jpg" alt="Soldier Soldier "><span>Play</span></a><h2>Soldier Soldier</h2>
+                       #
+                       for my $e_line ( grep /vodcrid=crid/, ( split /\n/, request_url_retry($ua, $url, 3, '.', "WARNING: Failed to get ${name} index from site\n") ) ) {
+                               my ($guidance, $pid, $episode, $thumbnail);
+                               logger "DEBUG: Match Line: $e_line\n" if $opt{debug};
+                               # Extract episode data
+                               ($guidance, $pid, $episode)  = ($2, $3, $4) if $e_line =~ m{<li.*?><a\s+(class="nsat"\s+)?title="\s*(.+?)\s*"\s+href="\?vodcrid=crid://itv.com/(\d+?)&.+?>\s*(.+?)\s*<};
+                               ($pid, $thumbnail, $episode) = ($1, $2, $3) if $e_line =~ m{vodcrid=crid://itv.com/(\d+?)&.+?><img\s+src="(.+?)"\s+alt="\s*(.+?)\s*"};
+                               next if ! ($pid && $episode);
+                               # Remove 'Play'
+                               $guidance =~ s/^Play$//ig;
+                               # Strip non-printables
+                               $guidance =~ s/[\s\x00\xc2\xa0]+$//ig;
+                               #$guidance =~ s|[^\w\s\-\!"£\$\\/%\^&\*\(\)\+=,\.\?':;@~\[\]]+||gi;
+                               #$guidance =~ s/(\s\s)+//g;
+                               logger "DEBUG: PID: '$pid' Episode: '$episode' Guidance: '$guidance'\n" if $opt{debug};
+
+                               # Skip if this pid is a duplicate
+                               if ( defined $prog{$pid} ) {
+                                       logger "WARNING: '$pid, $prog{$pid}{name} - $prog{$pid}{episode}, $prog{$pid}{channel}' already exists (this channel = $channel)\n" if $opt{verbose};
+                                       # Merge data (hack)
+                                       #$prog{$pid}{episode} .= ','.$episode;
+                                       my $oldname = $prog{$pid}{name};
+                                       $prog{$pid}{episode} = $episode if (! $prog{$pid}{episode}) || $prog{$pid}{episode} =~ /$oldname/i;
+                                       $prog{$pid}{thumbnail} = $thumbnail if ! $prog{$pid}{thumbnail};
+                                       $prog{$pid}{guidance} = $guidance if ! $prog{$pid}{guidance};
+                                       next;
+                               }
+
+                               # build data structure
+                               $prog{$pid} = {
+                                       'name'          => $name,
+                                       'versions'      => 'default',
+                                       'episode'       => $episode,
+                                       'channel'       => (split /\|/, $channels{$channel})[1],
+                                       'guidance'      => $guidance,
+                                       'categories'    => (split /\|/, $channels{$channel})[1],
+                                       'type'          => 'itv',
+                                       'web'           => $url,
+                               };
+                       }
+               }
+       }
+
+       my $xmlindex = request_url_retry($ua, $itv_index_feed_url, 3, '.', "WARNING: Failed to get itv index from site\n");
+       $xmlindex =~ s/[\n\r]//g;
+
+       # This gives a list of programme series (sometimes episodes)
+       #    <ITVCatchUpProgramme>
+       #      <ProgrammeId>50</ProgrammeId>
+       #      <ProgrammeTitle>A CHRISTMAS CAROL</ProgrammeTitle>
+       #      <ProgrammeMediaId>615915</ProgrammeMediaId>
+       #      <ProgrammeMediaUrl>
+       #      http://www.itv.com//img/150x113/A-Christmas-Carol-2f16d25a-de1d-4a3a-90cb-d47489eee98e.jpg</ProgrammeMediaUrl>
+       #      <LastUpdated>2009-01-06T12:24:22.7419643+00:00</LastUpdated>
+       #      <Url>
+       #      http://www.itv.com/CatchUp/Video/default.html?ViewType=5&amp;Filter=32910</Url>
+       #      <EpisodeCount>1</EpisodeCount>
+       #      <VideoID>32910</VideoID>
+       #      <DentonID>-1</DentonID>
+       #      <DentonRating></DentonRating>
+       #      <AdditionalContentUrl />
+       #      <AdditionalContentUrlText />
+       #    </ITVCatchUpProgramme>
+
+       for my $feedxml ( split /<ITVCatchUpProgramme>/, $xmlindex ) {
+               # Extract feed data
+               my ($episodecount, $viewtype, $videoid, $url);
+               my @entries;
+
+               logger "\n\nDEBUG: XML: $feedxml\n"  if $opt{debug}; 
+
+               # <EpisodeCount>1</EpisodeCount>
+               $episodecount = $1 if $feedxml =~ m{<EpisodeCount>\s*(\d+)\s*<\/EpisodeCount>};
+
+               # <Url>http://www.itv.com/CatchUp/Video/default.html?ViewType=5&amp;Filter=32910</Url>
+               ($viewtype, $videoid) = ($1, $2) if $feedxml =~ m{<Url>\s*.+?ViewType=(\d+).+?Filter=(\d+)\s*<\/Url>}i;
+
+               ## <VideoID>32910</VideoID>
+               #$videoid = $1 if $feedxml =~ m{<VideoID>\s*(\d+)\s*<\/VideoID>};
+
+               # Skip if there is no feed data for channel
+               next if ($viewtype =~ /^0*$/ || $videoid =~ /^0*$/ );
+
+               logger "DEBUG: Got ViewType=$viewtype VideoId=$videoid EpisodeCount=$episodecount\n" if $opt{debug};
+
+               my $url = "http://www.itv.com/_app/Dynamic/CatchUpData.ashx?ViewType=${viewtype}&Filter=${videoid}";
+
+               # Add response from episode metadata url to list to be parsed if this is an episode link
+               if ( $viewtype == 5 ) {
+                       next if $episode_pid{$videoid};
+                       $episode_pid{$videoid} = 1;
+                       # Get metadata pages for episode
+
+                       my ( $name, $guidance, $channel, $episode, $desc, $pid, $available, $duration, $thumbnail );
+
+                       $pid = $videoid;
+                       $channel = 'ITV Catch-up';
+
+                       # Skip if this pid is a duplicate
+                       if ( defined $prog{$pid} ) {
+                               logger "WARNING: '$pid, $prog{$pid}{name} - $prog{$pid}{episode}, $prog{$pid}{channel}' already exists (this channel = $channel)\n" if $opt{verbose};
+                               next;
+                       }
+
+                       $name = $1 if $feedxml =~ m{<ProgrammeTitle>\s*(.+?)\s*<\/ProgrammeTitle>};
+                       $guidance = $1 if $feedxml =~ m{<DentonRating>\s*(.+?)\s*<\/DentonRating>};
+                       $thumbnail = $1 if $feedxml =~ m{<ProgrammeMediaUrl>\s*(.+?)\s*<\/ProgrammeMediaUrl>};
+                       $episode = $pid;
+                       # Strip non-printable chars
+                       $guidance =~ s/[\s\x00\xc2\xa0]+$//ig;
+
+                       # build data structure
+                       $prog{$pid} = {
+                               'name'          => $name,
+                               'versions'      => 'default',
+                               'episode'       => $episode,
+                               'guidance'      => $guidance,
+                               'desc'          => $desc,
+                               'available'     => $available,
+                               'duration'      => $duration,
+                               'thumbnail'     => $thumbnail,
+                               'channel'       => $channel,
+                               'categories'    => 'TV',
+                               'type'          => 'itv',
+                               'web'           => ${itv_catchup_page_prefix}.${pid},
+                       };
+
+
+
+
+
+               # Get next episode list and parse
+               #     <div class="listItem highlight contain">
+               #      <div class="floatLeft"><a href="http://www.itv.com/CatchUp/Video/default.html?ViewType=5&amp;Filter=33383"><img src="http://www.itv.com//img/157x88/P7-67e0b86f-b335-4f6b-8db
+               #      <div class="content">
+               #        <h3><a href="http://www.itv.com/CatchUp/Video/default.html?ViewType=5&amp;Filter=33383">Emmerdale</a></h3>
+               #        <p class="date">Mon 05 Jan 2009</p>
+               #        <p class="progDesc">Donna is stunned to learn Marlon has pointed the finger at Ross. Aaron defaces Tom King's grave.</p>
+               #        <ul class="progDetails">
+               #          <li>
+               #                          Duration: 30 min
+               #          </li>
+               #          <li class="days">
+               #            Expires in
+               #                        <strong>29</strong>
+               #                                        days
+               #                                </li>
+               #        </ul>
+               #      </div>
+               #    </div>
+               #    <div class="listItem contain">
+               #      <div class="floatLeft"><a href="http://www.itv.com/CatchUp/Video/default.html?ViewType=5&amp;Filter=33245"><img src="http://www.itv.com//img/157x88/Marlon-Dingle-742c50b3-3b
+               #      <div class="content">
+               #        <h3><a href="http://www.itv.com/CatchUp/Video/default.html?ViewType=5&amp;Filter=33245">Emmerdale</a></h3>
+               #        <p class="date">Fri 02 Jan 2009</p>
+               #        <p class="progDesc">Marlon gets his revenge on Ross. The King brothers struggle to restart their business without Matthew. Scarlett is fed up with Victoria getting all Daz
+               #        <ul class="progDetails">
+               #          <li>
+               #                          Duration: 30 min
+               #          </li>
+               #          <li class="days">
+               #            Expires in
+               #                        <strong>26</strong>
+               #                                        days
+               #                                </li>
+               #        </ul>
+               #      </div>
+               #    </div>
+               # 
+               } elsif ( $viewtype == 1 ) {
+                       # Make sure we don't duplicate parsing a series
+                       next if $series_pid{$videoid};
+                       $series_pid{$videoid} = 1;
+
+                       # Get metadata pages for each series
+                       logger "DEBUG: Getting series metadata $url\n" if $opt{debug};
+                       $xml = request_url_retry($ua, $url, 2, '.', "WARNING: Failed to get itv series data for ${videoid} from itv site\n") if $opt{verbose};
+                       $xml = request_url_retry($ua, $url, 2, '.', '') if ! $opt{verbose};
+
+                       # skip if no data
+                       next if ! $xml;
+
+                       decode_entities($xml);
+                       # Flatten entry
+                       $xml =~ s/[\n\r]//g;
+
+                       # Extract Filter (pids) from this list
+                       # e.g. <h3><a href="http://www.itv.com/CatchUp/Video/default.html?ViewType=5&amp;Filter=32042">Emmerdale</a></h3>
+                       my @videoids = (split /<h3><a href=.+?Filter=/, $xml);
+
+                       # Get episode data for each videoid
+                       $viewtype = 5;
+
+                       my @episode_data = split/<h3><a href=.+?Filter=/, $xml;
+                       # Ignore first entry
+                       shift @episode_data;
+                       logger "INFO: Got ".($#episode_data+1)." programmes\n" if $opt{verbose};
+
+                       for my $xml (@episode_data) {
+                               $videoid = $1 if $xml =~ m{^(\d+?)".+$}i;
+
+                               # Make sure we don't duplicate parsing an episode
+                               next if $episode_pid{$videoid};
+                               $episode_pid{$videoid} = 1;
+
+                               my ( $name, $guidance, $channel, $episode, $desc, $pid, $available, $duration, $thumbnail );
+       
+                               $pid = $videoid;
+                               $channel = 'ITV Catch-up';
+       
+                               # Skip if this pid is a duplicate
+                               if ( defined $prog{$pid} ) {
+                                       logger "WARNING: '$pid, $prog{$pid}{name} - $prog{$pid}{episode}, $prog{$pid}{channel}' already exists (this channel = $channel)\n" if $opt{verbose};
+                                       next;
+                               }
+                               $name = $1 if $feedxml =~ m{<ProgrammeTitle>\s*(.+?)\s*<\/ProgrammeTitle>};
+                               $available = $1 if $xml =~ m{<p\s+class="date">(.+?)<\/p>}i;
+                               $episode = $available;
+                               $duration = $1 if $xml =~ m{<li>Duration:\s*(.+?)\s*<\/li>}i;
+                               $desc = $1 if $xml =~ m{<p\s+class="progDesc">(.+?)\s*<\/p>};
+                               $guidance = $1 if $feedxml =~ m{<DentonRating>\s*(.+?)\s*<\/DentonRating>};
+                               $thumbnail = $1 if $feedxml =~ m{<ProgrammeMediaUrl>\s*(.+?)\s*<\/ProgrammeMediaUrl>};
+                               $guidance =~ s/[\s\x00\xc2\xa0]+$//ig;
+
+                               logger "DEBUG: name='$name' episode='$episode' pid=$pid available='$available' \n" if $opt{debug};      
+       
+                               # build data structure
+                               $prog{$pid} = {
+                                       'name'          => $name,
+                                       'versions'      => 'default',
+                                       'episode'       => $episode,
+                                       'guidance'      => $guidance,
+                                       'desc'          => $desc,
+                                       'available'     => $available,
+                                       'duration'      => $duration,
+                                       'thumbnail'     => $thumbnail,
+                                       'channel'       => $channel,
+                                       'categories'    => 'TV',
+                                       'type'          => 'itv',
+                                       'web'           => ${itv_catchup_page_prefix}.${pid},
+                               };
+                       }
+               }       
+
+       }
+       logger "\n";
+       return 0;
+}
+
+
+
+# Feed info:
+#      # Also see http://derivadow.com/2008/07/18/interesting-bbc-data-to-hack-with/
+#      # All podcasts menu (iphone)
+#      http://www.bbc.co.uk/radio/podcasts/ip/
+#      # All radio1 podcasts
+#      http://www.bbc.co.uk/radio/podcasts/ip/lists/radio1.sssi
+#      # All radio1 -> moyles podcasts
+#      http://www.bbc.co.uk/radio/podcasts/moyles/assets/iphone_keepnet.sssi
+#      # RSS Feed (indexed from?)
+#      http://downloads.bbc.co.uk/podcasts/radio1/moyles/rss.xml
+#      # aod by channel see http://docs.google.com/View?docid=d9sxx7p_38cfsmxfcq
+#      # http://www.bbc.co.uk/radio/aod/availability/<channel>.xml
+#      # aod index
+#      http://www.bbc.co.uk/radio/aod/index_noframes.shtml
+#      # schedule feeds
+#      http://www.bbc.co.uk/bbcthree/programmes/schedules.xml
+#      # These need drill-down to get episodes:
+#      # TV schedules by date
+#      http://www.bbc.co.uk/iplayer/widget/schedule/service/cbeebies/date/20080704
+#      # TV schedules in JSON, Yaml or XML
+#      http://www.bbc.co.uk/cbbc/programmes/schedules.(json|yaml|xml)
+#      # TV index on programmes tv
+#      http://www.bbc.co.uk/tv/programmes/a-z/by/*/player
+#      # TV + Radio
+#      http://www.bbc.co.uk/programmes/a-z/by/*/player
+#      # All TV (limit has effect of limiting to 2.? times number entries kB??)
+#      # seems that only around 50% of progs are available here compared to programmes site:
+#      http://feeds.bbc.co.uk/iplayer/categories/tv/list/limit/200
+#      # All Radio
+#      http://feeds.bbc.co.uk/iplayer/categories/radio/list/limit/999
+#      # New:
+#      # iCal feeds see: http://www.bbc.co.uk/blogs/radiolabs/2008/07/some_ical_views_onto_programme.shtml
+#      http://bbc.co.uk/programmes/b0079cmw/episodes/player.ics
+#      # Other data
+#      http://www.bbc.co.uk/cbbc/programmes/genres/childrens/player
+#      http://www.bbc.co.uk/programmes/genres/childrens/schedules/upcoming.ics
+#
+# get_links( <prog_type> )
+sub get_links {
+       my @cache;
+       my $now = time();
+       my $prog_type = shift;
+
+       # Open cache file (need to verify we can even read this)
+       if ( open(CACHE, "< $cachefile{$prog_type}") ) {
+               # Get file contents less any comments
+               @cache = grep !/^[\#\s]/, <CACHE>;
+               close (CACHE);
+       }
+
+       # Read cache into %pid_old and %index_pid_old if cache exists
+       my %prog_old;
+       my %index_pid_old;
+       if (@cache) {
+               for (@cache) {
+                       # Populate %prog from cache
+                       chomp();
+                       # Get cache line
+                       my @record = split /\|/;
+                       my %record_entries;
+                       # Update fields in %prog hash for $pid
+                       $record_entries{$_} = shift @record for @cache_format;
+                       $prog_old{ $record_entries{pid} } = \%record_entries;
+                       $index_pid_old{ $record_entries{index} }  = $record_entries{pid};
+               }
+       }
+
+       # if a cache file doesn't exist/corrupted, flush option is specified or original file is older than $cache_sec then download new data
+       if ( (! @cache) || (! -f $cachefile{$prog_type}) || $opt{flush} || ($now >= ( stat($cachefile{$prog_type})->mtime + $cache_secs )) ) {
+
+               # BBC Podcast only
+               get_links_bbcpodcast() if $prog_type eq 'podcast';
+
+               # ITV only
+               get_links_itv( \%{$channels{$prog_type}} ) if $prog_type eq 'itv';
+
+               # BBC Radio and TV
+               get_links_bbciplayer( $prog_type, \%{$channels{$prog_type}} ) if $prog_type =~ /^(tv|radio)$/;
+
+               # Sort indexes
+               sort_indexes();
+               
+               # Open cache file for writing
+               unlink $cachefile{$prog_type};
+               my $now = time();
+               if ( open(CACHE, "> $cachefile{$prog_type}") ) {
+                       print CACHE "#".(join '|', @cache_format)."\n";
+                       for (sort {$a <=> $b} keys %index_pid) {
+                               my $pid = $index_pid{$_};
+                               # Only write entries for correct prog type
+                               if ($prog{$pid}{type} eq $prog_type) {
+                                       # Merge old and new data to retain timestamps
+                                       # if the entry was in old cache then retain timestamp from old entry
+                                       if ( $prog_old{$pid}{timeadded} ) {
+                                               $prog{$pid}{timeadded} = $prog_old{$pid}{timeadded};
+                                       # Else this is a new entry
+                                       } else {
+                                               $prog{$pid}{timeadded} = $now;
+                                               list_prog_entry( $pid, 'Added: ' );
+                                       }
+                                       # write to cache file
+                                       $prog{$pid}{pid} = $pid;
+                                       # Write each field into cache line
+                                       for my $field (@cache_format) {
+                                               print CACHE $prog{$pid}{$field}.'|';
+                                       }
+                                       print CACHE "\n";
+                               }
+                       }
+                       close (CACHE);
+               } else {
+                       logger "WARNING: Couldn't open cache file '$cachefile{$prog_type}' for writing\n";
+               }
+
+
+       # Else copy data from existing cache file into existing %prog hash
+       } else {
+               $prog{$_} = $prog_old{$_} for keys %prog_old;
+               $index_pid{$_} = $index_pid_old{$_} for keys %index_pid_old;
+       }
+       return 0;
+}
+
+
+
+# Usage: download_programme (<pid>)
+sub download_programme {
+       my $pid = shift;
+       my %streamdata;
+       my %version_pids;
+       my $return;
+
+       # Setup user-agent
+       my $ua = create_ua('desktop');
+
+       # download depending on the prog type
+       logger "INFO: Attempting to Download $prog{$pid}{type}: $prog{$pid}{name} - $prog{$pid}{episode}\n";
+
+       # ITV TV
+       if ( $prog{$pid}{type} eq 'itv' ) {
+               # stream data
+               # Display media stream data if required
+               if ( $opt{streaminfo} ) {
+                       display_stream_info( $pid, undef, 'all' );
+                       $opt{quiet} = 1;
+                       return 'skip';
+               }
+               return download_programme_itv( $ua, $pid );
+       }
+
+       # BBC Podcasts
+       if ( $prog{$pid}{type} eq 'podcast' ) {
+               # stream data not available
+               return 'skip' if $opt{streaminfo};
+               return download_programme_podcast( $ua, $pid );
+       }
+
+       # For BBC Radio/TV we might have a pid with no prog_type - determine here first
+       ( $prog{$pid}{type}, $prog{$pid}{longname}, %version_pids ) = get_version_pids( $ua, $pid );
+
+       # BBC TV
+       if ( $prog{$pid}{type} eq 'tv' ) {
+
+               # Deal with BBC TV fallback modes
+               # Valid modes are iphone,rtmp,flashhigh,flashnormal,flashwii,n95_wifi
+               $opt{vmode} = 'flashhigh,flashnormal' if $opt{vmode} eq 'rtmp' || $opt{vmode} eq 'flash';
+               # Defaults
+               if ( $opt{vmode} eq 'auto' || ! $opt{vmode} ) {
+                       if ( ! exists_in_path($rtmpdump) ) {
+                               logger "WARNING: Not using flash modes since rtmpdump is not found\n" if $opt{verbose};
+                               $opt{vmode} = 'iphone'; 
+                       } else {
+                               $opt{vmode} = 'iphone,flashhigh,flashnormal';
+                       }
+               }
+               # Expand the modes into a loop
+               logger "INFO: $opt{vmode} modes will be tried\n";
+               for my $mode ( split /,/, $opt{vmode} ) {
+                       chomp( $mode );
+                       logger "INFO: Attempting to download using $mode mode\n";
+                       $return = download_programme_tv( $ua, $pid, $mode, \%version_pids );
+                       logger "DEBUG: Download using $mode mode return code: '$return'\n" if $opt{debug};
+
+                       # Give up trying alternative download methods
+                       return 2 if $return eq 'abort';
+
+                       # Return to retry loop if successful or retry requested 
+                       return 'retry' if $return eq 'retry';
+
+                       # Return to retry loop and do nothing 
+                       return 'skip' if $return eq 'skip';
+
+                       # Return 0 if successful 
+                       return 0 if ! $return;
+
+                       # Return failed if there is no 'next'
+                       return 1 if $return ne 'next';
+               }
+       }
+
+       # BBC Radio
+       if ( $prog{$pid}{type} eq 'radio' ) {
+               # This will always be the pid version for radio
+               $prog{$pid}{version} = 'default';
+               # Display media stream data if required
+               if ( $opt{streaminfo} ) {
+                       display_stream_info( $pid, $version_pids{default}, 'all' );
+                       $opt{quiet} = 1;
+                       return 'skip';
+               }
+
+               # Deal with radio fallback modes
+               # Valid modes are mp3|iphone,flash|rtmp,real|ra
+               # Defaults
+               if ( $opt{amode} eq 'auto' || ! $opt{amode} ) {
+                       if ( ! exists_in_path($rtmpdump) ) {
+                               logger "WARNING: Not using flash modes since rtmpdump is not found\n" if $opt{verbose};
+                               $opt{amode} = 'iphone,real';
+                       } else {
+                               $opt{amode} = 'iphone,flash,real';
+                       }
+               }
+               $opt{amode} = 'iphone,flash,real' if $opt{amode} eq 'auto' || ! $opt{amode};
+
+               # Expand the modes into a loop
+               logger "INFO: $opt{amode} modes will be tried\n";
+               for my $mode ( split /,/, $opt{amode} ) {
+                       chomp( $mode );
+                       logger "INFO: Attempting to download using $mode mode\n";
+                       # RealAudio
+                       if ( $mode =~ /^(real|ra)/ ) {
+                               $return = download_programme_radio_realaudio( $ua, $pid, \%version_pids );
+
+                       # FlashAudio
+                       } elsif ( $mode =~ /^(flash|rtmp)/ ) {
+                               $return = download_programme_radio_flashaudio( $ua, $pid, $mode, \%version_pids );
+
+                       # iPhone
+                       } elsif ( $mode =~ /^(iphone|mp3)/ ) {
+                               $return = download_programme_radio_iphone( $ua, $pid, \%version_pids );
+                       }
+                       logger "DEBUG: Download using $mode mode return code: '$return'\n" if $opt{debug};
+
+                       # Give up trying alternative download methods
+                       return 2 if $return eq 'abort';
+
+                       # Not going to allow retries here until rtmpdump/flashaudio exits correctly - so just just skip to next mode for now
+                       $return = 'next' if $return eq 'retry';
+                       ## Return to retry loop if successful or retry requested 
+                       #return 'retry' if $return eq 'retry';
+
+                       # Return to retry loop and do nothing 
+                       return 'skip' if $return eq 'skip';
+
+                       # Return 0 if successful 
+                       return 0 if ! $return;
+
+                       # Return failed if there is no 'next'
+                       return 1 if $return ne 'next';
+               }
+       }
+
+       # If we get here then we have failed
+       return 1;
+}
+
+
+
+sub download_programme_itv {
+       my ( $ua, $pid ) = ( @_ );
+       my %streamdata;
+
+       # Check for mplayer (required)
+       if (! exists_in_path($mplayer)) {
+               logger "\nERROR: Required $mplayer does not exist, skipping\n";
+               return 21;
+       }
+
+       $prog{$pid}{dir} = $download_dir{ $prog{$pid}{type} };
+       $prog{$pid}{pid} = $pid;
+       $prog{$pid}{ext} = 'mp4';
+       $prog{$pid}{ext} = 'asf' if $opt{raw};
+       
+       my @url_list = split /\|/, %{get_media_stream_data( $pid, undef, 'itv')}->{streamurl};
+
+       # Get and set more meta data - Set the %prog values from metadata if they aren't already set
+       my %metadata = get_pid_metadata($ua, $pid);
+       for ( qw/ name episode available duration thumbnail desc guidance / ) {
+               $prog{$pid}{$_} = $metadata{$_} if ! $prog{$pid}{$_};
+       }
+
+       $prog{$pid}{fileprefix} = generate_download_filename_prefix( $pid, $prog{$pid}{dir}, $opt{fileprefix} || "<name> <episode> <pid>" );
+       logger "\rINFO: File name prefix = $prog{$pid}{fileprefix}                 \n";
+       # Create a subdir if there are multiple parts
+       if ($#url_list > 0) {
+               $prog{$pid}{dir} .= "/$prog{$pid}{fileprefix}";
+               logger "INFO: Creating subdirectory $prog{$pid}{dir} for programme\n" if $opt{verbose};
+               mkpath $prog{$pid}{dir} if ! -d $prog{$pid}{dir};
+       }
+       my $file_done = "$prog{$pid}{dir}/$prog{$pid}{fileprefix}.$prog{$pid}{ext}";
+       my $file = "$prog{$pid}{dir}/$prog{$pid}{fileprefix}.partial.$prog{$pid}{ext}";
+       $prog{$pid}{filename} = $file_done;
+
+       # Display metadata
+       display_metadata( $prog{$pid}, qw/ pid index name duration available expiry desc / );
+
+       # Skip from here if we are only testing downloads
+       return 1 if $opt{test};
+
+       return download_stream_mms_video( $ua, (join '|', @url_list), $file, $file_done, $pid );
+}      
+
+
+
+sub download_programme_podcast {
+       my ( $ua, $pid ) = ( @_ );
+       my %streamdata;
+
+       $prog{$pid}{dir} = $download_dir{ $prog{$pid}{type} };
+
+       # Determine the correct filename and extension for this download
+       my $filename_orig = $pid;
+       $prog{$pid}{ext} = $pid;
+       $filename_orig =~ s|^.+/(.+?)\.\w+$|$1|g;
+       $prog{$pid}{ext} =~ s|^.*\.(\w+)$|$1|g;
+       $prog{$pid}{fileprefix} = generate_download_filename_prefix($pid, $prog{$pid}{dir}, $opt{fileprefix} || "<longname> - <episode> $filename_orig");
+       logger "\rINFO: File name prefix = $prog{$pid}{fileprefix}                  \n";
+       my $file_done = "$prog{$pid}{dir}/$prog{$pid}{fileprefix}.$prog{$pid}{ext}";
+       my $file = "$prog{$pid}{dir}/$prog{$pid}{fileprefix}.partial.$prog{$pid}{ext}";
+       $prog{$pid}{filename} = $file_done;
+       if ( -f $file_done && stat($file_done)->size > $min_download_size ) {
+               logger "WARNING: File $file_done already exists\n\n";
+               return 1;
+       }
+
+       # Skip from here if we are only testing downloads
+       return 1 if $opt{test};
+
+       # Create symlink filename if required
+       my $file_symlink;
+       if ( $opt{symlink} ) {
+               # Substitute the fields for the pid
+               $file_symlink = substitute_fields( $pid, $opt{symlink} );
+       }
+
+       return download_stream_podcast( $ua, $pid, $file, $file_done, $file_symlink );
+}
+
+
+
+sub download_programme_radio_realaudio {
+       my $ua = shift; 
+       my $pid = shift;
+       my %version_pids = %{@_[0]};
+       my %streamdata;
+
+       # Check dependancies for radio programme transcoding / streaming
+       # Check if we need 'tee'
+       if ( (! exists_in_path($tee)) && $opt{stdout} && (! $opt{nowrite}) ) {
+               logger "\nERROR: $tee does not exist in path, skipping\n";
+               return 'abort';
+       }
+       if (! exists_in_path($mplayer)) {
+               logger "\nWARNING: Required $mplayer does not exist\n";
+               return 'next';
+       }
+       # Check if we have mplayer and lame
+       if ( (! $opt{wav}) && (! $opt{raw}) && (! exists_in_path($lame)) ) {
+               logger "\nWARNING: Required $lame does not exist, will save file in wav format\n";
+               $opt{wav} = 1;
+       }
+
+       $prog{$pid}{dir} = $download_dir{ $prog{$pid}{type} };
+       $prog{$pid}{ext} = 'mp3';
+       $prog{$pid}{ext} = 'ra'  if $opt{raw};
+       $prog{$pid}{ext} = 'wav' if $opt{wav};
+
+       my $url_2 = %{get_media_stream_data( $pid, $version_pids{default}, 'realaudio')}->{streamurl};
+               
+       # Report error if no versions are available
+       if ( ! $url_2 ) {
+               logger "WARNING: RealAudio version not available\n";
+               return 'next';
+       } else {
+               logger "INFO: Stage 2 URL = $url_2\n" if $opt{verbose};
+       }
+
+       # Determine the correct filenames for this download
+       $prog{$pid}{fileprefix} = generate_download_filename_prefix( $pid, $prog{$pid}{dir}, $opt{fileprefix} || "<longname> - <episode> <pid>" );
+       logger "\rINFO: File name prefix = $prog{$pid}{fileprefix}                 \n";
+       my $file_done = "$prog{$pid}{dir}/$prog{$pid}{fileprefix}.$prog{$pid}{ext}";
+       my $file = "$prog{$pid}{dir}/$prog{$pid}{fileprefix}.partial.$prog{$pid}{ext}";
+       $prog{$pid}{filename} = $file_done;
+       if ( -f $file_done ) {
+               logger "WARNING: File $file_done already exists\n\n";
+               return 'abort';
+       }
+
+       # Skip from here if we are only testing downloads
+       return 'abort' if $opt{test};
+
+       # Create symlink filename if required
+       my $file_symlink;
+       if ( $opt{symlink} ) {
+               # Substitute the fields for the pid
+               $file_symlink = substitute_fields( $pid, $opt{symlink} );
+       }
+
+       # Do the audio download
+       return download_stream_rtsp( $ua, $url_2, $file, $file_done, $file_symlink, $pid );
+}
+
+
+       
+sub download_programme_radio_iphone {
+       my $ua = shift; 
+       my $pid = shift;
+       my %version_pids = %{@_[0]};
+       my %streamdata;
+       my $url_2;
+
+       $prog{$pid}{dir} = $download_dir{ $prog{$pid}{type} };
+       $prog{$pid}{ext} = 'mp3';
+
+       my $url_2 = %{get_media_stream_data( $pid, $version_pids{ $prog{$pid}{version} }, 'iphone')}->{streamurl};
+
+       # Report error if no versions are available
+       if ( ! $url_2 ) {
+               logger "WARNING: iPhone stream media not available\n";
+               return 'next';
+       } else {
+               logger "INFO: Stage 2 URL = $url_2\n" if $opt{verbose};
+       }
+
+       # Determine the correct filenames for this download
+       $prog{$pid}{fileprefix} = generate_download_filename_prefix( $pid, $prog{$pid}{dir}, $opt{fileprefix} || "<longname> - <episode> <pid> <version>" );
+       logger "\rINFO: File name prefix = $prog{$pid}{fileprefix}                 \n";
+       my $file_done = "$prog{$pid}{dir}/$prog{$pid}{fileprefix}.$prog{$pid}{ext}";
+       my $file = "$prog{$pid}{dir}/$prog{$pid}{fileprefix}.partial.$prog{$pid}{ext}";
+       $prog{$pid}{filename} = $file_done;
+       if ( -f $file_done ) {
+               logger "WARNING: File $file_done already exists\n\n";
+               return 'abort';
+       }
+
+       # Skip from here if we are only testing downloads
+       return 'abort' if $opt{test};
+
+       # Create symlink filename if required
+       my $file_symlink;
+       if ( $opt{symlink} ) {
+               # Substitute the fields for the pid
+               $file_symlink = substitute_fields( $pid, $opt{symlink} );
+       }
+
+       my $return;
+       # Disable proxy here if required
+       $ua->proxy( ['http'] => undef ) if $opt{partialproxy};
+       $return = download_stream_iphone( $ua, $url_2, $pid, $file, $file_done, $file_symlink, 0 );
+       # Re-enable proxy here if required
+       $ua->proxy( ['http'] => $proxy_url ) if $opt{partialproxy};
+
+       return $return;
+}
+
+
+
+sub download_programme_radio_flashaudio {
+       my $ua = shift; 
+       my $pid = shift;
+       my $mode = shift;
+       my %version_pids = %{@_[0]};
+       my %streamdata;
+       my $url_2;
+
+       # Force raw mode if ffmpeg is not installed
+       if ( ! exists_in_path($mplayer) ) {
+               logger "\nWARNING: $mplayer does not exist - not converting flv file\n";
+               $opt{raw} = 1;
+       }
+       # Disable rtmp modes if rtmpdump does not exist
+       if ( ! exists_in_path($rtmpdump) ) {
+               logger "\nERROR: Required program $rtmpdump does not exist (see http://linuxcentre.net/getiplayer/installation and http://linuxcentre.net/getiplayer/download)\n";
+               return 'next';
+       }
+
+       $prog{$pid}{dir} = $download_dir{ $prog{$pid}{type} };
+       $prog{$pid}{ext} = 'mp3';
+       $prog{$pid}{ext} = 'flv' if $opt{raw};
+
+       logger "INFO: Trying to get media stream metadata for flashaudio RTMP mode\n" if $opt{verbose};
+       %streamdata = %{ get_media_stream_data( $pid, $version_pids{ $prog{$pid}{version} }, 'flashaudio') };
+       $url_2 = $streamdata{streamurl};
+       if ( ! $url_2 ) {
+               logger "\nWARNING: No flashaudio version available\n";
+               return 'next';
+       }
+
+       # Determine the correct filenames for this download
+       $prog{$pid}{fileprefix} = generate_download_filename_prefix( $pid, $prog{$pid}{dir}, $opt{fileprefix} || "<longname> - <episode> <pid> <version>" );
+       logger "\rINFO: File name prefix = $prog{$pid}{fileprefix}                 \n";
+       my $file_done = "$prog{$pid}{dir}/$prog{$pid}{fileprefix}.$prog{$pid}{ext}";
+       my $file = "$prog{$pid}{dir}/$prog{$pid}{fileprefix}.partial.$prog{$pid}{ext}";
+       $prog{$pid}{filename} = $file_done;
+       if ( -f $file_done ) {
+               logger "WARNING: File $file_done already exists\n\n";
+               return 'abort';
+       }
+
+       # Skip from here if we are only testing downloads
+       return 'abort' if $opt{test};
+
+       # Create symlink filename if required
+       my $file_symlink;
+       if ( $opt{symlink} ) {
+               # Substitute the fields for the pid
+               $file_symlink = substitute_fields( $pid, $opt{symlink} );
+       }
+
+       # Do the RTMP flashaudio download
+       return download_stream_rtmp( $ua, $streamdata{streamurl}, $pid, 'flashaudio', $streamdata{application}, $streamdata{tcurl}, $streamdata{authstring}, $streamdata{swfurl}, $file, $file_done, $file_symlink );
+}
+
+
+
+# Usage: download_programme_tv (<pid>)
+sub download_programme_tv {
+       my $ua = shift; 
+       my $pid = shift;
+       my $mode = shift;
+       my %version_pids = %{@_[0]};
+       my %streamdata;
+       my $url_2;
+       my $got_url;
+
+       # Check if we have vlc - if not use iPhone mode
+       if ( $opt{vmode} eq 'n95' && (! exists_in_path($vlc)) ) {
+               logger "\nWARNING: Required $vlc does not exist\n";
+               return 'next';
+       }               
+       # if rtmpdump does not exist
+       if ( $mode =~ /^(rtmp|flash)/ && ! exists_in_path($rtmpdump)) {
+               logger "WARNING: Required program $rtmpdump does not exist (see http://linuxcentre.net/getiplayer/installation and http://linuxcentre.net/getiplayer/download)\n";
+               return 'next';
+       }
+       # Force raw mode if ffmpeg is not installed
+       if ( $mode =~ /^(flash|rtmp)/ && ! exists_in_path($ffmpeg)) {
+               logger "\nWARNING: $ffmpeg does not exist - not converting flv file\n";
+               $opt{raw} = 1;
+       }
+
+       $prog{$pid}{dir} = $download_dir{ $prog{$pid}{type} };
+       $prog{$pid}{ext} = 'mov';
+       # Lookup table to determine which ext to use for different download methods
+       my %stream_ext = (
+               iphone          => 'mov',
+               flashhigh       => 'mp4',
+               flashnormal     => 'avi',
+               flashwii        => 'avi',
+               n95_wifi        => '3gp',
+               n95_3g          => '3gp',
+       );
+       $prog{$pid}{ext} = $stream_ext{$mode} if not $opt{raw};
+       $prog{$pid}{ext} = 'flv' if $mode =~ /^(flash|rtmp)/ && $opt{raw};
+
+       # Do this for each version tried in this order (if they appeared in the content)
+       for my $version ( @version_search_list ) {
+
+               # Change $verpid to 'default' type if it exists, then Used 'signed' otherwise
+               if ( $version_pids{$version} ) {
+                       logger "INFO: Checking existence of $version version\n";
+                       $prog{$pid}{version} = $version;
+                       logger "INFO: Version = $prog{$pid}{version}\n" if $opt{verbose};
+                       # Try to get stream data
+                       %streamdata = %{ get_media_stream_data( $pid, $version_pids{ $prog{$pid}{version} }, $mode) };
+                       $url_2 = $streamdata{streamurl};
+               }
+               # Break out of loop if we have an actual URL
+               last if $url_2;
+       }
+
+       # Display media stream data if required
+       if ( $opt{streaminfo} ) {
+               display_stream_info( $pid, $version_pids{ $prog{$pid}{version} }, 'all' );
+               $opt{quiet} = 1;
+               return 'skip';
+       }
+
+       # Report error if no versions are available
+       if ( ! $url_2 ) {
+               logger "\nWARNING: No $mode versions available\n";
+               return 'next';
+       }
+
+       # Determine the correct filenames for this download
+       $prog{$pid}{fileprefix} = generate_download_filename_prefix( $pid, $prog{$pid}{dir}, $opt{fileprefix} || "<longname> - <episode> <pid> <version>" );
+       logger "\rINFO: File name prefix = $prog{$pid}{fileprefix}                 \n";
+       my $file_done = "$prog{$pid}{dir}/$prog{$pid}{fileprefix}.$prog{$pid}{ext}";
+       my $file = "$prog{$pid}{dir}/$prog{$pid}{fileprefix}.partial.$prog{$pid}{ext}";
+       $prog{$pid}{filename} = $file_done;
+       if ( -f $file_done ) {
+               logger "ERROR: File $file_done already exists\n\n";
+               return 'abort';
+       }
+
+       # Skip from here if we are only testing downloads
+       return 'abort' if $opt{test};
+
+       # Create symlink filename if required
+       my $file_symlink;
+       if ( $opt{symlink} ) {
+               # Substitute the fields for the pid
+               $file_symlink = substitute_fields( $pid, $opt{symlink} );
+       }
+
+       # Get subtitles if they exist and are required 
+       # best to do this before d/l of file so that the subtitles can be enjoyed while download progresses
+       my $subfile_done;
+       my $subfile;
+       if ( $opt{subtitles} ) {
+               $subfile_done = "$prog{$pid}{dir}/$prog{$pid}{fileprefix}.srt";
+               $subfile = "$prog{$pid}{dir}/$prog{$pid}{fileprefix}.partial.srt";
+               $ua->proxy( ['http'] => undef ) if $opt{partialproxy};
+               download_stream_subtitles( $ua, $subfile, $version_pids{ $prog{$pid}{version} } );
+               $ua->proxy( ['http'] => $proxy_url ) if $opt{partialproxy};
+       }
+
+       my $return;
+       # Do rtmp download
+       if ( $mode =~ /^(rtmp|flash)/ ) {
+               $return = download_stream_rtmp( $ua, $streamdata{streamurl}, $pid, $mode, $streamdata{application}, $streamdata{tcurl}, $streamdata{authstring}, $streamdata{swfurl}, $file, $file_done, $file_symlink );
+
+       # Do the N95 h.264 download
+       } elsif ( $mode =~ /^n95/ ) {
+               $return = download_stream_h264_low( $ua, $url_2, $file, $file_done, $pid, $mode );
+
+       # Do the iPhone h.264 download
+       } else {
+               # Disable proxy here if required
+               $ua->proxy( ['http'] => undef ) if $opt{partialproxy};
+               $return = download_stream_iphone( $ua, $url_2, $pid, $file, $file_done, $file_symlink, 1 );
+               # Re-enable proxy here if required
+               $ua->proxy( ['http'] => $proxy_url ) if $opt{partialproxy};
+       }
+       
+       # Rename the subtitle file accordingly
+       move($subfile, $subfile_done) if $opt{subtitles} && -f $subfile;
+
+       # Re-symlink file
+       if ( $opt{symlink} ) {
+               # remove old symlink
+               unlink $file_symlink if -l $file_symlink;
+               symlink $file_done, $file_symlink;
+               logger "INFO: Created symlink from '$file_symlink' -> '$file_done'\n" if $opt{verbose};
+       }
+
+       return $return;
+}
+
+
+
+# Download Subtitles, convert to srt(SubRip) format and apply time offset
+sub download_stream_subtitles {
+       my ( $ua, $file, $verpid ) = @_;
+       my $suburl;
+       my $subs;
+       logger "INFO: Getting Subtitle metadata for $verpid\n" if $opt{verbose};
+       $suburl = %{get_media_stream_data( undef, $verpid, 'subtitles')}->{streamurl};
+       # Return if we have no url
+       if (! $suburl) {
+               logger "INFO: Subtitles not available\n";
+               return 2;
+       }
+
+       logger "INFO: Getting Subtitles from $suburl\n" if $opt{verbose};
+
+       # Open subs file
+       unlink($file);
+       my $fh = open_file_append($file);
+
+       # Download subs
+       $subs = request_url_retry($ua, $suburl, 2);
+       if (! $subs ) {
+               logger "ERROR: Subtitle Download failed\n";
+               return 1;
+       } else {
+               logger "INFO: Downloaded Subtitles\n";
+       }
+
+       # Convert the format to srt
+       # SRT:
+       #1
+       #00:01:22,490 --> 00:01:26,494
+       #Next round!
+       #
+       #2
+       #00:01:33,710 --> 00:01:37,714
+       #Now that we've moved to paradise, there's nothing to eat.
+       #
+       
+       # TT:
+       #<p begin="0:01:12.400" end="0:01:13.880">Thinking.</p>
+
+       my $count = 1;
+       my @lines = grep /<p\s+begin/, split /\n/, $subs;
+       for ( @lines ) {
+               my ( $begin, $end, $sub );
+               $begin = $1 if m{begin="(.+?)"};
+               $end =   $1 if m{end="(.+?)"};
+               $sub =   $1 if m{>(.+?)</p>};
+               if ($begin && $end && $sub ) {
+                       $begin =~ s/\./,/g;
+                       $end =~ s/\./,/g;
+                       if ($opt{suboffset}) {
+                               $begin = subtitle_offset( $begin, $opt{suboffset} );
+                               $end = subtitle_offset( $end, $opt{suboffset} );
+                       }
+                       decode_entities($sub);
+                       # Write to file
+                       print $fh "$count\n";
+                       print $fh "$begin --> $end\n";
+                       print $fh "$sub\n\n";
+                       $count++;
+               }
+       }       
+       close $fh;
+
+       return 0;
+}
+
+
+
+# Returns an offset timestamp given an srt begin or end timestamp and offset in ms
+sub subtitle_offset {
+       my ( $timestamp, $offset ) = @_;
+       my ( $hr, $min, $sec, $ms ) = split /[:,\.]/, $timestamp;
+       # split into hrs, mins, secs, ms
+       my $ts = $ms + $sec*1000 + $min*60*1000 + $hr*60*60*1000 + $offset;
+       $hr = int( $ts/(60*60*1000) );
+       $ts -= $hr*60*60*1000;
+       $min = int( $ts/(60*1000) );
+       $ts -= $min*60*1000;
+       $sec = int( $ts/1000 );
+       $ts -= $sec*1000;
+       $ms = $ts;
+       return "$hr:$min:$sec,$ms";
+}
+
+
+
+# Return hash of version => verpid given a pid
+sub get_version_pids {
+       my ( $ua, $pid ) = @_;
+       my %version_pids;
+       my $url = $prog_iplayer_metadata.$pid;
+
+       logger "INFO: iPlayer metadata URL = $url\n" if $opt{verbose};
+       logger "INFO: Getting version pids for programme $pid        \n" if ! $opt{verbose};
+
+       # send request
+       my $res = $ua->request( HTTP::Request->new( GET => $url ) );
+       if ( ! $res->is_success ) {
+               logger "\rERROR: Failed to get version pid metadata from iplayer site\n\n";
+               return %version_pids;
+       }
+
+       # The URL http://www.bbc.co.uk/iplayer/playlist/<PID> contains for example:
+       #<?xml version="1.0" encoding="UTF-8"?>                                   
+       #<playlist xmlns="http://bbc.co.uk/2008/emp/playlist" revision="1">       
+       #  <id>tag:bbc.co.uk,2008:pips:b00dlrc8:playlist</id>                     
+       #  <link rel="self" href="http://www.bbc.co.uk/iplayer/playlist/b00dlrc8"/>
+       #  <link rel="alternate" href="http://www.bbc.co.uk/iplayer/episode/b00dlrc8"/>
+       #  <link rel="holding" href="http://www.bbc.co.uk/iplayer/images/episode/b00dlrc8_640_360.jpg" height="360" width="640" type="image/jpeg" />
+       #  <title>Amazon with Bruce Parry: Episode 1</title>                                                                                        
+       #  <summary>Bruce Parry begins an epic adventure in the Amazon following the river from source to sea, beginning  in the High Andes and visiting the Ashaninka tribe.</summary>                                                                                                        
+       #  <updated>2008-09-18T14:03:35Z</updated>                                                                                                  
+       #  <item kind="ident">                                                                                                                      
+       #    <id>tag:bbc.co.uk,2008:pips:bbc_two</id>                                                                                               
+       #    <mediator identifier="bbc_two" name="pips"/>                                                                                           
+       #  </item>                                                                                                                                  
+       #  <item kind="programme" duration="3600" identifier="b00dlr9p" group="b00dlrc8" publisher="pips">                                          
+       #    <tempav>1</tempav>                                                                                                                     
+       #    <id>tag:bbc.co.uk,2008:pips:b00dlr9p</id>                                                                                              
+       #    <service id="bbc_two" href="http://www.bbc.co.uk/iplayer/bbc_two">BBC Two</service>                                                    
+       #    <masterbrand id="bbc_two" href="http://www.bbc.co.uk/iplayer/bbc_two">BBC Two</masterbrand>                                            
+       #
+       #    <alternate id="default" />
+       #    <guidance>Contains some strong language.</guidance>
+       #    <mediator identifier="b00dlr9p" name="pips"/>      
+       #  </item>
+       #  <item kind="programme" duration="3600" identifier="b00dp4xn" group="b00dlrc8" publisher="pips">
+       #    <tempav>1</tempav>
+       #    <id>tag:bbc.co.uk,2008:pips:b00dp4xn</id>
+       #    <service id="bbc_one" href="http://www.bbc.co.uk/iplayer/bbc_one">BBC One</service>
+       #    <masterbrand id="bbc_two" href="http://www.bbc.co.uk/iplayer/bbc_two">BBC Two</masterbrand>
+       #
+       #    <alternate id="signed" />
+       #    <guidance>Contains some strong language.</guidance>
+       #    <mediator identifier="b00dp4xn" name="pips"/>
+       #  </item>
+
+       my $xml = $res->content;        
+       # flatten
+       $xml =~ s/\n/ /g;
+
+       # Get title
+       # <title>Amazon with Bruce Parry: Episode 1</title>
+       my ( $title, $prog_type );
+       $title = $1 if $xml =~ m{<title>\s*(.+?)\s*<\/title>};
+
+       # Get type
+       $prog_type = 'tv' if grep /kind="programme"/, $xml;
+       $prog_type = 'radio' if grep /kind="radioProgramme"/, $xml;
+
+       # Split into <item kind="programme"> sections
+       for ( split /<item\s+kind="(radioProgramme|programme)"/, $xml ) {
+               logger "DEBUG: Block: $_\n" if $opt{debug};
+               my ($verpid, $version);
+               #  duration="3600" identifier="b00dp4xn" group="b00dlrc8" publisher="pips">
+               $verpid = $1 if m{\s+duration=".*?"\s+identifier="(.+?)"};
+               # <alternate id="default" />
+               $version = lc($1) if m{<alternate\s+id="(.+?)"};
+               next if ! ($verpid && $version);
+               $version_pids{$version} = $verpid;
+               logger "INFO: Version: $version, VersionPid: $verpid\n" if $opt{verbose};  
+       }
+
+       # Extract Long Name, e.g.: iplayer.episode.setTitle("DIY SOS: Series 16: Swansea"), Strip off the episode name
+       $title =~ s/^(.+):.*?$/$1/g;
+
+       # Add to prog hash
+       $prog{$pid}{versions} = join ',', keys %version_pids;
+       return ( $prog_type, $title, %version_pids );
+}
+
+
+
+# Gets media streams data for this version pid
+# $media = all|itv|flashhigh|flashnormal|iphone|flashwii|n95_wifi|n95_3g|mobile|flashaudio|realaudio|wma|subtitles
+sub get_media_stream_data {
+       my ( $pid, $verpid, $media ) = @_;
+       my %data;
+
+       # Setup user agent with redirection enabled
+       my $ua = create_ua('desktop');
+       $opt{quiet} = 0 if $opt{streaminfo};
+
+       # ITV streams
+       if ( $prog{$pid}{type} eq 'itv' ) {
+               my $prog_type = 'itv';
+               $data{$prog_type}{type} = 'ITV ASF Video stream';
+               $opt{quiet} = 1 if $opt{streaminfo};
+               $data{$prog_type}{streamurl} = join('|', get_stream_url_itv($ua, $pid) );
+               $opt{quiet} = 0 if $opt{streaminfo};
+
+       # BBC streams
+       } else {
+               my $xml1 = request_url_retry($ua, $media_stream_data_prefix.$verpid, 3, '', '');
+               logger "\n$xml1\n" if $opt{debug};
+               # flatten
+               $xml1 =~ s/\n/ /g;
+               
+               for my $xml ( split /<media/, $xml1 ) {
+                       $xml = "<media".$xml;
+                       my $prog_type;
+                       
+                       # h.264 high quality stream
+                       #       <media kind="video"
+                       #        width="640"
+                       #        height="360"
+                       #        type="video/mp4"
+                       #        encoding="h264"  >
+                       #        <connection
+                       #                priority="10"
+                       #                application="bbciplayertok"
+                       #                kind="level3"
+                       #                server="bbciplayertokfs.fplive.net"
+                       #                identifier="mp4:b000zxf4-H26490898078"
+                       #                authString="d52f77fede048f1ffd6587fd47446dee"
+                       #        />
+                       # application: bbciplayertok
+                       # tcURL: rtmp://bbciplayertokfs.fplive.net:80/bbciplayertok
+                       if ( $media =~ /^(flashhigh|all)$/ && $xml =~ m{<media\s+kind="video".+?type="video/mp4".+?encoding="h264".+?application="(.+?)".+?kind="level3"\s+server="(.+?)"\s+?identifier="(.+?)"\s+?authString="(.+?)"} ) {
+                               $prog_type = 'flashhigh';
+                               logger "DEBUG: Processing $prog_type stream\n" if $opt{verbose};
+                               ( $data{$prog_type}{application}, $data{$prog_type}{server}, $data{$prog_type}{identifier}, $data{$prog_type}{authstring} ) = ( $1, $2, $3, $4 );
+                               $data{$prog_type}{type} = 'Flash RTMP H.264 high quality stream';
+                               $data{$prog_type}{tcurl} = "rtmp://$data{$prog_type}{server}:80/$data{$prog_type}{application}";
+                               $data{$prog_type}{swfurl} = "http://www.bbc.co.uk/emp/9player.swf?revision=7276";
+                               $data{$prog_type}{streamurl} = "rtmp://$data{$prog_type}{server}:1935/ondemand?_fcs_vhost=$data{$prog_type}{server}&auth=$data{$prog_type}{authstring}&aifp=v001&slist=$data{$prog_type}{identifier}";
+                       }
+                       
+                       # h.264 normal quality stream
+                       #       <media kind="video"
+                       #        width="640"
+                       #        height="360"
+                       #        type="video/x-flv"
+                       #        encoding="vp6"  >
+                       #        <connection
+                       #                priority="10"
+                       #                kind="akamai"
+                       #                server="cp41752.edgefcs.net"
+                       #                identifier="secure/b000zxf4-streaming90898078"
+                       #                authString="daEdSdgbcaibFa7biaobCaYdadyaTamazbq-biXsum-cCp-FqrECnEoGBwFvwG"
+                       #        />
+                       #       </media>
+                       #
+                       # application (e.g.):            ondemand?_fcs_vhost=cp41752.edgefcs.net&auth=daEcia8aQaRardxdwb_dCbvc0cPbLavc2cL-bjw5rj-cCp-JnlDCnzn.MEqHpxF&aifp=v001&slist=secure/b000gy717streaming103693754
+                       # tcURL: rtmp://88.221.26.165:80/ondemand?_fcs_vhost=cp41752.edgefcs.net&auth=daEcia8aQaRardxdwb_dCbvc0cPbLavc.2cL-bjw5rj-cCp-JnlDCnznMEqHpxF&aifp=v001&slist=secure/b000gy717streaming103693754
+                       if ( $media =~ /^(flashnormal|all)$/ && $xml =~ m{<media\s+kind="video".+?type="video/x-flv".+?encoding="vp6".+?kind="akamai"\s+server="(.+?)"\s+?identifier="(.+?)"\s+?authString="(.+?)"} ) {
+                               $prog_type = 'flashnormal';
+                               logger "DEBUG: Processing $prog_type stream\n" if $opt{verbose};
+                               ( $data{$prog_type}{server}, $data{$prog_type}{identifier}, $data{$prog_type}{authstring} ) = ( $1, $2, $3 );
+                               $data{$prog_type}{application} = "ondemand?_fcs_vhost=$data{$prog_type}{server}&auth=$data{$prog_type}{authstring}&aifp=v001&slist=$data{$prog_type}{identifier}";
+                               $data{$prog_type}{type} = 'Flash RTMP H.264 normal quality stream';
+                               $data{$prog_type}{tcurl} = "rtmp://$data{$prog_type}{server}:80/$data{$prog_type}{application}";
+                               $data{$prog_type}{swfurl} = "http://www.bbc.co.uk/emp/9player.swf?revision=7276";
+                               $data{$prog_type}{streamurl} = "rtmp://$data{$prog_type}{server}:1935/ondemand?_fcs_vhost=$data{$prog_type}{server}&auth=$data{$prog_type}{authstring}&aifp=v001&slist=$data{$prog_type}{identifier}";
+                       }
+                       
+                       # Wii h.264 standard quality stream
+                       #<media kind="video"
+                       #        width="512"
+                       #        height="288"
+                       #        type="video/x-flv"
+                       #        encoding="spark"  >
+                       #        <connection
+                       #                priority="10"
+                       #                kind="akamai"
+                       #                server="cp41752.edgefcs.net"
+                       #                identifier="secure/5242138581547639062"
+                       #                authString="daEd8dLbGaPaZdzdNcwd.auaydJcxcHandp-biX5YL-cCp-BqsECnxnGEsHwyE"
+                       #        />
+                       #</media>
+                       # application (e.g.):               ondemand?_fcs_vhost=cp41752.edgefcs.net&auth=daEcpc6cYbhdIakdWduc6bJdPbydbazdmdp-bjxPBF-cCp-GptFAoDqJBnHvzC&aifp=v001&slist=secure/b000g884xstreaming101052333
+                       # tcURL: rtmp: //88.221.26.173:1935/ondemand?_fcs_vhost=cp41752.edgefcs.net&auth=daEcpc6cYbhdIakdWduc6bJdPbydbazdmdp-bjxPBF-cCp-GptFAoDqJBnHvzC&aifp=v001&slist=secure/b000g884xstreaming101052333
+                       # swfUrl: http://www.bbc.co.uk/emp/iplayer/7player.swf?revision=3897
+                       if ( $media =~ /^(flashwii|all)$/ && $xml =~ m{<media\s+kind="video".+?type="video/x-flv".+?encoding="spark".+?kind="akamai"\s+server="(.+?)"\s+?identifier="(.+?)"\s+?authString="(.+?)"} ) {
+                               $prog_type = 'flashwii';
+                               logger "DEBUG: Processing $prog_type stream\n" if $opt{verbose};
+                               ( $data{$prog_type}{server}, $data{$prog_type}{identifier}, $data{$prog_type}{authstring} ) = ( $1, $2, $3 );
+                               $data{$prog_type}{application} = "ondemand?_fcs_vhost=$data{$prog_type}{server}&auth=$data{$prog_type}{authstring}&aifp=v001&slist=$data{$prog_type}{identifier}";
+                               $data{$prog_type}{type} = 'Flash RTMP H.264 Wii stream';
+                               $data{$prog_type}{tcurl} = "rtmp://$data{$prog_type}{server}:1935/$data{$prog_type}{application}";
+                               $data{$prog_type}{swfurl} = "http://www.bbc.co.uk/emp/iplayer/7player.swf?revision=3897";
+                               $data{$prog_type}{streamurl} = "rtmp://$data{$prog_type}{server}:1935/ondemand?_fcs_vhost=$data{$prog_type}{server}&auth=$data{$prog_type}{authstring}&aifp=v001&slist=$data{$prog_type}{identifier}";
+                       }
+               
+                       # iPhone h.264/mp3 stream
+                       #<media kind="video"
+                       #        width="480"
+                       #        height="272"
+                       #        type="video/mp4"
+                       #        encoding="h264"  >
+                       #        <connection
+                       #                priority="10"
+                       #                kind="sis"
+                       #                server="http://www.bbc.co.uk/mediaselector/3/auth/stream/"
+                       #                identifier="5242138581547639062"
+                       #                href="http://www.bbc.co.uk/mediaselector/3/auth/stream/5242138581547639062.mp4"
+                       #        />
+                       #</media>
+                       if ( $media =~ /^(iphone|all)$/ && $xml =~ m{<media\s+kind="video".+?type="video/mp4".+?encoding="h264".+?kind="sis"\s+server="(.+?)"\s+?identifier="(.+?)"\s+?href="(.+?)"} ) {
+                               $prog_type = 'iphone';
+                               logger "DEBUG: Processing $prog_type stream\n" if $opt{verbose};
+                               ( $data{$prog_type}{server}, $data{$prog_type}{identifier}, $data{$prog_type}{streamurl} ) = ( $1, $2, $3 );
+                               $data{$prog_type}{type} = 'iPhone stream';
+                       }
+               
+                       # Nokia N95 h.264 low quality stream (WiFi)
+                       #<media kind="video"
+                       #        type="video/mpeg"
+                       #        encoding="h264"  >
+                       #        <connection
+                       #                priority="10"
+                       #                kind="sis"
+                       #                server="http://www.bbc.co.uk/mediaselector/4/sdp/"
+                       #                identifier="b00108ld/iplayer_streaming_n95_wifi"
+                       #                href="http://www.bbc.co.uk/mediaselector/4/sdp/b00108ld/iplayer_streaming_n95_wifi"
+                       #        />
+                       #</media>
+                       if ( $media =~ /^(n95_wifi|all)$/ && $xml =~ m{<media\s+kind="video".+?type="video/mpeg".+?encoding="h264".+?kind="sis"\s+server="(.+?)"\s+?identifier="(.+?)"\s+?href="(.+?)"} ) {
+                               $prog_type = 'n95_wifi';
+                               logger "DEBUG: Processing $prog_type stream\n" if $opt{verbose};
+                               ( $data{$prog_type}{server}, $data{$prog_type}{identifier}, $data{$prog_type}{href} ) = ( $1, $2, $3 );
+                               $data{$prog_type}{type} = 'Nokia N95 h.264 low quality WiFi stream';
+                               $opt{quiet} = 1 if $opt{streaminfo};
+                               chomp( $data{$prog_type}{streamurl} = request_url_retry($ua, $data{$prog_type}{href}, 2, '', '') );
+                               $opt{quiet} = 0 if $opt{streaminfo};
+                       }
+               
+                       # Nokia N95 h.264 low quality stream (3G)
+                       #<media kind="" 
+                       #        expires="2008-10-30T12:29:00+00:00"
+                       #        type="video/mpeg"                  
+                       #        encoding="h264"  >                 
+                       #        <connection                        
+                       #                priority="10"              
+                       #                kind="sis"                 
+                       #                server="http://www.bbc.co.uk/mediaselector/4/sdp/" 
+                       #                identifier="b009tzxx/iplayer_streaming_n95_3g"     
+                       #                href="http://www.bbc.co.uk/mediaselector/4/sdp/b009tzxx/iplayer_streaming_n95_3g" 
+                       #        />                                                                                        
+                       #</media>    
+                       if ( $media =~ /^(n95_3g|all)$/ && $xml =~ m{<media\s+kind="".+?type="video/mpeg".+?encoding="h264".+?kind="sis"\s+server="(.+?)"\s+?identifier="(.+?)"\s+?href="(.+?)"} ) {
+                               $prog_type = 'n95_3g';
+                               logger "DEBUG: Processing $prog_type stream\n" if $opt{verbose};
+                               ( $data{$prog_type}{server}, $data{$prog_type}{identifier}, $data{$prog_type}{href} ) = ( $1, $2, $3 );
+                               $data{$prog_type}{type} = 'Nokia N95 h.264 low quality 3G stream';
+                               $opt{quiet} = 1 if $opt{streaminfo};
+                               chomp( $data{$prog_type}{streamurl} = request_url_retry($ua, $data{$prog_type}{href}, 2, '', '') );
+                               $opt{quiet} = 0 if $opt{streaminfo};
+                       }
+               
+                       # Mobile WMV DRM
+                       #<media kind="video"
+                       #        expires="2008-10-20T21:59:00+01:00"
+                       #        type="video/wmv"   >
+                       #        <connection
+                       #                priority="10"
+                       #                kind="licence"
+                       #                server="http://iplayldsvip.iplayer.bbc.co.uk/WMLicenceIssuer/LicenceDelivery.asmx"
+                       #                identifier="0A1CA43B-98A8-43EA-B684-DA06672C0575"
+                       #                href="http://iplayldsvip.iplayer.bbc.co.uk/WMLicenceIssuer/LicenceDelivery.asmx/0A1CA43B-98A8-43EA-B684-DA06672C0575"
+                       #        />
+                       #<connection
+                       #                priority="10"
+                       #                kind="sis"
+                       #                server="http://directdl.iplayer.bbc.co.uk/windowsmedia/"
+                       #                identifier="AmazonwithBruceParry_Episode5_200810132100_mobile"
+                       #                href="http://directdl.iplayer.bbc.co.uk/windowsmedia/AmazonwithBruceParry_Episode5_200810132100_mobile.wmv"
+                       #        />
+                       #</media>
+                       if ( $media =~ /^(mobile|all)$/ && $xml =~ m{<media\s+kind="video".+?type="video/wmv".+?kind="sis"\s+server="(.+?)"\s+?identifier="(.+?)"\s+?href="(.+?)"} ) {
+                               $prog_type = 'mobile';
+                               logger "DEBUG: Processing $prog_type stream\n" if $opt{verbose};
+                               ( $data{$prog_type}{server}, $data{$prog_type}{identifier}, $data{$prog_type}{streamurl} ) = ( $1, $2, $3 );
+                               $data{$prog_type}{type} = 'Mobile WMV DRM stream';
+                       }
+               
+                       # Audio rtmp mp3
+                       #<media kind="audio"
+                       #        type="audio/mpeg"
+                       #        encoding="mp3"  >
+                       #        <connection
+                       #                priority="10"
+                       #                kind="akamai"
+                       #                server="cp48181.edgefcs.net"
+                       #                identifier="mp3:secure/radio1/RBN2_mashup_b00d67h9_2008_09_05_22_14_25"
+                       #                authString="daEbQa1c6cda6aHdudxagcCcUcVbvbncmdK-biXtzq-cCp-DnoFIpznNBqHnzF"
+                       #        />
+                       #</media>
+                       #app: ondemand?_fcs_vhost=cp48181.edgefcs.net&auth=daEasducLbidOancObacmc0amd6d7ana8c6-bjx.9v-cCp-JqlFHoEq.FBqGnxC&aifp=v001&slist=secure/radio1/RBN2_radio_1_-_wednesday_1000_b00g3xcj_2008_12_31_13_21_49
+                       #swfUrl: http://www.bbc.co.uk/emp/9player.swf?revision=7276
+                       #tcUrl: rtmp://92.122.210.173:1935/ondemand?_fcs_vhost=cp48181.edgefcs.net&auth=daEasducLbidOancObacmc0amd6d7ana8c6-bjx.9v-cCp-JqlFHoEqFBqGnxC&aifp=v001&slist=secure/radio1/RBN2_radio_1_-_wednesday_1.000_b00g3xcj_2008_12_31_13_21_49
+                       #pageUrl: http://www.bbc.co.uk/iplayer/episode/b00g3xp7/Annie_Mac_31_12_2008/
+                       if ( $media =~ /^(flashaudio|all)$/ && $xml =~ m{<media\s+kind="audio".+?type="audio/mpeg".+?encoding="mp3".+?kind="akamai"\s+server="(.+?)"\s+?identifier="(.+?)"\s+?authString="(.+?)"} ) {
+                               $prog_type = 'flashaudio';
+                               logger "DEBUG: Processing $prog_type stream\n" if $opt{verbose};
+                               ( $data{$prog_type}{server}, $data{$prog_type}{identifier}, $data{$prog_type}{authstring} ) = ( $1, $2, $3 );
+                               $data{$prog_type}{streamurl} = "rtmp://$data{$prog_type}{server}:1935/ondemand?_fcs_vhost=$data{$prog_type}{server}&auth=$data{$prog_type}{authstring}&aifp=v001&slist=$data{$prog_type}{identifier}";
+                               # Remove offending mp3: at the start of the identifier (don't remove in stream url)
+                               $data{$prog_type}{identifier} =~ s/^mp3://;
+                               $data{$prog_type}{application} = "ondemand?_fcs_vhost=$data{$prog_type}{server}&auth=$data{$prog_type}{authstring}&aifp=v001&slist=$data{$prog_type}{identifier}";
+                               $data{$prog_type}{type} = 'RTMP MP3 stream';
+                               $data{$prog_type}{tcurl} = "rtmp://$data{$prog_type}{server}:1935/$data{$prog_type}{application}";
+                               $data{$prog_type}{swfurl} = "http://www.bbc.co.uk/emp/9player.swf?revision=7276";
+                       }
+               
+                       # RealAudio stream
+                       #<media kind="audio"
+                       #       type="audio/real"
+                       #        encoding="real"  >
+                       #        <connection
+                       #                priority="10"
+                       #                kind="sis"
+                       #                server="http://www.bbc.co.uk"
+                       #                identifier="/radio/aod/playlists/9h/76/d0/0b/2000_bbc_radio_one"
+                       #                href="http://www.bbc.co.uk/radio/aod/playlists/9h/76/d0/0b/2000_bbc_radio_one.ram"
+                       #        />
+                       #</media>
+                       # Realaudio for worldservice    
+                       #<media kind=""
+                       #type="audio/real"
+                       #encoding="real"  >
+                       #<connection
+                       #        priority="10"
+                       #        kind="edgesuite"
+                       #        server="http://http-ws.bbc.co.uk.edgesuite.net"
+                       #        identifier="/generatecssram.esi?file=/worldservice/css/nb/410060838.ra"
+                       #        href="http://http-ws.bbc.co.uk.edgesuite.net/generatecssram.esi?file=/worldservice/css/nb/410060838.ra"
+                       #/>
+                       #</media>
+                       #</mediaSelection>
+                       if ( $media =~ /^(realaudio|all)$/ && $xml =~ m{<media\s+kind="(audio|)".+?type="audio/real".+?encoding="real".+?kind="(sis|edgesuite)"\s+server="(.+?)"\s+?identifier="(.+?)"\s+?href="(.+?)"} ) {
+                               $prog_type = 'realaudio';
+                               logger "DEBUG: Processing $prog_type stream\n" if $opt{verbose};
+                               ( $data{$prog_type}{server}, $data{$prog_type}{identifier}, $data{$prog_type}{href} ) = ( $3, $4, $5 );
+                               $data{$prog_type}{type} = 'RealAudio RTSP stream';
+                               $opt{quiet} = 1 if $opt{streaminfo};
+                               chomp( $data{$prog_type}{streamurl} = request_url_retry($ua, $data{$prog_type}{href}, 2, '', '') );
+                               $data{$prog_type}{streamurl} =~ s/[\s\n]//g;
+                               $opt{quiet} = 0 if $opt{streaminfo};
+                       }
+               
+                       # Radio WMA (low quality)
+                       #<mediaSelection xmlns="http://bbc.co.uk/2008/mp/mediaselection">
+                       #<media kind=""
+                       #        type="audio/wma"
+                       #        encoding="wma"  >
+                       #        <connection
+                       #                priority="10"
+                       #                kind="edgesuite"
+                       #                server="http://http-ws.bbc.co.uk.edgesuite.net"
+                       #                identifier="/generatecssasx.esi?file=/worldservice/css/nb/410060838"
+                       #                href="http://http-ws.bbc.co.uk.edgesuite.net/generatecssasx.esi?file=/worldservice/css/nb/410060838.wma"
+                       #        />
+                       #</media>
+                       if ( $media =~ /^(wma|all)$/ && $xml =~ m{<media\s+kind="(audio|)".+?type="audio/wma".+?encoding="wma".+?kind="(sis|edgesuite)"\s+server="(.+?)"\s+?identifier="(.+?)"\s+?href="(.+?)"} ) {
+                               $prog_type = 'wma';
+                               logger "DEBUG: Processing $prog_type stream\n" if $opt{verbose};
+                               ( $data{$prog_type}{server}, $data{$prog_type}{identifier}, $data{$prog_type}{href} ) = ( $3, $4, $5 );
+                               $data{$prog_type}{type} = 'WMA MMS stream';
+                               $opt{quiet} = 1 if $opt{streaminfo};
+                               chomp( $data{$prog_type}{streamurl} = request_url_retry($ua, $data{$prog_type}{href}, 2, '', '') );
+                               $data{$prog_type}{streamurl} =~ s/[\s\n]//g;
+                               # HREF="mms://a1899.v394403.c39440.g.vm.akamaistream.net/7/1899/39440/1/bbcworldservice.download.akamai.com/39440//worldservice/css/nb/410060838.wma"
+                               $data{$prog_type}{streamurl} =~ s/^.*href=\"(.+?)\".*$/$1/gi;
+                               $opt{quiet} = 0 if $opt{streaminfo};
+                       }
+               
+                       # Subtitles stream
+                       #<media kind="captions"
+                       #        type="application/ttaf+xml"   >
+                       #        <connection
+                       #                priority="10"
+                       #                kind="http"
+                       #                server="http://www.bbc.co.uk/iplayer/subtitles/"
+                       #                identifier="b0008dc8rstreaming89808204.xml"
+                       #                href="http://www.bbc.co.uk/iplayer/subtitles/b0008dc8rstreaming89808204.xml"
+                       #        />
+                       #</media>
+                       if ( $media =~ /^(subtitles|all)$/ && $xml =~ m{<media\s+kind="captions".+?type="application/ttaf\+xml".+?kind="http"\s+server="(.+?)"\s+?identifier="(.+?)"\s+?href="(.+?)"} ) {
+                               $prog_type = 'subtitles';
+                               logger "DEBUG: Processing $prog_type stream\n" if $opt{verbose};
+                               ( $data{$prog_type}{server}, $data{$prog_type}{identifier}, $data{$prog_type}{streamurl} ) = ( $1, $2, $3 );
+                               $data{$prog_type}{type} = 'Subtitles stream';
+                       }       
+               }
+               # Do iphone redirect check regardless of an xml entry for iphone - sometimes the iphone streams exist regardless
+               if ( my $streamurl = get_stream_url_iphone($ua, $verpid) ) {
+                       my $prog_type = 'iphone';
+                       $data{$prog_type}{type} = 'iPhone stream';
+                       # Get iphone redirect
+                       $data{$prog_type}{streamurl} = $streamurl;
+               } else {
+                       logger "DEBUG: No iphone redirect stream\n" if $opt{verbose};
+               }
+
+       }
+       # Return a hash with media => url if 'all' is specified - otherwise just the specified url
+       if ( $media eq 'all' ) {
+               return %data;
+       } else {
+               # Make sure this hash exists before we pass it back...
+               $data{$media}{exists} = 0 if not defined $data{$media};
+               return $data{$media};
+       }
+}
+
+
+
+sub display_stream_info {
+       my ($pid, $verpid, $media) = (@_);
+       logger "INFO: Getting media stream metadata for $prog{$pid}{name} - $prog{$pid}{episode}, $verpid\n" if $pid;
+       my %data = get_media_stream_data( $pid, $verpid, $media);
+       # Print out stream data
+       for my $prog_type (sort keys %data) {
+               logger "stream:     $prog_type\n";
+               for my $entry ( sort keys %{ $data{$prog_type} } ) {
+                       logger sprintf("%-11s %s\n", $entry.':', $data{$prog_type}{$entry} );
+               }
+               logger "\n";
+       }
+       return 0;
+}
+
+
+
+# Displays specified metadata from supplied hash
+# Usage: display_metadata( <hashref>, <array of elements to display> )
+sub display_metadata {
+       my %data = %{$_[0]};
+       shift;
+       my @keys = @_;
+       @keys = keys %data if $#_ < 0;
+       logger "\n";
+       for (@keys) {
+               logger sprintf "%-15s %s\n", ucfirst($_).':', $data{$_} if $data{$_};
+       }
+       return 0;
+}
+
+
+
+# Actually do the h.264/mp3 downloading
+# ( $ua, $pid, $url_2, $file, $file_done, '0|1 == rearrange moov' )
+sub download_stream_iphone {
+       my ( $ua, $url_2, $pid, $file, $file_done, $file_symlink, $rearrange ) = @_;
+
+       # Stage 3a: Download 1st byte to get exact file length
+       logger "INFO: Stage 3 URL = $url_2\n" if $opt{verbose};
+
+       # Override the $rearrange value is --raw option is specified
+       $rearrange = 0 if $opt{raw};
+       
+       # Setup request header
+       my $h = new HTTP::Headers(
+               'User-Agent'    => $user_agent{coremedia},
+               'Accept'        => '*/*',
+               'Range'         => 'bytes=0-1',
+       );
+       my $req = HTTP::Request->new ('GET', $url_2, $h);
+       my $res = $ua->request($req);
+       # e.g. Content-Range: bytes 0-1/181338136 (return if no content length returned)
+       my $download_len = $res->header("Content-Range");
+       if ( ! $download_len ) {
+               #logger "ERROR: No Content-Range was obtained\n" if $opt{verbose};
+               logger "WARNING: iphone version not available\n";
+               return 'retry'
+       }
+       $download_len =~ s|^bytes 0-1/(\d+).*$|$1|;
+       logger "INFO: Download File Length $download_len\n" if $opt{verbose};
+
+       # Only do this if we're rearranging QT streams
+       my $mdat_start = 0;
+       # default to this if we are not rearranging (tells the download chunk loop where to stop - i.e. EOF instead of end of mdat atom)
+       my $moov_start = $download_len + 1;
+       my $header;
+       if ($rearrange) {
+               # Get ftyp+wide header etc
+               $mdat_start = 0x1c;
+               my $buffer = download_block(undef, $url_2, $ua, 0, $mdat_start + 4);
+               # Get bytes upto (but not including) mdat atom start -> $header
+               $header = substr($buffer, 0, $mdat_start);
+               
+               # Detemine moov start
+               # Get mdat_length_chars from downloaded block
+               my $mdat_length_chars = substr($buffer, $mdat_start, 4);
+               my $mdat_length = bytestring_to_int($mdat_length_chars);
+               logger "DEBUG: mdat_length = ".get_hex($mdat_length_chars)." = $mdat_length\n" if $opt{debug};
+               logger "DEBUG: mdat_length (decimal) = $mdat_length\n" if $opt{debug};
+               # The MOOV box starts one byte after MDAT box ends
+               $moov_start = $mdat_start + $mdat_length;
+       }
+
+       # If we have partial content and wish to stream, resume the download & spawn off STDOUT from existing file start 
+       # Sanity check - we cannot support downloading of partial content if we're streaming also. 
+       if ( $opt{stdout} && (! $opt{nowrite}) && -f $file ) {
+               logger "WARNING: Partially downloaded file exists, streaming will start from the beginning of the programme\n";
+               # Don't do usual streaming code - also force all messages to go to stderr
+               delete $opt{stdout};
+               $opt{stderr} = 1;
+               $childpid = fork();
+               if (! $childpid) {
+                       # Child starts here
+                       logger "INFO: Streaming directly for partially downloaded file $file\n";
+                       if ( ! open( STREAMIN, "< $file" ) ) {
+                               logger "INFO: Cannot Read partially downloaded file to stream\n";
+                               exit 4;
+                       }
+                       my $outbuf;
+                       # Write out until we run out of bytes
+                       my $bytes_read = 65536;
+                       while ( $bytes_read == 65536 ) {
+                               $bytes_read = read(STREAMIN, $outbuf, 65536 );
+                               #logger "INFO: Read $bytes_read bytes\n";
+                               print STDOUT $outbuf;
+                       }
+                       close STREAMIN;
+                       logger "INFO: Stream thread has completed\n";
+                       exit 0;
+               }
+       }
+
+       # Open file if required
+       my $fh = open_file_append($file);
+
+       # If the partial file already exists, then resume from the correct mdat/download offset
+       my $restart_offset = 0;
+       my $moovdata;
+       my $moov_length = 0;
+
+       if ($rearrange) {
+               # if cookie fails then trigger a retry after deleting cookiejar
+               # Determine orginal moov atom length so we can work out if the partially downloaded file has the moov atom in it already
+               $moov_length = bytestring_to_int( download_block( undef, $url_2, $ua, $moov_start, $moov_start+3 ) );
+               logger "INFO: original moov atom length = $moov_length                          \n" if $opt{verbose};
+               # Sanity check this moov length - chances are that were being served up a duff file if this is > 10% of the file size or < 64k
+               if ( $moov_length > (${moov_start}/9.0) || $moov_length < 65536 ) {
+                       logger "WARNING: Bad file download, deleting cookie                 \n";
+                       $ua->cookie_jar( HTTP::Cookies->new( file => $cookiejar, autosave => 0, ignore_discard => 0 ) );
+                       unlink $cookiejar;
+                       unlink $file;
+                       return 'retry';
+               }
+
+               # we still need an accurate moovlength for the already downloaded moov atom for resume restart_offset.....
+               # If we have no existing file, a file which doesn't yet even have the moov atom, or using stdout (or no-write option)
+               # (allow extra 1k on moov_length for metadata when testing)
+               if ( $opt{stdout} || $opt{nowrite} || stat($file)->size < ($moov_length+$mdat_start+1024) ) {
+                       # get moov chunk into memory
+                       $moovdata = download_block( undef, $url_2, $ua, $moov_start, (${download_len}-1) );
+
+                       # Create new udta atom with child atoms for metadata
+                       my $udta_new = create_qt_atom('udta',
+                               create_qt_atom( chr(0xa9).'nam', $prog{$pid}{name}.' - '.$prog{$pid}{episode}, 'string' ).
+                               create_qt_atom( chr(0xa9).'alb', $prog{$pid}{name}, 'string' ).
+                               create_qt_atom( chr(0xa9).'trk', $prog{$pid}{episode}, 'string' ).
+                               create_qt_atom( chr(0xa9).'aut', $prog{$pid}{channel}, 'string' ).
+                               create_qt_atom( chr(0xa9).'ART', $prog{$pid}{channel}, 'string' ).
+                               create_qt_atom( chr(0xa9).'des', $prog{$pid}{desc}, 'string' ).
+                               create_qt_atom( chr(0xa9).'cmt', 'Downloaded with get_iplayer', 'string' ).
+                               create_qt_atom( chr(0xa9).'req', 'QuickTime 6.0 or greater', 'string' ).
+                               create_qt_atom( chr(0xa9).'day', (localtime())[5] + 1900, 'string' )
+                       );
+                       # Insert new udta atom over the old one and get the new $moov_length (and update moov atom size field)
+                       replace_moov_udta_atom ( $udta_new, $moovdata );
+
+                       # Process the moov data so that we can relocate it (change the chunk offsets that are absolute)
+                       # Also update moov+_length to be accurate after metadata is added etc
+                       $moov_length = relocate_moov_chunk_offsets( $moovdata );
+                       logger "INFO: New moov atom length = $moov_length                          \n" if $opt{verbose};
+                       # write moov atom to file next (yes - were rearranging the file - header+moov+mdat - not header+mdat+moov)
+                       logger "INFO: Appending ftype+wide+moov atoms to $file\n" if $opt{verbose};
+                       # Write header atoms (ftyp, wide)
+                       print $fh $header if ! $opt{nowrite};
+                       print STDOUT $header if $opt{stdout};
+                       # Write moov atom
+                       print $fh $moovdata if ! $opt{nowrite};
+                       print STDOUT $moovdata if $opt{stdout};
+                       # If were not resuming we want to only start the download chunk loop from mdat_start 
+                       $restart_offset = $mdat_start;
+               }
+
+               # Get accurate moov_length from file (unless stdout or nowrite options are specified)
+               # Assume header+moov+mdat atom layout
+               if ( (! $opt{stdout}) && (! $opt{nowrite}) && stat($file)->size > ($moov_length+$mdat_start) ) {
+                               logger "INFO: Getting moov atom length from partially downloaded file $file\n" if $opt{verbose};
+                               if ( ! open( MOOVDATA, "< $file" ) ) {
+                                       logger "ERROR: Cannot Read partially downloaded file\n";
+                                       return 4;
+                               }
+                               my $data;
+                               seek(MOOVDATA, $mdat_start, 0);
+                               if ( read(MOOVDATA, $data, 4, 0) != 4 ) {
+                                       logger "ERROR: Cannot Read moov atom length from partially downloaded file\n";
+                                       return 4;
+                               }
+                               close MOOVDATA;
+                               # Get moov atom size from file
+                               $moov_length = bytestring_to_int( substr($data, 0, 4) );
+                               logger "INFO: moov atom length (from partially downloaded file) = $moov_length                          \n" if $opt{verbose};
+               }
+       }
+
+       # If we have a too-small-sized file (greater than moov_length+mdat_start) and not stdout and not no-write then this is a partial download
+       if (-f $file && (! $opt{stdout}) && (! $opt{nowrite}) && stat($file)->size > ($moov_length+$mdat_start) ) {
+               # Calculate new start offset (considering that we've put moov first in file)
+               $restart_offset = stat($file)->size - $moov_length;
+               logger "INFO: Resuming download from $restart_offset                        \n";
+       }
+
+       # Create symlink if required
+       if ( $opt{symlink} ) {
+               # remove old symlink
+               unlink $file_symlink if -l $file_symlink;
+               symlink $file, $file_symlink;
+               logger "INFO: Created symlink from '$file_symlink' -> '$file'\n" if $opt{verbose};
+       }
+
+       # Start marker
+       my $start_time = time();
+
+       # Download mdat in blocks
+       my $chunk_size = $iphone_block_size;
+       for ( my $s = $restart_offset; $s < ${moov_start}-1; $s+= $chunk_size ) {
+               # get mdat chunk into file
+               my $retcode;
+               my $e;
+               # Get block end offset
+               if ( ($s + $chunk_size - 1) > (${moov_start}-1) ) {
+                       $e = $moov_start - 1;
+               } else {
+                       $e = $s + $chunk_size - 1;
+               }
+               # Get block from URL and append to $file
+               if ( download_block($file, $url_2, $ua, $s, $e, $download_len, $fh ) ) {
+                       logger "ERROR: Could not download block $s - $e from $file\n\n";
+                       return 'retry';
+               }
+       }
+
+       # end marker
+       my $end_time = time();
+
+       # Calculate average speed, duration and total bytes downloaded
+       logger sprintf("INFO: Downloaded %.2fMB in %s at %5.0fkbps to %s\n", 
+               ($moov_start - 1 - $restart_offset) / (1024.0 * 1024.0),
+               sprintf("%02d:%02d:%02d", ( gmtime($end_time - $start_time))[2,1,0] ), 
+               ( $moov_start - 1 - $restart_offset ) / ($end_time - $start_time) / 1024.0 * 8.0, 
+               $file_done );
+
+       # Moving file into place as complete (if not stdout)
+       move($file, $file_done) if ! $opt{stdout};
+
+       # Re-symlink file
+       if ( $opt{symlink} ) {
+               # remove old symlink
+               unlink $file_symlink if -l $file_symlink;
+               symlink $file_done, $file_symlink;
+               logger "INFO: Created symlink from '$file_symlink' -> '$file_done'\n" if $opt{verbose};
+       }
+       $prog{$pid}{mode} = 'iphone';
+       return 0;
+}
+
+
+
+# Actually do the RTMP stream downloading
+sub download_stream_rtmp {
+       my ( $ua, $url_2, $pid, $mode, $application, $tcurl, $authstring, $swfurl, $file, $file_done, $file_symlink ) = @_;
+       my $file_tmp;
+       my $cmd;
+       
+       if ( $opt{raw} ) {
+               $file_tmp = $file;
+       } else {
+               $file_tmp = $file.'.flv'
+       }
+
+       # Remove failed file download (below a certain size) - hack to get around rtmpdump not returning correct exit code
+       if ( -f $file_tmp && stat($file_tmp)->size < $min_download_size ) {
+               unlink( $file_tmp );
+       }
+               
+       logger "INFO: RTMP_URL: $url_2, tcUrl: $tcurl, application: $application, authString: $authstring, swfUrl: $swfurl, file: $file, file_done: $file_done\n" if $opt{verbose};
+
+       # Create symlink if required
+       if ( $opt{symlink} ) {
+               # remove old symlink
+               unlink $file_symlink if -l $file_symlink;
+               symlink $file_tmp, $file_symlink;
+               logger "INFO: Created symlink from '$file_symlink' -> '$file_tmp'\n" if $opt{verbose};
+       }
+       $cmd = "$rtmpdump --resume --rtmp \"$url_2\" --auth \"$authstring\" --swfUrl \"$swfurl\" --tcUrl \"$tcurl\" --app \"$application\" -o \"$file_tmp\" 1>&2";
+       logger "\n\nINFO: Command: $cmd\n";
+       my $return = system($cmd) >> 8;
+       logger "INFO: Command exit code = $return\n" if $opt{verbose};
+       # Hack to get around rtmpdump prentending to fail on successful flash downloads
+       if ( (! -f $file_tmp) || ($return && -f $file_tmp && stat($file_tmp)->size < $min_download_size) ) {
+               logger "INFO: Command: $cmd\n" if $opt{verbose};
+               logger "WARNING: Failed to download file $file_tmp via RTMP\n";
+               unlink $file_tmp;
+               return 'next';
+       }
+       
+       # Retain raw flv format if required
+       if ( $opt{raw} ) {
+               move($file_tmp, $file_done) if ! $opt{stdout};
+               return 0;
+
+       # Convert flv to mp3
+       } elsif ( $mode eq 'flashaudio' ) {
+               # We could do id3 tagging here with ffmpeg but id3v2 does this later anyway
+               # This fails
+               #$cmd = "$ffmpeg -i \"$file_tmp\" -vn -acodec copy -y \"$file\" 1>&2";
+               # This works but it's really bad bacause it re-transcodes mp3 and takes forever :-(
+               # $cmd = "$ffmpeg -i \"$file_tmp\" -acodec libmp3lame -ac 2 -ab 128k -vn -y \"$file\" 1>&2";
+               # At last this removes the flv container and dumps the mp3 stream! - mplayer dumps core but apparently succeeds
+               $cmd = "$mplayer $mplayer_opts -dumpaudio \"$file_tmp\" -dumpfile \"$file\" 1>&2";
+       # Convert video flv to mp4/avi if required
+       } else {
+               $cmd = "$ffmpeg $ffmpeg_opts -i \"$file_tmp\" -vcodec copy -acodec copy -f $prog{$pid}{ext} -y \"$file\" 1>&2";
+       }
+
+       logger "\n\nINFO: Command: $cmd\n\n" if $opt{verbose};
+       # Run flv conversion and delete source file on success
+       my $return = system($cmd) >> 8;
+       logger "INFO: Command exit code = $return\n" if $opt{verbose};
+       if ( (! $return) && -f $file && stat($file)->size > $min_download_size ) {
+                       unlink( $file_tmp );
+
+       # If the ffmpeg conversion failed, remove the failed-converted file attempt - move the file as done anyway
+       } else {
+               logger "WARNING: flv conversion failed - retaining flv file\n";
+               unlink $file;
+               $file = $file_tmp;
+               $file_done = $file_tmp;
+       }
+       # Moving file into place as complete (if not stdout)
+       move($file, $file_done) if ! $opt{stdout};
+       
+       # Re-symlink file
+       if ( $opt{symlink} ) {
+               # remove old symlink
+               unlink $file_symlink if -l $file_symlink;
+               symlink $file_done, $file_symlink;
+               logger "INFO: Created symlink from '$file_symlink' -> '$file_done'\n" if $opt{verbose};
+       }
+
+       logger "INFO: Downloaded $file_done\n";
+       $prog{$pid}{mode} = $mode;
+       return 0;
+}
+
+
+
+# Actually do the MMS video stream downloading
+sub download_stream_mms_video {
+       my ( $ua, $urls, $file, $file_done, $pid ) = @_;
+       my $file_tmp;
+       my $cmd;
+       my @url_list = split /\|/, $urls;
+       my @file_tmp_list;
+       my %threadpid;
+       my $retries = 3;
+               
+       logger "INFO: MMS_URLs: ".(join ', ', @url_list).", file: $file, file_done: $file_done\n" if $opt{verbose};
+
+       # Start marker
+       my $start_time = time();
+       # Download each mms url (multi-threaded to download in parallel)
+       my $file_part_prefix = "$prog{$pid}{dir}/$prog{$pid}{fileprefix}_part";
+       for ( my $count = 0; $count <= $#url_list; $count++ ) {
+
+               # Create temp download filename
+               $file_tmp = $file_part_prefix.($count+1).".asf";
+               $file_tmp_list[$count] = $file_tmp;
+               #my $null;
+               #$null = '-really-quiet' if ! $opt{quiet};
+               # Can also use 'mencoder mms://url/ -oac copy -ovc copy -o out.asf' - still gives zero exit code on failed stream...
+               # $cmd = "$mplayer $mplayer_opts -dumpstream \"$url_list[$count]\" -dumpfile \"$file_tmp\" $null 1>&2";
+               # Use backticks to invoke mplayer and grab all output then grep for 'read error'
+               $cmd = "$mplayer $mplayer_opts -dumpstream \"$url_list[$count]\" -dumpfile \"$file_tmp\" 2>&1";
+               logger "INFO: Command: $cmd\n" if $opt{verbose};
+
+               my $childpid = fork();
+               if (! $childpid) {
+                       # Child starts here
+                       logger "INFO: Downloading file $file_tmp\n";
+                       # Remove old file
+                       unlink $file_tmp;
+                       # Retry loop
+                       my $retry = $retries;
+                       while ($retry) {
+                               my $cmdoutput = `$cmd`;
+                               my $exitcode = $?;
+                               logger "DEBUG: Command '$cmd', Output:\n$cmdoutput\n\n" if $opt{debug};
+                               if ( grep /(read error|connect error|Failed, exiting)/i, $cmdoutput || $exitcode ) {
+                                       # Failed, retry
+                                       logger "WARNING: Failed, retrying to download $file_tmp, exit code: $exitcode\n";
+                                       $retry--;
+                               } else {
+                                       # Successfully downloaded
+                                       logger "INFO: Download thread has completed for file $file_tmp\n";
+                                       exit 0;
+                               }
+                       }
+                       logger "ERROR: Download thread failed after $retries retries for $file_tmp (renamed to ${file_tmp}.failed)\n";
+                       move $file_tmp, "${file_tmp}.failed";
+                       exit 1;
+               }
+               # Create a hash of process_id => 'count'
+               $threadpid{$childpid} = $count;
+       }
+       # Wait for all threads to complete
+       $| = 1;
+       # Autoreap zombies
+       $SIG{CHLD}='IGNORE';
+       my $done = 0;
+       while (keys %threadpid) {
+               my @sizes;
+               my $total_size = 0;
+               my $total_size_new = 0;
+               my $format = "Threads: ";
+               sleep 1;
+               #logger "DEBUG: ProcessIDs: ".(join ',', keys %threadpid)."\n";
+               for my $procid (sort keys %threadpid) {
+                       my $size = 0;
+                       # Is this child still alive?
+                       if ( kill 0 => $procid ) {
+                               logger "DEBUG Thread $threadpid{$procid} still alive ($file_tmp_list[$threadpid{$procid}])\n" if $opt{debug};
+                               # Build the status string
+                               $format .= "%d) %.3fMB   ";
+                               $size = stat($file_tmp_list[$threadpid{$procid}])->size if -f $file_tmp_list[$threadpid{$procid}];
+                               push @sizes, $threadpid{$procid}+1, $size/(1024.0*1024.0);
+                               $total_size_new += $size;
+                       # Thread has completed/failed
+                       } else {
+                               $size = stat($file_tmp_list[$threadpid{$procid}])->size if -f $file_tmp_list[$threadpid{$procid}];
+                               # end marker
+                               my $end_time = time();
+                               # Calculate average speed, duration and total bytes downloaded
+                               logger sprintf("INFO: Thread #%d Downloaded %.2fMB in %s at %5.0fkbps to %s\n", 
+                                       ($threadpid{$procid}+1),
+                                       $size / (1024.0 * 1024.0),
+                                       sprintf("%02d:%02d:%02d", ( gmtime($end_time - $start_time))[2,1,0] ), 
+                                       $size / ($end_time - $start_time) / 1024.0 * 8.0,
+                                       $file_tmp_list[$threadpid{$procid}] );
+                               # Remove from thread test list
+                               delete $threadpid{$procid};
+                       }
+               }
+               $format .= " downloaded (%.0fkbps)        \r";
+               logger sprintf $format, @sizes, ($total_size_new - $total_size) / (time() - $start_time) / 1024.0 * 8.0;
+       }
+       logger "INFO: All download threads completed\n";        
+       # Unset autoreap
+       delete $SIG{CHLD};
+
+       # If not all files > min_size then assume download failed
+       for (@file_tmp_list) {
+               # If file doesnt exist or too small then skip
+               if ( (! -f $_) || ( -f $_ && stat($_)->size < $min_download_size ) ) {
+                       logger "ERROR: Download of programme failed, skipping\n" if $opt{verbose};
+                       return 1;
+               }
+       }
+
+       $prog{$pid}{mode} = 'itv';
+       # Retain raw format if required
+       if ( $opt{raw} ) {
+               return 0;
+       }
+
+#      # Convert video asf to mp4 if required - need to find a suitable converter...
+#      } else {
+#              # Create part of cmd that specifies each partial file
+#              my $filestring;
+#              $filestring .= " -i \"$_\" " for (@file_tmp_list);
+#              $cmd = "$ffmpeg $ffmpeg_opts $filestring -vcodec copy -acodec copy -f $prog{$pid}{ext} -y \"$file\" 1>&2";
+#      }
+#
+#      logger "INFO: Command: $cmd\n\n" if $opt{verbose};
+#      # Run asf conversion and delete source file on success
+#      if ( ! system($cmd) ) {
+#              unlink( @file_tmp_list );
+#      } else {
+#              logger "ERROR: asf conversion failed - retaining files ".(join ', ', @file_tmp_list)."\n";
+#              return 2;
+#      }
+#      # Moving file into place as complete (if not stdout)
+#      move($file, $file_done) if ! $opt{stdout};
+
+       return 0;
+}
+
+
+
+# Actually do the N95 h.264 downloading
+sub download_stream_h264_low {
+       my ( $ua, $url_2, $file, $file_done, $pid, $mode ) = @_;
+
+       # Change filename extension
+       $file =~ s/mov$/mpg/gi;
+       $file_done =~ s/mov$/mpg/gi;
+
+       logger "INFO: Stage 3 URL = $url_2\n" if $opt{verbose};
+       if ( ! $opt{stdout} ) {
+               logger "INFO: Downloading Low Quality H.264 stream\n";
+               my $cmd = "$vlc $vlc_opts --sout file/ts:${file} $url_2 1>&2";
+               if ( system($cmd) ) {
+                       return 'next';
+               }
+
+       # to STDOUT
+       } else {
+               logger "INFO: Streaming Low Quality H.264 stream to stdout\n";
+               my $cmd = "$vlc $vlc_opts --sout file/ts:- $url_2 1>&2";
+               if ( system($cmd) ) {
+                       return 'next';
+               }       
+       }
+       logger "INFO: Downloaded $file_done\n";
+       # Moving file into place as complete (if not stdout)
+       move($file, $file_done) if ! $opt{stdout};
+
+       $prog{$pid}{mode} = $mode;
+       return 0;
+}
+
+
+
+# Actually do the rtsp downloading
+sub download_stream_rtsp {
+       my ( $ua, $url, $file, $file_done, $file_symlink, $pid ) = @_;
+       my $childpid;
+
+       # Create named pipe
+       if ( $^O !~ /^MSWin32$/ ) {
+               mkfifo($namedpipe, 0700) if (! $opt{wav}) && (! $opt{raw});
+       } else {
+               logger "WARNING: fifos/named pipes are not supported\n" if $opt{verbose};
+       }
+       
+       logger "INFO: Stage 3 URL = $url\n" if $opt{verbose};
+
+       # Create ID3 tagging options for lame (escape " for shell)
+       my ( $id3_name, $id3_episode, $id3_desc, $id3_channel ) = ( $prog{$pid}{name}, $prog{$pid}{episode}, $prog{$pid}{desc}, $prog{$pid}{channel} );
+       $id3_name =~ s|"|\"|g for ($id3_name, $id3_episode, $id3_desc, $id3_channel);
+       $lame_opts .= " --ignore-tag-errors --ty ".( (localtime())[5] + 1900 )." --tl \"$id3_name\" --tt \"$id3_episode\" --ta \"$id3_channel\" --tc \"$id3_desc\" ";
+
+       # Use post-download transcoding using lame if namedpipes are not supported (i.e. ActivePerl/Windows)
+       # (Fallback if no namedpipe support and raw/wav not specified)
+       if ( (! -p $namedpipe) && ! ( $opt{raw} || $opt{wav} ) ) {
+               my $cmd;
+               # Remove filename extension
+               $file =~ s/\.mp3$//gi;
+               # Remove named pipe
+               unlink $namedpipe;
+               logger "INFO: Downloading wav format (followed by transcoding)\n";
+               $cmd = "$mplayer $mplayer_opts -cache 128 -bandwidth $bandwidth -vc null -vo null -ao pcm:waveheader:fast:file=\"${file}.wav\" \"$url\" 1>&2";
+               if ( system($cmd) ) {
+                       return 'next';
+               }
+               # Transcode
+               logger "INFO: Transcoding ${file}.wav\n";
+               $cmd = "$lame $lame_opts \"${file}.wav\" \"${file}.mp3\" 1>&2";
+               logger "DEGUG: Running $cmd\n" if $opt{debug};
+               if ( system($cmd) || (-f "${file}.wav" && stat("${file}.wav")->size < $min_download_size) ) {
+                       return 'next';
+               }
+               unlink "${file}.wav";
+               move "${file}.mp3", $file_done;
+               $prog{$pid}{ext} = 'mp3';
+
+       # Fork a child to do transcoding on the fly using a named pipe written to by mplayer
+       # else do direct mplayer write to wav file if:
+       #  1) we don't have a named pipe available (e.g. in activeperl)
+       #  2) --wav was specified to write file only
+       } elsif ( $opt{wav} && ! $opt{stdout} ) {
+               logger "INFO: Writing wav format\n";
+               # Start the mplayer process and write to wav file
+               my $cmd = "$mplayer $mplayer_opts -cache 128 -bandwidth $bandwidth -vc null -vo null -ao pcm:waveheader:fast:file=\"$file\" \"$url\" 1>&2";
+               logger "DEGUG: Running $cmd\n" if $opt{debug};
+               if ( system($cmd) ) {
+                       return 'next';
+               }
+               # Move file to done state
+               move $file, $file_done if ! $opt{nowrite};
+
+       # No transcoding if --raw was specified
+       } elsif ( $opt{raw} && ! $opt{stdout} ) {
+               # Write out to .ra ext instead (used on fallback if no fifo support)
+               logger "INFO: Writing raw realaudio stream\n";
+               # Start the mplayer process and write to raw file
+               my $cmd = "$mplayer $mplayer_opts -cache 128 -bandwidth $bandwidth -dumpstream -dumpfile \"$file\" \"$url\" 1>&2";
+               logger "DEGUG: Running $cmd\n" if $opt{debug};
+               if ( system($cmd) ) {
+                       return 'next';
+               }
+               # Move file to done state
+               move $file, $file_done if ! $opt{nowrite};
+
+       # Use transcoding via named pipes
+       } else {
+               $childpid = fork();
+               if (! $childpid) {
+                       # Child starts here
+                       $| = 1;
+                       logger "INFO: Transcoding $file\n";
+
+                       # Stream mp3 to file and stdout simultaneously
+                       if ( $opt{stdout} && ! $opt{nowrite} ) {
+                               if ( $opt{wav} || $opt{raw} ) {
+                                       # Race condition - closes named pipe immediately unless we wait
+                                       sleep 5;
+                                       tee($namedpipe, $file);
+                                       #system( "cat $namedpipe 2>/dev/null| $tee $file");
+                               } else {
+                                       my $cmd = "$lame $lame_opts $namedpipe - 2>/dev/null| $tee \"$file\"";
+                                       logger "DEGUG: Running $cmd\n" if $opt{debug};
+                                       system($cmd);
+                               }
+
+                       # Stream mp3 stdout only
+                       } elsif ( $opt{stdout} && $opt{nowrite} ) {
+                               if ( $opt{wav} || $opt{raw} ) {
+                                       sleep 5;
+                                       tee($namedpipe);
+                                       #system( "cat $namedpipe 2>/dev/null");
+                               } else {
+                                       my $cmd = "$lame $lame_opts $namedpipe - 2>/dev/null";
+                                       logger "DEGUG: Running $cmd\n" if $opt{debug};
+                                       system( "$lame $lame_opts $namedpipe - 2>/dev/null");
+                               }
+
+                       # Stream mp3 to file directly
+                       } elsif ( ! $opt{stdout} ) {
+                               my $cmd = "$lame $lame_opts $namedpipe \"$file\" >/dev/null 2>/dev/null";
+                               logger "DEGUG: Running $cmd\n" if $opt{debug};
+                               system($cmd);
+                       }
+                       # Remove named pipe
+                       unlink $namedpipe;
+
+                       # Move file to done state
+                       move $file, $file_done if ! $opt{nowrite};
+                       logger "INFO: Transcoding thread has completed\n";
+                       exit 0;
+               }
+               # Start the mplayer process and write to named pipe
+               # Raw mode
+               if ( $opt{raw} ) {
+                       my $cmd = "$mplayer $mplayer_opts -cache 32 -bandwidth $bandwidth -dumpstream -dumpfile $namedpipe \"$url\" 1>&2";
+                       logger "DEGUG: Running $cmd\n" if $opt{debug};
+                       if ( system($cmd) ) {
+                               # If we fail then kill off child processes
+                               kill 9, $childpid;
+                               return 'next';
+                       }
+               # WAV / mp3 mode
+               } else {
+                       my $cmd = "$mplayer $mplayer_opts -cache 128 -bandwidth $bandwidth -vc null -vo null -ao pcm:waveheader:fast:file=$namedpipe \"$url\" 1>&2";
+                       if ( system($cmd) ) {
+                               # If we fail then kill off child processes
+                               kill 9, $childpid;
+                               return 'next';
+                       }
+               }
+               # Wait for child processes to prevent zombies
+               wait;
+       }
+       logger "INFO: Downloaded $file_done\n";
+
+       # Create symlink if required
+       if ( $opt{symlink} ) {
+               # remove old symlink
+               unlink $file_symlink if -l $file_symlink;
+               symlink $file_done, $file_symlink;
+               logger "INFO: Created symlink from '$file_symlink' -> '$file_done'\n" if $opt{verbose};
+       }
+
+       $prog{$pid}{mode} = 'realaudio';
+       return 0;
+}
+
+
+
+# Actually do the podcast downloading
+sub download_stream_podcast {
+       my ( $ua, $url_2, $file, $file_done, $file_symlink ) = @_;
+       my $start_time = time();
+
+       # Set user agent
+       $ua->agent( $user_agent{get_iplayer} );
+
+       logger "INFO: Stage 3 URL = $url_2\n" if $opt{verbose};
+
+       # Resume partial download?
+       my $start = 0;
+       if ( -f $file ) {
+               $start = stat($file)->size;
+               logger "INFO: Resuming download from $start\n";
+       }
+
+       my $fh = open_file_append($file);
+
+       if ( download_block($file, $url_2, $ua, $start, undef, undef, $fh) != 0 ) {
+               logger "ERROR: Download failed\n";
+               return 'next';
+       } else {
+               # end marker
+               my $end_time = time();
+               # Final file size
+               my $size = stat($file)->size;
+               # Calculate average speed, duration and total bytes downloaded
+               logger sprintf("INFO: Downloaded %.2fMB in %s at %5.0fkbps to %s\n", 
+                       ($size - $start) / (1024.0 * 1024.0),
+                       sprintf("%02d:%02d:%02d", ( gmtime($end_time - $start_time))[2,1,0] ), 
+                       ( $size - $start ) / ($end_time - $start_time) / 1024.0 * 8.0, 
+                       $file_done );
+               move $file, $file_done;
+               # re-symlink file
+               if ( $opt{symlink} ) {
+                       # remove old symlink
+                       unlink $file_symlink if -l $file_symlink;
+                       symlink $file_done, $file_symlink;
+                       logger "INFO: Created symlink from '$file_symlink' -> '$file_done'\n" if $opt{verbose};
+               }
+       }
+
+       $prog{$url_2}{mode} = 'podcast';
+       return 0;
+}
+
+
+
+# Get streaming iphone URL
+sub get_stream_url_iphone {
+       my $ua = shift;
+       my $pid = shift;
+
+       # Create url with appended 6 digit random number
+       my $url_1 = ${iphone_download_prefix}.'/'.${pid}.'?'.(sprintf "%06.0f", 1000000*rand(0)).'%20';
+       logger "INFO: media stream download URL = $url_1\n" if $opt{verbose};
+               
+       # Stage 2: e.g. "Location: http://download.iplayer.bbc.co.uk/iplayer_streaming_http_mp4/121285241910131406.mp4?token=iVXexp1yQt4jalB2Hkl%2BMqI25nz2WKiSsqD7LzRmowrwXGe%2Bq94k8KPsm7pI8kDkLslodvHySUyU%0ApM76%2BxEGtoQTF20ZdFjuqo1%2B3b7Qmb2StOGniozptrHEVQl%2FYebFKVNINg%3D%3D%0A"
+       logger "\rGetting iplayer download URL         " if (! $opt{verbose}) && ! $opt{streaminfo};
+       my $h = new HTTP::Headers(
+               'User-Agent'    => $user_agent{coremedia},
+               'Accept'        => '*/*',
+               'Range'         => 'bytes=0-1',
+       );
+       my $req = HTTP::Request->new ('GET', $url_1, $h);
+       # send request (use simple_request here because that will not allow redirects)
+       my $res = $ua->simple_request($req);
+       # Get resulting Location header (i.e. redirect URL)
+       my $url_2 = $res->header("location");
+       if ( ! $res->is_redirect ) {
+               logger "ERROR: Failed to get redirect from iplayer site\n\n";
+               return '';
+       }
+       # Extract redirection Location URL
+       $url_2 =~ s/^Location: (.*)$/$1/g;
+       # If we get a Redirection containing statuscode=404 then this prog is not yet ready
+       if ( $url_2 =~ /statuscode=404/ ) {
+               logger "\rERROR: Programme is not yet ready for download\n" if $opt{verbose};
+               return '';
+       }
+
+       return $url_2;
+}
+
+
+
+sub get_stream_url_itv {
+       my ( $ua, $pid ) = ( @_ );
+
+       my ( $response, $url_1, $url_2, $url_3, $url_4 );
+       my $part;
+       my $duration;
+       my $filename;
+       my @url_list;
+
+       # construct stage 1 request url
+       $url_1 = 'http://www.itv.com/_app/video/GetMediaItem.ashx?vodcrid=crid://itv.com/'.$pid.'&bitrate=384&adparams=SITE=ITV/AREA=CATCHUP.VIDEO/SEG=CATCHUP.VIDEO%20HTTP/1.1';
+
+       # Extract '<LicencePlaylist>(.+?) HTTP/1.1</LicencePlaylist>'
+       logger "INFO: ITV Video Stage 1 URL: $url_1\n" if $opt{verbose};
+       $response = request_url_retry($ua, $url_1, 2, '', '');
+       logger "DEBUG: Response data: $response\n" if $opt{debug};
+       $url_2 = $1 if $response =~ m{<LicencePlaylist>(.+?) HTTP/1.1</LicencePlaylist>};
+       # replace '&amp;' with '&' and append '%20HTTP/1.1'
+       $url_2 =~ s/&amp;/&/g;
+       $url_2 .= '%20HTTP/1.1';
+       logger "INFO: ITV Video Stage 2 URL: $url_2\n" if $opt{verbose};
+       $response = request_url_retry($ua, $url_2, 2, '', '');
+       logger "DEBUG: Response data: $response\n" if $opt{debug};
+
+       # Extract hrefs and names. There are multiple entries for parts of prog (due to ads):
+       # e.g. <asx><Title>Doctor Zhivago</Title><EntryRef href="HTTP://SAM.ITV.COM/XTSERVER/ACC_RANDOM=1231194223/SITE=ITV/AREA=CATCHUP.VIDEO/SEG=CATCHUP.VIDEO HTTP/1.1/SOURCE=CATCH.UP/GENRE=DRAMA/PROGNAME=DOCTOR.ZHIVAGO/PROGID=33105/SERIES=DOCTOR.ZHIVAGO/EPNUM=/EPTITLE=/BREAKNUM=0/ADPOS=1/PAGEID=01231194223/DENTON=0/CUSTOMRATING=/TOTDUR=90/PREDUR=0/POSDUR=905/GENERIC=6e0536bf-7883-4aaa-9230-94ecc4aea403/AAMSZ=VIDEO" /><EntryRef href="HTTP://SAM.ITV.COM/XTSERVER/ACC_RANDOM=1231194223/SITE=ITV/AREA=CATCHUP.VIDEO/SEG=CATCHUP.VIDEOHTTP/1.1/SOURCE=CATCH.UP/GENRE=DRAMA/PROGNAME=DOCTOR.ZHIVAGO/PROGID=33105/SERIES=DOCTOR.ZHIVAGO/EPNUM=/EPTITLE=/BREAKNUM=0/ADPOS=LAST/PAGEID=01231194223/DENTON=0/CUSTOMRATING=/TOTDUR=90/PREDUR=0/POSDUR=905/GENERIC=6e0536bf-7883-4aaa-9230-94ecc4aea403/AAMSZ=VIDEO" />
+       $prog{$pid}{name} = $1 if $response =~ m{<Title>(.+?)<\/Title>};
+       for my $entry (split /<Entry><ref\s+href=/, $response) {
+               logger "DEBUG: Entry data: $entry\n" if $opt{debug};
+               $entry .= '<Entry><ref href='.$entry;
+
+               ( $url_3, $part, $filename, $duration ) = ( $1, $2, $3, $4 ) if $entry =~ m{<Entry><ref\s+href="(.+?)"\s+\/><param\s+value="true"\s+name="Prebuffer"\s+\/>\s*<PARAM\s+NAME="PrgPartNumber"\s+VALUE="(.+?)"\s*\/><PARAM\s+NAME="FileName"\s+VALUE="(.+?)"\s*\/><PARAM\s+NAME="PrgLength"\s+VALUE="(.+?)"\s*\/>};
+               next if not $url_3;
+               # Replace '&amp;' with '&' in url
+               $url_3 =~ s/&amp;/&/g;
+               logger "INFO: ITV Video Name: $part\n";
+
+               logger "INFO: ITV Video Stage 3 URL: $url_3\n" if $opt{verbose};
+               $entry = request_url_retry($ua, $url_3, 2, '', '');
+               logger "DEBUG: Response data: $entry\n" if $opt{debug};
+
+               # Extract mms (replace 'http' with 'mms') url: e.g.: Ref1=http://itvbrdbnd.wmod.llnwd.net/a1379/o21/ucontent/2007/6/22/1549_384_1_2.wmv?MSWMExt=.asf
+               chomp( $url_4 = 'mms'.$1 ) if $entry =~ m{Ref1=http(.+?)[\r\n]+};
+               logger "INFO: ITV Video URL: $url_4\n" if $opt{verbose};
+               push @url_list, $url_4;
+       }
+       return @url_list;
+}
+
+
+
+# Generate the download filename prefix given a pid and optional format such as '<longname> - <episode> <pid> <version>'
+sub generate_download_filename_prefix {
+       my ( $pid, $dir, $file ) = ( @_ );
+
+       # If we dont have longname defined just set it to name
+       $prog{$pid}{longname} = $prog{$pid}{name} if ! $prog{$pid}{longname};
+
+       # substitute fields and sanitize $file
+       $file = substitute_fields( $pid, $file );
+
+       # Spaces
+       $file =~ s/\s+/_/g if ! $opt{whitespace};
+
+       # Don't create subdir if we are only testing downloads
+       # Create a subdir for programme sorting option
+       if ( $opt{subdir} && ! $opt{test} ) {
+               my $subdir = substitute_fields( $pid, '<longname>' );
+               $file = "${subdir}/${file}";
+               # Create dir if it does not exist
+               mkpath("${dir}/${subdir}") if ! -d "${dir}/${subdir}";
+       }
+
+       return $file;
+}
+
+
+
+# Usage: moov_length = relocate_moov_chunk_offsets(<binary string>)
+sub relocate_moov_chunk_offsets {
+       my $moovdata = $_[0];
+       # Change all the chunk offsets in moov->stco atoms and add moov_length to them all
+       # get moov atom length
+       my $moov_length = bytestring_to_int( substr($moovdata, 0, 4) );
+       # Use index() to search for a string within a string
+       my $i = -1;
+       while (($i = index($moovdata, 'stco', $i)) > -1) {
+
+               # determine length of atom (4 bytes preceding stco)
+               my $stco_len = bytestring_to_int( substr($moovdata, $i-4, 4) );
+               logger "INFO: Found stco atom at moov atom offset: $i length $stco_len\n" if $opt{verbose};
+
+               # loop through all chunk offsets in this atom and add offset (== moov atom length)
+               for (my $j = $i+12; $j < $stco_len+$i-4; $j+=4) {
+                       my $chunk_offset = bytestring_to_int( substr($moovdata, $j, 4) );
+                       #logger "chunk_offset @ $i, $j = '".get_hex( substr($moovdata, $j, 4) )."',     $chunk_offset + $moov_length = ";
+                       $chunk_offset += $moov_length;
+                       # write back bytes into $moovdata
+                       #substr($moovdata, $j+0, 1) = chr( ($chunk_offset >> 24) & 0xFF );
+                       #substr($moovdata, $j+1, 1) = chr( ($chunk_offset >> 16) & 0xFF );
+                       #substr($moovdata, $j+2, 1) = chr( ($chunk_offset >>  8) & 0xFF );
+                       #substr($moovdata, $j+3, 1) = chr( ($chunk_offset >>  0) & 0xFF );
+                       write_msb_value_at_offset( $moovdata, $j, $chunk_offset );
+                       #$chunk_offset = bytestring_to_int( substr($moovdata, $j, 4) );
+                       #logger "$chunk_offset\n";
+               }
+
+               # skip over this whole atom now it is processed
+               $i += $stco_len;
+       }
+       # Write $moovdata back to calling string
+       $_[0] = $moovdata;
+       return $moov_length;
+}
+
+
+
+# Replace the moov->udta atom with a new user-supplied one and update the moov atom size
+# Usage: replace_moov_udta_atom ( $udta_new, $moovdata )
+sub replace_moov_udta_atom {
+       my $udta_new = $_[0];
+       my $moovdata = $_[1];
+
+       # get moov atom length
+       my $moov_length = bytestring_to_int( substr($moovdata, 0, 4) );
+
+       # Find the original udta atom start 
+       # Use index() to search for a string within a string ($i will point at the beginning of the atom)
+       my $i = index($moovdata, 'udta', -1) - 4;
+
+       # determine length of atom (4 bytes preceding the name)
+       my $udta_len = bytestring_to_int( substr($moovdata, $i, 4) );
+       logger "INFO: Found udta atom at moov atom offset: $i length $udta_len\n" if $opt{verbose};
+
+       # Save the data before the udta atom
+       my $moovdata_before_udta = substr($moovdata, 0, $i);
+
+       # Save the remainder portion of data after the udta atom for later
+       my $moovdata_after_udta = substr($moovdata, $i, $moovdata - $i + $udta_len);
+
+       # Old udta atom should we need it
+       ### my $udta_old = substr($moovdata, $i, $udta_len);
+
+       # Create new moov atom
+       $moovdata = $moovdata_before_udta.$udta_new.$moovdata_after_udta;
+       
+       # Recalculate the moov size and insert into moovdata
+       write_msb_value_at_offset( $moovdata, 0, length($moovdata) );
+       
+       # Write $moovdata back to calling string
+       $_[1] = $moovdata;
+
+       return 0;
+}
+
+
+
+# Write the msb 4 byte $value starting at $offset into the passed string
+# Usage: write_msb_value($string, $offset, $value)
+sub write_msb_value_at_offset {
+       my $offset = $_[1];
+       my $value = $_[2];
+       substr($_[0], $offset+0, 1) = chr( ($value >> 24) & 0xFF );
+       substr($_[0], $offset+1, 1) = chr( ($value >> 16) & 0xFF );
+       substr($_[0], $offset+2, 1) = chr( ($value >>  8) & 0xFF );
+       substr($_[0], $offset+3, 1) = chr( ($value >>  0) & 0xFF );
+       return 0;
+}
+
+
+
+# Returns a string containing an QT atom
+# Usage: create_qt_atom(<atome name>, <atom data>, ['string'])
+sub create_qt_atom {
+       my ($name, $data, $prog_type) = (@_);
+       if (length($name) != 4) {
+               logger "ERROR: Inavlid QT atom name length '$name'\n";
+               exit 1;
+       }
+       # prepend string length if this is a string type
+       if ( $prog_type eq 'string' ) {
+               my $value = length($data);
+               $data = '1111'.$data;
+               # overwrite '1111' with total atom length in 2-byte MSB + 0x0 0x0
+               substr($data, 0, 1) = chr( ($value >> 8) & 0xFF );
+               substr($data, 1, 1) = chr( ($value >> 0) & 0xFF );
+               substr($data, 2, 1) = chr(0);
+               substr($data, 3, 1) = chr(0);
+       }
+       my $atom = '0000'.$name.$data;
+       # overwrite '0000' with total atom length in MSB
+       write_msb_value_at_offset( $atom, 0, length($name.$data) + 4 );
+       return $atom;
+}
+
+
+
+# Usage download_block($file, $url_2, $ua, $start, $end, $file_len, $fh);
+#  ensure filehandle $fh is open in append mode
+# or, $content = download_block(undef, $url_2, $ua, $start, $end, $file_len);
+# Called in 4 ways:
+# 1) write to real file                        => download_block($file, $url_2, $ua, $start, $end, $file_len, $fh);
+# 2) write to real file + STDOUT       => download_block($file, $url_2, $ua, $start, $end, $file_len, $fh); + $opt{stdout}==true
+# 3) write to STDOUT only              => download_block($file, $url_2, $ua, $start, $end, $file_len, $fh); + $opt{stdout}==true + $opt{nowrite}==false
+# 4) write to memory (and return data)  => download_block(undef, $url_2, $ua, $start, $end, $file_len, undef);
+# 4) write to memory (and return data)  => download_block(undef, $url_2, $ua, $start, $end);
+sub download_block {
+
+       my ($file, $url, $ua, $start, $end, $file_len, $fh) = @_;
+       my $orig_length;
+       my $buffer;
+       my $lastpercent = 0;
+       $now = time();
+
+       # If this is an 'append to file' mode call
+       if ( defined $file && $fh && (!$opt{nowrite}) ) {
+               # Stage 3b: Download File
+               $orig_length = tell $fh;
+               logger "INFO: Appending to $file\n" if $opt{verbose};
+       }
+
+       # Setup request headers
+       my $h = new HTTP::Headers(
+               'User-Agent'    => $user_agent{coremedia},
+               'Accept'        => '*/*',
+               'Range'        => "bytes=${start}-${end}",
+       );
+
+       my $req = HTTP::Request->new ('GET', $url, $h);
+
+       # Set time to use for download rate calculation
+       # Define callback sub that gets called during download request
+       # This sub actually writes to the open output file and reports on progress
+       my $callback = sub {
+               my ($data, $res, undef) = @_;
+               # Don't write the output to the file if there is no content-length header
+               return 0 if ( ! $res->header("Content-Length") );
+               # If we don't know file length in advanced then set to size reported reported from server upon download
+               $file_len = $res->header("Content-Length") + $start if ! defined $file_len;
+               # Write output
+               print $fh $data if ! $opt{nowrite};
+               print STDOUT $data if $opt{stdout};
+               # return if streaming to stdout - no need for progress
+               return if $opt{stdout} && $opt{nowrite};
+               return if $opt{quiet};
+               # current file size
+               my $size = tell $fh;
+               # Download percent
+               my $percent = 100.0 * $size / $file_len;
+               # Don't update display if we haven't dowloaded at least another 0.1%
+               return if ($percent - $lastpercent) < 0.1;
+               $lastpercent = $percent;
+               # download rates in bytes per second and time remaining
+               my $rate_bps;
+               my $rate;
+               my $time;
+               my $timecalled = time();
+               if ($timecalled - $now < 1) {
+                       $rate = '-----kbps';
+                       $time = '--:--:--';
+               } else {
+                       $rate_bps = ($size - $orig_length) / ($timecalled - $now);
+                       $rate = sprintf("%5.0fkbps", (8.0 / 1024.0) * $rate_bps);
+                       $time = sprintf("%02d:%02d:%02d", ( gmtime( ($file_len - $size) / $rate_bps ) )[2,1,0] );
+               }
+               logger sprintf "%8.2fMB / %.2fMB %s %5.1f%%, %s remaining         \r", 
+                       $size / 1024.0 / 1024.0, 
+                       $file_len / 1024.0 / 1024.0,
+                       $rate,
+                       $percent,
+                       $time,
+               ;
+       };
+
+       my $callback_memory = sub {
+               my ($data, $res, undef) = @_;
+               # append output to buffer
+               $buffer .= $data;
+               return if $opt{quiet};
+               # current buffer size
+               my $size = length($buffer);
+               # download rates in bytes per second
+               my $timecalled = time();
+               my $rate_bps;
+               my $rate;
+               my $time;
+               my $percent;
+               # If we can get Content_length then display full progress
+               if ($res->header("Content-Length")) {
+                       $file_len = $res->header("Content-Length") if ! defined $file_len;
+                       # Download percent
+                       $percent = 100.0 * $size / $file_len;
+                       return if ($percent - $lastpercent) < 0.1;
+                       $lastpercent = $percent;
+                       # Block length
+                       $file_len = $res->header("Content-Length");
+                       if ($timecalled - $now < 0.1) {
+                               $rate = '-----kbps';
+                               $time = '--:--:--';
+                       } else {
+                               $rate_bps = $size / ($timecalled - $now);
+                               $rate = sprintf("%5.0fkbps", (8.0 / 1024.0) * $rate_bps );
+                               $time = sprintf("%02d:%02d:%02d", ( gmtime( ($file_len - $size) / $rate_bps ) )[2,1,0] );
+                       }
+                       # time remaining
+                       logger sprintf "%8.2fMB / %.2fMB %s %5.1f%%, %s remaining         \r", 
+                               $size / 1024.0 / 1024.0,
+                               $file_len / 1024.0 / 1024.0,
+                               $rate,
+                               $percent,
+                               $time,
+                       ;
+               # Just used simple for if we cannot determine content length
+               } else {
+                       if ($timecalled - $now < 0.1) {
+                               $rate = '-----kbps';
+                       } else {
+                               $rate = sprintf("%5.0fkbps", (8.0 / 1024.0) * $size / ($timecalled - $now) );
+                       }
+                       logger sprintf "%8.2fMB %s         \r", $size / 1024.0 / 1024.0, $rate;
+               }
+       };
+
+       # send request
+       logger "\nINFO: Downloading range ${start}-${end}\n" if $opt{verbose};
+       logger "\r                              \r";
+       my $res;
+
+       # If $fh undefined then get block to memory (fh always defined for stdout or file d/load)
+       if (defined $fh) {
+               logger "DEBUG: writing stream to stdout, Range: $start - $end of $url\n" if $opt{verbose} && $opt{stdout};
+               logger "DEBUG: writing stream to $file, Range: $start - $end of $url\n" if $opt{verbose} && !$opt{nowrite};
+               $res = $ua->request($req, $callback);
+               if (  (! $res->is_success) || (! $res->header("Content-Length")) ) {
+                       logger "ERROR: Failed to Download block\n\n";
+                       return 5;
+               }
+                logger "INFO: Content-Length = ".$res->header("Content-Length")."                               \n" if $opt{verbose};
+               return 0;
+                  
+       # Memory Block
+       } else {
+               logger "DEBUG: writing stream to memory, Range: $start - $end of $url\n" if $opt{debug};
+               $res = $ua->request($req, $callback_memory);
+               if ( (! $res->is_success) ) {
+                       logger "ERROR: Failed to Download block\n\n";
+                       return '';
+               } else {
+                       return $buffer;
+               }
+       }
+}
+
+
+
+sub create_ua {
+       my $agent = shift;
+       my $ua = LWP::UserAgent->new;
+       $ua->timeout([$lwp_request_timeout]);
+       $ua->proxy( ['http'] => $proxy_url );
+       $ua->agent( $user_agent{$agent} );
+       $ua->conn_cache(LWP::ConnCache->new());
+       #$ua->conn_cache->total_capacity(50);
+       $ua->cookie_jar( HTTP::Cookies->new( file => $cookiejar, autosave => 1, ignore_discard => 1 ) );
+       return $ua;
+};     
+
+
+
+# Converts a string of chars to it's HEX representation
+sub get_hex {
+        my $buf = shift || '';
+        my $ret = '';
+        for (my $i=0; $i<length($buf); $i++) {
+                $ret .= " ".sprintf("%02lx", ord substr($buf, $i, 1) );
+        }
+       logger "DEBUG: HEX string value = $ret\n" if $opt{verbose};
+        return $ret;
+}
+
+
+
+# Converts a string of chars to it's MSB decimal value
+sub bytestring_to_int {
+       # Reverse to LSB order
+        my $buf = reverse shift;
+        my $dec = 0;
+        for (my $i=0; $i<length($buf); $i++) {
+               # Multiply byte value by 256^$i then accumulate
+                $dec += (ord substr($buf, $i, 1)) * 256 ** $i;
+        }
+        #logger "DEBUG: Decimal value = $dec\n" if $opt{verbose};
+        return $dec;
+}
+
+
+
+# version of unix tee
+# Usage tee ($infile, $outfile)
+# If $outfile is undef then just cat file to STDOUT
+sub tee {
+       my ( $infile, $outfile ) = @_;
+       # Open $outfile for writing, $infile for reading
+       if ( $outfile) {
+               if ( ! open( OUT, "> $outfile" ) ) {
+                       logger "ERROR: Could not open $outfile for writing\n";
+                       return 1;
+               } else {
+                       logger "INFO: Opened $outfile for writing\n" if $opt{verbose};
+               }
+       }
+       if ( ! open( IN, "< $infile" ) ) {
+               logger "ERROR: Could not open $infile for reading\n";
+               return 2;
+       } else {
+               logger "INFO: Opened $infile for reading\n" if $opt{verbose};
+       }
+       # Read and redirect IN
+       while ( <IN> ) {
+               print $_;
+               print OUT $_ if $outfile;
+       }
+       # Close output file
+       close OUT if $outfile;
+       close IN;
+       return 0;
+}
+
+
+
+# Usage: $fh = open_file_append($filename);
+sub open_file_append {
+       local *FH;
+       my $file = shift;
+       # Just in case we actually write to the file - make this /dev/null
+       $file = '/dev/null' if $opt{nowrite};
+       if ($file) {
+               if ( ! open(FH, ">> $file") ) {
+                       logger "ERROR: Cannot write or append to $file\n\n";
+                       exit 1;
+               }
+       }
+       # Fix for binary - needed for Windows
+       binmode FH;
+       return *FH;
+}
+
+
+
+# Updates and overwrites this script - makes backup as <this file>.old
+sub update_script {
+       # Get version URL
+       my $script_file = $0;
+       my $ua = create_ua('update');
+       logger "INFO: Current version is $version\n";
+       logger "INFO: Checking for latest version from linuxcentre.net\n";
+       my $res = $ua->request( HTTP::Request->new( GET => $version_url ) );
+       chomp( my $latest_ver = $res->content );
+       if ( $res->is_success ) {
+               # Compare version numbers
+               if ( $latest_ver > $version ) {
+                       logger "INFO: New version $latest_ver available, downloading\n";
+                       # Check if we are writable
+                       if ( ! -w $script_file ) {
+                               logger "ERROR: $script_file is not writable by the current user - Update aborted\n";
+                               exit 1;
+                       }
+                       my $req = HTTP::Request->new ('GET', $update_url);
+                       # Save content into a $script_file
+                       my $res = $ua->request($req, $script_file.'.tmp');
+                       if ( ! -f $script_file.'.tmp' ) {
+                               logger "ERROR: Could not download update to ${script_file}.tmp - Update aborted\n";
+                               exit 1;
+                       }
+                       # If the download was successful then copy over this script and make executable after making a backup of this script
+                       if ( $res->is_success ) {
+                               if ( copy($script_file, $script_file.'.old') ) {
+                                       move($script_file.'.tmp', $script_file);
+                                       chmod 0755, $script_file;
+                                       logger "INFO: Copied new version $latest_ver into place (previous version is now called '${script_file}.old')\n";
+                                       logger "INFO: Please see: http://linuxcentre.net/get_iplayer/CHANGELOG.txt\n";
+                               } else {
+                                       logger "ERROR: Could not create backup file ${script_file}.old - Update aborted\n";
+                                       exit 1;
+                               }
+                       }
+               } else {
+                       logger "INFO: No update is necessary (latest version = $latest_ver)\n";
+               }
+       } else {
+               logger "ERROR: Failed to connect to update site - Update aborted\n";
+               exit 2;
+       }
+       exit 0;
+}
+
+
+
+# Creates the Freevo FXD or MythTV Streams meta data (and pre-downloads graphics - todo)
+sub create_xml {
+       my $xmlfile = shift;
+       if ( ! open(XML, "> $xmlfile") ) {
+               logger "ERROR: Couldn't open xml file $xmlfile for writing\n";
+               return 1;
+       }
+       print XML '<?xml version="1.0" ?>';
+       print XML '<freevo>' if $opt{fxd};
+       print XML "\n<MediaStreams>\n" if $opt{mythtv};
+
+       if ( $opt{xmlnames} ) {
+               # containers sorted by prog names
+               print XML "\t<container title=\"iplayer by Programme Name\">\n" if $opt{fxd};
+               my %program_index;
+               my %program_count;
+               # create hash of programme_name -> index
+               for (@_) {
+                       $program_index{$prog{$index_pid{$_}}{name}} = $_;
+                       $program_count{$prog{$index_pid{$_}}{name}}++;
+               }
+               for my $name ( sort keys %program_index ) {
+                       my @count = grep /^$name$/, keys %program_index;
+                       print XML "\t<container title=\"".encode_entities( $name )." ($program_count{$name})\">\n" if $opt{fxd};
+                       print XML "\t<Streams>\n" if $opt{mythtv};
+                       for (@_) {
+                               my $pid = $index_pid{$_};
+                               # loop through and find matches for each progname
+                               if ( $prog{$index_pid{$_}}{name} =~ /^$name$/ ) {
+                                       my $episode = encode_entities( $prog{$pid}{episode} );
+                                       my $desc = encode_entities( $prog{$pid}{desc} );
+                                       my $title = "${episode} ($prog{$pid}{available})";
+                                       print XML "<movie title=\"${title}\">
+                                               <video><url id=\"p1\">${pid}.mov<playlist/></url></video>
+                                               <info><description>${desc}</description></info>
+                                       </movie>\n" if $opt{fxd};
+                                       print XML "<Stream>
+                                               <Name>\"${title}\"</Name>
+                                               <url>${pid}.mov</url>
+                                               <Subtitle></Subtitle>
+                                               <Synopsis>${desc}</Synopsis>
+                                       </Stream>\n" if $opt{mythtv};
+                               }
+                       }                       
+                       print XML "\t</container>\n" if $opt{fxd};
+                       print XML "\t</Streams>\n" if $opt{mythtv};
+               }
+               print XML "\t</container>\n" if $opt{fxd};
+       }
+
+
+       if ( $opt{xmlchannels} ) {
+               # containers for prog names sorted by channel
+               print XML "\t<container title=\"iplayer by Channel\">\n" if $opt{fxd};
+               my %program_index;
+               my %program_count;
+               my %channels;
+               # create hash of unique channel names and hash of programme_name -> index
+               for (@_) {
+                       $program_index{$prog{$index_pid{$_}}{name}} = $_;
+                       $program_count{$prog{$index_pid{$_}}{name}}++;
+                       $channels{$prog{$index_pid{$_}}{channel}} .= '|'.$prog{$index_pid{$_}}{name}.'|';
+               }
+               for my $channel ( sort keys %channels ) {
+                       print XML "\t<container title=\"".encode_entities( $channel )."\">\n" if $opt{fxd};
+                       print XML "\t<Feed>
+                               \t<Name>".encode_entities( $channel )."</Name>
+                               \t<Provider>BBC</Provider>\n
+                               \t<Streams>\n" if $opt{mythtv};
+                       for my $name ( sort keys %program_index ) {
+                               # Do we have any of this prog $name on this $channel?
+                               if ( $channels{$channel} =~ /\|$name\|/ ) {
+                                       my @count = grep /^$name$/, keys %program_index;
+                                       print XML "\t<container title=\"".encode_entities( $name )." ($program_count{$name})\">\n" if $opt{fxd};
+                                       print XML "\t\t<Stream>\n" if $opt{mythtv};
+                                       for (@_) {
+                                               # loop through and find matches for each progname for this channel
+                                               my $pid = $index_pid{$_};
+                                               if ( $prog{$pid}{channel} =~ /^$channel$/ && $prog{$pid}{name} =~ /^$name$/ ) {
+                                                       my $episode = encode_entities( $prog{$pid}{episode} );
+                                                       my $desc = encode_entities( $prog{$pid}{desc} );
+                                                       my $title = "${episode} ($prog{$pid}{available})";
+                                                       print XML "<movie title=\"${title}\">
+                                                               <video><url id=\"p1\">${pid}.mov<playlist/></url></video>
+                                                               <info><description>${desc}</description></info>
+                                                       </movie>\n" if $opt{fxd};
+                                                       print XML "\t\t<Name>$name</Name>\n\t\t<Url>${pid}.mov</Url>\n\t\t<Subtitle>${episode}</Subtitle>\n\t\t<Synopsis>${desc}</Synopsis>\n" if $opt{mythtv};
+                                               }
+                                       }
+                                       print XML "\t</container>\n" if $opt{fxd};
+                                       print XML "\t</Stream>\n" if $opt{mythtv};
+                               }
+                       }
+                       print XML "\t</container>\n" if $opt{fxd};
+                       print XML "\t</Streams>\n\t</Feed>\n" if $opt{mythtv};
+               }
+               print XML "\t</container>\n" if $opt{fxd};
+       }
+
+
+       if ( $opt{xmlalpha} ) {
+               my %table = (
+                       'A-C' => '[abc]',
+                       'D-F' => '[def]',
+                       'G-I' => '[ghi]',
+                       'J-L' => '[jkl]',
+                       'M-N' => '[mn]',
+                       'O-P' => '[op]',
+                       'Q-R' => '[qt]',
+                       'S-T' => '[st]',
+                       'U-V' => '[uv]',
+                       'W-Z' => '[wxyz]',
+                       '0-9' => '[\d]',
+               );
+               print XML "\t<container title=\"iplayer A-Z\">\n";
+               for my $folder (sort keys %table) {
+                       print XML "\t<container title=\"iplayer $folder\">\n";
+                       for (@_) {
+                               my $pid = $index_pid{$_};
+                               my $name = encode_entities( $prog{$pid}{name} );
+                               my $episode = encode_entities( $prog{$pid}{episode} );
+                               my $desc = encode_entities( $prog{$pid}{desc} );
+                               my $title = "${name} - ${episode} ($prog{$pid}{available})";
+                               my $regex = $table{$folder};
+                               if ( $name =~ /^$regex/i ) {
+                                       print XML "<movie title=\"${title}\">
+                                               <video><url id=\"p1\">${pid}.mov<playlist/></url></video>
+                                               <info><description>${desc}</description></info>
+                                       </movie>\n" if $opt{fxd};
+                                       print XML "<Stream title=\"${title}\">
+                                               <video><url id=\"p1\">${pid}.mov<playlist/></url></video>
+                                               <info><description>${desc}</description></info>
+                                       </Stream>\n" if $opt{mythtv};
+                               }
+                       }
+                       print XML "\t</container>\n";
+               }
+               print XML "\t</container>\n";
+       }
+
+       print XML '</freevo>' if $opt{fxd};
+       print XML '</MediaStreams>' if $opt{mythtv};
+       close XML;
+}
+
+
+
+sub create_html {
+       my %name_channel;
+       # Create local web page
+       if ( open(HTML, "> $opt{html}") ) {
+               print HTML '<html><head></head><body><table border=1>';
+               for (@_) {
+                       my $pid = $index_pid{$_};
+                       # Skip if pid isn't in index
+                       next if ! $pid;
+                       # Skip if already downloaded and --hide option is specified
+                       next if $opt{hide} && $pids_history{$pid};
+                       if (! defined $name_channel{ "$prog{$pid}{name}|$prog{$pid}{channel}" }) {
+                               print HTML list_prog_entry_html( $pid );
+                       } else {
+                               print HTML list_prog_entry_html( $pid, 1 );
+                       }
+                       $name_channel{ "$prog{$pid}{name}|$prog{$pid}{channel}" } = 1;
+               }
+               print HTML '</table></body>';
+               close (HTML);
+       } else {
+               logger "Couldn't open html file $opt{html} for writing\n";
+       }
+}
+
+
+
+sub list_prog_entry_html {
+       my ($pid, $tree) = (@_);
+       my $html;
+       # If tree view
+       my $name = encode_entities( $prog{$pid}{name} );
+       my $episode = encode_entities( $prog{$pid}{episode} );
+       my $desc = encode_entities( $prog{$pid}{desc} );
+       my $channel = encode_entities( $prog{$pid}{channel} );
+       my $type = encode_entities( $prog{$pid}{type} );
+       my $categories = encode_entities( $prog{$pid}{categories} );
+
+       # Header
+       if ( not $tree ) {
+               # Assume all thumbnails for a prog name are the same
+               $html = "<tr bgcolor='#cccccc'>
+                       <td rowspan=1 width=150><a href=\"$prog{$pid}{web}\"><img height=84 width=150 src=\"$prog{$pid}{thumbnail}\"></a></td>
+                               <td><a href=\"$prog{$pid}{web}\">${name}</a></td>
+                               <td>${channel}</td>
+                               <td>${type}</td>
+                               <td>${categories}</td>
+                       </tr>
+               \n";
+       # Follow-on episodes
+       }
+               $html .= "<tr>
+                               <td>$_</td>
+                               <td><a href=\"$prog{$pid}{web}\">${episode}</a></td>
+                               <td colspan=3>${desc}</td>
+                       </tr>
+               \n";
+       return $html;
+}
+
+
+
+# Save cmdline-only options to file
+sub save_options_file {
+       my $optfile = shift;
+       unlink $optfile;
+       logger "DEBUG: Saving options to $optfile:\n" if $opt{debug};
+       open (OPT, "> $optfile") || die ("ERROR: Cannot save options to $optfile\n");
+       # Save all opts except for these
+       for (grep !/(help|test|debug|get)/, keys %opt_cmdline) {
+               print OPT "$_ $opt_cmdline{$_}\n"  if defined $opt_cmdline{$_};
+               logger "DEBUG: Setting cmdline option $_ = $opt_cmdline{$_}\n" if $opt{debug} && defined $opt_cmdline{$_};
+       }
+       close OPT;
+       logger "INFO: Command Line Options saved as defult in $optfile\n";
+       exit 0;
+}
+
+
+
+# Load default options from file
+sub read_options_file {
+       my $optfile = shift;
+       return 0 if ! -f $optfile;
+       logger "DEBUG: Parsing options from $optfile:\n" if $opt{debug};
+       open (OPT, "< $optfile") || die ("ERROR: Cannot read options file $optfile\n");
+       while(<OPT>) {
+               /^\s*([\w\-_]+)\s+(.*)\s*$/;
+               chomp( $opt{$1} = $2 );
+               # keep track of which options came from files
+               chomp( $opt_file{$1} = $2 );
+               logger "DEBUG: Setting option $1 = $2\n" if $opt{debug};
+       }
+       close OPT;
+}
+
+
+
+# Display options from files
+sub display_default_options {
+       logger "Default options:\n";
+       for ( grep !/(help|debug|get|^pvr)/, sort keys %opt_file ) {
+               logger "\t$_ = $opt_file{$_}\n" if defined $opt_file{$_};
+       }
+       logger "\n";
+       return 0;
+}
+
+
+
+# Display options currently set
+sub display_current_options {
+       logger "Current options:\n";
+       for ( sort keys %opt ) {
+               logger "\t$_ = $opt{$_}\n" if defined $opt{$_} && $opt{$_};
+       }
+       logger "\n";
+       return 0;
+}
+
+
+
+# Get time ago made available (x days y hours ago) from '2008-06-22T05:01:49Z' and current time
+sub get_available_time_string {
+       my $datestring = shift;
+       # extract $year $mon $mday $hour $min $sec
+       $datestring =~ m{(\d\d\d\d)\-(\d\d)\-(\d\d)T(\d\d):(\d\d):(\d\d)Z};
+       my ($year, $mon, $mday, $hour, $min, $sec) = ($1, $2, $3, $4, $5, $6);
+       # Calculate the seconds difference between epoch_now and epoch_datestring and convert back into array_time
+       my @time = gmtime( time() - timelocal($sec, $min, $hour, $mday, ($mon-1), ($year-1900), undef, undef, 0) );
+       return "$time[7] days $time[2] hours ago";
+}
+
+
+
+# get full episode metadata given pid and ua. Uses two different urls to get data
+sub get_pid_metadata {
+       my $ua = shift;
+       my $pid = shift;
+       my $metadata;
+       my $entry3;
+       my ($name, $episode, $duration, $available, $channel, $expiry, $longdesc, $versions, $guidance, $prog_type, $categories, $player, $thumbnail);
+
+       # This URL works for all prog types:
+       # http://www.bbc.co.uk/iplayer/playlist/${pid}
+
+       # This URL only works for TV progs:
+       # http://www.bbc.co.uk/iplayer/metafiles/episode/${pid}.xml
+
+       # This URL works for tv/radio prog types:
+       # http://www.bbc.co.uk/iplayer/widget/episodedetail/episode/${pid}/template/mobile/service_type/tv/
+
+       # This URL works for tv/radio prog types:
+       # $prog_feed_url = http://feeds.bbc.co.uk/iplayer/episode/$pid
+
+       if ( $prog{$pid}{type} =~ /^(tv|radio)$/i ) {
+               $entry3 = request_url_retry($ua, $prog_feed_url.$pid, 3, '', '');
+               decode_entities($entry3);
+               logger "DEBUG: $prog_feed_url.$pid:\n$entry3\n\n" if $opt{debug};
+               # Flatten
+               $entry3 =~ s|\n| |g;
+
+               # Entry3 format
+               #<?xml version="1.0" encoding="utf-8"?>                                      
+               #<?xml-stylesheet href="http://www.bbc.co.uk/iplayer/style/rss.css" type="text/css"?>
+               #<feed xmlns="http://www.w3.org/2005/Atom" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:media="http://search.yahoo.com/mrss/" xml:lang="en-GB">
+               #  <title>BBC iPlayer - Episode Detail: Edith Bowman: 22/09/2008</title>                                                                          
+               #  <subtitle>Sara Cox sits in for Edith with another Cryptic Randomizer.</subtitle>
+               #  <updated>2008-09-29T10:59:45Z</updated>
+               #  <id>tag:feeds.bbc.co.uk,2008:/iplayer/feed/episode/b00djtfh</id>
+               #  <link rel="related" href="http://www.bbc.co.uk/iplayer" type="text/html" />
+               #  <link rel="self" href="http://feeds.bbc.co.uk/iplayer/episode/b00djtfh" type="application/atom+xml" />
+               #  <author>
+               #    <name>BBC</name>
+               #    <uri>http://www.bbc.co.uk</uri>
+               #  </author>
+               #  <entry>
+               #    <title type="text">Edith Bowman: 22/09/2008</title>
+               #    <id>tag:feeds.bbc.co.uk,2008:PIPS:b00djtfh</id>
+               #    <updated>2008-09-15T01:28:36Z</updated>
+               #    <summary>Sara Cox sits in for Edith with another Cryptic Randomizer.</summary>
+               #    <content type="html">
+               #      &lt;p&gt;
+               #        &lt;a href=&quot;http://www.bbc.co.uk/iplayer/episode/b00djtfh?src=a_syn30&quot;&gt;
+               #          &lt;img src=&quot;http://www.bbc.co.uk/iplayer/images/episode/b00djtfh_150_84.jpg&quot; alt=&quot;Edith Bowman: 22/09/2008&quot; /&gt;
+               #        &lt;/a&gt;
+               #      &lt;/p&gt;
+               #      &lt;p&gt;
+               #        Sara Cox sits in for Edith with movie reviews and great new music, plus another Cryptic Randomizer.
+               #      &lt;/p&gt;
+               #    </content>
+               #    <link rel="alternate" href="http://www.bbc.co.uk/iplayer/episode/b00djtfh?src=a_syn31" type="text/html" title="Edith Bowman: 22/09/2008">
+               #      <media:content medium="audio" duration="10800">
+               #        <media:title>Edith Bowman: 22/09/2008</media:title>
+               #        <media:description>Sara Cox sits in for Edith with movie reviews and great new music, plus another Cryptic Randomizer.</media:description>
+               #        <media:player url="http://www.bbc.co.uk/iplayer/episode/b00djtfh?src=a_syn31" />
+               #        <media:category scheme="urn:bbc:metadata:cs:iPlayerUXCategoriesCS" label="Entertainment">9100099</media:category>
+               #        <media:category scheme="urn:bbc:metadata:cs:iPlayerUXCategoriesCS" label="Music">9100006</media:category>
+               #        <media:category scheme="urn:bbc:metadata:cs:iPlayerUXCategoriesCS" label="Pop &amp; Chart">9200069</media:category>
+               #        <media:credit role="Production Department" scheme="urn:ebu">BBC Radio 1</media:credit>
+               #        <media:credit role="Publishing Company" scheme="urn:ebu">BBC Radio 1</media:credit>
+               #        <media:thumbnail url="http://www.bbc.co.uk/iplayer/images/episode/b00djtfh_86_48.jpg" width="86" height="48" />
+               #        <media:thumbnail url="http://www.bbc.co.uk/iplayer/images/episode/b00djtfh_150_84.jpg" width="150" height="84" />
+               #        <media:thumbnail url="http://www.bbc.co.uk/iplayer/images/episode/b00djtfh_178_100.jpg" width="178" height="100" />
+               #        <media:thumbnail url="http://www.bbc.co.uk/iplayer/images/episode/b00djtfh_512_288.jpg" width="512" height="288" />
+               #        <media:thumbnail url="http://www.bbc.co.uk/iplayer/images/episode/b00djtfh_528_297.jpg" width="528" height="297" />
+               #        <media:thumbnail url="http://www.bbc.co.uk/iplayer/images/episode/b00djtfh_640_360.jpg" width="640" height="360" />
+               #        <dcterms:valid>
+               #          start=2008-09-22T15:44:20Z;
+               #          end=2008-09-29T15:02:00Z;
+               #          scheme=W3C-DTF
+               #        </dcterms:valid>
+               #      </media:content>
+               #    </link>
+               #    <link rel="self" href="http://feeds.bbc.co.uk/iplayer/episode/b00djtfh?format=atom" type="application/atom+xml" title="22/09/2008" />
+               #    <link rel="related" href="http://www.bbc.co.uk/programmes/b006wks4/microsite" type="text/html" title="Edith Bowman" />
+               #    <link rel="parent" href="http://feeds.bbc.co.uk/iplayer/programme_set/b006wks4" type="application/atom+xml" title="Edith Bowman" />
+               #  </entry>
+               #</feed>
+                       
+               $expiry = $1 if $entry3 =~ m{<dcterms:valid>\s*start=.+?;\s*end=(.*?);};
+               $available = $1 if $entry3 =~ m{<dcterms:valid>\s*start=(.+?);\s*end=.*?;};
+               $duration = $1 if $entry3 =~ m{duration=\"(\d+?)\"};
+               $prog_type = $1 if $entry3 =~ m{medium=\"(\w+?)\"};
+               $longdesc = $1 if $entry3 =~ m{<media:description>\s*(.*?)\s*<\/media:description>};
+               $guidance = $1 if $entry3 =~ m{<media:rating scheme="urn:simple">(.+?)<\/media:rating>};
+               $player = $1 if $entry3 =~ m{<media:player\s*url=\"(.*?)\"\s*\/>};
+               $thumbnail = $1 if $entry3 =~ m{<media:thumbnail url="([^"]+?)"\s+width="150"\s+height="84"\s*/>};
+       
+               my @cats;
+               for (split /<media:category scheme=\".+?\"/, $entry3) {
+                       push @cats, $1 if m{\s*label="(.+?)">\d+<\/media:category>};
+               }
+               $categories = join ',', @cats;
+
+               # populate version pid metadata 
+               get_version_pids($ua, $pid);
+
+       # ITV Catch-Up metadata
+       } elsif ( $prog{$pid}{type} eq 'itv' ) {
+               my $prog_metadata_url_itv = 'http://www.itv.com/_app/Dynamic/CatchUpData.ashx?ViewType=5&Filter='; # +<pid>
+               $entry3 = request_url_retry($ua, "${prog_metadata_url_itv}${pid}", 3, '', '');
+               decode_entities($entry3);
+               logger "DEBUG: ${prog_metadata_url_itv}${pid}:\n$entry3\n\n" if $opt{debug};
+               # Flatten
+               $entry3 =~ s|[\r\n]||g;
+
+               #div class="itvCatchUpPlayerPanel" xmlns:ms="urn:schemas-microsoft-com:xslt">
+               #  <div class="cu-sponsor"><a href="http://sam.itv.com/accipiter/adclick/CID=000040d70000000000000000/acc_random=1/SITE=CLICKTRACK/AREAITVCATCHUP.VIDEO=CLICKTRACK..FREEVIEW.SPONSORBUTTON.OCT08/AAMSZ=120X60/pageid=1" title="ITV Player in assocation with Freeview"><img src="/_app/img/catchup/catchup_video_freeview2.jpg" alt="ITV Player is sponsored by Freeview"></a></div>
+               #  <h2>Doctor Zhivago</h2>
+               #  <p>Part 1 of 3. Dramatisation of the epic novel by Boris Pasternak. Growing up in Moscow with his uncle, aunt and cousin Tonya, Yury is captivated by a stunning young girl called ...</p>
+               #  <p class="timings"><span class="date">Mon 29 Dec 2008</span><br /><br /><span>
+               #
+               #        Duration: 1hr 30 min |
+               #                                Expires in
+               #                                <strong>22</strong>
+               #                                                days
+               #                                        </span></p>
+               #  <p><a href="http://www.itv.com/CatchUp/Programmes/default.html?ViewType=1&amp;Filter=2352">3 Episodes Available
+               #        </a><br></br></p>
+               #  <p class="channelLogo"><img src="/_app/img/logos/itv3-black.gif" alt="ITV 4"></p>
+               #  <div id="cu-2-0-VideoID">33105</div>
+               #  <div id="cu-2-0-DentonId">17</div>
+               #  <div id="cu-2-0-ItemMediaUrl">http://www.itv.com//img/480x272/Doctor-Zhivago-c47828f8-a1af-4cd2-b5a2-40c18eb7e63c.jpg</div>
+               #</div><script language="javascript" type="text/javascript" xmlns:ms="urn:schemas-microsoft-com:xslt">
+               #                        SetCatchUpModuleID(0);
+               #                </script>
+               #
+
+               #<div class="itvCatchUpPlayerPanel" xmlns:ms="urn:schemas-microsoft-com:xslt">
+               #  <div class="cu-sponsor"><a href="http://sam.itv.com/accipiter/adclick/CID=000040d70000000000000000/acc_random=1/SITE=CLICKTRACK/AREAITVCATCHUP.VIDEO=CLICKTRACK..FREEVIEW.SPONSORBUTTON.OCT08/AAMSZ=120X60/pageid=1" title="ITV Player in assocation with Freeview"><img src="/_app/img/catchup/catchup_video_freeview2.jpg" alt="ITV Player is sponsored by Freeview"></a></div>
+               #  <h2>Affinity</h2>
+               #  <p>Victorian period drama with a murderous, pyschological twist.</p>
+               #  <p class="timings"><span class="date">Sun 28 Dec 2008</span><br /><br /><span>
+               #
+               #        Duration: 2hr 00 min |
+               #                                Expires in
+               #                                <strong>21</strong>
+               #                                                days
+               #                                        </span></p>
+               #  <p class="channelLogo"><img src="/_app/img/logos/itv1-black.gif" alt="ITV 2"></p>
+               #  <div class="guidance">
+               #    <div><strong>ITV Video Guidance</strong><p>This programme contains strong language and scenes of a sexual nature                                                                                                                          Â </p>
+               #    </div>
+               #  </div>
+               #  <div id="cu-2-0-VideoID">33076</div>
+               #  <div id="cu-2-0-DentonId">11</div>
+               #  <div id="cu-2-0-ItemMediaUrl">http://www.itv.com//img/480x272/Affinity-9624033b-6e05-4784-85f7-114be0559b24.jpg</div>
+               #</div><script language="javascript" type="text/javascript" xmlns:ms="urn:schemas-microsoft-com:xslt">
+               #                        SetCatchUpModuleID(0);
+               #                </script>
+               #
+
+               #$expiry = $1 if $entry3 =~ m{<dcterms:valid>\s*start=.+?;\s*end=(.*?);};
+               $available = $1 if $entry3 =~ m{<p\s+class="timings">\s*<span\s+class="date">(.+?)<\/span>};
+               $duration = $1 if $entry3 =~ m{Duration:\s*(.+?)\s+\|};
+               #$prog_type = $1 if $entry3 =~ m{medium=\"(\w+?)\"};
+               $longdesc = $1 if $entry3 =~ m{<p>(.+?)<\/p>}i;
+               $guidance = $1 if $entry3 =~ m{ITV Video Guidance<\/strong><p>\s*(.+?)[\W\s]*<\/p>};
+               #$player = $1 if $entry3 =~ m{<media:player\s*url=\"(.*?)\"\s*\/>};
+               $thumbnail = $1 if $entry3 =~ m{<div id="cu-2-0-ItemMediaUrl">(.+?)</div>};
+               $name = $1 if $entry3 =~ m{<h2>(.+?)</h2>};
+       }
+
+       # Fill in from cache if not got from metadata
+       my %metadata;
+       $metadata{pid}          = $pid;
+       $metadata{index}        = $prog{$pid}{index};
+       $metadata{name}         = $name || $prog{$pid}{name};
+       $metadata{episode}      = $episode || $prog{$pid}{episode};
+       $metadata{type}         = $prog_type || $prog{$pid}{type};
+       $metadata{duration}     = $duration || $prog{$pid}{duration};
+       $metadata{channel}      = $channel || $prog{$pid}{channel};
+       $metadata{available}    = $available || $prog{$pid}{available};
+       $metadata{expiry}       = $expiry || $prog{$pid}{expiry};
+       $metadata{versions}     = $versions || $prog{$pid}{versions};
+       $metadata{guidance}     = $guidance || $prog{$pid}{guidance};
+       $metadata{categories}   = $categories || $prog{$pid}{categories};
+       $metadata{desc}         = $longdesc || $prog{$pid}{desc};
+       $metadata{player}       = $player;
+       $metadata{thumbnail}    = $thumbnail || $prog{$pid}{thumbnail};
+
+       return %metadata;
+}
+
+
+
+# Gets the contents of a URL and retries if it fails, returns '' if no page could be retrieved
+# Usage <content> = request_url_retry(<ua>, <url>, <retries>, <succeed message>, [<fail message>]);
+sub request_url_retry {
+       my ($ua, $url, $retries, $succeedmsg, $failmsg) = @_; 
+       my $res;
+
+       my $i;
+       logger "INFO: Getting page $url\n" if $opt{verbose};
+       for ($i = 0; $i < $retries; $i++) {
+               $res = $ua->request( HTTP::Request->new( GET => $url ) );
+               if ( ! $res->is_success ) {
+                       logger $failmsg;
+               } else {
+                       logger $succeedmsg;
+                       last;
+               }
+       }
+       # Return empty string if we failed
+       return '' if $i == $retries;
+       # otherwise return content
+       return $res->content;
+}
+
+
+
+# Checks if a particular program exists (or program.exe) in the $ENV{PATH} or if it has a path already check for existence of file
+sub exists_in_path {
+       my $file = shift;
+       # If this has a path specified, does file exist
+       return 1 if $file =~ /[\/\\]/ && (-f $file || -f "${file}.exe");
+       # Search PATH
+       for (@PATH) {
+               return 1 if -f "${_}/${file}" || -f "${_}/${file}.exe";
+       }
+       return 0;
+}
+
+
+
+# Run a user specified command
+# e.g. --command 'echo "<pid> <longname> downloaded"'
+# run_user_command($pid, 'echo "<pid> <longname> downloaded"');
+sub run_user_command {
+       my $pid = shift;
+       my $command = shift;
+
+       # Substitute the fields for the pid (don't sanitize)
+       $command = substitute_fields( $pid, $command, 1 );
+
+       # Escape chars in command for shell use
+       esc_chars(\$command);
+
+       # run command
+       logger "INFO: Running command '$command'\n" if $opt{verbose};
+       my $exit_value = system $command;
+       
+       # make exit code sane
+       $exit_value = $exit_value >> 8;
+       logger "ERROR: Command Exit Code: $exit_value\n" if $exit_value;
+       logger "INFO: Command succeeded\n" if $opt{verbose} && ! $exit_value;
+        return 0;
+}
+
+
+
+# Adds pid to history file (with a timestamp) so that it is not redownloaded after deletion
+sub add_to_download_history {
+       my $pid = shift;
+       # Only add if a pid is specified
+       return 0 if ! $pid;
+       # Don't add to history if stdout streaming is used
+       return 0 if ( $opt{stdout} && $opt{nowrite} ) || $opt{streaminfo};
+
+       # Add to history
+       if ( ! open(HIST, ">> $historyfile") ) {
+               logger "WARNING: Cannot write or append to $historyfile\n\n";
+               return 1;
+       }
+       print HIST "$pid|$prog{$pid}{name}|$prog{$pid}{episode}|$prog{$pid}{type}|".time()."|$prog{$pid}{mode}\n";
+       close HIST;
+       return 0;
+}
+
+
+
+# returns a hash (<pid> => <data>) for all the pids in the history file
+sub load_download_history {
+       my %pids_downloaded;
+
+       # Return if force-download option specified or stdout streaming only
+       return %pids_downloaded if $opt{forcedownload} || $opt{stdout} || $opt{nowrite};
+
+       logger "INFO: Loading download history\n" if $opt{verbose};
+       if ( ! open(HIST, "< $historyfile") ) {
+               logger "WARNING: Cannot read $historyfile\n\n";
+               return 0;
+       }
+       while ( <HIST> ) {
+               $pids_downloaded{$1} = $2 if m{^(.+?)\|(.*)$};
+               logger "DEBUG: Loaded '$1' = '$2' from download history\n" if $opt{debug};
+       }
+       return %pids_downloaded;
+}
+
+
+
+# Checks history for previous download of this pid
+sub check_download_history {
+       my $pid = shift;
+       my ($matchedpid, $name, $episode);
+       return 0 if ! $pid;
+               
+       # Return if force-download option specified or stdout streaming only
+       return 0 if $opt{forcedownload} || $opt{stdout} || $opt{nowrite};
+       
+       if ( ! open(HIST, "< $historyfile") ) {
+               logger "WARNING: Cannot read $historyfile\n\n";
+               return 0;
+       }
+
+       # Find and parse first matching line
+       ($matchedpid, $name, $episode) = ( split /\|/, (grep /^$pid/, <HIST>)[0] )[0,1,2];
+       if ( $matchedpid ) {
+               chomp $name;
+               chomp $episode;
+               logger "INFO: $name - $episode ($pid) Already in download history ($historyfile)\n";
+               close HIST;
+               return 1;
+
+       } else {
+               logger "INFO: Programme not in download history\n" if $opt{verbose};
+               close HIST;
+               return 0;
+       }
+}
+
+
+
+# Add id3 tag to MP3 files if required
+sub tag_file {
+       my $pid = shift;
+
+       if ( $prog{$pid}{ext} eq 'mp3' ) {
+               # Return if file does not exist
+               return if ! -f $prog{$pid}{filename};
+               # Create ID3 tagging options for external tagger program (escape " for shell)
+               my ( $id3_name, $id3_episode, $id3_desc, $id3_channel ) = ( $prog{$pid}{name}, $prog{$pid}{episode}, $prog{$pid}{desc}, $prog{$pid}{channel} );
+               $id3_name =~ s|"|\"|g for ($id3_name, $id3_episode, $id3_desc, $id3_channel);
+               # Only tag if the required tool exists
+               if ( exists_in_path($id3v2) ) {
+                       logger "INFO: id3 tagging MP3 file\n";
+                       my $cmd = "$id3v2 --artist \"$id3_channel\" --album \"$id3_name\" --song \"$id3_episode\" --comment \"Description\":\"$id3_desc\" --year ".( (localtime())[5] + 1900 )." \"$prog{$pid}{filename}\" 1>&2";
+                       logger "DEGUG: Running $cmd\n" if $opt{debug};
+                       if ( system($cmd) ) {
+                               logger "WARNING: Failed to tag MP3 file\n";
+                               return 2;
+                       }
+               } else {
+                       logger "WARNING: Cannot tag MP3 file\n" if $opt{verbose};
+               }
+       }
+}
+
+
+
+# Show channels for specified type if required
+sub list_unique_element_counts {
+       my $element_name = shift;
+       my %elements;
+       logger "INFO: ".(join ',', keys %type)." $element_name List:\n" if $opt{verbose};
+       for my $pid (keys %prog) {
+               my @element;
+               # Need to separate the categories
+               if ($element_name eq 'categories') {
+                       @element = split /,/, $prog{$pid}{$element_name};
+               } else {
+                       @element[0] = $prog{$pid}{$element_name};
+               }
+               for my $element (@element) {
+                       $elements{ $element }++;
+               }
+       }
+       # display element + prog count
+       logger "$_ ($elements{$_})\n" for sort keys %elements;
+       return 0;
+}
+
+
+
+# Escape chars in string for shell use
+sub esc_chars {
+       # will change, for example, a!!a to a\!\!a
+       s/([;<>\*\|&\$!#\(\)\[\]\{\}:'"])/\\$1/g;
+       return $_;
+}
+
+
+
+# Signal handler to clean up after a ctrl-c or kill
+sub cleanup {
+       logger "INFO: Cleaning up\n" if $opt{verbose};
+       unlink $namedpipe;
+       unlink $lockfile;
+       exit 1;
+}
+
+
+
+# Return a string with formatting fields substituted for a given pid
+# no_sanitize == 2 then just substitute only
+# no_sanitize == 1 then also sanitize '/' in field values
+# no_sanitize == 0 then dont sanitize '/' in field values
+sub substitute_fields {
+       my $pid = shift;
+       my $string = shift;
+       my $no_sanitize = shift || 0;
+       my $replace;
+               
+       # Tokenize and substitute $format
+       for my $key ( keys %{ $prog{$pid} } ) {
+               # Remove/replace all non-nice-filename chars if required
+               if ($no_sanitize == 0) {
+                       $replace = sanitize_path( $prog{$pid}{$key} );
+               } else {
+                       $replace = $prog{$pid}{$key};
+               }
+               $string =~ s|\<$key\>|$replace|gi;
+       }
+       if ($no_sanitize == 0) {
+               $replace = sanitize_path( $pid );       
+       } else {
+               $replace = $pid;
+       }
+       $string =~ s|<pid>|$replace|gi;
+       # Remove/replace all non-nice-filename chars if required except for fwd slashes
+       if ($no_sanitize != 2) {
+               return sanitize_path( $string, 1 );
+       } else {
+               return $string;
+       }
+}
+
+
+
+# Make a filename/path sane (optionally allow fwd slashes)
+sub sanitize_path {
+       my $string = shift;
+       my $allow_fwd_slash = shift || 0;
+
+       # Remove fwd slash if reqd
+       $string =~ s/\//_/g if ! $allow_fwd_slash;
+
+       # Replace backslashes with _ regardless
+       $string =~ s/\\/_/g;
+       # Sanitize by default
+       $string =~ s/\s/_/g if (! $opt{whitespace}) && (! $allow_fwd_slash);
+       $string =~ s/[^\w_\-\.\/\s]//gi if ! $opt{whitespace};
+       return $string;
+}
+
+
+
+
+# Save the options on the cmdline as a PVR search with the specified name
+sub pvr_add {
+       my $name = shift;
+       my @search_args = @_;
+       my @options;
+       # validate name
+       if ( $name !~ m{[\w\-\+]+} ) {
+               logger "ERROR: Invalid PVR search name '$name'\n";
+               return 1;
+       }
+       # Parse valid options and create array (ignore options from the options files that have not been overriden on the cmdline)
+       for (grep /^(amode|vmode|long|output.*|proxy|subdir|whitespace|versions|type|(exclude)?category|(exclude)?channel|command|realaudio|mp3audio|wav|raw|bandwidth|subtitles|suboffset|since|versionlist|verbose)$/, sort {lc $a cmp lc $b} keys %opt_cmdline) {
+               if ( defined $opt_cmdline{$_} ) {
+                               push @options, "$_ $opt_cmdline{$_}";
+                               logger "DEBUG: Adding option $_ = $opt_cmdline{$_}\n" if $opt{debug};
+               }
+       }
+       # Add search args to array
+       for ( my $count = 0; $count <= $#search_args; $count++ ) {
+               push @options, "search${count} $search_args[$count]";
+               logger "DEBUG: Adding search${count} = $search_args[$count]\n" if $opt{debug};
+       }
+       # Save search to file
+       pvr_save( $name, @options );
+       logger "INFO: Added PVR search '$name':\n";
+       return 0;
+}
+
+
+
+# Delete the named PVR search
+sub pvr_del {
+       my $name = shift;
+       # validate name
+       if ( $name !~ m{[\w\-\+]+} ) {
+               logger "ERROR: Invalid PVR search name '$name'\n";
+               return 1;
+       }
+       # Delete pvr search file
+       if ( -f ${pvr_dir}.$name ) {
+               unlink ${pvr_dir}.$name;
+       } else {
+               logger "ERROR: PVR search '$name' does not exist\n";
+               return 1;
+       }
+       return 0;
+}
+
+
+
+# Display all the PVR searches
+sub pvr_display_list {
+       # Load all the PVR searches
+       pvr_load_list();
+       # Print out list
+       logger "All PVR Searches:\n\n";
+       for my $name ( sort {lc $a cmp lc $b} keys %pvrsearches ) {
+               # Report whether disabled
+               if ( $pvrsearches{$name}{disable} ) {
+                       logger "(Disabled) PVR Search '$name':\n";
+               } else {
+                       logger "PVR Search '$name':\n";
+               }
+               for ( sort keys %{ $pvrsearches{$name} } ) {
+                       logger "\t$_ = $pvrsearches{$name}{$_}\n";
+               }
+               logger "\n";
+       }
+       return 0;
+}
+
+
+
+# Load all the PVR searches into %pvrsearches
+sub pvr_load_list {
+       # Make dir if not existing
+       mkpath $pvr_dir if ! -d $pvr_dir;
+       # Get list of files in pvr_dir
+       # open file with handle DIR
+       opendir( DIR, $pvr_dir );
+       if ( ! opendir( DIR, $pvr_dir) ) {
+               logger "ERROR: Cannot open directory $pvr_dir\n";
+               return 1;
+       }
+       # Get contents of directory (ignoring . .. and ~ files)
+       my @files = grep ! /(^\.{1,2}$|^.*~$)/, readdir DIR;
+       # Close the directory
+       closedir DIR;
+       # process each file
+       for my $file (@files) {
+               chomp($file);
+               # Re-add the dir
+               $file = "${pvr_dir}/$file";
+               next if ! -f $file;
+               if ( ! open (PVR, "< $file") ) {
+                       logger "WARNING: Cannot read PVR search file $file\n";
+                       next;
+               }
+               my @options = <PVR>;
+               close PVR;
+               # Get search name from filename
+               my $name = $file;
+               $name =~ s/^.*\/([^\/]+?)$/$1/g;
+               for (@options) {
+                       /^\s*([\w\-_]+?)\s+(.*)\s*$/;
+                       logger "DEBUG: PVR search '$name': option $1 = $2\n" if $opt{debug};
+                       $pvrsearches{$name}{$1} = $2;
+               }
+               logger "INFO: Loaded PVR search '$name'\n" if $opt{verbose};
+       }
+       logger "INFO: Loaded PVR search list\n" if $opt{verbose};
+       return 0;
+}
+
+
+
+# Save the array options specified as a PVR search
+sub pvr_save {
+       my $name = shift;
+       my @options = @_;
+       # Make dir if not existing
+       mkpath $pvr_dir if ! -d $pvr_dir;
+       # Open file
+       if ( ! open (PVR, "> ${pvr_dir}/${name}") ) { 
+               logger "ERROR: Cannot save PVR search to ${pvr_dir}.$name\n";
+               return 1;
+       }
+       # Write options array to file
+       for (@options) {
+               print PVR "$_\n";
+       }
+       close PVR;
+       logger "INFO: Saved PVR search '$name'\n";
+       return 0;
+}
+
+
+
+# Clear all exisiting global args and opts then load the options specified in the default options and specified PVR search
+sub pvr_load_options {
+       my $name = shift;
+       # Clear out existing options hash
+       %opt = ();
+       # Re-read options from the options files - these will act as defaults
+       for ( keys %opt_file ) {
+               $opt{$_} = $opt_file{$_} if defined $opt_file{$_};
+       }
+       # Clear search args
+       @search_args = ();
+       # Set each option from the search
+       for ( sort {$a <=> $b} keys %{ $pvrsearches{$name} } ) {
+               # Add to list of search args if this is not an option
+               if ( /^search\d+$/ ) {
+                       logger "INFO: $_ = $pvrsearches{$name}{$_}\n" if $opt{verbose};
+                       push @search_args, $pvrsearches{$name}{$_};
+               # Else populate options, ignore disable option
+               } elsif ( $_ ne 'disable' ) {
+                       logger "INFO: Option: $_ = $pvrsearches{$name}{$_}\n" if $opt{verbose};
+                       $opt{$_} = $pvrsearches{$name}{$_};
+               }
+       }
+       # Allow cmdline args to override those in the PVR search
+       # Re-read options from the cmdline
+       for ( keys %opt_cmdline ) {
+               $opt{$_} = $opt_cmdline{$_} if defined $opt_cmdline{$_};
+       }       return 0;
+}
+
+
+
+# Disable a PVR search by adding 'disable 1' option
+sub pvr_disable {
+       my $name = shift;
+       pvr_load_list();
+       my @options;
+       for ( keys %{ $pvrsearches{$name} }) {
+               push @options, "$_ $pvrsearches{$name}{$_}";
+       }
+       # Add the disable option
+       push @options, 'disable 1';
+       pvr_save( $name, @options );
+       return 0;
+}
+
+
+
+# Re-enable a PVR search by removing 'disable 1' option
+sub pvr_enable {
+       my $name = shift;
+       pvr_load_list();
+       my @options;
+       for ( keys %{ $pvrsearches{$name} }) {
+               push @options, "$_ $pvrsearches{$name}{$_}";
+       }
+       # Remove the disable option
+       @options = grep !/^disable\s/, @options;
+       pvr_save( $name, @options );    
+       return 0;
+}
+
+
+
+# Send a message to STDOUT so that cron can use this to email 
+sub pvr_report {
+       my $pid = shift;
+       print STDOUT "New $prog{$pid}{type} programme: '$prog{$pid}{name} - $prog{$pid}{episode}', '$prog{$pid}{desc}'\n";
+       return 0;
+}
+
+
+
+# Lock file detection (<lockfile>, <stale_secs>)
+# Global $lockfile
+sub lockfile {
+       my $stale_time = shift || 86400;
+       my $now = time();
+       # if lockfile exists then quit as we are already running
+       if ( -T $lockfile ) {
+               if ( ! open (LOCKFILE, $lockfile) ) {
+                       logger "ERROR: Cannot read lockfile '$lockfile'\n";
+                       exit 1;
+               }
+               my @lines = <LOCKFILE>;
+               close LOCKFILE;
+
+               # If the process is still running and the lockfile is newer than $stale_time seconds
+               if ( kill(0,$lines[0]) > 0 && $now < ( stat($lockfile)->mtime + $stale_time ) ) {
+                               logger "ERROR: Quitting - process is already running ($lockfile)\n";
+                               # redefine cleanup sub so that it doesn't delete $lockfile
+                               $lockfile = '';
+                               exit 0;
+               } else {
+                       logger "INFO: Removing stale lockfile\n" if $opt{verbose};
+                       unlink $lockfile;
+               }
+       }
+       # write our PID into this lockfile
+       if (! open (LOCKFILE, "> $lockfile") ) {
+               logger "ERROR: Cannot write to lockfile '$lockfile'\n";
+               exit 1;
+       }
+       print LOCKFILE $$;
+       close LOCKFILE;
+       return 0;
+}
+
diff --git a/get_iplayer_old b/get_iplayer_old
new file mode 100644 (file)
index 0000000..fd76ad0
--- /dev/null
@@ -0,0 +1,4725 @@
+#!/usr/bin/perl
+#
+# get_iplayer
+#
+# Lists and downloads BBC iPlayer audio and video streams
+# + Downloads ITVplayer Catch-Up video streams
+#
+# Author: Phil Lewis
+# Email: iplayer (at sign) linuxcentre.net
+# Web: http://linuxcentre.net/iplayer
+# License: GPLv3 (see LICENSE.txt)
+#
+# Other credits:
+#  RTMP additions: Andrej Stepanchuk
+#
+my $version = 1.15;
+#
+# Help:
+#      ./get_iplayer --help
+#
+# Changelog:
+#      http://linuxcentre.net/get_iplayer/CHANGELOG.txt
+#
+# Example Usage and Documentation:
+#      http://linuxcentre.net/getiplayer/documentation
+#
+# Todo:
+# * Use non-shell tee?
+# * Fix non-uk detection - iphone auth?
+# * Index/Download live radio streams w/schedule feeds to assist timing
+# * Podcasts for 'local' stations are missing (only a handful). They use a number of different station ids which will involve reading html to determine rss feed. 
+# * Remove all rtsp/mplayer/lame/tee dross when realaudio streams become obselete (not quite yet)
+# * Stdout mode with rtmp
+# * Do subtitle downloading after programme download so that rtmp auth doesn't timeout
+# * Scrape and index ITVplayer Non-Catch-up programmes 
+# * Download progress for ITVplayer
+
+# Known Issues:
+# * In ActivePerl/windows downloaded iPhone video files do not get renamed (remain with .partial.mov)
+# * vlc does not quit after downloading an rtsp N95 video stream (ctrl-c is required) - need a --play-and-quit option if such a thing exists
+# * rtmpdump (v1.2) of flashaudio fails at end of stream => non-zero exit code
+# * if ffmpeg trys to convert flv to mp3 it succeeds but => non-zero exit code
+# * resuming a flashaudio download fails
+# * No indication of progress with ITVplayer downloads
+
+use Env qw[@PATH];
+use Fcntl;
+use File::Copy;
+use File::Path;
+use File::stat;
+use Getopt::Long;
+use HTML::Entities;
+use HTTP::Cookies;
+use HTTP::Headers;
+use IO::Seekable;
+use IO::Socket;
+use LWP::ConnCache;
+#use LWP::Debug qw(+);
+use LWP::UserAgent;
+use POSIX qw(mkfifo);
+use strict;
+#use warnings;
+use Time::Local;
+use URI;
+
+$|=1;
+my %opt = ();
+my %opt_cmdline = (); # a hash of which options came from the cmdline rather than the options files
+my %opt_file = (); # a hash of which options came from the options files rather than the cmdline
+
+# Print to STDERR/STDOUT if not quiet unless verbose or debug
+sub logger(@) {
+       # Make sure quiet can be overridden by verbose and debug options
+       if ( $opt{verbose} || $opt{debug} || ! $opt{quiet} ) {
+               # Only send messages to STDERR if pvr or stdout options are being used.
+               if ( $opt{stdout} || $opt{pvr} || $opt{stderr} ) {
+                       print STDERR $_[0];
+               } else {
+                       print STDOUT $_[0];
+               }
+       }
+}
+
+sub usage {
+       logger <<EOF;
+get_iplayer v$version, Usage ( Also see http://linuxcentre.net/iplayer ):
+Search Programmes:  get_iplayer [<search options>] [<regex|index|pid|pidurl> ...]
+Download files:     get_iplayer --get [<search options>] <regex|index|pid|pidurl> ...
+                    get_iplayer --pid <pid|pidurl> [<options>]
+Stream Downloads:   get_iplayer --stdout [<options>] <regex|index|pid|pidurl> | mplayer -cache 2048 -
+Update get_iplayer: get_iplayer --update
+
+Search Options:
+ <regex|index|pid|url>         Search programme names based on given pattern
+ -l, --long                    Additionally search in long programme descriptions / episode names
+ --channel <regex>             Narrow search to matched channel(s)
+ --category <regex>            Narrow search to matched categories
+ --versions <regex>            Narrow search to matched programme version(s)
+ --exclude-channel <regex>     Narrow search to exclude matched channel(s)
+ --exclude-category <regex>    Narrow search to exclude matched catogories
+ --type <type>                 Only search in these types of programmes: radio, tv, podcast, all, itv (tv is default)
+ --since <hours>               Limit search to programmes added to the cache in the last N hours
+Display Options:
+ -l, --long                    Display long programme descriptions / episode names and other data
+ --terse                       Only show terse programme info (does not affect searching)
+ --tree                        Display Programme listings in a tree view
+ -i, --info                    Show full programme metadata (only if number of matches < 50)
+ --list <categories|channel>   Show a list of available categories/channels for the selected type and exit
+ --hide                        Hide previously downloaded programmes
+ --streaminfo                  Returns all of the media stream urls of the programme(s)
+
+Download Options:
+ -g, --get                     Download matching programmes
+ -x, --stdout                  Additionally stream to STDOUT (so you can pipe output to a player)
+ -p, --proxy <url>             Web proxy URL spec
+ --partial-proxy               Works around for some broken web proxies (try this extra option if your proxy fails)
+ --pid <pid|url>               Download an arbitrary pid that does not appear in the index
+ --force-download              Ignore download history (unsets --hide option also)
+ --amode <mode>                Download mode of Audio: mp3, flashaudio or realaudio (default: mp3 fallback to realaudio)
+ --vmode <mode>                Download mode for Video: iphone, rtmp, n95, flashhigh, flashnormal, or flashwii (default: iphone)
+ --wav                         In radio realaudio mode output as wav and don't transcode to mp3
+ --raw                         Don't transcode or change the downloaded stream in any way (i.e. radio/realaudio, rtmp/flv, iphone/mov)
+ --bandwidth                   In radio realaudio mode specify the link bandwidth in bps for rtsp streaming (default 512000)
+ --subtitles                   In TV mode, download subtitles into srt/SubRip format if available
+ --suboffset <offset>          Offset the subtitle timestamps by the specified number of milliseconds
+ --version-list <versions>     Override the version of programme to download (e.g. '--version-list signed,default')
+ -t, --test                    Test only - no download (will show programme type)
+PVR Options:
+ --pvr                         Runs the PVR download using all saved PVR searches (intended to be run every hour from cron etc)
+ --pvradd <search name>        Add the current search terms to the named PVR search
+ --pvrdel <search name>        Remove the named search from the PVR searches
+ --pvr-enable <search name>    Enable a previously disabled named PVR search
+ --pvr-disable <search name>   Disable (not delete) a named PVR search
+ --pvrlist                     Show the PVR search list
+
+Output Options:
+ -o, --output <dir>            Default Download output directory for all downloads
+ --outputradio <dir>           Download output directory for radio
+ --outputtv <dir>              Download output directory for tv
+ --outputpodcast <dir>         Download output directory for podcasts
+ --file-prefix <format>        The filename prefix (excluding dir and extension) using formatting fields. e.g. '<name>-<episode>-<pid>'
+ -s, --subdir                  Downloaded files into Programme name subdirectory
+ -n, --nowrite                 No writing of file to disk (use with -x to prevent a copy being stored on disk)
+ -w, --whitespace              Keep whitespace (and escape chars) in filenames
+ -q, --quiet                   No logging output
+ -c, --command <command>       Run user command after successful download using args such as <pid>, <name> etc
+
+Config Options:
+ -f, --flush, --refresh        Refresh cache
+ -e, --expiry <secs>           Cache expiry in seconds (default 4hrs)
+ --symlink <file>              Create symlink to <file> once we have the header of the download
+ --fxd <file>                  Create Freevo FXD XML in specified file
+ --mythtv <file>               Create Mythtv streams XML in specified file
+ --xml-channels                Create freevo/Mythtv menu of channels -> programme names -> episodes
+ --xml-names                   Create freevo/Mythtv menu of programme names -> episodes
+ --xml-alpha                   Create freevo/Mythtv menu sorted alphabetically by programme name
+ --html <file>                 Create basic HTML index of programmes in specified file
+ --mplayer <path>              Location of mplayer binary
+ --ffmpeg <path>               Location of ffmpeg binary
+ --lame <path>                 Location of lame binary
+ --id3v2 <path>                Location of id3v2 binary
+ --rtmpdump <path>             Location of rtmpdump binary
+ --vlc <path>                  Location of vlc or cvlc binary
+ -v, --verbose                 Verbose
+ -u, --update                  Update get_iplayer if a newer one exists
+ -h, --help                    Help
+ --save                        Save specified options as default in .get_iplayer/config
+EOF
+       exit 1;
+}
+
+# Get cmdline params
+my $save;
+# This is where all profile data/caches/cookies etc goes
+my $profile_dir;
+# This is where system-wide default options are specified
+my $optfile_system;
+# Options on unix-like systems
+if ( defined $ENV{HOME} ) {
+       $profile_dir = $ENV{HOME}.'/.get_iplayer';
+       $optfile_system = '/etc/get_iplayer/options';
+
+# Otherwise look for windows style file locations
+} elsif ( defined $ENV{USERPROFILE} ) {
+       $profile_dir = $ENV{USERPROFILE}.'/.get_iplayer';
+       $optfile_system = $ENV{ALLUSERSPROFILE}.'/get_iplayer/options';
+}
+# Make profile dir if it doesnt exist
+mkpath $profile_dir if ! -d $profile_dir;
+# Personal options go here
+my $optfile = "${profile_dir}/options";
+# PVR Lockfile location
+my $lockfile;
+# Parse options if we're not saving options (system-wide options are overridden by personal options)
+if ( ! grep /\-\-save/, @ARGV ) {
+       $opt{debug} = 1 if grep /\-\-debug/, @ARGV;
+       read_options_file($optfile_system);
+       read_options_file($optfile);
+}
+
+# Allow bundling of single char options
+Getopt::Long::Configure ("bundling");
+# cmdline opts take precedence
+GetOptions(
+       "amode=s"                       => \$opt_cmdline{amode},
+       "bandwidth=n"                   => \$opt_cmdline{bandwidth},
+       "category=s"                    => \$opt_cmdline{category},
+       "channel=s"                     => \$opt_cmdline{channel},
+       "c|command=s"                   => \$opt_cmdline{command},
+       "debug"                         => \$opt_cmdline{debug},
+       "exclude-category=s"            => \$opt_cmdline{excludecategory},
+       "exclude-channel=s"             => \$opt_cmdline{excludechannel},
+       "expiry|e=n"                    => \$opt_cmdline{expiry},
+       "ffmpeg=s"                      => \$opt_cmdline{ffmpeg},
+       "file-prefix|fileprefix=s"      => \$opt_cmdline{fileprefix},
+       "flush|refresh|f"               => \$opt_cmdline{flush},
+       "force-download"                => \$opt_cmdline{forcedownload},
+       "fxd=s"                         => \$opt_cmdline{fxd},
+       "get|g"                         => \$opt_cmdline{get},
+       "help|h"                        => \$opt_cmdline{help},
+       "hide"                          => \$opt_cmdline{hide},
+       "html=s"                        => \$opt_cmdline{html},
+       "id3v2=s"                       => \$opt_cmdline{id3v2},
+       "i|info"                        => \$opt_cmdline{info},
+       "lame=s"                        => \$opt_cmdline{lame},
+       "list=s"                        => \$opt_cmdline{list},
+       "long|l"                        => \$opt_cmdline{long},
+       "mp3audio"                      => \$opt_cmdline{mp3audio},
+       "mplayer=s"                     => \$opt_cmdline{mplayer},
+       "mythtv=s"                      => \$opt_cmdline{mythtv},
+       "n95"                           => \$opt_cmdline{n95},
+       "no-write|nowrite|n"            => \$opt_cmdline{nowrite},
+       "output|o=s"                    => \$opt_cmdline{output},
+       "outputpodcast=s"               => \$opt_cmdline{outputpodcast},
+       "outputradio=s"                 => \$opt_cmdline{outputradio},
+       "outputtv=s"                    => \$opt_cmdline{outputtv},
+       "partial-proxy"                 => \$opt_cmdline{partialproxy},
+       "pid=s"                         => \$opt_cmdline{pid},
+       "proxy|p=s"                     => \$opt_cmdline{proxy},
+       "pvr"                           => \$opt_cmdline{pvr},
+       "pvradd|pvr-add=s"              => \$opt_cmdline{pvradd},
+       "pvrdel|pvr-del=s"              => \$opt_cmdline{pvrdel},
+       "pvrdisable|pvr-disable=s"      => \$opt_cmdline{pvrdisable},
+       "pvrenable|pvr-enable=s"        => \$opt_cmdline{pvrenable},
+       "pvrlist|pvr-list"              => \$opt_cmdline{pvrlist},
+       "q|quiet"                       => \$opt_cmdline{quiet},
+       "raw"                           => \$opt_cmdline{raw},
+       "realaudio"                     => \$opt_cmdline{realaudio},
+       "rtmp"                          => \$opt_cmdline{rtmp},
+       "rtmpdump=s"                    => \$opt_cmdline{rtmpdump},
+       "save"                          => \$save,
+       "since=n"                       => \$opt_cmdline{since},
+       "stdout|stream|x"               => \$opt_cmdline{stdout},
+       "streaminfo"                    => \$opt_cmdline{streaminfo},
+       "subdirs|subdir|s"              => \$opt_cmdline{subdir},
+       "suboffset=n"                   => \$opt_cmdline{suboffset},
+       "subtitles"                     => \$opt_cmdline{subtitles},
+       "symlink|freevo=s"              => \$opt_cmdline{symlink},
+       "test|t"                        => \$opt_cmdline{test},
+       "terse"                         => \$opt_cmdline{terse},
+       "tree"                          => \$opt_cmdline{tree},
+       "type=s"                        => \$opt_cmdline{type},
+       "update|u"                      => \$opt_cmdline{update},
+       "versionlist|version-list=s"    => \$opt_cmdline{versionlist},
+       "versions=s"                    => \$opt_cmdline{versions},
+       "verbose|v"                     => \$opt_cmdline{verbose},
+       "vlc=s"                         => \$opt_cmdline{vlc},
+       "vmode=s"                       => \$opt_cmdline{vmode},
+       "wav"                           => \$opt_cmdline{wav},
+       "whitespace|ws|w"               => \$opt_cmdline{whitespace},
+       "xml-channels|fxd-channels"     => \$opt_cmdline{xmlchannels},
+       "xml-names|fxd-names"           => \$opt_cmdline{xmlnames},
+       "xml-alpha|fxd-alpha"           => \$opt_cmdline{xmlalpha},
+) || die usage();
+usage() if $opt_cmdline{help};
+
+# Merge cmdline options into %opt
+for ( keys %opt_cmdline ) {
+       $opt{$_} = $opt_cmdline{$_} if defined $opt_cmdline{$_};
+}
+# Save opts if specified
+save_options_file( $optfile ) if $save;
+
+
+# Global vars
+
+# Programme data structure
+# $prog{$pid} = {
+#      'index'         => <index number>,
+#      'name'          => <programme short name>,
+#      'episode'       => <Episode info>,
+#      'desc'          => <Long Description>,
+#      'available'     => <Date/Time made available or remaining>,
+#      'duration'      => <duration in HH:MM:SS>
+#      'versions'      => <comma separated list of versions, e.g default, signed>
+#      'thumbnail'     => <programme thumbnail url>
+#      'channel        => <channel>
+#      'categories'    => <Comma separated list of categories>
+#      'type'          => <Type: tv, radio, itv or podcast>
+#      'timeadded'     => <timestamp when programme was added to cache>
+#      'longname'      => <Long name (only parsed in stage 1 download)>,
+#      'version'       => <selected version e.g default, signed, etc - only set before d/load>
+#      'filename'      => <Path and Filename of saved file - set only while downloading>
+#      'dir'           => <Filename Directory of saved file - set only while downloading>
+#      'fileprefix'    => <Filename Prefix of saved file - set only while downloading>
+#      'ext'           => <Filename Extension of saved file - set only while downloading>
+#};
+my %prog;
+my %type;
+my %pids_history;
+my %index_pid; # Hash to obtain pid given an index
+my $now;
+my $childpid;
+
+# Static URLs
+my $channel_feed_url           = 'http://feeds.bbc.co.uk/iplayer'; # /$channel/list/limit/400
+my $prog_feed_url              = 'http://feeds.bbc.co.uk/iplayer/episode/'; # $pid
+my $prog_iplayer_metadata      = 'http://www.bbc.co.uk/iplayer/playlist/'; # $pid
+my $media_stream_data_prefix   = 'http://www.bbc.co.uk/mediaselector/4/mtis/stream/'; # $verpid
+my $iphone_download_prefix     = 'http://www.bbc.co.uk/mediaselector/3/auth/iplayer_streaming_http_mp4';
+my $prog_page_prefix           = 'http://www.bbc.co.uk/programmes';
+my $thumbnail_prefix           = 'http://www.bbc.co.uk/iplayer/images/episode';
+my $metadata_xml_prefix                = 'http://www.bbc.co.uk/iplayer/metafiles/episode'; # /${pid}.xml
+my $metadata_mobile_prefix     = 'http://www.bbc.co.uk/iplayer/widget/episodedetail/episode'; # /${pid}/template/mobile/service_type/tv/
+my $podcast_index_feed_url     = 'http://downloads.bbc.co.uk/podcasts/ppg.xml';
+my $version_url                        = 'http://linuxcentre.net/get_iplayer/VERSION-get_iplayer';
+my $update_url                 = 'http://linuxcentre.net/get_iplayer/get_iplayer'; # disabled since this is a modified version
+
+# Static hash definitions
+my %channels;
+$channels{tv} = {
+       'bbc_one'                               => 'tv|BBC One',
+       'bbc_two'                               => 'tv|BBC Two',
+       'bbc_three'                             => 'tv|BBC Three',
+       'bbc_four'                              => 'tv|BBC Four',
+       'cbbc'                                  => 'tv|CBBC',
+       'cbeebies'                              => 'tv|CBeebies',
+       'bbc_news24'                            => 'tv|BBC News 24',
+       'bbc_parliament'                        => 'tv|BBC Parliament',
+       'bbc_one_northern_ireland'              => 'tv|BBC One Northern Ireland',
+       'bbc_one_scotland'                      => 'tv|BBC One Scotland',
+       'bbc_one_wales'                         => 'tv|BBC One Wales',
+       'bbc_webonly'                           => 'tv|BBC Web Only',
+       'bbc_hd'                                => 'tv|BBC HD',
+       'bbc_alba'                              => 'tv|BBC Alba',
+       'categories/news/tv'                    => 'tv|BBC News',
+       'categories/sport/tv'                   => 'tv|BBC Sport',
+#      'categories/tv'                         => 'tv|All',
+       'categories/signed'                     => 'tv|Signed',
+};
+
+$channels{radio} = {
+       'bbc_1xtra'                             => 'radio|BBC 1Xtra',
+       'bbc_radio_one'                         => 'radio|BBC Radio 1',
+       'bbc_radio_two'                         => 'radio|BBC Radio 2',
+       'bbc_radio_three'                       => 'radio|BBC Radio 3',
+       'bbc_radio_four'                        => 'radio|BBC Radio 4',
+       'bbc_radio_five_live'                   => 'radio|BBC Radio 5 live',
+       'bbc_radio_five_live_sports_extra'      => 'radio|BBC 5 live Sports Extra',
+       'bbc_6music'                            => 'radio|BBC 6 Music',
+       'bbc_7'                                 => 'radio|BBC 7',
+       'bbc_asian_network'                     => 'radio|BBC Asian Network',
+       'bbc_radio_foyle'                       => 'radio|BBC Radio Foyle',
+       'bbc_radio_scotland'                    => 'radio|BBC Radio Scotland',
+       'bbc_radio_nan_gaidheal'                => 'radio|BBC Radio Nan Gaidheal',
+       'bbc_radio_ulster'                      => 'radio|BBC Radio Ulster',
+       'bbc_radio_wales'                       => 'radio|BBC Radio Wales',
+       'bbc_radio_cymru'                       => 'radio|BBC Radio Cymru',
+       'bbc_world_service'                     => 'radio|BBC World Service',
+#      'categories/radio'                      => 'radio|All',
+       'bbc_radio_cumbria'                     => 'radio|BBC Cumbria',
+       'bbc_radio_newcastle'                   => 'radio|BBC Newcastle',
+       'bbc_tees'                              => 'radio|BBC Tees',
+       'bbc_radio_lancashire'                  => 'radio|BBC Lancashire',
+       'bbc_radio_merseyside'                  => 'radio|BBC Merseyside',
+       'bbc_radio_manchester'                  => 'radio|BBC Manchester',
+       'bbc_radio_leeds'                       => 'radio|BBC Leeds',
+       'bbc_radio_sheffield'                   => 'radio|BBC Sheffield',
+       'bbc_radio_york'                        => 'radio|BBC York',
+       'bbc_radio_humberside'                  => 'radio|BBC Humberside',
+       'bbc_radio_lincolnshire'                => 'radio|BBC Lincolnshire',
+       'bbc_radio_nottingham'                  => 'radio|BBC Nottingham',
+       'bbc_radio_leicester'                   => 'radio|BBC Leicester',
+       'bbc_radio_derby'                       => 'radio|BBC Derby',
+       'bbc_radio_stoke'                       => 'radio|BBC Stoke',
+       'bbc_radio_shropshire'                  => 'radio|BBC Shropshire',
+       'bbc_wm'                                => 'radio|BBC WM',
+       'bbc_radio_coventry_warwickshire'       => 'radio|BBC Coventry &amp; Warwickshire',
+       'bbc_radio_hereford_worcester'          => 'radio|BBC Hereford &amp; Worcester',
+       'bbc_radio_northampton'                 => 'radio|BBC Northampton',
+       'bbc_three_counties_radio'              => 'radio|BBC Three Counties',
+       'bbc_radio_cambridge'                   => 'radio|BBC Cambridgeshire',
+       'bbc_radio_norfolk'                     => 'radio|BBC Norfolk',
+       'bbc_radio_suffolk'                     => 'radio|BBC Suffolk',
+       'bbc_radio_essex'                       => 'radio|BBC Essex',
+       'bbc_london'                            => 'radio|BBC London',
+       'bbc_radio_kent'                        => 'radio|BBC Kent',
+       'bbc_southern_counties_radio'           => 'radio|BBC Southern Counties',
+       'bbc_radio_oxford'                      => 'radio|BBC Oxford',
+       'bbc_radio_berkshire'                   => 'radio|BBC Berkshire',
+       'bbc_radio_solent'                      => 'radio|BBC Solent',
+       'bbc_radio_gloucestershire'             => 'radio|BBC Gloucestershire',
+       'bbc_radio_swindon'                     => 'radio|BBC Swindon',
+       'bbc_radio_wiltshire'                   => 'radio|BBC Wiltshire',
+       'bbc_radio_bristol'                     => 'radio|BBC Bristol',
+       'bbc_radio_somerset_sound'              => 'radio|BBC Somerset',
+       'bbc_radio_devon'                       => 'radio|BBC Devon',
+       'bbc_radio_cornwall'                    => 'radio|BBC Cornwall',
+       'bbc_radio_guernsey'                    => 'radio|BBC Guernsey',
+       'bbc_radio_jersey'                      => 'radio|BBC Jersey',
+};
+
+# User Agents
+my %user_agent = (
+       coremedia       => 'Apple iPhone v1.1.1 CoreMedia v1.0.0.3A110a',
+       safari          => 'Mozilla/5.0 (iPhone; U; CPU like Mac OS X; en) AppleWebKit/420.1 (KHTML, like Gecko) Version/3.0 Mobile/3A110a Safari/419.3',
+       update          => "get_iplayer updater (v${version} - $^O)",
+       desktop         => 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-GB; rv:1.9) Gecko/2008052906 Firefox/3.0',
+       get_iplayer     => "get_iplayer/$version $^O",
+);
+
+# Setup signal handlers
+$SIG{INT} = $SIG{PIPE} =\&cleanup;
+
+# Other Non-option dependant vars
+my %cachefile = (
+       'itv'           => "${profile_dir}/itv.cache",
+       'tv'            => "${profile_dir}/tv.cache",
+       'radio'         => "${profile_dir}/radio.cache",
+       'podcast'       => "${profile_dir}/podcast.cache",
+);
+my $get_iplayer_stream = 'get_iplayer_freevo_wrapper'; # Location of wrapper script for streaming with mplayer/xine on freevo
+my $historyfile                = "${profile_dir}/download_history";
+my $pvr_dir            = "${profile_dir}/pvr/";
+my $cookiejar          = "${profile_dir}/cookies";
+my $namedpipe          = "${profile_dir}/namedpipe.$$";
+my $lwp_request_timeout        = 20;
+my $info_limit         = 40;
+my $iphone_block_size  = 0x2000000; # 32MB
+
+
+# Option dependant var definitions
+my %download_dir;
+my $cache_secs;
+my $mplayer;
+#my $mencoder;
+my $ffmpeg;
+my $ffmpeg_opts;
+my $rtmpdump;
+my $mplayer_opts;
+my $lame;
+my $lame_opts;
+my $vlc;
+my $vlc_opts;
+my $id3v2;
+my $tee;
+my $bandwidth;
+my @version_search_list;
+my $proxy_url;
+my @search_args = @ARGV;
+# Assume search term is '.*' if nothing is specified - i.e. lists all programmes
+push @search_args, '.*' if ! $search_args[0];
+
+
+# PVR functions
+my %pvrsearches; # $pvrsearches{searchname}{<option>} = <value>;
+if ( $opt{pvradd} ) {
+       pvr_add( $opt{pvradd}, @search_args );
+       exit 0;
+
+} elsif ( $opt{pvrdel} ) {
+       pvr_del( $opt{pvrdel} );
+       exit 0;
+
+} elsif ( $opt{pvrdisable} ) {
+       pvr_disable( $opt{pvrdisable} );
+       exit 0;
+
+} elsif ( $opt{pvrenable} ) {
+       pvr_enable( $opt{pvrenable} );
+       exit 0;
+
+} elsif ( $opt{pvrlist} ) {
+       pvr_display_list();
+       exit 0;
+
+# Load all PVR searches and run one-by-one
+} elsif ( $opt{pvr} ) {
+       # PVR Lockfile detection (with 12 hrs stale lockfile check)
+       $lockfile = "${profile_dir}/pvr_lock";
+       lockfile(43200) if ! $opt{test};
+       # Don't attempt to download pids in download history
+       %pids_history = load_download_history();
+       # Load all PVR searches
+       pvr_load_list();
+       # Display default options
+       display_default_options();
+       # For each PVR search
+       for my $name ( sort {lc $a cmp lc $b} keys %pvrsearches ) {
+               # Ignore if this search is disabled
+               if ( $pvrsearches{$name}{disable} ) {
+                       logger "\nSkipping disabled PVR Search '$name'\n" if $opt{verbose};
+                       next;
+               }
+               logger "\nRunning PVR Search '$name'\n";
+               # Clear then Load options for specified pvr search name
+               pvr_load_options($name);
+               # Switch on --hide option
+               $opt{hide} = 1;
+               # Dont allow --flush with --pvr
+               $opt{flush} = '';
+               # Do the downloads (force --get option)
+               $opt{get} = 1 if ! $opt{test};
+               process_matches( @search_args );
+       }
+
+# Else just process command line args
+} else {
+       %pids_history = load_download_history() if $opt{hide};
+       process_matches( @search_args );
+}
+exit 0;
+
+
+
+# Use the specified options to process the matches in specified array
+sub process_matches {
+       my @search_args = @_;
+       # Show options
+       display_current_options() if $opt{verbose};
+       # Clear prog hash
+       %prog = ();
+       logger "INFO: Search args: '".(join "','", @search_args)."'\n" if $opt{verbose};
+
+       # Option dependant vars
+       %download_dir   = (
+               'tv'            => $opt{outputtv} || $opt{output} || $ENV{IPLAYER_OUTDIR} || '.',
+               'itv'           => $opt{outputtv} || $opt{output} || $ENV{IPLAYER_OUTDIR} || '.',
+               'radio'         => $opt{outputradio} || $opt{output} || $ENV{IPLAYER_OUTDIR} || '.',
+               'podcast'       => $opt{outputpodcast} || $opt{output} || $ENV{IPLAYER_OUTDIR} || '.',
+       );
+
+       # Ensure lowercase
+       $opt{type}              = lc( $opt{type} );
+       # Expand 'all' to various prog types
+       $opt{type}              = 'tv,radio,podcast,itv' if $opt{type} =~ /(all|any)/i;
+       # Hash to store specified prog types
+       %type = ();
+       $type{$_} = 1 for split /,/, $opt{type};
+       # Default to type=tv if no type option is set
+       $type{tv}               = 1 if keys %type == 0;
+
+       $cache_secs             = $opt{expiry} || 14400;
+       $mplayer                = $opt{mplayer} || 'mplayer';
+       $mplayer_opts           = '-nolirc';
+       $mplayer_opts           .= ' -really-quiet' if $opt{quiet};
+       $ffmpeg                 = $opt{ffmpeg} || 'ffmpeg';
+       $ffmpeg_opts            = '';
+       $lame                   = $opt{lame} || 'lame';
+       $lame_opts              = '-f';
+       $lame_opts              .= ' --quiet ' if $opt{quiet};
+       $vlc                    = $opt{vlc} || 'cvlc';
+       $vlc_opts               = '-vv';
+       $id3v2                  = $opt{id3v2} || 'id3v2';
+       $tee                    = 'tee';
+       $rtmpdump               = $opt{rtmpdump} || 'rtmpdump';
+       $bandwidth              = $opt{bandwidth} || 512000; # Download bandwidth bps used for rtsp streams
+       # Order with which to search for programme versions (can be overridden by --versionlist option)
+       @version_search_list    = qw/ default original signed audiodescribed opensubtitled shortened lengthened other /;
+       @version_search_list    = split /,/, $opt{versionlist} if $opt{versionlist};
+       # Set quiet, test and get options if we're asked for streaminfo
+       if ( $opt{streaminfo} ) {
+               $opt{test}      = 1;
+               $opt{get}       = 1;
+               $opt{quiet}     = 1;
+       }
+
+       # Sanity check some conflicting options
+       if ($opt{nowrite} && (!$opt{stdout})) {
+               logger "ERROR: Cannot download to nowhere\n";
+               exit 1;
+       }
+
+       # Backward compatability options - to be removed eventually
+       $opt{vmode} = 'rtmp' if $opt{rtmp};
+       $opt{vmode} = 'n95' if $opt{n95};
+       $opt{amode} = 'realaudio' if $opt{realaudio};
+       $opt{amode} = 'iphone' if $opt{mp3audio};
+
+       # Disable rtmp modes if rtmpdump does not exist
+       if ( ( $opt{vmode} =~ /^(rtmp|flash)/ || $opt{amode} =~ /^(rtmp|flash)/ ) && ! exists_in_path($rtmpdump)) {
+               logger "\nERROR: Required program $rtmpdump does not exist (see http://linuxcentre.net/getiplayer/installation and http://linuxcentre.net/getiplayer/download), falling back to iphone mode\n";
+               $opt{vmode} = 'iphone';
+               $opt{amode} = '';
+       }
+
+       # Web proxy
+       $proxy_url = $opt{proxy} || $ENV{HTTP_PROXY} || $ENV{http_proxy} || '';
+       logger "INFO: Using Proxy $proxy_url\n" if $proxy_url;
+
+       # Update this script if required
+       if ($opt{update}) {
+               update_script();
+       }
+
+       # Check for valid dload dirs or create them
+       for ( keys %download_dir ) {
+               if ( ! -d $download_dir{$_} ) {
+                       logger "INFO: Created directory $download_dir{$_}\n";
+                       mkpath($download_dir{$_});
+               }
+       }
+
+       # Get arbitrary pid
+       if ( $opt{pid} ) {
+
+               # Temporary hack to get 'ITV Catch-up' downloads specified as --pid itv:<pid>
+               $type{itv} = 1 if $opt{pid} =~ m{^itv:(.+?)$};
+               if ( $type{itv} ) {
+                       exit 1 if ( ! $opt{streaminfo} ) && check_download_history( $opt{pid} );
+                       # Remove leading itv: tag (backwards compat)
+                       $opt{pid} =~ s/^itv:(.+?)$/$1/ig;
+                       # Force prog type to itv
+                       $prog{$opt{pid}}{type} = 'itv';
+                       download_programme( $opt{pid} );
+                       exit 0;
+               }
+
+               # Remove any url parts from the pid
+               $opt{pid} =~ s/^.*(b0[a-z,0-9]{6}).*$/$1/g;
+               # Retry loop
+               my $count;
+               my $retries = 3;
+               my $retcode;
+               exit 1 if ( ! $opt{streaminfo} ) && check_download_history( $opt{pid} );
+               for ($count = 1; $count <= $retries; $count++) {
+                       $retcode = download_programme( $opt{pid} );
+                       if ( $retcode eq 'retry' && $count < $retries ) {
+                               logger "WARNING: Retrying download for PID $opt{pid}\n";
+                       } else {
+                               $retcode = 1 if $retcode eq 'retry';
+                               last;
+                       }
+               }
+               # Add to history, tag and Run post download command if download was successful
+               if ($retcode == 0) {
+                       add_to_download_history( $opt{pid} );
+                       tag_file( $opt{pid} );
+                       run_user_command( $opt{pid}, $opt{command} ) if $opt{command};
+               } else {
+                       logger "ERROR: Failed to download for PID $opt{pid}\n";
+               }
+               exit 0;
+       }
+
+       # Get stream links from BBC iplayer site or from cache (also populates all hashes) specified in --type option
+       get_links( $_ ) for keys %type;
+
+       # List elements (i.e. 'channel' 'categories') if required and exit
+       if ( $opt{list} ) {
+               list_unique_element_counts( $opt{list} );
+               exit 0;
+       }
+
+       # Parse remaining args
+       my @match_list;
+       for ( @search_args ) {
+               chomp();
+
+               # If Numerical value < 200000
+               if ( /^[\d]+$/ && $_ < 200000) {
+                       push @match_list, $_;
+       
+               # If PID then find matching programmes with this PID
+               } elsif ( /^.*b0[a-z,0-9]{6}.*$/ ) {
+                       s/^.*(b0[a-z,0-9]{6}).*$/$1/g;
+                       push @match_list, get_regex_matches( $1 );
+       
+               # Else assume this is a programme name regex
+               } else {
+                       push @match_list, get_regex_matches( $_ );
+               }
+       }
+       
+       # De-dup matches and retain order
+       my %seen = ();
+       my @unique = grep { ! $seen{ $_ }++ } @match_list;
+       @match_list = @unique;
+       
+       # Go get the cached data for other programme types if the index numbers require it
+       my %require;
+       for ( @match_list ) {
+               $require{tv} = 1 if $_ >= 1 && $_ < 10000 && ( ! $require{tv} ) && ( ! $type{tv} );
+               $require{radio} = 1 if $_ >= 10000 && $_ < 20000 && ( ! $require{radio} ) && ( ! $type{radio} );
+               $require{podcast} = 1 if $_ >= 20000 && $_ < 30000 && ( ! $require{podcast} ) && ( ! $type{podcast} );
+               $require{podcast} = 1 if $_ >= 20000 && $_ < 30000 && ( ! $require{podcast} ) && ( ! $type{podcast} );
+               $require{itv} = 1 if $_ >= 100000 && $_ < 200000 && ( ! $require{itv} ) && ( ! $type{itv} );
+       }
+       # Get extra required programme caches
+       logger "INFO: Additionally getting cached programme data for ".(join ', ', keys %require)."\n" if %require > 0;
+       # Get stream links from BBC iplayer site or from cache (also populates all hashes)
+       for (keys %require) {
+               # Get $_ stream links
+               get_links( $_ );
+               # Add new prog types to the type list
+               $type{$_} = 1;
+       }
+       # Display list for download
+       logger "Matches:\n" if @match_list;
+       @match_list = list_progs( @match_list );
+
+       # Write HTML and XML files if required (with search options applied)
+       create_html( @match_list ) if $opt{html};
+       create_xml( $opt{fxd}, @match_list ) if $opt{fxd};
+       create_xml( $opt{mythtv}, @match_list ) if $opt{mythtv};
+
+       # Do the downloads based on list of index numbers if required
+       if ( $opt{get} || $opt{stdout} ) {
+               for (@match_list) {
+                       # Retry loop
+                       my $count = 0;
+                       my $retries = 3;
+                       my $retcode;
+                       my $pid = $index_pid{$_};
+                       next if ( ! $opt{streaminfo} ) && check_download_history( $pid );
+                       # Skip and warn if there is no pid
+                       if ( ! $pid ) {
+                               logger "ERROR: No PID for index $_ (try using --type option ?)\n";
+                               next;
+                       }
+                       for ($count = 1; $count <= $retries; $count++) {
+                               $retcode = download_programme( $pid );
+                               if ( $retcode eq 'retry' && $count < $retries ) {
+                                       logger "WARNING: Retrying download for '$prog{$pid}{name} - $prog{$pid}{episode}'\n";
+                               } else {
+                                       $retcode = 1 if $retcode eq 'retry';
+                                       last;
+                               }
+                       }
+                       # Add to history, tag file, and run post download command if download was successful
+                       if ($retcode == 0) {
+                               add_to_download_history( $pid );
+                               tag_file( $pid );
+                               run_user_command( $pid, $opt{command} ) if $opt{command};
+                               pvr_report( $pid ) if $opt{pvr};
+                       } else {
+                               logger "ERROR: Failed to download for '$prog{$pid}{name} - $prog{$pid}{episode}'\n";
+                       }
+               }
+       }
+
+       return 0;
+}
+
+
+
+# Lists progs given an array of index numbers, also returns an array with non-existent entries removed
+sub list_progs {
+       my $ua;
+       my @checked;
+       my %names;
+       # Setup user agent for a persistent connection to get programme metadata
+       if ( $opt{info} ) {
+               $ua = LWP::UserAgent->new;
+               $ua->timeout([$lwp_request_timeout]);
+               $ua->proxy( ['http'] => $proxy_url );
+               $ua->agent( $user_agent{desktop} );
+               $ua->conn_cache(LWP::ConnCache->new());
+               # Truncate array if were lisiting info and > $info_limit entries are requested - be nice to the beeb!
+               if ( $#_ >= $info_limit ) {
+                       $#_ = $info_limit - 1;
+                       logger "WARNING: Only processing the first $info_limit matches\n";
+               }
+       }
+       for (@_) {
+               my $pid = $index_pid{$_};
+               # Skip if pid isn't in index
+               next if ! $pid;
+               # Skip if already downloaded and --hide option is specified
+               next if $opt{hide} && $pids_history{$pid};
+               if (! defined $names{ $prog{$pid}{name} }) {
+                       list_prog_entry( $pid, '' );
+               } else {
+                       list_prog_entry( $pid, '', 1 );
+               }
+               $names{ $prog{$pid}{name} } = 1;
+               push @checked, $_;
+               if ( $opt{info} ) {
+                       my %metadata = get_pid_metadata( $ua, $pid );
+                       logger "\nPid:\t\t$metadata{pid}\n";
+                       logger "Index:\t\t$metadata{index}\n";
+                       logger "Type:\t\t$metadata{type}\n";
+                       logger "Duration:\t$metadata{duration}\n";
+                       logger "Channel:\t$metadata{channel}\n";
+                       logger "Available:\t$metadata{available}\n";
+                       logger "Expires:\t$metadata{expiry}\n";
+                       logger "Versions:\t$metadata{versions}\n";
+                       logger "Guidance:\t$metadata{guidance}\n";
+                       logger "Categories:\t$metadata{categories}\n";
+                       logger "Description:\t$metadata{desc}\n";
+                       logger "Player:\t\t$metadata{player}\n";
+               }
+       }
+       logger "\n";
+
+       logger "INFO: ".($#checked + 1)." Matching Programmes\n";
+       return @checked;
+}
+
+
+
+# Display a line containing programme info (using long, terse, and type options)
+sub list_prog_entry {
+       my ( $pid, $prefix, $tree ) = ( @_ );
+       my $prog_type = '';
+       # Show the type field if >1 type has been specified
+       $prog_type = "$prog{$pid}{type}, " if keys %type > 1;
+       my $name;
+       # If tree view
+       if ( $opt{tree} ) {
+               $prefix = '  '.$prefix;
+               $name = '';
+       } else {
+               $name = "$prog{$pid}{name} - ";
+       }
+       # Remove some info depending on prog_type
+       my $optional;
+       $optional = ", '$prog{$pid}{channel}', $prog{$pid}{categories}, $prog{$pid}{versions}" if $prog{$pid}{type} eq 'tv';
+       $optional = ", '$prog{$pid}{channel}'" if $prog{$pid}{type} eq 'itv';
+       $optional = ", '$prog{$pid}{channel}', $prog{$pid}{categories}" if $prog{$pid}{type} eq 'radio';
+       $optional = ", '$prog{$pid}{available}', '$prog{$pid}{channel}', $prog{$pid}{categories}" if $prog{$pid}{type} eq 'podcast';
+       logger "\n${prog_type}$prog{$pid}{name}\n" if $opt{tree} && ! $tree;
+       # Display based on output options
+       if ( $opt{long} ) {
+               my @time = gmtime( time() - $prog{$pid}{timeadded} );
+               logger "${prefix}$prog{$pid}{index}:\t${prog_type}${name}$prog{$pid}{episode}${optional}, $time[7] days $time[2] hours ago - $prog{$pid}{desc}\n";
+       } elsif ( $opt{terse} ) {
+               logger "${prefix}$prog{$pid}{index}:\t${prog_type}${name}$prog{$pid}{episode}\n";
+       } else {
+               logger "${prefix}$prog{$pid}{index}:\t${prog_type}${name}$prog{$pid}{episode}${optional}\n";
+       }
+       return 0;
+}
+
+
+
+# Get matching programme index numbers using supplied regex
+sub get_regex_matches {
+       my $download_regex = shift;
+       my %download_hash;
+       my $channel_regex = $opt{channel} || '.*';
+       my $category_regex = $opt{category} || '.*';
+       my $versions_regex = $opt{versions} || '.*';
+       my $channel_exclude_regex = $opt{excludechannel} || '^ROGUE$';
+       my $category_exclude_regex = $opt{excludecategory} || '^ROGUE$';
+       my $since = $opt{since} || 99999;
+       my $now = time();
+               
+       for (keys %index_pid) {
+               my $pid = $index_pid{$_};
+
+               # Only include programmes matching channels and category regexes
+               if ( $prog{$pid}{channel} =~ /$channel_regex/i
+                 && $prog{$pid}{categories} =~ /$category_regex/i
+                 && $prog{$pid}{versions} =~ /$versions_regex/i
+                 && $prog{$pid}{channel} !~ /$channel_exclude_regex/i
+                 && $prog{$pid}{categories} !~ /$category_exclude_regex/i
+                 && $prog{$pid}{timeadded} >= $now - ($since * 3600)
+               ) {
+
+                       # Search prognames/pids while excluding channel_regex and category_regex
+                       $download_hash{$_} = 1 if (
+                               $prog{$pid}{name} =~ /$download_regex/i
+                               || ( $pid =~ /$download_regex/i && $download_regex =~ /b00/ )
+                               || ( $pid =~ /$download_regex/i && $download_regex =~ /b00/ )
+                       );
+                       # Also search long descriptions and episode data if -l is specified
+                       $download_hash{$_} = 1 if (
+                               $opt{long} 
+                               && 
+                               ( $prog{$pid}{desc} =~ /$download_regex/i
+                                 || $prog{$pid}{episode} =~ /$download_regex/i
+                               )
+                       );
+               }
+       }
+       return sort {$a <=> $b} keys %download_hash;
+}
+
+
+# get_links_atom (%channels)
+sub get_links_atom {
+       my $prog_type = shift;
+       my %channels = %{$_[0]};
+
+       my $xml;
+       my $feed_data;
+       my $res;
+       logger "INFO: Getting $prog_type Index Feeds\n";
+       # Setup User agent
+       my $ua = LWP::UserAgent->new;
+       $ua->timeout([$lwp_request_timeout]);
+       $ua->proxy( ['http'] => $proxy_url );
+       $ua->agent( $user_agent{desktop} );
+       $ua->conn_cache(LWP::ConnCache->new());
+
+       # Download index feed
+       # Sort feeds so that category based feeds are done last - this makes sure that the channels get defined correctly if there are dups
+       my @channel_list;
+       push @channel_list, grep !/categor/, keys %channels;
+       push @channel_list, grep  /categor/, keys %channels;
+       for ( @channel_list ) {
+
+               my $url = "${channel_feed_url}/$_/list/limit/400";
+               logger "DEBUG: Getting feed $url\n" if $opt{verbose};
+               $xml = request_url_retry($ua, $url, 3, '.', "WARNING: Failed to get programme index feed for $_ from iplayer site\n");
+               logger "INFO: Got ".(grep /<entry/, split /\n/, $xml)." programmes\n" if $opt{verbose};
+               decode_entities($xml);  
+               
+               # Feed as of August 2008
+               #        <entry>
+               #          <title type="text">Bargain Hunt: Series 18: Oswestry</title>
+               #          <id>tag:feeds.bbc.co.uk,2008:PIPS:b0088jgs</id>
+               #          <updated>2008-07-22T00:23:50Z</updated>
+               #          <content type="html">
+               #            &lt;p&gt;
+               #              &lt;a href=&quot;http://www.bbc.co.uk/iplayer/episode/b0088jgs?src=a_syn30&quot;&gt;
+               #                &lt;img src=&quot;http://www.bbc.co.uk/iplayer/images/episode/b0088jgs_150_84.jpg&quot; alt=&quot;Bargain Hunt: Series 18: Oswestry&quot; /&gt;
+               #              &lt;/a&gt;
+               #            &lt;/p&gt;
+               #            &lt;p&gt;
+               #              The teams are at an antiques fair in Oswestry showground. Hosted by Tim Wonnacott.
+               #            &lt;/p&gt;
+               #          </content>
+               #          <category term="Factual" />
+               #          <category term="TV" />
+               #          <link rel="via" href="http://www.bbc.co.uk/iplayer/episode/b0088jgs?src=a_syn30" type="text/html" title="Bargain Hunt: Series 18: Oswestry" />
+               #       </entry>
+               #
+
+               ### New Feed
+               #  <entry>
+               #    <title type="text">House of Lords: 02/07/2008</title>
+               #    <id>tag:bbc.co.uk,2008:PIPS:b00cd5p7</id>
+               #    <updated>2008-06-24T00:15:11Z</updated>
+               #    <content type="html">
+               #      <p>
+               #       <a href="http://www.bbc.co.uk/iplayer/episode/b00cd5p7?src=a_syn30">
+               #         <img src="http://www.bbc.co.uk/iplayer/images/episode/b00cd5p7_150_84.jpg" alt="House of Lords: 02/07/2008" />
+               #       </a>
+               #      </p>
+               #      <p>
+               #       House of Lords, including the third reading of the Health and Social Care Bill. 1 July.
+               #      </p>
+               #    </content>
+               #    <category term="Factual" scheme="urn:bbciplayer:category" />
+               #    <link rel="via" href="http://www.bbc.co.uk/iplayer/episode/b00cd5p7?src=a_syn30" type="application/atom+xml" title="House of Lords: 02/07/2008">
+               #    </link>
+               #  </entry>
+
+               # Parse XML
+
+               # get list of entries within <entry> </entry> tags
+               my @entries = split /<entry>/, $xml;
+               # Discard first element == header
+               shift @entries;
+
+               my ( $name, $episode, $desc, $pid, $available, $channel, $duration, $thumbnail, $prog_type, $versions );
+               foreach my $entry (@entries) {
+
+                       my $entry_flat = $entry;
+                       $entry_flat =~ s/\n/ /g;
+
+                       # <id>tag:bbc.co.uk,2008:PIPS:b008pj3w</id>
+                       $pid = $1 if $entry =~ m{<id>.*PIPS:(.+?)</id>};
+
+                       # parse name: episode, e.g. Take a Bow: Street Feet on the Farm
+                       $name = $1 if $entry =~ m{<title\s*.*?>\s*(.*?)\s*</title>};
+                       $episode = $name;
+                       $name =~ s/^(.*): .*$/$1/g;
+                       $episode =~ s/^.*: (.*)$/$1/g;
+
+                       # This is not the availability!
+                       # <updated>2008-06-22T05:01:49Z</updated>
+                       #$available = get_available_time_string( $1 ) if $entry =~ m{<updated>(\d{4}\-\d\d\-\d\dT\d\d:\d\d:\d\d.).*?</updated>};
+
+                       #<p>    House of Lords, including the third reading of the Health and Social Care Bill. 1 July.   </p>    </content>
+                       $desc = $1 if $entry =~ m{<p>\s*(.*?)\s*</p>\s*</content>};
+
+                       # Parse the categories into hash
+                       # <category term="Factual" />
+                       my @category;
+                       for my $line ( grep /<category/, (split /\n/, $entry) ) {
+                               push @category, $1 if $line =~ m{<category\s+term="(.+?)"};
+                       }
+
+                       # Extract channel and type
+                       ($prog_type, $channel) = (split /\|/, $channels{$_})[0,1];
+
+                       logger "DEBUG: '$pid, $name - $episode, $channel'\n" if $opt{debug};
+
+                       # Merge and Skip if this pid is a duplicate
+                       if ( defined $prog{$pid} ) {
+                               logger "WARNING: '$pid, $prog{$pid}{name} - $prog{$pid}{episode}, $prog{$pid}{channel}' already exists (this channel = $channel)\n" if $opt{verbose};
+                               # Since we use the 'Signed' channel to get sign zone data, merge the categories from this entry to the existing entry
+                               if ( $prog{$pid}{categories} ne join(',', @category) ) {
+                                       my %cats;
+                                       $cats{$_} = 1 for ( split /,/, $prog{$pid}{categories} );
+                                       $cats{$_} = 1 for ( @category );
+                                       logger "INFO: Merged categories for $pid from $prog{$pid}{categories} to ".join(',', sort keys %cats)."\n" if $opt{verbose};
+                                       $prog{$pid}{categories} = join(',', sort keys %cats);
+                               }
+                               # If this is a dupicate pid and the channel is now Signed then both versions are available
+                               $prog{$pid}{versions} = 'default,signed' if $channel eq 'Signed';
+                               next;
+                       }
+
+                       # Check for signed-only version from Channel
+                       if ($channel eq 'Signed') {
+                               $versions = 'signed';
+                       # Else if not channel 'Signed' then this must also have both versions available
+                       } elsif ( grep /Sign Zone/, @category ) {
+                               $versions = 'default,signed';
+                       } else {
+                               $versions = 'default';
+                       }
+                       
+                       # build data structure
+                       $prog{$pid} = {
+                               'name'          => $name,
+                               'versions'      => $versions,
+                               'episode'       => $episode,
+                               'desc'          => $desc,
+                               'available'     => 'Unknown',
+                               'duration'      => 'Unknown',
+                               'thumbnail'     => "${thumbnail_prefix}/${pid}_150_84.jpg",
+                               'channel'       => $channel,
+                               'categories'    => join(',', @category),
+                               'type'          => $prog_type,
+                       };
+               }
+       }
+       logger "\n";
+       return 0;
+}
+
+
+
+# Populates the index field of the prog hash as well as creating the %index_pid hash
+# Should be run after getting any link lists
+sub sort_indexes {
+
+       # Add index field based on alphabetical sorting by prog name
+       my %index;
+       $index{tv} = 1;
+       
+       # Start index counter at 10001 for radio progs
+       $index{radio} = 10001;
+
+       # Start index counter at 20001 for podcast progs
+       $index{podcast} = 20001;
+
+       # Start index counter at 100001 for itv progs
+       $index{itv} = 100001;
+
+       my @prog_pid;
+
+       # Create unique array of '<progname|pid>'
+       push @prog_pid, "$prog{$_}{name}|$_" for (keys %prog);
+
+       # Sort by progname and index 
+       for (sort @prog_pid) {
+               # Extract pid
+               my $pid = (split /\|/)[1];
+               my $prog_type = $prog{$pid}{type};
+               $index_pid{ $index{$prog_type} } = $pid;
+               $prog{$pid}{index} = $index{$prog_type};
+               $index{$prog_type}++;
+       }
+       return 0;
+}
+
+
+
+# Uses: $podcast_index_feed_url
+# get_podcast_links ()
+sub get_podcast_links {
+
+       my $xml;
+       my $res;
+       logger "INFO: Getting podcast Index Feeds\n";
+       # Setup User agent
+       my $ua = LWP::UserAgent->new;
+       $ua->timeout([$lwp_request_timeout]);
+       $ua->proxy( ['http'] => $proxy_url );
+       $ua->agent( $user_agent{get_iplayer} );
+       $ua->conn_cache(LWP::ConnCache->new());
+       
+       # Method
+       # $podcast_index_feed_url (gets list of rss feeds for each podcast prog) =>
+       # http://downloads.bbc.co.uk/podcasts/$channel/$name/rss.xml =>
+       
+       # Download index feed
+       my $xmlindex = request_url_retry($ua, $podcast_index_feed_url, 3, '.', "WARNING: Failed to get prodcast index from site\n");
+       $xmlindex =~ s/\n/ /g;
+
+       # Every RSS feed has an extry like below (all in a text block - not formatted like below)
+       #  <program xmlns="" language="en-gb" typicalDuration="P30M" active="true" public="true" bbcFlavour="Programme Highlights" region="all" wwpid="0">
+       #    <title>Best of Chris Moyles</title>
+       #    <shortTitle>moyles</shortTitle>
+       #    <description>Weekly highlights from the award-winning Chris Moyles breakfast show, as broadcast by Chris and team every morning from 6.30am to 10am.</description>
+       #    <network id="radio1" name="BBC Radio 1" />
+       #    <image use="itunes" url="http://www.bbc.co.uk/radio/podcasts/moyles/assets/_300x300.jpg" />
+       #    <link target="homepage" url="http://www.bbc.co.uk/radio1/chrismoyles/" />
+       #    <link target="feed" url="http://downloads.bbc.co.uk/podcasts/radio1/moyles/rss.xml" />
+       #    <link target="currentItem" url="http://downloads.bbc.co.uk/podcasts/radio1/moyles/moyles_20080926-0630a.mp3">
+       #      <title>Moyles: Guestfest. 26 Sep 08</title>
+       #      <description>Rihanna, Ross Kemp, Jack Osbourne, John
+       #      Barrowman, Cheggars, the legend that is Roy Walker and more,
+       #      all join the team in a celeb laden bundle of mirth and
+       #      merriment. It&#226;&#8364;&#8482;s all the best bits of the
+       #      week from The Chris Moyles Show on BBC Radio 1.</description>
+       #      <publishDate>2008-09-26T06:30:00+01:00</publishDate>
+       #    </link>
+       #    <bbcGenre id="entertainment" name="Entertainment" />
+       #    <systemRef systemId="podcast" key="42" />
+       #    <systemRef systemId="pid.brand" key="b006wkqb" />
+       #    <feed mimeType="audio/mpeg" content="audio" audioCodec="mp3" audioProfile="cbr" />
+       #  </program>
+       for ( split /<program/, $xmlindex ) {
+               # Extract channel name, rss feed data
+               my ($channel, $url);
+
+               # <network id="radio1" name="BBC Radio 1" />
+               $channel = $1 if m{<network\s+id=".*?"\s+name="(.*?)"\s*\/>};
+
+               # <link target="feed" url="http://downloads.bbc.co.uk/podcasts/radio1/moyles/rss.xml" />
+               $url = $1 if m{<link\s+target="feed"\s+url="(.*?)"\s*\/>};
+
+               # Skip if there is no feed data for channel
+               next if ! ($channel || $url);
+
+               my ( $name, $episode, $desc, $pid, $available, $duration, $thumbnail );
+
+               # Get RSS feeds for each podcast programme
+               logger "DEBUG: Getting podcast feed $url\n" if $opt{verbose};
+               $xml = request_url_retry($ua, $url, 3, '.', "WARNING: Failed to get podcast feed for $channel / $_ from iplayer site\n") if $opt{verbose};
+               $xml = request_url_retry($ua, $url, 3, '.', '') if ! $opt{verbose};
+               # skip if no data
+               next if ! $xml;
+
+               logger "INFO: Got ".(grep /<media:content/, split /<item>/, $xml)." programmes\n" if $opt{verbose};
+               decode_entities($xml);
+       
+               # First entry is channel data
+               # <?xml version="1.0" encoding="utf-8"?>
+               #<rss xmlns:media="http://search.yahoo.com/mrss/"
+               #xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd"
+               #version="2.0">
+               #  <channel>
+               #    <title>Stuart Maconie's Freak Zone</title>
+               #    <link>http://www.bbc.co.uk/6music/shows/freakzone/</link>
+               #    <description>Weekly highlights from Stuart Maconie's
+               #    ...podcast is only available in the UK.</description>
+               #    <itunes:summary>Weekly highlights from Stuart Maconie's
+               #    ...podcast is only available in the UK.</itunes:summary>
+               #    <itunes:author>BBC 6 Music</itunes:author>
+               #    <itunes:owner>
+               #      <itunes:name>BBC</itunes:name>
+               #      <itunes:email>podcast.support@bbc.co.uk</itunes:email>
+               #    </itunes:owner>
+               #    <language>en</language>
+               #    <ttl>720</ttl>
+               #    <image>
+               #      <url>
+               #      http://www.bbc.co.uk/radio/podcasts/freakzone/assets/_300x300.jpg</url>
+               #      <title>Stuart Maconie's Freak Zone</title>
+               #      <link>http://www.bbc.co.uk/6music/shows/freakzone/</link>
+               #    </image>
+               #    <itunes:image href="http://www.bbc.co.uk/radio/podcasts/freakzone/assets/_300x300.jpg" />
+               #    <copyright>(C) BBC 2008</copyright>
+               #    <pubDate>Sun, 06 Jul 2008 20:00:05 +0100</pubDate>
+               #    <itunes:category text="Music" />
+               #    <itunes:keywords>Stewart Maconie, Macconie, freekzone,
+               #    freakzone, macoonie</itunes:keywords>
+               #    <media:keywords>Stewart Maconie, Macconie, freekzone,
+               #    freakzone, macoonie</media:keywords>
+               #   <itunes:explicit>no</itunes:explicit>
+               #    <media:rating scheme="urn:simple">nonadult</media:rating>
+
+               # Parse XML
+
+               # get list of entries within <entry> </entry> tags
+               my @entries = split /<item>/, $xml;
+               # first element == <channel> header
+               my $header = shift @entries;
+
+               # Get podcast name
+               $name = $1 if $header =~ m{<title>\s*(.+?)\s*</title>};
+       
+               # Parse the categories into hash
+               # <itunes:category text="Music" />
+               my @category;
+               for my $line ( grep /<itunes:category/, (split /\n/, $header) ) {
+                       push @category, $1 if $line =~ m{<itunes:category\s+text="\s*(.+?)\s*"};
+               }
+       
+               # Get thumbnail from header
+               # <itunes:image href="http://www.bbc.co.uk/radio/podcasts/freakzone/assets/_300x300.jpg" />
+               $thumbnail = $1 if $header =~ m{<itunes:image href="\s*(.+?)\s*"};
+
+               # Followed by items:
+               #    <item>
+               #      <title>FreakZone: C'est Stuart avec le Professeur Spear et le
+               #      pop francais?</title>
+               #      <description>Stuart and Justin discuss the sub-genre of
+               #      French 'cold wave' in this week's module.</description>
+               #      <itunes:subtitle>Stuart and Justin discuss the sub-genre of
+               #      French 'cold wave' in this week's
+               #      module....</itunes:subtitle>
+               #      <itunes:summary>Stuart and Justin discuss the sub-genre of
+               #      French 'cold wave' in this week's module.</itunes:summary>
+               #      <pubDate>Sun, 06 Jul 2008 20:00:00 +0100</pubDate>
+               #      <itunes:duration>14:23</itunes:duration>
+               #      <enclosure url="http://downloads.bbc.co.uk/podcasts/6music/freakzone/freakzone_20080706-2000.mp3"
+               #      length="13891916" type="audio/mpeg" />
+               #      <guid isPermaLink="false">
+               #      http://downloads.bbc.co.uk/podcasts/6music/freakzone/freakzone_20080706-2000.mp3</guid>
+               #      <link>
+               #      http://downloads.bbc.co.uk/podcasts/6music/freakzone/freakzone_20080706-2000.mp3</link>
+               #      <media:content url="http://downloads.bbc.co.uk/podcasts/6music/freakzone/freakzone_20080706-2000.mp3"
+               #      fileSize="13891916" type="audio/mpeg" medium="audio"
+               #      expression="full" duration="863" />
+               #      <itunes:author>BBC 6 Music</itunes:author>
+               #    </item>
+       
+               foreach my $entry (@entries) {
+
+                       my $entry_flat = $entry;
+                       $entry_flat =~ s/\n/ /g;
+       
+                       # Use the link as a guid
+                       # <link>   http://downloads.bbc.co.uk/podcasts/6music/freakzone/freakzone_20080706-2000.mp3</link>
+                       $pid = $1 if $entry =~ m{<link>\s*(.+?)</link>};
+       
+                       # Skip if this pid is a duplicate
+                       if ( defined $prog{$pid} ) {
+                               logger "WARNING: '$pid, $prog{$pid}{name} - $prog{$pid}{episode}, $prog{$pid}{channel}' already exists (this channel = $_)\n" if $opt{verbose};
+                               next;
+                       }
+       
+                       # parse episode
+                       # <title>FreakZone: C'est Stuart avec le Professeur Spear et le pop francais?</title>
+                       $episode = $1 if $entry =~ m{<title>\s*(.*?)\s*</title>};
+       
+                       # <pubDate>Sun, 06 Jul 2008 20:00:00 +0100</pubDate>
+                       $available = $1 if $entry =~ m{<pubDate>\s*(.*?)\s*</pubDate>};
+       
+                       # <description>Stuart and Justin discuss the sub-genre of French 'cold wave' in this week's module.</description>
+                       $desc = $1 if $entry =~ m{<description>\s*(.*?)\s*</description>};
+       
+                       # Duration
+                       $duration = $1 if $entry =~ m{<itunes:duration>\s*(.*?)\s*</itunes:duration>};
+       
+                       # build data structure
+                       $prog{$pid} = {
+                               'name'          => $name,
+                               'versions'      => 'default',
+                               'episode'       => $episode,
+                               'desc'          => $desc,
+                               'available'     => $available,
+                               'duration'      => $duration,
+                               'thumbnail'     => $thumbnail,
+                               'channel'       => $channel,
+                               'categories'    => join(',', @category),
+                               'type'          => 'podcast',
+                       };
+               }
+       }
+       logger "\n";
+       return 0;
+}
+
+
+
+# Uses:
+# get_itv_links ()
+sub get_itv_links {
+
+       my $xml;
+       my $res;
+       my %series_pid;
+       my %episode_pid;
+       logger "INFO: Getting itv Index Feeds\n";
+       # Setup User agent
+       my $ua = LWP::UserAgent->new;
+       $ua->timeout([$lwp_request_timeout]);
+       $ua->proxy( ['http'] => $proxy_url );
+       $ua->agent( $user_agent{get_iplayer} );
+       $ua->conn_cache(LWP::ConnCache->new());
+       
+       # Method
+       # http://www.itv.com/_data/xml/CatchUpData/CatchUp360/CatchUpMenu.xml (gets list of urls for each prog series) =>
+       #  =>
+       
+       # Download index feed
+       my $itv_index_feed_url = 'http://www.itv.com/_data/xml/CatchUpData/CatchUp360/CatchUpMenu.xml';
+       my $xmlindex = request_url_retry($ua, $itv_index_feed_url, 3, '.', "WARNING: Failed to get itv index from site\n");
+       $xmlindex =~ s/[\n\r]//g;
+
+       # This gives a list of programme series (sometimes episodes)
+       #    <ITVCatchUpProgramme>
+       #      <ProgrammeId>50</ProgrammeId>
+       #      <ProgrammeTitle>A CHRISTMAS CAROL</ProgrammeTitle>
+       #      <ProgrammeMediaId>615915</ProgrammeMediaId>
+       #      <ProgrammeMediaUrl>
+       #      http://www.itv.com//img/150x113/A-Christmas-Carol-2f16d25a-de1d-4a3a-90cb-d47489eee98e.jpg</ProgrammeMediaUrl>
+       #      <LastUpdated>2009-01-06T12:24:22.7419643+00:00</LastUpdated>
+       #      <Url>
+       #      http://www.itv.com/CatchUp/Video/default.html?ViewType=5&amp;Filter=32910</Url>
+       #      <EpisodeCount>1</EpisodeCount>
+       #      <VideoID>32910</VideoID>
+       #      <DentonID>-1</DentonID>
+       #      <DentonRating></DentonRating>
+       #      <AdditionalContentUrl />
+       #      <AdditionalContentUrlText />
+       #    </ITVCatchUpProgramme>
+
+       for my $feedxml ( split /<ITVCatchUpProgramme>/, $xmlindex ) {
+               # Extract feed data
+               my ($episodecount, $viewtype, $videoid, $url);
+               my @entries;
+
+               logger "\n\nDEBUG: XML: $feedxml\n"  if $opt{debug}; 
+
+               # <EpisodeCount>1</EpisodeCount>
+               $episodecount = $1 if $feedxml =~ m{<EpisodeCount>\s*(\d+)\s*<\/EpisodeCount>};
+
+               # <Url>http://www.itv.com/CatchUp/Video/default.html?ViewType=5&amp;Filter=32910</Url>
+               ($viewtype, $videoid) = ($1, $2) if $feedxml =~ m{<Url>\s*.+?ViewType=(\d+).+?Filter=(\d+)\s*<\/Url>}i;
+
+               ## <VideoID>32910</VideoID>
+               #$videoid = $1 if $feedxml =~ m{<VideoID>\s*(\d+)\s*<\/VideoID>};
+
+               # Skip if there is no feed data for channel
+               next if ($viewtype =~ /^0*$/ || $videoid =~ /^0*$/ );
+
+               logger "DEBUG: Got ViewType=$viewtype VideoId=$videoid EpisodeCount=$episodecount\n" if $opt{debug};
+
+               my $url = "http://www.itv.com/_app/Dynamic/CatchUpData.ashx?ViewType=${viewtype}&Filter=${videoid}";
+
+               # Add response from episode metadata url to list to be parsed if this is an episode link
+               if ( $viewtype == 5 ) {
+                       next if $episode_pid{$videoid};
+                       $episode_pid{$videoid} = 1;
+                       # Get metadata pages for episode
+#                      logger "DEBUG: Getting episode metadata $url\n" if $opt{debug};
+#                      $xml = request_url_retry($ua, $url, 2, '.', "WARNING: Failed to get itv episode data for ${videoid} from itv site\n") if $opt{verbose};
+#                      $xml = request_url_retry($ua, $url, 2, '.', '') if ! $opt{verbose};
+#                      next if ! $xml;
+
+                       my ( $name, $guidance, $channel, $episode, $desc, $pid, $available, $duration, $thumbnail );
+#                      decode_entities($xml);
+#                      # Flatten
+#                      $xml =~ s|[\n\r]||g;
+
+                       $pid = $videoid;
+                       $channel = 'ITV Catch-up';
+
+                       # Skip if this pid is a duplicate
+                       if ( defined $prog{$pid} ) {
+                               logger "WARNING: '$pid, $prog{$pid}{name} - $prog{$pid}{episode}, $prog{$pid}{channel}' already exists (this channel = $channel)\n" if $opt{verbose};
+                               next;
+                       }
+
+                       $name = $1 if $feedxml =~ m{<ProgrammeTitle>\s*(.+?)\s*<\/ProgrammeTitle>};
+                       $guidance = $1 if $feedxml =~ m{<DentonRating>\s*(.+?)\s*<\/DentonRating>};
+                       $thumbnail = $1 if $feedxml =~ m{<ProgrammeMediaUrl>\s*(.+?)\s*<\/ProgrammeMediaUrl>};
+                       $episode = $pid;
+#                      $available = $1 if $xml =~ m{<p\s+class="timings">\s*<span\s+class="date">(.+?)<\/span>};
+#                      $episode = $available;
+#                      $duration = $1 if $xml =~ m{Duration:\s*(.+?)\s+\|};
+#                      $desc = $1 if $xml =~ m{<p>(.+?)<\/p>};
+
+                       # build data structure
+                       $prog{$pid} = {
+                               'name'          => $name,
+                               'versions'      => 'default',
+                               'episode'       => $episode,
+                               'desc'          => $desc,
+                               'available'     => $available,
+                               'duration'      => $duration,
+                               'thumbnail'     => $thumbnail,
+                               'channel'       => $channel,
+                               'categories'    => 'TV',
+                               'type'          => 'itv',
+                       };
+
+
+
+
+
+               # Get next episode list and parse
+               #     <div class="listItem highlight contain">
+               #      <div class="floatLeft"><a href="http://www.itv.com/CatchUp/Video/default.html?ViewType=5&amp;Filter=33383"><img src="http://www.itv.com//img/157x88/P7-67e0b86f-b335-4f6b-8db
+               #      <div class="content">
+               #        <h3><a href="http://www.itv.com/CatchUp/Video/default.html?ViewType=5&amp;Filter=33383">Emmerdale</a></h3>
+               #        <p class="date">Mon 05 Jan 2009</p>
+               #        <p class="progDesc">Donna is stunned to learn Marlon has pointed the finger at Ross. Aaron defaces Tom King's grave.</p>
+               #        <ul class="progDetails">
+               #          <li>
+               #                          Duration: 30 min
+               #          </li>
+               #          <li class="days">
+               #            Expires in
+               #                        <strong>29</strong>
+               #                                        days
+               #                                </li>
+               #        </ul>
+               #      </div>
+               #    </div>
+               #    <div class="listItem contain">
+               #      <div class="floatLeft"><a href="http://www.itv.com/CatchUp/Video/default.html?ViewType=5&amp;Filter=33245"><img src="http://www.itv.com//img/157x88/Marlon-Dingle-742c50b3-3b
+               #      <div class="content">
+               #        <h3><a href="http://www.itv.com/CatchUp/Video/default.html?ViewType=5&amp;Filter=33245">Emmerdale</a></h3>
+               #        <p class="date">Fri 02 Jan 2009</p>
+               #        <p class="progDesc">Marlon gets his revenge on Ross. The King brothers struggle to restart their business without Matthew. Scarlett is fed up with Victoria getting all Daz
+               #        <ul class="progDetails">
+               #          <li>
+               #                          Duration: 30 min
+               #          </li>
+               #          <li class="days">
+               #            Expires in
+               #                        <strong>26</strong>
+               #                                        days
+               #                                </li>
+               #        </ul>
+               #      </div>
+               #    </div>
+               # 
+               } elsif ( $viewtype == 1 ) {
+                       # Make sure we don't duplicate parsing a series
+                       next if $series_pid{$videoid};
+                       $series_pid{$videoid} = 1;
+
+                       # Get metadata pages for each series
+                       logger "DEBUG: Getting series metadata $url\n" if $opt{debug};
+                       $xml = request_url_retry($ua, $url, 2, '.', "WARNING: Failed to get itv series data for ${videoid} from itv site\n") if $opt{verbose};
+                       $xml = request_url_retry($ua, $url, 2, '.', '') if ! $opt{verbose};
+
+                       # skip if no data
+                       next if ! $xml;
+
+                       decode_entities($xml);
+                       # Flatten entry
+                       $xml =~ s/[\n\r]//g;
+
+                       # Extract Filter (pids) from this list
+                       # e.g. <h3><a href="http://www.itv.com/CatchUp/Video/default.html?ViewType=5&amp;Filter=32042">Emmerdale</a></h3>
+                       my @videoids = (split /<h3><a href=.+?Filter=/, $xml);
+
+                       # Get episode data for each videoid
+                       $viewtype = 5;
+
+                       my @episode_data = split/<h3><a href=.+?Filter=/, $xml;
+                       # Ignore first entry
+                       shift @episode_data;
+                       logger "INFO: Got ".($#episode_data+1)." programmes\n" if $opt{verbose};
+
+                       for my $xml (@episode_data) {
+                               $videoid = $1 if $xml =~ m{^(\d+?)".+$}i;
+
+                               # Make sure we don't duplicate parsing an episode
+                               next if $episode_pid{$videoid};
+                               $episode_pid{$videoid} = 1;
+
+                               my ( $name, $guidance, $channel, $episode, $desc, $pid, $available, $duration, $thumbnail );
+       
+                               $pid = $videoid;
+                               $channel = 'ITV Catch-up';
+       
+                               # Skip if this pid is a duplicate
+                               if ( defined $prog{$pid} ) {
+                                       logger "WARNING: '$pid, $prog{$pid}{name} - $prog{$pid}{episode}, $prog{$pid}{channel}' already exists (this channel = $channel)\n" if $opt{verbose};
+                                       next;
+                               }
+                               $name = $1 if $feedxml =~ m{<ProgrammeTitle>\s*(.+?)\s*<\/ProgrammeTitle>};
+                               $available = $1 if $xml =~ m{<p\s+class="date">(.+?)<\/p>}i;
+                               $episode = $available;
+                               $duration = $1 if $xml =~ m{<li>Duration:\s*(.+?)\s*<\/li>}i;
+                               $desc = $1 if $xml =~ m{<p\s+class="progDesc">(.+?)\s*<\/p>};
+                               $guidance = $1 if $feedxml =~ m{<DentonRating>\s*(.+?)\s*<\/DentonRating>};
+                               $thumbnail = $1 if $feedxml =~ m{<ProgrammeMediaUrl>\s*(.+?)\s*<\/ProgrammeMediaUrl>};
+
+                               logger "DEBUG: name='$name' episode='$episode' pid=$pid available='$available' \n" if $opt{debug};      
+       
+                               # build data structure
+                               $prog{$pid} = {
+                                       'name'          => $name,
+                                       'versions'      => 'default',
+                                       'episode'       => $episode,
+                                       'desc'          => $desc,
+                                       'available'     => $available,
+                                       'duration'      => $duration,
+                                       'thumbnail'     => $thumbnail,
+                                       'channel'       => $channel,
+                                       'categories'    => 'TV',
+                                       'type'          => 'itv',
+                               };
+
+
+
+
+                               # Dont get all metadata - use info in series page
+                               #$url = "http://www.itv.com/_app/Dynamic/CatchUpData.ashx?ViewType=${viewtype}&Filter=${videoid}";
+                               #logger "DEBUG: Getting episode metadata $url\n" if $opt{debug};
+                               #$xml = request_url_retry($ua, $url, 2, '.', "WARNING: Failed to get itv episode data for ${videoid} from itv site\n") if $opt{verbose};
+                               #$xml = request_url_retry($ua, $url, 2, '.', '') if ! $opt{verbose};
+                               #push @entries, $xml if $xml;
+                       }
+               }       
+
+       }
+       logger "\n";
+       return 0;
+}
+
+
+
+# Feed info:
+#      # Also see http://derivadow.com/2008/07/18/interesting-bbc-data-to-hack-with/
+#      # All podcasts menu (iphone)
+#      http://www.bbc.co.uk/radio/podcasts/ip/
+#      # All radio1 podcasts
+#      http://www.bbc.co.uk/radio/podcasts/ip/lists/radio1.sssi
+#      # All radio1 -> moyles podcasts
+#      http://www.bbc.co.uk/radio/podcasts/moyles/assets/iphone_keepnet.sssi
+#      # RSS Feed (indexed from?)
+#      http://downloads.bbc.co.uk/podcasts/radio1/moyles/rss.xml
+#      # aod by channel see http://docs.google.com/View?docid=d9sxx7p_38cfsmxfcq
+#      # http://www.bbc.co.uk/radio/aod/availability/<channel>.xml
+#      # aod index
+#      http://www.bbc.co.uk/radio/aod/index_noframes.shtml
+#      # schedule feeds
+#      http://www.bbc.co.uk/bbcthree/programmes/schedules.xml
+#      # These need drill-down to get episodes:
+#      # TV schedules by date
+#      http://www.bbc.co.uk/iplayer/widget/schedule/service/cbeebies/date/20080704
+#      # TV schedules in JSON, Yaml or XML
+#      http://www.bbc.co.uk/cbbc/programmes/schedules.(json|yaml|xml)
+#      # TV index on programmes tv
+#      http://www.bbc.co.uk/tv/programmes/a-z/by/*/player
+#      # TV + Radio
+#      http://www.bbc.co.uk/programmes/a-z/by/*/player
+#      # All TV (limit has effect of limiting to 2.? times number entries kB??)
+#      # seems that only around 50% of progs are available here compared to programmes site:
+#      http://feeds.bbc.co.uk/iplayer/categories/tv/list/limit/200
+#      # All Radio
+#      http://feeds.bbc.co.uk/iplayer/categories/radio/list/limit/999
+#      # New:
+#      # iCal feeds see: http://www.bbc.co.uk/blogs/radiolabs/2008/07/some_ical_views_onto_programme.shtml
+#      http://bbc.co.uk/programmes/b0079cmw/episodes/player.ics
+#      # Other data
+#      http://www.bbc.co.uk/cbbc/programmes/genres/childrens/player
+#      http://www.bbc.co.uk/programmes/genres/childrens/schedules/upcoming.ics
+#
+# get_links( <prog_type> )
+sub get_links {
+       my @cache;
+       my $now = time();
+       my $prog_type = shift;
+
+       # Open cache file (need to verify we can even read this)
+       if ( open(CACHE, "< $cachefile{$prog_type}") ) {
+               # Get file contents less any comments
+               @cache = grep !/^[\#\s]/, <CACHE>;
+               close (CACHE);
+       }
+
+       # Read cache into %pid_old and %index_pid_old if cache exists
+       my %prog_old;
+       my %index_pid_old;
+       if (@cache) {
+               for (@cache) {
+                       # Populate %prog from cache
+                       chomp();
+                       my ($index, $prog_type, $name, $pid, $available, $episode, $versions, $duration, $desc, $channel, $categories, $thumbnail, $timeadded) = split /\|/;
+                       # Create data structure with prog data
+                       $prog_old{$pid} = {
+                               'index'         => $index,
+                               'name'          => $name,
+                               'episode'       => $episode,
+                               'desc'          => $desc,
+                               'available'     => $available,
+                               'duration'      => $duration,
+                               'versions'      => $versions,
+                               'channel'       => $channel,
+                               'categories'    => $categories,
+                               'thumbnail'     => $thumbnail,
+                               'type'          => $prog_type,
+                               'timeadded'     => $timeadded,
+                       };
+                       $index_pid_old{$index}  = $pid;
+               }
+       }
+
+       # if a cache file doesn't exist/corrupted, flush option is specified or original file is older than $cache_sec then download new data
+       if ( (! @cache) || (! -f $cachefile{$prog_type}) || $opt{flush} || ($now >= ( stat($cachefile{$prog_type})->mtime + $cache_secs )) ) {
+
+               # Podcast only
+               get_podcast_links() if $prog_type eq 'podcast';
+
+               # ITV only
+               get_itv_links() if $prog_type eq 'itv';
+
+               # Radio and TV
+               get_links_atom( $prog_type, \%{$channels{$prog_type}} ) if $prog_type =~ /^(tv|radio)$/;
+
+               # Sort indexes
+               sort_indexes();
+               
+               # Open cache file for writing
+               unlink $cachefile{$prog_type};
+               my $now = time();
+               if ( open(CACHE, "> $cachefile{$prog_type}") ) {
+                       print CACHE "#Index|Type|Name|Pid|Available|Episode|Versions|Duration|Desc|Channel|Categories|Thumbnail|TimeAdded\n";
+                       for (sort {$a <=> $b} keys %index_pid) {
+                               my $pid = $index_pid{$_};
+                               # Only write entries for correct prog type
+                               if ($prog{$pid}{type} eq $prog_type) {
+                                       # Merge old and new data to retain timestamps
+                                       # if the entry was in old cache then retain timestamp from old entry
+                                       if ( $prog_old{$pid}{timeadded} ) {
+                                               $prog{$pid}{timeadded} = $prog_old{$pid}{timeadded};
+                                       # Else this is a new entry
+                                       } else {
+                                               $prog{$pid}{timeadded} = $now;
+                                               list_prog_entry( $pid, 'Added: ' );
+                                       }
+                                       # write to cache file
+                                       print CACHE "$_|$prog{$pid}{type}|$prog{$pid}{name}|$pid|$prog{$pid}{available}|$prog{$pid}{episode}|$prog{$pid}{versions}|$prog{$pid}{duration}|$prog{$pid}{desc}|$prog{$pid}{channel}|$prog{$pid}{categories}|$prog{$pid}{thumbnail}|$prog{$pid}{timeadded}\n";
+                               }
+                       }
+                       close (CACHE);
+               } else {
+                       logger "WARNING: Couldn't open cache file '$cachefile{$prog_type}' for writing\n";
+               }
+
+
+       # Else copy data from existing cache file into existing %prog hash
+       } else {
+               $prog{$_} = $prog_old{$_} for keys %prog_old;
+               $index_pid{$_} = $index_pid_old{$_} for keys %index_pid_old;
+       }
+       return 0;
+}
+
+
+
+# Usage: download_programme (<pid>)
+sub download_programme {
+       my $pid = shift;
+       my %streamdata;
+
+       # Setup user-agent
+       # Switch off automatic redirects
+       my $ua = LWP::UserAgent->new( requests_redirectable => [] );
+       # Setup user agent
+       $ua->timeout([$lwp_request_timeout]);
+       $ua->proxy( ['http'] => $proxy_url );
+       $ua->cookie_jar( HTTP::Cookies->new( file => $cookiejar, autosave => 1, ignore_discard => 1 ) );
+
+       my $dir = $download_dir{ $prog{$pid}{type} };
+       $prog{$pid}{ext} = 'mov';
+
+       # If were an ITV prog...
+       if ( $prog{$pid}{type} eq 'itv' ) {
+               if (! exists_in_path($mplayer)) {
+                       logger "\nERROR: Required $mplayer does not exist, skipping\n";
+                       return 21;
+               }
+               # stream data no available for itv
+               return 1 if $opt{streaminfo};
+               my ( $response, $url_1, $url_2, $url_3, $url_4 );
+               my $part;
+               my $duration;
+               my $filename;
+               my @url_list;
+               # Setup User agent (redirectable)
+               my $ua = LWP::UserAgent->new;
+               $ua->timeout([$lwp_request_timeout]);
+               $ua->proxy( ['http'] => $proxy_url );
+               $ua->agent( $user_agent{desktop} );
+               $ua->conn_cache(LWP::ConnCache->new());
+
+               # construct stage 1 request url
+               $url_1 = 'http://www.itv.com/_app/video/GetMediaItem.ashx?vodcrid=crid://itv.com/'.$pid.'&bitrate=384&adparams=SITE=ITV/AREA=CATCHUP.VIDEO/SEG=CATCHUP.VIDEO%20HTTP/1.1';
+
+               # Extract '<LicencePlaylist>(.+?) HTTP/1.1</LicencePlaylist>'
+               logger "INFO: ITV Video Stage 1 URL: $url_1\n" if $opt{verbose};
+               $response = request_url_retry($ua, $url_1, 2, '', '');
+               logger "DEBUG: Response data: $response\n" if $opt{debug};
+               $url_2 = $1 if $response =~ m{<LicencePlaylist>(.+?) HTTP/1.1</LicencePlaylist>};
+               # replace '&amp;' with '&' and append '%20HTTP/1.1'
+               $url_2 =~ s/&amp;/&/g;
+               $url_2 .= '%20HTTP/1.1';
+               logger "INFO: ITV Video Stage 2 URL: $url_2\n" if $opt{verbose};
+               $response = request_url_retry($ua, $url_2, 2, '', '');
+               logger "DEBUG: Response data: $response\n" if $opt{debug};
+
+               # Extract hrefs and names. There are multiple entries for parts of prog (due to ads):
+               # e.g. <asx><Title>Doctor Zhivago</Title><EntryRef href="HTTP://SAM.ITV.COM/XTSERVER/ACC_RANDOM=1231194223/SITE=ITV/AREA=CATCHUP.VIDEO/SEG=CATCHUP.VIDEO HTTP/1.1/SOURCE=CATCH.UP/GENRE=DRAMA/PROGNAME=DOCTOR.ZHIVAGO/PROGID=33105/SERIES=DOCTOR.ZHIVAGO/EPNUM=/EPTITLE=/BREAKNUM=0/ADPOS=1/PAGEID=01231194223/DENTON=0/CUSTOMRATING=/TOTDUR=90/PREDUR=0/POSDUR=905/GENERIC=6e0536bf-7883-4aaa-9230-94ecc4aea403/AAMSZ=VIDEO" /><EntryRef href="HTTP://SAM.ITV.COM/XTSERVER/ACC_RANDOM=1231194223/SITE=ITV/AREA=CATCHUP.VIDEO/SEG=CATCHUP.VIDEOHTTP/1.1/SOURCE=CATCH.UP/GENRE=DRAMA/PROGNAME=DOCTOR.ZHIVAGO/PROGID=33105/SERIES=DOCTOR.ZHIVAGO/EPNUM=/EPTITLE=/BREAKNUM=0/ADPOS=LAST/PAGEID=01231194223/DENTON=0/CUSTOMRATING=/TOTDUR=90/PREDUR=0/POSDUR=905/GENERIC=6e0536bf-7883-4aaa-9230-94ecc4aea403/AAMSZ=VIDEO" />
+               $prog{$pid}{name} = $1 if $response =~ m{<Title>(.+?)<\/Title>};
+               for my $entry (split /<Entry><ref\s+href=/, $response) {
+                       logger "DEBUG: Entry data: $entry\n" if $opt{debug};
+                       $entry .= '<Entry><ref href='.$entry;
+
+                       ( $url_3, $part, $filename, $duration ) = ( $1, $2, $3, $4 ) if $entry =~ m{<Entry><ref\s+href="(.+?)"\s+\/><param\s+value="true"\s+name="Prebuffer"\s+\/>\s*<PARAM\s+NAME="PrgPartNumber"\s+VALUE="(.+?)"\s*\/><PARAM\s+NAME="FileName"\s+VALUE="(.+?)"\s*\/><PARAM\s+NAME="PrgLength"\s+VALUE="(.+?)"\s*\/>};
+                       next if not $url_3;
+                       # Replace '&amp;' with '&' in url
+                       $url_3 =~ s/&amp;/&/g;
+                       # Accumulate duration
+                       #my @time = split /:/, $duration;
+                       #$prog{$pid}{duration} += pop @time;
+                       #$prog{$pid}{duration} += (pop @time) * 60;
+                       #$prog{$pid}{duration} += (pop @time) * 3600;
+                       logger "INFO: ITV Video Name: $part\n";
+
+                       logger "INFO: ITV Video Stage 3 URL: $url_3\n" if $opt{verbose};
+                       $entry = request_url_retry($ua, $url_3, 2, '', '');
+                       logger "DEBUG: Response data: $entry\n" if $opt{debug};
+
+                       # Extract mms (replace 'http' with 'mms') url: e.g.: Ref1=http://itvbrdbnd.wmod.llnwd.net/a1379/o21/ucontent/2007/6/22/1549_384_1_2.wmv?MSWMExt=.asf
+                       chomp( $url_4 = 'mms'.$1 ) if $entry =~ m{Ref1=http(.+?)[\r\n]+};
+                       logger "INFO: ITV Video URL: $url_4\n" if $opt{verbose};
+                       push @url_list, $url_4;
+               }
+               #logger "INFO: ITV Video Total Duration: $prog{$pid}{duration}\n";
+
+               # Determine final filename
+               $prog{$pid}{ext} = 'mp4';
+
+               # Get and set more meta data
+               my %metadata = get_pid_metadata($ua, $pid);
+               $prog{$pid}{name} = $metadata{name} if ! $prog{$pid}{name};
+               $prog{$pid}{episode} = $metadata{episode} if ! $prog{$pid}{episode};
+               $prog{$pid}{available} = $metadata{available} if ! $prog{$pid}{available};
+               $prog{$pid}{duration} = $metadata{duration} if ! $prog{$pid}{duration};
+               $prog{$pid}{thumbnail} = $metadata{thumbnail} if ! $prog{$pid}{thumbnail};
+               $prog{$pid}{desc} = $metadata{desc} if ! $prog{$pid}{desc};
+               $prog{$pid}{guidance} = $metadata{guidance} if ! $prog{$pid}{guidance};
+
+               $prog{$pid}{fileprefix} = generate_download_filename_prefix( $pid, ${dir}, $opt{fileprefix} || "<name> <pid>" );
+               logger "\rINFO: File name prefix = $prog{$pid}{fileprefix}                 \n";
+               $prog{$pid}{dir} = $download_dir{tv};
+               # Create a subdir if there are multiple parts
+               if ($#url_list > 0) {
+                       $prog{$pid}{dir} .= "/$prog{$pid}{fileprefix}";
+                       logger "INFO: Creating subdirectory $prog{$pid}{dir} for programme\n" if $opt{verbose};
+                       mkpath $prog{$pid}{dir} if ! -d $prog{$pid}{dir};
+               }
+               my $file_done = "$prog{$pid}{dir}/$prog{$pid}{fileprefix}.$prog{$pid}{ext}";
+               my $file = "$prog{$pid}{dir}/$prog{$pid}{fileprefix}.partial.$prog{$pid}{ext}";
+               $prog{$pid}{filename} = $file_done;
+       
+               # Display metadata
+               logger "\nPid:\t\t$pid\n";
+               logger "Name:\t\t$prog{$pid}{name}\n";
+               logger "Duration:\t$prog{$pid}{duration}\n";
+               logger "Available:\t$prog{$pid}{available}\n";
+               logger "Expires:\t$prog{$pid}{expiry}\n";
+               logger "Description:\t$prog{$pid}{desc}\n";
+               return download_mms_video_stream( $ua, (join '|', @url_list), $file, $file_done, $pid );
+               exit 0;
+       }
+
+       # If were a podcast...
+       if ( $prog{$pid}{type} eq 'podcast' ) {
+               # stream data no available for podcasts
+               return 1 if $opt{streaminfo};
+               # Determine the correct filename and extension for this download
+               my $filename_orig = $pid;
+               $prog{$pid}{ext} = $pid;
+               $filename_orig =~ s|^.+/(.+?)\.\w+$|$1|g;
+               $prog{$pid}{ext} =~ s|^.*\.(\w+)$|$1|g;
+               $prog{$pid}{fileprefix} = generate_download_filename_prefix($pid, $dir, $opt{fileprefix} || "<longname> - <episode> $filename_orig");
+               $prog{$pid}{dir} = $dir;
+               logger "\rINFO: File name prefix = $prog{$pid}{fileprefix}                  \n";
+               my $file_done = "${dir}/$prog{$pid}{fileprefix}.$prog{$pid}{ext}";
+               my $file = "${dir}/$prog{$pid}{fileprefix}.partial.$prog{$pid}{ext}";
+               $prog{$pid}{filename} = $file_done;
+               if ( -f $file_done ) {
+                       logger "WARNING: File $file_done already exists\n\n";
+                       return 1;
+               }
+
+               # Skip from here if we are only testing downloads
+               return 1 if $opt{test};
+
+               # Create symlink filename if required
+               my $file_symlink;
+               if ( $opt{symlink} ) {
+                       # Substitute the fields for the pid
+                       $file_symlink = substitute_fields( $pid, $opt{symlink} );
+               }
+
+               return download_podcast_stream( $ua, $pid, $file, $file_done, $file_symlink );
+       }
+
+       logger "INFO: Attempting to Download: $prog{$pid}{name} - $prog{$pid}{episode}\n";
+
+       # Get version => pid hash
+       my ( $prog_type, $title, %version_pids ) = get_version_pids( $ua, $pid );
+
+       # Extract Long Name, e.g.: iplayer.episode.setTitle("DIY SOS: Series 16: Swansea");
+       $prog{$pid}{longname} = $title;
+
+       # Strip off the episode name
+       $prog{$pid}{longname} =~ s/^(.+):.*?$/$1/g;
+
+       # Detect if this content is for radio
+       my $usemp3 = 0;
+       if ( $prog_type eq 'radio' ) {
+
+               # Display media stream data if required
+               if ( $opt{streaminfo} ) {
+                       display_stream_info( $pid, $version_pids{'default'}, 'all' );
+                       $opt{quiet} = 1;
+                       return 1;
+               }
+
+               # Type is definitely radio
+               $prog{$pid}{type} = 'radio';
+               $dir = $download_dir{ $prog{$pid}{type} };
+
+               # Check for flashaudio rtmp stream
+               if ( $opt{amode} eq 'flashaudio' ) {
+                       logger "DEBUG: Trying to get media stream metadata for $opt{amode} RTMP mode\n" if $opt{verbose};
+                       %streamdata = %{ get_media_stream_data( $pid, $version_pids{default}, $opt{amode}) };
+                       if ( ! $streamdata{streamurl} ) {
+                               logger "WARNING: No $opt{amode} version available - not falling back to RealAudio\n";
+                               return 1;
+                       } else {
+                               $prog{$pid}{ext} = 'mp3';
+                               logger "INFO: flashaudio MP3 RTMP stream media is available\n" if $opt{verbose};        
+                       }
+
+               # Check for mp3 stream - unless realaudio option is specified
+               } elsif ( $opt{amode} ne 'realaudio' ) {
+                       # Check for iphone mp3 radio stream
+                       if ( %{get_media_stream_data( $pid, $version_pids{default}, 'iphone')}->{streamurl} ) {
+                               $usemp3 = 1;
+                               $prog{$pid}{ext} = 'mp3';
+                               logger "INFO: MP3 stream media is available\n" if $opt{verbose};
+
+                       # if mp3audio option is specified do not fallback to realaudio
+                       } elsif ( $opt{amode} =~ /^(iphone|mp3)/ ) {
+                               logger "ERROR: No MP3 stream media is available - not falling back to RealAudio\n";
+                               return 1;
+
+                       # if not then force realaudio option as fallback
+                       } else {
+                               $opt{amode} = 'realaudio';
+                               logger "INFO: No MP3 stream media is available - falling back to RealAudio\n" if $opt{verbose};
+                       }
+               }
+
+               # Use realplayer stream
+               if ( $opt{amode} eq 'realaudio' ) {
+
+                       # Check dependancies for radio programme transcoding / streaming
+                       # Check if we need 'tee'
+                       if ( (! exists_in_path($tee)) && $opt{stdout} && (! $opt{nowrite}) ) {
+                               logger "\nERROR: $tee does not exist in path, skipping\n";
+                               return 20;
+                       }
+                       # Check if we have mplayer and lame
+                       if ( (! $opt{wav}) && (! $opt{raw}) && (! exists_in_path($lame)) ) {
+                               logger "\nWARNING: Required $lame does not exist, falling back to wav mode\n";
+                               $opt{wav} = 1;
+                       }               
+                       if (! exists_in_path($mplayer)) {
+                               logger "\nERROR: Required $mplayer does not exist, skipping\n";
+                               return 20;
+                       }
+
+                       my $url_2 = %{get_media_stream_data( $pid, $version_pids{default}, 'realaudio')}->{streamurl};
+                               
+                       logger "INFO: Version = $prog{$pid}{version}\n" if $opt{verbose};
+                       logger "INFO: Stage 2 URL = $url_2\n" if $opt{verbose};
+
+                       # Report error if no versions are available
+                       if ( ! $url_2 ) {
+                               logger "ERROR: No Stage 2 URL\n" if $opt{verbose};
+                               return 15;
+                       }
+
+                       # Determine the correct filenames for this download
+                       $prog{$pid}{ext} = 'mp3';
+                       $prog{$pid}{ext} = 'ra'  if $opt{raw};
+                       $prog{$pid}{ext} = 'wav' if $opt{wav};
+                       $prog{$pid}{fileprefix} = generate_download_filename_prefix( $pid, ${dir}, $opt{fileprefix} || "<longname> - <episode> <pid>" );
+                       logger "\rINFO: File name prefix = $prog{$pid}{fileprefix}                 \n";
+                       $prog{$pid}{dir} = $dir;
+                       my $file_done = "${dir}/$prog{$pid}{fileprefix}.$prog{$pid}{ext}";
+                       my $file = "${dir}/$prog{$pid}{fileprefix}.partial.$prog{$pid}{ext}";
+                       $prog{$pid}{filename} = $file_done;
+                       if ( -f $file_done ) {
+                               logger "WARNING: File $file_done already exists\n\n";
+                               return 1;
+                       }
+
+                       # Skip from here if we are only testing downloads
+                       return 1 if $opt{test};
+
+                       # Create symlink filename if required
+                       my $file_symlink;
+                       if ( $opt{symlink} ) {
+                               # Substitute the fields for the pid
+                               $file_symlink = substitute_fields( $pid, $opt{symlink} );
+                       }
+
+                       # Do the audio download
+                       return download_rtsp_stream( $ua, $url_2, $file, $file_done, $file_symlink, $pid );
+               }
+
+       } else {
+               # Type is definitely tv
+               $prog{$pid}{type} = 'tv';
+               $dir = $download_dir{ $prog{$pid}{type} };
+       }
+
+
+       # iPhone mp3/h.264 stream downloading...
+
+       # Check if we have vlc - if not use iPhone mode
+       if ( $opt{vmode} eq 'n95' && (! exists_in_path($vlc)) ) {
+               logger "\nWARNING: Required $vlc does not exist, falling back to iPhone mode\n";
+               $opt{vmode} = 'iphone';
+       }               
+
+       my $url_2;
+       my $got_url;
+       # Lookup table to determine which ext to use for different download methods
+       my %stream_ext = (
+               flashhigh       => 'mp4',
+               flashnormal     => 'avi',
+               flashwii        => 'avi',
+       );
+       # Do this for each version tried in this order (if they appeared in the content)
+       for my $version ( @version_search_list ) {
+
+               # Change $verpid to 'default' type if it exists, then Used 'signed' otherwise
+               if ( $version_pids{$version} ) {
+                       logger "INFO: Checking existence of $version version\n";
+                       $prog{$pid}{version} = $version;
+                       logger "INFO: Version = $prog{$pid}{version}\n" if $opt{verbose};
+                       if( $opt{vmode} !~ /^(rtmp|flash)/ ) {
+                               $url_2 = get_iphone_stream_download_url( $ua, $version_pids{$version} );
+                       } else {
+                               # Which d/l methods to use for rtmp
+                               my @rtmplist;
+                               # Try all if rtmp mode is used
+                               if ( $opt{vmode} eq 'rtmp' ) {
+                                       @rtmplist = ( 'flashhigh', 'flashhigh', 'flashwii' );
+                               # Otherwise use specified rtmp flash method only
+                               } else {
+                                       push @rtmplist, $opt{vmode};
+                               }
+                               
+                               for my $rtmpmethod (@rtmplist) {
+                                       logger "DEBUG: Trying to get media stream metadata for $rtmpmethod RTMP mode\n" if $opt{verbose};
+                                       $prog{$pid}{ext} = $stream_ext{$rtmpmethod};
+                                       %streamdata = %{ get_media_stream_data( $pid, $version_pids{ $prog{$pid}{version} }, $rtmpmethod) };
+                                       $url_2 = $streamdata{streamurl};
+                                       if ( ! $url_2 ) {
+                                               logger "WARNING: No $rtmpmethod version available\n";
+                                       } else {
+                                               last;
+                                       }
+                               }
+                               # Force raw mode if ffmpeg is not installed
+                               if ( ! exists_in_path($ffmpeg)) {
+                                       logger "\nWARNING: $ffmpeg does not exist - not converting flv file\n";
+                                       $opt{raw} = 1;
+                               }
+                               # If were rtmp and raw set file ext to flv
+                               $prog{$pid}{ext} = 'flv' if $opt{raw};
+                       }
+                       $got_url = 1;
+               }
+               # Break out of loop if we have an actual URL
+               last if $got_url && $url_2;
+       }
+
+       # Report error if no versions are available
+       if ( ! $got_url ) {
+               logger "ERROR: No versions exist for download\n";
+               return 14;
+       }
+
+       # Display media stream data if required
+       if ( $opt{streaminfo} ) {
+               display_stream_info( $pid, $version_pids{'default'}, 'all' );
+               $opt{quiet} = 1;
+               return 1;
+       }
+
+       # Report error if failed to get URL for version
+       if ( $got_url && ! $url_2 ) {
+               logger "ERROR: No Stage 2 URL\n" if $opt{verbose};
+               # If mp3 audio stream does not exist force realaudio mode and retry
+               if ( $usemp3 && $opt{amode} !~ /(mp3audio|flashaudio)/ ) {
+                       $opt{amode} = 'realaudio';
+                       return 'retry';
+               }
+               # If rtmp flash video stream does not exist
+               if ( $opt{vmode} =~ /^(rtmp|flash)/ ) {
+                       logger "ERROR: No RTMP Flash versions exist\n";
+               }
+               return 15;
+       }
+       
+       # Determine the correct filenames for this download
+       $prog{$pid}{fileprefix} = generate_download_filename_prefix( $pid, $dir, $opt{fileprefix} || "<longname> - <episode> <pid> <version>" );
+       logger "\rINFO: File name prefix = $prog{$pid}{fileprefix}                 \n";
+       $prog{$pid}{dir} = $dir;
+       my $file_done = "${dir}/$prog{$pid}{fileprefix}.$prog{$pid}{ext}";
+       my $file = "${dir}/$prog{$pid}{fileprefix}.partial.$prog{$pid}{ext}";
+       $prog{$pid}{filename} = $file_done;
+       if ( -f $file_done ) {
+               logger "WARNING: File $file_done already exists\n\n";
+               return 1;
+       }
+
+       # Create symlink filename if required
+       my $file_symlink;
+       if ( $opt{symlink} ) {
+               # Substitute the fields for the pid
+               $file_symlink = substitute_fields( $pid, $opt{symlink} );
+       }
+
+       # Skip from here if we are only testing downloads
+       return 1 if $opt{test};
+
+       # Get subtitles if they exist and are required
+       my $subfile_done;
+       my $subfile;
+       if ( $opt{subtitles} ) {
+               $subfile_done = "${dir}/$prog{$pid}{fileprefix}.srt";
+               $subfile = "${dir}/$prog{$pid}{fileprefix}.partial.srt";
+               $ua->proxy( ['http'] => undef ) if $opt{partialproxy};
+               download_subtitles( $ua, $subfile, $version_pids{ $prog{$pid}{version} } );
+               $ua->proxy( ['http'] => $proxy_url ) if $opt{partialproxy};
+       }
+
+       my $return;
+       # Do rtmp download
+       if ( $opt{vmode} =~ /^(rtmp|flash)/ ) {
+               $return = download_rtmp_stream( $ua, $streamdata{streamurl}, $pid, $streamdata{application}, $streamdata{tcurl}, $streamdata{authstring}, $streamdata{swfurl}, $file, $file_done, $file_symlink );
+
+       # Do the RTMP flashaudio download
+       } elsif ( $opt{amode} eq 'flashaudio' ) {
+               $return = download_rtmp_stream( $ua, $streamdata{streamurl}, $pid, $streamdata{application}, $streamdata{tcurl}, $streamdata{authstring}, $streamdata{swfurl}, $file, $file_done, $file_symlink );
+
+       # Do the N95 h.264 download
+       } elsif ( $opt{vmode} eq 'n95' ) {
+               my $url = %{get_media_stream_data( $pid, $version_pids{ $prog{$pid}{version} }, 'n95_wifi')}->{streamurl};
+               $return = download_h264_low_stream( $ua, $url, $file, $file_done );
+
+       # Do the iPhone h.264 download
+       } elsif ( $prog{$pid}{type} eq 'tv' ) {
+               # Disable proxy here if required
+               $ua->proxy( ['http'] => undef ) if $opt{partialproxy};
+               $return = download_iphone_stream( $ua, $url_2, $pid, $file, $file_done, $file_symlink, 1 );
+               # Re-enable proxy here if required
+               $ua->proxy( ['http'] => $proxy_url ) if $opt{partialproxy};
+
+       # Do the iPhone mp3 download
+       } elsif ( $prog{$pid}{type} eq 'radio' ) {
+               # Disable proxy here if required
+               $ua->proxy( ['http'] => undef ) if $opt{partialproxy};
+               $return = download_iphone_stream( $ua, $url_2, $pid, $file, $file_done, $file_symlink, 0 );
+               # Re-enable proxy here if required
+               $ua->proxy( ['http'] => $proxy_url ) if $opt{partialproxy};
+               # If the iphone mp3 download fails then it's probably not ready yet so retry using realaudio unless amode is set to iphone or mp3audio
+               $opt{amode} = 'realaudio' if $return eq 'retry' && $opt{amode} !~ /^(iphone|mp3audio)/ ;
+       }
+       
+       # Rename the subtitle file accordingly
+       move($subfile, $subfile_done) if $opt{subtitles} && -f $subfile;
+
+       # Re-symlink file
+       if ( $opt{symlink} ) {
+               # remove old symlink
+               unlink $file_symlink if -l $file_symlink;
+               symlink $file_done, $file_symlink;
+               logger "INFO: Created symlink from '$file_symlink' -> '$file_done'\n" if $opt{verbose};
+       }
+
+       return $return;
+}
+
+
+
+# Download Subtitles, convert to srt(SubRip) format and apply time offset
+sub download_subtitles {
+       my ( $ua, $file, $verpid ) = @_;
+       my $suburl;
+       my $subs;
+       logger "INFO: Getting Subtitle metadata for $verpid\n" if $opt{verbose};
+       $suburl = %{get_media_stream_data( undef, $verpid, 'subtitles')}->{streamurl};
+       # Return if we have no url
+       if (! $suburl) {
+               logger "INFO: Subtitles not available\n";
+               return 2;
+       }
+
+       logger "INFO: Getting Subtitles from $suburl\n" if $opt{verbose};
+
+       # Open subs file
+       unlink($file);
+       my $fh = open_file_append($file);
+
+       # Download subs
+       $subs = request_url_retry($ua, $suburl, 2);
+       if (! $subs ) {
+               logger "ERROR: Subtitle Download failed\n";
+               return 1;
+       } else {
+               logger "INFO: Downloaded Subtitles\n";
+       }
+
+       # Convert the format to srt
+       # SRT:
+       #1
+       #00:01:22,490 --> 00:01:26,494
+       #Next round!
+       #
+       #2
+       #00:01:33,710 --> 00:01:37,714
+       #Now that we've moved to paradise, there's nothing to eat.
+       #
+       
+       # TT:
+       #<p begin="0:01:12.400" end="0:01:13.880">Thinking.</p>
+
+       my $count = 1;
+       my @lines = grep /<p\s+begin/, split /\n/, $subs;
+       for ( @lines ) {
+               my ( $begin, $end, $sub );
+               $begin = $1 if m{begin="(.+?)"};
+               $end =   $1 if m{end="(.+?)"};
+               $sub =   $1 if m{>(.+?)</p>};
+               if ($begin && $end && $sub ) {
+                       $begin =~ s/\./,/g;
+                       $end =~ s/\./,/g;
+                       if ($opt{suboffset}) {
+                               $begin = subtitle_offset( $begin, $opt{suboffset} );
+                               $end = subtitle_offset( $end, $opt{suboffset} );
+                       }
+                       decode_entities($sub);
+                       # Write to file
+                       print $fh "$count\n";
+                       print $fh "$begin --> $end\n";
+                       print $fh "$sub\n\n";
+                       $count++;
+               }
+       }       
+       close $fh;
+
+       return 0;
+}
+
+
+
+# Returns an offset timestamp given an srt begin or end timestamp and offset in ms
+sub subtitle_offset {
+       my ( $timestamp, $offset ) = @_;
+       my ( $hr, $min, $sec, $ms ) = split /[:,\.]/, $timestamp;
+       # split into hrs, mins, secs, ms
+       my $ts = $ms + $sec*1000 + $min*60*1000 + $hr*60*60*1000 + $offset;
+       $hr = int( $ts/(60*60*1000) );
+       $ts -= $hr*60*60*1000;
+       $min = int( $ts/(60*1000) );
+       $ts -= $min*60*1000;
+       $sec = int( $ts/1000 );
+       $ts -= $sec*1000;
+       $ms = $ts;
+       return "$hr:$min:$sec,$ms";
+}
+
+
+
+# Return hash of version => verpid given a pid
+sub get_version_pids {
+       my ( $ua, $pid ) = @_;
+       my %version_pids;
+       my $url = $prog_iplayer_metadata.$pid;
+
+       logger "INFO: iPlayer metadata URL = $url\n" if $opt{verbose};
+       logger "INFO: Getting version pids for programme $pid        \n" if ! $opt{verbose};
+
+       # send request
+       my $res = $ua->request( HTTP::Request->new( GET => $url ) );
+       if ( ! $res->is_success ) {
+               logger "\rERROR: Failed to get version pid metadata from iplayer site\n\n";
+               return %version_pids;
+       }
+
+       # The URL http://www.bbc.co.uk/iplayer/playlist/<PID> contains for example:
+       #<?xml version="1.0" encoding="UTF-8"?>                                   
+       #<playlist xmlns="http://bbc.co.uk/2008/emp/playlist" revision="1">       
+       #  <id>tag:bbc.co.uk,2008:pips:b00dlrc8:playlist</id>                     
+       #  <link rel="self" href="http://www.bbc.co.uk/iplayer/playlist/b00dlrc8"/>
+       #  <link rel="alternate" href="http://www.bbc.co.uk/iplayer/episode/b00dlrc8"/>
+       #  <link rel="holding" href="http://www.bbc.co.uk/iplayer/images/episode/b00dlrc8_640_360.jpg" height="360" width="640" type="image/jpeg" />
+       #  <title>Amazon with Bruce Parry: Episode 1</title>                                                                                        
+       #  <summary>Bruce Parry begins an epic adventure in the Amazon following the river from source to sea, beginning  in the High Andes and visiting the Ashaninka tribe.</summary>                                                                                                        
+       #  <updated>2008-09-18T14:03:35Z</updated>                                                                                                  
+       #  <item kind="ident">                                                                                                                      
+       #    <id>tag:bbc.co.uk,2008:pips:bbc_two</id>                                                                                               
+       #    <mediator identifier="bbc_two" name="pips"/>                                                                                           
+       #  </item>                                                                                                                                  
+       #  <item kind="programme" duration="3600" identifier="b00dlr9p" group="b00dlrc8" publisher="pips">                                          
+       #    <tempav>1</tempav>                                                                                                                     
+       #    <id>tag:bbc.co.uk,2008:pips:b00dlr9p</id>                                                                                              
+       #    <service id="bbc_two" href="http://www.bbc.co.uk/iplayer/bbc_two">BBC Two</service>                                                    
+       #    <masterbrand id="bbc_two" href="http://www.bbc.co.uk/iplayer/bbc_two">BBC Two</masterbrand>                                            
+       #
+       #    <alternate id="default" />
+       #    <guidance>Contains some strong language.</guidance>
+       #    <mediator identifier="b00dlr9p" name="pips"/>      
+       #  </item>
+       #  <item kind="programme" duration="3600" identifier="b00dp4xn" group="b00dlrc8" publisher="pips">
+       #    <tempav>1</tempav>
+       #    <id>tag:bbc.co.uk,2008:pips:b00dp4xn</id>
+       #    <service id="bbc_one" href="http://www.bbc.co.uk/iplayer/bbc_one">BBC One</service>
+       #    <masterbrand id="bbc_two" href="http://www.bbc.co.uk/iplayer/bbc_two">BBC Two</masterbrand>
+       #
+       #    <alternate id="signed" />
+       #    <guidance>Contains some strong language.</guidance>
+       #    <mediator identifier="b00dp4xn" name="pips"/>
+       #  </item>
+
+       my $xml = $res->content;        
+       # flatten
+       $xml =~ s/\n/ /g;
+
+       # Get title
+       # <title>Amazon with Bruce Parry: Episode 1</title>
+       my ( $title, $prog_type );
+       $title = $1 if $xml =~ m{<title>\s*(.+?)\s*<\/title>};
+
+       # Get type
+       $prog_type = 'tv' if grep /kind="programme"/, $xml;
+       $prog_type = 'radio' if grep /kind="radioProgramme"/, $xml;
+
+       # Split into <item kind="programme"> sections
+       for ( split /<item\s+kind="(radioProgramme|programme)"/, $xml ) {
+               logger "DEBUG: Block: $_\n" if $opt{debug};
+               my ($verpid, $version);
+               #  duration="3600" identifier="b00dp4xn" group="b00dlrc8" publisher="pips">
+               $verpid = $1 if m{\s+duration=".*?"\s+identifier="(.+?)"};
+               # <alternate id="default" />
+               $version = lc($1) if m{<alternate\s+id="(.+?)"};
+               next if ! ($verpid && $version);
+               $version_pids{$version} = $verpid;
+               logger "INFO: Version: $version, VersionPid: $verpid\n" if $opt{verbose};  
+       }
+       # Add to prog hash
+       $prog{$pid}{versions} = join ',', keys %version_pids;
+       return ( $prog_type, $title, %version_pids );
+}
+
+
+
+# Gets media streams data for this version pid
+# $media = all|flashhigh|flashnormal|iphone|flashwii|n95_wifi|n95_3g|mobile|flashaudio|realaudio|wma|subtitles
+sub get_media_stream_data {
+       my ( $pid, $verpid, $media ) = @_;
+       my %data;
+
+       # Setup user agent with redirection enabled
+       my $ua = LWP::UserAgent->new();
+       $ua->timeout([$lwp_request_timeout]);
+       $ua->proxy( ['http'] => $proxy_url );
+       $ua->cookie_jar( HTTP::Cookies->new( file => $cookiejar, autosave => 1, ignore_discard => 1 ) );
+       $opt{quiet} = 0 if $opt{streaminfo};
+
+       my $xml1 = request_url_retry($ua, $media_stream_data_prefix.$verpid, 3, '', '');
+       logger "\n$xml1\n" if $opt{debug};
+       # flatten
+       $xml1 =~ s/\n/ /g;
+       
+       for my $xml ( split /<media/, $xml1 ) {
+               $xml = "<media".$xml;
+               my $prog_type;
+
+               # h.264 high quality stream
+               #       <media kind="video"
+               #        width="640"
+               #        height="360"
+               #        type="video/mp4"
+               #        encoding="h264"  >
+               #        <connection
+               #                priority="10"
+               #                application="bbciplayertok"
+               #                kind="level3"
+               #                server="bbciplayertokfs.fplive.net"
+               #                identifier="mp4:b000zxf4-H26490898078"
+               #                authString="d52f77fede048f1ffd6587fd47446dee"
+               #        />
+               # application: bbciplayertok
+               # tcURL: rtmp://bbciplayertokfs.fplive.net:80/bbciplayertok
+               if ( $media =~ /^(flashhigh|all)$/ && $xml =~ m{<media\s+kind="video".+?type="video/mp4".+?encoding="h264".+?application="(.+?)".+?kind="level3"\s+server="(.+?)"\s+?identifier="(.+?)"\s+?authString="(.+?)"} ) {
+                       $prog_type = 'flashhigh';
+                       logger "DEBUG: Processing $prog_type stream\n" if $opt{verbose};
+                       ( $data{$prog_type}{application}, $data{$prog_type}{server}, $data{$prog_type}{identifier}, $data{$prog_type}{authstring} ) = ( $1, $2, $3, $4 );
+                       $data{$prog_type}{type} = 'Flash RTMP H.264 high quality stream';
+                       $data{$prog_type}{tcurl} = "rtmp://$data{$prog_type}{server}:80/$data{$prog_type}{application}";
+                       $data{$prog_type}{swfurl} = "http://www.bbc.co.uk/emp/9player.swf?revision=7276";
+                       $data{$prog_type}{streamurl} = "rtmp://$data{$prog_type}{server}:1935/ondemand?_fcs_vhost=$data{$prog_type}{server}&auth=$data{$prog_type}{authstring}&aifp=v001&slist=$data{$prog_type}{identifier}";
+               }
+
+               # h.264 normal quality stream
+               #       <media kind="video"
+               #        width="640"
+               #        height="360"
+               #        type="video/x-flv"
+               #        encoding="vp6"  >
+               #        <connection
+               #                priority="10"
+               #                kind="akamai"
+               #                server="cp41752.edgefcs.net"
+               #                identifier="secure/b000zxf4-streaming90898078"
+               #                authString="daEdSdgbcaibFa7biaobCaYdadyaTamazbq-biXsum-cCp-FqrECnEoGBwFvwG"
+               #        />
+               #       </media>
+               #
+               # application (e.g.):            ondemand?_fcs_vhost=cp41752.edgefcs.net&auth=daEcia8aQaRardxdwb_dCbvc0cPbLavc2cL-bjw5rj-cCp-JnlDCnzn.MEqHpxF&aifp=v001&slist=secure/b000gy717streaming103693754
+               # tcURL: rtmp://88.221.26.165:80/ondemand?_fcs_vhost=cp41752.edgefcs.net&auth=daEcia8aQaRardxdwb_dCbvc0cPbLavc.2cL-bjw5rj-cCp-JnlDCnznMEqHpxF&aifp=v001&slist=secure/b000gy717streaming103693754
+               if ( $media =~ /^(flashnormal|all)$/ && $xml =~ m{<media\s+kind="video".+?type="video/x-flv".+?encoding="vp6".+?kind="akamai"\s+server="(.+?)"\s+?identifier="(.+?)"\s+?authString="(.+?)"} ) {
+                       $prog_type = 'flashnormal';
+                       logger "DEBUG: Processing $prog_type stream\n" if $opt{verbose};
+                       ( $data{$prog_type}{server}, $data{$prog_type}{identifier}, $data{$prog_type}{authstring} ) = ( $1, $2, $3 );
+                       $data{$prog_type}{application} = "ondemand?_fcs_vhost=$data{$prog_type}{server}&auth=$data{$prog_type}{authstring}&aifp=v001&slist=$data{$prog_type}{identifier}";
+                       $data{$prog_type}{type} = 'Flash RTMP H.264 normal quality stream';
+                       $data{$prog_type}{tcurl} = "rtmp://$data{$prog_type}{server}:80/$data{$prog_type}{application}";
+                       $data{$prog_type}{swfurl} = "http://www.bbc.co.uk/emp/9player.swf?revision=7276";
+                       $data{$prog_type}{streamurl} = "rtmp://$data{$prog_type}{server}:1935/ondemand?_fcs_vhost=$data{$prog_type}{server}&auth=$data{$prog_type}{authstring}&aifp=v001&slist=$data{$prog_type}{identifier}";
+               }
+
+               # Wii h.264 standard quality stream
+               #<media kind="video"
+               #        width="512"
+               #        height="288"
+               #        type="video/x-flv"
+               #        encoding="spark"  >
+               #        <connection
+               #                priority="10"
+               #                kind="akamai"
+               #                server="cp41752.edgefcs.net"
+               #                identifier="secure/5242138581547639062"
+               #                authString="daEd8dLbGaPaZdzdNcwd.auaydJcxcHandp-biX5YL-cCp-BqsECnxnGEsHwyE"
+               #        />
+               #</media>
+               # application (e.g.):               ondemand?_fcs_vhost=cp41752.edgefcs.net&auth=daEcpc6cYbhdIakdWduc6bJdPbydbazdmdp-bjxPBF-cCp-GptFAoDqJBnHvzC&aifp=v001&slist=secure/b000g884xstreaming101052333
+               # tcURL: rtmp: //88.221.26.173:1935/ondemand?_fcs_vhost=cp41752.edgefcs.net&auth=daEcpc6cYbhdIakdWduc6bJdPbydbazdmdp-bjxPBF-cCp-GptFAoDqJBnHvzC&aifp=v001&slist=secure/b000g884xstreaming101052333
+               # swfUrl: http://www.bbc.co.uk/emp/iplayer/7player.swf?revision=3897
+               if ( $media =~ /^(flashwii|all)$/ && $xml =~ m{<media\s+kind="video".+?type="video/x-flv".+?encoding="spark".+?kind="akamai"\s+server="(.+?)"\s+?identifier="(.+?)"\s+?authString="(.+?)"} ) {
+                       $prog_type = 'flashwii';
+                       logger "DEBUG: Processing $prog_type stream\n" if $opt{verbose};
+                       ( $data{$prog_type}{server}, $data{$prog_type}{identifier}, $data{$prog_type}{authstring} ) = ( $1, $2, $3 );
+                       $data{$prog_type}{application} = "ondemand?_fcs_vhost=$data{$prog_type}{server}&auth=$data{$prog_type}{authstring}&aifp=v001&slist=$data{$prog_type}{identifier}";
+                       $data{$prog_type}{type} = 'Flash RTMP H.264 Wii stream';
+                       $data{$prog_type}{tcurl} = "rtmp://$data{$prog_type}{server}:1935/$data{$prog_type}{application}";
+                       $data{$prog_type}{swfurl} = "http://www.bbc.co.uk/emp/iplayer/7player.swf?revision=3897";
+                       $data{$prog_type}{streamurl} = "rtmp://$data{$prog_type}{server}:1935/ondemand?_fcs_vhost=$data{$prog_type}{server}&auth=$data{$prog_type}{authstring}&aifp=v001&slist=$data{$prog_type}{identifier}";
+               }
+       
+               # iPhone h.264/mp3 stream
+               #<media kind="video"
+               #        width="480"
+               #        height="272"
+               #        type="video/mp4"
+               #        encoding="h264"  >
+               #        <connection
+               #                priority="10"
+               #                kind="sis"
+               #                server="http://www.bbc.co.uk/mediaselector/3/auth/stream/"
+               #                identifier="5242138581547639062"
+               #                href="http://www.bbc.co.uk/mediaselector/3/auth/stream/5242138581547639062.mp4"
+               #        />
+               #</media>
+               if ( $media =~ /^(iphone|all)$/ && $xml =~ m{<media\s+kind="video".+?type="video/mp4".+?encoding="h264".+?kind="sis"\s+server="(.+?)"\s+?identifier="(.+?)"\s+?href="(.+?)"} ) {
+                       $prog_type = 'iphone';
+                       logger "DEBUG: Processing $prog_type stream\n" if $opt{verbose};
+                       ( $data{$prog_type}{server}, $data{$prog_type}{identifier}, $data{$prog_type}{streamurl} ) = ( $1, $2, $3 );
+                       $data{$prog_type}{type} = 'iPhone stream';
+               }
+       
+               # Nokia N95 h.264 low quality stream (WiFi)
+               #<media kind="video"
+               #        type="video/mpeg"
+               #        encoding="h264"  >
+               #        <connection
+               #                priority="10"
+               #                kind="sis"
+               #                server="http://www.bbc.co.uk/mediaselector/4/sdp/"
+               #                identifier="b00108ld/iplayer_streaming_n95_wifi"
+               #                href="http://www.bbc.co.uk/mediaselector/4/sdp/b00108ld/iplayer_streaming_n95_wifi"
+               #        />
+               #</media>
+               if ( $media =~ /^(n95_wifi|all)$/ && $xml =~ m{<media\s+kind="video".+?type="video/mpeg".+?encoding="h264".+?kind="sis"\s+server="(.+?)"\s+?identifier="(.+?)"\s+?href="(.+?)"} ) {
+                       $prog_type = 'n95_wifi';
+                       logger "DEBUG: Processing $prog_type stream\n" if $opt{verbose};
+                       ( $data{$prog_type}{server}, $data{$prog_type}{identifier}, $data{$prog_type}{href} ) = ( $1, $2, $3 );
+                       $data{$prog_type}{type} = 'Nokia N95 h.264 low quality WiFi stream';
+                       $opt{quiet} = 1 if $opt{streaminfo};
+                       chomp( $data{$prog_type}{streamurl} = request_url_retry($ua, $data{$prog_type}{href}, 2, '', '') );
+                       $opt{quiet} = 0 if $opt{streaminfo};
+               }
+       
+               # Nokia N95 h.264 low quality stream (3G)
+               #<media kind="" 
+               #        expires="2008-10-30T12:29:00+00:00"
+               #        type="video/mpeg"                  
+               #        encoding="h264"  >                 
+               #        <connection                        
+               #                priority="10"              
+               #                kind="sis"                 
+               #                server="http://www.bbc.co.uk/mediaselector/4/sdp/" 
+               #                identifier="b009tzxx/iplayer_streaming_n95_3g"     
+               #                href="http://www.bbc.co.uk/mediaselector/4/sdp/b009tzxx/iplayer_streaming_n95_3g" 
+               #        />                                                                                        
+               #</media>    
+               if ( $media =~ /^(n95_3g|all)$/ && $xml =~ m{<media\s+kind="".+?type="video/mpeg".+?encoding="h264".+?kind="sis"\s+server="(.+?)"\s+?identifier="(.+?)"\s+?href="(.+?)"} ) {
+                       $prog_type = 'n95_3g';
+                       logger "DEBUG: Processing $prog_type stream\n" if $opt{verbose};
+                       ( $data{$prog_type}{server}, $data{$prog_type}{identifier}, $data{$prog_type}{href} ) = ( $1, $2, $3 );
+                       $data{$prog_type}{type} = 'Nokia N95 h.264 low quality 3G stream';
+                       $opt{quiet} = 1 if $opt{streaminfo};
+                       chomp( $data{$prog_type}{streamurl} = request_url_retry($ua, $data{$prog_type}{href}, 2, '', '') );
+                       $opt{quiet} = 0 if $opt{streaminfo};
+               }
+       
+               # Mobile WMV DRM
+               #<media kind="video"
+               #        expires="2008-10-20T21:59:00+01:00"
+               #        type="video/wmv"   >
+               #        <connection
+               #                priority="10"
+               #                kind="licence"
+               #                server="http://iplayldsvip.iplayer.bbc.co.uk/WMLicenceIssuer/LicenceDelivery.asmx"
+               #                identifier="0A1CA43B-98A8-43EA-B684-DA06672C0575"
+               #                href="http://iplayldsvip.iplayer.bbc.co.uk/WMLicenceIssuer/LicenceDelivery.asmx/0A1CA43B-98A8-43EA-B684-DA06672C0575"
+               #        />
+               #<connection
+               #                priority="10"
+               #                kind="sis"
+               #                server="http://directdl.iplayer.bbc.co.uk/windowsmedia/"
+               #                identifier="AmazonwithBruceParry_Episode5_200810132100_mobile"
+               #                href="http://directdl.iplayer.bbc.co.uk/windowsmedia/AmazonwithBruceParry_Episode5_200810132100_mobile.wmv"
+               #        />
+               #</media>
+               if ( $media =~ /^(mobile|all)$/ && $xml =~ m{<media\s+kind="video".+?type="video/wmv".+?kind="sis"\s+server="(.+?)"\s+?identifier="(.+?)"\s+?href="(.+?)"} ) {
+                       $prog_type = 'mobile';
+                       logger "DEBUG: Processing $prog_type stream\n" if $opt{verbose};
+                       ( $data{$prog_type}{server}, $data{$prog_type}{identifier}, $data{$prog_type}{streamurl} ) = ( $1, $2, $3 );
+                       $data{$prog_type}{type} = 'Mobile WMV DRM stream';
+               }
+       
+               # Audio rtmp mp3
+               #<media kind="audio"
+               #        type="audio/mpeg"
+               #        encoding="mp3"  >
+               #        <connection
+               #                priority="10"
+               #                kind="akamai"
+               #                server="cp48181.edgefcs.net"
+               #                identifier="mp3:secure/radio1/RBN2_mashup_b00d67h9_2008_09_05_22_14_25"
+               #                authString="daEbQa1c6cda6aHdudxagcCcUcVbvbncmdK-biXtzq-cCp-DnoFIpznNBqHnzF"
+               #        />
+               #</media>
+               #app: ondemand?_fcs_vhost=cp48181.edgefcs.net&auth=daEasducLbidOancObacmc0amd6d7ana8c6-bjx.9v-cCp-JqlFHoEq.FBqGnxC&aifp=v001&slist=secure/radio1/RBN2_radio_1_-_wednesday_1000_b00g3xcj_2008_12_31_13_21_49
+               #swfUrl: http://www.bbc.co.uk/emp/9player.swf?revision=7276
+               #tcUrl: rtmp://92.122.210.173:1935/ondemand?_fcs_vhost=cp48181.edgefcs.net&auth=daEasducLbidOancObacmc0amd6d7ana8c6-bjx.9v-cCp-JqlFHoEqFBqGnxC&aifp=v001&slist=secure/radio1/RBN2_radio_1_-_wednesday_1.000_b00g3xcj_2008_12_31_13_21_49
+               #pageUrl: http://www.bbc.co.uk/iplayer/episode/b00g3xp7/Annie_Mac_31_12_2008/
+               if ( $media =~ /^(flashaudio|all)$/ && $xml =~ m{<media\s+kind="audio".+?type="audio/mpeg".+?encoding="mp3".+?kind="akamai"\s+server="(.+?)"\s+?identifier="(.+?)"\s+?authString="(.+?)"} ) {
+                       $prog_type = 'flashaudio';
+                       logger "DEBUG: Processing $prog_type stream\n" if $opt{verbose};
+                       ( $data{$prog_type}{server}, $data{$prog_type}{identifier}, $data{$prog_type}{authstring} ) = ( $1, $2, $3 );
+                       $data{$prog_type}{streamurl} = "rtmp://$data{$prog_type}{server}:1935/ondemand?_fcs_vhost=$data{$prog_type}{server}&auth=$data{$prog_type}{authstring}&aifp=v001&slist=$data{$prog_type}{identifier}";
+                       # Remove offending mp3: at the start of the identifier (don't remove in stream url)
+                       $data{$prog_type}{identifier} =~ s/^mp3://;
+                       $data{$prog_type}{application} = "ondemand?_fcs_vhost=$data{$prog_type}{server}&auth=$data{$prog_type}{authstring}&aifp=v001&slist=$data{$prog_type}{identifier}";
+                       $data{$prog_type}{type} = 'RTMP MP3 stream';
+                       $data{$prog_type}{tcurl} = "rtmp://$data{$prog_type}{server}:1935/$data{$prog_type}{application}";
+                       $data{$prog_type}{swfurl} = "http://www.bbc.co.uk/emp/9player.swf?revision=7276";
+               }
+       
+               # RealAudio stream
+               #<media kind="audio"
+               #       type="audio/real"
+               #        encoding="real"  >
+               #        <connection
+               #                priority="10"
+               #                kind="sis"
+               #                server="http://www.bbc.co.uk"
+               #                identifier="/radio/aod/playlists/9h/76/d0/0b/2000_bbc_radio_one"
+               #                href="http://www.bbc.co.uk/radio/aod/playlists/9h/76/d0/0b/2000_bbc_radio_one.ram"
+               #        />
+               #</media>
+               # Realaudio for worldservice    
+               #<media kind=""
+               #type="audio/real"
+               #encoding="real"  >
+               #<connection
+               #        priority="10"
+               #        kind="edgesuite"
+               #        server="http://http-ws.bbc.co.uk.edgesuite.net"
+               #        identifier="/generatecssram.esi?file=/worldservice/css/nb/410060838.ra"
+               #        href="http://http-ws.bbc.co.uk.edgesuite.net/generatecssram.esi?file=/worldservice/css/nb/410060838.ra"
+               #/>
+               #</media>
+               #</mediaSelection>
+               if ( $media =~ /^(realaudio|all)$/ && $xml =~ m{<media\s+kind="(audio|)".+?type="audio/real".+?encoding="real".+?kind="(sis|edgesuite)"\s+server="(.+?)"\s+?identifier="(.+?)"\s+?href="(.+?)"} ) {
+                       $prog_type = 'realaudio';
+                       logger "DEBUG: Processing $prog_type stream\n" if $opt{verbose};
+                       ( $data{$prog_type}{server}, $data{$prog_type}{identifier}, $data{$prog_type}{href} ) = ( $3, $4, $5 );
+                       $data{$prog_type}{type} = 'RealAudio RTSP stream';
+                       $opt{quiet} = 1 if $opt{streaminfo};
+                       chomp( $data{$prog_type}{streamurl} = request_url_retry($ua, $data{$prog_type}{href}, 2, '', '') );
+                       $data{$prog_type}{streamurl} =~ s/[\s\n]//g;
+                       $opt{quiet} = 0 if $opt{streaminfo};
+               }
+       
+               # Radio WMA (low quality)
+               #<mediaSelection xmlns="http://bbc.co.uk/2008/mp/mediaselection">
+               #<media kind=""
+               #        type="audio/wma"
+               #        encoding="wma"  >
+               #        <connection
+               #                priority="10"
+               #                kind="edgesuite"
+               #                server="http://http-ws.bbc.co.uk.edgesuite.net"
+               #                identifier="/generatecssasx.esi?file=/worldservice/css/nb/410060838"
+               #                href="http://http-ws.bbc.co.uk.edgesuite.net/generatecssasx.esi?file=/worldservice/css/nb/410060838.wma"
+               #        />
+               #</media>
+               if ( $media =~ /^(wma|all)$/ && $xml =~ m{<media\s+kind="(audio|)".+?type="audio/wma".+?encoding="wma".+?kind="(sis|edgesuite)"\s+server="(.+?)"\s+?identifier="(.+?)"\s+?href="(.+?)"} ) {
+                       $prog_type = 'wma';
+                       logger "DEBUG: Processing $prog_type stream\n" if $opt{verbose};
+                       ( $data{$prog_type}{server}, $data{$prog_type}{identifier}, $data{$prog_type}{href} ) = ( $3, $4, $5 );
+                       $data{$prog_type}{type} = 'WMA MMS stream';
+                       $opt{quiet} = 1 if $opt{streaminfo};
+                       chomp( $data{$prog_type}{streamurl} = request_url_retry($ua, $data{$prog_type}{href}, 2, '', '') );
+                       $data{$prog_type}{streamurl} =~ s/[\s\n]//g;
+                       # HREF="mms://a1899.v394403.c39440.g.vm.akamaistream.net/7/1899/39440/1/bbcworldservice.download.akamai.com/39440//worldservice/css/nb/410060838.wma"
+                       $data{$prog_type}{streamurl} =~ s/^.*href=\"(.+?)\".*$/$1/gi;
+                       $opt{quiet} = 0 if $opt{streaminfo};
+               }
+       
+               # Subtitles stream
+               #<media kind="captions"
+               #        type="application/ttaf+xml"   >
+               #        <connection
+               #                priority="10"
+               #                kind="http"
+               #                server="http://www.bbc.co.uk/iplayer/subtitles/"
+               #                identifier="b0008dc8rstreaming89808204.xml"
+               #                href="http://www.bbc.co.uk/iplayer/subtitles/b0008dc8rstreaming89808204.xml"
+               #        />
+               #</media>
+               if ( $media =~ /^(subtitles|all)$/ && $xml =~ m{<media\s+kind="captions".+?type="application/ttaf\+xml".+?kind="http"\s+server="(.+?)"\s+?identifier="(.+?)"\s+?href="(.+?)"} ) {
+                       $prog_type = 'subtitles';
+                       logger "DEBUG: Processing $prog_type stream\n" if $opt{verbose};
+                       ( $data{$prog_type}{server}, $data{$prog_type}{identifier}, $data{$prog_type}{streamurl} ) = ( $1, $2, $3 );
+                       $data{$prog_type}{type} = 'Subtitles stream';
+               }
+
+       }
+       
+       # Return a hash with media => url if 'all' is specified - otherwise just the specified url
+       if ( $media eq 'all' ) {
+               return %data;
+       } else {
+               # Make sure this hash exists before we pass it back...
+               $data{$media}{exists} = 0 if not defined $data{$media};
+               return $data{$media};
+       }
+}
+
+
+
+sub display_stream_info {
+       my ($pid, $verpid, $media) = (@_);
+       logger "INFO: Getting media stream metadata for $prog{$pid}{name} - $prog{$pid}{episode}, $verpid\n" if $pid;
+       my %data = get_media_stream_data( $pid, $verpid, $media);
+       # Print out stream data
+       for my $prog_type (sort keys %data) {
+               logger "     stream = $prog_type\n";
+               for my $entry ( sort keys %{ $data{$prog_type} } ) {
+                       logger sprintf("%11s = %s\n", $entry, $data{$prog_type}{$entry} );
+               }
+               logger "\n";
+               #logger "INFO: $data{$prog_type}{type}:\nINFO:  application=$data{$prog_type}{application}\nINFO:  server=$data{$prog_type}{server}\nINFO:  identifier=$data{$prog_type}{identifier}\nINFO:  authstring=$data{$prog_type}{authstring}\n" if $opt{verbose};
+               #logger "INFO: $data{$prog_type}{type} URL: $data{$prog_type}{streamurl}\n";
+       }
+}
+
+
+
+# Actually do the h.264/mp3 downloading
+# ( $ua, $pid, $url_2, $file, $file_done, '0|1 == rearrange moov' )
+sub download_iphone_stream {
+       my ( $ua, $url_2, $pid, $file, $file_done, $file_symlink, $rearrange ) = @_;
+
+       # Stage 3a: Download 1st byte to get exact file length
+       logger "INFO: Stage 3 URL = $url_2\n" if $opt{verbose};
+
+       # Override the $rearrange value is --raw option is specified
+       $rearrange = 0 if $opt{raw};
+       
+       # Setup request header
+       my $h = new HTTP::Headers(
+               'User-Agent'    => $user_agent{coremedia},
+               'Accept'        => '*/*',
+               'Range'         => 'bytes=0-1',
+       );
+       my $req = HTTP::Request->new ('GET', $url_2, $h);
+       my $res = $ua->request($req);
+       # e.g. Content-Range: bytes 0-1/181338136 (return if no content length returned)
+       my $download_len = $res->header("Content-Range");
+       if ( ! $download_len ) {
+               logger "ERROR: No Content-Range was obtained\n" if $opt{verbose};
+               return 'retry'
+       }
+       $download_len =~ s|^bytes 0-1/(\d+).*$|$1|;
+       logger "INFO: Download File Length $download_len\n" if $opt{verbose};
+
+       # Only do this if we're rearranging QT streams
+       my $mdat_start = 0;
+       # default to this if we are not rearranging (tells the download chunk loop where to stop - i.e. EOF instead of end of mdat atom)
+       my $moov_start = $download_len + 1;
+       my $header;
+       if ($rearrange) {
+               # Get ftyp+wide header etc
+               $mdat_start = 0x1c;
+               my $buffer = download_block(undef, $url_2, $ua, 0, $mdat_start + 4);
+               # Get bytes upto (but not including) mdat atom start -> $header
+               $header = substr($buffer, 0, $mdat_start);
+               
+               # Detemine moov start
+               # Get mdat_length_chars from downloaded block
+               my $mdat_length_chars = substr($buffer, $mdat_start, 4);
+               my $mdat_length = bytestring_to_int($mdat_length_chars);
+               logger "DEBUG: mdat_length = ".get_hex($mdat_length_chars)." = $mdat_length\n" if $opt{debug};
+               logger "DEBUG: mdat_length (decimal) = $mdat_length\n" if $opt{debug};
+               # The MOOV box starts one byte after MDAT box ends
+               $moov_start = $mdat_start + $mdat_length;
+       }
+
+       # If we have partial content and wish to stream, resume the download & spawn off STDOUT from existing file start 
+       # Sanity check - we cannot support downloading of partial content if we're streaming also. 
+       if ( $opt{stdout} && (! $opt{nowrite}) && -f $file ) {
+               logger "WARNING: Partially downloaded file exists, streaming will start from the beginning of the programme\n";
+               # Don't do usual streaming code - also force all messages to go to stderr
+               delete $opt{stdout};
+               $opt{stderr} = 1;
+               $childpid = fork();
+               if (! $childpid) {
+                       # Child starts here
+                       logger "INFO: Streaming directly for partially downloaded file $file\n";
+                       if ( ! open( STREAMIN, "< $file" ) ) {
+                               logger "INFO: Cannot Read partially downloaded file to stream\n";
+                               exit 4;
+                       }
+                       my $outbuf;
+                       # Write out until we run out of bytes
+                       my $bytes_read = 65536;
+                       while ( $bytes_read == 65536 ) {
+                               $bytes_read = read(STREAMIN, $outbuf, 65536 );
+                               #logger "INFO: Read $bytes_read bytes\n";
+                               print STDOUT $outbuf;
+                       }
+                       close STREAMIN;
+                       logger "INFO: Stream thread has completed\n";
+                       exit 0;
+               }
+       }
+
+       # Open file if required
+       my $fh = open_file_append($file);
+
+       # If the partial file already exists, then resume from the correct mdat/download offset
+       my $restart_offset = 0;
+       my $moovdata;
+       my $moov_length = 0;
+
+       if ($rearrange) {
+               # if cookie fails then trigger a retry after deleting cookiejar
+               # Determine orginal moov atom length so we can work out if the partially downloaded file has the moov atom in it already
+               $moov_length = bytestring_to_int( download_block( undef, $url_2, $ua, $moov_start, $moov_start+3 ) );
+               logger "INFO: original moov atom length = $moov_length                          \n" if $opt{verbose};
+               # Sanity check this moov length - chances are that were being served up a duff file if this is > 10% of the file size or < 64k
+               if ( $moov_length > (${moov_start}/9.0) || $moov_length < 65536 ) {
+                       logger "WARNING: Bad file download, deleting cookie                 \n";
+                       $ua->cookie_jar( HTTP::Cookies->new( file => $cookiejar, autosave => 0, ignore_discard => 0 ) );
+                       unlink $cookiejar;
+                       unlink $file;
+                       return 'retry';
+               }
+
+               # we still need an accurate moovlength for the already downloaded moov atom for resume restart_offset.....
+               # If we have no existing file, a file which doesn't yet even have the moov atom, or using stdout (or no-write option)
+               # (allow extra 1k on moov_length for metadata when testing)
+               if ( $opt{stdout} || $opt{nowrite} || stat($file)->size < ($moov_length+$mdat_start+1024) ) {
+                       # get moov chunk into memory
+                       $moovdata = download_block( undef, $url_2, $ua, $moov_start, (${download_len}-1) );
+
+                       # Create new udta atom with child atoms for metadata
+                       my $udta_new = create_atom('udta',
+                               create_atom( chr(0xa9).'nam', $prog{$pid}{name}.' - '.$prog{$pid}{episode}, 'string' ).
+                               create_atom( chr(0xa9).'alb', $prog{$pid}{name}, 'string' ).
+                               create_atom( chr(0xa9).'trk', $prog{$pid}{episode}, 'string' ).
+                               create_atom( chr(0xa9).'aut', $prog{$pid}{channel}, 'string' ).
+                               create_atom( chr(0xa9).'ART', $prog{$pid}{channel}, 'string' ).
+                               create_atom( chr(0xa9).'des', $prog{$pid}{desc}, 'string' ).
+                               create_atom( chr(0xa9).'cmt', 'Downloaded with get_iplayer', 'string' ).
+                               create_atom( chr(0xa9).'req', 'QuickTime 6.0 or greater', 'string' ).
+                               create_atom( chr(0xa9).'day', (localtime())[5] + 1900, 'string' )
+                       );
+                       # Insert new udta atom over the old one and get the new $moov_length (and update moov atom size field)
+                       replace_moov_udta_atom ( $udta_new, $moovdata );
+
+                       # Process the moov data so that we can relocate it (change the chunk offsets that are absolute)
+                       # Also update moov+_length to be accurate after metadata is added etc
+                       $moov_length = relocate_moov_chunk_offsets( $moovdata );
+                       logger "INFO: New moov atom length = $moov_length                          \n" if $opt{verbose};
+                       # write moov atom to file next (yes - were rearranging the file - header+moov+mdat - not header+mdat+moov)
+                       logger "INFO: Appending ftype+wide+moov atoms to $file\n" if $opt{verbose};
+                       # Write header atoms (ftyp, wide)
+                       print $fh $header if ! $opt{nowrite};
+                       print STDOUT $header if $opt{stdout};
+                       # Write moov atom
+                       print $fh $moovdata if ! $opt{nowrite};
+                       print STDOUT $moovdata if $opt{stdout};
+                       # If were not resuming we want to only start the download chunk loop from mdat_start 
+                       $restart_offset = $mdat_start;
+               }
+
+               # Get accurate moov_length from file (unless stdout or nowrite options are specified)
+               # Assume header+moov+mdat atom layout
+               if ( (! $opt{stdout}) && (! $opt{nowrite}) && stat($file)->size > ($moov_length+$mdat_start) ) {
+                               logger "INFO: Getting moov atom length from partially downloaded file $file\n";
+                               if ( ! open( MOOVDATA, "< $file" ) ) {
+                                       logger "ERROR: Cannot Read partially downloaded file\n";
+                                       exit 4;
+                               }
+                               my $data;
+                               seek(MOOVDATA, $mdat_start, 0);
+                               if ( read(MOOVDATA, $data, 4, 0) != 4 ) {
+                                       logger "ERROR: Cannot Read moov atom length from partially downloaded file\n";
+                                       exit 4;
+                               }
+                               close MOOVDATA;
+                               # Get moov atom size from file
+                               $moov_length = bytestring_to_int( substr($data, 0, 4) );
+                               logger "INFO: moov atom length (from partially downloaded file) = $moov_length                          \n" if $opt{verbose};
+               }
+       }
+
+       # If we have a too-small-sized file (greater than moov_length+mdat_start) and not stdout and not no-write then this is a partial download
+       if (-f $file && (! $opt{stdout}) && (! $opt{nowrite}) && stat($file)->size > ($moov_length+$mdat_start) ) {
+               # Calculate new start offset (considering that we've put moov first in file)
+               $restart_offset = stat($file)->size - $moov_length;
+               logger "INFO: Resuming download from $restart_offset                        \n";
+       }
+
+       # Create symlink if required
+       if ( $opt{symlink} ) {
+               # remove old symlink
+               unlink $file_symlink if -l $file_symlink;
+               symlink $file, $file_symlink;
+               logger "INFO: Created symlink from '$file_symlink' -> '$file'\n" if $opt{verbose};
+       }
+
+       # Start marker
+       my $start_time = time();
+
+       # Download mdat in blocks
+       my $chunk_size = $iphone_block_size;
+       for ( my $s = $restart_offset; $s < ${moov_start}-1; $s+= $chunk_size ) {
+               # get mdat chunk into file
+               my $retcode;
+               my $e;
+               # Get block end offset
+               if ( ($s + $chunk_size - 1) > (${moov_start}-1) ) {
+                       $e = $moov_start - 1;
+               } else {
+                       $e = $s + $chunk_size - 1;
+               }
+               # Get block from URL and append to $file
+               if ( download_block($file, $url_2, $ua, $s, $e, $download_len, $fh ) ) {
+                       logger "ERROR: Could not download block $s - $e from $file\n\n";
+                       return 9;
+               }
+       }
+
+       # end marker
+       my $end_time = time();
+
+       # Calculate average speed, duration and total bytes downloaded
+       logger sprintf("INFO: Downloaded %.2fMB in %s at %5.0fkbps to %s\n", 
+               ($moov_start - 1 - $restart_offset) / (1024.0 * 1024.0),
+               sprintf("%02d:%02d:%02d", ( gmtime($end_time - $start_time))[2,1,0] ), 
+               ( $moov_start - 1 - $restart_offset ) / ($end_time - $start_time) / 1024.0 * 8.0, 
+               $file_done );
+
+       # Moving file into place as complete (if not stdout)
+       move($file, $file_done) if ! $opt{stdout};
+       return 0;
+}
+
+
+
+# Actually do the RTMP stream downloading
+sub download_rtmp_stream {
+       my ( $ua, $url_2, $pid, $application, $tcurl, $authstring, $swfurl, $file, $file_done, $file_symlink ) = @_;
+       my $file_tmp;
+       my $cmd;
+       
+       if ( $opt{raw} ) {
+               $file_tmp = $file;
+       } else {
+               $file_tmp = $file.'.flv'
+       }
+
+       logger "INFO: RTMP_URL: $url_2, tcUrl: $tcurl, application: $application, authString: $authstring, swfUrl: $swfurl, file: $file, file_done: $file_done\n" if $opt{verbose};
+
+       # Create symlink if required
+       if ( $opt{symlink} ) {
+               # remove old symlink
+               unlink $file_symlink if -l $file_symlink;
+               symlink $file_tmp, $file_symlink;
+               logger "INFO: Created symlink from '$file_symlink' -> '$file_tmp'\n" if $opt{verbose};
+       }
+       $cmd = "$rtmpdump --resume --rtmp \"$url_2\" --auth \"$authstring\" --swfUrl \"$swfurl\" --tcUrl \"$tcurl\" --app \"$application\" -o \"$file_tmp\" ";#>> debug.txt";
+       logger "\n\nINFO: Command: $cmd\n" if $opt{verbose};
+       if ( system($cmd) ) {
+               logger "\nWARNING: Failed to download file $file via RTMP\n";
+               return 1;
+       }
+       
+       # Retain raw flv format if required
+       if ( $opt{raw} ) {
+               move($file_tmp, $file_done) if ! $opt{stdout};
+               return 0;
+
+       # Convert flv to mp3 for flash audio
+       } elsif ( $opt{acodec} eq 'flashaudio' && $prog{$pid}{ext} eq 'mp3' ) {
+               # We could do id3 tagging here but id3v2 does this later anyway
+               $cmd = "$ffmpeg -i \"$file_tmp\" -vn -acodec copy -y \"$file\" >&2";
+
+       # Convert video flv to mp4 or avi if required
+       } else {
+               $cmd = "$ffmpeg $ffmpeg_opts -i \"$file_tmp\" -vcodec copy -acodec copy -f $prog{$pid}{ext} -y \"$file\" >&2";
+       }
+
+       logger "\n\nINFO: Command: $cmd\n\n" if $opt{verbose};
+       # Run flv conversion and delete source file on success
+       if ( ! system($cmd) ) {
+               unlink( $file_tmp );
+       } else {
+               logger "ERROR: flv conversion failed - retaining file '$file_tmp'\n";
+               return 2;
+       }
+       # Moving file into place as complete (if not stdout)
+       move($file, $file_done) if ! $opt{stdout};
+       
+       return 0;
+}
+
+
+
+# Actually do the MMS video stream downloading
+sub download_mms_video_stream {
+       my ( $ua, $urls, $file, $file_done, $pid ) = @_;
+       my $file_tmp;
+       my $cmd;
+       my $null;
+       my @url_list = split /\|/, $urls;
+       my @file_tmp_list;
+       my %threadpid;
+               
+       logger "INFO: MMS_URLs: ".(join ', ', @file_tmp_list).", file: $file, file_done: $file_done\n" if $opt{verbose};
+       if ( $opt{raw} ) {
+               $prog{$pid}{ext} = 'asf';
+       } else {
+               $prog{$pid}{ext} = 'mp4';
+       }
+
+
+       # Start marker
+       my $start_time = time();
+       # Download each mms url (multi-threaded to download in parallel)
+       my $file_part_prefix = "$prog{$pid}{dir}/$prog{$pid}{fileprefix}_part";
+       for ( my $count = 0; $count <= $#url_list; $count++ ) {
+
+               # Create temp download filename
+               $file_tmp = $file_part_prefix.($count+1).".asf";
+               $file_tmp_list[$count] = $file_tmp;
+               $null = " 2>/dev/null " if (! $opt{verbose}) && (! $opt{debug});
+               $cmd = "$mplayer -dumpstream \"$url_list[$count]\" -dumpfile \"$file_tmp\" $null >&2 </dev/null";
+               logger "\n\nINFO: Command: $cmd\n" if $opt{verbose};
+
+               my $childpid = fork();
+               if (! $childpid) {
+                       # Child starts here
+                       logger "INFO: Downloading file $file_tmp\n";
+                       if ( system($cmd) ) {
+                               logger "\nWARNING: Failed to download file $file_tmp via MMS\n";
+                               exit 1;
+                       }
+                       logger "INFO: Download thread has completed for file $file_tmp\n";
+                       exit 0;
+               }
+               # Create a hash of process_id => 'count'
+               $threadpid{$childpid} = $count;
+       }       
+       # Wait for all threads to complete
+       $| = 1;
+       # Autoreap zombies
+       $SIG{CHLD}='IGNORE';
+       my $done = 0;
+       while (keys %threadpid) {
+               my @sizes;
+               my $format = "Threads: ";
+               sleep 1;
+               #logger "DEBUG: ProcessIDs: ".(join ',', keys %threadpid)."\n";
+               for my $procid (sort keys %threadpid) {
+                       my $size = 0;
+                       # Is this child still alive?
+                       if ( kill 0 => $procid ) {
+                               logger "DEBUG Thread $threadpid{$procid} still alive ($file_tmp_list[$threadpid{$procid}])\n" if $opt{debug};
+                               # Build the status string
+                               $format .= "%d) %.3fMB   ";
+                               $size = stat($file_tmp_list[$threadpid{$procid}])->size if -f $file_tmp_list[$threadpid{$procid}];
+                               push @sizes, $threadpid{$procid}+1, $size/(1024.0*1024.0);
+                       } else {
+                               $size = stat($file_tmp_list[$threadpid{$procid}])->size if -f $file_tmp_list[$threadpid{$procid}];
+                               # end marker
+                               my $end_time = time();
+                               # Calculate average speed, duration and total bytes downloaded
+                               logger sprintf("INFO: Thread #%d Downloaded %.2fMB in %s at %5.0fkbps to %s\n", 
+                                       ($threadpid{$procid}+1),
+                                       $size / (1024.0 * 1024.0),
+                                       sprintf("%02d:%02d:%02d", ( gmtime($end_time - $start_time))[2,1,0] ), 
+                                       $size / ($end_time - $start_time) / 1024.0 * 8.0, 
+                                       $file_tmp_list[$threadpid{$procid}] );
+
+                               #logger "INFO: Download Thread #".($threadpid{$procid}+1)." has finished - downloaded ${size}MB\n";
+                               # Remove from thread test list
+                               delete $threadpid{$procid};
+                       }
+               }
+               $format .= " downloaded            \r";
+               logger sprintf $format, @sizes;
+       }
+       logger "INFO: All download threads completed\n";        
+       # Unset autoreap
+       delete $SIG{CHLD};
+       # Retain raw format if required
+       if ( $opt{raw} ) {
+               return 0;
+       }
+
+#      # Convert video asf to mp4 if required - need to find a suitable converter...
+#      } else {
+#              # Create part of cmd that specifies each partial file
+#              my $filestring;
+#              $filestring .= " -i \"$_\" " for (@file_tmp_list);
+#              $cmd = "$ffmpeg $ffmpeg_opts $filestring -vcodec copy -acodec copy -f $prog{$pid}{ext} -y \"$file\" >&2";
+#      }
+#
+#      logger "\n\nINFO: Command: $cmd\n\n" if $opt{verbose};
+#      # Run asf conversion and delete source file on success
+#      if ( ! system($cmd) ) {
+#              unlink( @file_tmp_list );
+#      } else {
+#              logger "ERROR: asf conversion failed - retaining files ".(join ', ', @file_tmp_list)."\n";
+#              return 2;
+#      }
+#      # Moving file into place as complete (if not stdout)
+#      move($file, $file_done) if ! $opt{stdout};
+       
+       return 0;
+}
+
+
+
+# Actually do the N95 h.264 downloading
+sub download_h264_low_stream {
+       my ( $ua, $url_2, $file, $file_done ) = @_;
+
+       # Change filename extension
+       $file =~ s/mov$/mpg/gi;
+       $file_done =~ s/mov$/mpg/gi;
+
+       logger "INFO: Stage 3 URL = $url_2\n" if $opt{verbose};
+       if ( ! $opt{stdout} ) {
+               logger "INFO: Downloading Low Quality H.264 stream\n";
+               my $cmd = "$vlc $vlc_opts --sout file/ts:${file} $url_2 1>&2";
+               if ( system($cmd) ) {
+                       return 2;
+               }
+
+       # to STDOUT
+       } else {
+               logger "INFO: Streaming Low Quality H.264 stream to stdout\n";
+               my $cmd = "$vlc $vlc_opts --sout file/ts:- $url_2 1>&2";
+               if ( system($cmd) ) {
+                       return 2;
+               }       
+       }
+       logger "INFO: Downloaded $file_done\n";
+       # Moving file into place as complete (if not stdout)
+       move($file, $file_done) if ! $opt{stdout};
+       return 0;
+}
+
+
+
+# Actually do the rtsp downloading
+sub download_rtsp_stream {
+       my ( $ua, $url, $file, $file_done, $file_symlink, $pid ) = @_;
+       my $childpid;
+
+       # Create named pipe
+       if ( $^O !~ /^MSWin32$/ ) {
+               mkfifo($namedpipe, 0700) if (! $opt{wav}) && (! $opt{raw});
+       } else {
+               logger "WARNING: fifos/named pipes are not supported\n" if $opt{verbose};
+       }
+       
+       logger "INFO: Stage 3 URL = $url\n" if $opt{verbose};
+
+       # Create ID3 tagging options for lame (escape " for shell)
+       my ( $id3_name, $id3_episode, $id3_desc, $id3_channel ) = ( $prog{$pid}{name}, $prog{$pid}{episode}, $prog{$pid}{desc}, $prog{$pid}{channel} );
+       $id3_name =~ s|"|\"|g for ($id3_name, $id3_episode, $id3_desc, $id3_channel);
+       $lame_opts .= " --ignore-tag-errors --ty ".( (localtime())[5] + 1900 )." --tl \"$id3_name\" --tt \"$id3_episode\" --ta \"$id3_channel\" --tc \"$id3_desc\" ";
+
+       # Use post-download transcoding using lame if namedpipes are not supported (i.e. ActivePerl/Windows)
+       # (Fallback if no namedpipe support and raw/wav not specified)
+       if ( (! -p $namedpipe) && ! ( $opt{raw} || $opt{wav} ) ) {
+               my $cmd;
+               # Remove filename extension
+               $file =~ s/\.mp3$//gi;
+               # Remove named pipe
+               unlink $namedpipe;
+               logger "INFO: Downloading wav format (followed by transcoding)\n";
+               $cmd = "$mplayer $mplayer_opts -cache 128 -bandwidth $bandwidth -vc null -vo null -ao pcm:waveheader:fast:file=\"${file}.wav\" \"$url\" 1>&2";
+               if ( system($cmd) ) {
+                       return 2;
+               }
+               # Transcode
+               logger "INFO: Transcoding ${file}.wav\n";
+               $cmd = "$lame $lame_opts \"${file}.wav\" \"${file}.mp3\" 1>&2";
+               logger "DEGUG: Running $cmd\n" if $opt{debug};
+               if ( system($cmd) ) {
+                       return 2;
+               }
+               unlink "${file}.wav";
+               move "${file}.mp3", $file_done;
+               $prog{$pid}{ext} = 'mp3';
+
+       # Fork a child to do transcoding on the fly using a named pipe written to by mplayer
+       # else do direct mplayer write to wav file if:
+       #  1) we don't have a named pipe available (e.g. in activeperl)
+       #  2) --wav was specified to write file only
+       } elsif ( $opt{wav} && ! $opt{stdout} ) {
+               logger "INFO: Writing wav format\n";
+               # Start the mplayer process and write to wav file
+               my $cmd = "$mplayer $mplayer_opts -cache 128 -bandwidth $bandwidth -vc null -vo null -ao pcm:waveheader:fast:file=\"$file\" \"$url\" 1>&2";
+               logger "DEGUG: Running $cmd\n" if $opt{debug};
+               if ( system($cmd) ) {
+                       return 2;
+               }
+               # Move file to done state
+               move $file, $file_done if ! $opt{nowrite};
+
+       # No transcoding if --raw was specified
+       } elsif ( $opt{raw} && ! $opt{stdout} ) {
+               # Write out to .ra ext instead (used on fallback if no fifo support)
+               logger "INFO: Writing raw realaudio stream\n";
+               # Start the mplayer process and write to raw file
+               my $cmd = "$mplayer $mplayer_opts -cache 128 -bandwidth $bandwidth -dumpstream -dumpfile \"$file\" \"$url\" 1>&2";
+               logger "DEGUG: Running $cmd\n" if $opt{debug};
+               if ( system($cmd) ) {
+                       return 2;
+               }
+               # Move file to done state
+               move $file, $file_done if ! $opt{nowrite};
+
+       # Use transcoding via named pipes
+       } else {
+               $childpid = fork();
+               if (! $childpid) {
+                       # Child starts here
+                       $| = 1;
+                       logger "INFO: Transcoding $file\n";
+
+                       # Stream mp3 to file and stdout simultaneously
+                       if ( $opt{stdout} && ! $opt{nowrite} ) {
+                               if ( $opt{wav} || $opt{raw} ) {
+                                       # Race condition - closes named pipe immediately unless we wait
+                                       sleep 5;
+                                       tee($namedpipe, $file);
+                                       #system( "cat $namedpipe 2>/dev/null| $tee $file");
+                               } else {
+                                       my $cmd = "$lame $lame_opts $namedpipe - 2>/dev/null| $tee \"$file\"";
+                                       logger "DEGUG: Running $cmd\n" if $opt{debug};
+                                       system($cmd);
+                               }
+
+                       # Stream mp3 stdout only
+                       } elsif ( $opt{stdout} && $opt{nowrite} ) {
+                               if ( $opt{wav} || $opt{raw} ) {
+                                       sleep 5;
+                                       tee($namedpipe);
+                                       #system( "cat $namedpipe 2>/dev/null");
+                               } else {
+                                       my $cmd = "$lame $lame_opts $namedpipe - 2>/dev/null";
+                                       logger "DEGUG: Running $cmd\n" if $opt{debug};
+                                       system( "$lame $lame_opts $namedpipe - 2>/dev/null");
+                               }
+
+                       # Stream mp3 to file directly
+                       } elsif ( ! $opt{stdout} ) {
+                               my $cmd = "$lame $lame_opts $namedpipe \"$file\" >/dev/null 2>/dev/null";
+                               logger "DEGUG: Running $cmd\n" if $opt{debug};
+                               system($cmd);
+                       }
+                       # Remove named pipe
+                       unlink $namedpipe;
+
+                       # Move file to done state
+                       move $file, $file_done if ! $opt{nowrite};
+                       logger "INFO: Transcoding thread has completed\n";
+                       exit 0;
+               }
+               # Start the mplayer process and write to named pipe
+               # Raw mode
+               if ( $opt{raw} ) {
+                       my $cmd = "$mplayer $mplayer_opts -cache 32 -bandwidth $bandwidth -dumpstream -dumpfile $namedpipe \"$url\" 1>&2";
+                       logger "DEGUG: Running $cmd\n" if $opt{debug};
+                       if ( system($cmd) ) {
+                               # If we fail then kill off child processes
+                               kill 9, $childpid;
+                               return 2;
+                       }
+               # WAV / mp3 mode
+               } else {
+                       my $cmd = "$mplayer $mplayer_opts -cache 128 -bandwidth $bandwidth -vc null -vo null -ao pcm:waveheader:fast:file=$namedpipe \"$url\" 1>&2";
+                       if ( system($cmd) ) {
+                               # If we fail then kill off child processes
+                               kill 9, $childpid;
+                               return 2;
+                       }
+               }
+               # Wait for child processes to prevent zombies
+               wait;
+       }
+       logger "INFO: Downloaded $file_done\n";
+
+       # Create symlink if required
+       if ( $opt{symlink} ) {
+               # remove old symlink
+               unlink $file_symlink if -l $file_symlink;
+               symlink $file_done, $file_symlink;
+               logger "INFO: Created symlink from '$file_symlink' -> '$file_done'\n" if $opt{verbose};
+       }
+
+       return 0;
+}
+
+
+
+# Actually do the podcast downloading
+sub download_podcast_stream {
+       my ( $ua, $url_2, $file, $file_done, $file_symlink ) = @_;
+       my $start_time = time();
+
+       # Set user agent
+       $ua->agent( $user_agent{get_iplayer} );
+
+       logger "INFO: Stage 3 URL = $url_2\n" if $opt{verbose};
+
+       # Resume partial download?
+       my $start = 0;
+       if ( -f $file ) {
+               $start = stat($file)->size;
+               logger "INFO: Resuming download from $start\n";
+       }
+
+       my $fh = open_file_append($file);
+
+       if ( download_block($file, $url_2, $ua, $start, undef, undef, $fh) != 0 ) {
+               logger "ERROR: Download failed\n";
+               return 22;
+       } else {
+               # end marker
+               my $end_time = time();
+               # Final file size
+               my $size = stat($file)->size;
+               # Calculate average speed, duration and total bytes downloaded
+               logger sprintf("INFO: Downloaded %.2fMB in %s at %5.0fkbps to %s\n", 
+                       ($size - $start) / (1024.0 * 1024.0),
+                       sprintf("%02d:%02d:%02d", ( gmtime($end_time - $start_time))[2,1,0] ), 
+                       ( $size - $start ) / ($end_time - $start_time) / 1024.0 * 8.0, 
+                       $file_done );
+               move $file, $file_done;
+               # re-symlink file
+               if ( $opt{symlink} ) {
+                       # remove old symlink
+                       unlink $file_symlink if -l $file_symlink;
+                       symlink $file_done, $file_symlink;
+                       logger "INFO: Created symlink from '$file_symlink' -> '$file_done'\n" if $opt{verbose};
+               }
+       }
+       return 0;
+}
+
+
+
+# Get streaming iphone URL
+sub get_iphone_stream_download_url {
+               my $ua = shift;
+               my $pid = shift;
+
+               # Create url with appended 6 digit random number
+               my $url_1 = ${iphone_download_prefix}.'/'.${pid}.'?'.(sprintf "%06.0f", 1000000*rand(0)).'%20';
+               logger "INFO: media stream download URL = $url_1\n" if $opt{verbose};
+               
+               # Stage 2: e.g. "Location: http://download.iplayer.bbc.co.uk/iplayer_streaming_http_mp4/121285241910131406.mp4?token=iVXexp1yQt4jalB2Hkl%2BMqI25nz2WKiSsqD7LzRmowrwXGe%2Bq94k8KPsm7pI8kDkLslodvHySUyU%0ApM76%2BxEGtoQTF20ZdFjuqo1%2B3b7Qmb2StOGniozptrHEVQl%2FYebFKVNINg%3D%3D%0A"
+               logger "\rGetting iplayer download URL         " if ! $opt{verbose};
+               my $h = new HTTP::Headers(
+                       'User-Agent'    => $user_agent{coremedia},
+                       'Accept'        => '*/*',
+                       'Range'         => 'bytes=0-1',
+               );
+               my $req = HTTP::Request->new ('GET', $url_1, $h);
+               # send request
+               my $res = $ua->request($req);
+               # Get resulting Location header (i.e. redirect URL)
+               my $url_2 = $res->header("location");
+               if ( ! $res->is_redirect ) {
+                       logger "ERROR: Failed to get redirect from iplayer site\n\n";
+                       return '';
+               }
+               # Extract redirection Location URL
+               $url_2 =~ s/^Location: (.*)$/$1/g;
+               # If we get a Redirection containing statuscode=404 then this prog is not yet ready
+               if ( $url_2 =~ /statuscode=404/ ) {
+                       logger "\rERROR: Programme is not yet ready for download\n";
+                       ## Now tell user what versions/streams are available:
+                       #my %streams = get_media_stream_data( $pid, $pid, 'all' );
+                       #logger "INFO: Other Streams Available: ".join( ',', keys %streams )."\n";
+                       return '';
+               }
+
+               return $url_2;
+}
+
+
+
+# Get streaming audio URL (Real => rtsp)
+#<media kind="audio"
+#        type="audio/real"
+#        encoding="real"  >
+#        <connection
+#                priority="10"
+#                kind="sis"
+#                server="http://www.bbc.co.uk"
+#                identifier="/radio/aod/playlists/gs/5d/c0/0b/0900_bbc_radio_two"
+#                href="http://www.bbc.co.uk/radio/aod/playlists/gs/5d/c0/0b/0900_bbc_radio_two.ram"
+#        />
+#</media>
+# OR
+#<media kind=""
+#        type="audio/real"
+#        encoding="real"  >
+#        <connection
+#                priority="10"
+#                kind="edgesuite"
+#                server="http://http-ws.bbc.co.uk.edgesuite.net"
+#                identifier="/generatecssram.esi?file=/worldservice/css/nb/410591221152760.ra"
+#                href="http://http-ws.bbc.co.uk.edgesuite.net/generatecssram.esi?file=/worldservice/css/nb/410591221152760.ra"
+#        />
+#</media>
+#
+sub get_audio_stream_download_url {
+               my $ua = shift;
+               my $url_1 = shift;
+               my $url_2;
+               
+               logger "\rGetting iplayer download URL         " if ! $opt{verbose};
+               my $h = new HTTP::Headers(
+                       'User-Agent'    => $user_agent{coremedia},
+                       'Accept'        => '*/*',
+                       'Range'         => 'bytes=0-',
+               );
+               my $req = HTTP::Request->new ('GET', $url_1, $h);
+               # send request
+               my $res = $ua->request($req);
+               # Get resulting content 
+               my $content = $res->content;
+               # Flatten
+               $content =~ s/\n/ /g;
+               if ( ! $res->is_success ) {
+                       logger "ERROR: Failed to get audio url from iplayer site\n\n";
+                       return '';
+               }
+               # If we get a Redirection containing statuscode=404 then this prog is not yet ready
+               if ( $content =~ /statuscode=404/ ) {
+                       logger "\rERROR: Programme is not yet ready for download\n";
+                       return '';
+               }
+               # extract ram URL
+               $url_2 = $2 if $content =~ m{<media kind="(|audio)"\s*type="audio/real".*href="(.+?)"\s*};
+
+               # If we cannot see 'encoding="real"...' then we don't have real audio transcoded format then skip
+               if ( ! $url_2 ) {
+                       logger "\rERROR: Programme is not yet ready for download in RealAudio format\n";
+                       return '';
+               }
+
+               return $url_2;
+}
+
+
+
+# Generate the download filename prefix given a pid and optional format such as '<longname> - <episode> <pid> <version>'
+sub generate_download_filename_prefix {
+       my ( $pid, $dir, $file ) = ( @_ );
+
+       # If we dont have longname defined just set it to name
+       $prog{$pid}{longname} = $prog{$pid}{name} if ! $prog{$pid}{longname};
+
+       # substitute fields and sanitize $file
+       $file = substitute_fields( $pid, $file );
+
+       # Spaces
+       $file =~ s/\s+/_/g if ! $opt{whitespace};
+
+       # Don't create subdir if we are only testing downloads
+       # Create a subdir for programme sorting option
+       if ( $opt{subdir} && ! $opt{test} ) {
+               my $subdir = substitute_fields( $pid, '<longname>' );
+               $file = "${subdir}/${file}";
+               # Create dir if it does not exist
+               mkpath("${dir}/${subdir}") if ! -d "${dir}/${subdir}";
+       }
+
+       return $file;
+}
+
+
+
+# Usage: moov_length = relocate_moov_chunk_offsets(<binary string>)
+sub relocate_moov_chunk_offsets {
+       my $moovdata = $_[0];
+       # Change all the chunk offsets in moov->stco atoms and add moov_length to them all
+       # get moov atom length
+       my $moov_length = bytestring_to_int( substr($moovdata, 0, 4) );
+       # Use index() to search for a string within a string
+       my $i = -1;
+       while (($i = index($moovdata, 'stco', $i)) > -1) {
+
+               # determine length of atom (4 bytes preceding stco)
+               my $stco_len = bytestring_to_int( substr($moovdata, $i-4, 4) );
+               logger "INFO: Found stco atom at moov atom offset: $i length $stco_len\n" if $opt{verbose};
+
+               # loop through all chunk offsets in this atom and add offset (== moov atom length)
+               for (my $j = $i+12; $j < $stco_len+$i-4; $j+=4) {
+                       my $chunk_offset = bytestring_to_int( substr($moovdata, $j, 4) );
+                       #logger "chunk_offset @ $i, $j = '".get_hex( substr($moovdata, $j, 4) )."',     $chunk_offset + $moov_length = ";
+                       $chunk_offset += $moov_length;
+                       # write back bytes into $moovdata
+                       #substr($moovdata, $j+0, 1) = chr( ($chunk_offset >> 24) & 0xFF );
+                       #substr($moovdata, $j+1, 1) = chr( ($chunk_offset >> 16) & 0xFF );
+                       #substr($moovdata, $j+2, 1) = chr( ($chunk_offset >>  8) & 0xFF );
+                       #substr($moovdata, $j+3, 1) = chr( ($chunk_offset >>  0) & 0xFF );
+                       write_msb_value_at_offset( $moovdata, $j, $chunk_offset );
+                       #$chunk_offset = bytestring_to_int( substr($moovdata, $j, 4) );
+                       #logger "$chunk_offset\n";
+               }
+
+               # skip over this whole atom now it is processed
+               $i += $stco_len;
+       }
+       # Write $moovdata back to calling string
+       $_[0] = $moovdata;
+       return $moov_length;
+}
+
+
+
+# Replace the moov->udta atom with a new user-supplied one and update the moov atom size
+# Usage: replace_moov_udta_atom ( $udta_new, $moovdata )
+sub replace_moov_udta_atom {
+       my $udta_new = $_[0];
+       my $moovdata = $_[1];
+
+       # get moov atom length
+       my $moov_length = bytestring_to_int( substr($moovdata, 0, 4) );
+
+       # Find the original udta atom start 
+       # Use index() to search for a string within a string ($i will point at the beginning of the atom)
+       my $i = index($moovdata, 'udta', -1) - 4;
+
+       # determine length of atom (4 bytes preceding the name)
+       my $udta_len = bytestring_to_int( substr($moovdata, $i, 4) );
+       logger "INFO: Found udta atom at moov atom offset: $i length $udta_len\n" if $opt{verbose};
+
+       # Save the data before the udta atom
+       my $moovdata_before_udta = substr($moovdata, 0, $i);
+
+       # Save the remainder portion of data after the udta atom for later
+       my $moovdata_after_udta = substr($moovdata, $i, $moovdata - $i + $udta_len);
+
+       # Old udta atom should we need it
+       ### my $udta_old = substr($moovdata, $i, $udta_len);
+
+       # Create new moov atom
+       $moovdata = $moovdata_before_udta.$udta_new.$moovdata_after_udta;
+       
+       # Recalculate the moov size and insert into moovdata
+       write_msb_value_at_offset( $moovdata, 0, length($moovdata) );
+       
+       # Write $moovdata back to calling string
+       $_[1] = $moovdata;
+
+       return 0;
+}
+
+
+
+# Write the msb 4 byte $value starting at $offset into the passed string
+# Usage: write_msb_value($string, $offset, $value)
+sub write_msb_value_at_offset {
+       my $offset = $_[1];
+       my $value = $_[2];
+       substr($_[0], $offset+0, 1) = chr( ($value >> 24) & 0xFF );
+       substr($_[0], $offset+1, 1) = chr( ($value >> 16) & 0xFF );
+       substr($_[0], $offset+2, 1) = chr( ($value >>  8) & 0xFF );
+       substr($_[0], $offset+3, 1) = chr( ($value >>  0) & 0xFF );
+       return 0;
+}
+
+
+
+# Returns a string containing an QT atom
+# Usage: create_atom(<atome name>, <atom data>, ['string'])
+sub create_atom {
+       my ($name, $data, $prog_type) = (@_);
+       if (length($name) != 4) {
+               logger "ERROR: Inavlid QT atom name length '$name'\n";
+               exit 1;
+       }
+       # prepend string length if this is a string type
+       if ( $prog_type eq 'string' ) {
+               my $value = length($data);
+               $data = '1111'.$data;
+               # overwrite '1111' with total atom length in 2-byte MSB + 0x0 0x0
+               substr($data, 0, 1) = chr( ($value >> 8) & 0xFF );
+               substr($data, 1, 1) = chr( ($value >> 0) & 0xFF );
+               substr($data, 2, 1) = chr(0);
+               substr($data, 3, 1) = chr(0);
+       }
+       my $atom = '0000'.$name.$data;
+       # overwrite '0000' with total atom length in MSB
+       write_msb_value_at_offset( $atom, 0, length($name.$data) + 4 );
+       return $atom;
+}
+
+
+
+# Usage download_block($file, $url_2, $ua, $start, $end, $file_len, $fh);
+#  ensure filehandle $fh is open in append mode
+# or, $content = download_block(undef, $url_2, $ua, $start, $end, $file_len);
+# Called in 4 ways:
+# 1) write to real file                        => download_block($file, $url_2, $ua, $start, $end, $file_len, $fh);
+# 2) write to real file + STDOUT       => download_block($file, $url_2, $ua, $start, $end, $file_len, $fh); + $opt{stdout}==true
+# 3) write to STDOUT only              => download_block($file, $url_2, $ua, $start, $end, $file_len, $fh); + $opt{stdout}==true + $opt{nowrite}==false
+# 4) write to memory (and return data)  => download_block(undef, $url_2, $ua, $start, $end, $file_len, undef);
+# 4) write to memory (and return data)  => download_block(undef, $url_2, $ua, $start, $end);
+sub download_block {
+
+       my ($file, $url, $ua, $start, $end, $file_len, $fh) = @_;
+       my $orig_length;
+       my $buffer;
+       my $lastpercent = 0;
+       $now = time();
+
+       # If this is an 'append to file' mode call
+       if ( defined $file && $fh && (!$opt{nowrite}) ) {
+               # Stage 3b: Download File
+               $orig_length = tell $fh;
+               logger "INFO: Appending to $file\n" if $opt{verbose};
+       }
+
+       # Setup request headers
+       my $h = new HTTP::Headers(
+               'User-Agent'    => $user_agent{coremedia},
+               'Accept'        => '*/*',
+               'Range'        => "bytes=${start}-${end}",
+       );
+
+       my $req = HTTP::Request->new ('GET', $url, $h);
+
+       # Set time to use for download rate calculation
+       # Define callback sub that gets called during download request
+       # This sub actually writes to the open output file and reports on progress
+       my $callback = sub {
+               my ($data, $res, undef) = @_;
+               # Don't write the output to the file if there is no content-length header
+               return 0 if ( ! $res->header("Content-Length") );
+               # If we don't know file length in advanced then set to size reported reported from server upon download
+               $file_len = $res->header("Content-Length") + $start if ! defined $file_len;
+               # Write output
+               print $fh $data if ! $opt{nowrite};
+               print STDOUT $data if $opt{stdout};
+               # return if streaming to stdout - no need for progress
+               return if $opt{stdout} && $opt{nowrite};
+               return if $opt{quiet};
+               # current file size
+               my $size = tell $fh;
+               # Download percent
+               my $percent = 100.0 * $size / $file_len;
+               # Don't update display if we haven't dowloaded at least another 0.1%
+               return if ($percent - $lastpercent) < 0.1;
+               $lastpercent = $percent;
+               # download rates in bytes per second and time remaining
+               my $rate_bps;
+               my $rate;
+               my $time;
+               my $timecalled = time();
+               if ($timecalled - $now < 1) {
+                       $rate = '-----kbps';
+                       $time = '--:--:--';
+               } else {
+                       $rate_bps = ($size - $orig_length) / ($timecalled - $now);
+                       $rate = sprintf("%5.0fkbps", (8.0 / 1024.0) * $rate_bps);
+                       $time = sprintf("%02d:%02d:%02d", ( gmtime( ($file_len - $size) / $rate_bps ) )[2,1,0] );
+               }
+               logger sprintf "%8.2fMB / %.2fMB %s %5.1f%%, %s remaining         \r", 
+                       $size / 1024.0 / 1024.0, 
+                       $file_len / 1024.0 / 1024.0,
+                       $rate,
+                       $percent,
+                       $time,
+               ;
+       };
+
+       my $callback_memory = sub {
+               my ($data, $res, undef) = @_;
+               # append output to buffer
+               $buffer .= $data;
+               return if $opt{quiet};
+               # current buffer size
+               my $size = length($buffer);
+               # download rates in bytes per second
+               my $timecalled = time();
+               my $rate_bps;
+               my $rate;
+               my $time;
+               my $percent;
+               # If we can get Content_length then display full progress
+               if ($res->header("Content-Length")) {
+                       $file_len = $res->header("Content-Length") if ! defined $file_len;
+                       # Download percent
+                       $percent = 100.0 * $size / $file_len;
+                       return if ($percent - $lastpercent) < 0.1;
+                       $lastpercent = $percent;
+                       # Block length
+                       $file_len = $res->header("Content-Length");
+                       if ($timecalled - $now < 0.1) {
+                               $rate = '-----kbps';
+                               $time = '--:--:--';
+                       } else {
+                               $rate_bps = $size / ($timecalled - $now);
+                               $rate = sprintf("%5.0fkbps", (8.0 / 1024.0) * $rate_bps );
+                               $time = sprintf("%02d:%02d:%02d", ( gmtime( ($file_len - $size) / $rate_bps ) )[2,1,0] );
+                       }
+                       # time remaining
+                       logger sprintf "%8.2fMB / %.2fMB %s %5.1f%%, %s remaining         \r", 
+                               $size / 1024.0 / 1024.0,
+                               $file_len / 1024.0 / 1024.0,
+                               $rate,
+                               $percent,
+                               $time,
+                       ;
+               # Just used simple for if we cannot determine content length
+               } else {
+                       if ($timecalled - $now < 0.1) {
+                               $rate = '-----kbps';
+                       } else {
+                               $rate = sprintf("%5.0fkbps", (8.0 / 1024.0) * $size / ($timecalled - $now) );
+                       }
+                       logger sprintf "%8.2fMB %s         \r", $size / 1024.0 / 1024.0, $rate;
+               }
+       };
+
+       # send request
+       logger "\nINFO: Downloading range ${start}-${end}\n" if $opt{verbose};
+       logger "\r                              \r";
+       my $res;
+
+       # If $fh undefined then get block to memory (fh always defined for stdout or file d/load)
+       if (defined $fh) {
+               logger "DEBUG: writing stream to stdout, Range: $start - $end of $url\n" if $opt{verbose} && $opt{stdout};
+               logger "DEBUG: writing stream to $file, Range: $start - $end of $url\n" if $opt{verbose} && !$opt{nowrite};
+               $res = $ua->request($req, $callback);
+               if (  (! $res->is_success) || (! $res->header("Content-Length")) ) {
+                       logger "ERROR: Failed to Download block\n\n";
+                       return 5;
+               }
+                logger "INFO: Content-Length = ".$res->header("Content-Length")."                               \n" if $opt{verbose};
+               return 0;
+                  
+       # Memory Block
+       } else {
+               logger "DEBUG: writing stream to memory, Range: $start - $end of $url\n" if $opt{debug};
+               $res = $ua->request($req, $callback_memory);
+               if ( (! $res->is_success) ) {
+                       logger "ERROR: Failed to Download block\n\n";
+                       return '';
+               } else {
+                       return $buffer;
+               }
+       }
+}
+
+
+
+# Converts a string of chars to it's HEX representation
+sub get_hex {
+        my $buf = shift || '';
+        my $ret = '';
+        for (my $i=0; $i<length($buf); $i++) {
+                $ret .= " ".sprintf("%02lx", ord substr($buf, $i, 1) );
+        }
+       logger "DEBUG: HEX string value = $ret\n" if $opt{verbose};
+        return $ret;
+}
+
+
+
+# Converts a string of chars to it's MSB decimal value
+sub bytestring_to_int {
+       # Reverse to LSB order
+        my $buf = reverse shift;
+        my $dec = 0;
+        for (my $i=0; $i<length($buf); $i++) {
+               # Multiply byte value by 256^$i then accumulate
+                $dec += (ord substr($buf, $i, 1)) * 256 ** $i;
+        }
+        #logger "DEBUG: Decimal value = $dec\n" if $opt{verbose};
+        return $dec;
+}
+
+
+
+# version of unix tee
+# Usage tee ($infile, $outfile)
+# If $outfile is undef then just cat file to STDOUT
+sub tee {
+       my ( $infile, $outfile ) = @_;
+       # Open $outfile for writing, $infile for reading
+       if ( $outfile) {
+               if ( ! open( OUT, "> $outfile" ) ) {
+                       logger "ERROR: Could not open $outfile for writing\n";
+                       return 1;
+               } else {
+                       logger "INFO: Opened $outfile for writing\n" if $opt{verbose};
+               }
+       }
+       if ( ! open( IN, "< $infile" ) ) {
+               logger "ERROR: Could not open $infile for reading\n";
+               return 2;
+       } else {
+               logger "INFO: Opened $infile for reading\n" if $opt{verbose};
+       }
+       # Read and redirect IN
+       while ( <IN> ) {
+               print $_;
+               print OUT $_ if $outfile;
+       }
+       # Close output file
+       close OUT if $outfile;
+       close IN;
+       return 0;
+}
+
+
+
+# Usage: $fh = open_file_append($filename);
+sub open_file_append {
+       local *FH;
+       my $file = shift;
+       # Just in case we actually write to the file - make this /dev/null
+       $file = '/dev/null' if $opt{nowrite};
+       if ($file) {
+               if ( ! open(FH, ">> $file") ) {
+                       logger "ERROR: Cannot write or append to $file\n\n";
+                       exit 1;
+               }
+       }
+       # Fix for binary - needed for Windows
+       binmode FH;
+       return *FH;
+}
+
+
+
+# Updates and overwrites this script - makes backup as <this file>.old
+sub update_script {
+       # Get version URL
+       my $script_file = $0;
+       my $ua = LWP::UserAgent->new;
+       $ua->timeout([$lwp_request_timeout]);
+       $ua->proxy( ['http'] => $proxy_url );
+       $ua->agent( $user_agent{update} );
+       $ua->conn_cache(LWP::ConnCache->new());
+       logger "INFO: Current version is $version\n";
+       logger "INFO: Checking for latest version from linuxcentre.net\n";
+       my $res = $ua->request( HTTP::Request->new( GET => $version_url ) );
+       chomp( my $latest_ver = $res->content );
+       if ( $res->is_success ) {
+               # Compare version numbers
+               if ( $latest_ver > $version ) {
+                       logger "INFO: New version $latest_ver available, downloading\n";
+                       # Check if we are writable
+                       if ( ! -w $script_file ) {
+                               logger "ERROR: $script_file is not writable by the current user - Update aborted\n";
+                               exit 1;
+                       }
+                       my $req = HTTP::Request->new ('GET', $update_url);
+                       # Save content into a $script_file
+                       my $res = $ua->request($req, $script_file.'.tmp');
+                       if ( ! -f $script_file.'.tmp' ) {
+                               logger "ERROR: Could not download update to ${script_file}.tmp - Update aborted\n";
+                               exit 1;
+                       }
+                       # If the download was successful then copy over this script and make executable after making a backup of this script
+                       if ( $res->is_success ) {
+                               if ( copy($script_file, $script_file.'.old') ) {
+                                       move($script_file.'.tmp', $script_file);
+                                       chmod 0755, $script_file;
+                                       logger "INFO: Copied new version $latest_ver into place (previous version is now called '${script_file}.old')\n";
+                                       logger "INFO: Please see: http://linuxcentre.net/get_iplayer/CHANGELOG.txt\n";
+                               } else {
+                                       logger "ERROR: Could not create backup file ${script_file}.old - Update aborted\n";
+                                       exit 1;
+                               }
+                       }
+               } else {
+                       logger "INFO: No update is necessary (latest version = $latest_ver)\n";
+               }
+       } else {
+               logger "ERROR: Failed to connect to update site - Update aborted\n";
+               exit 2;
+       }
+       exit 0;
+}
+
+
+
+# Creates the Freevo FXD or MythTV Streams meta data (and pre-downloads graphics - todo)
+sub create_xml {
+       my $xmlfile = shift;
+       if ( ! open(XML, "> $xmlfile") ) {
+               logger "ERROR: Couldn't open xml file $xmlfile for writing\n";
+               return 1;
+       }
+       print XML '<?xml version="1.0" ?>';
+       print XML '<freevo>' if $opt{fxd};
+       print XML "\n<MediaStreams>\n" if $opt{mythtv};
+
+       if ( $opt{xmlnames} ) {
+               # containers sorted by prog names
+               print XML "\t<container title=\"iplayer by Programme Name\">\n" if $opt{fxd};
+               my %program_index;
+               my %program_count;
+               # create hash of programme_name -> index
+               for (@_) {
+                       $program_index{$prog{$index_pid{$_}}{name}} = $_;
+                       $program_count{$prog{$index_pid{$_}}{name}}++;
+               }
+               for my $name ( sort keys %program_index ) {
+                       my @count = grep /^$name$/, keys %program_index;
+                       print XML "\t<container title=\"".encode_entities( $name )." ($program_count{$name})\">\n" if $opt{fxd};
+                       print XML "\t<Streams>\n" if $opt{mythtv};
+                       for (@_) {
+                               my $pid = $index_pid{$_};
+                               # loop through and find matches for each progname
+                               if ( $prog{$index_pid{$_}}{name} =~ /^$name$/ ) {
+                                       my $episode = encode_entities( $prog{$pid}{episode} );
+                                       my $desc = encode_entities( $prog{$pid}{desc} );
+                                       my $title = "${episode} ($prog{$pid}{available})";
+                                       print XML "<movie title=\"${title}\">
+                                               <video><url id=\"p1\">${pid}.mov<playlist/></url></video>
+                                               <info><description>${desc}</description></info>
+                                       </movie>\n" if $opt{fxd};
+                                       print XML "<Stream>
+                                               <Name>\"${title}\"</Name>
+                                               <url>${pid}.mov</url>
+                                               <Subtitle></Subtitle>
+                                               <Synopsis>${desc}</Synopsis>
+                                       </Stream>\n" if $opt{mythtv};
+                               }
+                       }                       
+                       print XML "\t</container>\n" if $opt{fxd};
+                       print XML "\t</Streams>\n" if $opt{mythtv};
+               }
+               print XML "\t</container>\n" if $opt{fxd};
+       }
+
+
+       if ( $opt{xmlchannels} ) {
+               # containers for prog names sorted by channel
+               print XML "\t<container title=\"iplayer by Channel\">\n" if $opt{fxd};
+               my %program_index;
+               my %program_count;
+               my %channels;
+               # create hash of unique channel names and hash of programme_name -> index
+               for (@_) {
+                       $program_index{$prog{$index_pid{$_}}{name}} = $_;
+                       $program_count{$prog{$index_pid{$_}}{name}}++;
+                       $channels{$prog{$index_pid{$_}}{channel}} .= '|'.$prog{$index_pid{$_}}{name}.'|';
+               }
+               for my $channel ( sort keys %channels ) {
+                       print XML "\t<container title=\"".encode_entities( $channel )."\">\n" if $opt{fxd};
+                       print XML "\t<Feed>
+                               \t<Name>".encode_entities( $channel )."</Name>
+                               \t<Provider>BBC</Provider>\n
+                               \t<Streams>\n" if $opt{mythtv};
+                       for my $name ( sort keys %program_index ) {
+                               # Do we have any of this prog $name on this $channel?
+                               if ( $channels{$channel} =~ /\|$name\|/ ) {
+                                       my @count = grep /^$name$/, keys %program_index;
+                                       print XML "\t<container title=\"".encode_entities( $name )." ($program_count{$name})\">\n" if $opt{fxd};
+                                       print XML "\t\t<Stream>\n" if $opt{mythtv};
+                                       for (@_) {
+                                               # loop through and find matches for each progname for this channel
+                                               my $pid = $index_pid{$_};
+                                               if ( $prog{$pid}{channel} =~ /^$channel$/ && $prog{$pid}{name} =~ /^$name$/ ) {
+                                                       my $episode = encode_entities( $prog{$pid}{episode} );
+                                                       my $desc = encode_entities( $prog{$pid}{desc} );
+                                                       my $title = "${episode} ($prog{$pid}{available})";
+                                                       print XML "<movie title=\"${title}\">
+                                                               <video><url id=\"p1\">${pid}.mov<playlist/></url></video>
+                                                               <info><description>${desc}</description></info>
+                                                       </movie>\n" if $opt{fxd};
+                                                       print XML "\t\t<Name>$name</Name>\n\t\t<Url>${pid}.mov</Url>\n\t\t<Subtitle>${episode}</Subtitle>\n\t\t<Synopsis>${desc}</Synopsis>\n" if $opt{mythtv};
+                                               }
+                                       }
+                                       print XML "\t</container>\n" if $opt{fxd};
+                                       print XML "\t</Stream>\n" if $opt{mythtv};
+                               }
+                       }
+                       print XML "\t</container>\n" if $opt{fxd};
+                       print XML "\t</Streams>\n\t</Feed>\n" if $opt{mythtv};
+               }
+               print XML "\t</container>\n" if $opt{fxd};
+       }
+
+
+       if ( $opt{xmlalpha} ) {
+               my %table = (
+                       'A-C' => '[abc]',
+                       'D-F' => '[def]',
+                       'G-I' => '[ghi]',
+                       'J-L' => '[jkl]',
+                       'M-N' => '[mn]',
+                       'O-P' => '[op]',
+                       'Q-R' => '[qt]',
+                       'S-T' => '[st]',
+                       'U-V' => '[uv]',
+                       'W-Z' => '[wxyz]',
+                       '0-9' => '[\d]',
+               );
+               print XML "\t<container title=\"iplayer A-Z\">\n";
+               for my $folder (sort keys %table) {
+                       print XML "\t<container title=\"iplayer $folder\">\n";
+                       for (@_) {
+                               my $pid = $index_pid{$_};
+                               my $name = encode_entities( $prog{$pid}{name} );
+                               my $episode = encode_entities( $prog{$pid}{episode} );
+                               my $desc = encode_entities( $prog{$pid}{desc} );
+                               my $title = "${name} - ${episode} ($prog{$pid}{available})";
+                               my $regex = $table{$folder};
+                               if ( $name =~ /^$regex/i ) {
+                                       print XML "<movie title=\"${title}\">
+                                               <video><url id=\"p1\">${pid}.mov<playlist/></url></video>
+                                               <info><description>${desc}</description></info>
+                                       </movie>\n" if $opt{fxd};
+                                       print XML "<Stream title=\"${title}\">
+                                               <video><url id=\"p1\">${pid}.mov<playlist/></url></video>
+                                               <info><description>${desc}</description></info>
+                                       </Stream>\n" if $opt{mythtv};
+                               }
+                       }
+                       print XML "\t</container>\n";
+               }
+               print XML "\t</container>\n";
+       }
+
+       print XML '</freevo>' if $opt{fxd};
+       print XML '</MediaStreams>' if $opt{mythtv};
+       close XML;
+}
+
+
+
+sub create_html {
+       # Create local web page
+       if ( open(HTML, "> $opt{html}") ) {
+               print HTML '<html><head></head><body><table border=1>';
+               for (@_) {
+                       my $pid = $index_pid{$_};
+                       my $name = encode_entities( $prog{$pid}{name} );
+                       my $episode = encode_entities( $prog{$pid}{episode} );
+                       my $desc = encode_entities( $prog{$pid}{desc} );
+                       my $channel = encode_entities( $prog{$pid}{channel} );
+                       print HTML "<tr>
+                                       <td rowspan=2 width=150><a href=\"${prog_page_prefix}/${pid}.html\"><img height=84 width=150 src=\"$prog{$pid}{thumbnail}\"></a></td>
+                                       <td>$_</td>
+                                       <td><a href=\"${prog_page_prefix}/${pid}.html\">${name}</a></td>
+                                       <td>${episode}</td>
+                                       <td>${channel}</td>
+                               </tr>
+                               <tr>
+                                       <td colspan=5>${desc}</td>
+                               </tr>
+                       \n";
+               }
+               print HTML '</table></body>';
+               close (HTML);
+       } else {
+               logger "Couldn't open html file $opt{html} for writing\n";
+       }
+}
+
+
+
+# Save cmdline-only options to file
+sub save_options_file {
+       my $optfile = shift;
+       unlink $optfile;
+       logger "DEBUG: Saving options to $optfile:\n" if $opt{debug};
+       open (OPT, "> $optfile") || die ("ERROR: Cannot save options to $optfile\n");
+       # Save all opts except for these
+       for (grep !/(help|test|debug|get)/, keys %opt_cmdline) {
+               print OPT "$_ $opt_cmdline{$_}\n"  if defined $opt_cmdline{$_};
+               logger "DEBUG: Setting cmdline option $_ = $opt_cmdline{$_}\n" if $opt{debug} && defined $opt_cmdline{$_};
+       }
+       close OPT;
+       logger "INFO: Command Line Options saved as defult in $optfile\n";
+       exit 0;
+}
+
+
+
+# Load default options from file
+sub read_options_file {
+       my $optfile = shift;
+       return 0 if ! -f $optfile;
+       logger "DEBUG: Parsing options from $optfile:\n" if $opt{debug};
+       open (OPT, "< $optfile") || die ("ERROR: Cannot read options file $optfile\n");
+       while(<OPT>) {
+               /^\s*([\w\-_]+)\s+(.*)\s*$/;
+               chomp( $opt{$1} = $2 );
+               # keep track of which options came from files
+               chomp( $opt_file{$1} = $2 );
+               logger "DEBUG: Setting option $1 = $2\n" if $opt{debug};
+       }
+       close OPT;
+}
+
+
+
+# Display options from files
+sub display_default_options {
+       logger "Default options:\n";
+       for ( grep !/(help|debug|get|^pvr)/, sort keys %opt_file ) {
+               logger "\t$_ = $opt_file{$_}\n" if defined $opt_file{$_};
+       }
+       logger "\n";
+       return 0;
+}
+
+
+
+# Display options currently set
+sub display_current_options {
+       logger "Current options:\n";
+       for ( sort keys %opt ) {
+               logger "\t$_ = $opt{$_}\n" if defined $opt{$_} && $opt{$_};
+       }
+       logger "\n";
+       return 0;
+}
+
+
+
+# Get time ago made available (x days y hours ago) from '2008-06-22T05:01:49Z' and current time
+sub get_available_time_string {
+       my $datestring = shift;
+       # extract $year $mon $mday $hour $min $sec
+       $datestring =~ m{(\d\d\d\d)\-(\d\d)\-(\d\d)T(\d\d):(\d\d):(\d\d)Z};
+       my ($year, $mon, $mday, $hour, $min, $sec) = ($1, $2, $3, $4, $5, $6);
+       # Calculate the seconds difference between epoch_now and epoch_datestring and convert back into array_time
+       my @time = gmtime( time() - timelocal($sec, $min, $hour, $mday, ($mon-1), ($year-1900), undef, undef, 0) );
+       return "$time[7] days $time[2] hours ago";
+}
+
+
+
+# get full episode metadata given pid and ua. Uses two different urls to get data
+sub get_pid_metadata {
+       my $ua = shift;
+       my $pid = shift;
+       my $metadata;
+       my $entry3;
+       my ($name, $episode, $duration, $available, $channel, $expiry, $longdesc, $versions, $guidance, $prog_type, $categories, $player, $thumbnail);
+
+       # This URL works for all prog types:
+       # http://www.bbc.co.uk/iplayer/playlist/${pid}
+
+       # This URL only works for TV progs:
+       # http://www.bbc.co.uk/iplayer/metafiles/episode/${pid}.xml
+
+       # This URL works for tv/radio prog types:
+       # http://www.bbc.co.uk/iplayer/widget/episodedetail/episode/${pid}/template/mobile/service_type/tv/
+
+       # This URL works for tv/radio prog types:
+       # $prog_feed_url = http://feeds.bbc.co.uk/iplayer/episode/$pid
+
+       if ( $prog{$pid}{type} =~ /^(tv|radio)$/i ) {
+               $entry3 = request_url_retry($ua, $prog_feed_url.$pid, 3, '', '');
+               decode_entities($entry3);
+               logger "DEBUG: $prog_feed_url.$pid:\n$entry3\n\n" if $opt{debug};
+               # Flatten
+               $entry3 =~ s|\n| |g;
+
+               # Entry3 format
+               #<?xml version="1.0" encoding="utf-8"?>                                      
+               #<?xml-stylesheet href="http://www.bbc.co.uk/iplayer/style/rss.css" type="text/css"?>
+               #<feed xmlns="http://www.w3.org/2005/Atom" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:media="http://search.yahoo.com/mrss/" xml:lang="en-GB">
+               #  <title>BBC iPlayer - Episode Detail: Edith Bowman: 22/09/2008</title>                                                                          
+               #  <subtitle>Sara Cox sits in for Edith with another Cryptic Randomizer.</subtitle>
+               #  <updated>2008-09-29T10:59:45Z</updated>
+               #  <id>tag:feeds.bbc.co.uk,2008:/iplayer/feed/episode/b00djtfh</id>
+               #  <link rel="related" href="http://www.bbc.co.uk/iplayer" type="text/html" />
+               #  <link rel="self" href="http://feeds.bbc.co.uk/iplayer/episode/b00djtfh" type="application/atom+xml" />
+               #  <author>
+               #    <name>BBC</name>
+               #    <uri>http://www.bbc.co.uk</uri>
+               #  </author>
+               #  <entry>
+               #    <title type="text">Edith Bowman: 22/09/2008</title>
+               #    <id>tag:feeds.bbc.co.uk,2008:PIPS:b00djtfh</id>
+               #    <updated>2008-09-15T01:28:36Z</updated>
+               #    <summary>Sara Cox sits in for Edith with another Cryptic Randomizer.</summary>
+               #    <content type="html">
+               #      &lt;p&gt;
+               #        &lt;a href=&quot;http://www.bbc.co.uk/iplayer/episode/b00djtfh?src=a_syn30&quot;&gt;
+               #          &lt;img src=&quot;http://www.bbc.co.uk/iplayer/images/episode/b00djtfh_150_84.jpg&quot; alt=&quot;Edith Bowman: 22/09/2008&quot; /&gt;
+               #        &lt;/a&gt;
+               #      &lt;/p&gt;
+               #      &lt;p&gt;
+               #        Sara Cox sits in for Edith with movie reviews and great new music, plus another Cryptic Randomizer.
+               #      &lt;/p&gt;
+               #    </content>
+               #    <link rel="alternate" href="http://www.bbc.co.uk/iplayer/episode/b00djtfh?src=a_syn31" type="text/html" title="Edith Bowman: 22/09/2008">
+               #      <media:content medium="audio" duration="10800">
+               #        <media:title>Edith Bowman: 22/09/2008</media:title>
+               #        <media:description>Sara Cox sits in for Edith with movie reviews and great new music, plus another Cryptic Randomizer.</media:description>
+               #        <media:player url="http://www.bbc.co.uk/iplayer/episode/b00djtfh?src=a_syn31" />
+               #        <media:category scheme="urn:bbc:metadata:cs:iPlayerUXCategoriesCS" label="Entertainment">9100099</media:category>
+               #        <media:category scheme="urn:bbc:metadata:cs:iPlayerUXCategoriesCS" label="Music">9100006</media:category>
+               #        <media:category scheme="urn:bbc:metadata:cs:iPlayerUXCategoriesCS" label="Pop &amp; Chart">9200069</media:category>
+               #        <media:credit role="Production Department" scheme="urn:ebu">BBC Radio 1</media:credit>
+               #        <media:credit role="Publishing Company" scheme="urn:ebu">BBC Radio 1</media:credit>
+               #        <media:thumbnail url="http://www.bbc.co.uk/iplayer/images/episode/b00djtfh_86_48.jpg" width="86" height="48" />
+               #        <media:thumbnail url="http://www.bbc.co.uk/iplayer/images/episode/b00djtfh_150_84.jpg" width="150" height="84" />
+               #        <media:thumbnail url="http://www.bbc.co.uk/iplayer/images/episode/b00djtfh_178_100.jpg" width="178" height="100" />
+               #        <media:thumbnail url="http://www.bbc.co.uk/iplayer/images/episode/b00djtfh_512_288.jpg" width="512" height="288" />
+               #        <media:thumbnail url="http://www.bbc.co.uk/iplayer/images/episode/b00djtfh_528_297.jpg" width="528" height="297" />
+               #        <media:thumbnail url="http://www.bbc.co.uk/iplayer/images/episode/b00djtfh_640_360.jpg" width="640" height="360" />
+               #        <dcterms:valid>
+               #          start=2008-09-22T15:44:20Z;
+               #          end=2008-09-29T15:02:00Z;
+               #          scheme=W3C-DTF
+               #        </dcterms:valid>
+               #      </media:content>
+               #    </link>
+               #    <link rel="self" href="http://feeds.bbc.co.uk/iplayer/episode/b00djtfh?format=atom" type="application/atom+xml" title="22/09/2008" />
+               #    <link rel="related" href="http://www.bbc.co.uk/programmes/b006wks4/microsite" type="text/html" title="Edith Bowman" />
+               #    <link rel="parent" href="http://feeds.bbc.co.uk/iplayer/programme_set/b006wks4" type="application/atom+xml" title="Edith Bowman" />
+               #  </entry>
+               #</feed>
+                       
+               $expiry = $1 if $entry3 =~ m{<dcterms:valid>\s*start=.+?;\s*end=(.*?);};
+               $available = $1 if $entry3 =~ m{<dcterms:valid>\s*start=(.+?);\s*end=.*?;};
+               $duration = $1 if $entry3 =~ m{duration=\"(\d+?)\"};
+               $prog_type = $1 if $entry3 =~ m{medium=\"(\w+?)\"};
+               $longdesc = $1 if $entry3 =~ m{<media:description>\s*(.*?)\s*<\/media:description>};
+               $guidance = $1 if $entry3 =~ m{<media:rating scheme="urn:simple">(.+?)<\/media:rating>};
+               $player = $1 if $entry3 =~ m{<media:player\s*url=\"(.*?)\"\s*\/>};
+               $thumbnail = $1 if $entry3 =~ m{<media:thumbnail url="([^"]+?)"\s+width="150"\s+height="84"\s*/>};
+       
+               my @cats;
+               for (split /<media:category scheme=\".+?\"/, $entry3) {
+                       push @cats, $1 if m{\s*label="(.+?)">\d+<\/media:category>};
+               }
+               $categories = join ',', @cats;
+
+               # populate version pid metadata 
+               get_version_pids($ua, $pid);
+
+       # ITV Catch-Up metadata
+       } elsif ( $prog{$pid}{type} eq 'itv' ) {
+               my $prog_metadata_url_itv = 'http://www.itv.com/_app/Dynamic/CatchUpData.ashx?ViewType=5&Filter='; # +<pid>
+               $entry3 = request_url_retry($ua, "${prog_metadata_url_itv}${pid}", 3, '', '');
+               decode_entities($entry3);
+               logger "DEBUG: ${prog_metadata_url_itv}${pid}:\n$entry3\n\n" if $opt{debug};
+               # Flatten
+               $entry3 =~ s|[\r\n]||g;
+
+               #div class="itvCatchUpPlayerPanel" xmlns:ms="urn:schemas-microsoft-com:xslt">
+               #  <div class="cu-sponsor"><a href="http://sam.itv.com/accipiter/adclick/CID=000040d70000000000000000/acc_random=1/SITE=CLICKTRACK/AREAITVCATCHUP.VIDEO=CLICKTRACK..FREEVIEW.SPONSORBUTTON.OCT08/AAMSZ=120X60/pageid=1" title="ITV Player in assocation with Freeview"><img src="/_app/img/catchup/catchup_video_freeview2.jpg" alt="ITV Player is sponsored by Freeview"></a></div>
+               #  <h2>Doctor Zhivago</h2>
+               #  <p>Part 1 of 3. Dramatisation of the epic novel by Boris Pasternak. Growing up in Moscow with his uncle, aunt and cousin Tonya, Yury is captivated by a stunning young girl called ...</p>
+               #  <p class="timings"><span class="date">Mon 29 Dec 2008</span><br /><br /><span>
+               #
+               #        Duration: 1hr 30 min |
+               #                                Expires in
+               #                                <strong>22</strong>
+               #                                                days
+               #                                        </span></p>
+               #  <p><a href="http://www.itv.com/CatchUp/Programmes/default.html?ViewType=1&amp;Filter=2352">3 Episodes Available
+               #        </a><br></br></p>
+               #  <p class="channelLogo"><img src="/_app/img/logos/itv3-black.gif" alt="ITV 4"></p>
+               #  <div id="cu-2-0-VideoID">33105</div>
+               #  <div id="cu-2-0-DentonId">17</div>
+               #  <div id="cu-2-0-ItemMediaUrl">http://www.itv.com//img/480x272/Doctor-Zhivago-c47828f8-a1af-4cd2-b5a2-40c18eb7e63c.jpg</div>
+               #</div><script language="javascript" type="text/javascript" xmlns:ms="urn:schemas-microsoft-com:xslt">
+               #                        SetCatchUpModuleID(0);
+               #                </script>
+               #
+
+               #<div class="itvCatchUpPlayerPanel" xmlns:ms="urn:schemas-microsoft-com:xslt">
+               #  <div class="cu-sponsor"><a href="http://sam.itv.com/accipiter/adclick/CID=000040d70000000000000000/acc_random=1/SITE=CLICKTRACK/AREAITVCATCHUP.VIDEO=CLICKTRACK..FREEVIEW.SPONSORBUTTON.OCT08/AAMSZ=120X60/pageid=1" title="ITV Player in assocation with Freeview"><img src="/_app/img/catchup/catchup_video_freeview2.jpg" alt="ITV Player is sponsored by Freeview"></a></div>
+               #  <h2>Affinity</h2>
+               #  <p>Victorian period drama with a murderous, pyschological twist.</p>
+               #  <p class="timings"><span class="date">Sun 28 Dec 2008</span><br /><br /><span>
+               #
+               #        Duration: 2hr 00 min |
+               #                                Expires in
+               #                                <strong>21</strong>
+               #                                                days
+               #                                        </span></p>
+               #  <p class="channelLogo"><img src="/_app/img/logos/itv1-black.gif" alt="ITV 2"></p>
+               #  <div class="guidance">
+               #    <div><strong>ITV Video Guidance</strong><p>This programme contains strong language and scenes of a sexual nature                                                                                                                          Â </p>
+               #    </div>
+               #  </div>
+               #  <div id="cu-2-0-VideoID">33076</div>
+               #  <div id="cu-2-0-DentonId">11</div>
+               #  <div id="cu-2-0-ItemMediaUrl">http://www.itv.com//img/480x272/Affinity-9624033b-6e05-4784-85f7-114be0559b24.jpg</div>
+               #</div><script language="javascript" type="text/javascript" xmlns:ms="urn:schemas-microsoft-com:xslt">
+               #                        SetCatchUpModuleID(0);
+               #                </script>
+               #
+
+               #$expiry = $1 if $entry3 =~ m{<dcterms:valid>\s*start=.+?;\s*end=(.*?);};
+               $available = $1 if $entry3 =~ m{<p\s+class="timings">\s*<span\s+class="date">(.+?)<\/span>};
+               $duration = $1 if $entry3 =~ m{Duration:\s*(.+?)\s+\|};
+               #$prog_type = $1 if $entry3 =~ m{medium=\"(\w+?)\"};
+               $longdesc = $1 if $entry3 =~ m{<p>(.+?)<\/p>}i;
+               $guidance = $1 if $entry3 =~ m{ITV Video Guidance<\/strong><p>\s*(.+?)[\W\s]*<\/p>};
+               #$player = $1 if $entry3 =~ m{<media:player\s*url=\"(.*?)\"\s*\/>};
+               $thumbnail = $1 if $entry3 =~ m{<div id="cu-2-0-ItemMediaUrl">(.+?)</div>};
+               $name = $1 if $entry3 =~ m{<h2>(.+?)</h2>};
+       }
+
+       # Fill in from cache if not got from metadata
+       my %metadata;
+       $metadata{pid}          = $pid;
+       $metadata{index}        = $prog{$pid}{index};
+       $metadata{name}         = $name || $prog{$pid}{name};
+       $metadata{episode}      = $episode || $prog{$pid}{episode};
+       $metadata{type}         = $prog_type || $prog{$pid}{type};
+       $metadata{duration}     = $duration || $prog{$pid}{duration};
+       $metadata{channel}      = $channel || $prog{$pid}{channel};
+       $metadata{available}    = $available || $prog{$pid}{available};
+       $metadata{expiry}       = $expiry || $prog{$pid}{expiry};
+       $metadata{versions}     = $versions || $prog{$pid}{versions};
+       $metadata{guidance}     = $guidance || $prog{$pid}{guidance};
+       $metadata{categories}   = $categories || $prog{$pid}{categories};
+       $metadata{desc}         = $longdesc || $prog{$pid}{desc};
+       $metadata{player}       = $player;
+       $metadata{thumbnail}    = $thumbnail || $prog{$pid}{thumbnail};
+
+       return %metadata;
+}
+
+
+
+# Gets the contents of a URL and retries if it fails, returns '' if no page could be retrieved
+# Usage <content> = request_url_retry(<ua>, <url>, <retries>, <succeed message>, [<fail message>]);
+sub request_url_retry {
+       my ($ua, $url, $retries, $succeedmsg, $failmsg) = @_; 
+       my $res;
+
+       my $i;
+       logger "INFO: Getting page $url\n" if $opt{verbose};
+       for ($i = 0; $i < $retries; $i++) {
+               $res = $ua->request( HTTP::Request->new( GET => $url ) );
+               if ( ! $res->is_success ) {
+                       logger $failmsg;
+               } else {
+                       logger $succeedmsg;
+                       last;
+               }
+       }
+       # Return empty string if we failed
+       return '' if $i == $retries;
+       # otherwise return content
+       return $res->content;
+}
+
+
+
+# Checks if a particular program exists (or program.exe) in the $ENV{PATH} or if it has a path already check for existence of file
+sub exists_in_path {
+       my $file = shift;
+       # If this has a path specified, does file exist
+       return 1 if $file =~ /[\/\\]/ && (-f $file || -f "${file}.exe");
+       # Search PATH
+       for (@PATH) {
+               return 1 if -f "${_}/${file}" || -f "${_}/${file}.exe";
+       }
+       return 0;
+}
+
+
+
+# Run a user specified command
+# e.g. --command 'echo "<pid> <longname> downloaded"'
+# run_user_command($pid, 'echo "<pid> <longname> downloaded"');
+sub run_user_command {
+       my $pid = shift;
+       my $command = shift;
+
+       # Substitute the fields for the pid (don't sanitize)
+       $command = substitute_fields( $pid, $command, 1 );
+
+       # Escape chars in command for shell use
+       esc_chars(\$command);
+
+       # run command
+       logger "INFO: Running command '$command'\n" if $opt{verbose};
+       my $exit_value = system $command;
+       
+       # make exit code sane
+       $exit_value = $exit_value >> 8;
+       logger "ERROR: Command Exit Code: $exit_value\n" if $exit_value;
+       logger "INFO: Command succeeded\n" if $opt{verbose} && ! $exit_value;
+        return 0;
+}
+
+
+
+# Adds pid to history file (with a timestamp) so that it is not redownloaded after deletion
+sub add_to_download_history {
+       my $pid = shift;
+       # Only add if a pid is specified
+       return 0 if ! $pid;
+       # Don't add to history if stdout streaming is used
+       return 0 if ( $opt{stdout} && $opt{nowrite} ) || $opt{streaminfo};
+
+       # Add to history
+       if ( ! open(HIST, ">> $historyfile") ) {
+               logger "WARNING: Cannot write or append to $historyfile\n\n";
+               return 1;
+       }
+       print HIST "$pid|$prog{$pid}{name}|$prog{$pid}{episode}|$prog{$pid}{type}|".time()."\n";
+       close HIST;
+       return 0;
+}
+
+
+
+# returns a hash (<pid> => <data>) for all the pids in the history file
+sub load_download_history {
+       my %pids_downloaded;
+
+       # Return if force-download option specified or stdout streaming only
+       return %pids_downloaded if $opt{forcedownload} || $opt{stdout} || $opt{nowrite};
+
+       logger "INFO: Loading download history\n" if $opt{verbose};
+       if ( ! open(HIST, "< $historyfile") ) {
+               logger "WARNING: Cannot read $historyfile\n\n";
+               return 0;
+       }
+       while ( <HIST> ) {
+               $pids_downloaded{$1} = $2 if m{^(.+?)\|(.*)$};
+               logger "DEBUG: Loaded '$1' = '$2' from download history\n" if $opt{debug};
+       }
+       return %pids_downloaded;
+}
+
+
+
+# Checks history for previous download of this pid
+sub check_download_history {
+       my $pid = shift;
+       my ($matchedpid, $name, $episode);
+       return 0 if ! $pid;
+               
+       # Return if force-download option specified or stdout streaming only
+       return 0 if $opt{forcedownload} || $opt{stdout} || $opt{nowrite};
+       
+       if ( ! open(HIST, "< $historyfile") ) {
+               logger "WARNING: Cannot read $historyfile\n\n";
+               return 0;
+       }
+
+       # Find and parse first matching line
+       ($matchedpid, $name, $episode) = ( split /\|/, (grep /^$pid/, <HIST>)[0] )[0,1,2];
+       if ( $matchedpid ) {
+               chomp $name;
+               chomp $episode;
+               logger "INFO: $name - $episode ($pid) Already in download history ($historyfile)\n";
+               close HIST;
+               return 1;
+
+       } else {
+               logger "INFO: Programme not in download history\n" if $opt{verbose};
+               close HIST;
+               return 0;
+       }
+}
+
+
+
+# Add id3 tag to MP3 files if required
+sub tag_file {
+       my $pid = shift;
+       if ( $prog{$pid}{ext} eq 'mp3' ) {
+               # Create ID3 tagging options for external tagger program (escape " for shell)
+               my ( $id3_name, $id3_episode, $id3_desc, $id3_channel ) = ( $prog{$pid}{name}, $prog{$pid}{episode}, $prog{$pid}{desc}, $prog{$pid}{channel} );
+               $id3_name =~ s|"|\"|g for ($id3_name, $id3_episode, $id3_desc, $id3_channel);
+               # Only tag if the required tool exists
+               if ( exists_in_path($id3v2) ) {
+                       logger "INFO: id3 tagging MP3 file\n";
+                       my $cmd = "$id3v2 --artist \"$id3_channel\" --album \"$id3_name\" --song \"$id3_episode\" --comment \"Description\":\"$id3_desc\" --year ".( (localtime())[5] + 1900 )." \"$prog{$pid}{filename}\" 1>&2";
+                       logger "DEGUG: Running $cmd\n" if $opt{debug};
+                       if ( system($cmd) ) {
+                               logger "WARNING: Failed to tag MP3 file\n";
+                               return 2;
+                       }
+               } else {
+                       logger "WARNING: Cannot tag MP3 file\n" if $opt{verbose};
+               }
+       }
+}
+
+
+
+# Show channels for specified type if required
+sub list_unique_element_counts {
+       my $element_name = shift;
+       my %elements;
+       logger "INFO: ".(join ',', keys %type)." $element_name List:\n" if $opt{verbose};
+       for my $pid (keys %prog) {
+               my @element;
+               # Need to separate the categories
+               if ($element_name eq 'categories') {
+                       @element = split /,/, $prog{$pid}{$element_name};
+               } else {
+                       @element[0] = $prog{$pid}{$element_name};
+               }
+               for my $element (@element) {
+                       $elements{ $element }++;
+               }
+       }
+       # display element + prog count
+       logger "$_ ($elements{$_})\n" for sort keys %elements;
+       return 0;
+}
+
+
+
+# Escape chars in string for shell use
+sub esc_chars {
+       # will change, for example, a!!a to a\!\!a
+       s/([;<>\*\|&\$!#\(\)\[\]\{\}:'"])/\\$1/g;
+       return $_;
+}
+
+
+
+# Signal handler to clean up after a ctrl-c or kill
+sub cleanup {
+       logger "INFO: Cleaning up\n" if $opt{verbose};
+       unlink $namedpipe;
+       unlink $lockfile;
+       exit 1;
+}
+
+
+
+# Return a string with formatting fields substituted for a given pid
+sub substitute_fields {
+       my $pid = shift;
+       my $string = shift;
+       my $no_sanitize = shift || 0;
+       my $replace;
+               
+       # Tokenize and substitute $format
+       for my $key ( keys %{ $prog{$pid} } ) {
+               # Remove/replace all non-nice-filename chars if required
+               if (! $no_sanitize) {
+                       $replace = sanitize_path( $prog{$pid}{$key} );
+               } else {
+                       $replace = $prog{$pid}{$key};
+               }
+               $string =~ s|\<$key\>|$replace|gi;
+       }
+       if (! $no_sanitize) {
+               $replace = sanitize_path( $pid );       
+       } else {
+               $replace = $pid;
+       }
+       $string =~ s|<pid>|$replace|gi;
+       # Remove/replace all non-nice-filename chars if required except for fwd slashes
+       return sanitize_path( $string, 1 );
+}
+
+
+
+# Make a filename/path sane (optionally allow fwd slashes)
+sub sanitize_path {
+       my $string = shift;
+       my $allow_fwd_slash = shift || 0;
+
+       # Remove fwd slash if reqd
+       $string =~ s/\//_/g if ! $allow_fwd_slash;
+
+       # Replace backslashes with _ regardless
+       $string =~ s/\\/_/g;
+       # Sanitize by default
+       $string =~ s/\s/_/g if (! $opt{whitespace}) && (! $allow_fwd_slash);
+       $string =~ s/[^\w_\-\.\/\s]//gi if ! $opt{whitespace};
+       return $string;
+}
+
+
+
+
+# Save the options on the cmdline as a PVR search with the specified name
+sub pvr_add {
+       my $name = shift;
+       my @search_args = @_;
+       my @options;
+       # validate name
+       if ( $name !~ m{[\w\-\+]+} ) {
+               logger "ERROR: Invalid PVR search name '$name'\n";
+               return 1;
+       }
+       # Parse valid options and create array (ignore options from the options files that have not been overriden on the cmdline)
+       for (grep /^(amode|vmode|long|output.*|proxy|subdir|whitespace|versions|type|(exclude)?category|(exclude)?channel|command|realaudio|mp3audio|wav|raw|bandwidth|subtitles|suboffset|since|versionlist|verbose)$/, sort {lc $a cmp lc $b} keys %opt_cmdline) {
+               if ( defined $opt_cmdline{$_} ) {
+                               push @options, "$_ $opt_cmdline{$_}";
+                               logger "DEBUG: Adding option $_ = $opt_cmdline{$_}\n" if $opt{debug};
+               }
+       }
+       # Add search args to array
+       for ( my $count = 0; $count <= $#search_args; $count++ ) {
+               push @options, "search${count} $search_args[$count]";
+               logger "DEBUG: Adding search${count} = $search_args[$count]\n" if $opt{debug};
+       }
+       # Save search to file
+       pvr_save( $name, @options );
+       logger "INFO: Added PVR search '$name':\n";
+       return 0;
+}
+
+
+
+# Delete the named PVR search
+sub pvr_del {
+       my $name = shift;
+       # validate name
+       if ( $name !~ m{[\w\-\+]+} ) {
+               logger "ERROR: Invalid PVR search name '$name'\n";
+               return 1;
+       }
+       # Delete pvr search file
+       if ( -f ${pvr_dir}.$name ) {
+               unlink ${pvr_dir}.$name;
+       } else {
+               logger "ERROR: PVR search '$name' does not exist\n";
+               return 1;
+       }
+       return 0;
+}
+
+
+
+# Display all the PVR searches
+sub pvr_display_list {
+       # Load all the PVR searches
+       pvr_load_list();
+       # Print out list
+       logger "All PVR Searches:\n\n";
+       for my $name ( sort {lc $a cmp lc $b} keys %pvrsearches ) {
+               # Report whether disabled
+               if ( $pvrsearches{$name}{disable} ) {
+                       logger "(Disabled) PVR Search '$name':\n";
+               } else {
+                       logger "PVR Search '$name':\n";
+               }
+               for ( sort keys %{ $pvrsearches{$name} } ) {
+                       logger "\t$_ = $pvrsearches{$name}{$_}\n";
+               }
+               logger "\n";
+       }
+       return 0;
+}
+
+
+
+# Load all the PVR searches into %pvrsearches
+sub pvr_load_list {
+       # Make dir if not existing
+       mkpath $pvr_dir if ! -d $pvr_dir;
+       # Get list of files in pvr_dir
+       # open file with handle DIR
+       opendir( DIR, $pvr_dir );
+       if ( ! opendir( DIR, $pvr_dir) ) {
+               logger "ERROR: Cannot open directory $pvr_dir\n";
+               return 1;
+       }
+       # Get contents of directory (ignoring . .. and ~ files)
+       my @files = grep ! /(^\.{1,2}$|^.*~$)/, readdir DIR;
+       # Close the directory
+       closedir DIR;
+       # process each file
+       for my $file (@files) {
+               chomp($file);
+               # Re-add the dir
+               $file = "${pvr_dir}/$file";
+               next if ! -f $file;
+               if ( ! open (PVR, "< $file") ) {
+                       logger "WARNING: Cannot read PVR search file $file\n";
+                       next;
+               }
+               my @options = <PVR>;
+               close PVR;
+               # Get search name from filename
+               my $name = $file;
+               $name =~ s/^.*\/([^\/]+?)$/$1/g;
+               for (@options) {
+                       /^\s*([\w\-_]+?)\s+(.*)\s*$/;
+                       logger "DEBUG: PVR search '$name': option $1 = $2\n" if $opt{debug};
+                       $pvrsearches{$name}{$1} = $2;
+               }
+               logger "INFO: Loaded PVR search '$name'\n" if $opt{verbose};
+       }
+       logger "INFO: Loaded PVR search list\n" if $opt{verbose};
+       return 0;
+}
+
+
+
+# Save the array options specified as a PVR search
+sub pvr_save {
+       my $name = shift;
+       my @options = @_;
+       # Make dir if not existing
+       mkpath $pvr_dir if ! -d $pvr_dir;
+       # Open file
+       if ( ! open (PVR, "> ${pvr_dir}/${name}") ) { 
+               logger "ERROR: Cannot save PVR search to ${pvr_dir}.$name\n";
+               return 1;
+       }
+       # Write options array to file
+       for (@options) {
+               print PVR "$_\n";
+       }
+       close PVR;
+       logger "INFO: Saved PVR search '$name'\n";
+       return 0;
+}
+
+
+
+# Clear all exisiting global args and opts then load the options specified in the default options and specified PVR search
+sub pvr_load_options {
+       my $name = shift;
+       # Clear out existing options hash
+       %opt = ();
+       # Re-read options from the options files - these will act as defaults
+       for ( keys %opt_file ) {
+               $opt{$_} = $opt_file{$_} if defined $opt_file{$_};
+       }
+       # Clear search args
+       @search_args = ();
+       # Set each option from the search
+       for ( sort {$a <=> $b} keys %{ $pvrsearches{$name} } ) {
+               # Add to list of search args if this is not an option
+               if ( /^search\d+$/ ) {
+                       logger "INFO: $_ = $pvrsearches{$name}{$_}\n" if $opt{verbose};
+                       push @search_args, $pvrsearches{$name}{$_};
+               # Else populate options, ignore disable option
+               } elsif ( $_ ne 'disable' ) {
+                       logger "INFO: Option: $_ = $pvrsearches{$name}{$_}\n" if $opt{verbose};
+                       $opt{$_} = $pvrsearches{$name}{$_};
+               }
+       }
+       # Allow cmdline args to override those in the PVR search
+       # Re-read options from the cmdline
+       for ( keys %opt_cmdline ) {
+               $opt{$_} = $opt_cmdline{$_} if defined $opt_cmdline{$_};
+       }       return 0;
+}
+
+
+
+# Disable a PVR search by adding 'disable 1' option
+sub pvr_disable {
+       my $name = shift;
+       pvr_load_list();
+       my @options;
+       for ( keys %{ $pvrsearches{$name} }) {
+               push @options, "$_ $pvrsearches{$name}{$_}";
+       }
+       # Add the disable option
+       push @options, 'disable 1';
+       pvr_save( $name, @options );
+       return 0;
+}
+
+
+
+# Re-enable a PVR search by removing 'disable 1' option
+sub pvr_enable {
+       my $name = shift;
+       pvr_load_list();
+       my @options;
+       for ( keys %{ $pvrsearches{$name} }) {
+               push @options, "$_ $pvrsearches{$name}{$_}";
+       }
+       # Remove the disable option
+       @options = grep !/^disable\s/, @options;
+       pvr_save( $name, @options );    
+       return 0;
+}
+
+
+
+# Send a message to STDOUT so that cron can use this to email 
+sub pvr_report {
+       my $pid = shift;
+       print STDOUT "New $prog{$pid}{type} programme: '$prog{$pid}{name} - $prog{$pid}{episode}', '$prog{$pid}{desc}'\n";
+       return 0;
+}
+
+
+
+# Lock file detection (<lockfile>, <stale_secs>)
+# Global $lockfile
+sub lockfile {
+       my $stale_time = shift || 86400;
+       my $now = time();
+       # if lockfile exists then quit as we are already running
+       if ( -T $lockfile ) {
+               if ( ! open (LOCKFILE, $lockfile) ) {
+                       logger "ERROR: Cannot read lockfile '$lockfile'\n";
+                       exit 1;
+               }
+               my @lines = <LOCKFILE>;
+               close LOCKFILE;
+
+               # If the process is still running and the lockfile is newer than $stale_time seconds
+               if ( kill(0,$lines[0]) > 0 && $now < ( stat($lockfile)->mtime + $stale_time ) ) {
+                               logger "ERROR: Quitting - process is already running ($lockfile)\n";
+                               # redefine cleanup sub so that it doesn't delete $lockfile
+                               $lockfile = '';
+                               exit 0;
+               } else {
+                       logger "INFO: Removing stale lockfile\n" if $opt{verbose};
+                       unlink $lockfile;
+               }
+       }
+       # write our PID into this lockfile
+       if (! open (LOCKFILE, "> $lockfile") ) {
+               logger "ERROR: Cannot write to lockfile '$lockfile'\n";
+               exit 1;
+       }
+       print LOCKFILE $$;
+       close LOCKFILE;
+       return 0;
+}
+
diff --git a/log.cpp b/log.cpp
new file mode 100644 (file)
index 0000000..93156b9
--- /dev/null
+++ b/log.cpp
@@ -0,0 +1,38 @@
+/*  RTMPDump
+ *  Copyright (C) 2008-2009 Andrej Stepanchuk
+ *
+ *  This Program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2, or (at your option)
+ *  any later version.
+ *
+ *  This Program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with RTMPDump; see the file COPYING.  If not, write to
+ *  the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
+ *  http://www.gnu.org/copyleft/gpl.html
+ *
+ */
+
+#include <stdio.h>
+#include <stdarg.h>
+
+#include "log.h"
+
+void Log(int level, const char *format, ...)
+{
+        char str[256]="";
+
+        va_list args;
+        va_start(args, format);
+        vsnprintf(str, 255, format, args);
+        va_end(args);
+
+       //if(level != LOGDEBUG)
+        printf("\r%s: %s\n", level==LOGDEBUG?"DEBUG":(level==LOGERROR?"ERROR":"WARNING"), str);
+}
+
diff --git a/log.h b/log.h
new file mode 100644 (file)
index 0000000..05ee142
--- /dev/null
+++ b/log.h
@@ -0,0 +1,33 @@
+/*  RTMP Dump
+ *  Copyright (C) 2008-2009 Andrej Stepanchuk
+ *
+ *  This Program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2, or (at your option)
+ *  any later version.
+ *
+ *  This Program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with RTMPDump; see the file COPYING.  If not, write to
+ *  the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
+ *  http://www.gnu.org/copyleft/gpl.html
+ *
+ */
+
+#ifndef __LOG_H__
+#define __LOG_H__
+
+//#define _DEBUG
+
+#define LOGDEBUG        0
+#define LOGERROR        1
+#define LOGWARNING     2
+
+void Log(int level, const char *format, ...);
+
+#endif
+
diff --git a/rtmp.cpp b/rtmp.cpp
new file mode 100644 (file)
index 0000000..5c22e76
--- /dev/null
+++ b/rtmp.cpp
@@ -0,0 +1,1238 @@
+/*
+ *      Copyright (C) 2005-2008 Team XBMC
+ *      http://www.xbmc.org
+ *      Copyright (C) 2008-2009 Andrej Stepanchuk
+ *
+ *  This Program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2, or (at your option)
+ *  any later version.
+ *
+ *  This Program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with RTMPDump; see the file COPYING.  If not, write to
+ *  the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
+ *  http://www.gnu.org/copyleft/gpl.html
+ *
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/time.h>
+#include <sys/times.h>
+#include <time.h>
+#include <errno.h>
+#include <boost/regex.hpp>
+
+#include "rtmp.h"
+#include "AMFObject.h"
+#include "log.h"
+
+#define RTMP_SIG_SIZE 1536
+#define RTMP_LARGE_HEADER_SIZE 12
+
+#define RTMP_BUFFER_CACHE_SIZE (16*1024)
+
+using namespace RTMP_LIB;
+using namespace std;
+
+static const int packetSize[] = { 12, 8, 4, 1 };
+#define RTMP_PACKET_SIZE_LARGE    0
+#define RTMP_PACKET_SIZE_MEDIUM   1
+#define RTMP_PACKET_SIZE_SMALL    2
+#define RTMP_PACKET_SIZE_MINIMUM  3
+
+clock_t GetTime()
+{
+       struct tms t;
+       return times(&t);
+}
+
+CRTMP::CRTMP() : m_socket(0)
+{
+  Close();
+  m_pBuffer = new char[RTMP_BUFFER_CACHE_SIZE];
+  m_nBufferMS = 300;
+  m_fDuration = 0;
+}
+
+CRTMP::~CRTMP()
+{
+  Close();
+  delete [] m_pBuffer;
+}
+
+double CRTMP::GetDuration() { return m_fDuration; }
+bool CRTMP::IsConnected() { return m_socket != 0; }
+
+void CRTMP::SetBufferMS(int size)
+{
+  m_nBufferMS = size;
+}
+//*
+void CRTMP::UpdateBufferMS()
+{
+  SendPing(3, 1, m_nBufferMS);
+}
+//*/
+bool CRTMP::Connect(char *url, char *tcUrl, char *player, char *pageUrl, char *app, char *auth, char *flashVer, double dTime)
+{
+  // check url formatting
+  boost::regex re("^rtmp:\\/\\/[a-zA-Z0-9_\\.\\-]+((:(\\d)+\\/)|\\/)(([0-9a-zA-Z_:;\\+\\-\\.\\!\\\"\\$\\%\\&\\/\\(\\)\\=\\?\\<\\>\\s]*)$|$)");
+
+  if (!boost::regex_match(url, re))
+  {
+       Log(LOGERROR, "RTMP Connect: invalid url!");
+       return false;
+  }
+
+  Link.url = url;
+  Link.tcUrl = tcUrl;
+  Link.player = player;
+  Link.pageUrl = pageUrl;
+  Link.app = app;
+  Link.auth = auth;
+  Link.flashVer = flashVer;
+  Link.seekTime = dTime;
+
+  boost::cmatch matches;
+
+  boost::regex re1("^rtmp:\\/\\/([a-zA-Z0-9_\\.\\-]+)((:([0-9]+)\\/)|\\/)[0-9a-zA-Z_:;\\+\\-\\.\\!\\\"\\$\\%\\&\\/\\(\\)\\=\\?\\<\\>\\s]+");
+  if(!boost::regex_match(url, matches, re1))
+  {
+       Log(LOGERROR, "RTMP Connect: Regex for url doesn't match (error in programme)!");
+       return false;
+  }
+  /*for(int i=0; i<matches.size(); i++) {
+       Log(LOGDEBUG, "matches[%d]: %s, %s", i, matches[i].first, matches[i].second);
+  }*/
+
+  if(matches[1].second-matches[1].first > 255) {
+       Log(LOGERROR, "Hostname must not be longer than 255 characters!");
+       return false;
+  }
+  strncpy(Link.hostname, matches[1].first, matches[1].second-matches[1].first);
+  Link.hostname[matches[1].second-matches[1].first]=0x00;
+
+  Log(LOGDEBUG, "Hostname: %s", Link.hostname);
+
+  char portstr[6];
+  if(matches[4].second-matches[4].first > 5) {
+          Log(LOGERROR, "Port must not be longer than 5 digits!");
+         return false;
+  }
+  strncpy(portstr, matches[4].first, matches[4].second-matches[4].first);
+  portstr[matches[4].second-matches[4].first]=0x00;
+
+  Link.port = atoi(portstr);
+
+  if (Link.port == 0)
+    Link.port = 1935;
+  
+  Log(LOGDEBUG, "Port: %d", Link.port);
+
+  // obtain auth string if available
+  /*
+  boost::regex re2("^.*auth\\=([0-9a-zA-Z_:;\\-\\.\\!\\\"\\$\\%\\/\\(\\)\\=\\s]+)((&.+$)|$)");
+  boost::cmatch matches2;
+  
+  if(boost::regex_match(url, matches2, re2)) {
+    int len = matches2[1].second-matches2[1].first;
+    if(len > 255) {
+       Log(LOGERROR, "Auth string must not be longer than 255 characters!");
+    }
+    Link.auth = (char *)malloc((len+1)*sizeof(char));
+    strncpy(Link.auth, matches2[1].first, len);
+    Link.auth[len]=0;
+
+    Log(LOGDEBUG, "Auth: %s", Link.auth);
+  } else { Link.auth = 0; }
+  //*/
+
+  Close();
+
+  sockaddr_in service;
+  memset(&service, 0, sizeof(sockaddr_in));
+  service.sin_family = AF_INET;
+  service.sin_addr.s_addr = inet_addr(Link.hostname);
+  if (service.sin_addr.s_addr == INADDR_NONE)
+  {
+    struct hostent *host = gethostbyname(Link.hostname);
+    if (host == NULL || host->h_addr == NULL)
+    {
+      Log(LOGERROR, "Problem accessing the DNS. (addr: %s)", Link.hostname);
+      return false;
+    }
+    service.sin_addr = *(struct in_addr*)host->h_addr;
+  }
+
+  service.sin_port = htons(Link.port);
+  m_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
+  if (m_socket != 0 )
+  {
+    if (connect(m_socket, (sockaddr*) &service, sizeof(struct sockaddr)) < 0)
+    {
+      Log(LOGERROR, "%s, failed to connect.", __FUNCTION__);
+      Close();
+      return false;
+    }
+
+    Log(LOGDEBUG, "connected, hand shake:");
+    if (!HandShake())
+    {
+      Log(LOGERROR, "%s, handshake failed.", __FUNCTION__);
+      Close();
+      return false;
+    }
+
+    Log(LOGDEBUG, "handshaked");
+    if (!Connect())
+    {
+      Log(LOGERROR, "%s, connect failed.", __FUNCTION__);
+      Close();
+      return false;
+    }
+    // set timeout
+      struct timeval tv;
+      memset(&tv, 0, sizeof(tv));
+      tv.tv_sec = 300;
+      if (setsockopt(m_socket, SOL_SOCKET, SO_RCVTIMEO, (char *)&tv,  sizeof(tv)))
+      {
+       Log(LOGERROR,"Setting timeout failed!");
+      }
+  }
+  else
+  {
+    Log(LOGERROR, "%s, failed to create socket.", __FUNCTION__);
+    return false;
+  }
+
+  return true;
+}
+
+bool CRTMP::GetNextMediaPacket(RTMPPacket &packet)
+{
+  bool bHasMediaPacket = false;
+  while (!bHasMediaPacket && IsConnected() && ReadPacket(packet))
+  {
+    if (!packet.IsReady())
+    {
+      packet.FreePacket();
+      usleep(5000); // 5ms
+      //sleep(0.1);//30);
+      continue;
+    }
+
+    switch (packet.m_packetType)
+    {
+      case 0x01:
+        // chunk size
+        HandleChangeChunkSize(packet);
+        break;
+
+      case 0x03:
+        // bytes read report
+        Log(LOGDEBUG, "%s, received: bytes read report", __FUNCTION__);
+        break;
+
+      case 0x04:
+        // ping
+        HandlePing(packet);
+        break;
+
+      case 0x05:
+        // server bw
+        Log(LOGDEBUG, "%s, received: server BW", __FUNCTION__);
+        break;
+
+      case 0x06:
+        // client bw
+        Log(LOGDEBUG, "%s, received: client BW", __FUNCTION__);
+        break;
+
+      case 0x08:
+        // audio data
+        //Log(LOGDEBUG, "%s, received: audio %lu bytes", __FUNCTION__, packet.m_nBodySize);
+        HandleAudio(packet);
+        bHasMediaPacket = true;
+        break;
+
+      case 0x09:
+        // video data
+        //Log(LOGDEBUG, "%s, received: video %lu bytes", __FUNCTION__, packet.m_nBodySize);
+        HandleVideo(packet);
+        bHasMediaPacket = true;
+        break;
+
+      case 0x12:
+        // metadata (notify)
+        Log(LOGDEBUG, "%s, received: notify", __FUNCTION__);
+        HandleMetadata(packet.m_body, packet.m_nBodySize);
+        bHasMediaPacket = true;
+        break;
+
+      case 0x14:
+        // invoke
+       Log(LOGDEBUG, "%s, received: invoke", __FUNCTION__);
+        HandleInvoke(packet);
+        break;
+
+      case 0x16:
+       // ok, this might be a meta data packet as well, so check!
+       if(packet.m_body[0] == 0x12) {
+               HandleMetadata(packet.m_body+11, packet.m_nBodySize-11);
+       }
+        // FLV tag(s)
+        //Log(LOGDEBUG, "%s, received: FLV tag(s) %lu bytes", __FUNCTION__, packet.m_nBodySize);
+        bHasMediaPacket = true;
+        break;
+
+      default:
+        Log(LOGDEBUG, "unknown packet type received: 0x%02x", packet.m_packetType);
+    }
+
+    if (!bHasMediaPacket) { 
+      packet.FreePacket();
+    }
+  }
+        
+  if (bHasMediaPacket)
+    m_bPlaying = true;
+
+  return bHasMediaPacket;
+}
+
+int CRTMP::ReadN(char *buffer, int n)
+{
+  int nOriginalSize = n;
+  memset(buffer, 0, n);
+  char *ptr = buffer;
+  while (n > 0)
+  {
+    int nBytes = 0;
+    // for dumping we don't need buffering, so we won't get stuck in the end!
+    /*if (m_bPlaying)
+    {
+      if (m_nBufferSize < n)
+        FillBuffer();
+
+      int nRead = ((n<m_nBufferSize)?n:m_nBufferSize);
+      if (nRead > 0)
+      {
+        memcpy(buffer, m_pBuffer, nRead);
+        memmove(m_pBuffer, m_pBuffer + nRead, m_nBufferSize - nRead); // crunch buffer
+        m_nBufferSize -= nRead;
+        nBytes = nRead;
+        m_nBytesIn += nRead;
+        if (m_nBytesIn > m_nBytesInSent + (600*1024) ) // report every 600K
+          SendBytesReceived();
+      }
+    }
+    else*/
+      nBytes = recv(m_socket, ptr, n, 0);
+
+    if(m_bPlaying) {
+        m_nBytesIn += nBytes;
+       if (m_nBytesIn > m_nBytesInSent + (600*1024) ) // report every 600K
+                  SendBytesReceived();
+    }
+    if (nBytes == -1)
+    {
+      Log(LOGERROR, "%s, RTMP recv error %d", __FUNCTION__, errno);
+      Close();
+      return false;
+    }
+    
+    if (nBytes == 0)
+    {
+      Log(LOGDEBUG, "%s, RTMP socket closed by server", __FUNCTION__);
+      Close();
+      break;
+    }
+    
+    n -= nBytes;
+    ptr += nBytes;
+  }
+
+  return nOriginalSize - n;
+}
+
+bool CRTMP::WriteN(const char *buffer, int n)
+{
+  const char *ptr = buffer;
+  while (n > 0)
+  {
+    int nBytes = send(m_socket, ptr, n, 0);
+    if (nBytes < 0)
+    {
+      Log(LOGERROR, "%s, RTMP send error %d (%d bytes)", __FUNCTION__, errno, n);
+      Close();
+      return false;
+    }
+    
+    if (nBytes == 0)
+      break;
+    
+    n -= nBytes;
+    ptr += nBytes;
+  }
+
+  return n == 0;
+}
+
+bool CRTMP::Connect()
+{
+  if (!SendConnectPacket())
+  {
+    Log(LOGERROR, "%s, failed to send connect RTMP packet", __FUNCTION__);
+    return false;
+  }
+
+  return true;
+}
+
+bool CRTMP::SendConnectPacket()
+{
+  RTMPPacket packet;
+  packet.m_nChannel = 0x03;   // control channel (invoke)
+  packet.m_headerType = RTMP_PACKET_SIZE_LARGE;
+  packet.m_packetType = 0x14; // INVOKE
+  packet.AllocPacket(4096);
+
+  char *enc = packet.m_body;
+  enc += EncodeString(enc, "connect");
+  enc += EncodeNumber(enc, 1.0);
+  *enc = 0x03; //Object Datatype
+  enc++;
+  if(Link.app)
+       enc += EncodeString(enc, "app", Link.app);
+  if(Link.flashVer)
+       enc += EncodeString(enc, "flashVer", Link.flashVer);
+  if(Link.player)
+       enc += EncodeString(enc, "swfUrl", Link.player);
+  if(Link.tcUrl)
+       enc += EncodeString(enc, "tcUrl", Link.tcUrl);
+  
+  enc += EncodeBoolean(enc, "fpad", false);
+  enc += EncodeNumber(enc, "capabilities", 15.0);
+  enc += EncodeNumber(enc, "audioCodecs", 1639.0);
+  enc += EncodeNumber(enc, "videoCodecs", 252.0);
+  enc += EncodeNumber(enc, "videoFunction", 1.0);
+  if(Link.pageUrl)
+       enc += EncodeString(enc, "pageUrl", Link.pageUrl);  
+  enc += 2; // end of object - 0x00 0x00 0x09
+  *enc = 0x09;
+  enc++;
+  
+  // add auth string
+  if(Link.auth)
+  {
+       *enc = 0x01; enc++;
+       *enc = 0x01; enc++;
+
+       enc += EncodeString(enc, Link.auth);
+  }
+  packet.m_nBodySize = enc-packet.m_body;
+
+  return SendRTMP(packet);
+}
+
+bool CRTMP::SendCreateStream(double dStreamId)
+{
+  RTMPPacket packet;
+  packet.m_nChannel = 0x03;   // control channel (invoke)
+  packet.m_headerType = RTMP_PACKET_SIZE_MEDIUM;
+  packet.m_packetType = 0x14; // INVOKE
+
+  packet.AllocPacket(256); // should be enough
+  char *enc = packet.m_body;
+  enc += EncodeString(enc, "createStream");
+  enc += EncodeNumber(enc, dStreamId);
+  *enc = 0x05; // NULL
+  enc++;
+
+  packet.m_nBodySize = enc - packet.m_body;
+
+  return SendRTMP(packet);
+}
+
+bool CRTMP::SendPause()
+{
+  RTMPPacket packet;
+  packet.m_nChannel = 0x08;   // video channel 
+  packet.m_headerType = RTMP_PACKET_SIZE_MEDIUM;
+  packet.m_packetType = 0x14; // invoke
+
+  packet.AllocPacket(256); // should be enough
+  char *enc = packet.m_body;
+  enc += EncodeString(enc, "pause");
+  enc += EncodeNumber(enc, 0);
+  *enc = 0x05; // NULL
+  enc++;
+  enc += EncodeBoolean(enc, true);
+  enc += EncodeNumber(enc, 0);
+
+  packet.m_nBodySize = enc - packet.m_body;
+
+  return SendRTMP(packet);
+}
+
+bool CRTMP::SendSeek(double dTime)
+{
+  RTMPPacket packet;
+  packet.m_nChannel = 0x08;   // video channel 
+  packet.m_headerType = RTMP_PACKET_SIZE_MEDIUM;
+  packet.m_packetType = 0x14; // invoke
+
+  packet.AllocPacket(256); // should be enough
+  char *enc = packet.m_body;
+  enc += EncodeString(enc, "seek");
+  enc += EncodeNumber(enc, 0);
+  *enc = 0x05; // NULL
+  enc++;
+  enc += EncodeNumber(enc, dTime);
+
+  packet.m_nBodySize = enc - packet.m_body;
+
+  return SendRTMP(packet);
+}
+
+bool CRTMP::SendServerBW()
+{
+  RTMPPacket packet;
+  packet.m_nChannel = 0x02;   // control channel (invoke)
+  packet.m_headerType = RTMP_PACKET_SIZE_LARGE;
+  packet.m_packetType = 0x05; // Server BW
+
+  packet.AllocPacket(4);
+  packet.m_nBodySize = 4;
+
+  EncodeInt32(packet.m_body, 0x001312d0); // hard coded for now
+  return SendRTMP(packet);
+}
+
+bool CRTMP::SendBytesReceived()
+{
+  RTMPPacket packet;
+  packet.m_nChannel = 0x02;   // control channel (invoke)
+  packet.m_headerType = RTMP_PACKET_SIZE_MEDIUM;
+  packet.m_packetType = 0x03; // bytes in
+
+  packet.AllocPacket(4);
+  packet.m_nBodySize = 4;
+
+  EncodeInt32(packet.m_body, m_nBytesIn); // hard coded for now
+  m_nBytesInSent = m_nBytesIn;
+
+  //Log(LOGDEBUG, "Send bytes report. 0x%x (%d bytes)", (unsigned int)m_nBytesIn, m_nBytesIn);
+  return SendRTMP(packet);
+}
+
+bool CRTMP::SendCheckBW()
+{
+  RTMPPacket packet;
+  packet.m_nChannel = 0x03;   // control channel (invoke)
+  packet.m_headerType = RTMP_PACKET_SIZE_LARGE;
+  packet.m_packetType = 0x14; // INVOKE
+  packet.m_nInfoField1 = GetTime();
+
+  packet.AllocPacket(256); // should be enough
+  char *enc = packet.m_body;
+  enc += EncodeString(enc, "_checkbw");
+  enc += EncodeNumber(enc, 0x00);
+  *enc = 0x05; // NULL
+  enc++;
+
+  packet.m_nBodySize = enc - packet.m_body;
+
+  return SendRTMP(packet);
+}
+
+bool CRTMP::SendCheckBWResult()
+{
+  RTMPPacket packet;
+  packet.m_nChannel = 0x03;   // control channel (invoke)
+  packet.m_headerType = RTMP_PACKET_SIZE_MEDIUM;
+  packet.m_packetType = 0x14; // INVOKE
+  packet.m_nInfoField1 = 0x16 * m_nBWCheckCounter; // temp inc value. till we figure it out.
+
+  packet.AllocPacket(256); // should be enough
+  char *enc = packet.m_body;
+  enc += EncodeString(enc, "_result");
+  enc += EncodeNumber(enc, (double)time(NULL)); // temp
+  *enc = 0x05; // NULL
+  enc++;
+  enc += EncodeNumber(enc, (double)m_nBWCheckCounter++); 
+
+  packet.m_nBodySize = enc - packet.m_body;
+
+  return SendRTMP(packet);
+}
+
+bool CRTMP::SendPlay()
+{
+  RTMPPacket packet;
+  packet.m_nChannel = 0x08;   // we make 8 our stream channel
+  packet.m_headerType = RTMP_PACKET_SIZE_LARGE;
+  packet.m_packetType = 0x14; // INVOKE
+  packet.m_nInfoField2 = 0x01000000;
+
+  packet.AllocPacket(256); // should be enough
+  char *enc = packet.m_body;
+  enc += EncodeString(enc, "play");
+  enc += EncodeNumber(enc, 0.0);
+  *enc = 0x05; // NULL
+  enc++;
+  // use m_strPlayPath
+  std::string strPlay;// = m_strPlayPath;
+  //if (strPlay.empty())
+  //{
+    // or use slist parameter, if there is one
+    std::string url = std::string(Link.url);
+    int nPos = url.find("slist=");
+    if (nPos > 0)
+      strPlay = url.substr(nPos+6, url.size()-(nPos+6)); //Mid(nPos + 6);
+               
+               if (strPlay.empty())
+               {
+                       // or use last piece of URL, if there's more than one level
+                       std::string::size_type pos_slash = url.find_last_of("/");
+                       if ( pos_slash != std::string::npos )
+                               strPlay = url.substr(pos_slash+1, url.size()-(pos_slash+1)); //Mid(pos_slash+1);
+               }
+
+               if (strPlay.empty()){
+                       Log(LOGERROR, "%s, no name to play!", __FUNCTION__);
+                       return false;
+               }
+  //}
+
+  Log(LOGDEBUG, "Sending play: %s", strPlay.c_str());
+  enc += EncodeString(enc, strPlay.c_str());
+  enc += EncodeNumber(enc, 0.0);
+
+  packet.m_nBodySize = enc - packet.m_body;
+
+  return SendRTMP(packet);
+}
+
+bool bSeekedSuccessfully = false;
+
+bool CRTMP::SendPing(short nType, unsigned int nObject, unsigned int nTime)
+{
+  Log(LOGDEBUG, "sending ping. type: 0x%04x", (unsigned short)nType);
+
+  RTMPPacket packet; 
+  packet.m_nChannel = 0x02;   // control channel (ping)
+  packet.m_headerType = RTMP_PACKET_SIZE_MEDIUM;
+  packet.m_packetType = 0x04; // ping
+  packet.m_nInfoField1 = GetTime();
+
+  int nSize = (nType==0x03?10:6); // type 3 is the buffer time and requires all 3 parameters. all in all 10 bytes.
+  packet.AllocPacket(nSize);
+  packet.m_nBodySize = nSize;
+
+  char *buf = packet.m_body;
+  buf += EncodeInt16(buf, nType);
+
+  if (nSize > 2)
+    buf += EncodeInt32(buf, nObject);
+
+  if (nSize > 6)
+    buf += EncodeInt32(buf, nTime);
+
+  return SendRTMP(packet);
+}
+
+void CRTMP::HandleInvoke(const RTMPPacket &packet)
+{
+  if (packet.m_body[0] != 0x02) // make sure it is a string method name we start with
+  {
+    Log(LOGWARNING, "%s, Sanity failed. no string method in invoke packet", __FUNCTION__);
+    return;
+  }
+
+  RTMP_LIB::AMFObject obj;
+  int nRes = obj.Decode(packet.m_body, packet.m_nBodySize);
+  if (nRes < 0)
+  { 
+    Log(LOGERROR, "%s, error decoding invoke packet", __FUNCTION__);
+    return;
+  }
+
+  obj.Dump();
+  std::string method = obj.GetProperty(0).GetString();
+  Log(LOGDEBUG, "%s, server invoking <%s>", __FUNCTION__, method.c_str());
+
+  if (method == "_result")
+  {
+    std::string methodInvoked = m_methodCalls[0];
+    m_methodCalls.erase(m_methodCalls.begin());
+
+    Log(LOGDEBUG, "%s, received result for method call <%s>", __FUNCTION__, methodInvoked.c_str());
+  
+    if (methodInvoked == "connect")
+    {
+      SendServerBW();
+      SendPing(3, 0, 300);
+      SendCreateStream(2.0);
+    }
+    else if (methodInvoked == "createStream")
+    {
+      SendPlay();
+      if(Link.seekTime > 0) {
+       Log(LOGDEBUG, "%s, sending seek: %f ms", __FUNCTION__, Link.seekTime);
+       SendSeek(Link.seekTime);
+      }
+       
+      SendPing(3, 1, m_nBufferMS);
+    }
+    else if (methodInvoked == "play")
+    {
+    }
+  }
+  else if (method == "onBWDone")
+  {
+    //SendCheckBW();
+  }
+  else if (method == "_onbwcheck")
+  {
+    SendCheckBWResult();
+  }
+  else if (method == "_error")
+  {
+    Log(LOGERROR, "rtmp server sent error");
+  }
+  else if (method == "close")
+  {
+    Log(LOGERROR, "rtmp server requested close");
+    Close();
+  }
+  else if (method == "onStatus")
+  {
+    std::string code  = obj.GetProperty(3).GetObject().GetProperty("code").GetString();
+    std::string level = obj.GetProperty(3).GetObject().GetProperty("level").GetString();
+
+    Log(LOGDEBUG, "%s, onStatus: %s", __FUNCTION__, code.c_str() );
+    if (code == "NetStream.Failed"
+    ||  code == "NetStream.Play.Failed"
+    ||  code == "NetStream.Play.Stop")
+      Close();
+
+    /*if(Link.seekTime > 0) {
+       if(code == "NetStream.Seek.Notify") { // seeked successfully, can play now!
+               bSeekedSuccessfully = true;
+       } else if(code == "NetStream.Play.Start" && !bSeekedSuccessfully) { // well, try to seek again
+               Log(LOGWARNING, "%s, server ignored seek!", __FUNCTION__);
+       }
+    }*/
+  }
+  else
+  {
+
+  }
+}
+
+//int pnum=0;
+
+bool CRTMP::FindFirstMatchingProperty(AMFObject &obj, std::string name, AMFObjectProperty &p)
+{
+       // this is a small object search to locate the "duration" property
+       for (int n=0; n<obj.GetPropertyCount(); n++) {
+               AMFObjectProperty prop = obj.GetProperty(n);
+
+               if(prop.GetPropName() == name) {
+                       
+                       p = obj.GetProperty(n);
+                       return true;
+               }
+
+               if(prop.GetType() == AMF_OBJECT) {
+                       AMFObject next = prop.GetObject();
+                       return FindFirstMatchingProperty(next, name, p);
+               }
+       }
+       return false;
+}
+
+void CRTMP::HandleMetadata(char *body, unsigned int len)
+{
+       /*Log(LOGDEBUG,"Parsing meta data: %d @0x%08X", packet.m_nBodySize, packet.m_body);
+       for(int i=0; i<packet.m_nBodySize; i++) {
+               printf("%02X ", packet.m_body[i]);
+       }
+       printf("\n");
+
+       char str[256]={0};
+       sprintf(str, "packet%d", pnum);
+       pnum++;
+       FILE *f = fopen(str, "wb");
+       fwrite(packet.m_body, 1, packet.m_nBodySize, f);
+       fclose(f);//*/
+
+       // allright we get some info here, so parse it and print it
+       // also keep duration or filesize to make a nice progress bar
+
+       //int len = packet.m_nBodySize;
+       //char *p = packet.m_body;
+
+       RTMP_LIB::AMFObject obj;
+       int nRes = obj.Decode(body, len);
+       if(nRes < 0) {
+               Log(LOGERROR, "%s, error decoding meta data packet", __FUNCTION__);
+               return;
+       }
+
+       obj.Dump();
+       std::string metastring = obj.GetProperty(0).GetString();
+
+       if(metastring == "onMetaData") {
+               AMFObjectProperty prop;
+               if(FindFirstMatchingProperty(obj, "duration", prop)) {
+                       m_fDuration = prop.GetNumber();
+                       Log(LOGDEBUG, "Set duration: %f", m_fDuration);
+               }
+       }
+}
+
+void CRTMP::HandleChangeChunkSize(const RTMPPacket &packet)
+{
+  if (packet.m_nBodySize >= 4)
+  {
+    m_chunkSize = ReadInt32(packet.m_body);
+    Log(LOGDEBUG, "%s, received: chunk size change to %d", __FUNCTION__, m_chunkSize);
+  }
+}
+
+void CRTMP::HandleAudio(const RTMPPacket &packet)
+{
+}
+
+void CRTMP::HandleVideo(const RTMPPacket &packet)
+{
+}
+
+void CRTMP::HandlePing(const RTMPPacket &packet)
+{
+  short nType = -1;
+  if (packet.m_body && packet.m_nBodySize >= sizeof(short))
+    nType = ReadInt16(packet.m_body);
+  Log(LOGDEBUG, "server sent ping. type: %d", nType);
+
+  if (nType == 0x06 && packet.m_nBodySize >= 6) // server ping. reply with pong.
+  {
+    unsigned int nTime = ReadInt32(packet.m_body + sizeof(short));
+    SendPing(0x07, nTime);
+  }
+}
+
+bool CRTMP::ReadPacket(RTMPPacket &packet)
+{
+  char type;
+  if (ReadN(&type,1) != 1)
+  {
+    Log(LOGERROR, "%s, failed to read RTMP packet header", __FUNCTION__);
+    return false;
+  } 
+
+  packet.m_headerType = (type & 0xc0) >> 6;
+  packet.m_nChannel = (type & 0x3f);
+
+  int nSize = packetSize[packet.m_headerType];
+  
+//  Log(LOGDEBUG, "%s, reading RTMP packet chunk on channel %x, headersz %i", __FUNCTION__, packet.m_nChannel, nSize);
+
+  if (nSize < RTMP_LARGE_HEADER_SIZE) // using values from the last message of this channel
+    packet = m_vecChannelsIn[packet.m_nChannel];
+  
+  nSize--;
+
+  char header[RTMP_LARGE_HEADER_SIZE] = {0};
+  if (nSize > 0 && ReadN(header,nSize) != nSize)
+  {
+    Log(LOGERROR, "%s, failed to read RTMP packet header. type: %x", __FUNCTION__, (unsigned int)type);
+    return false;
+  }
+
+  if (nSize >= 3)
+    packet.m_nInfoField1 = ReadInt24(header);
+
+  if (nSize >= 6)
+  {
+    packet.m_nBodySize = ReadInt24(header + 3);
+    packet.m_nBytesRead = 0;
+    packet.FreePacketHeader(); // new packet body
+  }
+  
+  if (nSize > 6)
+    packet.m_packetType = header[6];
+
+  if (nSize == 11)
+    packet.m_nInfoField2 = ReadInt32(header+7);
+
+  if (packet.m_nBodySize > 0 && packet.m_body == NULL && !packet.AllocPacket(packet.m_nBodySize))
+  {
+    Log(LOGDEBUG, "%s, failed to allocate packet", __FUNCTION__);
+    return false;
+  }
+
+  int nToRead = packet.m_nBodySize - packet.m_nBytesRead;
+  int nChunk = m_chunkSize;
+  if (nToRead < nChunk)
+     nChunk = nToRead;
+
+  if (ReadN(packet.m_body + packet.m_nBytesRead, nChunk) != nChunk)
+  {
+    Log(LOGERROR, "%s, failed to read RTMP packet body. len: %lu", __FUNCTION__, packet.m_nBodySize);
+    packet.m_body = NULL; // we dont want it deleted since its pointed to from the stored packets (m_vecChannelsIn)
+    return false;  
+  }
+
+  packet.m_nBytesRead += nChunk;
+
+  // keep the packet as ref for other packets on this channel
+  m_vecChannelsIn[packet.m_nChannel] = packet;
+
+  if (packet.IsReady())
+  {
+    // reset the data from the stored packet. we keep the header since we may use it later if a new packet for this channel
+    // arrives and requests to re-use some info (small packet header)
+    m_vecChannelsIn[packet.m_nChannel].m_body = NULL;
+    m_vecChannelsIn[packet.m_nChannel].m_nBytesRead = 0;
+  }
+  else
+    packet.m_body = NULL; // so it wont be erased on "free"
+
+  return true;
+}
+
+short  CRTMP::ReadInt16(const char *data)
+{
+  short val;
+  memcpy(&val,data,sizeof(short));
+  return ntohs(val);
+}
+
+int  CRTMP::ReadInt24(const char *data)
+{
+  char tmp[4] = {0};
+  memcpy(tmp+1, data, 3);
+  int val;
+  memcpy(&val, tmp, sizeof(int));
+  return ntohl(val);
+}
+
+int  CRTMP::ReadInt32(const char *data)
+{
+  int val;
+  memcpy(&val, data, sizeof(int));
+  return ntohl(val);
+}
+
+std::string CRTMP::ReadString(const char *data)
+{
+  std::string strRes;
+  short len = ReadInt16(data);
+  if (len > 0)
+  {
+    char *pStr = new char[len+1]; 
+    memset(pStr, 0, len+1);
+    memcpy(pStr, data + sizeof(short), len);
+    strRes = pStr;
+    delete [] pStr;
+  }
+  return strRes;
+}
+
+bool CRTMP::ReadBool(const char *data)
+{
+  return *data == 0x01;
+}
+
+double CRTMP::ReadNumber(const char *data)
+{
+  double val;
+  char *dPtr = (char *)&val;
+  for (int i=7;i>=0;i--)
+  {
+    *dPtr = data[i];
+    dPtr++;
+  }
+
+  return val;
+}
+
+int CRTMP::EncodeString(char *output, const std::string &strName, const std::string &strValue)
+{
+  char *buf = output;
+  short length = htons(strName.size());
+  memcpy(buf, &length, 2);
+  buf += 2;
+
+  memcpy(buf, strName.c_str(), strName.size());
+  buf += strName.size();
+  
+  buf += EncodeString(buf, strValue);
+  return buf - output;
+}
+
+int CRTMP::EncodeInt16(char *output, short nVal)
+{
+  nVal = htons(nVal);
+  memcpy(output, &nVal, sizeof(short));
+  return sizeof(short);
+}
+
+int CRTMP::EncodeInt24(char *output, int nVal)
+{
+  nVal = htonl(nVal);
+  char *ptr = (char *)&nVal;
+  ptr++;
+  memcpy(output, ptr, 3);
+  return 3;
+}
+
+int CRTMP::EncodeInt32(char *output, int nVal)
+{
+  nVal = htonl(nVal);
+  memcpy(output, &nVal, sizeof(int));
+  return sizeof(int);
+}
+
+int CRTMP::EncodeNumber(char *output, const std::string &strName, double dVal)
+{
+  char *buf = output;
+
+  unsigned short length = htons(strName.size());
+  memcpy(buf, &length, 2);
+  buf += 2;
+
+  memcpy(buf, strName.c_str(), strName.size());
+  buf += strName.size();
+
+  buf += EncodeNumber(buf, dVal);
+  return buf - output;
+}
+
+int CRTMP::EncodeBoolean(char *output, const std::string &strName, bool bVal)
+{
+  char *buf = output;
+  unsigned short length = htons(strName.size());
+  memcpy(buf, &length, 2);
+  buf += 2;
+
+  memcpy(buf, strName.c_str(), strName.size());
+  buf += strName.size();
+
+  buf += EncodeBoolean(buf, bVal);
+
+  return buf - output;
+}
+
+int CRTMP::EncodeString(char *output, const std::string &strValue)
+{
+  char *buf = output;
+  *buf = 0x02; // Datatype: String
+  buf++;
+
+  short length = htons(strValue.size());
+  memcpy(buf, &length, 2);
+  buf += 2;
+
+  memcpy(buf, strValue.c_str(), strValue.size());
+  buf += strValue.size();
+
+  return buf - output;
+}
+
+int CRTMP::EncodeNumber(char *output, double dVal)
+{
+  char *buf = output;  
+  *buf = 0x00; // type: Number
+  buf++;
+
+  char *dPtr = (char *)&dVal;
+  for (int i=7;i>=0;i--)
+  {
+    buf[i] = *dPtr;
+    dPtr++;
+  }
+
+  buf += 8;
+
+  return buf - output;
+}
+
+int CRTMP::EncodeBoolean(char *output, bool bVal)
+{
+  char *buf = output;  
+
+  *buf = 0x01; // type: Boolean
+  buf++;
+
+  *buf = bVal?0x01:0x00; 
+  buf++;
+
+  return buf - output;
+}
+
+bool CRTMP::HandShake()
+{
+  char clientsig[RTMP_SIG_SIZE+1];
+  char serversig[RTMP_SIG_SIZE];
+
+  //Log(LOGDEBUG, "HandShake: ");
+  clientsig[0] = 0x3;
+  uint32_t uptime = htonl(GetTime());
+  memcpy(clientsig + 1, &uptime, 4);
+  memset(clientsig + 5, 0, 4);
+
+  for (int i=9; i<=RTMP_SIG_SIZE; i++)
+    clientsig[i] = (char)(rand() % 256);
+
+  if (!WriteN(clientsig, RTMP_SIG_SIZE + 1))
+    return false;
+
+  char dummy;
+  if (ReadN(&dummy, 1) != 1) // 0x03
+    return false;
+
+  
+  if (ReadN(serversig, RTMP_SIG_SIZE) != RTMP_SIG_SIZE)
+    return false;
+
+  char resp[RTMP_SIG_SIZE];
+  if (ReadN(resp, RTMP_SIG_SIZE) != RTMP_SIG_SIZE)
+    return false;
+
+  bool bMatch = (memcmp(resp, clientsig + 1, RTMP_SIG_SIZE) == 0);
+  if (!bMatch)
+  {
+    Log(LOGWARNING, "%s, client signiture does not match!",__FUNCTION__);
+  }
+
+  if (!WriteN(serversig, RTMP_SIG_SIZE))
+    return false;
+
+  return true;
+}
+
+bool CRTMP::SendRTMP(RTMPPacket &packet)
+{
+  const RTMPPacket &prevPacket = m_vecChannelsOut[packet.m_nChannel];
+  if (packet.m_headerType != RTMP_PACKET_SIZE_LARGE)
+  {
+    // compress a bit by using the prev packet's attributes
+    if (prevPacket.m_nBodySize == packet.m_nBodySize && packet.m_headerType == RTMP_PACKET_SIZE_MEDIUM) 
+      packet.m_headerType = RTMP_PACKET_SIZE_SMALL;
+
+    if (prevPacket.m_nInfoField2 == packet.m_nInfoField2 && packet.m_headerType == RTMP_PACKET_SIZE_SMALL)
+      packet.m_headerType = RTMP_PACKET_SIZE_MINIMUM;
+      
+  }
+
+  if (packet.m_headerType > 3) // sanity
+  { 
+    Log(LOGERROR, "sanity failed!! tring to send header of type: 0x%02x.", (unsigned char)packet.m_headerType);
+    return false;
+  }
+
+  int nSize = packetSize[packet.m_headerType];
+  char header[RTMP_LARGE_HEADER_SIZE] = { 0 };
+  header[0] = (char)((packet.m_headerType << 6) | packet.m_nChannel);
+  if (nSize > 1)
+    EncodeInt24(header+1, packet.m_nInfoField1);
+  
+  if (nSize > 4)
+  {
+    EncodeInt24(header+4, packet.m_nBodySize);
+    header[7] = packet.m_packetType;
+  }
+
+  if (nSize > 8)
+    EncodeInt32(header+8, packet.m_nInfoField2);
+
+  if (!WriteN(header, nSize))
+  {
+    Log(LOGWARNING, "couldnt send rtmp header");
+    return false;
+  }
+
+  nSize = packet.m_nBodySize;
+  char *buffer = packet.m_body;
+
+  while (nSize)
+  {
+    int nChunkSize = packet.m_packetType == 0x14?m_chunkSize:packet.m_nBodySize;
+    if (nSize < m_chunkSize)
+      nChunkSize = nSize;
+
+    if (!WriteN(buffer, nChunkSize))
+      return false;
+
+    nSize -= nChunkSize;
+    buffer += nChunkSize;
+
+    if (nSize > 0)
+    {
+      char sep = (0xc0 | packet.m_nChannel);
+      if (!WriteN(&sep, 1))
+        return false;  
+    }
+  }
+
+  if (packet.m_packetType == 0x14) // we invoked a remote method, keep it in call queue till result arrives
+    m_methodCalls.push_back(ReadString(packet.m_body + 1));
+
+  m_vecChannelsOut[packet.m_nChannel] = packet;
+  m_vecChannelsOut[packet.m_nChannel].m_body = NULL;
+  return true;
+}
+
+void CRTMP::Close()
+{
+  if (IsConnected())
+    close(m_socket);
+
+  m_socket = 0;
+  m_chunkSize = 128;
+  m_nBWCheckCounter = 0;
+  m_nBytesIn = 0;
+  m_nBytesInSent = 0;
+
+  for (int i=0; i<64; i++)
+  {
+    m_vecChannelsIn[i].Reset();
+    m_vecChannelsIn[i].m_nChannel = i;
+    m_vecChannelsOut[i].Reset();
+    m_vecChannelsOut[i].m_nChannel = i;
+  }
+
+  m_bPlaying = false;
+  m_nBufferSize = 0;
+}
+
+bool CRTMP::FillBuffer()
+{
+  time_t now = time(NULL);
+  while (m_nBufferSize < RTMP_BUFFER_CACHE_SIZE && time(NULL) - now < 4)
+  {
+    int nBytes = recv(m_socket, m_pBuffer + m_nBufferSize, RTMP_BUFFER_CACHE_SIZE - m_nBufferSize, 0);
+    if (nBytes != -1)
+      m_nBufferSize += nBytes;
+    else
+    {
+      Log(LOGDEBUG, "%s, read buffer returned %d. errno: %d", __FUNCTION__, nBytes, errno);
+      break;
+    }
+  }
+
+  return true;
+}
diff --git a/rtmp.h b/rtmp.h
new file mode 100644 (file)
index 0000000..7394394
--- /dev/null
+++ b/rtmp.h
@@ -0,0 +1,155 @@
+#ifndef __RTMP_H__
+#define __RTMP_H__
+/*
+ *      Copyright (C) 2005-2008 Team XBMC
+ *      http://www.xbmc.org
+ *      Copyright (C) 2008-2009 Andrej Stepanchuk
+ *
+ *  This Program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2, or (at your option)
+ *  any later version.
+ *
+ *  This Program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with RTMPDump; see the file COPYING.  If not, write to
+ *  the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
+ *  http://www.gnu.org/copyleft/gpl.html
+ *
+ */
+
+#include <string>
+#include <vector>
+
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netdb.h>
+#include <arpa/inet.h>
+#include <unistd.h>
+#include <netinet/in.h>
+
+#include "AMFObject.h"
+#include "rtmppacket.h"
+
+namespace RTMP_LIB
+{
+
+typedef struct
+{
+        char hostname[256];
+        unsigned int port;
+
+        char *url;
+        char *tcUrl;
+        char *player;
+        char *pageUrl;
+        char *app;
+        char *auth;
+       char *flashVer;
+
+       double seekTime;
+} LNK;
+
+class CRTMP
+  {
+    public:
+
+      CRTMP();
+      virtual ~CRTMP();
+
+      //void SetPlayer(const std::string &strPlayer);
+      //void SetPageUrl(const std::string &strPageUrl);
+      //void SetPlayPath(const std::string &strPlayPath);
+      void SetBufferMS(int size);
+      void UpdateBufferMS();
+
+      bool Connect(char *url, char *tcUrl, char *player, char *pageUrl, char *app, char *auth, char *flashVer, double dTime);
+      bool IsConnected(); 
+      double GetDuration();
+
+      bool GetNextMediaPacket(RTMPPacket &packet);
+
+      void Close();
+
+      static int EncodeString(char *output, const std::string &strValue);
+      static int EncodeNumber(char *output, double dVal);
+      static int EncodeInt16(char *output, short nVal);
+      static int EncodeInt24(char *output, int nVal);
+      static int EncodeInt32(char *output, int nVal);
+      static int EncodeBoolean(char *output,bool bVal);
+
+      static short ReadInt16(const char *data);
+      static int  ReadInt24(const char *data);
+      static int  ReadInt32(const char *data);
+      static std::string ReadString(const char *data);
+      static bool ReadBool(const char *data);
+      static double ReadNumber(const char *data);
+
+      static bool FindFirstMatchingProperty(AMFObject &obj, std::string name, AMFObjectProperty &p);
+
+    protected:
+      bool HandShake();
+      bool Connect();
+
+      bool SendConnectPacket();
+      bool SendServerBW();
+      bool SendCheckBW();
+      bool SendCheckBWResult();
+      bool SendPing(short nType, unsigned int nObject, unsigned int nTime = 0);
+      bool SendCreateStream(double dStreamId);
+      bool SendPlay();
+      bool SendPause();
+      bool SendSeek(double dTime);
+      bool SendBytesReceived();
+
+      void HandleInvoke(const RTMPPacket &packet);
+      void HandleMetadata(char *body, unsigned int len);
+      void HandleChangeChunkSize(const RTMPPacket &packet);
+      void HandleAudio(const RTMPPacket &packet);
+      void HandleVideo(const RTMPPacket &packet);
+      void HandlePing(const RTMPPacket &packet);
+     
+      int EncodeString(char *output, const std::string &strName, const std::string &strValue);
+      int EncodeNumber(char *output, const std::string &strName, double dVal);
+      int EncodeBoolean(char *output, const std::string &strName, bool bVal);
+
+      bool SendRTMP(RTMPPacket &packet);
+
+      bool ReadPacket(RTMPPacket &packet);
+      int  ReadN(char *buffer, int n);
+      bool WriteN(const char *buffer, int n);
+
+      bool FillBuffer();
+
+      int  m_socket;
+      int  m_chunkSize;
+      int  m_nBWCheckCounter;
+      int  m_nBytesIn;
+      int  m_nBytesInSent;
+      bool m_bPlaying;
+      int  m_nBufferMS;
+
+      //std::string m_strPlayer;
+      //std::string m_strPageUrl;
+      //std::string m_strLink;
+      //std::string m_strPlayPath;
+
+      std::vector<std::string> m_methodCalls; //remote method calls queue
+
+      LNK Link;
+      char *m_pBuffer;
+      int  m_nBufferSize;
+      RTMPPacket m_vecChannelsIn[64];
+      RTMPPacket m_vecChannelsOut[64];
+
+      double m_fDuration; // duration of stream in seconds
+  };
+};
+
+#endif
+
+
diff --git a/rtmpdump.cpp b/rtmpdump.cpp
new file mode 100644 (file)
index 0000000..dcd2cf4
--- /dev/null
@@ -0,0 +1,824 @@
+/*  RTMPDump
+ *  Copyright (C) 2009 Andrej Stepanchuk
+ *
+ *  This Program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2, or (at your option)
+ *  any later version.
+ *
+ *  This Program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with RTMPDump; see the file COPYING.  If not, write to
+ *  the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
+ *  http://www.gnu.org/copyleft/gpl.html
+ *
+ */
+#include <string>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <math.h>
+#include <signal.h> // to catch Ctrl-C
+
+#include <getopt.h>
+
+#include "rtmp.h"
+#include "log.h"
+#include "AMFObject.h"
+
+using namespace RTMP_LIB;
+
+#define RTMPDUMP_VERSION       "v1.3d"
+
+#define RD_SUCCESS             0
+#define RD_FAILED              1
+#define RD_INCOMPLETE          2
+
+uint32_t nTimeStamp = 0;
+
+#ifdef _DEBUG
+uint32_t debugTS = 0;
+int pnum=0;
+#endif
+
+uint32_t nIgnoredFlvFrameCounter = 0;
+uint32_t nIgnoredFrameCounter = 0;
+#define MAX_IGNORED_FRAMES     50      
+
+int WriteStream(
+               CRTMP* rtmp, 
+               char **buf,                     // target pointer, maybe preallocated
+               unsigned int len,               // length of buffer if preallocated
+               uint32_t *tsm,                  // pointer to timestamp, will contain timestamp of last video packet returned
+               bool bNoHeader,                 // resuming mode, will not write FLV header and compare metaHeader and first kexframe
+               char *metaHeader,               // pointer to meta header (if bNoHeader == TRUE)
+               uint32_t nMetaHeaderSize,       // length of meta header, if zero meta header check omitted (if bNoHeader == TRUE)
+               char *initialFrame,             // pointer to initial keyframe (no FLV header or tagSize, raw data) (if bNoHeader == TRUE)
+               uint8_t initialFrameType,       // initial frame type (audio or video)
+               uint32_t nInitialFrameSize,     // length of initial frame in bytes, if zero initial frame check omitted (if bNoHeader == TRUE)
+               uint8_t *dataType               // whenever we get a video/audio packet we set an appropriate flag here, this will be later written to the FLV header
+       )
+{
+       char flvHeader[] = { 'F', 'L', 'V', 0x01,
+                             0x00,//5, // video + audio
+                             0x00, 0x00, 0x00, 0x09,
+                             0x00, 0x00, 0x00, 0x00 // first prevTagSize=0
+       };
+       
+       static bool bStopIgnoring = false;
+       static bool bSentHeader = false;
+       static bool bFoundKeyframe = false;
+       static bool bFoundFlvKeyframe = false;
+
+       uint32_t prevTagSize = 0;
+       RTMPPacket packet;
+
+       if(rtmp->GetNextMediaPacket(packet))
+       {
+               char *packetBody        = packet.m_body;
+               unsigned int nPacketLen = packet.m_nBodySize;
+
+               // skip video info/command packets
+               if(packet.m_packetType == 0x09 && 
+                  nPacketLen == 2 &&
+               ((*packetBody & 0xf0) == 0x50)) {
+                       return 0;
+               }
+
+               if(packet.m_packetType == 0x09 && nPacketLen <= 5) {
+                       Log(LOGWARNING, "ignoring too small video packet: size: %d", nPacketLen);
+                       return 0;
+               }
+               if(packet.m_packetType == 0x08 && nPacketLen <= 1) {
+                       Log(LOGWARNING, "ignoring too small audio packet: size: %d", nPacketLen);
+                       return 0;
+               }
+#ifdef _DEBUG
+               debugTS += packet.m_nInfoField1;
+               Log(LOGDEBUG, "type: %d, size: %d, TS: %d ms, sent TS: %d ms", packet.m_packetType, nPacketLen, debugTS, packet.m_nInfoField1);
+               if(packet.m_packetType == 0x09)
+                       Log(LOGDEBUG, "frametype: %02X", (*packetBody & 0xf0));
+#endif
+
+               // check the header if we get one
+               if(bNoHeader && packet.m_nInfoField1 == 0) {
+                       if(nMetaHeaderSize > 0 && packet.m_packetType == 0x12) {
+                       
+                               RTMP_LIB::AMFObject metaObj;
+                               int nRes = metaObj.Decode(packetBody, nPacketLen);
+                               if(nRes >= 0) {
+                                       std::string metastring = metaObj.GetProperty(0).GetString();
+
+                                       if(metastring == "onMetaData") {
+                                               // comapre
+                                               if((nMetaHeaderSize != nPacketLen) || 
+                                                  (memcmp(metaHeader, packetBody, nMetaHeaderSize) != 0)) {
+                                                       return -2;
+                                               }
+                                       }                       
+                               }
+                       }
+
+                       // check first keyframe to make sure we got the right position in the stream!
+                       // (the first non ignored frame)
+                       if(nInitialFrameSize > 0) {
+
+                               // video or audio data
+                               if(packet.m_packetType == initialFrameType && nInitialFrameSize == nPacketLen) {
+                                       // we don't compare the sizes since the packet can contain several FLV packets, just make
+                                       // sure the first frame is our keyframe (which we are going to rewrite)
+                                       if(memcmp(initialFrame, packetBody, nInitialFrameSize) == 0) {
+                                               Log(LOGDEBUG, "Checked keyframe successfully!");
+                                               bFoundKeyframe = true;
+                                               return 0; // ignore it! (what about audio data after it? it is handled by ignoring all 0ms frames, see below)
+                                       }
+                               }
+
+                               // hande FLV streams, even though the server resends the keyframe as an extra video packet
+                               // it is also included in the first FLV stream chunk and we have to compare it and
+                               // filter it out !!
+                               if(packet.m_packetType == 0x16) {
+                                       // basically we have to find the keyframe with the correct TS being nTimeStamp
+                                       unsigned int pos=0;
+                                       uint32_t ts = 0;
+                                       //bool bFound = false;
+
+                                       while(pos+11 < nPacketLen) {
+                                               uint32_t dataSize = CRTMP::ReadInt24(packetBody+pos+1); // size without header (11) and prevTagSize (4)
+                                               ts = CRTMP::ReadInt24(packetBody+pos+4);
+                                               ts |= (packetBody[pos+7]<<24);
+                                               
+                                               #ifdef _DEBUG   
+                                               Log(LOGDEBUG, "keyframe search: FLV Packet: type %02X, dataSize: %d, timeStamp: %d ms",
+                                                                                               packetBody[pos], dataSize, ts);
+                                               #endif
+                                               // ok, is it a keyframe!!!: well doesn't work for audio!
+                                               if(packetBody[0] == initialFrameType /* && (packetBody[11]&0xf0) == 0x10*/) {
+                                                       if(ts == nTimeStamp) {
+                                                               Log(LOGDEBUG, "Found keyframe with resume-keyframe timestamp!");
+                                                               if(nInitialFrameSize != dataSize || memcmp(initialFrame, packetBody+pos+11, nInitialFrameSize) != 0) {
+                                                                       Log(LOGERROR, "FLV Stream: Keyframe doesn't match!");
+                                                                       return -2;
+                                                               }
+                                                               bFoundFlvKeyframe = true;
+
+                                                               // ok, skip this packet
+                                                               // check whether skipable:
+                                                               if(pos+11+dataSize+4 > nPacketLen) {
+                                                                       Log(LOGWARNING, "Non skipable packet since it doesn't end with chunk, stream corrupt!");
+                                                                       return -2;
+                                                               }
+                                                               packetBody += (pos+11+dataSize+4);
+                                                               nPacketLen -= (pos+11+dataSize+4);
+
+                                                               goto stopKeyframeSearch;
+
+                                                       } else if(nTimeStamp < ts)
+                                                               goto stopKeyframeSearch; // the timestamp ts will only increase with further packets, wait for seek
+                                               } 
+                                               pos += (11+dataSize+4);
+                                       }
+                                       if(ts < nTimeStamp) {
+                                               Log(LOGERROR, "First packet does not contain keyframe, all timestamps are smaller than the keyframe timestamp, so probably the resume seek failed?");
+                                       }
+stopKeyframeSearch:
+                                       ;
+                                       //*
+                                       if(!bFoundFlvKeyframe) {
+                                               Log(LOGERROR, "Couldn't find the seeked keyframe in this chunk!");
+                                               return 0;//-2;
+                                       }//*/
+                               }
+                       }
+               }
+
+                // skip till we find out keyframe (seeking might put us somewhere before it)
+               if(bNoHeader && !bFoundKeyframe && packet.m_packetType != 0x16) {
+                        Log(LOGWARNING, "Stream does not start with requested frame, ignoring data... ");
+                        nIgnoredFrameCounter++;
+                        if(nIgnoredFrameCounter > MAX_IGNORED_FRAMES)
+                                return -2;
+                        return 0;
+                }
+               // ok, do the same for FLV streams
+               if(bNoHeader && !bFoundFlvKeyframe && packet.m_packetType == 0x16) {
+                        Log(LOGWARNING, "Stream does not start with requested FLV frame, ignoring data... ");
+                        nIgnoredFlvFrameCounter++;
+                        if(nIgnoredFlvFrameCounter > MAX_IGNORED_FRAMES)
+                                return -2;
+                        return 0;
+                }      
+
+               // if bNoHeader, we continue a stream, we have to ignore the 0ms frames since these are the first keyframes, we've got these
+               // so don't mess around with multiple copies sent by the server to us! (if the keyframe is found at a later position
+               // there is only one copy and it will be ignored by the preceding if clause)
+               if(!bStopIgnoring && bNoHeader && packet.m_packetType != 0x16) { // exclude type 0x16 (FLV) since it can conatin several FLV packets
+                       if(packet.m_nInfoField1 == 0) {
+                               return 0;
+                       } else {
+                               bStopIgnoring = true; // stop ignoring packets
+                       }
+               }
+
+               // calculate packet size and reallocate buffer if necessary
+               unsigned int size = nPacketLen 
+                       + ((bSentHeader || bNoHeader) ? 0 : sizeof(flvHeader))
+                       + ((packet.m_packetType == 0x08 || packet.m_packetType == 0x09 || packet.m_packetType == 0x12) ? 11 : 0)
+                       + (packet.m_packetType != 0x16 ? 4 : 0);
+               
+               if(size+4 > len) { // the extra 4 is for the case of an FLV stream without a last prevTagSize (we need extra 4 bytes to append it)
+                       *buf = (char *)realloc(*buf, size+4);
+                       if(*buf == 0) {
+                               Log(LOGERROR, "Couldn't reallocate memory!");
+                               return -1; // fatal error
+                       }
+               }
+               char *ptr = *buf;
+
+               if(!bSentHeader && !bNoHeader)
+               {
+                       memcpy(ptr, flvHeader, sizeof(flvHeader));
+                       ptr+=sizeof(flvHeader);
+
+                       bSentHeader = true;
+               }
+               // audio (0x08), video (0x09) or metadata (0x12) packets :
+               // construct 11 byte header then add rtmp packet's data
+               if(packet.m_packetType == 0x08 || packet.m_packetType == 0x09 || packet.m_packetType == 0x12)
+               {
+                       // set data type
+                       *dataType |= (((packet.m_packetType == 0x08)<<2)|(packet.m_packetType == 0x09));
+
+                       nTimeStamp += packet.m_nInfoField1;
+                       prevTagSize = 11 + nPacketLen;
+                       //nTimeStamp += packet.m_nInfoField1;
+
+                       //Log(LOGDEBUG, "%02X: Added TS: %d ms, TS: %d", packet.m_packetType, packet.m_nInfoField1, nTimeStamp);
+                       *ptr = packet.m_packetType;
+                       ptr++;
+                       ptr += CRTMP::EncodeInt24(ptr, nPacketLen);
+
+                       /*if(packet.m_packetType == 0x09) { // video
+
+                               // H264 fix:
+                               if((packetBody[0] & 0x0f) == 7) { // CodecId = H264
+                                       uint8_t packetType = *(packetBody+1);
+                                       
+                                       uint32_t ts = CRTMP::ReadInt24(packetBody+2); // composition time
+                                       int32_t cts = (ts+0xff800000)^0xff800000;
+                                       Log(LOGDEBUG, "cts  : %d\n", cts);
+
+                                       nTimeStamp -= cts;
+                                       // get rid of the composition time
+                                       CRTMP::EncodeInt24(packetBody+2, 0);
+                               }
+                               Log(LOGDEBUG, "VIDEO: nTimeStamp: 0x%08X (%d)\n", nTimeStamp, nTimeStamp);
+                       }*/
+
+                       ptr += CRTMP::EncodeInt24(ptr, nTimeStamp);
+                       *ptr = (char)((nTimeStamp & 0xFF000000) >> 24);
+                       ptr++;
+
+                       // stream id
+                       ptr += CRTMP::EncodeInt24(ptr, 0);
+               }
+
+               memcpy(ptr, packetBody, nPacketLen);
+               unsigned int len = nPacketLen;
+               
+               // correct tagSize and obtain timestamp if we have an FLV stream
+               if(packet.m_packetType == 0x16) 
+               {
+                       unsigned int pos=0;
+
+                        while(pos+11 < nPacketLen) {
+                               uint32_t dataSize = CRTMP::ReadInt24(packetBody+pos+1); // size without header (11) or prevTagSize (4)
+                                nTimeStamp = CRTMP::ReadInt24(packetBody+pos+4);
+                                nTimeStamp |= (packetBody[pos+7]<<24);
+                               
+                               // set data type
+                               *dataType |= (((*(packetBody+pos) == 0x08)<<2)|(*(packetBody+pos) == 0x09));
+
+                                if(pos+11+dataSize+4 > nPacketLen) {
+                                       Log(LOGWARNING, "No tagSize found, appending!");
+                                                
+                                       // we have to append a last tagSize!
+                                        prevTagSize = dataSize+11;
+                                        CRTMP::EncodeInt32(ptr+pos+11+dataSize, prevTagSize);
+                                        size+=4; len+=4;
+                                } else {
+                                        prevTagSize = CRTMP::ReadInt32(packetBody+pos+11+dataSize);
+                                        
+                                       #ifdef _DEBUG
+                                       Log(LOGDEBUG, "FLV Packet: type %02X, dataSize: %d, tagSize: %d, timeStamp: %d ms",
+                                                packetBody[pos], dataSize, prevTagSize, nTimeStamp);
+                                       #endif
+
+                                        if(prevTagSize != (dataSize+11)) {
+                                                #ifdef _DEBUG
+                                               Log(LOGWARNING, "tag size and data size are not consitent, writing tag size according to data size %d", dataSize+11);
+                                                #endif
+
+                                               prevTagSize = dataSize+11;
+                                                CRTMP::EncodeInt32(ptr+pos+11+dataSize, prevTagSize);
+                                        }
+                                }
+
+                                pos += (11+dataSize+4);
+                       }
+               }
+               ptr += len;
+
+               if(packet.m_packetType != 0x16) { // FLV tag packets contain their own prevTagSize
+                       CRTMP::EncodeInt32(ptr, prevTagSize);
+                       //ptr += 4;
+               }
+
+               if(tsm)
+                       *tsm = nTimeStamp;
+
+               return size;
+       }
+
+       return -1; // no more media packets
+}
+
+FILE *file = 0;
+bool bCtrlC = false;
+
+void sigIntHandler(int sig) {
+       printf("Catched signal: %d, cleaning up, just a second...\n", sig);
+       bCtrlC = true;
+       signal(SIGINT, SIG_DFL);
+}
+
+//#define _DEBUG_TEST_PLAYSTOP
+
+int main(int argc, char **argv)
+{
+//#ifdef _DEBUG_TEST_PLAYSTOP
+//     RTMPPacket packet;
+//#endif
+       int nStatus = RD_SUCCESS;
+       double percent = 0;
+       double duration = 0.0;
+
+       uint8_t dataType = 0;    // will be written into the FLV header (position 4)
+
+       bool bResume = false;    // true in resume mode
+       bool bNoHeader = false;  // in resume mode this will tell not to write an FLV header again
+       bool bAudioOnly = false; // when resuming this will tell whether its an audio only stream
+       uint32_t dSeek = 0;      // seek position in resume mode, 0 otherwise
+       uint32_t bufferTime = 10*60*60*1000; // 10 hours as default
+
+       // meta header and initial frame for the resume mode (they are read from the file and compared with
+       // the stream we are trying to continue
+       char *metaHeader = 0;
+       uint32_t nMetaHeaderSize = 0;
+       
+       // video keyframe for matching
+       char *initialFrame = 0;
+       uint32_t nInitialFrameSize = 0;
+       int initialFrameType = 0; // tye: audio or video
+
+       char *url = 0;
+       char *swfUrl = 0;
+       char *tcUrl = 0;
+       char *pageUrl = 0;
+       char *app = 0;
+       char *auth = 0;
+       char *flashVer = 0;
+
+       char *flvFile = 0;
+
+       char DEFAULT_FLASH_VER[]  = "LNX 9,0,124,0";
+
+       printf("RTMPDump %s\n", RTMPDUMP_VERSION);
+       printf("(c) 2009 Andrej Stepanchuk, license: GPL\n\n");
+
+       int opt;
+       struct option longopts[] = {
+               {"help",    0, NULL, 'h'},
+               {"rtmp",    1, NULL, 'r'},
+               {"swfUrl",  1, NULL, 's'},
+               {"tcUrl",   1, NULL, 't'},
+               {"pageUrl", 1, NULL, 'p'},
+               {"app",     1, NULL, 'a'},
+               {"auth",    1, NULL, 'u'},
+               {"flashVer",1, NULL, 'f'},
+               {"flv",     1, NULL, 'o'},
+               {"resume",  0, NULL, 'e'},
+               {0,0,0,0}
+       };
+
+       signal(SIGINT, sigIntHandler);
+
+       while((opt = getopt_long(argc, argv, "hr:s:t:p:a:f:o:u:", longopts, NULL)) != -1) {
+               switch(opt) {
+                       case 'h':
+                               printf("\nThis program dumps the media contnt streamed over rtmp.\n\n");
+                               printf("--help|-h\t\tPrints this help screen.\n");
+                               printf("--rtmp|-r url\t\tURL (e.g. rtmp//hotname[:port]/path)\n");
+                               printf("--swfUrl|-s url\t\tURL to player swf file\n");
+                               printf("--tcUrl|-t url\t\tURL to played stream\n");
+                               printf("--pageUrl|-p url\tWeb URL of played programme\n");
+                               printf("--app|-a app\t\tName of player used\n");
+                               printf("--auth|-u string\tAuthentication string to be appended to the connect string\n");
+                               printf("--flashVer|-f string\tflash version string (default: \"LNX 9,0,124,0\")\n");
+                               printf("--flv|-o string\t\tflv output file name\n\n");
+                               printf("--resume|-e\n\n");
+                               printf("If you don't pass parameters for swfUrl, tcUrl, pageUrl, app or auth these propertiews will not be included in the connect ");
+                               printf("packet.\n\n");
+                               return RD_SUCCESS;
+                       case 'r':
+                               url = optarg;
+                               break;
+                       case 's':
+                               swfUrl = optarg;
+                               break;
+                       case 't':
+                               tcUrl = optarg;
+                               break;
+                       case 'p':
+                               pageUrl = optarg;
+                               break;
+                       case 'a':
+                               app = optarg;
+                               break;
+                       case 'f':
+                               flashVer = optarg;
+                               break;
+                       case 'o':
+                               flvFile = optarg;
+                               break;
+                       case 'e':
+                               bResume = true;
+                               break;
+                       case 'u':
+                               auth = optarg;          
+                               break;
+                       default:
+                               printf("unknown option: %c\n", opt);
+                               break;
+               }
+       }
+
+       if(url == 0) {
+               printf("ERROR: You must specify a url (-r \"rtmp://host[:port]/playpath\" )\n");
+               return RD_FAILED;
+       }
+       if(flvFile == 0) {
+               printf("ERROR: You must specify an output flv file (-o filename)\n");
+               return RD_FAILED;
+       }
+
+       if(flashVer == 0)
+               flashVer = DEFAULT_FLASH_VER;
+
+       int bufferSize = 1024*1024;
+       char *buffer = (char *)malloc(bufferSize);
+        int nRead = 0;
+
+       memset(buffer, 0, bufferSize);
+
+       CRTMP  *rtmp = new CRTMP();
+
+       Log(LOGDEBUG, "Setting buffer time to: %dms", bufferTime);
+       rtmp->SetBufferMS(bufferTime);
+
+       unsigned long size = 0;
+        uint32_t timestamp = 0;
+
+       // ok, we have to get the timestamp of the last keyframe (only keyframes are seekable) / last audio frame (audio only streams) 
+       if(bResume) {
+               file = fopen(flvFile, "r+b");
+               if(file == 0) {
+                       bResume = false; // we are back in fresh file mode (otherwise finalizing file won't be done)
+                       goto start; // file does not exist, so go back into normal mode
+               }
+
+               fseek(file, 0, SEEK_END);
+               size = ftell(file);
+               fseek(file, 0, SEEK_SET);
+
+               if(size > 0) {
+                       // verify FLV format and read header 
+                       uint32_t prevTagSize = 0;
+
+                       // check we've got a valid FLV file to continue!
+                       if(fread(buffer, 1, 13, file) != 13) {
+                               Log(LOGERROR, "Couldn't read FLV file header!");
+                               nStatus = RD_FAILED;
+                               goto clean;
+                       }
+                       if(buffer[0] != 'F' || buffer[1] != 'L' || buffer[2] != 'V' || buffer[3] != 0x01) {
+                               Log(LOGERROR, "Inavlid FLV file!");
+                               nStatus = RD_FAILED;
+                                goto clean;
+                       }
+
+                       if((buffer[4]&0x05) == 0) {
+                                Log(LOGERROR, "FLV file contains neither video nor audio, aborting!");
+                               nStatus = RD_FAILED;
+                               goto clean;
+                       }
+                        bAudioOnly = (buffer[4] & 0x4) && !(buffer[4] & 0x1);
+                        if(bAudioOnly)
+                               Log(LOGDEBUG, "Resuming audio only stream!");
+       
+                       uint32_t dataOffset = RTMP_LIB::CRTMP::ReadInt32(buffer+5);
+                       fseek(file, dataOffset, SEEK_SET);
+
+                       if(fread(buffer, 1, 4, file) != 4) {
+                               Log(LOGERROR, "Invalid FLV file: missing first prevTagSize!");
+                               nStatus = RD_FAILED;
+                                goto clean;
+                       }
+                       prevTagSize = RTMP_LIB::CRTMP::ReadInt32(buffer);
+                       if(prevTagSize != 0) {
+                               Log(LOGWARNING, "First prevTagSize is not zero: prevTagSize = 0x%08X", prevTagSize);
+                       }
+
+                       // go through the file to find the mata data!
+                       uint32_t pos = dataOffset+4;
+                       bool bFoundMetaHeader = false;
+
+                       while(pos < size-4 && !bFoundMetaHeader) {
+                               fseek(file, pos, SEEK_SET);
+                               if(fread(buffer, 1, 4, file)!=4)
+                                       break;
+
+                               uint32_t dataSize = RTMP_LIB::CRTMP::ReadInt24(buffer+1);
+                               
+                               if(buffer[0] == 0x12) {
+                                       fseek(file, pos+11, SEEK_SET);
+                                       if(fread(buffer, 1, dataSize, file) != dataSize)
+                                               break;
+                                       
+                                       RTMP_LIB::AMFObject metaObj;
+                                       int nRes = metaObj.Decode(buffer, dataSize);
+                                       if(nRes < 0) {
+                                               Log(LOGERROR, "%s, error decoding meta data packet", __FUNCTION__);
+                                               break;
+                                       }
+                                       
+                                       std::string metastring = metaObj.GetProperty(0).GetString();
+
+                                       if(metastring == "onMetaData") {
+                                               metaObj.Dump();
+                                               
+                                               nMetaHeaderSize = dataSize;
+                                               metaHeader = (char *)malloc(nMetaHeaderSize);
+                                               memcpy(metaHeader, buffer, nMetaHeaderSize);
+
+                                               // get duration
+                                               AMFObjectProperty prop;
+                                               if(RTMP_LIB::CRTMP::FindFirstMatchingProperty(metaObj, "duration", prop)) {
+                                                       duration = prop.GetNumber();
+                                                       Log(LOGDEBUG, "File has duration: %f", duration);
+                                               }               
+
+                                               bFoundMetaHeader = true;
+                                               break;
+                                       }
+                                       //metaObj.Reset();
+                                       //delete obj;
+                               }
+                               pos += (dataSize+11+4);
+                       }
+
+                       if(!bFoundMetaHeader)
+                               Log(LOGWARNING, "Couldn't locate meta data!");
+
+                       //if(!bAudioOnly) // we have to handle video/video+audio different since we have non-seekable frames
+                       //{
+                               // find the last seekable frame
+                               uint32_t tsize = 0;
+
+                               // go through the file and find the last video keyframe
+                               do {
+                                       if(size-tsize < 13) {
+                                               Log(LOGERROR, "Unexpected start of file, error in tag sizes, couldn't arrive at prevTagSize=0");
+                                               nStatus = RD_FAILED; goto clean;
+                                       }
+
+                                       fseek(file, size-tsize-4, SEEK_SET);
+                                       if(fread(buffer, 1, 4, file) != 4) {
+                                               Log(LOGERROR, "Couldn't read prevTagSize from file!");
+                                               nStatus = RD_FAILED; goto clean;
+                                       }
+
+                                       prevTagSize = RTMP_LIB::CRTMP::ReadInt32(buffer);
+                                       //Log(LOGDEBUG, "Last packet: prevTagSize: %d", prevTagSize);
+                               
+                                       if(prevTagSize == 0) {
+                                               Log(LOGERROR, "Couldn't find keyframe to resume from!");
+                                               nStatus = RD_FAILED; goto clean;
+                                       }
+
+                                       if(prevTagSize < 0 || prevTagSize > size-4-13) {
+                                               Log(LOGERROR, "Last tag size must be greater/equal zero (prevTagSize=%d) and smaller then filesize, corrupt file!", prevTagSize);
+                                               nStatus = RD_FAILED; goto clean;
+                                       }
+                                       tsize += prevTagSize+4;
+
+                                       // read header
+                                       fseek(file, size-tsize, SEEK_SET);
+                                       if(fread(buffer, 1, 12, file) != 12) {
+                                               Log(LOGERROR, "Couldn't read header!");
+                                               nStatus=RD_FAILED; goto clean;
+                                       }
+                                       //*
+                                       #ifdef _DEBUG
+                                       uint32_t ts = RTMP_LIB::CRTMP::ReadInt24(buffer+4);
+                                       ts |= (buffer[7]<<24);
+                                       Log(LOGDEBUG, "%02X: TS: %d ms", buffer[0], ts);
+                                       #endif  //*/
+                               } while(
+                                               (bAudioOnly && buffer[0] != 0x08) ||
+                                               (!bAudioOnly && (buffer[0] != 0x09 || (buffer[11]&0xf0) != 0x10))
+                                       ); // as long as we don't have a keyframe / last audio frame
+               
+                               // save keyframe to compare/find position in stream
+                               initialFrameType = buffer[0];
+                               nInitialFrameSize = prevTagSize-11;
+                               initialFrame = (char *)malloc(nInitialFrameSize);
+                               
+                               fseek(file, size-tsize+11, SEEK_SET);
+                               if(fread(initialFrame, 1, nInitialFrameSize, file) != nInitialFrameSize) {
+                                       Log(LOGERROR, "Couldn't read last keyframe, aborting!");
+                                       nStatus=RD_FAILED;
+                                       goto clean;
+                               }
+
+                               dSeek = RTMP_LIB::CRTMP::ReadInt24(buffer+4); // set seek position to keyframe tmestamp
+                               dSeek |= (buffer[7]<<24);
+                       //} 
+                       //else // handle audio only, we can seek anywhere we'd like
+                       //{
+                       //}
+
+                       if(dSeek < 0) {
+                               Log(LOGERROR, "Last keyframe timestamp is negative, aborting, your file is corrupt!");
+                               nStatus=RD_FAILED;
+                               goto clean;
+                       }
+                       Log(LOGDEBUG,"Last keyframe found at: %d ms, size: %d, type: %02X", dSeek, nInitialFrameSize, initialFrameType);
+
+                       /*
+                       // now read the timestamp of the frame before the seekable keyframe:
+                       fseek(file, size-tsize-4, SEEK_SET);
+                       if(fread(buffer, 1, 4, file) != 4) {
+                               Log(LOGERROR, "Couldn't read prevTagSize from file!");
+                               goto start;
+                       }
+                       uint32_t prevTagSize = RTMP_LIB::CRTMP::ReadInt32(buffer);
+                       fseek(file, size-tsize-4-prevTagSize+4, SEEK_SET);
+                       if(fread(buffer, 1, 4, file) != 4) {
+                                Log(LOGERROR, "Couldn't read previous timestamp!");
+                                goto start;
+                        }
+                       uint32_t timestamp = RTMP_LIB::CRTMP::ReadInt24(buffer);
+                       timestamp |= (buffer[3]<<24);
+
+                       Log(LOGDEBUG, "Previuos timestamp: %d ms", timestamp);
+                       */
+
+                       // seek to position after keyframe in our file (we will ignore the keyframes resent by the server
+                       // since they are sent a couple of times and handling this would be a mess)
+                       fseek(file, size-tsize+prevTagSize+4, SEEK_SET);
+                       
+                       // make sure the WriteStream doesn't write headers and ignores all the 0ms TS packets
+                       // (including several meta data headers and the keyframe we seeked to)
+                       bNoHeader = true;
+               }
+       } else {
+start:
+               if(file != 0)
+                       fclose(file);
+
+               file = fopen(flvFile, "wb");
+               if(file == 0) {
+                        printf("Failed to open file!\n");
+                        return RD_FAILED;
+                }
+       }
+        
+       printf("Connecting to %s ...\n", url);
+/*
+#ifdef _DEBUG_TEST_PLAYSTOP
+       // DEBUG!!!! seek to end if duration known!
+       printf("duration: %f", duration);
+       //return 1;
+       if(duration > 0)
+               dSeek = (duration-5.0)*1000.0;
+#endif*/
+       if (!rtmp->Connect(url, tcUrl, swfUrl, pageUrl, app, auth, flashVer, dSeek)) {
+               printf("Failed to connect!\n");
+               return RD_FAILED;
+       }
+       printf("Connected...\n\n");
+
+/*
+       // DEBUG read out packets
+#ifdef _DEBUG_TEST_PLAYSTOP
+       printf("duration: %f", duration);
+       while(rtmp->IsConnected() && rtmp->GetNextMediaPacket(packet)) {
+               char str[256]={0};
+        sprintf(str, "packet%d", pnum);
+        pnum++;
+        FILE *f = fopen(str, "wb");
+        fwrite(packet.m_body, 1, packet.m_nBodySize, f);
+        fclose(f);
+               
+               printf(".");
+       }
+       return 1;
+#endif
+*/
+       #ifdef _DEBUG
+       debugTS = dSeek;
+       #endif
+
+       timestamp  = dSeek;
+       nTimeStamp = dSeek; // set offset if we continue        
+       if(dSeek != 0) {
+               printf("Continuing at TS: %d ms\n", nTimeStamp);
+       }
+
+       // print initial status
+       printf("Starting download at ");
+       if(duration > 0) {
+               percent = ((double)timestamp) / (duration*1000.0)*100.0;
+                percent = round(percent*10.0)/10.0;
+                printf("%.3f KB (%.1f%%)\n", (double)size/1024.0, percent);
+        } else {
+                printf("%.3f KB\n", (double)size/1024.0);
+        }
+
+       do
+       {
+               nRead = WriteStream(rtmp, &buffer, bufferSize, &timestamp, bNoHeader, metaHeader, nMetaHeaderSize, initialFrame, initialFrameType, nInitialFrameSize, &dataType);
+
+               //printf("nRead: %d\n", nRead);
+               if(nRead > 0) {
+                       fwrite(buffer, sizeof(unsigned char), nRead, file);
+                       size += nRead;
+       
+                       //printf("write %dbytes (%.1f KB)\n", nRead, nRead/1024.0);
+                       if(duration <= 0) // if duration unknown try to get it from the stream (onMetaData)
+                               duration = rtmp->GetDuration();
+
+                       if(duration > 0) {
+                               // make sure we claim to have enough buffer time!
+                               if(bufferTime < (duration*1000.0)) {
+                                       bufferTime = (uint32_t)(duration*1000.0)+5000; // extra 5sec to make sure we've got enough
+                                       
+                                       Log(LOGDEBUG, "Detected that buffer time is less than duration, resetting to: %dms", bufferTime);
+                                       rtmp->SetBufferMS(bufferTime);
+                                       rtmp->UpdateBufferMS();
+                               }
+                               percent = ((double)timestamp) / (duration*1000.0)*100.0;
+                               percent = round(percent*10.0)/10.0;
+                               printf("\r%.3f KB (%.1f%%)", (double)size/1024.0, percent);
+                       } else {
+                               printf("\r%.3f KB", (double)size/1024.0);
+                       }
+               }
+               #ifdef _DEBUG
+               else { Log(LOGDEBUG, "zero read!"); }
+               #endif
+
+       } while(!bCtrlC && nRead > -1 && rtmp->IsConnected());
+
+       if(nRead == -2) {
+               printf("Couldn't continue FLV file!\n\n");
+               nStatus = RD_FAILED;
+               goto clean;
+       }
+
+       // finalize header by writing the correct dataType
+       if(!bResume) {
+               //Log(LOGDEBUG, "Writing data type: %02X", dataType);
+               fseek(file, 4, SEEK_SET);
+               fwrite(&dataType, sizeof(unsigned char), 1, file);
+       }
+       if((duration > 0 && percent < 100.0) || bCtrlC || nRead != (-1)) {
+               Log(LOGWARNING, "Download may be incomplete, try --resume!");
+               nStatus = RD_INCOMPLETE;
+       }
+
+       fclose(file);
+
+clean:
+       printf("Closing connection... ");
+       rtmp->Close();
+       printf("done!\n\n");
+
+       return nStatus;
+}
+
diff --git a/rtmppacket.cpp b/rtmppacket.cpp
new file mode 100644 (file)
index 0000000..6053874
--- /dev/null
@@ -0,0 +1,84 @@
+/*
+ *      Copyright (C) 2005-2008 Team XBMC
+ *      http://www.xbmc.org
+ *      Copyright (C) 2008-2009 Andrej Stepanchuk
+ *
+ *  This Program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2, or (at your option)
+ *  any later version.
+ *
+ *  This Program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with RTMPDump; see the file COPYING.  If not, write to
+ *  the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
+ *  http://www.gnu.org/copyleft/gpl.html
+ *
+ */
+
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <math.h>
+#include <arpa/inet.h>
+
+#include "rtmppacket.h"
+#include "log.h"
+
+using namespace RTMP_LIB;
+
+RTMPPacket::RTMPPacket()
+{
+  Reset();
+}
+
+RTMPPacket::~RTMPPacket()
+{
+  FreePacket();
+}
+
+void RTMPPacket::Reset()
+{
+  m_headerType = 0;
+  m_packetType = 0;
+  m_nChannel = 0;
+  m_nInfoField1 = 0; 
+  m_nInfoField2 = 0; 
+  m_nBodySize = 0;
+  m_nBytesRead = 0;
+  m_nInternalTimestamp = 0;
+  m_body = NULL;
+}
+
+bool RTMPPacket::AllocPacket(int nSize)
+{
+  m_body = new char[nSize];
+  if (!m_body)
+    return false;
+  memset(m_body,0,nSize);
+  m_nBytesRead = 0;
+  return true;
+}
+
+void RTMPPacket::FreePacket()
+{
+  FreePacketHeader();
+  Reset();
+}
+
+void RTMPPacket::FreePacketHeader()
+{
+  if (m_body)
+    delete [] m_body;
+  m_body = NULL;
+}
+
+void RTMPPacket::Dump()
+{
+  Log(LOGDEBUG,"RTMP PACKET: packet type: 0x%02x. channel: 0x%02x. info 1: %d info 2: %d. Body size: %lu. body: 0x%02x", m_packetType, m_nChannel,
+           m_nInfoField1, m_nInfoField2, m_nBodySize, m_body?(unsigned char)m_body[0]:0);
+}
diff --git a/rtmppacket.h b/rtmppacket.h
new file mode 100644 (file)
index 0000000..12a3ea1
--- /dev/null
@@ -0,0 +1,63 @@
+#ifndef __RTMP_PACKET__
+#define __RTMP_PACKET__
+/*
+ *      Copyright (C) 2005-2008 Team XBMC
+ *      http://www.xbmc.org
+ *     Copyright (C) 2008-2009 Andrej Stepanchuk
+ *
+ *  This Program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2, or (at your option)
+ *  any later version.
+ *
+ *  This Program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with RTMPDump; see the file COPYING.  If not, write to
+ *  the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
+ *  http://www.gnu.org/copyleft/gpl.html
+ *
+ */
+
+#include <string>
+
+#define RTMP_PACKET_TYPE_AUDIO 0x08
+#define RTMP_PACKET_TYPE_VIDEO 0x09
+#define RTMP_PACKET_TYPE_INFO  0x12
+
+#define RTMP_MAX_HEADER_SIZE 12
+
+typedef unsigned char BYTE;
+
+namespace RTMP_LIB
+{
+  class RTMPPacket
+  {
+    public:
+      RTMPPacket();
+      virtual ~RTMPPacket();
+
+      void Reset();
+      bool AllocPacket(int nSize);
+      void FreePacket();
+      void FreePacketHeader();
+      
+      inline bool IsReady() { return m_nBytesRead == m_nBodySize; }
+      void Dump();
+
+      BYTE     m_headerType;
+      BYTE     m_packetType;
+      BYTE     m_nChannel;
+      int32_t  m_nInfoField1; // 3 first bytes
+      int32_t  m_nInfoField2; // last 4 bytes in a long header
+      uint32_t m_nBodySize;
+      uint32_t m_nBytesRead;
+      uint32_t m_nInternalTimestamp;
+      char     *m_body;
+  };
+};
+
+#endif