const API_BASE = 'https://tiktok.xvideos4.tk/api.php'; const NAV = [ { sort: 'daily', label: '今日', href: '/zh-CN/', icon: '🔥' }, { sort: 'weekly', label: '本周', href: '/zh-CN/weekly/', icon: '📅' }, { sort: 'monthly', label: '本月', href: '/zh-CN/monthly/',icon: '🗓' }, { sort: 'favorite', label: '全部', href: '/zh-CN/all/', icon: '✨' }, ]; const CATEGORIES = [ { id: 'gay', name: '男同性恋・男娘' }, { id: 'anime', name: '动漫 / 二次元' }, { id: 'lolita', name: '少女系' }, { id: 'shaved', name: '光滑风格' }, { id: 'kyonyu', name: '丰胸' }, { id: 'jk', name: '女高中生' }, { id: 'beautiful-girl', name: '美少女' }, { id: 'small-breasts', name: '小胸' }, { id: 'sm', name: 'SM 题材' }, { id: 'masturbation', name: '自我表达' }, { id: 'hamedori', name: '自摄' }, { id: 'female-pervert', name: '大胆女性' }, { id: 'personal-filming',name: '私人拍摄' }, { id: 'outdoor', name: '户外场景' }, { id: 'big-sister', name: '姐姐' }, { id: 'incest', name: '家庭主题' }, { id: 'married-woman', name: '已婚女性' }, { id: 'orgy', name: '群体场景' }, { id: 'fellatio', name: '口部艺术' }, { id: 'bukkake', name: '泼洒艺术' }, ]; async function apiList({ sort, category, page, q }) { const qp = new URLSearchParams({ action: 'list', sort: sort || 'daily', page: String(page || 1), }); if (category) qp.set('category', category); if (q) qp.set('q', q); const res = await fetch(`${API_BASE}?${qp}`, { headers: { 'Accept': 'application/json' } }); if (!res.ok) return null; return res.json().catch(() => null); } async function apiDetail(id) { const qpQ = new URLSearchParams({ action: 'list', sort: 'favorite', page: '1', q: id }); const res = await fetch(`${API_BASE}?${qpQ}`, { headers: { 'Accept': 'application/json' } }); if (!res.ok) return null; const data = await res.json().catch(() => null); if (!data?.items?.length) return null; return data.items.find(i => i.id === id) || data.items[0] || null; } function normalizeItem(item) { const videoUrl = item.encodedUrl || item.url || item.videoUrl || item.directUrl || item.src || item.mp4Url || item.mp4 || item.m3u8Url || item.m3u8 || ''; return { id: item.id || '', title: item.title || '未知', thumb: item.thumb || '', videoUrl, time: item.time || '00:00', pv: String(item.pv || 0), favorite: String(item.favorite || 0), }; } function isHlsUrl(url) { if (!url) return false; return /\.m3u8|playlist|\/HLS\/|\/hls\//i.test(url); } function isMp4Url(url) { if (!url) return false; return /\.mp4(\?|$)|\.webm(\?|$)|\.ogg(\?|$)/i.test(url); } function jsonResponse(data, status = 200) { return new Response(JSON.stringify(data), { status, headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache', }, }); } export default { async fetch(request) { const url = new URL(request.url); const path = url.pathname; const params = url.searchParams; if (request.method === 'OPTIONS') { return new Response(null, { headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, HEAD, OPTIONS', 'Access-Control-Allow-Headers': 'Range, Content-Type', } }); } try { if (path === '/play') return handlePlay(params); if (path === '/api/resolve') return handleResolve(params); if (path === '/api/refresh') return handleRefresh(params.get('id') || ''); if (path.match(/\/movie\//)) { const id = path.split('/').filter(Boolean).pop(); return handleDetail(id); } if (path === '/search') { const q = params.get('tag') || params.get('q') || ''; const page = parseInt(params.get('page') || '1', 10); return handleList({ sort: 'favorite', category: '', q, page, isSearch: true }); } const catMatch = path.match(/\/category\/([^\/\s?]+)/); if (catMatch) { const page = parseInt(params.get('page') || '1', 10); return handleList({ sort: 'favorite', category: catMatch[1], page }); } const sortMap = { '/zh-cn/weekly': 'weekly', '/zh-CN/weekly': 'weekly', '/zh-cn/weekly/': 'weekly', '/zh-CN/weekly/': 'weekly', '/zh-cn/monthly': 'monthly', '/zh-CN/monthly': 'monthly', '/zh-cn/monthly/':'monthly', '/zh-CN/monthly/':'monthly', '/zh-cn/all': 'favorite','/zh-CN/all': 'favorite', '/zh-cn/all/': 'favorite','/zh-CN/all/': 'favorite', }; const sort = sortMap[path] || 'daily'; const page = parseInt(params.get('page') || '1', 10); return handleList({ sort, category: '', page }); } catch (e) { return new Response('Error: ' + e.message + '\n' + e.stack, { status: 500 }); } } }; async function handleResolve(params) { const rawUrl = params.get('url') || ''; if (!rawUrl || !rawUrl.startsWith('http')) { return jsonResponse({ error: 'invalid_url' }, 400); } let origin = ''; try { origin = new URL(rawUrl).origin; } catch (_) {} const commonHeaders = { 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) ' + 'AppleWebKit/605.1.15 (KHTML, like Gecko) ' + 'Version/17.0 Mobile/15E148 Safari/604.1', 'Referer': origin ? origin + '/' : 'https://chaturbate.xvideos4.tk/', 'Origin': origin || 'https://chaturbate.xvideos4.tk', 'Accept': 'video/mp4,video/webm,application/x-mpegurl,*/*;q=0.9', }; try { const ctrl = new AbortController(); const timer = setTimeout(() => ctrl.abort(), 8000); const res = await fetch(rawUrl, { method: 'HEAD', redirect: 'follow', signal: ctrl.signal, headers: commonHeaders, }); clearTimeout(timer); const ct = res.headers.get('content-type') || ''; const finalUrl = res.url || rawUrl; const isHls = ct.includes('mpegurl') || ct.includes('x-mpegurl') || isHlsUrl(finalUrl); const isMp4 = ct.startsWith('video/') || isMp4Url(finalUrl); return jsonResponse({ url: finalUrl, isHls, isMp4, contentType: ct, status: res.status }); } catch (headErr) { try { const ctrl2 = new AbortController(); const timer2 = setTimeout(() => ctrl2.abort(), 8000); const res2 = await fetch(rawUrl, { method: 'GET', redirect: 'follow', signal: ctrl2.signal, headers: commonHeaders, }); clearTimeout(timer2); const ct2 = res2.headers.get('content-type') || ''; const finalUrl2 = res2.url || rawUrl; const isHls2 = ct2.includes('mpegurl') || ct2.includes('x-mpegurl') || isHlsUrl(finalUrl2); const isMp42 = ct2.startsWith('video/') || isMp4Url(finalUrl2); return jsonResponse({ url: finalUrl2, isHls: isHls2, isMp4: isMp42, contentType: ct2, status: res2.status }); } catch (getErr) { return jsonResponse({ url: rawUrl, isHls: isHlsUrl(rawUrl), isMp4: isMp4Url(rawUrl), error: getErr.message, }); } } } async function handleRefresh(id) { if (!id) return jsonResponse({ error: 'no id' }, 400); try { const raw = await apiDetail(id); if (!raw) return jsonResponse({ error: 'not_found' }, 404); const proxySrc = raw.encodedUrl || raw.url || raw.videoUrl || raw.directUrl || ''; if (!proxySrc) return jsonResponse({ error: 'no_url' }, 404); return jsonResponse({ proxySrc, isHls: isHlsUrl(proxySrc) }); } catch (e) { return jsonResponse({ error: e.message }, 500); } } async function handleList({ sort, category, q, page, isSearch }) { const data = await apiList({ sort, category, q, page }); const movies = (data?.items || []).map(normalizeItem); const lastPage = data?.lastPage || page; const catLabel = CATEGORIES.find(c => c.id === category)?.name || category; const title = category ? `# ${catLabel}` : (isSearch ? `搜索:${q}` : (NAV.find(n => n.sort === sort)?.label || '今日')); return new Response( renderLayout(renderList(movies, { sort, category, q, page, lastPage, isSearch }), title, sort), { headers: { 'Content-Type': 'text/html;charset=UTF-8' } } ); } async function handleDetail(id) { const raw = await apiDetail(id); const movie = raw ? normalizeItem(raw) : { id, title: id, thumb: '', videoUrl: '', time: '00:00', pv: '0', favorite: '0' }; return new Response( renderLayout(renderDetail(movie), movie.title, ''), { headers: { 'Content-Type': 'text/html;charset=UTF-8' } } ); } async function handlePlay(params) { const videoUrl = params.get('v') || ''; if (!videoUrl || !videoUrl.startsWith('http')) { return new Response('无效的播放链接', { status: 400 }); } return new Response(null, { status: 302, headers: { Location: videoUrl } }); } function renderList(movies, { sort, category, q, page, lastPage, isSearch }) { const grid = movies.map(m => { const thumb = m.thumb || 'https://placehold.co/300x533/111/333?text=No+Image'; // 直接跳转 encodedUrl const playHref = m.videoUrl ? m.videoUrl : `/zh-CN/movie/${encodeURIComponent(m.id)}`; const playTarget = m.videoUrl ? ' target="_blank" rel="noopener noreferrer"' : ''; return `
`; }).join(''); function pageUrl(p) { if (isSearch) return `/search?tag=${encodeURIComponent(q || category || '')}&page=${p}`; if (category) return `/zh-CN/category/${encodeURIComponent(category)}?page=${p}`; return (NAV.find(n => n.sort === sort)?.href || '/zh-CN/') + `?page=${p}`; } const pagination = (page > 1 || page < lastPage) ? ` ` : ''; const catChips = CATEGORIES.map(c => `${esc(c.name)}` ).join(''); const empty = movies.length === 0 ? `视频链接不可用