Repository: yt-dlp/yt-dlp

Issue:
support downloading from listennotes.com
### 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 new site support request
- [X] I've verified that I'm running yt-dlp version **2022.10.04** ([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/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#is-the-website-primarily-used-for-piracy) 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)
- [X] 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

Europe

### Example URLs

https://www.listennotes.com/podcasts/thriving-on-overload/tim-oreilly-on-noticing-KrDgvNb_u1n/
https://www.listennotes.com/podcasts/ask-noah-show/episode-177-wireguard-with-lwEA3154JzG/

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

Listen Notes is a website which aggregates podcasts from all over the internet, into one searchable platform.
This is a great place to find podcasts based on search criteria.


### 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: ['-vU', '-o', '/mnt/media/video/17-%(title).200s-%(upload_date>%Y-%m-%d)s.%(ext)s', 'https://www.listennotes.com/podcasts/ask-noah-show/episode-177-wireguard-with-lwEA3154JzG/']
[debug] User config "/home/karel/.config/yt-dlp/config": ['--restrict-filenames', '--no-check-certificate', '--sub-lang', 'en', '-f', '22/bestvideo[ext=mp4][width<=1920]+bestaudio[ext=m4a]/mp4/m4a/mp3']
[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.10.04 [4e0511f27] (debian*) API
[debug] Python 3.7.3 (CPython 64bit) - Linux-5.10.109-grsec-x86_64-with-debian-10.13 (glibc 2.28)
[debug] Checking exe version: ffmpeg -bsfs
[debug] Checking exe version: ffprobe -bsfs
[debug] exe versions: ffmpeg 4.1.9-0, ffprobe 4.1.9-0
[debug] Optional libraries: Cryptodome-3.6.1, brotli-1.0.7, certifi-2018.08.24, mutagen-1.40.0, secretstorage-2.3.1, sqlite3-2.6.0, websockets-7.0
[debug] Proxy map: {}
[debug] Loaded 1690 extractors
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest
Latest version: 2022.10.04, Current version: 2022.10.04
yt-dlp is up to date (2022.10.04)
[debug] [generic] Extracting URL: https://www.listennotes.com/podcasts/ask-noah-show/episode-177-wireguard-with-lwEA3154JzG/
[generic] episode-177-wireguard-with-lwEA3154JzG: Downloading webpage
WARNING: [generic] Falling back on generic information extractor
[generic] episode-177-wireguard-with-lwEA3154JzG: Extracting information
[debug] Looking for Brightcove embeds
[debug] Looking for embeds
[debug] Identified a twitter:player iframe
[debug] [generic] Extracting URL: https://www.listennotes.com/podcasts/ask-noah-show/episode-177-wireguard-with-lwEA3154JzG/embed/
[generic] embed: Downloading webpage
WARNING: [generic] Falling back on generic information extractor
[generic] embed: Extracting information
[debug] Looking for Brightcove embeds
[debug] Looking for embeds
ERROR: Unsupported URL: https://www.listennotes.com/podcasts/ask-noah-show/episode-177-wireguard-with-lwEA3154JzG/embed/
Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/yt_dlp/YoutubeDL.py", line 1477, in wrapper
    return func(self, *args, **kwargs)
  File "/usr/lib/python3/dist-packages/yt_dlp/YoutubeDL.py", line 1553, in __extract_info
    ie_result = ie.extract(url)
  File "/usr/lib/python3/dist-packages/yt_dlp/extractor/common.py", line 672, in extract
    ie_result = self._real_extract(url)
  File "/usr/lib/python3/dist-packages/yt_dlp/extractor/generic.py", line 3062, in _real_extract
    raise UnsupportedError(url)
yt_dlp.utils.UnsupportedError: Unsupported URL: https://www.listennotes.com/podcasts/ask-noah-show/episode-177-wireguard-with-lwEA3154JzG/embed/
```



Discussion:
The audio can be extracted in webpage in `<script id="original-content" type="application/json">`
Here's the sample of the tag:
<details> <summary> tag to extract </summary>

```html
<script id="original-content" type="application/json">
      {
        "uuid": "8c8954b95e0b4859ad1eecec8bf6d3a4",
        "channel_url": "https://www.listennotes.com/podcasts/ask-noah-show-noah-j-chelliah-4DQTzdS5-j7/",
        "channel_short_uuid": "4DQTzdS5-j7",
        "log_type": 2,
        "nlp_entities": [{
          "name": "noah showlink",
          "type": 1,
          "wikipedia_url": ""
        }, {
          "name": "noah show",
          "type": 1,
          "wikipedia_url": ""
        }, {
          "name": "noah dashboard",
          "type": 1,
          "wikipedia_url": ""
        }, {
          "name": "jason donenfeld",
          "type": 1,
          "wikipedia_url": ""
        }],
        "audio": "https://aphid.fireside.fm/d/1437767933/305e3275-6919-4ed2-9ca1-b21e0cf904b2/3c3ccc67-34cb-4e6f-a131-3aad800df648.mp3",
        "audio_length": "01:04:21",
        "transcript": "",
        "transcript_under_review": false,
        "is_owner": false
      }
    </script>
```
We can get the audio from `audio` and duration from `audio_length`
The description is in free text
```py
        description = self._html_search(r'(?si)ABOUT\s+THIS\s+EPISODE(?:\s*<[^>]+>)*?\s*<p\b[^>]*>(.+?)<(?:strong|p)', 'description', default=None)
```
Everything else (including a default abbreviated description) is in the `NewsArticle` ld+json block.
<details>
<summary>NewsArticle json</summary>
<code><pre>
{
  "@context": "http://schema.org",
  "@type": "NewsArticle",
  "mainEntityOfPage": {
    "@type": "WebPage",
    "@id": "https://www.listennotes.com/podcasts/ask-noah-show/episode-177-wireguard-with-lwEA3154JzG/"
  },
  "headline": "Episode 177: WireGuard with Jason Donenfeld",
  "image": [
    "https://production.listennotes.com/podcasts/ask-noah-show-noah-j-chelliah-yHvc176kdDu-4DQTzdS5-j7.1400x1400.jpg"
   ],
  "datePublished": "2020-04-21T18:00:00.101000-07:00",
  "dateModified": "2020-04-21T18:00:00.101000-07:00",
  
  "author": {
    "@type": "Person",
    "name": "Noah J. Chelliah"
  },
   "publisher": {
    "@type": "Organization",
    "name": "Noah J. Chelliah",
    "logo": {
      "@type": "ImageObject",
      "url": "https://production.listennotes.com/podcasts/ask-noah-show-noah-j-chelliah-yHvc176kdDu-4DQTzdS5-j7.1400x1400.jpg"
    }
  },
  
  "description": "01:04:21 \u002D Jason Donenfeld lead developer joins us this hour to discuss WireGuard, an extremely sim…"
}
</pre></code>
</details>


Resolving patch:
diff --git a/yt_dlp/compat/__init__.py b/yt_dlp/compat/__init__.py
--- a/yt_dlp/compat/__init__.py
+++ b/yt_dlp/compat/__init__.py
@@ -14,7 +14,7 @@
 # HTMLParseError has been deprecated in Python 3.3 and removed in
 # Python 3.5. Introducing dummy exception for Python >3.5 for compatible
 # and uniform cross-version exception handling
-class compat_HTMLParseError(Exception):
+class compat_HTMLParseError(ValueError):
     pass
 
 
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
@@ -890,6 +890,7 @@
 )
 from .linuxacademy import LinuxAcademyIE
 from .liputan6 import Liputan6IE
+from .listennotes import ListenNotesIE
 from .litv import LiTVIE
 from .livejournal import LiveJournalIE
 from .livestream import (
diff --git a/yt_dlp/extractor/listennotes.py b/yt_dlp/extractor/listennotes.py
new file mode 100644
--- /dev/null
+++ b/yt_dlp/extractor/listennotes.py
@@ -0,0 +1,86 @@
+import re
+
+from .common import InfoExtractor
+from ..utils import (
+    clean_html,
+    extract_attributes,
+    get_element_by_class,
+    get_element_html_by_id,
+    get_element_text_and_html_by_tag,
+    parse_duration,
+    strip_or_none,
+    traverse_obj,
+    try_call,
+)
+
+
+class ListenNotesIE(InfoExtractor):
+    _VALID_URL = r'https?://(?:www\.)?listennotes\.com/podcasts/[^/]+/[^/]+-(?P<id>.+)/'
+    _TESTS = [{
+        'url': 'https://www.listennotes.com/podcasts/thriving-on-overload/tim-oreilly-on-noticing-KrDgvNb_u1n/',
+        'md5': '5b91a32f841e5788fb82b72a1a8af7f7',
+        'info_dict': {
+            'id': 'KrDgvNb_u1n',
+            'ext': 'mp3',
+            'title': 'md5:32236591a921adf17bbdbf0441b6c0e9',
+            'description': 'md5:c581ed197eeddcee55a67cdb547c8cbd',
+            'duration': 2148.0,
+            'channel': 'Thriving on Overload',
+            'channel_id': 'ed84wITivxF',
+            'episode_id': 'e1312583fa7b4e24acfbb5131050be00',
+            'thumbnail': 'https://production.listennotes.com/podcasts/thriving-on-overload-ross-dawson-1wb_KospA3P-ed84wITivxF.300x300.jpg',
+            'channel_url': 'https://www.listennotes.com/podcasts/thriving-on-overload-ross-dawson-ed84wITivxF/',
+            'cast': ['Tim O’Reilly', 'Cookie Monster', 'Lao Tzu', 'Wallace Steven', 'Eric Raymond', 'Christine Peterson', 'John Maynard Keyne', 'Ross Dawson'],
+        }
+    }, {
+        'url': 'https://www.listennotes.com/podcasts/ask-noah-show/episode-177-wireguard-with-lwEA3154JzG/',
+        'md5': '62fb4ffe7fc525632a1138bf72a5ce53',
+        'info_dict': {
+            'id': 'lwEA3154JzG',
+            'ext': 'mp3',
+            'title': 'Episode 177: WireGuard with Jason Donenfeld',
+            'description': 'md5:24744f36456a3e95f83c1193a3458594',
+            'duration': 3861.0,
+            'channel': 'Ask Noah Show',
+            'channel_id': '4DQTzdS5-j7',
+            'episode_id': '8c8954b95e0b4859ad1eecec8bf6d3a4',
+            'channel_url': 'https://www.listennotes.com/podcasts/ask-noah-show-noah-j-chelliah-4DQTzdS5-j7/',
+            'thumbnail': 'https://production.listennotes.com/podcasts/ask-noah-show-noah-j-chelliah-cfbRUw9Gs3F-4DQTzdS5-j7.300x300.jpg',
+            'cast': ['noah showlink', 'noah show', 'noah dashboard', 'jason donenfeld'],
+        }
+    }]
+
+    def _clean_description(self, description):
+        return clean_html(re.sub(r'(</?(div|p)>\s*)+', '<br/><br/>', description or ''))
+
+    def _real_extract(self, url):
+        audio_id = self._match_id(url)
+        webpage = self._download_webpage(url, audio_id)
+        data = self._search_json(
+            r'<script id="original-content"[^>]+\btype="application/json">', webpage, 'content', audio_id)
+        data.update(extract_attributes(get_element_html_by_id(
+            r'episode-play-button-toolbar|episode-no-play-button-toolbar', webpage, escape_value=False)))
+
+        duration, description = self._search_regex(
+            r'(?P<duration>[\d:]+)\s*-\s*(?P<description>.+)',
+            self._html_search_meta(['og:description', 'description', 'twitter:description'], webpage),
+            'description', fatal=False, group=('duration', 'description')) or (None, None)
+
+        return {
+            'id': audio_id,
+            'url': data['audio'],
+            'title': (data.get('data-title')
+                      or try_call(lambda: get_element_text_and_html_by_tag('h1', webpage)[0])
+                      or self._html_search_meta(('og:title', 'title', 'twitter:title'), webpage, 'title')),
+            'description': (self._clean_description(get_element_by_class('ln-text-p', webpage))
+                            or strip_or_none(description)),
+            'duration': parse_duration(traverse_obj(data, 'audio_length', 'data-duration') or duration),
+            'episode_id': traverse_obj(data, 'uuid', 'data-episode-uuid'),
+            **traverse_obj(data, {
+                'thumbnail': 'data-image',
+                'channel': 'data-channel-title',
+                'cast': ('nlp_entities', ..., 'name'),
+                'channel_url': 'channel_url',
+                'channel_id': 'channel_short_uuid',
+            })
+        }


Test patch: