commit 461c1103af1d6725d14fd01b580d36adedbe1d6f
Author: Richard Shaw <hobbes1069(a)gmail.com>
Date: Wed May 27 15:17:43 2020 -0500
Update to latest fixes/31, fc90482281.
Update from fixes/31 and clean up spec file conditionals.
Remove duplicate libmythavutil.so from mythtv-libs as it's already in mythffmpeg.
mythtv-ChangeLog | 133 +
mythtv.spec | 73 +-
v31.0..fc90482281.patch | 8019 +++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 8182 insertions(+), 43 deletions(-)
---
diff --git a/mythtv-ChangeLog b/mythtv-ChangeLog
index e69de29..c62720d 100644
--- a/mythtv-ChangeLog
+++ b/mythtv-ChangeLog
@@ -0,0 +1,133 @@
+commit aa63cae341f4001b5a447dc871f2d1962b883845
+Author: Philipp Matthias Hahn <pmhahn+mythtv(a)pmhahn.de>
+Date: Mon Mar 30 09:53:27 2020 +0200
+
+ Python: Update JOBTYPEs
+
+ 89b6416b50c 78 (Robert McNamara 2011-07-03 16:49:38 -0700 79)
JOB_METADATA = 0x0004,
+ ab33dd919ef 80 (John Poet 2018-03-08 16:02:25 -0700 80)
JOB_PREVIEW = 0x0008,
+
+ (cherry picked from commit f9bb4f76c864c65969ccc64541c30548c6f65344)
+
+commit c8c59f5548ce99d1248cb52e467e4c9e1100476e
+Author: Klaas de Waal <kdewaal(a)mythtv.org>
+Date: Sun Mar 22 19:00:37 2020 +0100
+
+ Updated "Remove duplicates" channel scan option
+
+ Renamed the feature from "Remove duplicate channels" to "Remove
duplicates".
+ Changed the default for this option to Selected/Checked.
+ Removed the check on individual channels across all scanned channels.
+ The implementation does not check for original network ID plus transport ID
+ on a per-transport basis, as suggested in ticket #12107 for DVB, but it checks
+ this on all channels in the transport. The implementation is also expected to work
for ATSC.
+ Thanks to John Pilkington for numerous tests in the daily changing UK Freeview
landscape.
+
+ Refs #13472
+ Fixes #12107
+
+ (cherry picked from commit 1b4d44b468de0a8c7ad2c25a1d779ce1dc2c06b8)
+ Signed-off-by: Klaas de Waal <kdewaal(a)mythtv.org>
+
+commit fec7309d231992cc88156e7fe80fd060f5639142
+Author: Klaas de Waal <kdewaal(a)mythtv.org>
+Date: Fri Mar 20 23:37:32 2020 +0100
+
+ Fix for "Remove duplicate channels" scan option
+
+ Fix counting bug in this new feature.
+ Fixed corner case in updating existing channels where
+ the same channel was present more than once in the database.
+ Improved debug output.
+
+ (cherry picked from commit e9931870756c32d3c0ba85e6ab6a6d71130a571a)
+ Signed-off-by: Klaas de Waal <kdewaal(a)mythtv.org>
+
+commit e0e09b6b69c8e95fb45d97f1a2a56d625cb2df77
+Author: Klaas de Waal <kdewaal(a)mythtv.org>
+Date: Thu Mar 19 23:55:26 2020 +0100
+
+ Scan option "Remove duplicate channels"
+
+ Add new scan option to remove duplicate transports and duplicate
+ channels based on signal strength of the received signal.
+ This can be useful when receiving DVB-T2 and other OTA signals
+ when the same channels can sometimes be received from more than
+ one transmitter on different frequencies.
+
+ Refs #13472
+
+ (cherry picked from commit d0626e90287427408b28e2b0eabe12c0cb835118)
+ Signed-off-by: Klaas de Waal <kdewaal(a)mythtv.org>
+
+commit f48478b4772547cfb67cea011a962f068a057ff6
+Author: Klaas de Waal <kdewaal(a)mythtv.org>
+Date: Mon Mar 16 23:24:11 2020 +0100
+
+ Add Full Scan option for DVB-C Netherlands
+
+ Add an entry in the frequency tables for a "Full Scan" option
+ for DVB-C in The Netherlands. There is currently only one entry
+ which is the initial tuning frequency of the Ziggo network.
+
+ (cherry picked from commit a74700c34657ef0cb99b4207f069e7881b4d948c)
+ Signed-off-by: Klaas de Waal <kdewaal(a)mythtv.org>
+
+commit d052cbc41cce4201b7a578f3a0820a9c9d3771d9
+Author: Klaas de Waal <kdewaal(a)mythtv.org>
+Date: Sun Mar 15 22:26:28 2020 +0100
+
+ Signal strength of scanned transports
+
+ Show the signal strength of the scanned transports in the transport list.
+ The transport list is shown if the "-v chanscan" option is given when
running mythtv-setup.
+ The signal strengths are useful to determine which transport to choose when
identical
+ transports can be received from different transmitters on different frequencies.
+
+ Refs #13472
+
+ (cherry picked from commit 31129946b719ff21b1a6cad86b2580ef8043a10f)
+ Signed-off-by: Klaas de Waal <kdewaal(a)mythtv.org>
+
+commit 2ef589a8d742613ebe247362366cc045855b195c
+Author: Klaas de Waal <kdewaal(a)mythtv.org>
+Date: Sun Mar 15 22:10:01 2020 +0100
+
+ T2_terrestrial_delivery_system debug output
+
+ Debug output of the T2 terrestrial delivery system descriptor added.
+ First version with only the mandatory fields.
+
+ (cherry picked from commit 8bde08adc702f05344c12d6eeb2b6c6b37255924)
+ Signed-off-by: Klaas de Waal <kdewaal(a)mythtv.org>
+
+commit 81d4056c2402882621590e7cd88ae8af5ba134aa
+Author: Klaas de Waal <kdewaal(a)mythtv.org>
+Date: Wed Mar 4 22:08:23 2020 +0100
+
+ Wait for NIT or MGT when scanning
+
+ In mythtv-setup channel scan, wait for a NIT or a MGT when the
+ PAT/PMT have been found. This solves the problem that sometimes
+ channels are found but the channel names are missing.
+
+ Refs #13472
+
+ (cherry picked from commit ac67d5837062ab47aa6f9b93df001a2a245d32ad)
+ Signed-off-by: Klaas de Waal <kdewaal(a)mythtv.org>
+
+commit f496eb12ea1eab92d9ca0e57856da66b54e9a0fa
+Author: Mark Kendall <mark.kendall(a)gmail.com>
+Date: Fri Mar 27 17:28:58 2020 +0000
+
+ libmythtv.pro: Typo
+
+ (cherry picked from commit 59b00df23e73d842552c0105a7f51c6a12de7796)
+
+commit 32ae89ef505c77cb520f1e601efef58890bdcb57
+Author: Mark Kendall <mark.kendall(a)gmail.com>
+Date: Thu Mar 26 13:49:53 2020 +0000
+
+ MythCodecContext: Ignore hardware decoders when there is no GUI
+
+ (cherry picked from commit 1e06407c6edb2f73eacb3b7bb9782bd9375912cd)
diff --git a/mythtv.spec b/mythtv.spec
index f2d57b4..3d9baf4 100644
--- a/mythtv.spec
+++ b/mythtv.spec
@@ -55,12 +55,12 @@
%global desktop_applications mythfrontend mythtv-setup
# git has used to fetch fixes diff
-%global githash 9579662cdcb020440fdb358e044f417f20b55321
+%global githash fc9048228105e0bf416990f97c3ce3c2eceb3201
%global shorthash %(c=%{githash}; echo ${c:0:10})
# MythTV Version string -- preferably the output from git describe
-%global vers_string v31.0
-%global rel_date 20200323
+%global vers_string v31.0-47-gfc90482281
+%global rel_date 20200527
%global rel_string .%{rel_date}git%{shorthash}
%global branch fixes/31
@@ -70,7 +70,7 @@
#
Name: mythtv
Version: 31.0
-Release: 2%{?dist}
+Release: 3%{?dist}%{rel_string}
Summary: A digital video recorder (DVR) application
# The primary license is GPLv2+, but bits are borrowed from a number of
@@ -78,7 +78,7 @@ Summary: A digital video recorder (DVR) application
License: GPLv2+ and LGPLv2+ and LGPLv2 and (GPLv2 or QPL) and (GPLv2+ or LGPLv2+)
URL:
http://www.mythtv.org/
Source0:
https://github.com/MythTV/%{name}/archive/v%{version}/%{name}-%{version}....
-#Patch0:
https://github.com/MythTV/%{name}/compare/v%{version}..%{shorthash}.patch
+Patch0:
https://github.com/MythTV/%{name}/compare/v%{version}..%{shorthash}.patch
Patch1: %{name}-space_in_GB.patch
@@ -118,7 +118,7 @@ Patch1: %{name}-space_in_GB.patch
%global py_prefix python
%endif
-%if 0%{?fedora} && 0%{?fedora} > 30
+%if 0%{?fedora} || 0%{?rhel} >= 8
%global py_prefix python3
%else
%global py_prefix python2
@@ -156,30 +156,28 @@ Requires(preun): systemd
Requires(postun): systemd
BuildRequires: gcc-c++ lzo-devel
+# For binary diff support
+BuildRequires: git
BuildRequires: perl-generators
BuildRequires: desktop-file-utils
BuildRequires: qt5-qtbase-devel >= 5.2
BuildRequires: qt5-qtscript-devel >= 5.2
BuildRequires: qt5-qtwebkit-devel >= 5.2
BuildRequires: freetype-devel >= 2
-%if 0%{?fedora} || 0%{?rhel} > 7
+%if 0%{?fedora} || 0%{?rhel} >= 8
BuildRequires: mariadb-connector-c-devel
%else
BuildRequires: mariadb-devel >= 5
%endif
-%if 0%{?fedora} || 0%{?rhel} >= 7
BuildRequires: libcec-devel >= 1.7
-%endif
BuildRequires: libvpx-devel
BuildRequires: lm_sensors-devel
BuildRequires: lirc-devel
BuildRequires: nasm
-Buildrequires: yasm-devel
# X, and Xv video support
BuildRequires: libXmu-devel
BuildRequires: libXv-devel
-BuildRequires: libXvMC-devel
BuildRequires: libXxf86vm-devel
BuildRequires: libXinerama-devel
BuildRequires: libXrandr-devel
@@ -188,10 +186,6 @@ BuildRequires: mesa-libGLU-devel
BuildRequires: mesa-libGLES-devel
%endif
BuildRequires: xorg-x11-proto-devel
-%ifarch %{ix86} x86_64
-BuildRequires: xorg-x11-drv-intel-devel
-BuildRequires: xorg-x11-drv-openchrome-devel
-%endif
# OpenGL video output and vsync support
BuildRequires: libGL-devel
@@ -235,15 +229,17 @@ BuildRequires: libass-devel
BuildRequires: kernel-headers
# FireWire cable box support
+%if 0%{?fedora}
BuildRequires: libavc1394-devel
BuildRequires: libiec61883-devel
BuildRequires: libraw1394-devel
+%endif
# Tuner support
BuildRequires: hdhomerun-devel
BuildRequires: %{py_prefix}-future
-%if 0%{?fedora} || 0%{?rhel} > 7
+%if 0%{?fedora} || 0%{?rhel} >= 8
# For ttvdb.py, not available in EPEL
BuildRequires: %{py_prefix}-requests
BuildRequires: %{py_prefix}-requests-cache
@@ -264,11 +260,7 @@ BuildRequires: systemd-devel
%endif
%if %{with mythgame}
- %if 0%{?fedora} >= 30
BuildRequires: minizip-compat-devel
- %else
-BuildRequires: minizip-devel
- %endif
%endif
@@ -411,7 +403,7 @@ and miscellaneous other bits and pieces.
%package libs
Summary: Library providing mythtv support
-%{?el7:BuildRequires: epel-rpm-macros}
+%{?rhel:BuildRequires: epel-rpm-macros}
Requires: freetype%{?_isa} >= 2
Requires: qt5-qtbase-mysql%{?_isa}
Requires: libudisks2%{?_isa}
@@ -435,7 +427,7 @@ Summary: Development files for mythtv
Requires: mythtv-libs%{?_isa} = %{version}-%{release}
Requires: freetype-devel%{?_isa} >= 2
-%if 0%{?fedora} || 0%{?rhel} > 7
+%if 0%{?fedora} || 0%{?rhel} >= 8
BuildRequires: mariadb-connector-c-devel
%else
BuildRequires: mariadb-devel >= 5
@@ -526,7 +518,7 @@ Requires: mythtv-common%{?_isa} = %{version}-%{release}
Requires: mythtv-base-themes%{?_isa} = %{version}-%{release}
Requires: mysql%{?_isa} >= 5
Requires: %{py_prefix}-MythTV = %{version}-%{release}
-%if 0%{?fedora} || 0%{?rhel} > 7
+%if 0%{?fedora} || 0%{?rhel} >= 8
Recommends: libaacs%{?_isa}
%else
Requires: libaacs%{?_isa}
@@ -553,7 +545,7 @@ Requires: mythtv-common%{?_isa} = %{version}-%{release}
Requires: mythtv-libs%{?_isa} = %{version}-%{release}
Requires: mythtv-setup%{?_isa}
Requires: %{py_prefix}-future
-%if 0%{?fedora} || 0%{?rhel} > 7
+%if 0%{?fedora} || 0%{?rhel} >= 8
Requires: %{py_prefix}-requests
Requires: %{py_prefix}-requests-cache
%else
@@ -594,7 +586,7 @@ mythtv backend.
Summary: Common components needed by multiple other MythTV components
# For ttvdb.py
Requires: %{py_prefix}-future
-%if 0%{?fedora} || 0%{?rhel} > 7
+%if 0%{?fedora} || 0%{?rhel} >= 8
Requires: %{py_prefix}-requests
Requires: %{py_prefix}-requests-cache
%else
@@ -656,7 +648,7 @@ Provides a PHP-based interface to interacting with MythTV.
%package -n %{py_prefix}-MythTV
Summary: Python bindings for MythTV
-%if 0%{?fedora} > 30
+%if 0%{?fedora} || 0%{?rhel} >= 8
%{?python_provide:%python_provide python3-%{name}}
Obsoletes: python2-MythTV < 30.0-9.20190601git6bd8cd4993
%else
@@ -665,7 +657,7 @@ Obsoletes: python2-MythTV < 30.0-9.20190601git6bd8cd4993
%endif
BuildArch: noarch
-%if 0%{?fedora} || 0%{?rhel} > 7
+%if 0%{?fedora} || 0%{?rhel} >= 8
Requires: %{py_prefix}-mysql
Requires: %{py_prefix}-lxml
%else
@@ -729,7 +721,7 @@ Requires: dvdauthor%{?_isa} >= 0.6.11
Requires: ffmpeg%{?_isa} >= 0.4.9
Requires: mjpegtools%{?_isa} >= 1.6.2
Requires: genisoimage%{?_isa}
-%if 0%{?fedora} || 0%{?rhel} > 7
+%if 0%{?fedora} || 0%{?rhel} >= 8
Requires: %{py_prefix}-mysql
Requires: %{py_prefix}-pillow
%else
@@ -832,8 +824,6 @@ Requires: %{py_prefix}-pycurl
Requires: %{py_prefix} >= 2.5
Requires: %{py_prefix}-lxml
Requires: %{py_prefix}-urllib3
-# This is packaged in adobe's yum repo
-#Requires: flash-plugin
%description -n mythnetvision
A MythTV module that supports searching and browsing of Internet video
@@ -850,8 +840,6 @@ on demand content.
%prep
%autosetup -p1 -n %{name}-%{version}
-# Remove compiled python file
-#find -name *.pyc -exec rm -f {} \;
# Remove exe permissions
find . -type f -name "*.cpp" -exec chmod 0644 '{}' \;
@@ -879,11 +867,6 @@ EOF
cp -a %{SOURCE106} %{SOURCE107} %{SOURCE108} %{SOURCE109} .
popd
-#pushd mythplugins
-#sed -i "s|mysql\/mysql.h|mariadb\/mysql.h|g" configure
mythzoneminder/mythzmserver/zmserver.h mythmusic/contrib/import/itunes/it2m.h
-#popd
-
-
################################################################################
%build
@@ -907,7 +890,7 @@ pushd mythtv
--disable-vaapi \
%endif
--enable-bdjava \
-%if 0%{?fedora} > 30
+%if 0%{?fedora} || 0%{?rhel} >= 8
--python=%{__python3} \
%else
--python=%{__python2} \
@@ -1012,7 +995,7 @@ pushd mythplugins
--disable-mythnetvision \
%endif
--enable-opengl \
-%if 0%{?fedora} > 30
+%if 0%{?fedora} || 0%{?rhel} >= 8
--python=%{__python3} \
%else
--python=%{__python2} \
@@ -1111,8 +1094,8 @@ popd
# And back to the build/install root
%endif
-# Fixes ERROR: ambiguous python shebang in F30
-%if 0%{?fedora} > 30
+# Fixes ERROR: ambiguous python shebang
+%if 0%{?fedora} || 0%{?rhel} >= 8
find %{buildroot}%{_datadir}/mythtv/ -type f -name "*.py" -exec sed -i
'1s:#!/usr/bin/env python$:#!%{__python3}:' {} ';'
find %{buildroot}%{_datadir}/mythtv/ -type f -name "*.py" -exec sed -i
'1s:#!/usr/bin/python$:#!%{__python3}:' {} ';'
%else
@@ -1245,7 +1228,6 @@ exit 0
%files libs
%{_libdir}/libmyth-31.so.*
-%{_libdir}/libmythavutil.so.*
%{_libdir}/libmythbase-31.so.*
%{_libdir}/libmythfreemheg-31.so.*
%{_libdir}/libmythmetadata-31.so.*
@@ -1287,7 +1269,7 @@ exit 0
%files -n %{py_prefix}-MythTV
%{_bindir}/mythpython
%{_bindir}/mythwikiscripts
-%if 0%{?fedora} > 30
+%if 0%{?fedora} || 0%{?rhel} >= 8
%{python3_sitelib}/MythTV/
%{python3_sitelib}/MythTV-*.egg-info
%else
@@ -1396,6 +1378,11 @@ exit 0
%changelog
+* Wed May 27 2020 Richard Shaw <hobbes1069(a)gmail.com> -
31.0-3.20200527gitfc90482281
+- Update to latest fixes/31, fc90482281.
+- Update from fixes/31 and clean up spec file conditionals.
+- Remove duplicate libmythavutil.so from mythtv-libs as it's already in mythffmpeg.
+
* Sat Apr 11 2020 Leigh Scott <leigh123linux(a)gmail.com> - 31.0-2
- Rebuild for new libcdio version
diff --git a/v31.0..fc90482281.patch b/v31.0..fc90482281.patch
new file mode 100644
index 0000000..df40cdd
--- /dev/null
+++ b/v31.0..fc90482281.patch
@@ -0,0 +1,8019 @@
+From 32ae89ef505c77cb520f1e601efef58890bdcb57 Mon Sep 17 00:00:00 2001
+From: Mark Kendall <mark.kendall(a)gmail.com>
+Date: Thu, 26 Mar 2020 13:49:53 +0000
+Subject: [PATCH 01/47] MythCodecContext: Ignore hardware decoders when there
+ is no GUI
+
+(cherry picked from commit 1e06407c6edb2f73eacb3b7bb9782bd9375912cd)
+---
+ mythtv/libs/libmythtv/decoders/mythcodeccontext.cpp | 6 ++++++
+ 1 file changed, 6 insertions(+)
+
+diff --git a/mythtv/libs/libmythtv/decoders/mythcodeccontext.cpp
b/mythtv/libs/libmythtv/decoders/mythcodeccontext.cpp
+index 0880bf8212f..5932fafc780 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())
+
+From f496eb12ea1eab92d9ca0e57856da66b54e9a0fa Mon Sep 17 00:00:00 2001
+From: Mark Kendall <mark.kendall(a)gmail.com>
+Date: Fri, 27 Mar 2020 17:28:58 +0000
+Subject: [PATCH 02/47] libmythtv.pro: Typo
+
+(cherry picked from commit 59b00df23e73d842552c0105a7f51c6a12de7796)
+---
+ mythtv/libs/libmythtv/libmythtv.pro | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/mythtv/libs/libmythtv/libmythtv.pro b/mythtv/libs/libmythtv/libmythtv.pro
+index 036228f2d31..f1b0248d220 100644
+--- a/mythtv/libs/libmythtv/libmythtv.pro
++++ b/mythtv/libs/libmythtv/libmythtv.pro
+@@ -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
+
+From 81d4056c2402882621590e7cd88ae8af5ba134aa Mon Sep 17 00:00:00 2001
+From: Klaas de Waal <kdewaal(a)mythtv.org>
+Date: Wed, 4 Mar 2020 22:08:23 +0100
+Subject: [PATCH 03/47] Wait for NIT or MGT when scanning
+
+In mythtv-setup channel scan, wait for a NIT or a MGT when the
+PAT/PMT have been found. This solves the problem that sometimes
+channels are found but the channel names are missing.
+
+Refs #13472
+
+(cherry picked from commit ac67d5837062ab47aa6f9b93df001a2a245d32ad)
+Signed-off-by: Klaas de Waal <kdewaal(a)mythtv.org>
+---
+ mythtv/libs/libmythtv/channelscan/channelscan_sm.cpp | 11 ++++++++---
+ 1 file changed, 8 insertions(+), 3 deletions(-)
+
+diff --git a/mythtv/libs/libmythtv/channelscan/channelscan_sm.cpp
b/mythtv/libs/libmythtv/channelscan/channelscan_sm.cpp
+index 52c6bb1d606..0f2ebae0744 100644
+--- a/mythtv/libs/libmythtv/channelscan/channelscan_sm.cpp
++++ b/mythtv/libs/libmythtv/channelscan/channelscan_sm.cpp
+@@ -907,6 +907,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 +932,7 @@ bool ChannelScanSM::UpdateChannelInfo(bool wait_until_complete)
+ {
+ uint tsid = dtv_sm->GetTransportID();
+ LOG(VB_CHANSCAN, LOG_INFO, LOC +
+- QString("transport_tune_complete: ") +
++ QString("transport_tune_complete: wait_until_complete
%1").arg(wait_until_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 +957,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 &&
+
+From 2ef589a8d742613ebe247362366cc045855b195c Mon Sep 17 00:00:00 2001
+From: Klaas de Waal <kdewaal(a)mythtv.org>
+Date: Sun, 15 Mar 2020 22:10:01 +0100
+Subject: [PATCH 04/47] T2_terrestrial_delivery_system debug output
+
+Debug output of the T2 terrestrial delivery system descriptor added.
+First version with only the mandatory fields.
+
+(cherry picked from commit 8bde08adc702f05344c12d6eeb2b6c6b37255924)
+Signed-off-by: Klaas de Waal <kdewaal(a)mythtv.org>
+---
+ mythtv/libs/libmythtv/mpeg/dvbdescriptors.cpp | 12 +++++++
+ mythtv/libs/libmythtv/mpeg/dvbdescriptors.h | 31 +++++++++++++++++++
+ .../libs/libmythtv/mpeg/mpegdescriptors.cpp | 4 +++
+ 3 files changed, 47 insertions(+)
+
+diff --git a/mythtv/libs/libmythtv/mpeg/dvbdescriptors.cpp
b/mythtv/libs/libmythtv/mpeg/dvbdescriptors.cpp
+index 1212bbc9cf5..58f5b372df0 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 b6d57187628..4ccf33bcd82 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 e53a24e2c15..7d026ef10b1 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 &&
+
+From d052cbc41cce4201b7a578f3a0820a9c9d3771d9 Mon Sep 17 00:00:00 2001
+From: Klaas de Waal <kdewaal(a)mythtv.org>
+Date: Sun, 15 Mar 2020 22:26:28 +0100
+Subject: [PATCH 05/47] Signal strength of scanned transports
+
+Show the signal strength of the scanned transports in the transport list.
+The transport list is shown if the "-v chanscan" option is given when running
mythtv-setup.
+The signal strengths are useful to determine which transport to choose when identical
+transports can be received from different transmitters on different frequencies.
+
+Refs #13472
+
+(cherry picked from commit 31129946b719ff21b1a6cad86b2580ef8043a10f)
+Signed-off-by: Klaas de Waal <kdewaal(a)mythtv.org>
+---
+ .../libmythtv/channelscan/channelimporter.cpp | 1 +
+ .../libmythtv/channelscan/channelscan_sm.cpp | 36 ++++++++++---------
+ .../libmythtv/channelscan/channelscan_sm.h | 1 +
+ .../channelscan/frequencytablesetting.cpp | 1 +
+ mythtv/libs/libmythtv/dtvmultiplex.cpp | 4 +--
+ mythtv/libs/libmythtv/dtvmultiplex.h | 1 +
+ mythtv/libs/libmythtv/frequencytables.h | 1 +
+ .../libs/libmythtv/recorders/signalmonitor.h | 1 +
+ mythtv/libs/libmythtv/ringbuffer.cpp | 2 +-
+ 9 files changed, 28 insertions(+), 20 deletions(-)
+
+diff --git a/mythtv/libs/libmythtv/channelscan/channelimporter.cpp
b/mythtv/libs/libmythtv/channelscan/channelimporter.cpp
+index 5670c1e7ad8..623200543d1 100644
+--- a/mythtv/libs/libmythtv/channelscan/channelimporter.cpp
++++ b/mythtv/libs/libmythtv/channelscan/channelimporter.cpp
+@@ -1447,6 +1447,7 @@ QString ChannelImporter::FormatTransport(
+ QString msg;
+ QTextStream ssMsg(&msg);
+ ssMsg << transport.toString();
++ ssMsg << QString(" ss:%1").arg(transport.m_signalStrength);
+ return msg;
+ }
+
+diff --git a/mythtv/libs/libmythtv/channelscan/channelscan_sm.cpp
b/mythtv/libs/libmythtv/channelscan/channelscan_sm.cpp
+index 0f2ebae0744..c2ec52d199c 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)
+@@ -932,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: wait_until_complete
%1").arg(wait_until_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()) +
+@@ -1026,6 +1028,7 @@ 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();
+
+ if (m_scanDTVTunerType == DTVTunerType::kTunerTypeDVBT2)
+ {
+@@ -1036,8 +1039,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) +
+@@ -1135,9 +1139,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);
+
+@@ -1742,6 +1746,7 @@ 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;
+
+ QMap<uint,ChannelInsertInfo>::iterator dbchan_it;
+ for (dbchan_it = pnum_to_dbchan.begin();
+@@ -1814,7 +1819,6 @@ ScanDTVTransportList ChannelScanSM::GetChannelList(bool addFullTS)
const
+ return list;
+ }
+
+-
+ DTVSignalMonitor* ChannelScanSM::GetDTVSignalMonitor(void)
+ {
+ return dynamic_cast<DTVSignalMonitor*>(m_signalMonitor);
+@@ -1945,7 +1949,6 @@ bool ChannelScanSM::HasTimedOut(void)
+ }
+ #endif // USING_DVB
+
+-
+ // have the tables have timed out?
+ if (m_timer.hasExpired(m_channelTimeout))
+ {
+@@ -2052,8 +2055,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());
+ }
+@@ -2364,8 +2366,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 f0e50e65aaa..d809b0ccec8 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/frequencytablesetting.cpp
b/mythtv/libs/libmythtv/channelscan/frequencytablesetting.cpp
+index cd7027917a5..99ca0649b85 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/dtvmultiplex.cpp
b/mythtv/libs/libmythtv/dtvmultiplex.cpp
+index 26ac36c3d39..6477d526e1e 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;
+ }
+diff --git a/mythtv/libs/libmythtv/dtvmultiplex.h b/mythtv/libs/libmythtv/dtvmultiplex.h
+index 8bb3c2db4c7..608f9bf37a7 100644
+--- a/mythtv/libs/libmythtv/dtvmultiplex.h
++++ b/mythtv/libs/libmythtv/dtvmultiplex.h
+@@ -136,6 +136,7 @@ class MTV_PUBLIC ScanDTVTransport : public DTVMultiplex
+ DTVTunerType m_tuner_type {DTVTunerType::kTunerTypeUnknown};
+ uint m_cardid {0};
+ ChannelInsertInfoList m_channels;
++ int m_signalStrength {0};
+ };
+ using ScanDTVTransportList = vector<ScanDTVTransport>;
+
+diff --git a/mythtv/libs/libmythtv/frequencytables.h
b/mythtv/libs/libmythtv/frequencytables.h
+index 7eead33d7cc..e7ba30e6756 100644
+--- a/mythtv/libs/libmythtv/frequencytables.h
++++ b/mythtv/libs/libmythtv/frequencytables.h
+@@ -179,6 +179,7 @@ class TransportScanItem
+ bool m_scanning {false}; ///< Probably Unnecessary
+ int m_freqOffsets[3] {0,0,0}; ///< Frequency offsets
+ unsigned m_timeoutTune {1000}; ///< Timeout to tune to a frequency
++ int m_signalStrength {0};
+
+ DTVMultiplex m_tuning; ///< Tuning info
+ IPTVTuningData m_iptvTuning; ///< IPTV Tuning info
+diff --git a/mythtv/libs/libmythtv/recorders/signalmonitor.h
b/mythtv/libs/libmythtv/recorders/signalmonitor.h
+index fbe55da09b9..bae34ecf7b8 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/ringbuffer.cpp b/mythtv/libs/libmythtv/ringbuffer.cpp
+index d51a33ef087..a6c1e2c1681 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.
+ */
+
+From f48478b4772547cfb67cea011a962f068a057ff6 Mon Sep 17 00:00:00 2001
+From: Klaas de Waal <kdewaal(a)mythtv.org>
+Date: Mon, 16 Mar 2020 23:24:11 +0100
+Subject: [PATCH 06/47] Add Full Scan option for DVB-C Netherlands
+
+Add an entry in the frequency tables for a "Full Scan" option
+for DVB-C in The Netherlands. There is currently only one entry
+which is the initial tuning frequency of the Ziggo network.
+
+(cherry picked from commit a74700c34657ef0cb99b4207f069e7881b4d948c)
+Signed-off-by: Klaas de Waal <kdewaal(a)mythtv.org>
+---
+ mythtv/libs/libmythtv/cardutil.cpp | 2 ++
+ mythtv/libs/libmythtv/dtvmultiplex.h | 2 +-
+ mythtv/libs/libmythtv/frequencytables.cpp | 11 ++++++++++-
+ 3 files changed, 13 insertions(+), 2 deletions(-)
+
+diff --git a/mythtv/libs/libmythtv/cardutil.cpp b/mythtv/libs/libmythtv/cardutil.cpp
+index cdc9aea470a..f9098e7d911 100644
+--- a/mythtv/libs/libmythtv/cardutil.cpp
++++ b/mythtv/libs/libmythtv/cardutil.cpp
+@@ -2704,7 +2704,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/dtvmultiplex.h b/mythtv/libs/libmythtv/dtvmultiplex.h
+index 608f9bf37a7..0e018d92776 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
+diff --git a/mythtv/libs/libmythtv/frequencytables.cpp
b/mythtv/libs/libmythtv/frequencytables.cpp
+index 55e021fa5cb..26416adfb74 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,
+
+From e0e09b6b69c8e95fb45d97f1a2a56d625cb2df77 Mon Sep 17 00:00:00 2001
+From: Klaas de Waal <kdewaal(a)mythtv.org>
+Date: Thu, 19 Mar 2020 23:55:26 +0100
+Subject: [PATCH 07/47] Scan option "Remove duplicate channels"
+
+Add new scan option to remove duplicate transports and duplicate
+channels based on signal strength of the received signal.
+This can be useful when receiving DVB-T2 and other OTA signals
+when the same channels can sometimes be received from more than
+one transmitter on different frequencies.
+
+Refs #13472
+
+(cherry picked from commit d0626e90287427408b28e2b0eabe12c0cb835118)
+Signed-off-by: Klaas de Waal <kdewaal(a)mythtv.org>
+---
+ .../libmythtv/channelscan/channelimporter.cpp | 232 +++++++++++++++++-
+ .../libmythtv/channelscan/channelimporter.h | 40 ++-
+ .../channelscan/channelscanmiscsettings.h | 16 ++
+ .../libmythtv/channelscan/channelscanner.cpp | 2 +
+ .../libmythtv/channelscan/channelscanner.h | 4 +
+ .../channelscan/channelscanner_cli.cpp | 2 +-
+ .../channelscan/channelscanner_gui.cpp | 2 +-
+ .../channelscan/scanwizardconfig.cpp | 7 +
+ .../libmythtv/channelscan/scanwizardconfig.h | 1 +
+ mythtv/libs/libmythtv/scanwizard.cpp | 2 +
+ mythtv/libs/libmythtv/scanwizard.h | 2 +
+ mythtv/programs/mythtv-setup/main.cpp | 11 +-
+ 12 files changed, 284 insertions(+), 37 deletions(-)
+
+diff --git a/mythtv/libs/libmythtv/channelscan/channelimporter.cpp
b/mythtv/libs/libmythtv/channelscan/channelimporter.cpp
+index 623200543d1..5f15d1f1b16 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,17 +104,50 @@ void ChannelImporter::Process(const ScanDTVTransportList
&_transports,
+ if (m_doSave)
+ saved_scan = SaveScan(transports);
+
+- CleanupDuplicates(transports);
++ // Merge transports with the same frequency into one
++ MergeSameFrequency(transports);
++
++ // Remove duplicate transports with a lower signal strength.
++ ScanDTVTransportList duplicateTransports;
++ if (m_removeDuplicates)
++ {
++ ScanDTVTransportList duplicates;
++ RemoveDuplicateTransports(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;
++ }
++ }
++ }
+
++ // Remove the channels that do not pass various criteria.
+ FilterServices(transports);
+
+- // Print out each transport
+- uint transports_scanned_size = transports.size();
+- if (VERBOSE_LEVEL_CHECK(VB_CHANSCAN, LOG_ANY))
++ // When there are duplicate channels remove the channels that are received
++ // on the transport with the lowest signal strength.
++ if (m_removeDuplicates)
+ {
+- cout << endl;
+- cout << "Transport list (" << transports_scanned_size
<< "):" << endl;
+- cout << FormatTransports(transports).toLatin1().constData() <<
endl;
++ ScanDTVTransportList duplicates;
++ RemoveDuplicateChannels(transports, duplicates);
++ if (VERBOSE_LEVEL_CHECK(VB_CHANSCAN, LOG_ANY))
++ {
++ if (duplicates.size() > 0)
++ {
++ cout << endl;
++ cout << "Transports with discarded duplicate channels ("
<< duplicates.size() << "):" << endl;
++ cout << FormatTransports(duplicates).toLatin1().constData()
<< endl;
++ cout << endl;
++ cout << "Discarded duplicate channels (";
++ cout << SimpleCountChannels(duplicates) << "):"
<< endl;
++ ChannelImporterBasicStats infoA = CollectStats(duplicates);
++ cout << FormatChannels(transports,
&infoA).toLatin1().constData() << endl;
++ cout << endl;
++ }
++ }
+ }
+
+ // Pull in DB info in transports
+@@ -114,7 +159,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 +169,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;
+@@ -158,7 +203,7 @@ void ChannelImporter::Process(const ScanDTVTransportList
&_transports,
+ cout << FormatChannels(transports, &info).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 +950,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,15 +1000,173 @@ 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::RemoveDuplicateTransports
++//
++// 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.
++//
++void ChannelImporter::RemoveDuplicateTransports(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_DEBUG, 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");
++ LOG(VB_CHANSCAN, LOG_INFO, LOC + "Transport A " +
FormatTransport(transports[i]));
++ LOG(VB_CHANSCAN, LOG_INFO, LOC + "Transport B " +
FormatTransport(transports[j]));
++ LOG(VB_CHANSCAN, LOG_INFO, LOC + "Discarding " +
FormatTransport(transports[lowss]));
++ }
++ }
++ }
++ if (!ignore[i])
++ {
++ no_dups.push_back(transports[i]);
++ }
++ }
+
+ transports = no_dups;
+ }
+
++// ChannelImporter::RemoveDuplicateChannels
++//
++// When there are identical channels that are present on different transports
++// then remove the channel that is received on the transport with the weakest signal.
++//
++void ChannelImporter::RemoveDuplicateChannels(ScanDTVTransportList &transports,
ScanDTVTransportList &duplicates)
++{
++ LOG(VB_CHANSCAN, LOG_INFO, LOC +
++ QString("%1 for %2
transports").arg(__func__).arg(transports.size()));
++
++ // Flag the duplicate channels in this map.
++ // The key is transport index (16 bits, shifted to the left) plus channel index (16
bits).
++ // This works if there are max 65536 transports and max 65535 channels per
transport.
++ QMap<uint,bool> dup_chan;
++
++ // Compare each channel with every channel in the other transports.
++ // We do not compare against channels in the same transport as it is unlikely
++ // to find a duplicate in the same transport and also we cannot make a selection
++ // based on the signal strength when there is a duplicate in the same transport.
++ for (size_t ita = 0; ita < transports.size(); ++ita) // All
transports in the list
++ {
++ ScanDTVTransport &ta = transports[ita]; // Transport
A is one transport from the list
++ for (size_t ica = 0; ica < ta.m_channels.size(); ++ica) // All
channels in transport A
++ {
++ ChannelInsertInfo &ca = ta.m_channels[ica]; // Channel A
is one channel from transport A
++ for (size_t itb = ita + 1; itb < transports.size(); ++itb) // All
transports above transport A
++ {
++ ScanDTVTransport &tb = transports[itb]; // Transport
B is one transport from the list
++ for (size_t icb = 0; icb < tb.m_channels.size(); ++icb) // All
channels in transport B
++ {
++ ChannelInsertInfo &cb = tb.m_channels[icb]; // Channel B
is one channel from transport B
++ if (ca.IsSameChannel(cb, 1)) // Are Channel A
and Channel B duplicate?
++ {
++ LOG(VB_CHANSCAN, LOG_INFO, LOC + "Duplicate channels:
" +
++ "\n\t" + FormatTransport(ta) + " " +
FormatChannel(ta, ca) +
++ "\n\t" + FormatTransport(tb) + " " +
FormatChannel(tb, cb));
++ if (ta.m_signalStrength < tb.m_signalStrength) // Yes,
compare signal strength of transports
++ {
++ dup_chan[(ita<<16)+ica] = true; // Flag
Channel A as duplicate
++ }
++ else
++ {
++ dup_chan[(itb<<16)+icb] = true; // Flag
Channel B as duplicate
++ }
++ }
++ }
++ }
++ }
++ }
++
++ // Go throught the list and copy the channels we keep to no_duplicates and
++ // copy the channels that are discarded to the duplicates.
++ ScanDTVTransportList no_duplicates;
++ for (size_t ita = 0; ita < transports.size(); ++ita) // All
transports in the list
++ {
++ ScanDTVTransport &ta = transports[ita]; // One
transport from the list
++ ChannelInsertInfoList ch_dup;
++ ChannelInsertInfoList ch_nodup;
++ for (size_t ica = 0; ica < ta.m_channels.size(); ++ica) // All
channels in this transport
++ {
++ ChannelInsertInfo &ca = ta.m_channels[ica]; // One
channel from this transport
++ if (dup_chan[(ita<<16)+ica]) // Channel
flagged as duplicate?
++ {
++ ch_dup.push_back(ca); // Copy the
channel to the duplicates list
++ LOG(VB_CHANSCAN, LOG_INFO, LOC +
++ "Discard duplicate channel " +
++ FormatChannel(ta, ca));
++ }
++ else
++ {
++ ch_nodup.push_back(ca); // Copy the
channel to the no_duplicates list
++ }
++ }
++ if (ch_dup.size() > 0) // At least
one channel in this transport?
++ {
++ ScanDTVTransport tmp = ta; // Yes, put the
transport with the
++ ta.m_channels = ch_dup; // duplicate
channels in the list.
++ duplicates.push_back(tmp);
++ }
++ if (ch_nodup.size() > 0) // At leat
one non-duplicate channel in this transport?
++ {
++ ScanDTVTransport tmp = ta; // Yes, put the
transport with the
++ ta.m_channels = ch_nodup; // non-duplicate
channels in the list
++ no_duplicates.push_back(tmp);
++ }
++ }
++
++ transports = no_duplicates;
++}
++
+ void ChannelImporter::FilterServices(ScanDTVTransportList &transports) const
+ {
+ bool require_av = (m_serviceRequirements & kRequireAV) == kRequireAV;
+diff --git a/mythtv/libs/libmythtv/channelscan/channelimporter.h
b/mythtv/libs/libmythtv/channelscan/channelimporter.h
+index 92f56b1ba84..806bf8eb62d 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,9 @@ class MTV_PUBLIC ChannelImporter
+
+ static QString toString(ChannelType type);
+
+- static void CleanupDuplicates(ScanDTVTransportList &transports);
++ static void MergeSameFrequency(ScanDTVTransportList &transports);
++ static void RemoveDuplicateTransports(ScanDTVTransportList &transports,
ScanDTVTransportList &duplicates);
++ static void RemoveDuplicateChannels(ScanDTVTransportList &transports,
ScanDTVTransportList &duplicates);
+ void FilterServices(ScanDTVTransportList &transports) const;
+ ScanDTVTransportList GetDBTransports(
+ uint sourceid, ScanDTVTransportList &transports) const;
+@@ -252,26 +256,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/channelscanmiscsettings.h
b/mythtv/libs/libmythtv/channelscan/channelscanmiscsettings.h
+index e0d395f47d3..f1bd65b3a38 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 duplicate channels"));
++ setHelpText(
++ QObject::tr(
++ "If set, select the channel with the strongest signal when "
++ "there are duplicate transports and channels. "
++ "This option is useful for DVB-T2 and ATSC/OTA when the same
channel "
++ "can sometimes be received from different transmitters."));
++ setValue(false);
++ };
++};
++
+ class AddFullTS : public TransMythUICheckBoxSetting
+ {
+ public:
+diff --git a/mythtv/libs/libmythtv/channelscan/channelscanner.cpp
b/mythtv/libs/libmythtv/channelscan/channelscanner.cpp
+index a4b816a7f01..dbd671c6579 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 66e0f9b7d90..9afb92354f3 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 eae66d82165..cd7cafc6576 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 bc89b80264e..c1f7f6437e4 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/scanwizardconfig.cpp
b/mythtv/libs/libmythtv/channelscan/scanwizardconfig.cpp
+index 5b7b5b34e0f..3834cb1492d 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 6451da309bd..8d6438c8904 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/scanwizard.cpp b/mythtv/libs/libmythtv/scanwizard.cpp
+index 8d25b51910b..0c1fd9a0e8f 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 9e035282570..3c680ea1929 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/programs/mythtv-setup/main.cpp
b/mythtv/programs/mythtv-setup/main.cpp
+index 67ac7d54335..5f612cc2536 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;
+
+From fec7309d231992cc88156e7fe80fd060f5639142 Mon Sep 17 00:00:00 2001
+From: Klaas de Waal <kdewaal(a)mythtv.org>
+Date: Fri, 20 Mar 2020 23:37:32 +0100
+Subject: [PATCH 08/47] Fix for "Remove duplicate channels" scan option
+
+Fix counting bug in this new feature.
+Fixed corner case in updating existing channels where
+the same channel was present more than once in the database.
+Improved debug output.
+
+(cherry picked from commit e9931870756c32d3c0ba85e6ab6a6d71130a571a)
+Signed-off-by: Klaas de Waal <kdewaal(a)mythtv.org>
+---
+ .../libmythtv/channelscan/channelimporter.cpp | 98 ++++++++++---------
+ 1 file changed, 54 insertions(+), 44 deletions(-)
+
+diff --git a/mythtv/libs/libmythtv/channelscan/channelimporter.cpp
b/mythtv/libs/libmythtv/channelscan/channelimporter.cpp
+index 5f15d1f1b16..bfec5028844 100644
+--- a/mythtv/libs/libmythtv/channelscan/channelimporter.cpp
++++ b/mythtv/libs/libmythtv/channelscan/channelimporter.cpp
+@@ -108,7 +108,6 @@ void ChannelImporter::Process(const ScanDTVTransportList
&_transports,
+ MergeSameFrequency(transports);
+
+ // Remove duplicate transports with a lower signal strength.
+- ScanDTVTransportList duplicateTransports;
+ if (m_removeDuplicates)
+ {
+ ScanDTVTransportList duplicates;
+@@ -143,8 +142,7 @@ void ChannelImporter::Process(const ScanDTVTransportList
&_transports,
+ cout << endl;
+ cout << "Discarded duplicate channels (";
+ cout << SimpleCountChannels(duplicates) << "):"
<< endl;
+- ChannelImporterBasicStats infoA = CollectStats(duplicates);
+- cout << FormatChannels(transports,
&infoA).toLatin1().constData() << endl;
++ cout << FormatChannels(duplicates).toLatin1().constData() <<
endl;
+ cout << endl;
+ }
+ }
+@@ -200,7 +198,7 @@ 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.size(), info, stats);
+@@ -1037,13 +1035,13 @@ void
ChannelImporter::RemoveDuplicateTransports(ScanDTVTransportList &transports
+ bool found_diff = true;
+ if (ta.m_channels.size() == tb.m_channels.size())
+ {
+- LOG(VB_CHANSCAN, LOG_DEBUG, LOC + "Comparing transports "
+
++ 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)
++ if (tb.m_channels[k].IsSameChannel(ta.m_channels[k], 1))
+ {
+ found_diff = false;
+ }
+@@ -1061,10 +1059,11 @@ void
ChannelImporter::RemoveDuplicateTransports(ScanDTVTransportList &transports
+ ignore[lowss] = true;
+ duplicates.push_back(transports[lowss]);
+
+- LOG(VB_CHANSCAN, LOG_INFO, LOC + "Duplicate transports
found");
+- LOG(VB_CHANSCAN, LOG_INFO, LOC + "Transport A " +
FormatTransport(transports[i]));
+- LOG(VB_CHANSCAN, LOG_INFO, LOC + "Transport B " +
FormatTransport(transports[j]));
+- LOG(VB_CHANSCAN, LOG_INFO, LOC + "Discarding " +
FormatTransport(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]));
+ }
+ }
+ }
+@@ -1111,8 +1110,8 @@ void ChannelImporter::RemoveDuplicateChannels(ScanDTVTransportList
&transports,
+ if (ca.IsSameChannel(cb, 1)) // Are Channel A
and Channel B duplicate?
+ {
+ LOG(VB_CHANSCAN, LOG_INFO, LOC + "Duplicate channels:
" +
+- "\n\t" + FormatTransport(ta) + " " +
FormatChannel(ta, ca) +
+- "\n\t" + FormatTransport(tb) + " " +
FormatChannel(tb, cb));
++ "\n\t" + FormatTransport(ta) + " " +
FormatChannel(ta, ca) +
++ "\n\t" + FormatTransport(tb) + " " +
FormatChannel(tb, cb));
+ if (ta.m_signalStrength < tb.m_signalStrength) // Yes,
compare signal strength of transports
+ {
+ dup_chan[(ita<<16)+ica] = true; // Flag
Channel A as duplicate
+@@ -1153,13 +1152,13 @@ void
ChannelImporter::RemoveDuplicateChannels(ScanDTVTransportList &transports,
+ if (ch_dup.size() > 0) // At least
one channel in this transport?
+ {
+ ScanDTVTransport tmp = ta; // Yes, put the
transport with the
+- ta.m_channels = ch_dup; // duplicate
channels in the list.
++ tmp.m_channels = ch_dup; // duplicate
channels in the list.
+ duplicates.push_back(tmp);
+ }
+ if (ch_nodup.size() > 0) // At leat
one non-duplicate channel in this transport?
+ {
+ ScanDTVTransport tmp = ta; // Yes, put the
transport with the
+- ta.m_channels = ch_nodup; // non-duplicate
channels in the list
++ tmp.m_channels = ch_nodup; // non-duplicate
channels in the list
+ no_duplicates.push_back(tmp);
+ }
+ }
+@@ -1283,6 +1282,7 @@ ScanDTVTransportList ChannelImporter::GetDBTransports(
+ return not_in_scan;
+ }
+
++ QMap<uint,bool> found_in_scan;
+ while (query.next())
+ {
+ ScanDTVTransport db_transport;
+@@ -1296,31 +1296,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
++ }
+ }
+ }
+ }
+@@ -1333,22 +1337,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
++ }
++ }
+ }
+ }
+ }
+@@ -1365,7 +1375,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++;
+
+From c8c59f5548ce99d1248cb52e467e4c9e1100476e Mon Sep 17 00:00:00 2001
+From: Klaas de Waal <kdewaal(a)mythtv.org>
+Date: Sun, 22 Mar 2020 19:00:37 +0100
+Subject: [PATCH 09/47] Updated "Remove duplicates" channel scan option
+
+Renamed the feature from "Remove duplicate channels" to "Remove
duplicates".
+Changed the default for this option to Selected/Checked.
+Removed the check on individual channels across all scanned channels.
+The implementation does not check for original network ID plus transport ID
+on a per-transport basis, as suggested in ticket #12107 for DVB, but it checks
+this on all channels in the transport. The implementation is also expected to work for
ATSC.
+Thanks to John Pilkington for numerous tests in the daily changing UK Freeview
landscape.
+
+Refs #13472
+Fixes #12107
+
+(cherry picked from commit 1b4d44b468de0a8c7ad2c25a1d779ce1dc2c06b8)
+Signed-off-by: Klaas de Waal <kdewaal(a)mythtv.org>
+---
+ mythtv/libs/libmythtv/channelinfo.cpp | 2 +-
+ .../libmythtv/channelscan/channelimporter.cpp | 132 ++----------------
+ .../libmythtv/channelscan/channelimporter.h | 3 +-
+ .../libmythtv/channelscan/channelscan_sm.cpp | 4 +
+ .../channelscan/channelscanmiscsettings.h | 10 +-
+ mythtv/libs/libmythtv/dtvmultiplex.h | 6 +-
+ mythtv/libs/libmythtv/frequencytables.h | 5 +-
+ 7 files changed, 34 insertions(+), 128 deletions(-)
+
+diff --git a/mythtv/libs/libmythtv/channelinfo.cpp
b/mythtv/libs/libmythtv/channelinfo.cpp
+index 6a12848ba4b..114c2db6cee 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 bfec5028844..9573feb9344 100644
+--- a/mythtv/libs/libmythtv/channelscan/channelimporter.cpp
++++ b/mythtv/libs/libmythtv/channelscan/channelimporter.cpp
+@@ -111,7 +111,7 @@ void ChannelImporter::Process(const ScanDTVTransportList
&_transports,
+ if (m_removeDuplicates)
+ {
+ ScanDTVTransportList duplicates;
+- RemoveDuplicateTransports(transports, duplicates);
++ RemoveDuplicates(transports, duplicates);
+ if (VERBOSE_LEVEL_CHECK(VB_CHANSCAN, LOG_ANY))
+ {
+ if (duplicates.size() > 0)
+@@ -119,28 +119,8 @@ void ChannelImporter::Process(const ScanDTVTransportList
&_transports,
+ cout << endl;
+ cout << "Discarded duplicate transports (" <<
duplicates.size() << "):" << endl;
+ cout << FormatTransports(duplicates).toLatin1().constData()
<< endl;
+- }
+- }
+- }
+-
+- // Remove the channels that do not pass various criteria.
+- FilterServices(transports);
+-
+- // When there are duplicate channels remove the channels that are received
+- // on the transport with the lowest signal strength.
+- if (m_removeDuplicates)
+- {
+- ScanDTVTransportList duplicates;
+- RemoveDuplicateChannels(transports, duplicates);
+- if (VERBOSE_LEVEL_CHECK(VB_CHANSCAN, LOG_ANY))
+- {
+- if (duplicates.size() > 0)
+- {
+ cout << endl;
+- cout << "Transports with discarded duplicate channels ("
<< duplicates.size() << "):" << endl;
+- cout << FormatTransports(duplicates).toLatin1().constData()
<< endl;
+- cout << endl;
+- cout << "Discarded duplicate channels (";
++ cout << "With channels (";
+ cout << SimpleCountChannels(duplicates) << "):"
<< endl;
+ cout << FormatChannels(duplicates).toLatin1().constData() <<
endl;
+ cout << endl;
+@@ -148,6 +128,9 @@ void ChannelImporter::Process(const ScanDTVTransportList
&_transports,
+ }
+ }
+
++ // 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;
+@@ -1006,13 +989,17 @@ void ChannelImporter::MergeSameFrequency(ScanDTVTransportList
&transports)
+ transports = no_dups;
+ }
+
+-// ChannelImporter::RemoveDuplicateTransports
++// 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.
+ //
+-void ChannelImporter::RemoveDuplicateTransports(ScanDTVTransportList &transports,
ScanDTVTransportList &duplicates)
++// 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()));
+@@ -1041,7 +1028,7 @@ void
ChannelImporter::RemoveDuplicateTransports(ScanDTVTransportList &transports
+
+ for (size_t k = 0; found_same && k <
tb.m_channels.size(); ++k)
+ {
+- if (tb.m_channels[k].IsSameChannel(ta.m_channels[k], 1))
++ if (tb.m_channels[k].IsSameChannel(ta.m_channels[k], 0))
+ {
+ found_diff = false;
+ }
+@@ -1076,96 +1063,6 @@ void
ChannelImporter::RemoveDuplicateTransports(ScanDTVTransportList &transports
+ transports = no_dups;
+ }
+
+-// ChannelImporter::RemoveDuplicateChannels
+-//
+-// When there are identical channels that are present on different transports
+-// then remove the channel that is received on the transport with the weakest signal.
+-//
+-void ChannelImporter::RemoveDuplicateChannels(ScanDTVTransportList &transports,
ScanDTVTransportList &duplicates)
+-{
+- LOG(VB_CHANSCAN, LOG_INFO, LOC +
+- QString("%1 for %2
transports").arg(__func__).arg(transports.size()));
+-
+- // Flag the duplicate channels in this map.
+- // The key is transport index (16 bits, shifted to the left) plus channel index (16
bits).
+- // This works if there are max 65536 transports and max 65535 channels per
transport.
+- QMap<uint,bool> dup_chan;
+-
+- // Compare each channel with every channel in the other transports.
+- // We do not compare against channels in the same transport as it is unlikely
+- // to find a duplicate in the same transport and also we cannot make a selection
+- // based on the signal strength when there is a duplicate in the same transport.
+- for (size_t ita = 0; ita < transports.size(); ++ita) // All
transports in the list
+- {
+- ScanDTVTransport &ta = transports[ita]; // Transport
A is one transport from the list
+- for (size_t ica = 0; ica < ta.m_channels.size(); ++ica) // All
channels in transport A
+- {
+- ChannelInsertInfo &ca = ta.m_channels[ica]; // Channel A
is one channel from transport A
+- for (size_t itb = ita + 1; itb < transports.size(); ++itb) // All
transports above transport A
+- {
+- ScanDTVTransport &tb = transports[itb]; // Transport
B is one transport from the list
+- for (size_t icb = 0; icb < tb.m_channels.size(); ++icb) // All
channels in transport B
+- {
+- ChannelInsertInfo &cb = tb.m_channels[icb]; // Channel B
is one channel from transport B
+- if (ca.IsSameChannel(cb, 1)) // Are Channel A
and Channel B duplicate?
+- {
+- LOG(VB_CHANSCAN, LOG_INFO, LOC + "Duplicate channels:
" +
+- "\n\t" + FormatTransport(ta) + " " +
FormatChannel(ta, ca) +
+- "\n\t" + FormatTransport(tb) + " " +
FormatChannel(tb, cb));
+- if (ta.m_signalStrength < tb.m_signalStrength) // Yes,
compare signal strength of transports
+- {
+- dup_chan[(ita<<16)+ica] = true; // Flag
Channel A as duplicate
+- }
+- else
+- {
+- dup_chan[(itb<<16)+icb] = true; // Flag
Channel B as duplicate
+- }
+- }
+- }
+- }
+- }
+- }
+-
+- // Go throught the list and copy the channels we keep to no_duplicates and
+- // copy the channels that are discarded to the duplicates.
+- ScanDTVTransportList no_duplicates;
+- for (size_t ita = 0; ita < transports.size(); ++ita) // All
transports in the list
+- {
+- ScanDTVTransport &ta = transports[ita]; // One
transport from the list
+- ChannelInsertInfoList ch_dup;
+- ChannelInsertInfoList ch_nodup;
+- for (size_t ica = 0; ica < ta.m_channels.size(); ++ica) // All
channels in this transport
+- {
+- ChannelInsertInfo &ca = ta.m_channels[ica]; // One
channel from this transport
+- if (dup_chan[(ita<<16)+ica]) // Channel
flagged as duplicate?
+- {
+- ch_dup.push_back(ca); // Copy the
channel to the duplicates list
+- LOG(VB_CHANSCAN, LOG_INFO, LOC +
+- "Discard duplicate channel " +
+- FormatChannel(ta, ca));
+- }
+- else
+- {
+- ch_nodup.push_back(ca); // Copy the
channel to the no_duplicates list
+- }
+- }
+- if (ch_dup.size() > 0) // At least
one channel in this transport?
+- {
+- ScanDTVTransport tmp = ta; // Yes, put the
transport with the
+- tmp.m_channels = ch_dup; // duplicate
channels in the list.
+- duplicates.push_back(tmp);
+- }
+- if (ch_nodup.size() > 0) // At leat
one non-duplicate channel in this transport?
+- {
+- ScanDTVTransport tmp = ta; // Yes, put the
transport with the
+- tmp.m_channels = ch_nodup; // non-duplicate
channels in the list
+- no_duplicates.push_back(tmp);
+- }
+- }
+-
+- transports = no_duplicates;
+-}
+-
+ void ChannelImporter::FilterServices(ScanDTVTransportList &transports) const
+ {
+ bool require_av = (m_serviceRequirements & kRequireAV) == kRequireAV;
+@@ -1497,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") ?
+@@ -1665,6 +1561,8 @@ 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 806bf8eb62d..84ec87cb41f 100644
+--- a/mythtv/libs/libmythtv/channelscan/channelimporter.h
++++ b/mythtv/libs/libmythtv/channelscan/channelimporter.h
+@@ -143,8 +143,7 @@ class MTV_PUBLIC ChannelImporter
+ static QString toString(ChannelType type);
+
+ static void MergeSameFrequency(ScanDTVTransportList &transports);
+- static void RemoveDuplicateTransports(ScanDTVTransportList &transports,
ScanDTVTransportList &duplicates);
+- static void RemoveDuplicateChannels(ScanDTVTransportList &transports,
ScanDTVTransportList &duplicates);
++ static void RemoveDuplicates(ScanDTVTransportList &transports,
ScanDTVTransportList &duplicates);
+ void FilterServices(ScanDTVTransportList &transports) const;
+ ScanDTVTransportList GetDBTransports(
+ uint sourceid, ScanDTVTransportList &transports) const;
+diff --git a/mythtv/libs/libmythtv/channelscan/channelscan_sm.cpp
b/mythtv/libs/libmythtv/channelscan/channelscan_sm.cpp
+index c2ec52d199c..543c7e1a9cd 100644
+--- a/mythtv/libs/libmythtv/channelscan/channelscan_sm.cpp
++++ b/mythtv/libs/libmythtv/channelscan/channelscan_sm.cpp
+@@ -1029,6 +1029,8 @@ 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::kTunerTypeDVBT2)
+ {
+@@ -1747,6 +1749,8 @@ 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();
+diff --git a/mythtv/libs/libmythtv/channelscan/channelscanmiscsettings.h
b/mythtv/libs/libmythtv/channelscan/channelscanmiscsettings.h
+index f1bd65b3a38..67988838696 100644
+--- a/mythtv/libs/libmythtv/channelscan/channelscanmiscsettings.h
++++ b/mythtv/libs/libmythtv/channelscan/channelscanmiscsettings.h
+@@ -176,14 +176,14 @@ class RemoveDuplicates : public TransMythUICheckBoxSetting
+ public:
+ RemoveDuplicates()
+ {
+- setLabel(QObject::tr("Remove duplicate channels"));
++ setLabel(QObject::tr("Remove duplicates"));
+ setHelpText(
+ QObject::tr(
+- "If set, select the channel with the strongest signal when "
+- "there are duplicate transports and channels. "
+- "This option is useful for DVB-T2 and ATSC/OTA when the same
channel "
++ "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(false);
++ setValue(true);
+ };
+ };
+
+diff --git a/mythtv/libs/libmythtv/dtvmultiplex.h b/mythtv/libs/libmythtv/dtvmultiplex.h
+index 0e018d92776..d7bc90f8ae9 100644
+--- a/mythtv/libs/libmythtv/dtvmultiplex.h
++++ b/mythtv/libs/libmythtv/dtvmultiplex.h
+@@ -133,9 +133,11 @@ 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.h
b/mythtv/libs/libmythtv/frequencytables.h
+index e7ba30e6756..a2e2979ec0b 100644
+--- a/mythtv/libs/libmythtv/frequencytables.h
++++ b/mythtv/libs/libmythtv/frequencytables.h
+@@ -179,13 +179,16 @@ class TransportScanItem
+ bool m_scanning {false}; ///< Probably Unnecessary
+ int m_freqOffsets[3] {0,0,0}; ///< Frequency offsets
+ unsigned m_timeoutTune {1000}; ///< Timeout to tune to a frequency
+- int m_signalStrength {0};
+
+ DTVMultiplex m_tuning; ///< Tuning info
+ IPTVTuningData m_iptvTuning; ///< IPTV Tuning info
+ 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
+
+From aa63cae341f4001b5a447dc871f2d1962b883845 Mon Sep 17 00:00:00 2001
+From: Philipp Matthias Hahn <pmhahn+mythtv(a)pmhahn.de>
+Date: Mon, 30 Mar 2020 09:53:27 +0200
+Subject: [PATCH 10/47] Python: Update JOBTYPEs
+
+89b6416b50c 78 (Robert McNamara 2011-07-03 16:49:38 -0700 79) JOB_METADATA
= 0x0004,
+ab33dd919ef 80 (John Poet 2018-03-08 16:02:25 -0700 80) JOB_PREVIEW
= 0x0008,
+
+(cherry picked from commit f9bb4f76c864c65969ccc64541c30548c6f65344)
+---
+ mythtv/bindings/python/MythTV/static.py | 2 ++
+ 1 file changed, 2 insertions(+)
+
+diff --git a/mythtv/bindings/python/MythTV/static.py
b/mythtv/bindings/python/MythTV/static.py
+index 167d71377ac..1f9e1203f63 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
+
+From 57f25431f5a6935f6d6473323f7f66a7f6a80cc4 Mon Sep 17 00:00:00 2001
+From: Mark Kendall <mark.kendall(a)gmail.com>
+Date: Fri, 3 Apr 2020 11:09:51 +0100
+Subject: [PATCH 11/47] VAAPI: Fix direct rendering for Intel iHD series
+ drivers
+
+---
+ .../libmythtv/decoders/mythvaapicontext.cpp | 43 ++++++++++++-------
+ 1 file changed, 28 insertions(+), 15 deletions(-)
+
+diff --git a/mythtv/libs/libmythtv/decoders/mythvaapicontext.cpp
b/mythtv/libs/libmythtv/decoders/mythvaapicontext.cpp
+index 9819e981409..65bf676e3be 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));
+
+From 809ea6028d6b30315f88d6fbd3374317cedf4361 Mon Sep 17 00:00:00 2001
+From: Mark Kendall <mark.kendall(a)gmail.com>
+Date: Fri, 3 Apr 2020 11:46:43 +0100
+Subject: [PATCH 12/47] MythNVDECContext: Additional logging for decoder check
+
+---
+ .../libmythtv/decoders/mythnvdeccontext.cpp | 90 +++++++++++--------
+ 1 file changed, 55 insertions(+), 35 deletions(-)
+
+diff --git a/mythtv/libs/libmythtv/decoders/mythnvdeccontext.cpp
b/mythtv/libs/libmythtv/decoders/mythnvdeccontext.cpp
+index a54a340acc6..56f8187647c 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,13 +79,15 @@ 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)
+ {
+@@ -91,40 +98,40 @@ MythCodecID MythNVDECContext::GetSupportedCodec(AVCodecContext
**Context,
+ }
+ }
+
+- 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));
+ }
+ }
+ }
+
+From 2cd6ccb419cbab542c782f9b8df2cdfb7f406ee5 Mon Sep 17 00:00:00 2001
+From: Mark Kendall <mark.kendall(a)gmail.com>
+Date: Fri, 3 Apr 2020 17:02:09 +0100
+Subject: [PATCH 13/47] NVDEC: Fix decoder support check
+
+(cherry picked from commit a2f19766c768c5ef40f596e1edf30dc3afb6889c)
+---
+ mythtv/libs/libmythtv/decoders/mythnvdeccontext.cpp | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/mythtv/libs/libmythtv/decoders/mythnvdeccontext.cpp
b/mythtv/libs/libmythtv/decoders/mythnvdeccontext.cpp
+index 56f8187647c..04cb4601971 100644
+--- a/mythtv/libs/libmythtv/decoders/mythnvdeccontext.cpp
++++ b/mythtv/libs/libmythtv/decoders/mythnvdeccontext.cpp
+@@ -91,7 +91,7 @@ MythCodecID MythNVDECContext::GetSupportedCodec(AVCodecContext
**Context,
+ 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;
+
+From 9258fb56250392eb49abeb71785e4166b78c48fd Mon Sep 17 00:00:00 2001
+From: Paul Harrison <paul(a)mythqml.net>
+Date: Tue, 31 Mar 2020 18:25:58 +0100
+Subject: [PATCH 14/47] configure: enable by default gnutls support in our copy
+ of ffmpeg
+
+This is required to support playback from protocols using these schema :-
+https, rtmps, rtmpts and tls.
+
+Support is enabled by default if gnutls is found but can be explicitly
+disabled by passing --disable-gnutls to ./configure
+
+(cherry picked from commit 427d87b0c62d9c51a0ce8543b770abda73dfc6fc)
+---
+ mythtv/configure | 14 ++++++++++++++
+ 1 file changed, 14 insertions(+)
+
+diff --git a/mythtv/configure b/mythtv/configure
+index 77aee2d0768..3a7ec61fd74 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
+
+From ece4ff7ebc1b1383e62d11ad55bd9fa845185d90 Mon Sep 17 00:00:00 2001
+From: David Hampton <mythtv(a)love2code.net>
+Date: Tue, 7 Apr 2020 10:07:43 -0400
+Subject: [PATCH 15/47] Fix improper sorting of names that start with "An".
+
+The US English translation accidentally removed the space after this
+word, causing it to always be removed instead of just when it was a
+separate word.
+
+Fixes #13603.
+
+(cherry picked from commit 5d3743c7989813d4297ff97ba8959cf17b659889)
+---
+ mythtv/i18n/mythfrontend_en_us.qm | Bin 209928 -> 209845 bytes
+ mythtv/i18n/mythfrontend_en_us.ts | 2 +-
+ .../test_mythsorthelper.cpp | 5 +++++
+ 3 files changed, 6 insertions(+), 1 deletion(-)
+
+diff --git a/mythtv/i18n/mythfrontend_en_us.qm b/mythtv/i18n/mythfrontend_en_us.qm
+index 22d06341fa6fb59b3c6d2e590b5cbad8910a6a51..1658b5c5ffa525fb0f7f12a1ed682069e684f020
100644
+GIT binary patch
+delta 9235
+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
+
+delta 9294
+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
+Wn1LbTvqPd19hQ(nhb46N;r{~<++XMb
+
+diff --git a/mythtv/i18n/mythfrontend_en_us.ts b/mythtv/i18n/mythfrontend_en_us.ts
+index a1ff6bc1725..2c02d7ffc81 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/libmythbase/test/test_mythsorthelper/test_mythsorthelper.cpp
b/mythtv/libs/libmythbase/test/test_mythsorthelper/test_mythsorthelper.cpp
+index 478c77059ab..46fa7864631 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;
+
+From a465f1b03d505b0038ba5d40b11bb10039454733 Mon Sep 17 00:00:00 2001
+From: Klaas de Waal <kdewaal(a)mythtv.org>
+Date: Tue, 7 Apr 2020 22:33:36 +0200
+Subject: [PATCH 16/47] Use const_iterator for QMap m_encoderList
+
+Use const_iterator, constBegin, constEnd and constFind while accessing
+the tvList and pointers to the tvList such as m_encoderList and m_tvList.
+This fixes the problem of crashing while deleting a recording.
+When deleting, the m_encoderList is accessed by more than one thread.
+The QMap is documented to be re-entrant but this appears to be only the case
+when the QMap is accessed with the const variants of the member functions.
+
+Fixes #13571
+
+(cherry picked from commit 4192aab4d31301596b506c47507e8c3d872cc18f)
+Signed-off-by: Klaas de Waal <kdewaal(a)mythtv.org>
+---
+ mythtv/programs/mythbackend/autoexpire.cpp | 9 ++++----
+ mythtv/programs/mythbackend/mainserver.cpp | 26 +++++++++++-----------
+ mythtv/programs/mythbackend/scheduler.cpp | 10 ++++-----
+ 3 files changed, 22 insertions(+), 23 deletions(-)
+
+diff --git a/mythtv/programs/mythbackend/autoexpire.cpp
b/mythtv/programs/mythbackend/autoexpire.cpp
+index 6f2f309b754..efadbe08d4e 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/mainserver.cpp
b/mythtv/programs/mythbackend/mainserver.cpp
+index 76e3702a445..9d627c663b5 100644
+--- a/mythtv/programs/mythbackend/mainserver.cpp
++++ b/mythtv/programs/mythbackend/mainserver.cpp
+@@ -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;
+
+@@ -4315,8 +4315,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));
+@@ -4479,8 +4479,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 +4855,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() "
+
+@@ -4918,8 +4918,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 +
+@@ -7157,7 +7157,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 +7203,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();
+
+diff --git a/mythtv/programs/mythbackend/scheduler.cpp
b/mythtv/programs/mythbackend/scheduler.cpp
+index 7a9762ea46a..4ca86721472 100644
+--- a/mythtv/programs/mythbackend/scheduler.cpp
++++ b/mythtv/programs/mythbackend/scheduler.cpp
+@@ -2513,7 +2513,7 @@ void Scheduler::HandleWakeSlave(RecordingInfo &ri, int
prerollseconds)
+
+ QReadLocker tvlocker(&TVRec::s_inputsLock);
+
+- QMap<int, EncoderLink*>::iterator tvit = m_tvList->find(ri.GetInputID());
++ QMap<int, EncoderLink*>::const_iterator tvit =
m_tvList->constFind(ri.GetInputID());
+ if (tvit == m_tvList->end())
+ return;
+
+@@ -2671,7 +2671,7 @@ bool Scheduler::HandleRecording(
+
+ QReadLocker tvlocker(&TVRec::s_inputsLock);
+
+- QMap<int, EncoderLink*>::iterator tvit = m_tvList->find(ri.GetInputID());
++ QMap<int, EncoderLink*>::const_iterator tvit =
m_tvList->constFind(ri.GetInputID());
+ if (tvit == m_tvList->end())
+ {
+ QString msg = QString("Invalid cardid [%1] for %2")
+@@ -3078,8 +3078,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())
+@@ -3478,7 +3478,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()) &&
+
+From 3867297afe19fb6853143e9542688e66c7bc1f39 Mon Sep 17 00:00:00 2001
+From: John Poet <jpoet(a)mythtv.org>
+Date: Fri, 3 Apr 2020 10:09:18 -0600
+Subject: [PATCH 17/47] mythexternrec: Add a cleanup system command option to
+ the config file.
+
+If [RECORDER][cleanup] is defined, it will be run whenever this external
+recorder is shut down.
+
+(cherry picked from commit d5dacff66275b7e45a7c2cf3dadfadcc7fc2a87f)
+---
+ .../mythexternrecorder/MythExternControl.cpp | 6 ++++
+ .../mythexternrecorder/MythExternControl.h | 2 ++
+ .../mythexternrecorder/MythExternRecApp.cpp | 33 +++++++++++++++++++
+ .../mythexternrecorder/MythExternRecApp.h | 2 ++
+ mythtv/programs/mythexternrecorder/main.cpp | 2 ++
+ 5 files changed, 45 insertions(+)
+
+diff --git a/mythtv/programs/mythexternrecorder/MythExternControl.cpp
b/mythtv/programs/mythexternrecorder/MythExternControl.cpp
+index 3038a01dc25..a0de57140fc 100644
+--- a/mythtv/programs/mythexternrecorder/MythExternControl.cpp
++++ b/mythtv/programs/mythexternrecorder/MythExternControl.cpp
+@@ -188,6 +188,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());
+@@ -385,6 +390,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?"))
+ {
+diff --git a/mythtv/programs/mythexternrecorder/MythExternControl.h
b/mythtv/programs/mythexternrecorder/MythExternControl.h
+index c510ea1dfd6..308a9dd137b 100644
+--- a/mythtv/programs/mythexternrecorder/MythExternControl.h
++++ b/mythtv/programs/mythexternrecorder/MythExternControl.h
+@@ -106,6 +106,7 @@ class Commands : public QObject
+ void LoadChannels(const QString & serial);
+ void FirstChannel(const QString & serial);
+ void NextChannel(const QString & serial);
++ void Cleanup(void);
+
+ private:
+ std::thread m_thread;
+@@ -148,6 +149,7 @@ class MythExternControl : public QObject
+ void LoadChannels(const QString & serial);
+ void FirstChannel(const QString & serial);
+ void NextChannel(const QString & serial);
++ void Cleanup(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 a2597d2ea57..5d866922446 100644
+--- a/mythtv/programs/mythexternrecorder/MythExternRecApp.cpp
++++ b/mythtv/programs/mythexternrecorder/MythExternRecApp.cpp
+@@ -85,6 +85,7 @@ 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_channelsIni = settings.value("TUNER/channels",
"").toString();
+ m_lockTimeout = settings.value("TUNER/timeout", "").toInt();
+@@ -200,6 +201,8 @@ void MythExternRecApp::TerminateProcess(void)
+ m_proc.kill();
+ m_proc.waitForFinished();
+ }
++
++ return;
+ }
+
+ Q_SLOT void MythExternRecApp::Close(void)
+@@ -255,6 +258,36 @@ void MythExternRecApp::Run(void)
+ emit Done();
+ }
+
++Q_SLOT void MythExternRecApp::Cleanup(void)
++{
++ 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::LoadChannels(const QString & serial)
+ {
+ if (m_channelsIni.isEmpty())
+diff --git a/mythtv/programs/mythexternrecorder/MythExternRecApp.h
b/mythtv/programs/mythexternrecorder/MythExternRecApp.h
+index d09cffb4ce4..fbaa2b7596c 100644
+--- a/mythtv/programs/mythexternrecorder/MythExternRecApp.h
++++ b/mythtv/programs/mythexternrecorder/MythExternRecApp.h
+@@ -69,6 +69,7 @@ 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 LoadChannels(const QString & serial);
+ void FirstChannel(const QString & serial);
+ void NextChannel(const QString & serial);
+@@ -97,6 +98,7 @@ class MythExternRecApp : public QObject
+
+ QProcess m_proc;
+ QString m_command;
++ QString m_cleanup;
+
+ QString m_recCommand;
+ QString m_recDesc;
+diff --git a/mythtv/programs/mythexternrecorder/main.cpp
b/mythtv/programs/mythexternrecorder/main.cpp
+index 71ca26079f1..e05e047d7cc 100644
+--- a/mythtv/programs/mythexternrecorder/main.cpp
++++ b/mythtv/programs/mythexternrecorder/main.cpp
+@@ -112,6 +112,8 @@ 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::LoadChannels,
+ process, &MythExternRecApp::LoadChannels);
+ QObject::connect(control, &MythExternControl::FirstChannel,
+
+From 4f79764adab0faea6f0e76074358bef902b13f14 Mon Sep 17 00:00:00 2001
+From: John Poet <jpoet(a)mythtv.org>
+Date: Fri, 3 Apr 2020 10:09:18 -0600
+Subject: [PATCH 18/47] mythexternrec: Track channum so an unnecessary tune is
+ not issued on back-to-back recordings.
+
+(cherry picked from commit d8d3b7422b220cbecaec518b350373075797026e)
+---
+ .../mythexternrecorder/MythExternRecApp.cpp | 18 +++++++++++++++---
+ .../mythexternrecorder/MythExternRecApp.h | 2 +-
+ 2 files changed, 16 insertions(+), 4 deletions(-)
+
+diff --git a/mythtv/programs/mythexternrecorder/MythExternRecApp.cpp
b/mythtv/programs/mythexternrecorder/MythExternRecApp.cpp
+index 5d866922446..967e5e89c2a 100644
+--- a/mythtv/programs/mythexternrecorder/MythExternRecApp.cpp
++++ b/mythtv/programs/mythexternrecorder/MythExternRecApp.cpp
+@@ -260,6 +260,8 @@ void MythExternRecApp::Run(void)
+
+ Q_SLOT void MythExternRecApp::Cleanup(void)
+ {
++ m_tunedChannel.clear();
++
+ if (m_cleanup.isEmpty())
+ return;
+
+@@ -272,7 +274,8 @@ Q_SLOT void MythExternRecApp::Cleanup(void)
+ cleanup.start(cmd);
+ if (!cleanup.waitForStarted())
+ {
+- LOG(VB_RECORD, LOG_ERR, LOC + ": Failed to start cleanup process: " +
ENO);
++ LOG(VB_RECORD, LOG_ERR, LOC + ": Failed to start cleanup process: "
++ + ENO);
+ return;
+ }
+ cleanup.waitForFinished(5000);
+@@ -412,6 +415,15 @@ Q_SLOT void MythExternRecApp::NextChannel(const QString &
serial)
+ Q_SLOT void MythExternRecApp::TuneChannel(const QString & serial,
+ const QString & channum)
+ {
++ if (m_tunedChannel == channum)
++ {
++ LOG(VB_CHANNEL, LOG_INFO, LOC +
++ QString("TuneChanne: Already on %1").arg(channum));
++ emit SendMessage("TuneChannel", serial,
++ QString("OK:Tunned to %1").arg(channum));
++ return;
++ }
++
+ if (m_channelsIni.isEmpty())
+ {
+ LOG(VB_CHANNEL, LOG_ERR, LOC + ": No channels configured.");
+@@ -489,7 +501,7 @@ Q_SLOT void MythExternRecApp::TuneChannel(const QString &
serial,
+
+ LOG(VB_CHANNEL, LOG_INFO, LOC +
+ QString(": TuneChannel %1: URL '%2'").arg(channum).arg(url));
+- m_tuned = true;
++ m_tunedChannel = channum;
+
+ emit SetDescription(Desc());
+ emit SendMessage("TuneChannel", serial,
+@@ -528,7 +540,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,
+diff --git a/mythtv/programs/mythexternrecorder/MythExternRecApp.h
b/mythtv/programs/mythexternrecorder/MythExternRecApp.h
+index fbaa2b7596c..b94e01d0c86 100644
+--- a/mythtv/programs/mythexternrecorder/MythExternRecApp.h
++++ b/mythtv/programs/mythexternrecorder/MythExternRecApp.h
+@@ -117,7 +117,7 @@ class MythExternRecApp : public QObject
+ QString m_configIni;
+ QString m_desc;
+
+- bool m_tuned { false };
++ QString m_tunedChannel;
+
+ // Channel scanning
+ QSettings *m_chanSettings { nullptr };
+
+From 1244eddac0d8d65533b997eea67019aa1866acdd Mon Sep 17 00:00:00 2001
+From: John Poet <jpoet(a)mythtv.org>
+Date: Fri, 3 Apr 2020 10:09:18 -0600
+Subject: [PATCH 19/47] Dvr::AddRecordSchedule: Allow last_record to be
+ specified.
+
+Scheduler::UpdateManuals: When creating the mythconverg.program entry,
+populate program.originalairdate from record.last_record.
+
+
+A little bit of a hack, but it is the cleanest solution without adding
+another variable to mythconverg.record.
+
+(cherry picked from commit 56277c79b7474d87c31bc67785972df16620115e)
+---
+ .../libmythservicecontracts/services/dvrServices.h | 1 +
+ mythtv/programs/mythbackend/scheduler.cpp | 11 ++++++++---
+ mythtv/programs/mythbackend/services/dvr.cpp | 4 ++++
+ mythtv/programs/mythbackend/services/dvr.h | 4 +++-
+ 4 files changed, 16 insertions(+), 4 deletions(-)
+
+diff --git a/mythtv/libs/libmythservicecontracts/services/dvrServices.h
b/mythtv/libs/libmythservicecontracts/services/dvrServices.h
+index 30de373f00c..47ae6c71c98 100644
+--- a/mythtv/libs/libmythservicecontracts/services/dvrServices.h
++++ b/mythtv/libs/libmythservicecontracts/services/dvrServices.h
+@@ -210,6 +210,7 @@ class SERVICE_PUBLIC DvrServices : public Service //, public
QScriptable ???
+ uint PreferredInput,
+ int StartOffset,
+ int EndOffset,
++ QDateTime LastRecorded,
+ QString DupMethod,
+ QString DupIn,
+ uint Filter,
+diff --git a/mythtv/programs/mythbackend/scheduler.cpp
b/mythtv/programs/mythbackend/scheduler.cpp
+index 4ca86721472..42d8e1c8494 100644
+--- a/mythtv/programs/mythbackend/scheduler.cpp
++++ b/mythtv/programs/mythbackend/scheduler.cpp
+@@ -3660,7 +3660,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 +3687,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 +3757,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 +3770,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())
+ {
+diff --git a/mythtv/programs/mythbackend/services/dvr.cpp
b/mythtv/programs/mythbackend/services/dvr.cpp
+index ca9bf9759aa..07eb8e74cc5 100644
+--- a/mythtv/programs/mythbackend/services/dvr.cpp
++++ b/mythtv/programs/mythbackend/services/dvr.cpp
+@@ -1092,6 +1092,7 @@ uint Dvr::AddRecordSchedule (
+ uint nPreferredInput,
+ int nStartOffset,
+ int nEndOffset,
++ QDateTime lastrectsRaw,
+ QString sDupMethod,
+ QString sDupIn,
+ uint nFilter,
+@@ -1113,6 +1114,7 @@ uint Dvr::AddRecordSchedule (
+ {
+ QDateTime recstartts = recstarttsRaw.toUTC();
+ QDateTime recendts = recendtsRaw.toUTC();
++ QDateTime lastrects = lastrectsRaw.toUTC();
+ RecordingRule rule;
+ rule.LoadTemplate("Default");
+
+@@ -1199,6 +1201,8 @@ uint Dvr::AddRecordSchedule (
+
+ rule.m_transcoder = nTranscoder;
+
++ rule.m_lastRecorded = lastrects;
++
+ QString msg;
+ if (!rule.IsValid(msg))
+ throw msg;
+diff --git a/mythtv/programs/mythbackend/services/dvr.h
b/mythtv/programs/mythbackend/services/dvr.h
+index 7a6b1be80bc..5bf193c05ab 100644
+--- a/mythtv/programs/mythbackend/services/dvr.h
++++ b/mythtv/programs/mythbackend/services/dvr.h
+@@ -173,6 +173,7 @@ class Dvr : public DvrServices
+ uint PreferredInput,
+ int StartOffset,
+ int EndOffset,
++ QDateTime lastrectsRaw,
+ QString DupMethod,
+ QString DupIn,
+ uint Filter,
+@@ -491,7 +492,8 @@ class ScriptableDvr : public QObject
+ rule->Inetref(), rule->Type(),
+ rule->SearchType(), rule->RecPriority(),
+ rule->PreferredInput(), rule->StartOffset(),
+- rule->EndOffset(), rule->DupMethod(),
++ rule->EndOffset(), rule->LastRecorded(),
++ rule->DupMethod(),
+ rule->DupIn(), rule->Filter(),
+ rule->RecProfile(), rule->RecGroup(),
+ rule->StorageGroup(), rule->PlayGroup(),
+
+From 49e545531a8ef6383abeb76a03220ae3ee880f7b Mon Sep 17 00:00:00 2001
+From: John Poet <jpoet(a)mythtv.org>
+Date: Fri, 3 Apr 2020 10:09:18 -0600
+Subject: [PATCH 20/47] mythexternrecorder: Allow use of channum with tuning
+ command, even without a channel configuration file.
+
+(cherry picked from commit 356dd5e39a61e3a5e433508bd6afc937ca7c9e30)
+---
+ .../mythexternrecorder/MythExternControl.cpp | 4 +-
+ .../mythexternrecorder/MythExternRecApp.cpp | 95 +++++++++++--------
+ 2 files changed, 56 insertions(+), 43 deletions(-)
+
+diff --git a/mythtv/programs/mythexternrecorder/MythExternControl.cpp
b/mythtv/programs/mythexternrecorder/MythExternControl.cpp
+index a0de57140fc..0a0b933e8df 100644
+--- a/mythtv/programs/mythexternrecorder/MythExternControl.cpp
++++ b/mythtv/programs/mythexternrecorder/MythExternControl.cpp
+@@ -314,7 +314,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]);
+ }
+@@ -357,7 +357,7 @@ 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");
+diff --git a/mythtv/programs/mythexternrecorder/MythExternRecApp.cpp
b/mythtv/programs/mythexternrecorder/MythExternRecApp.cpp
+index 967e5e89c2a..494d207242a 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'")
+@@ -424,56 +424,62 @@ Q_SLOT void MythExternRecApp::TuneChannel(const QString &
serial,
+ return;
+ }
+
+- if (m_channelsIni.isEmpty())
++ if (m_tuneCommand.isEmpty())
+ {
+- LOG(VB_CHANNEL, LOG_ERR, LOC + ": No channels configured.");
+- emit SendMessage("TuneChannel", serial, "ERR:No channels
configured.");
++ LOG(VB_CHANNEL, LOG_ERR, LOC + ": No 'tuner' configured.");
++ emit SendMessage("TuneChannel", serial, "ERR:No 'tuner'
configured.");
+ return;
+ }
+
+- QSettings settings(m_channelsIni, QSettings::IniFormat);
+- settings.beginGroup(channum);
++ m_desc = m_recDesc;
++ m_command = m_recCommand;
+
+- QString url(settings.value("URL").toString());
++ QString tune = m_tuneCommand;
++ QString url;
+
+- if (url.isEmpty())
++ if (!m_channelsIni.isEmpty())
+ {
+- QString msg = QString("Channel number [%1] is missing a URL.")
+- .arg(channum);
++ QSettings settings(m_channelsIni, QSettings::IniFormat);
++ settings.beginGroup(channum);
+
+- LOG(VB_CHANNEL, LOG_ERR, LOC + ": " + msg);
++ url = settings.value("URL").toString();
+
+- emit SendMessage("TuneChannel", serial,
QString("ERR:%1").arg(msg));
+- return;
+- }
++ if (url.isEmpty())
++ {
++ QString msg = QString("Channel number [%1] is missing a URL.")
++ .arg(channum);
+
+- if (!m_tuneCommand.isEmpty())
+- {
+- // Repalce URL in command and execute it
+- QString tune = m_tuneCommand;
+- tune.replace("%URL%", url);
++ LOG(VB_CHANNEL, LOG_ERR, LOC + ": " + msg);
++ }
++ else
++ tune.replace("%URL%", url);
+
+- if (system(tune.toUtf8().constData()) != 0)
++ if (!url.isEmpty() && m_command.indexOf("%URL%") >= 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;
++ m_command.replace("%URL%", url);
++ LOG(VB_CHANNEL, LOG_DEBUG, LOC +
++ QString(": '%URL%' replaced with '%1' in cmd:
'%2'")
++ .arg(url).arg(m_command));
+ }
+- LOG(VB_CHANNEL, LOG_INFO, LOC +
+- QString(": TuneChannel, ran '%1'").arg(tune));
++
++ m_desc.replace("%CHANNAME%",
settings.value("NAME").toString());
++ m_desc.replace("%CALLSIGN%",
settings.value("CALLSIGN").toString());
++
++ settings.endGroup();
+ }
+
+- // Replace URL in recorder command
+- m_command = m_recCommand;
++ tune.replace("%CHANNUM%", channum);
++ m_command.replace("%CHANNUM%", channum);
+
+- if (!url.isEmpty() && m_command.indexOf("%URL%") >= 0)
++ if (system(tune.toUtf8().constData()) != 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));
++ 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));
+
+ if (!m_logFile.isEmpty() && m_command.indexOf("%LOGFILE%") >=
0)
+ {
+@@ -491,13 +497,8 @@ 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();
+
+ LOG(VB_CHANNEL, LOG_INFO, LOC +
+ QString(": TuneChannel %1: URL '%2'").arg(channum).arg(url));
+@@ -505,17 +506,29 @@ Q_SLOT void MythExternRecApp::TuneChannel(const QString &
serial,
+
+ emit SetDescription(Desc());
+ emit SendMessage("TuneChannel", serial,
+- QString("OK:Tunned to %1").arg(channum));
++ QString("OK:Tuned to %1").arg(channum));
+ }
+
+ 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));
+ }
+@@ -523,7 +536,7 @@ 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() ? "No" : "Yes"));
+ }
+
+ Q_SLOT void MythExternRecApp::HasPictureAttributes(const QString & serial)
+
+From a58ef59549d9accb5b874140a4a16823732bc4cf Mon Sep 17 00:00:00 2001
+From: John Poet <jpoet(a)mythtv.org>
+Date: Fri, 3 Apr 2020 10:09:18 -0600
+Subject: [PATCH 21/47] ExternalChannel: When mythbackend is startting up,
+ don't /actually/ tune a channel.
+
+Tinning with an External Recorder can take a long time. As long as the
+External Recorder can be executed, assume tinning a channel will succeed.
+
+(cherry picked from commit 7c0b1421c49173a8955f6e090fbac9fc3d280ea4)
+---
+ .../libmythtv/recorders/ExternalChannel.cpp | 37 ++++++++++++++-----
+ .../libmythtv/recorders/ExternalChannel.h | 1 +
+ 2 files changed, 29 insertions(+), 9 deletions(-)
+
+diff --git a/mythtv/libs/libmythtv/recorders/ExternalChannel.cpp
b/mythtv/libs/libmythtv/recorders/ExternalChannel.cpp
+index a8946108710..c1620a1dccd 100644
+--- a/mythtv/libs/libmythtv/recorders/ExternalChannel.cpp
++++ b/mythtv/libs/libmythtv/recorders/ExternalChannel.cpp
+@@ -98,18 +98,37 @@ 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();
++ UpdateDescription();
++ }
+
+ return true;
+ }
+diff --git a/mythtv/libs/libmythtv/recorders/ExternalChannel.h
b/mythtv/libs/libmythtv/recorders/ExternalChannel.h
+index 243934301ee..1a7fc75a7aa 100644
+--- a/mythtv/libs/libmythtv/recorders/ExternalChannel.h
++++ b/mythtv/libs/libmythtv/recorders/ExternalChannel.h
+@@ -52,6 +52,7 @@ class ExternalChannel : public DTVChannel
+ { return true; }
+
+ private:
++ int m_tuneTimeout { -1 };
+ QString m_device;
+ QStringList m_args;
+ ExternalStreamHandler *m_streamHandler {nullptr};
+
+From 74544819067773869c87c4b1974e46dc9ad9e41f Mon Sep 17 00:00:00 2001
+From: John Poet <jpoet(a)mythtv.org>
+Date: Fri, 3 Apr 2020 10:09:18 -0600
+Subject: [PATCH 22/47] MythExternRecorder: Add support for long channel change
+ times.
+
+Add support for the external application to respond with "OK:Running" in
+response to a tuning request. The external application must then respond to
+"TuningStatus" message.
+
+This keeps LiveTV from timing out after 7 seconds when the tuning command
+takes a long time. It also lets mythbackend start up much faster, since it
+doesn't have to wait for a tuning command to complete before initializing
+the next tuner.
+
+(cherry picked from commit 9a973f5b560c51b83f4af87c2e1a3bf82a5dbd77)
+---
+ .../libmythtv/recorders/ExternalChannel.cpp | 42 +++++++-
+ .../libmythtv/recorders/ExternalChannel.h | 3 +
+ .../recorders/ExternalSignalMonitor.cpp | 13 +++
+ .../mythexternrecorder/MythExternControl.cpp | 9 ++
+ .../mythexternrecorder/MythExternControl.h | 2 +
+ .../mythexternrecorder/MythExternRecApp.cpp | 97 +++++++++++++------
+ .../mythexternrecorder/MythExternRecApp.h | 5 +-
+ .../mythexternrecorder/commandlineparser.cpp | 2 +-
+ mythtv/programs/mythexternrecorder/main.cpp | 2 +
+ 9 files changed, 140 insertions(+), 35 deletions(-)
+
+diff --git a/mythtv/libs/libmythtv/recorders/ExternalChannel.cpp
b/mythtv/libs/libmythtv/recorders/ExternalChannel.cpp
+index c1620a1dccd..f9c295eda9c 100644
+--- a/mythtv/libs/libmythtv/recorders/ExternalChannel.cpp
++++ b/mythtv/libs/libmythtv/recorders/ExternalChannel.cpp
+@@ -119,8 +119,8 @@ bool ExternalChannel::Tune(const QString &channum)
+ {
+ LOG(VB_CHANNEL, LOG_INFO, LOC + "Tuning to " + channum);
+
+- if (!m_streamHandler->ProcessCommand("TuneChannel:" + channum,
result,
+- m_tuneTimeout))
++ 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));
+@@ -128,6 +128,7 @@ bool ExternalChannel::Tune(const QString &channum)
+ }
+
+ UpdateDescription();
++ m_backgroundTuning = result.startsWith("OK:Start");
+ }
+
+ return true;
+@@ -143,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:Running"))
++ 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 1a7fc75a7aa..0b09953493b 100644
+--- a/mythtv/libs/libmythtv/recorders/ExternalChannel.h
++++ b/mythtv/libs/libmythtv/recorders/ExternalChannel.h
+@@ -46,6 +46,8 @@ 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
+@@ -53,6 +55,7 @@ class ExternalChannel : public DTVChannel
+
+ 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/ExternalSignalMonitor.cpp
b/mythtv/libs/libmythtv/recorders/ExternalSignalMonitor.cpp
+index 6fb4cd592a9..2db72f50af8 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/programs/mythexternrecorder/MythExternControl.cpp
b/mythtv/programs/mythexternrecorder/MythExternControl.cpp
+index 0a0b933e8df..258dc64dc57 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);
+@@ -362,6 +367,10 @@ bool Commands::ProcessCommand(const QString & cmd)
+ 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]);
+diff --git a/mythtv/programs/mythexternrecorder/MythExternControl.h
b/mythtv/programs/mythexternrecorder/MythExternControl.h
+index 308a9dd137b..172bf5bc1f7 100644
+--- a/mythtv/programs/mythexternrecorder/MythExternControl.h
++++ b/mythtv/programs/mythexternrecorder/MythExternControl.h
+@@ -103,6 +103,7 @@ 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);
+@@ -146,6 +147,7 @@ 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);
+diff --git a/mythtv/programs/mythexternrecorder/MythExternRecApp.cpp
b/mythtv/programs/mythexternrecorder/MythExternRecApp.cpp
+index 494d207242a..b9e02f6f2ea 100644
+--- a/mythtv/programs/mythexternrecorder/MythExternRecApp.cpp
++++ b/mythtv/programs/mythexternrecorder/MythExternRecApp.cpp
+@@ -178,28 +178,28 @@ 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;
+@@ -215,10 +215,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));
+ }
+
+@@ -252,7 +258,7 @@ void MythExternRecApp::Run(void)
+ if (m_proc.state() == QProcess::Running)
+ {
+ m_proc.closeReadChannel(QProcess::StandardOutput);
+- TerminateProcess();
++ TerminateProcess(m_proc, "App");
+ }
+
+ emit Done();
+@@ -415,6 +421,13 @@ Q_SLOT void MythExternRecApp::NextChannel(const QString &
serial)
+ Q_SLOT void MythExternRecApp::TuneChannel(const QString & serial,
+ const QString & channum)
+ {
++ if (m_tuneCommand.isEmpty())
++ {
++ LOG(VB_CHANNEL, LOG_ERR, LOC + ": No 'tuner' configured.");
++ emit SendMessage("TuneChannel", serial, "ERR:No 'tuner'
configured.");
++ return;
++ }
++
+ if (m_tunedChannel == channum)
+ {
+ LOG(VB_CHANNEL, LOG_INFO, LOC +
+@@ -424,13 +437,6 @@ Q_SLOT void MythExternRecApp::TuneChannel(const QString &
serial,
+ return;
+ }
+
+- if (m_tuneCommand.isEmpty())
+- {
+- LOG(VB_CHANNEL, LOG_ERR, LOC + ": No 'tuner' configured.");
+- emit SendMessage("TuneChannel", serial, "ERR:No 'tuner'
configured.");
+- return;
+- }
+-
+ m_desc = m_recDesc;
+ m_command = m_recCommand;
+
+@@ -468,18 +474,20 @@ Q_SLOT void MythExternRecApp::TuneChannel(const QString &
serial,
+ settings.endGroup();
+ }
+
++ if (m_tuneProc.state() == QProcess::Running)
++ TerminateProcess(m_tuneProc, "Tune");
++
+ tune.replace("%CHANNUM%", channum);
+ m_command.replace("%CHANNUM%", channum);
+
+- if (system(tune.toUtf8().constData()) != 0)
++ m_tuneProc.start(tune);
++ if (!m_tuneProc.waitForStarted())
+ {
+- QString errmsg = QString("'%1' failed: ").arg(tune) + ENO;
++ QString errmsg = QString("Tune `%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));
+
+ if (!m_logFile.isEmpty() && m_command.indexOf("%LOGFILE%") >=
0)
+ {
+@@ -499,14 +507,41 @@ Q_SLOT void MythExternRecApp::TuneChannel(const QString &
serial,
+
+ m_desc.replace("%URL%", url);
+ m_desc.replace("%CHANNUM%", channum);
++ m_tuningChannel = channum;
+
+- LOG(VB_CHANNEL, LOG_INFO, LOC +
+- QString(": TuneChannel %1: URL '%2'").arg(channum).arg(url));
+- m_tunedChannel = channum;
++ LOG(VB_CHANNEL, LOG_INFO, LOC + QString(": Started `%1` URL
'%2'")
++ .arg(tune).arg(url));
++ emit SendMessage("TuneChannel", serial,
++ QString("OK:Started `%1`").arg(tune));
++}
++
++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:Running");
++ return;
++ }
++
++ if (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:Tuned to %1").arg(channum));
++ QString("OK:Tuned to %1").arg(m_tunedChannel));
+ }
+
+ Q_SLOT void MythExternRecApp::LockTimeout(const QString & serial)
+@@ -607,7 +642,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 b94e01d0c86..af87e21e272 100644
+--- a/mythtv/programs/mythexternrecorder/MythExternRecApp.h
++++ b/mythtv/programs/mythexternrecorder/MythExternRecApp.h
+@@ -75,12 +75,13 @@ class MythExternRecApp : public QObject
+ void NextChannel(const QString & serial);
+
+ 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);
+@@ -105,6 +106,7 @@ class MythExternRecApp : public QObject
+
+ QMap<QString, QString> m_appEnv;
+
++ QProcess m_tuneProc;
+ QString m_tuneCommand;
+ QString m_channelsIni;
+ uint m_lockTimeout { 0 };
+@@ -117,6 +119,7 @@ class MythExternRecApp : public QObject
+ QString m_configIni;
+ QString m_desc;
+
++ QString m_tuningChannel;
+ QString m_tunedChannel;
+
+ // Channel scanning
+diff --git a/mythtv/programs/mythexternrecorder/commandlineparser.cpp
b/mythtv/programs/mythexternrecorder/commandlineparser.cpp
+index 31c118385e3..f7ca200703a 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 e05e047d7cc..833ecb8a8c6 100644
+--- a/mythtv/programs/mythexternrecorder/main.cpp
++++ b/mythtv/programs/mythexternrecorder/main.cpp
+@@ -122,6 +122,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,
+
+From ecb0c15b4cc3bd86905602f93a453bad6459c08a Mon Sep 17 00:00:00 2001
+From: John Poet <jpoet(a)mythtv.org>
+Date: Sat, 4 Apr 2020 13:49:46 -0600
+Subject: [PATCH 23/47] ExtneralChannel: Use InProgress instead of running or
+ starting to indicate a long running tunning operation.
+
+Thanks to Gary Buhrmaster for the suggestion.
+
+(cherry picked from commit 1dd0408e236e354f72eaed02c1119bb2b3f4a157)
+---
+ mythtv/libs/libmythtv/recorders/ExternalChannel.cpp | 6 +++---
+ mythtv/programs/mythexternrecorder/MythExternRecApp.cpp | 4 ++--
+ 2 files changed, 5 insertions(+), 5 deletions(-)
+
+diff --git a/mythtv/libs/libmythtv/recorders/ExternalChannel.cpp
b/mythtv/libs/libmythtv/recorders/ExternalChannel.cpp
+index f9c295eda9c..5405c6f55fa 100644
+--- a/mythtv/libs/libmythtv/recorders/ExternalChannel.cpp
++++ b/mythtv/libs/libmythtv/recorders/ExternalChannel.cpp
+@@ -128,7 +128,7 @@ bool ExternalChannel::Tune(const QString &channum)
+ }
+
+ UpdateDescription();
+- m_backgroundTuning = result.startsWith("OK:Start");
++ m_backgroundTuning = result.startsWith("OK:InProgress");
+ }
+
+ return true;
+@@ -166,14 +166,14 @@ uint ExternalChannel::GetTuneStatus(void)
+ }
+ else
+ {
+- if (result.startsWith("OK:Running"))
++ if (result.startsWith("OK:InProgress"))
+ ret = 1;
+ else
+ {
+ ret = 3;
+ m_backgroundTuning = false;
++ UpdateDescription();
+ }
+- UpdateDescription();
+ }
+
+ LOG(VB_CHANNEL, LOG_DEBUG, LOC + QString("GetScriptStatus() %1 -> %2")
+diff --git a/mythtv/programs/mythexternrecorder/MythExternRecApp.cpp
b/mythtv/programs/mythexternrecorder/MythExternRecApp.cpp
+index b9e02f6f2ea..1758d7e5395 100644
+--- a/mythtv/programs/mythexternrecorder/MythExternRecApp.cpp
++++ b/mythtv/programs/mythexternrecorder/MythExternRecApp.cpp
+@@ -512,7 +512,7 @@ Q_SLOT void MythExternRecApp::TuneChannel(const QString &
serial,
+ LOG(VB_CHANNEL, LOG_INFO, LOC + QString(": Started `%1` URL
'%2'")
+ .arg(tune).arg(url));
+ emit SendMessage("TuneChannel", serial,
+- QString("OK:Started `%1`").arg(tune));
++ QString("OK:InProgress `%1`").arg(tune));
+ }
+
+ Q_SLOT void MythExternRecApp::TuneStatus(const QString & serial)
+@@ -521,7 +521,7 @@ Q_SLOT void MythExternRecApp::TuneStatus(const QString & serial)
+ {
+ LOG(VB_CHANNEL, LOG_INFO, LOC +
+ QString(": Tune process(%1) still
running").arg(m_tuneProc.pid()));
+- emit SendMessage("TuneStatus", serial, "OK:Running");
++ emit SendMessage("TuneStatus", serial, "OK:InProgress");
+ return;
+ }
+
+
+From f8495fd1564df151436ebe941a490ce5f7d871c8 Mon Sep 17 00:00:00 2001
+From: John Poet <jpoet(a)mythtv.org>
+Date: Sun, 5 Apr 2020 18:04:33 -0600
+Subject: [PATCH 24/47] ExternRecorder: Fix live tv channel changes.
+
+(cherry picked from commit 18fa5fff1b9ac862cea0c5e8a3fb8981052bd6a3)
+---
+ mythtv/libs/libmythtv/recorders/ExternalRecorder.cpp | 3 +++
+ 1 file changed, 3 insertions(+)
+
+diff --git a/mythtv/libs/libmythtv/recorders/ExternalRecorder.cpp
b/mythtv/libs/libmythtv/recorders/ExternalRecorder.cpp
+index 445325bc835..a2acc946557 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
+
+From daa1d5d8e2a617c2fadb01e22558e30f53aa86b4 Mon Sep 17 00:00:00 2001
+From: John Poet <jpoet(a)mythtv.org>
+Date: Wed, 8 Apr 2020 14:41:45 -0600
+Subject: [PATCH 25/47] ExternalRecorder: Allow for optional ICON field is
+ channels.
+
+Any ExternalRecorder which supports fetching channel information can
+now supply the icon image filename. That information will be
+populated into the mythconverg.channel table. The file is NOT
+/installed/ anywhere and must exist where mythbackend looks for such
+files.
+
+(cherry picked from commit be7417fa483f650980767d053fc0cf4a20eac8f2)
+---
+ .../libmythtv/channelscan/externrecscanner.cpp | 9 +++++----
+ .../recorders/ExternalRecChannelFetcher.cpp | 12 ++++++++----
+ .../recorders/ExternalRecChannelFetcher.h | 17 +++++++++++------
+ .../mythexternrecorder/MythExternRecApp.cpp | 10 ++++++----
+ 4 files changed, 30 insertions(+), 18 deletions(-)
+
+diff --git a/mythtv/libs/libmythtv/channelscan/externrecscanner.cpp
b/mythtv/libs/libmythtv/channelscan/externrecscanner.cpp
+index be13e4f968f..11964840c62 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/recorders/ExternalRecChannelFetcher.cpp
b/mythtv/libs/libmythtv/recorders/ExternalRecChannelFetcher.cpp
+index 28e11f2c98b..3a2385989ae 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 84f354839d2..638b81becf1 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/programs/mythexternrecorder/MythExternRecApp.cpp
b/mythtv/programs/mythexternrecorder/MythExternRecApp.cpp
+index 1758d7e5395..5a8acb28619 100644
+--- a/mythtv/programs/mythexternrecorder/MythExternRecApp.cpp
++++ b/mythtv/programs/mythexternrecorder/MythExternRecApp.cpp
+@@ -396,15 +396,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)
+
+From 2921af5591d127e4ff337d7c6c57e1015d28cfd8 Mon Sep 17 00:00:00 2001
+From: Paul Harrison <paul(a)mythqml.net>
+Date: Sat, 11 Apr 2020 15:45:10 +0100
+Subject: [PATCH 26/47] version.sh: if found use DESCRIBE to get branch and
+ version information
+
+The Ubuntu packaging scripts create a DESCRIBE file that has the branch and
+version information we need so use that if it's found.
+
+(cherry picked from commit 0851b35e3ded43ea738473bc60b8e5d13595b922)
+---
+ mythtv/version.sh | 90 +++++++++++++++++++++++++++++------------------
+ 1 file changed, 55 insertions(+), 35 deletions(-)
+
+diff --git a/mythtv/version.sh b/mythtv/version.sh
+index fd2c0be875f..56c04457622 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/DECRIBE 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
+
+From 3b54678feba714c514f32b38b0f5c1c6a74eaf3f Mon Sep 17 00:00:00 2001
+From: Mark Kendall <mark.kendall(a)gmail.com>
+Date: Mon, 13 Apr 2020 17:12:12 +0100
+Subject: [PATCH 27/47] VAAPI: Fix compilation for older drivers
+
+Fixes #13606
+
+(cherry picked from commit 394245f0dbab1f2680b5a6edc67764debe3d9dfd)
+---
+ mythtv/libs/libmythtv/opengl/mythvaapidrminterop.cpp | 4 ++++
+ 1 file changed, 4 insertions(+)
+
+diff --git a/mythtv/libs/libmythtv/opengl/mythvaapidrminterop.cpp
b/mythtv/libs/libmythtv/opengl/mythvaapidrminterop.cpp
+index 48be0267f11..c03766c6fad 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;
+ }
+
+From 4d0924203a33203eedc1d4cf0b3f242609d729ef Mon Sep 17 00:00:00 2001
+From: Nigel Jewell <nige(a)grufty.co.uk>
+Date: Sun, 12 Apr 2020 21:31:49 +0100
+Subject: [PATCH 28/47] Fix typo in 0851b35e3ded43ea738473bc60b8e5d13595b922
+ comment
+
+(cherry picked from commit dca115895bcc7631fd4eb9dcfa0b4d838ed1e786)
+---
+ mythtv/version.sh | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/mythtv/version.sh b/mythtv/version.sh
+index 56c04457622..d412cc0505d 100755
+--- a/mythtv/version.sh
++++ b/mythtv/version.sh
+@@ -21,7 +21,7 @@ GITREPOPATH="exported"
+
+ cd ${GITTREEDIR}
+
+-# if we have a mythtv/DECRIBE file use that to get the branch and version
++# 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
+
+From c8f62c1688bb2ceaedabb2563f434dac1d7d5694 Mon Sep 17 00:00:00 2001
+From: Paul Harrison <paul(a)mythqml.net>
+Date: Wed, 15 Apr 2020 20:12:47 +0100
+Subject: [PATCH 29/47] FAQ: trivial change to force an update
+
+---
+ mythtv/FAQ | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/mythtv/FAQ b/mythtv/FAQ
+index 593d3b9fea6..e4f6c451095 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
+
+
+From 4e3935420ac0da8997d01ce77672f90d6dcbd78e Mon Sep 17 00:00:00 2001
+From: Klaas de Waal <kdewaal(a)mythtv.org>
+Date: Thu, 16 Apr 2020 22:22:20 +0200
+Subject: [PATCH 30/47] Fix "Full Scan" for DVB-T only tuners
+
+The "Full Scan" for tuners that can do only DVB-T and not DVB-T2
+was done correct but the modulation system was not entered in
+the transport in the database. This is now fixed.
+This gave problems in mythbackend when used with DVB-T/T2 tuners
+because these need the modulation system information.
+
+(cherry picked from commit a618b675fdd3b388221159e4d72cf15e56b2dfe4)
+Signed-off-by: Klaas de Waal <kdewaal(a)mythtv.org>
+---
+ mythtv/libs/libmythtv/channelscan/channelscan_sm.cpp | 4 ++++
+ 1 file changed, 4 insertions(+)
+
+diff --git a/mythtv/libs/libmythtv/channelscan/channelscan_sm.cpp
b/mythtv/libs/libmythtv/channelscan/channelscan_sm.cpp
+index 543c7e1a9cd..98e9f9f7d4b 100644
+--- a/mythtv/libs/libmythtv/channelscan/channelscan_sm.cpp
++++ b/mythtv/libs/libmythtv/channelscan/channelscan_sm.cpp
+@@ -1032,6 +1032,10 @@ bool ChannelScanSM::UpdateChannelInfo(bool wait_until_complete)
+ 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)
+
+From 8bfc909dc70e8e9f156e66f7346f63e3e13660d5 Mon Sep 17 00:00:00 2001
+From: Paul Harrison <paul(a)mythqml.net>
+Date: Sat, 18 Apr 2020 15:38:32 +0100
+Subject: [PATCH 31/47] HLSStreamHandler: fix the formatting of a debug
+ statement
+
+Refs #13608
+
+(cherry picked from commit 2b31dbf2ff30ea73b5865918719d14076c39f0cf)
+---
+ mythtv/libs/libmythtv/recorders/hlsstreamhandler.cpp | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/mythtv/libs/libmythtv/recorders/hlsstreamhandler.cpp
b/mythtv/libs/libmythtv/recorders/hlsstreamhandler.cpp
+index 964f5396e46..22e0abae062 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;
+ }
+
+
+From 917a2087ef032b2c36a102cf4a2b220e10bf7bfe Mon Sep 17 00:00:00 2001
+From: David Hampton <mythtv(a)love2code.net>
+Date: Tue, 21 Apr 2020 14:58:59 -0400
+Subject: [PATCH 32/47] Fix segfault in code called from MythMainWindow::Draw.
+
+This reverts three of the changes in 380102ce34. In
+mythmainwindow.cpp while running the m_stackList, the call to
+MythScreenStack::GetDrawOrder can apparently modify m_stackList or
+something that it points to. Reverting the range-based for loops and
+restoring the original for loops prevents the crash.
+
+Fixes #13613.
+---
+ mythtv/libs/libmythui/mythmainwindow.cpp | 19 ++++++++++++-------
+ 1 file changed, 12 insertions(+), 7 deletions(-)
+
+diff --git a/mythtv/libs/libmythui/mythmainwindow.cpp
b/mythtv/libs/libmythui/mythmainwindow.cpp
+index 7bdfe41bb3b..619ba7f7f78 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);
+
+From 5f1993304e35042192aed059dd7cf8717b76c6a7 Mon Sep 17 00:00:00 2001
+From: David Hampton <mythtv(a)love2code.net>
+Date: Tue, 21 Apr 2020 15:24:15 -0400
+Subject: [PATCH 33/47] Fix incorrect data provided to UPnP client.
+
+This bug was introduced in 77b560f3cc when converting from the
+obsolete QString::sprintf function to typical QString formatting using
+QString::arg. In one case the arguments were all converted, but the
+format string wasn't . Fix that format string.
+
+Fixes #13612.
+---
+ mythtv/libs/libmythupnp/upnphelpers.cpp | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/mythtv/libs/libmythupnp/upnphelpers.cpp
b/mythtv/libs/libmythupnp/upnphelpers.cpp
+index cda4f86a778..3a2bfbba3ce 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
+
+From c0b8b6e036bdb531fe09be8005dcf1181d9333ec Mon Sep 17 00:00:00 2001
+From: =?UTF-8?q?Micha=C5=82=20Janiszewski?= <janisozaur+signed(a)gmail.com>
+Date: Tue, 30 Oct 2018 21:56:52 +0100
+Subject: [PATCH 34/47] Compare to `None` using identity `is` operator
+
+This is a trivial change that replaces `==` operator with `is` operator, following PEP 8
guideline:
+
+> Comparisons to singletons like None should always be done with is or is not, never
the equality operators.
+
+https://legacy.python.org/dev/peps/pep-0008/#programming-recommendations
+
+Closes #13343
+
+(cherry picked from commit 2e7e9e82ae71e958a7844e768d3d2758cf6df3ad)
+
+Signed-off-by: Bill Meek <billmeek(a)mythtv.org>
+---
+ .../mytharchive/mythburn/scripts/mythburn.py | 18 +++++------
+ .../scripts/giantbomb/giantbomb_api.py | 12 +++----
+ .../bindings/python/MythTV/ttvdb/tvdbXslt.py | 4 +--
+ .../bindings/python/MythTV/ttvdb/tvdb_api.py | 2 +-
+ .../bindings/python/tmdb3/tmdb3/cache_file.py | 2 +-
+ .../contrib/imports/mirobridge/mirobridge.py | 32 +++++++++----------
+ .../imports/mirobridge/mirobridge/metadata.py | 2 +-
+ .../mirobridge_interpreter_4_0_2.py | 4 +--
+ .../mirobridge_interpreter_6_0_0.py | 4 +--
+ .../distros/mythtv_data/uuiddb.py | 2 +-
+ .../scripts/hardwareprofile/sendProfile.py | 2 +-
+ .../programs/scripts/hardwareprofile/smolt.py | 4 +--
+ .../bbciplayer/bbciplayer_api.py | 16 +++++-----
+ .../nv_python_libs/bliptv/bliptv_api.py | 6 ++--
+ .../nv_python_libs/common/common_api.py | 10 +++---
+ .../dailymotion/dailymotion_api.py | 6 ++--
+ .../nv_python_libs/hulu/hulu_api.py | 14 ++++----
+ .../nv_python_libs/mainProcess.py | 4 +--
+ .../nv_python_libs/mashups/mashups_api.py | 10 +++---
+ .../nv_python_libs/mtv/mtv_api.py | 18 +++++------
+ .../nv_python_libs/rev3/rev3_api.py | 16 +++++-----
+ .../nv_python_libs/thewb/thewb_api.py | 10 +++---
+ .../nv_python_libs/vimeo/vimeo_api.py | 32 +++++++++----------
+ .../xsltfunctions/cinemarv_api.py | 6 ++--
+ .../nv_python_libs/xsltfunctions/nasa_api.py | 2 +-
+ .../xsltfunctions/skyAtNight_api.py | 2 +-
+ .../xsltfunctions/tributeca_api.py | 4 +--
+ .../nv_python_libs/youtube/youtube_api.py | 10 +++---
+ .../scripts/metadata/Music/mbutils.py | 16 +++++-----
+ .../metadata/Music/musicbrainzngs/util.py | 2 +-
+ .../scripts/metadata/Television/ttvdb.py | 20 ++++++------
+ 31 files changed, 146 insertions(+), 146 deletions(-)
+
+diff --git a/mythplugins/mytharchive/mythburn/scripts/mythburn.py
b/mythplugins/mytharchive/mythburn/scripts/mythburn.py
+index 371e32bd370..87e09c3920f 100755
+--- a/mythplugins/mytharchive/mythburn/scripts/mythburn.py
++++ b/mythplugins/mytharchive/mythburn/scripts/mythburn.py
+@@ -269,16 +269,16 @@ def __init__(self, name=None, fontFile=None, size=19,
color="white", effect="nor
+ 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/scripts/giantbomb/giantbomb_api.py
b/mythplugins/mythgame/mythgame/scripts/giantbomb/giantbomb_api.py
+index d0d8d002545..2a03a78c624 100644
+--- a/mythplugins/mythgame/mythgame/scripts/giantbomb/giantbomb_api.py
++++ b/mythplugins/mythgame/mythgame/scripts/giantbomb/giantbomb_api.py
+@@ -167,7 +167,7 @@ def fixup(m):
+
+
+ def textUtf8(self, text):
+- if text == None:
++ if text is None:
+ return text
+ try:
+ return unicode(text, 'utf8')
+@@ -268,15 +268,15 @@ def futureReleaseDate(self, context, gameElement):
+ 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 @@ def gameSearch(self, gameTitle):
+
+ 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 @@ def gameData(self, gameId):
+ 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/mythtv/bindings/python/MythTV/ttvdb/tvdbXslt.py
b/mythtv/bindings/python/MythTV/ttvdb/tvdbXslt.py
+index f4dc9ca1074..8af7851fd78 100644
+--- a/mythtv/bindings/python/MythTV/ttvdb/tvdbXslt.py
++++ b/mythtv/bindings/python/MythTV/ttvdb/tvdbXslt.py
+@@ -218,7 +218,7 @@ def imageElements(self, context, *args):
+ 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 @@ def imageElements(self, context, *args):
+ # 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 01b13c7ebc1..6ce90adc16e 100644
+--- a/mythtv/bindings/python/MythTV/ttvdb/tvdb_api.py
++++ b/mythtv/bindings/python/MythTV/ttvdb/tvdb_api.py
+@@ -1081,7 +1081,7 @@ def _getShowData(self, sid, language):
+
+ 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/tmdb3/tmdb3/cache_file.py
b/mythtv/bindings/python/tmdb3/tmdb3/cache_file.py
+index e2f6165ac82..f847005146c 100644
+--- a/mythtv/bindings/python/tmdb3/tmdb3/cache_file.py
++++ b/mythtv/bindings/python/tmdb3/tmdb3/cache_file.py
+@@ -384,7 +384,7 @@ def _write(self, data):
+ # 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/contrib/imports/mirobridge/mirobridge.py
b/mythtv/contrib/imports/mirobridge/mirobridge.py
+index f9417f4ff49..a7f87ffec00 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 4e0c882d2ce..76cbe025291 100644
+--- a/mythtv/contrib/imports/mirobridge/mirobridge/metadata.py
++++ b/mythtv/contrib/imports/mirobridge/mirobridge/metadata.py
+@@ -169,7 +169,7 @@ def getMetadata(self, title):
+ # 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 3061903e4f7..966d184677d 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 @@ def do_mythtv_getunwatched(self, line):
+ 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 @@ def do_mythtv_getwatched(self, line):
+ 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 1a5a6d9e78e..2282722e972 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 @@ def do_mythtv_getunwatched(self, line):
+
+ # 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 @@ def do_mythtv_getwatched(self, line):
+ 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/programs/scripts/hardwareprofile/distros/mythtv_data/uuiddb.py
b/mythtv/programs/scripts/hardwareprofile/distros/mythtv_data/uuiddb.py
+index febd16e9f35..6960f8705ae 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 @@ def get_priv_uuid(self):
+ 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 ca929654dbf..ccbfdeac6d4 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 5cf234e7c05..1bcc8060d07 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 01ee6052561..02cefe586b1 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 @@ def searchTitle(self, title, pagenumber, pagelen):
+ 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 @@ def displayTreeView(self):
+ 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 @@ def displayTreeView(self):
+ # 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 @@ def displayTreeView(self):
+ 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 @@ def displayTreeView(self):
+ 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 @@ def displayTreeView(self):
+ 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 @@ def displayTreeView(self):
+ 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 4abbf697dab..a59f99147d2 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 @@ def getExternalIP():
+
+ ip = getExternalIP()
+
+- if ip == None:
++ if ip is None:
+ return {}
+
+ try:
+@@ -371,7 +371,7 @@ def _initLogger(self):
+
+
+ def textUtf8(self, text):
+- if text == None:
++ if text is None:
+ return text
+ try:
+ return unicode(text, 'utf8')
+@@ -541,7 +541,7 @@ def searchForVideos(self, title, pagenumber):
+ 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 03341242ba5..2f58267c300 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 @@ def initLogger(self, path=sys.stderr,
log_name=u'MNV_Grabber'):
+
+
+ def textUtf8(self, text):
+- if text == None:
++ if text is None:
+ return text
+ try:
+ return unicode(text, 'utf8')
+@@ -369,7 +369,7 @@ def getExternalIP():
+
+ ip = getExternalIP()
+
+- if ip == None:
++ if ip is None:
+ return {}
+
+ try:
+@@ -508,7 +508,7 @@ def getUrlData(self, inputUrls, pageFilter=None):
+ 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 @@ def linkWebPage(self, context, sourceLink):
+ # 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 @@ def run(self):
+ 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 d4f502fc221..6cee18b2458 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 @@ def getExternalIP():
+
+ ip = getExternalIP()
+
+- if ip == None:
++ if ip is None:
+ return {}
+
+ try:
+@@ -658,7 +658,7 @@ def _initLogger(self):
+
+
+ def textUtf8(self, text):
+- if text == None:
++ if text is None:
+ return text
+ try:
+ return unicode(text, 'utf8')
+@@ -747,7 +747,7 @@ def searchForVideos(self, title, pagenumber):
+ 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 1152735289b..153a7b6e852 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 @@ def displayTreeView(self):
+ 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 @@ def displayTreeView(self):
+ # 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 @@ def displayTreeView(self):
+ 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 @@ def displayTreeView(self):
+ 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 @@ def displayTreeView(self):
+ 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 @@ def displayTreeView(self):
+ 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 0bc719f3cde..3a96c14c0d8 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 @@ def searchForVideos(self, search_text, pagenumber):
+ 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 @@ def displayTreeView(self):
+ 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 6fc4460f36d..962323384ce 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 @@ def searchForVideos(self, title, pagenumber):
+ 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 @@ def displayTreeView(self):
+ 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 @@ def displayTreeView(self):
+ 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 @@ def displayTreeView(self):
+
+ # 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 @@ def displayTreeView(self):
+ 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 47612f2a267..c047ca95291 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 @@ def _initLogger(self):
+
+
+ def textUtf8(self, text):
+- if text == None:
++ if text is None:
+ return text
+ try:
+ return unicode(text, 'utf8')
+@@ -392,7 +392,7 @@ def searchTitle(self, title, pagenumber, pagelen):
+ # 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 @@ def searchTitle(self, title, pagenumber, pagelen):
+ 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 @@ def videoDetails(self, url, title=u''):
+ 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 @@ def searchForVideos(self, title, pagenumber):
+ 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 @@ def getVideosForURL(self, url, dictionaries):
+ 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 08537df1c8e..b3cc354b09c 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 @@ def updateRev3(self, create=False):
+ 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 @@ def updateRev3(self, create=False):
+ 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 @@ def displayTreeView(self):
+ 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 @@ def displayTreeView(self):
+ 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 67ca0a123b8..4bfc32fc3ca 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 @@ def searchTitle(self, title, pagenumber, pagelen,
ignoreError=False):
+ 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 @@ def displayTreeView(self):
+
+ # 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 @@ def displayTreeView(self):
+ 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 37a7df966f7..e87655296b5 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 @@ def __init__(self, key, secret,
+ 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 @@ def vimeo_albums_getAll(self, user_id, sort=None,
+ 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 @@ def vimeo_videos_search(self, query, sort=None,
+ 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 @@ def vimeo_channels_getAll(self, sort=None,
+ 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 @@ def vimeo_channels_getVideos(self, channel_id=None,
full_response=None,
+ """
+ # 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 @@ def _initLogger(self):
+
+
+ def textUtf8(self, text):
+- if text == None:
++ if text is None:
+ return text
+ try:
+ return unicode(text, 'utf8')
+@@ -915,7 +915,7 @@ def searchTitle(self, title, pagenumber, pagelen):
+ 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 @@ def searchForVideos(self, title, pagenumber):
+ 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 @@ def getChannels(self):
+ 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 @@ def getTreeVideos(self, method, dictionaries):
+ 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 a29076f5850..1eedfa7f268 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 @@ def cinemarvLinkGeneration(self, context, *args):
+ 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 @@ def cinemarvLinkGeneration(self, context, *args):
+ 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 @@ def cinemarvIsCustomHTML(self, context, *args):
+ 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 057da71e43e..72208639be7 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 @@ def nasaTitleEp(self, context, *arg):
+ 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 dde9f1b10c0..a3cb81f6938 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 @@ def skyAtNightTitleEp(self, context, *arg):
+ 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 a7376e0e8d4..f0f80756a9a 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 @@ def tributecaLinkGeneration(self, context, *args):
+
+ # 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 @@ def tributecaIsCustomHTML(self, context, *args):
+ 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 62fde93b2e0..ff502cc9f9d 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 @@ def __init__(self,
+
+ # 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 @@ def __init__(self,
+ 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 @@ def getExternalIP():
+
+ ip = getExternalIP()
+
+- if ip == None:
++ if ip is None:
+ return {}
+
+ try:
+@@ -445,7 +445,7 @@ def parseDetails(self, entry):
+
+ 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 @@ def searchForVideos(self, title, pagenumber):
+ 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/Music/mbutils.py
b/mythtv/programs/scripts/metadata/Music/mbutils.py
+index 0a133fcbc67..bfe523c8353 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 37316f53b1e..5f48e6b0d09 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 64eab727f03..76b98618920 100755
+--- a/mythtv/programs/scripts/metadata/Television/ttvdb.py
++++ b/mythtv/programs/scripts/metadata/Television/ttvdb.py
+@@ -1451,7 +1451,7 @@ def fuzzysearch(self, term = None, key = None):
+ 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':
+@@ -2119,7 +2119,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 +2144,7 @@ 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:
++ 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 +2169,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
+
+From 3ccdb8c6d24e86a7282d32c14a1f4f09c87e9756 Mon Sep 17 00:00:00 2001
+From: Bill Meek <billmeek(a)mythtv.org>
+Date: Sun, 9 Feb 2020 19:03:03 -0600
+Subject: [PATCH 35/47] Python Bindings: fix warnings seen in *buntu packaging
+
+(cherry picked from commit 12f44c74ed60e3b0909040f30665c9d3fc58c17c)
+---
+ mythtv/bindings/python/MythTV/database.py | 2 +-
+ mythtv/bindings/python/MythTV/system.py | 6 +++---
+ 2 files changed, 4 insertions(+), 4 deletions(-)
+
+diff --git a/mythtv/bindings/python/MythTV/database.py
b/mythtv/bindings/python/MythTV/database.py
+index 86294414320..7e3c4fe42be 100644
+--- a/mythtv/bindings/python/MythTV/database.py
++++ b/mythtv/bindings/python/MythTV/database.py
+@@ -1167,7 +1167,7 @@ def __init__(self, db, log, host):
+ 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)"""
+diff --git a/mythtv/bindings/python/MythTV/system.py
b/mythtv/bindings/python/MythTV/system.py
+index d1d7546c0eb..f7966fb6d69 100644
+--- a/mythtv/bindings/python/MythTV/system.py
++++ b/mythtv/bindings/python/MythTV/system.py
+@@ -131,7 +131,7 @@ def command(self, *args):
+ 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 @@ def append(self, *args):
+ 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)
+@@ -441,7 +441,7 @@ def command(self, eventdata):
+ 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:
+
+From 723d46eaaa7be6bd760f2c5dfcb50b5410f315ec Mon Sep 17 00:00:00 2001
+From: Mark Kendall <mark.kendall(a)gmail.com>
+Date: Mon, 27 Apr 2020 21:28:44 +0100
+Subject: [PATCH 36/47] libmythtv: Fix VideoToolbox framework name
+
+Closes #13609
+
+(cherry picked from commit 00b8defa6d27bb5688b3217f597adb1faac4773f)
+---
+ mythtv/libs/libmythtv/libmythtv.pro | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/mythtv/libs/libmythtv/libmythtv.pro b/mythtv/libs/libmythtv/libmythtv.pro
+index f1b0248d220..646577cd5b5 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
+
+From f5d75a6de7c4ac668e1a64cdf31c7100bc81b65b Mon Sep 17 00:00:00 2001
+From: Hans Dingemans <jpldingemans(a)gmail.com>
+Date: Fri, 21 Feb 2020 12:13:00 -0500
+Subject: [PATCH 37/47] mythfilldatabase: reduce memory usage.
+
+Mythfilldatabase uses QDomDocument to parse and store the XML data that is read;
according to QDomDocument documentation this object is not meant to handle large XML
files; QXmlStreamReader should be used in these situations.
+
+This commit replaces QDomDocument by QXmlStreamreader.
+
+A test showed that memory usage dropped from 5.6 GB to 698 MB.
+
+Fixes #13517
+
+Signed-off-by: Peter Bennett <pbennett(a)mythtv.org>
+(cherry picked from commit a9aa006139da24cbad861a2c25d57fa3f71aba2a)
+---
+ mythtv/programs/mythfilldatabase/filldata.cpp | 1 -
+ .../programs/mythfilldatabase/xmltvparser.cpp | 997 +++++++++---------
+ .../programs/mythfilldatabase/xmltvparser.h | 4 -
+ 3 files changed, 496 insertions(+), 506 deletions(-)
+
+diff --git a/mythtv/programs/mythfilldatabase/filldata.cpp
b/mythtv/programs/mythfilldatabase/filldata.cpp
+index 59137f26427..f320857c186 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;
+
+diff --git a/mythtv/programs/mythfilldatabase/xmltvparser.cpp
b/mythtv/programs/mythfilldatabase/xmltvparser.cpp
+index b8cdff01310..66157ffc648 100644
+--- a/mythtv/programs/mythfilldatabase/xmltvparser.cpp
++++ b/mythtv/programs/mythfilldatabase/xmltvparser.cpp
+@@ -34,12 +34,6 @@ XMLTVParser::XMLTVParser()
+ m_currentYear = MythDate::current().date().toString("yyyy").toUInt();
+ }
+
+-void XMLTVParser::lateInit()
+-{
+- m_movieGrabberPath = MetadataDownload::GetMovieGrabber();
+- m_tvGrabberPath = MetadataDownload::GetTelevisionGrabber();
+-}
+-
+ static uint ELFHash(const QByteArray &ba)
+ {
+ const auto *k = (const uchar *)ba.data();
+@@ -60,74 +54,6 @@ static uint ELFHash(const QByteArray &ba)
+ return h;
+ }
+
+-static QString getFirstText(const QDomElement& element)
+-{
+- for (QDomNode dname = element.firstChild(); !dname.isNull();
+- dname = dname.nextSibling())
+- {
+- QDomText t = dname.toText();
+- if (!t.isNull())
+- return t.data();
+- }
+- return QString();
+-}
+-
+-ChannelInfo *XMLTVParser::parseChannel(QDomElement &element, QUrl &baseUrl)
+-{
+- auto *chaninfo = new ChannelInfo;
+-
+- QString xmltvid = element.attribute("id", "");
+-
+- chaninfo->m_xmltvId = xmltvid;
+- chaninfo->m_tvFormat = "Default";
+-
+- for (QDomNode child = element.firstChild(); !child.isNull();
+- child = child.nextSibling())
+- {
+- QDomElement info = child.toElement();
+- if (!info.isNull())
+- {
+- if (info.tagName() == "icon")
+- {
+- if (chaninfo->m_icon.isEmpty())
+- {
+- QString path = info.attribute("src", "");
+- if (!path.isEmpty() && !path.contains("://"))
+- {
+- QString base = baseUrl.toString(QUrl::StripTrailingSlash);
+- chaninfo->m_icon = base +
+- ((path.startsWith("/")) ? path :
QString("/") + path);
+- }
+- else if (!path.isEmpty())
+- {
+- QUrl url(path);
+- if (url.isValid())
+- chaninfo->m_icon = url.toString();
+- }
+- }
+- }
+- else if (info.tagName() == "display-name")
+- {
+- if (chaninfo->m_name.isEmpty())
+- {
+- chaninfo->m_name = info.text();
+- }
+- else if (chaninfo->m_callSign.isEmpty())
+- {
+- chaninfo->m_callSign = info.text();
+- }
+- else if (chaninfo->m_chanNum.isEmpty())
+- {
+- chaninfo->m_chanNum = info.text();
+- }
+- }
+- }
+- }
+-
+- chaninfo->m_freqId = chaninfo->m_chanNum;
+- return chaninfo;
+-}
+-
+ static void fromXMLTVDate(QString ×tr, QDateTime &dt)
+ {
+ // The XMLTV spec requires dates to either be in UTC/GMT or to specify a
+@@ -223,12 +149,12 @@ static void fromXMLTVDate(QString ×tr, QDateTime &dt)
+
+ QDateTime tmpDT = QDateTime(tmpDate, tmpTime, Qt::UTC);
+ if (!tmpDT.isValid())
+- {
+- LOG(VB_XMLTV, LOG_ERR,
+- QString("Invalid datetime (combination of date/time) "
++ {
++ LOG(VB_XMLTV, LOG_ERR,
++ QString("Invalid datetime (combination of date/time) "
+ "in XMLTV data, ignoring: %1").arg(timestr));
+- return;
+- }
++ return;
++ }
+
+ // While this seems like a hack, it's better than what was done before
+ QString isoDateString = tmpDT.toString(Qt::ISODate);
+@@ -248,485 +174,551 @@ static void fromXMLTVDate(QString ×tr, QDateTime
&dt)
+ timestr = MythDate::toString(dt, MythDate::kFilename);
+ }
+
+-static void parseCredits(QDomElement &element, ProgInfo *pginfo)
++static int readNextWithErrorCheck(QXmlStreamReader &xml)
+ {
+- for (QDomNode child = element.firstChild(); !child.isNull();
+- child = child.nextSibling())
++ xml.readNext();
++ if (xml.hasError())
+ {
+- QDomElement info = child.toElement();
+- if (!info.isNull())
+- pginfo->AddPerson(info.tagName(), getFirstText(info));
++ LOG(VB_GENERAL, LOG_ERR, QString("Malformed XML file at line %1,
%2").arg(xml.lineNumber()).arg(xml.errorString()));
++ return false;
+ }
++ return true;
+ }
+
+-static void parseVideo(QDomElement &element, ProgInfo *pginfo)
++bool XMLTVParser::parseFile(
++ const QString& filename, ChannelInfoList *chanlist,
++ QMap<QString, QList<ProgInfo> > *proglist)
+ {
+- for (QDomNode child = element.firstChild(); !child.isNull();
+- child = child.nextSibling())
++ m_movieGrabberPath = MetadataDownload::GetMovieGrabber();
++ m_tvGrabberPath = MetadataDownload::GetTelevisionGrabber();
++ QFile f;
++ if (!dash_open(f, filename, QIODevice::ReadOnly))
+ {
+- QDomElement info = child.toElement();
+- if (!info.isNull())
+- {
+- if (info.tagName() == "quality")
+- {
+- if (getFirstText(info) == "HDTV")
+- pginfo->m_videoProps |= VID_HDTV;
+- }
+- else if (info.tagName() == "aspect")
+- {
+- if (getFirstText(info) == "16:9")
+- pginfo->m_videoProps |= VID_WIDESCREEN;
+- }
+- }
++ LOG(VB_GENERAL, LOG_ERR,
++ QString("Error unable to open '%1' for reading.")
.arg(filename));
++ return false;
+ }
+-}
+
+-static void parseAudio(QDomElement &element, ProgInfo *pginfo)
+-{
+- for (QDomNode child = element.firstChild(); !child.isNull();
+- child = child.nextSibling())
++ QXmlStreamReader xml(&f);
++ QUrl baseUrl;
++ QUrl sourceUrl;
++ QString aggregatedTitle;
++ QString aggregatedDesc;
++ bool haveReadTV = false;
++ QString last_channel = ""; //xmltvId of the last program element we read
++ QDateTime last_starttime; //starttime of the last program element we read
++ 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")
+- {
+- pginfo->m_title_pronounce = getFirstText(info);
+- }
+- else if (pginfo->m_title.isEmpty())
++ if (!haveReadTV)
+ {
+- pginfo->m_title = 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 (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))
++ 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....
+- 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")
++ {
++ 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_totalepisodes = totalepisodes.toUInt();
++ 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....
++ 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);
+-
+- if (!pginfo->m_airdate
+- && ProgramInfo::kCategorySeries != pginfo->m_categoryType)
+- pginfo->m_airdate = m_currentYear;
+-
+- if (programid.isEmpty())
+- {
+-
+- /* Let's build ourself a programid */
++ while (! (xml.isEndElement() && xml.name() ==
"programme"));
+
+- 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";
++ if (pginfo->m_category.isEmpty() && pginfo->m_categoryType
!= ProgramInfo::kCategoryNone)
++ pginfo->m_category =
myth_category_type_to_string(pginfo->m_categoryType);
+
+- QString seriesid = QString::number(ELFHash(pginfo->m_title.toUtf8()));
+- pginfo->m_seriesId = seriesid;
+- programid.append(seriesid);
++ if (!pginfo->m_airdate && ProgramInfo::kCategorySeries !=
pginfo->m_categoryType)
++ pginfo->m_airdate = m_currentYear;
+
+- 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", ""));
++ //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";
+
+- QString aggregatedTitle;
+- QString aggregatedDesc;
+-
+- 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_channel != last_channel) {
++ //we have a channel change here
++ last_channel = pginfo->m_channel;
++ last_starttime = QDateTime(QDate(1970, 1, 1), QTime(0, 0, 0));
//initialize it to a time far, far away ...
++ }
++ else {
++ //we are still on the same channel
++ if (pginfo->m_starttime >= last_starttime) {
++ last_starttime = pginfo->m_starttime;
++ }
++ else {
++ LOG(VB_GENERAL, LOG_ERR, QString("Malformed XML file,
program out of order at line %1, %2").arg(xml.lineNumber()).arg(xml.errorString()));
++ return false;
++ }
++ }
++
+ if (pginfo->m_clumpidx.isEmpty())
+ (*proglist)[pginfo->m_channel].push_back(*pginfo);
+ else
+@@ -737,22 +729,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 +750,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 6d06aefe405..9fad22dc1e4 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);
+
+
+From a2b8c262dc96274ef55be25c510a4bbe9b6b52b2 Mon Sep 17 00:00:00 2001
+From: Mark Kendall <mark.kendall(a)gmail.com>
+Date: Tue, 3 Mar 2020 11:31:18 +0000
+Subject: [PATCH 38/47] mythfilldatabase: Fix 2 potential leaks
+
+- introduced in a9aa006139da24cb and picked up by coverity.
+
+(cherry picked from commit cf282591a249864215c82cb7248153b3033d6ea1)
+---
+ mythtv/programs/mythfilldatabase/xmltvparser.cpp | 4 ++++
+ 1 file changed, 4 insertions(+)
+
+diff --git a/mythtv/programs/mythfilldatabase/xmltvparser.cpp
b/mythtv/programs/mythfilldatabase/xmltvparser.cpp
+index 66157ffc648..a875020462b 100644
+--- a/mythtv/programs/mythfilldatabase/xmltvparser.cpp
++++ b/mythtv/programs/mythfilldatabase/xmltvparser.cpp
+@@ -236,7 +236,10 @@ bool XMLTVParser::parseFile(
+ do
+ {
+ if (!readNextWithErrorCheck(xml))
++ {
++ delete chaninfo;
+ return false;
++ }
+ if (xml.name() == "icon")
+ {
+ if (chaninfo->m_icon.isEmpty())
+@@ -715,6 +718,7 @@ bool XMLTVParser::parseFile(
+ }
+ else {
+ LOG(VB_GENERAL, LOG_ERR, QString("Malformed XML file,
program out of order at line %1, %2").arg(xml.lineNumber()).arg(xml.errorString()));
++ delete pginfo;
+ return false;
+ }
+ }
+
+From c8e779649384e9810bf8b2262a8f928b1986994d Mon Sep 17 00:00:00 2001
+From: Roland Ernst <rcrernst(a)gmail.com>
+Date: Fri, 6 Mar 2020 18:19:45 +0100
+Subject: [PATCH 39/47] BackendServerAddr and MasterServerName replaced
+ MasterServerIP
+
+in V30, according ticket #13024.
+
+Replace any occurrences of the deprecated settings and allow
+'BackendServerAddr(MasterServerName)' to be an alias for the
+host-ip as well.
+
+Fixes #13593
+
+(cherry picked from commit 105faee393b682e79a336d7ee893f82f8c10a896)
+---
+ mythtv/bindings/python/MythTV/database.py | 12 +---
+ mythtv/bindings/python/MythTV/methodheap.py | 42 ++++++++------
+ mythtv/bindings/python/MythTV/mythproto.py | 55 ++++++++++---------
+ .../python/MythTV/utility/__init__.py | 3 +-
+ .../bindings/python/MythTV/utility/other.py | 9 +++
+ 5 files changed, 66 insertions(+), 55 deletions(-)
+
+diff --git a/mythtv/bindings/python/MythTV/database.py
b/mythtv/bindings/python/MythTV/database.py
+index 7e3c4fe42be..e8b8bf5d52d 100644
+--- a/mythtv/bindings/python/MythTV/database.py
++++ b/mythtv/bindings/python/MythTV/database.py
+@@ -1338,15 +1338,7 @@ def _check_schema(self, value, local, name='Database',
update=None):
+
+ 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 @@ def gethostname(self):
+ 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/methodheap.py
b/mythtv/bindings/python/MythTV/methodheap.py
+index 04c7e98bb48..404864618c9 100644
+--- a/mythtv/bindings/python/MythTV/methodheap.py
++++ b/mythtv/bindings/python/MythTV/methodheap.py
+@@ -8,7 +8,7 @@
+ 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 @@ def scanStorageGroups(self, delete=True):
+ 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 @@ def __init__(self, backend=None, port=None, db=None):
+ 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 b388e9619ff..6b00ba222cb 100644
+--- a/mythtv/bindings/python/MythTV/mythproto.py
++++ b/mythtv/bindings/python/MythTV/mythproto.py
+@@ -12,7 +12,7 @@
+ 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 @@ def __init__(self, backend=None, blockshutdown=False, events=False,
db=None):
+ 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:
+diff --git a/mythtv/bindings/python/MythTV/utility/__init__.py
b/mythtv/bindings/python/MythTV/utility/__init__.py
+index 4f8d060a23f..1ca7087d7b2 100644
+--- a/mythtv/bindings/python/MythTV/utility/__init__.py
++++ b/mythtv/bindings/python/MythTV/utility/__init__.py
+@@ -7,5 +7,6 @@
+
+ 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/other.py
b/mythtv/bindings/python/MythTV/utility/other.py
+index bb8f29630dc..7f5b0c759c8 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]
+
+From ce23a0225fcec2afbdfe5a7e82170e28f406c830 Mon Sep 17 00:00:00 2001
+From: Roland Ernst <rcrernst(a)gmail.com>
+Date: Sun, 10 May 2020 12:22:25 +0200
+Subject: [PATCH 40/47] Fix mysql cursor class to handle bytearrays
+
+Newer Python MySQLdb modules call 'cursor.execute()' multiple times
+from 'cursor.executemany()'.
+With python3 and python3-MySQLdb > 1.4.0 these call-backs containing
+a query are bytearrays, resulting in a traceback in the '_sanitize' method.
+
+Note: This '_sanitize' method is only needed when creating a query within
+the Python Bindings, but not necessary when python-mysqldb itself calls the
+cursor.execute() method.
+
+Fixes #13614
+
+(cherry picked from commit b2e9c6a44233570704554894bf45e01bfa8e26a7)
+---
+ mythtv/bindings/python/MythTV/_conn_mysqldb.py | 6 +++++-
+ 1 file changed, 5 insertions(+), 1 deletion(-)
+
+diff --git a/mythtv/bindings/python/MythTV/_conn_mysqldb.py
b/mythtv/bindings/python/MythTV/_conn_mysqldb.py
+index 177a880a121..3f798198219 100644
+--- a/mythtv/bindings/python/MythTV/_conn_mysqldb.py
++++ b/mythtv/bindings/python/MythTV/_conn_mysqldb.py
+@@ -41,7 +41,11 @@ def __init__(self, connection):
+ 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):
+
+From 6a5afb4dba08fcd790279af97348f3e69ebec8c3 Mon Sep 17 00:00:00 2001
+From: Mark Kendall <mark.kendall(a)gmail.com>
+Date: Tue, 19 May 2020 18:08:50 +0100
+Subject: [PATCH 41/47] VDPAU: Try and fall 'back' to H264 Main support
+
+- if H264 constrained baseline is reported as not supported
+- this mimics the additional test carried out in FFmpeg
+- N.B. untested but as far as I can tell this should get this profile
+working again on older chipsets.
+
+(cherry picked from commit 7eb2231803ec9851d91bc7461c184079c65385d5)
+---
+ .../libs/libmythtv/decoders/mythvdpauhelper.cpp | 17 +++++++++++++++++
+ 1 file changed, 17 insertions(+)
+
+diff --git a/mythtv/libs/libmythtv/decoders/mythvdpauhelper.cpp
b/mythtv/libs/libmythtv/decoders/mythvdpauhelper.cpp
+index b8d3b2d1ef2..47b4a927bed 100644
+--- a/mythtv/libs/libmythtv/decoders/mythvdpauhelper.cpp
++++ b/mythtv/libs/libmythtv/decoders/mythvdpauhelper.cpp
+@@ -84,6 +84,23 @@ bool MythVDPAUHelper::ProfileCheck(VdpDecoderProfile Profile, uint32_t
&Level,
+ status = m_vdpDecoderQueryCapabilities(m_device, Profile, &supported,
+ &Level, &Macros, &Width,
&Height);
+ CHECK_ST
++
++ if (((supported != VDP_TRUE) || (status != VDP_STATUS_OK)) &&
++ (Profile == VDP_DECODER_PROFILE_H264_CONSTRAINED_BASELINE))
++ {
++ // 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.
++ status = m_vdpDecoderQueryCapabilities(m_device, VDP_DECODER_PROFILE_H264_MAIN,
&supported,
++ &Level, &Macros, &Width,
&Height);
++ if (supported > 0)
++ {
++ LOG(VB_GENERAL, LOG_INFO, LOC + "Driver does not report support for
H264 Constrained Baseline");
++ LOG(VB_GENERAL, LOG_INFO, LOC + " - but assuming available as H264 Main
is supported");
++ }
++ CHECK_ST
++ }
++
+ return supported > 0;
+ }
+
+
+From 7a31a2e35ccf338952f377d2885eb3af81defb54 Mon Sep 17 00:00:00 2001
+From: John Poet <jpoet(a)mythtv.org>
+Date: Tue, 12 May 2020 18:51:52 -0600
+Subject: [PATCH 42/47] mythexternrecorder: Add ondatastart command option
+
+The config file can now specify a command to run as soon as data is detected
+from the 'external' application.
+
+(cherry picked from commit e4d9172d6e1e636fd61e1484f858cb4010380b50)
+---
+ .../mythexternrecorder/MythExternControl.cpp | 7 ++++
+ .../mythexternrecorder/MythExternControl.h | 2 ++
+ .../mythexternrecorder/MythExternRecApp.cpp | 32 +++++++++++++++++++
+ .../mythexternrecorder/MythExternRecApp.h | 2 ++
+ mythtv/programs/mythexternrecorder/main.cpp | 2 ++
+ 5 files changed, 45 insertions(+)
+
+diff --git a/mythtv/programs/mythexternrecorder/MythExternControl.cpp
b/mythtv/programs/mythexternrecorder/MythExternControl.cpp
+index 258dc64dc57..a29efa17a99 100644
+--- a/mythtv/programs/mythexternrecorder/MythExternControl.cpp
++++ b/mythtv/programs/mythexternrecorder/MythExternControl.cpp
+@@ -513,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 172bf5bc1f7..57d26465fcd 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;
+ };
+@@ -152,6 +153,7 @@ class MythExternControl : public QObject
+ 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 5a8acb28619..29bc0fd2bd9 100644
+--- a/mythtv/programs/mythexternrecorder/MythExternRecApp.cpp
++++ b/mythtv/programs/mythexternrecorder/MythExternRecApp.cpp
+@@ -87,6 +87,7 @@ bool MythExternRecApp::config(void)
+ m_recDesc = settings.value("RECORDER/desc").toString();
+ m_cleanup = settings.value("RECORDER/cleanup").toString();
+ m_tuneCommand = settings.value("TUNER/command", "").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();
+@@ -297,6 +298,37 @@ Q_SLOT void MythExternRecApp::Cleanup(void)
+ LOG(VB_RECORD, LOG_INFO, LOC + ": Cleanup finished.");
+ }
+
++Q_SLOT void MythExternRecApp::DataStarted(void)
++{
++ if (m_onDataStart.isEmpty())
++ return;
++
++ QString cmd = m_onDataStart;
++
++ 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())
+diff --git a/mythtv/programs/mythexternrecorder/MythExternRecApp.h
b/mythtv/programs/mythexternrecorder/MythExternRecApp.h
+index af87e21e272..63f549e8836 100644
+--- a/mythtv/programs/mythexternrecorder/MythExternRecApp.h
++++ b/mythtv/programs/mythexternrecorder/MythExternRecApp.h
+@@ -70,6 +70,7 @@ class MythExternRecApp : public QObject
+ 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);
+@@ -108,6 +109,7 @@ class MythExternRecApp : public QObject
+
+ QProcess m_tuneProc;
+ QString m_tuneCommand;
++ QString m_onDataStart;
+ QString m_channelsIni;
+ uint m_lockTimeout { 0 };
+
+diff --git a/mythtv/programs/mythexternrecorder/main.cpp
b/mythtv/programs/mythexternrecorder/main.cpp
+index 833ecb8a8c6..7bbff574f27 100644
+--- a/mythtv/programs/mythexternrecorder/main.cpp
++++ b/mythtv/programs/mythexternrecorder/main.cpp
+@@ -114,6 +114,8 @@ int main(int argc, char *argv[])
+ 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,
+
+From fa2165511fe0735c612be69445b6acafc05e4caa Mon Sep 17 00:00:00 2001
+From: John Poet <jpoet(a)mythtv.org>
+Date: Sun, 17 May 2020 20:58:41 -0600
+Subject: [PATCH 43/47] mythexternrecorder: Add TUNER/newepisodecommand option.
+
+Some streaming sources have a "bandwidth saver" option and therefore need
+touched occationally to keep them going. If provided, this command will be
+executed whenever a new 'episode' starts up if the recording is already
+running on the desired "channel".
+
+(cherry picked from commit 07b49fc2546bbc4b4d7a85ffb4943d65a1986fbb)
+---
+ .../mythexternrecorder/MythExternRecApp.cpp | 34 +++++++++++++++++++
+ .../mythexternrecorder/MythExternRecApp.h | 2 ++
+ 2 files changed, 36 insertions(+)
+
+diff --git a/mythtv/programs/mythexternrecorder/MythExternRecApp.cpp
b/mythtv/programs/mythexternrecorder/MythExternRecApp.cpp
+index 29bc0fd2bd9..6ce2d9919cf 100644
+--- a/mythtv/programs/mythexternrecorder/MythExternRecApp.cpp
++++ b/mythtv/programs/mythexternrecorder/MythExternRecApp.cpp
+@@ -87,6 +87,7 @@ bool MythExternRecApp::config(void)
+ 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();
+@@ -452,6 +453,36 @@ Q_SLOT void MythExternRecApp::NextChannel(const QString &
serial)
+ GetChannel(serial, "NextChannel");
+ }
+
++void MythExternRecApp::NewEpisodeStarting(const QString & channum)
++{
++ 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_RECORD, LOG_ERR, LOC +
++ " NewEpisodeStarting: Failed to start process: " + ENO);
++ return;
++ }
++ proc.waitForFinished(5000);
++ if (proc.state() == QProcess::NotRunning)
++ {
++ if (proc.exitStatus() != QProcess::NormalExit)
++ {
++ LOG(VB_RECORD, LOG_ERR, LOC +
++ " NewEpisodeStarting: process failed: " + ENO);
++ return;
++ }
++ }
++
++ LOG(VB_RECORD, LOG_INFO, LOC + "NewEpisodeStarting: finished.");
++}
++
+ Q_SLOT void MythExternRecApp::TuneChannel(const QString & serial,
+ const QString & channum)
+ {
+@@ -464,6 +495,9 @@ Q_SLOT void MythExternRecApp::TuneChannel(const QString &
serial,
+
+ if (m_tunedChannel == channum)
+ {
++ if (!m_newEpisodeCommand.isEmpty())
++ NewEpisodeStarting(channum);
++
+ LOG(VB_CHANNEL, LOG_INFO, LOC +
+ QString("TuneChanne: Already on %1").arg(channum));
+ emit SendMessage("TuneChannel", serial,
+diff --git a/mythtv/programs/mythexternrecorder/MythExternRecApp.h
b/mythtv/programs/mythexternrecorder/MythExternRecApp.h
+index 63f549e8836..5d105691e26 100644
+--- a/mythtv/programs/mythexternrecorder/MythExternRecApp.h
++++ b/mythtv/programs/mythexternrecorder/MythExternRecApp.h
+@@ -75,6 +75,7 @@ class MythExternRecApp : public QObject
+ 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);
+@@ -110,6 +111,7 @@ class MythExternRecApp : public QObject
+ QProcess m_tuneProc;
+ QString m_tuneCommand;
+ QString m_onDataStart;
++ QString m_newEpisodeCommand;
+ QString m_channelsIni;
+ uint m_lockTimeout { 0 };
+
+
+From f9baf09e4397032f1a00d98b5085f912547380b6 Mon Sep 17 00:00:00 2001
+From: Klaas de Waal <kdewaal(a)mythtv.org>
+Date: Sun, 17 May 2020 19:57:18 +0200
+Subject: [PATCH 44/47] Crash of backend on delete of program being recorded
+
+Fix this crash and similar backend crashes in the scheduler
+by replacing all iterations over m_tvList/m_encoderLink/m_pEncoders
+from using the Qt extension foreach to the C++11 range-based for loop.
+The foreach apparently makes a deep copy of the container thereby
+invalidating the iterators that may be active on the same container
+simultaneously in a different thread.
+As an additional safeguard the qAsConst, a Qt-specific variant
+of std:as_const, is added to all loops.
+
+Fixes #13571
+
+(cherry picked from commit 8e2872679315547abc3c2f1a91e0f7b8baac79dc)
+Signed-off-by: Klaas de Waal <kdewaal(a)mythtv.org>
+---
+ mythtv/programs/mythbackend/httpstatus.cpp | 2 +-
+ mythtv/programs/mythbackend/mainserver.cpp | 22 ++++++++-------
+ mythtv/programs/mythbackend/scheduler.cpp | 28 ++++++++++----------
+ mythtv/programs/mythbackend/services/dvr.cpp | 2 +-
+ 4 files changed, 28 insertions(+), 26 deletions(-)
+
+diff --git a/mythtv/programs/mythbackend/httpstatus.cpp
b/mythtv/programs/mythbackend/httpstatus.cpp
+index 011c7f6ee1f..1dc6e6f038b 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/mainserver.cpp
b/mythtv/programs/mythbackend/mainserver.cpp
+index 9d627c663b5..072b1fb8887 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])
+ {
+@@ -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()))
+@@ -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();
+@@ -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)
+ {
+@@ -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;
+@@ -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 42d8e1c8494..b787be27ce3 100644
+--- a/mythtv/programs/mythbackend/scheduler.cpp
++++ b/mythtv/programs/mythbackend/scheduler.cpp
+@@ -2514,7 +2514,7 @@ void Scheduler::HandleWakeSlave(RecordingInfo &ri, int
prerollseconds)
+ QReadLocker tvlocker(&TVRec::s_inputsLock);
+
+ QMap<int, EncoderLink*>::const_iterator tvit =
m_tvList->constFind(ri.GetInputID());
+- if (tvit == m_tvList->end())
++ if (tvit == m_tvList->constEnd())
+ return;
+
+ QString sysEventKey = ri.MakeUniqueKey();
+@@ -2597,7 +2597,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);
+@@ -2672,7 +2672,7 @@ bool Scheduler::HandleRecording(
+ QReadLocker tvlocker(&TVRec::s_inputsLock);
+
+ QMap<int, EncoderLink*>::const_iterator tvit =
m_tvList->constFind(ri.GetInputID());
+- if (tvit == m_tvList->end())
++ if (tvit == m_tvList->constEnd())
+ {
+ QString msg = QString("Invalid cardid [%1] for %2")
+ .arg(ri.GetInputID()).arg(ri.GetTitle());
+@@ -2754,7 +2754,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);
+@@ -3442,7 +3442,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;
+@@ -3524,7 +3524,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 +3548,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 +3566,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 +3596,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 +3606,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 +3630,7 @@ void Scheduler::WakeUpSlaves(void)
+
+ QStringList SlavesThatCanWake;
+ QString thisSlave;
+- foreach (auto enc, *m_tvList)
++ for (auto * enc : qAsConst(*m_tvList))
+ {
+ if (enc->IsLocal())
+ continue;
+@@ -4304,7 +4304,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;
+@@ -5438,7 +5438,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())
+@@ -5596,7 +5596,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/services/dvr.cpp
b/mythtv/programs/mythbackend/services/dvr.cpp
+index 07eb8e74cc5..d5b2702877f 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)
+ {
+
+From 1ca7a4b09ef38cd6e108a26bdc358f280d6ae3d3 Mon Sep 17 00:00:00 2001
+From: Roland Ernst <rcrernst(a)gmail.com>
+Date: Thu, 21 May 2020 16:27:21 -0500
+Subject: [PATCH 45/47] Python Bindings: Services API, logging & XML
+ enhancements
+
+- Improve logging dump of 'postdata'
+- Add an option to return raw XML data, {'rawxml': True}
+
+Some lxml.etree functions, e.g. fromstring()/tostring() cause pylint 'I'
+messages. users may want to add the following to their .pylintrc (or just
+add # pylint: disable=c-extension-no-member inline):
+
+ [MESSAGE-CONTROL]
+ disable=c-extension-no-member
+
+Closes #13619
+
+Signed-off-by: Bill Meek <billmeek(a)mythtv.org>
+(cherry picked from commit 1a1b69836515a5ec77b09c4559a3bb729af9cd7b)
+---
+ .../bindings/python/MythTV/services_api/send.py | 15 +++++++++++----
+ 1 file changed, 11 insertions(+), 4 deletions(-)
+
+diff --git a/mythtv/bindings/python/MythTV/services_api/send.py
b/mythtv/bindings/python/MythTV/services_api/send.py
+index 1f49389508f..fa817066f27 100644
+--- a/mythtv/bindings/python/MythTV/services_api/send.py
++++ b/mythtv/bindings/python/MythTV/services_api/send.py
+@@ -141,6 +141,10 @@ def send(self, endpoint='', postdata=None, rest='',
opts=None):
+ 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 @@ def send(self, endpoint='', postdata=None, rest='',
opts=None):
+ 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 @@ def _set_missing_opts(self):
+ 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 @@ def _validate_postdata(self):
+ 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 @@ def _create_session(self):
+ 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'})
+
+From 9380616198f4149d20bb5ef41c3ca0f14944290b Mon Sep 17 00:00:00 2001
+From: Gary Buhrmaster <gary.buhrmaster(a)gmail.com>
+Date: Mon, 25 May 2020 11:52:11 -0500
+Subject: [PATCH 46/47] Always request a reschedule when running
+ mythfilldatabase
+
+Previous edits incorrectly left it where the request was only done
+when repeats were marked.
+
+Fixes #13625
+
+Signed-off-by: David Engel <dengel(a)mythtv.org>
+(cherry picked from commit 1ba15e5cdb91f121b60ae96358021d668cf73a71)
+---
+ mythtv/programs/mythfilldatabase/main.cpp | 5 ++---
+ 1 file changed, 2 insertions(+), 3 deletions(-)
+
+diff --git a/mythtv/programs/mythfilldatabase/main.cpp
b/mythtv/programs/mythfilldatabase/main.cpp
+index 3f764b16fb2..ef20217f1ce 100644
+--- a/mythtv/programs/mythfilldatabase/main.cpp
++++ b/mythtv/programs/mythfilldatabase/main.cpp
+@@ -660,9 +660,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");
+
+
+From fc9048228105e0bf416990f97c3ce3c2eceb3201 Mon Sep 17 00:00:00 2001
+From: Gary Buhrmaster <gary.buhrmaster(a)gmail.com>
+Date: Mon, 25 May 2020 12:02:17 -0500
+Subject: [PATCH 47/47] mythfilldatabase: remove program starttime order check
+
+Closes #13623
+
+Signed-off-by: Bill Meek <billmeek(a)mythtv.org>
+(cherry picked from commit 1f8b759dd7b047bde1b5c52f2471c79cd1619e30)
+---
+ .../programs/mythfilldatabase/xmltvparser.cpp | 19 -------------------
+ 1 file changed, 19 deletions(-)
+
+diff --git a/mythtv/programs/mythfilldatabase/xmltvparser.cpp
b/mythtv/programs/mythfilldatabase/xmltvparser.cpp
+index a875020462b..0024819bc3d 100644
+--- a/mythtv/programs/mythfilldatabase/xmltvparser.cpp
++++ b/mythtv/programs/mythfilldatabase/xmltvparser.cpp
+@@ -205,8 +205,6 @@ bool XMLTVParser::parseFile(
+ QString aggregatedTitle;
+ QString aggregatedDesc;
+ bool haveReadTV = false;
+- QString last_channel = ""; //xmltvId of the last program element we read
+- QDateTime last_starttime; //starttime of the last program element we read
+ while (!xml.atEnd() && !xml.hasError() && (! (xml.isEndElement()
&& xml.name() == "tv")))
+ {
+ if (xml.readNextStartElement())
+@@ -706,23 +704,6 @@ bool XMLTVParser::parseFile(
+ else
+ {
+ // so we have a (relatively) clean program element now, which is
good enough to process or to store
+- if (pginfo->m_channel != last_channel) {
+- //we have a channel change here
+- last_channel = pginfo->m_channel;
+- last_starttime = QDateTime(QDate(1970, 1, 1), QTime(0, 0, 0));
//initialize it to a time far, far away ...
+- }
+- else {
+- //we are still on the same channel
+- if (pginfo->m_starttime >= last_starttime) {
+- last_starttime = pginfo->m_starttime;
+- }
+- else {
+- LOG(VB_GENERAL, LOG_ERR, QString("Malformed XML file,
program out of order at line %1, %2").arg(xml.lineNumber()).arg(xml.errorString()));
+- delete pginfo;
+- return false;
+- }
+- }
+-
+ if (pginfo->m_clumpidx.isEmpty())
+ (*proglist)[pginfo->m_channel].push_back(*pginfo);
+ else