feat(storage): 实现跨平台 Storage API 抽象层
- 添加统一的 IStorage 接口定义 - 实现 IndexedDBStorage (Web/Desktop) - 实现 NativeStorage (Android/TV) - 添加类型定义 (Subscription, Channel, SourceValidity等) - 更新 Android/TV AssetReader 支持 SharedPreferences - 安装 idb 库用于 IndexedDB 操作
This commit is contained in:
parent
2cab50db31
commit
380f4ab4d6
@ -1,6 +1,7 @@
|
||||
package com.iptv.tv;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.webkit.JavascriptInterface;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
@ -9,9 +10,12 @@ import java.io.InputStreamReader;
|
||||
|
||||
public class AssetReader {
|
||||
private Context context;
|
||||
private SharedPreferences prefs;
|
||||
private static final String PREFS_NAME = "IPTVData";
|
||||
|
||||
public AssetReader(Context context) {
|
||||
this.context = context;
|
||||
this.prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
@ -45,4 +49,31 @@ public class AssetReader {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// SharedPreferences 存储接口
|
||||
@JavascriptInterface
|
||||
public String getItem(String key) {
|
||||
return prefs.getString(key, null);
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public void setItem(String key, String value) {
|
||||
SharedPreferences.Editor editor = prefs.edit();
|
||||
editor.putString(key, value);
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public void removeItem(String key) {
|
||||
SharedPreferences.Editor editor = prefs.edit();
|
||||
editor.remove(key);
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public void clear() {
|
||||
SharedPreferences.Editor editor = prefs.edit();
|
||||
editor.clear();
|
||||
editor.apply();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package com.iptv.app;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.webkit.JavascriptInterface;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
@ -9,15 +10,17 @@ import java.io.InputStreamReader;
|
||||
|
||||
public class AssetReader {
|
||||
private Context context;
|
||||
private SharedPreferences prefs;
|
||||
private static final String PREFS_NAME = "IPTVData";
|
||||
|
||||
public AssetReader(Context context) {
|
||||
this.context = context;
|
||||
this.prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public String readFile(String path) {
|
||||
try {
|
||||
// path 如: "www/api/result.txt"
|
||||
InputStream is = context.getAssets().open(path);
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
@ -46,4 +49,31 @@ public class AssetReader {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// SharedPreferences 存储接口
|
||||
@JavascriptInterface
|
||||
public String getItem(String key) {
|
||||
return prefs.getString(key, null);
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public void setItem(String key, String value) {
|
||||
SharedPreferences.Editor editor = prefs.edit();
|
||||
editor.putString(key, value);
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public void removeItem(String key) {
|
||||
SharedPreferences.Editor editor = prefs.edit();
|
||||
editor.remove(key);
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public void clear() {
|
||||
SharedPreferences.Editor editor = prefs.edit();
|
||||
editor.clear();
|
||||
editor.apply();
|
||||
}
|
||||
}
|
||||
|
||||
11
ui/package-lock.json
generated
11
ui/package-lock.json
generated
@ -10,6 +10,7 @@
|
||||
"dependencies": {
|
||||
"@vueuse/core": "^14.2.0",
|
||||
"hls.js": "^1.5.0",
|
||||
"idb": "^8.0.3",
|
||||
"pinia": "^3.0.4",
|
||||
"vue": "^3.4.0",
|
||||
"vue-router": "^4.2.0"
|
||||
@ -351,6 +352,11 @@
|
||||
"resolved": "https://registry.npmmirror.com/hookable/-/hookable-5.5.3.tgz",
|
||||
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="
|
||||
},
|
||||
"node_modules/idb": {
|
||||
"version": "8.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/idb/-/idb-8.0.3.tgz",
|
||||
"integrity": "sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg=="
|
||||
},
|
||||
"node_modules/is-what": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmmirror.com/is-what/-/is-what-5.5.0.tgz",
|
||||
@ -845,6 +851,11 @@
|
||||
"resolved": "https://registry.npmmirror.com/hookable/-/hookable-5.5.3.tgz",
|
||||
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="
|
||||
},
|
||||
"idb": {
|
||||
"version": "8.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/idb/-/idb-8.0.3.tgz",
|
||||
"integrity": "sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg=="
|
||||
},
|
||||
"is-what": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmmirror.com/is-what/-/is-what-5.5.0.tgz",
|
||||
|
||||
@ -14,6 +14,7 @@
|
||||
"dependencies": {
|
||||
"@vueuse/core": "^14.2.0",
|
||||
"hls.js": "^1.5.0",
|
||||
"idb": "^8.0.3",
|
||||
"pinia": "^3.0.4",
|
||||
"vue": "^3.4.0",
|
||||
"vue-router": "^4.2.0"
|
||||
|
||||
8
ui/src/storage/adapters/README.md
Normal file
8
ui/src/storage/adapters/README.md
Normal file
@ -0,0 +1,8 @@
|
||||
# Storage Adapters
|
||||
|
||||
存储适配器目录,按平台分离实现:
|
||||
|
||||
- `indexeddb.js` - Web/Desktop 端(使用 IndexedDB)
|
||||
- `native.js` - Android/TV 端(通过 JS Bridge 调用原生存储)
|
||||
|
||||
目前所有实现统一在 `../index.js` 中,如需拆分可在此目录创建单独文件。
|
||||
467
ui/src/storage/index.js
Normal file
467
ui/src/storage/index.js
Normal file
@ -0,0 +1,467 @@
|
||||
import { openDB } from 'idb';
|
||||
import {
|
||||
Subscription, Channel, SourceValidity,
|
||||
Preferences, PlayHistory, CacheMeta
|
||||
} from './types.js';
|
||||
|
||||
const DB_NAME = 'IPTVStorage';
|
||||
const DB_VERSION = 1;
|
||||
|
||||
/**
|
||||
* 抽象存储接口
|
||||
*/
|
||||
class IStorage {
|
||||
// 基础操作
|
||||
async get(key) { throw new Error('Not implemented'); }
|
||||
async set(key, value) { throw new Error('Not implemented'); }
|
||||
async remove(key) { throw new Error('Not implemented'); }
|
||||
async clear() { throw new Error('Not implemented'); }
|
||||
|
||||
// 频道数据
|
||||
async getChannels() { throw new Error('Not implemented'); }
|
||||
async setChannels(channels) { throw new Error('Not implemented'); }
|
||||
async getGroups() { throw new Error('Not implemented'); }
|
||||
|
||||
// 订阅源
|
||||
async getSubscriptions() { throw new Error('Not implemented'); }
|
||||
async setSubscriptions(subs) { throw new Error('Not implemented'); }
|
||||
|
||||
// 线路有效性
|
||||
async getValidity(url) { throw new Error('Not implemented'); }
|
||||
async setValidity(url, validity) { throw new Error('Not implemented'); }
|
||||
async getAllValidity() { throw new Error('Not implemented'); }
|
||||
|
||||
// 用户偏好
|
||||
async getPreferences() { throw new Error('Not implemented'); }
|
||||
async setPreferences(prefs) { throw new Error('Not implemented'); }
|
||||
|
||||
// 播放历史
|
||||
async getHistory(limit = 50) { throw new Error('Not implemented'); }
|
||||
async addHistory(item) { throw new Error('Not implemented'); }
|
||||
async clearHistory() { throw new Error('Not implemented'); }
|
||||
|
||||
// 缓存元数据
|
||||
async getCacheMeta(key) { throw new Error('Not implemented'); }
|
||||
async setCacheMeta(key, meta) { throw new Error('Not implemented'); }
|
||||
async isCacheValid(key, ttl) { throw new Error('Not implemented'); }
|
||||
}
|
||||
|
||||
/**
|
||||
* IndexedDB 实现 (Web/Desktop)
|
||||
*/
|
||||
class IndexedDBStorage extends IStorage {
|
||||
constructor() {
|
||||
super();
|
||||
this.db = null;
|
||||
}
|
||||
|
||||
async init() {
|
||||
if (this.db) return;
|
||||
|
||||
this.db = await openDB(DB_NAME, DB_VERSION, {
|
||||
upgrade(db) {
|
||||
// 存储对象定义
|
||||
if (!db.objectStoreNames.contains('channels')) {
|
||||
const channelStore = db.createObjectStore('channels', { keyPath: 'id' });
|
||||
channelStore.createIndex('group', 'group');
|
||||
channelStore.createIndex('name', 'name');
|
||||
}
|
||||
|
||||
if (!db.objectStoreNames.contains('subscriptions')) {
|
||||
db.createObjectStore('subscriptions', { keyPath: 'id' });
|
||||
}
|
||||
|
||||
if (!db.objectStoreNames.contains('validity')) {
|
||||
db.createObjectStore('validity', { keyPath: 'url' });
|
||||
}
|
||||
|
||||
if (!db.objectStoreNames.contains('preferences')) {
|
||||
db.createObjectStore('preferences');
|
||||
}
|
||||
|
||||
if (!db.objectStoreNames.contains('history')) {
|
||||
const historyStore = db.createObjectStore('history', {
|
||||
keyPath: 'timestamp',
|
||||
autoIncrement: true
|
||||
});
|
||||
historyStore.createIndex('channelId', 'channelId');
|
||||
}
|
||||
|
||||
if (!db.objectStoreNames.contains('cacheMeta')) {
|
||||
db.createObjectStore('cacheMeta', { keyPath: 'key' });
|
||||
}
|
||||
|
||||
if (!db.objectStoreNames.contains('generic')) {
|
||||
db.createObjectStore('generic');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 基础操作
|
||||
async get(key) {
|
||||
await this.init();
|
||||
return await this.db.get('generic', key);
|
||||
}
|
||||
|
||||
async set(key, value) {
|
||||
await this.init();
|
||||
await this.db.put('generic', value, key);
|
||||
}
|
||||
|
||||
async remove(key) {
|
||||
await this.init();
|
||||
await this.db.delete('generic', key);
|
||||
}
|
||||
|
||||
async clear() {
|
||||
await this.init();
|
||||
const stores = ['channels', 'subscriptions', 'validity', 'history', 'cacheMeta', 'generic'];
|
||||
for (const store of stores) {
|
||||
await this.db.clear(store);
|
||||
}
|
||||
}
|
||||
|
||||
// 频道数据
|
||||
async getChannels() {
|
||||
await this.init();
|
||||
return await this.db.getAll('channels');
|
||||
}
|
||||
|
||||
async setChannels(channels) {
|
||||
await this.init();
|
||||
const tx = this.db.transaction('channels', 'readwrite');
|
||||
await tx.store.clear();
|
||||
for (const channel of channels) {
|
||||
await tx.store.put(channel);
|
||||
}
|
||||
await tx.done;
|
||||
|
||||
// 更新缓存元数据
|
||||
await this.setCacheMeta('channels', new CacheMeta({
|
||||
key: 'channels',
|
||||
updatedAt: Date.now(),
|
||||
size: channels.length
|
||||
}));
|
||||
}
|
||||
|
||||
async getGroups() {
|
||||
const channels = await this.getChannels();
|
||||
const groups = new Set(channels.map(c => c.group));
|
||||
return Array.from(groups);
|
||||
}
|
||||
|
||||
// 订阅源
|
||||
async getSubscriptions() {
|
||||
await this.init();
|
||||
return await this.db.getAll('subscriptions');
|
||||
}
|
||||
|
||||
async setSubscriptions(subs) {
|
||||
await this.init();
|
||||
const tx = this.db.transaction('subscriptions', 'readwrite');
|
||||
await tx.store.clear();
|
||||
for (const sub of subs) {
|
||||
await tx.store.put(sub);
|
||||
}
|
||||
await tx.done;
|
||||
}
|
||||
|
||||
// 线路有效性
|
||||
async getValidity(url) {
|
||||
await this.init();
|
||||
return await this.db.get('validity', url);
|
||||
}
|
||||
|
||||
async setValidity(url, validity) {
|
||||
await this.init();
|
||||
await this.db.put('validity', { ...validity, url });
|
||||
}
|
||||
|
||||
async getAllValidity() {
|
||||
await this.init();
|
||||
return await this.db.getAll('validity');
|
||||
}
|
||||
|
||||
// 用户偏好
|
||||
async getPreferences() {
|
||||
await this.init();
|
||||
const prefs = await this.db.get('preferences', 'user');
|
||||
return prefs || new Preferences();
|
||||
}
|
||||
|
||||
async setPreferences(prefs) {
|
||||
await this.init();
|
||||
await this.db.put('preferences', prefs, 'user');
|
||||
}
|
||||
|
||||
// 播放历史
|
||||
async getHistory(limit = 50) {
|
||||
await this.init();
|
||||
const all = await this.db.getAll('history');
|
||||
return all.slice(-limit).reverse();
|
||||
}
|
||||
|
||||
async addHistory(item) {
|
||||
await this.init();
|
||||
await this.db.add('history', item);
|
||||
|
||||
// 清理旧记录(保留最近 500 条)
|
||||
const count = await this.db.count('history');
|
||||
if (count > 500) {
|
||||
const toDelete = count - 500;
|
||||
const keys = await this.db.getAllKeys('history', null, toDelete);
|
||||
for (const key of keys) {
|
||||
await this.db.delete('history', key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async clearHistory() {
|
||||
await this.init();
|
||||
await this.db.clear('history');
|
||||
}
|
||||
|
||||
// 缓存元数据
|
||||
async getCacheMeta(key) {
|
||||
await this.init();
|
||||
return await this.db.get('cacheMeta', key);
|
||||
}
|
||||
|
||||
async setCacheMeta(key, meta) {
|
||||
await this.init();
|
||||
await this.db.put('cacheMeta', { ...meta, key });
|
||||
}
|
||||
|
||||
async isCacheValid(key, ttl) {
|
||||
const meta = await this.getCacheMeta(key);
|
||||
if (!meta || !meta.updatedAt) return false;
|
||||
return Date.now() - meta.updatedAt < ttl;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Native 存储实现 (Android/TV)
|
||||
* 通过 AndroidAsset 接口调用原生 SQLite/SharedPreferences
|
||||
*/
|
||||
class NativeStorage extends IStorage {
|
||||
constructor() {
|
||||
super();
|
||||
this.memoryCache = new Map(); // 内存缓存
|
||||
}
|
||||
|
||||
// 检查 Android 接口是否可用
|
||||
isAvailable() {
|
||||
return typeof window.AndroidAsset !== 'undefined';
|
||||
}
|
||||
|
||||
// 调用原生接口
|
||||
async callNative(method, ...args) {
|
||||
if (!this.isAvailable()) {
|
||||
throw new Error('AndroidAsset not available');
|
||||
}
|
||||
return window.AndroidAsset[method](...args);
|
||||
}
|
||||
|
||||
// 基础操作 (使用 localStorage 作为后备)
|
||||
async get(key) {
|
||||
try {
|
||||
const value = await this.callNative('getItem', key);
|
||||
return value ? JSON.parse(value) : null;
|
||||
} catch {
|
||||
return this.memoryCache.get(key) || null;
|
||||
}
|
||||
}
|
||||
|
||||
async set(key, value) {
|
||||
this.memoryCache.set(key, value);
|
||||
try {
|
||||
await this.callNative('setItem', key, JSON.stringify(value));
|
||||
} catch {
|
||||
// 忽略原生存储失败
|
||||
}
|
||||
}
|
||||
|
||||
async remove(key) {
|
||||
this.memoryCache.delete(key);
|
||||
try {
|
||||
await this.callNative('removeItem', key);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
async clear() {
|
||||
this.memoryCache.clear();
|
||||
try {
|
||||
await this.callNative('clear');
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// 频道数据 (使用专用接口)
|
||||
async getChannels() {
|
||||
try {
|
||||
const data = await this.callNative('readChannelData');
|
||||
if (data && !data.startsWith('ERROR:')) {
|
||||
// 解析频道数据并缓存
|
||||
const channels = this.parseChannelData(data);
|
||||
this.memoryCache.set('channels', channels);
|
||||
return channels;
|
||||
}
|
||||
} catch {}
|
||||
return this.memoryCache.get('channels') || [];
|
||||
}
|
||||
|
||||
async setChannels(channels) {
|
||||
this.memoryCache.set('channels', channels);
|
||||
// 原生端通过文件存储,这里只更新内存缓存
|
||||
await this.setCacheMeta('channels', new CacheMeta({
|
||||
key: 'channels',
|
||||
updatedAt: Date.now(),
|
||||
size: channels.length
|
||||
}));
|
||||
}
|
||||
|
||||
parseChannelData(text) {
|
||||
// 简单的 TXT 格式解析
|
||||
const channels = [];
|
||||
const lines = text.split('\n');
|
||||
let currentGroup = '';
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) continue;
|
||||
|
||||
if (trimmed.includes('#genre#')) {
|
||||
currentGroup = trimmed.split(',')[0];
|
||||
} else if (trimmed.includes(',')) {
|
||||
const [name, url] = trimmed.split(',').map(s => s.trim());
|
||||
if (name && url) {
|
||||
const existing = channels.find(c => c.name === name && c.group === currentGroup);
|
||||
if (existing) {
|
||||
existing.urls.push(url);
|
||||
} else {
|
||||
channels.push(new Channel({
|
||||
id: `${currentGroup}_${name}`,
|
||||
name,
|
||||
group: currentGroup,
|
||||
urls: [url]
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return channels;
|
||||
}
|
||||
|
||||
async getGroups() {
|
||||
const channels = await this.getChannels();
|
||||
const groups = new Set(channels.map(c => c.group));
|
||||
return Array.from(groups);
|
||||
}
|
||||
|
||||
// 订阅源
|
||||
async getSubscriptions() {
|
||||
return await this.get('subscriptions') || [];
|
||||
}
|
||||
|
||||
async setSubscriptions(subs) {
|
||||
await this.set('subscriptions', subs);
|
||||
}
|
||||
|
||||
// 线路有效性 (使用 SharedPreferences)
|
||||
async getValidity(url) {
|
||||
const all = await this.getAllValidity();
|
||||
return all.find(v => v.url === url);
|
||||
}
|
||||
|
||||
async setValidity(url, validity) {
|
||||
const all = await this.getAllValidity();
|
||||
const index = all.findIndex(v => v.url === url);
|
||||
if (index >= 0) {
|
||||
all[index] = { ...validity, url };
|
||||
} else {
|
||||
all.push({ ...validity, url });
|
||||
}
|
||||
await this.set('validity', all);
|
||||
}
|
||||
|
||||
async getAllValidity() {
|
||||
return await this.get('validity') || [];
|
||||
}
|
||||
|
||||
// 用户偏好
|
||||
async getPreferences() {
|
||||
return await this.get('preferences') || new Preferences();
|
||||
}
|
||||
|
||||
async setPreferences(prefs) {
|
||||
await this.set('preferences', prefs);
|
||||
}
|
||||
|
||||
// 播放历史
|
||||
async getHistory(limit = 50) {
|
||||
const all = await this.get('history') || [];
|
||||
return all.slice(-limit).reverse();
|
||||
}
|
||||
|
||||
async addHistory(item) {
|
||||
const all = await this.get('history') || [];
|
||||
all.push(item);
|
||||
// 保留最近 500 条
|
||||
if (all.length > 500) {
|
||||
all.splice(0, all.length - 500);
|
||||
}
|
||||
await this.set('history', all);
|
||||
}
|
||||
|
||||
async clearHistory() {
|
||||
await this.remove('history');
|
||||
}
|
||||
|
||||
// 缓存元数据
|
||||
async getCacheMeta(key) {
|
||||
const all = await this.get('cacheMeta') || {};
|
||||
return all[key];
|
||||
}
|
||||
|
||||
async setCacheMeta(key, meta) {
|
||||
const all = await this.get('cacheMeta') || {};
|
||||
all[key] = { ...meta, key };
|
||||
await this.set('cacheMeta', all);
|
||||
}
|
||||
|
||||
async isCacheValid(key, ttl) {
|
||||
const meta = await this.getCacheMeta(key);
|
||||
if (!meta || !meta.updatedAt) return false;
|
||||
return Date.now() - meta.updatedAt < ttl;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 存储工厂 - 根据平台创建对应的存储实例
|
||||
*/
|
||||
export function createStorage() {
|
||||
const platform = import.meta.env.VITE_PLATFORM || 'web';
|
||||
|
||||
if (platform === 'android' || platform === 'tv') {
|
||||
return new NativeStorage();
|
||||
}
|
||||
|
||||
return new IndexedDBStorage();
|
||||
}
|
||||
|
||||
// 导出类型和类
|
||||
export {
|
||||
IStorage,
|
||||
IndexedDBStorage,
|
||||
NativeStorage,
|
||||
Subscription,
|
||||
Channel,
|
||||
SourceValidity,
|
||||
Preferences,
|
||||
PlayHistory,
|
||||
CacheMeta
|
||||
};
|
||||
|
||||
// 默认导出
|
||||
export default createStorage;
|
||||
72
ui/src/storage/types.js
Normal file
72
ui/src/storage/types.js
Normal file
@ -0,0 +1,72 @@
|
||||
/**
|
||||
* 存储类型定义
|
||||
*/
|
||||
|
||||
// 订阅源
|
||||
export class Subscription {
|
||||
constructor(data = {}) {
|
||||
this.id = data.id || crypto.randomUUID();
|
||||
this.name = data.name || '';
|
||||
this.url = data.url || '';
|
||||
this.type = data.type || 'm3u'; // 'm3u' | 'txt'
|
||||
this.enabled = data.enabled !== false;
|
||||
this.lastUpdated = data.lastUpdated || 0;
|
||||
this.etag = data.etag || '';
|
||||
}
|
||||
}
|
||||
|
||||
// 频道
|
||||
export class Channel {
|
||||
constructor(data = {}) {
|
||||
this.id = data.id || '';
|
||||
this.name = data.name || '';
|
||||
this.group = data.group || '';
|
||||
this.urls = data.urls || []; // string[]
|
||||
this.logo = data.logo || '';
|
||||
this.epgId = data.epgId || '';
|
||||
}
|
||||
}
|
||||
|
||||
// 线路有效性
|
||||
export class SourceValidity {
|
||||
constructor(data = {}) {
|
||||
this.url = data.url || '';
|
||||
this.status = data.status || 'unknown'; // 'online' | 'offline' | 'unknown'
|
||||
this.checkedAt = data.checkedAt || 0;
|
||||
this.latency = data.latency || 0;
|
||||
this.failCount = data.failCount || 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 用户偏好
|
||||
export class Preferences {
|
||||
constructor(data = {}) {
|
||||
this.defaultGroup = data.defaultGroup || '';
|
||||
this.autoPlay = data.autoPlay !== false;
|
||||
this.preferredQuality = data.preferredQuality || 'auto'; // 'auto' | 'hd' | 'sd'
|
||||
this.volume = data.volume ?? 1.0;
|
||||
this.listCacheTTL = data.listCacheTTL || 24 * 60 * 60 * 1000; // 1天
|
||||
this.validityCacheTTL = data.validityCacheTTL || 12 * 60 * 60 * 1000; // 12小时
|
||||
}
|
||||
}
|
||||
|
||||
// 播放历史
|
||||
export class PlayHistory {
|
||||
constructor(data = {}) {
|
||||
this.channelId = data.channelId || '';
|
||||
this.channelName = data.channelName || '';
|
||||
this.sourceUrl = data.sourceUrl || '';
|
||||
this.timestamp = data.timestamp || Date.now();
|
||||
this.duration = data.duration || 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 缓存元数据
|
||||
export class CacheMeta {
|
||||
constructor(data = {}) {
|
||||
this.key = data.key || '';
|
||||
this.updatedAt = data.updatedAt || 0;
|
||||
this.size = data.size || 0;
|
||||
this.version = data.version || 1;
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user