diff --git a/src/app/api/feed/route.ts b/src/app/api/feed/route.ts index 4aca07e8..830762a0 100644 --- a/src/app/api/feed/route.ts +++ b/src/app/api/feed/route.ts @@ -1,6 +1,5 @@ import { getQuery, NextServerResponse } from "~/lib/server-helper" import { getFeed } from "~/models/home.model" -import { decoratePageForImageDimensions } from "~/queries/page.server" export async function GET(req: Request) { const query = getQuery(req) @@ -15,13 +14,10 @@ export async function GET(req: Request) { searchType: query.searchType, tag: query.tag, useHTML: false, + useImageDimensions: query.useImageDimensions === "true", topic: query.topic, }) - for (const item of result.list) { - await decoratePageForImageDimensions(item) - } - const res = new NextServerResponse() return res.status(200).json(result) } diff --git a/src/app/api/image-dimensions/route.ts b/src/app/api/image-dimensions/route.ts new file mode 100644 index 00000000..508ca31b --- /dev/null +++ b/src/app/api/image-dimensions/route.ts @@ -0,0 +1,132 @@ +import AsyncLock from "async-lock" +import sizeOf from "image-size" + +import { Metadata } from "@prisma/client" + +import { toGateway } from "~/lib/ipfs-parser" +import prisma from "~/lib/prisma.server" +import { cacheGet } from "~/lib/redis.server" +import { getQuery, NextServerResponse } from "~/lib/server-helper" + +const lock = new AsyncLock() + +export const getImageDimensionByUri = async ( + uri: string, + useFullSize = false, +): Promise<{ width: number; height: number; uri: string } | null> => { + const headers: Record = {} + + if (!useFullSize) { + headers["Range"] = "bytes=0-10240" + } + + try { + const response = await fetch(uri, { headers }) + const arrayBuffer = await response.arrayBuffer() + const buffer = Buffer.from(arrayBuffer) + const dimensions = sizeOf(buffer) + if (dimensions.width && dimensions.height) { + return { + width: dimensions.width, + height: dimensions.height, + uri, + } + } else { + throw new Error("Could not determine image dimensions.") + } + } catch (e) { + if (!useFullSize) { + return getImageDimensionByUri(uri, true) + } + + return null + } +} + +export const getCoverDimensions = async ({ + cid, + uris, +}: { + cid: string + uris: string[] +}) => { + if (!uris.length) return + + const dimensions = (await cacheGet({ + key: ["coverDimensions", cid], + allowEmpty: true, + noUpdate: true, + noExpire: true, + getValueFun: async () => { + let result + + await lock.acquire(`coverDimensions_${cid}`, async () => { + const meta = await prisma.metadata.findFirst({ + where: { + uri: `ipfs://${cid}`, + }, + }) + + const key: keyof Metadata = "image_dimensions" + const dimensions = meta?.image_dimensions + + if (dimensions) { + result = dimensions + } else { + const newDimensions: Record< + string, + { width: number; height: number } + > = {} + + console.time(`get image dimensions ${cid}`) + for (const uri of uris) { + const dimension = await getImageDimensionByUri( + toGateway(uri), + ).catch(() => null) + if (dimension) { + newDimensions[uri] = dimension + } + } + console.timeEnd(`get image dimensions ${cid}`) + + if (meta) { + await prisma.metadata.update({ + where: { uri: `ipfs://${cid}` }, + data: { + [key]: newDimensions, + }, + }) + } else { + await prisma.metadata.create({ + data: { + uri: `ipfs://${cid}`, + [key]: newDimensions, + }, + }) + } + result = newDimensions + } + }) + + return result + }, + })) as Record | undefined + + return dimensions +} + +export async function GET(req: Request): Promise { + let { cid, uris: _uris } = getQuery(req) + const uris = _uris.split(",") + const res = new NextServerResponse() + + if (!cid) { + return res.status(400).send("Bad Request") + } + + if (uris.length === 0) return res.status(200).json({}) + + return res.status(200).json({ + data: await getCoverDimensions({ cid, uris }), + }) +} diff --git a/src/app/api/pages/route.ts b/src/app/api/pages/route.ts index b37404fc..457a67d7 100644 --- a/src/app/api/pages/route.ts +++ b/src/app/api/pages/route.ts @@ -1,7 +1,6 @@ import { getQuery, NextServerResponse } from "~/lib/server-helper" import { PagesSortTypes, PageVisibilityEnum } from "~/lib/types" import { getPagesBySite } from "~/models/page.model" -import { decoratePageForImageDimensions } from "~/queries/page.server" export async function GET(req: Request) { const query = getQuery(req) @@ -13,6 +12,7 @@ export async function GET(req: Request) { limit: query.limit ? parseInt(query.limit as string) : undefined, cursor: query.cursor as string, useStat: true, + useImageDimensions: query.useImageDimensions === "true", skipExpansion: query.skipExpansion === "true", sortType: query.sortType as PagesSortTypes, ...(query.tags && { @@ -22,10 +22,6 @@ export async function GET(req: Request) { }), }) - for (const item of result.list) { - await decoratePageForImageDimensions(item) - } - const res = new NextServerResponse() return res.status(200).json(result) } diff --git a/src/lib/expand-unit.ts b/src/lib/expand-unit.ts index 1c8b5c40..5aff87ff 100644 --- a/src/lib/expand-unit.ts +++ b/src/lib/expand-unit.ts @@ -13,6 +13,7 @@ export const expandCrossbellNote = async ({ note, useStat, useScore, + useImageDimensions, keyword, useHTML, disableAutofill, @@ -26,6 +27,7 @@ export const expandCrossbellNote = async ({ } useStat?: boolean useScore?: boolean + useImageDimensions?: boolean keyword?: string useHTML?: boolean disableAutofill?: boolean @@ -100,6 +102,40 @@ export const expandCrossbellNote = async ({ ...new Set(expandedNote.metadata.content.images), ] + if (useScore) { + try { + const score = ( + await ( + await fetch( + `${SCORE_API_DOMAIN || SITE_URL}/api/score?cid=${toCid( + expandedNote.metadata?.uri || "", + )}`, + ) + ).json() + ).data + expandedNote.metadata.content.score = score + } catch (e) { + // do nothing + } + } + + if (useImageDimensions) { + try { + const imageDimensions = await ( + await fetch( + `${SITE_URL}/api/image-dimensions?` + + new URLSearchParams({ + cid: toCid(expandedNote.metadata.uri || ""), + uris: expandedNote.metadata.content.images || [], + } as any), + ) + ).json() + expandedNote.metadata.content.imageDimensions = imageDimensions.data + } catch (e) { + // do nothing + } + } + expandedNote.metadata.content.slug = getNoteSlug( expandedNote, disableAutofill, diff --git a/src/models/home.model.ts b/src/models/home.model.ts index fd53a78b..353192de 100644 --- a/src/models/home.model.ts +++ b/src/models/home.model.ts @@ -35,6 +35,7 @@ export async function getFeed({ searchType, tag, useHTML, + useImageDimensions, topic, }: { type?: FeedType @@ -46,6 +47,7 @@ export async function getFeed({ searchType?: SearchType tag?: string useHTML?: boolean + useImageDimensions?: boolean topic?: string }) { if (type === "search" && !searchKeyword) { @@ -141,6 +143,7 @@ export async function getFeed({ note: page, useScore: true, useHTML, + useImageDimensions, }), ), ) @@ -235,6 +238,7 @@ export async function getFeed({ const expand = await expandCrossbellNote({ note: page, useHTML, + useImageDimensions, }) if (expand.toNote) { if (expand.toNote.toNote) { @@ -251,6 +255,7 @@ export async function getFeed({ expand.toNote = await expandCrossbellNote({ note: expand.toNote, useHTML, + useImageDimensions, }) delete expand.toNote.toNote } @@ -321,6 +326,7 @@ export async function getFeed({ useScore: true, useStat: true, useHTML, + useImageDimensions, }), ), ) @@ -359,6 +365,7 @@ export async function getFeed({ expandCrossbellNote({ note: page, useHTML, + useImageDimensions, }), ), ) @@ -465,6 +472,7 @@ export async function getFeed({ expandCrossbellNote({ note: page, useHTML, + useImageDimensions, }), ), ) @@ -540,6 +548,7 @@ export async function getFeed({ expandCrossbellNote({ note: page, useHTML, + useImageDimensions, }), ), ) @@ -644,6 +653,7 @@ export async function getFeed({ note: page, useHTML, useStat: true, + useImageDimensions, }) return expand }, @@ -769,6 +779,7 @@ export async function getFeed({ note: page, useHTML, useStat: true, + useImageDimensions, }) return expand }, @@ -813,6 +824,7 @@ export async function getFeed({ useScore: false, keyword: searchKeyword, useHTML, + useImageDimensions, }), ), ) @@ -850,6 +862,7 @@ export async function getFeed({ useStat: false, useScore: true, useHTML, + useImageDimensions, }), ), ) diff --git a/src/models/page.model.ts b/src/models/page.model.ts index b65ea0d0..8e561094 100644 --- a/src/models/page.model.ts +++ b/src/models/page.model.ts @@ -122,6 +122,7 @@ export async function getPagesBySite(input: { tags?: string[] useStat?: boolean useHTML?: boolean + useImageDimensions?: boolean keepBody?: boolean handle?: string // In order to be compatible with old drafts skipExpansion?: boolean @@ -311,6 +312,7 @@ export async function getPagesBySite(input: { useStat: input.useStat, useHTML: input.useHTML, useScore: false, + useImageDimensions: input.useImageDimensions, }) } diff --git a/src/queries/page.server.ts b/src/queries/page.server.ts index 8985330b..b0958619 100644 --- a/src/queries/page.server.ts +++ b/src/queries/page.server.ts @@ -1,7 +1,4 @@ -import https from "https" -import url from "url" import AsyncLock from "async-lock" -import sizeOf from "image-size" import { AnalyzeDocumentChain, loadSummarizationChain } from "langchain/chains" import { OpenAI } from "langchain/llms/openai" import { PromptTemplate } from "langchain/prompts" @@ -10,7 +7,7 @@ import removeMarkdown from "remove-markdown" import { Metadata } from "@prisma/client" import { QueryClient } from "@tanstack/react-query" -import { toCid, toGateway } from "~/lib/ipfs-parser" +import { toGateway } from "~/lib/ipfs-parser" import prisma from "~/lib/prisma.server" import { cacheGet } from "~/lib/redis.server" import * as pageModel from "~/models/page.model" @@ -38,145 +35,6 @@ export const fetchGetPage = async ( }) } -export const getImageDimensionByUri = async (uri: string) => { - return new Promise<{ width: number; height: number; uri: string }>( - (resolve, reject) => { - const options = url.parse(uri) - - https.get(options, function (response) { - const chunks: Buffer[] = [] - let totalLength = 0 - const MAX_BUFFER_LENGTH = 10240 - - response - .on("data", function (chunk) { - chunks.push(chunk) - totalLength += chunk.length - - if (totalLength > MAX_BUFFER_LENGTH) { - response.destroy() - const buffer = Buffer.concat(chunks) - try { - const dimension = sizeOf(buffer) - if (dimension.width && dimension.height) { - resolve({ - width: dimension.width, - height: dimension.height, - uri, - }) - } - } catch (e) { - reject("Not enough data for image size detection.") - } - } - }) - .on("end", function () { - if (totalLength <= MAX_BUFFER_LENGTH) { - const buffer = Buffer.concat(chunks) - try { - const dimension = sizeOf(buffer) - if (dimension.width && dimension.height) { - resolve({ - width: dimension.width, - height: dimension.height, - uri, - }) - } - } catch (e) { - reject(e) - } - } - }) - }) - }, - ) -} - -export const cacheCoverDimensions = async ({ - cid, - uris, -}: { - cid: string - uris: string[] -}) => { - if (!uris.length) return - - const dimensions = (await cacheGet({ - key: ["coverDimensions", cid], - allowEmpty: true, - noUpdate: true, - noExpire: true, - getValueFun: async () => { - let result - - await lock.acquire(`coverDimensions_${cid}`, async () => { - const meta = await prisma.metadata.findFirst({ - where: { - uri: `ipfs://${cid}`, - }, - }) - - const key: keyof Metadata = "image_dimensions" - const dimensions = meta?.image_dimensions - - if (dimensions) { - result = dimensions - } else { - const newDimensions: Record< - string, - { width: number; height: number } - > = {} - - console.time(`get image dimensions ${cid}`) - for (const uri of uris) { - const dimension = await getImageDimensionByUri(toGateway(uri)) - if (dimension) { - newDimensions[uri] = dimension - } - } - console.timeEnd(`get image dimensions ${cid}`) - - if (meta) { - await prisma.metadata.update({ - where: { uri: `ipfs://${cid}` }, - data: { - [key]: newDimensions, - }, - }) - } else { - await prisma.metadata.create({ - data: { - uri: `ipfs://${cid}`, - [key]: newDimensions, - }, - }) - } - result = newDimensions - } - }) - - return result - }, - })) as Record | undefined - - return dimensions -} - -export const decoratePageForImageDimensions = async ( - page?: Awaited> | null, -) => { - if (!page) return - - const dimensions = await cacheCoverDimensions({ - cid: toCid(page?.metadata?.uri || ""), - uris: page?.metadata?.content?.images || [], - }) - - if (dimensions && Object.keys(dimensions).length && page?.metadata?.content) { - page.metadata.content["imageDimensions"] = dimensions - } -} - export const prefetchGetPagesBySite = async ( input: Parameters[0], queryClient: QueryClient,