mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 08:21:39 -06:00
fix(kobo): fix device controller and service errors
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1771265803603,
|
||||
"when": 1771266812483,
|
||||
"tag": "0000_add_kobo_sync",
|
||||
"breakpoints": true
|
||||
}
|
||||
|
||||
+21
-1
@@ -5,6 +5,7 @@ import { IoAdapter } from '@nestjs/platform-socket.io';
|
||||
import { AppModule } from './app.module';
|
||||
import { GlobalExceptionFilter } from './common/filters/http-exception.filter';
|
||||
import { join } from 'path';
|
||||
import { Readable } from 'stream';
|
||||
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||
import { migrate } from 'drizzle-orm/node-postgres/migrator';
|
||||
import { Pool } from 'pg';
|
||||
@@ -16,7 +17,26 @@ async function bootstrap() {
|
||||
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
||||
await migrate(drizzle(pool), { migrationsFolder: join(__dirname, '..', 'src', 'db', 'migrations') });
|
||||
await pool.end();
|
||||
const app = await NestFactory.create<NestFastifyApplication>(AppModule, new FastifyAdapter({ logger: true }));
|
||||
|
||||
const adapter = new FastifyAdapter({ logger: true });
|
||||
const app = await NestFactory.create<NestFastifyApplication>(AppModule, adapter);
|
||||
|
||||
// Kobo devices send Content-Type: application/json with empty bodies on GET/DELETE.
|
||||
// Fastify's default JSON parser rejects empty bodies, so we inject '{}' before parsing.
|
||||
const fastify = adapter.getInstance();
|
||||
fastify.addHook('preParsing', (request, _reply, payload, done) => {
|
||||
const ct = request.headers['content-type'] ?? '';
|
||||
const isJson = ct.startsWith('application/json');
|
||||
const isEmpty = request.headers['content-length'] === '0' || request.headers['content-length'] === undefined;
|
||||
if (isJson && isEmpty) {
|
||||
const fake = new Readable();
|
||||
fake.push('{}');
|
||||
fake.push(null);
|
||||
done(null, fake);
|
||||
return;
|
||||
}
|
||||
done(null, payload);
|
||||
});
|
||||
|
||||
app.useWebSocketAdapter(new IoAdapter(app));
|
||||
|
||||
|
||||
@@ -14,6 +14,24 @@ import { KoboReadingStateService } from './services/kobo-reading-state.service';
|
||||
import { KoboThumbnailService } from './services/kobo-thumbnail.service';
|
||||
import { KoboDownloadService } from './services/kobo-download.service';
|
||||
import { KoboProxyService } from './services/kobo-proxy.service';
|
||||
import { KOBO_STORE_RESOURCES } from './kobo-store-resources';
|
||||
|
||||
function buildBaseUrl(req: FastifyRequest): string {
|
||||
const hasForwarded = req.headers['x-forwarded-host'] || req.headers['x-forwarded-port'] || req.headers['x-forwarded-proto'];
|
||||
const proto = (req.headers['x-forwarded-proto'] as string | undefined) ?? req.protocol;
|
||||
const headerHost = req.headers['x-forwarded-host'] ?? req.headers.host;
|
||||
let host = headerHost ? (Array.isArray(headerHost) ? headerHost[0] : headerHost) : req.hostname;
|
||||
|
||||
if (!hasForwarded && !host.includes(':')) {
|
||||
const localPort = req.socket?.localPort;
|
||||
const isDefault = (proto === 'http' && localPort === 80) || (proto === 'https' && localPort === 443);
|
||||
if (localPort && !isDefault) {
|
||||
host = host + ':' + String(localPort);
|
||||
}
|
||||
}
|
||||
|
||||
return proto + '://' + host;
|
||||
}
|
||||
|
||||
@Controller('kobo/:deviceToken')
|
||||
@Public()
|
||||
@@ -40,29 +58,30 @@ export class KoboDeviceController {
|
||||
};
|
||||
}
|
||||
|
||||
@Post('v1/auth/refresh')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
authRefresh(@Body() body: Record<string, unknown>) {
|
||||
return {
|
||||
AccessToken: randomUUID(),
|
||||
RefreshToken: body.RefreshToken ?? randomUUID(),
|
||||
TokenType: 'Bearer',
|
||||
TrackingId: randomUUID(),
|
||||
};
|
||||
}
|
||||
|
||||
@Get('v1/initialization')
|
||||
@Header('x-kobo-apitoken', 'e30=')
|
||||
initialization(@KoboDevice() device: KoboDeviceContext, @Req() req: FastifyRequest) {
|
||||
const baseUrl = `${req.protocol}://${req.hostname}`;
|
||||
const baseUrl = buildBaseUrl(req);
|
||||
const t = device.deviceToken;
|
||||
return {
|
||||
Resources: {
|
||||
image_host: baseUrl,
|
||||
image_url_template: `/api/kobo/${t}/v1/books/{ImageId}/thumbnail/{Width}/{Height}/false/image.jpg`,
|
||||
image_url_quality_template: `/api/kobo/${t}/v1/books/{ImageId}/thumbnail/{Width}/{Height}/{Quality}/false/image.jpg`,
|
||||
library_sync: `/api/kobo/${t}/v1/library/sync`,
|
||||
library_items: `/api/kobo/${t}/v1/library/{RevisionId}`,
|
||||
library_metadata: `/api/kobo/${t}/v1/library/{RevisionId}/metadata`,
|
||||
library_prices: `/api/kobo/${t}/v1/library/{RevisionId}/prices`,
|
||||
library_book_get: `/api/kobo/${t}/v1/library/{RevisionId}`,
|
||||
sign_in_page: `${baseUrl}/api/kobo/${t}/v1/signin`,
|
||||
userguide_host: `${baseUrl}/api/kobo/${t}`,
|
||||
user_loyalty_benefits: `/api/kobo/${t}/v1/user/loyalty/benefits`,
|
||||
giftcard_epd_redeem_url: `${baseUrl}/api/kobo/${t}/v1/giftcard/redeem`,
|
||||
get_tests_endpoint: `/api/kobo/${t}/v1/analytics/gettests`,
|
||||
},
|
||||
BookEntitlementsUrl: `/api/kobo/${t}/v1/library/sync`,
|
||||
const resources = {
|
||||
...KOBO_STORE_RESOURCES,
|
||||
image_host: baseUrl,
|
||||
image_url_template: `${baseUrl}/api/kobo/${t}/v1/books/{ImageId}/thumbnail/{Width}/{Height}/false/image.jpg`,
|
||||
image_url_quality_template: `${baseUrl}/api/kobo/${t}/v1/books/{ImageId}/thumbnail/{Width}/{Height}/{Quality}/{IsGreyscale}/image.jpg`,
|
||||
library_sync: `${baseUrl}/api/kobo/${t}/v1/library/sync`,
|
||||
};
|
||||
return { Resources: resources };
|
||||
}
|
||||
|
||||
@Post('v1/analytics/gettests')
|
||||
@@ -85,7 +104,7 @@ export class KoboDeviceController {
|
||||
@Req() req: FastifyRequest,
|
||||
@Res() reply: FastifyReply,
|
||||
) {
|
||||
const baseUrl = `${req.protocol}://${req.hostname}`;
|
||||
const baseUrl = buildBaseUrl(req);
|
||||
const settings = await this.settingsService.getSettings(user.id);
|
||||
const { entitlements, hasMore, syncToken } = await this.syncService.getDelta(
|
||||
user.id,
|
||||
@@ -110,7 +129,7 @@ export class KoboDeviceController {
|
||||
) {
|
||||
const id = parseInt(bookId, 10);
|
||||
if (isNaN(id)) return this.proxyService.forward(req, reply, device.deviceToken);
|
||||
const baseUrl = `${req.protocol}://${req.hostname}`;
|
||||
const baseUrl = buildBaseUrl(req);
|
||||
const metadata = await this.syncService.getBookMetadata(user.id, id, device.deviceToken, baseUrl);
|
||||
reply.send(metadata);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
// prettier-ignore
|
||||
export const KOBO_STORE_RESOURCES: Record<string, unknown> = {
|
||||
account_page: 'https://www.kobo.com/account/settings',
|
||||
account_page_rakuten: 'https://my.rakuten.co.jp/',
|
||||
add_device: 'https://storeapi.kobo.com/v1/user/add-device',
|
||||
add_entitlement: 'https://storeapi.kobo.com/v1/library/{RevisionIds}',
|
||||
affiliaterequest: 'https://storeapi.kobo.com/v1/affiliate',
|
||||
assets: 'https://storeapi.kobo.com/v1/assets',
|
||||
audiobook: 'https://storeapi.kobo.com/v1/products/audiobooks/{ProductId}',
|
||||
audiobook_detail_page: 'https://www.kobo.com/{region}/{language}/audiobook/{slug}',
|
||||
audiobook_get_credits: 'https://www.kobo.com/{region}/{language}/audiobooks/plans',
|
||||
audiobook_landing_page: 'https://www.kobo.com/{region}/{language}/audiobooks',
|
||||
audiobook_preview: 'https://storeapi.kobo.com/v1/products/audiobooks/{Id}/preview',
|
||||
audiobook_purchase_withcredit: 'https://storeapi.kobo.com/v1/store/audiobook/{Id}',
|
||||
audiobook_subscription_management: 'https://www.kobo.com/{region}/{language}/account/subscriptions',
|
||||
audiobook_subscription_orange_deal_inclusion_url: 'https://authorize.kobo.com/inclusion',
|
||||
audiobook_subscription_purchase: 'https://www.kobo.com/{region}/{language}/checkoutoption/21C6D938-934B-4A91-B979-E14D70B2F280',
|
||||
audiobook_subscription_tiers: 'https://www.kobo.com/{region}/{language}/checkoutoption/21C6D938-934B-4A91-B979-E14D70B2F280',
|
||||
authorproduct_recommendations: 'https://storeapi.kobo.com/v1/products/books/authors/recommendations',
|
||||
autocomplete: 'https://storeapi.kobo.com/v1/products/autocomplete',
|
||||
blackstone_header: { key: 'x-amz-request-payer', value: 'requester' },
|
||||
book: 'https://storeapi.kobo.com/v1/products/books/{ProductId}',
|
||||
book_detail_page: 'https://www.kobo.com/{region}/{language}/ebook/{slug}',
|
||||
book_detail_page_rakuten: 'https://books.rakuten.co.jp/rk/{crossrevisionid}',
|
||||
book_landing_page: 'https://www.kobo.com/ebooks',
|
||||
book_subscription: 'https://storeapi.kobo.com/v1/products/books/subscriptions',
|
||||
browse_history: 'https://storeapi.kobo.com/v1/user/browsehistory',
|
||||
categories: 'https://storeapi.kobo.com/v1/categories',
|
||||
categories_page: 'https://www.kobo.com/ebooks/categories',
|
||||
category: 'https://storeapi.kobo.com/v1/categories/{CategoryId}',
|
||||
category_featured_lists: 'https://storeapi.kobo.com/v1/categories/{CategoryId}/featured',
|
||||
category_products: 'https://storeapi.kobo.com/v1/categories/{CategoryId}/products',
|
||||
checkout_borrowed_book: 'https://storeapi.kobo.com/v1/library/borrow',
|
||||
client_authd_referral: 'https://authorize.kobo.com/api/AuthenticatedReferral/client/v1/getLink',
|
||||
configuration_data: 'https://storeapi.kobo.com/v1/configuration',
|
||||
content_access_book: 'https://storeapi.kobo.com/v1/products/books/{ProductId}/access',
|
||||
customer_care_live_chat: 'https://v2.zopim.com/widget/livechat.html?key=Y6gwUmnu4OATxN3Tli4Av9bYN319BTdO',
|
||||
daily_deal: 'https://storeapi.kobo.com/v1/products/dailydeal',
|
||||
deals: 'https://storeapi.kobo.com/v1/deals',
|
||||
delete_entitlement: 'https://storeapi.kobo.com/v1/library/{Ids}',
|
||||
delete_tag: 'https://storeapi.kobo.com/v1/library/tags/{TagId}',
|
||||
delete_tag_items: 'https://storeapi.kobo.com/v1/library/tags/{TagId}/items/delete',
|
||||
device_auth: 'https://storeapi.kobo.com/v1/auth/device',
|
||||
device_refresh: 'https://storeapi.kobo.com/v1/auth/refresh',
|
||||
dictionary_host: 'https://ereaderfiles.kobo.com',
|
||||
discovery_host: 'https://discovery.kobobooks.com',
|
||||
dropbox_link_account_poll: 'https://authorize.kobo.com/{region}/{language}/LinkDropbox',
|
||||
dropbox_link_account_start: 'https://authorize.kobo.com/LinkDropbox/start',
|
||||
eula_page: 'https://www.kobo.com/termsofuse?style=onestore',
|
||||
ereaderdevices: 'https://storeapi.kobo.com/v2/products/EReaderDeviceFeeds',
|
||||
exchange_auth: 'https://storeapi.kobo.com/v1/auth/exchange',
|
||||
external_book: 'https://storeapi.kobo.com/v1/products/books/external/{Ids}',
|
||||
facebook_sso_page: 'https://authorize.kobo.com/signin/provider/Facebook/login?returnUrl=http://kobo.com/',
|
||||
featured_list: 'https://storeapi.kobo.com/v1/products/featured/{FeaturedListId}',
|
||||
featured_lists: 'https://storeapi.kobo.com/v1/products/featured',
|
||||
free_books_page: {
|
||||
EN: 'https://www.kobo.com/{region}/{language}/p/free-ebooks',
|
||||
FR: 'https://www.kobo.com/{region}/{language}/p/livres-gratuits',
|
||||
IT: 'https://www.kobo.com/{region}/{language}/p/libri-gratuiti',
|
||||
NL: 'https://www.kobo.com/{region}/{language}/List/bekijk-het-overzicht-van-gratis-ebooks/QpkkVWnUw8sxmgjSlCbJRg',
|
||||
PT: 'https://www.kobo.com/{region}/{language}/p/livros-gratis',
|
||||
},
|
||||
fte_feedback: 'https://storeapi.kobo.com/v1/products/ftefeedback',
|
||||
funnel_metrics: 'https://storeapi.kobo.com/v1/funnelmetrics',
|
||||
get_download_keys: 'https://storeapi.kobo.com/v1/library/downloadkeys',
|
||||
get_download_link: 'https://storeapi.kobo.com/v1/library/downloadlink',
|
||||
get_tests_request: 'https://storeapi.kobo.com/v1/analytics/gettests',
|
||||
giftcard_epd_redeem_url: 'https://www.kobo.com/{storefront}/{language}/redeem-ereader',
|
||||
giftcard_redeem_url: 'https://www.kobo.com/{storefront}/{language}/redeem',
|
||||
googledrive_link_account_start: 'https://authorize.kobo.com/{region}/{language}/linkcloudstorage/provider/google_drive',
|
||||
gpb_flow_enabled: 'False',
|
||||
help_page: 'https://www.kobo.com/help',
|
||||
image_host: 'https://cdn.kobo.com/book-images/',
|
||||
image_url_quality_template: 'https://cdn.kobo.com/book-images/{ImageId}/{Width}/{Height}/{Quality}/{IsGreyscale}/image.jpg',
|
||||
image_url_template: 'https://cdn.kobo.com/book-images/{ImageId}/{Width}/{Height}/false/image.jpg',
|
||||
instapaper_enabled: 'True',
|
||||
instapaper_env_url: 'https://www.instapaper.com/api/kobo',
|
||||
instapaper_link_account_start: 'https://authorize.kobo.com/{region}/{language}/linkinstapaper',
|
||||
kobo_audiobooks_credit_redemption: 'True',
|
||||
kobo_audiobooks_enabled: 'True',
|
||||
kobo_audiobooks_orange_deal_enabled: 'True',
|
||||
kobo_audiobooks_subscriptions_enabled: 'True',
|
||||
kobo_display_price: 'True',
|
||||
kobo_dropbox_link_account_enabled: 'True',
|
||||
kobo_google_tax: 'False',
|
||||
kobo_googledrive_link_account_enabled: 'True',
|
||||
kobo_nativeborrow_enabled: 'True',
|
||||
kobo_onedrive_link_account_enabled: 'False',
|
||||
kobo_onestorelibrary_enabled: 'False',
|
||||
kobo_privacyCentre_url: 'https://www.kobo.com/privacy',
|
||||
kobo_redeem_enabled: 'True',
|
||||
kobo_shelfie_enabled: 'False',
|
||||
kobo_subscriptions_enabled: 'True',
|
||||
kobo_superpoints_enabled: 'True',
|
||||
kobo_wishlist_enabled: 'True',
|
||||
library_book: 'https://storeapi.kobo.com/v1/user/library/books/{LibraryItemId}',
|
||||
library_items: 'https://storeapi.kobo.com/v1/user/library',
|
||||
library_metadata: 'https://storeapi.kobo.com/v1/library/{Ids}/metadata',
|
||||
library_prices: 'https://storeapi.kobo.com/v1/user/library/previews/prices',
|
||||
library_search: 'https://storeapi.kobo.com/v1/library/search',
|
||||
library_sync: 'https://storeapi.kobo.com/v1/library/sync',
|
||||
love_dashboard_page: 'https://www.kobo.com/{region}/{language}/kobosuperpoints',
|
||||
love_points_redemption_page: 'https://www.kobo.com/{region}/{language}/KoboSuperPointsRedemption?productId={ProductId}',
|
||||
magazine_landing_page: 'https://www.kobo.com/emagazines',
|
||||
more_sign_in_options: 'https://authorize.kobo.com/signin?returnUrl=http://kobo.com/#allProviders',
|
||||
notebooks: 'https://storeapi.kobo.com/api/internal/notebooks',
|
||||
notifications_registration_issue: 'https://storeapi.kobo.com/v1/notifications/registration',
|
||||
oauth_host: 'https://oauth.kobo.com',
|
||||
password_retrieval_page: 'https://www.kobo.com/passwordretrieval.html',
|
||||
personalizedrecommendations: 'https://storeapi.kobo.com/v2/users/personalizedrecommendations',
|
||||
pocket_link_account_start: 'https://authorize.kobo.com/{region}/{language}/linkpocket',
|
||||
post_analytics_event: 'https://storeapi.kobo.com/v1/analytics/event',
|
||||
ppx_purchasing_url: 'https://purchasing.kobo.com',
|
||||
privacy_page: 'https://www.kobo.com/privacypolicy?style=onestore',
|
||||
product_nextread: 'https://storeapi.kobo.com/v1/products/{ProductIds}/nextread',
|
||||
product_prices: 'https://storeapi.kobo.com/v1/products/{ProductIds}/prices',
|
||||
product_recommendations: 'https://storeapi.kobo.com/v1/products/{ProductId}/recommendations',
|
||||
product_reviews: 'https://storeapi.kobo.com/v1/products/{ProductIds}/reviews',
|
||||
products: 'https://storeapi.kobo.com/v1/products',
|
||||
productsv2: 'https://storeapi.kobo.com/v2/products',
|
||||
provider_external_sign_in_page: 'https://authorize.kobo.com/ExternalSignIn/{providerName}?returnUrl=http://kobo.com/',
|
||||
purchase_buy: 'https://www.kobo.com/checkoutoption/',
|
||||
purchase_buy_templated: 'https://www.kobo.com/{region}/{language}/checkoutoption/{ProductId}',
|
||||
quickbuy_checkout: 'https://storeapi.kobo.com/v1/store/quickbuy/{PurchaseId}/checkout',
|
||||
quickbuy_create: 'https://storeapi.kobo.com/v1/store/quickbuy/purchase',
|
||||
rakuten_token_exchange: 'https://storeapi.kobo.com/v1/auth/rakuten_token_exchange',
|
||||
rating: 'https://storeapi.kobo.com/v1/products/{ProductId}/rating/{Rating}',
|
||||
reading_services_host: 'https://readingservices.kobo.com',
|
||||
reading_state: 'https://storeapi.kobo.com/v1/library/{Ids}/state',
|
||||
redeem_interstitial_page: 'https://www.kobo.com',
|
||||
registration_page: 'https://authorize.kobo.com/signup?returnUrl=http://kobo.com/',
|
||||
related_items: 'https://storeapi.kobo.com/v1/products/{Id}/related',
|
||||
remaining_book_series: 'https://storeapi.kobo.com/v1/products/books/series/{SeriesId}',
|
||||
rename_tag: 'https://storeapi.kobo.com/v1/library/tags/{TagId}',
|
||||
review: 'https://storeapi.kobo.com/v1/products/reviews/{ReviewId}',
|
||||
review_sentiment: 'https://storeapi.kobo.com/v1/products/reviews/{ReviewId}/sentiment/{Sentiment}',
|
||||
shelfie_recommendations: 'https://storeapi.kobo.com/v1/user/recommendations/shelfie',
|
||||
sign_in_page: 'https://auth.kobobooks.com/ActivateOnWeb',
|
||||
social_authorization_host: 'https://social.kobobooks.com:8443',
|
||||
social_host: 'https://social.kobobooks.com',
|
||||
store_home: 'www.kobo.com/{region}/{language}',
|
||||
store_host: 'www.kobo.com',
|
||||
store_newreleases: 'https://www.kobo.com/{region}/{language}/List/new-releases/961XUjtsU0qxkFItWOutGA',
|
||||
store_search: 'https://www.kobo.com/{region}/{language}/Search?Query={query}',
|
||||
store_top50: 'https://www.kobo.com/{region}/{language}/ebooks/Top',
|
||||
subs_landing_page: 'https://www.kobo.com/{region}/{language}/plus',
|
||||
subs_management_page: 'https://www.kobo.com/{region}/{language}/account/subscriptions',
|
||||
subs_plans_page: 'https://www.kobo.com/{region}/{language}/plus/plans',
|
||||
subs_purchase_buy_templated: 'https://www.kobo.com/{region}/{language}/Checkoutoption/{ProductId}/{TierId}',
|
||||
subscription_publisher_price_page: 'https://www.kobo.com/{region}/{language}/subscriptionpublisherprice',
|
||||
tag_items: 'https://storeapi.kobo.com/v1/library/tags/{TagId}/Items',
|
||||
tags: 'https://storeapi.kobo.com/v1/library/tags',
|
||||
taste_profile: 'https://storeapi.kobo.com/v1/products/tasteprofile',
|
||||
terms_of_sale_page: 'https://authorize.kobo.com/{region}/{language}/terms/termsofsale',
|
||||
update_accessibility_to_preview: 'https://storeapi.kobo.com/v1/library/{EntitlementIds}/preview',
|
||||
use_one_store: 'True',
|
||||
user_loyalty_benefits: 'https://storeapi.kobo.com/v1/user/loyalty/benefits',
|
||||
user_platform: 'https://storeapi.kobo.com/v1/user/platform',
|
||||
user_profile: 'https://storeapi.kobo.com/v1/user/profile',
|
||||
user_ratings: 'https://storeapi.kobo.com/v1/user/ratings',
|
||||
user_recommendations: 'https://storeapi.kobo.com/v1/user/recommendations',
|
||||
user_reviews: 'https://storeapi.kobo.com/v1/user/reviews',
|
||||
user_wishlist: 'https://storeapi.kobo.com/v1/user/wishlist',
|
||||
userguide_host: 'https://ereaderfiles.kobo.com',
|
||||
wishlist_page: 'https://www.kobo.com/{region}/{language}/account/wishlist',
|
||||
};
|
||||
@@ -7,6 +7,7 @@ const FORWARD_HEADERS = [
|
||||
'accept',
|
||||
'accept-encoding',
|
||||
'accept-language',
|
||||
'authorization',
|
||||
'content-type',
|
||||
'user-agent',
|
||||
'x-kobo-appversion',
|
||||
@@ -35,10 +36,15 @@ export class KoboProxyService {
|
||||
}
|
||||
|
||||
try {
|
||||
let body: string | undefined;
|
||||
if (!['GET', 'HEAD'].includes(req.method) && req.body != null) {
|
||||
body = typeof req.body === 'string' ? req.body : JSON.stringify(req.body);
|
||||
}
|
||||
|
||||
const upstream = await fetch(targetUrl, {
|
||||
method: req.method,
|
||||
headers,
|
||||
body: ['GET', 'HEAD'].includes(req.method) ? undefined : (req.body as string | undefined),
|
||||
body,
|
||||
});
|
||||
|
||||
reply.status(upstream.status);
|
||||
@@ -48,8 +54,8 @@ export class KoboProxyService {
|
||||
}
|
||||
});
|
||||
|
||||
const body = await upstream.arrayBuffer();
|
||||
reply.send(Buffer.from(body));
|
||||
const responseBody = await upstream.arrayBuffer();
|
||||
reply.send(Buffer.from(responseBody));
|
||||
} catch (err) {
|
||||
this.logger.warn(`Proxy failed for ${targetUrl}: ${(err as Error).message}`);
|
||||
reply.status(502).send({ message: 'Upstream Kobo API unavailable' });
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||
import { and, eq, isNull, or, sql } from 'drizzle-orm';
|
||||
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
||||
|
||||
import { DB } from '../../../db/db.module';
|
||||
import { DB } from '../../../db';
|
||||
import * as schema from '../../../db/schema';
|
||||
|
||||
type Db = NodePgDatabase<typeof schema>;
|
||||
@@ -25,6 +25,24 @@ export class KoboReadingStateService {
|
||||
const entitlementId = String(bookId);
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const book = await this.db.query.books.findFirst({
|
||||
where: eq(schema.books.id, bookId),
|
||||
columns: { id: true },
|
||||
});
|
||||
if (!book) {
|
||||
return {
|
||||
RequestResult: 'Success',
|
||||
UpdateResults: [
|
||||
{
|
||||
EntitlementId: entitlementId,
|
||||
CurrentBookmarkResult: { Result: 'Ignored' },
|
||||
StatisticsResult: { Result: 'Ignored' },
|
||||
StatusInfoResult: { Result: 'Ignored' },
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const created = (payload.Created as string | undefined) ?? now;
|
||||
const lastModified = (payload.LastModified as string | undefined) ?? now;
|
||||
const priorityTimestamp = (payload.PriorityTimestamp as string | undefined) ?? lastModified;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
||||
import { BadRequestException, Inject, Injectable, InternalServerErrorException } from '@nestjs/common';
|
||||
import { eq, sql } from 'drizzle-orm';
|
||||
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
||||
|
||||
import { DB } from '../../../db/db.module';
|
||||
import { DB } from '../../../db';
|
||||
import * as schema from '../../../db/schema';
|
||||
|
||||
type Db = NodePgDatabase<typeof schema>;
|
||||
@@ -30,7 +30,7 @@ export class KoboSettingsService {
|
||||
row = inserted ?? (await this.db.query.koboSyncSettings.findFirst({ where: eq(schema.koboSyncSettings.userId, userId) }));
|
||||
}
|
||||
|
||||
if (!row) throw new Error('Failed to create kobo settings row');
|
||||
if (!row) throw new InternalServerErrorException('Failed to create kobo settings row');
|
||||
return {
|
||||
readingThreshold: row.readingThreshold,
|
||||
finishedThreshold: row.finishedThreshold,
|
||||
|
||||
Reference in New Issue
Block a user