Sophie

Sophie

distrib > Mageia > 8 > armv7hl > media > tainted-release-src > by-pkgid > cdd941b1c1e55cb531763bf651b896b0 > files > 4

mythtv-31.0-20210112.1.mga8.tainted.src.rpm

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,
-                                   &paramSize, &deviceId);
+    OSStatus err = AudioObjectGetPropertyData(kAudioObjectSystemObject, &pa,
+					      0, nullptr, &paramSize, &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,
-                                   &param_size,
-                                   nullptr);
+    pa.mSelector = kAudioDevicePropertyPreferredChannelLayout;
+    err = AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &pa,
+					 0, nullptr, &param_size);
 
     if(!err)
     {
         layout = (AudioChannelLayout *) malloc(param_size);
 
-        err = AudioUnitGetProperty(mOutputUnit,
-                                   kAudioDevicePropertyPreferredChannelLayout,
-                                   kAudioUnitScope_Output,
-                                   0,
-                                   layout,
-                                   &param_size);
+	err = AudioObjectGetPropertyData(kAudioObjectSystemObject, &pa,
+					 0, nullptr, &param_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,
-                                     &paramSize, &mFormatOrig);
+        err = AudioObjectGetPropertyData(mStreamID, &pa, 0, nullptr,
+					 &paramSize, &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,
-                           &paramSize, &currentFormat);
+    AudioObjectGetPropertyData(s, &pa, 0, nullptr,
+			       &paramSize, &currentFormat);
 
     // 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 &timestr, 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 &timestr, 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 &timestr, 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