const API_BASE = 'https://x.xvideos4.tk/api.php/v1/records'; const API_KEY = 'text'; const NAV = [ { sort: 'daily', label: '今日', href: '/zh-CN/', icon: '🔥', range: 'today' }, { sort: 'weekly', label: '本周', href: '/zh-CN/weekly/', icon: '📅', range: 'week' }, { sort: 'monthly', label: '本月', href: '/zh-CN/monthly/',icon: '🗓', range: 'month' }, { sort: 'favorite', label: '收藏', href: '/zh-CN/all/', icon: '⭐', range: 'all' }, ]; 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(options) { const sort = options.sort; const category = options.category; const page = options.page; const q = options.q; const navItem = NAV.find(function(n) { return n.sort === sort; }) || NAV[0]; const qp = new URLSearchParams({ api_key: API_KEY, range: navItem.range, type: 'video', page: String(page || 1), limit: '20' }); let keyword = ''; if (q) { keyword = q; qp.set('q', q); } else if (category) { const cat = CATEGORIES.find(function(c) { return c.id === category; }); keyword = cat ? cat.name : category; qp.set('q', keyword); } const res = await fetch(API_BASE + '?' + qp.toString(), { headers: { 'Accept': 'application/json' } }); if (!res.ok) return null; const json = await res.json().catch(function() { return null; }); if (json && json.data && keyword) { json.data = json.data.filter(function(item) { return (item.tweet_text && item.tweet_text.indexOf(keyword) !== -1) || (item.author_name && item.author_name.indexOf(keyword) !== -1); }); } return json; } async function apiDetail(id) { const qpQ = new URLSearchParams({ api_key: API_KEY, range: 'all', type: 'video', page: '1', limit: '1', q: id }); const res = await fetch(API_BASE + '?' + qpQ.toString(), { headers: { 'Accept': 'application/json' } }); if (!res.ok) return null; const data = await res.json().catch(function() { return null; }); const list = (data && data.data) ? data.data : []; if (!list.length) return null; return list.find(function(i) { return String(i.id) === String(id); }) || list[0] || null; } function normalizeItem(item) { const video = (item.media && item.media.videos && item.media.videos[0]) ? item.media.videos[0] : {}; return { id: item.id || '', title: item.tweet_text || '未知', thumb: video.thumbnail || '', videoUrl: video.url || '', time: item.duration_str || '00:00', pv: '0', 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) { status = status || 200; return new Response(JSON.stringify(data), { status: 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, POST', '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 === '/api/extract' && request.method === 'POST') { const body = await request.json(); const res = await fetch('https://x.xvideos4.tk/extract', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); const data = await res.json(); return jsonResponse(data, res.status); } if (path.indexOf('/movie/') !== -1) { const parts = path.split('/').filter(Boolean); const id = parts.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: q, page: 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: 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: sort, category: '', page: 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.indexOf('http') !== 0) { 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://x.xvideos4.tk/', 'Origin': origin || 'https://x.xvideos4.tk', 'Accept': 'video/mp4,video/webm,application/x-mpegurl,*/*;q=0.9', }; try { const ctrl = new AbortController(); const timer = setTimeout(function() { 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.indexOf('mpegurl') !== -1 || ct.indexOf('x-mpegurl') !== -1 || isHlsUrl(finalUrl); const isMp4 = ct.indexOf('video/') === 0 || isMp4Url(finalUrl); return jsonResponse({ url: finalUrl, isHls: isHls, isMp4: isMp4, contentType: ct, status: res.status }); } catch (headErr) { try { const ctrl2 = new AbortController(); const timer2 = setTimeout(function() { 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.indexOf('mpegurl') !== -1 || ct2.indexOf('x-mpegurl') !== -1 || isHlsUrl(finalUrl2); const isMp42 = ct2.indexOf('video/') === 0 || 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 item = normalizeItem(raw); const proxySrc = item.videoUrl; if (!proxySrc) return jsonResponse({ error: 'no_url' }, 404); return jsonResponse({ proxySrc: proxySrc, isHls: isHlsUrl(proxySrc) }); } catch (e) { return jsonResponse({ error: e.message }, 500); } } async function handleList(options) { if (options.sort === 'favorite' && !options.category && !options.q && !options.isSearch) { return handleFavorites(); } const data = await apiList(options); const list = (data && data.data) ? data.data : []; const movies = list.map(normalizeItem); const page = options.page; const lastPage = page + 1; const category = options.category; const catLabel = CATEGORIES.find(function(c) { return c.id === category; })?.name || category; const title = category ? '# ' + catLabel : (options.isSearch ? '搜索:' + options.q : (NAV.find(function(n) { return n.sort === options.sort; })?.label || '今日')); return new Response( renderLayout(renderList(movies, options), title, options.sort), { headers: { 'Content-Type': 'text/html;charset=UTF-8' } } ); } function handleFavorites() { return new Response( renderLayout(renderFavoritesPage(), '我的收藏', 'favorite'), { headers: { 'Content-Type': 'text/html;charset=UTF-8' } } ); } function renderFavoritesPage() { return '
视频链接不可用