Repository: yt-dlp/yt-dlp

Issue:
[salttv] broken extractor (zattoo clone) 
### DO NOT REMOVE OR SKIP THE ISSUE TEMPLATE

- [X] I understand that I will be **blocked** if I remove or skip any mandatory\* field

### Checklist

- [X] I'm reporting a bug unrelated to a specific site
- [X] I've verified that I'm running yt-dlp version **2022.08.08** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
- [X] I've checked that all provided URLs are playable in a browser with the same IP and same login details
- [X] I've checked that all URLs and arguments with special characters are [properly quoted or escaped](https://github.com/ytdl-org/youtube-dl#video-url-contains-an-ampersand-and-im-getting-some-strange-output-1-2839-or-v-is-not-recognized-as-an-internal-or-external-command)
- [X] I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar issues **including closed ones**. DO NOT post duplicates
- [X] I've read the [guidelines for opening an issue](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#opening-an-issue)

### Provide a description that is worded well enough to be understood

First of all, thanks a lot to @goggle and team for the recent fixes, Zattoo looks fine now!   The issue here is about tv.salt.ch which is a rebranded zattoo.com: here the extractor doesn't seem to work at all (anymore). 


For a specific stream from today, zattoo download is working, Salt-TV (tv.salt.ch) is not.   For example for this stream it is ok with the Zattoo Extractor: 

```
$ yt-dlp --netrc https://zattoo.com/watch/telezueri/263168737
Zattoo] Downloading session token
[Zattoo] Opening session
[Zattoo] Logging in
[Zattoo] 263168737: Downloading video information (...)
```

but it fails with for the same show on Salt TV: 

```
$ yt-dlp --netrc https://tv.salt.ch/watch/telezueri/263052803
[SaltTV] Downloading session token
[SaltTV] Opening session
[SaltTV] Logging in
ERROR: This method must be implemented by subclasses
```

It is not a login issue (it then fails with a "Unable to login" message).      Not sure why this happens on SaltTv, as everything else seems to be the same as on the zattoo frontend. 

Do you have an idea how to fix this ?  I will also try 2-3 things the next days or weeks.  
Thanks & best regards. 



PS: Other small thing (possibly for another ticket), recorded shows extraction doesn't seem to work yet, or not always: 

```
$ yt-dlp --netrc https://tv.salt.ch/recording/463479573
[generic] 463479573: Downloading webpage
WARNING: [generic] Falling back on generic information extractor
[generic] 463479573: Extracting information
ERROR: Unsupported URL: https://tv.salt.ch/recording/463479573

$ yt-dlp --netrc https://zattoo.com/recording/444173552
[generic] 444173552: Downloading webpage
WARNING: [generic] Falling back on generic information extractor
[generic] 444173552: Extracting information
ERROR: Unsupported URL: https://zattoo.com/recording/444173552
```

but interesting enough, when switching to the another url scheme, it then works (stream from May 2022): 
```
yt-dlp --netrc https://zattoo.com/series/france-2/103863/2?recording=444173552
[ZattooRecordings] Downloading session token
[ZattooRecordings] Opening session
[ZattooRecordings] Logging in
[ZattooRecordings] 444173552: Downloading playlist
[ZattooRecordings] 254811212: Downloading video information (...)
```




### Provide verbose output that clearly demonstrates the problem

- [X] Run **your** yt-dlp command with **-vU** flag added (`yt-dlp -vU <your command line>`)
- [X] Copy the WHOLE output (starting with `[debug] Command-line config`) and insert it below

### Complete Verbose Output

```shell
$ yt-dlp -vU --netrc https://tv.salt.ch/watch/telezueri/263052803
[debug] Command-line config: ['-vU', '--netrc', 'https://tv.salt.ch/watch/telezueri/263052803']
[debug] Encodings: locale UTF-8, fs utf-8, pref UTF-8, out utf-8, error utf-8, screen utf-8
[debug] yt-dlp version 2022.08.08 [3157158] (pip)
[debug] Python 3.10.6 (CPython 64bit) - macOS-12.5-x86_64-i386-64bit 
[debug] Checking exe version: ffmpeg -bsfs
[debug] Checking exe version: ffprobe -bsfs
[debug] exe versions: ffmpeg 5.1 (setts), ffprobe 5.1, rtmpdump 2.4
[debug] Optional libraries: Cryptodome-3.15.0, brotli-1.0.9, certifi-2022.06.15, mutagen-1.45.1, sqlite3-2.6.0, websockets-10.3
[debug] Proxy map: {}
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest
Latest version: 2022.08.08, Current version: 2022.08.08
yt-dlp is up to date (2022.08.08)
[SaltTV] Downloading session token
[SaltTV] Opening session
[SaltTV] Logging in
[debug] [SaltTV] Extracting URL: https://tv.salt.ch/watch/telezueri/263052803
ERROR: This method must be implemented by subclasses
Traceback (most recent call last):
  File "/usr/local/Cellar/yt-dlp/2022.8.8/libexec/lib/python3.10/site-packages/yt_dlp/YoutubeDL.py", line 1441, in wrapper
    return func(self, *args, **kwargs)
  File "/usr/local/Cellar/yt-dlp/2022.8.8/libexec/lib/python3.10/site-packages/yt_dlp/YoutubeDL.py", line 1517, in __extract_info
    ie_result = ie.extract(url)
  File "/usr/local/Cellar/yt-dlp/2022.8.8/libexec/lib/python3.10/site-packages/yt_dlp/extractor/common.py", line 666, in extract
    ie_result = self._real_extract(url)
  File "/usr/local/Cellar/yt-dlp/2022.8.8/libexec/lib/python3.10/site-packages/yt_dlp/extractor/common.py", line 739, in _real_extract
    raise NotImplementedError('This method must be implemented by subclasses')
NotImplementedError: This method must be implemented by subclasses
```



Discussion:
@omueller If you or someone else can provide account credentials for Salt TV, i can have a look (you find my email on my profile page).

I do not expect any of the Zattoo resellers to work because I could never test any of them... but it's good to hear that at least the login seems to work.
The first issue should be fixed now. Pls confirm

Re-opening for the the URL pattern issue. But next time, make sure to open separate issues
Thanks for the patch and the ticket updates!  Now after patching the zattoo.py file manually, a new error appears:  `ERROR: no such group`.  

Full output:
```
$ yt-dlp -vU --netrc https://tv.salt.ch/watch/telezueri/263052803
[debug] Command-line config: ['-vU', '--netrc', 'https://tv.salt.ch/watch/telezueri/263052803']
[debug] Encodings: locale UTF-8, fs utf-8, pref UTF-8, out utf-8, error utf-8, screen utf-8
[debug] yt-dlp version 2022.08.08 [3157158] (pip)
[debug] Python 3.10.6 (CPython 64bit) - macOS-12.5-x86_64-i386-64bit 
[debug] Checking exe version: ffmpeg -bsfs
[debug] Checking exe version: ffprobe -bsfs
[debug] exe versions: ffmpeg 5.1 (setts), ffprobe 5.1, rtmpdump 2.4
[debug] Optional libraries: Cryptodome-3.15.0, brotli-1.0.9, certifi-2022.06.15, mutagen-1.45.1, sqlite3-2.6.0, websockets-10.3
[debug] Proxy map: {}
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest
Latest version: 2022.08.08, Current version: 2022.08.08
yt-dlp is up to date (2022.08.08)
[SaltTV] Downloading session token
[SaltTV] Opening session
[SaltTV] Logging in
[debug] [SaltTV] Extracting URL: https://tv.salt.ch/watch/telezueri/263052803
ERROR: no such group
Traceback (most recent call last):
  File "/usr/local/Cellar/yt-dlp/2022.8.8/libexec/lib/python3.10/site-packages/yt_dlp/YoutubeDL.py", line 1441, in wrapper
    return func(self, *args, **kwargs)
  File "/usr/local/Cellar/yt-dlp/2022.8.8/libexec/lib/python3.10/site-packages/yt_dlp/YoutubeDL.py", line 1517, in __extract_info
    ie_result = ie.extract(url)
  File "/usr/local/Cellar/yt-dlp/2022.8.8/libexec/lib/python3.10/site-packages/yt_dlp/extractor/common.py", line 666, in extract
    ie_result = self._real_extract(url)
  File "/usr/local/Cellar/yt-dlp/2022.8.8/libexec/lib/python3.10/site-packages/yt_dlp/extractor/zattoo.py", line 241, in _real_extract
    vid1, vid2 = self._match_valid_url(url).group('vid1', 'vid2')
IndexError: no such group
```

It seems I was too hasty with the patch. Will need account details to properly test and fix

Contact: discord (pukkandan#4207) / reddit (/u/pukkandan) / email (pukkandan.ytdlp@gmail.com)
> It seems I was too hasty with the patch. Will need account details to properly test and fix

@pukkandan: Thanks & done (to you and @goggle). Also available for any testing whenever needed.

I can't access the content due to geo-blocking

Resolving patch:
diff --git a/yt_dlp/extractor/_extractors.py b/yt_dlp/extractor/_extractors.py
--- a/yt_dlp/extractor/_extractors.py
+++ b/yt_dlp/extractor/_extractors.py
@@ -2200,17 +2200,41 @@
 from .zapiks import ZapiksIE
 from .zattoo import (
     BBVTVIE,
+    BBVTVLiveIE,
+    BBVTVRecordingsIE,
     EinsUndEinsTVIE,
+    EinsUndEinsTVLiveIE,
+    EinsUndEinsTVRecordingsIE,
     EWETVIE,
+    EWETVLiveIE,
+    EWETVRecordingsIE,
     GlattvisionTVIE,
+    GlattvisionTVLiveIE,
+    GlattvisionTVRecordingsIE,
     MNetTVIE,
-    NetPlusIE,
+    MNetTVLiveIE,
+    MNetTVRecordingsIE,
+    NetPlusTVIE,
+    NetPlusTVLiveIE,
+    NetPlusTVRecordingsIE,
     OsnatelTVIE,
+    OsnatelTVLiveIE,
+    OsnatelTVRecordingsIE,
     QuantumTVIE,
+    QuantumTVLiveIE,
+    QuantumTVRecordingsIE,
     SaltTVIE,
+    SaltTVLiveIE,
+    SaltTVRecordingsIE,
     SAKTVIE,
+    SAKTVLiveIE,
+    SAKTVRecordingsIE,
     VTXTVIE,
+    VTXTVLiveIE,
+    VTXTVRecordingsIE,
     WalyTVIE,
+    WalyTVLiveIE,
+    WalyTVRecordingsIE,
     ZattooIE,
     ZattooLiveIE,
     ZattooMoviesIE,
diff --git a/yt_dlp/extractor/zattoo.py b/yt_dlp/extractor/zattoo.py
--- a/yt_dlp/extractor/zattoo.py
+++ b/yt_dlp/extractor/zattoo.py
@@ -236,32 +236,24 @@ def _extract_ondemand(self, ondemand_id):
 
     def _real_extract(self, url):
         video_id, record_id = self._match_valid_url(url).groups()
-        return self._extract_video(video_id, record_id)
+        return getattr(self, f'_extract_{self._TYPE}')(video_id or record_id)
 
 
-def _make_valid_url(host):
-    return rf'https?://(?:www\.)?{re.escape(host)}/watch/[^/]+?/(?P<id>[0-9]+)[^/]+(?:/(?P<recid>[0-9]+))?'
+def _create_valid_url(host, match, qs, base_re=None):
+    match_base = fr'|{base_re}/(?P<vid1>{match})' if base_re else '(?P<vid1>)'
+    return rf'''(?x)https?://(?:www\.)?{re.escape(host)}/(?:
+        [^?#]+\?(?:[^#]+&)?{qs}=(?P<vid2>{match})
+        {match_base}
+    )'''
 
 
 class ZattooBaseIE(ZattooPlatformBaseIE):
     _NETRC_MACHINE = 'zattoo'
     _HOST = 'zattoo.com'
 
-    @staticmethod
-    def _create_valid_url(match, qs, base_re=None):
-        match_base = fr'|{base_re}/(?P<vid1>{match})' if base_re else '(?P<vid1>)'
-        return rf'''(?x)https?://(?:www\.)?zattoo\.com/(?:
-            [^?#]+\?(?:[^#]+&)?{qs}=(?P<vid2>{match})
-            {match_base}
-        )'''
-
-    def _real_extract(self, url):
-        vid1, vid2 = self._match_valid_url(url).group('vid1', 'vid2')
-        return getattr(self, f'_extract_{self._TYPE}')(vid1 or vid2)
-
 
 class ZattooIE(ZattooBaseIE):
-    _VALID_URL = ZattooBaseIE._create_valid_url(r'\d+', 'program', '(?:program|watch)/[^/]+')
+    _VALID_URL = _create_valid_url(ZattooBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
     _TYPE = 'video'
     _TESTS = [{
         'url': 'https://zattoo.com/program/zdf/250170418',
@@ -288,7 +280,7 @@ class ZattooIE(ZattooBaseIE):
 
 
 class ZattooLiveIE(ZattooBaseIE):
-    _VALID_URL = ZattooBaseIE._create_valid_url(r'[^/?&#]+', 'channel', 'live')
+    _VALID_URL = _create_valid_url(ZattooBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
     _TYPE = 'live'
     _TESTS = [{
         'url': 'https://zattoo.com/channels/german?channel=srf_zwei',
@@ -304,7 +296,7 @@ def suitable(cls, url):
 
 
 class ZattooMoviesIE(ZattooBaseIE):
-    _VALID_URL = ZattooBaseIE._create_valid_url(r'\w+', 'movie_id', 'vod/movies')
+    _VALID_URL = _create_valid_url(ZattooBaseIE._HOST, r'\w+', 'movie_id', 'vod/movies')
     _TYPE = 'ondemand'
     _TESTS = [{
         'url': 'https://zattoo.com/vod/movies/7521',
@@ -316,7 +308,7 @@ class ZattooMoviesIE(ZattooBaseIE):
 
 
 class ZattooRecordingsIE(ZattooBaseIE):
-    _VALID_URL = ZattooBaseIE._create_valid_url(r'\d+', 'recording')
+    _VALID_URL = _create_valid_url('zattoo.com', r'\d+', 'recording')
     _TYPE = 'record'
     _TESTS = [{
         'url': 'https://zattoo.com/recordings?recording=193615508',
@@ -327,139 +319,547 @@ class ZattooRecordingsIE(ZattooBaseIE):
     }]
 
 
-class NetPlusIE(ZattooPlatformBaseIE):
-    _NETRC_MACHINE = 'netplus'
+class NetPlusTVBaseIE(ZattooPlatformBaseIE):
+    _NETRC = 'netplus'
     _HOST = 'netplus.tv'
     _API_HOST = 'www.%s' % _HOST
-    _VALID_URL = _make_valid_url(_HOST)
 
+
+class NetPlusTVIE(NetPlusTVBaseIE):
+    _VALID_URL = _create_valid_url(NetPlusTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
+    _TYPE = 'video'
+    _TESTS = [{
+        'url': 'https://netplus.tv/program/daserste/210177916',
+        'only_matching': True,
+    }, {
+        'url': 'https://netplus.tv/guide/german?channel=srf1&program=169860555',
+        'only_matching': True,
+    }]
+
+
+class NetPlusTVLiveIE(NetPlusTVBaseIE):
+    _VALID_URL = _create_valid_url(NetPlusTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
+    _TYPE = 'live'
     _TESTS = [{
-        'url': 'https://www.netplus.tv/watch/abc/123-abc',
+        'url': 'https://netplus.tv/channels/german?channel=srf_zwei',
+        'only_matching': True,
+    }, {
+        'url': 'https://netplus.tv/live/srf1',
         'only_matching': True,
     }]
 
+    @classmethod
+    def suitable(cls, url):
+        return False if NetPlusTVIE.suitable(url) else super().suitable(url)
+
 
-class MNetTVIE(ZattooPlatformBaseIE):
+class NetPlusTVRecordingsIE(NetPlusTVBaseIE):
+    _VALID_URL = _create_valid_url(NetPlusTVBaseIE._HOST, r'\d+', 'recording')
+    _TYPE = 'record'
+    _TESTS = [{
+        'url': 'https://netplus.tv/recordings?recording=193615508',
+        'only_matching': True,
+    }, {
+        'url': 'https://netplus.tv/tc/ptc_recordings_all_recordings?recording=193615420',
+        'only_matching': True,
+    }]
+
+
+class MNetTVBaseIE(ZattooPlatformBaseIE):
     _NETRC_MACHINE = 'mnettv'
     _HOST = 'tvplus.m-net.de'
-    _VALID_URL = _make_valid_url(_HOST)
 
+
+class MNetTVIE(MNetTVBaseIE):
+    _VALID_URL = _create_valid_url(MNetTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
+    _TYPE = 'video'
+    _TESTS = [{
+        'url': 'https://tvplus.m-net.de/program/daserste/210177916',
+        'only_matching': True,
+    }, {
+        'url': 'https://tvplus.m-net.de/guide/german?channel=srf1&program=169860555',
+        'only_matching': True,
+    }]
+
+
+class MNetTVLiveIE(MNetTVBaseIE):
+    _VALID_URL = _create_valid_url(MNetTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
+    _TYPE = 'live'
+    _TESTS = [{
+        'url': 'https://tvplus.m-net.de/channels/german?channel=srf_zwei',
+        'only_matching': True,
+    }, {
+        'url': 'https://tvplus.m-net.de/live/srf1',
+        'only_matching': True,
+    }]
+
+    @classmethod
+    def suitable(cls, url):
+        return False if MNetTVIE.suitable(url) else super().suitable(url)
+
+
+class MNetTVRecordingsIE(MNetTVBaseIE):
+    _VALID_URL = _create_valid_url(MNetTVBaseIE._HOST, r'\d+', 'recording')
+    _TYPE = 'record'
     _TESTS = [{
-        'url': 'https://tvplus.m-net.de/watch/abc/123-abc',
+        'url': 'https://tvplus.m-net.de/recordings?recording=193615508',
+        'only_matching': True,
+    }, {
+        'url': 'https://tvplus.m-net.de/tc/ptc_recordings_all_recordings?recording=193615420',
         'only_matching': True,
     }]
 
 
-class WalyTVIE(ZattooPlatformBaseIE):
+class WalyTVBaseIE(ZattooPlatformBaseIE):
     _NETRC_MACHINE = 'walytv'
     _HOST = 'player.waly.tv'
-    _VALID_URL = _make_valid_url(_HOST)
 
+
+class WalyTVIE(WalyTVBaseIE):
+    _VALID_URL = _create_valid_url(WalyTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
+    _TYPE = 'video'
+    _TESTS = [{
+        'url': 'https://player.waly.tv/program/daserste/210177916',
+        'only_matching': True,
+    }, {
+        'url': 'https://player.waly.tv/guide/german?channel=srf1&program=169860555',
+        'only_matching': True,
+    }]
+
+
+class WalyTVLiveIE(WalyTVBaseIE):
+    _VALID_URL = _create_valid_url(WalyTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
+    _TYPE = 'live'
+    _TESTS = [{
+        'url': 'https://player.waly.tv/channels/german?channel=srf_zwei',
+        'only_matching': True,
+    }, {
+        'url': 'https://player.waly.tv/live/srf1',
+        'only_matching': True,
+    }]
+
+    @classmethod
+    def suitable(cls, url):
+        return False if WalyTVIE.suitable(url) else super().suitable(url)
+
+
+class WalyTVRecordingsIE(WalyTVBaseIE):
+    _VALID_URL = _create_valid_url(WalyTVBaseIE._HOST, r'\d+', 'recording')
+    _TYPE = 'record'
     _TESTS = [{
-        'url': 'https://player.waly.tv/watch/abc/123-abc',
+        'url': 'https://player.waly.tv/recordings?recording=193615508',
+        'only_matching': True,
+    }, {
+        'url': 'https://player.waly.tv/tc/ptc_recordings_all_recordings?recording=193615420',
         'only_matching': True,
     }]
 
 
-class BBVTVIE(ZattooPlatformBaseIE):
+class BBVTVBaseIE(ZattooPlatformBaseIE):
     _NETRC_MACHINE = 'bbvtv'
     _HOST = 'bbv-tv.net'
     _API_HOST = 'www.%s' % _HOST
-    _VALID_URL = _make_valid_url(_HOST)
 
+
+class BBVTVIE(BBVTVBaseIE):
+    _VALID_URL = _create_valid_url(BBVTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
+    _TYPE = 'video'
+    _TESTS = [{
+        'url': 'https://bbv-tv.net/program/daserste/210177916',
+        'only_matching': True,
+    }, {
+        'url': 'https://bbv-tv.net/guide/german?channel=srf1&program=169860555',
+        'only_matching': True,
+    }]
+
+
+class BBVTVLiveIE(BBVTVBaseIE):
+    _VALID_URL = _create_valid_url(BBVTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
+    _TYPE = 'live'
     _TESTS = [{
-        'url': 'https://www.bbv-tv.net/watch/abc/123-abc',
+        'url': 'https://bbv-tv.net/channels/german?channel=srf_zwei',
+        'only_matching': True,
+    }, {
+        'url': 'https://bbv-tv.net/live/srf1',
         'only_matching': True,
     }]
 
+    @classmethod
+    def suitable(cls, url):
+        return False if BBVTVIE.suitable(url) else super().suitable(url)
 
-class VTXTVIE(ZattooPlatformBaseIE):
+
+class BBVTVRecordingsIE(BBVTVBaseIE):
+    _VALID_URL = _create_valid_url(BBVTVBaseIE._HOST, r'\d+', 'recording')
+    _TYPE = 'record'
+    _TESTS = [{
+        'url': 'https://bbv-tv.net/recordings?recording=193615508',
+        'only_matching': True,
+    }, {
+        'url': 'https://bbv-tv.net/tc/ptc_recordings_all_recordings?recording=193615420',
+        'only_matching': True,
+    }]
+
+
+class VTXTVBaseIE(ZattooPlatformBaseIE):
     _NETRC_MACHINE = 'vtxtv'
     _HOST = 'vtxtv.ch'
     _API_HOST = 'www.%s' % _HOST
-    _VALID_URL = _make_valid_url(_HOST)
 
+
+class VTXTVIE(VTXTVBaseIE):
+    _VALID_URL = _create_valid_url(VTXTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
+    _TYPE = 'video'
+    _TESTS = [{
+        'url': 'https://vtxtv.ch/program/daserste/210177916',
+        'only_matching': True,
+    }, {
+        'url': 'https://vtxtv.ch/guide/german?channel=srf1&program=169860555',
+        'only_matching': True,
+    }]
+
+
+class VTXTVLiveIE(VTXTVBaseIE):
+    _VALID_URL = _create_valid_url(VTXTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
+    _TYPE = 'live'
+    _TESTS = [{
+        'url': 'https://vtxtv.ch/channels/german?channel=srf_zwei',
+        'only_matching': True,
+    }, {
+        'url': 'https://vtxtv.ch/live/srf1',
+        'only_matching': True,
+    }]
+
+    @classmethod
+    def suitable(cls, url):
+        return False if VTXTVIE.suitable(url) else super().suitable(url)
+
+
+class VTXTVRecordingsIE(VTXTVBaseIE):
+    _VALID_URL = _create_valid_url(VTXTVBaseIE._HOST, r'\d+', 'recording')
+    _TYPE = 'record'
     _TESTS = [{
-        'url': 'https://www.vtxtv.ch/watch/abc/123-abc',
+        'url': 'https://vtxtv.ch/recordings?recording=193615508',
+        'only_matching': True,
+    }, {
+        'url': 'https://vtxtv.ch/tc/ptc_recordings_all_recordings?recording=193615420',
         'only_matching': True,
     }]
 
 
-class GlattvisionTVIE(ZattooPlatformBaseIE):
+class GlattvisionTVBaseIE(ZattooPlatformBaseIE):
     _NETRC_MACHINE = 'glattvisiontv'
     _HOST = 'iptv.glattvision.ch'
-    _VALID_URL = _make_valid_url(_HOST)
 
+
+class GlattvisionTVIE(GlattvisionTVBaseIE):
+    _VALID_URL = _create_valid_url(GlattvisionTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
+    _TYPE = 'video'
     _TESTS = [{
-        'url': 'https://iptv.glattvision.ch/watch/abc/123-abc',
+        'url': 'https://iptv.glattvision.ch/program/daserste/210177916',
+        'only_matching': True,
+    }, {
+        'url': 'https://iptv.glattvision.ch/guide/german?channel=srf1&program=169860555',
         'only_matching': True,
     }]
 
 
-class SAKTVIE(ZattooPlatformBaseIE):
+class GlattvisionTVLiveIE(GlattvisionTVBaseIE):
+    _VALID_URL = _create_valid_url(GlattvisionTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
+    _TYPE = 'live'
+    _TESTS = [{
+        'url': 'https://iptv.glattvision.ch/channels/german?channel=srf_zwei',
+        'only_matching': True,
+    }, {
+        'url': 'https://iptv.glattvision.ch/live/srf1',
+        'only_matching': True,
+    }]
+
+    @classmethod
+    def suitable(cls, url):
+        return False if GlattvisionTVIE.suitable(url) else super().suitable(url)
+
+
+class GlattvisionTVRecordingsIE(GlattvisionTVBaseIE):
+    _VALID_URL = _create_valid_url(GlattvisionTVBaseIE._HOST, r'\d+', 'recording')
+    _TYPE = 'record'
+    _TESTS = [{
+        'url': 'https://iptv.glattvision.ch/recordings?recording=193615508',
+        'only_matching': True,
+    }, {
+        'url': 'https://iptv.glattvision.ch/tc/ptc_recordings_all_recordings?recording=193615420',
+        'only_matching': True,
+    }]
+
+
+class SAKTVBaseIE(ZattooPlatformBaseIE):
     _NETRC_MACHINE = 'saktv'
     _HOST = 'saktv.ch'
     _API_HOST = 'www.%s' % _HOST
-    _VALID_URL = _make_valid_url(_HOST)
 
+
+class SAKTVIE(SAKTVBaseIE):
+    _VALID_URL = _create_valid_url(SAKTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
+    _TYPE = 'video'
+    _TESTS = [{
+        'url': 'https://saktv.ch/program/daserste/210177916',
+        'only_matching': True,
+    }, {
+        'url': 'https://saktv.ch/guide/german?channel=srf1&program=169860555',
+        'only_matching': True,
+    }]
+
+
+class SAKTVLiveIE(SAKTVBaseIE):
+    _VALID_URL = _create_valid_url(SAKTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
+    _TYPE = 'live'
     _TESTS = [{
-        'url': 'https://www.saktv.ch/watch/abc/123-abc',
+        'url': 'https://saktv.ch/channels/german?channel=srf_zwei',
+        'only_matching': True,
+    }, {
+        'url': 'https://saktv.ch/live/srf1',
         'only_matching': True,
     }]
 
+    @classmethod
+    def suitable(cls, url):
+        return False if SAKTVIE.suitable(url) else super().suitable(url)
+
 
-class EWETVIE(ZattooPlatformBaseIE):
+class SAKTVRecordingsIE(SAKTVBaseIE):
+    _VALID_URL = _create_valid_url(SAKTVBaseIE._HOST, r'\d+', 'recording')
+    _TYPE = 'record'
+    _TESTS = [{
+        'url': 'https://saktv.ch/recordings?recording=193615508',
+        'only_matching': True,
+    }, {
+        'url': 'https://saktv.ch/tc/ptc_recordings_all_recordings?recording=193615420',
+        'only_matching': True,
+    }]
+
+
+class EWETVBaseIE(ZattooPlatformBaseIE):
     _NETRC_MACHINE = 'ewetv'
     _HOST = 'tvonline.ewe.de'
-    _VALID_URL = _make_valid_url(_HOST)
 
+
+class EWETVIE(EWETVBaseIE):
+    _VALID_URL = _create_valid_url(EWETVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
+    _TYPE = 'video'
+    _TESTS = [{
+        'url': 'https://tvonline.ewe.de/program/daserste/210177916',
+        'only_matching': True,
+    }, {
+        'url': 'https://tvonline.ewe.de/guide/german?channel=srf1&program=169860555',
+        'only_matching': True,
+    }]
+
+
+class EWETVLiveIE(EWETVBaseIE):
+    _VALID_URL = _create_valid_url(EWETVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
+    _TYPE = 'live'
+    _TESTS = [{
+        'url': 'https://tvonline.ewe.de/channels/german?channel=srf_zwei',
+        'only_matching': True,
+    }, {
+        'url': 'https://tvonline.ewe.de/live/srf1',
+        'only_matching': True,
+    }]
+
+    @classmethod
+    def suitable(cls, url):
+        return False if EWETVIE.suitable(url) else super().suitable(url)
+
+
+class EWETVRecordingsIE(EWETVBaseIE):
+    _VALID_URL = _create_valid_url(EWETVBaseIE._HOST, r'\d+', 'recording')
+    _TYPE = 'record'
     _TESTS = [{
-        'url': 'https://tvonline.ewe.de/watch/abc/123-abc',
+        'url': 'https://tvonline.ewe.de/recordings?recording=193615508',
+        'only_matching': True,
+    }, {
+        'url': 'https://tvonline.ewe.de/tc/ptc_recordings_all_recordings?recording=193615420',
         'only_matching': True,
     }]
 
 
-class QuantumTVIE(ZattooPlatformBaseIE):
+class QuantumTVBaseIE(ZattooPlatformBaseIE):
     _NETRC_MACHINE = 'quantumtv'
     _HOST = 'quantum-tv.com'
     _API_HOST = 'www.%s' % _HOST
-    _VALID_URL = _make_valid_url(_HOST)
 
+
+class QuantumTVIE(QuantumTVBaseIE):
+    _VALID_URL = _create_valid_url(QuantumTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
+    _TYPE = 'video'
     _TESTS = [{
-        'url': 'https://www.quantum-tv.com/watch/abc/123-abc',
+        'url': 'https://quantum-tv.com/program/daserste/210177916',
+        'only_matching': True,
+    }, {
+        'url': 'https://quantum-tv.com/guide/german?channel=srf1&program=169860555',
+        'only_matching': True,
+    }]
+
+
+class QuantumTVLiveIE(QuantumTVBaseIE):
+    _VALID_URL = _create_valid_url(QuantumTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
+    _TYPE = 'live'
+    _TESTS = [{
+        'url': 'https://quantum-tv.com/channels/german?channel=srf_zwei',
+        'only_matching': True,
+    }, {
+        'url': 'https://quantum-tv.com/live/srf1',
+        'only_matching': True,
+    }]
+
+    @classmethod
+    def suitable(cls, url):
+        return False if QuantumTVIE.suitable(url) else super().suitable(url)
+
+
+class QuantumTVRecordingsIE(QuantumTVBaseIE):
+    _VALID_URL = _create_valid_url(QuantumTVBaseIE._HOST, r'\d+', 'recording')
+    _TYPE = 'record'
+    _TESTS = [{
+        'url': 'https://quantum-tv.com/recordings?recording=193615508',
+        'only_matching': True,
+    }, {
+        'url': 'https://quantum-tv.com/tc/ptc_recordings_all_recordings?recording=193615420',
         'only_matching': True,
     }]
 
 
-class OsnatelTVIE(ZattooPlatformBaseIE):
+class OsnatelTVBaseIE(ZattooPlatformBaseIE):
     _NETRC_MACHINE = 'osnateltv'
     _HOST = 'tvonline.osnatel.de'
-    _VALID_URL = _make_valid_url(_HOST)
 
+
+class OsnatelTVIE(OsnatelTVBaseIE):
+    _VALID_URL = _create_valid_url(OsnatelTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
+    _TYPE = 'video'
+    _TESTS = [{
+        'url': 'https://tvonline.osnatel.de/program/daserste/210177916',
+        'only_matching': True,
+    }, {
+        'url': 'https://tvonline.osnatel.de/guide/german?channel=srf1&program=169860555',
+        'only_matching': True,
+    }]
+
+
+class OsnatelTVLiveIE(OsnatelTVBaseIE):
+    _VALID_URL = _create_valid_url(OsnatelTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
+    _TYPE = 'live'
     _TESTS = [{
-        'url': 'https://tvonline.osnatel.de/watch/abc/123-abc',
+        'url': 'https://tvonline.osnatel.de/channels/german?channel=srf_zwei',
+        'only_matching': True,
+    }, {
+        'url': 'https://tvonline.osnatel.de/live/srf1',
         'only_matching': True,
     }]
 
+    @classmethod
+    def suitable(cls, url):
+        return False if OsnatelTVIE.suitable(url) else super().suitable(url)
 
-class EinsUndEinsTVIE(ZattooPlatformBaseIE):
+
+class OsnatelTVRecordingsIE(OsnatelTVBaseIE):
+    _VALID_URL = _create_valid_url(OsnatelTVBaseIE._HOST, r'\d+', 'recording')
+    _TYPE = 'record'
+    _TESTS = [{
+        'url': 'https://tvonline.osnatel.de/recordings?recording=193615508',
+        'only_matching': True,
+    }, {
+        'url': 'https://tvonline.osnatel.de/tc/ptc_recordings_all_recordings?recording=193615420',
+        'only_matching': True,
+    }]
+
+
+class EinsUndEinsTVBaseIE(ZattooPlatformBaseIE):
     _NETRC_MACHINE = '1und1tv'
     _HOST = '1und1.tv'
     _API_HOST = 'www.%s' % _HOST
-    _VALID_URL = _make_valid_url(_HOST)
 
+
+class EinsUndEinsTVIE(EinsUndEinsTVBaseIE):
+    _VALID_URL = _create_valid_url(EinsUndEinsTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
+    _TYPE = 'video'
     _TESTS = [{
-        'url': 'https://www.1und1.tv/watch/abc/123-abc',
+        'url': 'https://1und1.tv/program/daserste/210177916',
+        'only_matching': True,
+    }, {
+        'url': 'https://1und1.tv/guide/german?channel=srf1&program=169860555',
         'only_matching': True,
     }]
 
 
-class SaltTVIE(ZattooPlatformBaseIE):
+class EinsUndEinsTVLiveIE(EinsUndEinsTVBaseIE):
+    _VALID_URL = _create_valid_url(EinsUndEinsTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
+    _TYPE = 'live'
+    _TESTS = [{
+        'url': 'https://1und1.tv/channels/german?channel=srf_zwei',
+        'only_matching': True,
+    }, {
+        'url': 'https://1und1.tv/live/srf1',
+        'only_matching': True,
+    }]
+
+    @classmethod
+    def suitable(cls, url):
+        return False if EinsUndEinsTVIE.suitable(url) else super().suitable(url)
+
+
+class EinsUndEinsTVRecordingsIE(EinsUndEinsTVBaseIE):
+    _VALID_URL = _create_valid_url(EinsUndEinsTVBaseIE._HOST, r'\d+', 'recording')
+    _TYPE = 'record'
+    _TESTS = [{
+        'url': 'https://1und1.tv/recordings?recording=193615508',
+        'only_matching': True,
+    }, {
+        'url': 'https://1und1.tv/tc/ptc_recordings_all_recordings?recording=193615420',
+        'only_matching': True,
+    }]
+
+
+class SaltTVBaseIE(ZattooPlatformBaseIE):
     _NETRC_MACHINE = 'salttv'
     _HOST = 'tv.salt.ch'
-    _VALID_URL = _make_valid_url(_HOST)
 
+
+class SaltTVIE(SaltTVBaseIE):
+    _VALID_URL = _create_valid_url(SaltTVBaseIE._HOST, r'\d+', 'program', '(?:program|watch)/[^/]+')
+    _TYPE = 'video'
+    _TESTS = [{
+        'url': 'https://tv.salt.ch/program/daserste/210177916',
+        'only_matching': True,
+    }, {
+        'url': 'https://tv.salt.ch/guide/german?channel=srf1&program=169860555',
+        'only_matching': True,
+    }]
+
+
+class SaltTVLiveIE(SaltTVBaseIE):
+    _VALID_URL = _create_valid_url(SaltTVBaseIE._HOST, r'[^/?&#]+', 'channel', 'live')
+    _TYPE = 'live'
     _TESTS = [{
-        'url': 'https://tv.salt.ch/watch/abc/123-abc',
+        'url': 'https://tv.salt.ch/channels/german?channel=srf_zwei',
+        'only_matching': True,
+    }, {
+        'url': 'https://tv.salt.ch/live/srf1',
+        'only_matching': True,
+    }]
+
+    @classmethod
+    def suitable(cls, url):
+        return False if SaltTVIE.suitable(url) else super().suitable(url)
+
+
+class SaltTVRecordingsIE(SaltTVBaseIE):
+    _VALID_URL = _create_valid_url(SaltTVBaseIE._HOST, r'\d+', 'recording')
+    _TYPE = 'record'
+    _TESTS = [{
+        'url': 'https://tv.salt.ch/recordings?recording=193615508',
+        'only_matching': True,
+    }, {
+        'url': 'https://tv.salt.ch/tc/ptc_recordings_all_recordings?recording=193615420',
         'only_matching': True,
     }]


Test patch: