[buildsys-build-rpmfusion/el7] rebuild for kernel 3.10.0-693.el7
by Nicolas Chauvet
commit d74bd2262ba0fce0112373dc8f843eb699326b73
Author: Nicolas Chauvet <kwizart(a)gmail.com>
Date: Sat Sep 16 14:10:32 2017 +0200
rebuild for kernel 3.10.0-693.el7
buildsys-build-rpmfusion-kerneldevpkgs-current | 8 ++++----
buildsys-build-rpmfusion.spec | 5 ++++-
2 files changed, 8 insertions(+), 5 deletions(-)
---
diff --git a/buildsys-build-rpmfusion-kerneldevpkgs-current b/buildsys-build-rpmfusion-kerneldevpkgs-current
index 545a5ac..374d0db 100644
--- a/buildsys-build-rpmfusion-kerneldevpkgs-current
+++ b/buildsys-build-rpmfusion-kerneldevpkgs-current
@@ -1,4 +1,4 @@
-3.10.0-514.el7
-3.10.0-514.el7smp
-3.10.0-514.el7PAE
-3.10.0-514.el7lpae
+3.10.0-693.el7
+3.10.0-693.el7smp
+3.10.0-693.el7PAE
+3.10.0-693.el7lpae
diff --git a/buildsys-build-rpmfusion.spec b/buildsys-build-rpmfusion.spec
index f8c08e4..a62c1f4 100644
--- a/buildsys-build-rpmfusion.spec
+++ b/buildsys-build-rpmfusion.spec
@@ -3,7 +3,7 @@
Name: buildsys-build-%{repo}
Epoch: 11
Version: 20
-Release: 102
+Release: 103
Summary: Tools and files used by the %{repo} buildsys
Group: Development/Tools
@@ -92,6 +92,9 @@ rm -rf $RPM_BUILD_ROOT
%changelog
+* Sat Sep 16 2017 Nicolas Chauvet <kwizart(a)gmail.com> - 11:20-103
+- rebuild for kernel 3.10.0-693.el7
+
* Tue Jun 27 2017 Nicolas Chauvet <kwizart(a)gmail.com> - 11:20-102
- Rebuilt for multilibs
7 years, 2 months
[mythtv] Bump release.
by Richard Shaw
commit bba2c350646684b395423f750fda288d122cb9bc
Author: Richard Shaw <hobbes1069(a)gmail.com>
Date: Sat Sep 16 06:54:34 2017 -0500
Bump release.
mythtv.spec | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
---
diff --git a/mythtv.spec b/mythtv.spec
index 4de338d..22967ee 100644
--- a/mythtv.spec
+++ b/mythtv.spec
@@ -81,7 +81,7 @@ Version: 0.28.1
%if "%{branch}" == "master"
Release: 0.5.git.%{_gitrev}%{?dist}
%else
-Release: 7%{?dist}
+Release: 8%{?dist}
%endif
# The primary license is GPLv2+, but bits are borrowed from a number of
@@ -1358,7 +1358,7 @@ exit 0
%changelog
-* Wed Sep 6 2017 Richard Shaw <hobbes1069(a)gmail.com> - 0.28.1-7
+* Wed Sep 6 2017 Richard Shaw <hobbes1069(a)gmail.com> - 0.28.1-8
- Update to latest fixes/0/28, v0.28.1-45-g73cf7474ad.
* Sun Aug 6 2017 Richard Shaw <hobbes1069(a)gmail.com> - 0.28.1-6
7 years, 2 months
[mythtv] Update to latest fixes/0/28, v0.28.1-45-g73cf7474ad.
by Richard Shaw
commit cf63d2037e4d1f0a703cb6a40eadc1f074b11e8e
Author: Richard Shaw <hobbes1069(a)gmail.com>
Date: Sat Sep 16 06:53:36 2017 -0500
Update to latest fixes/0/28, v0.28.1-45-g73cf7474ad.
ChangeLog | 82 +
mythtv-0.28-fixes.patch | 6285 ++++++++++++++++++++++++++++++++++++++++++++++-
mythtv.spec | 7 +-
3 files changed, 6320 insertions(+), 54 deletions(-)
---
diff --git a/ChangeLog b/ChangeLog
index 5399400..91b8ac2 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,85 @@
+commit 73cf7474ad38ee2edddfeb7540eb5b7f28b245f4
+Author: Peter Bennett <pbennett(a)mythtv.org>
+Date: Thu Aug 31 14:52:55 2017 -0400
+
+ Fix OpenGL bug introduced recently
+
+ commit ad3fc1b88c used an #ifdef instead of #if, resulting in
+ OpenGL being disabled for all instead of just for raspberry pi.
+
+ Refs #13110
+
+commit ebd69ec33b67df52628eedcb1dca647306cfc887
+Author: Peter Bennett <pbennett(a)mythtv.org>
+Date: Tue Aug 29 16:09:54 2017 -0400
+
+ Raspberry Pi: Support for Raspbian Stretch (fixes/0.28)
+
+ This is slightly different from the commit for master and fixes/29.
+
+ * some libraries in /opt/vc/lib were renamed
+ * QT version is now 5.7.1
+ * Incorrect code that was based on a custom QT compile was being activated
+ * Invalid parameters for creating textures with OpenGL ES
+ * QT XCB GL Integration caused a segfault because of incompatible GL libraries.
+
+ Fixes #13110
+
+commit ff9002adc9ef4dd58d54c66e9f85fa6a9e4edb41
+Author: David Engel <dengel(a)mythtv.org>
+Date: Sun Aug 27 15:31:43 2017 -0500
+
+ Fix invocation of Previously Recorded from Recording Rules.
+
+ When the Schedule Editor is entered from Recording Rules, there is no
+ ProgramInfo to use. Fix it calling Previously Recorded with the
+ recording rule ID and title.
+
+ Refs #13112
+
+ (cherry picked from commit a60e8c63bb185abc0741827349957f4d60222cc8)
+
+commit 204bb2e5345290ae5b112e8fc9381744339813b2
+Author: Mark Spieth <mark(a)digivation.com.au>
+Date: Sat Aug 26 13:32:40 2017 -0400
+
+ ttvdb: Update to support new JSON API.
+
+ The old xml ttvdb api will be terminated on September 30th 2017.
+
+ Consolidated from multiple commits.
+
+ Fixes #13084
+
+ * Update ttdvb to use V2 API
+ * Update python to 2/3 compatability
+ * Update tvdb_api with upstream 2.0-dev
+ * Fix some incorrect exception formatting in ttvdb.py
+
+ * Upstream support for partial language data
+ * Upstream handle loadurl errors by callers
+ * Upstream support no cache and non-conforming cache
+ * Upstream tolerate missing cache _ignored_parameters attribute
+ * Remove username and userkey from auth info, not required, less brittle
+ * python 3.6 support for num-seasons output order
+
+ * ttvdb fix output of more commands
+ * -l de -m -a US -D 72449 1 10
+ * -l en -a US -D 281053
+
+ * Handle empty cast
+ * ttvdb: Add more detailed episode data
+ * ttvdb.py Handle no actors in returned data
+ * ttvdb: add old requests_cache version compatability
+ * ttvdb Fix User-Agent to a specific one, fixes auth 403
+ * ttvdb Restore original api key
+ * ttvdb Fix up python3 compatability
+ * ttvdb fix all error outputs to stderr
+ * ttvdb: make doctest a bit less brittle with changing data
+
+ Signed-off-by: Peter Bennett <pbennett(a)mythtv.org>
+ (cherry picked from commit e60c0283eda6f2e094a0b002eaea72f0ba06455b)
+
commit 2c4c711b1ff47e3a3c39fd830e181fd6582f4887
Author: Peter Bennett <pbennett(a)mythtv.org>
Date: Sat Aug 12 16:16:58 2017 -0400
diff --git a/mythtv-0.28-fixes.patch b/mythtv-0.28-fixes.patch
index 156a406..292ed01 100644
--- a/mythtv-0.28-fixes.patch
+++ b/mythtv-0.28-fixes.patch
@@ -1,45 +1,87 @@
- .../mytharchive/mytharchive/thumbfinder.cpp | 8 +
- mythtv/bindings/perl/MythTV.pm | 2 +
- mythtv/bindings/python/MythTV/dataheap.py | 2 +-
- mythtv/configure | 15 +-
- mythtv/external/FFmpeg/libavutil/bswap.h | 15 --
- mythtv/libs/libmyth/programinfo.cpp | 2 +-
- mythtv/libs/libmythbase/lcddevice.cpp | 10 +-
- mythtv/libs/libmythbase/lcddevice.h | 5 +-
- mythtv/libs/libmythbase/loggingserver.cpp | 2 +-
- mythtv/libs/libmythtv/deletemap.cpp | 3 +-
- mythtv/libs/libmythtv/eitfixup.cpp | 2 +
- mythtv/libs/libmythtv/eithelper.cpp | 17 +-
- mythtv/libs/libmythtv/iptvtuningdata.h | 2 +-
- mythtv/libs/libmythtv/mythavutil.cpp | 15 +-
- mythtv/libs/libmythtv/privatedecoder_omx.cpp | 96 ++--------
- mythtv/libs/libmythtv/privatedecoder_omx.h | 4 -
- .../libs/libmythtv/recorders/ExternalChannel.cpp | 5 +
- mythtv/libs/libmythtv/recorders/ExternalChannel.h | 1 +
- mythtv/libs/libmythtv/tv_play.cpp | 31 +++-
- mythtv/libs/libmythtv/videoout_omx.cpp | 24 ++-
- mythtv/libs/libmythtv/videoout_omx.h | 1 +
- mythtv/libs/libmythtv/videoout_opengl.cpp | 19 +-
- mythtv/libs/libmythtv/videooutwindow.cpp | 8 +
- mythtv/libs/libmythtv/videooutwindow.h | 1 +
- mythtv/libs/libmythtv/videosource.cpp | 4 +
- mythtv/libs/libmythui/mythmainwindow.cpp | 4 +-
- mythtv/libs/libmythui/mythrender_opengl.h | 3 -
- mythtv/libs/libmythui/mythrender_opengl1.h | 3 +
- mythtv/libs/libmythupnp/mythxmlclient.cpp | 4 +-
- mythtv/libs/libmythupnp/soapclient.cpp | 4 +-
- mythtv/libs/libmythupnp/ssdp.cpp | 4 +
- mythtv/libs/libmythupnp/upnp.h | 1 +
- mythtv/programs/mythbackend/scheduler.cpp | 14 +-
- mythtv/programs/mythbackend/services/content.cpp | 4 +-
- mythtv/programs/mythbackend/services/dvr.cpp | 2 +-
- mythtv/programs/mythbackend/services/guide.cpp | 18 +-
- mythtv/programs/mythbackend/services/myth.cpp | 2 +-
- mythtv/programs/mythfilldatabase/channeldata.cpp | 39 +++-
- mythtv/programs/mythfilldatabase/channeldata.h | 6 +-
- mythtv/programs/mythfilldatabase/xmltvparser.cpp | 205 ++++++++++++---------
- mythtv/programs/mythfrontend/proglist.cpp | 4 +-
- 41 files changed, 349 insertions(+), 262 deletions(-)
+ .../mytharchive/mytharchive/thumbfinder.cpp | 8 +
+ mythtv/bindings/perl/MythTV.pm | 2 +
+ mythtv/bindings/python/MythTV/__init__.py | 22 +-
+ mythtv/bindings/python/MythTV/_conn_mysqldb.py | 6 +-
+ mythtv/bindings/python/MythTV/_conn_oursql.py | 4 +-
+ mythtv/bindings/python/MythTV/altdict.py | 9 +-
+ mythtv/bindings/python/MythTV/connections.py | 48 +-
+ mythtv/bindings/python/MythTV/database.py | 17 +-
+ mythtv/bindings/python/MythTV/dataheap.py | 42 +-
+ mythtv/bindings/python/MythTV/logging.py | 12 +-
+ mythtv/bindings/python/MythTV/methodheap.py | 14 +-
+ mythtv/bindings/python/MythTV/msearch.py | 2 +-
+ mythtv/bindings/python/MythTV/mythproto.py | 5 +-
+ .../python/MythTV/ttvdb/XSLT/tvdbCollection.xsl | 127 +-
+ .../python/MythTV/ttvdb/XSLT/tvdbQuery.xsl | 18 +-
+ .../python/MythTV/ttvdb/XSLT/tvdbVideo.xsl | 87 +-
+ mythtv/bindings/python/MythTV/ttvdb/cache.py | 230 ----
+ .../MythTV/ttvdb/requests_cache_compatability.py | 44 +
+ mythtv/bindings/python/MythTV/ttvdb/tvdbXslt.py | 108 +-
+ mythtv/bindings/python/MythTV/ttvdb/tvdb_api.py | 1186 ++++++++++++-------
+ .../python/MythTV/ttvdb/tvdb_create_key.py | 36 +
+ .../python/MythTV/ttvdb/tvdb_exceptions.py | 48 +-
+ mythtv/bindings/python/MythTV/ttvdb/tvdb_ui.py | 121 +-
+ mythtv/bindings/python/MythTV/utility/__init__.py | 18 +-
+ mythtv/bindings/python/MythTV/utility/altdict.py | 6 +-
+ .../bindings/python/MythTV/utility/dequebuffer.py | 13 +-
+ mythtv/bindings/python/MythTV/utility/dicttoxml.py | 400 +++++++
+ mythtv/bindings/python/MythTV/utility/dt.py | 6 +-
+ mythtv/bindings/python/MythTV/utility/enum.py | 8 +-
+ mythtv/bindings/python/MythTV/utility/other.py | 40 +-
+ mythtv/configure | 15 +-
+ mythtv/external/FFmpeg/libavutil/bswap.h | 15 -
+ mythtv/libs/libmyth/programinfo.cpp | 2 +-
+ mythtv/libs/libmythbase/lcddevice.cpp | 10 +-
+ mythtv/libs/libmythbase/lcddevice.h | 5 +-
+ mythtv/libs/libmythbase/loggingserver.cpp | 2 +-
+ mythtv/libs/libmythtv/deletemap.cpp | 3 +-
+ mythtv/libs/libmythtv/eitfixup.cpp | 2 +
+ mythtv/libs/libmythtv/eithelper.cpp | 17 +-
+ mythtv/libs/libmythtv/iptvtuningdata.h | 2 +-
+ mythtv/libs/libmythtv/libmythtv.pro | 11 +
+ mythtv/libs/libmythtv/mythavutil.cpp | 15 +-
+ mythtv/libs/libmythtv/privatedecoder_omx.cpp | 96 +-
+ mythtv/libs/libmythtv/privatedecoder_omx.h | 4 -
+ .../libs/libmythtv/recorders/ExternalChannel.cpp | 5 +
+ mythtv/libs/libmythtv/recorders/ExternalChannel.h | 1 +
+ mythtv/libs/libmythtv/tv_play.cpp | 31 +-
+ mythtv/libs/libmythtv/videoout_omx.cpp | 27 +-
+ mythtv/libs/libmythtv/videoout_omx.h | 1 +
+ mythtv/libs/libmythtv/videoout_opengl.cpp | 19 +-
+ mythtv/libs/libmythtv/videooutwindow.cpp | 8 +
+ mythtv/libs/libmythtv/videooutwindow.h | 1 +
+ mythtv/libs/libmythtv/videosource.cpp | 4 +
+ mythtv/libs/libmythui/libmythui.pro | 13 +
+ mythtv/libs/libmythui/mythmainwindow.cpp | 4 +-
+ mythtv/libs/libmythui/mythrender_opengl.cpp | 6 +
+ mythtv/libs/libmythui/mythrender_opengl.h | 19 +-
+ mythtv/libs/libmythui/mythrender_opengl1.h | 3 +
+ mythtv/libs/libmythui/mythrender_opengl2.cpp | 13 +
+ mythtv/libs/libmythupnp/mythxmlclient.cpp | 4 +-
+ mythtv/libs/libmythupnp/soapclient.cpp | 4 +-
+ mythtv/libs/libmythupnp/ssdp.cpp | 4 +
+ mythtv/libs/libmythupnp/upnp.h | 1 +
+ mythtv/programs/mythavtest/main.cpp | 5 +
+ mythtv/programs/mythbackend/scheduler.cpp | 14 +-
+ mythtv/programs/mythbackend/services/content.cpp | 4 +-
+ mythtv/programs/mythbackend/services/dvr.cpp | 2 +-
+ mythtv/programs/mythbackend/services/guide.cpp | 18 +-
+ mythtv/programs/mythbackend/services/myth.cpp | 2 +-
+ mythtv/programs/mythfilldatabase/channeldata.cpp | 39 +-
+ mythtv/programs/mythfilldatabase/channeldata.h | 6 +-
+ mythtv/programs/mythfilldatabase/xmltvparser.cpp | 205 ++--
+ mythtv/programs/mythfrontend/main.cpp | 4 +
+ mythtv/programs/mythfrontend/mythfrontend.pro | 25 +-
+ mythtv/programs/mythfrontend/proglist.cpp | 4 +-
+ mythtv/programs/mythfrontend/schedulecommon.cpp | 11 +-
+ mythtv/programs/mythfrontend/schedulecommon.h | 1 +
+ mythtv/programs/mythfrontend/scheduleeditor.cpp | 3 +-
+ mythtv/programs/mythscreenwizard/main.cpp | 5 +
+ mythtv/programs/mythtv-setup/main.cpp | 4 +
+ mythtv/programs/mythwelcome/main.cpp | 4 +
+ .../programs/scripts/metadata/Television/ttvdb.py | 1228 ++++++++++++++++----
+ .../scripts/metadata/Television/tvdb_test.conf | 7 +
+ 83 files changed, 3093 insertions(+), 1552 deletions(-)
diff --git a/mythplugins/mytharchive/mytharchive/thumbfinder.cpp b/mythplugins/mytharchive/mytharchive/thumbfinder.cpp
index d2bcdf56d1..25b3829237 100644
@@ -80,11 +122,423 @@ index 188fcc0080..db196ecdd2 100644
$self->{'dbh'}->do("SET time_zone = 'Etc/UTC'")
or die "Can't set timezone: $!\n\n";
+diff --git a/mythtv/bindings/python/MythTV/__init__.py b/mythtv/bindings/python/MythTV/__init__.py
+index 5f3b857698..81fda6a881 100644
+--- a/mythtv/bindings/python/MythTV/__init__.py
++++ b/mythtv/bindings/python/MythTV/__init__.py
+@@ -29,17 +29,17 @@ __all__ = ['static', 'MSearch', 'MythLog', 'StorageGroup']\
+ +__all_data__\
+ +__all_method__
+
+-import static
+-from exceptions import *
+-from logging import *
+-from msearch import *
+-from utility import *
+-from connections import dbmodule
+-from database import *
+-from system import *
+-from mythproto import *
+-from dataheap import *
+-from methodheap import *
++from . import static
++from .exceptions import *
++from .logging import *
++from .msearch import *
++from .utility import *
++from .connections import dbmodule
++from .database import *
++from .system import *
++from .mythproto import *
++from .dataheap import *
++from .methodheap import *
+
+
+ __version__ = OWN_VERSION
+diff --git a/mythtv/bindings/python/MythTV/_conn_mysqldb.py b/mythtv/bindings/python/MythTV/_conn_mysqldb.py
+index 42f368acff..63cc3c7425 100644
+--- a/mythtv/bindings/python/MythTV/_conn_mysqldb.py
++++ b/mythtv/bindings/python/MythTV/_conn_mysqldb.py
+@@ -44,7 +44,7 @@ class LoggedCursor( MySQLdb.cursors.Cursor ):
+ def _sanitize(self, query): return query.replace('?', '%s')
+
+ def log_query(self, query, args):
+- self.log(self.log.DATABASE, MythLog.DEBUG,
++ self.log(self.log.DATABASE, MythLog.DEBUG,
+ ' '.join(query.split()), str(args))
+
+ def execute(self, query, args=None):
+@@ -67,7 +67,7 @@ class LoggedCursor( MySQLdb.cursors.Cursor ):
+ if args is None:
+ return super(LoggedCursor, self).execute(query)
+ return super(LoggedCursor, self).execute(query, args)
+- except Exception, e:
++ except Exception as e:
+ raise MythDBError(MythDBError.DB_RAW, e.args)
+
+ def executemany(self, query, args):
+@@ -92,7 +92,7 @@ class LoggedCursor( MySQLdb.cursors.Cursor ):
+ self.log_query(query, args)
+ try:
+ return super(LoggedCursor, self).executemany(query, args)
+- except Exception, e:
++ except Exception as e:
+ raise MythDBError(MythDBError.DB_RAW, e.args)
+
+ def commit(self): self._get_db().commit()
+diff --git a/mythtv/bindings/python/MythTV/_conn_oursql.py b/mythtv/bindings/python/MythTV/_conn_oursql.py
+index e92f3f0f77..7f78c89867 100644
+--- a/mythtv/bindings/python/MythTV/_conn_oursql.py
++++ b/mythtv/bindings/python/MythTV/_conn_oursql.py
+@@ -54,7 +54,7 @@ class LoggedCursor( oursql.Cursor ):
+ if args:
+ return super(LoggedCursor, self).execute(query, args)
+ return super(LoggedCursor, self).execute(query)
+- except Exception, e:
++ except Exception as e:
+ raise MythDBError(MythDBError.DB_RAW, e.args)
+
+ def executemany(self, query, args):
+@@ -74,7 +74,7 @@ class LoggedCursor( oursql.Cursor ):
+ self.log_query(query, args)
+ try:
+ return super(LoggedCursor, self).executemany(query, args)
+- except Exception, e:
++ except Exception as e:
+ raise MythDBError(MythDBError.DB_RAW, e.args)
+
+ def commit(self): self.connection.commit()
+diff --git a/mythtv/bindings/python/MythTV/altdict.py b/mythtv/bindings/python/MythTV/altdict.py
+index 0b326d7bcf..337748a4e8 100644
+--- a/mythtv/bindings/python/MythTV/altdict.py
++++ b/mythtv/bindings/python/MythTV/altdict.py
+@@ -4,7 +4,8 @@
+ from MythTV.exceptions import MythError
+ from MythTV.utility import datetime
+
+-from itertools import imap, izip
++from builtins import map as imap
++from builtins import zip as izip
+ from datetime import date
+ import locale
+
+@@ -182,7 +183,7 @@ class DictData( OrdDict ):
+ field_order = self._field_order
+ dict.update(self, zip(field_order, [None]*len(field_order)))
+
+- def copy(self):
++ def copy(self):
+ """Returns a deep copy of itself."""
+ return self.__class__(zip(self.iteritems()), _process=False)
+
+@@ -192,7 +193,7 @@ class DictData( OrdDict ):
+ def __setstate__(self, state):
+ for k,v in state.iteritems():
+ self[k] = v
+-
++
+
+ class DictInvert(dict):
+ """
+@@ -204,7 +205,7 @@ class DictInvert(dict):
+ def __init__(self, other, mine=None):
+ self.other = other
+ if mine is None:
+- mine = dict(zip(*reversed(zip(*other.items()))))
++ mine = dict(zip(*reversed(list(zip(*other.items())))))
+ dict.__init__(self, mine)
+
+ @classmethod
+diff --git a/mythtv/bindings/python/MythTV/connections.py b/mythtv/bindings/python/MythTV/connections.py
+index 9cc48a4802..e541d2cca1 100644
+--- a/mythtv/bindings/python/MythTV/connections.py
++++ b/mythtv/bindings/python/MythTV/connections.py
+@@ -10,22 +10,32 @@ from MythTV.utility import deadlinesocket
+
+ from time import sleep, time
+ from select import select
+-from thread import start_new_thread, allocate_lock, get_ident
++try:
++ from thread import start_new_thread, allocate_lock, get_ident
++except ImportError:
++ from _thread import start_new_thread, allocate_lock, get_ident
+ import lxml.etree as etree
+ import weakref
+-import urllib2
++try:
++ import urllib2
++except ImportError:
++ import urllib.request as urllib2
+ import socket
+-import Queue
++try:
++ import Queue
++except ImportError:
++ import queue as Queue
+ import json
+ import re
++from builtins import str
+
+ try:
+- import _conn_oursql as dbmodule
+- from _conn_oursql import LoggedCursor
++ from . import _conn_oursql as dbmodule
++ from ._conn_oursql import LoggedCursor
+ except:
+ try:
+- import _conn_mysqldb as dbmodule
+- from _conn_mysqldb import LoggedCursor
++ from . import _conn_mysqldb as dbmodule
++ from ._conn_mysqldb import LoggedCursor
+ except:
+ raise MythError("No viable database module found.")
+
+@@ -198,7 +208,7 @@ class BEConnection( object ):
+
+ try:
+ self.connect()
+- except socket.error, e:
++ except socket.error as e:
+ self.log.logTB(MythLog.SOCKET)
+ self.connected = False
+ self.log(MythLog.GENERAL, MythLog.CRIT,
+@@ -273,7 +283,7 @@ class BEConnection( object ):
+ obj.backendCommand(data=None, timeout=None) -> response string
+
+ Sends a formatted command via a socket to the mythbackend. 'timeout'
+- will override the default timeout given when the object was
++ will override the default timeout given when the object was
+ created. If 'data' is None, the method will return any events
+ in the receive buffer.
+ """
+@@ -303,12 +313,12 @@ class BEConnection( object ):
+
+ # convert to unicode
+ try:
+- res = unicode(''.join([res]), 'utf8')
++ res = str(''.join([res]), 'utf8')
+ except:
+ res = u''.join([res])
+
+ return res
+- except MythError, e:
++ except MythError as e:
+ if e.sockcode == 54:
+ # remote has closed connection, attempt reconnect
+ self.reconnect(True)
+@@ -342,7 +352,7 @@ class BEEventConnection( BEConnection ):
+ self.threadrunning = False
+ self.eventqueue = Queue.Queue()
+
+- super(BEEventConnection, self).__init__(backend, port, localname,
++ super(BEEventConnection, self).__init__(backend, port, localname,
+ False, deadline)
+
+ def connect(self):
+@@ -386,7 +396,7 @@ class BEEventConnection( BEConnection ):
+ event = self.socket.recvheader(deadline=0.0)
+
+ try:
+- event = unicode(''.join([event]), 'utf8')
++ event = str(''.join([event]), 'utf8')
+ except:
+ event = u''.join([event])
+
+@@ -394,14 +404,14 @@ class BEEventConnection( BEConnection ):
+ self.eventqueue.put(event)
+ # else discard
+
+- except MythError, e:
++ except MythError as e:
+ if e.sockcode == 54:
+ # remote has closed connection, attempt reconnect
+ self.reconnect(True, True)
+- return self.backendCommand(data, deadline)
++ return self.backendCommand(event, self.socket.getdeadline())
+ else:
+ raise
+-
++
+ def registeruser(self, uuid, opts):
+ self._regusers[uuid] = opts
+
+@@ -482,7 +492,7 @@ class FEConnection( object ):
+ try:
+ t = time()
+ fe._test(t + 2.0)
+- except MythError, e:
++ except MythError as e:
+ continue
+ yield fe
+
+@@ -582,7 +592,7 @@ class XMLConnection( object ):
+
+ def __repr__(self):
+ return "<%s 'http://%s:%d/' at %s>" % \
+- (str(self.__class__).split("'")[1].split(".")[-1],
++ (str(self.__class__).split("'")[1].split(".")[-1],
+ self.host, self.port, hex(id(self)))
+
+ def __init__(self, host, port):
+@@ -605,7 +615,7 @@ class XMLConnection( object ):
+ 'keyvars' are a series of optional variables to specify on the URL.
+
+ The request object supports open() and read(), as well as supports
+- editing of HTTP headers and POST data.
++ editing of HTTP headers and POST data.
+ """
+ url = 'http://{0.host}:{0.port}/{1}'.format(self, path)
+ if keyvars:
+diff --git a/mythtv/bindings/python/MythTV/database.py b/mythtv/bindings/python/MythTV/database.py
+index 8fd299a5f8..f8f0a5b2a3 100644
+--- a/mythtv/bindings/python/MythTV/database.py
++++ b/mythtv/bindings/python/MythTV/database.py
+@@ -8,7 +8,7 @@ from MythTV.static import MythSchema
+ from MythTV.altdict import OrdDict, DictData
+ from MythTV.logging import MythLog
+ from MythTV.msearch import MSearch
+-from MythTV.utility import datetime, _donothing, QuickProperty
++from MythTV.utility import datetime, dt, _donothing, QuickProperty
+ from MythTV.exceptions import MythError, MythDBError, MythTZError
+ from MythTV.connections import DBConnection, LoggedCursor, XMLConnection
+
+@@ -19,6 +19,7 @@ import datetime as _pydt
+ import time as _pyt
+ import weakref
+ import os
++from builtins import int, str
+
+
+ class DBData( DictData, MythSchema ):
+@@ -118,7 +119,7 @@ class DBData( DictData, MythSchema ):
+ for row in cursor:
+ try:
+ yield cls.fromRaw(row, db)
+- except MythDBError, e:
++ except MythDBError as e:
+ if e.ecode == MythError.DB_RESTRICT:
+ pass
+
+@@ -507,7 +508,7 @@ class DBDataRef( list ):
+ if dat not in self:
+ data.append(dat)
+ return self.fromCopy(data, self._db)
+-
++
+ def __and__(self, other):
+ data = []
+ for dat in self:
+@@ -566,7 +567,7 @@ class DBDataRef( list ):
+ c = cls('', db=db, bypass=True)
+ c._populated = True
+ for dat in data:
+- list.append(c, c.SubData(zip(self._datfields, row)))
++ list.append(c, c.SubData(zip(cls._datfields, dat)))
+ return c
+
+ @classmethod
+@@ -1147,7 +1148,7 @@ class DBCache( MythSchema ):
+ # pull field list from database
+ try:
+ cursor.execute("DESC %s" % (key,))
+- except Exception, e:
++ except Exception as e:
+ raise MythDBError(MythDBError.DB_RAW, e.args)
+ self[key] = self._FieldData(cursor.fetchall())
+
+@@ -1280,7 +1281,7 @@ class DBCache( MythSchema ):
+
+ # apply the rest of object init if not already done
+ self._testconfig(self.dbconfig)
+-
++
+ def _testconfig(self, dbconfig):
+ self.dbconfig = dbconfig
+ if dbconfig in self.shared:
+@@ -1402,9 +1403,7 @@ class DBCache( MythSchema ):
+ """
+ conv = {int: str,
+ str: lambda x: '"%s"'%x,
+- long: str,
+ float: str,
+- unicode: lambda x: '"%s"'%x,
+ bool: str,
+ type(None): lambda x: 'NULL',
+ _pydt.datetime: lambda x: x.strftime('"%Y-%m-%d %H:%M:%S"'),
+@@ -1416,7 +1415,7 @@ class DBCache( MythSchema ):
+ x.seconds%60),
+ _pyt.struct_time: lambda x: _pyt.\
+ strftime('"%Y-%m-%d %H:%M:%S"',x)}
+-
++
+ if args is None:
+ return query
+
diff --git a/mythtv/bindings/python/MythTV/dataheap.py b/mythtv/bindings/python/MythTV/dataheap.py
-index 859e060112..4de3085423 100644
+index 859e060112..946015c412 100644
--- a/mythtv/bindings/python/MythTV/dataheap.py
+++ b/mythtv/bindings/python/MythTV/dataheap.py
-@@ -283,7 +283,7 @@ class Recorded( CMPRecord, DBDataWrite ):
+@@ -19,7 +19,14 @@ from datetime import date, time
+
+ _default_datetime = datetime(1900,1,1, tzinfo=datetime.UTCTZ())
+
+-from UserString import MutableString
++# from builtins import str
++try:
++ from UserString import MutableString
++except ImportError:
++ from collections import UserString as MutableString
++ unicode = str
++ MutableString = str
++
+ class Artwork( MutableString ):
+ _types = {'coverart': 'Coverart',
+ 'coverfile': 'Coverart',
+@@ -58,15 +65,18 @@ class Artwork( MutableString ):
+ if (imagetype is None) and (attr not in cls._types):
+ # usage appears to be export from immutable UserString methods
+ # return a dumb string
+- return unicode.__new__(unicode, attr)
++ return str.__new__(str, attr)
+ else:
+- return super(Artwork, cls).__new__(cls, attr, parent, imagetype)
++ try:
++ return super(Artwork, cls).__new__(cls, attr, parent, imagetype)
++ except TypeError:
++ return super(Artwork, cls).__new__(cls, attr)
+
+ def __init__(self, attr, parent=None, imagetype=None):
+ # replace standard MutableString init to not overwrite self.data
+- from warnings import warnpy3k
+- warnpy3k('the class UserString.MutableString has been removed in '
+- 'Python 3.0', stacklevel=2)
++ # from warnings import warnpy3k
++ # warnpy3k('the class UserString.MutableString has been removed in '
++ # 'Python 3.0', stacklevel=2)
+
+ self.attr = attr
+ if imagetype is None:
+@@ -94,7 +104,7 @@ class Artwork( MutableString ):
+ @property
+ def exists(self):
+ be = FileOps(self.hostname, db = self.parent._db)
+- return be.fileExists(unicode(self), self.imagetype)
++ return be.fileExists(str(self), self.imagetype)
+
+ def downloadFrom(self, url):
+ if self.parent is None:
+@@ -246,8 +256,8 @@ class Record( CMPRecord, DBDataWrite, RECTYPE ):
+ return rec.create(wait=wait)
+
+ @classmethod
+- def fromPowerRule(cls, title='unnamed (Power Search)', where='', args=None,
+- join='', db=None, type=RECTYPE.kAllRecord,
++ def fromPowerRule(cls, title='unnamed (Power Search)', where='', args=None,
++ join='', db=None, type=RECTYPE.kAllRecord,
+ searchtype=RECSEARCHTYPE.kPowerSearch, wait=False):
+
+ if type not in (RECTYPE.kAllRecord, RECTYPE.kDailyRecord,
+@@ -283,7 +293,7 @@ class Recorded( CMPRecord, DBDataWrite ):
'commflagged':0, 'recgroup':'Default', 'seriesid':'',
'programid':'', 'lastmodified':'CURRENT_TIMESTAMP',
'filesize':0, 'stars':0, 'previouslyshown':0,
@@ -93,6 +547,3555 @@ index 859e060112..4de3085423 100644
'findid':0, 'deletepending':0, 'transcoder':0,
'timestretch':1, 'recpriority':0, 'playgroup':'Default',
'profile':'No', 'duplicate':1, 'transcoded':0,
+@@ -315,7 +325,7 @@ class Recorded( CMPRecord, DBDataWrite ):
+ class _Rating( DBDataRef ):
+ _table = 'recordedrating'
+ _ref = ['chanid','starttime']
+-
++
+ def __str__(self):
+ if self._wheredat is None:
+ return u"<Uninitialized Recorded at %s>" % hex(id(self))
+@@ -554,7 +564,7 @@ class RecordedProgram( CMPRecord, DBDataWrite ):
+ 'colorcode':'', 'syndicatedepisodenumber':'',
+ 'programid':'', 'manualid':0, 'generic':0,
+ 'first':0, 'listingsource':0, 'last':0,
+- 'audioprop':u'','videoprop':u'',
++ 'audioprop':u'','videoprop':u'',
+ 'subtitletypes':u'', 'inputname':u''}
+
+ def __str__(self):
+@@ -588,7 +598,7 @@ class OldRecorded( CMPRecord, DBDataWrite, RECSTATUS ):
+ """
+
+ _key = ['chanid','starttime']
+- _defaults = {'title':'', 'subtitle':'',
++ _defaults = {'title':'', 'subtitle':'',
+ 'category':'', 'seriesid':'', 'programid':'',
+ 'findid':0, 'recordid':0, 'station':'',
+ 'rectype':0, 'duplicate':0, 'recstatus':-3,
+@@ -747,7 +757,7 @@ class Guide( CMPRecord, DBData ):
+ """
+ _table = 'program'
+ _key = ['chanid','starttime']
+-
++
+ def __str__(self):
+ if self._wheredat is None:
+ return u"<Uninitialized Guide at %s>" % hex(id(self))
+@@ -1145,7 +1155,7 @@ class Video( CMPVideo, VideoSchema, DBDataWrite ):
+ return vid
+
+ def _playOnFe(self, fe):
+- return fe.send('play','file myth://Videos@%s/%s' %
++ return fe.send('play','file myth://Videos@%s/%s' %
+ (self.host, self.filename))
+
+ #### LEGACY ####
+@@ -1303,7 +1313,7 @@ class Artist( MusicSchema, DBDataWrite ):
+ artist = cls(db=db)
+ artist.artist_name = name
+ return artist.create()
+-
++
+ @classmethod
+ def fromSong(cls, song, db=None):
+ """Returns the artist for the given song."""
+diff --git a/mythtv/bindings/python/MythTV/logging.py b/mythtv/bindings/python/MythTV/logging.py
+index 45ace3216d..7304be4d71 100644
+--- a/mythtv/bindings/python/MythTV/logging.py
++++ b/mythtv/bindings/python/MythTV/logging.py
+@@ -10,8 +10,14 @@ import codecs
+
+ from sys import version_info, stdout, argv
+ from datetime import datetime
+-from thread import allocate_lock
+-from StringIO import StringIO
++try:
++ from thread import allocate_lock
++except:
++ from _thread import allocate_lock
++try:
++ from StringIO import StringIO
++except:
++ from io import StringIO
+ from traceback import format_exc
+
+ def _donothing(*args, **kwargs):
+@@ -213,7 +219,7 @@ class MythLog( LOGLEVEL, LOGMASK, LOGFACILITY ):
+
+ def __repr__(self):
+ return "<%s '%s','%s' at %s>" % \
+- (str(self.__class__).split("'")[1].split(".")[-1],
++ (str(self.__class__).split("'")[1].split(".")[-1],
+ self.module, bin(self._MASK), hex(id(self)))
+
+ def __new__(cls, *args, **kwargs):
+diff --git a/mythtv/bindings/python/MythTV/methodheap.py b/mythtv/bindings/python/MythTV/methodheap.py
+index 871605565c..f68903a4e8 100644
+--- a/mythtv/bindings/python/MythTV/methodheap.py
++++ b/mythtv/bindings/python/MythTV/methodheap.py
+@@ -16,7 +16,11 @@ from MythTV.dataheap import *
+
+ from datetime import timedelta
+ from weakref import proxy
+-from urllib import urlopen
++try:
++ from urllib import urlopen
++except ImportError:
++ from urllib.request import urlopen
++
+ import re
+
+ class CaptureCard( DBData ):
+@@ -526,7 +530,7 @@ class Frontend( FEConnection ):
+ 273:'f9', 274:'f10', 275:'f11',
+ 276:'f12', 330:'delete', 331:'insert',
+ 338:'pagedown', 339:'pageup'}
+- _alnum = [chr(i) for i in range(48,58)+range(65,91)+range(97,123)]
++ _alnum = [chr(i) for i in list(range(48,58))+list(range(65,91))+list(range(97,123))]
+
+ def __str__(self): return str(self.list())
+ def __repr__(self): return str(self)
+@@ -890,7 +894,7 @@ class MythDB( DBCache ):
+ return ('program.endtime>?', datetime.duck(value), 0)
+ return None
+
+- def makePowerRule(self, ruletitle='unnamed (Power Search',
++ def makePowerRule(self, ruletitle='unnamed (Power Search',
+ type=RECTYPE.kAllRecord, **kwargs):
+ where, args, join = self.searchGuide.parseInp(kwargs)
+ where = ' AND '.join(where)
+@@ -1111,7 +1115,7 @@ class MythXML( XMLConnection ):
+ self.host = backend.split('.')[0]
+ self.port = int(self.db.setting[self.host].BackendStatusPort)
+ if not self.port:
+- raise MythDBError(MythError.DB_SETTING,
++ raise MythDBError(MythError.DB_SETTING,
+ backend+': BackendStatusPort')
+
+ def getHosts(self):
+@@ -1141,7 +1145,7 @@ class MythXML( XMLConnection ):
+ starttime = datetime.duck(starttime)
+ endtime = datetime.duck(endtime)
+ args = {'StartTime':starttime.utcisoformat().rsplit('.',1)[0],
+- 'EndTime':endtime.utcisoformat().rsplit('.',1)[0],
++ 'EndTime':endtime.utcisoformat().rsplit('.',1)[0],
+ 'StartChanId':startchan, 'Details':1}
+ if numchan:
+ args['NumOfChannels'] = numchan
+diff --git a/mythtv/bindings/python/MythTV/msearch.py b/mythtv/bindings/python/MythTV/msearch.py
+index 8142f5bf5c..a5d1f3d123 100644
+--- a/mythtv/bindings/python/MythTV/msearch.py
++++ b/mythtv/bindings/python/MythTV/msearch.py
+@@ -25,7 +25,7 @@ class MSearch( object ):
+ self.sock.bind(('', port))
+ self.addr = (addr, port)
+ listening = True
+- except socket.error, e:
++ except socket.error as e:
+ if port < 1910:
+ port += 1
+ else:
+diff --git a/mythtv/bindings/python/MythTV/mythproto.py b/mythtv/bindings/python/MythTV/mythproto.py
+index a3df5def54..aa45d326b7 100644
+--- a/mythtv/bindings/python/MythTV/mythproto.py
++++ b/mythtv/bindings/python/MythTV/mythproto.py
+@@ -16,7 +16,10 @@ from MythTV.utility import CMPRecord, datetime, \
+
+ from datetime import date
+ from time import sleep
+-from thread import allocate_lock
++try:
++ from thread import allocate_lock
++except ImportError:
++ from _thread import allocate_lock
+ from random import randint
+ import socket
+ import weakref
+diff --git a/mythtv/bindings/python/MythTV/ttvdb/XSLT/tvdbCollection.xsl b/mythtv/bindings/python/MythTV/ttvdb/XSLT/tvdbCollection.xsl
+index 4dfa7c1757..f9464db373 100644
+--- a/mythtv/bindings/python/MythTV/ttvdb/XSLT/tvdbCollection.xsl
++++ b/mythtv/bindings/python/MythTV/ttvdb/XSLT/tvdbCollection.xsl
+@@ -16,7 +16,7 @@
+ within a single Xslt file
+ -->
+ <xsl:template match="/">
+- <xsl:if test="//Series">
++ <xsl:if test="/data/series">
+ <metadata>
+ <xsl:call-template name='tvdbCollection'/>
+ </metadata>
+@@ -24,28 +24,28 @@
+ </xsl:template>
+
+ <xsl:template name="tvdbCollection">
+- <xsl:for-each select="//Series">
++ <xsl:for-each select="/data/series">
+ <item>
+- <language><xsl:value-of select="normalize-space(Language)"/></language>
+- <title><xsl:value-of select="normalize-space(SeriesName)"/></title>
+- <xsl:if test="./Network/text() != ''">
+- <network><xsl:value-of select="normalize-space(Network)"/></network>
++ <language><xsl:value-of select="normalize-space(language)"/></language>
++ <title><xsl:value-of select="normalize-space(seriesName)"/></title>
++ <xsl:if test="./network/text() != ''">
++ <network><xsl:value-of select="normalize-space(network)"/></network>
+ </xsl:if>
+- <xsl:if test="./Airs_DayOfWeek/text() != ''">
+- <airday><xsl:value-of select="normalize-space(Airs_DayOfWeek)"/></airday>
++ <xsl:if test="./airsDayOfWeek/text() != ''">
++ <airday><xsl:value-of select="normalize-space(airsDayOfWeek)"/></airday>
+ </xsl:if>
+- <xsl:if test="./Airs_Time/text() != ''">
+- <airtime><xsl:value-of select="normalize-space(Airs_Time)"/></airtime>
++ <xsl:if test="./airsTime/text() != ''">
++ <airtime><xsl:value-of select="normalize-space(airsTime)"/></airtime>
+ </xsl:if>
+- <xsl:if test="./Overview/text() != ''">
+- <description><xsl:value-of select="normalize-space(Overview)"/></description>
++ <xsl:if test="./overview/text() != ''">
++ <description><xsl:value-of select="normalize-space(overview)"/></description>
+ </xsl:if>
+ <!-- <xsl:if test="tvdbXpath:getValue(//requestDetails, ./, 'Overview') != ''">-->
+ <!-- <description><xsl:value-of select="normalize-space(tvdbXpath:htmlToString(tvdbXpath:getResult()))"/></description>-->
+ <!-- </xsl:if>-->
+- <xsl:if test="./ContentRating/text() != ''">
++ <xsl:if test="./rating/text() != ''">
+ <certifications>
+- <xsl:for-each select=".//ContentRating">
++ <xsl:for-each select=".//rating">
+ <xsl:element name="certification">
+ <xsl:attribute name="locale">us</xsl:attribute>
+ <xsl:attribute name="name"><xsl:value-of select="normalize-space(.)"/></xsl:attribute>
+@@ -53,9 +53,9 @@
+ </xsl:for-each>
+ </certifications>
+ </xsl:if>
+- <xsl:if test="./Genre/text() != ''">
++ <xsl:if test="./genre/text() != ''">
+ <categories>
+- <xsl:for-each select="tvdbXpath:stringToList(string(./Genre))">
++ <xsl:for-each select="tvdbXpath:stringToList(string(./genre))">
+ <xsl:element name="category">
+ <xsl:attribute name="type">genre</xsl:attribute>
+ <xsl:attribute name="name"><xsl:value-of select="normalize-space(.)"/></xsl:attribute>
+@@ -63,62 +63,87 @@
+ </xsl:for-each>
+ </categories>
+ </xsl:if>
+- <xsl:if test="./Network/text() != ''">
++ <xsl:if test="./network/text() != ''">
+ <studios>
+- <xsl:for-each select="./Network">
++ <xsl:for-each select="./network">
+ <xsl:element name="studio">
+ <xsl:attribute name="name"><xsl:value-of select="normalize-space(.)"/></xsl:attribute>
+ </xsl:element>
+ </xsl:for-each>
+ </studios>
+ </xsl:if>
+- <xsl:if test="./Runtime/text() != ''">
+- <runtime><xsl:value-of select="normalize-space(Runtime)"/></runtime>
++ <xsl:if test="./runtime/text() != ''">
++ <runtime><xsl:value-of select="normalize-space(runtime)"/></runtime>
+ </xsl:if>
+ <inetref><xsl:value-of select="normalize-space(id)"/></inetref>
+- <xsl:if test="./IMDB_ID/text() != ''">
+- <imdb><xsl:value-of select="normalize-space(substring-after(string(IMDB_ID), 'tt'))"/></imdb>
++ <xsl:if test="./imdbId/text() != ''">
++ <imdb><xsl:value-of select="normalize-space(substring-after(string(imdbId), 'tt'))"/></imdb>
+ </xsl:if>
+ <xsl:if test="./zap2it_id/text() != ''">
+ <tmsref><xsl:value-of select="normalize-space(zap2it_id)"/></tmsref>
+ </xsl:if>
+- <xsl:if test="./Rating/text() != ''">
+- <userrating><xsl:value-of select="normalize-space(Rating)"/></userrating>
++ <xsl:if test="./siteRating/text() != ''">
++ <userrating><xsl:value-of select="normalize-space(siteRating)"/></userrating>
+ </xsl:if>
+- <xsl:if test="./RatingCount/text() != ''">
+- <ratingcount><xsl:value-of select="normalize-space(RatingCount)"/></ratingcount>
++ <xsl:if test="./siteRatingCount/text() != ''">
++ <ratingcount><xsl:value-of select="normalize-space(siteRatingCount)"/></ratingcount>
+ </xsl:if>
+- <xsl:if test="./FirstAired/text() != ''">
+- <year><xsl:value-of select="normalize-space(substring-before(string(FirstAired), '-'))"/></year>
+- <releasedate><xsl:value-of select="normalize-space(FirstAired)"/></releasedate>
++ <xsl:if test="./firstAired/text() != ''">
++ <year><xsl:value-of select="normalize-space(substring-before(string(firstAired), '-'))"/></year>
++ <releasedate><xsl:value-of select="normalize-space(firstAired)"/></releasedate>
+ </xsl:if>
+- <xsl:if test="./lastupdated/text() != ''">
+- <lastupdated><xsl:value-of select="tvdbXpath:lastUpdated(string(./lastupdated))"/></lastupdated>
++ <xsl:if test="./lastUpdated/text() != ''">
++ <lastupdated><xsl:value-of select="tvdbXpath:lastUpdated(string(./lastUpdated))"/></lastupdated>
+ </xsl:if>
+- <xsl:if test="./Status/text() != ''">
+- <status><xsl:value-of select="normalize-space(Status)"/></status>
++ <xsl:if test="./status/text() != ''">
++ <status><xsl:value-of select="normalize-space(status)"/></status>
+ </xsl:if>
+- <xsl:if test=".//poster/text() != '' or .//fanart/text() != '' or .//banner/text() != ''">
++ <xsl:if test="./poster/text() != '' or ./fanart/text() != '' or ./banner/text() != '' or .//_banners/poster/raw or .//_banners/fanart/raw">
+ <images>
+- <xsl:if test=".//poster/text() != ''">
+- <xsl:element name="image">
+- <xsl:attribute name="type">coverart</xsl:attribute>
+- <xsl:attribute name="url"><xsl:value-of select="concat('http://www.thetvdb.com/banners/', normalize-space(poster))"/></xsl:attribute>
+- <xsl:attribute name="thumb"><xsl:value-of select="concat('http://www.thetvdb.com/banners/_cache/', normalize-space(poster))"/></xsl:attribute>
+- </xsl:element>
+- </xsl:if>
+- <xsl:if test=".//fanart/text() != ''">
+- <xsl:element name="image">
+- <xsl:attribute name="type">fanart</xsl:attribute>
+- <xsl:attribute name="url"><xsl:value-of select="concat('http://www.thetvdb.com/banners/', normalize-space(fanart))"/></xsl:attribute>
+- <xsl:attribute name="thumb"><xsl:value-of select="concat('http://www.thetvdb.com/banners/_cache/', normalize-space(fanart))"/></xsl:attribute>
+- </xsl:element>
+- </xsl:if>
+- <xsl:if test=".//banner/text() != ''">
++ <xsl:choose>
++ <xsl:when test="./poster/text() != ''">
++ <xsl:element name="image">
++ <xsl:attribute name="type">coverart</xsl:attribute>
++ <xsl:attribute name="url"><xsl:value-of select="normalize-space(poster)"/></xsl:attribute>
++ <xsl:attribute name="thumb"><xsl:value-of select="normalize-space(tvdbXpath:replace(string(poster), 'banners', 'banners/_cache'))"/></xsl:attribute>
++ </xsl:element>
++ </xsl:when>
++ <xsl:when test="./_banners/seasonswide/raw">
++ <xsl:element name="image">
++ <xsl:attribute name="type">coverart</xsl:attribute>
++ <xsl:attribute name="url"><xsl:value-of select="concat('http://www.thetvdb.com/banners/', normalize-space(_banners/seasonswide/raw/item[1]/fileName))"/></xsl:attribute>
++ <xsl:attribute name="thumb"><xsl:value-of select="concat('http://www.thetvdb.com/banners/', normalize-space(_banners/seasonswide/raw/item[1]/thumbnail))"/></xsl:attribute>
++ </xsl:element>
++ </xsl:when>
++ <xsl:when test="./_banners/poster/raw">
++ <xsl:element name="image">
++ <xsl:attribute name="type">coverart</xsl:attribute>
++ <xsl:attribute name="url"><xsl:value-of select="concat('http://www.thetvdb.com/banners/', normalize-space(_banners/poster/raw/item[1]/fileName))"/></xsl:attribute>
++ <xsl:attribute name="thumb"><xsl:value-of select="concat('http://www.thetvdb.com/banners/', normalize-space(_banners/poster/raw/item[1]/thumbnail))"/></xsl:attribute>
++ </xsl:element>
++ </xsl:when>
++ </xsl:choose>
++ <xsl:choose>
++ <xsl:when test="./fanart/text() != ''">
++ <xsl:element name="image">
++ <xsl:attribute name="type">fanart</xsl:attribute>
++ <xsl:attribute name="url"><xsl:value-of select="normalize-space(fanart)"/></xsl:attribute>
++ <xsl:attribute name="thumb"><xsl:value-of select="normalize-space(tvdbXpath:replace(string(fanart), 'banners', 'banners/_cache'))"/></xsl:attribute>
++ </xsl:element>
++ </xsl:when>
++ <xsl:when test="./_banners/fanart/raw">
++ <xsl:element name="image">
++ <xsl:attribute name="type">fanart</xsl:attribute>
++ <xsl:attribute name="url"><xsl:value-of select="concat('http://www.thetvdb.com/banners/', normalize-space(_banners/fanart/raw/item[1]/fileName))"/></xsl:attribute>
++ <xsl:attribute name="thumb"><xsl:value-of select="concat('http://www.thetvdb.com/banners/', normalize-space(_banners/fanart/raw/item[1]/thumbnail))"/></xsl:attribute>
++ </xsl:element>
++ </xsl:when>
++ </xsl:choose>
++ <xsl:if test="./banner/text() != ''">
+ <xsl:element name="image">
+ <xsl:attribute name="type">banner</xsl:attribute>
+- <xsl:attribute name="url"><xsl:value-of select="concat('http://www.thetvdb.com/banners/', normalize-space(banner))"/></xsl:attribute>
+- <xsl:attribute name="thumb"><xsl:value-of select="concat('http://www.thetvdb.com/banners/_cache/', normalize-space(banner))"/></xsl:attribute>
++ <xsl:attribute name="url"><xsl:value-of select="normalize-space(banner)"/></xsl:attribute>
++ <xsl:attribute name="thumb"><xsl:value-of select="normalize-space(tvdbXpath:replace(string(banner), 'banners', 'banners/_cache'))"/></xsl:attribute>
+ </xsl:element>
+ </xsl:if>
+ </images>
+diff --git a/mythtv/bindings/python/MythTV/ttvdb/XSLT/tvdbQuery.xsl b/mythtv/bindings/python/MythTV/ttvdb/XSLT/tvdbQuery.xsl
+index 28daee047e..bf7ad06a82 100644
+--- a/mythtv/bindings/python/MythTV/ttvdb/XSLT/tvdbQuery.xsl
++++ b/mythtv/bindings/python/MythTV/ttvdb/XSLT/tvdbQuery.xsl
+@@ -14,7 +14,7 @@
+ within a single Xslt file
+ -->
+ <xsl:template match="/">
+- <xsl:if test="//Series">
++ <xsl:if test="//series">
+ <metadata>
+ <xsl:call-template name='tvdbQuery'/>
+ </metadata>
+@@ -22,10 +22,10 @@
+ </xsl:template>
+
+ <xsl:template name="tvdbQuery">
+- <xsl:for-each select="//Series">
++ <xsl:for-each select="//series">
+ <item>
+ <language><xsl:value-of select="normalize-space(language)"/></language>
+- <title><xsl:value-of select="normalize-space(SeriesName)"/></title>
++ <title><xsl:value-of select="normalize-space(seriesName)"/></title>
+ <inetref><xsl:value-of select="normalize-space(id)"/></inetref>
+ <collectionref><xsl:value-of select="normalize-space(id)"/></collectionref>
+ <xsl:if test="./IMDB_ID/text() != ''">
+@@ -34,14 +34,14 @@
+ <xsl:if test="./zap2it_id/text() != ''">
+ <tmsref><xsl:value-of select="normalize-space(zap2it_id)"/></tmsref>
+ </xsl:if>
+- <xsl:if test="./Rating/text() != ''">
+- <userrating><xsl:value-of select="normalize-space(Rating)"/></userrating>
++ <xsl:if test="./rating/text() != ''">
++ <userrating><xsl:value-of select="normalize-space(rating)"/></userrating>
+ </xsl:if>
+- <xsl:if test="./Overview/text() != ''">
+- <description><xsl:value-of select="normalize-space(Overview)"/></description>
++ <xsl:if test="./overview/text() != ''">
++ <description><xsl:value-of select="normalize-space(overview)"/></description>
+ </xsl:if>
+- <xsl:if test="./FirstAired/text() != ''">
+- <releasedate><xsl:value-of select="normalize-space(FirstAired)"/></releasedate>
++ <xsl:if test="./firstAired/text() != ''">
++ <releasedate><xsl:value-of select="normalize-space(firstAired)"/></releasedate>
+ </xsl:if>
+ <xsl:if test=".//poster/text() != '' or .//fanart/text() != '' or .//banner/text() != ''">
+ <images>
+diff --git a/mythtv/bindings/python/MythTV/ttvdb/XSLT/tvdbVideo.xsl b/mythtv/bindings/python/MythTV/ttvdb/XSLT/tvdbVideo.xsl
+index 3eb3c8eb66..c36e13e41c 100644
+--- a/mythtv/bindings/python/MythTV/ttvdb/XSLT/tvdbVideo.xsl
++++ b/mythtv/bindings/python/MythTV/ttvdb/XSLT/tvdbVideo.xsl
+@@ -16,7 +16,7 @@
+ within a single Xslt file
+ -->
+ <xsl:template match="/">
+- <xsl:if test="//Data/Series">
++ <xsl:if test="//data/series">
+ <metadata>
+ <xsl:call-template name='tvdbVideoData'/>
+ </metadata>
+@@ -24,21 +24,20 @@
+ </xsl:template>
+
+ <xsl:template name="tvdbVideoData">
+- <xsl:for-each select="//Data/Series">
++ <xsl:for-each select="//data/series">
+ <item>
+- <title><xsl:value-of select="normalize-space(SeriesName)"/></title>
+- <xsl:if test="tvdbXpath:getValue(//requestDetails, //Data, 'subtitle') != ''">
++ <title><xsl:value-of select="normalize-space(seriesName)"/></title>
++ <xsl:if test="tvdbXpath:getValue(//requestDetails, //data, 'subtitle') != ''">
+ <subtitle><xsl:value-of select="normalize-space(tvdbXpath:getResult())"/></subtitle>
+ </xsl:if>
+- <language><xsl:value-of select="normalize-space(Language)"/></language>
+- <xsl:if test="tvdbXpath:getValue(//requestDetails, //Data, 'description') != ''">
++ <xsl:if test="tvdbXpath:getValue(//requestDetails, //data/series, 'description') != ''">
+ <description><xsl:value-of select="normalize-space(tvdbXpath:htmlToString(tvdbXpath:getResult()))"/></description>
+ </xsl:if>
+ <season><xsl:value-of select="normalize-space(//requestDetails/@season)"/></season>
+ <episode><xsl:value-of select="normalize-space(//requestDetails/@episode)"/></episode>
+- <xsl:if test="./ContentRating/text() != ''">
++ <xsl:if test="./rating/text() != ''">
+ <certifications>
+- <xsl:for-each select=".//ContentRating">
++ <xsl:for-each select=".//rating">
+ <xsl:element name="certification">
+ <xsl:attribute name="locale">us</xsl:attribute>
+ <xsl:attribute name="name"><xsl:value-of select="normalize-space(.)"/></xsl:attribute>
+@@ -46,9 +45,9 @@
+ </xsl:for-each>
+ </certifications>
+ </xsl:if>
+- <xsl:if test="./Genre/text() != ''">
++ <xsl:if test="./genre/text() != ''">
+ <categories>
+- <xsl:for-each select="tvdbXpath:stringToList(string(./Genre))">
++ <xsl:for-each select="tvdbXpath:stringToList(string(./genre))">
+ <xsl:element name="category">
+ <xsl:attribute name="type">genre</xsl:attribute>
+ <xsl:attribute name="name"><xsl:value-of select="normalize-space(.)"/></xsl:attribute>
+@@ -56,36 +55,39 @@
+ </xsl:for-each>
+ </categories>
+ </xsl:if>
+- <xsl:if test="./Network/text() != ''">
++ <xsl:if test="./network/text() != ''">
+ <studios>
+- <xsl:for-each select="./Network">
++ <xsl:for-each select="./network">
+ <xsl:element name="studio">
+ <xsl:attribute name="name"><xsl:value-of select="normalize-space(.)"/></xsl:attribute>
+ </xsl:element>
+ </xsl:for-each>
+ </studios>
+ </xsl:if>
+- <xsl:if test="./Runtime/text() != ''">
++ <xsl:if test="./runtime/text() != ''">
+ <runtime><xsl:value-of select="normalize-space(Runtime)"/></runtime>
+ </xsl:if>
+ <inetref><xsl:value-of select="normalize-space(id)"/></inetref>
+ <collectionref><xsl:value-of select="normalize-space(id)"/></collectionref>
+- <xsl:if test="./IMDB_ID/text() != '' and tvdbXpath:getValue(//requestDetails, //Data, 'IMDB') = ''">
+- <imdb><xsl:value-of select="normalize-space(substring-after(string(IMDB_ID), 'tt'))"/></imdb>
++ <xsl:if test="./imdbId/text() != '' and tvdbXpath:getValue(//requestDetails, //data, 'IMDB') = ''">
++ <imdb><xsl:value-of select="normalize-space(substring-after(string(imdbId), 'tt'))"/></imdb>
+ </xsl:if>
+- <xsl:if test="./zap2it_id/text() != ''">
+- <tmsref><xsl:value-of select="normalize-space(zap2it_id)"/></tmsref>
++ <xsl:if test="./zap2itId/text() != ''">
++ <tmsref><xsl:value-of select="normalize-space(zap2itId)"/></tmsref>
+ </xsl:if>
+- <xsl:for-each select="tvdbXpath:getValue(//requestDetails, //Data, 'allEpisodes', 'allresults')">
+- <xsl:if test="./IMDB_ID/text() != ''">
+- <imdb><xsl:value-of select="normalize-space(substring-after(string(IMDB_ID), 'tt'))"/></imdb>
++ <xsl:for-each select="tvdbXpath:getValue(//requestDetails, //data, 'allEpisodes', 'allresults')">
++ <xsl:if test="./imdbId/text() != ''">
++ <imdb><xsl:value-of select="normalize-space(substring-after(string(zap2itId), 'tt'))"/></imdb>
+ </xsl:if>
+- <xsl:if test="./Rating/text() != ''">
+- <userrating><xsl:value-of select="normalize-space(./Rating)"/></userrating>
++ <xsl:if test="./rating/text() != ''">
++ <userrating><xsl:value-of select="normalize-space(./rating)"/></userrating>
+ </xsl:if>
+- <xsl:if test="./FirstAired/text() != ''">
+- <year><xsl:value-of select="normalize-space(substring(string(./FirstAired), 1, 4))"/></year>
+- <releasedate><xsl:value-of select="normalize-space(./FirstAired)"/></releasedate>
++ <xsl:if test="./language/overview/text() != ''">
++ <language><xsl:value-of select="normalize-space(./language/overview)"/></language>
++ </xsl:if>
++ <xsl:if test="./firstAired/text() != ''">
++ <year><xsl:value-of select="normalize-space(substring(string(./firstAired), 1, 4))"/></year>
++ <releasedate><xsl:value-of select="normalize-space(./firstAired)"/></releasedate>
+ </xsl:if>
+ <xsl:if test="./lastupdated/text() != ''">
+ <lastupdated><xsl:value-of select="tvdbXpath:lastUpdated(string(./lastupdated))"/></lastupdated>
+@@ -99,32 +101,32 @@
+ <xsl:if test="./seriesid/text() != '' and ./seasonid/text() != ''">
+ <homepage><xsl:value-of select="normalize-space(concat('http://thetvdb.com/?tab=episode&seriesid=', string(./seriesid), '&seasonid=', string(./seasonid), '&id=', string(./id)))"/></homepage>
+ </xsl:if>
+- <xsl:if test="//Actors/Actor or .//GuestStars/text() != '' or .//Director/text() != '' or .//Writer/text() != ''">
++ <xsl:if test="//data/series/_actors/actor or .//guestStars/text() != '' or .//director/text() != '' or .//writer/text() != ''">
+ <people>
+- <xsl:for-each select="//Actors/Actor">
++ <xsl:for-each select="//data/series/_actors/actor">
+ <xsl:element name="person">
+ <xsl:attribute name="job">Actor</xsl:attribute>
+- <xsl:attribute name="name"><xsl:value-of select="normalize-space(Name)"/></xsl:attribute>
+- <xsl:attribute name="character"><xsl:value-of select="normalize-space(Role)"/></xsl:attribute>
+- <xsl:if test="./Image/text() != ''">
+- <xsl:attribute name="url"><xsl:value-of select="concat('http://www.thetvdb.com/banners/', normalize-space(Image))"/></xsl:attribute>
+- <xsl:attribute name="thumb"><xsl:value-of select="concat('http://www.thetvdb.com/banners/_cache/', normalize-space(Image))"/></xsl:attribute>
++ <xsl:attribute name="name"><xsl:value-of select="normalize-space(name)"/></xsl:attribute>
++ <xsl:attribute name="character"><xsl:value-of select="normalize-space(role)"/></xsl:attribute>
++ <xsl:if test="./image/text() != ''">
++ <xsl:attribute name="url"><xsl:value-of select="normalize-space(image)"/></xsl:attribute>
++ <xsl:attribute name="thumb"><xsl:value-of select="normalize-space(image)"/></xsl:attribute>
+ </xsl:if>
+ </xsl:element>
+ </xsl:for-each>
+- <xsl:for-each select="tvdbXpath:stringToList(string(./GuestStars))">
++ <xsl:for-each select="./guestStars/item">
+ <xsl:element name="person">
+ <xsl:attribute name="job">Guest Star</xsl:attribute>
+ <xsl:attribute name="name"><xsl:value-of select="normalize-space(.)"/></xsl:attribute>
+ </xsl:element>
+ </xsl:for-each>
+- <xsl:for-each select="tvdbXpath:stringToList(string(./Director))">
++ <xsl:for-each select="./directors/item">
+ <xsl:element name="person">
+ <xsl:attribute name="job">Director</xsl:attribute>
+ <xsl:attribute name="name"><xsl:value-of select="normalize-space(.)"/></xsl:attribute>
+ </xsl:element>
+ </xsl:for-each>
+- <xsl:for-each select="tvdbXpath:stringToList(string(./Writer))">
++ <xsl:for-each select="./writers/item">
+ <xsl:element name="person">
+ <xsl:attribute name="job">Author</xsl:attribute>
+ <xsl:attribute name="name"><xsl:value-of select="normalize-space(.)"/></xsl:attribute>
+@@ -132,16 +134,17 @@
+ </xsl:for-each>
+ </people>
+ </xsl:if>
+- <xsl:if test="./filename/text() != '' or //Banners/Banner">
++ <xsl:if test="./filename/text() != '' or //data/series/_banners/*/raw/item">
+ <images>
+ <xsl:if test="./filename/text() != ''">
+ <xsl:element name="image">
+ <xsl:attribute name="type">screenshot</xsl:attribute>
+- <xsl:attribute name="url"><xsl:value-of select="concat('http://www.thetvdb.com/banners/', normalize-space(filename))"/></xsl:attribute>
+- <xsl:attribute name="thumb"><xsl:value-of select="concat('http://www.thetvdb.com/banners/_cache/', normalize-space(filename))"/></xsl:attribute>
++ <xsl:attribute name="url"><xsl:value-of select="normalize-space(filename)"/></xsl:attribute>
++ <xsl:attribute name="thumb"><xsl:value-of select="normalize-space(tvdbXpath:replace(string(filename), 'banners', 'banners/_cache'))"/></xsl:attribute>
+ </xsl:element>
+ </xsl:if>
+- <xsl:for-each select="tvdbXpath:imageElements(//Banners, 'poster', //requestDetails)">
++ <xsl:for-each select="tvdbXpath:imageElements(//data/series/_banners/poster/raw, 'poster', //requestDetails)">
++ <xsl:sort select="@rating" data-type="number" order="descending"/>
+ <xsl:element name="image">
+ <xsl:attribute name="type"><xsl:value-of select="normalize-space(@type)"/></xsl:attribute>
+ <xsl:attribute name="url"><xsl:value-of select="normalize-space(@url)"/></xsl:attribute>
+@@ -154,7 +157,8 @@
+ </xsl:if>
+ </xsl:element>
+ </xsl:for-each>
+- <xsl:for-each select="tvdbXpath:imageElements(//Banners, 'fanart', //requestDetails)">
++ <xsl:for-each select="tvdbXpath:imageElements(//data/series/_banners/fanart/raw, 'fanart', //requestDetails)">
++ <xsl:sort select="@rating" data-type="number" order="descending"/>
+ <xsl:element name="image">
+ <xsl:attribute name="type"><xsl:value-of select="normalize-space(@type)"/></xsl:attribute>
+ <xsl:attribute name="url"><xsl:value-of select="normalize-space(@url)"/></xsl:attribute>
+@@ -167,7 +171,8 @@
+ </xsl:if>
+ </xsl:element>
+ </xsl:for-each>
+- <xsl:for-each select="tvdbXpath:imageElements(//Banners, 'banner', //requestDetails)">
++ <xsl:for-each select="tvdbXpath:imageElements(//data/series/_banners/banner/raw, 'banner', //requestDetails)">
++ <xsl:sort select="@rating" data-type="number" order="descending"/>
+ <xsl:element name="image">
+ <xsl:attribute name="type"><xsl:value-of select="normalize-space(@type)"/></xsl:attribute>
+ <xsl:attribute name="url"><xsl:value-of select="normalize-space(@url)"/></xsl:attribute>
+diff --git a/mythtv/bindings/python/MythTV/ttvdb/cache.py b/mythtv/bindings/python/MythTV/ttvdb/cache.py
+deleted file mode 100644
+index 1c37d795da..0000000000
+--- a/mythtv/bindings/python/MythTV/ttvdb/cache.py
++++ /dev/null
+@@ -1,230 +0,0 @@
+-#!/usr/bin/env python
+-#encoding:utf-8
+-#author:dbr/Ben
+-#project:tvdb_api
+-#repository:http://github.com/dbr/tvdb_api
+-#license:Creative Commons GNU GPL v2
+-# (http://creativecommons.org/licenses/GPL/2.0/)
+-
+-"""
+-urllib2 caching handler
+-Modified from http://code.activestate.com/recipes/491261/
+-"""
+-__author__ = "dbr/Ben"
+-__version__ = "1.2.1"
+-
+-import os
+-import time
+-import errno
+-import httplib
+-import urllib2
+-import StringIO
+-from hashlib import md5
+-from threading import RLock
+-
+-cache_lock = RLock()
+-
+-def locked_function(origfunc):
+- """Decorator to execute function under lock"""
+- def wrapped(*args, **kwargs):
+- cache_lock.acquire()
+- try:
+- return origfunc(*args, **kwargs)
+- finally:
+- cache_lock.release()
+- return wrapped
+-
+-def calculate_cache_path(cache_location, url):
+- """Checks if [cache_location]/[hash_of_url].headers and .body exist
+- """
+- thumb = md5(url).hexdigest()
+- header = os.path.join(cache_location, thumb + ".headers")
+- body = os.path.join(cache_location, thumb + ".body")
+- return header, body
+-
+-def check_cache_time(path, max_age):
+- """Checks if a file has been created/modified in the [last max_age] seconds.
+- False means the file is too old (or doesn't exist), True means it is
+- up-to-date and valid"""
+- if not os.path.isfile(path):
+- return False
+- cache_modified_time = os.stat(path).st_mtime
+- time_now = time.time()
+- if cache_modified_time < time_now - max_age:
+- # Cache is old
+- return False
+- else:
+- return True
+-
+-@locked_function
+-def exists_in_cache(cache_location, url, max_age):
+- """Returns if header AND body cache file exist (and are up-to-date)"""
+- hpath, bpath = calculate_cache_path(cache_location, url)
+- if os.path.exists(hpath) and os.path.exists(bpath):
+- return(
+- check_cache_time(hpath, max_age)
+- and check_cache_time(bpath, max_age)
+- )
+- else:
+- # File does not exist
+- return False
+-
+-@locked_function
+-def store_in_cache(cache_location, url, response):
+- """Tries to store response in cache."""
+- hpath, bpath = calculate_cache_path(cache_location, url)
+- try:
+- outf = open(hpath, "w")
+- headers = str(response.info())
+- outf.write(headers)
+- outf.close()
+-
+- outf = open(bpath, "w")
+- outf.write(response.read())
+- outf.close()
+- except IOError:
+- return True
+- else:
+- return False
+-
+-class CacheHandler(urllib2.BaseHandler):
+- """Stores responses in a persistant on-disk cache.
+-
+- If a subsequent GET request is made for the same URL, the stored
+- response is returned, saving time, resources and bandwidth
+- """
+- @locked_function
+- def __init__(self, cache_location, max_age = 21600):
+- """The location of the cache directory"""
+- self.max_age = max_age
+- self.cache_location = cache_location
+- if not os.path.exists(self.cache_location):
+- try:
+- os.mkdir(self.cache_location)
+- except OSError, e:
+- if e.errno == errno.EEXIST and os.path.isdir(self.cache_location):
+- # File exists, and it's a directory,
+- # another process beat us to creating this dir, that's OK.
+- pass
+- else:
+- # Our target dir is already a file, or different error,
+- # relay the error!
+- raise OSError(e)
+-
+- def default_open(self, request):
+- """Handles GET requests, if the response is cached it returns it
+- """
+- if request.get_method() is not "GET":
+- return None # let the next handler try to handle the request
+-
+- if exists_in_cache(
+- self.cache_location, request.get_full_url(), self.max_age
+- ):
+- return CachedResponse(
+- self.cache_location,
+- request.get_full_url(),
+- set_cache_header = True
+- )
+- else:
+- return None
+-
+- def http_response(self, request, response):
+- """Gets a HTTP response, if it was a GET request and the status code
+- starts with 2 (200 OK etc) it caches it and returns a CachedResponse
+- """
+- if (request.get_method() == "GET"
+- and str(response.code).startswith("2")
+- ):
+- if 'x-local-cache' not in response.info():
+- # Response is not cached
+- set_cache_header = store_in_cache(
+- self.cache_location,
+- request.get_full_url(),
+- response
+- )
+- else:
+- set_cache_header = True
+- #end if x-cache in response
+-
+- return CachedResponse(
+- self.cache_location,
+- request.get_full_url(),
+- set_cache_header = set_cache_header
+- )
+- else:
+- return response
+-
+-class CachedResponse(StringIO.StringIO):
+- """An urllib2.response-like object for cached responses.
+-
+- To determine if a response is cached or coming directly from
+- the network, check the x-local-cache header rather than the object type.
+- """
+-
+- @locked_function
+- def __init__(self, cache_location, url, set_cache_header=True):
+- self.cache_location = cache_location
+- hpath, bpath = calculate_cache_path(cache_location, url)
+-
+- StringIO.StringIO.__init__(self, file(bpath).read())
+-
+- self.url = url
+- self.code = 200
+- self.msg = "OK"
+- headerbuf = file(hpath).read()
+- if set_cache_header:
+- headerbuf += "x-local-cache: %s\r\n" % (bpath)
+- self.headers = httplib.HTTPMessage(StringIO.StringIO(headerbuf))
+-
+- def info(self):
+- """Returns headers
+- """
+- return self.headers
+-
+- def geturl(self):
+- """Returns original URL
+- """
+- return self.url
+-
+- @locked_function
+- def recache(self):
+- new_request = urllib2.urlopen(self.url)
+- set_cache_header = store_in_cache(
+- self.cache_location,
+- new_request.url,
+- new_request
+- )
+- CachedResponse.__init__(self, self.cache_location, self.url, True)
+-
+-
+-if __name__ == "__main__":
+- def main():
+- """Quick test/example of CacheHandler"""
+- opener = urllib2.build_opener(CacheHandler("/tmp/"))
+- response = opener.open("http://google.com")
+- print response.headers
+- print "Response:", response.read()
+-
+- response.recache()
+- print response.headers
+- print "After recache:", response.read()
+-
+- # Test usage in threads
+- from threading import Thread
+- class CacheThreadTest(Thread):
+- lastdata = None
+- def run(self):
+- req = opener.open("http://google.com")
+- newdata = req.read()
+- if self.lastdata is None:
+- self.lastdata = newdata
+- assert self.lastdata == newdata, "Data was not consistent, uhoh"
+- req.recache()
+- threads = [CacheThreadTest() for x in range(50)]
+- print "Starting threads"
+- [t.start() for t in threads]
+- print "..done"
+- print "Joining threads"
+- [t.join() for t in threads]
+- print "..done"
+- main()
+diff --git a/mythtv/bindings/python/MythTV/ttvdb/requests_cache_compatability.py b/mythtv/bindings/python/MythTV/ttvdb/requests_cache_compatability.py
+new file mode 100644
+index 0000000000..3d6a056f42
+--- /dev/null
++++ b/mythtv/bindings/python/MythTV/ttvdb/requests_cache_compatability.py
+@@ -0,0 +1,44 @@
++# -*- coding: utf-8 -*-
++
++'''
++Patches older versions of requests_cache with missing expire
++functionality and an updated create_key which excludes most
++HEADERS
++
++This module must be imported before any modules use requests_cache
++explicitly or implicitly
++'''
++
++import requests_cache
++from datetime import datetime
++
++# patch if required if older versions of
++try:
++ requests_cache.backends.base.BaseCache.remove_old_entries
++except Exception as e:
++ def remove_old_entries(self, created_before):
++ """ Deletes entries from cache with creation time older than ``created_before``
++ """
++ keys_to_delete = set()
++ for key in self.responses:
++ try:
++ response, created_at = self.responses[key]
++ except KeyError:
++ continue
++ if created_at < created_before:
++ keys_to_delete.add(key)
++
++ for key in keys_to_delete:
++ self.delete(key)
++
++
++ def remove_expired_responses(self):
++ """ Removes expired responses from storage
++ """
++ if not self._cache_expire_after:
++ return
++ self.cache.remove_old_entries(datetime.utcnow() - self._cache_expire_after)
++
++
++ requests_cache.backends.base.BaseCache.remove_old_entries = remove_old_entries
++ requests_cache.core.CachedSession.remove_expired_responses = remove_expired_responses
+diff --git a/mythtv/bindings/python/MythTV/ttvdb/tvdbXslt.py b/mythtv/bindings/python/MythTV/ttvdb/tvdbXslt.py
+index ff5dc6827d..cd34f93a9b 100755
+--- a/mythtv/bindings/python/MythTV/ttvdb/tvdbXslt.py
++++ b/mythtv/bindings/python/MythTV/ttvdb/tvdbXslt.py
+@@ -37,6 +37,12 @@ __xsltExtentionList__ = []
+ import os, sys, re, time, datetime, shutil, urllib, string
+ from copy import deepcopy
+
++IS_PY2 = sys.version_info[0] == 2
++if not IS_PY2:
++ unicode = str
++ long = int
++
++baseXsltDir = u'%s/XSLT/' % os.path.dirname(os.path.realpath(__file__))
+
+ class OutStreamEncoder(object):
+ """Wraps a stream with an encoder"""
+@@ -50,26 +56,35 @@ class OutStreamEncoder(object):
+ def write(self, obj):
+ """Wraps the output stream, encoding Unicode strings with the specified encoding"""
+ if isinstance(obj, unicode):
+- try:
+- self.out.write(obj.encode(self.encoding))
+- except IOError:
+- pass
+- else:
+- try:
++ obj.encode(self.encoding)
++ try:
++ if IS_PY2:
+ self.out.write(obj)
+- except IOError:
+- pass
++ else:
++ self.out.buffer.write(obj)
++ except IOError:
++ pass
+
+ def __getattr__(self, attr):
+ """Delegate everything but write to the stream"""
+ return getattr(self.out, attr)
+-sys.stdout = OutStreamEncoder(sys.stdout, 'utf8')
+-sys.stderr = OutStreamEncoder(sys.stderr, 'utf8')
++if IS_PY2:
++ stdio_type = file
++else:
++ import io
++ stdio_type = io.TextIOWrapper
++ unicode = str
++if isinstance(sys.stdout, stdio_type):
++ sys.stdout = OutStreamEncoder(sys.stdout, 'utf8')
++ sys.stderr = OutStreamEncoder(sys.stderr, 'utf8')
+
+ try:
+- from StringIO import StringIO
++ try:
++ from StringIO import StringIO
++ except ImportError:
++ from io import StringIO
+ from lxml import etree
+-except Exception, e:
++except Exception as e:
+ sys.stderr.write(u'\n! Error - Importing the "lxml" and "StringIO" python libraries failed on error(%s)\n' % e)
+ sys.exit(1)
+
+@@ -94,15 +109,21 @@ class xpathFunctions(object):
+ """
+ def __init__(self):
+ self.filters = {
+- 'fanart': [u'//Banner[BannerType/text()="%(type)s" and Language/text()="%(language)s"]', u'//Banner[BannerType/text()="%(type)s" and Language/text()="en"]', u'//Banner[BannerType/text()="%(type)s"]'],
+- 'poster': [u'//Banner[BannerType/text()="season" and Language/text()="%(language)s" and Season/text()="%(season)s" and BannerType2/text()="season"]', u'//Banner[BannerType/text()="%(type)s" and Language/text()="%(language)s"]', u'//Banner[BannerType/text()="season" and Language/text()="en" and Season/text()="%(season)s" and BannerType2/text()="season"]', u'//Banner[BannerType/text()="season" and Season/text()="%(season)s" and BannerType2/text()="season"]', u'//Banner[BannerType/text()="%(type)s" and Language/text()="en"]', u'//Banner[BannerType/text()="%(type)s"]'],
+- 'banner': ['//Banner[BannerType/text()="season" and Language/text()="%(language)s" and Season/text()="%(season)s" and BannerType2/text()="seasonwide"]', u'//Banner[BannerType/text()="series" and Language/text()="%(language)s" and BannerType2/text()="graphical"]', '//Banner[BannerType/text()="season" and Language/text()="en" and Season/text()="%(season)s" and BannerType2/text()="seasonwide"]', '//Banner[BannerType/text()="season" and Season/text()="%(season)s" and BannerType2/text()="seasonwide"]', u'//Banner[BannerType/text()="series" and Language/text()="en" and BannerType2/text()="graphical"]', '//Banner[BannerType/text()="series" and BannerType2/text()="graphical"]'],
++ 'fanart': [u'//_banners/%(type)s/raw/item'],
++ 'poster': [u'//_banners/season/raw/item[subKey/text()="%(season)s"]',
++ u'//_banners/%(type)s/raw/item'],
++ 'banner': [u'//_banners/seasonwide/raw/item[subKey/text()="%(season)s"]',
++ u'//_banners/series/raw/item[subKey/text()="graphical"]'],
+ }
+ self.dataFilters = {
+- 'subtitle': u'//Data/Episode[SeasonNumber/text()="%(season)s" and EpisodeNumber/text()="%(episode)s"]/EpisodeName/text()',
+- 'description': u'//Data/Episode[SeasonNumber/text()="%(season)s" and EpisodeNumber/text()="%(episode)s"]/Overview/text()',
+- 'IMDB': u'//Data/Episode[SeasonNumber/text()="%(season)s" and EpisodeNumber/text()="%(episode)s"]/IMDB_ID/text()',
+- 'allEpisodes': u'//Data/Episode[SeasonNumber/text()="%(season)s" and EpisodeNumber/text()="%(episode)s"]',
++ 'subtitle': [u'//Data/Episode[SeasonNumber/text()="%(season)s" and EpisodeNumber/text()="%(episode)s"]/EpisodeName/text()',
++ u'//data/n%(season)s/n%(episode)s/episodeName/text()'],
++ 'description': [u'//Data/Episode[SeasonNumber/text()="%(season)s" and EpisodeNumber/text()="%(episode)s"]/Overview/text()',
++ u'//data/n%(season)s/n%(episode)s/overview/text()'],
++ 'IMDB': [u'//Data/Episode[SeasonNumber/text()="%(season)s" and EpisodeNumber/text()="%(episode)s"]/IMDB_ID/text()',
++ u'//data/n%(season)s/n%(episode)s/imdbId/text()'],
++ 'allEpisodes': [u'//Data/Episode[SeasonNumber/text()="%(season)s" and EpisodeNumber/text()="%(episode)s"]',
++ u'//data/n%(season)s/n%(episode)s'],
+ }
+ self.persistentResult = ''
+ # end __init__()
+@@ -119,6 +140,7 @@ class xpathFunctions(object):
+ """
+ self.FuncDict = {
+ 'lastUpdated': self.lastUpdated,
++ 'replace': self.replace,
+ 'htmlToString': self.htmlToString,
+ 'stringToList': self.stringToList,
+ 'imageElements': self.imageElements,
+@@ -187,26 +209,33 @@ class xpathFunctions(object):
+ 'episode': args[2][0].attrib['episode'],
+ }
+ filters = []
+- for index in range(len(self.filters[args[1]])):
+- filters.append(etree.XPath(self.filters[args[1]][index] % parmDict))
++ # print("image filter on %s" % args[1])
++ for filter in self.filters[args[1]]:
++ filters.append(etree.XPath(filter % parmDict))
+
+ # Get the preferred images
+ for xpathFilter in filters:
++ # print("xpf %r %r" % (args[0][0], xpathFilter))
+ for image in xpathFilter(args[0][0]):
+- if image.find('BannerPath') == None:
++ # print("im %r" % image)
++ # print(etree.tostring(image, method="xml", xml_declaration=False, pretty_print=True, ))
++ if image.find('fileName') == None:
+ continue
++ # print("im2 %r" % image)
+ tmpElement = etree.XML(u'<image></image>')
+ if args[1] == 'poster':
+ tmpElement.attrib['type'] = 'coverart'
+ else:
+ tmpElement.attrib['type'] = args[1]
+- tmpElement.attrib['url'] = u'http://www.thetvdb.com/banners/%s' % image.find('BannerPath').text
+- tmpElement.attrib['thumb'] = u'http://www.thetvdb.com/banners/_cache/%s' % image.find('BannerPath').text
+- tmpImageSize = image.find('BannerType2').text
+- index = tmpImageSize.find('x')
+- if index != -1:
+- tmpElement.attrib['width'] = tmpImageSize[:index]
+- tmpElement.attrib['height'] = tmpImageSize[index+1:]
++ tmpElement.attrib['url'] = u'http://www.thetvdb.com/banners/%s' % image.find('fileName').text
++ tmpElement.attrib['thumb'] = u'http://www.thetvdb.com/banners/%s' % image.find('thumbnail').text
++ tmpElement.attrib['rating'] = image.find('ratingsInfo').find('average').text
++ tmpImageSize = image.find('resolution').text
++ if tmpImageSize:
++ index = tmpImageSize.find('x')
++ if index != -1:
++ tmpElement.attrib['width'] = tmpImageSize[:index]
++ tmpElement.attrib['height'] = tmpImageSize[index+1:]
+ elementList.append(tmpElement)
+ if len(elementList):
+ break
+@@ -224,6 +253,13 @@ class xpathFunctions(object):
+ return text
+ # end textUtf8()
+
++ def replace(self, context, text, search_text, replace_text):
++ '''Replace search with replace
++ '''
++ text = self.textUtf8(text)
++ return text.replace(self.textUtf8(search_text), self.textUtf8(replace_text))
++ # end ampReplace()
++
+ def ampReplace(self, text):
+ '''Replace all "&" characters with "&"
+ '''
+@@ -279,8 +315,18 @@ class xpathFunctions(object):
+ 'season': args[0][0].attrib['season'],
+ 'episode': args[0][0].attrib['episode'],
+ }
+- xpathFilter = etree.XPath(self.dataFilters[args[2]] % parmDict)
+- results = xpathFilter(args[1][0])
++
++ # print("filter on list %r" % args[2])
++ filters = self.dataFilters[args[2]]
++ if isinstance(filters, list):
++ for filter in filters:
++ xpathFilter = etree.XPath(filter % parmDict)
++ results = xpathFilter(args[1][0])
++ if len(results):
++ break
++ else:
++ xpathFilter = etree.XPath(filters % parmDict)
++ results = xpathFilter(args[1][0])
+
+ # Sometimes all the results are required
+ if allValues == True:
+diff --git a/mythtv/bindings/python/MythTV/ttvdb/tvdb_api.py b/mythtv/bindings/python/MythTV/ttvdb/tvdb_api.py
+index 6aa08945e1..aaef0dd691 100644
+--- a/mythtv/bindings/python/MythTV/ttvdb/tvdb_api.py
++++ b/mythtv/bindings/python/MythTV/ttvdb/tvdb_api.py
+@@ -1,68 +1,283 @@
+ #!/usr/bin/env python
+-#encoding:utf-8
+-#author:dbr/Ben
+-#project:tvdb_api
+-#repository:http://github.com/dbr/tvdb_api
+-#license:Creative Commons GNU GPL v2
+-# (http://creativecommons.org/licenses/GPL/2.0/)
++# encoding:utf-8
++# author:dbr/Ben
++# project:tvdb_api
++# repository:http://github.com/dbr/tvdb_api
++# license:unlicense (http://unlicense.org/)
++import sys
++import os
++import time
++from . import requests_cache_compatability
++from . import tvdb_create_key
++import requests
++import requests_cache
++import getpass
++import tempfile
++import warnings
++import logging
++import datetime
+
+-"""Simple-to-use Python interface to The TVDB's API (www.thetvdb.com)
++"""Simple-to-use Python interface to The TVDB's API (thetvdb.com)
+
+ Example usage:
+
+ >>> from tvdb_api import Tvdb
+ >>> t = Tvdb()
+->>> t['Lost'][4][11]['episodename']
++>>> t['Lost'][4][11]['episodeName']
+ u'Cabin Fever'
+ """
+ __author__ = "dbr/Ben"
+-__version__ = "1.2.1"
++__version__ = "2.0-dev"
+
+-import os
+-import sys
+-import urllib
+-import urllib2
+-import tempfile
+-import logging
+
+-try:
+- import xml.etree.cElementTree as ElementTree
+-except ImportError:
+- import xml.etree.ElementTree as ElementTree
+-
+-from cache import CacheHandler
+-
+-from tvdb_ui import BaseUI, ConsoleUI
+-from tvdb_exceptions import (tvdb_error, tvdb_userabort, tvdb_shownotfound,
+- tvdb_seasonnotfound, tvdb_episodenotfound, tvdb_attributenotfound)
+-
+-try:
+- from StringIO import StringIO
+- from lxml import etree as eTree
+-except Exception, e:
+- sys.stderr.write(u'\n! Error - Importing the "lxml" and "StringIO" python libraries failed on error(%s)\n' % e)
+- sys.exit(1)
+-
+-# Check that the lxml library is current enough
+-# From the lxml documents it states: (http://codespeak.net/lxml/installation.html)
+-# "If you want to use XPath, do not use libxml2 2.6.27. We recommend libxml2 2.7.2 or later"
+-# Testing was performed with the Ubuntu 9.10 "python-lxml" version "2.1.5-1ubuntu2" repository package
+-version = ''
+-for digit in eTree.LIBXML_VERSION:
+- version+=str(digit)+'.'
+-version = version[:-1]
+-if version < '2.7.2':
+- sys.stderr.write(u'''
+-! Error - The installed version of the "lxml" python library "libxml" version is too old.
+- At least "libxml" version 2.7.2 must be installed. Your version is (%s).
+-''' % version)
+- sys.exit(1)
++IS_PY2 = sys.version_info[0] == 2
++
++if IS_PY2:
++ user_input = raw_input
++ from urllib import quote as url_quote
++else:
++ from urllib.parse import quote as url_quote
++ user_input = input
++
++
++if IS_PY2:
++ int_types = (int, long)
++ text_type = unicode
++else:
++ int_types = int
++ text_type = str
++
++lastTimeout = None
++
++
++def log():
++ return logging.getLogger("tvdb_api")
++
++
++## Exceptions
++
++class tvdb_exception(Exception):
++ """Any exception generated by tvdb_api
++ """
++ pass
++
++class tvdb_error(tvdb_exception):
++ """An error with thetvdb.com (Cannot connect, for example)
++ """
++ pass
++
++class tvdb_userabort(tvdb_exception):
++ """User aborted the interactive selection (via
++ the q command, ^c etc)
++ """
++ pass
++
++class tvdb_notauthorized(tvdb_exception):
++ """An authorization error with thetvdb.com
++ """
++ pass
++
++class tvdb_shownotfound(tvdb_exception):
++ """Show cannot be found on thetvdb.com (non-existant show)
++ """
++ pass
++
++class tvdb_seasonnotfound(tvdb_exception):
++ """Season cannot be found on thetvdb.com
++ """
++ pass
++
++class tvdb_episodenotfound(tvdb_exception):
++ """Episode cannot be found on thetvdb.com
++ """
++ pass
++
++class tvdb_resourcenotfound(tvdb_exception):
++ """Resource cannot be found on thetvdb.com
++ """
++ pass
++
++class tvdb_invalidlanguage(tvdb_exception):
++ """invalid language given on thetvdb.com
++ """
++ def __init__(self, value):
++ self.value = value
++
++ def __str__(self):
++ return repr(self.value)
++
++class tvdb_attributenotfound(tvdb_exception):
++ """Raised if an episode does not have the requested
++ attribute (such as a episode name)
++ """
++ pass
++
++
++## UI
++
++class BaseUI(object):
++ """Base user interface for Tvdb show selection.
++
++ Selects first show.
++
++ A UI is a callback. A class, it's __init__ function takes two arguments:
++
++ - config, which is the Tvdb config dict, setup in tvdb_api.py
++ - log, which is Tvdb's logger instance (which uses the logging module). You can
++ call log.info() log.warning() etc
+
++ It must have a method "selectSeries", this is passed a list of dicts, each dict
++ contains the the keys "name" (human readable show name), and "sid" (the shows
++ ID as on thetvdb.com). For example:
++
++ [{'name': u'Lost', 'sid': u'73739'},
++ {'name': u'Lost Universe', 'sid': u'73181'}]
++
++ The "selectSeries" method must return the appropriate dict, or it can raise
++ tvdb_userabort (if the selection is aborted), tvdb_shownotfound (if the show
++ cannot be found).
++
++ A simple example callback, which returns a random series:
++
++ >>> import random
++ >>> from tvdb_ui import BaseUI
++ >>> class RandomUI(BaseUI):
++ ... def selectSeries(self, allSeries):
++ ... import random
++ ... return random.choice(allSeries)
++
++ Then to use it..
++
++ >>> from tvdb_api import Tvdb
++ >>> t = Tvdb(custom_ui = RandomUI)
++ >>> random_matching_series = t['Lost']
++ >>> type(random_matching_series)
++ <class 'tvdb_api.Show'>
++ """
++ def __init__(self, config, log = None):
++ self.config = config
++ if log is not None:
++ warnings.warn("the UI's log parameter is deprecated, instead use\n"
++ "use import logging; logging.getLogger('ui').info('blah')\n"
++ "The self.log attribute will be removed in the next version")
++ self.log = logging.getLogger(__name__)
++
++ def selectSeries(self, allSeries):
++ return allSeries[0]
++
++
++class ConsoleUI(BaseUI):
++ """Interactively allows the user to select a show from a console based UI
++ """
++
++ def _displaySeries(self, allSeries, limit = 6):
++ """Helper function, lists series with corresponding ID
++ """
++ if limit is not None:
++ toshow = allSeries[:limit]
++ else:
++ toshow = allSeries
++
++ print("TVDB Search Results:")
++ for i, cshow in enumerate(toshow):
++ i_show = i + 1 # Start at more human readable number 1 (not 0)
++ log().debug('Showing allSeries[%s], series %s)' % (i_show, allSeries[i]['seriesName']))
++ if i == 0:
++ extra = " (default)"
++ else:
++ extra = ""
++
++ lid_map = dict((v, k) for (k, v) in self.config['langabbv_to_id'].items())
++
++ output = "%s -> %s [%s] # http://thetvdb.com/?tab=series&id=%s&lid=%s%s" % (
++ i_show,
++ cshow['seriesName'],
++ lid_map[cshow['lid']],
++ str(cshow['id']),
++ cshow['lid'],
++ extra
++ )
++ if IS_PY2:
++ print(output.encode("UTF-8", "ignore"))
++ else:
++ print(output)
++
++ def selectSeries(self, allSeries):
++ self._displaySeries(allSeries)
++
++ if len(allSeries) == 1:
++ # Single result, return it!
++ print("Automatically selecting only result")
++ return allSeries[0]
++
++ if self.config['select_first'] is True:
++ print("Automatically returning first search result")
++ return allSeries[0]
++
++ while True: # return breaks this loop
++ try:
++ print("Enter choice (first number, return for default, 'all', ? for help):")
++ ans = user_input()
++ except KeyboardInterrupt:
++ raise tvdb_userabort("User aborted (^c keyboard interupt)")
++ except EOFError:
++ raise tvdb_userabort("User aborted (EOF received)")
++
++ log().debug('Got choice of: %s' % (ans))
++ try:
++ selected_id = int(ans) - 1 # The human entered 1 as first result, not zero
++ except ValueError: # Input was not number
++ if len(ans.strip()) == 0:
++ # Default option
++ log().debug('Default option, returning first series')
++ return allSeries[0]
++ if ans == "q":
++ log().debug('Got quit command (q)')
++ raise tvdb_userabort("User aborted ('q' quit command)")
++ elif ans == "?":
++ print("## Help")
++ print("# Enter the number that corresponds to the correct show.")
++ print("# a - display all results")
++ print("# all - display all results")
++ print("# ? - this help")
++ print("# q - abort tvnamer")
++ print("# Press return with no input to select first result")
++ elif ans.lower() in ["a", "all"]:
++ self._displaySeries(allSeries, limit = None)
++ else:
++ log().debug('Unknown keypress %s' % (ans))
++ else:
++ log().debug('Trying to return ID: %d' % (selected_id))
++ try:
++ return allSeries[selected_id]
++ except IndexError:
++ log().debug('Invalid show number entered!')
++ print("Invalid number (%s) selected!")
++ self._displaySeries(allSeries)
++
++
++## Main API
+
+ class ShowContainer(dict):
+ """Simple dict that holds a series of Show instances
+ """
+- pass
++
++ def __init__(self):
++ self._stack = []
++ self._lastgc = time.time()
++
++ def __setitem__(self, key, value):
++ self._stack.append(key)
++
++ # keep only the 100th latest results
++ if time.time() - self._lastgc > 20:
++ for o in self._stack[:-100]:
++ del self[o]
++ self._stack = self._stack[-100:]
++
++ self._lastgc = time.time()
++
++ super(ShowContainer, self).__setitem__(key, value)
+
+
+ class Show(dict):
+@@ -73,12 +288,22 @@ class Show(dict):
+ self.data = {}
+
+ def __repr__(self):
+- return "<Show %s (containing %s seasons)>" % (
+- self.data.get(u'seriesname', 'instance'),
++ return "<Show %r (containing %s seasons)>" % (
++ self.data.get(u'seriesName', 'instance'),
+ len(self)
+ )
+
+ def __getitem__(self, key):
++ v1_compatibility = {
++ 'seriesname': 'seriesName',
++ }
++
++ if key in v1_compatibility:
++ import warnings
++ msg = "v1 usage is deprecated, please use new names: old: '%s', new: '%s'" % (
++ key, v1_compatibility[key])
++ key = v1_compatibility[key]
++
+ if key in self:
+ # Key is an episode, return it
+ return dict.__getitem__(self, key)
+@@ -90,16 +315,28 @@ class Show(dict):
+ # Data wasn't found, raise appropriate error
+ if isinstance(key, int) or key.isdigit():
+ # Episode number x was not found
+- raise tvdb_seasonnotfound("Could not find season %s" % (repr(key)))
++ raise tvdb_seasonnotfound(
++ "Could not find season %s" % (repr(key))
++ )
+ else:
+ # If it's not numeric, it must be an attribute name, which
+ # doesn't exist, so attribute error.
+- raise tvdb_attributenotfound("Cannot find attribute %s" % (repr(key)))
++ raise tvdb_attributenotfound(
++ "Cannot find attribute %s" % (repr(key))
++ )
++
++ def airedOn(self, date):
++ ret = self.search(str(date), 'firstAired')
++ if len(ret) == 0:
++ raise tvdb_episodenotfound(
++ "Could not find any episodes that aired on %s" % date
++ )
++ return ret
+
+- def search(self, term = None, key = None):
++ def search(self, term=None, key=None):
+ """
+- Search all episodes in show. Can search all data, or a specific key (for
+- example, episodename)
++ Search all episodes in show. Can search all data, or a specific key
++ (for example, episodename)
+
+ Always returns an array (can be empty). First index contains the first
+ match, and so on.
+@@ -121,27 +358,27 @@ class Show(dict):
+ containing "my first day":
+
+ >>> t['Scrubs'].search("my first day")
+- [<Episode 01x01 - My First Day>]
++ [<Episode 01x01 - u'My First Day'>]
+ >>>
+
+ Search for "My Name Is Earl" episode named "Faked His Own Death":
+
+- >>> t['My Name Is Earl'].search('Faked His Own Death', key = 'episodename')
+- [<Episode 01x04 - Faked His Own Death>]
++ >>> t['My Name Is Earl'].search('Faked My Own Death', key='episodeName')
++ [<Episode 01x04 - u'Faked My Own Death'>]
+ >>>
+
+ To search Scrubs for all episodes with "mentor" in the episode name:
+
+- >>> t['scrubs'].search('mentor', key = 'episodename')
+- [<Episode 01x02 - My Mentor>, <Episode 03x15 - My Tormented Mentor>]
++ >>> t['scrubs'].search('mentor', key='episodeName')
++ [<Episode 01x02 - u'My Mentor'>, <Episode 03x15 - u'My Tormented Mentor'>]
+ >>>
+
+ # Using search results
+
+ >>> results = t['Scrubs'].search("my first")
+- >>> print results[0]['episodename']
++ >>> print results[0]['episodeName']
+ My First Day
+- >>> for x in results: print x['episodename']
++ >>> for x in results: print x['episodeName']
+ My First Day
+ My First Step
+ My First Kill
+@@ -149,14 +386,19 @@ class Show(dict):
+ """
+ results = []
+ for cur_season in self.values():
+- searchresult = cur_season.search(term = term, key = key)
++ searchresult = cur_season.search(term=term, key=key)
+ if len(searchresult) != 0:
+ results.extend(searchresult)
+- #end for cur_season
++
+ return results
+
+
+ class Season(dict):
++ def __init__(self, show=None):
++ """The show attribute points to the parent show
++ """
++ self.show = show
++
+ def __repr__(self):
+ return "<Season instance (containing %s episodes)>" % (
+ len(self.keys())
+@@ -168,20 +410,20 @@ class Season(dict):
+ else:
+ return dict.__getitem__(self, episode_number)
+
+- def search(self, term = None, key = None):
++ def search(self, term=None, key=None):
+ """Search all episodes in season, returns a list of matching Episode
+ instances.
+
+ >>> t = Tvdb()
+ >>> t['scrubs'][1].search('first day')
+- [<Episode 01x01 - My First Day>]
++ [<Episode 01x01 - u'My First Day'>]
+ >>>
+
+ See Show.search documentation for further information on search
+ """
+ results = []
+ for ep in self.values():
+- searchresult = ep.search(term = term, key = key)
++ searchresult = ep.search(term=term, key=key)
+ if searchresult is not None:
+ results.append(
+ searchresult
+@@ -190,12 +432,17 @@ class Season(dict):
+
+
+ class Episode(dict):
++ def __init__(self, season=None):
++ """The season attribute points to the parent season
++ """
++ self.season = season
++
+ def __repr__(self):
+- seasno = int(self.get(u'seasonnumber', 0))
+- epno = int(self.get(u'episodenumber', 0))
+- epname = self.get(u'episodename')
++ seasno = self.get(u'airedSeason', 0)
++ epno = self.get(u'airedEpisodeNumber', 0)
++ epname = self.get(u'episodeName')
+ if epname is not None:
+- return "<Episode %02dx%02d - %s>" % (seasno, epno, epname)
++ return "<Episode %02dx%02d - %r>" % (seasno, epno, epname)
+ else:
+ return "<Episode %02dx%02d>" % (seasno, epno)
+
+@@ -203,9 +450,31 @@ class Episode(dict):
+ try:
+ return dict.__getitem__(self, key)
+ except KeyError:
+- raise tvdb_attributenotfound("Cannot find attribute %s" % (repr(key)))
++ v1_compatibility = {
++ 'episodenumber': 'airedEpisodeNumber',
++ 'firstaired': 'firstAired',
++ 'seasonnumber': 'airedSeason',
++ 'episodename': 'episodeName',
++ }
++ if key in v1_compatibility:
++ import warnings
++ msg = "v1 usage is deprecated, please use new names: old: '%s', new: '%s'" % (
++ key, v1_compatibility[key])
++ warnings.warn(msg, category=DeprecationWarning)
++ try:
++ value = dict.__getitem__(self, v1_compatibility[key])
++ if key in ['episodenumber', 'seasonnumber']:
++ # This was a string in v1
++ return str(value)
++ else:
++ return value
++ except KeyError:
++ # We either return something or we get the exception below
++ pass
++
++ raise tvdb_attributenotfound("Cannot find attribute %s" % (repr(key)))
+
+- def search(self, term = None, key = None):
++ def search(self, term=None, key=None):
+ """Search episode data for term, if it matches, return the Episode (self).
+ The key parameter can be used to limit the search to a specific element,
+ for example, episodename.
+@@ -216,30 +485,29 @@ class Episode(dict):
+ Simple example:
+
+ >>> e = Episode()
+- >>> e['episodename'] = "An Example"
++ >>> e['episodeName'] = "An Example"
+ >>> e.search("examp")
+- <Episode 00x00 - An Example>
++ <Episode 00x00 - 'An Example'>
+ >>>
+
+ Limiting by key:
+
+- >>> e.search("examp", key = "episodename")
+- <Episode 00x00 - An Example>
++ >>> e.search("examp", key = "episodeName")
++ <Episode 00x00 - 'An Example'>
+ >>>
+ """
+- if term == None:
++ if term is None:
+ raise TypeError("must supply string to search for (contents)")
+
+- term = unicode(term).lower()
++ term = text_type(term).lower()
+ for cur_key, cur_value in self.items():
+- cur_key, cur_value = unicode(cur_key).lower(), unicode(cur_value).lower()
++ cur_key = text_type(cur_key)
++ cur_value = text_type(cur_value).lower()
+ if key is not None and cur_key != key:
+ # Do not search this key
+ continue
+- if cur_value.find( unicode(term).lower() ) > -1:
++ if cur_value.find(text_type(term)) > -1:
+ return self
+- #end if cur_value.find()
+- #end for cur_key, cur_value
+
+
+ class Actors(list):
+@@ -258,26 +526,31 @@ class Actor(dict):
+ sortorder
+ """
+ def __repr__(self):
+- return "<Actor \"%s\">" % (self.get("name"))
++ return "<Actor %r>" % self.get("name")
+
+
+ class Tvdb:
+ """Create easy-to-use interface to name of season/episode name
+ >>> t = Tvdb()
+- >>> t['Scrubs'][1][24]['episodename']
++ >>> t['Scrubs'][1][24]['episodeName']
+ u'My Last Day'
+ """
+ def __init__(self,
+- interactive = False,
+- select_first = False,
+- debug = False,
+- cache = True,
+- banners = False,
+- actors = False,
+- custom_ui = None,
+- language = None,
+- search_all_languages = False,
+- apikey = None):
++ interactive=False,
++ select_first=False,
++ debug=False,
++ cache=True,
++ banners=False,
++ actors=False,
++ custom_ui=None,
++ language=None,
++ search_all_languages=False,
++ apikey=None,
++ username=None,
++ userkey=None,
++ forceConnect=False,
++ dvdorder=False):
++
+ """interactive (True/False):
+ When True, uses built-in console UI is used to select the correct show.
+ When False, the first search result is used.
+@@ -287,20 +560,28 @@ class Tvdb:
+ than showing the user a list of more than one series).
+ Is overridden by interactive = False, or specifying a custom_ui
+
+- debug (True/False):
+- shows verbose debugging information
++ debug (True/False) DEPRECATED:
++ Replaced with proper use of logging module. To show debug messages:
++
++ >>> import logging
++ >>> logging.basicConfig(level = logging.DEBUG)
++
++ cache (True/False/str/requests_cache.CachedSession):
+
+- cache (True/False/str/unicode):
+- Retrieved XML are persisted to to disc. If true, stores in tvdb_api
+- folder under your systems TEMP_DIR, if set to str/unicode instance it
+- will use this as the cache location. If False, disables caching.
++ Retrieved URLs can be persisted to to disc.
++
++ True/False enable or disable default caching. Passing
++ string specifies the directory where to store the
++ "tvdb.sqlite3" cache file. Alternatively a custom
++ requests.Session instance can be passed (e.g maybe a
++ customised instance of `requests_cache.CachedSession`)
+
+ banners (True/False):
+ Retrieves the banners for a show. These are accessed
+ via the _banners key of a Show(), for example:
+
+ >>> Tvdb(banners=True)['scrubs']['_banners'].keys()
+- ['fanart', 'poster', 'series', 'season']
++ [u'fanart', u'poster', u'seasonwide', u'season', u'series']
+
+ actors (True/False):
+ Retrieves a list of the actors for a show. These are accessed
+@@ -308,7 +589,7 @@ class Tvdb:
+
+ >>> t = Tvdb(actors=True)
+ >>> t['scrubs']['_actors'][0]['name']
+- u'Zach Braff'
++ u'John C. McGinley'
+
+ custom_ui (tvdb_ui.BaseUI subclass):
+ A callable subclass of tvdb_ui.BaseUI (overrides interactive option)
+@@ -331,165 +612,259 @@ class Tvdb:
+ own key if desired - this is recommended if you are embedding
+ tvdb_api in a larger application)
+ See http://thetvdb.com/?tab=apiregister to get your own key
++
++ username (str/unicode):
++ Override the default thetvdb.com username. By default it will use
++ tvdb_api's own username (fine for small scripts), but you can use your
++ own key if desired - this is recommended if you are embedding
++ tvdb_api in a larger application)
++ See http://thetvdb.com/ to register an account
++
++ userkey (str/unicode):
++ Override the default thetvdb.com userkey. By default it will use
++ tvdb_api's own userkey (fine for small scripts), but you can use your
++ own key if desired - this is recommended if you are embedding
++ tvdb_api in a larger application)
++ See http://thetvdb.com/ to register an account
++
++ forceConnect (bool):
++ If true it will always try to connect to theTVDB.com even if we
++ recently timed out. By default it will wait one minute before
++ trying again, and any requests within that one minute window will
++ return an exception immediately.
+ """
+- self.shows = ShowContainer() # Holds all Show classes
+- self.corrections = {} # Holds show-name to show_id mapping
++
++ global lastTimeout
++
++ # if we're given a lastTimeout that is less than 1 min just give up
++ if not forceConnect and lastTimeout is not None and datetime.datetime.now() - lastTimeout < datetime.timedelta(minutes=1):
++ raise tvdb_error("We recently timed out, so giving up early this time")
++
++ self.shows = ShowContainer() # Holds all Show classes
++ self.corrections = {} # Holds show-name to show_id mapping
+
+ self.config = {}
+
+- if apikey is not None:
+- self.config['apikey'] = apikey
++ if apikey and username and userkey:
++ self.config['auth_payload'] = {
++ "apikey": apikey,
++ "username": username,
++ "userkey": userkey
++ }
+ else:
+- self.config['apikey'] = "0629B785CE550C8D" # tvdb_api's API key
++ self.config['auth_payload'] = {
++ "apikey": "0629B785CE550C8D",
++ "userkey": "",
++ "username": ""
++ }
+
+- self.config['debug_enabled'] = debug # show debugging messages
++ self.config['debug_enabled'] = debug # show debugging messages
+
+ self.config['custom_ui'] = custom_ui
+
+- self.config['interactive'] = interactive # prompt for correct series?
++ self.config['interactive'] = interactive # prompt for correct series?
+
+ self.config['select_first'] = select_first
+
+ self.config['search_all_languages'] = search_all_languages
+
++ self.config['dvdorder'] = dvdorder
++
+ if cache is True:
++ self.session = requests_cache.CachedSession(
++ expire_after=21600, # 6 hours
++ backend='sqlite',
++ cache_name=self._getTempDir(),
++ include_get_headers=True
++ )
++ self.session.remove_expired_responses()
+ self.config['cache_enabled'] = True
+- self.config['cache_location'] = self._getTempDir()
+- elif isinstance(cache, basestring):
+- self.config['cache_enabled'] = True
+- self.config['cache_location'] = cache
+- else:
++ elif cache is False:
++ self.session = requests.Session()
+ self.config['cache_enabled'] = False
+-
+- if self.config['cache_enabled']:
+- self.urlopener = urllib2.build_opener(
+- CacheHandler(self.config['cache_location'])
+- )
++ elif isinstance(cache, str):
++ # Specified cache path
++ self.session = requests_cache.CachedSession(
++ expire_after=21600, # 6 hours
++ backend='sqlite',
++ cache_name=os.path.join(cache, "tvdb_api"),
++ include_get_headers=True
++ )
++ self.session.remove_expired_responses()
+ else:
+- self.urlopener = urllib2.build_opener()
++ self.session = cache
++ try:
++ self.session.get
++ except AttributeError:
++ raise ValueError("cache argument must be True/False, string as cache path or requests.Session-type object (e.g from requests_cache.CachedSession)")
+
+ self.config['banners_enabled'] = banners
+ self.config['actors_enabled'] = actors
+
+- self.log = self._initLogger() # Setups the logger (self.log.debug() etc)
++ if self.config['debug_enabled']:
++ warnings.warn(
++ "The debug argument to tvdb_api.__init__ will be removed in the next version. "
++ "To enable debug messages, use the following code before importing: "
++ "import logging; logging.basicConfig(level=logging.DEBUG)"
++ )
++ logging.basicConfig(level=logging.DEBUG)
+
+- # List of language from http://www.thetvdb.com/api/0629B785CE550C8D/languages.xml
++ # List of language from http://thetvdb.com/api/0629B785CE550C8D/languages.xml
+ # Hard-coded here as it is realtively static, and saves another HTTP request, as
+ # recommended on http://thetvdb.com/wiki/index.php/API:languages.xml
+ self.config['valid_languages'] = [
+- "da", "fi", "nl", "de", "it", "es", "fr","pl", "hu","el","tr",
+- "ru","he","ja","pt","zh","cs","sl", "hr","ko","en","sv","no"
++ "da", "fi", "nl", "de", "it", "es", "fr", "pl", "hu", "el", "tr",
++ "ru", "he", "ja", "pt", "zh", "cs", "sl", "hr", "ko", "en", "sv",
++ "no"
+ ]
+
++ # thetvdb.com should be based around numeric language codes,
++ # but to link to a series like http://thetvdb.com/?tab=series&id=79349&lid=16
++ # requires the language ID, thus this mapping is required (mainly
++ # for usage in tvdb_ui - internally tvdb_api will use the language abbreviations)
++ self.config['langabbv_to_id'] = {
++ 'el': 20, 'en': 7, 'zh': 27, 'it': 15, 'cs': 28, 'es': 16,
++ 'ru': 22, 'nl': 13, 'pt': 26, 'no': 9, 'tr': 21, 'pl': 18,
++ 'fr': 17, 'hr': 31, 'de': 14, 'da': 10, 'fi': 11, 'hu': 19,
++ 'ja': 25, 'he': 24, 'ko': 32, 'sv': 8, 'sl': 30
++ }
++
+ if language is None:
+- self.config['language'] = "en"
+- elif language not in self.config['valid_languages']:
+- raise ValueError("Invalid language %s, options are: %s" % (
+- language, self.config['valid_languages']
+- ))
++ self.config['language'] = 'en'
+ else:
+- self.config['language'] = language
++ if language not in self.config['valid_languages']:
++ raise ValueError("Invalid language %s, options are: %s" % (
++ language, self.config['valid_languages']
++ ))
++ else:
++ self.config['language'] = language
+
+ # The following url_ configs are based of the
+ # http://thetvdb.com/wiki/index.php/Programmers_API
+- self.config['base_url'] = "http://www.thetvdb.com"
++ self.config['base_url'] = "http://thetvdb.com"
++ self.config['api_url'] = "https://api.thetvdb.com"
+
+- if self.config['search_all_languages']:
+- self.config['url_getSeries'] = "%(base_url)s/api/GetSeries.php?seriesname=%%s&language=all" % self.config
+- else:
+- self.config['url_getSeries'] = "%(base_url)s/api/GetSeries.php?seriesname=%%s&language=%(language)s" % self.config
++ self.config['url_getSeries'] = u"%(api_url)s/search/series?name=%%s" % self.config
+
+- self.config['url_epInfo'] = "%(base_url)s/api/%(apikey)s/series/%%s/all/%(language)s.xml" % self.config
++ self.config['url_epInfo'] = u"%(api_url)s/series/%%s/episodes" % self.config
++ self.config['url_epDetail'] = u"%(api_url)s/episodes/%%s" % self.config
+
+- self.config['url_seriesInfo'] = "%(base_url)s/api/%(apikey)s/series/%%s/%(language)s.xml" % self.config
+- self.config['url_actorsInfo'] = "%(base_url)s/api/%(apikey)s/series/%%s/actors.xml" % self.config
++ self.config['url_seriesInfo'] = u"%(api_url)s/series/%%s" % self.config
++ self.config['url_actorsInfo'] = u"%(api_url)s/series/%%s/actors" % self.config
+
+- self.config['url_seriesBanner'] = "%(base_url)s/api/%(apikey)s/series/%%s/banners.xml" % self.config
+- self.config['url_artworkPrefix'] = "%(base_url)s/banners/%%s" % self.config
++ self.config['url_seriesBanner'] = u"%(api_url)s/series/%%s/images" % self.config
++ self.config['url_seriesBannerInfo'] = u"%(api_url)s/series/%%s/images/query?keyType=%%s" % self.config
++ self.config['url_artworkPrefix'] = u"%(base_url)s/banners/%%s" % self.config
+
+- # Initialize XML display value to off
+- self.xml = False
+- self.searchTree = None
+- self.seriesInfoTree = None
+- self.epInfoTree = None
+- self.actorsInfoTree = None
+- self.imagesInfoTree = None
+- self.baseXsltDir = u'%s/XSLT/' % os.path.dirname( os.path.realpath( __file__ ))
+- #end __init__
++ self.__authorized = False
++ self.headers = {'Content-Type': 'application/json',
++ 'Accept': 'application/json',
++ 'Accept-Language': self.config['language'],
++ 'User-Agent': 'tvdb/2.0'
++ }
+
+- def _initLogger(self):
+- """Setups a logger using the logging module, returns a log object
++ def _getTempDir(self):
++ """Returns the [system temp dir]/tvdb_api-u501 (or
++ tvdb_api-myuser)
+ """
+- logger = logging.getLogger("tvdb")
+- formatter = logging.Formatter('%(asctime)s) %(levelname)s %(message)s')
++ if hasattr(os, 'getuid'):
++ uid = "u%d" % (os.getuid())
++ else:
++ # For Windows
++ try:
++ uid = getpass.getuser()
++ except ImportError:
++ return os.path.join(tempfile.gettempdir(), "tvdb_api")
+
+- hdlr = logging.StreamHandler(sys.stdout)
++ return os.path.join(tempfile.gettempdir(), "tvdb_api-%s" % (uid))
+
+- hdlr.setFormatter(formatter)
+- logger.addHandler(hdlr)
++ def _loadUrl(self, url, data=None, recache=False, language=None):
++ """Return response from The TVDB API"""
+
+- if self.config['debug_enabled']:
+- logger.setLevel(logging.DEBUG)
++ if not language:
++ language = self.config['language']
++ if language not in self.config['valid_languages']:
++ raise ValueError("Invalid language %s, options are: %s" % (
++ language, self.config['valid_languages']
++ ))
++ self.headers['Accept-Language'] = language
++
++ # TODO: обрабатывать исключения (Handle Exceptions)
++ # TODO: обновлять токен (Update Token)
++ # encoded url is used for hashing in the cache so
++ # python 2 and 3 generate the same hash
++ if not self.__authorized:
++ # only authorize of we haven't before and we
++ # don't have the url in the cache
++ fake_session_for_key = requests.Session()
++ fake_session_for_key.headers['Accept-Language'] = language
++ cache_key = None
++ try:
++ # in case the session class has no cache object, fail gracefully
++ cache_key = self.session.cache.create_key(fake_session_for_key.prepare_request(requests.Request('GET', url)))
++ except:
++ pass
++ if not cache_key or not self.session.cache.has_key(cache_key):
++ self.authorize()
++
++ response = self.session.get(url, headers=self.headers)
++ r = response.json()
++ log().debug("loadurl: %s lid=%s" % (url, language))
++ log().debug("response:")
++ log().debug(r)
++ error = r.get('Error')
++ errors = r.get('errors')
++ r_data = r.get('data')
++ links = r.get('links')
++
++ if error:
++ if error == u'Resource not found':
++ # raise(tvdb_resourcenotfound)
++ # handle no data at a different level so it is more specific
++ pass
++ if error == u'Not Authorized':
++ raise(tvdb_notauthorized)
++ if errors:
++ if u'invalidLanguage' in errors:
++ # raise(tvdb_invalidlanguage(errors[u'invalidLanguage']))
++ # invalidLanguage does not mean there is no data
++ # there is just less data
++ pass
++
++ if data and isinstance(data, list):
++ data.extend(r_data)
+ else:
+- logger.setLevel(logging.WARNING)
+- return logger
+- #end initLogger
+-
+- def _getTempDir(self):
+- """Returns the [system temp dir]/tvdb_api
+- """
+- return os.path.join(tempfile.gettempdir(), "tvdb_api")
++ data = r_data
+
+- def _loadUrl(self, url, recache = False):
+- try:
+- self.log.debug("Retrieving URL %s" % url)
+- resp = self.urlopener.open(url)
+- if 'x-local-cache' in resp.headers:
+- self.log.debug("URL %s was cached in %s" % (
+- url,
+- resp.headers['x-local-cache'])
+- )
+- if recache:
+- self.log.debug("Attempting to recache %s" % url)
+- resp.recache()
+- except urllib2.URLError, errormsg:
+- raise tvdb_error("Could not connect to server: %s" % (errormsg))
+- #end try
++ if links and links['next']:
++ url = url.split('?')[0]
++ _url = url + "?page=%s" % links['next']
++ self._loadUrl(_url, data)
+
+- return resp.read()
++ return data
+
+- def _getetsrc(self, url):
++ def authorize(self):
++ log().debug("auth")
++ r = self.session.post('https://api.thetvdb.com/login', json=self.config['auth_payload'], headers=self.headers)
++ r_json = r.json()
++ error = r_json.get('Error')
++ if error:
++ if error == u'Not Authorized':
++ raise(tvdb_notauthorized)
++ token = r_json.get('token')
++ self.headers['Authorization'] = "Bearer %s" % text_type(token)
++ self.__authorized = True
++
++ def _getetsrc(self, url, language=None):
+ """Loads a URL using caching, returns an ElementTree of the source
+ """
+- src = self._loadUrl(url)
+- try:
+- if self.xml:
+- self.tmpTree = eTree.XML(src)
+- return ElementTree.fromstring(src)
+- except SyntaxError:
+- src = self._loadUrl(url, recache=True)
+- try:
+- if self.xml:
+- self.tmpTree = eTree.XML(src)
+- return ElementTree.fromstring(src)
+- except SyntaxError, exceptionmsg:
+- errormsg = "There was an error with the XML retrieved from thetvdb.com:\n%s" % (
+- exceptionmsg
+- )
+-
+- if self.config['cache_enabled']:
+- errormsg += "\nFirst try emptying the cache folder at..\n%s" % (
+- self.config['cache_location']
+- )
++ src = self._loadUrl(url, language=language)
+
+- errormsg += "\nIf this does not resolve the issue, please try again later. If the error persists, report a bug on"
+- errormsg += "\nhttp://dbr.lighthouseapp.com/projects/13342-tvdb_api/overview\n"
+- raise tvdb_error(errormsg)
+- #end _getetsrc
++ return src
+
+ def _setItem(self, sid, seas, ep, attrib, value):
+ """Creates a new episode, creating Show(), Season() and
+- Episode()s as required. Called by _getShowData to populute
++ Episode()s as required. Called by _getShowData to populate show
+
+ Since the nice-to-use tvdb[1][24]['name] interface
+ makes it impossible to do tvdb[1][24]['name] = "name"
+@@ -505,11 +880,10 @@ class Tvdb:
+ if sid not in self.shows:
+ self.shows[sid] = Show()
+ if seas not in self.shows[sid]:
+- self.shows[sid][seas] = Season()
++ self.shows[sid][seas] = Season(show=self.shows[sid])
+ if ep not in self.shows[sid][seas]:
+- self.shows[sid][seas][ep] = Episode()
++ self.shows[sid][seas][ep] = Episode(season=self.shows[sid][seas])
+ self.shows[sid][seas][ep][attrib] = value
+- #end _set_item
+
+ def _setShowData(self, sid, key, value):
+ """Sets self.shows[sid] to a new Show instance, or sets the data
+@@ -518,17 +892,25 @@ class Tvdb:
+ self.shows[sid] = Show()
+ self.shows[sid].data[key] = value
+
+- def _cleanData(self, data):
+- """Cleans up strings returned by TheTVDB.com
+-
+- Issues corrected:
+- - Replaces & with &
+- - Trailing whitespace
++ def search(self, series):
++ """This searches TheTVDB.com for the series name
++ and returns the result list
+ """
+- data = data.replace(u"&", u"&")
+- data = data.strip()
+- return data
+- #end _cleanData
++ series = url_quote(series.encode("utf-8"))
++ log().debug("Searching for show %s" % series)
++ seriesEt = self._getetsrc(self.config['url_getSeries'] % (series))
++ if not seriesEt:
++ log().debug('Series result returned zero')
++ raise tvdb_shownotfound("Show-name search returned zero results (cannot find show on TVDB)")
++
++ allSeries = []
++ for series in seriesEt:
++ series['lid'] = self.config['langabbv_to_id'][self.config['language']]
++ series['language'] = self.config['language']
++ log().debug('Found series %(seriesName)s' % series)
++ allSeries.append(series)
++
++ return allSeries
+
+ def _getSeries(self, series):
+ """This searches TheTVDB.com for the series name,
+@@ -536,52 +918,32 @@ class Tvdb:
+ series. If not, and interactive == True, ConsoleUI is used, if not
+ BaseUI is used to select the first result.
+ """
+- series = urllib.quote(series.encode("utf-8"))
+- self.log.debug("Searching for show %s" % series)
+- seriesEt = self._getetsrc(self.config['url_getSeries'] % (series))
+- if self.xml:
+- self.searchTree = self.tmpTree
+- allSeries = []
+- for series in seriesEt:
+- sn = series.find('SeriesName')
+- value = self._cleanData(sn.text)
+- cur_sid = series.find('id').text
+- self.log.debug('Found series %s (id: %s)' % (value, cur_sid))
+- allSeries.append( {'sid':cur_sid, 'name':value} )
+- #end for series
+-
+- if len(allSeries) == 0:
+- self.log.debug('Series result returned zero')
+- raise tvdb_shownotfound("Show-name search returned zero results (cannot find show on TVDB)")
++ allSeries = self.search(series)
+
+ if self.config['custom_ui'] is not None:
+- self.log.debug("Using custom UI %s" % (repr(self.config['custom_ui'])))
+- ui = self.config['custom_ui'](config = self.config, log = self.log)
++ log().debug("Using custom UI %s" % (repr(self.config['custom_ui'])))
++ ui = self.config['custom_ui'](config=self.config)
+ else:
+ if not self.config['interactive']:
+- self.log.debug('Auto-selecting first search result using BaseUI')
+- ui = BaseUI(config = self.config, log = self.log)
++ log().debug('Auto-selecting first search result using BaseUI')
++ ui = BaseUI(config=self.config)
+ else:
+- self.log.debug('Interactivily selecting show using ConsoleUI')
+- ui = ConsoleUI(config = self.config, log = self.log)
+- #end if config['interactive]
+- #end if custom_ui != None
++ log().debug('Interactively selecting show using ConsoleUI')
++ ui = ConsoleUI(config=self.config)
+
+ return ui.selectSeries(allSeries)
+
+- #end _getSeries
+-
+ def _parseBanners(self, sid):
+ """Parses banners XML, from
+- http://www.thetvdb.com/api/[APIKEY]/series/[SERIES ID]/banners.xml
++ http://thetvdb.com/api/[APIKEY]/series/[SERIES ID]/banners.xml
+
+ Banners are retrieved using t['show name]['_banners'], for example:
+
+ >>> t = Tvdb(banners = True)
+ >>> t['scrubs']['_banners'].keys()
+- ['fanart', 'poster', 'series', 'season']
+- >>> t['scrubs']['_banners']['poster']['680x1000']['35308']['_bannerpath']
+- 'http://www.thetvdb.com/banners/posters/76156-2.jpg'
++ [u'fanart', u'poster', u'seasonwide', u'season', u'series']
++ >>> t['scrubs']['_banners']['poster']['680x1000'][35308]['_bannerpath']
++ u'http://thetvdb.com/banners/posters/76156-2.jpg'
+ >>>
+
+ Any key starting with an underscore has been processed (not the raw
+@@ -589,121 +951,42 @@ class Tvdb:
+
+ This interface will be improved in future versions.
+ """
+- self.log.debug('Getting season banners for %s' % (sid))
+- bannersEt = self._getetsrc( self.config['url_seriesBanner'] % (sid) )
+- if self.xml:
+- self.imagesInfoTree = self.tmpTree
++ log().debug('Getting season banners for %s' % (sid))
++ bannersEt = self._getetsrc(self.config['url_seriesBanner'] % sid)
+ banners = {}
+- for cur_banner in bannersEt.findall('Banner'):
+- bid = cur_banner.find('id').text
+- btype = cur_banner.find('BannerType')
+- btype2 = cur_banner.find('BannerType2')
+- if btype is None or btype2 is None:
+- continue
+- btype, btype2 = btype.text, btype2.text
+- if not btype in banners:
+- banners[btype] = {}
+- if not btype2 in banners[btype]:
+- banners[btype][btype2] = {}
+- if not bid in banners[btype][btype2]:
+- banners[btype][btype2][bid] = {}
+-
+- self.log.debug("Banner: %s", bid)
+- for cur_element in cur_banner.getchildren():
+- tag = cur_element.tag.lower()
+- value = cur_element.text
+- if tag is None or value is None:
++ for cur_banner in bannersEt.keys():
++ banners_info = self._getetsrc(self.config['url_seriesBannerInfo'] % (sid, cur_banner))
++ for banner_info in banners_info:
++ bid = banner_info.get('id')
++ btype = banner_info.get('keyType')
++ btype2 = banner_info.get('resolution')
++ if btype is None or btype2 is None:
+ continue
+- tag, value = tag.lower(), value.lower()
+- self.log.debug("Banner info: %s = %s" % (tag, value))
+- banners[btype][btype2][bid][tag] = value
+-
+- for k, v in banners[btype][btype2][bid].items():
+- if k.endswith("path"):
+- new_key = "_%s" % (k)
+- self.log.debug("Transforming %s to %s" % (k, new_key))
+- new_url = self.config['url_artworkPrefix'] % (v)
+- self.log.debug("New banner URL: %s" % (new_url))
+- banners[btype][btype2][bid][new_key] = new_url
+-
+- self._setShowData(sid, "_banners", banners)
+-
+-
+- # Alternate tvdb_api's method for retrieving graphics URLs but returned as a list that preserves
+- # the user rating order highest rated to lowest rated
+- def ttvdb_parseBanners(self, sid):
+- """Parses banners XML, from
+- http://www.thetvdb.com/api/[APIKEY]/series/[SERIES ID]/banners.xml
+-
+- Banners are retrieved using t['show name]['_banners'], for example:
+
+- >>> t = Tvdb(banners = True)
+- >>> t['scrubs']['_banners'].keys()
+- ['fanart', 'poster', 'series', 'season']
+- >>> t['scrubs']['_banners']['poster']['680x1000']['35308']['_bannerpath']
+- 'http://www.thetvdb.com/banners/posters/76156-2.jpg'
+- >>>
++ if btype not in banners:
++ banners[btype] = {}
++ if btype2 not in banners[btype]:
++ banners[btype][btype2] = {}
++ if bid not in banners[btype][btype2]:
++ banners[btype][btype2][bid] = {}
+
+- Any key starting with an underscore has been processed (not the raw
+- data from the XML)
++ banners[btype][btype2][bid]['bannerpath'] = banner_info['fileName']
++ banners[btype][btype2][bid]['resolution'] = banner_info['resolution']
++ banners[btype][btype2][bid]['subKey'] = banner_info['subKey']
+
+- This interface will be improved in future versions.
+- Changed in this interface is that a list or URLs is created to preserve the user rating order from
+- top rated to lowest rated.
+- """
+-
+- self.log.debug('Getting season banners for %s' % (sid))
+- bannersEt = self._getetsrc( self.config['url_seriesBanner'] % (sid) )
+- if self.xml:
+- self.imagesInfoTree = self.tmpTree
+- banners = {}
+- bid_order = {'fanart': [], 'poster': [], 'series': [], 'season': []}
+- for cur_banner in bannersEt.findall('Banner'):
+- bid = cur_banner.find('id').text
+- btype = cur_banner.find('BannerType')
+- btype2 = cur_banner.find('BannerType2')
+- if btype is None or btype2 is None:
+- continue
+- btype, btype2 = btype.text, btype2.text
+- if not btype in banners:
+- banners[btype] = {}
+- if not btype2 in banners[btype]:
+- banners[btype][btype2] = {}
+- if not bid in banners[btype][btype2]:
+- banners[btype][btype2][bid] = {}
+- if btype in bid_order.keys():
+- if btype2 != u'blank':
+- bid_order[btype].append([bid, btype2])
+-
+- self.log.debug("Banner: %s", bid)
+- for cur_element in cur_banner.getchildren():
+- tag = cur_element.tag.lower()
+- value = cur_element.text
+- if tag is None or value is None:
+- continue
+- tag, value = tag.lower(), value.lower()
+- self.log.debug("Banner info: %s = %s" % (tag, value))
+- banners[btype][btype2][bid][tag] = value
+-
+- for k, v in banners[btype][btype2][bid].items():
+- if k.endswith("path"):
+- new_key = "_%s" % (k)
+- self.log.debug("Transforming %s to %s" % (k, new_key))
+- new_url = self.config['url_artworkPrefix'] % (v)
+- self.log.debug("New banner URL: %s" % (new_url))
+- banners[btype][btype2][bid][new_key] = new_url
+-
+- graphics_in_order = {'fanart': [], 'poster': [], 'series': [], 'season': []}
+- for key in bid_order.keys():
+- for bid in bid_order[key]:
+- graphics_in_order[key].append(banners[key][bid[1]][bid[0]])
+- return graphics_in_order
+- # end ttvdb_parseBanners()
++ for k, v in list(banners[btype][btype2][bid].items()):
++ if k.endswith("path"):
++ new_key = "_%s" % k
++ log().debug("Transforming %s to %s" % (k, new_key))
++ new_url = self.config['url_artworkPrefix'] % v
++ banners[btype][btype2][bid][new_key] = new_url
+
++ banners[btype]['raw'] = banners_info
++ self._setShowData(sid, "_banners", banners)
+
+ def _parseActors(self, sid):
+ """Parsers actors XML, from
+- http://www.thetvdb.com/api/[APIKEY]/series/[SERIES ID]/actors.xml
++ http://thetvdb.com/api/[APIKEY]/series/[SERIES ID]/actors.xml
+
+ Actors are retrieved using t['show name]['_actors'], for example:
+
+@@ -714,59 +997,71 @@ class Tvdb:
+ >>> type(actors[0])
+ <class 'tvdb_api.Actor'>
+ >>> actors[0]
+- <Actor "Zach Braff">
++ <Actor u'John C. McGinley'>
+ >>> sorted(actors[0].keys())
+- ['id', 'image', 'name', 'role', 'sortorder']
++ [u'id', u'image', u'imageAdded', u'imageAuthor', u'lastUpdated', u'name', u'role', u'seriesId', u'sortOrder']
+ >>> actors[0]['name']
+- u'Zach Braff'
++ u'John C. McGinley'
+ >>> actors[0]['image']
+- 'http://www.thetvdb.com/banners/actors/43640.jpg'
++ u'http://thetvdb.com/banners/actors/43638.jpg'
+
+ Any key starting with an underscore has been processed (not the raw
+ data from the XML)
+ """
+- self.log.debug("Getting actors for %s" % (sid))
++ log().debug("Getting actors for %s" % (sid))
+ actorsEt = self._getetsrc(self.config['url_actorsInfo'] % (sid))
+- if self.xml:
+- self.actorsInfoTree = self.tmpTree
++
+ cur_actors = Actors()
+- for curActorItem in actorsEt.findall("Actor"):
+- curActor = Actor()
+- for curInfo in curActorItem:
+- tag = curInfo.tag.lower()
+- value = curInfo.text
+- if value is not None:
+- if tag == "image":
+- value = self.config['url_artworkPrefix'] % (value)
+- else:
+- value = self._cleanData(value)
+- curActor[tag] = value
+- cur_actors.append(curActor)
++
++ if actorsEt is not None:
++ for curActorItem in actorsEt:
++ curActor = Actor()
++ for curInfo in curActorItem.keys():
++ tag = curInfo
++ value = curActorItem[curInfo]
++ if value is not None:
++ if tag == "image":
++ value = self.config['url_artworkPrefix'] % (value)
++ curActor[tag] = value
++ cur_actors.append(curActor)
+ self._setShowData(sid, '_actors', cur_actors)
+
+- def _getShowData(self, sid):
++ def _getShowData(self, sid, language):
+ """Takes a series ID, gets the epInfo URL and parses the TVDB
+ XML file into the shows dict in layout:
+ shows[series_id][season_number][episode_number]
+ """
++
++ if self.config['language'] is None:
++ log().debug('Config language is none, using show language')
++ if language is None:
++ raise tvdb_error("config['language'] was None, this should not happen")
++ else:
++ log().debug(
++ 'Configured language %s override show language of %s' % (
++ self.config['language'],
++ language
++ )
++ )
++
+ # Parse show information
+- self.log.debug('Getting all series data for %s' % (sid))
+- seriesInfoEt = self._getetsrc(self.config['url_seriesInfo'] % (sid))
+- if self.xml:
+- self.seriesInfoTree = self.tmpTree
+- for curInfo in seriesInfoEt.findall("Series")[0]:
+- tag = curInfo.tag.lower()
+- value = curInfo.text
++ log().debug('Getting all series data for %s' % (sid))
++ seriesInfoEt = self._getetsrc(
++ self.config['url_seriesInfo'] % sid
++ )
++ for curInfo in seriesInfoEt.keys():
++ tag = curInfo
++ value = seriesInfoEt[curInfo]
+
+ if value is not None:
+ if tag in ['banner', 'fanart', 'poster']:
+ value = self.config['url_artworkPrefix'] % (value)
+- else:
+- value = self._cleanData(value)
+
+ self._setShowData(sid, tag, value)
+- self.log.debug("Got info: %s = %s" % (tag, value))
+- #end for series
++ # set language
++ if language == None:
++ language = self.config['language']
++ self._setShowData(sid, u'language', language)
+
+ # Parse banners
+ if self.config['banners_enabled']:
+@@ -777,24 +1072,61 @@ class Tvdb:
+ self._parseActors(sid)
+
+ # Parse episode data
+- self.log.debug('Getting all episodes of %s' % (sid))
+- epsEt = self._getetsrc( self.config['url_epInfo'] % (sid) )
+- if self.xml:
+- self.epInfoTree = self.tmpTree
+- for cur_ep in epsEt.findall("Episode"):
+- seas_no = int(cur_ep.find('SeasonNumber').text)
+- ep_no = int(cur_ep.find('EpisodeNumber').text)
+- for cur_item in cur_ep.getchildren():
+- tag = cur_item.tag.lower()
+- value = cur_item.text
+- if value is not None:
+- if tag == 'filename':
+- value = self.config['url_artworkPrefix'] % (value)
+- else:
+- value = self._cleanData(value)
+- self._setItem(sid, seas_no, ep_no, tag, value)
+- #end for cur_ep
+- #end _geEps
++ log().debug('Getting all episodes of %s' % (sid))
++
++ url = self.config['url_epInfo'] % sid
++ epsEt = self._getetsrc(url, language=self.shows[sid].data[u'language'])
++ for cur_ep in epsEt:
++ self._parseEpisodeInfo(sid, cur_ep)
++
++ def _parseEpisodeInfo(self, sid, cur_ep):
++ if self.config['dvdorder']:
++ log().debug('Using DVD ordering.')
++ use_dvd = cur_ep.get('dvdSeason') is not None and cur_ep.get('dvdEpisodeNumber') is not None
++ else:
++ use_dvd = False
++
++ if use_dvd:
++ elem_seasnum, elem_epno = cur_ep.get('dvdSeason'), cur_ep.get('dvdEpisodeNumber')
++ else:
++ elem_seasnum, elem_epno = cur_ep['airedSeason'], cur_ep['airedEpisodeNumber']
++
++ if elem_seasnum is None or elem_epno is None:
++ log().warning("An episode has incomplete season/episode number (season: %r, episode: %r)" % (
++ elem_seasnum, elem_epno))
++ #log().debug(
++ # " ".join(
++ # "%r is %r" % (child.tag, child.text) for child in cur_ep.getchildren()))
++ # TODO: Should this happen?
++ return # Skip to next episode
++
++ # float() is because https://github.com/dbr/tvnamer/issues/95 - should probably be fixed in TVDB data
++ seas_no = elem_seasnum
++ ep_no = elem_epno
++
++ for cur_item in cur_ep.keys():
++ tag = cur_item
++ value = cur_ep[cur_item]
++ if value is not None:
++ if tag == 'filename' and value:
++ value = self.config['url_artworkPrefix'] % (value)
++ self._setItem(sid, seas_no, ep_no, tag, value)
++
++ def getDetailedEpisodeInfo(self, sid, season, episode):
++ """Get detailed episode info"""
++ try:
++ if isinstance(episode, Episode):
++ url = self.config['url_epDetail'] % episode[u'id']
++ else:
++ season = int(season)
++ episode = int(episode)
++ url = self.config['url_epDetail'] % self.shows[sid][season][episode][u'id']
++ epInfo = self._getetsrc(url, language=self.shows[sid].data[u'language'])
++ self._parseEpisodeInfo(sid, epInfo)
++ except KeyError:
++ import traceback
++ traceback.print_exc()
++ raise tvdb_episodenotfound()
+
+ def _nameToSid(self, name):
+ """Takes show name, returns the correct series ID (if the show has
+@@ -802,48 +1134,48 @@ class Tvdb:
+ the correct SID.
+ """
+ if name in self.corrections:
+- self.log.debug('Correcting %s to %s' % (name, self.corrections[name]) )
++ log().debug('Correcting %s to %s' % (name, self.corrections[name]))
+ sid = self.corrections[name]
+ else:
+- self.log.debug('Getting show %s' % (name))
+- selected_series = self._getSeries( name )
+- sname, sid = selected_series['name'], selected_series['sid']
+- self.log.debug('Got %s, sid %s' % (sname, sid))
++ log().debug('Getting show %s' % name)
++ selected_series = self._getSeries(name)
++ sid = selected_series['id']
++ log().debug('Got %(seriesName)s, id %(id)s' % selected_series)
+
+ self.corrections[name] = sid
+- self._getShowData(sid)
+- #end if name in self.corrections
++ self._getShowData(selected_series['id'], self.config['language'])
++
+ return sid
+- #end _nameToSid
+
+ def __getitem__(self, key):
+ """Handles tvdb_instance['seriesname'] calls.
+ The dict index should be the show id
+ """
+- if isinstance(key, (int, long)):
++ if isinstance(key, int_types):
+ # Item is integer, treat as show id
+ if key not in self.shows:
+- self._getShowData(key)
++ self._getShowData(key, self.config['language'])
+ return self.shows[key]
+
+- key = key.lower() # make key lower case
+ sid = self._nameToSid(key)
+- self.log.debug('Got series id %s' % (sid))
++ log().debug('Got series id %s' % sid)
+ return self.shows[sid]
+- #end __getitem__
+
+ def __repr__(self):
+- return str(self.shows)
+- #end __repr__
+-#end Tvdb
++ return repr(self.shows)
++
+
+ def main():
+ """Simple example of using tvdb_api - it just
+ grabs an episode name interactively.
+ """
+- tvdb_instance = Tvdb(interactive=True, debug=True, cache=False)
+- print tvdb_instance['Lost']['seriesname']
+- print tvdb_instance['Lost'][1][4]['episodename']
++ import logging
++ logging.basicConfig(level=logging.DEBUG)
++
++ tvdb_instance = Tvdb(interactive=False, cache=False)
++ print(tvdb_instance['Lost']['seriesname'])
++ print(tvdb_instance['Lost'][1][4]['episodename'])
++
+
+ if __name__ == '__main__':
+ main()
+diff --git a/mythtv/bindings/python/MythTV/ttvdb/tvdb_create_key.py b/mythtv/bindings/python/MythTV/ttvdb/tvdb_create_key.py
+new file mode 100644
+index 0000000000..7a4deeca37
+--- /dev/null
++++ b/mythtv/bindings/python/MythTV/ttvdb/tvdb_create_key.py
+@@ -0,0 +1,36 @@
++# -*- coding: utf-8 -*-
++
++'''
++Patches tvdb specific create_key in requests_cache which only includes
++Accept-Language in the key
++
++This module must be imported before any modules use requests_cache
++explicitly or implicitly
++'''
++
++import requests_cache
++
++import hashlib
++def create_key(self, request):
++ try:
++ if self._ignored_parameters:
++ url, body = self._remove_ignored_parameters(request)
++ else:
++ url, body = request.url, request.body
++ except AttributeError:
++ url, body = request.url, request.body
++ key = hashlib.sha256()
++ key.update(requests_cache.backends.base._to_bytes(request.method.upper()))
++ key.update(requests_cache.backends.base._to_bytes(url))
++ if request.body:
++ key.update(requests_cache.backends.base._to_bytes(body))
++ else:
++ if self._include_get_headers and request.headers != requests_cache.backends.base._DEFAULT_HEADERS:
++ for name, value in sorted(request.headers.items()):
++ # include only Accept-Language as it is important for context
++ if name in ['Accept-Language']:
++ key.update(requests_cache.backends.base._to_bytes(name))
++ key.update(requests_cache.backends.base._to_bytes(value))
++ return key.hexdigest()
++
++requests_cache.backends.base.BaseCache.create_key = create_key
+diff --git a/mythtv/bindings/python/MythTV/ttvdb/tvdb_exceptions.py b/mythtv/bindings/python/MythTV/ttvdb/tvdb_exceptions.py
+index 722e2cfd87..bf59b28982 100644
+--- a/mythtv/bindings/python/MythTV/ttvdb/tvdb_exceptions.py
++++ b/mythtv/bindings/python/MythTV/ttvdb/tvdb_exceptions.py
+@@ -3,46 +3,26 @@
+ #author:dbr/Ben
+ #project:tvdb_api
+ #repository:http://github.com/dbr/tvdb_api
+-#license:Creative Commons GNU GPL v2
+-# (http://creativecommons.org/licenses/GPL/2.0/)
++#license:unlicense (http://unlicense.org/)
+
+ """Custom exceptions used or raised by tvdb_api
+ """
+
+ __author__ = "dbr/Ben"
+-__version__ = "1.2.1"
++__version__ = "2.0-dev"
+
+-__all__ = ["tvdb_error", "tvdb_userabort", "tvdb_shownotfound",
+-"tvdb_seasonnotfound", "tvdb_episodenotfound", "tvdb_attributenotfound"]
++import logging
+
+-class tvdb_error(Exception):
+- """An error with www.thetvdb.com (Cannot connect, for example)
+- """
+- pass
++__all__ = ["tvdb_error", "tvdb_userabort", "tvdb_notauthorized", "tvdb_shownotfound",
++"tvdb_seasonnotfound", "tvdb_episodenotfound", "tvdb_attributenotfound",
++"tvdb_resourcenotfound", "tvdb_invalidlanguage"]
+
+-class tvdb_userabort(Exception):
+- """User aborted the interactive selection (via
+- the q command, ^c etc)
+- """
+- pass
++logging.getLogger(__name__).warning(
++ "tvdb_exceptions module is deprecated - use classes directly from tvdb_api instead")
+
+-class tvdb_shownotfound(Exception):
+- """Show cannot be found on www.thetvdb.com (non-existant show)
+- """
+- pass
+-
+-class tvdb_seasonnotfound(Exception):
+- """Season cannot be found on www.thetvdb.com
+- """
+- pass
+-
+-class tvdb_episodenotfound(Exception):
+- """Episode cannot be found on www.thetvdb.com
+- """
+- pass
+-
+-class tvdb_attributenotfound(Exception):
+- """Raised if an episode does not have the requested
+- attribute (such as a episode name)
+- """
+- pass
++from tvdb_api import (
++ tvdb_error, tvdb_userabort, tvdb_notauthorized, tvdb_shownotfound,
++ tvdb_seasonnotfound, tvdb_episodenotfound,
++ tvdb_resourcenotfound, tvdb_invalidlanguage,
++ tvdb_attributenotfound
++)
+diff --git a/mythtv/bindings/python/MythTV/ttvdb/tvdb_ui.py b/mythtv/bindings/python/MythTV/ttvdb/tvdb_ui.py
+index a66a16eff1..9f9e417d06 100644
+--- a/mythtv/bindings/python/MythTV/ttvdb/tvdb_ui.py
++++ b/mythtv/bindings/python/MythTV/ttvdb/tvdb_ui.py
+@@ -3,122 +3,19 @@
+ #author:dbr/Ben
+ #project:tvdb_api
+ #repository:http://github.com/dbr/tvdb_api
+-#license:Creative Commons GNU GPL v2
+-# (http://creativecommons.org/licenses/GPL/2.0/)
++#license:unlicense (http://unlicense.org/)
+
+-"""Contains included user interfaces for Tvdb show selection.
+-
+-A UI is a callback. A class, it's __init__ function takes two arguments:
+-
+-- config, which is the Tvdb config dict, setup in tvdb_api.py
+-- log, which is Tvdb's logger instance (which uses the logging module). You can
+-call log.info() log.warning() etc
+-
+-It must have a method "selectSeries", this is passed a list of dicts, each dict
+-contains the the keys "name" (human readable show name), and "sid" (the shows
+-ID as on thetvdb.com). For example:
+-
+-[{'name': u'Lost', 'sid': u'73739'},
+- {'name': u'Lost Universe', 'sid': u'73181'}]
+-
+-The "selectSeries" method must return the appropriate dict, or it can raise
+-tvdb_userabort (if the selection is aborted), tvdb_shownotfound (if the show
+-cannot be found).
+-
+-A simple example callback, which returns a random series:
+-
+->>> import random
+->>> from tvdb_ui import BaseUI
+->>> class RandomUI(BaseUI):
+-... def selectSeries(self, allSeries):
+-... import random
+-... return random.choice(allSeries)
+-
+-Then to use it..
+-
+->>> from tvdb_api import Tvdb
+->>> t = Tvdb(custom_ui = RandomUI)
+->>> random_matching_series = t['Lost']
+->>> type(random_matching_series)
+-<class 'tvdb_api.Show'>
+-"""
+
+ __author__ = "dbr/Ben"
+-__version__ = "1.2.1"
++__version__ = "2.0-dev"
+
+-from tvdb_exceptions import tvdb_userabort
++import sys
++import logging
++import warnings
+
+-class BaseUI:
+- """Default non-interactive UI, which auto-selects first results
+- """
+- def __init__(self, config, log):
+- self.config = config
+- self.log = log
+-
+- def selectSeries(self, allSeries):
+- return allSeries[0]
+-
+-
+-class ConsoleUI(BaseUI):
+- """Interactively allows the user to select a show from a console based UI
+- """
+-
+- def _displaySeries(self, allSeries):
+- """Helper function, lists series with corresponding ID
+- """
+- print "TVDB Search Results:"
+- for i in range(len(allSeries[:6])): # list first 6 search results
+- i_show = i + 1 # Start at more human readable number 1 (not 0)
+- self.log.debug('Showing allSeries[%s] = %s)' % (i_show, allSeries[i]))
+- print "%s -> %s # http://thetvdb.com/?tab=series&id=%s" % (
+- i_show,
+- allSeries[i]['name'].encode("UTF-8","ignore"),
+- allSeries[i]['sid'].encode("UTF-8","ignore")
+- )
+-
+- def selectSeries(self, allSeries):
+- self._displaySeries(allSeries)
+-
+- if len(allSeries) == 1:
+- # Single result, return it!
+- print "Automatically selecting only result"
+- return allSeries[0]
+-
+- if self.config['select_first'] is True:
+- print "Automatically returning first search result"
+- return allSeries[0]
+-
+- while True: # return breaks this loop
+- try:
+- print "Enter choice (first number, ? for help):"
+- ans = raw_input()
+- except KeyboardInterrupt:
+- raise tvdb_userabort("User aborted (^c keyboard interupt)")
+- except EOFError:
+- raise tvdb_userabort("User aborted (EOF received)")
++from tvdb_exceptions import tvdb_userabort
+
+- self.log.debug('Got choice of: %s' % (ans))
+- try:
+- selected_id = int(ans) - 1 # The human entered 1 as first result, not zero
+- except ValueError: # Input was not number
+- if ans == "q":
+- self.log.debug('Got quit command (q)')
+- raise tvdb_userabort("User aborted ('q' quit command)")
+- elif ans == "?":
+- print "## Help"
+- print "# Enter the number that corresponds to the correct show."
+- print "# ? - this help"
+- print "# q - abort tvnamer"
+- else:
+- self.log.debug('Unknown keypress %s' % (ans))
+- else:
+- self.log.debug('Trying to return ID: %d' % (selected_id))
+- try:
+- return allSeries[ selected_id ]
+- except IndexError:
+- self.log.debug('Invalid show number entered!')
+- print "Invalid number (%s) selected!"
+- self._displaySeries(allSeries)
+- #end try
+- #end while not valid_input
++logging.getLogger(__name__).warning(
++ "tvdb_ui module is deprecated - use classes directly from tvdb_api instead")
+
++from tvdb_api import BaseUI, ConsoleUI
+diff --git a/mythtv/bindings/python/MythTV/utility/__init__.py b/mythtv/bindings/python/MythTV/utility/__init__.py
+index 091343dbba..31d2afae28 100644
+--- a/mythtv/bindings/python/MythTV/utility/__init__.py
++++ b/mythtv/bindings/python/MythTV/utility/__init__.py
+@@ -1,10 +1,10 @@
+-from dt import datetime
+-from enum import EnumValue, Enum, BitwiseEnum
+-from singleton import Singleton, InputSingleton, CmpSingleton
+-from dequebuffer import DequeBuffer
+-from mixin import CMPVideo, CMPRecord
+-from altdict import OrdDict, DictInvert, DictInvertCI
++from .dt import datetime
++from .enum import EnumValue, Enum, BitwiseEnum
++from .singleton import Singleton, InputSingleton, CmpSingleton
++from .dequebuffer import DequeBuffer
++from .mixin import CMPVideo, CMPRecord
++from .altdict import OrdDict, DictInvert, DictInvertCI
+
+-from other import _donothing, SchemaUpdate, databaseSearch, deadlinesocket, \
+- MARKUPLIST, levenshtein, ParseEnum, ParseSet, CopyData, \
+- CopyData2, check_ipv6, QuickProperty
++from .other import _donothing, SchemaUpdate, databaseSearch, deadlinesocket, \
++ MARKUPLIST, levenshtein, ParseEnum, ParseSet, CopyData, \
++ CopyData2, check_ipv6, QuickProperty
+diff --git a/mythtv/bindings/python/MythTV/utility/altdict.py b/mythtv/bindings/python/MythTV/utility/altdict.py
+index 103738b28c..346bd47049 100644
+--- a/mythtv/bindings/python/MythTV/utility/altdict.py
++++ b/mythtv/bindings/python/MythTV/utility/altdict.py
+@@ -4,7 +4,7 @@
+ # Description: Provides various custom dict-like classes
+ #------------------------------
+
+-from itertools import imap, izip
++from builtins import map, zip
+
+ class OrdDict( dict ):
+ """
+@@ -64,13 +64,13 @@ class OrdDict( dict ):
+ return list(self.itervalues())
+
+ def itervalues(self):
+- return imap(self.get, self.iterkeys())
++ return map(self.get, self.iterkeys())
+
+ def items(self):
+ return list(self.iteritems())
+
+ def iteritems(self):
+- return izip(self.iterkeys(), self.itervalues())
++ return zip(self.iterkeys(), self.itervalues())
+
+ def copy(self):
+ c = self.__class__(self.iteritems())
+diff --git a/mythtv/bindings/python/MythTV/utility/dequebuffer.py b/mythtv/bindings/python/MythTV/utility/dequebuffer.py
+index f6b62e8ccc..650ac609d6 100644
+--- a/mythtv/bindings/python/MythTV/utility/dequebuffer.py
++++ b/mythtv/bindings/python/MythTV/utility/dequebuffer.py
+@@ -4,11 +4,18 @@
+ # Description: A rolling buffer class that discards handled information.
+ #------------------------------
+
+-from cStringIO import StringIO
++try:
++ from cStringIO import StringIO
++except:
++ from io import BytesIO as StringIO
++
+ from time import time, sleep
+ from threading import Thread, Lock
+ from collections import deque
+-from Queue import Queue
++try:
++ from Queue import Queue
++except:
++ from queue import Queue
+ import weakref
+
+ try:
+@@ -239,7 +246,7 @@ class DequeBuffer( object ):
+
+ def read(self, nbytes=None):
+ """
+- Read up to specified amount from buffer, or whatever is available.
++ Read up to specified amount from buffer, or whatever is available.
+ """
+ # flush existing buffer
+ self._rollback_pool = []
+diff --git a/mythtv/bindings/python/MythTV/utility/dicttoxml.py b/mythtv/bindings/python/MythTV/utility/dicttoxml.py
+new file mode 100644
+index 0000000000..4ad258449f
+--- /dev/null
++++ b/mythtv/bindings/python/MythTV/utility/dicttoxml.py
+@@ -0,0 +1,400 @@
++#!/usr/bin/env python
++# coding: utf-8
++
++"""
++Converts a Python dictionary or other native data type into a valid XML string.
++
++Supports item (`int`, `float`, `long`, `decimal.Decimal`, `bool`, `str`, `unicode`, `datetime`, `none` and other number-like objects) and collection (`list`, `set`, `tuple` and `dict`, as well as iterable and dict-like objects) data types, with arbitrary nesting for the collections. Items with a `datetime` type are converted to ISO format strings. Items with a `None` type become empty XML elements.
++
++This module works with both Python 2 and 3.
++"""
++
++from __future__ import unicode_literals
++
++__version__ = '1.7.4'
++version = __version__
++
++from random import randint
++import collections
++import numbers
++import logging
++from xml.dom.minidom import parseString
++
++
++LOG = logging.getLogger("dicttoxml")
++
++# python 3 doesn't have a unicode type
++try:
++ unicode
++except:
++ unicode = str
++
++# python 3 doesn't have a long type
++try:
++ long
++except:
++ long = int
++
++
++def set_debug(debug=True, filename='dicttoxml.log'):
++ if debug:
++ import datetime
++ print('Debug mode is on. Events are logged at: %s' % (filename))
++ logging.basicConfig(filename=filename, level=logging.INFO)
++ LOG.info('\nLogging session starts: %s' % (
++ str(datetime.datetime.today()))
++ )
++ else:
++ logging.basicConfig(level=logging.WARNING)
++ print('Debug mode is off.')
++
++
++def unicode_me(something):
++ """Converts strings with non-ASCII characters to unicode for LOG.
++ Python 3 doesn't have a `unicode()` function, so `unicode()` is an alias
++ for `str()`, but `str()` doesn't take a second argument, hence this kludge.
++ """
++ try:
++ return unicode(something, 'utf-8')
++ except:
++ return unicode(something)
++
++
++ids = [] # initialize list of unique ids
++
++def make_id(element, start=100000, end=999999):
++ """Returns a random integer"""
++ return '%s_%s' % (element, randint(start, end))
++
++
++def get_unique_id(element):
++ """Returns a unique id for a given element"""
++ this_id = make_id(element)
++ dup = True
++ while dup:
++ if this_id not in ids:
++ dup = False
++ ids.append(this_id)
++ else:
++ this_id = make_id(element)
++ return ids[-1]
++
++
++def get_xml_type(val):
++ """Returns the data type for the xml type attribute"""
++ if type(val).__name__ in ('str', 'unicode'):
++ return 'str'
++ if type(val).__name__ in ('int', 'long'):
++ return 'int'
++ if type(val).__name__ == 'float':
++ return 'float'
++ if type(val).__name__ == 'bool':
++ return 'bool'
++ if isinstance(val, numbers.Number):
++ return 'number'
++ if type(val).__name__ == 'NoneType':
++ return 'null'
++ if isinstance(val, dict):
++ return 'dict'
++ if isinstance(val, collections.Iterable):
++ return 'list'
++ return type(val).__name__
++
++
++def escape_xml(s):
++ if type(s) in (str, unicode):
++ s = unicode_me(s) # avoid UnicodeDecodeError
++ s = s.replace('&', '&')
++ s = s.replace('"', '"')
++ s = s.replace('\'', ''')
++ s = s.replace('<', '<')
++ s = s.replace('>', '>')
++ return s
++
++
++def make_attrstring(attr):
++ """Returns an attribute string in the form key="val" """
++ attrstring = ' '.join(['%s="%s"' % (k, v) for k, v in attr.items()])
++ return '%s%s' % (' ' if attrstring != '' else '', attrstring)
++
++
++def key_is_valid_xml(key):
++ """Checks that a key is a valid XML name"""
++ LOG.info('Inside key_is_valid_xml(). Testing "%s"' % (unicode_me(key)))
++ test_xml = '<?xml version="1.0" encoding="UTF-8" ?><%s>foo</%s>' % (key, key)
++ try:
++ parseString(test_xml)
++ return True
++ except Exception: # minidom does not implement exceptions well
++ return False
++
++
++def make_valid_xml_name(key, attr):
++ """Tests an XML name and fixes it if invalid"""
++ LOG.info('Inside make_valid_xml_name(). Testing key "%s" with attr "%s"' % (
++ unicode_me(key), unicode_me(attr))
++ )
++ key = escape_xml(key)
++ attr = escape_xml(attr)
++
++ # pass through if key is already valid
++ if key_is_valid_xml(key):
++ return key, attr
++
++ # prepend a lowercase n if the key is numeric
++ if str(key).isdigit():
++ return 'n%s' % (key), attr
++
++ # replace spaces with underscores if that fixes the problem
++ if key_is_valid_xml(key.replace(' ', '_')):
++ return key.replace(' ', '_'), attr
++
++ # key is still invalid - move it into a name attribute
++ attr['name'] = key
++ key = 'key'
++ return key, attr
++
++
++def wrap_cdata(s):
++ """Wraps a string into CDATA sections"""
++ s = unicode_me(s).replace(']]>', ']]]]><![CDATA[>')
++ return '<![CDATA[' + s + ']]>'
++
++
++def default_item_func(parent):
++ return 'item'
++
++
++def convert(obj, ids, attr_type, item_func, cdata, parent='root'):
++ """Routes the elements of an object to the right function to convert them
++ based on their data type"""
++
++ LOG.info('Inside convert(). obj type is: "%s", obj="%s"' % (type(obj).__name__, unicode_me(obj)))
++
++ item_name = item_func(parent)
++
++ if isinstance(obj, numbers.Number) or type(obj) in (str, unicode):
++ return convert_kv(item_name, obj, attr_type, cdata)
++
++ if hasattr(obj, 'isoformat'):
++ return convert_kv(item_name, obj.isoformat(), attr_type, cdata)
++
++ if type(obj) == bool:
++ return convert_bool(item_name, obj, attr_type, cdata)
++
++ if obj is None:
++ return convert_none(item_name, '', attr_type, cdata)
++
++ if isinstance(obj, dict):
++ return convert_dict(obj, ids, parent, attr_type, item_func, cdata)
++
++ if isinstance(obj, collections.Iterable):
++ return convert_list(obj, ids, parent, attr_type, item_func, cdata)
++
++ raise TypeError('Unsupported data type: %s (%s)' % (obj, type(obj).__name__))
++
++
++def convert_dict(obj, ids, parent, attr_type, item_func, cdata):
++ """Converts a dict into an XML string."""
++ LOG.info('Inside convert_dict(): obj type is: "%s", obj="%s"' % (
++ type(obj).__name__, unicode_me(obj))
++ )
++ output = []
++ addline = output.append
++
++ item_name = item_func(parent)
++
++ for key, val in obj.items():
++ LOG.info('Looping inside convert_dict(): key="%s", val="%s", type(val)="%s"' % (
++ unicode_me(key), unicode_me(val), type(val).__name__)
++ )
++
++ attr = {} if not ids else {'id': '%s' % (get_unique_id(parent)) }
++
++ key, attr = make_valid_xml_name(key, attr)
++
++ if isinstance(val, numbers.Number) or type(val) in (str, unicode):
++ addline(convert_kv(key, val, attr_type, attr, cdata))
++
++ elif hasattr(val, 'isoformat'): # datetime
++ addline(convert_kv(key, val.isoformat(), attr_type, attr, cdata))
++
++ elif type(val) == bool:
++ addline(convert_bool(key, val, attr_type, attr, cdata))
++
++ elif isinstance(val, dict):
++ if attr_type:
++ attr['type'] = get_xml_type(val)
++ addline('<%s%s>%s</%s>' % (
++ key, make_attrstring(attr),
++ convert_dict(val, ids, key, attr_type, item_func, cdata),
++ key
++ )
++ )
++
++ elif isinstance(val, collections.Iterable):
++ if attr_type:
++ attr['type'] = get_xml_type(val)
++ addline('<%s%s>%s</%s>' % (
++ key,
++ make_attrstring(attr),
++ convert_list(val, ids, key, attr_type, item_func, cdata),
++ key
++ )
++ )
++
++ elif val is None:
++ addline(convert_none(key, val, attr_type, attr, cdata))
++
++ else:
++ raise TypeError('Unsupported data type: %s (%s)' % (
++ val, type(val).__name__)
++ )
++
++ return ''.join(output)
++
++
++def convert_list(items, ids, parent, attr_type, item_func, cdata):
++ """Converts a list into an XML string."""
++ LOG.info('Inside convert_list()')
++ output = []
++ addline = output.append
++
++ item_name = item_func(parent)
++
++ if ids:
++ this_id = get_unique_id(parent)
++
++ for i, item in enumerate(items):
++ LOG.info('Looping inside convert_list(): item="%s", item_name="%s", type="%s"' % (
++ unicode_me(item), item_name, type(item).__name__)
++ )
++ attr = {} if not ids else { 'id': '%s_%s' % (this_id, i+1) }
++ if isinstance(item, numbers.Number) or type(item) in (str, unicode):
++ addline(convert_kv(item_name, item, attr_type, attr, cdata))
++
++ elif hasattr(item, 'isoformat'): # datetime
++ addline(convert_kv(item_name, item.isoformat(), attr_type, attr, cdata))
++
++ elif type(item) == bool:
++ addline(convert_bool(item_name, item, attr_type, attr, cdata))
++
++ elif isinstance(item, dict):
++ if not attr_type:
++ addline('<%s>%s</%s>' % (
++ item_name,
++ convert_dict(item, ids, parent, attr_type, item_func, cdata),
++ item_name,
++ )
++ )
++ else:
++ addline('<%s type="dict">%s</%s>' % (
++ item_name,
++ convert_dict(item, ids, parent, attr_type, item_func, cdata),
++ item_name,
++ )
++ )
++
++ elif isinstance(item, collections.Iterable):
++ if not attr_type:
++ addline('<%s %s>%s</%s>' % (
++ item_name, make_attrstring(attr),
++ convert_list(item, ids, item_name, attr_type, item_func, cdata),
++ item_name,
++ )
++ )
++ else:
++ addline('<%s type="list"%s>%s</%s>' % (
++ item_name, make_attrstring(attr),
++ convert_list(item, ids, item_name, attr_type, item_func, cdata),
++ item_name,
++ )
++ )
++
++ elif item is None:
++ addline(convert_none(item_name, None, attr_type, attr, cdata))
++
++ else:
++ raise TypeError('Unsupported data type: %s (%s)' % (
++ item, type(item).__name__)
++ )
++ return ''.join(output)
++
++
++def convert_kv(key, val, attr_type, attr={}, cdata=False):
++ """Converts a number or string into an XML element"""
++ LOG.info('Inside convert_kv(): key="%s", val="%s", type(val) is: "%s"' % (
++ unicode_me(key), unicode_me(val), type(val).__name__)
++ )
++
++ key, attr = make_valid_xml_name(key, attr)
++
++ if attr_type:
++ attr['type'] = get_xml_type(val)
++ attrstring = make_attrstring(attr)
++ return '<%s%s>%s</%s>' % (
++ key, attrstring,
++ wrap_cdata(val) if cdata == True else escape_xml(val),
++ key
++ )
++
++
++def convert_bool(key, val, attr_type, attr={}, cdata=False):
++ """Converts a boolean into an XML element"""
++ LOG.info('Inside convert_bool(): key="%s", val="%s", type(val) is: "%s"' % (
++ unicode_me(key), unicode_me(val), type(val).__name__)
++ )
++
++ key, attr = make_valid_xml_name(key, attr)
++
++ if attr_type:
++ attr['type'] = get_xml_type(val)
++ attrstring = make_attrstring(attr)
++ return '<%s%s>%s</%s>' % (key, attrstring, unicode(val).lower(), key)
++
++
++def convert_none(key, val, attr_type, attr={}, cdata=False):
++ """Converts a null value into an XML element"""
++ LOG.info('Inside convert_none(): key="%s"' % (unicode_me(key)))
++
++ key, attr = make_valid_xml_name(key, attr)
++
++ if attr_type:
++ attr['type'] = get_xml_type(val)
++ attrstring = make_attrstring(attr)
++ return '<%s%s></%s>' % (key, attrstring, key)
++
++
++def dicttoxml(obj, root=True, custom_root='root', ids=False, attr_type=True,
++ item_func=default_item_func, cdata=False):
++ """Converts a python object into XML.
++ Arguments:
++ - root specifies whether the output is wrapped in an XML root element
++ Default is True
++ - custom_root allows you to specify a custom root element.
++ Default is 'root'
++ - ids specifies whether elements get unique ids.
++ Default is False
++ - attr_type specifies whether elements get a data type attribute.
++ Default is True
++ - item_func specifies what function should generate the element name for
++ items in a list.
++ Default is 'item'
++ - cdata specifies whether string values should be wrapped in CDATA sections.
++ Default is False
++ """
++ LOG.info('Inside dicttoxml(): type(obj) is: "%s", obj="%s"' % (type(obj).__name__, unicode_me(obj)))
++ output = []
++ addline = output.append
++ if root == True:
++ addline('<?xml version="1.0" encoding="UTF-8" ?>')
++ addline('<%s>%s</%s>' % (
++ custom_root,
++ convert(obj, ids, attr_type, item_func, cdata, parent=custom_root),
++ custom_root,
++ )
++ )
++ else:
++ addline(convert(obj, ids, attr_type, item_func, cdata, parent=''))
++ return ''.join(output).encode('utf-8')
++
+diff --git a/mythtv/bindings/python/MythTV/utility/dt.py b/mythtv/bindings/python/MythTV/utility/dt.py
+index f00386d9e4..f688ea24c6 100644
+--- a/mythtv/bindings/python/MythTV/utility/dt.py
++++ b/mythtv/bindings/python/MythTV/utility/dt.py
+@@ -14,7 +14,7 @@ from collections import namedtuple
+ import os
+ import re
+ import time
+-import singleton
++from . import singleton
+ time.tzset()
+
+ class basetzinfo( _pytzinfo ):
+@@ -69,7 +69,7 @@ class basetzinfo( _pytzinfo ):
+ break
+ elif index < 0:
+ # out of bounds past, undefined time frame
+- raise MythTZError(MythTZError.TZ_CONVERSION_ERROR,
++ raise MythTZError(MythTZError.TZ_CONVERSION_ERROR,
+ self.tzname(), dt)
+
+ self.__last = index
+@@ -436,7 +436,7 @@ class datetime( _pydatetime ):
+
+ def __new__(cls, year, month, day, hour=None, minute=None, second=None,
+ microsecond=None, tzinfo=None):
+-
++
+ if tzinfo is None:
+ kwargs = {'tzinfo':cls.localTZ()}
+ else:
+diff --git a/mythtv/bindings/python/MythTV/utility/enum.py b/mythtv/bindings/python/MythTV/utility/enum.py
+index a4bfac65bd..3875054c7b 100644
+--- a/mythtv/bindings/python/MythTV/utility/enum.py
++++ b/mythtv/bindings/python/MythTV/utility/enum.py
+@@ -6,11 +6,7 @@
+ # operation.
+ #------------------------------
+
+-from abc import ABCMeta
+-class number( object ):
+- __metaclass__ = ABCMeta
+-number.register(int)
+-number.register(long)
++from builtins import int
+
+ class EnumValue( object ):
+ _next = 0
+@@ -37,7 +33,7 @@ class EnumValue( object ):
+ class EnumType( type ):
+ def __new__(mcs, name, bases, attrs):
+ for k,v in attrs.items():
+- if isinstance(v, number):
++ if isinstance(v, int):
+ EnumValue(k, v)
+ del attrs[k]
+ values = {}
+diff --git a/mythtv/bindings/python/MythTV/utility/other.py b/mythtv/bindings/python/MythTV/utility/other.py
+index a91ff47b5d..543bef38a0 100644
+--- a/mythtv/bindings/python/MythTV/utility/other.py
++++ b/mythtv/bindings/python/MythTV/utility/other.py
+@@ -3,15 +3,19 @@
+
+ from MythTV.logging import MythLog
+ from MythTV.exceptions import MythDBError, MythError
+-from dt import datetime
++from .dt import datetime
+
+-from cStringIO import StringIO
++try:
++ from cStringIO import StringIO
++except ImportError:
++ from io import BytesIO as StringIO
+ from select import select
+ from time import time
+-from itertools import imap
++from builtins import map
+ import weakref
+ import socket
+ import re
++from builtins import range
+
+ def _donothing(*args, **kwargs):
+ pass
+@@ -40,7 +44,7 @@ class SchemaUpdate( object ):
+ schema = origschema
+ try:
+ while True:
+-
++
+ newschema = getattr(self, 'up%d' % schema)()
+ self.log(MythLog.GENERAL, MythLog.INFO,
+ 'successfully updated from %d to %d' %\
+@@ -48,7 +52,7 @@ class SchemaUpdate( object ):
+ schema = newschema
+ self.db.settings.NULL[self._schema_name] = schema
+
+- except AttributeError, e:
++ except AttributeError as e:
+ self.log(MythLog.GENERAL, MythLog.CRIT,
+ 'failed at %d' % schema, 'no handler method')
+ raise MythDBError('Schema update failed, '
+@@ -60,7 +64,7 @@ class SchemaUpdate( object ):
+ '%s update complete' % self._schema_name)
+ pass
+
+- except Exception, e:
++ except Exception as e:
+ raise MythDBError(MythError.DB_SCHEMAUPDATE, e.args)
+
+ def create(self):
+@@ -77,7 +81,7 @@ class databaseSearch( object ):
+ of the following format
+ (<table name>, -- Primary table to pull data from.
+ <data class>, -- Data handling class to use to process
+- data. Ideally a subclass of DBData,
++ data. Ideally a subclass of DBData,
+ this class must provide a 'fromRaw'
+ classmethod.
+ <required keywords>, -- Tuple of keywords that must be
+@@ -231,7 +235,7 @@ class databaseSearch( object ):
+ res[2]),
+ len(lval)))
+ fields += lval
+-
++
+ for key in self.require:
+ if key not in kwargs:
+ res = self.func(self.inst, key=key)
+@@ -330,7 +334,7 @@ class deadlinesocket( socket.socket ):
+ p = buff.tell()
+ try:
+ buff.write(self.recv(bufsize-buff.tell(), flags))
+- except socket.error, e:
++ except socket.error as e:
+ raise MythError(MythError.SOCKET, e.args)
+ if buff.tell() == p:
+ # no data read from a 'ready' socket, connection terminated
+@@ -362,7 +366,7 @@ class deadlinesocket( socket.socket ):
+ p = buff.tell()
+ try:
+ buff.write(self.recv(100, flags))
+- except socket.error, e:
++ except socket.error as e:
+ raise MythError(MythError.SOCKET, e.args)
+ if buff.tell() == p:
+ # no data read from a 'ready' socket, connection terminated
+@@ -390,7 +394,7 @@ class deadlinesocket( socket.socket ):
+ 'write --> %d' % len(data), data)
+ data = '%-8d%s' % (len(data), data)
+ self.send(data, flags)
+- except socket.error, e:
++ except socket.error as e:
+ raise MythError(MythError.SOCKET, e.args)
+
+ class MARKUPLIST( object ):
+@@ -428,8 +432,8 @@ def levenshtein(s1, s2):
+ return levenshtein(s2, s1)
+ if not s1:
+ return len(s2)
+-
+- previous_row = xrange(len(s2) + 1)
++
++ previous_row = range(len(s2) + 1)
+ for i, c1 in enumerate(s1):
+ current_row = [i + 1]
+ for j, c2 in enumerate(s2):
+@@ -438,12 +442,12 @@ def levenshtein(s1, s2):
+ substitutions = previous_row[j] + (c1 != c2)
+ current_row.append(min(insertions, deletions, substitutions))
+ previous_row = current_row
+-
++
+ return previous_row[-1]
+
+ class ParseEnum( object ):
+ _static = None
+- def __str__(self):
++ def __str__(self):
+ return str([k for k,v in self.iteritems() if v==True])
+ def __repr__(self): return str(self)
+ def __init__(self, parent, field_name, enum, editable=True):
+@@ -470,7 +474,7 @@ class ParseEnum( object ):
+
+ def __setitem__(self, key, value):
+ if self._static:
+- raise KeyError("'%s' cannot be edited." % name)
++ raise KeyError("'%s' cannot be edited." % key)
+ val = getattr(self._enum, key)
+ if value:
+ self._parent[self._field] |= val
+@@ -493,7 +497,7 @@ class ParseEnum( object ):
+ return iter(self.keys())
+
+ def itervalues(self):
+- return imap(self.__getitem__, self.keys())
++ return map(self.__getitem__, self.keys())
+
+ def iteritems(self):
+ for key in self.keys():
+@@ -529,7 +533,7 @@ class ParseSet( ParseEnum ):
+
+ def __setitem__(self, key, value):
+ if self._static:
+- raise KeyError("'%s' cannot be edited." % name)
++ raise KeyError("'%s' cannot be edited." % key)
+ if self[key] == value:
+ return
+ tmp = self._parent[self._field].split(',')
diff --git a/mythtv/configure b/mythtv/configure
index af0d6a6caf..5019732a40 100755
--- a/mythtv/configure
@@ -297,6 +4300,28 @@ index c5a3c2ea87..111052d6b9 100644
return QString("%1").arg(u.toString());
return QString("%1:%2:%3")
.arg(u.host()).arg(u.userInfo()).arg(u.port()).toLower();
+diff --git a/mythtv/libs/libmythtv/libmythtv.pro b/mythtv/libs/libmythtv/libmythtv.pro
+index 5c29520c3d..83dda8e9e6 100644
+--- a/mythtv/libs/libmythtv/libmythtv.pro
++++ b/mythtv/libs/libmythtv/libmythtv.pro
+@@ -908,6 +908,17 @@ using_backend: LIBS += -lmp3lame
+ using_backend: LIBS += -L../../external/minilzo -lmythminilzo-$$LIBVERSION
+ LIBS += $$EXTRA_LIBS $$QMAKE_LIBS_DYNLOAD
+
++using_openmax {
++ contains( HAVE_OPENMAX_BROADCOM, yes ) {
++ using_opengl {
++ # For raspberry Pi Raspbian
++ exists(/opt/vc/lib/libbrcmEGL.so) {
++ LIBS += -L/opt/vc/lib/ -lbrcmGLESv2 -lbrcmEGL
++ }
++ }
++ }
++}
++
+ !win32-msvc* {
+ POST_TARGETDEPS += ../libmyth/libmyth-$${MYTH_SHLIB_EXT}
+ POST_TARGETDEPS += ../../external/FFmpeg/libswresample/$$avLibName(swresample)
diff --git a/mythtv/libs/libmythtv/mythavutil.cpp b/mythtv/libs/libmythtv/mythavutil.cpp
index afed323917..c69c4d279e 100644
--- a/mythtv/libs/libmythtv/mythavutil.cpp
@@ -562,10 +4587,20 @@ index d6367a6ebb..12fa8391b6 100644
mwnd->setMinimumSize(QSize(16, 16));
mwnd->setMaximumSize(QSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX));
diff --git a/mythtv/libs/libmythtv/videoout_omx.cpp b/mythtv/libs/libmythtv/videoout_omx.cpp
-index 3c5c4486af..859eb8ea64 100644
+index 3c5c4486af..a9dbc4a62f 100644
--- a/mythtv/libs/libmythtv/videoout_omx.cpp
+++ b/mythtv/libs/libmythtv/videoout_omx.cpp
-@@ -286,7 +286,8 @@ VideoOutputOMX::VideoOutputOMX() :
+@@ -29,9 +29,6 @@
+ #ifdef OSD_EGL
+ #include <EGL/egl.h>
+ #include <QtGlobal>
+-#if QT_VERSION >= QT_VERSION_CHECK(5, 4, 0)
+-#include <QtPlatformHeaders/QEGLNativeContext>
+-#endif
+ #endif
+
+ // MythTV
+@@ -286,7 +283,8 @@ VideoOutputOMX::VideoOutputOMX() :
m_render(gCoreContext->GetSetting("OMXVideoRender", VIDEO_RENDER), *this),
m_imagefx(gCoreContext->GetSetting("OMXVideoFilter", IMAGE_FX), *this),
m_context(0),
@@ -575,7 +4610,7 @@ index 3c5c4486af..859eb8ea64 100644
{
#ifdef OSD_EGL
m_osdpainter = 0;
-@@ -755,6 +756,10 @@ void VideoOutputOMX::UpdatePauseFrame(int64_t &disp_timecode)
+@@ -755,6 +753,10 @@ void VideoOutputOMX::UpdatePauseFrame(int64_t &disp_timecode)
CopyFrame(&av_pause_frame, used_frame);
}
@@ -586,7 +4621,7 @@ index 3c5c4486af..859eb8ea64 100644
disp_timecode = av_pause_frame.disp_timecode;
}
-@@ -773,9 +778,11 @@ void VideoOutputOMX::ProcessFrame(VideoFrame *frame, OSD *osd,
+@@ -773,9 +775,11 @@ void VideoOutputOMX::ProcessFrame(VideoFrame *frame, OSD *osd,
return;
}
@@ -598,7 +4633,7 @@ index 3c5c4486af..859eb8ea64 100644
vbuffers.Enqueue(kVideoBuffer_pause, vbuffers.Dequeue(kVideoBuffer_pause));
frame = vbuffers.GetScratchFrame();
CopyFrame(frame, &av_pause_frame);
-@@ -877,7 +884,22 @@ void VideoOutputOMX::Show(FrameScanType scan)
+@@ -877,7 +881,22 @@ void VideoOutputOMX::Show(FrameScanType scan)
hdr->nFilledLen = frame->offsets[2] + (frame->offsets[1] >> 2);
hdr->nFlags = OMX_BUFFERFLAG_ENDOFFRAME;
@@ -717,6 +4752,37 @@ index 97618adc80..0b530ae3a7 100644
#include <sys/stat.h>
// C++ headers
+diff --git a/mythtv/libs/libmythui/libmythui.pro b/mythtv/libs/libmythui/libmythui.pro
+index 321e9a34a2..b35060b5bf 100644
+--- a/mythtv/libs/libmythui/libmythui.pro
++++ b/mythtv/libs/libmythui/libmythui.pro
+@@ -183,6 +183,7 @@ using_opengl {
+ using_opengles {
+ DEFINES += USING_OPENGLES
+ HEADERS += mythrender_opengl2es.h
++ LIBS += -L/opt/vc/include -lbrcmGLESv2 -lbrcmEGL
+ }
+ !using_opengles {
+ SOURCES += mythrender_opengl1.cpp
+@@ -194,6 +195,18 @@ using_opengl {
+ mingw|win32-msvc*:LIBS += -lopengl32
+ }
+
++using_openmax {
++ contains( HAVE_OPENMAX_BROADCOM, yes ) {
++ using_opengl {
++ # For raspberry Pi Raspbian
++ exists(/opt/vc/lib/libbrcmEGL.so) {
++ LIBS += -L/opt/vc/lib/ -lbrcmGLESv2 -lbrcmEGL
++ }
++ }
++ }
++}
++
++
+ DEFINES += USING_QTWEBKIT
+ DEFINES += MUI_API
+
diff --git a/mythtv/libs/libmythui/mythmainwindow.cpp b/mythtv/libs/libmythui/mythmainwindow.cpp
index 8a903c625f..55f90ab01d 100644
--- a/mythtv/libs/libmythui/mythmainwindow.cpp
@@ -733,11 +4799,52 @@ index 8a903c625f..55f90ab01d 100644
}
#endif
+diff --git a/mythtv/libs/libmythui/mythrender_opengl.cpp b/mythtv/libs/libmythui/mythrender_opengl.cpp
+index b376008252..b1409d9085 100644
+--- a/mythtv/libs/libmythui/mythrender_opengl.cpp
++++ b/mythtv/libs/libmythui/mythrender_opengl.cpp
+@@ -510,6 +510,12 @@ uint MythRenderOpenGL::CreateTexture(QSize act_size, bool use_pbo,
+ uint data_fmt, uint internal_fmt,
+ uint filter, uint wrap)
+ {
++#ifdef USING_OPENGLES
++ //OPENGLES requires same formats for internal and external.
++ internal_fmt = data_fmt;
++ glCheck();
++#endif
++
+ if (!type)
+ type = m_default_texture_type;
+
diff --git a/mythtv/libs/libmythui/mythrender_opengl.h b/mythtv/libs/libmythui/mythrender_opengl.h
-index adaf302d5a..a18c5152d6 100644
+index adaf302d5a..d6eb5abe2b 100644
--- a/mythtv/libs/libmythui/mythrender_opengl.h
+++ b/mythtv/libs/libmythui/mythrender_opengl.h
-@@ -19,9 +19,6 @@
+@@ -4,12 +4,18 @@
+ #include <stdint.h>
+
+ #include <QtGlobal>
+-#if defined USING_OPENGLES && QT_VERSION >= QT_VERSION_CHECK(5, 4, 0)
+-#define USE_OPENGL_QT5
+-#include <QOpenGLContext>
+-#else
++// The below is commented because it causes raspberry Pi with OpenMAX
++// to fail. If commenting it out causes problems with other
++// platforms we can add it back with additional conditions that
++// will exclude it for Raspberry Pi. With this commented, all
++// code that depends on USE_OPENGL_QT5 will be bypassed and maybe can
++// be removed later.
++//#if defined USING_OPENGLES && QT_VERSION >= QT_VERSION_CHECK(5, 4, 0)
++//#define USE_OPENGL_QT5
++//#include <QOpenGLContext>
++//#else
+ #include <QGLContext>
+-#endif
++//#endif
+ #include <QHash>
+ #include <QMutex>
+ #include <QMatrix4x4>
+@@ -19,9 +25,6 @@
#ifdef USING_X11
#define GLX_GLXEXT_PROTOTYPES
#define XMD_H 1
@@ -761,6 +4868,37 @@ index ce651dfa94..53e25fedc4 100644
class MUI_PUBLIC MythRenderOpenGL1 : public MythRenderOpenGL
{
+diff --git a/mythtv/libs/libmythui/mythrender_opengl2.cpp b/mythtv/libs/libmythui/mythrender_opengl2.cpp
+index 74718886b0..dcac95381d 100644
+--- a/mythtv/libs/libmythui/mythrender_opengl2.cpp
++++ b/mythtv/libs/libmythui/mythrender_opengl2.cpp
+@@ -6,6 +6,18 @@
+
+ #define LOC QString("OpenGL2: ")
+
++static inline int __glCheck__(const QString &loc, const char* fileName, int n)
++{
++ int error = glGetError();
++ if (error)
++ {
++ LOG(VB_GENERAL, LOG_ERR, QString("%1: %2 @ %3, %4")
++ .arg(loc).arg(error).arg(fileName).arg(n));
++ }
++ return error;
++}
++#define glCheck() __glCheck__(LOC, __FILE__, __LINE__)
++
+ #define VERTEX_INDEX 0
+ #define COLOR_INDEX 1
+ #define TEXTURE_INDEX 2
+@@ -456,6 +468,7 @@ void MythRenderOpenGL2::DrawBitmapPriv(uint tex, const QRect *src,
+ (const void *) kTextureOffset);
+
+ glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
++ glCheck();
+
+ m_glDisableVertexAttribArray(TEXTURE_INDEX);
+ m_glDisableVertexAttribArray(VERTEX_INDEX);
diff --git a/mythtv/libs/libmythupnp/mythxmlclient.cpp b/mythtv/libs/libmythupnp/mythxmlclient.cpp
index 45d7497fd9..c6fe011338 100644
--- a/mythtv/libs/libmythupnp/mythxmlclient.cpp
@@ -820,6 +4958,22 @@ index 4b0457f4e8..acc2bd9ff7 100644
} UPnPResultCode;
+diff --git a/mythtv/programs/mythavtest/main.cpp b/mythtv/programs/mythavtest/main.cpp
+index 96381e4f8c..511cc31197 100644
+--- a/mythtv/programs/mythavtest/main.cpp
++++ b/mythtv/programs/mythavtest/main.cpp
+@@ -146,6 +146,11 @@ class VideoPerformanceTest
+
+ int main(int argc, char *argv[])
+ {
++
++#if HAVE_OPENMAX_BROADCOM
++ setenv("QT_XCB_GL_INTEGRATION","none",0);
++#endif
++
+ MythAVTestCommandLineParser cmdline;
+ if (!cmdline.Parse(argc, argv))
+ {
diff --git a/mythtv/programs/mythbackend/scheduler.cpp b/mythtv/programs/mythbackend/scheduler.cpp
index a5eb1a1cdb..cad6bdd000 100644
--- a/mythtv/programs/mythbackend/scheduler.cpp
@@ -1358,6 +5512,62 @@ index 62a3a13820..21bf5b8324 100644
return true;
}
-
+diff --git a/mythtv/programs/mythfrontend/main.cpp b/mythtv/programs/mythfrontend/main.cpp
+index 4e6573bdcb..0dd5731f4a 100644
+--- a/mythtv/programs/mythfrontend/main.cpp
++++ b/mythtv/programs/mythfrontend/main.cpp
+@@ -1675,6 +1675,10 @@ int main(int argc, char **argv)
+ bool bPromptForBackend = false;
+ bool bBypassAutoDiscovery = false;
+
++#if HAVE_OPENMAX_BROADCOM
++ setenv("QT_XCB_GL_INTEGRATION","none",0);
++#endif
++
+ #ifdef Q_OS_ANDROID
+ // extra for 0 termination
+ char *newargv[argc+4+1];
+diff --git a/mythtv/programs/mythfrontend/mythfrontend.pro b/mythtv/programs/mythfrontend/mythfrontend.pro
+index ed69b93136..cece2d6a23 100644
+--- a/mythtv/programs/mythfrontend/mythfrontend.pro
++++ b/mythtv/programs/mythfrontend/mythfrontend.pro
+@@ -158,17 +158,30 @@ android {
+ using_openmax {
+ contains( HAVE_OPENMAX_BROADCOM, yes ) {
+ using_opengl {
+- # For raspberry Pi Raspbian
+- exists(/opt/vc/lib/libEGL.so) {
++ # For raspberry Pi Raspbian Stretch
++ exists(/opt/vc/lib/libbrcmEGL.so) {
+ DEFINES += USING_OPENGLES
+ # For raspberry pi raspbian
+ QMAKE_RPATHDIR += $${RUNPREFIX}/share/mythtv/lib
+ createlinks.path = $${PREFIX}/share/mythtv/lib
+- createlinks.extra = ln -fs /opt/vc/lib/libEGL.so $(INSTALL_ROOT)/$${PREFIX}/share/mythtv/lib/libEGL.so.1.0.0 ;
+- createlinks.extra += ln -fs /opt/vc/lib/libEGL.so $(INSTALL_ROOT)/$${PREFIX}/share/mythtv/lib/libEGL.so.1 ;
+- createlinks.extra += ln -fs /opt/vc/lib/libGLESv2.so $(INSTALL_ROOT)/$${PREFIX}/share/mythtv/lib/libGLESv2.so.2.0.0 ;
+- createlinks.extra += ln -fs /opt/vc/lib/libGLESv2.so $(INSTALL_ROOT)/$${PREFIX}/share/mythtv/lib/libGLESv2.so.2 ;
++ createlinks.extra = ln -fs /opt/vc/lib/libbrcmEGL.so $(INSTALL_ROOT)/$${PREFIX}/share/mythtv/lib/libEGL.so.1.0.0 ;
++ createlinks.extra += ln -fs /opt/vc/lib/libbrcmEGL.so $(INSTALL_ROOT)/$${PREFIX}/share/mythtv/lib/libEGL.so.1 ;
++ createlinks.extra += ln -fs /opt/vc/lib/libbrcmGLESv2.so $(INSTALL_ROOT)/$${PREFIX}/share/mythtv/lib/libGLESv2.so.2.0.0 ;
++ createlinks.extra += ln -fs /opt/vc/lib/libbrcmGLESv2.so $(INSTALL_ROOT)/$${PREFIX}/share/mythtv/lib/libGLESv2.so.2 ;
+ INSTALLS += createlinks
++ } else {
++ # For raspberry Pi Raspbian pre-stretch
++ exists(/opt/vc/lib/libEGL.so) {
++ DEFINES += USING_OPENGLES
++ # For raspberry pi raspbian
++ QMAKE_RPATHDIR += $${RUNPREFIX}/share/mythtv/lib
++ createlinks.path = $${PREFIX}/share/mythtv/lib
++ createlinks.extra = ln -fs /opt/vc/lib/libEGL.so $(INSTALL_ROOT)/$${PREFIX}/share/mythtv/lib/libEGL.so.1.0.0 ;
++ createlinks.extra += ln -fs /opt/vc/lib/libEGL.so $(INSTALL_ROOT)/$${PREFIX}/share/mythtv/lib/libEGL.so.1 ;
++ createlinks.extra += ln -fs /opt/vc/lib/libGLESv2.so $(INSTALL_ROOT)/$${PREFIX}/share/mythtv/lib/libGLESv2.so.2.0.0 ;
++ createlinks.extra += ln -fs /opt/vc/lib/libGLESv2.so $(INSTALL_ROOT)/$${PREFIX}/share/mythtv/lib/libGLESv2.so.2 ;
++ INSTALLS += createlinks
++ }
+ }
+ } else {
+ # For raspberry pi ubuntu
diff --git a/mythtv/programs/mythfrontend/proglist.cpp b/mythtv/programs/mythfrontend/proglist.cpp
index 37e4506130..ebd94c18a1 100644
--- a/mythtv/programs/mythfrontend/proglist.cpp
@@ -1376,3 +5586,1976 @@ index 37e4506130..ebd94c18a1 100644
m_viewList.push_back(QString(">= %1").arg(stars));
m_viewTextList.push_back(tr("%n star(s) and above", "", i));
}
+diff --git a/mythtv/programs/mythfrontend/schedulecommon.cpp b/mythtv/programs/mythfrontend/schedulecommon.cpp
+index d09a7d1f67..00b77bfdea 100644
+--- a/mythtv/programs/mythfrontend/schedulecommon.cpp
++++ b/mythtv/programs/mythfrontend/schedulecommon.cpp
+@@ -247,9 +247,16 @@ void ScheduleCommon::ShowPrevious(void) const
+ if (!pginfo)
+ return;
+
++ ShowPrevious(pginfo->GetRecordingRuleID(), pginfo->GetTitle());
++}
++
++/**
++* \brief Show the previous recordings for this recording rule
++*/
++void ScheduleCommon::ShowPrevious(uint ruleid, const QString &title) const
++{
+ MythScreenStack *mainStack = GetMythMainWindow()->GetMainStack();
+- ProgLister *pl = new ProgLister(mainStack, pginfo->GetRecordingRuleID(),
+- pginfo->GetTitle());
++ ProgLister *pl = new ProgLister(mainStack, ruleid, title);
+ if (pl->Create())
+ mainStack->AddScreen(pl);
+ else
+diff --git a/mythtv/programs/mythfrontend/schedulecommon.h b/mythtv/programs/mythfrontend/schedulecommon.h
+index 590e56ef81..a7aca2d274 100644
+--- a/mythtv/programs/mythfrontend/schedulecommon.h
++++ b/mythtv/programs/mythfrontend/schedulecommon.h
+@@ -36,6 +36,7 @@ class ScheduleCommon : public MythScreenType
+ virtual void EditRecording(void);
+ virtual void QuickRecord(void);
+ virtual void ShowPrevious(void) const;
++ virtual void ShowPrevious(uint ruleid, const QString &title) const;
+ virtual void ShowUpcoming(void) const;
+ virtual void ShowUpcomingScheduled(void) const;
+ virtual void ShowChannelSearch(void) const;
+diff --git a/mythtv/programs/mythfrontend/scheduleeditor.cpp b/mythtv/programs/mythfrontend/scheduleeditor.cpp
+index 339c6b5d9d..3d5379bfb5 100644
+--- a/mythtv/programs/mythfrontend/scheduleeditor.cpp
++++ b/mythtv/programs/mythfrontend/scheduleeditor.cpp
+@@ -605,7 +605,8 @@ void ScheduleEditor::customEvent(QEvent *event)
+ else if (resulttext == tr("Upcoming Recordings"))
+ showUpcomingByRule();
+ else if (resulttext == tr("Previously Recorded"))
+- ShowPrevious();
++ ShowPrevious(m_recordingRule->m_recordID,
++ m_recordingRule->m_title);
+ }
+ else if (resultid == "newrecgroup")
+ {
+diff --git a/mythtv/programs/mythscreenwizard/main.cpp b/mythtv/programs/mythscreenwizard/main.cpp
+index d94ad76c93..15a7d1f9e8 100644
+--- a/mythtv/programs/mythscreenwizard/main.cpp
++++ b/mythtv/programs/mythscreenwizard/main.cpp
+@@ -111,6 +111,11 @@ static void startAppearWiz(int _x, int _y, int _w, int _h)
+
+ int main(int argc, char **argv)
+ {
++
++#if HAVE_OPENMAX_BROADCOM
++ setenv("QT_XCB_GL_INTEGRATION","none",0);
++#endif
++
+ MythScreenWizardCommandLineParser cmdline;
+ if (!cmdline.Parse(argc, argv))
+ {
+diff --git a/mythtv/programs/mythtv-setup/main.cpp b/mythtv/programs/mythtv-setup/main.cpp
+index 59a1591795..04f0ce5bfe 100644
+--- a/mythtv/programs/mythtv-setup/main.cpp
++++ b/mythtv/programs/mythtv-setup/main.cpp
+@@ -238,6 +238,10 @@ int main(int argc, char *argv[])
+ QString scanTableName = "atsc-vsb8-us";
+ QString scanInputName = "";
+
++#if HAVE_OPENMAX_BROADCOM
++ setenv("QT_XCB_GL_INTEGRATION","none",0);
++#endif
++
+ MythTVSetupCommandLineParser cmdline;
+ if (!cmdline.Parse(argc, argv))
+ {
+diff --git a/mythtv/programs/mythwelcome/main.cpp b/mythtv/programs/mythwelcome/main.cpp
+index 0e24f42093..6f276c56c2 100644
+--- a/mythtv/programs/mythwelcome/main.cpp
++++ b/mythtv/programs/mythwelcome/main.cpp
+@@ -46,6 +46,10 @@ int main(int argc, char **argv)
+ {
+ bool bShowSettings = false;
+
++#if HAVE_OPENMAX_BROADCOM
++ setenv("QT_XCB_GL_INTEGRATION","none",0);
++#endif
++
+ MythWelcomeCommandLineParser cmdline;
+ if (!cmdline.Parse(argc, argv))
+ {
+diff --git a/mythtv/programs/scripts/metadata/Television/ttvdb.py b/mythtv/programs/scripts/metadata/Television/ttvdb.py
+index 492feb7f09..20ad08d433 100755
+--- a/mythtv/programs/scripts/metadata/Television/ttvdb.py
++++ b/mythtv/programs/scripts/metadata/Television/ttvdb.py
+@@ -34,10 +34,552 @@
+ #
+ # License:Creative Commons GNU GPL v2
+ # (http://creativecommons.org/licenses/GPL/2.0/)
+-#-------------------------------------
++# -------------------------------------
++"""
++Doctests
++
++>>> sys.argv = shlex.split('./ttvdb.py -B Sanctuary')
++>>> main()
++Banner:http://thetvdb.com/banners/graphical/80159-g4.jpg,http://thetvdb.com/banners/graphical/80159-g5.jpg,http://thetvdb.com/banners/graphical/80159-g3.jpg,http://thetvdb.com/banners/graphical/80159-g6.jpg,http://thetvdb.com/banners/graphical/80159-g2.jpg,http://thetvdb.com/banners/graphical/80159-g.jpg,http://thetvdb.com/banners/graphical/80159-g8.jpg
++0
++>>> sys.argv = shlex.split('./ttvdb.py -S SG-1 1 10')
++>>> main()
++<?xml version='1.0' encoding='UTF-8'?>
++<metadata>
++ <item>
++ <title>Stargate SG-1</title>
++ <subtitle>Thor's Hammer</subtitle>
++ <description>Teal'c and O'Neill are transported to an underground cage designed by the Asgard to protect an alien world from the Goa'uld.</description>
++ <season>1</season>
++ <episode>10</episode>
++ <certifications>
++ <certification locale="us" name="TV-PG"/>
++ </certifications>
++ <studios>
++ <studio name="Syfy"/>
++ </studios>
++ <runtime/>
++ <inetref>72449</inetref>
++ <collectionref>72449</collectionref>
++ <imdb>0118480</imdb>
++ <tmsref>EP00225421</tmsref>
++ <language>en</language>
++ <year>1997</year>
++ <releasedate>1997-09-26</releasedate>
++ <people>
++ <person job="Actor" name="Richard Dean Anderson" character="Jack O'Neill" url="http://thetvdb.com/banners/actors/17720.jpg" thumb="http://thetvdb.com/banners/actors/17720.jpg"/>
++ <person job="Actor" name="Amanda Tapping" character="Samantha Carter" url="http://thetvdb.com/banners/actors/17722.jpg" thumb="http://thetvdb.com/banners/actors/17722.jpg"/>
++ <person job="Actor" name="Michael Shanks" character="Dr. Daniel Jackson" url="http://thetvdb.com/banners/actors/17723.jpg" thumb="http://thetvdb.com/banners/actors/17723.jpg"/>
++ <person job="Actor" name="Ben Browder" character="Cameron Mitchell" url="http://thetvdb.com/banners/actors/17725.jpg" thumb="http://thetvdb.com/banners/actors/17725.jpg"/>
++ <person job="Actor" name="Christopher Judge" character="Teal'c" url="http://thetvdb.com/banners/actors/17726.jpg" thumb="http://thetvdb.com/banners/actors/17726.jpg"/>
++ <person job="Actor" name="Beau Bridges" character="Henry "Hank" Landry" url="http://thetvdb.com/banners/actors/17719.jpg" thumb="http://thetvdb.com/banners/actors/17719.jpg"/>
++ <person job="Actor" name="Don S. Davis" character="George S. Hammond" url="http://thetvdb.com/banners/actors/17721.jpg" thumb="http://thetvdb.com/banners/actors/17721.jpg"/>
++...
++ <person job="Guest Star" name="James Earl Jones"/>
++ <person job="Guest Star" name="Galyn Gorg"/>
++ <person job="Guest Star" name="Tamsin Kelsey"/>
++ <person job="Guest Star" name="Vincent Hammond"/>
++ <person job="Guest Star" name="Mark Gibbon"/>
++ <person job="Director" name="Brad Turner"/>
++ <person job="Author" name="Katharyn Michaelian Powers"/>
++ </people>
++ <images>
++ <image type="screenshot" url="http://thetvdb.com/banners/episodes/72449/85759.jpg" thumb="http://thetvdb.com/banners/_cache/episodes/72449/85759.jpg"/>
++ <image type="coverart" url="http://www.thetvdb.com/banners/seasons/72449-1-9.jpg" thumb="http://www.thetvdb.com/banners/_cache/seasons/72449-1-9.jpg"/>
++ <image type="coverart" url="http://www.thetvdb.com/banners/seasons/72449-1.jpg" thumb="http://www.thetvdb.com/banners/_cache/seasons/72449-1.jpg"/>
++ <image type="coverart" url="http://www.thetvdb.com/banners/seasons/72449-1-2.jpg" thumb="http://www.thetvdb.com/banners/_cache/seasons/72449-1-2.jpg"/>
++ <image type="coverart" url="http://www.thetvdb.com/banners/seasons/72449-1-8.jpg" thumb="http://www.thetvdb.com/banners/_cache/seasons/72449-1-8.jpg"/>
++ <image type="coverart" url="http://www.thetvdb.com/banners/seasons/185-1.jpg" thumb="http://www.thetvdb.com/banners/_cache/seasons/185-1.jpg"/>
++ <image type="fanart" url="http://www.thetvdb.com/banners/fanart/original/72449-55.jpg" thumb="http://www.thetvdb.com/banners/_cache/fanart/original/72449-55.jpg" width="1920" height="1080"/>
++ <image type="fanart" url="http://www.thetvdb.com/banners/fanart/original/72449-34.jpg" thumb="http://www.thetvdb.com/banners/_cache/fanart/original/72449-34.jpg" width="1280" height="720"/>
++...
++ <image type="fanart" url="http://www.thetvdb.com/banners/fanart/original/72449-75.jpg" thumb="http://www.thetvdb.com/banners/_cache/fanart/original/72449-75.jpg" width="1280" height="720"/>
++ </images>
++ </item>
++</metadata>
++0
++>>> sys.argv = shlex.split('ttvdb -PFB "Stargate SG-1"')
++>>> main()
++Coverart:http://thetvdb.com/banners/posters/72449-4.jpg,http://thetvdb.com/banners/posters/72449-5.jpg,http://thetvdb.com/banners/posters/72449-9.jpg,http://thetvdb.com/banners/posters/72449-6.jpg,http://thetvdb.com/banners/posters/72449-7.jpg,http://thetvdb.com/banners/posters/72449-8.jpg,http://thetvdb.com/banners/posters/72449-1.jpg,http://thetvdb.com/banners/posters/72449-3.jpg,http://thetvdb.com/banners/posters/72449-2.jpg
++Fanart:http://thetvdb.com/banners/fanart/original/72449-55.jpg,http://thetvdb.com/banners/fanart/original/72449-34.jpg,http://thetvdb.com/banners/fanart/original/72449-23.jpg,http://thetvdb.com/banners/fanart/original/72449-24.jpg,http://thetvdb.com/banners/fanart/original/72449-29.jpg,http://thetvdb.com/banners/fanart/original/72449-6.jpg,http://thetvdb.com/banners/fanart/original/72449-26.jpg,http://thetvdb.com/banners/fanart/original/72449-36.jpg,http://thetvdb.com/banners/fanart/original/72449-38.jpg,http://thetvdb.com/banners/fanart/original/72449-50.jpg,http://thetvdb.com/banners/fanart/original/72449-27.jpg,http://thetvdb.com/banners/fanart/original/72449-31.jpg,http://thetvdb.com/banners/fanart/original/72449-32.jpg,http://thetvdb.com/banners/fanart/original/72449-35.jpg,http://thetvdb.com/banners/fanart/original/72449-42.jpg,http://thetvdb.com/banners/fanart/original/72449-44.jpg,http://thetvdb.com/banners/fanart/original/72449-25.jpg,http://thetvdb.com/banners/fanart/orig
inal/72449-28.jpg,http://thetvdb.com/banners/fanart/original/72449-47.jpg...
anart/original/72449-4.jpg,http://thetvdb.com/banners/fanart/original/724...
rs/fanart/original/72449-18.jpg,http://thetvdb.com/banners/fanart/origina...
++Banner:http://thetvdb.com/banners/graphical/72449-g6.jpg,http://thetvdb.com/banners/graphical/72449-g7.jpg,http://thetvdb.com/banners/graphical/185-g3.jpg,http://thetvdb.com/banners/graphical/185-g2.jpg,http://thetvdb.com/banners/graphical/72449-g2.jpg,http://thetvdb.com/banners/graphical/72449-g9.jpg,http://thetvdb.com/banners/blank/72449.jpg,http://thetvdb.com/banners/graphical/72449-g3.jpg,http://thetvdb.com/banners/graphical/72449-g4.jpg,http://thetvdb.com/banners/graphical/185-g.jpg,http://thetvdb.com/banners/graphical/72449-g.jpg,http://thetvdb.com/banners/text/185.jpg,http://thetvdb.com/banners/graphical/72449-g5.jpg,http://thetvdb.com/banners/graphical/72449-g8.jpg
++0
++
++# Coverart:http://www.thetvdb.com/banners/posters/72449-1.jpg
++# Fanart:http://www.thetvdb.com/banners/fanart/original/72449-1.jpg
++# Banner:http://www.thetvdb.com/banners/graphical/185-g3.jpg
++>>> sys.argv = shlex.split('ttvdb -B "Night Gallery"')
++>>> main()
++Banner:http://thetvdb.com/banners/graphical/70382-g4.jpg,http://thetvdb.com/banners/graphical/1013-g.jpg,http://thetvdb.com/banners/blank/70382.jpg,http://thetvdb.com/banners/graphical/70382-g.jpg,http://thetvdb.com/banners/graphical/70382-g2.jpg,http://thetvdb.com/banners/graphical/70382-g3.jpg
++0
++
++# http://www.thetvdb.com/banners/blank/70382.jpg
++>>> sys.argv = shlex.split('ttvdb -Bl en Lost')
++>>> main()
++Banner:http://thetvdb.com/banners/graphical/73739-g4.jpg,http://thetvdb.com/banners/graphical/73739-g13.jpg,http://thetvdb.com/banners/graphical/73739-g18.jpg,http://thetvdb.com/banners/graphical/73739-g6.jpg,http://thetvdb.com/banners/graphical/73739-g12.jpg,http://thetvdb.com/banners/graphical/73739-g3.jpg,http://thetvdb.com/banners/graphical/24313-g2.jpg,http://thetvdb.com/banners/graphical/73739-g8.jpg,http://thetvdb.com/banners/graphical/73739-g.jpg,http://thetvdb.com/banners/graphical/73739-g5.jpg,http://thetvdb.com/banners/graphical/73739-g7.jpg,http://thetvdb.com/banners/graphical/73739-g10.jpg,http://thetvdb.com/banners/graphical/73739-g11.jpg,http://thetvdb.com/banners/graphical/24313-g.jpg,http://thetvdb.com/banners/graphical/73739-g2.jpg,http://thetvdb.com/banners/blank/73739.jpg
++0
++
++# Banner:http://www.thetvdb.com/banners/graphical/73739-g4.jpg,http://www.t...
++> ttvdb -N --configure="/home/user/.tvdb/tvdb.conf" "Eleventh Hour" "H2O"
++>>> sys.argv = shlex.split('ttvdb -N --configure=./tvdb_test.conf "Eleventh Hour" H2O')
++>>> main()
++<?xml version='1.0' encoding='UTF-8'?>
++<metadata>
++ <item>
++ <title>Eleventh Hour (US)</title>
++ <subtitle>H2O</subtitle>
++ <description>An epidemic of sudden, violent outbursts by law-abiding citizens draws Dr. Jacob Hood to a quiet Texas community to investigate - but he soon succumbs to the same erratic behavior.</description>
++ <season>1</season>
++ <episode>10</episode>
++ <certifications>
++ <certification locale="us" name="TV-14"/>
++ </certifications>
++ <studios>
++ <studio name="CBS"/>
++ </studios>
++ <runtime/>
++ <inetref>83066</inetref>
++ <collectionref>83066</collectionref>
++ <imdb>1118697</imdb>
++ <language>en</language>
++ <year>2009</year>
++ <releasedate>2009-01-15</releasedate>
++ <people>
++ <person job="Actor" name="Rufus Sewell" character="Jacob Hood" url="http://thetvdb.com/banners/actors/78899.jpg" thumb="http://thetvdb.com/banners/actors/78899.jpg"/>
++ <person job="Actor" name="Marley Shelton" character="Rachel Young" url="http://thetvdb.com/banners/actors/78898.jpg" thumb="http://thetvdb.com/banners/actors/78898.jpg"/>
++ <person job="Actor" name="Omar Benson Miller" character="Felix Lee" url="http://thetvdb.com/banners/" thumb="http://thetvdb.com/banners/"/>
++ <person job="Actor" name="Chris Krauser" character="EMT" url="http://thetvdb.com/banners/" thumb="http://thetvdb.com/banners/"/>
++ <person job="Actor" name="Erica Frene" character="Receptionist" url="http://thetvdb.com/banners/" thumb="http://thetvdb.com/banners/"/>
++ <person job="Actor" name="Lei'lah Star" character="Sick Kid" url="http://thetvdb.com/banners/" thumb="http://thetvdb.com/banners/"/>
++ <person job="Actor" name="Mark C. Baldwin" character="Infomercial Announcer" url="http://thetvdb.com/banners/" thumb="http://thetvdb.com/banners/"/>
++ <person job="Director" name="McDonough"/>
++ <person job="Author" name="Kim Newton"/>
++ </people>
++ <images>
++ <image type="screenshot" url="http://thetvdb.com/banners/episodes/83066/416216.jpg" thumb="http://thetvdb.com/banners/_cache/episodes/83066/416216.jpg"/>
++ <image type="coverart" url="http://www.thetvdb.com/banners/seasons/83066-1-2.jpg" thumb="http://www.thetvdb.com/banners/_cache/seasons/83066-1-2.jpg"/>
++ <image type="coverart" url="http://www.thetvdb.com/banners/seasons/83066-1.jpg" thumb="http://www.thetvdb.com/banners/_cache/seasons/83066-1.jpg"/>
++ <image type="fanart" url="http://www.thetvdb.com/banners/fanart/original/83066-1.jpg" thumb="http://www.thetvdb.com/banners/_cache/fanart/original/83066-1.jpg" width="1280" height="720"/>
++ <image type="fanart" url="http://www.thetvdb.com/banners/fanart/original/83066-3.jpg" thumb="http://www.thetvdb.com/banners/_cache/fanart/original/83066-3.jpg" width="1280" height="720"/>
++ <image type="fanart" url="http://www.thetvdb.com/banners/fanart/original/83066-5.jpg" thumb="http://www.thetvdb.com/banners/_cache/fanart/original/83066-5.jpg" width="1920" height="1080"/>
++ <image type="fanart" url="http://www.thetvdb.com/banners/fanart/original/83066-2.jpg" thumb="http://www.thetvdb.com/banners/_cache/fanart/original/83066-2.jpg" width="1280" height="720"/>
++ <image type="fanart" url="http://www.thetvdb.com/banners/fanart/original/83066-4.jpg" thumb="http://www.thetvdb.com/banners/_cache/fanart/original/83066-4.jpg" width="1280" height="720"/>
++ </images>
++ </item>
++</metadata>
++0
++
++# <language>en</language>
++# <image type="fanart" url="http://www.thetvdb.com/banners/fanart/original/83066-4.jpg" thumb="http://www.thetvdb.com/banners/_cache/fanart/original/83066-4.jpg" width="1280" height="720"/>
++# <image type="banner" url="http://www.thetvdb.com/banners/graphical/83066-g.jpg" thumb="http://www.thetvdb.com/banners/_cache/graphical/83066-g.jpg"/>
++(Return the season numbers for a series)
++> ttvdb --configure="./tvdb_test.conf" -n "SG-1"
++>>> sys.argv = shlex.split('ttvdb --configure=./tvdb_test.conf -n SG-1')
++>>> main()
++0,1,2,3,4,5,6,7,8,9,10
++0
++
++(Return the meta data for a specific series/season/episode)
++> ttvdb.py -D 80159 2 2
++>>> sys.argv = shlex.split('ttvdb -D 80159 2 2')
++>>> main()
++<?xml version='1.0' encoding='UTF-8'?>
++<metadata>
++ <item>
++ <title>Sanctuary</title>
++ <subtitle>End of Nights (2)</subtitle>
++ <description>Furious at being duped into a trap, Magnus takes on Kate, demanding information and complete access to her Cabal contacts. The Cabal’s true agenda is revealed and Magnus realizes that they are not only holding Ashley as ransom to obtain complete control of the Sanctuary Network, but turning her into the ultimate weapon. Now transformed into a Super Abnormal with devastating powers, Ashley and her newly cloned fighters begin their onslaught, destroying Sanctuaries in cities around the world. Tesla and Henry attempt to create a weapon that can stop the attacks…without killing Ashley. As the team prepares to defend the Sanctuary with Tesla’s new weapon, Magnus must come to the realization that they may not be able to stop the Cabal’s attacks without harming Ashley. She realizes she might have to choose between saving her only daughter, or losing the Sanctuary and all the lives and secrets within it.</description>
++ <season>2</season>
++ <episode>2</episode>
++ <certifications>
++ <certification locale="us" name="TV-PG"/>
++ </certifications>
++ <studios>
++ <studio name="Space"/>
++ </studios>
++ <runtime/>
++ <inetref>80159</inetref>
++ <collectionref>80159</collectionref>
++ <imdb>0965394</imdb>
++ <tmsref>EP01085421</tmsref>
++ <language>en</language>
++ <year>2009</year>
++ <releasedate>2009-10-16</releasedate>
++ <people>
++ <person job="Actor" name="Amanda Tapping" character="Dr. Helen Magnus" url="http://thetvdb.com/banners/actors/73053.jpg" thumb="http://thetvdb.com/banners/actors/73053.jpg"/>
++ <person job="Actor" name="Robin Dunne" character="Will Zimmerman" url="http://thetvdb.com/banners/actors/73054.jpg" thumb="http://thetvdb.com/banners/actors/73054.jpg"/>
++ <person job="Actor" name="Emilie Ullerup" character="Ashley Magnus" url="http://thetvdb.com/banners/actors/73055.jpg" thumb="http://thetvdb.com/banners/actors/73055.jpg"/>
++ <person job="Actor" name="Christopher Heyerdahl" character="John Druitt" url="http://thetvdb.com/banners/actors/73056.jpg" thumb="http://thetvdb.com/banners/actors/73056.jpg"/>
++ <person job="Actor" name="Christopher Heyerdahl" character="Bigfoot" url="http://thetvdb.com/banners/actors/309797.jpg" thumb="http://thetvdb.com/banners/actors/309797.jpg"/>
++ <person job="Actor" name="Ryan Robbins" character="Henry Foss" url="http://thetvdb.com/banners/actors/80072.jpg" thumb="http://thetvdb.com/banners/actors/80072.jpg"/>
++ <person job="Actor" name="Agam Darshi" character="Kate Freelander" url="http://thetvdb.com/banners/actors/118211.jpg" thumb="http://thetvdb.com/banners/actors/118211.jpg"/>
++ <person job="Actor" name="Vincent Gale" character="Nigel Griffin" url="http://thetvdb.com/banners/actors/372548.jpg" thumb="http://thetvdb.com/banners/actors/372548.jpg"/>
++ <person job="Actor" name="Peter Wingfield" character="James Watson" url="http://thetvdb.com/banners/actors/372549.jpg" thumb="http://thetvdb.com/banners/actors/372549.jpg"/>
++ <person job="Actor" name="Jonathon Young" character="Nikola Tesla" url="http://thetvdb.com/banners/actors/372550.jpg" thumb="http://thetvdb.com/banners/actors/372550.jpg"/>
++ <person job="Actor" name="Ian Tracey" character="Adam Worth" url="http://thetvdb.com/banners/actors/372551.jpg" thumb="http://thetvdb.com/banners/actors/372551.jpg"/>
++ <person job="Actor" name="Jim Byrnes" character="Gregory Magnus" url="http://thetvdb.com/banners/actors/372552.jpg" thumb="http://thetvdb.com/banners/actors/372552.jpg"/>
++ <person job="Actor" name="Polly Walker" character="Ranna Seneschal" url="http://thetvdb.com/banners/actors/372553.jpg" thumb="http://thetvdb.com/banners/actors/372553.jpg"/>
++ <person job="Actor" name="Robert Lawrenson" character="Declan Macrae" url="http://thetvdb.com/banners/" thumb="http://thetvdb.com/banners/"/>
++ <person job="Actor" name="Pascale Hutton" character="Abby Corrigan" url="http://thetvdb.com/banners/" thumb="http://thetvdb.com/banners/"/>
++ <person job="Actor" name="Lynda Boyd" character="Dana Whitcomb" url="http://thetvdb.com/banners/" thumb="http://thetvdb.com/banners/"/>
++ <person job="Actor" name="Shekhar Paleja" character="Ravi" url="http://thetvdb.com/banners/" thumb="http://thetvdb.com/banners/"/>
++ <person job="Actor" name="Chuck Campbell" character="Two Faced Guy" url="http://thetvdb.com/banners/" thumb="http://thetvdb.com/banners/"/>
++ <person job="Guest Star" name="Jonathon Young"/>
++ <person job="Guest Star" name="Christine Chatelain"/>
++ <person job="Guest Star" name="Robert Lawrenson"/>
++ <person job="Guest Star" name="Maiko Yamamoto"/>
++ <person job="Guest Star" name="Stanley Tsang"/>
++ <person job="Guest Star" name="Darren A. Hebert"/>
++ <person job="Guest Star" name="Lynda Boyd"/>
++ <person job="Director" name="Martin Wood"/>
++ <person job="Author" name="Damian Kindler"/>
++ </people>
++ <images>
++ <image type="screenshot" url="http://thetvdb.com/banners/episodes/80159/998441.jpg" thumb="http://thetvdb.com/banners/_cache/episodes/80159/998441.jpg"/>
++ <image type="coverart" url="http://www.thetvdb.com/banners/seasons/80159-2.jpg" thumb="http://www.thetvdb.com/banners/_cache/seasons/80159-2.jpg"/>
++ <image type="coverart" url="http://www.thetvdb.com/banners/seasons/80159-2-3.jpg" thumb="http://www.thetvdb.com/banners/_cache/seasons/80159-2-3.jpg"/>
++ <image type="coverart" url="http://www.thetvdb.com/banners/seasons/80159-2-2.jpg" thumb="http://www.thetvdb.com/banners/_cache/seasons/80159-2-2.jpg"/>
++ <image type="fanart" url="http://www.thetvdb.com/banners/fanart/original/80159-10.jpg" thumb="http://www.thetvdb.com/banners/_cache/fanart/original/80159-10.jpg" width="1280" height="720"/>
++ <image type="fanart" url="http://www.thetvdb.com/banners/fanart/original/80159-6.jpg" thumb="http://www.thetvdb.com/banners/_cache/fanart/original/80159-6.jpg" width="1920" height="1080"/>
++ <image type="fanart" url="http://www.thetvdb.com/banners/fanart/original/80159-3.jpg" thumb="http://www.thetvdb.com/banners/_cache/fanart/original/80159-3.jpg" width="1920" height="1080"/>
++ <image type="fanart" url="http://www.thetvdb.com/banners/fanart/original/80159-9.jpg" thumb="http://www.thetvdb.com/banners/_cache/fanart/original/80159-9.jpg" width="1920" height="1080"/>
++ <image type="fanart" url="http://www.thetvdb.com/banners/fanart/original/80159-7.jpg" thumb="http://www.thetvdb.com/banners/_cache/fanart/original/80159-7.jpg" width="1920" height="1080"/>
++ <image type="fanart" url="http://www.thetvdb.com/banners/fanart/original/80159-8.jpg" thumb="http://www.thetvdb.com/banners/_cache/fanart/original/80159-8.jpg" width="1920" height="1080"/>
++ <image type="fanart" url="http://www.thetvdb.com/banners/fanart/original/80159-2.jpg" thumb="http://www.thetvdb.com/banners/_cache/fanart/original/80159-2.jpg" width="1920" height="1080"/>
++ <image type="fanart" url="http://www.thetvdb.com/banners/fanart/original/80159-4.jpg" thumb="http://www.thetvdb.com/banners/_cache/fanart/original/80159-4.jpg" width="1280" height="720"/>
++ <image type="fanart" url="http://www.thetvdb.com/banners/fanart/original/80159-5.jpg" thumb="http://www.thetvdb.com/banners/_cache/fanart/original/80159-5.jpg" width="1920" height="1080"/>
++ <image type="fanart" url="http://www.thetvdb.com/banners/fanart/original/80159-21.jpg" thumb="http://www.thetvdb.com/banners/_cache/fanart/original/80159-21.jpg" width="1920" height="1080"/>
++ <image type="fanart" url="http://www.thetvdb.com/banners/fanart/original/80159-16.jpg" thumb="http://www.thetvdb.com/banners/_cache/fanart/original/80159-16.jpg" width="1280" height="720"/>
++ <image type="fanart" url="http://www.thetvdb.com/banners/fanart/original/80159-1.jpg" thumb="http://www.thetvdb.com/banners/_cache/fanart/original/80159-1.jpg" width="1280" height="720"/>
++ <image type="fanart" url="http://www.thetvdb.com/banners/fanart/original/80159-15.jpg" thumb="http://www.thetvdb.com/banners/_cache/fanart/original/80159-15.jpg" width="1920" height="1080"/>
++ <image type="fanart" url="http://www.thetvdb.com/banners/fanart/original/80159-17.jpg" thumb="http://www.thetvdb.com/banners/_cache/fanart/original/80159-17.jpg" width="1920" height="1080"/>
++ <image type="fanart" url="http://www.thetvdb.com/banners/fanart/original/80159-18.jpg" thumb="http://www.thetvdb.com/banners/_cache/fanart/original/80159-18.jpg" width="1920" height="1080"/>
++ <image type="fanart" url="http://www.thetvdb.com/banners/fanart/original/80159-19.jpg" thumb="http://www.thetvdb.com/banners/_cache/fanart/original/80159-19.jpg" width="1920" height="1080"/>
++ <image type="fanart" url="http://www.thetvdb.com/banners/fanart/original/80159-20.jpg" thumb="http://www.thetvdb.com/banners/_cache/fanart/original/80159-20.jpg" width="1920" height="1080"/>
++ <image type="fanart" url="http://www.thetvdb.com/banners/fanart/original/80159-22.jpg" thumb="http://www.thetvdb.com/banners/_cache/fanart/original/80159-22.jpg" width="1920" height="1080"/>
++ </images>
++ </item>
++</metadata>
++0
++
++(Return a list of "thetv.com series id and series name" that contain specific search word(s) )
++(!! Be careful with this option as poorly defined search words can result in large lists being returned !!)
++> ttvdb.py -M "night a"
++>>> sys.argv = shlex.split('ttvdb -M "night a"')
++>>> main()
++<?xml version='1.0' encoding='UTF-8'?>
++<metadata>
++ <item>
++ <language>en</language>
++ <title>A Night of Numbers</title>
++ <inetref>249306</inetref>
++ <collectionref>249306</collectionref>
++ <description>BBC FOUR celebrates mathematics and the beauty of numbers with a series of programmes about this most precise and exacting of all intellectual disciplines. Throughout the night, the channel will show films that offer insights into the minds of great mathematicians, and reveal the stories behind some of the great mathematical breakthroughs.</description>
++ <releasedate>2005-12-06</releasedate>
++ <images>
++ <image type="banner" url="http://www.thetvdb.com/banners/graphical/249306-g.jpg" thumb="http://www.thetvdb.com/banners/_cache/graphical/249306-g.jpg"/>
++ </images>
++ </item>
++ <item>
++ <language>en</language>
++ <title>A night at The Classic</title>
++ <inetref>224951</inetref>
++ <collectionref>224951</collectionref>
++ <description>Each episode of A Night at The Classic follows MC Brendhan Lovegrove and guest comedians as they perform for a different crowd on a different "night" at The Classic. Along with stand-up comedy recorded in front of a live audience, viewers are given a glimpse of what the comedians are like backstage, providing a rare insight into the rivalries and rituals of stand-up comedians.</description>
++ <releasedate>2010-11-03</releasedate>
++ <images>
++ <image type="banner" url="http://www.thetvdb.com/banners/graphical/224951-g.jpg" thumb="http://www.thetvdb.com/banners/_cache/graphical/224951-g.jpg"/>
++ </images>
++ </item>
++ <item>
++ <language>en</language>
++ <title>A Night at the Rijksmuseum</title>
++ <inetref>268908</inetref>
++ <collectionref>268908</collectionref>
++ <releasedate>2013-04-18</releasedate>
++ </item>
++ <item>
++ <language>en</language>
++ <title>A Night of Heroes: The Sun Military Awards</title>
++ <inetref>270984</inetref>
++ <collectionref>270984</collectionref>
++ <description>Annual celebration, A Night of Heroes: Also known as The Millies, the awards recognize the excellence and sacrifice made by Britain's Armed Forces</description>
++ <images>
++ <image type="banner" url="http://www.thetvdb.com/banners/graphical/270984-g.jpg" thumb="http://www.thetvdb.com/banners/_cache/graphical/270984-g.jpg"/>
++ </images>
++ </item>
++ <item>
++ <language>en</language>
++ <title>A Night of Exploration</title>
++ <inetref>271528</inetref>
++ <collectionref>271528</collectionref>
++ <description>For well over a century the National Geographic Society has been synonymous with pioneering expeditions, groundbreaking discoveries and breathtaking imagery of world cultures and exotic locations. In celebration of the iconic yellow border’s 125th anniversary, National Geographic Channel pays tribute to the hotshots, the mavericks and the best in their field who have devoted their lives to exploring the world around us and the groundbreaking discoveries that are making a difference.</description>
++ <images>
++ <image type="banner" url="http://www.thetvdb.com/banners/graphical/271528-g.jpg" thumb="http://www.thetvdb.com/banners/_cache/graphical/271528-g.jpg"/>
++ </images>
++ </item>
++ <item>
++ <language>en</language>
++ <title>A Night at the Office</title>
++ <inetref>118511</inetref>
++ <collectionref>118511</collectionref>
++ <description>On August 11th 2009, it was announced that the cast of The Office would be reuniting for a special, called "A Night at The Office", available at BBC2 and online, it was the entire first series of the seminal BBC comedy 'The Office' with new comments from the writers and celebrity fans shown between each episode.</description>
++ <releasedate>2009-08-17</releasedate>
++ <images>
++ <image type="banner" url="http://www.thetvdb.com/banners/graphical/118511-g.jpg" thumb="http://www.thetvdb.com/banners/_cache/graphical/118511-g.jpg"/>
++ </images>
++ </item>
++ <item>
++ <language>en</language>
++ <title>A Night With The Stars</title>
++ <inetref>256045</inetref>
++ <collectionref>256045</collectionref>
++ <description>For one night only, Professor Brian Cox goes unplugged in a specially recorded programme from the lecture theatre of the Royal Institution of Great Britain. In his own inimitable style, Brian takes an audience of famous faces, scientists and members of the public on a journey through some of the most challenging concepts in physics. With the help of Jonathan Ross, Simon Pegg, Sarah Millican and James May, Brian shows how diamonds - the hardest material in nature - are made up of nothingness; how things can be in an infinite number of places at once; why everything we see or touch in the universe exists; and how a diamond in the heart of London is in communication with the largest diamond in the cosmos.</description>
++ <releasedate>2011-12-18</releasedate>
++ <images>
++ <image type="banner" url="http://www.thetvdb.com/banners/graphical/256045-g.jpg" thumb="http://www.thetvdb.com/banners/_cache/graphical/256045-g.jpg"/>
++ </images>
++ </item>
++ <item>
++ <language>en</language>
++ <title>A Night at the Festival Club</title>
++ <inetref>268969</inetref>
++ <collectionref>268969</collectionref>
++ <description>A Night at the Festival Club is an Australian stand-up comedy television event created and executive produced by the Comedy Channel programming director Darren Chau, produced by Ted Robinson and GNW TV Productions for the Comedy Channel as part of the Melbourne International Comedy Festival. The series centres around bottling the unique comedic live performances and moments that occur late night in the Festival Club during the Melbourne International Comedy Festival.</description>
++ <releasedate>2008-05-02</releasedate>
++ </item>
++ <item>
++ <language>en</language>
++ <title>A Clear Midsummer Night</title>
++ <inetref>286538</inetref>
++ <collectionref>286538</collectionref>
++ <description>The daughter of a real estate mogul Xia Wan Qing, has seemingly no way of retreating after a friend's betrayal and her boyfriend backing out of their wedding. Fortunately, she's saved by business genius Qiao Jin Fan. Jin Fan is a "playboy" and the future successor for Qiao corporation. He extends an offering hand and together they embark on a path of revenge. Each for reasons of their own, begin a love with "uncertain motives." After enduring circumstances because of their families' competing interests and a number of conspiracies the two find true love.</description>
++ </item>
++ <item>
++ <language>en</language>
++ <title>A Christmas Night with the Stars</title>
++ <inetref>248911</inetref>
++ <collectionref>248911</collectionref>
++ <description>Christmas Night with the Stars was a television show broadcast each Christmas night by the BBC from 1958 to 1972 (with the exception of 1961, 1965 and 1966) and also revived in 1994. The show was hosted each year by a leading star of BBC TV and featured specially made short seasonal editions (typically about 10 minutes long) of the previous year's most popular BBC sitcoms and light entertainment programs. The show was voted 24th in the Channel 4 100 Greatest Christmas Moments. Most of the variety segments no longer exist.</description>
++ <releasedate>1958-12-25</releasedate>
++ <images>
++ <image type="banner" url="http://www.thetvdb.com/banners/graphical/248911-g2.jpg" thumb="http://www.thetvdb.com/banners/_cache/graphical/248911-g2.jpg"/>
++ </images>
++ </item>
++ <item>
++ <language>en</language>
++ <title>A Night With My Ex</title>
++ <inetref>331751</inetref>
++ <collectionref>331751</collectionref>
++ <description>Do you have unfinished business with a partner from a previous relationship? All of the onetime couples featured on ``A Night With My Ex'' do, and the show is letting them tie up loose ends from the past. In each episode, a pair of exes spend a night together in a one-bedroom apartment complete with a multiple-camera setup. They are left to their own devices -- with no producers and no interruptions -- to try to hash things out. The participants get things off their chests, ask hard-hitting questions and face accusations of infidelity with the ultimate goal of achieving closure on the relationship. Sometimes that closure means a clean break, and other times it leads to renewing the spark and rekindling the romance. Regardless of the outcome, anything goes on the road to reaching that point as the couples confront their pasts -- and their futures.</description>
++ <releasedate>2017-07-18</releasedate>
++ </item>
++ <item>
++ <language>en</language>
++ <title>On a Lustful Night Mingling with a Priest...</title>
++ <inetref>325375</inetref>
++ <collectionref>325375</collectionref>
++ <description>The reunion of Kujo with his old female classmate. He has inherited his parents' temple and became a priest. However, after the two became drunk, he does something unexpected of him to her!</description>
++ <releasedate>2017-04-03</releasedate>
++ </item>
++ <item>
++ <language>en</language>
++ <title>Love on a Saturday Night</title>
++ <inetref>74382</inetref>
++ <collectionref>74382</collectionref>
++ <releasedate>2004-02-01</releasedate>
++ </item>
++ <item>
++ <language>en</language>
++ <title>Britain's Tudor Treasure A Night At Hampton Court</title>
++ <inetref>332440</inetref>
++ <collectionref>332440</collectionref>
++ <description>Lucy Worsley and David Starkey celebrate the 500th anniversary of Britain's finest surviving Tudor building, Hampton Court. As Henry VIII's pleasure palace, Hampton Court was a showcase for royal magnificence and ceremony - and the most important event of all was the christening of Henry's long-awaited son, Prince Edward, on October 15th, 1537. Lucy and David explore how Tudor art, architecture and ritual came together for this momentous occasion. Drawing on historical records and with the help of a team of experts, they recreate key elements of the christening ceremony - including a magnificent set-piece procession through Hampton Court involving nearly 100 people in full Tudor costume.</description>
++ </item>
++</metadata>
++0
++
++(Return TV series collection data of "thetv.com series id" for a specified language)
++>>> sys.argv = shlex.split('ttvdb -l de -C 80159')
++>>> main()
++<?xml version='1.0' encoding='UTF-8'?>
++<metadata>
++ <item>
++ <language>de</language>
++ <title>Sanctuary</title>
++ <network>Space</network>
++ <airday>Friday</airday>
++ <airtime>10:00 PM</airtime>
++ <description>Dr. Helen Magnus ist eine so brillante wie geheimnisvolle Wissenschaftlerin die sich mit den Kreaturen der Nacht beschäftigt. In ihrem Unterschlupf - genannt "Sanctuary" - hat sie ein Team versammelt, das seltsame und furchteinflößende Ungeheuer untersucht, die mit den Menschen auf der Erde leben. Konfrontiert mit ihren düstersten Ängsten und ihren schlimmsten Alpträumen versucht das Sanctuary-Team, die Welt vor den Monstern - und die Monster vor der Welt zu schützen.</description>
++ <certifications>
++ <certification locale="us" name="TV-PG"/>
++ </certifications>
++ <studios>
++ <studio name="Space"/>
++ </studios>
++ <runtime>60</runtime>
++ <inetref>80159</inetref>
++ <imdb>0965394</imdb>
++ <userrating>8.1</userrating>
++ <ratingcount>168</ratingcount>
++ <year>2007</year>
++ <releasedate>2007-03-14</releasedate>
++ <lastupdated>...</lastupdated>
++ <status>Ended</status>
++ <images>
++ <image type="fanart" url="http://www.thetvdb.com/banners/fanart/original/80159-11.jpg" thumb="http://www.thetvdb.com/banners/_cache/fanart/original/80159-11.jpg"/>
++ <image type="banner" url="http://thetvdb.com/banners/graphical/80159-g4.jpg" thumb="http://thetvdb.com/banners/_cache/graphical/80159-g4.jpg"/>
++ </images>
++ </item>
++</metadata>
++0
++
++# test match is loose due ordering differences between py2 and 3
++# i.e. dict key ordering
++# key ordering is not sorted so that Title is first for existing client
++# compatability
++>>> sys.argv = shlex.split('ttvdb -l en -a US -D 281053')
++>>> main()
++Title:Fixer Upper
++Season:0
++Episode:1
++Subtitle:The Waco Way of Life
++Year:2014
++ReleaseDate:2014-07-16
++Director:
++Plot:Chip and Joanna Gaines tell why they love raising a family in Waco, Texas.
++UserRating:
++Writers:
++Screenshot:
++Language:en
++Airedseasonid:583817
++Dvddiscid:
++Id:5463514
++Imdbid:
++Lastupdated:1451954464
++Lastupdatedby:447800
++Productioncode:
++Seriesid:281053
++Showurl:
++Siterating:0
++Siteratingcount:0
++Thumbadded:
++Thumbauthor:1
++Cast:Chip Gaines, Joanna Gaines
++Runtime:45
++Title:Fixer Upper
++...
++Coverart:http://thetvdb.com/banners/posters/281053-4.jpg,http://thetvdb.com/banners/posters/281053-3.jpg,http://thetvdb.com/banners/posters/281053-1.jpg,http://thetvdb.com/banners/posters/281053-4.jpg,http://thetvdb.com/banners/posters/281053-3.jpg,http://thetvdb.com/banners/posters/281053-1.jpg,http://thetvdb.com/banners/posters/281053-2.jpg,http://thetvdb.com/banners/posters/281053-2.jpg
++Fanart:http://thetvdb.com/banners/fanart/original/281053-3.jpg,http://thetvdb.com/banners/fanart/original/281053-3.jpg,http://thetvdb.com/banners/fanart/original/281053-4.jpg,http://thetvdb.com/banners/fanart/original/281053-4.jpg,http://thetvdb.com/banners/fanart/original/281053-1.jpg,http://thetvdb.com/banners/fanart/original/281053-1.jpg,http://thetvdb.com/banners/fanart/original/281053-2.jpg,http://thetvdb.com/banners/fanart/original/281053-2.jpg,http://thetvdb.com/banners/fanart/original/281053-6.jpg,http://thetvdb.com/banners/fanart/original/281053-5.jpg,http://thetvdb.com/banners/fanart/original/281053-7.jpg,http://thetvdb.com/banners/fanart/original/281053-8.jpg,http://thetvdb.com/banners/fanart/original/281053-9.jpg,http://thetvdb.com/banners/fanart/original/281053-10.jpg,http://thetvdb.com/banners/fanart/original/281053-6.jpg,http://thetvdb.com/banners/fanart/original/281053-5.jpg,http://thetvdb.com/banners/fanart/original/281053-7.jpg,http://thetvdb.com/banners/fanart/or
iginal/281053-8.jpg,http://thetvdb.com/banners/fanart/original/281053-9.j...
++Banner:http://thetvdb.com/banners/graphical/281053-g2.jpg,http://thetvdb.com/banners/graphical/281053-g2.jpg,http://thetvdb.com/banners/text/281053.jpg,http://thetvdb.com/banners/graphical/281053-g.jpg,http://thetvdb.com/banners/text/281053.jpg,http://thetvdb.com/banners/graphical/281053-g.jpg
++0
++
++>>> sys.argv = shlex.split('ttvdb.py -l en -a US -N 72108 Pyramid')
++>>> main()
++<?xml version='1.0' encoding='UTF-8'?>
++<metadata>
++ <item>
++ <title>NCIS</title>
++ <subtitle>Pyramid</subtitle>
++ <description>The lives of NCIS members are in jeopardy when they come face-to-face with the infamous Port-to-Port killer, on the eighth season finale of NCIS.</description>
++ <season>8</season>
++ <episode>24</episode>
++ <certifications>
++ <certification locale="us" name="TV-14"/>
++ </certifications>
++ <studios>
++ <studio name="CBS"/>
++ </studios>
++ <runtime/>
++ <inetref>72108</inetref>
++ <collectionref>72108</collectionref>
++ <tmsref>EP00681911</tmsref>
++ <imdb/>
++ <language>en</language>
++ <year>2011</year>
++ <releasedate>2011-05-17</releasedate>
++ <people>
++ <person job="Actor" name="Mark Harmon" character="Leroy Jethro Gibbs" url="http://thetvdb.com/banners/actors/70164.jpg" thumb="http://thetvdb.com/banners/actors/70164.jpg"/>
++ <person job="Actor" name="Sean Murray" character="Timothy "Tim" McGee" url="http://thetvdb.com/banners/actors/70163.jpg" thumb="http://thetvdb.com/banners/actors/70163.jpg"/>
++ <person job="Actor" name="Emily Wickersham" character="Eleanor “Ellie” Bishop" url="http://thetvdb.com/banners/actors/321201.jpg" thumb="http://thetvdb.com/banners/actors/321201.jpg"/>
++ <person job="Actor" name="David McCallum" character="Donald "Ducky" Mallard" url="http://thetvdb.com/banners/actors/70159.jpg" thumb="http://thetvdb.com/banners/actors/70159.jpg"/>
++ <person job="Actor" name="Pauley Perrette" character="Abigail "Abby" Sciuto" url="http://thetvdb.com/banners/actors/70161.jpg" thumb="http://thetvdb.com/banners/actors/70161.jpg"/>
++ <person job="Actor" name="Rocky Carroll" character="Leon Vance" url="http://thetvdb.com/banners/actors/127861.jpg" thumb="http://thetvdb.com/banners/actors/127861.jpg"/>
++ <person job="Actor" name="Brian Dietzen" character="Jimmy Palmer" url="http://thetvdb.com/banners/actors/219761.jpg" thumb="http://thetvdb.com/banners/actors/219761.jpg"/>
++ <person job="Actor" name="Wilmer Valderrama" character="Nicholas "Nick" Torres" url="http://thetvdb.com/banners/actors/394895.jpg" thumb="http://thetvdb.com/banners/actors/394895.jpg"/>
++ <person job="Actor" name="Michael Weatherly" character="Anthony "Tony" DiNozzo" url="http://thetvdb.com/banners/actors/70160.jpg" thumb="http://thetvdb.com/banners/actors/70160.jpg"/>
++ <person job="Actor" name="Sasha Alexander" character="Caitlin "Kate" Todd" url="http://thetvdb.com/banners/actors/70162.jpg" thumb="http://thetvdb.com/banners/actors/70162.jpg"/>
++ <person job="Actor" name="Cote de Pablo" character="Ziva David" url="http://thetvdb.com/banners/actors/70165.jpg" thumb="http://thetvdb.com/banners/actors/70165.jpg"/>
++ <person job="Actor" name="Lauren Holly" character="Jennifer "Jenny" Shepard" url="http://thetvdb.com/banners/actors/77850.jpg" thumb="http://thetvdb.com/banners/actors/77850.jpg"/>
++ <person job="Actor" name="Jennifer Espósito" character="Alex Quinn" url="http://thetvdb.com/banners/actors/402126.jpg" thumb="http://thetvdb.com/banners/actors/402126.jpg"/>
++ <person job="Actor" name="Duane Henry" character="Clayton Reeves" url="http://thetvdb.com/banners/actors/418530.jpg" thumb="http://thetvdb.com/banners/actors/418530.jpg"/>
++ <person job="Guest Star" name="Kerr Smith"/>
++ <person job="Guest Star" name="Sarah Jane Morris"/>
++ <person job="Guest Star" name="Matt Craven"/>
++ <person job="Guest Star" name="David Dayan Fisher"/>
++ <person job="Guest Star" name="Muse Watson"/>
++ <person job="Guest Star" name="Alimi Ballard"/>
++ <person job="Guest Star" name="Matthew Willig"/>
++ <person job="Guest Star" name="Tehmina Sunny"/>
++ <person job="Guest Star" name="Enrique Murciano"/>
++ <person job="Guest Star" name="Jude Ciccolella"/>
++ <person job="Guest Star" name="Vera Miao"/>
++ <person job="Director" name="Dennis Smith"/>
++ <person job="Author" name="Gary Glasberg"/>
++ </people>
++ <images>
++ <image type="screenshot" url="http://thetvdb.com/banners/episodes/72108/4078484.jpg" thumb="http://thetvdb.com/banners/_cache/episodes/72108/4078484.jpg"/>
++ <image type="coverart" url="http://www.thetvdb.com/banners/seasons/72108-8-2.jpg" thumb="http://www.thetvdb.com/banners/_cache/seasons/72108-8-2.jpg"/>
++ <image type="coverart" url="http://www.thetvdb.com/banners/seasons/72108-8.jpg" thumb="http://www.thetvdb.com/banners/_cache/seasons/72108-8.jpg"/>
++ <image type="coverart" url="http://www.thetvdb.com/banners/seasons/72108-8-7.jpg" thumb="http://www.thetvdb.com/banners/_cache/seasons/72108-8-7.jpg"/>
++ <image type="fanart" url="http://www.thetvdb.com/banners/fanart/original/72108-31.jpg" thumb="http://www.thetvdb.com/banners/_cache/fanart/original/72108-31.jpg" width="1920" height="1080"/>
++...
++ <image type="fanart" url="http://www.thetvdb.com/banners/fanart/original/72108-33.jpg" thumb="http://www.thetvdb.com/banners/_cache/fanart/original/72108-33.jpg" width="1920" height="1080"/>
++ </images>
++ </item>
++</metadata>
++0
++
++>>> sys.argv = shlex.split('ttvdb.py -l en -a US -N 283661 "Egg Hunt"')
++>>> main()
++<?xml version='1.0' encoding='UTF-8'?>
++<metadata>
++ <item>
++ <title>Jack Hanna's Wild Countdown</title>
++ <subtitle>Egg Hunt</subtitle>
++ <description>Jungle Jack takes off on a very special Egg Hunt, looking for creatures big and small that hatch from eggs! Crocodiles, Bald Eagles, Sea Turtles, Ostriches, Penguins, and more!</description>
++ <season>6</season>
++ <episode>17</episode>
++ <certifications>
++ <certification locale="us" name="TV-G"/>
++ </certifications>
++ <studios>
++ <studio name="ABC (US)"/>
++ </studios>
++ <runtime/>
++ <inetref>283661</inetref>
++ <collectionref>283661</collectionref>
++ <imdb>3062384</imdb>
++ <tmsref>EP01441760</tmsref>
++ <language>en</language>
++ <year>2017</year>
++ <releasedate>2017-04-15</releasedate>
++ <images>
++ <image type="screenshot" url="http://thetvdb.com/banners/episodes/283661/6050716.jpg" thumb="http://thetvdb.com/banners/_cache/episodes/283661/6050716.jpg"/>
++ <image type="coverart" url="http://www.thetvdb.com/banners/posters/283661-1.jpg" thumb="http://www.thetvdb.com/banners/_cache/posters/283661-1.jpg" width="680" height="1000"/>
++ <image type="fanart" url="http://www.thetvdb.com/banners/fanart/original/283661-1.jpg" thumb="http://www.thetvdb.com/banners/_cache/fanart/original/283661-1.jpg" width="1920" height="1080"/>
++ </images>
++ </item>
++</metadata>
++0
++
++"""
++from __future__ import print_function
++
+ __title__ ="TheTVDB.com";
+ __author__="R.D.Vaughan"
+-__version__="1.1.5"
++__version__="2.0.0"
++
+ # Version .1 Initial development
+ # Version .2 Add an option to get season and episode numbers from ep name
+ # Version .3 Cleaned up the documentation and added a usage display option
+@@ -137,6 +679,7 @@ __version__="1.1.5"
+ # Version 1.1.5 Add the -C (collection option) with corresponding XML output
+ # and add a <collectionref> XML tag to Search and Query XML output
+ # Version 1.1.6 Honor series name overrides during TV series search
++# Version 2.0.0 Update to API V2
+
+ usage_txt='''
+ Usage: ttvdb.py usage: ttvdb -hdruviomMPFBDSC [parameters]
+@@ -338,6 +881,7 @@ Banner:http://www.thetvdb.com/banners/graphical/73739-g4.jpg,http://www.t....
+ </item>
+ </metadata>
+ '''
++
+ # Episode keys that can be used in a episode data/information search.
+ # All keys are currently being used.
+ '''
+@@ -368,47 +912,94 @@ Banner:http://www.thetvdb.com/banners/graphical/73739-g4.jpg,http://www.t....
+ 'episodename'
+ '''
+
+-
+ # System modules
+-import sys, os, re, locale, ConfigParser
++import sys, os, re
+ from optparse import OptionParser
+ from copy import deepcopy
+-
+-# Verify that tvdb_api.py, tvdb_ui.py and tvdb_exceptions.py are available
++# shlex for doctest
++import shlex
++
++# import logging
++# logger = logging.getLogger()
++# ch = logging.StreamHandler()
++# fh = logging.FileHandler("ttvdb.log")
++# #ch.setLevel(logging.DEBUG)
++# fh.setLevel(logging.DEBUG)
++# logging.getLogger("dicttoxml").setLevel(logging.WARN)
++# logging.getLogger("tvdb_api").setLevel(logging.DEBUG)
++# logger.addHandler(ch)
++# logger.addHandler(fh)
++
++IS_PY2 = sys.version_info[0] == 2
++if IS_PY2:
++ import ConfigParser
++else:
++ import configparser as ConfigParser
++
++class tvdb_account:
++ # explicit username and account id are not required
++ # to use the API. API documentation is unclear in this regard
++ username = ""
++ account_identifier = ""
++ apikey = '0BB856A59C51D607'
++
++# Verify that tvdb_api.py are available
+ try:
+ # thetvdb.com specific modules
+- import MythTV.ttvdb.tvdb_ui as tvdb_ui
+- # from tvdb_api import Tvdb
+ import MythTV.ttvdb.tvdb_api as tvdb_api
+- from MythTV.ttvdb.tvdb_exceptions import (tvdb_error, tvdb_shownotfound, tvdb_seasonnotfound, tvdb_episodenotfound, tvdb_episodenotfound, tvdb_attributenotfound, tvdb_userabort)
++ from MythTV.ttvdb.tvdb_api import (tvdb_error, tvdb_shownotfound, tvdb_seasonnotfound, tvdb_episodenotfound, tvdb_episodenotfound, tvdb_attributenotfound, tvdb_userabort)
+
+ # verify version of tvdbapi to make sure it is at least 1.0
+- if tvdb_api.__version__ < '1.0':
+- print "\nYour current installed tvdb_api.py version is (%s)\n" % tvdb_api.__version__
++ if tvdb_api.__version__ < '2.0':
++ print("\nYour current installed tvdb_api.py version is (%s)\n" % tvdb_api.__version__)
+ raise
+-except Exception, e:
+- print '''
+-The modules tvdb_api.py (v1.0.0 or greater), tvdb_ui.py, tvdb_exceptions.py and cache.py.
++except Exception as e:
++ print('''
++The modules tvdb_api.py (v2.0 or greater).
+ They should have been installed along with the MythTV python bindings.
+ Error:(%s)
+-''' % e
++''' % e)
+ sys.exit(1)
++finally:
++ pass
+
+ try:
+ from MythTV.utility import levenshtein
+-except Exception, e:
+- print """Could not import levenshtein string distance method from MythTV Python Bindings
++except Exception as e:
++ print("""Could not import levenshtein string distance method from MythTV Python Bindings
+ Error:(%s)
+-""" % e
++""" % e)
+ sys.exit(1)
+
+ try:
+- from StringIO import StringIO
++ if IS_PY2:
++ from StringIO import StringIO
++ else:
++ from io import StringIO
+ from lxml import etree as etree
+-except Exception, e:
++except Exception as e:
+ sys.stderr.write(u'\n! Error - Importing the "lxml" and "StringIO" python libraries failed on error(%s)\n' % e)
+ sys.exit(1)
+
++from MythTV.utility.dicttoxml import dicttoxml
++try:
++ import json
++ from lxml import etree as eTree
++except Exception as e:
++ sys.stderr.write(u'\n! Error - Importing the "lxml" python library failed on error(%s)\n' % e)
++ sys.exit(1)
++
++if IS_PY2:
++ stdio_type = file
++else:
++ import io
++ stdio_type = io.TextIOWrapper
++ unicode = str
++
++# disable the insecure request warning that we know we are going to get
++import urllib3
++urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
++
+ # Check that the lxml library is current enough
+ # From the lxml documents it states: (http://codespeak.net/lxml/installation.html)
+ # "If you want to use XPath, do not use libxml2 2.6.27. We recommend libxml2 2.7.2 or later"
+@@ -429,8 +1020,6 @@ if version < '2.7.2':
+ http_find="http://www.thetvdb.com"
+ http_replace="http://www.thetvdb.com" #Keep replace code "just in case"
+
+-logfile="/tmp/ttvdb.log"
+-
+ name_parse=[
+ # foo_[s01]_[e01]
+ re.compile('''^(.+?)[ \._\-]\[[Ss]([0-9]+?)\]_\[[Ee]([0-9]+?)\]?[^\\/]*$'''),
+@@ -447,7 +1036,7 @@ name_parse=[
+ # Episode meta data that is massaged
+ massage={'writer':'|','director':'|', 'overview':'&', 'gueststars':'|' }
+ # Keys and titles used for episode data (option '-D')
+-data_keys =['seasonnumber','episodenumber','episodename','firstaired','director','overview','rating','writer','filename','language' ]
++data_keys =['airedSeason','airedEpisodeNumber','episodeName','firstAired','directors','overview','rating','writers','filename','language' ]
+ data_titles=['Season:','Episode:','Subtitle:','ReleaseDate:','Director:','Plot:','UserRating:','Writers:','Screenshot:','Language:' ]
+ # High level dictionay keys for select graphics URL(s)
+ fanart_key='fanart'
+@@ -472,10 +1061,17 @@ confdir = os.environ.get('MYTHCONFDIR', '')
+ if (not confdir) or (confdir == '/'):
+ confdir = os.environ.get('HOME', '')
+ if (not confdir) or (confdir == '/'):
+- print "Unable to find MythTV directory for metadata cache."
++ print("Unable to find MythTV directory for metadata cache.")
+ sys.exit(1)
+ confdir = os.path.join(confdir, '.mythtv')
+-cache_dir=os.path.join(confdir, "cache/tvdb_api/")
++# different cache dirs due to different pickle protocols
++# TODO massage pickle so python3 generates python2 compatible pickles
++if IS_PY2:
++ cache_dir=os.path.join(confdir, "cache/tvdb_api/")
++else:
++ cache_dir=os.path.join(confdir, "cache/tvdb_api3/")
++if not os.path.exists(cache_dir):
++ os.mkdir(cache_dir)
+
+ def _can_int(x):
+ """Takes a string, checks if it is numeric.
+@@ -492,14 +1088,6 @@ def _can_int(x):
+ return True
+ # end _can_int
+
+-def debuglog(message):
+- message+='\n'
+- target_socket = open(logfile, "a")
+- target_socket.write(message)
+- target_socket.close()
+- return
+-# end debuglog
+-
+ class OutStreamEncoder(object):
+ """Wraps a stream with an encoder"""
+ def __init__(self, outstream, encoding=None):
+@@ -512,15 +1100,18 @@ class OutStreamEncoder(object):
+ def write(self, obj):
+ """Wraps the output stream, encoding Unicode strings with the specified encoding"""
+ if isinstance(obj, unicode):
+- self.out.write(obj.encode(self.encoding))
+- else:
++ obj = obj.encode(self.encoding)
++ if IS_PY2:
+ self.out.write(obj)
++ else:
++ self.out.buffer.write(obj)
+
+ def __getattr__(self, attr):
+ """Delegate everything but write to the stream"""
+ return getattr(self.out, attr)
+-sys.stdout = OutStreamEncoder(sys.stdout, 'utf8')
+-sys.stderr = OutStreamEncoder(sys.stderr, 'utf8')
++if isinstance(sys.stdout, stdio_type):
++ sys.stdout = OutStreamEncoder(sys.stdout, 'utf8')
++ sys.stderr = OutStreamEncoder(sys.stderr, 'utf8')
+
+ # modified Show class implementing a fuzzy search
+ class Show( tvdb_api.Show ):
+@@ -574,11 +1165,12 @@ class Episode( tvdb_api.Episode ):
+
+ # modified Tvdb API class using modified show classes
+ class Tvdb( tvdb_api.Tvdb ):
+- def series_by_sid(self, sid):
++ def series_by_sid(self, sid, language):
+ "Lookup a series via it's sid"
+ seriesid = 'sid:' + sid
+- if not self.corrections.has_key(seriesid):
+- self._getShowData(sid)
++ sid = int(sid)
++ if not seriesid in self.corrections:
++ self._getShowData(sid, language=language)
+ self.corrections[seriesid] = sid
+ return self.shows[sid]
+ #end series_by_sid
+@@ -603,10 +1195,10 @@ class Tvdb( tvdb_api.Tvdb ):
+ #end Tvdb
+
+ # Search for a series by SID or Series name
+-def search_for_series(tvdb, sid_or_name):
++def search_for_series(tvdb, sid_or_name, language):
+ "Get series data by sid or series name of the Tv show"
+ if SID == True:
+- return tvdb.series_by_sid(sid_or_name)
++ return tvdb.series_by_sid(sid_or_name, language)
+ else:
+ return tvdb[sid_or_name]
+ # end search_for_series
+@@ -615,19 +1207,21 @@ def search_for_series(tvdb, sid_or_name):
+ def searchseries(t, opts, series_season_ep):
+ global SID
+ series_name=''
+- if opts.configure != "" and override.has_key(series_season_ep[0].lower()):
+- series_name=override[series_season_ep[0].lower()][0] # Override series name
++ key = series_season_ep[0].lower()
++ if opts.configure != "" and key in override:
++ series_name=override[key][0] # Override series name
+ else:
+ series_name=series_season_ep[0] # Leave the series name alone
+ try:
+ # Search for the series or series & season or series & season & episode
++ series_data = search_for_series(t, series_name, opts.language)
+ if len(series_season_ep)>1:
+ if len(series_season_ep)>2: # series & season & episode
+- seriesfound=search_for_series(t, series_name)[ int(series_season_ep[1]) ][ int(series_season_ep[2]) ]
++ seriesfound=series_data[ int(series_season_ep[1]) ][ int(series_season_ep[2]) ]
+ else:
+- seriesfound=search_for_series(t, series_name)[ int(series_season_ep[1]) ] # series & season
++ seriesfound=series_data[ int(series_season_ep[1]) ] # series & season
+ else:
+- seriesfound=search_for_series(t, series_name) # Series only
++ seriesfound=series_data # Series only
+ except tvdb_shownotfound:
+ # No such show found.
+ # Use the show-name from the files name, and None as the ep name
+@@ -636,25 +1230,25 @@ def searchseries(t, opts, series_season_ep):
+ # The season, episode or name wasn't found, but the show was.
+ # Use the corrected show-name, but no episode name.
+ sys.exit(0)
+- except tvdb_error, errormsg:
++ except tvdb_error as errormsg:
+ # Error communicating with thetvdb.com
+ if SID == True: # Maybe the digits were a series name (e.g. 90210)
+ SID = False
+ return searchseries(t, opts, series_season_ep)
+ sys.exit(0)
+- except tvdb_userabort, errormsg:
++ except tvdb_userabort as errormsg:
+ # User aborted selection (q or ^c)
+- print "\n", errormsg
++ print("\n", errormsg)
+ sys.exit(0)
+ else:
+ if opts.raw==True:
+- print "="*20
+- print "Raw Series Data:\n"
++ print("="*20)
++ print("Raw Series Data:\n")
+ if len(series_season_ep)>1:
+- print t[ series_name ][ int(series_season_ep[1]) ]
++ print(t[ series_name ][ int(series_season_ep[1]) ])
+ else:
+- print t[ series_name ]
+- print "="*20
++ print(t[ series_name ])
++ print("="*20)
+ return(seriesfound)
+ # end searchseries
+
+@@ -663,15 +1257,28 @@ def get_graphics(t, opts, series_season_ep, graphics_type, single_option, langua
+ banners='_banners'
+ series_name=''
+ graphics=[]
+- if opts.configure != "" and override.has_key(series_season_ep[0].lower()):
+- series_name=override[series_season_ep[0].lower()][0] # Override series name
++ key = series_season_ep[0].lower()
++ if opts.configure != "" and key in override:
++ series_name=override[key][0] # Override series name
+ else:
+ series_name=series_season_ep[0] # Leave the series name alone
+
+ if SID == True:
+- URLs = t.ttvdb_parseBanners(series_name)
++ t._parseBanners(series_name)
+ else:
+- URLs = t.ttvdb_parseBanners(t._nameToSid(series_name))
++ t._parseBanners(t._nameToSid(series_name))
++ bid_order = {'fanart': [], 'poster': [], 'series': [], 'season': [], 'seasonwide': []}
++ URLs = {'fanart': [], 'poster': [], 'series': [], 'season': [], 'seasonwide': []}
++
++ # get the urls in presented order
++ for key in t.shows.keys():
++ banner = t.shows[key].data['_banners']
++ for graphic_type_items in bid_order.keys():
++ if graphic_type_items in banner:
++ for graphic_item in banner[graphic_type_items]['raw']:
++ url = banner[graphic_type_items][graphic_item['resolution']][graphic_item['id']]
++ url['rating'] = graphic_item['ratingsInfo']['average']
++ URLs[graphic_type_items].append(url)
+
+ if graphics_type == fanart_type: # Series fanart graphics
+ if not len(URLs[u'fanart']):
+@@ -692,16 +1299,18 @@ def get_graphics(t, opts, series_season_ep, graphics_type, single_option, langua
+ return []
+ if graphics_type == banner_type: # Season Banners
+ season_banners=[]
+- for url in URLs[u'season']:
+- if url[u'bannertype2'] == u'seasonwide' and url[u'season'] == series_season_ep[1]:
++ # seasonwide has blank resolution
++ for url in URLs[u'seasonwide']:
++ if url[u'resolution'] == u'' and url[u'subKey'] == series_season_ep[1]:
+ season_banners.append(url)
+ if not len(season_banners):
+ return []
+ graphics = season_banners
+ else: # Season Posters
++ # season has blank resolution
+ season_posters=[]
+ for url in URLs[u'season']:
+- if url[u'bannertype2'] == u'season' and url[u'season'] == series_season_ep[1]:
++ if url[u'resolution'] == u'' and url[u'subKey'] == series_season_ep[1]:
+ season_posters.append(url)
+ if not len(season_posters):
+ return []
+@@ -715,11 +1324,12 @@ def get_graphics(t, opts, series_season_ep, graphics_type, single_option, langua
+ wasanythingadded = 0
+ anyotherlanguagegraphics=[]
+ englishlanguagegraphics=[]
++ graphics = sorted(graphics, key=lambda k: k['rating'], reverse=True)
+ for URL in graphics:
+ if graphics_type == 'filename':
+ if URL[graphics_type] == None:
+ continue
+- if language: # Is there a language to filter URLs on?
++ if language and 'language' in URL: # Is there a language to filter URLs on?
+ if language == URL['language']:
+ graphicsURLs.append((URL['_bannerpath']).replace(http_find, http_replace))
+ else: # Check for fall back graphics in case there are no selected language graphics
+@@ -740,7 +1350,7 @@ def get_graphics(t, opts, series_season_ep, graphics_type, single_option, langua
+ graphicsURLs = anyotherlanguagegraphics
+
+ if opts.debug == True:
+- print u"\nGraphics:\n", graphicsURLs
++ print(u"\nGraphics:\n", graphicsURLs)
+
+ if len(graphicsURLs) == 1 and graphicsURLs[0] == graphics_type+':':
+ return [] # Due to the language filter there may not be any URLs
+@@ -764,8 +1374,11 @@ def change_to_commas(meta_data):
+ # Change & values to ascii equivalents
+ def change_amp(text):
+ if not text: return text
+- text = text.replace(""", "'").replace("\r\n", " ")
+- text = text.replace(r"\'", "'")
++ try:
++ text = text.replace(""", "'").replace("\r\n", " ")
++ text = text.replace(r"\'", "'")
++ except Exception as e:
++ pass
+ return text
+ # end change_amp
+
+@@ -789,26 +1402,30 @@ def Getseries_episode_data(t, opts, series_season_ep, language = None):
+
+ args = len(series_season_ep)
+ series_name=''
+- if opts.configure != "" and override.has_key(series_season_ep[0].lower()):
+- series_name=override[series_season_ep[0].lower()][0] # Override series name
++ key = series_season_ep[0].lower()
++ if opts.configure != "" and key in override:
++ series_name=override[key][0] # Override series name
+ else:
+ series_name=series_season_ep[0] # Leave the series name alone
+
+ # Get Cast members
+ cast_members=''
++ tmp_cast = ''
+ try:
+- tmp_cast = search_for_series(t, series_name)['_actors']
++ series_data = search_for_series(t, series_name, opts.language)
++ tmp_cast = series_data['_actors']
++ tmp_cast = sorted(tmp_cast, key=lambda k: k['sortOrder'])
+ except:
+ cast_members=''
+ if len(tmp_cast):
+- cast_members=''
++ cast_members=''.encode('utf8')
+ for cast in tmp_cast:
+ if cast['name']:
+- cast_members+=(cast['name']+u', ').encode('utf8')
++ cast_members+=(cast['name']+', ').encode('utf8')
+ if cast_members != '':
+ try:
+ cast_members = cast_members[:-2].encode('utf8')
+- except UnicodeDecodeError:
++ except (UnicodeDecodeError, AttributeError):
+ cast_members = unicode(cast_members[:-2],'utf8')
+ cast_members = change_amp(cast_members)
+ cast_members = change_to_commas(cast_members)
+@@ -817,43 +1434,56 @@ def Getseries_episode_data(t, opts, series_season_ep, language = None):
+ # Get genre(s)
+ genres=''
+ try:
+- genres_string = search_for_series(t, series_name)[u'genre'].encode('utf-8')
++ genres_string = series_data[u'genre'].encode('utf-8')
+ except:
+ genres_string=''
+ if genres_string != None and genres_string != '':
+ genres = change_amp(genres_string)
+ genres = change_to_commas(genres)
+
+- seasons=search_for_series(t, series_name).keys() # Get the seasons for this series
++ seasons=sorted(series_data.keys()) # Get the seasons for this series
+ for season in seasons:
+ if args > 1: # If a season was specified skip other seasons
+ if season != int(series_season_ep[1]):
+ continue
+- episodes=search_for_series(t, series_name)[season].keys() # Get the episodes for this season
++ episodes=sorted(series_data[season].keys()) # Get the episodes for this season
+ for episode in episodes: # If an episode was specified skip other episodes
+ if args > 2:
+ if episode != int(series_season_ep[2]):
+ continue
++ # get more detailed episode info
++ t.getDetailedEpisodeInfo(int(series_data['id']), season, episode)
+ extra_ep_data=[]
+- available_keys=search_for_series(t, series_name)[season][episode].keys()
++ available_keys=series_data[season][episode].keys()
+ if screenshot_request:
+ if u'filename' in available_keys:
+- screenshot = search_for_series(t, series_name)[season][episode][u'filename']
++ screenshot = series_data[season][episode][u'filename']
+ if screenshot:
+- print screenshot.replace(http_find, http_replace)
++ print(screenshot.replace(http_find, http_replace))
+ return
+ else:
+ return
++ # key ordering is not sorted so that Title is first for existing client
++ # compatability
+ key_values=[]
+ for values in data_keys: # Initialize an array for each possible data element for
+ key_values.append('') # each episode within a season
+- for key in available_keys:
++ for key in sorted(available_keys):
+ try:
++ # skip deprecated keys
++ if key in ['director']:
++ continue
+ i = data_keys.index(key) # Include only specific episode data
+ except ValueError:
+- if search_for_series(t, series_name)[season][episode][key] != None:
+- text = search_for_series(t, series_name)[season][episode][key]
+- text = change_amp(text)
++ if series_data[season][episode][key] != None:
++ text = series_data[season][episode][key]
++ if isinstance(text, dict):
++ # handle language tuple
++ text = list(text.values())[0]
++ elif isinstance(text, list):
++ # handle guest stars lists
++ text = ', '.join(text)
++ text = change_amp(unicode(text))
+ text = change_to_commas(text)
+ if text == 'None' and key.title() == 'Director':
+ text = u"Unknown"
+@@ -862,34 +1492,42 @@ def Getseries_episode_data(t, opts, series_season_ep, language = None):
+ except UnicodeDecodeError:
+ extra_ep_data.append(u"%s:%s" % (key.title(), unicode(text, "utf8")))
+ continue
+- text = search_for_series(t, series_name)[season][episode][key]
++ text = series_data[season][episode][key]
+
+ if text == None and key.title() == 'Director':
+ text = u"Unknown"
++ if isinstance(text, list):
++ text = ', '.join(text)
+ if text == None or text == 'None':
+ continue
+ else:
+- text = change_amp(text)
++ # handle language tuple
++ if isinstance(text, dict):
++ # handle language tuple
++ text = list(text.values())[0]
++ text = change_amp(unicode(text))
+ value = change_to_commas(text)
+ value = value.replace(u'\n', u' ')
+ key_values[i]=value
+ index = 0
+ if SID == False:
+- print u"Title:%s" % series_name # Ouput the full series name
++ print(u"Title:%s" % series_name) # Ouput the full series name
+ else:
+- print u"Title:%s" % search_for_series(t, series_name)[u'seriesname']
++ print(u"Title:%s" % series_data[u'seriesname'])
+
+ for key in data_titles:
+ if key_values[index] != None:
+ if data_titles[index] == u'ReleaseDate:' and len(key_values[index]) > 4:
+- print u'%s%s'% (u'Year:', key_values[index][:4])
++ print(u'%s%s'% (u'Year:', key_values[index][:4]))
+ if key_values[index] != 'None':
+- print u'%s%s' % (data_titles[index], key_values[index])
++ print(u'%s%s' % (data_titles[index], key_values[index]))
+ index+=1
+ cast_print=False
+ for extra_data in extra_ep_data:
+ if extra_data[:extra_data.index(':')] == u'Gueststars':
+ extra_cast = extra_data[extra_data.index(':')+1:]
++ if not extra_cast:
++ continue
+ if (len(extra_cast)>128) and not extra_cast.count(','):
+ continue
+ if cast_members:
+@@ -897,19 +1535,23 @@ def Getseries_episode_data(t, opts, series_season_ep, language = None):
+ else:
+ extra_data=u"Cast:%s" % extra_cast
+ cast_print=True
+- print extra_data
++ print(extra_data)
+ if cast_print == False:
+- print u"Cast:%s" % cast_members
++ print(u"Cast:%s" % cast_members)
+ if genres != '':
+- print u"Genres:%s" % genres
+- print u"Runtime:%s" % search_for_series(t, series_name)[u'runtime']
++ print(u"Genres:%s" % genres)
++ print(u"Runtime:%s" % series_data[u'runtime'])
+
+ # URL to TVDB web site episode web page for this series
+ for url_data in [u'seriesid', u'seasonid', u'id']:
+ if not url_data in available_keys:
+ break
+ else:
+- print u'URL:http://www.thetvdb.com/?tab=episode&seriesid=%s&seasonid=%s&id=%s' % (search_for_series(t, series_name)[season][episode][u'seriesid'], search_for_series(t, series_name)[season][episode][u'seasonid'],search_for_series(t, series_name)[season][episode][u'id'])
++ results = series_data
++ print(u'URL:http://www.thetvdb.com/?tab=episode&seriesid=%s&seasonid=%s&id=%s' %
++ (results[season][episode][u'seriesid'],
++ results[season][episode][u'seasonid'],
++ results[season][episode][u'id']))
+ # end Getseries_episode_data
+
+ # Get Series Season and Episode numbers
+@@ -926,8 +1568,9 @@ def Getseries_episode_numbers(t, opts, series_season_ep):
+ global xmlFlag
+ series_name=''
+ ep_name=''
+- if opts.configure != "" and override.has_key(series_season_ep[0].lower()):
+- series_name=override[series_season_ep[0].lower()][0] # Override series name
++ key = series_season_ep[0].lower()
++ if opts.configure != "" and key in override:
++ series_name=override[key][0] # Override series name
+ ep_name=series_season_ep[1]
+ if len(override[series_season_ep[0].lower()][1]) != 0: # Are there search-replace strings?
+ ep_name=massageEpisode_name(ep_name, series_season_ep)
+@@ -935,14 +1578,19 @@ def Getseries_episode_numbers(t, opts, series_season_ep):
+ series_name=series_season_ep[0] # Leave the series name alone
+ ep_name=series_season_ep[1] # Leave the episode name alone
+
+- season_ep_num=search_for_series(t, series_name).fuzzysearch(ep_name, 'episodename')
++ series = search_for_series(t, series_name, opts.language)
++ season_ep_num = series.fuzzysearch(ep_name, 'episodename')
+ if len(season_ep_num) != 0:
+ for episode in sorted(season_ep_num, key=lambda ep: _episode_sort(ep), reverse=True):
+ # if episode.distance == 0: # exact match
+ if xmlFlag:
++ # get more detailed episode info
++ t.getDetailedEpisodeInfo(series['id'], episode['airedSeason'], episode)
++ convert_series_to_xml(t, series_season_ep, season_ep_num)
+ displaySeriesXML(t, [series_name, episode['seasonnumber'], episode['episodenumber']])
+- sys.exit(0)
+- print season_and_episode_num.replace('\\n', '\n') % (int(episode['seasonnumber']), int(episode['episodenumber']))
++ return 0
++ print(season_and_episode_num.replace('\\n', '\n') %
++ (int(episode['seasonnumber']), int(episode['episodenumber'])))
+ # elif (episode['episodename'].lower()).startswith(ep_name.lower()):
+ # if len(episode['episodename']) > (len(ep_name)+1):
+ # if episode['episodename'][len(ep_name):len(ep_name)+2] != ' (':
+@@ -950,12 +1598,12 @@ def Getseries_episode_numbers(t, opts, series_season_ep):
+ # if xmlFlag:
+ # displaySeriesXML(t, [series_name, episode['seasonnumber'], episode['episodenumber']])
+ # sys.exit(0)
+-# print season_and_episode_num.replace('\\n', '\n') % (int(episode['seasonnumber']), int(episode['episodenumber']))
++# print(season_and_episode_num.replace('\\n', '\n') % (int(episode['seasonnumber']), int(episode['episodenumber'])))
+ # end Getseries_episode_numbers
+
+ # Set up a custom interface to get all series matching a partial series name
+-class returnAllSeriesUI(tvdb_ui.BaseUI):
+- def __init__(self, config, log):
++class returnAllSeriesUI(tvdb_api.BaseUI):
++ def __init__(self, config, log=None):
+ self.config = config
+ self.log = log
+
+@@ -963,7 +1611,7 @@ class returnAllSeriesUI(tvdb_ui.BaseUI):
+ return allSeries
+ # ends returnAllSeriesUI
+
+-def initialize_override_dictionary(useroptions):
++def initialize_override_dictionary(useroptions, language):
+ """ Change variables through a user supplied configuration file
+ return False and exit the script if there are issues with the configuration file values
+ """
+@@ -1001,7 +1649,15 @@ def initialize_override_dictionary(useroptions):
+ if section =='series_name_override':
+ for option in cfg.options(section):
+ overrides[option] = cfg.get(section, option)
+- tvdb = Tvdb(banners=False, debug = False, interactive = False, cache = cache_dir, custom_ui=returnAllSeriesUI, apikey="0BB856A59C51D607") # thetvdb.com API key requested by MythTV
++ tvdb = Tvdb(banners=False,
++ debug = False,
++ interactive = False,
++ cache = cache_dir,
++ custom_ui=returnAllSeriesUI,
++ apikey=tvdb_account.apikey, # thetvdb.com API key requested by MythTV
++ username=tvdb_account.username,
++ userkey=tvdb_account.account_identifier)
++ tvdb.session.verify = False
+ for key in overrides.keys():
+ sid = overrides[key]
+ if len(sid) == 0:
+@@ -1014,28 +1670,88 @@ def initialize_override_dictionary(useroptions):
+ # Make sure that the series name is not empty or all blanks
+ if len(key.replace(' ','')) == 0:
+ sys.stdout.write("! Invalid Series name (must have some non-blank characters) [%s] in config file\n" % key)
+- print parts
++ print(overrides.keys())
+ sys.exit(1)
+
+ try:
+- series_name_sid=tvdb.series_by_sid(sid)
++ series_name_sid=tvdb.series_by_sid(sid, language)
+ except:
+ sys.stdout.write("! Invalid Series (no matches found in thetvdb,com) (%s) sid (%s) in config file\n" % (key, sid))
+ sys.exit(1)
+- overrides[key]=series_name_sid[u'seriesname'].encode('utf-8')
++ overrides[key]=unicode(series_name_sid.data[u'seriesName']) #.encode('utf-8')
+ continue
+
+ for key in overrides.keys():
+ override[key] = [overrides[key],[]]
+
+ for key in massage.keys():
+- if override.has_key(key):
++ if key in override:
+ override[key][1]=massage[key]
+ else:
+ override[key]=[key, massage[key]]
+ return
+ # END initialize_override_dictionary
+
++def convert_search_to_xml(t, allSeries):
++ """
++ Convert json to xml and set up tvdb_api object as other stuff expects
++ :param t: tvdb_api object
++ :param allSeries: json array of series
++ :return: xml version of allseries
++ """
++ # Initialize XML display value to off
++ t.xml = False
++ def series_item_func(parent):
++ if parent == "root":
++ return "series"
++ return "alias"
++ xml = dicttoxml(allSeries, item_func=series_item_func, attr_type=False)
++ t.searchTree = eTree.XML(xml)
++ t.seriesInfoTree = None
++ t.epInfoTree = None
++ t.actorsInfoTree = None
++ t.imagesInfoTree = None
++ t.baseXsltDir = xslt.baseXsltPath
++
++def convert_series_to_xml(t, series_season_ep, ep_info):
++ """
++ Convert json to xml and set up tvdb_api object as other stuff expects
++ :param t: tvdb_api object
++ :param ep_info: json array of series
++ """
++ # Initialize XML display value to off
++ t.xml = False
++ def series_ep_item_func(parent):
++ if parent == "data":
++ return "series"
++ if parent == "_banners_raw":
++ return "banner"
++ if parent == "_actors":
++ return "actor"
++ return "item"
++ def series_people_item_func(parent):
++ if parent == "Actors":
++ return "Actor"
++ return "item"
++ def series_images_item_func(parent):
++ if parent == "root":
++ return "images"
++ return "Banner"
++ for show_id in t.shows.keys():
++ break
++ # sort the cast into sort order
++ t.shows[show_id].data['_actors'] = sorted(t.shows[show_id].data['_actors'], key=lambda k: k['sortOrder'])
++ t.searchTree = None
++ t.seriesInfoTree = None
++ t.epInfoTree = None
++ t.actorsInfoTree = None
++ t.imagesInfoTree = None
++ sxml = dicttoxml(t.shows[show_id].data, custom_root='series', item_func=series_ep_item_func, attr_type=False)
++ exml = dicttoxml(t.shows[show_id], custom_root='data', item_func=series_ep_item_func, attr_type=False)
++ t.seriesInfoTree = eTree.XML(exml)
++ t.seriesInfoTree.append(eTree.XML(sxml))
++ t.baseXsltDir = xslt.baseXsltPath
++
+ def initializeXslt(language):
+ ''' Initalize all data and functions for XSLT stylesheet processing
+ return nothing
+@@ -1043,13 +1759,14 @@ def initializeXslt(language):
+ global xslt, tvdbXpath
+ try:
+ import MythTV.ttvdb.tvdbXslt as tvdbXslt
+- except Exception, errmsg:
++ except Exception as errmsg:
+ sys.stderr.write('! Error: Importing tvdbXslt error(%s)\n' % errmsg)
+ sys.exit(1)
+
+ xslt = tvdbXslt.xpathFunctions()
+ xslt.language = language
+ xslt.buildFuncDict()
++ xslt.baseXsltPath = tvdbXslt.baseXsltDir
+ tvdbXpath = etree.FunctionNamespace('http://www.mythtv.org/wiki/MythTV_Universal_Metadata_Format')
+ tvdbXpath.prefix = 'tvdbXpath'
+ for key in xslt.FuncDict.keys():
+@@ -1079,7 +1796,7 @@ def displaySearchXML(tvdb_api):
+ if items.getroot() != None:
+ if len(items.xpath('//item')):
+ sys.stdout.write(etree.tostring(items, encoding='UTF-8', method="xml", xml_declaration=True, pretty_print=True, ))
+- sys.exit(0)
++ return 0
+ # end displaySearchXML()
+
+ def displaySeriesXML(tvdb_api, series_season_ep):
+@@ -1097,25 +1814,14 @@ def displaySeriesXML(tvdb_api, series_season_ep):
+ allDataElement.append(requestDetails)
+
+ # Combine the various XML inputs into a single XML element and send to the XSLT stylesheet
+- if tvdb_api.epInfoTree != None:
+- allDataElement.append(tvdb_api.epInfoTree)
+- else:
+- sys.exit(0)
+- if tvdb_api.actorsInfoTree != None:
+- allDataElement.append(tvdb_api.actorsInfoTree)
+- else:
+- allDataElement.append(etree.XML(u'<Actors></Actors>'))
+- if tvdb_api.imagesInfoTree != None:
+- allDataElement.append(tvdb_api.imagesInfoTree)
+- else:
+- allDataElement.append(etree.XML(u'<Banners></Banners>'))
++ allDataElement.append(tvdb_api.seriesInfoTree)
+
+ tvdbQueryXslt = etree.XSLT(etree.parse(u'%s%s' % (tvdb_api.baseXsltDir, u'tvdbVideo.xsl')))
+ items = tvdbQueryXslt(allDataElement)
+ if items.getroot() != None:
+ if len(items.xpath('//item')):
+ sys.stdout.write(etree.tostring(items, encoding='UTF-8', method="xml", xml_declaration=True, pretty_print=True, ))
+- sys.exit(0)
++ return 0
+ # end displaySeriesXML()
+
+ def displayCollectionXML(tvdb_api):
+@@ -1140,12 +1846,48 @@ def displayCollectionXML(tvdb_api):
+ if items.getroot() != None:
+ if len(items.xpath('//item')):
+ sys.stdout.write(etree.tostring(items, encoding='UTF-8', method="xml", xml_declaration=True, pretty_print=True, ))
+- sys.exit(0)
++ return 0
+ # end displayCollectionXML()
+
++
++def doc_test(opts):
++ import doctest
++
++ if not IS_PY2:
++ # python 3 doctest capture when teh output is utf8 (bytes)
++ # convert it back to str
++ if isinstance(sys.stdout, OutStreamEncoder):
++ sys.stdout = sys.stdout.out
++ sys.stderr = sys.stderr.out
++
++ class SpoofDocTestWriter(doctest._SpoofOut):
++ """Wraps a stream with an decoder"""
++ def __init__(self, *cwargs, **kwargs):
++ super(SpoofDocTestWriter, self).__init__(*cwargs, **kwargs)
++
++ def write(self, obj):
++ """Wraps the output stream, encoding Unicode strings with the specified encoding"""
++ if isinstance(obj, bytes):
++ obj = obj.decode('utf-8')
++ return super(SpoofDocTestWriter, self).write(obj)
++
++ def __getattr__(self, attr):
++ """Delegate everything but write to the stream"""
++ return getattr(self.out, attr)
++
++ # replace _SpoofOut with our massager
++ doctest._SpoofOut = SpoofDocTestWriter
++ return doctest.testmod(verbose=opts.debug, optionflags=doctest.ELLIPSIS, )
++
+ def main():
++ global season_and_episode_num, screenshot_request
++ # reset some globals for doctest mode
++ screenshot_request = False
++
+ parser = OptionParser(usage=u"%prog usage: ttvdb -hdruviomMPFBDS [parameters]\n <series name or 'series and season number' or 'series and season number and episode number'>\n\nFor details on using ttvdb with Mythvideo see the ttvdb wiki page at:\nhttp://www.mythtv.org/wiki/Ttvdb.py")
+
++ parser.add_option( "--doctest", action="store_true", default=False, dest="doctest",
++ help=u"Run doctests")
+ parser.add_option( "-d", "--debug", action="store_true", default=False, dest="debug",
+ help=u"Show debugging info")
+ parser.add_option( "-r", "--raw", action="store_true",default=False, dest="raw",
+@@ -1187,19 +1929,22 @@ def main():
+
+ opts, series_season_ep = parser.parse_args()
+
++ if opts.doctest:
++ return doc_test(opts)
+
+ # Test mode, if we've made it here, everything is ok
+ if opts.test:
+- print "Everything appears to be in order"
+- sys.exit(0)
++ print("Everything appears to be in order")
++ return 0
+
+ # Make everything unicode utf8
+- for index in range(len(series_season_ep)):
+- series_season_ep[index] = unicode(series_season_ep[index], 'utf8')
++ if IS_PY2:
++ for index in range(len(series_season_ep)):
++ series_season_ep[index] = unicode(series_season_ep[index], 'utf8')
+
+ if opts.debug == True:
+- print "opts", opts
+- print "\nargs", series_season_ep
++ print("opts", opts)
++ print("\nargs", series_season_ep)
+
+ # Process version command line requests
+ if opts.version == True:
+@@ -1212,34 +1957,33 @@ def main():
+ etree.SubElement(version, "description").text = 'Search and metadata downloads for thetvdb.com'
+ etree.SubElement(version, "version").text = __version__
+ sys.stdout.write(etree.tostring(version, encoding='UTF-8', pretty_print=True))
+- sys.exit(0)
++ return 0
+
+ # Process usage command line requests
+ if opts.usage == True:
+ sys.stdout.write(usage_txt)
+- sys.exit(0)
++ return 0
+
+ if len(series_season_ep) == 0:
+ parser.error("! No series or series season episode supplied")
+- sys.exit(1)
++ return 1
+
+ # Default output format of season and episode numbers
+- global season_and_episode_num, screenshot_request
+ season_and_episode_num='S%02dE%02d' # Format output example "S04E12"
+
+ if opts.numbers == False:
+ if len(series_season_ep) > 1:
+ if not _can_int(series_season_ep[1]):
+ parser.error("! Season is not numeric")
+- sys.exit(1)
++ return 1
+ if len(series_season_ep) > 2:
+ if not _can_int(series_season_ep[2]):
+ parser.error("! Episode is not numeric")
+- sys.exit(1)
++ return 1
+ else:
+ if len(series_season_ep) < 2:
+ parser.error("! An Episode name must be included")
+- sys.exit(1)
++ return 1
+ if len(series_season_ep) == 3:
+ season_and_episode_num = series_season_ep[2] # Override default output format
+
+@@ -1247,27 +1991,27 @@ def main():
+ if len(series_season_ep) > 1:
+ if not _can_int(series_season_ep[1]):
+ parser.error("! Season is not numeric")
+- sys.exit(1)
++ return 1
+ if len(series_season_ep) > 2:
+ if not _can_int(series_season_ep[2]):
+ parser.error("! Episode is not numeric")
+- sys.exit(1)
++ return 1
+ if not len(series_season_ep) > 2:
+ parser.error("! Option (-S), episode screenshot search requires Season and Episode numbers")
+- sys.exit(1)
++ return 1
+ screenshot_request = True
+
+ if opts.debug == True:
+- print series_season_ep
++ print(series_season_ep)
+
+ if opts.debug == True:
+- print "#"*20
+- print "# series_season_ep array(",series_season_ep,")"
++ print("#"*20)
++ print("# series_season_ep array(",series_season_ep,")")
+
+ if opts.debug == True:
+- print "#"*20
+- print "# Starting tvtvb"
+- print "# Processing (%s) Series" % ( series_season_ep[0] )
++ print("#"*20)
++ print("# Starting tvtvb")
++ print("# Processing (%s) Series" % ( series_season_ep[0] ))
+
+ # List of language from http://www.thetvdb.com/api/0629B785CE550C8D/languages.xml
+ # Hard-coded here as it is realtively static, and saves another HTTP request, as
+@@ -1284,18 +2028,44 @@ def main():
+
+ # Access thetvdb.com API with banners (Posters, Fanart, banners, screenshots) data retrieval enabled
+ if opts.list ==True:
+- t = Tvdb(banners=False, debug = opts.debug, cache = cache_dir, custom_ui=returnAllSeriesUI, language = opts.language, apikey="0BB856A59C51D607") # thetvdb.com API key requested by MythTV
++ t = Tvdb(banners=False,
++ debug = opts.debug,
++ cache = cache_dir,
++ custom_ui=returnAllSeriesUI,
++ language = opts.language,
++ apikey=tvdb_account.apikey, # thetvdb.com API key requested by MythTV
++ username=tvdb_account.username,
++ userkey=tvdb_account.account_identifier)
+ if opts.xml:
+ t.xml = True
+ elif opts.interactive == True:
+- t = Tvdb(banners=True, debug=opts.debug, interactive=True, select_first=False, cache=cache_dir, actors = True, language = opts.language, apikey="0BB856A59C51D607") # thetvdb.com API key requested by MythTV
++ t = Tvdb(banners=True,
++ debug=opts.debug,
++ interactive=True,
++ select_first=False,
++ cache=cache_dir,
++ actors = True,
++ language = opts.language,
++ apikey=tvdb_account.apikey, # thetvdb.com API key requested by MythTV
++ username=tvdb_account.username,
++ userkey=tvdb_account.account_identifier)
+ if opts.xml:
+ t.xml = True
+ else:
+- t = Tvdb(banners=True, debug = opts.debug, cache = cache_dir, actors = True, language = opts.language, apikey="0BB856A59C51D607") # thetvdb.com API key requested by MythTV
++ t = Tvdb(banners=True,
++ debug = opts.debug,
++ cache = cache_dir,
++ actors = True,
++ language = opts.language,
++ apikey=tvdb_account.apikey, # thetvdb.com API key requested by MythTV
++ username=tvdb_account.username,
++ userkey=tvdb_account.account_identifier)
+ if opts.xml:
+ t.xml = True
+
++ # disable certificate check
++ t.session.verify = False
++
+ # Determine if there is a SID or a series name to search with
+ global SID
+ SID = False
+@@ -1310,12 +2080,12 @@ def main():
+ pass
+ else:
+ parser.error("! Option (-C), collection requires an inetref number")
+- sys.exit(1)
++ return 1
+
+ if opts.debug == True:
+- print "# ..got tvdb mirrors"
+- print "# Start to process series or series_season_ep"
+- print "#"*20
++ print("# ..got tvdb mirrors")
++ print("# Start to process series or series_season_ep")
++ print("#"*20)
+
+ global override
+ override={} # Initialize series name override dictionary
+@@ -1324,15 +2094,15 @@ def main():
+ if opts.configure[0]=='~':
+ opts.configure=os.path.expanduser("~")+opts.configure[1:]
+ if os.path.exists(opts.configure) == 1: # Do overrides exist?
+- initialize_override_dictionary(opts.configure)
++ initialize_override_dictionary(opts.configure, opts.language)
+ else:
+- debuglog("! The specified override file (%s) does not exist" % opts.configure)
+- sys.exit(1)
++ sys.stderr.write("! The specified override file (%s) does not exist\n" % opts.configure)
++ return 1
+ else: # Check if there is a default configuration file
+ default_config = u"%s/%s" % (os.path.expanduser(u"~"), u".mythtv/ttvdb.conf")
+ if os.path.isfile(default_config):
+ opts.configure = default_config
+- initialize_override_dictionary(opts.configure)
++ initialize_override_dictionary(opts.configure, opts.language)
+
+ if len(override) == 0:
+ opts.configure = False # Turn off the override option as there is nothing to override
+@@ -1351,37 +2121,42 @@ def main():
+ # Fetch a list of matching series names
+ if opts.list ==True:
+ try:
+- if opts.configure != "" and override.has_key(series_season_ep[0].lower()):
+- allSeries = t._getSeries(override[series_season_ep[0].lower()][0])
++ key = series_season_ep[0].lower()
++ if opts.configure != "" and key in override:
++ allSeries = t._getSeries(override[key][0])
+ else:
+ allSeries=t._getSeries(series_season_ep[0])
+ except tvdb_shownotfound:
+- sys.exit(0) # No matching series
+- except Exception, e:
++ return 0 # No matching series
++ except Exception as e:
+ sys.stderr.write("! Error: %s\n" % (e))
+- sys.exit(1) # Most likely a communications error
++ raise
++ return 1 # Most likely a communications error
+ if opts.xml:
++ convert_search_to_xml(t, allSeries)
+ displaySearchXML(t)
+- sys.exit(0)
++ return 0
+ match_list = []
+ for series_name_sid in allSeries: # list search results
+ key_value = u"%s:%s" % (series_name_sid['sid'], series_name_sid['name'])
+ if not key_value in match_list: # Do not add duplicates
+ match_list.append(key_value)
+- print key_value
+- sys.exit(0) # The Series list option (-M) is the only option honoured when used
++ print(key_value)
++ return 0 # The Series list option (-M) is the only option honoured when used
+
+ # Fetch TV series collection information
+ if opts.collection:
+ try:
+- t._getShowData(series_season_ep[0])
++ t._getShowData(series_season_ep[0], opts.language)
+ except tvdb_shownotfound:
+- sys.exit(0) # No matching series
+- except Exception, e:
++ return 0 # No matching series
++ except Exception as e:
+ sys.stderr.write("! Error: %s\n" % (e))
+- sys.exit(1) # Most likely a communications error
++ raise
++ return 1 # Most likely a communications error
++ convert_series_to_xml(t, series_season_ep, None)
+ displayCollectionXML(t)
+- sys.exit(0) # The TV Series collection option (-C) is the only option honoured when used
++ return 0 # The TV Series collection option (-C) is the only option honoured when used
+
+ # Verify that thetvdb.com has the desired series_season_ep.
+ # Exit this module if series_season_ep is not found
+@@ -1396,38 +2171,38 @@ def main():
+ # Return the season numbers for a series
+ if opts.num_seasons == True:
+ season_numbers=''
+- for x in seriesfound.keys():
++ for x in sorted(seriesfound.keys()):
+ season_numbers+='%d,' % x
+- print season_numbers[:-1]
+- sys.exit(0) # Option (-n) is the only option honoured when used
++ print(season_numbers[:-1])
++ return 0 # Option (-n) is the only option honoured when used
+
+ # Dump information accessible for a Series and ONLY first season of episoded data
+ if opts.debug == True:
+- print "#"*20
+- print "# Starting Raw keys call"
+- print "Lvl #1:" # Seasons for series
++ print("#"*20)
++ print("# Starting Raw keys call")
++ print("Lvl #1:") # Seasons for series
+ x = t[series_season_ep[0]].keys()
+- print t[series_season_ep[0]].keys()
+- print "#"*20
+- print "Lvl #2:" # Episodes for each season
++ print(t[series_season_ep[0]].keys())
++ print("#"*20)
++ print("Lvl #2:") # Episodes for each season
+ for y in x:
+- print t[series_season_ep[0]][y].keys()
+- print "#"*20
+- print "Lvl #3:" # Keys for each episode within the 1st season
++ print(t[series_season_ep[0]][y].keys())
++ print("#"*20)
++ print("Lvl #3:") # Keys for each episode within the 1st season
+ z = t[series_season_ep[0]][1].keys()
+ for aa in z:
+- print t[series_season_ep[0]][1][aa].keys()
+- print "#"*20
+- print "Lvl #4:" # Available data for each episode in 1st season
++ print(t[series_season_ep[0]][1][aa].keys())
++ print("#"*20)
++ print("Lvl #4:") # Available data for each episode in 1st season
+ for aa in z:
+ codes = t[series_season_ep[0]][1][aa].keys()
+- print "\n\nStart:"
++ print("\n\nStart:")
+ for c in codes:
+- print "="*50
+- print 'Key Name=('+c+'):'
+- print t[series_season_ep[0]][1][aa][c]
+- print "="*50
+- print "#"*20
++ print("="*50)
++ print('Key Name=('+c+'):')
++ print(t[series_season_ep[0]][1][aa][c])
++ print("="*50)
++ print("#"*20)
+ sys.exit (True)
+
+ if opts.numbers == True: # Fetch and output season and episode numbers
+@@ -1437,33 +2212,38 @@ def main():
+ else:
+ xmlFlag = False
+ Getseries_episode_numbers(t, opts, series_season_ep)
+- sys.exit(0) # The Numbers option (-N) is the only option honoured when used
++ return 0 # The Numbers option (-N) is the only option honoured when used
+
+ if opts.data or screenshot_request: # Fetch and output episode data
+ if opts.mythvideo:
+ if len(series_season_ep) != 3:
+- print u"Season and Episode numbers required."
++ print(u"Season and Episode numbers required.")
+ else:
+ if opts.xml:
++ t.getDetailedEpisodeInfo(seriesfound[u'id'], series_season_ep[1], series_season_ep[2])
++ convert_series_to_xml(t, series_season_ep, seriesfound)
+ displaySeriesXML(t, series_season_ep)
+- sys.exit(0)
++ return 0
+ Getseries_episode_data(t, opts, series_season_ep, language=opts.language)
+ else:
+ if opts.xml and len(series_season_ep) == 3:
++ t.getDetailedEpisodeInfo(list(t.shows.values())[0].data['id'], series_season_ep[1], series_season_ep[2])
++ convert_series_to_xml(t, series_season_ep, seriesfound)
+ displaySeriesXML(t, series_season_ep)
+- sys.exit(0)
++ return 0
+ Getseries_episode_data(t, opts, series_season_ep, language=opts.language)
+
+ # Fetch the requested graphics URL(s)
+ if opts.debug == True:
+- print "#"*20
+- print "# Checking if Posters, Fanart or Banners are available"
+- print "#"*20
++ print("#"*20)
++ print("# Checking if Posters, Fanart or Banners are available")
++ print("#"*20)
+
+- if opts.configure != "" and override.has_key(series_season_ep[0].lower()):
+- banners_keys = search_for_series(t, override[series_season_ep[0].lower()][0])['_banners'].keys()
++ key = series_season_ep[0].lower()
++ if opts.configure != "" and key in override:
++ banners_keys = search_for_series(t, override[key][0], opts.language)['_banners'].keys()
+ else:
+- banners_keys = search_for_series(t, series_season_ep[0])['_banners'].keys()
++ banners_keys = search_for_series(t, series_season_ep[0], opts.language)['_banners'].keys()
+
+ banner= False
+ poster= False
+@@ -1479,12 +2259,12 @@ def main():
+
+ # Make sure that some graphics URL(s) (Posters, FanArt or Banners) are available
+ if ( fanart!=True and poster!=True and banner!=True ):
+- sys.exit(0)
++ return 0
+
+ if opts.debug == True:
+- print "#"*20
+- print "# One or more of Posters, Fanart or Banners are available"
+- print "#"*20
++ print("#"*20)
++ print("# One or more of Posters, Fanart or Banners are available")
++ print("#"*20)
+
+ # Determine if graphic URL identification output is required
+ if opts.data: # Along with episode data get all graphics
+@@ -1507,8 +2287,8 @@ def main():
+ season_poster_found = False
+ if opts.mythvideo:
+ if len(series_season_ep) < 2:
+- print u"Season and Episode numbers required."
+- sys.exit(0)
++ print(u"Season and Episode numbers required.")
++ return 0
+ all_posters = u'Coverart:'
+ all_empty = len(all_posters)
+ for p in get_graphics(t, opts, series_season_ep, poster_type, single_option, opts.language):
+@@ -1516,17 +2296,18 @@ def main():
+ season_poster_found = True
+ if season_poster_found == False: # If there were no season posters get the series top poster
+ series_name=''
+- if opts.configure != "" and override.has_key(series_season_ep[0].lower()):
+- series_name=override[series_season_ep[0].lower()][0] # Override series name
++ key = series_season_ep[0].lower()
++ if opts.configure != "" and key in override:
++ series_name=override[key][0] # Override series name
+ else:
+ series_name=series_season_ep[0] # Leave the series name alone
+ for p in get_graphics(t, opts, [series_name], poster_type, single_option, opts.language):
+ all_posters = all_posters+p+u','
+ if len(all_posters) > all_empty:
+ if all_posters[-1] == u',':
+- print all_posters[:-1]
++ print(all_posters[:-1])
+ else:
+- print all_posters
++ print(all_posters)
+
+ if (fanart==True and opts.fanart==True and opts.raw!=True): # Get Fan Art and send to stdout
+ all_fanart = u'Fanart:'
+@@ -1535,16 +2316,16 @@ def main():
+ all_fanart = all_fanart+f+u','
+ if len(all_fanart) > all_empty:
+ if all_fanart[-1] == u',':
+- print all_fanart[:-1]
++ print(all_fanart[:-1])
+ else:
+- print all_fanart
++ print(all_fanart)
+
+ if (banner==True and opts.banner==True and opts.raw!=True): # Also change to get ALL Series graphics
+ season_banner_found = False
+ if opts.mythvideo:
+ if len(series_season_ep) < 2:
+- print u"Season and Episode numbers required."
+- sys.exit(0)
++ print(u"Season and Episode numbers required.")
++ return 0
+ all_banners = u'Banner:'
+ all_empty = len(all_banners)
+ for b in get_graphics(t, opts, series_season_ep, banner_type, single_option, opts.language):
+@@ -1552,24 +2333,25 @@ def main():
+ season_banner_found = True
+ if not season_banner_found: # If there were no season banner get the series top banner
+ series_name=''
+- if opts.configure != "" and override.has_key(series_season_ep[0].lower()):
+- series_name=override[series_season_ep[0].lower()][0] # Override series name
++ key = series_season_ep[0].lower()
++ if opts.configure != "" and key in override:
++ series_name=override[key][0] # Override series name
+ else:
+ series_name=series_season_ep[0] # Leave the series name alone
+ for b in get_graphics(t, opts, [series_name], banner_type, single_option, opts.language):
+ all_banners = all_banners+b+u','
+ if len(all_banners) > all_empty:
+ if all_banners[-1] == u',':
+- print all_banners[:-1]
++ print(all_banners[:-1])
+ else:
+- print all_banners
++ print(all_banners)
+
+ if opts.debug == True:
+- print "#"*20
+- print "# Processing complete"
+- print "#"*20
+- sys.exit(0)
++ print("#"*20)
++ print("# Processing complete")
++ print("#"*20)
++ return 0
+ #end main
+
+ if __name__ == "__main__":
+- main()
++ sys.exit(main())
+diff --git a/mythtv/programs/scripts/metadata/Television/tvdb_test.conf b/mythtv/programs/scripts/metadata/Television/tvdb_test.conf
+new file mode 100644
+index 0000000000..709b8e7f30
+--- /dev/null
++++ b/mythtv/programs/scripts/metadata/Television/tvdb_test.conf
+@@ -0,0 +1,7 @@
++#
++[series_name_override]
++# Specify recorded "Life On Mars" shows as the US version
++# Specify recorded "Eleventh Hour" shows as the US version
++Eleventh Hour:83066
++# For overnight episode updates when a filename is used
++Eleventh Hour (US):83066
diff --git a/mythtv.spec b/mythtv.spec
index a50428e..4de338d 100644
--- a/mythtv.spec
+++ b/mythtv.spec
@@ -60,7 +60,7 @@
%define desktop_vendor RPMFusion
# MythTV Version string -- preferably the output from git describe
-%define vers_string v0.28.1-41-g2c4c711b1f
+%define vers_string v0.28.1-45-g73cf7474ad
%define branch fixes/0.28
# Git revision and branch ID
@@ -305,6 +305,7 @@ BuildRequires: zlib-devel
BuildRequires: ncurses-devel
+
%if %{with mythweather}
Requires: mythweather >= %{version}
BuildRequires: perl(XML::Simple)
@@ -1357,8 +1358,8 @@ exit 0
%changelog
-* Sat Aug 19 2017 Richard Shaw <hobbes1069(a)gmail.com> - 0.28.1-7
-- Update to latest fixes/0/28, v0.28.1-41-g2c4c711b1f.
+* Wed Sep 6 2017 Richard Shaw <hobbes1069(a)gmail.com> - 0.28.1-7
+- Update to latest fixes/0/28, v0.28.1-45-g73cf7474ad.
* Sun Aug 6 2017 Richard Shaw <hobbes1069(a)gmail.com> - 0.28.1-6
- Update to latest fixes/0.28, v0.28.1-38-geef6a48.
7 years, 2 months
[VirtualBox-kmod/el7: 3/3] Tempory disable broken dep of buildsys-build-rpmfusion-kerneldevpkgs-current
by Sérgio M. Basto
commit ecac1f71f486b8cf5d5e3274a65e6245e9c03ab2
Author: Sérgio M. Basto <sergio(a)serjux.com>
Date: Sat Sep 16 12:34:57 2017 +0100
Tempory disable broken dep of buildsys-build-rpmfusion-kerneldevpkgs-current
VirtualBox-kmod.spec | 9 ++++++---
1 file changed, 6 insertions(+), 3 deletions(-)
---
diff --git a/VirtualBox-kmod.spec b/VirtualBox-kmod.spec
index 5102a1c..58e4e0b 100644
--- a/VirtualBox-kmod.spec
+++ b/VirtualBox-kmod.spec
@@ -3,11 +3,11 @@
# "buildforkernels newest" macro for just that build; immediately after
# queuing that build enable the macro again for subsequent builds; that way
# a new akmod package will only get build when a new one is actually needed
-%if 0%{?fedora}
+#if 0%{?fedora}
%global buildforkernels akmod
%global debug_package %{nil}
-%endif
+#endif
#akmods still generate debuginfo but have the wrong name:
#/var/cache/akmods/VirtualBox/VirtualBox-kmod-debuginfo-5.0.4-1.fc21.x86_64.rpm
#/var/cache/akmods/VirtualBox/kmod-VirtualBox-4.1.8-100.fc21.x86_64-5.0.4-1.fc21.x86_64.rpm
@@ -30,7 +30,7 @@
Name: VirtualBox-kmod
Version: 5.1.28
#Release: 1%%{?prerel:.%%{prerel}}%%{?dist}
-Release: 1%{?dist}
+Release: 2%{?dist}
Summary: Kernel module for VirtualBox
Group: System Environment/Kernel
@@ -108,6 +108,9 @@ DIRS=$(ls %{name}-%{version} |wc -l)
%changelog
+* Sat Sep 16 2017 Sérgio Basto <sergio(a)serjux.com> - 5.1.28-2
+- Tempory disable broken dep of buildsys-build-rpmfusion-kerneldevpkgs-current
+
* Fri Sep 15 2017 Sérgio Basto <sergio(a)serjux.com> - 5.1.28-1
- Update VBox to 5.1.28
7 years, 2 months
[VirtualBox/el7] Epel 7 with X 1.19 don't need vboxvideo_drv https://forums.virtualbox.org/viewtopic.php?f=15&t=842
by Sérgio M. Basto
commit 6225cb4bbd0e7a56b6919a555d23aa7032210d8e
Author: Sérgio M. Basto <sergio(a)serjux.com>
Date: Sat Sep 16 05:49:55 2017 +0100
Epel 7 with X 1.19 don't need vboxvideo_drv
https://forums.virtualbox.org/viewtopic.php?f=15&t=84201
VirtualBox.spec | 13 +++++++++----
1 file changed, 9 insertions(+), 4 deletions(-)
---
diff --git a/VirtualBox.spec b/VirtualBox.spec
index d0e100b..0d11dde 100644
--- a/VirtualBox.spec
+++ b/VirtualBox.spec
@@ -22,11 +22,12 @@
%bcond_without docs
%endif
%bcond_with vnc
+%bcond_with vboxvideo_drv
Name: VirtualBox
Version: 5.1.28
#Release: 1%%{?prerel:.%%{prerel}}%%{?dist}
-Release: 1%{?dist}
+Release: 2%{?dist}
Summary: A general-purpose full virtualizer for PC hardware
License: GPLv2 or (GPLv2 and CDDL)
@@ -258,7 +259,7 @@ rm -r src/libs/zlib-1.2.8/
%patch2 -p1 -b .strings
%patch18 -p1 -b .aiobug
%patch23 -p1 -b .xserver_guest
-%if 0%{?fedora}
+%if ! %{with vboxvideo_drv}
%patch24 -p1 -b .xserver_guest_xorg19
%endif
%patch26 -p1 -b .nobundles
@@ -462,7 +463,7 @@ install -p -m 0644 obj/bin/virtualbox.xml %{buildroot}%{_datadir}/mime/packages
#
# [1] https://www.virtualbox.org/changeset/43588/vbox
-%if 0%{?rhel}
+%if %{with vboxvideo_drv}
install -m 0755 -D obj/bin/additions/vboxvideo_drv_system.so \
%{buildroot}%{_libdir}/xorg/modules/drivers/vboxvideo_drv.so
%endif
@@ -737,7 +738,7 @@ getent group vboxsf >/dev/null || groupadd -r vboxsf 2>&1
%{_sbindir}/VBoxService
%{_sbindir}/mount.vboxsf
%{_libdir}/security/pam_vbox.so
-%if 0%{?rhel}
+%if %{with vboxvideo_drv}
# do not use xorg module drive in newer versions
%{_libdir}/xorg/modules/drivers/*
%endif
@@ -753,6 +754,10 @@ getent group vboxsf >/dev/null || groupadd -r vboxsf 2>&1
%{_datadir}/%{name}-kmod-%{version}
%changelog
+* Sat Sep 16 2017 Sérgio Basto <sergio(a)serjux.com> - 5.1.28-2
+- Epel 7 with X 1.19 don't need vboxvideo_drv
+ https://forums.virtualbox.org/viewtopic.php?f=15&t=84201
+
* Thu Sep 14 2017 Sérgio Basto <sergio(a)serjux.com> - 5.1.28-1
- Update VBox to 5.1.28
7 years, 2 months
[mpv/f26] Update to 0.27.0
by Leigh Scott
Summary of changes:
30a9178... Update to 0.27.0 (*)
(*) This commit already existed in another branch; no separate mail sent
7 years, 2 months
[mpv/f27] Update to 0.27.0
by Leigh Scott
Summary of changes:
30a9178... Update to 0.27.0 (*)
(*) This commit already existed in another branch; no separate mail sent
7 years, 2 months
[mpv] Update to 0.27.0
by Leigh Scott
commit 30a9178d46c4b2be8677573a09af9d5b7533813e
Author: leigh123linux <leigh123linux(a)googlemail.com>
Date: Fri Sep 15 13:33:52 2017 +0100
Update to 0.27.0
mpv.spec | 11 +++++++----
sources | 2 +-
2 files changed, 8 insertions(+), 5 deletions(-)
---
diff --git a/mpv.spec b/mpv.spec
index 7bb9718..96cabe2 100644
--- a/mpv.spec
+++ b/mpv.spec
@@ -1,6 +1,6 @@
Name: mpv
-Version: 0.26.0
-Release: 3%{?dist}
+Version: 0.27.0
+Release: 1%{?dist}
Summary: Movie player playing most video formats and DVDs
License: GPLv2+
URL: http://%{name}.io/
@@ -108,6 +108,7 @@ waf configure \
--disable-build-date \
--enable-libmpv-shared \
--enable-sdl2 \
+ --enable-libarchive \
--enable-libsmbclient \
--enable-encoding \
--enable-dvdread \
@@ -126,11 +127,9 @@ desktop-file-validate %{buildroot}%{_datadir}/applications/%{name}.desktop
install -Dpm 644 README.md etc/input.conf etc/mpv.conf -t %{buildroot}%{_docdir}/%{name}
%post
-/usr/bin/update-desktop-database &> /dev/null || :
/bin/touch --no-create %{_datadir}/icons/hicolor &> /dev/null || :
%postun
-/usr/bin/update-desktop-database &> /dev/null || :
if [ $1 -eq 0 ] ; then
/bin/touch --no-create %{_datadir}/icons/hicolor &> /dev/null || :
/usr/bin/gtk-update-icon-cache %{_datadir}/icons/hicolor &> /dev/null || :
@@ -164,6 +163,10 @@ fi
%{_libdir}/pkgconfig/mpv.pc
%changelog
+* Fri Sep 15 2017 Leigh Scott <leigh123linux(a)googlemail.com> - 0.27.0-1
+- Update to 0.27.0
+- Enable libarchive support (play .zip, .iso and other formats)
+
* Fri Aug 11 2017 Leigh Scott <leigh123linux(a)googlemail.com> - 0.26.0-3
- Enable Samba support (rfbz#4624)
- Enable TV and DVB support
diff --git a/sources b/sources
index 86e9352..69fc15b 100644
--- a/sources
+++ b/sources
@@ -1 +1 @@
-038d0b660de07ff645ad6a741704ecab mpv-0.26.0.tar.gz
+ec86f42b091d891f9a932de0f6e873ad mpv-0.27.0.tar.gz
7 years, 2 months