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
134 changes: 99 additions & 35 deletions src/api/lastfm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@ interface LastfmErrorResponse {
message: string;
}

interface LastfmAuthTokenResponse {
token: string;
}

interface LastfmSessionResponse {
session: {
key: string;
name: string;
};
}

const LASTFM_API_URL = "https://ws.audioscrobbler.com/2.0/";

// Last.fm API 客户端
Expand All @@ -21,44 +32,66 @@ const lastfmClient: AxiosInstance = axios.create({
timeout: 15000,
});

// 响应拦截器,显示错误提示
export const getLastfmErrorMessage = (code: number, fallback: string) => {
switch (code) {
case 2:
return "无效服务 - 此服务不存在";
case 3:
return "无效方法 - 此包中不存在具有该名称的方法";
case 4:
return "身份验证失败 - 您没有访问该服务的权限";
case 5:
return "格式无效 - 此服务不存在该格式";
case 6:
return "参数无效 - 您的请求缺少必需参数";
case 7:
return "指定的资源无效";
case 8:
return "操作失败 - 出现了其他问题";
case 9:
return "会话密钥无效 - 请重新验证身份";
case 10:
return "API Key 无效,请检查是否填写正确";
case 13:
return "API Secret 无效,请检查是否填写正确";
case 16:
return "处理请求时出现临时错误,请重试";
case 26:
return "API 密钥已暂停";
case 29:
return "超出速率限制 - 您的 IP 地址在短时间内发出了过多请求";
default:
return fallback;
}
};

const shouldDisconnect = (code: number) => code === 9 || code === 4 || code === 26;

// 响应拦截器,处理需要自动断开连接的错误
lastfmClient.interceptors.response.use(
(response) => response,
(error: AxiosError<LastfmErrorResponse>) => {
const response = error.response;
if (!response) {
window.$message.error("Last.fm 请求失败,请检查网络连接");
return Promise.reject(error);
}

const { status, data } = response;

switch (status) {
case 403: {
const code = data?.error;
if (code === 9 || code === 4 || code === 26) {
window.$message.error("Last.fm 认证失败,需要重新授权,已断开与 Last.fm 的连接!");
disconnect();
} else {
window.$message.error("Last.fm 认证失败,可能需要重新授权");
}
break;
const code = data?.error;
if (typeof code === "number") {
if (code === 14 || code === 15) {
return Promise.reject(error);
}
case 401:
window.$message.error("Last.fm 未授权,已断开与 Last.fm 的连接!");
if (shouldDisconnect(code)) {
Comment on lines +81 to +86
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里仅在 data.error 为 number 时才处理错误码;但调用方(如 Setting 里的 connectLastfm)已在用 Number(...) 做兼容,说明 error 字段可能是 string。若这里严格判断 number,会导致部分错误既不提示也不触发 disconnect。建议对 string/number 都解析为 number(并校验非 NaN)后再走分支。

Copilot uses AI. Check for mistakes.
window.$message.error("Last.fm 认证失败,需要重新授权,已断开与 Last.fm 的连接!");
disconnect();
break;
case 429:
window.$message.error("Last.fm 请求过于频繁,请稍后再试");
break;
case 500:
case 502:
case 503:
window.$message.error("Last.fm 服务暂时不可用,请稍后再试");
break;
default:
window.$message.error("Last.fm 请求失败");
break;
} else {
window.$message.error(getLastfmErrorMessage(code, data?.message || "Last.fm 请求失败"));
}
} else if (status === 401) {
window.$message.error("Last.fm 未授权,已断开与 Last.fm 的连接!");
disconnect();
}
return Promise.reject(error);
},
Comment on lines 71 to 97
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

新的错误拦截器在处理 Last.fm 特定 API 错误方面做得很好。然而,它似乎移除了旧代码中对通用网络错误和标准 HTTP 错误状态(如 429、5xx)的处理。这可能导致在这些情况下出现静默失败,用户不会收到任何反馈。

例如:

  1. 如果发生网络错误(error.response 为假值),不会显示任何消息,而旧代码会显示“请检查网络连接”。
  2. 如果服务器返回 500 错误并附带 HTML 页面,data.error 将是 undefined,状态也不是 401,因此不会显示任何消息。旧代码对此有后备处理。

建议重新引入对这些情况的处理,以确保在所有错误条件下都能提供稳健的用户体验。

lastfmClient.interceptors.response.use(
  (response) => response,
  (error: AxiosError<LastfmErrorResponse>) => {
    const response = error.response;
    if (!response) {
      window.$message.error("Last.fm 请求失败,请检查网络连接");
      return Promise.reject(error);
    }

    const { status, data } = response;

    const code = data?.error;
    if (typeof code === "number") {
      if (code === 14 || code === 15) {
        return Promise.reject(error);
      }
      if (shouldDisconnect(code)) {
        window.$message.error("Last.fm 认证失败,需要重新授权,已断开与 Last.fm 的连接!");
        disconnect();
      } else {
        window.$message.error(getLastfmErrorMessage(code, data?.message || "Last.fm 请求失败"));
      }
    } else {
      switch (status) {
        case 401:
          window.$message.error("Last.fm 未授权,已断开与 Last.fm 的连接!");
          disconnect();
          break;
        case 429:
          window.$message.error("Last.fm 请求过于频繁,请稍后再试");
          break;
        case 500:
        case 502:
        case 503:
          window.$message.error("Last.fm 服务暂时不可用,请稍后再试");
          break;
        default:
          if (status >= 400) {
            window.$message.error(`Last.fm 请求失败 (${status})`);
          }
          break;
      }
    }
    return Promise.reject(error);
  },
);

Expand Down Expand Up @@ -122,11 +155,11 @@ const generateSignature = (params: Record<string, string | number>): string => {
* @param params 参数
* @param needAuth 是否需要签名
*/
const lastfmRequest = async (
const lastfmRequest = async <T extends object>(
method: string,
params: Record<string, string | number> = {},
needAuth: boolean = false,
) => {
): Promise<T> => {
const requestParams = prepareRequestParams(method, params);

if (needAuth) {
Expand All @@ -135,7 +168,21 @@ const lastfmRequest = async (

try {
const response = await lastfmClient.get("", { params: requestParams });
return response.data;
const data = response.data as LastfmErrorResponse | T;
if (data && typeof data === "object" && "error" in data) {
const apiError = new Error(
typeof data.message === "string" && data.message ? data.message : "Last.fm 请求失败",
) as AxiosError<LastfmErrorResponse>;
apiError.response = {
status: response.status,
data: data as LastfmErrorResponse,
statusText: response.statusText,
headers: response.headers,
config: response.config,
};
throw apiError;
}
return data as T;
Comment on lines +171 to +185
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lastfmRequest 在 HTTP 成功响应里检测到 { error, message } 后会手动 throw,但这条错误不会进入 axios 的 response error interceptor,因此 shouldDisconnect/disconnect 逻辑不会执行。像 session key 失效(9)这类需要自动断开连接的情况,可能会导致状态不一致(仍然保留旧 sessionKey)。建议在这里检测到 error 时复用 shouldDisconnect 并主动 disconnect(以及按需提示)。

Copilot uses AI. Check for mistakes.
} catch (error) {
console.error("Last.fm API 错误:", error);
throw error;
Expand All @@ -147,7 +194,10 @@ const lastfmRequest = async (
* @param method API 方法名
* @param params 参数
*/
const lastfmPostRequest = async (method: string, params: Record<string, string | number> = {}) => {
const lastfmPostRequest = async <T extends object>(
method: string,
params: Record<string, string | number> = {},
): Promise<T> => {
const requestParams = prepareRequestParams(method, params);

requestParams.api_sig = generateSignature(requestParams);
Expand All @@ -163,7 +213,21 @@ const lastfmPostRequest = async (method: string, params: Record<string, string |
"Content-Type": "application/x-www-form-urlencoded",
},
});
return response.data;
const data = response.data as LastfmErrorResponse | T;
if (data && typeof data === "object" && "error" in data) {
const apiError = new Error(
typeof data.message === "string" && data.message ? data.message : "Last.fm 请求失败",
) as AxiosError<LastfmErrorResponse>;
apiError.response = {
status: response.status,
data: data as LastfmErrorResponse,
statusText: response.statusText,
headers: response.headers,
config: response.config,
};
throw apiError;
}
return data as T;
Comment on lines +216 to +230
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lastfmPostRequest 同样在 2xx 响应体里发现 { error, message } 时手动 throw,这会绕过 axios 的 response error interceptor,从而跳过 shouldDisconnect/disconnect 逻辑(例如 code=9 时不会自动清理 sessionKey)。建议与 lastfmRequest 一样在此处复用 shouldDisconnect 并执行 disconnect,保证行为一致。

Copilot uses AI. Check for mistakes.
} catch (error) {
console.error("Last.fm API POST 错误:", error);
throw error;
Expand All @@ -173,8 +237,8 @@ const lastfmPostRequest = async (method: string, params: Record<string, string |
/**
* 获取认证令牌
*/
export const getAuthToken = async () => {
return await lastfmRequest("auth.getToken", {}, true);
export const getAuthToken = async (): Promise<LastfmAuthTokenResponse> => {
return await lastfmRequest<LastfmAuthTokenResponse>("auth.getToken", {}, true);
};

/**
Expand All @@ -190,8 +254,8 @@ export const getAuthUrl = (token: string): string => {
* 获取会话密钥
* @param token 认证令牌
*/
export const getSession = async (token: string) => {
return await lastfmRequest("auth.getSession", { token }, true);
export const getSession = async (token: string): Promise<LastfmSessionResponse> => {
return await lastfmRequest<LastfmSessionResponse>("auth.getSession", { token }, true);
};

/**
Expand Down
59 changes: 54 additions & 5 deletions src/components/Setting/config/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { computed, ref, h, markRaw } from "vue";
import { debounce } from "lodash-es";
import { NA } from "naive-ui";
import { disableDiscordRpc, enableDiscordRpc, updateDiscordConfig } from "@/core/player/PlayerIpc";
import { getAuthToken, getAuthUrl, getSession } from "@/api/lastfm";
import { getAuthToken, getAuthUrl, getSession, getLastfmErrorMessage } from "@/api/lastfm";
import StreamingServerList from "../components/StreamingServerList.vue";

export const useNetworkSettings = (): SettingConfig => {
Expand Down Expand Up @@ -147,6 +147,22 @@ export const useNetworkSettings = (): SettingConfig => {
const tokenResponse = await getAuthToken();
if (!tokenResponse.token) throw new Error("无法获取认证令牌");
const token = tokenResponse.token;

try {
await getSession(token);
} catch (error: any) {
const errorData = error.response?.data;
if (errorData && errorData.error) {
const code = Number(errorData.error);
if (code !== 14 && code !== 15) {
lastfmAuthLoading.value = false;
const errorMessage = errorData.message || "未知错误";
window.$message.error(getLastfmErrorMessage(code, `认证失败: ${errorMessage}`));
return;
}
}
Comment on lines +155 to +163
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里把 Last.fm 错误码 15(token 过期/无效)当作“继续流程/继续轮询”的情况处理:初次预检时 code===15 仍会继续打开授权页,轮询时 code===15 也会一直等待到超时。这样会导致用户永远无法完成授权。建议对 15 单独处理:停止轮询/关闭窗口并重新获取 token(或提示用户重试)。

Copilot uses AI. Check for mistakes.
}

const authUrl = getAuthUrl(token);

if (typeof window !== "undefined") {
Expand All @@ -170,8 +186,23 @@ export const useNetworkSettings = (): SettingConfig => {
window.$message.success(`已成功连接到 Last.fm 账号: ${sessionResponse.session.name}`);
lastfmAuthLoading.value = false;
}
} catch {
// 用户还未授权,继续等待
} catch (error: any) {
const errorData = error.response?.data;
if (errorData && errorData.error) {
const code = Number(errorData.error);
if (code === 14) {
window.$message.info("等待在 Last.fm 授权页面完成授权");
return;
Comment on lines +192 to +195
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

轮询中遇到错误码 14 时每 2 秒都会调用一次 $message.info,容易刷屏并影响使用体验。建议加一个本地 flag/节流逻辑,仅在首次检测到 14 时提示一次(后续静默等待即可)。

Copilot uses AI. Check for mistakes.
}
if (code !== 15) {
clearInterval(checkAuth);
authWindow?.close();
lastfmAuthLoading.value = false;
const message = window.$message || console;
const errorMessage = errorData.message || "未知错误";
message.error(getLastfmErrorMessage(code, `认证失败: ${errorMessage}`));
}
}
}
}, 2000);

Expand All @@ -181,12 +212,30 @@ export const useNetworkSettings = (): SettingConfig => {
lastfmAuthLoading.value = false;
window.$message.warning("授权超时,请重试");
}
}, 30000);
}, 60000);
}
} catch (error: any) {
console.error("Last.fm 连接失败:", error);
window.$message.error(`连接失败: ${error.message || "未知错误"}`);
lastfmAuthLoading.value = false;

const message = window.$message || {
error: console.error,
warning: console.warn,
success: console.log,
};

const errorData = error.response?.data;

if (errorData && typeof errorData === "object" && "error" in errorData) {
const errorCode = Number(errorData.error);
const errorMessage = errorData.message || "未知错误";
message.error(getLastfmErrorMessage(errorCode, `连接失败: ${errorMessage}`));
} else {
const msg = error.message || "未知错误";
if (msg !== "canceled") {
message.error(`连接失败: ${msg}`);
}
}
}
};

Expand Down
Loading