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/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"
},
+23 -21
View File
@@ -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 {