diff --git a/app/build.gradle b/app/build.gradle index 76be781d..c8eca697 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -68,6 +68,7 @@ dependencies { implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' implementation 'androidx.recyclerview:recyclerview:1.1.0' + implementation files('libs\\thunder.jar') annotationProcessor 'androidx.room:room-compiler:2.3.0' implementation 'androidx.room:room-runtime:2.3.0' diff --git a/app/libs/thunder.jar b/app/libs/thunder.jar new file mode 100644 index 00000000..d5c06f79 Binary files /dev/null and b/app/libs/thunder.jar differ diff --git a/app/src/main/java/com/github/tvbox/osc/ui/activity/PlayActivity.java b/app/src/main/java/com/github/tvbox/osc/ui/activity/PlayActivity.java index 85902633..2040af3d 100644 --- a/app/src/main/java/com/github/tvbox/osc/ui/activity/PlayActivity.java +++ b/app/src/main/java/com/github/tvbox/osc/ui/activity/PlayActivity.java @@ -53,6 +53,7 @@ import com.github.tvbox.osc.util.LOG; import com.github.tvbox.osc.util.MD5; import com.github.tvbox.osc.util.PlayerHelper; import com.github.tvbox.osc.util.XWalkUtils; +import com.github.tvbox.osc.util.thunder.Thunder; import com.github.tvbox.osc.viewmodel.SourceViewModel; import com.lzy.okgo.OkGo; import com.lzy.okgo.callback.AbsCallback; @@ -462,6 +463,28 @@ public class PlayActivity extends BaseActivity { playUrl(null, null); String progressKey = mVodInfo.sourceKey + mVodInfo.id + mVodInfo.playFlag + mVodInfo.playIndex; + if (Thunder.play(vs.url, new Thunder.ThunderCallback() { + @Override + public void status(int code, String info) { + if (code < 0) { + setTip(info, false, true); + } else { + setTip(info, true, false); + } + } + + @Override + public void list(String playList) { + } + + @Override + public void play(String url) { + playUrl(url, null); + } + })) { + mController.showParse(false); + return; + } sourceViewModel.getPlay(sourceKey, mVodInfo.playFlag, progressKey, vs.url); } diff --git a/app/src/main/java/com/github/tvbox/osc/util/thunder/Thunder.java b/app/src/main/java/com/github/tvbox/osc/util/thunder/Thunder.java new file mode 100644 index 00000000..be0b8829 --- /dev/null +++ b/app/src/main/java/com/github/tvbox/osc/util/thunder/Thunder.java @@ -0,0 +1,333 @@ +package com.github.tvbox.osc.util.thunder; + +import android.content.Context; +import android.content.SharedPreferences; +import android.net.Uri; +import android.text.TextUtils; + +import com.xunlei.downloadlib.XLDownloadManager; +import com.xunlei.downloadlib.XLTaskHelper; +import com.xunlei.downloadlib.android.XLUtil; +import com.xunlei.downloadlib.parameter.TorrentFileInfo; +import com.xunlei.downloadlib.parameter.TorrentInfo; +import com.xunlei.downloadlib.parameter.XLTaskInfo; + +import java.io.File; +import java.util.ArrayList; +import java.util.Random; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class Thunder { + + private static String cacheRoot = ""; + private static long currentTask = 0L; + private static ArrayList torrentFileInfoArrayList = null; + private static ExecutorService threadPool = null; + + + private static void init(Context context) { + // fake deviceId and Mac + SharedPreferences sharedPreferences = context.getSharedPreferences("rand_thunder_id", Context.MODE_PRIVATE); + String imei = sharedPreferences.getString("imei", null); + String mac = sharedPreferences.getString("mac", null); + if (imei == null) { + imei = randomImei(); + sharedPreferences.edit().putString("imei", imei).commit(); + } + if (mac == null) { + mac = randomMac(); + sharedPreferences.edit().putString("mac", mac).commit(); + } + + XLUtil.mIMEI = imei; + XLUtil.isGetIMEI = true; + XLUtil.mMAC = mac; + XLUtil.isGetMAC = true; + String cd3 = "cee25055f125a2fde0"; + String base64Decode = "axzNjAwMQ^^yb==0^852^083dbcff^"; + String substring = base64Decode.substring(1); + String substring2 = cd3.substring(0, cd3.length() - 1); + String cd = substring + substring2; + XLTaskHelper.init(context, cd, "21.01.07.800002"); + cacheRoot = context.getCacheDir().getAbsolutePath() + File.separator + "thunder"; + } + + public static void stop() { + if (currentTask > 0) { + XLTaskHelper.instance().stopTask(currentTask); + currentTask = 0L; + } + torrentFileInfoArrayList = null; + // del cache file + File cache = new File(cacheRoot); + recursiveDelete(cache); + if (!cache.exists()) + cache.mkdirs(); + if (threadPool != null) { + try { + threadPool.shutdownNow(); + threadPool = null; + } catch (Throwable th) { + + } + } + } + + public interface ThunderCallback { + + void status(int code, String info); + + void list(String playList); + + void play(String url); + } + + public static void parse(Context context, String url, ThunderCallback callback) { + init(context); + stop(); + threadPool = Executors.newSingleThreadExecutor(); + if (isMagnet(url) || isThunder(url)) { + String link = isThunder(url) ? XLDownloadManager.getInstance().parserThunderUrl(url) : url; + Uri p = Uri.parse(link); + if (p == null) { + callback.status(-1, "链接错误"); + return; + } + String fileName = XLTaskHelper.instance().getFileName(link); + File cache = new File(cacheRoot + File.separator + fileName); + threadPool.execute(new Runnable() { + @Override + public void run() { + try { + currentTask = isMagnet(url) ? + XLTaskHelper.instance().addMagentTask(url, cacheRoot, fileName) : + XLTaskHelper.instance().addThunderTask(url, cacheRoot, fileName); + } catch (Exception exception) { + exception.printStackTrace(); + currentTask = 0; + } + if (currentTask <= 0) { + callback.status(-1, "链接错误"); + return; + } + int count = 30; + while (true) { + count--; + if (count <= 0) { + callback.status(-1, "解析超时"); + break; + } + XLTaskInfo taskInfo = XLTaskHelper.instance().getTaskInfo(currentTask); + switch (taskInfo.mTaskStatus) { + case 2: { + callback.status(0, "正在获取文件列表..."); + try { + TorrentInfo torrentInfo = XLTaskHelper.instance().getTorrentInfo(cache.getAbsolutePath()); + if (torrentInfo == null || TextUtils.isEmpty(torrentInfo.mInfoHash)) { + callback.status(-1, "解析失败"); + } else { + TorrentFileInfo[] mSubFileInfo = torrentInfo.mSubFileInfo; + ArrayList playList = new ArrayList<>(); + ArrayList list = new ArrayList<>(); + if (mSubFileInfo != null && mSubFileInfo.length >= 0) { + for (TorrentFileInfo sub : mSubFileInfo) { + if (isMedia(sub.mFileName)) { + sub.torrentPath = cache.getAbsolutePath(); + playList.add(sub.mFileName + "$tvbox-torrent:" + list.size()); + list.add(sub); + } + } + } + if (list.size() > 0) { + torrentFileInfoArrayList = list; + callback.list(TextUtils.join("#", playList)); + } else { + callback.status(-1, "文件列表为空!"); + } + } + } catch (Throwable throwable) { + throwable.printStackTrace(); + callback.status(-1, "解析失败"); + } + return; + } + case 3: { + callback.status(-1, "解析失败"); + return; + } + default: { + callback.status(0, "解析中..."); + break; + } + } + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + }); + } + } + + + public static boolean play(String url, ThunderCallback callback) { + if (url.startsWith("tvbox-torrent:")) { + int idx = Integer.parseInt(url.substring(14)); + TorrentFileInfo info = torrentFileInfoArrayList.get(idx); + if (currentTask > 0) { + XLTaskHelper.instance().stopTask(currentTask); + currentTask = 0L; + } + threadPool.execute(new Runnable() { + @Override + public void run() { + String torrentName = new File(info.torrentPath).getName(); + String cache = cacheRoot + File.separator + torrentName.substring(0, torrentName.lastIndexOf(".")); + currentTask = XLTaskHelper.instance().addTorrentTask(info.torrentPath, cache, info.mFileIndex); + if (currentTask < 0) + callback.status(-1, "下载出错"); + int count = 30; + while (true) { + count--; + if (count <= 0) { + callback.status(-1, "解析下载超时"); + break; + } + XLTaskInfo taskInfo = XLTaskHelper.instance().getBtSubTaskInfo(currentTask, info.mFileIndex).mTaskInfo; + switch (taskInfo.mTaskStatus) { + case 3: { + callback.status(-1, errorInfo(taskInfo.mErrorCode)); + return; + } + case 1: + case 4: // 下载中 + case 2: { // 下载完成 + String pUrl = XLTaskHelper.instance().getLoclUrl(cache + File.separator + info.mFileName); + callback.play(pUrl); + return; + } + } + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + }); + return true; + } + return false; + } + + private static String errorInfo(int code) { + switch (code) { + case 9125: + return "文件名太长"; + case 111120: + return "文件路径太长"; + case 111142: + return "文件太小"; + case 111085: + return "磁盘空间不足"; + case 111171: + return "拒绝的网络连接"; + case 9301: + return "缓冲区不足"; + case 114001: + case 114004: + case 114005: + case 114006: + case 114007: + case 114011: + case 9304: + case 111154: + return "版权限制:无权下载"; + case 114101: + return "无效链接"; + default: + return "ErrorCode=" + code; + } + } + + + public static boolean isSupportUrl(String url) { + return isMagnet(url) || isThunder(url)/* || isTorrent(url) || isEd2k(url)*/; + } + + private static boolean isMagnet(String url) { + return url.toLowerCase().startsWith("magnet:"); + } + + private static boolean isThunder(String url) { + return url.toLowerCase().startsWith("thunder"); + } + + private static boolean isTorrent(String url) { + return url.toLowerCase().split(";")[0].endsWith(".torrent"); + } + + private static boolean isEd2k(String url) { + return url.toLowerCase().startsWith("ed2k:"); + } + + static void recursiveDelete(File file) { + if (!file.exists()) + return; + if (file.isDirectory()) { + for (File f : file.listFiles()) { + recursiveDelete(f); + } + } + file.delete(); + } + + static ArrayList formats = new ArrayList<>(); + + static boolean isMedia(String name) { + if (formats.size() == 0) { + formats.add(".rmvb"); + formats.add(".avi"); + formats.add(".mkv"); + formats.add(".flv"); + formats.add(".mp4"); + formats.add(".rm"); + formats.add(".vob"); + formats.add(".wmv"); + formats.add(".mov"); + formats.add(".3gp"); + formats.add(".asf"); + formats.add("mpg"); + formats.add("mpeg"); + formats.add("mpe"); + } + for (String f : formats) { + if (name.toLowerCase().endsWith(f)) + return true; + + } + return false; + } + + static String randomImei() { + return randomString("0123456", 15); + } + + static String randomMac() { + return randomString("ABCDEF0123456", 12).toUpperCase(); + } + + static String randomString(String base, int length) { + Random random = new Random(); + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < length; i++) { + int number = random.nextInt(base.length()); + sb.append(base.charAt(number)); + } + return sb.toString(); + } + +} diff --git a/app/src/main/java/com/github/tvbox/osc/viewmodel/SourceViewModel.java b/app/src/main/java/com/github/tvbox/osc/viewmodel/SourceViewModel.java index 8b2c0350..d59c8d53 100644 --- a/app/src/main/java/com/github/tvbox/osc/viewmodel/SourceViewModel.java +++ b/app/src/main/java/com/github/tvbox/osc/viewmodel/SourceViewModel.java @@ -7,6 +7,7 @@ import androidx.lifecycle.ViewModel; import com.github.catvod.crawler.Spider; import com.github.tvbox.osc.api.ApiConfig; +import com.github.tvbox.osc.base.App; import com.github.tvbox.osc.bean.AbsJson; import com.github.tvbox.osc.bean.AbsSortJson; import com.github.tvbox.osc.bean.AbsSortXml; @@ -17,6 +18,8 @@ import com.github.tvbox.osc.bean.SourceBean; import com.github.tvbox.osc.event.RefreshEvent; import com.github.tvbox.osc.util.DefaultConfig; import com.github.tvbox.osc.util.HawkConfig; +import com.github.tvbox.osc.util.LOG; +import com.github.tvbox.osc.util.thunder.Thunder; import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonElement; @@ -618,6 +621,55 @@ public class SourceViewModel extends ViewModel { } } + private void checkThunder(AbsXml data) { + boolean thunderParse = false; + if (data.movie != null && data.movie.videoList != null && data.movie.videoList.size() == 1) { + Movie.Video video = data.movie.videoList.get(0); + if (video != null && video.urlBean != null && video.urlBean.infoList != null && video.urlBean.infoList.size() == 1) { + Movie.Video.UrlBean.UrlInfo urlInfo = video.urlBean.infoList.get(0); + if (urlInfo != null && urlInfo.beanList.size() == 1 && Thunder.isSupportUrl(urlInfo.beanList.get(0).url)) { + thunderParse = true; + Thunder.parse(App.getInstance(), urlInfo.beanList.get(0).url, new Thunder.ThunderCallback() { + @Override + public void status(int code, String info) { + if (code >= 0) { + LOG.i(info); + } else { + urlInfo.beanList.get(0).name = info; + detailResult.postValue(data); + } + } + + @Override + public void list(String playList) { + urlInfo.urls = playList; + String[] str = playList.split("#"); + List infoBeanList = new ArrayList<>(); + for (String s : str) { + if (s.contains("$")) { + String[] ss = s.split("\\$"); + if (ss.length >= 2) { + infoBeanList.add(new Movie.Video.UrlBean.UrlInfo.InfoBean(ss[0], ss[1])); + } + } + } + urlInfo.beanList = infoBeanList; + detailResult.postValue(data); + } + + @Override + public void play(String url) { + + } + }); + } + } + } + if (!thunderParse) { + detailResult.postValue(data); + } + } + private AbsXml xml(MutableLiveData result, String xml, String sourceKey) { try { XStream xstream = new XStream(new DomDriver());//创建Xstram对象 @@ -637,7 +689,11 @@ public class SourceViewModel extends ViewModel { } else if (quickSearchResult == result) { EventBus.getDefault().post(new RefreshEvent(RefreshEvent.TYPE_QUICK_SEARCH_RESULT, data)); } else if (result != null) { - result.postValue(data); + if (result == detailResult) { + checkThunder(data); + } else { + result.postValue(data); + } } return data; } catch (Exception e) { @@ -654,6 +710,23 @@ public class SourceViewModel extends ViewModel { private AbsXml json(MutableLiveData result, String json, String sourceKey) { try { + // 测试数据 + /*json = "{\n" + + "\t\"list\": [{\n" + + "\t\t\"vod_id\": \"137133\",\n" + + "\t\t\"vod_name\": \"磁力测试\",\n" + + "\t\t\"vod_pic\": \"https:/img9.doubanio.com/view/photo/s_ratio_poster/public/p2656327176.webp\",\n" + + "\t\t\"type_name\": \"剧情 / 爱情 / 古装\",\n" + + "\t\t\"vod_year\": \"2022\",\n" + + "\t\t\"vod_area\": \"中国大陆\",\n" + + "\t\t\"vod_remarks\": \"40集全\",\n" + + "\t\t\"vod_actor\": \"刘亦菲\",\n" + + "\t\t\"vod_director\": \"杨阳\",\n" + + "\t\t\"vod_content\": \"  在钱塘开茶铺的赵盼儿(刘亦菲 饰)惊闻未婚夫、新科探花欧阳旭(徐海乔 饰)要另娶当朝高官之女,不甘命运的她誓要上京讨个公道。在途中她遇到了出自权门但生性正直的皇城司指挥顾千帆(陈晓 饰),并卷入江南一场大案,两人不打不相识从而结缘。赵盼儿凭借智慧解救了被骗婚而惨遭虐待的“江南第一琵琶高手”宋引章(林允 饰)与被苛刻家人逼得离家出走的豪爽厨娘孙三娘(柳岩 饰),三位姐妹从此结伴同行,终抵汴京,见识世间繁华。为了不被另攀高枝的欧阳旭从东京赶走,赵盼儿与宋引章、孙三娘一起历经艰辛,将小小茶坊一步步发展为汴京最大的酒楼,揭露了负心人的真面目,收获了各自的真挚感情和人生感悟,也为无数平凡女子推开了一扇平等救赎之门。\",\n" + + "\t\t\"vod_play_from\": \"磁力测试\",\n" + + "\t\t\"vod_play_url\": \"0$magnet:?xt=urn:btih:9e9358b946c427962533472efdd2efd9e9e38c67&dn=%e9%98%b3%e5%85%89%e7%94%b5%e5%bd%b1www.ygdy8.com.%e7%83%ad%e8%a1%80.2022.BD.1080P.%e9%9f%a9%e8%af%ad%e4%b8%ad%e8%8b%b1%e5%8f%8c%e5%ad%97.mkv&tr=udp%3a%2f%2ftracker.opentrackr.org%3a1337%2fannounce&tr=udp%3a%2f%2fexodus.desync.com%3a6969%2fannounce\"\n" + + "\t}]\n" + + "}";*/ AbsJson absJson = new Gson().fromJson(json, new TypeToken() { }.getType()); AbsXml data = absJson.toAbsXml(); @@ -663,7 +736,11 @@ public class SourceViewModel extends ViewModel { } else if (quickSearchResult == result) { EventBus.getDefault().post(new RefreshEvent(RefreshEvent.TYPE_QUICK_SEARCH_RESULT, data)); } else if (result != null) { - result.postValue(data); + if (result == detailResult) { + checkThunder(data); + } else { + result.postValue(data); + } } return data; } catch (Exception e) { diff --git a/player/src/main/jniLibs/armeabi-v7a/libxl_stat.so b/player/src/main/jniLibs/armeabi-v7a/libxl_stat.so new file mode 100644 index 00000000..f3eaddf2 Binary files /dev/null and b/player/src/main/jniLibs/armeabi-v7a/libxl_stat.so differ diff --git a/player/src/main/jniLibs/armeabi-v7a/libxl_thunder_sdk.so b/player/src/main/jniLibs/armeabi-v7a/libxl_thunder_sdk.so new file mode 100644 index 00000000..25dac438 Binary files /dev/null and b/player/src/main/jniLibs/armeabi-v7a/libxl_thunder_sdk.so differ