feat: portfolio stats
This commit is contained in:
parent
3677a3507d
commit
6de03a24f3
|
|
@ -70,6 +70,7 @@
|
|||
"aplayer": "^1.10.1",
|
||||
"async-lock": "1.4.0",
|
||||
"canvas-confetti": "1.6.0",
|
||||
"cheerio": "1.0.0-rc.12",
|
||||
"chroma-js": "2.4.2",
|
||||
"clsx": "2.0.0",
|
||||
"crossbell": "1.5.5",
|
||||
|
|
|
|||
|
|
@ -125,6 +125,9 @@ dependencies:
|
|||
canvas-confetti:
|
||||
specifier: 1.6.0
|
||||
version: 1.6.0
|
||||
cheerio:
|
||||
specifier: 1.0.0-rc.12
|
||||
version: 1.0.0-rc.12
|
||||
chroma-js:
|
||||
specifier: 2.4.2
|
||||
version: 2.4.2
|
||||
|
|
@ -184,7 +187,7 @@ dependencies:
|
|||
version: 0.16.8
|
||||
langchain:
|
||||
specifier: 0.0.122
|
||||
version: 0.0.122(ioredis@5.3.2)
|
||||
version: 0.0.122(cheerio@1.0.0-rc.12)(ioredis@5.3.2)
|
||||
lottie-react:
|
||||
specifier: 2.4.0
|
||||
version: 2.4.0(react-dom@18.2.0)(react@18.2.0)
|
||||
|
|
@ -7843,7 +7846,7 @@ packages:
|
|||
engines: {node: '>=6'}
|
||||
dev: false
|
||||
|
||||
/langchain@0.0.122(ioredis@5.3.2):
|
||||
/langchain@0.0.122(cheerio@1.0.0-rc.12)(ioredis@5.3.2):
|
||||
resolution: {integrity: sha512-M/w6CZH+Y12jrts5ENFLpQF3ZzwI6bluubeL0wjyqz/btd8puXsHyyyt58bKg8bIMZF02Y2Tjr1qyqKQKTaJ0Q==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
|
|
@ -8039,6 +8042,7 @@ packages:
|
|||
ansi-styles: 5.2.0
|
||||
binary-extensions: 2.2.0
|
||||
camelcase: 6.3.0
|
||||
cheerio: 1.0.0-rc.12
|
||||
decamelize: 1.2.0
|
||||
expr-eval: 2.0.2
|
||||
flat: 5.0.2
|
||||
|
|
|
|||
|
|
@ -0,0 +1,127 @@
|
|||
import * as cheerio from "cheerio"
|
||||
|
||||
import { cacheGet } from "~/lib/redis.server"
|
||||
import { NextServerResponse, getQuery } from "~/lib/server-helper"
|
||||
import { PortfolioStats } from "~/lib/types"
|
||||
|
||||
export async function GET(req: Request) {
|
||||
const query = getQuery(req)
|
||||
|
||||
const res = new NextServerResponse()
|
||||
if (!query.url) {
|
||||
return res.status(400).json({ error: "Missing url" })
|
||||
}
|
||||
|
||||
let url: URL | undefined
|
||||
try {
|
||||
url = new URL(query.url)
|
||||
} catch (error) {
|
||||
return res.status(400).json({ error: "Invalid url" })
|
||||
}
|
||||
|
||||
const ua =
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36"
|
||||
|
||||
const result = (await cacheGet({
|
||||
key: `portfoliostats/${url}`,
|
||||
allowEmpty: true,
|
||||
noUpdate: true,
|
||||
expireTime: 60 * 60 * 24,
|
||||
getValueFun: async () => {
|
||||
let result: PortfolioStats | null = null
|
||||
switch (url?.hostname) {
|
||||
// https://www.bilibili.com/video/BV1yz4y147Ht/
|
||||
case "www.bilibili.com": {
|
||||
const bvid = url.pathname.split("/")[2]
|
||||
const { data } = await (
|
||||
await fetch(
|
||||
`https://api.bilibili.com/x/web-interface/view?bvid=${bvid}`,
|
||||
{
|
||||
headers: {
|
||||
"User-Agent": ua,
|
||||
Referer: "https://www.bilibili.com/",
|
||||
},
|
||||
},
|
||||
)
|
||||
).json()
|
||||
if (data?.stat) {
|
||||
result = {
|
||||
videoViewsCount: data.stat.view,
|
||||
commentsCount: data.stat.reply + data.stat.danmaku,
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// https://www.xiaoyuzhoufm.com/episode/645a76f67d934b85051081c8
|
||||
case "www.xiaoyuzhoufm.com": {
|
||||
const data = await (
|
||||
await fetch(url, {
|
||||
headers: {
|
||||
"User-Agent": ua,
|
||||
},
|
||||
})
|
||||
).text()
|
||||
const $ = cheerio.load(data)
|
||||
const audoListensCount = parseInt($(".stat").eq(0).text())
|
||||
const commentsCount = parseInt($(".stat").eq(1).text())
|
||||
if (audoListensCount || commentsCount) {
|
||||
result = {
|
||||
audoListensCount,
|
||||
commentsCount,
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// https://github.com/Crossbell-Box/xlog
|
||||
case "github.com": {
|
||||
const repoData = await (
|
||||
await fetch(
|
||||
`https://api.github.com/repos/${url.pathname.split("/")[1]}/${
|
||||
url.pathname.split("/")[2]
|
||||
}`,
|
||||
)
|
||||
).json()
|
||||
const issuesData = await (
|
||||
await fetch(
|
||||
`https://api.github.com/repos/${url.pathname.split("/")[1]}/${
|
||||
url.pathname.split("/")[2]
|
||||
}/issues`,
|
||||
)
|
||||
).json()
|
||||
if (repoData?.stargazers_count || issuesData?.[0]?.number) {
|
||||
result = {
|
||||
projectStarsCount: repoData.stargazers_count,
|
||||
commentsCount: issuesData[0].number,
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// https://www.pixiv.net/artworks/69178118
|
||||
case "www.pixiv.net": {
|
||||
const id = url.pathname.split("/")[2]
|
||||
const { body } = await (
|
||||
await fetch(`https://www.pixiv.net/ajax/illust/${id}`, {
|
||||
headers: {
|
||||
"User-Agent": ua,
|
||||
Referer: "https://www.pixiv.net/",
|
||||
},
|
||||
})
|
||||
).json()
|
||||
|
||||
if (body?.viewCount || body?.commentCount) {
|
||||
result = {
|
||||
textViewsCount: body.viewCount,
|
||||
commentsCount: body.commentCount,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
},
|
||||
})) as PortfolioStats
|
||||
|
||||
return res.status(200).json(result)
|
||||
}
|
||||
|
|
@ -111,7 +111,7 @@ const Card = ({
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="xlog-post-meta text-zinc-400 flex items-center text-[13px] h-[26px] truncate">
|
||||
<div className="xlog-post-meta text-zinc-400 flex items-center text-[13px] h-[26px] truncate space-x-2">
|
||||
{isPortfolio ? (
|
||||
<>
|
||||
<Tooltip
|
||||
|
|
@ -130,12 +130,42 @@ const Card = ({
|
|||
<span>{t("Portfolio")}</span>
|
||||
</span>
|
||||
</Tooltip>
|
||||
{!!post.stat?.portfolio?.videoViewsCount && (
|
||||
<span className="xlog-post-views inline-flex items-center">
|
||||
<i className="icon-[mingcute--youtube-line] mr-[2px] text-base" />
|
||||
<span>{post.stat.portfolio.videoViewsCount}</span>
|
||||
</span>
|
||||
)}
|
||||
{!!post.stat?.portfolio?.audoListensCount && (
|
||||
<span className="xlog-post-views inline-flex items-center">
|
||||
<i className="icon-[mingcute--headphone-line] mr-[2px] text-base" />
|
||||
<span>{post.stat.portfolio.audoListensCount}</span>
|
||||
</span>
|
||||
)}
|
||||
{!!post.stat?.portfolio?.projectStarsCount && (
|
||||
<span className="xlog-post-views inline-flex items-center">
|
||||
<i className="icon-[mingcute--star-line] mr-[2px] text-base" />
|
||||
<span>{post.stat.portfolio.projectStarsCount}</span>
|
||||
</span>
|
||||
)}
|
||||
{!!post.stat?.portfolio?.textViewsCount && (
|
||||
<span className="xlog-post-views inline-flex items-center">
|
||||
<i className="icon-[mingcute--eye-line] mr-[2px] text-base" />
|
||||
<span>{post.stat.portfolio.textViewsCount}</span>
|
||||
</span>
|
||||
)}
|
||||
{!!post.stat?.portfolio?.commentsCount && (
|
||||
<span className="xlog-post-views inline-flex items-center">
|
||||
<i className="icon-[mingcute--comment-line] mr-[2px] text-base" />
|
||||
<span>{post.stat.portfolio.commentsCount}</span>
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{!!post.metadata?.content?.tags?.[1] && (
|
||||
<span
|
||||
className="xlog-post-tags hover:text-zinc-600 hover:bg-zinc-200 border transition-colors text-zinc-500 inline-flex items-center bg-zinc-100 rounded-full px-2 py-[1.5px] truncate text-xs sm:text-[13px] mr-2"
|
||||
className="xlog-post-tags hover:text-zinc-600 hover:bg-zinc-200 border transition-colors text-zinc-500 inline-flex items-center bg-zinc-100 rounded-full px-2 py-[1.5px] truncate text-xs sm:text-[13px]"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
router.push(`/tag/${post.metadata?.content?.tags?.[1]}`)
|
||||
|
|
@ -145,8 +175,14 @@ const Card = ({
|
|||
{post.metadata?.content?.tags?.[1]}
|
||||
</span>
|
||||
)}
|
||||
<span className="xlog-post-word-count sm:inline-flex items-center hidden mr-2">
|
||||
<i className="icon-[mingcute--time-line] mr-[2px]" />
|
||||
{!!post.stat?.viewDetailCount && (
|
||||
<span className="xlog-post-views inline-flex items-center">
|
||||
<i className="icon-[mingcute--eye-line] mr-[2px] text-base" />
|
||||
<span>{post.stat?.viewDetailCount}</span>
|
||||
</span>
|
||||
)}
|
||||
<span className="xlog-post-word-count sm:inline-flex items-center hidden">
|
||||
<i className="icon-[mingcute--sandglass-line] mr-[2px] text-sm" />
|
||||
<span
|
||||
style={{
|
||||
wordSpacing: "-.2ch",
|
||||
|
|
@ -155,12 +191,6 @@ const Card = ({
|
|||
{post.metadata?.content?.readingTime} {t("min")}
|
||||
</span>
|
||||
</span>
|
||||
{!!post.stat?.viewDetailCount && (
|
||||
<span className="xlog-post-views inline-flex items-center">
|
||||
<i className="icon-[mingcute--eye-line] mr-[2px]" />
|
||||
<span>{post.stat?.viewDetailCount}</span>
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import removeMarkdown from "remove-markdown"
|
|||
import { SCORE_API_DOMAIN, SITE_URL } from "~/lib/env"
|
||||
import { toCid, toGateway } from "~/lib/ipfs-parser"
|
||||
import readingTime from "~/lib/reading-time"
|
||||
import { ExpandedCharacter, ExpandedNote } from "~/lib/types"
|
||||
import { ExpandedCharacter, ExpandedNote, PortfolioStats } from "~/lib/types"
|
||||
|
||||
import { getNoteSlug } from "./helpers"
|
||||
|
||||
|
|
@ -94,7 +94,18 @@ export const expandCrossbellNote = async ({
|
|||
: 0
|
||||
|
||||
if (useStat) {
|
||||
if (!expandedNote.stat) {
|
||||
if (expandedNote.metadata?.content?.tags?.[0] === "portfolio") {
|
||||
const stat = (await (
|
||||
await fetch(
|
||||
`${SITE_URL}/api/portfolio-stats?url=${encodeURIComponent(
|
||||
expandedNote.metadata?.content?.external_urls?.[0] || "",
|
||||
)}`,
|
||||
)
|
||||
).json()) as PortfolioStats
|
||||
expandedNote.stat = {
|
||||
portfolio: stat,
|
||||
}
|
||||
} else if (!expandedNote.stat) {
|
||||
const stat = await (
|
||||
await fetch(
|
||||
`https://indexer.crossbell.io/v1/stat/notes/${expandedNote.characterId}/${expandedNote.noteId}`,
|
||||
|
|
|
|||
|
|
@ -72,6 +72,14 @@ export type SiteNavigationItem = {
|
|||
url: string
|
||||
}
|
||||
|
||||
export type PortfolioStats = {
|
||||
videoViewsCount?: number
|
||||
audoListensCount?: number
|
||||
projectStarsCount?: number
|
||||
textViewsCount?: number
|
||||
commentsCount?: number
|
||||
}
|
||||
|
||||
export type ExpandedNote = NoteEntity & {
|
||||
draftKey?: string
|
||||
metadata: {
|
||||
|
|
@ -92,8 +100,9 @@ export type ExpandedNote = NoteEntity & {
|
|||
}
|
||||
}
|
||||
stat?: {
|
||||
viewDetailCount: number
|
||||
viewDetailCount?: number
|
||||
hotScore?: number
|
||||
portfolio?: PortfolioStats
|
||||
}
|
||||
local?: boolean
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue