diff --git a/migrations/004_add_tvbox_subscribe_token.sql b/migrations/004_add_tvbox_subscribe_token.sql new file mode 100644 index 0000000..9918900 --- /dev/null +++ b/migrations/004_add_tvbox_subscribe_token.sql @@ -0,0 +1,11 @@ +-- ============================================ +-- 添加 TVBox 订阅 token 字段 +-- 版本: 004 +-- 创建时间: 2026-02-25 +-- ============================================ + +-- 为 users 表添加 tvbox_subscribe_token 字段 +ALTER TABLE users ADD COLUMN tvbox_subscribe_token TEXT; + +-- 创建索引以加速 token 查询 +CREATE INDEX IF NOT EXISTS idx_users_tvbox_token ON users(tvbox_subscribe_token) WHERE tvbox_subscribe_token IS NOT NULL; diff --git a/migrations/postgres/004_add_tvbox_subscribe_token.sql b/migrations/postgres/004_add_tvbox_subscribe_token.sql new file mode 100644 index 0000000..ea9e9d9 --- /dev/null +++ b/migrations/postgres/004_add_tvbox_subscribe_token.sql @@ -0,0 +1,14 @@ +-- ============================================ +-- 添加 TVBox 订阅 token 字段 +-- 版本: 004 +-- 创建时间: 2026-02-25 +-- ============================================ + +-- 为 users 表添加 tvbox_subscribe_token 字段 +ALTER TABLE users ADD COLUMN IF NOT EXISTS tvbox_subscribe_token TEXT; + +-- 创建索引以加速 token 查询 +CREATE INDEX IF NOT EXISTS idx_users_tvbox_token ON users(tvbox_subscribe_token) WHERE tvbox_subscribe_token IS NOT NULL; + +-- 添加注释 +COMMENT ON COLUMN users.tvbox_subscribe_token IS 'TVBox订阅token,用于用户独立订阅'; diff --git a/src/app/api/emby/cms-proxy/[token]/route.ts b/src/app/api/emby/cms-proxy/[token]/route.ts index 77a4082..b1b4a1f 100644 --- a/src/app/api/emby/cms-proxy/[token]/route.ts +++ b/src/app/api/emby/cms-proxy/[token]/route.ts @@ -29,11 +29,29 @@ export async function GET( ); } - // 验证 TVBox Token + // 验证 TVBox Token(从路径中获取) const requestToken = params.token; - const subscribeToken = process.env.TVBOX_SUBSCRIBE_TOKEN; + const globalToken = process.env.TVBOX_SUBSCRIBE_TOKEN; - if (!subscribeToken || requestToken !== subscribeToken) { + // 检查是否是全局token或用户token + let isValidToken = false; + if (globalToken && requestToken === globalToken) { + // 全局token + isValidToken = true; + } else { + // 检查是否是用户token + const { db } = await import('@/lib/db'); + const username = await db.getUsernameByTvboxToken(requestToken); + if (username) { + // 检查用户是否被封禁 + const userInfo = await db.getUserInfoV2(username); + if (userInfo && !userInfo.banned) { + isValidToken = true; + } + } + } + + if (!isValidToken) { return NextResponse.json({ code: 401, msg: '无效的访问token', diff --git a/src/app/api/emby/play/[token]/[filename]/route.ts b/src/app/api/emby/play/[token]/[filename]/route.ts index 7518487..7c0317d 100644 --- a/src/app/api/emby/play/[token]/[filename]/route.ts +++ b/src/app/api/emby/play/[token]/[filename]/route.ts @@ -35,13 +35,29 @@ export async function GET( try { const { searchParams } = new URL(request.url); - // 双重验证:TVBox Token 或 用户登录 + // 双重验证:TVBox Token(全局或用户) 或 用户登录 const requestToken = params.token; - const subscribeToken = process.env.TVBOX_SUBSCRIBE_TOKEN; + const globalToken = process.env.TVBOX_SUBSCRIBE_TOKEN; const authInfo = getAuthInfoFromCookie(request); - // 验证 TVBox Token - const hasValidToken = subscribeToken && requestToken === subscribeToken; + // 验证 TVBox Token(全局token或用户token) + let hasValidToken = false; + if (globalToken && requestToken === globalToken) { + // 全局token + hasValidToken = true; + } else { + // 检查是否是用户token + const { db } = await import('@/lib/db'); + const username = await db.getUsernameByTvboxToken(requestToken); + if (username) { + // 检查用户是否被封禁 + const userInfo = await db.getUserInfoV2(username); + if (userInfo && !userInfo.banned) { + hasValidToken = true; + } + } + } + // 验证用户登录 const hasValidAuth = authInfo && authInfo.username; diff --git a/src/app/api/openlist/cms-proxy/[token]/route.ts b/src/app/api/openlist/cms-proxy/[token]/route.ts index 97d2834..be3f8e9 100644 --- a/src/app/api/openlist/cms-proxy/[token]/route.ts +++ b/src/app/api/openlist/cms-proxy/[token]/route.ts @@ -30,9 +30,27 @@ export async function GET( // 验证 TVBox Token(从路径中获取) const requestToken = params.token; - const subscribeToken = process.env.TVBOX_SUBSCRIBE_TOKEN; + const globalToken = process.env.TVBOX_SUBSCRIBE_TOKEN; - if (!subscribeToken || requestToken !== subscribeToken) { + // 检查是否是全局token或用户token + let isValidToken = false; + if (globalToken && requestToken === globalToken) { + // 全局token + isValidToken = true; + } else { + // 检查是否是用户token + const { db } = await import('@/lib/db'); + const username = await db.getUsernameByTvboxToken(requestToken); + if (username) { + // 检查用户是否被封禁 + const userInfo = await db.getUserInfoV2(username); + if (userInfo && !userInfo.banned) { + isValidToken = true; + } + } + } + + if (!isValidToken) { return NextResponse.json( { code: 401, diff --git a/src/app/api/openlist/play/[token]/route.ts b/src/app/api/openlist/play/[token]/route.ts index 7b51566..5a3eb13 100644 --- a/src/app/api/openlist/play/[token]/route.ts +++ b/src/app/api/openlist/play/[token]/route.ts @@ -22,13 +22,29 @@ export async function GET( try { const { searchParams } = new URL(request.url); - // 双重验证:TVBox Token 或 用户登录 + // 双重验证:TVBox Token(全局或用户) 或 用户登录 const requestToken = params.token; - const subscribeToken = process.env.TVBOX_SUBSCRIBE_TOKEN; + const globalToken = process.env.TVBOX_SUBSCRIBE_TOKEN; const authInfo = getAuthInfoFromCookie(request); - // 验证 TVBox Token - const hasValidToken = subscribeToken && requestToken === subscribeToken; + // 验证 TVBox Token(全局token或用户token) + let hasValidToken = false; + if (globalToken && requestToken === globalToken) { + // 全局token + hasValidToken = true; + } else { + // 检查是否是用户token + const { db } = await import('@/lib/db'); + const username = await db.getUsernameByTvboxToken(requestToken); + if (username) { + // 检查用户是否被封禁 + const userInfo = await db.getUserInfoV2(username); + if (userInfo && !userInfo.banned) { + hasValidToken = true; + } + } + } + // 验证用户登录 const hasValidAuth = authInfo && authInfo.username; diff --git a/src/app/api/tvbox/subscribe/route.ts b/src/app/api/tvbox/subscribe/route.ts index 4e55f59..686ada6 100644 --- a/src/app/api/tvbox/subscribe/route.ts +++ b/src/app/api/tvbox/subscribe/route.ts @@ -3,6 +3,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { getAvailableApiSites, getConfig } from '@/lib/config'; +import { db } from '@/lib/db'; import { getCachedLiveChannels } from '@/lib/live'; export const runtime = 'nodejs'; @@ -10,6 +11,7 @@ export const runtime = 'nodejs'; /** * TVBOX订阅API * 根据视频源和直播源生成TVBOX订阅 + * 支持全局token(管理员)和用户token(普通用户) */ export async function GET(request: NextRequest) { // 检查是否开启订阅功能 @@ -24,22 +26,53 @@ export async function GET(request: NextRequest) { // 验证token const searchParams = request.nextUrl.searchParams; const token = searchParams.get('token'); - const subscribeToken = process.env.TVBOX_SUBSCRIBE_TOKEN; + const globalToken = process.env.TVBOX_SUBSCRIBE_TOKEN; const adFilter = searchParams.get('adFilter') === 'true'; // 获取去广告参数 - if (!subscribeToken || token !== subscribeToken) { + if (!token) { return NextResponse.json( - { error: '无效的订阅token' }, + { error: '缺少订阅token' }, { status: 401 } ); } + // 判断是全局token还是用户token + let username: string | undefined; + let isGlobalToken = false; + + if (globalToken && token === globalToken) { + // 全局token(管理员订阅) + isGlobalToken = true; + console.log('使用全局token访问TVBox订阅'); + } else { + // 用户token,查询用户名 + username = await db.getUsernameByTvboxToken(token) || undefined; + if (!username) { + return NextResponse.json( + { error: '无效的订阅token' }, + { status: 401 } + ); + } + + // 检查用户是否被封禁 + const userInfo = await db.getUserInfoV2(username); + if (userInfo?.banned) { + return NextResponse.json( + { error: '用户已被封禁' }, + { status: 403 } + ); + } + + console.log(`用户 ${username} 访问TVBox订阅`); + } + try { // 获取配置 const config = await getConfig(); - // 获取视频源(TVBox客户端不会发送Cookie,直接获取所有启用的源) - const apiSites = await getAvailableApiSites(); + // 获取视频源 + // 全局token返回所有源,用户token返回该用户有权限的源 + const apiSites = await getAvailableApiSites(username); // 获取直播源 const liveConfig = config.LiveConfig?.filter(live => !live.disabled) || []; @@ -75,7 +108,7 @@ export async function GET(request: NextRequest) { key: 'openlist', name: '私人影库', type: 1, - api: `${baseUrl}/api/openlist/cms-proxy/${encodeURIComponent(subscribeToken)}`, + api: `${baseUrl}/api/openlist/cms-proxy/${encodeURIComponent(token)}`, searchable: 1, quickSearch: 1, filterable: 1, @@ -87,7 +120,7 @@ export async function GET(request: NextRequest) { key: `emby_${source.key}`, name: source.name || 'Emby媒体库', type: 1, - api: `${baseUrl}/api/emby/cms-proxy/${encodeURIComponent(subscribeToken)}?embyKey=${source.key}`, + api: `${baseUrl}/api/emby/cms-proxy/${encodeURIComponent(token)}?embyKey=${source.key}`, searchable: 1, quickSearch: 1, filterable: 1, diff --git a/src/app/api/user/tvbox-token/reset/route.ts b/src/app/api/user/tvbox-token/reset/route.ts new file mode 100644 index 0000000..7227341 --- /dev/null +++ b/src/app/api/user/tvbox-token/reset/route.ts @@ -0,0 +1,46 @@ +import { NextRequest, NextResponse } from 'next/server'; + +import { getAuthInfoFromCookie } from '@/lib/auth'; +import { db } from '@/lib/db'; +import { generateTvboxToken } from '@/lib/tvbox-token'; + +export const runtime = 'nodejs'; + +/** + * 重置用户的TVBox订阅token + * 旧token将失效 + */ +export async function POST(request: NextRequest) { + try { + // 验证用户登录 + const authInfo = getAuthInfoFromCookie(request); + if (!authInfo?.username) { + return NextResponse.json( + { error: '未登录' }, + { status: 401 } + ); + } + + const username = authInfo.username; + + // 生成新token + const newToken = generateTvboxToken(); + await db.setTvboxSubscribeToken(username, newToken); + + console.log(`用户 ${username} 重置了TVBox订阅token`); + + return NextResponse.json({ + token: newToken, + message: '订阅token已重置,旧链接已失效', + }); + } catch (error) { + console.error('重置TVBox订阅token失败:', error); + return NextResponse.json( + { + error: '重置订阅token失败', + details: (error as Error).message, + }, + { status: 500 } + ); + } +} diff --git a/src/app/api/user/tvbox-token/route.ts b/src/app/api/user/tvbox-token/route.ts new file mode 100644 index 0000000..21eb4ed --- /dev/null +++ b/src/app/api/user/tvbox-token/route.ts @@ -0,0 +1,47 @@ +import { NextRequest, NextResponse } from 'next/server'; + +import { getAuthInfoFromCookie } from '@/lib/auth'; +import { db } from '@/lib/db'; +import { generateTvboxToken } from '@/lib/tvbox-token'; + +export const runtime = 'nodejs'; + +/** + * 获取用户的TVBox订阅token + * 如果用户没有token,自动生成一个 + */ +export async function GET(request: NextRequest) { + try { + // 验证用户登录 + const authInfo = getAuthInfoFromCookie(request); + if (!authInfo?.username) { + return NextResponse.json( + { error: '未登录' }, + { status: 401 } + ); + } + + const username = authInfo.username; + + // 获取token,如果没有则生成 + let token = await db.getTvboxSubscribeToken(username); + + if (!token) { + // 懒加载:首次访问时生成token + token = generateTvboxToken(); + await db.setTvboxSubscribeToken(username, token); + console.log(`为用户 ${username} 生成TVBox订阅token`); + } + + return NextResponse.json({ token }); + } catch (error) { + console.error('获取TVBox订阅token失败:', error); + return NextResponse.json( + { + error: '获取订阅token失败', + details: (error as Error).message, + }, + { status: 500 } + ); + } +} diff --git a/src/components/UserMenu.tsx b/src/components/UserMenu.tsx index 81b5b5b..9022f9b 100644 --- a/src/components/UserMenu.tsx +++ b/src/components/UserMenu.tsx @@ -76,8 +76,12 @@ export const UserMenu: React.FC = () => { // 订阅相关状态 const [subscribeEnabled, setSubscribeEnabled] = useState(false); const [subscribeUrl, setSubscribeUrl] = useState(''); + const [subscribeUrlWithAdFilter, setSubscribeUrlWithAdFilter] = useState(''); const [copySuccess, setCopySuccess] = useState(false); - const [adFilterEnabled, setAdFilterEnabled] = useState(true); // 去广告开关,默认开启 + const [copySuccessAdFilter, setCopySuccessAdFilter] = useState(false); + const [tvboxToken, setTvboxToken] = useState(''); + const [isResettingToken, setIsResettingToken] = useState(false); + const [isLoadingSubscribeUrl, setIsLoadingSubscribeUrl] = useState(false); // Body 滚动锁定 - 使用 overflow 方式避免布局问题 useEffect(() => { @@ -304,20 +308,91 @@ export const UserMenu: React.FC = () => { }, []); // 懒加载订阅 URL - 只在打开订阅面板时请求 - const fetchSubscribeUrl = async (adFilter?: boolean) => { + const fetchSubscribeUrl = async () => { + setIsLoadingSubscribeUrl(true); try { - const currentOrigin = window.location.origin; - const filterValue = adFilter !== undefined ? adFilter : adFilterEnabled; - const response = await fetch(`/api/tvbox/config?origin=${encodeURIComponent(currentOrigin)}&adFilter=${filterValue}`); + // 获取用户的 TVBox token + const response = await fetch('/api/user/tvbox-token'); if (response.ok) { const data = await response.json(); - setSubscribeUrl(data.url); + const token = data.token; + setTvboxToken(token); + + // 前端拼接订阅链接 + const currentOrigin = window.location.origin; + const standardUrl = `${currentOrigin}/api/tvbox/subscribe?token=${token}`; + const adFilterUrl = `${currentOrigin}/api/tvbox/subscribe?token=${token}&adFilter=true`; + + setSubscribeUrl(standardUrl); + setSubscribeUrlWithAdFilter(adFilterUrl); } } catch (error) { console.error('获取订阅URL失败:', error); + } finally { + setIsLoadingSubscribeUrl(false); } }; + // 重置 TVBox token + const handleResetToken = async () => { + setConfirmDialog({ + isOpen: true, + title: '重置订阅Token', + message: '确定要重置订阅token吗?重置后旧的订阅链接将失效。', + onConfirm: async () => { + setConfirmDialog({ ...confirmDialog, isOpen: false }); + setIsResettingToken(true); + + try { + const response = await fetch('/api/user/tvbox-token/reset', { + method: 'POST', + }); + + const messageEl = document.getElementById('tvbox-token-message'); + if (response.ok) { + const data = await response.json(); + const token = data.token; + setTvboxToken(token); + + // 更新订阅链接 + const currentOrigin = window.location.origin; + const standardUrl = `${currentOrigin}/api/tvbox/subscribe?token=${token}`; + const adFilterUrl = `${currentOrigin}/api/tvbox/subscribe?token=${token}&adFilter=true`; + + setSubscribeUrl(standardUrl); + setSubscribeUrlWithAdFilter(adFilterUrl); + + if (messageEl) { + messageEl.textContent = '订阅token已重置!'; + messageEl.className = 'text-xs text-center text-green-600 dark:text-green-400 mt-2'; + messageEl.classList.remove('hidden'); + setTimeout(() => { + messageEl.classList.add('hidden'); + }, 3000); + } + } else { + const data = await response.json(); + if (messageEl) { + messageEl.textContent = data.error || '重置失败,请重试'; + messageEl.className = 'text-xs text-center text-red-600 dark:text-red-400 mt-2'; + messageEl.classList.remove('hidden'); + } + } + } catch (error) { + console.error('重置token失败:', error); + const messageEl = document.getElementById('tvbox-token-message'); + if (messageEl) { + messageEl.textContent = '重置失败,请重试'; + messageEl.className = 'text-xs text-center text-red-600 dark:text-red-400 mt-2'; + messageEl.classList.remove('hidden'); + } + } finally { + setIsResettingToken(false); + } + }, + }); + }; + // 获取认证信息和存储类型 useEffect(() => { if (typeof window !== 'undefined') { @@ -726,12 +801,7 @@ export const UserMenu: React.FC = () => { const handleCloseSubscribe = () => { setIsSubscribeOpen(false); setCopySuccess(false); - }; - - const handleAdFilterToggle = async (checked: boolean) => { - setAdFilterEnabled(checked); - // 当去广告开关改变时,重新获取订阅URL,传入新的值 - await fetchSubscribeUrl(checked); + setCopySuccessAdFilter(false); }; const handleCopySubscribeUrl = async () => { @@ -746,6 +816,18 @@ export const UserMenu: React.FC = () => { } }; + const handleCopySubscribeUrlWithAdFilter = async () => { + try { + await navigator.clipboard.writeText(subscribeUrlWithAdFilter); + setCopySuccessAdFilter(true); + setTimeout(() => { + setCopySuccessAdFilter(false); + }, 2000); + } catch (error) { + console.error('复制失败:', error); + } + }; + const handleSubmitChangePassword = async () => { setPasswordError(''); @@ -2527,7 +2609,7 @@ export const UserMenu: React.FC = () => { {/* 标题栏 */}

- 订阅 + TVBox订阅

-
- + {/* 加载骨架 - 订阅链接(去广告) */} +
+
+
+
+
+
+
+
+ + {/* 加载骨架 - 重置按钮 */} +
+
+
+
+ + ) : ( + <> + {/* 订阅链接(标准) */} +
+

+ 订阅链接(标准) +

+
+ + +
+
+ + {/* 订阅链接(去广告) */} +
+

+ 订阅链接(去广告) +

+
+ + +
+

+ 💡 去广告需要经过服务器代理,某些源可能因为区域或兼容问题无法播放 +

+
+ + {/* 重置Token按钮 */} +
+ +

+ ⚠️ 重置后旧链接将失效 +

+ {/* 消息提示 */} +

+
+ + )} {/* 底部说明 */}

- 将订阅链接复制到TVBOX应用中使用 + 将订阅链接复制到TVBox应用中使用

diff --git a/src/lib/d1.db.ts b/src/lib/d1.db.ts index 0bc2c60..aa47bce 100644 --- a/src/lib/d1.db.ts +++ b/src/lib/d1.db.ts @@ -1346,6 +1346,51 @@ export class D1Storage implements IStorage { } } + // ==================== TVBox订阅token ==================== + + async getTvboxSubscribeToken?(userName: string): Promise { + try { + const result = await this.db + .prepare('SELECT tvbox_subscribe_token FROM users WHERE username = ?') + .bind(userName) + .first(); + + return result?.tvbox_subscribe_token || null; + } catch (err) { + console.error('D1Storage.getTvboxSubscribeToken error:', err); + return null; + } + } + + async setTvboxSubscribeToken?(userName: string, token: string): Promise { + try { + await this.db + .prepare('UPDATE users SET tvbox_subscribe_token = ? WHERE username = ?') + .bind(token, userName) + .run(); + + // 清除缓存 + userInfoCache?.delete(userName); + } catch (err) { + console.error('D1Storage.setTvboxSubscribeToken error:', err); + throw err; + } + } + + async getUsernameByTvboxToken?(token: string): Promise { + try { + const result = await this.db + .prepare('SELECT username FROM users WHERE tvbox_subscribe_token = ?') + .bind(token) + .first(); + + return result?.username || null; + } catch (err) { + console.error('D1Storage.getUsernameByTvboxToken error:', err); + return null; + } + } + // ==================== 搜索历史 ==================== async getSearchHistory(userName: string): Promise { diff --git a/src/lib/db.ts b/src/lib/db.ts index 3ef2b22..5af0f91 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -482,6 +482,27 @@ export class DbManager { return []; } + // ---------- TVBox订阅token ---------- + async getTvboxSubscribeToken(userName: string): Promise { + if (typeof (this.storage as any).getTvboxSubscribeToken === 'function') { + return (this.storage as any).getTvboxSubscribeToken(userName); + } + return null; + } + + async setTvboxSubscribeToken(userName: string, token: string): Promise { + if (typeof (this.storage as any).setTvboxSubscribeToken === 'function') { + await (this.storage as any).setTvboxSubscribeToken(userName, token); + } + } + + async getUsernameByTvboxToken(token: string): Promise { + if (typeof (this.storage as any).getUsernameByTvboxToken === 'function') { + return (this.storage as any).getUsernameByTvboxToken(token); + } + return null; + } + // ---------- 播放记录迁移 ---------- async migratePlayRecords(userName: string): Promise { if (typeof (this.storage as any).migratePlayRecords === 'function') { diff --git a/src/lib/postgres.db.ts b/src/lib/postgres.db.ts index 4279948..e138944 100644 --- a/src/lib/postgres.db.ts +++ b/src/lib/postgres.db.ts @@ -911,6 +911,52 @@ export class PostgresStorage implements IStorage { } } + // ==================== TVBox订阅token ==================== + + async getTvboxSubscribeToken(userName: string): Promise { + try { + const result = await this.db + .prepare('SELECT tvbox_subscribe_token FROM users_v2 WHERE username = $1') + .bind(userName) + .first(); + + return result?.tvbox_subscribe_token || null; + } catch (err) { + console.error('PostgresStorage.getTvboxSubscribeToken error:', err); + return null; + } + } + + async setTvboxSubscribeToken(userName: string, token: string): Promise { + try { + await this.db + .prepare('UPDATE users_v2 SET tvbox_subscribe_token = $1 WHERE username = $2') + .bind(token, userName) + .run(); + + // 清除用户信息缓存 + const { userInfoCache } = await import('./user-cache'); + userInfoCache.delete(userName); + } catch (err) { + console.error('PostgresStorage.setTvboxSubscribeToken error:', err); + throw err; + } + } + + async getUsernameByTvboxToken(token: string): Promise { + try { + const result = await this.db + .prepare('SELECT username FROM users_v2 WHERE tvbox_subscribe_token = $1') + .bind(token) + .first(); + + return result?.username || null; + } catch (err) { + console.error('PostgresStorage.getUsernameByTvboxToken error:', err); + return null; + } + } + // ==================== 音乐播放记录 ==================== async getMusicPlayRecord(userName: string, key: string): Promise { diff --git a/src/lib/redis-base.db.ts b/src/lib/redis-base.db.ts index 7c60f05..4ddf952 100644 --- a/src/lib/redis-base.db.ts +++ b/src/lib/redis-base.db.ts @@ -1723,4 +1723,35 @@ export abstract class BaseRedisStorage implements IStorage { // 清除缓存 userInfoCache?.delete(userName); } + + // ---------- TVBox订阅token相关 ---------- + async getTvboxSubscribeToken(userName: string): Promise { + // 直接从数据库读取,不使用缓存 + const token = await this.withRetry(() => + this.adapter.hGet(this.userInfoKey(userName), 'tvboxSubscribeToken') + ); + return token || null; + } + + async setTvboxSubscribeToken(userName: string, token: string): Promise { + // 保存token到用户信息 + await this.withRetry(() => + this.adapter.hSet(this.userInfoKey(userName), 'tvboxSubscribeToken', token) + ); + + // 创建token到用户名的反向索引 + await this.withRetry(() => + this.adapter.set(`tvbox:token:${token}`, userName) + ); + + // 清除缓存 + userInfoCache?.delete(userName); + } + + async getUsernameByTvboxToken(token: string): Promise { + const userName = await this.withRetry(() => + this.adapter.get(`tvbox:token:${token}`) + ); + return userName || null; + } } diff --git a/src/lib/tvbox-token.ts b/src/lib/tvbox-token.ts new file mode 100644 index 0000000..c8c5ad0 --- /dev/null +++ b/src/lib/tvbox-token.ts @@ -0,0 +1,9 @@ +import { randomBytes } from 'crypto'; + +/** + * 生成TVBox订阅token + * 使用crypto生成32位随机hex字符串 + */ +export function generateTvboxToken(): string { + return randomBytes(16).toString('hex'); +} diff --git a/src/lib/types.ts b/src/lib/types.ts index 526bc8f..e08c0a5 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -161,6 +161,11 @@ export interface IStorage { setUserEmail?(userName: string, email: string): Promise; getEmailNotificationPreference?(userName: string): Promise; setEmailNotificationPreference?(userName: string, enabled: boolean): Promise; + + // TVBox订阅token相关 + getTvboxSubscribeToken?(userName: string): Promise; + setTvboxSubscribeToken?(userName: string, token: string): Promise; + getUsernameByTvboxToken?(token: string): Promise; } // 搜索结果数据结构