fix(opds): probe filename when downloading PDFs and CBZ files, closes #2921 (#2932)

This commit is contained in:
Huang Xin
2026-01-12 17:55:50 +01:00
committed by GitHub
parent 48e2bfa82c
commit aaee04c290
3 changed files with 86 additions and 39 deletions
@@ -103,6 +103,7 @@ async function handleRequest(request: NextRequest, method: 'GET' | 'HEAD') {
return new NextResponse(null, {
status: 200,
headers: {
...Object.fromEntries(response.headers.entries()),
'Content-Type': contentType,
'Content-Length': contentLength || '',
'Cache-Control': 'public, max-age=300',
+45 -39
View File
@@ -29,7 +29,13 @@ import {
parseMediaType,
resolveURL,
} from './utils/opdsUtils';
import { getProxiedURL, fetchWithAuth, probeAuth, needsProxy } from './utils/opdsReq';
import {
getProxiedURL,
fetchWithAuth,
probeAuth,
needsProxy,
probeFilename,
} from './utils/opdsReq';
import { READEST_OPDS_USER_AGENT } from '@/services/constants';
import { FeedView } from './components/FeedView';
import { PublicationView } from './components/PublicationView';
@@ -414,49 +420,49 @@ export default function BrowserPage() {
}
return;
} else {
const username = usernameRef.current || '';
const password = passwordRef.current || '';
const useProxy = needsProxy(url);
let downloadUrl = useProxy ? getProxiedURL(url, '', true) : url;
const headers: Record<string, string> = {
'User-Agent': READEST_OPDS_USER_AGENT,
Accept: '*/*',
};
if (username || password) {
const authHeader = await probeAuth(url, username, password, useProxy);
if (authHeader) {
headers['Authorization'] = authHeader;
downloadUrl = useProxy ? getProxiedURL(url, authHeader, true) : url;
}
}
const probedFilename = await probeFilename(url, useProxy, headers);
const pathname = decodeURIComponent(new URL(url).pathname);
const ext = getFileExtFromMimeType(parsed?.mediaType) || getFileExtFromPath(pathname);
const basename = pathname.replaceAll('/', '_');
const filename = ext ? `${basename}.${ext}` : basename;
const filename = probedFilename ? probedFilename : ext ? `${basename}.${ext}` : basename;
const dstFilePath = await appService?.resolveFilePath(filename, 'Cache');
if (dstFilePath) {
const username = usernameRef.current || '';
const password = passwordRef.current || '';
const useProxy = needsProxy(url);
let downloadUrl = useProxy ? getProxiedURL(url, '', true) : url;
const headers: Record<string, string> = {
'User-Agent': READEST_OPDS_USER_AGENT,
Accept: '*/*',
};
if (username || password) {
const authHeader = await probeAuth(url, username, password, useProxy);
if (authHeader) {
headers['Authorization'] = authHeader;
downloadUrl = useProxy ? getProxiedURL(url, authHeader, true) : url;
}
}
await downloadFile({
appService,
dst: dstFilePath,
cfp: '',
url: downloadUrl,
headers,
singleThreaded: true,
skipSslVerification: true,
onProgress,
});
const { library, setLibrary } = useLibraryStore.getState();
const book = await appService.importBook(dstFilePath, library);
if (user && book && !book.uploadedAt && settings.autoUpload) {
setTimeout(() => {
transferManager.queueUpload(book);
}, 3000);
}
setLibrary(library);
appService.saveLibraryBooks(library);
return book;
console.log('Downloading to:', dstFilePath);
await downloadFile({
appService,
dst: dstFilePath,
cfp: '',
url: downloadUrl,
headers,
singleThreaded: true,
skipSslVerification: true,
onProgress,
});
const { library, setLibrary } = useLibraryStore.getState();
const book = await appService.importBook(dstFilePath, library);
if (user && book && !book.uploadedAt && settings.autoUpload) {
setTimeout(() => {
transferManager.queueUpload(book);
}, 3000);
}
setLibrary(library);
appService.saveLibraryBooks(library);
return book;
}
} catch (e) {
console.error('Download error:', e);
@@ -245,6 +245,46 @@ export const probeAuth = async (
return createBasicAuth(finalUsername, finalPassword);
};
export const probeFilename = async (
url: string,
useProxy = false,
headers?: Record<string, string>,
) => {
const { url: cleanUrl } = extractCredentialsFromURL(url);
const fetchURL = useProxy ? getProxiedURL(cleanUrl) : cleanUrl;
const baseHeaders: Record<string, string> = {
'User-Agent': READEST_OPDS_USER_AGENT,
Accept: 'application/atom+xml, application/xml, text/xml, */*',
};
const fetch = isTauriAppPlatform() ? tauriFetch : window.fetch;
const res = await fetch(fetchURL, {
method: 'HEAD',
headers: { ...baseHeaders, ...(headers || {}) },
danger: { acceptInvalidCerts: true, acceptInvalidHostnames: true },
});
if (res.ok) {
const contentDisposition = res.headers.get('content-disposition');
if (contentDisposition) {
const extendedMatch = contentDisposition.match(
/filename\*\s*=\s*(?:utf-8|UTF-8)'[^']*'([^;\s]+)/i,
);
if (extendedMatch?.[1]) {
return decodeURIComponent(extendedMatch[1]);
}
const plainMatch = contentDisposition.match(/filename\s*=\s*["']?([^"';\s]+)["']?/i);
if (plainMatch?.[1]) {
return decodeURIComponent(plainMatch[1]);
}
}
}
return '';
};
/**
* Perform authenticated HTTP request with retry logic for Digest/Basic auth
*/