commit 36a6afcff19f5bbca7e2de7f3139f657f6f69bc4
Author: Andrew Bauer <zonexpertconsulting(a)outlook.com>
Date: Sat Jan 22 07:37:09 2022 -0600
Update to latest fixes/31
Reenable libcec on el8
mythtv.spec | 13 +-
v31.0..25f1bb1d12.patch => v31.0..4f7953f6ee.patch | 4929 +++++++++++++++++++-
2 files changed, 4771 insertions(+), 171 deletions(-)
---
diff --git a/mythtv.spec b/mythtv.spec
index 6d1ee8b..92aa487 100644
--- a/mythtv.spec
+++ b/mythtv.spec
@@ -1,10 +1,9 @@
# The full MythTV Version string is computed from the output of git describe.
-%global vers_string v31.0-167-g25f1bb1d12
+%global vers_string v31.0-173-g4f7953f6ee
# The git date of last commit on mythtv repo
# git_date=$(git log -1 --format=%cd --date=format:"%Y%m%d")
-%global git_date 20211108
-
+%global git_date 20220120
# Specfile for building MythTV and MythPlugins RPMs from a git checkout.
#
@@ -76,7 +75,7 @@
#
Name: mythtv
Version: 31.0
-Release: 24%{rel_string}%{?dist}
+Release: 25%{rel_string}%{?dist}
Summary: A digital video recorder (DVR) application
# The primary license is GPLv2+, but bits are borrowed from a number of
@@ -171,9 +170,7 @@ BuildRequires: mariadb-connector-c-devel
%else
BuildRequires: mariadb-devel >= 5
%endif
-%if 0%{?fedora} || 0%{?rhel} < 8
BuildRequires: libcec-devel >= 1.7
-%endif
BuildRequires: libvpx-devel
BuildRequires: lm_sensors-devel
BuildRequires: lirc-devel
@@ -1404,6 +1401,10 @@ exit 0
################################################################################
%changelog
+* Sat Jan 22 2021 Andrew Bauer <zonexpertconsulting(a)outlook.com> -
31.0-25.167.20220120gitg4f7953f6e
+- Update to latest fixes/31
+- Reenable libcec on el8
+
* Tue Dec 14 2021 Andrew Bauer <zonexpertconsulting(a)outlook.com> -
31.0-24.167.20211108git25f1bb1d12
- Don't require mariadb. Let end user choose the db engine.
diff --git a/v31.0..25f1bb1d12.patch b/v31.0..4f7953f6ee.patch
similarity index 85%
rename from v31.0..25f1bb1d12.patch
rename to v31.0..4f7953f6ee.patch
index c69d62c..6df8beb 100644
--- a/v31.0..25f1bb1d12.patch
+++ b/v31.0..4f7953f6ee.patch
@@ -1,7 +1,7 @@
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 001/165] MythCodecContext: Ignore hardware decoders when there
+Subject: [PATCH 001/171] MythCodecContext: Ignore hardware decoders when there
is no GUI
(cherry picked from commit 1e06407c6edb2f73eacb3b7bb9782bd9375912cd)
@@ -30,7 +30,7 @@ index 0880bf8212f..5932fafc780 100644
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 002/165] libmythtv.pro: Typo
+Subject: [PATCH 002/171] libmythtv.pro: Typo
(cherry picked from commit 59b00df23e73d842552c0105a7f51c6a12de7796)
---
@@ -54,7 +54,7 @@ index 036228f2d31..f1b0248d220 100644
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 003/165] Wait for NIT or MGT when scanning
+Subject: [PATCH 003/171] 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
@@ -108,7 +108,7 @@ index 52c6bb1d606..0f2ebae0744 100644
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 004/165] T2_terrestrial_delivery_system debug output
+Subject: [PATCH 004/171] T2_terrestrial_delivery_system debug output
Debug output of the T2 terrestrial delivery system descriptor added.
First version with only the mandatory fields.
@@ -205,7 +205,7 @@ index e53a24e2c15..7d026ef10b1 100644
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 005/165] Signal strength of scanned transports
+Subject: [PATCH 005/171] 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.
@@ -453,7 +453,7 @@ index d51a33ef087..a6c1e2c1681 100644
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 006/165] Add Full Scan option for DVB-C Netherlands
+Subject: [PATCH 006/171] 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
@@ -528,7 +528,7 @@ index 55e021fa5cb..26416adfb74 100644
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 007/165] Scan option "Remove duplicate channels"
+Subject: [PATCH 007/171] Scan option "Remove duplicate channels"
Add new scan option to remove duplicate transports and duplicate
channels based on signal strength of the received signal.
@@ -1150,7 +1150,7 @@ index 67ac7d54335..5f612cc2536 100644
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 008/165] Fix for "Remove duplicate channels" scan option
+Subject: [PATCH 008/171] Fix for "Remove duplicate channels" scan option
Fix counting bug in this new feature.
Fixed corner case in updating existing channels where
@@ -1368,7 +1368,7 @@ index 5f15d1f1b16..bfec5028844 100644
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 009/165] Updated "Remove duplicates" channel scan option
+Subject: [PATCH 009/171] 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.
@@ -1708,7 +1708,7 @@ index e7ba30e6756..a2e2979ec0b 100644
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 010/165] Python: Update JOBTYPEs
+Subject: [PATCH 010/171] 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,
@@ -1735,7 +1735,7 @@ index 167d71377ac..1f9e1203f63 100644
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 011/165] VAAPI: Fix direct rendering for Intel iHD series
+Subject: [PATCH 011/171] VAAPI: Fix direct rendering for Intel iHD series
drivers
---
@@ -1804,7 +1804,7 @@ index 9819e981409..65bf676e3be 100644
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 012/165] MythNVDECContext: Additional logging for decoder
+Subject: [PATCH 012/171] MythNVDECContext: Additional logging for decoder
check
---
@@ -1957,7 +1957,7 @@ index a54a340acc6..56f8187647c 100644
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 013/165] NVDEC: Fix decoder support check
+Subject: [PATCH 013/171] NVDEC: Fix decoder support check
(cherry picked from commit a2f19766c768c5ef40f596e1edf30dc3afb6889c)
---
@@ -1981,7 +1981,7 @@ index 56f8187647c..04cb4601971 100644
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 014/165] configure: enable by default gnutls support in our
+Subject: [PATCH 014/171] configure: enable by default gnutls support in our
copy of ffmpeg
This is required to support playback from protocols using these schema :-
@@ -2059,7 +2059,7 @@ index 77aee2d0768..3a7ec61fd74 100755
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 015/165] Fix improper sorting of names that start with "An".
+Subject: [PATCH 015/171] 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
@@ -2453,7 +2453,7 @@ index 478c77059ab..46fa7864631 100644
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 016/165] Use const_iterator for QMap m_encoderList
+Subject: [PATCH 016/171] 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.
@@ -2638,7 +2638,7 @@ index 7a9762ea46a..4ca86721472 100644
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 017/165] mythexternrec: Add a cleanup system command option to
+Subject: [PATCH 017/171] mythexternrec: Add a cleanup system command option to
the config file.
If [RECORDER][cleanup] is defined, it will be run whenever this external
@@ -2792,7 +2792,7 @@ index 71ca26079f1..e05e047d7cc 100644
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 018/165] mythexternrec: Track channum so an unnecessary tune
+Subject: [PATCH 018/171] mythexternrec: Track channum so an unnecessary tune
is not issued on back-to-back recordings.
(cherry picked from commit d8d3b7422b220cbecaec518b350373075797026e)
@@ -2875,7 +2875,7 @@ index fbaa2b7596c..b94e01d0c86 100644
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 019/165] Dvr::AddRecordSchedule: Allow last_record to be
+Subject: [PATCH 019/171] Dvr::AddRecordSchedule: Allow last_record to be
specified.
Scheduler::UpdateManuals: When creating the mythconverg.program entry,
@@ -3005,7 +3005,7 @@ index 7a6b1be80bc..5bf193c05ab 100644
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 020/165] mythexternrecorder: Allow use of channum with tuning
+Subject: [PATCH 020/171] mythexternrecorder: Allow use of channum with tuning
command, even without a channel configuration file.
(cherry picked from commit 356dd5e39a61e3a5e433508bd6afc937ca7c9e30)
@@ -3211,7 +3211,7 @@ index 967e5e89c2a..494d207242a 100644
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 021/165] ExternalChannel: When mythbackend is startting up,
+Subject: [PATCH 021/171] 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
@@ -3290,7 +3290,7 @@ index 243934301ee..1a7fc75a7aa 100644
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 022/165] MythExternRecorder: Add support for long channel
+Subject: [PATCH 022/171] MythExternRecorder: Add support for long channel
change times.
Add support for the external application to respond with "OK:Running" in
@@ -3724,7 +3724,7 @@ index e05e047d7cc..833ecb8a8c6 100644
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 023/165] ExtneralChannel: Use InProgress instead of running or
+Subject: [PATCH 023/171] ExtneralChannel: Use InProgress instead of running or
starting to indicate a long running tunning operation.
Thanks to Gary Buhrmaster for the suggestion.
@@ -3791,7 +3791,7 @@ index b9e02f6f2ea..1758d7e5395 100644
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 024/165] ExternRecorder: Fix live tv channel changes.
+Subject: [PATCH 024/171] ExternRecorder: Fix live tv channel changes.
(cherry picked from commit 18fa5fff1b9ac862cea0c5e8a3fb8981052bd6a3)
---
@@ -3823,7 +3823,7 @@ index 445325bc835..a2acc946557 100644
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 025/165] ExternalRecorder: Allow for optional ICON field is
+Subject: [PATCH 025/171] ExternalRecorder: Allow for optional ICON field is
channels.
Any ExternalRecorder which supports fetching channel information can
@@ -3996,7 +3996,7 @@ index 1758d7e5395..5a8acb28619 100644
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 026/165] version.sh: if found use DESCRIBE to get branch and
+Subject: [PATCH 026/171] 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
@@ -4115,7 +4115,7 @@ index fd2c0be875f..56c04457622 100755
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 027/165] VAAPI: Fix compilation for older drivers
+Subject: [PATCH 027/171] VAAPI: Fix compilation for older drivers
Fixes #13606
@@ -4145,7 +4145,7 @@ index 48be0267f11..c03766c6fad 100644
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 028/165] Fix typo in 0851b35e3ded43ea738473bc60b8e5d13595b922
+Subject: [PATCH 028/171] Fix typo in 0851b35e3ded43ea738473bc60b8e5d13595b922
comment
(cherry picked from commit dca115895bcc7631fd4eb9dcfa0b4d838ed1e786)
@@ -4170,7 +4170,7 @@ index 56c04457622..d412cc0505d 100755
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 029/165] FAQ: trivial change to force an update
+Subject: [PATCH 029/171] FAQ: trivial change to force an update
---
mythtv/FAQ | 2 +-
@@ -4191,7 +4191,7 @@ index 593d3b9fea6..e4f6c451095 100644
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 030/165] Fix "Full Scan" for DVB-T only tuners
+Subject: [PATCH 030/171] 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
@@ -4224,7 +4224,7 @@ index 543c7e1a9cd..98e9f9f7d4b 100644
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 031/165] HLSStreamHandler: fix the formatting of a debug
+Subject: [PATCH 031/171] HLSStreamHandler: fix the formatting of a debug
statement
Refs #13608
@@ -4251,7 +4251,7 @@ index 964f5396e46..22e0abae062 100644
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 032/165] Fix segfault in code called from
+Subject: [PATCH 032/171] Fix segfault in code called from
MythMainWindow::Draw.
This reverts three of the changes in 380102ce34. In
@@ -4319,7 +4319,7 @@ index 7bdfe41bb3b..619ba7f7f78 100644
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 033/165] Fix incorrect data provided to UPnP client.
+Subject: [PATCH 033/171] 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
@@ -4348,7 +4348,7 @@ index cda4f86a778..3a2bfbba3ce 100644
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 034/165] Compare to `None` using identity `is` operator
+Subject: [PATCH 034/171] Compare to `None` using identity `is` operator
This is a trivial change that replaces `==` operator with `is` operator, following PEP 8
guideline:
@@ -5730,7 +5730,7 @@ index 64eab727f03..76b98618920 100755
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 035/165] Python Bindings: fix warnings seen in *buntu
+Subject: [PATCH 035/171] Python Bindings: fix warnings seen in *buntu
packaging
(cherry picked from commit 12f44c74ed60e3b0909040f30665c9d3fc58c17c)
@@ -5787,7 +5787,7 @@ index d1d7546c0eb..f7966fb6d69 100644
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 036/165] libmythtv: Fix VideoToolbox framework name
+Subject: [PATCH 036/171] libmythtv: Fix VideoToolbox framework name
Closes #13609
@@ -5813,7 +5813,7 @@ index f1b0248d220..646577cd5b5 100644
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 037/165] mythfilldatabase: reduce memory usage.
+Subject: [PATCH 037/171] 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.
@@ -6983,7 +6983,7 @@ index 6d06aefe405..9fad22dc1e4 100644
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 038/165] mythfilldatabase: Fix 2 potential leaks
+Subject: [PATCH 038/171] mythfilldatabase: Fix 2 potential leaks
- introduced in a9aa006139da24cb and picked up by coverity.
@@ -7019,7 +7019,7 @@ index 66157ffc648..a875020462b 100644
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 039/165] BackendServerAddr and MasterServerName replaced
+Subject: [PATCH 039/171] BackendServerAddr and MasterServerName replaced
MasterServerIP
in V30, according ticket #13024.
@@ -7257,7 +7257,7 @@ index bb8f29630dc..7f5b0c759c8 100644
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 040/165] Fix mysql cursor class to handle bytearrays
+Subject: [PATCH 040/171] Fix mysql cursor class to handle bytearrays
Newer Python MySQLdb modules call 'cursor.execute()' multiple times
from 'cursor.executemany()'.
@@ -7296,7 +7296,7 @@ index 177a880a121..3f798198219 100644
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 041/165] VDPAU: Try and fall 'back' to H264 Main support
+Subject: [PATCH 041/171] 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
@@ -7340,7 +7340,7 @@ index b8d3b2d1ef2..47b4a927bed 100644
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 042/165] mythexternrecorder: Add ondatastart command option
+Subject: [PATCH 042/171] 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.
@@ -7479,7 +7479,7 @@ index 833ecb8a8c6..7bbff574f27 100644
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 043/165] mythexternrecorder: Add TUNER/newepisodecommand
+Subject: [PATCH 043/171] mythexternrecorder: Add TUNER/newepisodecommand
option.
Some streaming sources have a "bandwidth saver" option and therefore need
@@ -7576,7 +7576,7 @@ index 63f549e8836..5d105691e26 100644
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 044/165] Crash of backend on delete of program being recorded
+Subject: [PATCH 044/171] 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
@@ -7861,7 +7861,7 @@ index 07eb8e74cc5..d5b2702877f 100644
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 045/165] Python Bindings: Services API, logging & XML
+Subject: [PATCH 045/171] Python Bindings: Services API, logging & XML
enhancements
- Improve logging dump of 'postdata'
@@ -7940,7 +7940,7 @@ index 1f49389508f..fa817066f27 100644
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 046/165] Always request a reschedule when running
+Subject: [PATCH 046/171] Always request a reschedule when running
mythfilldatabase
Previous edits incorrectly left it where the request was only done
@@ -7974,7 +7974,7 @@ index 3f764b16fb2..ef20217f1ce 100644
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 047/165] mythfilldatabase: remove program starttime order
+Subject: [PATCH 047/171] mythfilldatabase: remove program starttime order
check
Closes #13623
@@ -8026,7 +8026,7 @@ index a875020462b..0024819bc3d 100644
From c89a7e3771ed094662710fdb18fe972d09a55955 Mon Sep 17 00:00:00 2001
From: Mark Kendall <mark.kendall(a)gmail.com>
Date: Mon, 1 Jun 2020 18:26:44 +0100
-Subject: [PATCH 048/165] MythDisplay: Track device pixel ratio
+Subject: [PATCH 048/171] MythDisplay: Track device pixel ratio
(cherry picked from commit 907841a119d94edc4c66c1f1af1114c5ff012258)
---
@@ -8099,7 +8099,7 @@ index 62600435384..d3b2a97a210 100644
From 8c1fd6bb95bfc554d66a444cbfb177a2cd58c485 Mon Sep 17 00:00:00 2001
From: Mark Kendall <mark.kendall(a)gmail.com>
Date: Sat, 6 Jun 2020 14:30:55 +0100
-Subject: [PATCH 049/165] VDPAU: Extend logging of profile check
+Subject: [PATCH 049/171] VDPAU: Extend logging of profile check
(cherry picked from commit 43293708579193ff23459feba159cb9fab5259d1)
---
@@ -8152,7 +8152,7 @@ index 47b4a927bed..bb5ef391406 100644
From 672d45b7bd8f03514e9936a0abdcb5c6d17c3112 Mon Sep 17 00:00:00 2001
From: Ian Campbell <ijc(a)hellion.org.uk>
Date: Sat, 6 Jun 2020 09:06:46 -0400
-Subject: [PATCH 050/165] Fix musicmetadata handling of compilations.
+Subject: [PATCH 050/171] Fix musicmetadata handling of compilations.
Fixes #13585
Closes #192
@@ -8554,7 +8554,7 @@ index 969b3908eb1..0b34b188aa5 100644
From dd35db8df117b5f2d2c6706438dcf2e316c19d95 Mon Sep 17 00:00:00 2001
From: Mark Kendall <mark.kendall(a)gmail.com>
Date: Sat, 6 Jun 2020 22:09:21 +0100
-Subject: [PATCH 051/165] MythPlayer: Fix double rate CPU deinterlacing
+Subject: [PATCH 051/171] MythPlayer: Fix double rate CPU deinterlacing
- the second field was not being processed as after the first pass the
frame was marked 'already_deinterlaced'
@@ -8608,7 +8608,7 @@ index ec0d58d4c2a..7865bab69af 100644
From aa753a179b3a08d0935f8adacc8d2611190bdac0 Mon Sep 17 00:00:00 2001
From: John Poet <jpoet(a)mythtv.org>
Date: Sat, 6 Jun 2020 20:29:45 -0600
-Subject: [PATCH 052/165] mythexternrecorder: ondatastart might need to know
+Subject: [PATCH 052/171] mythexternrecorder: ondatastart might need to know
the channel number.
(cherry picked from commit 5cf1846f76ff3a18212c2d6693b4701bdf64c03f)
@@ -8632,7 +8632,7 @@ index 6ce2d9919cf..514a6ca6b0e 100644
From 6b45963cf8a25a6b0852edf3943db9b6857bb939 Mon Sep 17 00:00:00 2001
From: John Poet <jpoet(a)mythtv.org>
Date: Sun, 7 Jun 2020 14:12:37 -0600
-Subject: [PATCH 053/165] mythexternrecorder: Fix "tuning" of channels by
+Subject: [PATCH 053/171] mythexternrecorder: Fix "tuning" of channels by
external recorder, instead of separate "tuner".
(cherry picked from commit d03307172137afeaa1f70e4f9e5458ff5f46570b)
@@ -8753,7 +8753,7 @@ index 514a6ca6b0e..624b30c98ab 100644
From 445cf1fe6be324245dfc1c548d265bec147f711c Mon Sep 17 00:00:00 2001
From: John Poet <jpoet(a)mythtv.org>
Date: Sun, 7 Jun 2020 14:37:09 -0600
-Subject: [PATCH 054/165] ExternalStreamHandler: Use DEBUG log level for
+Subject: [PATCH 054/171] ExternalStreamHandler: Use DEBUG log level for
TunerStatus
(cherry picked from commit 118db4df5d0fc70971f7aa1d4f468f41bf3baa81)
@@ -8780,7 +8780,7 @@ index 87548086a1b..74265cf9bd3 100644
From 0d8e6f1b14e6d670e54c70fac80cde797e100111 Mon Sep 17 00:00:00 2001
From: Paul Harrison <mythtv(a)sky.com>
Date: Thu, 27 Feb 2020 17:52:31 +0000
-Subject: [PATCH 055/165] Merge pull request #191 from
+Subject: [PATCH 055/171] Merge pull request #191 from
ijc/musicmetadata-disc-number
mythmusic fixes for multiple discs
@@ -8846,7 +8846,7 @@ index b505fb6bb3b..ae9cdee0473 100644
From d8ecd8fe7c85165fe3c818a6d07e7ca9472e7735 Mon Sep 17 00:00:00 2001
From: Roland Ernst <rcrernst(a)gmail.com>
Date: Wed, 10 Jun 2020 19:24:00 +0200
-Subject: [PATCH 056/165] Python: fix timestamp calculation
+Subject: [PATCH 056/171] Python: fix timestamp calculation
Python 3.8 changed the handling of the 'datetime' class:
According release notes ("What's New In Python 3.8"):
@@ -8890,7 +8890,7 @@ index ef61749a56f..97ef75a243a 100644
From 4c990647889687b5a5ee5951d289a5f0777dda90 Mon Sep 17 00:00:00 2001
From: Roland Ernst <rcrernst(a)gmail.com>
Date: Wed, 10 Jun 2020 19:25:02 +0200
-Subject: [PATCH 057/165] Set MySQL Mode explicitely when starting a session
+Subject: [PATCH 057/171] Set MySQL Mode explicitely when starting a session
Newer SQL server enable the 'strict' MySQL Modes
"STRICT_TRANS_TABLES" and/or "STRICT_ALL_TABLES",
@@ -8925,7 +8925,7 @@ index 3f798198219..6f50433036b 100644
From 134ebd7b2938dfede4a916932e52146cf66c4a9c Mon Sep 17 00:00:00 2001
From: Roland Ernst <rcrernst(a)gmail.com>
Date: Wed, 10 Jun 2020 19:28:39 +0200
-Subject: [PATCH 058/165] Python: Resolve deprecation warnings
+Subject: [PATCH 058/171] Python: Resolve deprecation warnings
Python3.8 shows a couple of deprecation warnings when running
with the "-Wall" switch:
@@ -8973,7 +8973,7 @@ index 650ac609d67..df4b537ced2 100644
From 16e06262ba588597496306467dbea845d5f53c47 Mon Sep 17 00:00:00 2001
From: David Hampton <mythtv(a)love2code.net>
Date: Sun, 31 May 2020 20:20:21 -0400
-Subject: [PATCH 059/165] Fix missing Qt 5.15 include in mythpainter.cpp.
+Subject: [PATCH 059/171] Fix missing Qt 5.15 include in mythpainter.cpp.
(cherry picked from commit f12096ba57c37f8966b9cc8fa2a775255862df9f)
---
@@ -8996,7 +8996,7 @@ index 4435efb78a2..d70010839d0 100644
From 9e47ae9d385be2b5fdb72007aad998443fbab11d Mon Sep 17 00:00:00 2001
From: Mark Kendall <mark.kendall(a)gmail.com>
Date: Tue, 16 Jun 2020 17:30:20 +0100
-Subject: [PATCH 060/165] MythOpenGLVideo: Fix chroma sampling for multiplanar
+Subject: [PATCH 060/171] MythOpenGLVideo: Fix chroma sampling for multiplanar
formats when resizing
- we need GL_NEAREST for YUY2 and when using unsigned integers texture
@@ -9068,7 +9068,7 @@ index 6313fdafaa5..0fd86ae50a9 100644
From e9e48d190e11b51051dfea9485dc4f903af7b8e9 Mon Sep 17 00:00:00 2001
From: Mark Kendall <mark.kendall(a)gmail.com>
Date: Thu, 18 Jun 2020 11:22:02 +0100
-Subject: [PATCH 061/165] VDPAU: Further extend debug logging of support tests
+Subject: [PATCH 061/171] VDPAU: Further extend debug logging of support tests
(cherry picked from commit 43714e821ba5e53a4e3e9726c658fb4b2aaeccea)
---
@@ -9138,7 +9138,7 @@ index bb5ef391406..9bddec96d65 100644
From 94423151e51e155472531d80a7e24b875a13b13c Mon Sep 17 00:00:00 2001
From: Mark Kendall <mark.kendall(a)gmail.com>
Date: Thu, 18 Jun 2020 11:25:09 +0100
-Subject: [PATCH 062/165] MythPlayer: Use yadif for deinterlacing previews
+Subject: [PATCH 062/171] MythPlayer: Use yadif for deinterlacing previews
(regression)
- the cpu deinterlacers were re-factored shortly before v31 and yadif is
@@ -9169,7 +9169,7 @@ index 7865bab69af..1eecc103cfe 100644
From bcd9a63ec012adeb4a028d6873c512bb93afa3e2 Mon Sep 17 00:00:00 2001
From: Mark Kendall <mark.kendall(a)gmail.com>
Date: Mon, 22 Jun 2020 18:36:33 +0100
-Subject: [PATCH 063/165] VDPAU: Fall 'back' to H264 Main profile for
+Subject: [PATCH 063/171] VDPAU: Fall 'back' to H264 Main profile for
H264Baseline
- as well as constrained baseline
@@ -9216,7 +9216,7 @@ index 9bddec96d65..d529f1b5453 100644
From 1182a9abea4724d8af0b8690f04e39779855fce9 Mon Sep 17 00:00:00 2001
From: Bill Meek <billmeek(a)mythtv.org>
Date: Mon, 22 Jun 2020 13:23:25 -0500
-Subject: [PATCH 064/165] Python Bindings: care for python3.3+ use of
+Subject: [PATCH 064/171] Python Bindings: care for python3.3+ use of
ElementTree
Fedora 33 is using Python 3.9 and no longer has cElementTree.
@@ -9251,7 +9251,7 @@ index a50c762734d..bfb84d10dcb 100644
From b89d76fa944a5dfc61e96eaa532eb399d92419ab Mon Sep 17 00:00:00 2001
From: Klaas de Waal <kdewaal(a)mythtv.org>
Date: Tue, 5 May 2020 23:36:21 +0200
-Subject: [PATCH 065/165] Transport Editor updates
+Subject: [PATCH 065/171] Transport Editor updates
Show the DVB-C parameter edit page for HDHomeRun tuners when they can do DVB-C.
Previously the ATSC parameter edit page was shown.
@@ -9521,7 +9521,7 @@ index dae14fece8e..0359370a245 100644
From 261eeff1bd1a5ec8a3573f5957238961793d9356 Mon Sep 17 00:00:00 2001
From: Mark Kendall <mark.kendall(a)gmail.com>
Date: Thu, 25 Jun 2020 11:04:14 +0100
-Subject: [PATCH 066/165] VDPAU: Extend FFmpeg constrained baseline check to
+Subject: [PATCH 066/171] VDPAU: Extend FFmpeg constrained baseline check to
include baseline
- to match the changes in our own code.
@@ -9550,7 +9550,7 @@ index 167f06d7aeb..2e7e8d757ca 100644
From a675ee1e110fca3ea61a197729ca42c899db94f9 Mon Sep 17 00:00:00 2001
From: Mark Kendall <mark.kendall(a)gmail.com>
Date: Thu, 25 Jun 2020 11:34:54 +0100
-Subject: [PATCH 067/165] VDPAU: Disable level checks in MythTV and FFmpeg
+Subject: [PATCH 067/171] VDPAU: Disable level checks in MythTV and FFmpeg
- per the FFmpeg docs, this should be the default and it is causing
hardware acceleration to fail on older VDPAU devices.
@@ -9595,7 +9595,7 @@ index d529f1b5453..918ce6275f7 100644
From ade713f98c5846e9c5f08a76e72cbe24ad7057ef Mon Sep 17 00:00:00 2001
From: Peter Bennett <pbennett(a)mythtv.org>
Date: Fri, 5 Jun 2020 14:21:59 -0400
-Subject: [PATCH 068/165] Services: Add new GetStreamInfo method
+Subject: [PATCH 068/171] Services: Add new GetStreamInfo method
GetStreamInfo gets basic stream information for a video or recording, including
frame rate, picture dimensons and video codec, as well as audio codec and number
@@ -10162,7 +10162,7 @@ index 93469c246b9..5f49e65d1df 100644
From 8a0c4f9bd2d9567ccbdb4151d7c03068388b45a6 Mon Sep 17 00:00:00 2001
From: Mark Kendall <mark.kendall(a)gmail.com>
Date: Mon, 29 Jun 2020 17:15:57 +0100
-Subject: [PATCH 069/165] macos: Handle high DPI displays
+Subject: [PATCH 069/171] macos: Handle high DPI displays
- behaviour should be unchanged for non-macos installations
- the underlying problem is that when macos is using high DPI, the
@@ -10587,7 +10587,7 @@ index 199f0d642be..2ccb9a60d5b 100644
From 3ef7db67fec2a2d843cea673d14240d9ee512332 Mon Sep 17 00:00:00 2001
From: Bill Meek <billmeek(a)mythtv.org>
Date: Fri, 3 Jul 2020 19:45:12 -0500
-Subject: [PATCH 070/165] Games Plugin: change system to `system` for MySQL v8
+Subject: [PATCH 070/171] Games Plugin: change system to `system` for MySQL v8
(cherry picked from commit 94931c00dc5b67d72503fd112846f148a8e942c4)
---
@@ -10638,7 +10638,7 @@ index 2146fac0009..71e48750fe0 100644
From 0add177794446f98e0dfc4b40d628017e864701f Mon Sep 17 00:00:00 2001
From: Bill Meek <billmeek(a)mythtv.org>
Date: Mon, 6 Jul 2020 22:07:23 -0500
-Subject: [PATCH 071/165] MacOS: remove hard-coded python2.6 PYTHONPATH code
+Subject: [PATCH 071/171] MacOS: remove hard-coded python2.6 PYTHONPATH code
Based on a patch from John Hoyt, and thanks for testing
on MacOS.
@@ -10708,7 +10708,7 @@ index 423b7190419..78393d9cbe5 100644
From 8212a9b7bf0354894013b102f88e4ef9d827c26c Mon Sep 17 00:00:00 2001
From: Mark Kendall <mark.kendall(a)gmail.com>
Date: Sat, 11 Jul 2020 11:47:01 +0100
-Subject: [PATCH 072/165] VDPAU: Fix VDPAU rendering for AMD/Gallium
+Subject: [PATCH 072/171] VDPAU: Fix VDPAU rendering for AMD/Gallium
Fixes #13253
@@ -10781,7 +10781,7 @@ index 79f7bb40b8d..6a8b3bf6b9d 100644
From 7bf1284867b94509cdbf473cb6e216c0c36145f1 Mon Sep 17 00:00:00 2001
From: Peter Bennett <pbennett(a)mythtv.org>
Date: Fri, 10 Jul 2020 15:00:58 -0400
-Subject: [PATCH 073/165] Services: Add new Video GetSavedBookmark and
+Subject: [PATCH 073/171] Services: Add new Video GetSavedBookmark and
SetSavedBookmark methods
Previously these methods only existed for recordings. Now adding
@@ -10943,7 +10943,7 @@ index 5f49e65d1df..fdafadf1781 100644
From 167c8d56e20167a5cc39f1ab8b301313a562b929 Mon Sep 17 00:00:00 2001
From: Bill Meek <billmeek(a)mythtv.org>
Date: Thu, 16 Jul 2020 07:55:45 -0500
-Subject: [PATCH 074/165] Plugins/dbcheck: Replace CHARACTER SET 'default' with
+Subject: [PATCH 074/171] Plugins/dbcheck: Replace CHARACTER SET 'default' with
'utf8'
Fix required for MySQL v8 because using 'default' CHARACTER
@@ -11130,7 +11130,7 @@ index 0e5a053acee..50ac049becf 100644
From 62af47c971c30d2201705302ab260147b7fffd26 Mon Sep 17 00:00:00 2001
From: Gary Buhrmaster <gary.buhrmaster(a)gmail.com>
Date: Sat, 13 Jun 2020 23:50:45 +0000
-Subject: [PATCH 075/165] add missing(?) log message when grabber interrupted
+Subject: [PATCH 075/171] add missing(?) log message when grabber interrupted
Signed-off-by: David Hampton <mythtv(a)love2code.net>
(cherry picked from commit da860e00f7ca51e709485cb7358546a551b46828)
@@ -11155,7 +11155,7 @@ index f320857c186..db751bebf39 100644
From 396dd023e60aecf667d2e7affa63bb18b468c660 Mon Sep 17 00:00:00 2001
From: Gary Buhrmaster <gary.buhrmaster(a)gmail.com>
Date: Sat, 13 Jun 2020 23:54:59 +0000
-Subject: [PATCH 076/165] eliminate extranous LOC in logging
+Subject: [PATCH 076/171] eliminate extranous LOC in logging
All the other LOG_ERR logging in mythfilldata do not
include the LOC field. Eliminate it for consistency
@@ -11184,7 +11184,7 @@ index db751bebf39..1e42900832e 100644
From 85dec4804c286483794bb4d11646ef1ff910c0ee Mon Sep 17 00:00:00 2001
From: Gary Buhrmaster <gary.buhrmaster(a)gmail.com>
Date: Sun, 14 Jun 2020 00:01:39 +0000
-Subject: [PATCH 077/165] Enable output from the grabber to be logged
+Subject: [PATCH 077/171] Enable output from the grabber to be logged
Invoke the running of the grabber such that the output is
actually captured and logged if the verbose xmltv option
@@ -11228,7 +11228,7 @@ index 1e42900832e..e3c97bd8016 100644
From 8602e978777fc20bea760ff37b620017f77a1fc1 Mon Sep 17 00:00:00 2001
From: Gary Buhrmaster <gary.buhrmaster(a)gmail.com>
Date: Sun, 14 Jun 2020 00:19:39 +0000
-Subject: [PATCH 078/165] Update XMLTV loglevel in programdata
+Subject: [PATCH 078/171] Update XMLTV loglevel in programdata
Change the loglevel in programdata to be the same as
EIT updates for the equivalent changes (i.e. debug).
@@ -11299,7 +11299,7 @@ index e32c678e861..8495e941558 100644
From 89d1991ef285567f8b46f287786b2d8ca1fb9236 Mon Sep 17 00:00:00 2001
From: Peter Bennett <pbennett(a)mythtv.org>
Date: Wed, 15 Jul 2020 15:13:26 -0400
-Subject: [PATCH 079/165] Android: Fix support for android 5
+Subject: [PATCH 079/171] Android: Fix support for android 5
For android 5 we need to build with api level 21 and that level does
not have the ftello and fseeko functions.
@@ -11346,7 +11346,7 @@ index a9575bf18ff..f6e60ab8419 100644
From f7a1f4b1a0204f624985d872395c7d689ca91da2 Mon Sep 17 00:00:00 2001
From: Klaas de Waal <kdewaal(a)mythtv.org>
Date: Thu, 16 Jul 2020 21:26:02 +0200
-Subject: [PATCH 080/165] Create key for DVB channel master lock only once
+Subject: [PATCH 080/171] Create key for DVB channel master lock only once
Create master key only once and use that each time the lock is requested instead of
creating the key every time again. This solves the problem that the database is accessed
40 times per second for the source ID of a capture card for each capture card while
monitoring the signal status.
@@ -11452,7 +11452,7 @@ index 2ce0898e4dc..99caaabf3a1 100644
From 05a613f9faaf9193f86fc29a3e42e730e401a27f Mon Sep 17 00:00:00 2001
From: Bill Meek <billmeek(a)mythtv.org>
Date: Thu, 30 Jul 2020 08:58:37 -0500
-Subject: [PATCH 081/165] dbcheck: quote yet another MySQL v8 reserved work
+Subject: [PATCH 081/171] dbcheck: quote yet another MySQL v8 reserved work
Forum user reports being unable to upgrade a 0.25 DB to v31.
@@ -11478,7 +11478,7 @@ index 329a716a4db..90915e6cc8c 100644
From e537ea801af3a1d69c6fd0dbf8060ff22ba34cf2 Mon Sep 17 00:00:00 2001
From: Mark Kendall <mark.kendall(a)gmail.com>
Date: Wed, 29 Jul 2020 16:08:57 +0100
-Subject: [PATCH 082/165] Wayland: Fix alpha blending
+Subject: [PATCH 082/171] Wayland: Fix alpha blending
- each window in wayland has its own buffer/texture and these are always
composited with alpha blending
@@ -11541,7 +11541,7 @@ index cf9b1d795c7..2470ef3ae8c 100644
From 3322b374c1850fd2fec170db15a3be349db73b8d Mon Sep 17 00:00:00 2001
From: Bill Meek <billmeek(a)mythtv.org>
Date: Tue, 11 Aug 2020 13:12:38 -0500
-Subject: [PATCH 083/165] mythfilldatabase: mark --dd-grab-all as deprecated
+Subject: [PATCH 083/171] mythfilldatabase: mark --dd-grab-all as deprecated
Add a log warning, but continue running.
@@ -11583,7 +11583,7 @@ index ef20217f1ce..b197347128e 100644
From fb389f2100bc5179390de1cbab80cb410b1e2520 Mon Sep 17 00:00:00 2001
From: David Engel <dengel(a)mythtv.org>
Date: Sun, 16 Aug 2020 14:18:40 -0500
-Subject: [PATCH 084/165] Fix issue with daily and weekly, manual, recording
+Subject: [PATCH 084/171] Fix issue with daily and weekly, manual, recording
rules.
Commit 5f6697ec removed the setting of subtitle to the recording time.
@@ -11664,7 +11664,7 @@ index cdb7f6d68b5..ddd18d4e26c 100644
From d3088629deadc957eb26ba3f6b16698b6e7f668b Mon Sep 17 00:00:00 2001
From: Stuart Auchterlonie <stuarta(a)mythtv.org>
Date: Tue, 18 Aug 2020 22:45:26 +0100
-Subject: [PATCH 085/165] Refs #12307 - Respect the user setting to disable
+Subject: [PATCH 085/171] Refs #12307 - Respect the user setting to disable
media monitor
Also fix a typo in the message stating it is disabled.
@@ -11719,7 +11719,7 @@ index c93de12c9fe..d9bf7ae2630 100644
From 623192215ae0d08af094a11e148efdac664eb2bc Mon Sep 17 00:00:00 2001
From: Klaas de Waal <kdewaal(a)mythtv.org>
Date: Sun, 23 Aug 2020 21:09:34 +0200
-Subject: [PATCH 086/165] Accept VBOX version numbers starting with VT
+Subject: [PATCH 086/171] Accept VBOX version numbers starting with VT
(cherry picked from commit 083367b4907afbb40c5ee0adbb402a1aabc92468)
Signed-off-by: Klaas de Waal <kdewaal(a)mythtv.org>
@@ -11745,7 +11745,7 @@ index bc316e4ebfa..d3fff12317b 100644
From aac5e7f0f454a95a69de15f40d62306f11033060 Mon Sep 17 00:00:00 2001
From: Bill Meek <billmeek(a)mythtv.org>
Date: Thu, 27 Aug 2020 16:10:55 -0500
-Subject: [PATCH 087/165] mythfilldatabase: Change one more LOG to debug
+Subject: [PATCH 087/171] mythfilldatabase: Change one more LOG to debug
For users that don't run MFDB using the --only-update-guide
switch, only print these with xmltv:debug:
@@ -11776,7 +11776,7 @@ index 5b2c5811427..caafde56b11 100644
From 5c395c59e26a1989c2cc1aced55506dbc1d9be7a Mon Sep 17 00:00:00 2001
From: Roland Ernst <rcrernst(a)gmail.com>
Date: Wed, 26 Aug 2020 22:51:23 +0200
-Subject: [PATCH 088/165] Python Bindings: Open video/recoring in binary mode
+Subject: [PATCH 088/171] Python Bindings: Open video/recoring in binary mode
Since the exported function `ftopen` is meant for videos or
recordings, open those type of files in binary mode.
@@ -11835,7 +11835,7 @@ index 6b00ba222cb..5aa730cef87 100644
From ab0c38a4764c29019f1fe10c8a8315bb85d65150 Mon Sep 17 00:00:00 2001
From: Roland Ernst <rcrernst(a)gmail.com>
Date: Wed, 26 Aug 2020 23:50:26 +0200
-Subject: [PATCH 089/165] Python Bindings: Add robustness on using paths to
+Subject: [PATCH 089/171] Python Bindings: Add robustness on using paths to
videos or recordings
Storage Group paths may be defined with or without trailing slash ('/').
@@ -11894,7 +11894,7 @@ index 5aa730cef87..a89d50f6ef4 100644
From 40841f5804501478a271369da2bc4ee93418a9dc Mon Sep 17 00:00:00 2001
From: Klaas de Waal <kdewaal(a)mythtv.org>
Date: Sun, 30 Aug 2020 12:26:33 +0200
-Subject: [PATCH 090/165] No discontinuity for first TS packet of PID
+Subject: [PATCH 090/171] No discontinuity for first TS packet of PID
Initialize all elements of m_continuityCounter to 0xff.
This is an essential part of the existing code that avoids giving continuity
@@ -11922,7 +11922,7 @@ index 3c1248ea14f..a868c6f0489 100644
From b282809197febe7a7619e99c3b3067215a62afc3 Mon Sep 17 00:00:00 2001
From: Peter Bennett <pbennett(a)mythtv.org>
Date: Wed, 26 Aug 2020 15:51:54 -0400
-Subject: [PATCH 091/165] Service API: Fix bug where "New Episodes Only"
+Subject: [PATCH 091/171] Service API: Fix bug where "New Episodes Only"
corrupts value of DupIn
DupIn value carries two meanings, "New Episodes Only" flag as well as
@@ -12184,7 +12184,7 @@ index b1d5071d6a4..bfce03776dd 100644
From 2cff5c78b2227eb69efb22e384e20bad90b804ae Mon Sep 17 00:00:00 2001
From: David Engel <dengel(a)mythtv.org>
Date: Tue, 8 Sep 2020 16:30:32 -0500
-Subject: [PATCH 092/165] Backport onc foreach fix from master commit
+Subject: [PATCH 092/171] Backport onc foreach fix from master commit
11df0636c5a.
I believe the use of foreach at this location has been causing some of
@@ -12210,7 +12210,7 @@ index 102395fcaed..1df2f65be3d 100644
From 48b2a03b78c74cc4268df91f442a4a5e5c3af363 Mon Sep 17 00:00:00 2001
From: Mark Kendall <mark.kendall(a)gmail.com>
Date: Tue, 15 Sep 2020 10:18:08 +0100
-Subject: [PATCH 093/165] Add github workflow for fixes/31
+Subject: [PATCH 093/171] Add github workflow for fixes/31
---
.github/workflows/buildfixes31.yml | 85 ++++++++++++++++++++++++++++++
@@ -12312,7 +12312,7 @@ index 00000000000..ec42bc4f6b8
From b033cd7b75a9220c46e230e2a62802e923119332 Mon Sep 17 00:00:00 2001
From: Mark Kendall <mark.kendall(a)gmail.com>
Date: Mon, 14 Sep 2020 18:51:12 +0100
-Subject: [PATCH 094/165] TV: Fix crash when playback exits and guide grid is
+Subject: [PATCH 094/171] TV: Fix crash when playback exits and guide grid is
showing
- not normally a problem in live tv but if playback is allowed to
@@ -12403,7 +12403,7 @@ index dde085348d6..83062a98eae 100644
From 0942abd9ec355443ad477dce735167206ba4faf5 Mon Sep 17 00:00:00 2001
From: Mark Kendall <mark.kendall(a)gmail.com>
Date: Thu, 17 Sep 2020 13:56:02 +0100
-Subject: [PATCH 095/165] macOS video: Fix scaling of OSD
+Subject: [PATCH 095/171] macOS video: Fix scaling of OSD
- when high dpi is in use
@@ -12455,7 +12455,7 @@ index abbb7685f0a..0d58bc27e76 100644
From b5f1d03fa8040dbf94621eb6d52cb30b7832fd3d Mon Sep 17 00:00:00 2001
From: Mark Kendall <mark.kendall(a)gmail.com>
Date: Thu, 17 Sep 2020 13:58:13 +0100
-Subject: [PATCH 096/165] macOS video: Fix scaling of video after an input
+Subject: [PATCH 096/171] macOS video: Fix scaling of video after an input
change (high dpi)
- when the display is using high DPI, after an input change we re-init MythVideoBounds
but with the current display rectangle; that has already been scaled for high DPI -
leading to a doubling of the display rectangle on each input change and a halving of the
displayed video.
@@ -12484,7 +12484,7 @@ index 225c58fe259..d70eb05975a 100644
From 674fd1eb16aa052afd277a857cf68b65870ac3e9 Mon Sep 17 00:00:00 2001
From: Mark Kendall <mark.kendall(a)gmail.com>
Date: Fri, 18 Sep 2020 09:55:13 +0100
-Subject: [PATCH 097/165] workflows: Don't build mythbrowser
+Subject: [PATCH 097/171] workflows: Don't build mythbrowser
---
.github/workflows/buildfixes31.yml | 2 +-
@@ -12507,7 +12507,7 @@ index ec42bc4f6b8..894da832c27 100644
From 21ef6eed645d42488cc3506794f2a6f955cb227d Mon Sep 17 00:00:00 2001
From: Mark Kendall <mark.kendall(a)gmail.com>
Date: Fri, 18 Sep 2020 10:47:11 +0100
-Subject: [PATCH 098/165] workflows: Don't build macOS
+Subject: [PATCH 098/171] workflows: Don't build macOS
Something has obviously changed in master versus .31 for QtWebkit support. As this build
is just informative, disable macOS which does not have webkit
---
@@ -12540,7 +12540,7 @@ index 894da832c27..f3910f9ff91 100644
From 01bc4fc628a2b294c931b467894254eff1871c2f Mon Sep 17 00:00:00 2001
From: Mark Kendall <mark.kendall(a)gmail.com>
Date: Sat, 19 Sep 2020 09:03:01 +0100
-Subject: [PATCH 099/165] High DPI scaling: Fix displaying the ProgramGuide
+Subject: [PATCH 099/171] High DPI scaling: Fix displaying the ProgramGuide
when embedded
- previously, when playing video, we disabled framebuffer clearing, setting of the
viewport and any high dpi scaling in the painter.
@@ -12691,7 +12691,7 @@ index 540f7db79df..06407b93802 100644
From ea0831c74a1c7bcb116eff79691ef24612343a62 Mon Sep 17 00:00:00 2001
From: Mark Kendall <mark.kendall(a)gmail.com>
Date: Sat, 19 Sep 2020 09:06:36 +0100
-Subject: [PATCH 100/165] macos High DPI: Fix scaling of embedded video
+Subject: [PATCH 100/171] macos High DPI: Fix scaling of embedded video
- when high DPI is in use, we need to apply the correct scaling to
both the embedding rect and ITV resize rect
@@ -12805,7 +12805,7 @@ index cce077b5fde..a5efc757f52 100644
From 55dd6a75dfb06e47b66d87874ae30470441decbb Mon Sep 17 00:00:00 2001
From: Mark Kendall <mark.kendall(a)gmail.com>
Date: Mon, 21 Sep 2020 20:18:48 +0100
-Subject: [PATCH 101/165] AvFormatDecoder: Fix some DVD menus with VAAPI and
+Subject: [PATCH 101/171] AvFormatDecoder: Fix some DVD menus with VAAPI and
VDPAU
- as noted in the code, overriding the aspect ratio from the DVD
@@ -12853,7 +12853,7 @@ index 7ce2f85b6e6..dce359edb5d 100644
From 2b753d95fdc7ea2223f32c790b2f84c901b6bc50 Mon Sep 17 00:00:00 2001
From: Mark Kendall <mark.kendall(a)gmail.com>
Date: Mon, 21 Sep 2020 20:28:41 +0100
-Subject: [PATCH 102/165] AvFormatDecoder: Fix potential error in DVD aspect
+Subject: [PATCH 102/171] AvFormatDecoder: Fix potential error in DVD aspect
ratio
- from last commit, zero is not a valid aspect ratio
@@ -12882,7 +12882,7 @@ index dce359edb5d..7165b73581c 100644
From 8e36eda32195b9ad2cd3bc4f2120efb32e87a926 Mon Sep 17 00:00:00 2001
From: Mark Kendall <mark.kendall(a)gmail.com>
Date: Mon, 21 Sep 2020 20:51:38 +0100
-Subject: [PATCH 103/165] MythVideoOutput: Ensure deinterlacers are updated
+Subject: [PATCH 103/171] MythVideoOutput: Ensure deinterlacers are updated
after input change
Closes #222
@@ -12973,7 +12973,7 @@ index 0c8afe4e85f..0e0447194f0 100644
From 2e1cccb628f92091641657f67f25852c746a7887 Mon Sep 17 00:00:00 2001
From: Klaas de Waal <klaas(a)kldo.nl>
Date: Mon, 21 Sep 2020 22:40:12 +0200
-Subject: [PATCH 104/165] Support DMBTH (DTMB) as DVB-T
+Subject: [PATCH 104/171] Support DMBTH (DTMB) as DVB-T
Select tuner type DVB-T when the card supports modulation system DMBTH (DTMB).
This restore the behavior of MythTV v30 that was lost in v31.
@@ -13001,7 +13001,7 @@ index 5f2d8940ee2..005d0e5a197 100644
From 8ca77476484b7179cbd1843d715e0145e13744c6 Mon Sep 17 00:00:00 2001
From: Klaas de Waal <kdewaal(a)mythtv.org>
Date: Wed, 16 Sep 2020 21:00:25 +0200
-Subject: [PATCH 105/165] Time for preview max 10 minutes into the program
+Subject: [PATCH 105/171] Time for preview max 10 minutes into the program
The time of the preview is 1/3 of the total program duration
limited to 10 minutes after the start of the program.
@@ -13034,7 +13034,7 @@ index d78f9ab9bd2..e2e3ef6483b 100644
From abad9e2de7c771029e8b0333d55643855be2a6bf Mon Sep 17 00:00:00 2001
From: Peter Bennett <pbennett(a)mythtv.org>
Date: Tue, 29 Sep 2020 15:25:24 -0400
-Subject: [PATCH 106/165] Fix bug caused by commit a3ae3a8
+Subject: [PATCH 106/171] Fix bug caused by commit a3ae3a8
Problem was that in Terra theme, watched icon was not disappearing
when an unwatched show was selected.
@@ -13064,7 +13064,7 @@ index 2d1237787b5..229d99be36c 100644
From 070623c6c65ffbc1fc635cf8a8333a4173d6b35f Mon Sep 17 00:00:00 2001
From: Mark Kendall <mark.kendall(a)gmail.com>
Date: Wed, 7 Oct 2020 07:36:29 +0100
-Subject: [PATCH 107/165] Fix 0.31 build
+Subject: [PATCH 107/171] Fix 0.31 build
---
mythtv/libs/libmythui/mythuistatetype.cpp | 2 +-
@@ -13087,7 +13087,7 @@ index 229d99be36c..39808cc1988 100644
From a7d51639918e7fd1fc0b896248817ac7db732bcf Mon Sep 17 00:00:00 2001
From: Paul Gardiner <mythtv(a)glidos.net>
Date: Sat, 26 Sep 2020 16:22:22 +0100
-Subject: [PATCH 108/165] Fix incorrect artwork urls returned from ttvdb
+Subject: [PATCH 108/171] Fix incorrect artwork urls returned from ttvdb
grabber
When performing a manual search for metadata for a video, the artwork
@@ -13139,7 +13139,7 @@ index bf7ad06a82e..ee6c8ade816 100644
From 76edb08de77d76d2e7baf1ea1fa7072032b6dc4c Mon Sep 17 00:00:00 2001
From: Bas Hulsken <bhulsken(a)hotmail.com>
Date: Tue, 7 Jul 2020 10:09:32 +0200
-Subject: [PATCH 109/165] extend metadatagrabber timeout to 3 minutes
+Subject: [PATCH 109/171] extend metadatagrabber timeout to 3 minutes
(cherry picked from commit e17de9cd618838e14081322051b199e278100c2b)
---
@@ -13177,7 +13177,7 @@ index 2757e2fa7a4..972cfaa9eab 100755
From 47a45b1e2e972ba2a5f0a464fef9a968e98138c4 Mon Sep 17 00:00:00 2001
From: Roland Ernst <rcrernst(a)gmail.com>
Date: Thu, 1 Oct 2020 21:21:12 +0200
-Subject: [PATCH 110/165] Revert commit 2738b98, but add robustness
+Subject: [PATCH 110/171] Revert commit 2738b98, but add robustness
Commit 2738b98 removed the functionality to download fanart and coverart
upon a manual search for metadata.
@@ -13261,7 +13261,7 @@ index 3747f5a4d40..cdadd7ee5a3 100644
From 6ad0b90949c956048aa8daccfb3da49b45f62e7e Mon Sep 17 00:00:00 2001
From: Roland Ernst <rcrernst(a)gmail.com>
Date: Thu, 1 Oct 2020 21:45:08 +0200
-Subject: [PATCH 111/165] Fix running a metadata grabber twice in manual mode
+Subject: [PATCH 111/171] Fix running a metadata grabber twice in manual mode
Commit e81c7fd added accuracy of retrieving metadata in automatic mode,
but leads to some inefficiency in the use of manual mode:
@@ -13332,7 +13332,7 @@ index 2ecebd0fef7..5b60f2b1b84 100644
From 4ab0425bd5e774978f170d3c76f9855072b09307 Mon Sep 17 00:00:00 2001
From: Roland Ernst <rcrernst(a)gmail.com>
Date: Tue, 6 Oct 2020 19:52:32 +0200
-Subject: [PATCH 112/165] Automated metadata lookup: Return if no match found
+Subject: [PATCH 112/171] Automated metadata lookup: Return if no match found
When performing a lookup in automatic mode, do not present a list
of all similar matches. Instead, terminate that lookup with a
@@ -13366,7 +13366,7 @@ index 5b60f2b1b84..4566270ef74 100644
From f450e74550936e719134cf817d1fb7dc4e3e259e Mon Sep 17 00:00:00 2001
From: Roland Ernst <rcrernst(a)gmail.com>
Date: Tue, 6 Oct 2020 20:01:03 +0200
-Subject: [PATCH 113/165] Automated metadata lookup: Pass through automatic
+Subject: [PATCH 113/171] Automated metadata lookup: Pass through automatic
flag
If a new metadata lookup is spun off during lookup, pass through
@@ -13394,7 +13394,7 @@ index 4566270ef74..56d0cd87c83 100644
From 30017d33d692a8aa5823d317ce741dbf092fd8cd Mon Sep 17 00:00:00 2001
From: Roland Ernst <rcrernst(a)gmail.com>
Date: Tue, 6 Oct 2020 20:25:31 +0200
-Subject: [PATCH 114/165] Metadata Lookup: Handle 'mxml' and 'nfo' files
only
+Subject: [PATCH 114/171] Metadata Lookup: Handle 'mxml' and 'nfo' files
only
once
Videos in the video storage group may have an associated file
@@ -13562,7 +13562,7 @@ index 56d0cd87c83..d5ea413fe7b 100644
From 5a366872567c6c29390f8847cfaec6f7e027ae5b Mon Sep 17 00:00:00 2001
From: Roland Ernst <rcrernst(a)gmail.com>
Date: Tue, 6 Oct 2020 20:40:16 +0200
-Subject: [PATCH 115/165] Metadata Lookup: Add an experimental feature in
+Subject: [PATCH 115/171] Metadata Lookup: Add an experimental feature in
automatic mode
In case that no exact matches have been found during automatic lookup,
@@ -13633,7 +13633,7 @@ index d5ea413fe7b..0ce2647e858 100644
From d285bdd9948655f98914ea47f61efa290bb86e85 Mon Sep 17 00:00:00 2001
From: Mark Kendall <mark.kendall(a)gmail.com>
Date: Wed, 7 Oct 2020 08:09:28 +0100
-Subject: [PATCH 116/165] MetadataDownload: Fix clang-tidy warning - else after
+Subject: [PATCH 116/171] MetadataDownload: Fix clang-tidy warning - else after
continue
(cherry picked from commit d78f56e083f44224509bae53ffcdfbd21cca446a)
@@ -13668,7 +13668,7 @@ index 0ce2647e858..2869793c8b8 100644
From 577dd50df1cf6c6629eb708ee3a88a724bdbedff Mon Sep 17 00:00:00 2001
From: Roland Ernst <rcrernst(a)gmail.com>
Date: Sat, 10 Oct 2020 22:12:48 +0200
-Subject: [PATCH 117/165] Python Bindings: Allow searching for collections
+Subject: [PATCH 117/171] Python Bindings: Allow searching for collections
The ttvdb.py script does not return a valid xml when searching
for an ID without season or episode, like
@@ -13714,7 +13714,7 @@ index f7966fb6d69..3f4f5e2196f 100644
From eb3c84de5fe12831d386d15452aeb7658841e072 Mon Sep 17 00:00:00 2001
From: David Engel <dengel(a)mythtv.org>
Date: Mon, 12 Oct 2020 15:36:27 -0500
-Subject: [PATCH 118/165] Fix longstanding issue with
+Subject: [PATCH 118/171] Fix longstanding issue with
Scheduler::getConflicting().
Somewhere along the line, probably in a refactoring or cleanup,
@@ -13781,7 +13781,7 @@ index d0cb90cac9c..bc8050c5f91 100644
From f829ab8c6888dce04d79fbb5c5e38b6069894f78 Mon Sep 17 00:00:00 2001
From: Mark Kendall <mark.kendall(a)gmail.com>
Date: Wed, 25 Nov 2020 21:17:15 +0000
-Subject: [PATCH 119/165] Update buildfixes31.yml
+Subject: [PATCH 119/171] Update buildfixes31.yml
---
.github/workflows/buildfixes31.yml | 4 ++--
@@ -13813,7 +13813,7 @@ index f3910f9ff91..9e7f8129d4e 100644
From 207ccf1aeb333152fa5f17d0b4898f8605ccc2ca Mon Sep 17 00:00:00 2001
From: Mark Kendall <mark.kendall(a)gmail.com>
Date: Wed, 25 Nov 2020 21:20:12 +0000
-Subject: [PATCH 120/165] Typos in buildfixes31.yml
+Subject: [PATCH 120/171] Typos in buildfixes31.yml
---
.github/workflows/buildfixes31.yml | 4 ++--
@@ -13845,7 +13845,7 @@ index 9e7f8129d4e..b230dac0009 100644
From a087a172311a253bfe488e9139c0c603b7ae4853 Mon Sep 17 00:00:00 2001
From: John Hoyt <john.hoyt(a)gmail.com>
Date: Thu, 26 Nov 2020 13:13:30 -0500
-Subject: [PATCH 121/165] backport master:5c180c6 to fixes/31 - Fix
+Subject: [PATCH 121/171] backport master:5c180c6 to fixes/31 - Fix
deprecation warnings in OSX audio
---
@@ -14792,7 +14792,7 @@ index e5f52aa4f5f..8193d3b5b62 100644
From 102dbe673325fccbc5bf073f94e43fb292bea4b6 Mon Sep 17 00:00:00 2001
From: John Hoyt <john.hoyt(a)gmail.com>
Date: Thu, 26 Nov 2020 13:14:27 -0500
-Subject: [PATCH 122/165] backport master:d0d9a4e to fixes/31 - Fix
+Subject: [PATCH 122/171] backport master:d0d9a4e to fixes/31 - Fix
audioconvert test failures when compiling X86 optimized code.
---
@@ -14876,7 +14876,7 @@ index 6f4703642d6..0f9eb8e5df6 100644
From 6b93a8acd8c9eaf74b7ca66eedb54299edc5e012 Mon Sep 17 00:00:00 2001
From: Roland Ernst <rcrernst(a)gmail.com>
Date: Tue, 8 Dec 2020 19:20:02 +0100
-Subject: [PATCH 123/165] Fix ttvdb.py to get coverarts for seasons.
+Subject: [PATCH 123/171] Fix ttvdb.py to get coverarts for seasons.
Fetching the coverart with the scrip ttvdb.py fails,
if the artwork is listed in data['_banners']['season'],
@@ -14912,7 +14912,7 @@ index 76b98618920..1c0bb1f27d1 100755
From e174aa49f5841bb05ac038e0ec8e57200e3c3419 Mon Sep 17 00:00:00 2001
From: Roland Ernst <rcrernst(a)gmail.com>
Date: Tue, 8 Dec 2020 19:27:52 +0100
-Subject: [PATCH 124/165] Temporary fix for missing coverart for seasons from
+Subject: [PATCH 124/171] Temporary fix for missing coverart for seasons from
ttvdb.py
If ttvdb.py does not return any coverart for specific seasons,
@@ -14950,7 +14950,7 @@ index 1c0bb1f27d1..e244eec41c4 100755
From f42a3dbbd159bfc6028d78b6f507596e196af1cc Mon Sep 17 00:00:00 2001
From: Roland Ernst <rcrernst(a)gmail.com>
Date: Tue, 8 Dec 2020 19:46:11 +0100
-Subject: [PATCH 125/165] TMDB3.py: Sort coverarts by system language or 'en',
+Subject: [PATCH 125/171] TMDB3.py: Sort coverarts by system language or 'en',
if none found for given language
The grabber script tmdb3.py already sorts the posters
@@ -15050,7 +15050,7 @@ index 972cfaa9eab..d82fcd63991 100755
From f29a663a2577d832584ef289e118ff7d1cbade6a Mon Sep 17 00:00:00 2001
From: Paul Harrison <paul(a)mythqml.net>
Date: Sat, 5 Dec 2020 18:14:49 +0000
-Subject: [PATCH 126/165] VBox: use the common part of the UDN to identify
+Subject: [PATCH 126/171] VBox: use the common part of the UDN to identify
VBoxes found by UPnP
(cherry picked from commit cc9b462e7254f96ad370db6405481421d4b8caf9)
@@ -15088,7 +15088,7 @@ index d3fff12317b..265a53a0e2a 100644
From e9b795a1e43023b4141a28b9c620213097cdfbfe Mon Sep 17 00:00:00 2001
From: Paul Harrison <paul(a)mythqml.net>
Date: Tue, 8 Dec 2020 18:02:06 +0000
-Subject: [PATCH 127/165] Guide Data: allow for previously shown dates before
+Subject: [PATCH 127/171] Guide Data: allow for previously shown dates before
1940
According to Google the first commercial movie screening was December 28, 1895
@@ -15172,7 +15172,7 @@ index 9e140a3fe5c..7138c2be097 100644
From 0bc3cdfbf7b649e6060db3fa6b67be54f788b00e Mon Sep 17 00:00:00 2001
From: David Engel <dengel(a)mythtv.org>
Date: Mon, 28 Dec 2020 17:18:13 -0600
-Subject: [PATCH 128/165] Fix handling of deleted channels in
+Subject: [PATCH 128/171] Fix handling of deleted channels in
Scheduler::GetAllScheduled()
Deleted channels should not be joined at all. Joining them only on
@@ -15203,7 +15203,7 @@ index 383f3c91ba5..666b67820f4 100644
From e8d34eacf75bd7c1aa86c709aabc1098f0bfccc3 Mon Sep 17 00:00:00 2001
From: Roland Ernst <rcrernst(a)gmail.com>
Date: Wed, 6 Jan 2021 15:10:54 +0100
-Subject: [PATCH 129/165] Be robust if grabber ttvdb.py does not return any
+Subject: [PATCH 129/171] Be robust if grabber ttvdb.py does not return any
banners.
Refs #298
@@ -15238,7 +15238,7 @@ index e244eec41c4..7421969948e 100755
From 016630a35cd24d3d1e4eca11e62758161d5af92f Mon Sep 17 00:00:00 2001
From: Roland Ernst <rcrernst(a)gmail.com>
Date: Fri, 8 Jan 2021 18:06:54 +0100
-Subject: [PATCH 130/165] Fix adding missing coverart in ttvdb.py
+Subject: [PATCH 130/171] Fix adding missing coverart in ttvdb.py
and ignore incomplete urls.
@@ -15272,7 +15272,7 @@ index 7421969948e..a95ae8484cb 100755
From e152fb5aec6089c08c0628e133f073270f8510a5 Mon Sep 17 00:00:00 2001
From: John Hoyt <jhoyt(a)Builbot-Cat.local>
Date: Sat, 30 Jan 2021 03:22:59 -0800
-Subject: [PATCH 131/165] MythUIButtonListItem: Ensure all member vars are
+Subject: [PATCH 131/171] MythUIButtonListItem: Ensure all member vars are
initialised
---
@@ -15298,7 +15298,7 @@ index aaf1bb31dff..c8d59a44f1a 100644
From 93adb6dc753fcdff1350df24cdaf473d823abdc2 Mon Sep 17 00:00:00 2001
From: Dario <ddafre(a)gmail.com>
Date: Mon, 1 Feb 2021 02:20:53 +0000
-Subject: [PATCH 132/165] Translations: update mythfrontend/mythplugins Italian
+Subject: [PATCH 132/171] Translations: update mythfrontend/mythplugins Italian
translations
Signed-off-by: Nick Morrott <knowledgejunkie(a)gmail.com>
@@ -24849,7 +24849,7 @@ index 9c8402ffa91..3eab2affce3 100644
From 01a15fcf7c2c0bfee390f92199a450319dae7559 Mon Sep 17 00:00:00 2001
From: Peter Bennett <pbennett(a)mythtv.org>
Date: Sat, 23 Jan 2021 15:45:32 -0500
-Subject: [PATCH 133/165] Fix frontend setup showing musicbrainz as grabber for
+Subject: [PATCH 133/171] Fix frontend setup showing musicbrainz as grabber for
movie metadata
When a grabber does not return a type (movie or television), the system
@@ -24878,7 +24878,7 @@ index d9c0c1ebb7a..47e0f4d7d23 100644
From 1aff6fd2db9258d7aab45c3f07f3442f17d04fdd Mon Sep 17 00:00:00 2001
From: Peter Bennett <pbennett(a)mythtv.org>
Date: Tue, 26 Jan 2021 12:32:54 -0500
-Subject: [PATCH 134/165] Modify tmdb3 grabber to support TV in addition to
+Subject: [PATCH 134/171] Modify tmdb3 grabber to support TV in addition to
Movies
- Moved the logic of creating the xml files for MythTV into a new
@@ -26124,7 +26124,7 @@ index 00000000000..84d5dceadaf
From 7763a3363d7a05aaf45d6d4cb13cffa54274e2b4 Mon Sep 17 00:00:00 2001
From: Paul Harrison <paul(a)mythqml.net>
Date: Thu, 4 Feb 2021 18:24:29 +0000
-Subject: [PATCH 135/165] MythMusic: make parsePLS more robust and improve
+Subject: [PATCH 135/171] MythMusic: make parsePLS more robust and improve
logging
This fixes playing some radio streams which send slightly out of spec PLS
@@ -26185,7 +26185,7 @@ index bb08e0bba6d..4f2204d518b 100644
From ba4036099ffce6364c389211e7b9cd38489f7ad4 Mon Sep 17 00:00:00 2001
From: Mark Kendall <mark.kendall(a)gmail.com>
Date: Fri, 5 Feb 2021 17:56:56 +0000
-Subject: [PATCH 136/165] AudioOutputGraph: Fix buffer overflow
+Subject: [PATCH 136/171] AudioOutputGraph: Fix buffer overflow
- this just fixes the obvious and, on macos at least, fatal symptoms but
this code needs a sizeable cleanup.
@@ -26248,7 +26248,7 @@ index 13dc1a06542..286f4249662 100644
From 525e3b0bb4ec0a160ea35591b28ca7d4ae067313 Mon Sep 17 00:00:00 2001
From: Roland Ernst <rcrernst(a)gmail.com>
Date: Mon, 18 Jan 2021 22:50:54 +0100
-Subject: [PATCH 137/165] Add optional metadata grabber for television: tvmaze
+Subject: [PATCH 137/171] Add optional metadata grabber for television: tvmaze
See
https://www.tvmaze.com/
@@ -28562,7 +28562,7 @@ index 00000000000..644b6c10fe1
From b6ddf202a496dac180218a6581344251804f2086 Mon Sep 17 00:00:00 2001
From: Roland Ernst <rcrernst(a)gmail.com>
Date: Thu, 25 Feb 2021 19:18:27 +0100
-Subject: [PATCH 138/165] Fix minor issues on the TV grabber TVMmaze
+Subject: [PATCH 138/171] Fix minor issues on the TV grabber TVMmaze
Syntax warnig during installation:
/tvmaze/utils.py:45: SyntaxWarning: "is not" with a literal. Did you mean
"!="?
@@ -28606,7 +28606,7 @@ index 078a646c187..c80b5d9eb38 100755
From e705d36a368134ef8871f92656ded94293bb7e0d Mon Sep 17 00:00:00 2001
From: Klaas de Waal <kdewaal(a)mythtv.org>
Date: Sun, 28 Feb 2021 20:03:59 +0100
-Subject: [PATCH 139/165] No multirec for V4L2ENC and HDPVR devices
+Subject: [PATCH 139/171] No multirec for V4L2ENC and HDPVR devices
In commit f58f474bb1 on 6 nov. 2019 the default for field schedgroup in new entries in
table capturecard is set to 1 for all types of devices.
@@ -28688,7 +28688,7 @@ index 9ddad8ba034..6ca651f2989 100644
From 6c7c8b0351cd227fdfae5534b0eab710e1a5f8fe Mon Sep 17 00:00:00 2001
From: Bill Meek <billmeek(a)mythtv.org>
Date: Fri, 5 Mar 2021 13:35:49 -0600
-Subject: [PATCH 140/165] database: legacy version of mc.sql
+Subject: [PATCH 140/171] database: legacy version of mc.sql
Currently, for MariaDB versions below 10.3
@@ -28729,7 +28729,7 @@ index 00000000000..be9554b5c8f
From 5efa91cf604f0f6af282f440b37b78d0613790d2 Mon Sep 17 00:00:00 2001
From: Klaas de Waal <kdewaal(a)mythtv.org>
Date: Mon, 22 Mar 2021 00:16:59 +0100
-Subject: [PATCH 141/165] ATSC Closed Captions
+Subject: [PATCH 141/171] ATSC Closed Captions
In avformatdecoder.cppp the closed captions packets are extracted from
the video stream and sent to the decoders for processing.
@@ -28765,7 +28765,7 @@ index 7165b73581c..13cc3fa49ad 100644
From dbac81d4f979bce594646bc8e09819442f8f4d0f Mon Sep 17 00:00:00 2001
From: Klaas de Waal <kdewaal(a)mythtv.org>
Date: Fri, 12 Mar 2021 22:09:48 +0100
-Subject: [PATCH 142/165] CEA-608/VBI CC3 closed captions/subtitles
+Subject: [PATCH 142/171] CEA-608/VBI CC3 closed captions/subtitles
The CEA-608 closed captions can show two different subtitle streams
for two different languages, called CC1 and CC3.
@@ -28798,7 +28798,7 @@ index 865c466c9d2..1d0f58f98e6 100644
From 563a05b7a89b09a18edf874fbe347b6255ee07d3 Mon Sep 17 00:00:00 2001
From: Klaas de Waal <kdewaal(a)mythtv.org>
Date: Thu, 1 Apr 2021 21:57:53 +0200
-Subject: [PATCH 143/165] Show "Use FFmpeg's original MPEG-TS demuxer"
option
+Subject: [PATCH 143/171] Show "Use FFmpeg's original MPEG-TS demuxer"
option
Make the option "Use FFmpeg's original MPEG-TS demuxer" in mythfrontend
menu Setup/Video/Playback/General Playback available in all builds instead
@@ -28855,7 +28855,7 @@ index f64c4f12882..d55226f620c 100644
From d4997f4629833ae0f7e25b116a291109bf9535dd Mon Sep 17 00:00:00 2001
From: kingsley <krt(a)krt.com.au>
Date: Sun, 18 Apr 2021 17:39:15 +1000
-Subject: [PATCH 144/165] Increase cut-off for subtitle buffer clear, too small
+Subject: [PATCH 144/171] Increase cut-off for subtitle buffer clear, too small
for SSA karaoke subs
---
@@ -28897,7 +28897,7 @@ index a8ff9382b46..33b1285c0ff 100644
From 05c16580e185435b45722cc590f715e1b78eff83 Mon Sep 17 00:00:00 2001
From: Peter Bennett <pbennett(a)mythtv.org>
Date: Sun, 18 Apr 2021 15:39:35 -0400
-Subject: [PATCH 145/165] tmdb3tv: Prevent exception when non-existent season
+Subject: [PATCH 145/171] tmdb3tv: Prevent exception when non-existent season
is requested.
(cherry picked from commit a064e11921d19fd3ee08a09553e48b80b5eba6cf)
@@ -28925,7 +28925,7 @@ index 89b1d74d5db..e7d2229c41b 100644
From b2ed400037de11c7e5546381e60cc9de7b1e15eb Mon Sep 17 00:00:00 2001
From: Klaas de Waal <kdewaal(a)mythtv.org>
Date: Sat, 1 May 2021 21:48:02 +0200
-Subject: [PATCH 146/165] Update file size before skip forward
+Subject: [PATCH 146/171] Update file size before skip forward
Get the actual file size before determining if it is
possible to skip the requested amount of seconds forward.
@@ -28960,7 +28960,7 @@ index 1eecc103cfe..1e4b34216b8 100644
From f7de8cea580dfd215dfe6d3e83201edb85647b8d Mon Sep 17 00:00:00 2001
From: Peter Bennett <pbennett(a)mythtv.org>
Date: Thu, 6 May 2021 11:21:36 -0400
-Subject: [PATCH 147/165] tmdb3 lookup: Prevent exception when there are no
+Subject: [PATCH 147/171] tmdb3 lookup: Prevent exception when there are no
posters.
Index out of range occurred in number of posters was 0.
@@ -28996,7 +28996,7 @@ index e7d2229c41b..9482280c2e6 100644
From 200e3f47576d29af307ba26aed8710f02e0df227 Mon Sep 17 00:00:00 2001
From: Klaas de Waal <kdewaal(a)mythtv.org>
Date: Mon, 19 Apr 2021 20:56:39 +0200
-Subject: [PATCH 148/165] Update changed streams on PMT update
+Subject: [PATCH 148/171] Update changed streams on PMT update
On playback, when a new version of the PMT (program map table) is received, start
updating the streams at the first stream that is changed, beginning with stream 0,
@@ -29184,7 +29184,7 @@ index 149c737dda6..a361106164c 100644
From bcc01811cabd66eabc79987c5672a6a3d0c02243 Mon Sep 17 00:00:00 2001
From: Gary Buhrmaster <gary.buhrmaster(a)gmail.com>
Date: Thu, 27 Aug 2020 22:32:41 +0100
-Subject: [PATCH 149/165] V4LChannel: Remove unneeded definitions
+Subject: [PATCH 149/171] V4LChannel: Remove unneeded definitions
Refs #13653
@@ -29221,7 +29221,7 @@ index 34a6f5d7f59..0367e81ede1 100644
From 9b74773e3ca4c762c884eaaeb68077f236a45395 Mon Sep 17 00:00:00 2001
From: Gary Buhrmaster <gary.buhrmaster(a)gmail.com>
Date: Wed, 30 Sep 2020 12:08:32 +0100
-Subject: [PATCH 150/165] Use system videodev2.h
+Subject: [PATCH 150/171] Use system videodev2.h
The project no longer supports distributions with the older kernels that
required having a local copy of videodev2.h.
@@ -29266,7 +29266,7 @@ index 48322c78450..aeaa14df2f5 100644
From fe5a6243cef9a838786225fb70c330d42be15bd0 Mon Sep 17 00:00:00 2001
From: Stuart Auchterlonie <stuarta(a)mythtv.org>
Date: Mon, 17 May 2021 23:31:46 +0100
-Subject: [PATCH 151/165] Revert "Use system videodev2.h" - Broke
+Subject: [PATCH 151/171] Revert "Use system videodev2.h" - Broke
debian-stretch and centos7 builds
This reverts commit 9b74773e3ca4c762c884eaaeb68077f236a45395.
@@ -29305,7 +29305,7 @@ index aeaa14df2f5..48322c78450 100644
From f8c59ee69d71beb825d8765ad6bc729b1cfc0150 Mon Sep 17 00:00:00 2001
From: Stuart Auchterlonie <stuarta(a)mythtv.org>
Date: Mon, 17 May 2021 23:32:29 +0100
-Subject: [PATCH 152/165] Revert "V4LChannel: Remove unneeded definitions" -
+Subject: [PATCH 152/171] Revert "V4LChannel: Remove unneeded definitions" -
Broke debian-stretch and centos7 builds
This reverts commit bcc01811cabd66eabc79987c5672a6a3d0c02243.
@@ -29339,7 +29339,7 @@ index 0367e81ede1..34a6f5d7f59 100644
From 1510439288cb874e51aacd14e248bf14c32269b5 Mon Sep 17 00:00:00 2001
From: Bill Meek <billmeek(a)mythtv.org>
Date: Tue, 25 May 2021 12:18:56 -0500
-Subject: [PATCH 153/165] DB: Reconnect if MySQL error code 4031 is received
+Subject: [PATCH 153/171] DB: Reconnect if MySQL error code 4031 is received
A new error code was introduced in MySQL client v8.0.24 that
mythdbcon.cpp didn't handle. Users would loose the ability
@@ -29466,7 +29466,7 @@ index 18efb81158d..17d9a4190d9 100644
From df29c72b4d1d5aa6728bf1ad4264439a4d121855 Mon Sep 17 00:00:00 2001
From: Bill Meek <billmeek(a)mythtv.org>
Date: Tue, 25 May 2021 22:44:51 -0500
-Subject: [PATCH 154/165] tidy: simplified return from lostConnectionCheck()
+Subject: [PATCH 154/171] tidy: simplified return from lostConnectionCheck()
(cherry picked from commit eafe170b260ce4d6128e14a10af54d80e297d1b9)
---
@@ -29493,7 +29493,7 @@ index 0a1a81ddedb..dfd07f0a966 100644
From 3162473370b31c749e8417b1b19fe6dc2186cb95 Mon Sep 17 00:00:00 2001
From: John Hoyt <john.hoyt(a)gmail.com>
Date: Sun, 30 May 2021 07:52:59 -0400
-Subject: [PATCH 155/165] OSX: Rename VERSION file to SRC_VERSION to correct
+Subject: [PATCH 155/171] OSX: Rename VERSION file to SRC_VERSION to correct
conflict with C++17 version header on case insensitive filesystems
---
@@ -29555,7 +29555,7 @@ index d412cc0505d..9b8f144bdb0 100755
From 0680b37c6841e10f494cf836862c6d0879e573b0 Mon Sep 17 00:00:00 2001
From: Klaas de Waal <kdewaal(a)mythtv.org>
Date: Thu, 10 Jun 2021 23:23:01 +0200
-Subject: [PATCH 156/165] Crash in libCEC on mythfrontend GUI rebuild
+Subject: [PATCH 156/171] Crash in libCEC on mythfrontend GUI rebuild
Fix mythfrontend crash in GUI rebuild due to a theme change or due
to a change in the window size. This happens only when mythfrontend
@@ -29598,7 +29598,7 @@ index 3f05df23309..2de86217ded 100644
From 7037525723b56a30959d6fdf8e4b5a72c336a6ac Mon Sep 17 00:00:00 2001
From: Klaas de Waal <klaas(a)kldo.nl>
Date: Sun, 25 Jul 2021 08:51:33 +0200
-Subject: [PATCH 157/165] DB access for information about existing transports
+Subject: [PATCH 157/171] DB access for information about existing transports
In mythtv-setup, the channels found in a scan are compared with the channels in the
database.
Do not retrieve the sourceid from the the first channel of the first transport
@@ -29628,7 +29628,7 @@ index 9573feb9344..99154dbdcc7 100644
From d59053b747b64b2efd654aacbbc8d049be173257 Mon Sep 17 00:00:00 2001
From: David Hampton <mythtv(a)love2code.net>
Date: Tue, 27 Jul 2021 16:45:56 -0400
-Subject: [PATCH 158/165] Fix videodev2.h compilation errors on FreeBSD.
+Subject: [PATCH 158/171] Fix videodev2.h compilation errors on FreeBSD.
The file v4lchannel.cpp should include our own copy of videodev2.h.
Make the same change to this file as was made to the v4l2 files in
@@ -29657,7 +29657,7 @@ index d6c16e81da7..84d5f93fd59 100644
From 46cf50b44d1d9dd5e0234ffc6200179e6930b44a Mon Sep 17 00:00:00 2001
From: Gary Buhrmaster <gary.buhrmaster(a)gmail.com>
Date: Tue, 27 Jul 2021 01:34:13 +0000
-Subject: [PATCH 159/165] mytharchive: change offset to `offset` for MariaDB
+Subject: [PATCH 159/171] mytharchive: change offset to `offset` for MariaDB
10.6
MariaDB 10.6 has added a new reserved word offset which
@@ -29696,7 +29696,7 @@ index 13ca02ddeed..ba0f8feb166 100644
From 5824c588db24b4e71a7d94e829e6419f71089297 Mon Sep 17 00:00:00 2001
From: Gary Buhrmaster <gary.buhrmaster(a)gmail.com>
Date: Tue, 27 Jul 2021 01:36:31 +0000
-Subject: [PATCH 160/165] programinfo: change offset to `offset` for MariaDB
+Subject: [PATCH 160/171] programinfo: change offset to `offset` for MariaDB
10.6
MariaDB 10.6 has added a new reserved word offset which
@@ -29893,7 +29893,7 @@ index eebb99a4fd5..fced95fc0a5 100644
From 5da25231540158faa10ee2309eb94103ff74f417 Mon Sep 17 00:00:00 2001
From: Gary Buhrmaster <gary.buhrmaster(a)gmail.com>
Date: Sat, 7 Aug 2021 15:44:37 +0200
-Subject: [PATCH 161/165] Correct FSF address
+Subject: [PATCH 161/171] Correct FSF address
Correct FSF address (low hanging fruit) in a few files that seem to have been
added/updated
since the last pass was performed to correct the FSF address (initially identified by
rpmlint).
@@ -29973,7 +29973,7 @@ index a2acc946557..1c14e85ce6b 100644
From 1db67b26d8bfae8d5a43dca2eeb012efa5a1d776 Mon Sep 17 00:00:00 2001
From: Gary Buhrmaster <gary.buhrmaster(a)gmail.com>
Date: Thu, 22 Jul 2021 17:29:03 +0000
-Subject: [PATCH 162/165] Adjust for the removal of the ABCs from the python
+Subject: [PATCH 162/171] Adjust for the removal of the ABCs from the python
collections module
Python 3.10 will finally remove the long deprecated aliases to the
@@ -30061,7 +30061,7 @@ index 6fb9d91a8fe..0964ed8b789 100644
From 5d6f06cc3a72e46700df268d11e20c78274359d5 Mon Sep 17 00:00:00 2001
From: David Engel <dengel(a)mythtv.org>
Date: Thu, 28 Oct 2021 14:32:23 -0500
-Subject: [PATCH 163/165] Fix boolean conversion issue with new MySQL 8.
+Subject: [PATCH 163/171] Fix boolean conversion issue with new MySQL 8.
MySQL 8.0.27 changed something with how it handles the implicit
conversions of booleans when used with arithmetic operators. The
@@ -30143,7 +30143,7 @@ index 666b67820f4..20672bb194a 100644
From bb507cf183e26b5c61fd12b4aa7b2d2434b2489a Mon Sep 17 00:00:00 2001
From: Bill Meek <billmeek(a)mythtv.org>
Date: Mon, 1 Nov 2021 13:22:15 -0500
-Subject: [PATCH 164/165] Fix boolean conversion issue with new MySQL 8.
+Subject: [PATCH 164/171] Fix boolean conversion issue with new MySQL 8.
Care for Custom Priorities.
@@ -30171,7 +30171,7 @@ index 20672bb194a..cb475e75ed6 100644
From 25f1bb1d12fdee5b9ea3841fd39332db9431e4a2 Mon Sep 17 00:00:00 2001
From: Klaas de Waal <kdewaal(a)mythtv.org>
Date: Wed, 3 Nov 2021 21:18:47 +0100
-Subject: [PATCH 165/165] Restarting playback with VDPAU after PMT change
+Subject: [PATCH 165/171] Restarting playback with VDPAU after PMT change
Playback of some recordings of the Finnish broadcaster YLE fails
when the VDPAU hardware accelerator is selected in the playback profile.
@@ -30230,3 +30230,4602 @@ index 56e57ee2d26..bd6659f4c54 100644
(decodeonly ? codec_is_vdpau_dechw(success) :
codec_is_vdpau_hw(success));
if (vdpau)
+
+From a7826bec682828d0d55fcaf05958ea1aedcd03fb Mon Sep 17 00:00:00 2001
+From: Roland Ernst <rcrernst(a)gmail.com>
+Date: Tue, 18 Jan 2022 22:18:51 +0100
+Subject: [PATCH 166/171] TV Grabber: Support for TheTVDB v4 API
+
+This is a backport of the 'ttvdb4.py' television grabber from
+MythTV/master to fixes/31.
+The current API from
thetvdb.com (v3) is marked as deprecated
+and will be removed soon.
+
+The TheTVDB v4 API specification is located at
+https://github.com/thetvdb/v4-api/blob/main/docs/swagger.yml
+
+Please look at
https://github.com/thetvdb/v4-api/issues
+before raising any issues against MythTV.
+
+In order to enable this grabber, run in mythtv frontend
+"Setup" -> "Artwork and Data Sources"
+and select 'TheTVDatabaseV4' for the default TelevisionGrabber.
+
+Upon first start, a file named 'ttvdb4.ini' is created under
+the '$HOME/.mythtv' folder, which lets you add some options, like
+additional languages to seach. Edit this file according your needs.
+
+Refs #463
+---
+ mythtv/bindings/python/setup.py | 4 +-
+ mythtv/bindings/python/ttvdbv4/__init__.py | 0
+ mythtv/bindings/python/ttvdbv4/definitions.py | 850 +++++++++++++++++
+ mythtv/bindings/python/ttvdbv4/get_api_v4.py | 286 ++++++
+ mythtv/bindings/python/ttvdbv4/locales.py | 586 ++++++++++++
+ .../bindings/python/ttvdbv4/myth4ttvdbv4.py | 882 ++++++++++++++++++
+ mythtv/bindings/python/ttvdbv4/ttvdbv4_api.py | 602 ++++++++++++
+ mythtv/bindings/python/ttvdbv4/utils.py | 73 ++
+ .../scripts/metadata/Television/ttvdb4.ini | 59 ++
+ .../scripts/metadata/Television/ttvdb4.py | 296 ++++++
+ .../metadata/Television/ttvdb4_doctests | 576 ++++++++++++
+ 11 files changed, 4212 insertions(+), 2 deletions(-)
+ create mode 100644 mythtv/bindings/python/ttvdbv4/__init__.py
+ create mode 100644 mythtv/bindings/python/ttvdbv4/definitions.py
+ create mode 100644 mythtv/bindings/python/ttvdbv4/get_api_v4.py
+ create mode 100644 mythtv/bindings/python/ttvdbv4/locales.py
+ create mode 100644 mythtv/bindings/python/ttvdbv4/myth4ttvdbv4.py
+ create mode 100644 mythtv/bindings/python/ttvdbv4/ttvdbv4_api.py
+ create mode 100644 mythtv/bindings/python/ttvdbv4/utils.py
+ create mode 100644 mythtv/programs/scripts/metadata/Television/ttvdb4.ini
+ create mode 100755 mythtv/programs/scripts/metadata/Television/ttvdb4.py
+ create mode 100644 mythtv/programs/scripts/metadata/Television/ttvdb4_doctests
+
+diff --git a/mythtv/bindings/python/setup.py b/mythtv/bindings/python/setup.py
+index c7270faee9b..b9adb75fe9a 100755
+--- a/mythtv/bindings/python/setup.py
++++ b/mythtv/bindings/python/setup.py
+@@ -82,10 +82,10 @@ def run(self):
+ version='31.0.-1',
+ description='MythTV Python bindings.',
+ long_description='Provides canned database and protocol access to the MythTV
database, mythproto, mythxml, services_api and frontend remote control.',
+- packages=['MythTV', 'MythTV/tmdb3', 'MythTV/ttvdb',
'MythTV/tvmaze',
++ packages=['MythTV', 'MythTV/tmdb3', 'MythTV/ttvdb',
'MythTV/tvmaze', 'MythTV/ttvdbv4',
+ 'MythTV/wikiscripts', 'MythTV/utility',
+ 'MythTV/services_api'],
+- package_dir={'MythTV/tmdb3':'./tmdb3/tmdb3',
'MythTV/tvmaze':'./tvmaze'},
++ package_dir={'MythTV/tmdb3':'./tmdb3/tmdb3',
'MythTV/tvmaze':'./tvmaze',
'MythTV/ttvdbv4':'./ttvdbv4'},
+ data_files=[('MythTV/ttvdb/XSLT',
glob.glob('MythTV/ttvdb/XSLT/*'))],
+
url=['http://www.mythtv.org/'],
+ scripts=SCRIPTS,
+diff --git a/mythtv/bindings/python/ttvdbv4/__init__.py
b/mythtv/bindings/python/ttvdbv4/__init__.py
+new file mode 100644
+index 00000000000..e69de29bb2d
+diff --git a/mythtv/bindings/python/ttvdbv4/definitions.py
b/mythtv/bindings/python/ttvdbv4/definitions.py
+new file mode 100644
+index 00000000000..f4904b3e548
+--- /dev/null
++++ b/mythtv/bindings/python/ttvdbv4/definitions.py
+@@ -0,0 +1,850 @@
++# -*- coding: UTF-8 -*-
++
++# ----------------------------------------------------
++# Purpose: MythTV Python Bindings for TheTVDB v4 API
++# Copyright: (c) 2021 Roland Ernst
++# License: GPL v2 or later, see LICENSE for details
++# ----------------------------------------------------
++
++
++def _get_list(item, name):
++ l = []
++ if item is not None:
++ try:
++ l = [x for x in item.get('%s' % name)]
++ except (AttributeError, ValueError, IndexError, TypeError):
++ pass
++ return l
++
++
++def _handle_single(handle, data):
++ try:
++ return handle(data)
++ except (AttributeError, ValueError):
++ return None
++
++
++def _handle_list(handle, data):
++ l = []
++ if data is not None:
++ try:
++ for d in data:
++ el = _handle_single(handle, d)
++ if el is not None:
++ l.append(el)
++ except IndexError:
++ pass
++ return l
++ else:
++ return l
++
++
++"""Generated API for
thetvdb.com TVDB API V4 v 4.5.0"""
++# modifications marked with '### XXX'
++
++
++class Alias(object):
++ """An alias model, which can be associated with a series, season,
movie, person, or list."""
++ def __init__(self, data):
++ self.language = data.get('language', '')
# string
++ self.name = data.get('name', '')
# string
++
++
++class ArtworkBaseRecord(object):
++ """base artwork record"""
++ def __init__(self, data):
++ self.id = data.get('id', 0) #
integer
++ self.image = data.get('image', '')
# string
++ self.language = data.get('language', '')
# string
++ self.score = data.get('score', 0.0) #
number
++ self.thumbnail = data.get('thumbnail', '')
# string
++ self.type = data.get('type', 0) #
integer
++
++
++class ArtworkExtendedRecord(object):
++ """extended artwork record"""
++ def __init__(self, data):
++ self.episodeId = data.get('episodeId', 0) #
integer
++ self.height = data.get('height', 0) #
integer
++ self.id = data.get('id', 0) #
integer
++ self.image = data.get('image', '')
# string
++ self.language = data.get('language', '')
# string
++ self.movieId = data.get('movieId', 0) #
integer
++ self.networkId = data.get('networkId', 0) #
integer
++ self.peopleId = data.get('peopleId', 0) #
integer
++ self.score = data.get('score', 0.0) #
number
++ self.seasonId = data.get('seasonId', 0) #
integer
++ self.seriesId = data.get('seriesId', 0) #
integer
++ self.seriesPeopleId = data.get('seriesPeopleId', 0) #
integer
++ self.tagOptions = _handle_list(TagOption, data.get('tagOptions'))
++ self.thumbnail = data.get('thumbnail', '')
# string
++ self.thumbnailHeight = data.get('thumbnailHeight', 0) #
integer
++ self.thumbnailWidth = data.get('thumbnailWidth', 0) #
integer
++ self.type = data.get('type', 0) #
integer
++ self.updatedAt = data.get('updatedAt', 0) #
integer
++ self.width = data.get('width', 0) #
integer
++
++
++class ArtworkStatus(object):
++ """artwork status record"""
++ def __init__(self, data):
++ self.id = data.get('id', 0) #
integer
++ self.name = data.get('name', '')
# string
++
++
++class ArtworkType(object):
++ """artwork type record"""
++ def __init__(self, data):
++ self.height = data.get('height', 0) #
integer
++ self.id = data.get('id', 0) #
integer
++ self.imageFormat = data.get('imageFormat', '')
# string
++ self.name = data.get('name', '')
# string
++ self.recordType = data.get('recordType', '')
# string
++ self.slug = data.get('slug', '')
# string
++ self.thumbHeight = data.get('thumbHeight', 0) #
integer
++ self.thumbWidth = data.get('thumbWidth', 0) #
integer
++ self.width = data.get('width', 0) #
integer
++
++
++class AwardBaseRecord(object):
++ """base award record"""
++ def __init__(self, data):
++ self.id = data.get('id', 0) #
integer
++ self.name = data.get('name', '')
# string
++
++
++class AwardCategoryBaseRecord(object):
++ """base award category record"""
++ def __init__(self, data):
++ self.allowCoNominees = data.get('allowCoNominees', False) #
boolean
++ self.award = _handle_single(AwardBaseRecord, data.get('award'))
++ self.forMovies = data.get('forMovies', False) #
boolean
++ self.forSeries = data.get('forSeries', False) #
boolean
++ self.id = data.get('id', 0) #
integer
++ self.name = data.get('name', '')
# string
++
++
++class AwardCategoryExtendedRecord(object):
++ """extended award category record"""
++ def __init__(self, data):
++ self.allowCoNominees = data.get('allowCoNominees', False) #
boolean
++ self.award = _handle_single(AwardBaseRecord, data.get('award'))
++ self.forMovies = data.get('forMovies', False) #
boolean
++ self.forSeries = data.get('forSeries', False) #
boolean
++ self.id = data.get('id', 0) #
integer
++ self.name = data.get('name', '')
# string
++ self.nominees = _handle_list(AwardNomineeBaseRecord,
data.get('nominees'))
++
++
++class AwardExtendedRecord(object):
++ """extended award record"""
++ def __init__(self, data):
++ self.categories = _handle_list(AwardCategoryBaseRecord,
data.get('categories'))
++ self.id = data.get('id', 0) #
integer
++ self.name = data.get('name', '')
# string
++ self.score = data.get('score', 0) #
integer
++
++
++class AwardNomineeBaseRecord(object):
++ """base award nominee record"""
++ def __init__(self, data):
++ self.character = _handle_single(Character, data.get('character'))
++ self.details = data.get('details', '')
# string
++ self.episode = _handle_single(EpisodeBaseRecord, data.get('episode'))
++ self.id = data.get('id', 0) #
integer
++ self.isWinner = data.get('isWinner', False) #
boolean
++ self.movie = _handle_single(MovieBaseRecord, data.get('movie'))
++ self.series = _handle_single(SeriesBaseRecord, data.get('series'))
++ self.year = data.get('year', '')
# string
++ self.category = data.get('category', '')
# string
++ self.name = data.get('name', '')
# string
++
++
++class Biography(object):
++ """biography record"""
++ def __init__(self, data):
++ self.biography = data.get('biography', '')
# string
++ self.language = data.get('language', '')
# string
++
++
++class Character(object):
++ """character record"""
++ def __init__(self, data):
++ self.aliases = _handle_list(Alias, data.get('aliases'))
++ self.episodeId = data.get('episodeId', 0) #
integer
++ self.id = data.get('id', 0) #
integer
++ self.image = data.get('image', '')
# string
++ self.isFeatured = data.get('isFeatured', False) #
boolean
++ self.movieId = data.get('movieId', 0) #
integer
++ self.name = data.get('name', '')
# string
++ self.nameTranslations = _get_list(data, 'nameTranslations')
++ self.overviewTranslations = _get_list(data, 'overviewTranslations')
++ self.peopleId = data.get('peopleId', 0) #
integer
++ self.personImgURL = data.get('personImgURL', '')
# string
++ self.seriesId = data.get('seriesId', 0) #
integer
++ self.sort = data.get('sort', 0) #
integer
++ self.type = data.get('type', 0) #
integer
++ self.url = data.get('url', '')
# string
++ self.personName = data.get('personName', '')
# string
++ # additional attributes needed by the mythtv grabber script:
++ self.fetched_translations = []
++ self.name_similarity = 0.0
++
++
++class Company(object):
++ """A company record"""
++ def __init__(self, data):
++ self.activeDate = data.get('activeDate', '')
# string
++ self.aliases = _handle_list(Alias, data.get('aliases'))
++ self.country = data.get('country', '')
# string
++ self.id = data.get('id', 0) #
integer
++ self.inactiveDate = data.get('inactiveDate', '')
# string
++ self.name = data.get('name', '')
# string
++ self.nameTranslations = _get_list(data, 'nameTranslations')
++ self.overviewTranslations = _get_list(data, 'overviewTranslations')
++ self.primaryCompanyType = data.get('primaryCompanyType', 0) #
integer
++ self.slug = data.get('slug', '')
# string
++ self.companies = _handle_single(Companies, data.get('companies'))
++ self.parentCompany = _handle_single(ParentCompany,
data.get('parentCompany'))
++ # additional attributes needed by the mythtv grabber script:
++ self.fetched_translations = []
++ self.name_similarity = 0.0
++
++
++class ParentCompany(object):
++ """A parent company record"""
++ def __init__(self, data):
++ self.id = data.get('id', 0) #
integer
++ self.name = data.get('name', '')
# string
++ self.relation = _handle_single(CompanyRelationShip,
data.get('relation'))
++
++
++class CompanyRelationShip(object):
++ """A company relationship"""
++ def __init__(self, data):
++ self.id = data.get('id', 0) #
integer
++ self.typeName = data.get('typeName', '')
# string
++
++
++class CompanyType(object):
++ """A company type record"""
++ def __init__(self, data):
++ self.companyTypeId = data.get('companyTypeId', 0) #
integer
++ self.companyTypeName = data.get('companyTypeName', '')
# string
++
++
++class ContentRating(object):
++ """content rating record"""
++ def __init__(self, data):
++ self.id = data.get('id', 0) #
integer
++ self.name = data.get('name', '')
# string
++ self.description = data.get('description', '')
# string
++ self.country = data.get('country', '')
# string
++ self.contentType = data.get('contentType', '')
# string
++ self.order = data.get('order', 0) #
integer
++ self.fullName = data.get('fullName', '')
# string
++
++
++class Country(object):
++ """country record"""
++ def __init__(self, data):
++ self.id = data.get('id', '')
# string
++ self.name = data.get('name', '')
# string
++ self.shortCode = data.get('shortCode', '')
# string
++
++
++class Entity(object):
++ """Entity record"""
++ def __init__(self, data):
++ self.movieId = data.get('movieId', 0) #
integer
++ self.order = data.get('order', 0) #
integer
++ self.seriesId = data.get('seriesId', 0) #
integer
++
++
++class EntityType(object):
++ """Entity Type record"""
++ def __init__(self, data):
++ self.id = data.get('id', 0) #
integer
++ self.name = data.get('name', '')
# string
++ self.hasSpecials = data.get('hasSpecials', False) #
boolean
++
++
++class EntityUpdate(object):
++ """entity update record"""
++ def __init__(self, data):
++ self.entityType = data.get('entityType', '')
# string
++ self.method = data.get('method', '')
# string
++ self.recordId = data.get('recordId', 0) #
integer
++ self.timeStamp = data.get('timeStamp', 0) #
integer
++ self.seriesId = data.get('seriesId', 0) #
integer
++
++
++class EpisodeBaseRecord(object):
++ """base episode record"""
++ def __init__(self, data):
++ self.aired = data.get('aired', '')
# string
++ self.id = data.get('id', 0) #
integer
++ self.image = data.get('image', '')
# string
++ self.imageType = data.get('imageType', 0) #
integer
++ self.isMovie = data.get('isMovie', 0) #
integer
++ self.name = data.get('name', '')
# string
++ self.nameTranslations = _get_list(data, 'nameTranslations')
++ self.number = data.get('number', 0) #
integer
++ self.overviewTranslations = _get_list(data, 'overviewTranslations')
++ self.runtime = data.get('runtime', 0) #
integer
++ self.seasonNumber = data.get('seasonNumber', 0) #
integer
++ self.seasons = _handle_list(SeasonBaseRecord, data.get('seasons'))
++ self.seriesId = data.get('seriesId', 0) #
integer
++ self.seasonName = data.get('seasonName', '')
# string
++ self.lastUpdated = data.get('lastUpdated', '')
# string
++ self.finaleType = data.get('finaleType', '')
# string
++ # additional attributes needed by the mythtv grabber script:
++ self.fetched_translations = []
++ self.name_similarity = 0.0
++
++
++class EpisodeExtendedRecord(object):
++ """extended episode record"""
++ def __init__(self, data):
++ self.aired = data.get('aired', '')
# string
++ self.airsAfterSeason = data.get('airsAfterSeason', 0) #
integer
++ self.airsBeforeEpisode = data.get('airsBeforeEpisode', 0) #
integer
++ self.airsBeforeSeason = data.get('airsBeforeSeason', 0) #
integer
++ self.awards = _handle_list(AwardBaseRecord, data.get('awards'))
++ self.characters = _handle_list(Character, data.get('characters'))
++ self.contentRatings = _handle_list(ContentRating,
data.get('contentRatings'))
++ self.id = data.get('id', 0) #
integer
++ self.image = data.get('image', '')
# string
++ self.imageType = data.get('imageType', 0) #
integer
++ self.isMovie = data.get('isMovie', 0) #
integer
++ self.name = data.get('name', '')
# string
++ self.nameTranslations = _get_list(data, 'nameTranslations')
++ self.network = _handle_single(NetworkBaseRecord, data.get('network'))
++ self.number = data.get('number', 0) #
integer
++ self.overviewTranslations = _get_list(data, 'overviewTranslations')
++ self.productionCode = data.get('productionCode', '')
# string
++ self.remoteIds = _handle_list(RemoteID, data.get('remoteIds'))
++ self.runtime = data.get('runtime', 0) #
integer
++ self.seasonNumber = data.get('seasonNumber', 0) #
integer
++ self.seasons = _handle_list(SeasonBaseRecord, data.get('seasons'))
++ self.seriesId = data.get('seriesId', 0) #
integer
++ self.tagOptions = _handle_list(TagOption, data.get('tagOptions'))
++ self.trailers = _handle_list(Trailer, data.get('trailers'))
++ self.companies = _handle_list(Company, data.get('companies'))
++ self.lastUpdated = data.get('lastUpdated', '')
# string
++ self.finaleType = data.get('finaleType', '')
# string
++ # additional attributes needed by the mythtv grabber script:
++ self.fetched_translations = []
++ self.name_similarity = 0.0
++
++
++class Gender(object):
++ """gender record"""
++ def __init__(self, data):
++ self.id = data.get('id', 0) #
integer
++ self.name = data.get('name', '')
# string
++
++
++class GenreBaseRecord(object):
++ """base genre record"""
++ def __init__(self, data):
++ self.id = data.get('id', 0) #
integer
++ self.name = data.get('name', '')
# string
++ self.slug = data.get('slug', '')
# string
++
++
++class Language(object):
++ """language record"""
++ def __init__(self, data):
++ self.id = data.get('id', '')
# string
++ self.name = data.get('name', '')
# string
++ self.nativeName = data.get('nativeName', '')
# string
++ self.shortCode = data.get('shortCode', '')
# string
++
++
++class ListBaseRecord(object):
++ """base list record"""
++ def __init__(self, data):
++ self.aliases = _handle_list(Alias, data.get('aliases'))
++ self.id = data.get('id', 0) #
integer
++ self.isOfficial = data.get('isOfficial', False) #
boolean
++ self.name = data.get('name', '')
# string
++ self.nameTranslations = _get_list(data, 'nameTranslations')
++ self.overview = data.get('overview', '')
# string
++ self.overviewTranslations = _get_list(data, 'overviewTranslations')
++ self.url = data.get('url', '')
# string
++ self.score = data.get('score', 0) #
integer
++ # additional attributes needed by the mythtv grabber script:
++ self.fetched_translations = []
++ self.name_similarity = 0.0
++
++
++class ListExtendedRecord(object):
++ """extended list record"""
++ def __init__(self, data):
++ self.aliases = _handle_list(Alias, data.get('aliases'))
++ self.entities = _handle_list(Entity, data.get('entities'))
++ self.id = data.get('id', 0) #
integer
++ self.image = data.get('image', '')
# string
++ self.imageIsFallback = data.get('imageIsFallback', False) #
boolean
++ self.isOfficial = data.get('isOfficial', False) #
boolean
++ self.name = data.get('name', '')
# string
++ self.nameTranslations = _get_list(data, 'nameTranslations')
++ self.overview = data.get('overview', '')
# string
++ self.overviewTranslations = _get_list(data, 'overviewTranslations')
++ self.score = data.get('score', 0) #
integer
++ self.url = data.get('url', '')
# string
++ # additional attributes needed by the mythtv grabber script:
++ self.fetched_translations = []
++ self.name_similarity = 0.0
++
++
++class MovieBaseRecord(object):
++ """base movie record"""
++ def __init__(self, data):
++ self.aliases = _handle_list(Alias, data.get('aliases'))
++ self.id = data.get('id', 0) #
integer
++ self.image = data.get('image', '')
# string
++ self.name = data.get('name', '')
# string
++ self.nameTranslations = _get_list(data, 'nameTranslations')
++ self.overviewTranslations = _get_list(data, 'overviewTranslations')
++ self.score = data.get('score', 0.0) #
number
++ self.slug = data.get('slug', '')
# string
++ self.status = _handle_single(Status, data.get('status'))
++ self.runtime = data.get('runtime', 0) #
integer
++ self.lastUpdated = data.get('lastUpdated', '')
# string
++ # additional attributes needed by the mythtv grabber script:
++ self.fetched_translations = []
++ self.name_similarity = 0.0
++
++
++class MovieExtendedRecord(object):
++ """extended movie record"""
++ def __init__(self, data):
++ self.aliases = _handle_list(Alias, data.get('aliases'))
++ self.artworks = _handle_list(ArtworkBaseRecord, data.get('artworks'))
++ self.audioLanguages = _get_list(data, 'audioLanguages')
++ self.awards = _handle_list(AwardBaseRecord, data.get('awards'))
++ self.boxOffice = data.get('boxOffice', '')
# string
++ self.budget = data.get('budget', '')
# string
++ self.characters = _handle_list(Character, data.get('characters'))
++ self.lists = _handle_list(ListBaseRecord, data.get('lists'))
++ self.genres = _handle_list(GenreBaseRecord, data.get('genres'))
++ self.id = data.get('id', 0) #
integer
++ self.image = data.get('image', '')
# string
++ self.name = data.get('name', '')
# string
++ self.nameTranslations = _get_list(data, 'nameTranslations')
++ self.originalCountry = data.get('originalCountry', '')
# string
++ self.originalLanguage = data.get('originalLanguage', '')
# string
++ self.overviewTranslations = _get_list(data, 'overviewTranslations')
++ self.releases = _handle_list(Release, data.get('releases'))
++ self.remoteIds = _handle_list(RemoteID, data.get('remoteIds'))
++ self.contentRatings = _handle_list(ContentRating,
data.get('contentRatings'))
++ self.score = data.get('score', 0.0) #
number
++ self.slug = data.get('slug', '')
# string
++ self.status = _handle_single(Status, data.get('status'))
++ self.studios = _handle_list(StudioBaseRecord, data.get('studios'))
++ self.subtitleLanguages = _get_list(data, 'subtitleLanguages')
++ self.tagOptions = _handle_list(TagOption, data.get('tagOptions'))
++ self.trailers = _handle_list(Trailer, data.get('trailers'))
++ self.inspirations = _handle_list(Inspiration, data.get('inspirations'))
++ self.productionCountries = _handle_list(ProductionCountry,
data.get('productionCountries'))
++ self.spokenLanguages = _get_list(data, 'spokenLanguages')
++ self.firstRelease = _handle_single(Release, data.get('firstRelease'))
++ self.companies = _handle_single(Companies, data.get('companies'))
++ # additional attributes needed by the mythtv grabber script:
++ self.fetched_translations = []
++ self.name_similarity = 0.0
++
++
++class NetworkBaseRecord(object):
++ """base network record"""
++ def __init__(self, data):
++ self.abbreviation = data.get('abbreviation', '')
# string
++ self.country = data.get('country', '')
# string
++ self.id = data.get('id', 0) #
integer
++ self.name = data.get('name', '')
# string
++ self.slug = data.get('slug', '')
# string
++
++
++class PeopleBaseRecord(object):
++ """base people record"""
++ def __init__(self, data):
++ self.aliases = _handle_list(Alias, data.get('aliases'))
++ self.id = data.get('id', 0) #
integer
++ self.image = data.get('image', '')
# string
++ self.name = data.get('name', '')
# string
++ self.nameTranslations = _get_list(data, 'nameTranslations')
++ self.overviewTranslations = _get_list(data, 'overviewTranslations')
++ self.score = data.get('score', 0) #
integer
++ # additional attributes needed by the mythtv grabber script:
++ self.fetched_translations = []
++ self.name_similarity = 0.0
++
++
++class PeopleExtendedRecord(object):
++ """extended people record"""
++ def __init__(self, data):
++ self.aliases = _handle_list(Alias, data.get('aliases'))
++ self.awards = _handle_list(AwardBaseRecord, data.get('awards'))
++ self.biographies = _handle_list(Biography, data.get('biographies'))
++ self.birth = data.get('birth', '')
# string
++ self.birthPlace = data.get('birthPlace', '')
# string
++ self.characters = _handle_list(Character, data.get('characters'))
++ self.death = data.get('death', '')
# string
++ self.gender = data.get('gender', 0) #
integer
++ self.id = data.get('id', 0) #
integer
++ self.image = data.get('image', '')
# string
++ self.name = data.get('name', '')
# string
++ self.races = _handle_list(Race, data.get('races'))
++ self.remoteIds = _handle_list(RemoteID, data.get('remoteIds'))
++ self.score = data.get('score', 0) #
integer
++ self.tagOptions = _handle_list(TagOption, data.get('tagOptions'))
++
++
++class PeopleType(object):
++ """people type record"""
++ def __init__(self, data):
++ self.id = data.get('id', 0) #
integer
++ self.name = data.get('name', '')
# string
++
++
++class Race(object):
++ """race record"""
++ def __init__(self, data):
++ pass
++
++
++class Release(object):
++ """release record"""
++ def __init__(self, data):
++ self.country = data.get('country', '')
# string
++ self.date = data.get('date', '')
# string
++ self.detail = data.get('detail', '')
# string
++
++
++class RemoteID(object):
++ """remote id record"""
++ def __init__(self, data):
++ self.id = data.get('id', '')
# string
++ self.type = data.get('type', 0) #
integer
++ self.sourceName = data.get('sourceName', '')
# string
++
++
++class SearchResult(object):
++ """search result"""
++ def __init__(self, data):
++ self.aliases = _get_list(data, 'aliases')
++ self.companies = _get_list(data, 'companies')
++ self.companyType = data.get('companyType', '')
# string
++ self.country = data.get('country', '')
# string
++ self.director = data.get('director', '')
# string
++ self.genres = _get_list(data, 'genres')
++ self.objectID = data.get('objectID', '')
# string
++ self.slug = data.get('slug', '')
# string
++ self.id = data.get('id', '')
# string
++ self.image_url = data.get('image_url', '')
# string
++ self.name = data.get('name', '')
# string
++ self.name_translated = data.get('name_translated', '')
# string
++ self.officialList = data.get('officialList', '')
# string
++ self.overview = data.get('overview', '')
# string
++ self.overview_translated = _get_list(data, 'overview_translated')
++ self.posters = _get_list(data, 'posters')
++ self.primary_language = data.get('primary_language', '')
# string
++ self.status = data.get('status', '')
# string
++ self.translationsWithLang = _get_list(data, 'translationsWithLang')
++ self.tvdb_id = data.get('tvdb_id', '')
# string
++ self.type = data.get('type', '')
# string
++ self.year = data.get('year', '')
# string
++ self.thumbnail = data.get('thumbnail', '')
# string
++ self.poster = data.get('poster', '')
# string
++### XXX self.translations = _handle_list(TranslationSimple,
data.get('translations'))
++ self.translations = data.get('translations', {}) ###
XXX
++ self.is_official = data.get('is_official', False) #
boolean
++ self.remote_ids = _handle_list(RemoteID, data.get('remote_ids'))
++ self.network = data.get('network', '')
# string
++ self.title = data.get('title', '')
# string
++### XXX self.overviews = _handle_list(TranslationSimple, data.get('overviews'))
++ self.overviews = data.get('overviews', {}) ###
XXX
++ # additional attributes needed by the mythtv grabber script:
++ self.name_similarity = 0.0 ### XXX
++
++
++class SeasonBaseRecord(object):
++ """season genre record"""
++ def __init__(self, data):
++ self.id = data.get('id', 0) #
integer
++ self.image = data.get('image', '')
# string
++ self.imageType = data.get('imageType', 0) #
integer
++ self.name = data.get('name', '')
# string
++ self.nameTranslations = _get_list(data, 'nameTranslations')
++ self.number = data.get('number', 0) #
integer
++ self.overviewTranslations = _get_list(data, 'overviewTranslations')
++ self.companies = _handle_single(Companies, data.get('companies'))
++ self.seriesId = data.get('seriesId', 0) #
integer
++ self.type = _handle_single(SeasonType, data.get('type'))
++ # additional attributes needed by the mythtv grabber script:
++ self.fetched_translations = []
++ self.name_similarity = 0.0
++
++
++class SeasonExtendedRecord(object):
++ """extended season record"""
++ def __init__(self, data):
++ self.abbreviation = data.get('abbreviation', '')
# string
++ self.artwork = _handle_list(ArtworkBaseRecord, data.get('artwork'))
++ self.episodes = _handle_list(EpisodeBaseRecord, data.get('episodes'))
++ self.id = data.get('id', 0) #
integer
++ self.image = data.get('image', '')
# string
++ self.imageType = data.get('imageType', 0) #
integer
++ self.name = data.get('name', '')
# string
++ self.nameTranslations = _get_list(data, 'nameTranslations')
++ self.number = data.get('number', 0) #
integer
++ self.overviewTranslations = _get_list(data, 'overviewTranslations')
++ self.seriesId = data.get('seriesId', 0) #
integer
++ self.slug = data.get('slug', '')
# string
++ self.trailers = _handle_list(Trailer, data.get('trailers'))
++ self.type = data.get('type', 0) #
integer
++ self.companies = _handle_single(Companies, data.get('companies'))
++ self.tagOptions = _handle_list(TagOption, data.get('tagOptions'))
++ self.translations = _handle_list(Translation, data.get('translations'))
++ # additional attributes needed by the mythtv grabber script:
++ self.fetched_translations = []
++ self.name_similarity = 0.0
++
++
++class SeasonType(object):
++ """season type record"""
++ def __init__(self, data):
++ self.alternateName = data.get('alternateName', '')
# string
++ self.id = data.get('id', 0) #
integer
++ self.name = data.get('name', '')
# string
++ self.type = data.get('type', '')
# string
++
++
++class SeriesAirsDays(object):
++ """A series airs day record"""
++ def __init__(self, data):
++ self.friday = data.get('friday', False) #
boolean
++ self.monday = data.get('monday', False) #
boolean
++ self.saturday = data.get('saturday', False) #
boolean
++ self.sunday = data.get('sunday', False) #
boolean
++ self.thursday = data.get('thursday', False) #
boolean
++ self.tuesday = data.get('tuesday', False) #
boolean
++ self.wednesday = data.get('wednesday', False) #
boolean
++
++
++class SeriesBaseRecord(object):
++ """
++ The base record for a series. All series airs time like firstAired, lastAired,
nextAired, etc.
++ are in US EST for US series, and for all non-US series, the time of the show’s
++ country capital or most populous city. For streaming services, is the official
release time.
++ See
https://support.thetvdb.com/kb/faq.php?id=29.
++ """
++ def __init__(self, data):
++ self.abbreviation = data.get('abbreviation', '')
# string
++ self.aliases = _handle_list(Alias, data.get('aliases'))
++ self.country = data.get('country', '')
# string
++ self.defaultSeasonType = data.get('defaultSeasonType', 0) #
integer
++ self.firstAired = data.get('firstAired', '')
# string
++ self.id = data.get('id', 0) #
integer
++ self.image = data.get('image', '')
# string
++ self.isOrderRandomized = data.get('isOrderRandomized', False) #
boolean
++ self.lastAired = data.get('lastAired', '')
# string
++ self.name = data.get('name', '')
# string
++ self.nameTranslations = _get_list(data, 'nameTranslations')
++ self.nextAired = data.get('nextAired', '')
# string
++ self.originalCountry = data.get('originalCountry', '')
# string
++ self.originalLanguage = data.get('originalLanguage', '')
# string
++ self.overviewTranslations = _get_list(data, 'overviewTranslations')
++ self.score = data.get('score', 0.0) #
number
++ self.slug = data.get('slug', '')
# string
++ self.status = _handle_single(Status, data.get('status'))
++ self.lastUpdated = data.get('lastUpdated', '')
# string
++ self.averageRuntime = data.get('averageRuntime', 0) #
integer
++ self.episodes = _handle_list(EpisodeBaseRecord, data.get('episodes'))
++ # additional attributes needed by the mythtv grabber script:
++ self.fetched_translations = []
++ self.name_similarity = 0.0
++
++
++class SeriesExtendedRecord(object):
++ """
++ The extended record for a series. All series airs time like firstAired, lastAired,
nextAired, etc.
++ are in US EST for US series, and for all non-US series, the time of the show’s
country capital
++ or most populous city. For streaming services, is the official release time.
++ See
https://support.thetvdb.com/kb/faq.php?id=29.
++ """
++ def __init__(self, data):
++ self.abbreviation = data.get('abbreviation', '')
# string
++ self.airsDays = _handle_single(SeriesAirsDays, data.get('airsDays'))
++ self.airsTime = data.get('airsTime', '')
# string
++ self.aliases = _handle_list(Alias, data.get('aliases'))
++ self.artworks = _handle_list(ArtworkExtendedRecord,
data.get('artworks'))
++ self.characters = _handle_list(Character, data.get('characters'))
++ self.country = data.get('country', '')
# string
++ self.defaultSeasonType = data.get('defaultSeasonType', 0) #
integer
++ self.firstAired = data.get('firstAired', '')
# string
++ self.genres = _handle_list(GenreBaseRecord, data.get('genres'))
++ self.id = data.get('id', 0) #
integer
++ self.image = data.get('image', '')
# string
++ self.isOrderRandomized = data.get('isOrderRandomized', False) #
boolean
++ self.lastAired = data.get('lastAired', '')
# string
++ self.name = data.get('name', '')
# string
++ self.nameTranslations = _get_list(data, 'nameTranslations')
++ self.companies = _handle_list(Company, data.get('companies'))
++ self.nextAired = data.get('nextAired', '')
# string
++ self.originalCountry = data.get('originalCountry', '')
# string
++ self.originalLanguage = data.get('originalLanguage', '')
# string
++ self.originalNetwork = _handle_single(Company,
data.get('originalNetwork'))
++ self.latestNetwork = _handle_single(Company, data.get('latestNetwork'))
++ self.overviewTranslations = _get_list(data, 'overviewTranslations')
++ self.remoteIds = _handle_list(RemoteID, data.get('remoteIds'))
++ self.score = data.get('score', 0.0) #
number
++ self.seasons = _handle_list(SeasonBaseRecord, data.get('seasons'))
++ self.slug = data.get('slug', '')
# string
++ self.status = _handle_single(Status, data.get('status'))
++ self.tags = _handle_list(TagOption, data.get('tags'))
++ self.trailers = _handle_list(Trailer, data.get('trailers'))
++ self.translations = _handle_list(TranslationExtended,
data.get('translations'))
++ # additional attributes needed by the mythtv grabber script:
++ self.fetched_translations = []
++ self.name_similarity = 0.0
++
++
++class SourceType(object):
++ """source type record"""
++ def __init__(self, data):
++ self.id = data.get('id', 0) #
integer
++ self.name = data.get('name', '')
# string
++ self.postfix = data.get('postfix', '')
# string
++ self.prefix = data.get('prefix', '')
# string
++ self.slug = data.get('slug', '')
# string
++ self.sort = data.get('sort', 0) #
integer
++
++
++class Status(object):
++ """status record"""
++ def __init__(self, data):
++ self.id = data.get('id', 0) #
integer
++ self.keepUpdated = data.get('keepUpdated', False) #
boolean
++ self.name = data.get('name', '')
# string
++ self.recordType = data.get('recordType', '')
# string
++
++
++class StudioBaseRecord(object):
++ """studio record"""
++ def __init__(self, data):
++ self.id = data.get('id', 0) #
integer
++ self.name = data.get('name', '')
# string
++ self.parentStudio = data.get('parentStudio', 0) #
integer
++
++
++class Tag(object):
++ """tag record"""
++ def __init__(self, data):
++ self.allowsMultiple = data.get('allowsMultiple', False) #
boolean
++ self.helpText = data.get('helpText', '')
# string
++ self.id = data.get('id', 0) #
integer
++ self.name = data.get('name', '')
# string
++ self.options = _handle_list(TagOption, data.get('options'))
++
++
++class TagOption(object):
++ """tag option record"""
++ def __init__(self, data):
++ self.helpText = data.get('helpText', '')
# string
++ self.id = data.get('id', 0) #
integer
++ self.name = data.get('name', '')
# string
++ self.tag = data.get('tag', 0) #
integer
++ self.tagName = data.get('tagName', '')
# string
++
++
++class Trailer(object):
++ """trailer record"""
++ def __init__(self, data):
++ self.id = data.get('id', 0) #
integer
++ self.language = data.get('language', '')
# string
++ self.name = data.get('name', '')
# string
++ self.url = data.get('url', '')
# string
++
++
++class Translation(object):
++ """translation record"""
++ def __init__(self, data):
++ self.aliases = _get_list(data, 'aliases')
++ self.isAlias = data.get('isAlias', False) #
boolean
++ self.isPrimary = data.get('isPrimary', False) #
boolean
++ self.language = data.get('language', '')
# string
++ self.name = data.get('name', '')
# string
++ self.overview = data.get('overview', '')
# string
++ self.tagline = data.get('tagline', '')
# string
++
++
++class TranslationSimple(object):
++ """translation simple record"""
++ def __init__(self, data):
++ self.language = data.get('language', '')
# string
++
++
++class TranslationExtended(object):
++ """translation extended record"""
++ def __init__(self, data):
++ self.nameTranslations = _handle_list(Translation,
data.get('nameTranslations'))
++ self.overviewTranslations = _handle_list(Translation,
data.get('overviewTranslations'))
++ self.alias = _get_list(data, 'alias')
++
++
++class TagOptionEntity(object):
++ """a entity with selected tag option"""
++ def __init__(self, data):
++ self.name = data.get('name', '')
# string
++ self.tagName = data.get('tagName', '')
# string
++ self.tagId = data.get('tagId', 0) #
integer
++
++
++class Inspiration(object):
++ """Movie inspiration record"""
++ def __init__(self, data):
++ self.id = data.get('id', 0) #
integer
++ self.type = data.get('type', '')
# string
++ self.typeName = data.get('typeName', '')
# string
++ self.url = data.get('url', '')
# string
++
++
++class InspirationType(object):
++ """Movie inspiration type record"""
++ def __init__(self, data):
++ self.id = data.get('id', 0) #
integer
++ self.name = data.get('name', '')
# string
++ self.description = data.get('description', '')
# string
++ self.reference_name = data.get('reference_name', '')
# string
++ self.url = data.get('url', '')
# string
++
++
++class ProductionCountry(object):
++ """Production country record"""
++ def __init__(self, data):
++ self.id = data.get('id', 0) #
integer
++ self.country = data.get('country', '')
# string
++ self.name = data.get('name', '')
# string
++
++
++class Companies(object):
++ """Companies by type record"""
++ def __init__(self, data):
++ self.studio = _handle_single(Company, data.get('studio'))
++ self.network = _handle_single(Company, data.get('network'))
++ self.production = _handle_single(Company, data.get('production'))
++ self.distributor = _handle_single(Company, data.get('distributor'))
++ self.specialEffects = _handle_single(Company,
data.get('specialEffects'))
++
++
++class Links(object):
++ """Links for next, previous and current record"""
++ def __init__(self, data):
++ self.prev = data.get('prev', '')
# string
++ self.self = data.get('self', '')
# string
++ self.next = data.get('next', '')
# string
++ self.total_items = data.get('total_items', 0) #
integer
++ self.page_size = data.get('page_size', 0) #
integer
+diff --git a/mythtv/bindings/python/ttvdbv4/get_api_v4.py
b/mythtv/bindings/python/ttvdbv4/get_api_v4.py
+new file mode 100644
+index 00000000000..2aefd3fb3d5
+--- /dev/null
++++ b/mythtv/bindings/python/ttvdbv4/get_api_v4.py
+@@ -0,0 +1,286 @@
++# -*- coding: UTF-8 -*-
++
++# ----------------------------------------------------
++# Purpose: MythTV Python Bindings for TheTVDB v4 API
++# Copyright: (c) 2021 Roland Ernst
++# License: GPL v2 or later, see LICENSE for details
++# ----------------------------------------------------
++
++"""
++Parse openapi specification for ttvdb v4.
++See
https://github.com/thetvdb/v4-api/blob/main/docs/swagger.yml
++Create definitions and api for the files
++ - definitions.py
++ - ttvdbv4_api.py
++"""
++
++import sys
++import os
++import yaml
++import re
++
++
++# default strings for api functions
++func_str = \
++ "def {fname}({fparams}):\n"
++param_str = \
++ " params = {}\n"
++param_item_str = \
++ " if %s is not None:\n\
++ params['%s'] = %s\n"
++query_str = \
++ " res = _query_api(%s)\n"
++query_str_params = \
++ " res = _query_api(%s, params)\n"
++query_yielded_str = \
++ " return _query_yielded(%s, %s, params, %s)\n"
++res_str = \
++ " data = res['data'] if res.get('data') is not None else
None\n"
++array_str = \
++ "[%s(x) for x in %s] if data is not None else []"
++item_str = \
++ "%s(%s) if data is not None else None"
++
++
++# default values for basic types
++defaults = {'string': "''",
++ 'integer': 0,
++ 'number': 0.0,
++ 'boolean': False
++ }
++
++
++# global
++pathlist = []
++
++
++def read_yaml_from_file(api_spec):
++ with open(api_spec) as f:
++ data = yaml.safe_load(f)
++ return data
++
++
++def print_api(title, version, pathlist, api_calls):
++ print('"""Generated API for
thetvdb.com {} v
{}"""'.format(title, version))
++ print("\n")
++ for i in pathlist:
++ print(i)
++ print("\n\n")
++ print(api_calls)
++
++
++def print_api_gets(pathname, pathdata):
++ global pathlist
++ paged = False
++ pstring = ""
++ # get the function definition for 'operationId'
++ if pathdata.get('get') is not None:
++ operationId = pathdata['get']['operationId']
++ plist = [] # for the 'path' string
++ pitems = [] # for the function parameter string
++ params = {} # for the query parameters
++ if pathdata['get'].get('parameters') is not None:
++ # required params are member of the path
++ # optional params are attached as 'params' dict
++ for param in pathdata['get']['parameters']:
++ # search for paged requests
++ pkey = param.get('name').replace('-', '_')
++ pvalue = pkey
++ required = param.get('required', False)
++ if pkey == 'page':
++ paged = True
++ continue
++ if pkey == 'id':
++ pvalue = 'str(id)'
++ if required:
++ plist.append("%s=%s" % (pkey, pvalue))
++ pitems.append(pkey)
++ else:
++ params[pkey] = pvalue
++ pitems.append("%s=None" % pvalue)
++ if paged:
++ params['page'] = 'page'
++ pitems.append("%s=%s" % ('page', 0))
++ pitems.append("%s=%s" % ('yielded', False))
++ pstring += func_str.format(fname=operationId,
++ fparams=", ".join(pitems))
++
++ # define the parameter
++ if params:
++ pstring += param_str
++ for k, v in params.items():
++ pstring += param_item_str % (k, k, v)
++
++ # define the url
++ path = "%s_path.format(%s)" % (operationId, ",
".join(plist))
++ pstring += " path = %s\n" % path
++
++ # evaluate response properties['data']:
++ content =
pathdata['get']['responses']['200']['content']
++ data =
content['application/json']['schema']['properties']['data']
++
++ # look for references ('$ref') starting with
'#/components/schemas/'
++ ref_str = '#/components/schemas/'
++ tmplst = []
++ tmpref = ""
++ listname = "listname=None"
++ if data.get('type') is not None:
++ if data.get('items'):
++ ref = data['items']['$ref']
++ if ref.startswith(ref_str):
++ if data.get('type') == 'array':
++ tmpref = ref.split('/')[-1]
++ tmplst.append("%s" % (array_str % (tmpref,
'data')))
++ elif data.get('properties'):
++ for prop in data['properties'].keys():
++ if data['properties'][prop].get('$ref') is not
None:
++ ref = data['properties'][prop]['$ref']
++ if ref.startswith(ref_str):
++ tmpref = ref.split('/')[-1]
++ tmplst.append(item_str % (tmpref,
"data['%s']" % prop))
++ elif data['properties'][prop].get('items') is not
None:
++ if data['properties'][prop].get('type') ==
'array':
++ ref =
data['properties'][prop]['items']['$ref']
++ if ref.startswith(ref_str):
++ listname = "listname='%s'" % prop
++ tmpref = ref.split('/')[-1]
++ tmplst.append(array_str % (tmpref,
"data['%s']" % prop))
++
++ elif data.get('$ref') is not None:
++ if data['$ref'].startswith(ref_str):
++ pref = data['$ref'].split('/')[-1]
++ tmplst.append("%s" % (item_str % (pref, 'data')))
++
++ # format output
++ add_ident = ""
++ if params:
++ if paged:
++ add_ident = " "
++ pstring += add_ident + "if yielded:\n"
++ pstring += add_ident + query_yielded_str % (tmpref, "path",
listname)
++ pstring += add_ident + "else:\n"
++
++ pstring += add_ident + query_str_params % ("path")
++ pstring += add_ident + res_str
++ else:
++ pstring += query_str % ("path")
++ pstring += res_str
++
++ pstring += \
++ ' %sreturn( %s )' % (add_ident,
++ (",\n " +
add_ident).join(tmplst))
++
++ # update pathlist as well
++ # replace ('-', '_') in parameters identified within
'{}'
++ pattern = re.compile(r'{[a-z]+-[a-z]+}')
++ s = '%s_path = TTVDBV4_path + "%s"' % (operationId, pathname)
++ for match in pattern.findall(s):
++ s = s.replace(match, match.replace('-', '_'))
++ pathlist.append(s)
++
++ return pstring
++
++
++def print_definition(name, defitem, setdefaults=False):
++
++ # string definitions
++ defstr = '#/components/schemas' # openapi 3.0
++ classstr = \
++ 'class %s(object):\n' % name + \
++ ' """%s"""\n' %
defitem['description'] + \
++ ' def __init__(self, data):\n'
++ handle_list_str = \
++ " self.%s = _handle_list(%s, data.get('%s'))\n"
++ get_list_str = \
++ " self.%s = _get_list(data, '%s')\n"
++ handle_single_str = \
++ " self.%s = _handle_single(%s, data.get('%s'))\n"
++ translations_str = \
++ " self.fetched_translations = []\n"
++ similarity_str = \
++ " self.name_similarity = 0.0\n"
++ added_str = \
++ " # additional attributes needed by the mythtv grabber
script:\n"
++
++ if defitem.get('properties') is None:
++ classstr += " pass\n"
++ else:
++ needs_translations = False
++ for i in defitem['properties'].keys():
++ if ('items') in defitem['properties'][i]:
++ # handle arrays and lists of basic types
++ if ('type') in defitem['properties'][i]:
++ if defitem['properties'][i]['type'] ==
'array':
++ if ('$ref') in
defitem['properties'][i]['items']:
++ ref =
defitem['properties'][i]['items']['$ref']
++ atype = ref.split("/")[-1]
++ classstr += handle_list_str % (i, atype, i)
++ else:
++ if i == 'nameTranslations':
++ needs_translations = True
++ classstr += get_list_str % (i, i)
++
++ elif ('$ref') in defitem['properties'][i]:
++ # handle special types
++ v = defitem['properties'][i]['$ref']
++ stype = v.split("/")[-1]
++ classstr += handle_single_str % (i, stype, i)
++
++ elif ('type') in defitem['properties'][i]:
++ # handle basic types
++ stype = defitem['properties'][i]['type']
++ if setdefaults:
++ d = defaults.get(stype)
++ s = " self.%s = data.get('%s', %s)" % (i,
i, d)
++ else:
++ s = " self.%s = data.get('%s')" % (i, i)
++ alignment = 80 - len(s) + len(stype)
++ classstr += s + ("# %s\n" % stype).rjust(alignment)
++ if needs_translations:
++ # below are additions needed by the mythtv grabber script
++ classstr += added_str
++ classstr += translations_str
++ classstr += similarity_str
++
++ return(classstr)
++
++
++if __name__ == '__main__':
++ """
++ Download the latest api specification from the TheTVDB official repo:
++
https://github.com/thetvdb/v4-api/blob/main/docs/swagger.yml
++ """
++ api_spec = sys.argv[1]
++ if not os.path.isfile(api_spec):
++ print("Error: main() needs to be called with an OAS3 spec file
(yaml)")
++ sys.exit(1)
++ apidata = read_yaml_from_file(api_spec)
++ api_title = apidata['info']['title']
++ api_version = apidata['info']['version']
++ apiv4_basepath = apidata['servers'][0]['url']
++
++ pathlist.append('TTVDBV4_path = "{}"'.format(apiv4_basepath))
++
++ # get api functions
++ api_calls = ""
++ for k in apidata['paths'].keys():
++ if apidata['paths'][k].get('get'):
++ api_calls += print_api_gets(k, apidata['paths'][k])
++ api_calls += "\n\n\n"
++
++ print_api(api_title, api_version, pathlist, api_calls)
++
++ # get api definitions
++ api_defs = ""
++ api_defs += ('"""Generated API for
thetvdb.com {} v
{}"""'
++ .format(api_title, api_version))
++ api_defs += "\n\n"
++ schemas = apidata['components']['schemas'] # openapi 3.0
++ for k in schemas.keys():
++ api_defs += "\n"
++ api_defs += print_definition(k, schemas[k], setdefaults=True)
++ api_defs += "\n"
++
++ print(api_defs)
++
++
+diff --git a/mythtv/bindings/python/ttvdbv4/locales.py
b/mythtv/bindings/python/ttvdbv4/locales.py
+new file mode 100644
+index 00000000000..1e51f37c67c
+--- /dev/null
++++ b/mythtv/bindings/python/ttvdbv4/locales.py
+@@ -0,0 +1,586 @@
++# -*- coding: utf-8 -*-
++
++#-----------------------
++# Name: locales.py Stores locale information for filtering results
++# Python Library
++# Author: Raymond Wagner
++#-----------------------
++# Author: Roland Ernst
++# Modifications:
++# - Added ISO931-2T/2B
++# - Added ISO-3166 alpha-3
++# - Removed unused code
++#----------------------
++
++import locale
++
++
++class LocaleBase(object):
++ __slots__ = ['__immutable']
++ _stored = {}
++ fallthrough = False
++
++ def __init__(self, *keys):
++ for key in keys:
++ self._stored[key.lower()] = self
++ self.__immutable = True
++
++ def __setattr__(self, key, value):
++ if getattr(self, '__immutable', False):
++ raise NotImplementedError(self.__class__.__name__ +
++ ' does not support modification.')
++ super(LocaleBase, self).__setattr__(key, value)
++
++ def __delattr__(self, key):
++ if getattr(self, '__immutable', False):
++ raise NotImplementedError(self.__class__.__name__ +
++ ' does not support modification.')
++ super(LocaleBase, self).__delattr__(key)
++
++ @classmethod
++ def getstored(cls, key):
++ if key is None:
++ return None
++ try:
++ return cls._stored[key.lower()]
++ except:
++ raise Exception("'{0}' is not a known valid {1} code."\
++ .format(key, cls.__name__))
++
++
++class Language(LocaleBase):
++ __slots__ = ['ISO639_1', 'ISO639_2T', 'ISO639_2B',
'englishname',
++ 'nativename']
++ _stored = {}
++
++ def __init__(self, iso1, iso2b, iso2t, ename):
++ self.ISO639_1 = iso1
++ self.ISO639_2B = iso2b
++ self.ISO639_2T = iso2t
++ self.englishname = ename
++# self.nativename = nname
++ super(Language, self).__init__(iso1, iso2b, iso2t, ename)
++
++ def __str__(self):
++ return self.ISO639_1
++
++ def __repr__(self):
++ return u"<Language '{0.englishname}' ({0.ISO639_1})
({0.ISO639_2T}) ({0.ISO639_2B})>".format(self)
++
++
++class Country(LocaleBase):
++ __slots__ = ['alpha2', 'alpha3', 'name']
++ _stored = {}
++
++ def __init__(self, alpha2, alpha3, name):
++ self.alpha2 = alpha2
++ self.alpha3 = alpha3
++ self.name = name
++ super(Country, self).__init__(alpha2, alpha3, name)
++
++ def __str__(self):
++ return self.alpha2
++
++ def __repr__(self):
++ return u"<Country '{0.name}' ({0.alpha2})
({0.alpha3})>".format(self)
++
++
++# ISO-639-1, ISO-639-2/B, ISO-639-2/T, English Name
++Language("ab", "abk", "abk", u"Abkhazian")
++Language("aa", "aar", "aar", u"Afar")
++Language("af", "afr", "afr", u"Afrikaans")
++Language("ak", "aka", "aka", u"Akan")
++Language("sq", "alb", "sqi", u"Albanian")
++Language("am", "amh", "amh", u"Amharic")
++Language("ar", "ara", "ara", u"Arabic")
++Language("an", "arg", "arg", u"Aragonese")
++Language("hy", "arm", "hye", u"Armenian")
++Language("as", "asm", "asm", u"Assamese")
++Language("av", "ava", "ava", u"Avaric")
++Language("ae", "ave", "ave", u"Avestan")
++Language("ay", "aym", "aym", u"Aymara")
++Language("az", "aze", "aze", u"Azerbaijani")
++Language("bm", "bam", "bam", u"Bambara")
++Language("ba", "bak", "bak", u"Bashkir")
++Language("eu", "baq", "eus", u"Basque")
++Language("be", "bel", "bel", u"Belarusian")
++Language("bn", "ben", "ben", u"Bengali")
++Language("bh", "bih", "bih", u"Bihari
languages")
++Language("bi", "bis", "bis", u"Bislama")
++Language("nb", "nob", "nob", u"Bokmål,
Norwegian")
++Language("bs", "bos", "bos", u"Bosnian")
++Language("br", "bre", "bre", u"Breton")
++Language("bg", "bul", "bul", u"Bulgarian")
++Language("my", "bur", "mya", u"Burmese")
++Language("es", "spa", "spa", u"Castilian")
++Language("ca", "cat", "cat", u"Catalan")
++Language("km", "khm", "khm", u"Central Khmer")
++Language("ch", "cha", "cha", u"Chamorro")
++Language("ce", "che", "che", u"Chechen")
++Language("ny", "nya", "nya", u"Chewa")
++Language("ny", "nya", "nya", u"Chichewa")
++Language("zh", "chi", "zho", u"Chinese")
++Language("za", "zha", "zha", u"Chuang")
++Language("cu", "chu", "chu", u"Church Slavic")
++Language("cu", "chu", "chu", u"Church
Slavonic")
++Language("cv", "chv", "chv", u"Chuvash")
++Language("kw", "cor", "cor", u"Cornish")
++Language("co", "cos", "cos", u"Corsican")
++Language("cr", "cre", "cre", u"Cree")
++Language("hr", "hrv", "hrv", u"Croatian")
++Language("cs", "cze", "ces", u"Czech")
++Language("da", "dan", "dan", u"Danish")
++Language("dv", "div", "div", u"Dhivehi")
++Language("dv", "div", "div", u"Divehi")
++Language("nl", "dut", "nld", u"Dutch")
++Language("dz", "dzo", "dzo", u"Dzongkha")
++Language("en", "eng", "eng", u"English")
++Language("eo", "epo", "epo", u"Esperanto")
++Language("et", "est", "est", u"Estonian")
++Language("ee", "ewe", "ewe", u"Ewe")
++Language("fo", "fao", "fao", u"Faroese")
++Language("fj", "fij", "fij", u"Fijian")
++Language("fi", "fin", "fin", u"Finnish")
++Language("nl", "dut", "nld", u"Flemish")
++Language("fr", "fre", "fra", u"French")
++Language("ff", "ful", "ful", u"Fulah")
++Language("gd", "gla", "gla", u"Gaelic")
++Language("gl", "glg", "glg", u"Galician")
++Language("lg", "lug", "lug", u"Ganda")
++Language("ka", "geo", "kat", u"Georgian")
++Language("de", "ger", "deu", u"German")
++Language("ki", "kik", "kik", u"Gikuyu")
++Language("el", "gre", "ell", u"Greek, Modern
(1453-)")
++Language("kl", "kal", "kal", u"Greenlandic")
++Language("gn", "grn", "grn", u"Guarani")
++Language("gu", "guj", "guj", u"Gujarati")
++Language("ht", "hat", "hat", u"Haitian")
++Language("ht", "hat", "hat", u"Haitian Creole")
++Language("ha", "hau", "hau", u"Hausa")
++Language("he", "heb", "heb", u"Hebrew")
++Language("hz", "her", "her", u"Herero")
++Language("hi", "hin", "hin", u"Hindi")
++Language("ho", "hmo", "hmo", u"Hiri Motu")
++Language("hu", "hun", "hun", u"Hungarian")
++Language("is", "ice", "isl", u"Icelandic")
++Language("io", "ido", "ido", u"Ido")
++Language("ig", "ibo", "ibo", u"Igbo")
++Language("id", "ind", "ind", u"Indonesian")
++Language("ia", "ina", "ina", u"Interlingua
(International Auxiliary Language Association)")
++Language("ie", "ile", "ile", u"Interlingue")
++Language("iu", "iku", "iku", u"Inuktitut")
++Language("ik", "ipk", "ipk", u"Inupiaq")
++Language("ga", "gle", "gle", u"Irish")
++Language("it", "ita", "ita", u"Italian")
++Language("ja", "jpn", "jpn", u"Japanese")
++Language("jv", "jav", "jav", u"Javanese")
++Language("kl", "kal", "kal", u"Kalaallisut")
++Language("kn", "kan", "kan", u"Kannada")
++Language("kr", "kau", "kau", u"Kanuri")
++Language("ks", "kas", "kas", u"Kashmiri")
++Language("kk", "kaz", "kaz", u"Kazakh")
++Language("ki", "kik", "kik", u"Kikuyu")
++Language("rw", "kin", "kin", u"Kinyarwanda")
++Language("ky", "kir", "kir", u"Kirghiz")
++Language("kv", "kom", "kom", u"Komi")
++Language("kg", "kon", "kon", u"Kongo")
++Language("ko", "kor", "kor", u"Korean")
++Language("kj", "kua", "kua", u"Kuanyama")
++Language("ku", "kur", "kur", u"Kurdish")
++Language("kj", "kua", "kua", u"Kwanyama")
++Language("ky", "kir", "kir", u"Kyrgyz")
++Language("lo", "lao", "lao", u"Lao")
++Language("la", "lat", "lat", u"Latin")
++Language("lv", "lav", "lav", u"Latvian")
++Language("lb", "ltz", "ltz", u"Letzeburgesch")
++Language("li", "lim", "lim", u"Limburgan")
++Language("li", "lim", "lim", u"Limburger")
++Language("li", "lim", "lim", u"Limburgish")
++Language("ln", "lin", "lin", u"Lingala")
++Language("lt", "lit", "lit", u"Lithuanian")
++Language("lu", "lub", "lub", u"Luba-Katanga")
++Language("lb", "ltz", "ltz", u"Luxembourgish")
++Language("mk", "mac", "mkd", u"Macedonian")
++Language("mg", "mlg", "mlg", u"Malagasy")
++Language("ms", "may", "msa", u"Malay")
++Language("ml", "mal", "mal", u"Malayalam")
++Language("dv", "div", "div", u"Maldivian")
++Language("mt", "mlt", "mlt", u"Maltese")
++Language("gv", "glv", "glv", u"Manx")
++Language("mi", "mao", "mri", u"Maori")
++Language("mr", "mar", "mar", u"Marathi")
++Language("mh", "mah", "mah", u"Marshallese")
++Language("ro", "rum", "ron", u"Moldavian")
++Language("ro", "rum", "ron", u"Moldovan")
++Language("mn", "mon", "mon", u"Mongolian")
++Language("na", "nau", "nau", u"Nauru")
++Language("nv", "nav", "nav", u"Navaho")
++Language("nv", "nav", "nav", u"Navajo")
++Language("nd", "nde", "nde", u"Ndebele, North")
++Language("nr", "nbl", "nbl", u"Ndebele, South")
++Language("ng", "ndo", "ndo", u"Ndonga")
++Language("ne", "nep", "nep", u"Nepali")
++Language("nd", "nde", "nde", u"North Ndebele")
++Language("se", "sme", "sme", u"Northern Sami")
++Language("no", "nor", "nor", u"Norwegian")
++Language("nb", "nob", "nob", u"Norwegian
Bokmål")
++Language("nn", "nno", "nno", u"Norwegian
Nynorsk")
++Language("ii", "iii", "iii", u"Nuosu")
++Language("ny", "nya", "nya", u"Nyanja")
++Language("nn", "nno", "nno", u"Nynorsk,
Norwegian")
++Language("ie", "ile", "ile", u"Occidental")
++Language("oc", "oci", "oci", u"Occitan (post
1500)")
++Language("oj", "oji", "oji", u"Ojibwa")
++Language("cu", "chu", "chu", u"Old Bulgarian")
++Language("cu", "chu", "chu", u"Old Church
Slavonic")
++Language("cu", "chu", "chu", u"Old Slavonic")
++Language("or", "ori", "ori", u"Oriya")
++Language("om", "orm", "orm", u"Oromo")
++Language("os", "oss", "oss", u"Ossetian")
++Language("os", "oss", "oss", u"Ossetic")
++Language("pi", "pli", "pli", u"Pali")
++Language("pa", "pan", "pan", u"Panjabi")
++Language("ps", "pus", "pus", u"Pashto")
++Language("fa", "per", "fas", u"Persian")
++Language("pl", "pol", "pol", u"Polish")
++Language("pt", "por", "por", u"Portuguese")
++Language("pa", "pan", "pan", u"Punjabi")
++Language("ps", "pus", "pus", u"Pushto")
++Language("qu", "que", "que", u"Quechua")
++Language("ro", "rum", "ron", u"Romanian")
++Language("rm", "roh", "roh", u"Romansh")
++Language("rn", "run", "run", u"Rundi")
++Language("ru", "rus", "rus", u"Russian")
++Language("sm", "smo", "smo", u"Samoan")
++Language("sg", "sag", "sag", u"Sango")
++Language("sa", "san", "san", u"Sanskrit")
++Language("sc", "srd", "srd", u"Sardinian")
++Language("gd", "gla", "gla", u"Scottish
Gaelic")
++Language("sr", "srp", "srp", u"Serbian")
++Language("sn", "sna", "sna", u"Shona")
++Language("ii", "iii", "iii", u"Sichuan Yi")
++Language("sd", "snd", "snd", u"Sindhi")
++Language("si", "sin", "sin", u"Sinhala")
++Language("si", "sin", "sin", u"Sinhalese")
++Language("sk", "slo", "slk", u"Slovak")
++Language("sl", "slv", "slv", u"Slovenian")
++Language("so", "som", "som", u"Somali")
++Language("st", "sot", "sot", u"Sotho,
Southern")
++Language("nr", "nbl", "nbl", u"South Ndebele")
++Language("es", "spa", "spa", u"Spanish")
++Language("su", "sun", "sun", u"Sundanese")
++Language("sw", "swa", "swa", u"Swahili")
++Language("ss", "ssw", "ssw", u"Swati")
++Language("sv", "swe", "swe", u"Swedish")
++Language("tl", "tgl", "tgl", u"Tagalog")
++Language("ty", "tah", "tah", u"Tahitian")
++Language("tg", "tgk", "tgk", u"Tajik")
++Language("ta", "tam", "tam", u"Tamil")
++Language("tt", "tat", "tat", u"Tatar")
++Language("te", "tel", "tel", u"Telugu")
++Language("th", "tha", "tha", u"Thai")
++Language("bo", "tib", "bod", u"Tibetan")
++Language("ti", "tir", "tir", u"Tigrinya")
++Language("to", "ton", "ton", u"Tonga (Tonga
Islands)")
++Language("ts", "tso", "tso", u"Tsonga")
++Language("tn", "tsn", "tsn", u"Tswana")
++Language("tr", "tur", "tur", u"Turkish")
++Language("tk", "tuk", "tuk", u"Turkmen")
++Language("tw", "twi", "twi", u"Twi")
++Language("ug", "uig", "uig", u"Uighur")
++Language("uk", "ukr", "ukr", u"Ukrainian")
++Language("ur", "urd", "urd", u"Urdu")
++Language("ug", "uig", "uig", u"Uyghur")
++Language("uz", "uzb", "uzb", u"Uzbek")
++Language("ca", "cat", "cat", u"Valencian")
++Language("ve", "ven", "ven", u"Venda")
++Language("vi", "vie", "vie", u"Vietnamese")
++Language("vo", "vol", "vol", u"Volapük")
++Language("wa", "wln", "wln", u"Walloon")
++Language("cy", "wel", "cym", u"Welsh")
++Language("fy", "fry", "fry", u"Western
Frisian")
++Language("wo", "wol", "wol", u"Wolof")
++Language("xh", "xho", "xho", u"Xhosa")
++Language("yi", "yid", "yid", u"Yiddish")
++Language("yo", "yor", "yor", u"Yoruba")
++Language("za", "zha", "zha", u"Zhuang")
++Language("zu", "zul", "zul", u"Zulu")
++
++
++
++# ISO-3166 alpha-2, alpha-3, English Name
++Country("AF", "AFG", u"Afghanistan")
++Country("AL", "ALB", u"Albania")
++Country("DZ", "DZA", u"Algeria")
++Country("AS", "ASM", u"American Samoa")
++Country("AD", "AND", u"Andorra")
++Country("AO", "AGO", u"Angola")
++Country("AI", "AIA", u"Anguilla")
++Country("AQ", "ATA", u"Antarctica")
++Country("AG", "ATG", u"Antigua and Barbuda")
++Country("AR", "ARG", u"Argentina")
++Country("AM", "ARM", u"Armenia")
++Country("AW", "ABW", u"Aruba")
++Country("AU", "AUS", u"Australia")
++Country("AT", "AUT", u"Austria")
++Country("AZ", "AZE", u"Azerbaijan")
++Country("BS", "BHS", u"Bahamas (the)")
++Country("BH", "BHR", u"Bahrain")
++Country("BD", "BGD", u"Bangladesh")
++Country("BB", "BRB", u"Barbados")
++Country("BY", "BLR", u"Belarus")
++Country("BE", "BEL", u"Belgium")
++Country("BZ", "BLZ", u"Belize")
++Country("BJ", "BEN", u"Benin")
++Country("BM", "BMU", u"Bermuda")
++Country("BT", "BTN", u"Bhutan")
++Country("BO", "BOL", u"Bolivia (Plurinational State of)")
++Country("BQ", "BES", u"Bonaire, Sint Eustatius and Saba")
++Country("BA", "BIH", u"Bosnia and Herzegovina")
++Country("BW", "BWA", u"Botswana")
++Country("BV", "BVT", u"Bouvet Island")
++Country("BR", "BRA", u"Brazil")
++Country("IO", "IOT", u"British Indian Ocean Territory
(the)")
++Country("BN", "BRN", u"Brunei Darussalam")
++Country("BG", "BGR", u"Bulgaria")
++Country("BF", "BFA", u"Burkina Faso")
++Country("BI", "BDI", u"Burundi")
++Country("CV", "CPV", u"Cabo Verde")
++Country("KH", "KHM", u"Cambodia")
++Country("CM", "CMR", u"Cameroon")
++Country("CA", "CAN", u"Canada")
++Country("KY", "CYM", u"Cayman Islands (the)")
++Country("CF", "CAF", u"Central African Republic (the)")
++Country("TD", "TCD", u"Chad")
++Country("CL", "CHL", u"Chile")
++Country("CN", "CHN", u"China")
++Country("CX", "CXR", u"Christmas Island")
++Country("CC", "CCK", u"Cocos (Keeling) Islands (the)")
++Country("CO", "COL", u"Colombia")
++Country("KM", "COM", u"Comoros (the)")
++Country("CD", "COD", u"Congo (the Democratic Republic of
the)")
++Country("CG", "COG", u"Congo (the)")
++Country("CK", "COK", u"Cook Islands (the)")
++Country("CR", "CRI", u"Costa Rica")
++Country("HR", "HRV", u"Croatia")
++Country("CU", "CUB", u"Cuba")
++Country("CW", "CUW", u"Curaçao")
++Country("CY", "CYP", u"Cyprus")
++Country("CZ", "CZE", u"Czechia")
++Country("CI", "CIV", u"Côte d'Ivoire")
++Country("DK", "DNK", u"Denmark")
++Country("DJ", "DJI", u"Djibouti")
++Country("DM", "DMA", u"Dominica")
++Country("DO", "DOM", u"Dominican Republic (the)")
++Country("EC", "ECU", u"Ecuador")
++Country("EG", "EGY", u"Egypt")
++Country("SV", "SLV", u"El Salvador")
++Country("GQ", "GNQ", u"Equatorial Guinea")
++Country("ER", "ERI", u"Eritrea")
++Country("EE", "EST", u"Estonia")
++Country("SZ", "SWZ", u"Eswatini")
++Country("ET", "ETH", u"Ethiopia")
++Country("FK", "FLK", u"Falkland Islands (the)
[Malvinas]")
++Country("FO", "FRO", u"Faroe Islands (the)")
++Country("FJ", "FJI", u"Fiji")
++Country("FI", "FIN", u"Finland")
++Country("FR", "FRA", u"France")
++Country("GF", "GUF", u"French Guiana")
++Country("PF", "PYF", u"French Polynesia")
++Country("TF", "ATF", u"French Southern Territories
(the)")
++Country("GA", "GAB", u"Gabon")
++Country("GM", "GMB", u"Gambia (the)")
++Country("GE", "GEO", u"Georgia")
++Country("DE", "DEU", u"Germany")
++Country("GH", "GHA", u"Ghana")
++Country("GI", "GIB", u"Gibraltar")
++Country("GR", "GRC", u"Greece")
++Country("GL", "GRL", u"Greenland")
++Country("GD", "GRD", u"Grenada")
++Country("GP", "GLP", u"Guadeloupe")
++Country("GU", "GUM", u"Guam")
++Country("GT", "GTM", u"Guatemala")
++Country("GG", "GGY", u"Guernsey")
++Country("GN", "GIN", u"Guinea")
++Country("GW", "GNB", u"Guinea-Bissau")
++Country("GY", "GUY", u"Guyana")
++Country("HT", "HTI", u"Haiti")
++Country("HM", "HMD", u"Heard Island and McDonald
Islands")
++Country("VA", "VAT", u"Holy See (the)")
++Country("HN", "HND", u"Honduras")
++Country("HK", "HKG", u"Hong Kong")
++Country("HU", "HUN", u"Hungary")
++Country("IS", "ISL", u"Iceland")
++Country("IN", "IND", u"India")
++Country("ID", "IDN", u"Indonesia")
++Country("IR", "IRN", u"Iran (Islamic Republic of)")
++Country("IQ", "IRQ", u"Iraq")
++Country("IE", "IRL", u"Ireland")
++Country("IM", "IMN", u"Isle of Man")
++Country("IL", "ISR", u"Israel")
++Country("IT", "ITA", u"Italy")
++Country("JM", "JAM", u"Jamaica")
++Country("JP", "JPN", u"Japan")
++Country("JE", "JEY", u"Jersey")
++Country("JO", "JOR", u"Jordan")
++Country("KZ", "KAZ", u"Kazakhstan")
++Country("KE", "KEN", u"Kenya")
++Country("KI", "KIR", u"Kiribati")
++Country("KP", "PRK", u"Korea (the Democratic People's
Republic of)")
++Country("KR", "KOR", u"Korea (the Republic of)")
++Country("KW", "KWT", u"Kuwait")
++Country("KG", "KGZ", u"Kyrgyzstan")
++Country("LA", "LAO", u"Lao People's Democratic Republic
(the)")
++Country("LV", "LVA", u"Latvia")
++Country("LB", "LBN", u"Lebanon")
++Country("LS", "LSO", u"Lesotho")
++Country("LR", "LBR", u"Liberia")
++Country("LY", "LBY", u"Libya")
++Country("LI", "LIE", u"Liechtenstein")
++Country("LT", "LTU", u"Lithuania")
++Country("LU", "LUX", u"Luxembourg")
++Country("MO", "MAC", u"Macao")
++Country("MG", "MDG", u"Madagascar")
++Country("MW", "MWI", u"Malawi")
++Country("MY", "MYS", u"Malaysia")
++Country("MV", "MDV", u"Maldives")
++Country("ML", "MLI", u"Mali")
++Country("MT", "MLT", u"Malta")
++Country("MH", "MHL", u"Marshall Islands (the)")
++Country("MQ", "MTQ", u"Martinique")
++Country("MR", "MRT", u"Mauritania")
++Country("MU", "MUS", u"Mauritius")
++Country("YT", "MYT", u"Mayotte")
++Country("MX", "MEX", u"Mexico")
++Country("FM", "FSM", u"Micronesia (Federated States of)")
++Country("MD", "MDA", u"Moldova (the Republic of)")
++Country("MC", "MCO", u"Monaco")
++Country("MN", "MNG", u"Mongolia")
++Country("ME", "MNE", u"Montenegro")
++Country("MS", "MSR", u"Montserrat")
++Country("MA", "MAR", u"Morocco")
++Country("MZ", "MOZ", u"Mozambique")
++Country("MM", "MMR", u"Myanmar")
++Country("NA", "NAM", u"Namibia")
++Country("NR", "NRU", u"Nauru")
++Country("NP", "NPL", u"Nepal")
++Country("NL", "NLD", u"Netherlands (the)")
++Country("NC", "NCL", u"New Caledonia")
++Country("NZ", "NZL", u"New Zealand")
++Country("NI", "NIC", u"Nicaragua")
++Country("NE", "NER", u"Niger (the)")
++Country("NG", "NGA", u"Nigeria")
++Country("NU", "NIU", u"Niue")
++Country("NF", "NFK", u"Norfolk Island")
++Country("MP", "MNP", u"Northern Mariana Islands (the)")
++Country("NO", "NOR", u"Norway")
++Country("OM", "OMN", u"Oman")
++Country("PK", "PAK", u"Pakistan")
++Country("PW", "PLW", u"Palau")
++Country("PS", "PSE", u"Palestine, State of")
++Country("PA", "PAN", u"Panama")
++Country("PG", "PNG", u"Papua New Guinea")
++Country("PY", "PRY", u"Paraguay")
++Country("PE", "PER", u"Peru")
++Country("PH", "PHL", u"Philippines (the)")
++Country("PN", "PCN", u"Pitcairn")
++Country("PL", "POL", u"Poland")
++Country("PT", "PRT", u"Portugal")
++Country("PR", "PRI", u"Puerto Rico")
++Country("QA", "QAT", u"Qatar")
++Country("MK", "MKD", u"Republic of North Macedonia")
++Country("RO", "ROU", u"Romania")
++Country("RU", "RUS", u"Russian Federation (the)")
++Country("RW", "RWA", u"Rwanda")
++Country("RE", "REU", u"Réunion")
++Country("BL", "BLM", u"Saint Barthélemy")
++Country("SH", "SHN", u"Saint Helena, Ascension and Tristan da
Cunha")
++Country("KN", "KNA", u"Saint Kitts and Nevis")
++Country("LC", "LCA", u"Saint Lucia")
++Country("MF", "MAF", u"Saint Martin (French part)")
++Country("PM", "SPM", u"Saint Pierre and Miquelon")
++Country("VC", "VCT", u"Saint Vincent and the Grenadines")
++Country("WS", "WSM", u"Samoa")
++Country("SM", "SMR", u"San Marino")
++Country("ST", "STP", u"Sao Tome and Principe")
++Country("SA", "SAU", u"Saudi Arabia")
++Country("SN", "SEN", u"Senegal")
++Country("RS", "SRB", u"Serbia")
++Country("SC", "SYC", u"Seychelles")
++Country("SL", "SLE", u"Sierra Leone")
++Country("SG", "SGP", u"Singapore")
++Country("SX", "SXM", u"Sint Maarten (Dutch part)")
++Country("SK", "SVK", u"Slovakia")
++Country("SI", "SVN", u"Slovenia")
++Country("SB", "SLB", u"Solomon Islands")
++Country("SO", "SOM", u"Somalia")
++Country("ZA", "ZAF", u"South Africa")
++Country("GS", "SGS", u"South Georgia and the South Sandwich
Islands")
++Country("SS", "SSD", u"South Sudan")
++Country("ES", "ESP", u"Spain")
++Country("LK", "LKA", u"Sri Lanka")
++Country("SD", "SDN", u"Sudan (the)")
++Country("SR", "SUR", u"Suriname")
++Country("SJ", "SJM", u"Svalbard and Jan Mayen")
++Country("SE", "SWE", u"Sweden")
++Country("CH", "CHE", u"Switzerland")
++Country("SY", "SYR", u"Syrian Arab Republic")
++Country("TW", "TWN", u"Taiwan (Province of China)")
++Country("TJ", "TJK", u"Tajikistan")
++Country("TZ", "TZA", u"Tanzania, United Republic of")
++Country("TH", "THA", u"Thailand")
++Country("TL", "TLS", u"Timor-Leste")
++Country("TG", "TGO", u"Togo")
++Country("TK", "TKL", u"Tokelau")
++Country("TO", "TON", u"Tonga")
++Country("TT", "TTO", u"Trinidad and Tobago")
++Country("TN", "TUN", u"Tunisia")
++Country("TR", "TUR", u"Turkey")
++Country("TM", "TKM", u"Turkmenistan")
++Country("TC", "TCA", u"Turks and Caicos Islands (the)")
++Country("TV", "TUV", u"Tuvalu")
++Country("UG", "UGA", u"Uganda")
++Country("UA", "UKR", u"Ukraine")
++Country("AE", "ARE", u"United Arab Emirates (the)")
++Country("GB", "GBR", u"United Kingdom of Great Britain and
Northern Ireland (the)")
++Country("UM", "UMI", u"United States Minor Outlying Islands
(the)")
++Country("US", "USA", u"United States of America (the)")
++Country("UY", "URY", u"Uruguay")
++Country("UZ", "UZB", u"Uzbekistan")
++Country("VU", "VUT", u"Vanuatu")
++Country("VE", "VEN", u"Venezuela (Bolivarian Republic
of)")
++Country("VN", "VNM", u"Viet Nam")
++Country("VG", "VGB", u"Virgin Islands (British)")
++Country("VI", "VIR", u"Virgin Islands (U.S.)")
++Country("WF", "WLF", u"Wallis and Futuna")
++Country("EH", "ESH", u"Western Sahara")
++Country("YE", "YEM", u"Yemen")
++Country("ZM", "ZMB", u"Zambia")
++Country("ZW", "ZWE", u"Zimbabwe")
++Country("AX", "ALA", u"Åland Islands")
++
++
++
++if __name__ == '__main__':
++
++ print(repr(Language.getstored('de')))
++ print(repr(Language.getstored('hrv')))
++ print(repr(Language.getstored('Danish')))
++ print(repr(Language.getstored('sqi')))
++ print(repr(Language.getstored("alb")))
++ print(repr(Language.getstored("German")))
++
++ print(Language.getstored('hrv').ISO639_1)
++ print(Language.getstored('de').ISO639_2B)
++ print(Language.getstored('de').ISO639_2T)
++ print(Language.getstored('deu').ISO639_2T)
++
++ print(repr(Country.getstored('ax')))
++ print(Country.getstored('AX').name)
++ print(Country.getstored('ALA').alpha2)
++ print(Country.getstored(u"Åland Islands").alpha3)
++
++ print(repr(Country.getstored('MC')))
++ print(Country.getstored('MC').name)
++ print(Country.getstored('mco').alpha2)
++ print(Country.getstored(u"Monaco").alpha3)
+diff --git a/mythtv/bindings/python/ttvdbv4/myth4ttvdbv4.py
b/mythtv/bindings/python/ttvdbv4/myth4ttvdbv4.py
+new file mode 100644
+index 00000000000..e0101220268
+--- /dev/null
++++ b/mythtv/bindings/python/ttvdbv4/myth4ttvdbv4.py
+@@ -0,0 +1,882 @@
++# -*- coding: UTF-8 -*-
++
++# ----------------------------------------------------
++# Purpose: MythTV Python Bindings for TheTVDB v4 API
++# Copyright: (c) 2021 Roland Ernst
++# License: GPL v2 or later, see LICENSE for details
++# ----------------------------------------------------
++
++
++import sys
++import os
++import json
++import re
++import requests
++import operator
++import time
++import pickle
++from lxml import etree
++from collections import OrderedDict
++from enum import IntEnum
++from datetime import timedelta, datetime
++from MythTV.ttvdbv4 import ttvdbv4_api as ttvdb
++from MythTV.ttvdbv4.locales import Language, Country
++from MythTV.ttvdbv4.utils import convert_date, strip_tags
++from MythTV import VideoMetadata
++from MythTV.utility import levenshtein
++
++
++def _print_class_content(obj):
++ for k, v in obj.__dict__.items():
++ if k.startswith('aliases'):
++ print(" ", k, " : ", "[ %s ]" % ';
'.join(x.name for x in obj.aliases))
++ elif k.startswith('nameTranslations'):
++ print(" ", k, " : ", v)
++ elif k.startswith('fetched_translations'):
++ print(" ", k, " : ", "[ %s ]" % ';
'.join(x.name for x in obj.fetched_translations))
++ elif k.startswith('name_similarity'):
++ # name similarity is not calculated by now.
++ pass
++ elif isinstance(v, list):
++ print(" ", k, " : ", "[ %d items ]" %
len(v))
++ else:
++ print(" ", k, " : ", v)
++
++
++def check_item(m, mitem, ignore=True):
++ # item is a tuple of (str, value)
++ # ToDo: Add this to the 'Metadata' class of MythTV's python bindings
++ try:
++ k, v = mitem
++ if v is None:
++ return None
++ m._inv_trans[m._global_type[k]](v)
++ return v
++ except:
++ if ignore:
++ return None
++ else:
++ raise
++
++
++def _sanitize_me(obj, attr, default):
++ try:
++ if getattr(obj, attr) is None:
++ setattr(obj, attr, default)
++ return obj
++ except (KeyError, AttributeError):
++ raise
++
++
++def sort_list_by_key(inlist, key, default, reverse=True):
++ """ Returns a sorted list by the rating attribute.
++ """
++ inlist = [_sanitize_me(i, key, default) for i in inlist]
++ opkey = operator.attrgetter(key)
++ return sorted(inlist, key=opkey, reverse=reverse)
++
++
++def sort_list_by_lang(inlist, languages, other_key=None):
++ """ Returns a sorted list by given languages.
++ The items of the inlist must have an attribute 'language'.
++ Equal items are sorted by the 'other_key' attribute.
++ """
++ outlist = []
++ for lang in languages:
++ # sorting "==" puts the desired lang at the end of the sorted list!
++ inlist.sort(key=lambda x: x.language == lang, reverse=False)
++ if other_key:
++ for i, tmp in enumerate(inlist):
++ if tmp.language == lang:
++ ranking_list = sort_list_by_key(inlist[i:], other_key, 0)
++ outlist.extend(ranking_list)
++ del inlist[i:]
++ break
++ outlist.extend(inlist)
++ return outlist
++
++
++def _name_match_quality(name, tvtitle):
++ distance = levenshtein(name.lower(), tvtitle.lower())
++ if len(tvtitle) > len(name):
++ match_quality = float(len(tvtitle) - distance) / len(tvtitle)
++ else:
++ match_quality = float(len(name) - distance) / len(name)
++ return match_quality
++
++
++class People(IntEnum):
++ """
++ Static definitions of 'people' type according API function
++ 'getAllPeopleTypes' to speed up queries.
++ """
++ Director = 1
++ Writer = 2
++ Actor = 3
++ Guest_Star = 4
++ Crew = 5
++ Creator = 6
++ Producer = 7
++ Showrunner = 8
++ Musical_Guest = 9
++ Host = 10
++ Executive_Producer = 11
++
++
++class Myth4TTVDBv4(object):
++ """
++ MythTV Bindings for TheTVDB v4.
++ This class implements the following grabber options by means of :
++ -N inetref subtitle : 'def buildNumbers()'
++ -N title subtitle : 'def buildNumbers()'
++ -D inetref season episode: 'def buildSingle()'
++ -C collectionref: 'def buildCollection()'
++ -M title: 'def buildList()'
++ It handles all the translations and name matching functionality.
++ """
++
++ def __init__(self, **kwargs):
++ for k, v in kwargs.items():
++ setattr(self, k, v)
++ # print("Init : ", k,v)
++
++ self.starttime = time.time()
++
++ # Authorization:
++ TTVDBv4Key = '708401c2-b73e-4ce0-97ec-8ef3a5038c3a'
++ TTVDBv4Pin = None
++ # get authorization data from local config
++ try:
++ TTVDBv4Key = self.config['Authorization']['TTVDBv4Key']
++ TTVDBv4Pin = self.config['Authorization']['TTVDBv4Pin']
++ except KeyError:
++ pass
++ auth_payload = {"apikey": TTVDBv4Key,
++ "pin": TTVDBv4Pin,
++ }
++
++ # get the lifetime of the access token from local config
++ try:
++ self.token_lifetime =
int(self.config['AccessToken']['Lifetime'])
++ except:
++ self.token_lifetime = 1
++ if self.debug:
++ print("%04d: Init: Using this lifetime for the access token:
'%d' day(s)."
++ % (self._get_ellapsed_time(), self.token_lifetime))
++
++ # prepare list of preferred languages
++ input_language = Language.getstored(self.language).ISO639_2T
++ self.languagelist = [input_language]
++ try:
++ for k in sorted(self.config['Languages'].keys()):
++ v = Language.getstored(self.config['Languages'][k]).ISO639_2T
++ if v != input_language:
++ self.languagelist.append(v)
++ except:
++ # considered as nice to have
++ pass
++ if self.debug:
++ print("%04d: Init: Using these languages to search: '%s'"
++ % (self._get_ellapsed_time(), " ".join(self.languagelist)))
++
++ # get preferences:
++ for pref in self.config['Preferences'].keys():
++ try:
++ if self.config['Preferences'][pref].lower() in ('1',
'yes'):
++ setattr(self, pref, True)
++ else:
++ setattr(self, pref, False)
++ except:
++ # considered a nice to have
++ setattr(self, pref, False)
++ if self.debug:
++ print("%04d: Init: Using these preferences to search:"
++ % self._get_ellapsed_time())
++ for pref in self.config['Preferences'].keys():
++ print(" '%s' : '%s'" % (pref, getattr(self,
pref)))
++
++ # get thresholds:
++ for t in ('NameThreshold', 'ThresholdStart',
'ThresholdDecrease'):
++ try:
++ setattr(self, t, float(self.config['Thresholds'][t]))
++ except:
++ self.NameThreshold = 0.99
++ self.ThresholdStart = 0.60
++ self.ThresholdDecrease = 0.1
++ if self.debug:
++ print("%04d: Init: Using these thresholds for name searching:"
++ % self._get_ellapsed_time())
++ for t in ('NameThreshold', 'ThresholdStart',
'ThresholdDecrease'):
++ print(" '%s' : '%0.2f'" % (t, getattr(self,
t)))
++
++ # get title conversions and inetref conversions
++ if self.debug:
++ print("%04d: Init: Title and Inetref Conversions:"
++ % self._get_ellapsed_time())
++ for k in self.config['TitleConversions'].keys():
++ print(" %s: %s" % (k,
self.config['TitleConversions'][k]))
++ for k in self.config['InetrefConversions'].keys():
++ print(" %s: %s" % (k,
self.config['InetrefConversions'][k]))
++ print(" Done")
++
++ # read cached authorization token
++ if self.debug:
++ print("%04d: Init: Bearer Authentication:"
++ % self._get_ellapsed_time())
++ auth_tuple = tuple()
++ auth_pfile = self.config.get('auth_file')
++ if auth_pfile and os.path.isfile(auth_pfile):
++ try:
++ with open(self.config['auth_file'], 'rb') as f:
++ auth_tuple = pickle.load(f)
++ except:
++ if self.debug:
++ print(" Reading cache-authentication file failed.")
++ # validate authorization token (valid one day)
++ token = None
++ if auth_tuple:
++ try:
++ auth_time = auth_tuple[0]
++ ptoken = auth_tuple[1]
++ if ptoken and \
++ auth_time + timedelta(days=self.token_lifetime) >
datetime.now():
++ if self.debug:
++ print(" Cached authentication token is valid.")
++ token = ptoken
++ else:
++ if self.debug:
++ print(" Cached authentication token is invalid.")
++ # print(token)
++ except:
++ if self.debug:
++ print(" Reading cached authentication failed.")
++
++ # start html session, re-use token if cached
++ self.session = requests.Session()
++ if not token or self.debug:
++ r =
self.session.post('https://api4.thetvdb.com/v4/login';,
json=auth_payload)
++ r_json = r.json()
++ # if self.debug:
++ # print(r_json)
++ error = r_json.get('message')
++ if error:
++ if error == 'Unauthorized':
++ print("tvdb_notauthorized: Error occured")
++ sys.exit(1)
++ status = r_json.get('status')
++ if status:
++ if status != 'success':
++ print("tvdb_notauthorized: Wrong status")
++ sys.exit(1)
++ try:
++ token = "Bearer %s" %
(r_json['data'].get('token'))
++ # print(token)
++ except:
++ print("tvdb_notauthorized: No token")
++ sys.exit(1)
++ if self.debug:
++ print(" Bearer Authentication passed with '%s'." %
status)
++
++ if auth_pfile:
++ now = datetime.now()
++ auth_tuple = (now, token)
++ try:
++ with open(auth_pfile, 'wb') as f:
++ pickle.dump(auth_tuple, f, -1)
++ except:
++ if self.debug:
++ print(" Writing cache-authentication file failed.")
++
++ self.session.headers.update({'Accept': 'application/json',
++ 'Accept-Language': self.language,
++ 'User-Agent': 'mythtv.org ttvdb v4
grabber',
++ 'Authorization': "%s" % (token)
++ })
++ # set the session for the tvbd4 api, enable json debug logs
++ ttvdb.set_jsondebug(self.jsondebug)
++ ttvdb.set_session(self.session)
++
++ # prepare the resulting xml tree
++ self.tree = etree.XML(u'<metadata></metadata>')
++
++ def _get_ellapsed_time(self):
++ return (int(time.time() - self.starttime))
++
++ def _select_preferred_langs(self, languages):
++ pref_langs = []
++ for lang in self.languagelist:
++ if lang in languages:
++ pref_langs.append(lang)
++ return pref_langs
++
++ def _get_title_conversion(self, title):
++ try:
++ if title in self.config['TitleConversions'].keys():
++ return self.config['TitleConversions'][title]
++ else:
++ return title
++ except:
++ return title
++
++ def _get_inetref_conversion(self, title):
++ try:
++ if str(title) in self.config['InetrefConversions'].keys():
++ return int(self.config['InetrefConversions'][str(title)])
++ else:
++ return title
++ except:
++ return title
++
++ def _split_title(self, title):
++ """ separate trailing ' (year)' from title
"""
++ rs = r'(?P<rtitle>.*) \((?P<ryear>(?:19|20)[0-9][0-9])\)$'
++ match = re.search(rs, title)
++ try:
++ title_wo_year = match.group('rtitle')
++ year = match.group('ryear')
++ except:
++ year = None
++ title_wo_year = title
++ return (title_wo_year, year)
++
++ def _get_names_translated(self, translations):
++ name_dict = OrderedDict()
++ for lang in self.languagelist:
++ if translations.get(lang) is not None:
++ name_dict[lang] = translations.get(lang)
++ return name_dict
++
++ def _get_info_from_translations(self, translations):
++ desc = ""
++ name = ""
++ name_found = False
++ for lang in self.languagelist:
++ try:
++ for t in translations:
++ try:
++ if lang == t.language:
++ if not name_found:
++ name = t.name
++ name_found = True
++ desc = t.overview
++ if name_found and desc:
++ raise StopIteration
++ except (KeyError, ValueError):
++ continue
++ except StopIteration:
++ break
++ return (name, desc)
++
++ def _get_crew_for_xml(self, crew_list, crew_type, list_character=False):
++ crew_dict_list = []
++ for c in crew_list:
++ try:
++ if c.personName:
++ d = {'name': c.personName, 'job': '%s' %
crew_type,
++ 'url': 'https://thetvdb.com/people/%s' %
c.peopleId}
++ if list_character and c.name:
++ d['character'] = c.name
++ crew_dict_list.append(d)
++ except:
++ # considered as nice to have
++ pass
++ return crew_dict_list
++
++ def _format_xml(self, ser_x, sea_x=None, epi_x=None):
++ m = VideoMetadata()
++ m.inetref = check_item(m, ("inetref", str(ser_x.id)), ignore=False)
++ # try to get title and description for the preferred language list:
++ # note: there could be a title but no description:
++ # $ ttvdb4.py -l ja -C 360893 --debug
++
++ # get series name and overview:
++ ser_title, desc = self._get_info_from_translations(ser_x.fetched_translations)
++ if not ser_title:
++ ser_title = ser_x.name
++ m.title = check_item(m, ("title", ser_title), ignore=False)
++
++ # get subtitle and overview:
++ if epi_x:
++ sub_title, sub_desc =
self._get_info_from_translations(epi_x.fetched_translations)
++ if not sub_title:
++ sub_title = epi_x.name
++ m.subtitle = check_item(m, ("subtitle", sub_title), ignore=False)
++ if sub_desc:
++ desc = sub_desc
++ m.season = check_item(m, ("season", epi_x.seasonNumber),
ignore=False)
++ m.episode = check_item(m, ("episode", epi_x.number),
ignore=False)
++ m.runtime = check_item(m, ("runtime", epi_x.runtime),
ignore=True)
++ desc = strip_tags(desc).replace("\r\n",
"").replace("\n", "")
++ m.description = check_item(m, ("description", desc))
++
++ try:
++ if ser_x.originalLanguage:
++ lang = ser_x.originalLanguage
++ else:
++ lang = self.language
++ if ser_x.originalCountry:
++ country = ser_x.originalCountry
++ else:
++ country = ser_x.country
++ m.language = check_item(m, ("language",
Language.getstored(lang).ISO639_1))
++ if country:
++ m.countries.append(country.upper()) # could be 'None'
++ m.year = check_item(m, ('year',
int(ser_x.firstAired.split('-')[0])))
++ m.userrating = check_item(m, ('userrating', ser_x.score))
++ for h in [x.id for x in ser_x.remoteIds if x.type == 4]:
++ # type 4 is 'Official Website'
++ m.homepage = check_item(m, ('homepage', h))
++ # MythTV supports only one entry
++ break
++ except:
++ # considered as nice to have
++ pass
++
++ # add categories:
++ try:
++ for genre in ser_x.genres:
++ if genre.name:
++ m.categories.append(genre.name)
++ except:
++ pass
++
++ # add optional fields:
++ if epi_x:
++ try:
++ # add studios
++ for c in epi_x.companies:
++ m.studios.append(c.name)
++ except:
++ pass
++ try:
++ # add IMDB reference
++ for r in epi_x.remoteIds:
++ if r.sourceName == 'IMDB':
++ m.imdb = check_item(m, ('imdb', r.id))
++ break
++ except:
++ raise
++ if self.country:
++ try:
++ # add certificates:
++ area = Country.getstored(self.country).alpha3
++ for c in epi_x.contentRatings:
++ if c.country.lower() == area.lower():
++ m.certifications[self.country] = c.name
++ break
++ except:
++ pass
++
++ if self.ShowPeople:
++ # add characters: see class 'People'
++ # characters of type 'Actor' are sorted in ascending order
++ actors = [c for c in ser_x.characters if c.type == People.Actor]
++ actors_sorted = sort_list_by_key(actors, "sort", 99,
reverse=False)
++ # prefer actors that are sorted, i.e.: 'sort' > 0
++ actors_s_1 = [x for x in actors_sorted if x.sort > 0]
++ # append the rest, i.e.: actors with sort == 0
++ actors_s_1.extend([x for x in actors_sorted if x.sort == 0])
++ m.people.extend(self._get_crew_for_xml(actors_s_1, 'Actor',
list_character=True))
++
++ # on episodes, characters of type 'Guest Star' are sorted in
ascending order
++ if epi_x:
++ guests = [c for c in epi_x.characters if c.type == People.Guest_Star]
++ guests_sorted = sort_list_by_key(guests, "sort", 99,
reverse=False)
++ m.people.extend(self._get_crew_for_xml(guests_sorted, 'Guest
Star'))
++
++ directors = [c for c in epi_x.characters if c.type ==
People.Showrunner]
++ directors_sorted = sort_list_by_key(directors, "sort", 99,
reverse=False)
++ m.people.extend(self._get_crew_for_xml(directors_sorted,
'Director'))
++
++ # no we have all extended records for series, season, episode, create xml for
them
++ series_banners = []; season_banners = []
++ series_posters = []; season_posters = []
++ series_fanarts = []; season_fanarts = []
++
++ # add the artworks, season preferred
++ # art_name what art_type(s) from_r / from_a
++ arts = [('coverart', season_posters, (7,), sea_x, sea_x.artwork if
sea_x else []),
++ ('coverart', series_posters, (2,), ser_x, ser_x.artworks),
++ ('fanart', season_fanarts, (8, 9), sea_x, sea_x.artwork if
sea_x else []),
++ ('fanart', series_fanarts, (3, 4), ser_x, ser_x.artworks),
++ ('banner', season_banners, (6,), sea_x, sea_x.artwork if
sea_x else []),
++ ('banner', series_banners, (1,), ser_x, ser_x.artworks),
++ ]
++ # avoid duplicates
++ used_urls = []
++ for (art_name, what, art_types, from_r, from_a) in arts:
++ artlist = [art for art in from_a if art.type in art_types]
++ what.extend(sort_list_by_lang(artlist, self.languagelist,
other_key='score'))
++ for entry in what:
++ try:
++ if entry.image not in used_urls:
++ used_urls.append(entry.image)
++ m.images.append({'type': art_name, 'url':
entry.image,
++ 'thumb': entry.thumbnail})
++ except:
++ pass
++ if epi_x and epi_x.imageType in (11, 12):
++ m.images.append({'type': 'screenshot', 'url':
epi_x.image})
++
++ self.tree.append(m.toXML())
++
++ def buildSingle(self):
++ """
++ The ttvdb api returns different id's for series, season, and episode.
++ MythTV stores only the series-id, therefore we need to fetch the correct
id's
++ for season and episode for that series-id.
++ """
++ # option -D inetref season episode
++ # $ ttvdb4.py -D 76568 2 8 --debug
++ # $ ttvdb4.py -l de -D 76568 2 8 --debug
++
++ if self.debug:
++ print("\n%04d: buildSingle: Query 'buildSingle' called with
'%s'"
++ % (self._get_ellapsed_time(), " ".join([str(x) for x in
self.tvdata])))
++ inetref = self.tvdata[0]
++ season = self.tvdata[1]
++ episode = self.tvdata[2]
++
++ # get series data:
++ ser_x = ttvdb.getSeriesExtended(inetref)
++
++ ser_x.fetched_translations = []
++ for lang in self._select_preferred_langs(ser_x.nameTranslations):
++ translation = ttvdb.getSeriesTranslation(inetref, lang)
++ ser_x.fetched_translations.append(translation)
++
++ if self.debug:
++ print("%04d: buildSingle: Series information for %s:"
++ % (self._get_ellapsed_time(), inetref))
++ _print_class_content(ser_x)
++
++ gen_episodes = ttvdb.getSeriesEpisodes(inetref, season_type='default',
season=season,
++ episodeNumber=episode, yielded=True)
++ ep = next(gen_episodes)
++ epi_x = ttvdb.getEpisodeExtended(ep.id)
++ for lang in self._select_preferred_langs(epi_x.nameTranslations):
++ translation = ttvdb.getEpisodeTranslation(epi_x.id, lang)
++ epi_x.fetched_translations.append(translation)
++ if self.debug:
++ print("%04d: buildSingle: Episode Information for %s : %s : %s"
++ % (self._get_ellapsed_time(), inetref, season, episode))
++ _print_class_content(epi_x)
++
++ # get season information:
++ sea_x = None
++ for s in epi_x.seasons:
++ if s.type.id == ser_x.defaultSeasonType:
++ sea_x = ttvdb.getSeasonExtended(s.id)
++ if self.debug:
++ print("%04d: buildSingle: Season Information for %s : %s"
++ % (self._get_ellapsed_time(), inetref, season))
++ _print_class_content(sea_x)
++ break
++
++ # no we have all extended records for series, season, episode, create xml for
them
++ self._format_xml(ser_x, sea_x, epi_x)
++
++ def buildCollection(self, other_inetref=None, xml_output=True):
++ """
++ Creates a single extendedSeriesRecord matching the 'inetref' provided by
the
++ command line.
++ If xml_output requested, update the common xml data, otherwise return this
record.
++ The 'other_inetref' option overrides the default 'inetref' from
the command line.
++ """
++ # option -C inetref
++ # $ ttvdb4.py -l en -C 360893 --debug
++ # $ ttvdb4.py -l de -C 360893 --debug
++ # $ ttvdb4.py -l ja -C 360893 --debug (missing Japanese overview)
++ # $ ttvdb4.py -l fr -C 76568 --debug
++
++ if other_inetref:
++ tvinetref = other_inetref
++ else:
++ tvinetref = self.collectionref[0]
++
++ if self.debug:
++ print("\n%04d: buildCollection: Query 'buildCollection' called
with "
++ "'%s', xml_output = %s"
++ % (self._get_ellapsed_time(), tvinetref, xml_output))
++
++ # get data for passed inetref and preferred translations
++ ser_x = ttvdb.getSeriesExtended(tvinetref)
++
++ ser_x.fetched_translations = []
++ for lang in self._select_preferred_langs(ser_x.nameTranslations):
++ translation = ttvdb.getSeriesTranslation(tvinetref, lang)
++ ser_x.fetched_translations.append(translation)
++
++ # define exact name found:
++ ser_x.name_similarity = 1.0
++
++ if self.debug:
++ print("%04d: buildCollection: Series information for %s:"
++ % (self._get_ellapsed_time(), tvinetref))
++ _print_class_content(ser_x)
++
++ if xml_output:
++ self._format_xml(ser_x)
++
++ return ser_x
++
++ def buildList(self, other_title=None, xml_output=True, get_all=False):
++ """
++ Creates a list of extendedSeriesRecords matching the 'title' provided by
the
++ command line.
++ If xml_output requested, update the common xml data, and return this list.
++ The 'other_title' option overrides the default 'title' provided
by the command line.
++ Calls 'buildCollection(other_inetref=<inetref_matching_title>,
xml_output=xml_output)
++ for each record of the list.
++ Stops on searching if 'get_all' is 'False' and an exact match
was found.
++ Returns a sorted list according preferred languages and
'name_similarity'.
++ """
++ # option -M title
++ # this works on:
++ # $ ttvdb4.py -l de -M "Chernobyl" --debug --> exact match
++ # $ ttvdb4.py -l en -M "Chernobyl" --debug --> exact match
(inetref 360893)
++ # $ ttvdb4.py -l de -M "Chernobyl:" --debug --> multiple matches
++ # $ ttvdb4.py -l de -M "Die Munsters" --> single match
++ # $ ttvdb4.py -l en -M "The Munsters" --> single match
++ # $ ttvdb4.py -l de -M "Hawaii Five-0" --> single match --->
inetref 164541
++ # $ ttvdb4.py -l de -M "Hawaii Five-O" --> multiple matches (71223
and 164541)
++ # $ ttvdb4.py -l en -M "Hawaii Five-0 (2010)" --debug ---> inetref
164541
++ # $ ttvdb4.py -l el -M "Star Trek Discovery" ok (see pull request
#180)
++ # $ ttvdb4.py -l en -M "Marvel's Agents of S H I E L D" returns
nothing, see #247
++ # see override in ttvdbv4.ini
++
++ if other_title:
++ tvtitle = other_title
++ else:
++ # take care on conversions defined in the ttvdb4.ini file
++ tvtitle = self._get_title_conversion(self.tvtitle[0])
++
++ if self.debug:
++ print("\n%04d: buildList: Query 'buildList' called with
'%s', xml_output = %s"
++ % (self._get_ellapsed_time(), tvtitle, xml_output))
++
++ # separate trailing ' (year)' from title
++ title_wo_year, year = self._split_title(tvtitle)
++
++ # get series overview
++ so = ttvdb.getSearchResults(query=title_wo_year, type='series',
year=year)
++ if not so:
++ return
++ exact = False
++ series = []
++ for item in so:
++ # check if we can find exact match for 'tvtitle'
++ translated_names = self._get_names_translated(item.translations)
++ # ttvdb returns sometimes '<tvtitle> (<year>)' like
"The Forsyte Saga (2002)"
++ translated_names_wo_year = \
++ [self._split_title(x)[0] for x in
translated_names.values()]
++ if (item.name == tvtitle or item.name == title_wo_year or
++ tvtitle in item.aliases or
++ tvtitle in translated_names.values() or
++ tvtitle in translated_names_wo_year or
++ title_wo_year in item.aliases or
++ title_wo_year in translated_names.values()):
++ item.name_similarity = 1.0
++ series.append(item)
++ exact = True
++ if exact:
++ if self.debug:
++ te = self._get_ellapsed_time()
++ if len(series) == 1:
++ print("%04d: buildList: Found unique match for '%s':
%s"
++ % (te, tvtitle, series[0].tvdb_id))
++ else:
++ print("%04d: buildList: Found multiple exact matches for
'%s':"
++ % (te, tvtitle))
++ for i in series:
++ print(" %s" % i.tvdb_id)
++ if get_all or not exact:
++ # if no exact match was found, append all series for given languages:
++ if self.debug and not exact:
++ print("%04d: buildList: Found no exact match for '%s',
return a sorted list."
++ % (self._get_ellapsed_time(), tvtitle))
++ for item in so:
++ if item in series:
++ continue
++ # select only items with preferred languages and calculate name
similarity
++ all_names = [item.name]
++ all_names.extend(item.aliases)
++
all_names.extend(self._get_names_translated(item.translations).values())
++ item.name_similarity = \
++ max([_name_match_quality(x, title_wo_year) for x in
all_names])
++ series.append(item)
++
++ # sort the list by name similarity
++ series = sort_list_by_key(series, 'name_similarity', 0.0)
++ ser_x_list = []
++ for s in series:
++ if self.debug:
++ print("\n%04d: buildList: Chosen series record according "
++ "'overall name similarity': '%0.2f':"
++ % (self._get_ellapsed_time(), s.name_similarity))
++ try:
++ if s.tvdb_id: # sometimes, this is empty
++ sid = s.tvdb_id
++ elif s.id:
++ sid = s.id.split('-')[1]
++ else:
++ sid = None
++ ser_x = self.buildCollection(other_inetref=int(sid), xml_output=False)
++ # re-use name similarity from SearchResult
++ ser_x.name_similarity = s.name_similarity
++ ser_x_list.append(ser_x)
++ if self.debug:
++ print(" ", "name_similarity", " :
", "%0.2f" % s.name_similarity)
++ if xml_output:
++ self._format_xml(ser_x)
++ except:
++ # ser_x could be 'None'
++ pass
++
++ return ser_x_list
++
++ def buildNumbers(self):
++ """
++ If option -N inetref subtitle:
++ calls 'buildCollection(other_inetref=inetref, xml_output=False)'
++ --> single
extendedSeriesRecord
++ If option -N title subtitle:
++ calls 'buildList(other_title=title, xml_output=False)'
++ --> list of
extendedSeriesRecords
++ Then checks the most matching records for valid subtitle.
++ """
++ # either option -N inetref subtitle
++ # or option -N title subtitle # note: title may include a trailing '
(year)'
++ # or option -N title timestamp ### XXX ToDo implement me
++ # this may take several minutes
++ # $ ttvdb4.py -l de -N 76568 "Emily in Nöten" --debug
++ # $ ttvdb4.py -l en -a us - N 76568 "The Road Trip to Harvard"
--debug
++ # $ ttvdb4.py -l de -N "Die Munsters" "Der Liebestrank"
--debug
++ # $ ttvdb4.py -l de -N "Hawaii Five-0" "Geflügelsalat"
--debug
++ # $ ttvdb4.py -l en -N "The Munsters" 'My Fair Munster'
--debug
++ # $ ttvdb4.py -l en -N "The Forsyte Saga (2002)" "Episode 1"
--debug --> multiple episodes
++ # $ ttvdb4.py -l en -N "The Forsyte Saga" "A Silent Wooing"
--debug
++ # $ ttvdb4.py -l en -N "The Forsyte Saga" "Episode 1"
--debug --> multiple episodes
++ # $ ttvdb4.py -l en -N "The Forsyte Saga (2002)" "A Silent
Wooing" --debug --> no match
++ # $ ttvdb4.py -l de -N "Es war einmal... Das Leben" "Ein ganz
besonderer Saft / Das Blut" --> single match
++ # $ ttvdb4.py -l de -N "Es war einmal Das Leben" "Ein ganz
besonderer Saft / Das Blut" --> single match
++ # $ ttvdb4.py -l de -N "Es war einmal Das Leben" "Das Blut"
--> no matches
++ # $ ttvdb4.py -l en -N "Once Upon a Time Life" "The Blood"
--> single match
++ # $ ttvdb4.py -l en -N "Marvel's Agents of S H I E L D"
"Shadows" only with override in ttvdb4.ini
++ # $ ttvdb4.py -l en -N "Eleventh Hour" "Frozen" works with
and without override in ttvdb4.ini
++
++ if self.debug:
++ print("\n%04d: buildNumbers: Query 'buildNumbers' called with
'%s' '%s'"
++ % (self._get_ellapsed_time(), self.tvnumbers[0], self.tvnumbers[1]))
++ # ToDo:
++ # below check shows a deficiency of the MythTV grabber API itself:
++ # TV-Shows or Movies with an integer as title are not recognized correctly.
++ # see
https://www.mythtv.org/wiki/MythTV_Universal_Metadata_Format
++ # and
https://code.mythtv.org/trac/ticket/11850
++
++ # take care on conversions through the ttvdb.ini file
++ converted_inetref = self._get_inetref_conversion(self.tvnumbers[0])
++ try:
++ inetref = int(converted_inetref)
++ tvsubtitle = self.tvnumbers[1]
++ ser_x_list = [self.buildCollection(other_inetref=inetref,
xml_output=False)]
++ except ValueError:
++ ### XXX ToDo: implement datetime as option for subtitle
++ tvtitle = self._get_title_conversion(self.tvnumbers[0])
++ tvsubtitle = self.tvnumbers[1]
++ inetref = None
++ ser_b_list = self.buildList(other_title=tvtitle, xml_output=False,
get_all=True)
++ # reduce list according name similarity
++ if ser_b_list:
++ ser_x_list = [x for x in ser_b_list if x.name_similarity >
self.ThresholdStart]
++ else:
++ ser_x_list = []
++ # loop over the list and calculate name_similarity of series and episode
++ found_items = []
++ for ser_x in ser_x_list:
++ if self.debug and ser_x.fetched_translations:
++ print("%04d: buildNumbers: Checking series named '%s' with
inetref '%s':"
++ % (self._get_ellapsed_time(), ser_x.fetched_translations[0].name,
ser_x.id))
++ # get all episodes for every season, and calculate the name similarity
++ episodes = []
++ # get the default season type and search only for that:
++ def_season_type = ser_x.defaultSeasonType
++ for sea_id in [x.id for x in ser_x.seasons if x.type.id ==
def_season_type]:
++ sea_x = ttvdb.getSeasonExtended(sea_id)
++ epi_id_list = []
++ if sea_x is None:
++ continue
++ for epi in sea_x.episodes:
++ # an episode may be listed multiple times, include it only once:
++ # ### XXX Note: this is a bug in TVDBv4 API
++ if epi.id not in epi_id_list:
++ all_names = []
++ for lang in self._select_preferred_langs(epi.nameTranslations):
++ translation = ttvdb.getEpisodeTranslation(epi.id, lang)
++ if translation is None:
++ continue
++ all_names.append(translation.name)
++ all_names.extend(translation.aliases)
++ epi.name_similarity = \
++ max([_name_match_quality(x, tvsubtitle) for x in
all_names])
++ epi.fetched_translations.append(translation)
++ episodes.append(epi)
++ epi_id_list.append(epi.id)
++ if self.debug:
++ print("%04d: buildNumbers: Found episode names:
'%s'"
++ % (self._get_ellapsed_time(),
++ '; '.join([n.name for n in
epi.fetched_translations])))
++ print(" with name similarity : %0.2f" %
epi.name_similarity)
++ print(" with inetref %s : season# %d : episode# %d
"
++ % (ser_x.id, epi.seasonNumber, epi.number))
++
++ # sort the list by name_similarity, generate xml output for max. 10 items
++ sorted_episodes = sort_list_by_key(episodes, 'name_similarity',
0.0)
++ for epi in sorted_episodes[:10]:
++ # apply minimum threshold for name similarity
++ if epi.name_similarity < self.ThresholdStart:
++ break
++ # get episode and season data for that episode
++ epi_x = ttvdb.getEpisodeExtended(epi.id)
++ # append the translations already collected:
++ epi_x.fetched_translations = epi.fetched_translations
++ epi_x.name_similarity = epi.name_similarity
++
++ sea_x = None
++ for s in epi_x.seasons:
++ if s.type.id == ser_x.defaultSeasonType:
++ sea_x = ttvdb.getSeasonExtended(s.id)
++ break
++ if self.debug:
++ print("%04d: buildNumbers: Selected episode names:
'%s'"
++ % (self._get_ellapsed_time(),
++ '; '.join([n.name for n in
epi_x.fetched_translations])))
++ print(" with name similarity : %0.2f" %
epi.name_similarity)
++
++ found_items.append((ser_x, sea_x, epi_x))
++
++ # now sort the found_items list and create xml:
++ found_items.sort(reverse=True,
++ key=lambda x: x[0].name_similarity + x[2].name_similarity)
++
++ exact_match_count = 0
++ last_similarity = 2 * self.ThresholdStart
++ for (ser_x, sea_x, epi_x) in found_items:
++ overall_similarity = epi_x.name_similarity + ser_x.name_similarity
++ if self.debug and ser_x.fetched_translations:
++ print("%04d: buildNumbers: Found item series: '%s', season:
'%s', "
++ "episode: '%s' with overall similarity: %0.2f"
++ % (self._get_ellapsed_time(), ser_x.fetched_translations[0].name,
++ sea_x.number, epi_x.fetched_translations[0].name,
overall_similarity))
++ # stop if match similarity decreases
++ if overall_similarity > 2 * self.NameThreshold: # default 1.98
++ exact_match_count += 1
++ if overall_similarity > last_similarity:
++ last_similarity = overall_similarity
++ elif exact_match_count and \
++ abs(last_similarity - overall_similarity) > self.ThresholdDecrease:
++ if self.debug:
++ print(" canceling output due to decrease of 'name
similarity'.")
++ break
++
++ self._format_xml(ser_x, sea_x, epi_x)
+diff --git a/mythtv/bindings/python/ttvdbv4/ttvdbv4_api.py
b/mythtv/bindings/python/ttvdbv4/ttvdbv4_api.py
+new file mode 100644
+index 00000000000..23e43155d00
+--- /dev/null
++++ b/mythtv/bindings/python/ttvdbv4/ttvdbv4_api.py
+@@ -0,0 +1,602 @@
++# -*- coding: UTF-8 -*-
++
++# ----------------------------------------------------
++# Purpose: MythTV Python Bindings for TheTVDB v4 API
++# Copyright: (c) 2021 Roland Ernst
++# License: GPL v2 or later, see LICENSE for details
++# ----------------------------------------------------
++
++
++import sys
++from requests import codes as requestcodes
++if 0:
++ from urllib.parse import urlencode, quote_plus, quote
++from pprint import pprint
++
++from .definitions import *
++
++
++MYTHTV_TTVDBV4_API_VERSION = "4.5.0.1"
++
++# set this to true for showing raw json data
++#JSONDEBUG = True
++JSONDEBUG = False
++
++# Set an open requests session here
++ReqSession = None
++
++def set_jsondebug(b):
++ global JSONDEBUG
++ JSONDEBUG = bool(b)
++
++def set_session(s):
++ global ReqSession
++ #print("set_session called with %s" % type(s))
++ ReqSession = s
++
++
++def _query_api(url, params=None):
++ global ReqSession
++ if JSONDEBUG:
++ print("Params: ", params)
++ if 0:
++ # python requests encodes everything with 'quote_plus"
++ # thetvdb api may need '%20' instead of '+' for a space
character
++ if params:
++ url = '{0}?{1}'.format(url, urlencode(params, safe='',
quote_via=quote))
++ res = ReqSession.get(url)
++ else:
++ res = ReqSession.get(url, params=params)
++
++ if res is None:
++ return {}
++ if JSONDEBUG:
++ print(res.url)
++ print(res.request.headers)
++ if res.status_code != requestcodes.OK:
++ if JSONDEBUG:
++ print('http request was unsuccessful: ' + str(res.status_code),
res.reason, res.url)
++ #sys.exit(1)
++ return {}
++ # res is a dictionary with 'data', 'status' and 'links'
dicts
++ resjson = res.json()
++ if JSONDEBUG:
++ print("Successful http request '%s':" % res.url)
++ pprint(resjson)
++ return resjson
++
++
++def _query_yielded(record, path, params, listname=None):
++ # do not trust the 'links' section in the response
++ # simply loop until no data are provided
++ curr_page = int(params['page'])
++ while True:
++ res = _query_api(path, params)
++ # check for 'success'
++ if res is not None and res.get('data') is not None and \
++ res.get('status') is not None and res['status'] ==
'success':
++ if listname:
++ datalist = res['data'][listname]
++ else:
++ datalist = res['data']
++ if datalist:
++ for item in datalist:
++ yield record(item)
++ else:
++ raise StopIteration
++ else:
++ #break
++ raise StopIteration
++ curr_page += 1
++ params['page'] = curr_page
++
++
++"""Generated API for
thetvdb.com TVDB API V4 v 4.5.0"""
++# modifications marked with '### XXX'
++
++
++TTVDBV4_path = "https://api4.thetvdb.com/v4"
++getArtworkBase_path = TTVDBV4_path + "/artwork/{id}"
++getArtworkExtended_path = TTVDBV4_path + "/artwork/{id}/extended"
++getAllArtworkStatuses_path = TTVDBV4_path + "/artwork/statuses"
++getAllArtworkTypes_path = TTVDBV4_path + "/artwork/types"
++getAllAwards_path = TTVDBV4_path + "/awards"
++getAward_path = TTVDBV4_path + "/awards/{id}"
++getAwardExtended_path = TTVDBV4_path + "/awards/{id}/extended"
++getAwardCategory_path = TTVDBV4_path + "/awards/categories/{id}"
++getAwardCategoryExtended_path = TTVDBV4_path +
"/awards/categories/{id}/extended"
++getCharacterBase_path = TTVDBV4_path + "/characters/{id}"
++getAllCompanies_path = TTVDBV4_path + "/companies"
++getCompanyTypes_path = TTVDBV4_path + "/companies/types"
++getCompany_path = TTVDBV4_path + "/companies/{id}"
++getAllContentRatings_path = TTVDBV4_path + "/content/ratings"
++getAllCountries_path = TTVDBV4_path + "/countries"
++getEntityTypes_path = TTVDBV4_path + "/entities/types"
++getEpisodeBase_path = TTVDBV4_path + "/episodes/{id}"
++getEpisodeExtended_path = TTVDBV4_path + "/episodes/{id}/extended"
++getEpisodeTranslation_path = TTVDBV4_path +
"/episodes/{id}/translations/{language}"
++getAllGenders_path = TTVDBV4_path + "/genders"
++getAllGenres_path = TTVDBV4_path + "/genres"
++getGenreBase_path = TTVDBV4_path + "/genres/{id}"
++getAllInspirationTypes_path = TTVDBV4_path + "/inspiration/types"
++getAllLanguages_path = TTVDBV4_path + "/languages"
++getAllLists_path = TTVDBV4_path + "/lists"
++getList_path = TTVDBV4_path + "/lists/{id}"
++getListExtended_path = TTVDBV4_path + "/lists/{id}/extended"
++getListTranslation_path = TTVDBV4_path +
"/lists/{id}/translations/{language}"
++getAllMovie_path = TTVDBV4_path + "/movies"
++getMovieBase_path = TTVDBV4_path + "/movies/{id}"
++getMovieExtended_path = TTVDBV4_path + "/movies/{id}/extended"
++getMovieTranslation_path = TTVDBV4_path +
"/movies/{id}/translations/{language}"
++getAllMovieStatuses_path = TTVDBV4_path + "/movies/statuses"
++getPeopleBase_path = TTVDBV4_path + "/people/{id}"
++getPeopleExtended_path = TTVDBV4_path + "/people/{id}/extended"
++getPeopleTranslation_path = TTVDBV4_path +
"/people/{id}/translations/{language}"
++getAllPeopleTypes_path = TTVDBV4_path + "/people/types"
++getSearchResults_path = TTVDBV4_path + "/search"
++getAllSeasons_path = TTVDBV4_path + "/seasons"
++getSeasonBase_path = TTVDBV4_path + "/seasons/{id}"
++getSeasonExtended_path = TTVDBV4_path + "/seasons/{id}/extended"
++getSeasonTypes_path = TTVDBV4_path + "/seasons/types"
++getSeasonTranslation_path = TTVDBV4_path +
"/seasons/{id}/translations/{language}"
++getAllSeries_path = TTVDBV4_path + "/series"
++getSeriesBase_path = TTVDBV4_path + "/series/{id}"
++getSeriesExtended_path = TTVDBV4_path + "/series/{id}/extended"
++getSeriesEpisodes_path = TTVDBV4_path + "/series/{id}/episodes/{season_type}"
++getSeriesSeasonEpisodesTranslated_path = TTVDBV4_path +
"/series/{id}/episodes/{season_type}/{lang}"
++getSeriesTranslation_path = TTVDBV4_path +
"/series/{id}/translations/{language}"
++getAllSeriesStatuses_path = TTVDBV4_path + "/series/statuses"
++getAllSourceTypes_path = TTVDBV4_path + "/sources/types"
++updates_path = TTVDBV4_path + "/updates"
++
++
++def getArtworkBase(id):
++ path = getArtworkBase_path.format(id=str(id))
++ res = _query_api(path)
++ data = res['data'] if res.get('data') is not None else None
++ return( ArtworkBaseRecord(data) if data is not None else None )
++
++
++def getArtworkExtended(id):
++ path = getArtworkExtended_path.format(id=str(id))
++ res = _query_api(path)
++ data = res['data'] if res.get('data') is not None else None
++ return( ArtworkExtendedRecord(data) if data is not None else None )
++
++
++def getAllArtworkStatuses():
++ path = getAllArtworkStatuses_path.format()
++ res = _query_api(path)
++ data = res['data'] if res.get('data') is not None else None
++ return( [ArtworkStatus(x) for x in data] if data is not None else [] )
++
++
++def getAllArtworkTypes():
++ path = getAllArtworkTypes_path.format()
++ res = _query_api(path)
++ data = res['data'] if res.get('data') is not None else None
++ return( [ArtworkType(x) for x in data] if data is not None else [] )
++
++
++def getAllAwards():
++ path = getAllAwards_path.format()
++ res = _query_api(path)
++ data = res['data'] if res.get('data') is not None else None
++ return( [AwardBaseRecord(x) for x in data] if data is not None else [] )
++
++
++def getAward(id):
++ path = getAward_path.format(id=str(id))
++ res = _query_api(path)
++ data = res['data'] if res.get('data') is not None else None
++ return( AwardBaseRecord(data) if data is not None else None )
++
++
++def getAwardExtended(id):
++ path = getAwardExtended_path.format(id=str(id))
++ res = _query_api(path)
++ data = res['data'] if res.get('data') is not None else None
++ return( AwardExtendedRecord(data) if data is not None else None )
++
++
++def getAwardCategory(id):
++ path = getAwardCategory_path.format(id=str(id))
++ res = _query_api(path)
++ data = res['data'] if res.get('data') is not None else None
++ return( AwardCategoryBaseRecord(data) if data is not None else None )
++
++
++def getAwardCategoryExtended(id):
++ path = getAwardCategoryExtended_path.format(id=str(id))
++ res = _query_api(path)
++ data = res['data'] if res.get('data') is not None else None
++ return( AwardCategoryExtendedRecord(data) if data is not None else None )
++
++
++def getCharacterBase(id):
++ path = getCharacterBase_path.format(id=str(id))
++ res = _query_api(path)
++ data = res['data'] if res.get('data') is not None else None
++ return( Character(data) if data is not None else None )
++
++
++def getAllCompanies(page=0, yielded=False):
++ params = {}
++ if page is not None:
++ params['page'] = page
++ path = getAllCompanies_path.format()
++ if yielded:
++ return _query_yielded(Company, path, params, listname=None)
++ else:
++ res = _query_api(path, params)
++ data = res['data'] if res.get('data') is not None else None
++ return( [Company(x) for x in data] if data is not None else [] )
++
++
++def getCompanyTypes():
++ path = getCompanyTypes_path.format()
++ res = _query_api(path)
++ data = res['data'] if res.get('data') is not None else None
++ return( [CompanyType(x) for x in data] if data is not None else [] )
++
++
++def getCompany(id):
++ path = getCompany_path.format(id=str(id))
++ res = _query_api(path)
++ data = res['data'] if res.get('data') is not None else None
++ return( Company(data) if data is not None else None )
++
++
++def getAllContentRatings():
++ path = getAllContentRatings_path.format()
++ res = _query_api(path)
++ data = res['data'] if res.get('data') is not None else None
++ return( [ContentRating(x) for x in data] if data is not None else [] )
++
++
++def getAllCountries():
++ path = getAllCountries_path.format()
++ res = _query_api(path)
++ data = res['data'] if res.get('data') is not None else None
++ return( [Country(x) for x in data] if data is not None else [] )
++
++
++def getEntityTypes():
++ path = getEntityTypes_path.format()
++ res = _query_api(path)
++ data = res['data'] if res.get('data') is not None else None
++ return( [EntityType(x) for x in data] if data is not None else [] )
++
++
++def getEpisodeBase(id):
++ path = getEpisodeBase_path.format(id=str(id))
++ res = _query_api(path)
++ data = res['data'] if res.get('data') is not None else None
++ return( EpisodeBaseRecord(data) if data is not None else None )
++
++
++def getEpisodeExtended(id, meta=None):
++ params = {}
++ if meta is not None:
++ params['meta'] = meta
++ path = getEpisodeExtended_path.format(id=str(id))
++ res = _query_api(path, params)
++ data = res['data'] if res.get('data') is not None else None
++ return( EpisodeExtendedRecord(data) if data is not None else None )
++
++
++def getEpisodeTranslation(id, language):
++ path = getEpisodeTranslation_path.format(id=str(id), language=language)
++ res = _query_api(path)
++ data = res['data'] if res.get('data') is not None else None
++ return( Translation(data) if data is not None else None )
++
++
++def getAllGenders():
++ path = getAllGenders_path.format()
++ res = _query_api(path)
++ data = res['data'] if res.get('data') is not None else None
++ return( [Gender(x) for x in data] if data is not None else [] )
++
++
++def getAllGenres():
++ path = getAllGenres_path.format()
++ res = _query_api(path)
++ data = res['data'] if res.get('data') is not None else None
++ return( [GenreBaseRecord(x) for x in data] if data is not None else [] )
++
++
++def getGenreBase(id):
++ path = getGenreBase_path.format(id=str(id))
++ res = _query_api(path)
++ data = res['data'] if res.get('data') is not None else None
++ return( GenreBaseRecord(data) if data is not None else None )
++
++
++def getAllInspirationTypes():
++ path = getAllInspirationTypes_path.format()
++ res = _query_api(path)
++ data = res['data'] if res.get('data') is not None else None
++ return( [InspirationType(x) for x in data] if data is not None else [] )
++
++
++def getAllLanguages():
++ path = getAllLanguages_path.format()
++ res = _query_api(path)
++ data = res['data'] if res.get('data') is not None else None
++ return( [Language(x) for x in data] if data is not None else [] )
++
++
++def getAllLists(page=0, yielded=False):
++ params = {}
++ if page is not None:
++ params['page'] = page
++ path = getAllLists_path.format()
++ if yielded:
++ return _query_yielded(ListBaseRecord, path, params, listname=None)
++ else:
++ res = _query_api(path, params)
++ data = res['data'] if res.get('data') is not None else None
++ return( [ListBaseRecord(x) for x in data] if data is not None else [] )
++
++
++def getList(id):
++ path = getList_path.format(id=str(id))
++ res = _query_api(path)
++ data = res['data'] if res.get('data') is not None else None
++ return( ListBaseRecord(data) if data is not None else None )
++
++
++def getListExtended(id):
++ path = getListExtended_path.format(id=str(id))
++ res = _query_api(path)
++ data = res['data'] if res.get('data') is not None else None
++ return( ListExtendedRecord(data) if data is not None else None )
++
++
++def getListTranslation(id, language):
++ path = getListTranslation_path.format(id=str(id), language=language)
++ res = _query_api(path)
++ data = res['data'] if res.get('data') is not None else None
++ return( [Translation(x) for x in data] if data is not None else [] )
++
++
++def getAllMovie(page=0, yielded=False):
++ params = {}
++ if page is not None:
++ params['page'] = page
++ path = getAllMovie_path.format()
++ if yielded:
++ return _query_yielded(MovieBaseRecord, path, params, listname=None)
++ else:
++ res = _query_api(path, params)
++ data = res['data'] if res.get('data') is not None else None
++ return( [MovieBaseRecord(x) for x in data] if data is not None else [] )
++
++
++def getMovieBase(id):
++ path = getMovieBase_path.format(id=str(id))
++ res = _query_api(path)
++ data = res['data'] if res.get('data') is not None else None
++ return( MovieBaseRecord(data) if data is not None else None )
++
++
++def getMovieExtended(id, meta=None):
++ params = {}
++ if meta is not None:
++ params['meta'] = meta
++ path = getMovieExtended_path.format(id=str(id))
++ res = _query_api(path, params)
++ data = res['data'] if res.get('data') is not None else None
++ return( MovieExtendedRecord(data) if data is not None else None )
++
++
++def getMovieTranslation(id, language):
++ path = getMovieTranslation_path.format(id=str(id), language=language)
++ res = _query_api(path)
++ data = res['data'] if res.get('data') is not None else None
++ return( Translation(data) if data is not None else None )
++
++
++def getAllMovieStatuses():
++ path = getAllMovieStatuses_path.format()
++ res = _query_api(path)
++ data = res['data'] if res.get('data') is not None else None
++ return( [Status(x) for x in data] if data is not None else [] )
++
++
++def getPeopleBase(id):
++ path = getPeopleBase_path.format(id=str(id))
++ res = _query_api(path)
++ data = res['data'] if res.get('data') is not None else None
++ return( PeopleBaseRecord(data) if data is not None else None )
++
++
++def getPeopleExtended(id, meta=None):
++ params = {}
++ if meta is not None:
++ params['meta'] = meta
++ path = getPeopleExtended_path.format(id=str(id))
++ res = _query_api(path, params)
++ data = res['data'] if res.get('data') is not None else None
++ return( PeopleExtendedRecord(data) if data is not None else None )
++
++
++def getPeopleTranslation(id, language):
++ path = getPeopleTranslation_path.format(id=str(id), language=language)
++ res = _query_api(path)
++ data = res['data'] if res.get('data') is not None else None
++ return( Translation(data) if data is not None else None )
++
++
++def getAllPeopleTypes():
++ path = getAllPeopleTypes_path.format()
++ res = _query_api(path)
++ data = res['data'] if res.get('data') is not None else None
++ return( [PeopleType(x) for x in data] if data is not None else [] )
++
++
++def getSearchResults(q=None, query=None, type=None, remote_id=None, year=None,
offset=None, limit=None):
++ params = {}
++ if q is not None:
++ params['q'] = q
++ if query is not None:
++ params['query'] = query
++ if type is not None:
++ params['type'] = type
++ if remote_id is not None:
++ params['remote_id'] = remote_id
++ if year is not None:
++ params['year'] = year
++ if offset is not None:
++ params['offset'] = offset
++ if limit is not None:
++ params['limit'] = limit
++ path = getSearchResults_path.format()
++ res = _query_api(path, params)
++ data = res['data'] if res.get('data') is not None else None
++ return( [SearchResult(x) for x in data] if data is not None else [] )
++
++
++def getAllSeasons(page=0, yielded=False):
++ params = {}
++ if page is not None:
++ params['page'] = page
++ path = getAllSeasons_path.format()
++ if yielded:
++ return _query_yielded(SeasonBaseRecord, path, params, listname=None)
++ else:
++ res = _query_api(path, params)
++ data = res['data'] if res.get('data') is not None else None
++ return( [SeasonBaseRecord(x) for x in data] if data is not None else [] )
++
++
++def getSeasonBase(id):
++ path = getSeasonBase_path.format(id=str(id))
++ res = _query_api(path)
++ data = res['data'] if res.get('data') is not None else None
++ return( SeasonBaseRecord(data) if data is not None else None )
++
++
++def getSeasonExtended(id):
++ path = getSeasonExtended_path.format(id=str(id))
++ res = _query_api(path)
++ data = res['data'] if res.get('data') is not None else None
++ return( SeasonExtendedRecord(data) if data is not None else None )
++
++
++def getSeasonTypes():
++ path = getSeasonTypes_path.format()
++ res = _query_api(path)
++ data = res['data'] if res.get('data') is not None else None
++ return( SeasonType(data) if data is not None else None )
++
++
++def getSeasonTranslation(id, language):
++ path = getSeasonTranslation_path.format(id=str(id), language=language)
++ res = _query_api(path)
++ data = res['data'] if res.get('data') is not None else None
++ return( Translation(data) if data is not None else None )
++
++
++def getAllSeries(page=0, yielded=False):
++ params = {}
++ if page is not None:
++ params['page'] = page
++ path = getAllSeries_path.format()
++ if yielded:
++ return _query_yielded(SeriesBaseRecord, path, params, listname=None)
++ else:
++ res = _query_api(path, params)
++ data = res['data'] if res.get('data') is not None else None
++ return( [SeriesBaseRecord(x) for x in data] if data is not None else [] )
++
++
++def getSeriesBase(id):
++ path = getSeriesBase_path.format(id=str(id))
++ res = _query_api(path)
++ data = res['data'] if res.get('data') is not None else None
++ return( SeriesBaseRecord(data) if data is not None else None )
++
++
++def getSeriesExtended(id, meta=None):
++ params = {}
++ if meta is not None:
++ params['meta'] = meta
++ path = getSeriesExtended_path.format(id=str(id))
++ res = _query_api(path, params)
++ data = res['data'] if res.get('data') is not None else None
++ return( SeriesExtendedRecord(data) if data is not None else None )
++
++
++def getSeriesEpisodes(id, season_type, season=None, episodeNumber=None, airDate=None,
page=0, yielded=False):
++ params = {}
++ if season is not None:
++ params['season'] = season
++ if episodeNumber is not None:
++ params['episodeNumber'] = episodeNumber
++ if airDate is not None:
++ params['airDate'] = airDate
++ if page is not None:
++ params['page'] = page
++ path = getSeriesEpisodes_path.format(id=str(id), season_type=season_type)
++ if yielded:
++ return _query_yielded(EpisodeBaseRecord, path, params,
listname='episodes')
++ else:
++ res = _query_api(path, params)
++ data = res['data'] if res.get('data') is not None else None
++ return( SeriesBaseRecord(data['series']) if data is not None else None,
++ [EpisodeBaseRecord(x) for x in data['episodes']] if data is not
None else [] )
++
++
++def getSeriesSeasonEpisodesTranslated(id, season_type, lang, page=0, yielded=False):
++ params = {}
++ if page is not None:
++ params['page'] = page
++ path = getSeriesSeasonEpisodesTranslated_path.format(id=str(id),
season_type=season_type, lang=lang)
++ if yielded:
++ return _query_yielded(EpisodeBaseRecord, path, params,
listname='episodes')
++ else:
++ res = _query_api(path, params)
++ data = res['data'] if res.get('data') is not None else None
++ return( SeriesExtendedRecord(data['series']) if data is not None else
None,
++ [EpisodeBaseRecord(x) for x in data['episodes']] if data is not
None else [] )
++
++
++def getSeriesTranslation(id, language):
++ path = getSeriesTranslation_path.format(id=str(id), language=language)
++ res = _query_api(path)
++ data = res['data'] if res.get('data') is not None else None
++ return( Translation(data) if data is not None else None )
++
++
++def getAllSeriesStatuses():
++ path = getAllSeriesStatuses_path.format()
++ res = _query_api(path)
++ data = res['data'] if res.get('data') is not None else None
++ return( [Status(x) for x in data] if data is not None else [] )
++
++
++def getAllSourceTypes():
++ path = getAllSourceTypes_path.format()
++ res = _query_api(path)
++ data = res['data'] if res.get('data') is not None else None
++ return( [SourceType(x) for x in data] if data is not None else [] )
++
++
++def updates(since, type=None, action=None, page=0, yielded=False):
++ params = {}
++ if type is not None:
++ params['type'] = type
++ if action is not None:
++ params['action'] = action
++ if page is not None:
++ params['page'] = page
++ path = updates_path.format(since=since)
++ if yielded:
++ return _query_yielded(EntityUpdate, path, params, listname=None)
++ else:
++ res = _query_api(path, params)
++ data = res['data'] if res.get('data') is not None else None
++ return( [EntityUpdate(x) for x in data] if data is not None else [] )
++
+diff --git a/mythtv/bindings/python/ttvdbv4/utils.py
b/mythtv/bindings/python/ttvdbv4/utils.py
+new file mode 100644
+index 00000000000..b43c711afcf
+--- /dev/null
++++ b/mythtv/bindings/python/ttvdbv4/utils.py
+@@ -0,0 +1,73 @@
++# -*- coding: UTF-8 -*-
++
++# ----------------------------------------------------
++# Purpose: MythTV Python Bindings for TheTVDB v4 API
++# Copyright: (c) 2021 Roland Ernst
++# License: GPL v2 or later, see LICENSE for details
++# ----------------------------------------------------
++
++
++from datetime import datetime
++import sys
++
++
++if sys.version_info[0] == 2:
++ from HTMLParser import HTMLParser
++ from StringIO import StringIO
++
++
++ class MLStripper(HTMLParser):
++ def __init__(self):
++ self.reset()
++ self.text = StringIO()
++
++ def handle_data(self, d):
++ self.text.write(d)
++
++ def get_data(self):
++ return self.text.getvalue()
++
++else:
++ from io import StringIO
++ from html.parser import HTMLParser
++
++ class MLStripper(HTMLParser):
++ def __init__(self):
++ super().__init__()
++ self.reset()
++ self.strict = False
++ self.convert_charrefs= True
++ self.text = StringIO()
++
++ def handle_data(self, d):
++ self.text.write(d)
++
++ def get_data(self):
++ return self.text.getvalue()
++
++
++def strip_tags(html):
++ if html is not None and html != "":
++ s = MLStripper()
++ s.feed(html)
++ return s.get_data()
++ else:
++ return ""
++
++
++def convert_date(tstring):
++ if tstring is None or tstring == '':
++ return None
++ try:
++ return datetime.strptime(tstring, '%Y-%m-%d').date()
++ except(TypeError, ValueError):
++ return None
++
++
++def convert_time(tstring):
++ if tstring is None or tstring == '':
++ return None
++ try:
++ return datetime.strptime(tstring, '%Y-%m-%d')
++ except(TypeError, ValueError):
++ return None
+diff --git a/mythtv/programs/scripts/metadata/Television/ttvdb4.ini
b/mythtv/programs/scripts/metadata/Television/ttvdb4.ini
+new file mode 100644
+index 00000000000..65b96c00caa
+--- /dev/null
++++ b/mythtv/programs/scripts/metadata/Television/ttvdb4.ini
+@@ -0,0 +1,59 @@
++# -*- coding: UTF-8 -*-
++
++# ---------------------------------------
++# Author: Roland Ernst
++# Purpose: MythTV Bindings for TheTVDB v4
++# ---------------------------------------
++
++# MythTV's television grabber 'ttvdb4.py':
++# User modifiable configuration
++# Hints:
++# Option is separated from the value by a colon (:) or equal sign (=).
++# Whitespace around the separator is ignored when the file is parsed.
++
++[ConfigVersion]
++TTVDBv4ConfigVersion = 1
++
++[Authorization]
++# Put your own key in if you have one:
++# TTVDBv4Key =
++# TTVDBv4Pin =
++
++[AccessToken]
++# Cache the token for x days:
++# TheTVDB says, that it is valid for 1 month.
++Lifetime = 1
++
++[Languages]
++# Search order, preferred languages in ISO-639 Codes
++# keys must be sortable in ascending order
++Lang1 = eng
++# Lang2 = deu
++# Lang3 = fra
++# Lang4 = it
++
++[Preferences]
++# Turn off fetching cast if query takes too long:
++ShowPeople = 1
++
++[Thresholds]
++# Each entry is a floating point value of kind 'name similarity',
++# with a maximum value of '1.00' considered as exact match.
++
++# NameThreshold considers an exact match on:
++NameThreshold = 0.99
++
++# Start output if name similarity is bigger as:
++ThresholdStart = 0.60
++
++# Decrease of similarity which leads to stop the search:
++ThresholdDecrease = 0.1
++
++[TitleConversions]
++# Used to convert titles for searching TTVDB
++# MythTV Title = Original Title
++# Marvel's Agents of S H I E L D = Marvel's Agents of S.H.I.E.L.D.
++
++[InetrefConversions]
++# Assign title to inetref for better search results
++# Eleventh Hour = 83066
+diff --git a/mythtv/programs/scripts/metadata/Television/ttvdb4.py
b/mythtv/programs/scripts/metadata/Television/ttvdb4.py
+new file mode 100755
+index 00000000000..bf3c1234ed5
+--- /dev/null
++++ b/mythtv/programs/scripts/metadata/Television/ttvdb4.py
+@@ -0,0 +1,296 @@
++#!/usr/bin/env python3
++# -*- coding: UTF-8 -*-
++
++# ----------------------------------------------------
++# Purpose: MythTV Python Bindings for TheTVDB v4 API
++# Copyright: (c) 2021 Roland Ernst
++# License: GPL v2 or later, see LICENSE for details
++# ----------------------------------------------------
++
++
++import argparse
++import sys
++import os
++import shutil
++import shlex
++from pprint import pprint
++from configparser import ConfigParser
++
++
++__title__ = "TheTVDatabaseV4"
++__author__ = "Roland Ernst"
++__version__ = "0.5.0"
++
++
++def print_etree(etostr):
++ """lxml.etree.tostring is a bytes object in python3, and a str in
python2.
++ """
++ if sys.version_info[0] == 2:
++ sys.stdout.write(etostr)
++ else:
++ sys.stdout.write(etostr.decode("utf-8"))
++
++
++def _parse_config(config):
++ """ Parse the config read by ConfigParser."""
++ d = {}
++ for section in config.sections():
++ d[section] = {}
++ for k, v in config[section].items():
++ d[section][k] = v
++ return d
++
++
++def buildVersion():
++ from lxml import etree
++ version = etree.XML(u'<grabber></grabber>')
++ etree.SubElement(version, "name").text = __title__
++ etree.SubElement(version, "author").text = __author__
++ etree.SubElement(version, "thumbnail").text = 'ttvdb.png'
++ etree.SubElement(version, "command").text = 'ttvdb4.py'
++ etree.SubElement(version, "type").text = 'television'
++ etree.SubElement(version, "description").text = \
++ 'Search and downloads metadata from
TheTVDB.com (API v4)'
++ etree.SubElement(version, "version").text = __version__
++ print_etree(etree.tostring(version, encoding='UTF-8', pretty_print=True,
++ xml_declaration=True))
++ sys.exit(0)
++
++
++def performSelfTest(args):
++ err = 0
++ try:
++ import lxml
++ except:
++ err = 1
++ print("Failed to import python lxml library.")
++ try:
++ import requests
++ import requests_cache
++ except:
++ err = 1
++ print("Failed to import python-requests or python-request-cache
library.")
++ try:
++ import MythTV
++ except:
++ err = 1
++ print("Failed to import MythTV bindings. Check your `configure` output
"
++ "to make sure installation was not disabled due to external
dependencies.")
++ try:
++ from MythTV.ttvdbv4.myth4ttvdbv4 import Myth4TTVDBv4
++ from MythTV.ttvdbv4 import ttvdbv4_api as ttvdb
++ if args.debug:
++ print("TheTVDBv4 Script Version: ", __version__)
++ print("TheTVDBv4-API version: ",
ttvdb.MYTHTV_TTVDBV4_API_VERSION)
++ print("TheTVDBv4-API file location: ", ttvdb.__file__)
++ except:
++ err = 1
++ print("Failed to import Py TTVDB4 library. This should have been included
"
++ "with the python MythTV bindings.")
++ try:
++ inipath = os.path.abspath(os.path.dirname(sys.argv[0]))
++ inifile = os.path.join(inipath, "ttvdb4.ini")
++ config = ConfigParser()
++ # preserve capital letters:
++ config.optionxform = str
++ config.read(inifile, 'UTF-8')
++ config_dict = _parse_config(config)
++ config_version =
config_dict['ConfigVersion']['TTVDBv4ConfigVersion']
++ if args.debug:
++ print("Config version of 'ttvdb4.ini': ", config_version)
++ except:
++ err = 1
++ print("Failed to read the ini file 'ttvdb4.ini'. Check your
installation "
++ "if such a file exists alongside this grabber script.")
++ if not err:
++ print("Everything appears in order.")
++ sys.exit(err)
++
++
++def main():
++ """
++ Main executor for MythTV's ttvdb v4 grabber.
++ """
++ description = '''A python script to retrieve metadata for
TV-Shows.'''
++
++ parser = argparse.ArgumentParser(description=description)
++
++ parser.add_argument('-v', '--version',
action="store_true",
++ dest="version", help="Display version and
author")
++
++ parser.add_argument('-t', '--test', action="store_true",
default=False,
++ dest="test", help="Perform self-test for
dependencies.")
++
++ parser.add_argument('-l', "--language",
metavar="LANGUAGE", default=u'en',
++ dest="language", help="Specify language for
filtering.")
++
++ parser.add_argument('-a', "--area", metavar="COUNTRY",
default=None,
++ dest="country", help="Specify country for custom
data.")
++
++ group = parser.add_mutually_exclusive_group()
++
++ group.add_argument('-M', "--list", nargs=1,
++ dest="tvtitle", help="Get TV Shows matching
'tvtitle'.")
++
++ group.add_argument('-D', "--data",
metavar=("INETREF","SEASON","EPISODE"),
++ nargs=3, type=int, dest="tvdata",
++ help="Get TV-Show data for 'inetref',
'season' and 'episode.")
++
++ group.add_argument('-C', "--collection", nargs=1, type=int,
dest="collectionref",
++ help="Get Collection data for
'collectionref'.")
++
++ group.add_argument('-N', "--numbers",
metavar=("ARG0","ARG1"), nargs=2, type=str,
++ dest="tvnumbers",
++ help="Get Season and Episode numbers: "
++ "'ARG0' can be ['series-title',
'inetref'] and "
++ "'ARG1': ['episode-title',
'iso-date-time'].")
++
++ parser.add_argument('--configure', nargs='?', type=str,
default='ttvdb4.ini',
++ dest="inifile", help="Use local configuration
file, defaults to "
++
"'~/.mythtv/'ttvdb4.ini'.")
++
++ parser.add_argument('--debug', action="store_true", default=False,
dest="debug",
++ help="Disable caching and enable raw data output.")
++
++ parser.add_argument('--jsondebug', action="store_true",
default=False, dest="jsondebug",
++ help="Enable raw json data output.")
++
++ parser.add_argument('--doctest', action="store_true",
default=False, dest="doctest",
++ help="Run doctests. You need to change to the folder where
ttvdb4.py "
++ "is installed.")
++
++ args = parser.parse_args()
++
++ if args.version:
++ buildVersion()
++
++ if args.test:
++ performSelfTest(args)
++
++ # assemble arguments
++ cmd_args = vars(args)
++ if args.debug:
++ print("0000: Init: cmd_args: ", cmd_args)
++
++ # read the ini files
++ import requests
++ confdir = os.environ.get('MYTHCONFDIR', '')
++ if (not confdir) or (confdir == '/'):
++ confdir = os.environ.get('HOME', '')
++ if (not confdir) or (confdir == '/'):
++ print("Unable to find MythTV directory for grabber
initialization.")
++ sys.exit(1)
++ confdir = os.path.join(confdir, '.mythtv')
++
++ cachedir = os.path.join(confdir, 'cache')
++ if not os.path.exists(cachedir):
++ os.makedirs(cachedir)
++
++ if not args.debug and not args.doctest:
++ if sys.version_info[0] == 2:
++ cache_name = os.path.join(cachedir, 'py2ttvdb4')
++ else:
++ cache_name = os.path.join(cachedir, 'py3ttvdb4')
++ import requests_cache
++ requests_cache.install_cache(cache_name, backend='sqlite',
expire_after=3600)
++
++ # Add config from config file to cmd_args:
++ config_dict = {}
++ # read global config
++ inipath = os.path.abspath(os.path.dirname(sys.argv[0]))
++ inifile = os.path.join(inipath, "ttvdb4.ini")
++ try:
++ global_config = ConfigParser()
++ # preserve capital letters:
++ global_config.optionxform = str
++ global_config.read(inifile, 'UTF-8')
++ config_dict = _parse_config(global_config)
++ if args.debug:
++ print("0000: Init: Global Config File parsed successfully.")
++ except KeyError:
++ if args.debug:
++ print("0000: Init: Parsing Global Config File failed.")
++ # read local config, which overrides the global one
++ if args.inifile:
++ local_config_file = os.path.join(confdir, args.inifile)
++ if os.path.isfile(local_config_file):
++ try:
++ local_config = ConfigParser()
++ # preserve capital letters:
++ local_config.optionxform = str
++ local_config.read(local_config_file, 'UTF-8')
++ for section in local_config.sections():
++ for k,v in local_config[section].items():
++ config_dict[section][k] = v
++ if args.debug:
++ print("0000: Init: Local Config File '%s' parsed
successfully."
++ % local_config_file)
++ except KeyError:
++ if args.debug:
++ print("0000: Init: Parsing Local Config File failed.")
++ else:
++ # create local config with values from global config
++ shutil.copy(inifile, local_config_file)
++ if args.debug:
++ print("0000: Init: Local config file '%s' created." %
local_config_file)
++
++ # storage for authentication bearer token
++ if sys.version_info[0] == 2:
++ config_dict['auth_file'] = os.path.join(cachedir,
"py2ttvdb4_bearer.pickle")
++ else:
++ config_dict['auth_file'] = os.path.join(cachedir,
"py3ttvdb4_bearer.pickle")
++
++ cmd_args["config"] = config_dict
++ if args.debug:
++ print("0000: Init: Using this configuration:")
++ pprint(cmd_args["config"])
++
++ if args.doctest:
++ import doctest
++ try:
++ with open("ttvdb4_doctests") as f:
++ if sys.version_info[0] == 2:
++ dtests = b"".join(f.readlines()).decode('utf-8')
++ else:
++ dtests = "".join(f.readlines())
++ main.__doc__ += dtests
++ except IOError:
++ pass
++ # perhaps try optionflags=doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE
++ return doctest.testmod(verbose=args.debug, optionflags=doctest.ELLIPSIS)
++
++ # finally, grab the damn metadata
++ try:
++ from MythTV.ttvdbv4.myth4ttvdbv4 import Myth4TTVDBv4
++ from lxml import etree
++ mttvdb = Myth4TTVDBv4(**cmd_args)
++ if args.tvdata:
++ # option -D inetref season episode
++ mttvdb.buildSingle()
++ elif args.collectionref:
++ # option -C inetref
++ mttvdb.buildCollection()
++ elif args.tvtitle:
++ # option -M title
++ mttvdb.buildList()
++ elif args.tvnumbers:
++ # option -N title subtitle
++ # option -N inetref subtitle
++ mttvdb.buildNumbers()
++ else:
++ sys.stdout.write('ERROR: This script must be called with one of '
++ '[-t, -v, -C, -D, -M, -N] switches.')
++ sys.exit(1)
++
++ print_etree(etree.tostring(mttvdb.tree, encoding='UTF-8',
pretty_print=True,
++ xml_declaration=True))
++ except:
++ if args.debug:
++ raise
++ sys.stdout.write('ERROR: ' + str(sys.exc_info()[0]) + ' : '
++ + str(sys.exc_info()[1]) + '\n')
++ sys.exit(1)
++
++
++if __name__ == "__main__":
++ main()
+diff --git a/mythtv/programs/scripts/metadata/Television/ttvdb4_doctests
b/mythtv/programs/scripts/metadata/Television/ttvdb4_doctests
+new file mode 100644
+index 00000000000..42129ff0d67
+--- /dev/null
++++ b/mythtv/programs/scripts/metadata/Television/ttvdb4_doctests
+@@ -0,0 +1,576 @@
++# -*- coding: UTF-8 -*-
++
++# ---------------------------------------
++# Author: Roland Ernst
++# Purpose: MythTV Bindings for TheTVDB v4
++# ---------------------------------------
++
++# Doc-Tests for ttvdb4
++# file: ttvdb4_doctests
++
++# Note: These tests take several minutes to finish! Please be patient.
++
++# ttvdb4.py -l en -C 360893
++>>> sys.argv = shlex.split('ttvdb4 -l en -C 360893')
++>>> main()
++<?xml version='1.0' encoding='UTF-8'?>
++<metadata>
++ <item>
++ <title>Chernobyl</title>
++ <description>Chernobyl dramatizes the story of the 1986 nuclear accident — one
of the worst man-made catastrophes in history — and the sacrifices made to save Europe
from unimaginable disaster.</description>
++ <inetref>360893</inetref>
++ <
homepage>https://www.hbo.com/chernobyl/season-1</homepage>
++ <language>en</language>
++ <userrating>8.700000</userrating>
++ <year>2019</year>
++ <categories>
++ <category name="Mini-Series"/>
++ <category name="Drama"/>
++ <category name="Thriller"/>
++ <category name="History"/>
++ </categories>
++ <countries>
++ <country name="USA"/>
++ </countries>
++ <people>
++ <person name="Jared Harris" job="Actor"
url="https://thetvdb.com/people/360332" character="Valery
Legasov"/>
++ <person name="Stellan Skarsgård" job="Actor"
url="https://thetvdb.com/people/378106" character="Boris
Shcherbina"/>
++ <person name="Emily Watson" job="Actor"
url="https://thetvdb.com/people/300426" character="Ulana
Khomyuk"/>
++...
++ </people>
++ <images>
++ <image type="coverart"
url="https://artworks.thetvdb.com/banners/posters/5cc12861c93e4.jpg&...
thumb="https://artworks.thetvdb.com/banners/posters/5cc12861c93e4_t....
++ <image type="coverart"
url="https://artworks.thetvdb.com/banners/posters/5d218b09c7dc3.jpg&...
thumb="https://artworks.thetvdb.com/banners/posters/5d218b09c7dc3_t....
++ <image type="coverart"
url="https://artworks.thetvdb.com/banners/series/360893/posters/6201...
thumb="https://artworks.thetvdb.com/banners/series/360893/posters/62...
++...
++ <image type="fanart"
url="https://artworks.thetvdb.com/banners/fanart/original/5cd7e75b9e...
thumb="https://artworks.thetvdb.com/banners/fanart/original/5cd7e75b...
++ <image type="fanart"
url="https://artworks.thetvdb.com/banners/fanart/original/5cb116e858...
thumb="https://artworks.thetvdb.com/banners/fanart/original/5cb116e8...
++ <image type="fanart"
url="https://artworks.thetvdb.com/banners/fanart/original/5cbaf5ff51...
thumb="https://artworks.thetvdb.com/banners/fanart/original/5cbaf5ff...
++...
++ <image type="banner"
url="https://artworks.thetvdb.com/banners/graphical/5cc9f74c2ddd3.jp...
thumb="https://artworks.thetvdb.com/banners/graphical/5cc9f74c2ddd3_...
++ <image type="banner"
url="https://artworks.thetvdb.com/banners/graphical/5cd2c005cab09.jp...
thumb="https://artworks.thetvdb.com/banners/graphical/5cd2c005cab09_...
++ </images>
++ </item>
++</metadata>
++
++### Japanese title, but only English overview:
++# ttvdb4.py -l ja -C 360893
++>>> sys.argv = shlex.split('ttvdb4 -l ja -C 360893')
++>>> main()
++<?xml version='1.0' encoding='UTF-8'?>
++<metadata>
++ <item>
++ <title>チェルノブイリ</title>
++ <description>Chernobyl dramatizes the story of the 1986 nuclear accident — one
of the worst man-made catastrophes in history — and the sacrifices made to save Europe
from unimaginable disaster.</description>
++ <inetref>360893</inetref>
++ <
homepage>https://www.hbo.com/chernobyl/season-1</homepage>
++ <language>en</language>
++ <userrating>8.700000</userrating>
++ <year>2019</year>
++ <categories>
++ <category name="Mini-Series"/>
++ <category name="Drama"/>
++ <category name="Thriller"/>
++ <category name="History"/>
++ </categories>
++ <countries>
++ <country name="USA"/>
++ </countries>
++ <people>
++ <person name="Jared Harris" job="Actor"
url="https://thetvdb.com/people/360332" character="Valery
Legasov"/>
++ <person name="Stellan Skarsgård" job="Actor"
url="https://thetvdb.com/people/378106" character="Boris
Shcherbina"/>
++ <person name="Emily Watson" job="Actor"
url="https://thetvdb.com/people/300426" character="Ulana
Khomyuk"/>
++...
++ </people>
++ <images>
++ <image type="coverart"
url="https://artworks.thetvdb.com/banners/posters/5cc12861c93e4.jpg&...
thumb="https://artworks.thetvdb.com/banners/posters/5cc12861c93e4_t....
++ <image type="coverart"
url="https://artworks.thetvdb.com/banners/posters/5d218b09c7dc3.jpg&...
thumb="https://artworks.thetvdb.com/banners/posters/5d218b09c7dc3_t....
++...
++ <image type="fanart"
url="https://artworks.thetvdb.com/banners/fanart/original/5cd7e75b9e...
thumb="https://artworks.thetvdb.com/banners/fanart/original/5cd7e75b...
++ <image type="fanart"
url="https://artworks.thetvdb.com/banners/fanart/original/5cb116e858...
thumb="https://artworks.thetvdb.com/banners/fanart/original/5cb116e8...
++ <image type="fanart"
url="https://artworks.thetvdb.com/banners/fanart/original/5cbaf5ff51...
thumb="https://artworks.thetvdb.com/banners/fanart/original/5cbaf5ff...
++...
++ <image type="banner"
url="https://artworks.thetvdb.com/banners/graphical/5cc9f74c2ddd3.jp...
thumb="https://artworks.thetvdb.com/banners/graphical/5cc9f74c2ddd3_...
++ <image type="banner"
url="https://artworks.thetvdb.com/banners/graphical/5cd2c005cab09.jp...
thumb="https://artworks.thetvdb.com/banners/graphical/5cd2c005cab09_...
++ </images>
++ </item>
++</metadata>
++
++
++# ttvdb4.py -l en -a us -D 76568 2 8
++>>> sys.argv = shlex.split('ttvdb4 -l en -a us -D 76568 2 8')
++>>> main()
++<?xml version='1.0' encoding='UTF-8'?>
++<metadata>
++ <item>
++ <title>Gilmore Girls</title>
++ <subtitle>The Ins and Outs of Inns</subtitle>
++ <description>Lorelai and Sookie gear up their plans to open an inn but
encounter a roadblock when the owner of their dream location refuses to sell the property.
The unexpected arrival of Mia, the absentee owner of the Independence Inn, complicates
matters even further.</description>
++ <season>2</season>
++ <episode>8</episode>
++ <inetref>76568</inetref>
++ <imdb>tt0588208</imdb>
++ <
homepage>https://www.warnerbros.com/tv/gilmore-girls</homepage>
++ <language>en</language>
++ <userrating>9.000000</userrating>
++ <year>2000</year>
++ <runtime>44</runtime>
++ <certifications>
++ <certification locale="us" name="TV-14"/>
++ </certifications>
++ <categories>
++ <category name="Drama"/>
++ <category name="Comedy"/>
++ <category name="Romance"/>
++ </categories>
++ <countries>
++ <country name="USA"/>
++ </countries>
++ <studios>
++ <studio name="The WB"/>
++ </studios>
++ <people>
++ <person name="Lauren Graham" job="Actor"
url="https://thetvdb.com/people/255106" character="Lorelai
Gilmore"/>
++ <person name="Alexis Bledel" job="Actor"
url="https://thetvdb.com/people/384680" character="Rory Gilmore"/>
++ <person name="Melissa McCarthy" job="Actor"
url="https://thetvdb.com/people/296532" character="Sookie St.
James"/>
++...
++ </people>
++ <images>
++ <image type="coverart"
url="https://artworks.thetvdb.com/banners/series/76568/seasons/10755...
thumb="https://artworks.thetvdb.com/banners/series/76568/seasons/107...
++ <image type="coverart"
url="https://artworks.thetvdb.com/banners/seasons/76568-2-4.jpg"
thumb="https://artworks.thetvdb.com/banners/seasons/76568-2-4_t.jpg&...
++ <image type="coverart"
url="https://artworks.thetvdb.com/banners/seasons/76568-2-3.jpg"
thumb="https://artworks.thetvdb.com/banners/seasons/76568-2-3_t.jpg&...
++...
++ <image type="fanart"
url="https://artworks.thetvdb.com/banners/fanart/original/76568-14.j...
thumb="https://artworks.thetvdb.com/banners/fanart/original/76568-14...
++ <image type="fanart"
url="https://artworks.thetvdb.com/banners/fanart/original/76568-15.j...
thumb="https://artworks.thetvdb.com/banners/fanart/original/76568-15...
++ <image type="fanart"
url="https://artworks.thetvdb.com/banners/fanart/original/76568-18.j...
thumb="https://artworks.thetvdb.com/banners/fanart/original/76568-18...
++...
++ <image type="banner"
url="https://artworks.thetvdb.com/banners/graphical/76568-g.jpg"
thumb="https://artworks.thetvdb.com/banners/graphical/76568-g_t.jpg&...
++ <image type="banner"
url="https://artworks.thetvdb.com/banners/text/76568-3.jpg"
thumb="https://artworks.thetvdb.com/banners/text/76568-3_t.jpg"...
++ <image type="banner"
url="https://artworks.thetvdb.com/banners/graphical/44-g.jpg"
thumb="https://artworks.thetvdb.com/banners/graphical/44-g_t.jpg&quo...
++...
++ <image type="screenshot"
url="https://artworks.thetvdb.com/banners/episodes/76568/200299.jpg&...
++ </images>
++ </item>
++</metadata>
++
++
++# ttvdb4.py -l de -D 76568 3 9
++>>> sys.argv = shlex.split('ttvdb4 -l de -D 76568 3 9')
++>>> main()
++<?xml version='1.0' encoding='UTF-8'?>
++<metadata>
++ <item>
++ <title>Gilmore Girls</title>
++ <subtitle>Stress hoch vier</subtitle>
++ <description>Rory und Lorelai haben mehrere Einladungen zu Thanksgiving. Wie
jedes Jahr wollen sie zuerst zu den Kims, danach zu Sookie und zu guter Letzt zu Luke.
Doch dann taucht Emily auf und besteht darauf, dass die beiden auch bei ihr und Richard
vorbeikommen. Rory und Lorelai bleibt nichts anderes übrig, als an Thanksgiving vier
Einladungen zu bewältigen.Nach kurzen Besuchen bei den Kims, bei Sookie und Jackson und
bei Luke machen sich Lorelai und Rory auf den Weg nach Hartford. Dort erfährt Lorelai,
dass Rory sich in Yale beworben hat, und glaubt nun, Richard und Emily hätten Rory
manipuliert. Erbost nimmt sie ihre Tochter an die Hand und verlässt die
Party.</description>
++ <season>3</season>
++ <episode>9</episode>
++ <inetref>76568</inetref>
++ <imdb>tt0588112</imdb>
++ <
homepage>https://www.warnerbros.com/tv/gilmore-girls</homepage>
++ <language>en</language>
++ <userrating>9.000000</userrating>
++ <year>2000</year>
++ <runtime>41</runtime>
++ <categories>
++ <category name="Drama"/>
++ <category name="Comedy"/>
++ <category name="Romance"/>
++ </categories>
++ <countries>
++ <country name="USA"/>
++ </countries>
++ <studios>
++ <studio name="The WB"/>
++ </studios>
++ <people>
++ <person name="Lauren Graham" job="Actor"
url="https://thetvdb.com/people/255106" character="Lorelai
Gilmore"/>
++ <person name="Alexis Bledel" job="Actor"
url="https://thetvdb.com/people/384680" character="Rory Gilmore"/>
++ <person name="Melissa McCarthy" job="Actor"
url="https://thetvdb.com/people/296532" character="Sookie St.
James"/>
++...
++ </people>
++ <images>
++ <image type="coverart"
url="https://artworks.thetvdb.com/banners/seasons/76568-3-3.jpg"
thumb="https://artworks.thetvdb.com/banners/seasons/76568-3-3_t.jpg&...
++ <image type="coverart"
url="https://artworks.thetvdb.com/banners/series/76568/seasons/10756...
thumb="https://artworks.thetvdb.com/banners/series/76568/seasons/107...
++ <image type="coverart"
url="https://artworks.thetvdb.com/banners/seasons/76568-3-7.jpg"
thumb="https://artworks.thetvdb.com/banners/seasons/76568-3-7_t.jpg&...
++...
++ <image type="fanart"
url="https://artworks.thetvdb.com/banners/fanart/original/76568-14.j...
thumb="https://artworks.thetvdb.com/banners/fanart/original/76568-14...
++ <image type="fanart"
url="https://artworks.thetvdb.com/banners/fanart/original/76568-15.j...
thumb="https://artworks.thetvdb.com/banners/fanart/original/76568-15...
++ <image type="fanart"
url="https://artworks.thetvdb.com/banners/fanart/original/76568-18.j...
thumb="https://artworks.thetvdb.com/banners/fanart/original/76568-18...
++...
++ <image type="banner"
url="https://artworks.thetvdb.com/banners/graphical/76568-g4.jpg"
thumb="https://artworks.thetvdb.com/banners/graphical/76568-g4_t.jpg...
++ <image type="banner"
url="https://artworks.thetvdb.com/banners/graphical/76568-g.jpg"
thumb="https://artworks.thetvdb.com/banners/graphical/76568-g_t.jpg&...
++ <image type="banner"
url="https://artworks.thetvdb.com/banners/text/76568-3.jpg"
thumb="https://artworks.thetvdb.com/banners/text/76568-3_t.jpg"...
++...
++ <image type="screenshot"
url="https://artworks.thetvdb.com/banners/episodes/76568/200322.jpg&...
++ </images>
++ </item>
++</metadata>
++
++
++# Single match with year:
++# ttvdb4.py -l en -M "Hawaii Five-0 (2010)"
++>>> sys.argv = shlex.split('ttvdb4 -l en -M "Hawaii Five-0
(2010)"')
++>>> main()
++<?xml version='1.0' encoding='UTF-8'?>
++<metadata>
++ <item>
++ <title>Hawaii Five-0</title>
++ <description>Detective Steve McGarrett, a decorated Naval officer turned cop,
returned to Oahu after Hawaii's former governor persuaded him to head up the new team:
his rules, no red tape and full blanket authority to hunt down the biggest
"game" in town. Joining McGarrett are Detective Danny "Danno"
Williams, a relocated ex-New Jersey cop; Captain Lou Grover, who formerly headed
Hawaii's SWAT unit; Jerry Ortega, the islands' local conspiracy theorist; and Tani
Rey, a bold, recent police academy graduate. Helping them is Adam Noshimuri, a friend with
old ties to a deadly crime family; Junior Reigns, a former SEAL; Kamekona, a local
entrepreneur who has his pulse on the Island; Sgt. Duke Lukela, a trusted member of the
HPD; and medical examiner Dr. Noelani Cunha. The state's brash Five-0 unit, who may
spar and jest among themselves, remain determined to eliminate the seedy elements from the
50th state.</description>
++ <inetref>164541</inetref>
++ <language>en</language>
++ <userrating>8.100000</userrating>
++ <year>2010</year>
++ <categories>
++ <category name="Drama"/>
++ <category name="Crime"/>
++ <category name="Action"/>
++ </categories>
++ <countries>
++ <country name="USA"/>
++ </countries>
++ <people>
++ <person name="Alex O'Loughlin" job="Actor"
url="https://thetvdb.com/people/334781" character="Steve
McGarrett"/>
++ <person name="Scott Caan" job="Actor"
url="https://thetvdb.com/people/327319" character="Danny
"Danno" Williams"/>
++ <person name="Daniel Dae Kim" job="Actor"
url="https://thetvdb.com/people/268546" character="Chin Ho
Kelly"/>
++ <person name="Grace Park" job="Actor"
url="https://thetvdb.com/people/295725" character="Kono
Kalakaua"/>
++...
++ </people>
++ <images>
++ <image type="coverart"
url="https://artworks.thetvdb.com/banners/posters/164541-1.jpg"
thumb="https://artworks.thetvdb.com/banners/posters/164541-1_t.jpg&q...
++ <image type="coverart"
url="https://artworks.thetvdb.com/banners/posters/164541-8.jpg"
thumb="https://artworks.thetvdb.com/banners/posters/164541-8_t.jpg&q...
++ <image type="coverart"
url="https://artworks.thetvdb.com/banners/posters/164541-10.jpg"
thumb="https://artworks.thetvdb.com/banners/posters/164541-10_t.jpg&...
++ <image type="coverart"
url="https://artworks.thetvdb.com/banners/posters/164541-5.jpg"
thumb="https://artworks.thetvdb.com/banners/posters/164541-5_t.jpg&q...
++...
++ <image type="fanart"
url="https://artworks.thetvdb.com/banners/fanart/original/164541-2.j...
thumb="https://artworks.thetvdb.com/banners/fanart/original/164541-2...
++ <image type="fanart"
url="https://artworks.thetvdb.com/banners/fanart/original/164541-19....
thumb="https://artworks.thetvdb.com/banners/fanart/original/164541-1...
++ <image type="fanart"
url="https://artworks.thetvdb.com/banners/fanart/original/164541-4.j...
thumb="https://artworks.thetvdb.com/banners/fanart/original/164541-4...
++ <image type="fanart"
url="https://artworks.thetvdb.com/banners/fanart/original/164541-3.j...
thumb="https://artworks.thetvdb.com/banners/fanart/original/164541-3...
++...
++ <image type="banner"
url="https://artworks.thetvdb.com/banners/graphical/164541-g3.jpg"
thumb="https://artworks.thetvdb.com/banners/graphical/164541-g3_t.jp...
++ <image type="banner"
url="https://artworks.thetvdb.com/banners/graphical/164541-g5.jpg"
thumb="https://artworks.thetvdb.com/banners/graphical/164541-g5_t.jp...
++ <image type="banner"
url="https://artworks.thetvdb.com/banners/graphical/164541-g4.jpg"
thumb="https://artworks.thetvdb.com/banners/graphical/164541-g4_t.jp...
++ <image type="banner"
url="https://artworks.thetvdb.com/banners/graphical/164541-g8.jpg"
thumb="https://artworks.thetvdb.com/banners/graphical/164541-g8_t.jp...
++...
++ </images>
++ </item>
++</metadata>
++
++
++# Single match:
++# ttvdb4.py -l de -M "Hawaii Five-O"
++>>> sys.argv = shlex.split('ttvdb4 -l de -M "Hawaii
Five-O"')
++>>> main()
++<?xml version='1.0' encoding='UTF-8'?>
++<metadata>
++ <item>
++ <title>Hawaii Fünf-Null</title>
++ <description>Steve McGarrett ist der Leiter einer Polizei-Spezialeinheit,
deren Aufgabe es ist, gegen das organisierte Verbrechen auf der Insel Hawaii vorzugehen.
Dabei sind er und sein Assistent Danny Williams direkt dem Gouverneur von Hawaii
unterstellt. Bei ihren Ermittlungen und Einsätzen werden sie durch Kollegen von der
Polizei von Honolulu unterstützt.</description>
++ <inetref>71223</inetref>
++ <language>en</language>
++ <userrating>8.500000</userrating>
++ <year>1968</year>
++ <categories>
++ <category name="Drama"/>
++ <category name="Crime"/>
++ <category name="Adventure"/>
++ <category name="Action"/>
++ <category name="Mystery"/>
++ </categories>
++ <countries>
++ <country name="USA"/>
++ </countries>
++ <people>
++ <person name="Peggy Ryan" job="Actor"
url="https://thetvdb.com/people/287834" character="Jenny"/>
++ <person name="William Smith" job="Actor"
url="https://thetvdb.com/people/256501"/>
++ <person name="James MacArthur" job="Actor"
url="https://thetvdb.com/people/263175" character="Det. Danny
Williams"/>
++ <person name="Morgan White" job="Actor"
url="https://thetvdb.com/people/287835"/>
++...
++ </people>
++ <images>
++ <image type="coverart"
url="https://artworks.thetvdb.com/banners/posters/71223-1.jpg"
thumb="https://artworks.thetvdb.com/banners/posters/71223-1_t.jpg&qu...
++ <image type="coverart"
url="https://artworks.thetvdb.com/banners/posters/71223-2.jpg"
thumb="https://artworks.thetvdb.com/banners/posters/71223-2_t.jpg&qu...
++ <image type="coverart"
url="https://artworks.thetvdb.com/banners/posters/71223-3.jpg"
thumb="https://artworks.thetvdb.com/banners/posters/71223-3_t.jpg&qu...
++ <image type="fanart"
url="https://artworks.thetvdb.com/banners/fanart/original/71223-2.jp...
thumb="https://artworks.thetvdb.com/banners/fanart/original/71223-2_...
++ <image type="fanart"
url="https://artworks.thetvdb.com/banners/fanart/original/71223-3.jp...
thumb="https://artworks.thetvdb.com/banners/fanart/original/71223-3_...
++ <image type="fanart"
url="https://artworks.thetvdb.com/banners/fanart/original/71223-1.jp...
thumb="https://artworks.thetvdb.com/banners/fanart/original/71223-1_...
++...
++ <image type="banner"
url="https://artworks.thetvdb.com/banners/graphical/71223-g.jpg"
thumb="https://artworks.thetvdb.com/banners/graphical/71223-g_t.jpg&...
++ <image type="banner"
url="https://artworks.thetvdb.com/banners/graphical/1277-g.jpg"
thumb="https://artworks.thetvdb.com/banners/graphical/1277-g_t.jpg&q...
++ <image type="banner"
url="https://artworks.thetvdb.com/banners/graphical/71223-g2.jpg"
thumb="https://artworks.thetvdb.com/banners/graphical/71223-g2_t.jpg...
++...
++ </images>
++ </item>
++</metadata>
++
++
++# This takes long:
++# ttvdb4.py -l en -N 76568 "The Road Trip to Harvard"
++>>> sys.argv = shlex.split('ttvdb4 -l en -N 76568 "The Road Trip to
Harvard"')
++>>> main()
++<?xml version='1.0' encoding='UTF-8'?>
++<metadata>
++ <item>
++ <title>Gilmore Girls</title>
++ <subtitle>The Road Trip to Harvard</subtitle>
++ <description>To escape everyone's reaction to the news of the broken
engagement, Lorelai and Rory hit the road and stumble upon the bed and breakfast from
hell. Rory gets a taste of college life while visiting Harvard, and Lorelai muses over
what might have been. Lorelai decides to get serious about making her dream of opening an
inn a reality.</description>
++ <season>2</season>
++ <episode>4</episode>
++ <inetref>76568</inetref>
++ <imdb>tt0588216</imdb>
++ <
homepage>https://www.warnerbros.com/tv/gilmore-girls</homepage>
++ <language>en</language>
++ <userrating>9.000000</userrating>
++ <year>2000</year>
++ <runtime>43</runtime>
++ <categories>
++ <category name="Drama"/>
++ <category name="Comedy"/>
++ <category name="Romance"/>
++ </categories>
++ <countries>
++ <country name="USA"/>
++ </countries>
++ <studios>
++ <studio name="The WB"/>
++ </studios>
++ <people>
++ <person name="Lauren Graham" job="Actor"
url="https://thetvdb.com/people/255106" character="Lorelai
Gilmore"/>
++ <person name="Alexis Bledel" job="Actor"
url="https://thetvdb.com/people/384680" character="Rory Gilmore"/>
++ <person name="Melissa McCarthy" job="Actor"
url="https://thetvdb.com/people/296532" character="Sookie St.
James"/>
++ <person name="Keiko Agena" job="Actor"
url="https://thetvdb.com/people/267049" character="Lane Kim"/>
++...
++ </people>
++ <images>
++ <image type="coverart"
url="https://artworks.thetvdb.com/banners/series/76568/seasons/10755...
thumb="https://artworks.thetvdb.com/banners/series/76568/seasons/107...
++ <image type="coverart"
url="https://artworks.thetvdb.com/banners/seasons/76568-2-4.jpg"
thumb="https://artworks.thetvdb.com/banners/seasons/76568-2-4_t.jpg&...
++ <image type="coverart"
url="https://artworks.thetvdb.com/banners/seasons/76568-2-3.jpg"
thumb="https://artworks.thetvdb.com/banners/seasons/76568-2-3_t.jpg&...
++ <image type="coverart"
url="https://artworks.thetvdb.com/banners/seasons/76568-2-5.jpg"
thumb="https://artworks.thetvdb.com/banners/seasons/76568-2-5_t.jpg&...
++...
++ <image type="fanart"
url="https://artworks.thetvdb.com/banners/fanart/original/76568-14.j...
thumb="https://artworks.thetvdb.com/banners/fanart/original/76568-14...
++ <image type="fanart"
url="https://artworks.thetvdb.com/banners/fanart/original/76568-15.j...
thumb="https://artworks.thetvdb.com/banners/fanart/original/76568-15...
++ <image type="fanart"
url="https://artworks.thetvdb.com/banners/fanart/original/76568-18.j...
thumb="https://artworks.thetvdb.com/banners/fanart/original/76568-18...
++ <image type="fanart"
url="https://artworks.thetvdb.com/banners/fanart/original/76568-23.j...
thumb="https://artworks.thetvdb.com/banners/fanart/original/76568-23...
++...
++ <image type="banner"
url="https://artworks.thetvdb.com/banners/graphical/76568-g.jpg"
thumb="https://artworks.thetvdb.com/banners/graphical/76568-g_t.jpg&...
++ <image type="banner"
url="https://artworks.thetvdb.com/banners/text/76568-3.jpg"
thumb="https://artworks.thetvdb.com/banners/text/76568-3_t.jpg"...
++ <image type="banner"
url="https://artworks.thetvdb.com/banners/graphical/44-g.jpg"
thumb="https://artworks.thetvdb.com/banners/graphical/44-g_t.jpg&quo...
++ <image type="banner"
url="https://artworks.thetvdb.com/banners/graphical/76568-g3.jpg"
thumb="https://artworks.thetvdb.com/banners/graphical/76568-g3_t.jpg...
++...
++ <image type="screenshot"
url="https://artworks.thetvdb.com/banners/episodes/76568/200295.jpg&...
++ </images>
++ </item>
++</metadata>
++
++
++# Multiple episodes with the same name:
++# ttvdb4.py -l en -N "The Forsyte Saga" "Episode 1"
++>>> sys.argv = shlex.split('ttvdb4 -l en -N "The Forsyte Saga"
"Episode 1"')
++>>> main()
++<?xml version='1.0' encoding='UTF-8'?>
++<metadata>
++ <item>
++ <title>The Forsyte Saga (2002)</title>
++ <subtitle>Episode 1</subtitle>
++ <description>The Forsyte family gathers to celebrate the engagement of
Winifred Forsythe to Montague Dartie. Soames Forsyte, a rich and successful partner in the
family law firm, meets penniless but beautiful and accomplished Irene Heron. Young Jolyon,
who is in love with his children’s French governess, Helene, decides to leave his wife,
cutting himself off from the family and forfeiting his fortune.</description>
++ <season>1</season>
++ <episode>1</episode>
++ <inetref>81610</inetref>
++ <language>en</language>
++ <userrating>7.700000</userrating>
++ <year>2002</year>
++ <runtime>55</runtime>
++ <categories>
++ <category name="Drama"/>
++ <category name="Romance"/>
++ </categories>
++ <countries>
++ <country name="GBR"/>
++ </countries>
++ <studios>
++ <studio name="ITV"/>
++ </studios>
++ <people>
++ <person name="Damian Lewis" job="Actor"
url="https://thetvdb.com/people/327374" character="Soames
Forsyte"/>
++ <person name="Rupert Graves" job="Actor"
url="https://thetvdb.com/people/258611" character="Jolyon Forsyte
Jr."/>
++ <person name="Gina McKee" job="Actor"
url="https://thetvdb.com/people/260544" character="Irene
Forsyte"/>
++...
++ </people>
++ <images>
++ <image type="coverart"
url="https://artworks.thetvdb.com/banners/seasons/81610-1.jpg"
thumb="https://artworks.thetvdb.com/banners/seasons/81610-1_t.jpg&qu...
++ <image type="coverart"
url="https://artworks.thetvdb.com/banners/seasons/81610-1-3.jpg"
thumb="https://artworks.thetvdb.com/banners/seasons/81610-1-3_t.jpg&...
++ <image type="coverart"
url="https://artworks.thetvdb.com/banners/seasons/81610-1-4.jpg"
thumb="https://artworks.thetvdb.com/banners/seasons/81610-1-4_t.jpg&...
++ <image type="coverart"
url="https://artworks.thetvdb.com/banners/seasons/81610-1-2.jpg"
thumb="https://artworks.thetvdb.com/banners/seasons/81610-1-2_t.jpg&...
++...
++ <image type="fanart"
url="https://artworks.thetvdb.com/banners/fanart/original/81610-1.jp...
thumb="https://artworks.thetvdb.com/banners/fanart/original/81610-1_...
++ <image type="fanart"
url="https://artworks.thetvdb.com/banners/fanart/original/81610-3.jp...
thumb="https://artworks.thetvdb.com/banners/fanart/original/81610-3_...
++ <image type="fanart"
url="https://artworks.thetvdb.com/banners/fanart/original/81610-2.jp...
thumb="https://artworks.thetvdb.com/banners/fanart/original/81610-2_...
++ <image type="fanart"
url="https://artworks.thetvdb.com/banners/fanart/original/5d1d1c2975...
thumb="https://artworks.thetvdb.com/banners/fanart/original/5d1d1c29...
++...
++ <image type="banner"
url="https://artworks.thetvdb.com/banners/graphical/81610-g.jpg"
thumb="https://artworks.thetvdb.com/banners/graphical/81610-g_t.jpg&...
++ <image type="banner"
url="https://artworks.thetvdb.com/banners/graphical/81610-g2.jpg"
thumb="https://artworks.thetvdb.com/banners/graphical/81610-g2_t.jpg...
++ <image type="screenshot"
url="https://artworks.thetvdb.com/banners/episodes/81610/358149.jpg&...
++ </images>
++ </item>
++ <item>
++ <title>The Forsyte Saga (2002)</title>
++ <subtitle>Episode 1</subtitle>
++ <description>The Forsyte family, including Soames and Annette’s daughter,
Fleur, gathers in London when June arrives with her half-brother Jon (son of young Jolyon
and Irene). Eleven years later, in 1920, Fleur and Jon meet again, unaware of their family
connection.</description>
++ <season>2</season>
++ <episode>1</episode>
++ <inetref>81610</inetref>
++ <language>en</language>
++ <userrating>7.700000</userrating>
++ <year>2002</year>
++ <runtime>55</runtime>
++ <categories>
++ <category name="Drama"/>
++ <category name="Romance"/>
++ </categories>
++ <countries>
++ <country name="GBR"/>
++ </countries>
++ <studios>
++ <studio name="ITV"/>
++ </studios>
++ <people>
++ <person name="Damian Lewis" job="Actor"
url="https://thetvdb.com/people/327374" character="Soames
Forsyte"/>
++ <person name="Rupert Graves" job="Actor"
url="https://thetvdb.com/people/258611" character="Jolyon Forsyte
Jr."/>
++ <person name="Gina McKee" job="Actor"
url="https://thetvdb.com/people/260544" character="Irene
Forsyte"/>
++...
++ </people>
++ <images>
++ <image type="coverart"
url="https://artworks.thetvdb.com/banners/seasons/81610-2.jpg"
thumb="https://artworks.thetvdb.com/banners/seasons/81610-2_t.jpg&qu...
++ <image type="coverart"
url="https://artworks.thetvdb.com/banners/seasons/81610-2-2.jpg"
thumb="https://artworks.thetvdb.com/banners/seasons/81610-2-2_t.jpg&...
++ <image type="coverart"
url="https://artworks.thetvdb.com/banners/seasons/81610-2-3.jpg"
thumb="https://artworks.thetvdb.com/banners/seasons/81610-2-3_t.jpg&...
++ <image type="coverart"
url="https://artworks.thetvdb.com/banners/seasons/81610-2-4.jpg"
thumb="https://artworks.thetvdb.com/banners/seasons/81610-2-4_t.jpg&...
++...
++ <image type="fanart"
url="https://artworks.thetvdb.com/banners/fanart/original/81610-1.jp...
thumb="https://artworks.thetvdb.com/banners/fanart/original/81610-1_...
++ <image type="fanart"
url="https://artworks.thetvdb.com/banners/fanart/original/81610-3.jp...
thumb="https://artworks.thetvdb.com/banners/fanart/original/81610-3_...
++ <image type="fanart"
url="https://artworks.thetvdb.com/banners/fanart/original/81610-2.jp...
thumb="https://artworks.thetvdb.com/banners/fanart/original/81610-2_...
++ <image type="fanart"
url="https://artworks.thetvdb.com/banners/fanart/original/5d1d1c2975...
thumb="https://artworks.thetvdb.com/banners/fanart/original/5d1d1c29...
++...
++ <image type="banner"
url="https://artworks.thetvdb.com/banners/graphical/81610-g.jpg"
thumb="https://artworks.thetvdb.com/banners/graphical/81610-g_t.jpg&...
++ <image type="banner"
url="https://artworks.thetvdb.com/banners/graphical/81610-g2.jpg"
thumb="https://artworks.thetvdb.com/banners/graphical/81610-g2_t.jpg...
++ <image type="screenshot"
url="https://artworks.thetvdb.com/banners/episodes/81610/358157.jpg&...
++ </images>
++ </item>
++</metadata>
++
++
++# This takes really long:
++# ttvdb4.py -l de -N "Die Munsters" "Der Liebestrank"
++>>> sys.argv = shlex.split('ttvdb4 -l de -N "Die Munsters"
"Der Liebestrank"')
++>>> main()
++<?xml version='1.0' encoding='UTF-8'?>
++<metadata>
++ <item>
++ <title>Die Munsters</title>
++ <subtitle>Der Liebestrank</subtitle>
++ <description>Wieder einmal packt einen Liebhaber von Marilyn Munster das kalte
Grausen, als er erstmals einem Familienmitglied der Munsters begegnet. Diesmal ist es
Herman, der Marilyns Lover zu Tode erschreckt. Der Familienrat beschließt, daß Marilyn mit
der irgendetwas nicht in Ordnung zu sein scheint schleunigst an den Mann gebracht werden
muß. Um dem Plan gezielten Nachdruck zu verleihen, braut Opa Munster einen wirkungsvollen
Liebestrank. Opas Flirt Cocktail hat Folgen, mit denen keiner gerechnet hat. Sein
magischer Saft verleiht allen, die ihn zu sich nehmen, eine unwiderstehliche
Anziehungskraft...</description>
++ <season>1</season>
++ <episode>2</episode>
++ <inetref>77280</inetref>
++ <language>en</language>
++ <userrating>8.300000</userrating>
++ <year>1964</year>
++ <runtime>25</runtime>
++ <categories>
++ <category name="Horror"/>
++ <category name="Fantasy"/>
++ <category name="Family"/>
++ <category name="Drama"/>
++ <category name="Comedy"/>
++ </categories>
++ <countries>
++ <country name="USA"/>
++ </countries>
++ <studios>
++ <studio name="CBS"/>
++ </studios>
++ <people>
++ <person name="Beverly Owen" job="Actor"
url="https://thetvdb.com/people/7994017" character="Marilyn
Munster"/>
++ <person name="Butch Patrick" job="Actor"
url="https://thetvdb.com/people/270064" character="Eddie
Munster"/>
++ <person name="Al Lewis" job="Actor"
url="https://thetvdb.com/people/257438" character="Grandpa"/>
++ <person name="Yvonne De Carlo" job="Actor"
url="https://thetvdb.com/people/353794" character="Lily Munster"/>
++ <person name="Pat Priest" job="Actor"
url="https://thetvdb.com/people/265126" character="Marilyn
Munster"/>
++ <person name="Fred Gwynne" job="Actor"
url="https://thetvdb.com/people/297141" character="Herman
Munster"/>
++ <person name="John Fiedler" job="Guest Star"
url="https://thetvdb.com/people/256476"/>
++ <person name="Claire Carleton" job="Guest Star"
url="https://thetvdb.com/people/259938"/>
++ <person name="Edward Mallory" job="Guest Star"
url="https://thetvdb.com/people/264854"/>
++ </people>
++ <images>
++ <image type="coverart"
url="https://artworks.thetvdb.com/banners/seasons/77280-1-3.jpg"
thumb="https://artworks.thetvdb.com/banners/seasons/77280-1-3_t.jpg&...
++ <image type="coverart"
url="https://artworks.thetvdb.com/banners/seasons/77280-1.jpg"
thumb="https://artworks.thetvdb.com/banners/seasons/77280-1_t.jpg&qu...
++ <image type="coverart"
url="https://artworks.thetvdb.com/banners/seasons/77280-1-2.jpg"
thumb="https://artworks.thetvdb.com/banners/seasons/77280-1-2_t.jpg&...
++ <image type="coverart"
url="https://artworks.thetvdb.com/banners/seasons/77280-1-4.jpg"
thumb="https://artworks.thetvdb.com/banners/seasons/77280-1-4_t.jpg&...
++...
++ <image type="fanart"
url="https://artworks.thetvdb.com/banners/fanart/original/77280-4.jp...
thumb="https://artworks.thetvdb.com/banners/fanart/original/77280-4_...
++ <image type="fanart"
url="https://artworks.thetvdb.com/banners/fanart/original/77280-5.jp...
thumb="https://artworks.thetvdb.com/banners/fanart/original/77280-5_...
++ <image type="fanart"
url="https://artworks.thetvdb.com/banners/fanart/original/77280-7.jp...
thumb="https://artworks.thetvdb.com/banners/fanart/original/77280-7_...
++ <image type="fanart"
url="https://artworks.thetvdb.com/banners/fanart/original/77280-1.jp...
thumb="https://artworks.thetvdb.com/banners/fanart/original/77280-1_...
++...
++ <image type="banner"
url="https://artworks.thetvdb.com/banners/graphical/589-g.jpg"
thumb="https://artworks.thetvdb.com/banners/graphical/589-g_t.jpg&qu...
++ <image type="banner"
url="https://artworks.thetvdb.com/banners/text/589.jpg"
thumb="https://artworks.thetvdb.com/banners/text/589_t.jpg"/>
++ <image type="screenshot"
url="https://artworks.thetvdb.com/banners/episodes/77280/236307.jpg&...
++ </images>
++ </item>
++</metadata>
++
++
++# Series title with multiple dots, which gets removed during scanning for updates:
++# ttvdb4.py -l en -a us -N "Once Upon a Time Life" "The Blood"
++>>> sys.argv = shlex.split('ttvdb4 -l en -a us -N "Once Upon a Time
Life" "The Blood"')
++>>> main()
++<?xml version='1.0' encoding='UTF-8'?>
++<metadata>
++ <item>
++ <title>Once Upon a Time... Life</title>
++ <subtitle>The Blood</subtitle>
++ <description>An exciting and interesting journey through the history of
mankind, which begins with the creation of the world, the first living cell and
evolution.</description>
++ <season>1</season>
++ <episode>5</episode>
++ <inetref>79158</inetref>
++ <language>fr</language>
++ <userrating>8.600000</userrating>
++ <year>1987</year>
++ <runtime>25</runtime>
++ <certifications>
++ <certification locale="us" name="TV-Y"/>
++ </certifications>
++ <categories>
++ <category name="Family"/>
++ <category name="Children"/>
++ <category name="Animation"/>
++ </categories>
++ <countries>
++ <country name="FRA"/>
++ </countries>
++ <studios>
++ <studio name="France 3"/>
++ </studios>
++ <people>
++ <person name="Roger Carel" job="Actor"
url="https://thetvdb.com/people/318920"/>
++ <person name="Gilles Laurent" job="Actor"
url="https://thetvdb.com/people/335829"/>
++ <person name="Gilles Tamiz" job="Actor"
url="https://thetvdb.com/people/447047"/>
++ </people>
++ <images>
++ <image type="coverart"
url="https://artworks.thetvdb.com/banners/seasons/79158-1-10.jpg"
thumb="https://artworks.thetvdb.com/banners/seasons/79158-1-10_t.jpg...
++ <image type="coverart"
url="https://artworks.thetvdb.com/banners/seasons/79158-1.jpg"
thumb="https://artworks.thetvdb.com/banners/seasons/79158-1_t.jpg&qu...
++ <image type="coverart"
url="https://artworks.thetvdb.com/banners/seasons/79158-1-5.jpg"
thumb="https://artworks.thetvdb.com/banners/seasons/79158-1-5_t.jpg&...
++ <image type="coverart"
url="https://artworks.thetvdb.com/banners/seasons/79158-1-9.jpg"
thumb="https://artworks.thetvdb.com/banners/seasons/79158-1-9_t.jpg&...
++...
++ <image type="fanart"
url="https://artworks.thetvdb.com/banners/fanart/original/79158-1.jp...
thumb="https://artworks.thetvdb.com/banners/fanart/original/79158-1_...
++ <image type="fanart"
url="https://artworks.thetvdb.com/banners/fanart/original/79158-2.jp...
thumb="https://artworks.thetvdb.com/banners/fanart/original/79158-2_...
++ <image type="fanart"
url="https://artworks.thetvdb.com/banners/fanart/original/79158-3.jp...
thumb="https://artworks.thetvdb.com/banners/fanart/original/79158-3_...
++ <image type="fanart"
url="https://artworks.thetvdb.com/banners/fanart/original/79158-4.jp...
thumb="https://artworks.thetvdb.com/banners/fanart/original/79158-4_...
++...
++ <image type="banner"
url="https://artworks.thetvdb.com/banners/graphical/79158-g10.jpg"
thumb="https://artworks.thetvdb.com/banners/graphical/79158-g10_t.jp...
++ <image type="banner"
url="https://artworks.thetvdb.com/banners/graphical/e8537n-g.jpg"
thumb="https://artworks.thetvdb.com/banners/graphical/e8537n-g_t.jpg...
++ <image type="banner"
url="https://artworks.thetvdb.com/banners/graphical/e8537n-g2.jpg"
thumb="https://artworks.thetvdb.com/banners/graphical/e8537n-g2_t.jp...
++ <image type="banner"
url="https://artworks.thetvdb.com/banners/graphical/79158-g.jpg"
thumb="https://artworks.thetvdb.com/banners/graphical/79158-g_t.jpg&...
++...
++ <image type="screenshot"
url="https://artworks.thetvdb.com/banners/episodes/79158/316495.jpg&...
++ </images>
++ </item>
++</metadata>
+
+From 9c53e17d754b50ac8ef4e02552620a1332fba718 Mon Sep 17 00:00:00 2001
+From: Roland Ernst <rcrernst(a)gmail.com>
+Date: Sun, 21 Nov 2021 21:22:48 +0100
+Subject: [PATCH 167/171] Fix time and date handling on upgraded MariaDB
+
+In MariaDB 10.1.2 a new temporal format was introduced from
+MySQL 5.6 that alters how the `TIME`, `DATETIME` and `TIMESTAMP`
+columns operate at lower levels. These changes allow these temporal
+data types to have fractional parts and negative values.
+You can disable this feature using the `mysql56_temporal_format`
+system variable.
+
+Starting from MariaDB 10.5.1 columns with old temporal formats are
+marked with a `/* mariadb-5.3 */` comment in the output of
+`SHOW CREATE TABLE`, `SHOW COLUMNS`, `DESCRIBE` statements, as well
+as in the `COLUMN_TYPE` column of the `INFORMATION_SCHEMA.COLUMNS`
+Table.
+
+Since the python bindings use the `DESCRIBE` statement to identify
+the type of an MySQL entry, the comment must be stripped off.
+
+This happens on old MariaDB databases upgraded automatically
+during system upgrade, e.g.: Debian 10 -> 11.
+
+Ref:
https://mariadb.com/kb/en/datetime/
+
+Fixes #384
+
+(cherry picked from commit 645ad05a3ecccb7ceee3cce4897bd6b6aae6d94c)
+---
+ mythtv/bindings/python/MythTV/database.py | 5 +++--
+ 1 file changed, 3 insertions(+), 2 deletions(-)
+
+diff --git a/mythtv/bindings/python/MythTV/database.py
b/mythtv/bindings/python/MythTV/database.py
+index e8b8bf5d52d..d35a77ad472 100644
+--- a/mythtv/bindings/python/MythTV/database.py
++++ b/mythtv/bindings/python/MythTV/database.py
+@@ -1119,10 +1119,11 @@ def __str__(self): return str(list(self))
+ def __repr__(self): return py23_repr(str(self))
+ def __iter__(self): return self.iterkeys()
+ def __init__(self, result):
++ # remove comments in 'type' like "datetime /* mariadb-5.3
*/"
+ data = [(row[0],
+ OrdDict(zip( \
+-
('type','null','key','default','extra'),
+- row[1:])) \
++
('type','null','key','default','extra'),
++ (row[1].split('/*', 1)[0].rstrip(), ) + row[2:])) \
+ )for row in result]
+ OrdDict.__init__(self, data)
+ def __getitem__(self,key):
+
+From 96d1a1952536c6ca019a0240332f0a899672f78e Mon Sep 17 00:00:00 2001
+From: Roland Ernst <rcrernst(a)gmail.com>
+Date: Sun, 21 Nov 2021 22:11:40 +0100
+Subject: [PATCH 168/171] TV grabber ttvdb.py fails with newer versions of
+
+python package request-cache.
+
+This was reported on the forum, it throws this
+Error: (module 'requests_cache' has no attribute 'core')
+
+It turned out, that the monkey patch provided by
+`mythtv/bindings/python/MythTV/ttvdb/requests_cache_compatability.py`
+is only valid for versions of request-cache < '0.5.0'. Newer versions
+do not need this patch and versions beyond '0.8.0' crashes with above
+error message.
+Note: The module requests_cache.core was removed in version '0.8.0'
+on Sept. 2021 and deprecated in version '0.6.0' in April, 2021.
+
+Refs #408
+
+(cherry picked from commit da3f00653588813fc225eafa146d975b26986819)
+---
+ .../ttvdb/requests_cache_compatability.py | 73 ++++++++++---------
+ 1 file changed, 38 insertions(+), 35 deletions(-)
+
+diff --git a/mythtv/bindings/python/MythTV/ttvdb/requests_cache_compatability.py
b/mythtv/bindings/python/MythTV/ttvdb/requests_cache_compatability.py
+index 248a9b1d0c3..bf75864564e 100644
+--- a/mythtv/bindings/python/MythTV/ttvdb/requests_cache_compatability.py
++++ b/mythtv/bindings/python/MythTV/ttvdb/requests_cache_compatability.py
+@@ -12,38 +12,41 @@
+ import requests_cache
+ from datetime import datetime, timedelta
+
+-# patch if required if older versions of
+-try:
+- requests_cache.backends.base.BaseCache.remove_old_entries
+-except Exception as e:
+- def remove_old_entries(self, created_before):
+- """ Deletes entries from cache with creation time older than
``created_before``
+- """
+- keys_to_delete = set()
+- for key in self.responses:
+- try:
+- response, created_at = self.responses[key]
+- except KeyError:
+- continue
+- if created_at < created_before:
+- keys_to_delete.add(key)
+-
+- for key in keys_to_delete:
+- self.delete(key)
+-
+-
+- def remove_expired_responses(self):
+- """ Removes expired responses from storage
+- """
+- if not self._cache_expire_after:
+- return
+- # just in case expire_after is not converted in the
+- # original constructor, convert it to a timedelta
+- if not isinstance(self._cache_expire_after, timedelta):
+- self._cache_expire_after = timedelta(seconds=self._cache_expire_after)
+-
+- self.cache.remove_old_entries(datetime.utcnow() - self._cache_expire_after)
+-
+-
+- requests_cache.backends.base.BaseCache.remove_old_entries = remove_old_entries
+- requests_cache.core.CachedSession.remove_expired_responses =
remove_expired_responses
++rc_version_info = tuple(int(x) for x in requests_cache.__version__.split('.'))
++# patch if required if older versions than '0.5.0'
++# request_cache.core is deprecated in '0.6.0' and removed on '0.8.0'.
++if rc_version_info < (0, 5, 0):
++ try:
++ requests_cache.backends.base.BaseCache.remove_old_entries
++ except Exception as e:
++ def remove_old_entries(self, created_before):
++ """ Deletes entries from cache with creation time older than
``created_before``
++ """
++ keys_to_delete = set()
++ for key in self.responses:
++ try:
++ response, created_at = self.responses[key]
++ except KeyError:
++ continue
++ if created_at < created_before:
++ keys_to_delete.add(key)
++
++ for key in keys_to_delete:
++ self.delete(key)
++
++
++ def remove_expired_responses(self):
++ """ Removes expired responses from storage
++ """
++ if not self._cache_expire_after:
++ return
++ # just in case expire_after is not converted in the
++ # original constructor, convert it to a timedelta
++ if not isinstance(self._cache_expire_after, timedelta):
++ self._cache_expire_after = timedelta(seconds=self._cache_expire_after)
++
++ self.cache.remove_old_entries(datetime.utcnow() - self._cache_expire_after)
++
++
++ requests_cache.backends.base.BaseCache.remove_old_entries = remove_old_entries
++ requests_cache.core.CachedSession.remove_expired_responses =
remove_expired_responses
+
+From 5b46d518a5987a94d27718c122f2a1f07d6e3543 Mon Sep 17 00:00:00 2001
+From: Roland Ernst <rcrernst(a)gmail.com>
+Date: Wed, 15 Dec 2021 19:07:18 +0100
+Subject: [PATCH 169/171] Fix error with python3.10 on MythTV/msearch.py
+
+Traceback (most recent call last):
+ File "/.../MythTV/msearch.py", line 35, in __init__
+ self.sock.setblocking(0.1)
+TypeError: 'float' object cannot be interpreted as an integer
+
+Python's socket implementation allows to set the blocking mode with
+ - setblocking([0/1] or
+ - settimeout(float) # i.e '0.0'
+
+Prior to python3.10, 'setblocking' evaluated the float value to an integer,
+which results in this case to zero (tested with 'socket.gettimeout'()).
+
+Make it clear, that we want the non-blocking mode.
+
+Tested with python3.6 and 3.10.
+
+(cherry picked from commit e57584bab7d25dc3438b7661c3cc33f5d92b4a13)
+---
+ mythtv/bindings/python/MythTV/msearch.py | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/mythtv/bindings/python/MythTV/msearch.py
b/mythtv/bindings/python/MythTV/msearch.py
+index b884679ce21..99d14a277a7 100644
+--- a/mythtv/bindings/python/MythTV/msearch.py
++++ b/mythtv/bindings/python/MythTV/msearch.py
+@@ -32,7 +32,7 @@ def __init__(self):
+ raise MythError(MythError.SOCKET, e)
+ self.log(MythLog.UPNP|MythLog.SOCKET, MythLog.DEBUG,
+ 'Port %d opened for UPnP search.' % port)
+- self.sock.setblocking(0.1)
++ self.sock.setblocking(0)
+
+ def __del__(self):
+ self.sock.close()
+
+From 23738a70743aef39e6e47c1b78c1fa8e9ec03a7a Mon Sep 17 00:00:00 2001
+From: Roland Ernst <rcrernst(a)gmail.com>
+Date: Wed, 15 Dec 2021 22:43:09 +0100
+Subject: [PATCH 170/171] Python Bindings: Fix uncaught exception in
+ 'deadlinesocket'
+
+Python Bindings throws an error if the socket does not provide data:
+Traceback (most recent call last):
+....
+File "/usr/lib/python3/dist-packages/MythTV/utility/other.py", line 374, in
dlexpect
+raise MythError(MythError.CLOSEDSOCKET)
+AttributeError: type object 'MythError' has no attribute 'CLOSEDSOCKET'
+
+Solution:
+Align the error code to the methods provided by MythError.SOCKET.
+
+Fixes #423
+
+(cherry picked from commit 1e6246da6845370938eee6f75ecbf915559f7448)
+---
+ mythtv/bindings/python/MythTV/utility/other.py | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/mythtv/bindings/python/MythTV/utility/other.py
b/mythtv/bindings/python/MythTV/utility/other.py
+index 7f5b0c759c8..3e9dcc9e9d8 100644
+--- a/mythtv/bindings/python/MythTV/utility/other.py
++++ b/mythtv/bindings/python/MythTV/utility/other.py
+@@ -371,7 +371,7 @@ def dlexpect(self, pattern, flags=0, deadline=None):
+ raise MythError(MythError.SOCKET, e.args)
+ if buff.tell() == p:
+ # no data read from a 'ready' socket, connection terminated
+- raise MythError(MythError.CLOSEDSOCKET)
++ raise MythError(MythError.SOCKET, (54, 'Connection reset by
peer'))
+
+ if timeout == 0:
+ break
+
+From 4f7953f6ee1bc83fdb3e45dc4dadb8ea1108990f Mon Sep 17 00:00:00 2001
+From: Bill Meek <billmeek(a)mythtv.org>
+Date: Thu, 20 Jan 2022 10:49:46 -0600
+Subject: [PATCH 171/171] HTTPServer: Remove Allowed Origin
+
http://chromecast.mythtvcast.com
+
+From: a1e72a209bfa9056c7a26a34ee3a701d1808f76b, but only one file
+was affected.
+
+Refs: #453
+---
+ mythtv/libs/libmythupnp/httprequest.cpp | 3 +--
+ 1 file changed, 1 insertion(+), 2 deletions(-)
+
+diff --git a/mythtv/libs/libmythupnp/httprequest.cpp
b/mythtv/libs/libmythupnp/httprequest.cpp
+index f9d4d652de7..ddd387670de 100644
+--- a/mythtv/libs/libmythupnp/httprequest.cpp
++++ b/mythtv/libs/libmythupnp/httprequest.cpp
+@@ -2300,8 +2300,7 @@ void HTTPRequest::AddCORSHeaders( const QString &sOrigin )
+
+ QStringList allowedOriginsList =
+ gCoreContext->GetSetting("AllowedOriginsList", QString(
+- "https://chromecast.mythtv.org,"
+- "http://chromecast.mythtvcast.com"
++ "https://chromecast.mythtv.org"
+ )).split(",");
+
+ for (QStringList::const_iterator it = allowedOriginsList.begin();