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

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.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) 
核心类分析
  1. App.java:App类是安卓App的入口类,在应用启动时进行一系列初始化操作,包括初始化字体、设置视图尺寸配置、初始化数据库、加载服务器和数据管理相关模块、设置加载状态回调、初始化播放器辅助类、处理本地化设置、初始化权限检查和网络请求库等,同时提供了获取应用实例、设置和获取特定数据、初始化 P2P 类以及在应用终止时进行清理操作等功能,整体上为应用的运行提供了基础配置和资源管理。
  2. HomeActivity.java:HomeActivity是一个 Android 活动类,主要作为应用的主界面或首页,负责展示和管理多个视图组件,与数据源交互以获取分类信息,处理用户交互事件,并提供一些实用的功能和导航选项。
  3. Spider.java:Spider抽象类定义了一系列与爬虫相关的方法,包括初始化、获取首页内容、分类内容、搜索内容、视频详情、播放内容等,还包括视频格式判断、本地代理以及提供安全 DNS 的方法,为实现具体的爬虫功能提供了规范和基础结构。
  4. JarLoader.java:JarLoader类主要用于加载外部的 JAR 文件以实现自定义爬虫功能。核心功能包括加载主 JAR 文件和外部特定 JAR 文件,通过 DexClassLoader 加载类,并调用特定类的方法进行初始化和获取爬虫实例等操作。
  5. JsLoader.java:JsLoader类主要用于加载包含自定义 JavaScript API 的 JAR 文件,实现了加载 JAR 文件、缓存管理、通过反射加载特定类以及获取爬虫实例等功能。它可以加载指定 URL 的 JAR 文件并根据其 MD5 值进行缓存校验,加载成功后可以创建基于 JavaScript API 的爬虫实例,并提供了停止所有爬虫操作和通过参数调用代理方法的功能。
  6. 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源(最新接口地址)

  • 8
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

李小白杂货铺

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

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

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

打赏作者

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

抵扣说明:

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

余额充值