fix: cookie jar + XSRF token for Listenarr POST requests
This commit is contained in:
@@ -19,6 +19,7 @@
|
||||
"@solana/spl-token": "^0.4.14",
|
||||
"@solana/web3.js": "^1.87.6",
|
||||
"axios": "^1.6.2",
|
||||
"axios-cookiejar-support": "^5.0.5",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
@@ -30,6 +31,7 @@
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"morgan": "^1.10.0",
|
||||
"socket.io": "^4.7.3",
|
||||
"tough-cookie": "^5.1.2",
|
||||
"tweetnacl": "^1.0.3",
|
||||
"ws": "^8.15.1"
|
||||
},
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
import axios from "axios";
|
||||
import { wrapper } from "axios-cookiejar-support";
|
||||
import { CookieJar } from "tough-cookie";
|
||||
|
||||
const LISTENARR_URL = process.env.LISTENARR_URL || "";
|
||||
const LISTENARR_API_KEY = process.env.LISTENARR_API_KEY || "";
|
||||
|
||||
const listenarrApi = axios.create({
|
||||
baseURL: LISTENARR_URL,
|
||||
timeout: 15000,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(LISTENARR_API_KEY ? { "X-Api-Key": LISTENARR_API_KEY } : {}),
|
||||
},
|
||||
});
|
||||
const jar = new CookieJar();
|
||||
const listenarrApi = wrapper(
|
||||
axios.create({
|
||||
baseURL: LISTENARR_URL,
|
||||
timeout: 15000,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(LISTENARR_API_KEY ? { "X-Api-Key": LISTENARR_API_KEY } : {}),
|
||||
},
|
||||
withCredentials: true,
|
||||
jar,
|
||||
}),
|
||||
);
|
||||
|
||||
// ponytail: fetch CSRF token from Listenarr's /antiforgery/token endpoint
|
||||
// ponytail: fetch + cache CSRF token (cookie comes along via jar)
|
||||
let cachedCsrfToken: string | null = null;
|
||||
async function getCsrfToken(): Promise<string | null> {
|
||||
if (cachedCsrfToken) return cachedCsrfToken;
|
||||
@@ -25,9 +32,7 @@ async function getCsrfToken(): Promise<string | null> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function searchAudiobooks(
|
||||
query: string
|
||||
): Promise<any[]> {
|
||||
export async function searchAudiobooks(query: string): Promise<any[]> {
|
||||
const resp = await listenarrApi.get("/api/v1/search/audible", {
|
||||
params: { query },
|
||||
});
|
||||
@@ -55,7 +60,6 @@ export async function addAudiobook(options: {
|
||||
explicit?: boolean;
|
||||
abridged?: boolean;
|
||||
}): Promise<any> {
|
||||
// Defensive: ensure series/seriesNumber are always strings, not arrays
|
||||
const series = typeof options.series === "string" ? options.series : "";
|
||||
const seriesNumber = typeof options.seriesNumber === "string" ? options.seriesNumber : "";
|
||||
|
||||
@@ -74,8 +78,8 @@ export async function addAudiobook(options: {
|
||||
releaseDate: options.releaseDate || "",
|
||||
runtime: options.runtimeLengthMin ? options.runtimeLengthMin * 60 : 0,
|
||||
genres: options.genres || [],
|
||||
series: series,
|
||||
seriesNumber: seriesNumber,
|
||||
series,
|
||||
seriesNumber,
|
||||
isbn: Array.isArray(options.isbn) ? options.isbn : (typeof options.isbn === "string" && options.isbn ? options.isbn.split(",") : []),
|
||||
explicit: options.explicit || false,
|
||||
abridged: options.abridged || false,
|
||||
@@ -93,13 +97,13 @@ export async function addAudiobook(options: {
|
||||
const resp = await listenarrApi.post("/api/v1/library/add", body, { headers });
|
||||
return resp.data;
|
||||
} catch (err: any) {
|
||||
// ponytail: retry once on CSRF failure after refreshing token
|
||||
// ponytail: retry once on CSRF failure after refreshing token + cookie
|
||||
if (err?.response?.status === 400 && /csrf|xsrf|antiforgery/i.test(JSON.stringify(err?.response?.data))) {
|
||||
cachedCsrfToken = null;
|
||||
const freshToken = await getCsrfToken();
|
||||
if (freshToken) {
|
||||
const retryResp = await listenarrApi.post("/api/v1/library/add", body, { headers: { "X-XSRF-TOKEN": freshToken } });
|
||||
return retryResp.data;
|
||||
const resp = await listenarrApi.post("/api/v1/library/add", body, { headers: { "X-XSRF-TOKEN": freshToken } });
|
||||
return resp.data;
|
||||
}
|
||||
}
|
||||
throw err;
|
||||
@@ -112,9 +116,7 @@ export async function getListenarrStatus(): Promise<{
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const resp = await listenarrApi.get("/api/v1/system/info", {
|
||||
timeout: 5000,
|
||||
});
|
||||
const resp = await listenarrApi.get("/api/v1/system/info", { timeout: 5000 });
|
||||
return { connected: true, data: resp.data };
|
||||
} catch (error: any) {
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user