Repository: yt-dlp/yt-dlp
Issue category: Bug Report

Patreon: --write-comments is broken
### DO NOT REMOVE OR SKIP THE ISSUE TEMPLATE

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

### Checklist

- [X] I'm reporting that yt-dlp is broken on a **supported** site
- [X] I've verified that I have **updated yt-dlp to nightly or master** ([update instructions](https://github.com/yt-dlp/yt-dlp#update-channels))
- [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/yt-dlp/yt-dlp/wiki/FAQ#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 [known issues](https://github.com/yt-dlp/yt-dlp/issues/3766) and 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 I'm willing to share it if required

### Region

_No response_

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

Downloading comments from Patreon videos is broken.

Note: I didn't run the update_version script, but I built yt-dlp from the current master, be3579aaf0c3b71a0a3195e1955415d5e4d6b3d8

### Provide verbose output that clearly demonstrates the problem

- [X] Run **your** yt-dlp command with **-vU** flag added (`yt-dlp -vU <your command line>`)
- [ ] If using API, add `'verbose': True` to `YoutubeDL` params instead
- [X] Copy the WHOLE output (starting with `[debug] Command-line config`) and insert it below

### Complete Verbose Output

```shell
[debug] Command-line config: ['-vU', 'https://www.patreon.com/posts/114721679', '--write-comments']
[debug] User config "/home/mateon/.config/yt-dlp/config": ['--compat-options=no-certifi']
[debug] Encodings: locale UTF-8, fs utf-8, pref UTF-8, out utf-8, error utf-8, screen utf-8
[debug] yt-dlp version stable@2024.11.04 from yt-dlp/yt-dlp [197d0b03b]
[debug] Lazy loading extractors is disabled
[debug] Compatibility options: no-certifi
[debug] Python 3.12.7 (CPython x86_64 64bit) - Linux-6.6.53-x86_64-with-glibc2.40 (OpenSSL 3.3.2 3 Sep 2024, glibc 2.40)
[debug] exe versions: ffmpeg 7.1 (setts), ffprobe 7.1, rtmpdump 2.4
[debug] Optional libraries: Cryptodome-3.20.0, brotlicffi-1.1.0.0, certifi-2024.08.30, curl_cffi-0.7.2 (unsupported), mutagen-1.47.0, requests-2.32.3, secretstorage-3.3.3, sqlite3-3.46.1, urllib3-2.2.3, websockets-13.1
[debug] Proxy map: {}
[debug] Request Handlers: urllib, requests, websockets
[debug] Loaded 1839 extractors
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest
Latest version: stable@2024.11.04 from yt-dlp/yt-dlp
yt-dlp is up to date (stable@2024.11.04 from yt-dlp/yt-dlp)
[patreon] Extracting URL: https://www.patreon.com/posts/114721679
[patreon] 114721679: Downloading API JSON
[patreon] 114721679: Downloading m3u8 information
[debug] Formats sorted by: hasvid, ie_pref, lang, quality, res, fps, hdr:12(7), vcodec, channels, acodec, size, br, asr, proto, vext, aext, hasaud, source, id
[patreon] 114721679: Downloading comments page 1
ERROR: 'str' object has no attribute 'get'
Traceback (most recent call last):
  File "/nix/store/y3q4p7gjj7kz37gkiyw17vanglji0kry-yt-dlp-be3579aaf0c3b71a0a3195e1955415d5e4d6b3d8/lib/python3.12/site-packages/yt_dlp/YoutubeDL.py", line 1625, in wrapper
    return func(self, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/nix/store/y3q4p7gjj7kz37gkiyw17vanglji0kry-yt-dlp-be3579aaf0c3b71a0a3195e1955415d5e4d6b3d8/lib/python3.12/site-packages/yt_dlp/YoutubeDL.py", line 1781, in __extract_info
    return self.process_ie_result(ie_result, download, extra_info)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/nix/store/y3q4p7gjj7kz37gkiyw17vanglji0kry-yt-dlp-be3579aaf0c3b71a0a3195e1955415d5e4d6b3d8/lib/python3.12/site-packages/yt_dlp/YoutubeDL.py", line 1840, in process_ie_result
    ie_result = self.process_video_result(ie_result, download=download)
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/nix/store/y3q4p7gjj7kz37gkiyw17vanglji0kry-yt-dlp-be3579aaf0c3b71a0a3195e1955415d5e4d6b3d8/lib/python3.12/site-packages/yt_dlp/YoutubeDL.py", line 2926, in process_video_result
    self.post_extract(info_dict)
  File "/nix/store/y3q4p7gjj7kz37gkiyw17vanglji0kry-yt-dlp-be3579aaf0c3b71a0a3195e1955415d5e4d6b3d8/lib/python3.12/site-packages/yt_dlp/YoutubeDL.py", line 3694, in post_extract
    actual_post_extract(info_dict or {})
  File "/nix/store/y3q4p7gjj7kz37gkiyw17vanglji0kry-yt-dlp-be3579aaf0c3b71a0a3195e1955415d5e4d6b3d8/lib/python3.12/site-packages/yt_dlp/YoutubeDL.py", line 3692, in actual_post_extract
    info_dict.update(post_extractor())
                     ^^^^^^^^^^^^^^^^
  File "/nix/store/y3q4p7gjj7kz37gkiyw17vanglji0kry-yt-dlp-be3579aaf0c3b71a0a3195e1955415d5e4d6b3d8/lib/python3.12/site-packages/yt_dlp/extractor/common.py", line 3734, in extractor
    comments.append(next(generator))
                    ^^^^^^^^^^^^^^^
  File "/nix/store/y3q4p7gjj7kz37gkiyw17vanglji0kry-yt-dlp-be3579aaf0c3b71a0a3195e1955415d5e4d6b3d8/lib/python3.12/site-packages/yt_dlp/extractor/patreon.py", line 409, in _get_comments
    comment_id = comment.get('id')
                 ^^^^^^^^^^^
AttributeError: 'str' object has no attribute 'get'
```



Functions to edit:
yt_dlp/extractor/patreon.py:PatreonIE
yt_dlp/extractor/patreon.py:PatreonIE._get_comments
Functions added: yt_dlp/extractor/patreon.py:PatreonIE

Patch:
diff --git a/yt_dlp/extractor/patreon.py b/yt_dlp/extractor/patreon.py
index 4d668cd37dc0..6bdeaf15710d 100644
--- a/yt_dlp/extractor/patreon.py
+++ b/yt_dlp/extractor/patreon.py
@@ -16,10 +16,10 @@
     parse_iso8601,
     smuggle_url,
     str_or_none,
-    traverse_obj,
     url_or_none,
     urljoin,
 )
+from ..utils.traversal import traverse_obj, value
 
 
 class PatreonBaseIE(InfoExtractor):
@@ -252,6 +252,27 @@ class PatreonIE(PatreonBaseIE):
             'thumbnail': r're:^https?://.+',
         },
         'skip': 'Patron-only content',
+    }, {
+        # Contains a comment reply in the 'included' section
+        'url': 'https://www.patreon.com/posts/114721679',
+        'info_dict': {
+            'id': '114721679',
+            'ext': 'mp4',
+            'upload_date': '20241025',
+            'uploader': 'Japanalysis',
+            'like_count': int,
+            'thumbnail': r're:^https?://.+',
+            'comment_count': int,
+            'title': 'Karasawa Part 2',
+            'description': 'Part 2 of this video https://www.youtube.com/watch?v=Azms2-VTASk',
+            'uploader_url': 'https://www.patreon.com/japanalysis',
+            'uploader_id': '80504268',
+            'channel_url': 'https://www.patreon.com/japanalysis',
+            'channel_follower_count': int,
+            'timestamp': 1729897015,
+            'channel_id': '9346307',
+        },
+        'params': {'getcomments': True},
     }]
     _RETURN_TYPE = 'video'
 
@@ -404,26 +425,24 @@ def _get_comments(self, post_id):
                 f'posts/{post_id}/comments', post_id, query=params, note=f'Downloading comments page {page}')
 
             cursor = None
-            for comment in traverse_obj(response, (('data', ('included', lambda _, v: v['type'] == 'comment')), ...)):
+            for comment in traverse_obj(response, (('data', 'included'), lambda _, v: v['type'] == 'comment' and v['id'])):
                 count += 1
-                comment_id = comment.get('id')
-                attributes = comment.get('attributes') or {}
-                if comment_id is None:
-                    continue
                 author_id = traverse_obj(comment, ('relationships', 'commenter', 'data', 'id'))
-                author_info = traverse_obj(
-                    response, ('included', lambda _, v: v['id'] == author_id and v['type'] == 'user', 'attributes'),
-                    get_all=False, expected_type=dict, default={})
 
                 yield {
-                    'id': comment_id,
-                    'text': attributes.get('body'),
-                    'timestamp': parse_iso8601(attributes.get('created')),
-                    'parent': traverse_obj(comment, ('relationships', 'parent', 'data', 'id'), default='root'),
-                    'author_is_uploader': attributes.get('is_by_creator'),
+                    **traverse_obj(comment, {
+                        'id': ('id', {str_or_none}),
+                        'text': ('attributes', 'body', {str}),
+                        'timestamp': ('attributes', 'created', {parse_iso8601}),
+                        'parent': ('relationships', 'parent', 'data', ('id', {value('root')}), {str}, any),
+                        'author_is_uploader': ('attributes', 'is_by_creator', {bool}),
+                    }),
+                    **traverse_obj(response, (
+                        'included', lambda _, v: v['id'] == author_id and v['type'] == 'user', 'attributes', {
+                            'author': ('full_name', {str}),
+                            'author_thumbnail': ('image_url', {url_or_none}),
+                        }), get_all=False),
                     'author_id': author_id,
-                    'author': author_info.get('full_name'),
-                    'author_thumbnail': author_info.get('image_url'),
                 }
 
             if count < traverse_obj(response, ('meta', 'count')):