Repository: yt-dlp/yt-dlp

Issue:
[Mixcloud] Failed to parse JSON JSONDecodeError
### 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'm running yt-dlp version **2023.07.06** ([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/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

Failed to parse JSON (caused by JSONDecodeError("Expecting value in '': line 1 column 1 (char 0)"))

Last used successfully on this site 10 days ago.

### 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.mixcloud.com/NTSRadio/nts-guide-to-nimbus-west-11th-september-2023/']
[debug] Encodings: locale cp1252, fs utf-8, pref cp1252, out utf-8, error utf-8, screen utf-8
[debug] yt-dlp version stable@2023.07.06 [b532a3481] (win_exe)
[debug] Python 3.8.10 (CPython AMD64 64bit) - Windows-10-10.0.17763-SP0 (OpenSSL 1.1.1k  25 Mar 2021)
[debug] exe versions: none
[debug] Optional libraries: Cryptodome-3.18.0, brotli-1.0.9, certifi-2023.05.07, mutagen-1.46.0, sqlite3-2.6.0, websockets-11.0.3
[debug] Proxy map: {}
[debug] Loaded 1855 extractors
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest
Available version: stable@2023.07.06, Current version: stable@2023.07.06
Current Build Hash: 5ff3e702171a50175c34397494e2d18ce35d771c2110b1e59bd173ec2fb352aa
yt-dlp is up to date (stable@2023.07.06)
[mixcloud] Extracting URL: https://www.mixcloud.com/NTSRadio/nts-guide-to-nimbus-west-11th-september-2023/
[mixcloud] NTSRadio_nts-guide-to-nimbus-west-11th-september-2023: Downloading JSON metadata
ERROR: [mixcloud] None: NTSRadio_nts-guide-to-nimbus-west-11th-september-2023: Failed to parse JSON
(caused by JSONDecodeError("Expecting value in '': line 1 column 1 (char 0)")); please report this issue on  https://github.com/yt-dlp/yt-dlp/issues?q= , filling out the appropriate issue template. Confirm you are on the latest version using  yt-dlp -U
  File "yt_dlp\extractor\common.py", line 710, in extract
  File "yt_dlp\extractor\mixcloud.py", line 82, in _real_extract
  File "yt_dlp\extractor\mixcloud.py", line 22, in _call_api
  File "yt_dlp\extractor\common.py", line 1064, in download_content
  File "yt_dlp\extractor\common.py", line 1034, in download_handle
  File "yt_dlp\extractor\common.py", line 1024, in parse
  File "yt_dlp\extractor\common.py", line 1011, in _parse_json
  File "yt_dlp\extractor\common.py", line 994, in __print_error

  File "yt_dlp\utils\_utils.py", line 624, in decode
  File "json\decoder.py", line 337, in decode
  File "json\decoder.py", line 355, in raw_decode
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "yt_dlp\extractor\common.py", line 1008, in _parse_json
  File "json\__init__.py", line 370, in loads
  File "yt_dlp\utils\_utils.py", line 632, in decode
json.decoder.JSONDecodeError: Expecting value in '': line 1 column 1 (char 0)
```

[Mixcloud] Failed to parse JSON JSONDecodeError
### 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'm running yt-dlp version **2023.07.06** ([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/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

Failed to parse JSON (caused by JSONDecodeError("Expecting value in '': line 1 column 1 (char 0)"))

Last used successfully on this site 10 days ago.

### 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.mixcloud.com/NTSRadio/nts-guide-to-nimbus-west-11th-september-2023/']
[debug] Encodings: locale cp1252, fs utf-8, pref cp1252, out utf-8, error utf-8, screen utf-8
[debug] yt-dlp version stable@2023.07.06 [b532a3481] (win_exe)
[debug] Python 3.8.10 (CPython AMD64 64bit) - Windows-10-10.0.17763-SP0 (OpenSSL 1.1.1k  25 Mar 2021)
[debug] exe versions: none
[debug] Optional libraries: Cryptodome-3.18.0, brotli-1.0.9, certifi-2023.05.07, mutagen-1.46.0, sqlite3-2.6.0, websockets-11.0.3
[debug] Proxy map: {}
[debug] Loaded 1855 extractors
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest
Available version: stable@2023.07.06, Current version: stable@2023.07.06
Current Build Hash: 5ff3e702171a50175c34397494e2d18ce35d771c2110b1e59bd173ec2fb352aa
yt-dlp is up to date (stable@2023.07.06)
[mixcloud] Extracting URL: https://www.mixcloud.com/NTSRadio/nts-guide-to-nimbus-west-11th-september-2023/
[mixcloud] NTSRadio_nts-guide-to-nimbus-west-11th-september-2023: Downloading JSON metadata
ERROR: [mixcloud] None: NTSRadio_nts-guide-to-nimbus-west-11th-september-2023: Failed to parse JSON
(caused by JSONDecodeError("Expecting value in '': line 1 column 1 (char 0)")); please report this issue on  https://github.com/yt-dlp/yt-dlp/issues?q= , filling out the appropriate issue template. Confirm you are on the latest version using  yt-dlp -U
  File "yt_dlp\extractor\common.py", line 710, in extract
  File "yt_dlp\extractor\mixcloud.py", line 82, in _real_extract
  File "yt_dlp\extractor\mixcloud.py", line 22, in _call_api
  File "yt_dlp\extractor\common.py", line 1064, in download_content
  File "yt_dlp\extractor\common.py", line 1034, in download_handle
  File "yt_dlp\extractor\common.py", line 1024, in parse
  File "yt_dlp\extractor\common.py", line 1011, in _parse_json
  File "yt_dlp\extractor\common.py", line 994, in __print_error

  File "yt_dlp\utils\_utils.py", line 624, in decode
  File "json\decoder.py", line 337, in decode
  File "json\decoder.py", line 355, in raw_decode
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "yt_dlp\extractor\common.py", line 1008, in _parse_json
  File "json\__init__.py", line 370, in loads
  File "yt_dlp\utils\_utils.py", line 632, in decode
json.decoder.JSONDecodeError: Expecting value in '': line 1 column 1 (char 0)
```



Discussion:
Seems possibly related to #7444, though obviously not the same error
if you manage to get the manifest.mpd from dev tools in firefox or chrome you can still download the actual m4a or webm stream. Needless to say, however, this returns no metadata and you just get manifest.m4a. For example:

https://audio9.mixcloud.com/secure/dash2/1/b/7/a/0d0a-b60c-41f7-8098-f7eb5f3f23f0.m4a/manifest.mpd

It's time consuming having to download and rename like this, so would be good if a fix could be found soon.
<details><summary>stuff about it being json over POST instead of query over GET, not actually relevant</summary>

had a brief look at this, the site seems to send its graphql stuff in POST requests with the query as json, whereas the extractor uses GETs with the query as an url query

i wasn't able to get a post request to work in the actual extractor (possibly due to skill issue rather than extractor issue), but with this hack:
```python
    def _call_api(self, object_type, object_fields, display_id, username, slug=None):
        lookup_key = object_type + 'Lookup'
        query={
                'query': '''{
  %s(lookup: {username: "%s"%s}) {
    %s
  }
}''' % (lookup_key, username, ', slug: "%s"' % slug if slug else '', object_fields)
            }
        import json
        print(json.dumps(query))
        return self._download_json(
            'https://www.mixcloud.com/graphql', display_id, query=query)['data'][lookup_key]
```
i can get the query as json, put it into a request body with the firefox dev tools' resend request thing, and it works
![Screenshot from 2023-09-15 09-28-00](https://github.com/yt-dlp/yt-dlp/assets/76261416/6a01220f-3635-4a77-9d1e-b54a55174525)
i assume there's some header/cookie that it's sending that makes it all work, but what it is i couldnt tell you

hope this helps
</details>
oh, much simpler than that actually
api url changed, `https://www.mixcloud.com/graphql` -> `https://app.mixcloud.com/graphql`
~~brb committing kms~~
Seems possibly related to #7444, though obviously not the same error
if you manage to get the manifest.mpd from dev tools in firefox or chrome you can still download the actual m4a or webm stream. Needless to say, however, this returns no metadata and you just get manifest.m4a. For example:

https://audio9.mixcloud.com/secure/dash2/1/b/7/a/0d0a-b60c-41f7-8098-f7eb5f3f23f0.m4a/manifest.mpd

It's time consuming having to download and rename like this, so would be good if a fix could be found soon.
<details><summary>stuff about it being json over POST instead of query over GET, not actually relevant</summary>

had a brief look at this, the site seems to send its graphql stuff in POST requests with the query as json, whereas the extractor uses GETs with the query as an url query

i wasn't able to get a post request to work in the actual extractor (possibly due to skill issue rather than extractor issue), but with this hack:
```python
    def _call_api(self, object_type, object_fields, display_id, username, slug=None):
        lookup_key = object_type + 'Lookup'
        query={
                'query': '''{
  %s(lookup: {username: "%s"%s}) {
    %s
  }
}''' % (lookup_key, username, ', slug: "%s"' % slug if slug else '', object_fields)
            }
        import json
        print(json.dumps(query))
        return self._download_json(
            'https://www.mixcloud.com/graphql', display_id, query=query)['data'][lookup_key]
```
i can get the query as json, put it into a request body with the firefox dev tools' resend request thing, and it works
![Screenshot from 2023-09-15 09-28-00](https://github.com/yt-dlp/yt-dlp/assets/76261416/6a01220f-3635-4a77-9d1e-b54a55174525)
i assume there's some header/cookie that it's sending that makes it all work, but what it is i couldnt tell you

hope this helps
</details>
oh, much simpler than that actually
api url changed, `https://www.mixcloud.com/graphql` -> `https://app.mixcloud.com/graphql`
~~brb committing kms~~

Resolving patch:
diff --git a/yt_dlp/extractor/mixcloud.py b/yt_dlp/extractor/mixcloud.py
--- a/yt_dlp/extractor/mixcloud.py
+++ b/yt_dlp/extractor/mixcloud.py
@@ -20,7 +20,7 @@ class MixcloudBaseIE(InfoExtractor):
     def _call_api(self, object_type, object_fields, display_id, username, slug=None):
         lookup_key = object_type + 'Lookup'
         return self._download_json(
-            'https://www.mixcloud.com/graphql', display_id, query={
+            'https://app.mixcloud.com/graphql', display_id, query={
                 'query': '''{
   %s(lookup: {username: "%s"%s}) {
     %s
@@ -46,7 +46,15 @@ class MixcloudIE(MixcloudBaseIE):
             'view_count': int,
             'timestamp': 1321359578,
             'upload_date': '20111115',
+            'uploader_url': 'https://www.mixcloud.com/dholbach/',
+            'artist': 'Submorphics & Chino , Telekinesis, Porter Robinson, Enei, Breakage ft Jess Mills',
+            'duration': 3723,
+            'tags': [],
+            'comment_count': int,
+            'repost_count': int,
+            'like_count': int,
         },
+        'params': {'skip_download': 'm3u8'},
     }, {
         'url': 'http://www.mixcloud.com/gillespeterson/caribou-7-inch-vinyl-mix-chat/',
         'info_dict': {
@@ -60,7 +68,14 @@ class MixcloudIE(MixcloudBaseIE):
             'view_count': int,
             'timestamp': 1422987057,
             'upload_date': '20150203',
+            'uploader_url': 'https://www.mixcloud.com/gillespeterson/',
+            'duration': 2992,
+            'tags': [],
+            'comment_count': int,
+            'repost_count': int,
+            'like_count': int,
         },
+        'params': {'skip_download': '404 playback error on site'},
     }, {
         'url': 'https://beta.mixcloud.com/RedLightRadio/nosedrip-15-red-light-radio-01-18-2016/',
         'only_matching': True,
@@ -259,9 +274,9 @@ def _real_extract(self, url):
                 cloudcast_url = cloudcast.get('url')
                 if not cloudcast_url:
                     continue
-                slug = try_get(cloudcast, lambda x: x['slug'], compat_str)
+                item_slug = try_get(cloudcast, lambda x: x['slug'], compat_str)
                 owner_username = try_get(cloudcast, lambda x: x['owner']['username'], compat_str)
-                video_id = '%s_%s' % (owner_username, slug) if slug and owner_username else None
+                video_id = f'{owner_username}_{item_slug}' if item_slug and owner_username else None
                 entries.append(self.url_result(
                     cloudcast_url, MixcloudIE.ie_key(), video_id))
 
@@ -284,7 +299,7 @@ class MixcloudUserIE(MixcloudPlaylistBaseIE):
         'info_dict': {
             'id': 'dholbach_uploads',
             'title': 'Daniel Holbach (uploads)',
-            'description': 'md5:b60d776f0bab534c5dabe0a34e47a789',
+            'description': 'md5:a3f468a60ac8c3e1f8616380fc469b2b',
         },
         'playlist_mincount': 36,
     }, {
@@ -292,7 +307,7 @@ class MixcloudUserIE(MixcloudPlaylistBaseIE):
         'info_dict': {
             'id': 'dholbach_uploads',
             'title': 'Daniel Holbach (uploads)',
-            'description': 'md5:b60d776f0bab534c5dabe0a34e47a789',
+            'description': 'md5:a3f468a60ac8c3e1f8616380fc469b2b',
         },
         'playlist_mincount': 36,
     }, {
@@ -300,7 +315,7 @@ class MixcloudUserIE(MixcloudPlaylistBaseIE):
         'info_dict': {
             'id': 'dholbach_favorites',
             'title': 'Daniel Holbach (favorites)',
-            'description': 'md5:b60d776f0bab534c5dabe0a34e47a789',
+            'description': 'md5:a3f468a60ac8c3e1f8616380fc469b2b',
         },
         # 'params': {
         #     'playlist_items': '1-100',
@@ -323,9 +338,9 @@ class MixcloudUserIE(MixcloudPlaylistBaseIE):
         'info_dict': {
             'id': 'FirstEar_stream',
             'title': 'First Ear (stream)',
-            'description': 'Curators of good music\r\n\r\nfirstearmusic.com',
+            'description': 'we maraud for ears',
         },
-        'playlist_mincount': 271,
+        'playlist_mincount': 269,
     }]
 
     _TITLE_KEY = 'displayName'


Test patch: