文章目录
- TVBox官网
- 核心代码分析
- 源内容的结构定义
- 源内容的主体结构解析
- 直播的结构解析
- ApiConfig其他处理代码
- 核心类分析
- 完整代码参考
- 合并多个catvod、TVBox源的内容(Python脚本)
- 可用catvod、TVBox源参考(最新接口)
TVBox官网
TVBox项目索引:https://github.com/o0HalfLife0o/TVBoxOSC/
核心代码分析
源内容的结构定义
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.javaprivate 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;#idSourceBean 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);elsesetSourceBean(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 ConfigliveChannelGroupList.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);//clanString 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 ConfigSystem.out.println("Live URL :" + extUrlFix);putLiveHistory(extUrlFix);// Overwrite with Live URL from Settingsif (StringUtils.isBlank(liveURL)) {Hawk.put(HawkConfig.LIVE_URL, extUrlFix);} else {extUrlFix = liveURL;}// Final Live URLliveURL_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 Settingsif (livesOBJ.has("epg")) {String epg = livesOBJ.get("epg").getAsString();System.out.println("EPG URL :" + epg);putEPGHistory(epg);// Overwrite with EPG URL from Settingsif (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 existsif (!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 Settingsif (fengMiLives.has("epg")) {String epg = fengMiLives.get("epg").getAsString();System.out.println("EPG URL :" + epg);putEPGHistory(epg);// Overwrite with EPG URL from Settingsif (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 SettingsSystem.out.println("Live URL :" + url);putLiveHistory(url);// Overwrite with Live URL from Settingsif (StringUtils.isBlank(liveURL)) {Hawk.put(HawkConfig.LIVE_URL, url);} else {url = liveURL;}// Final Live URLliveURL_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 hostif (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、valueJsonObject 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]);elseliveChannelGroup.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]);elsesourceNames.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源(最新接口地址)