Repository: yt-dlp/yt-dlp

Issue:
Add NBC TV News Sites,  www.NBCLosAngeles.com , www.NBCBayArea.com, www.NBCSanDiego.com ..etc
### Checklist

- [X] I'm reporting a new site support request
- [X] I've verified that I'm running yt-dlp version **2022.07.18** ([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 none of provided URLs [violate any copyrights](https://github.com/ytdl-org/youtube-dl#can-you-add-support-for-this-anime-video-site-or-site-which-shows-current-movies-for-free) or contain any [DRM](https://en.wikipedia.org/wiki/Digital_rights_management) to the best of my knowledge
- [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)
- [ ] I've read about [sharing account credentials](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#are-you-willing-to-share-account-details-if-needed) and am willing to share it if required

### Region

_No response_

### Example URLs

https://www.nbclosangeles.com/news/local/junior-lifeguards-pull-pilot-from-small-plane-crash/2947359/

https://www.nbcbayarea.com/news/national-international/small-banner-plane-crash-huntington-beach/2955788/

https://www.nbcsandiego.com/live
https://www.nbcconnecticut.com/
https://www.nbcwashington.com/
https://www.nbcmiami.com/
https://www.nbcchicago.com/
https://www.nbcboston.com/
https://www.nbcnewyork.com/
https://www.nbcphiladelphia.com/
https://www.telemundopr.com/
https://www.necn.com/

List from: https://en.wikipedia.org/wiki/NBC_Owned_Television_Stations

### Provide a description that is worded well enough to be understood

Please add support for all of the mentioned domains in the example links.

The sites seem to have a very similar if not the same exact backend, just different end-user content for different region. They also link to their other sister sites a lot, for example the news article on nbcsandiego.com has an embedded video that points to nbclosangeles.com

Sites seem to use /master.m3u8, but I got it from Firefox's "network" tab and not from page source.

A Livestream for each site are accessible on the domain/live address for all sites. However livestreams are either geographically restricted or locked under subscribtion. The geographic restriction that I am seeing is most likely due to EU GDPR laws which many USA based news sites were not willing to support so many of them completely banned all EU IP addresses for the whole domain. Larger establishments have since started supporting EU GDPR laws to allow EU citizen to access the sites, which includes these NBC local TV stations, with the exception of livestreams it seems.

www.nbcbayarea.com/live gives me 
> Login with your TV provider to watch unlimited NBC content on
> [www.nbc.com/live]()

www.nbconnecticut.com/live gives a black unseen message with just a close button, inspecting webpage source gives: 

> <div class="tpErrorOverlay tpPlayerArea" style="">
>       <div class="tpErrorCard tpCard">
>         <div class="tpErrorTitle" tabindex="0"><span>Geographic Restriction</span></div>
>         <div class="tpErrorBody" tabindex="0"><span>This content is not available in your location.</span></div>
>       </div>
>       <div class="tpActions">
> <button class="tpCloseButton" value="close" aria-label="Close">Close</button>
> </div>
>     </div>

TOS:

> To the extent that NBCUniversal makes Content available to you via a widget, embedded player or other technology that allows you to embed or stream Content on or to another site, you may not modify, enhance, remove, interfere with, or otherwise alter in any way any portion of such technology, any digital rights management mechanism, device, or other content protection or access control measure associated with Content, or any advertisement delivered with Content. You are also prohibited from disabling, modifying, interfering with or otherwise circumventing any technology to allow users to view Content without: (i) displaying visibly both Content and all surrounding elements (including the graphical user interface, any advertising, copyright notices, and trademarks); and (ii) having full access to all functionality permitting viewing of Content, including, without limitation, all video quality and display functionality and all interactive, elective or click-through advertising functionality.

For the first sentence: The videos/media in question is publicly available and has no DRM.
Second sentence: yt-dlp and it's users would not be serving the Content to other users.

So I think it's fine for a personal copy then, right?


### 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
[debug] Command-line config: ['-vvU', 'https://www.nbclosangeles.com/news/local/junior-lifeguards-pull-pilot-from-small-plane-crash/2947359/']
[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.07.18 [135f05e] (zip)
[debug] Python 3.10.5 (CPython 64bit) - Linux-5.18.12-1-default-x86_64-with-glibc2.35 (glibc 2.35)
[debug] Checking exe version: ffmpeg -bsfs
[debug] Checking exe version: ffprobe -bsfs
[debug] exe versions: ffmpeg 5.1 (fdk,setts), ffprobe 5.1
[debug] Optional libraries: brotli-1.0.9, certifi-2021.10.08, sqlite3-2.6.0, websockets-10.1
[debug] Proxy map: {}
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest
Latest version: 2022.07.18, Current version: 2022.07.18
yt-dlp is up to date (2022.07.18)
[debug] [generic] Extracting URL: https://www.nbclosangeles.com/news/local/junior-lifeguards-pull-pilot-from-small-plane-crash/2947359/
[generic] 2947359: Downloading webpage
WARNING: [generic] Falling back on generic information extractor.
[generic] 2947359: Extracting information
[debug] Looking for video embeds
ERROR: Unsupported URL: https://www.nbclosangeles.com/news/local/junior-lifeguards-pull-pilot-from-small-plane-crash/2947359/
Traceback (most recent call last):
  File "/usr/bin/yt-dlp/yt_dlp/YoutubeDL.py", line 1431, in wrapper
    return func(self, *args, **kwargs)
  File "/usr/bin/yt-dlp/yt_dlp/YoutubeDL.py", line 1506, in __extract_info
    ie_result = ie.extract(url)
  File "/usr/bin/yt-dlp/yt_dlp/extractor/common.py", line 644, in extract
    ie_result = self._real_extract(url)
  File "/usr/bin/yt-dlp/yt_dlp/extractor/generic.py", line 4108, in _real_extract
    raise UnsupportedError(url)
yt_dlp.utils.UnsupportedError: Unsupported URL: https://www.nbclosangeles.com/news/local/junior-lifeguards-pull-pilot-from-small-plane-crash/2947359/

------------------------------

[debug] Command-line config: ['-vU', '-F', 'https://nbclim-f.akamaihd.net/i/Prod/NBCU_LM_VMS_-_KNBC/599/827/17552149962-,1080p,720p,540phigh,540plow,360p,234p,nbcstations.mp4.csmil/master.m3u8']
[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.07.18 [135f05e] (zip)
[debug] Python 3.10.5 (CPython 64bit) - Linux-5.18.12-1-default-x86_64-with-glibc2.35 (glibc 2.35)
[debug] Checking exe version: ffmpeg -bsfs
[debug] Checking exe version: ffprobe -bsfs
[debug] exe versions: ffmpeg 5.1 (fdk,setts), ffprobe 5.1
[debug] Optional libraries: brotli-1.0.9, certifi-2021.10.08, sqlite3-2.6.0, websockets-10.1
[debug] Proxy map: {}
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest
Latest version: 2022.07.18, Current version: 2022.07.18
yt-dlp is up to date (2022.07.18)
[debug] [generic] Extracting URL: https://nbclim-f.akamaihd.net/i/Prod/NBCU_LM_VMS_-_KNBC/599/827/17552149962-,1080p,720p,540phigh,540plow,360p,234p,nbcstations.mp4.csmil/master.m3u8
[generic] master: Downloading webpage
[debug] Identified a direct video link
[generic] master: Downloading m3u8 information
[debug] Formats sorted by: hasvid, ie_pref, lang, quality, res, fps, hdr:12(7), vcodec:vp9.2(10), acodec, filesize, fs_approx, tbr, vbr, abr, asr, proto, vext, aext, hasaud, source, id
[info] Available formats for master:
ID   EXT RESOLUTION │   TBR PROTO │ VCODEC       VBR ACODEC    ABR
──────────────────────────────────────────────────────────────────
304  mp4 416x234    │  304k m3u8  │ avc1.77.30  304k mp4a.40.2  0k
820  mp4 640x360    │  820k m3u8  │ avc1.77.30  820k mp4a.40.2  0k
1535 mp4 960x540    │ 1535k m3u8  │ avc1.77.30 1535k mp4a.40.2  0k
1905 mp4 960x540    │ 1905k m3u8  │ avc1.77.30 1905k mp4a.40.2  0k
2742 mp4 1280x720   │ 2742k m3u8  │ avc1.77.30 2742k mp4a.40.2  0k
4103 mp4 1920x1080  │ 4103k m3u8  │ avc1.77.30 4103k mp4a.40.2  0k
```



Discussion:
for devs: It's possible these may all be able to be integrated with the existing NBC extraction (see nbc.py). The sites do call theplatform.com similar to them, so you will most likely be making use of that extractor.

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
@@ -1079,6 +1079,7 @@
     NBCSportsIE,
     NBCSportsStreamIE,
     NBCSportsVPlayerIE,
+    NBCStationsIE,
 )
 from .ndr import (
     NDRIE,
diff --git a/yt_dlp/extractor/nbc.py b/yt_dlp/extractor/nbc.py
--- a/yt_dlp/extractor/nbc.py
+++ b/yt_dlp/extractor/nbc.py
@@ -7,14 +7,20 @@
 from .adobepass import AdobePassIE
 from ..compat import compat_urllib_parse_unquote
 from ..utils import (
+    ExtractorError,
     int_or_none,
     parse_age_limit,
     parse_duration,
     RegexNotFoundError,
     smuggle_url,
+    str_or_none,
+    traverse_obj,
     try_get,
+    unified_strdate,
     unified_timestamp,
     update_url_query,
+    url_basename,
+    variadic,
 )
 
 
@@ -584,3 +590,169 @@ def _real_extract(self, url):
             'formats': formats,
             'is_live': is_live,
         }
+
+
+class NBCStationsIE(InfoExtractor):
+    _DOMAIN_RE = '|'.join(map(re.escape, (
+        'nbcbayarea', 'nbcboston', 'nbcchicago', 'nbcconnecticut', 'nbcdfw', 'nbclosangeles',
+        'nbcmiami', 'nbcnewyork', 'nbcphiladelphia', 'nbcsandiego', 'nbcwashington',
+        'necn', 'telemundo52', 'telemundoarizona', 'telemundochicago', 'telemundonuevainglaterra',
+    )))
+    _VALID_URL = rf'https?://(?:www\.)?(?P<site>{_DOMAIN_RE})\.com/(?:[^/?#]+/)*(?P<id>[^/?#]+)/?(?:$|[#?])'
+
+    _TESTS = [{
+        'url': 'https://www.nbclosangeles.com/news/local/large-structure-fire-in-downtown-la-prompts-smoke-odor-advisory/2968618/',
+        'md5': '462041d91bd762ef5a38b7d85d6dc18f',
+        'info_dict': {
+            'id': '2968618',
+            'ext': 'mp4',
+            'title': 'Large Structure Fire in Downtown LA Prompts Smoke Odor Advisory',
+            'description': None,
+            'timestamp': 1661135892,
+            'upload_date': '20220821',
+            'uploader': 'NBC 4',
+            'uploader_id': 'KNBC',
+            'channel': 'nbclosangeles',
+        },
+    }, {
+        'url': 'https://www.telemundoarizona.com/responde/huracan-complica-reembolso-para-televidente-de-tucson/2247002/',
+        'md5': '0917dcf7885be1023a9220630d415f67',
+        'info_dict': {
+            'id': '2247002',
+            'ext': 'mp4',
+            'title': 'Huracán complica que televidente de Tucson reciba reembolso',
+            'description': 'md5:af298dc73aab74d4fca6abfb12acb6cf',
+            'timestamp': 1660886507,
+            'upload_date': '20220819',
+            'uploader': 'Telemundo Arizona',
+            'uploader_id': 'KTAZ',
+            'channel': 'telemundoarizona',
+        },
+    }]
+
+    _RESOLUTIONS = {
+        '1080': '1920',
+        '720': '1280',
+        '540': '960',
+        '360': '640',
+        '234': '416',
+    }
+
+    def _real_extract(self, url):
+        channel, video_id = self._match_valid_url(url).group('site', 'id')
+        webpage = self._download_webpage(url, video_id)
+
+        nbc_data = self._search_json(
+            r'<script>var\s*nbc\s*=\s*', webpage, 'NBC JSON data', video_id)
+        pdk_acct = nbc_data.get('pdkAcct') or 'Yh1nAC'
+        fw_ssid = traverse_obj(nbc_data, ('video', 'fwSSID'))
+        fw_network_id = traverse_obj(nbc_data, ('video', 'fwNetworkID'), default='382114')
+
+        video_data = self._parse_json(self._html_search_regex(
+            r'data-videos="([^"]*)"', webpage, 'video data', default='{}'), video_id)
+        video_data = variadic(video_data)[0]
+        video_data.update(self._parse_json(self._html_search_regex(
+            r'data-meta="([^"]*)"', webpage, 'metadata', default='{}'), video_id))
+
+        formats = []
+
+        if video_data.get('mpx_is_livestream') == '1':
+            live = True
+            player_id = traverse_obj(
+                video_data, 'mpx_m3upid', ('video', 'meta', 'mpx_m3upid'), 'mpx_pid',
+                ('video', 'meta', 'mpx_pid'), 'pid_streaming_web_medium')
+            query = {
+                'mbr': 'true',
+                'assetTypes': 'LegacyRelease',
+                'fwsitesection': fw_ssid,
+                'fwNetworkID': fw_network_id,
+                'pprofile': 'ots_desktop_html',
+                'sensitive': 'false',
+                'w': '1920',
+                'h': '1080',
+                'rnd': '1660303',
+                'mode': 'LIVE',
+                'format': 'SMIL',
+                'tracking': 'true',
+                'formats': 'M3U+none,MPEG-DASH+none,MPEG4,MP3',
+                'vpaid': 'script',
+                'schema': '2.0',
+                'SDK': 'PDK+6.1.3',
+            }
+            info = {
+                'title': f'{channel} livestream',
+            }
+
+        else:
+            live = False
+            player_id = traverse_obj(
+                video_data, ('video', 'meta', 'pid_streaming_web_high'), 'pid_streaming_web_high',
+                ('video', 'meta', 'mpx_pid'), 'mpx_pid')
+
+            date_string = traverse_obj(video_data, 'date_string', 'date_gmt')
+            if date_string:
+                date_string = self._search_regex(
+                    r'datetime="([^"]+)"', date_string, 'date string', fatal=False)
+            else:
+                date_string = traverse_obj(
+                    nbc_data, ('dataLayer', 'adobe', 'prop70'), ('dataLayer', 'adobe', 'eVar70'),
+                    ('dataLayer', 'adobe', 'eVar59'))
+
+            video_url = traverse_obj(video_data, ('video', 'meta', 'mp4_url'), 'mp4_url')
+            if video_url:
+                height = url_basename(video_url).split('-')[1].split('p')[0]
+                formats.append({
+                    'url': video_url,
+                    'ext': 'mp4',
+                    'width': int_or_none(self._RESOLUTIONS.get(height)),
+                    'height': int_or_none(height),
+                    'format_id': f'http-{height}',
+                })
+
+            query = {
+                'mbr': 'true',
+                'assetTypes': 'LegacyRelease',
+                'fwsitesection': fw_ssid,
+                'fwNetworkID': fw_network_id,
+                'format': 'redirect',
+                'manifest': 'm3u',
+                'Tracking': 'true',
+                'Embedded': 'true',
+                'formats': 'MPEG4',
+            }
+            info = {
+                'title': video_data.get('title') or traverse_obj(
+                    nbc_data, ('dataLayer', 'contenttitle'), ('dataLayer', 'title'),
+                    ('dataLayer', 'adobe', 'prop22'), ('dataLayer', 'id')),
+                'description': traverse_obj(video_data, 'summary', 'excerpt', 'video_hero_text'),
+                'upload_date': str_or_none(unified_strdate(date_string)),
+                'timestamp': int_or_none(unified_timestamp(date_string)),
+            }
+
+        if not player_id:
+            raise ExtractorError(
+                'No video player ID or livestream player ID found in webpage', expected=True)
+
+        headers = {'Origin': f'https://www.{channel}.com'}
+        manifest, urlh = self._download_webpage_handle(
+            f'https://link.theplatform.com/s/{pdk_acct}/{player_id}', video_id,
+            headers=headers, query=query, note='Downloading manifest')
+        if live:
+            manifest_url = self._search_regex(r'<video src="([^"]*)', manifest, 'manifest URL')
+        else:
+            manifest_url = urlh.geturl()
+
+        formats.extend(self._extract_m3u8_formats(
+            manifest_url, video_id, 'mp4', headers=headers, m3u8_id='hls',
+            fatal=live, live=live, errnote='No HLS formats found'))
+        self._sort_formats(formats)
+
+        return {
+            'id': str_or_none(video_id),
+            'channel': channel,
+            'uploader': str_or_none(nbc_data.get('on_air_name')),
+            'uploader_id': str_or_none(nbc_data.get('callLetters')),
+            'formats': formats,
+            'is_live': live,
+            **info,
+        }


Test patch: