列表开始就展示20条,

This commit is contained in:
云上贵猪 2026-03-11 03:52:28 +08:00
parent c5f2641377
commit b77ff62c58
5 changed files with 261 additions and 68 deletions

View File

@ -1,4 +1,5 @@
import { chromium } from 'playwright'
import readlineSync from 'readline-sync'
import { ensureDataFiles, readCache, writeCache, writeStatus } from './lib/cache.js'
const PAGE_URL = 'https://www1.gdtv.cn/tvColumn/768'
@ -73,10 +74,7 @@ async function getPageScrollInfo(page) {
return await page.evaluate(() => {
return {
scrollTop: window.scrollY || document.documentElement.scrollTop || document.body.scrollTop || 0,
scrollHeight: Math.max(
document.body.scrollHeight,
document.documentElement.scrollHeight
),
scrollHeight: Math.max(document.body.scrollHeight, document.documentElement.scrollHeight),
innerHeight: window.innerHeight
}
})
@ -153,7 +151,12 @@ async function clickLoadMoreAndWait(page) {
return !!response
}
async function autoCollectByScrollAndClick(page, maxRounds = 120) {
function askContinue() {
const answer = readlineSync.question('\n页面已滑到底部继续采集吗(y/n): ')
return answer.trim().toLowerCase() === 'y'
}
async function autoCollectByScrollAndClick(page, maxRounds = 150) {
let round = 0
let noChangeRounds = 0
let lastDomCount = await getDomItemCount(page)
@ -214,15 +217,31 @@ async function autoCollectByScrollAndClick(page, maxRounds = 120) {
await sleep(1800)
const retryButton = await findLoadMoreButton(page)
if (!retryButton) {
noChangeRounds += 1
if (retryButton) {
log('到底部后重新检测到“加载更多”按钮,继续点击')
continue
}
const shouldContinue = askContinue()
if (!shouldContinue) {
log('你选择结束采集')
break
}
log('你选择继续采集,尝试再次下滑检测')
noChangeRounds = 0
await sleep(1000)
}
if (noChangeRounds >= 6) {
log('连续多轮没有新内容,停止采集')
const shouldContinue = askContinue()
if (!shouldContinue) {
log('连续多轮没有新内容,且你选择结束采集')
break
}
log('你选择继续采集,重置无变化计数')
noChangeRounds = 0
}
}
log(`自动采集流程结束,共执行 ${round}`)

View File

@ -1,7 +1,5 @@
{
"running": true,
"lastMessage": "已采集第 2 页",
"updatedAt": "2026-03-10T19:14:36.004Z",
"capturedPages": 26,
"totalItems": 1141
"running": false,
"lastMessage": "自动采集完成",
"updatedAt": "2026-03-10T19:27:40.184Z"
}

View File

@ -1,170 +1,170 @@
{
"updatedAt": "2026-03-10T19:14:35.996Z",
"updatedAt": "2026-03-10T19:27:32.690Z",
"name": "七十二家房客",
"coverUrl": "https://p2-grtn.itouchtv.cn/image/20191010/0.15048946623517580e4543e24e68e4824OSS1570677193.jpg",
"displayType": 0,
"pages": {
"1": {
"currentPage": 1,
"beginScore": 1669824000000,
"beginScore": 1730390400000,
"count": 40,
"capturedAt": "2026-03-10T19:13:55.281Z"
"capturedAt": "2026-03-10T19:27:28.717Z"
},
"2": {
"currentPage": 2,
"beginScore": 1669824000000,
"count": 40,
"capturedAt": "2026-03-10T19:14:35.996Z"
"beginScore": 1730390400000,
"count": 20,
"capturedAt": "2026-03-10T19:27:31.309Z"
},
"3": {
"currentPage": 3,
"beginScore": 1701360000000,
"beginScore": 1730390400000,
"count": 0,
"capturedAt": "2026-03-10T19:13:21.335Z"
"capturedAt": "2026-03-10T19:27:32.690Z"
},
"4": {
"currentPage": 4,
"beginScore": 1763561820000,
"count": 40,
"capturedAt": "2026-03-10T19:11:26.676Z"
"capturedAt": "2026-03-10T19:25:11.140Z"
},
"5": {
"currentPage": 5,
"beginScore": 1762962000000,
"count": 40,
"capturedAt": "2026-03-10T19:11:29.024Z"
"capturedAt": "2026-03-10T19:25:13.417Z"
},
"6": {
"currentPage": 6,
"beginScore": 1762352220000,
"count": 40,
"capturedAt": "2026-03-10T19:11:33.591Z"
"capturedAt": "2026-03-10T19:25:18.001Z"
},
"7": {
"currentPage": 7,
"beginScore": 1761746220000,
"count": 40,
"capturedAt": "2026-03-10T19:11:36.420Z"
"capturedAt": "2026-03-10T19:25:20.845Z"
},
"8": {
"currentPage": 8,
"beginScore": 1761056280000,
"count": 40,
"capturedAt": "2026-03-10T19:11:39.251Z"
"capturedAt": "2026-03-10T19:25:23.655Z"
},
"9": {
"currentPage": 9,
"beginScore": 1740315600000,
"count": 40,
"capturedAt": "2026-03-10T19:11:41.578Z"
"capturedAt": "2026-03-10T19:25:26.285Z"
},
"10": {
"currentPage": 10,
"beginScore": 1738414800000,
"count": 40,
"capturedAt": "2026-03-10T19:11:44.739Z"
"capturedAt": "2026-03-10T19:25:29.425Z"
},
"11": {
"currentPage": 11,
"beginScore": 1736514000000,
"count": 40,
"capturedAt": "2026-03-10T19:11:47.585Z"
"capturedAt": "2026-03-10T19:25:32.213Z"
},
"12": {
"currentPage": 12,
"beginScore": 1734526800000,
"count": 40,
"capturedAt": "2026-03-10T19:11:50.449Z"
"capturedAt": "2026-03-10T19:25:35.060Z"
},
"13": {
"currentPage": 13,
"beginScore": 1732714295000,
"count": 40,
"capturedAt": "2026-03-10T19:11:52.794Z"
"capturedAt": "2026-03-10T19:25:37.402Z"
},
"14": {
"currentPage": 14,
"beginScore": 1730986358000,
"count": 40,
"capturedAt": "2026-03-10T19:11:57.376Z"
"capturedAt": "2026-03-10T19:25:42.036Z"
},
"15": {
"currentPage": 15,
"beginScore": 1729171974000,
"count": 40,
"capturedAt": "2026-03-10T19:12:00.272Z"
"capturedAt": "2026-03-10T19:25:44.872Z"
},
"16": {
"currentPage": 16,
"beginScore": 1727442000000,
"count": 40,
"capturedAt": "2026-03-10T19:12:03.129Z"
"capturedAt": "2026-03-10T19:25:47.704Z"
},
"17": {
"currentPage": 17,
"beginScore": 1725713999999,
"count": 40,
"capturedAt": "2026-03-10T19:12:05.452Z"
"capturedAt": "2026-03-10T19:25:50.068Z"
},
"18": {
"currentPage": 18,
"beginScore": 1723899600000,
"count": 40,
"capturedAt": "2026-03-10T19:12:10.101Z"
"capturedAt": "2026-03-10T19:25:54.781Z"
},
"19": {
"currentPage": 19,
"beginScore": 1721998800000,
"count": 40,
"capturedAt": "2026-03-10T19:12:12.989Z"
"capturedAt": "2026-03-10T19:25:57.635Z"
},
"20": {
"currentPage": 20,
"beginScore": 1720186339000,
"count": 40,
"capturedAt": "2026-03-10T19:12:15.860Z"
"capturedAt": "2026-03-10T19:26:00.455Z"
},
"21": {
"currentPage": 21,
"beginScore": 1718371929000,
"count": 40,
"capturedAt": "2026-03-10T19:12:18.172Z"
"capturedAt": "2026-03-10T19:26:02.811Z"
},
"22": {
"currentPage": 22,
"beginScore": 1716641999999,
"count": 40,
"capturedAt": "2026-03-10T19:12:21.399Z"
"capturedAt": "2026-03-10T19:26:06.118Z"
},
"23": {
"currentPage": 23,
"beginScore": 1714827600000,
"count": 40,
"capturedAt": "2026-03-10T19:12:24.238Z"
"capturedAt": "2026-03-10T19:26:08.954Z"
},
"24": {
"currentPage": 24,
"beginScore": 1713099599999,
"count": 40,
"capturedAt": "2026-03-10T19:12:27.289Z"
"capturedAt": "2026-03-10T19:26:11.917Z"
},
"25": {
"currentPage": 25,
"beginScore": 1711372007000,
"count": 40,
"capturedAt": "2026-03-10T19:12:29.500Z"
"capturedAt": "2026-03-10T19:26:14.215Z"
},
"26": {
"currentPage": 26,
"beginScore": 1709559052000,
"count": 0,
"capturedAt": "2026-03-10T19:12:34.177Z"
"capturedAt": "2026-03-10T19:26:19.076Z"
}
},
"beginScoreMap": {
"1": 1669824000000,
"2": 1669824000000,
"3": 1701360000000,
"1": 1730390400000,
"2": 1730390400000,
"3": 1730390400000,
"4": 1763561820000,
"5": 1762962000000,
"6": 1762352220000,

View File

@ -1,11 +1,17 @@
<template>
<div class="page">
<div class="header">
<div>
<h1>{{ columnInfo.name || '七十二家房客播放器' }}</h1>
<p>缓存 {{ episodeList.length }} </p>
<p> {{ allEpisodeList.length }} 当前显示 {{ visibleEpisodeList.length }} </p>
<p class="sub">最后更新{{ updatedAtText }}</p>
</div>
<div class="header-actions">
<button class="action-btn" @click="fetchList">刷新数据</button>
</div>
</div>
<div class="layout">
<div class="player-panel">
<video
@ -24,23 +30,31 @@
</p>
</div>
<div v-if="errorMsg" class="error">{{ errorMsg }}</div>
<div v-if="errorMsg" class="error">
{{ errorMsg }}
</div>
</div>
<div class="list-panel">
<div
v-for="item in episodeList"
v-for="item in visibleEpisodeList"
:key="item.id"
class="episode-item"
:class="{ active: currentEpisode?.id === item.id }"
@click="playEpisode(item)"
>
<img :src="item.coverUrl" alt="" />
<img :src="item.coverUrl" alt="" loading="lazy" />
<div class="episode-text">
<div class="title">{{ item.title }}</div>
<div class="meta">{{ formatDate(item.releasedAt) }}</div>
</div>
</div>
<div class="load-more-wrap" v-if="visibleEpisodeList.length < allEpisodeList.length">
<button class="load-more-btn" @click="loadMoreEpisodes">
加载更多
</button>
</div>
</div>
</div>
</div>
@ -59,33 +73,112 @@ const columnInfo = reactive({
coverUrl: ''
})
const episodeList = ref<EpisodeData[]>([])
const allEpisodeList = ref<EpisodeData[]>([])
const visibleEpisodeList = ref<EpisodeData[]>([])
const currentEpisode = ref<EpisodeData | null>(null)
const errorMsg = ref('')
const updatedAt = ref<string | null>(null)
const pageSize = 20
const currentVisibleCount = ref(pageSize)
const updatedAtText = computed(() => {
if (!updatedAt.value) return '暂无缓存'
return new Date(updatedAt.value).toLocaleString('zh-CN')
})
function setCookie(name: string, value: string, days = 30) {
const expires = new Date()
expires.setTime(expires.getTime() + days * 24 * 60 * 60 * 1000)
document.cookie = `${name}=${encodeURIComponent(value)}; expires=${expires.toUTCString()}; path=/`
}
function getCookie(name: string): string | null {
const nameEQ = `${name}=`
const cookies = document.cookie.split(';')
for (let cookie of cookies) {
cookie = cookie.trim()
if (cookie.indexOf(nameEQ) === 0) {
return decodeURIComponent(cookie.substring(nameEQ.length))
}
}
return null
}
function updateVisibleList() {
visibleEpisodeList.value = allEpisodeList.value.slice(0, currentVisibleCount.value)
}
function loadMoreEpisodes() {
currentVisibleCount.value += pageSize
updateVisibleList()
}
function destroyHls(): void {
if (hlsRef.value) {
hlsRef.value.destroy()
hlsRef.value = null
}
if (videoRef.value) {
videoRef.value.ontimeupdate = null
videoRef.value.onloadedmetadata = null
}
}
function bindProgressSave(video: HTMLVideoElement, item: EpisodeData) {
video.ontimeupdate = () => {
setCookie(`qishier_progress_${item.id}`, String(Math.floor(video.currentTime)), 30)
}
}
function restoreProgress(video: HTMLVideoElement, item: EpisodeData) {
const savedTime = getCookie(`qishier_progress_${item.id}`)
if (savedTime && !Number.isNaN(Number(savedTime))) {
video.currentTime = Number(savedTime)
}
}
async function fetchList(): Promise<void> {
try {
errorMsg.value = ''
const res = await getAllQishierList()
const data = res.data
console.log('前端拿到缓存:', {
updatedAt: data.updatedAt,
total: data.total,
pages: Object.keys(data.pages || {}).length,
listLength: Array.isArray(data.list) ? data.list.length : 0
})
columnInfo.name = data.name || '七十二家房客'
columnInfo.coverUrl = data.coverUrl || ''
updatedAt.value = data.updatedAt || null
episodeList.value = Array.isArray(data.list) ? data.list : []
if (episodeList.value.length > 0) {
await playEpisode(episodeList.value[0])
allEpisodeList.value = Array.isArray(data.list) ? data.list : []
currentVisibleCount.value = pageSize
updateVisibleList()
if (allEpisodeList.value.length > 0) {
const savedEpisodeId = getCookie('qishier_current_episode_id')
const savedEpisode = allEpisodeList.value.find((item) => item.id === savedEpisodeId)
if (savedEpisode) {
const index = allEpisodeList.value.findIndex((item) => item.id === savedEpisode.id)
if (index >= 0 && index + 1 > currentVisibleCount.value) {
currentVisibleCount.value = Math.ceil((index + 1) / pageSize) * pageSize
updateVisibleList()
}
await playEpisode(savedEpisode)
} else {
await playEpisode(allEpisodeList.value[0])
}
} else {
errorMsg.value = '缓存里还没有节目数据,请先运行采集脚本'
}
} catch (error: any) {
console.error('fetchList error:', error)
errorMsg.value = error?.message || '读取缓存失败'
}
}
@ -93,6 +186,7 @@ async function fetchList(): Promise<void> {
async function playEpisode(item: EpisodeData): Promise<void> {
currentEpisode.value = item
errorMsg.value = ''
setCookie('qishier_current_episode_id', item.id, 30)
await nextTick()
@ -104,6 +198,12 @@ async function playEpisode(item: EpisodeData): Promise<void> {
if (video.canPlayType('application/vnd.apple.mpegurl')) {
video.src = item.videoUrl
video.load()
video.onloadedmetadata = () => {
restoreProgress(video, item)
}
bindProgressSave(video, item)
void video.play().catch(() => {})
return
}
@ -114,6 +214,8 @@ async function playEpisode(item: EpisodeData): Promise<void> {
hls.attachMedia(video)
hls.on(Hls.Events.MANIFEST_PARSED, () => {
restoreProgress(video, item)
bindProgressSave(video, item)
void video.play().catch(() => {})
})
@ -127,13 +229,6 @@ async function playEpisode(item: EpisodeData): Promise<void> {
}
}
function destroyHls(): void {
if (hlsRef.value) {
hlsRef.value.destroy()
hlsRef.value = null
}
}
function formatDate(timestamp: number): string {
if (!timestamp) return '--'
const d = new Date(timestamp)
@ -166,45 +261,93 @@ onBeforeUnmount(() => {
background: #111;
color: #fff;
}
.header {
margin-bottom: 20px;
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 16px;
}
.header h1 {
margin: 0 0 8px;
}
.sub {
.header p {
margin: 0 0 6px;
color: #999;
}
.sub {
font-size: 13px;
}
.header-actions {
display: flex;
gap: 10px;
}
.action-btn {
padding: 10px 16px;
border: none;
border-radius: 8px;
background: #333;
color: #fff;
cursor: pointer;
}
.action-btn:hover {
background: #444;
}
.layout {
display: grid;
grid-template-columns: 1.6fr 1fr;
gap: 20px;
}
.player-panel,
.list-panel {
background: #1b1b1b;
border-radius: 12px;
overflow: hidden;
}
.video {
width: 100%;
aspect-ratio: 16 / 9;
background: #000;
display: block;
}
.info {
padding: 16px;
}
.info h2 {
margin: 0 0 10px;
font-size: 20px;
line-height: 1.5;
}
.info p {
margin: 0;
color: #aaa;
font-size: 14px;
}
.error {
padding: 0 16px 16px;
color: #ff7875;
}
.list-panel {
max-height: 80vh;
overflow-y: auto;
padding: 12px;
}
.episode-item {
display: flex;
gap: 12px;
@ -212,13 +355,17 @@ onBeforeUnmount(() => {
border-radius: 10px;
cursor: pointer;
margin-bottom: 10px;
transition: 0.2s;
}
.episode-item:hover {
background: rgba(255, 255, 255, 0.06);
}
.episode-item.active {
background: rgba(255, 193, 7, 0.16);
}
.episode-item img {
width: 120px;
height: 68px;
@ -226,23 +373,49 @@ onBeforeUnmount(() => {
border-radius: 8px;
flex-shrink: 0;
}
.episode-text {
flex: 1;
min-width: 0;
}
.title {
font-size: 15px;
line-height: 1.5;
margin-bottom: 8px;
font-weight: 600;
}
.meta {
font-size: 13px;
color: #999;
}
.load-more-wrap {
padding: 12px 0 4px;
text-align: center;
}
.load-more-btn {
padding: 10px 18px;
border: none;
border-radius: 8px;
background: #333;
color: #fff;
cursor: pointer;
}
.load-more-btn:hover {
background: #444;
}
@media (max-width: 900px) {
.layout {
grid-template-columns: 1fr;
}
.header {
flex-direction: column;
}
}
</style>

View File

@ -10,11 +10,14 @@ export default defineConfig({
}
},
server: {
port: 5173,
host: true,
open: true,
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true
}
}
}
},
},
})