--- /dev/null
+/*
+ * 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();
+}
+
--- /dev/null
+#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
--- /dev/null
+ 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.
--- /dev/null
+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
+
--- /dev/null
+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
+
--- /dev/null
+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.
+
--- /dev/null
+#!/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;
+
--- /dev/null
+#!/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&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 & Warwickshire',
+ 'bbc_radio_hereford_worcester' => 'radio|BBC Hereford & 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">
+ # <p>
+ # <a href="http://www.bbc.co.uk/iplayer/episode/b0088jgs?src=a_syn30">
+ # <img src="http://www.bbc.co.uk/iplayer/images/episode/b0088jgs_150_84.jpg" alt="Bargain Hunt: Series 18: Oswestry" />
+ # </a>
+ # </p>
+ # <p>
+ # The teams are at an antiques fair in Oswestry showground. Hosted by Tim Wonnacott.
+ # </p>
+ # </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’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&DF=0">Episode one</a><br>The Sun in a Bottle</li>
+ # <li><a title="Play" href="?vodcrid=crid://itv.com/994&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&G=10&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&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&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&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&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&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&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&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&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&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&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 '&' with '&' and append '%20HTTP/1.1'
+ $url_2 =~ s/&/&/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 '&' with '&' in url
+ $url_3 =~ s/&/&/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">
+ # <p>
+ # <a href="http://www.bbc.co.uk/iplayer/episode/b00djtfh?src=a_syn30">
+ # <img src="http://www.bbc.co.uk/iplayer/images/episode/b00djtfh_150_84.jpg" alt="Edith Bowman: 22/09/2008" />
+ # </a>
+ # </p>
+ # <p>
+ # Sara Cox sits in for Edith with movie reviews and great new music, plus another Cryptic Randomizer.
+ # </p>
+ # </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 & 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&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;
+}
+
--- /dev/null
+#!/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 & Warwickshire',
+ 'bbc_radio_hereford_worcester' => 'radio|BBC Hereford & 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">
+ # <p>
+ # <a href="http://www.bbc.co.uk/iplayer/episode/b0088jgs?src=a_syn30">
+ # <img src="http://www.bbc.co.uk/iplayer/images/episode/b0088jgs_150_84.jpg" alt="Bargain Hunt: Series 18: Oswestry" />
+ # </a>
+ # </p>
+ # <p>
+ # The teams are at an antiques fair in Oswestry showground. Hosted by Tim Wonnacott.
+ # </p>
+ # </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’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&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&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&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&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&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&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&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 '&' with '&' and append '%20HTTP/1.1'
+ $url_2 =~ s/&/&/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 '&' with '&' in url
+ $url_3 =~ s/&/&/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">
+ # <p>
+ # <a href="http://www.bbc.co.uk/iplayer/episode/b00djtfh?src=a_syn30">
+ # <img src="http://www.bbc.co.uk/iplayer/images/episode/b00djtfh_150_84.jpg" alt="Edith Bowman: 22/09/2008" />
+ # </a>
+ # </p>
+ # <p>
+ # Sara Cox sits in for Edith with movie reviews and great new music, plus another Cryptic Randomizer.
+ # </p>
+ # </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 & 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&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;
+}
+
--- /dev/null
+/* 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);
+}
+
--- /dev/null
+/* 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
+
--- /dev/null
+/*
+ * 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;
+}
--- /dev/null
+#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
+
+
--- /dev/null
+/* 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, ×tamp, 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;
+}
+
--- /dev/null
+/*
+ * 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);
+}
--- /dev/null
+#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