Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ version: '3'

services:
bookgrab:
image: mrorbitman/bookgrab:latest
build: . # Build from local Dockerfile instead of using remote image
container_name: bookgrab
ports:
- "3000:3000"
Expand Down
111 changes: 78 additions & 33 deletions src/app/api/image-proxy/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,68 +13,113 @@ export async function GET(request: NextRequest) {
}

try {
const { MAM_TOKEN } = getServerEnvVariables();
// Get MAM token from request header (sent by client) or fall back to env
const mamToken = request.headers.get("x-mam-token");
const { MAM_TOKEN: envToken } = getServerEnvVariables();
const MAM_TOKEN = mamToken || envToken;

// Log the URL we're trying to fetch for debugging
console.log("Fetching image from:", url);

// Create a new URL object to ensure we're working with a valid URL
const imageUrl = new URL(url);
// If it's a MAM viewImageFull URL, we need to fetch the torrent page first
// to get the actual CDN URL
if (url.includes("viewImageFull.php")) {
const bookId = url.split("/").pop();

// Fetch the torrent page HTML
const pageUrl = `https://www.myanonamouse.net/t/${bookId}`;
const pageResponse = await fetch(pageUrl, {
headers: {
"User-Agent": "BookGrab/1.0",
Cookie: MAM_TOKEN ? `mam_id=${MAM_TOKEN}` : "",
},
});

if (!pageResponse.ok) {
throw new Error(`Failed to fetch torrent page: ${pageResponse.status}`);
}

const html = await pageResponse.text();

// Extract the CDN image URL from the HTML
// Look for the pattern: https://cdn.myanonamouse.net/t/p/{timestamp}/large/{id}.{ext}
const imageMatch = html.match(/https:\/\/cdn\.myanonamouse\.net\/t\/p\/\d+\/large\/\d+\.(jpg|jpeg|png)/i);

if (!imageMatch) {
console.log("No image found in HTML for book", bookId);
throw new Error("Image URL not found in page");
}

const cdnUrl = imageMatch[0];
console.log("Found CDN URL:", cdnUrl);

// Now fetch the actual image from CDN (no auth needed for CDN)
const imageResponse = await fetch(cdnUrl, {
headers: {
"User-Agent": "BookGrab/1.0",
Referer: "https://www.myanonamouse.net/",
},
});

if (!imageResponse.ok) {
throw new Error(`Failed to fetch image from CDN: ${imageResponse.status}`);
}

// Prepare headers that mimic a browser request
const imageData = await imageResponse.arrayBuffer();
const contentType = imageResponse.headers.get("content-type") || "image/jpeg";

return new NextResponse(imageData, {
headers: {
"Content-Type": contentType,
"Cache-Control": "public, max-age=86400",
},
});
}

// For direct image URLs (fallback)
const imageUrl = new URL(url);
const headers = {
"User-Agent":
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
"User-Agent": "BookGrab/1.0",
Referer: "https://www.myanonamouse.net/",
"sec-ch-ua": '"Chromium";v="131", "Not_A Brand";v="24"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"macOS"',
Accept:
"image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.9",
Accept: "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8",
};

// Add the MAM token as a cookie if the URL is from myanonamouse.net
if (
imageUrl.hostname.includes("myanonamouse.net") ||
imageUrl.hostname.includes("cdn.myanonamouse.net")
) {
if (MAM_TOKEN && imageUrl.hostname.includes("myanonamouse.net")) {
headers["Cookie"] = `mam_id=${MAM_TOKEN}`;
}

const response = await fetch(imageUrl.toString(), {
headers,
next: { revalidate: 0 }, // Don't cache this request
next: { revalidate: 0 },
});

if (!response.ok) {
console.error(
`Image fetch failed: ${response.status} ${response.statusText}`,
);
throw new Error(
`Failed to fetch image: ${response.status} ${response.statusText}`,
);
console.error(`Image fetch failed: ${response.status} ${response.statusText}`);
throw new Error(`Failed to fetch image: ${response.status} ${response.statusText}`);
}

// Get the image data as an array buffer
const imageData = await response.arrayBuffer();

// Get the content type from the response
const contentType = response.headers.get("content-type") || "image/jpeg";

// Return the image with the correct content type
return new NextResponse(imageData, {
headers: {
"Content-Type": contentType,
"Cache-Control": "public, max-age=86400", // Cache for 24 hours
"Cache-Control": "public, max-age=86400",
},
});
} catch (error) {
console.error("Image proxy error:", error);

// Return a placeholder image instead of an error
return NextResponse.redirect(
new URL("/placeholder-book.png", request.nextUrl.origin),
// Return a 1x1 transparent PNG instead of redirecting
const transparentPng = Buffer.from(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
"base64"
);

return new NextResponse(transparentPng, {
headers: {
"Content-Type": "image/png",
"Cache-Control": "public, max-age=3600",
},
});
}
}
95 changes: 95 additions & 0 deletions src/app/api/mam-keepalive/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { NextRequest, NextResponse } from "next/server";
import { getServerEnvVariables } from "@/lib/env";

export async function POST(request: NextRequest) {
try {
// Get MAM token from request header (sent by client) or fall back to env
const mamToken = request.headers.get("x-mam-token");
const { MAM_TOKEN: envToken } = getServerEnvVariables();
const MAM_TOKEN = mamToken || envToken;

if (!MAM_TOKEN) {
return NextResponse.json(
{ success: false, error: "No MAM token configured" },
{ status: 400 }
);
}

// Ping the MAM dynamic seedbox endpoint to keep the session alive
const response = await fetch(
"https://t.myanonamouse.net/json/dynamicSeedbox.php",
{
method: "GET",
headers: {
Cookie: `mam_id=${MAM_TOKEN}`,
"User-Agent": "BookGrab/1.0",
},
}
);

if (!response.ok) {
throw new Error(`MAM responded with status ${response.status}`);
}

const text = await response.text();

// Parse the response - MAM returns JSON with status
let result;
try {
result = JSON.parse(text);
} catch {
// If not JSON, use the text directly
result = { message: text };
}

// Check for known success responses
const successMessages = ["Completed", "No Change"];
const isSuccess = successMessages.some(
(msg) => text.includes(msg) || JSON.stringify(result).includes(msg)
);

if (isSuccess) {
console.log("MAM keepalive successful:", text);
return NextResponse.json({
success: true,
message: text,
timestamp: new Date().toISOString(),
});
}

// Check for known error responses
if (text.includes("No Session Cookie") || text.includes("Incorrect session type")) {
console.error("MAM keepalive failed:", text);
return NextResponse.json(
{
success: false,
error: text,
hint: "Make sure 'Allow session to set dynamic seedbox IP' is enabled in MAM Security settings",
},
{ status: 400 }
);
}

// Unknown response
console.log("MAM keepalive response:", text);
return NextResponse.json({
success: true,
message: text,
timestamp: new Date().toISOString(),
});
} catch (error) {
console.error("MAM keepalive error:", error);
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : "Unknown error",
},
{ status: 500 }
);
}
}

// Also support GET for easy testing
export async function GET(request: NextRequest) {
return POST(request);
}
13 changes: 11 additions & 2 deletions src/app/api/search/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { searchBooks } from "@/lib/mam-api";
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const query = searchParams.get("q");
const startNumber = parseInt(searchParams.get("start") || "0", 10);
const sortType = searchParams.get("sort") || "seeds";

if (!query) {
return NextResponse.json(
Expand All @@ -13,12 +15,19 @@ export async function GET(request: NextRequest) {
}

try {
const result = await searchBooks(query);
// Get MAM token from request header (sent by client)
const mamToken = request.headers.get("x-mam-token") || undefined;
const result = await searchBooks(query, mamToken, startNumber, sortType);
return NextResponse.json(result);
} catch (error) {
console.error("Search API error:", error);
return NextResponse.json(
{ error: "Failed to search books" },
{
error:
error instanceof Error
? error.message
: "Failed to search books",
},
{ status: 500 },
);
}
Expand Down
Loading