|
|
|
|
@ -0,0 +1,346 @@ |
|
|
|
|
package com.github.catvod.spider; |
|
|
|
|
|
|
|
|
|
import android.content.Context; |
|
|
|
|
import android.content.SharedPreferences; |
|
|
|
|
import android.text.TextUtils; |
|
|
|
|
|
|
|
|
|
import com.github.catvod.bean.Class; |
|
|
|
|
import com.github.catvod.bean.Result; |
|
|
|
|
import com.github.catvod.bean.Vod; |
|
|
|
|
import com.github.catvod.crawler.Spider; |
|
|
|
|
import com.github.catvod.crawler.SpiderDebug; |
|
|
|
|
import com.google.gson.Gson; |
|
|
|
|
import com.google.gson.JsonArray; |
|
|
|
|
import com.google.gson.JsonObject; |
|
|
|
|
|
|
|
|
|
import java.util.ArrayList; |
|
|
|
|
import java.util.HashMap; |
|
|
|
|
import java.util.List; |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* 央视直播爬虫 |
|
|
|
|
* |
|
|
|
|
* 功能: |
|
|
|
|
* 1. homeContent - 显示直播列表 |
|
|
|
|
* 2. categoryContent - 显示栏目列表 + 设置 |
|
|
|
|
* 3. 支持切换内核(系统WebView/X5) |
|
|
|
|
* 4. 预留API接口 |
|
|
|
|
* |
|
|
|
|
* @author xMydev |
|
|
|
|
* @date 2025-01-22 |
|
|
|
|
*/ |
|
|
|
|
public class YangShi extends Spider { |
|
|
|
|
|
|
|
|
|
private static final String TAG = "YangShi"; |
|
|
|
|
|
|
|
|
|
// 配置存储
|
|
|
|
|
private static final String PREF_NAME = "yangshi_config"; |
|
|
|
|
private static final String KEY_USE_X5 = "use_x5_webview"; |
|
|
|
|
private static final String KEY_API_URL = "api_url"; |
|
|
|
|
|
|
|
|
|
// 预留API地址(暂未使用)
|
|
|
|
|
private String apiUrl = ""; |
|
|
|
|
|
|
|
|
|
// 默认使用系统WebView
|
|
|
|
|
private boolean useX5 = false; |
|
|
|
|
|
|
|
|
|
private SharedPreferences prefs; |
|
|
|
|
|
|
|
|
|
// ========== 写死的直播频道列表 ==========
|
|
|
|
|
private static final String[][] LIVE_CHANNELS = { |
|
|
|
|
{"1", "CCTV-1 综合", "https://tv.cctv.com/live/cctv1/"}, |
|
|
|
|
{"2", "CCTV-2 财经", "https://tv.cctv.com/live/cctv2/"}, |
|
|
|
|
{"3", "CCTV-3 综艺", "https://tv.cctv.com/live/cctv3/"}, |
|
|
|
|
{"4", "CCTV-4 中文国际", "https://tv.cctv.com/live/cctv4/"}, |
|
|
|
|
{"5", "CCTV-5 体育", "https://tv.cctv.com/live/cctv5/"}, |
|
|
|
|
{"6", "CCTV-6 电影", "https://tv.cctv.com/live/cctv6/"}, |
|
|
|
|
{"7", "CCTV-7 国防军事", "https://tv.cctv.com/live/cctv7/"}, |
|
|
|
|
{"8", "CCTV-8 电视剧", "https://tv.cctv.com/live/cctv8/"}, |
|
|
|
|
{"9", "CCTV-9 纪录", "https://tv.cctv.com/live/cctvjilu"}, |
|
|
|
|
{"10", "CCTV-10 科教", "https://tv.cctv.com/live/cctv10/"}, |
|
|
|
|
{"11", "CCTV-11 戏曲", "https://tv.cctv.com/live/cctv11/"}, |
|
|
|
|
{"12", "CCTV-12 社会与法", "https://tv.cctv.com/live/cctv12/"}, |
|
|
|
|
{"13", "CCTV-13 新闻", "https://tv.cctv.com/live/cctv13/"}, |
|
|
|
|
{"14", "CCTV-14 少儿", "https://tv.cctv.com/live/cctvchild"}, |
|
|
|
|
{"15", "CCTV-15 音乐", "https://tv.cctv.com/live/cctv15/"}, |
|
|
|
|
{"16", "CCTV-16 奥林匹克", "https://tv.cctv.com/live/cctv16/"}, |
|
|
|
|
{"17", "CCTV-17 农业农村", "https://tv.cctv.com/live/cctv17/"}, |
|
|
|
|
{"18", "CCTV-5+ 体育赛事", "https://tv.cctv.com/live/cctv5plus/"} |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
// ========== 写死的栏目分类列表 ==========
|
|
|
|
|
private static final String[][] CATEGORIES = { |
|
|
|
|
{"news", "新闻"}, |
|
|
|
|
{"tv", "电视剧"}, |
|
|
|
|
{"variety", "综艺"}, |
|
|
|
|
{"documentary", "纪录片"}, |
|
|
|
|
{"sports", "体育"}, |
|
|
|
|
{"settings", "设置"} |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
@Override |
|
|
|
|
public void init(Context context, String extend) throws Exception { |
|
|
|
|
super.init(context, extend); |
|
|
|
|
|
|
|
|
|
// 初始化SharedPreferences
|
|
|
|
|
prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); |
|
|
|
|
|
|
|
|
|
// 读取配置
|
|
|
|
|
useX5 = prefs.getBoolean(KEY_USE_X5, false); |
|
|
|
|
apiUrl = prefs.getString(KEY_API_URL, ""); |
|
|
|
|
|
|
|
|
|
SpiderDebug.log(TAG + " 初始化成功"); |
|
|
|
|
SpiderDebug.log("当前内核: " + (useX5 ? "X5 WebView" : "系统 WebView")); |
|
|
|
|
|
|
|
|
|
// TODO: 从extend参数解析API地址(预留)
|
|
|
|
|
if (!TextUtils.isEmpty(extend)) { |
|
|
|
|
try { |
|
|
|
|
JsonObject config = new Gson().fromJson(extend, JsonObject.class); |
|
|
|
|
if (config.has("api")) { |
|
|
|
|
apiUrl = config.get("api").getAsString(); |
|
|
|
|
saveConfig(KEY_API_URL, apiUrl); |
|
|
|
|
} |
|
|
|
|
} catch (Exception e) { |
|
|
|
|
SpiderDebug.log(TAG + " 解析extend失败: " + e.getMessage()); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* 首页内容 - 显示直播列表 |
|
|
|
|
*/ |
|
|
|
|
@Override |
|
|
|
|
public String homeContent(boolean filter) throws Exception { |
|
|
|
|
// TODO: 未来从API获取数据
|
|
|
|
|
// if (!TextUtils.isEmpty(apiUrl)) {
|
|
|
|
|
// return fetchFromApi(apiUrl + "/live/list");
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// 当前使用写死的数据
|
|
|
|
|
List<Class> classes = new ArrayList<>(); |
|
|
|
|
classes.add(new Class("live", "<EFBFBD><EFBFBD>视直播")); |
|
|
|
|
|
|
|
|
|
List<Vod> list = new ArrayList<>(); |
|
|
|
|
for (String[] channel : LIVE_CHANNELS) { |
|
|
|
|
Vod vod = new Vod(); |
|
|
|
|
vod.setVodId(channel[0]); |
|
|
|
|
vod.setVodName(channel[1]); |
|
|
|
|
vod.setVodPic("https://wtv.tools.yigechengzi.pro/prod-api/app/img/cctv" + channel[0] + ".png"); |
|
|
|
|
vod.setVodRemarks("直播中"); |
|
|
|
|
list.add(vod); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return Result.string(classes, list); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* 分类内容 - 显示栏目列表或设置 |
|
|
|
|
*/ |
|
|
|
|
@Override |
|
|
|
|
public String categoryContent(String tid, String pg, boolean filter, HashMap<String, String> extend) throws Exception { |
|
|
|
|
List<Vod> list = new ArrayList<>(); |
|
|
|
|
|
|
|
|
|
// 特殊处理:设置分类
|
|
|
|
|
if ("settings".equals(tid)) { |
|
|
|
|
return getSettingsContent(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// TODO: 未来从API获取栏目数据
|
|
|
|
|
// if (!TextUtils.isEmpty(apiUrl)) {
|
|
|
|
|
// return fetchFromApi(apiUrl + "/category/" + tid + "?page=" + pg);
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// 当前返回写死的示例数据
|
|
|
|
|
for (int i = 0; i < 10; i++) { |
|
|
|
|
Vod vod = new Vod(); |
|
|
|
|
vod.setVodId(tid + "_" + i); |
|
|
|
|
vod.setVodName(getCategoryName(tid) + " 节目 " + i); |
|
|
|
|
vod.setVodPic("https://via.placeholder.com/300x400"); |
|
|
|
|
vod.setVodRemarks("更新至第" + i + "期"); |
|
|
|
|
list.add(vod); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return Result.string(list); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* 获取设置内容 |
|
|
|
|
*/ |
|
|
|
|
private String getSettingsContent() { |
|
|
|
|
List<Vod> list = new ArrayList<>(); |
|
|
|
|
|
|
|
|
|
// 设置项1:切换内核
|
|
|
|
|
Vod kernelSetting = new Vod(); |
|
|
|
|
kernelSetting.setVodId("setting_kernel"); |
|
|
|
|
kernelSetting.setVodName("切换内核"); |
|
|
|
|
kernelSetting.setVodPic("https://via.placeholder.com/300x400?text=Kernel"); |
|
|
|
|
kernelSetting.setVodRemarks("当前: " + (useX5 ? "X5 WebView" : "系统 WebView")); |
|
|
|
|
kernelSetting.setVodContent("点击切换播放内核\n\n" + |
|
|
|
|
"系统 WebView: 轻量级,兼容性好\n" + |
|
|
|
|
"X5 WebView: 功能强大,适合复杂页面\n\n" + |
|
|
|
|
"当前使用: " + (useX5 ? "X5 WebView" : "系统 WebView")); |
|
|
|
|
list.add(kernelSetting); |
|
|
|
|
|
|
|
|
|
// 设置项2:API地址(预留)
|
|
|
|
|
Vod apiSetting = new Vod(); |
|
|
|
|
apiSetting.setVodId("setting_api"); |
|
|
|
|
apiSetting.setVodName("API设置"); |
|
|
|
|
apiSetting.setVodPic("https://via.placeholder.com/300x400?text=API"); |
|
|
|
|
apiSetting.setVodRemarks(TextUtils.isEmpty(apiUrl) ? "未配置" : "已配置"); |
|
|
|
|
apiSetting.setVodContent("设置数据源API地址\n\n当前: " + |
|
|
|
|
(TextUtils.isEmpty(apiUrl) ? "使用内置数据" : apiUrl)); |
|
|
|
|
list.add(apiSetting); |
|
|
|
|
|
|
|
|
|
return Result.string(list); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* 详情内容 |
|
|
|
|
*/ |
|
|
|
|
@Override |
|
|
|
|
public String detailContent(List<String> ids) throws Exception { |
|
|
|
|
String vodId = ids.get(0); |
|
|
|
|
|
|
|
|
|
// 处理设置项的点击
|
|
|
|
|
if (vodId.startsWith("setting_")) { |
|
|
|
|
return handleSettingClick(vodId); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// TODO: 未来从API获取详情
|
|
|
|
|
// if (!TextUtils.isEmpty(apiUrl)) {
|
|
|
|
|
// return fetchFromApi(apiUrl + "/detail/" + vodId);
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// 查找对应的频道
|
|
|
|
|
for (String[] channel : LIVE_CHANNELS) { |
|
|
|
|
if (channel[0].equals(vodId)) { |
|
|
|
|
Vod vod = new Vod(); |
|
|
|
|
vod.setVodId(channel[0]); |
|
|
|
|
vod.setVodName(channel[1]); |
|
|
|
|
vod.setVodPic("https://wtv.tools.yigechengzi.pro/prod-api/app/img/cctv" + channel[0] + ".png"); |
|
|
|
|
vod.setVodYear("2024"); |
|
|
|
|
vod.setVodArea("中国"); |
|
|
|
|
vod.setVodRemarks("24小时直播"); |
|
|
|
|
vod.setVodContent("中央广播电视总台 " + channel[1] + " 官方直播频道"); |
|
|
|
|
|
|
|
|
|
// 播放信息
|
|
|
|
|
String playFrom = useX5 ? "X5-WebView" : "System-WebView"; |
|
|
|
|
vod.setVodPlayFrom(playFrom); |
|
|
|
|
vod.setVodPlayUrl("播放$" + channel[2]); |
|
|
|
|
|
|
|
|
|
return Result.string(vod); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// 未找到,返回空
|
|
|
|
|
return Result.string(new Vod()); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* 处理设置项点击 |
|
|
|
|
*/ |
|
|
|
|
private String handleSettingClick(String settingId) { |
|
|
|
|
if ("setting_kernel".equals(settingId)) { |
|
|
|
|
// 切换内核
|
|
|
|
|
useX5 = !useX5; |
|
|
|
|
saveConfig(KEY_USE_X5, useX5); |
|
|
|
|
|
|
|
|
|
SpiderDebug.log("内核已切换为: " + (useX5 ? "X5 WebView" : "系统 WebView")); |
|
|
|
|
|
|
|
|
|
// 返回更新后的设置项
|
|
|
|
|
return getSettingsContent(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return Result.string(new Vod()); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* 搜索内容 |
|
|
|
|
*/ |
|
|
|
|
@Override |
|
|
|
|
public String searchContent(String key, boolean quick) throws Exception { |
|
|
|
|
// TODO: 未来从API搜索
|
|
|
|
|
// if (!TextUtils.isEmpty(apiUrl)) {
|
|
|
|
|
// return fetchFromApi(apiUrl + "/search?keyword=" + key);
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
List<Vod> list = new ArrayList<>(); |
|
|
|
|
|
|
|
|
|
// 简单的本地搜索
|
|
|
|
|
for (String[] channel : LIVE_CHANNELS) { |
|
|
|
|
if (channel[1].toLowerCase().contains(key.toLowerCase())) { |
|
|
|
|
Vod vod = new Vod(); |
|
|
|
|
vod.setVodId(channel[0]); |
|
|
|
|
vod.setVodName(channel[1]); |
|
|
|
|
vod.setVodPic("https://wtv.tools.yigechengzi.pro/prod-api/app/img/cctv" + channel[0] + ".png"); |
|
|
|
|
vod.setVodRemarks("直播中"); |
|
|
|
|
list.add(vod); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return Result.string(list); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* 播放器内容 |
|
|
|
|
*/ |
|
|
|
|
@Override |
|
|
|
|
public String playerContent(String flag, String id, List<String> vipFlags) throws Exception { |
|
|
|
|
HashMap<String, String> headers = new HashMap<>(); |
|
|
|
|
headers.put("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"); |
|
|
|
|
headers.put("Referer", "https://tv.cctv.com/"); |
|
|
|
|
|
|
|
|
|
// parse=1 表示需要WebView解析
|
|
|
|
|
return Result.get() |
|
|
|
|
.url(id) |
|
|
|
|
.parse(1) |
|
|
|
|
.header(headers) |
|
|
|
|
.string(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// ========== 工具方法 ==========
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* 保存配置(布尔值) |
|
|
|
|
*/ |
|
|
|
|
private void saveConfig(String key, boolean value) { |
|
|
|
|
prefs.edit().putBoolean(key, value).apply(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* 保存配置(字符串) |
|
|
|
|
*/ |
|
|
|
|
private void saveConfig(String key, String value) { |
|
|
|
|
prefs.edit().putString(key, value).apply(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* 获取分类名称 |
|
|
|
|
*/ |
|
|
|
|
private String getCategoryName(String tid) { |
|
|
|
|
for (String[] category : CATEGORIES) { |
|
|
|
|
if (category[0].equals(tid)) { |
|
|
|
|
return category[1]; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
return "未知分类"; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* 从API获取数据(预留方法) |
|
|
|
|
*/ |
|
|
|
|
private String fetchFromApi(String url) throws Exception { |
|
|
|
|
// TODO: 实现API请求逻辑
|
|
|
|
|
// String response = OkHttp.string(url);
|
|
|
|
|
// return parseApiResponse(response);
|
|
|
|
|
throw new Exception("API功能暂未实现"); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* 判断是否使用X5内核 |
|
|
|
|
*/ |
|
|
|
|
public static boolean isUseX5(Context context) { |
|
|
|
|
SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); |
|
|
|
|
return prefs.getBoolean(KEY_USE_X5, false); |
|
|
|
|
} |
|
|
|
|
} |