文章目录
TVBox官网
TVBox项目索引:https://github.com/o0HalfLife0o/TVBoxOSC/
核心代码分析
源内容的结构定义
// https://github.com/takagen99/Box/blob/main/app/src/main/java/com/github/tvbox/osc/api/ApiConfig.java
public class ApiConfig {
private static ApiConfig instance;
private final LinkedHashMap<String, SourceBean> sourceBeanList;
private SourceBean mHomeSource;
private ParseBean mDefaultParse;
private final List<LiveChannelGroup> liveChannelGroupList;
private final List<ParseBean> parseBeanList;
private List<String> vipParseFlags;
private List<IJKCode> ijkCodes;
private String spider = null;
public String wallpaper = "";
public JsonArray livePlayHeaders;
private final SourceBean emptyHome = new SourceBean();
private final JarLoader jarLoader = new JarLoader();
private final JsLoader jsLoader = new JsLoader();
private final String userAgent = "okhttp/3.15";
private final String requestAccept = "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9";
源内容的主体结构解析
// https://github.com/takagen99/Box/blob/main/app/src/main/java/com/github/tvbox/osc/api/ApiConfig.java
private void parseJson(String apiUrl, String jsonStr) {
JsonObject infoJson = new Gson().fromJson(jsonStr, JsonObject.class);
// ==> spider字段
spider = DefaultConfig.safeJsonString(infoJson, "spider", "");
// ==> wallpaper字段
wallpaper = DefaultConfig.safeJsonString(infoJson, "wallpaper", "");
// ==> 直播播放请求头,livePlayHeaders字段
livePlayHeaders = infoJson.getAsJsonArray("livePlayHeaders");
// ==> 远端站点源,video.sites[] 或 sites[]
SourceBean firstSite = null;
JsonArray sites = infoJson.has("video") ? infoJson.getAsJsonObject("video").getAsJsonArray("sites") : infoJson.get("sites").getAsJsonArray();
for (JsonElement opt : sites) {
JsonObject obj = (JsonObject) opt;
// ==> 远端站点元素的解析,具体的字段
// public class SourceBean {
// private String key;
// private String name;
// private String api;
// private int type; // 0 xml 1 json 3 Spider
// private int searchable; // 是否可搜索
// private int quickSearch; // 是否可以快速搜索
// private int filterable; // 可筛选?
// private int hide; // 设置的选择列表里隐藏
// private String playerUrl; // 站点解析Url
// private String ext; // 扩展数据
// private String jar; // 自定义jar
// private ArrayList<String> categories = null; // 分类&排序
// private int playerType; // 0 system 1 ikj 2 exo 10 mxplayer -1 以参数设置页面的为准
// private String clickSelector; // 需要点击播放的嗅探站点selector ddrk.me;#id
SourceBean sb = new SourceBean();
String siteKey = obj.get("key").getAsString().trim();
sb.setKey(siteKey);
sb.setName(obj.get("name").getAsString().trim());
sb.setType(obj.get("type").getAsInt());
sb.setApi(obj.get("api").getAsString().trim());
sb.setSearchable(DefaultConfig.safeJsonInt(obj, "searchable", 1));
sb.setQuickSearch(DefaultConfig.safeJsonInt(obj, "quickSearch", 1));
sb.setFilterable(DefaultConfig.safeJsonInt(obj, "filterable", 1));
sb.setHide(DefaultConfig.safeJsonInt(obj, "hide", 0));
sb.setPlayerUrl(DefaultConfig.safeJsonString(obj, "playUrl", ""));
if (obj.has("ext") && (obj.get("ext").isJsonObject() || obj.get("ext").isJsonArray())) {
sb.setExt(obj.get("ext").toString());
} else {
sb.setExt(DefaultConfig.safeJsonString(obj, "ext", ""));
}
sb.setJar(DefaultConfig.safeJsonString(obj, "jar", ""));
sb.setPlayerType(DefaultConfig.safeJsonInt(obj, "playerType", -1));
sb.setCategories(DefaultConfig.safeJsonStringList(obj, "categories"));
sb.setClickSelector(DefaultConfig.safeJsonString(obj, "click", ""));
if (firstSite == null && sb.getHide() == 0)
firstSite = sb;
sourceBeanList.put(siteKey, sb);
}
// 根据配置来设置主页
if (sourceBeanList != null && sourceBeanList.size() > 0) {
// “Hawk” 是一个适用于 Android 的简单而高效的键值存储库。
String home = Hawk.get(HawkConfig.HOME_API, "");
SourceBean sh = getSource(home);
if (sh == null || sh.getHide() == 1)
setSourceBean(firstSite);
else
setSourceBean(sh);
}
// ==> 需要使用vip账号来解析的flag,flags[String]字段
vipParseFlags = DefaultConfig.safeJsonStringList(infoJson, "flags");
// 解析地址
parseBeanList.clear();
if (infoJson.has("parses")) {
// ==> parses[]字段
JsonArray parses = infoJson.get("parses").getAsJsonArray();
for (JsonElement opt : parses) {
JsonObject obj = (JsonObject) opt;
// ==> 解析具体的Parse(解析方式)的字段
// public class ParseBean {
// private String name;
// private String url;
// private String ext;
// private int type; // 0 普通嗅探 1 json 2 Json扩展 3 聚合
ParseBean pb = new ParseBean();
pb.setName(obj.get("name").getAsString().trim());
pb.setUrl(obj.get("url").getAsString().trim());
String ext = obj.has("ext") ? obj.get("ext").getAsJsonObject().toString() : "";
pb.setExt(ext);
pb.setType(DefaultConfig.safeJsonInt(obj, "type", 0));
parseBeanList.add(pb);
}
}
// 获取默认解析方式
if (parseBeanList != null && parseBeanList.size() > 0) {
String defaultParse = Hawk.get(HawkConfig.DEFAULT_PARSE, "");
if (!TextUtils.isEmpty(defaultParse))
for (ParseBean pb : parseBeanList) {
if (pb.getName().equals(defaultParse))
setDefaultParse(pb);
}
if (mDefaultParse == null)
setDefaultParse(parseBeanList.get(0));
}
// takagen99: Check if Live URL is setup in Settings, if no, get from File Config
liveChannelGroupList.clear(); //修复从后台切换重复加载频道列表
String liveURL = Hawk.get(HawkConfig.LIVE_URL, "");
String epgURL = Hawk.get(HawkConfig.EPG_URL, "");
String liveURL_final = null;
try {
// ==> lives[]字段
if (infoJson.has("lives") && infoJson.get("lives").getAsJsonArray() != null) {
// ==> lives[]字段的进一步解析:其第1个元素比较特殊,含有proxy://(代理)、http或clan(liveUrl)、epg(直播节目预告)
JsonObject livesOBJ = infoJson.get("lives").getAsJsonArray().get(0).getAsJsonObject();
String lives = livesOBJ.toString();
int index = lives.indexOf("proxy://");
if (index != -1) {
// 如果含有代理URL部分的代码
int endIndex = lives.lastIndexOf("\"");
String url = lives.substring(index, endIndex);
url = DefaultConfig.checkReplaceProxy(url);
//clan
String extUrl = Uri.parse(url).getQueryParameter("ext");
if (extUrl != null && !extUrl.isEmpty()) {
String extUrlFix;
if (extUrl.startsWith("http") || extUrl.startsWith("clan://")) {
extUrlFix = extUrl;
} else {
extUrlFix = new String(Base64.decode(extUrl, Base64.DEFAULT | Base64.URL_SAFE | Base64.NO_WRAP), "UTF-8");
}
if (extUrlFix.startsWith("clan://")) {
extUrlFix = clanContentFix(clanToAddress(apiUrl), extUrlFix);
}
// takagen99: Capture Live URL into Config
System.out.println("Live URL :" + extUrlFix);
putLiveHistory(extUrlFix);
// Overwrite with Live URL from Settings
if (StringUtils.isBlank(liveURL)) {
Hawk.put(HawkConfig.LIVE_URL, extUrlFix);
} else {
extUrlFix = liveURL;
}
// Final Live URL
liveURL_final = extUrlFix;
// // Encoding the Live URL
// extUrlFix = Base64.encodeToString(extUrlFix.getBytes("UTF-8"), Base64.DEFAULT | Base64.URL_SAFE | Base64.NO_WRAP);
// url = url.replace(extUrl, extUrlFix);
}
// takagen99 : Getting EPG URL from File Config & put into Settings
if (livesOBJ.has("epg")) {
String epg = livesOBJ.get("epg").getAsString();
System.out.println("EPG URL :" + epg);
putEPGHistory(epg);
// Overwrite with EPG URL from Settings
if (StringUtils.isBlank(epgURL)) {
Hawk.put(HawkConfig.EPG_URL, epg);
} else {
Hawk.put(HawkConfig.EPG_URL, epgURL);
}
}
// // Populate Live Channel Listing
// LiveChannelGroup liveChannelGroup = new LiveChannelGroup();
// liveChannelGroup.setGroupName(url);
// liveChannelGroupList.add(liveChannelGroup);
} else {
// 不含有代理URL部分的代码
// ==> lives[]字段的第1个元素:type字段,有了进行细致解析,没有直接把lives[]交给loadLives()函数处理
// if FongMi Live URL Formatting exists
if (!lives.contains("type")) {
loadLives(infoJson.get("lives").getAsJsonArray());
} else {
// ==> lives[]字段的第1个元素有type字段,细致解析:playerType字段
JsonObject fengMiLives = infoJson.get("lives").getAsJsonArray().get(0).getAsJsonObject();
Hawk.put(HawkConfig.LIVE_PLAYER_TYPE, DefaultConfig.safeJsonInt(fengMiLives, "playerType", -1));
String type = fengMiLives.get("type").getAsString();
if (type.equals("0")) {
// ==> lives[]字段的第1个元素type字段==0,细致解析:url字段、epg字段
String url = fengMiLives.get("url").getAsString();
// takagen99 : Getting EPG URL from File Config & put into Settings
if (fengMiLives.has("epg")) {
String epg = fengMiLives.get("epg").getAsString();
System.out.println("EPG URL :" + epg);
putEPGHistory(epg);
// Overwrite with EPG URL from Settings
if (StringUtils.isBlank(epgURL)) {
Hawk.put(HawkConfig.EPG_URL, epg);
} else {
Hawk.put(HawkConfig.EPG_URL, epgURL);
}
}
if (url.startsWith("http")) {
// takagen99: Capture Live URL into Settings
System.out.println("Live URL :" + url);
putLiveHistory(url);
// Overwrite with Live URL from Settings
if (StringUtils.isBlank(liveURL)) {
Hawk.put(HawkConfig.LIVE_URL, url);
} else {
url = liveURL;
}
// Final Live URL
liveURL_final = url;
// url = Base64.encodeToString(url.getBytes("UTF-8"), Base64.DEFAULT | Base64.URL_SAFE | Base64.NO_WRAP);
}
}
}
}
// takagen99: Load Live Channel from settings URL (WIP)
if (StringUtils.isBlank(liveURL_final)) {
liveURL_final = liveURL;
}
liveURL_final = Base64.encodeToString(liveURL_final.getBytes("UTF-8"), Base64.DEFAULT | Base64.URL_SAFE | Base64.NO_WRAP);
liveURL_final = "http://127.0.0.1:9978/proxy?do=live&type=txt&ext=" + liveURL_final;
LiveChannelGroup liveChannelGroup = new LiveChannelGroup();
liveChannelGroup.setGroupName(liveURL_final);
liveChannelGroupList.add(liveChannelGroup);
}
} catch (Throwable th) {
th.printStackTrace();
}
// ==> host的视频解析规则组,rules[]字段
// Video parse rule for host
if (infoJson.has("rules")) {
VideoParseRuler.clearRule();
for(JsonElement oneHostRule : infoJson.getAsJsonArray("rules")) {
JsonObject obj = (JsonObject) oneHostRule;
// ==> 继续解析规则字段:host、rule[]、filter[]
if (obj.has("host")) {
String host = obj.get("host").getAsString();
if (obj.has("rule")) {
JsonArray ruleJsonArr = obj.getAsJsonArray("rule");
ArrayList<String> rule = new ArrayList<>();
for (JsonElement one : ruleJsonArr) {
String oneRule = one.getAsString();
rule.add(oneRule);
}
if (rule.size() > 0) {
VideoParseRuler.addHostRule(host, rule);
}
}
if (obj.has("filter")) {
JsonArray filterJsonArr = obj.getAsJsonArray("filter");
ArrayList<String> filter = new ArrayList<>();
for (JsonElement one : filterJsonArr) {
String oneFilter = one.getAsString();
filter.add(oneFilter);
}
if (filter.size() > 0) {
VideoParseRuler.addHostFilter(host, filter);
}
}
}
// ==> 继续解析规则字段:hosts[]、regex[]
if (obj.has("hosts") && obj.has("regex")) {
ArrayList<String> rule = new ArrayList<>();
ArrayList<String> ads = new ArrayList<>();
JsonArray regexArray = obj.getAsJsonArray("regex");
for (JsonElement one : regexArray) {
String regex = one.getAsString();
if (M3U8.isAd(regex)) ads.add(regex);
else rule.add(regex);
}
JsonArray array = obj.getAsJsonArray("hosts");
for (JsonElement one : array) {
String host = one.getAsString();
VideoParseRuler.addHostRule(host, rule);
VideoParseRuler.addHostRegex(host, ads);
}
}
}
}
// 该JSON数据描述了IJKPlayer播放器的一些配置选项以及一些广告服务器的域名
String defaultIJKADS = "...";
JsonObject defaultJson = new Gson().fromJson(defaultIJKADS, JsonObject.class);
// 广告地址
if (AdBlocker.isEmpty()) {
// AdBlocker.clear();
// ==> 追加的广告拦截,ads[]字段
if (infoJson.has("ads")) {
for (JsonElement host : infoJson.getAsJsonArray("ads")) {
AdBlocker.addAdHost(host.getAsString());
}
} else {
//默认广告拦截
for (JsonElement host : defaultJson.getAsJsonArray("ads")) {
AdBlocker.addAdHost(host.getAsString());
}
}
}
// IJK解码配置
if (ijkCodes == null) {
ijkCodes = new ArrayList<>();
boolean foundOldSelect = false;
String ijkCodec = Hawk.get(HawkConfig.IJK_CODEC, "");
// ==> 解析ijk[]字段
JsonArray ijkJsonArray = infoJson.has("ijk") ? infoJson.get("ijk").getAsJsonArray() : defaultJson.get("ijk").getAsJsonArray();
for (JsonElement opt : ijkJsonArray) {
JsonObject obj = (JsonObject) opt;
// ==> 继续解析单个ijk[]元素的字段:group、options[]
String name = obj.get("group").getAsString();
LinkedHashMap<String, String> baseOpt = new LinkedHashMap<>();
for (JsonElement cfg : obj.get("options").getAsJsonArray()) {
// ==> 继续解析单个ijk[]元素的options[]字段的单个元素:category、name、value
JsonObject cObj = (JsonObject) cfg;
String key = cObj.get("category").getAsString() + "|" + cObj.get("name").getAsString();
String val = cObj.get("value").getAsString();
baseOpt.put(key, val);
}
IJKCode codec = new IJKCode();
codec.setName(name);
codec.setOption(baseOpt);
if (name.equals(ijkCodec) || TextUtils.isEmpty(ijkCodec)) {
codec.selected(true);
ijkCodec = name;
foundOldSelect = true;
} else {
codec.selected(false);
}
ijkCodes.add(codec);
}
if (!foundOldSelect && ijkCodes.size() > 0) {
ijkCodes.get(0).selected(true);
}
}
}
直播的结构解析
// https://github.com/takagen99/Box/blob/main/app/src/main/java/com/github/tvbox/osc/api/ApiConfig.java
// ==> 入口参数为主体结构的lives[]字段,当且仅当lives[]字段的第1个元素没有type字段时才调用本函数进行处理
public void loadLives(JsonArray livesArray) {
liveChannelGroupList.clear();
int groupIndex = 0;
int channelIndex = 0;
int channelNum = 0;
for (JsonElement groupElement : livesArray) {
// ==> 继续解析lives[]字段的元素的字段:group、channels[]
LiveChannelGroup liveChannelGroup = new LiveChannelGroup();
liveChannelGroup.setLiveChannels(new ArrayList<LiveChannelItem>());
liveChannelGroup.setGroupIndex(groupIndex++);
String groupName = ((JsonObject) groupElement).get("group").getAsString().trim();
String[] splitGroupName = groupName.split("_", 2);
liveChannelGroup.setGroupName(splitGroupName[0]);
if (splitGroupName.length > 1)
liveChannelGroup.setGroupPassword(splitGroupName[1]);
else
liveChannelGroup.setGroupPassword("");
channelIndex = 0;
for (JsonElement channelElement : ((JsonObject) groupElement).get("channels").getAsJsonArray()) {
// ==> 继续解析lives[]字段的元素的channels[]字段的元素的字段:name、urls[String]
JsonObject obj = (JsonObject) channelElement;
LiveChannelItem liveChannelItem = new LiveChannelItem();
liveChannelItem.setChannelName(obj.get("name").getAsString().trim());
liveChannelItem.setChannelIndex(channelIndex++);
liveChannelItem.setChannelNum(++channelNum);
ArrayList<String> urls = DefaultConfig.safeJsonStringList(obj, "urls");
ArrayList<String> sourceNames = new ArrayList<>();
ArrayList<String> sourceUrls = new ArrayList<>();
int sourceIndex = 1;
for (String url : urls) {
// 处理每个url:按$字符分割字符串,前面的是url,后面的时源名称(源名称不存在则命名为:源1、源2...)。
// 例如:"http://example2.com/video2.mp4$源名称"
String[] splitText = url.split("\\$", 2);
sourceUrls.add(splitText[0]);
if (splitText.length > 1)
sourceNames.add(splitText[1]);
else
sourceNames.add("源" + sourceIndex);
sourceIndex++;
}
liveChannelItem.setChannelSourceNames(sourceNames);
liveChannelItem.setChannelUrls(sourceUrls);
liveChannelGroup.getLiveChannels().add(liveChannelItem);
}
liveChannelGroupList.add(liveChannelGroup);
}
}
ApiConfig其他处理代码
// https://github.com/takagen99/Box/blob/main/app/src/main/java/com/github/tvbox/osc/api/ApiConfig.java
// 查找并处理输入的字符串,根据不同条件进行解密或直接返回原字符串
public static String FindResult(String json, String configKey)
// 获取带'img+'标记的Jar包文件字节内容。从给定的字符串中提取特定格式的子字符串并进行 Base64 解码,若找不到特定格式则返回空字节数组
private static byte[] getImgJar(String body)
// 加载配置信息,支持从指定 URL 拉取源内容数据并进行缓存处理,通过回调通知加载结果。
// 源的URL是从配置中读取的。URL支持临时密钥,即格式:"<URL>[;pk;<TempKey>]"
// 从本地缓存或者远程URL获取源内容并解析的功能实现,是调用 parseJson() 的2个重载方法来完成的。
// 仅在 HomeActivity.java 中调用本函数,入参 activity 即为 HomeActivity 实例
public void loadConfig(boolean useCache, LoadConfigCallback callback, Activity activity)
// 加载 JAR 文件,可以从本地缓存中加载或者从网络上下载并缓存到本地后加载(以'img+'开头则调用getImgJar()处理),通过回调通知加载结果
// 仅在 HomeActivity.java 中调用本函数,入参 spider 为ApiConfig的 "spider" 字段,格式:"<jarUlr>[;md5;<md5>]",可选的md5码用于校验Jar包和从本地缓存加载时定位Jar包。
public void loadJar(boolean useCache, String spider, LoadConfigCallback callback)
// 从本地缓存加载源内容的解析结果。
// 先从本地缓存加载源内容,然后调用重载方法(见上文"源内容的主体结构解析")去解析源内容。
private void parseJson(String apiUrl, File f) throws Throwable
// 将给定的 URL 添加到直播历史记录列表中,并确保列表长度不超过 20
private void putLiveHistory(String url)
// 将给定的 URL 添加到电子节目指南(EPG)历史记录列表中,并确保列表长度不超过 20
public static void putEPGHistory(String url)
// 根据传入的 SourceBean 对象获取一个爬虫实例,如果 api 属性以.js 结尾或包含.js? 则使用 jsLoader 获取,否则使用 jarLoader 获取
public Spider getCSP(SourceBean sourceBean)
核心类分析
- App.java:App类是安卓App的入口类,在应用启动时进行一系列初始化操作,包括初始化字体、设置视图尺寸配置、初始化数据库、加载服务器和数据管理相关模块、设置加载状态回调、初始化播放器辅助类、处理本地化设置、初始化权限检查和网络请求库等,同时提供了获取应用实例、设置和获取特定数据、初始化 P2P 类以及在应用终止时进行清理操作等功能,整体上为应用的运行提供了基础配置和资源管理。
- HomeActivity.java:HomeActivity是一个 Android 活动类,主要作为应用的主界面或首页,负责展示和管理多个视图组件,与数据源交互以获取分类信息,处理用户交互事件,并提供一些实用的功能和导航选项。
- Spider.java:Spider抽象类定义了一系列与爬虫相关的方法,包括初始化、获取首页内容、分类内容、搜索内容、视频详情、播放内容等,还包括视频格式判断、本地代理以及提供安全 DNS 的方法,为实现具体的爬虫功能提供了规范和基础结构。
- JarLoader.java:JarLoader类主要用于加载外部的 JAR 文件以实现自定义爬虫功能。核心功能包括加载主 JAR 文件和外部特定 JAR 文件,通过 DexClassLoader 加载类,并调用特定类的方法进行初始化和获取爬虫实例等操作。
- JsLoader.java:JsLoader类主要用于加载包含自定义 JavaScript API 的 JAR 文件,实现了加载 JAR 文件、缓存管理、通过反射加载特定类以及获取爬虫实例等功能。它可以加载指定 URL 的 JAR 文件并根据其 MD5 值进行缓存校验,加载成功后可以创建基于 JavaScript API 的爬虫实例,并提供了停止所有爬虫操作和通过参数调用代理方法的功能。
- SourceViewModel.java:SourceViewModel类主要用于处理各种数据获取和转换操作,具体是处理ApiConfig解析的单个SourceBean内容(单个站点源),包括获取分类数据、列表数据、搜索结果、详情数据、播放数据等,通过不同的网络请求方式(如OkGo库)或使用特定的爬虫对象(Spider)从不同类型的数据源获取数据,并进行数据格式的转换和处理,同时还包括处理推送链接、迅雷链接解析等特殊情况,以及使用多线程执行耗时任务,并在视图模型销毁时关闭相关线程池。
完整代码参考
参见:https://download.csdn.net/download/zhiyuan411/89648187
合并多个catvod、TVBox源的内容(Python脚本)
参见:Python简记#将多个网址URL或本地路径文件的Json内容进行嵌套合并
可用catvod、TVBox源参考(最新接口)
参见:电视版免费影视App推荐和猫影视catvod、TVBox源(最新接口地址)
更新:解决Spider参数覆盖问题
参见:catvod、TVBox源的解析过程分析和Spider参数覆盖问题解决
标签:obj,String,get,Python,catvod,TVBox,private,URL,url From: https://blog.csdn.net/zhiyuan411/article/details/141289555