Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions electron/main/apis/netease/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const NON_CACHEABLE: ReadonlySet<string> = new Set([
"playlist_detail",
"user_playlist",
"user_subcount",
"user_audio",
"user_cloud",
"user_cloud_del",
"cloud_upload_check",
Expand All @@ -59,10 +60,15 @@ const NON_CACHEABLE: ReadonlySet<string> = new Set([
"cloud_upload_check_v2",
"cloud_song_import",
"album_sub",
"dj_sub",
"dj_sublist",
"dj_detail",
"dj_program",
"playmode_intelligence",
"personal_fm",
"fm_trash",
"recommend_songs",
"sso_login_token",
]);

/** 国内 IP 前缀池 */
Expand Down
14 changes: 14 additions & 0 deletions electron/main/apis/netease/modules/dj_detail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* 播客详情
*
* params:
* - rid 播客 ID
*/

import { createOption } from "../core/option";
import type { NeteaseModule } from "../core/types";

const djDetail: NeteaseModule = (query, request) =>
request("/api/djradio/v2/get", { id: query.rid }, createOption(query, "weapi"));

export default djDetail;
26 changes: 26 additions & 0 deletions electron/main/apis/netease/modules/dj_program.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* 播客节目列表
*
* params:
* - rid 播客 ID
* - limit / offset 分页
* - asc 是否按时间升序
*/

import { createOption } from "../core/option";
import { UA_MAP } from "../core/config";
import type { NeteaseModule } from "../core/types";

const djProgram: NeteaseModule = (query, request) => {
const data = {
radioId: query.rid,
limit: query.limit ?? 30,
offset: query.offset ?? 0,
asc: query.asc === true || query.asc === "true" || query.asc === 1,
};
const option = createOption(query, "eapi");
option.ua ||= UA_MAP.api.pc;
return request("/api/v6/dj/program/byradio", data, option);
};

export default djProgram;
26 changes: 26 additions & 0 deletions electron/main/apis/netease/modules/dj_program_search.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* 搜索指定播客内的声音
*
* params:
* - rid 播客 ID
* - keyword 搜索关键词
* - limit / offset 分页
*/

import { UA_MAP } from "../core/config";
import { createOption } from "../core/option";
import type { NeteaseModule } from "../core/types";

const djProgramSearch: NeteaseModule = (query, request) => {
const data = {
radioId: query.rid,
keyword: query.keyword,
limit: query.limit ?? 200,
offset: query.offset ?? 0,
};
const option = createOption(query, "eapi");
option.ua ||= UA_MAP.api.pc;
return request("/api/dj/radio/program/search", data, option);
};

export default djProgramSearch;
17 changes: 17 additions & 0 deletions electron/main/apis/netease/modules/dj_sub.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* 收藏或取消收藏播客
*
* params:
* - rid 播客 ID
* - t 1 收藏,0 取消收藏
*/

import { createOption } from "../core/option";
import type { NeteaseModule } from "../core/types";

const djSub: NeteaseModule = (query, request) => {
const action = Number(query.t) === 1 ? "sub" : "unsub";
return request(`/api/djradio/${action}`, { id: query.rid }, createOption(query, "weapi"));
};

export default djSub;
21 changes: 21 additions & 0 deletions electron/main/apis/netease/modules/dj_sublist.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* 用户收藏的播客列表
*
* params:
* - limit / offset 分页
*/

import { createOption } from "../core/option";
import type { NeteaseModule } from "../core/types";

const djSublist: NeteaseModule = (query, request) => {
const data = {
limit: query.limit ?? 30,
offset: query.offset ?? 0,
total: true,
timestamp: query.timestamp ?? Date.now(),
};
return request("/api/djradio/get/subed", data, createOption(query, "weapi"));
};

export default djSublist;
14 changes: 14 additions & 0 deletions electron/main/apis/netease/modules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import login_refresh from "./login_refresh";
import login_status from "./login_status";
import logout from "./logout";
import register_anonimous from "./register_anonimous";
import sso_login_token from "./sso_login_token";

// 用户
import user_account from "./user_account";
Expand All @@ -38,6 +39,7 @@ import user_level from "./user_level";
import user_playlist from "./user_playlist";
import user_record from "./user_record";
import user_subcount from "./user_subcount";
import user_audio from "./user_audio";

// 搜索
import cloudsearch from "./cloudsearch";
Expand Down Expand Up @@ -101,6 +103,11 @@ import artist_songs from "./artist_songs";
import album_sublist from "./album_sublist";
import artist_sub from "./artist_sub";
import artist_sublist from "./artist_sublist";
import dj_detail from "./dj_detail";
import dj_program from "./dj_program";
import dj_program_search from "./dj_program_search";
import dj_sub from "./dj_sub";
import dj_sublist from "./dj_sublist";

export const modules: Record<string, NeteaseModule> = {
captcha_sent,
Expand All @@ -114,6 +121,7 @@ export const modules: Record<string, NeteaseModule> = {
login_status,
logout,
register_anonimous,
sso_login_token,

user_account,
user_cloud,
Expand All @@ -132,6 +140,7 @@ export const modules: Record<string, NeteaseModule> = {
user_playlist,
user_record,
user_subcount,
user_audio,

cloudsearch,
search,
Expand Down Expand Up @@ -186,4 +195,9 @@ export const modules: Record<string, NeteaseModule> = {
album_sublist,
artist_sub,
artist_sublist,
dj_detail,
dj_program,
dj_program_search,
dj_sub,
dj_sublist,
};
79 changes: 79 additions & 0 deletions electron/main/apis/netease/modules/podcast.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { describe, expect, it, vi } from "vitest";
import type { Query } from "../core/option";
import type { RequestFn } from "../core/types";
import djDetail from "./dj_detail";
import djProgram from "./dj_program";
import djProgramSearch from "./dj_program_search";
import djSub from "./dj_sub";
import djSublist from "./dj_sublist";
import userAudio from "./user_audio";

const createRequest = () =>
vi.fn().mockResolvedValue({ status: 200, body: { code: 200 }, cookie: [] });

describe("网易云播客接口", () => {
it("请求用户创建和收藏的播客", async () => {
const request = createRequest();

await userAudio({ uid: 1 } satisfies Query, request as unknown as RequestFn);
await djSublist({ limit: 20, offset: 5 } satisfies Query, request as unknown as RequestFn);

expect(request.mock.calls.map(([url]) => url)).toEqual([
"/api/djradio/get/byuser",
"/api/djradio/get/subed",
]);
expect(request.mock.calls[0]?.[1]).toMatchObject({ userId: 1, timestamp: expect.any(Number) });
expect(request.mock.calls[1]?.[1]).toMatchObject({
limit: 20,
offset: 5,
total: true,
timestamp: expect.any(Number),
});
});

it("请求播客详情和节目列表", async () => {
const request = createRequest();

await djDetail({ rid: "2" } satisfies Query, request as unknown as RequestFn);
await djProgram(
{ rid: "2", limit: 50, offset: 10, asc: true } satisfies Query,
request as unknown as RequestFn,
);
await djProgramSearch(
{ rid: "2", keyword: "测试", limit: 200, offset: 0 } satisfies Query,
request as unknown as RequestFn,
);

expect(request.mock.calls.map(([url]) => url)).toEqual([
"/api/djradio/v2/get",
"/api/v6/dj/program/byradio",
"/api/dj/radio/program/search",
]);
expect(request.mock.calls[1]?.[1]).toMatchObject({
radioId: "2",
limit: 50,
offset: 10,
asc: true,
});
expect(request.mock.calls[1]?.[2]).toMatchObject({ crypto: "eapi" });
expect(request.mock.calls[2]?.[1]).toMatchObject({
radioId: "2",
keyword: "测试",
limit: 200,
offset: 0,
});
expect(request.mock.calls[2]?.[2]).toMatchObject({ crypto: "eapi" });
});

it("按目标状态收藏或取消收藏播客", async () => {
const request = createRequest();

await djSub({ rid: "3", t: 1 } satisfies Query, request as unknown as RequestFn);
await djSub({ rid: "3", t: 0 } satisfies Query, request as unknown as RequestFn);

expect(request.mock.calls.map(([url]) => url)).toEqual([
"/api/djradio/sub",
"/api/djradio/unsub",
]);
});
});
20 changes: 20 additions & 0 deletions electron/main/apis/netease/modules/sso_login_token.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { describe, expect, it, vi } from "vitest";
import type { Query } from "../core/option";
import type { RequestFn } from "../core/types";
import ssoLoginToken from "./sso_login_token";

describe("网易云网页单点登录接口", () => {
it("使用 eapi 获取短期登录 token", async () => {
const request = vi
.fn()
.mockResolvedValue({ status: 200, body: { code: 200, token: "token" }, cookie: [] });

await ssoLoginToken({} satisfies Query, request as unknown as RequestFn);

expect(request).toHaveBeenCalledWith(
"/api/sso/login/token",
{},
expect.objectContaining({ crypto: "eapi" }),
);
});
});
11 changes: 11 additions & 0 deletions electron/main/apis/netease/modules/sso_login_token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* 获取网页单点登录使用的短期 token
*/

import { createOption } from "../core/option";
import type { NeteaseModule } from "../core/types";

const ssoLoginToken: NeteaseModule = (query, request) =>
request("/api/sso/login/token", {}, createOption(query, "eapi"));

export default ssoLoginToken;
18 changes: 18 additions & 0 deletions electron/main/apis/netease/modules/user_audio.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* 用户创建的播客
*
* params:
* - uid 用户 ID
*/

import { createOption } from "../core/option";
import type { NeteaseModule } from "../core/types";

const userAudio: NeteaseModule = (query, request) =>
request(
"/api/djradio/get/byuser",
{ userId: query.uid, timestamp: query.timestamp ?? Date.now() },
createOption(query, "weapi"),
);

export default userAudio;
11 changes: 11 additions & 0 deletions electron/main/ipc/apis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { cookieToJson } from "@main/apis/netease/core/cookie";
import { callQQMusic } from "@main/apis/qqmusic";
import { callKugou } from "@main/apis/kugou";
import { openNeteaseLoginWindow } from "@main/window/login";
import { openNeteasePodcastManager } from "@main/window/neteasePodcastManager";
import { coreLog } from "@main/utils/logger";
import type { ApiPlatform } from "@shared/types/apis";

Expand Down Expand Up @@ -88,4 +89,14 @@ export const registerApisIpc = (): void => {
mergeNeteaseCookies(parsed);
return { ok: true };
});

ipcMain.handle("apis:openPodcastManager", async (_event, userId: number) => {
try {
await openNeteasePodcastManager(userId);
return { ok: true };
} catch (err) {
coreLog.warn("[apis] openPodcastManager failed:", err);
return { ok: false, error: err instanceof Error ? err.message : String(err) };
}
});
};
24 changes: 24 additions & 0 deletions electron/main/window/neteasePodcastManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { shell } from "electron";
import { callNetease } from "@main/apis/netease";

const NETEASE_SSO_URL = "https://music.163.com/sso/login";
const PODCAST_MANAGER_PATH = "/radio/my/#/plist";

/**
* 使用应用登录态在系统浏览器打开网易云播客管理页
* @param userId - 当前网易云用户 ID
*/
export const openNeteasePodcastManager = async (userId: number): Promise<void> => {
if (!Number.isSafeInteger(userId) || userId <= 0) throw new Error("invalid netease user id");

const { body } = await callNetease("sso_login_token");
const token = typeof body.token === "string" ? body.token : "";
if (!token) throw new Error("netease sso token missing");

const url = new URL(NETEASE_SSO_URL);
url.searchParams.set("token", token);
url.searchParams.set("uid", String(userId));
url.searchParams.set("url", PODCAST_MANAGER_PATH);
url.searchParams.set("forceReplaceLogin", "true");
await shell.openExternal(url.toString());
};
2 changes: 2 additions & 0 deletions electron/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,8 @@ const api = {
// 手动写入 cookie 登录
setCookie: (platform: string, cookie: string) =>
ipcRenderer.invoke("apis:setCookie", platform, cookie),
// 使用应用登录态在系统浏览器打开网易云播客管理页
openPodcastManager: (userId: number) => ipcRenderer.invoke("apis:openPodcastManager", userId),
},
cloud: {
// 弹出文件选择器选择待上传歌曲
Expand Down
5 changes: 5 additions & 0 deletions shared/types/apis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,9 @@ export interface ApisApi {
platform: ApiPlatform,
cookie: string,
) => Promise<{ ok: true } | { ok: false; error: string }>;
/**
* 使用应用内网易云登录态在系统浏览器打开播客管理页
* @param userId - 当前网易云用户 ID
*/
openPodcastManager: (userId: number) => Promise<{ ok: true } | { ok: false; error: string }>;
}
6 changes: 6 additions & 0 deletions shared/types/player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,12 @@ export interface Track {
mtime?: number;
/** 创建时间(Unix ms) */
ctime?: number;
/** 发布或更新时间(Unix ms) */
publishTime?: number;
/** 在线内容播放量 */
playCount?: number;
/** 在线内容点赞数 */
likedCount?: number;
/** 音质信息 */
quality?: AudioQuality;
/** 付费等级 */
Expand Down
Loading