diff -Nru mythtv-0.28.1+fixes.20170818.2c4c711/debian/changelog mythtv-0.28.1+fixes.20170903.73cf747/debian/changelog --- mythtv-0.28.1+fixes.20170818.2c4c711/debian/changelog 2017-08-18 01:06:15.000000000 +0000 +++ mythtv-0.28.1+fixes.20170903.73cf747/debian/changelog 2017-09-03 02:17:39.000000000 +0000 @@ -1,22 +1,22 @@ -mythtv (2:0.28.1+fixes.20170818.2c4c711-0ubuntu0mythbuntu3) vivid; urgency=medium +mythtv (2:0.28.1+fixes.20170903.73cf747-0ubuntu0mythbuntu3) vivid; urgency=medium - * Scripted Build from fixes git packaging [8729945] - * Packaging changes between 20160325 and 20170818: + * Scripted Build from fixes git packaging [4f6bcc5] + * Packaging changes between 20160325 and 20170903: * [dd75c01] use init script to be more compatible to systemd/upstart in backports * [b7c38ab] make sure to set buildroot * [7a4983b] cover one more pro file for mysql5.7 fix * [8d19bc0] let mysql-server-5.7 resolve things * [35b1bba] Add in support to compile against mysql 5.7 (LP: #1528583) - * Automated Build: New upstream checkout (2c4c711) - * >>Upstream changes since last upload (eef6a48): - * [2c4c711] Fix raspberry pi compile eror when using old header files - * [3b18a09] Avoid busy looping in some cases in the scheduler when the - time to check on slaves that can be put to sleep has already passed. - Thanks to lucylangthorne55 for helping to debug this issue. - * [f1a485c] Restore SendAction operation, Fixes #12738 + * Automated Build: New upstream checkout (73cf747) + * >>Upstream changes since last upload (2c4c711): + * [73cf747] Fix OpenGL bug introduced recently + * [ebd69ec] Raspberry Pi: Support for Raspbian Stretch (fixes/0.28) + * [ff9002a] Fix invocation of Previously Recorded from Recording + Rules. + * [204bb2e] ttvdb: Update to support new JSON API. - -- Mythbuntu Automated Package Builder Fri, 18 Aug 2017 01:06:12 +0000 + -- Mythbuntu Automated Package Builder Sun, 03 Sep 2017 02:17:35 +0000 mythtv (2:0.28.0+fixes.20160325.2520617-0ubuntu3) xenial; urgency=medium diff -Nru mythtv-0.28.1+fixes.20170818.2c4c711/debian/control mythtv-0.28.1+fixes.20170903.73cf747/debian/control --- mythtv-0.28.1+fixes.20170818.2c4c711/debian/control 2017-08-18 01:06:15.000000000 +0000 +++ mythtv-0.28.1+fixes.20170903.73cf747/debian/control 2017-09-03 02:17:38.000000000 +0000 @@ -142,6 +142,9 @@ pwgen, pciutils, usbutils, + python-future, + python-requests, + python-requests-cache, ${shlibs:Depends}, ${misc:Depends} Suggests: mythtv-doc diff -Nru mythtv-0.28.1+fixes.20170818.2c4c711/debian/control.in mythtv-0.28.1+fixes.20170903.73cf747/debian/control.in --- mythtv-0.28.1+fixes.20170818.2c4c711/debian/control.in 2017-08-18 01:06:03.000000000 +0000 +++ mythtv-0.28.1+fixes.20170903.73cf747/debian/control.in 2017-09-03 02:17:27.000000000 +0000 @@ -142,6 +142,9 @@ pwgen, pciutils, usbutils, + python-future, + python-requests, + python-requests-cache, ${shlibs:Depends}, ${misc:Depends} Suggests: mythtv-doc diff -Nru mythtv-0.28.1+fixes.20170818.2c4c711/debian/DESCRIBE mythtv-0.28.1+fixes.20170903.73cf747/debian/DESCRIBE --- mythtv-0.28.1+fixes.20170818.2c4c711/debian/DESCRIBE 2017-08-18 01:06:09.000000000 +0000 +++ mythtv-0.28.1+fixes.20170903.73cf747/debian/DESCRIBE 2017-09-03 02:17:32.000000000 +0000 @@ -1,2 +1,2 @@ BRANCH="fixes/0.28" -SOURCE_VERSION="v0.28.1-41-g2c4c711" +SOURCE_VERSION="v0.28.1-45-g73cf747" diff -Nru mythtv-0.28.1+fixes.20170818.2c4c711/debian/PPA-published-git-checker.py mythtv-0.28.1+fixes.20170903.73cf747/debian/PPA-published-git-checker.py --- mythtv-0.28.1+fixes.20170818.2c4c711/debian/PPA-published-git-checker.py 2017-08-18 01:06:03.000000000 +0000 +++ mythtv-0.28.1+fixes.20170903.73cf747/debian/PPA-published-git-checker.py 2017-09-03 02:17:27.000000000 +0000 @@ -1,6 +1,6 @@ #!/usr/bin/env python -import sys +import sys cachedir = "~/.launchpadlib/cache/" @@ -9,20 +9,28 @@ people = launchpad.people -## Which team +## Which team mythbuntugroup = people['mythbuntu'] +version = sys.argv[1] + ## Which PPA? -archive = mythbuntugroup.getPPAByName(name=sys.argv[1]) +archive = mythbuntugroup.getPPAByName(name=version) ## Which source package? package=archive.getPublishedSources(source_name="mythtv") +if version == '0.28': + splitnum = 4 +else: + splitnum = 3 + try: ## Print latest published source package fullversion=package[0].source_package_version ## Pull out GIT hash - hash = fullversion.split('-')[0].split('.')[4] + hash = fullversion.split('-')[0].split('.')[splitnum] except IndexError: hash = '' + print hash diff -Nru mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/altdict.py mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/altdict.py --- mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/altdict.py 2017-08-11 01:44:06.000000000 +0000 +++ mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/altdict.py 2017-08-28 01:11:03.000000000 +0000 @@ -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 @@ 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 @@ def __setstate__(self, state): for k,v in state.iteritems(): self[k] = v - + class DictInvert(dict): """ @@ -204,7 +205,7 @@ 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 -Nru mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/connections.py mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/connections.py --- mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/connections.py 2017-08-11 01:44:06.000000000 +0000 +++ mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/connections.py 2017-08-28 01:11:03.000000000 +0000 @@ -10,22 +10,32 @@ 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 @@ 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 @@ 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 @@ # 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 @@ 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 @@ 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 @@ 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 @@ try: t = time() fe._test(t + 2.0) - except MythError, e: + except MythError as e: continue yield fe @@ -582,7 +592,7 @@ 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 @@ '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 -Nru mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/_conn_mysqldb.py mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/_conn_mysqldb.py --- mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/_conn_mysqldb.py 2017-08-11 01:44:06.000000000 +0000 +++ mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/_conn_mysqldb.py 2017-08-28 01:11:03.000000000 +0000 @@ -44,7 +44,7 @@ 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 @@ 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 @@ 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 -Nru mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/_conn_oursql.py mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/_conn_oursql.py --- mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/_conn_oursql.py 2017-08-11 01:44:06.000000000 +0000 +++ mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/_conn_oursql.py 2017-08-28 01:11:03.000000000 +0000 @@ -54,7 +54,7 @@ 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 @@ 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 -Nru mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/database.py mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/database.py --- mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/database.py 2017-08-18 01:00:46.000000000 +0000 +++ mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/database.py 2017-09-03 02:12:08.000000000 +0000 @@ -8,7 +8,7 @@ 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 time as _pyt import weakref import os +from builtins import int, str class DBData( DictData, MythSchema ): @@ -118,7 +119,7 @@ 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 @@ 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 @@ 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 @@ # 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 @@ # 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 @@ """ 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 @@ x.seconds%60), _pyt.struct_time: lambda x: _pyt.\ strftime('"%Y-%m-%d %H:%M:%S"',x)} - + if args is None: return query diff -Nru mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/dataheap.py mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/dataheap.py --- mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/dataheap.py 2017-08-11 01:44:06.000000000 +0000 +++ mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/dataheap.py 2017-08-28 01:11:03.000000000 +0000 @@ -19,7 +19,14 @@ _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 @@ 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 @@ @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 @@ 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, @@ -315,7 +325,7 @@ class _Rating( DBDataRef ): _table = 'recordedrating' _ref = ['chanid','starttime'] - + def __str__(self): if self._wheredat is None: return u"" % hex(id(self)) @@ -554,7 +564,7 @@ '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 @@ """ _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 @@ """ _table = 'program' _key = ['chanid','starttime'] - + def __str__(self): if self._wheredat is None: return u"" % hex(id(self)) @@ -1145,7 +1155,7 @@ 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 @@ 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 -Nru mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/__init__.py mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/__init__.py --- mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/__init__.py 2017-08-11 01:44:06.000000000 +0000 +++ mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/__init__.py 2017-08-28 01:11:03.000000000 +0000 @@ -29,17 +29,17 @@ +__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 -Nru mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/logging.py mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/logging.py --- mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/logging.py 2017-08-11 01:44:06.000000000 +0000 +++ mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/logging.py 2017-08-28 01:11:03.000000000 +0000 @@ -10,8 +10,14 @@ 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 @@ 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 -Nru mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/methodheap.py mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/methodheap.py --- mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/methodheap.py 2017-08-11 01:44:06.000000000 +0000 +++ mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/methodheap.py 2017-08-28 01:11:03.000000000 +0000 @@ -16,7 +16,11 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 -Nru mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/msearch.py mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/msearch.py --- mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/msearch.py 2017-08-11 01:44:06.000000000 +0000 +++ mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/msearch.py 2017-08-28 01:11:03.000000000 +0000 @@ -25,7 +25,7 @@ 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 -Nru mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/mythproto.py mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/mythproto.py --- mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/mythproto.py 2017-08-11 01:44:06.000000000 +0000 +++ mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/mythproto.py 2017-08-28 01:11:03.000000000 +0000 @@ -16,7 +16,10 @@ 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 -Nru mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/ttvdb/cache.py mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/ttvdb/cache.py --- mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/ttvdb/cache.py 2017-08-11 01:44:06.000000000 +0000 +++ mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/ttvdb/cache.py 1970-01-01 00:00:00.000000000 +0000 @@ -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 -Nru mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/ttvdb/requests_cache_compatability.py mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/ttvdb/requests_cache_compatability.py --- mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/ttvdb/requests_cache_compatability.py 1970-01-01 00:00:00.000000000 +0000 +++ mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/ttvdb/requests_cache_compatability.py 2017-08-28 01:11:03.000000000 +0000 @@ -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 -Nru mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/ttvdb/tvdb_api.py mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/ttvdb/tvdb_api.py --- mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/ttvdb/tvdb_api.py 2017-08-11 01:44:06.000000000 +0000 +++ mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/ttvdb/tvdb_api.py 2017-08-28 01:11:03.000000000 +0000 @@ -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) + + """ + 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 @@ self.data = {} def __repr__(self): - return "" % ( - self.data.get(u'seriesname', 'instance'), + return "" % ( + 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 @@ # 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 @@ containing "my first day": >>> t['Scrubs'].search("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') - [] + >>> t['My Name Is Earl'].search('Faked My Own Death', key='episodeName') + [] >>> To search Scrubs for all episodes with "mentor" in the episode name: - >>> t['scrubs'].search('mentor', key = 'episodename') - [, ] + >>> t['scrubs'].search('mentor', key='episodeName') + [, ] >>> # 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 @@ """ 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 "" % ( len(self.keys()) @@ -168,20 +410,20 @@ 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') - [] + [] >>> 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 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 "" % (seasno, epno, epname) + return "" % (seasno, epno, epname) else: return "" % (seasno, epno) @@ -203,9 +450,31 @@ 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 - def search(self, term = None, key = None): + raise tvdb_attributenotfound("Cannot find attribute %s" % (repr(key))) + + 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 @@ Simple example: >>> e = Episode() - >>> e['episodename'] = "An Example" + >>> e['episodeName'] = "An Example" >>> e.search("examp") - + >>> Limiting by key: - >>> e.search("examp", key = "episodename") - + >>> e.search("examp", key = "episodeName") + >>> """ - 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 @@ sortorder """ def __repr__(self): - return "" % (self.get("name")) + return "" % 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 @@ 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/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. + cache (True/False/str/requests_cache.CachedSession): + + 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 @@ >>> 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 @@ 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" - - 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_epInfo'] = "%(base_url)s/api/%(apikey)s/series/%%s/all/%(language)s.xml" % self.config + self.config['base_url'] = "http://thetvdb.com" + self.config['api_url'] = "https://api.thetvdb.com" - 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_getSeries'] = u"%(api_url)s/search/series?name=%%s" % 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_epInfo'] = u"%(api_url)s/series/%%s/episodes" % self.config + self.config['url_epDetail'] = u"%(api_url)s/episodes/%%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.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'] = 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 + + 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') - - hdlr = logging.StreamHandler(sys.stdout) + 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") + + return os.path.join(tempfile.gettempdir(), "tvdb_api-%s" % (uid)) + + def _loadUrl(self, url, data=None, recache=False, language=None): + """Return response from The TVDB API""" + + 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 - hdlr.setFormatter(formatter) - logger.addHandler(hdlr) + # 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 self.config['debug_enabled']: - logger.setLevel(logging.DEBUG) + if data and isinstance(data, list): + data.extend(r_data) else: - logger.setLevel(logging.WARNING) - return logger - #end initLogger + data = r_data - def _getTempDir(self): - """Returns the [system temp dir]/tvdb_api - """ - return os.path.join(tempfile.gettempdir(), "tvdb_api") + if links and links['next']: + url = url.split('?')[0] + _url = url + "?page=%s" % links['next'] + self._loadUrl(_url, 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 + return data - return resp.read() + 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): + 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 - ) + src = self._loadUrl(url, language=language) - if self.config['cache_enabled']: - errormsg += "\nFirst try emptying the cache folder at..\n%s" % ( - self.config['cache_location'] - ) - - 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 @@ 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 @@ 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 @@ 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 @@ 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' - >>> - - Any key starting with an underscore has been processed (not the raw - data from the XML) - - 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() + 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] = {} + + banners[btype][btype2][bid]['bannerpath'] = banner_info['fileName'] + banners[btype][btype2][bid]['resolution'] = banner_info['resolution'] + banners[btype][btype2][bid]['subKey'] = banner_info['subKey'] + + 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 @@ >>> type(actors[0]) >>> actors[0] - + >>> 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 @@ 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 @@ 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 -Nru mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/ttvdb/tvdb_create_key.py mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/ttvdb/tvdb_create_key.py --- mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/ttvdb/tvdb_create_key.py 1970-01-01 00:00:00.000000000 +0000 +++ mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/ttvdb/tvdb_create_key.py 2017-08-28 01:11:03.000000000 +0000 @@ -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 -Nru mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/ttvdb/tvdb_exceptions.py mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/ttvdb/tvdb_exceptions.py --- mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/ttvdb/tvdb_exceptions.py 2017-08-11 01:44:06.000000000 +0000 +++ mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/ttvdb/tvdb_exceptions.py 2017-08-28 01:11:03.000000000 +0000 @@ -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 - -class tvdb_userabort(Exception): - """User aborted the interactive selection (via - the q command, ^c etc) - """ - pass - -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 +__all__ = ["tvdb_error", "tvdb_userabort", "tvdb_notauthorized", "tvdb_shownotfound", +"tvdb_seasonnotfound", "tvdb_episodenotfound", "tvdb_attributenotfound", +"tvdb_resourcenotfound", "tvdb_invalidlanguage"] + +logging.getLogger(__name__).warning( + "tvdb_exceptions module is deprecated - use classes directly from tvdb_api instead") + +from tvdb_api import ( + tvdb_error, tvdb_userabort, tvdb_notauthorized, tvdb_shownotfound, + tvdb_seasonnotfound, tvdb_episodenotfound, + tvdb_resourcenotfound, tvdb_invalidlanguage, + tvdb_attributenotfound +) diff -Nru mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/ttvdb/tvdb_ui.py mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/ttvdb/tvdb_ui.py --- mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/ttvdb/tvdb_ui.py 2017-08-11 01:44:06.000000000 +0000 +++ mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/ttvdb/tvdb_ui.py 2017-08-28 01:11:03.000000000 +0000 @@ -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) - -""" __author__ = "dbr/Ben" -__version__ = "1.2.1" +__version__ = "2.0-dev" + +import sys +import logging +import warnings from tvdb_exceptions import tvdb_userabort -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)") - - 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 -Nru mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/ttvdb/tvdbXslt.py mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/ttvdb/tvdbXslt.py --- mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/ttvdb/tvdbXslt.py 2017-08-11 01:44:06.000000000 +0000 +++ mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/ttvdb/tvdbXslt.py 2017-08-28 01:11:03.000000000 +0000 @@ -37,6 +37,12 @@ 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 @@ 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 @@ """ 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 @@ """ self.FuncDict = { 'lastUpdated': self.lastUpdated, + 'replace': self.replace, 'htmlToString': self.htmlToString, 'stringToList': self.stringToList, 'imageElements': self.imageElements, @@ -187,26 +209,33 @@ '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'') 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 @@ 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 @@ '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 -Nru mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/ttvdb/XSLT/tvdbCollection.xsl mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/ttvdb/XSLT/tvdbCollection.xsl --- mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/ttvdb/XSLT/tvdbCollection.xsl 2017-08-11 01:44:06.000000000 +0000 +++ mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/ttvdb/XSLT/tvdbCollection.xsl 2017-08-28 01:11:03.000000000 +0000 @@ -16,7 +16,7 @@ within a single Xslt file --> - + @@ -24,28 +24,28 @@ - + - - <xsl:value-of select="normalize-space(SeriesName)"/> - - + + <xsl:value-of select="normalize-space(seriesName)"/> + + - - + + - - + + - - + + - + - + us @@ -53,9 +53,9 @@ - + - + genre @@ -63,62 +63,87 @@ - + - + - - + + - - + + - - + + - - + + - - - + + + - - + + - - + + - + - - - coverart - - - - - - - fanart - - - - - + + + + coverart + + + + + + + coverart + + + + + + + coverart + + + + + + + + + fanart + + + + + + + fanart + + + + + + banner - - + + diff -Nru mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/ttvdb/XSLT/tvdbQuery.xsl mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/ttvdb/XSLT/tvdbQuery.xsl --- mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/ttvdb/XSLT/tvdbQuery.xsl 2017-08-11 01:44:06.000000000 +0000 +++ mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/ttvdb/XSLT/tvdbQuery.xsl 2017-08-28 01:11:03.000000000 +0000 @@ -14,7 +14,7 @@ within a single Xslt file --> - + @@ -22,10 +22,10 @@ - + - <xsl:value-of select="normalize-space(SeriesName)"/> + <xsl:value-of select="normalize-space(seriesName)"/> @@ -34,14 +34,14 @@ - - + + - - + + - - + + diff -Nru mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/ttvdb/XSLT/tvdbVideo.xsl mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/ttvdb/XSLT/tvdbVideo.xsl --- mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/ttvdb/XSLT/tvdbVideo.xsl 2017-08-11 01:44:06.000000000 +0000 +++ mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/ttvdb/XSLT/tvdbVideo.xsl 2017-08-28 01:11:03.000000000 +0000 @@ -16,7 +16,7 @@ within a single Xslt file --> - + @@ -24,21 +24,20 @@ - + - <xsl:value-of select="normalize-space(SeriesName)"/> - + <xsl:value-of select="normalize-space(seriesName)"/> + - - + - + - + us @@ -46,9 +45,9 @@ - + - + genre @@ -56,36 +55,39 @@ - + - + - + - - + + - - + + - - - - - - - - - - + + + + + + + + + + + + + @@ -99,32 +101,32 @@ - + - + Actor - - - - - + + + + + - + Guest Star - + Director - + Author @@ -132,16 +134,17 @@ - + screenshot - - + + - + + @@ -154,7 +157,8 @@ - + + @@ -167,7 +171,8 @@ - + + diff -Nru mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/utility/altdict.py mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/utility/altdict.py --- mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/utility/altdict.py 2017-08-11 01:44:06.000000000 +0000 +++ mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/utility/altdict.py 2017-08-28 01:11:03.000000000 +0000 @@ -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 @@ 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 -Nru mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/utility/dequebuffer.py mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/utility/dequebuffer.py --- mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/utility/dequebuffer.py 2017-08-11 01:44:06.000000000 +0000 +++ mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/utility/dequebuffer.py 2017-08-28 01:11:03.000000000 +0000 @@ -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 @@ 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 -Nru mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/utility/dicttoxml.py mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/utility/dicttoxml.py --- mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/utility/dicttoxml.py 1970-01-01 00:00:00.000000000 +0000 +++ mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/utility/dicttoxml.py 2017-08-28 01:11:03.000000000 +0000 @@ -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 = '<%s>foo' % (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(']]>', ']]]]>') + return '' + + +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' % ( + 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' % ( + 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' % ( + item_name, + convert_dict(item, ids, parent, attr_type, item_func, cdata), + item_name, + ) + ) + else: + addline('<%s type="dict">%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' % ( + 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' % ( + 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' % ( + 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' % (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>' % (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('') + addline('<%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 -Nru mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/utility/dt.py mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/utility/dt.py --- mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/utility/dt.py 2017-08-11 01:44:06.000000000 +0000 +++ mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/utility/dt.py 2017-08-28 01:11:03.000000000 +0000 @@ -14,7 +14,7 @@ import os import re import time -import singleton +from . import singleton time.tzset() class basetzinfo( _pytzinfo ): @@ -69,7 +69,7 @@ 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 @@ 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 -Nru mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/utility/enum.py mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/utility/enum.py --- mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/utility/enum.py 2017-08-11 01:44:06.000000000 +0000 +++ mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/utility/enum.py 2017-08-28 01:11:03.000000000 +0000 @@ -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 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 -Nru mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/utility/__init__.py mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/utility/__init__.py --- mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/utility/__init__.py 2017-08-11 01:44:06.000000000 +0000 +++ mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/utility/__init__.py 2017-08-28 01:11:03.000000000 +0000 @@ -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 -Nru mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/utility/other.py mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/utility/other.py --- mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/bindings/python/MythTV/utility/other.py 2017-08-11 01:44:06.000000000 +0000 +++ mythtv-0.28.1+fixes.20170903.73cf747/mythtv/bindings/python/MythTV/utility/other.py 2017-08-28 01:11:03.000000000 +0000 @@ -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 @@ 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 @@ 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 @@ '%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 @@ of the following format (, -- Primary table to pull data from. , -- 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. , -- Tuple of keywords that must be @@ -231,7 +235,7 @@ 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 @@ 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 @@ 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 @@ '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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 -Nru mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/libs/libmythtv/libmythtv.pro mythtv-0.28.1+fixes.20170903.73cf747/mythtv/libs/libmythtv/libmythtv.pro --- mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/libs/libmythtv/libmythtv.pro 2017-08-18 01:00:47.000000000 +0000 +++ mythtv-0.28.1+fixes.20170903.73cf747/mythtv/libs/libmythtv/libmythtv.pro 2017-09-03 02:12:09.000000000 +0000 @@ -908,6 +908,17 @@ 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 -Nru mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/libs/libmythtv/videoout_omx.cpp mythtv-0.28.1+fixes.20170903.73cf747/mythtv/libs/libmythtv/videoout_omx.cpp --- mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/libs/libmythtv/videoout_omx.cpp 2017-08-18 01:00:48.000000000 +0000 +++ mythtv-0.28.1+fixes.20170903.73cf747/mythtv/libs/libmythtv/videoout_omx.cpp 2017-09-03 02:12:09.000000000 +0000 @@ -29,9 +29,6 @@ #ifdef OSD_EGL #include #include -#if QT_VERSION >= QT_VERSION_CHECK(5, 4, 0) -#include -#endif #endif // MythTV diff -Nru mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/libs/libmythui/libmythui.pro mythtv-0.28.1+fixes.20170903.73cf747/mythtv/libs/libmythui/libmythui.pro --- mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/libs/libmythui/libmythui.pro 2017-08-18 01:00:48.000000000 +0000 +++ mythtv-0.28.1+fixes.20170903.73cf747/mythtv/libs/libmythui/libmythui.pro 2017-09-03 02:12:09.000000000 +0000 @@ -183,6 +183,7 @@ 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 @@ 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 -Nru mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/libs/libmythui/mythrender_opengl2.cpp mythtv-0.28.1+fixes.20170903.73cf747/mythtv/libs/libmythui/mythrender_opengl2.cpp --- mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/libs/libmythui/mythrender_opengl2.cpp 2017-08-11 01:44:08.000000000 +0000 +++ mythtv-0.28.1+fixes.20170903.73cf747/mythtv/libs/libmythui/mythrender_opengl2.cpp 2017-08-30 01:00:47.000000000 +0000 @@ -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 @@ (const void *) kTextureOffset); glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + glCheck(); m_glDisableVertexAttribArray(TEXTURE_INDEX); m_glDisableVertexAttribArray(VERTEX_INDEX); diff -Nru mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/libs/libmythui/mythrender_opengl.cpp mythtv-0.28.1+fixes.20170903.73cf747/mythtv/libs/libmythui/mythrender_opengl.cpp --- mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/libs/libmythui/mythrender_opengl.cpp 2017-08-18 01:00:48.000000000 +0000 +++ mythtv-0.28.1+fixes.20170903.73cf747/mythtv/libs/libmythui/mythrender_opengl.cpp 2017-09-03 02:12:09.000000000 +0000 @@ -510,6 +510,12 @@ 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 -Nru mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/libs/libmythui/mythrender_opengl.h mythtv-0.28.1+fixes.20170903.73cf747/mythtv/libs/libmythui/mythrender_opengl.h --- mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/libs/libmythui/mythrender_opengl.h 2017-08-18 01:00:48.000000000 +0000 +++ mythtv-0.28.1+fixes.20170903.73cf747/mythtv/libs/libmythui/mythrender_opengl.h 2017-09-03 02:12:09.000000000 +0000 @@ -4,12 +4,18 @@ #include #include -#if defined USING_OPENGLES && QT_VERSION >= QT_VERSION_CHECK(5, 4, 0) -#define USE_OPENGL_QT5 -#include -#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 +//#else #include -#endif +//#endif #include #include #include diff -Nru mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/programs/mythavtest/main.cpp mythtv-0.28.1+fixes.20170903.73cf747/mythtv/programs/mythavtest/main.cpp --- mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/programs/mythavtest/main.cpp 2017-08-18 01:00:48.000000000 +0000 +++ mythtv-0.28.1+fixes.20170903.73cf747/mythtv/programs/mythavtest/main.cpp 2017-09-03 02:12:09.000000000 +0000 @@ -146,6 +146,11 @@ 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 -Nru mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/programs/mythfrontend/main.cpp mythtv-0.28.1+fixes.20170903.73cf747/mythtv/programs/mythfrontend/main.cpp --- mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/programs/mythfrontend/main.cpp 2017-08-18 01:00:48.000000000 +0000 +++ mythtv-0.28.1+fixes.20170903.73cf747/mythtv/programs/mythfrontend/main.cpp 2017-09-03 02:12:09.000000000 +0000 @@ -1675,6 +1675,10 @@ 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 -Nru mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/programs/mythfrontend/mythfrontend.pro mythtv-0.28.1+fixes.20170903.73cf747/mythtv/programs/mythfrontend/mythfrontend.pro --- mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/programs/mythfrontend/mythfrontend.pro 2017-08-18 01:00:48.000000000 +0000 +++ mythtv-0.28.1+fixes.20170903.73cf747/mythtv/programs/mythfrontend/mythfrontend.pro 2017-09-03 02:12:09.000000000 +0000 @@ -158,17 +158,30 @@ 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 -Nru mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/programs/mythfrontend/schedulecommon.cpp mythtv-0.28.1+fixes.20170903.73cf747/mythtv/programs/mythfrontend/schedulecommon.cpp --- mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/programs/mythfrontend/schedulecommon.cpp 2017-08-18 01:00:48.000000000 +0000 +++ mythtv-0.28.1+fixes.20170903.73cf747/mythtv/programs/mythfrontend/schedulecommon.cpp 2017-09-03 02:12:09.000000000 +0000 @@ -247,9 +247,16 @@ 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 -Nru mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/programs/mythfrontend/schedulecommon.h mythtv-0.28.1+fixes.20170903.73cf747/mythtv/programs/mythfrontend/schedulecommon.h --- mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/programs/mythfrontend/schedulecommon.h 2017-08-18 01:00:48.000000000 +0000 +++ mythtv-0.28.1+fixes.20170903.73cf747/mythtv/programs/mythfrontend/schedulecommon.h 2017-09-03 02:12:09.000000000 +0000 @@ -36,6 +36,7 @@ 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 -Nru mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/programs/mythfrontend/scheduleeditor.cpp mythtv-0.28.1+fixes.20170903.73cf747/mythtv/programs/mythfrontend/scheduleeditor.cpp --- mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/programs/mythfrontend/scheduleeditor.cpp 2017-08-18 01:00:48.000000000 +0000 +++ mythtv-0.28.1+fixes.20170903.73cf747/mythtv/programs/mythfrontend/scheduleeditor.cpp 2017-09-03 02:12:09.000000000 +0000 @@ -605,7 +605,8 @@ 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 -Nru mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/programs/mythscreenwizard/main.cpp mythtv-0.28.1+fixes.20170903.73cf747/mythtv/programs/mythscreenwizard/main.cpp --- mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/programs/mythscreenwizard/main.cpp 2017-08-11 01:44:08.000000000 +0000 +++ mythtv-0.28.1+fixes.20170903.73cf747/mythtv/programs/mythscreenwizard/main.cpp 2017-09-03 02:12:09.000000000 +0000 @@ -111,6 +111,11 @@ 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 -Nru mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/programs/mythtv-setup/main.cpp mythtv-0.28.1+fixes.20170903.73cf747/mythtv/programs/mythtv-setup/main.cpp --- mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/programs/mythtv-setup/main.cpp 2017-08-18 01:00:48.000000000 +0000 +++ mythtv-0.28.1+fixes.20170903.73cf747/mythtv/programs/mythtv-setup/main.cpp 2017-09-03 02:12:09.000000000 +0000 @@ -238,6 +238,10 @@ 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 -Nru mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/programs/mythwelcome/main.cpp mythtv-0.28.1+fixes.20170903.73cf747/mythtv/programs/mythwelcome/main.cpp --- mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/programs/mythwelcome/main.cpp 2017-08-18 01:00:48.000000000 +0000 +++ mythtv-0.28.1+fixes.20170903.73cf747/mythtv/programs/mythwelcome/main.cpp 2017-09-03 02:12:09.000000000 +0000 @@ -46,6 +46,10 @@ { bool bShowSettings = false; +#if HAVE_OPENMAX_BROADCOM + setenv("QT_XCB_GL_INTEGRATION","none",0); +#endif + MythWelcomeCommandLineParser cmdline; if (!cmdline.Parse(argc, argv)) { diff -Nru mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/programs/scripts/metadata/Television/ttvdb.py mythtv-0.28.1+fixes.20170903.73cf747/mythtv/programs/scripts/metadata/Television/ttvdb.py --- mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/programs/scripts/metadata/Television/ttvdb.py 2017-08-11 01:44:08.000000000 +0000 +++ mythtv-0.28.1+fixes.20170903.73cf747/mythtv/programs/scripts/metadata/Television/ttvdb.py 2017-08-28 01:11:03.000000000 +0000 @@ -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() + + + + Stargate SG-1 + Thor's Hammer + Teal'c and O'Neill are transported to an underground cage designed by the Asgard to protect an alien world from the Goa'uld. + 1 + 10 + + + + + + + + 72449 + 72449 + 0118480 + EP00225421 + en + 1997 + 1997-09-26 + + + + + + + + +... + + + + + + + + + + + + + + + + + +... + + + + +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/original/72449-28.jpg,http://thetvdb.com/banners/fanart/original/72449-47.jpg,http://thetvdb.com/banners/fanart/original/72449-33.jpg,http://thetvdb.com/banners/fanart/original/72449-39.jpg,http://thetvdb.com/banners/fanart/original/72449-43.jpg,http://thetvdb.com/banners/fanart/original/72449-22.jpg,http://thetvdb.com/banners/fanart/original/72449-30.jpg,http://thetvdb.com/banners/fanart/original/72449-40.jpg,http://thetvdb.com/banners/fanart/original/72449-41.jpg,http://thetvdb.com/banners/fanart/original/72449-49.jpg,http://thetvdb.com/banners/fanart/original/72449-51.jpg,http://thetvdb.com/banners/fanart/original/72449-21.jpg,http://thetvdb.com/banners/fanart/original/72449-20.jpg,http://thetvdb.com/banners/fanart/original/72449-45.jpg,http://thetvdb.com/banners/fanart/original/72449-67.jpg,http://thetvdb.com/banners/fanart/original/72449-9.jpg,http://thetvdb.com/banners/fanart/original/72449-46.jpg,http://thetvdb.com/banners/fanart/original/72449-48.jpg,http://thetvdb.com/banners/fanart/original/72449-4.jpg,http://thetvdb.com/banners/fanart/original/72449-1.jpg,http://thetvdb.com/banners/fanart/original/72449-65.jpg,http://thetvdb.com/banners/fanart/original/72449-37.jpg,http://thetvdb.com/banners/fanart/original/72449-16.jpg,http://thetvdb.com/banners/fanart/original/72449-17.jpg,http://thetvdb.com/banners/fanart/original/72449-3.jpg,http://thetvdb.com/banners/fanart/original/72449-7.jpg,http://thetvdb.com/banners/fanart/original/72449-10.jpg,http://thetvdb.com/banners/fanart/original/72449-8.jpg,http://thetvdb.com/banners/fanart/original/72449-5.jpg,http://thetvdb.com/banners/fanart/original/72449-64.jpg,http://thetvdb.com/banners/fanart/original/72449-2.jpg,http://thetvdb.com/banners/fanart/original/72449-61.jpg,http://thetvdb.com/banners/fanart/original/72449-12.jpg,http://thetvdb.com/banners/fanart/original/72449-13.jpg,http://thetvdb.com/banners/fanart/original/72449-14.jpg,http://thetvdb.com/banners/fanart/original/72449-15.jpg,http://thetvdb.com/banners/fanart/original/72449-18.jpg,http://thetvdb.com/banners/fanart/original/72449-63.jpg,http://thetvdb.com/banners/fanart/original/72449-11.jpg,http://thetvdb.com/banners/fanart/original/72449-19.jpg,http://thetvdb.com/banners/fanart/original/72449-52.jpg,http://thetvdb.com/banners/fanart/original/72449-53.jpg,http://thetvdb.com/banners/fanart/original/72449-54.jpg,http://thetvdb.com/banners/fanart/original/72449-56.jpg,http://thetvdb.com/banners/fanart/original/72449-57.jpg,http://thetvdb.com/banners/fanart/original/72449-58.jpg,http://thetvdb.com/banners/fanart/original/72449-59.jpg,http://thetvdb.com/banners/fanart/original/72449-60.jpg,http://thetvdb.com/banners/fanart/original/72449-62.jpg,http://thetvdb.com/banners/fanart/original/72449-73.jpg,http://thetvdb.com/banners/fanart/original/72449-74.jpg,http://thetvdb.com/banners/fanart/original/72449-75.jpg +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.thetvdb.com/banners/graphical/73739-g.jpg,http://www.thetvdb.com/banners/graphical/73739-g6.jpg,http://www.thetvdb.com/banners/graphical/73739-g8.jpg,http://www.thetvdb.com/banners/graphical/73739-g3.jpg,http://www.thetvdb.com/banners/graphical/73739-g7.jpg,http://www.thetvdb.com/banners/graphical/73739-g5.jpg,http://www.thetvdb.com/banners/graphical/24313-g2.jpg,http://www.thetvdb.com/banners/graphical/24313-g.jpg,http://www.thetvdb.com/banners/graphical/73739-g10.jpg,http://www.thetvdb.com/banners/graphical/73739-g2.jpg +> 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() + + + + Eleventh Hour (US) + H2O + 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. + 1 + 10 + + + + + + + + 83066 + 83066 + 1118697 + en + 2009 + 2009-01-15 + + + + + + + + + + + + + + + + + + + + + + + +0 + +# en +# +# +(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() + + + + Sanctuary + End of Nights (2) + 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. + 2 + 2 + + + + + + + + 80159 + 80159 + 0965394 + EP01085421 + en + 2009 + 2009-10-16 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +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() + + + + en + A Night of Numbers + 249306 + 249306 + 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. + 2005-12-06 + + + + + + en + A night at The Classic + 224951 + 224951 + 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. + 2010-11-03 + + + + + + en + A Night at the Rijksmuseum + 268908 + 268908 + 2013-04-18 + + + en + A Night of Heroes: The Sun Military Awards + 270984 + 270984 + Annual celebration, A Night of Heroes: Also known as The Millies, the awards recognize the excellence and sacrifice made by Britain's Armed Forces + + + + + + en + A Night of Exploration + 271528 + 271528 + 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. + + + + + + en + A Night at the Office + 118511 + 118511 + 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. + 2009-08-17 + + + + + + en + A Night With The Stars + 256045 + 256045 + 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. + 2011-12-18 + + + + + + en + A Night at the Festival Club + 268969 + 268969 + 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. + 2008-05-02 + + + en + A Clear Midsummer Night + 286538 + 286538 + 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. + + + en + A Christmas Night with the Stars + 248911 + 248911 + 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. + 1958-12-25 + + + + + + en + A Night With My Ex + 331751 + 331751 + 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. + 2017-07-18 + + + en + On a Lustful Night Mingling with a Priest... + 325375 + 325375 + 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! + 2017-04-03 + + + en + Love on a Saturday Night + 74382 + 74382 + 2004-02-01 + + + en + Britain's Tudor Treasure A Night At Hampton Court + 332440 + 332440 + 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. + + +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() + + + + de + Sanctuary + Space + Friday + 10:00 PM + 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. + + + + + + + 60 + 80159 + 0965394 + 8.1 + 168 + 2007 + 2007-03-14 + ... + Ended + + + + + + +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/original/281053-8.jpg,http://thetvdb.com/banners/fanart/original/281053-9.jpg,http://thetvdb.com/banners/fanart/original/281053-10.jpg +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() + + + + NCIS + Pyramid + 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. + 8 + 24 + + + + + + + + 72108 + 72108 + EP00681911 + + en + 2011 + 2011-05-17 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +... + + + + +0 + +>>> sys.argv = shlex.split('ttvdb.py -l en -a US -N 283661 "Egg Hunt"') +>>> main() + + + + Jack Hanna's Wild Countdown + Egg Hunt + 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! + 6 + 17 + + + + + + + + 283661 + 283661 + 3062384 + EP01441760 + en + 2017 + 2017-04-15 + + + + + + + +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 Add the -C (collection option) with corresponding XML output # and add a 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 @@ ''' + # Episode keys that can be used in a episode data/information search. # All keys are currently being used. ''' @@ -368,47 +912,94 @@ 'episodename' ''' - # System modules -import sys, os, re, locale, ConfigParser +import sys, os, re from optparse import OptionParser from copy import deepcopy +# 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, tvdb_ui.py and tvdb_exceptions.py are available +# 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 @@ 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 @@ # 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 @@ 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 @@ 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 @@ 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 @@ # 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 @@ #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 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 @@ # 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ # 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 @@ 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 @@ # 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ # 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 @@ 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 @@ 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 @@ # 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 @@ 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 @@ 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 @@ 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'')) - if tvdb_api.imagesInfoTree != None: - allDataElement.append(tvdb_api.imagesInfoTree) - else: - allDataElement.append(etree.XML(u'')) + 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 @@ 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 \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 @@ 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 @@ 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 @@ 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 @@ # 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 @@ 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 @@ 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 @@ # 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 @@ # 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 @@ 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 - - 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() + print("#"*20) + print("# Checking if Posters, Fanart or Banners are available") + print("#"*20) + + 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 @@ # 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 @@ 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 @@ 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 @@ 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 @@ 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 -Nru mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/programs/scripts/metadata/Television/tvdb_test.conf mythtv-0.28.1+fixes.20170903.73cf747/mythtv/programs/scripts/metadata/Television/tvdb_test.conf --- mythtv-0.28.1+fixes.20170818.2c4c711/mythtv/programs/scripts/metadata/Television/tvdb_test.conf 1970-01-01 00:00:00.000000000 +0000 +++ mythtv-0.28.1+fixes.20170903.73cf747/mythtv/programs/scripts/metadata/Television/tvdb_test.conf 2017-08-28 01:11:03.000000000 +0000 @@ -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