diff --git a/apps/readest-app/src/app/api/opds/proxy/route.ts b/apps/readest-app/src/app/api/opds/proxy/route.ts index 06e58f8e..c8836857 100644 --- a/apps/readest-app/src/app/api/opds/proxy/route.ts +++ b/apps/readest-app/src/app/api/opds/proxy/route.ts @@ -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', diff --git a/apps/readest-app/src/app/opds/page.tsx b/apps/readest-app/src/app/opds/page.tsx index 8f20368a..7b9f5b18 100644 --- a/apps/readest-app/src/app/opds/page.tsx +++ b/apps/readest-app/src/app/opds/page.tsx @@ -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 = { + '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 = { - '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); diff --git a/apps/readest-app/src/app/opds/utils/opdsReq.ts b/apps/readest-app/src/app/opds/utils/opdsReq.ts index 09d6640e..738a7020 100644 --- a/apps/readest-app/src/app/opds/utils/opdsReq.ts +++ b/apps/readest-app/src/app/opds/utils/opdsReq.ts @@ -245,6 +245,46 @@ export const probeAuth = async ( return createBasicAuth(finalUsername, finalPassword); }; +export const probeFilename = async ( + url: string, + useProxy = false, + headers?: Record, +) => { + const { url: cleanUrl } = extractCredentialsFromURL(url); + + const fetchURL = useProxy ? getProxiedURL(cleanUrl) : cleanUrl; + const baseHeaders: Record = { + '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 */