catvod、TVBox源的解析过程分析和Spider参数覆盖问题解决

文章目录

      • TVBox官网
        • 源内容的结构
      • Spider参数覆盖问题
      • Spider参数覆盖问题的解决脚本
      • 附录:核心代码分析
        • 首页
        • 详情页面
        • 播放界面
        • 获取播放信息
        • 源内容配置类
        • Jar包解析类
        • Js解析类
        • Spider

TVBox官网

TVBox项目索引:https://github.com/o0HalfLife0o/TVBoxOSC/

完整代码参考,参见CSDN本地:https://download.csdn.net/download/zhiyuan411/89648187

源内容的结构

参见:catvod、TVBox源的格式解析及合并多个源的内容(Python脚本)

Spider参数覆盖问题

由下文的核心代码分析可知:

  1. 在站点类型为type: 3时,为Spider模式,无论是Jar或Js的子类型,都会依据Jar包来通过反射方式生成解析类。
  2. 在寻找Jar包时,站点的jar参数的优先级会大于源的spider参数。当不存在站点的jar参数时,才会使用源的spider参数进行默认解析。
  3. 对不同源进行合并时,覆盖源的spider参数,对于类型为type: 3且不带jar参数的站点会造成解析错误!
  4. type: 3之外的其他类型场景中,未发现对站点的jar参数的使用和依赖。修改站点的jar参数应该对其他类型常见并无影响。

Spider参数覆盖问题的解决脚本

解决思路:当源内容存在spider参数时,则对所有的不自带jar参数的站点添加jar参数,并将其值设置为spider参数值。

具体脚本参见:Python简记#5. 支持多种常见预处理的嵌套合并Json内容的脚本

附录:核心代码分析

首页
// https://github.com/takagen99/Box/blob/main/app/src/main/java/com/github/tvbox/osc/ui/activity/HomeActivity.java// 订阅刷新事件
@Subscribe(threadMode = ThreadMode.MAIN)
public void refresh(RefreshEvent event) {// 检查事件类型是否为TYPE_PUSH_URL。if (event.type == RefreshEvent.TYPE_PUSH_URL) {// 检查是否存在名为"push_agent"的源。if (ApiConfig.get().getSource("push_agent") != null) {// 创建新的Intent以启动DetailActivity。Intent newIntent = new Intent(mContext, DetailActivity.class);// 将ID添加到Intent的额外数据中。newIntent.putExtra("id", (String) event.obj);// 将sourceKey添加到Intent的额外数据中。newIntent.putExtra("sourceKey", "push_agent");// 设置Intent的标志位。newIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);// 启动DetailActivity。// 注:这只是DetailActivity的一处启动来源,还有多处启动来源,比如:CollectActivity.java、FastSearchActivity.java、HistoryActivity.java、PushActivity.java、SearchActivity.javaHomeActivity.this.startActivity(newIntent);}} else if (event.type == RefreshEvent.TYPE_FILTER_CHANGE) {// 检查当前视图是否非空。if (currentView != null) {// TODO: 实现过滤器图标显示的逻辑。// showFilterIcon((int) event.obj);}}
}
详情页面
// https://github.com/takagen99/Box/blob/main/app/src/main/java/com/github/tvbox/osc/ui/activity/DetailActivity.java// 跳转到播放界面
private void jumpToPlay() {// 检查视频信息是否有效并且至少有一个可播放的剧集。if (vodInfo != null && vodInfo.seriesMap.get(vodInfo.playFlag).size() > 0) {// 保存当前播放标志供将来参考。preFlag = vodInfo.playFlag;// 创建一个Bundle对象用于传递数据。Bundle bundle = new Bundle();// 保存历史记录。insertVod(firstsourceKey, vodInfo);// 将sourceKey添加到Bundle中。// sourceKey是成员变量。// 它是初始化时从bundle获取的,如果没有则默认置为空字符串。// 初始化之后使用mVideo.sourceKey 进行了设置。// mVideo = absXml.movie.videoList.get(0)// absXml是AbsXml类型,是刷新变化时解析好后传下来的实例
// @XStreamAlias("rss")
// public class AbsXml implements Serializable {
//     @XStreamAlias("list")
//     public Movie movie;//     @XStreamAlias("msg")
//     public String msg;
// }// @XStreamAlias("list")
// public class Movie implements Serializable {
//     @XStreamAsAttribute
//     public int page;
//     @XStreamAsAttribute
//     public int pagecount;//总页数
//     @XStreamAsAttribute
//     public int pagesize;
//     @XStreamAsAttribute
//     public int recordcount;//总条数
//     @XStreamImplicit(itemFieldName = "video")
//     public List<Video> videoList;//     @XStreamAlias("video")
//     public static class Video implements Serializable {
//         @XStreamAlias("last")//时间
//         public String last;
//         @XStreamAlias("id")//内容id
//         public String id;
//         @XStreamAlias("tid")//父级id
//         public int tid;
//         @XStreamAlias("name")//影片名称 <![CDATA[老爸当家]]>
//         public String name;
//         @XStreamAlias("type")//类型名称
//         public String type;
//         /*@XStreamAlias("dt")//视频分类 zuidam3u8,zuidall
//         public String dt;*/
//         @XStreamAlias("pic")//图片
//         public String pic;
//         @XStreamAlias("lang")//语言
//         public String lang;
//         @XStreamAlias("area")//地区
//         public String area;
//         @XStreamAlias("year")//年份
//         public int year;
//         @XStreamAlias("state")
//         public String state;
//         @XStreamAlias("note")//描述集数或者影片信息<![CDATA[共40集]]>
//         public String note;
//         @XStreamAlias("actor")//演员<![CDATA[张国立,蒋欣,高鑫,曹艳艳,王维维,韩丹彤,孟秀,王新]]>
//         public String actor;
//         @XStreamAlias("director")//导演<![CDATA[陈国星]]>
//         public String director;
//         @XStreamAlias("dl")
//         public UrlBean urlBean;
//         @XStreamAlias("des")
//         public String des;// <![CDATA[权来]
//         public String sourceKey;
//         @XStreamAlias("tag")
//         public String tag;bundle.putString("sourceKey", sourceKey);// 将视频信息序列化并添加到Bundle中。// vodInfo是VodInfo类型,成员变量。// 它是初始化时新建的对象实例,填充了:setVideo(mVideo)、sourceKey = mVideo.sourceKeybundle.putSerializable("VodInfo", vodInfo);// 如果需要预览模式,则进行预览准备。if (showPreview) {// 如果预览信息尚未创建,则创建一个预览信息的副本。if (previewVodInfo == null) {try {// 创建字节输出流和对象输出流。ByteArrayOutputStream bos = new ByteArrayOutputStream();ObjectOutputStream oos = new ObjectOutputStream(bos);// 将vodInfo对象写入字节流。oos.writeObject(vodInfo);oos.flush();oos.close();// 创建字节输入流和对象输入流。ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));// 从字节流中读取并反序列化得到vodInfo对象。previewVodInfo = (VodInfo) ois.readObject();} catch (Exception e) {e.printStackTrace();}}// 如果预览信息已经创建,则复制播放配置和播放信息。if (previewVodInfo != null) {previewVodInfo.playerCfg = vodInfo.playerCfg;previewVodInfo.playFlag = vodInfo.playFlag;previewVodInfo.playIndex = vodInfo.playIndex;previewVodInfo.playGroup = vodInfo.playGroup;previewVodInfo.reverseSort = vodInfo.reverseSort;previewVodInfo.playGroupCount = vodInfo.playGroupCount;previewVodInfo.seriesMap = vodInfo.seriesMap;// 将预览信息序列化并添加到Bundle中。bundle.putSerializable("VodInfo", previewVodInfo);}// 设置预览Fragment的数据。playFragment.setData(bundle);} else {// 如果不需要预览模式,则跳转到播放活动。// 在 bundle 中初始化了sourceKey、VodInfo信息,会传递给PlayActivityjumpActivity(PlayActivity.class, bundle);}}
}
播放界面
// https://github.com/takagen99/Box/blob/main/app/src/main/java/com/github/tvbox/osc/ui/activity/PlayActivity.java// 启动视频播放过程,并包含了处理不同URL格式、头部信息以及迅雷链接回调等功能
public void play(boolean reset) {// 从系列映射中获取当前正在播放的剧集,使用播放标志和播放索引。VodInfo.VodSeries vs = mVodInfo.seriesMap.get(mVodInfo.playFlag).get(mVodInfo.getplayIndex());// 发布一个事件以刷新UI并更新当前播放索引。EventBus.getDefault().post(new RefreshEvent(RefreshEvent.TYPE_REFRESH, mVodInfo.getplayIndex()));// 发布另一个事件来通知刷新,并附带视频名称和剧集名称。EventBus.getDefault().post(new RefreshEvent(RefreshEvent.TYPE_REFRESH_NOTIFY, mVodInfo.name + "&&" + vs.name));// 创建一个包含视频名称和剧集名称的字符串。String playTitleInfo = mVodInfo.name + " : " + vs.name;// 设置提示信息,指示正在获取播放信息。setTip("正在获取播放信息", true, false);// 设置控制器的标题为播放标题信息。mController.setTitle(playTitleInfo);// 停止解析器。stopParse();// 初始化解析加载器。initParseLoadFound();// 如果视频视图不为空,则释放它。if (mVideoView != null) mVideoView.release();// 创建字幕缓存键。subtitleCacheKey = mVodInfo.sourceKey + "-" + mVodInfo.id + "-" + mVodInfo.playFlag + "-" + mVodInfo.getplayIndex() + "-" + vs.name + "-subt";// 创建进度键。progressKey = mVodInfo.sourceKey + mVodInfo.id + mVodInfo.playFlag + mVodInfo.getplayIndex();// 如果是重播,则清除现有的进度。if (reset) {CacheManager.delete(MD5.string2MD5(progressKey), 0);CacheManager.delete(MD5.string2MD5(subtitleCacheKey), "");}// 处理 tvbox-drive:// 开头的 URL。if (vs.url.startsWith("tvbox-drive://")) {progressKey = vs.url.replace("tvbox-drive://", "");// takagen99: 快速修复在 tvbox-drive 播放中的媒体选项问题initPlayerDrive();mController.showParse(false);// 如果存在播放配置,则根据配置创建头部信息。HashMap<String, String> headers = null;if (mVodInfo.playerCfg != null && mVodInfo.playerCfg.length() > 0) {JsonObject playerConfig = JsonParser.parseString(mVodInfo.playerCfg).getAsJsonObject();if (playerConfig.has("headers")) {headers = new HashMap<>();for (JsonElement headerEl : playerConfig.getAsJsonArray("headers")) {JsonObject headerJson = headerEl.getAsJsonObject();headers.put(headerJson.get("name").getAsString(), headerJson.get("value").getAsString());}}}// 播放 tvbox-drive:// URL 替换后的地址。playUrl(vs.url.replace("tvbox-drive://", ""), headers);return;}// 处理 tvbox-xg: 开头的 URL。if (vs.url.startsWith("tvbox-xg:") && !TextUtils.isEmpty(vs.url.substring(9))) {this.mController.showParse(false);// 解码 tvbox-xg: 后面的内容并播放。playUrl(Jianpian.JPUrlDec(vs.url.substring(9)), null);return;}// 处理迅雷链接。if (Thunder.play(vs.url, new Thunder.ThunderCallback() {@Overridepublic void status(int code, String info) {if (code < 0) {// 如果状态码小于0,设置提示信息为错误信息,并显示错误提示。setTip(info, false, true);} else {// 如果状态码大于等于0,设置提示信息为信息内容,并隐藏错误提示。setTip(info, true, false);}}@Overridepublic void list(Map<Integer, String> urlMap) {// 这个方法未实现,可能用于处理返回的URL列表。}@Overridepublic void play(String url) {// 当迅雷链接成功解析后,播放URL。playUrl(url, null);}})) {mController.showParse(false);return;}// 获取播放信息,使用提供的源键、进度键、播放URL 和字幕缓存键。// sourceKey是成员变量,其值是初始化时从详情页面传递过来的// mVodInfo是成员变量,VodInfo类型,其值是初始化时从详情页面传递过来的// progressKey在本函数内初始化// vs在本函数内初始化// subtitleCacheKey在本函数内初始化
// public class VodInfo implements Serializable {
//     public String last;//时间
//     //内容id
//     public String id;
//     //父级id
//     public int tid;
//     //影片名称 <![CDATA[老爸当家]]>
//     public String name;
//     //类型名称
//     public String type;
//     //视频分类zuidam3u8,zuidall
//     public String dt;
//     //图片
//     public String pic;
//     //语言
//     public String lang;
//     //地区
//     public String area;
//     //年份
//     public int year;
//     public String state;
//     //描述集数或者影片信息<![CDATA[共40集]]>
//     public String note;
//     //演员<![CDATA[张国立,蒋欣,高鑫,曹艳艳,王维维,韩丹彤,孟秀,王新]]>
//     public String actor;
//     //导演<![CDATA[陈国星]]>
//     public String director;
//     public ArrayList<VodSeriesFlag> seriesFlags;
//     public LinkedHashMap<String, List<VodSeries>> seriesMap;
//     public String des;// <![CDATA[权来]
//     public String playFlag = null;
//     public int playIndex = 0;
//     public int playGroup = 0;
//     public int playGroupCount = 0;
//     public String playNote = "";
//     public String sourceKey;
//     public String playerCfg = "";
//     public boolean reverseSort = false;// public static class VodSeries implements Serializable {//     public String name;
//     public String url;
//     public boolean selected;sourceViewModel.getPlay(sourceKey, mVodInfo.playFlag, progressKey, vs.url, subtitleCacheKey);
}
获取播放信息
// https://github.com/takagen99/Box/blob/main/app/src/main/java/com/github/tvbox/osc/viewmodel/SourceViewModel.java// 获取指定视频源的播放信息
public void getPlay(String sourceKey, String playFlag, String progressKey, String url, String subtitleKey) {// 如果已有线程池,立即关闭它。if (threadPoolGetPlay != null) threadPoolGetPlay.shutdownNow();// 创建一个新的固定大小的线程池,大小为2。threadPoolGetPlay = Executors.newFixedThreadPool(2);// 创建一个Callable任务,用于异步获取播放信息。Callable<JSONObject> callable = () -> {// 如果当前线程被中断,则返回null。if (Thread.currentThread().isInterrupted()) return null;// 获取指定sourceKey的视频源信息。根据sourceKey来从ApiConfig的sourceBeanList中获取SourceBeanSourceBean sourceBean = ApiConfig.get().getSource(sourceKey);// 获取视频源的类型。int type = sourceBean.getType();// 结果JSON对象,用于封装播放信息。JSONObject result = null;// 根据视频源的类型进行不同的处理。if (type == 3) { // Spider// 获取爬虫实例。Spider sp = ApiConfig.get().getCSP(sourceBean);// 使用爬虫获取播放内容。String json = sp.playerContent(playFlag, url, ApiConfig.get().getVipParseFlags());// 将结果转换为JSON对象。result = new JSONObject(json);} else if (type == 0 || type == 1) { // 0 xml 1 json// 创建一个空的JSON对象。result = new JSONObject();// 获取源内容里的站点的播放URL。String playUrl = sourceBean.getPlayerUrl().trim();// url是入参,表示视频的实际url。// isVideoFormat()函数是判断url是否匹配视频格式的URL(比如结尾后缀等)// 判断是否需要解析。仅当实际url是视频格式URL,且播放URL为空时,才不需要解析(使用实际url)boolean parse = DefaultConfig.isVideoFormat(url) && playUrl.isEmpty();// 设置是否需要解析的标志。result.put("parse", BooleanUtils.toInteger(!parse));// 设置播放URL。result.put("url", url);} else if (type == 4) {// 对站点的api网址发起HTTP GET请求获取播放信息。okhttp3.Response response = OkGo.<String>get(sourceBean.getApi()).params("play", url).params("flag", playFlag).tag("play").execute();// 读取响应体并转换为JSON对象。String json = response.body().string();result = new JSONObject(json);}// 如果结果不为空,则添加额外的键值对。if (result != null) {result.put("key", url);               // 播放URL键值result.put("proKey", progressKey);    // 进度键值result.put("subtKey", subtitleKey);   // 字幕键值if (!result.has("flag"))              // 如果结果中没有播放标志,则添加播放标志。result.put("flag", playFlag);}// 返回结果JSON对象。return result;};// 提交Callable任务到线程池执行。threadPoolGetPlay.execute(() -> {// 提交Callable任务并获取Future对象。Future<JSONObject> future = threadPoolGetPlay.submit(callable);try {// 等待最多15秒获取结果。JSONObject jsonObject = future.get(15, TimeUnit.SECONDS);// 将结果发布给LiveData观察者。playResult.postValue(jsonObject);} catch (Throwable e) {// 如果发生异常,打印堆栈跟踪,并发布null结果。e.printStackTrace();playResult.postValue(null);}});
}
源内容配置类
// https://github.com/takagen99/Box/blob/main/app/src/main/java/com/github/tvbox/osc/api/ApiConfig.java// 获取与给定的SourceBean相关的爬虫(Spider)实例
public Spider getCSP(SourceBean sourceBean) {// 检查视频源的API是否以".js"结尾或包含".js?"。if (sourceBean.getApi().endsWith(".js") || sourceBean.getApi().contains(".js?")) {// 如果API与JavaScript相关,则从jsLoader中获取爬虫实例。return jsLoader.getSpider(sourceBean.getKey(), sourceBean.getApi(), sourceBean.getExt(), sourceBean.getJar());}// 如果API不是JavaScript相关,则从jarLoader中获取爬虫实例。return jarLoader.getSpider(sourceBean.getKey(), sourceBean.getApi(), sourceBean.getExt(), sourceBean.getJar());
}
Jar包解析类
// https://github.com/takagen99/Box/blob/main/app/src/main/java/com/github/catvod/crawler/JarLoader.java// 根据给定的参数获取或创建一个Spider实例
public Spider getSpider(String key, String cls, String ext, String jar) {// 移除爬虫类名前缀"csp_"。String clsKey = cls.replace("csp_", "");// 初始化jar包相关信息。String jarUrl = "";String jarMd5 = "";String jarKey = "";// 如果未提供jar包路径,则使用默认的主jar包。if (jar.isEmpty()) {jarKey = "main";} else {// 分割jar包路径和MD5校验码。String[] urls = jar.split(";md5;");jarUrl = urls[0];// 生成jar包路径的MD5作为唯一标识。jarKey = MD5.string2MD5(jarUrl);// 获取MD5校验码。jarMd5 = urls.length > 1 ? urls[1].trim() : "";}// 更新最近使用的jar包标识。recentJarKey = jarKey;// 如果已经存在与key关联的Spider实例,则直接返回。if (spiders.containsKey(key))return spiders.get(key);// 加载类加载器。DexClassLoader classLoader = null;// 如果使用的是主jar包,则从缓存中获取类加载器。if (jarKey.equals("main"))classLoader = classLoaders.get("main");else {// 如果使用的是其他jar包,则加载新的类加载器。// loadJarInternal是成员函数,它会根据提供的jar文件URL下载并加载jar文件到DexClassLoader中// ,如果缓存中已有对应的加载器则直接返回// ,否则下载文件、保存到缓存并加载// ,最终返回对应的DexClassLoader实例。classLoader = loadJarInternal(jarUrl, jarMd5, jarKey);}// 如果类加载器为空,则返回一个空的Spider实例。if (classLoader == null)return new SpiderNull();try {// 对Jar包内的clsKey类使用反射机制创建爬虫类的实例。Spider sp = (Spider) classLoader.loadClass("com.github.catvod.spider." + clsKey).newInstance();// 调用clsKey类的init()函数来初始化爬虫。sp.init(App.getInstance(), ext);// 如果提供了jar包路径,则调用homeContent方法。if (!jar.isEmpty()) {sp.homeContent(false); // 增加此行 应该可以解决部分写的有问题源的历史记录问题 但会增加这个源的首次加载时间 不需要可以已删掉}// 将爬虫实例缓存起来。spiders.put(key, sp);// 返回爬虫实例。return sp;} catch (Throwable th) {// 如果发生异常,则打印堆栈跟踪。th.printStackTrace();}// 如果出现异常或其他问题,则返回一个空的Spider实例。return new SpiderNull();
}
Js解析类
// https://github.com/takagen99/Box/blob/main/app/src/main/java/com/github/catvod/crawler/JsLoader.java// 根据给定的参数获取或创建一个Spider实例
public Spider getSpider(String key, String api, String ext, String jar) {// 如果提供了jar包路径,则加载类加载器。Class<?> classLoader = null;if (!jar.isEmpty()) {// 分割jar包路径和MD5校验码。String[] urls = jar.split(";md5;");String jarUrl = urls[0];// 生成jar包路径的MD5作为唯一标识。String jarKey = MD5.string2MD5(jarUrl);// 获取MD5校验码。String jarMd5 = urls.length > 1 ? urls[1].trim() : "";// 优先使用缓存中已有对应的加载器// 否则,根据提供的jar文件URL下载、缓存并加载jar文件到DexClassLoader中classLoader = loadJarInternal(jarUrl, jarMd5, jarKey);}// 更新最近使用的jar包标识。recentJarKey = key;// 如果已经存在与key关联的Spider实例,则直接返回。if (spiders.containsKey(key))return spiders.get(key);try {// 创建爬虫类的实例。// public class JsSpider extends SpiderSpider sp = new JsSpider(key, api, classLoader);// 初始化爬虫。sp.init(App.getInstance(), ext);// 将爬虫实例缓存起来。spiders.put(key, sp);// 返回爬虫实例。return sp;} catch (Throwable th) {// 如果发生异常,则打印堆栈跟踪。th.printStackTrace();// 记录错误日志。LOG.e("QuJS", th);}// 如果出现异常或其他问题,则返回一个空的Spider实例。return new SpiderNull();
}
Spider
// https://github.com/takagen99/Box/blob/main/app/src/main/java/com/github/tvbox/osc/util/js/JsSpider.javapublic class JsSpider extends Spider {...// 提交一个Runnable任务到线程池中执行private void submit(Runnable runnable) {executor.submit(runnable);}// 提交一个Callable任务到线程池中执行,并返回Future对象private <T> Future<T> submit(Callable<T> callable) {return executor.submit(callable);}// 使用CompletableFuture来异步调用JavaScript对象上的方法,并返回结果private Object call(String func, Object... args) throws Exception {//return executor.submit((FunCall.call(jsObject, func, args))).get();return CompletableFuture.supplyAsync(() -> Async.run(jsObject, func, args), executor).join().get();} // 构造函数public JsSpider(String key, String api, Class<?> cls) throws Exception {this.key = "J" + MD5.encode(key);this.executor = Executors.newSingleThreadExecutor();this.api = api;this.dex = cls;initializeJS();}// 初始化JavaScript环境private void initializeJS() throws Exception {// 提交任务到线程池执行。submit(() -> {// 如果JavaScript上下文不存在,则创建。if (ctx == null) createCtx();// 如果DexClassLoader不存在,则创建。if (dex != null) createDex();// 加载API地址的内容。String content = FileUtils.loadModule(api);// 如果内容为空,则直接返回。if (TextUtils.isEmpty(content)) {return null;}// 处理以 "//bb" 开头的内容。if (content.startsWith("//bb")) {// 设置标志位。cat = true;// 解码Base64编码的内容。byte[] b = Base64.decode(content.replace("//bb", ""), 0);// 在JavaScript上下文中执行解码后的字节流。ctx.execute(byteFF(b), key + ".js");// 评估并执行模块代码,将全局上下文中的变量绑定到指定名称。ctx.evaluateModule(String.format(SPIDER_STRING_CODE, key + ".js") + "globalThis." + key + " = __JS_SPIDER__;", "tv_box_root.js");// 注释掉的代码,用于备选处理方式。//ctx.execute(byteFF(b), key + ".js", "__jsEvalReturn");//ctx.evaluate("globalThis." + key + " = __JS_SPIDER__;");} else {// 替换 "__JS_SPIDER__ =" 为 "export default"。if (content.contains("__JS_SPIDER__")) {content = content.replaceAll("__JS_SPIDER__\\s*=", "export default ");}// 初始化模块扩展名。String moduleExtName = "default";// 如果内容包含 "__jsEvalReturn" 并且没有 "export default",则设置模块扩展名并标记。if (content.contains("__jsEvalReturn") && !content.contains("export default")) {moduleExtName = "__jsEvalReturn";cat = true;}// 评估并执行模块代码。ctx.evaluateModule(content, api);// 评估并执行模块代码,将全局上下文中的变量绑定到指定名称。ctx.evaluateModule(String.format(SPIDER_STRING_CODE, api) + "globalThis." + key + " = __JS_SPIDER__;", "tv_box_root.js");// 注释掉的代码,用于备选处理方式。//ctx.evaluateModule(content, api, moduleExtName);//ctx.evaluate("globalThis." + key + " = __JS_SPIDER__;");}// 获取JavaScript对象。jsObject = (JSObject) ctx.get(ctx.getGlobalObject(), key);// 返回 null。return null;}).get();}// 根据提供的参数获取播放内容@Overridepublic String playerContent(String flag, String id, List<String> vipFlags) throws Exception {// 将VIP标志列表转换为JSArray。JSArray array = submit(() -> new JSUtils<String>().toArray(ctx, vipFlags)).get();// 调用JavaScript中的 "play" 方法,并传递参数。// 返回方法调用的结果,即播放内容的字符串表示形式。return (String) call("play", flag, id, array);}...}

注:Jar包类型Spider在在Jar包内有具体的实现类。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/52331.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

LibreWolf使用记录

之前一直使用Firefox作为个人主力浏览器&#xff0c;工作上由于有些网页兼容问题&#xff0c;一直使用的是Chrome。自从看了Eric Murphy 关于浏览器的分类之后, 出于对隐私的考虑&#xff0c;还是决定把chrome换成Brave&#xff0c;同时考虑对Firefox进行harden或者使用LibreWo…

从力扣中等+困难题+表白HTML测试 -- 文心快码(Baidu Comate)

0 写在前面 &#xff08;通过如下链接/二维码进入官网注册&#xff0c;并在IDE使用三次及以上可以找我领计算机基础/ML/DL 面经/知识点一份~&#xff09; 官网地址&#xff1a;Baidu Comate Step1 打开文心快码&#xff08;Baidu Comate&#xff09;官网&#xff0c;点击「免…

[Python可视化]空气污染物浓度地图可视化

[Python可视化]空气污染物浓度地图可视化&#xff0c;果然是路边浓度最大 在本篇文章中&#xff0c;我将展示如何使用 Python 结合 OSMnx、NetworkX 和 GeoPandas 等库&#xff0c;计算给定路径的最短路线&#xff0c;并基于该路径穿过的网格单元计算总污染量。最终&#xff0c…

【ORACLE】listagg() 函数

在Oracle数据库中&#xff0c;LISTAGG 函数是一个非常有用的聚合函数&#xff0c;它能够将多个行的字符串值连接成一个单独的字符串。这个函数在处理需要将多行数据合并为一行数据的场景中特别有用&#xff0c;比如生成报表或者构建复杂的字符串输出。 基本语法 LISTAGG 函数…

k8s - Secret实践练习

参考文档&#xff1a;https://kubernetes.io/zh-cn/docs/concepts/configuration/secret/ 这个和ConfigMap很相似&#xff0c;这里选两个做下测试&#xff0c;就不过多赘述了 简介 Secret 类似于 ConfigMap 但专门用于保存机密数据。 Secret 是一种包含少量敏感信息例如密码…

<数据集>鸟类识别数据集<目标检测>

数据集格式&#xff1a;VOCYOLO格式 图片数量&#xff1a;16287张 标注数量(xml文件个数)&#xff1a;16287 标注数量(txt文件个数)&#xff1a;16287 标注类别数&#xff1a;10 标注类别名称&#xff1a;[Chestnut Munia, Zebra Dove, Garden Sunbird, Collared Kingfish…

一篇文章入门Java虚拟机(JVM)

JVM全称是Java Virtual Machine&#xff0c;中文译名Java虚拟机。本质上是一个运行在计算机上的程序 一&#xff0c;JVM的功能 功能描述解释和运行对字节码文件中的指令&#xff0c;实时的解释成机器码&#xff0c;让计算机执行内存管理自动为对象、方法等分配内存&#xff1…

ChatGPT 3.5/4.0 新手使用手册

ChatGPT 3.5/4.0 新手使用手册 一、引言 ChatGPT 是由 OpenAI 开发的一种基于人工智能技术的聊天机器人&#xff0c;它能够自动生成自然语言的响应&#xff0c;与用户进行高质量的对话。ChatGPT 3.5 和 4.0 是该系列的最新版本&#xff0c;它们在自然语言处理、对话能力和知识…

海山数据库(He3DB)源码详解:CommitTransaction函数源码详解

文章目录 海山数据库(He3DB)源码详解&#xff1a;CommitTransaction函数1. 执行条件2. 执行过程2.1 获取当前节点状态&#xff1a;2.2 检查当前状态&#xff1a;2.3 预提交处理&#xff1a;2.4 提交处理&#xff1a;2.5 释放资源&#xff1a;2.6 提交事务&#xff1a; 作者介绍…

数据结构-线性表-了解循环链表

了解循环链表 循环链表是一种特殊的链表结构&#xff0c;其中最后一个节点的指针指向头节点&#xff0c;从而形成一个闭环。循环链表可以分为两种类型&#xff1a;单循环链表和双循环链表。 1. 单循环链表&#xff08;Singly Circular Linked List&#xff09; 定义&#xf…

Qt第十八章 XML和Json格式解析

文章目录 JSON格式解析Json生成案例 XML简介与HTML的区别格式XML解析流的方式DOM XML生成 JSON与XML的区别比较 JSON 格式 JSON是一个标记符的序列。这套标记符包含六个构造字符、字符串、数字和三个字面名 六个构造字符 开始和结束数组&#xff1a;[ ]开始和结束对象&#x…

基于HarmonyOS的宠物收养系统的设计与实现(一)

基于HarmonyOS的宠物收养系统的设计与实现&#xff08;一&#xff09; 本系统是简易的宠物收养系统&#xff0c;为了更加熟练地掌握HarmonyOS相关技术的使用。 项目创建 创建一个空项目取名为PetApp 首页实现&#xff08;组件导航使用&#xff09; 官方文档&#xff1a;组…

安卓中设置渐变字体和描边字体

1.CommonFontSpan abstract class CommonFontSpan : ReplacementSpan() {/** 测量的文本宽度 */private var mMeasureTextWidth 0foverride fun getSize(paint: Paint,text: CharSequence?,start: Int,end: Int,fontMetricsInt: FontMetricsInt?): Int {mMeasureTextWidth…

机器学习辅助复合材料预测,性能管理优化创新材料,这种王炸般的组合,还真是大开眼界!

在人工智能与复合材料技术融合的背景下&#xff0c;复合材料的研究和应用正迅速发展&#xff0c;创新解决方案层出不穷。从复合材料性能的精确预测到复杂材料结构的智能设计&#xff0c;从数据驱动的材料结构优化到多尺度分析&#xff0c;人工智能技术正以其强大的数据处理能力…

Eureka 原理与实践全攻略

一、Eureka 概述 Eureka 在微服务架构中具有举足轻重的地位。它作为服务注册与发现的核心组件&#xff0c;为分布式系统中的服务管理提供了关键支持。 Eureka 的主要功能包括服务注册、服务发现、服务健康监测和自我保护机制。服务注册功能使得服务提供者能够在启动时将自身的…

git-版本管理工具基本操作-创建仓库-拉取-推送-暂存库-版本库

1、创建仓库和版本说明 2、克隆仓库到本地&#xff08;首次拉取需要输入用户名和密码&#xff0c;用户名用邮箱&#xff0c;密码用登录gitee的密码&#xff0c;后面配置密钥后可以直接clone&#xff09; 在命令行输出两行指令配置git才能克隆&#xff1a; username&#xff1…

JUnit 断言验证

assertEquals(expected, actual)&#xff1a; 检查 actual 是否等于 expected。示例&#xff1a; assertEquals(5, myMethod(2, 3)); // 检查 2 3 是否等于 5 assertTrue(condition)&#xff1a; 检查 condition 是否为 true。示例&#xff1a; assertTrue(myMethod(1) > …

求数组中出现次数超过一半的数字

一、题目 数组中有一个数字出现的次数超过数组长度的一半&#xff0c;请找出这个数字。 假设数组非空&#xff0c;并且一定存在满足条件的数字。 思考题&#xff1a; 假设要求只能使用 O(n)的时间和额外 O(1)的空间&#xff0c;该怎么做呢&#xff1f; 数据范围 数组长度…

《Web项目跨域请求后端Api设置Cookie失败问题?》

问题描述&#xff1a; 在web项目中跨域请求api时&#xff0c;api登录成功后需要向域名中设置cookie实现在两个域名下共享&#xff0c;但是登录接口返回成功&#xff0c;响应头中也有set-cookie&#xff0c;实际却无法设置到cookie中… web项目访问时的域名https://b.com/ api所…

解锁Spring Boot、Prometheus与Grafana三合一:打造你的专属自定义指标炫酷大屏!

1. 集成Prometheus到Spring Boot 需要在Spring Boot应用中集成Prometheus。可以通过micrometer库来实现。micrometer是一个应用程序监控库&#xff0c;它支持多种监控系统&#xff0c;包括Prometheus。 一招制胜&#xff01;Spring Boot、Prometheus和Grafana三剑合璧&#x…