From 08bffa5f6df497f28fe3481fe80b517628b0f1a3 Mon Sep 17 00:00:00 2001 From: Raymond Wagner Date: Thu, 29 Mar 2012 13:36:54 -0400 Subject: [PATCH 01/69] Add __contains__ for proper lookup in cache Engines class. --- tmdb3/cache_engine.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tmdb3/cache_engine.py b/tmdb3/cache_engine.py index a98a110fb28..b1cdd284ea3 100644 --- a/tmdb3/cache_engine.py +++ b/tmdb3/cache_engine.py @@ -15,6 +15,8 @@ def register(self, engine): self._engines[engine.name] = engine def __getitem__(self, key): return self._engines[key] + def __contains__(self, key): + return self._engines.__contains__(key) Engines = Engines() class CacheEngineType( type ): From 263d8680de3566f2109b76b4b1d4d2779b967d10 Mon Sep 17 00:00:00 2001 From: Raymond Wagner Date: Thu, 29 Mar 2012 14:15:19 -0400 Subject: [PATCH 02/69] Improves cache file selection for Windows. --- tmdb3/cache_file.py | 41 +++++++++++++++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/tmdb3/cache_file.py b/tmdb3/cache_file.py index 867306c3d12..0085f412e4a 100644 --- a/tmdb3/cache_file.py +++ b/tmdb3/cache_file.py @@ -45,11 +45,25 @@ def __exit__(self, exc_type, exc_value, exc_tb): suppress = self.callback(exc_type, exc_value, exc_tb) fcntl.flock(self.fileobj, fcntl.LOCK_UN) return suppress + + def parse_filename(filename): + if '$' in filename: + # replace any environmental variables + filename = os.path.expandvars(filename) + if filename.startswith('~'): + # check for home directory + return os.path.expanduser(filename) + elif not filename.startswith('/'): + # check for absolute path + return filename + # return path with temp directory prepended + return '/tmp/' + filename + except ImportError: import msvcrt class Flock( object ): - LOCK_EX = msvcrt.NBLCK - LOCK_SH = msvcrt.NBLCK + LOCK_EX = msvcrt.LK_LOCK + LOCK_SH = msvcrt.LK_LOCK def __init__(self, fileobj, operation, callback=None): self.fileobj = fileobj @@ -65,6 +79,23 @@ def __exit__(self, exc_type, exc_value, exc_tb): msvcrt.locking(self.fileobj.fileno(), msvcrt.LK_UNLCK, self.size) return suppress + def parse_filename(filename): + if '%' in filename: + # replace any environmental variables + filename = os.path.expandvars(filename) + if filename.startswith('~'): + # check for home directory + return os.path.expanduser(filename) + elif (ord(filename[0]) in (range(65,91)+range(99,123))) \ + and (filename[1:3] == ':\\'): + # check for absolute drive path (e.g. C:\...) + return filename + elif (filename.count('\\') >= 3) and (filename.startswith('\\\\')): + # check for absolute UNC path (e.g. \\server\...) + return filename + # return path with temp directory prepended + return os.path.expandvars(os.path.join('%TEMP%',filename)) + class FileEngine( CacheEngine ): """Simple file-backed engine.""" name = 'file' @@ -86,10 +117,8 @@ def _init_cache(self): if self.cachefile is None: raise TMDBCacheError("No cache filename given.") - if self.cachefile.startswith('~'): - self.cachefile = os.path.expanduser(self.cachefile) - elif not self.cachefile.startswith('/'): - self.cachefile = '/tmp/' + self.cachefile + + self.cachefile = parse_filename(self.cachefile) try: # attempt to read existing cache at filename From 38922793f5732d10c4dfbcf6d421d40667812675 Mon Sep 17 00:00:00 2001 From: Raymond Wagner Date: Thu, 29 Mar 2012 23:43:12 -0400 Subject: [PATCH 03/69] Add 'adult' and 'alias' to Person class. --- tmdb3/tmdb_api.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tmdb3/tmdb_api.py b/tmdb3/tmdb_api.py index 1d123e9e293..89591ee4c8a 100644 --- a/tmdb3/tmdb_api.py +++ b/tmdb3/tmdb_api.py @@ -22,7 +22,7 @@ Preliminary API specifications can be found at http://help.themoviedb.org/kb/api/about-3""" -__version__="v0.4.1" +__version__="v0.4.3" # 0.1.0 Initial development # 0.2.0 Add caching mechanism for API queries # 0.2.1 Temporary work around for broken search paging @@ -36,6 +36,8 @@ # 0.3.7 Generalize caching mechanism, and allow controllability # 0.4.0 Add full locale support (language and country) and optional fall through # 0.4.1 Add custom classmethod for dealing with IMDB movie IDs +# 0.4.2 Improve cache file selection for Windows systems +# 0.4.3 Add a few missed Person properties from request import set_key, Request from util import Datapoint, Datalist, Datadict, Element @@ -132,6 +134,8 @@ class Person( Element ): homepage = Datapoint('homepage') birthplace = Datapoint('place_of_birth') profile = Datapoint('profile_path', handler=Profile, raw=False) + adult = Datapoint('adult') + aliases = Datalist('also_known_as') def __repr__(self): return u"<{0} '{1.name}' at {2}>".\ From e99630e023a6ae8033bba27528e9a3ca45e72c1e Mon Sep 17 00:00:00 2001 From: Raymond Wagner Date: Fri, 30 Mar 2012 00:41:31 -0400 Subject: [PATCH 04/69] Add support for additional Studio information. --- tmdb3/tmdb_api.py | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/tmdb3/tmdb_api.py b/tmdb3/tmdb_api.py index 89591ee4c8a..9af6d7a97f9 100644 --- a/tmdb3/tmdb_api.py +++ b/tmdb3/tmdb_api.py @@ -22,7 +22,7 @@ Preliminary API specifications can be found at http://help.themoviedb.org/kb/api/about-3""" -__version__="v0.4.3" +__version__="v0.4.4" # 0.1.0 Initial development # 0.2.0 Add caching mechanism for API queries # 0.2.1 Temporary work around for broken search paging @@ -38,6 +38,7 @@ # 0.4.1 Add custom classmethod for dealing with IMDB movie IDs # 0.4.2 Improve cache file selection for Windows systems # 0.4.3 Add a few missed Person properties +# 0.4.4 Add support for additional Studio information from request import set_key, Request from util import Datapoint, Datalist, Datadict, Element @@ -118,6 +119,10 @@ def sizes(self): class Profile( Image ): def sizes(self): return Configuration.images['profile_sizes'] +class Logo( Image ): + def sizes(self): + # FIXME: documentation does not list available sizes + return ['original'] class AlternateTitle( Element ): country = Datapoint('iso_3166_1') @@ -226,12 +231,32 @@ def __repr__(self): return u"<{0.__class__.__name__} '{0.name}'>".format(self) class Studio( Element ): - id = Datapoint('id') - name = Datapoint('name') + id = Datapoint('id', initarg=1) + name = Datapoint('name') + description = Datapoint('description') + headquarters = Datapoint('headquarters') + logo = Datapoint('logo_path', handler=Logo) + # FIXME: manage not-yet-defined handlers in a way that will propogate + # locale information properly + parent = Datapoint('parent_company', handler=lambda x: Studio(raw=x)) def __repr__(self): return u"<{0.__class__.__name__} '{0.name}'>".format(self) + def _populate(self): + return Request('company/{0}'.format(self.id)) + def _populate_movies(self): + return Request('company/{0}/movies'.format(self.id), language=self._locale.language) + + # FIXME: add a cleaner way of adding types with no additional processing + @property + def movies(self): + if 'movies' not in self._data: + search = MovieSearchResult(self._populate_movies(), locale=self._locale) + search._name = "{0.name} Movies".format(self) + self._data['movies'] = search + return self._data['movies'] + class Country( Element ): code = Datapoint('iso_3166_1') name = Datapoint('name') From 166655c70bb4ac340c6e559b167114f3e08bbdc0 Mon Sep 17 00:00:00 2001 From: Raymond Wagner Date: Fri, 30 Mar 2012 00:48:04 -0400 Subject: [PATCH 05/69] Update README for additional options. --- README | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/README b/README index e4f205cbdfe..fde1c90863a 100644 --- a/README +++ b/README @@ -106,18 +106,20 @@ out. The people search method behaves similarly. ## Direct Queries -There are currently three data types that support direct access: collections, -movies, and people. These each take a single integer ID as an argument. All -data attributes are implemented as properties, and populated on-demand as -used, rather than when the object is created. +There are currently four data types that support direct access: collections, +movies, people, and studios. These each take a single integer ID as an +argument. All data attributes are implemented as properties, and populated +on-demand as used, rather than when the object is created. - >>> from tmdb3 import Collection, Movie, Person + >>> from tmdb3 import Collection, Movie, Person, Studio >>> Collection(10) >>> Movie(11) >>> Person(2) + >>> Studio(1) + ## Image Behavior @@ -223,7 +225,9 @@ Person: datetime dayofbirth datetime dayofdeath string homepage - Profile profilie + Profile profile + boolean adult + list(string) aliases list(ReverseCast) roles list(ReverseCrew) crew list(Profile) profiles @@ -255,6 +259,7 @@ Image: Backdrop (derived from Image) Poster (derived from Image) Profile (derived from Image) +Logo (derived from Image) AlternateTitle: string country @@ -277,6 +282,11 @@ Genre: Studio: integer id string name + string description + string headquarters + Logo logo + Studio parent + list(Movie) movies Country: string code From 75a14b7da282f15fc84819418ec845e86559eaf2 Mon Sep 17 00:00:00 2001 From: Raymond Wagner Date: Fri, 30 Mar 2012 00:49:19 -0400 Subject: [PATCH 06/69] Correct cache file selection logic inverted by 4663490f8fa. --- tmdb3/cache_file.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tmdb3/cache_file.py b/tmdb3/cache_file.py index 0085f412e4a..2626805eb4c 100644 --- a/tmdb3/cache_file.py +++ b/tmdb3/cache_file.py @@ -53,7 +53,7 @@ def parse_filename(filename): if filename.startswith('~'): # check for home directory return os.path.expanduser(filename) - elif not filename.startswith('/'): + elif filename.startswith('/'): # check for absolute path return filename # return path with temp directory prepended From 61045e47bca155fb40ec2de47d01cd86bd28992b Mon Sep 17 00:00:00 2001 From: Raymond Wagner Date: Sat, 31 Mar 2012 19:15:39 -0400 Subject: [PATCH 07/69] Allow Datalists to be sorted natively without supplying a key. --- tmdb3/util.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tmdb3/util.py b/tmdb3/util.py index 2fefaf98ca8..4d6939b7972 100644 --- a/tmdb3/util.py +++ b/tmdb3/util.py @@ -181,7 +181,10 @@ def __set__(self, inst, value): val._locale = inst._locale data.append(val) if self.sort: - data.sort(key=lambda x: getattr(x, self.sort)) + if self.sort is True: + data.sort() + else: + data.sort(key=lambda x: getattr(x, self.sort)) inst._data[self.field] = data class Datadict( Data ): From ddc0cb0550b8de72e4ddad874ad05865b3b50c3f Mon Sep 17 00:00:00 2001 From: Raymond Wagner Date: Sat, 31 Mar 2012 19:52:53 -0400 Subject: [PATCH 08/69] Allow locale fallthrough for movie images and alternate titles. When fallthrough is enabled, queries for backdrops, posters, and alternate titles will not filter for the selected locale, but will instead sort the results to place those matching the specified locale at the top of the list. Also resolves an issue where Request() was incorrectly passing 'None' as an argument when no locale was set. --- tmdb3/locales.py | 10 ++++++++++ tmdb3/request.py | 6 ++++-- tmdb3/tmdb_api.py | 32 +++++++++++++++++++++++++++----- 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/tmdb3/locales.py b/tmdb3/locales.py index 2cb17aaa4e2..ff7dbddb91b 100644 --- a/tmdb3/locales.py +++ b/tmdb3/locales.py @@ -32,6 +32,13 @@ def __delattr__(self, key): ' does not support modification.') super(LocaleBase, self).__delattr__(key) + def __lt__(self, other): + return (id(self) != id(other)) and (str(self) > str(other)) + def __gt__(self, other): + return (id(self) != id(other)) and (str(self) < str(other)) + def __eq__(self, other): + return (id(self) == id(other)) or (str(self) == str(other)) + @classmethod def getstored(cls, key): if key is None: @@ -82,6 +89,9 @@ def __init__(self, language, country): self.language = Language.getstored(language) self.country = Country.getstored(country) + def __str__(self): + return u"{0}_{1}".format(self.language, self.country) + def __repr__(self): return u"".format(self) diff --git a/tmdb3/request.py b/tmdb3/request.py index ea7139658b2..0a69b9d41f0 100644 --- a/tmdb3/request.py +++ b/tmdb3/request.py @@ -17,9 +17,11 @@ import os DEBUG = False - cache = Cache(filename='pytmdb3.cache') +#DEBUG = True +#cache = Cache(engine='null') + def set_key(key): """ Specify the API key to use retrieving data from themoviedb.org. This @@ -55,7 +57,7 @@ def __init__(self, url, **kwargs): self._kwargs = dict([(kwa,kwv) for kwa,kwv in kwargs.items() if kwv is not None]) url = '{0}{1}?{2}'.format(self._base_url, self._url, - urllib.urlencode(kwargs)) + urllib.urlencode(self._kwargs)) urllib2.Request.__init__(self, url) self.add_header('Accept', 'application/json') self.lifetime = 3600 # 1hr diff --git a/tmdb3/tmdb_api.py b/tmdb3/tmdb_api.py index 9af6d7a97f9..60d90becdf0 100644 --- a/tmdb3/tmdb_api.py +++ b/tmdb3/tmdb_api.py @@ -107,6 +107,16 @@ def geturl(self, size='original'): url = Configuration.images['base_url'].rstrip('/') return url+'/{0}/{1}'.format(size, self.filename) + # sort preferring locale's language, but keep remaining ordering consistent + def __lt__(self, other): + return (self.language == self._locale.language) \ + and (self.language != other.language) + def __gt__(self, other): + return (self.language != other.language) \ + and (other.language == self._locale.language) + def __eq__(self, other): + return self.language == other.language + def __repr__(self): return u"<{0.__class__.__name__} '{0.filename}'>".format(self) @@ -128,6 +138,16 @@ class AlternateTitle( Element ): country = Datapoint('iso_3166_1') title = Datapoint('title') + # sort preferring locale's country, but keep remaining ordering consistent + def __lt__(self, other): + return (self.country == self._locale.country) \ + and (self.country != other.country) + def __gt__(self, other): + return (self.country != other.country) \ + and (other.country == self._locale.country) + def __eq__(self, other): + return self.country == other.country + class Person( Element ): id = Datapoint('id', initarg=1) name = Datapoint('name') @@ -342,11 +362,13 @@ def fromIMDB(cls, imdbid, locale=None): def _populate(self): return Request('movie/{0}'.format(self.id), language=self._locale.language) def _populate_titles(self): - return Request('movie/{0}/alternative_titles'.format(self.id), country=self._locale.country) + kwargs = {'country':self._locale.country} if not self._locale.fallthrough else {} + return Request('movie/{0}/alternative_titles'.format(self.id), **kwargs) def _populate_cast(self): return Request('movie/{0}/casts'.format(self.id)) def _populate_images(self): - return Request('movie/{0}/images'.format(self.id), language=self._locale.language) + kwargs = {'language':self._locale.language} if not self._locale.fallthrough else {} + return Request('movie/{0}/images'.format(self.id), **kwargs) def _populate_keywords(self): return Request('movie/{0}/keywords'.format(self.id)) def _populate_releases(self): @@ -356,11 +378,11 @@ def _populate_trailers(self): def _populate_translations(self): return Request('movie/{0}/translations'.format(self.id)) - alternate_titles = Datalist('titles', handler=AlternateTitle, poller=_populate_titles) + alternate_titles = Datalist('titles', handler=AlternateTitle, poller=_populate_titles, sort=True) cast = Datalist('cast', handler=Cast, poller=_populate_cast, sort='order') crew = Datalist('crew', handler=Crew, poller=_populate_cast) - backdrops = Datalist('backdrops', handler=Backdrop, poller=_populate_images) - posters = Datalist('posters', handler=Poster, poller=_populate_images) + backdrops = Datalist('backdrops', handler=Backdrop, poller=_populate_images, sort=True) + posters = Datalist('posters', handler=Poster, poller=_populate_images, sort=True) keywords = Datalist('keywords', handler=Keyword, poller=_populate_keywords) releases = Datadict('countries', handler=Release, poller=_populate_releases, attr='country') youtube_trailers = Datalist('youtube', handler=YoutubeTrailer, poller=_populate_trailers) From 2e4bc644b0d2b0277a18ffa0b0e66aa18c17dfc6 Mon Sep 17 00:00:00 2001 From: Raymond Wagner Date: Sat, 31 Mar 2012 20:03:41 -0400 Subject: [PATCH 09/69] Correct Studio.logo processing. --- tmdb3/tmdb_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tmdb3/tmdb_api.py b/tmdb3/tmdb_api.py index 60d90becdf0..a4d585fdaea 100644 --- a/tmdb3/tmdb_api.py +++ b/tmdb3/tmdb_api.py @@ -255,7 +255,7 @@ class Studio( Element ): name = Datapoint('name') description = Datapoint('description') headquarters = Datapoint('headquarters') - logo = Datapoint('logo_path', handler=Logo) + logo = Datapoint('logo_path', handler=Logo, raw=False) # FIXME: manage not-yet-defined handlers in a way that will propogate # locale information properly parent = Datapoint('parent_company', handler=lambda x: Studio(raw=x)) From 2198d9ef21d5aaf47550084b8904e3da73137956 Mon Sep 17 00:00:00 2001 From: Raymond Wagner Date: Sat, 31 Mar 2012 20:14:52 -0400 Subject: [PATCH 10/69] Update Logo() class with proper image sizes from Configuration(). --- tmdb3/tmdb_api.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tmdb3/tmdb_api.py b/tmdb3/tmdb_api.py index a4d585fdaea..725911640f6 100644 --- a/tmdb3/tmdb_api.py +++ b/tmdb3/tmdb_api.py @@ -131,8 +131,7 @@ def sizes(self): return Configuration.images['profile_sizes'] class Logo( Image ): def sizes(self): - # FIXME: documentation does not list available sizes - return ['original'] + return Configuration.images['logo_sizes'] class AlternateTitle( Element ): country = Datapoint('iso_3166_1') From 86b54a14992a24c3f727efd5247842ee2d2393c3 Mon Sep 17 00:00:00 2001 From: Raymond Wagner Date: Sun, 1 Apr 2012 03:14:49 -0400 Subject: [PATCH 11/69] Add slice support to search result pager. --- tmdb3/pager.py | 2 ++ tmdb3/tmdb_api.py | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/tmdb3/pager.py b/tmdb3/pager.py index efa34d20bb6..6cb874c0824 100644 --- a/tmdb3/pager.py +++ b/tmdb3/pager.py @@ -57,6 +57,8 @@ def __init__(self, iterable, pagesize=20): self._pagesize = pagesize def __getitem__(self, index): + if isinstance(index, slice): + return [self[x] for x in xrange(*index.indices(len(self)))] if index >= len(self): raise IndexError("list index outside range") if (index >= len(self._data)) \ diff --git a/tmdb3/tmdb_api.py b/tmdb3/tmdb_api.py index 725911640f6..573a1f316f8 100644 --- a/tmdb3/tmdb_api.py +++ b/tmdb3/tmdb_api.py @@ -22,7 +22,7 @@ Preliminary API specifications can be found at http://help.themoviedb.org/kb/api/about-3""" -__version__="v0.4.4" +__version__="v0.4.6" # 0.1.0 Initial development # 0.2.0 Add caching mechanism for API queries # 0.2.1 Temporary work around for broken search paging @@ -39,6 +39,8 @@ # 0.4.2 Improve cache file selection for Windows systems # 0.4.3 Add a few missed Person properties # 0.4.4 Add support for additional Studio information +# 0.4.5 Add locale fallthrough for images and alternate titles +# 0.4.6 Add slice support for search results from request import set_key, Request from util import Datapoint, Datalist, Datadict, Element From e0d604111ba9f09e4f161d4280ea132e0e4a1990 Mon Sep 17 00:00:00 2001 From: Raymond Wagner Date: Mon, 2 Apr 2012 13:28:11 -0400 Subject: [PATCH 12/69] Adds proper encoding to strings being passed to a request. --- tmdb3/locales.py | 59 +++++++++++++++++++++++++++++++++++++++--------- tmdb3/request.py | 12 +++++++--- 2 files changed, 57 insertions(+), 14 deletions(-) diff --git a/tmdb3/locales.py b/tmdb3/locales.py index ff7dbddb91b..bb1c0f40a91 100644 --- a/tmdb3/locales.py +++ b/tmdb3/locales.py @@ -83,11 +83,12 @@ def __repr__(self): return u"".format(self) class Locale( LocaleBase ): - __slots__ = ['language', 'country'] + __slots__ = ['language', 'country', 'encoding'] - def __init__(self, language, country): + def __init__(self, language, country, encoding): self.language = Language.getstored(language) self.country = Country.getstored(country) + self.encoding = encoding if encoding else 'latin-1' def __str__(self): return u"{0}_{1}".format(self.language, self.country) @@ -95,33 +96,69 @@ def __str__(self): def __repr__(self): return u"".format(self) + def encode(self, dat): + """Encode using system default encoding for network/file output.""" + try: + return dat.encode(self.encoding) + except AttributeError: + # not a string type, pass along + return dat + except UnicodeDecodeError: + # just return unmodified and hope for the best + return dat + + def decode(self, dat): + """Decode to system default encoding for internal use.""" + try: + return dat.decode(self.encoding) + except AttributeError: + # not a string type, pass along + return dat + except UnicodeEncodeError: + # just return unmodified and hope for the best + return dat + def set_locale(language=None, country=None, fallthrough=False): global syslocale LocaleBase.fallthrough = fallthrough + sysloc, sysenc = locale.getdefaultlocale() + if (not language) or (not country): dat = None if syslocale is not None: dat = (str(syslocale.language), str(syslocale.country)) else: - res = locale.getdefaultlocale()[0] - if (res is None) or ('_' not in res): + if (sysloc is None) or ('_' not in sysloc): dat = ('en', 'US') else: - dat = res.split('_') + dat = sysloc.split('_') if language is None: language = dat[0] if country is None: country = dat[1] - syslocale = Locale(language, country) -def get_locale(language=None, country=None): + syslocale = Locale(language, country, sysenc) + +def get_locale(language=-1, country=-1): + """Output locale using provided attributes, or return system locale.""" global syslocale - if language and country: - return Locale(language, country) + # pull existing stored values if syslocale is None: - return Locale(None, None) - return syslocale + loc = Locale(None, None, locale.getdefaultlocale()[1]) + else: + loc = syslocale + + # both options are default, return stored values + if language == country == -1: + return loc + + # supplement default option with stored values + if language == -1: + language = loc.language + elif country == -1: + country = loc.country + return Locale(language, country, loc.encoding) ######## AUTOGENERATED LANGUAGE AND COUNTRY DATA BELOW HERE ######### diff --git a/tmdb3/request.py b/tmdb3/request.py index 0a69b9d41f0..b074d22ca50 100644 --- a/tmdb3/request.py +++ b/tmdb3/request.py @@ -9,10 +9,11 @@ #----------------------- from tmdb_exceptions import * +from locales import get_locale from cache import Cache +from urllib import urlencode import urllib2 -import urllib import json import os @@ -56,8 +57,13 @@ def __init__(self, url, **kwargs): self._url = url.lstrip('/') self._kwargs = dict([(kwa,kwv) for kwa,kwv in kwargs.items() if kwv is not None]) - url = '{0}{1}?{2}'.format(self._base_url, self._url, - urllib.urlencode(self._kwargs)) + + locale = get_locale() + kwargs = {} + for k,v in self._kwargs.items(): + kwargs[k] = locale.encode(v) + url = '{0}{1}?{2}'.format(self._base_url, self._url, urlencode(kwargs)) + urllib2.Request.__init__(self, url) self.add_header('Accept', 'application/json') self.lifetime = 3600 # 1hr From c35f455ee4bf2dbaac87f6ce2fca2f489cd2d209 Mon Sep 17 00:00:00 2001 From: Raymond Wagner Date: Wed, 11 Apr 2012 01:23:18 -0400 Subject: [PATCH 13/69] Remove empty space. --- tmdb3/tmdb_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tmdb3/tmdb_api.py b/tmdb3/tmdb_api.py index 573a1f316f8..aec637a7682 100644 --- a/tmdb3/tmdb_api.py +++ b/tmdb3/tmdb_api.py @@ -75,7 +75,7 @@ def __init__(self, request, locale=None): super(MovieSearchResult, self).__init__( request.new(language=locale.language), lambda x: Movie(raw=x, locale=locale)) - + def __repr__(self): name = self._name if self._name else self._request._kwargs['query'] return u"".format(name) From 642de961e034072eaf6342c01451b040f0b9ab3b Mon Sep 17 00:00:00 2001 From: Raymond Wagner Date: Wed, 11 Apr 2012 01:24:21 -0400 Subject: [PATCH 14/69] Fix Cache exceptions. --- tmdb3/tmdb_exceptions.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tmdb3/tmdb_exceptions.py b/tmdb3/tmdb_exceptions.py index 7c8f04aa6f2..96dc564570e 100644 --- a/tmdb3/tmdb_exceptions.py +++ b/tmdb3/tmdb_exceptions.py @@ -52,19 +52,19 @@ class TMDBCacheError( TMDBRequestError ): class TMDBCacheReadError( TMDBCacheError ): def __init__(self, filename): - super(TMDBCachePermissionsError, self).__init__( + super(TMDBCacheReadError, self).__init__( "User does not have permission to access cache file: {0}.".format(filename)) self.filename = filename class TMDBCacheWriteError( TMDBCacheError ): def __init__(self, filename): - super(TMDBCachePermissionsError, self).__init__( + super(TMDBCacheWriteError, self).__init__( "User does not have permission to write cache file: {0}.".format(filename)) self.filename = filename class TMDBCacheDirectoryError( TMDBCacheError ): def __init__(self, filename): - super(TMDBCachePermissionsError, self).__init__( + super(TMDBCacheDirectoryError, self).__init__( "Directory containing cache file does not exist: {0}.".format(filename)) self.filename = filename From 23fa543ceb2690340e74d257a6bc86f817c02be6 Mon Sep 17 00:00:00 2001 From: Raymond Wagner Date: Wed, 11 Apr 2012 01:28:27 -0400 Subject: [PATCH 15/69] Improve file cache performance. This reworks the cache framework, and alters the file cache engine to support incremental writes. This allows the results from 256 queries to be stored before having to rewrite the file. Previously, having to rewrite the entire cache file on each step would take longer than actually querying TMDB once the cache stored more than ~100 entries. May be a bit fragile. --- tmdb3/cache.py | 33 +++-- tmdb3/cache_engine.py | 29 +++- tmdb3/cache_file.py | 301 +++++++++++++++++++++++++++++++++--------- tmdb3/cache_null.py | 4 +- tmdb3/tmdb_api.py | 3 +- 5 files changed, 292 insertions(+), 78 deletions(-) diff --git a/tmdb3/cache.py b/tmdb3/cache.py index 0e9e70ac60b..83d81f87136 100644 --- a/tmdb3/cache.py +++ b/tmdb3/cache.py @@ -27,8 +27,23 @@ class Cache( object ): """ def __init__(self, engine=None, *args, **kwargs): self._engine = None + self._data = {} + self._age = 0 self.configure(engine, *args, **kwargs) + def _import(self, data=None): + if data is None: + data = self._engine.get(self._age) + for obj in sorted(data, key=lambda x: x.creation): + if not obj.expired: + self._data[obj.key] = obj + self._age = max(self._age, obj.creation) + + def _expire(self): + for k,v in self._data.items(): + if v.expired: + del self._data[k] + def configure(self, engine, *args, **kwargs): if engine is None: engine = 'file' @@ -37,21 +52,23 @@ def configure(self, engine, *args, **kwargs): self._engine = Engines[engine](self) self._engine.configure(*args, **kwargs) - def put(self, key, data, lifetime=3600): + def put(self, key, data, lifetime=60*60*12): # pull existing data, so cache will be fresh when written back out if self._engine is None: raise TMDBCacheError("No cache engine configured") - self._engine.put(key, data, lifetime) + self._expire() + self._import(self._engine.put(key, data, lifetime)) def get(self, key): if self._engine is None: raise TMDBCacheError("No cache engine configured") - return self._engine.get(key) - - self._read() - if (key in self._cache) and (time.time() < self._cache[key][0]): - return self._cache[key][1] - return None + self._expire() + if key not in self._data: + self._import() + try: + return self._data[key].data + except: + return None def cached(self, callback): """ diff --git a/tmdb3/cache_engine.py b/tmdb3/cache_engine.py index b1cdd284ea3..99ad4cdaeb7 100644 --- a/tmdb3/cache_engine.py +++ b/tmdb3/cache_engine.py @@ -7,6 +7,9 @@ # Purpose: Base cache engine class for collecting registered engines #----------------------- +import time +from weakref import ref + class Engines( object ): def __init__(self): self._engines = {} @@ -35,13 +38,35 @@ class CacheEngine( object ): name = 'unspecified' def __init__(self, parent): - self.parent = parent + self.parent = ref(parent) def configure(self): raise RuntimeError - def get(self, key): + def get(self, date): raise RuntimeError def put(self, key, value, lifetime): raise RuntimeError def expire(self, key): raise RuntimeError +class CacheObject( object ): + """ + Cache object class, containing one stored record. + """ + + def __init__(self, key, data, lifetime=0, creation=None): + self.key = key + self.data = data + self.lifetime = lifetime + self.creation = creation if creation is not None else time.time() + + def __len__(self): + return len(self.data) + + @property + def expired(self): + return (self.remaining == 0) + + @property + def remaining(self): + return max((self.creation + self.lifetime) - time.time(), 0) + diff --git a/tmdb3/cache_file.py b/tmdb3/cache_file.py index 2626805eb4c..319eff79e55 100644 --- a/tmdb3/cache_file.py +++ b/tmdb3/cache_file.py @@ -14,8 +14,41 @@ import json import time import os +import io + +from cStringIO import StringIO + +from tmdb_exceptions import * +from cache_engine import CacheEngine, CacheObject + +#################### +# Cache File Format +#------------------ +# cache version (2) unsigned short +# slot count (2) unsigned short +# slot 0: timestamp (8) double +# slot 0: lifetime (4) unsigned int +# slot 0: seek point (4) unsigned int +# slot 1: timestamp +# slot 1: lifetime index slots are IDd by their query date and +# slot 1: seek point are filled incrementally forwards. lifetime +# .... is how long after query date before the item +# .... expires, and seek point is the location of the +# slot N-2: timestamp start of data for that entry. 256 empty slots +# slot N-2: lifetime are pre-allocated, allowing fast updates. +# slot N-2: seek point when all slots are filled, the cache file is +# slot N-1: timestamp rewritten from scrach to add more slots. +# slot N-1: lifetime +# slot N-1: seek point +# block 1 (?) ASCII +# block 2 +# .... blocks are just simple ASCII text, generated +# .... as independent objects by the JSON encoder +# block N-2 +# block N-1 +# +#################### -from cache_engine import CacheEngine def _donothing(*args, **kwargs): pass @@ -96,20 +129,90 @@ def parse_filename(filename): # return path with temp directory prepended return os.path.expandvars(os.path.join('%TEMP%',filename)) + +class FileCacheObject( CacheObject ): + _struct = struct.Struct('dII') # double and two ints + # timestamp, lifetime, position + + @classmethod + def fromFile(cls, fd): + dat = cls._struct.unpack(fd.read(cls._struct.size)) + obj = cls(None, None, dat[1], dat[0]) + obj.position = dat[2] + return obj + + def __init__(self, *args, **kwargs): + self._key = None + self._data = None + self._size = None + self._buff = StringIO() + super(FileCacheObject, self).__init__(*args, **kwargs) + + @property + def size(self): + if self._size is None: + self._buff.seek(0,2) + size = self._buff.tell() + if size == 0: + if (self._key is None) or (self._data is None): + raise RuntimeError + json.dump([self.key, self.data], self._buff) + self._size = self._buff.tell() + self._size = size + return self._size + @size.setter + def size(self, value): self._size = value + + @property + def key(self): + if self._key is None: + try: + self._key, self._data = json.loads(self._buff.getvalue()) + except: + raise + return self._key + @key.setter + def key(self, value): self._key = value + + @property + def data(self): + if self._data is None: + self._key, self._data = json.loads(self._buff.getvalue()) + return self._data + @data.setter + def data(self, value): self._data = value + + def load(self, fd): + fd.seek(self.position) + self._buff.seek(0) + self._buff.write(fd.read(self.size)) + + def dumpslot(self, fd): + pos = fd.tell() + fd.write(self._struct.pack(self.creation, self.lifetime, self.position)) + + def dumpdata(self, fd): + self.size + fd.seek(self.position) + fd.write(self._buff.getvalue()) + + class FileEngine( CacheEngine ): """Simple file-backed engine.""" name = 'file' - __time_struct = struct.Struct('d') # double placed at start of file to - # check for updated data + _struct = struct.Struct('HH') # two shorts for version and count + _version = 2 def __init__(self, parent): super(FileEngine, self).__init__(parent) self.configure(None) - def configure(self, filename): + def configure(self, filename, preallocate=256): + self.preallocate = preallocate self.cachefile = filename - self.cacheage = 0 - self.cache = None + self.size = 0 + self.free = 0 + self.age = 0 def _init_cache(self): # only run this once @@ -123,7 +226,7 @@ def _init_cache(self): try: # attempt to read existing cache at filename # handle any errors that occur - self._read() + self._open('r+b') # seems to have read fine, make sure we have write access if not os.access(self.cachefile, os.W_OK): raise TMDBCacheWriteError(self.cachefile) @@ -131,9 +234,9 @@ def _init_cache(self): except IOError as e: if e.errno == errno.ENOENT: # file does not exist, create a new one - self.cache = {} try: - self._write() + self._open('w+b') + self._write([]) except IOError as e: if e.errno == errno.ENOENT: # directory does not exist @@ -151,71 +254,139 @@ def _init_cache(self): # let the unhandled error continue through raise - def _open(self, mode='r'): + def get(self, date): + self._init_cache() + self._open('r+b') + + with Flock(self.cachefd, Flock.LOCK_SH): # lock for shared access + # return any new objects in the cache + return self._read(date) + + def put(self, key, value, lifetime): + self._init_cache() + self._open('r+b') + + with Flock(self.cachefd, Flock.LOCK_EX): # lock for exclusive access + newobjs = self._read(self.age) + newobjs.append(FileCacheObject(key, value, lifetime)) + + # this will cause a new file object to be opened with the proper + # access mode, however the Flock should keep the old object open + # and properly locked + self._open('r+b') + self._write(newobjs) + return newobjs + + def _open(self, mode='r+b'): # enforce binary operation - mode += 'b' try: if self.cachefd.mode == mode: # already opened in requested mode, nothing to do self.cachefd.seek(0) return except: pass # catch issue of no cachefile yet opened - self.cachefd = open(self.cachefile, mode) + self.cachefd = io.open(self.cachefile, mode) + def _read(self, date): + try: + self.cachefd.seek(0) + version, count = self._struct.unpack(\ + self.cachefd.read(self._struct.size)) + if version != self._version: + # old version, break out and well rewrite when finished + raise Exception - def _read(self): - self._init_cache() - self._open('r') - - with Flock(self.cachefd, Flock.LOCK_SH): # lock for shared access - try: - age = self.__time_struct.unpack(self.cachefd.read(8)) - except: - # failed to read age, ignore and we'll clean up when - # it gets rewritten - if self.cache is None: - self.cache = {} - return - - if self.cacheage >= age: - # local copy is sufficiently new, no need to read - return - # read remaining data from file - self.cacheage = age - self.cache = json.load(self.cachefd) + self.size = count + cache = [] + while count: + # loop through storage definitions + obj = FileCacheObject.fromFile(self.cachefd) + cache.append(obj) + count -= 1 - def _write(self): - self._init_cache() - # WARNING: this does no testing to ensure this instance has the newest - # copy of the file cache - self._open('w') - # the slight delay between truncating the file with 'w' and flocking - # could cause problems with another instance simultaneously trying to - # read the timestamp - with Flock(self.cachefd, Flock.LOCK_EX): # lock for exclusive access - # filter expired data from cache - # running this while flocked means the file is locked for additional - # time, however we do not want anyone else writing to the file - # before we write our stuff - self.expire() - self.cacheage = time.time() - self.cachefd.write(self.__time_struct.pack(self.cacheage)) - json.dump(self.cache, self.cachefd) - - def get(self, key): - self._read() - if (key in self.cache) and (time.time() < self.cache[key][0]): - return self.cache[key][1] - return None + except: + # failed to read information, so just discard it and return empty + self.size = 0 + self.free = 0 + return [] + + # get end of file + self.cachefd.seek(0,2) + position = self.cachefd.tell() + newobjs = [] + emptycount = 0 + + # walk backward through all, collecting new content and populating size + while len(cache): + obj = cache.pop() + if obj.creation == 0: + # unused slot, skip + emptycount += 1 + elif obj.expired: + # object has passed expiration date, no sense processing + continue + elif obj.creation > date: + # used slot with new data, process + obj.size, position = position - obj.position, obj.position + newobjs.append(obj) + # update age + self.age = max(self.age, obj.creation) + elif len(newobjs): + # end of new data, break + break + + # walk forward and load new content + for obj in newobjs: + obj.load(self.cachefd) + + self.free = emptycount + return newobjs + + def _write(self, data): + if self.free and (self.size != self.free): + # we only care about the last data point, since the rest are + # already stored in the file + data = data[-1] + + # determine write position of data in cache + self.cachefd.seek(0,2) + end = self.cachefd.tell() + data.position = end + + # write incremental update to free slot + self.cachefd.seek(4 + 16*(self.size-self.free)) + data.dumpslot(self.cachefd) + data.dumpdata(self.cachefd) + + else: + # rewrite cache file from scratch + # pull data from parent cache + data.extend(self.parent()._data.values()) + data.sort(key=lambda x: x.creation) + # write header + size = len(data) + self.preallocate + self.cachefd.seek(0) + self.cachefd.truncate() + self.cachefd.write(self._struct.pack(self._version, size)) + # write storage slot definitions + prev = None + for d in data: + if prev == None: + d.position = 4 + 16*size + else: + d.position = prev.position + prev.size + d.dumpslot(self.cachefd) + prev = d + # fill in allocated slots + for i in range(2**8): + self.cachefd.write(FileCacheObject._struct.pack(0, 0, 0)) + # write stored data + for d in data: + d.dumpdata(self.cachefd) + + self.cachefd.flush() + + def expire(self, key): + pass - def put(self, key, value, lifetime): - self._read() - self.cache[key] = (time.time()+lifetime, value) - self._write() - - def expire(self, key=None): - t = time.time() - for k,v in self.cache.items(): - if v[0] < t: # expiration has passed - del self.cache[k] diff --git a/tmdb3/cache_null.py b/tmdb3/cache_null.py index ae2f2b966e6..a59741c4ec7 100644 --- a/tmdb3/cache_null.py +++ b/tmdb3/cache_null.py @@ -13,7 +13,7 @@ class NullEngine( CacheEngine ): """Non-caching engine for debugging.""" name = 'null' def configure(self): pass - def get(self, key): return None - def put(self, key, value, lifetime): pass + def get(self, date): return [] + def put(self, key, value, lifetime): return [] def expire(self, key): pass diff --git a/tmdb3/tmdb_api.py b/tmdb3/tmdb_api.py index aec637a7682..5be5df05f2d 100644 --- a/tmdb3/tmdb_api.py +++ b/tmdb3/tmdb_api.py @@ -22,7 +22,7 @@ Preliminary API specifications can be found at http://help.themoviedb.org/kb/api/about-3""" -__version__="v0.4.6" +__version__="v0.5.0" # 0.1.0 Initial development # 0.2.0 Add caching mechanism for API queries # 0.2.1 Temporary work around for broken search paging @@ -41,6 +41,7 @@ # 0.4.4 Add support for additional Studio information # 0.4.5 Add locale fallthrough for images and alternate titles # 0.4.6 Add slice support for search results +# 0.5.0 Rework cache framework and improve file cache performance from request import set_key, Request from util import Datapoint, Datalist, Datadict, Element From 500454560030795a7c914cb985c30711e302d65a Mon Sep 17 00:00:00 2001 From: Raymond Wagner Date: Sat, 14 Apr 2012 15:30:26 -0400 Subject: [PATCH 16/69] Corrects handling of '0' for cast ordering. --- tmdb3/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tmdb3/util.py b/tmdb3/util.py index 4d6939b7972..29e07519a9c 100644 --- a/tmdb3/util.py +++ b/tmdb3/util.py @@ -124,7 +124,7 @@ def __get__(self, inst, owner): return inst._data[self.field] def __set__(self, inst, value): - if value: + if value is not None: value = self.handler(value) else: value = self.default From 3debcd531d8ed454b8ed9f4b61da6e03690ce13c Mon Sep 17 00:00:00 2001 From: Raymond Wagner Date: Sat, 14 Apr 2012 17:31:20 -0400 Subject: [PATCH 17/69] Add basic session handling and passthrough. --- tmdb3/tmdb_api.py | 23 ++++++++ tmdb3/tmdb_auth.py | 130 +++++++++++++++++++++++++++++++++++++++++++++ tmdb3/util.py | 9 ++++ 3 files changed, 162 insertions(+) create mode 100644 tmdb3/tmdb_auth.py diff --git a/tmdb3/tmdb_api.py b/tmdb3/tmdb_api.py index 5be5df05f2d..72fbffcbfce 100644 --- a/tmdb3/tmdb_api.py +++ b/tmdb3/tmdb_api.py @@ -47,6 +47,7 @@ from util import Datapoint, Datalist, Datadict, Element from pager import PagedRequest from locales import get_locale, set_locale +from tmdb_auth import get_session, set_session from tmdb_exceptions import * import json @@ -62,6 +63,28 @@ def _populate(self): return Request('configuration') Configuration = Configuration() +class Account( Element ): + session = Datapoint('session', initarg=1, default=None) + + def _populate_account(self): + if self.session is None: + self.session = get_session() + return Request('account', session_id=self.session.sessionid) + + id = Datapoint('id', poller=_populate_account) + adult = Datapoint('include_adult', poller=_populate_account) + country = Datapoint('iso_3166_1', poller=_populate_account) + language = Datapoint('iso_639_1', poller=_populate_account) + name = Datapoint('name', poller=_populate_account) + username = Datapoint('username', poller=_populate_account) + + @property + def locale(self): + return get_locale(self.language, self.country) + + def __repr__(self): + return "<{0} {1.name}>".format(self.__class__.__name__, self) + def searchMovie(query, locale=None, adult=False): return MovieSearchResult( Request('search/movie', query=query, include_adult=adult), diff --git a/tmdb3/tmdb_auth.py b/tmdb3/tmdb_auth.py new file mode 100644 index 00000000000..39f99256769 --- /dev/null +++ b/tmdb3/tmdb_auth.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +#----------------------- +# Name: tmdb_auth.py +# Python Library +# Author: Raymond Wagner +# Purpose: Provide authentication and session services for +# calls against the TMDB v3 API +#----------------------- + +from datetime import datetime as _pydatetime, \ + tzinfo as _pytzinfo +import re +class datetime( _pydatetime ): + """Customized datetime class with ISO format parsing.""" + _reiso = re.compile('(?P[0-9]{4})' + '-(?P[0-9]{1,2})' + '-(?P[0-9]{1,2})' + '.' + '(?P[0-9]{2})' + ':(?P[0-9]{2})' + '(:(?P[0-9]{2}))?' + '(?PZ|' + '(?P[-+])' + '(?P[0-9]{1,2})' + '(:)?' + '(?P[0-9]{2})?' + ')?') + + class _tzinfo( _pytzinfo): + def __init__(self, direc='+', hr=0, min=0): + if direc == '-': + hr = -1*int(hr) + self._offset = timedelta(hours=int(hr), minutes=int(min)) + def utcoffset(self, dt): return self._offset + def tzname(self, dt): return '' + def dst(self, dt): return timedelta(0) + + @classmethod + def fromIso(cls, isotime, sep='T'): + match = cls._reiso.match(isotime) + if match is None: + raise TypeError("time data '%s' does not match ISO 8601 format" \ + % isotime) + + dt = [int(a) for a in match.groups()[:5]] + if match.group('sec') is not None: + dt.append(int(match.group('sec'))) + else: + dt.append(0) + if match.group('tz'): + if match.group('tz') == 'Z': + tz = cls._tzinfo() + elif match.group('tzmin'): + tz = cls._tzinfo(*match.group('tzdirec','tzhour','tzmin')) + else: + tz = cls._tzinfo(*match.group('tzdirec','tzhour')) + dt.append(0) + dt.append(tz) + return cls(*dt) + +from request import Request + +syssession = None + +def set_session(sessionid): + global syssession + syssession = Session(sessionid) + +def get_session(sessionid=None): + global syssession + if sessionid: + return Session(sessionid) + elif syssession is not None: + return syssession + else: + return Session.new() + +class Session( object ): + + @classmethod + def new(cls): + return cls(None) + + def __init__(self, sessionid): + self.sessionid = sessionid + + @property + def sessionid(self): + if self._sessionid is None: + if self._authtoken is None: + raise RuntimeError("No Auth Token to produce Session for") + # TODO: check authtokenexpiration against current time + req = Request('authentication/session/new', \ + request_token=self._authtoken) + req.lifetime = 0 + dat = req.readJSON() + if not dat['success']: + raise RuntimeError("Session generation failed") + self._sessionid = dat['session_id'] + return self._sessionid + + @sessionid.setter + def sessionid(self, value): + self._sessionid = value + self._authtoken = None + self._authtokenexpiration = None + if value is None: + self.authenticated = False + else: + self.authenticated = True + + @property + def authtoken(self): + if self.authenticated: + raise RuntimeError("Session is already authenticated") + if self._authtoken is None: + req = Request('authentication/token/new') + req.lifetime = 0 + dat = req.readJSON() + if not dat['success']: + raise RuntimeError("Auth Token request failed") + self._authtoken = dat['request_token'] + self._authtokenexpiration = datetime.fromIso(dat['expires_at']) + return self._authtoken + + @property + def callbackurl(self): + return "http://www.themoviedb.org/authenticate/"+self._authtoken + diff --git a/tmdb3/util.py b/tmdb3/util.py index 29e07519a9c..f97f9ea62d2 100644 --- a/tmdb3/util.py +++ b/tmdb3/util.py @@ -8,6 +8,7 @@ from copy import copy from locales import get_locale +from tmdb_auth import get_session class Poller( object ): """ @@ -130,6 +131,7 @@ def __set__(self, inst, value): value = self.default if isinstance(value, Element): value._locale = inst._locale + value._session = inst._session inst._data[self.field] = value def sethandler(self, handler): @@ -179,6 +181,7 @@ def __set__(self, inst, value): val = self.handler(val) if isinstance(val, Element): val._locale = inst._locale + val._session = inst._session data.append(val) if self.sort: if self.sort is True: @@ -232,6 +235,7 @@ def __set__(self, inst, value): val = self.handler(val) if isinstance(val, Element): val._locale = inst._locale + val._session = inst._session data[self.getkey(val)] = val inst._data[self.field] = data @@ -313,6 +317,11 @@ def __call__(cls, *args, **kwargs): else: obj._locale = get_locale() + if 'session' in kwargs: + obj._session = kwargs['session'] + else: + obj._session = get_session() + obj._data = {} if 'raw' in kwargs: # if 'raw' keyword is supplied, create populate object manually From fb7c7cd3dfd4692e6c5acc61f0d2fd927b04d50e Mon Sep 17 00:00:00 2001 From: Raymond Wagner Date: Sat, 14 Apr 2012 18:26:59 -0400 Subject: [PATCH 18/69] Add assorted authenticated methods. This adds methods for setting ratings and favorite status against movies, as well as pulling lists of movies for which those have been set. This adds a bypass in the cache to not operate on anything with a lifetime of '0'. --- tmdb3/cache.py | 2 ++ tmdb3/request.py | 6 +++++ tmdb3/tmdb_api.py | 61 +++++++++++++++++++++++++++++++++++----------- tmdb3/tmdb_auth.py | 9 ++++--- 4 files changed, 60 insertions(+), 18 deletions(-) diff --git a/tmdb3/cache.py b/tmdb3/cache.py index 83d81f87136..9e124eae7a0 100644 --- a/tmdb3/cache.py +++ b/tmdb3/cache.py @@ -102,6 +102,8 @@ def __call__(self, *args, **kwargs): raise TMDBCacheError('Cache.Cached must be provided a '+\ 'callable object.') return self.__class__(self.cache, self.callback, args[0]) + elif self.inst.lifetime == 0: + return self.func(*args, **kwargs) else: key = self.callback() data = self.cache.get(key) diff --git a/tmdb3/request.py b/tmdb3/request.py index b074d22ca50..c92806f49ce 100644 --- a/tmdb3/request.py +++ b/tmdb3/request.py @@ -81,11 +81,17 @@ def new(self, **kwargs): obj.lifetime = self.lifetime return obj + def add_data(self, data): + """Provide data to be sent with POST.""" + urllib2.Request.add_data(self, urlencode(data)) + def open(self): """Open a file object to the specified URL.""" try: if DEBUG: print 'loading '+self.get_full_url() + if self.has_data(): + print ' '+self.get_data() return urllib2.urlopen(self) except urllib2.HTTPError, e: raise TMDBHTTPError(str(e)) diff --git a/tmdb3/tmdb_api.py b/tmdb3/tmdb_api.py index 72fbffcbfce..70d4c0a7220 100644 --- a/tmdb3/tmdb_api.py +++ b/tmdb3/tmdb_api.py @@ -22,7 +22,7 @@ Preliminary API specifications can be found at http://help.themoviedb.org/kb/api/about-3""" -__version__="v0.5.0" +__version__="v0.6.0" # 0.1.0 Initial development # 0.2.0 Add caching mechanism for API queries # 0.2.1 Temporary work around for broken search paging @@ -42,6 +42,7 @@ # 0.4.5 Add locale fallthrough for images and alternate titles # 0.4.6 Add slice support for search results # 0.5.0 Rework cache framework and improve file cache performance +# 0.6.0 Add user authentication support from request import set_key, Request from util import Datapoint, Datalist, Datadict, Element @@ -64,26 +65,22 @@ def _populate(self): Configuration = Configuration() class Account( Element ): - session = Datapoint('session', initarg=1, default=None) - - def _populate_account(self): - if self.session is None: - self.session = get_session() - return Request('account', session_id=self.session.sessionid) + def _populate(self): + return Request('account', session_id=self._session.sessionid) - id = Datapoint('id', poller=_populate_account) - adult = Datapoint('include_adult', poller=_populate_account) - country = Datapoint('iso_3166_1', poller=_populate_account) - language = Datapoint('iso_639_1', poller=_populate_account) - name = Datapoint('name', poller=_populate_account) - username = Datapoint('username', poller=_populate_account) + id = Datapoint('id') + adult = Datapoint('include_adult') + country = Datapoint('iso_3166_1') + language = Datapoint('iso_639_1') + name = Datapoint('name') + username = Datapoint('username') @property def locale(self): return get_locale(self.language, self.country) def __repr__(self): - return "<{0} {1.name}>".format(self.__class__.__name__, self) + return '<{0} "{1.name}">'.format(self.__class__.__name__, self) def searchMovie(query, locale=None, adult=False): return MovieSearchResult( @@ -341,6 +338,28 @@ def toprated(cls, locale=None): res._name = 'Top Rated' return res + @classmethod + def favorites(cls, session=None): + if session is None: + session = get_session() + account = Account(session=session) + res = MovieSearchResult( + Request('account/{0}/favorite_movies'.format(account.id), + session_id=session.sessionid)) + res._name = "Favorites" + return res + + @classmethod + def ratedmovies(cls, session=None): + if session is None: + session = get_session() + account = Account(session=session) + res = MovieSearchResult( + Request('account/{0}/rated_movies'.format(account.id), + session_id=session.sessionid)) + res._name = "Movies You Rated" + return res + @classmethod def fromIMDB(cls, imdbid, locale=None): try: @@ -414,6 +433,20 @@ def _populate_translations(self): apple_trailers = Datalist('quicktime', handler=AppleTrailer, poller=_populate_trailers) translations = Datalist('translations', handler=Translation, poller=_populate_translations) + def setFavorite(self, value): + req = Request('account/{0}/favorite'.format(Account(session=self._session).id), session_id=self._session.sessionid) + req.add_data({'movie_id':self.id, 'favorite':str(bool(value)).lower()}) + req.lifetime = 0 + req.readJSON() + + def setRating(self, value): + if not (0 <= value <= 10): + raise TMDBError("Ratings must be between '0' and '10'.") + req = Request('movie/{0}/favorite'.format(self.id), session_id=self._session.sessionid) + req.lifetime = 0 + req.add_data({'value':value}) + req.readJSON() + def __repr__(self): if self.title is not None: s = u"'{0}'".format(self.title) diff --git a/tmdb3/tmdb_auth.py b/tmdb3/tmdb_auth.py index 39f99256769..8583b990c58 100644 --- a/tmdb3/tmdb_auth.py +++ b/tmdb3/tmdb_auth.py @@ -60,6 +60,7 @@ def fromIso(cls, isotime, sep='T'): return cls(*dt) from request import Request +from tmdb_exceptions import * syssession = None @@ -89,14 +90,14 @@ def __init__(self, sessionid): def sessionid(self): if self._sessionid is None: if self._authtoken is None: - raise RuntimeError("No Auth Token to produce Session for") + raise TMDBError("No Auth Token to produce Session for") # TODO: check authtokenexpiration against current time req = Request('authentication/session/new', \ request_token=self._authtoken) req.lifetime = 0 dat = req.readJSON() if not dat['success']: - raise RuntimeError("Session generation failed") + raise TMDBError("Session generation failed") self._sessionid = dat['session_id'] return self._sessionid @@ -113,13 +114,13 @@ def sessionid(self, value): @property def authtoken(self): if self.authenticated: - raise RuntimeError("Session is already authenticated") + raise TMDBError("Session is already authenticated") if self._authtoken is None: req = Request('authentication/token/new') req.lifetime = 0 dat = req.readJSON() if not dat['success']: - raise RuntimeError("Auth Token request failed") + raise TMDBError("Auth Token request failed") self._authtoken = dat['request_token'] self._authtokenexpiration = datetime.fromIso(dat['expires_at']) return self._authtoken From 48d84958a93e94e97591f0f239902fecfca8607f Mon Sep 17 00:00:00 2001 From: Jean-Yves Avenard Date: Thu, 26 Apr 2012 12:26:58 +1000 Subject: [PATCH 19/69] Add ability to override Bonjour's service name. This is a work around a more complex issue. By default, Bonjour would automatically detect the computer name and use it to advertise the server. From Bonjour man-page: "If non-NULL, specifies the service name to be registered. Most applications will not specify a name, in which case the computer name is used (this name is communicated to the client via the callback). If a name is specified, it must be 1-63 bytes of UTF-8 text. If the name is longer than 63 bytes it will be automatically truncated to a legal length" However, for some people with broken DNS configuration, they can't resolve that name or it resolves improperly. So add a BonjourHostname setting to override the name used by Bonjour. Use it as an extra argument: -O BonjourHostname=mycomputer. --- mythtv/libs/libmythbase/bonjourregister.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mythtv/libs/libmythbase/bonjourregister.cpp b/mythtv/libs/libmythbase/bonjourregister.cpp index 017ee6d7521..7f9873261b2 100644 --- a/mythtv/libs/libmythbase/bonjourregister.cpp +++ b/mythtv/libs/libmythbase/bonjourregister.cpp @@ -5,6 +5,7 @@ #include "mythlogging.h" #include "bonjourregister.h" +#include "mythcorecontext.h" #define LOC QString("Bonjour: ") @@ -41,11 +42,13 @@ bool BonjourRegister::Register(uint16_t port, const QByteArray &type, return true; } + QByteArray host(gCoreContext->GetSetting("BonjourHostname", "").toUtf8()); + const char *host_ptr = host.size() > 0 ? host.constData() : NULL; uint16_t qport = qToBigEndian(port); DNSServiceErrorType res = DNSServiceRegister(&m_dnssref, 0, 0, (const char*)name.data(), (const char*)type.data(), - NULL, 0, qport, txt.size(), (void*)txt.data(), + NULL, host_ptr, qport, txt.size(), (void*)txt.data(), BonjourCallback, this); if (kDNSServiceErr_NoError != res) From f2e29bb1102d1f77e514cd7b0f8f87fad457f285 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Thu, 26 Apr 2012 23:13:34 +0100 Subject: [PATCH 20/69] Reduce the 'blank' visualiser framerate to 1fps as a temporary fix for the ridiculously high CPU usage of a visualiser than should use next to no CPU at all. --- mythplugins/mythmusic/mythmusic/visualize.cpp | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/mythplugins/mythmusic/mythmusic/visualize.cpp b/mythplugins/mythmusic/mythmusic/visualize.cpp index 66b050c79a5..fac7989ec04 100644 --- a/mythplugins/mythmusic/mythmusic/visualize.cpp +++ b/mythplugins/mythmusic/mythmusic/visualize.cpp @@ -168,11 +168,11 @@ bool StereoScope::process( VisualNode *node ) bool allZero = true; - if (node) + if (node) { double index = 0; double const step = (double)SAMPLES_DEFAULT_SIZE / size.width(); - for ( int i = 0; i < size.width(); i++) + for ( int i = 0; i < size.width(); i++) { unsigned long indexTo = (unsigned long)(index + step); if (indexTo == (unsigned long)(index)) @@ -194,13 +194,13 @@ bool StereoScope::process( VisualNode *node ) if ( valL < 0. ) valL = 0.; } - if (valR < 0.) + if (valR < 0.) { valR += falloff; if ( valR > 0. ) valR = 0.; - } - else + } + else { valR -= falloff; if ( valR < 0. ) @@ -208,7 +208,7 @@ bool StereoScope::process( VisualNode *node ) } } #endif - for (unsigned long s = (unsigned long)index; s < indexTo && s < node->length; s++) + for (unsigned long s = (unsigned long)index; s < indexTo && s < node->length; s++) { double tmpL = ( ( node->left ? double( node->left[s] ) : 0.) * @@ -235,10 +235,10 @@ bool StereoScope::process( VisualNode *node ) index = index + step; } #if RUBBERBAND - } - else if (rubberband) + } + else if (rubberband) { - for ( int i = 0; i < size.width(); i++) + for ( int i = 0; i < size.width(); i++) { double valL = magnitudes[ i ]; if (valL < 0) { @@ -271,8 +271,8 @@ bool StereoScope::process( VisualNode *node ) magnitudes[ i + size.width() ] = valR; } #endif - } - else + } + else { for ( int i = 0; (unsigned) i < magnitudes.size(); i++ ) magnitudes[ i ] = 0.; @@ -284,7 +284,7 @@ bool StereoScope::process( VisualNode *node ) bool StereoScope::draw( QPainter *p, const QColor &back ) { p->fillRect(0, 0, size.width(), size.height(), back); - for ( int i = 1; i < size.width(); i++ ) + for ( int i = 1; i < size.width(); i++ ) { #if TWOCOLOUR double r, g, b, per; @@ -611,7 +611,7 @@ Spectrum::Spectrum() rplan = fftw_plan_dft_r2c_1d(FFTW_N, rin, (myth_fftw_complex_cast*)rout, FFTW_MEASURE); startColor = QColor(0,0,255); - targetColor = QColor(255,0,0); + targetColor = QColor(255,0,0); } Spectrum::~Spectrum() @@ -679,7 +679,7 @@ bool Spectrum::process(VisualNode *node) double *magnitudesp = magnitudes.data(); double magL, magR, tmp; - if (node) + if (node) { i = node->length; if (i > FFTW_N) @@ -700,9 +700,9 @@ bool Spectrum::process(VisualNode *node) for (i = 0; (int)i < rects.size(); i++, w += analyzerBarWidth) { - magL = (log(sq(real(lout[index])) + sq(real(lout[FFTW_N - index]))) - 22.0) * + magL = (log(sq(real(lout[index])) + sq(real(lout[FFTW_N - index]))) - 22.0) * scaleFactor; - magR = (log(sq(real(rout[index])) + sq(real(rout[FFTW_N - index]))) - 22.0) * + magR = (log(sq(real(rout[index])) + sq(real(rout[FFTW_N - index]))) - 22.0) * scaleFactor; if (magL > size.height() / 2) @@ -784,11 +784,11 @@ bool Spectrum::draw(QPainter *p, const QColor &back) per = clamp(per, 1.0, 0.0); - r = startColor.red() + + r = startColor.red() + (targetColor.red() - startColor.red()) * (per * per); - g = startColor.green() + + g = startColor.green() + (targetColor.green() - startColor.green()) * (per * per); - b = startColor.blue() + + b = startColor.blue() + (targetColor.blue() - startColor.blue()) * (per * per); r = clamp(r, 255.0, 0.0); @@ -848,14 +848,14 @@ void Squares::resize (const QSize &newsize) { size = newsize; } -void Squares::drawRect(QPainter *p, QRect *rect, int i, int c, int w, int h) +void Squares::drawRect(QPainter *p, QRect *rect, int i, int c, int w, int h) { double r, g, b, per; int correction = (size.width() % rects.size ()) / 2; int x = ((i / 2) * w) + correction; int y; - if (i % 2 == 0) + if (i % 2 == 0) { y = c - h; per = double(fake_height - rect->top()) / double(fake_height); @@ -866,15 +866,15 @@ void Squares::drawRect(QPainter *p, QRect *rect, int i, int c, int w, int h) per = double(rect->bottom()) / double(fake_height); } - per = clamp(per, 1.0, 0.0); - - r = startColor.red() + + per = clamp(per, 1.0, 0.0); + + r = startColor.red() + (targetColor.red() - startColor.red()) * (per * per); - g = startColor.green() + + g = startColor.green() + (targetColor.green() - startColor.green()) * (per * per); - b = startColor.blue() + + b = startColor.blue() + (targetColor.blue() - startColor.blue()) * (per * per); - + r = clamp(r, 255.0, 0.0); g = clamp(g, 255.0, 0.0); b = clamp(b, 255.0, 0.0); @@ -1496,7 +1496,7 @@ void AlbumArt::handleKeyPress(const QString &action) /// this is the time an image is shown in the albumart visualizer #define ALBUMARTCYCLETIME 10 -bool AlbumArt::needsUpdate() +bool AlbumArt::needsUpdate() { // if the track has changed we need to update the image if (gPlayer->getCurrentMetadata() && m_currentMetadata != gPlayer->getCurrentMetadata()) @@ -1536,7 +1536,7 @@ bool AlbumArt::draw(QPainter *p, const QColor &back) } } - if (m_image.isNull()) + if (m_image.isNull()) { drawWarning(p, back, m_size, QObject::tr("?"), 100); return true; @@ -1581,7 +1581,7 @@ static class AlbumArtFactory : public VisFactory Blank::Blank() : VisualBase(true) { - m_fps = 20; + m_fps = 1; } Blank::~Blank() From 055a511791aa01dcbeb9743b5db9f242d0f9b517 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Thu, 26 Apr 2012 23:23:53 +0100 Subject: [PATCH 21/69] Fix naming issue spotted by 'dekarl'. The channel icon storage group is called 'ChannelIcons' not 'ChannelIcon', the icon lookups would still work because of some fallback checks but they would be slower. --- mythtv/libs/libmythtv/osd.cpp | 2 +- mythtv/programs/mythfrontend/guidegrid.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mythtv/libs/libmythtv/osd.cpp b/mythtv/libs/libmythtv/osd.cpp index 03f3072b20a..d72d5b03c8f 100644 --- a/mythtv/libs/libmythtv/osd.cpp +++ b/mythtv/libs/libmythtv/osd.cpp @@ -440,7 +440,7 @@ void OSD::SetText(const QString &window, QHash &map, if (!iconpath.isEmpty()) { QString iconurl = - gCoreContext->GetMasterHostPrefix("ChannelIcon", + gCoreContext->GetMasterHostPrefix("ChannelIcons", iconpath); icon->SetFilename(iconurl); diff --git a/mythtv/programs/mythfrontend/guidegrid.cpp b/mythtv/programs/mythfrontend/guidegrid.cpp index 3fa00e3ad89..0fa7768a2ff 100644 --- a/mythtv/programs/mythfrontend/guidegrid.cpp +++ b/mythtv/programs/mythfrontend/guidegrid.cpp @@ -1578,7 +1578,7 @@ void GuideGrid::updateChannels(void) if (!chinfo->icon.isEmpty()) { QString iconurl = - gCoreContext->GetMasterHostPrefix("ChannelIcon", + gCoreContext->GetMasterHostPrefix("ChannelIcons", chinfo->icon); item->SetImage(iconurl, "channelicon"); } @@ -1612,7 +1612,7 @@ void GuideGrid::updateInfo(void) m_channelImage->Reset(); if (!chinfo->icon.isEmpty()) { - QString iconurl = gCoreContext->GetMasterHostPrefix("ChannelIcon", + QString iconurl = gCoreContext->GetMasterHostPrefix("ChannelIcons", chinfo->icon); m_channelImage->SetFilename(iconurl); From ca11a909d531ad6032c01d248d0cd8a8f68d7067 Mon Sep 17 00:00:00 2001 From: Raymond Wagner Date: Sat, 28 Apr 2012 16:17:24 -0400 Subject: [PATCH 22/69] Correct FreeSpace class broken by 1508085eb --- mythtv/bindings/python/MythTV/mythproto.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mythtv/bindings/python/MythTV/mythproto.py b/mythtv/bindings/python/MythTV/mythproto.py index d3264eae063..fc4ba24d3fc 100644 --- a/mythtv/bindings/python/MythTV/mythproto.py +++ b/mythtv/bindings/python/MythTV/mythproto.py @@ -794,7 +794,7 @@ class FreeSpace( DictData ): _field_order = [ 'host', 'path', 'islocal', 'disknumber', 'sgroupid', 'blocksize', 'totalspace', 'usedspace'] - _field_type = [3, 3, 2, 0, 0, 0, 0, 0, 0, 0] + _field_type = [3, 3, 2, 0, 0, 0, 0, 0] def __str__(self): return ""\ % (self.path, self.host, hex(id(self))) From b28c566ef2b6a6379821bcc9023f84cba04b4e05 Mon Sep 17 00:00:00 2001 From: Raymond Wagner Date: Mon, 30 Apr 2012 01:59:11 -0400 Subject: [PATCH 23/69] Add adult filter for people searches --- tmdb3/tmdb_api.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tmdb3/tmdb_api.py b/tmdb3/tmdb_api.py index 70d4c0a7220..fdedb3e2016 100644 --- a/tmdb3/tmdb_api.py +++ b/tmdb3/tmdb_api.py @@ -22,7 +22,7 @@ Preliminary API specifications can be found at http://help.themoviedb.org/kb/api/about-3""" -__version__="v0.6.0" +__version__="v0.6.1" # 0.1.0 Initial development # 0.2.0 Add caching mechanism for API queries # 0.2.1 Temporary work around for broken search paging @@ -43,6 +43,7 @@ # 0.4.6 Add slice support for search results # 0.5.0 Rework cache framework and improve file cache performance # 0.6.0 Add user authentication support +# 0.6.1 Add adult filtering for people searches from request import set_key, Request from util import Datapoint, Datalist, Datadict, Element @@ -101,8 +102,9 @@ def __repr__(self): name = self._name if self._name else self._request._kwargs['query'] return u"".format(name) -def searchPerson(query): - return PeopleSearchResult(Request('search/person', query=query)) +def searchPerson(query, adult=False): + return PeopleSearchResult(Request('search/person', query=query, + include_adult=adult)) class PeopleSearchResult( PagedRequest ): """Stores a list of search matches.""" From 361e06851f02f5a70f554fcee0890a7a37e6ad95 Mon Sep 17 00:00:00 2001 From: Raymond Wagner Date: Mon, 30 Apr 2012 02:12:12 -0400 Subject: [PATCH 24/69] Correct missed changes in __init__.py --- tmdb3/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tmdb3/__init__.py b/tmdb3/__init__.py index 76bc473c980..e5e9d23fd2f 100644 --- a/tmdb3/__init__.py +++ b/tmdb3/__init__.py @@ -4,6 +4,7 @@ Movie, Collection, __version__ from request import set_key, set_cache from locales import get_locale, set_locale -from cache import CacheEngine +from tmdb_auth import get_session, set_session +from cache_engine import CacheEngine from tmdb_exceptions import * From ef2c748f001a0fd560583cb3ddbe72bcc9b03fcd Mon Sep 17 00:00:00 2001 From: Raymond Wagner Date: Mon, 30 Apr 2012 02:14:03 -0400 Subject: [PATCH 25/69] Update version listed in python bindings egg. --- mythtv/bindings/python/setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mythtv/bindings/python/setup.py b/mythtv/bindings/python/setup.py index f4bdbe62c28..44dcc5fb9bd 100755 --- a/mythtv/bindings/python/setup.py +++ b/mythtv/bindings/python/setup.py @@ -78,7 +78,7 @@ def run(self): setup( name='MythTV', - version='0.24.0', + version='0.25.0', description='MythTV Python bindings.', long_description='Provides canned database and protocol access to the MythTV database, mythproto, mythxml, and frontend remote control.', packages=['MythTV', 'MythTV/tmdb', 'MythTV/tmdb3', 'MythTV/ttvdb', 'MythTV/wikiscripts'], From e778f23d0eeabf99be23121cebc190191aa7041f Mon Sep 17 00:00:00 2001 From: Gavin Hurlbut Date: Mon, 30 Apr 2012 09:43:07 -0700 Subject: [PATCH 26/69] Add more debug logs in the DataDirect and MythDLMgr code Refs #10662 Turns out the problem we are seeing is a known bug in Qt 4.7.1 that is fixed in 4.7.2 (and wasn't there before 4.7.1). In particular, if your Schedules Direct username is an email address, Qt4.7.1 stupidly puts the username as the part before the @ and the realm as the portion after the @, even though the realm was already provided. SO, if you have a Schedules Direct username that is an email address, either upgrade to 4.7.2 (or higher), or downgrade to 4.7.0 (or lower). --- mythtv/libs/libmythbase/mythdownloadmanager.cpp | 13 +++++++++++++ mythtv/libs/libmythtv/datadirect.cpp | 1 + 2 files changed, 14 insertions(+) diff --git a/mythtv/libs/libmythbase/mythdownloadmanager.cpp b/mythtv/libs/libmythbase/mythdownloadmanager.cpp index eba42b5e18b..0e4917404ae 100644 --- a/mythtv/libs/libmythbase/mythdownloadmanager.cpp +++ b/mythtv/libs/libmythbase/mythdownloadmanager.cpp @@ -768,7 +768,10 @@ void MythDownloadManager::authCallback(QNetworkReply *reply, return; if (dlInfo->m_authCallback) + { + LOG(VB_FILE, LOG_DEBUG, "Calling auth callback"); dlInfo->m_authCallback(reply, authenticator, dlInfo->m_authArg); + } } /** \brief Download helper for download() blocking methods. @@ -817,7 +820,11 @@ bool MythDownloadManager::downloadNow(MythDownloadInfo *dlInfo, bool deleteInfo) dlInfo->m_syncMode = false; // Let downloadFinished() cleanup for us if ((dlInfo->m_reply) && (dlInfo->m_errorCode == QNetworkReply::NoError)) + { + LOG(VB_FILE, LOG_DEBUG, + LOC + QString("Aborting download - lack of data transfer")); dlInfo->m_reply->abort(); + } } else if (deleteInfo) { @@ -847,7 +854,11 @@ void MythDownloadManager::cancelDownload(const QString &url) { // this shouldn't happen if (dlInfo->m_reply) + { + LOG(VB_FILE, LOG_DEBUG, + LOC + QString("Aborting download - user request")); dlInfo->m_reply->abort(); + } lit.remove(); delete dlInfo; dlInfo = NULL; @@ -859,6 +870,8 @@ void MythDownloadManager::cancelDownload(const QString &url) dlInfo = m_downloadInfos[url]; if (dlInfo->m_reply) { + LOG(VB_FILE, LOG_DEBUG, + LOC + QString("Aborting download - user request")); m_downloadReplies.remove(dlInfo->m_reply); dlInfo->m_reply->abort(); } diff --git a/mythtv/libs/libmythtv/datadirect.cpp b/mythtv/libs/libmythtv/datadirect.cpp index f4bb8383565..799b7a1148d 100644 --- a/mythtv/libs/libmythtv/datadirect.cpp +++ b/mythtv/libs/libmythtv/datadirect.cpp @@ -975,6 +975,7 @@ void authenticationCallback(QNetworkReply *reply, QAuthenticator *auth, void DataDirectProcessor::authenticationCallback(QNetworkReply *reply, QAuthenticator *auth) { + LOG(VB_FILE, LOG_DEBUG, "DataDirect auth callback"); (void)reply; auth->setUser(GetUserID()); auth->setPassword(GetPassword()); From bd883d7216b497d8193975e3b19cb7134eaff710 Mon Sep 17 00:00:00 2001 From: Jean-Yves Avenard Date: Mon, 30 Apr 2012 21:48:14 +1000 Subject: [PATCH 27/69] Almost complete rewrite of RAOP server Let me commend Mark Kendall for his previous implementation. As far as I could tell, his RAOP server was the only one implementing Airtunes v2 functionalities with a/v sync timestamps. The rewrite serves several purposes. Mainly, I wanted to take ownership of this area of the code, and there were things I couldn't figure out. Most likely because whatever Mark used for AirPort's technical documentation isn't what I got. Technical description of RAOP came mostly from: -http://git.zx2c4.com/Airtunes2/about -http://blog.technologeek.org/airtunes-v2 I found the later to be the most correct. Main area of focus: -Audio Quality: Over slow or poor network connectivity (e.g. slow wireless) and with lots of packet drops: audio would have been corrupted (the system played audio packets in the order they were received, and not in the order they were supposed to be). For testing purposes, I simulated a 30% packet drop, and playback remained perfect. -A/V Sync: Achieving perfect A/V sync across all platforms and with all the different type of audio architecture is almost impossible. However, I believe the results achieved are very good. Playback will automatically adjust itself according to the network latency and the audio hardware latency. New features: We now retrieve the media's metadata: Album name, artist name, song title and coverart. This only works when using iTunes. iOS device do not send metadata unless using FairPlay encryption. We only support RSA encryption. Currently, this information is only shown in the logs, but in the future we'll be able to nicely show them in mythfrontend. Additional credits: -http://nto.github.com/AirPlay.html, gave information about how to configure Bonjour in order to receive MetaData from iTunes -http://code.google.com/p/ytrack/wiki/DMAP : iTunes DMAP metadata structure --- mythtv/libs/libmythtv/mythraopconnection.cpp | 1262 +++++++++++++----- mythtv/libs/libmythtv/mythraopconnection.h | 141 +- mythtv/libs/libmythtv/mythraopdevice.cpp | 22 +- 3 files changed, 1068 insertions(+), 357 deletions(-) diff --git a/mythtv/libs/libmythtv/mythraopconnection.cpp b/mythtv/libs/libmythtv/mythraopconnection.cpp index df0b3a9c4be..c40dd3aad3a 100644 --- a/mythtv/libs/libmythtv/mythraopconnection.cpp +++ b/mythtv/libs/libmythtv/mythraopconnection.cpp @@ -2,13 +2,13 @@ #include #include #include +#include #include "mythlogging.h" #include "mythcorecontext.h" #include "mythdirs.h" #include "serverpool.h" -#include // for ntohs #include "audiooutput.h" #include "mythraopdevice.h" @@ -16,10 +16,27 @@ #define LOC QString("RAOP Conn: ") #define MAX_PACKET_SIZE 2048 -#define DEFAULT_SAMPLE_RATE 44100 RSA* MythRAOPConnection::g_rsa = NULL; +// RAOP RTP packet type +#define TIMING_REQUEST 0x52 +#define TIMING_RESPONSE 0x53 +#define SYNC 0x54 +#define FIRSTSYNC (0x54 | 0x80) +#define RANGE_RESEND 0x55 +#define AUDIO_RESEND 0x56 +#define AUDIO_DATA 0x60 +#define FIRSTAUDIO_DATA (0x60 | 0x80) + + +// Size (in ms) of audio buffered in audio card +#define AUDIOCARD_BUFFER 800 +// How frequently we may call ProcessAudio (via QTimer) +// ideally 20ms, but according to documentation +// anything lower than 50ms on windows, isn't reliable +#define AUDIO_BUFFER 100 + class NetStream : public QTextStream { public: @@ -38,15 +55,29 @@ class NetStream : public QTextStream MythRAOPConnection::MythRAOPConnection(QObject *parent, QTcpSocket *socket, QByteArray id, int port) - : QObject(parent), m_watchdogTimer(NULL), m_socket(socket), - m_textStream(NULL), m_hardwareId(id), - m_dataPort(port), m_dataSocket(NULL), - m_clientControlSocket(NULL), m_clientControlPort(0), - m_audio(NULL), m_codec(NULL), m_codeccontext(NULL), - m_sampleRate(DEFAULT_SAMPLE_RATE), m_queueLength(0), m_allowVolumeControl(true), - m_seenPacket(false), m_lastPacketSequence(0), m_lastPacketTimestamp(0), - m_lastSyncTime(0), m_lastSyncTimestamp(0), m_lastLatency(0), m_latencyAudio(0), - m_latencyQueued(0), m_latencyCounter(0), m_avSync(0), m_audioTimer(NULL) + : QObject(parent), m_watchdogTimer(NULL), m_socket(socket), + m_textStream(NULL), m_hardwareId(id), + m_incomingHeaders(), m_incomingContent(), m_incomingPartial(false), + m_incomingSize(0), + m_dataSocket(NULL), m_dataPort(port), + m_clientControlSocket(NULL), m_clientControlPort(0), + m_clientTimingSocket(NULL), m_clientTimingPort(0), + m_audio(NULL), m_codec(NULL), m_codeccontext(NULL), + m_channels(2), m_sampleSize(16), m_frameRate(44100), + m_framesPerPacket(352),m_dequeueAudioTimer(NULL), + m_queueLength(0), m_streamingStarted(false), + m_allowVolumeControl(true), + //audio sync + m_seqNum(0), + m_lastSequence(0), m_lastTimestamp(0), + m_currentTimestamp(0), m_nextSequence(0), m_nextTimestamp(0), + m_bufferLength(0), m_timeLastSync(0), + m_cardLatency(0), m_adjustedLatency(0), m_audioStarted(false), + // clock sync + m_masterTimeStamp(0), m_deviceTimeStamp(0), m_networkLatency(0), + m_clockSkew(0), + m_audioTimer(NULL), + m_progressStart(0), m_progressCurrent(0), m_progressEnd(0) { } @@ -57,9 +88,16 @@ MythRAOPConnection::~MythRAOPConnection() // stop and delete watchdog timer if (m_watchdogTimer) + { m_watchdogTimer->stop(); - delete m_watchdogTimer; - m_watchdogTimer = NULL; + delete m_watchdogTimer; + } + + if (m_dequeueAudioTimer) + { + m_dequeueAudioTimer->stop(); + delete m_dequeueAudioTimer; + } // delete main socket if (m_socket) @@ -67,7 +105,6 @@ MythRAOPConnection::~MythRAOPConnection() m_socket->close(); m_socket->deleteLater(); } - m_socket = NULL; // delete data socket if (m_dataSocket) @@ -76,7 +113,6 @@ MythRAOPConnection::~MythRAOPConnection() m_dataSocket->close(); m_dataSocket->deleteLater(); } - m_dataSocket = NULL; // client control socket if (m_clientControlSocket) @@ -85,16 +121,15 @@ MythRAOPConnection::~MythRAOPConnection() m_clientControlSocket->close(); m_clientControlSocket->deleteLater(); } - m_clientControlSocket = NULL; // close audio decoder DestroyDecoder(); + // free decoded audio buffer + ResetAudio(); + // close audio device CloseAudioDevice(); - - // free decoded audio buffer - ExpireAudio(UINT64_MAX); } bool MythRAOPConnection::Init(void) @@ -148,9 +183,16 @@ bool MythRAOPConnection::Init(void) connect(m_watchdogTimer, SIGNAL(timeout()), this, SLOT(timeout())); m_watchdogTimer->start(10000); + m_dequeueAudioTimer = new QTimer(); + connect(m_dequeueAudioTimer, SIGNAL(timeout()), this, SLOT(ProcessAudio())); + return true; } +/** + * Socket incoming data signal handler + * use for audio, control and timing socket + */ void MythRAOPConnection::udpDataReady(void) { QUdpSocket *socket = dynamic_cast(sender()); @@ -172,14 +214,6 @@ void MythRAOPConnection::udpDataReady(void) void MythRAOPConnection::udpDataReady(QByteArray buf, QHostAddress peer, quint16 port) { - // get the time of day - // NOTE: the previous code would perform this once, and loop internally - // since the new code loops externally, and this must get performed - // on each pass, there may be issues - timeval t; - gettimeofday(&t, NULL); - uint64_t timenow = (t.tv_sec * 1000) + (t.tv_usec / 1000); - // restart the idle timer if (m_watchdogTimer) m_watchdogTimer->start(10000); @@ -187,114 +221,185 @@ void MythRAOPConnection::udpDataReady(QByteArray buf, QHostAddress peer, if (!m_audio || !m_codec || !m_codeccontext) return; - unsigned int type = (unsigned int)((char)(buf[1] & ~0x80)); - - if (type == 0x54) - ProcessSyncPacket(buf, timenow); + uint8_t type; + uint16_t seq; + uint64_t timestamp; - if (!(type == 0x60 || type == 0x56)) + if (!GetPacketType(buf, type, seq, timestamp)) + { + LOG(VB_GENERAL, LOG_DEBUG, LOC + + QString("Packet doesn't start with valid Rtp Header (0x%1)") + .arg((uint8_t)buf[0], 0, 16)); return; + } + + switch (type) + { + case SYNC: + case FIRSTSYNC: + ProcessSync(buf); + ProcessAudio(); + return; + + case FIRSTAUDIO_DATA: + m_nextSequence = seq; + m_nextTimestamp = timestamp; + // With iTunes we know what the first sequence is going to be. + // iOS device do not tell us before streaming start what the first + // packet is going to be. + m_streamingStarted = true; + break; + + case AUDIO_DATA: + case AUDIO_RESEND: + break; + + case TIMING_RESPONSE: + ProcessTimeResponse(buf); + return; - int offset = type == 0x60 ? 0 : 4; - uint16_t this_sequence = ntohs(*(uint16_t *)(buf.data() + offset + 2)); - uint64_t this_timestamp = FramesToMs(ntohl(*(uint64_t*)(buf.data() + offset + 4))); - uint16_t expected_sequence = m_lastPacketSequence + 1; // should wrap ok + default: + LOG(VB_GENERAL, LOG_DEBUG, LOC + + QString("Packet type (0x%1) not handled") + .arg(type, 0, 16)); + return; + } + timestamp = framesToMs(timestamp); + if (timestamp < m_currentTimestamp) + { + LOG(VB_GENERAL, LOG_DEBUG, LOC + + QString("Received packet %1 too late, ignoring") + .arg(seq)); + return; + } // regular data packet - if (type == 0x60) + if (type == AUDIO_DATA || type == FIRSTAUDIO_DATA) { - if (m_seenPacket && (this_sequence != expected_sequence)) - SendResendRequest(timenow, expected_sequence, this_sequence); + if (m_streamingStarted && seq != m_nextSequence) + SendResendRequest(timestamp, m_nextSequence, seq); - // don't update the sequence for resends - m_seenPacket = true; - m_lastPacketSequence = this_sequence; - m_lastPacketTimestamp = this_timestamp; + m_nextSequence = seq + 1; + m_nextTimestamp = timestamp; + m_streamingStarted = true; } + if (!m_streamingStarted) + return; + // resent packet - if (type == 0x56) + if (type == AUDIO_RESEND) { - if (m_resends.contains(this_sequence)) + if (m_resends.contains(seq)) { - LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("Received required resend %1") - .arg(this_sequence)); - m_resends.remove(this_sequence); + LOG(VB_GENERAL, LOG_DEBUG, LOC + + QString("Received required resend %1 (with ts:%2 last:%3)") + .arg(seq).arg(timestamp).arg(m_nextSequence)); + m_resends.remove(seq); } else - LOG(VB_GENERAL, LOG_WARNING, LOC + QString("Received unexpected resent packet %1") - .arg(this_sequence)); + LOG(VB_GENERAL, LOG_WARNING, LOC + + QString("Received unexpected resent packet %1") + .arg(seq)); } - ExpireResendRequests(timenow); - - offset += 12; - char* data_in = buf.data() + offset; - int len = buf.size() - offset; - if (len < 16) + // Check that the audio packet is valid, do so by decoding it. If an error + // occurs, ask to resend it + QList *decoded = new QList(); + int numframes = decodeAudioPacket(type, &buf, decoded); + if (numframes < 0) + { + // an error occurred, ask for the audio packet once again. + LOG(VB_GENERAL, LOG_ERR, LOC + QString("Error decoding audio")); + SendResendRequest(timestamp, seq, seq+1); return; + } + AudioPacket frames; + frames.seq = seq; + frames.data = decoded; + m_audioQueue.insert(timestamp, frames); + ProcessAudio(); +} - int aeslen = len & ~0xf; - unsigned char iv[16]; - unsigned char decrypted_data[MAX_PACKET_SIZE]; - memcpy(iv, m_AESIV.data(), sizeof(iv)); - AES_cbc_encrypt((const unsigned char*)data_in, - decrypted_data, aeslen, - &m_aesKey, iv, AES_DECRYPT); - memcpy(decrypted_data + aeslen, data_in + aeslen, len - aeslen); +void MythRAOPConnection::ProcessSync(const QByteArray &buf) +{ + bool first = (uint8_t)buf[0] == 0x90; // First sync is 0x90,0x55 + const char *req = buf.constData(); + uint64_t current_ts = qFromBigEndian(*(uint32_t*)(req + 4)); + uint64_t next_ts = qFromBigEndian(*(uint32_t*)(req + 16)); - AVPacket tmp_pkt; - AVCodecContext *ctx = m_codeccontext; + uint64_t current = framesToMs(current_ts); + uint64_t next = framesToMs(next_ts); - av_init_packet(&tmp_pkt); - tmp_pkt.data = decrypted_data; - tmp_pkt.size = len; + m_currentTimestamp = current; + m_nextTimestamp = next; + m_bufferLength = m_nextTimestamp - m_currentTimestamp; - while (tmp_pkt.size > 0) + if (first) { - int decoded_data_size = AVCODEC_MAX_AUDIO_FRAME_SIZE; - int16_t *samples = (int16_t *)av_mallocz(AVCODEC_MAX_AUDIO_FRAME_SIZE); - int ret = avcodec_decode_audio3(ctx, samples, - &decoded_data_size, &tmp_pkt); + LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("Receiving first SYNC packet")); + } + else + { + LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("Receiving SYNC packet")); + } - if (ret < 0) - { - LOG(VB_GENERAL, LOG_ERR, LOC + QString("Error decoding audio")); - break; - } + timeval t; gettimeofday(&t, NULL); + m_timeLastSync = t.tv_sec * 1000 + t.tv_usec / 1000; - if (decoded_data_size > 0) - { - int frames = (ctx->channels <= 0 || decoded_data_size < 0) ? -1 : - decoded_data_size / - (ctx->channels * av_get_bits_per_sample_fmt(ctx->sample_fmt)>>3); - AudioFrame aframe; - aframe.samples = samples; - aframe.frames = frames; - aframe.size = decoded_data_size; + LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("SYNC: cur:%1 next:%2 time:%3") + .arg(m_currentTimestamp).arg(m_nextTimestamp).arg(m_timeLastSync)); - if (m_audioQueue.contains(this_timestamp)) - LOG(VB_GENERAL, LOG_WARNING, - LOC + "Duplicate packet timestamp."); + uint64_t delay = framesToMs(m_audioQueue.size() * m_framesPerPacket); + delay += m_networkLatency; - m_audioQueue.insert(this_timestamp, aframe); - m_queueLength += aframe.frames; - ProcessAudio(timenow); + // Calculate audio card latency + if (first) + { + m_cardLatency = AudioCardLatency(); + // if audio isn't started, start playing 200ms worth of silence + // and measure timestamp difference + LOG(VB_GENERAL, LOG_DEBUG, LOC + + QString("Audio hardware latency: %1ms").arg(m_cardLatency)); + } - this_timestamp += (frames * 1000) / m_sampleRate; - } + uint64_t audiots = m_audio->GetAudiotime(); + if (m_audioStarted) + { + m_adjustedLatency = (int64_t)audiots - (int64_t)m_currentTimestamp; + } + if (m_adjustedLatency > (int64_t)m_bufferLength) + { + // Too much delay in playback + // will reset audio card in next ProcessAudio + m_audioStarted = false; + m_adjustedLatency = 0; + } - tmp_pkt.data += ret; - tmp_pkt.size -= ret; + delay += m_audio->GetAudioBufferedTime(); + delay += m_adjustedLatency; + + // Expire old audio + ExpireResendRequests(m_currentTimestamp); + int res = ExpireAudio(m_currentTimestamp); + if (res > 0) + { + LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("Drop %1 packets").arg(res)); } -} -uint64_t MythRAOPConnection::FramesToMs(uint64_t timestamp) -{ - return (uint64_t)((double)timestamp * 1000.0 / m_sampleRate); + LOG(VB_GENERAL, LOG_DEBUG, LOC + + QString("Queue=%1 buffer=%2ms ideal=%3ms diffts:%4ms") + .arg(m_audioQueue.size()) + .arg(delay) + .arg(m_bufferLength) + .arg(m_adjustedLatency)); } -void MythRAOPConnection::SendResendRequest(uint64_t timenow, +/** + * SendResendRequest: + * Request RAOP client to resend missed RTP packets + */ +void MythRAOPConnection::SendResendRequest(uint64_t timestamp, uint16_t expected, uint16_t got) { if (!m_clientControlSocket) @@ -304,40 +409,47 @@ void MythRAOPConnection::SendResendRequest(uint64_t timenow, (int16_t)(((int32_t)got + UINT16_MAX + 1) - expected) : got - expected; - LOG(VB_GENERAL, LOG_INFO, LOC + QString("Missed %1 packet(s): expected %2 got %3") - .arg(missed).arg(expected).arg(got)); + LOG(VB_GENERAL, LOG_INFO, LOC + + QString("Missed %1 packet(s): expected %2 got %3 ts:%4") + .arg(missed).arg(expected).arg(got).arg(timestamp)); char req[8]; req[0] = 0x80; - req[1] = 0x55 | 0x80; - *(uint16_t *)(req + 2) = htons(1); - *(uint16_t *)(req + 4) = htons(expected); // missed seqnum - *(uint16_t *)(req + 6) = htons(missed); // count - - if (m_clientControlSocket->writeDatagram(req, 8, m_peerAddress, m_clientControlPort) == 8) + req[1] = RANGE_RESEND | 0x80; + *(uint16_t *)(req + 2) = qToBigEndian(m_seqNum++); + *(uint16_t *)(req + 4) = qToBigEndian(expected); // missed seqnum + *(uint16_t *)(req + 6) = qToBigEndian(missed); // count + + if (m_clientControlSocket->writeDatagram(req, sizeof(req), + m_peerAddress, m_clientControlPort) + == sizeof(req)) { for (uint16_t count = 0; count < missed; count++) { LOG(VB_GENERAL, LOG_INFO, LOC + QString("Sent resend for %1") .arg(expected + count)); - m_resends.insert(expected + count, timenow); + m_resends.insert(expected + count, timestamp); } } else LOG(VB_GENERAL, LOG_ERR, LOC + "Failed to send resend request."); } -void MythRAOPConnection::ExpireResendRequests(uint64_t timenow) +/** + * ExpireResendRequests: + * Expire resend requests that are older than timestamp. Those requests are + * expired when audio with older timestamp has already been played + */ +void MythRAOPConnection::ExpireResendRequests(uint64_t timestamp) { if (!m_resends.size()) return; - uint64_t too_old = timenow - 500; QMutableMapIterator it(m_resends); while (it.hasNext()) { it.next(); - if (it.value() < too_old) + if (it.value() < timestamp && m_streamingStarted) { LOG(VB_GENERAL, LOG_WARNING, LOC + QString("Never received resend packet %1").arg(it.key())); @@ -346,104 +458,291 @@ void MythRAOPConnection::ExpireResendRequests(uint64_t timenow) } } -void MythRAOPConnection::ProcessSyncPacket(const QByteArray &buf, uint64_t timenow) +/** + * SendTimeRequest: + * Send a time request to the RAOP client. + */ +void MythRAOPConnection::SendTimeRequest(void) { - m_lastSyncTimestamp = FramesToMs(ntohl(*(uint64_t*)(buf.data() + 4))); - uint64_t now = FramesToMs(ntohl(*(uint64_t*)(buf.data() + 16))); - m_lastLatency = now - m_lastSyncTimestamp; - m_lastSyncTime = timenow; - uint64_t averageaudio = 0; - uint64_t averagequeue = 0; - double averageav = 0; - if (m_latencyCounter) + if (!m_clientControlSocket) // should never happen + return; + + timeval t; + gettimeofday(&t, NULL); + + char req[32]; + req[0] = 0x80; + req[1] = TIMING_REQUEST | 0x80; + // this is always 0x00 0x07 according to http://blog.technologeek.org/airtunes-v2 + // no other value works + req[2] = 0x00; + req[3] = 0x07; + *(uint32_t *)(req + 4) = (uint32_t)0; + *(uint64_t *)(req + 8) = (uint64_t)0; + *(uint64_t *)(req + 16) = (uint64_t)0; + *(uint32_t *)(req + 24) = qToBigEndian((uint32_t)t.tv_sec); + *(uint32_t *)(req + 28) = qToBigEndian((uint32_t)t.tv_usec); + + if (m_clientControlSocket->writeDatagram(req, sizeof(req), m_peerAddress, m_clientTimingPort) != sizeof(req)) { - averageaudio = m_latencyAudio / m_latencyCounter; - averagequeue = m_latencyQueued / m_latencyCounter; - averageav = m_avSync / (double)m_latencyCounter; + LOG(VB_GENERAL, LOG_ERR, LOC + "Failed to send resend time request."); + return; } + LOG(VB_GENERAL, LOG_DEBUG, LOC + + QString("Requesting master time (Local %1.%2)") + .arg(t.tv_sec).arg(t.tv_usec)); +} - if (m_audio) +/** + * ProcessTimeResponse: + * Calculate the network latency, we do not use the reference time send by itunes + * instead we measure the time lapsed between the request and the response + * the latency is calculated in ms + */ +void MythRAOPConnection::ProcessTimeResponse(const QByteArray &buf) +{ + timeval t1, t2; + const char *req = buf.constData(); + + t1.tv_sec = qFromBigEndian(*(uint32_t*)(req + 8)); + t1.tv_usec = qFromBigEndian(*(uint32_t*)(req + 12)); + + gettimeofday(&t2, NULL); + uint64_t time1, time2; + time1 = t1.tv_sec * 1000 + t1.tv_usec / 1000; + time2 = t2.tv_sec * 1000 + t2.tv_usec / 1000; + LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("Read back time (Local %1.%2)") + .arg(t1.tv_sec).arg(t1.tv_usec)); + // network latency equal time difference in ms between request and response + // divide by two for approximate time of one way trip + m_networkLatency = (time2 - time1) / 2; + + // now calculate the time difference between the client and us. + // this is NTP time, where sec is in seconds, and ticks is in 1/2^32s + uint32_t sec = qFromBigEndian(*(uint32_t*)(req + 24)); + uint32_t ticks = qFromBigEndian(*(uint32_t*)(req + 28)); + // convert ticks into ms + int64_t master = NTPToLocal(sec, ticks); + m_clockSkew = master - time2; +} + +uint64_t MythRAOPConnection::NTPToLocal(uint32_t sec, uint32_t ticks) +{ + return (int64_t)sec * 1000LL + (((int64_t)ticks * 1000LL) >> 32); +} + +bool MythRAOPConnection::GetPacketType(const QByteArray &buf, uint8_t &type, + uint16_t &seq, uint64_t ×tamp) +{ + // All RAOP packets start with | 0x80/0x90 (first sync) | PACKET_TYPE | + if ((uint8_t)buf[0] != 0x80 && (uint8_t)buf[0] != 0x90) { - uint64_t total = averageaudio + averagequeue; - LOG(VB_GENERAL, LOG_DEBUG, LOC + - QString("Sync packet: Timestamp: %1 Current Audio ts: %2 (avsync %3ms) " - "Latency: audio %4 queue %5 total %6ms <-> target %7ms") - .arg(m_lastSyncTimestamp).arg(m_audio->GetAudiotime()).arg(averageav, 0) - .arg(averageaudio).arg(averagequeue) - .arg(total).arg(m_lastLatency)); + return false; + } + + type = (char)buf[1]; + // Is it first sync packet? + if ((uint8_t)buf[0] == 0x90 && type == FIRSTSYNC) + { + return true; + } + if (type != FIRSTAUDIO_DATA) + { + type &= ~0x80; } - m_latencyAudio = m_latencyQueued = m_latencyCounter = m_avSync = 0; + + if (type != AUDIO_DATA && type != FIRSTAUDIO_DATA && type != AUDIO_RESEND) + return true; + + const char *ptr = buf.constData(); + if (type == AUDIO_RESEND) + { + ptr += 4; + } + seq = qFromBigEndian(*(uint16_t *)(ptr + 2)); + timestamp = qFromBigEndian(*(uint32_t*)(ptr + 4)); + return true; } -int MythRAOPConnection::ExpireAudio(uint64_t timestamp) +// Audio decode / playback related routines + +uint32_t MythRAOPConnection::decodeAudioPacket(uint8_t type, + const QByteArray *buf, + QList *dest) { - int res = 0; - QMutableMapIterator it(m_audioQueue); - while (it.hasNext()) + const char *data_in = buf->constData(); + int len = buf->size(); + if (type == AUDIO_RESEND) { - it.next(); - if (it.key() < timestamp) + data_in += 4; + len -= 4; + } + data_in += 12; + len -= 12; + if (len < 16) + return -1; + + int aeslen = len & ~0xf; + unsigned char iv[16]; + unsigned char decrypted_data[MAX_PACKET_SIZE]; + memcpy(iv, m_AESIV.constData(), sizeof(iv)); + AES_cbc_encrypt((const unsigned char*)data_in, + decrypted_data, aeslen, + &m_aesKey, iv, AES_DECRYPT); + memcpy(decrypted_data + aeslen, data_in + aeslen, len - aeslen); + + AVPacket tmp_pkt; + AVCodecContext *ctx = m_codeccontext; + + av_init_packet(&tmp_pkt); + tmp_pkt.data = decrypted_data; + tmp_pkt.size = len; + + uint32_t frames_added = 0; + while (tmp_pkt.size > 0) + { + int decoded_data_size = AVCODEC_MAX_AUDIO_FRAME_SIZE; + int16_t *samples = (int16_t *)av_mallocz(AVCODEC_MAX_AUDIO_FRAME_SIZE); + int ret = avcodec_decode_audio3(ctx, samples, &decoded_data_size, &tmp_pkt); + + if (ret < 0) { - AudioFrame frame = it.value(); - av_free((void *)frame.samples); - m_audioQueue.remove(it.key()); - m_queueLength -= frame.frames; - res++; + return -1; } + + if (decoded_data_size > 0) + { + int frames = decoded_data_size / + (ctx->channels * av_get_bits_per_sample_fmt(ctx->sample_fmt)>>3); + frames_added += frames; + AudioData block; + block.data = samples; + block.length = decoded_data_size; + block.frames = frames; + dest->append(block); + } + tmp_pkt.data += ret; + tmp_pkt.size -= ret; } - return res; + return frames_added; } -void MythRAOPConnection::ProcessAudio(uint64_t timenow) +void MythRAOPConnection::ProcessAudio() { - if (!m_audio) - { - ExpireAudio(UINT64_MAX); + if (!m_streamingStarted || !m_audio) return; + + if (m_audio->IsPaused()) + { + // ALSA takes a while to unpause, enough to have SYNC starting to drop + // packets, so unpause as early as possible + m_audio->Pause(false); } + timeval t; gettimeofday(&t, NULL); + uint64_t dtime = (t.tv_sec * 1000 + t.tv_usec / 1000) - m_timeLastSync; + uint64_t rtp = dtime + m_currentTimestamp + m_networkLatency; + uint64_t buffered = m_audioStarted ? m_audio->GetAudioBufferedTime() : 0; + + // Keep audio framework buffer as short as possible, keeping everything in + // m_audioQueue, so we can easily reset the least amount possible + if (buffered > AUDIOCARD_BUFFER) + return; + + // Also make sure m_audioQueue never goes to less than 1/3 of the RDP stream + // total latency, this should gives us enough time to receive missed packets + uint64_t queue = framesToMs(m_audioQueue.size() * m_framesPerPacket); + if (queue < m_bufferLength / 3) + return; - uint64_t updatedsync = m_lastSyncTimestamp + (timenow - m_lastSyncTime); + rtp += buffered; + rtp += m_cardLatency; - // expire anything that is late - int dumped = ExpireAudio(m_lastSyncTimestamp-m_lastLatency); + // How many packets to add to the audio card, to fill AUDIOCARD_BUFFER + int max_packets = ((AUDIOCARD_BUFFER - buffered) + * m_frameRate / 1000) / m_framesPerPacket; + int i = 0; + uint64_t timestamp = 0; - if (dumped > 0) + QMapIterator packet_it(m_audioQueue); + while (packet_it.hasNext() && i <= max_packets) { - LOG(VB_GENERAL, LOG_INFO, LOC + QString("Dumped %1 audio packets") - .arg(dumped)); + packet_it.next(); + + timestamp = packet_it.key(); + if (timestamp < rtp) + { + if (!m_audioStarted) + { + m_audio->Reset(); // clear audio card + } + AudioPacket frames = packet_it.value(); + + if (m_lastSequence != frames.seq) + { + LOG(VB_GENERAL, LOG_ERR, LOC + + QString("Audio discontinuity seen. Played %1 (%3) expected %2") + .arg(frames.seq).arg(m_lastSequence).arg(timestamp)); + m_lastSequence = frames.seq; + } + m_lastSequence++; + + QList::iterator it = frames.data->begin(); + for (; it != frames.data->end(); it++) + { + m_audio->AddData((char *)it->data, it->length, + timestamp, it->frames); + timestamp += m_audio->LengthLastData(); + } + i++; + m_audioStarted = true; + } + else // QMap is sorted, so no need to continue if not found + break; } - int64_t avsync = (int64_t)(m_audio->GetAudiotime() - (int64_t)updatedsync); - uint64_t queue_length = FramesToMs(m_queueLength); - uint64_t ideal_ts = updatedsync - queue_length - avsync + m_lastLatency; + ExpireAudio(timestamp); + m_lastTimestamp = timestamp; - m_avSync += avsync; - m_latencyAudio += m_audio->GetAudioBufferedTime();; - m_latencyQueued += queue_length; - m_latencyCounter++; + // restart audio timer should we stop receiving data on regular interval, + // we need to continue processing the audio queue + m_dequeueAudioTimer->start(AUDIO_BUFFER); +} - QMapIterator it(m_audioQueue); - while (it.hasNext()) +int MythRAOPConnection::ExpireAudio(uint64_t timestamp) +{ + int res = 0; + QMutableMapIterator packet_it(m_audioQueue); + while (packet_it.hasNext()) { - it.next(); - if (it.key() < ideal_ts) + packet_it.next(); + if (packet_it.key() < timestamp) { - AudioFrame aframe = it.value(); - m_audio->AddData((char *)aframe.samples, aframe.size, - it.key(), aframe.frames); + AudioPacket frames = packet_it.value(); + if (frames.data) + { + QList::iterator it = frames.data->begin(); + for (; it != frames.data->end(); it++) + { + av_free(it->data); + } + delete frames.data; + } + m_audioQueue.remove(packet_it.key()); + res++; } } - - ExpireAudio(ideal_ts); + return res; } void MythRAOPConnection::ResetAudio(void) { - ExpireAudio(UINT64_MAX); - m_latencyCounter = m_latencyAudio = m_latencyQueued = m_avSync = 0; - m_seenPacket = false; if (m_audio) + { m_audio->Reset(); + } + ExpireAudio(UINT64_MAX); + ExpireResendRequests(UINT64_MAX); + m_audioStarted = false; } void MythRAOPConnection::timeout(void) @@ -458,37 +757,95 @@ void MythRAOPConnection::audioRetry(void) { MythRAOPDevice* p = (MythRAOPDevice*)parent(); if (p && p->NextInAudioQueue(this) && OpenAudioDevice()) + { CreateDecoder(); + } } if (m_audio && m_codec && m_codeccontext) + { StopAudioTimer(); + } } +/** + * readClient: signal handler for RAOP client connection + * Handle initialisation of session + */ void MythRAOPConnection::readClient(void) { QTcpSocket *socket = (QTcpSocket *)sender(); if (!socket) return; - QList lines; - while (socket->canReadLine()) + QByteArray data = socket->readAll(); + LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("readClient(%1): ") + .arg(data.size()) + data.constData()); + + // For big content, we may be called several times for a single packet + if (!m_incomingPartial) + { + m_incomingHeaders.clear(); + m_incomingContent.clear(); + m_incomingSize = 0; + + QTextStream stream(data); + QString line; + do + { + line = stream.readLine(); + if (line.size() == 0) + break; + LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("Header = %1").arg(line)); + m_incomingHeaders.append(line); + if (line.contains("Content-Length:")) + { + m_incomingSize = line.mid(line.indexOf(" ") + 1).toInt(); + } + } + while (!line.isNull()); + + if (m_incomingHeaders.size() == 0) + return; + + if (!stream.atEnd()) + { + int pos = stream.pos(); + if (pos > 0) + { + m_incomingContent.append(data.mid(pos)); + } + } + } + else + { + m_incomingContent.append(data); + } + + // If we haven't received all the content yet, wait (see when receiving + // coverart + if (m_incomingContent.size() < m_incomingSize) + { + m_incomingPartial = true; + return; + } + else { - QByteArray line = socket->readLine(); - lines.append(line); - LOG(VB_GENERAL, LOG_DEBUG, LOC + "readClient: " + line.trimmed().data()); + m_incomingPartial = false; } + LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("Content(%1) = %2") + .arg(m_incomingContent.size()).arg(m_incomingContent.constData())); - if (lines.size()) - ProcessRequest(lines); + ProcessRequest(m_incomingHeaders, m_incomingContent); } -void MythRAOPConnection::ProcessRequest(const QList &lines) +void MythRAOPConnection::ProcessRequest(const QStringList &header, + const QByteArray &content) { - if (lines.isEmpty()) + if (header.isEmpty()) return; - RawHash tags = FindTags(lines); + RawHash tags = FindTags(header); if (!tags.contains("CSeq")) { @@ -498,98 +855,131 @@ void MythRAOPConnection::ProcessRequest(const QList &lines) *m_textStream << "RTSP/1.0 200 OK\r\n"; - if (tags.contains("Apple-Challenge")) - { - LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("Received Apple-Challenge")); - - *m_textStream << "Apple-Response: "; - if (!LoadKey()) - return; - int tosize = RSA_size(LoadKey()); - unsigned char to[tosize]; + QString option = header[0].left(header[0].indexOf(" ")); - QByteArray challenge = QByteArray::fromBase64(tags["Apple-Challenge"].data()); - int challenge_size = challenge.size(); - if (challenge_size != 16) - { - LOG(VB_GENERAL, LOG_ERR, LOC + - QString("Decoded challenge size %1, expected 16").arg(challenge_size)); - if (challenge_size > 16) - challenge_size = 16; - } - - int i = 0; - unsigned char from[38]; - memcpy(from, challenge.data(), challenge_size); - i += challenge_size; - if (m_socket->localAddress().protocol() == QAbstractSocket::IPv4Protocol) - { - uint32_t ip = m_socket->localAddress().toIPv4Address(); - ip = qToBigEndian(ip); - memcpy(from + i, &ip, 4); - i += 4; - } - else if (m_socket->localAddress().protocol() == QAbstractSocket::IPv6Protocol) + // process RTP-info field + bool gotRTP = false; + uint16_t RTPseq; + uint64_t RTPtimestamp; + if (tags.contains("RTP-Info")) + { + gotRTP = true; + QString data = tags["RTP-Info"]; + QStringList items = data.split(";"); + foreach (QString item, items) { - // NB IPv6 untested - Q_IPV6ADDR ip = m_socket->localAddress().toIPv6Address(); - //ip = qToBigEndian(ip); - memcpy(from + i, &ip, 16); - i += 16; + if (item.startsWith("seq")) + { + RTPseq = item.mid(item.indexOf("=") + 1).trimmed().toUShort(); + } + else if (item.startsWith("rtptime")) + { + RTPtimestamp = item.mid(item.indexOf("=") + 1).trimmed().toUInt(); + } } - memcpy(from + i, m_hardwareId.data(), RAOP_HARDWARE_ID_SIZE); - i += RAOP_HARDWARE_ID_SIZE; + LOG(VB_GENERAL, LOG_INFO, LOC + QString("RTP-Info: seq=%1 rtptime=%2") + .arg(RTPseq).arg(RTPtimestamp)); + } - int pad = 32 - i; - if (pad > 0) + if (option == "OPTIONS") + { + if (tags.contains("Apple-Challenge")) { - memset(from + i, 0, pad); - i += pad; - } + LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("Received Apple-Challenge")); + + *m_textStream << "Apple-Response: "; + if (!LoadKey()) + return; + int tosize = RSA_size(LoadKey()); + uint8_t *to = new uint8_t[tosize]; + + QByteArray challenge = + QByteArray::fromBase64(tags["Apple-Challenge"].toAscii()); + int challenge_size = challenge.size(); + if (challenge_size != 16) + { + LOG(VB_GENERAL, LOG_ERR, LOC + + QString("Decoded challenge size %1, expected 16") + .arg(challenge_size)); + if (challenge_size > 16) + challenge_size = 16; + } - LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("Full base64 response: '%1' size %2") - .arg(QByteArray((const char*)from, i).toBase64().data()).arg(i)); + int i = 0; + unsigned char from[38]; + memcpy(from, challenge.constData(), challenge_size); + i += challenge_size; + if (m_socket->localAddress().protocol() == + QAbstractSocket::IPv4Protocol) + { + uint32_t ip = m_socket->localAddress().toIPv4Address(); + ip = qToBigEndian(ip); + memcpy(from + i, &ip, 4); + i += 4; + } + else if (m_socket->localAddress().protocol() == + QAbstractSocket::IPv6Protocol) + { + // NB IPv6 untested + Q_IPV6ADDR ip = m_socket->localAddress().toIPv6Address(); + //ip = qToBigEndian(ip); + memcpy(from + i, &ip, 16); + i += 16; + } + memcpy(from + i, m_hardwareId.constData(), RAOP_HARDWARE_ID_SIZE); + i += RAOP_HARDWARE_ID_SIZE; - RSA_private_encrypt(i, from, to, LoadKey(), RSA_PKCS1_PADDING); + int pad = 32 - i; + if (pad > 0) + { + memset(from + i, 0, pad); + i += pad; + } - QByteArray base64 = QByteArray((const char*)to, tosize).toBase64(); + LOG(VB_GENERAL, LOG_DEBUG, LOC + + QString("Full base64 response: '%1' size %2") + .arg(QByteArray((const char*)from, i).toBase64().constData()) + .arg(i)); - for (int pos = base64.size() - 1; pos > 0; pos--) - { - if (base64[pos] == '=') - base64[pos] = ' '; - else - break; - } - LOG(VB_GENERAL, LOG_DEBUG, QString("tSize=%1 tLen=%2 tResponse=%3") - .arg(tosize).arg(base64.size()).arg(base64.data())); - *m_textStream << base64.trimmed() << "\r\n"; - } + RSA_private_encrypt(i, from, to, LoadKey(), RSA_PKCS1_PADDING); - QByteArray option = lines[0].left(lines[0].indexOf(" ")); + QByteArray base64 = QByteArray((const char*)to, tosize).toBase64(); + delete[] to; - if (option == "OPTIONS") - { + for (int pos = base64.size() - 1; pos > 0; pos--) + { + if (base64[pos] == '=') + base64[pos] = ' '; + else + break; + } + LOG(VB_GENERAL, LOG_DEBUG, QString("tSize=%1 tLen=%2 tResponse=%3") + .arg(tosize).arg(base64.size()).arg(base64.constData())); + *m_textStream << base64.trimmed() << "\r\n"; + } StartResponse(m_textStream, option, tags["CSeq"]); - *m_textStream << "Public: ANNOUNCE, SETUP, RECORD, PAUSE, FLUSH, TEARDOWN, OPTIONS, GET_PARAMETER, SET_PARAMETER\r\n"; + *m_textStream << "Public: ANNOUNCE, SETUP, RECORD, PAUSE, FLUSH, " + "TEARDOWN, OPTIONS, GET_PARAMETER, SET_PARAMETER. POST, GET\r\n"; } else if (option == "ANNOUNCE") { - foreach (QByteArray line, lines) + QStringList lines = splitLines(content); + foreach (QString line, lines) { if (line.startsWith("a=rsaaeskey:")) { - QByteArray key = line.mid(12).trimmed(); - QByteArray decodedkey = QByteArray::fromBase64(key); - LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("RSAAESKey: %1 (decoded size %2)") - .arg(key.data()).arg(decodedkey.size())); + QString key = line.mid(12).trimmed(); + QByteArray decodedkey = QByteArray::fromBase64(key.toAscii()); + LOG(VB_GENERAL, LOG_DEBUG, LOC + + QString("RSAAESKey: %1 (decoded size %2)") + .arg(key).arg(decodedkey.size())); if (LoadKey()) { int size = sizeof(char) * RSA_size(LoadKey()); char *decryptedkey = new char[size]; if (RSA_private_decrypt(decodedkey.size(), - (const unsigned char*)decodedkey.data(), + (const unsigned char*)decodedkey.constData(), (unsigned char*)decryptedkey, LoadKey(), RSA_PKCS1_OAEP_PADDING)) { @@ -609,21 +999,27 @@ void MythRAOPConnection::ProcessRequest(const QList &lines) } else if (line.startsWith("a=aesiv:")) { - QByteArray aesiv = line.mid(8).trimmed(); - m_AESIV = QByteArray::fromBase64(aesiv.data()); - LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("AESIV: %1 (decoded size %2)") - .arg(aesiv.data()).arg(m_AESIV.size())); + QString aesiv = line.mid(8).trimmed(); + m_AESIV = QByteArray::fromBase64(aesiv.toAscii()); + LOG(VB_GENERAL, LOG_DEBUG, LOC + + QString("AESIV: %1 (decoded size %2)") + .arg(aesiv).arg(m_AESIV.size())); } else if (line.startsWith("a=fmtp:")) { m_audioFormat.clear(); - QByteArray format = line.mid(7).trimmed(); - QList fmts = format.split(' '); - foreach (QByteArray fmt, fmts) + QString format = line.mid(7).trimmed(); + QList fmts = format.split(' '); + foreach (QString fmt, fmts) m_audioFormat.append(fmt.toInt()); foreach (int fmt, m_audioFormat) - LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("Audio parameter: %1").arg(fmt)); + LOG(VB_GENERAL, LOG_DEBUG, LOC + + QString("Audio parameter: %1").arg(fmt)); + m_framesPerPacket = m_audioFormat[1]; + m_sampleSize = m_audioFormat[3]; + m_channels = m_audioFormat[7]; + m_frameRate = m_audioFormat[11]; } } StartResponse(m_textStream, option, tags["CSeq"]); @@ -636,6 +1032,7 @@ void MythRAOPConnection::ProcessRequest(const QList &lines) int timing_port = 0; QString data = tags["Transport"]; QStringList items = data.split(";"); + foreach (QString item, items) { if (item.startsWith("control_port")) @@ -652,6 +1049,8 @@ void MythRAOPConnection::ProcessRequest(const QList &lines) QString("control port: %1 timing port: %2") .arg(control_port).arg(timing_port)); + m_peerAddress = m_socket->peerAddress(); + if (m_clientControlSocket) { m_clientControlSocket->disconnect(); @@ -660,29 +1059,84 @@ void MythRAOPConnection::ProcessRequest(const QList &lines) } m_clientControlSocket = new QUdpSocket(this); - if (!m_clientControlSocket->bind(control_port)) + int controlbind_port = findNextBindingPort(m_clientControlSocket, + control_port); + if (controlbind_port < 0) { LOG(VB_GENERAL, LOG_ERR, LOC + - QString("Failed to bind to client control port %1. " - "Control of audio stream may fail") - .arg(control_port)); + QString("Failed to bind to client control port. " + "Control of audio stream may fail")); } else { LOG(VB_GENERAL, LOG_INFO, LOC + - QString("Bound to client control port %1").arg(control_port)); + QString("Bound to client control port %1 on port %2") + .arg(control_port).arg(controlbind_port)); + } + m_clientControlPort = control_port; + connect(m_clientControlSocket, SIGNAL(readyRead()), + this, SLOT(udpDataReady())); + + if (m_clientTimingSocket) + { + m_clientTimingSocket->disconnect(); + m_clientTimingSocket->close(); + delete m_clientTimingSocket; } + m_clientTimingSocket = new QUdpSocket(this); + int timingbind_port = findNextBindingPort(m_clientTimingSocket, + timing_port); + if (timingbind_port < 0) + { + LOG(VB_GENERAL, LOG_ERR, LOC + + QString("Failed to bind to client timing port. " + "Timing of audio stream will be incorrect")); + } + else + { + LOG(VB_GENERAL, LOG_INFO, LOC + + QString("Bound to client timing port %1 on port %2") + .arg(timing_port).arg(timingbind_port)); + } + m_clientTimingPort = timing_port; + connect(m_clientTimingSocket, SIGNAL(readyRead()), + this, SLOT(udpDataReady())); + if (OpenAudioDevice()) CreateDecoder(); - m_peerAddress = m_socket->peerAddress(); - m_clientControlPort = control_port; - connect(m_clientControlSocket, SIGNAL(readyRead()), this, SLOT(udpDataReady())); + // Recreate transport line with new ports value + QString newdata; + bool first = true; + foreach (QString item, items) + { + if (!first) + { + newdata += ";"; + } + if (item.startsWith("control_port")) + { + newdata += "control_port=" + QString::number(controlbind_port); + } + else if (item.startsWith("timing_port")) + { + newdata += "timing_port=" + QString::number(timingbind_port); + } + else + { + newdata += item; + } + first = false; + } + if (!first) + { + newdata += ";"; + } + newdata += "server_port=" + QString::number(m_dataPort); StartResponse(m_textStream, option, tags["CSeq"]); - *m_textStream << "Transport: " << tags["Transport"].data(); - *m_textStream << ";server_port=" << QString::number(m_dataPort); + *m_textStream << "Transport: " << newdata; *m_textStream << "\r\nSession: MYTHTV\r\n"; } else @@ -693,65 +1147,131 @@ void MythRAOPConnection::ProcessRequest(const QList &lines) } else if (option == "RECORD") { - StartResponse(m_textStream, option, tags["CSeq"]); - } - else if (option == "TEARDOWN") - { - *m_textStream << "Connection: close\r\n"; + if (gotRTP) + { + m_nextSequence = RTPseq; + m_nextTimestamp = RTPtimestamp; + } + // Ask for master clock value to determine time skew and average network latency + SendTimeRequest(); StartResponse(m_textStream, option, tags["CSeq"]); } else if (option == "FLUSH") { + if (gotRTP) + { + m_nextSequence = RTPseq; + m_nextTimestamp = RTPtimestamp; + m_currentTimestamp = m_nextTimestamp - m_bufferLength; + } + // determine RTP timestamp of last sample played + uint64_t timestamp = m_audioStarted && m_audio ? + m_audio->GetAudiotime() : m_lastTimestamp; + *m_textStream << "RTP-Info: rtptime=" << QString::number(timestamp); + m_streamingStarted = false; ResetAudio(); - *m_textStream << "flush\r\n"; StartResponse(m_textStream, option, tags["CSeq"]); } else if (option == "SET_PARAMETER") { - foreach (QByteArray line, lines) + if (tags.contains("Content-Type")) { - StartResponse(m_textStream, option, tags["CSeq"]); - if (line.startsWith("volume:") && m_allowVolumeControl && m_audio) + if (tags["Content-Type"] == "text/parameters") { - QByteArray rawvol = line.mid(7).trimmed(); - float volume = (rawvol.toFloat() + 30.0) / 0.3; - if (volume < 0.01) - volume = 0.0; - LOG(VB_GENERAL, LOG_INFO, LOC + QString("Setting volume to %1 (raw %3)") - .arg(volume).arg(rawvol.data())); - m_audio->SetCurrentVolume((int)volume); + QString name = content.left(content.indexOf(":")); + QString param = content.mid(content.indexOf(":") + 1).trimmed(); + + LOG(VB_GENERAL, LOG_DEBUG, LOC + + QString("text/parameters: name=%1 parem=%2") + .arg(name).arg(param)); + + if (name == "volume" && m_allowVolumeControl && m_audio) + { + float volume = (param.toFloat() + 30.0f) * 100.0f / 30.0f; + if (volume < 0.01f) + volume = 0.0f; + LOG(VB_GENERAL, LOG_INFO, + LOC + QString("Setting volume to %1 (raw %3)") + .arg(volume).arg(param)); + m_audio->SetCurrentVolume((int)volume); + } + else if (name == "progress") + { + QStringList items = param.split("/"); + if (items.size() == 3) + { + m_progressStart = items[0].toUInt(); + m_progressCurrent = items[1].toUInt(); + m_progressEnd = items[2].toUInt(); + } + int length = + (m_progressEnd-m_progressStart) / m_frameRate; + int current = + (m_progressCurrent-m_progressStart) / m_frameRate; + + LOG(VB_GENERAL, LOG_INFO, + LOC +QString("Progress: %1/%2") + .arg(stringFromSeconds(current)) + .arg(stringFromSeconds(length))); + } + } + else if(tags["Content-Type"] == "image/jpeg") + { + // Receiving image coverart + m_artwork = content; + } + else if (tags["Content-Type"] == "application/x-dmap-tagged") + { + // Receiving DMAP metadata + QMap map = decodeDMAP(content); + LOG(VB_GENERAL, LOG_INFO, + QString("Receiving Title:%1 Artist:%2 Album:%3 Format:%4") + .arg(map["minm"]).arg(map["asar"]) + .arg(map["asal"]).arg(map["asfm"])); } } + StartResponse(m_textStream, option, tags["CSeq"]); + } + else if (option == "TEARDOWN") + { + StartResponse(m_textStream, option, tags["CSeq"]); + *m_textStream << "Connection: close\r\n"; } else { LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("Command not handled: %1") - .arg(option.data())); + .arg(option)); StartResponse(m_textStream, option, tags["CSeq"]); } FinishResponse(m_textStream, m_socket, option, tags["CSeq"]); } + void MythRAOPConnection::StartResponse(NetStream *stream, - QByteArray &option, QByteArray &cseq) + QString &option, QString &cseq) { if (!stream) return; LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("%1 sequence %2") - .arg(option.data()).arg(cseq.data())); + .arg(option).arg(cseq)); *stream << "Audio-Jack-Status: connected; type=analog\r\n"; *stream << "CSeq: " << cseq << "\r\n"; } void MythRAOPConnection::FinishResponse(NetStream *stream, QTcpSocket *socket, - QByteArray &option, QByteArray &cseq) + QString &option, QString &cseq) { *stream << "\r\n"; stream->flush(); LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("Finished %1 %2 , Send: %3") - .arg(option.data()).arg(cseq.data()).arg(socket->flush())); + .arg(option).arg(cseq).arg(socket->flush())); } +/** + * LoadKey. Load RSA key into static variable for re-using it later + * The RSA key is resident in memory for the entire duration of the application + * as such RSA_free is never called on it. + */ RSA* MythRAOPConnection::LoadKey(void) { static QMutex lock; @@ -786,7 +1306,7 @@ RSA* MythRAOPConnection::LoadKey(void) return NULL; } -RawHash MythRAOPConnection::FindTags(const QList &lines) +RawHash MythRAOPConnection::FindTags(const QStringList &lines) { RawHash result; if (lines.isEmpty()) @@ -804,6 +1324,91 @@ RawHash MythRAOPConnection::FindTags(const QList &lines) return result; } +QStringList MythRAOPConnection::splitLines(const QByteArray &lines) +{ + QStringList list; + QTextStream stream(lines); + + QString line; + do + { + line = stream.readLine(); + if (!line.isNull()) + { + list.append(line); + } + } + while (!line.isNull()); + + return list; +} + +/** + * stringFromSeconds: + * + * Usage: stringFromSeconds(seconds) + * Description: create a string in the format HH:mm:ss from a duration in seconds + * HH: will not be displayed if there's less than one hour + */ +QString MythRAOPConnection::stringFromSeconds(int time) +{ + int hour = time / 3600; + int minute = (time - hour * 3600) / 60; + int seconds = time - hour * 3600 - minute * 60; + QString str; + + if (hour) + { + str += QString("%1:").arg(hour); + } + if (minute < 10) + { + str += "0"; + } + str += QString("%1:").arg(minute); + if (seconds < 10) + { + str += "0"; + } + str += QString::number(seconds); + return str; +} + +/** + * framesDuration + * Description: return the duration in ms of frames + * + */ +uint64_t MythRAOPConnection::framesToMs(uint64_t frames) +{ + return (frames * 1000ULL) / m_frameRate; +} + +/** + * decodeDMAP: + * + * Usage: decodeDMAP(QByteArray &dmap) + * Description: decode the DMAP (Digital Media Access Protocol) object. + * The object returned is a map of the dmap tags and their associated content + */ +QMap MythRAOPConnection::decodeDMAP(const QByteArray &dmap) +{ + QMap result; + int offset = 8; + while (offset < dmap.size()) + { + QString tag = dmap.mid(offset, 4); + offset += 4; + uint32_t length = qFromBigEndian(*(uint32_t *)(dmap.constData() + offset)); + offset += sizeof(uint32_t); + QString content = QString::fromUtf8(dmap.mid(offset, + length).constData()); + offset += length; + result.insert(tag, content); + } + return result; +} + bool MythRAOPConnection::CreateDecoder(void) { DestroyDecoder(); @@ -816,7 +1421,8 @@ bool MythRAOPConnection::CreateDecoder(void) m_codec = avcodec_find_decoder(CODEC_ID_ALAC); if (!m_codec) { - LOG(VB_GENERAL, LOG_ERR, LOC + "Failed to create ALAC decoder- going silent..."); + LOG(VB_GENERAL, LOG_ERR, LOC + + "Failed to create ALAC decoder- going silent..."); return false; } @@ -827,7 +1433,8 @@ bool MythRAOPConnection::CreateDecoder(void) memset(extradata, 0, 36); if (m_audioFormat.size() < 12) { - LOG(VB_GENERAL, LOG_ERR, LOC + "Creating decoder but haven't seen audio format."); + LOG(VB_GENERAL, LOG_ERR, LOC + + "Creating decoder but haven't seen audio format."); } else { @@ -847,7 +1454,8 @@ bool MythRAOPConnection::CreateDecoder(void) m_codeccontext->channels = m_channels; if (avcodec_open(m_codeccontext, m_codec) < 0) { - LOG(VB_GENERAL, LOG_ERR, LOC + "Failed to open ALAC decoder - going silent..."); + LOG(VB_GENERAL, LOG_ERR, LOC + + "Failed to open ALAC decoder - going silent..."); DestroyDecoder(); return false; } @@ -872,21 +1480,17 @@ bool MythRAOPConnection::OpenAudioDevice(void) { CloseAudioDevice(); - m_sampleRate = m_audioFormat.size() >= 12 ? m_audioFormat[11] : DEFAULT_SAMPLE_RATE; - m_channels = m_audioFormat[7] > 0 ? m_audioFormat[7] : 2; - if (m_sampleRate < 1) - m_sampleRate = DEFAULT_SAMPLE_RATE; - QString passthru = gCoreContext->GetNumSetting("PassThruDeviceOverride", false) ? gCoreContext->GetSetting("PassThruOutputDevice") : QString::null; QString device = gCoreContext->GetSetting("AudioOutputDevice"); m_audio = AudioOutput::OpenAudio(device, passthru, FORMAT_S16, m_channels, - 0, m_sampleRate, AUDIOOUTPUT_MUSIC, + 0, m_frameRate, AUDIOOUTPUT_MUSIC, m_allowVolumeControl, false); if (!m_audio) { - LOG(VB_GENERAL, LOG_ERR, LOC + "Failed to open audio device. Going silent..."); + LOG(VB_GENERAL, LOG_ERR, LOC + + "Failed to open audio device. Going silent..."); CloseAudioDevice(); StartAudioTimer(); return false; @@ -895,7 +1499,8 @@ bool MythRAOPConnection::OpenAudioDevice(void) QString error = m_audio->GetError(); if (!error.isEmpty()) { - LOG(VB_GENERAL, LOG_ERR, LOC + QString("Audio not initialised. Message was '%1'") + LOG(VB_GENERAL, LOG_ERR, LOC + + QString("Audio not initialised. Message was '%1'") .arg(error)); CloseAudioDevice(); StartAudioTimer(); @@ -926,7 +1531,52 @@ void MythRAOPConnection::StartAudioTimer(void) void MythRAOPConnection::StopAudioTimer(void) { if (m_audioTimer) + { m_audioTimer->stop(); + } delete m_audioTimer; m_audioTimer = NULL; } + +/** + * AudioCardLatency: + * Description: Play silence and calculate audio latency between input / output + */ +int64_t MythRAOPConnection::AudioCardLatency(void) +{ + if (!m_audio) + return 0; + + uint64_t timestamp = 123456; + + int16_t *samples = (int16_t *)av_mallocz(AVCODEC_MAX_AUDIO_FRAME_SIZE); + int frames = AUDIOCARD_BUFFER * m_frameRate / 1000; + m_audio->AddData((char *)samples, + frames * (m_sampleSize>>3) * m_channels, + timestamp, + frames); + av_free(samples); + usleep(AUDIOCARD_BUFFER * 1000); + uint64_t audiots = m_audio->GetAudiotime(); + return (int64_t)timestamp - (int64_t)audiots; +} + +int MythRAOPConnection::findNextBindingPort(QUdpSocket *socket, int baseport) +{ + // try a few ports in case the first is in use + int port = baseport; + while (port < baseport + RAOP_PORT_RANGE) + { + if (socket->bind(port)) + { + break; + } + port++; + } + + if (port >= baseport + RAOP_PORT_RANGE) + { + return -1; + } + return port; +} diff --git a/mythtv/libs/libmythtv/mythraopconnection.h b/mythtv/libs/libmythtv/mythraopconnection.h index e229f839b12..05086918e57 100644 --- a/mythtv/libs/libmythtv/mythraopconnection.h +++ b/mythtv/libs/libmythtv/mythraopconnection.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -21,8 +22,23 @@ class QTimer; class AudioOutput; class ServerPool; class NetStream; +class AudioData; +struct AudioData; -typedef QHash RawHash; +typedef QHash RawHash; + +struct AudioData +{ + int16_t *data; + int32_t length; + int32_t frames; +}; + +struct AudioPacket +{ + uint16_t seq; + QList *data; +}; class MythRAOPConnection : public QObject { @@ -38,6 +54,7 @@ class MythRAOPConnection : public QObject QTcpSocket* GetSocket() { return m_socket; } int GetDataPort() { return m_dataPort; } bool HasAudio() { return m_audio; } + static QMap decodeDMAP(const QByteArray &dmap); public slots: void readClient(void); @@ -50,38 +67,63 @@ class MythRAOPConnection : public QObject static RSA* LoadKey(void); private: - uint64_t FramesToMs(uint64_t timestamp); - void ProcessSyncPacket(const QByteArray &buf, uint64_t timenow); - void SendResendRequest(uint64_t timenow, uint16_t expected, - uint16_t got); - void ExpireResendRequests(uint64_t timenow); - int ExpireAudio(uint64_t timestamp); - void ProcessAudio(uint64_t timenow); - void ResetAudio(void); - void ProcessRequest(const QList &lines); - void StartResponse(NetStream *stream, - QByteArray &option, QByteArray &cseq); - void FinishResponse(NetStream *stream, QTcpSocket *socket, - QByteArray &option, QByteArray &cseq); - RawHash FindTags(const QList &lines); - bool CreateDecoder(void); - void DestroyDecoder(void); - bool OpenAudioDevice(void); - void CloseAudioDevice(void); - void StartAudioTimer(void); - void StopAudioTimer(void); + void ProcessSync(const QByteArray &buf); + void SendResendRequest(uint64_t timestamp, + uint16_t expected, uint16_t got); + void ExpireResendRequests(uint64_t timestamp); + uint32_t decodeAudioPacket(uint8_t type, const QByteArray *buf, + QList *dest); + void ProcessAudio(); + int ExpireAudio(uint64_t timestamp); + void ResetAudio(void); + void ProcessRequest(const QStringList &header, + const QByteArray &content); + void StartResponse(NetStream *stream, + QString &option, QString &cseq); + void FinishResponse(NetStream *stream, QTcpSocket *socket, + QString &option, QString &cseq); + RawHash FindTags(const QStringList &lines); + bool CreateDecoder(void); + void DestroyDecoder(void); + bool OpenAudioDevice(void); + void CloseAudioDevice(void); + void StartAudioTimer(void); + void StopAudioTimer(void); + + // time sync + void SendTimeRequest(void); + void ProcessTimeResponse(const QByteArray &buf); + uint64_t NTPToLocal(uint32_t sec, uint32_t ticks); + + // incoming data packet + bool GetPacketType(const QByteArray &buf, uint8_t &type, + uint16_t &seq, uint64_t ×tamp); + + // utility functions + int64_t AudioCardLatency(void); + int findNextBindingPort(QUdpSocket *socket, int port); + QStringList splitLines(const QByteArray &lines); + QString stringFromSeconds(int seconds); + uint64_t framesToMs(uint64_t frames); QTimer *m_watchdogTimer; // comms socket QTcpSocket *m_socket; NetStream *m_textStream; QByteArray m_hardwareId; - // incoming audio + QStringList m_incomingHeaders; + QByteArray m_incomingContent; + bool m_incomingPartial; + int32_t m_incomingSize; QHostAddress m_peerAddress; - int m_dataPort; ServerPool *m_dataSocket; + int m_dataPort; QUdpSocket *m_clientControlSocket; int m_clientControlPort; + QUdpSocket *m_clientTimingSocket; + int m_clientTimingPort; + + // incoming audio QMap m_resends; // crypto QByteArray m_AESIV; @@ -92,31 +134,46 @@ class MythRAOPConnection : public QObject AVCodec *m_codec; AVCodecContext *m_codeccontext; QList m_audioFormat; - int m_sampleRate; int m_channels; - typedef struct - { - int16_t *samples; - uint32_t frames; - uint32_t size; - } AudioFrame; - - QMap m_audioQueue; + int m_sampleSize; + int m_frameRate; + int m_framesPerPacket; + QTimer *m_dequeueAudioTimer; + + QMap m_audioQueue; uint32_t m_queueLength; + bool m_streamingStarted; bool m_allowVolumeControl; + + // packet index, increase after each resend packet request + uint16_t m_seqNum; // audio/packet sync - bool m_seenPacket; - int16_t m_lastPacketSequence; - uint64_t m_lastPacketTimestamp; - uint64_t m_lastSyncTime; - uint64_t m_lastSyncTimestamp; - uint64_t m_lastLatency; - uint64_t m_latencyAudio; - uint64_t m_latencyQueued; - uint64_t m_latencyCounter; - int64_t m_avSync; + uint16_t m_lastSequence; + uint64_t m_lastTimestamp; + uint64_t m_currentTimestamp; + uint16_t m_nextSequence; + uint64_t m_nextTimestamp; + uint64_t m_bufferLength; + uint64_t m_timeLastSync; + int64_t m_cardLatency; + int64_t m_adjustedLatency; + bool m_audioStarted; + + // clock sync + uint64_t m_masterTimeStamp; + uint64_t m_deviceTimeStamp; + uint64_t m_networkLatency; + int64_t m_clockSkew; // difference in ms between reference + // audio retry timer QTimer *m_audioTimer; + + //Current Stream Info + uint32_t m_progressStart; + uint32_t m_progressCurrent; + uint32_t m_progressEnd; + QByteArray m_artwork; + QByteArray m_dmap; }; #endif // MYTHRAOPCONNECTION_H diff --git a/mythtv/libs/libmythtv/mythraopdevice.cpp b/mythtv/libs/libmythtv/mythraopdevice.cpp index bad49327bbf..a42191176c6 100644 --- a/mythtv/libs/libmythtv/mythraopdevice.cpp +++ b/mythtv/libs/libmythtv/mythraopdevice.cpp @@ -161,15 +161,18 @@ void MythRAOPDevice::Start(void) txt.append(6); txt.append("tp=UDP"); txt.append(8); txt.append("sm=false"); txt.append(8); txt.append("sv=false"); - txt.append(4); txt.append("ek=1"); - txt.append(6); txt.append("et=0,1"); - txt.append(6); txt.append("cn=0,1"); - txt.append(4); txt.append("ch=2"); - txt.append(5); txt.append("ss=16"); - txt.append(8); txt.append("sr=44100"); - txt.append(8); txt.append("pw=false"); + txt.append(4); txt.append("ek=1"); // + txt.append(6); txt.append("et=0,1"); // encryption type: no, RSA + txt.append(6); txt.append("cn=0,1"); // audio codec: pcm, alac + txt.append(4); txt.append("ch=2"); // audio channels + txt.append(5); txt.append("ss=16"); // sample size + txt.append(8); txt.append("sr=44100"); // sample rate + txt.append(8); txt.append("pw=false"); // no password txt.append(4); txt.append("vn=3"); - txt.append(9); txt.append("txtvers=1"); + txt.append(9); txt.append("txtvers=1"); // TXT record version 1 + txt.append(8); txt.append("md=0,1,2"); // metadata-type: text, artwork, progress + txt.append(9); txt.append("vs=130.14"); + txt.append(7); txt.append("da=true"); LOG(VB_GENERAL, LOG_INFO, QString("Registering service %1.%2 port %3 TXT %4") .arg(QString(name)).arg(QString(type)).arg(m_setupPort).arg(QString(txt))); @@ -224,7 +227,8 @@ void MythRAOPDevice::newConnection(QTcpSocket *client) return; } - LOG(VB_GENERAL, LOG_ERR, LOC + "Failed to initialise client connection - closing."); + LOG(VB_GENERAL, LOG_ERR, LOC + + "Failed to initialise client connection - closing."); delete obj; client->disconnectFromHost(); delete client; From db30e6eaad92d067a84314d43f912f57e4d89465 Mon Sep 17 00:00:00 2001 From: Jean-Yves Avenard Date: Tue, 1 May 2012 02:42:52 +1000 Subject: [PATCH 28/69] Add two network utility methods to ServerPool Add tryListeningPort and tryBindingPort ; those methods are used to bind/listen on all local interfaces, both IPv6 and IPv4. Add them to ServerPool class as they add functionality to it, but they do not change its functionality and how by default it binds on interfaces by looping individually through all of the. They will be use for local services that are discovered through Bonjour. --- mythtv/libs/libmythbase/serverpool.cpp | 128 +++++++++++++++++++++++++ mythtv/libs/libmythbase/serverpool.h | 8 +- 2 files changed, 135 insertions(+), 1 deletion(-) diff --git a/mythtv/libs/libmythbase/serverpool.cpp b/mythtv/libs/libmythbase/serverpool.cpp index 7ff6df88e7e..8cccc050917 100644 --- a/mythtv/libs/libmythbase/serverpool.cpp +++ b/mythtv/libs/libmythbase/serverpool.cpp @@ -452,3 +452,131 @@ void ServerPool::newUdpDatagram(void) emit newDatagram(buffer, sender, senderPort); } } + +/** + * tryListeningPort + * + * Description: + * Tells the server to listen for incoming connections on port port. + * The server will attempt to listen on all IPv6 and IPv4 interfaces. + * If IPv6 isn't available, the server will listen on all IPv4 network interfaces. + * + * Usage: + * server: QTcpServer object to use + * baseport: port to listen on. If port is 0, a port is chosen automatically. + * range: range of ports to try (default 1) + * isipv6: is set to true if IPv6 was successful (default NULL) + * + * Returns port used on success; otherwise returns -1. + */ +int ServerPool::tryListeningPort(QTcpServer *server, int baseport, + int range, bool *isipv6) +{ + bool ipv6 = true; + // try a few ports in case the first is in use + int port = baseport; + while (port < baseport + range) + { + if (ipv6) + { + if (server->listen(QHostAddress::AnyIPv6, port)) + { + break; + } + else + { + // did we fail because IPv6 isn't available? + QAbstractSocket::SocketError err = server->serverError(); + if (err == QAbstractSocket::UnsupportedSocketOperationError) + { + ipv6 = false; + } + } + } + if (!ipv6) + { + if (server->listen(QHostAddress::Any, port)) + { + break; + } + } + port++; + } + + if (isipv6) + { + *isipv6 = ipv6; + } + + if (port >= baseport + range) + { + return -1; + } + if (port == 0) + { + port = server->serverPort(); + } + return port; +} + +/** + * tryBindingPort + * + * Description: + * Binds this socket for incoming connections on port port. + * The socket will attempt to bind on all IPv6 and IPv4 interfaces. + * If IPv6 isn't available, the socket will be bound to all IPv4 network interfaces. + * + * Usage: + * socket: QUdpSocket object to use + * baseport: port to bind to. + * range: range of ports to try (default 1) + * isipv6: is set to true if IPv6 was successful (default NULL) + * + * Returns port used on success; otherwise returns -1. + */ +int ServerPool::tryBindingPort(QUdpSocket *socket, int baseport, + int range, bool *isipv6) +{ + bool ipv6 = true; + // try a few ports in case the first is in use + int port = baseport; + while (port < baseport + range) + { + if (ipv6) + { + if (socket->bind(QHostAddress::AnyIPv6, port)) + { + break; + } + else + { + // did we fail because IPv6 isn't available? + QAbstractSocket::SocketError err = socket->error(); + if (err == QAbstractSocket::UnsupportedSocketOperationError) + { + ipv6 = false; + } + } + } + if (!ipv6) + { + if (socket->bind(QHostAddress::Any, port)) + { + break; + } + } + port++; + } + + if (isipv6) + { + *isipv6 = ipv6; + } + + if (port >= baseport + range) + { + return -1; + } + return port; +} diff --git a/mythtv/libs/libmythbase/serverpool.h b/mythtv/libs/libmythbase/serverpool.h index 1eabeda2053..3351f373185 100644 --- a/mythtv/libs/libmythbase/serverpool.h +++ b/mythtv/libs/libmythbase/serverpool.h @@ -76,7 +76,13 @@ class MBASE_PUBLIC ServerPool : public QObject void close(void); - signals: + // Utility functions + static int tryListeningPort(QTcpServer *server, int baseport, + int range = 1, bool *isipv6 = NULL); + static int tryBindingPort(QUdpSocket *socket, int baseport, + int range = 1, bool *isipv6 = NULL); + +signals: void newConnection(QTcpSocket *); void newDatagram(QByteArray, QHostAddress, quint16); From 662728fe7a6a45127664b9af042c4802b7487507 Mon Sep 17 00:00:00 2001 From: Jean-Yves Avenard Date: Tue, 1 May 2012 02:55:15 +1000 Subject: [PATCH 29/69] Add full IPv6 support to RAOP and fix Bonjour discovery of RAOP service. Previously, what hostname Bonjour used to advertise a service was a bit of a mystery. If IPv6 was available , it could very well advertise an hostname resolving as IPv6 or use a link-local address. This cause the service to be visible on client (here iTunes), but it wouldn't have worked. We know listen on all local interfaces, IPv6 and IPv4. Also, do not advertise RAOP via Bonjour, if listening on the local interfaces failed --- mythtv/libs/libmythtv/mythraopconnection.cpp | 73 +++++------- mythtv/libs/libmythtv/mythraopconnection.h | 4 +- mythtv/libs/libmythtv/mythraopdevice.cpp | 111 +++++++++---------- mythtv/libs/libmythtv/mythraopdevice.h | 6 +- 4 files changed, 88 insertions(+), 106 deletions(-) diff --git a/mythtv/libs/libmythtv/mythraopconnection.cpp b/mythtv/libs/libmythtv/mythraopconnection.cpp index c40dd3aad3a..a5e548b54b8 100644 --- a/mythtv/libs/libmythtv/mythraopconnection.cpp +++ b/mythtv/libs/libmythtv/mythraopconnection.cpp @@ -144,32 +144,24 @@ bool MythRAOPConnection::Init(void) } // create the data socket - m_dataSocket = new ServerPool(); - if (!connect(m_dataSocket, SIGNAL(newDatagram(QByteArray, QHostAddress, quint16)), - this, SLOT(udpDataReady(QByteArray, QHostAddress, quint16)))) + m_dataSocket = new QUdpSocket(); + if (!connect(m_dataSocket, SIGNAL(readyRead()), this, SLOT(udpDataReady()))) { LOG(VB_GENERAL, LOG_ERR, LOC + "Failed to connect data socket signal."); return false; } // try a few ports in case the first is in use - int baseport = m_dataPort; - while (m_dataPort < baseport + RAOP_PORT_RANGE) - { - if (m_dataSocket->bind(m_dataPort)) - { - LOG(VB_GENERAL, LOG_INFO, LOC + - QString("Bound to port %1 for incoming data").arg(m_dataPort)); - break; - } - m_dataPort++; - } - - if (m_dataPort >= baseport + RAOP_PORT_RANGE) + int baseport = ServerPool::tryBindingPort(m_dataSocket, m_dataPort, + RAOP_PORT_RANGE); + if (baseport < 0) { LOG(VB_GENERAL, LOG_ERR, LOC + "Failed to bind to a port for data."); return false; } + m_dataPort = baseport; + LOG(VB_GENERAL, LOG_INFO, LOC + + QString("Bound to port %1 for incoming data").arg(m_dataPort)); // load the private key if (!LoadKey()) @@ -920,11 +912,19 @@ void MythRAOPConnection::ProcessRequest(const QStringList &header, else if (m_socket->localAddress().protocol() == QAbstractSocket::IPv6Protocol) { - // NB IPv6 untested Q_IPV6ADDR ip = m_socket->localAddress().toIPv6Address(); - //ip = qToBigEndian(ip); - memcpy(from + i, &ip, 16); - i += 16; + if(memcmp(&ip, + "\x00\x00\x00\x00" "\x00\x00\x00\x00" "\x00\x00\xff\xff", + 12) == 0) + { + memcpy(from + i, &ip[12], 4); + i += 4; + } + else + { + memcpy(from + i, &ip, 16); + i += 16; + } } memcpy(from + i, m_hardwareId.constData(), RAOP_HARDWARE_ID_SIZE); i += RAOP_HARDWARE_ID_SIZE; @@ -1059,8 +1059,10 @@ void MythRAOPConnection::ProcessRequest(const QStringList &header, } m_clientControlSocket = new QUdpSocket(this); - int controlbind_port = findNextBindingPort(m_clientControlSocket, - control_port); + int controlbind_port = + ServerPool::tryBindingPort(m_clientControlSocket, + control_port, + RAOP_PORT_RANGE); if (controlbind_port < 0) { LOG(VB_GENERAL, LOG_ERR, LOC + @@ -1085,8 +1087,10 @@ void MythRAOPConnection::ProcessRequest(const QStringList &header, } m_clientTimingSocket = new QUdpSocket(this); - int timingbind_port = findNextBindingPort(m_clientTimingSocket, - timing_port); + int timingbind_port = + ServerPool::tryBindingPort(m_clientTimingSocket, + timing_port, + RAOP_PORT_RANGE); if (timingbind_port < 0) { LOG(VB_GENERAL, LOG_ERR, LOC + @@ -1421,7 +1425,7 @@ bool MythRAOPConnection::CreateDecoder(void) m_codec = avcodec_find_decoder(CODEC_ID_ALAC); if (!m_codec) { - LOG(VB_GENERAL, LOG_ERR, LOC + LOG(VB_GENERAL, LOG_ERR, LOC + "Failed to create ALAC decoder- going silent..."); return false; } @@ -1561,22 +1565,3 @@ int64_t MythRAOPConnection::AudioCardLatency(void) return (int64_t)timestamp - (int64_t)audiots; } -int MythRAOPConnection::findNextBindingPort(QUdpSocket *socket, int baseport) -{ - // try a few ports in case the first is in use - int port = baseport; - while (port < baseport + RAOP_PORT_RANGE) - { - if (socket->bind(port)) - { - break; - } - port++; - } - - if (port >= baseport + RAOP_PORT_RANGE) - { - return -1; - } - return port; -} diff --git a/mythtv/libs/libmythtv/mythraopconnection.h b/mythtv/libs/libmythtv/mythraopconnection.h index 05086918e57..198ab122877 100644 --- a/mythtv/libs/libmythtv/mythraopconnection.h +++ b/mythtv/libs/libmythtv/mythraopconnection.h @@ -20,7 +20,6 @@ class QTcpSocket; class QUdpSocket; class QTimer; class AudioOutput; -class ServerPool; class NetStream; class AudioData; struct AudioData; @@ -101,7 +100,6 @@ class MythRAOPConnection : public QObject // utility functions int64_t AudioCardLatency(void); - int findNextBindingPort(QUdpSocket *socket, int port); QStringList splitLines(const QByteArray &lines); QString stringFromSeconds(int seconds); uint64_t framesToMs(uint64_t frames); @@ -116,7 +114,7 @@ class MythRAOPConnection : public QObject bool m_incomingPartial; int32_t m_incomingSize; QHostAddress m_peerAddress; - ServerPool *m_dataSocket; + QUdpSocket *m_dataSocket; int m_dataPort; QUdpSocket *m_clientControlSocket; int m_clientControlPort; diff --git a/mythtv/libs/libmythtv/mythraopdevice.cpp b/mythtv/libs/libmythtv/mythraopdevice.cpp index a42191176c6..d57c66eca9b 100644 --- a/mythtv/libs/libmythtv/mythraopdevice.cpp +++ b/mythtv/libs/libmythtv/mythraopdevice.cpp @@ -1,10 +1,11 @@ #include #include -#include +#include #include "mthread.h" #include "mythlogging.h" #include "mythcorecontext.h" +#include "serverpool.h" #include "bonjourregister.h" #include "mythraopconnection.h" @@ -80,7 +81,7 @@ void MythRAOPDevice::Cleanup(void) } MythRAOPDevice::MythRAOPDevice() - : ServerPool(), m_name(QString("MythTV")), m_bonjour(NULL), m_valid(false), + : QTcpServer(), m_name(QString("MythTV")), m_bonjour(NULL), m_valid(false), m_lock(new QMutex(QMutex::Recursive)), m_setupPort(5000) { for (int i = 0; i < RAOP_HARDWARE_ID_SIZE; i++) @@ -124,64 +125,61 @@ void MythRAOPDevice::Start(void) return; // join the dots - connect(this, SIGNAL(newConnection(QTcpSocket *)), - this, SLOT(newConnection(QTcpSocket *))); + connect(this, SIGNAL(newConnection()), this, SLOT(newConnection())); - // start listening for connections (try a few ports in case the default is in use) - int baseport = m_setupPort; - while (m_setupPort < baseport + RAOP_PORT_RANGE) + // start listening for connections + // (try a few ports in case the default is in use) + int baseport = ServerPool::tryListeningPort(this, m_setupPort, + RAOP_PORT_RANGE); + if (baseport < 0) { - if (listen(QNetworkInterface::allAddresses(), m_setupPort, false)) - { - LOG(VB_GENERAL, LOG_INFO, LOC + - QString("Listening for connections on port %1").arg(m_setupPort)); - break; - } - m_setupPort++; + LOG(VB_GENERAL, LOG_ERR, LOC + + "Failed to find a port for incoming connections."); } - - if (m_setupPort >= baseport + RAOP_PORT_RANGE) - LOG(VB_GENERAL, LOG_ERR, LOC + "Failed to find a port for incoming connections."); - - // announce service - m_bonjour = new BonjourRegister(this); - - // give each frontend a unique name - int multiple = m_setupPort - baseport; - if (multiple > 0) - m_name += QString::number(multiple); - - QByteArray name = m_hardwareId.toHex(); - name.append("@"); - name.append(m_name); - name.append(" on "); - name.append(gCoreContext->GetHostName()); - QByteArray type = "_raop._tcp"; - QByteArray txt; - txt.append(6); txt.append("tp=UDP"); - txt.append(8); txt.append("sm=false"); - txt.append(8); txt.append("sv=false"); - txt.append(4); txt.append("ek=1"); // - txt.append(6); txt.append("et=0,1"); // encryption type: no, RSA - txt.append(6); txt.append("cn=0,1"); // audio codec: pcm, alac - txt.append(4); txt.append("ch=2"); // audio channels - txt.append(5); txt.append("ss=16"); // sample size - txt.append(8); txt.append("sr=44100"); // sample rate - txt.append(8); txt.append("pw=false"); // no password - txt.append(4); txt.append("vn=3"); - txt.append(9); txt.append("txtvers=1"); // TXT record version 1 - txt.append(8); txt.append("md=0,1,2"); // metadata-type: text, artwork, progress - txt.append(9); txt.append("vs=130.14"); - txt.append(7); txt.append("da=true"); - - LOG(VB_GENERAL, LOG_INFO, QString("Registering service %1.%2 port %3 TXT %4") - .arg(QString(name)).arg(QString(type)).arg(m_setupPort).arg(QString(txt))); - if (!m_bonjour->Register(m_setupPort, type, name, txt)) + else { - LOG(VB_GENERAL, LOG_ERR, LOC + "Failed to register service."); - return; + m_setupPort = baseport; + LOG(VB_GENERAL, LOG_INFO, LOC + + QString("Listening for connections on port %1").arg(m_setupPort)); + // announce service + m_bonjour = new BonjourRegister(this); + + // give each frontend a unique name + int multiple = m_setupPort - baseport; + if (multiple > 0) + m_name += QString::number(multiple); + + QByteArray name = m_hardwareId.toHex(); + name.append("@"); + name.append(m_name); + name.append(" on "); + name.append(gCoreContext->GetHostName()); + QByteArray type = "_raop._tcp"; + QByteArray txt; + txt.append(6); txt.append("tp=UDP"); + txt.append(8); txt.append("sm=false"); + txt.append(8); txt.append("sv=false"); + txt.append(4); txt.append("ek=1"); // + txt.append(6); txt.append("et=0,1"); // encryption type: no, RSA + txt.append(6); txt.append("cn=0,1"); // audio codec: pcm, alac + txt.append(4); txt.append("ch=2"); // audio channels + txt.append(5); txt.append("ss=16"); // sample size + txt.append(8); txt.append("sr=44100"); // sample rate + txt.append(8); txt.append("pw=false"); // no password + txt.append(4); txt.append("vn=3"); + txt.append(9); txt.append("txtvers=1"); // TXT record version 1 + txt.append(8); txt.append("md=0,1,2"); // metadata-type: text, artwork, progress + txt.append(9); txt.append("vs=130.14"); + txt.append(7); txt.append("da=true"); + + LOG(VB_GENERAL, LOG_INFO, QString("Registering service %1.%2 port %3 TXT %4") + .arg(QString(name)).arg(QString(type)).arg(m_setupPort).arg(QString(txt))); + if (!m_bonjour->Register(m_setupPort, type, name, txt)) + { + LOG(VB_GENERAL, LOG_ERR, LOC + "Failed to register service."); + return; + } } - m_valid = true; return; } @@ -196,9 +194,10 @@ bool MythRAOPDevice::NextInAudioQueue(MythRAOPConnection *conn) return true; } -void MythRAOPDevice::newConnection(QTcpSocket *client) +void MythRAOPDevice::newConnection() { QMutexLocker locker(m_lock); + QTcpSocket *client = this->nextPendingConnection(); LOG(VB_GENERAL, LOG_INFO, LOC + QString("New connection from %1:%2") .arg(client->peerAddress().toString()).arg(client->peerPort())); diff --git a/mythtv/libs/libmythtv/mythraopdevice.h b/mythtv/libs/libmythtv/mythraopdevice.h index 13fb730e09a..ecfad27eca4 100644 --- a/mythtv/libs/libmythtv/mythraopdevice.h +++ b/mythtv/libs/libmythtv/mythraopdevice.h @@ -2,8 +2,8 @@ #define MYTHRAOPDEVICE_H #include +#include -#include "serverpool.h" #include "mythtvexp.h" class QMutex; @@ -14,7 +14,7 @@ class MythRAOPConnection; #define RAOP_PORT_RANGE 100 #define RAOP_HARDWARE_ID_SIZE 6 -class MTV_PUBLIC MythRAOPDevice : public ServerPool +class MTV_PUBLIC MythRAOPDevice : public QTcpServer { Q_OBJECT @@ -27,7 +27,7 @@ class MTV_PUBLIC MythRAOPDevice : public ServerPool private slots: void Start(); - void newConnection(QTcpSocket *client); + void newConnection(); void deleteClient(); private: From 86e1b41c2cef74fde65f3e16591d8b9d8a4b5051 Mon Sep 17 00:00:00 2001 From: Jean-Yves Avenard Date: Tue, 1 May 2012 03:52:15 +1000 Subject: [PATCH 30/69] Revert "Add ability to override Bonjour's service name." This reverts commit 48d84958a93e94e97591f0f239902fecfca8607f. Now that the issue is properly fixed by using db30e6eaad92d067a84314d43f912f57e4d89465, revert it --- mythtv/libs/libmythbase/bonjourregister.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/mythtv/libs/libmythbase/bonjourregister.cpp b/mythtv/libs/libmythbase/bonjourregister.cpp index 7f9873261b2..017ee6d7521 100644 --- a/mythtv/libs/libmythbase/bonjourregister.cpp +++ b/mythtv/libs/libmythbase/bonjourregister.cpp @@ -5,7 +5,6 @@ #include "mythlogging.h" #include "bonjourregister.h" -#include "mythcorecontext.h" #define LOC QString("Bonjour: ") @@ -42,13 +41,11 @@ bool BonjourRegister::Register(uint16_t port, const QByteArray &type, return true; } - QByteArray host(gCoreContext->GetSetting("BonjourHostname", "").toUtf8()); - const char *host_ptr = host.size() > 0 ? host.constData() : NULL; uint16_t qport = qToBigEndian(port); DNSServiceErrorType res = DNSServiceRegister(&m_dnssref, 0, 0, (const char*)name.data(), (const char*)type.data(), - NULL, host_ptr, qport, txt.size(), (void*)txt.data(), + NULL, 0, qport, txt.size(), (void*)txt.data(), BonjourCallback, this); if (kDNSServiceErr_NoError != res) From aa0e26f1abc7370d8840abdfd8ced2fdf2ef76f7 Mon Sep 17 00:00:00 2001 From: Raymond Wagner Date: Mon, 30 Apr 2012 16:49:57 -0400 Subject: [PATCH 31/69] Bypass aborts resulting from attempting to process empty strings. --- tmdb3/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tmdb3/util.py b/tmdb3/util.py index f97f9ea62d2..27cb91a6dd0 100644 --- a/tmdb3/util.py +++ b/tmdb3/util.py @@ -125,7 +125,7 @@ def __get__(self, inst, owner): return inst._data[self.field] def __set__(self, inst, value): - if value is not None: + if (value is not None) and (value != ''): value = self.handler(value) else: value = self.default From 5b86d8bb95c69205d8669bb23c30ccd6b8514c4b Mon Sep 17 00:00:00 2001 From: Raymond Wagner Date: Mon, 30 Apr 2012 16:50:20 -0400 Subject: [PATCH 32/69] Use `date` type rather than `datetime` for 'releasedate attribute'. --- tmdb3/tmdb_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tmdb3/tmdb_api.py b/tmdb3/tmdb_api.py index fdedb3e2016..db6b5e0c2d5 100644 --- a/tmdb3/tmdb_api.py +++ b/tmdb3/tmdb_api.py @@ -386,7 +386,7 @@ def fromIMDB(cls, imdbid, locale=None): budget = Datapoint('budget') revenue = Datapoint('revenue') releasedate = Datapoint('release_date', handler=lambda x: \ - datetime.datetime.strptime(x, '%Y-%m-%d')) + datetime.date(*[int(y) for y in x.split('-')])) homepage = Datapoint('homepage') imdb = Datapoint('imdb_id') From 27dff377fe6612b870a4cf28b9c058567be1a3bb Mon Sep 17 00:00:00 2001 From: Raymond Wagner Date: Mon, 30 Apr 2012 20:07:29 -0400 Subject: [PATCH 33/69] Correct file cache error handling to ignore rather than raise errors. --- tmdb3/cache_file.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tmdb3/cache_file.py b/tmdb3/cache_file.py index 319eff79e55..54a9ca414b0 100644 --- a/tmdb3/cache_file.py +++ b/tmdb3/cache_file.py @@ -169,7 +169,7 @@ def key(self): try: self._key, self._data = json.loads(self._buff.getvalue()) except: - raise + pass return self._key @key.setter def key(self, value): self._key = value From e16d9af6cb08ff94bf2fdf0879b8e722b3327b51 Mon Sep 17 00:00:00 2001 From: Raymond Wagner Date: Mon, 30 Apr 2012 20:10:03 -0400 Subject: [PATCH 34/69] Assorted fixes and emable tmdb3.py metadata grabber. This enables the new grabber for version 3 of TheMovieDb API. This is still in debugging, and some additional work needs to be done with regards to the cache engine and query rate limiting before it gets pushed as the primary. --- .../programs/scripts/metadata/Movie/tmdb3.py | 68 +++++++++++++------ 1 file changed, 48 insertions(+), 20 deletions(-) diff --git a/mythtv/programs/scripts/metadata/Movie/tmdb3.py b/mythtv/programs/scripts/metadata/Movie/tmdb3.py index 048880fc28a..f3c03712c67 100755 --- a/mythtv/programs/scripts/metadata/Movie/tmdb3.py +++ b/mythtv/programs/scripts/metadata/Movie/tmdb3.py @@ -9,11 +9,12 @@ # http://www.mythtv.org/wiki/MythVideo_Grabber_Script_Format # http://help.themoviedb.org/kb/api/about-3 #----------------------- -__title__ = "TheMovieDB.org" +__title__ = "TheMovieDB.org V3" __author__ = "Raymond Wagner" -__version__ = "0.2.0" +__version__ = "0.3.0" # 0.1.0 Initial version # 0.2.0 Add language support, move cache to home directory +# 0.3.0 Enable version detection to allow use in MythTV from MythTV.tmdb3 import searchMovie, Movie, Collection, set_key, set_cache, set_locale from MythTV import VideoMetadata @@ -32,9 +33,11 @@ def buildSingle(inetref): ['budget', 'budget'], ['revenue', 'revenue']] m = VideoMetadata() for i,j in mapping: - setattr(m, i, getattr(movie, j)) + if getattr(movie, j): + setattr(m, i, getattr(movie, j)) m.inetref = str(movie.id) - m.year = movie.releasedate.year + if movie.releasedate: + m.year = movie.releasedate.year if movie.collection: m.collectionref = str(movie.collection.id) for country, release in movie.releases.items(): @@ -63,26 +66,47 @@ def buildSingle(inetref): m.images.append({'type':'coverart', 'url':poster.geturl(), 'thumb':poster.geturl(poster.sizes()[0])}) tree.append(m.toXML()) - sys.stdout.write(etree.tostring(tree, encoding='UTF-8', pretty_print=True)) + sys.stdout.write(etree.tostring(tree, encoding='UTF-8', pretty_print=True, + xml_declaration=True)) sys.exit() def buildList(query): + # TEMPORARY FIX: + # remove all dashes from queries to work around search behavior + # as negative to all text that comes afterwards + query = query.replace('-','') results = searchMovie(query) tree = etree.XML(u'') mapping = [['runtime', 'runtime'], ['title', 'originaltitle'], ['releasedate', 'releasedate'], ['tagline', 'tagline'], ['description', 'overview'], ['homepage', 'homepage'], ['userrating', 'userrating'], ['popularity', 'popularity']] + + count = 0 for res in results: m = VideoMetadata() for i,j in mapping: - setattr(m, i, getattr(res, j)) + if getattr(res, j): + setattr(m, i, getattr(res, j)) m.inetref = str(res.id) - m.year = res.releasedate.year - m.images.append({'type':'fanart', 'url':res.backdrop.geturl()}) - m.images.append({'type':'coverart', 'url':res.poster.geturl()}) + if res.releasedate: + m.year = res.releasedate.year + if res.backdrop: + b = res.backdrop + m.images.append({'type':'fanart', 'url':b.geturl(), + 'thumb':b.geturl(b.sizes()[0])}) + if res.poster: + p = res.poster + m.images.append({'type':'coverart', 'url':p.geturl(), + 'thumb':p.geturl(p.sizes()[0])}) tree.append(m.toXML()) - sys.stdout.write(etree.tostring(tree, encoding='UTF-8', pretty_print=True)) + count += 1 + if count >= 60: + # page limiter, dont want to overload the server + break + + sys.stdout.write(etree.tostring(tree, encoding='UTF-8', pretty_print=True, + xml_declaration=True)) sys.exit(0) def buildCollection(inetref): @@ -92,13 +116,16 @@ def buildCollection(inetref): m.collectionref = str(collection.id) m.title = collection.name if collection.backdrop: - m.images.append({'type':'fanart', 'url':collection.backdrop.geturl(), - 'thumb':collection.backdrop.geturl(collection.backdrop.sizes()[0])}) + b = collection.backdrop + m.images.append({'type':'fanart', 'url':b.geturl(), + 'thumb':b.geturl(b.sizes()[0])}) if collection.poster: - m.images.append({'type':'coverart', 'url':collection.poster.geturl(), - 'thumb':collection.poster.geturl(collection.poster.sizes()[0])}) + p = collection.poster + m.images.append({'type':'coverart', 'url':p.geturl(), + 'thumb':p.geturl(p.sizes()[0])}) tree.append(m.toXML()) - sys.stdout.write(etree.tostring(tree, encoding='UTF-8', pretty_print=True)) + sys.stdout.write(etree.tostring(tree, encoding='UTF-8', pretty_print=True, + xml_declaration=True)) sys.exit() def buildVersion(): @@ -111,7 +138,8 @@ def buildVersion(): etree.SubElement(version, "description").text = \ 'Search and metadata downloads for themoviedb.org' etree.SubElement(version, "version").text = __version__ - sys.stdout.write(etree.tostring(version, encoding='UTF-8', pretty_print=True)) + sys.stdout.write(etree.tostring(version, encoding='UTF-8', pretty_print=True, + xml_declaration=True)) sys.exit(0) def main(): @@ -120,8 +148,8 @@ def main(): parser = OptionParser() -# parser.add_option('-v', "--version", action="store_true", default=False, -# dest="version", help="Display version and author") + parser.add_option('-v', "--version", action="store_true", default=False, + dest="version", help="Display version and author") parser.add_option('-M', "--movielist", action="store_true", default=False, dest="movielist", help="Get Movies matching search.") parser.add_option('-D', "--moviedata", action="store_true", default=False, @@ -133,8 +161,8 @@ def main(): opts, args = parser.parse_args() -# if opts.version: -# buildVersion() + if opts.version: + buildVersion() if opts.language: set_locale(language=opts.language, fallthrough=True) From ac36239755c1eb4e241e801c4c3b30f2f41b123d Mon Sep 17 00:00:00 2001 From: Daniel Thor Kristjansson Date: Tue, 1 May 2012 09:47:53 -0400 Subject: [PATCH 35/69] Fixes #10668. Fixes segfault in mythtranscode use of DTVRecorder. This was discovered and diagnosed by Chris Tracy . The problem is a debugging macro that assumes the recorder is being controlled by TVRec. --- mythtv/libs/libmythtv/dtvrecorder.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mythtv/libs/libmythtv/dtvrecorder.cpp b/mythtv/libs/libmythtv/dtvrecorder.cpp index 31a5971951e..9d7d89fbbbb 100644 --- a/mythtv/libs/libmythtv/dtvrecorder.cpp +++ b/mythtv/libs/libmythtv/dtvrecorder.cpp @@ -32,7 +32,9 @@ extern "C" { extern const uint8_t *ff_find_start_code(const uint8_t *p, const uint8_t *end, uint32_t *state); } -#define LOC QString("DTVRec(%1): ").arg(tvrec->GetCaptureCardNum()) +#define LOC ((tvrec) ? \ + QString("DTVRec(%1): ").arg(tvrec->GetCaptureCardNum()) : \ + QString("DTVRec(0x%1): ").arg(intptr_t(this),0,16)) const uint DTVRecorder::kMaxKeyFrameDistance = 80; From 7d103240977ef9b04819fadcf9b453631079d2a0 Mon Sep 17 00:00:00 2001 From: Jean-Yves Avenard Date: Wed, 2 May 2012 03:58:10 +1000 Subject: [PATCH 36/69] Revert "Add full IPv6 support to RAOP and fix Bonjour discovery of RAOP service." This reverts commit 662728fe7a6a45127664b9af042c4802b7487507. --- mythtv/libs/libmythtv/mythraopconnection.cpp | 73 +++++++----- mythtv/libs/libmythtv/mythraopconnection.h | 4 +- mythtv/libs/libmythtv/mythraopdevice.cpp | 111 ++++++++++--------- mythtv/libs/libmythtv/mythraopdevice.h | 6 +- 4 files changed, 106 insertions(+), 88 deletions(-) diff --git a/mythtv/libs/libmythtv/mythraopconnection.cpp b/mythtv/libs/libmythtv/mythraopconnection.cpp index a5e548b54b8..c40dd3aad3a 100644 --- a/mythtv/libs/libmythtv/mythraopconnection.cpp +++ b/mythtv/libs/libmythtv/mythraopconnection.cpp @@ -144,24 +144,32 @@ bool MythRAOPConnection::Init(void) } // create the data socket - m_dataSocket = new QUdpSocket(); - if (!connect(m_dataSocket, SIGNAL(readyRead()), this, SLOT(udpDataReady()))) + m_dataSocket = new ServerPool(); + if (!connect(m_dataSocket, SIGNAL(newDatagram(QByteArray, QHostAddress, quint16)), + this, SLOT(udpDataReady(QByteArray, QHostAddress, quint16)))) { LOG(VB_GENERAL, LOG_ERR, LOC + "Failed to connect data socket signal."); return false; } // try a few ports in case the first is in use - int baseport = ServerPool::tryBindingPort(m_dataSocket, m_dataPort, - RAOP_PORT_RANGE); - if (baseport < 0) + int baseport = m_dataPort; + while (m_dataPort < baseport + RAOP_PORT_RANGE) + { + if (m_dataSocket->bind(m_dataPort)) + { + LOG(VB_GENERAL, LOG_INFO, LOC + + QString("Bound to port %1 for incoming data").arg(m_dataPort)); + break; + } + m_dataPort++; + } + + if (m_dataPort >= baseport + RAOP_PORT_RANGE) { LOG(VB_GENERAL, LOG_ERR, LOC + "Failed to bind to a port for data."); return false; } - m_dataPort = baseport; - LOG(VB_GENERAL, LOG_INFO, LOC + - QString("Bound to port %1 for incoming data").arg(m_dataPort)); // load the private key if (!LoadKey()) @@ -912,19 +920,11 @@ void MythRAOPConnection::ProcessRequest(const QStringList &header, else if (m_socket->localAddress().protocol() == QAbstractSocket::IPv6Protocol) { + // NB IPv6 untested Q_IPV6ADDR ip = m_socket->localAddress().toIPv6Address(); - if(memcmp(&ip, - "\x00\x00\x00\x00" "\x00\x00\x00\x00" "\x00\x00\xff\xff", - 12) == 0) - { - memcpy(from + i, &ip[12], 4); - i += 4; - } - else - { - memcpy(from + i, &ip, 16); - i += 16; - } + //ip = qToBigEndian(ip); + memcpy(from + i, &ip, 16); + i += 16; } memcpy(from + i, m_hardwareId.constData(), RAOP_HARDWARE_ID_SIZE); i += RAOP_HARDWARE_ID_SIZE; @@ -1059,10 +1059,8 @@ void MythRAOPConnection::ProcessRequest(const QStringList &header, } m_clientControlSocket = new QUdpSocket(this); - int controlbind_port = - ServerPool::tryBindingPort(m_clientControlSocket, - control_port, - RAOP_PORT_RANGE); + int controlbind_port = findNextBindingPort(m_clientControlSocket, + control_port); if (controlbind_port < 0) { LOG(VB_GENERAL, LOG_ERR, LOC + @@ -1087,10 +1085,8 @@ void MythRAOPConnection::ProcessRequest(const QStringList &header, } m_clientTimingSocket = new QUdpSocket(this); - int timingbind_port = - ServerPool::tryBindingPort(m_clientTimingSocket, - timing_port, - RAOP_PORT_RANGE); + int timingbind_port = findNextBindingPort(m_clientTimingSocket, + timing_port); if (timingbind_port < 0) { LOG(VB_GENERAL, LOG_ERR, LOC + @@ -1425,7 +1421,7 @@ bool MythRAOPConnection::CreateDecoder(void) m_codec = avcodec_find_decoder(CODEC_ID_ALAC); if (!m_codec) { - LOG(VB_GENERAL, LOG_ERR, LOC + LOG(VB_GENERAL, LOG_ERR, LOC + "Failed to create ALAC decoder- going silent..."); return false; } @@ -1565,3 +1561,22 @@ int64_t MythRAOPConnection::AudioCardLatency(void) return (int64_t)timestamp - (int64_t)audiots; } +int MythRAOPConnection::findNextBindingPort(QUdpSocket *socket, int baseport) +{ + // try a few ports in case the first is in use + int port = baseport; + while (port < baseport + RAOP_PORT_RANGE) + { + if (socket->bind(port)) + { + break; + } + port++; + } + + if (port >= baseport + RAOP_PORT_RANGE) + { + return -1; + } + return port; +} diff --git a/mythtv/libs/libmythtv/mythraopconnection.h b/mythtv/libs/libmythtv/mythraopconnection.h index 198ab122877..05086918e57 100644 --- a/mythtv/libs/libmythtv/mythraopconnection.h +++ b/mythtv/libs/libmythtv/mythraopconnection.h @@ -20,6 +20,7 @@ class QTcpSocket; class QUdpSocket; class QTimer; class AudioOutput; +class ServerPool; class NetStream; class AudioData; struct AudioData; @@ -100,6 +101,7 @@ class MythRAOPConnection : public QObject // utility functions int64_t AudioCardLatency(void); + int findNextBindingPort(QUdpSocket *socket, int port); QStringList splitLines(const QByteArray &lines); QString stringFromSeconds(int seconds); uint64_t framesToMs(uint64_t frames); @@ -114,7 +116,7 @@ class MythRAOPConnection : public QObject bool m_incomingPartial; int32_t m_incomingSize; QHostAddress m_peerAddress; - QUdpSocket *m_dataSocket; + ServerPool *m_dataSocket; int m_dataPort; QUdpSocket *m_clientControlSocket; int m_clientControlPort; diff --git a/mythtv/libs/libmythtv/mythraopdevice.cpp b/mythtv/libs/libmythtv/mythraopdevice.cpp index d57c66eca9b..a42191176c6 100644 --- a/mythtv/libs/libmythtv/mythraopdevice.cpp +++ b/mythtv/libs/libmythtv/mythraopdevice.cpp @@ -1,11 +1,10 @@ #include #include -#include +#include #include "mthread.h" #include "mythlogging.h" #include "mythcorecontext.h" -#include "serverpool.h" #include "bonjourregister.h" #include "mythraopconnection.h" @@ -81,7 +80,7 @@ void MythRAOPDevice::Cleanup(void) } MythRAOPDevice::MythRAOPDevice() - : QTcpServer(), m_name(QString("MythTV")), m_bonjour(NULL), m_valid(false), + : ServerPool(), m_name(QString("MythTV")), m_bonjour(NULL), m_valid(false), m_lock(new QMutex(QMutex::Recursive)), m_setupPort(5000) { for (int i = 0; i < RAOP_HARDWARE_ID_SIZE; i++) @@ -125,61 +124,64 @@ void MythRAOPDevice::Start(void) return; // join the dots - connect(this, SIGNAL(newConnection()), this, SLOT(newConnection())); + connect(this, SIGNAL(newConnection(QTcpSocket *)), + this, SLOT(newConnection(QTcpSocket *))); - // start listening for connections - // (try a few ports in case the default is in use) - int baseport = ServerPool::tryListeningPort(this, m_setupPort, - RAOP_PORT_RANGE); - if (baseport < 0) + // start listening for connections (try a few ports in case the default is in use) + int baseport = m_setupPort; + while (m_setupPort < baseport + RAOP_PORT_RANGE) { - LOG(VB_GENERAL, LOG_ERR, LOC + - "Failed to find a port for incoming connections."); - } - else - { - m_setupPort = baseport; - LOG(VB_GENERAL, LOG_INFO, LOC + - QString("Listening for connections on port %1").arg(m_setupPort)); - // announce service - m_bonjour = new BonjourRegister(this); - - // give each frontend a unique name - int multiple = m_setupPort - baseport; - if (multiple > 0) - m_name += QString::number(multiple); - - QByteArray name = m_hardwareId.toHex(); - name.append("@"); - name.append(m_name); - name.append(" on "); - name.append(gCoreContext->GetHostName()); - QByteArray type = "_raop._tcp"; - QByteArray txt; - txt.append(6); txt.append("tp=UDP"); - txt.append(8); txt.append("sm=false"); - txt.append(8); txt.append("sv=false"); - txt.append(4); txt.append("ek=1"); // - txt.append(6); txt.append("et=0,1"); // encryption type: no, RSA - txt.append(6); txt.append("cn=0,1"); // audio codec: pcm, alac - txt.append(4); txt.append("ch=2"); // audio channels - txt.append(5); txt.append("ss=16"); // sample size - txt.append(8); txt.append("sr=44100"); // sample rate - txt.append(8); txt.append("pw=false"); // no password - txt.append(4); txt.append("vn=3"); - txt.append(9); txt.append("txtvers=1"); // TXT record version 1 - txt.append(8); txt.append("md=0,1,2"); // metadata-type: text, artwork, progress - txt.append(9); txt.append("vs=130.14"); - txt.append(7); txt.append("da=true"); - - LOG(VB_GENERAL, LOG_INFO, QString("Registering service %1.%2 port %3 TXT %4") - .arg(QString(name)).arg(QString(type)).arg(m_setupPort).arg(QString(txt))); - if (!m_bonjour->Register(m_setupPort, type, name, txt)) + if (listen(QNetworkInterface::allAddresses(), m_setupPort, false)) { - LOG(VB_GENERAL, LOG_ERR, LOC + "Failed to register service."); - return; + LOG(VB_GENERAL, LOG_INFO, LOC + + QString("Listening for connections on port %1").arg(m_setupPort)); + break; } + m_setupPort++; } + + if (m_setupPort >= baseport + RAOP_PORT_RANGE) + LOG(VB_GENERAL, LOG_ERR, LOC + "Failed to find a port for incoming connections."); + + // announce service + m_bonjour = new BonjourRegister(this); + + // give each frontend a unique name + int multiple = m_setupPort - baseport; + if (multiple > 0) + m_name += QString::number(multiple); + + QByteArray name = m_hardwareId.toHex(); + name.append("@"); + name.append(m_name); + name.append(" on "); + name.append(gCoreContext->GetHostName()); + QByteArray type = "_raop._tcp"; + QByteArray txt; + txt.append(6); txt.append("tp=UDP"); + txt.append(8); txt.append("sm=false"); + txt.append(8); txt.append("sv=false"); + txt.append(4); txt.append("ek=1"); // + txt.append(6); txt.append("et=0,1"); // encryption type: no, RSA + txt.append(6); txt.append("cn=0,1"); // audio codec: pcm, alac + txt.append(4); txt.append("ch=2"); // audio channels + txt.append(5); txt.append("ss=16"); // sample size + txt.append(8); txt.append("sr=44100"); // sample rate + txt.append(8); txt.append("pw=false"); // no password + txt.append(4); txt.append("vn=3"); + txt.append(9); txt.append("txtvers=1"); // TXT record version 1 + txt.append(8); txt.append("md=0,1,2"); // metadata-type: text, artwork, progress + txt.append(9); txt.append("vs=130.14"); + txt.append(7); txt.append("da=true"); + + LOG(VB_GENERAL, LOG_INFO, QString("Registering service %1.%2 port %3 TXT %4") + .arg(QString(name)).arg(QString(type)).arg(m_setupPort).arg(QString(txt))); + if (!m_bonjour->Register(m_setupPort, type, name, txt)) + { + LOG(VB_GENERAL, LOG_ERR, LOC + "Failed to register service."); + return; + } + m_valid = true; return; } @@ -194,10 +196,9 @@ bool MythRAOPDevice::NextInAudioQueue(MythRAOPConnection *conn) return true; } -void MythRAOPDevice::newConnection() +void MythRAOPDevice::newConnection(QTcpSocket *client) { QMutexLocker locker(m_lock); - QTcpSocket *client = this->nextPendingConnection(); LOG(VB_GENERAL, LOG_INFO, LOC + QString("New connection from %1:%2") .arg(client->peerAddress().toString()).arg(client->peerPort())); diff --git a/mythtv/libs/libmythtv/mythraopdevice.h b/mythtv/libs/libmythtv/mythraopdevice.h index ecfad27eca4..13fb730e09a 100644 --- a/mythtv/libs/libmythtv/mythraopdevice.h +++ b/mythtv/libs/libmythtv/mythraopdevice.h @@ -2,8 +2,8 @@ #define MYTHRAOPDEVICE_H #include -#include +#include "serverpool.h" #include "mythtvexp.h" class QMutex; @@ -14,7 +14,7 @@ class MythRAOPConnection; #define RAOP_PORT_RANGE 100 #define RAOP_HARDWARE_ID_SIZE 6 -class MTV_PUBLIC MythRAOPDevice : public QTcpServer +class MTV_PUBLIC MythRAOPDevice : public ServerPool { Q_OBJECT @@ -27,7 +27,7 @@ class MTV_PUBLIC MythRAOPDevice : public QTcpServer private slots: void Start(); - void newConnection(); + void newConnection(QTcpSocket *client); void deleteClient(); private: From 34c46f1933e3480bd36441b737f46f5058c85e5e Mon Sep 17 00:00:00 2001 From: Jean-Yves Avenard Date: Wed, 2 May 2012 04:35:07 +1000 Subject: [PATCH 37/69] Fix handling of IPv6 link-local address ServerPool previously couldn't use IPv6 link-local addresses. Also, fix the issue where no messages could be received on UDP bound sockets initiated from ServerPool --- mythtv/libs/libmythbase/serverpool.cpp | 139 +++++++++++++++++++------ mythtv/libs/libmythbase/serverpool.h | 6 +- 2 files changed, 114 insertions(+), 31 deletions(-) diff --git a/mythtv/libs/libmythbase/serverpool.cpp b/mythtv/libs/libmythbase/serverpool.cpp index 8cccc050917..66775414e81 100644 --- a/mythtv/libs/libmythbase/serverpool.cpp +++ b/mythtv/libs/libmythbase/serverpool.cpp @@ -1,4 +1,3 @@ - #include #include #include @@ -21,6 +20,36 @@ static QList naList_4; static QList naList_6; static QReadWriteLock naLock; +class PrivUdpSocket : public QUdpSocket +{ +public: + PrivUdpSocket(QObject *parent, QNetworkAddressEntry host) : + QUdpSocket(parent), m_host(host) { }; + ~PrivUdpSocket() { }; + QNetworkAddressEntry host() + { + return m_host; + }; + bool contains(QHostAddress addr) + { + return contains(m_host, addr); + }; + static bool contains(QNetworkAddressEntry host, QHostAddress addr) + { +#if !defined(QT_NO_IPV6) + if (addr.protocol() == QAbstractSocket::IPv6Protocol && + addr.isInSubnet(QHostAddress::parseSubnet("fe80::/10")) && + host.ip().scopeId() != addr.scopeId()) + { + return false; + } +#endif + return addr.isInSubnet(host.ip(), host.prefixLength()); + } +private: + QNetworkAddressEntry m_host; +}; + PrivTcpServer::PrivTcpServer(QObject *parent) : QTcpServer(parent) { } @@ -32,7 +61,7 @@ void PrivTcpServer::incomingConnection(int socket) ServerPool::ServerPool(QObject *parent) : QObject(parent), m_listening(false), m_maxPendingConn(30), m_port(0), - m_proxy(QNetworkProxy::DefaultProxy), m_udpSend(NULL) + m_proxy(QNetworkProxy::DefaultProxy), m_lastUdpSocket(NULL) { } @@ -69,6 +98,9 @@ void ServerPool::SelectDefaultListen(bool force) QList::const_iterator qni; for (qni = IFs.begin(); qni != IFs.end(); ++qni) { + if (qni->flags() & QNetworkInterface::IsRunning == 0) + continue; + QList IPs = qni->addressEntries(); QList::const_iterator qnai; for (qnai = IPs.begin(); qnai != IPs.end(); ++qnai) @@ -137,20 +169,25 @@ void ServerPool::SelectDefaultListen(bool force) else if (config_v6.isNull() && (ip.protocol() == QAbstractSocket::IPv6Protocol)) { - // IPv6 address is not defined, populate one - // put in additional block filtering here? - if (!ip.isInSubnet(QHostAddress::parseSubnet("fe80::/10"))) + bool linklocal = false; + if (ip.isInSubnet(QHostAddress::parseSubnet("fe80::/10"))) { - LOG(VB_GENERAL, LOG_DEBUG, - QString("Adding '%1' to address list.") - .arg(PRETTYIP_(ip))); - naList_6.append(*qnai); + // Link-local address, find its scope ID (interface name) + QNetworkAddressEntry ae = *qnai; + QHostAddress ha = ip; + ha.setScopeId(qni->name()); + ae.setIp(ha); + naList_6.append(ae); + linklocal = true; } else - LOG(VB_GENERAL, LOG_DEBUG, QString("Skipping link-local " - "address during IPv6 autoselection: %1") - .arg(PRETTYIP_(ip))); - + { + naList_6.append(*qnai); + } + LOG(VB_GENERAL, LOG_DEBUG, + QString("Adding%1 '%2' to address list.") + .arg(linklocal ? " link-local" : "") + .arg(PRETTYIP_(ip))); } #endif else @@ -267,7 +304,7 @@ void ServerPool::close(void) server->deleteLater(); } - QUdpSocket *socket; + PrivUdpSocket *socket; while (!m_udpSockets.isEmpty()) { socket = m_udpSockets.takeLast(); @@ -275,13 +312,7 @@ void ServerPool::close(void) socket->close(); socket->deleteLater(); } - - if (m_udpSend) - { - delete m_udpSend; - m_udpSend = NULL; - } - + m_lastUdpSocket = NULL; m_listening = false; } @@ -356,15 +387,44 @@ bool ServerPool::bind(QList addrs, quint16 port, for (it = addrs.begin(); it != addrs.end(); ++it) { - QUdpSocket *socket = new QUdpSocket(this); - connect(socket, SIGNAL(readyRead()), - this, SLOT(newUdpDatagram())); + QNetworkAddressEntry host; + +#if !defined(QT_NO_IPV6) + if (it->protocol() == QAbstractSocket::IPv6Protocol) + { + QList::iterator iae; + for (iae = naList_6.begin(); iae != naList_6.end(); iae++) + { + if (PrivUdpSocket::contains(*iae, *it)) + { + host = *iae; + break; + } + } + } + else +#endif + { + QList::iterator iae; + for (iae = naList_4.begin(); iae != naList_4.end(); iae++) + { + if (PrivUdpSocket::contains(*iae, *it)) + { + host = *iae; + break; + } + } + } + + PrivUdpSocket *socket = new PrivUdpSocket(this, host); if (socket->bind(*it, port)) { LOG(VB_GENERAL, LOG_INFO, QString("Binding to UDP %1:%2") .arg(PRETTYIP(it)).arg(port)); m_udpSockets.append(socket); + connect(socket, SIGNAL(readyRead()), + this, SLOT(newUdpDatagram())); } else { @@ -388,9 +448,6 @@ bool ServerPool::bind(QList addrs, quint16 port, if (m_udpSockets.size() == 0) return false; - if (!m_udpSend) - m_udpSend = new QUdpSocket(); - m_listening = true; return true; } @@ -412,14 +469,38 @@ bool ServerPool::bind(quint16 port, bool requireall) qint64 ServerPool::writeDatagram(const char * data, qint64 size, const QHostAddress &addr, quint16 port) { - if (!m_listening || !m_udpSend) + if (!m_listening || m_udpSockets.size() == 0) { LOG(VB_GENERAL, LOG_ERR, "Trying to write datagram to disconnected " "ServerPool instance."); return -1; } - return m_udpSend->writeDatagram(data, size, addr, port); + // check if can re-use the last one, so there's no need for a linear search + if (!m_lastUdpSocket || !m_lastUdpSocket->contains(addr)) + { + // find the right socket to use + QList::iterator it; + for (it = m_udpSockets.begin(); it != m_udpSockets.end(); it++) + { + PrivUdpSocket *val = *it; + if (val->contains(addr)) + { + m_lastUdpSocket = val; + break; + } + } + } + if (!m_lastUdpSocket) + return -1; + + qint64 ret = m_lastUdpSocket->writeDatagram(data, size, addr, port); + if (ret != size) + { + LOG(VB_GENERAL, LOG_DEBUG, QString("Error = %1 : %2") + .arg(ret).arg(m_lastUdpSocket->error())); + } + return ret; } qint64 ServerPool::writeDatagram(const QByteArray &datagram, diff --git a/mythtv/libs/libmythbase/serverpool.h b/mythtv/libs/libmythbase/serverpool.h index 3351f373185..7dd4c77c77b 100644 --- a/mythtv/libs/libmythbase/serverpool.h +++ b/mythtv/libs/libmythbase/serverpool.h @@ -23,6 +23,8 @@ * methods to allow signalling for alternate socket types. */ +class PrivUdpSocket; + class PrivTcpServer : public QTcpServer { Q_OBJECT @@ -99,8 +101,8 @@ class MBASE_PUBLIC ServerPool : public QObject QNetworkProxy m_proxy; QList m_tcpServers; - QList m_udpSockets; - QUdpSocket *m_udpSend; + QList m_udpSockets; + PrivUdpSocket *m_lastUdpSocket; }; #endif From 1d375dbd5ea6b7a44d23e47c5a2f8d35c2e2f23c Mon Sep 17 00:00:00 2001 From: Jean-Yves Avenard Date: Wed, 2 May 2012 04:58:36 +1000 Subject: [PATCH 38/69] Add two network utility methods to ServerPool Will be used by Airplay and RAOP --- mythtv/libs/libmythbase/serverpool.cpp | 66 ++++++++++++++++++++++++++ mythtv/libs/libmythbase/serverpool.h | 2 + 2 files changed, 68 insertions(+) diff --git a/mythtv/libs/libmythbase/serverpool.cpp b/mythtv/libs/libmythbase/serverpool.cpp index 66775414e81..609e190a57c 100644 --- a/mythtv/libs/libmythbase/serverpool.cpp +++ b/mythtv/libs/libmythbase/serverpool.cpp @@ -534,6 +534,72 @@ void ServerPool::newUdpDatagram(void) } } +/** + * tryListeningPort + * + * Description: + * Tells the server to listen for incoming connections on port port. + * The server will attempt to listen on all local interfaces. + * + * Usage: + * baseport: port to listen on. + * range: range of ports to try (default 1) + * + * Returns port used on success; otherwise returns -1. + */ +int ServerPool::tryListeningPort(int baseport, int range) +{ + // try a few ports in case the first is in use + int port = baseport; + while (port < baseport + range) + { + if (listen(port)) + { + break; + } + port++; + } + + if (port >= baseport + range) + { + return -1; + } + return port; +} + +/** + * tryBindingPort + * + * Description: + * Binds this socket for incoming connections on port port. + * The socket will attempt to bind on all local interfaces. + * + * Usage: + * baseport: port to bind to. + * range: range of ports to try (default 1) + * + * Returns port used on success; otherwise returns -1. + */ +int ServerPool::tryBindingPort(int baseport, int range) +{ + // try a few ports in case the first is in use + int port = baseport; + while (port < baseport + range) + { + if (bind(port)) + { + break; + } + port++; + } + + if (port >= baseport + range) + { + return -1; + } + return port; +} + /** * tryListeningPort * diff --git a/mythtv/libs/libmythbase/serverpool.h b/mythtv/libs/libmythbase/serverpool.h index 7dd4c77c77b..bced16dd369 100644 --- a/mythtv/libs/libmythbase/serverpool.h +++ b/mythtv/libs/libmythbase/serverpool.h @@ -78,6 +78,8 @@ class MBASE_PUBLIC ServerPool : public QObject void close(void); + int tryListeningPort(int baseport, int range = 1); + int tryBindingPort(int baseport, int range = 1); // Utility functions static int tryListeningPort(QTcpServer *server, int baseport, int range = 1, bool *isipv6 = NULL); From 34dcb2bc7a882eb97fbdb9c43a480165c3551140 Mon Sep 17 00:00:00 2001 From: Jean-Yves Avenard Date: Wed, 2 May 2012 05:04:56 +1000 Subject: [PATCH 39/69] Add full IPv6 support to RAOP and fix Bonjour discovery of RAOP service. Do not advertise RAOP via Bonjour, if listening on the local interfaces failed --- mythtv/libs/libmythtv/mythraopconnection.cpp | 101 +++++++------------ mythtv/libs/libmythtv/mythraopconnection.h | 10 +- mythtv/libs/libmythtv/mythraopdevice.cpp | 95 +++++++++-------- 3 files changed, 85 insertions(+), 121 deletions(-) diff --git a/mythtv/libs/libmythtv/mythraopconnection.cpp b/mythtv/libs/libmythtv/mythraopconnection.cpp index c40dd3aad3a..2bfdd5d0da7 100644 --- a/mythtv/libs/libmythtv/mythraopconnection.cpp +++ b/mythtv/libs/libmythtv/mythraopconnection.cpp @@ -1,6 +1,5 @@ #include #include -#include #include #include @@ -153,24 +152,16 @@ bool MythRAOPConnection::Init(void) } // try a few ports in case the first is in use - int baseport = m_dataPort; - while (m_dataPort < baseport + RAOP_PORT_RANGE) - { - if (m_dataSocket->bind(m_dataPort)) - { - LOG(VB_GENERAL, LOG_INFO, LOC + - QString("Bound to port %1 for incoming data").arg(m_dataPort)); - break; - } - m_dataPort++; - } - - if (m_dataPort >= baseport + RAOP_PORT_RANGE) + m_dataPort = m_dataSocket->tryBindingPort(m_dataPort, RAOP_PORT_RANGE); + if (m_dataPort < 0) { LOG(VB_GENERAL, LOG_ERR, LOC + "Failed to bind to a port for data."); return false; } + LOG(VB_GENERAL, LOG_INFO, LOC + + QString("Bound to port %1 for incoming data").arg(m_dataPort)); + // load the private key if (!LoadKey()) return false; @@ -193,24 +184,6 @@ bool MythRAOPConnection::Init(void) * Socket incoming data signal handler * use for audio, control and timing socket */ -void MythRAOPConnection::udpDataReady(void) -{ - QUdpSocket *socket = dynamic_cast(sender()); - - while (socket->state() == QAbstractSocket::BoundState && - socket->hasPendingDatagrams()) - { - QByteArray buffer; - buffer.resize(socket->pendingDatagramSize()); - QHostAddress sender; - quint16 senderPort; - - socket->readDatagram(buffer.data(), buffer.size(), - &sender, &senderPort); - udpDataReady(buffer, sender, senderPort); - } -} - void MythRAOPConnection::udpDataReady(QByteArray buf, QHostAddress peer, quint16 port) { @@ -920,11 +893,19 @@ void MythRAOPConnection::ProcessRequest(const QStringList &header, else if (m_socket->localAddress().protocol() == QAbstractSocket::IPv6Protocol) { - // NB IPv6 untested Q_IPV6ADDR ip = m_socket->localAddress().toIPv6Address(); - //ip = qToBigEndian(ip); - memcpy(from + i, &ip, 16); - i += 16; + if(memcmp(&ip, + "\x00\x00\x00\x00" "\x00\x00\x00\x00" "\x00\x00\xff\xff", + 12) == 0) + { + memcpy(from + i, &ip[12], 4); + i += 4; + } + else + { + memcpy(from + i, &ip, 16); + i += 16; + } } memcpy(from + i, m_hardwareId.constData(), RAOP_HARDWARE_ID_SIZE); i += RAOP_HARDWARE_ID_SIZE; @@ -1058,9 +1039,10 @@ void MythRAOPConnection::ProcessRequest(const QStringList &header, delete m_clientControlSocket; } - m_clientControlSocket = new QUdpSocket(this); - int controlbind_port = findNextBindingPort(m_clientControlSocket, - control_port); + m_clientControlSocket = new ServerPool(this); + int controlbind_port = + m_clientControlSocket->tryBindingPort(control_port, + RAOP_PORT_RANGE); if (controlbind_port < 0) { LOG(VB_GENERAL, LOG_ERR, LOC + @@ -1074,8 +1056,10 @@ void MythRAOPConnection::ProcessRequest(const QStringList &header, .arg(control_port).arg(controlbind_port)); } m_clientControlPort = control_port; - connect(m_clientControlSocket, SIGNAL(readyRead()), - this, SLOT(udpDataReady())); + connect(m_clientControlSocket, + SIGNAL(newDatagram(QByteArray, QHostAddress, quint16)), + this, + SLOT(udpDataReady(QByteArray, QHostAddress, quint16))); if (m_clientTimingSocket) { @@ -1084,9 +1068,10 @@ void MythRAOPConnection::ProcessRequest(const QStringList &header, delete m_clientTimingSocket; } - m_clientTimingSocket = new QUdpSocket(this); - int timingbind_port = findNextBindingPort(m_clientTimingSocket, - timing_port); + m_clientTimingSocket = new ServerPool(this); + int timingbind_port = + m_clientTimingSocket->tryBindingPort(timing_port, + RAOP_PORT_RANGE); if (timingbind_port < 0) { LOG(VB_GENERAL, LOG_ERR, LOC + @@ -1100,8 +1085,10 @@ void MythRAOPConnection::ProcessRequest(const QStringList &header, .arg(timing_port).arg(timingbind_port)); } m_clientTimingPort = timing_port; - connect(m_clientTimingSocket, SIGNAL(readyRead()), - this, SLOT(udpDataReady())); + connect(m_clientTimingSocket, + SIGNAL(newDatagram(QByteArray, QHostAddress, quint16)), + this, + SLOT(udpDataReady(QByteArray, QHostAddress, quint16))); if (OpenAudioDevice()) CreateDecoder(); @@ -1421,7 +1408,7 @@ bool MythRAOPConnection::CreateDecoder(void) m_codec = avcodec_find_decoder(CODEC_ID_ALAC); if (!m_codec) { - LOG(VB_GENERAL, LOG_ERR, LOC + LOG(VB_GENERAL, LOG_ERR, LOC + "Failed to create ALAC decoder- going silent..."); return false; } @@ -1560,23 +1547,3 @@ int64_t MythRAOPConnection::AudioCardLatency(void) uint64_t audiots = m_audio->GetAudiotime(); return (int64_t)timestamp - (int64_t)audiots; } - -int MythRAOPConnection::findNextBindingPort(QUdpSocket *socket, int baseport) -{ - // try a few ports in case the first is in use - int port = baseport; - while (port < baseport + RAOP_PORT_RANGE) - { - if (socket->bind(port)) - { - break; - } - port++; - } - - if (port >= baseport + RAOP_PORT_RANGE) - { - return -1; - } - return port; -} diff --git a/mythtv/libs/libmythtv/mythraopconnection.h b/mythtv/libs/libmythtv/mythraopconnection.h index 05086918e57..4cdd20362fc 100644 --- a/mythtv/libs/libmythtv/mythraopconnection.h +++ b/mythtv/libs/libmythtv/mythraopconnection.h @@ -59,7 +59,6 @@ class MythRAOPConnection : public QObject public slots: void readClient(void); void udpDataReady(QByteArray buf, QHostAddress peer, quint16 port); - void udpDataReady(void); void timeout(void); void audioRetry(void); @@ -73,7 +72,6 @@ class MythRAOPConnection : public QObject void ExpireResendRequests(uint64_t timestamp); uint32_t decodeAudioPacket(uint8_t type, const QByteArray *buf, QList *dest); - void ProcessAudio(); int ExpireAudio(uint64_t timestamp); void ResetAudio(void); void ProcessRequest(const QStringList &header, @@ -101,7 +99,6 @@ class MythRAOPConnection : public QObject // utility functions int64_t AudioCardLatency(void); - int findNextBindingPort(QUdpSocket *socket, int port); QStringList splitLines(const QByteArray &lines); QString stringFromSeconds(int seconds); uint64_t framesToMs(uint64_t frames); @@ -118,9 +115,9 @@ class MythRAOPConnection : public QObject QHostAddress m_peerAddress; ServerPool *m_dataSocket; int m_dataPort; - QUdpSocket *m_clientControlSocket; + ServerPool *m_clientControlSocket; int m_clientControlPort; - QUdpSocket *m_clientTimingSocket; + ServerPool *m_clientTimingSocket; int m_clientTimingPort; // incoming audio @@ -174,6 +171,9 @@ class MythRAOPConnection : public QObject uint32_t m_progressEnd; QByteArray m_artwork; QByteArray m_dmap; + + private slots: + void ProcessAudio(void); }; #endif // MYTHRAOPCONNECTION_H diff --git a/mythtv/libs/libmythtv/mythraopdevice.cpp b/mythtv/libs/libmythtv/mythraopdevice.cpp index a42191176c6..78720046351 100644 --- a/mythtv/libs/libmythtv/mythraopdevice.cpp +++ b/mythtv/libs/libmythtv/mythraopdevice.cpp @@ -127,59 +127,56 @@ void MythRAOPDevice::Start(void) connect(this, SIGNAL(newConnection(QTcpSocket *)), this, SLOT(newConnection(QTcpSocket *))); - // start listening for connections (try a few ports in case the default is in use) int baseport = m_setupPort; - while (m_setupPort < baseport + RAOP_PORT_RANGE) + m_setupPort = tryListeningPort(m_setupPort, RAOP_PORT_RANGE); + // start listening for connections (try a few ports in case the default is in use) + if (m_setupPort < 0) { - if (listen(QNetworkInterface::allAddresses(), m_setupPort, false)) - { - LOG(VB_GENERAL, LOG_INFO, LOC + - QString("Listening for connections on port %1").arg(m_setupPort)); - break; - } - m_setupPort++; + LOG(VB_GENERAL, LOG_ERR, LOC + + "Failed to find a port for incoming connections."); } - - if (m_setupPort >= baseport + RAOP_PORT_RANGE) - LOG(VB_GENERAL, LOG_ERR, LOC + "Failed to find a port for incoming connections."); - - // announce service - m_bonjour = new BonjourRegister(this); - - // give each frontend a unique name - int multiple = m_setupPort - baseport; - if (multiple > 0) - m_name += QString::number(multiple); - - QByteArray name = m_hardwareId.toHex(); - name.append("@"); - name.append(m_name); - name.append(" on "); - name.append(gCoreContext->GetHostName()); - QByteArray type = "_raop._tcp"; - QByteArray txt; - txt.append(6); txt.append("tp=UDP"); - txt.append(8); txt.append("sm=false"); - txt.append(8); txt.append("sv=false"); - txt.append(4); txt.append("ek=1"); // - txt.append(6); txt.append("et=0,1"); // encryption type: no, RSA - txt.append(6); txt.append("cn=0,1"); // audio codec: pcm, alac - txt.append(4); txt.append("ch=2"); // audio channels - txt.append(5); txt.append("ss=16"); // sample size - txt.append(8); txt.append("sr=44100"); // sample rate - txt.append(8); txt.append("pw=false"); // no password - txt.append(4); txt.append("vn=3"); - txt.append(9); txt.append("txtvers=1"); // TXT record version 1 - txt.append(8); txt.append("md=0,1,2"); // metadata-type: text, artwork, progress - txt.append(9); txt.append("vs=130.14"); - txt.append(7); txt.append("da=true"); - - LOG(VB_GENERAL, LOG_INFO, QString("Registering service %1.%2 port %3 TXT %4") - .arg(QString(name)).arg(QString(type)).arg(m_setupPort).arg(QString(txt))); - if (!m_bonjour->Register(m_setupPort, type, name, txt)) + else { - LOG(VB_GENERAL, LOG_ERR, LOC + "Failed to register service."); - return; + LOG(VB_GENERAL, LOG_INFO, LOC + + QString("Listening for connections on port %1").arg(m_setupPort)); + // announce service + m_bonjour = new BonjourRegister(this); + + // give each frontend a unique name + int multiple = m_setupPort - baseport; + if (multiple > 0) + m_name += QString::number(multiple); + + QByteArray name = m_hardwareId.toHex(); + name.append("@"); + name.append(m_name); + name.append(" on "); + name.append(gCoreContext->GetHostName()); + QByteArray type = "_raop._tcp"; + QByteArray txt; + txt.append(6); txt.append("tp=UDP"); + txt.append(8); txt.append("sm=false"); + txt.append(8); txt.append("sv=false"); + txt.append(4); txt.append("ek=1"); // + txt.append(6); txt.append("et=0,1"); // encryption type: no, RSA + txt.append(6); txt.append("cn=0,1"); // audio codec: pcm, alac + txt.append(4); txt.append("ch=2"); // audio channels + txt.append(5); txt.append("ss=16"); // sample size + txt.append(8); txt.append("sr=44100"); // sample rate + txt.append(8); txt.append("pw=false"); // no password + txt.append(4); txt.append("vn=3"); + txt.append(9); txt.append("txtvers=1"); // TXT record version 1 + txt.append(8); txt.append("md=0,1,2"); // metadata-type: text, artwork, progress + txt.append(9); txt.append("vs=130.14"); + txt.append(7); txt.append("da=true"); + + LOG(VB_GENERAL, LOG_INFO, QString("Registering service %1.%2 port %3 TXT %4") + .arg(QString(name)).arg(QString(type)).arg(m_setupPort).arg(QString(txt))); + if (!m_bonjour->Register(m_setupPort, type, name, txt)) + { + LOG(VB_GENERAL, LOG_ERR, LOC + "Failed to register service."); + return; + } } m_valid = true; From c2ef5d143f1bfc9931ef9f7487fda3b93780fe8f Mon Sep 17 00:00:00 2001 From: Raymond Wagner Date: Tue, 1 May 2012 15:50:44 -0400 Subject: [PATCH 40/69] Use `date` rather than `datetime` for storing dates. --- tmdb3/tmdb_api.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/tmdb3/tmdb_api.py b/tmdb3/tmdb_api.py index db6b5e0c2d5..4e01d1941f0 100644 --- a/tmdb3/tmdb_api.py +++ b/tmdb3/tmdb_api.py @@ -59,6 +59,9 @@ DEBUG = False +def process_date(datestr): + return datetime.date(*[int(x) for x in datestr.split('-')]) + class Configuration( Element ): images = Datapoint('images') def _populate(self): @@ -176,10 +179,8 @@ class Person( Element ): id = Datapoint('id', initarg=1) name = Datapoint('name') biography = Datapoint('biography') - dayofbirth = Datapoint('birthday', default=None, handler=lambda x: \ - datetime.datetime.strptime(x, '%Y-%m-%d')) - dayofdeath = Datapoint('deathday', default=None, handler=lambda x: \ - datetime.datetime.strptime(x, '%Y-%m-%d')) + dayofbirth = Datapoint('birthday', default=None, handler=process_date) + dayofdeath = Datapoint('deathday', default=None, handler=process_date) homepage = Datapoint('homepage') birthplace = Datapoint('place_of_birth') profile = Datapoint('profile_path', handler=Profile, raw=False) @@ -226,8 +227,7 @@ def __repr__(self): class Release( Element ): certification = Datapoint('certification') country = Datapoint('iso_3166_1') - releasedate = Datapoint('release_date', handler=lambda x: \ - datetime.datetime.strptime(x, '%Y-%m-%d')) + releasedate = Datapoint('release_date', handler=process_date) def __repr__(self): return u"".format(self) @@ -385,8 +385,7 @@ def fromIMDB(cls, imdbid, locale=None): runtime = Datapoint('runtime') budget = Datapoint('budget') revenue = Datapoint('revenue') - releasedate = Datapoint('release_date', handler=lambda x: \ - datetime.date(*[int(y) for y in x.split('-')])) + releasedate = Datapoint('release_date', handler=process_date) homepage = Datapoint('homepage') imdb = Datapoint('imdb_id') From 8a4adc5b70816a0c278af0ca1a7c776786d99b03 Mon Sep 17 00:00:00 2001 From: Daniel Kristjansson Date: Tue, 1 May 2012 19:06:15 -0400 Subject: [PATCH 41/69] Provide GetNodeValue(.. const char*) implementation. When GetNodeValue is called with a C string as the default value, the value is promoted to an int not a QString() leading to unintended results. --- mythtv/libs/libmythupnp/soapclient.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/mythtv/libs/libmythupnp/soapclient.h b/mythtv/libs/libmythupnp/soapclient.h index 981d741f5bd..6b35a91e303 100644 --- a/mythtv/libs/libmythupnp/soapclient.h +++ b/mythtv/libs/libmythupnp/soapclient.h @@ -47,6 +47,12 @@ class UPNP_PUBLIC SOAPClient QString GetNodeValue(const QDomNode &node, const QString &sName, const QString &sDefault) const; + QString GetNodeValue(const QDomNode &node, + const QString &sName, + const char *sDefault) const + { + return GetNodeValue(node, sName, QString(sDefault)); + } QDomNode FindNode(const QString &sName, const QDomNode &baseNode) const; From f13eeb0bfee20070e4a2e47cc71210e172fe6380 Mon Sep 17 00:00:00 2001 From: Jim Stichnoth Date: Tue, 1 May 2012 21:55:26 -0700 Subject: [PATCH 42/69] Subtitles: Allow the theme to customize caption/subtitle formatting. Subtitle format specification, e.g. the choice of font and background, is moved into the theme's osd_subtitle.xml file, in the osd_subtitle window. Separate controls are given for text, teletext, CEA-608, and the 8 CEA-708 fonts. Subtitle types are named text, teletext, 608, 708_0, 708_1, ..., 708_7. Each subtitle type has a fontdef component for the text and a shape component for the background. By default, the attributes of a subtitle type come from either the provider (e.g. text color, italics) or the system default (e.g. font family). These attributes can be overridden by the xml file. The names of the fontdef and the background shape are both simply the name of the subtitle type. The fontdef and shape should ultimately inherit from the special value "provider", otherwise all provider values will be ignored. The following example forces .srt subtitles to be rendered in yellow text, FreeSans font, black shadow, with a translucent black background. The fontdef and shape names are "text" since the subtitles come from a .srt file. Note that in this example, color formatting controls in the .srt file will be ignored due to the explicit setting of yellow text. #FFFF00 2,2 #000000 The settings CCBackground and DefaultSubtitleFont are no longer necessary and have been removed. Teletext does not yet use this mechanism, but some temporary code allows the teletext font to be taken from osd_subtitle.xml rather than the DefaultSubtitleFont setting. Note that as before, the frontend must be restarted for teletext font changes to take effect (this will be fixed in the future). All elements/attributes of fontdef and shape can be used. Note however that area and position are explicitly ignored, as these are dictated by the subtitle layout. The default values for the specific fonts are kept unchanged from the original code (except that a proportional font is now used for text subtitles). Note that some of these fonts, particularly many of the CEA-708 fonts, were somewhat arbitrarily chosen and aren't necessarily present on the user's system, so we will need to settle on the precise set of fonts and likely include them in the MythTV fonts directory. These changes are implemented with almost no MythUI modifications (the only such change is to make the new helper class a friend of MythUIShape in order to get access to some protected members). The basic idea is to construct two different "provider" objects; then parse osd_subtitle.xml twice with respect to each provider object; and finally compare the resulting objects, attribute by attribute, to discover which attributes are fixed by the theme and which the provider may control. This is done once to collect the set of attributes, and the results are cached and used for drawing each subtitle. --- mythtv/libs/libmythtv/cc708window.h | 5 +- mythtv/libs/libmythtv/subtitlescreen.cpp | 790 ++++++++++++++---- mythtv/libs/libmythtv/subtitlescreen.h | 52 +- mythtv/libs/libmythtv/teletextscreen.cpp | 7 +- mythtv/libs/libmythui/mythuishape.h | 1 + .../programs/mythfrontend/globalsettings.cpp | 31 - mythtv/themes/default/osd_subtitle.xml | 57 ++ 7 files changed, 716 insertions(+), 227 deletions(-) create mode 100644 mythtv/themes/default/osd_subtitle.xml diff --git a/mythtv/libs/libmythtv/cc708window.h b/mythtv/libs/libmythtv/cc708window.h index c5657db8d52..12fc066e0bf 100644 --- a/mythtv/libs/libmythtv/cc708window.h +++ b/mythtv/libs/libmythtv/cc708window.h @@ -90,7 +90,7 @@ class CC708CharacterAttribute QColor actual_fg_color; CC708CharacterAttribute(bool isItalic, bool isBold, bool isUnderline, - QColor fgColor, bool hasBackground) : + QColor fgColor) : pen_size(k708AttrSizeStandard), offset(k708AttrOffsetNormal), text_tag(0), // "dialog", ignored @@ -102,8 +102,7 @@ class CC708CharacterAttribute fg_color(k708AttrColorWhite), // will be overridden fg_opacity(k708AttrOpacitySolid), // solid bg_color(k708AttrColorBlack), - bg_opacity(hasBackground ? k708AttrOpacitySolid : - k708AttrOpacityTransparent), + bg_opacity(k708AttrOpacitySolid), edge_color(k708AttrColorBlack), override_fg_color(true), actual_fg_color(fgColor) diff --git a/mythtv/libs/libmythtv/subtitlescreen.cpp b/mythtv/libs/libmythtv/subtitlescreen.cpp index e92a8b55003..f3c8914d22d 100644 --- a/mythtv/libs/libmythtv/subtitlescreen.cpp +++ b/mythtv/libs/libmythtv/subtitlescreen.cpp @@ -11,6 +11,507 @@ #define LOC QString("Subtitles: ") #define LOC_WARN QString("Subtitles Warning: ") + +// Class SubtitleFormat manages fonts and backgrounds for subtitles. +// +// Formatting is specified by the theme in the new file +// osd_subtitle.xml, in the osd_subtitle window. Subtitle types are +// text, teletext, 608, 708_0, 708_1, ..., 708_7. Each subtitle type +// has a fontdef component for the text and a shape component for the +// background. By default, the attributes of a subtitle type come +// from either the provider (e.g. font color, italics) or the system +// default (e.g. font family). These attributes can be overridden by +// the xml file. +// +// The fontdef name and the background shape name are both simply the +// name of the subtitle type. The fontdef and shape should ultimately +// inherit from the special value "provider", otherwise all provider +// values will be ignored. +// +// The following example forces .srt subtitles to be rendered in +// yellow text, FreeSans font, black shadow, with a translucent black +// background. The fontdef and shape names are "text" since the +// subtitles come from a .srt file. Note that in this example, color +// formatting controls in the .srt file will be ignored due to the +// explicit setting of yellow text. +// +// +// +// +// +// +// #FFFF00 +// 2,2 +// #000000 +// +// +// +// +// +// +// +// All elements/attributes of fontdef and shape can be used. Note +// however that area and position are explicitly ignored, as these are +// dictated by the subtitle layout. +// +// This is implemented with almost no MythUI changes. Two copies of a +// "provider" object are created, with default/representative provider +// attributes. One copy is then "complemented" to have a different +// value for each attribute that a provider might change. The +// osd_subtitle.xml file is loaded twice, once with respect to each +// provider object, and the desired fontdef or shape is looked up. +// The two fontdefs or shapes are compared attribute by attribute, and +// each attribute that is different is an attribute that the provider +// may modify, whereas each identical attribute represents one that is +// fixed by the theme. +class SubtitleFormat +{ +public: + SubtitleFormat(void) {} + ~SubtitleFormat(void); + MythFontProperties *GetFont(const QString &family, + const CC708CharacterAttribute &attr, + int pixelSize, bool isTeletext, + int zoom, int stretch); + MythUIShape *GetBackground(MythUIType *parent, const QString &name, + const QString &family, + const CC708CharacterAttribute &attr); + static QString MakePrefix(const QString &family, + const CC708CharacterAttribute &attr); + bool IsUnlocked(const QString &prefix, const QString &property) const + { + return m_changeMap[prefix].contains(property); + } +private: + void Load(const QString &family, + const CC708CharacterAttribute &attr); + static void CreateProviderDefault(const QString &family, + const CC708CharacterAttribute &attr, + MythUIType *parent, + bool isComplement, + MythFontProperties **font, + MythUIShape **bg); + static void Complement(MythFontProperties *font, MythUIShape *bg); + static QSet Diff(const QString &family, + const CC708CharacterAttribute &attr, + MythFontProperties *font1, + MythFontProperties *font2, + MythUIShape *bg1, + MythUIShape *bg2); + + QHash m_fontMap; + QHash m_shapeMap; + QHash > m_changeMap; + QHash m_pixelSizeMap; + QVector m_cleanup; +}; + +static const QString kSubProvider("provider"); +static const QString kSubFileName ("osd_subtitle.xml"); +static const QString kSubWindowName("osd_subtitle"); +static const QString kSubFamily608 ("608"); +static const QString kSubFamily708 ("708"); +static const QString kSubFamilyText ("text"); +static const QString kSubFamilyTeletext("teletext"); + +static const QString kSubAttrItalics ("italics"); +static const QString kSubAttrBold ("bold"); +static const QString kSubAttrUnderline("underline"); +static const QString kSubAttrPixelsize("pixelsize"); +static const QString kSubAttrColor ("color"); +static const QString kSubAttrBGfill ("bgfill"); +static const QString kSubAttrShadow ("shadow"); +static const QString kSubAttrShadowoffset("shadowoffset"); +static const QString kSubAttrShadowcolor ("shadowcolor"); +static const QString kSubAttrShadowalpha ("shadowalpha"); +static const QString kSubAttrOutline ("outline"); +static const QString kSubAttrOutlinecolor("outlinecolor"); +static const QString kSubAttrOutlinesize ("outlinesize"); +static const QString kSubAttrOutlinealpha("outlinealpha"); + +static QString srtColorString(QColor color); +static QString fontToString(MythFontProperties *f) +{ + QString result; + result = QString("face=%1 pixelsize=%2 color=%3 " + "italics=%4 weight=%5 underline=%6") + .arg(f->GetFace()->family()) + .arg(f->GetFace()->pixelSize()) + .arg(srtColorString(f->color())) + .arg(f->GetFace()->italic()) + .arg(f->GetFace()->weight()) + .arg(f->GetFace()->underline()); + QPoint offset; + QColor color; + int alpha; + int size; + f->GetShadow(offset, color, alpha); + result += QString(" shadow=%1 shadowoffset=%2 " + "shadowcolor=%3 shadowalpha=%4") + .arg(f->hasShadow()) + .arg(QString("(%1,%2)").arg(offset.x()).arg(offset.y())) + .arg(srtColorString(color)) + .arg(alpha); + f->GetOutline(color, size, alpha); + result += QString(" outline=%1 outlinecolor=%2 " + "outlinesize=%3 outlinealpha=%4") + .arg(f->hasOutline()) + .arg(srtColorString(color)) + .arg(size) + .arg(alpha); + return result; +} + +SubtitleFormat::~SubtitleFormat(void) +{ + for (int i = 0; i < m_cleanup.size(); ++i) + { + m_cleanup[i]->DeleteAllChildren(); + delete m_cleanup[i]; + m_cleanup[i] = NULL; // just to be safe + } +} + +QString SubtitleFormat::MakePrefix(const QString &family, + const CC708CharacterAttribute &attr) +{ + if (family == kSubFamily708) + return family + "_" + QString::number(attr.font_tag & 0x7); + else + return family; +} + +void SubtitleFormat::CreateProviderDefault(const QString &family, + const CC708CharacterAttribute &attr, + MythUIType *parent, + bool isComplement, + MythFontProperties **returnFont, + MythUIShape **returnBg) +{ + MythFontProperties *font = new MythFontProperties(); + MythUIShape *bg = new MythUIShape(parent, kSubProvider); + if (family == kSubFamily608) + { + font->GetFace()->setFamily("FreeMono"); + bg->SetFillBrush(QBrush(Qt::black)); + } + else if (family == kSubFamily708) + { + static const char *cc708Fonts[] = { + "FreeMono", // default + "FreeMono", // mono serif + "DejaVu Serif", // prop serif + "Droid Sans Mono", // mono sans + "Liberation Sans", // prop sans + "Purisa", // casual + "URW Chancery L", // cursive + "Impact" // capitals + }; + font->GetFace()->setFamily(cc708Fonts[attr.font_tag & 0x7]); + } + else if (family == kSubFamilyText) + { + font->GetFace()->setFamily("Droid Sans"); + bg->SetFillBrush(QBrush(Qt::black)); + } + else if (family == kSubFamilyTeletext) + { + font->GetFace()->setFamily("FreeMono"); + } + font->GetFace()->setPixelSize(10); + + if (isComplement) + Complement(font, bg); + parent->AddFont(kSubProvider, font); + + *returnFont = font; + *returnBg = bg; +} + +static QColor differentColor(const QColor &color) +{ + return color == Qt::white ? Qt::black : Qt::white; +} + +// Change everything (with respect to the == operator) that the +// provider might define or override. +void SubtitleFormat::Complement(MythFontProperties *font, MythUIShape *bg) +{ + QPoint offset; + QColor color; + int alpha; + int size; + QFont *face = font->GetFace(); + face->setItalic(!face->italic()); + face->setPixelSize(face->pixelSize() + 1); + face->setUnderline(!face->underline()); + face->setWeight((face->weight() + 1) % 32); + font->SetColor(differentColor(font->color())); + + font->GetShadow(offset, color, alpha); + offset.setX(offset.x() + 1); + font->SetShadow(!font->hasShadow(), offset, differentColor(color), + 255 - alpha); + + font->GetOutline(color, size, alpha); + font->SetOutline(!font->hasOutline(), differentColor(color), + size + 1, 255 - alpha); + + bg->SetFillBrush(bg->m_fillBrush == Qt::NoBrush ? + Qt::SolidPattern : Qt::NoBrush); +} + +void SubtitleFormat::Load(const QString &family, + const CC708CharacterAttribute &attr) +{ + // Widgets for the actual values + MythUIType *baseParent = new MythUIType(NULL, "base"); + m_cleanup += baseParent; + MythFontProperties *providerBaseFont; + MythUIShape *providerBaseShape; + CreateProviderDefault(family, attr, baseParent, false, + &providerBaseFont, &providerBaseShape); + + // Widgets for the "negative" values + MythUIType *negParent = new MythUIType(NULL, "base"); + m_cleanup += negParent; + MythFontProperties *negFont; + MythUIShape *negBG; + CreateProviderDefault(family, attr, negParent, true, &negFont, &negBG); + + bool posResult = + XMLParseBase::LoadWindowFromXML(kSubFileName, kSubWindowName, + baseParent); + bool negResult = + XMLParseBase::LoadWindowFromXML(kSubFileName, kSubWindowName, + negParent); + if (!posResult || !negResult) + LOG(VB_VBI, LOG_INFO, + QString("Couldn't load theme file %1").arg(kSubFileName)); + QString prefix = MakePrefix(family, attr); + MythFontProperties *resultFont = baseParent->GetFont(prefix); + if (!resultFont) + resultFont = providerBaseFont; + MythUIShape *resultBG = + dynamic_cast(baseParent->GetChild(prefix)); + if (!resultBG) + resultBG = providerBaseShape; + MythFontProperties *testFont = negParent->GetFont(prefix); + if (!testFont) + testFont = negFont; + MythUIShape *testBG = + dynamic_cast(negParent->GetChild(prefix)); + if (!testBG) + testBG = negBG; + m_fontMap[prefix] = resultFont; + m_shapeMap[prefix] = resultBG; + LOG(VB_VBI, LOG_DEBUG, + QString("providerBaseFont = %1").arg(fontToString(providerBaseFont))); + LOG(VB_VBI, LOG_DEBUG, + QString("negFont = %1").arg(fontToString(negFont))); + LOG(VB_VBI, LOG_DEBUG, + QString("resultFont = %1").arg(fontToString(resultFont))); + LOG(VB_VBI, LOG_DEBUG, + QString("testFont = %1").arg(fontToString(testFont))); + m_changeMap[prefix] = Diff(family, attr, resultFont, testFont, + resultBG, testBG); + if (!IsUnlocked(prefix, kSubAttrPixelsize)) + m_pixelSizeMap[prefix] = resultFont->GetFace()->pixelSize(); +} + +QSet SubtitleFormat::Diff(const QString &family, + const CC708CharacterAttribute &attr, + MythFontProperties *font1, + MythFontProperties *font2, + MythUIShape *bg1, + MythUIShape *bg2) +{ + bool is708 = (family == kSubFamily708); + QSet result; + QFont *face1 = font1->GetFace(); + QFont *face2 = font2->GetFace(); + if (face1->italic() != face2->italic()) + result << kSubAttrItalics; + if (face1->weight() != face2->weight()) + result << kSubAttrBold; + if (face1->underline() != face2->underline()) + result << kSubAttrUnderline; + if (face1->pixelSize() != face2->pixelSize()) + result << kSubAttrPixelsize; + if (font1->color() != font2->color()) + result << kSubAttrColor; + if (is708 && font1->hasShadow() != font2->hasShadow()) + { + result << kSubAttrShadow; + QPoint offset1, offset2; + QColor color1, color2; + int alpha1, alpha2; + font1->GetShadow(offset1, color1, alpha1); + font2->GetShadow(offset2, color2, alpha2); + if (offset1 != offset2) + result << kSubAttrShadowoffset; + if (color1 != color2) + result << kSubAttrShadowcolor; + if (alpha1 != alpha2) + result << kSubAttrShadowalpha; + } + if (is708 && font1->hasOutline() != font2->hasOutline()) + { + result << kSubAttrOutline; + QColor color1, color2; + int size1, size2; + int alpha1, alpha2; + font1->GetOutline(color1, size1, alpha1); + font2->GetOutline(color2, size2, alpha2); + if (color1 != color2) + result << kSubAttrOutlinecolor; + if (size1 != size2) + result << kSubAttrOutlinesize; + if (alpha1 != alpha2) + result << kSubAttrOutlinealpha; + } + if (bg1->m_fillBrush != bg2->m_fillBrush) + result << kSubAttrBGfill; + + QString values = ""; + QSet::const_iterator i = result.constBegin(); + for (; i != result.constEnd(); ++i) + values += " " + (*i); + LOG(VB_VBI, LOG_INFO, + QString("Subtitle family %1 allows provider to change:%2") + .arg(MakePrefix(family, attr)).arg(values)); + + return result; +} + +MythFontProperties * +SubtitleFormat::GetFont(const QString &family, + const CC708CharacterAttribute &attr, + int pixelSize, bool isTeletext, + int zoom, int stretch) +{ + int origPixelSize = pixelSize; + float scale = zoom / 100.0; + if (isTeletext) + scale = scale * 17 / 25; + if ((attr.pen_size & 0x3) == k708AttrSizeSmall) + scale = scale * 32 / 42; + else if ((attr.pen_size & 0x3) == k708AttrSizeLarge) + scale = scale * 42 / 32; + + QString prefix = MakePrefix(family, attr); + if (!m_fontMap.contains(prefix)) + Load(family, attr); + MythFontProperties *result = m_fontMap[prefix]; + + // Apply the scaling factor to pixelSize even if the theme + // explicitly sets pixelSize. + if (!IsUnlocked(prefix, kSubAttrPixelsize)) + pixelSize = m_pixelSizeMap[prefix]; + pixelSize *= scale; + result->GetFace()->setPixelSize(pixelSize); + + result->GetFace()->setStretch(stretch); + if (IsUnlocked(prefix, kSubAttrItalics)) + result->GetFace()->setItalic(attr.italics); + if (IsUnlocked(prefix, kSubAttrUnderline)) + result->GetFace()->setUnderline(attr.underline); + if (IsUnlocked(prefix, kSubAttrBold)) + result->GetFace()->setBold(attr.boldface); + if (IsUnlocked(prefix, kSubAttrColor)) + result->SetColor(attr.GetFGColor()); + if (IsUnlocked(prefix, kSubAttrShadow)) + { + QPoint offset; + QColor color; + int alpha; + bool shadow = result->hasShadow(); + result->GetShadow(offset, color, alpha); + if (IsUnlocked(prefix, kSubAttrShadowcolor)) + color = attr.GetEdgeColor(); + if (IsUnlocked(prefix, kSubAttrShadowalpha)) + alpha = attr.GetFGAlpha(); + if (IsUnlocked(prefix, kSubAttrShadowoffset)) + { + int off = pixelSize / 20; + offset = QPoint(off, off); + if (attr.edge_type == k708AttrEdgeLeftDropShadow) + { + shadow = true; + offset.setX(-off); + } + else if (attr.edge_type == k708AttrEdgeRightDropShadow) + shadow = true; + else + shadow = false; + } + result->SetShadow(shadow, offset, color, alpha); + } + if (IsUnlocked(prefix, kSubAttrOutline)) + { + QColor color; + int off; + int alpha; + bool outline = result->hasOutline(); + result->GetOutline(color, off, alpha); + if (IsUnlocked(prefix, kSubAttrOutlinecolor)) + color = attr.GetEdgeColor(); + if (IsUnlocked(prefix, kSubAttrOutlinealpha)) + alpha = attr.GetFGAlpha(); + if (IsUnlocked(prefix, kSubAttrOutlinesize)) + { + if (attr.edge_type == k708AttrEdgeUniform || + attr.edge_type == k708AttrEdgeRaised || + attr.edge_type == k708AttrEdgeDepressed) + { + outline = true; + off = pixelSize / 20; + } + else + outline = false; + } + result->SetOutline(outline, color, off, alpha); + } + LOG(VB_VBI, LOG_DEBUG, + QString("GetFont(family=%1, prefix=%2, orig pixelSize=%3, " + "new pixelSize=%4 zoom=%5) = %6") + .arg(family).arg(prefix).arg(origPixelSize).arg(pixelSize) + .arg(zoom).arg(fontToString(result))); + return result; +} + +MythUIShape * +SubtitleFormat::GetBackground(MythUIType *parent, const QString &name, + const QString &family, + const CC708CharacterAttribute &attr) +{ + QString prefix = MakePrefix(family, attr); + if (!m_shapeMap.contains(prefix)) + Load(family, attr); + MythUIShape *result = new MythUIShape(parent, name); + result->CopyFrom(m_shapeMap[prefix]); + if (family == kSubFamily708) + { + if (IsUnlocked(prefix, kSubAttrBGfill)) + { + result->SetFillBrush(QBrush(attr.GetBGColor())); + } + } + else if (family == kSubFamilyTeletext) + { + // add code here when teletextscreen.cpp is refactored + } + LOG(VB_VBI, LOG_DEBUG, + QString("GetBackground(prefix=%1) = " + "{type=%2 alpha=%3 brushstyle=%4 brushcolor=%5}") + .arg(prefix).arg(result->m_type).arg(result->GetAlpha()) + .arg(result->m_fillBrush.style()) + .arg(srtColorString(result->m_fillBrush.color()))); + return result; +} + +//////////////////////////////////////////////////////////////////////////// + static const float PAD_WIDTH = 0.5; static const float PAD_HEIGHT = 0.04; @@ -23,11 +524,11 @@ SubtitleScreen::SubtitleScreen(MythPlayer *player, const char * name, int fontStretch) : MythScreenType((MythScreenType*)NULL, name), m_player(player), m_subreader(NULL), m_608reader(NULL), - m_708reader(NULL), m_safeArea(QRect()), m_useBackground(false), + m_708reader(NULL), m_safeArea(QRect()), m_removeHTML(QRegExp("")), m_subtitleType(kDisplayNone), m_textFontZoom(100), m_refreshArea(false), m_fontStretch(fontStretch), - m_fontsAreInitialized(false) + m_format(new SubtitleFormat) { m_removeHTML.setMinimal(true); @@ -43,6 +544,7 @@ SubtitleScreen::SubtitleScreen(MythPlayer *player, const char * name, SubtitleScreen::~SubtitleScreen(void) { ClearAllSubtitles(); + delete m_format; #ifdef USING_LIBASS CleanupAssLibrary(); #endif @@ -71,6 +573,19 @@ void SubtitleScreen::EnableSubtitles(int type, bool forced_only) ClearAllSubtitles(); SetVisible(m_subtitleType != kDisplayNone); SetArea(MythRect()); + switch (m_subtitleType) + { + case kDisplayTextSubtitle: + case kDisplayRawTextSubtitle: + m_family = kSubFamilyText; + break; + case kDisplayCC608: + m_family = kSubFamily608; + break; + case kDisplayCC708: + m_family = kSubFamily708; + break; + } } void SubtitleScreen::DisableForcedSubtitles(void) @@ -96,20 +611,8 @@ bool SubtitleScreen::Create(void) LOG(VB_GENERAL, LOG_WARNING, LOC + "Failed to get CEA-608 reader."); if (!m_708reader) LOG(VB_GENERAL, LOG_WARNING, LOC + "Failed to get CEA-708 reader."); - m_useBackground = (bool)gCoreContext->GetNumSetting("CCBackground", 0); m_textFontZoom = gCoreContext->GetNumSetting("OSDCC708TextZoom", 100); - QString defaultFont = - gCoreContext->GetSetting("DefaultSubtitleFont", "FreeMono"); - m_fontNames.append(defaultFont); // default - m_fontNames.append("FreeMono"); // mono serif - m_fontNames.append("DejaVu Serif"); // prop serif - m_fontNames.append("Droid Sans Mono"); // mono sans - m_fontNames.append("Liberation Sans"); // prop sans - m_fontNames.append("Purisa"); // casual - m_fontNames.append("URW Chancery L"); // cursive - m_fontNames.append("Impact"); // capitals - return true; } @@ -363,17 +866,9 @@ void SubtitleScreen::DisplayTextSubtitles(void) bool changed = false; VideoOutput *vo = m_player->GetVideoOutput(); - if (vo) - { - QRect oldsafe = m_safeArea; - m_safeArea = vo->GetSafeRect(); - changed = (oldsafe != m_safeArea); - InitializeFonts(changed); - } - else - { + if (!vo) return; - } + m_safeArea = vo->GetSafeRect(); VideoFrame *currentFrame = vo->GetLastShownFrame(); if (!currentFrame) @@ -450,11 +945,7 @@ void SubtitleScreen::DisplayRawTextSubtitles(void) if (!currentFrame) return; - bool changed = false; - QRect oldsafe = m_safeArea; m_safeArea = vo->GetSafeRect(); - changed = (oldsafe != m_safeArea); - InitializeFonts(changed); // delete old subs that may still be on screen DeleteAllChildren(); @@ -464,11 +955,11 @@ void SubtitleScreen::DisplayRawTextSubtitles(void) void SubtitleScreen::DrawTextSubtitles(QStringList &wrappedsubs, uint64_t start, uint64_t duration) { - FormattedTextSubtitle fsub(m_safeArea, m_useBackground, this); + FormattedTextSubtitle fsub(m_safeArea, this); fsub.InitFromSRT(wrappedsubs, m_textFontZoom); fsub.WrapLongLines(); fsub.Layout(); - m_refreshArea = fsub.Draw(0, start, duration) || m_refreshArea; + m_refreshArea = fsub.Draw(m_family, NULL, start, duration) || m_refreshArea; } void SubtitleScreen::DisplayDVDButton(AVSubtitle* dvdButton, QRect &buttonPos) @@ -607,18 +1098,9 @@ void SubtitleScreen::DisplayCC608Subtitles(void) bool changed = false; - if (m_player && m_player->GetVideoOutput()) - { - QRect oldsafe = m_safeArea; - m_safeArea = m_player->GetVideoOutput()->GetSafeRect(); - if (oldsafe != m_safeArea) - changed = true; - } - else - { + if (!m_player || !m_player->GetVideoOutput()) return; - } - InitializeFonts(changed); + m_safeArea = m_player->GetVideoOutput()->GetSafeRect(); CC608Buffer* textlist = m_608reader->GetOutputText(changed); if (!changed) @@ -639,11 +1121,11 @@ void SubtitleScreen::DisplayCC608Subtitles(void) return; } - FormattedTextSubtitle fsub(m_safeArea, m_useBackground, this); + FormattedTextSubtitle fsub(m_safeArea, this); fsub.InitFromCC608(textlist->buffers, m_textFontZoom); fsub.Layout608(); fsub.Layout(); - m_refreshArea = fsub.Draw() || m_refreshArea; + m_refreshArea = fsub.Draw(m_family) || m_refreshArea; textlist->lock.unlock(); } @@ -672,8 +1154,6 @@ void SubtitleScreen::DisplayCC708Subtitles(void) return; } - InitializeFonts(changed); - for (uint i = 0; i < 8; i++) { CC708Window &win = cc708service->windows[i]; @@ -688,7 +1168,7 @@ void SubtitleScreen::DisplayCC708Subtitles(void) vector list = win.GetStrings(); if (!list.empty()) { - FormattedTextSubtitle fsub(m_safeArea, m_useBackground, this); + FormattedTextSubtitle fsub(m_safeArea, this); fsub.InitFromCC708(win, i, list, video_aspect, m_textFontZoom); fsub.Layout(); // Draw the window background after calculating bounding @@ -704,7 +1184,7 @@ void SubtitleScreen::DisplayCC708Subtitles(void) m_refreshArea = true; } m_refreshArea = - fsub.Draw(&m_708imageCache[i]) || m_refreshArea; + fsub.Draw(m_family, &m_708imageCache[i]) || m_refreshArea; } for (uint j = 0; j < list.size(); j++) delete list[j]; @@ -753,73 +1233,11 @@ void SubtitleScreen::AddScaledImage(QImage &img, QRect &pos) } } -void SubtitleScreen::InitializeFonts(bool wasResized) +MythFontProperties* SubtitleScreen::GetFont(CC708CharacterAttribute attr, + bool teletext) const { - if (!m_fontsAreInitialized) - { - int count = 0; - foreach(QString font, m_fontNames) - { - MythFontProperties *mythfont = new MythFontProperties(); - QFont newfont(font); - newfont.setStretch(m_fontStretch); - font.detach(); - mythfont->SetFace(newfont); - m_fontSet.insert(count, mythfont); - count++; - } - } - - if (wasResized || !m_fontsAreInitialized) - { - foreach(MythFontProperties* font, m_fontSet) - font->face().setStretch(m_fontStretch); - // XXX reset font sizes - } - - m_fontsAreInitialized = true; -} - -MythFontProperties* SubtitleScreen::Get708Font(CC708CharacterAttribute attr) - const -{ - MythFontProperties *mythfont = m_fontSet[attr.font_tag & 0x7]; - if (!mythfont) - return NULL; - - mythfont->GetFace()->setItalic(attr.italics); - mythfont->GetFace()->setPixelSize(m_708fontSizes[attr.pen_size & 0x3]); - mythfont->GetFace()->setUnderline(attr.underline); - mythfont->GetFace()->setBold(attr.boldface); - mythfont->SetColor(attr.GetFGColor()); - - int off = m_708fontSizes[attr.pen_size & 0x3] / 20; - QPoint shadowsz(off, off); - QColor colour = attr.GetEdgeColor(); - int alpha = attr.GetFGAlpha(); - bool outline = false; - bool shadow = false; - - if (attr.edge_type == k708AttrEdgeLeftDropShadow) - { - shadow = true; - shadowsz.setX(-off); - } - else if (attr.edge_type == k708AttrEdgeRightDropShadow) - { - shadow = true; - } - else if (attr.edge_type == k708AttrEdgeUniform || - attr.edge_type == k708AttrEdgeRaised || - attr.edge_type == k708AttrEdgeDepressed) - { - outline = true; - } - - mythfont->SetOutline(outline, colour, off, alpha); - mythfont->SetShadow(shadow, shadowsz, colour, alpha); - - return mythfont; + return m_format->GetFont(m_family, attr, m_fontSize, + teletext, m_textFontZoom, m_fontStretch); } static QString srtColorString(QColor color) @@ -843,20 +1261,21 @@ void FormattedTextSubtitle::InitFromCC608(vector &buffers, return; vector::iterator i = buffers.begin(); bool teletextmode = (*i)->teletextmode; - bool useBackground = m_useBackground && !teletextmode; int xscale = teletextmode ? 40 : 36; int yscale = teletextmode ? 25 : 17; - int pixelSize = m_safeArea.height() * textFontZoom - / (yscale * LINE_SPACING * 100); + // For the purpose of pixel size calculation, use a normalized + // size based on 17 non-teletext rows rather than 25. The + // shrinking is done in GetFont so that the theme can supply a + // uniform normalized pixelsize value. + int pixelSize = m_safeArea.height() / (/*yscale*/17 * LINE_SPACING); int fontwidth = 0; int xmid = 0; if (parent) { - parent->SetFontSizes(pixelSize, pixelSize, pixelSize); - CC708CharacterAttribute def_attr(false, false, false, clr[0], - useBackground); - QFont *font = parent->Get708Font(def_attr)->GetFace(); + parent->SetFontSize(pixelSize); + CC708CharacterAttribute def_attr(false, false, false, clr[0]); + QFont *font = parent->GetFont(def_attr, teletextmode)->GetFace(); QFontMetrics fm(*font); fontwidth = fm.averageCharWidth(); xmid = m_safeArea.width() / 2; @@ -903,9 +1322,9 @@ void FormattedTextSubtitle::InitFromCC608(vector &buffers, extract_cc608(text, cc->teletextmode, color, isItalic, isUnderline); CC708CharacterAttribute attr(isItalic, isBold, isUnderline, - clr[min(max(0, color), 7)], - useBackground); - FormattedTextChunk chunk(captionText, attr, parent); + clr[min(max(0, color), 7)]); + FormattedTextChunk chunk(captionText, attr, parent, + cc->teletextmode); line.chunks += chunk; LOG(VB_VBI, LOG_INFO, QString("Adding cc608 chunk (%1,%2): %3") @@ -925,9 +1344,9 @@ void FormattedTextSubtitle::InitFromCC708(const CC708Window &win, int num, "relative %5") .arg(num).arg(win.anchor_point).arg(win.anchor_horizontal) .arg(win.anchor_vertical).arg(win.relative_pos)); - int pixelSize = (m_safeArea.height() * textFontZoom) / 2000; + int pixelSize = m_safeArea.height() / 20; if (parent) - parent->SetFontSizes(pixelSize * 32 / 42, pixelSize, pixelSize * 42 / 32); + parent->SetFontSize(pixelSize); float xrange = win.relative_pos ? 100.0f : (aspect > 1.4f) ? 210.0f : 160.0f; @@ -945,11 +1364,6 @@ void FormattedTextSubtitle::InitFromCC708(const CC708Window &win, int num, { if (list[i]->y >= (uint)m_lines.size()) m_lines.resize(list[i]->y + 1); - if (m_useBackground) - { - list[i]->attr.bg_color = k708AttrColorBlack; - list[i]->attr.bg_opacity = k708AttrOpacitySolid; - } FormattedTextChunk chunk(list[i]->str, list[i]->attr, parent); m_lines[list[i]->y].chunks += chunk; LOG(VB_VBI, LOG_INFO, QString("Adding cc708 chunk: win %1 row %2: %3") @@ -973,9 +1387,9 @@ void FormattedTextSubtitle::InitFromSRT(QStringList &subs, int textFontZoom) // - change font color // - reset font color to white - int pixelSize = (m_safeArea.height() * textFontZoom) / 2000; + int pixelSize = m_safeArea.height() / 20; if (parent) - parent->SetFontSizes(pixelSize, pixelSize, pixelSize); + parent->SetFontSize(pixelSize); m_xAnchorPoint = 1; // center m_yAnchorPoint = 2; // bottom m_xAnchor = m_safeArea.width() / 2; @@ -998,7 +1412,7 @@ void FormattedTextSubtitle::InitFromSRT(QStringList &subs, int textFontZoom) if (pos != 0) // don't add a zero-length string { CC708CharacterAttribute attr(isItalic, isBold, isUnderline, - color, m_useBackground); + color); FormattedTextChunk chunk(text.left(pos), attr, parent); line.chunks += chunk; text = (pos < 0 ? "" : text.mid(pos)); @@ -1072,6 +1486,7 @@ bool FormattedTextChunk::Split(FormattedTextChunk &newChunk) QString("Failed to split chunk '%1'").arg(text)); return false; } + newChunk.isTeletext = isTeletext; newChunk.parent = parent; newChunk.format = format; newChunk.text = text.mid(lastSpace + 1).trimmed() + ' '; @@ -1268,7 +1683,8 @@ void FormattedTextSubtitle::Layout(void) // Returns true if anything new was drawn, false if not. The caller // should call SubtitleScreen::OptimiseDisplayedArea() if true is // returned. -bool FormattedTextSubtitle::Draw(QList *imageCache, +bool FormattedTextSubtitle::Draw(const QString &base, + QList *imageCache, uint64_t start, uint64_t duration) const { bool result = false; @@ -1285,7 +1701,8 @@ bool FormattedTextSubtitle::Draw(QList *imageCache, chunk != m_lines[i].chunks.constEnd(); ++chunk) { - MythFontProperties *mythfont = parent->Get708Font((*chunk).format); + MythFontProperties *mythfont = + parent->GetFont((*chunk).format, (*chunk).isTeletext); if (!mythfont) continue; QFontMetrics font(*(mythfont->GetFace())); @@ -1301,37 +1718,36 @@ bool FormattedTextSubtitle::Draw(QList *imageCache, ++count; } int x_adjust = count * font.width(" "); - int padding = (*chunk).CalcPadding(); + int leftPadding = (*chunk).CalcPadding(true); + int rightPadding = (*chunk).CalcPadding(false); // Account for extra padding before the first chunk. if (first) - x += padding; + x += leftPadding; QSize chunk_sz = (*chunk).CalcSize(); - if ((*chunk).format.GetBGAlpha()) - { - QBrush bgfill = QBrush((*chunk).format.GetBGColor()); - QRect bgrect(x - padding, y, - chunk_sz.width() + 2 * padding, height); - // Don't draw a background behind leading spaces. - if (first) - bgrect.setLeft(bgrect.left() + x_adjust); - MythUIShape *bgshape = new MythUIShape(parent, - QString("subbg%1x%2@%3,%4") - .arg(chunk_sz.width()) - .arg(height) - .arg(x).arg(y)); - bgshape->SetFillBrush(bgfill); - bgshape->SetArea(MythRect(bgrect)); - if (imageCache) - imageCache->append(bgshape); - if (duration > 0) - parent->RegisterExpiration(bgshape, start + duration); - result = true; - } + QRect bgrect(x - leftPadding, y, + chunk_sz.width() + leftPadding + rightPadding, + height); + // Don't draw a background behind leading spaces. + if (first) + bgrect.setLeft(bgrect.left() + x_adjust); + MythUIShape *bgshape = + parent->m_format-> + GetBackground(parent, + QString("subbg%1x%2@%3,%4") + .arg(chunk_sz.width()).arg(height) + .arg(x).arg(y), + base, (*chunk).format); + bgshape->SetArea(MythRect(bgrect)); + if (imageCache) + imageCache->append(bgshape); + if (duration > 0) + parent->RegisterExpiration(bgshape, start + duration); + result = true; // Shift to the right to account for leading spaces that // are removed by the MythUISimpleText constructor. Also // add in padding at the end to avoid clipping. QRect rect(x + x_adjust, y, - chunk_sz.width() - x_adjust + padding, height); + chunk_sz.width() - x_adjust + rightPadding, height); MythUISimpleText *text = new MythUISimpleText((*chunk).text, *mythfont, rect, @@ -1409,31 +1825,71 @@ QStringList FormattedTextSubtitle::ToSRT(void) const return result; } -void SubtitleScreen::SetFontSizes(int nSmall, int nMedium, int nLarge) +// The QFontMetrics class does not account for the MythFontProperties +// shadow and offset properties. This method calculates the +// additional padding to the right and below that is needed for proper +// bounding box computation. +static QSize CalcShadowOffsetPadding(MythFontProperties *mythfont) { - m_708fontSizes[k708AttrSizeSmall] = nSmall; - m_708fontSizes[k708AttrSizeStandard] = nMedium; - m_708fontSizes[k708AttrSizeLarge] = nLarge; + QColor color; + int alpha; + int outlineSize = 0; + int shadowWidth = 0, shadowHeight = 0; + if (mythfont->hasOutline()) + { + mythfont->GetOutline(color, outlineSize, alpha); + } + if (mythfont->hasShadow()) + { + QPoint shadowOffset; + mythfont->GetShadow(shadowOffset, color, alpha); + shadowWidth = abs(shadowOffset.x()); + shadowHeight = abs(shadowOffset.y()); + // Shadow and outline overlap, so don't just add them. + shadowWidth = max(shadowWidth, outlineSize); + shadowHeight = max(shadowHeight, outlineSize); + } + return QSize(shadowWidth + outlineSize, shadowHeight + outlineSize); } QSize SubtitleScreen::CalcTextSize(const QString &text, const CC708CharacterAttribute &format, + bool teletext, float layoutSpacing) const { - QFont *font = Get708Font(format)->GetFace(); + MythFontProperties *mythfont = GetFont(format, teletext); + QFont *font = mythfont->GetFace(); QFontMetrics fm(*font); int width = fm.width(text); int height = fm.height() * (1 + PAD_HEIGHT); if (layoutSpacing > 0 && !text.trimmed().isEmpty()) height = max(height, (int)(font->pixelSize() * layoutSpacing)); + height += CalcShadowOffsetPadding(mythfont).height(); return QSize(width, height); } -int SubtitleScreen::CalcPadding(const CC708CharacterAttribute &format) const +// Padding calculation is different depending on whether the padding +// is on the left side or the right side of the text. Padding on the +// right needs to add the shadow and offset padding. +int SubtitleScreen::CalcPadding(const CC708CharacterAttribute &format, + bool teletext, bool isLeft) const { - QFont *font = Get708Font(format)->GetFace(); + MythFontProperties *mythfont = GetFont(format, teletext); + QFont *font = mythfont->GetFace(); QFontMetrics fm(*font); - return fm.maxWidth() * PAD_WIDTH; + int result = fm.maxWidth() * PAD_WIDTH; + if (!isLeft) + result += CalcShadowOffsetPadding(mythfont).width(); + return result; +} + +QString SubtitleScreen::GetTeletextFontName(void) +{ + SubtitleFormat format; + CC708CharacterAttribute attr(false, false, false, Qt::white); + MythFontProperties *mythfont = + format.GetFont(kSubFamilyTeletext, attr, 20, false, 100, 100); + return mythfont->face().family(); } #ifdef USING_LIBASS diff --git a/mythtv/libs/libmythtv/subtitlescreen.h b/mythtv/libs/libmythtv/subtitlescreen.h index 30c8e660300..1fe310eaddb 100644 --- a/mythtv/libs/libmythtv/subtitlescreen.h +++ b/mythtv/libs/libmythtv/subtitlescreen.h @@ -41,15 +41,19 @@ class SubtitleScreen : public MythScreenType QSize CalcTextSize(const QString &text, const CC708CharacterAttribute &format, + bool teletext, float layoutSpacing) const; - int CalcPadding(const CC708CharacterAttribute &format) const; + int CalcPadding(const CC708CharacterAttribute &format, + bool teletext, bool isLeft) const; void RegisterExpiration(MythUIType *shape, long long endTime) { m_expireTimes.insert(shape, endTime); } - bool GetUseBackground(void) { return m_useBackground; } + // Temporary method until teletextscreen.cpp is refactored into + // subtitlescreen.cpp + static QString GetTeletextFontName(void); // MythScreenType methods virtual bool Create(void); @@ -67,26 +71,25 @@ class SubtitleScreen : public MythScreenType void AddScaledImage(QImage &img, QRect &pos); void Clear708Cache(int num); void InitializeFonts(bool wasResized); - MythFontProperties* Get708Font(CC708CharacterAttribute attr) const; - void SetFontSizes(int nSmall, int nMedium, int nLarge); + MythFontProperties* GetFont(CC708CharacterAttribute attr, + bool teletext) const; + void SetFontSize(int pixelSize) { m_fontSize = pixelSize; } MythPlayer *m_player; SubtitleReader *m_subreader; CC608Reader *m_608reader; CC708Reader *m_708reader; QRect m_safeArea; - bool m_useBackground; QRegExp m_removeHTML; int m_subtitleType; QHash m_expireTimes; - int m_708fontSizes[4]; + int m_fontSize; int m_textFontZoom; // valid for 708 & text subs bool m_refreshArea; QHash > m_708imageCache; int m_fontStretch; - bool m_fontsAreInitialized; - QStringList m_fontNames; - QHash m_fontSet; + QString m_family; // 608, 708, text, teletext + class SubtitleFormat *m_format; #ifdef USING_LIBASS bool InitialiseAssLibrary(void); @@ -110,19 +113,19 @@ class FormattedTextChunk { public: FormattedTextChunk(const QString &t, CC708CharacterAttribute formatting, - SubtitleScreen *p) - : text(t), format(formatting), parent(p) + SubtitleScreen *p, bool teletext = false) + : text(t), format(formatting), parent(p), isTeletext(teletext) { } FormattedTextChunk(void) : parent(NULL) {} QSize CalcSize(float layoutSpacing = 0.0f) const { - return parent->CalcTextSize(text, format, layoutSpacing); + return parent->CalcTextSize(text, format, isTeletext, layoutSpacing); } - int CalcPadding(void) const + int CalcPadding(bool isLeft) const { - return parent->CalcPadding(format); + return parent->CalcPadding(format, isTeletext, isLeft); } bool Split(FormattedTextChunk &newChunk); QString ToLogString(void) const; @@ -130,6 +133,7 @@ class FormattedTextChunk QString text; CC708CharacterAttribute format; SubtitleScreen *parent; // where fonts and sizes are kept + bool isTeletext; }; class FormattedTextLine @@ -141,18 +145,19 @@ class FormattedTextLine QSize CalcSize(float layoutSpacing = 0.0f) const { int height = 0, width = 0; - int padding = 0; + int leftPadding = 0, rightPadding = 0; QList::const_iterator it; for (it = chunks.constBegin(); it != chunks.constEnd(); ++it) { QSize tmp = (*it).CalcSize(layoutSpacing); height = max(height, tmp.height()); width += tmp.width(); - padding = (*it).CalcPadding(); + leftPadding = (*it).CalcPadding(true); + rightPadding = (*it).CalcPadding(false); if (it == chunks.constBegin()) - width += padding; + width += leftPadding; } - return QSize(width + padding, height); + return QSize(width + rightPadding, height); } QList chunks; @@ -164,10 +169,8 @@ class FormattedTextLine class FormattedTextSubtitle { public: - FormattedTextSubtitle(const QRect &safearea, bool useBackground, - SubtitleScreen *p) - : m_safeArea(safearea), m_useBackground(useBackground), - parent(p) + FormattedTextSubtitle(const QRect &safearea, SubtitleScreen *p) + : m_safeArea(safearea), parent(p) { // make cppcheck happy m_xAnchorPoint = 0; @@ -176,7 +179,7 @@ class FormattedTextSubtitle m_yAnchor = 0; } FormattedTextSubtitle(void) - : m_safeArea(QRect()), m_useBackground(false), parent(NULL) + : m_safeArea(QRect()), parent(NULL) { // make cppcheck happy m_xAnchorPoint = 0; @@ -193,7 +196,7 @@ class FormattedTextSubtitle void WrapLongLines(void); void Layout(void); void Layout608(void); - bool Draw(QList *imageCache = 0, + bool Draw(const QString &base, QList *imageCache = NULL, uint64_t start = 0, uint64_t duration = 0) const; QStringList ToSRT(void) const; QRect m_bounds; @@ -201,7 +204,6 @@ class FormattedTextSubtitle private: QVector m_lines; const QRect m_safeArea; - const bool m_useBackground; SubtitleScreen *parent; // where fonts and sizes are kept int m_xAnchorPoint; // 0=left, 1=center, 2=right int m_yAnchorPoint; // 0=top, 1=center, 2=bottom diff --git a/mythtv/libs/libmythtv/teletextscreen.cpp b/mythtv/libs/libmythtv/teletextscreen.cpp index 6bb0e137c11..69dd8101dfe 100644 --- a/mythtv/libs/libmythtv/teletextscreen.cpp +++ b/mythtv/libs/libmythtv/teletextscreen.cpp @@ -10,6 +10,7 @@ #include "mythuiimage.h" #include "mythpainter.h" #include "teletextscreen.h" +#include "subtitlescreen.h" #define LOC QString("TeletextScreen: ") @@ -678,15 +679,19 @@ void TeletextScreen::DrawStatus(void) bool TeletextScreen::InitialiseFont() { static bool initialised = false; - QString font = gCoreContext->GetSetting("DefaultSubtitleFont", "FreeMono"); + //QString font = gCoreContext->GetSetting("DefaultSubtitleFont", "FreeMono"); if (initialised) { + return true; +#if 0 if (gTTFont->face().family() == font) return true; delete gTTFont; +#endif // 0 } MythFontProperties *mythfont = new MythFontProperties(); + QString font = SubtitleScreen::GetTeletextFontName(); if (mythfont) { QFont newfont(font); diff --git a/mythtv/libs/libmythui/mythuishape.h b/mythtv/libs/libmythui/mythuishape.h index b078e463618..15c363fb91e 100644 --- a/mythtv/libs/libmythui/mythuishape.h +++ b/mythtv/libs/libmythui/mythuishape.h @@ -46,6 +46,7 @@ class MUI_PUBLIC MythUIShape : public MythUIType friend class MythUIProgressBar; friend class MythUIEditBar; + friend class SubtitleFormat; }; #endif diff --git a/mythtv/programs/mythfrontend/globalsettings.cpp b/mythtv/programs/mythfrontend/globalsettings.cpp index 806ad7eb154..9f9ee5a695d 100644 --- a/mythtv/programs/mythfrontend/globalsettings.cpp +++ b/mythtv/programs/mythfrontend/globalsettings.cpp @@ -1405,23 +1405,6 @@ static HostSpinBox *OSDCC708TextZoomPercentage(void) return gs; } -static HostComboBox *SubtitleFont() -{ - HostComboBox *hcb = new HostComboBox("DefaultSubtitleFont"); - QFontDatabase db; - QStringList fonts = db.families(); - QStringList hide = db.families(QFontDatabase::Symbol); - - hcb->setLabel(QObject::tr("Subtitle Font")); - hcb->setHelpText(QObject::tr("The font to use for text based subtitles.")); - foreach (QString font, fonts) - { - if (!hide.contains(font)) - hcb->addSelection(font, font, font.toLower() == "freemono"); - } - return hcb; -} - static HostComboBox *SubtitleCodec() { HostComboBox *gc = new HostComboBox("SubtitleCodec"); @@ -1486,18 +1469,6 @@ static HostSpinBox *YScanDisplacement() return gs; }; -static HostCheckBox *CCBackground() -{ - HostCheckBox *gc = new HostCheckBox("CCBackground"); - gc->setLabel(QObject::tr("Black background for closed captioning")); - gc->setValue(false); - gc->setHelpText(QObject::tr( - "If enabled, captions will be displayed " - "over a black background " - "for better contrast.")); - return gc; -} - static HostCheckBox *DefaultCCMode() { HostCheckBox *gc = new HostCheckBox("DefaultCCMode"); @@ -3530,9 +3501,7 @@ OSDSettings::OSDSettings() osd->addChild(EnableMHEG()); osd->addChild(PersistentBrowseMode()); osd->addChild(BrowseAllTuners()); - osd->addChild(CCBackground()); osd->addChild(DefaultCCMode()); - osd->addChild(SubtitleFont()); osd->addChild(OSDCC708TextZoomPercentage()); osd->addChild(SubtitleCodec()); addChild(osd); diff --git a/mythtv/themes/default/osd_subtitle.xml b/mythtv/themes/default/osd_subtitle.xml new file mode 100644 index 00000000000..7c45f0f4098 --- /dev/null +++ b/mythtv/themes/default/osd_subtitle.xml @@ -0,0 +1,57 @@ + + + + + + + + box + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 29cd39bcb1213a31f2ec8b4d679a8dde6556b652 Mon Sep 17 00:00:00 2001 From: Jean-Yves Avenard Date: Wed, 2 May 2012 18:45:04 +1000 Subject: [PATCH 43/69] Use new ServerPool capabilities, and only advertise AirPlay via Bonjour if the server creation was successful --- mythtv/libs/libmythtv/mythairplayserver.cpp | 70 +++++++++------------ 1 file changed, 31 insertions(+), 39 deletions(-) diff --git a/mythtv/libs/libmythtv/mythairplayserver.cpp b/mythtv/libs/libmythtv/mythairplayserver.cpp index 18aa13fd3cc..aa30bcc32e1 100644 --- a/mythtv/libs/libmythtv/mythairplayserver.cpp +++ b/mythtv/libs/libmythtv/mythairplayserver.cpp @@ -327,51 +327,43 @@ void MythAirplayServer::Start(void) // start listening for connections // try a few ports in case the default is in use int baseport = m_setupPort; - while (m_setupPort < baseport + AIRPLAY_PORT_RANGE) + m_setupPort = tryListeningPort(m_setupPort, AIRPLAY_PORT_RANGE); + if (m_setupPort < 0) { - if (listen(QNetworkInterface::allAddresses(), m_setupPort, false)) - { - LOG(VB_GENERAL, LOG_INFO, LOC + - QString("Listening for connections on port %1") - .arg(m_setupPort)); - break; - } - m_setupPort++; - } - - if (m_setupPort >= baseport + AIRPLAY_PORT_RANGE) LOG(VB_GENERAL, LOG_ERR, LOC + "Failed to find a port for incoming connections."); - - // announce service - m_bonjour = new BonjourRegister(this); - if (!m_bonjour) - { - LOG(VB_GENERAL, LOG_ERR, LOC + "Failed to create Bonjour object."); - return; } - - // give each frontend a unique name - int multiple = m_setupPort - baseport; - if (multiple > 0) - m_name += QString::number(multiple); - - QByteArray name = m_name.toUtf8(); - name.append(" on "); - name.append(gCoreContext->GetHostName()); - QByteArray type = "_airplay._tcp"; - QByteArray txt; - txt.append(26); txt.append("deviceid=00:00:00:00:00:00"); - txt.append(13); txt.append("features=0x77"); - txt.append(16); txt.append("model=AppleTV2,1"); - txt.append(14); txt.append("srcvers=101.28"); - - if (!m_bonjour->Register(m_setupPort, type, name, txt)) + else { - LOG(VB_GENERAL, LOG_ERR, LOC + "Failed to register service."); - return; - } + // announce service + m_bonjour = new BonjourRegister(this); + if (!m_bonjour) + { + LOG(VB_GENERAL, LOG_ERR, LOC + "Failed to create Bonjour object."); + return; + } + // give each frontend a unique name + int multiple = m_setupPort - baseport; + if (multiple > 0) + m_name += QString::number(multiple); + + QByteArray name = m_name.toUtf8(); + name.append(" on "); + name.append(gCoreContext->GetHostName()); + QByteArray type = "_airplay._tcp"; + QByteArray txt; + txt.append(26); txt.append("deviceid=00:00:00:00:00:00"); + txt.append(13); txt.append("features=0x77"); + txt.append(16); txt.append("model=AppleTV2,1"); + txt.append(14); txt.append("srcvers=101.28"); + + if (!m_bonjour->Register(m_setupPort, type, name, txt)) + { + LOG(VB_GENERAL, LOG_ERR, LOC + "Failed to register service."); + return; + } + } m_valid = true; return; } From 374e2cd64c77f061aea398658a808c1ebdbb8d7e Mon Sep 17 00:00:00 2001 From: Jean-Yves Avenard Date: Wed, 2 May 2012 23:58:48 +1000 Subject: [PATCH 44/69] Fix RAOP time request failing to send Was using the wrong socket. Also fix typo in OPTIONS response. --- mythtv/libs/libmythtv/mythraopconnection.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mythtv/libs/libmythtv/mythraopconnection.cpp b/mythtv/libs/libmythtv/mythraopconnection.cpp index 2bfdd5d0da7..5ca19e4f870 100644 --- a/mythtv/libs/libmythtv/mythraopconnection.cpp +++ b/mythtv/libs/libmythtv/mythraopconnection.cpp @@ -456,7 +456,7 @@ void MythRAOPConnection::SendTimeRequest(void) *(uint32_t *)(req + 24) = qToBigEndian((uint32_t)t.tv_sec); *(uint32_t *)(req + 28) = qToBigEndian((uint32_t)t.tv_usec); - if (m_clientControlSocket->writeDatagram(req, sizeof(req), m_peerAddress, m_clientTimingPort) != sizeof(req)) + if (m_clientTimingSocket->writeDatagram(req, sizeof(req), m_peerAddress, m_clientTimingPort) != sizeof(req)) { LOG(VB_GENERAL, LOG_ERR, LOC + "Failed to send resend time request."); return; @@ -940,7 +940,7 @@ void MythRAOPConnection::ProcessRequest(const QStringList &header, } StartResponse(m_textStream, option, tags["CSeq"]); *m_textStream << "Public: ANNOUNCE, SETUP, RECORD, PAUSE, FLUSH, " - "TEARDOWN, OPTIONS, GET_PARAMETER, SET_PARAMETER. POST, GET\r\n"; + "TEARDOWN, OPTIONS, GET_PARAMETER, SET_PARAMETER, POST, GET\r\n"; } else if (option == "ANNOUNCE") { From f125f53c8be5c9b6bd7c1ea785b606d24bf5bd7b Mon Sep 17 00:00:00 2001 From: Jean-Yves Avenard Date: Thu, 3 May 2012 00:08:31 +1000 Subject: [PATCH 45/69] Always re-use the same computer ID for AirPlay. Share ID between RAOP and AirPlay Random ID is generated the first time, the following session will always re-use the same ID. Having AirPlay and RAOP share the same hardware ID makes mythfrontend behave more like an AppleTV: only one device is showing in the list. Either one for audio or one for audio+video, not both at the same time. --- mythtv/libs/libmythtv/mythairplayserver.cpp | 41 +++++++-------------- mythtv/libs/libmythtv/mythairplayserver.h | 2 +- mythtv/libs/libmythtv/mythraopdevice.cpp | 21 ++++++++++- mythtv/libs/libmythtv/mythraopdevice.h | 1 + 4 files changed, 34 insertions(+), 31 deletions(-) diff --git a/mythtv/libs/libmythtv/mythairplayserver.cpp b/mythtv/libs/libmythtv/mythairplayserver.cpp index aa30bcc32e1..89cb5d2f3f2 100644 --- a/mythtv/libs/libmythtv/mythairplayserver.cpp +++ b/mythtv/libs/libmythtv/mythairplayserver.cpp @@ -3,8 +3,6 @@ // race on startup? // http date format and locale // GET scrub on iOS 5 -// binary plist for non mac -// same mac address for bonjour service and server-info #include #include @@ -22,6 +20,7 @@ #include "bonjourregister.h" #include "mythairplayserver.h" +#include "mythraopdevice.h" MythAirplayServer* MythAirplayServer::gMythAirplayServer = NULL; MThread* MythAirplayServer::gMythAirplayServerThread = NULL; @@ -353,8 +352,8 @@ void MythAirplayServer::Start(void) name.append(gCoreContext->GetHostName()); QByteArray type = "_airplay._tcp"; QByteArray txt; - txt.append(26); txt.append("deviceid=00:00:00:00:00:00"); - txt.append(13); txt.append("features=0x77"); + txt.append(26); txt.append("deviceid="); txt.append(GetMacAddress()); + txt.append(14); txt.append("features=0x219"); txt.append(16); txt.append("model=AppleTV2,1"); txt.append(14); txt.append("srcvers=101.28"); @@ -516,7 +515,7 @@ void MythAirplayServer::HandleResponse(APHTTPRequest *req, { content_type = "text/x-apple-plist+xml\r\n"; body = SERVER_INFO; - body.replace("%1", GetMacAddress(socket)); + body.replace("%1", GetMacAddress()); LOG(VB_GENERAL, LOG_INFO, body); } else if (req->GetURI() == "/scrub") @@ -785,34 +784,20 @@ void MythAirplayServer::GetPlayerStatus(bool &playing, float &speed, duration = state["totalseconds"].toDouble(); } -QString MythAirplayServer::GetMacAddress(QTcpSocket *socket) +QString MythAirplayServer::GetMacAddress() { - if (!socket) - return ""; + QString id = MythRAOPDevice::HardwareId(); - QString res(""); - QString fallback(""); - - foreach(QNetworkInterface interface, QNetworkInterface::allInterfaces()) + QString res; + for (int i = 1; i <= id.size(); i++) { - if (!(interface.flags() & QNetworkInterface::IsLoopBack)) + res.append(id[i-1]); + if (i % 2 == 0 && i != id.size()) { - fallback = interface.hardwareAddress(); - QList entries = interface.addressEntries(); - foreach (QNetworkAddressEntry entry, entries) - if (entry.ip() == socket->localAddress()) - res = fallback; + res.append(':'); } } - - if (res.isEmpty()) - { - LOG(VB_GENERAL, LOG_WARNING, LOC + "Using fallback MAC address."); - res = fallback; - } - - if (res.isEmpty()) - LOG(VB_GENERAL, LOG_ERR, LOC + "Didn't find MAC address."); - + QByteArray ba = res.toAscii(); + const char *t = ba.constData(); return res; } diff --git a/mythtv/libs/libmythtv/mythairplayserver.h b/mythtv/libs/libmythtv/mythairplayserver.h index 66561e36260..e793c6b769a 100644 --- a/mythtv/libs/libmythtv/mythairplayserver.h +++ b/mythtv/libs/libmythtv/mythairplayserver.h @@ -65,7 +65,7 @@ class MTV_PUBLIC MythAirplayServer : public ServerPool QString eventToString(AirplayEvent event); void GetPlayerStatus(bool &playing, float &speed, double &position, double &duration); - QString GetMacAddress(QTcpSocket *socket); + QString GetMacAddress(); void SendReverseEvent(QByteArray &session, AirplayEvent event); // Globals diff --git a/mythtv/libs/libmythtv/mythraopdevice.cpp b/mythtv/libs/libmythtv/mythraopdevice.cpp index 78720046351..f996cacde8c 100644 --- a/mythtv/libs/libmythtv/mythraopdevice.cpp +++ b/mythtv/libs/libmythtv/mythraopdevice.cpp @@ -55,10 +55,28 @@ bool MythRAOPDevice::Create(void) gMythRAOPDeviceThread->start(QThread::LowestPriority); } + LOG(VB_GENERAL, LOG_INFO, LOC + "Created RAOP device objects."); return true; } +QString MythRAOPDevice::HardwareId() +{ + QString key = "AirPlayId"; + QString id = gCoreContext->GetSetting(key); + int size = id.size(); + if (size == 12) + return id; + + QByteArray ba; + for (int i = 0; i < RAOP_HARDWARE_ID_SIZE; i++) + ba.append((random() % 80) + 33); + id = ba.toHex(); + + gCoreContext->SaveSetting(key, id); + return id; +} + void MythRAOPDevice::Cleanup(void) { LOG(VB_GENERAL, LOG_INFO, LOC + "Cleaning up."); @@ -83,8 +101,7 @@ MythRAOPDevice::MythRAOPDevice() : ServerPool(), m_name(QString("MythTV")), m_bonjour(NULL), m_valid(false), m_lock(new QMutex(QMutex::Recursive)), m_setupPort(5000) { - for (int i = 0; i < RAOP_HARDWARE_ID_SIZE; i++) - m_hardwareId.append((random() % 80) + 33); + m_hardwareId = QByteArray::fromHex(HardwareId().toAscii()); } MythRAOPDevice::~MythRAOPDevice() diff --git a/mythtv/libs/libmythtv/mythraopdevice.h b/mythtv/libs/libmythtv/mythraopdevice.h index 13fb730e09a..0a4117de759 100644 --- a/mythtv/libs/libmythtv/mythraopdevice.h +++ b/mythtv/libs/libmythtv/mythraopdevice.h @@ -24,6 +24,7 @@ class MTV_PUBLIC MythRAOPDevice : public ServerPool MythRAOPDevice(); bool NextInAudioQueue(MythRAOPConnection* conn); + static QString HardwareId(); private slots: void Start(); From 4ba7c3d447411da3cb34c34ca978b8f2e6f5fae0 Mon Sep 17 00:00:00 2001 From: Gavin Hurlbut Date: Wed, 2 May 2012 13:16:13 -0700 Subject: [PATCH 46/69] Make mythfilldatabase calls put user-provided args last Fixes #10683 --- mythtv/programs/mythbackend/housekeeper.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mythtv/programs/mythbackend/housekeeper.cpp b/mythtv/programs/mythbackend/housekeeper.cpp index 866429e442c..29ab7ea979e 100644 --- a/mythtv/programs/mythbackend/housekeeper.cpp +++ b/mythtv/programs/mythbackend/housekeeper.cpp @@ -429,8 +429,8 @@ void HouseKeeper::RunMFD(void) if (mfpath == "mythfilldatabase") mfpath = GetInstallPrefix() + "/bin/mythfilldatabase"; - QString command = QString("%1 %2").arg(mfpath).arg(mfarg); - command += logPropagateArgs; + QString command = QString("%1 %2 %3").arg(mfpath).arg(logPropagateArgs) + .arg(mfarg); { QMutexLocker locker(&fillDBLock); From c877947a5065846c6c4174ea36804f810c49168f Mon Sep 17 00:00:00 2001 From: Raymond Wagner Date: Wed, 2 May 2012 19:51:04 -0400 Subject: [PATCH 47/69] Add command string parser to break up commands for direct execution. This adds a parser for the UNIX version of MythSystem, to break commands apart at white space characters, obeying quoting and escaping where appropriate, to allow commands passed as a single string to be executed directly by execv, rather than be passed through the Bourne shell. Note, this does not include support for any form of piping, redirects, or backgrounding. This includes an ABI bump. Refs #10860 --- mythtv/libs/libmythbase/mythsystem.h | 3 + mythtv/libs/libmythbase/mythversion.h | 2 +- mythtv/libs/libmythbase/system-unix.cpp | 125 +++++++++++++++++++++ mythtv/libs/libmythbase/system-unix.h | 5 + mythtv/libs/libmythbase/system-windows.cpp | 6 + mythtv/libs/libmythbase/system-windows.h | 3 + 6 files changed, 143 insertions(+), 1 deletion(-) diff --git a/mythtv/libs/libmythbase/mythsystem.h b/mythtv/libs/libmythbase/mythsystem.h index 5c4bcb13ae3..05c60b4dda0 100644 --- a/mythtv/libs/libmythbase/mythsystem.h +++ b/mythtv/libs/libmythbase/mythsystem.h @@ -144,6 +144,9 @@ class MBASE_PUBLIC MythSystemPrivate : public QObject virtual void Signal(int sig) = 0; virtual void JumpAbort(void) = 0; + virtual bool ParseShell(const QString cmd, QString &abscmd, + QStringList &args) = 0; + protected: MythSystem *m_parent; diff --git a/mythtv/libs/libmythbase/mythversion.h b/mythtv/libs/libmythbase/mythversion.h index 635c09af507..ae78adb7e99 100644 --- a/mythtv/libs/libmythbase/mythversion.h +++ b/mythtv/libs/libmythbase/mythversion.h @@ -12,7 +12,7 @@ /// Update this whenever the plug-in API changes. /// Including changes in the libmythbase, libmyth, libmythtv, libmythav* and /// libmythui class methods used by plug-ins. -#define MYTH_BINARY_VERSION "0.26.20120417-1" +#define MYTH_BINARY_VERSION "0.26.20120502-1" /** \brief Increment this whenever the MythTV network protocol changes. * diff --git a/mythtv/libs/libmythbase/system-unix.cpp b/mythtv/libs/libmythbase/system-unix.cpp index a7a24c9edbf..5fd20c508b6 100644 --- a/mythtv/libs/libmythbase/system-unix.cpp +++ b/mythtv/libs/libmythbase/system-unix.cpp @@ -582,6 +582,131 @@ MythSystemUnix::~MythSystemUnix(void) { } +bool MythSystemUnix::ParseShell(const QString cmd, QString &abscmd, + QStringList &args) +{ + QList whitespace; whitespace << ' ' << '\t' << '\n' << '\r'; + QList whitechr; whitechr << 't' << 'n' << 'r'; + QChar quote = '"', + hardquote = '\'', + escape = '\\'; + bool quoted = false, + hardquoted = false, + escaped = false; + + QString tmp; + QString::const_iterator i = cmd.begin(); + while (i != cmd.end()) + { + if (quoted || hardquoted) + { + if (escaped) + { + if ((quote == *i) || (escape == *i) || + whitespace.contains(*i)) + // pass through escape (\), quote ("), and any whitespace + tmp += *i; + else if (whitechr.contains(*i)) + // process whitespace escape code, and pass character + tmp += whitespace[whitechr.indexOf(*i)+1]; + else + // unhandled escape code, abort + return false; + + escaped = false; + } + + else if (*i == escape) + { + if (hardquoted) + // hard quotes (') pass everything + tmp += *i; + else + // otherwise, mark escaped to handle next character + escaped = true; + } + + else if ((quoted & (*i == quote)) || + (hardquoted && (*i == hardquote))) + // end of quoted sequence + quoted = hardquoted = false; + + else + // pass through character + tmp += *i; + } + + else if (escaped) + { + if ((*i == quote) || (*i == hardquote) || (*i == escape) || + whitespace.contains(*i)) + // pass through special characters + tmp += *i; + else if (whitechr.contains(*i)) + // process whitespace escape code, and pass character + tmp += whitespace[whitechr.indexOf(*i)+1]; + else + // unhandled escape code, abort + return false; + + escaped = false; + } + + // handle quotes and escape characters + else if (quote == *i) + quoted = true; + else if (hardquote == *i) + hardquoted = true; + else if (escape == *i) + escaped = true; + + // handle whitespace characters + else if (whitespace.contains(*i) && !tmp.isEmpty()) + { + args << tmp; + tmp.clear(); + } + + else + // pass everything else + tmp += *i; + + // step forward to next character + ++i; + } + + if (quoted || hardquoted || escaped) + // command not terminated cleanly + return false; + + if (!tmp.isEmpty()) + // collect last argument + args << tmp; + + if (args.isEmpty()) + // this shouldnt happen + return false; + + // grab the first argument to use as the command + abscmd = args.takeFirst(); + if (!abscmd.startsWith('/')) + { + // search for absolute path + QStringList path = QString(getenv("PATH")).split(':'); + QStringList::const_iterator i = path.begin(); + for (; i != path.end(); ++i) + { + QFile file(QString("%1/%2").arg(*i).arg(abscmd)); + if (file.exists()) + { + abscmd = file.fileName(); + break; + } + } + } + + return true; +} void MythSystemUnix::Term(bool force) { diff --git a/mythtv/libs/libmythbase/system-unix.h b/mythtv/libs/libmythbase/system-unix.h index a71141f520e..c7436bd645d 100644 --- a/mythtv/libs/libmythbase/system-unix.h +++ b/mythtv/libs/libmythbase/system-unix.h @@ -5,6 +5,8 @@ #include #include +#include +#include #include #include #include @@ -89,6 +91,9 @@ class MBASE_PUBLIC MythSystemUnix : public MythSystemPrivate virtual void Signal(int sig); virtual void JumpAbort(void); + virtual bool ParseShell(const QString cmd, QString &abscmd, + QStringList &args); + friend class MythSystemManager; friend class MythSystemSignalManager; friend class MythSystemIOHandler; diff --git a/mythtv/libs/libmythbase/system-windows.cpp b/mythtv/libs/libmythbase/system-windows.cpp index 71832ff8b15..992895f0fc3 100644 --- a/mythtv/libs/libmythbase/system-windows.cpp +++ b/mythtv/libs/libmythbase/system-windows.cpp @@ -532,6 +532,12 @@ MythSystemWindows::~MythSystemWindows(void) { } +bool MythSystemWindows::ParseShell(const QString cmd, QString &abscmd, + QStringList &args) +{ + return false; +} + void MythSystemWindows::Term(bool force) { if( (GetStatus() != GENERIC_EXIT_RUNNING) || (!m_child) ) diff --git a/mythtv/libs/libmythbase/system-windows.h b/mythtv/libs/libmythbase/system-windows.h index 125c6875e37..dbf1a71f604 100644 --- a/mythtv/libs/libmythbase/system-windows.h +++ b/mythtv/libs/libmythbase/system-windows.h @@ -90,6 +90,9 @@ class MBASE_PUBLIC MythSystemWindows : public MythSystemPrivate virtual void Signal(int sig); virtual void JumpAbort(void); + virtual bool ParseShell(const QString cmd, QString &abscmd, + QStringList &args); + friend class MythSystemManager; friend class MythSystemSignalManager; friend class MythSystemIOHandler; From e229f68b879d7594b4b2df03df928bc3ee21458f Mon Sep 17 00:00:00 2001 From: Raymond Wagner Date: Wed, 2 May 2012 20:14:13 -0400 Subject: [PATCH 48/69] Enable command splitter for anything not explicitly flagging kMSRunShell This alters the MythSystem(QString, uint) constructor to automatically parse any commands passed through it that do not explicitly set the kMSRunShell flag. A quick run through the code does not turn up any instances where this is currently the case, so the code should remain effectively unused for now. Individual MythSystem users and the block of myth_system() calls will need to be tested individually with this path to debug the parser. Ref #10860 --- mythtv/libs/libmythbase/mythsystem.cpp | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/mythtv/libs/libmythbase/mythsystem.cpp b/mythtv/libs/libmythbase/mythsystem.cpp index 382ba2b0d30..6a9a9ead51d 100644 --- a/mythtv/libs/libmythbase/mythsystem.cpp +++ b/mythtv/libs/libmythbase/mythsystem.cpp @@ -63,7 +63,23 @@ MythSystem::MythSystem(const QString &command, uint flags) */ void MythSystem::SetCommand(const QString &command, uint flags) { - SetCommand(command, QStringList(), flags | kMSRunShell); + if (flags & kMSRunShell) + SetCommand(command, QStringList(), flags); + else + { + QString abscommand; + QStringList args; + if (!d->ParseShell(command, abscommand, args)) + { + LOG(VB_GENERAL, LOG_ERR, + QString("MythSystem(%1) command not understood") + .arg(command)); + m_status = GENERIC_EXIT_INVALID_CMDLINE; + return; + } + + SetCommand(abscommand, args, flags); + } } From 7425634d7b92f8bbe9abdc8b50c4148c96c87f40 Mon Sep 17 00:00:00 2001 From: Raymond Wagner Date: Thu, 3 May 2012 01:12:28 -0400 Subject: [PATCH 49/69] Improve handling of invalid responses from TMDB When an HTML error is received from the TMDB API, this will now attempt to process a specific error type from the returned JSON. If no error type is given, or the resposnse was not JSON, it will raise the previous TMDBHTTPError. --- tmdb3/request.py | 27 +++++++++++++++++++++------ tmdb3/tmdb_exceptions.py | 11 +++++++++-- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/tmdb3/request.py b/tmdb3/request.py index c92806f49ce..d521959d894 100644 --- a/tmdb3/request.py +++ b/tmdb3/request.py @@ -94,7 +94,7 @@ def open(self): print ' '+self.get_data() return urllib2.urlopen(self) except urllib2.HTTPError, e: - raise TMDBHTTPError(str(e)) + raise TMDBHTTPError(e) def read(self): """Return result from specified URL as a string.""" @@ -104,7 +104,21 @@ def read(self): def readJSON(self): """Parse result from specified URL as JSON data.""" url = self.get_full_url() - data = json.load(self.open()) + try: + # catch HTTP error from open() + data = json.load(self.open()) + except TMDBHTTPError, e: + try: + # try to load whatever was returned + data = json.loads(e.response) + except: + # cannot parse json, just raise existing error + raise e + else: + # response parsed, try to raise error from TMDB + handle_status(data, url) + # no error from TMDB, just raise existing error + raise e handle_status(data, url) if DEBUG: import pprint @@ -113,14 +127,14 @@ def readJSON(self): status_handlers = { 1: None, - 2: TMDBRequestError('Invalid service - This service does not exist.'), + 2: TMDBRequestInvalid('Invalid service - This service does not exist.'), 3: TMDBRequestError('Authentication Failed - You do not have '+\ 'permissions to access this service.'), - 4: TMDBRequestError("Invalid format - This service doesn't exist "+\ + 4: TMDBRequestInvalid("Invalid format - This service doesn't exist "+\ 'in that format.'), - 5: TMDBRequestError('Invalid parameters - Your request parameters '+\ + 5: TMDBRequestInvalid('Invalid parameters - Your request parameters '+\ 'are incorrect.'), - 6: TMDBRequestError('Invalid id - The pre-requisite id is invalid '+\ + 6: TMDBRequestInvalid('Invalid id - The pre-requisite id is invalid '+\ 'or not found.'), 7: TMDBKeyInvalid('Invalid API key - You must be granted a valid key.'), 8: TMDBRequestError('Duplicate entry - The data you tried to submit '+\ @@ -139,5 +153,6 @@ def readJSON(self): def handle_status(data, query): status = status_handlers[data.get('status_code', 1)] if status is not None: + status.tmdberrno = data['status_code'] status.query = query raise status diff --git a/tmdb3/tmdb_exceptions.py b/tmdb3/tmdb_exceptions.py index 96dc564570e..35e0364b372 100644 --- a/tmdb3/tmdb_exceptions.py +++ b/tmdb3/tmdb_exceptions.py @@ -13,6 +13,7 @@ class TMDBError( Exception ): KeyInvalid = 30 KeyRevoked = 40 RequestError = 50 + RequestInvalid = 51 PagingIssue = 60 CacheError = 70 CacheReadError = 71 @@ -25,7 +26,7 @@ class TMDBError( Exception ): def __init__(self, msg=None, errno=0): self.errno = errno - if errno: + if errno == 0: self.errno = getattr(self, 'TMDB'+self.__class__.__name__, errno) self.args = (msg,) @@ -44,6 +45,9 @@ class TMDBKeyRevoked( TMDBKeyInvalid ): class TMDBRequestError( TMDBError ): pass +class TMDBRequestInvalid( TMDBRequestError ): + pass + class TMDBPagingIssue( TMDBRequestError ): pass @@ -72,7 +76,10 @@ class TMDBImageSizeError( TMDBError ): pass class TMDBHTTPError( TMDBError ): - pass + def __init__(self, err): + self.httperrno = err.code + self.response = err.fp.read() + super(TMDBHTTPError, self).__init__(str(err)) class TMDBOffline( TMDBError ): pass From 676cbb1aaf9c8d19d207ec9575ffaa9665d8e710 Mon Sep 17 00:00:00 2001 From: Raymond Wagner Date: Thu, 3 May 2012 02:43:07 -0400 Subject: [PATCH 50/69] Clean up __repr__ methods, and impose soft 80-character line limit. --- setup.py | 2 +- tmdb3/locales.py | 3 +- tmdb3/tmdb_api.py | 169 +++++++++++++++++++++++----------------------- tmdb3/util.py | 26 +++++-- 4 files changed, 108 insertions(+), 92 deletions(-) diff --git a/setup.py b/setup.py index a4023a2bebe..9a391022f9f 100755 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ setup( name='tmdb3', - version='0.3.4', + version='0.6.1', description='TheMovieDB.org APIv3 interface', long_description="Object-oriented interface to TheMovieDB.org's v3 API.", packages=['tmdb3'] diff --git a/tmdb3/locales.py b/tmdb3/locales.py index bb1c0f40a91..1c41983bc6c 100644 --- a/tmdb3/locales.py +++ b/tmdb3/locales.py @@ -46,7 +46,8 @@ def getstored(cls, key): try: return cls._stored[key.lower()] except: - raise TMDBLocaleError("'{0}' is not a known valid {1} code.".format(key, cls.__name__)) + raise TMDBLocaleError("'{0}' is not a known valid {1} code."\ + .format(key, cls.__name__)) class Language( LocaleBase ): __slots__ = ['ISO639_1', 'ISO639_2', 'ISO639_2B', 'englishname', diff --git a/tmdb3/tmdb_api.py b/tmdb3/tmdb_api.py index 4e01d1941f0..c698ec654cd 100644 --- a/tmdb3/tmdb_api.py +++ b/tmdb3/tmdb_api.py @@ -46,7 +46,7 @@ # 0.6.1 Add adult filtering for people searches from request import set_key, Request -from util import Datapoint, Datalist, Datadict, Element +from util import Datapoint, Datalist, Datadict, Element, NameRepr, SearchRepr from pager import PagedRequest from locales import get_locale, set_locale from tmdb_auth import get_session, set_session @@ -68,7 +68,7 @@ def _populate(self): return Request('configuration') Configuration = Configuration() -class Account( Element ): +class Account( NameRepr, Element ): def _populate(self): return Request('account', session_id=self._session.sessionid) @@ -83,15 +83,12 @@ def _populate(self): def locale(self): return get_locale(self.language, self.country) - def __repr__(self): - return '<{0} "{1.name}">'.format(self.__class__.__name__, self) - def searchMovie(query, locale=None, adult=False): return MovieSearchResult( Request('search/movie', query=query, include_adult=adult), locale=locale) -class MovieSearchResult( PagedRequest ): +class MovieSearchResult( SearchRepr, PagedRequest ): """Stores a list of search matches.""" _name = None def __init__(self, request, locale=None): @@ -101,23 +98,17 @@ def __init__(self, request, locale=None): request.new(language=locale.language), lambda x: Movie(raw=x, locale=locale)) - def __repr__(self): - name = self._name if self._name else self._request._kwargs['query'] - return u"".format(name) - def searchPerson(query, adult=False): return PeopleSearchResult(Request('search/person', query=query, include_adult=adult)) -class PeopleSearchResult( PagedRequest ): +class PeopleSearchResult( SearchRepr, PagedRequest ): """Stores a list of search matches.""" + _name = None def __init__(self, request): super(PeopleSearchResult, self).__init__(request, lambda x: Person(raw=x)) - def __repr__(self): - return u"".format(self._request._kwargs['query']) - class Image( Element ): filename = Datapoint('file_path', initarg=1, handler=lambda x: x.lstrip('/')) @@ -146,6 +137,7 @@ def __eq__(self, other): return self.language == other.language def __repr__(self): + # BASE62 encoded filename, no need to worry about unicode return u"<{0.__class__.__name__} '{0.filename}'>".format(self) class Backdrop( Image ): @@ -175,6 +167,10 @@ def __gt__(self, other): def __eq__(self, other): return self.country == other.country + def __repr__(self): + return u"<{0.__class__.__name__} '{0.title}' ({0.country})>"\ + .format(self).encode('utf-8') + class Person( Element ): id = Datapoint('id', initarg=1) name = Datapoint('name') @@ -188,18 +184,21 @@ class Person( Element ): aliases = Datalist('also_known_as') def __repr__(self): - return u"<{0} '{1.name}' at {2}>".\ - format(self.__class__.__name__, self, hex(id(self))) + return u"<{0.__class__.__name__} '{0.name}'>"\ + .format(self).encode('utf-8') def _populate(self): return Request('person/{0}'.format(self.id)) def _populate_credits(self): - return Request('person/{0}/credits'.format(self.id), language=self._locale.language) + return Request('person/{0}/credits'.format(self.id), \ + language=self._locale.language) def _populate_images(self): return Request('person/{0}/images'.format(self.id)) - roles = Datalist('cast', handler=lambda x: ReverseCast(raw=x), poller=_populate_credits) - crew = Datalist('crew', handler=lambda x: ReverseCrew(raw=x), poller=_populate_credits) + roles = Datalist('cast', handler=lambda x: ReverseCast(raw=x), \ + poller=_populate_credits) + crew = Datalist('crew', handler=lambda x: ReverseCrew(raw=x), \ + poller=_populate_credits) profiles = Datalist('profiles', handler=Profile, poller=_populate_images) class Cast( Person ): @@ -207,29 +206,31 @@ class Cast( Person ): order = Datapoint('order') def __repr__(self): - return u"<{0} '{1.name}' as '{1.character}'>".\ - format(self.__class__.__name__, self) + return u"<{0.__class__.__name__} '{0.name}' as '{0.character}'>"\ + .format(self).encode('utf-8') class Crew( Person ): job = Datapoint('job') department = Datapoint('department') def __repr__(self): - return u"<{0.__class__.__name__} '{1.name}','{1.job}'>".format(self) + return u"<{0.__class__.__name__} '{0.name}','{0.job}'>"\ + .format(self).encode('utf-8') class Keyword( Element ): id = Datapoint('id') name = Datapoint('name') def __repr__(self): - return u"<{0.__class__.__name__} {0.name}>".format(self) + return u"<{0.__class__.__name__} {0.name}>".format(self).encode('utf-8') class Release( Element ): certification = Datapoint('certification') country = Datapoint('iso_3166_1') releasedate = Datapoint('release_date', handler=process_date) def __repr__(self): - return u"".format(self) + return u"<{0.__class__.__name__} {0.country}, {0.releasedate}>"\ + .format(self).encode('utf-8') class Trailer( Element ): name = Datapoint('name') @@ -241,6 +242,7 @@ def geturl(self): return "http://www.youtube.com/watch?v={0}".format(self.source) def __repr__(self): + # modified BASE64 encoding, no need to worry about unicode return u"<{0.__class__.__name__} '{0.name}'>".format(self) class AppleTrailer( Element ): @@ -265,16 +267,14 @@ class Translation( Element ): englishname = Datapoint('english_name') def __repr__(self): - return u"<{0.__class__.__name__} '{0.name}' ({0.language})>".format(self) + return u"<{0.__class__.__name__} '{0.name}' ({0.language})>"\ + .format(self).encode('utf-8') -class Genre( Element ): +class Genre( NameRepr, Element ): id = Datapoint('id') name = Datapoint('name') - def __repr__(self): - return u"<{0.__class__.__name__} '{0.name}'>".format(self) - -class Studio( Element ): +class Studio( NameRepr, Element ): id = Datapoint('id', initarg=1) name = Datapoint('name') description = Datapoint('description') @@ -282,39 +282,33 @@ class Studio( Element ): logo = Datapoint('logo_path', handler=Logo, raw=False) # FIXME: manage not-yet-defined handlers in a way that will propogate # locale information properly - parent = Datapoint('parent_company', handler=lambda x: Studio(raw=x)) - - def __repr__(self): - return u"<{0.__class__.__name__} '{0.name}'>".format(self) + parent = Datapoint('parent_company', \ + handler=lambda x: Studio(raw=x)) def _populate(self): return Request('company/{0}'.format(self.id)) def _populate_movies(self): - return Request('company/{0}/movies'.format(self.id), language=self._locale.language) + return Request('company/{0}/movies'.format(self.id), \ + language=self._locale.language) # FIXME: add a cleaner way of adding types with no additional processing @property def movies(self): if 'movies' not in self._data: - search = MovieSearchResult(self._populate_movies(), locale=self._locale) + search = MovieSearchResult(self._populate_movies(), \ + locale=self._locale) search._name = "{0.name} Movies".format(self) self._data['movies'] = search return self._data['movies'] -class Country( Element ): +class Country( NameRepr, Element ): code = Datapoint('iso_3166_1') name = Datapoint('name') - def __repr__(self): - return u"<{0.__class__.__name__} '{0.name}'>".format(self) - -class Language( Element ): +class Language( NameRepr, Element ): code = Datapoint('iso_639_1') name = Datapoint('name') - def __repr__(self): - return u"<{0.__class__.__name__} '{0.name}'>".format(self) - class Movie( Element ): @classmethod def latest(cls): @@ -405,37 +399,54 @@ def fromIMDB(cls, imdbid, locale=None): languages = Datalist('spoken_languages', handler=Language) def _populate(self): - return Request('movie/{0}'.format(self.id), language=self._locale.language) + return Request('movie/{0}'.format(self.id), \ + language=self._locale.language) def _populate_titles(self): - kwargs = {'country':self._locale.country} if not self._locale.fallthrough else {} + kwargs = {} + if not self._locale.fallthrough: + kwargs['country'] = self._locale.country return Request('movie/{0}/alternative_titles'.format(self.id), **kwargs) def _populate_cast(self): return Request('movie/{0}/casts'.format(self.id)) def _populate_images(self): - kwargs = {'language':self._locale.language} if not self._locale.fallthrough else {} + kwargs = {} + if not self._locale.fallthrough: + kwargs['country'] = self._locale.country return Request('movie/{0}/images'.format(self.id), **kwargs) def _populate_keywords(self): return Request('movie/{0}/keywords'.format(self.id)) def _populate_releases(self): return Request('movie/{0}/releases'.format(self.id)) def _populate_trailers(self): - return Request('movie/{0}/trailers'.format(self.id), language=self._locale.language) + return Request('movie/{0}/trailers'.format(self.id), \ + language=self._locale.language) def _populate_translations(self): return Request('movie/{0}/translations'.format(self.id)) - alternate_titles = Datalist('titles', handler=AlternateTitle, poller=_populate_titles, sort=True) - cast = Datalist('cast', handler=Cast, poller=_populate_cast, sort='order') + alternate_titles = Datalist('titles', handler=AlternateTitle, \ + poller=_populate_titles, sort=True) + cast = Datalist('cast', handler=Cast, \ + poller=_populate_cast, sort='order') crew = Datalist('crew', handler=Crew, poller=_populate_cast) - backdrops = Datalist('backdrops', handler=Backdrop, poller=_populate_images, sort=True) - posters = Datalist('posters', handler=Poster, poller=_populate_images, sort=True) - keywords = Datalist('keywords', handler=Keyword, poller=_populate_keywords) - releases = Datadict('countries', handler=Release, poller=_populate_releases, attr='country') - youtube_trailers = Datalist('youtube', handler=YoutubeTrailer, poller=_populate_trailers) - apple_trailers = Datalist('quicktime', handler=AppleTrailer, poller=_populate_trailers) - translations = Datalist('translations', handler=Translation, poller=_populate_translations) + backdrops = Datalist('backdrops', handler=Backdrop, \ + poller=_populate_images, sort=True) + posters = Datalist('posters', handler=Poster, \ + poller=_populate_images, sort=True) + keywords = Datalist('keywords', handler=Keyword, \ + poller=_populate_keywords) + releases = Datadict('countries', handler=Release, \ + poller=_populate_releases, attr='country') + youtube_trailers = Datalist('youtube', handler=YoutubeTrailer, \ + poller=_populate_trailers) + apple_trailers = Datalist('quicktime', handler=AppleTrailer, \ + poller=_populate_trailers) + translations = Datalist('translations', handler=Translation, \ + poller=_populate_translations) def setFavorite(self, value): - req = Request('account/{0}/favorite'.format(Account(session=self._session).id), session_id=self._session.sessionid) + req = Request('account/{0}/favorite'.format(\ + Account(session=self._session).id), + session_id=self._session.sessionid) req.add_data({'movie_id':self.id, 'favorite':str(bool(value)).lower()}) req.lifetime = 0 req.readJSON() @@ -443,12 +454,13 @@ def setFavorite(self, value): def setRating(self, value): if not (0 <= value <= 10): raise TMDBError("Ratings must be between '0' and '10'.") - req = Request('movie/{0}/favorite'.format(self.id), session_id=self._session.sessionid) + req = Request('movie/{0}/favorite'.format(self.id), \ + session_id=self._session.sessionid) req.lifetime = 0 req.add_data({'value':value}) req.readJSON() - def __repr__(self): + def _printable_name(self): if self.title is not None: s = u"'{0}'".format(self.title) elif self.originaltitle is not None: @@ -457,38 +469,28 @@ def __repr__(self): s = u"'No Title'" if self.releasedate: s = u"{0} ({1})".format(s, self.releasedate.year) - return u"<{0} {1}>".format(self.__class__.__name__, s).encode('utf-8') + return s + + def __repr__(self): + return u"<{0} {1}>".format(self.__class__.__name__,\ + self._printable_name()).encode('utf-8') class ReverseCast( Movie ): character = Datapoint('character') def __repr__(self): - if self.title is not None: - s = u"'{0}'".format(self.title) - elif self.originaltitle is not None: - s = u"'{0}'".format(self.originaltitle) - else: - s = u"'No Title'" - if self.releasedate: - s = u"{0} ({1})".format(s, self.releasedate.year) - return u"<{0.__class__.__name__} '{0.character}' on {1}>".format(self, s).encode('utf-8') + return u"<{0.__class__.__name__} '{0.character}' on {1}>"\ + .format(self, self._printable_name()).encode('utf-8') class ReverseCrew( Movie ): department = Datapoint('department') job = Datapoint('job') def __repr__(self): - if self.title is not None: - s = u"'{0}'".format(self.title) - elif self.originaltitle is not None: - s = u"'{0}'".format(self.originaltitle) - else: - s = u"'No Title'" - if self.releasedate: - s = u"{0} ({1})".format(s, self.releasedate.year) - return u"<{0.__class__.__name__} '{0.job}' for {1}>".format(self, s).encode('utf-8') + return u"<{0.__class__.__name__} '{0.job}' for {1}>"\ + .format(self, self._printable_name()).encode('utf-8') -class Collection( Element ): +class Collection( NameRepr, Element ): id = Datapoint('id', initarg=1) name = Datapoint('name') backdrop = Datapoint('backdrop_path', handler=Backdrop, raw=False) @@ -496,11 +498,8 @@ class Collection( Element ): members = Datalist('parts', handler=Movie, sort='releasedate') def _populate(self): - return Request('collection/{0}'.format(self.id), language=self._locale.language) - - def __repr__(self): - return u"<{0.__class__.__name__} '{0.name}'>".format(self).encode('utf-8') - return u"<{0} {1}>".format(self.__class__.__name__, s).encode('utf-8') + return Request('collection/{0}'.format(self.id), \ + language=self._locale.language) if __name__ == '__main__': set_key('c27cb71cff5bd76e1a7a009380562c62') #MythTV API Key diff --git a/tmdb3/util.py b/tmdb3/util.py index 27cb91a6dd0..1c3209e0fe2 100644 --- a/tmdb3/util.py +++ b/tmdb3/util.py @@ -10,6 +10,21 @@ from locales import get_locale from tmdb_auth import get_session +class NameRepr( object ): + """Mixin for __repr__ methods using 'name' attribute.""" + def __repr__(self): + return u"<{0.__class__.__name__} '{0.name}'>"\ + .format(self).encode('utf-8') + +class SearchRepr( object ): + """ + Mixin for __repr__ methods for classes with '_name' and + '_request' attributes. + """ + def __repr__(self): + name = self._name if self._name else self._request._kwargs['query'] + return u"".format(name).encode('utf-8') + class Poller( object ): """ Wrapper for an optional callable to populate an Element derived class @@ -91,10 +106,10 @@ def __init__(self, field, initarg=None, handler=None, poller=None, """ This defines how the dictionary value is to be processed by the poller field -- defines the dictionary key that filters what data this uses - initarg -- (optional) specifies that this field must be supplied when - creating a new instance of the Element class this definition - is mapped to. Takes an integer for the order it should be - used in the input arguments + initarg -- (optional) specifies that this field must be supplied + when creating a new instance of the Element class this + definition is mapped to. Takes an integer for the order + it should be used in the input arguments handler -- (optional) callable used to process the received value before being stored in the Element object. poller -- (optional) callable to be used if data is requested and @@ -195,7 +210,8 @@ class Datadict( Data ): Response definition class for dictionary data This maps to a key in a JSON dictionary storing a dictionary of data """ - def __init__(self, field, handler=None, poller=None, raw=True, key=None, attr=None): + def __init__(self, field, handler=None, poller=None, raw=True, + key=None, attr=None): """ This defines how the dictionary value is to be processed by the poller field -- defines the dictionary key that filters what data this uses From 30a0d398cc5f40487a69ddcc3beff77b09052def Mon Sep 17 00:00:00 2001 From: Raymond Wagner Date: Thu, 3 May 2012 02:56:30 -0400 Subject: [PATCH 51/69] Add method to search for movies similar to current. --- README | 3 ++- setup.py | 2 +- tmdb3/tmdb_api.py | 10 +++++++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/README b/README index fde1c90863a..0d7d5006ff0 100644 --- a/README +++ b/README @@ -170,7 +170,7 @@ trailers offer multiple sizes. ## List of Available Data - type name + type name Collection: integer id @@ -210,6 +210,7 @@ Movie: list(Keyword) keywords dict(Release) releases (indexed by country) list(Translation) translations + list(Movie) getSimilar() Movie classmethods Movie fromIMDB(imdbid) special constructor for use with IMDb codes diff --git a/setup.py b/setup.py index 9a391022f9f..f6a3c67534e 100755 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ setup( name='tmdb3', - version='0.6.1', + version='0.6.2', description='TheMovieDB.org APIv3 interface', long_description="Object-oriented interface to TheMovieDB.org's v3 API.", packages=['tmdb3'] diff --git a/tmdb3/tmdb_api.py b/tmdb3/tmdb_api.py index c698ec654cd..c361536d008 100644 --- a/tmdb3/tmdb_api.py +++ b/tmdb3/tmdb_api.py @@ -22,7 +22,7 @@ Preliminary API specifications can be found at http://help.themoviedb.org/kb/api/about-3""" -__version__="v0.6.1" +__version__="v0.6.2" # 0.1.0 Initial development # 0.2.0 Add caching mechanism for API queries # 0.2.1 Temporary work around for broken search paging @@ -44,6 +44,7 @@ # 0.5.0 Rework cache framework and improve file cache performance # 0.6.0 Add user authentication support # 0.6.1 Add adult filtering for people searches +# 0.6.2 Add similar movie search for Movie objects from request import set_key, Request from util import Datapoint, Datalist, Datadict, Element, NameRepr, SearchRepr @@ -460,6 +461,13 @@ def setRating(self, value): req.add_data({'value':value}) req.readJSON() + def getSimilar(self): + res = MovieSearchResult(Request('movie/{0}/similar_movies'\ + .format(self.id)), + locale=self._locale) + res._name = 'Similar to {0}'.format(self._printable_name()) + return res + def _printable_name(self): if self.title is not None: s = u"'{0}'".format(self.title) From 1debfc665fdda71e328f8c649f2c25a87912426a Mon Sep 17 00:00:00 2001 From: Raymond Wagner Date: Thu, 3 May 2012 10:55:25 -0400 Subject: [PATCH 52/69] Add --parse-video-filename option to test filename parser. This adds a new option to MythUtil to hook directly into the filename parsing routines used by the Video Library, to allow them to be tested directly for irregularities rather than having to go through the scanner. --- mythtv/programs/mythutil/backendutils.cpp | 20 +++++++++++++++++++ .../programs/mythutil/commandlineparser.cpp | 4 ++++ mythtv/programs/mythutil/mythutil.pro | 2 +- 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/mythtv/programs/mythutil/backendutils.cpp b/mythtv/programs/mythutil/backendutils.cpp index f4f43ea28ee..8c6394aa760 100644 --- a/mythtv/programs/mythutil/backendutils.cpp +++ b/mythtv/programs/mythutil/backendutils.cpp @@ -1,9 +1,13 @@ +// C++ includes +#include + // libmyth* headers #include "exitcodes.h" #include "mythcorecontext.h" #include "mythlogging.h" #include "remoteutil.h" #include "scheduledrecording.h" +#include "videometadata.h" // local headers #include "backendutils.h" @@ -76,6 +80,21 @@ static int ScanVideos(const MythUtilCommandLineParser &cmdline) return GENERIC_EXIT_CONNECT_ERROR; } +static int ParseVideoFilename(const MythUtilCommandLineParser &cmdline) +{ + QString filename = cmdline.toString("parsevideo"); + cout << "Title: " << VideoMetadata::FilenameToMeta(filename, 1) + .toLocal8Bit().constData() << endl + << "Season: " << VideoMetadata::FilenameToMeta(filename, 2) + .toLocal8Bit().constData() << endl + << "Episode: " << VideoMetadata::FilenameToMeta(filename, 3) + .toLocal8Bit().constData() << endl + << "Subtitle: " << VideoMetadata::FilenameToMeta(filename, 4) + .toLocal8Bit().constData() << endl; + + return GENERIC_EXIT_OK; +} + void registerBackendUtils(UtilMap &utilMap) { utilMap["clearcache"] = &ClearSettingsCache; @@ -83,6 +102,7 @@ void registerBackendUtils(UtilMap &utilMap) utilMap["resched"] = &Reschedule; utilMap["scanvideos"] = &ScanVideos; utilMap["systemevent"] = &SendSystemEvent; + utilMap["parsevideo"] = &ParseVideoFilename; } /* vim: set expandtab tabstop=4 shiftwidth=4: */ diff --git a/mythtv/programs/mythutil/commandlineparser.cpp b/mythtv/programs/mythutil/commandlineparser.cpp index 897be65be6c..0c7cc0c0962 100644 --- a/mythtv/programs/mythutil/commandlineparser.cpp +++ b/mythtv/programs/mythutil/commandlineparser.cpp @@ -97,6 +97,10 @@ void MythUtilCommandLineParser::LoadArguments(void) "local database settings cache used by each program, causing " "options to be re-read from the database upon next use.") ->SetGroup("Backend") + << add("--parse-video-filename", "parsevideo", "", "", + "Diagnostic tool for testing filename formats against what " + "the Video Library name parser will detect them as.") + ->SetGroup("Backend") // jobutils.cpp << add("--queuejob", "queuejob", "", diff --git a/mythtv/programs/mythutil/mythutil.pro b/mythtv/programs/mythutil/mythutil.pro index 774528fff77..066e6182481 100644 --- a/mythtv/programs/mythutil/mythutil.pro +++ b/mythtv/programs/mythutil/mythutil.pro @@ -2,7 +2,7 @@ include ( ../../settings.pro ) include ( ../../version.pro ) include ( ../programs-libs.pro ) -QT += network sql +QT += network sql xml TEMPLATE = app CONFIG += thread From 7fd0a8c47b6e773fd1370870eea0a0575e5757da Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Thu, 3 May 2012 15:43:17 +0100 Subject: [PATCH 53/69] Don't refresh the whole tree when changing the watched status on a video, this avoids us losing our place and is faster. --- mythtv/programs/mythfrontend/videodlg.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mythtv/programs/mythfrontend/videodlg.cpp b/mythtv/programs/mythfrontend/videodlg.cpp index 829b77b03c4..7a8a9b2b516 100644 --- a/mythtv/programs/mythfrontend/videodlg.cpp +++ b/mythtv/programs/mythfrontend/videodlg.cpp @@ -3402,8 +3402,8 @@ void VideoDialog::ToggleWatched() { metadata->SetWatched(!metadata->GetWatched()); metadata->UpdateDatabase(); - - refreshData(); + GetItemCurrent()->DisplayState(WatchedToState(metadata->GetWatched()), + "watchedstate"); } } From a08bfed70d5d8eb72142078a9e383da7001eeef4 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Thu, 3 May 2012 16:09:08 +0100 Subject: [PATCH 54/69] Guard against a null pointer in ToggleWatched(), unlikely but best not to assume it's impossible. --- mythtv/programs/mythfrontend/videodlg.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/mythtv/programs/mythfrontend/videodlg.cpp b/mythtv/programs/mythfrontend/videodlg.cpp index 7a8a9b2b516..3cc51bcfd7f 100644 --- a/mythtv/programs/mythfrontend/videodlg.cpp +++ b/mythtv/programs/mythfrontend/videodlg.cpp @@ -3397,12 +3397,16 @@ void VideoDialog::VideoAutoSearch(MythGenericTree *node) void VideoDialog::ToggleWatched() { - VideoMetadata *metadata = GetMetadata(GetItemCurrent()); + MythUIButtonListItem *item = GetItemCurrent(); + if (!item) + return; + + VideoMetadata *metadata = GetMetadata(item); if (metadata) { metadata->SetWatched(!metadata->GetWatched()); metadata->UpdateDatabase(); - GetItemCurrent()->DisplayState(WatchedToState(metadata->GetWatched()), + item->DisplayState(WatchedToState(metadata->GetWatched()), "watchedstate"); } } From 33705ae938d5240b9f74bb8f54b2ff8bd765ede2 Mon Sep 17 00:00:00 2001 From: Jim Stichnoth Date: Thu, 3 May 2012 09:49:10 -0700 Subject: [PATCH 55/69] Apply the MythUIShape's alpha attribute when drawing it. This is primarily to allow osd_subtitle.xml to use the alpha attribute in addition to or in place of the line and fill alpha attributes, when defining subtitle backgrounds. --- mythtv/libs/libmythui/mythuishape.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/mythtv/libs/libmythui/mythuishape.cpp b/mythtv/libs/libmythui/mythuishape.cpp index 4d6d31916dc..0f3653f05c1 100644 --- a/mythtv/libs/libmythui/mythuishape.cpp +++ b/mythtv/libs/libmythui/mythuishape.cpp @@ -55,6 +55,7 @@ void MythUIShape::SetLinePen(QPen pen) void MythUIShape::DrawSelf(MythPainter *p, int xoffset, int yoffset, int alphaMod, QRect clipRect) { + int alpha = CalcAlpha(alphaMod); QRect area = GetArea(); m_cropRect.CalculateArea(area); @@ -64,11 +65,11 @@ void MythUIShape::DrawSelf(MythPainter *p, int xoffset, int yoffset, area.translate(xoffset, yoffset); if (m_type == "box") - p->DrawRect(area, m_fillBrush, m_linePen, alphaMod); + p->DrawRect(area, m_fillBrush, m_linePen, alpha); else if (m_type == "roundbox") - p->DrawRoundRect(area, m_cornerRadius, m_fillBrush, m_linePen, alphaMod); + p->DrawRoundRect(area, m_cornerRadius, m_fillBrush, m_linePen, alpha); else if (m_type == "ellipse") - p->DrawEllipse(area, m_fillBrush, m_linePen, alphaMod); + p->DrawEllipse(area, m_fillBrush, m_linePen, alpha); } /** From 390569b20e952777140d95ff6c40e754044628bd Mon Sep 17 00:00:00 2001 From: Gavin Hurlbut Date: Thu, 3 May 2012 13:50:00 -0700 Subject: [PATCH 56/69] Run git status before git describe to clear false dirties --- mythtv/version.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/mythtv/version.sh b/mythtv/version.sh index d2c53af234a..a8939c45d18 100755 --- a/mythtv/version.sh +++ b/mythtv/version.sh @@ -22,6 +22,7 @@ GITREPOPATH="exported" cd ${GITTREEDIR} +git status > /dev/null 2>&1 SOURCE_VERSION=$(git describe --dirty || git describe || echo Unknown) case "${SOURCE_VERSION}" in From 71c65ba67614c751ea149a1960e0db60437591af Mon Sep 17 00:00:00 2001 From: David Engel Date: Thu, 3 May 2012 20:23:39 -0500 Subject: [PATCH 57/69] Add recording rule templates. Recording rule templates are used to initialize new recording rules. Templates can also be used to modify existing rules. The "Default" template replaces several, individual settings previously available in the Setup wizard. Those settings are automatically propagated to the Default template. Templates can be added, edited and deleted from within the Recording Rules screen. To add a new template, choose MENU / "New Template" and then enter the name of the new template. To edit a template, highlight it and then choose ENTER or EDIT to bring up the schedule editor. To delete a template, highlight it and then choose DELETE or edit it and change the type to "Delete this recording rule template." When a regular rule is created, MythTV first tries to use the template whose name matches the category of the program. Failing that, it tries to use the template whose name matches the category type of the program. If no template is found which matches the category or category type, the Default template is used. For example, if a user frequently records baseball games, he can create a "Baseball" template which automatically sets the end-late option to allow for extra innings. Likewise, a user can create a "Series" template which automatically sets the duplicate checking method and the episode limit. When a search, manual or template rule is created, the Default template is always used. The names of template need not match program categories or category types. They can simply be available to apply to other rules. To apply a different template to an existing rule, including a template rule, begin editing the rule and then press MENU / "Use Template." For example, a user can create an "Archive" template which changes the recording group to "Archive" and turns off auto-expiry. --- mythtv/bindings/perl/MythTV.pm | 2 +- mythtv/bindings/python/MythTV/static.py | 2 +- mythtv/libs/libmyth/programinfo.h | 1 + mythtv/libs/libmyth/recordingtypes.cpp | 7 + mythtv/libs/libmyth/recordingtypes.h | 3 +- mythtv/libs/libmythbase/mythversion.h | 4 +- mythtv/libs/libmythtv/dbcheck.cpp | 13 + mythtv/libs/libmythtv/recordingrule.cpp | 220 ++++++-- mythtv/libs/libmythtv/recordingrule.h | 13 +- mythtv/libs/libmythtv/tv_play.cpp | 7 +- mythtv/libs/libmythtv/tv_rec.cpp | 2 +- mythtv/programs/mythbackend/scheduler.cpp | 3 + .../programs/mythfrontend/globalsettings.cpp | 127 ----- mythtv/programs/mythfrontend/playbackbox.cpp | 6 +- .../mythfrontend/programrecpriority.cpp | 518 ++++++++++-------- .../mythfrontend/programrecpriority.h | 15 +- .../programs/mythfrontend/scheduleeditor.cpp | 127 ++++- mythtv/programs/mythfrontend/scheduleeditor.h | 6 + mythtv/themes/MythCenter-wide/base.xml | 8 +- 19 files changed, 678 insertions(+), 406 deletions(-) diff --git a/mythtv/bindings/perl/MythTV.pm b/mythtv/bindings/perl/MythTV.pm index 8ed7c6582c5..1076d85a526 100644 --- a/mythtv/bindings/perl/MythTV.pm +++ b/mythtv/bindings/perl/MythTV.pm @@ -114,7 +114,7 @@ package MythTV; # schema version supported in the main code. We need to check that the schema # version in the database is as expected by the bindings, which are expected # to be kept in sync with the main code. - our $SCHEMA_VERSION = "1301"; + our $SCHEMA_VERSION = "1302"; # NUMPROGRAMLINES is defined in mythtv/libs/libmythtv/programinfo.h and is # the number of items in a ProgramInfo QStringList group used by diff --git a/mythtv/bindings/python/MythTV/static.py b/mythtv/bindings/python/MythTV/static.py index 8a0e7d20046..8b8abf6fa3b 100644 --- a/mythtv/bindings/python/MythTV/static.py +++ b/mythtv/bindings/python/MythTV/static.py @@ -5,7 +5,7 @@ """ OWN_VERSION = (0,26,-1,0) -SCHEMA_VERSION = 1301 +SCHEMA_VERSION = 1302 NVSCHEMA_VERSION = 1007 MUSICSCHEMA_VERSION = 1018 PROTO_VERSION = '73' diff --git a/mythtv/libs/libmyth/programinfo.h b/mythtv/libs/libmyth/programinfo.h index 34a609c7497..99326627619 100644 --- a/mythtv/libs/libmyth/programinfo.h +++ b/mythtv/libs/libmyth/programinfo.h @@ -479,6 +479,7 @@ class MPUBLIC ProgramInfo void SetFilesize( uint64_t sz) { filesize = sz; } void SetSeriesID( const QString &id) { seriesid = id; } void SetProgramID( const QString &id) { programid = id; } + void SetCategory( const QString &cat) { category = cat; } void SetCategoryType( const QString &type) { catType = type; } void SetRecordingPriority(int priority) { recpriority = priority; } void SetRecordingPriority2(int priority) { recpriority2 = priority; } diff --git a/mythtv/libs/libmyth/recordingtypes.cpp b/mythtv/libs/libmyth/recordingtypes.cpp index faed2a49ca0..4c9441ed470 100644 --- a/mythtv/libs/libmyth/recordingtypes.cpp +++ b/mythtv/libs/libmyth/recordingtypes.cpp @@ -19,6 +19,7 @@ int RecTypePriority(RecordingType rectype) case kFindDailyRecord: return 8; break; case kChannelRecord: return 9; break; case kAllRecord: return 10; break; + case kTemplateRecord: return 0; break; default: return 11; } } @@ -47,6 +48,8 @@ QString toString(RecordingType rectype) case kOverrideRecord: case kDontRecord: return QObject::tr("Override Recording"); + case kTemplateRecord: + return QObject::tr("Template Recording"); default: return QObject::tr("Not Recording"); } @@ -101,6 +104,8 @@ RecordingType recTypeFromString(QString type) return kFindDailyRecord; else if (type.toLower() == "find weekly" || type.toLower() == "findweekly") return kFindWeeklyRecord; + else if (type.toLower() == "template" || type.toLower() == "template") + return kTemplateRecord; else if (type.toLower() == "override recording" || type.toLower() == "override") return kOverrideRecord; else @@ -133,6 +138,8 @@ QChar toQChar(RecordingType rectype) case kDontRecord: ret = QObject::tr("O", "RecTypeChar kOverrideRecord/kDontRecord"); break; + case kTemplateRecord: + ret = QObject::tr("t", "RecTypeChar kTemplateRecord"); break; case kNotRecording: default: ret = " "; diff --git a/mythtv/libs/libmyth/recordingtypes.h b/mythtv/libs/libmyth/recordingtypes.h index d9c41f2952d..91f056406ed 100644 --- a/mythtv/libs/libmyth/recordingtypes.h +++ b/mythtv/libs/libmyth/recordingtypes.h @@ -17,7 +17,8 @@ typedef enum RecordingTypes kOverrideRecord, kDontRecord, kFindDailyRecord, - kFindWeeklyRecord + kFindWeeklyRecord, + kTemplateRecord } RecordingType; // note stored in uin8_t in ProgramInfo MPUBLIC QString toString(RecordingType); MPUBLIC QString toRawString(RecordingType); diff --git a/mythtv/libs/libmythbase/mythversion.h b/mythtv/libs/libmythbase/mythversion.h index ae78adb7e99..3390189b6f0 100644 --- a/mythtv/libs/libmythbase/mythversion.h +++ b/mythtv/libs/libmythbase/mythversion.h @@ -12,7 +12,7 @@ /// Update this whenever the plug-in API changes. /// Including changes in the libmythbase, libmyth, libmythtv, libmythav* and /// libmythui class methods used by plug-ins. -#define MYTH_BINARY_VERSION "0.26.20120502-1" +#define MYTH_BINARY_VERSION "0.26.20120503-1" /** \brief Increment this whenever the MythTV network protocol changes. * @@ -57,7 +57,7 @@ * mythtv/bindings/php/MythBackend.php #endif -#define MYTH_DATABASE_VERSION "1301" +#define MYTH_DATABASE_VERSION "1302" MBASE_PUBLIC const char *GetMythSourceVersion(); diff --git a/mythtv/libs/libmythtv/dbcheck.cpp b/mythtv/libs/libmythtv/dbcheck.cpp index 6d75e472d4a..85c4683fb7a 100644 --- a/mythtv/libs/libmythtv/dbcheck.cpp +++ b/mythtv/libs/libmythtv/dbcheck.cpp @@ -15,6 +15,7 @@ using namespace std; #include "diseqcsettings.h" // for convert_diseqc_db() #include "videodbcheck.h" // for 1267 #include "compat.h" +#include "recordingrule.h" #define MINIMUM_DBMS_VERSION 5,0,15 @@ -1982,6 +1983,18 @@ NULL return false; } + if (dbver == "1301") + { + // Create the Default recording rule template + RecordingRule record; + record.MakeTemplate("Default"); + record.m_type = kTemplateRecord; + record.Save(false); + + if (!UpdateDBVersionNumber("1302", dbver)) + return false; + } + return true; } diff --git a/mythtv/libs/libmythtv/recordingrule.cpp b/mythtv/libs/libmythtv/recordingrule.cpp index 907f92b0e1d..8897c68e757 100644 --- a/mythtv/libs/libmythtv/recordingrule.cpp +++ b/mythtv/libs/libmythtv/recordingrule.cpp @@ -18,6 +18,11 @@ static inline QString null_to_empty(const QString &str) return str.isEmpty() ? "" : str; } +// If the GetNumSetting() calls here are ever removed, update schema +// upgrade 1302 in dbcheck.cpp to manually apply them to the Default +// template. Failing to do so will cause users upgrading from older +// versions to lose those settings. + RecordingRule::RecordingRule() : m_recordID(-1), m_parentRecID(0), m_isInactive(false), @@ -65,12 +70,14 @@ RecordingRule::RecordingRule() m_recordTable("record"), m_tempID(0), m_isOverride(false), + m_isTemplate(false), + m_template(), m_progInfo(NULL), m_loaded(false) { } -bool RecordingRule::Load() +bool RecordingRule::Load(bool asTemplate) { if (m_recordID <= 0) return false; @@ -95,40 +102,51 @@ bool RecordingRule::Load() return false; } - if (query.next()) + if (!query.next()) + return false; + + // Schedule + if (!asTemplate) { - // Schedule m_type = static_cast(query.value(0).toInt()); m_searchType = static_cast(query.value(1).toInt()); - m_recPriority = query.value(2).toInt(); - m_prefInput = query.value(3).toInt(); - m_startOffset = query.value(4).toInt(); - m_endOffset = query.value(5).toInt(); - m_dupMethod = static_cast - (query.value(6).toInt()); - m_dupIn = static_cast(query.value(7).toInt()); - m_filter = query.value(47).toUInt(); - m_isInactive = query.value(8).toBool(); - // Storage - m_recProfile = query.value(9).toString(); - m_recGroup = query.value(10).toString(); - m_storageGroup = query.value(11).toString(); - m_playGroup = query.value(12).toString(); - m_autoExpire = query.value(13).toBool(); - m_maxEpisodes = query.value(14).toInt(); - m_maxNewest = query.value(15).toBool(); - // Post Process - m_autoCommFlag = query.value(16).toBool(); - m_autoTranscode = query.value(17).toBool(); - m_transcoder = query.value(18).toInt(); - m_autoUserJob1 = query.value(19).toBool(); - m_autoUserJob2 = query.value(20).toBool(); - m_autoUserJob3 = query.value(21).toBool(); - m_autoUserJob4 = query.value(22).toBool(); - m_autoMetadataLookup = query.value(23).toBool(); - // Original rule id for override rule + } + m_recPriority = query.value(2).toInt(); + m_prefInput = query.value(3).toInt(); + m_startOffset = query.value(4).toInt(); + m_endOffset = query.value(5).toInt(); + m_dupMethod = static_cast + (query.value(6).toInt()); + m_dupIn = static_cast(query.value(7).toInt()); + m_filter = query.value(47).toUInt(); + m_isInactive = query.value(8).toBool(); + + // Storage + m_recProfile = query.value(9).toString(); + m_recGroup = query.value(10).toString(); + m_storageGroup = query.value(11).toString(); + m_playGroup = query.value(12).toString(); + m_autoExpire = query.value(13).toBool(); + m_maxEpisodes = query.value(14).toInt(); + m_maxNewest = query.value(15).toBool(); + + // Post Process + m_autoCommFlag = query.value(16).toBool(); + m_autoTranscode = query.value(17).toBool(); + m_transcoder = query.value(18).toInt(); + m_autoUserJob1 = query.value(19).toBool(); + m_autoUserJob2 = query.value(20).toBool(); + m_autoUserJob3 = query.value(21).toBool(); + m_autoUserJob4 = query.value(22).toBool(); + m_autoMetadataLookup = query.value(23).toBool(); + + // Original rule id for override rule + if (!asTemplate) m_parentRecID = query.value(24).toInt(); - // Recording metadata + + // Recording metadata + if (!asTemplate) + { m_title = query.value(25).toString(); m_subtitle = query.value(26).toString(); m_description = query.value(27).toString(); @@ -142,27 +160,36 @@ bool RecordingRule::Load() m_seriesid = query.value(35).toString(); m_programid = query.value(36).toString(); m_inetref = query.value(37).toString(); - // Associated data for rule types + } + + // Associated data for rule types + if (!asTemplate) + { m_channelid = query.value(38).toInt(); m_station = query.value(39).toString(); m_findday = query.value(40).toInt(); m_findtime = query.value(41).toTime(); m_findid = query.value(42).toInt(); - // Statistic fields - Used to generate statistics about particular rules - // and influence watch list weighting + } + + // Statistic fields - Used to generate statistics about particular rules + // and influence watch list weighting + if (!asTemplate) + { m_nextRecording = query.value(43).toDateTime(); m_lastRecorded = query.value(44).toDateTime(); m_lastDeleted = query.value(45).toDateTime(); m_averageDelay = query.value(46).toInt(); - - m_isOverride = (m_type == kOverrideRecord || m_type == kDontRecord); - } - else - { - return false; } - m_loaded = true; + m_isOverride = (m_type == kOverrideRecord || m_type == kDontRecord); + m_isTemplate = (m_type == kTemplateRecord); + m_template = (asTemplate || m_isTemplate) ? + query.value(30).toString() : ""; + + if (!asTemplate) + m_loaded = true; + return true; } @@ -173,13 +200,14 @@ bool RecordingRule::LoadByProgram(const ProgramInfo* proginfo) m_progInfo = proginfo; - if (proginfo->GetRecordingRuleID()) - { - m_recordID = proginfo->GetRecordingRuleID(); + m_recordID = proginfo->GetRecordingRuleID(); + if (m_recordID) Load(); - } + else + LoadTemplate(proginfo->GetCategory(), proginfo->GetCategoryType()); - if (m_searchType == kNoSearch || m_searchType == kManualSearch) + if (m_type != kTemplateRecord && + (m_searchType == kNoSearch || m_searchType == kManualSearch)) { AssignProgramInfo(); if (!proginfo->GetRecordingRuleID()) @@ -221,6 +249,8 @@ bool RecordingRule::LoadBySearch(RecSearchType lsearch, QString textname, } else { + LoadTemplate("Default"); + QString searchType; m_searchType = lsearch; searchType = SearchTypeToString(m_searchType); @@ -238,6 +268,66 @@ bool RecordingRule::LoadBySearch(RecSearchType lsearch, QString textname, return true; } +bool RecordingRule::LoadTemplate(QString category, QString categoryType) +{ + MSqlQuery query(MSqlQuery::InitCon()); + query.prepare("SELECT recordid, category, " + " (category = :CAT1) AS catmatch, " + " (category = :CATTYPE1) AS typematch " + "FROM record " + "WHERE type = :TEMPLATE AND " + " (category = :CAT2 OR category = :CATTYPE2 " + " OR category = 'Default') " + "ORDER BY catmatch DESC, typematch DESC" + ); + query.bindValue(":TEMPLATE", kTemplateRecord); + query.bindValue(":CAT1", category); + query.bindValue(":CAT2", category); + query.bindValue(":CATTYPE1", categoryType); + query.bindValue(":CATTYPE2", categoryType); + + if (!query.exec()) + { + MythDB::DBError("LoadByTemplate", query); + return false; + } + + if (!query.next()) + return false; + + int savedRecordID = m_recordID; + m_recordID = query.value(0).toInt(); + bool result = Load(true); + m_recordID = savedRecordID; + + return result; +} + +bool RecordingRule::MakeTemplate(QString category) +{ + if (m_recordID > 0) + return false; + + if (category.compare(QObject::tr("Default"), Qt::CaseInsensitive) == 0) + { + category = "Default"; + m_title = QObject::tr("Default (Template)"); + } + else + { + m_title = category + QObject::tr(" (Template)"); + } + + LoadTemplate(category); + m_recordID = 0; + m_type = kNotRecording; + m_category = category; + m_loaded = true; + m_isTemplate = true; + + return true; +} + bool RecordingRule::ModifyPowerSearchByID(int rid, QString textname, QString forwhat, QString from) { @@ -422,13 +512,19 @@ bool RecordingRule::Delete(bool sendSig) void RecordingRule::ToMap(InfoMap &infoMap) const { - infoMap["title"] = m_title; + if (m_title == "Default (Template)") + infoMap["title"] = QObject::tr("Default (Template)"); + else + infoMap["title"] = m_title; infoMap["subtitle"] = m_subtitle; infoMap["description"] = m_description; infoMap["season"] = QString::number(m_season); infoMap["episode"] = QString::number(m_episode); - infoMap["category"] = m_category; + if (m_category == "Default") + infoMap["category"] = QObject::tr("Default"); + else + infoMap["category"] = m_category; infoMap["callsign"] = m_station; infoMap["starttime"] = MythTimeToString(m_starttime, kTime); @@ -504,6 +600,11 @@ void RecordingRule::ToMap(InfoMap &infoMap) const infoMap["ruletype"] = toString(m_type); infoMap["rectype"] = toString(m_type); + + if (m_template == "Default") + infoMap["template"] = QObject::tr("Default"); + else + infoMap["template"] = m_template; } void RecordingRule::UseTempTable(bool usetemp, QString table) @@ -654,3 +755,26 @@ QString RecordingRule::SearchTypeToString(const RecSearchType searchType) return searchTypeString; } +QStringList RecordingRule::GetTemplateNames(void) +{ + QStringList result; + + MSqlQuery query(MSqlQuery::InitCon()); + query.prepare("SELECT category " + "FROM record " + "WHERE type = :TEMPLATE " + "ORDER BY category = 'Default' DESC, category" + ); + query.bindValue(":TEMPLATE", kTemplateRecord); + + if (!query.exec()) + { + MythDB::DBError("LoadByTemplate", query); + return result; + } + + while (query.next()) + result << query.value(0).toString(); + + return result; +} diff --git a/mythtv/libs/libmythtv/recordingrule.h b/mythtv/libs/libmythtv/recordingrule.h index d7192310b1f..4184e10be8b 100644 --- a/mythtv/libs/libmythtv/recordingrule.h +++ b/mythtv/libs/libmythtv/recordingrule.h @@ -5,6 +5,7 @@ #include #include #include +#include // libmythbase #include "mythtvexp.h" @@ -35,14 +36,16 @@ class MTV_PUBLIC RecordingRule RecordingRule(); ~RecordingRule() {}; - bool Load(void); + bool Load(bool asTemplate = false); bool LoadByProgram(const ProgramInfo* proginfo); bool LoadBySearch(RecSearchType lsearch, QString textname, QString forwhat, QString from = ""); + bool LoadTemplate(QString category, QString categoryType = "Default"); bool ModifyPowerSearchByID(int rid, QString textname, QString forwhat, QString from = ""); bool MakeOverride(void); + bool MakeTemplate(QString category); bool Save(bool sendSig = true); bool Delete(bool sendSig = true); @@ -57,6 +60,7 @@ class MTV_PUBLIC RecordingRule { return m_autoExpire ? kNormalAutoExpire : kDisableAutoExpire; } static QString SearchTypeToString(const RecSearchType searchType); + static QStringList GetTemplateNames(void); int m_recordID; /// Unique Recording Rule ID int m_parentRecID; @@ -134,10 +138,17 @@ class MTV_PUBLIC RecordingRule // different options for override rules bool m_isOverride; + // Is this a template rule? Purely for the benefit of the UI, we + // display all options for template rules + bool m_isTemplate; + private: // Populate variables from a ProgramInfo object void AssignProgramInfo(); + // Name of template used for new rule. + QString m_template; + // Pointer for ProgramInfo, exists only if we loaded from ProgramInfo in // the first place const ProgramInfo *m_progInfo; diff --git a/mythtv/libs/libmythtv/tv_play.cpp b/mythtv/libs/libmythtv/tv_play.cpp index 295fa1c931b..f378e861a81 100644 --- a/mythtv/libs/libmythtv/tv_play.cpp +++ b/mythtv/libs/libmythtv/tv_play.cpp @@ -34,6 +34,7 @@ using namespace std; #include "DisplayRes.h" #include "signalmonitorvalue.h" #include "scheduledrecording.h" +#include "recordingrule.h" #include "previewgenerator.h" #include "mythconfig.h" #include "livetvchain.h" @@ -1014,7 +1015,6 @@ void TV::InitFromDB(void) kv["LiveTVIdleTimeout"] = "0"; kv["BrowseMaxForward"] = "240"; kv["PlaybackExitPrompt"] = "0"; - kv["AutoExpireDefault"] = "0"; kv["AutomaticSetWatched"] = "0"; kv["EndOfRecordingExitPrompt"] = "0"; kv["JumpToProgramOSD"] = "1"; @@ -1063,7 +1063,6 @@ void TV::InitFromDB(void) db_idle_timeout = kv["LiveTVIdleTimeout"].toInt() * 60 * 1000; db_browse_max_forward = kv["BrowseMaxForward"].toInt() * 60; db_playback_exit_prompt= kv["PlaybackExitPrompt"].toInt(); - db_autoexpire_default = kv["AutoExpireDefault"].toInt(); db_auto_set_watched = kv["AutomaticSetWatched"].toInt(); db_end_of_rec_exit_prompt = kv["EndOfRecordingExitPrompt"].toInt(); db_jump_prefer_osd = kv["JumpToProgramOSD"].toInt(); @@ -1092,6 +1091,10 @@ void TV::InitFromDB(void) QString beVBI = kv["VbiFormat"]; QString feVBI = kv["DecodeVBIFormat"]; + RecordingRule record; + record.LoadTemplate("Default"); + db_autoexpire_default = record.m_autoExpire; + if (db_use_channel_groups) { db_channel_groups = ChannelGroup::GetChannelGroups(); diff --git a/mythtv/libs/libmythtv/tv_rec.cpp b/mythtv/libs/libmythtv/tv_rec.cpp index 75078c2de76..873b57878e6 100644 --- a/mythtv/libs/libmythtv/tv_rec.cpp +++ b/mythtv/libs/libmythtv/tv_rec.cpp @@ -2661,7 +2661,7 @@ void TVRec::NotifySchedulerOfRecording(RecordingInfo *rec) rec->GetRecordingRule()->m_type = kSingleRecord; } - // + remove DefaultEndOffset which would mismatch the live session + // + remove any end offset which would mismatch the live session rec->GetRecordingRule()->m_endOffset = 0; // + save rsInactive recstatus to so that a reschedule call diff --git a/mythtv/programs/mythbackend/scheduler.cpp b/mythtv/programs/mythbackend/scheduler.cpp index 9328ab88098..1831f05207d 100644 --- a/mythtv/programs/mythbackend/scheduler.cpp +++ b/mythtv/programs/mythbackend/scheduler.cpp @@ -3391,11 +3391,13 @@ void Scheduler::BuildNewRecordsQueries(uint recordid, QStringList &from, if (recordid != 0) recidmatch = "RECTABLE.recordid = :NRRECORDID AND "; QString s1 = recidmatch + + "RECTABLE.type <> :NRTEMPLATE AND " "RECTABLE.search = :NRST AND " "program.manualid = 0 AND " "program.title = RECTABLE.title "; s1.replace("RECTABLE", recordTable); QString s2 = recidmatch + + "RECTABLE.type <> :NRTEMPLATE AND " "RECTABLE.search = :NRST AND " "program.manualid = 0 AND " "program.seriesid <> '' AND " @@ -3406,6 +3408,7 @@ void Scheduler::BuildNewRecordsQueries(uint recordid, QStringList &from, where << s1; from << ""; where << s2; + bindings[":NRTEMPLATE"] = kTemplateRecord; bindings[":NRST"] = kNoSearch; if (recordid != 0) bindings[":NRRECORDID"] = recordid; diff --git a/mythtv/programs/mythfrontend/globalsettings.cpp b/mythtv/programs/mythfrontend/globalsettings.cpp index 9f9ee5a695d..9d19e9b8406 100644 --- a/mythtv/programs/mythfrontend/globalsettings.cpp +++ b/mythtv/programs/mythfrontend/globalsettings.cpp @@ -210,51 +210,6 @@ static HostComboBox *AutoCommercialSkip() return gc; } -static GlobalCheckBox *AutoMetadataLookup() -{ - GlobalCheckBox *bc = new GlobalCheckBox("AutoMetadataLookup"); - bc->setLabel(QObject::tr("Run metadata lookup")); - bc->setValue(true); - bc->setHelpText(QObject::tr("This is the default value used for the " - "automatic metadata lookup setting when a new " - "scheduled recording is created.")); - return bc; -} - -static GlobalCheckBox *AutoCommercialFlag() -{ - GlobalCheckBox *bc = new GlobalCheckBox("AutoCommercialFlag"); - bc->setLabel(QObject::tr("Run commercial detection")); - bc->setValue(true); - bc->setHelpText(QObject::tr("This is the default value used for the " - "automatic commercial detection setting when a new " - "scheduled recording is created.")); - return bc; -} - -static GlobalCheckBox *AutoTranscode() -{ - GlobalCheckBox *bc = new GlobalCheckBox("AutoTranscode"); - bc->setLabel(QObject::tr("Run transcoder")); - bc->setValue(false); - bc->setHelpText(QObject::tr("This is the default value used for the " - "automatic-transcode setting when a new scheduled " - "recording is created.")); - return bc; -} - -static GlobalComboBox *DefaultTranscoder() -{ - GlobalComboBox *bc = new GlobalComboBox("DefaultTranscoder"); - bc->setLabel(QObject::tr("Default transcoder")); - RecordingProfile::fillSelections(bc, RecordingProfile::TranscoderGroup, - true); - bc->setHelpText(QObject::tr("This is the default value used for the " - "transcoder setting when a new scheduled " - "recording is created.")); - return bc; -} - static GlobalSpinBox *DeferAutoTranscodeDays() { GlobalSpinBox *gs = new GlobalSpinBox("DeferAutoTranscodeDays", 0, 365, 1); @@ -266,22 +221,6 @@ static GlobalSpinBox *DeferAutoTranscodeDays() return gs; } -static GlobalCheckBox *AutoRunUserJob(uint job_num) -{ - QString dbStr = QString("AutoRunUserJob%1").arg(job_num); - QString label = QObject::tr("Run user job #%1") - .arg(job_num); - GlobalCheckBox *bc = new GlobalCheckBox(dbStr); - bc->setLabel(label); - bc->setValue(false); - bc->setHelpText(QObject::tr("This is the default value used for the " - "'Run %1' setting when a new scheduled " - "recording is created.") - .arg(gCoreContext->GetSetting(QString("UserJobDesc%1") - .arg(job_num)))); - return bc; -} - static GlobalCheckBox *AggressiveCommDetect() { GlobalCheckBox *bc = new GlobalCheckBox("AggressiveCommDetect"); @@ -449,17 +388,6 @@ static GlobalSpinBox *AutoExpireDayPriority() return bs; }; -static GlobalCheckBox *AutoExpireDefault() -{ - GlobalCheckBox *bc = new GlobalCheckBox("AutoExpireDefault"); - bc->setLabel(QObject::tr("Auto-Expire default")); - bc->setValue(true); - bc->setHelpText(QObject::tr("If enabled, any new recording schedules " - "will be marked as eligible for auto-expiration. " - "Existing schedules will keep their current value.")); - return bc; -} - static GlobalSpinBox *AutoExpireLiveTVMaxAge() { GlobalSpinBox *bs = new GlobalSpinBox("AutoExpireLiveTVMaxAge", 1, 365, 1); @@ -2410,36 +2338,6 @@ static GlobalComboBox *GRSchedOpenEnd() return bc; } -static GlobalSpinBox *GRDefaultStartOffset() -{ - GlobalSpinBox *bs = new GlobalSpinBox("DefaultStartOffset", - -10, 30, 5, true); - bs->setLabel(QObject::tr("Default 'Start Early' minutes for new " - "recording rules")); - bs->setHelpText(QObject::tr("Set this to '0' unless you expect that the " - "majority of your show times will not match your TV " - "listings. This sets the initial start early or start " - "late time when rules are created. These can then be " - "adjusted per recording rule.")); - bs->setValue(0); - return bs; -} - -static GlobalSpinBox *GRDefaultEndOffset() -{ - GlobalSpinBox *bs = new GlobalSpinBox("DefaultEndOffset", - -10, 30, 5, true); - bs->setLabel(QObject::tr("Default 'End Late' minutes for new " - "recording rules")); - bs->setHelpText(QObject::tr("Set this to '0' unless you expect that the " - "majority of your show times will not match your TV " - "listings. This sets the initial end late or end early " - "time when rules are created. These can then be adjusted " - "per recording rule.")); - bs->setValue(0); - return bs; -} - static GlobalSpinBox *GRPrefInputRecPriority() { GlobalSpinBox *bs = new GlobalSpinBox("PrefInputPriority", 1, 99, 1); @@ -3533,7 +3431,6 @@ GeneralSettings::GeneralSettings() VerticalConfigurationGroup *expgrp0 = new VerticalConfigurationGroup(false, false, true, true); - expgrp0->addChild(AutoExpireDefault()); expgrp0->addChild(RerecordWatched()); expgrp0->addChild(AutoExpireWatchedPriority()); @@ -3560,30 +3457,8 @@ GeneralSettings::GeneralSettings() jobs->addChild(CommercialSkipMethod()); jobs->addChild(CommFlagFast()); jobs->addChild(AggressiveCommDetect()); - jobs->addChild(DefaultTranscoder()); jobs->addChild(DeferAutoTranscodeDays()); - VerticalConfigurationGroup* autogrp0 = - new VerticalConfigurationGroup(false, false, true, true); - autogrp0->addChild(AutoMetadataLookup()); - autogrp0->addChild(AutoCommercialFlag()); - autogrp0->addChild(AutoTranscode()); - - VerticalConfigurationGroup* autogrp1 = - new VerticalConfigurationGroup(false, false, true, true); - autogrp0->addChild(AutoRunUserJob(1)); - autogrp1->addChild(AutoRunUserJob(2)); - autogrp1->addChild(AutoRunUserJob(3)); - autogrp1->addChild(AutoRunUserJob(4)); - - HorizontalConfigurationGroup *autogrp = - new HorizontalConfigurationGroup(true, true, false, true); - autogrp->setLabel( - QObject::tr("Default Job Queue Settings for New Scheduled Recordings")); - autogrp->addChild(autogrp0); - autogrp->addChild(autogrp1); - jobs->addChild(autogrp); - addChild(jobs); VerticalConfigurationGroup* general2 = new VerticalConfigurationGroup(false); @@ -3618,8 +3493,6 @@ GeneralRecPrioritiesSettings::GeneralRecPrioritiesSettings() sched->addChild(GRSchedMoveHigher()); sched->addChild(GRSchedOpenEnd()); - sched->addChild(GRDefaultStartOffset()); - sched->addChild(GRDefaultEndOffset()); sched->addChild(GRPrefInputRecPriority()); sched->addChild(GRHDTVRecPriority()); sched->addChild(GRWSRecPriority()); diff --git a/mythtv/programs/mythfrontend/playbackbox.cpp b/mythtv/programs/mythfrontend/playbackbox.cpp index 163681660bf..e1f89c91dd7 100644 --- a/mythtv/programs/mythfrontend/playbackbox.cpp +++ b/mythtv/programs/mythfrontend/playbackbox.cpp @@ -20,6 +20,7 @@ #include "mythuispinbox.h" #include "mythdialogbox.h" #include "recordinginfo.h" +#include "recordingrule.h" #include "mythuihelper.h" #include "storagegroup.h" #include "mythuibutton.h" @@ -4792,8 +4793,9 @@ void PlaybackBox::setRecGroup(QString newRecGroup) return; } - uint defaultAutoExpire = - gCoreContext->GetNumSetting("AutoExpireDefault", 0); + RecordingRule record; + record.LoadTemplate("Default"); + uint defaultAutoExpire = record.m_autoExpire; if (m_op_on_playlist) { diff --git a/mythtv/programs/mythfrontend/programrecpriority.cpp b/mythtv/programs/mythfrontend/programrecpriority.cpp index a33acf8555f..e67fe2866b3 100644 --- a/mythtv/programs/mythfrontend/programrecpriority.cpp +++ b/mythtv/programs/mythfrontend/programrecpriority.cpp @@ -123,56 +123,61 @@ void ProgramRecPriorityInfo::clear(void) profile.clear(); } -typedef struct RecPriorityInfo +void ProgramRecPriorityInfo::ToMap(QHash &progMap, + bool showrerecord, uint star_range) const { - ProgramRecPriorityInfo *prog; - int cnt; -} RecPriorityInfo; + RecordingInfo::ToMap(progMap, showrerecord, star_range); + progMap["title"] = (title == "Default (Template)") ? + QObject::tr("Default (Template)") : title;; + progMap["category"] = (category == "Default") ? + QObject::tr("Default") : category; +} class TitleSort { public: TitleSort(bool reverse) : m_reverse(reverse) {} - bool operator()(const RecPriorityInfo &a, const RecPriorityInfo &b) const + bool operator()(const ProgramRecPriorityInfo *a, + const ProgramRecPriorityInfo *b) const { - if (a.prog->sortTitle != b.prog->sortTitle) + if (a->sortTitle != b->sortTitle) { if (m_reverse) - return (a.prog->sortTitle < b.prog->sortTitle); + return (a->sortTitle > b->sortTitle); else - return (a.prog->sortTitle > b.prog->sortTitle); + return (a->sortTitle < b->sortTitle); } - int finalA = a.prog->GetRecordingPriority() + - a.prog->recTypeRecPriority; - int finalB = b.prog->GetRecordingPriority() + - b.prog->recTypeRecPriority; + int finalA = a->GetRecordingPriority() + + a->recTypeRecPriority; + int finalB = b->GetRecordingPriority() + + b->recTypeRecPriority; if (finalA != finalB) { if (m_reverse) - return finalA > finalB; - else return finalA < finalB; + else + return finalA > finalB; } - int typeA = RecTypePriority(a.prog->recType); - int typeB = RecTypePriority(b.prog->recType); + int typeA = RecTypePriority(a->recType); + int typeB = RecTypePriority(b->recType); if (typeA != typeB) { if (m_reverse) - return typeA < typeB; - else return typeA > typeB; + else + return typeA < typeB; } if (m_reverse) - return (a.prog->GetRecordingRuleID() < - b.prog->GetRecordingRuleID()); + return (a->GetRecordingRuleID() > + b->GetRecordingRuleID()); else - return (a.prog->GetRecordingRuleID() > - b.prog->GetRecordingRuleID()); + return (a->GetRecordingRuleID() < + b->GetRecordingRuleID()); } private: @@ -184,40 +189,41 @@ class ProgramRecPrioritySort public: ProgramRecPrioritySort(bool reverse) : m_reverse(reverse) {} - bool operator()(const RecPriorityInfo &a, const RecPriorityInfo &b) const + bool operator()(const ProgramRecPriorityInfo *a, + const ProgramRecPriorityInfo *b) const { - int finalA = (a.prog->GetRecordingPriority() + - a.prog->autoRecPriority + - a.prog->recTypeRecPriority); - int finalB = (b.prog->GetRecordingPriority() + - b.prog->autoRecPriority + - b.prog->recTypeRecPriority); + int finalA = (a->GetRecordingPriority() + + a->autoRecPriority + + a->recTypeRecPriority); + int finalB = (b->GetRecordingPriority() + + b->autoRecPriority + + b->recTypeRecPriority); if (finalA != finalB) { if (m_reverse) - return finalA > finalB; - else return finalA < finalB; + else + return finalA > finalB; } - int typeA = RecTypePriority(a.prog->recType); - int typeB = RecTypePriority(b.prog->recType); + int typeA = RecTypePriority(a->recType); + int typeB = RecTypePriority(b->recType); if (typeA != typeB) { if (m_reverse) - return typeA < typeB; - else return typeA > typeB; + else + return typeA < typeB; } if (m_reverse) - return (a.prog->GetRecordingRuleID() < - b.prog->GetRecordingRuleID()); + return (a->GetRecordingRuleID() > + b->GetRecordingRuleID()); else - return (a.prog->GetRecordingRuleID() > - b.prog->GetRecordingRuleID()); + return (a->GetRecordingRuleID() < + b->GetRecordingRuleID()); } private: @@ -229,38 +235,39 @@ class ProgramRecTypeSort public: ProgramRecTypeSort(bool reverse) : m_reverse(reverse) {} - bool operator()(const RecPriorityInfo &a, const RecPriorityInfo &b) const + bool operator()(const ProgramRecPriorityInfo *a, + const ProgramRecPriorityInfo *b) const { - int typeA = RecTypePriority(a.prog->recType); - int typeB = RecTypePriority(b.prog->recType); + int typeA = RecTypePriority(a->recType); + int typeB = RecTypePriority(b->recType); if (typeA != typeB) { if (m_reverse) - return (typeA < typeB); - else return (typeA > typeB); + else + return (typeA < typeB); } - int finalA = (a.prog->GetRecordingPriority() + - a.prog->recTypeRecPriority); - int finalB = (b.prog->GetRecordingPriority() + - b.prog->recTypeRecPriority); + int finalA = (a->GetRecordingPriority() + + a->recTypeRecPriority); + int finalB = (b->GetRecordingPriority() + + b->recTypeRecPriority); if (finalA != finalB) { if (m_reverse) - return finalA > finalB; - else return finalA < finalB; + else + return finalA > finalB; } if (m_reverse) - return (a.prog->GetRecordingRuleID() < - b.prog->GetRecordingRuleID()); + return (a->GetRecordingRuleID() > + b->GetRecordingRuleID()); else - return (a.prog->GetRecordingRuleID() > - b.prog->GetRecordingRuleID()); + return (a->GetRecordingRuleID() < + b->GetRecordingRuleID()); } private: @@ -272,30 +279,34 @@ class ProgramCountSort public: ProgramCountSort(bool reverse) : m_reverse(reverse) {} - bool operator()(const RecPriorityInfo &a, const RecPriorityInfo &b) const + bool operator()(const ProgramRecPriorityInfo *a, + const ProgramRecPriorityInfo *b) const { - int countA = a.prog->matchCount; - int countB = b.prog->matchCount; - int recCountA = a.prog->recCount; - int recCountB = b.prog->recCount; + int countA = a->matchCount; + int countB = b->matchCount; + int recCountA = a->recCount; + int recCountB = b->recCount; if (countA != countB) { if (m_reverse) - return countA > countB; - else return countA < countB; + else + return countA > countB; } if (recCountA != recCountB) { if (m_reverse) - return recCountA > recCountB; - else return recCountA < recCountB; + else + return recCountA > recCountB; } - return (a.prog->sortTitle > b.prog->sortTitle); + if (m_reverse) + return (a->sortTitle > b->sortTitle); + else + return (a->sortTitle < b->sortTitle); } private: @@ -307,30 +318,34 @@ class ProgramRecCountSort public: ProgramRecCountSort(bool reverse) : m_reverse(reverse) {} - bool operator()(const RecPriorityInfo &a, const RecPriorityInfo &b) const + bool operator()(const ProgramRecPriorityInfo *a, + const ProgramRecPriorityInfo *b) const { - int countA = a.prog->matchCount; - int countB = b.prog->matchCount; - int recCountA = a.prog->recCount; - int recCountB = b.prog->recCount; + int countA = a->matchCount; + int countB = b->matchCount; + int recCountA = a->recCount; + int recCountB = b->recCount; if (recCountA != recCountB) { if (m_reverse) - return recCountA > recCountB; - else return recCountA < recCountB; + else + return recCountA > recCountB; } if (countA != countB) { if (m_reverse) - return countA > countB; - else return countA < countB; + else + return countA > countB; } - return (a.prog->sortTitle > b.prog->sortTitle); + if (m_reverse) + return (a->sortTitle > b->sortTitle); + else + return (a->sortTitle < b->sortTitle); } private: @@ -342,20 +357,24 @@ class ProgramLastRecordSort public: ProgramLastRecordSort(bool reverse) : m_reverse(reverse) {} - bool operator()(const RecPriorityInfo &a, const RecPriorityInfo &b) const + bool operator()(const ProgramRecPriorityInfo *a, + const ProgramRecPriorityInfo *b) const { - QDateTime lastRecA = a.prog->last_record; - QDateTime lastRecB = b.prog->last_record; + QDateTime lastRecA = a->last_record; + QDateTime lastRecB = b->last_record; if (lastRecA != lastRecB) { if (m_reverse) - return lastRecA > lastRecB; - else return lastRecA < lastRecB; + else + return lastRecA > lastRecB; } - return (a.prog->sortTitle > b.prog->sortTitle); + if (m_reverse) + return (a->sortTitle > b->sortTitle); + else + return (a->sortTitle < b->sortTitle); } private: @@ -367,20 +386,24 @@ class ProgramAvgDelaySort public: ProgramAvgDelaySort(bool reverse) : m_reverse(reverse) {} - bool operator()(const RecPriorityInfo &a, const RecPriorityInfo &b) const + bool operator()(const ProgramRecPriorityInfo *a, + const ProgramRecPriorityInfo *b) const { - int avgA = a.prog->avg_delay; - int avgB = b.prog->avg_delay; + int avgA = a->avg_delay; + int avgB = b->avg_delay; if (avgA != avgB) { if (m_reverse) - return avgA < avgB; - else return avgA > avgB; + else + return avgA < avgB; } - return (a.prog->sortTitle > b.prog->sortTitle); + if (m_reverse) + return (a->sortTitle > b->sortTitle); + else + return (a->sortTitle < b->sortTitle); } private: @@ -636,6 +659,7 @@ void ProgramRecPriority::showMenu(void) menuPopup->AddButton(tr("Upcoming")); menuPopup->AddButton(tr("Custom Edit")); menuPopup->AddButton(tr("Delete Rule")); + menuPopup->AddButton(tr("New Template")); popupStack->AddScreen(menuPopup); } @@ -716,6 +740,19 @@ void ProgramRecPriority::customEvent(QEvent *event) saveRecPriority(); remove(); } + else if (resulttext == tr("New Template")) + { + MythScreenStack *popupStack = + GetMythMainWindow()->GetStack("popup stack"); + MythTextInputDialog *textInput = + new MythTextInputDialog(popupStack, + tr("Template Name")); + if (textInput->Create()) + { + textInput->SetReturnEvent(this, "templatecat"); + popupStack->AddScreen(textInput); + } + } } else if (resultid == "sortmenu") { @@ -824,6 +861,10 @@ void ProgramRecPriority::customEvent(QEvent *event) delete record; } } + else if (resultid == "templatecat") + { + newTemplate(resulttext); + } else ScheduleCommon::customEvent(event); } @@ -851,30 +892,116 @@ void ProgramRecPriority::edit(MythUIButtonListItem *item) { mainStack->AddScreen(schededit); connect(schededit, SIGNAL(ruleSaved(int)), SLOT(scheduleChanged(int))); + connect(schededit, SIGNAL(ruleDeleted(int)), SLOT(scheduleChanged(int))); } else delete schededit; +} + +void ProgramRecPriority::newTemplate(QString category) +{ + category = category.trimmed(); + if (category.isEmpty()) + return; + + // Try to find an existing template and use it. + QMap::Iterator it; + for (it = m_programData.begin(); it != m_programData.end(); ++it) + { + ProgramRecPriorityInfo *progInfo = &(*it); + if (progInfo->GetRecordingRuleType() == kTemplateRecord && + category.compare(progInfo->GetCategory(), + Qt::CaseInsensitive) == 0) + { + m_programList->SetValueByData(qVariantFromValue(progInfo)); + edit(m_programList->GetItemCurrent()); + return; + } + } + RecordingRule *record = new RecordingRule(); + if (!record) + return; + record->MakeTemplate(category); + + MythScreenStack *mainStack = GetMythMainWindow()->GetMainStack(); + ScheduleEditor *schededit = new ScheduleEditor(mainStack, record); + if (schededit->Create()) + { + mainStack->AddScreen(schededit); + connect(schededit, SIGNAL(ruleSaved(int)), SLOT(scheduleChanged(int))); + connect(schededit, SIGNAL(ruleDeleted(int)), SLOT(scheduleChanged(int))); + } + else + delete schededit; } void ProgramRecPriority::scheduleChanged(int recid) { + int rtRecPriors[12]; + rtRecPriors[kNotRecording] = 0; + rtRecPriors[kSingleRecord] = + gCoreContext->GetNumSetting("SingleRecordRecPriority", 1); + rtRecPriors[kTimeslotRecord] = + gCoreContext->GetNumSetting("TimeslotRecordRecPriority", 0); + rtRecPriors[kChannelRecord] = + gCoreContext->GetNumSetting("ChannelRecordRecPriority", 0); + rtRecPriors[kAllRecord] = + gCoreContext->GetNumSetting("AllRecordRecPriority", 0); + rtRecPriors[kWeekslotRecord] = + gCoreContext->GetNumSetting("WeekslotRecordRecPriority", 0); + rtRecPriors[kFindOneRecord] = + gCoreContext->GetNumSetting("FindOneRecordRecPriority", -1); + rtRecPriors[kOverrideRecord] = + gCoreContext->GetNumSetting("OverrideRecordRecPriority", 0); + rtRecPriors[kDontRecord] = + gCoreContext->GetNumSetting("OverrideRecordRecPriority", 0); + rtRecPriors[kFindDailyRecord] = + gCoreContext->GetNumSetting("FindOneRecordRecPriority", -1); + rtRecPriors[kFindWeeklyRecord] = + gCoreContext->GetNumSetting("FindOneRecordRecPriority", -1); + rtRecPriors[kTemplateRecord] = 0; + // Assumes that the current item didn't change, which isn't guaranteed MythUIButtonListItem *item = m_programList->GetItemCurrent(); - ProgramRecPriorityInfo *pgRecInfo = - qVariantValue(item->GetData()); + ProgramRecPriorityInfo *pgRecInfo = NULL; + if (item) + pgRecInfo = qVariantValue(item->GetData()); + + // If the recording id doesn't match, the user created a new + // template. + if (!pgRecInfo || recid != pgRecInfo->getRecordID()) + { + RecordingRule record; + record.m_recordID = recid; + if (!record.Load() || record.m_type == kNotRecording) + return; + + ProgramRecPriorityInfo progInfo; + progInfo.SetRecordingRuleID(record.m_recordID); + progInfo.SetRecordingRuleType(record.m_type); + progInfo.SetTitle(record.m_title); + progInfo.SetCategory(record.m_category); + progInfo.SetRecordingPriority(record.m_recPriority); + progInfo.recType = record.m_type; + progInfo.sortTitle = record.m_title; + progInfo.recTypeRecPriority = rtRecPriors[progInfo.recType]; + progInfo.recstatus = record.m_isInactive ? + rsInactive : rsUnknown; + progInfo.profile = record.m_recProfile; + progInfo.last_record = record.m_lastRecorded; + + m_programData[recid] = progInfo; + m_origRecPriorityData[record.m_recordID] = + record.m_recPriority; + SortList(&m_programData[recid]); - if (!pgRecInfo) return; + } // We need to refetch the recording priority values since the Advanced // Recording Options page could've been used to change them - // Only time the recording id would not match is if this wasn't the same - // item we started editing earlier. - if (recid != pgRecInfo->getRecordID()) - return; - MSqlQuery query(MSqlQuery::InitCon()); query.prepare("SELECT recpriority, type, inactive " "FROM record " @@ -890,29 +1017,6 @@ void ProgramRecPriority::scheduleChanged(int recid) int rectype = query.value(1).toInt(); int inactive = query.value(2).toInt(); - int rtRecPriors[11]; - rtRecPriors[0] = 0; - rtRecPriors[kSingleRecord] = - gCoreContext->GetNumSetting("SingleRecordRecPriority", 1); - rtRecPriors[kTimeslotRecord] = - gCoreContext->GetNumSetting("TimeslotRecordRecPriority", 0); - rtRecPriors[kChannelRecord] = - gCoreContext->GetNumSetting("ChannelRecordRecPriority", 0); - rtRecPriors[kAllRecord] = - gCoreContext->GetNumSetting("AllRecordRecPriority", 0); - rtRecPriors[kWeekslotRecord] = - gCoreContext->GetNumSetting("WeekslotRecordRecPriority", 0); - rtRecPriors[kFindOneRecord] = - gCoreContext->GetNumSetting("FindOneRecordRecPriority", -1); - rtRecPriors[kOverrideRecord] = - gCoreContext->GetNumSetting("OverrideRecordRecPriority", 0); - rtRecPriors[kDontRecord] = - gCoreContext->GetNumSetting("OverrideRecordRecPriority", 0); - rtRecPriors[kFindDailyRecord] = - gCoreContext->GetNumSetting("FindOneRecordRecPriority", -1); - rtRecPriors[kFindWeeklyRecord] = - gCoreContext->GetNumSetting("FindOneRecordRecPriority", -1); - // set the recording priorities of that program pgRecInfo->SetRecordingPriority(recPriority); pgRecInfo->recType = (RecordingType)rectype; @@ -955,8 +1059,13 @@ void ProgramRecPriority::remove(void) ProgramRecPriorityInfo *pgRecInfo = qVariantValue(item->GetData()); - if (!pgRecInfo) + if (!pgRecInfo || + (pgRecInfo->recType == kTemplateRecord && + pgRecInfo->GetCategory() + .compare("Default", Qt::CaseInsensitive) == 0)) + { return; + } RecordingRule *record = new RecordingRule(); record->m_recordID = pgRecInfo->GetRecordingRuleID(); @@ -1145,7 +1254,7 @@ void ProgramRecPriority::changeRecPriority(int howMuch) void ProgramRecPriority::saveRecPriority(void) { - QMap::Iterator it; + QMap::Iterator it; for (it = m_programData.begin(); it != m_programData.end(); ++it) { @@ -1162,21 +1271,22 @@ void ProgramRecPriority::saveRecPriority(void) void ProgramRecPriority::FillList(void) { - int cnt = 999, rtRecPriors[11]; + int rtRecPriors[12]; vector recordinglist; int autopriority = gCoreContext->GetNumSetting("AutoRecPriority", 0); m_programData.clear(); - m_sortedProgram.clear(); RemoteGetAllScheduledRecordings(recordinglist); - vector::reverse_iterator pgiter = recordinglist.rbegin(); + vector::iterator pgiter = recordinglist.begin(); - for (; pgiter != recordinglist.rend(); ++pgiter) + for (; pgiter != recordinglist.end(); ++pgiter) { - m_programData[QString::number(cnt)] = *(*pgiter); + ProgramInfo *progInfo = *pgiter; + m_programData[(*pgiter)->GetRecordingRuleID()] = + (*progInfo); // save recording priority value in map so we don't have to // save all program's recording priority values when we exit @@ -1184,11 +1294,10 @@ void ProgramRecPriority::FillList(void) (*pgiter)->GetRecordingPriority(); delete (*pgiter); - cnt--; } // get all the recording type recording priority values - rtRecPriors[0] = 0; + rtRecPriors[kNotRecording] = 0; rtRecPriors[kSingleRecord] = gCoreContext->GetNumSetting("SingleRecordRecPriority", 1); rtRecPriors[kTimeslotRecord] = @@ -1209,6 +1318,7 @@ void ProgramRecPriority::FillList(void) gCoreContext->GetNumSetting("FindOneRecordRecPriority", -1); rtRecPriors[kFindWeeklyRecord] = gCoreContext->GetNumSetting("FindOneRecordRecPriority", -1); + rtRecPriors[kTemplateRecord] = 0; // get recording types associated with each program from db // (hope this is ok to do here, it's so much lighter doing @@ -1241,46 +1351,42 @@ void ProgramRecPriority::FillList(void) // find matching program in m_programData and set // recTypeRecPriority and recType - QMap::Iterator it; - for (it = m_programData.begin(); it != m_programData.end(); ++it) + QMap::Iterator it; + it = m_programData.find(recordid); + if (it != m_programData.end()) { ProgramRecPriorityInfo *progInfo = &(*it); - if (progInfo->GetRecordingRuleID() == recordid) - { - progInfo->sortTitle = progInfo->title; - progInfo->sortTitle.remove(QRegExp(tr("^(The |A |An )"))); - - progInfo->recTypeRecPriority = recTypeRecPriority; - progInfo->recType = recType; - progInfo->matchCount = - m_listMatch[progInfo->GetRecordingRuleID()]; - progInfo->recCount = - m_recMatch[progInfo->GetRecordingRuleID()]; - progInfo->last_record = lastrec; - progInfo->avg_delay = avgd; - progInfo->profile = profile; - - if (autopriority) - progInfo->autoRecPriority = - autopriority - (progInfo->avg_delay * - (autopriority * 2 + 1) / 200); - else - progInfo->autoRecPriority = 0; - - if (inactive) - progInfo->recstatus = rsInactive; - else if (m_conMatch[progInfo->GetRecordingRuleID()] > 0) - progInfo->recstatus = rsConflict; - else if (m_nowMatch[progInfo->GetRecordingRuleID()] > 0) - progInfo->recstatus = rsRecording; - else if (m_recMatch[progInfo->GetRecordingRuleID()] > 0) - progInfo->recstatus = rsWillRecord; - else - progInfo->recstatus = rsUnknown; - - break; - } + progInfo->sortTitle = progInfo->title; + progInfo->sortTitle.remove(QRegExp(tr("^(The |A |An )"))); + + progInfo->recTypeRecPriority = recTypeRecPriority; + progInfo->recType = recType; + progInfo->matchCount = + m_listMatch[progInfo->GetRecordingRuleID()]; + progInfo->recCount = + m_recMatch[progInfo->GetRecordingRuleID()]; + progInfo->last_record = lastrec; + progInfo->avg_delay = avgd; + progInfo->profile = profile; + + if (autopriority) + progInfo->autoRecPriority = + autopriority - (progInfo->avg_delay * + (autopriority * 2 + 1) / 200); + else + progInfo->autoRecPriority = 0; + + if (inactive) + progInfo->recstatus = rsInactive; + else if (m_conMatch[progInfo->GetRecordingRuleID()] > 0) + progInfo->recstatus = rsConflict; + else if (m_nowMatch[progInfo->GetRecordingRuleID()] > 0) + progInfo->recstatus = rsRecording; + else if (m_recMatch[progInfo->GetRecordingRuleID()] > 0) + progInfo->recstatus = rsWillRecord; + else + progInfo->recstatus = rsUnknown; } } while (result.next()); } @@ -1317,75 +1423,62 @@ void ProgramRecPriority::countMatches() } } -void ProgramRecPriority::SortList() +void ProgramRecPriority::SortList(ProgramRecPriorityInfo *newCurrentItem) { - MythUIButtonListItem *item = m_programList->GetItemCurrent(); + if (newCurrentItem) + m_currentItem = newCurrentItem; + else + { + MythUIButtonListItem *item = m_programList->GetItemCurrent(); + if (item) + m_currentItem = + qVariantValue(item->GetData()); + } - if (item) - m_currentItem = qVariantValue(item->GetData()); - - int i, j; - vector sortingList; - QMap::Iterator pit; - vector::iterator sit; - RecPriorityInfo *recPriorityInfo; - - // copy m_programData into sortingList and make a copy - // of m_programData in pdCopy - for (i = 0, pit = m_programData.begin(); pit != m_programData.end(); - ++pit, ++i) + QMap::Iterator pit; + vector::iterator sit; + + // copy m_programData into m_sortedProgram + m_sortedProgram.clear(); + for (pit = m_programData.begin(); pit != m_programData.end(); ++pit) { ProgramRecPriorityInfo *progInfo = &(*pit); - RecPriorityInfo tmp = {progInfo, i}; - sortingList.push_back(tmp); + m_sortedProgram.push_back(progInfo); } - // sort sortingList + // sort m_sortedProgram switch (m_sortType) { case byTitle : - sort(sortingList.begin(), sortingList.end(), + sort(m_sortedProgram.begin(), m_sortedProgram.end(), TitleSort(m_reverseSort)); break; case byRecPriority : - sort(sortingList.begin(), sortingList.end(), + sort(m_sortedProgram.begin(), m_sortedProgram.end(), ProgramRecPrioritySort(m_reverseSort)); break; case byRecType : - sort(sortingList.begin(), sortingList.end(), + sort(m_sortedProgram.begin(), m_sortedProgram.end(), ProgramRecTypeSort(m_reverseSort)); break; case byCount : - sort(sortingList.begin(), sortingList.end(), + sort(m_sortedProgram.begin(), m_sortedProgram.end(), ProgramCountSort(m_reverseSort)); break; case byRecCount : - sort(sortingList.begin(), sortingList.end(), + sort(m_sortedProgram.begin(), m_sortedProgram.end(), ProgramRecCountSort(m_reverseSort)); break; case byLastRecord : - sort(sortingList.begin(), sortingList.end(), + sort(m_sortedProgram.begin(), m_sortedProgram.end(), ProgramLastRecordSort(m_reverseSort)); break; case byAvgDelay : - sort(sortingList.begin(), sortingList.end(), + sort(m_sortedProgram.begin(), m_sortedProgram.end(), ProgramAvgDelaySort(m_reverseSort)); break; } - m_sortedProgram.clear(); - - // rebuild m_channelData in sortingList order from m_sortedProgram - for (i = 0, sit = sortingList.begin(); sit != sortingList.end(); ++i, ++sit) - { - recPriorityInfo = &(*sit); - - // find recPriorityInfo[i] in m_sortedChannel - for (j = 0, pit = m_programData.begin(); j !=recPriorityInfo->cnt; j++, ++pit); - - m_sortedProgram[QString::number(999-i)] = &(*pit); - } - UpdateList(); } @@ -1397,7 +1490,7 @@ void ProgramRecPriority::UpdateList() m_programList->Reset(); - QMap::Iterator it; + vector::iterator it; MythUIButtonListItem *item; for (it = m_sortedProgram.begin(); it != m_sortedProgram.end(); ++it) { @@ -1426,11 +1519,13 @@ void ProgramRecPriority::UpdateList() QString state; if (progInfo->recType == kDontRecord || - progInfo->recstatus == rsInactive) + (progInfo->recType != kTemplateRecord && + progInfo->recstatus == rsInactive)) state = "disabled"; else if (m_conMatch[progInfo->GetRecordingRuleID()] > 0) state = "error"; - else if (m_recMatch[progInfo->GetRecordingRuleID()] > 0) + else if (m_recMatch[progInfo->GetRecordingRuleID()] > 0 || + progInfo->recType == kTemplateRecord) state = "normal"; else if (m_nowMatch[progInfo->GetRecordingRuleID()] > 0) state = "running"; @@ -1679,16 +1774,11 @@ void ProgramRecPriority::RemoveItemFromList(MythUIButtonListItem *item) if (!pgRecInfo) return; - QMap::iterator it; - for (it = m_programData.begin(); it != m_programData.end(); ++it) - { - ProgramRecPriorityInfo *value = &(it.value()); - if (value == pgRecInfo) - { - m_programData.erase(it); - break; - } - } + QMap::iterator it; + it = m_programData.find(pgRecInfo->GetRecordingRuleID()); + if (it != m_programData.end()) + m_programData.erase(it); + m_programList->RemoveItem(item); } diff --git a/mythtv/programs/mythfrontend/programrecpriority.h b/mythtv/programs/mythfrontend/programrecpriority.h index 24e6a10ff1e..e3e0ea35d4a 100644 --- a/mythtv/programs/mythfrontend/programrecpriority.h +++ b/mythtv/programs/mythfrontend/programrecpriority.h @@ -1,6 +1,8 @@ #ifndef PROGRAMRECPROIRITY_H_ #define PROGRAMRECPROIRITY_H_ +#include + #include "recordinginfo.h" #include "mythscreentype.h" @@ -15,6 +17,8 @@ class MythUIText; class MythUIStateType; class ProgramRecPriority; +class RecordingRule; + class ProgramRecPriorityInfo : public RecordingInfo { friend class ProgramRecPriority; @@ -30,6 +34,10 @@ class ProgramRecPriorityInfo : public RecordingInfo virtual ProgramRecPriorityInfo &clone(const ProgramInfo &other); virtual void clear(void); + virtual void ToMap(QHash &progMap, + bool showrerecord = false, + uint star_range = 10) const; + int recTypeRecPriority; RecordingType recType; int matchCount; @@ -72,13 +80,14 @@ class ProgramRecPriority : public ScheduleCommon virtual void Init(void); void FillList(void); - void SortList(void); + void SortList(ProgramRecPriorityInfo *newCurrentItem = NULL); void UpdateList(); void RemoveItemFromList(MythUIButtonListItem *item); void changeRecPriority(int howMuch); void saveRecPriority(void); void customEdit(); + void newTemplate(QString category); void remove(); void deactivate(); void upcoming(); @@ -87,8 +96,8 @@ class ProgramRecPriority : public ScheduleCommon void showMenu(void); void showSortMenu(void); - QMap m_programData; - QMap m_sortedProgram; + QMap m_programData; + vector m_sortedProgram; QMap m_origRecPriorityData; void countMatches(void); diff --git a/mythtv/programs/mythfrontend/scheduleeditor.cpp b/mythtv/programs/mythfrontend/scheduleeditor.cpp index 877371a0d79..ad55ec4f298 100644 --- a/mythtv/programs/mythfrontend/scheduleeditor.cpp +++ b/mythtv/programs/mythfrontend/scheduleeditor.cpp @@ -154,6 +154,10 @@ bool ScheduleEditor::Create() connect(m_cancelButton, SIGNAL(Clicked()), SLOT(Close())); connect(m_saveButton, SIGNAL(Clicked()), SLOT(Save())); + m_schedInfoButton->SetEnabled(!m_recordingRule->m_isTemplate); + m_previewButton->SetEnabled(!m_recordingRule->m_isTemplate); + m_metadataButton->SetEnabled(!m_recordingRule->m_isTemplate); + BuildFocusList(); if (!m_recordingRule->IsLoaded()) @@ -193,7 +197,20 @@ void ScheduleEditor::Load() RecordingType type = m_recordingRule->m_type; // Rules List - if (m_recordingRule->m_isOverride) + if (m_recordingRule->m_isTemplate) + { + if (m_recordingRule->m_category + .compare("Default", Qt::CaseInsensitive) != 0) + { + new MythUIButtonListItem(m_rulesList, + tr("Delete this recording rule template"), + ENUM_TO_QVARIANT(kNotRecording)); + } + new MythUIButtonListItem(m_rulesList, + tr("Modify this recording rule template"), + ENUM_TO_QVARIANT(kTemplateRecord)); + } + else if (m_recordingRule->m_isOverride) { new MythUIButtonListItem(m_rulesList, tr("Record this showing with normal options"), @@ -258,6 +275,17 @@ void ScheduleEditor::Load() SetTextFromMap(progMap); } +void ScheduleEditor::LoadTemplate(QString name) +{ + m_recordingRule->LoadTemplate(name); + + InfoMap progMap; + m_recordingRule->ToMap(progMap); + if (m_recInfo) + m_recInfo->ToMap(progMap); + SetTextFromMap(progMap); +} + void ScheduleEditor::RuleChanged(MythUIButtonListItem *item) { if (!item) @@ -282,7 +310,10 @@ void ScheduleEditor::Save() RecordingType type = static_cast(item->GetData().toInt()); if (type == kNotRecording) { + int recid = m_recordingRule->m_recordID; DeleteRule(); + if (recid) + emit ruleDeleted(recid); Close(); return; } @@ -355,6 +386,33 @@ void ScheduleEditor::ShowSchedInfo() delete menuPopup; } +bool ScheduleEditor::keyPressEvent(QKeyEvent *event) +{ + if (GetFocusWidget()->keyPressEvent(event)) + return true; + + bool handled = false; + QStringList actions; + handled = GetMythMainWindow()-> + TranslateKeyPress("TV Frontend", event, actions); + + for (int i = 0; i < actions.size() && !handled; i++) + { + QString action = actions[i]; + handled = true; + + if (action == "MENU") + showMenu(); + else + handled = false; + } + + if (!handled && MythScreenType::keyPressEvent(event)) + handled = true; + + return handled; +} + void ScheduleEditor::customEvent(QEvent *event) { if (event->type() == DialogCompletionEvent::kEventType) @@ -362,9 +420,21 @@ void ScheduleEditor::customEvent(QEvent *event) DialogCompletionEvent *dce = (DialogCompletionEvent*)(event); QString resultid = dce->GetId(); + QString resulttext = dce->GetResultText(); int buttonnum = dce->GetResult(); - if (resultid == "schedinfo") + if (resultid == "menu") + { + if (resulttext == tr("Use Template")) + { + showTemplateMenu(); + } + } + else if (resultid == "templatemenu") + { + LoadTemplate(resulttext); + } + else if (resultid == "schedinfo") { switch (buttonnum) { @@ -458,6 +528,59 @@ void ScheduleEditor::ShowMetadataOptions(void) delete rad; } +void ScheduleEditor::showMenu(void) +{ + QString label = tr("Options"); + MythScreenStack *popupStack = GetMythMainWindow()->GetStack("popup stack"); + MythDialogBox *menuPopup = + new MythDialogBox(label, popupStack, "menuPopup"); + + if (menuPopup->Create()) + { + menuPopup->SetReturnEvent(this, "menu"); + menuPopup->AddButton(tr("Use Template")); + popupStack->AddScreen(menuPopup); + } + else + { + delete menuPopup; + } +} + +void ScheduleEditor::showTemplateMenu(void) +{ + QStringList templates = RecordingRule::GetTemplateNames(); + if (templates.empty()) + { + ShowOkPopup(tr("No templates available")); + return; + } + + QString label = tr("Template Options"); + MythScreenStack *popupStack = GetMythMainWindow()->GetStack("popup stack"); + MythDialogBox *menuPopup = + new MythDialogBox(label, popupStack, "menuPopup"); + + if (menuPopup->Create()) + { + menuPopup->SetReturnEvent(this, "templatemenu"); + while (!templates.empty()) + { + QString name = templates.front(); + if (name == "Default") + menuPopup->AddButton(tr("Default")); + else + menuPopup->AddButton(name); + templates.pop_front(); + } + popupStack->AddScreen(menuPopup); + } + else + { + delete menuPopup; + } +} + //////////////////////////////////////////////////////// /** \class SchedOptEditor diff --git a/mythtv/programs/mythfrontend/scheduleeditor.h b/mythtv/programs/mythfrontend/scheduleeditor.h index f9dfeb01120..23dec93f9d7 100644 --- a/mythtv/programs/mythfrontend/scheduleeditor.h +++ b/mythtv/programs/mythfrontend/scheduleeditor.h @@ -37,6 +37,7 @@ class ScheduleEditor : public ScheduleCommon ~ScheduleEditor(); bool Create(void); + bool keyPressEvent(QKeyEvent *event); void customEvent(QEvent *event); /// Callback @@ -44,6 +45,7 @@ class ScheduleEditor : public ScheduleCommon signals: void ruleSaved(int ruleId); + void ruleDeleted(int ruleId); protected slots: void RuleChanged(MythUIButtonListItem *item); @@ -58,12 +60,16 @@ class ScheduleEditor : public ScheduleCommon private: void Load(void); + void LoadTemplate(QString name); void DeleteRule(void); void showPrevious(void); void showUpcomingByRule(void); void showUpcomingByTitle(void); + void showMenu(void); + void showTemplateMenu(void); + RecordingInfo *m_recInfo; RecordingRule *m_recordingRule; diff --git a/mythtv/themes/MythCenter-wide/base.xml b/mythtv/themes/MythCenter-wide/base.xml index 106e8edda21..dee85c3df31 100644 --- a/mythtv/themes/MythCenter-wide/base.xml +++ b/mythtv/themes/MythCenter-wide/base.xml @@ -669,8 +669,14 @@ 26,246,440,28 + + From bf8d4290625a55af84d915e3edcba4e76e04cd83 Mon Sep 17 00:00:00 2001 From: Jim Stichnoth Date: Thu, 3 May 2012 21:41:09 -0700 Subject: [PATCH 58/69] Subtitles: Call Pulse() on the SubtitleScreen children. --- mythtv/libs/libmythtv/subtitlescreen.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/mythtv/libs/libmythtv/subtitlescreen.cpp b/mythtv/libs/libmythtv/subtitlescreen.cpp index f3c8914d22d..ca7589cb01a 100644 --- a/mythtv/libs/libmythtv/subtitlescreen.cpp +++ b/mythtv/libs/libmythtv/subtitlescreen.cpp @@ -632,6 +632,7 @@ void SubtitleScreen::Pulse(void) DisplayRawTextSubtitles(); OptimiseDisplayedArea(); + MythScreenType::Pulse(); m_refreshArea = false; } From fe11b61ed74e7c128cb7f59e88596e842ef64d75 Mon Sep 17 00:00:00 2001 From: Jean-Yves Avenard Date: Fri, 4 May 2012 23:42:16 +1000 Subject: [PATCH 59/69] Various fixes in Airplay Fixes: -Playback on frontend always start at the beginning, iOS continuing to play where left off. -Scrubbing not working -Playback position on frontend and iOS device not in sync -When quitting playback on iOS, video continue on frontend. -When quitting playback on frontend, iOS not notified and playback counter continues progressing There's still a minor cosmetic defect, when scrubbing quickly and the iOS device pause playback, the positions between the frontend and iOS device are not in sync. It will become in sync only once playback resume. MythTV doesn't handle http live streaming playback yet, so Airplay will not work with some applications (in particular AirVideo) streaming via HLS --- mythtv/libs/libmythtv/mythairplayserver.cpp | 77 +++++++++++++-------- mythtv/libs/libmythtv/mythairplayserver.h | 8 ++- 2 files changed, 55 insertions(+), 30 deletions(-) diff --git a/mythtv/libs/libmythtv/mythairplayserver.cpp b/mythtv/libs/libmythtv/mythairplayserver.cpp index 89cb5d2f3f2..53ef014b8d1 100644 --- a/mythtv/libs/libmythtv/mythairplayserver.cpp +++ b/mythtv/libs/libmythtv/mythairplayserver.cpp @@ -2,7 +2,6 @@ // locking ? // race on startup? // http date format and locale -// GET scrub on iOS 5 #include #include @@ -511,6 +510,46 @@ void MythAirplayServer::HandleResponse(APHTTPRequest *req, m_connections[session].controlSocket = socket; } + double position = 0.0f; + double duration = 0.0f; + float playerspeed = 0.0f; + bool playing = false; + GetPlayerStatus(playing, playerspeed, position, duration); + // set initial position if it was set at start of playback. + if (duration > 0.01f && playing) + { + if (m_connections[session].initial_position > 0.0f) + { + position = duration * m_connections[session].initial_position; + MythEvent* me = new MythEvent(ACTION_SEEKABSOLUTE, + QStringList(QString::number((uint64_t)position))); + qApp->postEvent(GetMythMainWindow(), me); + m_connections[session].position = position; + m_connections[session].initial_position = -1.0f; + if(!m_connections[session].was_playing) + { + SendReverseEvent(session, AP_EVENT_PLAYING); + } + } + else if (position < .01f) + { + // Assume playback hasn't started yet, get saved position + position = m_connections[session].position; + } + } + if (!playing && m_connections[session].was_playing) + { + // playback got interrupted, notify client to stop + if (SendReverseEvent(session, AP_EVENT_STOPPED)) + { + m_connections[session].was_playing = false; + } + } + else + { + m_connections[session].was_playing = playing; + } + if (req->GetURI() == "/server-info") { content_type = "text/x-apple-plist+xml\r\n"; @@ -533,11 +572,7 @@ void MythAirplayServer::HandleResponse(APHTTPRequest *req, } else if (req->GetMethod() == "GET") { - double position = 0.0f; - double duration = 0.0f; - float playerspeed = 0.0f; - bool playing = false; - GetPlayerStatus(playing, playerspeed, position, duration); + content_type = "text/parameters\r\n"; body = QString("duration: %1\r\nposition: %2\r\n") .arg((double)duration, 0, 'f', 6, '0') .arg((double)position, 0, 'f', 6, '0'); @@ -560,9 +595,8 @@ void MythAirplayServer::HandleResponse(APHTTPRequest *req, else if (req->GetURI() == "/stop") { m_connections[session].stopped = true; - // FIXME unpause playback and it will exit when the remote socket closes QKeyEvent* ke = new QKeyEvent(QEvent::KeyPress, 0, - Qt::NoModifier, ACTION_PLAY); + Qt::NoModifier, ACTION_STOP); qApp->postEvent(GetMythMainWindow(), (QEvent*)ke); } else if (req->GetURI() == "/photo" || @@ -576,11 +610,6 @@ void MythAirplayServer::HandleResponse(APHTTPRequest *req, } else if (req->GetURI() == "/rate") { - double dummy1 = 0.0f; - double dummy2 = 0.0f; - float playerspeed = 0.0f; - bool playing = false; - GetPlayerStatus(playing, playerspeed, dummy1, dummy2); float rate = req->GetQueryValue("value").toFloat(); m_connections[session].speed = rate; @@ -634,7 +663,8 @@ void MythAirplayServer::HandleResponse(APHTTPRequest *req, if (!file.isEmpty()) { m_connections[session].url = QUrl(file); - m_connections[session].position = start_pos; + m_connections[session].position = 0.0f; + m_connections[session].initial_position = start_pos; MythEvent* me = new MythEvent(ACTION_HANDLEMEDIA, QStringList(file)); @@ -647,7 +677,7 @@ void MythAirplayServer::HandleResponse(APHTTPRequest *req, "Ignoring playback - something else is playing."); } - SendReverseEvent(session, AP_EVENT_PLAYING); + SendReverseEvent(session, AP_EVENT_LOADING); LOG(VB_GENERAL, LOG_INFO, LOC + QString("File: '%1' start_pos '%2'") .arg(file.data()).arg(start_pos)); } @@ -655,12 +685,6 @@ void MythAirplayServer::HandleResponse(APHTTPRequest *req, { content_type = "text/x-apple-plist+xml\r\n"; - double position = 0.0f; - double duration = 0.0f; - float playerspeed = 0.0f; - bool playing = false; - GetPlayerStatus(playing, playerspeed, position, duration); - if (!playing) { body = NOT_READY; @@ -714,15 +738,15 @@ void MythAirplayServer::HandleResponse(APHTTPRequest *req, .arg(socket->flush()).arg(reply.data())); } -void MythAirplayServer::SendReverseEvent(QByteArray &session, +bool MythAirplayServer::SendReverseEvent(QByteArray &session, AirplayEvent event) { if (!m_connections.contains(session)) - return; + return false; if (m_connections[session].lastEvent == event) - return; + return false; if (!m_connections[session].reverseSocket) - return; + return false; QString body; if (AP_EVENT_PLAYING == event || @@ -755,6 +779,7 @@ void MythAirplayServer::SendReverseEvent(QByteArray &session, LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("Send reverse: %1 \n\n%2\n") .arg(m_connections[session].reverseSocket->flush()) .arg(reply.data())); + return true; } QString MythAirplayServer::eventToString(AirplayEvent event) @@ -797,7 +822,5 @@ QString MythAirplayServer::GetMacAddress() res.append(':'); } } - QByteArray ba = res.toAscii(); - const char *t = ba.constData(); return res; } diff --git a/mythtv/libs/libmythtv/mythairplayserver.h b/mythtv/libs/libmythtv/mythairplayserver.h index e793c6b769a..e384be693fe 100644 --- a/mythtv/libs/libmythtv/mythairplayserver.h +++ b/mythtv/libs/libmythtv/mythairplayserver.h @@ -27,16 +27,18 @@ class AirplayConnection public: AirplayConnection() : controlSocket(NULL), reverseSocket(NULL), speed(1.0f), - position(0.0f), url(QUrl()), lastEvent(AP_EVENT_NONE), - stopped(false) + position(0.0f), initial_position(-1.0f), url(QUrl()), + lastEvent(AP_EVENT_NONE), stopped(false), was_playing(false) { } QTcpSocket *controlSocket; QTcpSocket *reverseSocket; float speed; double position; + double initial_position; QUrl url; AirplayEvent lastEvent; bool stopped; + bool was_playing; }; class APHTTPRequest; @@ -66,7 +68,7 @@ class MTV_PUBLIC MythAirplayServer : public ServerPool void GetPlayerStatus(bool &playing, float &speed, double &position, double &duration); QString GetMacAddress(); - void SendReverseEvent(QByteArray &session, AirplayEvent event); + bool SendReverseEvent(QByteArray &session, AirplayEvent event); // Globals static MythAirplayServer *gMythAirplayServer; From 3fb9d6eb779e08d91df8a849a5be38e219c34bed Mon Sep 17 00:00:00 2001 From: Daniel Kristjansson Date: Fri, 3 Feb 2012 10:48:29 -0500 Subject: [PATCH 60/69] Fixes #10305. Remove mysql.txt support. --- mythtv/bindings/perl/MythTV.pm | 6 +- mythtv/bindings/php/MythBackend.php | 4 +- mythtv/bindings/python/MythTV/static.py | 4 +- mythtv/docs/doxygen-architecture-document.cpp | 2 +- mythtv/libs/libmyth/backendselect.h | 8 +- mythtv/libs/libmyth/langsettings.cpp | 2 - mythtv/libs/libmyth/mythcontext.cpp | 205 ++++++------ mythtv/libs/libmythbase/libmythbase.pro | 2 +- mythtv/libs/libmythbase/mythcorecontext.cpp | 16 +- mythtv/libs/libmythbase/mythcorecontext.h | 3 +- mythtv/libs/libmythbase/mythdb.cpp | 297 ++++-------------- mythtv/libs/libmythbase/mythdb.h | 14 +- mythtv/libs/libmythbase/mythdbparams.cpp | 78 +++++ mythtv/libs/libmythbase/mythdbparams.h | 16 +- mythtv/libs/libmythbase/mythlocale.cpp | 6 - mythtv/libs/libmythbase/mythtranslation.cpp | 1 - mythtv/libs/libmythbase/mythversion.h | 6 +- .../libs/libmythmetadata/parentalcontrols.cpp | 1 - mythtv/libs/libmythui/myththemedmenu.cpp | 2 - mythtv/libs/libmythupnp/configuration.cpp | 74 ++++- mythtv/libs/libmythupnp/configuration.h | 6 +- mythtv/programs/mythbackend/mythsettings.cpp | 49 +-- mythtv/programs/mythbackend/mythsettings.h | 1 - mythtv/programs/mythtv-setup/main.cpp | 2 +- mythtv/programs/mythtv-setup/startprompt.cpp | 4 +- 25 files changed, 376 insertions(+), 433 deletions(-) create mode 100644 mythtv/libs/libmythbase/mythdbparams.cpp diff --git a/mythtv/bindings/perl/MythTV.pm b/mythtv/bindings/perl/MythTV.pm index 1076d85a526..73d208c5196 100644 --- a/mythtv/bindings/perl/MythTV.pm +++ b/mythtv/bindings/perl/MythTV.pm @@ -8,7 +8,7 @@ # # Version - $VERSION = '.25svn'; + $VERSION = '.26git'; # Load sub libraries use IO::Socket::INET::MythTV; @@ -106,8 +106,8 @@ package MythTV; # Note: as of July 21, 2010, this is actually a string, to account for proto # versions of the form "58a". This will get used if protocol versions are # changed on a fixes branch ongoing. - our $PROTO_VERSION = "73"; - our $PROTO_TOKEN = "D7FE8D6F"; + our $PROTO_VERSION = "74"; + our $PROTO_TOKEN = "SingingPotato"; # currentDatabaseVersion is defined in libmythtv in # mythtv/libs/libmythtv/dbcheck.cpp and should be the current MythTV core diff --git a/mythtv/bindings/php/MythBackend.php b/mythtv/bindings/php/MythBackend.php index 9b43d4ec6b7..86f416b31b1 100644 --- a/mythtv/bindings/php/MythBackend.php +++ b/mythtv/bindings/php/MythBackend.php @@ -11,8 +11,8 @@ class MythBackend { // MYTH_PROTO_VERSION is defined in libmyth in mythtv/libs/libmyth/mythcontext.h // and should be the current MythTV protocol version. - static $protocol_version = '73'; - static $protocol_token = 'D7FE8D6F'; + static $protocol_version = '74'; + static $protocol_token = 'SingingPotato'; // The character string used by the backend to separate records static $backend_separator = '[]:[]'; diff --git a/mythtv/bindings/python/MythTV/static.py b/mythtv/bindings/python/MythTV/static.py index 8b8abf6fa3b..a548c7f580d 100644 --- a/mythtv/bindings/python/MythTV/static.py +++ b/mythtv/bindings/python/MythTV/static.py @@ -8,8 +8,8 @@ SCHEMA_VERSION = 1302 NVSCHEMA_VERSION = 1007 MUSICSCHEMA_VERSION = 1018 -PROTO_VERSION = '73' -PROTO_TOKEN = 'D7FE8D6F' +PROTO_VERSION = '74' +PROTO_TOKEN = 'SingingPotato' BACKEND_SEP = '[]:[]' INSTALL_PREFIX = '/usr/local' diff --git a/mythtv/docs/doxygen-architecture-document.cpp b/mythtv/docs/doxygen-architecture-document.cpp index 464a5f47a8f..8b8e90fa47d 100644 --- a/mythtv/docs/doxygen-architecture-document.cpp +++ b/mythtv/docs/doxygen-architecture-document.cpp @@ -576,7 +576,7 @@ Most MythTV programs follow a common sequence:
  • Initialise the MythContext, which:
    • Tries to find a database on localhost, - or on the host specified in mysql.txt,
    • + or on the host specified in config.xml,
    • Tries to locate exactly one backend host via UPnP, to find its database,
    • If possible, displays a list of all backends located via UPnP diff --git a/mythtv/libs/libmyth/backendselect.h b/mythtv/libs/libmyth/backendselect.h index 560622c605e..c96fdb9c3dd 100644 --- a/mythtv/libs/libmyth/backendselect.h +++ b/mythtv/libs/libmyth/backendselect.h @@ -19,9 +19,11 @@ struct DatabaseParams; // location at this moment in time // Some common UPnP search and XML value strings const QString gBackendURI = "urn:schemas-mythtv-org:device:MasterMediaServer:1"; -const QString kDefaultBE = "UPnP/MythFrontend/DefaultBackend/"; -const QString kDefaultPIN = kDefaultBE + "SecurityPin"; -const QString kDefaultUSN = kDefaultBE + "USN"; +const QString kDefaultDB = "Database/"; +const QString kDefaultWOL = "WakeOnLAN/"; +const QString kDefaultMFE = "UPnP/MythFrontend/DefaultBackend/"; +const QString kDefaultPIN = kDefaultMFE + "SecurityPin"; +const QString kDefaultUSN = kDefaultMFE + "USN"; typedef QMap ItemMap; diff --git a/mythtv/libs/libmyth/langsettings.cpp b/mythtv/libs/libmyth/langsettings.cpp index 5cbe870ef1f..70ccd7fe354 100644 --- a/mythtv/libs/libmyth/langsettings.cpp +++ b/mythtv/libs/libmyth/langsettings.cpp @@ -187,13 +187,11 @@ void LanguageSelection::Save(void) MythUIButtonListItem *item = m_languageList->GetItemCurrent(); QString langCode = item->GetData().toString(); - gCoreContext->SetSetting("Language", langCode); gCoreContext->SaveSetting("Language", langCode); item = m_countryList->GetItemCurrent(); QString countryCode = item->GetData().toString(); - gCoreContext->SetSetting("Country", countryCode); gCoreContext->SaveSetting("Country", countryCode); if (m_language != langCode) diff --git a/mythtv/libs/libmyth/mythcontext.cpp b/mythtv/libs/libmyth/mythcontext.cpp index 58168eb4393..30706a9bb1c 100644 --- a/mythtv/libs/libmyth/mythcontext.cpp +++ b/mythtv/libs/libmyth/mythcontext.cpp @@ -14,7 +14,6 @@ using namespace std; #include "config.h" #include "mythcontext.h" #include "exitcodes.h" -#include "oldsettings.h" #include "mythmiscutil.h" #include "remotefile.h" #include "mythplugin.h" @@ -66,6 +65,7 @@ class MythContextPrivate : public QObject void EndTempWindow(void); bool LoadDatabaseSettings(void); + bool SaveDatabaseParams(const DatabaseParams ¶ms, bool force); bool PromptForDatabaseParams(const QString &error); QString TestDBconnection(void); @@ -75,7 +75,6 @@ class MythContextPrivate : public QObject int ChooseBackend(const QString &error); int UPnPautoconf(const int milliSeconds = 2000); - void StoreConnectionInfo(void); bool DefaultUPnP(QString &error); bool UPnPconnect(const DeviceLocation *device, const QString &PIN); @@ -217,6 +216,8 @@ MythContextPrivate::MythContextPrivate(MythContext *lparent) MythContextPrivate::~MythContextPrivate() { + if (m_pConfig) + delete m_pConfig; if (m_ui) DestroyMythUI(); if (m_sh) @@ -240,10 +241,10 @@ void MythContextPrivate::TempMainWindow(bool languagePrompt) SilenceDBerrors(); - gCoreContext->SetSetting("Theme", DEFAULT_UI_THEME); + gCoreContext->OverrideSettingForSession("Theme", DEFAULT_UI_THEME); #ifdef Q_WS_MACX // Qt 4.4 has window-focus problems - gCoreContext->SetSetting("RunFrontendInWindow", "1"); + gCoreContext->OverrideSettingForSession("RunFrontendInWindow", "1"); #endif GetMythUI()->LoadQtConfig(); @@ -261,6 +262,7 @@ void MythContextPrivate::TempMainWindow(bool languagePrompt) void MythContextPrivate::EndTempWindow(void) { DestroyMythMainWindow(); + gCoreContext->ClearOverrideSettingForSession("Theme"); EnableDBerrors(); } @@ -273,8 +275,7 @@ bool MythContextPrivate::Init(const bool gui, m_gui = gui; // We don't have a database yet, so lets use the config.xml file. - - m_pConfig = UPnp::GetConfiguration(); + m_pConfig = new XmlConfiguration("config.xml"); // Creates screen saver control if we will have a GUI if (gui) @@ -333,11 +334,13 @@ bool MythContextPrivate::FindDatabase(const bool prompt, const bool noPrompt) QString failure; - // 1. Load either mysql.txt, or use sensible "localhost" defaults: + // 1. Either load config.xml or use sensible "localhost" defaults: bool loaded = LoadDatabaseSettings(); + DatabaseParams dbParamsFromFile = m_DBparams; // In addition to the UI chooser, we can also try to autoSelect later, - // but only if we're not doing manualSelect and there was no mysql.txt + // but only if we're not doing manualSelect and there was no + // valid config.xml bool autoSelect = !manualSelect && !loaded; // 2. If the user isn't forcing up the chooser UI, look for a default @@ -424,41 +427,63 @@ bool MythContextPrivate::FindDatabase(const bool prompt, const bool noPrompt) } DBfound: -#if 0 LOG(VB_GENERAL, LOG_DEBUG, "FindDatabase() - Success!"); -#endif - StoreConnectionInfo(); + SaveDatabaseParams(m_DBparams, + !loaded || m_DBparams.forceSave || + dbParamsFromFile != m_DBparams); EnableDBerrors(); ResetDatabase(); return true; NoDBfound: -#if 0 LOG(VB_GENERAL, LOG_DEBUG, "FindDatabase() - failed"); -#endif return false; } -/** Load database and host settings from mysql.txt or config.xml, - * or set some defaults. - * \return true if mysql.txt or config.xml was parsed +/** Load database and host settings from config.xml, or set some defaults. + * \return true if config.xml was parsed */ bool MythContextPrivate::LoadDatabaseSettings(void) { - bool ok = MythDB::LoadDatabaseParamsFromDisk(m_DBparams); - if (!ok) + // try new format first + m_DBparams.LoadDefaults(); + + m_DBparams.localHostName = m_pConfig->GetValue("LocalHostName", ""); + + m_DBparams.dbHostName = m_pConfig->GetValue(kDefaultDB + "Host", ""); + m_DBparams.dbUserName = m_pConfig->GetValue(kDefaultDB + "UserName", ""); + m_DBparams.dbPassword = m_pConfig->GetValue(kDefaultDB + "Password", ""); + m_DBparams.dbName = m_pConfig->GetValue(kDefaultDB + "DatabaseName", ""); + m_DBparams.dbPort = m_pConfig->GetValue(kDefaultDB + "Port", 0); + + m_DBparams.wolEnabled = + m_pConfig->GetValue(kDefaultWOL + "Enabled", false); + m_DBparams.wolReconnect = + m_pConfig->GetValue(kDefaultWOL + "SQLReconnectWaitTime", 0); + m_DBparams.wolRetry = + m_pConfig->GetValue(kDefaultWOL + "SQLConnectRetry", 0); + m_DBparams.wolCommand = + m_pConfig->GetValue(kDefaultWOL + "Command", ""); + + bool ok = m_DBparams.IsValid("config.xml"); + if (!ok) // if new format fails, try legacy format { - XmlConfiguration cfg("config.xml"); - MythDB::LoadDefaultDatabaseParams(m_DBparams); - m_DBparams.dbHostName = cfg.GetValue(kDefaultBE + "DBHostName", ""); - m_DBparams.dbUserName = cfg.GetValue(kDefaultBE + "DBUserName", ""); - m_DBparams.dbPassword = cfg.GetValue(kDefaultBE + "DBPassword", ""); - m_DBparams.dbName = cfg.GetValue(kDefaultBE + "DBName", ""); - m_DBparams.dbPort = cfg.GetValue(kDefaultBE + "DBPort", 0); - ok = MythDB::ValidateDatabaseParams(m_DBparams, "config.xml"); + m_DBparams.LoadDefaults(); + m_DBparams.dbHostName = m_pConfig->GetValue( + kDefaultMFE + "DBHostName", ""); + m_DBparams.dbUserName = m_pConfig->GetValue( + kDefaultMFE + "DBUserName", ""); + m_DBparams.dbPassword = m_pConfig->GetValue( + kDefaultMFE + "DBPassword", ""); + m_DBparams.dbName = m_pConfig->GetValue( + kDefaultMFE + "DBName", ""); + m_DBparams.dbPort = m_pConfig->GetValue( + kDefaultMFE + "DBPort", 0); + m_DBparams.forceSave = true; + ok = m_DBparams.IsValid("config.xml"); } if (!ok) - MythDB::LoadDefaultDatabaseParams(m_DBparams); + m_DBparams.LoadDefaults(); gCoreContext->GetDB()->SetDatabaseParams(m_DBparams); @@ -476,6 +501,10 @@ bool MythContextPrivate::LoadDatabaseSettings(void) hostname = localhostname; LOG(VB_GENERAL, LOG_NOTICE, "Empty LocalHostName."); } + else + { + m_DBparams.localEnabled = true; + } LOG(VB_GENERAL, LOG_INFO, QString("Using localhost value of %1") .arg(hostname)); @@ -484,6 +513,59 @@ bool MythContextPrivate::LoadDatabaseSettings(void) return ok; } +bool MythContextPrivate::SaveDatabaseParams( + const DatabaseParams ¶ms, bool force) +{ + bool ret = true; + + // only rewrite file if it has changed + if (params != m_DBparams || force) + { + m_pConfig->SetValue( + "LocalHostName", params.localHostName); + + m_pConfig->SetValue( + kDefaultDB + "PingHost", params.dbHostPing); + m_pConfig->SetValue( + kDefaultDB + "Host", params.dbHostName); + m_pConfig->SetValue( + kDefaultDB + "UserName", params.dbUserName); + m_pConfig->SetValue( + kDefaultDB + "Password", params.dbPassword); + m_pConfig->SetValue( + kDefaultDB + "DatabaseName", params.dbName); + m_pConfig->SetValue( + kDefaultDB + "Port", params.dbPort); + + m_pConfig->SetValue( + kDefaultWOL + "Enabled", params.wolEnabled); + m_pConfig->SetValue( + kDefaultWOL + "SQLReconnectWaitTime", params.wolReconnect); + m_pConfig->SetValue( + kDefaultWOL + "SQLConnectRetry", params.wolRetry); + m_pConfig->SetValue( + kDefaultWOL + "Command", params.wolCommand); + + // clear out any legacy nodes.. + m_pConfig->ClearValue(kDefaultMFE + "DBHostName"); + m_pConfig->ClearValue(kDefaultMFE + "DBUserName"); + m_pConfig->ClearValue(kDefaultMFE + "DBPassword"); + m_pConfig->ClearValue(kDefaultMFE + "DBName"); + m_pConfig->ClearValue(kDefaultMFE + "DBPort"); + + // actually save the file + m_pConfig->Save(); + + // Save the new settings: + m_DBparams = params; + gCoreContext->GetDB()->SetDatabaseParams(m_DBparams); + + // If database has changed, force its use: + ResetDatabase(); + } + return ret; +} + bool MythContextPrivate::PromptForDatabaseParams(const QString &error) { bool accepted = false; @@ -705,27 +787,6 @@ int MythContextPrivate::ChooseBackend(const QString &error) return 1; } -/** - * Try to store the current location of this backend in config.xml - * - * This is intended as a last resort for future connections - * (e.g. Perl scripts, backend not running). - * - * Note that the Save() may fail (e.g. non writable ~/.mythtv) - */ -void MythContextPrivate::StoreConnectionInfo(void) -{ - if (!m_pConfig) - return; - - m_pConfig->SetValue(kDefaultBE + "DBHostName", m_DBparams.dbHostName); - m_pConfig->SetValue(kDefaultBE + "DBUserName", m_DBparams.dbUserName); - m_pConfig->SetValue(kDefaultBE + "DBPassword", m_DBparams.dbPassword); - m_pConfig->SetValue(kDefaultBE + "DBName", m_DBparams.dbName); - m_pConfig->SetValue(kDefaultBE + "DBPort", m_DBparams.dbPort); - m_pConfig->Save(); -} - /** * If there is only a single UPnP backend, use it. * @@ -802,13 +863,9 @@ int MythContextPrivate::UPnPautoconf(const int milliSeconds) */ bool MythContextPrivate::DefaultUPnP(QString &error) { - Configuration *pConfig = new XmlConfiguration("config.xml"); QString loc = "DefaultUPnP() - "; - QString localHostName = pConfig->GetValue(kDefaultBE + "LocalHostName", ""); - QString PIN = pConfig->GetValue(kDefaultPIN, ""); - QString USN = pConfig->GetValue(kDefaultUSN, ""); - - delete pConfig; + QString PIN = m_pConfig->GetValue(kDefaultPIN, ""); + QString USN = m_pConfig->GetValue(kDefaultUSN, ""); if (USN.isEmpty()) { @@ -856,13 +913,6 @@ bool MythContextPrivate::DefaultUPnP(QString &error) if (UPnPconnect(pDevLoc, PIN)) { - if (localHostName.length()) - { - m_DBparams.localHostName = localHostName; - m_DBparams.localEnabled = true; - gCoreContext->GetDB()->SetDatabaseParams(m_DBparams); - } - pDevLoc->Release(); return true; @@ -1176,38 +1226,7 @@ DatabaseParams MythContext::GetDatabaseParams(void) bool MythContext::SaveDatabaseParams(const DatabaseParams ¶ms) { - bool ret = true; - DatabaseParams cur_params = GetDatabaseParams(); - - // only rewrite file if it has changed - if (params.dbHostName != cur_params.dbHostName || - params.dbHostPing != cur_params.dbHostPing || - params.dbPort != cur_params.dbPort || - params.dbUserName != cur_params.dbUserName || - params.dbPassword != cur_params.dbPassword || - params.dbName != cur_params.dbName || - params.dbType != cur_params.dbType || - params.localEnabled != cur_params.localEnabled || - params.wolEnabled != cur_params.wolEnabled || - (params.localEnabled && - (params.localHostName != cur_params.localHostName)) || - (params.wolEnabled && - (params.wolReconnect != cur_params.wolReconnect || - params.wolRetry != cur_params.wolRetry || - params.wolCommand != cur_params.wolCommand))) - { - ret = MythDB::SaveDatabaseParamsToDisk(params, GetConfDir(), true); - if (ret) - { - // Save the new settings: - d->m_DBparams = params; - gCoreContext->GetDB()->SetDatabaseParams(d->m_DBparams); - - // If database has changed, force its use: - d->ResetDatabase(); - } - } - return ret; + return d->SaveDatabaseParams(params, false); } /* vim: set expandtab tabstop=4 shiftwidth=4: */ diff --git a/mythtv/libs/libmythbase/libmythbase.pro b/mythtv/libs/libmythbase/libmythbase.pro index 5fe8c5198af..6f940d1a1de 100644 --- a/mythtv/libs/libmythbase/libmythbase.pro +++ b/mythtv/libs/libmythbase/libmythbase.pro @@ -28,7 +28,7 @@ HEADERS += plist.h SOURCES += mthread.cpp mthreadpool.cpp SOURCES += mythsocket.cpp mythsocketthread.cpp msocketdevice.cpp -SOURCES += mythdbcon.cpp mythdb.cpp oldsettings.cpp +SOURCES += mythdbcon.cpp mythdb.cpp mythdbparams.cpp oldsettings.cpp SOURCES += mythobservable.cpp mythevent.cpp httpcomms.cpp mcodecs.cpp SOURCES += mythdirs.cpp mythsignalingtimer.cpp SOURCES += lcddevice.cpp mythstorage.cpp remotefile.cpp diff --git a/mythtv/libs/libmythbase/mythcorecontext.cpp b/mythtv/libs/libmythbase/mythcorecontext.cpp index ad58f597619..9bb536947bc 100644 --- a/mythtv/libs/libmythbase/mythcorecontext.cpp +++ b/mythtv/libs/libmythbase/mythcorecontext.cpp @@ -60,9 +60,9 @@ class MythCoreContextPrivate : public QObject QObject *m_GUIobject; QString m_appBinaryVersion; - QMutex m_localHostLock; ///< Locking for thread-safe copying of: - QString m_localHostname; ///< hostname from mysql.txt or gethostname() - QMutex m_masterHostLock; + QMutex m_localHostLock; ///< Locking for m_localHostname + QString m_localHostname; ///< hostname from config.xml or gethostname() + QMutex m_masterHostLock; ///< Locking for m_masterHostname QString m_masterHostname; ///< master backend hostname QMutex m_sockLock; ///< protects both m_serverSock and m_eventSock @@ -905,17 +905,17 @@ QString MythCoreContext::GetBackendServerIP(const QString &host) return addr6; } -void MythCoreContext::SetSetting(const QString &key, const QString &newValue) -{ - d->m_database->SetSetting(key, newValue); -} - void MythCoreContext::OverrideSettingForSession(const QString &key, const QString &value) { d->m_database->OverrideSettingForSession(key, value); } +void MythCoreContext::ClearOverrideSettingForSession(const QString &key) +{ + d->m_database->ClearOverrideSettingForSession(key); +} + bool MythCoreContext::IsUIThread(void) { return is_current_thread(d->m_UIThread); diff --git a/mythtv/libs/libmythbase/mythcorecontext.h b/mythtv/libs/libmythbase/mythcorecontext.h index 0917313a455..62f2847dc6f 100644 --- a/mythtv/libs/libmythbase/mythcorecontext.h +++ b/mythtv/libs/libmythbase/mythcorecontext.h @@ -150,11 +150,10 @@ class MBASE_PUBLIC MythCoreContext : public MythObservable, public MythSocketCBs QString GetBackendServerIP(void); QString GetBackendServerIP(const QString &host); - void SetSetting(const QString &key, const QString &newValue); - void ClearSettingsCache(const QString &myKey = QString("")); void ActivateSettingsCache(bool activate = true); void OverrideSettingForSession(const QString &key, const QString &value); + void ClearOverrideSettingForSession(const QString &key); void dispatch(const MythEvent &event); void dispatchNow(const MythEvent &event); // MDEPRECATED; diff --git a/mythtv/libs/libmythbase/mythdb.cpp b/mythtv/libs/libmythbase/mythdb.cpp index ea30783fed2..e60fce029cf 100644 --- a/mythtv/libs/libmythbase/mythdb.cpp +++ b/mythtv/libs/libmythbase/mythdb.cpp @@ -12,7 +12,6 @@ using namespace std; #include "mythdb.h" #include "mythdbcon.h" #include "mythlogging.h" -#include "oldsettings.h" #include "mythdirs.h" #include "mythcorecontext.h" @@ -21,6 +20,7 @@ static QMutex dbLock; // For thread safety reasons this is not a QString const char *kSentinelValue = ""; +const char *kClearSettingValue = ""; MythDB *MythDB::getMythDB(void) { @@ -72,8 +72,6 @@ class MythDBPrivate QString m_localhostname; MDBManager m_dbmanager; - Settings *m_settings; - bool ignoreDatabase; bool suppressDBMessages; @@ -93,10 +91,9 @@ class MythDBPrivate static const int settings_reserve = 61; -MythDBPrivate::MythDBPrivate() - : m_settings(new Settings()), - ignoreDatabase(false), suppressDBMessages(true), useSettingsCache(false), - haveDBConnection(false), haveSchema(false) +MythDBPrivate::MythDBPrivate() : + ignoreDatabase(false), suppressDBMessages(true), useSettingsCache(false), + haveDBConnection(false), haveSchema(false) { m_localhostname.clear(); settingsCache.reserve(settings_reserve); @@ -105,7 +102,6 @@ MythDBPrivate::MythDBPrivate() MythDBPrivate::~MythDBPrivate() { LOG(VB_DATABASE, LOG_INFO, "Destroying MythDBPrivate"); - delete m_settings; } MythDB::MythDB() @@ -123,11 +119,6 @@ MDBManager *MythDB::GetDBManager(void) return &(d->m_dbmanager); } -Settings *MythDB::GetOldSettings(void) -{ - return d->m_settings; -} - QString MythDB::toCommaList(const QMap &bindings, uint indent, uint maxColumn) { @@ -266,10 +257,10 @@ bool MythDB::SaveSettingOnHost(const QString &key, const QString &newValueRaw, const QString &host) { - QString LOC = QString("SaveSettingOnHost('%1') ").arg(key); + QString loc = QString("SaveSettingOnHost('%1') ").arg(key); if (key.isEmpty()) { - LOG(VB_GENERAL, LOG_ERR, LOC + "- Illegal null key"); + LOG(VB_GENERAL, LOG_ERR, loc + "- Illegal null key"); return false; } @@ -278,7 +269,12 @@ bool MythDB::SaveSettingOnHost(const QString &key, if (d->ignoreDatabase) { if (host.toLower() == d->m_localhostname) - OverrideSettingForSession(key, newValue); + { + if (newValue != kClearSettingValue) + OverrideSettingForSession(key, newValue); + else + ClearOverrideSettingForSession(key); + } return true; } @@ -287,7 +283,7 @@ bool MythDB::SaveSettingOnHost(const QString &key, if (host.toLower() == d->m_localhostname) OverrideSettingForSession(key, newValue); if (!d->suppressDBMessages) - LOG(VB_GENERAL, LOG_ERR, LOC + "- No database yet"); + LOG(VB_GENERAL, LOG_ERR, loc + "- No database yet"); SingleSetting setting; setting.host = host; setting.key = key; @@ -313,9 +309,19 @@ bool MythDB::SaveSettingOnHost(const QString &key, if (!host.isEmpty()) query.bindValue(":HOSTNAME", host); - if (!query.exec() && !(GetMythDB()->SuppressDBMessages())) - MythDB::DBError("Clear setting", query); + if (!query.exec()) + { + if (!GetMythDB()->SuppressDBMessages()) + MythDB::DBError("Clear setting", query); + } + else + { + success = true; + } + } + if (success && (newValue != kClearSettingValue)) + { if (!host.isEmpty()) query.prepare("INSERT INTO settings (value,data,hostname) " "VALUES ( :VALUE, :DATA, :HOSTNAME );"); @@ -328,14 +334,16 @@ bool MythDB::SaveSettingOnHost(const QString &key, if (!host.isEmpty()) query.bindValue(":HOSTNAME", host); - if (query.exec()) - success = true; - else if (!(GetMythDB()->SuppressDBMessages())) - MythDB::DBError(LOC + "- query failure: ", query); + if (!query.exec()) + { + success = false; + if (!(GetMythDB()->SuppressDBMessages())) + MythDB::DBError(loc + "- query failure: ", query); + } } - else + else if (!success) { - LOG(VB_GENERAL, LOG_ERR, LOC + "- database not open"); + LOG(VB_GENERAL, LOG_ERR, loc + "- database not open"); } ClearSettingsCache(host + ' ' + key); @@ -343,10 +351,20 @@ bool MythDB::SaveSettingOnHost(const QString &key, return success; } +bool MythDB::ClearSetting(const QString &key) +{ + return ClearSettingOnHost(key, d->m_localhostname); +} + +bool MythDB::ClearSettingOnHost(const QString &key, const QString &host) +{ + return SaveSettingOnHost(key, kClearSettingValue, host); +} + QString MythDB::GetSetting(const QString &_key, const QString &defaultval) { QString key = _key.toLower(); - QString value; + QString value = defaultval; d->settingsCacheLock.lockForRead(); if (d->useSettingsCache) @@ -369,17 +387,11 @@ QString MythDB::GetSetting(const QString &_key, const QString &defaultval) d->settingsCacheLock.unlock(); if (d->ignoreDatabase || !HaveValidDatabase()) - return defaultval; + return value; MSqlQuery query(MSqlQuery::InitCon()); if (!query.isConnected()) - { - if (!d->suppressDBMessages) - LOG(VB_GENERAL, LOG_ERR, - QString("Database not open while trying to load setting: %1") - .arg(key)); - return d->m_settings->GetSetting(key, defaultval); - } + return value; query.prepare( "SELECT data " @@ -404,10 +416,6 @@ QString MythDB::GetSetting(const QString &_key, const QString &defaultval) { value = query.value(0).toString(); } - else - { - value = d->m_settings->GetSetting(key, defaultval); - } } if (d->useSettingsCache && value != kSentinelValue) @@ -688,13 +696,6 @@ double MythDB::GetFloatSettingOnHost(const QString &key, const QString &host) return (retval == sentinel) ? 0.0 : retval.toDouble(); } -void MythDB::SetSetting(const QString &key, const QString &newValueRaw) -{ - QString newValue = (newValueRaw.isNull()) ? "" : newValueRaw; - d->m_settings->SetSetting(key, newValue); - ClearSettingsCache(key); -} - void MythDB::GetResolutionSetting(const QString &type, int &width, int &height, double &forced_aspect, @@ -789,6 +790,29 @@ void MythDB::OverrideSettingForSession( d->settingsCacheLock.unlock(); } +/// \brief Clears session Overrides for the given setting. +void MythDB::ClearOverrideSettingForSession(const QString &key) +{ + QString mk = key.toLower(); + QString mk2 = d->m_localhostname + ' ' + mk; + + d->settingsCacheLock.lockForWrite(); + + SettingsMap::iterator oit = d->overriddenSettings.find(mk); + if (oit != d->overriddenSettings.end()) + d->overriddenSettings.erase(oit); + + SettingsMap::iterator sit = d->settingsCache.find(mk); + if (sit != d->settingsCache.end()) + d->settingsCache.erase(sit); + + sit = d->settingsCache.find(mk2); + if (sit != d->settingsCache.end()) + d->settingsCache.erase(sit); + + d->settingsCacheLock.unlock(); +} + static void clear( SettingsMap &cache, SettingsMap &overrides, const QString &myKey) { @@ -857,189 +881,6 @@ void MythDB::ActivateSettingsCache(bool activate) ClearSettingsCache(); } -bool MythDB::LoadDatabaseParamsFromDisk(DatabaseParams ¶ms) -{ - Settings settings; - if (!settings.LoadSettingsFiles( - "mysql.txt", GetInstallPrefix(), GetConfDir())) - { - LOG(VB_GENERAL, LOG_ERR, "Unable to read configuration file mysql.txt"); - return false; - } - - params.dbHostName = settings.GetSetting("DBHostName"); - params.dbHostPing = settings.GetSetting("DBHostPing") != "no"; - params.dbPort = settings.GetNumSetting("DBPort", 3306); - params.dbUserName = settings.GetSetting("DBUserName"); - params.dbPassword = settings.GetSetting("DBPassword"); - params.dbName = settings.GetSetting("DBName"); - params.dbType = settings.GetSetting("DBType"); - - params.localHostName = settings.GetSetting("LocalHostName"); - params.localEnabled = !params.localHostName.isEmpty(); - - params.wolReconnect = - settings.GetNumSetting("WOLsqlReconnectWaitTime"); - params.wolEnabled = params.wolReconnect > 0; - - params.wolRetry = settings.GetNumSetting("WOLsqlConnectRetry"); - params.wolCommand = settings.GetSetting("WOLsqlCommand"); - - return ValidateDatabaseParams(params, "mysql.txt"); -} - -bool MythDB::ValidateDatabaseParams( - const DatabaseParams ¶ms, const QString &source) -{ - // Print some warnings if things look fishy.. - QString msg = QString(" is not set in %1").arg(source); - - if (params.dbHostName.isEmpty()) - { - LOG(VB_GENERAL, LOG_ERR, "DBHostName" + msg); - return false; - } - if (params.dbUserName.isEmpty()) - { - LOG(VB_GENERAL, LOG_ERR, "DBUserName" + msg); - return false; - } - if (params.dbPassword.isEmpty()) - { - LOG(VB_GENERAL, LOG_ERR, "DBPassword" + msg); - return false; - } - if (params.dbName.isEmpty()) - { - LOG(VB_GENERAL, LOG_ERR, "DBName" + msg); - return false; - } - - return true; -} - -// Sensible connection defaults. -void MythDB::LoadDefaultDatabaseParams(DatabaseParams ¶ms) -{ - params.dbHostName = "localhost"; - params.dbHostPing = true; - params.dbPort = 3306; - params.dbUserName = "mythtv"; - params.dbPassword = "mythtv"; - params.dbName = "mythconverg"; - params.dbType = "QMYSQL"; - params.localEnabled = false; - params.localHostName = "my-unique-identifier-goes-here"; - params.wolEnabled = false; - params.wolReconnect = 0; - params.wolRetry = 5; - params.wolCommand = "echo 'WOLsqlServerCommand not set'"; -} - -bool MythDB::SaveDatabaseParamsToDisk( - const DatabaseParams ¶ms, const QString &confdir, bool overwrite) -{ - QString path = confdir + "/mysql.txt"; - QFile * f = new QFile(path); - - if (!overwrite && f->exists()) - { - return false; - } - - QString dirpath = confdir; - QDir createDir(dirpath); - - if (!createDir.exists()) - { - if (!createDir.mkdir(dirpath)) - { - LOG(VB_GENERAL, LOG_ERR, - QString("Could not create %1").arg(dirpath)); - return false; - } - } - - if (!f->open(QIODevice::WriteOnly)) - { - LOG(VB_GENERAL, LOG_ERR, QString("Could not open settings file %1 " - "for writing").arg(path)); - return false; - } - - LOG(VB_GENERAL, LOG_NOTICE, QString("Writing settings file %1").arg(path)); - QTextStream s(f); - s << "DBHostName=" << params.dbHostName << endl; - - s << "\n" - << "# By default, Myth tries to ping the DB host to see if it exists.\n" - << "# If your DB host or network doesn't accept pings, set this to no:\n" - << "#\n"; - - if (params.dbHostPing) - s << "#DBHostPing=no" << endl << endl; - else - s << "DBHostPing=no" << endl << endl; - - if (params.dbPort) - s << "DBPort=" << params.dbPort << endl; - - s << "DBUserName=" << params.dbUserName << endl - << "DBPassword=" << params.dbPassword << endl - << "DBName=" << params.dbName << endl - << "DBType=" << params.dbType << endl - << endl - << "# Set the following if you want to use something other than this\n" - << "# machine's real hostname for identifying settings in the database.\n" - << "# This is useful if your hostname changes often, as otherwise you\n" - << "# will need to reconfigure mythtv every time.\n" - << "# NO TWO HOSTS MAY USE THE SAME VALUE\n" - << "#\n"; - - if (params.localEnabled) - s << "LocalHostName=" << params.localHostName << endl; - else - s << "#LocalHostName=my-unique-identifier-goes-here\n"; - - s << endl - << "# If you want your frontend to be able to wake your MySQL server\n" - << "# using WakeOnLan, have a look at the following settings:\n" - << "#\n" - << "#\n" - << "# The time the frontend waits (in seconds) between reconnect tries.\n" - << "# This should be the rough time your MySQL server needs for startup\n" - << "#\n"; - - if (params.wolEnabled) - s << "WOLsqlReconnectWaitTime=" << params.wolReconnect << endl; - else - s << "#WOLsqlReconnectWaitTime=0\n"; - - s << "#\n" - << "#\n" - << "# This is the number of retries to wake the MySQL server\n" - << "# until the frontend gives up\n" - << "#\n"; - - if (params.wolEnabled) - s << "WOLsqlConnectRetry=" << params.wolRetry << endl; - else - s << "#WOLsqlConnectRetry=5\n"; - - s << "#\n" - << "#\n" - << "# This is the command executed to wake your MySQL server.\n" - << "#\n"; - - if (params.wolEnabled) - s << "WOLsqlCommand=" << params.wolCommand << endl; - else - s << "#WOLsqlCommand=echo 'WOLsqlServerCommand not set'\n"; - - f->close(); - return true; -} - void MythDB::WriteDelayedSettings(void) { if (!HaveValidDatabase()) diff --git a/mythtv/libs/libmythbase/mythdb.h b/mythtv/libs/libmythbase/mythdb.h index 79abe569964..ac2a8eca22d 100644 --- a/mythtv/libs/libmythbase/mythdb.h +++ b/mythtv/libs/libmythbase/mythdb.h @@ -9,7 +9,6 @@ #include "mythdbparams.h" class MythDBPrivate; -class Settings; class MDBManager; class MBASE_PUBLIC MythDB @@ -17,7 +16,6 @@ class MBASE_PUBLIC MythDB friend class MSqlQuery; public: MDBManager *GetDBManager(void); - Settings *GetOldSettings(void); static QString GetError(const QString &where, const MSqlQuery &query); static void DBError(const QString &where, const MSqlQuery &query); @@ -38,11 +36,14 @@ class MBASE_PUBLIC MythDB void ClearSettingsCache(const QString &key = QString()); void ActivateSettingsCache(bool activate = true); void OverrideSettingForSession(const QString &key, const QString &newValue); + void ClearOverrideSettingForSession(const QString &key); void SaveSetting(const QString &key, int newValue); void SaveSetting(const QString &key, const QString &newValue); bool SaveSettingOnHost(const QString &key, const QString &newValue, const QString &host); + bool ClearSetting(const QString &key); + bool ClearSettingOnHost(const QString &key, const QString &host); bool GetSettings(QMap &_key_value_pairs); @@ -71,8 +72,6 @@ class MBASE_PUBLIC MythDB void GetResolutionSetting(const QString &type, int &width, int &height, int index=-1); - void SetSetting(const QString &key, const QString &newValue); - void WriteDelayedSettings(void); void SetHaveDBConnection(bool connected); @@ -85,13 +84,6 @@ class MBASE_PUBLIC MythDB static QString toCommaList(const QMap &bindings, uint indent = 0, uint softMaxColumn = 80); - static bool LoadDatabaseParamsFromDisk(DatabaseParams ¶ms); - static bool ValidateDatabaseParams( - const DatabaseParams ¶ms, const QString &source); - static void LoadDefaultDatabaseParams(DatabaseParams ¶ms); - static bool SaveDatabaseParamsToDisk( - const DatabaseParams ¶ms, const QString &confdir, bool overwrite); - protected: MythDB(); ~MythDB(); diff --git a/mythtv/libs/libmythbase/mythdbparams.cpp b/mythtv/libs/libmythbase/mythdbparams.cpp new file mode 100644 index 00000000000..273804ced3d --- /dev/null +++ b/mythtv/libs/libmythbase/mythdbparams.cpp @@ -0,0 +1,78 @@ +#include "mythdbparams.h" +#include "mythlogging.h" + +/// Load sensible connection defaults. +void DatabaseParams::LoadDefaults(void) +{ + dbHostName = "localhost"; + dbHostPing = true; + dbPort = 3306; + dbUserName = "mythtv"; + dbPassword = "mythtv"; + dbName = "mythconverg"; + dbType = "QMYSQL"; + + localEnabled = false; + localHostName = "my-unique-identifier-goes-here"; + + wolEnabled = false; + wolReconnect = 0; + wolRetry = 5; + wolCommand = "echo 'WOLsqlServerCommand not set'"; + + forceSave = false; + + verVersion.clear(); + verBranch.clear(); + verProtocol.clear(); + verBinary.clear(); + verSchema.clear(); +} + +bool DatabaseParams::IsValid(const QString &source) const +{ + // Print some warnings if things look fishy.. + QString msg = QString(" is not set in %1").arg(source); + + if (dbHostName.isEmpty()) + { + LOG(VB_GENERAL, LOG_ERR, "DBHostName" + msg); + return false; + } + if (dbUserName.isEmpty()) + { + LOG(VB_GENERAL, LOG_ERR, "DBUserName" + msg); + return false; + } + if (dbPassword.isEmpty()) + { + LOG(VB_GENERAL, LOG_ERR, "DBPassword" + msg); + return false; + } + if (dbName.isEmpty()) + { + LOG(VB_GENERAL, LOG_ERR, "DBName" + msg); + return false; + } + + return true; +} + +bool DatabaseParams::operator==(const DatabaseParams &other) const +{ + return + dbHostName == other.dbHostName && + dbHostPing == other.dbHostPing && + dbPort == other.dbPort && + dbUserName == other.dbUserName && + dbPassword == other.dbPassword && + dbName == other.dbName && + dbType == other.dbType && + localEnabled == other.localEnabled && + wolEnabled == other.wolEnabled && + (!localEnabled || (localHostName == other.localHostName)) && + (!wolEnabled || + (wolReconnect == other.wolReconnect && + wolRetry == other.wolRetry && + wolCommand == other.wolCommand)); +} diff --git a/mythtv/libs/libmythbase/mythdbparams.h b/mythtv/libs/libmythbase/mythdbparams.h index a60f57664a1..a6d349defee 100644 --- a/mythtv/libs/libmythbase/mythdbparams.h +++ b/mythtv/libs/libmythbase/mythdbparams.h @@ -1,11 +1,23 @@ #ifndef MYTHDBPARAMS_H_ #define MYTHDBPARAMS_H_ +#include + #include "mythbaseexp.h" /// Structure containing the basic Database parameters -struct MBASE_PUBLIC DatabaseParams +class MBASE_PUBLIC DatabaseParams { + public: + DatabaseParams() { LoadDefaults(); } + + void LoadDefaults(void); + bool IsValid(const QString &source = QString("Unknown")) const; + + bool operator==(const DatabaseParams &other) const; + bool operator!=(const DatabaseParams &other) const + { return !((*this)==other); } + QString dbHostName; ///< database server bool dbHostPing; ///< Can we test connectivity using ping? int dbPort; ///< database port @@ -22,6 +34,8 @@ struct MBASE_PUBLIC DatabaseParams int wolRetry; ///< times to retry to reconnect QString wolCommand; ///< command to use for wake-on-lan + bool forceSave; ///< set to true to force a save of the settings file + QString verVersion; ///< git version string QString verBranch; ///< git branch QString verProtocol; ///< backend protocol diff --git a/mythtv/libs/libmythbase/mythlocale.cpp b/mythtv/libs/libmythbase/mythlocale.cpp index 1fa60b4c5e9..409c15d6e05 100644 --- a/mythtv/libs/libmythbase/mythlocale.cpp +++ b/mythtv/libs/libmythbase/mythlocale.cpp @@ -179,20 +179,14 @@ void MythLocale::SaveLocaleDefaults(bool overwrite) { MythDB *mythDB = MythDB::getMythDB(); if (overwrite || mythDB->GetSetting(it.key()).isEmpty()) - { - mythDB->SetSetting(it.key(), it.value()); mythDB->SaveSettingOnHost(it.key(), it.value(), ""); - } } for (it = m_hostSettings.begin(); it != m_hostSettings.end(); ++it) { MythDB *mythDB = MythDB::getMythDB(); if (overwrite || mythDB->GetSetting(it.key()).isEmpty()) - { - mythDB->SetSetting(it.key(), it.value()); mythDB->SaveSetting(it.key(), it.value()); - } } } diff --git a/mythtv/libs/libmythbase/mythtranslation.cpp b/mythtv/libs/libmythbase/mythtranslation.cpp index 46adf08459c..6690cf9c32d 100644 --- a/mythtv/libs/libmythbase/mythtranslation.cpp +++ b/mythtv/libs/libmythbase/mythtranslation.cpp @@ -52,7 +52,6 @@ void MythTranslation::load(const QString &module_name) if (lang == "en") { - gCoreContext->SetSetting("Language", "en_US"); gCoreContext->SaveSetting("Language", "en_US"); lang = "en_us"; } diff --git a/mythtv/libs/libmythbase/mythversion.h b/mythtv/libs/libmythbase/mythversion.h index 3390189b6f0..4e4736f9481 100644 --- a/mythtv/libs/libmythbase/mythversion.h +++ b/mythtv/libs/libmythbase/mythversion.h @@ -12,7 +12,7 @@ /// Update this whenever the plug-in API changes. /// Including changes in the libmythbase, libmyth, libmythtv, libmythav* and /// libmythui class methods used by plug-ins. -#define MYTH_BINARY_VERSION "0.26.20120503-1" +#define MYTH_BINARY_VERSION "0.26.20120504-1" /** \brief Increment this whenever the MythTV network protocol changes. * @@ -35,8 +35,8 @@ * mythtv/bindings/python/MythTV/static.py (version number) * mythtv/bindings/python/MythTV/mythproto.py (layout) */ -#define MYTH_PROTO_VERSION "73" -#define MYTH_PROTO_TOKEN "D7FE8D6F" +#define MYTH_PROTO_VERSION "74" +#define MYTH_PROTO_TOKEN "SingingPotato" /** \brief Increment this whenever the MythTV core database schema changes. * diff --git a/mythtv/libs/libmythmetadata/parentalcontrols.cpp b/mythtv/libs/libmythmetadata/parentalcontrols.cpp index 74b20546aaa..124c832d76e 100644 --- a/mythtv/libs/libmythmetadata/parentalcontrols.cpp +++ b/mythtv/libs/libmythmetadata/parentalcontrols.cpp @@ -277,7 +277,6 @@ class ParentalLevelChangeCheckerPrivate : public QObject { // Two minute window last_time_stamp = curr_time.toString(Qt::ISODate); - gCoreContext->SetSetting("VideoPasswordTime", last_time_stamp); gCoreContext->SaveSetting("VideoPasswordTime", last_time_stamp); return true; } diff --git a/mythtv/libs/libmythui/myththemedmenu.cpp b/mythtv/libs/libmythui/myththemedmenu.cpp index 3bc7aab7276..7d9d00b01c7 100644 --- a/mythtv/libs/libmythui/myththemedmenu.cpp +++ b/mythtv/libs/libmythui/myththemedmenu.cpp @@ -398,7 +398,6 @@ void MythThemedMenu::customEvent(QEvent *event) QString timestamp_setting = QString("%1Time").arg(button.password); QDateTime curr_time = QDateTime::currentDateTime(); QString last_time_stamp = curr_time.toString(Qt::TextDate); - GetMythDB()->SetSetting(timestamp_setting, last_time_stamp); GetMythDB()->SaveSetting(timestamp_setting, last_time_stamp); buttonAction(item, true); } @@ -897,7 +896,6 @@ bool MythThemedMenu::checkPinCode(const QString &password_setting) if (last_time.secsTo(curr_time) < 120) { last_time_stamp = curr_time.toString(Qt::TextDate); - GetMythDB()->SetSetting(timestamp_setting, last_time_stamp); GetMythDB()->SaveSetting(timestamp_setting, last_time_stamp); return true; } diff --git a/mythtv/libs/libmythupnp/configuration.cpp b/mythtv/libs/libmythupnp/configuration.cpp index ef494deb7ab..1ecd8531925 100644 --- a/mythtv/libs/libmythupnp/configuration.cpp +++ b/mythtv/libs/libmythupnp/configuration.cpp @@ -10,6 +10,8 @@ // ////////////////////////////////////////////////////////////////////////////// +#include // for fsync + #include #include @@ -87,14 +89,15 @@ bool XmlConfiguration::Save( void ) if (m_sFileName.isEmpty()) // Special case. No file is created return true; - QString sName = m_sPath + '/' + m_sFileName; + QString config_temppath = m_sPath + '/' + m_sFileName + ".new"; + QString config_filepath = m_sPath + '/' + m_sFileName; + QString config_origpath = m_sPath + '/' + m_sFileName + ".orig"; - QFile file( sName ); + QFile file(config_temppath); if (!file.exists()) { - QDir createDir( m_sPath ); - + QDir createDir(m_sPath); if (!createDir.exists()) { if (!createDir.mkdir(m_sPath)) @@ -106,21 +109,46 @@ bool XmlConfiguration::Save( void ) } } - if (!file.open( QIODevice::WriteOnly | QIODevice::Truncate )) + if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { LOG(VB_GENERAL, LOG_ERR, - QString("Could not open settings file %1 for writing").arg(sName)); + QString("Could not open settings file %1 for writing") + .arg(config_temppath)); return false; } - QTextStream ts( &file ); + { + QTextStream ts(&file); + m_config.save(ts, 2); + } - m_config.save( ts, 2 ); + file.flush(); + + fsync(file.handle()); file.close(); - return true; + bool ok = true; + if (QFile::exists(config_filepath)) + ok = QFile::rename(config_filepath, config_origpath); + + if (ok) + { + ok = file.rename(config_filepath); + if (ok) + QFile::remove(config_origpath); + else if (QFile::exists(config_origpath)) + QFile::rename(config_origpath, config_filepath); + } + + if (!ok) + { + LOG(VB_GENERAL, LOG_ERR, + QString("Could not save settings file %1").arg(config_filepath)); + } + + return ok; } ////////////////////////////////////////////////////////////////////////////// @@ -257,6 +285,25 @@ void XmlConfiguration::SetValue( const QString &sSetting, QString sValue ) } } +////////////////////////////////////////////////////////////////////////////// +// +////////////////////////////////////////////////////////////////////////////// + +void XmlConfiguration::ClearValue( const QString &sSetting ) +{ + QDomNode node = FindNode(sSetting); + if (!node.isNull()) + { + QDomNode parent = node.parentNode(); + parent.removeChild(node); + while (parent.childNodes().count() == 0) + { + QDomNode next_parent = parent.parentNode(); + next_parent.removeChild(parent); + parent = next_parent; + } + } +} ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// @@ -324,3 +371,12 @@ void DBConfiguration::SetValue( const QString &sSetting, QString sValue ) { GetMythDB()->SaveSetting( sSetting, sValue ); } + +////////////////////////////////////////////////////////////////////////////// +// +////////////////////////////////////////////////////////////////////////////// + +void DBConfiguration::ClearValue( const QString &sSetting ) +{ + GetMythDB()->ClearSetting( sSetting ); +} diff --git a/mythtv/libs/libmythupnp/configuration.h b/mythtv/libs/libmythupnp/configuration.h index 5b6095a2b3b..ef3ad8ffb2e 100644 --- a/mythtv/libs/libmythupnp/configuration.h +++ b/mythtv/libs/libmythupnp/configuration.h @@ -32,7 +32,7 @@ class UPNP_PUBLIC Configuration virtual void SetValue( const QString &sSetting, int value ) = 0; virtual void SetValue( const QString &sSetting, QString value ) = 0; - + virtual void ClearValue( const QString &sSetting ) = 0; }; @@ -73,7 +73,7 @@ class UPNP_PUBLIC XmlConfiguration : public Configuration virtual void SetValue( const QString &sSetting, int value ); virtual void SetValue( const QString &sSetting, QString value ); - + virtual void ClearValue( const QString &sSetting ); }; ////////////////////////////////////////////////////////////////////////////// @@ -96,7 +96,7 @@ class UPNP_PUBLIC DBConfiguration : public Configuration virtual void SetValue( const QString &sSetting, int value ); virtual void SetValue( const QString &sSetting, QString value ); - + virtual void ClearValue( const QString &sSetting ); }; #endif diff --git a/mythtv/programs/mythbackend/mythsettings.cpp b/mythtv/programs/mythbackend/mythsettings.cpp index 4b033cc0ee5..cfdebfa8462 100644 --- a/mythtv/programs/mythbackend/mythsettings.cpp +++ b/mythtv/programs/mythbackend/mythsettings.cpp @@ -355,15 +355,7 @@ QMap GetSettingsMap(MythSettingList &settings, QMap result; MSqlQuery query(MSqlQuery::InitCon()); - QString list = extract_query_list(settings, MythSetting::kFile); - if (!list.isEmpty()) - { - result = GetConfigFileSettingValues(); - if (result.isEmpty()) - return result; - } - - list = extract_query_list(settings, MythSetting::kHost); + QString list = extract_query_list(settings, MythSetting::kHost); QString qstr = "SELECT value, data " "FROM settings " @@ -474,31 +466,6 @@ QString StringListToJSON(const QString &key, return result; } -QMap GetConfigFileSettingValues(void) -{ - QMap map; - DatabaseParams params; - if (!MythDB::LoadDatabaseParamsFromDisk(params)) - MythDB::LoadDefaultDatabaseParams(params); - - map["dbHostName"] = params.dbHostName; - map["dbPort"] = QString::number(params.dbPort); - map["dbPing"] = QString::number(params.dbHostPing); - map["dbName"] = params.dbName; - map["dbUserName"] = params.dbUserName; - map["dbPassword"] = params.dbPassword; - map["dbHostID"] = params.localHostName; - map["dbWOLEnabled"] = - QString::number(params.wolEnabled); - map["dbWOLReconnectCount"] = - QString::number(params.wolReconnect); - map["dbWOLRetryCount"] = - QString::number(params.wolRetry); - map["dbWOLCommand"] = params.wolCommand; - - return map; -} - bool parse_dom(MythSettingList &settings, const QDomElement &element, const QString &filename, const QString &group, bool includeAllChildren, bool &foundGroup) @@ -706,19 +673,7 @@ bool load_settings(MythSettingList &settings, const QString &hostname) { MSqlQuery query(MSqlQuery::InitCon()); - QString list = extract_query_list(settings, MythSetting::kFile); - if (!list.isEmpty()) - { - QMap map = GetConfigFileSettingValues(); - if (map.isEmpty()) - return false; - - MythSettingList::const_iterator it = settings.begin(); - for (; it != settings.end(); ++it) - fill_setting(*it, map, MythSetting::kFile); - } - - list = extract_query_list(settings, MythSetting::kHost); + QString list = extract_query_list(settings, MythSetting::kHost); QString qstr = "SELECT value, data " "FROM settings " diff --git a/mythtv/programs/mythbackend/mythsettings.h b/mythtv/programs/mythbackend/mythsettings.h index 43fa892646b..2c6f812ce1e 100644 --- a/mythtv/programs/mythbackend/mythsettings.h +++ b/mythtv/programs/mythbackend/mythsettings.h @@ -125,7 +125,6 @@ bool check_settings(MythSettingList &database_settings, QStringList GetSettingValueList(const QString &type); QString StringMapToJSON(const QMap &map); QString StringListToJSON(const QString &key, const QStringList &sList); -QMap GetConfigFileSettingValues(); QMap GetSettingsMap(MythSettingList &settings, const QString &hostname); #endif diff --git a/mythtv/programs/mythtv-setup/main.cpp b/mythtv/programs/mythtv-setup/main.cpp index a901e3812d8..ee2e96ea956 100644 --- a/mythtv/programs/mythtv-setup/main.cpp +++ b/mythtv/programs/mythtv-setup/main.cpp @@ -348,7 +348,7 @@ int main(int argc, char *argv[]) if (use_display) { - gCoreContext->SetSetting("Theme", DEFAULT_UI_THEME); + gCoreContext->OverrideSettingForSession("Theme", DEFAULT_UI_THEME); GetMythUI()->LoadQtConfig(); QString fileprefix = GetConfDir(); diff --git a/mythtv/programs/mythtv-setup/startprompt.cpp b/mythtv/programs/mythtv-setup/startprompt.cpp index b49385a617d..e61cf1246e0 100644 --- a/mythtv/programs/mythtv-setup/startprompt.cpp +++ b/mythtv/programs/mythtv-setup/startprompt.cpp @@ -44,7 +44,7 @@ void StartPrompter::handleStart() void StartPrompter::leaveBackendRunning() { LOG(VB_GENERAL, LOG_INFO, "Continuing with backend running"); - gCoreContext->SetSetting("AutoRestartBackend", "0"); + gCoreContext->OverrideSettingForSession("AutoRestartBackend", "0"); } void StartPrompter::stopBackend() @@ -56,7 +56,7 @@ void StartPrompter::stopBackend() { myth_system(commandString); } - gCoreContext->SetSetting("AutoRestartBackend", "1"); + gCoreContext->OverrideSettingForSession("AutoRestartBackend", "1"); } void StartPrompter::backendRunningPrompt(void) From 725264c03485c909b9e7f0455c15464fe963dff4 Mon Sep 17 00:00:00 2001 From: Daniel Kristjansson Date: Fri, 27 Apr 2012 09:57:01 -0400 Subject: [PATCH 61/69] Minor LOG fixes. --- mythtv/libs/libmyth/backendselect.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/mythtv/libs/libmyth/backendselect.cpp b/mythtv/libs/libmyth/backendselect.cpp index 0b2240bf09c..309a7a3a8cf 100644 --- a/mythtv/libs/libmyth/backendselect.cpp +++ b/mythtv/libs/libmyth/backendselect.cpp @@ -199,12 +199,12 @@ bool BackendSelection::ConnectBackend(DeviceLocation *dev) { case UPnPResult_Success: LOG(VB_UPNP, LOG_INFO, - QString("ConnectBackend() - success. New hostname: %1") - .arg(m_DBparams->dbHostName)); + QString("ConnectBackend() - success. New hostname: %1") + .arg(m_DBparams->dbHostName)); return true; case UPnPResult_HumanInterventionRequired: - LOG(VB_UPNP, LOG_ERR, error); + LOG(VB_GENERAL, LOG_ERR, QString("Need Human: %1").arg(message)); ShowOkPopup(message); if (TryDBfromURL("", dev->m_sLocation)) @@ -213,16 +213,16 @@ bool BackendSelection::ConnectBackend(DeviceLocation *dev) break; case UPnPResult_ActionNotAuthorized: - LOG(VB_UPNP, LOG_ERR, - QString("Access denied for %1. Wrong PIN?") - .arg(backendName)); + LOG(VB_GENERAL, LOG_ERR, + QString("Access denied for %1. Wrong PIN?") + .arg(backendName)); PromptForPassword(); break; default: - LOG(VB_UPNP, LOG_ERR, - QString("GetConnectionInfo() failed for %1") - .arg(backendName)); + LOG(VB_GENERAL, LOG_ERR, + QString("GetConnectionInfo() failed for %1 : %2") + .arg(backendName).arg(message)); ShowOkPopup(message); } From 47fb47c37ef56db211566c00ed524b6eafd3e240 Mon Sep 17 00:00:00 2001 From: Daniel Kristjansson Date: Fri, 27 Apr 2012 09:58:24 -0400 Subject: [PATCH 62/69] Set MythXMLClient::GetConnectionInfo sMsg for some of the error cases so the user knows what is going on, instead of getting OK dialogs with no text. --- mythtv/libs/libmythupnp/mythxmlclient.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/mythtv/libs/libmythupnp/mythxmlclient.cpp b/mythtv/libs/libmythupnp/mythxmlclient.cpp index cb2ea07cca0..696fef7799c 100644 --- a/mythtv/libs/libmythupnp/mythxmlclient.cpp +++ b/mythtv/libs/libmythupnp/mythxmlclient.cpp @@ -91,7 +91,17 @@ UPnPResultCode MythXMLClient::GetConnectionInfo( const QString &sPin, DatabasePa if ((pParams->verProtocol != MYTH_PROTO_VERSION) || (pParams->verSchema != MYTH_DATABASE_VERSION)) // incompatible version, we cannot use this backend + { + LOG(VB_GENERAL, LOG_ERR, + QString("MythXMLClient::GetConnectionInfo Failed - " + "Version Mismatch (%1,%2) != (%3,%4)") + .arg(pParams->verProtocol) + .arg(pParams->verSchema) + .arg(MYTH_PROTO_VERSION) + .arg(MYTH_DATABASE_VERSION)); + sMsg = QObject::tr("Version Mismatch", "UPNP Errors"); return UPnPResult_ActionFailed; + } return UPnPResult_Success; } @@ -110,8 +120,10 @@ UPnPResultCode MythXMLClient::GetConnectionInfo( const QString &sPin, DatabasePa { // Service calls no longer return UPnPResult codes, // convert standard 501 to UPnPResult code for now. + sMsg = QObject::tr("Not Authorized", "UPNP Errors"); return UPnPResult_ActionNotAuthorized; } + sMsg = QObject::tr("Unknown Error", "UPNP Errors"); return UPnPResult_ActionFailed; } From fe25cc5425ff37c65ce39193f2aba6db1b75619e Mon Sep 17 00:00:00 2001 From: Daniel Kristjansson Date: Fri, 27 Apr 2012 10:00:35 -0400 Subject: [PATCH 63/69] Rename noPrompt variable noAutodetect to reflect it's actual meaning and fix one incorrect assumption made because of the old name. --- mythtv/libs/libmyth/mythcontext.cpp | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/mythtv/libs/libmyth/mythcontext.cpp b/mythtv/libs/libmyth/mythcontext.cpp index 30706a9bb1c..54aa33b97e7 100644 --- a/mythtv/libs/libmyth/mythcontext.cpp +++ b/mythtv/libs/libmyth/mythcontext.cpp @@ -327,10 +327,10 @@ bool MythContextPrivate::Init(const bool gui, * Despite its name, the disable argument currently only disables the chooser. * If set, autoconfigure will still be attempted in some situations. */ -bool MythContextPrivate::FindDatabase(const bool prompt, const bool noPrompt) +bool MythContextPrivate::FindDatabase(bool prompt, bool noAutodetect) { - // The two bool. args actually form a Yes/Maybe/No (A tristate bool :-) - bool manualSelect = prompt && !noPrompt; + // We can only prompt if autodiscovery is enabled.. + bool manualSelect = prompt && !noAutodetect; QString failure; @@ -377,14 +377,11 @@ bool MythContextPrivate::FindDatabase(const bool prompt, const bool noPrompt) goto DBfound; } - if (count > 1 || count == -1) // Multiple BEs, or needs PIN. - manualSelect = !noPrompt; // If allowed, prompt user + // Multiple BEs, or needs PIN. + manualSelect |= (count > 1 || count == -1); } - if (!m_gui) - manualSelect = false; // no interactive command-line chooser yet - - + manualSelect &= m_gui; // no interactive command-line chooser yet // Last, get the user to select a backend from a possible list: if (manualSelect) From 395518d5957bb755313dd37fdf35b3008d827f38 Mon Sep 17 00:00:00 2001 From: Daniel Kristjansson Date: Fri, 27 Apr 2012 10:01:44 -0400 Subject: [PATCH 64/69] Use QString::isEmpty() to determine if string is empty instead of QString::length() + print logging message when the error sting is not empty. --- mythtv/libs/libmyth/mythcontext.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mythtv/libs/libmyth/mythcontext.cpp b/mythtv/libs/libmyth/mythcontext.cpp index 54aa33b97e7..c36d5212abd 100644 --- a/mythtv/libs/libmyth/mythcontext.cpp +++ b/mythtv/libs/libmyth/mythcontext.cpp @@ -772,8 +772,11 @@ int MythContextPrivate::ChooseBackend(const QString &error) TempMainWindow(); // Tell the user what went wrong: - if (error.length()) + if (!error.isEmpty()) + { + LOG(VB_GENERAL, LOG_ERR, QString("Error: %1").arg(error)); ShowOkPopup(error); + } LOG(VB_GENERAL, LOG_INFO, "Putting up the UPnP backend chooser"); From ac042469221572eaeea9dd8faa9392407dd5a22b Mon Sep 17 00:00:00 2001 From: Daniel Kristjansson Date: Fri, 27 Apr 2012 10:02:00 -0400 Subject: [PATCH 65/69] Don't autoselect a backend if the user has specified that no autodetection should be done. --- mythtv/libs/libmyth/mythcontext.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mythtv/libs/libmyth/mythcontext.cpp b/mythtv/libs/libmyth/mythcontext.cpp index c36d5212abd..f0723075b7e 100644 --- a/mythtv/libs/libmyth/mythcontext.cpp +++ b/mythtv/libs/libmyth/mythcontext.cpp @@ -341,7 +341,7 @@ bool MythContextPrivate::FindDatabase(bool prompt, bool noAutodetect) // In addition to the UI chooser, we can also try to autoSelect later, // but only if we're not doing manualSelect and there was no // valid config.xml - bool autoSelect = !manualSelect && !loaded; + bool autoSelect = !manualSelect && !loaded && !noAutodetect; // 2. If the user isn't forcing up the chooser UI, look for a default // backend in config.xml, then test DB settings we've got so far: From f9e869f36979513572f64687fcad4941b0d38e3c Mon Sep 17 00:00:00 2001 From: Daniel Kristjansson Date: Tue, 1 May 2012 10:45:08 -0400 Subject: [PATCH 66/69] Make sure UPnP autoconfiguration allows backends time to respond. According to the UPnP spec the backends have up to the timeout value to respond but we don't wait that long for all backends to respond so long as one backend has responded. Our backends try to respond ASAP so this isn't as bad as doing this with other UPnP devices, but there is a race condition which results in us connecting to the first backend to respond instead of popping up the chooser when there is more than one backend on the network. This also resends the search request every 250 ms. When testing this I ran into some problems due to lost packets. When the broadcast search packet doesn't make it onto the network we obviously never get a response. This just resends that packet every 250 ms until only one second remains for the backends to respond. This increases the odds that we get a response in the presence of an unreliable network (in my case WiFi with Ubuntu Precise & bug 836250). --- mythtv/libs/libmyth/mythcontext.cpp | 56 ++++++++++++++--------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/mythtv/libs/libmyth/mythcontext.cpp b/mythtv/libs/libmyth/mythcontext.cpp index f0723075b7e..a05f5fe7955 100644 --- a/mythtv/libs/libmyth/mythcontext.cpp +++ b/mythtv/libs/libmyth/mythcontext.cpp @@ -795,46 +795,46 @@ int MythContextPrivate::ChooseBackend(const QString &error) */ int MythContextPrivate::UPnPautoconf(const int milliSeconds) { - SSDPCacheEntries *backends = NULL; - int count; - QString loc = "UPnPautoconf() - "; - QTime timer; + LOG(VB_GENERAL, LOG_INFO, QString("UPNP Search %1 secs") + .arg(milliSeconds / 1000)); - SSDP::Instance()->PerformSearch( gBackendURI ); + SSDP::Instance()->PerformSearch(gBackendURI, milliSeconds / 1000); - for (timer.start(); timer.elapsed() < milliSeconds; ) + // Search for a total of 'milliSeconds' ms, sending new search packet + // about every 250 ms until less than one second remains. + MythTimer totalTime; totalTime.start(); + MythTimer searchTime; searchTime.start(); + while (totalTime.elapsed() < milliSeconds) { usleep(25000); - backends = SSDP::Instance()->Find( gBackendURI ); - if (backends) - break; -#if 0 - putchar('.'); -#endif + int ttl = milliSeconds - totalTime.elapsed(); + if ((searchTime.elapsed() > 249) && (ttl > 1000)) + { + LOG(VB_GENERAL, LOG_INFO, QString("UPNP Search %1 secs") + .arg(ttl / 1000)); + SSDP::Instance()->PerformSearch(gBackendURI, ttl / 1000); + searchTime.start(); + } } -#if 0 - putchar('\n'); -#endif + + SSDPCacheEntries *backends = SSDP::Instance()->Find(gBackendURI); if (!backends) { - LOG(VB_GENERAL, LOG_INFO, loc + "No UPnP backends found"); + LOG(VB_GENERAL, LOG_INFO, "No UPnP backends found"); return 0; } - count = backends->Count(); - switch (count) + int count = backends->Count(); + if (count) { - case 0: - LOG(VB_GENERAL, LOG_ALERT, loc + - "No UPnP backends found, but SSDP::Find() not NULL!"); - break; - case 1: - LOG(VB_GENERAL, LOG_INFO, loc + "Found one UPnP backend"); - break; - default: - LOG(VB_GENERAL, LOG_INFO, loc + - QString("More than one UPnP backend found (%1)") .arg(count)); + LOG(VB_GENERAL, LOG_INFO, + QString("Found %1 UPnP backends").arg(count)); + } + else + { + LOG(VB_GENERAL, LOG_ERR, + "No UPnP backends found, but SSDP::Find() not NULL"); } if (count != 1) From 77b68d51fec1871b5c9dbf56a5902a7645035ffc Mon Sep 17 00:00:00 2001 From: Daniel Kristjansson Date: Tue, 1 May 2012 10:48:48 -0400 Subject: [PATCH 67/69] Resend UPnP search request when waiting for one backend to respond. In DefaultUPnP we try to get some extra information from the backend we've chosen to connect to, but we only send the search packet once. This could get lost due to networking problems, so resend the packet approximately every 250 ms to increase the odds of a response. --- mythtv/libs/libmyth/mythcontext.cpp | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/mythtv/libs/libmyth/mythcontext.cpp b/mythtv/libs/libmyth/mythcontext.cpp index a05f5fe7955..d452c286646 100644 --- a/mythtv/libs/libmyth/mythcontext.cpp +++ b/mythtv/libs/libmyth/mythcontext.cpp @@ -878,30 +878,36 @@ bool MythContextPrivate::DefaultUPnP(QString &error) // ---------------------------------------------------------------------- - SSDP::Instance()->PerformSearch( gBackendURI ); + int timeout_ms = 2000; + LOG(VB_GENERAL, LOG_INFO, QString("UPNP Search up to %1 secs") + .arg(timeout_ms / 1000)); + SSDP::Instance()->PerformSearch(gBackendURI, timeout_ms / 1000); // ---------------------------------------------------------------------- // We need to give the server time to respond... // ---------------------------------------------------------------------- DeviceLocation *pDevLoc = NULL; - QTime timer; - - for (timer.start(); timer.elapsed() < 5000; ) + MythTimer totalTime; totalTime.start(); + MythTimer searchTime; searchTime.start(); + while (totalTime.elapsed() < timeout_ms) { - usleep(25000); pDevLoc = SSDP::Instance()->Find( gBackendURI, USN ); if (pDevLoc) break; -#if 0 - putchar('.'); -#endif + usleep(25000); + + int ttl = timeout_ms - totalTime.elapsed(); + if ((searchTime.elapsed() > 249) && (ttl > 1000)) + { + LOG(VB_GENERAL, LOG_INFO, QString("UPNP Search up to %1 secs") + .arg(ttl / 1000)); + SSDP::Instance()->PerformSearch(gBackendURI, ttl / 1000); + searchTime.start(); + } } -#if 0 - putchar('\n'); -#endif // ---------------------------------------------------------------------- From 217be2413a2638566ccdc7b12472976d18fe1f6a Mon Sep 17 00:00:00 2001 From: Daniel Kristjansson Date: Thu, 3 May 2012 10:46:15 -0400 Subject: [PATCH 68/69] Make UPnP backend selector buttons work. I assume at some point these buttons worked, but the code to make them work was not there anymore. I didn't bother to do any archeology I just added the functionality back in a way that made sense to me. --- mythtv/libs/libmyth/backendselect.cpp | 35 ++++++++-------- mythtv/libs/libmyth/backendselect.h | 20 +++++---- mythtv/libs/libmyth/mythcontext.cpp | 59 ++++++++++++--------------- 3 files changed, 55 insertions(+), 59 deletions(-) diff --git a/mythtv/libs/libmyth/backendselect.cpp b/mythtv/libs/libmyth/backendselect.cpp index 309a7a3a8cf..1d010e6a16e 100644 --- a/mythtv/libs/libmyth/backendselect.cpp +++ b/mythtv/libs/libmyth/backendselect.cpp @@ -37,31 +37,28 @@ BackendSelection::~BackendSelection() m_devices.clear(); } -bool BackendSelection::m_backendChanged = false; - -bool BackendSelection::prompt(DatabaseParams *dbParams, - Configuration *pConfig) +BackendSelection::Decision BackendSelection::Prompt( + DatabaseParams *dbParams, Configuration *pConfig) { - m_backendChanged = false; - + Decision ret = kCancelConfigure; MythScreenStack *mainStack = GetMythMainWindow()->GetMainStack(); if (!mainStack) - return false; + return ret; - BackendSelection *backendSettings = new BackendSelection(mainStack, - dbParams, - pConfig, true); + BackendSelection *backendSettings = + new BackendSelection(mainStack, dbParams, pConfig, true); if (backendSettings->Create()) { mainStack->AddScreen(backendSettings, false); qApp->exec(); + ret = backendSettings->m_backendDecision; mainStack->PopScreen(backendSettings, false); } else delete backendSettings; - return m_backendChanged; + return ret; } bool BackendSelection::Create(void) @@ -97,7 +94,7 @@ void BackendSelection::Accept(MythUIButtonListItem *item) DeviceLocation *dev = qVariantValue(item->GetData()); if (!dev) - Close(); + Cancel(); if (ConnectBackend(dev)) // this does a Release() { @@ -108,7 +105,7 @@ void BackendSelection::Accept(MythUIButtonListItem *item) m_pConfig->SetValue(kDefaultUSN, m_USN); m_pConfig->Save(); } - Close(); + Close(kAcceptConfigure); } } @@ -231,12 +228,12 @@ bool BackendSelection::ConnectBackend(DeviceLocation *dev) return false; } -void BackendSelection::Cancel() +void BackendSelection::Cancel(void) { - Close(); + Close(kCancelConfigure); } -void BackendSelection::Load() +void BackendSelection::Load(void) { SSDP::Instance()->AddListener(this); SSDP::Instance()->PerformSearch(gBackendURI); @@ -259,7 +256,7 @@ void BackendSelection::Init(void) void BackendSelection::Manual(void) { - Close(); + Close(kManualConfigure); } void BackendSelection::RemoveItem(QString USN) @@ -361,8 +358,10 @@ void BackendSelection::PromptForPassword(void) delete pwDialog; } -void BackendSelection::Close(void) +void BackendSelection::Close(Decision d) { + m_backendDecision = d; + if (m_exitOnFinish) qApp->quit(); else diff --git a/mythtv/libs/libmyth/backendselect.h b/mythtv/libs/libmyth/backendselect.h index c96fdb9c3dd..6a8bae1d317 100644 --- a/mythtv/libs/libmyth/backendselect.h +++ b/mythtv/libs/libmyth/backendselect.h @@ -39,6 +39,15 @@ class BackendSelection : public MythScreenType Q_OBJECT public: + typedef enum Decision + { + kManualConfigure = -1, + kCancelConfigure = 0, + kAcceptConfigure = +1, + } BackendDecision; + static Decision Prompt( + DatabaseParams *dbParams, Configuration *pConfig); + BackendSelection(MythScreenStack *parent, DatabaseParams *params, Configuration *pConfig, bool exitOnFinish = false); virtual ~BackendSelection(); @@ -46,17 +55,11 @@ class BackendSelection : public MythScreenType bool Create(void); void customEvent(QEvent *event); - static bool prompt(DatabaseParams *dbParams, Configuration *pConfig); - - signals: -// void - - public slots: + protected slots: void Accept(void); void Accept(MythUIButtonListItem *); void Manual(void); ///< Linked to 'Configure Manually' button void Cancel(void); ///< Linked to 'Cancel' button - void Close(void); private: void Load(void); @@ -66,6 +69,7 @@ class BackendSelection : public MythScreenType void RemoveItem(QString URN); bool TryDBfromURL(const QString &error, QString URL); void PromptForPassword(void); + void Close(Decision); DatabaseParams *m_DBparams; Configuration *m_pConfig; @@ -83,7 +87,7 @@ class BackendSelection : public MythScreenType QMutex m_mutex; - static bool m_backendChanged; + BackendDecision m_backendDecision; }; Q_DECLARE_METATYPE(DeviceLocation*) diff --git a/mythtv/libs/libmyth/mythcontext.cpp b/mythtv/libs/libmyth/mythcontext.cpp index d452c286646..9f012cf8129 100644 --- a/mythtv/libs/libmyth/mythcontext.cpp +++ b/mythtv/libs/libmyth/mythcontext.cpp @@ -383,45 +383,37 @@ bool MythContextPrivate::FindDatabase(bool prompt, bool noAutodetect) manualSelect &= m_gui; // no interactive command-line chooser yet - // Last, get the user to select a backend from a possible list: - if (manualSelect) + // Queries the user for the DB info + do { - switch (ChooseBackend(QString::null)) + if (manualSelect) { - case -1: // User asked to configure database manually - if (PromptForDatabaseParams("")) + // Get the user to select a backend from a possible list: + BackendSelection::Decision d = (BackendSelection::Decision) + ChooseBackend(failure); + switch (d) + { + case BackendSelection::kAcceptConfigure: break; - else - goto NoDBfound; // User cancelled - changed their mind? - - case 0: // User cancelled. Exit application - goto NoDBfound; - - case 1: // User selected a backend, so m_DBparams - break; // should now contain the database details - - default: - goto NoDBfound; + case BackendSelection::kManualConfigure: + manualSelect = false; + break; + case BackendSelection::kCancelConfigure: + goto NoDBfound; + } } - failure = TestDBconnection(); - } - - // Queries the user for the DB info, using the command - // line or the GUI depending on the application. - while (!failure.isEmpty()) - { - LOG(VB_GENERAL, LOG_ALERT, failure); - if (( manualSelect && ChooseBackend(failure)) || - (!manualSelect && PromptForDatabaseParams(failure))) + if (!manualSelect) { - failure = TestDBconnection(); - if (failure.length()) - LOG(VB_GENERAL, LOG_ALERT, failure); + if (!PromptForDatabaseParams(failure)) + goto NoDBfound; } - else - goto NoDBfound; + + failure = TestDBconnection(); + if (!failure.isEmpty()) + LOG(VB_GENERAL, LOG_ALERT, failure); } + while (!failure.isEmpty()); DBfound: LOG(VB_GENERAL, LOG_DEBUG, "FindDatabase() - Success!"); @@ -780,11 +772,12 @@ int MythContextPrivate::ChooseBackend(const QString &error) LOG(VB_GENERAL, LOG_INFO, "Putting up the UPnP backend chooser"); - BackendSelection::prompt(&m_DBparams, m_pConfig); + BackendSelection::Decision ret = + BackendSelection::Prompt(&m_DBparams, m_pConfig); EndTempWindow(); - return 1; + return (int)ret; } /** From 406dcfad485dd9050aef9c7eb7f68ef6eb0a05cb Mon Sep 17 00:00:00 2001 From: Daniel Kristjansson Date: Thu, 3 May 2012 10:56:07 -0400 Subject: [PATCH 69/69] Make DB setup strings translable and put them in the same context. --- mythtv/libs/libmyth/mythcontext.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/mythtv/libs/libmyth/mythcontext.cpp b/mythtv/libs/libmyth/mythcontext.cpp index 9f012cf8129..2c36d442ea4 100644 --- a/mythtv/libs/libmyth/mythcontext.cpp +++ b/mythtv/libs/libmyth/mythcontext.cpp @@ -368,7 +368,7 @@ bool MythContextPrivate::FindDatabase(bool prompt, bool noAutodetect) int count = UPnPautoconf(); if (count == 0) - failure = "No UPnP backends found"; + failure = QObject::tr("No UPnP backends found", "Backend Setup"); if (count == 1) { @@ -682,7 +682,9 @@ QString MythContextPrivate::TestDBconnection(void) if (doPing && !ping(host, 3)) // Fail after trying for 3 seconds { SilenceDBerrors(); - err = QObject::tr("Cannot find (ping) database host %1 on the network"); + err = QObject::tr( + "Cannot find (ping) database host %1 on the network", + "Backend Setup"); return err.arg(host); } @@ -695,7 +697,7 @@ QString MythContextPrivate::TestDBconnection(void) if (!MSqlQuery::testDBConnection()) { SilenceDBerrors(); - return QObject::tr("Cannot login to database?"); + return QObject::tr("Cannot login to database", "Backend Setup"); }