diff --git a/.github/workflows/buildfixes31.yml b/.github/workflows/buildfixes31.yml new file mode 100644 index 0000000000..b230dac000 --- /dev/null +++ b/.github/workflows/buildfixes31.yml @@ -0,0 +1,85 @@ +name: fixes/31 + +on: + push: + branches: [ fixes/31 ] + pull_request: + branches: [ fixes/31 ] + +jobs: + build: + name: build + strategy: + matrix: + os: ['ubuntu-18.04'] + cc: ['gcc', 'clang'] + include: + - cc: 'gcc' + cxx: 'g++' + - cc: 'clang' + cxx: 'clang++' + fail-fast: false + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout fixes/31 + uses: actions/checkout@v2 + + - name: Setup build environment + run: echo "MYTHTV_CONFIG=--prefix=${{ github.workspace }}/build/install --cc=${{ matrix.cc }} --cxx=${{ matrix.cxx }}" >> $GITHUB_ENV + + - name: Check ccache + uses: actions/cache@v2 + with: + path: ~/.ccache + key: ${{ matrix.os }}-${{ matrix.cc }}-ccache-${{ github.sha }} + restore-keys: ${{ matrix.os }}-${{ matrix.cc }}-ccache + + # N.B. These dependencies are for the fixes/31 branch. The list is intended to provide as much code coverage as possible (i.e. enable as many options as possible) + - name: Install core dependencies (linux) + run: | + sudo apt update + sudo apt install ccache qt5-qmake qtscript5-dev nasm libsystemd-dev libfreetype6-dev libmp3lame-dev libx264-dev libx265-dev libxrandr-dev libxml2-dev libavahi-compat-libdnssd-dev libasound2-dev liblzo2-dev libhdhomerun-dev libsamplerate0-dev libva-dev libdrm-dev libvdpau-dev libass-dev libpulse-dev libcec-dev libfftw3-dev libssl-dev libtag1-dev libbluray-dev libbluray-bdj libgnutls28-dev libqt5webkit5-dev libvpx-dev python3-mysqldb python3-lxml python3-simplejson python3-future libdbi-perl libdbd-mysql-perl libnet-upnp-perl libio-socket-inet6-perl libxml-simple-perl libqt5sql5-mysql libxxf86vm-dev libxinerama-dev libexiv2-dev + if: runner.os == 'Linux' + + - name: Install core dependencies (macOS) + run: | + brew install pkg-config ccache qt5 nasm libsamplerate taglib lzo libcec libbluray fftw libass libhdhomerun dav1d x264 x265 libvpx openssl exiv2 + brew link qt5 --force + echo "MYTHTV_CONFIG=$MYTHTV_CONFIG --extra-cxxflags=-I/usr/local/include --extra-ldflags=-L/usr/local/lib" >> $GITHUB_ENV + if: runner.os == 'macOS' + + - name: Configure core + working-directory: ./mythtv + run: ./configure $MYTHTV_CONFIG --enable-libmp3lame --enable-libvpx --enable-libx264 --enable-libx265 --enable-bdjava + + - name: Make core + working-directory: ./mythtv + run: make all_no_test -j4 + + - name: Install core + working-directory: ./mythtv + run: make install + + # QTest requires a QT SQL plugin - but there are currently none available via brew on macOS + - name: Unit test core + working-directory: ./mythtv + run: make test + if: runner.os == 'Linux' + + - name: Install plugin dependencies (linux) + run: sudo apt install libvorbis-dev libflac++-dev libminizip-dev libcdio-dev libcdio-paranoia-dev python3-oauth python3-pycurl + libxml-xpath-perl libdate-manip-perl libdatetime-format-iso8601-perl libsoap-lite-perl libjson-perl libimage-size-perl + if: runner.os == 'Linux' + + - name: Install plugin dependencies (macOS) + run: brew install minizip flac libvorbis libcdio + if: runner.os == 'macOS' + + - name: Configure plugins + working-directory: ./mythplugins + run: ./configure $MYTHTV_CONFIG + + - name: Make plugins + working-directory: ./mythplugins + run: make -j4 diff --git a/mythplugins/mytharchive/mytharchive/dbcheck.cpp b/mythplugins/mytharchive/mytharchive/dbcheck.cpp index 4bc4b4de57..9f3cc0ae40 100644 --- a/mythplugins/mytharchive/mytharchive/dbcheck.cpp +++ b/mythplugins/mytharchive/mytharchive/dbcheck.cpp @@ -143,12 +143,12 @@ bool UpgradeArchiveDatabaseSchema(void) QString("ALTER DATABASE %1 DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;") .arg(gContext->GetDatabaseParams().m_dbName), "ALTER TABLE archiveitems" - " DEFAULT CHARACTER SET default," - " MODIFY title varchar(128) CHARACTER SET utf8 default NULL," - " MODIFY subtitle varchar(128) CHARACTER SET utf8 default NULL," + " DEFAULT CHARACTER SET utf8," + " MODIFY title varchar(128) CHARACTER SET utf8 NULL," + " MODIFY subtitle varchar(128) CHARACTER SET utf8 NULL," " MODIFY description text CHARACTER SET utf8," - " MODIFY startdate varchar(30) CHARACTER SET utf8 default NULL," - " MODIFY starttime varchar(30) CHARACTER SET utf8 default NULL," + " MODIFY startdate varchar(30) CHARACTER SET utf8 NULL," + " MODIFY starttime varchar(30) CHARACTER SET utf8 NULL," " MODIFY filename text CHARACTER SET utf8 NOT NULL," " MODIFY cutlist text CHARACTER SET utf8;", "" diff --git a/mythplugins/mytharchive/mythburn/scripts/mythburn.py b/mythplugins/mytharchive/mythburn/scripts/mythburn.py index 371e32bd37..87e09c3920 100755 --- a/mythplugins/mytharchive/mythburn/scripts/mythburn.py +++ b/mythplugins/mytharchive/mythburn/scripts/mythburn.py @@ -269,16 +269,16 @@ class FontDef(object): self.font = None def getFont(self): - if self.font == None: + if self.font is None: self.font = ImageFont.truetype(self.fontFile, int(self.size)) return self.font def drawText(self, text, color=None): - if self.font == None: + if self.font is None: self.font = ImageFont.truetype(self.fontFile, int(self.size)) - if color == None: + if color is None: color = self.color textwidth, textheight = self.font.getsize(text) @@ -1170,7 +1170,7 @@ def paintText(draw, image, text, node, color = None, """Takes a piece of text and draws it onto an image inside a bounding box.""" #The text is wider than the width of the bounding box - if x == None: + if x is None: x = getScaledAttribute(node, "x") y = getScaledAttribute(node, "y") width = getScaledAttribute(node, "w") @@ -1178,7 +1178,7 @@ def paintText(draw, image, text, node, color = None, font = themeFonts[node.attributes["font"].value] - if color == None: + if color is None: if node.hasAttribute("colour"): color = node.attributes["colour"].value elif node.hasAttribute("color"): @@ -3498,7 +3498,7 @@ def drawThemeItem(page, itemsonthispage, itemnum, menuitem, bgimage, draw, else: write( "Dont know how to process %s" % node.nodeName) - if drawmask == None: + if drawmask is None: return #Draw the selection mask for this item @@ -3685,7 +3685,7 @@ def createMenu(screensize, screendpi, numberofitems): picture = Image.open(imagefile, "r").resize((previeww[itemsonthispage-1], previewh[itemsonthispage-1])) picture = picture.convert("RGBA") imagemaskfile = os.path.join(previewpath, "mask-i%d.png" % itemsonthispage) - if previewmask[itemsonthispage-1] != None: + if previewmask[itemsonthispage-1] is not None: bgimage.paste(picture, (previewx[itemsonthispage-1], previewy[itemsonthispage-1]), previewmask[itemsonthispage-1]) else: bgimage.paste(picture, (previewx[itemsonthispage-1], previewy[itemsonthispage-1])) @@ -3886,7 +3886,7 @@ def createChapterMenu(screensize, screendpi, numberofitems): picture = Image.open(imagefile, "r").resize((previeww[previewchapter], previewh[previewchapter])) picture = picture.convert("RGBA") imagemaskfile = os.path.join(previewpath, "mask-i%d.png" % previewchapter) - if previewmask[previewchapter] != None: + if previewmask[previewchapter] is not None: bgimage.paste(picture, (previewx[previewchapter], previewy[previewchapter]), previewmask[previewchapter]) else: bgimage.paste(picture, (previewx[previewchapter], previewy[previewchapter])) @@ -4035,7 +4035,7 @@ def createDetailsPage(screensize, screendpi, numberofitems): picture = Image.open(imagefile, "r").resize((previeww, previewh)) picture = picture.convert("RGBA") imagemaskfile = os.path.join(previewpath, "mask-i%d.png" % 1) - if previewmask != None: + if previewmask is not None: bgimage.paste(picture, (previewx, previewy), previewmask) else: bgimage.paste(picture, (previewx, previewy)) diff --git a/mythplugins/mythgame/mythgame/dbcheck.cpp b/mythplugins/mythgame/mythgame/dbcheck.cpp index dc7de5972c..47f22fe68e 100644 --- a/mythplugins/mythgame/mythgame/dbcheck.cpp +++ b/mythplugins/mythgame/mythgame/dbcheck.cpp @@ -343,7 +343,7 @@ QString("ALTER DATABASE %1 DEFAULT CHARACTER SET latin1;") QString("ALTER DATABASE %1 DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;") .arg(gContext->GetDatabaseParams().m_dbName), "ALTER TABLE gamemetadata" -" DEFAULT CHARACTER SET default," +" DEFAULT CHARACTER SET utf8," " MODIFY `system` varchar(128) CHARACTER SET utf8 NOT NULL default ''," " MODIFY romname varchar(128) CHARACTER SET utf8 NOT NULL default ''," " MODIFY gamename varchar(128) CHARACTER SET utf8 NOT NULL default ''," @@ -356,7 +356,7 @@ QString("ALTER DATABASE %1 DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;") " MODIFY crc_value varchar(64) CHARACTER SET utf8 NOT NULL default ''," " MODIFY version varchar(64) CHARACTER SET utf8 NOT NULL default '';", "ALTER TABLE gameplayers" -" DEFAULT CHARACTER SET default," +" DEFAULT CHARACTER SET utf8," " MODIFY playername varchar(64) CHARACTER SET utf8 NOT NULL default ''," " MODIFY workingpath varchar(255) CHARACTER SET utf8 NOT NULL default ''," " MODIFY rompath varchar(255) CHARACTER SET utf8 NOT NULL default ''," @@ -365,7 +365,7 @@ QString("ALTER DATABASE %1 DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;") " MODIFY gametype varchar(64) CHARACTER SET utf8 NOT NULL default ''," " MODIFY extensions varchar(128) CHARACTER SET utf8 NOT NULL default '';", "ALTER TABLE romdb" -" DEFAULT CHARACTER SET default," +" DEFAULT CHARACTER SET utf8," " MODIFY crc varchar(64) CHARACTER SET utf8 NOT NULL default ''," " MODIFY name varchar(128) CHARACTER SET utf8 NOT NULL default ''," " MODIFY description varchar(128) CHARACTER SET utf8 NOT NULL default ''," diff --git a/mythplugins/mythgame/mythgame/gameui.cpp b/mythplugins/mythgame/mythgame/gameui.cpp index 2146fac000..71e48750fe 100644 --- a/mythplugins/mythgame/mythgame/gameui.cpp +++ b/mythplugins/mythgame/mythgame/gameui.cpp @@ -112,7 +112,7 @@ void GameUI::BuildTree() { QString system = GameHandler::getHandler(i)->SystemName(); if (i == 0) - systemFilter = "system in ('" + system + "'"; + systemFilter = "`system` in ('" + system + "'"; else systemFilter += ",'" + system + "'"; } @@ -655,7 +655,7 @@ QString GameUI::getFillSql(MythGenericTree *node) const if ((childLevel == "gamename") && (m_gameShowFileName)) { columns = childIsLeaf - ? "romname,system,year,genre,gamename" + ? "romname,`system`,year,genre,gamename" : "romname"; if (m_showHashed) @@ -665,7 +665,7 @@ QString GameUI::getFillSql(MythGenericTree *node) const else if ((childLevel == "gamename") && (layer.length() == 1)) { columns = childIsLeaf - ? childLevel + ",system,year,genre,gamename" + ? childLevel + ",`system`,year,genre,gamename" : childLevel; if (m_showHashed) @@ -680,7 +680,7 @@ QString GameUI::getFillSql(MythGenericTree *node) const { columns = childIsLeaf - ? childLevel + ",system,year,genre,gamename" + ? childLevel + ",`system`,year,genre,gamename" : childLevel; } diff --git a/mythplugins/mythgame/mythgame/scripts/giantbomb/giantbomb_api.py b/mythplugins/mythgame/mythgame/scripts/giantbomb/giantbomb_api.py index d0d8d00254..2a03a78c62 100644 --- a/mythplugins/mythgame/mythgame/scripts/giantbomb/giantbomb_api.py +++ b/mythplugins/mythgame/mythgame/scripts/giantbomb/giantbomb_api.py @@ -167,7 +167,7 @@ class gamedbQueries(): def textUtf8(self, text): - if text == None: + if text is None: return text try: return unicode(text, 'utf8') @@ -268,15 +268,15 @@ class gamedbQueries(): return If there is not enough information to make a date then return an empty string ''' try: - if gameElement.find('expected_release_year').text != None: + if gameElement.find('expected_release_year').text is not None: year = gameElement.find('expected_release_year').text else: year = None - if gameElement.find('expected_release_quarter').text != None: + if gameElement.find('expected_release_quarter').text is not None: quarter = gameElement.find('expected_release_quarter').text else: quarter = None - if gameElement.find('expected_release_month').text != None: + if gameElement.find('expected_release_month').text is not None: month = gameElement.find('expected_release_month').text else: month = None @@ -416,7 +416,7 @@ class gamedbQueries(): items = queryXslt(queryResult) - if items.getroot() != None: + if items.getroot() is not None: if len(items.xpath('//item')): sys.stdout.write(etree.tostring(items, encoding='UTF-8', method="xml", xml_declaration=True, pretty_print=True, )) sys.exit(0) @@ -446,7 +446,7 @@ class gamedbQueries(): gamebombXpath[key] = self.FuncDict[key] items = gameXslt(videoResult) - if items.getroot() != None: + if items.getroot() is not None: if len(items.xpath('//item')): sys.stdout.write(etree.tostring(items, encoding='UTF-8', method="xml", xml_declaration=True, pretty_print=True, )) sys.exit(0) diff --git a/mythplugins/mythmusic/mythmusic/dbcheck.cpp b/mythplugins/mythmusic/mythmusic/dbcheck.cpp index eef679d1c0..17c1766635 100644 --- a/mythplugins/mythmusic/mythmusic/dbcheck.cpp +++ b/mythplugins/mythmusic/mythmusic/dbcheck.cpp @@ -788,49 +788,49 @@ static bool doUpgradeMusicDatabaseSchema(QString &dbver) .arg(gContext->GetDatabaseParams().m_dbName), // NOLINTNEXTLINE(bugprone-suspicious-missing-comma) "ALTER TABLE music_albumart" - " DEFAULT CHARACTER SET default," + " DEFAULT CHARACTER SEt utf8," " MODIFY filename varchar(255) CHARACTER SET utf8 NOT NULL default '';", "ALTER TABLE music_albums" - " DEFAULT CHARACTER SET default," + " DEFAULT CHARACTER SEt utf8," " MODIFY album_name varchar(255) CHARACTER SET utf8 NOT NULL default '';", "ALTER TABLE music_artists" - " DEFAULT CHARACTER SET default," + " DEFAULT CHARACTER SEt utf8," " MODIFY artist_name varchar(255) CHARACTER SET utf8 NOT NULL default '';", "ALTER TABLE music_directories" - " DEFAULT CHARACTER SET default," + " DEFAULT CHARACTER SEt utf8," " MODIFY path text CHARACTER SET utf8 NOT NULL;", "ALTER TABLE music_genres" - " DEFAULT CHARACTER SET default," + " DEFAULT CHARACTER SEt utf8," " MODIFY genre varchar(255) CHARACTER SET utf8 NOT NULL default '';", "ALTER TABLE music_playlists" - " DEFAULT CHARACTER SET default," + " DEFAULT CHARACTER SEt utf8," " MODIFY playlist_name varchar(255) CHARACTER SET utf8 NOT NULL default ''," " MODIFY playlist_songs text CHARACTER SET utf8 NOT NULL," " MODIFY hostname varchar(64) CHARACTER SET utf8 NOT NULL default '';", "ALTER TABLE music_smartplaylist_categories" - " DEFAULT CHARACTER SET default," + " DEFAULT CHARACTER SEt utf8," " MODIFY name varchar(128) CHARACTER SET utf8 NOT NULL;", "ALTER TABLE music_smartplaylist_items" - " DEFAULT CHARACTER SET default," + " DEFAULT CHARACTER SEt utf8," " MODIFY field varchar(50) CHARACTER SET utf8 NOT NULL," " MODIFY operator varchar(20) CHARACTER SET utf8 NOT NULL," " MODIFY value1 varchar(255) CHARACTER SET utf8 NOT NULL," " MODIFY value2 varchar(255) CHARACTER SET utf8 NOT NULL;", "ALTER TABLE music_smartplaylists" - " DEFAULT CHARACTER SET default," + " DEFAULT CHARACTER SEt utf8," " MODIFY name varchar(128) CHARACTER SET utf8 NOT NULL," " MODIFY orderby varchar(128) CHARACTER SET utf8 NOT NULL default '';", "ALTER TABLE music_songs" - " DEFAULT CHARACTER SET default," + " DEFAULT CHARACTER SEt utf8," " MODIFY filename text CHARACTER SET utf8 NOT NULL," " MODIFY name varchar(255) CHARACTER SET utf8 NOT NULL default ''," " MODIFY format varchar(4) CHARACTER SET utf8 NOT NULL default '0'," - " MODIFY mythdigest varchar(255) CHARACTER SET utf8 default NULL," - " MODIFY description varchar(255) CHARACTER SET utf8 default NULL," - " MODIFY comment varchar(255) CHARACTER SET utf8 default NULL," - " MODIFY eq_preset varchar(255) CHARACTER SET utf8 default NULL;", + " MODIFY mythdigest varchar(255) CHARACTER SET utf8 NULL," + " MODIFY description varchar(255) CHARACTER SET utf8 NULL," + " MODIFY comment varchar(255) CHARACTER SET utf8 NULL," + " MODIFY eq_preset varchar(255) CHARACTER SET utf8 NULL;", "ALTER TABLE music_stats" - " DEFAULT CHARACTER SET default," + " DEFAULT CHARACTER SEt utf8," " MODIFY total_time varchar(12) CHARACTER SET utf8 NOT NULL default '0'," " MODIFY total_size varchar(10) CHARACTER SET utf8 NOT NULL default '0';", "" diff --git a/mythplugins/mythmusic/mythmusic/playlist.cpp b/mythplugins/mythmusic/mythmusic/playlist.cpp index f0ca201e07..86addc845b 100644 --- a/mythplugins/mythmusic/mythmusic/playlist.cpp +++ b/mythplugins/mythmusic/mythmusic/playlist.cpp @@ -386,8 +386,10 @@ void Playlist::shuffleTracks(MusicPlayer::ShuffleMode shuffleMode) } else { - album_order = Ialbum->second * 1000; + album_order = Ialbum->second * 10000; } + if (mdata->DiscNumber() != -1) + album_order += mdata->DiscNumber()*100; album_order += mdata->Track(); songMap.insert(album_order, m_songs.at(x)); diff --git a/mythplugins/mythweather/mythweather/dbcheck.cpp b/mythplugins/mythweather/mythweather/dbcheck.cpp index 0e5a053ace..50ac049bec 100644 --- a/mythplugins/mythweather/mythweather/dbcheck.cpp +++ b/mythplugins/mythweather/mythweather/dbcheck.cpp @@ -165,21 +165,21 @@ bool InitializeDatabase() updates << QString("ALTER DATABASE %1 DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;") .arg(gContext->GetDatabaseParams().m_dbName) << "ALTER TABLE weatherdatalayout" - " DEFAULT CHARACTER SET default," + " DEFAULT CHARACTER SET utf8," " MODIFY location varchar(64) CHARACTER SET utf8 NOT NULL," " MODIFY dataitem varchar(64) CHARACTER SET utf8 NOT NULL;" << "ALTER TABLE weatherscreens" - " DEFAULT CHARACTER SET default," + " DEFAULT CHARACTER SET utf8," " MODIFY container varchar(64) CHARACTER SET utf8 NOT NULL," - " MODIFY hostname varchar(64) CHARACTER SET utf8 default NULL;" << + " MODIFY hostname varchar(64) CHARACTER SET utf8 NULL;" << "ALTER TABLE weathersourcesettings" - " DEFAULT CHARACTER SET default," + " DEFAULT CHARACTER SET utf8," " MODIFY source_name varchar(64) CHARACTER SET utf8 NOT NULL," - " MODIFY hostname varchar(64) CHARACTER SET utf8 default NULL," - " MODIFY path varchar(255) CHARACTER SET utf8 default NULL," - " MODIFY author varchar(128) CHARACTER SET utf8 default NULL," - " MODIFY version varchar(32) CHARACTER SET utf8 default NULL," - " MODIFY email varchar(255) CHARACTER SET utf8 default NULL," + " MODIFY hostname varchar(64) CHARACTER SET utf8 NULL," + " MODIFY path varchar(255) CHARACTER SET utf8 NULL," + " MODIFY author varchar(128) CHARACTER SET utf8 NULL," + " MODIFY version varchar(32) CHARACTER SET utf8 NULL," + " MODIFY email varchar(255) CHARACTER SET utf8 NULL," " MODIFY types mediumtext CHARACTER SET utf8;"; if (!performActualUpdate(updates, "1003", dbver)) diff --git a/mythtv/FAQ b/mythtv/FAQ index 593d3b9fea..e4f6c45109 100644 --- a/mythtv/FAQ +++ b/mythtv/FAQ @@ -1,5 +1,5 @@ MythTV FAQ The FAQ is available on the MythTV wiki at -http://www.mythtv.org/wiki/Frequently_Asked_Questions +https://www.mythtv.org/wiki/Frequently_Asked_Questions diff --git a/mythtv/bindings/python/MythTV/_conn_mysqldb.py b/mythtv/bindings/python/MythTV/_conn_mysqldb.py index 177a880a12..6f50433036 100644 --- a/mythtv/bindings/python/MythTV/_conn_mysqldb.py +++ b/mythtv/bindings/python/MythTV/_conn_mysqldb.py @@ -24,6 +24,7 @@ def dbconnect(dbconn, log): use_unicode=True, charset='utf8') db.autocommit(True) + db.set_sql_mode("") # reset default sql_mode return db class LoggedCursor( MySQLdb.cursors.Cursor ): @@ -41,7 +42,11 @@ class LoggedCursor( MySQLdb.cursors.Cursor ): def _ping121(self): self._get_db().ping() def _ping122(self): self._get_db().ping(True) - def _sanitize(self, query): return query.replace('?', '%s') + def _sanitize(self, query): + if isinstance(query, bytearray): + # MySQLdb calls execute() as bytearrays, already sanitized + return query + return query.replace('?', '%s') def log_query(self, query, args): if isinstance(query, bytearray): diff --git a/mythtv/bindings/python/MythTV/altdict.py b/mythtv/bindings/python/MythTV/altdict.py index a90e9c3baa..90b4fd606d 100644 --- a/mythtv/bindings/python/MythTV/altdict.py +++ b/mythtv/bindings/python/MythTV/altdict.py @@ -107,7 +107,7 @@ class DictData( OrdDict ): lambda x: datetime.fromRfc(x, datetime.UTCTZ())\ .astimezone(datetime.localTZ())] _inv_trans = [ str, - lambda x: locale.format("%0.6f", x), + lambda x: locale.format_string("%0.6f", x), lambda x: str(int(x)), lambda x: x, lambda x: str(int(x.timestamp())), diff --git a/mythtv/bindings/python/MythTV/database.py b/mythtv/bindings/python/MythTV/database.py index 8629441432..e8b8bf5d52 100644 --- a/mythtv/bindings/python/MythTV/database.py +++ b/mythtv/bindings/python/MythTV/database.py @@ -1167,7 +1167,7 @@ class DBCache( MythSchema ): self._db = db self._host = host self._log = log - if host is 'NULL': + if host == 'NULL': self._insert = """INSERT INTO settings (value, data, hostname) VALUES (?, ?, NULL)""" @@ -1338,15 +1338,7 @@ class DBCache( MythSchema ): def _gethostfromaddr(self, addr, value=None): if value is None: - for value in ['BackendServerAddr']: - try: - return self._gethostfromaddr(addr, value) - except MythDBError: - pass - else: - raise MythDBError(MythError.DB_SETTING, - 'BackendServerAddr', addr) - + value = 'BackendServerAddr' with self as cursor: if cursor.execute("""SELECT hostname FROM settings WHERE value=? AND data=?""", [value, addr]) == 0: @@ -1360,7 +1352,7 @@ class DBCache( MythSchema ): return self.dbconfig.profile def getMasterBackend(self): - return self._gethostfromaddr(self.settings.NULL.MasterServerIP) + return self.settings.NULL.MasterServerName def getStorageGroup(self, groupname=None, hostname=None): """ diff --git a/mythtv/bindings/python/MythTV/dataheap.py b/mythtv/bindings/python/MythTV/dataheap.py index a50c762734..bfb84d10dc 100644 --- a/mythtv/bindings/python/MythTV/dataheap.py +++ b/mythtv/bindings/python/MythTV/dataheap.py @@ -15,7 +15,13 @@ from MythTV.utility import CMPRecord, CMPVideo, MARKUPLIST, datetime, ParseSet,\ import re import locale -import xml.etree.cElementTree as etree + +# TODO: if Python 3.3+ is in use by all distributions, use ElementTree only. +try: + import xml.etree.cElementTree as etree +except ImportError: + import xml.etree.ElementTree as etree + from datetime import date, time _default_datetime = datetime(1900,1,1, tzinfo=datetime.UTCTZ()) diff --git a/mythtv/bindings/python/MythTV/methodheap.py b/mythtv/bindings/python/MythTV/methodheap.py index 04c7e98bb4..404864618c 100644 --- a/mythtv/bindings/python/MythTV/methodheap.py +++ b/mythtv/bindings/python/MythTV/methodheap.py @@ -8,7 +8,7 @@ from MythTV.static import * from MythTV.exceptions import * from MythTV.logging import MythLog from MythTV.connections import FEConnection, XMLConnection, BEEventConnection -from MythTV.utility import databaseSearch, datetime, check_ipv6, _donothing +from MythTV.utility import databaseSearch, datetime, check_ipv6, _donothing, resolve_ip from MythTV.database import DBCache, DBData from MythTV.system import SystemEvent from MythTV.mythproto import BECache, FileOps, Program, FreeSpace, EventLock @@ -1131,6 +1131,8 @@ class MythVideo( MythDB ): class MythXML( XMLConnection ): """ Provides convenient methods to access the backend XML server. + Parameter 'backend' is either a hostname from 'settings', + an ip address or a hostname in ip-notation. """ def __init__(self, backend=None, port=None, db=None): if backend and port: @@ -1142,24 +1144,28 @@ class MythXML( XMLConnection ): self.log = MythLog('Python XML Connection') if backend is None: # use master backend - backend = self.db.settings.NULL.MasterServerIP - if re.match(r'(?:\d{1,3}\.){3}\d{1,3}',backend) or \ - check_ipv6(backend): - # process ip address - host = self.db._gethostfromaddr(backend) - self.host = backend - self.port = int(self.db.settings[host].BackendStatusPort) + backend = self.db.getMasterBackend() + + # assume hostname from settings + host = self.db._getpreferredaddr(backend) + if host: + port = int(self.db.settings[backend].BackendStatusPort) else: - # assume given a hostname - self.host = backend - self.port = int(self.db.settings[self.host].BackendStatusPort) - if not self.port: - # try a truncated hostname - self.host = backend.split('.')[0] - self.port = int(self.db.setting[self.host].BackendStatusPort) - if not self.port: - raise MythDBError(MythError.DB_SETTING, - backend+': BackendStatusPort') + # assume ip address + hostname = self.db._gethostfromaddr(backend) + host = backend + port = int(self.db.settings[hostname].BackendStatusPort) + + # resolve ip address from name + reshost, resport = resolve_ip(host,port) + if not reshost: + raise MythDBError(MythError.DB_SETTING, + backend+': BackendServerAddr') + if not resport: + raise MythDBError(MythError.DB_SETTING, + backend+': BackendStatusPort') + self.host = host + self.port = port def getHosts(self): """Returns a list of unique hostnames found in the settings table.""" diff --git a/mythtv/bindings/python/MythTV/mythproto.py b/mythtv/bindings/python/MythTV/mythproto.py index b388e9619f..a89d50f6ef 100644 --- a/mythtv/bindings/python/MythTV/mythproto.py +++ b/mythtv/bindings/python/MythTV/mythproto.py @@ -12,7 +12,7 @@ from MythTV.altdict import DictData from MythTV.connections import BEConnection, BEEventConnection from MythTV.database import DBCache from MythTV.utility import CMPRecord, datetime, ParseEnum, \ - CopyData, CopyData2, check_ipv6, py23_repr + CopyData, CopyData2, check_ipv6, py23_repr, resolve_ip from datetime import date from time import sleep @@ -75,32 +75,33 @@ class BECache( object ): self.receiveevents = events if backend is None: - # no backend given, use master - self.host = self.db.settings.NULL.MasterServerIP - self.hostname = self.db._gethostfromaddr(self.host) - + # use master backend + backend = self.db.getMasterBackend() else: backend = backend.strip('[]') - if self._reip.match(backend): - # given backend is IP address - self.host = backend - self.hostname = self.db._gethostfromaddr( - backend, 'BackendServerAddr') - elif check_ipv6(backend): - # given backend is IPv6 address - self.host = backend - self.hostname = self.db._gethostfromaddr( - backend, 'BackendServerAddr') - else: - # given backend is hostname, pull address from database - self.hostname = backend - self.host = self.db._getpreferredaddr(backend) - # lookup port from database - self.port = int(self.db.settings[self.hostname].BackendServerPort) - if not self.port: - raise MythDBError(MythError.DB_SETTING, 'BackendServerPort', - self.port) + # assume backend is hostname from settings + host = self.db._getpreferredaddr(backend) + if host: + port = int(self.db.settings[backend].BackendServerPort) + self.hostname = backend + else: + # assume ip address + self.hostname = self.db._gethostfromaddr(backend) + host = backend + port = int(self.db.settings[self.hostname].BackendServerPort) + + # resolve ip address from name + reshost, resport = resolve_ip(host,port) + if not reshost: + raise MythDBError(MythError.DB_SETTING, + backend+': BackendServerAddr') + if not resport: + raise MythDBError(MythError.DB_SETTING, + backend+': BackendServerPort') + + self.host = host + self.port = port self._ident = '%s:%d' % (self.host, self.port) if self._ident in self._shared: @@ -241,9 +242,11 @@ def ftopen(file, mode, forceremote=False, nooverwrite=False, db=None, \ else: raise MythError('Invalid FileTransfer input string: '+file) - # get full system name + # prefer hostname from settings over IP address host = host.strip('[]') - if reip.match(host) or check_ipv6(host): + if ( not db._getpreferredaddr(host) and \ + resolve_ip(host, None)[0] ): + # host is either IPv4, IPv6 or an (aliased) name host = db._gethostfromaddr(host) # select the correct transfer function: @@ -285,7 +288,7 @@ def ftopen(file, mode, forceremote=False, nooverwrite=False, db=None, \ for sg in sgs: if sg.dirname in path: if sg.local: - return open(sg.dirname+filename, mode) + return open(os.path.join(sg.dirname, filename), mode+'b') else: return protoopen(host, filename, sgroup) @@ -301,12 +304,12 @@ def ftopen(file, mode, forceremote=False, nooverwrite=False, db=None, \ sg = sorted(sgs, key=lambda sg: sg.free, reverse=True)[0] # create folder if it does not exist if filename.find('/') != -1: - path = sg.dirname+filename.rsplit('/',1)[0] + path = os.path.join(sg.dirname, filename.rsplit('/',1)[0]) if not os.access(path, os.F_OK): os.makedirs(path) - log(log.FILE, log.INFO, 'Opening local file (w)', - sg.dirname+filename) - return open(sg.dirname+filename, mode) + log(log.FILE, log.INFO, 'Opening local file (wb)', + os.path.join(sg.dirname, filename)) + return open(os.path.join(sg.dirname, filename), mode+'b') # fallback to remote write else: @@ -319,9 +322,9 @@ def ftopen(file, mode, forceremote=False, nooverwrite=False, db=None, \ sg = findfile(filename, sgroup, db) if sg is not None: # file found, open local - log(log.FILE, log.INFO, 'Opening local file (r)', - sg.dirname+filename) - return open(sg.dirname+filename, mode) + log(log.FILE, log.INFO, 'Opening local file (rb)', + os.path.join(sg.dirname, filename)) + return open(os.path.join(sg.dirname, filename), mode+'b') else: # file not found, open remote return protoopen(host, filename, sgroup) diff --git a/mythtv/bindings/python/MythTV/services_api/send.py b/mythtv/bindings/python/MythTV/services_api/send.py index 1f49389508..fa817066f2 100644 --- a/mythtv/bindings/python/MythTV/services_api/send.py +++ b/mythtv/bindings/python/MythTV/services_api/send.py @@ -141,6 +141,10 @@ class Send(object): its response in XML rather than JSON. Defaults to False. + opts['rawxml']: If True, causes the backend to send it's response in + XML as bytes. This can be easily parsed by Python's + 'lxml.etree.fromstring()'. Defaults to False. + opts['wrmi']: If True and there is postdata, the URL is then sent to the server. @@ -296,6 +300,9 @@ class Send(object): if self.opts['usexml']: return response.text + if self.opts['rawxml']: + return response.content + try: return response.json() except ValueError as err: @@ -320,7 +327,7 @@ class Send(object): if not isinstance(self.opts, dict): self.opts = {} - for option in ('noetag', 'nogzip', 'usexml', 'wrmi', 'wsdl'): + for option in ('noetag', 'nogzip', 'usexml', 'rawxml', 'wrmi', 'wsdl'): try: self.opts[option] except (KeyError, TypeError): @@ -368,8 +375,8 @@ class Send(object): raise RuntimeError('usage: postdata must be passed as a dict') self.logger.debug('The following postdata was included:') - for key in self.postdata: - self.logger.debug('%15s: %s', key, self.postdata[key]) + for k, v in self.postdata.items(): + self.logger.debug('%15s: %s', k, v) if not self.opts['wrmi']: raise RuntimeWarning('wrmi=False') @@ -396,7 +403,7 @@ class Send(object): else: self.session.headers.update({'Accept-Encoding': 'gzip,deflate'}) - if self.opts['usexml']: + if self.opts['usexml'] or self.opts['rawxml']: self.session.headers.update({'Accept': ''}) else: self.session.headers.update({'Accept': 'application/json'}) diff --git a/mythtv/bindings/python/MythTV/static.py b/mythtv/bindings/python/MythTV/static.py index 167d71377a..1f9e1203f6 100644 --- a/mythtv/bindings/python/MythTV/static.py +++ b/mythtv/bindings/python/MythTV/static.py @@ -119,6 +119,8 @@ class JOBTYPE( object ): SYSTEMJOB = 0x00ff TRANSCODE = 0x0001 COMMFLAG = 0x0002 + METADATA = 0x0004 + PREVIEW = 0x0008 USERJOB = 0xff00 USERJOB1 = 0x0100 USERJOB2 = 0x0200 diff --git a/mythtv/bindings/python/MythTV/system.py b/mythtv/bindings/python/MythTV/system.py index d1d7546c0e..3f4f5e2196 100644 --- a/mythtv/bindings/python/MythTV/system.py +++ b/mythtv/bindings/python/MythTV/system.py @@ -131,7 +131,7 @@ class System( DBCache ): stderr will be available in the exception and this object as attributes 'returncode' and 'stderr'. """ - if self.path is '': + if self.path == '': return '' cmd = '%s %s' % (self.path, ' '.join(['%s' % a for a in args])) return self._runcmd(cmd) @@ -165,7 +165,7 @@ class System( DBCache ): self.path += ' '+' '.join(['%s' % a for a in args]) def _runasync(self, *args): - if self.path is '': + if self.path == '': return '' cmd = '%s %s' % (self.path, ' '.join(['%s' % a for a in args])) return self.Process(cmd, self.useshell, self.log) @@ -390,9 +390,10 @@ class Grabber( System ): return sorted(self.search(phrase, subtitle, tolerance), \ key=lambda r: r.levenshtein) - def grabInetref(self, inetref, season=None, episode=None): + def grabInetref(self, inetref, season=None, episode=None, search_collection=False): """ - obj.grabInetref(inetref, season=None, episode=None) -> metadata object + obj.grabInetref(inetref, season=None, episode=None, search_collection=False) + -> metadata object Returns a direct search for a specific movie or episode. 'inetref' can be an existing VideoMetadata object, and @@ -411,7 +412,10 @@ class Grabber( System ): # inetref may expand to "my_grabber_script.xyz_1234" or "9876" args = list(args) args[0] = args[0].split("_")[-1] - return next(self.command('-D', *args)) + if search_collection: + return next(self.command('-C', *args)) + else: + return next(self.command('-D', *args)) class SystemEvent( System ): """ @@ -441,7 +445,7 @@ class SystemEvent( System ): stderr will be available in the exception and this object as attributes 'returncode' and 'stderr'. """ - if self.path is '': + if self.path == '': return cmd = self.path if 'program' in eventdata: diff --git a/mythtv/bindings/python/MythTV/ttvdb/XSLT/tvdbQuery.xsl b/mythtv/bindings/python/MythTV/ttvdb/XSLT/tvdbQuery.xsl index bf7ad06a82..ee6c8ade81 100644 --- a/mythtv/bindings/python/MythTV/ttvdb/XSLT/tvdbQuery.xsl +++ b/mythtv/bindings/python/MythTV/ttvdb/XSLT/tvdbQuery.xsl @@ -48,22 +48,22 @@ <xsl:if test=".//poster/text() != ''"> <xsl:element name="image"> <xsl:attribute name="type">coverart</xsl:attribute> - <xsl:attribute name="url"><xsl:value-of select="concat('http://www.thetvdb.com/banners/', normalize-space(poster))"/></xsl:attribute> - <xsl:attribute name="thumb"><xsl:value-of select="concat('http://www.thetvdb.com/banners/_cache/', normalize-space(poster))"/></xsl:attribute> + <xsl:attribute name="url"><xsl:value-of select="concat('http://www.thetvdb.com', normalize-space(poster))"/></xsl:attribute> + <xsl:attribute name="thumb"><xsl:value-of select="tvdbXpath:replace(concat('http://www.thetvdb.com', normalize-space(poster)), '/banners/', '/banners/_cache/')"/></xsl:attribute> </xsl:element> </xsl:if> <xsl:if test=".//fanart/text() != ''"> <xsl:element name="image"> <xsl:attribute name="type">fanart</xsl:attribute> - <xsl:attribute name="url"><xsl:value-of select="concat('http://www.thetvdb.com/banners/', normalize-space(fanart))"/></xsl:attribute> - <xsl:attribute name="thumb"><xsl:value-of select="concat('http://www.thetvdb.com/banners/_cache/', normalize-space(fanart))"/></xsl:attribute> + <xsl:attribute name="url"><xsl:value-of select="concat('http://www.thetvdb.com', normalize-space(fanart))"/></xsl:attribute> + <xsl:attribute name="thumb"><xsl:value-of select="tvdbXpath:replace(concat('http://www.thetvdb.com', normalize-space(fanart)), '/banners/', '/banners/_cache/')"/></xsl:attribute> </xsl:element> </xsl:if> <xsl:if test=".//banner/text() != ''"> <xsl:element name="image"> <xsl:attribute name="type">banner</xsl:attribute> - <xsl:attribute name="url"><xsl:value-of select="concat('http://www.thetvdb.com/banners/', normalize-space(banner))"/></xsl:attribute> - <xsl:attribute name="thumb"><xsl:value-of select="concat('http://www.thetvdb.com/banners/_cache/', normalize-space(banner))"/></xsl:attribute> + <xsl:attribute name="url"><xsl:value-of select="concat('http://www.thetvdb.com', normalize-space(banner))"/></xsl:attribute> + <xsl:attribute name="thumb"><xsl:value-of select="tvdbXpath:replace(concat('http://www.thetvdb.com', normalize-space(banner)), '/banners/', '/banners/_cache/')"/></xsl:attribute> </xsl:element> </xsl:if> </images> diff --git a/mythtv/bindings/python/MythTV/ttvdb/tvdbXslt.py b/mythtv/bindings/python/MythTV/ttvdb/tvdbXslt.py index f4dc9ca107..8af7851fd7 100644 --- a/mythtv/bindings/python/MythTV/ttvdb/tvdbXslt.py +++ b/mythtv/bindings/python/MythTV/ttvdb/tvdbXslt.py @@ -218,7 +218,7 @@ class xpathFunctions(object): for image in xpathFilter(args[0][0]): # print("im %r" % image) # print(etree.tostring(image, method="xml", xml_declaration=False, pretty_print=True, )) - if image.find('fileName') == None: + if image.find('fileName') is None: continue # print("im2 %r" % image) tmpElement = etree.XML(u'<image></image>') @@ -242,7 +242,7 @@ class xpathFunctions(object): # end imageElements() def textUtf8(self, text): - if text == None: + if text is None: return text try: return unicode(text, 'utf8') diff --git a/mythtv/bindings/python/MythTV/ttvdb/tvdb_api.py b/mythtv/bindings/python/MythTV/ttvdb/tvdb_api.py index 01b13c7ebc..6ce90adc16 100644 --- a/mythtv/bindings/python/MythTV/ttvdb/tvdb_api.py +++ b/mythtv/bindings/python/MythTV/ttvdb/tvdb_api.py @@ -1081,7 +1081,7 @@ class Tvdb: self._setShowData(sid, tag, value) # set language - if language == None: + if language is None: language = self.config['language'] self._setShowData(sid, u'language', language) diff --git a/mythtv/bindings/python/MythTV/utility/__init__.py b/mythtv/bindings/python/MythTV/utility/__init__.py index 4f8d060a23..1ca7087d7b 100644 --- a/mythtv/bindings/python/MythTV/utility/__init__.py +++ b/mythtv/bindings/python/MythTV/utility/__init__.py @@ -7,5 +7,6 @@ from .altdict import OrdDict, DictInvert, DictInvertCI from .other import _donothing, SchemaUpdate, databaseSearch, deadlinesocket, \ MARKUPLIST, levenshtein, ParseEnum, ParseSet, CopyData, \ - CopyData2, check_ipv6, QuickProperty, py23_str, py23_repr + CopyData2, check_ipv6, QuickProperty, py23_str, py23_repr, \ + resolve_ip diff --git a/mythtv/bindings/python/MythTV/utility/dequebuffer.py b/mythtv/bindings/python/MythTV/utility/dequebuffer.py index 650ac609d6..df4b537ced 100644 --- a/mythtv/bindings/python/MythTV/utility/dequebuffer.py +++ b/mythtv/bindings/python/MythTV/utility/dequebuffer.py @@ -354,7 +354,7 @@ class DequeBuffer( object ): # get IO mode from pipe mode = pipe.mode - if (cls._pollingthread is None) or not cls._pollingthread.isAlive(): + if (cls._pollingthread is None) or not cls._pollingthread.is_alive(): # create new thread, and set it to not block shutdown cls._pollingthread = _PollingThread() cls._pollingthread.daemon = True diff --git a/mythtv/bindings/python/MythTV/utility/dt.py b/mythtv/bindings/python/MythTV/utility/dt.py index ef61749a56..97ef75a243 100644 --- a/mythtv/bindings/python/MythTV/utility/dt.py +++ b/mythtv/bindings/python/MythTV/utility/dt.py @@ -475,10 +475,11 @@ class datetime( _pydatetime ): return self.astimezone(self.UTCTZ()).strftime('%Y%m%d%H%M%S') def timestamp(self): - # utc time = local time - utc offset - utc_naive = self.replace(tzinfo=None) - self.utcoffset() - utc_epoch = self.utcfromtimestamp(0).replace(tzinfo=None) - return ((utc_naive - utc_epoch).total_seconds()) + # utc time = local time - utc offset + utc_naive = self.replace(tzinfo=None) - self.utcoffset() + utc_naive = utc_naive.replace(tzinfo=None) + utc_epoch = self.utcfromtimestamp(0).replace(tzinfo=None) + return ((utc_naive - utc_epoch).total_seconds()) def rfcformat(self): return self.strftime('%a, %d %b %Y %H:%M:%S %z') diff --git a/mythtv/bindings/python/MythTV/utility/other.py b/mythtv/bindings/python/MythTV/utility/other.py index bb8f29630d..7f5b0c759c 100644 --- a/mythtv/bindings/python/MythTV/utility/other.py +++ b/mythtv/bindings/python/MythTV/utility/other.py @@ -576,6 +576,15 @@ def check_ipv6(n): except socket.error: return False +def resolve_ip(host, port): + try: + res = socket.getaddrinfo(host, port, socket.AF_UNSPEC, socket.SOCK_STREAM)[0] + # (family, socktype, proto, canonname, sockaddr) + af, socktype, proto, canonname, sa = res + return(sa[0], sa[1]) + except: + return (None, None) + def py23_str(value, ignore_errors=False): error_methods = ('strict', 'ignore') error_method = error_methods[ignore_errors] diff --git a/mythtv/bindings/python/tmdb3/tmdb3/cache_file.py b/mythtv/bindings/python/tmdb3/tmdb3/cache_file.py index e2f6165ac8..f847005146 100644 --- a/mythtv/bindings/python/tmdb3/tmdb3/cache_file.py +++ b/mythtv/bindings/python/tmdb3/tmdb3/cache_file.py @@ -384,7 +384,7 @@ class FileEngine( CacheEngine ): # write storage slot definitions prev = None for d in data: - if prev == None: + if prev is None: d.position = 4 + 16*size else: d.position = prev.position + prev.size diff --git a/mythtv/configure b/mythtv/configure index 77aee2d076..3a7ec61fd7 100755 --- a/mythtv/configure +++ b/mythtv/configure @@ -154,6 +154,7 @@ Advanced options (experts only): directory with parser.h [$libxml2_path_default] --disable-libdns-sd disable DNS Service Discovery (Bonjour/Zeroconf/Avahi) --disable-libcrypto disable use of the OpenSSL cryptographic library + --disable-gnutls disable use of GnuTLS for SSL/TLS protocol support in ffmpeg --with-bindings=LIST install the bindings specified in the comma-separated list @@ -1967,6 +1968,7 @@ MYTHTV_CONFIG_LIST=' joystick_menu libcec libcrypto + gnutls libdns_sd libfftw3 libmpeg2external @@ -2758,6 +2760,7 @@ enable libaom enable libass enable libcec enable libcrypto +enable gnutls enable libdav1d enable libdns_sd enable libxml2 @@ -6642,6 +6645,12 @@ fi enabled libcrypto && check_lib crypto openssl/rsa.h RSA_new -lcrypto || disable libcrypto +if enabled gnutls ; then + if ! $(pkg-config --exists gnutls) ; then + disable gnutls + fi +fi + if test $target_os != darwin ; then enabled libdns_sd && check_lib dns_sd dns_sd.h DNSServiceRegister -ldns_sd || disable libdns_sd fi @@ -7225,6 +7234,10 @@ if enabled libdav1d; then ffopts="$ffopts --enable-libdav1d" fi +if enabled gnutls; then + ffopts="$ffopts --enable-gnutls" +fi + ffmpeg_extra_cflags="$extra_cflags -w" ## Call FFmpeg configure here @@ -7448,6 +7461,7 @@ if enabled frontend; then fi echo "libdns_sd (Bonjour) ${libdns_sd-no}" echo "libcrypto ${libcrypto-no}" +echo "gnutls ${gnutls-no}" if enabled libbluray_external; then echo "bluray support yes (system)" else diff --git a/mythtv/contrib/imports/mirobridge/mirobridge.py b/mythtv/contrib/imports/mirobridge/mirobridge.py index f9417f4ff4..a7f87ffec0 100755 --- a/mythtv/contrib/imports/mirobridge/mirobridge.py +++ b/mythtv/contrib/imports/mirobridge/mirobridge.py @@ -537,7 +537,7 @@ def _can_int(x): >>> _can_int("A test") False """ - if x == None: + if x is None: return False try: int(x) @@ -571,7 +571,7 @@ def sanitiseFileName(name): return a sanitised valid file name ''' global filename_char_filter - if name == None or name == u'': + if name is None or name == u'': return u'_' for char in filename_char_filter: name = name.replace(char, u'_') @@ -793,7 +793,7 @@ def rtnAbsolutePath(relpath, filetype=u'mythvideo'): return an absolute path and file name return the relpath sting if the file does not actually exist in the absolute path location ''' - if relpath == None or relpath == u'': + if relpath is None or relpath == u'': return relpath # There is a chance that this is already an absolute path @@ -1264,7 +1264,7 @@ def getStartEndTimes(duration, downloadedTime): starttime.strftime('%Y-%m-%d %H:%M:%S'), starttime.strftime('%Y%m%d%H%M%S')] - if downloadedTime != None: + if downloadedTime is not None: try: dummy = downloadedTime.strftime('%Y-%m-%d') except ValueError: @@ -1416,7 +1416,7 @@ def createRecordedRecords(item): ffmpeg_details = metadata.getVideoDetails(item[u'videoFilename']) start_end = getStartEndTimes(ffmpeg_details[u'duration'], item[u'downloadedTime']) - if item[u'releasedate'] == None: + if item[u'releasedate'] is None: item[u'releasedate'] = item[u'downloadedTime'] try: dummy = item[u'releasedate'].strftime('%Y-%m-%d') @@ -1444,12 +1444,12 @@ def createRecordedRecords(item): tmp_recorded[u'hostname'] = localhostname tmp_recorded[u'lastmodified'] = tmp_recorded[u'endtime'] tmp_recorded[u'filesize'] = item[u'size'] - if item[u'releasedate'] != None: + if item[u'releasedate'] is not None: tmp_recorded[u'originalairdate'] = item[u'releasedate'].strftime('%Y-%m-%d') basename = setSymbolic(item[u'videoFilename'], u'default', u"%s_%s" % \ (channel_id, start_end[2]), allow_symlink=True) - if basename != None: + if basename is not None: tmp_recorded[u'basename'] = basename else: logger.critical(u"The file (%s) must exist to create a recorded record" % \ @@ -1472,7 +1472,7 @@ def createRecordedRecords(item): tmp_recordedprogram[u'category'] = u"Miro" tmp_recordedprogram[u'category_type'] = u"series" - if item[u'releasedate'] != None: + if item[u'releasedate'] is not None: tmp_recordedprogram[u'airdate'] = item[u'releasedate'].strftime('%Y') tmp_recordedprogram[u'originalairdate'] = item[u'releasedate'].strftime('%Y-%m-%d') tmp_recordedprogram[u'stereo'] = ffmpeg_details[u'stereo'] @@ -1524,14 +1524,14 @@ def createVideometadataRecord(item): for key in details.keys(): videometadata[key] = details[key] - if item[u'releasedate'] == None: + if item[u'releasedate'] is None: item[u'releasedate'] = item[u'downloadedTime'] try: dummy = item[u'releasedate'].strftime('%Y-%m-%d') except ValueError: item[u'releasedate'] = item[u'downloadedTime'] - if item[u'releasedate'] != None: + if item[u'releasedate'] is not None: videometadata[u'year'] = item[u'releasedate'].strftime('%Y') videometadata[u'releasedate'] = item[u'releasedate'].strftime('%Y-%m-%d') videometadata[u'length'] = ffmpeg_details[u'duration']/60 @@ -1544,7 +1544,7 @@ def createVideometadataRecord(item): videofile = setSymbolic(item[u'videoFilename'], u'mythvideo', "%s/%s - %s" % \ (sympath, sanitiseFileName(item[u'channelTitle']), sanitiseFileName(item[u'title'])), allow_symlink=True) - if videofile != None: + if videofile is not None: videometadata[u'filename'] = videofile if not local_only and videometadata[u'filename'][0] != u'/': videometadata[u'host'] = localhostname.lower() @@ -1565,14 +1565,14 @@ def createVideometadataRecord(item): elif item[u'channel_icon'] and not item[u'channelTitle'].lower() in channel_icon_override: filename = setSymbolic(item[u'channel_icon'], u'posterdir', u"%s" % \ (sanitiseFileName(item[u'channelTitle']))) - if filename != None: + if filename is not None: videometadata[u'coverfile'] = filename else: if item[u'item_icon']: filename = setSymbolic(item[u'item_icon'], u'posterdir', u"%s - %s" % \ (sanitiseFileName(item[u'channelTitle']), sanitiseFileName(item[u'title']))) - if filename != None: + if filename is not None: videometadata[u'coverfile'] = filename else: videometadata[u'coverfile'] = item[u'channel_icon'] @@ -1582,7 +1582,7 @@ def createVideometadataRecord(item): filename = setSymbolic(item[u'screenshot'], u'episodeimagedir', u"%s - %s" % \ (sanitiseFileName(item[u'channelTitle']), sanitiseFileName(item[u'title']))) - if filename != None: + if filename is not None: videometadata[u'screenshot'] = filename else: if item[u'screenshot']: @@ -1818,7 +1818,7 @@ def updateMythRecorded(items): # Add new Miro unwatched videos to MythTV'd data base for item in items_copy: # Do not create records for Miro video files when Miro has a corrupt or missing file name - if item[u'videoFilename'] == None: + if item[u'videoFilename'] is None: continue # Do not create records for Miro video files that do not exist if not os.path.isfile(os.path.realpath(item[u'videoFilename'])): @@ -2021,7 +2021,7 @@ def updateMythVideo(items): result = takeScreenShot(item[u'videoFilename'], screenshot_mythvideo, size_limit=False) except: result = None - if result != None: + if result is not None: item[u'screenshot'] = screenshot_mythvideo tmp_array = createVideometadataRecord(item) videometadata = tmp_array[0] diff --git a/mythtv/contrib/imports/mirobridge/mirobridge/metadata.py b/mythtv/contrib/imports/mirobridge/mirobridge/metadata.py index 4e0c882d2c..76cbe02529 100644 --- a/mythtv/contrib/imports/mirobridge/mirobridge/metadata.py +++ b/mythtv/contrib/imports/mirobridge/mirobridge/metadata.py @@ -169,7 +169,7 @@ class MetaData(object): # If there is no Record rule then check ttvdb.com if not len(recordedRules_array): inetref = self.searchTvdb(title) - if inetref != None: # Create a new rule for this Miro Channel title + if inetref is not None: # Create a new rule for this Miro Channel title ttvdbGraphics['inetref'] = inetref self.makeRecordRule['title'] = title self.makeRecordRule['inetref'] = inetref diff --git a/mythtv/contrib/imports/mirobridge/mirobridge/mirobridge_interpreter_4_0_2.py b/mythtv/contrib/imports/mirobridge/mirobridge/mirobridge_interpreter_4_0_2.py index 3061903e4f..966d184677 100644 --- a/mythtv/contrib/imports/mirobridge/mirobridge/mirobridge_interpreter_4_0_2.py +++ b/mythtv/contrib/imports/mirobridge/mirobridge/mirobridge_interpreter_4_0_2.py @@ -280,7 +280,7 @@ class MiroInterpreter(cmd.Cmd): continue # Any item without a proper file name needs to be removed as Miro metadata is corrupt - if it.get_filename() == None: + if it.get_filename() is None: it.expire() self.statistics[u'Miro_videos_deleted']+=1 logging.info(u'Unwatched video (%s) has been removed from Miro as item had no valid file name' % it.get_title()) @@ -314,7 +314,7 @@ class MiroInterpreter(cmd.Cmd): continue # Any item without a proper file name needs to be removed as Miro metadata is corrupt - if it.get_filename() == None: + if it.get_filename() is None: it.expire() self.statistics[u'Miro_videos_deleted']+=1 logging.info(u'Watched video (%s) has been removed from Miro as item had no valid file name' % it.get_title()) diff --git a/mythtv/contrib/imports/mirobridge/mirobridge/mirobridge_interpreter_6_0_0.py b/mythtv/contrib/imports/mirobridge/mirobridge/mirobridge_interpreter_6_0_0.py index 1a5a6d9e78..2282722e97 100644 --- a/mythtv/contrib/imports/mirobridge/mirobridge/mirobridge_interpreter_6_0_0.py +++ b/mythtv/contrib/imports/mirobridge/mirobridge/mirobridge_interpreter_6_0_0.py @@ -292,7 +292,7 @@ class MiroInterpreter(cmd.Cmd): # Any item without a proper file name needs to be removed # as Miro metadata is corrupt - if it.get_filename() == None: + if it.get_filename() is None: it.expire() self.statistics[u'Miro_videos_deleted']+=1 logging.info( @@ -327,7 +327,7 @@ u'Unwatched video (%s) has been removed from Miro as item had no valid file name continue # Any item without a proper file name needs to be removed as Miro metadata is corrupt - if it.get_filename() == None: + if it.get_filename() is None: it.expire() self.statistics[u'Miro_videos_deleted']+=1 logging.info( diff --git a/mythtv/external/FFmpeg/libavcodec/vdpau.c b/mythtv/external/FFmpeg/libavcodec/vdpau.c index 167f06d7ae..2e7e8d757c 100644 --- a/mythtv/external/FFmpeg/libavcodec/vdpau.c +++ b/mythtv/external/FFmpeg/libavcodec/vdpau.c @@ -243,7 +243,9 @@ int ff_vdpau_common_init(AVCodecContext *avctx, VdpDecoderProfile profile, status = decoder_query_caps(vdctx->device, profile, &supported, &max_level, &max_mb, &max_width, &max_height); #ifdef VDP_DECODER_PROFILE_H264_CONSTRAINED_BASELINE - if ((status != VDP_STATUS_OK || supported != VDP_TRUE) && profile == VDP_DECODER_PROFILE_H264_CONSTRAINED_BASELINE) { + if ((status != VDP_STATUS_OK || supported != VDP_TRUE) && + (profile == VDP_DECODER_PROFILE_H264_CONSTRAINED_BASELINE || + profile == VDP_DECODER_PROFILE_H264_BASELINE)) { profile = VDP_DECODER_PROFILE_H264_MAIN; status = decoder_query_caps(vdctx->device, profile, &supported, &max_level, &max_mb, diff --git a/mythtv/i18n/mythfrontend_en_us.qm b/mythtv/i18n/mythfrontend_en_us.qm index 22d06341fa..1658b5c5ff 100644 Binary files a/mythtv/i18n/mythfrontend_en_us.qm and b/mythtv/i18n/mythfrontend_en_us.qm differ diff --git a/mythtv/i18n/mythfrontend_en_us.ts b/mythtv/i18n/mythfrontend_en_us.ts index a1ff6bc172..2c02d7ffc8 100644 --- a/mythtv/i18n/mythfrontend_en_us.ts +++ b/mythtv/i18n/mythfrontend_en_us.ts @@ -12365,7 +12365,7 @@ Error: %1</translation> <location filename="../libs/libmythbase/mythsorthelper.cpp" line="16"/> <source>^(The |A |An )</source> <comment>Regular Expression for what to ignore when sorting</comment> - <translation>^(The |A |An)</translation> + <translation>^(The |A |An )</translation> </message> </context> <context> diff --git a/mythtv/libs/libmyth/audio/audioconvert.cpp b/mythtv/libs/libmyth/audio/audioconvert.cpp index bb8e0be5fc..8dd3efdc7d 100644 --- a/mythtv/libs/libmyth/audio/audioconvert.cpp +++ b/mythtv/libs/libmyth/audio/audioconvert.cpp @@ -142,6 +142,7 @@ static int toFloat8(float* out, const uchar* in, int len) "jnz 1b \n\t" :"+r"(out),"+r"(in) :"c"(loops), "r"(a), "r"(f) + :"xmm0","xmm1","xmm2","xmm3","xmm4","xmm5","xmm6","xmm7" ); } #endif //ARCH_x86 @@ -204,6 +205,7 @@ static int fromFloat8(uchar* out, const float* in, int len) "jnz 1b \n\t" :"+r"(out),"+r"(in) :"c"(loops), "r"(a), "r"(f) + :"xmm0","xmm1","xmm2","xmm3","xmm4","xmm7" ); } #endif //ARCH_x86 @@ -258,6 +260,7 @@ static int toFloat16(float* out, const short* in, int len) "jnz 1b \n\t" :"+r"(out),"+r"(in) :"c"(loops), "r"(f) + :"xmm1","xmm2","xmm3","xmm4","xmm5","xmm6","xmm7" ); } #endif //ARCH_x86 @@ -311,6 +314,7 @@ static int fromFloat16(short* out, const float* in, int len) "jnz 1b \n\t" :"+r"(out),"+r"(in) :"c"(loops), "r"(f) + :"xmm1","xmm2","xmm3","xmm4","xmm7" ); } #endif //ARCH_x86 @@ -367,6 +371,7 @@ static int toFloat32(AudioFormat format, float* out, const int* in, int len) "jnz 1b \n\t" :"+r"(out),"+r"(in) :"c"(loops), "r"(f), "r"(shift) + :"xmm1","xmm2","xmm3","xmm4","xmm6","xmm7" ); } #endif //ARCH_x86 @@ -439,6 +444,7 @@ static int fromFloat32(AudioFormat format, int* out, const float* in, int len) "jnz 1b \n\t" :"+r"(out), "+r"(in) :"c"(loops), "r"(f), "m"(o), "m"(mo), "r"(shift) + :"xmm0","xmm1","xmm2","xmm3","xmm4","xmm5","xmm6","xmm7" ); } #endif //ARCH_x86 @@ -504,6 +510,7 @@ static int fromFloatFLT(float* out, const float* in, int len) "jnz 1b \n\t" :"+r"(out), "+r"(in) :"c"(loops), "m"(o), "m"(mo) + :"xmm1","xmm2","xmm3","xmm4","xmm6","xmm7" ); } #endif //ARCH_x86 diff --git a/mythtv/libs/libmyth/audio/audiooutputca.cpp b/mythtv/libs/libmyth/audio/audiooutputca.cpp index e5f52aa4f5..8193d3b5b6 100644 --- a/mythtv/libs/libmyth/audio/audiooutputca.cpp +++ b/mythtv/libs/libmyth/audio/audiooutputca.cpp @@ -12,6 +12,8 @@ * Jeremiah Morris, Andrew Kimpton, Nigel Pearson, Jean-Yves Avenard *****************************************************************************/ +#include <vector> + #include <CoreServices/CoreServices.h> #include <CoreAudio/CoreAudio.h> #include <AudioUnit/AudioUnit.h> @@ -27,6 +29,9 @@ #define CHANNELS_MIN 1 #define CHANNELS_MAX 8 +using AudioStreamIDVec = std::vector<AudioStreamID>; +using AudioStreamRangedVec = std::vector<AudioStreamRangedDescription>; + #define OSS_STATUS(x) UInt32ToFourCC((UInt32*)&(x)) char* UInt32ToFourCC(UInt32* pVal) { @@ -112,8 +117,8 @@ public: bool *ChannelsList(AudioDeviceID d, bool passthru); // Helpers for iterating. Returns a malloc'd array - AudioStreamID *StreamsList(AudioDeviceID d); - AudioStreamBasicDescription *FormatsList(AudioStreamID s); + AudioStreamIDVec StreamsList(AudioDeviceID d); + AudioStreamRangedVec FormatsList(AudioStreamID s); int AudioStreamChangeFormat(AudioStreamID s, AudioStreamBasicDescription format); @@ -148,6 +153,7 @@ public: bool mInitialized {false}; bool mStarted {false}; bool mWasDigital {false}; + AudioDeviceIOProcID mIoProcID {}; }; // These callbacks communicate with Core Audio. @@ -531,12 +537,28 @@ AudioDeviceID CoreAudioData::GetDeviceWithName(const QString &deviceName) { UInt32 size = 0; AudioDeviceID deviceID = 0; + AudioObjectPropertyAddress pa + { + kAudioHardwarePropertyDevices, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster + }; + + OSStatus err = AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &pa, + 0, nullptr, &size); + if (err) + { + Warn(QString("GetPropertyDataSize: Unable to retrieve the property sizes. " + "Error [%1]") + .arg(err)); + return deviceID; + } - AudioHardwareGetPropertyInfo(kAudioHardwarePropertyDevices, &size, nullptr); UInt32 deviceCount = size / sizeof(AudioDeviceID); AudioDeviceID* pDevices = new AudioDeviceID[deviceCount]; - OSStatus err = AudioHardwareGetProperty(kAudioHardwarePropertyDevices, &size, pDevices); + err = AudioObjectGetPropertyData(kAudioObjectSystemObject, &pa, + 0, nullptr, &size, pDevices); if (err) { Warn(QString("GetDeviceWithName: Unable to retrieve the list of available devices. " @@ -568,13 +590,18 @@ AudioDeviceID CoreAudioData::GetDeviceWithName(const QString &deviceName) AudioDeviceID CoreAudioData::GetDefaultOutputDevice() { UInt32 paramSize; - OSStatus err; AudioDeviceID deviceId = 0; + AudioObjectPropertyAddress pa + { + kAudioHardwarePropertyDefaultOutputDevice, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster + }; // Find the ID of the default Device paramSize = sizeof(deviceId); - err = AudioHardwareGetProperty(kAudioHardwarePropertyDefaultOutputDevice, - ¶mSize, &deviceId); + OSStatus err = AudioObjectGetPropertyData(kAudioObjectSystemObject, &pa, + 0, nullptr, ¶mSize, &deviceId); if (err == noErr) Debug(QString("GetDefaultOutputDevice: default device ID = %1").arg(deviceId)); else @@ -592,13 +619,27 @@ int CoreAudioData::GetTotalOutputChannels() return 0; UInt32 channels = 0; UInt32 size = 0; - AudioDeviceGetPropertyInfo(mDeviceID, 0, false, - kAudioDevicePropertyStreamConfiguration, - &size, nullptr); + AudioObjectPropertyAddress pa + { + kAudioDevicePropertyStreamConfiguration, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster + }; + + OSStatus err = AudioObjectGetPropertyDataSize(mDeviceID, &pa, + 0, nullptr, &size); + if (err) + { + Warn(QString("GetTotalOutputChannels: Unable to get " + "size of device output channels - id: %1 Error = [%2]") + .arg(mDeviceID) + .arg(err)); + return 0; + } + AudioBufferList *pList = (AudioBufferList *)malloc(size); - OSStatus err = AudioDeviceGetProperty(mDeviceID, 0, false, - kAudioDevicePropertyStreamConfiguration, - &size, pList); + err = AudioObjectGetPropertyData(mDeviceID, &pa, + 0, nullptr, &size, pList); if (!err) { for (UInt32 buffer = 0; buffer < pList->mNumberBuffers; buffer++) @@ -621,15 +662,17 @@ QString *CoreAudioData::GetName() { if (!mDeviceID) return nullptr; - UInt32 propertySize; - AudioObjectPropertyAddress propertyAddress; + + AudioObjectPropertyAddress pa + { + kAudioObjectPropertyName, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster + }; CFStringRef name; - propertySize = sizeof(CFStringRef); - propertyAddress.mSelector = kAudioObjectPropertyName; - propertyAddress.mScope = kAudioObjectPropertyScopeGlobal; - propertyAddress.mElement = kAudioObjectPropertyElementMaster; - OSStatus err = AudioObjectGetPropertyData(mDeviceID, &propertyAddress, + UInt32 propertySize = sizeof(CFStringRef); + OSStatus err = AudioObjectGetPropertyData(mDeviceID, &pa, 0, nullptr, &propertySize, &name); if (err) { @@ -648,8 +691,14 @@ bool CoreAudioData::GetAutoHogMode() { UInt32 val = 0; UInt32 size = sizeof(val); - OSStatus err = AudioHardwareGetProperty(kAudioHardwarePropertyHogModeIsAllowed, - &size, &val); + AudioObjectPropertyAddress pa + { + kAudioHardwarePropertyHogModeIsAllowed, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster + }; + + OSStatus err = AudioObjectGetPropertyData(kAudioObjectSystemObject, &pa, 0, nullptr, &size, &val); if (err) { Warn(QString("GetAutoHogMode: Unable to get auto 'hog' mode. Error = [%1]") @@ -662,8 +711,15 @@ bool CoreAudioData::GetAutoHogMode() void CoreAudioData::SetAutoHogMode(bool enable) { UInt32 val = enable ? 1 : 0; - OSStatus err = AudioHardwareSetProperty(kAudioHardwarePropertyHogModeIsAllowed, - sizeof(val), &val); + AudioObjectPropertyAddress pa + { + kAudioHardwarePropertyHogModeIsAllowed, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster + }; + + OSStatus err = AudioObjectSetPropertyData(kAudioObjectSystemObject, &pa, 0, nullptr, + sizeof(val), &val); if (err) { Warn(QString("SetAutoHogMode: Unable to set auto 'hog' mode. Error = [%1]") @@ -673,12 +729,16 @@ void CoreAudioData::SetAutoHogMode(bool enable) pid_t CoreAudioData::GetHogStatus() { - OSStatus err; pid_t PID; UInt32 PIDsize = sizeof(PID); + AudioObjectPropertyAddress pa + { + kAudioDevicePropertyHogMode, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster + }; - err = AudioDeviceGetProperty(mDeviceID, 0, FALSE, - kAudioDevicePropertyHogMode, + OSStatus err = AudioObjectGetPropertyData(kAudioObjectSystemObject, &pa, 0, nullptr, &PIDsize, &PID); if (err != noErr) { @@ -693,6 +753,13 @@ pid_t CoreAudioData::GetHogStatus() bool CoreAudioData::SetHogStatus(bool hog) { + AudioObjectPropertyAddress pa + { + kAudioDevicePropertyHogMode, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster + }; + // According to Jeff Moore (Core Audio, Apple), Setting kAudioDevicePropertyHogMode // is a toggle and the only way to tell if you do get hog mode is to compare // the returned pid against getpid, if the match, you have hog mode, if not you don't. @@ -705,9 +772,8 @@ bool CoreAudioData::SetHogStatus(bool hog) { Debug(QString("SetHogStatus: Setting 'hog' status on device %1") .arg(mDeviceID)); - OSStatus err = AudioDeviceSetProperty(mDeviceID, nullptr, 0, false, - kAudioDevicePropertyHogMode, - sizeof(mHog), &mHog); + OSStatus err = AudioObjectSetPropertyData(mDeviceID, &pa, 0, nullptr, + sizeof(mHog), &mHog); if (err || mHog != getpid()) { Warn(QString("SetHogStatus: Unable to set 'hog' status. Error = [%1]") @@ -725,9 +791,8 @@ bool CoreAudioData::SetHogStatus(bool hog) Debug(QString("SetHogStatus: Releasing 'hog' status on device %1") .arg(mDeviceID)); pid_t hogPid = -1; - OSStatus err = AudioDeviceSetProperty(mDeviceID, nullptr, 0, false, - kAudioDevicePropertyHogMode, - sizeof(hogPid), &hogPid); + OSStatus err = AudioObjectSetPropertyData(mDeviceID, &pa, 0, nullptr, + sizeof(hogPid), &hogPid); if (err || hogPid == getpid()) { Warn(QString("SetHogStatus: Unable to release 'hog' status. Error = [%1]") @@ -751,9 +816,15 @@ bool CoreAudioData::SetMixingSupport(bool mix) Debug(QString("SetMixingSupport: %1abling mixing for device %2") .arg(mix ? "En" : "Dis") .arg(mDeviceID)); - OSStatus err = AudioDeviceSetProperty(mDeviceID, nullptr, 0, false, - kAudioDevicePropertySupportsMixing, - sizeof(mixEnable), &mixEnable); + + AudioObjectPropertyAddress pa + { + kAudioDevicePropertySupportsMixing, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster + }; + OSStatus err = AudioObjectSetPropertyData(mDeviceID, &pa, 0, nullptr, + sizeof(mixEnable), &mixEnable); if (err) { Warn(QString("SetMixingSupport: Unable to set MixingSupport to %1. Error = [%2]") @@ -772,9 +843,14 @@ bool CoreAudioData::GetMixingSupport() return false; UInt32 val = 0; UInt32 size = sizeof(val); - OSStatus err = AudioDeviceGetProperty(mDeviceID, 0, false, - kAudioDevicePropertySupportsMixing, - &size, &val); + AudioObjectPropertyAddress pa + { + kAudioDevicePropertySupportsMixing, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster + }; + OSStatus err = AudioObjectGetPropertyData(mDeviceID, &pa, 0, nullptr, + &size, &val); if (err) return false; return (val > 0); @@ -783,92 +859,91 @@ bool CoreAudioData::GetMixingSupport() /** * Get a list of all the streams on this device */ -AudioStreamID *CoreAudioData::StreamsList(AudioDeviceID d) +AudioStreamIDVec CoreAudioData::StreamsList(AudioDeviceID d) { OSStatus err; UInt32 listSize; - AudioStreamID *list; + AudioStreamIDVec vec {}; + AudioObjectPropertyAddress pa + { + kAudioDevicePropertyStreams, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster + }; - err = AudioDeviceGetPropertyInfo(d, 0, FALSE, - kAudioDevicePropertyStreams, - &listSize, nullptr); + err = AudioObjectGetPropertyDataSize(d, &pa, + 0, nullptr, &listSize); if (err != noErr) { Error(QString("StreamsList: could not get list size: [%1]") .arg(OSS_STATUS(err))); - return nullptr; + return {}; } - // Space for a terminating ID: - listSize += sizeof(AudioStreamID); - list = (AudioStreamID *)malloc(listSize); - - if (list == nullptr) + try + { + vec.reserve(listSize / sizeof(AudioStreamID)); + } + catch (...) { Error("StreamsList(): out of memory?"); - return nullptr; + return {}; } - err = AudioDeviceGetProperty(d, 0, FALSE, - kAudioDevicePropertyStreams, - &listSize, list); + err = AudioObjectGetPropertyData(d, &pa, + 0, nullptr, &listSize, vec.data()); if (err != noErr) { Error(QString("StreamsList: could not get list: [%1]") .arg(OSS_STATUS(err))); - return nullptr; + return {}; } - // Add a terminating ID: - list[listSize/sizeof(AudioStreamID)] = kAudioHardwareBadStreamError; - return list; + return vec; } -AudioStreamBasicDescription *CoreAudioData::FormatsList(AudioStreamID s) +AudioStreamRangedVec CoreAudioData::FormatsList(AudioStreamID s) { OSStatus err; - AudioStreamBasicDescription *list; + AudioStreamRangedVec vec; UInt32 listSize; - AudioDevicePropertyID p; - - // This is deprecated for kAudioStreamPropertyAvailablePhysicalFormats, - // but compiling on 10.3 requires the older constant - p = kAudioStreamPropertyPhysicalFormats; + AudioObjectPropertyAddress pa + { + kAudioStreamPropertyPhysicalFormats, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster + }; // Retrieve all the stream formats supported by this output stream - err = AudioStreamGetPropertyInfo(s, 0, p, &listSize, nullptr); + err = AudioObjectGetPropertyDataSize(s, &pa, 0, nullptr, &listSize); if (err != noErr) { Warn(QString("FormatsList(): couldn't get list size: [%1]") .arg(OSS_STATUS(err))); - return nullptr; + return {}; } - // Space for a terminating ID: - listSize += sizeof(AudioStreamBasicDescription); - list = (AudioStreamBasicDescription *)malloc(listSize); - - if (list == nullptr) + try + { + vec.reserve(listSize / sizeof(AudioStreamRangedDescription)); + } + catch (...) { Error("FormatsList(): out of memory?"); - return nullptr; + return {}; } - err = AudioStreamGetProperty(s, 0, p, &listSize, list); + err = AudioObjectGetPropertyData(s, &pa, 0, nullptr, &listSize, vec.data()); if (err != noErr) { Warn(QString("FormatsList: couldn't get list: [%1]") .arg(OSS_STATUS(err))); - free(list); - return nullptr; + return {}; } - // Add a terminating ID: - list[listSize/sizeof(AudioStreamBasicDescription)].mFormatID = 0; - - return list; + return vec; } static UInt32 sNumberCommonSampleRates = 15; @@ -897,10 +972,15 @@ int *CoreAudioData::RatesList(AudioDeviceID d) UInt32 listSize; UInt32 nbitems = 0; + AudioObjectPropertyAddress pa + { + kAudioDevicePropertyAvailableNominalSampleRates, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster + }; + // retrieve size of rate list - err = AudioDeviceGetPropertyInfo(d, 0, 0, - kAudioDevicePropertyAvailableNominalSampleRates, - &listSize, nullptr); + err = AudioObjectGetPropertyDataSize(d, &pa, 0, nullptr, &listSize); if (err != noErr) { Warn(QString("RatesList(): couldn't get data rate list size: [%1]") @@ -915,9 +995,7 @@ int *CoreAudioData::RatesList(AudioDeviceID d) return nullptr; } - err = AudioDeviceGetProperty( - d, 0, 0, kAudioDevicePropertyAvailableNominalSampleRates, - &listSize, list); + err = AudioObjectGetPropertyData(d, &pa, 0, nullptr, &listSize, list); if (err != noErr) { Warn(QString("RatesList(): couldn't get list: [%1]") @@ -970,8 +1048,8 @@ int *CoreAudioData::RatesList(AudioDeviceID d) bool *CoreAudioData::ChannelsList(AudioDeviceID /*d*/, bool passthru) { - AudioStreamID *streams; - AudioStreamBasicDescription *formats; + AudioStreamIDVec streams; + AudioStreamRangedVec formats; bool founddigital = false; bool *list; @@ -981,7 +1059,7 @@ bool *CoreAudioData::ChannelsList(AudioDeviceID /*d*/, bool passthru) memset(list, 0, (CHANNELS_MAX+1) * sizeof(bool)); streams = StreamsList(mDeviceID); - if (!streams) + if (streams.empty()) { free(list); return nullptr; @@ -989,37 +1067,35 @@ bool *CoreAudioData::ChannelsList(AudioDeviceID /*d*/, bool passthru) if (passthru) { - for (int i = 0; streams[i] != kAudioHardwareBadStreamError; i++) + for (auto stream : streams) { - formats = FormatsList(streams[i]); - if (!formats) + formats = FormatsList(stream); + if (formats.empty()) continue; // Find a stream with a cac3 stream - for (int j = 0; formats[j].mFormatID != 0; j++) + for (auto format : formats) { - if (formats[j].mFormatID == 'IAC3' || - formats[j].mFormatID == kAudioFormat60958AC3) + if (format.mFormat.mFormatID == 'IAC3' || + format.mFormat.mFormatID == kAudioFormat60958AC3) { - list[formats[j].mChannelsPerFrame] = true; + list[format.mFormat.mChannelsPerFrame] = true; founddigital = true; } } - free(formats); } } if (!founddigital) { - for (int i = 0; streams[i] != kAudioHardwareBadStreamError; i++) + for (auto stream : streams) { - formats = FormatsList(streams[i]); - if (!formats) + formats = FormatsList(stream); + if (formats.empty()) continue; - for (int j = 0; formats[j].mFormatID != 0; j++) - if (formats[j].mChannelsPerFrame <= CHANNELS_MAX) - list[formats[j].mChannelsPerFrame] = true; - free(formats); + for (auto format : formats) + if (format.mFormat.mChannelsPerFrame <= CHANNELS_MAX) + list[format.mFormat.mChannelsPerFrame] = true; } } return list; @@ -1027,11 +1103,17 @@ bool *CoreAudioData::ChannelsList(AudioDeviceID /*d*/, bool passthru) int CoreAudioData::OpenAnalog() { - ComponentDescription desc; + AudioComponentDescription desc; AudioStreamBasicDescription DeviceFormat; AudioChannelLayout *layout; AudioChannelLayout new_layout; AudioDeviceID defaultDevice = GetDefaultOutputDevice(); + AudioObjectPropertyAddress pa + { + kAudioHardwarePropertyDevices, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster + }; Debug("OpenAnalog: Entering"); @@ -1049,14 +1131,14 @@ int CoreAudioData::OpenAnalog() desc.componentFlagsMask = 0; mDigitalInUse = false; - Component comp = FindNextComponent(nullptr, &desc); + AudioComponent comp = AudioComponentFindNext(nullptr, &desc); if (comp == nullptr) { Error("OpenAnalog: AudioComponentFindNext failed"); return false; } - OSErr err = OpenAComponent(comp, &mOutputUnit); + OSErr err = AudioComponentInstanceNew(comp, &mOutputUnit); if (err) { Error(QString("OpenAnalog: AudioComponentInstanceNew returned %1") @@ -1134,23 +1216,16 @@ int CoreAudioData::OpenAnalog() .arg(StreamDescriptionToString(DeviceFormat))); } /* Get the channel layout of the device side of the unit */ - err = AudioUnitGetPropertyInfo(mOutputUnit, - kAudioDevicePropertyPreferredChannelLayout, - kAudioUnitScope_Output, - 0, - ¶m_size, - nullptr); + pa.mSelector = kAudioDevicePropertyPreferredChannelLayout; + err = AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &pa, + 0, nullptr, ¶m_size); if(!err) { layout = (AudioChannelLayout *) malloc(param_size); - err = AudioUnitGetProperty(mOutputUnit, - kAudioDevicePropertyPreferredChannelLayout, - kAudioUnitScope_Output, - 0, - layout, - ¶m_size); + err = AudioObjectGetPropertyData(kAudioObjectSystemObject, &pa, + 0, nullptr, ¶m_size, layout); /* We need to "fill out" the ChannelLayout, because there are multiple ways that it can be set */ if(layout->mChannelLayoutTag == kAudioChannelLayoutTag_UseChannelBitmap) @@ -1365,7 +1440,7 @@ void CoreAudioData::CloseAnalog() Debug(QString("CloseAnalog: AudioUnitUninitialize %1") .arg(err)); } - err = CloseComponent(mOutputUnit); + err = AudioComponentInstanceDispose(mOutputUnit); Debug(QString("CloseAnalog: CloseComponent %1") .arg(err)); mOutputUnit = nullptr; @@ -1379,46 +1454,43 @@ void CoreAudioData::CloseAnalog() bool CoreAudioData::OpenSPDIF() { OSStatus err; - AudioStreamID *streams; + AudioStreamIDVec streams; AudioStreamBasicDescription outputFormat {}; Debug("OpenSPDIF: Entering"); streams = StreamsList(mDeviceID); - if (!streams) + if (streams.empty()) { Warn("OpenSPDIF: Couldn't retrieve list of streams"); return false; } - for (int i = 0; streams[i] != kAudioHardwareBadStreamError; ++i) + for (size_t i = 0; i < streams.size(); ++i) { - AudioStreamBasicDescription *formats = FormatsList(streams[i]); - if (!formats) + AudioStreamRangedVec formats = FormatsList(streams[i]); + if (formats.empty()) continue; // Find a stream with a cac3 stream - for (int j = 0; formats[j].mFormatID != 0; j++) + for (auto format : formats) { Debug(QString("OpenSPDIF: Considering Physical Format: %1") - .arg(StreamDescriptionToString(formats[j]))); - if ((formats[j].mFormatID == 'IAC3' || - formats[j].mFormatID == kAudioFormat60958AC3) && - formats[j].mSampleRate == mCA->m_sampleRate) + .arg(StreamDescriptionToString(format.mFormat))); + if ((format.mFormat.mFormatID == 'IAC3' || + format.mFormat.mFormatID == kAudioFormat60958AC3) && + format.mFormat.mSampleRate == mCA->m_sampleRate) { Debug("OpenSPDIF: Found digital format"); mStreamIndex = i; mStreamID = streams[i]; - outputFormat = formats[j]; + outputFormat = format.mFormat; break; } } - free(formats); - if (outputFormat.mFormatID) break; } - free(streams); if (!outputFormat.mFormatID) { @@ -1428,12 +1500,18 @@ bool CoreAudioData::OpenSPDIF() if (mRevertFormat == false) { + AudioObjectPropertyAddress pa + { + kAudioStreamPropertyPhysicalFormat, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster + }; + // Retrieve the original format of this stream first // if not done so already UInt32 paramSize = sizeof(mFormatOrig); - err = AudioStreamGetProperty(mStreamID, 0, - kAudioStreamPropertyPhysicalFormat, - ¶mSize, &mFormatOrig); + err = AudioObjectGetPropertyData(mStreamID, &pa, 0, nullptr, + ¶mSize, &mFormatOrig); if (err != noErr) { Warn(QString("OpenSPDIF - could not retrieve the original streamformat: [%1]") @@ -1465,19 +1543,19 @@ bool CoreAudioData::OpenSPDIF() mBytesPerPacket = mFormatNew.mBytesPerPacket; // Add IOProc callback - err = AudioDeviceAddIOProc(mDeviceID, - (AudioDeviceIOProc)RenderCallbackSPDIF, - (void *)this); + err = AudioDeviceCreateIOProcID(mDeviceID, + (AudioDeviceIOProc)RenderCallbackSPDIF, + (void *)this, &mIoProcID); if (err != noErr) { - Error(QString("OpenSPDIF: AudioDeviceAddIOProc failed: [%1]") + Error(QString("OpenSPDIF: AudioDeviceCreateIOProcID failed: [%1]") .arg(OSS_STATUS(err))); return false; } mIoProc = true; // Start device - err = AudioDeviceStart(mDeviceID, (AudioDeviceIOProc)RenderCallbackSPDIF); + err = AudioDeviceStart(mDeviceID, mIoProcID); if (err != noErr) { Error(QString("OpenSPDIF: AudioDeviceStart failed: [%1]") @@ -1499,7 +1577,7 @@ void CoreAudioData::CloseSPDIF() // Stop device if (mStarted) { - err = AudioDeviceStop(mDeviceID, (AudioDeviceIOProc)RenderCallbackSPDIF); + err = AudioDeviceStop(mDeviceID, mIoProcID); if (err != noErr) Error(QString("CloseSPDIF: AudioDeviceStop failed: [%1]") .arg(OSS_STATUS(err))); @@ -1509,10 +1587,9 @@ void CoreAudioData::CloseSPDIF() // Remove IOProc callback if (mIoProc) { - err = AudioDeviceRemoveIOProc(mDeviceID, - (AudioDeviceIOProc)RenderCallbackSPDIF); + err = AudioDeviceDestroyIOProcID(mDeviceID, mIoProcID); if (err != noErr) - Error(QString("CloseSPDIF: AudioDeviceRemoveIOProc failed: [%1]") + Error(QString("CloseSPDIF: AudioDeviceDestroyIOProcID failed: [%1]") .arg(OSS_STATUS(err))); mIoProc = false; } @@ -1540,9 +1617,14 @@ int CoreAudioData::AudioStreamChangeFormat(AudioStreamID s, .arg(s) .arg(StreamDescriptionToString(format))); - OSStatus err = AudioStreamSetProperty(s, nullptr, 0, - kAudioStreamPropertyPhysicalFormat, - sizeof(format), &format); + AudioObjectPropertyAddress pa + { + kAudioStreamPropertyPhysicalFormat, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster + }; + OSStatus err = AudioObjectSetPropertyData(s, &pa, 0, nullptr, + sizeof(format), &format); if (err != noErr) { Error(QString("AudioStreamChangeFormat couldn't set stream format: [%1]") @@ -1554,37 +1636,31 @@ int CoreAudioData::AudioStreamChangeFormat(AudioStreamID s, bool CoreAudioData::FindAC3Stream() { - bool foundAC3Stream = false; - AudioStreamID *streams; + AudioStreamIDVec streams; // Get a list of all the streams on this device streams = StreamsList(mDeviceID); - if (!streams) + if (streams.empty()) return false; - for (int i = 0; !foundAC3Stream && - streams[i] != kAudioHardwareBadStreamError; ++i) + for (auto stream : streams) { - AudioStreamBasicDescription *formats = FormatsList(streams[i]); - if (!formats) + AudioStreamRangedVec formats = FormatsList(stream); + if (formats.empty()) continue; // Find a stream with a cac3 stream - for (int j = 0; formats[j].mFormatID != 0; j++) - if (formats[j].mFormatID == 'IAC3' || - formats[j].mFormatID == kAudioFormat60958AC3) + for (auto format : formats) + if (format.mFormat.mFormatID == 'IAC3' || + format.mFormat.mFormatID == kAudioFormat60958AC3) { Debug("FindAC3Stream: found digital format"); - foundAC3Stream = true; - break; + return true; } - - free(formats); } - free(streams); - return foundAC3Stream; + return false; } /** @@ -1593,34 +1669,46 @@ bool CoreAudioData::FindAC3Stream() */ void CoreAudioData::ResetAudioDevices() { - AudioDeviceID *devices; - int numDevices; UInt32 size; + AudioObjectPropertyAddress pa + { + kAudioHardwarePropertyDevices, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster + }; + OSStatus err = AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &pa, + 0, nullptr, &size); + if (err) + { + Warn(QString("GetPropertyDataSize: Unable to retrieve the property sizes. " + "Error [%1]") + .arg(err)); + return; + } - AudioHardwareGetPropertyInfo(kAudioHardwarePropertyDevices, &size, nullptr); - devices = (AudioDeviceID*)malloc(size); - if (!devices) + std::vector<AudioDeviceID> devices = {}; + devices.resize(size / sizeof(AudioDeviceID)); + err = AudioObjectGetPropertyData(kAudioObjectSystemObject, &pa, + 0, nullptr, &size, devices.data()); + if (err) { - Error("ResetAudioDevices: out of memory?"); - return; + Warn(QString("GetPropertyData: Unable to retrieve the list of available devices. " + "Error [%1]") + .arg(err)); + return; } - numDevices = size / sizeof(AudioDeviceID); - AudioHardwareGetProperty(kAudioHardwarePropertyDevices, &size, devices); - for (int i = 0; i < numDevices; i++) + for (const auto & dev : devices) { - AudioStreamID *streams; + AudioStreamIDVec streams; - streams = StreamsList(devices[i]); - if (!streams) + streams = StreamsList(dev); + if (streams.empty()) continue; - for (int j = 0; streams[j] != kAudioHardwareBadStreamError; j++) - ResetStream(streams[j]); - - free(streams); + for (auto stream : streams) + ResetStream(stream); } - free(devices); } void CoreAudioData::ResetStream(AudioStreamID s) @@ -1628,29 +1716,35 @@ void CoreAudioData::ResetStream(AudioStreamID s) AudioStreamBasicDescription currentFormat; OSStatus err; UInt32 paramSize; + AudioObjectPropertyAddress pa + { + kAudioStreamPropertyPhysicalFormat, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster + }; + // Find the streams current physical format paramSize = sizeof(currentFormat); - AudioStreamGetProperty(s, 0, kAudioStreamPropertyPhysicalFormat, - ¶mSize, ¤tFormat); + AudioObjectGetPropertyData(s, &pa, 0, nullptr, + ¶mSize, ¤tFormat); // If it's currently AC-3/SPDIF then reset it to some mixable format if (currentFormat.mFormatID == 'IAC3' || currentFormat.mFormatID == kAudioFormat60958AC3) { - AudioStreamBasicDescription *formats = FormatsList(s); + AudioStreamRangedVec formats = FormatsList(s); bool streamReset = false; - if (!formats) + if (formats.empty()) return; - for (int i = 0; !streamReset && formats[i].mFormatID != 0; i++) - if (formats[i].mFormatID == kAudioFormatLinearPCM) + for (auto format : formats) + if (format.mFormat.mFormatID == kAudioFormatLinearPCM) { - err = AudioStreamSetProperty(s, nullptr, 0, - kAudioStreamPropertyPhysicalFormat, - sizeof(formats[i]), &(formats[i])); + err = AudioObjectSetPropertyData(s, &pa, 0, nullptr, + sizeof(format), &(format.mFormat)); if (err != noErr) { Warn(QString("ResetStream: could not set physical format: [%1]") @@ -1663,8 +1757,6 @@ void CoreAudioData::ResetStream(AudioStreamID s) sleep(1); // For the change to take effect } } - - free(formats); } } @@ -1674,11 +1766,28 @@ QMap<QString, QString> *AudioOutputCA::GetDevices(const char */*type*/) // Obtain a list of all available audio devices UInt32 size = 0; - AudioHardwareGetPropertyInfo(kAudioHardwarePropertyDevices, &size, nullptr); + + AudioObjectPropertyAddress pa + { + kAudioHardwarePropertyDevices, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster + }; + + OSStatus err = AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &pa, + 0, nullptr, &size); + if (err) + { + VBAUDIO(QString("GetPropertyDataSize: Unable to retrieve the property sizes. " + "Error [%1]") + .arg(err)); + return devs; + } + UInt32 deviceCount = size / sizeof(AudioDeviceID); AudioDeviceID* pDevices = new AudioDeviceID[deviceCount]; - OSStatus err = AudioHardwareGetProperty(kAudioHardwarePropertyDevices, - &size, pDevices); + err = AudioObjectGetPropertyData(kAudioObjectSystemObject, &pa, + 0, nullptr, &size, pDevices); if (err) VBAUDIO(QString("AudioOutputCA::GetDevices: Unable to retrieve the list of " "available devices. Error [%1]") diff --git a/mythtv/libs/libmyth/audio/audiooutpututil.cpp b/mythtv/libs/libmyth/audio/audiooutpututil.cpp index 6f4703642d..0f9eb8e5df 100644 --- a/mythtv/libs/libmyth/audio/audiooutpututil.cpp +++ b/mythtv/libs/libmyth/audio/audiooutpututil.cpp @@ -148,6 +148,7 @@ void AudioOutputUtil::AdjustVolume(void *buf, int len, int volume, "jnz 1b \n\t" :"+r"(fptr) :"c"(loops),"m"(g) + :"xmm0","xmm1","xmm2","xmm3","xmm4" ); } #endif //ARCH_X86 diff --git a/mythtv/libs/libmyth/mediamonitor-unix.cpp b/mythtv/libs/libmyth/mediamonitor-unix.cpp index 04df271132..367b0f8c16 100644 --- a/mythtv/libs/libmyth/mediamonitor-unix.cpp +++ b/mythtv/libs/libmyth/mediamonitor-unix.cpp @@ -38,6 +38,7 @@ using namespace std; #include "mythmediamonitor.h" #include "mediamonitor-unix.h" #include "mythconfig.h" +#include "mythcorecontext.h" #include "mythcdrom.h" #include "mythhdd.h" #include "mythlogging.h" @@ -119,7 +120,13 @@ MediaMonitorUnix::MediaMonitorUnix(QObject* par, : MediaMonitor(par, interval, allowEject) { CheckFileSystemTable(); - CheckMountable(); + if (!gCoreContext->GetBoolSetting("MonitorDrives", false)) { + LOG(VB_GENERAL, LOG_NOTICE, "MediaMonitor disabled by user setting."); + } + else + { + CheckMountable(); + } LOG(VB_MEDIA, LOG_INFO, "Initial device list...\n" + listDevices()); } diff --git a/mythtv/libs/libmyth/mythmediamonitor.cpp b/mythtv/libs/libmyth/mythmediamonitor.cpp index c93de12c9f..d9bf7ae263 100644 --- a/mythtv/libs/libmyth/mythmediamonitor.cpp +++ b/mythtv/libs/libmyth/mythmediamonitor.cpp @@ -460,7 +460,7 @@ void MediaMonitor::StartMonitoring(void) if (m_Active) return; if (!gCoreContext->GetBoolSetting("MonitorDrives", false)) { - LOG(VB_MEDIA, LOG_NOTICE, "MediaMonitor diasabled by user setting."); + LOG(VB_MEDIA, LOG_NOTICE, "MediaMonitor disabled by user setting."); return; } diff --git a/mythtv/libs/libmyth/programinfo.cpp b/mythtv/libs/libmyth/programinfo.cpp index 19b3a6498b..eebb99a4fd 100644 --- a/mythtv/libs/libmyth/programinfo.cpp +++ b/mythtv/libs/libmyth/programinfo.cpp @@ -405,7 +405,7 @@ ProgramInfo::ProgramInfo( m_inputName(std::move(_inputname)), m_bookmarkUpdate(std::move(_bookmarkupdate)) { - if (m_originalAirDate.isValid() && m_originalAirDate < QDate(1940, 1, 1)) + if (m_originalAirDate.isValid() && m_originalAirDate < QDate(1895, 12, 28)) m_originalAirDate = QDate(); SetPathname(_pathname); @@ -586,7 +586,7 @@ ProgramInfo::ProgramInfo( m_programFlags |= (commfree) ? FL_CHANCOMMFREE : 0; m_programFlags |= (repeat) ? FL_REPEAT : 0; - if (m_originalAirDate.isValid() && m_originalAirDate < QDate(1940, 1, 1)) + if (m_originalAirDate.isValid() && m_originalAirDate < QDate(1895, 12, 28)) m_originalAirDate = QDate(); for (auto *it : schedList) @@ -2064,7 +2064,7 @@ bool ProgramInfo::LoadProgramFromRecorded( (query.value(42).toUInt() << kAudioPropertyOffset)); // ancillary data -- end - if (m_originalAirDate.isValid() && m_originalAirDate < QDate(1940, 1, 1)) + if (m_originalAirDate.isValid() && m_originalAirDate < QDate(1895, 12, 28)) m_originalAirDate = QDate(); // Extra stuff which is not serialized and may get lost. diff --git a/mythtv/libs/libmyth/recordingtypes.cpp b/mythtv/libs/libmyth/recordingtypes.cpp index cc86b2be17..86658d05e4 100644 --- a/mythtv/libs/libmyth/recordingtypes.cpp +++ b/mythtv/libs/libmyth/recordingtypes.cpp @@ -161,6 +161,8 @@ QString toString(RecordingDupInType recdupin) return QObject::tr("Previous Recordings"); case kDupsInAll: return QObject::tr("All Recordings"); + // TODO: This is wrong, kDupsNewEpi is returned in conjunction with + // one of the other values. case kDupsNewEpi: return QObject::tr("New Episodes Only"); default: @@ -179,6 +181,8 @@ QString toDescription(RecordingDupInType recdupin) case kDupsInAll: return QObject::tr("Look for duplicates in current and previous " "recordings"); + // TODO: This is wrong, kDupsNewEpi is returned in conjunction with + // one of the other values. case kDupsNewEpi: return QObject::tr("Record new episodes only"); default: @@ -188,6 +192,8 @@ QString toDescription(RecordingDupInType recdupin) QString toRawString(RecordingDupInType recdupin) { + // Remove "New Episodes" flag + recdupin = (RecordingDupInType) (recdupin & (-1 - kDupsNewEpi)); switch (recdupin) { case kDupsInRecorded: @@ -196,13 +202,17 @@ QString toRawString(RecordingDupInType recdupin) return QString("Previous Recordings"); case kDupsInAll: return QString("All Recordings"); - case kDupsNewEpi: - return QString("New Episodes Only"); default: return QString("Unknown"); } } +// New Episodes Only is a flag added to DupIn +bool newEpifromDupIn(RecordingDupInType recdupin) +{ + return (recdupin & kDupsNewEpi); +} + RecordingDupInType dupInFromString(const QString& type) { if (type.toLower() == "current recordings" || type.toLower() == "current") @@ -212,10 +222,17 @@ RecordingDupInType dupInFromString(const QString& type) if (type.toLower() == "all recordings" || type.toLower() == "all") return kDupsInAll; if (type.toLower() == "new episodes only" || type.toLower() == "new") - return kDupsNewEpi; + return static_cast<RecordingDupInType> (kDupsInAll | kDupsNewEpi); return kDupsInAll; } +RecordingDupInType dupInFromStringAndBool(const QString& type, bool newEpisodesOnly) { + RecordingDupInType result = dupInFromString(type); + if (newEpisodesOnly) + result = static_cast<RecordingDupInType> (result | kDupsNewEpi); + return result; +} + QString toString(RecordingDupMethodType duptype) { switch (duptype) diff --git a/mythtv/libs/libmyth/recordingtypes.h b/mythtv/libs/libmyth/recordingtypes.h index ccb02120d5..c5654dd6b1 100644 --- a/mythtv/libs/libmyth/recordingtypes.h +++ b/mythtv/libs/libmyth/recordingtypes.h @@ -50,7 +50,9 @@ enum RecordingDupInType MPUBLIC QString toString(RecordingDupInType rectype); MPUBLIC QString toDescription(RecordingDupInType rectype); MPUBLIC QString toRawString(RecordingDupInType rectype); +MPUBLIC bool newEpifromDupIn(RecordingDupInType recdupin); MPUBLIC RecordingDupInType dupInFromString(const QString& type); +MPUBLIC RecordingDupInType dupInFromStringAndBool(const QString& type, bool newEpisodesOnly); enum RecordingDupMethodType { diff --git a/mythtv/libs/libmythbase/mythcommandlineparser.cpp b/mythtv/libs/libmythbase/mythcommandlineparser.cpp index e58f2a55b0..d20ad9d29e 100644 --- a/mythtv/libs/libmythbase/mythcommandlineparser.cpp +++ b/mythtv/libs/libmythbase/mythcommandlineparser.cpp @@ -20,6 +20,12 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA */ +#if defined ANDROID && __ANDROID_API__ < 24 +// ftello and fseeko do not exist in android before api level 24 +#define ftello ftell +#define fseeko fseek +#endif + // C++ headers #include <algorithm> #include <csignal> diff --git a/mythtv/libs/libmythbase/mythdbcon.cpp b/mythtv/libs/libmythbase/mythdbcon.cpp index 102395fcae..1df2f65be3 100644 --- a/mythtv/libs/libmythbase/mythdbcon.cpp +++ b/mythtv/libs/libmythbase/mythdbcon.cpp @@ -472,7 +472,7 @@ void MDBManager::CloseDatabases() m_pool[QThread::currentThread()].clear(); m_lock.unlock(); - foreach (auto & conn, list) + for (auto *conn : qAsConst(list)) { LOG(VB_DATABASE, LOG_INFO, "Closing DB connection named '" + conn->m_name + "'"); diff --git a/mythtv/libs/libmythbase/test/test_mythsorthelper/test_mythsorthelper.cpp b/mythtv/libs/libmythbase/test/test_mythsorthelper/test_mythsorthelper.cpp index 478c77059a..46fa786463 100644 --- a/mythtv/libs/libmythbase/test/test_mythsorthelper/test_mythsorthelper.cpp +++ b/mythtv/libs/libmythbase/test/test_mythsorthelper/test_mythsorthelper.cpp @@ -92,6 +92,7 @@ void TestSortHelper::Variations_test(void) QVERIFY(sh->doTitle("The Blob") != "blob"); QVERIFY(sh->doTitle("The Blob") != "Blob, The"); QVERIFY(sh->doTitle("The Blob") != "blob, the"); + QVERIFY(sh->doTitle("Any Given Sunday") == "Any Given Sunday"); QVERIFY(sh->doPathname("/video/recordings/The Flash/Season 1/The Flash - S01E01.ts") == "/video/recordings/The Flash/Season 1/The Flash - S01E01.ts"); delete sh; @@ -104,6 +105,7 @@ void TestSortHelper::Variations_test(void) QVERIFY(sh->doTitle("The Blob") != "blob"); QVERIFY(sh->doTitle("The Blob") != "Blob, The"); QVERIFY(sh->doTitle("The Blob") != "blob, the"); + QVERIFY(sh->doTitle("Any Given Sunday") == "any given sunday"); QVERIFY(sh->doPathname("/video/recordings/The Flash/Season 1/The Flash - S01E01.ts") == "/video/recordings/the flash/season 1/the flash - s01e01.ts"); delete sh; @@ -116,6 +118,7 @@ void TestSortHelper::Variations_test(void) QVERIFY(sh->doTitle("The Sting") != "sting"); QVERIFY(sh->doTitle("The Sting") != "Sting, The"); QVERIFY(sh->doTitle("The Sting") != "sting, the"); + QVERIFY(sh->doTitle("Any Given Sunday") == "Any Given Sunday"); QVERIFY(sh->doPathname("/video/recordings/The Flash/Season 1/The Flash - S01E01.ts") == "/video/recordings/Flash/Season 1/Flash - S01E01.ts"); delete sh; @@ -128,6 +131,7 @@ void TestSortHelper::Variations_test(void) QVERIFY(sh->doTitle("The Thing") == "thing"); QVERIFY(sh->doTitle("The Thing") != "Thing, The"); QVERIFY(sh->doTitle("The Thing") != "thing, the"); + QVERIFY(sh->doTitle("Any Given Sunday") == "any given sunday"); QVERIFY(sh->doPathname("/video/recordings/The Flash/Season 1/The Flash - S01E01.ts") == "/video/recordings/flash/season 1/flash - s01e01.ts"); delete sh; @@ -138,6 +142,7 @@ void TestSortHelper::Variations_test(void) QVERIFY(sh->doTitle("The Flash") == "flash, the"); QVERIFY(sh->doTitle("The Flash") != "Flash"); QVERIFY(sh->doTitle("The Flash") != "flash"); + QVERIFY(sh->doTitle("Any Given Sunday") == "any given sunday"); QVERIFY(sh->doPathname("/video/recordings/The Flash/Season 1/The Flash - S01E01.ts") == "/video/recordings/flash, the/season 1/flash - s01e01.ts, the"); delete sh; diff --git a/mythtv/libs/libmythmetadata/metadatadownload.cpp b/mythtv/libs/libmythmetadata/metadatadownload.cpp index 2ecebd0fef..2869793c8b 100644 --- a/mythtv/libs/libmythmetadata/metadatadownload.cpp +++ b/mythtv/libs/libmythmetadata/metadatadownload.cpp @@ -1,3 +1,6 @@ +// C/C++ +#include <cstdlib> + // qt #include <QCoreApplication> #include <QEvent> @@ -90,43 +93,51 @@ void MetadataDownload::run() if (lookup->GetType() == kMetadataVideo || lookup->GetType() == kMetadataRecording) { - if (lookup->GetSubtype() == kProbableTelevision) + // First, look for mxml and nfo files in video storage groups + if (lookup->GetType() == kMetadataVideo && + !lookup->GetFilename().isEmpty()) { - list = handleTelevision(lookup); - if (findExactMatchCount(list, lookup->GetBaseTitle(), true) == 0) - { - // There are no exact match prospects with artwork from TV search, - // so add in movies, where we might find a better match. - list.append(handleMovie(lookup)); - } + QString mxml = getMXMLPath(lookup->GetFilename()); + QString nfo = getNFOPath(lookup->GetFilename()); + + if (!mxml.isEmpty()) + list = readMXML(mxml, lookup); + else if (!nfo.isEmpty()) + list = readNFO(nfo, lookup); } - else if (lookup->GetSubtype() == kProbableMovie) + + // If nothing found, create lookups based on filename + if (list.isEmpty()) { - list = handleMovie(lookup); - if (findExactMatchCount(list, lookup->GetBaseTitle(), true) == 0) + if (lookup->GetSubtype() == kProbableTelevision) { - // There are no exact match prospects with artwork from Movie search - // so add in television, where we might find a better match. - list.append(handleTelevision(lookup)); + list = handleTelevision(lookup); + if ((findExactMatchCount(list, lookup->GetBaseTitle(), true) == 0) || + (list.size() > 1 && !lookup->GetAutomatic())) + { + // There are no exact match prospects with artwork from TV search, + // so add in movies, where we might find a better match. + // In case of manual mode and ambiguous result, add it as well. + list.append(handleMovie(lookup)); + } + } + else if (lookup->GetSubtype() == kProbableMovie) + { + list = handleMovie(lookup); + if ((findExactMatchCount(list, lookup->GetBaseTitle(), true) == 0) || + (list.size() > 1 && !lookup->GetAutomatic())) + { + // There are no exact match prospects with artwork from Movie search + // so add in television, where we might find a better match. + // In case of manual mode and ambiguous result, add it as well. + list.append(handleTelevision(lookup)); + } + } + else + { + // will try both movie and TV + list = handleVideoUndetermined(lookup); } - } - else - { - // will try both movie and TV - list = handleVideoUndetermined(lookup); - } - - if ((list.isEmpty() || - (list.size() > 1 && !lookup->GetAutomatic())) && - lookup->GetSubtype() == kProbableTelevision) - { - list.append(handleMovie(lookup)); - } - else if ((list.isEmpty() || - (list.size() > 1 && !lookup->GetAutomatic())) && - lookup->GetSubtype() == kProbableMovie) - { - list.append(handleTelevision(lookup)); } } else if (lookup->GetType() == kMetadataGame) @@ -163,6 +174,8 @@ void MetadataDownload::run() { MetadataLookup *newlookup = bestLookup; + // pass through automatic type + newlookup->SetAutomatic(true); // bestlookup is owned by list, we need an extra reference newlookup->IncrRef(); newlookup->SetStep(kLookupData); @@ -176,8 +189,29 @@ void MetadataDownload::run() continue; } + // Experimental: + // If nothing matches, always return the first found item + if (getenv("EXPERIMENTAL_METADATA_GRAB")) + { + MetadataLookup *newlookup = list.takeFirst(); + + // pass through automatic type + newlookup->SetAutomatic(true); // ### XXX RER + newlookup->SetStep(kLookupData); + // Type may have changed + LookupType ret = GuessLookupType(newlookup); + if (ret != kUnknownVideo) + { + newlookup->SetSubtype(ret); + } + prependLookup(newlookup); + continue; + } + + // nothing more we can do in automatic mode QCoreApplication::postEvent(m_parent, new MetadataLookupFailure(MetadataLookupList() << lookup)); + continue; } LOG(VB_GENERAL, LOG_INFO, @@ -567,8 +601,8 @@ MetadataLookupList MetadataDownload::handleGame(MetadataLookup *lookup) /** * handleMovie: * attempt to find movie data via the following (in order) - * 1- Local MXML - * 2- Local NFO + * 1- Local MXML: already done before + * 2- Local NFO: already done * 3- By title * 4- By inetref (if present) */ @@ -576,23 +610,6 @@ MetadataLookupList MetadataDownload::handleMovie(MetadataLookup *lookup) { MetadataLookupList list; - QString mxml; - QString nfo; - - if (!lookup->GetFilename().isEmpty()) - { - mxml = getMXMLPath(lookup->GetFilename()); - nfo = getNFOPath(lookup->GetFilename()); - } - - if (!mxml.isEmpty()) - list = readMXML(mxml, lookup); - else if (!nfo.isEmpty()) - list = readNFO(nfo, lookup); - - if (!list.isEmpty()) - return list; - MetaGrabberScript grabber = MetaGrabberScript::GetGrabber(kGrabberMovie, lookup); @@ -621,8 +638,8 @@ MetadataLookupList MetadataDownload::handleMovie(MetadataLookup *lookup) /** * handleTelevision * attempt to find television data via the following (in order) - * 1- Local MXML - * 2- Local NFO + * 1- Local MXML: already done before + * 2- Local NFO: already done * 3- By inetref with subtitle * 4- By inetref with season and episode * 5- By inetref @@ -633,23 +650,6 @@ MetadataLookupList MetadataDownload::handleTelevision(MetadataLookup *lookup) { MetadataLookupList list; - QString mxml; - QString nfo; - - if (!lookup->GetFilename().isEmpty()) - { - mxml = getMXMLPath(lookup->GetFilename()); - nfo = getNFOPath(lookup->GetFilename()); - } - - if (!mxml.isEmpty()) - list = readMXML(mxml, lookup); - else if (!nfo.isEmpty()) - list = readNFO(nfo, lookup); - - if (!list.isEmpty()) - return list; - MetaGrabberScript grabber = MetaGrabberScript::GetGrabber(kGrabberTelevision, lookup); bool searchcollection = false; diff --git a/mythtv/libs/libmythmetadata/metadatagrabber.cpp b/mythtv/libs/libmythmetadata/metadatagrabber.cpp index 9907eb4e95..d9c0c1ebb7 100644 --- a/mythtv/libs/libmythmetadata/metadatagrabber.cpp +++ b/mythtv/libs/libmythmetadata/metadatagrabber.cpp @@ -425,7 +425,7 @@ MetadataLookupList MetaGrabberScript::RunGrabber(const QStringList &args, .arg(m_fullcommand).arg(args.join(" "))); grabber.Run(); - if (grabber.Wait(60) != GENERIC_EXIT_OK) + if (grabber.Wait(180) != GENERIC_EXIT_OK) return list; QByteArray result = grabber.ReadAll(); diff --git a/mythtv/libs/libmythmetadata/metaioflacvorbis.cpp b/mythtv/libs/libmythmetadata/metaioflacvorbis.cpp index 142894835f..ae9cdee047 100644 --- a/mythtv/libs/libmythmetadata/metaioflacvorbis.cpp +++ b/mythtv/libs/libmythmetadata/metaioflacvorbis.cpp @@ -125,6 +125,16 @@ MusicMetadata* MetaIOFLACVorbis::read(const QString &filename) compilation = true; } } + else if (tag->contains("ALBUMARTIST")) + { + QString compilation_artist = TStringToQString( + tag->fieldListMap()["ALBUMARTIST"].toString()).trimmed(); + if (compilation_artist != metadata->Artist()) + { + metadata->setCompilationArtist(compilation_artist); + compilation = true; + } + } if (!compilation && tag->contains("MUSICBRAINZ_ALBUMARTISTID")) { @@ -139,6 +149,30 @@ MusicMetadata* MetaIOFLACVorbis::read(const QString &filename) if (metadata->Length() <= 0) metadata->setLength(getTrackLength(flacfile)); + if (tag->contains("DISCNUMBER")) + { + bool valid = false; + int n = tag->fieldListMap()["DISCNUMBER"].toString().toInt(&valid); + if (valid) + metadata->setDiscNumber(n); + } + + if (tag->contains("TOTALTRACKS")) + { + bool valid = false; + int n = tag->fieldListMap()["TOTALTRACKS"].toString().toInt(&valid); + if (valid) + metadata->setTrackCount(n); + } + + if (tag->contains("TOTALDISCS")) + { + bool valid = false; + int n = tag->fieldListMap()["TOTALDISCS"].toString().toInt(&valid); + if (valid) + metadata->setDiscCount(n); + } + delete flacfile; return metadata; diff --git a/mythtv/libs/libmythmetadata/musicfilescanner.cpp b/mythtv/libs/libmythmetadata/musicfilescanner.cpp index 8a43a3eee0..9b5e3212a4 100644 --- a/mythtv/libs/libmythmetadata/musicfilescanner.cpp +++ b/mythtv/libs/libmythmetadata/musicfilescanner.cpp @@ -13,7 +13,7 @@ #include <metaio.h> #include <musicfilescanner.h> -MusicFileScanner::MusicFileScanner() +MusicFileScanner::MusicFileScanner(bool force) : m_forceupdate{force} { MSqlQuery query(MSqlQuery::InitCon()); @@ -318,6 +318,10 @@ void MusicFileScanner::AddFileToDB(const QString &filename, const QString &start data->setAlbumId(m_albumid[album_cache_string]); } + int caid = m_artistid[data->CompilationArtist().toLower()]; + if (caid > 0) + data->setCompilationArtistId(caid); + int gid = m_genreid[data->Genre().toLower()]; if (gid > 0) data->setGenreId(gid); @@ -329,6 +333,9 @@ void MusicFileScanner::AddFileToDB(const QString &filename, const QString &start m_artistid[data->Artist().toLower()] = data->getArtistId(); + m_artistid[data->CompilationArtist().toLower()] = + data->getCompilationArtistId(); + m_genreid[data->Genre().toLower()] = data->getGenreId(); @@ -599,6 +606,10 @@ void MusicFileScanner::UpdateFileInDB(const QString &filename, const QString &st disk_meta->setAlbumId(m_albumid[album_cache_string]); } + int caid = m_artistid[disk_meta->CompilationArtist().toLower()]; + if (caid > 0) + disk_meta->setCompilationArtistId(caid); + int gid = m_genreid[disk_meta->Genre().toLower()]; if (gid > 0) disk_meta->setGenreId(gid); @@ -613,6 +624,8 @@ void MusicFileScanner::UpdateFileInDB(const QString &filename, const QString &st // Update the cache m_artistid[disk_meta->Artist().toLower()] = disk_meta->getArtistId(); + m_artistid[disk_meta->CompilationArtist().toLower()] + = disk_meta->getCompilationArtistId(); m_genreid[disk_meta->Genre().toLower()] = disk_meta->getGenreId(); album_cache_string = QString::number(disk_meta->getArtistId()) + "#" + @@ -803,7 +816,7 @@ void MusicFileScanner::ScanMusic(MusicLoadedMap &music_files) { if (music_files[name].location == MusicFileScanner::kDatabase) continue; - if (HasFileChanged(name, query.value(1).toString())) + if (m_forceupdate || HasFileChanged(name, query.value(1).toString())) music_files[name].location = MusicFileScanner::kNeedUpdate; else { diff --git a/mythtv/libs/libmythmetadata/musicfilescanner.h b/mythtv/libs/libmythmetadata/musicfilescanner.h index 5f2cae2ce1..8ceefdd189 100644 --- a/mythtv/libs/libmythmetadata/musicfilescanner.h +++ b/mythtv/libs/libmythmetadata/musicfilescanner.h @@ -29,7 +29,7 @@ class META_PUBLIC MusicFileScanner using MusicLoadedMap = QMap <QString, MusicFileData>; public: - MusicFileScanner(void); + MusicFileScanner(bool force = false); ~MusicFileScanner(void) = default; void SearchDirs(const QStringList &dirList); @@ -69,6 +69,8 @@ class META_PUBLIC MusicFileScanner uint m_coverartAdded {0}; uint m_coverartRemoved {0}; uint m_coverartUpdated {0}; + + bool m_forceupdate; }; #endif // _MUSICFILESCANNER_H_ diff --git a/mythtv/libs/libmythmetadata/musicmetadata.cpp b/mythtv/libs/libmythmetadata/musicmetadata.cpp index f5959d3397..bcf9d561f0 100644 --- a/mythtv/libs/libmythmetadata/musicmetadata.cpp +++ b/mythtv/libs/libmythmetadata/musicmetadata.cpp @@ -537,11 +537,20 @@ int MusicMetadata::getArtistId() } m_artistId = query.lastInsertId().toInt(); } + } + + return m_artistId; +} + +int MusicMetadata::getCompilationArtistId() +{ + if (m_compartistId < 0) { + MSqlQuery query(MSqlQuery::InitCon()); // Compilation Artist if (m_artist == m_compilationArtist) { - m_compartistId = m_artistId; + m_compartistId = getArtistId(); } else { @@ -572,7 +581,7 @@ int MusicMetadata::getArtistId() } } - return m_artistId; + return m_compartistId; } int MusicMetadata::getAlbumId() @@ -667,12 +676,17 @@ QString MusicMetadata::Url(uint index) void MusicMetadata::dumpToDatabase() { + checkEmptyFields(); + if (m_directoryId < 0) getDirectoryId(); if (m_artistId < 0) getArtistId(); + if (m_compartistId < 0) + getCompilationArtistId(); + if (m_albumId < 0) getAlbumId(); @@ -764,10 +778,14 @@ void MusicMetadata::dumpToDatabase() if (m_albumArt) m_albumArt->dumpToDatabase(); - // make sure the compilation flag is updated - query.prepare("UPDATE music_albums SET compilation = :COMPILATION, year = :YEAR " + // update the album + query.prepare("UPDATE music_albums SET album_name = :ALBUM_NAME, " + "artist_id = :COMP_ARTIST_ID, compilation = :COMPILATION, " + "year = :YEAR " "WHERE music_albums.album_id = :ALBUMID"); query.bindValue(":ALBUMID", m_albumId); + query.bindValue(":ALBUM_NAME", m_album); + query.bindValue(":COMP_ARTIST_ID", m_compartistId); query.bindValue(":COMPILATION", m_compilation); query.bindValue(":YEAR", m_year); @@ -849,16 +867,28 @@ inline QString MusicMetadata::formatReplaceSymbols(const QString &format) void MusicMetadata::checkEmptyFields() { if (m_artist.isEmpty()) + { m_artist = tr("Unknown Artist", "Default artist if no artist"); + m_artistId = -1; + } // This should be the same as Artist if it's a compilation track or blank if (!m_compilation || m_compilationArtist.isEmpty()) + { m_compilationArtist = m_artist; + m_compartistId = -1; + } if (m_album.isEmpty()) + { m_album = tr("Unknown Album", "Default album if no album"); + m_albumId = -1; + } if (m_title.isEmpty()) m_title = m_filename; if (m_genre.isEmpty()) + { m_genre = tr("Unknown Genre", "Default genre if no genre"); + m_genreId = -1; + } ensureSortFields(); } @@ -1537,6 +1567,7 @@ void AllMusic::resync() dbMeta->setDirectoryId(query.value(11).toInt()); dbMeta->setArtistId(query.value(1).toInt()); + dbMeta->setCompilationArtistId(query.value(3).toInt()); dbMeta->setAlbumId(query.value(4).toInt()); dbMeta->setTrackCount(query.value(19).toInt()); dbMeta->setFileSize(query.value(20).toULongLong()); diff --git a/mythtv/libs/libmythmetadata/musicmetadata.h b/mythtv/libs/libmythmetadata/musicmetadata.h index 9dec96b1f6..c554a0befe 100644 --- a/mythtv/libs/libmythmetadata/musicmetadata.h +++ b/mythtv/libs/libmythmetadata/musicmetadata.h @@ -108,7 +108,6 @@ class META_PUBLIC MusicMetadata m_filename(std::move(lfilename)) { checkEmptyFields(); - ensureSortFields(); } MusicMetadata(int lid, QString lbroadcaster, QString lchannel, QString ldescription, UrlList lurls, QString llogourl, @@ -130,6 +129,7 @@ class META_PUBLIC MusicMetadata const QString &lartist_sort = nullptr) { m_artist = lartist; + m_artistId = -1; m_artistSort = lartist_sort; m_formattedArtist.clear(); m_formattedTitle.clear(); ensureSortFields(); @@ -141,6 +141,7 @@ class META_PUBLIC MusicMetadata const QString &lcompilation_artist_sort = nullptr) { m_compilationArtist = lcompilation_artist; + m_compartistId = -1; m_compilationArtistSort = lcompilation_artist_sort; m_formattedArtist.clear(); m_formattedTitle.clear(); ensureSortFields(); @@ -152,6 +153,7 @@ class META_PUBLIC MusicMetadata const QString &lalbum_sort = nullptr) { m_album = lalbum; + m_albumId = -1; m_albumSort = lalbum_sort; m_formattedArtist.clear(); m_formattedTitle.clear(); ensureSortFields(); @@ -171,7 +173,10 @@ class META_PUBLIC MusicMetadata QString FormatTitle(); QString Genre() const { return m_genre; } - void setGenre(const QString &lgenre) { m_genre = lgenre; } + void setGenre(const QString &lgenre) { + m_genre = lgenre; + m_genreId = -1; + } void setDirectoryId(int ldirectoryid) { m_directoryId = ldirectoryid; } int getDirectoryId(); @@ -179,6 +184,9 @@ class META_PUBLIC MusicMetadata void setArtistId(int lartistid) { m_artistId = lartistid; } int getArtistId(); + void setCompilationArtistId(int lartistid) { m_compartistId = lartistid; } + int getCompilationArtistId(); + void setAlbumId(int lalbumid) { m_albumId = lalbumid; } int getAlbumId(); diff --git a/mythtv/libs/libmythservicecontracts/datacontracts/recRule.h b/mythtv/libs/libmythservicecontracts/datacontracts/recRule.h index 9fe14910d8..518e941702 100644 --- a/mythtv/libs/libmythservicecontracts/datacontracts/recRule.h +++ b/mythtv/libs/libmythservicecontracts/datacontracts/recRule.h @@ -15,7 +15,7 @@ namespace DTC class SERVICE_PUBLIC RecRule : public QObject { Q_OBJECT - Q_CLASSINFO( "version" , "2.00" ); + Q_CLASSINFO( "version" , "2.10" ); Q_PROPERTY( int Id READ Id WRITE setId ) Q_PROPERTY( int ParentId READ ParentId WRITE setParentId ) @@ -46,6 +46,7 @@ class SERVICE_PUBLIC RecRule : public QObject Q_PROPERTY( int EndOffset READ EndOffset WRITE setEndOffset ) Q_PROPERTY( QString DupMethod READ DupMethod WRITE setDupMethod ) Q_PROPERTY( QString DupIn READ DupIn WRITE setDupIn ) + Q_PROPERTY( bool NewEpisOnly READ NewEpisOnly WRITE setNewEpisOnly ) Q_PROPERTY( uint Filter READ Filter WRITE setFilter ) Q_PROPERTY( QString RecProfile READ RecProfile WRITE setRecProfile ) @@ -97,6 +98,7 @@ class SERVICE_PUBLIC RecRule : public QObject PROPERTYIMP ( int , EndOffset ) PROPERTYIMP ( QString , DupMethod ) PROPERTYIMP ( QString , DupIn ) + PROPERTYIMP ( bool , NewEpisOnly ) PROPERTYIMP ( uint , Filter ) PROPERTYIMP ( QString , RecProfile ) PROPERTYIMP ( QString , RecGroup ) @@ -135,6 +137,7 @@ class SERVICE_PUBLIC RecRule : public QObject m_PreferredInput( 0 ), m_StartOffset ( 0 ), m_EndOffset ( 0 ), + m_NewEpisOnly ( false ), m_Filter ( 0 ), m_AutoExpire ( false ), m_MaxEpisodes ( 0 ), @@ -179,6 +182,7 @@ class SERVICE_PUBLIC RecRule : public QObject m_EndOffset = src->m_EndOffset ; m_DupMethod = src->m_DupMethod ; m_DupIn = src->m_DupIn ; + m_NewEpisOnly = src->m_NewEpisOnly ; m_Filter = src->m_Filter ; m_RecProfile = src->m_RecProfile ; m_RecGroup = src->m_RecGroup ; diff --git a/mythtv/libs/libmythservicecontracts/datacontracts/videoStreamInfo.h b/mythtv/libs/libmythservicecontracts/datacontracts/videoStreamInfo.h new file mode 100644 index 0000000000..176d4d998b --- /dev/null +++ b/mythtv/libs/libmythservicecontracts/datacontracts/videoStreamInfo.h @@ -0,0 +1,93 @@ +////////////////////////////////////////////////////////////////////////////// +// Program Name: videoStreamInfo.h +// Created : May. 30, 2020 +// +// Copyright (c) 2020 Peter Bennett <pbennett@mythtv.org> +// +// Licensed under the GPL v2 or later, see COPYING for details +// +////////////////////////////////////////////////////////////////////////////// + +#ifndef VIDEOSTREAMINFO_H_ +#define VIDEOSTREAMINFO_H_ + +#include <QString> +#include <QDateTime> + +#include "serviceexp.h" +#include "datacontracthelper.h" + +namespace DTC +{ + +///////////////////////////////////////////////////////////////////////////// + +class SERVICE_PUBLIC VideoStreamInfo : public QObject +{ + Q_OBJECT + Q_CLASSINFO( "version" , "1.00" ); + + Q_PROPERTY( QString CodecType READ CodecType WRITE setCodecType ) + Q_PROPERTY( QString CodecName READ CodecName WRITE setCodecName ) + Q_PROPERTY( int Width READ Width WRITE setWidth ) + Q_PROPERTY( int Height READ Height WRITE setHeight ) + Q_PROPERTY( float AspectRatio READ AspectRatio WRITE setAspectRatio ) + Q_PROPERTY( QString FieldOrder READ FieldOrder WRITE setFieldOrder ) + Q_PROPERTY( float FrameRate READ FrameRate WRITE setFrameRate ) + Q_PROPERTY( float AvgFrameRate READ AvgFrameRate WRITE setAvgFrameRate ) + Q_PROPERTY( int Channels READ Channels WRITE setChannels ) + Q_PROPERTY( qlonglong Duration READ Duration WRITE setDuration ) + + PROPERTYIMP ( QString , CodecType ) + PROPERTYIMP ( QString , CodecName ) + PROPERTYIMP ( int , Width ) + PROPERTYIMP ( int , Height ) + PROPERTYIMP ( float , AspectRatio ) + PROPERTYIMP ( QString , FieldOrder ) + PROPERTYIMP ( float , FrameRate ) + PROPERTYIMP ( float , AvgFrameRate ) + PROPERTYIMP ( int , Channels ) + PROPERTYIMP ( qlonglong , Duration ) + + public: + + static inline void InitializeCustomTypes(); + + Q_INVOKABLE VideoStreamInfo(QObject *parent = nullptr) + : QObject ( parent ), + m_Width ( 0 ), + m_Height ( 0 ), + m_AspectRatio ( 0 ), + m_FrameRate ( 0 ), + m_AvgFrameRate ( 0 ), + m_Channels ( 0 ), + m_Duration ( 0 ) + { + } + + void Copy( const VideoStreamInfo *src ) + { + m_CodecType = src->m_CodecType ; + m_CodecName = src->m_CodecName ; + m_Width = src->m_Width ; + m_Height = src->m_Height ; + m_AspectRatio = src->m_AspectRatio ; + m_FieldOrder = src->m_FieldOrder ; + m_FrameRate = src->m_FrameRate ; + m_AvgFrameRate = src->m_AvgFrameRate ; + m_Channels = src->m_Channels ; + m_Duration = src->m_Duration ; + } + + private: + Q_DISABLE_COPY(VideoStreamInfo); +}; + +inline void VideoStreamInfo::InitializeCustomTypes() +{ + qRegisterMetaType< VideoStreamInfo* >(); +} + +} // namespace DTC + +#endif diff --git a/mythtv/libs/libmythservicecontracts/datacontracts/videoStreamInfoList.h b/mythtv/libs/libmythservicecontracts/datacontracts/videoStreamInfoList.h new file mode 100644 index 0000000000..fa9e417fcf --- /dev/null +++ b/mythtv/libs/libmythservicecontracts/datacontracts/videoStreamInfoList.h @@ -0,0 +1,99 @@ +////////////////////////////////////////////////////////////////////////////// +// Program Name: videoStreamInfoList.h +// Created : May. 30, 2020 +// +// Copyright (c) 2011 Peter Bennett <pbennett@mythtv.org> +// +// Licensed under the GPL v2 or later, see COPYING for details +// +////////////////////////////////////////////////////////////////////////////// + +#ifndef VIDEOSTREAMINFOLIST_H_ +#define VIDEOSTREAMINFOLIST_H_ + +#include <QVariantList> + +#include "serviceexp.h" +#include "datacontracthelper.h" + +#include "videoStreamInfo.h" + +namespace DTC +{ + +class SERVICE_PUBLIC VideoStreamInfoList : public QObject +{ + Q_OBJECT + Q_CLASSINFO( "version", "1.00" ); + + // Q_CLASSINFO Used to augment Metadata for properties. + // See datacontracthelper.h for details + + Q_CLASSINFO( "VideoStreamInfos", "type=DTC::VideoStreamInfo"); + Q_CLASSINFO( "AsOf" , "transient=true" ); + + Q_PROPERTY( int Count READ Count WRITE setCount ) + Q_PROPERTY( QDateTime AsOf READ AsOf WRITE setAsOf ) + Q_PROPERTY( QString Version READ Version WRITE setVersion ) + Q_PROPERTY( QString ProtoVer READ ProtoVer WRITE setProtoVer ) + Q_PROPERTY( int ErrorCode READ ErrorCode WRITE setErrorCode ) + Q_PROPERTY( QString ErrorMsg READ ErrorMsg WRITE setErrorMsg ) + + Q_PROPERTY( QVariantList VideoStreamInfos READ VideoStreamInfos DESIGNABLE true ) + + PROPERTYIMP ( int , Count ) + PROPERTYIMP ( QDateTime , AsOf ) + PROPERTYIMP ( QString , Version ) + PROPERTYIMP ( QString , ProtoVer ) + PROPERTYIMP ( int , ErrorCode ) + PROPERTYIMP ( QString , ErrorMsg ) + + PROPERTYIMP_RO_REF( QVariantList, VideoStreamInfos ); + + public: + + static inline void InitializeCustomTypes(); + + Q_INVOKABLE VideoStreamInfoList(QObject *parent = nullptr) + : QObject( parent ), + m_Count ( 0 ) + { + } + + void Copy( const VideoStreamInfoList *src ) + { + m_Count = src->m_Count ; + m_AsOf = src->m_AsOf ; + m_Version = src->m_Version ; + m_ProtoVer = src->m_ProtoVer ; + m_ErrorCode = src->m_ErrorCode ; + m_ErrorMsg = src->m_ErrorMsg ; + + CopyListContents< VideoStreamInfo >( this, m_VideoStreamInfos, src->m_VideoStreamInfos ); + } + + VideoStreamInfo *AddNewVideoStreamInfo() + { + // We must make sure the object added to the QVariantList has + // a parent of 'this' + + auto *pObject = new VideoStreamInfo( this ); + m_VideoStreamInfos.append( QVariant::fromValue<QObject *>( pObject )); + + return pObject; + } + + private: + Q_DISABLE_COPY(VideoStreamInfoList); +}; + +inline void VideoStreamInfoList::InitializeCustomTypes() +{ + qRegisterMetaType< VideoStreamInfoList* >(); + + VideoStreamInfo::InitializeCustomTypes(); +} + +} // namespace DTC + +#endif diff --git a/mythtv/libs/libmythservicecontracts/libmythservicecontracts.pro b/mythtv/libs/libmythservicecontracts/libmythservicecontracts.pro index 2c4a0e12d5..aee335ac7f 100644 --- a/mythtv/libs/libmythservicecontracts/libmythservicecontracts.pro +++ b/mythtv/libs/libmythservicecontracts/libmythservicecontracts.pro @@ -39,6 +39,7 @@ HEADERS += datacontracts/channelInfoList.h datacontracts/videoSource.h HEADERS += datacontracts/videoSourceList.h datacontracts/videoMultiplex.h HEADERS += datacontracts/videoMultiplexList.h datacontracts/videoMetadataInfo.h HEADERS += datacontracts/videoMetadataInfoList.h datacontracts/blurayInfo.h +HEADERS += datacontracts/videoStreamInfoList.h datacontracts/videoStreamInfo.h HEADERS += datacontracts/timeZoneInfo.h datacontracts/videoLookupInfo.h HEADERS += datacontracts/videoLookupInfoList.h datacontracts/versionInfo.h HEADERS += datacontracts/lineup.h datacontracts/captureCard.h @@ -103,6 +104,7 @@ incDatacontracts.files += datacontracts/wolInfo.h datacontracts/chan incDatacontracts.files += datacontracts/videoSource.h datacontracts/videoSourceList.h incDatacontracts.files += datacontracts/videoMultiplex.h datacontracts/videoMultiplexList.h incDatacontracts.files += datacontracts/videoMetadataInfo.h datacontracts/videoMetadataInfoList.h +incDatacontracts.files += datacontracts/videoSTreamInfo.h datacontracts/videoStreamInfoList.h incDatacontracts.files += datacontracts/musicMetadataInfo.h datacontracts/musicMetadataInfoList.h incDatacontracts.files += datacontracts/blurayInfo.h datacontracts/videoLookupInfo.h incDatacontracts.files += datacontracts/timeZoneInfo.h datacontracts/videoLookupInfoList.h diff --git a/mythtv/libs/libmythservicecontracts/services/dvrServices.h b/mythtv/libs/libmythservicecontracts/services/dvrServices.h index 30de373f00..d3041751ad 100644 --- a/mythtv/libs/libmythservicecontracts/services/dvrServices.h +++ b/mythtv/libs/libmythservicecontracts/services/dvrServices.h @@ -210,8 +210,10 @@ class SERVICE_PUBLIC DvrServices : public Service //, public QScriptable ??? uint PreferredInput, int StartOffset, int EndOffset, + QDateTime LastRecorded, QString DupMethod, QString DupIn, + bool NewEpisOnly, uint Filter, QString RecProfile, QString RecGroup, @@ -254,6 +256,7 @@ class SERVICE_PUBLIC DvrServices : public Service //, public QScriptable ??? int EndOffset, QString DupMethod, QString DupIn, + bool NewEpisOnly, uint Filter, QString RecProfile, QString RecGroup, diff --git a/mythtv/libs/libmythservicecontracts/services/videoServices.h b/mythtv/libs/libmythservicecontracts/services/videoServices.h index 894df6632e..b1d439e1ed 100644 --- a/mythtv/libs/libmythservicecontracts/services/videoServices.h +++ b/mythtv/libs/libmythservicecontracts/services/videoServices.h @@ -21,6 +21,7 @@ #include "datacontracts/videoMetadataInfoList.h" #include "datacontracts/videoLookupInfoList.h" #include "datacontracts/blurayInfo.h" +#include "datacontracts/videoStreamInfoList.h" ///////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////// @@ -46,6 +47,7 @@ class SERVICE_PUBLIC VideoServices : public Service //, public QScriptable ??? Q_CLASSINFO( "RemoveVideoFromDB_Method", "POST" ) Q_CLASSINFO( "UpdateVideoWatchedStatus_Method", "POST" ) Q_CLASSINFO( "UpdateVideoMetadata_Method", "POST" ) + Q_CLASSINFO( "SetSavedBookmark_Method", "POST" ) public: @@ -57,6 +59,7 @@ class SERVICE_PUBLIC VideoServices : public Service //, public QScriptable ??? DTC::VideoMetadataInfoList::InitializeCustomTypes(); DTC::VideoLookupList::InitializeCustomTypes(); DTC::BlurayInfo::InitializeCustomTypes(); + DTC::VideoStreamInfoList::InitializeCustomTypes(); } public slots: @@ -130,6 +133,15 @@ class SERVICE_PUBLIC VideoServices : public Service //, public QScriptable ??? const QString &Genres, const QString &Cast, const QString &Countries) = 0; + + virtual DTC::VideoStreamInfoList* GetStreamInfo ( const QString &StorageGroup, + const QString &FileName ) = 0; + + virtual long GetSavedBookmark ( int Id) = 0; + + virtual bool SetSavedBookmark ( int Id, + long Offset ) = 0; + }; #endif diff --git a/mythtv/libs/libmythtv/cardutil.cpp b/mythtv/libs/libmythtv/cardutil.cpp index cdc9aea470..005d0e5a19 100644 --- a/mythtv/libs/libmythtv/cardutil.cpp +++ b/mythtv/libs/libmythtv/cardutil.cpp @@ -265,7 +265,7 @@ bool CardUtil::IsTunerShared(uint inputidA, uint inputidB) if (!query.exec()) { - MythDB::DBError("CardUtil::is_tuner_shared", query); + MythDB::DBError("CardUtil::is_tuner_shared()", query); return false; } @@ -320,7 +320,7 @@ bool CardUtil::IsInputTypePresent(const QString &rawtype, QString hostname) if (!query.exec()) { - MythDB::DBError("CardUtil::IsInputTypePresent", query); + MythDB::DBError("CardUtil::IsInputTypePresent()", query); return false; } @@ -770,6 +770,9 @@ DTVTunerType CardUtil::ConvertToTunerType(DTVModulationSystem delsys) case DTVModulationSystem::kModulationSystem_DVBT2: tunertype = DTVTunerType::kTunerTypeDVBT2; break; + case DTVModulationSystem::kModulationSystem_DMBTH: + tunertype = DTVTunerType::kTunerTypeDVBT; + break; case DTVModulationSystem::kModulationSystem_ATSC: tunertype = DTVTunerType::kTunerTypeATSC; break; @@ -1229,7 +1232,7 @@ QString get_on_input(const QString &to_get, uint inputid) query.bindValue(":INPUTID", inputid); if (!query.exec()) - MythDB::DBError("CardUtil::get_on_source", query); + MythDB::DBError("CardUtil::get_on_input", query); else if (query.next()) return query.value(0).toString(); @@ -1597,7 +1600,7 @@ vector<uint> CardUtil::GetInputIDs(uint sourceid) if (!query.exec()) { - MythDB::DBError("CardUtil::GetInputIDs()", query); + MythDB::DBError("CardUtil::GetInputIDs(sourceid)", query); return list; } @@ -1618,7 +1621,7 @@ bool CardUtil::SetStartChannel(uint inputid, const QString &channum) if (!query.exec()) { - MythDB::DBError("set_startchan", query); + MythDB::DBError("CardUtil::SetStartChannel", query); return false; } @@ -1836,7 +1839,7 @@ int CardUtil::CreateCardInput(const uint inputid, if (!query.exec()) { - MythDB::DBError("CreateCardInput", query); + MythDB::DBError("CardUtil::CreateCardInput()", query); return -1; } @@ -1853,7 +1856,7 @@ uint CardUtil::CreateInputGroup(const QString &name) query.bindValue(":GROUPNAME", name); if (!query.exec()) { - MythDB::DBError("CreateNewInputGroup 0", query); + MythDB::DBError("CardUtil::CreateNewInputGroup 0", query); return 0; } @@ -1863,7 +1866,7 @@ uint CardUtil::CreateInputGroup(const QString &name) query.prepare("SELECT MAX(inputgroupid) FROM inputgroup"); if (!query.exec()) { - MythDB::DBError("CreateNewInputGroup 1", query); + MythDB::DBError("CardUtil::CreateNewInputGroup 1", query); return 0; } @@ -1878,7 +1881,7 @@ uint CardUtil::CreateInputGroup(const QString &name) query.bindValue(":GROUPNAME", name); if (!query.exec()) { - MythDB::DBError("CreateNewInputGroup 2", query); + MythDB::DBError("CardUtil::CreateNewInputGroup 2", query); return 0; } @@ -2704,7 +2707,9 @@ vector<uint> CardUtil::GetLiveTVInputList(void) QString CardUtil::GetDeviceName(dvb_dev_type_t type, const QString &device) { QString devname = QString(device); +#if 0 LOG(VB_RECORD, LOG_DEBUG, LOC + QString("DVB Device (%1)").arg(devname)); +#endif QString tmp = devname; if (DVB_DEV_FRONTEND == type) diff --git a/mythtv/libs/libmythtv/cardutil.h b/mythtv/libs/libmythtv/cardutil.h index 416c29da3b..55aae9d9ac 100644 --- a/mythtv/libs/libmythtv/cardutil.h +++ b/mythtv/libs/libmythtv/cardutil.h @@ -79,10 +79,16 @@ class MTV_PUBLIC CardUtil return ERROR_PROBE; if ("QPSK" == name) return QPSK; + if ("DVBS" == name) + return DVBS; if ("QAM" == name) return QAM; + if ("DVBC" == name) + return DVBC; if ("OFDM" == name) return OFDM; + if ("DVBT" == name) + return DVBT; if ("ATSC" == name) return ATSC; if ("V4L" == name) diff --git a/mythtv/libs/libmythtv/channelinfo.cpp b/mythtv/libs/libmythtv/channelinfo.cpp index 6a12848ba4..114c2db6ce 100644 --- a/mythtv/libs/libmythtv/channelinfo.cpp +++ b/mythtv/libs/libmythtv/channelinfo.cpp @@ -487,7 +487,7 @@ bool ChannelInsertInfo::IsSameChannel( if (relaxed > 1) { if (("mpeg" == m_siStandard || "mpeg" == other.m_siStandard || - "dvb" == m_siStandard || "dvb" == other.m_siStandard || + "dvb" == m_siStandard || "dvb" == other.m_siStandard || m_siStandard.isEmpty() || other.m_siStandard.isEmpty()) && (m_serviceId == other.m_serviceId)) { diff --git a/mythtv/libs/libmythtv/channelscan/channelimporter.cpp b/mythtv/libs/libmythtv/channelscan/channelimporter.cpp index 5670c1e7ad..9573feb934 100644 --- a/mythtv/libs/libmythtv/channelscan/channelimporter.cpp +++ b/mythtv/libs/libmythtv/channelscan/channelimporter.cpp @@ -76,6 +76,18 @@ void ChannelImporter::Process(const ScanDTVTransportList &_transports, cout << "Logical Channel Numbers only: " << (m_lcnOnly ? "yes" : "no") << endl; cout << "Complete scan data required : " << (m_completeOnly ? "yes" : "no") << endl; cout << "Full search for old channels: " << (m_fullChannelSearch ? "yes" : "no") << endl; + cout << "Remove duplicate channels : " << (m_removeDuplicates ? "yes" : "no") << endl; + } + + // List of transports + if (VERBOSE_LEVEL_CHECK(VB_CHANSCAN, LOG_ANY)) + { + if (transports.size() > 0) + { + cout << endl; + cout << "Transport list before processing (" << transports.size() << "):" << endl; + cout << FormatTransports(transports).toLatin1().constData() << endl; + } } // Print out each channel @@ -92,19 +104,33 @@ void ChannelImporter::Process(const ScanDTVTransportList &_transports, if (m_doSave) saved_scan = SaveScan(transports); - CleanupDuplicates(transports); - - FilterServices(transports); + // Merge transports with the same frequency into one + MergeSameFrequency(transports); - // Print out each transport - uint transports_scanned_size = transports.size(); - if (VERBOSE_LEVEL_CHECK(VB_CHANSCAN, LOG_ANY)) + // Remove duplicate transports with a lower signal strength. + if (m_removeDuplicates) { - cout << endl; - cout << "Transport list (" << transports_scanned_size << "):" << endl; - cout << FormatTransports(transports).toLatin1().constData() << endl; + ScanDTVTransportList duplicates; + RemoveDuplicates(transports, duplicates); + if (VERBOSE_LEVEL_CHECK(VB_CHANSCAN, LOG_ANY)) + { + if (duplicates.size() > 0) + { + cout << endl; + cout << "Discarded duplicate transports (" << duplicates.size() << "):" << endl; + cout << FormatTransports(duplicates).toLatin1().constData() << endl; + cout << endl; + cout << "With channels ("; + cout << SimpleCountChannels(duplicates) << "):" << endl; + cout << FormatChannels(duplicates).toLatin1().constData() << endl; + cout << endl; + } + } } + // Remove the channels that do not pass various criteria. + FilterServices(transports); + // Pull in DB info in transports // Channels not found in scan but only in DB are returned in db_trans sourceid = transports[0].m_channels[0].m_sourceId; @@ -114,7 +140,7 @@ void ChannelImporter::Process(const ScanDTVTransportList &_transports, if (!db_trans.empty()) { cout << endl; - cout << "Transport list of transports with channels in DB but not in scan ("; + cout << "Transports with channels in DB but not in scan ("; cout << db_trans.size() << "):" << endl; cout << FormatTransports(db_trans).toLatin1().constData() << endl; } @@ -124,7 +150,7 @@ void ChannelImporter::Process(const ScanDTVTransportList &_transports, FixUpOpenCable(transports); // All channels in the scan after comparing with the database - if (VERBOSE_LEVEL_CHECK(VB_CHANSCAN, LOG_ANY)) + if (VERBOSE_LEVEL_CHECK(VB_CHANSCAN, LOG_DEBUG)) { cout << endl << "Channel list after compare with database ("; cout << SimpleCountChannels(transports) << "):" << endl; @@ -155,10 +181,10 @@ void ChannelImporter::Process(const ScanDTVTransportList &_transports, // Print out each channel cout << endl; cout << "Channel list (" << SimpleCountChannels(transports) << "):" << endl; - cout << FormatChannels(transports, &info).toLatin1().constData() << endl; + cout << FormatChannels(transports).toLatin1().constData() << endl; // Create summary - QString msg = GetSummary(transports_scanned_size, info, stats); + QString msg = GetSummary(transports.size(), info, stats); cout << msg.toLatin1().constData() << endl << endl; if (m_doInsert) @@ -905,7 +931,12 @@ void ChannelImporter::AddChanToCopy( transport_copy.m_channels.push_back(chan); } -void ChannelImporter::CleanupDuplicates(ScanDTVTransportList &transports) +// ChannelImporter::MergeSameFrequency +// +// Merge transports that are on the same frequency by +// combining all channels of both transports into one transport +// +void ChannelImporter::MergeSameFrequency(ScanDTVTransportList &transports) { ScanDTVTransportList no_dups; @@ -950,11 +981,84 @@ void ChannelImporter::CleanupDuplicates(ScanDTVTransportList &transports) transports[i].m_channels.push_back(transports[j].m_channels[k]); } LOG(VB_CHANSCAN, LOG_INFO, LOC + - QString("Duplicate transport ") + FormatTransport(transports[j])); + QString("Transport on same frequency:") + FormatTransport(transports[j])); ignore[j] = true; } no_dups.push_back(transports[i]); } + transports = no_dups; +} + +// ChannelImporter::RemoveDuplicates +// +// When there are two transports that have the same list of channels +// but that are received on different frequencies then remove +// the transport with the weakest signal. +// +// In DVB two transports are duplicates when the original network ID and the +// transport ID are the same. This is possibly different in ATSC. +// Here all channels of both transports are compared. +// +void ChannelImporter::RemoveDuplicates(ScanDTVTransportList &transports, ScanDTVTransportList &duplicates) +{ + LOG(VB_CHANSCAN, LOG_INFO, LOC + + QString("Number of transports:%1").arg(transports.size())); + + ScanDTVTransportList no_dups; + vector<bool> ignore; + ignore.resize(transports.size()); + for (size_t i = 0; i < transports.size(); ++i) + { + ScanDTVTransport &ta = transports[i]; + LOG(VB_CHANSCAN, LOG_INFO, LOC + "Transport " + + FormatTransport(ta) + QString(" size(%1)").arg(ta.m_channels.size())); + + if (!ignore[i]) + { + for (size_t j = i+1; j < transports.size(); ++j) + { + ScanDTVTransport &tb = transports[j]; + bool found_same = true; + bool found_diff = true; + if (ta.m_channels.size() == tb.m_channels.size()) + { + LOG(VB_CHANSCAN, LOG_INFO, LOC + "Comparing transports " + + FormatTransport(ta) + QString(" size(%1)").arg(ta.m_channels.size()) + + FormatTransport(tb) + QString(" size(%1)").arg(tb.m_channels.size())); + + for (size_t k = 0; found_same && k < tb.m_channels.size(); ++k) + { + if (tb.m_channels[k].IsSameChannel(ta.m_channels[k], 0)) + { + found_diff = false; + } + else + { + found_same = false; + } + } + } + + // Transport with the lowest signal strength is duplicate + if (found_same && !found_diff) + { + size_t lowss = transports[i].m_signalStrength < transports[j].m_signalStrength ? i : j; + ignore[lowss] = true; + duplicates.push_back(transports[lowss]); + + LOG(VB_CHANSCAN, LOG_INFO, LOC + + "Duplicate transports found:" + + "\n\t" + "Transport A " + FormatTransport(transports[i]) + + "\n\t" + "Transport B " + FormatTransport(transports[j]) + + "\n\t" + "Discarding " + FormatTransport(transports[lowss])); + } + } + } + if (!ignore[i]) + { + no_dups.push_back(transports[i]); + } + } transports = no_dups; } @@ -1075,6 +1179,7 @@ ScanDTVTransportList ChannelImporter::GetDBTransports( return not_in_scan; } + QMap<uint,bool> found_in_scan; while (query.next()) { ScanDTVTransport db_transport; @@ -1088,31 +1193,35 @@ ScanDTVTransportList ChannelImporter::GetDBTransports( } bool found_transport = false; - QMap<uint,bool> found_chan; + QMap<uint,bool> found_in_database; // Search for old channels in the same transport of the scan. - for (auto & transport : transports) // All transports in scan - { // Scanned transport - if (transport.IsEqual(tuner_type, db_transport, 500 * freq_mult, true)) // Same transport? + for (size_t ist = 0; ist < transports.size(); ++ist) // All transports in scan + { + ScanDTVTransport &scan_transport = transports[ist]; // Transport from the scan + if (scan_transport.IsEqual(tuner_type, db_transport, 500 * freq_mult, true)) // Same transport? { - found_transport = true; - transport.m_mplex = db_transport.m_mplex; // Found multiplex - + found_transport = true; // Yes + scan_transport.m_mplex = db_transport.m_mplex; // Found multiplex for (size_t jdc = 0; jdc < db_transport.m_channels.size(); ++jdc) // All channels in database transport { - if (!found_chan[jdc]) // Channel not found yet? + if (!found_in_database[jdc]) // Channel not found yet? { ChannelInsertInfo &db_chan = db_transport.m_channels[jdc]; // Channel in database transport - - for (auto & chan : transport.m_channels) // All channels in scanned transport + for (size_t ksc = 0; ksc < scan_transport.m_channels.size(); ++ksc) // All channels in scanned transport { // Channel in scanned transport - if (db_chan.IsSameChannel(chan, 2)) // Same transport, relaxed check + if (!found_in_scan[(ist<<16)+ksc]) // Scanned channel not yet found? { - found_in_same_transport++; - found_chan[jdc] = true; // Found channel from database in scan - chan.m_dbMplexId = mplexid; // Found multiplex - chan.m_channelId = db_chan.m_channelId; // This is the crucial field - break; // Ready with scanned transport + ChannelInsertInfo &scan_chan = scan_transport.m_channels[ksc]; + if (db_chan.IsSameChannel(scan_chan, 2)) // Same transport, relaxed check + { + found_in_same_transport++; + found_in_database[jdc] = true; // Channel from db found in scan + found_in_scan[(ist<<16)+ksc] = true; // Channel from scan found in db + scan_chan.m_dbMplexId = db_transport.m_mplex; // Found multiplex + scan_chan.m_channelId = db_chan.m_channelId; // This is the crucial field + break; // Ready with scanned transport + } } } } @@ -1125,22 +1234,28 @@ ScanDTVTransportList ChannelImporter::GetDBTransports( // This can identify the channels that have moved to another transport. if (m_fullChannelSearch) { - for (size_t idc = 0; idc < db_transport.m_channels.size(); ++idc) // All channels in database transport + for (size_t ist = 0; ist < transports.size(); ++ist) // All transports in scan { - ChannelInsertInfo &db_chan = db_transport.m_channels[idc]; // Channel in database transport - - for (size_t jst = 0; jst < transports.size() && !found_chan[idc]; ++jst) // All transports in scan until found + ScanDTVTransport &scan_transport = transports[ist]; // Scanned transport + for (size_t jdc = 0; jdc < db_transport.m_channels.size(); ++jdc) // All channels in database transport { - ScanDTVTransport &transport = transports[jst]; // Scanned transport - for (auto & chan : transport.m_channels) // All channels in scanned transport + if (!found_in_database[jdc]) // Channel not found yet? { - // Channel in scanned transport - if (db_chan.IsSameChannel(chan, 1)) // Different transport, check - { // network id and service id - found_in_other_transport++; - found_chan[idc] = true; // Found channel from database in scan - chan.m_channelId = db_chan.m_channelId; // This is the crucial field - break; // Ready with scanned transport + ChannelInsertInfo &db_chan = db_transport.m_channels[jdc]; // Channel in database transport + for (size_t ksc = 0; ksc < scan_transport.m_channels.size(); ++ksc) // All channels in scanned transport + { + if (!found_in_scan[(ist<<16)+ksc]) // Scanned channel not yet found? + { + ChannelInsertInfo &scan_chan = scan_transport.m_channels[ksc]; + if (db_chan.IsSameChannel(scan_chan, 1)) // Other transport, check + { // network id and service id + found_in_other_transport++; + found_in_database[jdc] = true; // Channel from db found in scan + found_in_scan[(ist<<16)+ksc] = true; // Channel from scan found in db + scan_chan.m_channelId = db_chan.m_channelId; // This is the crucial field + break; // Ready with scanned transport + } + } } } } @@ -1157,7 +1272,7 @@ ScanDTVTransportList ChannelImporter::GetDBTransports( for (size_t idc = 0; idc < db_transport.m_channels.size(); ++idc) { - if (!found_chan[idc]) + if (!found_in_database[idc]) { tmp.m_channels.push_back(db_transport.m_channels[idc]); found_nowhere++; @@ -1279,8 +1394,7 @@ QString ChannelImporter::FormatChannel( QString msg; QTextStream ssMsg(&msg); - ssMsg << transport.m_modulation.toString().toLatin1().constData() - << ":"; + ssMsg << transport.m_modulation.toString().toLatin1().constData() << ":"; ssMsg << transport.m_frequency << ":"; QString si_standard = (chan.m_siStandard=="opencable") ? @@ -1447,6 +1561,9 @@ QString ChannelImporter::FormatTransport( QString msg; QTextStream ssMsg(&msg); ssMsg << transport.toString(); + ssMsg << QString(" onid:%1").arg(transport.m_networkID); + ssMsg << QString(" tsid:%1").arg(transport.m_transportID); + ssMsg << QString(" ss:%1").arg(transport.m_signalStrength); return msg; } diff --git a/mythtv/libs/libmythtv/channelscan/channelimporter.h b/mythtv/libs/libmythtv/channelscan/channelimporter.h index 92f56b1ba8..84ec87cb41 100644 --- a/mythtv/libs/libmythtv/channelscan/channelimporter.h +++ b/mythtv/libs/libmythtv/channelscan/channelimporter.h @@ -79,6 +79,7 @@ class MTV_PUBLIC ChannelImporter bool _delete, bool insert, bool save, bool fta_only, bool lcn_only, bool complete_only, bool full_channel_search, + bool remove_duplicates, ServiceRequirements service_requirements, bool success = false) : m_useGui(gui), @@ -90,6 +91,7 @@ class MTV_PUBLIC ChannelImporter m_lcnOnly(lcn_only), m_completeOnly(complete_only), m_fullChannelSearch(full_channel_search), + m_removeDuplicates(remove_duplicates), m_success(success), m_serviceRequirements(service_requirements) { } @@ -140,7 +142,8 @@ class MTV_PUBLIC ChannelImporter static QString toString(ChannelType type); - static void CleanupDuplicates(ScanDTVTransportList &transports); + static void MergeSameFrequency(ScanDTVTransportList &transports); + static void RemoveDuplicates(ScanDTVTransportList &transports, ScanDTVTransportList &duplicates); void FilterServices(ScanDTVTransportList &transports) const; ScanDTVTransportList GetDBTransports( uint sourceid, ScanDTVTransportList &transports) const; @@ -252,26 +255,20 @@ class MTV_PUBLIC ChannelImporter const ChannelInsertInfo &chan); private: - bool m_useGui; - bool m_isInteractive; - bool m_doDelete; - bool m_doInsert; - bool m_doSave; - /// Only FreeToAir (non-encrypted) channels desired post scan? - bool m_ftaOnly; - /// Only services with logical channel numbers desired post scan? - bool m_lcnOnly; - /// Only services with complete scandata desired post scan? - bool m_completeOnly; - /// Keep existing channel numbers on channel update - bool m_keepChannelNumbers {true}; - /// Full search for old channels - bool m_fullChannelSearch {false}; - /// To pass information IPTV channel scan succeeded - bool m_success {false}; - /// Services desired post scan - ServiceRequirements m_serviceRequirements; - + bool m_useGui; + bool m_isInteractive; + bool m_doDelete; + bool m_doInsert; + bool m_doSave; + bool m_ftaOnly {true}; // Only FreeToAir (non-encrypted) channels desired post scan? + bool m_lcnOnly {false}; // Only services with logical channel numbers desired post scan? + bool m_completeOnly {true}; // Only services with complete scandata desired post scan? + bool m_keepChannelNumbers {true}; // Keep existing channel numbers on channel update + bool m_fullChannelSearch {false}; // Full search for old channels across transports in database + bool m_removeDuplicates {false}; // Remove duplicate transports and channels in scan + bool m_success {false}; // To pass information IPTV channel scan succeeded + + ServiceRequirements m_serviceRequirements; // Services desired post scan QEventLoop m_eventLoop; }; diff --git a/mythtv/libs/libmythtv/channelscan/channelscan_sm.cpp b/mythtv/libs/libmythtv/channelscan/channelscan_sm.cpp index 52c6bb1d60..98e9f9f7d4 100644 --- a/mythtv/libs/libmythtv/channelscan/channelscan_sm.cpp +++ b/mythtv/libs/libmythtv/channelscan/channelscan_sm.cpp @@ -188,7 +188,8 @@ ChannelScanSM::ChannelScanSM(ScanMonitor *_scan_monitor, QString("Setting NIT-ID to %1").arg(nitid)); m_bouquetId = query.value(1).toUInt(); - m_regionId = query.value(2).toUInt(); + m_regionId = query.value(2).toUInt(); + m_nitId = nitid > 0 ? nitid : 0; } LOG(VB_CHANSCAN, LOG_INFO, LOC + @@ -352,15 +353,17 @@ bool ChannelScanSM::ScanExistingTransports(uint sourceid, bool follow_nit) return false; } - return m_scanning; } void ChannelScanSM::LogLines(const QString& string) { - QStringList lines = string.split('\n'); - for (int i = 0; i < lines.size(); ++i) - LOG(VB_CHANSCAN, LOG_DEBUG, lines[i]); + if (VERBOSE_LEVEL_CHECK(VB_CHANSCAN, LOG_DEBUG)) + { + QStringList lines = string.split('\n'); + for (int i = 0; i < lines.size(); ++i) + LOG(VB_CHANSCAN, LOG_DEBUG, lines[i]); + } } void ChannelScanSM::HandlePAT(const ProgramAssociationTable *pat) @@ -907,6 +910,12 @@ bool ChannelScanSM::UpdateChannelInfo(bool wait_until_complete) if (transport_tune_complete) { transport_tune_complete &= !m_currentInfo->m_pmts.empty(); + + if (!(sd->HasCachedMGT() || sd->HasCachedAnyNIT())) + { + transport_tune_complete = false; + } + if (sd->HasCachedMGT() || sd->HasCachedAnyVCTs()) { transport_tune_complete &= sd->HasCachedMGT(); @@ -926,7 +935,6 @@ bool ChannelScanSM::UpdateChannelInfo(bool wait_until_complete) { uint tsid = dtv_sm->GetTransportID(); LOG(VB_CHANSCAN, LOG_INFO, LOC + - QString("transport_tune_complete: ") + QString("\n\t\t\tsd->HasCachedAnyNIT(): %1").arg(sd->HasCachedAnyNIT()) + QString("\n\t\t\tsd->HasCachedAnySDTs(): %1").arg(sd->HasCachedAnySDTs()) + QString("\n\t\t\tsd->HasCachedAnyBATs(): %1").arg(sd->HasCachedAnyBATs()) + @@ -951,8 +959,7 @@ bool ChannelScanSM::UpdateChannelInfo(bool wait_until_complete) if (transport_tune_complete) { LOG(VB_CHANSCAN, LOG_INFO, LOC + - QString("transport_tune_complete: wait_until_complete %1") - .arg(wait_until_complete)); + QString("transport_tune_complete: wait_until_complete %1").arg(wait_until_complete)); } if (transport_tune_complete && @@ -1021,7 +1028,14 @@ bool ChannelScanSM::UpdateChannelInfo(bool wait_until_complete) { TransportScanItem &item = *m_current; item.m_tuning.m_frequency = item.freq_offset(m_current.offset()); + item.m_signalStrength = m_signalMonitor->GetSignalStrength(); + item.m_networkID = dtv_sm->GetNetworkID(); + item.m_transportID = dtv_sm->GetTransportID(); + if (m_scanDTVTunerType == DTVTunerType::kTunerTypeDVBT) + { + item.m_tuning.m_modSys = DTVModulationSystem::kModulationSystem_DVBT; + } if (m_scanDTVTunerType == DTVTunerType::kTunerTypeDVBT2) { if (m_dvbt2Tried) @@ -1031,8 +1045,9 @@ bool ChannelScanSM::UpdateChannelInfo(bool wait_until_complete) } LOG(VB_CHANSCAN, LOG_INFO, LOC + - QString("Adding %1 offset %2 to m_channelList.") - .arg((*m_current).m_tuning.toString()).arg(m_current.offset())); + QString("Adding %1 offset %2 ss %3 to m_channelList.") + .arg(item.m_tuning.toString()).arg(m_current.offset()) + .arg(item.m_signalStrength)); LOG(VB_CHANSCAN, LOG_DEBUG, LOC + QString("%1(%2) m_inputName: %3 ").arg(__FUNCTION__).arg(__LINE__).arg(m_inputName) + @@ -1130,9 +1145,9 @@ static void update_info(ChannelInsertInfo &info, info.m_chanNum.clear(); - info.m_serviceId = vct->ProgramNumber(i); - info.m_atscMajorChannel = vct->MajorChannel(i); - info.m_atscMinorChannel = vct->MinorChannel(i); + info.m_serviceId = vct->ProgramNumber(i); + info.m_atscMajorChannel = vct->MajorChannel(i); + info.m_atscMinorChannel = vct->MinorChannel(i); info.m_useOnAirGuide = !vct->IsHidden(i) || !vct->IsHiddenInGuide(i); @@ -1737,6 +1752,9 @@ ScanDTVTransportList ChannelScanSM::GetChannelList(bool addFullTS) const ScanDTVTransport item((*it.first).m_tuning, tuner_type, cardid); item.m_iptvTuning = (*(it.first)).m_iptvTuning; + item.m_signalStrength = (*(it.first)).m_signalStrength; + item.m_networkID = (*(it.first)).m_networkID; + item.m_transportID = (*(it.first)).m_transportID; QMap<uint,ChannelInsertInfo>::iterator dbchan_it; for (dbchan_it = pnum_to_dbchan.begin(); @@ -1809,7 +1827,6 @@ ScanDTVTransportList ChannelScanSM::GetChannelList(bool addFullTS) const return list; } - DTVSignalMonitor* ChannelScanSM::GetDTVSignalMonitor(void) { return dynamic_cast<DTVSignalMonitor*>(m_signalMonitor); @@ -1940,7 +1957,6 @@ bool ChannelScanSM::HasTimedOut(void) } #endif // USING_DVB - // have the tables have timed out? if (m_timer.hasExpired(m_channelTimeout)) { @@ -2047,8 +2063,7 @@ void ChannelScanSM::HandleActiveScan(void) { QString name = QString("TransportID %1").arg(it.key() & 0xffff); TransportScanItem item(m_sourceID, name, *it, m_signalTimeout); - LOG(VB_CHANSCAN, LOG_INFO, LOC + "Adding " + name + " - " + - item.m_tuning.toString()); + LOG(VB_CHANSCAN, LOG_INFO, LOC + "Adding " + name + ' ' + item.m_tuning.toString()); m_scanTransports.push_back(item); m_tsScanned.insert(it.key()); } @@ -2359,8 +2374,8 @@ bool ChannelScanSM::ScanIPTVChannels(uint sourceid, bool ChannelScanSM::ScanTransportsStartingOn( int sourceid, const QMap<QString,QString> &startChan) { - if (startChan.find("std") == startChan.end() || - startChan.find("type") == startChan.end()) + if (startChan.find("std") == startChan.end() || + startChan.find("type") == startChan.end()) { return false; } diff --git a/mythtv/libs/libmythtv/channelscan/channelscan_sm.h b/mythtv/libs/libmythtv/channelscan/channelscan_sm.h index f0e50e65aa..d809b0ccec 100644 --- a/mythtv/libs/libmythtv/channelscan/channelscan_sm.h +++ b/mythtv/libs/libmythtv/channelscan/channelscan_sm.h @@ -221,6 +221,7 @@ class ChannelScanSM : public MPEGStreamListener, uint m_frequency {0}; uint m_bouquetId {0}; uint m_regionId {0}; + uint m_nitId {0}; // Optional info DTVTunerType m_scanDTVTunerType {DTVTunerType::kTunerTypeUnknown}; diff --git a/mythtv/libs/libmythtv/channelscan/channelscanmiscsettings.h b/mythtv/libs/libmythtv/channelscan/channelscanmiscsettings.h index e0d395f47d..6798883869 100644 --- a/mythtv/libs/libmythtv/channelscan/channelscanmiscsettings.h +++ b/mythtv/libs/libmythtv/channelscan/channelscanmiscsettings.h @@ -171,6 +171,22 @@ class FullChannelSearch : public TransMythUICheckBoxSetting }; }; +class RemoveDuplicates : public TransMythUICheckBoxSetting +{ + public: + RemoveDuplicates() + { + setLabel(QObject::tr("Remove duplicates")); + setHelpText( + QObject::tr( + "If set, select the transport stream multiplex with the best signal " + "when identical transports are received on different frequencies. " + "This option is useful for DVB-T2 and ATSC/OTA when a transport " + "can sometimes be received from different transmitters.")); + setValue(true); + }; +}; + class AddFullTS : public TransMythUICheckBoxSetting { public: diff --git a/mythtv/libs/libmythtv/channelscan/channelscanner.cpp b/mythtv/libs/libmythtv/channelscan/channelscanner.cpp index a4b816a7f0..dbd671c657 100644 --- a/mythtv/libs/libmythtv/channelscan/channelscanner.cpp +++ b/mythtv/libs/libmythtv/channelscan/channelscanner.cpp @@ -120,6 +120,7 @@ void ChannelScanner::Scan( bool do_lcn_only, bool do_complete_only, bool do_full_channel_search, + bool do_remove_duplicates, bool do_add_full_ts, ServiceRequirements service_requirements, // stuff needed for particular scans @@ -135,6 +136,7 @@ void ChannelScanner::Scan( m_channelNumbersOnly = do_lcn_only; m_completeOnly = do_complete_only; m_fullSearch = do_full_channel_search; + m_removeDuplicates = do_remove_duplicates; m_addFullTS = do_add_full_ts; m_serviceRequirements = service_requirements; m_sourceid = sourceid; diff --git a/mythtv/libs/libmythtv/channelscan/channelscanner.h b/mythtv/libs/libmythtv/channelscan/channelscanner.h index 66e0f9b7d9..9afb92354f 100644 --- a/mythtv/libs/libmythtv/channelscan/channelscanner.h +++ b/mythtv/libs/libmythtv/channelscan/channelscanner.h @@ -79,6 +79,7 @@ class MTV_PUBLIC ChannelScanner bool do_lcn_only, bool do_complete_only, bool do_full_channel_search, + bool do_remove_duplicates, bool do_add_full_ts, ServiceRequirements service_requirements, // stuff needed for particular scans @@ -147,6 +148,9 @@ class MTV_PUBLIC ChannelScanner /// Extended search for old channels post scan? bool m_fullSearch {false}; + /// Remove duplicate transports and channels? + bool m_removeDuplicates {false}; + /// Add MPTS "full transport stream" channels bool m_addFullTS {false}; diff --git a/mythtv/libs/libmythtv/channelscan/channelscanner_cli.cpp b/mythtv/libs/libmythtv/channelscan/channelscanner_cli.cpp index eae66d8216..cd7cafc657 100644 --- a/mythtv/libs/libmythtv/channelscan/channelscanner_cli.cpp +++ b/mythtv/libs/libmythtv/channelscan/channelscanner_cli.cpp @@ -137,7 +137,7 @@ void ChannelScannerCLI::Process(const ScanDTVTransportList &_transports) { ChannelImporter ci(false, m_interactive, !m_onlysavescan, !m_onlysavescan, true, m_freeToAirOnly, m_channelNumbersOnly, m_completeOnly, - m_fullSearch, m_serviceRequirements); + m_fullSearch, m_removeDuplicates, m_serviceRequirements); ci.Process(_transports, m_sourceid); } diff --git a/mythtv/libs/libmythtv/channelscan/channelscanner_gui.cpp b/mythtv/libs/libmythtv/channelscan/channelscanner_gui.cpp index bc89b80264..c1f7f6437e 100644 --- a/mythtv/libs/libmythtv/channelscan/channelscanner_gui.cpp +++ b/mythtv/libs/libmythtv/channelscan/channelscanner_gui.cpp @@ -136,7 +136,7 @@ void ChannelScannerGUI::Process(const ScanDTVTransportList &_transports, { ChannelImporter ci(true, true, true, true, true, m_freeToAirOnly, m_channelNumbersOnly, m_completeOnly, - m_fullSearch, m_serviceRequirements, success); + m_fullSearch, m_removeDuplicates, m_serviceRequirements, success); ci.Process(_transports, m_sourceid); } diff --git a/mythtv/libs/libmythtv/channelscan/externrecscanner.cpp b/mythtv/libs/libmythtv/channelscan/externrecscanner.cpp index be13e4f968..11964840c6 100644 --- a/mythtv/libs/libmythtv/channelscan/externrecscanner.cpp +++ b/mythtv/libs/libmythtv/channelscan/externrecscanner.cpp @@ -120,9 +120,10 @@ void ExternRecChannelScanner::run(void) QString name; QString callsign; QString xmltvid; + QString icon; int cnt = 0; - if (!fetch.FirstChannel(channum, name, callsign, xmltvid)) + if (!fetch.FirstChannel(channum, name, callsign, xmltvid, icon)) { LOG(VB_CHANNEL, LOG_WARNING, LOC + "No channels found."); QMutexLocker locker(&m_lock); @@ -156,7 +157,7 @@ void ExternRecChannelScanner::run(void) ChannelUtil::CreateChannel(0, m_sourceId, chanid, callsign, name, channum, 1, 0, 0, false, kChannelVisible, QString(), - QString(), "Default", xmltvid); + icon, "Default", xmltvid); } else { @@ -166,7 +167,7 @@ void ExternRecChannelScanner::run(void) ChannelUtil::UpdateChannel(0, m_sourceId, chanid, callsign, name, channum, 1, 0, 0, false, kChannelVisible, QString(), - QString(), "Default", xmltvid); + icon, "Default", xmltvid); } SetNumChannelsInserted(cnt); @@ -178,7 +179,7 @@ void ExternRecChannelScanner::run(void) } if (++idx < m_channelTotal) - fetch.NextChannel(channum, name, callsign, xmltvid); + fetch.NextChannel(channum, name, callsign, xmltvid, icon); else break; } diff --git a/mythtv/libs/libmythtv/channelscan/frequencytablesetting.cpp b/mythtv/libs/libmythtv/channelscan/frequencytablesetting.cpp index cd7027917a..99ca0649b8 100644 --- a/mythtv/libs/libmythtv/channelscan/frequencytablesetting.cpp +++ b/mythtv/libs/libmythtv/channelscan/frequencytablesetting.cpp @@ -87,5 +87,6 @@ ScanNetwork::ScanNetwork() setLabel(QObject::tr("Country")); addSelection(QObject::tr("Germany"), "de", country == "de"); + addSelection(QObject::tr("Netherlands"), "nl", country == "nl"); addSelection(QObject::tr("United Kingdom"), "gb", country == "gb"); } diff --git a/mythtv/libs/libmythtv/channelscan/scanwizardconfig.cpp b/mythtv/libs/libmythtv/channelscan/scanwizardconfig.cpp index 5b7b5b34e0..3834cb1492 100644 --- a/mythtv/libs/libmythtv/channelscan/scanwizardconfig.cpp +++ b/mythtv/libs/libmythtv/channelscan/scanwizardconfig.cpp @@ -35,6 +35,7 @@ void ScanWizard::SetupConfig( m_lcnOnly = new ChannelNumbersOnly(); m_completeOnly = new CompleteChannelsOnly(); m_fullSearch = new FullChannelSearch(); + m_removeDuplicates = new RemoveDuplicates(); m_addFullTS = new AddFullTS(); m_trustEncSI = new TrustEncSISetting(); @@ -45,6 +46,7 @@ void ScanWizard::SetupConfig( addChild(m_lcnOnly); addChild(m_completeOnly); addChild(m_fullSearch); + addChild(m_removeDuplicates); addChild(m_addFullTS); addChild(m_trustEncSI); @@ -100,6 +102,11 @@ bool ScanWizard::DoFullChannelSearch(void) const return m_fullSearch->boolValue(); } +bool ScanWizard::DoRemoveDuplicates(void) const +{ + return m_removeDuplicates->boolValue(); +} + bool ScanWizard::DoAddFullTS(void) const { return m_addFullTS->boolValue(); diff --git a/mythtv/libs/libmythtv/channelscan/scanwizardconfig.h b/mythtv/libs/libmythtv/channelscan/scanwizardconfig.h index 6451da309b..8d6438c890 100644 --- a/mythtv/libs/libmythtv/channelscan/scanwizardconfig.h +++ b/mythtv/libs/libmythtv/channelscan/scanwizardconfig.h @@ -47,6 +47,7 @@ class FreeToAirOnly; class ChannelNumbersOnly; class CompleteChannelsOnly; class FullChannelSearch; +class RemoveDuplicates; class AddFullTS; class TrustEncSISetting; diff --git a/mythtv/libs/libmythtv/dbcheck.cpp b/mythtv/libs/libmythtv/dbcheck.cpp index 329a716a4d..90915e6cc8 100644 --- a/mythtv/libs/libmythtv/dbcheck.cpp +++ b/mythtv/libs/libmythtv/dbcheck.cpp @@ -1787,7 +1787,7 @@ nullptr " ADD COLUMN tid INT(11) NOT NULL DEFAULT '0' AFTER pid, " " ADD COLUMN filename VARCHAR(255) NOT NULL DEFAULT '' AFTER thread, " " ADD COLUMN line INT(11) NOT NULL DEFAULT '0' AFTER filename, " -" ADD COLUMN function VARCHAR(255) NOT NULL DEFAULT '' AFTER line;", +" ADD COLUMN `function` VARCHAR(255) NOT NULL DEFAULT '' AFTER line;", nullptr }; diff --git a/mythtv/libs/libmythtv/decoders/avformatdecoder.cpp b/mythtv/libs/libmythtv/decoders/avformatdecoder.cpp index 7ce2f85b6e..7165b73581 100644 --- a/mythtv/libs/libmythtv/decoders/avformatdecoder.cpp +++ b/mythtv/libs/libmythtv/decoders/avformatdecoder.cpp @@ -3153,8 +3153,9 @@ void AvFormatDecoder::MpegPreProcessPkt(AVStream *stream, AVPacket *pkt) int height = static_cast<int>(seq->height()) >> context->lowres; float aspect = seq->aspect(context->codec_id == AV_CODEC_ID_MPEG1VIDEO); if (stream->sample_aspect_ratio.num) - aspect = static_cast<float>(av_q2d(stream->sample_aspect_ratio) * - width / height); + aspect = static_cast<float>(av_q2d(stream->sample_aspect_ratio) * width / height); + if (aspect_override > 0.0F) + aspect = aspect_override; float seqFPS = seq->fps(); bool changed = (width != m_currentWidth ); @@ -3166,13 +3167,8 @@ void AvFormatDecoder::MpegPreProcessPkt(AVStream *stream, AVPacket *pkt) // ratio changes bool forceaspectchange = !qFuzzyCompare(m_currentAspect + 10.0F, aspect + 10.0F) && m_mythCodecCtx && m_mythCodecCtx->DecoderWillResetOnAspect(); - m_currentAspect = aspect; - // N.B. this will break aspect ratio change detection above - if (aspect_override > 0.0F) - m_currentAspect = aspect_override; - if (changed || forceaspectchange) { if (m_privateDec) diff --git a/mythtv/libs/libmythtv/decoders/mythcodeccontext.cpp b/mythtv/libs/libmythtv/decoders/mythcodeccontext.cpp index 0880bf8212..5932fafc78 100644 --- a/mythtv/libs/libmythtv/decoders/mythcodeccontext.cpp +++ b/mythtv/libs/libmythtv/decoders/mythcodeccontext.cpp @@ -141,6 +141,12 @@ QStringList MythCodecContext::GetDecoderDescription(void) void MythCodecContext::GetDecoders(RenderOptions &Opts) { + if (!HasMythMainWindow()) + { + LOG(VB_GENERAL, LOG_INFO, LOC + "No window: Ignoring hardware decoders"); + return; + } + #ifdef USING_VDPAU // Only enable VDPAU support if it is actually present if (MythVDPAUHelper::HaveVDPAU()) diff --git a/mythtv/libs/libmythtv/decoders/mythnvdeccontext.cpp b/mythtv/libs/libmythtv/decoders/mythnvdeccontext.cpp index a54a340acc..04cb460197 100644 --- a/mythtv/libs/libmythtv/decoders/mythnvdeccontext.cpp +++ b/mythtv/libs/libmythtv/decoders/mythnvdeccontext.cpp @@ -65,6 +65,11 @@ MythCodecID MythNVDECContext::GetSupportedCodec(AVCodecContext **Context, cudaVideoChromaFormat cudaformat = cudaVideoChromaFormat_Monochrome; VideoFrameType type = PixelFormatToFrameType((*Context)->pix_fmt); + uint depth = static_cast<uint>(ColorDepth(type) - 8); + QString desc = QString("'%1 %2 %3 Depth:%4 %5x%6'") + .arg(codecstr).arg(profile).arg(pixfmt).arg(depth + 8) + .arg((*Context)->width).arg((*Context)->height); + // N.B. on stream changes format is set to CUDA/NVDEC. This may break if the new // stream has an unsupported chroma but the decoder should fail gracefully - just later. if ((FMT_NVDEC == type) || (format_is_420(type))) @@ -74,57 +79,59 @@ MythCodecID MythNVDECContext::GetSupportedCodec(AVCodecContext **Context, else if (format_is_444(type)) cudaformat = cudaVideoChromaFormat_444; - uint depth = static_cast<uint>(ColorDepth(type) - 8); - bool supported = false; - if ((cudacodec == cudaVideoCodec_NumCodecs) || (cudaformat == cudaVideoChromaFormat_Monochrome)) + { + LOG(VB_PLAYBACK, LOG_DEBUG, LOC + "Unknown codec or format"); + LOG(VB_PLAYBACK, LOG_INFO, LOC + QString("NVDEC does NOT support %1").arg(desc)); return failure; + } // iterate over known decoder capabilities + bool supported = false; const std::vector<MythNVDECCaps>& profiles = MythNVDECContext::GetProfiles(); for (auto cap : profiles) { - if (cap.Supports(cudacodec, cudaformat, depth, (*Context)->width, (*Context)->width)) + if (cap.Supports(cudacodec, cudaformat, depth, (*Context)->width, (*Context)->height)) { supported = true; break; } } - QString desc = QString("'%1 %2 %3 Depth:%4 %5x%6'") - .arg(codecstr).arg(profile).arg(pixfmt).arg(depth + 8) - .arg((*Context)->width).arg((*Context)->height); + if (!supported) + { + LOG(VB_PLAYBACK, LOG_DEBUG, LOC + "No matching profile support"); + LOG(VB_PLAYBACK, LOG_INFO, LOC + QString("NVDEC does NOT support %1").arg(desc)); + return failure; + } // and finally try and retrieve the actual FFmpeg decoder - if (supported) + QString name = QString((*Codec)->name) + "_cuvid"; + if (name == "mpeg2video_cuvid") + name = "mpeg2_cuvid"; + for (int i = 0; ; i++) { - for (int i = 0; ; i++) - { - const AVCodecHWConfig *config = avcodec_get_hw_config(*Codec, i); - if (!config) - break; + const AVCodecHWConfig *config = avcodec_get_hw_config(*Codec, i); + if (!config) + break; - if ((config->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX) && - (config->device_type == AV_HWDEVICE_TYPE_CUDA)) + if ((config->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX) && + (config->device_type == AV_HWDEVICE_TYPE_CUDA)) + { + AVCodec *codec = avcodec_find_decoder_by_name(name.toLocal8Bit()); + if (codec) { - QString name = QString((*Codec)->name) + "_cuvid"; - if (name == "mpeg2video_cuvid") - name = "mpeg2_cuvid"; - AVCodec *codec = avcodec_find_decoder_by_name(name.toLocal8Bit()); - if (codec) - { - LOG(VB_PLAYBACK, LOG_INFO, LOC + QString("NVDEC supports decoding %1").arg(desc)); - *Codec = codec; - gCodecMap->freeCodecContext(Stream); - *Context = gCodecMap->getCodecContext(Stream, *Codec); - return success; - } - break; + LOG(VB_PLAYBACK, LOG_INFO, LOC + QString("NVDEC supports decoding %1").arg(desc)); + *Codec = codec; + gCodecMap->freeCodecContext(Stream); + *Context = gCodecMap->getCodecContext(Stream, *Codec); + return success; } + break; } } - LOG(VB_PLAYBACK, LOG_INFO, LOC + QString("NVDEC does NOT support %1").arg(desc)); + LOG(VB_GENERAL, LOG_ERR, LOC + QString("Failed to find decoder '%1'").arg(name)); return failure; } @@ -497,10 +504,23 @@ bool MythNVDECContext::MythNVDECCaps::Supports(cudaVideoCodec Codec, cudaVideoCh uint Depth, int Width, int Height) { uint mblocks = static_cast<uint>((Width * Height) / 256); - return (Codec == m_codec) && (Format == m_format) && (Depth == m_depth) && - (m_maximum.width() >= Width) && (m_maximum.height() >= Height) && - (m_minimum.width() <= Width) && (m_minimum.height() <= Height) && - (m_macroBlocks >= mblocks); + + LOG(VB_PLAYBACK, LOG_DEBUG, LOC + + QString("Trying to match: Codec %1 Format %2 Depth %3 Width %4 Height %5 MBs %6") + .arg(Codec).arg(Format).arg(Depth).arg(Width).arg(Height).arg(mblocks)); + LOG(VB_PLAYBACK, LOG_DEBUG, LOC + + QString("to this profile: Codec %1 Format %2 Depth %3 Width %4<->%5 Height %6<->%7 MBs %8") + .arg(m_codec).arg(m_format).arg(m_depth) + .arg(m_minimum.width()).arg(m_maximum.width()) + .arg(m_minimum.height()).arg(m_maximum.height()).arg(m_macroBlocks)); + + bool result = (Codec == m_codec) && (Format == m_format) && (Depth == m_depth) && + (m_maximum.width() >= Width) && (m_maximum.height() >= Height) && + (m_minimum.width() <= Width) && (m_minimum.height() <= Height) && + (m_macroBlocks >= mblocks); + + LOG(VB_PLAYBACK, LOG_DEBUG, LOC + QString("%1 Match").arg(result ? "" : "NO")); + return result; } bool MythNVDECContext::HaveNVDEC(void) @@ -524,9 +544,9 @@ bool MythNVDECContext::HaveNVDEC(void) LOG(VB_GENERAL, LOG_INFO, LOC + "Supported/available NVDEC decoders:"); for (auto profile : profiles) { - LOG(VB_GENERAL, LOG_INFO, LOC + - MythCodecContext::GetProfileDescription(profile.m_profile,profile.m_maximum, - profile.m_type, profile.m_depth + 8)); + QString desc = MythCodecContext::GetProfileDescription(profile.m_profile,profile.m_maximum, + profile.m_type, profile.m_depth + 8); + LOG(VB_GENERAL, LOG_INFO, LOC + desc + QString(" MBs: %1").arg(profile.m_macroBlocks)); } } } diff --git a/mythtv/libs/libmythtv/decoders/mythvaapicontext.cpp b/mythtv/libs/libmythtv/decoders/mythvaapicontext.cpp index 9819e98140..65bf676e3b 100644 --- a/mythtv/libs/libmythtv/decoders/mythvaapicontext.cpp +++ b/mythtv/libs/libmythtv/decoders/mythvaapicontext.cpp @@ -333,25 +333,38 @@ int MythVAAPIContext::InitialiseContext(AVCodecContext *Context) // MPEG2 on Ironlake where it seems to return I420 labelled as NV12. I420 is // buggy on Sandybridge (stride?) and produces a mixture of I420/NV12 frames // for H.264 on Ironlake. - int format = VA_FOURCC_NV12; - QString vendor = interop->GetVendor(); - if (vendor.contains("ironlake", Qt::CaseInsensitive)) - if (CODEC_IS_MPEG(Context->codec_id)) - format = VA_FOURCC_I420; + // This may need extending for AMD etc - if (format != VA_FOURCC_NV12) + QString vendor = interop->GetVendor(); + // Intel NUC + if (vendor.contains("iHD", Qt::CaseInsensitive) && vendor.contains("Intel", Qt::CaseInsensitive)) + { + vaapi_frames_ctx->attributes = nullptr; + vaapi_frames_ctx->nb_attributes = 0; + } + // i965 series + else { - auto vaapiid = static_cast<MythCodecID>(kCodec_MPEG1_VAAPI + (mpeg_version(Context->codec_id) - 1)); - LOG(VB_GENERAL, LOG_INFO, LOC + QString("Forcing surface format for %1 and %2 with driver '%3'") - .arg(toString(vaapiid)).arg(MythOpenGLInterop::TypeToString(type)).arg(vendor)); + int format = VA_FOURCC_NV12; + if (vendor.contains("ironlake", Qt::CaseInsensitive)) + if (CODEC_IS_MPEG(Context->codec_id)) + format = VA_FOURCC_I420; + + if (format != VA_FOURCC_NV12) + { + auto vaapiid = static_cast<MythCodecID>(kCodec_MPEG1_VAAPI + (mpeg_version(Context->codec_id) - 1)); + LOG(VB_GENERAL, LOG_INFO, LOC + QString("Forcing surface format for %1 and %2 with driver '%3'") + .arg(toString(vaapiid)).arg(MythOpenGLInterop::TypeToString(type)).arg(vendor)); + } + + VASurfaceAttrib prefs[3] = { + { VASurfaceAttribPixelFormat, VA_SURFACE_ATTRIB_SETTABLE, { VAGenericValueTypeInteger, { format } } }, + { VASurfaceAttribUsageHint, VA_SURFACE_ATTRIB_SETTABLE, { VAGenericValueTypeInteger, { VA_SURFACE_ATTRIB_USAGE_HINT_DISPLAY } } }, + { VASurfaceAttribMemoryType, VA_SURFACE_ATTRIB_SETTABLE, { VAGenericValueTypeInteger, { VA_SURFACE_ATTRIB_MEM_TYPE_VA} } } }; + vaapi_frames_ctx->attributes = prefs; + vaapi_frames_ctx->nb_attributes = 3; } - VASurfaceAttrib prefs[3] = { - { VASurfaceAttribPixelFormat, VA_SURFACE_ATTRIB_SETTABLE, { VAGenericValueTypeInteger, { format } } }, - { VASurfaceAttribUsageHint, VA_SURFACE_ATTRIB_SETTABLE, { VAGenericValueTypeInteger, { VA_SURFACE_ATTRIB_USAGE_HINT_DISPLAY } } }, - { VASurfaceAttribMemoryType, VA_SURFACE_ATTRIB_SETTABLE, { VAGenericValueTypeInteger, { VA_SURFACE_ATTRIB_MEM_TYPE_VA} } } }; - vaapi_frames_ctx->attributes = prefs; - vaapi_frames_ctx->nb_attributes = 3; hw_frames_ctx->sw_format = FramesFormat(Context->sw_pix_fmt); int referenceframes = AvFormatDecoder::GetMaxReferenceFrames(Context); hw_frames_ctx->initial_pool_size = static_cast<int>(VideoBuffers::GetNumBuffers(FMT_VAAPI, referenceframes, true)); diff --git a/mythtv/libs/libmythtv/decoders/mythvdpaucontext.cpp b/mythtv/libs/libmythtv/decoders/mythvdpaucontext.cpp index 73d31dd913..56e57ee2d2 100644 --- a/mythtv/libs/libmythtv/decoders/mythvdpaucontext.cpp +++ b/mythtv/libs/libmythtv/decoders/mythvdpaucontext.cpp @@ -106,7 +106,8 @@ int MythVDPAUContext::InitialiseContext(AVCodecContext* Context) } auto* vdpaudevicectx = static_cast<AVVDPAUDeviceContext*>(hwdevicecontext->hwctx); - if (av_vdpau_bind_context(Context, vdpaudevicectx->device, vdpaudevicectx->get_proc_address, 0) != 0) + if (av_vdpau_bind_context(Context, vdpaudevicectx->device, + vdpaudevicectx->get_proc_address, AV_HWACCEL_FLAG_IGNORE_LEVEL) != 0) { LOG(VB_GENERAL, LOG_ERR, LOC + "Failed to bind VDPAU context"); av_buffer_unref(&hwdeviceref); @@ -168,18 +169,27 @@ MythCodecID MythVDPAUContext::GetSupportedCodec(AVCodecContext **Context, vdpau = false; for (auto vdpauprofile : profiles) { - if (vdpauprofile.first == mythprofile && - vdpauprofile.second.Supported((*Context)->width, (*Context)->height, (*Context)->level)) + bool match = vdpauprofile.first == mythprofile; + if (match) { - vdpau = true; - break; + LOG(VB_PLAYBACK, LOG_DEBUG, LOC + QString("Trying %1") + .arg(MythCodecContext::GetProfileDescription(mythprofile, QSize()))); + if (vdpauprofile.second.Supported((*Context)->width, (*Context)->height, (*Context)->level)) + { + vdpau = true; + break; + } } } } // H264 needs additional checks for old hardware if (vdpau && (success == kCodec_H264_VDPAU || success == kCodec_H264_VDPAU_DEC)) + { vdpau = MythVDPAUHelper::CheckH264Decode(*Context); + if (!vdpau) + LOG(VB_PLAYBACK, LOG_DEBUG, LOC + "H264 decode check failed"); + } QString desc = QString("'%1 %2 %3 %4x%5'") .arg(codec).arg(profile).arg(pixfmt).arg((*Context)->width).arg((*Context)->height); diff --git a/mythtv/libs/libmythtv/decoders/mythvdpauhelper.cpp b/mythtv/libs/libmythtv/decoders/mythvdpauhelper.cpp index b8d3b2d1ef..918ce6275f 100644 --- a/mythtv/libs/libmythtv/decoders/mythvdpauhelper.cpp +++ b/mythtv/libs/libmythtv/decoders/mythvdpauhelper.cpp @@ -37,9 +37,17 @@ VDPAUCodec::VDPAUCodec(MythCodecContext::CodecProfile Profile, QSize Size, uint3 bool VDPAUCodec::Supported(int Width, int Height, int Level) { + // Note - level checks are now ignored here and in FFmpeg uint32_t macros = static_cast<uint32_t>(((Width + 15) & ~15) * ((Height + 15) & ~15)) / 256; - return (Width <= m_maxSize.width()) && (Height <= m_maxSize.height()) && - (macros <= m_maxMacroBlocks) && (static_cast<uint32_t>(Level) <= m_maxLevel); + bool result = (Width <= m_maxSize.width()) && (Height <= m_maxSize.height()) && + (macros <= m_maxMacroBlocks) /*&& (static_cast<uint32_t>(Level) <= m_maxLevel)*/; + if (!result) + { + LOG(VB_PLAYBACK, LOG_DEBUG, LOC + QString("Not supported: Size %1x%2 > %3x%4, MBs %5 > %6, Level %7 > %8") + .arg(Width).arg(Height).arg(m_maxSize.width()).arg(m_maxSize.height()) + .arg(macros).arg(m_maxMacroBlocks).arg(Level).arg(m_maxLevel)); + } + return result; } bool MythVDPAUHelper::HaveVDPAU(void) @@ -80,11 +88,33 @@ bool MythVDPAUHelper::ProfileCheck(VdpDecoderProfile Profile, uint32_t &Level, return false; INIT_ST - VdpBool supported = 0; + VdpBool supported = VDP_FALSE; status = m_vdpDecoderQueryCapabilities(m_device, Profile, &supported, &Level, &Macros, &Width, &Height); CHECK_ST - return supported > 0; + + LOG(VB_PLAYBACK, LOG_DEBUG, LOC + QString("ProfileCheck: Prof %1 Supp %2 Level %3 Macros %4 Width %5 Height %6 Status %7") + .arg(Profile).arg(supported).arg(Level).arg(Macros).arg(Width).arg(Height).arg(status)); + + if (((supported != VDP_TRUE) || (status != VDP_STATUS_OK)) && + (Profile == VDP_DECODER_PROFILE_H264_CONSTRAINED_BASELINE || + Profile == VDP_DECODER_PROFILE_H264_BASELINE)) + { + LOG(VB_GENERAL, LOG_INFO, LOC + QString("Driver does not report support for H264 %1Baseline") + .arg(Profile == VDP_DECODER_PROFILE_H264_CONSTRAINED_BASELINE ? "Constrained " : "")); + + // H264 Constrained baseline is reported as not supported on older chipsets but + // works due to support for H264 Main. Test for H264 main if constrained baseline + // fails - which mimics the fallback in FFmpeg. + // Updated to included baseline... not so sure about that:) + status = m_vdpDecoderQueryCapabilities(m_device, VDP_DECODER_PROFILE_H264_MAIN, &supported, + &Level, &Macros, &Width, &Height); + CHECK_ST + if (supported == VDP_TRUE) + LOG(VB_GENERAL, LOG_INFO, LOC + "... but assuming available as H264 Main is supported"); + } + + return supported == VDP_TRUE; } const VDPAUProfiles& MythVDPAUHelper::GetProfiles(void) @@ -324,9 +354,7 @@ bool MythVDPAUHelper::CheckH264Decode(AVCodecContext *Context) switch (Context->profile & ~FF_PROFILE_H264_INTRA) { case FF_PROFILE_H264_BASELINE: profile = VDP_DECODER_PROFILE_H264_BASELINE; break; -#ifdef VDP_DECODER_PROFILE_H264_CONSTRAINED_BASELINE case FF_PROFILE_H264_CONSTRAINED_BASELINE: profile = VDP_DECODER_PROFILE_H264_CONSTRAINED_BASELINE; break; -#endif case FF_PROFILE_H264_MAIN: profile = VDP_DECODER_PROFILE_H264_MAIN; break; case FF_PROFILE_H264_HIGH: profile = VDP_DECODER_PROFILE_H264_HIGH; break; #ifdef VDP_DECODER_PROFILE_H264_EXTENDED diff --git a/mythtv/libs/libmythtv/dtvmultiplex.cpp b/mythtv/libs/libmythtv/dtvmultiplex.cpp index 26ac36c3d3..a55f1251ee 100644 --- a/mythtv/libs/libmythtv/dtvmultiplex.cpp +++ b/mythtv/libs/libmythtv/dtvmultiplex.cpp @@ -41,8 +41,8 @@ QString DTVMultiplex::toString() const .arg(m_bandwidth.toString()).arg(m_transMode.toString()) .arg(m_guardInterval.toString()).arg(m_hierarchy.toString()) .arg(m_polarity.toString()); - ret += QString(" fec: %1 msys: %2 rolloff: %3") - .arg(m_fec.toString()).arg(m_modSys.toString()).arg(m_rolloff.toString()); + ret += QString(" fec:%1 msys:%2 rolloff:%3") + .arg(m_fec.toString(),-4).arg(m_modSys.toString(),-6).arg(m_rolloff.toString()); return ret; } @@ -509,7 +509,7 @@ bool DTVMultiplex::FillFromDeliverySystemDesc(DTVTunerType type, return false; } - return ParseDVB_S_and_C( + return ParseDVB_S( QString::number(cd.FrequencykHz()), "a", QString::number(cd.SymbolRateHz()), cd.FECInnerString(), cd.ModulationString(), diff --git a/mythtv/libs/libmythtv/dtvmultiplex.h b/mythtv/libs/libmythtv/dtvmultiplex.h index 8bb3c2db4c..d7bc90f8ae 100644 --- a/mythtv/libs/libmythtv/dtvmultiplex.h +++ b/mythtv/libs/libmythtv/dtvmultiplex.h @@ -103,7 +103,7 @@ class MTV_PUBLIC DTVMultiplex DTVHierarchy m_hierarchy; DTVPolarity m_polarity; DTVCodeRate m_fec; ///< Inner Forward Error Correction rate - DTVModulationSystem m_modSys; ///< Modulation system + DTVModulationSystem m_modSys; ///< Modulation system DTVRollOff m_rolloff; // Optional additional info @@ -133,9 +133,12 @@ class MTV_PUBLIC ScanDTVTransport : public DTVMultiplex const QString& mod_sys, const QString& rolloff); public: - DTVTunerType m_tuner_type {DTVTunerType::kTunerTypeUnknown}; - uint m_cardid {0}; + DTVTunerType m_tuner_type {DTVTunerType::kTunerTypeUnknown}; + uint m_cardid {0}; ChannelInsertInfoList m_channels; + uint m_networkID {0}; + uint m_transportID {0}; + int m_signalStrength {0}; }; using ScanDTVTransportList = vector<ScanDTVTransport>; diff --git a/mythtv/libs/libmythtv/frequencytables.cpp b/mythtv/libs/libmythtv/frequencytables.cpp index 55e021fa5c..26416adfb7 100644 --- a/mythtv/libs/libmythtv/frequencytables.cpp +++ b/mythtv/libs/libmythtv/frequencytables.cpp @@ -194,8 +194,11 @@ QString TransportScanItem::toString() const .arg(m_tuning.m_transMode) .arg(m_tuning.m_guardInterval) .arg(m_tuning.m_hierarchy); + str += QString("\t symbol_rate(%1) fec(%2)\n") + .arg(m_tuning.m_symbolRate) + .arg(m_tuning.m_fec); } - str += QString("\t offset[0..2]: %1 %2 %3") + str += QString("\toffset[0..2]: %1 %2 %3") .arg(m_freqOffsets[0]).arg(m_freqOffsets[1]).arg(m_freqOffsets[2]); return str; } @@ -526,6 +529,12 @@ static void init_freq_tables(freq_table_map_t &fmap) DTVCodeRate::kFECAuto, DTVModulation::kModulationQAMAuto, 6900000, 0, 0); + // DVB-C Netherlands + fmap["dvbc_qam_nl0"] = new FrequencyTable( + 474000000, 474000000, 8000000, "Channel %1", 21, + DTVCodeRate::kFECAuto, DTVModulation::kModulationQAM64, + 6875000, 0, 0); + // DVB-C United Kingdom fmap["dvbc_qam_gb0"] = new FrequencyTable( 12324000, 12324000+1, 10, "Channel %1", 1, diff --git a/mythtv/libs/libmythtv/frequencytables.h b/mythtv/libs/libmythtv/frequencytables.h index 7eead33d7c..a2e2979ec0 100644 --- a/mythtv/libs/libmythtv/frequencytables.h +++ b/mythtv/libs/libmythtv/frequencytables.h @@ -185,6 +185,10 @@ class TransportScanItem QString m_iptvChannel; ///< IPTV base channel DTVChannelInfoList m_expectedChannels; + + int m_signalStrength {0}; + uint m_networkID {0}; + uint m_transportID {0}; }; class transport_scan_items_it_t diff --git a/mythtv/libs/libmythtv/libmythtv.pro b/mythtv/libs/libmythtv/libmythtv.pro index 036228f2d3..646577cd5b 100644 --- a/mythtv/libs/libmythtv/libmythtv.pro +++ b/mythtv/libs/libmythtv/libmythtv.pro @@ -66,7 +66,7 @@ macx { LIBS += -framework OpenGL LIBS += -framework IOKit LIBS += -framework CoreVideo - LIBS += -framework VideoToolBox + LIBS += -framework VideoToolbox LIBS += -framework IOSurface DEFINES += USING_VTB HEADERS += decoders/mythvtbcontext.h @@ -169,7 +169,7 @@ SOURCES += channelgroup.cpp SOURCES += recordingrule.cpp SOURCES += mythsystemevent.cpp SOURCES += avfringbuffer.cpp -SOURCES += ringbuffer.cpp fileringBuffer.cpp +SOURCES += ringbuffer.cpp fileringbuffer.cpp SOURCES += streamingringbuffer.cpp metadataimagehelper.cpp SOURCES += icringbuffer.cpp SOURCES += mythframe.cpp mythavutil.cpp diff --git a/mythtv/libs/libmythtv/mpeg/dishdescriptors.cpp b/mythtv/libs/libmythtv/mpeg/dishdescriptors.cpp index b549379261..62224aabf9 100644 --- a/mythtv/libs/libmythtv/mpeg/dishdescriptors.cpp +++ b/mythtv/libs/libmythtv/mpeg/dishdescriptors.cpp @@ -158,7 +158,7 @@ QDate DishEventTagsDescriptor::originalairdate(void) const QDate originalairdate = t.date(); - if (originalairdate.year() < 1940) + if (originalairdate.year() < 1895) return {}; return originalairdate; diff --git a/mythtv/libs/libmythtv/mpeg/dvbdescriptors.cpp b/mythtv/libs/libmythtv/mpeg/dvbdescriptors.cpp index 1212bbc9cf..58f5b372df 100644 --- a/mythtv/libs/libmythtv/mpeg/dvbdescriptors.cpp +++ b/mythtv/libs/libmythtv/mpeg/dvbdescriptors.cpp @@ -595,6 +595,18 @@ QString TerrestrialDeliverySystemDescriptor::toString() const return str; } +QString T2TerrestrialDeliverySystemDescriptor::toString() const +{ + QString str = QString("T2TerrestrialDeliverySystemDescriptor: "); + str.append(QString("plp_id(%1) T2_system_id(%2)") + .arg(PlpID()) + .arg(T2SystemID())); + // + // TBD + // + return str; +} + QString DVBLogicalChannelDescriptor::toString() const { QString ret = "UKChannelListDescriptor sid->chan_num: "; diff --git a/mythtv/libs/libmythtv/mpeg/dvbdescriptors.h b/mythtv/libs/libmythtv/mpeg/dvbdescriptors.h index b6d5718762..4ccf33bcd8 100644 --- a/mythtv/libs/libmythtv/mpeg/dvbdescriptors.h +++ b/mythtv/libs/libmythtv/mpeg/dvbdescriptors.h @@ -1041,6 +1041,37 @@ class TerrestrialDeliverySystemDescriptor : public MPEGDescriptor QString toString(void) const override; // MPEGDescriptor }; +// DVB Bluebook A038 (Feb 2019) p 104 +class T2TerrestrialDeliverySystemDescriptor : public MPEGDescriptor +{ + public: + explicit T2TerrestrialDeliverySystemDescriptor( + const unsigned char *data, int len = 300) : + MPEGDescriptor(data, len, DescriptorID::t2_terrestrial_delivery_system) { } + // Name bits loc expected value + // descriptor_tag 8 0.0 0x7f + // descriptor_length 8 1.0 + // descriptor_tag_extension 8 2.0 0x4 + + // plp_id 8 3.0 + uint PlpID(void) const + { + return m_data[3]; + } + + // T2_system_id 16 4.0 + uint T2SystemID(void) const + { + return ((m_data[4]<<8) | (m_data[5])); + } + + // + // TBD + // + + QString toString(void) const override; // MPEGDescriptor +}; + // DVB Bluebook A038 (Sept 2011) p 58 class DSNGDescriptor : public MPEGDescriptor { diff --git a/mythtv/libs/libmythtv/mpeg/mpegdescriptors.cpp b/mythtv/libs/libmythtv/mpeg/mpegdescriptors.cpp index e53a24e2c1..7d026ef10b 100644 --- a/mythtv/libs/libmythtv/mpeg/mpegdescriptors.cpp +++ b/mythtv/libs/libmythtv/mpeg/mpegdescriptors.cpp @@ -491,6 +491,10 @@ QString MPEGDescriptor::toStringPD(uint priv_dsid) const { SET_STRING(DefaultAuthorityDescriptor); } + else if (DescriptorID::t2_terrestrial_delivery_system == DescriptorTag()) + { + SET_STRING(T2TerrestrialDeliverySystemDescriptor); + } // // User Defined DVB descriptors, range 0x80-0xFE else if (priv_dsid == PrivateDataSpecifierID::BSB1 && diff --git a/mythtv/libs/libmythtv/mythavutil.cpp b/mythtv/libs/libmythtv/mythavutil.cpp index 0905bf6ada..4f18e37cca 100644 --- a/mythtv/libs/libmythtv/mythavutil.cpp +++ b/mythtv/libs/libmythtv/mythavutil.cpp @@ -20,6 +20,7 @@ extern "C" { #include "libavformat/avformat.h" } #include <QMutexLocker> +#include <QFile> AVPixelFormat FrameTypeToPixelFormat(VideoFrameType type) { @@ -576,3 +577,140 @@ void MythCodecMap::freeAllCodecContexts() freeCodecContext(stream); } } + +MythStreamInfoList::MythStreamInfoList(QString filename) +{ + const int probeBufferSize = 8 * 1024; + AVInputFormat *fmt = nullptr; + AVProbeData probe; + memset(&probe, 0, sizeof(AVProbeData)); + probe.filename = ""; + probe.buf = new unsigned char[probeBufferSize + AVPROBE_PADDING_SIZE]; + probe.buf_size = probeBufferSize; + memset(probe.buf, 0, probeBufferSize + AVPROBE_PADDING_SIZE); + av_log_set_level(AV_LOG_FATAL); + m_errorCode = 0; + if (filename == "") + m_errorCode = 97; + QFile infile(filename); + if (m_errorCode == 0 && !infile.open(QIODevice::ReadOnly)) + m_errorCode = 99; + if (m_errorCode==0) { + int64_t leng = infile.read(reinterpret_cast<char*>(probe.buf), probeBufferSize); + probe.buf_size = static_cast<int>(leng); + infile.close(); + fmt = av_probe_input_format(&probe, static_cast<int>(true)); + if (fmt == nullptr) + m_errorCode = 98; + } + AVFormatContext *ctx = nullptr; + if (m_errorCode==0) + { + ctx = avformat_alloc_context(); + m_errorCode = avformat_open_input(&ctx, filename.toUtf8(), fmt, nullptr); + } + if (m_errorCode==0) + m_errorCode = avformat_find_stream_info(ctx, nullptr); + + if (m_errorCode==0) + { + for (uint ix = 0; ix < ctx->nb_streams; ix++) + { + AVStream *stream = ctx->streams[ix]; + if (stream == nullptr) + continue; + AVCodecParameters *codecpar = stream->codecpar; + const AVCodecDescriptor* desc = nullptr; + if (codecpar != nullptr) + desc = avcodec_descriptor_get(codecpar->codec_id); + MythStreamInfo info; + info.m_codecType = ' '; + switch (codecpar->codec_type) + { + case AVMEDIA_TYPE_VIDEO: + info.m_codecType = 'V'; + break; + case AVMEDIA_TYPE_AUDIO: + info.m_codecType = 'A'; + break; + case AVMEDIA_TYPE_SUBTITLE: + info.m_codecType = 'S'; + break; + default: + continue; + } + if (desc != nullptr) + info.m_codecName = desc->name; + info.m_duration = stream->duration * stream->time_base.num / stream->time_base.den; + if (info.m_codecType == 'V') + { + if (codecpar != nullptr) + { + info.m_width = codecpar->width; + info.m_height = codecpar->height; + info.m_SampleAspectRatio = static_cast<float>(codecpar->sample_aspect_ratio.num) + / static_cast<float>(codecpar->sample_aspect_ratio.den); + switch (codecpar->field_order) + { + case AV_FIELD_PROGRESSIVE: + info.m_fieldOrder = "PR"; + break; + case AV_FIELD_TT: + info.m_fieldOrder = "TT"; + break; + case AV_FIELD_BB: + info.m_fieldOrder = "BB"; + break; + case AV_FIELD_TB: + info.m_fieldOrder = "TB"; + break; + case AV_FIELD_BT: + info.m_fieldOrder = "BT"; + break; + default: + break; + } + } + info.m_frameRate = static_cast<float>(stream->r_frame_rate.num) + / static_cast<float>(stream->r_frame_rate.den); + info.m_avgFrameRate = static_cast<float>(stream->avg_frame_rate.num) + / static_cast<float>(stream->avg_frame_rate.den); + } + if (info.m_codecType == 'A') + info.m_channels = codecpar->channels; + m_streamInfoList.append(info); + } + } + if (m_errorCode != 0) + { + switch(m_errorCode) { + case 97: + m_errorMsg = "File Not Found"; + break; + case 98: + m_errorMsg = "av_probe_input_format returned no result"; + break; + case 99: + m_errorMsg = "File could not be opened"; + break; + default: + char errbuf[256]; + if (av_strerror(m_errorCode, errbuf, sizeof errbuf) == 0) + m_errorMsg = QString(errbuf); + else + m_errorMsg = "UNKNOWN"; + } + LOG(VB_GENERAL, LOG_ERR, + QString("MythStreamInfoList failed for %1. Error code:%2 Message:%3") + .arg(filename).arg(m_errorCode).arg(m_errorMsg)); + + } + + if (ctx != nullptr) + { + avformat_close_input(&ctx); + avformat_free_context(ctx); + } + if (probe.buf != nullptr) + delete probe.buf; +} \ No newline at end of file diff --git a/mythtv/libs/libmythtv/mythavutil.h b/mythtv/libs/libmythtv/mythavutil.h index c58dcf1c0a..a3730e54e8 100644 --- a/mythtv/libs/libmythtv/mythavutil.h +++ b/mythtv/libs/libmythtv/mythavutil.h @@ -16,6 +16,7 @@ extern "C" { #include <QMap> #include <QMutex> +#include <QVector> struct AVFilterGraph; struct AVFilterContext; @@ -198,4 +199,39 @@ private: float m_ar; bool m_errored {false}; }; + + +class MTV_PUBLIC MythStreamInfo { +public: + // These are for All types + char m_codecType {' '}; // V=video, A=audio, S=subtitle + QString m_codecName; + int64_t m_duration {0}; + // These are for Video only + int m_width {0}; + int m_height {0}; + float m_SampleAspectRatio {0.0}; + // AV_FIELD_TT, //< Top coded_first, top displayed first + // AV_FIELD_BB, //< Bottom coded first, bottom displayed first + // AV_FIELD_TB, //< Top coded first, bottom displayed first + // AV_FIELD_BT, //< Bottom coded first, top displayed first + QString m_fieldOrder {"UN"}; // UNknown, PRogressive, TT, BB, TB, BT + float m_frameRate {0.0}; + float m_avgFrameRate {0.0}; + // This is for audio only + int m_channels {0}; +}; + + +/* +* Class to get stream info, used by service Video/GetStreamInfo +*/ +class MTV_PUBLIC MythStreamInfoList { +public: + MythStreamInfoList(QString filename); + int m_errorCode {0}; + QString m_errorMsg; + QVector<MythStreamInfo> m_streamInfoList; +}; + #endif diff --git a/mythtv/libs/libmythtv/mythdeinterlacer.cpp b/mythtv/libs/libmythtv/mythdeinterlacer.cpp index 7c22a484d2..0b19b69b12 100644 --- a/mythtv/libs/libmythtv/mythdeinterlacer.cpp +++ b/mythtv/libs/libmythtv/mythdeinterlacer.cpp @@ -70,13 +70,14 @@ void MythDeinterlacer::Filter(VideoFrame *Frame, FrameScanType Scan, VideoDisplayProfile *Profile, bool Force) { // nothing to see here - if (!Frame || (Scan != kScan_Interlaced && Scan != kScan_Intr2ndField)) + + if (!Frame || !is_interlaced(Scan)) { Cleanup(); return; } - if (Frame && Frame->already_deinterlaced) + if (Frame->already_deinterlaced) return; // Sanity check frame format diff --git a/mythtv/libs/libmythtv/mythplayer.cpp b/mythtv/libs/libmythtv/mythplayer.cpp index ec0d58d4c2..1eecc103cf 100644 --- a/mythtv/libs/libmythtv/mythplayer.cpp +++ b/mythtv/libs/libmythtv/mythplayer.cpp @@ -1868,7 +1868,11 @@ void MythPlayer::AVSync(VideoFrame *buffer) m_osdLock.lock(); // Only double rate CPU deinterlacers require an extra call to ProcessFrame if (GetDoubleRateOption(buffer, DEINT_CPU) && !GetDoubleRateOption(buffer, DEINT_SHADER)) + { + // the first deinterlacing pass will have marked the frame as already deinterlaced + buffer->already_deinterlaced = false; m_videoOutput->ProcessFrame(buffer, m_osd, m_pipPlayers, ps); + } m_videoOutput->PrepareFrame(buffer, ps, m_osd); m_osdLock.unlock(); // Display the second field @@ -4522,9 +4526,9 @@ char *MythPlayer::GetScreenGrabAtFrame(uint64_t FrameNum, bool Absolute, if (frame->interlaced_frame) { - // Use medium quality - which is currently yadif + // Use high quality - which is currently yadif frame->deinterlace_double = DEINT_NONE; - frame->deinterlace_allowed = frame->deinterlace_single = DEINT_CPU | DEINT_MEDIUM; + frame->deinterlace_allowed = frame->deinterlace_single = DEINT_CPU | DEINT_HIGH; MythDeinterlacer deinterlacer; deinterlacer.Filter(frame, kScan_Interlaced, nullptr, true); } diff --git a/mythtv/libs/libmythtv/mythvideoout.cpp b/mythtv/libs/libmythtv/mythvideoout.cpp index ee4b759d81..2326c84b7c 100644 --- a/mythtv/libs/libmythtv/mythvideoout.cpp +++ b/mythtv/libs/libmythtv/mythvideoout.cpp @@ -369,11 +369,18 @@ void MythVideoOutput::SetDeinterlacing(bool Enable, bool DoubleRate, MythDeintTy { if (!Enable) { + m_deinterlacing = false; + m_deinterlacing2X = false; + m_forcedDeinterlacer = DEINT_NONE; m_videoBuffers.SetDeinterlacing(DEINT_NONE, DEINT_NONE, m_videoCodecID); LOG(VB_PLAYBACK, LOG_INFO, LOC + "Disabled all deinterlacing"); return; } + m_deinterlacing = Enable; + m_deinterlacing2X = DoubleRate; + m_forcedDeinterlacer = Force; + MythDeintType singlerate = DEINT_NONE; MythDeintType doublerate = DEINT_NONE; if (DEINT_NONE != Force) @@ -427,6 +434,9 @@ bool MythVideoOutput::InputChanged(const QSize &VideoDim, const QSize &VideoDisp m_dbDisplayProfile->SetInput(m_window.GetVideoDispDim(), 0 ,codecName); m_videoCodecID = CodecID; DiscardFrames(true, true); + + // Update deinterlacers for any input change + SetDeinterlacing(m_deinterlacing, m_deinterlacing2X, m_forcedDeinterlacer); return true; } /** @@ -1020,7 +1030,7 @@ void MythVideoOutput::InitDisplayMeasurements(void) .arg(displayaspect).arg(source)); // Get the window and screen resolutions - QSize window = m_window.GetWindowRect().size(); + QSize window = m_window.GetRawWindowRect().size(); QSize screen = m_display->GetResolution(); // If not running fullscreen, adjust for window size and ignore any video diff --git a/mythtv/libs/libmythtv/mythvideoout.h b/mythtv/libs/libmythtv/mythvideoout.h index c26550a69d..b2314cefec 100644 --- a/mythtv/libs/libmythtv/mythvideoout.h +++ b/mythtv/libs/libmythtv/mythvideoout.h @@ -171,6 +171,9 @@ class MythVideoOutput StereoscopicMode m_stereo {kStereoscopicModeNone}; MythAVCopy m_copyFrame; MythDeinterlacer m_deinterlacer; + bool m_deinterlacing { false }; + bool m_deinterlacing2X { false }; + MythDeintType m_forcedDeinterlacer { DEINT_NONE }; }; #endif // MYTH_VIDEOOUT_H_ diff --git a/mythtv/libs/libmythtv/mythvideooutnull.cpp b/mythtv/libs/libmythtv/mythvideooutnull.cpp index 2171bce90a..c28ae10797 100644 --- a/mythtv/libs/libmythtv/mythvideooutnull.cpp +++ b/mythtv/libs/libmythtv/mythvideooutnull.cpp @@ -205,6 +205,10 @@ void MythVideoOutputNull::SetDeinterlacing(bool Enable, bool DoubleRate, MythDei MythVideoOutput::SetDeinterlacing(Enable, DoubleRate, Force); return; } + + m_deinterlacing = false; + m_deinterlacing2X = false; + m_forcedDeinterlacer = DEINT_NONE; m_videoBuffers.SetDeinterlacing(DEINT_NONE, DEINT_NONE, m_videoCodecID); } diff --git a/mythtv/libs/libmythtv/opengl/mythopenglvideo.cpp b/mythtv/libs/libmythtv/opengl/mythopenglvideo.cpp index 6313fdafaa..0fd86ae50a 100644 --- a/mythtv/libs/libmythtv/opengl/mythopenglvideo.cpp +++ b/mythtv/libs/libmythtv/opengl/mythopenglvideo.cpp @@ -303,8 +303,8 @@ bool MythOpenGLVideo::AddDeinterlacer(const VideoFrame *Frame, FrameScanType Sca sizes.emplace_back(QSize(m_videoDim)); m_prevTextures = MythVideoTexture::CreateTextures(m_render, m_inputType, m_outputType, sizes); m_nextTextures = MythVideoTexture::CreateTextures(m_render, m_inputType, m_outputType, sizes); - // ensure we use GL_NEAREST if resizing is already active - if (m_resizing) + // ensure we use GL_NEAREST if resizing is already active and needed + if (m_resizing & Sampling) { MythVideoTexture::SetTextureFilters(m_render, m_prevTextures, QOpenGLTexture::Nearest); MythVideoTexture::SetTextureFilters(m_render, m_nextTextures, QOpenGLTexture::Nearest); @@ -803,7 +803,12 @@ void MythOpenGLVideo::PrepareFrame(VideoFrame *Frame, bool TopFieldFirst, FrameS // N.B. not needed for the basic deinterlacer if (deinterlacing && !basicdeinterlacing && (m_videoDispDim.height() > m_displayVideoRect.height())) resize |= Deinterlacer; - // UYVY packed pixels must be sampled exactly + + // NB GL_NEAREST introduces some 'minor' chroma sampling errors + // for the following 2 cases. For YUY2 this may be better handled in the + // shader. For GLES3.0 10bit textures - Vulkan is probably the better solution. + + // UYVY packed pixels must be sampled exactly with GL_NEAREST if (FMT_YUY2 == m_outputType) resize |= Sampling; // unsigned integer texture formats need GL_NEAREST sampling @@ -850,9 +855,10 @@ void MythOpenGLVideo::PrepareFrame(VideoFrame *Frame, bool TopFieldFirst, FrameS else if (!m_resizing && resize) { // framebuffer will be created as needed below - MythVideoTexture::SetTextureFilters(m_render, m_inputTextures, QOpenGLTexture::Nearest); - MythVideoTexture::SetTextureFilters(m_render, m_prevTextures, QOpenGLTexture::Nearest); - MythVideoTexture::SetTextureFilters(m_render, m_nextTextures, QOpenGLTexture::Nearest); + QOpenGLTexture::Filter filter = (resize & Sampling) ? QOpenGLTexture::Nearest : QOpenGLTexture::Linear; + MythVideoTexture::SetTextureFilters(m_render, m_inputTextures, filter); + MythVideoTexture::SetTextureFilters(m_render, m_prevTextures, filter); + MythVideoTexture::SetTextureFilters(m_render, m_nextTextures, filter); m_resizing = resize; LOG(VB_PLAYBACK, LOG_INFO, LOC + QString("Resizing from %1x%2 to %3x%4 for %5") .arg(m_videoDispDim.width()).arg(m_videoDispDim.height()) @@ -863,7 +869,7 @@ void MythOpenGLVideo::PrepareFrame(VideoFrame *Frame, bool TopFieldFirst, FrameS // check hardware frames have the correct filtering if (hwframes) { - QOpenGLTexture::Filter filter = resize ? QOpenGLTexture::Nearest : QOpenGLTexture::Linear; + QOpenGLTexture::Filter filter = (resize & Sampling) ? QOpenGLTexture::Nearest : QOpenGLTexture::Linear; if (inputtextures[0]->m_filter != filter) MythVideoTexture::SetTextureFilters(m_render, inputtextures, filter); } diff --git a/mythtv/libs/libmythtv/opengl/mythvaapidrminterop.cpp b/mythtv/libs/libmythtv/opengl/mythvaapidrminterop.cpp index 48be0267f1..c03766c6fa 100644 --- a/mythtv/libs/libmythtv/opengl/mythvaapidrminterop.cpp +++ b/mythtv/libs/libmythtv/opengl/mythvaapidrminterop.cpp @@ -350,8 +350,12 @@ VideoFrameType MythVAAPIInteropDRM::VATypeToMythType(uint32_t Fourcc) case VA_FOURCC_NV12: return FMT_NV12; case VA_FOURCC_YUY2: case VA_FOURCC_UYVY: return FMT_YUY2; +#if defined (VA_FOURCC_P010) case VA_FOURCC_P010: return FMT_P010; +#endif +#if defined (VA_FOURCC_P016) case VA_FOURCC_P016: return FMT_P016; +#endif case VA_FOURCC_ARGB: return FMT_ARGB32; case VA_FOURCC_RGBA: return FMT_RGBA32; } diff --git a/mythtv/libs/libmythtv/opengl/mythvdpauinterop.cpp b/mythtv/libs/libmythtv/opengl/mythvdpauinterop.cpp index e01dd9b50c..a20dca8d9b 100644 --- a/mythtv/libs/libmythtv/opengl/mythvdpauinterop.cpp +++ b/mythtv/libs/libmythtv/opengl/mythvdpauinterop.cpp @@ -109,7 +109,7 @@ bool MythVDPAUInterop::InitNV(AVVDPAUDeviceContext* DeviceContext) if (!DeviceContext || !m_context) return false; - if (m_initNV && m_finiNV && m_registerNV && m_accessNV && m_mapNV && + if (m_initNV && m_finiNV && m_registerNV && m_accessNV && m_mapNV && m_unmapNV && m_helper && m_helper->IsValid()) return true; @@ -119,11 +119,12 @@ bool MythVDPAUInterop::InitNV(AVVDPAUDeviceContext* DeviceContext) m_registerNV = reinterpret_cast<MYTH_VDPAUREGOUTSURFNV>(m_context->GetProcAddress("glVDPAURegisterOutputSurfaceNV")); m_accessNV = reinterpret_cast<MYTH_VDPAUSURFACCESSNV>(m_context->GetProcAddress("glVDPAUSurfaceAccessNV")); m_mapNV = reinterpret_cast<MYTH_VDPAUMAPSURFNV>(m_context->GetProcAddress("glVDPAUMapSurfacesNV")); + m_unmapNV = reinterpret_cast<MYTH_VDPAUMAPSURFNV>(m_context->GetProcAddress("glVDPAUUnmapSurfacesNV")); delete m_helper; m_helper = nullptr; - if (m_initNV && m_finiNV && m_registerNV && m_accessNV && m_mapNV) + if (m_initNV && m_finiNV && m_registerNV && m_accessNV && m_mapNV && m_unmapNV) { m_helper = new MythVDPAUHelper(DeviceContext); if (m_helper->IsValid()) @@ -198,7 +199,6 @@ bool MythVDPAUInterop::InitVDPAU(AVVDPAUDeviceContext* DeviceContext, VdpVideoSu else { m_accessNV(m_outputSurfaceReg, QOpenGLBuffer::ReadOnly); - m_mapNV(1, &m_outputSurfaceReg); } } return true; @@ -347,9 +347,11 @@ vector<MythVideoTexture*> MythVDPAUInterop::Acquire(MythRenderOpenGL *Context, } // Render surface + m_unmapNV(1, &m_outputSurfaceReg); m_helper->MixerRender(m_mixer, surface, m_outputSurface, Scan, Frame->interlaced_reversed ? !Frame->top_field_first : Frame->top_field_first, m_referenceFrames); + m_mapNV(1, &m_outputSurfaceReg); return m_openglTextures[DUMMY_INTEROP_ID]; } diff --git a/mythtv/libs/libmythtv/opengl/mythvdpauinterop.h b/mythtv/libs/libmythtv/opengl/mythvdpauinterop.h index 79f7bb40b8..6a8b3bf6b9 100644 --- a/mythtv/libs/libmythtv/opengl/mythvdpauinterop.h +++ b/mythtv/libs/libmythtv/opengl/mythvdpauinterop.h @@ -65,6 +65,7 @@ class MythVDPAUInterop : public MythOpenGLInterop MYTH_VDPAUREGOUTSURFNV m_registerNV { nullptr }; MYTH_VDPAUSURFACCESSNV m_accessNV { nullptr }; MYTH_VDPAUMAPSURFNV m_mapNV { nullptr }; + MYTH_VDPAUMAPSURFNV m_unmapNV { nullptr }; MythCodecID m_codec { kCodec_NONE }; bool m_preempted { false }; bool m_preemptedWarning { false }; diff --git a/mythtv/libs/libmythtv/opengl/mythvideooutopengl.cpp b/mythtv/libs/libmythtv/opengl/mythvideooutopengl.cpp index 225c58fe25..0e0447194f 100644 --- a/mythtv/libs/libmythtv/opengl/mythvideooutopengl.cpp +++ b/mythtv/libs/libmythtv/opengl/mythvideooutopengl.cpp @@ -139,7 +139,7 @@ MythVideoOutputOpenGL::MythVideoOutputOpenGL(QString Profile) } // we need to control buffer swapping - m_openGLPainter->SetSwapControl(false); + m_openGLPainter->SetViewControl(MythOpenGLPainter::None); // Create OpenGLVideo QRect dvr = GetDisplayVisibleRect(); @@ -164,7 +164,7 @@ MythVideoOutputOpenGL::~MythVideoOutputOpenGL() } m_openGLVideoPiPsReady.clear(); if (m_openGLPainter) - m_openGLPainter->SetSwapControl(true); + m_openGLPainter->SetViewControl(MythOpenGLPainter::Viewport | MythOpenGLPainter::Framebuffer); delete m_openGLVideo; if (m_render) { @@ -424,7 +424,7 @@ void MythVideoOutputOpenGL::ProcessFrame(VideoFrame *Frame, OSD */*osd*/, m_dbDisplayProfile->SetInput(m_window.GetVideoDispDim(), 0 , codecName); bool ok = Init(m_newVideoDim, m_newVideoDispDim, m_newAspect, - m_display, m_window.GetDisplayVisibleRect(), m_newCodecId); + m_display, m_window.GetRawWindowRect(), m_newCodecId); m_newCodecId = kCodec_NONE; m_newVideoDim = QSize(); m_newVideoDispDim = QSize(); @@ -434,6 +434,9 @@ void MythVideoOutputOpenGL::ProcessFrame(VideoFrame *Frame, OSD */*osd*/, if (wasembedding && ok) EmbedInWidget(oldrect); + // Update deinterlacers for any input change + SetDeinterlacing(m_deinterlacing, m_deinterlacing2X, m_forcedDeinterlacer); + if (!ok) return; } @@ -580,6 +583,9 @@ void MythVideoOutputOpenGL::PrepareFrame(VideoFrame *Frame, FrameScanType Scan, // main UI when embedded if (m_window.IsEmbedding()) { + // If we are using high dpi, the painter needs to set the appropriate + // viewport and enable scaling of its images + m_openGLPainter->SetViewControl(MythOpenGLPainter::Viewport); MythMainWindow *win = GetMythMainWindow(); if (win && win->GetPaintWindow()) { @@ -595,6 +601,7 @@ void MythVideoOutputOpenGL::PrepareFrame(VideoFrame *Frame, FrameScanType Scan, m_render->SetViewPort(main, true); } } + m_openGLPainter->SetViewControl(MythOpenGLPainter::None); } // video diff --git a/mythtv/libs/libmythtv/previewgenerator.cpp b/mythtv/libs/libmythtv/previewgenerator.cpp index d78f9ab9bd..e2e3ef6483 100644 --- a/mythtv/libs/libmythtv/previewgenerator.cpp +++ b/mythtv/libs/libmythtv/previewgenerator.cpp @@ -677,7 +677,10 @@ bool PreviewGenerator::LocalPreviewRun(void) } if (programDuration > 0) { - captime = startEarly + (programDuration / 3); + captime = programDuration / 3; + if (captime > 600) + captime = 600; + captime += startEarly; } if (captime < 0) captime = 600; diff --git a/mythtv/libs/libmythtv/programdata.cpp b/mythtv/libs/libmythtv/programdata.cpp index e32c678e86..8495e94155 100644 --- a/mythtv/libs/libmythtv/programdata.cpp +++ b/mythtv/libs/libmythtv/programdata.cpp @@ -1205,7 +1205,7 @@ void ProgInfo::Squeeze(void) */ uint ProgInfo::InsertDB(MSqlQuery &query, uint chanid) const { - LOG(VB_XMLTV, LOG_INFO, + LOG(VB_XMLTV, LOG_DEBUG, QString("Inserting new program : %1 - %2 %3 %4") .arg(m_starttime.toString(Qt::ISODate)) .arg(m_endtime.toString(Qt::ISODate)) @@ -1426,14 +1426,14 @@ void ProgramData::FixProgramList(QList<ProgInfo*> &fixlist) tokeep = it, todelete = cur; - LOG(VB_XMLTV, LOG_INFO, + LOG(VB_XMLTV, LOG_DEBUG, QString("Removing conflicting program: %1 - %2 %3 %4") .arg((*todelete)->m_starttime.toString(Qt::ISODate)) .arg((*todelete)->m_endtime.toString(Qt::ISODate)) .arg((*todelete)->m_channel) .arg((*todelete)->m_title)); - LOG(VB_XMLTV, LOG_INFO, + LOG(VB_XMLTV, LOG_DEBUG, QString("Conflicted with : %1 - %2 %3 %4") .arg((*tokeep)->m_starttime.toString(Qt::ISODate)) .arg((*tokeep)->m_endtime.toString(Qt::ISODate)) @@ -1684,7 +1684,7 @@ bool ProgramData::IsUnchanged( bool ProgramData::DeleteOverlaps( MSqlQuery &query, uint chanid, const ProgInfo &pi) { - if (VERBOSE_LEVEL_CHECK(VB_XMLTV, LOG_INFO)) + if (VERBOSE_LEVEL_CHECK(VB_XMLTV, LOG_DEBUG)) { // Get overlaps.. query.prepare( @@ -1705,7 +1705,7 @@ bool ProgramData::DeleteOverlaps( do { - LOG(VB_XMLTV, LOG_INFO, + LOG(VB_XMLTV, LOG_DEBUG, QString("Removing existing program: %1 - %2 %3 %4") .arg(MythDate::as_utc(query.value(1).toDateTime()).toString(Qt::ISODate)) .arg(MythDate::as_utc(query.value(2).toDateTime()).toString(Qt::ISODate)) diff --git a/mythtv/libs/libmythtv/recorders/ExternalChannel.cpp b/mythtv/libs/libmythtv/recorders/ExternalChannel.cpp index a894610871..5405c6f55f 100644 --- a/mythtv/libs/libmythtv/recorders/ExternalChannel.cpp +++ b/mythtv/libs/libmythtv/recorders/ExternalChannel.cpp @@ -98,18 +98,38 @@ bool ExternalChannel::Tune(const QString &channum) return true; QString result; - - LOG(VB_CHANNEL, LOG_INFO, LOC + "Tuning to " + channum); - - if (!m_streamHandler->ProcessCommand("TuneChannel:" + channum, result, - 20000)) + if (m_tuneTimeout < 0) { - LOG(VB_CHANNEL, LOG_ERR, LOC + QString - ("Failed to Tune %1: %2").arg(channum).arg(result)); - return false; + // When mythbackend first starts up, just retrive the + // tuneTimeout for subsequent tune requests. + + if (!m_streamHandler->ProcessCommand("LockTimeout?", result)) + { + LOG(VB_CHANNEL, LOG_ERR, LOC + QString + ("Failed to retrieve LockTimeout: %1").arg(result)); + m_tuneTimeout = 60000; + } + else + m_tuneTimeout = result.split(":")[1].toInt(); + + LOG(VB_CHANNEL, LOG_INFO, LOC + QString("Using Tune timeout of %1ms") + .arg(m_tuneTimeout)); + } + else + { + LOG(VB_CHANNEL, LOG_INFO, LOC + "Tuning to " + channum); + + if (!m_streamHandler->ProcessCommand("TuneChannel:" + channum, + result, m_tuneTimeout)) + { + LOG(VB_CHANNEL, LOG_ERR, LOC + QString + ("Failed to Tune %1: %2").arg(channum).arg(result)); + return false; + } + + UpdateDescription(); + m_backgroundTuning = result.startsWith("OK:InProgress"); } - - UpdateDescription(); return true; } @@ -124,3 +144,40 @@ bool ExternalChannel::EnterPowerSavingMode(void) Close(); return true; } + +uint ExternalChannel::GetTuneStatus(void) +{ + + if (!m_backgroundTuning) + return 3; + + LOG(VB_CHANNEL, LOG_DEBUG, LOC + QString("GetScriptStatus() %1") + .arg(m_systemStatus)); + + QString result; + int ret; + + if (!m_streamHandler->ProcessCommand("TuneStatus?", result)) + { + LOG(VB_CHANNEL, LOG_ERR, LOC + QString + ("Failed to Tune: %1").arg(result)); + ret = 2; + m_backgroundTuning = false; + } + else + { + if (result.startsWith("OK:InProgress")) + ret = 1; + else + { + ret = 3; + m_backgroundTuning = false; + UpdateDescription(); + } + } + + LOG(VB_CHANNEL, LOG_DEBUG, LOC + QString("GetScriptStatus() %1 -> %2") + .arg(m_systemStatus). arg(ret)); + + return ret; +} diff --git a/mythtv/libs/libmythtv/recorders/ExternalChannel.h b/mythtv/libs/libmythtv/recorders/ExternalChannel.h index 243934301e..0b09953493 100644 --- a/mythtv/libs/libmythtv/recorders/ExternalChannel.h +++ b/mythtv/libs/libmythtv/recorders/ExternalChannel.h @@ -46,12 +46,16 @@ class ExternalChannel : public DTVChannel QString UpdateDescription(void); QString GetDescription(void); + bool IsBackgroundTuning(void) const { return m_backgroundTuning; } + uint GetTuneStatus(void); protected: bool IsExternalChannelChangeSupported(void) override // ChannelBase { return true; } private: + int m_tuneTimeout { -1 }; + bool m_backgroundTuning {false}; QString m_device; QStringList m_args; ExternalStreamHandler *m_streamHandler {nullptr}; diff --git a/mythtv/libs/libmythtv/recorders/ExternalRecChannelFetcher.cpp b/mythtv/libs/libmythtv/recorders/ExternalRecChannelFetcher.cpp index 28e11f2c98..3a2385989a 100644 --- a/mythtv/libs/libmythtv/recorders/ExternalRecChannelFetcher.cpp +++ b/mythtv/libs/libmythtv/recorders/ExternalRecChannelFetcher.cpp @@ -70,7 +70,8 @@ bool ExternalRecChannelFetcher::FetchChannel(const QString & cmd, QString & channum, QString & name, QString & callsign, - QString & xmltvid) + QString & xmltvid, + QString & icon) { if (!Valid()) return false; @@ -95,13 +96,14 @@ bool ExternalRecChannelFetcher::FetchChannel(const QString & cmd, return false; } - // Expect csv: channum, name, callsign, xmltvid + // Expect csv: channum, name, callsign, xmltvid, icon QStringList fields = result.mid(3).split(","); - if (fields.size() != 4) + if (fields.size() != 4 && fields.size() != 5) { LOG(VB_CHANNEL, LOG_ERR, LOC + - QString("Expecting channum, name, callsign, xmltvid; " + QString("Expecting channum, name, callsign, xmltvid and " + "optionally icon; " "Received '%1").arg(result)); return false; } @@ -110,6 +112,8 @@ bool ExternalRecChannelFetcher::FetchChannel(const QString & cmd, name = fields[1]; callsign = fields[2]; xmltvid = fields[3]; + if (fields.size() == 5) + icon = fields[4]; return true; } diff --git a/mythtv/libs/libmythtv/recorders/ExternalRecChannelFetcher.h b/mythtv/libs/libmythtv/recorders/ExternalRecChannelFetcher.h index 84f354839d..638b81becf 100644 --- a/mythtv/libs/libmythtv/recorders/ExternalRecChannelFetcher.h +++ b/mythtv/libs/libmythtv/recorders/ExternalRecChannelFetcher.h @@ -35,18 +35,22 @@ class ExternalRecChannelFetcher bool FirstChannel(QString & channum, QString & name, QString & callsign, - QString & xmltvid) + QString & xmltvid, + QString & icon) { - return FetchChannel("FirstChannel", channum, name, callsign, xmltvid); + return FetchChannel("FirstChannel", channum, name, callsign, + xmltvid, icon); } bool NextChannel(QString & channum, QString & name, QString & callsign, - QString & xmltvid) + QString & xmltvid, + QString & icon) { - return FetchChannel("NextChannel", channum, name, callsign, xmltvid); - } + return FetchChannel("NextChannel", channum, name, callsign, + xmltvid, icon); + } protected: void Close(void); @@ -54,7 +58,8 @@ class ExternalRecChannelFetcher QString & channum, QString & name, QString & callsign, - QString & xmltvid); + QString & xmltvid, + QString & icon); private: diff --git a/mythtv/libs/libmythtv/recorders/ExternalRecorder.cpp b/mythtv/libs/libmythtv/recorders/ExternalRecorder.cpp index 445325bc83..a2acc94655 100644 --- a/mythtv/libs/libmythtv/recorders/ExternalRecorder.cpp +++ b/mythtv/libs/libmythtv/recorders/ExternalRecorder.cpp @@ -177,6 +177,7 @@ bool ExternalRecorder::PauseAndWait(int timeout) { LOG(VB_RECORD, LOG_INFO, LOC + "PauseAndWait pause"); + m_streamHandler->RemoveListener(m_streamData); StopStreaming(); m_paused = true; @@ -196,6 +197,8 @@ bool ExternalRecorder::PauseAndWait(int timeout) m_streamData->Reset(m_streamData->DesiredProgram()); m_paused = false; + m_streamHandler->AddListener(m_streamData); + StartStreaming(); } // Always wait a little bit, unless woken up diff --git a/mythtv/libs/libmythtv/recorders/ExternalSignalMonitor.cpp b/mythtv/libs/libmythtv/recorders/ExternalSignalMonitor.cpp index 6fb4cd592a..2db72f50af 100644 --- a/mythtv/libs/libmythtv/recorders/ExternalSignalMonitor.cpp +++ b/mythtv/libs/libmythtv/recorders/ExternalSignalMonitor.cpp @@ -53,6 +53,9 @@ ExternalSignalMonitor::ExternalSignalMonitor(int db_cardnum, LOG(VB_GENERAL, LOG_ERR, LOC + "Open failed"); else m_lock_timeout = GetLockTimeout() * 1000; + + if (GetExternalChannel()->IsBackgroundTuning()) + m_scriptStatus.SetValue(1); } /** \fn ExternalSignalMonitor::~ExternalSignalMonitor() @@ -105,6 +108,16 @@ void ExternalSignalMonitor::UpdateValues(void) return; } + if (GetExternalChannel()->IsBackgroundTuning()) + { + QMutexLocker locker(&m_statusLock); + if (m_scriptStatus.GetValue() < 2) + m_scriptStatus.SetValue(GetExternalChannel()->GetTuneStatus()); + + if (!m_scriptStatus.IsGood()) + return; + } + if (m_stream_handler_started) { if (!m_stream_handler->IsRunning()) diff --git a/mythtv/libs/libmythtv/recorders/ExternalStreamHandler.cpp b/mythtv/libs/libmythtv/recorders/ExternalStreamHandler.cpp index 87548086a1..74265cf9bd 100644 --- a/mythtv/libs/libmythtv/recorders/ExternalStreamHandler.cpp +++ b/mythtv/libs/libmythtv/recorders/ExternalStreamHandler.cpp @@ -1449,7 +1449,9 @@ bool ExternalStreamHandler::ProcessVer2(const QString & command, m_ioErrCnt = 0; if (!okay) level = LOG_WARNING; - else if (command.startsWith("SendBytes")) + else if (command.startsWith("SendBytes") || + (command.startsWith("TuneStatus") && + result == "OK:InProgress")) level = LOG_DEBUG; LOG(VB_RECORD, level, diff --git a/mythtv/libs/libmythtv/recorders/dtvrecorder.cpp b/mythtv/libs/libmythtv/recorders/dtvrecorder.cpp index 3c1248ea14..a868c6f048 100644 --- a/mythtv/libs/libmythtv/recorders/dtvrecorder.cpp +++ b/mythtv/libs/libmythtv/recorders/dtvrecorder.cpp @@ -55,6 +55,8 @@ DTVRecorder::DTVRecorder(TVRec *rec) : gCoreContext->GetNumSetting("MinimumRecordingQuality", 95); m_containerFormat = formatMPEG2_TS; + + memset(m_continuityCounter, 0xff, sizeof(m_continuityCounter)); } DTVRecorder::~DTVRecorder(void) diff --git a/mythtv/libs/libmythtv/recorders/dvbchannel.cpp b/mythtv/libs/libmythtv/recorders/dvbchannel.cpp index 9158cd7554..47957a16c7 100644 --- a/mythtv/libs/libmythtv/recorders/dvbchannel.cpp +++ b/mythtv/libs/libmythtv/recorders/dvbchannel.cpp @@ -77,12 +77,13 @@ DVBChannel::DVBChannel(QString aDevice, TVRec *parent) : DTVChannel(parent), m_device(std::move(aDevice)) { s_master_map_lock.lockForWrite(); - QString key = CardUtil::GetDeviceName(DVB_DEV_FRONTEND, m_device); + m_key = CardUtil::GetDeviceName(DVB_DEV_FRONTEND, m_device); if (m_pParent) - key += QString(":%1") + m_key += QString(":%1") .arg(CardUtil::GetSourceID(m_pParent->GetInputId())); - s_master_map[key].push_back(this); // == RegisterForMaster - auto *master = static_cast<DVBChannel*>(s_master_map[key].front()); + + s_master_map[m_key].push_back(this); // == RegisterForMaster + auto *master = dynamic_cast<DVBChannel*>(s_master_map[m_key].front()); if (master == this) { m_dvbCam = new DVBCam(m_device); @@ -103,17 +104,13 @@ DVBChannel::~DVBChannel() // set a new master if there are other instances and we're the master // whether we are the master or not remove us from the map.. s_master_map_lock.lockForWrite(); - QString key = CardUtil::GetDeviceName(DVB_DEV_FRONTEND, m_device); - if (m_pParent) - key += QString(":%1") - .arg(CardUtil::GetSourceID(m_pParent->GetInputId())); - auto *master = static_cast<DVBChannel*>(s_master_map[key].front()); + auto *master = dynamic_cast<DVBChannel*>(s_master_map[m_key].front()); if (master == this) { - s_master_map[key].pop_front(); + s_master_map[m_key].pop_front(); DVBChannel *new_master = nullptr; - if (!s_master_map[key].empty()) - new_master = dynamic_cast<DVBChannel*>(s_master_map[key].front()); + if (!s_master_map[m_key].empty()) + new_master = dynamic_cast<DVBChannel*>(s_master_map[m_key].front()); if (new_master) { QMutexLocker master_locker(&(master->m_hwLock)); @@ -123,7 +120,7 @@ DVBChannel::~DVBChannel() } else { - s_master_map[key].removeAll(this); + s_master_map[m_key].removeAll(this); } s_master_map_lock.unlock(); @@ -131,7 +128,7 @@ DVBChannel::~DVBChannel() // if we're the last one out delete dvbcam s_master_map_lock.lockForRead(); - MasterMap::iterator mit = s_master_map.find(key); + MasterMap::iterator mit = s_master_map.find(m_key); if ((*mit).empty()) delete m_dvbCam; m_dvbCam = nullptr; @@ -1358,11 +1355,7 @@ void DVBChannel::ReturnMasterLock(DVBChannel* &dvbm) DVBChannel *DVBChannel::GetMasterLock(void) const { - QString key = CardUtil::GetDeviceName(DVB_DEV_FRONTEND, m_device); - if (m_pParent) - key += QString(":%1") - .arg(CardUtil::GetSourceID(m_pParent->GetInputId())); - DTVChannel *master = DTVChannel::GetMasterLock(key); + DTVChannel *master = DTVChannel::GetMasterLock(m_key); auto *dvbm = dynamic_cast<DVBChannel*>(master); if (master && !dvbm) DTVChannel::ReturnMasterLock(master); diff --git a/mythtv/libs/libmythtv/recorders/dvbchannel.h b/mythtv/libs/libmythtv/recorders/dvbchannel.h index 2ce0898e4d..99caaabf3a 100644 --- a/mythtv/libs/libmythtv/recorders/dvbchannel.h +++ b/mythtv/libs/libmythtv/recorders/dvbchannel.h @@ -162,7 +162,8 @@ class DVBChannel : public DTVChannel // Other State /// File descriptor for tuning hardware int m_fdFrontend {-1}; - QString m_device; ///< DVB Device + QString m_device; // DVB Device + QString m_key; // master lock key /// true iff our driver munges PMT bool m_hasCrcBug {false}; diff --git a/mythtv/libs/libmythtv/recorders/hlsstreamhandler.cpp b/mythtv/libs/libmythtv/recorders/hlsstreamhandler.cpp index 964f5396e4..22e0abae06 100644 --- a/mythtv/libs/libmythtv/recorders/hlsstreamhandler.cpp +++ b/mythtv/libs/libmythtv/recorders/hlsstreamhandler.cpp @@ -181,7 +181,7 @@ void HLSStreamHandler::run(void) { LOG(VB_RECORD, LOG_INFO, LOC + QString("Packet not starting with SYNC Byte (got 0x%1)") - .arg((char)m_readbuffer[0], 2, QLatin1Char('0'))); + .arg((char)m_readbuffer[0], 2, 16, QLatin1Char('0'))); continue; } diff --git a/mythtv/libs/libmythtv/recorders/signalmonitor.h b/mythtv/libs/libmythtv/recorders/signalmonitor.h index fbe55da09b..bae34ecf7b 100644 --- a/mythtv/libs/libmythtv/recorders/signalmonitor.h +++ b/mythtv/libs/libmythtv/recorders/signalmonitor.h @@ -70,6 +70,7 @@ class SignalMonitor : protected MThread /// \brief Returns milliseconds between signal monitoring events. int GetUpdateRate() const { return m_update_rate; } virtual QStringList GetStatusList(void) const; + int GetSignalStrength(void) { return m_signalStrength.GetNormalizedValue(0,100); } /// \brief Returns true iff scriptStatus.IsGood() and signalLock.IsGood() /// return true diff --git a/mythtv/libs/libmythtv/recorders/vboxutils.cpp b/mythtv/libs/libmythtv/recorders/vboxutils.cpp index bc316e4ebf..265a53a0e2 100644 --- a/mythtv/libs/libmythtv/recorders/vboxutils.cpp +++ b/mythtv/libs/libmythtv/recorders/vboxutils.cpp @@ -22,6 +22,7 @@ #define SEARCH_TIME 3000 #define VBOX_URI "urn:schemas-upnp-org:device:MediaServer:1" +#define VBOX_UDN "uuid:b7531642-0123-3210" VBox::VBox(const QString &url) { @@ -102,11 +103,12 @@ QStringList VBox::doUPNPSearch(void) QString friendlyName = BE->GetDeviceDesc()->m_rootDevice.m_sFriendlyName; QString ip = BE->GetDeviceDesc()->m_hostUrl.host(); + QString udn = BE->GetDeviceDesc()->m_rootDevice.m_sUDN; int port = BE->GetDeviceDesc()->m_hostUrl.port(); LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("Found possible VBox at %1 (%2:%3)").arg(friendlyName).arg(ip).arg(port)); - if (friendlyName.startsWith("VBox")) + if (udn.startsWith(VBOX_UDN)) { // we found one QString id; @@ -240,7 +242,8 @@ bool VBox::checkVersion(QString &version) sList = version.split('.'); // sanity check this looks like a VBox version string - if (sList.count() < 3 || !(version.startsWith("VB.") || version.startsWith("VJ."))) + if (sList.count() < 3 || !(version.startsWith("VB.") || version.startsWith("VJ.") + || version.startsWith("VT."))) { LOG(VB_GENERAL, LOG_INFO, LOC + QString("Failed to parse version from %1").arg(version)); delete xmlDoc; diff --git a/mythtv/libs/libmythtv/recordinginfo.cpp b/mythtv/libs/libmythtv/recordinginfo.cpp index 9e140a3fe5..7138c2be09 100644 --- a/mythtv/libs/libmythtv/recordinginfo.cpp +++ b/mythtv/libs/libmythtv/recordinginfo.cpp @@ -126,7 +126,7 @@ RecordingInfo::RecordingInfo( m_stars = clamp(_stars, 0.0F, 1.0F); m_originalAirDate = _originalAirDate; - if (m_originalAirDate.isValid() && m_originalAirDate < QDate(1940, 1, 1)) + if (m_originalAirDate.isValid() && m_originalAirDate < QDate(1895, 12, 28)) m_originalAirDate = QDate(); m_programFlags &= ~FL_REPEAT; @@ -1093,7 +1093,7 @@ bool RecordingInfo::InsertProgram(RecordingInfo *pg, query.bindValue(":ORIGAIRDATE", pg->m_originalAirDate); // If there is no originalairdate use "year" } - else if (pg->m_year >= 1940) + else if (pg->m_year >= 1895) { query.bindValue(":ORIGAIRDATE", QDate(pg->m_year,1,1)); } diff --git a/mythtv/libs/libmythtv/ringbuffer.cpp b/mythtv/libs/libmythtv/ringbuffer.cpp index d51a33ef08..a6c1e2c168 100644 --- a/mythtv/libs/libmythtv/ringbuffer.cpp +++ b/mythtv/libs/libmythtv/ringbuffer.cpp @@ -344,7 +344,7 @@ void RingBuffer::UpdatePlaySpeed(float play_speed) } /** \fn RingBuffer::SetBufferSizeFactors(bool, bool) - * \brief Tells RingBuffer that the raw bitrate may be innacurate and the + * \brief Tells RingBuffer that the raw bitrate may be inaccurate and the * underlying container is matroska, both of which may require a larger * buffer size. */ diff --git a/mythtv/libs/libmythtv/scanwizard.cpp b/mythtv/libs/libmythtv/scanwizard.cpp index 8d25b51910..0c1fd9a0e8 100644 --- a/mythtv/libs/libmythtv/scanwizard.cpp +++ b/mythtv/libs/libmythtv/scanwizard.cpp @@ -140,6 +140,7 @@ void ScanWizard::Scan() DoChannelNumbersOnly(), DoCompleteChannelsOnly(), DoFullChannelSearch(), + DoRemoveDuplicates(), GetServiceRequirements()); ci.Process(transports, sourceid); } @@ -185,6 +186,7 @@ void ScanWizard::Scan() DoTestDecryption(), DoFreeToAirOnly(), DoChannelNumbersOnly(), DoCompleteChannelsOnly(), DoFullChannelSearch(), + DoRemoveDuplicates(), DoAddFullTS(), GetServiceRequirements(), diff --git a/mythtv/libs/libmythtv/scanwizard.h b/mythtv/libs/libmythtv/scanwizard.h index 9e03528257..3c680ea192 100644 --- a/mythtv/libs/libmythtv/scanwizard.h +++ b/mythtv/libs/libmythtv/scanwizard.h @@ -92,6 +92,7 @@ class MTV_PUBLIC ScanWizard : public GroupSetting bool DoChannelNumbersOnly(void) const; bool DoCompleteChannelsOnly(void) const; bool DoFullChannelSearch(void) const; + bool DoRemoveDuplicates(void) const; bool DoAddFullTS(void) const; bool DoTestDecryption(void) const; bool DoScanOpenTV(void) const; @@ -106,6 +107,7 @@ class MTV_PUBLIC ScanWizard : public GroupSetting ChannelNumbersOnly *m_lcnOnly {nullptr}; CompleteChannelsOnly *m_completeOnly {nullptr}; FullChannelSearch *m_fullSearch {nullptr}; + RemoveDuplicates *m_removeDuplicates {nullptr}; AddFullTS *m_addFullTS {nullptr}; TrustEncSISetting *m_trustEncSI {nullptr}; // End of members moved from ScanWizardConfig diff --git a/mythtv/libs/libmythtv/sourceutil.cpp b/mythtv/libs/libmythtv/sourceutil.cpp index b9758d9fd4..9a8334c84a 100644 --- a/mythtv/libs/libmythtv/sourceutil.cpp +++ b/mythtv/libs/libmythtv/sourceutil.cpp @@ -65,6 +65,29 @@ QString SourceUtil::GetSourceName(uint sourceid) return query.value(0).toString(); } +uint SourceUtil::GetSourceID(const QString &name) +{ + MSqlQuery query(MSqlQuery::InitCon()); + + query.prepare( + "SELECT sourceid " + "FROM videosource " + "WHERE name = :NAME"); + query.bindValue(":NAME", name); + + if (!query.exec()) + { + MythDB::DBError("SourceUtil::GetSourceID()", query); + return 0; + } + if (!query.next()) + { + return 0; + } + + return query.value(0).toUInt(); +} + QString SourceUtil::GetChannelSeparator(uint sourceid) { MSqlQuery query(MSqlQuery::InitCon()); diff --git a/mythtv/libs/libmythtv/sourceutil.h b/mythtv/libs/libmythtv/sourceutil.h index 496c6959c7..666e9a3a32 100644 --- a/mythtv/libs/libmythtv/sourceutil.h +++ b/mythtv/libs/libmythtv/sourceutil.h @@ -17,6 +17,7 @@ class MTV_PUBLIC SourceUtil public: static bool HasDigitalChannel(uint sourceid); static QString GetSourceName(uint sourceid); + static uint GetSourceID(const QString &name); static QString GetChannelSeparator(uint sourceid); static QString GetChannelFormat(uint sourceid); static uint GetChannelCount(uint sourceid); diff --git a/mythtv/libs/libmythtv/transporteditor.cpp b/mythtv/libs/libmythtv/transporteditor.cpp index dae14fece8..0359370a24 100644 --- a/mythtv/libs/libmythtv/transporteditor.cpp +++ b/mythtv/libs/libmythtv/transporteditor.cpp @@ -35,6 +35,7 @@ using namespace std; #include "transporteditor.h" #include "videosource.h" +#include "sourceutil.h" #include "mythcorecontext.h" #include "mythdb.h" @@ -109,6 +110,14 @@ static CardUtil::INPUT_TYPES get_cardtype(uint sourceid) cardtype = CardUtil::ProbeSubTypeName(cardid); nType = CardUtil::toInputType(cardtype); + if (nType == CardUtil::HDHOMERUN) + { + if (CardUtil::HDHRdoesDVBC(CardUtil::GetVideoDevice(cardid))) + nType = CardUtil::DVBC; + else if (CardUtil::HDHRdoesDVB(CardUtil::GetVideoDevice(cardid))) + nType = CardUtil::DVBT2; + } + if ((CardUtil::ERROR_OPEN == nType) || (CardUtil::ERROR_UNKNOWN == nType) || (CardUtil::ERROR_PROBE == nType)) @@ -161,27 +170,22 @@ static CardUtil::INPUT_TYPES get_cardtype(uint sourceid) return cardtypes[0]; } -void TransportListEditor::SetSourceID(uint _sourceid) +void TransportListEditor::SetSourceID(uint sourceid) { for (auto *setting : m_list) removeChild(setting); m_list.clear(); -#if 0 - LOG(VB_GENERAL, LOG_DEBUG, QString("TransportList::SetSourceID(%1)") - .arg(_sourceid)); -#endif - - if (!_sourceid) + if (!sourceid) { m_sourceid = 0; } else { - m_cardtype = get_cardtype(_sourceid); + m_cardtype = get_cardtype(sourceid); m_sourceid = ((CardUtil::ERROR_OPEN == m_cardtype) || (CardUtil::ERROR_UNKNOWN == m_cardtype) || - (CardUtil::ERROR_PROBE == m_cardtype)) ? 0 : _sourceid; + (CardUtil::ERROR_PROBE == m_cardtype)) ? 0 : sourceid; } } @@ -204,11 +208,14 @@ TransportListEditor::TransportListEditor(uint sourceid) : SetSourceID(sourceid); } -void TransportListEditor::SetSourceID(const QString& sourceid) +void TransportListEditor::SetSourceID(const QString& name) { if (m_isLoading) return; - SetSourceID(sourceid.toUInt()); + + uint sourceid = SourceUtil::GetSourceID(name); + + SetSourceID(sourceid); Load(); } diff --git a/mythtv/libs/libmythtv/tv_play.cpp b/mythtv/libs/libmythtv/tv_play.cpp index 7679b6acde..9bfae2cd32 100644 --- a/mythtv/libs/libmythtv/tv_play.cpp +++ b/mythtv/libs/libmythtv/tv_play.cpp @@ -2602,6 +2602,9 @@ void TV::StopStuff(PlayerContext *mctx, PlayerContext *ctx, LOC + QString("For player ctx %1 -- begin") .arg(find_player_index(ctx))); + emit PlaybackExiting(this); + m_isEmbedded = false; + SetActive(mctx, 0, false); if (ctx->m_buffer) diff --git a/mythtv/libs/libmythtv/tv_play.h b/mythtv/libs/libmythtv/tv_play.h index 05940d3555..421cda8981 100644 --- a/mythtv/libs/libmythtv/tv_play.h +++ b/mythtv/libs/libmythtv/tv_play.h @@ -330,6 +330,9 @@ class MTV_PUBLIC TV : public QObject, public MenuItemDisplayer void timerEvent(QTimerEvent *te) override; // QObject void StopPlayback(void); + signals: + void PlaybackExiting(TV* Player); + protected: // Protected event handling void customEvent(QEvent *e) override; // QObject diff --git a/mythtv/libs/libmythtv/videooutwindow.cpp b/mythtv/libs/libmythtv/videooutwindow.cpp index af8af15c31..a5d76e7ae7 100644 --- a/mythtv/libs/libmythtv/videooutwindow.cpp +++ b/mythtv/libs/libmythtv/videooutwindow.cpp @@ -38,6 +38,11 @@ #define LOC QString("VideoWin: ") +#define SCALED_RECT(SRC, SCALE) QRect{ static_cast<int>(SRC.left() * SCALE), \ + static_cast<int>(SRC.top() * SCALE), \ + static_cast<int>(SRC.width() * SCALE), \ + static_cast<int>(SRC.height() * SCALE) } + static float fix_aspect(float raw); static float snap(float value, float snapto, float diff); @@ -63,6 +68,14 @@ void VideoOutWindow::ScreenChanged(QScreen */*screen*/) MoveResize(); } +void VideoOutWindow::PhysicalDPIChanged(qreal /*DPI*/) +{ + // PopulateGeometry will update m_devicePixelRatio + PopulateGeometry(); + m_windowRect = m_displayVisibleRect = SCALED_RECT(m_rawWindowRect, m_devicePixelRatio); + MoveResize(); +} + void VideoOutWindow::PopulateGeometry(void) { if (!m_display) @@ -72,6 +85,10 @@ void VideoOutWindow::PopulateGeometry(void) if (!screen) return; +#ifdef Q_OS_MACOS + m_devicePixelRatio = screen->devicePixelRatio(); +#endif + if (MythDisplay::SpanAllScreens() && MythDisplay::GetScreenCount() > 1) { m_screenGeometry = screen->virtualGeometry(); @@ -416,6 +433,9 @@ bool VideoOutWindow::Init(const QSize &VideoDim, const QSize &VideoDispDim, { m_display = Display; connect(m_display, &MythDisplay::CurrentScreenChanged, this, &VideoOutWindow::ScreenChanged); +#ifdef Q_OS_MACOS + connect(m_display, &MythDisplay::PhysicalDPIChanged, this, &VideoOutWindow::PhysicalDPIChanged); +#endif } if (m_display) @@ -429,7 +449,8 @@ bool VideoOutWindow::Init(const QSize &VideoDim, const QSize &VideoDispDim, // N.B. we are always confined to the window size so use that for the initial // displayVisibleRect - m_windowRect = m_displayVisibleRect = WindowRect; + m_rawWindowRect = WindowRect; + m_windowRect = m_displayVisibleRect = SCALED_RECT(WindowRect, m_devicePixelRatio); int pbp_width = m_displayVisibleRect.width() / 2; if (m_pipState == kPBPLeft || m_pipState == kPBPRight) @@ -613,34 +634,38 @@ void VideoOutWindow::SetDisplayAspect(float DisplayAspect) void VideoOutWindow::SetWindowSize(QSize Size) { - if (Size != m_windowRect.size()) + if (Size != m_rawWindowRect.size()) { - QRect rect(m_windowRect.topLeft(), Size); + QRect rect(m_rawWindowRect.topLeft(), Size); LOG(VB_PLAYBACK, LOG_INFO, LOC + QString("New window rect: %1x%2+%3+%4") .arg(rect.width()).arg(rect.height()).arg(rect.left()).arg(rect.top())); - m_windowRect = m_displayVisibleRect = rect; + m_rawWindowRect = rect; + m_windowRect = m_displayVisibleRect = SCALED_RECT(rect, m_devicePixelRatio); MoveResize(); } } void VideoOutWindow::SetITVResize(QRect Rect) { - QRect oldrect = m_itvDisplayVideoRect; + QRect oldrect = m_rawItvDisplayVideoRect; if (Rect.isEmpty()) { m_itvResizing = false; m_itvDisplayVideoRect = QRect(); + m_rawItvDisplayVideoRect = QRect(); } else { m_itvResizing = true; - m_itvDisplayVideoRect = Rect; + m_rawItvDisplayVideoRect = Rect; + m_itvDisplayVideoRect = SCALED_RECT(Rect, m_devicePixelRatio); } - if (m_itvDisplayVideoRect != oldrect) + if (m_rawItvDisplayVideoRect != oldrect) { - LOG(VB_PLAYBACK, LOG_INFO, LOC + QString("New ITV display rect: %1x%2+%3+%4") + LOG(VB_PLAYBACK, LOG_INFO, LOC + QString("New ITV display rect: %1x%2+%3+%4 (Scale: %1)") .arg(m_itvDisplayVideoRect.width()).arg(m_itvDisplayVideoRect.height()) - .arg(m_itvDisplayVideoRect.left()).arg(m_itvDisplayVideoRect.right())); + .arg(m_itvDisplayVideoRect.left()).arg(m_itvDisplayVideoRect.right()) + .arg(m_devicePixelRatio)); MoveResize(); } } @@ -679,14 +704,19 @@ void VideoOutWindow::ResizeDisplayWindow(const QRect &Rect, bool SaveVisibleRect */ void VideoOutWindow::EmbedInWidget(const QRect &Rect) { - if (m_embedding && (Rect == m_embeddingRect)) + if (m_embedding && (Rect == m_rawEmbeddingRect)) return; - LOG(VB_PLAYBACK, LOG_INFO, LOC + QString("New embedding rect: %1x%2+%3+%4") - .arg(Rect.width()).arg(Rect.height()).arg(Rect.left()).arg(Rect.top())); - m_embeddingRect = Rect; + + m_rawEmbeddingRect = Rect; + m_embeddingRect = SCALED_RECT(Rect, m_devicePixelRatio); bool savevisiblerect = !m_embedding; m_embedding = true; - m_displayVideoRect = Rect; + m_displayVideoRect = m_embeddingRect; + + LOG(VB_PLAYBACK, LOG_INFO, LOC + QString("New embedding rect: %1x%2+%3+%4 (Scale: %1)") + .arg(m_embeddingRect.width()).arg(m_embeddingRect.height()) + .arg(m_embeddingRect.left()).arg(m_embeddingRect.top()) + .arg(m_devicePixelRatio)); ResizeDisplayWindow(m_displayVideoRect, savevisiblerect); } @@ -699,6 +729,7 @@ void VideoOutWindow::StopEmbedding(void) if (!m_embedding) return; LOG(VB_PLAYBACK, LOG_INFO, LOC + "Stopped embedding"); + m_rawEmbeddingRect = QRect(); m_embeddingRect = QRect(); m_displayVisibleRect = m_tmpDisplayVisibleRect; m_embedding = false; diff --git a/mythtv/libs/libmythtv/videooutwindow.h b/mythtv/libs/libmythtv/videooutwindow.h index 9480045c92..a5efc757f5 100644 --- a/mythtv/libs/libmythtv/videooutwindow.h +++ b/mythtv/libs/libmythtv/videooutwindow.h @@ -45,6 +45,7 @@ class VideoOutWindow : public QObject public slots: void ScreenChanged (QScreen *screen); + void PhysicalDPIChanged (qreal /*DPI*/); // Sets void InputChanged (const QSize &VideoDim, const QSize &VideoDispDim, float Aspect); @@ -74,13 +75,12 @@ class VideoOutWindow : public QObject float GetOverridenVideoAspect(void) const { return m_videoAspectOverride;} QRect GetDisplayVisibleRect(void) const { return m_displayVisibleRect; } QRect GetWindowRect(void) const { return m_windowRect; } + QRect GetRawWindowRect(void) const { return m_rawWindowRect; } QRect GetScreenGeometry(void) const { return m_screenGeometry; } QRect GetVideoRect(void) const { return m_videoRect; } QRect GetDisplayVideoRect(void) const { return m_displayVideoRect; } - QRect GetEmbeddingRect(void) const { return m_embeddingRect; } + QRect GetEmbeddingRect(void) const { return m_rawEmbeddingRect; } bool UsingGuiSize(void) const { return m_dbUseGUISize; } - bool GetITVResizing(void) const { return m_itvResizing; } - QRect GetITVDisplayRect(void) const { return m_itvDisplayVideoRect; } QString GetZoomString(void) const; AspectOverrideMode GetAspectOverride(void) const { return m_videoAspectOverrideMode; } AdjustFillMode GetAdjustFill(void) const { return m_adjustFill; } @@ -115,6 +115,7 @@ class VideoOutWindow : public QObject bool m_dbScalingAllowed {true}; ///< disable this to prevent overscan/underscan bool m_dbUseGUISize {false}; ///< Use the gui size for video window QRect m_screenGeometry {0,0,1024,768}; ///< Full screen geometry + qreal m_devicePixelRatio {1.0}; // Manual Zoom float m_manualVertScale {1.0F}; ///< Manually applied vertical scaling. @@ -147,15 +148,19 @@ class VideoOutWindow : public QObject QRect m_displayVisibleRect {0,0,0,0}; /// Rectangle describing QWidget bounds. QRect m_windowRect {0,0,0,0}; + /// Rectangle describing QWidget bounds - not adjusted for high DPI scaling (macos) + QRect m_rawWindowRect {0,0,0,0}; /// Used to save the display_visible_rect for /// restoration after video embedding ends. QRect m_tmpDisplayVisibleRect {0,0,0,0}; /// Embedded video rectangle QRect m_embeddingRect; + QRect m_rawEmbeddingRect; // Interactive TV (MHEG) video embedding bool m_itvResizing {false}; QRect m_itvDisplayVideoRect; + QRect m_rawItvDisplayVideoRect; /// State variables bool m_embedding {false}; diff --git a/mythtv/libs/libmythui/mythdisplay.cpp b/mythtv/libs/libmythui/mythdisplay.cpp index e3f1483a8c..2470ef3ae8 100644 --- a/mythtv/libs/libmythui/mythdisplay.cpp +++ b/mythtv/libs/libmythui/mythdisplay.cpp @@ -183,7 +183,10 @@ MythDisplay::MythDisplay() m_screen = GetDesiredScreen(); DebugScreen(m_screen, "Using"); if (m_screen) + { connect(m_screen, &QScreen::geometryChanged, this, &MythDisplay::GeometryChanged); + connect(m_screen, &QScreen::physicalDotsPerInchChanged, this, &MythDisplay::PhysicalDPIChanged); + } connect(qGuiApp, &QGuiApplication::screenRemoved, this, &MythDisplay::ScreenRemoved); connect(qGuiApp, &QGuiApplication::screenAdded, this, &MythDisplay::ScreenAdded); @@ -389,10 +392,18 @@ void MythDisplay::ScreenChanged(QScreen *qScreen) DebugScreen(qScreen, "Changed to"); m_screen = qScreen; connect(m_screen, &QScreen::geometryChanged, this, &MythDisplay::GeometryChanged); + connect(m_screen, &QScreen::physicalDotsPerInchChanged, this, &MythDisplay::PhysicalDPIChanged); Initialise(); emit CurrentScreenChanged(qScreen); } +void MythDisplay::PhysicalDPIChanged(qreal DPI) +{ + LOG(VB_GENERAL, LOG_INFO, LOC + QString("Qt screen pixel ratio changed to %1") + .arg(DPI, 2, 'f', 2, '0')); + emit CurrentDPIChanged(DPI); +} + void MythDisplay::PrimaryScreenChanged(QScreen* qScreen) { DebugScreen(qScreen, "New primary"); @@ -475,7 +486,8 @@ void MythDisplay::DebugScreen(QScreen *qScreen, const QString &Message) LOG(VB_GENERAL, LOG_INFO, LOC + QString("%1 screen '%2' %3") .arg(Message).arg(qScreen->name()).arg(extra)); - + LOG(VB_GENERAL, LOG_INFO, LOC + QString("Qt screen pixel ratio: %1") + .arg(qScreen->devicePixelRatio(), 2, 'f', 2, '0')); LOG(VB_GENERAL, LOG_INFO, LOC + QString("Geometry: %1x%2+%3+%4 Size(Qt): %5mmx%6mm") .arg(geom.width()).arg(geom.height()).arg(geom.left()).arg(geom.top()) .arg(qScreen->physicalSize().width()).arg(qScreen->physicalSize().height())); @@ -1047,6 +1059,7 @@ void MythDisplay::ConfigureQtGUI(int SwapInterval) { // Set the default surface format. Explicitly required on some platforms. QSurfaceFormat format; + format.setAlphaBufferSize(0); format.setDepthBufferSize(0); format.setStencilBufferSize(0); format.setSwapBehavior(QSurfaceFormat::DoubleBuffer); @@ -1059,6 +1072,20 @@ void MythDisplay::ConfigureQtGUI(int SwapInterval) // of the MythPushButton widgets, and they don't use the themed background. QApplication::setDesktopSettingsAware(false); #endif + + // If Wayland decorations are enabled, the default framebuffer format is forced + // to use alpha. This framebuffer is rendered with alpha blending by the wayland + // compositor - so any translucent areas of our UI will allow the underlying + // window to bleed through. + // N.B. this is probably not the most performant solution as compositors MAY + // still render hidden windows. A better solution is probably to call + // wl_surface_set_opaque_region on the wayland surface. This is confirmed to work + // and should allow the compositor to optimise rendering for opaque areas. It does + // however require linking to libwayland-client AND including private Qt headers + // to retrieve the surface and compositor structures (the latter being a significant issue). + // see also setAlphaBufferSize above + setenv("QT_WAYLAND_DISABLE_WINDOWDECORATION", "1", 0); + #if defined (Q_OS_LINUX) && defined (USING_EGL) // We want to use EGL for VAAPI/MMAL/DRMPRIME rendering to ensure we // can use zero copy video buffers for the best performance (N.B. not tested diff --git a/mythtv/libs/libmythui/mythdisplay.h b/mythtv/libs/libmythui/mythdisplay.h index 6260043538..d3b2a97a21 100644 --- a/mythtv/libs/libmythui/mythdisplay.h +++ b/mythtv/libs/libmythui/mythdisplay.h @@ -57,10 +57,12 @@ class MUI_PUBLIC MythDisplay : public QObject, public ReferenceCounter void ScreenAdded (QScreen *qScreen); void ScreenRemoved (QScreen *qScreen); void GeometryChanged (const QRect &Geometry); + void PhysicalDPIChanged (qreal DPI); signals: void CurrentScreenChanged (QScreen *qScreen); void ScreenCountChanged (int Screens); + void CurrentDPIChanged (qreal DPI); protected: MythDisplay(); diff --git a/mythtv/libs/libmythui/mythmainwindow.cpp b/mythtv/libs/libmythui/mythmainwindow.cpp index 7bdfe41bb3..619ba7f7f7 100644 --- a/mythtv/libs/libmythui/mythmainwindow.cpp +++ b/mythtv/libs/libmythui/mythmainwindow.cpp @@ -690,10 +690,12 @@ void MythMainWindow::animate(void) if (!d->m_repaintRegion.isEmpty()) redraw = true; - foreach (auto & widget, d->m_stackList) + // The call to GetDrawOrder can apparently alter m_stackList. + // NOLINTNEXTLINE(modernize-loop-convert) + for (auto it = d->m_stackList.begin(); it != d->m_stackList.end(); ++it) { QVector<MythScreenType *> drawList; - widget->GetDrawOrder(drawList); + (*it)->GetDrawOrder(drawList); foreach (auto & screen, drawList) { @@ -733,10 +735,12 @@ void MythMainWindow::drawScreen(void) // Check for any widgets that have been updated since we built // the dirty region list in ::animate() - foreach (auto & widget, d->m_stackList) + // The call to GetDrawOrder can apparently alter m_stackList. + // NOLINTNEXTLINE(modernize-loop-convert) + for (auto it = d->m_stackList.begin(); it != d->m_stackList.end(); ++it) { QVector<MythScreenType *> redrawList; - widget->GetDrawOrder(redrawList); + (*it)->GetDrawOrder(redrawList); foreach (auto & screen, redrawList) { @@ -823,11 +827,12 @@ void MythMainWindow::draw(MythPainter *painter /* = 0 */) if (r != d->m_uiScreenRect) painter->SetClipRect(r); - foreach (auto & widget, d->m_stackList) + // The call to GetDrawOrder can apparently alter m_stackList. + // NOLINTNEXTLINE(modernize-loop-convert) + for (auto it = d->m_stackList.begin(); it != d->m_stackList.end(); ++it) { QVector<MythScreenType *> redrawList; - widget->GetDrawOrder(redrawList); - + (*it)->GetDrawOrder(redrawList); foreach (auto & screen, redrawList) { screen->Draw(painter, 0, 0, 255, r); diff --git a/mythtv/libs/libmythui/mythpainter.cpp b/mythtv/libs/libmythui/mythpainter.cpp index 4435efb78a..d70010839d 100644 --- a/mythtv/libs/libmythui/mythpainter.cpp +++ b/mythtv/libs/libmythui/mythpainter.cpp @@ -5,6 +5,7 @@ // QT headers #include <QRect> #include <QPainter> +#include <QPainterPath> // libmythbase headers #include "mythlogging.h" diff --git a/mythtv/libs/libmythui/mythpainter.h b/mythtv/libs/libmythui/mythpainter.h index b6b054a813..67175a1ed5 100644 --- a/mythtv/libs/libmythui/mythpainter.h +++ b/mythtv/libs/libmythui/mythpainter.h @@ -29,8 +29,10 @@ class UIEffects; using LayoutVector = QVector<QTextLayout *>; using FormatVector = QVector<QTextLayout::FormatRange>; -class MUI_PUBLIC MythPainter +class MUI_PUBLIC MythPainter : public QObject { + Q_OBJECT + public: MythPainter(); /** MythPainter destructor. diff --git a/mythtv/libs/libmythui/mythuistatetype.cpp b/mythtv/libs/libmythui/mythuistatetype.cpp index 2d1237787b..39808cc198 100644 --- a/mythtv/libs/libmythui/mythuistatetype.cpp +++ b/mythtv/libs/libmythui/mythuistatetype.cpp @@ -93,7 +93,7 @@ bool MythUIStateType::DisplayState(const QString &name) if (i != m_ObjectsByName.end()) m_CurrentState = i.value(); else - return false; + m_CurrentState = nullptr; if (m_CurrentState != old) { diff --git a/mythtv/libs/libmythui/opengl/mythpainteropengl.cpp b/mythtv/libs/libmythui/opengl/mythpainteropengl.cpp index 8fec14d3c2..2510d479c6 100644 --- a/mythtv/libs/libmythui/opengl/mythpainteropengl.cpp +++ b/mythtv/libs/libmythui/opengl/mythpainteropengl.cpp @@ -20,10 +20,20 @@ MythOpenGLPainter::MythOpenGLPainter(MythRenderOpenGL *Render, QWidget *Parent) if (!m_render) LOG(VB_GENERAL, LOG_ERR, "OpenGL painter has no render device"); + +#ifdef Q_OS_MACOS + m_display = MythDisplay::AcquireRelease(); + CurrentDPIChanged(m_parent->devicePixelRatioF()); + connect(m_display, &MythDisplay::CurrentDPIChanged, this, &MythOpenGLPainter::CurrentDPIChanged); +#endif } MythOpenGLPainter::~MythOpenGLPainter() { +#ifdef Q_OS_MACOS + MythDisplay::AcquireRelease(false); +#endif + if (!m_render) return; if (!m_render->IsReady()) @@ -84,6 +94,13 @@ void MythOpenGLPainter::ClearCache(void) m_imageToTextureMap.clear(); } +void MythOpenGLPainter::CurrentDPIChanged(qreal DPI) +{ + m_pixelRatio = DPI; + m_usingHighDPI = !qFuzzyCompare(m_pixelRatio, 1.0); + LOG(VB_GENERAL, LOG_INFO, QString("High DPI scaling %1").arg(m_usingHighDPI ? "enabled" : "disabled")); +} + void MythOpenGLPainter::Begin(QPaintDevice *Parent) { MythPainter::Begin(Parent); @@ -109,13 +126,17 @@ void MythOpenGLPainter::Begin(QPaintDevice *Parent) buf = m_render->CreateVBO(static_cast<int>(MythRenderOpenGL::kVertexSize)); } + QSize currentsize = m_parent->size(); + // check if we need to adjust cache sizes - if (m_lastSize != m_parent->size()) + // NOTE - don't use the scaled size if using high DPI. Our images are at the lower + // resolution + if (m_lastSize != currentsize) { // This will scale the cache depending on the resolution in use static const int s_onehd = 1920 * 1080; static const int s_basesize = 64; - m_lastSize = m_parent->size(); + m_lastSize = currentsize; float hdscreens = (static_cast<float>(m_lastSize.width() + 1) * m_lastSize.height()) / s_onehd; int cpu = qMax(static_cast<int>(hdscreens * s_basesize), s_basesize); int gpu = cpu * 3 / 2; @@ -128,13 +149,20 @@ void MythOpenGLPainter::Begin(QPaintDevice *Parent) DeleteTextures(); m_render->makeCurrent(); - if (m_target || m_swapControl) + if (m_target || m_viewControl.testFlag(Framebuffer)) { m_render->BindFramebuffer(m_target); - m_render->SetViewPort(QRect(0, 0, m_parent->width(), m_parent->height())); m_render->SetBackground(0, 0, 0, 0); m_render->ClearFramebuffer(); } + + if (m_target || m_viewControl.testFlag(Viewport)) + { + // If using high DPI then scale the viewport + if (m_usingHighDPI) + currentsize *= m_pixelRatio; + m_render->SetViewPort(QRect(0, 0, currentsize.width(), currentsize.height())); + } } void MythOpenGLPainter::End(void) @@ -147,7 +175,7 @@ void MythOpenGLPainter::End(void) if (VERBOSE_LEVEL_CHECK(VB_GPU, LOG_INFO)) m_render->logDebugMarker("PAINTER_FRAME_END"); - if (m_target == nullptr && m_swapControl) + if (m_target == nullptr && m_viewControl.testFlag(Framebuffer)) { m_render->Flush(); m_render->swapBuffers(); @@ -221,12 +249,28 @@ MythGLTexture* MythOpenGLPainter::GetTextureFromCache(MythImage *Image) return texture; } +#ifdef Q_OS_MACOS +#define DEST dest +#else +#define DEST Dest +#endif + void MythOpenGLPainter::DrawImage(const QRect &Dest, MythImage *Image, const QRect &Source, int Alpha) { if (m_render) { - // Drawing an image multiple times with the same VBO will stall most GPUs as + qreal pixelratio = 1.0; + if (m_usingHighDPI && m_viewControl.testFlag(Viewport)) + pixelratio = m_pixelRatio; +#ifdef Q_OS_MACOS + QRect dest = QRect(static_cast<int>(Dest.left() * pixelratio), + static_cast<int>(Dest.top() * pixelratio), + static_cast<int>(Dest.width() * pixelratio), + static_cast<int>(Dest.height() * pixelratio)); +#endif + + // Drawing an image multiple times with the same VBO will stall most GPUs as // the VBO is re-mapped whilst still in use. Use a pooled VBO instead. MythGLTexture *texture = GetTextureFromCache(Image); if (texture && m_mappedTextures.contains(texture)) @@ -234,7 +278,7 @@ void MythOpenGLPainter::DrawImage(const QRect &Dest, MythImage *Image, QOpenGLBuffer *vbo = texture->m_vbo; texture->m_vbo = m_mappedBufferPool[m_mappedBufferPoolIdx]; texture->m_destination = QRect(); - m_render->DrawBitmap(texture, m_target, Source, Dest, nullptr, Alpha); + m_render->DrawBitmap(texture, m_target, Source, DEST, nullptr, Alpha, pixelratio); texture->m_destination = QRect(); texture->m_vbo = vbo; if (++m_mappedBufferPoolIdx >= MAX_BUFFER_POOL) @@ -242,17 +286,26 @@ void MythOpenGLPainter::DrawImage(const QRect &Dest, MythImage *Image, } else { - m_render->DrawBitmap(texture, m_target, Source, Dest, nullptr, Alpha); + m_render->DrawBitmap(texture, m_target, Source, DEST, nullptr, Alpha, pixelratio); m_mappedTextures.append(texture); } } } +/*! \brief Draw a rectangle + * + * If it is a simple rectangle, then use our own shaders for rendering (which + * saves texture memory but may not be as accurate as Qt rendering) otherwise + * fallback to Qt painting to a QImage, which is uploaded as a texture. + * + * \note If high DPI scaling is in use, just use Qt painting rather than + * handling all of the adjustments required for pen width etc etc. +*/ void MythOpenGLPainter::DrawRect(const QRect &Area, const QBrush &FillBrush, const QPen &LinePen, int Alpha) { if ((FillBrush.style() == Qt::SolidPattern || - FillBrush.style() == Qt::NoBrush) && m_render) + FillBrush.style() == Qt::NoBrush) && m_render && !m_usingHighDPI) { m_render->DrawRect(m_target, Area, FillBrush, LinePen, Alpha); return; @@ -265,7 +318,7 @@ void MythOpenGLPainter::DrawRoundRect(const QRect &Area, int CornerRadius, const QPen &LinePen, int Alpha) { if ((FillBrush.style() == Qt::SolidPattern || - FillBrush.style() == Qt::NoBrush) && m_render) + FillBrush.style() == Qt::NoBrush) && m_render && !m_usingHighDPI) { m_render->DrawRoundRect(m_target, Area, CornerRadius, FillBrush, LinePen, Alpha); diff --git a/mythtv/libs/libmythui/opengl/mythpainteropengl.h b/mythtv/libs/libmythui/opengl/mythpainteropengl.h index 097577231f..06407b9380 100644 --- a/mythtv/libs/libmythui/opengl/mythpainteropengl.h +++ b/mythtv/libs/libmythui/opengl/mythpainteropengl.h @@ -6,6 +6,7 @@ #include <QQueue> // MythTV +#include "mythdisplay.h" #include "mythpainter.h" #include "mythimage.h" @@ -22,12 +23,22 @@ class QOpenGLFramebufferObject; class MUI_PUBLIC MythOpenGLPainter : public MythPainter { + Q_OBJECT + public: + enum ViewControl + { + None = 0x00, + Viewport = 0x01, + Framebuffer = 0x02 + }; + Q_DECLARE_FLAGS(ViewControls, ViewControl) + explicit MythOpenGLPainter(MythRenderOpenGL *Render = nullptr, QWidget *Parent = nullptr); ~MythOpenGLPainter() override; void SetTarget(QOpenGLFramebufferObject* NewTarget) { m_target = NewTarget; } - void SetSwapControl(bool Swap) { m_swapControl = Swap; } + void SetViewControl(ViewControls Control) { m_viewControl = Control; } void DeleteTextures(void); // MythPainter @@ -46,6 +57,9 @@ class MUI_PUBLIC MythOpenGLPainter : public MythPainter void PushTransformation(const UIEffects &Fx, QPointF Center = QPointF()) override; void PopTransformation(void) override; + public slots: + void CurrentDPIChanged(qreal DPI); + protected: void ClearCache(void); MythGLTexture* GetTextureFromCache(MythImage *Image); @@ -58,8 +72,11 @@ class MUI_PUBLIC MythOpenGLPainter : public MythPainter QWidget *m_parent { nullptr }; MythRenderOpenGL *m_render { nullptr }; QOpenGLFramebufferObject* m_target { nullptr }; - bool m_swapControl { true }; + ViewControls m_viewControl { Viewport | Framebuffer }; QSize m_lastSize { }; + qreal m_pixelRatio { 1.0 }; + MythDisplay* m_display { nullptr }; + bool m_usingHighDPI { false }; QMap<MythImage *, MythGLTexture*> m_imageToTextureMap; std::list<MythImage *> m_ImageExpireList; @@ -72,4 +89,6 @@ class MUI_PUBLIC MythOpenGLPainter : public MythPainter bool m_mappedBufferPoolReady { false }; }; +Q_DECLARE_OPERATORS_FOR_FLAGS(MythOpenGLPainter::ViewControls) + #endif diff --git a/mythtv/libs/libmythui/opengl/mythrenderopengl.cpp b/mythtv/libs/libmythui/opengl/mythrenderopengl.cpp index e34320f3dc..684740c584 100644 --- a/mythtv/libs/libmythui/opengl/mythrenderopengl.cpp +++ b/mythtv/libs/libmythui/opengl/mythrenderopengl.cpp @@ -804,7 +804,7 @@ void MythRenderOpenGL::ClearFramebuffer(void) void MythRenderOpenGL::DrawBitmap(MythGLTexture *Texture, QOpenGLFramebufferObject *Target, const QRect &Source, const QRect &Destination, - QOpenGLShaderProgram *Program, int Alpha) + QOpenGLShaderProgram *Program, int Alpha, qreal Scale) { makeCurrent(); @@ -827,7 +827,7 @@ void MythRenderOpenGL::DrawBitmap(MythGLTexture *Texture, QOpenGLFramebufferObje QOpenGLBuffer* buffer = Texture->m_vbo; buffer->bind(); - if (UpdateTextureVertices(Texture, Source, Destination, 0)) + if (UpdateTextureVertices(Texture, Source, Destination, 0, Scale)) { if (m_extraFeaturesUsed & kGLBufferMap) { @@ -1262,7 +1262,7 @@ QStringList MythRenderOpenGL::GetDescription(void) } bool MythRenderOpenGL::UpdateTextureVertices(MythGLTexture *Texture, const QRect &Source, - const QRect &Destination, int Rotation) + const QRect &Destination, int Rotation, qreal Scale) { if (!Texture || (Texture && Texture->m_size.isEmpty())) return false; @@ -1301,8 +1301,8 @@ bool MythRenderOpenGL::UpdateTextureVertices(MythGLTexture *Texture, const QRect data[4 + TEX_OFFSET] = data[6 + TEX_OFFSET]; data[5 + TEX_OFFSET] = data[1 + TEX_OFFSET]; - width = Texture->m_crop ? min(width, Destination.width()) : Destination.width(); - height = Texture->m_crop ? min(height, Destination.height()) : Destination.height(); + width = Texture->m_crop ? min(static_cast<int>(width * Scale), Destination.width()) : Destination.width(); + height = Texture->m_crop ? min(static_cast<int>(height * Scale), Destination.height()) : Destination.height(); data[2] = data[0] = Destination.left(); data[5] = data[1] = Destination.top(); diff --git a/mythtv/libs/libmythui/opengl/mythrenderopengl.h b/mythtv/libs/libmythui/opengl/mythrenderopengl.h index 199f0d642b..2ccb9a60d5 100644 --- a/mythtv/libs/libmythui/opengl/mythrenderopengl.h +++ b/mythtv/libs/libmythui/opengl/mythrenderopengl.h @@ -143,7 +143,7 @@ class MUI_PUBLIC MythRenderOpenGL : public QOpenGLContext, public QOpenGLFunctio void DrawBitmap(MythGLTexture *Texture, QOpenGLFramebufferObject *Target, const QRect &Source, const QRect &Destination, - QOpenGLShaderProgram *Program, int Alpha = 255); + QOpenGLShaderProgram *Program, int Alpha = 255, qreal Scale = 1.0); void DrawBitmap(MythGLTexture **Textures, uint TextureCount, QOpenGLFramebufferObject *Target, const QRect &Source, const QRect &Destination, @@ -171,7 +171,7 @@ class MUI_PUBLIC MythRenderOpenGL : public QOpenGLContext, public QOpenGLFunctio void SetMatrixView(void); void DeleteFramebuffers(void); static bool UpdateTextureVertices(MythGLTexture *Texture, const QRect &Source, - const QRect &Destination, int Rotation); + const QRect &Destination, int Rotation, qreal Scale = 1.0); GLfloat* GetCachedVertices(GLuint Type, const QRect &Area); void ExpireVertices(int Max = 0); void GetCachedVBO(GLuint Type, const QRect &Area); diff --git a/mythtv/libs/libmythupnp/upnphelpers.cpp b/mythtv/libs/libmythupnp/upnphelpers.cpp index cda4f86a77..3a2bfbba3c 100644 --- a/mythtv/libs/libmythupnp/upnphelpers.cpp +++ b/mythtv/libs/libmythupnp/upnphelpers.cpp @@ -90,7 +90,7 @@ QString resDurationFormat(uint32_t msec) // M = Minutes (2 digits, 0 prefix) // S = Seconds (2 digits, 0 prefix) // FS = Fractional Seconds (milliseconds) - return QString("%01u:%02u:%02u.%01u") + return QString("%1:%2:%3.%4") .arg((msec / (1000 * 60 * 60)) % 24, 1,10,QChar('0')) // Hours .arg((msec / (1000 * 60)) % 60, 2,10,QChar('0')) // Minutes .arg((msec / 1000) % 60, 2,10,QChar('0')) // Seconds diff --git a/mythtv/programs/mythbackend/autoexpire.cpp b/mythtv/programs/mythbackend/autoexpire.cpp index 6f2f309b75..efadbe08d4 100644 --- a/mythtv/programs/mythbackend/autoexpire.cpp +++ b/mythtv/programs/mythbackend/autoexpire.cpp @@ -190,7 +190,7 @@ void AutoExpire::CalcParams() foreach (auto cardid, fsEncoderMap[fsit->getFSysID()]) { - EncoderLink *enc = *(m_encoderList->find(cardid)); + EncoderLink *enc = *(m_encoderList->constFind(cardid)); if (!enc->IsConnected() || !enc->IsBusy()) { @@ -541,9 +541,8 @@ void AutoExpire::ExpireRecordings(void) if (!p->IsLocal()) { bool foundFile = false; - QMap<int, EncoderLink *>::Iterator eit = - m_encoderList->begin(); - while (eit != m_encoderList->end()) + auto eit = m_encoderList->constBegin(); + while (eit != m_encoderList->constEnd()) { EncoderLink *el = *eit; eit++; @@ -555,7 +554,7 @@ void AutoExpire::ExpireRecordings(void) if (el->IsConnected()) foundFile = el->CheckFile(p); - eit = m_encoderList->end(); + eit = m_encoderList->constEnd(); } } diff --git a/mythtv/programs/mythbackend/httpstatus.cpp b/mythtv/programs/mythbackend/httpstatus.cpp index 011c7f6ee1..1dc6e6f038 100644 --- a/mythtv/programs/mythbackend/httpstatus.cpp +++ b/mythtv/programs/mythbackend/httpstatus.cpp @@ -197,7 +197,7 @@ void HttpStatus::FillStatusXML( QDomDocument *pDoc ) TVRec::s_inputsLock.lockForRead(); - foreach (auto elink, *m_pEncoders) + for (auto * elink : qAsConst(*m_pEncoders)) { if (elink != nullptr) { diff --git a/mythtv/programs/mythbackend/main.cpp b/mythtv/programs/mythbackend/main.cpp index 7cf7f5b644..ddbef2fa53 100644 --- a/mythtv/programs/mythbackend/main.cpp +++ b/mythtv/programs/mythbackend/main.cpp @@ -96,8 +96,9 @@ int main(int argc, char **argv) #ifdef Q_OS_MAC QString path = QCoreApplication::applicationDirPath(); setenv("PYTHONPATH", - QString("%1/../Resources/lib/python2.6/site-packages:%2") + QString("%1/../Resources/lib/%2/site-packages:%3") .arg(path) + .arg(QFileInfo(PYTHON_EXE).fileName()) .arg(QProcessEnvironment::systemEnvironment().value("PYTHONPATH")) .toUtf8().constData(), 1); #endif diff --git a/mythtv/programs/mythbackend/mainserver.cpp b/mythtv/programs/mythbackend/mainserver.cpp index 76e3702a44..072b1fb888 100644 --- a/mythtv/programs/mythbackend/mainserver.cpp +++ b/mythtv/programs/mythbackend/mainserver.cpp @@ -1863,7 +1863,7 @@ void MainServer::HandleAnnounce(QStringList &slist, QStringList commands, bool wasAsleep = true; TVRec::s_inputsLock.lockForRead(); - foreach (auto elink, *m_encoderList) + for (auto * elink : qAsConst(*m_encoderList)) { if (elink->GetHostName() == commands[2]) { @@ -2810,7 +2810,7 @@ void MainServer::HandleCheckRecordingActive(QStringList &slist, else { TVRec::s_inputsLock.lockForRead(); - for (auto iter = m_encoderList->begin(); iter != m_encoderList->end(); ++iter) + for (auto iter = m_encoderList->constBegin(); iter != m_encoderList->constEnd(); ++iter) { EncoderLink *elink = *iter; @@ -2909,7 +2909,7 @@ void MainServer::DoHandleStopRecording( int recnum = -1; TVRec::s_inputsLock.lockForRead(); - for (auto iter = m_encoderList->begin(); iter != m_encoderList->end(); ++iter) + for (auto iter = m_encoderList->constBegin(); iter != m_encoderList->constEnd(); ++iter) { EncoderLink *elink = *iter; @@ -2930,6 +2930,8 @@ void MainServer::DoHandleStopRecording( if (m_sched) m_sched->UpdateRecStatus(&recinfo); } + + break; } } TVRec::s_inputsLock.unlock(); @@ -4238,7 +4240,7 @@ void MainServer::HandleLockTuner(PlaybackSock *pbs, int cardid) QString enchost; TVRec::s_inputsLock.lockForRead(); - foreach (auto elink, *m_encoderList) + for (auto * elink : qAsConst(*m_encoderList)) { // we're looking for a specific card but this isn't the one we want if ((cardid != -1) && (cardid != elink->GetInputID())) @@ -4315,8 +4317,8 @@ void MainServer::HandleFreeTuner(int cardid, PlaybackSock *pbs) EncoderLink *encoder = nullptr; TVRec::s_inputsLock.lockForRead(); - auto iter = m_encoderList->find(cardid); - if (iter == m_encoderList->end()) + auto iter = m_encoderList->constFind(cardid); + if (iter == m_encoderList->constEnd()) { LOG(VB_GENERAL, LOG_ERR, LOC + "MainServer::HandleFreeTuner() " + QString("Unknown encoder: %1").arg(cardid)); @@ -4363,7 +4365,7 @@ void MainServer::HandleGetFreeInputInfo(PlaybackSock *pbs, // Lopp over each encoder and divide the inputs into busy and free // lists. TVRec::s_inputsLock.lockForRead(); - foreach (auto elink, *m_encoderList) + for (auto * elink : qAsConst(*m_encoderList)) { InputInfo info; info.m_inputId = elink->GetInputID(); @@ -4479,8 +4481,8 @@ void MainServer::HandleRecorderQuery(QStringList &slist, QStringList &commands, int recnum = commands[1].toInt(); TVRec::s_inputsLock.lockForRead(); - auto iter = m_encoderList->find(recnum); - if (iter == m_encoderList->end()) + auto iter = m_encoderList->constFind(recnum); + if (iter == m_encoderList->constEnd()) { TVRec::s_inputsLock.unlock(); LOG(VB_GENERAL, LOG_ERR, LOC + "MainServer::HandleRecorderQuery() " + @@ -4855,8 +4857,8 @@ void MainServer::HandleSetNextLiveTVDir(QStringList &commands, int recnum = commands[1].toInt(); TVRec::s_inputsLock.lockForRead(); - auto iter = m_encoderList->find(recnum); - if (iter == m_encoderList->end()) + auto iter = m_encoderList->constFind(recnum); + if (iter == m_encoderList->constEnd()) { TVRec::s_inputsLock.unlock(); LOG(VB_GENERAL, LOG_ERR, LOC + "MainServer::HandleSetNextLiveTVDir() " + @@ -4895,7 +4897,7 @@ void MainServer::HandleSetChannelInfo(QStringList &slist, PlaybackSock *pbs) } TVRec::s_inputsLock.lockForRead(); - foreach (auto & encoder, *m_encoderList) + for (auto * encoder : qAsConst(*m_encoderList)) { if (encoder) { @@ -4918,8 +4920,8 @@ void MainServer::HandleRemoteEncoder(QStringList &slist, QStringList &commands, QStringList retlist; TVRec::s_inputsLock.lockForRead(); - auto iter = m_encoderList->find(recnum); - if (iter == m_encoderList->end()) + auto iter = m_encoderList->constFind(recnum); + if (iter == m_encoderList->constEnd()) { TVRec::s_inputsLock.unlock(); LOG(VB_GENERAL, LOG_ERR, LOC + @@ -5091,7 +5093,7 @@ size_t MainServer::GetCurrentMaxBitrate(void) size_t totalKBperMin = 0; TVRec::s_inputsLock.lockForRead(); - foreach (auto enc, *m_encoderList) + for (auto * enc : qAsConst(*m_encoderList)) { if (!enc->IsConnected() || !enc->IsBusy()) continue; @@ -7157,7 +7159,7 @@ void MainServer::HandleGetRecorderNum(QStringList &slist, PlaybackSock *pbs) EncoderLink *encoder = nullptr; TVRec::s_inputsLock.lockForRead(); - for (auto iter = m_encoderList->begin(); iter != m_encoderList->end(); ++iter) + for (auto iter = m_encoderList->constBegin(); iter != m_encoderList->constEnd(); ++iter) { EncoderLink *elink = *iter; @@ -7203,8 +7205,8 @@ void MainServer::HandleGetRecorderFromNum(QStringList &slist, QStringList strlist; TVRec::s_inputsLock.lockForRead(); - auto iter = m_encoderList->find(recordernum); - if (iter != m_encoderList->end()) + auto iter = m_encoderList->constFind(recordernum); + if (iter != m_encoderList->constEnd()) encoder = (*iter); TVRec::s_inputsLock.unlock(); @@ -7328,7 +7330,7 @@ void MainServer::HandleIsRecording(QStringList &slist, PlaybackSock *pbs) QStringList retlist; TVRec::s_inputsLock.lockForRead(); - foreach (auto elink, *m_encoderList) + for (auto * elink : qAsConst(*m_encoderList)) { if (elink->IsBusyRecording()) { RecordingsInProgress++; @@ -7793,7 +7795,7 @@ void MainServer::connectionClosed(MythSocket *socket) bool isFallingAsleep = true; TVRec::s_inputsLock.lockForRead(); - foreach (auto elink, *m_encoderList) + for (auto * elink : qAsConst(*m_encoderList)) { if (elink->GetSocket() == pbs) { @@ -7834,7 +7836,7 @@ void MainServer::connectionClosed(MythSocket *socket) if (chain->HostSocketCount() == 0) { TVRec::s_inputsLock.lockForRead(); - foreach (auto enc, *m_encoderList) + for (auto * enc : qAsConst(*m_encoderList)) { if (enc->IsLocal()) { @@ -8181,7 +8183,7 @@ void MainServer::reconnectTimeout(void) QStringList strlist( str ); TVRec::s_inputsLock.lockForRead(); - foreach (auto elink, *m_encoderList) + for (auto * elink : qAsConst(*m_encoderList)) { elink->CancelNextRecording(true); ProgramInfo *pinfo = elink->GetRecording(); @@ -8354,7 +8356,7 @@ void MainServer::UpdateSystemdStatus (void) { int active = 0; TVRec::s_inputsLock.lockForRead(); - foreach (auto elink, *m_encoderList) + for (auto * elink : qAsConst(*m_encoderList)) { if (not elink->IsLocal()) continue; diff --git a/mythtv/programs/mythbackend/scheduler.cpp b/mythtv/programs/mythbackend/scheduler.cpp index 7a9762ea46..666b67820f 100644 --- a/mythtv/programs/mythbackend/scheduler.cpp +++ b/mythtv/programs/mythbackend/scheduler.cpp @@ -1065,7 +1065,8 @@ bool Scheduler::FindNextConflict( const RecordingInfo *p, RecConstIter &iter, OpenEndType openEnd, - uint *paffinity) const + uint *paffinity, + bool ignoreinput) const { uint affinity = 0; for ( ; iter != cardlist.end(); ++iter) @@ -1082,7 +1083,7 @@ bool Scheduler::FindNextConflict( if (debugConflicts) msg = QString("comparing with '%1' ").arg(q->GetTitle()); - if (p->GetInputID() != q->GetInputID()) + if (p->GetInputID() != q->GetInputID() && !ignoreinput) { const vector <uint> &conflicting_inputs = m_sinputInfoMap[p->GetInputID()].m_conflictingInputs; @@ -1715,7 +1716,8 @@ void Scheduler::getConflicting(RecordingInfo *pginfo, RecList *retlist) QReadLocker tvlocker(&TVRec::s_inputsLock); RecConstIter i = m_recList.begin(); - for (; FindNextConflict(m_recList, pginfo, i); ++i) + for (; FindNextConflict(m_recList, pginfo, i, openEndNever, + nullptr, true); ++i) { const RecordingInfo *p = *i; retlist->push_back(new RecordingInfo(*p)); @@ -2513,8 +2515,8 @@ void Scheduler::HandleWakeSlave(RecordingInfo &ri, int prerollseconds) QReadLocker tvlocker(&TVRec::s_inputsLock); - QMap<int, EncoderLink*>::iterator tvit = m_tvList->find(ri.GetInputID()); - if (tvit == m_tvList->end()) + QMap<int, EncoderLink*>::const_iterator tvit = m_tvList->constFind(ri.GetInputID()); + if (tvit == m_tvList->constEnd()) return; QString sysEventKey = ri.MakeUniqueKey(); @@ -2597,7 +2599,7 @@ void Scheduler::HandleWakeSlave(RecordingInfo &ri, int prerollseconds) "to reschedule around its tuners.") .arg(nexttv->GetHostName())); - foreach (auto & enc, *m_tvList) + for (auto * enc : qAsConst(*m_tvList)) { if (enc->GetHostName() == nexttv->GetHostName()) enc->SetSleepStatus(sStatus_Undefined); @@ -2671,8 +2673,8 @@ bool Scheduler::HandleRecording( QReadLocker tvlocker(&TVRec::s_inputsLock); - QMap<int, EncoderLink*>::iterator tvit = m_tvList->find(ri.GetInputID()); - if (tvit == m_tvList->end()) + QMap<int, EncoderLink*>::const_iterator tvit = m_tvList->constFind(ri.GetInputID()); + if (tvit == m_tvList->constEnd()) { QString msg = QString("Invalid cardid [%1] for %2") .arg(ri.GetInputID()).arg(ri.GetTitle()); @@ -2754,7 +2756,7 @@ bool Scheduler::HandleRecording( "to reschedule around its tuners.") .arg(nexttv->GetHostName())); - foreach (auto enc, *m_tvList) + for (auto * enc : qAsConst(*m_tvList)) { if (enc->GetHostName() == nexttv->GetHostName()) enc->SetSleepStatus(sStatus_Undefined); @@ -3078,8 +3080,8 @@ void Scheduler::HandleIdleShutdown( bool recording = false; m_schedLock.unlock(); TVRec::s_inputsLock.lockForRead(); - QMap<int, EncoderLink *>::Iterator it; - for (it = m_tvList->begin(); (it != m_tvList->end()) && + QMap<int, EncoderLink *>::const_iterator it; + for (it = m_tvList->constBegin(); (it != m_tvList->constEnd()) && !recording; ++it) { if ((*it)->IsBusy()) @@ -3442,7 +3444,7 @@ void Scheduler::PutInactiveSlavesToSleep(void) QReadLocker tvlocker(&TVRec::s_inputsLock); bool someSlavesCanSleep = false; - foreach (auto enc, *m_tvList) + for (auto * enc : qAsConst(*m_tvList)) { if (enc->CanSleep()) someSlavesCanSleep = true; @@ -3478,7 +3480,7 @@ void Scheduler::PutInactiveSlavesToSleep(void) if (secsleft > sleepThreshold) continue; - if (m_tvList->find(pginfo->GetInputID()) != m_tvList->end()) + if (m_tvList->constFind(pginfo->GetInputID()) != m_tvList->constEnd()) { EncoderLink *enc = (*m_tvList)[pginfo->GetInputID()]; if ((!enc->IsLocal()) && @@ -3524,7 +3526,7 @@ void Scheduler::PutInactiveSlavesToSleep(void) "be inactive for the next %1 minutes and can be put to sleep.") .arg(sleepThreshold / 60)); - foreach (auto enc, *m_tvList) + for (auto * enc : qAsConst(*m_tvList)) { if ((!enc->IsLocal()) && (enc->IsAwake()) && @@ -3548,7 +3550,7 @@ void Scheduler::PutInactiveSlavesToSleep(void) if (enc->GoToSleep()) { - foreach (auto slv, *m_tvList) + for (auto * slv : qAsConst(*m_tvList)) { if (slv->GetHostName() == thisHost) { @@ -3566,7 +3568,7 @@ void Scheduler::PutInactiveSlavesToSleep(void) LOG(VB_GENERAL, LOG_ERR, LOC + QString("Unable to shutdown %1 slave backend, setting " "sleep status to undefined.").arg(thisHost)); - foreach (auto slv, *m_tvList) + for (auto * slv : qAsConst(*m_tvList)) { if (slv->GetHostName() == thisHost) slv->SetSleepStatus(sStatus_Undefined); @@ -3596,7 +3598,7 @@ bool Scheduler::WakeUpSlave(const QString& slaveHostname, bool setWakingStatus) QString("Trying to Wake Up %1, but this slave " "does not have a WakeUpCommand set.").arg(slaveHostname)); - foreach (auto enc, *m_tvList) + for (auto * enc : qAsConst(*m_tvList)) { if (enc->GetHostName() == slaveHostname) enc->SetSleepStatus(sStatus_Undefined); @@ -3606,7 +3608,7 @@ bool Scheduler::WakeUpSlave(const QString& slaveHostname, bool setWakingStatus) } QDateTime curtime = MythDate::current(); - foreach (auto enc, *m_tvList) + for (auto * enc : qAsConst(*m_tvList)) { if (setWakingStatus && (enc->GetHostName() == slaveHostname)) enc->SetSleepStatus(sStatus_Waking); @@ -3630,7 +3632,7 @@ void Scheduler::WakeUpSlaves(void) QStringList SlavesThatCanWake; QString thisSlave; - foreach (auto enc, *m_tvList) + for (auto * enc : qAsConst(*m_tvList)) { if (enc->IsLocal()) continue; @@ -3660,7 +3662,7 @@ void Scheduler::UpdateManuals(uint recordid) query.prepare(QString("SELECT type,title,subtitle,description," "station,startdate,starttime," - "enddate,endtime,season,episode,inetref " + "enddate,endtime,season,episode,inetref,last_record " "FROM %1 WHERE recordid = :RECORDID").arg(m_recordTable)); query.bindValue(":RECORDID", recordid); if (!query.exec() || query.size() != 1) @@ -3687,6 +3689,10 @@ void Scheduler::UpdateManuals(uint recordid) int episode = query.value(10).toInt(); QString inetref = query.value(11).toString(); + // A bit of a hack: mythconverg.record.last_record can be used by + // the services API to propegate originalairdate information. + QDate originalairdate = QDate(query.value(12).toDate()); + if (description.isEmpty()) description = startdt.toLocalTime().toString(); @@ -3753,10 +3759,10 @@ void Scheduler::UpdateManuals(uint recordid) query.prepare("REPLACE INTO program (chanid, starttime, endtime," " title, subtitle, description, manualid," - " season, episode, inetref, generic) " + " season, episode, inetref, originalairdate, generic) " "VALUES (:CHANID, :STARTTIME, :ENDTIME, :TITLE," " :SUBTITLE, :DESCRIPTION, :RECORDID, " - " :SEASON, :EPISODE, :INETREF, 1)"); + " :SEASON, :EPISODE, :INETREF, :ORIGINALAIRDATE, 1)"); query.bindValue(":CHANID", id); query.bindValue(":STARTTIME", startdt); query.bindValue(":ENDTIME", startdt.addSecs(duration)); @@ -3766,6 +3772,7 @@ void Scheduler::UpdateManuals(uint recordid) query.bindValue(":SEASON", season); query.bindValue(":EPISODE", episode); query.bindValue(":INETREF", inetref); + query.bindValue(":ORIGINALAIRDATE", originalairdate); query.bindValue(":RECORDID", recordid); if (!query.exec()) { @@ -4299,7 +4306,7 @@ void Scheduler::AddNewRecords(void) RecList tmpList; QMap<int, bool> cardMap; - foreach (auto enc, *m_tvList) + for (auto * enc : qAsConst(*m_tvList)) { if (enc->IsConnected() || enc->IsAsleep()) cardMap[enc->GetInputID()] = true; @@ -4892,7 +4899,7 @@ void Scheduler::GetAllScheduled(RecList &proglist, SchedSortColumn sortBy, " channel.commmethod " // 25 "FROM record " "LEFT JOIN channel ON channel.callsign = record.station " - "WHERE deleted IS NULL " + " AND deleted IS NULL " "GROUP BY recordid " "ORDER BY %1 %2"); @@ -5433,7 +5440,7 @@ int Scheduler::FillRecordingDir( ProgramInfo *programinfo = expire; bool foundSlave = false; - foreach (auto & enc, *m_tvList) + for (auto * enc : qAsConst(*m_tvList)) { if (enc->GetHostName() == programinfo->GetHostname()) @@ -5591,7 +5598,7 @@ void Scheduler::SchedLiveTV(void) return; // Build a list of active livetv programs - foreach (auto enc, *m_tvList) + for (auto * enc : qAsConst(*m_tvList)) { if (kState_WatchingLiveTV != enc->GetState()) continue; diff --git a/mythtv/programs/mythbackend/scheduler.h b/mythtv/programs/mythbackend/scheduler.h index d0cb90cac9..bc8050c5f9 100644 --- a/mythtv/programs/mythbackend/scheduler.h +++ b/mythtv/programs/mythbackend/scheduler.h @@ -158,7 +158,8 @@ class Scheduler : public MThread, public MythScheduler bool FindNextConflict(const RecList &cardlist, const RecordingInfo *p, RecConstIter &iter, OpenEndType openEnd = openEndNever, - uint *paffinity = nullptr) const; + uint *paffinity = nullptr, + bool ignoreinput = false) const; const RecordingInfo *FindConflict(const RecordingInfo *p, OpenEndType openEnd = openEndNever, uint *affinity = nullptr, diff --git a/mythtv/programs/mythbackend/services/dvr.cpp b/mythtv/programs/mythbackend/services/dvr.cpp index ca9bf9759a..60cd91af90 100644 --- a/mythtv/programs/mythbackend/services/dvr.cpp +++ b/mythtv/programs/mythbackend/services/dvr.cpp @@ -680,7 +680,7 @@ DTC::EncoderList* Dvr::GetEncoderList() QReadLocker tvlocker(&TVRec::s_inputsLock); QList<InputInfo> inputInfoList = CardUtil::GetAllInputInfo(); - foreach (auto elink, tvList) + for (auto * elink : qAsConst(tvList)) { if (elink != nullptr) { @@ -1092,8 +1092,10 @@ uint Dvr::AddRecordSchedule ( uint nPreferredInput, int nStartOffset, int nEndOffset, + QDateTime lastrectsRaw, QString sDupMethod, QString sDupIn, + bool bNewEpisOnly, uint nFilter, QString sRecProfile, QString sRecGroup, @@ -1113,6 +1115,7 @@ uint Dvr::AddRecordSchedule ( { QDateTime recstartts = recstarttsRaw.toUTC(); QDateTime recendts = recendtsRaw.toUTC(); + QDateTime lastrects = lastrectsRaw.toUTC(); RecordingRule rule; rule.LoadTemplate("Default"); @@ -1139,8 +1142,11 @@ uint Dvr::AddRecordSchedule ( rule.m_type = recTypeFromString(sType); rule.m_searchType = searchTypeFromString(sSearchType); - rule.m_dupMethod = dupMethodFromString(sDupMethod); - rule.m_dupIn = dupInFromString(sDupIn); + if (rule.m_searchType == kManualSearch) + rule.m_dupMethod = kDupCheckNone; + else + rule.m_dupMethod = dupMethodFromString(sDupMethod); + rule.m_dupIn = dupInFromStringAndBool(sDupIn, bNewEpisOnly); if (sRecProfile.isEmpty()) sRecProfile = "Default"; @@ -1199,6 +1205,8 @@ uint Dvr::AddRecordSchedule ( rule.m_transcoder = nTranscoder; + rule.m_lastRecorded = lastrects; + QString msg; if (!rule.IsValid(msg)) throw msg; @@ -1235,6 +1243,7 @@ bool Dvr::UpdateRecordSchedule ( uint nRecordId, int nEndOffset, QString sDupMethod, QString sDupIn, + bool bNewEpisOnly, uint nFilter, QString sRecProfile, QString sRecGroup, @@ -1280,8 +1289,11 @@ bool Dvr::UpdateRecordSchedule ( uint nRecordId, pRule.m_type = recTypeFromString(sType); pRule.m_searchType = searchTypeFromString(sSearchType); - pRule.m_dupMethod = dupMethodFromString(sDupMethod); - pRule.m_dupIn = dupInFromString(sDupIn); + if (pRule.m_searchType == kManualSearch) + pRule.m_dupMethod = kDupCheckNone; + else + pRule.m_dupMethod = dupMethodFromString(sDupMethod); + pRule.m_dupIn = dupInFromStringAndBool(sDupIn, bNewEpisOnly); if (sRecProfile.isEmpty()) sRecProfile = "Default"; diff --git a/mythtv/programs/mythbackend/services/dvr.h b/mythtv/programs/mythbackend/services/dvr.h index 7a6b1be80b..3799f27488 100644 --- a/mythtv/programs/mythbackend/services/dvr.h +++ b/mythtv/programs/mythbackend/services/dvr.h @@ -173,8 +173,10 @@ class Dvr : public DvrServices uint PreferredInput, int StartOffset, int EndOffset, + QDateTime lastrectsRaw, QString DupMethod, QString DupIn, + bool NewEpisOnly, uint Filter, QString RecProfile, QString RecGroup, @@ -217,6 +219,7 @@ class Dvr : public DvrServices int EndOffset, QString DupMethod, QString DupIn, + bool NewEpisOnly, uint Filter, QString RecProfile, QString RecGroup, @@ -491,8 +494,10 @@ class ScriptableDvr : public QObject rule->Inetref(), rule->Type(), rule->SearchType(), rule->RecPriority(), rule->PreferredInput(), rule->StartOffset(), - rule->EndOffset(), rule->DupMethod(), - rule->DupIn(), rule->Filter(), + rule->EndOffset(), rule->LastRecorded(), + rule->DupMethod(), + rule->DupIn(), rule->NewEpisOnly(), + rule->Filter(), rule->RecProfile(), rule->RecGroup(), rule->StorageGroup(), rule->PlayGroup(), rule->AutoExpire(), rule->MaxEpisodes(), @@ -525,7 +530,8 @@ class ScriptableDvr : public QObject rule->SearchType(), rule->RecPriority(), rule->PreferredInput(), rule->StartOffset(), rule->EndOffset(), rule->DupMethod(), - rule->DupIn(), rule->Filter(), + rule->DupIn(), rule->NewEpisOnly(), + rule->Filter(), rule->RecProfile(), rule->RecGroup(), rule->StorageGroup(), rule->PlayGroup(), rule->AutoExpire(), rule->MaxEpisodes(), diff --git a/mythtv/programs/mythbackend/services/serviceUtil.cpp b/mythtv/programs/mythbackend/services/serviceUtil.cpp index b1d5071d6a..bfce03776d 100644 --- a/mythtv/programs/mythbackend/services/serviceUtil.cpp +++ b/mythtv/programs/mythbackend/services/serviceUtil.cpp @@ -299,6 +299,7 @@ void FillRecRuleInfo( DTC::RecRule *pRecRule, pRecRule->setEndOffset ( pRule->m_endOffset ); pRecRule->setDupMethod ( toRawString(pRule->m_dupMethod) ); pRecRule->setDupIn ( toRawString(pRule->m_dupIn) ); + pRecRule->setNewEpisOnly ( newEpifromDupIn(pRule->m_dupIn) ); pRecRule->setFilter ( pRule->m_filter ); pRecRule->setRecProfile ( pRule->m_recProfile ); pRecRule->setRecGroup ( RecordingInfo::GetRecgroupString(pRule->m_recGroupID) ); diff --git a/mythtv/programs/mythbackend/services/video.cpp b/mythtv/programs/mythbackend/services/video.cpp index 80ace22749..03eef32d94 100644 --- a/mythtv/programs/mythbackend/services/video.cpp +++ b/mythtv/programs/mythbackend/services/video.cpp @@ -44,6 +44,7 @@ #include "mythdate.h" #include "serviceUtil.h" #include "mythmiscutil.h" +#include "mythavutil.h" ///////////////////////////////////////////////////////////////////////////// // @@ -780,6 +781,150 @@ bool Video::UpdateVideoMetadata ( int nId, return true; } +///////////////////////////////////////////////////////////////////////////// +// Jun 3, 2020 +// Service to get stream info for all streams in a media file. +// This gets some basic info. If anything more is needed it can be added, +// depending on whether it is available from ffmpeg avformat apis. +// See the MythStreamInfoList class for the code that uses avformat to +// extract the information. +///////////////////////////////////////////////////////////////////////////// + +DTC::VideoStreamInfoList* Video::GetStreamInfo + ( const QString &storageGroup, + const QString &FileName ) +{ + + // Search for the filename + + StorageGroup storage( storageGroup ); + QString sFullFileName = storage.FindFile( FileName ); + MythStreamInfoList infos(sFullFileName); + + // The constructor of this class reads the file and gets the needed + // information. + auto *pVideoStreamInfos = new DTC::VideoStreamInfoList(); + + pVideoStreamInfos->setCount ( infos.m_streamInfoList.size() ); + pVideoStreamInfos->setAsOf ( MythDate::current() ); + pVideoStreamInfos->setVersion ( MYTH_BINARY_VERSION ); + pVideoStreamInfos->setProtoVer ( MYTH_PROTO_VERSION ); + pVideoStreamInfos->setErrorCode ( infos.m_errorCode ); + pVideoStreamInfos->setErrorMsg ( infos.m_errorMsg ); + + for( int n = 0; n < infos.m_streamInfoList.size() ; n++ ) + { + DTC::VideoStreamInfo *pVideoStreamInfo = pVideoStreamInfos->AddNewVideoStreamInfo(); + const MythStreamInfo &info = infos.m_streamInfoList.at(n); + pVideoStreamInfo->setCodecType ( QString(QChar(info.m_codecType)) ); + pVideoStreamInfo->setCodecName ( info.m_codecName ); + pVideoStreamInfo->setWidth ( info.m_width ); + pVideoStreamInfo->setHeight ( info.m_height ); + pVideoStreamInfo->setAspectRatio ( info.m_SampleAspectRatio ); + pVideoStreamInfo->setFieldOrder ( info.m_fieldOrder ); + pVideoStreamInfo->setFrameRate ( info.m_frameRate ); + pVideoStreamInfo->setAvgFrameRate ( info.m_avgFrameRate ); + pVideoStreamInfo->setChannels ( info.m_channels ); + pVideoStreamInfo->setDuration ( info.m_duration ); + + } + return pVideoStreamInfos; +} + +///////////////////////////////////////////////////////////////////////////// +// Get bookmark of a video as a frame number. +///////////////////////////////////////////////////////////////////////////// + +long Video::GetSavedBookmark( int Id ) +{ + MSqlQuery query(MSqlQuery::InitCon()); + + query.prepare("SELECT filename " + "FROM videometadata " + "WHERE intid = :ID "); + query.bindValue(":ID", Id); + + if (!query.exec()) + { + MythDB::DBError("Video::GetSavedBookmark", query); + return 0; + } + + QString fileName; + + if (query.next()) + fileName = query.value(0).toString(); + else + { + LOG(VB_GENERAL, LOG_ERR, QString("Video/GetSavedBookmark Video id %1 Not found.").arg(Id)); + return -1; + } + + ProgramInfo pi(fileName, + nullptr, // _plot, + nullptr, // _title, + nullptr, // const QString &_sortTitle, + nullptr, // const QString &_subtitle, + nullptr, // const QString &_sortSubtitle, + nullptr, // const QString &_director, + 0, // int _season, + 0, // int _episode, + nullptr, // const QString &_inetref, + 0, // uint _length_in_minutes, + 0, // uint _year, + nullptr); //const QString &_programid); + + long ret = pi.QueryBookmark(); + return ret; +} + +///////////////////////////////////////////////////////////////////////////// +// Set bookmark of a video as a frame number. +///////////////////////////////////////////////////////////////////////////// + +bool Video::SetSavedBookmark( int Id, long Offset ) +{ + MSqlQuery query(MSqlQuery::InitCon()); + + query.prepare("SELECT filename " + "FROM videometadata " + "WHERE intid = :ID "); + query.bindValue(":ID", Id); + + if (!query.exec()) + { + MythDB::DBError("Video::SetSavedBookmark", query); + return false; + } + + QString fileName; + + if (query.next()) + fileName = query.value(0).toString(); + else + { + LOG(VB_GENERAL, LOG_ERR, QString("Video/SetSavedBookmark Video id %1 Not found.").arg(Id)); + return false; + } + + ProgramInfo pi(fileName, + nullptr, // _plot, + nullptr, // _title, + nullptr, // const QString &_sortTitle, + nullptr, // const QString &_subtitle, + nullptr, // const QString &_sortSubtitle, + nullptr, // const QString &_director, + 0, // int _season, + 0, // int _episode, + nullptr, // const QString &_inetref, + 0, // uint _length_in_minutes, + 0, // uint _year, + nullptr); //const QString &_programid); + + pi.SaveBookmark(Offset); + return true; +} + ///////////////////////////////////////////////////////////////////////////// // ///////////////////////////////////////////////////////////////////////////// diff --git a/mythtv/programs/mythbackend/services/video.h b/mythtv/programs/mythbackend/services/video.h index 93469c246b..fdafadf178 100644 --- a/mythtv/programs/mythbackend/services/video.h +++ b/mythtv/programs/mythbackend/services/video.h @@ -110,10 +110,17 @@ class Video : public VideoServices const QString &Countries ) override; // VideoServices + long GetSavedBookmark ( int Id ) override; + + bool SetSavedBookmark ( int Id, + long Offset ) override; + /* Bluray Methods */ DTC::BlurayInfo* GetBluray ( const QString &Path ) override; // VideoServices + DTC::VideoStreamInfoList* GetStreamInfo ( const QString &StorageGroup, + const QString &FileName ) override; // VideoServices }; // -------------------------------------------------------------------------- diff --git a/mythtv/programs/mythcommflag/main.cpp b/mythtv/programs/mythcommflag/main.cpp index a9575bf18f..f6e60ab841 100644 --- a/mythtv/programs/mythcommflag/main.cpp +++ b/mythtv/programs/mythcommflag/main.cpp @@ -1,3 +1,10 @@ + +#if defined ANDROID && __ANDROID_API__ < 24 +// ftello and fseeko do not exist in android before api level 24 +#define ftello ftell +#define fseeko fseek +#endif + // POSIX headers #include <unistd.h> #include <sys/time.h> // for gettimeofday diff --git a/mythtv/programs/mythexternrecorder/MythExternControl.cpp b/mythtv/programs/mythexternrecorder/MythExternControl.cpp index 3038a01dc2..a29efa17a9 100644 --- a/mythtv/programs/mythexternrecorder/MythExternControl.cpp +++ b/mythtv/programs/mythexternrecorder/MythExternControl.cpp @@ -173,6 +173,11 @@ void Commands::TuneChannel(const QString & serial, const QString & channum) emit m_parent->TuneChannel(serial, channum); } +void Commands::TuneStatus(const QString & serial) +{ + emit m_parent->TuneStatus(serial); +} + void Commands::LoadChannels(const QString & serial) { emit m_parent->LoadChannels(serial); @@ -188,6 +193,11 @@ void Commands::NextChannel(const QString & serial) emit m_parent->NextChannel(serial); } +void Commands::Cleanup(void) +{ + emit m_parent->Cleanup(); +} + bool Commands::SendStatus(const QString & command, const QString & status) { int len = write(2, status.toUtf8().constData(), status.size()); @@ -309,7 +319,7 @@ bool Commands::ProcessCommand(const QString & cmd) else SendStatus(cmd, tokens[0], "OK:20"); } - else if (tokens[1].startsWith("LockTimeout")) + else if (tokens[1].startsWith("LockTimeout?")) { LockTimeout(tokens[0]); } @@ -352,11 +362,15 @@ bool Commands::ProcessCommand(const QString & cmd) } else if (tokens[1].startsWith("TuneChannel")) { - if (tokens.size() > 1) + if (tokens.size() > 2) TuneChannel(tokens[0], tokens[2]); else SendStatus(cmd, tokens[0], "ERR:Missing channum"); } + else if (tokens[1].startsWith("TuneStatus?")) + { + TuneStatus(tokens[0]); + } else if (tokens[1].startsWith("LoadChannels")) { LoadChannels(tokens[0]); @@ -385,6 +399,7 @@ bool Commands::ProcessCommand(const QString & cmd) StopStreaming(tokens[0], true); m_parent->Terminate(); SendStatus(cmd, tokens[0], "OK:Terminating"); + Cleanup(); } else if (tokens[1].startsWith("FlowControl?")) { @@ -498,6 +513,13 @@ bool Buffer::Fill(const QByteArray & buffer) static int s_droppedBytes = 0; m_parent->m_flow_mutex.lock(); + + if (!m_dataSeen) + { + m_dataSeen = true; + emit m_parent->DataStarted(); + } + if (m_data.size() < MAX_QUEUE) { block_t blk(reinterpret_cast<const uint8_t *>(buffer.constData()), diff --git a/mythtv/programs/mythexternrecorder/MythExternControl.h b/mythtv/programs/mythexternrecorder/MythExternControl.h index c510ea1dfd..57d26465fc 100644 --- a/mythtv/programs/mythexternrecorder/MythExternControl.h +++ b/mythtv/programs/mythexternrecorder/MythExternControl.h @@ -66,6 +66,7 @@ class Buffer : QObject std::thread m_thread; stack_t m_data; + bool m_dataSeen {false}; std::chrono::time_point<std::chrono::system_clock> m_heartbeat; }; @@ -103,9 +104,11 @@ class Commands : public QObject void HasPictureAttributes(const QString & serial) const; void SetBlockSize(const QString & serial, int blksz); void TuneChannel(const QString & serial, const QString & channum); + void TuneStatus(const QString & serial); void LoadChannels(const QString & serial); void FirstChannel(const QString & serial); void NextChannel(const QString & serial); + void Cleanup(void); private: std::thread m_thread; @@ -145,9 +148,12 @@ class MythExternControl : public QObject void HasPictureAttributes(const QString & serial) const; void SetBlockSize(const QString & serial, int blksz); void TuneChannel(const QString & serial, const QString & channum); + void TuneStatus(const QString & serial); void LoadChannels(const QString & serial); void FirstChannel(const QString & serial); void NextChannel(const QString & serial); + void Cleanup(void); + void DataStarted(void); public slots: void SetDescription(const QString & desc) { m_desc = desc; } diff --git a/mythtv/programs/mythexternrecorder/MythExternRecApp.cpp b/mythtv/programs/mythexternrecorder/MythExternRecApp.cpp index a2597d2ea5..624b30c98a 100644 --- a/mythtv/programs/mythexternrecorder/MythExternRecApp.cpp +++ b/mythtv/programs/mythexternrecorder/MythExternRecApp.cpp @@ -27,6 +27,7 @@ #include <QFileInfo> #include <QProcess> #include <QtCore/QtCore> +#include <unistd.h> #define LOC Desc() @@ -42,8 +43,7 @@ MythExternRecApp::MythExternRecApp(QString command, if (m_configIni.isEmpty() || !config()) m_recDesc = m_recCommand; - if (m_tuneCommand.isEmpty()) - m_command = m_recCommand; + m_command = m_recCommand; LOG(VB_CHANNEL, LOG_INFO, LOC + QString("Channels in '%1', Tuner: '%2', Scanner: '%3'") @@ -85,7 +85,10 @@ bool MythExternRecApp::config(void) m_recCommand = settings.value("RECORDER/command").toString(); m_recDesc = settings.value("RECORDER/desc").toString(); + m_cleanup = settings.value("RECORDER/cleanup").toString(); m_tuneCommand = settings.value("TUNER/command", "").toString(); + m_newEpisodeCommand = settings.value("TUNER/newepisodecommand", "").toString(); + m_onDataStart = settings.value("TUNER/ondatastart", "").toString(); m_channelsIni = settings.value("TUNER/channels", "").toString(); m_lockTimeout = settings.value("TUNER/timeout", "").toInt(); m_scanCommand = settings.value("SCANNER/command", "").toString(); @@ -177,29 +180,31 @@ bool MythExternRecApp::Open(void) return true; } -void MythExternRecApp::TerminateProcess(void) +void MythExternRecApp::TerminateProcess(QProcess & proc, const QString & desc) { - if (m_proc.state() == QProcess::Running) + if (proc.state() == QProcess::Running) { LOG(VB_RECORD, LOG_INFO, LOC + - QString("Sending SIGINT to %1").arg(m_proc.pid())); - kill(m_proc.pid(), SIGINT); - m_proc.waitForFinished(5000); + QString("Sending SIGINT to %1(%2)").arg(desc).arg(proc.pid())); + kill(proc.pid(), SIGINT); + proc.waitForFinished(5000); } - if (m_proc.state() == QProcess::Running) + if (proc.state() == QProcess::Running) { LOG(VB_RECORD, LOG_INFO, LOC + - QString("Sending SIGTERM to %1").arg(m_proc.pid())); - m_proc.terminate(); - m_proc.waitForFinished(); + QString("Sending SIGTERM to %1(%2)").arg(desc).arg(proc.pid())); + proc.terminate(); + proc.waitForFinished(); } - if (m_proc.state() == QProcess::Running) + if (proc.state() == QProcess::Running) { LOG(VB_RECORD, LOG_INFO, LOC + - QString("Sending SIGKILL to %1").arg(m_proc.pid())); - m_proc.kill(); - m_proc.waitForFinished(); + QString("Sending SIGKILL to %1(%2)").arg(desc).arg(proc.pid())); + proc.kill(); + proc.waitForFinished(); } + + return; } Q_SLOT void MythExternRecApp::Close(void) @@ -212,10 +217,16 @@ Q_SLOT void MythExternRecApp::Close(void) std::this_thread::sleep_for(std::chrono::microseconds(50)); } + if (m_tuneProc.state() == QProcess::Running) + { + m_tuneProc.closeReadChannel(QProcess::StandardOutput); + TerminateProcess(m_tuneProc, "App"); + } + if (m_proc.state() == QProcess::Running) { m_proc.closeReadChannel(QProcess::StandardOutput); - TerminateProcess(); + TerminateProcess(m_proc, "App"); std::this_thread::sleep_for(std::chrono::microseconds(50)); } @@ -249,12 +260,77 @@ void MythExternRecApp::Run(void) if (m_proc.state() == QProcess::Running) { m_proc.closeReadChannel(QProcess::StandardOutput); - TerminateProcess(); + TerminateProcess(m_proc, "App"); } emit Done(); } +Q_SLOT void MythExternRecApp::Cleanup(void) +{ + m_tunedChannel.clear(); + + if (m_cleanup.isEmpty()) + return; + + QString cmd = m_cleanup; + + LOG(VB_RECORD, LOG_WARNING, LOC + + QString(" Beginning cleanup: '%1'").arg(cmd)); + + QProcess cleanup; + cleanup.start(cmd); + if (!cleanup.waitForStarted()) + { + LOG(VB_RECORD, LOG_ERR, LOC + ": Failed to start cleanup process: " + + ENO); + return; + } + cleanup.waitForFinished(5000); + if (cleanup.state() == QProcess::NotRunning) + { + if (cleanup.exitStatus() != QProcess::NormalExit) + { + LOG(VB_RECORD, LOG_ERR, LOC + ": Cleanup process failed: " + ENO); + return; + } + } + + LOG(VB_RECORD, LOG_INFO, LOC + ": Cleanup finished."); +} + +Q_SLOT void MythExternRecApp::DataStarted(void) +{ + if (m_onDataStart.isEmpty()) + return; + + QString cmd = m_onDataStart; + cmd.replace("%CHANNUM%", m_tunedChannel); + + LOG(VB_RECORD, LOG_INFO, LOC + + QString(" Data started, finishing tune: '%1'").arg(cmd)); + + QProcess finish; + finish.start(cmd); + if (!finish.waitForStarted()) + { + LOG(VB_RECORD, LOG_ERR, LOC + ": Failed to finish tune process: " + + ENO); + return; + } + finish.waitForFinished(5000); + if (finish.state() == QProcess::NotRunning) + { + if (finish.exitStatus() != QProcess::NormalExit) + { + LOG(VB_RECORD, LOG_ERR, LOC + ": Finish tune failed: " + ENO); + return; + } + } + + LOG(VB_RECORD, LOG_INFO, LOC + ": tunning finished."); +} + Q_SLOT void MythExternRecApp::LoadChannels(const QString & serial) { if (m_channelsIni.isEmpty()) @@ -354,15 +430,17 @@ void MythExternRecApp::GetChannel(const QString & serial, const QString & func) QString name = m_chanSettings->value("NAME").toString(); QString callsign = m_chanSettings->value("CALLSIGN").toString(); QString xmltvid = m_chanSettings->value("XMLTVID").toString(); + QString icon = m_chanSettings->value("ICON").toString(); m_chanSettings->endGroup(); LOG(VB_CHANNEL, LOG_INFO, LOC + - QString(": NextChannel Name:'%1',Callsign:'%2',xmltvid:%3") - .arg(name).arg(callsign).arg(xmltvid)); + QString(": NextChannel Name:'%1',Callsign:'%2',xmltvid:%3,Icon:%4") + .arg(name).arg(callsign).arg(xmltvid).arg(icon)); - emit SendMessage(func, serial, QString("OK:%1,%2,%3,%4") - .arg(channum).arg(name).arg(callsign).arg(xmltvid)); + emit SendMessage(func, serial, QString("OK:%1,%2,%3,%4,%5") + .arg(channum).arg(name).arg(callsign) + .arg(xmltvid).arg(icon)); } Q_SLOT void MythExternRecApp::FirstChannel(const QString & serial) @@ -376,60 +454,101 @@ Q_SLOT void MythExternRecApp::NextChannel(const QString & serial) GetChannel(serial, "NextChannel"); } -Q_SLOT void MythExternRecApp::TuneChannel(const QString & serial, - const QString & channum) +void MythExternRecApp::NewEpisodeStarting(const QString & channum) { - if (m_channelsIni.isEmpty()) + QString cmd = m_newEpisodeCommand; + cmd.replace("%CHANNUM%", channum); + + LOG(VB_RECORD, LOG_WARNING, LOC + + QString(" New episode starting on current channel: '%1'").arg(cmd)); + + QProcess proc; + proc.start(cmd); + if (!proc.waitForStarted()) { - LOG(VB_CHANNEL, LOG_ERR, LOC + ": No channels configured."); - emit SendMessage("TuneChannel", serial, "ERR:No channels configured."); + LOG(VB_RECORD, LOG_ERR, LOC + + " NewEpisodeStarting: Failed to start process: " + ENO); return; } - - QSettings settings(m_channelsIni, QSettings::IniFormat); - settings.beginGroup(channum); - - QString url(settings.value("URL").toString()); - - if (url.isEmpty()) + proc.waitForFinished(5000); + if (proc.state() == QProcess::NotRunning) { - QString msg = QString("Channel number [%1] is missing a URL.") - .arg(channum); + if (proc.exitStatus() != QProcess::NormalExit) + { + LOG(VB_RECORD, LOG_ERR, LOC + + " NewEpisodeStarting: process failed: " + ENO); + return; + } + } - LOG(VB_CHANNEL, LOG_ERR, LOC + ": " + msg); + LOG(VB_RECORD, LOG_INFO, LOC + "NewEpisodeStarting: finished."); +} - emit SendMessage("TuneChannel", serial, QString("ERR:%1").arg(msg)); +Q_SLOT void MythExternRecApp::TuneChannel(const QString & serial, + const QString & channum) +{ + if (m_tuneCommand.isEmpty() && m_channelsIni.isEmpty()) + { + LOG(VB_CHANNEL, LOG_ERR, LOC + ": No 'tuner' configured."); + emit SendMessage("TuneChannel", serial, "ERR:No 'tuner' configured."); return; } - if (!m_tuneCommand.isEmpty()) + if (m_tunedChannel == channum) { - // Repalce URL in command and execute it - QString tune = m_tuneCommand; - tune.replace("%URL%", url); + if (!m_newEpisodeCommand.isEmpty()) + NewEpisodeStarting(channum); - if (system(tune.toUtf8().constData()) != 0) - { - QString errmsg = QString("'%1' failed: ").arg(tune) + ENO; - LOG(VB_CHANNEL, LOG_ERR, LOC + ": " + errmsg); - emit SendMessage("TuneChannel", serial, QString("ERR:%1").arg(errmsg)); - return; - } LOG(VB_CHANNEL, LOG_INFO, LOC + - QString(": TuneChannel, ran '%1'").arg(tune)); + QString("TuneChanne: Already on %1").arg(channum)); + emit SendMessage("TuneChannel", serial, + QString("OK:Tunned to %1").arg(channum)); + return; } - // Replace URL in recorder command + m_desc = m_recDesc; m_command = m_recCommand; - if (!url.isEmpty() && m_command.indexOf("%URL%") >= 0) + QString tunecmd = m_tuneCommand; + QString url; + + if (!m_channelsIni.isEmpty()) { - m_command.replace("%URL%", url); - LOG(VB_CHANNEL, LOG_DEBUG, LOC + - QString(": '%URL%' replaced with '%1' in cmd: '%2'") - .arg(url).arg(m_command)); + QSettings settings(m_channelsIni, QSettings::IniFormat); + settings.beginGroup(channum); + + url = settings.value("URL").toString(); + + if (url.isEmpty()) + { + QString msg = QString("Channel number [%1] is missing a URL.") + .arg(channum); + + LOG(VB_CHANNEL, LOG_ERR, LOC + ": " + msg); + } + else + tunecmd.replace("%URL%", url); + + if (!url.isEmpty() && m_command.indexOf("%URL%") >= 0) + { + m_command.replace("%URL%", url); + LOG(VB_CHANNEL, LOG_DEBUG, LOC + + QString(": '%URL%' replaced with '%1' in cmd: '%2'") + .arg(url).arg(m_command)); + } + + m_desc.replace("%CHANNAME%", settings.value("NAME").toString()); + m_desc.replace("%CALLSIGN%", settings.value("CALLSIGN").toString()); + + settings.endGroup(); } + if (m_tuneProc.state() == QProcess::Running) + TerminateProcess(m_tuneProc, "Tune"); + + tunecmd.replace("%CHANNUM%", channum); + m_command.replace("%CHANNUM%", channum); + if (!m_logFile.isEmpty() && m_command.indexOf("%LOGFILE%") >= 0) { m_command.replace("%LOGFILE%", m_logFile); @@ -446,31 +565,86 @@ Q_SLOT void MythExternRecApp::TuneChannel(const QString & serial, .arg(m_logging).arg(m_command)); } - m_desc = m_recDesc; m_desc.replace("%URL%", url); m_desc.replace("%CHANNUM%", channum); - m_desc.replace("%CHANNAME%", settings.value("NAME").toString()); - m_desc.replace("%CALLSIGN%", settings.value("CALLSIGN").toString()); - settings.endGroup(); + if (!m_tuneCommand.isEmpty()) + { + m_tuningChannel = channum; + m_tuneProc.start(tunecmd); + if (!m_tuneProc.waitForStarted()) + { + QString errmsg = QString("Tune `%1` failed: ").arg(tunecmd) + ENO; + LOG(VB_CHANNEL, LOG_ERR, LOC + ": " + errmsg); + emit SendMessage("TuneChannel", serial, + QString("ERR:%1").arg(errmsg)); + return; + } + + LOG(VB_CHANNEL, LOG_INFO, LOC + QString(": Started `%1` URL '%2'") + .arg(tunecmd).arg(url)); + emit SendMessage("TuneChannel", serial, + QString("OK:InProgress `%1`").arg(tunecmd)); + } + else + { + m_tunedChannel = channum; + emit SetDescription(Desc()); + emit SendMessage("TuneChannel", serial, + QString("OK:Tuned to %1").arg(m_tunedChannel)); + } +} - LOG(VB_CHANNEL, LOG_INFO, LOC + - QString(": TuneChannel %1: URL '%2'").arg(channum).arg(url)); - m_tuned = true; +Q_SLOT void MythExternRecApp::TuneStatus(const QString & serial) +{ + if (m_tuneProc.state() == QProcess::Running) + { + LOG(VB_CHANNEL, LOG_INFO, LOC + + QString(": Tune process(%1) still running").arg(m_tuneProc.pid())); + emit SendMessage("TuneStatus", serial, "OK:InProgress"); + return; + } + + if (!m_tuneCommand.isEmpty() && + m_tuneProc.exitStatus() != QProcess::NormalExit) + { + QString errmsg = QString("'%1' failed: ") + .arg(m_tuneProc.program()) + ENO; + LOG(VB_CHANNEL, LOG_ERR, LOC + ": " + errmsg); + emit SendMessage("TuneStatus", serial, + QString("ERR:%1").arg(errmsg)); + return; + } + + m_tunedChannel = m_tuningChannel; + m_tuningChannel.clear(); + LOG(VB_CHANNEL, LOG_INFO, LOC + QString(": Tuned %1").arg(m_tunedChannel)); emit SetDescription(Desc()); emit SendMessage("TuneChannel", serial, - QString("OK:Tunned to %1").arg(channum)); + QString("OK:Tuned to %1").arg(m_tunedChannel)); } Q_SLOT void MythExternRecApp::LockTimeout(const QString & serial) { if (!Open()) + { + LOG(VB_CHANNEL, LOG_WARNING, LOC + + "Cannot read LockTimeout from config file."); + emit SendMessage("LockTimeout", serial, "ERR: Not open"); return; + } if (m_lockTimeout > 0) + { + LOG(VB_CHANNEL, LOG_INFO, LOC + + QString("Using configured LockTimeout of %1").arg(m_lockTimeout)); emit SendMessage("LockTimeout", serial, QString("OK:%1").arg(m_lockTimeout)); + return; + } + LOG(VB_CHANNEL, LOG_INFO, LOC + + "No LockTimeout defined in config, defaulting to 12000ms"); emit SendMessage("LockTimeout", serial, QString("OK:%1") .arg(m_scanCommand.isEmpty() ? 12000 : 120000)); } @@ -478,7 +652,8 @@ Q_SLOT void MythExternRecApp::LockTimeout(const QString & serial) Q_SLOT void MythExternRecApp::HasTuner(const QString & serial) { emit SendMessage("HasTuner", serial, QString("OK:%1") - .arg(m_channelsIni.isEmpty() ? "No" : "Yes")); + .arg(m_tuneCommand.isEmpty() && + m_channelsIni.isEmpty() ? "No" : "Yes")); } Q_SLOT void MythExternRecApp::HasPictureAttributes(const QString & serial) @@ -495,7 +670,7 @@ Q_SLOT void MythExternRecApp::SetBlockSize(const QString & serial, int blksz) Q_SLOT void MythExternRecApp::StartStreaming(const QString & serial) { m_streaming = true; - if (!m_tuned && !m_channelsIni.isEmpty()) + if (m_tunedChannel.isEmpty() && !m_channelsIni.isEmpty()) { LOG(VB_RECORD, LOG_ERR, LOC + ": No channel has been tuned"); emit SendMessage("StartStreaming", serial, @@ -549,7 +724,7 @@ Q_SLOT void MythExternRecApp::StopStreaming(const QString & serial, bool silent) m_streaming = false; if (m_proc.state() == QProcess::Running) { - TerminateProcess(); + TerminateProcess(m_proc, "App"); LOG(VB_RECORD, LOG_INFO, LOC + ": External application terminated."); if (silent) diff --git a/mythtv/programs/mythexternrecorder/MythExternRecApp.h b/mythtv/programs/mythexternrecorder/MythExternRecApp.h index d09cffb4ce..5d105691e2 100644 --- a/mythtv/programs/mythexternrecorder/MythExternRecApp.h +++ b/mythtv/programs/mythexternrecorder/MythExternRecApp.h @@ -69,17 +69,21 @@ class MythExternRecApp : public QObject void StopStreaming(const QString & serial, bool silent); void LockTimeout(const QString & serial); void HasTuner(const QString & serial); + void Cleanup(void); + void DataStarted(void); void LoadChannels(const QString & serial); void FirstChannel(const QString & serial); void NextChannel(const QString & serial); + void NewEpisodeStarting(const QString & channum); void TuneChannel(const QString & serial, const QString & channum); + void TuneStatus(const QString & serial); void HasPictureAttributes(const QString & serial); void SetBlockSize(const QString & serial, int blksz); protected: void GetChannel(const QString & serial, const QString & func); - void TerminateProcess(void); + void TerminateProcess(QProcess & proc, const QString & desc); private: bool config(void); @@ -97,13 +101,17 @@ class MythExternRecApp : public QObject QProcess m_proc; QString m_command; + QString m_cleanup; QString m_recCommand; QString m_recDesc; QMap<QString, QString> m_appEnv; + QProcess m_tuneProc; QString m_tuneCommand; + QString m_onDataStart; + QString m_newEpisodeCommand; QString m_channelsIni; uint m_lockTimeout { 0 }; @@ -115,7 +123,8 @@ class MythExternRecApp : public QObject QString m_configIni; QString m_desc; - bool m_tuned { false }; + QString m_tuningChannel; + QString m_tunedChannel; // Channel scanning QSettings *m_chanSettings { nullptr }; diff --git a/mythtv/programs/mythexternrecorder/commandlineparser.cpp b/mythtv/programs/mythexternrecorder/commandlineparser.cpp index 31c118385e..f7ca200703 100644 --- a/mythtv/programs/mythexternrecorder/commandlineparser.cpp +++ b/mythtv/programs/mythexternrecorder/commandlineparser.cpp @@ -9,7 +9,7 @@ MythExternRecorderCommandLineParser::MythExternRecorderCommandLineParser() : QString MythExternRecorderCommandLineParser::GetHelpHeader(void) const { - return "MythFileRecorder is a go-between app which interfaces " + return "mythexternrecorder is a go-between app which interfaces " "between a recording device and mythbackend."; } diff --git a/mythtv/programs/mythexternrecorder/main.cpp b/mythtv/programs/mythexternrecorder/main.cpp index 71ca26079f..7bbff574f2 100644 --- a/mythtv/programs/mythexternrecorder/main.cpp +++ b/mythtv/programs/mythexternrecorder/main.cpp @@ -112,6 +112,10 @@ int main(int argc, char *argv[]) process, &MythExternRecApp::LockTimeout); QObject::connect(control, &MythExternControl::HasTuner, process, &MythExternRecApp::HasTuner); + QObject::connect(control, &MythExternControl::Cleanup, + process, &MythExternRecApp::Cleanup); + QObject::connect(control, &MythExternControl::DataStarted, + process, &MythExternRecApp::DataStarted); QObject::connect(control, &MythExternControl::LoadChannels, process, &MythExternRecApp::LoadChannels); QObject::connect(control, &MythExternControl::FirstChannel, @@ -120,6 +124,8 @@ int main(int argc, char *argv[]) process, &MythExternRecApp::NextChannel); QObject::connect(control, &MythExternControl::TuneChannel, process, &MythExternRecApp::TuneChannel); + QObject::connect(control, &MythExternControl::TuneStatus, + process, &MythExternRecApp::TuneStatus); QObject::connect(control, &MythExternControl::HasPictureAttributes, process, &MythExternRecApp::HasPictureAttributes); QObject::connect(control, &MythExternControl::SetBlockSize, diff --git a/mythtv/programs/mythfilldatabase/channeldata.cpp b/mythtv/programs/mythfilldatabase/channeldata.cpp index 5b2c581142..caafde56b1 100644 --- a/mythtv/programs/mythfilldatabase/channeldata.cpp +++ b/mythtv/programs/mythfilldatabase/channeldata.cpp @@ -264,7 +264,7 @@ void ChannelData::handleChannels(int id, ChannelInfoList *chanlist) ChannelInfo dbChan = FindMatchingChannel(*i, existingChannels); if (dbChan.m_chanId > 0) // Channel exists, updating { - LOG(VB_XMLTV, LOG_NOTICE, + LOG(VB_XMLTV, LOG_DEBUG, QString("Match found for xmltvid %1 to channel %2 (%3)") .arg((*i).m_xmltvId).arg(dbChan.m_name).arg(dbChan.m_chanId)); if (m_interactive) diff --git a/mythtv/programs/mythfilldatabase/commandlineparser.cpp b/mythtv/programs/mythfilldatabase/commandlineparser.cpp index 787e46b5f0..b3b9f7e38d 100644 --- a/mythtv/programs/mythfilldatabase/commandlineparser.cpp +++ b/mythtv/programs/mythfilldatabase/commandlineparser.cpp @@ -164,4 +164,8 @@ void MythFillDatabaseCommandLineParser::LoadArguments(void) add("--mark-repeats", "oldmarkrepeats", "", "", "") ->SetRemoved("This is now the default behavior. Use\n" " --no-mark-repeats to disable.", "0.25"); + add("--dd-grab-all", "ddgraball", false, "", "") + ->SetDeprecated("It's no longer valid with Schedules Direct XMLTV.\n" + " Remove in mythtv-setup General -> Program Schedule\n" + " -> Downloading Options -> Guide Data Arguements"); } diff --git a/mythtv/programs/mythfilldatabase/filldata.cpp b/mythtv/programs/mythfilldatabase/filldata.cpp index 59137f2642..e3c97bd801 100644 --- a/mythtv/programs/mythfilldatabase/filldata.cpp +++ b/mythtv/programs/mythfilldatabase/filldata.cpp @@ -92,7 +92,6 @@ bool FillData::GrabDataFromFile(int id, QString &filename) ChannelInfoList chanlist; QMap<QString, QList<ProgInfo> > proglist; - m_xmltvParser.lateInit(); if (!m_xmltvParser.parseFile(filename, &chanlist, &proglist)) return false; @@ -179,9 +178,20 @@ bool FillData::GrabData(const Source& source, int offset) LOG(VB_XMLTV, LOG_INFO, "----------------- Start of XMLTV output -----------------"); - uint systemcall_status = myth_system(command, kMSRunShell); + MythSystemLegacy run_grabber(command, kMSRunShell | kMSStdErr); + + run_grabber.Run(); + uint systemcall_status = run_grabber.Wait(); bool succeeded = (systemcall_status == GENERIC_EXIT_OK); + QByteArray result = run_grabber.ReadAllErr(); + QTextStream ostream(result); + while (!ostream.atEnd()) + { + QString line = ostream.readLine().simplified(); + LOG(VB_XMLTV, LOG_INFO, line); + } + LOG(VB_XMLTV, LOG_INFO, "------------------ End of XMLTV output ------------------"); @@ -195,12 +205,14 @@ bool FillData::GrabData(const Source& source, int offset) { m_interrupted = true; status = QObject::tr("FAILED: XMLTV grabber ran but was interrupted."); + LOG(VB_GENERAL, LOG_ERR, + QString("XMLTV grabber ran but was interrupted.")); } else { status = QObject::tr("FAILED: XMLTV grabber returned error code %1.") .arg(systemcall_status); - LOG(VB_GENERAL, LOG_ERR, LOC + + LOG(VB_GENERAL, LOG_ERR, QString("XMLTV grabber returned error code %1") .arg(systemcall_status)); } diff --git a/mythtv/programs/mythfilldatabase/main.cpp b/mythtv/programs/mythfilldatabase/main.cpp index 3f764b16fb..b197347128 100644 --- a/mythtv/programs/mythfilldatabase/main.cpp +++ b/mythtv/programs/mythfilldatabase/main.cpp @@ -82,6 +82,10 @@ int main(int argc, char *argv[]) if (retval != GENERIC_EXIT_OK) return retval; + if (cmdline.toBool("ddgraball")) + LOG(VB_GENERAL, LOG_WARNING, + "Invalid option, see: mythfilldatabase --help dd-grab-all"); + if (cmdline.toBool("manual")) { cout << "###\n"; @@ -660,9 +664,8 @@ int main(int argc, char *argv[]) "| the master backend is restarted. |\n" "==============================================================="); - if (mark_repeats) - ScheduledRecording::RescheduleMatch(0, 0, 0, QDateTime(), - "MythFillDatabase"); + ScheduledRecording::RescheduleMatch(0, 0, 0, QDateTime(), + "MythFillDatabase"); gCoreContext->SendMessage("CLEAR_SETTINGS_CACHE"); diff --git a/mythtv/programs/mythfilldatabase/xmltvparser.cpp b/mythtv/programs/mythfilldatabase/xmltvparser.cpp index b8cdff0131..0024819bc3 100644 --- a/mythtv/programs/mythfilldatabase/xmltvparser.cpp +++ b/mythtv/programs/mythfilldatabase/xmltvparser.cpp @@ -34,12 +34,6 @@ XMLTVParser::XMLTVParser() m_currentYear = MythDate::current().date().toString("yyyy").toUInt(); } -void XMLTVParser::lateInit() -{ - m_movieGrabberPath = MetadataDownload::GetMovieGrabber(); - m_tvGrabberPath = MetadataDownload::GetTelevisionGrabber(); -} - static uint ELFHash(const QByteArray &ba) { const auto *k = (const uchar *)ba.data(); @@ -60,74 +54,6 @@ static uint ELFHash(const QByteArray &ba) return h; } -static QString getFirstText(const QDomElement& element) -{ - for (QDomNode dname = element.firstChild(); !dname.isNull(); - dname = dname.nextSibling()) - { - QDomText t = dname.toText(); - if (!t.isNull()) - return t.data(); - } - return QString(); -} - -ChannelInfo *XMLTVParser::parseChannel(QDomElement &element, QUrl &baseUrl) -{ - auto *chaninfo = new ChannelInfo; - - QString xmltvid = element.attribute("id", ""); - - chaninfo->m_xmltvId = xmltvid; - chaninfo->m_tvFormat = "Default"; - - for (QDomNode child = element.firstChild(); !child.isNull(); - child = child.nextSibling()) - { - QDomElement info = child.toElement(); - if (!info.isNull()) - { - if (info.tagName() == "icon") - { - if (chaninfo->m_icon.isEmpty()) - { - QString path = info.attribute("src", ""); - if (!path.isEmpty() && !path.contains("://")) - { - QString base = baseUrl.toString(QUrl::StripTrailingSlash); - chaninfo->m_icon = base + - ((path.startsWith("/")) ? path : QString("/") + path); - } - else if (!path.isEmpty()) - { - QUrl url(path); - if (url.isValid()) - chaninfo->m_icon = url.toString(); - } - } - } - else if (info.tagName() == "display-name") - { - if (chaninfo->m_name.isEmpty()) - { - chaninfo->m_name = info.text(); - } - else if (chaninfo->m_callSign.isEmpty()) - { - chaninfo->m_callSign = info.text(); - } - else if (chaninfo->m_chanNum.isEmpty()) - { - chaninfo->m_chanNum = info.text(); - } - } - } - } - - chaninfo->m_freqId = chaninfo->m_chanNum; - return chaninfo; -} - static void fromXMLTVDate(QString ×tr, QDateTime &dt) { // The XMLTV spec requires dates to either be in UTC/GMT or to specify a @@ -223,12 +149,12 @@ static void fromXMLTVDate(QString ×tr, QDateTime &dt) QDateTime tmpDT = QDateTime(tmpDate, tmpTime, Qt::UTC); if (!tmpDT.isValid()) - { - LOG(VB_XMLTV, LOG_ERR, - QString("Invalid datetime (combination of date/time) " + { + LOG(VB_XMLTV, LOG_ERR, + QString("Invalid datetime (combination of date/time) " "in XMLTV data, ignoring: %1").arg(timestr)); - return; - } + return; + } // While this seems like a hack, it's better than what was done before QString isoDateString = tmpDT.toString(Qt::ISODate); @@ -248,485 +174,536 @@ static void fromXMLTVDate(QString ×tr, QDateTime &dt) timestr = MythDate::toString(dt, MythDate::kFilename); } -static void parseCredits(QDomElement &element, ProgInfo *pginfo) +static int readNextWithErrorCheck(QXmlStreamReader &xml) { - for (QDomNode child = element.firstChild(); !child.isNull(); - child = child.nextSibling()) + xml.readNext(); + if (xml.hasError()) { - QDomElement info = child.toElement(); - if (!info.isNull()) - pginfo->AddPerson(info.tagName(), getFirstText(info)); + LOG(VB_GENERAL, LOG_ERR, QString("Malformed XML file at line %1, %2").arg(xml.lineNumber()).arg(xml.errorString())); + return false; } + return true; } -static void parseVideo(QDomElement &element, ProgInfo *pginfo) +bool XMLTVParser::parseFile( + const QString& filename, ChannelInfoList *chanlist, + QMap<QString, QList<ProgInfo> > *proglist) { - for (QDomNode child = element.firstChild(); !child.isNull(); - child = child.nextSibling()) + m_movieGrabberPath = MetadataDownload::GetMovieGrabber(); + m_tvGrabberPath = MetadataDownload::GetTelevisionGrabber(); + QFile f; + if (!dash_open(f, filename, QIODevice::ReadOnly)) { - QDomElement info = child.toElement(); - if (!info.isNull()) - { - if (info.tagName() == "quality") - { - if (getFirstText(info) == "HDTV") - pginfo->m_videoProps |= VID_HDTV; - } - else if (info.tagName() == "aspect") - { - if (getFirstText(info) == "16:9") - pginfo->m_videoProps |= VID_WIDESCREEN; - } - } + LOG(VB_GENERAL, LOG_ERR, + QString("Error unable to open '%1' for reading.") .arg(filename)); + return false; } -} -static void parseAudio(QDomElement &element, ProgInfo *pginfo) -{ - for (QDomNode child = element.firstChild(); !child.isNull(); - child = child.nextSibling()) + QXmlStreamReader xml(&f); + QUrl baseUrl; + QUrl sourceUrl; + QString aggregatedTitle; + QString aggregatedDesc; + bool haveReadTV = false; + while (!xml.atEnd() && !xml.hasError() && (! (xml.isEndElement() && xml.name() == "tv"))) { - QDomElement info = child.toElement(); - if (!info.isNull()) + if (xml.readNextStartElement()) { - if (info.tagName() == "stereo") + if (xml.name() == "tv") { - if (getFirstText(info) == "mono") - { - pginfo->m_audioProps |= AUD_MONO; - } - else if (getFirstText(info) == "stereo") - { - pginfo->m_audioProps |= AUD_STEREO; - } - else if (getFirstText(info) == "dolby" || - getFirstText(info) == "dolby digital") - { - pginfo->m_audioProps |= AUD_DOLBY; - } - else if (getFirstText(info) == "surround") - { - pginfo->m_audioProps |= AUD_SURROUND; - } + sourceUrl = QUrl(xml.attributes().value("source-info-url").toString()); + baseUrl = QUrl(xml.attributes().value("source-data-url").toString()); + haveReadTV = true; } - } - } -} - -ProgInfo *XMLTVParser::parseProgram(QDomElement &element) -{ - QString programid; - QString season; - QString episode; - QString totalepisodes; - auto *pginfo = new ProgInfo(); - - QString text = element.attribute("start", ""); - fromXMLTVDate(text, pginfo->m_starttime); - pginfo->m_startts = text; - - text = element.attribute("stop", ""); - fromXMLTVDate(text, pginfo->m_endtime); - pginfo->m_endts = text; - - text = element.attribute("channel", ""); - QStringList split = text.split(" "); - - pginfo->m_channel = split[0]; - - text = element.attribute("clumpidx", ""); - if (!text.isEmpty()) - { - split = text.split('/'); - pginfo->m_clumpidx = split[0]; - pginfo->m_clumpmax = split[1]; - } - - for (QDomNode child = element.firstChild(); !child.isNull(); - child = child.nextSibling()) - { - QDomElement info = child.toElement(); - if (!info.isNull()) - { - if (info.tagName() == "title") + if (xml.name() == "channel") { - if (info.attribute("lang") == "ja_JP") - { // NOLINT(bugprone-branch-clone) - pginfo->m_title = getFirstText(info); - } - else if (info.attribute("lang") == "ja_JP@kana") + if (!haveReadTV) { - pginfo->m_title_pronounce = getFirstText(info); + LOG(VB_GENERAL, LOG_ERR, QString("Malformed XML file, no <tv> element found, at line %1, %2").arg(xml.lineNumber()).arg(xml.errorString())); + return false; } - else if (pginfo->m_title.isEmpty()) - { - pginfo->m_title = getFirstText(info); - } - } - else if (info.tagName() == "sub-title" && - pginfo->m_subtitle.isEmpty()) - { - pginfo->m_subtitle = getFirstText(info); - } - else if (info.tagName() == "desc" && pginfo->m_description.isEmpty()) - { - pginfo->m_description = getFirstText(info); - } - else if (info.tagName() == "category") - { - const QString cat = getFirstText(info); - if (ProgramInfo::kCategoryNone == pginfo->m_categoryType && - string_to_myth_category_type(cat) != ProgramInfo::kCategoryNone) - { - pginfo->m_categoryType = string_to_myth_category_type(cat); - } - else if (pginfo->m_category.isEmpty()) - { - pginfo->m_category = cat; - } + //get id attribute + QString xmltvid; + xmltvid = xml.attributes().value( "id").toString(); + auto *chaninfo = new ChannelInfo; + chaninfo->m_xmltvId = xmltvid; + chaninfo->m_tvFormat = "Default"; - if ((cat.compare(QObject::tr("movie"),Qt::CaseInsensitive) == 0) || - (cat.compare(QObject::tr("film"),Qt::CaseInsensitive) == 0)) + //readNextStartElement says it reads for the next start element WITHIN the current element; but it doesnt; so we use readNext() + do { - // Hack for tv_grab_uk_rt - pginfo->m_categoryType = ProgramInfo::kCategoryMovie; + if (!readNextWithErrorCheck(xml)) + { + delete chaninfo; + return false; + } + if (xml.name() == "icon") + { + if (chaninfo->m_icon.isEmpty()) + { + QString path = xml.attributes().value("src").toString(); + if (!path.isEmpty() && !path.contains("://")) + { + QString base = baseUrl.toString(QUrl::StripTrailingSlash); + chaninfo->m_icon = base + + ((path.startsWith("/")) ? path : QString("/") + path); + } + else if (!path.isEmpty()) + { + QUrl url(path); + if (url.isValid()) + chaninfo->m_icon = url.toString(); + } + } + } + else if (xml.name() == "display-name") + { + //now get text + QString text; + text = xml.readElementText(QXmlStreamReader::SkipChildElements); + if (!text.isEmpty()) + { + if (chaninfo->m_name.isEmpty()) + { + chaninfo->m_name = text; + } + else if (chaninfo->m_callSign.isEmpty()) + { + chaninfo->m_callSign = text; + } + else if (chaninfo->m_chanNum.isEmpty()) + { + chaninfo->m_chanNum = text; + } + } + } } - - pginfo->m_genres.append(cat); - } - else if (info.tagName() == "date" && (pginfo->m_airdate == 0U)) + while (! (xml.isEndElement() && xml.name() == "channel")); + chaninfo->m_freqId = chaninfo->m_chanNum; + //TODO optimize this, no use to do al this parsing if xmltvid is empty; but make sure you will read until the next channel!! + if (!chaninfo->m_xmltvId.isEmpty()) + chanlist->push_back(*chaninfo); + delete chaninfo; + }//channel + else if (xml.name() == "programme") { - // Movie production year - QString date = getFirstText(info); - pginfo->m_airdate = date.left(4).toUInt(); - } - else if (info.tagName() == "star-rating" && pginfo->m_stars == 0.0F) - { - QDomNodeList values = info.elementsByTagName("value"); - QDomElement item; - QString stars; - float rating = 0.0; - - // Use the first rating to appear in the xml, this should be - // the most important one. - // - // Averaging is not a good idea here, any subsequent ratings - // are likely to represent that days recommended programmes - // which on a bad night could given to an average programme. - // In the case of uk_rt it's not unknown for a recommendation - // to be given to programmes which are 'so bad, you have to - // watch!' - // - // XMLTV uses zero based ratings and signals no rating by absence. - // A rating from 1 to 5 is encoded as 0/4 to 4/4. - // MythTV uses zero to signal no rating! - // The same rating is encoded as 0.2 to 1.0 with steps of 0.2, it - // is not encoded as 0.0 to 1.0 with steps of 0.25 because - // 0 signals no rating! - // See http://xmltv.cvs.sourceforge.net/viewvc/xmltv/xmltv/xmltv.dtd?revision=1.47&view=markup#l539 - item = values.item(0).toElement(); - if (!item.isNull()) + if (!haveReadTV) { - stars = getFirstText(item); - float num = stars.section('/', 0, 0).toFloat() + 1; - float den = stars.section('/', 1, 1).toFloat() + 1; - if (0.0F < den) - rating = num/den; + LOG(VB_GENERAL, LOG_ERR, QString("Malformed XML file, no <tv> element found, at line %1, %2").arg(xml.lineNumber()).arg(xml.errorString())); + return false; } - pginfo->m_stars = rating; - } - else if (info.tagName() == "rating") - { - // again, the structure of ratings seems poorly represented - // in the XML. no idea what we'd do with multiple values. - QDomNodeList values = info.elementsByTagName("value"); - QDomElement item = values.item(0).toElement(); - if (item.isNull()) - continue; - EventRating rating; - rating.m_system = info.attribute("system", ""); - rating.m_rating = getFirstText(item); - pginfo->m_ratings.append(rating); - } - else if (info.tagName() == "previously-shown") - { - pginfo->m_previouslyshown = true; + QString programid, season, episode, totalepisodes; + auto *pginfo = new ProgInfo(); - QString prevdate = info.attribute("start"); - if (!prevdate.isEmpty()) - { - QDateTime date; - fromXMLTVDate(prevdate, date); - pginfo->m_originalairdate = date.date(); - } - } - else if (info.tagName() == "credits") - { - parseCredits(info, pginfo); - } - else if (info.tagName() == "subtitles") - { - if (info.attribute("type") == "teletext") - pginfo->m_subtitleType |= SUB_NORMAL; - else if (info.attribute("type") == "onscreen") - pginfo->m_subtitleType |= SUB_ONSCREEN; - else if (info.attribute("type") == "deaf-signed") - pginfo->m_subtitleType |= SUB_SIGNED; - } - else if (info.tagName() == "audio") - { - parseAudio(info, pginfo); - } - else if (info.tagName() == "video") - { - parseVideo(info, pginfo); - } - else if (info.tagName() == "episode-num") - { - if (info.attribute("system") == "dd_progid") + QString text = xml.attributes().value("start").toString(); + fromXMLTVDate(text, pginfo->m_starttime); + pginfo->m_startts = text; + + text = xml.attributes().value("stop").toString(); + //not a mandatory attribute according to XMLTV DTD https://github.com/XMLTV/xmltv/blob/master/xmltv.dtd + fromXMLTVDate(text, pginfo->m_endtime); + pginfo->m_endts = text; + + text = xml.attributes().value("channel").toString(); + QStringList split = text.split(" "); + pginfo->m_channel = split[0]; + + text = xml.attributes().value("clumpidx").toString(); + if (!text.isEmpty()) { - QString episodenum(getFirstText(info)); - // if this field includes a dot, strip it out - int idx = episodenum.indexOf('.'); - if (idx != -1) - episodenum.remove(idx, 1); - programid = episodenum; - /* Only EPisodes and SHows are part of a series for SD */ - if (programid.startsWith(QString("EP")) || - programid.startsWith(QString("SH"))) - pginfo->m_seriesId = QString("EP") + programid.mid(2,8); + split = text.split('/'); + pginfo->m_clumpidx = split[0]; + pginfo->m_clumpmax = split[1]; } - else if (info.attribute("system") == "xmltv_ns") + + do { - QString episodenum(getFirstText(info)); - episode = episodenum.section('.',1,1); - totalepisodes = episode.section('/',1,1).trimmed(); - episode = episode.section('/',0,0).trimmed(); - season = episodenum.section('.',0,0).trimmed(); - season = season.section('/',0,0).trimmed(); - QString part(episodenum.section('.',2,2)); - QString partnumber(part.section('/',0,0).trimmed()); - QString parttotal(part.section('/',1,1).trimmed()); - - pginfo->m_categoryType = ProgramInfo::kCategorySeries; - - if (!season.isEmpty()) + if (!readNextWithErrorCheck(xml)) + return false; + if (xml.name() == "title") { - int tmp = season.toUInt() + 1; - pginfo->m_season = tmp; - season = QString::number(tmp); - pginfo->m_syndicatedepisodenumber = 'S' + season; + QString text2=xml.readElementText(QXmlStreamReader::SkipChildElements); + if (xml.attributes().value("lang").toString() == "ja_JP") + { + pginfo->m_title = text2; + } + else if (xml.attributes().value("lang").toString() == "ja_JP@kana") + { + pginfo->m_title_pronounce = text2; + } + else if (pginfo->m_title.isEmpty()) + { + pginfo->m_title = text2; + } } - - if (!episode.isEmpty()) + else if (xml.name() == "sub-title" && pginfo->m_subtitle.isEmpty()) { - int tmp = episode.toUInt() + 1; - pginfo->m_episode = tmp; - episode = QString::number(tmp); - pginfo->m_syndicatedepisodenumber.append('E' + episode); + pginfo->m_subtitle = xml.readElementText(QXmlStreamReader::SkipChildElements); } - - if (!totalepisodes.isEmpty()) + else if (xml.name() == "subtitles") { - pginfo->m_totalepisodes = totalepisodes.toUInt(); + if (xml.attributes().value("type").toString() == "teletext") + pginfo->m_subtitleType |= SUB_NORMAL; + else if (xml.attributes().value("type").toString() == "onscreen") + pginfo->m_subtitleType |= SUB_ONSCREEN; + else if (xml.attributes().value("type").toString() == "deaf-signed") + pginfo->m_subtitleType |= SUB_SIGNED; } + else if (xml.name() == "desc" && pginfo->m_description.isEmpty()) + { + pginfo->m_description = xml.readElementText(QXmlStreamReader::SkipChildElements); + } + else if (xml.name() == "category") + { + const QString cat = xml.readElementText(QXmlStreamReader::SkipChildElements); - uint partno = 0; - if (!partnumber.isEmpty()) + if (ProgramInfo::kCategoryNone == pginfo->m_categoryType && string_to_myth_category_type(cat) != ProgramInfo::kCategoryNone) + { + pginfo->m_categoryType = string_to_myth_category_type(cat); + } + else if (pginfo->m_category.isEmpty()) + { + pginfo->m_category = cat; + } + if ((cat.compare(QObject::tr("movie"),Qt::CaseInsensitive) == 0) || (cat.compare(QObject::tr("film"),Qt::CaseInsensitive) == 0)) + { + // Hack for tv_grab_uk_rt + pginfo->m_categoryType = ProgramInfo::kCategoryMovie; + } + pginfo->m_genres.append(cat); + } + else if (xml.name() == "date" && (pginfo->m_airdate == 0U)) + { + // Movie production year + QString date = xml.readElementText(QXmlStreamReader::SkipChildElements); + pginfo->m_airdate = date.left(4).toUInt(); + } + else if (xml.name() == "star-rating") { - bool ok = false; - partno = partnumber.toUInt(&ok) + 1; - partno = (ok) ? partno : 0; + QString stars; + float rating = 0.0; + + // Use the first rating to appear in the xml, this should be + // the most important one. + // + // Averaging is not a good idea here, any subsequent ratings + // are likely to represent that days recommended programmes + // which on a bad night could given to an average programme. + // In the case of uk_rt it's not unknown for a recommendation + // to be given to programmes which are 'so bad, you have to + // watch!' + // + // XMLTV uses zero based ratings and signals no rating by absence. + // A rating from 1 to 5 is encoded as 0/4 to 4/4. + // MythTV uses zero to signal no rating! + // The same rating is encoded as 0.2 to 1.0 with steps of 0.2, it + // is not encoded as 0.0 to 1.0 with steps of 0.25 because + // 0 signals no rating! + // See http://xmltv.cvs.sourceforge.net/viewvc/xmltv/xmltv/xmltv.dtd?revision=1.47&view=markup#l539 + stars = "0"; //no rating + do + { + if (!readNextWithErrorCheck(xml)) + return false; + if (xml.isStartElement()) + { + if (xml.name() == "value") + { + stars=xml.readElementText(QXmlStreamReader::SkipChildElements); + } + } + } + while (! (xml.isEndElement() && xml.name() == "star-rating")); + if (pginfo->m_stars == 0.0F) + { + float num = stars.section('/', 0, 0).toFloat() + 1; + float den = stars.section('/', 1, 1).toFloat() + 1; + if (0.0F < den) + rating = num/den; + } + pginfo->m_stars = rating; } + else if (xml.name() == "rating") + { + // again, the structure of ratings seems poorly represented + // in the XML. no idea what we'd do with multiple values. + QString rat; + QString rating_system = xml.attributes().value("system").toString(); + if (rating_system == NULL) + rating_system = ""; + + do + { + if (!readNextWithErrorCheck(xml)) + return false; + if (xml.isStartElement()) + { + if (xml.name() == "value") + { + rat=xml.readElementText(QXmlStreamReader::SkipChildElements); + } + } + } + while (! (xml.isEndElement() && xml.name() == "rating")); - if (!parttotal.isEmpty() && partno > 0) + if (!rat.isEmpty()) + { + EventRating rating; + rating.m_system = rating_system; + rating.m_rating = rat; + pginfo->m_ratings.append(rating); + } + } + else if (xml.name() == "previously-shown") { - bool ok = false; - uint partto = parttotal.toUInt(&ok); - if (ok && partnumber <= parttotal) + pginfo->m_previouslyshown = true; + QString prevdate = xml.attributes().value( "start").toString(); + if (!prevdate.isEmpty()) { - pginfo->m_parttotal = partto; - pginfo->m_partnumber = partno; + QDateTime date; + fromXMLTVDate(prevdate, date); + pginfo->m_originalairdate = date.date(); } } - } - else if (info.attribute("system") == "onscreen") - { - pginfo->m_categoryType = ProgramInfo::kCategorySeries; - if (pginfo->m_subtitle.isEmpty()) + else if (xml.name() == "credits") { - pginfo->m_subtitle = getFirstText(info); + do + { + if (!readNextWithErrorCheck(xml)) + return false; + if (xml.isStartElement()) + { + QString tagname=xml.name().toString(); + QString text2=xml.readElementText(QXmlStreamReader::SkipChildElements); + pginfo->AddPerson(tagname, text2); + } + } + while (! (xml.isEndElement() && xml.name() == "credits")); } - } - else if ((info.attribute("system") == "themoviedb.org") && - (m_movieGrabberPath.endsWith(QString("/tmdb3.py")))) - { - /* text is movie/<inetref> */ - QString inetrefRaw(getFirstText(info)); - if (inetrefRaw.startsWith(QString("movie/"))) { - QString inetref(QString ("tmdb3.py_") + inetrefRaw.section('/',1,1).trimmed()); - pginfo->m_inetref = inetref; + else if (xml.name() == "audio") + { + do + { + if (!readNextWithErrorCheck(xml)) + return false; + if (xml.isStartElement()) + { + if (xml.name() == "stereo") + { + QString text2=xml.readElementText(QXmlStreamReader::SkipChildElements); + if (text2 == "mono") + { + pginfo->m_audioProps |= AUD_MONO; + } + else if (text2 == "stereo") + { + pginfo->m_audioProps |= AUD_STEREO; + } + else if (text2 == "dolby" || text2 == "dolby digital") + { + pginfo->m_audioProps |= AUD_DOLBY; + } + else if (text2 == "surround") + { + pginfo->m_audioProps |= AUD_SURROUND; + } + } + } + } + while (! (xml.isEndElement() && xml.name() == "audio")); } - } - else if ((info.attribute("system") == "thetvdb.com") && - (m_tvGrabberPath.endsWith(QString("/ttvdb.py")))) - { - /* text is series/<inetref> */ - QString inetrefRaw(getFirstText(info)); - if (inetrefRaw.startsWith(QString("series/"))) { - QString inetref(QString ("ttvdb.py_") + inetrefRaw.section('/',1,1).trimmed()); - pginfo->m_inetref = inetref; - /* ProgInfo does not have a collectionref, so we don't set any */ + else if (xml.name() == "video") + { + do + { + if (!readNextWithErrorCheck(xml)) + return false; + if (xml.isStartElement()) + { + if (xml.name() == "quality") + { + if (xml.readElementText(QXmlStreamReader::SkipChildElements) == "HDTV") + pginfo->m_videoProps |= VID_HDTV; + } + else if (xml.name() == "aspect") + { + if (xml.readElementText(QXmlStreamReader::SkipChildElements) == "16:9") + pginfo->m_videoProps |= VID_WIDESCREEN; + } + } + } + while (! (xml.isEndElement() && xml.name() == "video")); } + else if (xml.name() == "episode-num") + { + QString system = xml.attributes().value( "system").toString(); + if (system == "dd_progid") + { + QString episodenum(xml.readElementText(QXmlStreamReader::SkipChildElements)); + // if this field includes a dot, strip it out + int idx = episodenum.indexOf('.'); + if (idx != -1) + episodenum.remove(idx, 1); + programid = episodenum; + // Only EPisodes and SHows are part of a series for SD + if (programid.startsWith(QString("EP")) || + programid.startsWith(QString("SH"))) + pginfo->m_seriesId = QString("EP") + programid.mid(2,8); + } + else if (system == "xmltv_ns") + { + QString episodenum(xml.readElementText(QXmlStreamReader::SkipChildElements)); + episode = episodenum.section('.',1,1); + totalepisodes = episode.section('/',1,1).trimmed(); + episode = episode.section('/',0,0).trimmed(); + season = episodenum.section('.',0,0).trimmed(); + season = season.section('/',0,0).trimmed(); + QString part(episodenum.section('.',2,2)); + QString partnumber(part.section('/',0,0).trimmed()); + QString parttotal(part.section('/',1,1).trimmed()); + pginfo->m_categoryType = ProgramInfo::kCategorySeries; + if (!season.isEmpty()) + { + int tmp = season.toUInt() + 1; + pginfo->m_season = tmp; + season = QString::number(tmp); + pginfo->m_syndicatedepisodenumber = 'S' + season; + } + if (!episode.isEmpty()) + { + int tmp = episode.toUInt() + 1; + pginfo->m_episode = tmp; + episode = QString::number(tmp); + pginfo->m_syndicatedepisodenumber.append('E' + episode); + } + if (!totalepisodes.isEmpty()) + { + pginfo->m_totalepisodes = totalepisodes.toUInt(); + } + uint partno = 0; + if (!partnumber.isEmpty()) + { + bool ok = false; + partno = partnumber.toUInt(&ok) + 1; + partno = (ok) ? partno : 0; + } + if (!parttotal.isEmpty() && partno > 0) + { + bool ok = false; + uint partto = parttotal.toUInt(&ok); + if (ok && partnumber <= parttotal) + { + pginfo->m_parttotal = partto; + pginfo->m_partnumber = partno; + } + } + } + else if (system == "onscreen") + { + pginfo->m_categoryType = ProgramInfo::kCategorySeries; + if (pginfo->m_subtitle.isEmpty()) + { + pginfo->m_subtitle = xml.readElementText(QXmlStreamReader::SkipChildElements); + } + } + else if ((system == "themoviedb.org") && (m_movieGrabberPath.endsWith(QString("/tmdb3.py")))) + { + // text is movie/<inetref> + QString inetrefRaw(xml.readElementText(QXmlStreamReader::SkipChildElements)); + if (inetrefRaw.startsWith(QString("movie/"))) + { + QString inetref(QString ("tmdb3.py_") + inetrefRaw.section('/',1,1).trimmed()); + pginfo->m_inetref = inetref; + } + } + else if ((system == "thetvdb.com") && (m_tvGrabberPath.endsWith(QString("/ttvdb.py")))) + { + // text is series/<inetref> + QString inetrefRaw(xml.readElementText(QXmlStreamReader::SkipChildElements)); + if (inetrefRaw.startsWith(QString("series/"))) + { + QString inetref(QString ("ttvdb.py_") + inetrefRaw.section('/',1,1).trimmed()); + pginfo->m_inetref = inetref; + // ProgInfo does not have a collectionref, so we don't set any + } + } + }//episode-num } - } - } - } - - if (pginfo->m_category.isEmpty() && - pginfo->m_categoryType != ProgramInfo::kCategoryNone) - pginfo->m_category = myth_category_type_to_string(pginfo->m_categoryType); + while (! (xml.isEndElement() && xml.name() == "programme")); - if (!pginfo->m_airdate - && ProgramInfo::kCategorySeries != pginfo->m_categoryType) - pginfo->m_airdate = m_currentYear; - - if (programid.isEmpty()) - { + if (pginfo->m_category.isEmpty() && pginfo->m_categoryType != ProgramInfo::kCategoryNone) + pginfo->m_category = myth_category_type_to_string(pginfo->m_categoryType); - /* Let's build ourself a programid */ + if (!pginfo->m_airdate && ProgramInfo::kCategorySeries != pginfo->m_categoryType) + pginfo->m_airdate = m_currentYear; - if (ProgramInfo::kCategoryMovie == pginfo->m_categoryType) - programid = "MV"; - else if (ProgramInfo::kCategorySeries == pginfo->m_categoryType) - programid = "EP"; - else if (ProgramInfo::kCategorySports == pginfo->m_categoryType) - programid = "SP"; - else - programid = "SH"; - - QString seriesid = QString::number(ELFHash(pginfo->m_title.toUtf8())); - pginfo->m_seriesId = seriesid; - programid.append(seriesid); - - if (!episode.isEmpty() && !season.isEmpty()) - { - /* Append unpadded episode and season number to the seriesid (to - maintain consistency with historical encoding), but limit the - season number representation to a single base-36 character to - ensure unique programid generation. */ - int season_int = season.toInt(); - if (season_int > 35) - { - // Cannot represent season as a single base-36 character, so - // remove the programid and fall back to normal dup matching. - if (ProgramInfo::kCategoryMovie != pginfo->m_categoryType) - programid.clear(); - } - else - { - programid.append(episode); - programid.append(QString::number(season_int, 36)); - if (pginfo->m_partnumber && pginfo->m_parttotal) + if (programid.isEmpty()) { - programid += QString::number(pginfo->m_partnumber); - programid += QString::number(pginfo->m_parttotal); - } - } - } - else - { - /* No ep/season info? Well then remove the programid and rely on - normal dupchecking methods instead. */ - if (ProgramInfo::kCategoryMovie != pginfo->m_categoryType) - programid.clear(); - } - } - - pginfo->m_programId = programid; - - return pginfo; -} - -bool XMLTVParser::parseFile( - const QString& filename, ChannelInfoList *chanlist, - QMap<QString, QList<ProgInfo> > *proglist) -{ - QDomDocument doc; - QFile f; - - if (!dash_open(f, filename, QIODevice::ReadOnly)) - { - LOG(VB_GENERAL, LOG_ERR, - QString("Error unable to open '%1' for reading.") .arg(filename)); - return false; - } - - QString errorMsg = "unknown"; - int errorLine = 0; - int errorColumn = 0; - - if (!doc.setContent(&f, &errorMsg, &errorLine, &errorColumn)) - { - LOG(VB_GENERAL, LOG_ERR, QString("Error in %1:%2: %3") - .arg(errorLine).arg(errorColumn).arg(errorMsg)); - - f.close(); - return true; - } - - f.close(); - - QDomElement docElem = doc.documentElement(); - - QUrl baseUrl(docElem.attribute("source-data-url", "")); - //QUrl sourceUrl(docElem.attribute("source-info-url", "")); - - QString aggregatedTitle; - QString aggregatedDesc; + //Let's build ourself a programid + if (ProgramInfo::kCategoryMovie == pginfo->m_categoryType) + programid = "MV"; + else if (ProgramInfo::kCategorySeries == pginfo->m_categoryType) + programid = "EP"; + else if (ProgramInfo::kCategorySports == pginfo->m_categoryType) + programid = "SP"; + else + programid = "SH"; - QDomNode n = docElem.firstChild(); - while (!n.isNull()) - { - QDomElement e = n.toElement(); - if (!e.isNull()) - { - if (e.tagName() == "channel") - { - ChannelInfo *chinfo = parseChannel(e, baseUrl); - if (!chinfo->m_xmltvId.isEmpty()) - chanlist->push_back(*chinfo); - delete chinfo; - } - else if (e.tagName() == "programme") - { - ProgInfo *pginfo = parseProgram(e); + QString seriesid = QString::number(ELFHash(pginfo->m_title.toUtf8())); + pginfo->m_seriesId = seriesid; + programid.append(seriesid); + if (!episode.isEmpty() && !season.isEmpty()) + { + /* Append unpadded episode and season number to the seriesid (to + maintain consistency with historical encoding), but limit the + season number representation to a single base-36 character to + ensure unique programid generation. */ + int season_int = season.toInt(); + if (season_int > 35) + { + // Cannot represent season as a single base-36 character, so + // remove the programid and fall back to normal dup matching. + if (ProgramInfo::kCategoryMovie != pginfo->m_categoryType) + programid.clear(); + } + else + { + programid.append(episode); + programid.append(QString::number(season_int, 36)); + if (pginfo->m_partnumber && pginfo->m_parttotal) + { + programid += QString::number(pginfo->m_partnumber); + programid += QString::number(pginfo->m_parttotal); + } + } + } + else + { + /* No ep/season info? Well then remove the programid and rely on + normal dupchecking methods instead. */ + if (ProgramInfo::kCategoryMovie != pginfo->m_categoryType) + programid.clear(); + } + } + pginfo->m_programId = programid; if (!(pginfo->m_starttime.isValid())) { - LOG(VB_GENERAL, LOG_WARNING, QString("Invalid programme (%1), " - "invalid start time, " - "skipping") - .arg(pginfo->m_title)); + LOG(VB_GENERAL, LOG_WARNING, QString("Invalid programme (%1), " "invalid start time, " "skipping").arg(pginfo->m_title)); } else if (pginfo->m_channel.isEmpty()) { - LOG(VB_GENERAL, LOG_WARNING, QString("Invalid programme (%1), " - "missing channel, " - "skipping") - .arg(pginfo->m_title)); + LOG(VB_GENERAL, LOG_WARNING, QString("Invalid programme (%1), " "missing channel, " "skipping").arg(pginfo->m_title)); } else if (pginfo->m_startts == pginfo->m_endts) { - LOG(VB_GENERAL, LOG_WARNING, QString("Invalid programme (%1), " - "identical start and end " - "times, skipping") - .arg(pginfo->m_title)); + LOG(VB_GENERAL, LOG_WARNING, QString("Invalid programme (%1), " "identical start and end " "times, skipping").arg(pginfo->m_title)); } else { + // so we have a (relatively) clean program element now, which is good enough to process or to store if (pginfo->m_clumpidx.isEmpty()) (*proglist)[pginfo->m_channel].push_back(*pginfo); else @@ -737,22 +714,19 @@ bool XMLTVParser::parseFile( aggregatedTitle.clear(); aggregatedDesc.clear(); } - if (!pginfo->m_title.isEmpty()) { if (!aggregatedTitle.isEmpty()) aggregatedTitle.append(" | "); aggregatedTitle.append(pginfo->m_title); } - if (!pginfo->m_description.isEmpty()) { if (!aggregatedDesc.isEmpty()) aggregatedDesc.append(" | "); aggregatedDesc.append(pginfo->m_description); } - if (pginfo->m_clumpidx.toInt() == - pginfo->m_clumpmax.toInt() - 1) + if (pginfo->m_clumpidx.toInt() == pginfo->m_clumpmax.toInt() - 1) { pginfo->m_title = aggregatedTitle; pginfo->m_description = aggregatedDesc; @@ -761,10 +735,16 @@ bool XMLTVParser::parseFile( } } delete pginfo; - } - } - n = n.nextSibling(); + }//if programme + }//if readNextStartElement + }//while loop + if (! (xml.isEndElement() && xml.name() == "tv")) + { + LOG(VB_GENERAL, LOG_ERR, QString("Malformed XML file, missing </tv> element, at line %1, %2").arg(xml.lineNumber()).arg(xml.errorString())); + return false; } + //TODO add code for adding data on the run + f.close(); return true; } diff --git a/mythtv/programs/mythfilldatabase/xmltvparser.h b/mythtv/programs/mythfilldatabase/xmltvparser.h index 6d06aefe40..9fad22dc1e 100644 --- a/mythtv/programs/mythfilldatabase/xmltvparser.h +++ b/mythtv/programs/mythfilldatabase/xmltvparser.h @@ -17,10 +17,6 @@ class XMLTVParser { public: XMLTVParser(); - void lateInit(); - - static ChannelInfo *parseChannel(QDomElement &element, QUrl &baseUrl); - ProgInfo *parseProgram(QDomElement &element); bool parseFile(const QString& filename, ChannelInfoList *chanlist, QMap<QString, QList<ProgInfo> > *proglist); diff --git a/mythtv/programs/mythfrontend/guidegrid.cpp b/mythtv/programs/mythfrontend/guidegrid.cpp index 6bf47163af..9001182bf4 100644 --- a/mythtv/programs/mythfrontend/guidegrid.cpp +++ b/mythtv/programs/mythfrontend/guidegrid.cpp @@ -518,6 +518,19 @@ GuideGrid::GuideGrid(MythScreenStack *parent, m_originalStartTime.time().second()); m_currentStartTime = m_originalStartTime.addSecs(secsoffset); m_threadPool.setMaxThreadCount(1); + + if (m_player) + connect(m_player, &TV::PlaybackExiting, this, &GuideGrid::PlayerExiting); +} + +void GuideGrid::PlayerExiting(TV* Player) +{ + if (Player && (Player == m_player)) + { + m_player->StopEmbedding(); + HideTVWindow(); + m_player = nullptr; + } } bool GuideGrid::Create() diff --git a/mythtv/programs/mythfrontend/guidegrid.h b/mythtv/programs/mythfrontend/guidegrid.h index dde085348d..83062a98ea 100644 --- a/mythtv/programs/mythfrontend/guidegrid.h +++ b/mythtv/programs/mythfrontend/guidegrid.h @@ -134,6 +134,9 @@ class GuideGrid : public ScheduleCommon, public JumpToChannelListener uint GetCurrentStartChannel(void) const { return m_currentStartChannel; } QDateTime GetCurrentStartTime(void) const { return m_currentStartTime; } + public slots: + void PlayerExiting(TV* Player); + protected slots: void cursorLeft(); void cursorRight(); diff --git a/mythtv/programs/mythfrontend/main.cpp b/mythtv/programs/mythfrontend/main.cpp index 16bc8819be..fa713e5d51 100644 --- a/mythtv/programs/mythfrontend/main.cpp +++ b/mythtv/programs/mythfrontend/main.cpp @@ -1868,8 +1868,9 @@ int main(int argc, char **argv) #ifdef Q_OS_MAC QString path = QCoreApplication::applicationDirPath(); setenv("PYTHONPATH", - QString("%1/../Resources/lib/python2.6/site-packages:%2") + QString("%1/../Resources/lib/%2/site-packages:%3") .arg(path) + .arg(QFileInfo(PYTHON_EXE).fileName()) .arg(QProcessEnvironment::systemEnvironment().value("PYTHONPATH")) .toUtf8().constData(), 1); #endif diff --git a/mythtv/programs/mythfrontend/manualschedule.cpp b/mythtv/programs/mythfrontend/manualschedule.cpp index e8e2b349b0..521f8707f6 100644 --- a/mythtv/programs/mythfrontend/manualschedule.cpp +++ b/mythtv/programs/mythfrontend/manualschedule.cpp @@ -219,6 +219,7 @@ void ManualSchedule::recordClicked(void) auto *record = new RecordingRule(); record->LoadByProgram(&p); record->m_searchType = kManualSearch; + record->m_dupMethod = kDupCheckNone; MythScreenStack *mainStack = GetMythMainWindow()->GetMainStack(); auto *schededit = new ScheduleEditor(mainStack, record); diff --git a/mythtv/programs/mythfrontend/scheduleeditor.cpp b/mythtv/programs/mythfrontend/scheduleeditor.cpp index cdb7f6d68b..ddd18d4e26 100644 --- a/mythtv/programs/mythfrontend/scheduleeditor.cpp +++ b/mythtv/programs/mythfrontend/scheduleeditor.cpp @@ -2138,6 +2138,7 @@ void SchedOptMixin::RuleChanged(void) m_rule->m_type != kDontRecord); bool isSingle = (m_rule->m_type == kSingleRecord || m_rule->m_type == kOverrideRecord); + bool isManual = (m_rule->m_searchType == kManualSearch); if (m_prioritySpin) m_prioritySpin->SetEnabled(isScheduled); @@ -2146,7 +2147,9 @@ void SchedOptMixin::RuleChanged(void) if (m_endoffsetSpin) m_endoffsetSpin->SetEnabled(isScheduled); if (m_dupmethodList) - m_dupmethodList->SetEnabled(isScheduled && !isSingle); + m_dupmethodList->SetEnabled( + isScheduled && !isSingle && + (!isManual || m_rule->m_dupMethod != kDupCheckNone)); if (m_dupscopeList) m_dupscopeList->SetEnabled(isScheduled && !isSingle && m_rule->m_dupMethod != kDupCheckNone); diff --git a/mythtv/programs/mythfrontend/videodlg.cpp b/mythtv/programs/mythfrontend/videodlg.cpp index a9ae42cb09..345acb93eb 100644 --- a/mythtv/programs/mythfrontend/videodlg.cpp +++ b/mythtv/programs/mythfrontend/videodlg.cpp @@ -51,6 +51,8 @@ // for ImageDLFailureEvent #include "metadataimagedownload.h" +#define LOC_MML QString("Manual Metadata Lookup: ") + static const QString _Location = "MythVideo"; namespace @@ -3506,12 +3508,33 @@ void VideoDialog::ToggleWatched() } } -void VideoDialog::OnVideoSearchListSelection(const RefCountHandler<MetadataLookup>& lookup) +void VideoDialog::OnVideoSearchListSelection(RefCountHandler<MetadataLookup> lookup) { if (!lookup) return; - OnVideoSearchDone(lookup); + if(!lookup->GetInetref().isEmpty() && lookup->GetInetref() != "00000000") + { + LOG(VB_GENERAL, LOG_INFO, LOC_MML + + QString("Selected Item: Type: %1%2 : Subtype: %3%4%5 : InetRef: %6") + .arg(lookup->GetType() == kMetadataVideo ? "Video" : "") + .arg(lookup->GetType() == kMetadataRecording ? "Recording" : "") + .arg(lookup->GetSubtype() == kProbableMovie ? "Movie" : "") + .arg(lookup->GetSubtype() == kProbableTelevision ? "Television" : "") + .arg(lookup->GetSubtype() == kUnknownVideo ? "Unknown" : "") + .arg(lookup->GetInetref())); + + lookup->SetStep(kLookupData); + lookup->IncrRef(); + m_metadataFactory->Lookup(lookup); + } + else + { + LOG(VB_GENERAL, LOG_ERR, LOC_MML + + QString("Selected Item has no InetRef Number!")); + + OnVideoSearchDone(lookup); + } } void VideoDialog::OnParentalChange(int amount) diff --git a/mythtv/programs/mythfrontend/videodlg.h b/mythtv/programs/mythfrontend/videodlg.h index 3747f5a4d4..cdadd7ee5a 100644 --- a/mythtv/programs/mythfrontend/videodlg.h +++ b/mythtv/programs/mythfrontend/videodlg.h @@ -131,7 +131,7 @@ class VideoDialog : public MythScreenType void OnParentalChange(int amount); // Called when the underlying data for an item changes - void OnVideoSearchListSelection(const RefCountHandler<MetadataLookup>& lookup); + void OnVideoSearchListSelection(RefCountHandler<MetadataLookup> lookup); void doVideoScan(); diff --git a/mythtv/programs/mythmetadatalookup/main.cpp b/mythtv/programs/mythmetadatalookup/main.cpp index 423b719041..78393d9cbe 100644 --- a/mythtv/programs/mythmetadatalookup/main.cpp +++ b/mythtv/programs/mythmetadatalookup/main.cpp @@ -68,8 +68,9 @@ int main(int argc, char *argv[]) #ifdef Q_OS_MAC QString path = QCoreApplication::applicationDirPath(); setenv("PYTHONPATH", - QString("%1/../Resources/lib/python2.6/site-packages:%2") + QString("%1/../Resources/lib/%2/site-packages:%3") .arg(path) + .arg(QFileInfo(PYTHON_EXE).fileName()) .arg(QProcessEnvironment::systemEnvironment().value("PYTHONPATH")) .toUtf8().constData(), 1); #endif diff --git a/mythtv/programs/mythtv-setup/main.cpp b/mythtv/programs/mythtv-setup/main.cpp index 67ac7d5433..5f612cc253 100644 --- a/mythtv/programs/mythtv-setup/main.cpp +++ b/mythtv/programs/mythtv-setup/main.cpp @@ -258,6 +258,7 @@ int main(int argc, char *argv[]) bool scanLCNOnly = false; bool scanCompleteOnly = false; bool scanFullChannelSearch = false; + bool scanRemoveDuplicates = false; bool addFullTS = false; ServiceRequirements scanServiceRequirements = kRequireAV; uint scanCardId = 0; @@ -346,6 +347,8 @@ int main(int argc, char *argv[]) scanCompleteOnly = true; if (cmdline.toBool("fullsearch")) scanFullChannelSearch = true; + if (cmdline.toBool("removeduplicates")) + scanRemoveDuplicates = true; if (cmdline.toBool("addfullts")) addFullTS = true; if (cmdline.toBool("servicetype")) @@ -501,6 +504,7 @@ int main(int argc, char *argv[]) scanLCNOnly, scanCompleteOnly, scanFullChannelSearch, + scanRemoveDuplicates, addFullTS, scanServiceRequirements, // stuff needed for particular scans @@ -535,8 +539,11 @@ int main(int argc, char *argv[]) { ScanDTVTransportList list = LoadScan(scanImport); ChannelImporter ci(false, true, true, true, false, - scanFTAOnly, scanLCNOnly, scanCompleteOnly, - scanFullChannelSearch, scanServiceRequirements); + scanFTAOnly, scanLCNOnly, + scanCompleteOnly, + scanFullChannelSearch, + scanRemoveDuplicates, + scanServiceRequirements); ci.Process(list); } cout<<"*** SCAN IMPORT END ***"<<endl; diff --git a/mythtv/programs/mythutil/commandlineparser.cpp b/mythtv/programs/mythutil/commandlineparser.cpp index d6c8e14658..9483adf58b 100644 --- a/mythtv/programs/mythutil/commandlineparser.cpp +++ b/mythtv/programs/mythutil/commandlineparser.cpp @@ -232,6 +232,8 @@ void MythUtilCommandLineParser::LoadArguments(void) ->SetChildOf("notification"); // musicmetautils.cpp + add("--force", "musicforce", false, "Ignore file timestamps", "") + ->SetChildOf("scanmusic"); add("--songid", "songid", "", "ID of track to update", "") ->SetChildOf("updatemeta"); add("--title", "title", "", "(optional) Title of track", "") diff --git a/mythtv/programs/mythutil/musicmetautils.cpp b/mythtv/programs/mythutil/musicmetautils.cpp index 969b3908eb..0b34b188aa 100644 --- a/mythtv/programs/mythutil/musicmetautils.cpp +++ b/mythtv/programs/mythutil/musicmetautils.cpp @@ -178,9 +178,9 @@ static int ExtractImage(const MythUtilCommandLineParser &cmdline) return GENERIC_EXIT_OK; } -static int ScanMusic(const MythUtilCommandLineParser &/*cmdline*/) +static int ScanMusic(const MythUtilCommandLineParser &cmdline) { - auto *fscan = new MusicFileScanner(); + auto *fscan = new MusicFileScanner(cmdline.toBool("musicforce")); QStringList dirList; if (!StorageGroup::FindDirs("Music", gCoreContext->GetHostName(), &dirList)) diff --git a/mythtv/programs/scripts/hardwareprofile/distros/mythtv_data/uuiddb.py b/mythtv/programs/scripts/hardwareprofile/distros/mythtv_data/uuiddb.py index febd16e9f3..6960f8705a 100644 --- a/mythtv/programs/scripts/hardwareprofile/distros/mythtv_data/uuiddb.py +++ b/mythtv/programs/scripts/hardwareprofile/distros/mythtv_data/uuiddb.py @@ -132,7 +132,7 @@ _uuid_db_instance = None def UuidDb(): """Simple singleton wrapper with lazy initialization""" global _uuid_db_instance - if _uuid_db_instance == None: + if _uuid_db_instance is None: import config from smolt import get_config_attr _uuid_db_instance = _UuidDb(get_config_attr("UUID_DB", os.path.expanduser('~/.smolt/uuiddb.cfg'))) diff --git a/mythtv/programs/scripts/hardwareprofile/sendProfile.py b/mythtv/programs/scripts/hardwareprofile/sendProfile.py index ca929654db..ccbfdeac6d 100755 --- a/mythtv/programs/scripts/hardwareprofile/sendProfile.py +++ b/mythtv/programs/scripts/hardwareprofile/sendProfile.py @@ -286,7 +286,7 @@ def mention_profile_web_view(opts, pub_uuid, admin): def get_proxies(opts): - if opts.httpproxy == None: + if opts.httpproxy is None: proxies = dict() else: proxies = {'http':opts.httpproxy} diff --git a/mythtv/programs/scripts/hardwareprofile/smolt.py b/mythtv/programs/scripts/hardwareprofile/smolt.py index 5cf234e7c0..1bcc8060d0 100644 --- a/mythtv/programs/scripts/hardwareprofile/smolt.py +++ b/mythtv/programs/scripts/hardwareprofile/smolt.py @@ -376,7 +376,7 @@ def ignoreDevice(device): ignore = 1 if device.bus == 'Unknown' or device.bus == 'unknown': return 1 - if device.vendorid in (0, None) and device.type == None: + if device.vendorid in (0, None) and device.type is None: return 1 if device.bus == 'usb' and device.driver == 'hub': return 1 @@ -388,7 +388,7 @@ def ignoreDevice(device): return 1 if device.bus == 'block' and device.type == 'DISK': return 1 - if device.bus == 'usb_device' and device.type == None: + if device.bus == 'usb_device' and device.type is None: return 1 return 0 diff --git a/mythtv/programs/scripts/internetcontent/nv_python_libs/bbciplayer/bbciplayer_api.py b/mythtv/programs/scripts/internetcontent/nv_python_libs/bbciplayer/bbciplayer_api.py index 01ee605256..02cefe586b 100644 --- a/mythtv/programs/scripts/internetcontent/nv_python_libs/bbciplayer/bbciplayer_api.py +++ b/mythtv/programs/scripts/internetcontent/nv_python_libs/bbciplayer/bbciplayer_api.py @@ -349,7 +349,7 @@ class Videos(object): pubDate = datetime.datetime.now().strftime(self.common.pubDateFormat) # Set the display type for the link (Fullscreen, Web page, Game Console) - if self.userPrefs.find('displayURL') != None: + if self.userPrefs.find('displayURL') is not None: urlType = self.userPrefs.find('displayURL').text else: urlType = u'fullscreen' @@ -519,7 +519,7 @@ class Videos(object): searchResultTree = [] searchFilter = etree.XPath(u"//item") userSearchStrings = u'userSearchStrings' - if self.userPrefs.find(userSearchStrings) != None: + if self.userPrefs.find(userSearchStrings) is not None: userSearch = self.userPrefs.find(userSearchStrings).xpath('./userSearch') if len(userSearch): for searchDetails in userSearch: @@ -554,7 +554,7 @@ class Videos(object): # Create a structure of feeds that can be concurrently downloaded rssData = etree.XML(u'<xml></xml>') for feedType in [u'treeviewURLS', u'userFeeds']: - if self.userPrefs.find(feedType) == None: + if self.userPrefs.find(feedType) is None: continue if not len(self.userPrefs.find(feedType).xpath('./url')): continue @@ -581,7 +581,7 @@ class Videos(object): print # Get the RSS Feed data - if rssData.find('url') != None: + if rssData.find('url') is not None: try: resultTree = self.common.getUrlData(rssData) except Exception, errormsg: @@ -592,7 +592,7 @@ class Videos(object): print # Set the display type for the link (Fullscreen, Web page, Game Console) - if self.userPrefs.find('displayURL') != None: + if self.userPrefs.find('displayURL') is not None: urlType = self.userPrefs.find('displayURL').text else: urlType = u'fullscreen' @@ -638,7 +638,7 @@ class Videos(object): channelLanguage = u'en' # Create a new directory and/or subdirectory if required if names[0] != categoryDir: - if categoryDir != None: + if categoryDir is not None: channelTree.append(categoryElement) categoryElement = etree.XML(u'<directory></directory>') categoryElement.attrib['name'] = names[0] @@ -714,8 +714,8 @@ class Videos(object): break # Add the last directory processed - if categoryElement != None: - if categoryElement.xpath('.//item') != None: + if categoryElement is not None: + if categoryElement.xpath('.//item') is not None: channelTree.append(categoryElement) # Check that there was at least some items diff --git a/mythtv/programs/scripts/internetcontent/nv_python_libs/bliptv/bliptv_api.py b/mythtv/programs/scripts/internetcontent/nv_python_libs/bliptv/bliptv_api.py index 4abbf697da..a59f99147d 100644 --- a/mythtv/programs/scripts/internetcontent/nv_python_libs/bliptv/bliptv_api.py +++ b/mythtv/programs/scripts/internetcontent/nv_python_libs/bliptv/bliptv_api.py @@ -279,7 +279,7 @@ class Videos(object): ip = getExternalIP() - if ip == None: + if ip is None: return {} try: @@ -371,7 +371,7 @@ class Videos(object): def textUtf8(self, text): - if text == None: + if text is None: return text try: return unicode(text, 'utf8') @@ -541,7 +541,7 @@ class Videos(object): sys.stderr.write(u"! Error: Unknown error during a Video search (%s)\nError(%s)\n" % (title, e)) sys.exit(1) - if data == None: + if data is None: return None if not len(data): return None diff --git a/mythtv/programs/scripts/internetcontent/nv_python_libs/common/common_api.py b/mythtv/programs/scripts/internetcontent/nv_python_libs/common/common_api.py index 03341242ba..2f58267c30 100644 --- a/mythtv/programs/scripts/internetcontent/nv_python_libs/common/common_api.py +++ b/mythtv/programs/scripts/internetcontent/nv_python_libs/common/common_api.py @@ -276,7 +276,7 @@ class Common(object): def textUtf8(self, text): - if text == None: + if text is None: return text try: return unicode(text, 'utf8') @@ -369,7 +369,7 @@ class Common(object): ip = getExternalIP() - if ip == None: + if ip is None: return {} try: @@ -508,7 +508,7 @@ class Common(object): urlDictionary[key]['morePages'] = u'false' urlDictionary[key]['tmp'] = None urlDictionary[key]['tree'] = None - if element.find('parameter') != None: + if element.find('parameter') is not None: urlDictionary[key]['parameter'] = element.find('parameter').text if self.debug: @@ -747,7 +747,7 @@ for xsltExtension in %(filename)s.__xsltExtentionList__: # Currently there are no link specific Web pages if not self.linksWebPage: self.linksWebPage = etree.parse(u'%s/nv_python_libs/configs/XML/customeHtmlPageList.xml' % (self.baseProcessingDir, )) - if self.linksWebPage.find(sourceLink) != None: + if self.linksWebPage.find(sourceLink) is not None: return u'file://%s/nv_python_libs/configs/HTML/%s' % (self.baseProcessingDir, self.linksWebPage.find(sourceLink).text) return u'file://%s/nv_python_libs/configs/HTML/%s' % (self.baseProcessingDir, 'nodownloads.html') # end linkWebPage() @@ -1015,7 +1015,7 @@ self.urlDictionary[self.urlKey]['parameter']) ) else: continue # Was any data found? - if self.urlDictionary[self.urlKey]['tmp'].getroot() == None: + if self.urlDictionary[self.urlKey]['tmp'].getroot() is None: sys.stderr.write(u"No Xslt results for Name(%s)\n" % self.urlKey) sys.stderr.write(u"No Xslt results for url(%s)\n" % self.urlDictionary[self.urlKey]['href']) if len(self.urlDictionary[self.urlKey]['filter']) == index-1: diff --git a/mythtv/programs/scripts/internetcontent/nv_python_libs/dailymotion/dailymotion_api.py b/mythtv/programs/scripts/internetcontent/nv_python_libs/dailymotion/dailymotion_api.py index d4f502fc22..6cee18b245 100644 --- a/mythtv/programs/scripts/internetcontent/nv_python_libs/dailymotion/dailymotion_api.py +++ b/mythtv/programs/scripts/internetcontent/nv_python_libs/dailymotion/dailymotion_api.py @@ -566,7 +566,7 @@ class Videos(object): ip = getExternalIP() - if ip == None: + if ip is None: return {} try: @@ -658,7 +658,7 @@ class Videos(object): def textUtf8(self, text): - if text == None: + if text is None: return text try: return unicode(text, 'utf8') @@ -747,7 +747,7 @@ class Videos(object): sys.stderr.write(u"! Error: Unknown error during a Video search (%s)\nError(%s)\n" % (title, e)) sys.exit(1) - if data == None: + if data is None: return None if not len(data): return None diff --git a/mythtv/programs/scripts/internetcontent/nv_python_libs/hulu/hulu_api.py b/mythtv/programs/scripts/internetcontent/nv_python_libs/hulu/hulu_api.py index 1152735289..153a7b6e85 100644 --- a/mythtv/programs/scripts/internetcontent/nv_python_libs/hulu/hulu_api.py +++ b/mythtv/programs/scripts/internetcontent/nv_python_libs/hulu/hulu_api.py @@ -478,7 +478,7 @@ class Videos(object): searchResultTree = [] searchFilter = etree.XPath(u"//item") userSearchStrings = u'userSearchStrings' - if self.userPrefs.find(userSearchStrings) != None: + if self.userPrefs.find(userSearchStrings) is not None: userSearch = self.userPrefs.find(userSearchStrings).xpath('./userSearch') if len(userSearch): for searchDetails in userSearch: @@ -513,7 +513,7 @@ class Videos(object): # Create a structure of feeds that can be concurrently downloaded rssData = etree.XML(u'<xml></xml>') for feedType in [u'treeviewURLS', ]: - if self.userPrefs.find(feedType) == None: + if self.userPrefs.find(feedType) is None: continue if not len(self.userPrefs.find(feedType).xpath('./url')): continue @@ -540,7 +540,7 @@ class Videos(object): print # Get the RSS Feed data - if rssData.find('url') != None: + if rssData.find('url') is not None: try: resultTree = self.common.getUrlData(rssData) except Exception, errormsg: @@ -591,7 +591,7 @@ class Videos(object): channelLanguage = u'en' # Create a new directory and/or subdirectory if required if names[0] != categoryDir: - if categoryDir != None: + if categoryDir is not None: channelTree.append(categoryElement) categoryElement = etree.XML(u'<directory></directory>') categoryElement.attrib['name'] = names[0] @@ -617,7 +617,7 @@ class Videos(object): huluItem.find('author').text = u'Hulu' huluItem.find('pubDate').text = pubdate description = etree.HTML(etree.tostring(descriptionFilter(itemData)[0], method="text", encoding=unicode).strip()) - if descFilter2(description)[0].text != None: + if descFilter2(description)[0].text is not None: huluItem.find('description').text = self.common.massageText(descFilter2(description)[0].text.strip()) else: huluItem.find('description').text = u'' @@ -667,8 +667,8 @@ class Videos(object): break # Add the last directory processed - if categoryElement != None: - if categoryElement.xpath('.//item') != None: + if categoryElement is not None: + if categoryElement.xpath('.//item') is not None: channelTree.append(categoryElement) # Check that there was at least some items diff --git a/mythtv/programs/scripts/internetcontent/nv_python_libs/mainProcess.py b/mythtv/programs/scripts/internetcontent/nv_python_libs/mainProcess.py index 0bc719f3cd..3a96c14c0d 100755 --- a/mythtv/programs/scripts/internetcontent/nv_python_libs/mainProcess.py +++ b/mythtv/programs/scripts/internetcontent/nv_python_libs/mainProcess.py @@ -241,7 +241,7 @@ xmlns:mythtv="http://www.mythtv.org/wiki/MythNetvision_Grabber_Script_Format">"" self.config['target'].mashup_title = self.mashup_title data_sets = self.config['target'].searchForVideos(search_text, pagenumber) - if data_sets == None: + if data_sets is None: return if not len(data_sets): return @@ -272,7 +272,7 @@ xmlns:mythtv="http://www.mythtv.org/wiki/MythNetvision_Grabber_Script_Format">"" self.config['target'].mashup_title = self.mashup_title data_sets = self.config['target'].displayTreeView() - if data_sets == None: + if data_sets is None: return if not len(data_sets): return diff --git a/mythtv/programs/scripts/internetcontent/nv_python_libs/mashups/mashups_api.py b/mythtv/programs/scripts/internetcontent/nv_python_libs/mashups/mashups_api.py index 6fc4460f36..962323384c 100644 --- a/mythtv/programs/scripts/internetcontent/nv_python_libs/mashups/mashups_api.py +++ b/mythtv/programs/scripts/internetcontent/nv_python_libs/mashups/mashups_api.py @@ -395,7 +395,7 @@ class Videos(object): print # Get the source data - if sourceData.find('url') != None: + if sourceData.find('url') is not None: # Process each directory of the user preferences that have an enabled rss feed try: resultTree = self.common.getUrlData(sourceData) @@ -488,7 +488,7 @@ class Videos(object): url = etree.XML(u'<url></url>') etree.SubElement(url, "name").text = uniqueName etree.SubElement(url, "href").text = source.attrib.get('url') - if source.attrib.get('parameter') != None: + if source.attrib.get('parameter') is not None: etree.SubElement(url, "parameter").text = source.attrib.get('parameter') if len(xsltFilename(source)): for xsltName in xsltFilename(source): @@ -502,7 +502,7 @@ class Videos(object): print # Get the source data - if sourceData.find('url') != None: + if sourceData.find('url') is not None: # Process each directory of the user preferences that have an enabled rss feed try: resultTree = self.common.getUrlData(sourceData) @@ -566,7 +566,7 @@ class Videos(object): # Create a new directory and/or subdirectory if required if names[0] != categoryDir: - if categoryDir != None: + if categoryDir is not None: channelTree.append(categoryElement) categoryElement = etree.XML(u'<directory></directory>') categoryElement.attrib['name'] = names[0] @@ -627,7 +627,7 @@ class Videos(object): break # Add the last directory processed and the "Special" directories - if categoryElement != None: + if categoryElement is not None: if len(itemFilter(categoryElement)): channelTree.append(categoryElement) # Add the special directories videos diff --git a/mythtv/programs/scripts/internetcontent/nv_python_libs/mtv/mtv_api.py b/mythtv/programs/scripts/internetcontent/nv_python_libs/mtv/mtv_api.py index 47612f2a26..c047ca9529 100644 --- a/mythtv/programs/scripts/internetcontent/nv_python_libs/mtv/mtv_api.py +++ b/mythtv/programs/scripts/internetcontent/nv_python_libs/mtv/mtv_api.py @@ -307,7 +307,7 @@ class Videos(object): def textUtf8(self, text): - if text == None: + if text is None: return text try: return unicode(text, 'utf8') @@ -392,7 +392,7 @@ class Videos(object): # Make sure there are no item elements that are None for item in data: for key in item.keys(): - if item[key] == None: + if item[key] is None: item[key] = u'' # Massage each field and eliminate any item without a URL @@ -420,7 +420,7 @@ class Videos(object): if key == 'content': if len(item[key]): if item[key][0].has_key('language'): - if item[key][0]['language'] != None: + if item[key][0]['language'] is not None: item['language'] = item[key][0]['language'] if key == 'published_parsed': # '2009-12-21T00:00:00Z' if item[key]: @@ -465,7 +465,7 @@ class Videos(object): metadata = {} cur_size = True for e in etree: - if e.tag.endswith(u'content') and e.text == None: + if e.tag.endswith(u'content') and e.text is None: index = e.get('url').rindex(u':') metadata['video'] = self.mtvHtmlPath % (title, e.get('url')[index+1:]) # !! This tag will need to be added at a later date @@ -536,7 +536,7 @@ class Videos(object): sys.stderr.write(u"! Error: Unknown error during a Video search (%s)\nError(%s)\n" % (title, e)) sys.exit(1) - if data == None: + if data is None: return None if not len(data): return None @@ -696,25 +696,25 @@ class Videos(object): metadata['language'] = self.config['language'] for e in elements: if e.tag.endswith(u'title'): - if e.text != None: + if e.text is not None: metadata['title'] = self.massageDescription(e.text.strip()) else: metadata['title'] = u'' continue if e.tag == u'content': - if e.text != None: + if e.text is not None: metadata['media_description'] = self.massageDescription(e.text.strip()) else: metadata['media_description'] = u'' continue if e.tag.endswith(u'published'): # '2007-03-06T00:00:00Z' - if e.text != None: + if e.text is not None: pub_time = time.strptime(e.text.strip(), "%Y-%m-%dT%H:%M:%SZ") metadata['published_parsed'] = time.strftime('%a, %d %b %Y %H:%M:%S GMT', pub_time) else: metadata['published_parsed'] = u'' continue - if e.tag.endswith(u'content') and e.text == None: + if e.tag.endswith(u'content') and e.text is None: metadata['video'] = self.ampReplace(e.get('url')) metadata['duration'] = e.get('duration') continue diff --git a/mythtv/programs/scripts/internetcontent/nv_python_libs/rev3/rev3_api.py b/mythtv/programs/scripts/internetcontent/nv_python_libs/rev3/rev3_api.py index 08537df1c8..b3cc354b09 100644 --- a/mythtv/programs/scripts/internetcontent/nv_python_libs/rev3/rev3_api.py +++ b/mythtv/programs/scripts/internetcontent/nv_python_libs/rev3/rev3_api.py @@ -332,13 +332,13 @@ class Videos(object): tmpName = anchor.text if tmpName == u'Revision3 Beta': continue - if showURL != None: + if showURL is not None: url = etree.SubElement(tmpDirectory, "url") etree.SubElement(url, "name").text = tmpName etree.SubElement(url, "href").text = showURL etree.SubElement(url, "filter").text = showFilter etree.SubElement(url, "parserType").text = u'html' - if tmpDirectory.find('url') != None: + if tmpDirectory.find('url') is not None: showData.append(tmpDirectory) if self.config['debug_enabled']: @@ -391,11 +391,11 @@ class Videos(object): mp4Format.attrib['enabled'] = u'false' mp4Format.attrib['name'] = format.text mp4Format.attrib['rss'] = link - if tmpShow.find('mp4Format') != None: + if tmpShow.find('mp4Format') is not None: tmpDirectory.append(tmpShow) # If there is any data then add to new rev3.xml element tree - if tmpDirectory.find('show') != None: + if tmpDirectory.find('show') is not None: userRev3.append(tmpDirectory) if self.config['debug_enabled']: @@ -731,16 +731,16 @@ class Videos(object): for index in range(len(names)): names[index] = self.common.massageText(names[index]) channel = channelFilter(result)[0] - if channel.find('image') != None: + if channel.find('image') is not None: channelThumbnail = self.common.ampReplace(imageFilter(channel)[0].text) else: channelThumbnail = self.common.ampReplace(channel.find('link').text.replace(u'/watch/', u'/images/')+u'100.jpg') channelLanguage = u'en' - if channel.find('language') != None: + if channel.find('language') is not None: channelLanguage = channel.find('language').text[:2] # Create a new directory and/or subdirectory if required if names[0] != categoryDir: - if categoryDir != None: + if categoryDir is not None: channelTree.append(categoryElement) categoryElement = etree.XML(u'<directory></directory>') if names[0] == personalFeed: @@ -813,7 +813,7 @@ class Videos(object): showElement.append(rev3Item) # Add the last directory processed - if categoryElement.xpath('.//item') != None: + if categoryElement.xpath('.//item') is not None: channelTree.append(categoryElement) # Check that there was at least some items diff --git a/mythtv/programs/scripts/internetcontent/nv_python_libs/thewb/thewb_api.py b/mythtv/programs/scripts/internetcontent/nv_python_libs/thewb/thewb_api.py index 67ca0a123b..4bfc32fc3c 100644 --- a/mythtv/programs/scripts/internetcontent/nv_python_libs/thewb/thewb_api.py +++ b/mythtv/programs/scripts/internetcontent/nv_python_libs/thewb/thewb_api.py @@ -92,7 +92,7 @@ def can_int(x): >>> _can_int("A test") False """ - if x == None: + if x is None: return False try: int(x) @@ -433,7 +433,7 @@ class Videos(object): itemDwnLink = etree.XPath('.//media:content', namespaces=self.common.namespaces) itemDict = {} for result in searchResults: - if linkFilter(result) != None: # Make sure that this result actually has a video + if linkFilter(result) is not None: # Make sure that this result actually has a video thewbItem = etree.XML(self.common.mnvItem) # These videos are only viewable in the US so add a country indicator etree.SubElement(thewbItem, "{http://www.mythtv.org/wiki/MythNetvision_Grabber_Script_Format}country").text = u'us' @@ -600,11 +600,11 @@ class Videos(object): # Process any user specified searches showItems = {} - if len(showFeeds) != None: + if len(showFeeds) is not None: for searchDetails in showFeeds: try: data = self.searchTitle(searchDetails.text.strip(), 1, self.page_limit, ignoreError=True) - if data[0] == None: + if data[0] is None: continue except TheWBVideoNotFound, msg: sys.stderr.write(u"%s\n" % msg) @@ -685,7 +685,7 @@ class Videos(object): self.rssName = etree.XPath('title', namespaces=self.common.namespaces) self.feedFilter = etree.XPath('//url[text()=$url]') self.HTMLparser = etree.HTMLParser() - if rssData.find('url') != None: + if rssData.find('url') is not None: try: resultTree = self.common.getUrlData(rssData) except Exception, errormsg: diff --git a/mythtv/programs/scripts/internetcontent/nv_python_libs/vimeo/vimeo_api.py b/mythtv/programs/scripts/internetcontent/nv_python_libs/vimeo/vimeo_api.py index 37a7df966f..e87655296b 100644 --- a/mythtv/programs/scripts/internetcontent/nv_python_libs/vimeo/vimeo_api.py +++ b/mythtv/programs/scripts/internetcontent/nv_python_libs/vimeo/vimeo_api.py @@ -231,7 +231,7 @@ class SimpleOAuthClient(oauth.OAuthClient): self.authorization_url = authorization_url self.consumer = oauth.OAuthConsumer(self.key, self.secret) - if token != None and token_secret != None: + if token is not None and token_secret is not None: self.token = oauth.OAuthToken(token, token_secret) else: self.token = None @@ -325,9 +325,9 @@ class SimpleOAuthClient(oauth.OAuthClient): params = {'user_id': user_id} if sort in ('newest', 'oldest', 'alphabetical'): params['sort'] = sort - if per_page != None: + if per_page is not None: params['per_page'] = per_page - if page != None: + if page is not None: params['page'] = page return self._do_vimeo_unauthenticated_call(inspect.stack()[0][3].replace('_', '.'), parameters=params) @@ -347,9 +347,9 @@ class SimpleOAuthClient(oauth.OAuthClient): params['sort'] = sort else: params['sort'] = 'most_liked' - if per_page != None: + if per_page is not None: params['per_page'] = per_page - if page != None: + if page is not None: params['page'] = page params['full_response'] = '1' #params['query'] = query.replace(u' ', u'_') @@ -371,9 +371,9 @@ class SimpleOAuthClient(oauth.OAuthClient): if sort in ('newest', 'oldest', 'alphabetical', 'most_videos', 'most_subscribed', 'most_recently_updated'): params['sort'] = sort - if per_page != None: + if per_page is not None: params['per_page'] = per_page - if page != None: + if page is not None: params['page'] = page return self._do_vimeo_unauthenticated_call(inspect.stack()[0][3].replace('_', '.'), @@ -388,13 +388,13 @@ class SimpleOAuthClient(oauth.OAuthClient): """ # full_response channel_id params = {} - if channel_id != None: + if channel_id is not None: params['channel_id'] = channel_id - if full_response != None: + if full_response is not None: params['full_response'] = 1 - if per_page != None: + if per_page is not None: params['per_page'] = per_page - if page != None: + if page is not None: params['page'] = page return self._do_vimeo_unauthenticated_call(inspect.stack()[0][3].replace('_', '.'), @@ -824,7 +824,7 @@ class Videos(object): def textUtf8(self, text): - if text == None: + if text is None: return text try: return unicode(text, 'utf8') @@ -915,7 +915,7 @@ class Videos(object): except Exception, msg: raise VimeoVideosSearchError(u'%s' % msg) - if xml_data == None: + if xml_data is None: raise VimeoVideoNotFound(self.error_messages['VimeoVideoNotFound'] % title) if not len(xml_data.keys()): @@ -1063,7 +1063,7 @@ class Videos(object): sys.stderr.write(u"! Error: Unknown error during a Video search (%s)\nError(%s)\n" % (title, e)) sys.exit(1) - if data == None: + if data is None: return None if not len(data): return None @@ -1123,7 +1123,7 @@ class Videos(object): except Exception, msg: raise VimeoAllChannelError(u'%s' % msg) - if xml_data == None: + if xml_data is None: raise VimeoAllChannelError(self.error_messages['1-VimeoAllChannelError'] % sort) if not len(xml_data.keys()): @@ -1311,7 +1311,7 @@ class Videos(object): except Exception, msg: raise VimeoVideosSearchError(u'%s' % msg) - if xml_data == None: + if xml_data is None: raise VimeoVideoNotFound(self.error_messages['VimeoVideoNotFound'] % self.dir_name) if not len(xml_data.keys()): diff --git a/mythtv/programs/scripts/internetcontent/nv_python_libs/xsltfunctions/cinemarv_api.py b/mythtv/programs/scripts/internetcontent/nv_python_libs/xsltfunctions/cinemarv_api.py index a29076f585..1eedfa7f26 100644 --- a/mythtv/programs/scripts/internetcontent/nv_python_libs/xsltfunctions/cinemarv_api.py +++ b/mythtv/programs/scripts/internetcontent/nv_python_libs/xsltfunctions/cinemarv_api.py @@ -111,7 +111,7 @@ class xpathFunctions(object): webURL = args[0] # If this is for the download then just return what was found for the "link" element if self.persistence.has_key('cinemarvLinkGeneration'): - if self.persistence['cinemarvLinkGeneration'] != None: + if self.persistence['cinemarvLinkGeneration'] is not None: returnValue = self.persistence['cinemarvLinkGeneration'] self.persistence['cinemarvLinkGeneration'] = None return returnValue @@ -124,7 +124,7 @@ class xpathFunctions(object): except Exception, errmsg: sys.stderr.write(u'!Warning: The web page URL(%s) could not be read, error(%s)\n' % (webURL, errmsg)) return webURL - if webPageElement == None: + if webPageElement is None: self.persistence['cinemarvLinkGeneration'] = webURL return webURL @@ -147,7 +147,7 @@ class xpathFunctions(object): return True if the link does not starts with "http://" return False if the link starts with "http://" ''' - if self.persistence['cinemarvLinkGeneration'] == None: + if self.persistence['cinemarvLinkGeneration'] is None: return False if self.persistence['cinemarvLinkGeneration'].startswith(u'http://'): diff --git a/mythtv/programs/scripts/internetcontent/nv_python_libs/xsltfunctions/nasa_api.py b/mythtv/programs/scripts/internetcontent/nv_python_libs/xsltfunctions/nasa_api.py index 057da71e43..72208639be 100644 --- a/mythtv/programs/scripts/internetcontent/nv_python_libs/xsltfunctions/nasa_api.py +++ b/mythtv/programs/scripts/internetcontent/nv_python_libs/xsltfunctions/nasa_api.py @@ -124,7 +124,7 @@ class xpathFunctions(object): mythtv = "{%s}" % mythtvNamespace NSMAP = {'mythtv' : mythtvNamespace} elementTmp = etree.Element(mythtv + "mythtv", nsmap=NSMAP) - if not episodeNumber == None: + if not episodeNumber is None: etree.SubElement(elementTmp, "title").text = u"EP%02d: %s" % (episodeNumber, title) etree.SubElement(elementTmp, mythtv + "episode").text = u"%s" % episodeNumber else: diff --git a/mythtv/programs/scripts/internetcontent/nv_python_libs/xsltfunctions/skyAtNight_api.py b/mythtv/programs/scripts/internetcontent/nv_python_libs/xsltfunctions/skyAtNight_api.py index dde9f1b10c..a3cb81f693 100644 --- a/mythtv/programs/scripts/internetcontent/nv_python_libs/xsltfunctions/skyAtNight_api.py +++ b/mythtv/programs/scripts/internetcontent/nv_python_libs/xsltfunctions/skyAtNight_api.py @@ -117,7 +117,7 @@ class xpathFunctions(object): mythtv = "{%s}" % mythtvNamespace NSMAP = {'mythtv' : mythtvNamespace} elementTmp = etree.Element(mythtv + "mythtv", nsmap=NSMAP) - if not episodeNumber == None: + if not episodeNumber is None: etree.SubElement(elementTmp, "title").text = u"EP%02d" % episodeNumber etree.SubElement(elementTmp, mythtv + "episode").text = u"%s" % episodeNumber else: diff --git a/mythtv/programs/scripts/internetcontent/nv_python_libs/xsltfunctions/tributeca_api.py b/mythtv/programs/scripts/internetcontent/nv_python_libs/xsltfunctions/tributeca_api.py index a7376e0e8d..f0f80756a9 100644 --- a/mythtv/programs/scripts/internetcontent/nv_python_libs/xsltfunctions/tributeca_api.py +++ b/mythtv/programs/scripts/internetcontent/nv_python_libs/xsltfunctions/tributeca_api.py @@ -115,7 +115,7 @@ class xpathFunctions(object): # If this is for the download then just return what was found for the "link" element if self.persistence.has_key('tributecaLinkGeneration'): - if self.persistence['tributecaLinkGeneration'] != None: + if self.persistence['tributecaLinkGeneration'] is not None: returnValue = self.persistence['tributecaLinkGeneration'] self.persistence['tributecaLinkGeneration'] = None if returnValue != webURL: @@ -233,7 +233,7 @@ class xpathFunctions(object): return True if the link does not starts with "http://" return False if the link starts with "http://" ''' - if self.persistence['tributecaLinkGeneration'] == None: + if self.persistence['tributecaLinkGeneration'] is None: return False if self.persistence['tributecaLinkGeneration'].startswith(u'http://'): diff --git a/mythtv/programs/scripts/internetcontent/nv_python_libs/youtube/youtube_api.py b/mythtv/programs/scripts/internetcontent/nv_python_libs/youtube/youtube_api.py index 62fde93b2e..ff502cc9f9 100644 --- a/mythtv/programs/scripts/internetcontent/nv_python_libs/youtube/youtube_api.py +++ b/mythtv/programs/scripts/internetcontent/nv_python_libs/youtube/youtube_api.py @@ -171,7 +171,7 @@ class Videos(object): # Read region code from user preferences, used by tree view region = self.userPrefs.find("region") - if region != None and region.text: + if region is not None and region.text: self.config['region'] = region.text else: self.config['region'] = u'us' @@ -179,7 +179,7 @@ class Videos(object): self.apikey = getData().update(getData().a) apikey = self.userPrefs.find("apikey") - if apikey != None and apikey.text: + if apikey is not None and apikey.text: self.apikey = apikey.text self.feed_icons = { @@ -256,7 +256,7 @@ class Videos(object): ip = getExternalIP() - if ip == None: + if ip is None: return {} try: @@ -445,7 +445,7 @@ class Videos(object): for key in item.keys(): # Make sure there are no item elements that are None - if item[key] == None: + if item[key] is None: item[key] = u'' elif key == 'published_parsed': # 2010-01-23T08:38:39.000Z if item[key]: @@ -499,7 +499,7 @@ class Videos(object): sys.stderr.write(u"! Error: Unknown error during a Video search (%s)\nError(%s)\n" % (title, e)) sys.exit(1) - if data == None: + if data is None: return None if not len(data): return None diff --git a/mythtv/programs/scripts/metadata/Movie/tmdb3.py b/mythtv/programs/scripts/metadata/Movie/tmdb3.py index 2757e2fa7a..d82fcd6399 100755 --- a/mythtv/programs/scripts/metadata/Movie/tmdb3.py +++ b/mythtv/programs/scripts/metadata/Movie/tmdb3.py @@ -10,8 +10,8 @@ # http://help.themoviedb.org/kb/api/about-3 #----------------------- __title__ = "TheMovieDB.org V3" -__author__ = "Raymond Wagner" -__version__ = "0.3.7" +__author__ = "Raymond Wagner, Roland Ernst" +__version__ = "0.3.8" # 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 @@ -27,6 +27,7 @@ __version__ = "0.3.7" # 0.3.7 Add handling for TMDB site returning insufficient results from a # query # 0.3.7.a : Added compatibiliy to python3, tested with python 3.6 and 2.7 +# 0.3.8 Sort posters by system language or 'en', if not found for given language from optparse import OptionParser import sys @@ -45,11 +46,13 @@ def timeouthandler(signal, frame): def buildSingle(inetref, opts): from MythTV.tmdb3.tmdb_exceptions import TMDBRequestInvalid - from MythTV.tmdb3 import Movie + from MythTV.tmdb3 import Movie, get_locale from MythTV import VideoMetadata from lxml import etree + import locale as py_locale import re + if re.match('^0[0-9]{6}$', inetref): movie = Movie.fromIMDB(inetref) else: @@ -120,11 +123,42 @@ def buildSingle(inetref, opts): 'thumb':backdrop.geturl(backdrop.sizes()[0]), 'height':str(backdrop.height), 'width':str(backdrop.width)}) - for poster in movie.posters: + + # tmdb already sorts the posters by language + # if no poster of given language was found, + # try to sort by system language and then by language "en" + system_language = py_locale.getdefaultlocale()[0].split("_")[0] + locale_language = get_locale().language + if opts.debug: + print("system_language : ", system_language) + print("locale_language : ", locale_language) + + loc_posters = movie.posters + if loc_posters[0].language != locale_language \ + and locale_language != system_language: + if opts.debug: + print("1: No poster found for language '%s', trying to sort posters by '%s' :" + %(locale_language, system_language)) + loc_posters = sorted(movie.posters, + key = lambda x: x.language==system_language, reverse = True) + + if loc_posters[0].language != system_language \ + and loc_posters[0].language != locale_language: + if opts.debug: + print("2: No poster found for language '%s', trying to sort posters by '%s' :" + %(system_language, "en")) + loc_posters = sorted(movie.posters, + key = lambda x: x.language=="en", reverse = True) + + for poster in loc_posters: + if opts.debug: + print("Poster : ", poster.language, " | ", poster.userrating, + "\t | ", poster.geturl()) m.images.append({'type':'coverart', 'url':poster.geturl(), 'thumb':poster.geturl(poster.sizes()[0]), 'height':str(poster.height), 'width':str(poster.width)}) + tree.append(m.toXML()) print_etree(etree.tostring(tree, encoding='UTF-8', pretty_print=True, xml_declaration=True)) @@ -289,7 +323,7 @@ def main(): opts, args = parser.parse_args() signal.signal(signal.SIGALRM, timeouthandler) - signal.alarm(30) + signal.alarm(180) if opts.version: buildVersion() diff --git a/mythtv/programs/scripts/metadata/Music/mbutils.py b/mythtv/programs/scripts/metadata/Music/mbutils.py index 0a133fcbc6..bfe523c835 100755 --- a/mythtv/programs/scripts/metadata/Music/mbutils.py +++ b/mythtv/programs/scripts/metadata/Music/mbutils.py @@ -246,7 +246,7 @@ def find_disc(cddrive): if "offset-list" in result['disc']: offsets = None for offset in result['disc']['offset-list']: - if offsets == None: + if offsets is None: offsets = str(offset) else: offsets += " " + str(offset) @@ -358,11 +358,11 @@ def main(): performSelfTest() if opts.searchreleases: - if opts.artist == None: + if opts.artist is None: print("Missing --artist argument") sys.exit(1) - if opts.album == None: + if opts.album is None: print("Missing --album argument") sys.exit(1) @@ -373,7 +373,7 @@ def main(): search_releases(opts.artist, opts.album, limit) if opts.searchartists: - if opts.artist == None: + if opts.artist is None: print("Missing --artist argument") sys.exit(1) @@ -384,25 +384,25 @@ def main(): search_artists(opts.artist, limit) if opts.getartist: - if opts.id == None: + if opts.id is None: print("Missing --id argument") sys.exit(1) get_artist(opts.id) if opts.finddisc: - if opts.cddevice == None: + if opts.cddevice is None: print("Missing --cddevice argument") sys.exit(1) find_disc(opts.cddevice) if opts.findcoverart: - if opts.id == None and opts.relgroupid == None: + if opts.id is None and opts.relgroupid is None: print("Missing --id or --relgroupid argument") sys.exit(1) - if opts.id != None: + if opts.id is not None: find_coverart(opts.id) else: find_coverart_releasegroup(opts.relgroupid) diff --git a/mythtv/programs/scripts/metadata/Music/musicbrainzngs/util.py b/mythtv/programs/scripts/metadata/Music/musicbrainzngs/util.py index 37316f53b1..5f48e6b0d0 100644 --- a/mythtv/programs/scripts/metadata/Music/musicbrainzngs/util.py +++ b/mythtv/programs/scripts/metadata/Music/musicbrainzngs/util.py @@ -17,7 +17,7 @@ def _unicode(string, encoding=None): if isinstance(string, compat.unicode): unicode_string = string elif isinstance(string, compat.bytes): - # use given encoding, stdin, preferred until something != None is found + # use given encoding, stdin, preferred until something is not None is found if encoding is None: encoding = sys.stdin.encoding if encoding is None: diff --git a/mythtv/programs/scripts/metadata/Television/ttvdb.py b/mythtv/programs/scripts/metadata/Television/ttvdb.py index 64eab727f0..a95ae8484c 100755 --- a/mythtv/programs/scripts/metadata/Television/ttvdb.py +++ b/mythtv/programs/scripts/metadata/Television/ttvdb.py @@ -1451,7 +1451,7 @@ class Season( tvdb_api.Season ): class Episode( tvdb_api.Episode ): _re_strippart = re.compile('(.*) \([0-9]+\)') def fuzzysearch(self, term = None, key = None): - if term == None: + if term is None: raise TypeError("must supply string to search for (contents)") term = unicode(term).lower() @@ -1643,7 +1643,7 @@ def get_graphics(t, opts, series_season_ep, graphics_type, single_option, langua graphics = sorted(graphics, key=lambda k: k['rating'], reverse=True) for URL in graphics: if graphics_type == 'filename': - if URL[graphics_type] == None: + if URL[graphics_type] is None: continue if language and 'language' in URL: # Is there a language to filter URLs on? if language == URL['language']: @@ -1753,7 +1753,7 @@ def Getseries_episode_data(t, opts, series_season_ep, language = None): genres_string = series_data[u'genre'].encode('utf-8') except: genres_string='' - if genres_string != None and genres_string != '': + if genres_string is not None and genres_string != '': genres = change_amp(genres_string) genres = change_to_commas(genres) @@ -1791,7 +1791,7 @@ def Getseries_episode_data(t, opts, series_season_ep, language = None): continue i = data_keys.index(key) # Include only specific episode data except ValueError: - if series_data[season][episode][key] != None: + if series_data[season][episode][key] is not None: text = series_data[season][episode][key] if isinstance(text, dict): # handle language tuple @@ -1810,11 +1810,11 @@ def Getseries_episode_data(t, opts, series_season_ep, language = None): continue text = series_data[season][episode][key] - if text == None and key.title() == 'Director': + if text is None and key.title() == 'Director': text = u"Unknown" if isinstance(text, list): text = ', '.join(text) - if text == None or text == 'None': + if text is None or text == 'None': continue else: # handle language tuple @@ -1832,7 +1832,7 @@ def Getseries_episode_data(t, opts, series_season_ep, language = None): print(u"Title:%s" % series_data[u'seriesname']) for key in data_titles: - if key_values[index] != None: + if key_values[index] is not None: if data_titles[index] == u'ReleaseDate:' and len(key_values[index]) > 4: print(u'%s%s'% (u'Year:', key_values[index][:4])) if key_values[index] != 'None': @@ -2065,6 +2065,17 @@ def convert_series_to_xml(t, series_season_ep, ep_info): return "Banner" for show_id in t.shows.keys(): break + + # dict for 'data['_banners']['poster']['raw'] must exist for fetching coverarts, + # check with ttvdb.py -l de -a CH -D 89901 36 4 + try: + if 'poster' not in t.shows[show_id].data['_banners'].keys(): + t.shows[show_id].data['_banners']['poster'] = {} + t.shows[show_id].data['_banners']['poster']['raw'] = {} + except KeyError: + # no banner fanart exists + pass + # sort the cast into sort order t.shows[show_id].data['_actors'] = sorted(t.shows[show_id].data['_actors'], key=lambda k: k['sortOrder']) t.searchTree = None @@ -2119,7 +2130,7 @@ def displaySearchXML(tvdb_api): tvdbQueryXslt = etree.XSLT(etree.parse(u'%s%s' % (tvdb_api.baseXsltDir, u'tvdbQuery.xsl'))) items = tvdbQueryXslt(tvdb_api.searchTree) - if items.getroot() != None: + if items.getroot() is not None: if len(items.xpath('//item')): sys.stdout.write(etree.tostring(items, encoding='UTF-8', method="xml", xml_declaration=True, pretty_print=True, )) return 0 @@ -2144,7 +2155,22 @@ def displaySeriesXML(tvdb_api, series_season_ep): tvdbQueryXslt = etree.XSLT(etree.parse(u'%s%s' % (tvdb_api.baseXsltDir, u'tvdbVideo.xsl'))) items = tvdbQueryXslt(allDataElement) - if items.getroot() != None: + + # temporary fix for missing coverart: use global coverart from series + if len(items.xpath("//image[@type='coverart']")) == 0: + for el in allDataElement.iter("series"): + glob_poster = el.find("poster") + if glob_poster is not None and glob_poster.text != 'http://thetvdb.com/banners/': + glob_url = glob_poster.text + glob_thumb = glob_url.replace("posters", "_cache/posters") + glob_coverart = etree.Element("image", type = "coverart", url = glob_url, thumb = glob_thumb) + image_items = items.find("item").find("images") + if image_items is None: + etree.SubElement(items.find("item"), "images") + items.find("item").find("images").append(glob_coverart) + break + + if items.getroot() is not None: if len(items.xpath('//item')): sys.stdout.write(etree.tostring(items, encoding='UTF-8', method="xml", xml_declaration=True, pretty_print=True, )) return 0 @@ -2169,7 +2195,7 @@ def displayCollectionXML(tvdb_api): tvdbCollectionXslt = etree.XSLT(etree.parse(u'%s%s' % (tvdb_api.baseXsltDir, u'tvdbCollection.xsl'))) items = tvdbCollectionXslt(tvdb_api.seriesInfoTree) - if items.getroot() != None: + if items.getroot() is not None: if len(items.xpath('//item')): sys.stdout.write(etree.tostring(items, encoding='UTF-8', method="xml", xml_declaration=True, pretty_print=True, )) return 0 diff --git a/mythtv/version.sh b/mythtv/version.sh index fd2c0be875..d412cc0505 100755 --- a/mythtv/version.sh +++ b/mythtv/version.sh @@ -21,44 +21,64 @@ GITREPOPATH="exported" cd ${GITTREEDIR} -git status > /dev/null 2>&1 -SOURCE_VERSION=$(git describe --dirty || git describe || echo Unknown) +# if we have a mythtv/DESCRIBE file use that to get the branch and version +if test -e $GITTREEDIR/DESCRIBE ; then + echo "Using $GITTREEDIR/DESCRIBE" + . $GITTREEDIR/DESCRIBE + echo "BRANCH: $BRANCH" + echo "SOURCE_VERSION: $SOURCE_VERSION" +else + # get the branch and version from git or fall back to EXPORTED_VERSION then VERSION as last resort + git status > /dev/null 2>&1 + SOURCE_VERSION=$(git describe --dirty || git describe || echo Unknown) + echo "SOURCE_VERSION: $SOURCE_VERSION" -case "${SOURCE_VERSION}" in - exported|Unknown) - if ! grep -q Format $GITTREEDIR/EXPORTED_VERSION; then - . $GITTREEDIR/EXPORTED_VERSION - # This file has SOURCE_VERSION and BRANCH - # example SOURCE_VERSION="30d8a96" - # BRANCH examples from github - # BRANCH=" (HEAD -> master)" - # BRANCH=" (fixes/0.28)" - # BRANCH=" (tag: v0.28.1)" - # From a checkout they can be as follows: - # " (origin/fixes/0.28, fixes/0.28)" - # " (HEAD -> master, origin/master, origin/HEAD)" - # " (tag: v0.28.1)" - hash="$SOURCE_VERSION" - # This extracts after the last comma inside the parens: - BRANCH=$(echo "${BRANCH}" | sed -e 's/ (\(.*, \)\{0,1\}\(.*\))/\2/' -e 's,origin/,,') - # Create a suitable version (hash is no good) - SOURCE_VERSION="$BRANCH" - SOURCE_VERSION=`echo "$SOURCE_VERSION" | sed "s/tag: *//"` - if ! echo "$SOURCE_VERSION" | grep "^v[0-9]" ; then + case "${SOURCE_VERSION}" in + exported|Unknown) + if ! grep -q Format $GITTREEDIR/EXPORTED_VERSION; then + . $GITTREEDIR/EXPORTED_VERSION + echo "Using $GITTREEDIR/EXPORTED_VERSION" + echo "BRANCH: $BRANCH" + echo "SOURCE_VERSION: $SOURCE_VERSION" + # This file has SOURCE_VERSION and BRANCH + # example SOURCE_VERSION="30d8a96" + # BRANCH examples from github + # BRANCH=" (HEAD -> master)" + # BRANCH=" (fixes/0.28)" + # BRANCH=" (tag: v0.28.1)" + # From a checkout they can be as follows: + # " (origin/fixes/0.28, fixes/0.28)" + # " (HEAD -> master, origin/master, origin/HEAD)" + # " (tag: v0.28.1)" + hash="$SOURCE_VERSION" + # This extracts after the last comma inside the parens: + BRANCH=$(echo "${BRANCH}" | sed -e 's/ (\(.*, \)\{0,1\}\(.*\))/\2/' -e 's,origin/,,') + # Create a suitable version (hash is no good) + SOURCE_VERSION="$BRANCH" + SOURCE_VERSION=`echo "$SOURCE_VERSION" | sed "s/tag: *//"` + if ! echo "$SOURCE_VERSION" | grep "^v[0-9]" ; then + . $GITTREEDIR/VERSION + fi + SOURCE_VERSION="${SOURCE_VERSION}-${hash}" + echo "Source Version created as $SOURCE_VERSION" + echo "Branch created as $BRANCH" + elif test -e $GITTREEDIR/VERSION ; then + echo "Using $GITTREEDIR/VERSION" . $GITTREEDIR/VERSION + echo "BRANCH: $BRANCH" + echo "SOURCE_VERSION: $SOURCE_VERSION" + fi + ;; + *) + if [ -z "${BRANCH}" ]; then + BRANCH=$(git branch --no-color | sed -e '/^[^\*]/d' -e 's/^\* //' -e 's/(no branch)/exported/') + echo "Using git to get branch and version" + echo "BRANCH: $BRANCH" + echo "SOURCE_VERSION: $SOURCE_VERSION" fi - SOURCE_VERSION="${SOURCE_VERSION}-${hash}" - echo "Source Version created as $SOURCE_VERSION" - elif test -e $GITTREEDIR/VERSION ; then - . $GITTREEDIR/VERSION - fi - ;; - *) - if [ -z "${BRANCH}" ]; then - BRANCH=$(git branch --no-color | sed -e '/^[^\*]/d' -e 's/^\* //' -e 's/(no branch)/exported/') - fi - ;; -esac + ;; + esac +fi if ! echo "${SOURCE_VERSION}" | egrep -i "v[0-9]+.*" ; then # Invalid version - use VERSION file