列表开始就展示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 { chromium } from 'playwright'
import readlineSync from 'readline-sync'
import { ensureDataFiles, readCache, writeCache, writeStatus } from './lib/cache.js' import { ensureDataFiles, readCache, writeCache, writeStatus } from './lib/cache.js'
const PAGE_URL = 'https://www1.gdtv.cn/tvColumn/768' const PAGE_URL = 'https://www1.gdtv.cn/tvColumn/768'
@ -73,10 +74,7 @@ async function getPageScrollInfo(page) {
return await page.evaluate(() => { return await page.evaluate(() => {
return { return {
scrollTop: window.scrollY || document.documentElement.scrollTop || document.body.scrollTop || 0, scrollTop: window.scrollY || document.documentElement.scrollTop || document.body.scrollTop || 0,
scrollHeight: Math.max( scrollHeight: Math.max(document.body.scrollHeight, document.documentElement.scrollHeight),
document.body.scrollHeight,
document.documentElement.scrollHeight
),
innerHeight: window.innerHeight innerHeight: window.innerHeight
} }
}) })
@ -153,7 +151,12 @@ async function clickLoadMoreAndWait(page) {
return !!response 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 round = 0
let noChangeRounds = 0 let noChangeRounds = 0
let lastDomCount = await getDomItemCount(page) let lastDomCount = await getDomItemCount(page)
@ -214,14 +217,30 @@ async function autoCollectByScrollAndClick(page, maxRounds = 120) {
await sleep(1800) await sleep(1800)
const retryButton = await findLoadMoreButton(page) const retryButton = await findLoadMoreButton(page)
if (!retryButton) { if (retryButton) {
noChangeRounds += 1 log('到底部后重新检测到“加载更多”按钮,继续点击')
continue
} }
const shouldContinue = askContinue()
if (!shouldContinue) {
log('你选择结束采集')
break
}
log('你选择继续采集,尝试再次下滑检测')
noChangeRounds = 0
await sleep(1000)
} }
if (noChangeRounds >= 6) { if (noChangeRounds >= 6) {
log('连续多轮没有新内容,停止采集') const shouldContinue = askContinue()
break if (!shouldContinue) {
log('连续多轮没有新内容,且你选择结束采集')
break
}
log('你选择继续采集,重置无变化计数')
noChangeRounds = 0
} }
} }

View File

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

View File

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

View File

@ -1,9 +1,15 @@
<template> <template>
<div class="page"> <div class="page">
<div class="header"> <div class="header">
<h1>{{ columnInfo.name || '七十二家房客播放器' }}</h1> <div>
<p>缓存 {{ episodeList.length }} </p> <h1>{{ columnInfo.name || '七十二家房客播放器' }}</h1>
<p class="sub">最后更新{{ updatedAtText }}</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>
<div class="layout"> <div class="layout">
@ -24,23 +30,31 @@
</p> </p>
</div> </div>
<div v-if="errorMsg" class="error">{{ errorMsg }}</div> <div v-if="errorMsg" class="error">
{{ errorMsg }}
</div>
</div> </div>
<div class="list-panel"> <div class="list-panel">
<div <div
v-for="item in episodeList" v-for="item in visibleEpisodeList"
:key="item.id" :key="item.id"
class="episode-item" class="episode-item"
:class="{ active: currentEpisode?.id === item.id }" :class="{ active: currentEpisode?.id === item.id }"
@click="playEpisode(item)" @click="playEpisode(item)"
> >
<img :src="item.coverUrl" alt="" /> <img :src="item.coverUrl" alt="" loading="lazy" />
<div class="episode-text"> <div class="episode-text">
<div class="title">{{ item.title }}</div> <div class="title">{{ item.title }}</div>
<div class="meta">{{ formatDate(item.releasedAt) }}</div> <div class="meta">{{ formatDate(item.releasedAt) }}</div>
</div> </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> </div>
</div> </div>
@ -59,33 +73,112 @@ const columnInfo = reactive({
coverUrl: '' coverUrl: ''
}) })
const episodeList = ref<EpisodeData[]>([]) const allEpisodeList = ref<EpisodeData[]>([])
const visibleEpisodeList = ref<EpisodeData[]>([])
const currentEpisode = ref<EpisodeData | null>(null) const currentEpisode = ref<EpisodeData | null>(null)
const errorMsg = ref('') const errorMsg = ref('')
const updatedAt = ref<string | null>(null) const updatedAt = ref<string | null>(null)
const pageSize = 20
const currentVisibleCount = ref(pageSize)
const updatedAtText = computed(() => { const updatedAtText = computed(() => {
if (!updatedAt.value) return '暂无缓存' if (!updatedAt.value) return '暂无缓存'
return new Date(updatedAt.value).toLocaleString('zh-CN') 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> { async function fetchList(): Promise<void> {
try { try {
errorMsg.value = '' errorMsg.value = ''
const res = await getAllQishierList() const res = await getAllQishierList()
const data = res.data 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.name = data.name || '七十二家房客'
columnInfo.coverUrl = data.coverUrl || '' columnInfo.coverUrl = data.coverUrl || ''
updatedAt.value = data.updatedAt || null updatedAt.value = data.updatedAt || null
episodeList.value = Array.isArray(data.list) ? data.list : []
if (episodeList.value.length > 0) { allEpisodeList.value = Array.isArray(data.list) ? data.list : []
await playEpisode(episodeList.value[0]) 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 { } else {
errorMsg.value = '缓存里还没有节目数据,请先运行采集脚本' errorMsg.value = '缓存里还没有节目数据,请先运行采集脚本'
} }
} catch (error: any) { } catch (error: any) {
console.error('fetchList error:', error)
errorMsg.value = error?.message || '读取缓存失败' errorMsg.value = error?.message || '读取缓存失败'
} }
} }
@ -93,6 +186,7 @@ async function fetchList(): Promise<void> {
async function playEpisode(item: EpisodeData): Promise<void> { async function playEpisode(item: EpisodeData): Promise<void> {
currentEpisode.value = item currentEpisode.value = item
errorMsg.value = '' errorMsg.value = ''
setCookie('qishier_current_episode_id', item.id, 30)
await nextTick() await nextTick()
@ -104,6 +198,12 @@ async function playEpisode(item: EpisodeData): Promise<void> {
if (video.canPlayType('application/vnd.apple.mpegurl')) { if (video.canPlayType('application/vnd.apple.mpegurl')) {
video.src = item.videoUrl video.src = item.videoUrl
video.load() video.load()
video.onloadedmetadata = () => {
restoreProgress(video, item)
}
bindProgressSave(video, item)
void video.play().catch(() => {}) void video.play().catch(() => {})
return return
} }
@ -114,6 +214,8 @@ async function playEpisode(item: EpisodeData): Promise<void> {
hls.attachMedia(video) hls.attachMedia(video)
hls.on(Hls.Events.MANIFEST_PARSED, () => { hls.on(Hls.Events.MANIFEST_PARSED, () => {
restoreProgress(video, item)
bindProgressSave(video, item)
void video.play().catch(() => {}) 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 { function formatDate(timestamp: number): string {
if (!timestamp) return '--' if (!timestamp) return '--'
const d = new Date(timestamp) const d = new Date(timestamp)
@ -166,45 +261,93 @@ onBeforeUnmount(() => {
background: #111; background: #111;
color: #fff; color: #fff;
} }
.header { .header {
margin-bottom: 20px; margin-bottom: 20px;
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 16px;
} }
.header h1 { .header h1 {
margin: 0 0 8px; margin: 0 0 8px;
} }
.sub {
.header p {
margin: 0 0 6px;
color: #999; color: #999;
}
.sub {
font-size: 13px; 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 { .layout {
display: grid; display: grid;
grid-template-columns: 1.6fr 1fr; grid-template-columns: 1.6fr 1fr;
gap: 20px; gap: 20px;
} }
.player-panel, .player-panel,
.list-panel { .list-panel {
background: #1b1b1b; background: #1b1b1b;
border-radius: 12px; border-radius: 12px;
overflow: hidden; overflow: hidden;
} }
.video { .video {
width: 100%; width: 100%;
aspect-ratio: 16 / 9; aspect-ratio: 16 / 9;
background: #000; background: #000;
display: block; display: block;
} }
.info { .info {
padding: 16px; 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 { .error {
padding: 0 16px 16px; padding: 0 16px 16px;
color: #ff7875; color: #ff7875;
} }
.list-panel { .list-panel {
max-height: 80vh; max-height: 80vh;
overflow-y: auto; overflow-y: auto;
padding: 12px; padding: 12px;
} }
.episode-item { .episode-item {
display: flex; display: flex;
gap: 12px; gap: 12px;
@ -212,13 +355,17 @@ onBeforeUnmount(() => {
border-radius: 10px; border-radius: 10px;
cursor: pointer; cursor: pointer;
margin-bottom: 10px; margin-bottom: 10px;
transition: 0.2s;
} }
.episode-item:hover { .episode-item:hover {
background: rgba(255, 255, 255, 0.06); background: rgba(255, 255, 255, 0.06);
} }
.episode-item.active { .episode-item.active {
background: rgba(255, 193, 7, 0.16); background: rgba(255, 193, 7, 0.16);
} }
.episode-item img { .episode-item img {
width: 120px; width: 120px;
height: 68px; height: 68px;
@ -226,23 +373,49 @@ onBeforeUnmount(() => {
border-radius: 8px; border-radius: 8px;
flex-shrink: 0; flex-shrink: 0;
} }
.episode-text { .episode-text {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
} }
.title { .title {
font-size: 15px; font-size: 15px;
line-height: 1.5; line-height: 1.5;
margin-bottom: 8px; margin-bottom: 8px;
font-weight: 600; font-weight: 600;
} }
.meta { .meta {
font-size: 13px; font-size: 13px;
color: #999; 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) { @media (max-width: 900px) {
.layout { .layout {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.header {
flex-direction: column;
}
} }
</style> </style>

View File

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