feat: portfolio stats

This commit is contained in:
DIYgod 2023-08-07 18:52:06 +01:00
parent 3677a3507d
commit 6de03a24f3
No known key found for this signature in database
6 changed files with 197 additions and 15 deletions

View File

@ -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",

View File

@ -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

View File

@ -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)
}

View File

@ -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>

View File

@ -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}`,

View File

@ -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
}