Repository: yt-dlp
Issue: [kuwo] rewrite the outdated kuwo extractor
Labels: geo-blocked,site-bug,patch-available

### 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

China

### Provide a description that is worded well enough to be understood

>[!NOTE]
>[geo-blocked] you can NOT access anything on kuwo outside china, it gives HTTP 500

#### Example URL
`http://kuwo.cn/play_detail/28115171`

#### Description
the current code is 8 years old. it needs an update

I have a VPN to debug though I'm not in china.
I have no kuwo account so please ask someone else if necessary.

the verbose output below from the latest nightly shows that:
1. the _VALID_URL needs an update(it's processed by the genericie)
2. it's geo-blocked outside china(see the response code)

#### Related info

Related to pr: #7470 

EDIT: there may be easier way to extract than changing the API, the current code fails on __metadata extraction__. `http://antiserver.kuwo.cn/anti.s` is still usable with some param changes

since the URL path has changed, _VALID_URL should be updated.

playurl API URL:
`http://www.kuwo.cn/api/v1/www/music/playUrl`
query: `{'mid': song_id}`

not sure how to extract a specific format, the API just gives 128k.

required headers in API request:
* `Hm_Iuvt_cdb524f42f23cer9b268564v7y735ewrq2324` in `Cookies`
* `Secret`
the `Secret` header seems to be related to `Hm_Iuvt_cdb524f42f23cer9b268564v7y735ewrq2324` value.
but it has nothing to do with the query param(`mid`)
haven't tested if they'll expire yet.

EDIT: the header `Secret` can be obtained by the func below with:
`f(<unescaped Hm_Iuvt_cdb524f42f23cer9b268564v7y735ewrq2324 value>, "Hm_Iuvt_cdb524f42f23cer9b268564v7y735ewrq2324")`
```js
function f(t, e) {
  if (null == e || e.length <= 0) return null;
  for (var n = "", i = 0; i < e.length; i++) n += e.charCodeAt(i).toString();
  var o = Math.floor(n.length / 5),
    r = parseInt(
      n.charAt(o) +
        n.charAt(2 * o) +
        n.charAt(3 * o) +
        n.charAt(4 * o) +
        n.charAt(5 * o)
    ),
    c = Math.ceil(e.length / 2),
    l = Math.pow(2, 31) - 1;
  if (r < 2) return null;
  var d = Math.round(1e9 * Math.random()) % 1e8;
  for (n += d; n.length > 10; )
    n = (
      parseInt(n.substring(0, 10)) + parseInt(n.substring(10, n.length))
    ).toString();
  n = (r * n + c) % l;
  var f = "",
    h = "";
  for (i = 0; i < t.length; i++)
    (h +=
      (f = parseInt(t.charCodeAt(i) ^ Math.floor((n / l) * 255))) < 16
        ? "0" + f.toString(16)
        : f.toString(16)),
      (n = (r * n + c) % l);
  for (d = d.toString(16); d.length < 8; ) d = "0" + d;
  return (h += d);
}
```
python algorithm:
```py
import random
import math


def f(t_cookie_val, e_cookie_key):
    def charAt(lst, idx):
        try:
            return lst[idx]
        except BaseException:
            return ''

    def parseInt(value: str):
        if isinstance(value, int):
            return value
        num_str = ''
        for char in value:
            if char.isdigit():
                num_str += char
            else:
                break
        return int(num_str) if num_str else None

    def toString(value):
        if len(str(value)) > 20:
            return f'{value:.16e}'
        return str(value)

    if e_cookie_key is None or len(e_cookie_key) <= 0:
        return None
    n = ''
    for i in e_cookie_key:
        n += str(ord(i))

    o = len(n) // 5
    r = int(''.join(charAt(n, i * o) for i in range(1, 6)))
    c = len(e_cookie_key) // 2 + 1
    l = pow(2, 31) - 1

    if r < 2:
        return None
    d = random.randint(0, int(1e8 - 1))

    n += str(d)
    while len(n) > 10:
        n = toString(parseInt(n[:10]) + parseInt(n[10:]))
    n = (r * int(n) + c) % l

    f = ''
    h = ''
    i = 0
    while i < len(t_cookie_val):
        f = parseInt(ord(t_cookie_val[i]) ^ math.floor((n / l) * 255))
        if f < 16:
            h += '0'
        h += hex(f)[2:]
        n = (r * n + c) % l
        i += 1
    d = hex(d)[2:]
    while len(d) < 8:
        d = '0' + d
    h += d
    return h


print(f('AnMG6YKJNGDkSmm2TY7xJMmw67XFiCmN', 'Hm_Iuvt_cdb524f42f23cer9b268564v7y735ewrq2324'))
```

### 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', 'http://kuwo.cn/play_detail/28115171']
[debug] Encodings: locale cp936, fs utf-8, pref cp936, out utf-8, error utf-8, screen utf-8
[debug] yt-dlp version nightly@2024.08.06.232802 from yt-dlp/yt-dlp-nightly-builds [a06508664] (win_exe)
[debug] Python 3.8.10 (CPython AMD64 64bit) - Windows-10-10.0.19045-SP0 (OpenSSL 1.1.1k  25 Mar 2021)
[debug] exe versions: ffmpeg N-115821-g0060a368b1-20240613 (setts), ffprobe N-115821-g0060a368b1-20240613
[debug] Optional libraries: Cryptodome-3.20.0, brotli-1.1.0, certifi-2024.07.04, curl_cffi-0.5.10, mutagen-1.47.0, requests-2.32.3, sqlite3-3.35.5, urllib3-2.2.2, websockets-12.0
[debug] Proxy map: {}
[debug] Request Handlers: urllib, requests, websockets, curl_cffi
[debug] Loaded 1830 extractors
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp-nightly-builds/releases/latest
Latest version: nightly@2024.08.06.232802 from yt-dlp/yt-dlp-nightly-builds
yt-dlp is up to date (nightly@2024.08.06.232802 from yt-dlp/yt-dlp-nightly-builds)
[generic] Extracting URL: http://kuwo.cn/play_detail/28115171
[generic] 28115171: Downloading webpage
ERROR: [generic] Unable to download webpage: HTTP Error 500: OK (caused by <HTTPError 500: OK>)
  File "yt_dlp\extractor\common.py", line 740, in extract
  File "yt_dlp\extractor\generic.py", line 2384, in _real_extract
  File "yt_dlp\extractor\common.py", line 909, in _request_webpage

  File "yt_dlp\extractor\common.py", line 896, in _request_webpage
  File "yt_dlp\YoutubeDL.py", line 4165, in urlopen
  File "yt_dlp\networking\common.py", line 117, in send
  File "yt_dlp\networking\_helper.py", line 208, in wrapper
  File "yt_dlp\networking\common.py", line 340, in send
  File "yt_dlp\networking\_requests.py", line 365, in _send
yt_dlp.networking.exceptions.HTTPError: HTTP Error 500: OK
```