├── .github └── workflows │ └── release.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle.kts ├── extension-app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ ├── com │ │ └── heyanle │ │ │ └── easybangumi_extension │ │ │ ├── anfun │ │ │ ├── AnfunDetailedComponent.kt │ │ │ ├── AnfunListComponent.kt │ │ │ ├── AnfunPageComponent.kt │ │ │ ├── AnfunSearchComponent.kt │ │ │ └── AnfunSource.kt │ │ │ ├── anim │ │ │ ├── AnimOneInfo.kt │ │ │ ├── AnimOneSource.kt │ │ │ ├── AnimPageComponent.kt │ │ │ └── DataSource.kt │ │ │ └── ggl │ │ │ ├── GGLComponent.kt │ │ │ ├── GGLDetailedComponent.kt │ │ │ ├── GGLListComponent.kt │ │ │ ├── GGLPlayComponent.kt │ │ │ └── GGLSource.kt │ ├── io │ │ └── github │ │ │ ├── easybangumiorg │ │ │ └── source │ │ │ │ └── aio │ │ │ │ ├── AIOHtpHelper.kt │ │ │ │ ├── OkHttp.kt │ │ │ │ ├── SourceResult.kt │ │ │ │ ├── String.kt │ │ │ │ ├── auete │ │ │ │ ├── AueteDetail.kt │ │ │ │ ├── AuetePage.kt │ │ │ │ ├── AuetePlay.kt │ │ │ │ ├── AueteSearch.kt │ │ │ │ ├── AueteSource.kt │ │ │ │ └── Common.kt │ │ │ │ ├── changzhang │ │ │ │ ├── ChangZhangDetailPage.kt │ │ │ │ ├── ChangZhangPage.kt │ │ │ │ ├── ChangZhangPlayPage.kt │ │ │ │ ├── ChangZhangSearchPage.kt │ │ │ │ └── ChangZhangSource.kt │ │ │ │ ├── fengche │ │ │ │ ├── Common.kt │ │ │ │ ├── FengCheDetail.kt │ │ │ │ ├── FengCheHostUrlHelper.kt │ │ │ │ ├── FengChePage.kt │ │ │ │ ├── FengChePlay.kt │ │ │ │ ├── FengChePrefer.kt │ │ │ │ ├── FengCheSearch.kt │ │ │ │ └── FengCheSource.kt │ │ │ │ ├── libvio │ │ │ │ ├── LibVioDetail.kt │ │ │ │ ├── LibVioPage.kt │ │ │ │ ├── LibVioPlay.kt │ │ │ │ ├── LibVioSearch.kt │ │ │ │ └── LibVioSource.kt │ │ │ │ └── xigua │ │ │ │ ├── Common.kt │ │ │ │ ├── XiGuaPage.kt │ │ │ │ ├── XiguaDetail.kt │ │ │ │ ├── XiguaPlay.kt │ │ │ │ ├── XiguaSearch.kt │ │ │ │ └── XiguaSource.kt │ │ │ └── peacefulprogram │ │ │ ├── easybangumi_mikudm │ │ │ ├── MikudmApiSource.kt │ │ │ ├── MikudmDetailComponent.kt │ │ │ ├── MikudmPageComponent.kt │ │ │ ├── MikudmPlayComponent.kt │ │ │ ├── MikudmPreferenceComponent.kt │ │ │ ├── MikudmSearchComponent.kt │ │ │ └── MikudmUtil.kt │ │ │ ├── easybangumi_mxdm │ │ │ ├── MxdmApiSource.kt │ │ │ ├── MxdmDetailComponent.kt │ │ │ ├── MxdmPageComponent.kt │ │ │ ├── MxdmPlayComponent.kt │ │ │ ├── MxdmPreferenceComponent.kt │ │ │ ├── MxdmSearchComponent.kt │ │ │ └── MxdmUtil.kt │ │ │ └── easybangumi_nivod │ │ │ ├── NivodApiSource.kt │ │ │ ├── NivodConstants.kt │ │ │ ├── NivodDetailComponent.kt │ │ │ ├── NivodPageComponent.kt │ │ │ ├── NivodPlayComponent.kt │ │ │ ├── NivodSearchComponent.kt │ │ │ ├── Util.kt │ │ │ └── dto │ │ │ ├── ChannelRecommendResponse.kt │ │ │ ├── Common.kt │ │ │ ├── SearchVideoResponse.kt │ │ │ ├── VideoDetailResponse.kt │ │ │ └── VideoStreamUrlResponse.kt │ └── org │ │ └── easybangumi │ │ └── extension │ │ └── EasySourceFactory.kt │ └── res │ ├── drawable │ ├── anfun.png │ ├── anim1.png │ ├── auete.png │ ├── changzhang.png │ ├── cycplus.png │ ├── fengche.png │ ├── ggl.png │ ├── libvio.png │ ├── mikudm.png │ ├── mxdm.png │ ├── nivod.png │ ├── xigua.png │ └── yhdm.png │ ├── mipmap-xxhdpi │ ├── app_logo.png │ └── logo_new.png │ ├── values │ └── strings.xml │ └── xml │ ├── backup_rules.xml │ └── data_extraction_rules.xml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── headline.png └── settings.gradle.kts /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | push: 4 | tags: 5 | - '**' 6 | workflow_dispatch: 7 | jobs: 8 | build-and-release: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v3.5.2 13 | - name: Setup Java JDK 14 | uses: actions/setup-java@v1.4.4 15 | with: 16 | java-version: 17 17 | - name: Build 18 | run: | 19 | chmod +x ./gradlew 20 | ./gradlew :extension-app:assemble 21 | - name: Setup build tool version variable 22 | shell: bash 23 | run: | 24 | BUILD_TOOL_VERSION=$(ls /usr/local/lib/android/sdk/build-tools/ | tail -n 1) 25 | echo "BUILD_TOOL_VERSION=$BUILD_TOOL_VERSION" >> $GITHUB_ENV 26 | echo Last build tool version is: $BUILD_TOOL_VERSION 27 | - name: sign-apk 28 | uses: r0adkll/sign-android-release@v1 29 | with: 30 | releaseDirectory: extension-app/build/outputs/apk/release 31 | signingKeyBase64: ${{ secrets.SIGNING_KEY }} 32 | alias: ${{ secrets.KEY_ALIAS }} 33 | keyStorePassword: ${{ secrets.KEY_STORE_PWD }} 34 | keyPassword: ${{ secrets.KEY_PWD }} 35 | env: 36 | # override default build-tools version (29.0.3) -- optional 37 | BUILD_TOOLS_VERSION: ${{ env.BUILD_TOOL_VERSION }} 38 | - name: rename-apk 39 | run: | 40 | mv extension-app/build/outputs/apk/release/extension-app-release-unsigned-signed.apk extension-app-${{ github.ref_name }}.apk 41 | - name: Release 42 | run: | 43 | gh release create -d ${{ github.ref_name }} extension-app-${{ github.ref_name }}.apk 44 | env: 45 | GITHUB_TOKEN: ${{ secrets.TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | .idea 4 | /local.properties 5 | /.idea/caches 6 | /.idea/libraries 7 | /.idea/modules.xml 8 | /.idea/workspace.xml 9 | /.idea/navEditor.xml 10 | /.idea/assetWizardSettings.xml 11 | .DS_Store 12 | /build 13 | /captures 14 | /publishing 15 | .externalNativeBuild 16 | .cxx 17 | local.properties 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![纯纯看番本体番剧源](./headline.png) 2 | 3 |

4 | release 5 | license 6 |

7 | 8 | 本仓库内容均来自互联网投稿,为了代码安全性只收录提交到纯纯看番官方的代码,并由官方编译。 9 | 10 | 如果使用其他的源,用户需要自己甄别插件安全性,纯纯看番官方无法保证这些插件是否安全。 11 | 12 | 如果本仓库内容您认为侵犯了你的权益,请提交issue联系我们删除。 13 | 14 | 纯纯看番本体:[https://github.com/easybangumiorg/EasyBangumi](https://github.com/easybangumiorg/EasyBangumi) 15 | 16 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | plugins { 3 | id("com.android.application") version "8.1.2" apply false 4 | id("org.jetbrains.kotlin.android") version "1.9.0" apply false 5 | } 6 | 7 | tasks.create("clean") { 8 | delete { 9 | rootProject.buildDir 10 | } 11 | } 12 | 13 | subprojects { 14 | // 定义检查依赖变化的时间间隔,!!配置为0实时刷新 15 | configurations.all { 16 | // check for updates every build 17 | resolutionStrategy.cacheChangingModulesFor(0, java.util.concurrent.TimeUnit.SECONDS) 18 | } 19 | } -------------------------------------------------------------------------------- /extension-app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /extension-app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id ("com.android.application") 3 | id ("org.jetbrains.kotlin.android") 4 | } 5 | 6 | // 包名 7 | val packageName = "org.easybangumi.extension" 8 | 9 | // 库版本,目前 5.0.3 支持的库版本为 3 到 5 10 | val extensionLibVersion = 7 11 | 12 | android { 13 | namespace = packageName 14 | compileSdk = 34 15 | 16 | defaultConfig { 17 | applicationId = packageName 18 | minSdk = 21 19 | targetSdk = 34 20 | versionCode = 7 21 | versionName = "1.6" 22 | 23 | manifestPlaceholders.put("extensionLibVersion", extensionLibVersion) 24 | 25 | } 26 | 27 | buildTypes { 28 | release { 29 | isMinifyEnabled = false 30 | } 31 | } 32 | compileOptions { 33 | sourceCompatibility = JavaVersion.VERSION_1_8 34 | targetCompatibility = JavaVersion.VERSION_1_8 35 | } 36 | kotlinOptions { 37 | jvmTarget = "1.8" 38 | } 39 | 40 | dependenciesInfo{ 41 | includeInApk = false 42 | includeInBundle = false 43 | } 44 | } 45 | 46 | 47 | dependencies { 48 | implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1") 49 | compileOnly("io.github.easybangumiorg:extension-api:1.${extensionLibVersion}-SNAPSHOT") 50 | } 51 | -------------------------------------------------------------------------------- /extension-app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | 23 | -keep class com.heyanle.**{*;} 24 | -keep interface com.heyanle.**{*;} 25 | 26 | # 协程 27 | 28 | -keep class kotlin.** { *; } 29 | -keep class kotlin.Metadata { *; } 30 | -dontwarn kotlin.** 31 | -keepclassmembers class **$WhenMappings { 32 | ; 33 | } 34 | -keepclassmembers class kotlin.Metadata { 35 | public ; 36 | } 37 | -assumenosideeffects class kotlin.jvm.internal.Intrinsics { 38 | static void checkParameterIsNotNull(java.lang.Object, java.lang.String); 39 | } 40 | -keep class kotlinx.coroutines.android.** {*;} 41 | # ServiceLoader support 42 | -keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {} 43 | -keepnames class kotlinx.coroutines.CoroutineExceptionHandler {} 44 | -keepnames class kotlinx.coroutines.android.AndroidExceptionPreHandler {} 45 | -keepnames class kotlinx.coroutines.android.AndroidDispatcherFactory {} 46 | 47 | # Most of volatile fields are updated with AFU and should not be mangled 48 | -keepclassmembernames class kotlinx.** { 49 | volatile ; 50 | } 51 | 52 | -------------------------------------------------------------------------------- /extension-app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 12 | 13 | 14 | 15 | 18 | 19 | 20 | 23 | 24 | 25 | 27 | 28 | 29 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /extension-app/src/main/java/com/heyanle/easybangumi_extension/anfun/AnfunListComponent.kt: -------------------------------------------------------------------------------- 1 | package com.heyanle.easybangumi_extension.anfun 2 | 3 | import com.heyanle.easybangumi4.source_api.component.ComponentWrapper 4 | import com.heyanle.easybangumi4.source_api.entity.CartoonCover 5 | import com.heyanle.easybangumi4.source_api.entity.CartoonCoverImpl 6 | import com.heyanle.easybangumi4.source_api.utils.api.OkhttpHelper 7 | import com.heyanle.easybangumi4.source_api.utils.core.SourceUtils 8 | import org.jsoup.select.Elements 9 | 10 | /** 11 | * Created by heyanle on 2024/1/28. 12 | * https://github.com/heyanLE 13 | */ 14 | class AnfunListComponent( 15 | private val okhttpHelper: OkhttpHelper, 16 | ) : ComponentWrapper() { 17 | 18 | companion object { 19 | const val ROOT_URL = "https://www.anfuns.cc" 20 | } 21 | 22 | suspend fun listPage( 23 | element: Elements, 24 | ): Pair> { 25 | val r = arrayListOf() 26 | for (video in element) { 27 | video.apply { 28 | val name = select("a").attr("title") 29 | val videoUrl = select("a").attr("href") 30 | val coverUrl = select("a").attr("data-original") 31 | val episode = select(".remarks").text() 32 | val id = videoUrl.subSequence(7, videoUrl.length - 5).toString() 33 | if (!name.isNullOrBlank() && !videoUrl.isNullOrBlank() && !coverUrl.isNullOrBlank()) { 34 | val b = CartoonCoverImpl( 35 | id = id, 36 | source = source.key, 37 | url = videoUrl, 38 | title = name, 39 | intro = episode ?: "", 40 | coverUrl = SourceUtils.urlParser(ROOT_URL, coverUrl) 41 | ) 42 | r.add(b) 43 | } 44 | } 45 | } 46 | return Pair(null, r) 47 | } 48 | 49 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/com/heyanle/easybangumi_extension/anfun/AnfunPageComponent.kt: -------------------------------------------------------------------------------- 1 | package com.heyanle.easybangumi_extension.anfun 2 | 3 | import com.heyanle.easybangumi4.source_api.component.ComponentWrapper 4 | import com.heyanle.easybangumi4.source_api.component.page.PageComponent 5 | import com.heyanle.easybangumi4.source_api.component.page.SourcePage 6 | import com.heyanle.easybangumi4.source_api.entity.CartoonCover 7 | import com.heyanle.easybangumi4.source_api.entity.CartoonCoverImpl 8 | import com.heyanle.easybangumi4.source_api.utils.api.OkhttpHelper 9 | import com.heyanle.easybangumi4.source_api.utils.core.SourceUtils 10 | import com.heyanle.easybangumi4.source_api.utils.core.network.GET 11 | import com.heyanle.easybangumi4.source_api.withResult 12 | import kotlinx.coroutines.Dispatchers 13 | import org.jsoup.Jsoup 14 | 15 | /** 16 | * Created by heyanle on 2024/1/28. 17 | * https://github.com/heyanLE 18 | */ 19 | class AnfunPageComponent( 20 | private val anfunListComponent: AnfunListComponent, 21 | private val okhttpHelper: OkhttpHelper, 22 | ): ComponentWrapper(), PageComponent { 23 | 24 | override fun getPages(): List { 25 | return listOf( 26 | // 首页 27 | SourcePage.Group( 28 | "首页", 29 | false, 30 | ) { 31 | withResult(Dispatchers.IO) { 32 | homeListPages() 33 | } 34 | }, 35 | 36 | // 新番时刻表 37 | SourcePage.Group( 38 | "每日更新列表", 39 | false, 40 | ) { 41 | withResult(Dispatchers.IO) { 42 | homeTimelinePages() 43 | } 44 | }, 45 | ) 46 | } 47 | 48 | // 获取主页所有 ListPage 49 | private suspend fun homeListPages(): List { 50 | val res = arrayListOf() 51 | val doc = Jsoup.parse( 52 | okhttpHelper.cloudflareWebViewClient.newCall(GET(AnfunSource.ROOT_URL)) 53 | .execute().body?.string()!! 54 | ) 55 | 56 | val modules = doc.select("#conch-content").select("div[class='container']") 57 | for (em in modules){ 58 | val moduleHeading = em.select(".hl-rb-head").first() 59 | val type = moduleHeading?.select(".hl-rb-title") 60 | val label = type?.text()?:continue 61 | if (label == "每周更新" || label == "网络资讯" || label == "动漫专题") continue 62 | val lis = em.select(".row").select("ul").select("li") 63 | val page = SourcePage.SingleCartoonPage.WithCover( 64 | label = label, 65 | firstKey = { 0 }, 66 | ) { 67 | withResult(Dispatchers.IO) { 68 | anfunListComponent.listPage( 69 | lis 70 | ) 71 | } 72 | } 73 | res.add(page) 74 | } 75 | return res 76 | } 77 | 78 | // 获取新番时刻表 ListPage 79 | private suspend fun homeTimelinePages(): List { 80 | val res = arrayListOf() 81 | val docmuent = Jsoup.parse( 82 | okhttpHelper.cloudflareWebViewClient.newCall(GET(AnfunSource.ROOT_URL)) 83 | .execute().body?.string()!! 84 | ) 85 | val doc = docmuent.select("#conch-content").select("div[class='container']")[2] 86 | val days = mutableListOf() 87 | doc.select(".hl-rb-head").select("span").select("a").forEach { 88 | // Log.e("星期", it.text()) 89 | days.add(it.text()) 90 | } 91 | val updateList = doc.select(".row").select(".hl-list-wrap") 92 | for ((index,ems) in updateList.withIndex()){ 93 | val r = arrayListOf() 94 | val target = ems.select("ul").select("li") 95 | for (em in target) { 96 | val titleEm = em.select("a") 97 | val cover = titleEm.attr("data-original") 98 | val title = titleEm.attr("title") 99 | val episode = titleEm.select(".remarks").text() 100 | val url = titleEm.attr("href") 101 | val id = url.subSequence(7, url.length - 5).toString() 102 | val desc = em.select(".hl-item-sub").text() 103 | if (!title.isNullOrBlank() && !episode.isNullOrBlank() && !url.isNullOrBlank()) { 104 | val car = CartoonCoverImpl( 105 | id = id, 106 | source = source.key, 107 | url = url, 108 | title = title, 109 | intro = episode, 110 | coverUrl = null, 111 | ) 112 | r.add(car) 113 | } 114 | } 115 | res.add( 116 | SourcePage.SingleCartoonPage.WithoutCover( 117 | days[index], 118 | firstKey = { 0 }, 119 | load = { 120 | withResult { 121 | null to r 122 | } 123 | } 124 | )) 125 | if (index == 6) break 126 | } 127 | return res 128 | } 129 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/com/heyanle/easybangumi_extension/anfun/AnfunSearchComponent.kt: -------------------------------------------------------------------------------- 1 | package com.heyanle.easybangumi_extension.anfun 2 | 3 | import android.util.Log 4 | import com.heyanle.easybangumi4.source_api.SourceResult 5 | import com.heyanle.easybangumi4.source_api.component.ComponentWrapper 6 | import com.heyanle.easybangumi4.source_api.component.search.SearchComponent 7 | import com.heyanle.easybangumi4.source_api.entity.CartoonCover 8 | import com.heyanle.easybangumi4.source_api.entity.CartoonCoverImpl 9 | import com.heyanle.easybangumi4.source_api.utils.api.OkhttpHelper 10 | import com.heyanle.easybangumi4.source_api.utils.core.SourceUtils 11 | import com.heyanle.easybangumi4.source_api.utils.core.network.GET 12 | import com.heyanle.easybangumi4.source_api.withResult 13 | import kotlinx.coroutines.Dispatchers 14 | import org.jsoup.Jsoup 15 | import org.jsoup.select.Elements 16 | 17 | /** 18 | * Created by heyanle on 2024/1/29. 19 | * https://github.com/heyanLE 20 | */ 21 | class AnfunSearchComponent( 22 | private val okhttpHelper: OkhttpHelper 23 | ) : ComponentWrapper(), SearchComponent { 24 | override fun getFirstSearchKey(keyword: String): Int { 25 | return 0 26 | } 27 | 28 | override suspend fun search( 29 | pageKey: Int, 30 | keyword: String 31 | ): SourceResult>> { 32 | return withResult(Dispatchers.IO) { 33 | val url = SourceUtils.urlParser( 34 | AnfunSource.ROOT_URL, 35 | "/search/page/${pageKey + 1}/wd/${keyword}.html" 36 | ) 37 | Log.e("TAG", "--->${url}") 38 | val d = okhttpHelper.cloudflareWebViewClient.newCall( 39 | GET( 40 | SourceUtils.urlParser( 41 | AnfunSource.ROOT_URL, 42 | url 43 | ) 44 | ) 45 | ).execute().body?.string()!! 46 | val doc = Jsoup.parse(d) 47 | val r = arrayListOf() 48 | val lpic = doc.select("#conch-content").select(".row").select("ul")[0] 49 | val results: Elements = lpic.select("li") 50 | if (results.size == 0) { 51 | Log.e("TAG", "已经加载完毕~") 52 | return@withResult Pair(null, r) 53 | } 54 | for (i in results.indices) { 55 | var cover = results[i].select("a").attr("data-original") 56 | if (cover.startsWith("//")) { 57 | cover = "https:${cover}" 58 | } 59 | val title = results[i].select("a").attr("title") 60 | val itemUrl = results[i].select("a").attr("href") 61 | val id = itemUrl.subSequence(7, itemUrl.length - 5).toString() 62 | val episode = results[i].select(".hl-pic-text").select("span").text() 63 | val describe = 64 | results[i].select("p[class='hl-item-sub hl-text-muted hl-lc-2']").text() 65 | 66 | val b = CartoonCoverImpl( 67 | id = id, 68 | title = title, 69 | url = itemUrl, 70 | intro = episode, 71 | coverUrl = SourceUtils.urlParser(AnfunSource.ROOT_URL, cover), 72 | source = source.key, 73 | ) 74 | r.add(b) 75 | } 76 | val pages = doc.select(".hl-list-wrap").select(".hl-page-wrap").select("li") 77 | return@withResult if (pages.isEmpty()) { 78 | Pair(null, r) 79 | } else { 80 | var hasNext = false 81 | for (p in pages) { 82 | if (p.text() == (pageKey + 2).toString() || p.text() == "下一页") { 83 | hasNext = true 84 | break 85 | } 86 | } 87 | if (!hasNext) { 88 | Pair(null, r) 89 | } else { 90 | Pair(pageKey + 1, r) 91 | } 92 | } 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/com/heyanle/easybangumi_extension/anfun/AnfunSource.kt: -------------------------------------------------------------------------------- 1 | package com.heyanle.easybangumi_extension.anfun 2 | 3 | import com.heyanle.extension_api.ExtensionIconSource 4 | import com.heyanle.extension_api.ExtensionSource 5 | import org.easybangumi.extension.R 6 | import kotlin.reflect.KClass 7 | 8 | /** 9 | * Created by heyanle on 2024/1/28. 10 | * https://github.com/heyanLE 11 | */ 12 | class AnfunSource : ExtensionSource(), ExtensionIconSource { 13 | 14 | companion object { 15 | const val ROOT_URL = "https://www.anfuns.cc" 16 | } 17 | 18 | override fun getIconResourcesId(): Int? { 19 | return R.drawable.anfun 20 | } 21 | 22 | override val sourceKey: String 23 | get() = "heyanle_Anfun" 24 | override val describe: String? 25 | get() = null 26 | override val label: String 27 | get() = "AnFuns动漫" 28 | override val version: String 29 | get() = "1.2" 30 | override val versionCode: Int 31 | get() = 2 32 | 33 | override fun register(): List> { 34 | return listOf( 35 | AnfunDetailedComponent::class, 36 | AnfunListComponent::class, 37 | AnfunPageComponent::class, 38 | AnfunSearchComponent::class 39 | ) 40 | } 41 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/com/heyanle/easybangumi_extension/anim/AnimOneInfo.kt: -------------------------------------------------------------------------------- 1 | package com.heyanle.easybangumi_extension.anim 2 | 3 | import com.heyanle.easybangumi4.source_api.Source 4 | import com.heyanle.easybangumi4.source_api.entity.CartoonCoverImpl 5 | import com.heyanle.easybangumi4.source_api.entity.CartoonImpl 6 | 7 | 8 | /** 9 | * Created by HeYanLe on 2023/7/16 16:56. 10 | * https://github.com/heyanLE 11 | */ 12 | data class AnimOneInfo( 13 | val id: Long, 14 | val name: String, 15 | val intro: String, 16 | val year: String, 17 | val season: String, 18 | val translator: String, 19 | ){ 20 | 21 | fun toCartoon(source: Source) = CartoonImpl( 22 | id = id.toString(), 23 | source = source.key, 24 | url = source.describe?:"", 25 | title = name, 26 | genre = "${year}, ${season}, $translator", 27 | intro = intro, 28 | ) 29 | 30 | fun toCartoonCover(source: Source) = CartoonCoverImpl( 31 | id = id.toString(), 32 | source = source.key, 33 | url = source.describe?:"", 34 | title = name 35 | ) 36 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/com/heyanle/easybangumi_extension/anim/AnimOneSource.kt: -------------------------------------------------------------------------------- 1 | package com.heyanle.easybangumi_extension.anim 2 | 3 | 4 | import com.heyanle.easybangumi4.source_api.Source 5 | import com.heyanle.extension_api.ExtensionIconSource 6 | import com.heyanle.extension_api.ExtensionSource 7 | import org.easybangumi.extension.R 8 | import kotlin.reflect.KClass 9 | 10 | /** 11 | * Created by HeYanLe on 2023/6/6 18:25. 12 | * https://github.com/heyanLE 13 | */ 14 | class AnimOneSource : Source, ExtensionIconSource { 15 | 16 | override fun getIconResourcesId(): Int? { 17 | return R.drawable.anim1 18 | } 19 | 20 | override val key: String 21 | get() = "com.heyanle.easybangumi_extension.animone-anim_one" 22 | override val describe: String? 23 | get() = "https://anime1.me/" 24 | override val label: String 25 | get() = "Anim1" 26 | override val version: String 27 | get() = "2.0" 28 | override val versionCode: Int 29 | get() = 2 30 | 31 | override fun register(): List> { 32 | return listOf( 33 | AnimPageComponent::class 34 | ) 35 | } 36 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/com/heyanle/easybangumi_extension/anim/DataSource.kt: -------------------------------------------------------------------------------- 1 | package com.heyanle.easybangumi_extension.anim 2 | 3 | import com.google.gson.JsonParser 4 | import com.heyanle.easybangumi4.source_api.utils.api.NetworkHelper 5 | import com.heyanle.easybangumi4.source_api.utils.api.OkhttpHelper 6 | import com.heyanle.easybangumi4.source_api.utils.core.network.GET 7 | import kotlinx.coroutines.Dispatchers 8 | import kotlinx.coroutines.withContext 9 | import org.jsoup.Jsoup 10 | 11 | 12 | /** 13 | * Created by HeYanLe on 2023/7/16 16:56. 14 | * https://github.com/heyanLE 15 | */ 16 | object DataSource { 17 | 18 | private var allData: List? = null 19 | 20 | suspend fun getData( 21 | refresh: Boolean, 22 | okhttpHelper: OkhttpHelper, 23 | ):List { 24 | return withContext(Dispatchers.IO){ 25 | if(allData == null || refresh){ 26 | val rs = arrayListOf() 27 | runCatching { 28 | val url = "https://d1zquzjgwo9yb.cloudfront.net/?_=" 29 | val res = okhttpHelper.client.newCall(GET(url)).execute().body?.string()?:"" 30 | val jsonElement = JsonParser.parseString(res).asJsonArray 31 | rs.addAll(jsonElement.map { 32 | val d = it.asJsonArray 33 | AnimOneInfo( 34 | id = d.get(0).asLong, 35 | name = Jsoup.parse(d.get(1).asString).text(), 36 | intro = d.get(2).asString, 37 | year = d.get(3).asString, 38 | season = d.get(4).asString, 39 | translator = d.get(5).asString, 40 | ) 41 | }) 42 | }.onFailure { 43 | it.printStackTrace() 44 | } 45 | allData = rs 46 | rs 47 | }else{ 48 | allData?: emptyList() 49 | } 50 | } 51 | } 52 | 53 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/com/heyanle/easybangumi_extension/ggl/GGLComponent.kt: -------------------------------------------------------------------------------- 1 | package com.heyanle.easybangumi_extension.ggl 2 | 3 | import android.util.Log 4 | import com.heyanle.easybangumi4.source_api.SourceResult 5 | import com.heyanle.easybangumi4.source_api.component.ComponentWrapper 6 | import com.heyanle.easybangumi4.source_api.component.page.PageComponent 7 | import com.heyanle.easybangumi4.source_api.component.page.SourcePage 8 | import com.heyanle.easybangumi4.source_api.component.search.SearchComponent 9 | import com.heyanle.easybangumi4.source_api.entity.CartoonCover 10 | import com.heyanle.easybangumi4.source_api.entity.CartoonCoverImpl 11 | import com.heyanle.easybangumi4.source_api.utils.core.SourceUtils 12 | import com.heyanle.easybangumi4.source_api.withResult 13 | import kotlinx.coroutines.Dispatchers 14 | import java.net.URLEncoder 15 | 16 | /** 17 | * Created by heyanle on 2024/1/28. 18 | * https://github.com/heyanLE 19 | */ 20 | class GGLComponent( 21 | private val gglListComponent: GGLListComponent, 22 | ) : ComponentWrapper(), PageComponent, SearchComponent { 23 | 24 | 25 | override fun getPages(): List { 26 | return listOf( 27 | SourcePage.SingleCartoonPage.WithCover( 28 | label = "日番", 29 | firstKey = { 1 }, 30 | ) { 31 | withResult(Dispatchers.IO) { 32 | gglListComponent.listHomePage( 33 | "https://anime.girigirilove.com/show/2-----------/", 34 | it 35 | ) 36 | } 37 | }, 38 | SourcePage.SingleCartoonPage.WithCover( 39 | label = "美番", 40 | firstKey = { 1 }, 41 | ) { 42 | withResult(Dispatchers.IO) { 43 | gglListComponent.listHomePage( 44 | "https://anime.girigirilove.com/show/3-----------/", 45 | it 46 | ) 47 | } 48 | 49 | }, 50 | SourcePage.SingleCartoonPage.WithCover( 51 | label = "剧场版", 52 | firstKey = { 1 }, 53 | ) { 54 | withResult(Dispatchers.IO) { 55 | gglListComponent.listHomePage( 56 | "https://anime.girigirilove.com/show/21-----------/", 57 | it 58 | ) 59 | } 60 | 61 | }, 62 | ) 63 | } 64 | 65 | override fun getFirstSearchKey(keyword: String): Int { 66 | return 1 67 | } 68 | 69 | override suspend fun search( 70 | pageKey: Int, 71 | keyword: String 72 | ): SourceResult>> { 73 | return withResult(Dispatchers.IO) { 74 | val url = SourceUtils.urlParser( 75 | GGLListComponent.ROOT_URL, 76 | "/search/${URLEncoder.encode(keyword, "utf-8")}----------${pageKey}---/" 77 | ) 78 | val doc = gglListComponent.getDoc(url).getOrThrow() 79 | val list = arrayListOf() 80 | 81 | doc.select("div div.public-list-box.search-box").forEach { 82 | val uu = it.child(1).child(0).attr("href") 83 | val id = uu.subSequence(1, uu.length - 1).toString() 84 | 85 | val coverStyle = it.select("div.cover")[0].attr("style") 86 | val coverPattern = Regex("""(?<=url\().*(?=\))""") 87 | var cover = coverPattern.find(coverStyle)?.value ?: "" 88 | if (cover.startsWith("//")) { 89 | cover = "http:${cover}" 90 | } 91 | Log.d("GGLSearchComponent", coverStyle) 92 | 93 | val title = it.select("div.thumb-content div.thumb-txt").first()?.text() ?: "" 94 | val b = CartoonCoverImpl( 95 | id = id, 96 | title = title, 97 | url = SourceUtils.urlParser(GGLListComponent.ROOT_URL, uu), 98 | intro = "", 99 | coverUrl = SourceUtils.urlParser(GGLListComponent.ROOT_URL, cover), 100 | source = source.key, 101 | ) 102 | list.add(b) 103 | } 104 | 105 | (if (list.isEmpty()) null else pageKey + 1) to list 106 | 107 | 108 | } 109 | } 110 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/com/heyanle/easybangumi_extension/ggl/GGLDetailedComponent.kt: -------------------------------------------------------------------------------- 1 | package com.heyanle.easybangumi_extension.ggl 2 | 3 | import com.heyanle.easybangumi4.source_api.SourceResult 4 | import com.heyanle.easybangumi4.source_api.component.ComponentWrapper 5 | import com.heyanle.easybangumi4.source_api.component.detailed.DetailedComponent 6 | import com.heyanle.easybangumi4.source_api.component.page.PageComponent 7 | import com.heyanle.easybangumi4.source_api.component.search.SearchComponent 8 | import com.heyanle.easybangumi4.source_api.component.update.UpdateComponent 9 | import com.heyanle.easybangumi4.source_api.entity.Cartoon 10 | import com.heyanle.easybangumi4.source_api.entity.CartoonImpl 11 | import com.heyanle.easybangumi4.source_api.entity.CartoonSummary 12 | import com.heyanle.easybangumi4.source_api.entity.Episode 13 | import com.heyanle.easybangumi4.source_api.entity.PlayLine 14 | import com.heyanle.easybangumi4.source_api.utils.api.OkhttpHelper 15 | import com.heyanle.easybangumi4.source_api.utils.core.SourceUtils 16 | import com.heyanle.easybangumi4.source_api.utils.core.network.GET 17 | import com.heyanle.easybangumi4.source_api.withResult 18 | import kotlinx.coroutines.Dispatchers 19 | import org.jsoup.Jsoup 20 | import org.jsoup.nodes.Document 21 | 22 | /** 23 | * Created by heyanle on 2024/1/28. 24 | * https://github.com/heyanLE 25 | */ 26 | class GGLDetailedComponent( 27 | private val okhttpHelper: OkhttpHelper, 28 | ) : ComponentWrapper(), DetailedComponent, UpdateComponent { 29 | 30 | override suspend fun getAll(summary: CartoonSummary): SourceResult>> { 31 | return withResult(Dispatchers.IO) { 32 | val doc = getDoc(summary) 33 | detailed(doc, summary) to playLine(doc, summary) 34 | } 35 | 36 | } 37 | 38 | override suspend fun getDetailed(summary: CartoonSummary): SourceResult { 39 | return withResult(Dispatchers.IO) { 40 | val doc = getDoc(summary) 41 | detailed(doc, summary) 42 | } 43 | } 44 | 45 | override suspend fun getPlayLine(summary: CartoonSummary): SourceResult> { 46 | return withResult(Dispatchers.IO) { 47 | val doc = getDoc(summary) 48 | playLine(doc, summary) 49 | } 50 | } 51 | 52 | private fun getDoc(summary: CartoonSummary): Document { 53 | val d = okhttpHelper.cloudflareWebViewClient.newCall( 54 | GET( 55 | SourceUtils.urlParser( 56 | GGLListComponent.ROOT_URL, 57 | summary.id 58 | ) 59 | ) 60 | ) 61 | .execute().body?.string() ?: throw NullPointerException() 62 | return Jsoup.parse(d) 63 | } 64 | 65 | private fun detailed(doc: Document, summary: CartoonSummary): Cartoon { 66 | val title = doc.select("div.detail-info h3.slide-info-title").text() 67 | // val genre = doc.select("div.detail-info div.slide-info span").map { it.text() }.joinToString { ", " } 68 | val cover = doc.select("div.wow div.detail-pic img").first()?.attr("data-src") ?: "" 69 | val desc = doc.select("div.switch-box div.check div.text").first()?.text() ?: "" 70 | return CartoonImpl( 71 | id = summary.id, 72 | url = SourceUtils.urlParser(GGLListComponent.ROOT_URL, summary.id), 73 | source = summary.source, 74 | title = title, 75 | coverUrl = SourceUtils.urlParser(GGLListComponent.ROOT_URL, cover), 76 | intro = "", 77 | description = desc, 78 | genre = null, 79 | status = Cartoon.STATUS_UNKNOWN, 80 | updateStrategy = Cartoon.UPDATE_STRATEGY_ALWAYS, 81 | ) 82 | } 83 | 84 | private fun playLine(doc: Document, summary: CartoonSummary): List { 85 | val tabs = 86 | doc.select("div.anthology.wow div.anthology-tab div.swiper-wrapper a.swiper-slide") 87 | .iterator() 88 | val epRoot = doc.select("div.anthology-list-box div ul.anthology-list-play").iterator() 89 | val playLines = arrayListOf() 90 | var ii = 1 91 | while (tabs.hasNext() && epRoot.hasNext()) { 92 | val tab = tabs.next() 93 | val ul = epRoot.next() 94 | 95 | val es = arrayListOf() 96 | ul.children().forEachIndexed { index, element -> 97 | es.add( 98 | Episode( 99 | id = (index + 1).toString(), 100 | label = element?.text() ?: "", 101 | order = index 102 | ) 103 | ) 104 | } 105 | 106 | playLines.add( 107 | PlayLine( 108 | id = ii.toString(), 109 | label = tab.text(), 110 | episode = es 111 | ) 112 | ) 113 | ii++ 114 | } 115 | return playLines 116 | } 117 | 118 | override suspend fun update( 119 | cartoon: Cartoon, 120 | oldPlayLine: List 121 | ): SourceResult { 122 | return withResult(Dispatchers.IO) { 123 | when (val n = getAll(CartoonSummary(cartoon.id, cartoon.source))) { 124 | is SourceResult.Complete -> { 125 | n.data.first.apply { 126 | 127 | val newPlayLine = n.data.second 128 | 129 | if (oldPlayLine.size != newPlayLine.size) { 130 | isUpdate = true 131 | } else { 132 | isUpdate = false 133 | for (i in oldPlayLine.indices) { 134 | if (oldPlayLine[i].episode.size != newPlayLine[i].episode.size) { 135 | isUpdate = true 136 | break 137 | } 138 | } 139 | } 140 | } 141 | } 142 | 143 | is SourceResult.Error -> { 144 | throw n.throwable 145 | } 146 | } 147 | } 148 | } 149 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/com/heyanle/easybangumi_extension/ggl/GGLListComponent.kt: -------------------------------------------------------------------------------- 1 | package com.heyanle.easybangumi_extension.ggl 2 | 3 | import com.heyanle.easybangumi4.source_api.component.ComponentWrapper 4 | import com.heyanle.easybangumi4.source_api.entity.CartoonCover 5 | import com.heyanle.easybangumi4.source_api.entity.CartoonCoverImpl 6 | import com.heyanle.easybangumi4.source_api.utils.api.OkhttpHelper 7 | import com.heyanle.easybangumi4.source_api.utils.core.SourceUtils 8 | import com.heyanle.easybangumi4.source_api.utils.core.network.GET 9 | 10 | import org.jsoup.Jsoup 11 | import org.jsoup.nodes.Document 12 | 13 | /** 14 | * Created by HeYanLe on 2023/5/21 21:18. 15 | * https://github.com/heyanLE 16 | */ 17 | class GGLListComponent( 18 | private val okhttpHelper: OkhttpHelper, 19 | ): ComponentWrapper() { 20 | 21 | companion object { 22 | val ROOT_URL = "https://anime.girigirilove.com" 23 | } 24 | suspend fun listHomePage( 25 | url: String, 26 | page: Int, 27 | ): Pair>{ 28 | val d = if(!url.endsWith("/")) "${url}/" else url 29 | val u = d.replace("---/", "${page}---/") 30 | val list = arrayListOf() 31 | val doc = getDoc(u).getOrThrow() 32 | doc.select("div.border-box div.public-list-box").forEach { 33 | val uu = it.child(0).child(0).attr("href") 34 | val id = uu.subSequence(1, uu.length-1).toString() 35 | list.add( 36 | CartoonCoverImpl( 37 | id = id, 38 | source = source.key, 39 | url = SourceUtils.urlParser(ROOT_URL, uu), 40 | title = it.child(1).child(0).text(), 41 | intro = it.select("span .public-list-prb").first()?.text(), 42 | coverUrl = SourceUtils.urlParser(ROOT_URL,it.select("img").first()?.attr("data-src")?:""), 43 | ) 44 | ) 45 | } 46 | 47 | 48 | return (if(list.isEmpty()) null else page+1) to list 49 | } 50 | 51 | fun getDoc(target: String): Result { 52 | return runCatching { 53 | val req = okhttpHelper.cloudflareWebViewClient.newCall( 54 | GET( 55 | SourceUtils.urlParser(ROOT_URL, target), 56 | ) 57 | ).execute() 58 | val body = req.body!!.string() 59 | Jsoup.parse(body) 60 | } 61 | } 62 | } 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /extension-app/src/main/java/com/heyanle/easybangumi_extension/ggl/GGLPlayComponent.kt: -------------------------------------------------------------------------------- 1 | package com.heyanle.easybangumi_extension.ggl 2 | 3 | import android.util.Log 4 | import com.heyanle.easybangumi4.source_api.ParserException 5 | import com.heyanle.easybangumi4.source_api.SourceResult 6 | import com.heyanle.easybangumi4.source_api.component.ComponentWrapper 7 | import com.heyanle.easybangumi4.source_api.component.play.PlayComponent 8 | import com.heyanle.easybangumi4.source_api.entity.CartoonSummary 9 | import com.heyanle.easybangumi4.source_api.entity.Episode 10 | import com.heyanle.easybangumi4.source_api.entity.PlayLine 11 | import com.heyanle.easybangumi4.source_api.entity.PlayerInfo 12 | import com.heyanle.easybangumi4.source_api.utils.api.NetworkHelper 13 | import com.heyanle.easybangumi4.source_api.utils.api.OkhttpHelper 14 | import com.heyanle.easybangumi4.source_api.utils.api.WebViewHelper 15 | import com.heyanle.easybangumi4.source_api.utils.core.SourceUtils 16 | import com.heyanle.easybangumi4.source_api.withResult 17 | import kotlinx.coroutines.Dispatchers 18 | import org.jsoup.Jsoup 19 | 20 | /** 21 | * Created by heyanle on 2024/1/28. 22 | * https://github.com/heyanLE 23 | */ 24 | class GGLPlayComponent( 25 | private val webViewHelper: WebViewHelper, 26 | private val networkHelper: NetworkHelper, 27 | ): ComponentWrapper(), PlayComponent { 28 | 29 | override suspend fun getPlayInfo( 30 | summary: CartoonSummary, 31 | playLine: PlayLine, 32 | episode: Episode 33 | ): SourceResult { 34 | return withResult(Dispatchers.IO) { 35 | val urlPath = "${if(summary.id.startsWith("GV"))summary.id else "GV${summary.id}"}-${playLine.id}-${episode.id}" 36 | val url = SourceUtils.urlParser(GGLListComponent.ROOT_URL, "/play${urlPath}/") 37 | val doc = webViewHelper.getRenderedHtmlCode(url = url, callBackRegex = "https://anime.girigirilove.com/addons/dp/player/index.php?.*", "utf-8", networkHelper.defaultLinuxUA, null, null, 20000L) 38 | Log.i("GGLPlayComponent", doc) 39 | val jsoup = Jsoup.parse(doc) 40 | val src = jsoup.select("tbody td iframe").first()?.attr("src")?:"" 41 | val u = src.split("?").last().split("&").find { 42 | it.startsWith("url=") 43 | }?.let { 44 | it.subSequence(4, it.length) 45 | }?.toString() ?:"" 46 | if(u.isEmpty()){ 47 | throw ParserException("url 解析失败") 48 | } 49 | PlayerInfo( 50 | decodeType = if(u.endsWith("m3u8")) PlayerInfo.DECODE_TYPE_HLS else PlayerInfo.DECODE_TYPE_OTHER, 51 | uri = u 52 | ) 53 | } 54 | } 55 | 56 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/com/heyanle/easybangumi_extension/ggl/GGLSource.kt: -------------------------------------------------------------------------------- 1 | package com.heyanle.easybangumi_extension.ggl 2 | 3 | import com.heyanle.easybangumi4.source_api.Source 4 | import com.heyanle.extension_api.ExtensionIconSource 5 | import com.heyanle.extension_api.ExtensionSource 6 | import org.easybangumi.extension.R 7 | import kotlin.reflect.KClass 8 | 9 | /** 10 | * Created by heyanle on 2024/1/28. 11 | * https://github.com/heyanLE 12 | */ 13 | class GGLSource : ExtensionSource(), ExtensionIconSource { 14 | 15 | override val describe: String? 16 | get() = "girigirilove" 17 | override val label: String 18 | get() = "girigirilove" 19 | override val version: String 20 | get() = "3.0" 21 | override val versionCode: Int 22 | get() = 3 23 | 24 | override fun register(): List> { 25 | return listOf( 26 | GGLComponent::class, 27 | GGLListComponent::class, 28 | GGLPlayComponent::class, 29 | GGLDetailedComponent::class 30 | ) 31 | } 32 | 33 | override fun getIconResourcesId(): Int? { 34 | return R.drawable.ggl 35 | } 36 | 37 | override val sourceKey: String 38 | get() = "heyanle_girigirilove" 39 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/easybangumiorg/source/aio/AIOHtpHelper.kt: -------------------------------------------------------------------------------- 1 | package io.github.easybangumiorg.source.aio 2 | 3 | /** 4 | * Created by heyanle on 2024/1/28. 5 | * https://github.com/heyanLE 6 | */ 7 | class AIOHtpHelper { 8 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/easybangumiorg/source/aio/OkHttp.kt: -------------------------------------------------------------------------------- 1 | package io.github.easybangumiorg.source.aio 2 | 3 | import android.util.Log 4 | import kotlinx.serialization.json.Json 5 | import okhttp3.OkHttpClient 6 | import okhttp3.Request 7 | import okhttp3.Response 8 | import org.jsoup.Jsoup 9 | import org.jsoup.nodes.Document 10 | import java.nio.charset.Charset 11 | 12 | val json = Json { 13 | ignoreUnknownKeys = true 14 | } 15 | 16 | 17 | val commonHttpClient = OkHttpClient.Builder() 18 | .addInterceptor { 19 | val req = it.request() 20 | Log.d("CommonOkHttpClient", req.url.toString()) 21 | val builder = req.newBuilder() 22 | if (req.header("user-agent")?.isNotEmpty() != true) { 23 | builder.header( 24 | "user-agent", 25 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" 26 | ) 27 | } 28 | it.proceed(builder.build()) 29 | } 30 | .build() 31 | 32 | fun OkHttpClient.newRequest(block: Request.Builder.() -> Unit): Response { 33 | val req = Request.Builder() 34 | .apply(block) 35 | .build() 36 | return newCall(req).execute() 37 | } 38 | 39 | fun OkHttpClient.newGetRequest(block: Request.Builder.() -> Unit): Response { 40 | return newRequest { 41 | block() 42 | get() 43 | } 44 | } 45 | 46 | fun Response.asDocument(): Document { 47 | return Jsoup.parse(this.bodyString()).apply { 48 | setBaseUri(request.url.toString()) 49 | } 50 | } 51 | 52 | fun Response.bodyString(charset: Charset = Charsets.UTF_8): String { 53 | if (code != 200) { 54 | throw RuntimeException("请求${request.url}失败,code: $code") 55 | } 56 | return this.use { 57 | val body = it.body ?: throw RuntimeException("请求${request.url}失败,响应体为空") 58 | body.string() 59 | } 60 | } 61 | 62 | inline fun Response.readJson(): T { 63 | return json.decodeFromString(this.bodyString()) 64 | } 65 | 66 | -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/easybangumiorg/source/aio/SourceResult.kt: -------------------------------------------------------------------------------- 1 | package io.github.easybangumiorg.source.aio 2 | 3 | 4 | 5 | import com.heyanle.easybangumi4.source_api.SourceResult 6 | import com.heyanle.easybangumi4.source_api.withResult 7 | import kotlinx.coroutines.Dispatchers 8 | 9 | fun SourceResult.map(convert: (T) -> R): SourceResult { 10 | return when (this) { 11 | is SourceResult.Complete -> SourceResult.Complete(convert(data)) 12 | is SourceResult.Error -> SourceResult.Error(throwable, isParserError) 13 | } 14 | } 15 | 16 | suspend fun withIoResult(block: suspend () -> T) = withResult(Dispatchers.IO, block) -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/easybangumiorg/source/aio/String.kt: -------------------------------------------------------------------------------- 1 | package io.github.easybangumiorg.source.aio 2 | 3 | import java.net.URLDecoder 4 | import java.net.URLEncoder 5 | 6 | fun String.encodeUri(): String = URLEncoder.encode(this, "UTF-8") 7 | 8 | fun String.decodeUri(): String = URLDecoder.decode(this, "UTF-8") 9 | -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/easybangumiorg/source/aio/auete/AueteDetail.kt: -------------------------------------------------------------------------------- 1 | package io.github.easybangumiorg.source.aio.auete 2 | 3 | import com.heyanle.easybangumi4.source_api.SourceResult 4 | import com.heyanle.easybangumi4.source_api.component.ComponentWrapper 5 | import com.heyanle.easybangumi4.source_api.component.detailed.DetailedComponent 6 | import com.heyanle.easybangumi4.source_api.entity.Cartoon 7 | import com.heyanle.easybangumi4.source_api.entity.CartoonImpl 8 | import com.heyanle.easybangumi4.source_api.entity.CartoonSummary 9 | import com.heyanle.easybangumi4.source_api.entity.Episode 10 | import com.heyanle.easybangumi4.source_api.entity.PlayLine 11 | import io.github.easybangumiorg.source.aio.asDocument 12 | import io.github.easybangumiorg.source.aio.commonHttpClient 13 | import io.github.easybangumiorg.source.aio.map 14 | import io.github.easybangumiorg.source.aio.newGetRequest 15 | import io.github.easybangumiorg.source.aio.withIoResult 16 | 17 | class AueteDetail : ComponentWrapper(), DetailedComponent { 18 | override suspend fun getAll(summary: CartoonSummary): SourceResult>> = 19 | withIoResult { 20 | val doc = commonHttpClient.newGetRequest { 21 | url("$AueteBaseUrl/${summary.id}/") 22 | }.asDocument() 23 | val container = doc.selectFirst(".main .card-thread > .card-body")!! 24 | val img = container.selectFirst(".cover img")?.absUrl("src") ?: "" 25 | val title = 26 | container.selectFirst(".media > .media-body > .title")!!.text().trim().let { str -> 27 | val idx = str.indexOf('《') 28 | if (idx == -1) { 29 | str 30 | } else { 31 | var end = str.length 32 | for (i in (idx + 1) until str.length) { 33 | if (str[i] == '》') { 34 | end = i 35 | break 36 | } 37 | } 38 | str.substring(idx + 1, end) 39 | } 40 | } 41 | val infoList = mutableListOf() 42 | val infoEls = container.select(".message > p") 43 | var desc = "" 44 | for ((index, p) in infoEls.withIndex()) { 45 | val text = p.text().trim() 46 | if (text.contains("简介")) { 47 | if (index + 1 < infoEls.size) { 48 | desc = infoEls[index + 1].text().trim() 49 | } 50 | break 51 | } 52 | infoList.add(text) 53 | 54 | } 55 | 56 | val playlists = container.select("[id=player_list]").map { playlistContainer -> 57 | var name = playlistContainer.selectFirst(".title")!!.text().trim() 58 | name.indexOf('』').let { 59 | if (it >= 0) { 60 | name = name.substring(it + 1) 61 | } 62 | } 63 | name.indexOf(':').let { 64 | if (it >= 0) { 65 | name = name.substring(0, it) 66 | } 67 | } 68 | val episodes = playlistContainer.select("ul > li > a").map { epEl -> 69 | val id = epEl.attr("href").let { 70 | it.substring(it.lastIndexOf('/') + 1, it.lastIndexOf('.')) 71 | } 72 | Episode(label = epEl.text().trim(), id = id, order = 0) 73 | } 74 | PlayLine(id = name, label = name, episode = arrayListOf(*episodes.toTypedArray())) 75 | } 76 | val cartoon = CartoonImpl( 77 | id = summary.id, 78 | source = source.key, 79 | url = "$AueteBaseUrl/${summary.id}/", 80 | title = title, 81 | coverUrl = img, 82 | description = desc, 83 | intro = infoList.joinToString(separator = "\n") 84 | ) 85 | cartoon to playlists 86 | } 87 | 88 | override suspend fun getDetailed(summary: CartoonSummary): SourceResult = 89 | getAll(summary).map { it.first } 90 | 91 | override suspend fun getPlayLine(summary: CartoonSummary): SourceResult> = 92 | getAll(summary).map { it.second } 93 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/easybangumiorg/source/aio/auete/AuetePage.kt: -------------------------------------------------------------------------------- 1 | package io.github.easybangumiorg.source.aio.auete 2 | 3 | import com.heyanle.easybangumi4.source_api.component.ComponentWrapper 4 | import com.heyanle.easybangumi4.source_api.component.page.PageComponent 5 | import com.heyanle.easybangumi4.source_api.component.page.SourcePage 6 | import com.heyanle.easybangumi4.source_api.entity.CartoonCover 7 | import com.heyanle.easybangumi4.source_api.entity.CartoonCoverImpl 8 | import com.heyanle.easybangumi4.source_api.withResult 9 | import io.github.easybangumiorg.source.aio.asDocument 10 | import io.github.easybangumiorg.source.aio.commonHttpClient 11 | import io.github.easybangumiorg.source.aio.newGetRequest 12 | import io.github.easybangumiorg.source.aio.withIoResult 13 | 14 | class AuetePage : ComponentWrapper(), PageComponent { 15 | override fun getPages(): List { 16 | val home = SourcePage.Group(label = "首页", newScreen = false) { 17 | withIoResult { 18 | val doc = commonHttpClient.newGetRequest { 19 | url(AueteBaseUrl) 20 | }.asDocument() 21 | val areas: MutableList = 22 | doc.select(".container > .row > .main .card").map { area -> 23 | val areaName = area.firstElementChild()!!.selectFirst("a")!!.text().trim() 24 | val videos = 25 | area.select(".threadlist > li").map { it.parseAueteVideo(source.key) } 26 | SourcePage.SingleCartoonPage.WithCover(label = areaName, firstKey = { 0 }) { 27 | withResult { 28 | null to videos 29 | } 30 | } 31 | }.toMutableList() 32 | doc.selectFirst(".card-site-info") 33 | ?.parent() 34 | ?.children() 35 | ?.asSequence() 36 | ?.forEach { 37 | val title = it.selectFirst(".card-header")?.text()?.trim() ?: return@forEach 38 | val videos = it.select(".card-body > .list-ul > li > a").map { videoEl -> 39 | CartoonCoverImpl( 40 | id = videoEl.attr("href").trim('/'), 41 | source = source.key, 42 | url = videoEl.absUrl("href"), 43 | title = videoEl.text().trim() 44 | ) 45 | } 46 | if (videos.isEmpty()) { 47 | return@forEach 48 | } 49 | SourcePage.SingleCartoonPage.WithoutCover(label = title, firstKey = { 0 }) { 50 | withResult { 51 | null to videos 52 | } 53 | }.let { page -> areas.add(page) } 54 | } 55 | 56 | areas 57 | } 58 | } 59 | val others = getVideoCategories().map { (pageLabel, pageKey, subTypes) -> 60 | SourcePage.Group(label = pageLabel, newScreen = false) { 61 | withResult { 62 | subTypes.map { (typeName, typeKey) -> 63 | SourcePage.SingleCartoonPage.WithCover( 64 | label = typeName, 65 | firstKey = { 1 }) { page -> 66 | withIoResult { 67 | getVideoOfCategoryAndType( 68 | category = pageKey, 69 | type = typeKey, 70 | page = page 71 | ) 72 | } 73 | } 74 | } 75 | } 76 | } 77 | } 78 | return listOf(home) + others 79 | } 80 | 81 | private fun getVideoOfCategoryAndType( 82 | category: String, 83 | type: String, 84 | page: Int 85 | ): Pair> { 86 | val urlBuilder = StringBuilder(AueteBaseUrl) 87 | .append('/') 88 | .append(category) 89 | if (type.isNotEmpty()) { 90 | urlBuilder.append('/') 91 | .append(type) 92 | } 93 | urlBuilder.append("/index") 94 | if (page > 1) { 95 | urlBuilder.append(page) 96 | } 97 | urlBuilder.append(".html") 98 | val doc = commonHttpClient.newGetRequest { 99 | url(urlBuilder.toString()) 100 | }.asDocument() 101 | val videos = doc.select(".threadlist > li").map { it.parseAueteVideo(source = source.key) } 102 | val nextPage = if (doc.aueteHaveNextPage()) page + 1 else null 103 | return nextPage to videos 104 | 105 | } 106 | 107 | private fun getVideoCategories() = listOf( 108 | Triple( 109 | "电影", 110 | "Movie", 111 | listOf( 112 | "全部" to "", 113 | "喜剧片" to "xjp", 114 | "动作片" to "dzp", 115 | "爱情片" to "aqp", 116 | "科幻片" to "khp", 117 | "恐怖片" to "kbp", 118 | "惊悚片" to "jsp", 119 | "战争片" to "zzp", 120 | "剧情片" to "jqp" 121 | ) 122 | ), 123 | Triple( 124 | "电视剧", 125 | "Tv", 126 | listOf( 127 | "全部" to "", 128 | "美剧" to "oumei", 129 | "韩剧" to "hanju", 130 | "日剧" to "riju", 131 | "泰剧" to "yataiju", 132 | "网剧" to "wangju", 133 | "台剧" to "taiju", 134 | "国产" to "neidi", 135 | "港剧" to "tvbgj", 136 | "英剧" to "yingju", 137 | "外剧" to "waiju" 138 | ) 139 | ), 140 | Triple( 141 | "综艺", 142 | "Zy", 143 | listOf( 144 | "全部" to "", 145 | "国综" to "guozong", 146 | "韩综" to "hanzong", 147 | "美综" to "meizong" 148 | ) 149 | ), 150 | Triple( 151 | "动漫", 152 | "Dm", 153 | listOf( 154 | "全部" to "", 155 | "动画" to "donghua", 156 | "日漫" to "riman", 157 | "国漫" to "guoman", 158 | "美漫" to "meiman" 159 | ) 160 | ), 161 | Triple( 162 | "其他", 163 | "qita", 164 | listOf( 165 | "全部" to "", 166 | "记录片" to "Jlp", 167 | "经典片" to "Jdp", 168 | "经典剧" to "Jdj", 169 | "网大电影" to "wlp", 170 | "国产老电影" to "laodianying" 171 | ) 172 | ) 173 | ) 174 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/easybangumiorg/source/aio/auete/AuetePlay.kt: -------------------------------------------------------------------------------- 1 | package io.github.easybangumiorg.source.aio.auete 2 | 3 | import com.heyanle.easybangumi4.source_api.ParserException 4 | import com.heyanle.easybangumi4.source_api.SourceResult 5 | import com.heyanle.easybangumi4.source_api.component.ComponentWrapper 6 | import com.heyanle.easybangumi4.source_api.component.play.PlayComponent 7 | import com.heyanle.easybangumi4.source_api.entity.CartoonSummary 8 | import com.heyanle.easybangumi4.source_api.entity.Episode 9 | import com.heyanle.easybangumi4.source_api.entity.PlayLine 10 | import com.heyanle.easybangumi4.source_api.entity.PlayerInfo 11 | import io.github.easybangumiorg.source.aio.bodyString 12 | import io.github.easybangumiorg.source.aio.commonHttpClient 13 | import io.github.easybangumiorg.source.aio.newGetRequest 14 | import io.github.easybangumiorg.source.aio.withIoResult 15 | import kotlin.io.encoding.ExperimentalEncodingApi 16 | 17 | class AuetePlay : ComponentWrapper(), PlayComponent { 18 | @OptIn(ExperimentalEncodingApi::class) 19 | override suspend fun getPlayInfo( 20 | summary: CartoonSummary, 21 | playLine: PlayLine, 22 | episode: Episode 23 | ): SourceResult = withIoResult { 24 | val html = commonHttpClient.newGetRequest { 25 | url("$AueteBaseUrl/${summary.id}/${episode.id}.html") 26 | }.bodyString() 27 | val idx = html.indexOf(" now") 28 | var start = -1 29 | if (idx > 0) { 30 | for (i in (idx + 4) until html.length) { 31 | val c = html[i] 32 | if (c == '"') { 33 | if (start == -1) { 34 | start = i 35 | } else { 36 | val videoUrl = if (html.substring(idx, start).contains("base64")) { 37 | kotlin.io.encoding.Base64.decode(html.substring(start + 1, i)) 38 | .toString(Charsets.UTF_8) 39 | } else { 40 | html.substring(start + 1, i) 41 | } 42 | return@withIoResult PlayerInfo( 43 | uri = videoUrl, 44 | decodeType = PlayerInfo.DECODE_TYPE_HLS 45 | ) 46 | } 47 | } 48 | } 49 | } 50 | throw ParserException("未获取到视频链接") 51 | } 52 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/easybangumiorg/source/aio/auete/AueteSearch.kt: -------------------------------------------------------------------------------- 1 | package io.github.easybangumiorg.source.aio.auete 2 | 3 | //import com.heyanle.easybangumi4.source_api.SourceResult 4 | import com.heyanle.easybangumi4.source_api.SourceResult 5 | import com.heyanle.easybangumi4.source_api.component.ComponentWrapper 6 | import com.heyanle.easybangumi4.source_api.component.search.SearchComponent 7 | import com.heyanle.easybangumi4.source_api.entity.CartoonCover 8 | import com.heyanle.easybangumi4.source_api.entity.CartoonCoverImpl 9 | import io.github.easybangumiorg.source.aio.asDocument 10 | import io.github.easybangumiorg.source.aio.commonHttpClient 11 | import io.github.easybangumiorg.source.aio.encodeUri 12 | import io.github.easybangumiorg.source.aio.newGetRequest 13 | import io.github.easybangumiorg.source.aio.withIoResult 14 | 15 | class AueteSearch : ComponentWrapper(), SearchComponent { 16 | override fun getFirstSearchKey(keyword: String): Int = 1 17 | 18 | override suspend fun search( 19 | pageKey: Int, 20 | keyword: String 21 | ): SourceResult>> = withIoResult { 22 | val doc = commonHttpClient.newGetRequest { 23 | url("$AueteBaseUrl/auete3so.php?searchword=${keyword.encodeUri()}") 24 | }.asDocument() 25 | val videos = doc.getElementsByClass("threadlist").map { el -> 26 | val linkEl = el.selectFirst("a")!! 27 | CartoonCoverImpl( 28 | id = linkEl.attr("href").trim('/'), 29 | url = linkEl.absUrl("href"), 30 | title = linkEl.text().trim(), 31 | source = source.key, 32 | ) 33 | } 34 | null to videos 35 | } 36 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/easybangumiorg/source/aio/auete/AueteSource.kt: -------------------------------------------------------------------------------- 1 | package io.github.easybangumiorg.source.aio.auete 2 | 3 | import com.heyanle.easybangumi4.source_api.Source 4 | import com.heyanle.extension_api.ExtensionIconSource 5 | import org.easybangumi.extension.R 6 | import kotlin.reflect.KClass 7 | 8 | class AueteSource : Source, ExtensionIconSource { 9 | override val describe: String 10 | get() = label 11 | override val key: String 12 | get() = "auete" 13 | override val label: String 14 | get() = "Auete影视" 15 | override val version: String 16 | get() = "2.0" 17 | override val versionCode: Int 18 | get() = 4 19 | 20 | override fun getIconResourcesId(): Int = R.drawable.auete 21 | 22 | override fun register(): List> = listOf( 23 | AuetePage::class, 24 | AueteDetail::class, 25 | AueteSearch::class, 26 | AuetePlay::class 27 | ) 28 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/easybangumiorg/source/aio/auete/Common.kt: -------------------------------------------------------------------------------- 1 | package io.github.easybangumiorg.source.aio.auete 2 | 3 | import com.heyanle.easybangumi4.source_api.entity.CartoonCover 4 | import com.heyanle.easybangumi4.source_api.entity.CartoonCoverImpl 5 | import org.jsoup.nodes.Document 6 | import org.jsoup.nodes.Element 7 | 8 | const val AueteBaseUrl = "https://auete.pro" 9 | 10 | fun Element.parseAueteVideo(source: String): CartoonCover { 11 | val linkEl = selectFirst("a")!! 12 | val id = linkEl.attr("href").trim('/') 13 | val image = selectFirst("img")?.absUrl("src") ?: "" 14 | val episode = selectFirst(".hdtag")?.text()?.trim() 15 | val title = selectFirst(".title")!!.text().trim() 16 | return CartoonCoverImpl( 17 | id = id, 18 | source = source, 19 | url = linkEl.absUrl("href"), 20 | title = title, 21 | coverUrl = image, 22 | intro = episode 23 | ) 24 | } 25 | 26 | fun Document.aueteHaveNextPage(): Boolean { 27 | val pageItems = select(".pagination > li") 28 | if (pageItems.isEmpty()) { 29 | return false 30 | } 31 | return pageItems.indexOfLast { it.hasClass("active") } < pageItems.size - 3 32 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/easybangumiorg/source/aio/changzhang/ChangZhangDetailPage.kt: -------------------------------------------------------------------------------- 1 | package io.github.easybangumiorg.source.aio.changzhang 2 | 3 | import com.heyanle.easybangumi4.source_api.SourceResult 4 | import com.heyanle.easybangumi4.source_api.component.ComponentWrapper 5 | import com.heyanle.easybangumi4.source_api.component.detailed.DetailedComponent 6 | import com.heyanle.easybangumi4.source_api.entity.Cartoon 7 | import com.heyanle.easybangumi4.source_api.entity.CartoonImpl 8 | import com.heyanle.easybangumi4.source_api.entity.CartoonSummary 9 | import com.heyanle.easybangumi4.source_api.entity.Episode 10 | import com.heyanle.easybangumi4.source_api.entity.PlayLine 11 | import com.heyanle.easybangumi4.source_api.utils.api.OkhttpHelper 12 | import io.github.easybangumiorg.source.aio.asDocument 13 | import io.github.easybangumiorg.source.aio.map 14 | import io.github.easybangumiorg.source.aio.newGetRequest 15 | import io.github.easybangumiorg.source.aio.withIoResult 16 | 17 | class ChangZhangDetailPage(private val okhttpHelper: OkhttpHelper):DetailedComponent,ComponentWrapper() { 18 | override suspend fun getAll(summary: CartoonSummary): SourceResult>> = 19 | withIoResult{ 20 | val detailPageUrl = "${ChangZhangSource.BASE_URL}/movie/${summary.id}.html" 21 | val doc = okhttpHelper.cloudflareClient.newGetRequest { 22 | url(detailPageUrl) 23 | }.asDocument() 24 | val episodes = doc.select(".paly_list_btn > a").map { epEl -> 25 | val url = epEl.attr("href") 26 | Episode( 27 | id = url.substring(url.lastIndexOf('/') + 1, url.lastIndexOf('.')), 28 | label = epEl.text().trim(), 29 | order = 0 30 | ) 31 | } 32 | val container = doc.selectFirst(".dyxingq")!! 33 | val image = container.selectFirst(".dyimg > img")!!.let { 34 | it.dataset()["original"] ?: it.attr("src") 35 | } 36 | val title = container.selectFirst(".dytext > .moviedteail_tt > h1")!!.text() 37 | val infoList = container.select(".dytext > .moviedteail_list > li").map { it.text().trim() } 38 | val desc = doc.selectFirst(".yp_context")?.text()?.trim() 39 | CartoonImpl( 40 | id = summary.id, 41 | source = source.key, 42 | url = detailPageUrl, 43 | title = title, 44 | intro = infoList.joinToString(separator = "\n"), 45 | coverUrl = image, 46 | description = desc 47 | ) to listOf(PlayLine(id = "1", label = "播放列表", episode = arrayListOf(*episodes.toTypedArray()))) 48 | } 49 | 50 | override suspend fun getDetailed(summary: CartoonSummary): SourceResult = getAll(summary).map { it.first } 51 | 52 | override suspend fun getPlayLine(summary: CartoonSummary): SourceResult> = getAll(summary).map { it.second } 53 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/easybangumiorg/source/aio/changzhang/ChangZhangPage.kt: -------------------------------------------------------------------------------- 1 | package io.github.easybangumiorg.source.aio.changzhang 2 | 3 | import com.heyanle.easybangumi4.source_api.SourceResult 4 | import com.heyanle.easybangumi4.source_api.component.ComponentWrapper 5 | import com.heyanle.easybangumi4.source_api.component.page.PageComponent 6 | import com.heyanle.easybangumi4.source_api.component.page.SourcePage 7 | import com.heyanle.easybangumi4.source_api.entity.CartoonCover 8 | import com.heyanle.easybangumi4.source_api.utils.api.OkhttpHelper 9 | import io.github.easybangumiorg.source.aio.asDocument 10 | import io.github.easybangumiorg.source.aio.changzhang.ChangZhangSource.Companion.hasNextPage 11 | import io.github.easybangumiorg.source.aio.changzhang.ChangZhangSource.Companion.parseAnime 12 | import io.github.easybangumiorg.source.aio.newGetRequest 13 | import io.github.easybangumiorg.source.aio.withIoResult 14 | 15 | class ChangZhangPage(private val okhttpHelper: OkhttpHelper):PageComponent,ComponentWrapper() { 16 | override fun getPages(): List { 17 | val homePage = SourcePage.Group("首页",newScreen = false){ 18 | withIoResult { 19 | val doc = okhttpHelper.cloudflareClient.newGetRequest { url(ChangZhangSource.BASE_URL) }.asDocument() 20 | doc.getElementsByClass("mi_btcon").asSequence().map { groupEl -> 21 | val name = groupEl.selectFirst(".bt_tit a")?.text()?.trim()?:"推荐" 22 | val videos = groupEl.select(".bt_img > ul > li").map { it.parseAnime(source = source.key) } 23 | name to videos 24 | } 25 | .filter { it.second.isNotEmpty() } 26 | .map { (label,videos)-> 27 | SourcePage.SingleCartoonPage.WithCover(label = label, firstKey = {0}){ 28 | SourceResult.Complete(null to videos) 29 | } 30 | } 31 | .toList() 32 | } 33 | } 34 | 35 | val movie = SourcePage.Group("电影",newScreen = false){ 36 | withIoResult { 37 | val doc = okhttpHelper.cloudflareClient.newGetRequest { url("${ChangZhangSource.BASE_URL}/movie_bt") }.asDocument() 38 | doc.select("#beautiful-taxonomy-filters-tax-movie_bt_tags > a").asSequence() 39 | .map { 40 | it.text() to (it.attr("cat-url").takeIf { it.isNotEmpty() }?: it.attr("href")) 41 | } 42 | .map { (label,url)-> 43 | SourcePage.SingleCartoonPage.WithCover(label = label, firstKey = {1}){page-> 44 | withIoResult { fetchVideoCategoryPage(url,page) } 45 | } 46 | } 47 | .toList() 48 | } 49 | } 50 | val otherPages = listOf( 51 | "美剧" to "meijutt", 52 | "日剧" to "riju", 53 | "韩剧" to "hanjutv", 54 | "番剧" to "fanju", 55 | "电视剧" to "dsj", 56 | "国产剧" to "gcj", 57 | "剧场版" to "dongmanjuchangban", 58 | "海外剧" to "haiwaijuqita", 59 | "热映中" to "benyueremen" 60 | ).map { (label,urlSuffix)-> 61 | SourcePage.SingleCartoonPage.WithCover(label = label, firstKey = {1}){page-> 62 | withIoResult { 63 | fetchVideoCategoryPage("${ChangZhangSource.BASE_URL}/$urlSuffix",page) 64 | } 65 | } 66 | } 67 | return listOf(homePage,movie)+otherPages 68 | } 69 | 70 | private fun fetchVideoCategoryPage(url:String,page:Int): Pair> { 71 | val actualUrl = if (page>1) "$url/page/$page" else url 72 | val doc = okhttpHelper.cloudflareClient.newGetRequest { url(actualUrl) }.asDocument() 73 | val videos = doc.select(".bt_img > ul > li").map { it.parseAnime(source = source.key) } 74 | val next = if (doc.hasNextPage()) page +1 else null 75 | return next to videos 76 | } 77 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/easybangumiorg/source/aio/changzhang/ChangZhangSearchPage.kt: -------------------------------------------------------------------------------- 1 | package io.github.easybangumiorg.source.aio.changzhang 2 | 3 | import com.heyanle.easybangumi4.source_api.SourceResult 4 | import com.heyanle.easybangumi4.source_api.component.ComponentWrapper 5 | import com.heyanle.easybangumi4.source_api.component.search.SearchComponent 6 | import com.heyanle.easybangumi4.source_api.entity.CartoonCover 7 | import com.heyanle.easybangumi4.source_api.utils.api.OkhttpHelper 8 | import io.github.easybangumiorg.source.aio.asDocument 9 | import io.github.easybangumiorg.source.aio.changzhang.ChangZhangSource.Companion.hasNextPage 10 | import io.github.easybangumiorg.source.aio.changzhang.ChangZhangSource.Companion.parseAnime 11 | import io.github.easybangumiorg.source.aio.encodeUri 12 | import io.github.easybangumiorg.source.aio.newGetRequest 13 | import io.github.easybangumiorg.source.aio.newRequest 14 | import io.github.easybangumiorg.source.aio.withIoResult 15 | import okhttp3.Cookie 16 | import okhttp3.FormBody 17 | 18 | class ChangZhangSearchPage(private val okhttpHelper: OkhttpHelper):SearchComponent,ComponentWrapper() { 19 | 20 | var searchUrl:String? = null 21 | 22 | override fun getFirstSearchKey(keyword: String): Int { 23 | return 1 24 | } 25 | 26 | override suspend fun search( 27 | pageKey: Int, 28 | keyword: String 29 | ): SourceResult>> = withIoResult{ 30 | if (searchUrl == null) { 31 | searchUrl = okhttpHelper.client.newGetRequest { 32 | url(ChangZhangSource.BASE_URL) 33 | } 34 | .asDocument() 35 | .selectFirst(".w-search-form") 36 | ?.attr("action") 37 | ?: throw RuntimeException("未获取到搜索视频链接") 38 | } 39 | val doc = okhttpHelper.cloudflareClient.newGetRequest { 40 | url("$searchUrl?q=${keyword.encodeUri()}&f=_all&p=$pageKey") 41 | }.use { resp -> 42 | val respDoc = resp.asDocument() 43 | if (respDoc.title().contains("人机验证")) { 44 | val value = Cookie.parseAll(resp.request.url, resp.headers) 45 | .lastOrNull { it.name == "result" }?.value 46 | ?: throw RuntimeException("Cookie中无验证码") 47 | okhttpHelper.cloudflareClient.newRequest { 48 | url(resp.request.url) 49 | post(FormBody.Builder().add("result", value).build()) 50 | } 51 | okhttpHelper.cloudflareClient.newGetRequest { url(resp.request.url) }.asDocument() 52 | } else { 53 | respDoc 54 | } 55 | } 56 | val videos = doc.select(".search_list > ul > li").map { it.parseAnime(source = source.key) } 57 | val nextPage = if (doc.hasNextPage()) pageKey + 1 else null 58 | nextPage to videos 59 | } 60 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/easybangumiorg/source/aio/changzhang/ChangZhangSource.kt: -------------------------------------------------------------------------------- 1 | package io.github.easybangumiorg.source.aio.changzhang 2 | 3 | import com.heyanle.easybangumi4.source_api.entity.CartoonCover 4 | import com.heyanle.easybangumi4.source_api.entity.CartoonCoverImpl 5 | import com.heyanle.extension_api.ExtensionIconSource 6 | import com.heyanle.extension_api.ExtensionSource 7 | import org.easybangumi.extension.R 8 | import org.jsoup.nodes.Document 9 | import org.jsoup.nodes.Element 10 | import kotlin.reflect.KClass 11 | 12 | class ChangZhangSource:ExtensionSource(),ExtensionIconSource { 13 | override val describe: String 14 | get() = "厂长资源" 15 | override val label: String 16 | get() = describe 17 | override val sourceKey: String 18 | get() = "changzhang" 19 | override val version: String 20 | get() = "1.0" 21 | override val versionCode: Int 22 | get() = 1 23 | 24 | override fun getIconResourcesId(): Int= R.drawable.changzhang 25 | 26 | override fun register(): List> = listOf( 27 | ChangZhangPage::class, 28 | ChangZhangDetailPage::class, 29 | ChangZhangSearchPage::class, 30 | ChangZhangPlayPage::class 31 | ) 32 | 33 | companion object { 34 | const val BASE_URL = "https://www.czzy88.com" 35 | 36 | fun Element.parseAnime(source:String): CartoonCover { 37 | val url = selectFirst("a")!!.absUrl("href") 38 | val image = selectFirst("img")!!.let { 39 | it.dataset()["original"] ?: it.attr("src") 40 | } 41 | val ep = selectFirst(".jidi")?.text()?.trim() 42 | val title = selectFirst(".dytit")!!.text().trim() 43 | return CartoonCoverImpl( 44 | id = url.substring(url.lastIndexOf('/') + 1, url.lastIndexOf('.')), 45 | url = url, 46 | title = title, 47 | source = source, 48 | intro = ep, 49 | coverUrl = image 50 | ) 51 | } 52 | 53 | fun Document.hasNextPage(): Boolean { 54 | val pageItems = select(".pagenavi_txt > a") 55 | if (pageItems.isEmpty()) { 56 | return false 57 | } 58 | return !pageItems.last()!!.hasClass("current") 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/easybangumiorg/source/aio/fengche/Common.kt: -------------------------------------------------------------------------------- 1 | package io.github.easybangumiorg.source.aio.fengche 2 | 3 | import android.util.Log 4 | import com.heyanle.easybangumi4.source_api.entity.CartoonCover 5 | import com.heyanle.easybangumi4.source_api.entity.CartoonCoverImpl 6 | import io.github.easybangumiorg.source.aio.asDocument 7 | import io.github.easybangumiorg.source.aio.commonHttpClient 8 | import io.github.easybangumiorg.source.aio.newGetRequest 9 | import org.jsoup.nodes.Document 10 | import org.jsoup.nodes.Element 11 | import java.util.concurrent.locks.ReentrantLock 12 | import kotlin.concurrent.withLock 13 | 14 | 15 | //private val urlPageUrl = "https://wedm.cc/" 16 | // 17 | //private val baseUrlFallback = "https://www.886dm.tv" 18 | // 19 | //@Volatile 20 | //private var _fengCheBaseUrl: String? = null 21 | // 22 | //val FengCheBaseUrl: String 23 | // get() = requireFengCheBaseUrl() 24 | // 25 | //private val baseUrlLock = ReentrantLock() 26 | // 27 | //private fun requireFengCheBaseUrl(): String { 28 | // if (_fengCheBaseUrl == null) { 29 | // baseUrlLock.withLock { 30 | // if (_fengCheBaseUrl == null) { 31 | // _fengCheBaseUrl = runCatching { 32 | // requestFengCheBaseUrl() 33 | // }.onFailure { 34 | // Log.e("FengCheSourceCommon", "requireFengCheBaseUrl:${it.message}", it) 35 | // } 36 | // .getOrNull() ?: baseUrlFallback 37 | // } 38 | // } 39 | // } 40 | // return _fengCheBaseUrl!! 41 | //} 42 | // 43 | //private fun requestFengCheBaseUrl(): String { 44 | // val doc = commonHttpClient.newGetRequest { 45 | // url(urlPageUrl) 46 | // }.asDocument() 47 | // 48 | // val website = doc.selectFirst(".main .speedlist li a i")?.text()?.trim()?.let { text -> 49 | // text 50 | // } 51 | // return if (website?.isNotBlank() == true) { 52 | // "https://$website" 53 | // } else { 54 | // baseUrlFallback 55 | // } 56 | //} 57 | 58 | 59 | fun String.extractFengCheIdFromUrl() = 60 | this.substring(this.lastIndexOf('/') + 1, this.lastIndexOf('.')) 61 | 62 | 63 | fun Document.hasNextPage(): Boolean = 64 | this.getElementById("aapages") 65 | ?.children() 66 | ?.findLast { it.hasClass("pagenow") } 67 | ?.nextElementSibling() 68 | ?.tagName() === "a" 69 | 70 | fun Element.parseFengCheAnime(sourceKey: String): CartoonCover { 71 | val linkEl = selectFirst("a")!! 72 | val url = linkEl.absUrl("href") 73 | val imageUrl = selectFirst(".img_wrapper")?.dataset()?.get("original") 74 | val episode = linkEl.children().last()?.text()?.trim() ?: "" 75 | val title = linkEl.attr("title").takeIf { it.isNotEmpty() }?: linkEl.text().trim() 76 | return CartoonCoverImpl( 77 | id = url.extractFengCheIdFromUrl(), 78 | url = url, 79 | title = title, 80 | source = sourceKey, 81 | coverUrl = imageUrl, 82 | intro = episode 83 | ) 84 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/easybangumiorg/source/aio/fengche/FengCheDetail.kt: -------------------------------------------------------------------------------- 1 | package io.github.easybangumiorg.source.aio.fengche 2 | 3 | import com.heyanle.easybangumi4.source_api.SourceResult 4 | import com.heyanle.easybangumi4.source_api.component.ComponentWrapper 5 | import com.heyanle.easybangumi4.source_api.component.detailed.DetailedComponent 6 | import com.heyanle.easybangumi4.source_api.entity.Cartoon 7 | import com.heyanle.easybangumi4.source_api.entity.CartoonImpl 8 | import com.heyanle.easybangumi4.source_api.entity.CartoonSummary 9 | import com.heyanle.easybangumi4.source_api.entity.Episode 10 | import com.heyanle.easybangumi4.source_api.entity.PlayLine 11 | import com.heyanle.easybangumi4.source_api.withResult 12 | //import com.heyanle.easybangumi4.source_api.withResult 13 | import io.github.easybangumiorg.source.aio.asDocument 14 | import io.github.easybangumiorg.source.aio.commonHttpClient 15 | import io.github.easybangumiorg.source.aio.map 16 | import io.github.easybangumiorg.source.aio.newGetRequest 17 | import kotlinx.coroutines.Dispatchers 18 | 19 | class FengCheDetail ( 20 | private val hostUrlHelper: FengCheHostUrlHelper 21 | ): ComponentWrapper(), DetailedComponent { 22 | override suspend fun getAll(summary: CartoonSummary): SourceResult>> = 23 | withResult(Dispatchers.IO) { 24 | val document = commonHttpClient.newGetRequest { 25 | url("${hostUrlHelper.fengcheBaseUrl}/video/${summary.id}.html") 26 | }.asDocument() 27 | val imageUrl = 28 | document.selectFirst(".con_c1 .img_wrapper")!!.dataset()["original"]!! 29 | val detailContainer = document.selectFirst(".con_xinxi")!! 30 | val title = detailContainer.selectFirst("a")!!.text().trim() 31 | val intro = detailContainer.selectFirst(".yplx_c1")!!.children() 32 | .find { it.text().contains("类型:") } 33 | ?.children() 34 | ?.lastOrNull() 35 | ?.text() 36 | ?.trim() 37 | ?: "" 38 | val desc = detailContainer.selectFirst(".yplx_c3")?.children() 39 | ?.lastOrNull() 40 | ?.text() 41 | ?.trim() 42 | val playlistNames = document.select(".playlist-tab a").map { it.text().trim() } 43 | val episodesGroup = 44 | document.select(".con_juji_bg > .tab-content > .con_c2_list").asSequence() 45 | .map { epContainer -> 46 | epContainer.select("a").map { 47 | Episode( 48 | id = it.attr("href").extractFengCheIdFromUrl(), 49 | label = it.text().trim(), 50 | order = 1 51 | ) 52 | } 53 | } 54 | .toList() 55 | val playlists = List(playlistNames.size.coerceAtMost(episodesGroup.size)) { 56 | PlayLine( 57 | id = it.toString(), 58 | label = playlistNames[it], 59 | episode = arrayListOf(*episodesGroup[it].toTypedArray()) 60 | ) 61 | } 62 | val cartoon = CartoonImpl( 63 | id = summary.id, 64 | source = source.key, 65 | url = "${hostUrlHelper.fengcheBaseUrl}/video/${summary.id}.html", 66 | title = title, 67 | coverUrl = imageUrl, 68 | description = desc, 69 | intro = intro 70 | ) 71 | cartoon to playlists 72 | } 73 | 74 | override suspend fun getDetailed(summary: CartoonSummary): SourceResult { 75 | return getAll(summary).map { it.first } 76 | } 77 | 78 | override suspend fun getPlayLine(summary: CartoonSummary): SourceResult> { 79 | return getAll(summary).map { it.second } 80 | } 81 | 82 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/easybangumiorg/source/aio/fengche/FengCheHostUrlHelper.kt: -------------------------------------------------------------------------------- 1 | package io.github.easybangumiorg.source.aio.fengche 2 | 3 | import com.heyanle.easybangumi4.source_api.utils.api.PreferenceHelper 4 | import io.github.easybangumiorg.source.aio.asDocument 5 | import io.github.easybangumiorg.source.aio.commonHttpClient 6 | import io.github.easybangumiorg.source.aio.newGetRequest 7 | 8 | /** 9 | * Created by heyanlin on 2024/5/23. 10 | */ 11 | class FengCheHostUrlHelper( 12 | private val preferenceHelper: PreferenceHelper 13 | ) { 14 | 15 | private val autoHostUrl: Boolean 16 | get() = preferenceHelper.get("auto_host_url", "false") == "true" 17 | 18 | private val diyUrl: String 19 | get() = preferenceHelper.get("BaseUrl", "http://www.fcdm9.com/") 20 | 21 | private val urlPageUrl = "https://wedm.cc/" 22 | 23 | private val fengcheBaseUrlAutoUrl: String by lazy { 24 | val doc = commonHttpClient.newGetRequest { 25 | url(urlPageUrl) 26 | }.asDocument() 27 | 28 | val website = doc.selectFirst(".main .speedlist li a i")?.text()?.trim()?.let { text -> 29 | text 30 | } 31 | return@lazy if (website?.isNotBlank() == true) { 32 | "https://$website" 33 | } else { 34 | diyUrl 35 | } 36 | } 37 | 38 | val fengcheBaseUrl: String 39 | get() = (if(autoHostUrl) fengcheBaseUrlAutoUrl else diyUrl).let { 40 | if (it.endsWith("/")){ 41 | it.substring(0, it.length - 1) 42 | }else{ 43 | it 44 | } 45 | } 46 | 47 | 48 | 49 | 50 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/easybangumiorg/source/aio/fengche/FengChePage.kt: -------------------------------------------------------------------------------- 1 | package io.github.easybangumiorg.source.aio.fengche 2 | 3 | import com.heyanle.easybangumi4.source_api.component.ComponentWrapper 4 | import com.heyanle.easybangumi4.source_api.component.page.PageComponent 5 | import com.heyanle.easybangumi4.source_api.component.page.SourcePage 6 | import com.heyanle.easybangumi4.source_api.entity.CartoonCover 7 | import com.heyanle.easybangumi4.source_api.entity.CartoonCoverImpl 8 | import com.heyanle.easybangumi4.source_api.utils.api.OkhttpHelper 9 | import com.heyanle.easybangumi4.source_api.withResult 10 | import io.github.easybangumiorg.source.aio.asDocument 11 | import io.github.easybangumiorg.source.aio.commonHttpClient 12 | import io.github.easybangumiorg.source.aio.encodeUri 13 | import io.github.easybangumiorg.source.aio.newGetRequest 14 | import kotlinx.coroutines.Dispatchers 15 | import kotlinx.coroutines.sync.Mutex 16 | import kotlinx.coroutines.sync.withLock 17 | 18 | class FengChePage( 19 | val okhttpHelper: OkhttpHelper, 20 | private val hostUrlHelper: FengCheHostUrlHelper 21 | ) : ComponentWrapper(), PageComponent { 22 | 23 | private val categoryLock = Mutex() 24 | 25 | private var categories: List? = null 26 | 27 | override fun getPages(): List { 28 | val home = SourcePage.Group("首页", false) { 29 | withResult(Dispatchers.IO) { 30 | val document = okhttpHelper.client.newGetRequest { 31 | url(hostUrlHelper.fengcheBaseUrl) 32 | }.asDocument() 33 | val pages = mutableListOf() 34 | document.select("body > div.wrapper").forEach { wrapperEl -> 35 | val videoEls = wrapperEl.select(".picList > li") 36 | .takeIf { it.isNotEmpty() } 37 | ?: wrapperEl.select(".c2_list > li") 38 | val title = wrapperEl.selectFirst(".cont_title") 39 | ?: wrapperEl.selectFirst(".title > .t_head") 40 | title ?: return@forEach 41 | val videos = videoEls.map { it.parseFengCheAnime(source.key)} 42 | if (videos.isNotEmpty()) { 43 | val page = 44 | SourcePage.SingleCartoonPage.WithCover(title.text().trim(), { 0 }) { 45 | withResult { 46 | null to videos 47 | } 48 | } 49 | pages.add(page) 50 | } 51 | } 52 | pages 53 | } 54 | } 55 | val categoryPages = listOf( 56 | "ribendongman" to "日本动漫", 57 | "guochandongman" to "国产动漫", 58 | "dongmandianying" to "动漫电影", 59 | "oumeidongman" to "欧美动漫", 60 | ).map { (typeKey, label) -> 61 | SourcePage.Group(label = label, false) { 62 | withResult(Dispatchers.IO) { 63 | val categories = getVideoCategories() 64 | categories.map { category -> 65 | SourcePage.SingleCartoonPage.WithCover( 66 | category.label, 67 | firstKey = { 1 }) { page -> 68 | withResult(Dispatchers.IO) { 69 | requestVideoOfCategory(typeKey, category.value, page) 70 | } 71 | } 72 | } 73 | } 74 | } 75 | } 76 | return listOf(home) + categoryPages 77 | } 78 | 79 | private fun requestVideoOfCategory( 80 | typeKey: String, 81 | category: String, 82 | page: Int 83 | ): Pair> { 84 | val pageUrl = "${hostUrlHelper.fengcheBaseUrl}/show/$typeKey---$category-----$page---.html" 85 | val document = okhttpHelper.client.newGetRequest { 86 | url(pageUrl) 87 | }.asDocument() 88 | val videos = document.select(".wrapper > .culum_con > .c2_list > li") 89 | .map { it.parseFengCheAnime(source.key) } 90 | val nextPage = if (document.hasNextPage()) page + 1 else null 91 | return nextPage to videos 92 | } 93 | 94 | 95 | private suspend fun getVideoCategories(): List { 96 | if (categories == null) { 97 | categoryLock.withLock { 98 | if (categories == null) { 99 | categories = requestVideoCategories() 100 | } 101 | } 102 | } 103 | return categories!! 104 | } 105 | 106 | private fun requestVideoCategories(): List { 107 | val document = okhttpHelper.client.newGetRequest { 108 | url("${hostUrlHelper.fengcheBaseUrl}/type/ribendongman.html") 109 | }.asDocument() 110 | val row = 111 | document.select("body > div.wrapper.culum_list > div.conx.star_bot > div > div")[1] 112 | val ignoreKey = "ribendongman" 113 | val linkList = row.getElementsByTag("a") 114 | val valueIndex = linkList.last()!!.attr("href").run { 115 | substring(lastIndexOf('/') + 1, lastIndexOf('.')) 116 | } 117 | .split('-') 118 | .indexOfFirst { it.isNotEmpty() && it != ignoreKey } 119 | // 忽略第一个 120 | val categories = linkList.map {el-> 121 | val value = el.attr("href").run { 122 | substring(lastIndexOf('/') + 1, lastIndexOf('.')) 123 | } 124 | .split('-')[valueIndex] 125 | VideoCategory(label = el.text().trim(), value = value) 126 | } 127 | return categories 128 | } 129 | 130 | private data class VideoCategory( 131 | val label: String, 132 | val value: String 133 | ) 134 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/easybangumiorg/source/aio/fengche/FengChePlay.kt: -------------------------------------------------------------------------------- 1 | package io.github.easybangumiorg.source.aio.fengche 2 | 3 | import android.util.Base64 4 | import com.google.gson.Gson 5 | import com.heyanle.easybangumi4.source_api.SourceResult 6 | import com.heyanle.easybangumi4.source_api.component.ComponentWrapper 7 | import com.heyanle.easybangumi4.source_api.component.play.PlayComponent 8 | import com.heyanle.easybangumi4.source_api.entity.CartoonSummary 9 | import com.heyanle.easybangumi4.source_api.entity.Episode 10 | import com.heyanle.easybangumi4.source_api.entity.PlayLine 11 | import com.heyanle.easybangumi4.source_api.entity.PlayerInfo 12 | import com.heyanle.easybangumi4.source_api.withResult 13 | import io.github.easybangumiorg.source.aio.bodyString 14 | import io.github.easybangumiorg.source.aio.commonHttpClient 15 | import io.github.easybangumiorg.source.aio.newGetRequest 16 | import kotlinx.coroutines.Dispatchers 17 | import javax.crypto.Cipher 18 | import javax.crypto.spec.IvParameterSpec 19 | import javax.crypto.spec.SecretKeySpec 20 | 21 | class FengChePlay( 22 | private val hostUrlHelper: FengCheHostUrlHelper 23 | ) : ComponentWrapper(), PlayComponent { 24 | override suspend fun getPlayInfo( 25 | summary: CartoonSummary, 26 | playLine: PlayLine, 27 | episode: Episode 28 | ): SourceResult = withResult(Dispatchers.IO) { 29 | val newHtml = commonHttpClient.newGetRequest { 30 | url("https://danmu.yhdmjx.com/m3u8.php?url=" + extractPlayerParam(episode.id)) 31 | }.bodyString() 32 | val btToken = extractBtToken(newHtml) 33 | val encryptedUrl = extractEncryptedUrl(newHtml) 34 | 35 | val plainUrl = Cipher.getInstance("AES/CBC/PKCS5Padding").run { 36 | init( 37 | Cipher.DECRYPT_MODE, 38 | SecretKeySpec("57A891D97E332A9D".toByteArray(), "AES"), 39 | IvParameterSpec(btToken.toByteArray(Charsets.UTF_8)) 40 | ) 41 | doFinal(Base64.decode(encryptedUrl, Base64.DEFAULT)) 42 | }.toString(Charsets.UTF_8) 43 | PlayerInfo( 44 | uri = plainUrl 45 | ) 46 | } 47 | 48 | private fun extractPlayerParam(episodeId: String): String { 49 | val html = commonHttpClient.newGetRequest { 50 | url("${hostUrlHelper.fengcheBaseUrl}/play/$episodeId.html") 51 | }.bodyString() 52 | val startIndex = html.indexOf("player_aaaa") 53 | val startBracketIndex = html.indexOf('{', startIndex + 1) 54 | var endIndex = -1 55 | var bracketCount = 0 56 | for (i in (startBracketIndex + 1)..>( 72 | html.substring(startBracketIndex, endIndex + 1), 73 | Map::class.java 74 | )["url"] ?: throw RuntimeException("未获取到播放信息") 75 | } 76 | 77 | private fun extractEncryptedUrl(html: String): String { 78 | val key = "getVideoInfo(" 79 | val i1 = html.indexOf(key) 80 | val i2 = html.indexOf('"', i1 + key.length) 81 | val i3 = html.indexOf('"', i2 + 1) 82 | return html.substring(i2 + 1, i3) 83 | } 84 | 85 | private fun extractBtToken(html: String): String { 86 | val i1 = html.indexOf("bt_token") 87 | val i2 = html.indexOf('"', i1) 88 | val i3 = html.indexOf('"', i2 + 1) 89 | return html.substring(i2 + 1, i3) 90 | } 91 | 92 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/easybangumiorg/source/aio/fengche/FengChePrefer.kt: -------------------------------------------------------------------------------- 1 | package io.github.easybangumiorg.source.aio.fengche 2 | 3 | import com.heyanle.easybangumi4.source_api.component.ComponentWrapper 4 | import com.heyanle.easybangumi4.source_api.component.preference.PreferenceComponent 5 | import com.heyanle.easybangumi4.source_api.component.preference.SourcePreference 6 | import com.heyanle.easybangumi4.source_api.component.search.SearchComponent 7 | 8 | /** 9 | * Created by heyanlin on 2024/5/23. 10 | */ 11 | class FengChePrefer : ComponentWrapper(), PreferenceComponent { 12 | 13 | override fun register(): List = listOf( 14 | SourcePreference.Switch("自动从 wedm.cc 获取网址", "auto_host_url", true), 15 | SourcePreference.Edit("自定义网址(当自动关闭时才有效)", "BaseUrl", "http://www.fcdm9.com/"), 16 | ) 17 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/easybangumiorg/source/aio/fengche/FengCheSearch.kt: -------------------------------------------------------------------------------- 1 | package io.github.easybangumiorg.source.aio.fengche 2 | 3 | import com.heyanle.easybangumi4.source_api.SourceResult 4 | import com.heyanle.easybangumi4.source_api.component.ComponentWrapper 5 | import com.heyanle.easybangumi4.source_api.component.search.SearchComponent 6 | import com.heyanle.easybangumi4.source_api.entity.CartoonCover 7 | import com.heyanle.easybangumi4.source_api.entity.CartoonCoverImpl 8 | import com.heyanle.easybangumi4.source_api.withResult 9 | import io.github.easybangumiorg.source.aio.asDocument 10 | import io.github.easybangumiorg.source.aio.commonHttpClient 11 | import io.github.easybangumiorg.source.aio.encodeUri 12 | import io.github.easybangumiorg.source.aio.newGetRequest 13 | import kotlinx.coroutines.Dispatchers 14 | 15 | class FengCheSearch( 16 | private val hostUrlHelper: FengCheHostUrlHelper 17 | ) : ComponentWrapper(), SearchComponent { 18 | 19 | override fun getFirstSearchKey(keyword: String): Int = 1 20 | 21 | override suspend fun search( 22 | pageKey: Int, 23 | keyword: String 24 | ): SourceResult>> = withResult(Dispatchers.IO) { 25 | val document = commonHttpClient.newGetRequest { 26 | url("${hostUrlHelper.fengcheBaseUrl}/search/${keyword.encodeUri()}----------$pageKey---.html") 27 | }.asDocument() 28 | val videos = 29 | document.select(".sear_con > .reusltbox").map { box-> 30 | val link = box.selectFirst("a")!!.absUrl("href") 31 | val img = box.selectFirst(".img_wrapper")?.dataset()?.get("original") 32 | val title = box.selectFirst(".result_title > a")!!.text().trim() 33 | CartoonCoverImpl( 34 | id = link.extractFengCheIdFromUrl(), 35 | source = source.key, 36 | url = link, 37 | title = title, 38 | coverUrl = img 39 | ) 40 | } 41 | val nextPage = if (document.hasNextPage() && videos.isNotEmpty()) pageKey + 1 else null 42 | nextPage to videos 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/easybangumiorg/source/aio/fengche/FengCheSource.kt: -------------------------------------------------------------------------------- 1 | package io.github.easybangumiorg.source.aio.fengche 2 | 3 | import com.heyanle.easybangumi4.source_api.Source 4 | import com.heyanle.extension_api.ExtensionIconSource 5 | import org.easybangumi.extension.R 6 | import kotlin.reflect.KClass 7 | 8 | class FengCheSource : Source, ExtensionIconSource { 9 | override val describe: String 10 | get() = "风车动漫" 11 | override val key: String 12 | get() = "wedm.cc" 13 | override val label: String 14 | get() = "风车动漫" 15 | override val version: String 16 | get() = "2.1" 17 | override val versionCode: Int 18 | get() = 5 19 | 20 | override fun getIconResourcesId(): Int = R.drawable.fengche 21 | 22 | override fun register(): List> = listOf( 23 | FengChePage::class, 24 | FengCheDetail::class, 25 | FengChePlay::class, 26 | FengCheSearch::class, 27 | FengChePrefer::class, 28 | FengCheHostUrlHelper::class 29 | ) 30 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/easybangumiorg/source/aio/libvio/LibVioDetail.kt: -------------------------------------------------------------------------------- 1 | package io.github.easybangumiorg.source.aio.libvio 2 | 3 | import com.heyanle.easybangumi4.source_api.SourceResult 4 | import com.heyanle.easybangumi4.source_api.component.ComponentWrapper 5 | import com.heyanle.easybangumi4.source_api.component.detailed.DetailedComponent 6 | import com.heyanle.easybangumi4.source_api.entity.Cartoon 7 | import com.heyanle.easybangumi4.source_api.entity.CartoonImpl 8 | import com.heyanle.easybangumi4.source_api.entity.CartoonSummary 9 | import com.heyanle.easybangumi4.source_api.entity.Episode 10 | import com.heyanle.easybangumi4.source_api.entity.PlayLine 11 | import io.github.easybangumiorg.source.aio.asDocument 12 | import io.github.easybangumiorg.source.aio.map 13 | import io.github.easybangumiorg.source.aio.withIoResult 14 | 15 | class LibVioDetail(private val libVioSource: LibVioSource) : ComponentWrapper(), DetailedComponent { 16 | override suspend fun getAll(summary: CartoonSummary): SourceResult>> = 17 | withIoResult { 18 | val doc = libVioSource.requestWithFunCDNInterceptor { 19 | url("${LibVioSource.BASE_URL}/detail/${summary.id}.html") 20 | get() 21 | }.asDocument() 22 | val playlists = mutableListOf() 23 | for (playlistEl in doc.select(".stui-vodlist__head > .stui-content__playlist")) { 24 | val name = 25 | playlistEl.previousElementSibling()?.takeIf { it.hasClass("stui-pannel__head") } 26 | ?.text()?.trim() ?: continue 27 | if (name.contains("下载") || name.contains("网盘") || name.contains("云盘")) { 28 | continue 29 | } 30 | val episodes = playlistEl.select("a").map { el -> 31 | Episode( 32 | label = el.text(), 33 | order = 0, 34 | id = el.attr("href") 35 | .let { it.substring(it.lastIndexOf('/') + 1, it.lastIndexOf('.')) } 36 | ) 37 | } 38 | if (episodes.isEmpty()) { 39 | continue 40 | } 41 | playlists.add( 42 | PlayLine( 43 | id = name, 44 | label = name, 45 | episode = arrayListOf(*episodes.toTypedArray()) 46 | ) 47 | ) 48 | } 49 | 50 | val detailContainer = 51 | doc.selectFirst(".stui-content") ?: throw RuntimeException("未找到视频详情") 52 | val img = detailContainer.selectFirst("img")?.dataset()?.get("original") ?: "" 53 | val title = detailContainer.selectFirst(".stui-content__detail > .title")!!.text() 54 | val infos = detailContainer.select(".stui-content__detail > .data").map { it.text() } 55 | val desc = 56 | detailContainer.selectFirst(".stui-content__detail > .desc > .detail-content") 57 | ?.text() 58 | ?: "" 59 | val cartoon = CartoonImpl( 60 | id = summary.id, 61 | source = libVioSource.key, 62 | title = title, 63 | coverUrl = img, 64 | intro = infos.joinToString(separator = "\n"), 65 | description = desc, 66 | url = "${LibVioSource.BASE_URL}/detail/${summary.id}.html" 67 | ) 68 | cartoon to playlists 69 | } 70 | 71 | override suspend fun getDetailed(summary: CartoonSummary): SourceResult = 72 | getAll(summary).map { it.first } 73 | 74 | override suspend fun getPlayLine(summary: CartoonSummary): SourceResult> = 75 | getAll(summary).map { it.second } 76 | 77 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/easybangumiorg/source/aio/libvio/LibVioPage.kt: -------------------------------------------------------------------------------- 1 | package io.github.easybangumiorg.source.aio.libvio 2 | 3 | import android.content.Context 4 | import com.heyanle.easybangumi4.source_api.component.ComponentWrapper 5 | import com.heyanle.easybangumi4.source_api.component.page.PageComponent 6 | import com.heyanle.easybangumi4.source_api.component.page.SourcePage 7 | import com.heyanle.easybangumi4.source_api.entity.CartoonCover 8 | import com.heyanle.easybangumi4.source_api.utils.api.OkhttpHelper 9 | import com.heyanle.easybangumi4.source_api.utils.api.StringHelper 10 | import com.heyanle.easybangumi4.source_api.withResult 11 | import io.github.easybangumiorg.source.aio.asDocument 12 | import io.github.easybangumiorg.source.aio.withIoResult 13 | 14 | class LibVioPage( 15 | private val libVioSource: LibVioSource, 16 | context: Context, 17 | okhttpHelper: OkhttpHelper, 18 | stringHelper: StringHelper 19 | ) : ComponentWrapper(), PageComponent { 20 | 21 | init { 22 | libVioSource.init(context, okhttpHelper, stringHelper) 23 | } 24 | 25 | override fun getPages(): List { 26 | val pages = 27 | listOf("电影" to "1", "剧集" to "2", "动漫" to "4").map { (groupName, groupKey) -> 28 | SourcePage.Group(label = groupName, newScreen = false) { 29 | withIoResult { 30 | val doc = libVioSource.requestWithFunCDNInterceptor { 31 | url("${LibVioSource.BASE_URL}/type/$groupKey.html") 32 | get() 33 | }.asDocument() 34 | val categories = 35 | listOf("全部" to "") + doc.getElementById("screenbox")!!.child(0) 36 | .children() 37 | .let { 38 | it.subList(1, it.size) 39 | }.map { el -> 40 | val link = el.getElementsByTag("a")[0] 41 | val url = link.attr("href") 42 | val key = 43 | url.substring( 44 | url.lastIndexOf('/') + 1, 45 | url.lastIndexOf('.') 46 | ) 47 | .split('-') 48 | .asSequence() 49 | .filter { it.isNotEmpty() } 50 | .last() 51 | link.text().trim() to key 52 | }.distinctBy { it.second } 53 | 54 | categories.map { (categoryName, categoryKey) -> 55 | SourcePage.SingleCartoonPage.WithCover( 56 | label = categoryName, 57 | firstKey = { 1 }) { page -> 58 | withIoResult { 59 | loadVideoOfCategory( 60 | groupKey = groupKey, 61 | categoryKey = categoryKey, 62 | page = page 63 | ) 64 | } 65 | } 66 | } 67 | } 68 | } 69 | } 70 | return listOf(SourcePage.Group(label = "首页", newScreen = false) { 71 | withIoResult { 72 | val doc = libVioSource.requestWithFunCDNInterceptor { 73 | url(LibVioSource.BASE_URL) 74 | get() 75 | }.asDocument() 76 | val list = mutableListOf() 77 | 78 | val containers = doc.getElementsByClass("stui-vodlist") 79 | for (container in containers) { 80 | val videoEls = container.select("li > .stui-vodlist__box") 81 | if (videoEls.isEmpty()) { 82 | break 83 | } 84 | 85 | val groupName = 86 | container.previousElementSibling() 87 | ?.takeIf { it.hasClass("stui-vodlist__head") } 88 | ?.selectFirst("h3")?.text()?.trim() ?: "推荐视频" 89 | val videos = videoEls.map { libVioSource.parseLibVioVideo(it) } 90 | val page = SourcePage.SingleCartoonPage.WithCover( 91 | label = groupName, 92 | firstKey = { 0 }) { 93 | withResult { 94 | null to videos 95 | } 96 | } 97 | list.add(page) 98 | } 99 | list 100 | } 101 | }) + pages 102 | } 103 | 104 | private suspend fun loadVideoOfCategory( 105 | groupKey: String, 106 | categoryKey: String, 107 | page: Int 108 | ): Pair> { 109 | val doc = libVioSource.requestWithFunCDNInterceptor { 110 | url("${LibVioSource.BASE_URL}/show/$groupKey--time-$categoryKey-----$page---.html") 111 | get() 112 | }.asDocument() 113 | return libVioSource.parseVideoPage(doc, page) 114 | } 115 | 116 | 117 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/easybangumiorg/source/aio/libvio/LibVioPlay.kt: -------------------------------------------------------------------------------- 1 | package io.github.easybangumiorg.source.aio.libvio 2 | 3 | import android.util.Log 4 | import com.google.gson.Gson 5 | import com.heyanle.easybangumi4.source_api.SourceResult 6 | import com.heyanle.easybangumi4.source_api.component.ComponentWrapper 7 | import com.heyanle.easybangumi4.source_api.component.play.PlayComponent 8 | import com.heyanle.easybangumi4.source_api.entity.CartoonSummary 9 | import com.heyanle.easybangumi4.source_api.entity.Episode 10 | import com.heyanle.easybangumi4.source_api.entity.PlayLine 11 | import com.heyanle.easybangumi4.source_api.entity.PlayerInfo 12 | import io.github.easybangumiorg.source.aio.bodyString 13 | import io.github.easybangumiorg.source.aio.decodeUri 14 | import io.github.easybangumiorg.source.aio.encodeUri 15 | import io.github.easybangumiorg.source.aio.newGetRequest 16 | import io.github.easybangumiorg.source.aio.withIoResult 17 | import java.net.URI 18 | import java.util.concurrent.ConcurrentHashMap 19 | import kotlin.io.encoding.Base64 20 | import kotlin.io.encoding.ExperimentalEncodingApi 21 | 22 | class LibVioPlay(private val libVioSource: LibVioSource) : ComponentWrapper(), PlayComponent { 23 | private val playerServerCache = ConcurrentHashMap() 24 | 25 | @OptIn(ExperimentalEncodingApi::class) 26 | override suspend fun getPlayInfo( 27 | summary: CartoonSummary, 28 | playLine: PlayLine, 29 | episode: Episode 30 | ): SourceResult = withIoResult { 31 | val html = libVioSource.requestWithFunCDNInterceptor { 32 | url("${LibVioSource.BASE_URL}/play/${episode.id}.html") 33 | get() 34 | }.bodyString() 35 | val keyword = "player_aaaa" 36 | val startIndex = html.indexOf(keyword) 37 | if (startIndex == -1) { 38 | throw RuntimeException("未获取到视频播放信息") 39 | } 40 | val configStartIndex = html.indexOf('{', startIndex = startIndex + keyword.length) 41 | val configJson = 42 | html.substring( 43 | configStartIndex, 44 | html.indexOf('}', startIndex = configStartIndex + 1) + 1 45 | ) 46 | val cfg = Gson().fromJson>(configJson, Map::class.java) 47 | val encryptMode = cfg["encrypt"]?.toString() ?: "" 48 | val url = cfg["url"]?.toString()?.takeIf { it.isNotEmpty() } 49 | ?: throw RuntimeException("播放信息中无url") 50 | val nextLink = cfg["link_next"]?.toString()?.encodeUri() ?: "" 51 | val data = when (encryptMode) { 52 | "1" -> url.decodeUri() 53 | "2" -> Base64.decode(url).toString(Charsets.UTF_8).decodeUri() 54 | else -> url 55 | } 56 | val playFrom = cfg["from"]?.toString()?.takeIf { it.isNotEmpty() } 57 | ?: throw RuntimeException("播放器配置中无from属性") 58 | var playerServer = playerServerCache[playFrom] 59 | if (playerServer?.isNotEmpty() != true) { 60 | val jsContent = libVioSource.requestWithFunCDNInterceptor { 61 | url("${LibVioSource.BASE_URL}/static/player/${playFrom}.js?v=3.5") 62 | }.bodyString() 63 | playerServer = parseServerFromJs(jsContent) 64 | if (!playerServer.startsWith("http:") && !playerServer.startsWith("https")) { 65 | playerServer = LibVioSource.BASE_URL + playerServer 66 | } 67 | playerServerCache[playFrom] = playerServer 68 | } 69 | Log.d(TAG, "getPlayInfo: $playerServer") 70 | val playerHtml = 71 | libVioSource.httpClient.newGetRequest { 72 | url("$playerServer?url=$data&next=$nextLink&id=${summary.id}&nid=${cfg["nid"]}") 73 | header("referer", "${LibVioSource.BASE_URL}/") 74 | }.bodyString() 75 | val videoUrl = getStringVariableValue(html = playerHtml, variableName = "urls") 76 | ?: throw RuntimeException("未获取到视频链接") 77 | PlayerInfo(uri = videoUrl) 78 | } 79 | 80 | 81 | private fun parseServerFromJs(jsContent: String): String { 82 | val keyword = " src=" 83 | val idx = jsContent.indexOf(keyword) 84 | if (idx == -1) { 85 | throw RuntimeException("js中无src") 86 | } 87 | var startIndex = -1 88 | var endIndex = -1 89 | for (i in (keyword.length + idx) until jsContent.length) { 90 | val c = jsContent[i] 91 | if (c == '"' || c == '\'') { 92 | if (startIndex == -1) { 93 | startIndex = i + 1 94 | } else { 95 | endIndex = i 96 | break 97 | } 98 | } 99 | } 100 | if (startIndex >= endIndex) { 101 | throw RuntimeException("提前src失败") 102 | } 103 | return jsContent.substring(startIndex, endIndex).run { 104 | val questionIndex = indexOf('?') 105 | if (questionIndex == -1) { 106 | this 107 | } else { 108 | substring(0, questionIndex) 109 | } 110 | } 111 | } 112 | 113 | private fun getStringVariableValue(html: String, variableName: String): String? { 114 | val keyword = "var $variableName" 115 | val index = html.indexOf(keyword) 116 | if (index == -1) { 117 | return null 118 | } 119 | var startIndex = -1 120 | var quote = '"' 121 | var endIndex = -1 122 | for (i in (index + keyword.length) until html.length) { 123 | val c = html[i] 124 | if (startIndex == -1 && (c == '\'' || c == '"')) { 125 | startIndex = i + 1 126 | quote = c 127 | continue 128 | } 129 | if (startIndex != -1 && c == quote) { 130 | endIndex = i 131 | break 132 | } 133 | } 134 | if (startIndex < endIndex) { 135 | return html.substring(startIndex, endIndex) 136 | } 137 | return null 138 | } 139 | 140 | companion object{ 141 | private const val TAG = "LibVioPlay" 142 | } 143 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/easybangumiorg/source/aio/libvio/LibVioSearch.kt: -------------------------------------------------------------------------------- 1 | package io.github.easybangumiorg.source.aio.libvio 2 | 3 | import com.heyanle.easybangumi4.source_api.SourceResult 4 | import com.heyanle.easybangumi4.source_api.component.ComponentWrapper 5 | import com.heyanle.easybangumi4.source_api.component.search.SearchComponent 6 | import com.heyanle.easybangumi4.source_api.entity.CartoonCover 7 | import io.github.easybangumiorg.source.aio.asDocument 8 | import io.github.easybangumiorg.source.aio.encodeUri 9 | import io.github.easybangumiorg.source.aio.withIoResult 10 | 11 | class LibVioSearch( 12 | private val libVioSource: LibVioSource 13 | ) : ComponentWrapper(), SearchComponent { 14 | override fun getFirstSearchKey(keyword: String): Int = 1 15 | 16 | override suspend fun search( 17 | pageKey: Int, 18 | keyword: String 19 | ): SourceResult>> = withIoResult { 20 | val doc = libVioSource.requestWithFunCDNInterceptor { 21 | url("${LibVioSource.BASE_URL}/search/${keyword.encodeUri()}----------$pageKey---.html") 22 | }.asDocument() 23 | libVioSource.parseVideoPage(doc, pageKey) 24 | } 25 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/easybangumiorg/source/aio/xigua/Common.kt: -------------------------------------------------------------------------------- 1 | package io.github.easybangumiorg.source.aio.xigua 2 | 3 | const val XiguaBaseUrl = "https://cn.xgcartoon.com" 4 | 5 | fun String.extractXiguaIdFromUrl() = this.substring(this.lastIndexOf('/') + 1) 6 | -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/easybangumiorg/source/aio/xigua/XiguaDetail.kt: -------------------------------------------------------------------------------- 1 | package io.github.easybangumiorg.source.aio.xigua 2 | 3 | import com.heyanle.easybangumi4.source_api.SourceResult 4 | import com.heyanle.easybangumi4.source_api.component.ComponentWrapper 5 | import com.heyanle.easybangumi4.source_api.component.detailed.DetailedComponent 6 | import com.heyanle.easybangumi4.source_api.entity.Cartoon 7 | import com.heyanle.easybangumi4.source_api.entity.CartoonImpl 8 | import com.heyanle.easybangumi4.source_api.entity.CartoonSummary 9 | import com.heyanle.easybangumi4.source_api.entity.Episode 10 | import com.heyanle.easybangumi4.source_api.entity.PlayLine 11 | import com.heyanle.easybangumi4.source_api.withResult 12 | import io.github.easybangumiorg.source.aio.asDocument 13 | import io.github.easybangumiorg.source.aio.commonHttpClient 14 | import io.github.easybangumiorg.source.aio.map 15 | import io.github.easybangumiorg.source.aio.newGetRequest 16 | import kotlinx.coroutines.Dispatchers 17 | 18 | class XiguaDetail : ComponentWrapper(), DetailedComponent { 19 | override suspend fun getAll(summary: CartoonSummary): SourceResult>> { 20 | return withResult(Dispatchers.IO) { 21 | 22 | val doc = commonHttpClient.newGetRequest { 23 | url("$XiguaBaseUrl/detail/${summary.id}") 24 | }.asDocument() 25 | val detailContainer = doc.selectFirst(".detail-right")!! 26 | val name = detailContainer.selectFirst(".detail-right__title")!!.text() 27 | val tags = detailContainer.select(".detail-right__tags .tag").map { it.text() } 28 | val desc = 29 | detailContainer.selectFirst(".detail-right__desc")?.children()?.last()?.text() 30 | val cartoon = CartoonImpl( 31 | id = summary.id, 32 | source = source.key, 33 | url = "$XiguaBaseUrl/detail/${summary.id}", 34 | title = name, 35 | genre = tags.joinToString(" "), 36 | coverUrl = "https://static-a.xgcartoon.com/cover/${summary.id}.jpg", 37 | description = desc 38 | ) 39 | val playlists = mutableListOf() 40 | var currentEpisodes = arrayListOf() 41 | var playLineId = 0 42 | detailContainer.selectFirst(".detail-right__volumes")?.lastElementChild()?.children() 43 | ?.forEach { element -> 44 | if (element.hasClass("volume-title")) { 45 | currentEpisodes = arrayListOf() 46 | playlists.add( 47 | PlayLine( 48 | (playLineId++).toString(), 49 | label = element.text(), 50 | currentEpisodes 51 | ) 52 | ) 53 | } else { 54 | val linkEl = element.selectFirst("a")!! 55 | currentEpisodes.add( 56 | Episode( 57 | id = linkEl.attr("href").let { 58 | it.substring(it.lastIndexOf('=') + 1) 59 | }, 60 | label = linkEl.text(), 61 | order = 0 62 | ) 63 | ) 64 | } 65 | } 66 | cartoon to playlists 67 | } 68 | 69 | } 70 | 71 | override suspend fun getDetailed(summary: CartoonSummary): SourceResult { 72 | return getAll(summary).map { it.first } 73 | } 74 | 75 | override suspend fun getPlayLine(summary: CartoonSummary): SourceResult> { 76 | return getAll(summary).map { it.second } 77 | } 78 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/easybangumiorg/source/aio/xigua/XiguaPlay.kt: -------------------------------------------------------------------------------- 1 | package io.github.easybangumiorg.source.aio.xigua 2 | 3 | import com.heyanle.easybangumi4.source_api.ParserException 4 | import com.heyanle.easybangumi4.source_api.SourceResult 5 | import com.heyanle.easybangumi4.source_api.component.ComponentWrapper 6 | import com.heyanle.easybangumi4.source_api.component.play.PlayComponent 7 | import com.heyanle.easybangumi4.source_api.entity.CartoonSummary 8 | import com.heyanle.easybangumi4.source_api.entity.Episode 9 | import com.heyanle.easybangumi4.source_api.entity.PlayLine 10 | import com.heyanle.easybangumi4.source_api.entity.PlayerInfo 11 | import com.heyanle.easybangumi4.source_api.withResult 12 | import io.github.easybangumiorg.source.aio.asDocument 13 | import io.github.easybangumiorg.source.aio.commonHttpClient 14 | import io.github.easybangumiorg.source.aio.newGetRequest 15 | import kotlinx.coroutines.Dispatchers 16 | import okhttp3.HttpUrl.Companion.toHttpUrlOrNull 17 | 18 | class XiguaPlay : PlayComponent, ComponentWrapper() { 19 | 20 | override suspend fun getPlayInfo( 21 | summary: CartoonSummary, 22 | playLine: PlayLine, 23 | episode: Episode 24 | ): SourceResult { 25 | val url = "$XiguaBaseUrl/video/${summary.id}/${episode.id}.html" 26 | return withResult(Dispatchers.IO) { 27 | val doc = commonHttpClient.newGetRequest { 28 | url(url) 29 | }.asDocument() 30 | val iframe = 31 | doc.selectFirst("#video_content iframe") 32 | ?: throw ParserException("未获取到播放器") 33 | val iframeSrc = iframe.attr("src") 34 | if (iframeSrc.isEmpty()) { 35 | throw ParserException("未获取到播放器链接") 36 | } 37 | val vid = iframeSrc.toHttpUrlOrNull()?.queryParameter("vid") 38 | val videoUrl = if (vid != null) { 39 | "https://xgct-video.vzcdn.net/$vid/playlist.m3u8" 40 | } else { 41 | commonHttpClient.newGetRequest { url(iframeSrc) }.asDocument() 42 | .getElementsByTag("source") 43 | .firstOrNull() 44 | ?.attr("src") 45 | ?.takeIf { it.isNotEmpty() } 46 | ?: throw RuntimeException("未获取到video source") 47 | } 48 | PlayerInfo( 49 | decodeType = PlayerInfo.DECODE_TYPE_HLS, 50 | uri = videoUrl 51 | ) 52 | } 53 | } 54 | 55 | companion object { 56 | private const val TAG = "XiguaPlay" 57 | } 58 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/easybangumiorg/source/aio/xigua/XiguaSearch.kt: -------------------------------------------------------------------------------- 1 | package io.github.easybangumiorg.source.aio.xigua 2 | 3 | import com.heyanle.easybangumi4.source_api.SourceResult 4 | import com.heyanle.easybangumi4.source_api.component.ComponentWrapper 5 | import com.heyanle.easybangumi4.source_api.component.search.SearchComponent 6 | import com.heyanle.easybangumi4.source_api.entity.CartoonCover 7 | import com.heyanle.easybangumi4.source_api.entity.CartoonCoverImpl 8 | import com.heyanle.easybangumi4.source_api.withResult 9 | import io.github.easybangumiorg.source.aio.asDocument 10 | import io.github.easybangumiorg.source.aio.commonHttpClient 11 | import io.github.easybangumiorg.source.aio.encodeUri 12 | import io.github.easybangumiorg.source.aio.newGetRequest 13 | import kotlinx.coroutines.Dispatchers 14 | 15 | class XiguaSearch : ComponentWrapper(), SearchComponent { 16 | override fun getFirstSearchKey(keyword: String): Int = 0 17 | 18 | override suspend fun search( 19 | pageKey: Int, 20 | keyword: String 21 | ): SourceResult>> = withResult(Dispatchers.IO) { 22 | val doc = commonHttpClient.newGetRequest { 23 | url("$XiguaBaseUrl/search?q=${keyword.encodeUri()}") 24 | }.asDocument() 25 | val videos = 26 | doc.select("#layout > .container.search > .topic-list > .topic-list-box").map { box -> 27 | val url = box.selectFirst("a")!!.absUrl("href") 28 | val id = url.extractXiguaIdFromUrl() 29 | val tags = 30 | box.select(".topic-tag .tag").asSequence().map { it.text() }.toMutableList() 31 | val infoList = box.selectFirst(".topic-list-item__info")!!.children() 32 | val name = infoList.last()!!.text() 33 | if (infoList.size > 1) { 34 | tags.add(infoList[0].text()) 35 | } 36 | CartoonCoverImpl( 37 | id = id, 38 | source = source.key, 39 | url = url, 40 | title = name, 41 | intro = tags.joinToString(" "), 42 | coverUrl = "https://static-a.xgcartoon.com/cover/$id.jpg" 43 | ) 44 | } 45 | null to videos 46 | } 47 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/easybangumiorg/source/aio/xigua/XiguaSource.kt: -------------------------------------------------------------------------------- 1 | package io.github.easybangumiorg.source.aio.xigua 2 | 3 | import com.heyanle.easybangumi4.source_api.Source 4 | import com.heyanle.extension_api.ExtensionIconSource 5 | import org.easybangumi.extension.R 6 | import kotlin.reflect.KClass 7 | 8 | class XiguaSource : Source, ExtensionIconSource { 9 | override val describe: String 10 | get() = "西瓜卡通" 11 | override val key: String 12 | get() = "xgcartoon" 13 | override val label: String 14 | get() = "西瓜卡通" 15 | override val version: String 16 | get() = "2.0" 17 | override val versionCode: Int 18 | get() = 4 19 | 20 | override fun getIconResourcesId(): Int = R.drawable.xigua 21 | 22 | 23 | override fun register(): List> { 24 | return listOf( 25 | XiGuaPage::class, 26 | XiguaDetail::class, 27 | XiguaPlay::class, 28 | XiguaSearch::class 29 | ) 30 | } 31 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/peacefulprogram/easybangumi_mikudm/MikudmApiSource.kt: -------------------------------------------------------------------------------- 1 | package io.github.peacefulprogram.easybangumi_mikudm 2 | 3 | import com.heyanle.easybangumi4.source_api.Source 4 | import com.heyanle.extension_api.ExtensionIconSource 5 | import com.heyanle.extension_api.ExtensionSource 6 | import org.easybangumi.extension.R 7 | import kotlin.reflect.KClass 8 | 9 | class MikudmApiSource : Source, ExtensionIconSource { 10 | override val describe: String 11 | get() = label 12 | override val label: String 13 | get() = "异世界动漫" 14 | override val version: String 15 | get() = "2.0" 16 | override val versionCode: Int 17 | get() = 2 18 | 19 | override fun getIconResourcesId(): Int = R.drawable.mikudm 20 | override val key: String 21 | get() = "io.github.peacefulprogram.easybangumi_mikudm-io.github.peacefulprogram.easybangumi_mikudm" 22 | 23 | override fun register(): List> { 24 | return listOf( 25 | MikudmPageComponent::class, 26 | MikudmSearchComponent::class, 27 | MikudmDetailComponent::class, 28 | MikudmPlayComponent::class, 29 | MikudmPreferenceComponent::class, 30 | MikudmUtil::class 31 | ) 32 | } 33 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/peacefulprogram/easybangumi_mikudm/MikudmDetailComponent.kt: -------------------------------------------------------------------------------- 1 | package io.github.peacefulprogram.easybangumi_mikudm 2 | 3 | import com.heyanle.easybangumi4.source_api.SourceResult 4 | import com.heyanle.easybangumi4.source_api.component.ComponentWrapper 5 | import com.heyanle.easybangumi4.source_api.component.detailed.DetailedComponent 6 | import com.heyanle.easybangumi4.source_api.entity.Cartoon 7 | import com.heyanle.easybangumi4.source_api.entity.CartoonImpl 8 | import com.heyanle.easybangumi4.source_api.entity.CartoonSummary 9 | import com.heyanle.easybangumi4.source_api.entity.Episode 10 | import com.heyanle.easybangumi4.source_api.entity.PlayLine 11 | import com.heyanle.easybangumi4.source_api.utils.api.OkhttpHelper 12 | import com.heyanle.easybangumi4.source_api.withResult 13 | import kotlinx.coroutines.Dispatchers 14 | import org.jsoup.Jsoup 15 | 16 | class MikudmDetailComponent( 17 | private val okhttpHelper: OkhttpHelper, 18 | private val mikudmUtil: MikudmUtil, 19 | ) : ComponentWrapper(), DetailedComponent { 20 | override suspend fun getAll(summary: CartoonSummary): SourceResult>> = 21 | withResult(Dispatchers.IO) { 22 | getVideoDetail(summary.id) 23 | } 24 | 25 | override suspend fun getDetailed(summary: CartoonSummary): SourceResult = 26 | withResult(Dispatchers.IO) { 27 | getVideoDetail(summary.id).first 28 | } 29 | 30 | override suspend fun getPlayLine(summary: CartoonSummary): SourceResult> = 31 | withResult(Dispatchers.IO) { 32 | getVideoDetail(summary.id).second 33 | } 34 | 35 | private fun getVideoDetail(videoId: String): Pair> { 36 | val document = 37 | Jsoup.parse( 38 | mikudmUtil.getDocument(okhttpHelper, "/index.php/vod/detail/id/$videoId.html"), 39 | mikudmUtil.BASE_URL 40 | ) 41 | val detailContainer = document.selectFirst(".detail_list")!! 42 | val videoTitle = detailContainer.selectFirst(".content_detail .title")!!.text().trim() 43 | 44 | val desc = detailContainer.selectFirst(".desc")?.text() 45 | val coverUrl = detailContainer.selectFirst(".vodlist_thumb")?.run { 46 | mikudmUtil.extractImageSrc(this) 47 | } 48 | val cartoon = CartoonImpl( 49 | id = videoId, 50 | source = source.key, 51 | url = "${mikudmUtil.BASE_URL}/index.php/vod/detail/id/$videoId.html", 52 | title = videoTitle, 53 | coverUrl = coverUrl, 54 | description = desc 55 | ) 56 | 57 | val episodeList = (document.select(".content_playlist").last()?.let { playlist -> 58 | playlist.getElementsByTag("a").map { it.text().trim() } 59 | } ?: emptyList()) 60 | val ep = arrayListOf() 61 | for (s in episodeList.indices) { 62 | val d = episodeList[s] 63 | ep.add(Episode(s.toString(), d, s)) 64 | } 65 | 66 | return cartoon to DetailedComponent.NonPlayLine( 67 | PlayLine( 68 | id = "1", 69 | label = "异世界动漫", 70 | episode = ep 71 | ) 72 | ) 73 | } 74 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/peacefulprogram/easybangumi_mikudm/MikudmPageComponent.kt: -------------------------------------------------------------------------------- 1 | package io.github.peacefulprogram.easybangumi_mikudm 2 | 3 | import com.heyanle.easybangumi4.source_api.component.ComponentWrapper 4 | import com.heyanle.easybangumi4.source_api.component.page.PageComponent 5 | import com.heyanle.easybangumi4.source_api.component.page.SourcePage 6 | import com.heyanle.easybangumi4.source_api.entity.CartoonCover 7 | import com.heyanle.easybangumi4.source_api.entity.CartoonCoverImpl 8 | import com.heyanle.easybangumi4.source_api.utils.api.NetworkHelper 9 | import com.heyanle.easybangumi4.source_api.utils.api.OkhttpHelper 10 | import com.heyanle.easybangumi4.source_api.withResult 11 | import kotlinx.coroutines.Dispatchers 12 | import org.jsoup.Jsoup 13 | import org.jsoup.nodes.Document 14 | import org.jsoup.nodes.Element 15 | 16 | class MikudmPageComponent( 17 | private val okhttpHelper: OkhttpHelper, 18 | private val mikudmUtil: MikudmUtil, 19 | ) : ComponentWrapper(), PageComponent { 20 | override fun getPages(): List { 21 | val pages = mutableListOf() 22 | val homePage = SourcePage.Group("首页", false) { 23 | withResult(Dispatchers.IO) { 24 | parseHomePage(Jsoup.parse(mikudmUtil.getDocument(okhttpHelper, "/"), mikudmUtil.BASE_URL)) 25 | } 26 | } 27 | pages.add(homePage) 28 | 29 | listOf( 30 | 22 to "新番", 31 | 20 to "完结" 32 | ).forEach { (typeId, typeName) -> 33 | val page = SourcePage.SingleCartoonPage.WithCover(typeName, { 1 }) { page -> 34 | withResult(Dispatchers.IO) { 35 | buildPageOfType(typeId, page) 36 | } 37 | } 38 | pages.add(page) 39 | } 40 | 41 | return pages 42 | } 43 | 44 | 45 | private fun buildPageOfType(typeId: Int, page: Int): Pair> { 46 | val document = Jsoup.parse( 47 | mikudmUtil.getDocument(okhttpHelper, "/index.php/vod/type/id/$typeId/page/$page.html"), 48 | mikudmUtil.BASE_URL 49 | ) 50 | val videos = document.select(".vodlist_item").map { it.parseToCartoon() } 51 | val nextPage = 52 | if (mikudmUtil.hasNextPage(document) && videos.isNotEmpty()) page + 1 else null 53 | return nextPage to videos 54 | } 55 | 56 | private fun parseHomePage(document: Document): List { 57 | val result = mutableListOf() 58 | document.select(".vod_row .pannel").forEach { videoGroupContainer -> 59 | val groupTitle = 60 | videoGroupContainer.selectFirst(".title")!!.textNodes().last().text().trim() 61 | if (groupTitle.contains("福利")) { 62 | return@forEach 63 | } 64 | val videos = videoGroupContainer.select(".vodlist_item").map { it.parseToCartoon() } 65 | val page = SourcePage.SingleCartoonPage.WithCover(groupTitle, { 0 }) { 66 | withResult { 67 | null to videos 68 | } 69 | } 70 | result.add(page) 71 | } 72 | return result 73 | } 74 | 75 | private fun Element.parseToCartoon(): CartoonCover { 76 | val linkEl = this.selectFirst("a")!! 77 | val url = linkEl.absUrl("href") 78 | val coverUrl = mikudmUtil.extractImageSrc(linkEl) 79 | val title = this.selectFirst(".vodlist_title")!!.text().trim() 80 | val episode = this.selectFirst(".pic_text")?.text()?.trim() 81 | return CartoonCoverImpl( 82 | id = url.substring(url.lastIndexOf('/') + 1, url.lastIndexOf('.')), 83 | source = source.key, 84 | url = url, 85 | title = title, 86 | intro = episode, 87 | coverUrl = coverUrl 88 | ) 89 | } 90 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/peacefulprogram/easybangumi_mikudm/MikudmPlayComponent.kt: -------------------------------------------------------------------------------- 1 | package io.github.peacefulprogram.easybangumi_mikudm 2 | 3 | import android.util.Base64 4 | import com.google.gson.Gson 5 | import com.heyanle.easybangumi4.source_api.SourceResult 6 | import com.heyanle.easybangumi4.source_api.component.ComponentWrapper 7 | import com.heyanle.easybangumi4.source_api.component.play.PlayComponent 8 | import com.heyanle.easybangumi4.source_api.entity.CartoonSummary 9 | import com.heyanle.easybangumi4.source_api.entity.Episode 10 | import com.heyanle.easybangumi4.source_api.entity.PlayLine 11 | import com.heyanle.easybangumi4.source_api.entity.PlayerInfo 12 | import com.heyanle.easybangumi4.source_api.utils.api.OkhttpHelper 13 | import com.heyanle.easybangumi4.source_api.utils.api.WebViewHelper 14 | import com.heyanle.easybangumi4.source_api.utils.core.SourceUtils 15 | import com.heyanle.easybangumi4.source_api.withResult 16 | import kotlinx.coroutines.Dispatchers 17 | import javax.crypto.Cipher 18 | import javax.crypto.spec.IvParameterSpec 19 | import javax.crypto.spec.SecretKeySpec 20 | 21 | class MikudmPlayComponent( 22 | private val okhttpHelper: OkhttpHelper, 23 | private val mikudmUtil: MikudmUtil, 24 | private val webViewHelper: WebViewHelper, 25 | ) : ComponentWrapper(), PlayComponent { 26 | override suspend fun getPlayInfo( 27 | summary: CartoonSummary, 28 | playLine: PlayLine, 29 | episode: Episode, 30 | ): SourceResult = withResult(Dispatchers.IO) { 31 | // val url = SourceUtils.urlParser(mikudmUtil.BASE_URL, "/index.php/vod/play/id/${summary.id}/sid/${playLine.id}/nid/${(episode.id.toIntOrNull() ?: episode.order) + 1}.html") 32 | // val playerUrl = webViewHelper.interceptResource( 33 | // url, 34 | // "https://json.mmiku.net/jsonapi.php?.*", 35 | // userAgentString = mikudmUtil.USER_AGENT 36 | // ) 37 | 38 | 39 | val html = 40 | mikudmUtil.getDocument(okhttpHelper, "/index.php/vod/play/id/${summary.id}/sid/${playLine.id}/nid/${(episode.id.toIntOrNull() ?: episode.order) + 1}.html") 41 | val newHtml = 42 | mikudmUtil.getDocument( 43 | okhttpHelper, "${mikudmUtil.BASE_M3U8_URL}/m3u8.php?url=" + extractPlayerParam( 44 | html 45 | ) 46 | ) 47 | val leToken = extractLeToken(newHtml) 48 | val encryptedUrl = extractEncryptedUrl(newHtml) 49 | 50 | val plainUrl = Cipher.getInstance("AES/CBC/PKCS5Padding").run { 51 | init( 52 | Cipher.DECRYPT_MODE, 53 | SecretKeySpec("A42EAC0C2B408472".toByteArray(), "AES"), 54 | IvParameterSpec(leToken.toByteArray(Charsets.UTF_8)) 55 | ) 56 | doFinal(Base64.decode(encryptedUrl, Base64.DEFAULT)) 57 | }.toString(Charsets.UTF_8) 58 | PlayerInfo(uri = plainUrl, decodeType = PlayerInfo.DECODE_TYPE_HLS).apply { 59 | header = mapOf("User-Agent" to mikudmUtil.USER_AGENT) 60 | } 61 | } 62 | 63 | private fun extractPlayerParam(html: String): String { 64 | val startIndex = html.indexOf("player_aaaa") 65 | val startBracketIndex = html.indexOf('{', startIndex + 1) 66 | var endIndex = -1 67 | var bracketCount = 0 68 | for (i in (startBracketIndex + 1)..>( 84 | html.substring(startBracketIndex, endIndex + 1), 85 | Map::class.java 86 | )["url"] ?: throw RuntimeException("未获取到播放信息") 87 | } 88 | 89 | fun extractEncryptedUrl(html: String): String { 90 | val key = "getVideoInfo(" 91 | val i1 = html.indexOf(key) 92 | val i2 = html.indexOf('"', i1 + key.length) 93 | val i3 = html.indexOf('"', i2 + 1) 94 | return html.substring(i2 + 1, i3) 95 | } 96 | 97 | fun extractLeToken(html: String): String { 98 | val i1 = html.indexOf("le_token") 99 | val i2 = html.indexOf('"', i1) 100 | val i3 = html.indexOf('"', i2 + 1) 101 | return html.substring(i2 + 1, i3) 102 | } 103 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/peacefulprogram/easybangumi_mikudm/MikudmPreferenceComponent.kt: -------------------------------------------------------------------------------- 1 | package io.github.peacefulprogram.easybangumi_mikudm 2 | 3 | import com.heyanle.easybangumi4.source_api.component.ComponentWrapper 4 | import com.heyanle.easybangumi4.source_api.component.preference.PreferenceComponent 5 | import com.heyanle.easybangumi4.source_api.component.preference.SourcePreference 6 | 7 | /** 8 | * Created by heyanle on 2024/6/9. 9 | * https://github.com/heyanLE 10 | */ 11 | class MikudmPreferenceComponent : ComponentWrapper(), PreferenceComponent { 12 | 13 | override fun register(): List = listOf( 14 | SourcePreference.Edit("异世界动漫网址", "BaseUrl", "https://www.dmmiku.com"), 15 | SourcePreference.Edit("异世界动漫视频 M3U8 网址", "BaseM3u8Url", "https://bf.mmiku.net"), 16 | ) 17 | 18 | override fun needMigrate(oldVersionCode: Int): Boolean { 19 | return false 20 | } 21 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/peacefulprogram/easybangumi_mikudm/MikudmSearchComponent.kt: -------------------------------------------------------------------------------- 1 | package io.github.peacefulprogram.easybangumi_mikudm 2 | 3 | import com.heyanle.easybangumi4.source_api.SourceResult 4 | import com.heyanle.easybangumi4.source_api.component.ComponentWrapper 5 | import com.heyanle.easybangumi4.source_api.component.search.SearchComponent 6 | import com.heyanle.easybangumi4.source_api.entity.CartoonCover 7 | import com.heyanle.easybangumi4.source_api.entity.CartoonCoverImpl 8 | import com.heyanle.easybangumi4.source_api.utils.api.OkhttpHelper 9 | import com.heyanle.easybangumi4.source_api.utils.api.WebViewHelper 10 | import com.heyanle.easybangumi4.source_api.withResult 11 | import kotlinx.coroutines.Dispatchers 12 | import org.jsoup.Jsoup 13 | import java.net.URLEncoder 14 | 15 | class MikudmSearchComponent( 16 | private val okhttpHelper: OkhttpHelper, 17 | private val mikudmUtil: MikudmUtil, 18 | ) : ComponentWrapper(), SearchComponent { 19 | override fun getFirstSearchKey(keyword: String): Int = 1 20 | 21 | override suspend fun search( 22 | pageKey: Int, 23 | keyword: String 24 | ): SourceResult>> = withResult(Dispatchers.IO) { 25 | val document = 26 | Jsoup.parse( 27 | mikudmUtil.getDocument( 28 | okhttpHelper, "/index.php/vod/search/page/$pageKey/wd/${ 29 | encodeUrlComponent( 30 | keyword 31 | ) 32 | }.html" 33 | ), 34 | mikudmUtil.BASE_URL 35 | ) 36 | val videos = document.select(".searchlist_item").map { videoElement -> 37 | val link = videoElement.selectFirst(".vodlist_thumb")!! 38 | val url = link.absUrl("href") 39 | val videoTitle = videoElement.selectFirst(".vodlist_title>a")!!.attr("title") 40 | val coverUrl = mikudmUtil.extractImageSrc(link) 41 | CartoonCoverImpl( 42 | id = url.substring(url.lastIndexOf('/') + 1, url.lastIndexOf('.')), 43 | source = source.key, 44 | url = url, 45 | title = videoTitle, 46 | coverUrl = coverUrl 47 | ) 48 | } 49 | val nextPage = 50 | if (mikudmUtil.hasNextPage(document) && videos.isNotEmpty()) pageKey + 1 else null 51 | nextPage to videos 52 | } 53 | 54 | private fun encodeUrlComponent(text: String): String = 55 | URLEncoder.encode(text, Charsets.UTF_8.name()) 56 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/peacefulprogram/easybangumi_mikudm/MikudmUtil.kt: -------------------------------------------------------------------------------- 1 | package io.github.peacefulprogram.easybangumi_mikudm 2 | 3 | import com.heyanle.easybangumi4.source_api.utils.api.OkhttpHelper 4 | import com.heyanle.easybangumi4.source_api.utils.api.PreferenceHelper 5 | import okhttp3.Request 6 | import org.jsoup.nodes.Document 7 | import org.jsoup.nodes.Element 8 | 9 | class MikudmUtil( 10 | private val preferenceHelper: PreferenceHelper 11 | ) { 12 | val BASE_URL:String 13 | get() = preferenceHelper.get("BaseUrl", "https://www.dmmiku.com").let { 14 | if (it.endsWith("/")){ 15 | it.substring(0, it.length - 1) 16 | }else{ 17 | it 18 | } 19 | } 20 | 21 | val BASE_M3U8_URL:String 22 | get() = preferenceHelper.get("BaseM3u8Url", "https://bf.mmiku.net").let { 23 | if (it.endsWith("/")){ 24 | it.substring(0, it.length - 1) 25 | }else{ 26 | it 27 | } 28 | } 29 | val USER_AGENT = 30 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36" 31 | 32 | fun getDocument(okhttpHelper: OkhttpHelper, url: String): String { 33 | val actualUrl = if (url.startsWith("http")) { 34 | url 35 | } else { 36 | BASE_URL + url 37 | } 38 | val req = Request.Builder() 39 | .header("user-agent", USER_AGENT) 40 | .header("referer", "$BASE_URL/") 41 | .url(actualUrl) 42 | .get() 43 | .build() 44 | return okhttpHelper.client.newCall(req).execute().body?.string() ?: throw RuntimeException( 45 | "响应为空:$url" 46 | ) 47 | } 48 | 49 | 50 | fun hasNextPage(document: Document): Boolean { 51 | val page = document.selectFirst(".page") ?: return false 52 | val currentIndex = page.children().indexOfFirst { it.hasClass("active") } 53 | return page.child(currentIndex + 1).getElementsByTag("a").attr("href") != page.child( 54 | currentIndex 55 | ).getElementsByTag("a").attr("href") 56 | } 57 | 58 | fun extractImageSrc(imageElement: Element): String { 59 | val img = imageElement.dataset()["original"] ?: "" 60 | if (img.isEmpty()) { 61 | return img 62 | } 63 | if (img.startsWith("http")) { 64 | return img 65 | } 66 | return BASE_URL + img 67 | } 68 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/peacefulprogram/easybangumi_mxdm/MxdmApiSource.kt: -------------------------------------------------------------------------------- 1 | package io.github.peacefulprogram.easybangumi_mxdm 2 | 3 | import com.heyanle.easybangumi4.source_api.Source 4 | import com.heyanle.extension_api.ExtensionIconSource 5 | import com.heyanle.extension_api.ExtensionSource 6 | import org.easybangumi.extension.R 7 | import kotlin.reflect.KClass 8 | 9 | class MxdmApiSource : Source, ExtensionIconSource { 10 | override val describe: String 11 | get() = label 12 | override val label: String 13 | get() = "MX动漫" 14 | override val version: String 15 | get() = "1.1" 16 | override val versionCode: Int 17 | get() = 2 18 | 19 | override fun getIconResourcesId(): Int = R.drawable.mxdm 20 | 21 | override val key: String 22 | get() = "io.github.peacefulprogram.easybangumi_mxdm-io.github.peacefulprogram.easybangumi_mxdm" 23 | 24 | override fun register(): List> { 25 | return listOf( 26 | MxdmPageComponent::class, 27 | MxdmSearchComponent::class, 28 | MxdmDetailComponent::class, 29 | MxdmPlayComponent::class, 30 | MxdmPreferenceComponent::class, 31 | MxdmUtil::class 32 | ) 33 | } 34 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/peacefulprogram/easybangumi_mxdm/MxdmDetailComponent.kt: -------------------------------------------------------------------------------- 1 | package io.github.peacefulprogram.easybangumi_mxdm 2 | 3 | import com.heyanle.easybangumi4.source_api.SourceResult 4 | import com.heyanle.easybangumi4.source_api.component.ComponentWrapper 5 | import com.heyanle.easybangumi4.source_api.component.detailed.DetailedComponent 6 | import com.heyanle.easybangumi4.source_api.entity.Cartoon 7 | import com.heyanle.easybangumi4.source_api.entity.CartoonImpl 8 | import com.heyanle.easybangumi4.source_api.entity.CartoonSummary 9 | import com.heyanle.easybangumi4.source_api.entity.Episode 10 | import com.heyanle.easybangumi4.source_api.entity.PlayLine 11 | import com.heyanle.easybangumi4.source_api.utils.api.OkhttpHelper 12 | import com.heyanle.easybangumi4.source_api.withResult 13 | import kotlinx.coroutines.Dispatchers 14 | import org.jsoup.Jsoup 15 | 16 | class MxdmDetailComponent( 17 | private val okhttpHelper: OkhttpHelper, 18 | private val mxdmUtil: MxdmUtil, 19 | ) : ComponentWrapper(), DetailedComponent { 20 | override suspend fun getAll(summary: CartoonSummary): SourceResult>> = 21 | withResult(Dispatchers.IO) { 22 | getVideoDetail(summary.id) 23 | } 24 | 25 | override suspend fun getDetailed(summary: CartoonSummary): SourceResult = 26 | withResult(Dispatchers.IO) { 27 | getVideoDetail(summary.id).first 28 | } 29 | 30 | override suspend fun getPlayLine(summary: CartoonSummary): SourceResult> = 31 | withResult(Dispatchers.IO) { 32 | getVideoDetail(summary.id).second 33 | } 34 | 35 | private fun getVideoDetail(videoId: String): Pair> { 36 | val document = 37 | Jsoup.parse(mxdmUtil.getDocument("/dongman/$videoId.html"), mxdmUtil.baseUrl) 38 | val videoTitle = document.selectFirst(".page-title")!!.text().trim() 39 | val tags = document.select(".video-info-aux a").joinToString(", ") { it.text().trim() } 40 | val desc = document.selectFirst(".video-info-content")?.text() 41 | val coverUrl = document.selectFirst(".module-item-pic img")?.run { 42 | mxdmUtil.extractImageSrc(this) 43 | } 44 | val cartoon = CartoonImpl( 45 | id = videoId, 46 | source = source.key, 47 | url = "${mxdmUtil.baseUrl}/dongman/$videoId.html", 48 | title = videoTitle, 49 | genre = tags, 50 | coverUrl = coverUrl, 51 | description = desc 52 | ) 53 | val playlistNames = document.select(".module-player-tab .module-tab-item").map { el -> 54 | if (el.childrenSize() > 0) { 55 | el.child(0).text().trim() 56 | } else { 57 | el.text().trim() 58 | } 59 | } 60 | val playLineList = mutableListOf() 61 | document.select(".module-player-list > .module-blocklist") 62 | .forEachIndexed { index, container -> 63 | if (index >= playlistNames.size) { 64 | return@forEachIndexed 65 | } 66 | val episodeElements = container.getElementsByTag("a") 67 | if (episodeElements.isEmpty()) { 68 | return@forEachIndexed 69 | } 70 | val playlistId = episodeElements[0].attr("href").split('-').run { 71 | this[size - 2] 72 | } 73 | val episodeNames = container.getElementsByTag("a").map { it.text().trim() } 74 | val ep = arrayListOf() 75 | for (s in episodeNames.indices) { 76 | val d = episodeNames[s] 77 | ep.add(Episode(s.toString(), d, s)) 78 | } 79 | playLineList.add( 80 | PlayLine( 81 | playlistId, 82 | playlistNames[index], 83 | ep 84 | ) 85 | ) 86 | } 87 | 88 | return cartoon to playLineList 89 | } 90 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/peacefulprogram/easybangumi_mxdm/MxdmPageComponent.kt: -------------------------------------------------------------------------------- 1 | package io.github.peacefulprogram.easybangumi_mxdm 2 | 3 | 4 | import android.util.Log 5 | import com.heyanle.easybangumi4.source_api.component.ComponentWrapper 6 | import com.heyanle.easybangumi4.source_api.component.page.PageComponent 7 | import com.heyanle.easybangumi4.source_api.component.page.SourcePage 8 | import com.heyanle.easybangumi4.source_api.entity.CartoonCover 9 | import com.heyanle.easybangumi4.source_api.entity.CartoonCoverImpl 10 | import com.heyanle.easybangumi4.source_api.utils.api.OkhttpHelper 11 | import com.heyanle.easybangumi4.source_api.withResult 12 | import kotlinx.coroutines.Dispatchers 13 | import org.jsoup.Jsoup 14 | import org.jsoup.nodes.Document 15 | import org.jsoup.nodes.Element 16 | 17 | class MxdmPageComponent( 18 | private val okhttpHelper: OkhttpHelper, 19 | private val mxdmUtil: MxdmUtil 20 | ) : ComponentWrapper(), PageComponent { 21 | override fun getPages(): List { 22 | val pages = mutableListOf() 23 | val homePage = SourcePage.Group("首页", false) { 24 | withResult(Dispatchers.IO) { 25 | parseHomePage(Jsoup.parse(mxdmUtil.getDocument( "/").apply { 26 | Log.i("MxdmPageComponent", this) 27 | }, mxdmUtil.baseUrl)) 28 | } 29 | } 30 | pages.add(homePage) 31 | val timeline = SourcePage.Group("更新时间表", false) { 32 | withResult(Dispatchers.IO) { 33 | parseTimeLine(Jsoup.parse(mxdmUtil.getDocument("/"), mxdmUtil.baseUrl)) 34 | } 35 | } 36 | pages.add(timeline) 37 | 38 | listOf( 39 | "riman" to "日本动漫", 40 | "guoman" to "国产动漫", 41 | "dmdianying" to "动漫电影", 42 | "oman" to "欧美动漫", 43 | ).forEach { (typeId, typeName) -> 44 | val page = SourcePage.SingleCartoonPage.WithCover(typeName, { 1 }) { page -> 45 | withResult(Dispatchers.IO) { 46 | buildPageOfType(typeId, page) 47 | } 48 | } 49 | pages.add(page) 50 | } 51 | 52 | return pages 53 | } 54 | 55 | private fun buildPageOfType(typeId: String, page: Int): Pair> { 56 | val document = Jsoup.parse( 57 | mxdmUtil.getDocument("/show/${typeId}--------${page}---.html"), 58 | mxdmUtil.baseUrl 59 | ) 60 | val videos = document.select(".content .module .module-item").map { it.parseToCartoon() } 61 | val nextPage = if (mxdmUtil.hasNextPage(document) && videos.isNotEmpty()) page + 1 else null 62 | return nextPage to videos 63 | } 64 | 65 | private fun parseTimeLine(document: Document): List { 66 | val tabs = document.selectFirst(".mxoneweek-tabs") ?: return emptyList() 67 | val result = mutableListOf() 68 | var activeTabIndex = 0 69 | val tabNames = tabs.children().mapIndexed { index, el -> 70 | if (el.hasClass("active")) { 71 | activeTabIndex = index 72 | } 73 | el.text().trim() 74 | } 75 | val videoGroups = document.select(".mxoneweek-list").map { el -> 76 | el.getElementsByTag("a").map { link -> 77 | val title = 78 | if (link.childrenSize() > 0) link.child(0).text().trim() else link.text().trim() 79 | val episodeText = if (link.childrenSize() > 1) link.child(1).text().trim() else "" 80 | val url = link.absUrl("href") 81 | val videoId = url.substring(url.lastIndexOf('/') + 1, url.lastIndexOf('.')) 82 | CartoonCoverImpl( 83 | id = videoId, 84 | source = source.key, 85 | url = "${mxdmUtil.baseUrl}/dongman/$videoId.html", 86 | title = title, 87 | intro = episodeText 88 | ) 89 | } 90 | 91 | } 92 | for (i in activeTabIndex until tabNames.size.coerceAtMost(videoGroups.size)) { 93 | val page = SourcePage.SingleCartoonPage.WithoutCover(tabNames[i], { 0 }) { 94 | withResult { 95 | null to videoGroups[i] 96 | } 97 | } 98 | result.add(page) 99 | } 100 | for (i in 0 until activeTabIndex) { 101 | val page = SourcePage.SingleCartoonPage.WithoutCover(tabNames[i], { 0 }) { 102 | withResult { 103 | null to videoGroups[i] 104 | } 105 | } 106 | result.add(page) 107 | } 108 | return result 109 | } 110 | 111 | private fun parseHomePage(document: Document): List { 112 | val result = mutableListOf() 113 | val contents = document.select(".content .module .module-list>.module-items").iterator() 114 | val titles = document.select(".content .module .module-title").iterator() 115 | while (contents.hasNext() && titles.hasNext()) { 116 | val contentEl = contents.next() 117 | val titleEl = titles.next() 118 | val videos = contentEl.select(".module-item") 119 | if (videos.isEmpty()) { 120 | continue 121 | } 122 | if (videos[0].classNames().size > 1) { 123 | continue 124 | } 125 | val cartoonList = videos.map { videoEl -> videoEl.parseToCartoon() } 126 | val page = SourcePage.SingleCartoonPage.WithCover(titleEl.text().trim(), { 0 }) { 127 | withResult { 128 | Pair(null, cartoonList) 129 | } 130 | } 131 | result.add(page) 132 | } 133 | return result 134 | } 135 | 136 | private fun Element.parseToCartoon(): CartoonCover { 137 | val coverUrl = mxdmUtil.extractImageSrc(this.selectFirst("img")!!) 138 | val linkEl = this.selectFirst("a")!! 139 | val url = linkEl.absUrl("href") 140 | val id = url.substring(url.lastIndexOf('/') + 1, url.lastIndexOf('.')) 141 | val videoTitle = this.selectFirst(".video-name")!!.text().trim() 142 | val tags = LinkedHashSet() 143 | val episode = this.selectFirst(".module-item-text")?.text()?.trim() ?: "" 144 | if (episode.isEmpty()) { 145 | tags.add(episode) 146 | } 147 | this.selectFirst(".module-item-caption")?.children()?.forEach { 148 | val text = it.text().trim() 149 | if (text.isNotEmpty()) { 150 | tags.add(text) 151 | } 152 | } 153 | return CartoonCoverImpl( 154 | id = id, 155 | source = source.key, 156 | url = url, 157 | title = videoTitle, 158 | intro = tags.joinToString(" | "), 159 | coverUrl = coverUrl 160 | ) 161 | } 162 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/peacefulprogram/easybangumi_mxdm/MxdmPlayComponent.kt: -------------------------------------------------------------------------------- 1 | package io.github.peacefulprogram.easybangumi_mxdm 2 | 3 | import android.util.Base64 4 | import com.google.gson.Gson 5 | import com.heyanle.easybangumi4.source_api.SourceResult 6 | import com.heyanle.easybangumi4.source_api.component.ComponentWrapper 7 | import com.heyanle.easybangumi4.source_api.component.play.PlayComponent 8 | import com.heyanle.easybangumi4.source_api.entity.CartoonSummary 9 | import com.heyanle.easybangumi4.source_api.entity.Episode 10 | import com.heyanle.easybangumi4.source_api.entity.PlayLine 11 | import com.heyanle.easybangumi4.source_api.entity.PlayerInfo 12 | import com.heyanle.easybangumi4.source_api.utils.api.OkhttpHelper 13 | import com.heyanle.easybangumi4.source_api.withResult 14 | import kotlinx.coroutines.Dispatchers 15 | import javax.crypto.Cipher 16 | import javax.crypto.spec.IvParameterSpec 17 | import javax.crypto.spec.SecretKeySpec 18 | 19 | class MxdmPlayComponent( 20 | private val okhttpHelper: OkhttpHelper, 21 | private val mxdmUtil: MxdmUtil, 22 | ) : ComponentWrapper(), PlayComponent { 23 | override suspend fun getPlayInfo( 24 | summary: CartoonSummary, 25 | playLine: PlayLine, 26 | episode: Episode, 27 | ): SourceResult = withResult(Dispatchers.IO) { 28 | val html = 29 | mxdmUtil.getDocument( "/dongmanplay/${summary.id}-${playLine.id}-${(episode.id.toIntOrNull()?:episode.order) + 1}.html") 30 | val newHtml = 31 | mxdmUtil.getDocument( "https://danmu.yhdmjx.com/m3u8.php?url=" + extractPlayerParam(html)) 32 | val btToken = extractBtToken(newHtml) 33 | val encryptedUrl = extractEncryptedUrl(newHtml) 34 | 35 | val plainUrl = Cipher.getInstance("AES/CBC/PKCS5Padding").run { 36 | init( 37 | Cipher.DECRYPT_MODE, 38 | SecretKeySpec("57A891D97E332A9D".toByteArray(), "AES"), 39 | IvParameterSpec(btToken.toByteArray(Charsets.UTF_8)) 40 | ) 41 | doFinal(Base64.decode(encryptedUrl, Base64.DEFAULT)) 42 | }.toString(Charsets.UTF_8) 43 | PlayerInfo(uri = plainUrl) 44 | } 45 | 46 | private fun extractPlayerParam(html: String): String { 47 | val startIndex = html.indexOf("player_aaaa") 48 | val startBracketIndex = html.indexOf('{', startIndex + 1) 49 | var endIndex = -1 50 | var bracketCount = 0 51 | for (i in (startBracketIndex + 1)..>( 67 | html.substring(startBracketIndex, endIndex + 1), 68 | Map::class.java 69 | )["url"] ?: throw RuntimeException("未获取到播放信息") 70 | } 71 | 72 | fun extractEncryptedUrl(html: String): String { 73 | val key = "getVideoInfo(" 74 | val i1 = html.indexOf(key) 75 | val i2 = html.indexOf('"', i1 + key.length) 76 | val i3 = html.indexOf('"', i2 + 1) 77 | return html.substring(i2 + 1, i3) 78 | } 79 | 80 | fun extractBtToken(html: String): String { 81 | val i1 = html.indexOf("bt_token") 82 | val i2 = html.indexOf('"', i1) 83 | val i3 = html.indexOf('"', i2 + 1) 84 | return html.substring(i2 + 1, i3) 85 | } 86 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/peacefulprogram/easybangumi_mxdm/MxdmPreferenceComponent.kt: -------------------------------------------------------------------------------- 1 | package io.github.peacefulprogram.easybangumi_mxdm 2 | 3 | import com.heyanle.easybangumi4.source_api.component.ComponentWrapper 4 | import com.heyanle.easybangumi4.source_api.component.play.PlayComponent 5 | import com.heyanle.easybangumi4.source_api.component.preference.PreferenceComponent 6 | import com.heyanle.easybangumi4.source_api.component.preference.SourcePreference 7 | 8 | /** 9 | * Created by heyanlin on 2024/5/22. 10 | */ 11 | class MxdmPreferenceComponent : ComponentWrapper(), PreferenceComponent { 12 | 13 | override fun register(): List = listOf( 14 | SourcePreference.Edit("MX 动漫网址", "BaseUrl", "https://www.mxdm.tv/"), 15 | ) 16 | 17 | override fun needMigrate(oldVersionCode: Int): Boolean { 18 | return false 19 | } 20 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/peacefulprogram/easybangumi_mxdm/MxdmSearchComponent.kt: -------------------------------------------------------------------------------- 1 | package io.github.peacefulprogram.easybangumi_mxdm 2 | 3 | import com.heyanle.easybangumi4.source_api.SourceResult 4 | import com.heyanle.easybangumi4.source_api.component.ComponentWrapper 5 | import com.heyanle.easybangumi4.source_api.component.search.SearchComponent 6 | import com.heyanle.easybangumi4.source_api.entity.CartoonCover 7 | import com.heyanle.easybangumi4.source_api.entity.CartoonCoverImpl 8 | import com.heyanle.easybangumi4.source_api.utils.api.OkhttpHelper 9 | import com.heyanle.easybangumi4.source_api.withResult 10 | import kotlinx.coroutines.Dispatchers 11 | import org.jsoup.Jsoup 12 | import java.net.URLEncoder 13 | 14 | class MxdmSearchComponent( 15 | private val okhttpHelper: OkhttpHelper, 16 | private val mxdmUtil: MxdmUtil 17 | ) : ComponentWrapper(), SearchComponent { 18 | override fun getFirstSearchKey(keyword: String): Int = 1 19 | 20 | override suspend fun search( 21 | pageKey: Int, 22 | keyword: String 23 | ): SourceResult>> = withResult(Dispatchers.IO) { 24 | val document = 25 | Jsoup.parse( 26 | mxdmUtil.getDocument( "/search/${encodeUrlComponent(keyword)}----------$pageKey---.html"), 27 | mxdmUtil.baseUrl 28 | ) 29 | val videos = document.select(".module-search-item").map { videoElement -> 30 | val link = videoElement.selectFirst("a")!! 31 | val url = link.absUrl("href") 32 | val videoTitle = videoElement.selectFirst("h3")!!.text() 33 | val coverUrl = mxdmUtil.extractImageSrc(videoElement.selectFirst("img")!!) 34 | val desc = videoElement.select(".video-info-item").last()?.text() 35 | val videoId = url.substring(url.lastIndexOf('/') + 1, url.lastIndexOf('.')) 36 | CartoonCoverImpl( 37 | id = url.substring(url.lastIndexOf('/') + 1, url.lastIndexOf('.')), 38 | source = source.key, 39 | url = "${mxdmUtil.baseUrl}/dongman/$videoId.html", 40 | title = videoTitle, 41 | intro = desc, 42 | coverUrl = coverUrl 43 | ) 44 | } 45 | val nextPage = 46 | if (mxdmUtil.hasNextPage(document) && videos.isNotEmpty()) pageKey + 1 else null 47 | nextPage to videos 48 | } 49 | 50 | private fun encodeUrlComponent(text: String): String = 51 | URLEncoder.encode(text, Charsets.UTF_8.name()) 52 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/peacefulprogram/easybangumi_mxdm/MxdmUtil.kt: -------------------------------------------------------------------------------- 1 | package io.github.peacefulprogram.easybangumi_mxdm 2 | 3 | import com.heyanle.easybangumi4.source_api.utils.api.NetworkHelper 4 | import com.heyanle.easybangumi4.source_api.utils.api.OkhttpHelper 5 | import com.heyanle.easybangumi4.source_api.utils.api.PreferenceHelper 6 | import okhttp3.Request 7 | import org.jsoup.nodes.Document 8 | import org.jsoup.nodes.Element 9 | 10 | class MxdmUtil( 11 | private val okhttpHelper: OkhttpHelper, 12 | private val networkHelper: NetworkHelper, 13 | private val preferenceHelper: PreferenceHelper 14 | ) { 15 | val baseUrl:String 16 | get() = preferenceHelper.get("BaseUrl", "https://www.mxdm.tv").let { 17 | if (it.endsWith("/")){ 18 | it.substring(0, it.length - 1) 19 | }else{ 20 | it 21 | } 22 | } 23 | val userAgent = 24 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36" 25 | 26 | fun getDocument(url: String): String { 27 | val actualUrl = if (url.startsWith("http")) { 28 | url 29 | } else { 30 | baseUrl + url 31 | } 32 | val req = Request.Builder() 33 | .header("user-agent", networkHelper.defaultAndroidUA) 34 | .url(actualUrl) 35 | .get() 36 | .build() 37 | return okhttpHelper.cloudflareWebViewClient.newCall(req).execute().body?.string() ?: throw RuntimeException( 38 | "响应为空:$url" 39 | ) 40 | } 41 | 42 | 43 | fun hasNextPage(document: Document): Boolean { 44 | val page = document.getElementById("page") ?: return false 45 | val currentPageIndex = page.children().indexOfFirst { it.hasClass("page-current") } 46 | return currentPageIndex != -1 && currentPageIndex < page.childrenSize() - 3 47 | } 48 | 49 | fun extractImageSrc(imageElement: Element): String { 50 | var img = imageElement.dataset()["src"] ?: "" 51 | if (img.isEmpty() && imageElement.hasAttr("src")) { 52 | img = imageElement.attr("src") 53 | } 54 | return img 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/peacefulprogram/easybangumi_nivod/NivodApiSource.kt: -------------------------------------------------------------------------------- 1 | package io.github.peacefulprogram.easybangumi_nivod 2 | 3 | import com.heyanle.easybangumi4.source_api.Source 4 | import com.heyanle.extension_api.ExtensionIconSource 5 | import com.heyanle.extension_api.ExtensionSource 6 | import org.easybangumi.extension.R 7 | import kotlin.reflect.KClass 8 | 9 | class NivodApiSource : Source, ExtensionIconSource { 10 | override val describe: String 11 | get() = "nivod.tv,科学上网" 12 | override val label: String 13 | get() = "泥视频" 14 | override val version: String 15 | get() = "2.0" 16 | override val versionCode: Int 17 | get() = 4 18 | 19 | override fun getIconResourcesId(): Int = R.drawable.nivod 20 | 21 | override fun register(): List> { 22 | return listOf( 23 | NivodPageComponent::class, 24 | NivodSearchComponent::class, 25 | NivodDetailComponent::class, 26 | NivodPlayComponent::class 27 | ) 28 | } 29 | 30 | override val key: String 31 | get() = "io.github.peacefulprogram.easybangumi_nivod-io.github.peacefulprogram.easybangumi_nivod" 32 | 33 | 34 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/peacefulprogram/easybangumi_nivod/NivodConstants.kt: -------------------------------------------------------------------------------- 1 | package io.github.peacefulprogram.easybangumi_nivod 2 | 3 | object NivodConstants { 4 | 5 | const val BASE_URL = "https://api.nivodz.com" 6 | 7 | const val WEBPAGE_URL = "https://www.nivod4.tv" 8 | 9 | const val REFERER = "$WEBPAGE_URL/" 10 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/peacefulprogram/easybangumi_nivod/NivodDetailComponent.kt: -------------------------------------------------------------------------------- 1 | package io.github.peacefulprogram.easybangumi_nivod 2 | 3 | import com.google.gson.Gson 4 | import com.heyanle.easybangumi4.source_api.SourceResult 5 | import com.heyanle.easybangumi4.source_api.component.ComponentWrapper 6 | import com.heyanle.easybangumi4.source_api.component.detailed.DetailedComponent 7 | import com.heyanle.easybangumi4.source_api.entity.Cartoon 8 | import com.heyanle.easybangumi4.source_api.entity.CartoonImpl 9 | import com.heyanle.easybangumi4.source_api.entity.CartoonSummary 10 | import com.heyanle.easybangumi4.source_api.entity.Episode 11 | import com.heyanle.easybangumi4.source_api.entity.PlayLine 12 | import com.heyanle.easybangumi4.source_api.utils.api.OkhttpHelper 13 | import com.heyanle.easybangumi4.source_api.withResult 14 | import io.github.peacefulprogram.easybangumi_nivod.dto.VideoDetailResponse 15 | import kotlinx.coroutines.Dispatchers 16 | 17 | class NivodDetailComponent( 18 | private val okhttpHelper: OkhttpHelper 19 | ) : ComponentWrapper(), 20 | DetailedComponent { 21 | val gson = Gson() 22 | override suspend fun getAll(summary: CartoonSummary): SourceResult>> { 23 | return withResult(Dispatchers.IO) { 24 | val detail = getVideoDetail(summary.id) 25 | Pair(detail.toCartoon(), getVideoDetail(summary.id).toPlayLine()) 26 | } 27 | } 28 | 29 | override suspend fun getDetailed(summary: CartoonSummary): SourceResult { 30 | return withResult(Dispatchers.IO) { 31 | getVideoDetail(summary.id).toCartoon() 32 | } 33 | } 34 | 35 | override suspend fun getPlayLine(summary: CartoonSummary): SourceResult> { 36 | return withResult(Dispatchers.IO) { 37 | getVideoDetail(summary.id).toPlayLine() 38 | } 39 | } 40 | 41 | private fun VideoDetailResponse.toPlayLine(): List { 42 | val ids = mutableListOf() 43 | val names = arrayListOf() 44 | 45 | entity.plays.forEachIndexed { i, it -> 46 | ids.add(it.playIdCode) 47 | names.add(Episode(it.playIdCode, it.episodeName, it.seq)) 48 | } 49 | return DetailedComponent.NonPlayLine( 50 | PlayLine( 51 | "播放列表", 52 | "泥视频", 53 | episode = names 54 | ) 55 | ) 56 | } 57 | 58 | 59 | private fun getVideoDetail(id: String): VideoDetailResponse { 60 | val req = NivodRequest( 61 | "/show/detail/WEB/3.2", 62 | body = mapOf("show_id_code" to id) 63 | ) 64 | return gson.fromJson( 65 | okhttpHelper.client.newCall(req).execute().decryptResponseBodyIfCan() 66 | ) 67 | } 68 | 69 | private fun VideoDetailResponse.toCartoon(): Cartoon = CartoonImpl( 70 | id = this.entity.showIdCode, 71 | source = source.key, 72 | url = "${NivodConstants.WEBPAGE_URL}/detail.html?showIdCode=${entity.showIdCode}", 73 | title = this.entity.showTitle, 74 | coverUrl = this.entity.showImg, 75 | description = this.entity.showDesc, 76 | genre = listOf( 77 | entity.showTypeName, 78 | entity.postYear.toString(), 79 | entity.episodesUpdateDesc 80 | ).filter { it.isNotBlank() }.joinToString(", ") 81 | ) 82 | 83 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/peacefulprogram/easybangumi_nivod/NivodPageComponent.kt: -------------------------------------------------------------------------------- 1 | package io.github.peacefulprogram.easybangumi_nivod 2 | 3 | import com.google.gson.Gson 4 | import com.heyanle.easybangumi4.source_api.Source 5 | import com.heyanle.easybangumi4.source_api.component.ComponentWrapper 6 | import com.heyanle.easybangumi4.source_api.component.page.PageComponent 7 | import com.heyanle.easybangumi4.source_api.component.page.SourcePage 8 | import com.heyanle.easybangumi4.source_api.entity.CartoonCoverImpl 9 | import com.heyanle.easybangumi4.source_api.utils.api.NetworkHelper 10 | import com.heyanle.easybangumi4.source_api.utils.api.OkhttpHelper 11 | import com.heyanle.easybangumi4.source_api.withResult 12 | 13 | import io.github.peacefulprogram.easybangumi_nivod.dto.ChannelRecommendResponse 14 | import kotlinx.coroutines.Dispatchers 15 | 16 | class NivodPageComponent( 17 | private val okhttpHelper: OkhttpHelper 18 | ) : PageComponent, ComponentWrapper() { 19 | 20 | val gson = Gson() 21 | 22 | override fun getPages(): List { 23 | val channels = listOf( 24 | Pair("首页", null), 25 | Pair("电影", 1), 26 | Pair("动漫", 4), 27 | Pair("电视剧", 2), 28 | Pair("综艺", 3), 29 | Pair("纪录片", 6) 30 | ) 31 | return channels.map { (channelName, channelId) -> 32 | SourcePage.Group(channelName, false) { 33 | withResult(Dispatchers.IO) { 34 | getChannelRecommend(channelId = channelId) 35 | } 36 | } 37 | } 38 | } 39 | 40 | private fun getChannelRecommend(channelId: Int?): List { 41 | val pages = mutableListOf() 42 | var start = 0 43 | val step = 6 44 | while (true) { 45 | val req = NivodRequest( 46 | "/index/desktop/WEB/3.4", 47 | body = NoEmptyValueMap( 48 | "channel_id" to channelId, 49 | "start" to start, 50 | "more" to "1" 51 | ) 52 | ) 53 | val resp = gson.fromJson( 54 | okhttpHelper.client.newCall(req).execute().decryptResponseBodyIfCan() 55 | ) 56 | if (resp.banners.isNotEmpty()) { 57 | SourcePage.SingleCartoonPage.WithCover("推荐", { 1 }) { 58 | withResult { 59 | null to resp.banners.filter { it.show != null }.map { banner -> 60 | CartoonCoverImpl( 61 | id = banner.show!!.showIdCode, 62 | source = source.key, 63 | url = "${NivodConstants.WEBPAGE_URL}/detail.html?showIdCode=${banner.show.showIdCode}", 64 | title = banner.show.showTitle, 65 | coverUrl = banner.show.showImg 66 | ) 67 | } 68 | } 69 | }.let { pages.add(it) } 70 | } 71 | resp.list.forEach { recommendRow -> 72 | SourcePage.SingleCartoonPage.WithCover(recommendRow.title, { 1 }) { 73 | withResult { 74 | null to recommendRow.rows.flatMap { it.cells }.map { video -> 75 | CartoonCoverImpl( 76 | id = video.show.showIdCode, 77 | source = source.key, 78 | url = "${NivodConstants.WEBPAGE_URL}/detail.html?showIdCode=${video.show.showIdCode}", 79 | title = video.show.showTitle, 80 | coverUrl = video.show.showImg 81 | ) 82 | } 83 | } 84 | }.let { pages.add(it) } 85 | } 86 | if (resp.more != 1) { 87 | break 88 | } 89 | start += step 90 | } 91 | 92 | return pages 93 | 94 | } 95 | 96 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/peacefulprogram/easybangumi_nivod/NivodPlayComponent.kt: -------------------------------------------------------------------------------- 1 | package io.github.peacefulprogram.easybangumi_nivod 2 | 3 | import com.google.gson.Gson 4 | import com.heyanle.easybangumi4.source_api.SourceResult 5 | import com.heyanle.easybangumi4.source_api.component.ComponentWrapper 6 | import com.heyanle.easybangumi4.source_api.component.play.PlayComponent 7 | import com.heyanle.easybangumi4.source_api.entity.CartoonSummary 8 | import com.heyanle.easybangumi4.source_api.entity.Episode 9 | import com.heyanle.easybangumi4.source_api.entity.PlayLine 10 | import com.heyanle.easybangumi4.source_api.entity.PlayerInfo 11 | import com.heyanle.easybangumi4.source_api.utils.api.OkhttpHelper 12 | import com.heyanle.easybangumi4.source_api.withResult 13 | 14 | import io.github.peacefulprogram.easybangumi_nivod.dto.VideoStreamUrlResponse 15 | import kotlinx.coroutines.Dispatchers 16 | 17 | class NivodPlayComponent( 18 | private val okhttpHelper: OkhttpHelper, 19 | ) : ComponentWrapper(), PlayComponent { 20 | val gson = Gson() 21 | override suspend fun getPlayInfo( 22 | summary: CartoonSummary, 23 | playLine: PlayLine, 24 | episode: Episode 25 | ): SourceResult { 26 | return withResult(Dispatchers.IO) { 27 | val episodeId = episode.id 28 | val req = NivodRequest( 29 | "/show/play/info/WEB/3.2", 30 | body = mapOf( 31 | "show_id_code" to summary.id, 32 | "play_id_code" to episodeId 33 | ) 34 | ) 35 | val resp = gson.fromJson( 36 | okhttpHelper.client.newCall(req).execute().decryptResponseBodyIfCan() 37 | ) 38 | PlayerInfo( 39 | decodeType = PlayerInfo.DECODE_TYPE_HLS, 40 | uri = resp.entity.playUrl 41 | ) 42 | } 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/peacefulprogram/easybangumi_nivod/NivodSearchComponent.kt: -------------------------------------------------------------------------------- 1 | package io.github.peacefulprogram.easybangumi_nivod 2 | 3 | import com.google.gson.Gson 4 | import com.heyanle.easybangumi4.source_api.SourceResult 5 | import com.heyanle.easybangumi4.source_api.component.ComponentWrapper 6 | import com.heyanle.easybangumi4.source_api.component.search.SearchComponent 7 | import com.heyanle.easybangumi4.source_api.entity.CartoonCover 8 | import com.heyanle.easybangumi4.source_api.entity.CartoonCoverImpl 9 | import com.heyanle.easybangumi4.source_api.utils.api.OkhttpHelper 10 | import com.heyanle.easybangumi4.source_api.withResult 11 | import io.github.peacefulprogram.easybangumi_nivod.dto.SearchVideoResponse 12 | import kotlinx.coroutines.Dispatchers 13 | 14 | class NivodSearchComponent( 15 | private val okhttpHelper: OkhttpHelper, 16 | ) : ComponentWrapper(), SearchComponent { 17 | private val pageSize = 20 18 | private val gson = Gson() 19 | override fun getFirstSearchKey(keyword: String): Int { 20 | return 0 21 | } 22 | 23 | override suspend fun search( 24 | pageKey: Int, 25 | keyword: String 26 | ): SourceResult>> { 27 | return withResult(Dispatchers.IO) { 28 | val req = NivodRequest( 29 | "/show/search/WEB/3.2", 30 | body = mapOf( 31 | "keyword" to keyword, 32 | "start" to pageKey.toString(), 33 | "cat_id" to "1", 34 | "keyword_type" to "0" 35 | ) 36 | ) 37 | val resp = gson.fromJson( 38 | okhttpHelper.client.newCall(req).execute().decryptResponseBodyIfCan() 39 | ) 40 | val nextPageKey = if (resp.more == 1) pageKey + pageSize else null 41 | val videos = resp.list.map { video -> 42 | CartoonCoverImpl( 43 | id = video.showIdCode, 44 | source = source.key, 45 | url = "${NivodConstants.WEBPAGE_URL}/detail.html?showIdCode=${video.showIdCode}", 46 | title = video.showTitle, 47 | coverUrl = video.showImg, 48 | intro = video.episodesTxt 49 | ) 50 | } 51 | Pair(nextPageKey, videos) 52 | } 53 | 54 | } 55 | 56 | 57 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/peacefulprogram/easybangumi_nivod/Util.kt: -------------------------------------------------------------------------------- 1 | package io.github.peacefulprogram.easybangumi_nivod 2 | 3 | import com.google.gson.Gson 4 | import com.google.gson.reflect.TypeToken 5 | import okhttp3.FormBody 6 | import okhttp3.HttpUrl 7 | import okhttp3.HttpUrl.Companion.toHttpUrl 8 | import okhttp3.Request 9 | import okhttp3.Response 10 | import java.security.MessageDigest 11 | import javax.crypto.Cipher 12 | import javax.crypto.spec.SecretKeySpec 13 | 14 | fun Response.decryptResponseBodyIfCan(): String { 15 | if (code == 403) { 16 | throw RuntimeException("请求被禁止,请科学上网后重试") 17 | } 18 | val encryptedText = body?.string() ?: throw RuntimeException("响应体为空") 19 | val cipherText = (encryptedText).decodeHexString() 20 | val key = "diao.com".toByteArray() 21 | return Cipher.getInstance("DES/ECB/PKCS5Padding").run { 22 | init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "DES")) 23 | doFinal(cipherText) 24 | }.toString(Charsets.UTF_8) 25 | } 26 | 27 | private fun String.decodeHexString(): ByteArray = 28 | this.chunked(2).map { it.toInt(16).toByte() }.toByteArray() 29 | 30 | @OptIn(ExperimentalStdlibApi::class) 31 | private fun createSign(queryParams: Map, body: Map): String { 32 | val prefixes = arrayOf("__QUERY::", "__BODY::") 33 | val params = arrayOf(queryParams, body) 34 | val str = StringBuilder() 35 | for (i in prefixes.indices) { 36 | val param = params[i] 37 | str.append(prefixes[i]) 38 | param.keys.toList().sorted().filter { 39 | it.isNotEmpty() && param[it]?.isNotEmpty() == true 40 | }.forEach { 41 | str.append(it) 42 | str.append('=') 43 | str.append(param[it]) 44 | str.append('&') 45 | } 46 | } 47 | str.append("__KEY::2x_Give_it_a_shot") 48 | return MessageDigest.getInstance("MD5").digest(str.toString().toByteArray(Charsets.UTF_8)) 49 | .toHexString() 50 | } 51 | 52 | private fun HttpUrl.Builder.withSign( 53 | body: Map = emptyMap(), 54 | queryParams: Map = emptyMap() 55 | ): HttpUrl.Builder { 56 | val allQueryParams = mutableMapOf( 57 | "_ts" to System.currentTimeMillis().toString(), 58 | "app_version" to "1.0", 59 | "platform" to "3", 60 | "market_id" to "web_nivod", 61 | "device_code" to "web", 62 | "versioncode" to "1", 63 | "oid" to "8ca275aa5e12ba504b266d4c70d95d77a0c2eac5726198ea" 64 | ).apply { 65 | putAll(queryParams) 66 | } 67 | allQueryParams.forEach { (name, value) -> 68 | addQueryParameter(name, value) 69 | } 70 | addQueryParameter("sign", createSign(allQueryParams, body = body)) 71 | return this 72 | } 73 | 74 | 75 | fun NoEmptyValueMap(vararg pairs: Pair): Map { 76 | val result = 77 | pairs.asSequence() 78 | .filter { (k, v) -> k.isNotEmpty() && v != null && (v !is String || v.isNotEmpty()) } 79 | .map { (k, v) -> k to v.toString() } 80 | .toList() 81 | .toTypedArray() 82 | return mapOf(*result) 83 | } 84 | 85 | inline fun Gson.fromJson(json: String): T = fromJson(json, object : TypeToken() {}) 86 | 87 | 88 | fun NivodRequest( 89 | url: String, 90 | body: Map = emptyMap(), 91 | queryParams: Map = emptyMap() 92 | ): Request { 93 | val url = (NivodConstants.BASE_URL + url).toHttpUrl() 94 | .newBuilder() 95 | .withSign(body, queryParams) 96 | .build() 97 | val formBody = FormBody.Builder() 98 | .apply { 99 | body.entries.forEach { (name, value) -> 100 | add(name, value) 101 | } 102 | }.build() 103 | return Request.Builder() 104 | .header("referer", NivodConstants.REFERER) 105 | .header( 106 | "user-agent", 107 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36" 108 | ) 109 | .post(formBody) 110 | .url(url) 111 | .build() 112 | } -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/peacefulprogram/easybangumi_nivod/dto/ChannelRecommendResponse.kt: -------------------------------------------------------------------------------- 1 | package io.github.peacefulprogram.easybangumi_nivod.dto 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class ChannelRecommendResponse( 6 | @SerializedName("banners") 7 | val banners: List, 8 | @SerializedName("list") 9 | val list: List, 10 | @SerializedName("more") 11 | val more: Int, 12 | @SerializedName("msg") 13 | val msg: String, 14 | @SerializedName("start") 15 | val start: String, 16 | @SerializedName("status") 17 | val status: Int 18 | ) 19 | 20 | data class ChannelRecommendBanner( 21 | @SerializedName("id") 22 | val id: Int, 23 | @SerializedName("imageUrl") 24 | val imageUrl: String, 25 | @SerializedName("seq") 26 | val seq: Int, 27 | @SerializedName("show") 28 | val show: ChannelRecommendShow? = null, 29 | @SerializedName("title") 30 | val title: String 31 | ) 32 | 33 | data class ChannelRecommend( 34 | @SerializedName("blockId") 35 | val blockId: Int, 36 | @SerializedName("blockType") 37 | val blockType: Int, 38 | @SerializedName("channelId") 39 | val channelId: Int, 40 | @SerializedName("layout") 41 | val layout: Int, 42 | @SerializedName("rows") 43 | val rows: List, 44 | @SerializedName("srcChannelId") 45 | val srcChannelId: Int, 46 | @SerializedName("title") 47 | val title: String 48 | ) 49 | 50 | data class ChannelRecommendShow( 51 | @SerializedName("actors") 52 | val actors: String, 53 | @SerializedName("addDate") 54 | val addDate: Long, 55 | @SerializedName("catId") 56 | val catId: Int, 57 | @SerializedName("channelId") 58 | val channelId: Int, 59 | @SerializedName("channelName") 60 | val channelName: String, 61 | @SerializedName("commentCount") 62 | val commentCount: Int, 63 | @SerializedName("director") 64 | val director: String, 65 | @SerializedName("episodesTxt") 66 | val episodesTxt: String, 67 | @SerializedName("favoriteCount") 68 | val favoriteCount: Int, 69 | @SerializedName("hot") 70 | val hot: Int, 71 | @SerializedName("inSeries") 72 | val inSeries: Int, 73 | @SerializedName("isEpisodes") 74 | val isEpisodes: Int, 75 | @SerializedName("isEpisodesEnd") 76 | val isEpisodesEnd: Int, 77 | @SerializedName("pageBgImg") 78 | val pageBgImg: String, 79 | @SerializedName("playLangs") 80 | val playLangs: List, 81 | @SerializedName("playResolutions") 82 | val playResolutions: List, 83 | @SerializedName("playSources") 84 | val playSources: List, 85 | @SerializedName("postYear") 86 | val postYear: Int, 87 | @SerializedName("rating") 88 | val rating: Int, 89 | @SerializedName("regionId") 90 | val regionId: Int, 91 | @SerializedName("regionName") 92 | val regionName: String, 93 | @SerializedName("shareCount") 94 | val shareCount: Int, 95 | @SerializedName("shareForced") 96 | val shareForced: Int, 97 | @SerializedName("showId") 98 | val showId: Int, 99 | @SerializedName("showIdCode") 100 | val showIdCode: String, 101 | @SerializedName("showImg") 102 | val showImg: String, 103 | @SerializedName("showTcTitle") 104 | val showTcTitle: String, 105 | @SerializedName("showTitle") 106 | val showTitle: String, 107 | @SerializedName("showTypeId") 108 | val showTypeId: Int, 109 | @SerializedName("showTypeName") 110 | val showTypeName: String = "", 111 | @SerializedName("status") 112 | val status: Int, 113 | @SerializedName("titleImg") 114 | val titleImg: String, 115 | @SerializedName("voteDown") 116 | val voteDown: Int, 117 | @SerializedName("voteUp") 118 | val voteUp: Int 119 | ) 120 | 121 | 122 | data class ChannelRecommendRow( 123 | @SerializedName("blockId") 124 | val blockId: Int, 125 | @SerializedName("cells") 126 | val cells: List, 127 | @SerializedName("overflow") 128 | val overflow: String, 129 | @SerializedName("rowId") 130 | val rowId: Int, 131 | @SerializedName("type") 132 | val type: Int 133 | ) 134 | 135 | data class ChannelRecommendCell( 136 | @SerializedName("bottomRightText") 137 | val bottomRightText: String, 138 | @SerializedName("cellId") 139 | val cellId: Int, 140 | @SerializedName("img") 141 | val img: String, 142 | @SerializedName("intro") 143 | val intro: String = "", 144 | @SerializedName("rowId") 145 | val rowId: Int, 146 | @SerializedName("show") 147 | val show: ChannelRecommendShow, 148 | @SerializedName("title") 149 | val title: String 150 | ) 151 | -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/peacefulprogram/easybangumi_nivod/dto/Common.kt: -------------------------------------------------------------------------------- 1 | package io.github.peacefulprogram.easybangumi_nivod.dto 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | 6 | data class VideoPlayLang( 7 | @SerializedName("langId") 8 | val langId: Int, 9 | @SerializedName("langName") 10 | val langName: String 11 | ) 12 | 13 | 14 | data class VideoPlaySource( 15 | @SerializedName("sourceId") 16 | val sourceId: Int, 17 | @SerializedName("sourceName") 18 | val sourceName: String 19 | ) -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/peacefulprogram/easybangumi_nivod/dto/SearchVideoResponse.kt: -------------------------------------------------------------------------------- 1 | package io.github.peacefulprogram.easybangumi_nivod.dto 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | 6 | data class SearchVideoResponse( 7 | @SerializedName("list") 8 | val list: List, 9 | @SerializedName("more") 10 | val more: Int, 11 | @SerializedName("msg") 12 | val msg: String, 13 | @SerializedName("start") 14 | val start: String, 15 | @SerializedName("status") 16 | val status: Int 17 | ) 18 | 19 | data class SearchVideo( 20 | @SerializedName("actors") 21 | val actors: String = "", 22 | @SerializedName("addDate") 23 | val addDate: Long, 24 | @SerializedName("catId") 25 | val catId: Int, 26 | @SerializedName("channelId") 27 | val channelId: Int, 28 | @SerializedName("channelName") 29 | val channelName: String, 30 | @SerializedName("commentCount") 31 | val commentCount: Int, 32 | @SerializedName("director") 33 | val director: String = "", 34 | @SerializedName("episodesTxt") 35 | val episodesTxt: String = "", 36 | @SerializedName("favoriteCount") 37 | val favoriteCount: Int, 38 | @SerializedName("hot") 39 | val hot: Int, 40 | @SerializedName("inSeries") 41 | val inSeries: Int, 42 | @SerializedName("isEpisodes") 43 | val isEpisodes: Int, 44 | @SerializedName("isEpisodesEnd") 45 | val isEpisodesEnd: Int, 46 | @SerializedName("playLangs") 47 | val playLangs: List, 48 | @SerializedName("playResolutions") 49 | val playResolutions: List, 50 | @SerializedName("playSources") 51 | val playSources: List, 52 | @SerializedName("postYear") 53 | val postYear: Int, 54 | @SerializedName("rating") 55 | val rating: Int, 56 | @SerializedName("regionId") 57 | val regionId: Int, 58 | @SerializedName("regionName") 59 | val regionName: String, 60 | @SerializedName("shareCount") 61 | val shareCount: Int, 62 | @SerializedName("shareForced") 63 | val shareForced: Int, 64 | @SerializedName("showId") 65 | val showId: Int, 66 | @SerializedName("showIdCode") 67 | val showIdCode: String, 68 | @SerializedName("showImg") 69 | val showImg: String, 70 | @SerializedName("showTcTitle") 71 | val showTcTitle: String, 72 | @SerializedName("showTitle") 73 | val showTitle: String, 74 | @SerializedName("showTypeId") 75 | val showTypeId: Int, 76 | @SerializedName("showTypeName") 77 | val showTypeName: String = "", 78 | @SerializedName("status") 79 | val status: Int, 80 | @SerializedName("voteDown") 81 | val voteDown: Int, 82 | @SerializedName("voteUp") 83 | val voteUp: Int 84 | ) -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/peacefulprogram/easybangumi_nivod/dto/VideoDetailResponse.kt: -------------------------------------------------------------------------------- 1 | package io.github.peacefulprogram.easybangumi_nivod.dto 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class VideoDetailResponse( 6 | @SerializedName("entity") 7 | val entity: VideoDetailEntity, 8 | @SerializedName("msg") 9 | val msg: String, 10 | @SerializedName("status") 11 | val status: Int 12 | ) 13 | 14 | data class VideoDetailEntity( 15 | @SerializedName("actors") 16 | val actors: String, 17 | @SerializedName("addDate") 18 | val addDate: Long, 19 | @SerializedName("catId") 20 | val catId: Int, 21 | @SerializedName("channelId") 22 | val channelId: Int, 23 | @SerializedName("channelName") 24 | val channelName: String, 25 | @SerializedName("commentCount") 26 | val commentCount: Int, 27 | @SerializedName("director") 28 | val director: String, 29 | @SerializedName("episodesTxt") 30 | val episodesTxt: String, 31 | @SerializedName("episodesUpdateDesc") 32 | val episodesUpdateDesc: String, 33 | @SerializedName("episodesUpdateRemark") 34 | val episodesUpdateRemark: String, 35 | @SerializedName("favoriteCount") 36 | val favoriteCount: Int, 37 | @SerializedName("forceApp") 38 | val forceApp: String, 39 | @SerializedName("hot") 40 | val hot: Int, 41 | @SerializedName("inSeries") 42 | val inSeries: Int, 43 | @SerializedName("isEpisodes") 44 | val isEpisodes: Int, 45 | @SerializedName("isEpisodesEnd") 46 | val isEpisodesEnd: Int, 47 | @SerializedName("pageBgImg") 48 | val pageBgImg: String, 49 | @SerializedName("playLangs") 50 | val playLangs: List, 51 | @SerializedName("playResolutions") 52 | val playResolutions: List, 53 | @SerializedName("playSources") 54 | val playSources: List, 55 | @SerializedName("plays") 56 | val plays: List, 57 | @SerializedName("postYear") 58 | val postYear: Int, 59 | @SerializedName("rating") 60 | val rating: Int, 61 | @SerializedName("regionId") 62 | val regionId: Int, 63 | @SerializedName("regionName") 64 | val regionName: String, 65 | @SerializedName("shareCount") 66 | val shareCount: Int, 67 | @SerializedName("shareForced") 68 | val shareForced: Int, 69 | @SerializedName("shareTxt") 70 | val shareTxt: String, 71 | @SerializedName("shareUrl") 72 | val shareUrl: String, 73 | @SerializedName("showDesc") 74 | val showDesc: String, 75 | @SerializedName("showId") 76 | val showId: Int, 77 | @SerializedName("showIdCode") 78 | val showIdCode: String, 79 | @SerializedName("showImg") 80 | val showImg: String, 81 | @SerializedName("showTcTitle") 82 | val showTcTitle: String, 83 | @SerializedName("showTitle") 84 | val showTitle: String, 85 | @SerializedName("showTypeId") 86 | val showTypeId: Int, 87 | @SerializedName("showTypeName") 88 | val showTypeName: String = "", 89 | @SerializedName("status") 90 | val status: Int, 91 | @SerializedName("titleImg") 92 | val titleImg: String, 93 | @SerializedName("voteDown") 94 | val voteDown: Int, 95 | @SerializedName("voteUp") 96 | val voteUp: Int 97 | ) 98 | 99 | data class VideoDetailPlay( 100 | @SerializedName("displayName") 101 | val displayName: String, 102 | @SerializedName("episodeId") 103 | val episodeId: Int, 104 | @SerializedName("episodeName") 105 | val episodeName: String, 106 | @SerializedName("external") 107 | val `external`: Int, 108 | @SerializedName("langId") 109 | val langId: Int, 110 | @SerializedName("playIdCode") 111 | val playIdCode: String, 112 | @SerializedName("resolution") 113 | val resolution: String, 114 | @SerializedName("resolutionInt") 115 | val resolutionInt: Int, 116 | @SerializedName("seq") 117 | val seq: Int, 118 | @SerializedName("size") 119 | val size: Long, 120 | @SerializedName("sourceId") 121 | val sourceId: Int 122 | ) -------------------------------------------------------------------------------- /extension-app/src/main/java/io/github/peacefulprogram/easybangumi_nivod/dto/VideoStreamUrlResponse.kt: -------------------------------------------------------------------------------- 1 | package io.github.peacefulprogram.easybangumi_nivod.dto 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class VideoStreamUrlResponse( 6 | @SerializedName("entity") 7 | val entity: VideoStreamUrlEntity, 8 | @SerializedName("msg") 9 | val msg: String, 10 | @SerializedName("status") 11 | val status: Int 12 | ) 13 | 14 | data class VideoStreamUrlEntity( 15 | @SerializedName("playType") 16 | val playType: Int, 17 | @SerializedName("playUrl") 18 | val playUrl: String 19 | ) -------------------------------------------------------------------------------- /extension-app/src/main/java/org/easybangumi/extension/EasySourceFactory.kt: -------------------------------------------------------------------------------- 1 | package org.easybangumi.extension 2 | 3 | import com.heyanle.easybangumi4.source_api.Source 4 | import com.heyanle.easybangumi4.source_api.SourceFactory 5 | import com.heyanle.easybangumi_extension.anfun.AnfunSource 6 | import com.heyanle.easybangumi_extension.anim.AnimOneSource 7 | import com.heyanle.easybangumi_extension.ggl.GGLSource 8 | import io.github.easybangumiorg.source.aio.auete.AueteSource 9 | import io.github.easybangumiorg.source.aio.changzhang.ChangZhangSource 10 | import io.github.easybangumiorg.source.aio.fengche.FengCheSource 11 | import io.github.easybangumiorg.source.aio.libvio.LibVioSource 12 | import io.github.easybangumiorg.source.aio.xigua.XiguaSource 13 | import io.github.peacefulprogram.easybangumi_mikudm.MikudmApiSource 14 | import io.github.peacefulprogram.easybangumi_mxdm.MxdmApiSource 15 | import io.github.peacefulprogram.easybangumi_nivod.NivodApiSource 16 | 17 | 18 | /** 19 | * Created by HeYanLe on 2023/2/19 23:23. 20 | * https://github.com/heyanLE 21 | */ 22 | class EasySourceFactory: SourceFactory { 23 | 24 | override fun create(): List { 25 | return listOf( 26 | AnimOneSource(), 27 | LibVioSource(), 28 | AueteSource(), 29 | FengCheSource(), 30 | XiguaSource(), 31 | MikudmApiSource(), 32 | MxdmApiSource(), 33 | NivodApiSource(), 34 | GGLSource(), 35 | AnfunSource(), 36 | ChangZhangSource(), 37 | ) 38 | } 39 | } -------------------------------------------------------------------------------- /extension-app/src/main/res/drawable/anfun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easybangumiorg/CommunityExtension/4140b6849f425a8aad670418818363b51d436503/extension-app/src/main/res/drawable/anfun.png -------------------------------------------------------------------------------- /extension-app/src/main/res/drawable/anim1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easybangumiorg/CommunityExtension/4140b6849f425a8aad670418818363b51d436503/extension-app/src/main/res/drawable/anim1.png -------------------------------------------------------------------------------- /extension-app/src/main/res/drawable/auete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easybangumiorg/CommunityExtension/4140b6849f425a8aad670418818363b51d436503/extension-app/src/main/res/drawable/auete.png -------------------------------------------------------------------------------- /extension-app/src/main/res/drawable/changzhang.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easybangumiorg/CommunityExtension/4140b6849f425a8aad670418818363b51d436503/extension-app/src/main/res/drawable/changzhang.png -------------------------------------------------------------------------------- /extension-app/src/main/res/drawable/cycplus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easybangumiorg/CommunityExtension/4140b6849f425a8aad670418818363b51d436503/extension-app/src/main/res/drawable/cycplus.png -------------------------------------------------------------------------------- /extension-app/src/main/res/drawable/fengche.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easybangumiorg/CommunityExtension/4140b6849f425a8aad670418818363b51d436503/extension-app/src/main/res/drawable/fengche.png -------------------------------------------------------------------------------- /extension-app/src/main/res/drawable/ggl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easybangumiorg/CommunityExtension/4140b6849f425a8aad670418818363b51d436503/extension-app/src/main/res/drawable/ggl.png -------------------------------------------------------------------------------- /extension-app/src/main/res/drawable/libvio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easybangumiorg/CommunityExtension/4140b6849f425a8aad670418818363b51d436503/extension-app/src/main/res/drawable/libvio.png -------------------------------------------------------------------------------- /extension-app/src/main/res/drawable/mikudm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easybangumiorg/CommunityExtension/4140b6849f425a8aad670418818363b51d436503/extension-app/src/main/res/drawable/mikudm.png -------------------------------------------------------------------------------- /extension-app/src/main/res/drawable/mxdm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easybangumiorg/CommunityExtension/4140b6849f425a8aad670418818363b51d436503/extension-app/src/main/res/drawable/mxdm.png -------------------------------------------------------------------------------- /extension-app/src/main/res/drawable/nivod.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easybangumiorg/CommunityExtension/4140b6849f425a8aad670418818363b51d436503/extension-app/src/main/res/drawable/nivod.png -------------------------------------------------------------------------------- /extension-app/src/main/res/drawable/xigua.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easybangumiorg/CommunityExtension/4140b6849f425a8aad670418818363b51d436503/extension-app/src/main/res/drawable/xigua.png -------------------------------------------------------------------------------- /extension-app/src/main/res/drawable/yhdm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easybangumiorg/CommunityExtension/4140b6849f425a8aad670418818363b51d436503/extension-app/src/main/res/drawable/yhdm.png -------------------------------------------------------------------------------- /extension-app/src/main/res/mipmap-xxhdpi/app_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easybangumiorg/CommunityExtension/4140b6849f425a8aad670418818363b51d436503/extension-app/src/main/res/mipmap-xxhdpi/app_logo.png -------------------------------------------------------------------------------- /extension-app/src/main/res/mipmap-xxhdpi/logo_new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easybangumiorg/CommunityExtension/4140b6849f425a8aad670418818363b51d436503/extension-app/src/main/res/mipmap-xxhdpi/logo_new.png -------------------------------------------------------------------------------- /extension-app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 纯纯看番:AllInOne 3 | -------------------------------------------------------------------------------- /extension-app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /extension-app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easybangumiorg/CommunityExtension/4140b6849f425a8aad670418818363b51d436503/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun Dec 10 21:18:58 HKT 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /headline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easybangumiorg/CommunityExtension/4140b6849f425a8aad670418818363b51d436503/headline.png -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | gradlePluginPortal() 6 | } 7 | } 8 | dependencyResolutionManagement { 9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 10 | repositories { 11 | google() 12 | mavenCentral() 13 | maven { url = uri("https://s01.oss.sonatype.org/content/repositories/snapshots/") } 14 | maven { url = uri("https://jitpack.io") } 15 | } 16 | } 17 | rootProject.name = "EasyBangumi-Extension" 18 | include(":extension-app") 19 | --------------------------------------------------------------------------------