WebDAV同步功能重大改进:修复编码问题和双向同步

主要改进:
1. 修复观看记录key中站点名称的编码问题(电视版乱码修复)
2. 实现智能合并策略,支持时间和进度比较
3. 自动修复过期时间戳,确保记录能正常显示
4. 上传和下载都使用findAllRecent(0),确保完整同步
5. 添加详细日志,方便调试定位问题

技术细节:
- 新增fixHistoryKey()方法,单独修复key中的站点名称部分
- 改进合并算法,考虑时间相近、进度领先等多种情况
- 修复createTime超过60天被过滤的问题
- 统一本地和远程记录的编码处理

删除的文件:
- other/sample/* - 示例配置文件
- other/image/* - 示例图片
- .vscode/settings.json - 编辑器配置
This commit is contained in:
您的名字
2025-11-18 10:22:38 +08:00
parent 156cecc848
commit 59a4096b37
17 changed files with 355 additions and 400 deletions
-3
View File
@@ -1,3 +0,0 @@
{
"java.configuration.updateBuildConfiguration": "automatic"
}
@@ -245,7 +245,24 @@ public class HomeActivity extends BaseActivity implements CustomTitleView.Listen
} }
private void getHistory(boolean renew) { private void getHistory(boolean renew) {
List<History> items = History.get(); // 获取所有视频源的观看记录(最近60天)
List<History> items = History.getAll();
com.github.catvod.utils.Logger.d("HomeActivity: 获取观看记录,共 " + items.size() + "");
// 对比一下数据库中所有记录
List<com.fongmi.android.tv.bean.History> allInDb = com.fongmi.android.tv.db.AppDatabase.get().getHistoryDao().findAllRecent(0);
com.github.catvod.utils.Logger.d("HomeActivity: 数据库总记录数: " + allInDb.size() + " 条(包含所有时间)");
if (items.size() < allInDb.size()) {
com.github.catvod.utils.Logger.w("HomeActivity: 有 " + (allInDb.size() - items.size()) + " 条记录因为时间过滤被隐藏");
}
for (History h : items) {
com.github.catvod.utils.Logger.d("HomeActivity: 记录 - " + h.getVodName() +
" (cid=" + h.getCid() +
", createTime=" + h.getCreateTime() + ")");
}
int historyIndex = getHistoryIndex(); int historyIndex = getHistoryIndex();
int recommendIndex = getRecommendIndex(); int recommendIndex = getRecommendIndex();
boolean exist = recommendIndex - historyIndex == 2; boolean exist = recommendIndex - historyIndex == 2;
-1
View File
@@ -10,7 +10,6 @@
<item name="colorPrimary">@color/primary</item> <item name="colorPrimary">@color/primary</item>
<item name="colorPrimaryDark">@color/primaryDark</item> <item name="colorPrimaryDark">@color/primaryDark</item>
<item name="colorAccent">@color/accent</item> <item name="colorAccent">@color/accent</item>
<item name="colorControlHighlight">@color/primary</item>
<item name="android:windowFullscreen">true</item> <item name="android:windowFullscreen">true</item>
<item name="android:windowBackground">@null</item> <item name="android:windowBackground">@null</item>
<item name="android:windowDisablePreview">true</item> <item name="android:windowDisablePreview">true</item>
@@ -54,7 +54,9 @@ public class App extends Application {
executor = Executors.newFixedThreadPool(Constant.THREAD_POOL); executor = Executors.newFixedThreadPool(Constant.THREAD_POOL);
handler = HandlerCompat.createAsync(Looper.getMainLooper()); handler = HandlerCompat.createAsync(Looper.getMainLooper());
time = System.currentTimeMillis(); time = System.currentTimeMillis();
gson = new Gson(); gson = new com.google.gson.GsonBuilder()
.disableHtmlEscaping()
.create();
cleanTask = this::checkCacheClean; cleanTask = this::checkCacheClean;
syncTask = this::checkWebDAVSync; syncTask = this::checkWebDAVSync;
appJustLaunched = true; appJustLaunched = true;
@@ -227,18 +229,8 @@ public class App extends Application {
if (manager.isConfigured()) { if (manager.isConfigured()) {
// 应用启动时,如果已配置WebDAV,立即执行一次同步(下载远程数据) // 应用启动时,如果已配置WebDAV,立即执行一次同步(下载远程数据)
// 这样新设备配置后,下次启动应用时就能看到其他设备的历史记录 // 这样新设备配置后,下次启动应用时就能看到其他设备的历史记录
App.execute(() -> { Logger.d("App: WebDAV已配置,准备执行同步");
try { manager.syncHistory(true); // 使用统一的同步方法,包含防重复逻辑
Logger.d("App: 应用启动,执行WebDAV同步");
// 先上传本地记录
manager.uploadHistory();
// 再下载远程记录并合并
manager.downloadHistory();
Logger.d("App: WebDAV同步完成");
} catch (Exception e) {
Logger.e("App: WebDAV同步失败: " + e.getMessage());
}
});
// 如果启用了自动同步,设置定期同步 // 如果启用了自动同步,设置定期同步
if (Setting.isWebDAVAutoSync()) { if (Setting.isWebDAVAutoSync()) {
@@ -246,6 +238,8 @@ public class App extends Application {
// 延迟执行下次同步,避免影响启动速度 // 延迟执行下次同步,避免影响启动速度
post(syncTask, interval * 60 * 1000L); post(syncTask, interval * 60 * 1000L);
} }
} else {
Logger.d("App: WebDAV未配置,跳过同步");
} }
} }
@@ -248,6 +248,10 @@ public class History {
return AppDatabase.get().getHistoryDao().find(cid, System.currentTimeMillis() - Constant.HISTORY_TIME); return AppDatabase.get().getHistoryDao().find(cid, System.currentTimeMillis() - Constant.HISTORY_TIME);
} }
public static List<History> getAll() {
return AppDatabase.get().getHistoryDao().findAllRecent(System.currentTimeMillis() - Constant.HISTORY_TIME);
}
public static History find(String key) { public static History find(String key) {
return AppDatabase.get().getHistoryDao().find(VodConfig.getCid(), key); return AppDatabase.get().getHistoryDao().find(VodConfig.getCid(), key);
} }
@@ -272,8 +276,15 @@ public class History {
} }
public void update() { public void update() {
merge(find(), false); try {
save(); com.github.catvod.utils.Logger.d("History.update: 开始更新观看记录 key=" + getKey());
merge(find(), false);
save();
com.github.catvod.utils.Logger.d("History.update: 更新成功");
} catch (Exception e) {
com.github.catvod.utils.Logger.e("History.update: 更新失败 - " + e.getMessage());
e.printStackTrace();
}
} }
public History update(int cid) { public History update(int cid) {
@@ -287,6 +298,7 @@ public class History {
} }
public History save() { public History save() {
com.github.catvod.utils.Logger.d("History.save: key=" + getKey() + ", vodName=" + getVodName());
AppDatabase.get().getHistoryDao().insertOrUpdate(this); AppDatabase.get().getHistoryDao().insertOrUpdate(this);
return this; return this;
} }
@@ -105,11 +105,36 @@ public class Site implements Parcelable {
public static Site objectFrom(JsonElement element) { public static Site objectFrom(JsonElement element) {
try { try {
return App.gson().fromJson(element, Site.class); Site site = App.gson().fromJson(element, Site.class);
// 尝试修复可能的编码问题
if (site != null && site.getKey() != null) {
site.setKey(fixEncoding(site.getKey()));
if (site.getName() != null) {
site.setName(fixEncoding(site.getName()));
}
}
return site;
} catch (Exception e) { } catch (Exception e) {
return new Site(); return new Site();
} }
} }
private static String fixEncoding(String str) {
if (str == null || str.isEmpty()) return str;
try {
// 检查是否包含乱码字符(替换字符 U+FFFD)
if (str.indexOf('\uFFFD') >= 0) {
// 尝试用ISO-8859-1重新解码为UTF-8
byte[] bytes = str.getBytes(java.nio.charset.StandardCharsets.ISO_8859_1);
String fixed = new String(bytes, java.nio.charset.StandardCharsets.UTF_8);
com.github.catvod.utils.Logger.d("Site.fixEncoding: 修复编码 '" + str + "' -> '" + fixed + "'");
return fixed;
}
} catch (Exception e) {
com.github.catvod.utils.Logger.e("Site.fixEncoding: 修复失败 - " + e.getMessage());
}
return str;
}
public static Site get(String key) { public static Site get(String key) {
Site site = new Site(); Site site = new Site();
@@ -133,6 +158,13 @@ public class Site implements Parcelable {
public void setKey(@NonNull String key) { public void setKey(@NonNull String key) {
this.key = key; this.key = key;
// 检查key中是否有异常字符
for (int i = 0; i < key.length(); i++) {
char c = key.charAt(i);
if (c == 0xFFFD || c < 0x20 || (c >= 0x7F && c < 0xA0)) {
com.github.catvod.utils.Logger.w("Site.setKey: 检测到异常字符 at position " + i + ": U+" + String.format("%04X", (int)c));
}
}
} }
public String getName() { public String getName() {
@@ -141,6 +173,15 @@ public class Site implements Parcelable {
public void setName(String name) { public void setName(String name) {
this.name = name; this.name = name;
// 检查name中是否有异常字符
if (name != null) {
for (int i = 0; i < name.length(); i++) {
char c = name.charAt(i);
if (c == 0xFFFD || c < 0x20 || (c >= 0x7F && c < 0xA0)) {
com.github.catvod.utils.Logger.w("Site.setName: 检测到异常字符 at position " + i + ": U+" + String.format("%04X", (int)c) + " in name: " + name);
}
}
}
} }
public String getApi() { public String getApi() {
@@ -16,6 +16,9 @@ public abstract class HistoryDao extends BaseDao<History> {
@Query("SELECT * FROM History WHERE cid = :cid AND createTime >= :createTime ORDER BY createTime DESC") @Query("SELECT * FROM History WHERE cid = :cid AND createTime >= :createTime ORDER BY createTime DESC")
public abstract List<History> find(int cid, long createTime); public abstract List<History> find(int cid, long createTime);
@Query("SELECT * FROM History WHERE createTime >= :createTime ORDER BY createTime DESC")
public abstract List<History> findAllRecent(long createTime);
@Query("SELECT * FROM History WHERE cid = :cid AND `key` = :key") @Query("SELECT * FROM History WHERE cid = :cid AND `key` = :key")
public abstract History find(int cid, String key); public abstract History find(int cid, String key);
@@ -6,6 +6,7 @@ import com.fongmi.android.tv.App;
import com.fongmi.android.tv.bean.Backup; import com.fongmi.android.tv.bean.Backup;
import com.fongmi.android.tv.bean.History; import com.fongmi.android.tv.bean.History;
import com.fongmi.android.tv.db.AppDatabase; import com.fongmi.android.tv.db.AppDatabase;
import com.fongmi.android.tv.event.RefreshEvent;
import com.github.catvod.utils.Logger; import com.github.catvod.utils.Logger;
import com.github.catvod.utils.Prefers; import com.github.catvod.utils.Prefers;
import com.google.gson.Gson; import com.google.gson.Gson;
@@ -280,20 +281,60 @@ public class WebDAVSyncManager {
} }
try { try {
// 获取所有观看记录 // 获取所有观看记录 - 使用findAllRecent(0)来获取所有记录(包括旧记录)
List<History> historyList = AppDatabase.get().getHistoryDao().findAll(); Logger.d("WebDAV: 开始查询数据库中的观看记录...");
List<History> historyList = AppDatabase.get().getHistoryDao().findAllRecent(0);
Logger.d("WebDAV: 数据库查询完成,结果: " + (historyList == null ? "null" : historyList.size() + ""));
if (historyList == null) { if (historyList == null) {
Logger.w("WebDAV: 查询结果为null,创建空列表");
historyList = new java.util.ArrayList<>(); historyList = new java.util.ArrayList<>();
} }
// 修复数据中可能的编码问题(重点修复key中的站点名称部分)
Logger.d("WebDAV: 开始修复上传数据的编码问题...");
for (History h : historyList) {
String originalKey = h.getKey();
// key格式: 站点key$视频ID$cid,需要单独修复站点key部分
String fixedKey = fixHistoryKey(originalKey);
if (!originalKey.equals(fixedKey)) {
Logger.d("WebDAV: 修复key编码: '" + originalKey + "' -> '" + fixedKey + "'");
h.setKey(fixedKey);
}
String originalName = h.getVodName();
String fixedName = fixEncodingIfNeeded(originalName);
if (!originalName.equals(fixedName)) {
Logger.d("WebDAV: 修复vodName编码: '" + originalName + "' -> '" + fixedName + "'");
h.setVodName(fixedName);
}
}
Logger.d("WebDAV: 准备上传观看记录,共 " + historyList.size() + ""); Logger.d("WebDAV: 准备上传观看记录,共 " + historyList.size() + "");
// 记录前3条数据的详细信息
for (int i = 0; i < Math.min(3, historyList.size()); i++) {
History h = historyList.get(i);
Logger.d("WebDAV: 上传记录[" + i + "] key=" + h.getKey() + ", vodName=" + h.getVodName());
// 检查key中的每个字符
String key = h.getKey();
StringBuilder hexDump = new StringBuilder();
for (int j = 0; j < Math.min(20, key.length()); j++) {
hexDump.append(String.format("%04x ", (int)key.charAt(j)));
}
Logger.d("WebDAV: key前20字符的Unicode: " + hexDump.toString());
}
String json = App.gson().toJson(historyList); String json = App.gson().toJson(historyList);
if (TextUtils.isEmpty(json)) { if (TextUtils.isEmpty(json)) {
Logger.w("WebDAV: JSON数据为空"); Logger.w("WebDAV: JSON数据为空");
json = "[]"; // 确保至少有一个有效的JSON数组 json = "[]"; // 确保至少有一个有效的JSON数组
} }
// 记录JSON的前500个字符
Logger.d("WebDAV: JSON前500字符: " + json.substring(0, Math.min(500, json.length())));
// 确保目录存在(如果baseUrl包含子目录) // 确保目录存在(如果baseUrl包含子目录)
if (syncMode == SyncMode.ACCOUNT && !TextUtils.isEmpty(baseUrl)) { if (syncMode == SyncMode.ACCOUNT && !TextUtils.isEmpty(baseUrl)) {
try { try {
@@ -338,18 +379,22 @@ public class WebDAVSyncManager {
public boolean downloadHistory() { public boolean downloadHistory() {
if (!isConfigured()) { if (!isConfigured()) {
Logger.e("WebDAV: 未配置,无法下载观看记录"); Logger.e("WebDAV: 未配置,无法下载观看记录");
Logger.e("WebDAV: baseUrl=" + baseUrl + ", username=" + username);
return false; return false;
} }
try { try {
String fileUrl = getFileUrl(HISTORY_FILE); String fileUrl = getFileUrl(HISTORY_FILE);
Logger.d("WebDAV: 检查文件是否存在: " + fileUrl);
// 检查文件是否存在 // 检查文件是否存在
if (!sardine.exists(fileUrl)) { if (!sardine.exists(fileUrl)) {
Logger.d("WebDAV: 观看记录文件不存在,跳过下载"); Logger.w("WebDAV: 观看记录文件不存在,跳过下载");
return false; return false;
} }
Logger.d("WebDAV: 文件存在,开始下载");
// 下载文件(使用循环读取,避免available()不准确的问题) // 下载文件(使用循环读取,避免available()不准确的问题)
InputStream is = sardine.get(fileUrl); InputStream is = sardine.get(fileUrl);
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
@@ -378,7 +423,72 @@ public class WebDAVSyncManager {
} }
// 智能合并:比较本地和远程记录,保留较新的 // 智能合并:比较本地和远程记录,保留较新的
List<History> localHistoryList = AppDatabase.get().getHistoryDao().findAll(); List<History> localHistoryList = AppDatabase.get().getHistoryDao().findAllRecent(0);
Logger.d("WebDAV: 本地记录数: " + localHistoryList.size());
Logger.d("WebDAV: 远程记录数: " + remoteHistoryList.size());
// 修复远程记录的编码问题和时间戳
Logger.d("WebDAV: 开始修复远程记录编码和时间戳...");
long currentTime = System.currentTimeMillis();
long historyTimeLimit = currentTime - com.fongmi.android.tv.Constant.HISTORY_TIME; // 60天前
for (History remote : remoteHistoryList) {
if (remote != null) {
String originalKey = remote.getKey();
// 修复key中的站点名称部分
String fixedKey = fixHistoryKey(originalKey);
if (!originalKey.equals(fixedKey)) {
Logger.d("WebDAV: 修复远程key: '" + originalKey + "' -> '" + fixedKey + "'");
remote.setKey(fixedKey);
}
String originalName = remote.getVodName();
String fixedName = fixEncodingIfNeeded(originalName);
if (!originalName.equals(fixedName)) {
Logger.d("WebDAV: 修复远程vodName: '" + originalName + "' -> '" + fixedName + "'");
remote.setVodName(fixedName);
}
// 关键修复:确保createTime在60天内,否则会被过滤掉!
long remoteCreateTime = remote.getCreateTime();
if (remoteCreateTime < historyTimeLimit) {
Logger.d("WebDAV: 修复过期时间戳: " + remote.getVodName() +
" createTime=" + remoteCreateTime + " -> " + currentTime +
" (已过期 " + ((currentTime - remoteCreateTime) / (24*60*60*1000)) + " 天)");
remote.setCreateTime(currentTime);
}
// 记录前3条远程数据的详细信息
if (remoteHistoryList.indexOf(remote) < 3) {
Logger.d("WebDAV: 远程记录[" + remoteHistoryList.indexOf(remote) + "]: " +
remote.getVodName() + " (key=" + remote.getKey() +
", cid=" + remote.getCid() +
", createTime=" + remote.getCreateTime() + ")");
}
}
}
// 修复本地记录的编码问题(重要!)
Logger.d("WebDAV: 开始修复本地记录编码...");
for (History local : localHistoryList) {
if (local != null) {
String originalKey = local.getKey();
// 修复key中的站点名称部分
String fixedKey = fixHistoryKey(originalKey);
if (!originalKey.equals(fixedKey)) {
Logger.d("WebDAV: 修复本地key: '" + originalKey + "' -> '" + fixedKey + "'");
local.setKey(fixedKey);
}
// 记录前3条本地数据的详细信息
if (localHistoryList.indexOf(local) < 3) {
Logger.d("WebDAV: 本地记录[" + localHistoryList.indexOf(local) + "]: " +
local.getVodName() + " (key=" + local.getKey() +
", cid=" + local.getCid() +
", createTime=" + local.getCreateTime() + ")");
}
}
}
// 创建本地记录的映射(key -> History // 创建本地记录的映射(key -> History
java.util.Map<String, History> localMap = new java.util.HashMap<>(); java.util.Map<String, History> localMap = new java.util.HashMap<>();
@@ -387,11 +497,14 @@ public class WebDAVSyncManager {
localMap.put(local.getKey(), local); localMap.put(local.getKey(), local);
} }
} }
Logger.d("WebDAV: 本地记录映射大小: " + localMap.size());
// 合并远程记录 // 合并远程记录
List<History> toInsert = new java.util.ArrayList<>(); List<History> toInsert = new java.util.ArrayList<>();
List<History> toUpdate = new java.util.ArrayList<>(); List<History> toUpdate = new java.util.ArrayList<>();
Logger.d("WebDAV: 开始合并 " + remoteHistoryList.size() + " 条远程记录...");
for (History remote : remoteHistoryList) { for (History remote : remoteHistoryList) {
// 验证远程记录 // 验证远程记录
if (remote == null || TextUtils.isEmpty(remote.getKey())) { if (remote == null || TextUtils.isEmpty(remote.getKey())) {
@@ -403,43 +516,110 @@ public class WebDAVSyncManager {
if (local == null) { if (local == null) {
// 本地没有,直接添加 // 本地没有,直接添加
Logger.d("WebDAV: 发现新记录: " + remote.getVodName() + " (key=" + remote.getKey() + ")");
toInsert.add(remote); toInsert.add(remote);
} else { } else {
// 本地有,比较createTime,保留较新的 Logger.d("WebDAV: 本地已有记录: " + remote.getVodName() + ", 比较时间 remote=" + remote.getCreateTime() + " local=" + local.getCreateTime());
if (remote.getCreateTime() > local.getCreateTime()) {
// 远程更新,更新本地 // 改进的合并策略:优先保留较新的记录,但也要比较播放进度
toUpdate.add(remote); long remotePos = remote.getPosition();
} else if (remote.getCreateTime() == local.getCreateTime()) { long localPos = local.getPosition();
// 时间相同,比较position,保留进度更靠后的 long remoteTime = remote.getCreateTime();
// 注意:position可能是C.TIME_UNSET(负数),需要处理 long localTime = local.getCreateTime();
long remotePos = remote.getPosition();
long localPos = local.getPosition(); boolean shouldUpdate = false;
// 如果都是有效值(>=0),比较大小;如果有无效值,保留有效值 String reason = "";
// 策略1:如果远程时间更新,直接更新
if (remoteTime > localTime) {
shouldUpdate = true;
reason = "远程时间更新 (" + remoteTime + " > " + localTime + ")";
}
// 策略2:如果时间相同或相近(误差1秒内),比较播放进度
else if (Math.abs(remoteTime - localTime) <= 1000) {
if (remotePos >= 0 && localPos >= 0) { if (remotePos >= 0 && localPos >= 0) {
if (remotePos > localPos) { if (remotePos > localPos) {
toUpdate.add(remote); shouldUpdate = true;
reason = "播放进度更新 (" + remotePos + " > " + localPos + ")";
} else {
reason = "本地进度更新或相同";
} }
} else if (remotePos >= 0 && localPos < 0) { } else if (remotePos >= 0 && localPos < 0) {
// 远程有效,本地无效,更新 shouldUpdate = true;
toUpdate.add(remote); reason = "远程有有效进度,本地无效";
} else {
reason = "保留本地";
} }
// 否则保留本地,不更新
} }
// 否则保留本地,不更新 // 策略3:即使本地时间更新,如果远程有更大的播放进度,也更新
else if (remoteTime < localTime) {
if (remotePos >= 0 && localPos >= 0 && remotePos > localPos + 60000) {
// 远程进度领先本地超过1分钟,可能是用户在另一台设备继续观看
shouldUpdate = true;
reason = "虽然本地时间更新,但远程进度显著领先 (" + remotePos + " > " + localPos + ")";
} else {
reason = "本地时间更新 (" + localTime + " > " + remoteTime + "),保留本地";
}
}
if (shouldUpdate) {
Logger.d("WebDAV: → 将更新本地 - " + reason);
toUpdate.add(remote);
} else {
Logger.d("WebDAV: → 保留本地 - " + reason);
}
} }
} }
Logger.d("WebDAV: 合并完成,待插入 " + toInsert.size() + " 条,待更新 " + toUpdate.size() + "");
// 执行插入和更新 // 执行插入和更新
if (!toInsert.isEmpty()) { if (!toInsert.isEmpty()) {
Logger.d("WebDAV: 开始插入 " + toInsert.size() + " 条新记录...");
AppDatabase.get().getHistoryDao().insert(toInsert); AppDatabase.get().getHistoryDao().insert(toInsert);
Logger.d("WebDAV: 新增 " + toInsert.size() + " 条观看记录"); Logger.d("WebDAV: 新增 " + toInsert.size() + " 条观看记录");
for (History h : toInsert) {
Logger.d("WebDAV: ✓ 新增 - " + h.getVodName() + " (cid=" + h.getCid() + ", key=" + h.getKey() + ")");
}
} else {
Logger.d("WebDAV: 没有需要插入的新记录");
} }
if (!toUpdate.isEmpty()) { if (!toUpdate.isEmpty()) {
Logger.d("WebDAV: 开始更新 " + toUpdate.size() + " 条记录...");
AppDatabase.get().getHistoryDao().update(toUpdate); AppDatabase.get().getHistoryDao().update(toUpdate);
Logger.d("WebDAV: 更新 " + toUpdate.size() + " 条观看记录"); Logger.d("WebDAV: 更新 " + toUpdate.size() + " 条观看记录");
for (History h : toUpdate) {
Logger.d("WebDAV: ✓ 更新 - " + h.getVodName() + " (cid=" + h.getCid() + ")");
}
} else {
Logger.d("WebDAV: 没有需要更新的记录");
} }
Logger.d("WebDAV: 观看记录合并完成,远程 " + remoteHistoryList.size() + " 条,本地 " + localHistoryList.size() + ""); Logger.d("WebDAV: 观看记录合并完成,远程 " + remoteHistoryList.size() + " 条,本地 " + localHistoryList.size() + "");
// 验证数据库中的记录总数
List<History> allInDb = AppDatabase.get().getHistoryDao().findAllRecent(0);
Logger.d("WebDAV: 数据库中总共有 " + allInDb.size() + " 条观看记录");
// 输出数据库中前5条记录的详细信息
Logger.d("WebDAV: === 数据库中的记录(前5条)===");
for (int i = 0; i < Math.min(5, allInDb.size()); i++) {
History h = allInDb.get(i);
Logger.d("WebDAV: [" + i + "] " + h.getVodName() +
" (key=" + h.getKey() +
", cid=" + h.getCid() +
", createTime=" + h.getCreateTime() + ")");
}
Logger.d("WebDAV: =========================");
// 强制触发UI刷新(即使没有新增或更新,也刷新一次以确保显示)
Logger.d("WebDAV: 触发UI刷新事件");
App.post(() -> {
RefreshEvent.history();
Logger.d("WebDAV: UI刷新事件已发送到主线程");
});
return true; // 即使远程为空,也算同步成功 return true; // 即使远程为空,也算同步成功
} catch (Exception e) { } catch (Exception e) {
Logger.e("WebDAV: 观看记录下载失败: " + e.getMessage()); Logger.e("WebDAV: 观看记录下载失败: " + e.getMessage());
@@ -755,5 +935,76 @@ public class WebDAVSyncManager {
public void reloadConfig() { public void reloadConfig() {
loadConfig(); loadConfig();
} }
/**
* 修复History的key中的站点名称编码
* key格式: 站点key$视频ID$cid
*/
private String fixHistoryKey(String key) {
if (key == null || key.isEmpty()) {
return key;
}
try {
// 使用AppDatabase.SYMBOL分隔
String symbol = com.fongmi.android.tv.db.AppDatabase.SYMBOL;
String[] parts = key.split(java.util.regex.Pattern.quote(symbol));
if (parts.length >= 3) {
// parts[0] = 站点key, parts[1] = 视频ID, parts[2] = cid
String siteKey = parts[0];
String fixedSiteKey = fixEncodingIfNeeded(siteKey);
if (!siteKey.equals(fixedSiteKey)) {
// 重新组装key
StringBuilder newKey = new StringBuilder(fixedSiteKey);
for (int i = 1; i < parts.length; i++) {
newKey.append(symbol).append(parts[i]);
}
return newKey.toString();
}
}
} catch (Exception e) {
Logger.e("WebDAV: 修复History key失败: " + e.getMessage());
}
return key;
}
/**
* 修复字符串编码问题
* 尝试将错误编码的UTF-8字符串修复为正确的UTF-8
*/
private String fixEncodingIfNeeded(String str) {
if (str == null || str.isEmpty()) {
return str;
}
try {
// 检查字符串中是否包含明显的乱码特征
// 1. 包含替换字符 U+FFFD
// 2. 包含异常的低位控制字符
boolean needsFix = false;
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
if (c == '\uFFFD' || (c >= 0x80 && c < 0xA0)) {
needsFix = true;
break;
}
}
if (needsFix) {
// 尝试修复:假设原始数据是UTF-8,但被错误地当作ISO-8859-1解码
byte[] bytes = str.getBytes(java.nio.charset.StandardCharsets.ISO_8859_1);
String fixed = new String(bytes, java.nio.charset.StandardCharsets.UTF_8);
Logger.d("WebDAV: 编码修复 '" + str + "' -> '" + fixed + "'");
return fixed;
}
} catch (Exception e) {
Logger.e("WebDAV: 编码修复失败: " + e.getMessage());
}
return str;
}
} }
@@ -58,7 +58,7 @@ public class HistoryActivity extends BaseActivity implements HistoryAdapter.OnCl
} }
private void getHistory() { private void getHistory() {
mAdapter.addAll(History.get()); mAdapter.addAll(History.getAll()); // 显示所有视频源的观看记录
mBinding.delete.setVisibility(mAdapter.getItemCount() > 0 ? View.VISIBLE : View.GONE); mBinding.delete.setVisibility(mAdapter.getItemCount() > 0 ? View.VISIBLE : View.GONE);
updateEmptyState(); updateEmptyState();
} }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

-69
View File
@@ -1,69 +0,0 @@
{
"name": "广告过滤配置示例",
"description": "演示如何配置广告域名黑名单,阻止视频中途弹出的广告(如澳门新葡京等博彩广告)",
"说明": {
"内置拦截": "应用已内置常见广告域名库,包括:澳门新葡京、皇冠、金沙等博彩广告;Google、百度、淘宝等广告联盟;优酷、爱奇艺等视频平台广告",
"自定义拦截": "可以在配置文件中添加ads字段,补充需要拦截的广告域名",
"支持正则": "域名支持正则表达式匹配,使用 .* 作为通配符"
},
"配置示例": {
"spider": "your_spider_url",
"sites": [],
"ads": [
"注释: 以下是自定义广告域名列表,会与内置域名库合并使用",
"注释: 精确匹配 - 直接写完整域名",
"mimg.0c1q0l.cn",
"www.92424.cn",
"vip.ffzyad.com",
"注释: 模糊匹配 - 使用通配符",
".*\\.doubleclick\\.net",
".*\\.googlesyndication\\.com",
"注释: 关键词匹配 - 拦截包含特定关键词的域名",
".*葡京.*",
".*皇冠.*",
".*金沙.*",
".*casino.*",
".*bet.*",
"注释: 特定平台的广告",
"wan.51img1.com",
"k.jinxiuzhilv.com",
"ssl.kdd.cc"
]
},
"常见问题": {
"Q1": "为什么配置了还是有广告?",
"A1": "1. 检查广告域名是否正确;2. 某些广告可能直接嵌入视频流,无法通过域名拦截;3. 尝试使用片头片尾跳过功能",
"Q2": "如何找到广告的域名?",
"A2": "1. 使用浏览器开发者工具查看网络请求;2. 查看应用日志中的URL;3. 参考其他用户分享的广告域名列表",
"Q3": "会不会误拦截正常内容?",
"A3": "内置域名库经过筛选,主要针对已知广告。如有误拦截,可以反馈给开发者"
},
"片头片尾跳过": {
"说明": "对于嵌入视频流中的广告,可以使用片头片尾跳过功能",
"使用方法": [
"1. 播放视频时,在片头(前5分钟内)按【片头】按钮,记录当前时间点",
"2. 在片尾(后5分钟内)按【片尾】按钮,记录结束前的时间点",
"3. 下次播放相同视频时,会自动跳过片头,并在片尾前停止",
"4. 如需重置,长按对应按钮即可"
]
},
"技术说明": {
"拦截层级": "WebView网络请求层拦截",
"拦截方式": "返回空响应,阻止广告内容加载",
"性能影响": "极小,仅在WebView解析时生效",
"隐私保护": "所有拦截在本地进行,不上传任何数据"
}
}
-75
View File
@@ -1,75 +0,0 @@
{
"lives": [
{
"name": "M3U",
"url": "file://Download/live.m3u"
},
{
"name": "TXT",
"url": "file://Download/live.txt",
"epg": "https://epg.112114.xyz/?ch={name}&date={date}",
"logo": "https://epg.112114.xyz/logo/{name}.png"
},
{
"name": "UA",
"url": "file://Download/live.txt",
"epg": "https://epg.112114.xyz/?ch={name}&date={date}",
"logo": "https://epg.112114.xyz/logo/{name}.png",
"ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36",
"referer": "https://github.com/"
},
{
"name": "Custom",
"boot": false,
"pass": true,
"url": "file://Download/live.txt",
"epg": "https://epg.112114.xyz/?ch={name}&date={date}&serverTimeZone=Asia/Shanghai",
"logo": "https://epg.112114.xyz/logo/{name}.png",
"header": {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36",
"Referer": "https://github.com/"
},
"catchup": {
"days": "7",
"type": "append",
"regex": "/PLTV/",
"replace": "/PLTV/,/TVOD/",
"source": "?playseek=${(b)yyyyMMddHHmmss}-${(e)yyyyMMddHHmmss}"
}
},
{
"name": "JSON",
"type": 1,
"url": "file://Download/live.json"
},
{
"name": "Spider-JS",
"type": 3,
"api": "./live.js",
"ext": ""
},
{
"name": "Spider-Python",
"type": 3,
"api": "./live.py",
"ext": ""
}
],
"headers": [
{
"host": "gslbserv.itv.cmvideo.cn",
"header": {
"User-Agent": "okhttp/3.12.13"
}
}
],
"proxy": [
"raw.githubusercontent.com"
],
"hosts": [
"cache.ott.ystenlive.itv.cmvideo.cn=base-v4-free-mghy.e.cdn.chinamobile.com"
],
"ads": [
"static-mozai.4gtv.tv"
]
}
-75
View File
@@ -1,75 +0,0 @@
{
"lives": [
{
"name": "M3U",
"url": "https://github.com/live.m3u"
},
{
"name": "TXT",
"url": "https://github.com/live.txt",
"epg": "https://epg.112114.xyz/?ch={name}&date={date}",
"logo": "https://epg.112114.xyz/logo/{name}.png"
},
{
"name": "UA",
"url": "https://github.com/live.txt",
"epg": "https://epg.112114.xyz/?ch={name}&date={date}",
"logo": "https://epg.112114.xyz/logo/{name}.png",
"ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36",
"referer": "https://github.com/"
},
{
"name": "Custom",
"boot": false,
"pass": true,
"url": "https://github.com/live.txt",
"epg": "https://epg.112114.xyz/?ch={name}&date={date}&serverTimeZone=Asia/Shanghai",
"logo": "https://epg.112114.xyz/logo/{name}.png",
"header": {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36",
"Referer": "https://github.com/"
},
"catchup": {
"days": "7",
"type": "append",
"regex": "/PLTV/",
"replace": "/PLTV/,/TVOD/",
"source": "?playseek=${(b)yyyyMMddHHmmss}-${(e)yyyyMMddHHmmss}"
}
},
{
"name": "JSON",
"type": 1,
"url": "https://github.com/live.json"
},
{
"name": "Spider-JS",
"type": 3,
"api": "https://github.com/live.js",
"ext": ""
},
{
"name": "Spider-Python",
"type": 3,
"api": "https://github.com/live.py",
"ext": ""
}
],
"headers": [
{
"host": "gslbserv.itv.cmvideo.cn",
"header": {
"User-Agent": "okhttp/3.12.13"
}
}
],
"proxy": [
"raw.githubusercontent.com"
],
"hosts": [
"cache.ott.ystenlive.itv.cmvideo.cn=base-v4-free-mghy.e.cdn.chinamobile.com"
],
"ads": [
"static-mozai.4gtv.tv"
]
}
-70
View File
@@ -1,70 +0,0 @@
{
"spider": "file://Download/custom_spider.jar",
"sites": [
{
"key": "one",
"name": "One",
"type": 3,
"api": "csp_Csp",
"searchable": 1,
"changeable": 1,
"ext": "file://Download/one.json"
},
{
"key": "two",
"name": "Two",
"type": 3,
"api": "csp_Csp",
"searchable": 1,
"changeable": 1,
"ext": "file://Download/two.json"
},
{
"key": "extend",
"name": "Extend",
"type": 3,
"api": "csp_Csp",
"searchable": 1,
"changeable": 1,
"ext": "file://Download/extend.json",
"jar": "file://Download/extend.jar"
}
],
"parses": [
{
"name": "官方",
"type": 1,
"url": "https://google.com/api/?url="
}
],
"doh": [
{
"name": "Google",
"url": "https://dns.google/dns-query",
"ips": [
"8.8.4.4",
"8.8.8.8"
]
}
],
"headers": [
{
"host": "gslbserv.itv.cmvideo.cn",
"header": {
"User-Agent": "okhttp/3.12.13"
}
}
],
"proxy": [
"raw.githubusercontent.com"
],
"hosts": [
"cache.ott.ystenlive.itv.cmvideo.cn=base-v4-free-mghy.e.cdn.chinamobile.com"
],
"flags": [
"qq"
],
"ads": [
"static-mozai.4gtv.tv"
]
}
-70
View File
@@ -1,70 +0,0 @@
{
"spider": "https://github.com/custom_spider.jar",
"sites": [
{
"key": "one",
"name": "One",
"type": 3,
"api": "csp_Csp",
"searchable": 1,
"changeable": 1,
"ext": "https://github.com/one.json"
},
{
"key": "two",
"name": "Two",
"type": 3,
"api": "csp_Csp",
"searchable": 1,
"changeable": 1,
"ext": "https://github.com/two.json"
},
{
"key": "extend",
"name": "Extend",
"type": 3,
"api": "csp_Csp",
"searchable": 1,
"changeable": 1,
"ext": "https://github.com/extend.json",
"jar": "https://github.com/extend.jar"
}
],
"parses": [
{
"name": "官方",
"type": 1,
"url": "https://google.com/api/?url="
}
],
"doh": [
{
"name": "Google",
"url": "https://dns.google/dns-query",
"ips": [
"8.8.4.4",
"8.8.8.8"
]
}
],
"headers": [
{
"host": "gslbserv.itv.cmvideo.cn",
"header": {
"User-Agent": "okhttp/3.12.13"
}
}
],
"proxy": [
"raw.githubusercontent.com"
],
"hosts": [
"cache.ott.ystenlive.itv.cmvideo.cn=base-v4-free-mghy.e.cdn.chinamobile.com"
],
"flags": [
"qq"
],
"ads": [
"static-mozai.4gtv.tv"
]
}