Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 17 additions & 17 deletions apps/backend/src/routes/playlists.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ const playlistsRoute = new Hono<HonoEnv>()
cover_url: playlist.coverUrl,
created_at: playlist.createdAt.getTime(),
updated_at: playlist.updatedAt.getTime(),
track_count: Number(trackCount ?? 0),
track_count: trackCount ?? 0,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 8 'createDb|DATABASE_URL|postgres|neon|sqlite|count\(\*\)' apps/backend/src
rg -n -C 5 '\btrack_count\b' apps/backend/src apps/mobile/src

Repository: bbplayer-app/BBPlayer

Length of output: 25673


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- database connection ---'
cat -n apps/backend/src/db/index.ts

printf '%s\n' '--- backend dependency manifests and lockfile references ---'
git ls-files | grep -E '(^|/)(package\.json|pnpm-lock\.yaml|yarn\.lock|package-lock\.json)$' | sort
rg -n -C 3 '"(pg|postgres|drizzle-orm)"|^  (pg|postgres|drizzle-orm)@|node-postgres' \
  --glob 'package.json' --glob 'pnpm-lock.yaml' --glob 'yarn.lock' --glob 'package-lock.json' .

printf '%s\n' '--- API contract and route implementation ---'
cat -n apps/backend/src/routes/playlists.ts | sed -n '80,150p'
cat -n apps/mobile/src/lib/facades/sharedPlaylist.ts | sed -n '330,375p'

printf '%s\n' '--- read-only static invariant check ---'
python3 - <<'PY'
from pathlib import Path
route = Path("apps/backend/src/routes/playlists.ts").read_text()
db = Path("apps/backend/src/db/index.ts").read_text()
mobile = Path("apps/mobile/src/lib/facades/sharedPlaylist.ts").read_text()
print("uses_node_postgres:", "drizzle-orm/node-postgres" in db)
print("configures_pg_type_parser:", "setTypeParser" in db or "types:" in db)
print("count_expression_is_typed_only:", "sql<number>`count(*)`" in route)
print("track_count_is_explicitly_coerced:", "track_count: Number(trackCount ?? 0)" in route)
print("mobile_contract_requires_number:", "track_count: number" in mobile)
PY

Repository: bbplayer-app/BBPlayer

Length of output: 9990


track_count 转换为数值。

createDb 使用 node-postgres,且未配置 count(*) 的数值解析。sql<number> 只提供 TypeScript 类型,不转换运行时值。pg 默认将 PostgreSQL count(*)int8 结果返回为字符串,导致接口违反 track_count: number 契约。保留 Number(trackCount ?? 0),或配置显式数据库映射。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/src/routes/playlists.ts` at line 143, Update the track_count
assignment in the playlist response to explicitly convert trackCount to a
runtime number, using Number(trackCount ?? 0), since the TypeScript sql<number>
annotation does not alter node-postgres string results.

},
owner: owner
? {
Expand Down Expand Up @@ -251,25 +251,25 @@ const playlistsRoute = new Hono<HonoEnv>()
(a, b) => a.operation_at - b.operation_at,
)

const upsertChanges = sorted.filter((c) => c.op === 'upsert')
const removeChanges = sorted.filter((c) => c.op === 'remove')
const reorderChanges = sorted.filter((c) => c.op === 'reorder')
const upsertChanges = sorted.filter((ch) => ch.op === 'upsert')
const removeChanges = sorted.filter((ch) => ch.op === 'remove')
const reorderChanges = sorted.filter((ch) => ch.op === 'reorder')

await db.transaction(async (tx) => {
// 1. 批量 upsert shared_tracks(资源池)
if (upsertChanges.length > 0) {
await tx
.insert(sharedTracks)
.values(
upsertChanges.map((c) => ({
uniqueKey: c.track.unique_key,
title: c.track.title,
artistName: c.track.artist_name,
artistId: c.track.artist_id,
coverUrl: c.track.cover_url,
duration: c.track.duration,
bilibiliBvid: c.track.bilibili_bvid,
bilibiliCid: c.track.bilibili_cid,
upsertChanges.map((ch) => ({
uniqueKey: ch.track.unique_key,
title: ch.track.title,
artistName: ch.track.artist_name,
artistId: ch.track.artist_id,
coverUrl: ch.track.cover_url,
duration: ch.track.duration,
bilibiliBvid: ch.track.bilibili_bvid,
bilibiliCid: ch.track.bilibili_cid,
})),
)
.onConflictDoUpdate({
Expand All @@ -286,12 +286,12 @@ const playlistsRoute = new Hono<HonoEnv>()
await tx
.insert(sharedPlaylistTracks)
.values(
upsertChanges.map((c) => ({
upsertChanges.map((ch) => ({
playlistId,
trackUniqueKey: c.track.unique_key,
sortKey: c.sort_key,
trackUniqueKey: ch.track.unique_key,
sortKey: ch.sort_key,
addedByUserId: userId,
updatedAt: new Date(c.operation_at),
updatedAt: new Date(ch.operation_at),
deletedAt: null,
})),
)
Expand Down
9 changes: 8 additions & 1 deletion apps/mobile/src/app/(tabs)/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import dayjs from 'dayjs'
import { eq } from 'drizzle-orm'
import { useLiveQuery } from 'drizzle-orm/expo-sqlite'
import { Image } from 'expo-image'
import { useObserve } from 'expo-observe'
import { useRouter } from 'expo-router'
import { useIncomingShare } from 'expo-sharing'
import {
Expand Down Expand Up @@ -102,6 +103,12 @@ function HomePage() {

const { data: recentPlaylists } = useRecentPlaylists()

const { markInteractive } = useObserve()

useEffect(() => {
markInteractive()
}, [markInteractive])

const greeting = getGreetingMsg()

const saveSearchHistory = useCallback(
Expand Down Expand Up @@ -303,7 +310,7 @@ function HomePage() {
)}
<Touchable
androidRipple={{}}
onPress={() => router.push('/settings/bilibili-account' as never)}
onPress={() => router.push('/settings/bilibili-account')}
style={styles.avatarButton}
>
<Image
Expand Down
8 changes: 7 additions & 1 deletion apps/mobile/src/app/(tabs)/library/[tab].tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Icon from '@react-native-vector-icons/material-design-icons'
import { useObserve } from 'expo-observe'
import { useFocusEffect, useLocalSearchParams, useRouter } from 'expo-router'
import { useState, useTransition } from 'react'
import { useEffect, useState, useTransition } from 'react'
import { StyleSheet, View } from 'react-native'
import { Text, useTheme } from 'react-native-paper'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
Expand Down Expand Up @@ -42,6 +43,11 @@ export default function Library() {
const colors = useTheme().colors
const router = useRouter()
const { tab } = useLocalSearchParams<{ tab: string }>()
const { markInteractive } = useObserve()

useEffect(() => {
markInteractive()
}, [markInteractive])

useFocusEffect(() => {
if (tab === undefined) return
Expand Down
13 changes: 10 additions & 3 deletions apps/mobile/src/app/(tabs)/settings/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { LinearGradient } from 'expo-linear-gradient'
import { useObserve } from 'expo-observe'
import { useRouter } from 'expo-router'
import { useEffect } from 'react'
import { ScrollView, StyleSheet, View } from 'react-native'
import { Divider, List, Text, useTheme } from 'react-native-paper'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
Expand All @@ -17,6 +19,11 @@ export default function SettingsPage() {
const account = useAppStore((state) => state.bbplayerAccount)
const hasBilibiliCookie = useAppStore((state) => state.hasBilibiliCookie())
const bilibiliUserInfo = useAppStore((state) => state.bilibiliUserInfo)
const { markInteractive } = useObserve()

useEffect(() => {
markInteractive()
}, [markInteractive])

return (
<View style={[styles.container, { backgroundColor: colors.background }]}>
Expand Down Expand Up @@ -155,7 +162,7 @@ export default function SettingsPage() {
icon='chevron-right'
/>
)}
onPress={() => router.push('/settings/bilibili-account' as never)}
onPress={() => router.push('/settings/bilibili-account')}
/>
<Divider style={styles.divider} />
<List.Item
Expand All @@ -177,7 +184,7 @@ export default function SettingsPage() {
icon='chevron-right'
/>
)}
onPress={() => router.push('/settings/account' as never)}
onPress={() => router.push('/settings/account')}
/>
<Divider style={styles.divider} />
<List.Item
Expand Down Expand Up @@ -232,7 +239,7 @@ export default function SettingsPage() {
icon='chevron-right'
/>
)}
onPress={() => router.push('/settings/about' as never)}
onPress={() => router.push('/settings/about')}
/>
</ScrollView>
</View>
Expand Down
56 changes: 30 additions & 26 deletions apps/mobile/src/app/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { Orpheus } from '@bbplayer/orpheus'
import {
fetch as fetchNetInfo,
addEventListener as addNetInfoEventListener,
fetch as fetchNetInfo,
} from '@react-native-community/netinfo'
import * as Sentry from '@sentry/react-native'
import { focusManager, onlineManager } from '@tanstack/react-query'
import * as Application from 'expo-application'
import { ObserveRoot, useObserve } from 'expo-observe'
import { Stack, router } from 'expo-router'
import { Observe, ObserveRoot, useObserve } from 'expo-observe'
import { router, Stack } from 'expo-router'
import * as Updates from 'expo-updates'
import { useEffect, useState } from 'react'
import type { AppStateStatus } from 'react-native'
Expand Down Expand Up @@ -42,6 +42,10 @@ import migrations from '../../drizzle/migrations'

const logger = log.extend('UI.RootLayout')

Observe.configure({
integrations: { 'expo-router': true },
})

// 初始化 Sentry
initializeSentry()

Expand All @@ -53,6 +57,28 @@ function onAppStateChange(status: AppStateStatus) {
}
}

const checkOverlayPermissionOnStart = async () => {
if (Orpheus.isDesktopLyricsShown) {
const hasPermission = await Orpheus.checkOverlayPermission()
if (!hasPermission) {
// 延迟显示,确保 UI 已经加载
setTimeout(() => {
alert(
'桌面歌词',
'检测到桌面歌词已开启,但缺少悬浮窗权限,请授权以恢复显示。',
[
{ text: '取消' },
{
text: '去授权',
onPress: () => Orpheus.requestOverlayPermission(),
},
],
)
}, 1000)
}
}
}

function RootLayout() {
const [isReady, setIsReady] = useState(false)
const { markInteractive } = useObserve()
Expand All @@ -66,10 +92,9 @@ function RootLayout() {
setOnline(!isActuallyOffline(state))
})

const unsubscribe = addNetInfoEventListener((state) => {
return addNetInfoEventListener((state) => {
setOnline(!isActuallyOffline(state))
})
return unsubscribe
})

useEffect(() => {
Expand Down Expand Up @@ -115,27 +140,6 @@ function RootLayout() {
initPlayerQueueStore()

// 桌面歌词权限启动检查
const checkOverlayPermissionOnStart = async () => {
if (Orpheus.isDesktopLyricsShown) {
const hasPermission = await Orpheus.checkOverlayPermission()
if (!hasPermission) {
// 延迟显示,确保 UI 已经加载
setTimeout(() => {
alert(
'桌面歌词',
'检测到桌面歌词已开启,但缺少悬浮窗权限,请授权以恢复显示。',
[
{ text: '取消' },
{
text: '去授权',
onPress: () => Orpheus.requestOverlayPermission(),
},
],
)
}, 1000)
}
}
}
void checkOverlayPermissionOnStart()

// 初始化播放器 Cookie
Expand Down
15 changes: 10 additions & 5 deletions apps/mobile/src/app/download.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,19 @@ export default function DownloadPage() {
taskCount={tasks.length}
retryableCount={tasks.filter(canRetryDownloadTask).length}
onRetryAll={async () => {
await Promise.all(
tasks.filter(canRetryDownloadTask).map((task) => {
const promises = tasks
.filter(canRetryDownloadTask)
.map((task) => {
if (task.state === DownloadState.STOPPED) {
return Orpheus.resumeDownload(task.id)
}
return task.track ? Orpheus.retryDownload(task.track) : undefined
}),
)
if (task.track) {
return Orpheus.retryDownload(task.track)
}
return undefined
})
.filter(Boolean) as Promise<void>[]
await Promise.all(promises)
await queryClient.invalidateQueries({
queryKey: orpheusQueryKeys.downloadTasks(),
})
Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/src/app/history/[date].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export default function DateHistoryPage() {
for (const record of historyRecords) {
const key = record.uniqueKey
if (!trackMap.has(key)) {
trackMap.set(key, { track: record as Track, playCount: 0 })
trackMap.set(key, { track: record, playCount: 0 })
}
trackMap.get(key)!.playCount += 1
duration += record.duration ?? 0
Expand Down
4 changes: 2 additions & 2 deletions apps/mobile/src/app/onboarding.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -170,13 +170,13 @@ export default function OnboardingPage() {
const handleQRCode = useCallback(() => {
storage.set('first_open', false)
setIsClickFinalButton(true)
router.replace('/settings/bilibili-account/qrcode-login' as never)
router.replace('/settings/bilibili-account/qrcode-login')
}, [])

const handlePhone = useCallback(() => {
storage.set('first_open', false)
setIsClickFinalButton(true)
router.replace('/settings/bilibili-account/phone-login' as never)
router.replace('/settings/bilibili-account/phone-login')
}, [])

const animatedRowStyle = useAnimatedStyle(() => ({
Expand Down
6 changes: 6 additions & 0 deletions apps/mobile/src/app/player.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
vec,
} from '@shopify/react-native-skia'
import { useImage } from 'expo-image'
import { useObserve } from 'expo-observe'
import { router } from 'expo-router'
import { useEffect, useMemo, useRef, useState } from 'react'
import {
Expand Down Expand Up @@ -78,6 +79,11 @@ export default function PlayerPage() {
const insets = useSafeAreaInsets()
const pagerRef = useRef<PagerView>(null)
const currentTrack = useCurrentTrack()
const { markInteractive } = useObserve()

useEffect(() => {
markInteractive()
}, [markInteractive])
const currentTrackCover = resolveBilibiliImageUrl(
currentTrack
? resolveTrackCover(currentTrack.uniqueKey, currentTrack.coverUrl)
Expand Down
10 changes: 8 additions & 2 deletions apps/mobile/src/app/playlist/local/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { TrueSheet } from '@lodev09/react-native-true-sheet'
import { and, eq } from 'drizzle-orm'
import { useLiveQuery } from 'drizzle-orm/expo-sqlite'
import { useImage } from 'expo-image'
import { useObserve } from 'expo-observe'
import { useLocalSearchParams, useRouter } from 'expo-router'
import { useDeferredValue, useEffect, useMemo, useRef, useState } from 'react'
import { StyleSheet, View } from 'react-native'
Expand Down Expand Up @@ -153,6 +154,11 @@ export default function LocalPlaylistPage() {
const theme = useTheme()
const { colors } = theme
const router = useRouter()
const { markInteractive } = useObserve()

useEffect(() => {
markInteractive()
}, [markInteractive])
const bbplayerToken = useAppStore((state) => state.bbplayerToken)
const [searchQuery, setSearchQuery] = useState('')
const [startSearch, setStartSearch] = useState(false)
Expand Down Expand Up @@ -342,14 +348,14 @@ export default function LocalPlaylistPage() {
const { expandMultiPageOnSync } = useAppStore.getState().settings
if (expandMultiPageOnSync === null) {
openModal('SyncOptions', {
favoriteId: Number(playlistMetadata.remoteSyncId),
favoriteId: playlistMetadata.remoteSyncId,
})
return
}
openModal(
'FavoriteSyncProgress',
{
favoriteId: Number(playlistMetadata.remoteSyncId),
favoriteId: playlistMetadata.remoteSyncId,
expandMultiPage: expandMultiPageOnSync,
},
{ dismissible: false },
Expand Down
4 changes: 2 additions & 2 deletions apps/mobile/src/app/playlist/recently/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,9 @@ export default function RecentlyPlayedPage() {
toast.error('没有可播放的歌曲')
return
}
const tracks = tracksData.map((item) => item.track)
const playlistTracks = tracksData.map((item) => item.track)
await addToQueue({
tracks,
tracks: playlistTracks,
playNow: true,
clearQueue: true,
playNext: false,
Expand Down
Loading
Loading