From 23a1e993b10a8d8b80b37d60baf8fcc79ca1a70d Mon Sep 17 00:00:00 2001
From: ll <48448919+llsccm@users.noreply.github.com>
Date: Wed, 27 May 2026 20:11:31 +0800
Subject: [PATCH] fix(provider): replace joox API
- new joox API
- use fake cookie
---
README.md | 2 +-
src/provider/joox.js | 74 ++++++++++++++++++++++++++++----------------
2 files changed, 49 insertions(+), 27 deletions(-)
diff --git a/README.md b/README.md
index 0c08ac17ae..49e5a0cb56 100644
--- a/README.md
+++ b/README.md
@@ -172,7 +172,7 @@ node app.js -o bilibili ytdlp
| 酷我音乐 | `kuwo` | | |
| 波点音乐 | `bodian` | ✅ | |
| 咪咕音乐 | `migu` | ✅ | 需要准备自己的 `MIGU_COOKIE`(请参阅下方〈环境变量〉处)。 |
-| JOOX | `joox` | | 需要准备自己的 `JOOX_COOKIE`(请参阅下方〈环境变量〉处)。似乎有严格地区限制。 |
+| JOOX | `joox` | | 需要准备自己的 `JOOX_COOKIE`(请参阅下方〈环境变量〉处)。
仅支持 Hong Kong, Macau, Thailand, Malaysia, Indonesia. |
| YouTube(纯 JS 解析方式) | `youtube` | | 需要 Google 认定的**非中国大陆区域** IP 地址。 |
| YouTube(通过 `youtube-dl`) | `youtubedl` | | 需要自行安装 `youtube-dl`。 |
| YouTube(通过 `yt-dlp`) | `ytdlp` | ✅ | 需要自行安装 `yt-dlp`(`youtube-dl` 仍在活跃维护的 fork)。 |
diff --git a/src/provider/joox.js b/src/provider/joox.js
index c7f1cd02fe..d4884fc070 100644
--- a/src/provider/joox.js
+++ b/src/provider/joox.js
@@ -1,6 +1,5 @@
const insure = require('./insure');
const select = require('./select');
-const crypto = require('../crypto');
const request = require('../request');
const { getManagedCacheStorage } = require('../cache');
@@ -10,7 +9,9 @@ const headers = {
// Refer to #95, you should register an account
// on Joox to use their service. We allow users
// to specify it manually.
- cookie: process.env.JOOX_COOKIE || null, // 'wmid=; session_key=;'
+ cookie:
+ process.env.JOOX_COOKIE ||
+ 'wmid=142420656; user_type=1; country=hk; session_key=2a5d97d05dc8fe238150184eaf3519ad; uid=142420656; backendCountry=hk',
};
const fit = (info) => {
@@ -21,15 +22,17 @@ const fit = (info) => {
};
const format = (song) => {
- const { decode } = crypto.base64;
return {
- id: song.songid,
- name: decode(song.info1 || ''),
- duration: song.playtime * 1000,
- album: { id: song.albummid, name: decode(song.info3 || '') },
- artists: song.singer_list.map(({ id, name }) => ({
+ id: song.id,
+ name: song.name || '',
+ duration: (parseInt(song.playtime) || 0) * 1000,
+ album: {
+ id: song.album_id,
+ name: song.album_name || '',
+ },
+ artists: (song.artist_list || []).map(({ id, name }) => ({
id,
- name: decode(name || ''),
+ name: name || '',
})),
};
};
@@ -37,17 +40,20 @@ const format = (song) => {
const search = (info) => {
const keyword = fit(info);
const url =
- 'http://api-jooxtt.sanook.com/web-fcgi-bin/web_search?' +
- 'country=hk&lang=zh_TW&' +
- 'search_input=' +
+ 'https://cache.api.joox.com/openjoox/v2/search_type?' +
+ 'country=hk&lang=zh_TW&key=' +
encodeURIComponent(keyword) +
- '&sin=0&ein=30';
+ '&type=0';
return request('GET', url, headers)
.then((response) => response.body())
.then((body) => {
- const jsonBody = JSON.parse(body.replace(/'/g, '"'));
- const list = jsonBody.itemlist.map(format);
+ const jsonBody = JSON.parse(body);
+ const tracks = jsonBody.tracks || [];
+ const list = tracks
+ .map((track) => (Array.isArray(track) ? track[0] : track))
+ .filter(Boolean)
+ .map(format);
const matched = select(list, info);
return matched ? matched.id : Promise.reject();
});
@@ -55,22 +61,38 @@ const search = (info) => {
const track = (id) => {
const url =
- 'http://api.joox.com/web-fcgi-bin/web_get_songinfo?' +
+ 'https://api.joox.com/web-fcgi-bin/web_get_songinfo?' +
'songid=' +
id +
- '&country=hk&lang=zh_cn&from_type=-1&' +
- 'channel_id=-1&_=' +
+ '&country=hk&lang=zh_TW&from_type=-1&channel_id=-1&_=' +
new Date().getTime();
return request('GET', url, headers)
- .then((response) => response.jsonp())
- .then((jsonBody) => {
- const songUrl = (
- jsonBody.r320Url ||
- jsonBody.r192Url ||
- jsonBody.mp3Url ||
- jsonBody.m4aUrl
- ).replace(/M\d00([\w]+).mp3/, 'M800$1.mp3');
+ .then((response) => response.body())
+ .then((body) => {
+ const jsonBody = JSON.parse(
+ body.replace(/^MusicInfoCallback\(/, '').replace(/\);?$/, '')
+ );
+ const candidateFields = [
+ 'master_tapeUrl',
+ 'master_tapeURL',
+ 'master_tape_url',
+ 'hiresUrl',
+ 'hiresURL',
+ 'hires_url',
+ 'flacUrl',
+ 'flacURL',
+ 'flac_url',
+ 'r320Url',
+ 'r192Url',
+ 'mp3Url',
+ 'm4aUrl',
+ ];
+
+ const songUrl = candidateFields
+ .map((field) => jsonBody[field])
+ .find((url) => url && String(url).startsWith('http'));
+
if (songUrl) return songUrl;
else return Promise.reject();
})