feat: Refactor server-side document, audiobook, and authentication logic, introduce new client-side APIs and caching, and update related tests.
This commit is contained in:
parent
8e1b6b4009
commit
c2188b476f
110 changed files with 302 additions and 548 deletions
|
|
@ -15,7 +15,7 @@ import { DocumentHeaderMenu } from '@/components/documents/DocumentHeaderMenu';
|
||||||
import { AudiobookExportModal } from '@/components/AudiobookExportModal';
|
import { AudiobookExportModal } from '@/components/AudiobookExportModal';
|
||||||
import type { TTSAudiobookChapter } from '@/types/tts';
|
import type { TTSAudiobookChapter } from '@/types/tts';
|
||||||
import type { AudiobookGenerationSettings } from '@/types/client';
|
import type { AudiobookGenerationSettings } from '@/types/client';
|
||||||
import { resolveDocumentId } from '@/lib/dexie';
|
import { resolveDocumentId } from '@/lib/client/dexie';
|
||||||
import { RateLimitBanner } from '@/components/auth/RateLimitBanner';
|
import { RateLimitBanner } from '@/components/auth/RateLimitBanner';
|
||||||
import { useAuthRateLimit } from '@/contexts/AuthRateLimitContext';
|
import { useAuthRateLimit } from '@/contexts/AuthRateLimitContext';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import { RateLimitPauseButton } from '@/components/player/RateLimitPauseButton';
|
||||||
import { Header } from '@/components/Header';
|
import { Header } from '@/components/Header';
|
||||||
import { useTTS } from "@/contexts/TTSContext";
|
import { useTTS } from "@/contexts/TTSContext";
|
||||||
import TTSPlayer from '@/components/player/TTSPlayer';
|
import TTSPlayer from '@/components/player/TTSPlayer';
|
||||||
import { resolveDocumentId } from '@/lib/dexie';
|
import { resolveDocumentId } from '@/lib/client/dexie';
|
||||||
import { DocumentHeaderMenu } from '@/components/documents/DocumentHeaderMenu';
|
import { DocumentHeaderMenu } from '@/components/documents/DocumentHeaderMenu';
|
||||||
import { RateLimitBanner } from '@/components/auth/RateLimitBanner';
|
import { RateLimitBanner } from '@/components/auth/RateLimitBanner';
|
||||||
import { useAuthRateLimit } from '@/contexts/AuthRateLimitContext';
|
import { useAuthRateLimit } from '@/contexts/AuthRateLimitContext';
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { Toaster } from 'react-hot-toast';
|
||||||
|
|
||||||
import { Providers } from '@/app/providers';
|
import { Providers } from '@/app/providers';
|
||||||
import ClaimDataPopup from '@/components/auth/ClaimDataModal';
|
import ClaimDataPopup from '@/components/auth/ClaimDataModal';
|
||||||
import { getAuthBaseUrl, isAnonymousAuthSessionsEnabled, isAuthEnabled, isGithubAuthEnabled } from '@/lib/server/auth-config';
|
import { getAuthBaseUrl, isAnonymousAuthSessionsEnabled, isAuthEnabled, isGithubAuthEnabled } from '@/lib/server/auth/config';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import type { TTSAudiobookChapter } from '@/types/tts';
|
||||||
import type { AudiobookGenerationSettings } from '@/types/client';
|
import type { AudiobookGenerationSettings } from '@/types/client';
|
||||||
import TTSPlayer from '@/components/player/TTSPlayer';
|
import TTSPlayer from '@/components/player/TTSPlayer';
|
||||||
import { RateLimitPauseButton } from '@/components/player/RateLimitPauseButton';
|
import { RateLimitPauseButton } from '@/components/player/RateLimitPauseButton';
|
||||||
import { resolveDocumentId } from '@/lib/dexie';
|
import { resolveDocumentId } from '@/lib/client/dexie';
|
||||||
import { RateLimitBanner } from '@/components/auth/RateLimitBanner';
|
import { RateLimitBanner } from '@/components/auth/RateLimitBanner';
|
||||||
import { useAuthRateLimit } from '@/contexts/AuthRateLimitContext';
|
import { useAuthRateLimit } from '@/contexts/AuthRateLimitContext';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { useState, useEffect, Suspense } from 'react';
|
||||||
import { Button, Input } from '@headlessui/react';
|
import { Button, Input } from '@headlessui/react';
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { getAuthClient } from '@/lib/auth-client';
|
import { getAuthClient } from '@/lib/client/auth-client';
|
||||||
import { useAuthConfig, useAuthRateLimit } from '@/contexts/AuthRateLimitContext';
|
import { useAuthConfig, useAuthRateLimit } from '@/contexts/AuthRateLimitContext';
|
||||||
import { showPrivacyModal } from '@/components/PrivacyModal';
|
import { showPrivacyModal } from '@/components/PrivacyModal';
|
||||||
import { GithubIcon } from '@/components/icons/Icons';
|
import { GithubIcon } from '@/components/icons/Icons';
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { useState, useEffect } from 'react';
|
||||||
import { Button, Input } from '@headlessui/react';
|
import { Button, Input } from '@headlessui/react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { getAuthClient } from '@/lib/auth-client';
|
import { getAuthClient } from '@/lib/client/auth-client';
|
||||||
import { useAuthConfig, useAuthRateLimit } from '@/contexts/AuthRateLimitContext';
|
import { useAuthConfig, useAuthRateLimit } from '@/contexts/AuthRateLimitContext';
|
||||||
import { showPrivacyModal } from '@/components/PrivacyModal';
|
import { showPrivacyModal } from '@/components/PrivacyModal';
|
||||||
import { LoadingSpinner } from '@/components/Spinner';
|
import { LoadingSpinner } from '@/components/Spinner';
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@ import Link from 'next/link';
|
||||||
import { headers } from 'next/headers';
|
import { headers } from 'next/headers';
|
||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
import { auth } from '@/lib/server/auth';
|
import { auth } from '@/lib/server/auth/auth';
|
||||||
import { isAuthEnabled } from '@/lib/server/auth-config';
|
import { isAuthEnabled } from '@/lib/server/auth/config';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Read and Listen to Documents',
|
title: 'Read and Listen to Documents',
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { headers } from 'next/headers';
|
import { headers } from 'next/headers';
|
||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { auth } from '@/lib/server/auth';
|
import { auth } from '@/lib/server/auth/auth';
|
||||||
import { isAuthEnabled } from '@/lib/server/auth-config';
|
import { isAuthEnabled } from '@/lib/server/auth/config';
|
||||||
|
|
||||||
export async function DELETE() {
|
export async function DELETE() {
|
||||||
if (!isAuthEnabled() || !auth) {
|
if (!isAuthEnabled() || !auth) {
|
||||||
|
|
|
||||||
|
|
@ -7,24 +7,24 @@ import { randomUUID } from 'crypto';
|
||||||
import { and, eq, inArray } from 'drizzle-orm';
|
import { and, eq, inArray } from 'drizzle-orm';
|
||||||
import { db } from '@/db';
|
import { db } from '@/db';
|
||||||
import { audiobooks, audiobookChapters } from '@/db/schema';
|
import { audiobooks, audiobookChapters } from '@/db/schema';
|
||||||
import { requireAuthContext } from '@/lib/server/auth';
|
import { requireAuthContext } from '@/lib/server/auth/auth';
|
||||||
import {
|
import {
|
||||||
deleteAudiobookObject,
|
deleteAudiobookObject,
|
||||||
getAudiobookObjectBuffer,
|
getAudiobookObjectBuffer,
|
||||||
isMissingBlobError,
|
isMissingBlobError,
|
||||||
listAudiobookObjects,
|
listAudiobookObjects,
|
||||||
putAudiobookObject,
|
putAudiobookObject,
|
||||||
} from '@/lib/server/audiobooks-blobstore';
|
} from '@/lib/server/audiobooks/blobstore';
|
||||||
import {
|
import {
|
||||||
decodeChapterFileName,
|
decodeChapterFileName,
|
||||||
encodeChapterFileName,
|
encodeChapterFileName,
|
||||||
encodeChapterTitleTag,
|
encodeChapterTitleTag,
|
||||||
ffprobeAudio,
|
ffprobeAudio,
|
||||||
} from '@/lib/server/audiobook';
|
} from '@/lib/server/audiobooks/chapters';
|
||||||
import { isS3Configured } from '@/lib/server/s3';
|
import { isS3Configured } from '@/lib/server/storage/s3';
|
||||||
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/test-namespace';
|
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/testing/test-namespace';
|
||||||
import { buildAllowedAudiobookUserIds, pickAudiobookOwner } from '@/lib/server/audiobook-scope';
|
import { buildAllowedAudiobookUserIds, pickAudiobookOwner } from '@/lib/server/audiobooks/user-scope';
|
||||||
import { getFFmpegPath } from '@/lib/server/ffmpeg-bin';
|
import { getFFmpegPath } from '@/lib/server/audiobooks/ffmpeg-bin';
|
||||||
import type { AudiobookGenerationSettings } from '@/types/client';
|
import type { AudiobookGenerationSettings } from '@/types/client';
|
||||||
import type { TTSAudioBytes, TTSAudiobookFormat } from '@/types/tts';
|
import type { TTSAudioBytes, TTSAudiobookFormat } from '@/types/tts';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { join } from 'path';
|
||||||
import { and, eq, inArray } from 'drizzle-orm';
|
import { and, eq, inArray } from 'drizzle-orm';
|
||||||
import { db } from '@/db';
|
import { db } from '@/db';
|
||||||
import { audiobooks, audiobookChapters } from '@/db/schema';
|
import { audiobooks, audiobookChapters } from '@/db/schema';
|
||||||
import { requireAuthContext } from '@/lib/server/auth';
|
import { requireAuthContext } from '@/lib/server/auth/auth';
|
||||||
import {
|
import {
|
||||||
audiobookPrefix,
|
audiobookPrefix,
|
||||||
deleteAudiobookObject,
|
deleteAudiobookObject,
|
||||||
|
|
@ -14,16 +14,16 @@ import {
|
||||||
getAudiobookObjectBuffer,
|
getAudiobookObjectBuffer,
|
||||||
listAudiobookObjects,
|
listAudiobookObjects,
|
||||||
putAudiobookObject,
|
putAudiobookObject,
|
||||||
} from '@/lib/server/audiobooks-blobstore';
|
} from '@/lib/server/audiobooks/blobstore';
|
||||||
import {
|
import {
|
||||||
decodeChapterFileName,
|
decodeChapterFileName,
|
||||||
escapeFFMetadata,
|
escapeFFMetadata,
|
||||||
ffprobeAudio,
|
ffprobeAudio,
|
||||||
} from '@/lib/server/audiobook';
|
} from '@/lib/server/audiobooks/chapters';
|
||||||
import { isS3Configured } from '@/lib/server/s3';
|
import { isS3Configured } from '@/lib/server/storage/s3';
|
||||||
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/test-namespace';
|
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/testing/test-namespace';
|
||||||
import { getFFmpegPath } from '@/lib/server/ffmpeg-bin';
|
import { getFFmpegPath } from '@/lib/server/audiobooks/ffmpeg-bin';
|
||||||
import { buildAllowedAudiobookUserIds, pickAudiobookOwner } from '@/lib/server/audiobook-scope';
|
import { buildAllowedAudiobookUserIds, pickAudiobookOwner } from '@/lib/server/audiobooks/user-scope';
|
||||||
import type { TTSAudiobookFormat } from '@/types/tts';
|
import type { TTSAudiobookFormat } from '@/types/tts';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,13 @@ import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { and, eq, inArray } from 'drizzle-orm';
|
import { and, eq, inArray } from 'drizzle-orm';
|
||||||
import { db } from '@/db';
|
import { db } from '@/db';
|
||||||
import { audiobooks, audiobookChapters } from '@/db/schema';
|
import { audiobooks, audiobookChapters } from '@/db/schema';
|
||||||
import { requireAuthContext } from '@/lib/server/auth';
|
import { requireAuthContext } from '@/lib/server/auth/auth';
|
||||||
import { getAudiobookObjectBuffer, isMissingBlobError, listAudiobookObjects } from '@/lib/server/audiobooks-blobstore';
|
import { getAudiobookObjectBuffer, isMissingBlobError, listAudiobookObjects } from '@/lib/server/audiobooks/blobstore';
|
||||||
import { decodeChapterFileName } from '@/lib/server/audiobook';
|
import { decodeChapterFileName } from '@/lib/server/audiobooks/chapters';
|
||||||
import { pruneAudiobookChaptersNotOnDisk } from '@/lib/server/audiobook-prune';
|
import { pruneAudiobookChaptersNotOnDisk } from '@/lib/server/audiobooks/prune';
|
||||||
import { isS3Configured } from '@/lib/server/s3';
|
import { isS3Configured } from '@/lib/server/storage/s3';
|
||||||
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/test-namespace';
|
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/testing/test-namespace';
|
||||||
import { buildAllowedAudiobookUserIds, pickAudiobookOwner } from '@/lib/server/audiobook-scope';
|
import { buildAllowedAudiobookUserIds, pickAudiobookOwner } from '@/lib/server/audiobooks/user-scope';
|
||||||
import type { AudiobookGenerationSettings } from '@/types/client';
|
import type { AudiobookGenerationSettings } from '@/types/client';
|
||||||
import type { TTSAudiobookChapter, TTSAudiobookFormat } from '@/types/tts';
|
import type { TTSAudiobookChapter, TTSAudiobookFormat } from '@/types/tts';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { auth } from "@/lib/server/auth"; // path to your auth file
|
import { auth } from "@/lib/server/auth/auth"; // path to your auth file
|
||||||
import { toNextJsHandler } from "better-auth/next-js";
|
import { toNextJsHandler } from "better-auth/next-js";
|
||||||
|
|
||||||
const handlers = auth
|
const handlers = auth
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,11 @@ import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { and, eq, inArray } from 'drizzle-orm';
|
import { and, eq, inArray } from 'drizzle-orm';
|
||||||
import { db } from '@/db';
|
import { db } from '@/db';
|
||||||
import { documents } from '@/db/schema';
|
import { documents } from '@/db/schema';
|
||||||
import { requireAuthContext } from '@/lib/server/auth';
|
import { requireAuthContext } from '@/lib/server/auth/auth';
|
||||||
import { contentTypeForName } from '@/lib/server/library';
|
import { contentTypeForName } from '@/lib/server/storage/library-mount';
|
||||||
import { getDocumentBlob, isMissingBlobError, isValidDocumentId } from '@/lib/server/documents-blobstore';
|
import { getDocumentBlob, isMissingBlobError, isValidDocumentId } from '@/lib/server/documents/blobstore';
|
||||||
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/test-namespace';
|
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/testing/test-namespace';
|
||||||
import { isS3Configured } from '@/lib/server/s3';
|
import { isS3Configured } from '@/lib/server/storage/s3';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,10 @@ import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { and, eq, inArray } from 'drizzle-orm';
|
import { and, eq, inArray } from 'drizzle-orm';
|
||||||
import { db } from '@/db';
|
import { db } from '@/db';
|
||||||
import { documents } from '@/db/schema';
|
import { documents } from '@/db/schema';
|
||||||
import { requireAuthContext } from '@/lib/server/auth';
|
import { requireAuthContext } from '@/lib/server/auth/auth';
|
||||||
import { isValidDocumentId, presignGet } from '@/lib/server/documents-blobstore';
|
import { isValidDocumentId, presignGet } from '@/lib/server/documents/blobstore';
|
||||||
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/test-namespace';
|
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/testing/test-namespace';
|
||||||
import { isS3Configured } from '@/lib/server/s3';
|
import { isS3Configured } from '@/lib/server/storage/s3';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { presignDocumentPreviewGet } from '@/lib/server/document-previews-blobstore';
|
import { presignDocumentPreviewGet } from '@/lib/server/documents/previews-blobstore';
|
||||||
import { ensureDocumentPreview } from '@/lib/server/document-previews';
|
import { ensureDocumentPreview } from '@/lib/server/documents/previews';
|
||||||
import { validatePreviewRequest } from '../utils';
|
import { validatePreviewRequest } from '../utils';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
|
||||||
|
|
@ -2,19 +2,24 @@ import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { and, eq, inArray } from 'drizzle-orm';
|
import { and, eq, inArray } from 'drizzle-orm';
|
||||||
import { db } from '@/db';
|
import { db } from '@/db';
|
||||||
import { documents } from '@/db/schema';
|
import { documents } from '@/db/schema';
|
||||||
import { requireAuthContext } from '@/lib/server/auth';
|
import { requireAuthContext } from '@/lib/server/auth/auth';
|
||||||
import { isValidDocumentId } from '@/lib/server/documents-blobstore';
|
import {
|
||||||
|
getDocumentRange,
|
||||||
|
isMissingBlobError as isMissingDocumentBlobError,
|
||||||
|
isValidDocumentId,
|
||||||
|
} from '@/lib/server/documents/blobstore';
|
||||||
import {
|
import {
|
||||||
getDocumentPreviewBuffer,
|
getDocumentPreviewBuffer,
|
||||||
isMissingBlobError,
|
isMissingBlobError as isMissingPreviewBlobError,
|
||||||
} from '@/lib/server/document-previews-blobstore';
|
} from '@/lib/server/documents/previews-blobstore';
|
||||||
import {
|
import {
|
||||||
ensureDocumentPreview,
|
ensureDocumentPreview,
|
||||||
enqueueDocumentPreview,
|
enqueueDocumentPreview,
|
||||||
isPreviewableDocumentType,
|
isPreviewableDocumentType,
|
||||||
} from '@/lib/server/document-previews';
|
} from '@/lib/server/documents/previews';
|
||||||
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/test-namespace';
|
import { extractRawTextSnippet } from '@/lib/server/documents/text-snippets';
|
||||||
import { isS3Configured } from '@/lib/server/s3';
|
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/testing/test-namespace';
|
||||||
|
import { isS3Configured } from '@/lib/server/storage/s3';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
|
|
@ -34,6 +39,11 @@ function streamBuffer(buffer: Buffer): ReadableStream<Uint8Array> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clampInt(value: number, min: number, max: number): number {
|
||||||
|
if (!Number.isFinite(value)) return min;
|
||||||
|
return Math.max(min, Math.min(max, Math.trunc(value)));
|
||||||
|
}
|
||||||
|
|
||||||
export async function GET(req: NextRequest) {
|
export async function GET(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
if (!isS3Configured()) return s3NotConfiguredResponse();
|
if (!isS3Configured()) return s3NotConfiguredResponse();
|
||||||
|
|
@ -48,6 +58,7 @@ export async function GET(req: NextRequest) {
|
||||||
|
|
||||||
const url = new URL(req.url);
|
const url = new URL(req.url);
|
||||||
const id = (url.searchParams.get('id') || '').trim().toLowerCase();
|
const id = (url.searchParams.get('id') || '').trim().toLowerCase();
|
||||||
|
const snippetRequested = (url.searchParams.get('snippet') || '').trim() === '1';
|
||||||
const presignUrl = `/api/documents/blob/preview/presign?id=${encodeURIComponent(id)}`;
|
const presignUrl = `/api/documents/blob/preview/presign?id=${encodeURIComponent(id)}`;
|
||||||
const fallbackUrl = `/api/documents/blob/preview/fallback?id=${encodeURIComponent(id)}`;
|
const fallbackUrl = `/api/documents/blob/preview/fallback?id=${encodeURIComponent(id)}`;
|
||||||
if (!isValidDocumentId(id)) {
|
if (!isValidDocumentId(id)) {
|
||||||
|
|
@ -74,6 +85,28 @@ export async function GET(req: NextRequest) {
|
||||||
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (snippetRequested) {
|
||||||
|
const maxChars = clampInt(Number.parseInt(url.searchParams.get('maxChars') || '1600', 10), 100, 8000);
|
||||||
|
const maxBytes = clampInt(Number.parseInt(url.searchParams.get('maxBytes') || '131072', 10), 4096, 1024 * 1024);
|
||||||
|
try {
|
||||||
|
const head = await getDocumentRange(id, 0, maxBytes - 1, testNamespace);
|
||||||
|
const decoded = new TextDecoder().decode(new Uint8Array(head));
|
||||||
|
const snippet = extractRawTextSnippet(decoded, maxChars);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ snippet },
|
||||||
|
{ headers: { 'Cache-Control': 'no-store' } },
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
if (isMissingDocumentBlobError(error)) {
|
||||||
|
await db
|
||||||
|
.delete(documents)
|
||||||
|
.where(and(eq(documents.id, id), inArray(documents.userId, allowedUserIds)));
|
||||||
|
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!isPreviewableDocumentType(doc.type)) {
|
if (!isPreviewableDocumentType(doc.type)) {
|
||||||
return NextResponse.json({ error: `Preview not supported for type ${doc.type}` }, { status: 415 });
|
return NextResponse.json({ error: `Preview not supported for type ${doc.type}` }, { status: 415 });
|
||||||
}
|
}
|
||||||
|
|
@ -112,7 +145,7 @@ export async function GET(req: NextRequest) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isMissingBlobError(error)) {
|
if (isMissingPreviewBlobError(error)) {
|
||||||
await enqueueDocumentPreview(
|
await enqueueDocumentPreview(
|
||||||
{
|
{
|
||||||
id: doc.id,
|
id: doc.id,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { presignDocumentPreviewGet } from '@/lib/server/document-previews-blobstore';
|
import { presignDocumentPreviewGet } from '@/lib/server/documents/previews-blobstore';
|
||||||
import { ensureDocumentPreview } from '@/lib/server/document-previews';
|
import { ensureDocumentPreview } from '@/lib/server/documents/previews';
|
||||||
import { validatePreviewRequest } from '../utils';
|
import { validatePreviewRequest } from '../utils';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,11 @@ import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { and, eq, inArray } from 'drizzle-orm';
|
import { and, eq, inArray } from 'drizzle-orm';
|
||||||
import { db } from '@/db';
|
import { db } from '@/db';
|
||||||
import { documents } from '@/db/schema';
|
import { documents } from '@/db/schema';
|
||||||
import { requireAuthContext } from '@/lib/server/auth';
|
import { requireAuthContext } from '@/lib/server/auth/auth';
|
||||||
import { isValidDocumentId } from '@/lib/server/documents-blobstore';
|
import { isValidDocumentId } from '@/lib/server/documents/blobstore';
|
||||||
import { isPreviewableDocumentType } from '@/lib/server/document-previews';
|
import { isPreviewableDocumentType } from '@/lib/server/documents/previews';
|
||||||
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/test-namespace';
|
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/testing/test-namespace';
|
||||||
import { isS3Configured } from '@/lib/server/s3';
|
import { isS3Configured } from '@/lib/server/storage/s3';
|
||||||
|
|
||||||
export function s3NotConfiguredResponse(): NextResponse {
|
export function s3NotConfiguredResponse(): NextResponse {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
|
|
|
||||||
|
|
@ -1,129 +0,0 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
|
||||||
import { and, eq, inArray } from 'drizzle-orm';
|
|
||||||
import { db } from '@/db';
|
|
||||||
import { documents } from '@/db/schema';
|
|
||||||
import { requireAuthContext } from '@/lib/server/auth';
|
|
||||||
import { contentTypeForName } from '@/lib/server/library';
|
|
||||||
import { extractRawTextSnippet } from '@/lib/text-snippets';
|
|
||||||
import {
|
|
||||||
getDocumentBlob,
|
|
||||||
getDocumentRange,
|
|
||||||
isMissingBlobError,
|
|
||||||
isValidDocumentId,
|
|
||||||
presignGet,
|
|
||||||
} from '@/lib/server/documents-blobstore';
|
|
||||||
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/test-namespace';
|
|
||||||
import { isS3Configured } from '@/lib/server/s3';
|
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
|
||||||
|
|
||||||
function clampInt(value: number, min: number, max: number): number {
|
|
||||||
if (!Number.isFinite(value)) return min;
|
|
||||||
return Math.max(min, Math.min(max, Math.trunc(value)));
|
|
||||||
}
|
|
||||||
|
|
||||||
function s3NotConfiguredResponse(): NextResponse {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Documents storage is not configured. Set S3_* environment variables.' },
|
|
||||||
{ status: 503 },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function GET(req: NextRequest) {
|
|
||||||
try {
|
|
||||||
if (!isS3Configured()) return s3NotConfiguredResponse();
|
|
||||||
|
|
||||||
const ctxOrRes = await requireAuthContext(req);
|
|
||||||
if (ctxOrRes instanceof Response) return ctxOrRes;
|
|
||||||
|
|
||||||
const testNamespace = getOpenReaderTestNamespace(req.headers);
|
|
||||||
const unclaimedUserId = getUnclaimedUserIdForNamespace(testNamespace);
|
|
||||||
const storageUserId = ctxOrRes.userId ?? unclaimedUserId;
|
|
||||||
const allowedUserIds = ctxOrRes.authEnabled ? [storageUserId, unclaimedUserId] : [unclaimedUserId];
|
|
||||||
|
|
||||||
const url = new URL(req.url);
|
|
||||||
const id = (url.searchParams.get('id') || '').trim().toLowerCase();
|
|
||||||
const format = (url.searchParams.get('format') || '').toLowerCase().trim();
|
|
||||||
const prefersProxy = (url.searchParams.get('proxy') || '').trim() === '1';
|
|
||||||
if (!isValidDocumentId(id)) {
|
|
||||||
return NextResponse.json({ error: 'Invalid id' }, { status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const rows = (await db
|
|
||||||
.select({ id: documents.id, userId: documents.userId, name: documents.name, filePath: documents.filePath })
|
|
||||||
.from(documents)
|
|
||||||
.where(and(eq(documents.id, id), inArray(documents.userId, allowedUserIds)))) as Array<{
|
|
||||||
id: string;
|
|
||||||
userId: string;
|
|
||||||
name: string;
|
|
||||||
filePath: string;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
const doc = rows.find((row) => row.userId === storageUserId) ?? rows[0];
|
|
||||||
if (!doc) {
|
|
||||||
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const filename = doc.name || `${id}.bin`;
|
|
||||||
const responseType = contentTypeForName(filename);
|
|
||||||
|
|
||||||
if (format === 'snippet') {
|
|
||||||
const maxChars = clampInt(Number.parseInt(url.searchParams.get('maxChars') || '1600', 10), 100, 8000);
|
|
||||||
const maxBytes = clampInt(Number.parseInt(url.searchParams.get('maxBytes') || '131072', 10), 4096, 1024 * 1024);
|
|
||||||
try {
|
|
||||||
const head = await getDocumentRange(id, 0, maxBytes - 1, testNamespace);
|
|
||||||
const decoded = new TextDecoder().decode(new Uint8Array(head));
|
|
||||||
const snippet = extractRawTextSnippet(decoded, maxChars);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ snippet },
|
|
||||||
{ headers: { 'Cache-Control': 'no-store' } },
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
if (isMissingBlobError(error)) {
|
|
||||||
await db
|
|
||||||
.delete(documents)
|
|
||||||
.where(and(eq(documents.id, id), inArray(documents.userId, allowedUserIds)));
|
|
||||||
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!prefersProxy) {
|
|
||||||
const directUrl = await presignGet(id, testNamespace).catch(() => null);
|
|
||||||
if (directUrl) {
|
|
||||||
return NextResponse.redirect(directUrl, {
|
|
||||||
status: 307,
|
|
||||||
headers: { 'Cache-Control': 'no-store' },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = await getDocumentBlob(id, testNamespace);
|
|
||||||
const stream = new ReadableStream<Uint8Array>({
|
|
||||||
start(controller) {
|
|
||||||
controller.enqueue(new Uint8Array(content));
|
|
||||||
controller.close();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return new NextResponse(stream, {
|
|
||||||
headers: {
|
|
||||||
'Content-Type': responseType,
|
|
||||||
'Cache-Control': 'no-store',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
if (isMissingBlobError(error)) {
|
|
||||||
await db
|
|
||||||
.delete(documents)
|
|
||||||
.where(and(eq(documents.id, id), inArray(documents.userId, allowedUserIds)));
|
|
||||||
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading document content:', error);
|
|
||||||
return NextResponse.json({ error: 'Failed to load document content' }, { status: 500 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { requireAuthContext } from '@/lib/server/auth';
|
import { requireAuthContext } from '@/lib/server/auth/auth';
|
||||||
import { isValidDocumentId, putDocumentBlob } from '@/lib/server/documents-blobstore';
|
import { isValidDocumentId, putDocumentBlob } from '@/lib/server/documents/blobstore';
|
||||||
import { isS3Configured } from '@/lib/server/s3';
|
import { isS3Configured } from '@/lib/server/storage/s3';
|
||||||
import { getOpenReaderTestNamespace } from '@/lib/server/test-namespace';
|
import { getOpenReaderTestNamespace } from '@/lib/server/testing/test-namespace';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { requireAuthContext } from '@/lib/server/auth';
|
import { requireAuthContext } from '@/lib/server/auth/auth';
|
||||||
import { isValidDocumentId, presignPut } from '@/lib/server/documents-blobstore';
|
import { isValidDocumentId, presignPut } from '@/lib/server/documents/blobstore';
|
||||||
import { getOpenReaderTestNamespace } from '@/lib/server/test-namespace';
|
import { getOpenReaderTestNamespace } from '@/lib/server/testing/test-namespace';
|
||||||
import { isS3Configured } from '@/lib/server/s3';
|
import { isS3Configured } from '@/lib/server/storage/s3';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,14 @@ import path from 'path';
|
||||||
import { existsSync } from 'fs';
|
import { existsSync } from 'fs';
|
||||||
import { randomUUID, createHash } from 'crypto';
|
import { randomUUID, createHash } from 'crypto';
|
||||||
import { pathToFileURL } from 'url';
|
import { pathToFileURL } from 'url';
|
||||||
import { requireAuthContext } from '@/lib/server/auth';
|
import { requireAuthContext } from '@/lib/server/auth/auth';
|
||||||
import { db } from '@/db';
|
import { db } from '@/db';
|
||||||
import { documents } from '@/db/schema';
|
import { documents } from '@/db/schema';
|
||||||
import { safeDocumentName } from '@/lib/server/documents-utils';
|
import { safeDocumentName } from '@/lib/server/documents/utils';
|
||||||
import { enqueueDocumentPreview } from '@/lib/server/document-previews';
|
import { enqueueDocumentPreview } from '@/lib/server/documents/previews';
|
||||||
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/test-namespace';
|
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/testing/test-namespace';
|
||||||
import { isS3Configured } from '@/lib/server/s3';
|
import { isS3Configured } from '@/lib/server/storage/s3';
|
||||||
import { putDocumentBlob } from '@/lib/server/documents-blobstore';
|
import { putDocumentBlob } from '@/lib/server/documents/blobstore';
|
||||||
|
|
||||||
const DOCSTORE_DIR = path.join(process.cwd(), 'docstore');
|
const DOCSTORE_DIR = path.join(process.cwd(), 'docstore');
|
||||||
const TEMP_DIR = path.join(DOCSTORE_DIR, 'tmp');
|
const TEMP_DIR = path.join(DOCSTORE_DIR, 'tmp');
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { readFile, stat } from 'fs/promises';
|
import { readFile, stat } from 'fs/promises';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { contentTypeForName, decodeLibraryId, isPathWithinRoot, parseLibraryRoots } from '@/lib/server/library';
|
import { contentTypeForName, decodeLibraryId, isPathWithinRoot, parseLibraryRoots } from '@/lib/server/storage/library-mount';
|
||||||
import { auth } from '@/lib/server/auth';
|
import { auth } from '@/lib/server/auth/auth';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@ import type { Dirent } from 'fs';
|
||||||
import { readdir, stat } from 'fs/promises';
|
import { readdir, stat } from 'fs/promises';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { parseLibraryRoots } from '@/lib/server/library';
|
import { parseLibraryRoots } from '@/lib/server/storage/library-mount';
|
||||||
import type { DocumentType } from '@/types/documents';
|
import type { DocumentType } from '@/types/documents';
|
||||||
import { auth } from '@/lib/server/auth';
|
import { auth } from '@/lib/server/auth/auth';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,16 @@ import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { and, count, eq, inArray } from 'drizzle-orm';
|
import { and, count, eq, inArray } from 'drizzle-orm';
|
||||||
import { db } from '@/db';
|
import { db } from '@/db';
|
||||||
import { documents } from '@/db/schema';
|
import { documents } from '@/db/schema';
|
||||||
import { requireAuthContext } from '@/lib/server/auth';
|
import { requireAuthContext } from '@/lib/server/auth/auth';
|
||||||
import { safeDocumentName, toDocumentTypeFromName } from '@/lib/server/documents-utils';
|
import { safeDocumentName, toDocumentTypeFromName } from '@/lib/server/documents/utils';
|
||||||
import {
|
import {
|
||||||
cleanupDocumentPreviewArtifacts,
|
cleanupDocumentPreviewArtifacts,
|
||||||
deleteDocumentPreviewRows,
|
deleteDocumentPreviewRows,
|
||||||
enqueueDocumentPreview,
|
enqueueDocumentPreview,
|
||||||
} from '@/lib/server/document-previews';
|
} from '@/lib/server/documents/previews';
|
||||||
import { deleteDocumentBlob, headDocumentBlob, isMissingBlobError, isValidDocumentId } from '@/lib/server/documents-blobstore';
|
import { deleteDocumentBlob, headDocumentBlob, isMissingBlobError, isValidDocumentId } from '@/lib/server/documents/blobstore';
|
||||||
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/test-namespace';
|
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/testing/test-namespace';
|
||||||
import { isS3Configured } from '@/lib/server/s3';
|
import { isS3Configured } from '@/lib/server/storage/s3';
|
||||||
import type { BaseDocument, DocumentType } from '@/types/documents';
|
import type { BaseDocument, DocumentType } from '@/types/documents';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { NextResponse, type NextRequest } from 'next/server';
|
import { NextResponse, type NextRequest } from 'next/server';
|
||||||
import { auth } from '@/lib/server/auth';
|
import { auth } from '@/lib/server/auth/auth';
|
||||||
import { rateLimiter, RATE_LIMITS, isTtsRateLimitEnabled } from '@/lib/server/rate-limiter';
|
import { rateLimiter, RATE_LIMITS, isTtsRateLimitEnabled } from '@/lib/server/rate-limit/rate-limiter';
|
||||||
import { headers } from 'next/headers';
|
import { headers } from 'next/headers';
|
||||||
import { isAuthEnabled } from '@/lib/server/auth-config';
|
import { isAuthEnabled } from '@/lib/server/auth/config';
|
||||||
import { getClientIp } from '@/lib/server/request-ip';
|
import { getClientIp } from '@/lib/server/rate-limit/request-ip';
|
||||||
import { getOrCreateDeviceId, setDeviceIdCookie } from '@/lib/server/device-id';
|
import { getOrCreateDeviceId, setDeviceIdCookie } from '@/lib/server/rate-limit/device-id';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import OpenAI from 'openai';
|
import OpenAI from 'openai';
|
||||||
import { SpeechCreateParams } from 'openai/resources/audio/speech.mjs';
|
import { SpeechCreateParams } from 'openai/resources/audio/speech.mjs';
|
||||||
import { isKokoroModel } from '@/lib/kokoro';
|
import { isKokoroModel } from '@/lib/shared/kokoro';
|
||||||
import { LRUCache } from 'lru-cache';
|
import { LRUCache } from 'lru-cache';
|
||||||
import { createHash } from 'crypto';
|
import { createHash } from 'crypto';
|
||||||
import type { TTSRequestPayload } from '@/types/client';
|
import type { TTSRequestPayload } from '@/types/client';
|
||||||
import type { TTSError, TTSAudioBuffer } from '@/types/tts';
|
import type { TTSError, TTSAudioBuffer } from '@/types/tts';
|
||||||
import { headers } from 'next/headers';
|
import { headers } from 'next/headers';
|
||||||
import { auth } from '@/lib/server/auth';
|
import { auth } from '@/lib/server/auth/auth';
|
||||||
import { rateLimiter, RATE_LIMITS, isTtsRateLimitEnabled } from '@/lib/server/rate-limiter';
|
import { rateLimiter, RATE_LIMITS, isTtsRateLimitEnabled } from '@/lib/server/rate-limit/rate-limiter';
|
||||||
import { isAuthEnabled } from '@/lib/server/auth-config';
|
import { isAuthEnabled } from '@/lib/server/auth/config';
|
||||||
import { getClientIp } from '@/lib/server/request-ip';
|
import { getClientIp } from '@/lib/server/rate-limit/request-ip';
|
||||||
import { getOrCreateDeviceId, setDeviceIdCookie } from '@/lib/server/device-id';
|
import { getOrCreateDeviceId, setDeviceIdCookie } from '@/lib/server/rate-limit/device-id';
|
||||||
|
|
||||||
function attachDeviceIdCookie(response: NextResponse, deviceId: string | null, didCreate: boolean) {
|
function attachDeviceIdCookie(response: NextResponse, deviceId: string | null, didCreate: boolean) {
|
||||||
if (didCreate && deviceId) {
|
if (didCreate && deviceId) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { isKokoroModel } from '@/lib/kokoro';
|
import { isKokoroModel } from '@/lib/shared/kokoro';
|
||||||
import { auth } from '@/lib/server/auth';
|
import { auth } from '@/lib/server/auth/auth';
|
||||||
|
|
||||||
const OPENAI_VOICES = ['alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer'];
|
const OPENAI_VOICES = ['alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer'];
|
||||||
const GPT4O_MINI_VOICES = ['alloy', 'ash', 'coral', 'echo', 'fable', 'onyx', 'nova', 'sage', 'shimmer'];
|
const GPT4O_MINI_VOICES = ['alloy', 'ash', 'coral', 'echo', 'fable', 'onyx', 'nova', 'sage', 'shimmer'];
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { claimAnonymousData } from '@/lib/server/claim-data';
|
import { claimAnonymousData } from '@/lib/server/user/claim-data';
|
||||||
import { auth } from '@/lib/server/auth';
|
import { auth } from '@/lib/server/auth/auth';
|
||||||
import { db } from '@/db';
|
import { db } from '@/db';
|
||||||
import { audiobooks, documents, userDocumentProgress, userPreferences } from '@/db/schema';
|
import { audiobooks, documents, userDocumentProgress, userPreferences } from '@/db/schema';
|
||||||
import { count, eq, ne } from 'drizzle-orm';
|
import { count, eq, ne } from 'drizzle-orm';
|
||||||
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/test-namespace';
|
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/testing/test-namespace';
|
||||||
|
|
||||||
async function checkClaimMigrationReadiness(): Promise<NextResponse | null> {
|
async function checkClaimMigrationReadiness(): Promise<NextResponse | null> {
|
||||||
const [legacyRows] = await db
|
const [legacyRows] = await db
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { PassThrough, Readable } from 'stream';
|
import { PassThrough, Readable } from 'stream';
|
||||||
import { auth } from '@/lib/server/auth';
|
import { auth } from '@/lib/server/auth/auth';
|
||||||
import { db } from '@/db';
|
import { db } from '@/db';
|
||||||
import { documents, audiobooks, audiobookChapters, userPreferences, userDocumentProgress, userTtsChars } from '@/db/schema';
|
import { documents, audiobooks, audiobookChapters, userPreferences, userDocumentProgress, userTtsChars } from '@/db/schema';
|
||||||
import { and, desc, eq, inArray } from 'drizzle-orm';
|
import { and, desc, eq, inArray } from 'drizzle-orm';
|
||||||
import archiver from 'archiver';
|
import archiver from 'archiver';
|
||||||
import { appendUserExportArchive } from '@/lib/server/user-export';
|
import { appendUserExportArchive } from '@/lib/server/user/data-export';
|
||||||
import { getDocumentBlobStream } from '@/lib/server/documents-blobstore';
|
import { getDocumentBlobStream } from '@/lib/server/documents/blobstore';
|
||||||
import { getAudiobookObjectStream, listAudiobookObjects } from '@/lib/server/audiobooks-blobstore';
|
import { getAudiobookObjectStream, listAudiobookObjects } from '@/lib/server/audiobooks/blobstore';
|
||||||
import { isS3Configured } from '@/lib/server/s3';
|
import { isS3Configured } from '@/lib/server/storage/s3';
|
||||||
import { getOpenReaderTestNamespace } from '@/lib/server/test-namespace';
|
import { getOpenReaderTestNamespace } from '@/lib/server/testing/test-namespace';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { eq } from 'drizzle-orm';
|
||||||
import { db } from '@/db';
|
import { db } from '@/db';
|
||||||
import { userPreferences } from '@/db/schema';
|
import { userPreferences } from '@/db/schema';
|
||||||
import { SYNCED_PREFERENCE_KEYS, type SyncedPreferencesPatch } from '@/types/user-state';
|
import { SYNCED_PREFERENCE_KEYS, type SyncedPreferencesPatch } from '@/types/user-state';
|
||||||
import { resolveUserStateScope } from '@/lib/server/user-state-scope';
|
import { resolveUserStateScope } from '@/lib/server/user/resolve-state-scope';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@ import { and, eq, sql } from 'drizzle-orm';
|
||||||
import { db } from '@/db';
|
import { db } from '@/db';
|
||||||
import { userDocumentProgress } from '@/db/schema';
|
import { userDocumentProgress } from '@/db/schema';
|
||||||
import type { ReaderType } from '@/types/user-state';
|
import type { ReaderType } from '@/types/user-state';
|
||||||
import { isValidDocumentId } from '@/lib/server/documents-blobstore';
|
import { isValidDocumentId } from '@/lib/server/documents/blobstore';
|
||||||
import { resolveUserStateScope } from '@/lib/server/user-state-scope';
|
import { resolveUserStateScope } from '@/lib/server/user/resolve-state-scope';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,9 @@ import { tmpdir } from 'os';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
import type { TTSSentenceAlignment, TTSAudioBytes, TTSAudioBuffer } from '@/types/tts';
|
import type { TTSSentenceAlignment, TTSAudioBytes, TTSAudioBuffer } from '@/types/tts';
|
||||||
import { preprocessSentenceForAudio } from '@/lib/nlp';
|
import { preprocessSentenceForAudio } from '@/lib/shared/nlp';
|
||||||
import { auth } from '@/lib/server/auth';
|
import { auth } from '@/lib/server/auth/auth';
|
||||||
import { getFFmpegPath } from '@/lib/server/ffmpeg-bin';
|
import { getFFmpegPath } from '@/lib/server/audiobooks/ffmpeg-bin';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import {
|
||||||
deleteAudiobook,
|
deleteAudiobook,
|
||||||
downloadAudiobookChapter,
|
downloadAudiobookChapter,
|
||||||
downloadAudiobook
|
downloadAudiobook
|
||||||
} from '@/lib/client';
|
} from '@/lib/client/api/audiobooks';
|
||||||
import type { AudiobookGenerationSettings } from '@/types/client';
|
import type { AudiobookGenerationSettings } from '@/types/client';
|
||||||
interface AudiobookExportModalProps {
|
interface AudiobookExportModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Analytics } from '@vercel/analytics/next';
|
import { Analytics } from '@vercel/analytics/next';
|
||||||
import { CONSENT_CHANGED_EVENT, disableAnalytics, getConsentState } from '@/lib/analytics';
|
import { CONSENT_CHANGED_EVENT, disableAnalytics, getConsentState } from '@/lib/client/analytics';
|
||||||
|
|
||||||
export function ConsentAwareAnalytics() {
|
export function ConsentAwareAnalytics() {
|
||||||
const [enabled, setEnabled] = useState(false);
|
const [enabled, setEnabled] = useState(false);
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { getConsentState, setConsentState } from '@/lib/analytics';
|
import { getConsentState, setConsentState } from '@/lib/client/analytics';
|
||||||
import { Transition } from '@headlessui/react';
|
import { Transition } from '@headlessui/react';
|
||||||
|
|
||||||
export function CookieConsentBanner() {
|
export function CookieConsentBanner() {
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import {
|
||||||
TransitionChild,
|
TransitionChild,
|
||||||
Button,
|
Button,
|
||||||
} from '@headlessui/react';
|
} from '@headlessui/react';
|
||||||
import { updateAppConfig, getAppConfig } from '@/lib/dexie';
|
import { updateAppConfig, getAppConfig } from '@/lib/client/dexie';
|
||||||
|
|
||||||
interface PrivacyModalProps {
|
interface PrivacyModalProps {
|
||||||
onAccept?: () => void;
|
onAccept?: () => void;
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ import Link from 'next/link';
|
||||||
import { useTheme } from '@/contexts/ThemeContext';
|
import { useTheme } from '@/contexts/ThemeContext';
|
||||||
import { useConfig } from '@/contexts/ConfigContext';
|
import { useConfig } from '@/contexts/ConfigContext';
|
||||||
import { ChevronUpDownIcon, CheckIcon, SettingsIcon } from '@/components/icons/Icons';
|
import { ChevronUpDownIcon, CheckIcon, SettingsIcon } from '@/components/icons/Icons';
|
||||||
import { getAppConfig, getFirstVisit, setFirstVisit } from '@/lib/dexie';
|
import { getAppConfig, getFirstVisit, setFirstVisit } from '@/lib/client/dexie';
|
||||||
import { useDocuments } from '@/contexts/DocumentContext';
|
import { useDocuments } from '@/contexts/DocumentContext';
|
||||||
import { ConfirmDialog } from '@/components/ConfirmDialog';
|
import { ConfirmDialog } from '@/components/ConfirmDialog';
|
||||||
import { ProgressPopup } from '@/components/ProgressPopup';
|
import { ProgressPopup } from '@/components/ProgressPopup';
|
||||||
|
|
@ -31,14 +31,14 @@ import { useTimeEstimation } from '@/hooks/useTimeEstimation';
|
||||||
import { THEMES } from '@/contexts/ThemeContext';
|
import { THEMES } from '@/contexts/ThemeContext';
|
||||||
import { DocumentSelectionModal } from '@/components/documents/DocumentSelectionModal';
|
import { DocumentSelectionModal } from '@/components/documents/DocumentSelectionModal';
|
||||||
import { BaseDocument } from '@/types/documents';
|
import { BaseDocument } from '@/types/documents';
|
||||||
import { getAuthClient } from '@/lib/auth-client';
|
import { getAuthClient } from '@/lib/client/auth-client';
|
||||||
import { useAuthSession } from '@/hooks/useAuthSession';
|
import { useAuthSession } from '@/hooks/useAuthSession';
|
||||||
import { useAuthConfig } from '@/contexts/AuthRateLimitContext';
|
import { useAuthConfig } from '@/contexts/AuthRateLimitContext';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { showPrivacyModal } from '@/components/PrivacyModal';
|
import { showPrivacyModal } from '@/components/PrivacyModal';
|
||||||
import { deleteDocuments, mimeTypeForDoc, uploadDocuments } from '@/lib/client-documents';
|
import { deleteDocuments, mimeTypeForDoc, uploadDocuments } from '@/lib/client/api/documents';
|
||||||
import { cacheStoredDocumentFromBytes, clearDocumentCache } from '@/lib/document-cache';
|
import { cacheStoredDocumentFromBytes, clearDocumentCache } from '@/lib/client/cache/documents';
|
||||||
import { clearAllDocumentPreviewCaches, clearInMemoryDocumentPreviewCache } from '@/lib/document-preview-cache';
|
import { clearAllDocumentPreviewCaches, clearInMemoryDocumentPreviewCache } from '@/lib/client/cache/previews';
|
||||||
|
|
||||||
const enableDestructiveDelete = process.env.NEXT_PUBLIC_ENABLE_DESTRUCTIVE_DELETE_ACTIONS !== 'false';
|
const enableDestructiveDelete = process.env.NEXT_PUBLIC_ENABLE_DESTRUCTIVE_DELETE_ACTIONS !== 'false';
|
||||||
const showAllDeepInfra = process.env.NEXT_PUBLIC_SHOW_ALL_DEEPINFRA_MODELS !== 'false';
|
const showAllDeepInfra = process.env.NEXT_PUBLIC_SHOW_ALL_DEEPINFRA_MODELS !== 'false';
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import type { BetterFetchError } from 'better-auth/react';
|
||||||
import { usePathname, useRouter } from 'next/navigation';
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
import { useAuthConfig, useAuthRateLimit } from '@/contexts/AuthRateLimitContext';
|
import { useAuthConfig, useAuthRateLimit } from '@/contexts/AuthRateLimitContext';
|
||||||
import { useAuthSession } from '@/hooks/useAuthSession';
|
import { useAuthSession } from '@/hooks/useAuthSession';
|
||||||
import { getAuthClient } from '@/lib/auth-client';
|
import { getAuthClient } from '@/lib/client/auth-client';
|
||||||
import { LoadingSpinner } from '@/components/Spinner';
|
import { LoadingSpinner } from '@/components/Spinner';
|
||||||
|
|
||||||
function sleep(ms: number) {
|
function sleep(ms: number) {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { Button } from '@headlessui/react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useAuthConfig } from '@/contexts/AuthRateLimitContext';
|
import { useAuthConfig } from '@/contexts/AuthRateLimitContext';
|
||||||
import { useAuthSession } from '@/hooks/useAuthSession';
|
import { useAuthSession } from '@/hooks/useAuthSession';
|
||||||
import { getAuthClient } from '@/lib/auth-client';
|
import { getAuthClient } from '@/lib/client/auth-client';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
export function UserMenu({ className = '' }: { className?: string }) {
|
export function UserMenu({ className = '' }: { className?: string }) {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { useDocuments } from '@/contexts/DocumentContext';
|
||||||
import { DndProvider } from 'react-dnd';
|
import { DndProvider } from 'react-dnd';
|
||||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||||
import { DocumentType, DocumentListDocument, Folder, DocumentListState, SortBy, SortDirection } from '@/types/documents';
|
import { DocumentType, DocumentListDocument, Folder, DocumentListState, SortBy, SortDirection } from '@/types/documents';
|
||||||
import { getDocumentListState, saveDocumentListState } from '@/lib/dexie';
|
import { getDocumentListState, saveDocumentListState } from '@/lib/client/dexie';
|
||||||
import { ConfirmDialog } from '@/components/ConfirmDialog';
|
import { ConfirmDialog } from '@/components/ConfirmDialog';
|
||||||
import { DocumentListItem } from '@/components/doclist/DocumentListItem';
|
import { DocumentListItem } from '@/components/doclist/DocumentListItem';
|
||||||
import { DocumentFolder } from '@/components/doclist/DocumentFolder';
|
import { DocumentFolder } from '@/components/doclist/DocumentFolder';
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,13 @@ import {
|
||||||
documentPreviewFallbackUrl,
|
documentPreviewFallbackUrl,
|
||||||
getDocumentContentSnippet,
|
getDocumentContentSnippet,
|
||||||
getDocumentPreviewStatus,
|
getDocumentPreviewStatus,
|
||||||
} from '@/lib/client-documents';
|
} from '@/lib/client/api/documents';
|
||||||
import {
|
import {
|
||||||
getInMemoryDocumentPreviewUrl,
|
getInMemoryDocumentPreviewUrl,
|
||||||
getPersistedDocumentPreviewUrl,
|
getPersistedDocumentPreviewUrl,
|
||||||
primeDocumentPreviewCache,
|
primeDocumentPreviewCache,
|
||||||
setInMemoryDocumentPreviewUrl,
|
setInMemoryDocumentPreviewUrl,
|
||||||
} from '@/lib/document-preview-cache';
|
} from '@/lib/client/cache/previews';
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
import remarkGfm from 'remark-gfm';
|
import remarkGfm from 'remark-gfm';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,11 @@
|
||||||
|
|
||||||
import { Fragment, useCallback, useEffect, useRef, useState } from 'react';
|
import { Fragment, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild, Button } from '@headlessui/react';
|
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild, Button } from '@headlessui/react';
|
||||||
import { getAppConfig, getAllEpubDocuments, getAllHtmlDocuments, getAllPdfDocuments, updateAppConfig } from '@/lib/dexie';
|
import { getAppConfig, getAllEpubDocuments, getAllHtmlDocuments, getAllPdfDocuments, updateAppConfig } from '@/lib/client/dexie';
|
||||||
import { listDocuments, mimeTypeForDoc, uploadDocuments } from '@/lib/client-documents';
|
import { listDocuments, mimeTypeForDoc, uploadDocuments } from '@/lib/client/api/documents';
|
||||||
import { useDocuments } from '@/contexts/DocumentContext';
|
import { useDocuments } from '@/contexts/DocumentContext';
|
||||||
import type { BaseDocument } from '@/types/documents';
|
import type { BaseDocument } from '@/types/documents';
|
||||||
import { cacheStoredDocumentFromBytes } from '@/lib/document-cache';
|
import { cacheStoredDocumentFromBytes } from '@/lib/client/cache/documents';
|
||||||
|
|
||||||
export function DexieMigrationModal() {
|
export function DexieMigrationModal() {
|
||||||
const { refreshDocuments } = useDocuments();
|
const { refreshDocuments } = useDocuments();
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { useState, useCallback } from 'react';
|
||||||
import { useDropzone } from 'react-dropzone';
|
import { useDropzone } from 'react-dropzone';
|
||||||
import { UploadIcon } from '@/components/icons/Icons';
|
import { UploadIcon } from '@/components/icons/Icons';
|
||||||
import { useDocuments } from '@/contexts/DocumentContext';
|
import { useDocuments } from '@/contexts/DocumentContext';
|
||||||
import { uploadDocxAsPdf } from '@/lib/client-documents';
|
import { uploadDocxAsPdf } from '@/lib/client/api/documents';
|
||||||
|
|
||||||
const enableDocx = process.env.NEXT_PUBLIC_ENABLE_DOCX_CONVERSION !== 'false';
|
const enableDocx = process.env.NEXT_PUBLIC_ENABLE_DOCX_CONVERSION !== 'false';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import {
|
||||||
} from '@headlessui/react';
|
} from '@headlessui/react';
|
||||||
import { ChevronUpDownIcon, AudioWaveIcon } from '@/components/icons/Icons';
|
import { ChevronUpDownIcon, AudioWaveIcon } from '@/components/icons/Icons';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { buildKokoroVoiceString, getMaxVoicesForProvider, isKokoroModel, parseKokoroVoiceNames } from '@/lib/kokoro';
|
import { buildKokoroVoiceString, getMaxVoicesForProvider, isKokoroModel, parseKokoroVoiceNames } from '@/lib/shared/kokoro';
|
||||||
|
|
||||||
export function VoicesControlBase({
|
export function VoicesControlBase({
|
||||||
availableVoices,
|
availableVoices,
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@
|
||||||
|
|
||||||
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, ReactNode } from 'react';
|
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, ReactNode } from 'react';
|
||||||
import { useLiveQuery } from 'dexie-react-hooks';
|
import { useLiveQuery } from 'dexie-react-hooks';
|
||||||
import { db, initDB, migrateLegacyDexieDocumentIdsToSha, updateAppConfig } from '@/lib/dexie';
|
import { db, initDB, migrateLegacyDexieDocumentIdsToSha, updateAppConfig } from '@/lib/client/dexie';
|
||||||
import { APP_CONFIG_DEFAULTS, type ViewType, type SavedVoices, type AppConfigValues, type AppConfigRow } from '@/types/config';
|
import { APP_CONFIG_DEFAULTS, type ViewType, type SavedVoices, type AppConfigValues, type AppConfigRow } from '@/types/config';
|
||||||
import { scheduleUserPreferencesSync, cancelPendingPreferenceSync, getUserPreferences, putUserPreferences } from '@/lib/client-user-state';
|
import { scheduleUserPreferencesSync, cancelPendingPreferenceSync, getUserPreferences, putUserPreferences } from '@/lib/client/api/user-state';
|
||||||
import { SYNCED_PREFERENCE_KEYS, type SyncedPreferenceKey, type SyncedPreferencesPatch } from '@/types/user-state';
|
import { SYNCED_PREFERENCE_KEYS, type SyncedPreferenceKey, type SyncedPreferencesPatch } from '@/types/user-state';
|
||||||
import { useAuthSession } from '@/hooks/useAuthSession';
|
import { useAuthSession } from '@/hooks/useAuthSession';
|
||||||
import { useAuthConfig } from '@/contexts/AuthRateLimitContext';
|
import { useAuthConfig } from '@/contexts/AuthRateLimitContext';
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
|
|
||||||
import { createContext, useCallback, useContext, useEffect, useMemo, useState, ReactNode } from 'react';
|
import { createContext, useCallback, useContext, useEffect, useMemo, useState, ReactNode } from 'react';
|
||||||
import type { BaseDocument } from '@/types/documents';
|
import type { BaseDocument } from '@/types/documents';
|
||||||
import { listDocuments, uploadDocuments, deleteDocuments } from '@/lib/client-documents';
|
import { listDocuments, uploadDocuments, deleteDocuments } from '@/lib/client/api/documents';
|
||||||
import { putCachedEpub, putCachedHtml, putCachedPdf, evictCachedEpub, evictCachedHtml, evictCachedPdf } from '@/lib/document-cache';
|
import { putCachedEpub, putCachedHtml, putCachedPdf, evictCachedEpub, evictCachedHtml, evictCachedPdf } from '@/lib/client/cache/documents';
|
||||||
import { useAuthSession } from '@/hooks/useAuthSession';
|
import { useAuthSession } from '@/hooks/useAuthSession';
|
||||||
|
|
||||||
interface DocumentContextType {
|
interface DocumentContextType {
|
||||||
|
|
|
||||||
|
|
@ -16,16 +16,16 @@ import type { NavItem } from 'epubjs';
|
||||||
import type { SpineItem } from 'epubjs/types/section';
|
import type { SpineItem } from 'epubjs/types/section';
|
||||||
import type { Book, Rendition } from 'epubjs';
|
import type { Book, Rendition } from 'epubjs';
|
||||||
|
|
||||||
import { setLastDocumentLocation } from '@/lib/dexie';
|
import { setLastDocumentLocation } from '@/lib/client/dexie';
|
||||||
import { scheduleDocumentProgressSync } from '@/lib/client-user-state';
|
import { scheduleDocumentProgressSync } from '@/lib/client/api/user-state';
|
||||||
import { getDocumentMetadata } from '@/lib/client-documents';
|
import { getDocumentMetadata } from '@/lib/client/api/documents';
|
||||||
import { ensureCachedDocument } from '@/lib/document-cache';
|
import { ensureCachedDocument } from '@/lib/client/cache/documents';
|
||||||
import { useTTS } from '@/contexts/TTSContext';
|
import { useTTS } from '@/contexts/TTSContext';
|
||||||
import { useAuthConfig } from '@/contexts/AuthRateLimitContext';
|
import { useAuthConfig } from '@/contexts/AuthRateLimitContext';
|
||||||
import { createRangeCfi } from '@/lib/epub';
|
import { createRangeCfi } from '@/lib/client/epub';
|
||||||
import { useParams } from 'next/navigation';
|
import { useParams } from 'next/navigation';
|
||||||
import { useConfig } from './ConfigContext';
|
import { useConfig } from './ConfigContext';
|
||||||
import { withRetry, getAudiobookStatus, generateTTS, createAudiobookChapter } from '@/lib/client';
|
import { withRetry, getAudiobookStatus, generateTTS, createAudiobookChapter } from '@/lib/client/api/audiobooks';
|
||||||
import { CmpStr } from 'cmpstr';
|
import { CmpStr } from 'cmpstr';
|
||||||
import type {
|
import type {
|
||||||
TTSSentenceAlignment,
|
TTSSentenceAlignment,
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,8 @@ import {
|
||||||
useEffect,
|
useEffect,
|
||||||
useRef,
|
useRef,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { getDocumentMetadata } from '@/lib/client-documents';
|
import { getDocumentMetadata } from '@/lib/client/api/documents';
|
||||||
import { ensureCachedDocument } from '@/lib/document-cache';
|
import { ensureCachedDocument } from '@/lib/client/cache/documents';
|
||||||
import { useTTS } from '@/contexts/TTSContext';
|
import { useTTS } from '@/contexts/TTSContext';
|
||||||
|
|
||||||
interface HTMLContextType {
|
interface HTMLContextType {
|
||||||
|
|
|
||||||
|
|
@ -27,19 +27,19 @@ import {
|
||||||
|
|
||||||
import type { PDFDocumentProxy } from 'pdfjs-dist';
|
import type { PDFDocumentProxy } from 'pdfjs-dist';
|
||||||
|
|
||||||
import { getDocumentMetadata } from '@/lib/client-documents';
|
import { getDocumentMetadata } from '@/lib/client/api/documents';
|
||||||
import { ensureCachedDocument } from '@/lib/document-cache';
|
import { ensureCachedDocument } from '@/lib/client/cache/documents';
|
||||||
import { useTTS } from '@/contexts/TTSContext';
|
import { useTTS } from '@/contexts/TTSContext';
|
||||||
import { useConfig } from '@/contexts/ConfigContext';
|
import { useConfig } from '@/contexts/ConfigContext';
|
||||||
import { normalizeTextForTts } from '@/lib/nlp';
|
import { normalizeTextForTts } from '@/lib/shared/nlp';
|
||||||
import { withRetry, getAudiobookStatus, generateTTS, createAudiobookChapter } from '@/lib/client';
|
import { withRetry, getAudiobookStatus, generateTTS, createAudiobookChapter } from '@/lib/client/api/audiobooks';
|
||||||
import {
|
import {
|
||||||
extractTextFromPDF,
|
extractTextFromPDF,
|
||||||
highlightPattern,
|
highlightPattern,
|
||||||
clearHighlights,
|
clearHighlights,
|
||||||
clearWordHighlights,
|
clearWordHighlights,
|
||||||
highlightWordIndex,
|
highlightWordIndex,
|
||||||
} from '@/lib/pdf';
|
} from '@/lib/client/pdf';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
TTSSentenceAlignment,
|
TTSSentenceAlignment,
|
||||||
|
|
|
||||||
|
|
@ -34,12 +34,12 @@ import { useAudioCache } from '@/hooks/audio/useAudioCache';
|
||||||
import { useVoiceManagement } from '@/hooks/audio/useVoiceManagement';
|
import { useVoiceManagement } from '@/hooks/audio/useVoiceManagement';
|
||||||
import { useMediaSession } from '@/hooks/audio/useMediaSession';
|
import { useMediaSession } from '@/hooks/audio/useMediaSession';
|
||||||
import { useAudioContext } from '@/hooks/audio/useAudioContext';
|
import { useAudioContext } from '@/hooks/audio/useAudioContext';
|
||||||
import { getLastDocumentLocation, setLastDocumentLocation } from '@/lib/dexie';
|
import { getLastDocumentLocation, setLastDocumentLocation } from '@/lib/client/dexie';
|
||||||
import { getDocumentProgress, scheduleDocumentProgressSync } from '@/lib/client-user-state';
|
import { getDocumentProgress, scheduleDocumentProgressSync } from '@/lib/client/api/user-state';
|
||||||
import { useBackgroundState } from '@/hooks/audio/useBackgroundState';
|
import { useBackgroundState } from '@/hooks/audio/useBackgroundState';
|
||||||
import { withRetry, generateTTS, alignAudio } from '@/lib/client';
|
import { withRetry, generateTTS, alignAudio } from '@/lib/client/api/audiobooks';
|
||||||
import { preprocessSentenceForAudio, splitTextToTtsBlocks, splitTextToTtsBlocksEPUB } from '@/lib/nlp';
|
import { preprocessSentenceForAudio, splitTextToTtsBlocks, splitTextToTtsBlocksEPUB } from '@/lib/shared/nlp';
|
||||||
import { isKokoroModel } from '@/lib/kokoro';
|
import { isKokoroModel } from '@/lib/shared/kokoro';
|
||||||
import { useAuthRateLimit } from '@/contexts/AuthRateLimitContext';
|
import { useAuthRateLimit } from '@/contexts/AuthRateLimitContext';
|
||||||
import type {
|
import type {
|
||||||
TTSLocation,
|
TTSLocation,
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,11 @@ export const documents = pgTable('documents', {
|
||||||
lastModified: bigint('last_modified', { mode: 'number' }).notNull(),
|
lastModified: bigint('last_modified', { mode: 'number' }).notNull(),
|
||||||
filePath: text('file_path').notNull(),
|
filePath: text('file_path').notNull(),
|
||||||
createdAt: timestamp('created_at').defaultNow(),
|
createdAt: timestamp('created_at').defaultNow(),
|
||||||
}, (table) => ({
|
}, (table) => [
|
||||||
pk: primaryKey({ columns: [table.id, table.userId] }),
|
primaryKey({ columns: [table.id, table.userId] }),
|
||||||
userIdIdx: index('idx_documents_user_id').on(table.userId),
|
index('idx_documents_user_id').on(table.userId),
|
||||||
userIdLastModifiedIdx: index('idx_documents_user_id_last_modified').on(table.userId, table.lastModified),
|
index('idx_documents_user_id_last_modified').on(table.userId, table.lastModified),
|
||||||
}));
|
]);
|
||||||
|
|
||||||
export const audiobooks = pgTable('audiobooks', {
|
export const audiobooks = pgTable('audiobooks', {
|
||||||
id: text('id').notNull(),
|
id: text('id').notNull(),
|
||||||
|
|
@ -25,9 +25,9 @@ export const audiobooks = pgTable('audiobooks', {
|
||||||
coverPath: text('cover_path'),
|
coverPath: text('cover_path'),
|
||||||
duration: real('duration').default(0),
|
duration: real('duration').default(0),
|
||||||
createdAt: timestamp('created_at').defaultNow(),
|
createdAt: timestamp('created_at').defaultNow(),
|
||||||
}, (table) => ({
|
}, (table) => [
|
||||||
pk: primaryKey({ columns: [table.id, table.userId] }),
|
primaryKey({ columns: [table.id, table.userId] }),
|
||||||
}));
|
]);
|
||||||
|
|
||||||
export const audiobookChapters = pgTable('audiobook_chapters', {
|
export const audiobookChapters = pgTable('audiobook_chapters', {
|
||||||
id: text('id').notNull(),
|
id: text('id').notNull(),
|
||||||
|
|
@ -38,13 +38,13 @@ export const audiobookChapters = pgTable('audiobook_chapters', {
|
||||||
duration: real('duration').default(0),
|
duration: real('duration').default(0),
|
||||||
filePath: text('file_path').notNull(),
|
filePath: text('file_path').notNull(),
|
||||||
format: text('format').notNull(), // mp3, m4b
|
format: text('format').notNull(), // mp3, m4b
|
||||||
}, (table) => ({
|
}, (table) => [
|
||||||
pk: primaryKey({ columns: [table.id, table.userId] }),
|
primaryKey({ columns: [table.id, table.userId] }),
|
||||||
bookFk: foreignKey({
|
foreignKey({
|
||||||
columns: [table.bookId, table.userId],
|
columns: [table.bookId, table.userId],
|
||||||
foreignColumns: [audiobooks.id, audiobooks.userId],
|
foreignColumns: [audiobooks.id, audiobooks.userId],
|
||||||
}).onDelete('cascade'),
|
}).onDelete('cascade'),
|
||||||
}));
|
]);
|
||||||
|
|
||||||
// Auth tables (user, session, account, verification) are managed by Better Auth.
|
// Auth tables (user, session, account, verification) are managed by Better Auth.
|
||||||
// They are created/migrated via `@better-auth/cli migrate` and should NOT be
|
// They are created/migrated via `@better-auth/cli migrate` and should NOT be
|
||||||
|
|
@ -56,10 +56,10 @@ export const userTtsChars = pgTable("user_tts_chars", {
|
||||||
charCount: bigint('char_count', { mode: 'number' }).default(0),
|
charCount: bigint('char_count', { mode: 'number' }).default(0),
|
||||||
createdAt: timestamp('created_at').defaultNow(),
|
createdAt: timestamp('created_at').defaultNow(),
|
||||||
updatedAt: timestamp('updated_at').defaultNow(),
|
updatedAt: timestamp('updated_at').defaultNow(),
|
||||||
}, (table) => ({
|
}, (table) => [
|
||||||
pk: primaryKey({ columns: [table.userId, table.date] }),
|
primaryKey({ columns: [table.userId, table.date] }),
|
||||||
dateIdx: index('idx_user_tts_chars_date').on(table.date),
|
index('idx_user_tts_chars_date').on(table.date),
|
||||||
}));
|
]);
|
||||||
|
|
||||||
export const userPreferences = pgTable('user_preferences', {
|
export const userPreferences = pgTable('user_preferences', {
|
||||||
userId: text('user_id').primaryKey().references(() => user.id, { onDelete: 'cascade' }),
|
userId: text('user_id').primaryKey().references(() => user.id, { onDelete: 'cascade' }),
|
||||||
|
|
@ -78,10 +78,10 @@ export const userDocumentProgress = pgTable('user_document_progress', {
|
||||||
clientUpdatedAtMs: bigint('client_updated_at_ms', { mode: 'number' }).notNull().default(0),
|
clientUpdatedAtMs: bigint('client_updated_at_ms', { mode: 'number' }).notNull().default(0),
|
||||||
createdAt: timestamp('created_at').defaultNow(),
|
createdAt: timestamp('created_at').defaultNow(),
|
||||||
updatedAt: timestamp('updated_at').defaultNow(),
|
updatedAt: timestamp('updated_at').defaultNow(),
|
||||||
}, (table) => ({
|
}, (table) => [
|
||||||
pk: primaryKey({ columns: [table.userId, table.documentId] }),
|
primaryKey({ columns: [table.userId, table.documentId] }),
|
||||||
userUpdatedIdx: index('idx_user_document_progress_user_id_updated_at').on(table.userId, table.updatedAt),
|
index('idx_user_document_progress_user_id_updated_at').on(table.userId, table.updatedAt),
|
||||||
}));
|
]);
|
||||||
|
|
||||||
export const documentPreviews = pgTable('document_previews', {
|
export const documentPreviews = pgTable('document_previews', {
|
||||||
documentId: text('document_id').notNull(),
|
documentId: text('document_id').notNull(),
|
||||||
|
|
@ -101,7 +101,7 @@ export const documentPreviews = pgTable('document_previews', {
|
||||||
lastError: text('last_error'),
|
lastError: text('last_error'),
|
||||||
createdAtMs: bigint('created_at_ms', { mode: 'number' }).notNull().default(0),
|
createdAtMs: bigint('created_at_ms', { mode: 'number' }).notNull().default(0),
|
||||||
updatedAtMs: bigint('updated_at_ms', { mode: 'number' }).notNull().default(0),
|
updatedAtMs: bigint('updated_at_ms', { mode: 'number' }).notNull().default(0),
|
||||||
}, (table) => ({
|
}, (table) => [
|
||||||
pk: primaryKey({ columns: [table.documentId, table.namespace, table.variant] }),
|
primaryKey({ columns: [table.documentId, table.namespace, table.variant] }),
|
||||||
statusLeaseIdx: index('idx_document_previews_status_lease').on(table.status, table.leaseUntilMs),
|
index('idx_document_previews_status_lease').on(table.status, table.leaseUntilMs),
|
||||||
}));
|
]);
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,11 @@ export const documents = sqliteTable('documents', {
|
||||||
lastModified: integer('last_modified').notNull(),
|
lastModified: integer('last_modified').notNull(),
|
||||||
filePath: text('file_path').notNull(),
|
filePath: text('file_path').notNull(),
|
||||||
createdAt: integer('created_at').default(sql`(cast(strftime('%s','now') as int) * 1000)`),
|
createdAt: integer('created_at').default(sql`(cast(strftime('%s','now') as int) * 1000)`),
|
||||||
}, (table) => ({
|
}, (table) => [
|
||||||
pk: primaryKey({ columns: [table.id, table.userId] }),
|
primaryKey({ columns: [table.id, table.userId] }),
|
||||||
userIdIdx: index('idx_documents_user_id').on(table.userId),
|
index('idx_documents_user_id').on(table.userId),
|
||||||
userIdLastModifiedIdx: index('idx_documents_user_id_last_modified').on(table.userId, table.lastModified),
|
index('idx_documents_user_id_last_modified').on(table.userId, table.lastModified),
|
||||||
}));
|
]);
|
||||||
|
|
||||||
export const audiobooks = sqliteTable('audiobooks', {
|
export const audiobooks = sqliteTable('audiobooks', {
|
||||||
id: text('id').notNull(),
|
id: text('id').notNull(),
|
||||||
|
|
@ -26,9 +26,9 @@ export const audiobooks = sqliteTable('audiobooks', {
|
||||||
coverPath: text('cover_path'),
|
coverPath: text('cover_path'),
|
||||||
duration: real('duration').default(0),
|
duration: real('duration').default(0),
|
||||||
createdAt: integer('created_at').default(sql`(cast(strftime('%s','now') as int) * 1000)`),
|
createdAt: integer('created_at').default(sql`(cast(strftime('%s','now') as int) * 1000)`),
|
||||||
}, (table) => ({
|
}, (table) => [
|
||||||
pk: primaryKey({ columns: [table.id, table.userId] }),
|
primaryKey({ columns: [table.id, table.userId] }),
|
||||||
}));
|
]);
|
||||||
|
|
||||||
export const audiobookChapters = sqliteTable('audiobook_chapters', {
|
export const audiobookChapters = sqliteTable('audiobook_chapters', {
|
||||||
id: text('id').notNull(),
|
id: text('id').notNull(),
|
||||||
|
|
@ -39,13 +39,13 @@ export const audiobookChapters = sqliteTable('audiobook_chapters', {
|
||||||
duration: real('duration').default(0),
|
duration: real('duration').default(0),
|
||||||
filePath: text('file_path').notNull(),
|
filePath: text('file_path').notNull(),
|
||||||
format: text('format').notNull(), // mp3, m4b
|
format: text('format').notNull(), // mp3, m4b
|
||||||
}, (table) => ({
|
}, (table) => [
|
||||||
pk: primaryKey({ columns: [table.id, table.userId] }),
|
primaryKey({ columns: [table.id, table.userId] }),
|
||||||
bookFk: foreignKey({
|
foreignKey({
|
||||||
columns: [table.bookId, table.userId],
|
columns: [table.bookId, table.userId],
|
||||||
foreignColumns: [audiobooks.id, audiobooks.userId],
|
foreignColumns: [audiobooks.id, audiobooks.userId],
|
||||||
}).onDelete('cascade'),
|
}).onDelete('cascade'),
|
||||||
}));
|
]);
|
||||||
|
|
||||||
// Auth tables (user, session, account, verification) are managed by Better Auth.
|
// Auth tables (user, session, account, verification) are managed by Better Auth.
|
||||||
// They are created/migrated via `@better-auth/cli migrate` and should NOT be
|
// They are created/migrated via `@better-auth/cli migrate` and should NOT be
|
||||||
|
|
@ -57,10 +57,10 @@ export const userTtsChars = sqliteTable("user_tts_chars", {
|
||||||
charCount: integer('char_count').default(0),
|
charCount: integer('char_count').default(0),
|
||||||
createdAt: integer('created_at').default(sql`(cast(strftime('%s','now') as int) * 1000)`),
|
createdAt: integer('created_at').default(sql`(cast(strftime('%s','now') as int) * 1000)`),
|
||||||
updatedAt: integer('updated_at').default(sql`(cast(strftime('%s','now') as int) * 1000)`),
|
updatedAt: integer('updated_at').default(sql`(cast(strftime('%s','now') as int) * 1000)`),
|
||||||
}, (table) => ({
|
}, (table) => [
|
||||||
pk: primaryKey({ columns: [table.userId, table.date] }),
|
primaryKey({ columns: [table.userId, table.date] }),
|
||||||
dateIdx: index('idx_user_tts_chars_date').on(table.date),
|
index('idx_user_tts_chars_date').on(table.date),
|
||||||
}));
|
]);
|
||||||
|
|
||||||
export const userPreferences = sqliteTable('user_preferences', {
|
export const userPreferences = sqliteTable('user_preferences', {
|
||||||
userId: text('user_id').primaryKey().references(() => user.id, { onDelete: 'cascade' }),
|
userId: text('user_id').primaryKey().references(() => user.id, { onDelete: 'cascade' }),
|
||||||
|
|
@ -79,10 +79,10 @@ export const userDocumentProgress = sqliteTable('user_document_progress', {
|
||||||
clientUpdatedAtMs: integer('client_updated_at_ms').notNull().default(0),
|
clientUpdatedAtMs: integer('client_updated_at_ms').notNull().default(0),
|
||||||
createdAt: integer('created_at').default(sql`(cast(strftime('%s','now') as int) * 1000)`),
|
createdAt: integer('created_at').default(sql`(cast(strftime('%s','now') as int) * 1000)`),
|
||||||
updatedAt: integer('updated_at').default(sql`(cast(strftime('%s','now') as int) * 1000)`),
|
updatedAt: integer('updated_at').default(sql`(cast(strftime('%s','now') as int) * 1000)`),
|
||||||
}, (table) => ({
|
}, (table) => [
|
||||||
pk: primaryKey({ columns: [table.userId, table.documentId] }),
|
primaryKey({ columns: [table.userId, table.documentId] }),
|
||||||
userUpdatedIdx: index('idx_user_document_progress_user_id_updated_at').on(table.userId, table.updatedAt),
|
index('idx_user_document_progress_user_id_updated_at').on(table.userId, table.updatedAt),
|
||||||
}));
|
]);
|
||||||
|
|
||||||
export const documentPreviews = sqliteTable('document_previews', {
|
export const documentPreviews = sqliteTable('document_previews', {
|
||||||
documentId: text('document_id').notNull(),
|
documentId: text('document_id').notNull(),
|
||||||
|
|
@ -102,7 +102,7 @@ export const documentPreviews = sqliteTable('document_previews', {
|
||||||
lastError: text('last_error'),
|
lastError: text('last_error'),
|
||||||
createdAtMs: integer('created_at_ms').notNull().default(0),
|
createdAtMs: integer('created_at_ms').notNull().default(0),
|
||||||
updatedAtMs: integer('updated_at_ms').notNull().default(0),
|
updatedAtMs: integer('updated_at_ms').notNull().default(0),
|
||||||
}, (table) => ({
|
}, (table) => [
|
||||||
pk: primaryKey({ columns: [table.documentId, table.namespace, table.variant] }),
|
primaryKey({ columns: [table.documentId, table.namespace, table.variant] }),
|
||||||
statusLeaseIdx: index('idx_document_previews_status_lease').on(table.status, table.leaseUntilMs),
|
index('idx_document_previews_status_lease').on(table.status, table.leaseUntilMs),
|
||||||
}));
|
]);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import { getVoices } from '@/lib/client';
|
import { getVoices } from '@/lib/client/api/audiobooks';
|
||||||
|
|
||||||
const DEFAULT_VOICES = ['alloy', 'ash', 'coral', 'echo', 'fable', 'onyx', 'nova', 'sage', 'shimmer'];
|
const DEFAULT_VOICES = ['alloy', 'ash', 'coral', 'echo', 'fable', 'onyx', 'nova', 'sage', 'shimmer'];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@
|
||||||
|
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useLiveQuery } from 'dexie-react-hooks';
|
import { useLiveQuery } from 'dexie-react-hooks';
|
||||||
import { db } from '@/lib/dexie';
|
import { db } from '@/lib/client/dexie';
|
||||||
import type { EPUBDocument } from '@/types/documents';
|
import type { EPUBDocument } from '@/types/documents';
|
||||||
import { sha256HexFromArrayBuffer } from '@/lib/sha256';
|
import { sha256HexFromArrayBuffer } from '@/lib/client/sha256';
|
||||||
|
|
||||||
export function useEPUBDocuments() {
|
export function useEPUBDocuments() {
|
||||||
const documents = useLiveQuery(
|
const documents = useLiveQuery(
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useEffect, RefObject, useState } from 'react';
|
import { useEffect, RefObject, useState } from 'react';
|
||||||
import { debounce } from '@/lib/pdf';
|
import { debounce } from '@/lib/client/pdf';
|
||||||
|
|
||||||
export function useEPUBResize(containerRef: RefObject<HTMLDivElement | null>) {
|
export function useEPUBResize(containerRef: RefObject<HTMLDivElement | null>) {
|
||||||
const [isResizing, setIsResizing] = useState(false);
|
const [isResizing, setIsResizing] = useState(false);
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@
|
||||||
|
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useLiveQuery } from 'dexie-react-hooks';
|
import { useLiveQuery } from 'dexie-react-hooks';
|
||||||
import { db } from '@/lib/dexie';
|
import { db } from '@/lib/client/dexie';
|
||||||
import type { HTMLDocument } from '@/types/documents';
|
import type { HTMLDocument } from '@/types/documents';
|
||||||
import { sha256HexFromString } from '@/lib/sha256';
|
import { sha256HexFromString } from '@/lib/client/sha256';
|
||||||
|
|
||||||
export function useHTMLDocuments() {
|
export function useHTMLDocuments() {
|
||||||
const documents = useLiveQuery(
|
const documents = useLiveQuery(
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@
|
||||||
|
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useLiveQuery } from 'dexie-react-hooks';
|
import { useLiveQuery } from 'dexie-react-hooks';
|
||||||
import { db } from '@/lib/dexie';
|
import { db } from '@/lib/client/dexie';
|
||||||
import type { PDFDocument } from '@/types/documents';
|
import type { PDFDocument } from '@/types/documents';
|
||||||
import { sha256HexFromArrayBuffer } from '@/lib/sha256';
|
import { sha256HexFromArrayBuffer } from '@/lib/client/sha256';
|
||||||
|
|
||||||
export function usePDFDocuments() {
|
export function usePDFDocuments() {
|
||||||
const documents = useLiveQuery(
|
const documents = useLiveQuery(
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { RefObject, useState, useEffect } from 'react';
|
import { RefObject, useState, useEffect } from 'react';
|
||||||
import { debounce } from '@/lib/pdf';
|
import { debounce } from '@/lib/client/pdf';
|
||||||
|
|
||||||
interface UsePDFResizeResult {
|
interface UsePDFResizeResult {
|
||||||
containerWidth: number;
|
containerWidth: number;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useAuthConfig } from '@/contexts/AuthRateLimitContext';
|
import { useAuthConfig } from '@/contexts/AuthRateLimitContext';
|
||||||
import { getAuthClient } from '@/lib/auth-client';
|
import { getAuthClient } from '@/lib/client/auth-client';
|
||||||
|
|
||||||
type SessionHookResult = ReturnType<ReturnType<typeof getAuthClient>['useSession']>;
|
type SessionHookResult = ReturnType<ReturnType<typeof getAuthClient>['useSession']>;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { sha256HexFromArrayBuffer } from '@/lib/sha256';
|
import { sha256HexFromArrayBuffer } from '@/lib/client/sha256';
|
||||||
import type { BaseDocument, DocumentType } from '@/types/documents';
|
import type { BaseDocument, DocumentType } from '@/types/documents';
|
||||||
|
|
||||||
export type UploadSource = {
|
export type UploadSource = {
|
||||||
|
|
@ -243,11 +243,11 @@ export async function getDocumentContentSnippet(
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
params.set('id', id);
|
params.set('id', id);
|
||||||
params.set('format', 'snippet');
|
params.set('snippet', '1');
|
||||||
if (typeof options?.maxChars === 'number') params.set('maxChars', String(options.maxChars));
|
if (typeof options?.maxChars === 'number') params.set('maxChars', String(options.maxChars));
|
||||||
if (typeof options?.maxBytes === 'number') params.set('maxBytes', String(options.maxBytes));
|
if (typeof options?.maxBytes === 'number') params.set('maxBytes', String(options.maxBytes));
|
||||||
|
|
||||||
const res = await fetch(`/api/documents/blob?${params.toString()}`, { signal: options?.signal });
|
const res = await fetch(`/api/documents/blob/preview/fallback?${params.toString()}`, { signal: options?.signal });
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const data = (await res.json().catch(() => null)) as { error?: string } | null;
|
const data = (await res.json().catch(() => null)) as { error?: string } | null;
|
||||||
throw new Error(data?.error || `Failed to load content snippet (status ${res.status})`);
|
throw new Error(data?.error || `Failed to load content snippet (status ${res.status})`);
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { BaseDocument, EPUBDocument, HTMLDocument, PDFDocument } from '@/types/documents';
|
import type { BaseDocument, EPUBDocument, HTMLDocument, PDFDocument } from '@/types/documents';
|
||||||
import { downloadDocumentContent } from '@/lib/client-documents';
|
import { downloadDocumentContent } from '@/lib/client/api/documents';
|
||||||
|
|
||||||
export type DocumentCacheBackend = {
|
export type DocumentCacheBackend = {
|
||||||
get: (meta: BaseDocument) => Promise<PDFDocument | EPUBDocument | HTMLDocument | null>;
|
get: (meta: BaseDocument) => Promise<PDFDocument | EPUBDocument | HTMLDocument | null>;
|
||||||
|
|
@ -42,12 +42,12 @@ export async function ensureCachedDocumentCore(
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCachedPdf(id: string): Promise<PDFDocument | null> {
|
export async function getCachedPdf(id: string): Promise<PDFDocument | null> {
|
||||||
const { getPdfDocument } = await import('@/lib/dexie');
|
const { getPdfDocument } = await import('@/lib/client/dexie');
|
||||||
return (await getPdfDocument(id)) ?? null;
|
return (await getPdfDocument(id)) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function putCachedPdf(meta: BaseDocument, data: ArrayBuffer): Promise<void> {
|
export async function putCachedPdf(meta: BaseDocument, data: ArrayBuffer): Promise<void> {
|
||||||
const { addPdfDocument } = await import('@/lib/dexie');
|
const { addPdfDocument } = await import('@/lib/client/dexie');
|
||||||
await addPdfDocument({
|
await addPdfDocument({
|
||||||
id: meta.id,
|
id: meta.id,
|
||||||
type: 'pdf',
|
type: 'pdf',
|
||||||
|
|
@ -59,17 +59,17 @@ export async function putCachedPdf(meta: BaseDocument, data: ArrayBuffer): Promi
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function evictCachedPdf(id: string): Promise<void> {
|
export async function evictCachedPdf(id: string): Promise<void> {
|
||||||
const { removePdfDocument } = await import('@/lib/dexie');
|
const { removePdfDocument } = await import('@/lib/client/dexie');
|
||||||
await removePdfDocument(id);
|
await removePdfDocument(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCachedEpub(id: string): Promise<EPUBDocument | null> {
|
export async function getCachedEpub(id: string): Promise<EPUBDocument | null> {
|
||||||
const { getEpubDocument } = await import('@/lib/dexie');
|
const { getEpubDocument } = await import('@/lib/client/dexie');
|
||||||
return (await getEpubDocument(id)) ?? null;
|
return (await getEpubDocument(id)) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function putCachedEpub(meta: BaseDocument, data: ArrayBuffer): Promise<void> {
|
export async function putCachedEpub(meta: BaseDocument, data: ArrayBuffer): Promise<void> {
|
||||||
const { addEpubDocument } = await import('@/lib/dexie');
|
const { addEpubDocument } = await import('@/lib/client/dexie');
|
||||||
await addEpubDocument({
|
await addEpubDocument({
|
||||||
id: meta.id,
|
id: meta.id,
|
||||||
type: 'epub',
|
type: 'epub',
|
||||||
|
|
@ -81,17 +81,17 @@ export async function putCachedEpub(meta: BaseDocument, data: ArrayBuffer): Prom
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function evictCachedEpub(id: string): Promise<void> {
|
export async function evictCachedEpub(id: string): Promise<void> {
|
||||||
const { removeEpubDocument } = await import('@/lib/dexie');
|
const { removeEpubDocument } = await import('@/lib/client/dexie');
|
||||||
await removeEpubDocument(id);
|
await removeEpubDocument(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCachedHtml(id: string): Promise<HTMLDocument | null> {
|
export async function getCachedHtml(id: string): Promise<HTMLDocument | null> {
|
||||||
const { getHtmlDocument } = await import('@/lib/dexie');
|
const { getHtmlDocument } = await import('@/lib/client/dexie');
|
||||||
return (await getHtmlDocument(id)) ?? null;
|
return (await getHtmlDocument(id)) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function putCachedHtml(meta: BaseDocument, data: string): Promise<void> {
|
export async function putCachedHtml(meta: BaseDocument, data: string): Promise<void> {
|
||||||
const { addHtmlDocument } = await import('@/lib/dexie');
|
const { addHtmlDocument } = await import('@/lib/client/dexie');
|
||||||
await addHtmlDocument({
|
await addHtmlDocument({
|
||||||
id: meta.id,
|
id: meta.id,
|
||||||
type: 'html',
|
type: 'html',
|
||||||
|
|
@ -103,12 +103,12 @@ export async function putCachedHtml(meta: BaseDocument, data: string): Promise<v
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function evictCachedHtml(id: string): Promise<void> {
|
export async function evictCachedHtml(id: string): Promise<void> {
|
||||||
const { removeHtmlDocument } = await import('@/lib/dexie');
|
const { removeHtmlDocument } = await import('@/lib/client/dexie');
|
||||||
await removeHtmlDocument(id);
|
await removeHtmlDocument(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function clearDocumentCache(): Promise<void> {
|
export async function clearDocumentCache(): Promise<void> {
|
||||||
const { clearPdfDocuments, clearEpubDocuments, clearHtmlDocuments } = await import('@/lib/dexie');
|
const { clearPdfDocuments, clearEpubDocuments, clearHtmlDocuments } = await import('@/lib/client/dexie');
|
||||||
await Promise.all([clearPdfDocuments(), clearEpubDocuments(), clearHtmlDocuments()]);
|
await Promise.all([clearPdfDocuments(), clearEpubDocuments(), clearHtmlDocuments()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -132,7 +132,7 @@ export async function ensureCachedDocument(meta: BaseDocument, options?: { signa
|
||||||
meta,
|
meta,
|
||||||
{
|
{
|
||||||
get: async (m) => {
|
get: async (m) => {
|
||||||
const { getPdfDocument, getEpubDocument, getHtmlDocument } = await import('@/lib/dexie');
|
const { getPdfDocument, getEpubDocument, getHtmlDocument } = await import('@/lib/client/dexie');
|
||||||
if (m.type === 'pdf') return (await getPdfDocument(m.id)) ?? null;
|
if (m.type === 'pdf') return (await getPdfDocument(m.id)) ?? null;
|
||||||
if (m.type === 'epub') return (await getEpubDocument(m.id)) ?? null;
|
if (m.type === 'epub') return (await getEpubDocument(m.id)) ?? null;
|
||||||
return (await getHtmlDocument(m.id)) ?? null;
|
return (await getHtmlDocument(m.id)) ?? null;
|
||||||
|
|
@ -3,8 +3,8 @@ import {
|
||||||
getDocumentPreviewCache,
|
getDocumentPreviewCache,
|
||||||
putDocumentPreviewCache,
|
putDocumentPreviewCache,
|
||||||
removeDocumentPreviewCache,
|
removeDocumentPreviewCache,
|
||||||
} from '@/lib/dexie';
|
} from '@/lib/client/dexie';
|
||||||
import { documentPreviewFallbackUrl, documentPreviewPresignUrl } from '@/lib/client-documents';
|
import { documentPreviewFallbackUrl, documentPreviewPresignUrl } from '@/lib/client/api/documents';
|
||||||
|
|
||||||
const inMemoryPreviewUrlCache = new Map<string, string>();
|
const inMemoryPreviewUrlCache = new Map<string, string>();
|
||||||
const inFlightPreviewPrime = new Map<string, Promise<string | null>>();
|
const inFlightPreviewPrime = new Map<string, Promise<string | null>>();
|
||||||
|
|
@ -8,9 +8,9 @@ import {
|
||||||
BaseDocument,
|
BaseDocument,
|
||||||
DocumentListDocument,
|
DocumentListDocument,
|
||||||
} from '@/types/documents';
|
} from '@/types/documents';
|
||||||
import { sha256HexFromBytes, sha256HexFromString } from '@/lib/sha256';
|
import { sha256HexFromBytes, sha256HexFromString } from '@/lib/client/sha256';
|
||||||
import { downloadDocumentContent, listDocuments, uploadDocumentSources, type UploadSource } from '@/lib/client-documents';
|
import { downloadDocumentContent, listDocuments, uploadDocumentSources, type UploadSource } from '@/lib/client/api/documents';
|
||||||
import { cacheStoredDocumentFromBytes } from '@/lib/document-cache';
|
import { cacheStoredDocumentFromBytes } from '@/lib/client/cache/documents';
|
||||||
|
|
||||||
const DB_NAME = 'openreader-db';
|
const DB_NAME = 'openreader-db';
|
||||||
// Managed via Dexie (version bumped from the original manual IndexedDB)
|
// Managed via Dexie (version bumped from the original manual IndexedDB)
|
||||||
|
|
@ -32,7 +32,7 @@ function getHighlightWorker(): Worker | null {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
highlightWorker = new Worker(
|
highlightWorker = new Worker(
|
||||||
new URL('pdfHighlightWorker.ts', import.meta.url),
|
new URL('pdf-highlight-worker.ts', import.meta.url),
|
||||||
{ type: 'module' }
|
{ type: 'module' }
|
||||||
);
|
);
|
||||||
return highlightWorker;
|
return highlightWorker;
|
||||||
|
|
@ -1,150 +0,0 @@
|
||||||
import { pdfjs } from 'react-pdf';
|
|
||||||
import ePub from 'epubjs';
|
|
||||||
export { extractRawTextSnippet, extractTextSnippet } from '@/lib/text-snippets';
|
|
||||||
|
|
||||||
function shouldUseLegacyPdfWorker(): boolean {
|
|
||||||
if (typeof window === 'undefined') return false;
|
|
||||||
const ua = window.navigator.userAgent;
|
|
||||||
const isSafari = /^((?!chrome|android).)*safari/i.test(ua);
|
|
||||||
if (!isSafari) return false;
|
|
||||||
const match = ua.match(/Version\/(\d+)/i);
|
|
||||||
if (!match?.[1]) return true;
|
|
||||||
const version = Number.parseInt(match[1], 10);
|
|
||||||
return Number.isFinite(version) ? version < 18 : true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ensurePdfWorker(): void {
|
|
||||||
if (typeof window === 'undefined') return;
|
|
||||||
if (pdfjs.GlobalWorkerOptions.workerSrc) return;
|
|
||||||
|
|
||||||
const useLegacy = shouldUseLegacyPdfWorker();
|
|
||||||
const workerSrc = useLegacy
|
|
||||||
? new URL('pdfjs-dist/legacy/build/pdf.worker.min.mjs', import.meta.url).href
|
|
||||||
: new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).href;
|
|
||||||
|
|
||||||
pdfjs.GlobalWorkerOptions.workerSrc = workerSrc;
|
|
||||||
pdfjs.GlobalWorkerOptions.workerPort = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function blobToDataUrl(blob: Blob): Promise<string> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onerror = () => reject(new Error('Failed to read blob'));
|
|
||||||
reader.onload = () => resolve(String(reader.result));
|
|
||||||
reader.readAsDataURL(blob);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function scaleImageBlobToDataUrl(blob: Blob, targetWidth: number): Promise<string> {
|
|
||||||
if (typeof window === 'undefined') {
|
|
||||||
return blobToDataUrl(blob);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof createImageBitmap !== 'function') {
|
|
||||||
return blobToDataUrl(blob);
|
|
||||||
}
|
|
||||||
|
|
||||||
const bitmap = await createImageBitmap(blob);
|
|
||||||
try {
|
|
||||||
const scale = targetWidth > 0 ? targetWidth / bitmap.width : 1;
|
|
||||||
const width = Math.max(1, Math.round(bitmap.width * scale));
|
|
||||||
const height = Math.max(1, Math.round(bitmap.height * scale));
|
|
||||||
|
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
canvas.width = width;
|
|
||||||
canvas.height = height;
|
|
||||||
const ctx = canvas.getContext('2d', { alpha: false });
|
|
||||||
if (!ctx) {
|
|
||||||
return blobToDataUrl(blob);
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.fillStyle = '#ffffff';
|
|
||||||
ctx.fillRect(0, 0, width, height);
|
|
||||||
ctx.drawImage(bitmap, 0, 0, width, height);
|
|
||||||
|
|
||||||
const dataUrl = canvas.toDataURL('image/jpeg', 0.85);
|
|
||||||
return dataUrl;
|
|
||||||
} finally {
|
|
||||||
bitmap.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function renderPdfFirstPageToDataUrl(
|
|
||||||
data: ArrayBuffer,
|
|
||||||
targetWidth: number,
|
|
||||||
): Promise<string> {
|
|
||||||
if (typeof window === 'undefined') {
|
|
||||||
throw new Error('PDF thumbnail rendering must run in the browser');
|
|
||||||
}
|
|
||||||
|
|
||||||
ensurePdfWorker();
|
|
||||||
|
|
||||||
const loadingTask = pdfjs.getDocument({ data });
|
|
||||||
const pdf = await loadingTask.promise;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const page = await pdf.getPage(1);
|
|
||||||
const viewport = page.getViewport({ scale: 1 });
|
|
||||||
const scale = targetWidth > 0 ? targetWidth / viewport.width : 1;
|
|
||||||
const scaledViewport = page.getViewport({ scale });
|
|
||||||
|
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
canvas.width = Math.max(1, Math.floor(scaledViewport.width));
|
|
||||||
canvas.height = Math.max(1, Math.floor(scaledViewport.height));
|
|
||||||
const ctx = canvas.getContext('2d', { alpha: false });
|
|
||||||
if (!ctx) {
|
|
||||||
throw new Error('Failed to create canvas context');
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.fillStyle = '#ffffff';
|
|
||||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
||||||
|
|
||||||
const renderTask = page.render({
|
|
||||||
canvasContext: ctx,
|
|
||||||
viewport: scaledViewport,
|
|
||||||
intent: 'display',
|
|
||||||
});
|
|
||||||
await renderTask.promise;
|
|
||||||
|
|
||||||
return canvas.toDataURL('image/jpeg', 0.82);
|
|
||||||
} finally {
|
|
||||||
await pdf.destroy().catch(() => undefined);
|
|
||||||
await loadingTask.destroy().catch(() => undefined);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function extractEpubCoverToDataUrl(
|
|
||||||
data: ArrayBuffer,
|
|
||||||
targetWidth: number,
|
|
||||||
): Promise<string | null> {
|
|
||||||
if (typeof window === 'undefined') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const book = ePub(data);
|
|
||||||
const opened = book.opened.catch(() => undefined);
|
|
||||||
try {
|
|
||||||
const coverObjectUrl = await book.coverUrl();
|
|
||||||
if (!coverObjectUrl) return null;
|
|
||||||
|
|
||||||
const res = await fetch(coverObjectUrl);
|
|
||||||
const blob = await res.blob();
|
|
||||||
if (coverObjectUrl.startsWith('blob:')) {
|
|
||||||
URL.revokeObjectURL(coverObjectUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await scaleImageBlobToDataUrl(blob, targetWidth);
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
} finally {
|
|
||||||
void opened.finally(() => {
|
|
||||||
try {
|
|
||||||
book.destroy();
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: text snippet helpers live in `src/lib/text-snippets.ts` to stay server-safe.
|
|
||||||
|
|
@ -6,7 +6,7 @@ import {
|
||||||
ListObjectsV2Command,
|
ListObjectsV2Command,
|
||||||
PutObjectCommand,
|
PutObjectCommand,
|
||||||
} from '@aws-sdk/client-s3';
|
} from '@aws-sdk/client-s3';
|
||||||
import { getS3Client, getS3Config } from '@/lib/server/s3';
|
import { getS3Client, getS3Config } from '@/lib/server/storage/s3';
|
||||||
|
|
||||||
const SAFE_NAMESPACE_REGEX = /^[a-zA-Z0-9._-]{1,128}$/;
|
const SAFE_NAMESPACE_REGEX = /^[a-zA-Z0-9._-]{1,128}$/;
|
||||||
const SAFE_BOOK_ID_REGEX = /^[a-zA-Z0-9._-]{1,128}$/;
|
const SAFE_BOOK_ID_REGEX = /^[a-zA-Z0-9._-]{1,128}$/;
|
||||||
|
|
@ -102,7 +102,7 @@ function parseTitleFromFFMetadata(stdout: string): string | undefined {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function ffprobeAudio(filePath: string, signal?: AbortSignal): Promise<ProbeResult> {
|
export async function ffprobeAudio(filePath: string, signal?: AbortSignal): Promise<ProbeResult> {
|
||||||
const { getFFmpegPath } = await import('@/lib/server/ffmpeg-bin');
|
const { getFFmpegPath } = await import('@/lib/server/audiobooks/ffmpeg-bin');
|
||||||
|
|
||||||
return new Promise<ProbeResult>((resolve, reject) => {
|
return new Promise<ProbeResult>((resolve, reject) => {
|
||||||
const ffmpeg = spawn(getFFmpegPath(), [
|
const ffmpeg = spawn(getFFmpegPath(), [
|
||||||
|
|
@ -5,7 +5,7 @@ import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import type { NextRequest } from 'next/server';
|
import type { NextRequest } from 'next/server';
|
||||||
import { db } from "@/db";
|
import { db } from "@/db";
|
||||||
import { isAuthEnabled, isAnonymousAuthSessionsEnabled } from "@/lib/server/auth-config";
|
import { isAuthEnabled, isAnonymousAuthSessionsEnabled } from "@/lib/server/auth/config";
|
||||||
import * as authSchemaSqlite from "@/db/schema_auth_sqlite";
|
import * as authSchemaSqlite from "@/db/schema_auth_sqlite";
|
||||||
import * as authSchemaPostgres from "@/db/schema_auth_postgres";
|
import * as authSchemaPostgres from "@/db/schema_auth_postgres";
|
||||||
|
|
||||||
|
|
@ -76,7 +76,7 @@ const createAuth = () => betterAuth({
|
||||||
enabled: true,
|
enabled: true,
|
||||||
beforeDelete: async (user) => {
|
beforeDelete: async (user) => {
|
||||||
try {
|
try {
|
||||||
const { deleteUserStorageData } = await import('@/lib/server/user-data-cleanup');
|
const { deleteUserStorageData } = await import('@/lib/server/user/data-cleanup');
|
||||||
await deleteUserStorageData(user.id, null);
|
await deleteUserStorageData(user.id, null);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[auth] Failed to clean up user storage before deletion:', error);
|
console.error('[auth] Failed to clean up user storage before deletion:', error);
|
||||||
|
|
@ -122,8 +122,8 @@ const createAuth = () => betterAuth({
|
||||||
|
|
||||||
// Lazy-load heavy modules only when account linking actually happens
|
// Lazy-load heavy modules only when account linking actually happens
|
||||||
const [{ rateLimiter }, claimData] = await Promise.all([
|
const [{ rateLimiter }, claimData] = await Promise.all([
|
||||||
import('@/lib/server/rate-limiter'),
|
import('@/lib/server/rate-limit/rate-limiter'),
|
||||||
import('@/lib/server/claim-data'),
|
import('@/lib/server/user/claim-data'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Transfer rate limiting data (TTS char counts) from anonymous user to authenticated user
|
// Transfer rate limiting data (TTS char counts) from anonymous user to authenticated user
|
||||||
|
|
@ -7,7 +7,7 @@ import {
|
||||||
PutObjectCommand,
|
PutObjectCommand,
|
||||||
} from '@aws-sdk/client-s3';
|
} from '@aws-sdk/client-s3';
|
||||||
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
||||||
import { getS3Client, getS3Config } from '@/lib/server/s3';
|
import { getS3Client, getS3Config } from '@/lib/server/storage/s3';
|
||||||
|
|
||||||
const DOCUMENT_ID_REGEX = /^[a-f0-9]{64}$/i;
|
const DOCUMENT_ID_REGEX = /^[a-f0-9]{64}$/i;
|
||||||
const SAFE_NAMESPACE_REGEX = /^[a-zA-Z0-9._-]{1,128}$/;
|
const SAFE_NAMESPACE_REGEX = /^[a-zA-Z0-9._-]{1,128}$/;
|
||||||
|
|
@ -4,8 +4,8 @@ import {
|
||||||
PutObjectCommand,
|
PutObjectCommand,
|
||||||
} from '@aws-sdk/client-s3';
|
} from '@aws-sdk/client-s3';
|
||||||
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
||||||
import { deleteDocumentPrefix, isMissingBlobError, isValidDocumentId } from '@/lib/server/documents-blobstore';
|
import { deleteDocumentPrefix, isMissingBlobError, isValidDocumentId } from '@/lib/server/documents/blobstore';
|
||||||
import { getS3Client, getS3Config } from '@/lib/server/s3';
|
import { getS3Client, getS3Config } from '@/lib/server/storage/s3';
|
||||||
|
|
||||||
const SAFE_NAMESPACE_REGEX = /^[a-zA-Z0-9._-]{1,128}$/;
|
const SAFE_NAMESPACE_REGEX = /^[a-zA-Z0-9._-]{1,128}$/;
|
||||||
const DEFAULT_NAMESPACE_SEGMENT = '_default';
|
const DEFAULT_NAMESPACE_SEGMENT = '_default';
|
||||||
|
|
@ -14,9 +14,9 @@ import {
|
||||||
headDocumentPreview,
|
headDocumentPreview,
|
||||||
isMissingBlobError,
|
isMissingBlobError,
|
||||||
putDocumentPreviewBuffer,
|
putDocumentPreviewBuffer,
|
||||||
} from '@/lib/server/document-previews-blobstore';
|
} from '@/lib/server/documents/previews-blobstore';
|
||||||
import { getDocumentBlob } from '@/lib/server/documents-blobstore';
|
import { getDocumentBlob } from '@/lib/server/documents/blobstore';
|
||||||
import { renderEpubCoverToJpeg, renderPdfFirstPageToJpeg } from '@/lib/server/document-previews-render';
|
import { renderEpubCoverToJpeg, renderPdfFirstPageToJpeg } from '@/lib/server/documents/previews-render';
|
||||||
|
|
||||||
const LEASE_MS = 45_000;
|
const LEASE_MS = 45_000;
|
||||||
const RETRY_AFTER_MS = 1_500;
|
const RETRY_AFTER_MS = 1_500;
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { db } from '@/db';
|
import { db } from '@/db';
|
||||||
import { userTtsChars } from '@/db/schema';
|
import { userTtsChars } from '@/db/schema';
|
||||||
import { isAuthEnabled } from '@/lib/server/auth-config';
|
import { isAuthEnabled } from '@/lib/server/auth/config';
|
||||||
import { eq, and, lt, sql } from 'drizzle-orm';
|
import { eq, and, lt, sql } from 'drizzle-orm';
|
||||||
|
|
||||||
function readPositiveIntEnv(name: string, fallback: number): number {
|
function readPositiveIntEnv(name: string, fallback: number): number {
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { UNCLAIMED_USER_ID } from '@/lib/server/docstore';
|
import { UNCLAIMED_USER_ID } from '@/lib/server/storage/docstore-legacy';
|
||||||
import { ensureSystemUserExists } from '@/db';
|
import { ensureSystemUserExists } from '@/db';
|
||||||
|
|
||||||
const TEST_NAMESPACE_HEADER = 'x-openreader-test-namespace';
|
const TEST_NAMESPACE_HEADER = 'x-openreader-test-namespace';
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
import { db } from '@/db';
|
import { db } from '@/db';
|
||||||
import { documents, audiobooks, audiobookChapters, userPreferences, userDocumentProgress } from '@/db/schema';
|
import { documents, audiobooks, audiobookChapters, userPreferences, userDocumentProgress } from '@/db/schema';
|
||||||
import { eq, and, inArray } from 'drizzle-orm';
|
import { eq, and, inArray } from 'drizzle-orm';
|
||||||
import { UNCLAIMED_USER_ID } from './docstore';
|
import { UNCLAIMED_USER_ID } from '../storage/docstore-legacy';
|
||||||
import {
|
import {
|
||||||
deleteAudiobookObject,
|
deleteAudiobookObject,
|
||||||
getAudiobookObjectBuffer,
|
getAudiobookObjectBuffer,
|
||||||
listAudiobookObjects,
|
listAudiobookObjects,
|
||||||
putAudiobookObject,
|
putAudiobookObject,
|
||||||
} from './audiobooks-blobstore';
|
} from '../audiobooks/blobstore';
|
||||||
import { isS3Configured } from './s3';
|
import { isS3Configured } from '../storage/s3';
|
||||||
|
|
||||||
import { isAuthEnabled } from '@/lib/server/auth-config';
|
import { isAuthEnabled } from '@/lib/server/auth/config';
|
||||||
|
|
||||||
type AudiobookRow = {
|
type AudiobookRow = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -7,10 +7,10 @@
|
||||||
import { db } from '@/db';
|
import { db } from '@/db';
|
||||||
import { documents, audiobooks } from '@/db/schema';
|
import { documents, audiobooks } from '@/db/schema';
|
||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
import { isS3Configured } from '@/lib/server/s3';
|
import { isS3Configured } from '@/lib/server/storage/s3';
|
||||||
import { deleteDocumentBlob } from '@/lib/server/documents-blobstore';
|
import { deleteDocumentBlob } from '@/lib/server/documents/blobstore';
|
||||||
import { deleteDocumentPreviewArtifacts } from '@/lib/server/document-previews-blobstore';
|
import { deleteDocumentPreviewArtifacts } from '@/lib/server/documents/previews-blobstore';
|
||||||
import { audiobookPrefix, deleteAudiobookPrefix } from '@/lib/server/audiobooks-blobstore';
|
import { audiobookPrefix, deleteAudiobookPrefix } from '@/lib/server/audiobooks/blobstore';
|
||||||
|
|
||||||
type DocumentRow = { id: string };
|
type DocumentRow = { id: string };
|
||||||
type AudiobookRow = { id: string };
|
type AudiobookRow = { id: string };
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import type { Archiver } from 'archiver';
|
import type { Archiver } from 'archiver';
|
||||||
import { Readable } from 'stream';
|
import { Readable } from 'stream';
|
||||||
import type { ReadableStream as NodeReadableStream } from 'stream/web';
|
import type { ReadableStream as NodeReadableStream } from 'stream/web';
|
||||||
import { decodeChapterFileName } from '@/lib/server/audiobook';
|
import { decodeChapterFileName } from '@/lib/server/audiobooks/chapters';
|
||||||
|
|
||||||
export type ExportBlobBody =
|
export type ExportBlobBody =
|
||||||
| NodeJS.ReadableStream
|
| NodeJS.ReadableStream
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import type { NextRequest } from 'next/server';
|
import type { NextRequest } from 'next/server';
|
||||||
import type { AuthContext } from '@/lib/server/auth';
|
import type { AuthContext } from '@/lib/server/auth/auth';
|
||||||
import { requireAuthContext } from '@/lib/server/auth';
|
import { requireAuthContext } from '@/lib/server/auth/auth';
|
||||||
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/test-namespace';
|
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/testing/test-namespace';
|
||||||
|
|
||||||
export type ResolvedUserStateScope = {
|
export type ResolvedUserStateScope = {
|
||||||
auth: AuthContext;
|
auth: AuthContext;
|
||||||
|
|
@ -2,9 +2,9 @@ import { ListObjectsV2Command } from '@aws-sdk/client-s3';
|
||||||
import { and, eq, inArray, like } from 'drizzle-orm';
|
import { and, eq, inArray, like } from 'drizzle-orm';
|
||||||
import { db } from '../src/db';
|
import { db } from '../src/db';
|
||||||
import { audiobooks, audiobookChapters, documents } from '../src/db/schema';
|
import { audiobooks, audiobookChapters, documents } from '../src/db/schema';
|
||||||
import { deleteDocumentPrefix } from '../src/lib/server/documents-blobstore';
|
import { deleteDocumentPrefix } from '../src/lib/server/documents/blobstore';
|
||||||
import { deleteAudiobookPrefix } from '../src/lib/server/audiobooks-blobstore';
|
import { deleteAudiobookPrefix } from '../src/lib/server/audiobooks/blobstore';
|
||||||
import { getS3Client, getS3Config, isS3Configured } from '../src/lib/server/s3';
|
import { getS3Client, getS3Config, isS3Configured } from '../src/lib/server/storage/s3';
|
||||||
|
|
||||||
function chunk<T>(items: T[], size: number): T[][] {
|
function chunk<T>(items: T[], size: number): T[][] {
|
||||||
if (items.length === 0) return [];
|
if (items.length === 0) return [];
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue