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/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"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user