catvod、TVBox源的解析过程分析和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.java
            HomeActivity.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.sourceKey
        bundle.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信息,会传递给PlayActivity
            jumpActivity(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() {
        @Override
        public void status(int code, String info) {
            if (code < 0) {
                // 如果状态码小于0,设置提示信息为错误信息,并显示错误提示。
                setTip(info, false, true);
            } else {
                // 如果状态码大于等于0,设置提示信息为信息内容,并隐藏错误提示。
                setTip(info, true, false);
            }
        }

        @Override
        public void list(Map<Integer, String> urlMap) {
            // 这个方法未实现,可能用于处理返回的URL列表。
        }

        @Override
        public 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中获取SourceBean
        SourceBean 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 Spider
        Spider 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.java

public 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();
    }

    // 根据提供的参数获取播放内容
    @Override
    public 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包内有具体的实现类。

爬虫(Web Crawler)是一种自动化程序,用于从互联网上收集信息。其主要功能是访问网页、提取数据并存储,以便后续分析或展示。爬虫通常由搜索引擎、数据挖掘工具、监测系统等应用于网络数据抓取的场景。 爬虫的工作流程包括以下几个关键步骤: URL收集: 爬虫从一个或多个初始URL开始,递归或迭代地发现新的URL,构建一个URL队列。这些URL可以通过链接分析、站点地图、搜索引擎等方式获取。 请求网页: 爬虫使用HTTP或其他协议向目标URL发起请求,获取网页的HTML内容。这通常通过HTTP请求库实现,如Python中的Requests库。 解析内容: 爬虫对获取的HTML进行解析,提取有用的信息。常用的解析工具有正则表达式、XPath、Beautiful Soup等。这些工具帮助爬虫定位提取目标数据,如文本、图片、链接等。 数据存储: 爬虫将提取的数据存储到数据库、文件或其他存储介质中,以备后续分析或展示。常用的存储形式包括关系型数据库、NoSQL数据库、JSON文件等。 遵守规则: 为避免对网站造成过大负担或触发反爬虫机制,爬虫需要遵守网站的robots.txt协议,限制访问频率深度,并模拟人类访问行为,如设置User-Agent。 反爬虫应对: 由于爬虫的存在,一些网站采取了反爬虫措施,如验证码、IP封锁等。爬虫工程师需要设计相应的策略来应对这些挑战。 爬虫在各个领域都有广泛的应用,包括搜索引擎索引、数据挖掘、价格监测、新闻聚合等。然而,使用爬虫需要遵守法律伦理规范,尊重网站的使用政策,并确保对被访问网站的服务器负责。
### TVBOX JSON 格式说明 #### 配置文件位置 对于0531之后打包的UI6-UI7,配置文件位于`public/uploads/tvbox/config/xxxx.json`目录下[^1]。如果该路径不存在所需的配置文件,则可以直接创建一个新的JSON文件来满足需求。 #### 文件命名规则 这里的`xxxx`代表具体的应用ID,意味着可以根据不同的应用程序创建相应的`.json`文件用于存储特定于该应用的设置信息。例如,针对某个具有编号为10000的应用程序,应当建立名为`10000.json`的文件来进行个性化定制。 #### 后台管理系统中的扩展字段利用 除了单独维护外部的JSON文件外,还可以通过进入后台管理界面,在“应用管理”的选项卡下的“编辑”功能区找到“扩展”,在此处直接录入或修改对应的JSON内容。这一步骤的前提条件是确保所使用的平台版本处于最新状态以便兼容此特性。 #### JSON结构概述 基于上述描述以及更广泛的实践案例来看,典型的TVBox JSON文档通常包含以下几个部分: - **jarPath**: 指定加载Java Archive (JAR) 的绝对路径; - **tvSourceUrl**: 提供直播频道列表的数据URL; - **vodSourceUrls[]**: 列举多个按需视频(VOD)服务提供商API端点组成的数组; - **searchEngines[]**: 定义一系列搜索引擎的服务地址集合; - **wallpaperLink**: 设置启动画面或者背景图片链接; 下面给出一段简化版的示例代码片段展示如何构建这样一个有效的JSON对象: ```json { "jarPath": "/path/to/jarfile.jar", "tvSourceUrl": "http://example.com/live_channels.json", "vodSourceUrls": [ {"name":"电影天堂","url":"http://movieapi.example.com"}, {"name":"电视剧大全","url":"http://seriesapi.example.com"} ], "searchEngines":[ {"name":"百度搜索","url":"https://www.baidu.com/s?wd={word}"}, {"name":"谷歌搜索","url":"https://google.com/search?q={word}"} ], "wallpaperLink": "http://image.example.com/wallpapers.jpg" } ``` 以上就是关于TVBox JSON格式的基础介绍及其实际应用场景的例子。值得注意的是,随着技术的发展社区贡献者的努力,具体的属性名称可能会有所变化,因此建议定期查阅官方文档获取最新的指导方针[^2]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

李小白杂货铺

打赏是一种友谊,让我们更亲密。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值