fix: cookie jar + XSRF token for Listenarr POST requests

This commit is contained in:
2026-07-01 09:55:31 -04:00
parent 215aad16e0
commit a022e0f7b7
2 changed files with 25 additions and 21 deletions
+2
View File
@@ -19,6 +19,7 @@
"@solana/spl-token": "^0.4.14", "@solana/spl-token": "^0.4.14",
"@solana/web3.js": "^1.87.6", "@solana/web3.js": "^1.87.6",
"axios": "^1.6.2", "axios": "^1.6.2",
"axios-cookiejar-support": "^5.0.5",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
@@ -30,6 +31,7 @@
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"socket.io": "^4.7.3", "socket.io": "^4.7.3",
"tough-cookie": "^5.1.2",
"tweetnacl": "^1.0.3", "tweetnacl": "^1.0.3",
"ws": "^8.15.1" "ws": "^8.15.1"
}, },
+23 -21
View File
@@ -1,18 +1,25 @@
import axios from "axios"; import axios from "axios";
import { wrapper } from "axios-cookiejar-support";
import { CookieJar } from "tough-cookie";
const LISTENARR_URL = process.env.LISTENARR_URL || ""; const LISTENARR_URL = process.env.LISTENARR_URL || "";
const LISTENARR_API_KEY = process.env.LISTENARR_API_KEY || ""; const LISTENARR_API_KEY = process.env.LISTENARR_API_KEY || "";
const listenarrApi = axios.create({ const jar = new CookieJar();
baseURL: LISTENARR_URL, const listenarrApi = wrapper(
timeout: 15000, axios.create({
headers: { baseURL: LISTENARR_URL,
"Content-Type": "application/json", timeout: 15000,
...(LISTENARR_API_KEY ? { "X-Api-Key": LISTENARR_API_KEY } : {}), 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; let cachedCsrfToken: string | null = null;
async function getCsrfToken(): Promise<string | null> { async function getCsrfToken(): Promise<string | null> {
if (cachedCsrfToken) return cachedCsrfToken; if (cachedCsrfToken) return cachedCsrfToken;
@@ -25,9 +32,7 @@ async function getCsrfToken(): Promise<string | null> {
} }
} }
export async function searchAudiobooks( export async function searchAudiobooks(query: string): Promise<any[]> {
query: string
): Promise<any[]> {
const resp = await listenarrApi.get("/api/v1/search/audible", { const resp = await listenarrApi.get("/api/v1/search/audible", {
params: { query }, params: { query },
}); });
@@ -55,7 +60,6 @@ export async function addAudiobook(options: {
explicit?: boolean; explicit?: boolean;
abridged?: boolean; abridged?: boolean;
}): Promise<any> { }): Promise<any> {
// Defensive: ensure series/seriesNumber are always strings, not arrays
const series = typeof options.series === "string" ? options.series : ""; const series = typeof options.series === "string" ? options.series : "";
const seriesNumber = typeof options.seriesNumber === "string" ? options.seriesNumber : ""; const seriesNumber = typeof options.seriesNumber === "string" ? options.seriesNumber : "";
@@ -74,8 +78,8 @@ export async function addAudiobook(options: {
releaseDate: options.releaseDate || "", releaseDate: options.releaseDate || "",
runtime: options.runtimeLengthMin ? options.runtimeLengthMin * 60 : 0, runtime: options.runtimeLengthMin ? options.runtimeLengthMin * 60 : 0,
genres: options.genres || [], genres: options.genres || [],
series: series, series,
seriesNumber: seriesNumber, seriesNumber,
isbn: Array.isArray(options.isbn) ? options.isbn : (typeof options.isbn === "string" && options.isbn ? options.isbn.split(",") : []), isbn: Array.isArray(options.isbn) ? options.isbn : (typeof options.isbn === "string" && options.isbn ? options.isbn.split(",") : []),
explicit: options.explicit || false, explicit: options.explicit || false,
abridged: options.abridged || 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 }); const resp = await listenarrApi.post("/api/v1/library/add", body, { headers });
return resp.data; return resp.data;
} catch (err: any) { } 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))) { if (err?.response?.status === 400 && /csrf|xsrf|antiforgery/i.test(JSON.stringify(err?.response?.data))) {
cachedCsrfToken = null; cachedCsrfToken = null;
const freshToken = await getCsrfToken(); const freshToken = await getCsrfToken();
if (freshToken) { if (freshToken) {
const retryResp = await listenarrApi.post("/api/v1/library/add", body, { headers: { "X-XSRF-TOKEN": freshToken } }); const resp = await listenarrApi.post("/api/v1/library/add", body, { headers: { "X-XSRF-TOKEN": freshToken } });
return retryResp.data; return resp.data;
} }
} }
throw err; throw err;
@@ -112,9 +116,7 @@ export async function getListenarrStatus(): Promise<{
error?: string; error?: string;
}> { }> {
try { try {
const resp = await listenarrApi.get("/api/v1/system/info", { const resp = await listenarrApi.get("/api/v1/system/info", { timeout: 5000 });
timeout: 5000,
});
return { connected: true, data: resp.data }; return { connected: true, data: resp.data };
} catch (error: any) { } catch (error: any) {
return { return {