├── .gitattributes
├── .github
└── workflows
│ ├── android_ci.yml
│ └── release.yml
├── .gitignore
├── LICENSE
├── README.md
├── app
├── .gitignore
├── build.gradle.kts
├── proguard-rules.pro
├── schemas
│ ├── com.lanlinju.animius.data.local.database.AnimeDatabase
│ │ └── 3.json
│ └── com.sakura.anime.data.local.database.AnimeDatabase
│ │ ├── 1.json
│ │ ├── 2.json
│ │ └── 3.json
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── lanlinju
│ │ └── animius
│ │ ├── ExampleInstrumentedTest.kt
│ │ ├── UpdateVersionTest.kt
│ │ ├── dandanplay
│ │ └── DandanplayDanmakuProviderTest.kt
│ │ └── database_test
│ │ ├── DownloadDaoTest.kt
│ │ ├── DownloadDetailTest.kt
│ │ ├── EpisodeDaoTest.kt
│ │ ├── FavouriteDaoTest.kt
│ │ └── HistoryDaoTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── ic_launcher-playstore.png
│ ├── java
│ │ └── com
│ │ │ └── lanlinju
│ │ │ └── animius
│ │ │ ├── MainActivity.kt
│ │ │ ├── application
│ │ │ └── AnimeApplication.kt
│ │ │ ├── data
│ │ │ ├── local
│ │ │ │ ├── dao
│ │ │ │ │ ├── DownloadDao.kt
│ │ │ │ │ ├── DownloadDetailDao.kt
│ │ │ │ │ ├── EpisodeDao.kt
│ │ │ │ │ ├── FavouriteDao.kt
│ │ │ │ │ └── HistoryDao.kt
│ │ │ │ ├── database
│ │ │ │ │ └── AnimeDatabase.kt
│ │ │ │ ├── entity
│ │ │ │ │ ├── DownloadDetailEntity.kt
│ │ │ │ │ ├── DownloadEntity.kt
│ │ │ │ │ ├── EpisodeEntity.kt
│ │ │ │ │ ├── FavouriteEntity.kt
│ │ │ │ │ └── HistoryEntity.kt
│ │ │ │ └── relation
│ │ │ │ │ ├── DownloadWithDownloadDetails.kt
│ │ │ │ │ └── HistoryWithEpisodes.kt
│ │ │ ├── remote
│ │ │ │ ├── api
│ │ │ │ │ ├── AnimeApi.kt
│ │ │ │ │ └── AnimeApiImpl.kt
│ │ │ │ ├── dandanplay
│ │ │ │ │ ├── DandanplayClient.kt
│ │ │ │ │ ├── DandanplayDanmakuProvider.kt
│ │ │ │ │ └── dto
│ │ │ │ │ │ ├── DandanplayDanmaku.kt
│ │ │ │ │ │ └── DandanplaySearchEpisodeResponse.kt
│ │ │ │ ├── dto
│ │ │ │ │ ├── AnimeBean.kt
│ │ │ │ │ ├── AnimeDetailBean.kt
│ │ │ │ │ ├── EpisodeBean.kt
│ │ │ │ │ ├── HomeBean.kt
│ │ │ │ │ └── VideoBean.kt
│ │ │ │ └── parse
│ │ │ │ │ ├── AgedmSource.kt
│ │ │ │ │ ├── AnfunsSource.kt
│ │ │ │ │ ├── AnimeSource.kt
│ │ │ │ │ ├── CycanimeSource.kt
│ │ │ │ │ ├── GirigiriSource.kt
│ │ │ │ │ ├── GogoanimeSource.kt
│ │ │ │ │ ├── MxdmSource.kt
│ │ │ │ │ ├── NyafunSource.kt
│ │ │ │ │ ├── SilisiliSource.kt
│ │ │ │ │ ├── YhdmSource.kt
│ │ │ │ │ └── util
│ │ │ │ │ └── WebViewUtil.kt
│ │ │ └── repository
│ │ │ │ ├── AnimeRepositoryImpl.kt
│ │ │ │ ├── DanmakuRepositoryImpl.kt
│ │ │ │ ├── RoomRepositoryImpl.kt
│ │ │ │ └── paging
│ │ │ │ └── SearchPagingSource.kt
│ │ │ ├── di
│ │ │ ├── ApiModule.kt
│ │ │ └── AppModule.kt
│ │ │ ├── domain
│ │ │ ├── model
│ │ │ │ ├── Anime.kt
│ │ │ │ ├── AnimeDetail.kt
│ │ │ │ ├── Download.kt
│ │ │ │ ├── DownloadDetail.kt
│ │ │ │ ├── Episode.kt
│ │ │ │ ├── Favourite.kt
│ │ │ │ ├── History.kt
│ │ │ │ ├── Home.kt
│ │ │ │ ├── Video.kt
│ │ │ │ └── WebVideo.kt
│ │ │ ├── repository
│ │ │ │ ├── AnimeRepository.kt
│ │ │ │ ├── DanmakuRepository.kt
│ │ │ │ └── RoomRepository.kt
│ │ │ └── usecase
│ │ │ │ └── GetAnimeDetailUseCase.kt
│ │ │ ├── presentation
│ │ │ ├── component
│ │ │ │ ├── BackTopAppBar.kt
│ │ │ │ ├── Forward85.kt
│ │ │ │ ├── LoadingIndicator.kt
│ │ │ │ ├── MediaSmall.kt
│ │ │ │ ├── NavigationBar.kt
│ │ │ │ ├── PaginationStateHandler.kt
│ │ │ │ ├── PopupMenuListItem.kt
│ │ │ │ ├── ScrollableText.kt
│ │ │ │ ├── SourceBadge.kt
│ │ │ │ ├── StateHandler.kt
│ │ │ │ ├── TranslucentStatusBarLayout.kt
│ │ │ │ └── WarningMessage.kt
│ │ │ ├── navigation
│ │ │ │ ├── AnimeNavHost.kt
│ │ │ │ └── Screen.kt
│ │ │ ├── screen
│ │ │ │ ├── crash
│ │ │ │ │ ├── CrashActivity.kt
│ │ │ │ │ └── CrashScreen.kt
│ │ │ │ ├── detail
│ │ │ │ │ ├── AnimeDetailScreen.kt
│ │ │ │ │ └── AnimeDetailViewModel.kt
│ │ │ │ ├── download
│ │ │ │ │ ├── DownloadScreen.kt
│ │ │ │ │ └── DownloadViewModel.kt
│ │ │ │ ├── downloaddetail
│ │ │ │ │ ├── DownloadDetailScreen.kt
│ │ │ │ │ └── DownloadDetailViewModel.kt
│ │ │ │ ├── favourite
│ │ │ │ │ ├── FavouriteScreen.kt
│ │ │ │ │ └── FavouriteViewModel.kt
│ │ │ │ ├── history
│ │ │ │ │ ├── HistoryScreen.kt
│ │ │ │ │ └── HistoryViewModel.kt
│ │ │ │ ├── home
│ │ │ │ │ ├── HomeScreen.kt
│ │ │ │ │ └── HomeViewModel.kt
│ │ │ │ ├── main
│ │ │ │ │ ├── MainScreen.kt
│ │ │ │ │ └── MainViewModel.kt
│ │ │ │ ├── search
│ │ │ │ │ ├── SearchScreen.kt
│ │ │ │ │ └── SearchViewModel.kt
│ │ │ │ ├── settings
│ │ │ │ │ ├── AppearanceScreen.kt
│ │ │ │ │ └── DanmakuSettingsScreen.kt
│ │ │ │ ├── videoplayer
│ │ │ │ │ ├── VideoPlayerScreen.kt
│ │ │ │ │ └── VideoPlayerViewModel.kt
│ │ │ │ └── week
│ │ │ │ │ ├── WeekScreen.kt
│ │ │ │ │ └── WeekViewModel.kt
│ │ │ └── theme
│ │ │ │ ├── Color.kt
│ │ │ │ ├── Theme.kt
│ │ │ │ └── Type.kt
│ │ │ ├── util
│ │ │ ├── Const.kt
│ │ │ ├── DownloadManager.kt
│ │ │ ├── HttpClientExt.kt
│ │ │ ├── ModifierExt.kt
│ │ │ ├── Preferences.kt
│ │ │ ├── Resource.kt
│ │ │ ├── Result.kt
│ │ │ ├── SettingsPreferences.kt
│ │ │ ├── SourceHolder.kt
│ │ │ ├── ThemeUtil.kt
│ │ │ └── Util.kt
│ │ │ └── work
│ │ │ └── UpdateWorker.kt
│ └── res
│ │ ├── drawable
│ │ ├── background.png
│ │ ├── home.xml
│ │ ├── ic_brightness.xml
│ │ ├── ic_content_paste.xml
│ │ ├── ic_domain.xml
│ │ ├── ic_github.png
│ │ ├── ic_history.xml
│ │ ├── ic_launcher_background.xml
│ │ ├── ic_launcher_foreground.xml
│ │ ├── ic_volume_mute.xml
│ │ ├── ic_volume_up.xml
│ │ ├── manga.xml
│ │ ├── rslash.xml
│ │ └── search.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher_foreground.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher_foreground.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher_foreground.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher_foreground.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher_foreground.webp
│ │ └── ic_launcher_round.webp
│ │ ├── values
│ │ ├── colors.xml
│ │ ├── dimens.xml
│ │ ├── ic_launcher_background.xml
│ │ ├── strings.xml
│ │ └── themes.xml
│ │ └── xml
│ │ ├── backup_rules.xml
│ │ ├── data_extraction_rules.xml
│ │ └── file_paths.xml
│ └── test
│ └── java
│ └── com
│ └── lanlinju
│ └── animius
│ ├── dandanplay
│ ├── DandanplayClientTest.kt
│ ├── MoviePatternTest.kt
│ └── SignatureGeneratorTest.kt
│ ├── parse
│ ├── cycanime
│ │ ├── CycanimeSourceTest.kt
│ │ └── html
│ │ │ ├── detail.html
│ │ │ ├── home.html
│ │ │ ├── player.html
│ │ │ └── search.html
│ ├── gogoanime
│ │ ├── GogoanimeTest.kt
│ │ └── html
│ │ │ ├── detail.html
│ │ │ ├── home.html
│ │ │ ├── player.html
│ │ │ ├── search.html
│ │ │ └── week.html
│ └── nyafun
│ │ ├── NyafunSourceTest.kt
│ │ ├── detail.html
│ │ ├── home.html
│ │ ├── player.html
│ │ └── search.html
│ ├── serializer
│ └── SerializerTest.kt
│ └── source
│ └── YhdmSourceTest.kt
├── build.gradle.kts
├── danmaku
├── .gitignore
├── build.gradle.kts
├── consumer-rules.pro
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── anime
│ │ └── danmaku
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ └── java
│ │ └── com
│ │ └── anime
│ │ └── danmaku
│ │ ├── api
│ │ ├── Danmaku.kt
│ │ └── DanmakuSession.kt
│ │ └── ui
│ │ ├── DanmakuConfig.kt
│ │ ├── DanmakuHost.kt
│ │ ├── DanmakuHostState.kt
│ │ ├── DanmakuTrack.kt
│ │ ├── FixedDanmakuTrack.kt
│ │ ├── FloatingDanmakuTrack.kt
│ │ ├── StyledDanmaku.kt
│ │ └── Util.kt
│ └── test
│ └── java
│ └── com
│ └── anime
│ └── danmaku
│ └── TimeBasedDanmakuSessionTest.kt
├── download
├── .gitignore
├── build.gradle.kts
├── consumer-rules.pro
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ └── java
│ └── com
│ └── lanlinju
│ └── download
│ ├── Download.kt
│ ├── Progress.kt
│ ├── State.kt
│ ├── core
│ ├── DownloadConfig.kt
│ ├── DownloadParam.kt
│ ├── DownloadQueue.kt
│ ├── DownloadTask.kt
│ ├── Downloader.kt
│ ├── Extensions.kt
│ ├── M3u8Downloader.kt
│ ├── M3u8Parser.kt
│ ├── NormalDownloader.kt
│ ├── RangeDownloader.kt
│ ├── RangeTmpFile.kt
│ └── TaskManager.kt
│ ├── helper
│ ├── Default.kt
│ └── Request.kt
│ └── utils
│ ├── FileUtils.kt
│ ├── HttpUtil.kt
│ ├── LogUtil.kt
│ └── Util.kt
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── image
├── README.md
├── background.png
├── detail.jpg
├── download_episode.jpg
├── download_list.jpg
├── favourite.jpg
├── history.jpg
├── home.jpg
├── icon.jpeg
├── img.jpg
├── miku_img.jpg
├── player.jpg
├── search.jpg
└── week.jpg
├── settings.gradle.kts
└── video-player
├── .gitignore
├── build.gradle.kts
├── consumer-rules.pro
├── proguard-rules.pro
└── src
└── main
├── AndroidManifest.xml
├── java
└── com
│ └── lanlinju
│ └── videoplayer
│ ├── Utils.kt
│ ├── VideoPlayer.kt
│ ├── VideoPlayerControl.kt
│ ├── VideoPlayerSate.kt
│ ├── component
│ └── Slider.kt
│ └── icons
│ ├── ArrowBackIos.kt
│ ├── Fullscreen.kt
│ ├── FullscreenExit.kt
│ ├── Pause.kt
│ ├── PlayArrow.kt
│ ├── Subtitles.kt
│ └── SubtitlesOff.kt
└── res
└── drawable
└── ic_next.xml
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.html linguist-vendored
--------------------------------------------------------------------------------
/.github/workflows/android_ci.yml:
--------------------------------------------------------------------------------
1 | name: Android CI
2 |
3 | on:
4 | workflow_dispatch:
5 | pull_request:
6 | push:
7 | branches:
8 | - 'main'
9 | - 'feat-compat-tv'
10 |
11 | jobs:
12 | build:
13 | runs-on: ubuntu-latest
14 | timeout-minutes: 30 # 设置超时时间为30分钟
15 |
16 | steps:
17 | # Checkout the code
18 | - name: Checkout code
19 | uses: actions/checkout@v4
20 |
21 | # Set up JDK 17
22 | - name: Set up JDK 17
23 | uses: actions/setup-java@v4
24 | with:
25 | distribution: 'temurin'
26 | java-version: '17'
27 | cache: 'gradle'
28 |
29 | - name: Set up Android SDK
30 | uses: android-actions/setup-android@v3
31 |
32 | - name: Set up Gradle
33 | uses: gradle/actions/setup-gradle@v4
34 |
35 | # Build the Debug APK
36 | - name: Build APK
37 | run: |
38 | chmod +x ./gradlew
39 | ./gradlew assembleDebug
40 | env:
41 | DANDANPLAY_APP_ID: ${{ secrets.DANDANPLAY_APP_ID }}
42 | DANDANPLAY_APP_SECRET: ${{ secrets.DANDANPLAY_APP_SECRET }}
43 |
44 | # Upload APK as artifact
45 | - name: Upload Signed APK
46 | uses: actions/upload-artifact@v4
47 | with:
48 | name: pre-release
49 | path: app/build/outputs/apk/debug/*.apk
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Build Release APK
2 |
3 | on:
4 | workflow_dispatch:
5 | push:
6 | tags: [ 'v*' ]
7 |
8 | jobs:
9 | release:
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | # Checkout the code
14 | - name: Checkout code
15 | uses: actions/checkout@v4
16 |
17 | # Set up JDK 17
18 | - name: Set up JDK 17
19 | uses: actions/setup-java@v4
20 | with:
21 | distribution: 'temurin'
22 | java-version: '17'
23 | cache: 'gradle'
24 |
25 | - name: Set up Android SDK
26 | uses: android-actions/setup-android@v3
27 |
28 | - name: Set up Gradle
29 | uses: gradle/actions/setup-gradle@v4
30 |
31 | # Decode and write the signing key from secrets
32 | - name: Decode signing key
33 | run: echo "${{ secrets.SIGNING_KEY_BASE64 }}" | base64 -di > keystore.jks
34 |
35 | # Build the APK
36 | - name: Build APK
37 | run: |
38 | chmod +x ./gradlew
39 | ./gradlew assembleRelease
40 | env:
41 | KEY_STORE_PASSWORD: ${{ secrets.KEY_STORE_PASSWORD }}
42 | KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
43 | KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
44 | DANDANPLAY_APP_ID: ${{ secrets.DANDANPLAY_APP_ID }}
45 | DANDANPLAY_APP_SECRET: ${{ secrets.DANDANPLAY_APP_SECRET }}
46 |
47 | # Sign the APK (Already handled by Gradle)
48 | # Upload APK as artifact
49 | - name: Upload Signed APK
50 | uses: actions/upload-artifact@v4
51 | with:
52 | name: app-release
53 | path: app/build/outputs/apk/release/*.apk
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | .idea/
5 | .DS_Store
6 | /build
7 | /captures
8 | .externalNativeBuild
9 | .cxx
10 | local.properties
11 | release/
12 | /.kotlin
13 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Animius
2 |
3 | 一个简洁的播放动漫的App,支持下载,弹幕,多数据源等功能,使用[Jetpack Compose](https://developer.android.com/jetpack?hl=zh-cn)
4 | 进行开发
5 |
6 | ## 如何下载安装
7 |
8 | 点击此链接[下载地址](https://github.com/Lanlinju/Anime/releases/latest)
9 | 前往下载页面,然后选择下载以`.apk`结尾的文件。Android
10 | TV或者系统版本低于安卓8.0的,请点击查看[这里](https://github.com/lanlinju/Anime/releases/tag/v1.2.1)
11 |
12 | ## 应用截图
13 |
14 |
15 |
16 |  |
17 |  |
18 |  |
19 |
20 |
21 |  |
22 |  |
23 |  |
24 |
25 |
26 |  |
27 |
28 |
29 |
30 | ## 相关功能
31 |
32 |
33 | - [x] 首页推荐
34 | - [x] 番剧搜索
35 | - [x] 番剧时间表
36 | - [x] 多数据源支持
37 | - [x] 历史记录
38 | - [x] 番剧下载
39 | - [x] 番剧收藏
40 | - [x] 动态主题颜色
41 | - [x] 视频播放器
42 | - [x] 倍速播放
43 | - [x] 外部播放器播放
44 | - [ ] 选择下载目录
45 | - [x] 弹幕功能
46 | - [ ] BT资源下载
47 | - [ ] 多平台 (Compose Multiplatform)
48 |
49 | ## Architecture
50 |
51 | 使用的是[Google应用架构指南](https://developer.android.com/topic/architecture), MVVM 和 Clean
52 | Architecture
53 |
54 | ## 参考来源
55 |
56 | 视频弹幕源来自于[弹弹play](https://www.dandanplay.com)开放API
57 |
58 | - [SakuraAnime](https://github.com/670848654/SakuraAnime):樱花动漫网站数据解析参考实现来源
59 | - [Animite](https://github.com/imashnake0/Animite):应用UI设计参考实现来源
60 | - [compose-video-player](https://github.com/imherrera/compose-video-player):Exoplayer视频播放器封装参考实现来源
61 | - [FreeToPlay](https://github.com/qababadr/FreeToPlay):应用MVVM架构参考实现来源
62 | - [DownloadX](https://github.com/ssseasonnn/DownloadX):视频文件下载功能参考实现来源
63 | - [Animeko](https://github.com/open-ani/animeko):视频弹幕功能参考实现来源
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/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 | # Please add these rules to your existing keep rules in order to suppress warnings.
23 | # This is generated automatically by the Android Gradle plugin.
24 | -dontwarn org.bouncycastle.jsse.BCSSLParameters
25 | -dontwarn org.bouncycastle.jsse.BCSSLSocket
26 | -dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider
27 | -dontwarn org.conscrypt.Conscrypt$Version
28 | -dontwarn org.conscrypt.Conscrypt
29 | -dontwarn org.conscrypt.ConscryptHostnameVerifier
30 | -dontwarn org.openjsse.javax.net.ssl.SSLParameters
31 | -dontwarn org.openjsse.javax.net.ssl.SSLSocket
32 | -dontwarn org.openjsse.net.ssl.OpenJSSE
33 |
34 | # 保留 SourceMode 枚举类及其成员
35 | -keep enum com.lanlinju.animius.util.SourceMode { *; }
36 |
37 | # https://developer.android.com/build/shrink-code?utm_source=android-studio&hl=zh-cn#retracing
38 | # 对堆栈轨迹进行轨迹还原
39 | -keepattributes LineNumberTable,SourceFile
40 | -renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/lanlinju/animius/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.lanlinju.animius
2 |
3 | import androidx.test.ext.junit.runners.AndroidJUnit4
4 | import androidx.test.platform.app.InstrumentationRegistry
5 | import org.junit.Assert.*
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | /**
10 | * Instrumented test, which will execute on an Android device.
11 | *
12 | * See [testing documentation](http://d.android.com/tools/testing).
13 | */
14 | @RunWith(AndroidJUnit4::class)
15 | class ExampleInstrumentedTest {
16 | @Test
17 | fun useAppContext() {
18 | // Context of the app under test.
19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
20 | assertEquals("com.sakura.anime", appContext.packageName)
21 | }
22 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/lanlinju/animius/UpdateVersionTest.kt:
--------------------------------------------------------------------------------
1 | package com.lanlinju.animius
2 |
3 | import androidx.test.ext.junit.runners.AndroidJUnit4
4 | import androidx.test.platform.app.InstrumentationRegistry
5 | import com.lanlinju.animius.util.DownloadManager
6 | import kotlinx.coroutines.runBlocking
7 | import org.json.JSONObject
8 | import org.junit.Assert
9 | import org.junit.Test
10 | import org.junit.runner.RunWith
11 |
12 | @RunWith(AndroidJUnit4::class)
13 | class UpdateVersionTest {
14 | val UPDATE_ADDRESS = "https://api.github.com/repos/Lanlinju/Anime/releases/latest"
15 |
16 | @Test
17 | fun checkUpdate(): Unit = runBlocking {
18 | val json = DownloadManager.getHtml(UPDATE_ADDRESS)
19 | val obj = JSONObject(json)
20 | val latestVersionName = obj.getString("name")
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | val curVersionName = appContext.packageManager
23 | .getPackageInfo(appContext.packageName, 0).versionName
24 | val downloadUrl = obj.getJSONArray("assets").getJSONObject(0).getString("browser_download_url")
25 | println(downloadUrl)
26 | println(obj.getString("body"))
27 | Assert.assertEquals(curVersionName, latestVersionName)
28 | }
29 |
30 | @Test
31 | fun getVersionName() {
32 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
33 | val versionName = appContext.packageManager
34 | .getPackageInfo(appContext.packageName, 0).versionName
35 | println("v$versionName")
36 | }
37 |
38 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/lanlinju/animius/dandanplay/DandanplayDanmakuProviderTest.kt:
--------------------------------------------------------------------------------
1 | package com.lanlinju.animius.dandanplay
2 |
3 | import com.lanlinju.animius.data.remote.dandanplay.DandanplayDanmakuProviderFactory
4 | import kotlinx.coroutines.runBlocking
5 | import org.junit.Assert.assertNotNull
6 | import org.junit.Test
7 |
8 | class DandanplayDanmakuProviderTest {
9 | private val provider = DandanplayDanmakuProviderFactory().create()
10 |
11 | @Test
12 | fun testFetchTVDanmakuSession() = runBlocking {
13 |
14 | val session = provider.fetch("海贼王", "第11话")
15 |
16 | assertNotNull(session)
17 | }
18 |
19 | @Test
20 | fun testFetchMovieDanmakuSession() = runBlocking {
21 |
22 | val session = provider.fetch("紫罗兰永恒花园 剧场版", "全集")
23 |
24 | assertNotNull(session)
25 | }
26 |
27 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/lanlinju/animius/database_test/DownloadDaoTest.kt:
--------------------------------------------------------------------------------
1 | package com.lanlinju.animius.database_test
2 |
3 | import android.content.Context
4 | import androidx.room.Room
5 | import androidx.test.core.app.ApplicationProvider
6 | import androidx.test.ext.junit.runners.AndroidJUnit4
7 | import com.lanlinju.animius.data.local.dao.DownloadDao
8 | import com.lanlinju.animius.data.local.database.AnimeDatabase
9 | import com.lanlinju.animius.data.local.entity.DownloadEntity
10 | import com.lanlinju.animius.util.SourceMode
11 | import kotlinx.coroutines.flow.first
12 | import kotlinx.coroutines.runBlocking
13 | import org.junit.After
14 | import org.junit.Assert.*
15 | import org.junit.Before
16 | import org.junit.Test
17 | import org.junit.runner.RunWith
18 | import java.io.IOException
19 |
20 | @RunWith(AndroidJUnit4::class)
21 | class DownloadDaoTest {
22 | private lateinit var downloadDao: DownloadDao
23 | private lateinit var animeDatabase: AnimeDatabase
24 |
25 | private var download1 =
26 | DownloadEntity(1, "海贼王1", "/detailUrl1", "/imgUrl1", SourceMode.Yhdm.name, System.currentTimeMillis())
27 | private var download2 =
28 | DownloadEntity(2, "海贼王2", "/detailUrl2", "/imgUrl2", SourceMode.Yhdm.name, System.currentTimeMillis())
29 |
30 | @Before
31 | fun createDb() {
32 | val context: Context = ApplicationProvider.getApplicationContext()
33 | animeDatabase = Room.inMemoryDatabaseBuilder(context, AnimeDatabase::class.java)
34 | .allowMainThreadQueries()
35 | .build()
36 | downloadDao = animeDatabase.downLoadDao()
37 | }
38 |
39 | @After
40 | @Throws(IOException::class)
41 | fun closeDb() {
42 | animeDatabase.close()
43 | }
44 |
45 | @Test
46 | @Throws(Exception::class)
47 | fun daoInsert_insertsHistoryIntoDB() = runBlocking {
48 | addOneDownloadToDb()
49 | val download = downloadDao.getDownload(download1.detailUrl).first()
50 | assertEquals(download.detailUrl, download1.detailUrl)
51 | }
52 |
53 | @Test
54 | fun daoCheckDownload_returnsNotNullFromDB() = runBlocking {
55 | val download = downloadDao.checkDownload(download1.detailUrl).first()
56 | assertNull(download)
57 | }
58 |
59 | @Test
60 | fun daoCheckDownload_returnsNullFromDB() = runBlocking {
61 | addOneDownloadToDb()
62 | val download = downloadDao.checkDownload(download1.detailUrl).first()
63 | assertNotNull(download)
64 | }
65 |
66 | @Test
67 | fun daoDeleteOneDownload_deleteOneDownloadFromDB() = runBlocking {
68 | addOneDownloadToDb()
69 | downloadDao.deleteDownload(download1.detailUrl)
70 | val actual = downloadDao.checkDownload(download1.detailUrl).first()
71 | assertNull(actual)
72 | }
73 |
74 | private suspend fun addOneDownloadToDb() {
75 | downloadDao.insertDownload(download1)
76 | }
77 |
78 | private suspend fun addTwoDownloadsToDb() {
79 | downloadDao.insertDownload(download1)
80 | downloadDao.insertDownload(download2)
81 | }
82 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
20 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
37 |
38 |
43 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lanlinju/Animius/f92df8fee26cdf39c8643a668c364cf996f172f8/app/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/app/src/main/java/com/lanlinju/animius/application/AnimeApplication.kt:
--------------------------------------------------------------------------------
1 | package com.lanlinju.animius.application
2 |
3 | import android.app.Application
4 | import android.content.Context
5 | import dagger.hilt.android.HiltAndroidApp
6 |
7 | @HiltAndroidApp
8 | class AnimeApplication : Application() {
9 |
10 | override fun onCreate() {
11 | super.onCreate()
12 |
13 | _instance = this
14 |
15 | }
16 |
17 | companion object {
18 | private lateinit var _instance: Application
19 |
20 | fun getInstance(): Context {
21 | return _instance
22 | }
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/lanlinju/animius/data/local/dao/DownloadDao.kt:
--------------------------------------------------------------------------------
1 | package com.lanlinju.animius.data.local.dao
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Insert
5 | import androidx.room.OnConflictStrategy
6 | import androidx.room.Query
7 | import androidx.room.Transaction
8 | import androidx.room.Update
9 | import com.lanlinju.animius.data.local.entity.DownloadEntity
10 | import com.lanlinju.animius.data.local.relation.DownloadWithDownloadDetails
11 | import com.lanlinju.animius.util.DOWNLOAD_TABLE
12 | import kotlinx.coroutines.flow.Flow
13 |
14 | @Dao
15 | interface DownloadDao {
16 |
17 | @Transaction
18 | @Query("SELECT * FROM $DOWNLOAD_TABLE ORDER BY created_at DESC")
19 | fun getDownloads(): Flow>
20 |
21 | @Transaction
22 | @Query("SELECT * FROM $DOWNLOAD_TABLE WHERE detail_url=:detailUrl")
23 | fun getDownloadWithDownloadDetails(detailUrl: String): Flow
24 |
25 | @Insert(onConflict = OnConflictStrategy.REPLACE)
26 | suspend fun insertDownload(downloadEntity: DownloadEntity): Long
27 |
28 | @Query("DELETE FROM $DOWNLOAD_TABLE WHERE detail_url=:detailUrl")
29 | suspend fun deleteDownload(detailUrl: String)
30 |
31 | @Update
32 | suspend fun updateDownload(downloadEntity: DownloadEntity)
33 |
34 | @Query("SELECT * FROM $DOWNLOAD_TABLE WHERE detail_url=:detailUrl")
35 | fun getDownload(detailUrl: String): Flow
36 |
37 | @Query("SELECT * FROM $DOWNLOAD_TABLE WHERE detail_url=:detailUrl")
38 | fun checkDownload(detailUrl: String): Flow
39 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/lanlinju/animius/data/local/dao/DownloadDetailDao.kt:
--------------------------------------------------------------------------------
1 | package com.lanlinju.animius.data.local.dao
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Insert
5 | import androidx.room.OnConflictStrategy
6 | import androidx.room.Query
7 | import androidx.room.Update
8 | import com.lanlinju.animius.data.local.entity.DownloadDetailEntity
9 | import com.lanlinju.animius.util.DOWNLOAD_DETAIL_TABLE
10 | import kotlinx.coroutines.flow.Flow
11 |
12 | @Dao
13 | interface DownloadDetailDao {
14 | @Query("SELECT * FROM $DOWNLOAD_DETAIL_TABLE WHERE download_id=:downloadId ORDER BY drama_number ASC")
15 | fun getDownloadDetails(downloadId: Long): Flow>
16 |
17 | @Query("SELECT * FROM $DOWNLOAD_DETAIL_TABLE WHERE download_url=:downloadUrl")
18 | fun getDownloadDetail(downloadUrl: String): Flow
19 |
20 | @Insert(onConflict = OnConflictStrategy.IGNORE)
21 | suspend fun insertDownloadDetail(downloadDetailEntity: DownloadDetailEntity)
22 |
23 | @Query("DELETE FROM $DOWNLOAD_DETAIL_TABLE WHERE download_url=:downloadUrl")
24 | suspend fun deleteDownloadDetail(downloadUrl: String)
25 |
26 | @Update
27 | suspend fun updateDownloadDetail(downloadDetailEntity: DownloadDetailEntity)
28 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/lanlinju/animius/data/local/dao/EpisodeDao.kt:
--------------------------------------------------------------------------------
1 | package com.lanlinju.animius.data.local.dao
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Insert
5 | import androidx.room.OnConflictStrategy
6 | import androidx.room.Query
7 | import androidx.room.Update
8 | import com.lanlinju.animius.data.local.entity.EpisodeEntity
9 | import com.lanlinju.animius.util.EPISODE_TABLE
10 | import kotlinx.coroutines.flow.Flow
11 |
12 | @Dao
13 | interface EpisodeDao {
14 |
15 | @Query("SELECT * FROM $EPISODE_TABLE WHERE history_id=:historyId ORDER BY created_at DESC")
16 | fun getEpisodes(historyId: Long): Flow>
17 |
18 | @Query("SELECT * FROM $EPISODE_TABLE WHERE episode_url=:episodeUrl")
19 | fun getEpisode(episodeUrl: String): Flow
20 |
21 | @Query("SELECT * FROM $EPISODE_TABLE WHERE episode_url=:episodeUrl")
22 | fun checkEpisode(episodeUrl: String): Flow
23 |
24 | @Insert(onConflict = OnConflictStrategy.REPLACE)
25 | suspend fun insertEpisodes(episodes: List)
26 |
27 | @Insert(onConflict = OnConflictStrategy.REPLACE)
28 | suspend fun insertEpisode(episode: EpisodeEntity)
29 |
30 | @Query("DELETE FROM $EPISODE_TABLE")
31 | suspend fun deleteAll()
32 |
33 | @Update
34 | suspend fun updateEpisodes(episodes: List)
35 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/lanlinju/animius/data/local/dao/FavouriteDao.kt:
--------------------------------------------------------------------------------
1 | package com.lanlinju.animius.data.local.dao
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Delete
5 | import androidx.room.Insert
6 | import androidx.room.OnConflictStrategy
7 | import androidx.room.Query
8 | import com.lanlinju.animius.data.local.entity.FavouriteEntity
9 | import com.lanlinju.animius.util.FAVOURITE_TABLE
10 | import kotlinx.coroutines.flow.Flow
11 |
12 | @Dao
13 | interface FavouriteDao {
14 | @Query("SELECT * FROM $FAVOURITE_TABLE ORDER BY created_at DESC")
15 | fun getAllFavourites(): Flow>
16 |
17 | @Query("SELECT * FROM $FAVOURITE_TABLE WHERE detail_url = :detailUrl")
18 | fun getFavouriteByDetailUrl(detailUrl: String): Flow
19 |
20 | @Query("SELECT * FROM $FAVOURITE_TABLE WHERE detail_url = :detailUrl")
21 | fun checkFavourite(detailUrl: String): Flow
22 |
23 | @Delete
24 | suspend fun deleteFavourite(favourite: FavouriteEntity)
25 |
26 | @Query("DELETE FROM $FAVOURITE_TABLE WHERE detail_url = :detailUrl")
27 | suspend fun deleteFavouriteByDetailUrl(detailUrl: String)
28 |
29 | @Insert(onConflict = OnConflictStrategy.REPLACE)
30 | suspend fun insertFavourite(favourite: FavouriteEntity)
31 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/lanlinju/animius/data/local/dao/HistoryDao.kt:
--------------------------------------------------------------------------------
1 | package com.lanlinju.animius.data.local.dao
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Insert
5 | import androidx.room.OnConflictStrategy
6 | import androidx.room.Query
7 | import androidx.room.Transaction
8 | import androidx.room.Update
9 | import com.lanlinju.animius.data.local.entity.HistoryEntity
10 | import com.lanlinju.animius.data.local.relation.HistoryWithEpisodes
11 | import com.lanlinju.animius.util.HISTORY_TABLE
12 | import kotlinx.coroutines.flow.Flow
13 |
14 | @Dao
15 | interface HistoryDao {
16 | @Transaction
17 | @Query("SELECT * FROM $HISTORY_TABLE ORDER BY updated_at DESC")
18 | fun getHistoryWithEpisodes(): Flow>
19 |
20 | @Transaction
21 | @Query("SELECT * FROM $HISTORY_TABLE WHERE detail_url =:detailUrl")
22 | fun getHistoryWithEpisodes(detailUrl: String): Flow
23 |
24 | @Query("DELETE FROM $HISTORY_TABLE")
25 | suspend fun deleteAll()
26 |
27 | @Query("DELETE FROM $HISTORY_TABLE WHERE detail_url=:detailUrl")
28 | suspend fun deleteHistory(detailUrl: String)
29 |
30 | @Insert(onConflict = OnConflictStrategy.REPLACE)
31 | suspend fun insertHistories(history: List)
32 |
33 | @Insert(onConflict = OnConflictStrategy.REPLACE)
34 | suspend fun insertHistory(history: HistoryEntity): Long
35 |
36 | @Update
37 | suspend fun updateHistory(history: HistoryEntity)
38 |
39 | @Query("UPDATE $HISTORY_TABLE SET updated_at=:date WHERE detail_url=:detailUrl")
40 | suspend fun updateHistoryDate(detailUrl: String, date: Long = System.currentTimeMillis())
41 |
42 | @Query("SELECT * FROM $HISTORY_TABLE WHERE detail_url=:detailUrl")
43 | fun getHistory(detailUrl: String): Flow
44 |
45 | @Query("SELECT * FROM $HISTORY_TABLE WHERE detail_url=:detailUrl")
46 | fun checkHistory(detailUrl: String): Flow
47 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/lanlinju/animius/data/local/database/AnimeDatabase.kt:
--------------------------------------------------------------------------------
1 | package com.lanlinju.animius.data.local.database
2 |
3 | import androidx.room.Database
4 | import androidx.room.RoomDatabase
5 | import com.lanlinju.animius.data.local.dao.DownloadDao
6 | import com.lanlinju.animius.data.local.dao.DownloadDetailDao
7 | import com.lanlinju.animius.data.local.dao.EpisodeDao
8 | import com.lanlinju.animius.data.local.dao.FavouriteDao
9 | import com.lanlinju.animius.data.local.dao.HistoryDao
10 | import com.lanlinju.animius.data.local.entity.DownloadDetailEntity
11 | import com.lanlinju.animius.data.local.entity.DownloadEntity
12 | import com.lanlinju.animius.data.local.entity.EpisodeEntity
13 | import com.lanlinju.animius.data.local.entity.FavouriteEntity
14 | import com.lanlinju.animius.data.local.entity.HistoryEntity
15 |
16 | @Database(
17 | version = 3,
18 | entities = [
19 | FavouriteEntity::class,
20 | HistoryEntity::class,
21 | EpisodeEntity::class,
22 | DownloadEntity::class,
23 | DownloadDetailEntity::class
24 | ]
25 | )
26 | abstract class AnimeDatabase : RoomDatabase() {
27 | abstract fun favouriteDao(): FavouriteDao
28 | abstract fun historyDao(): HistoryDao
29 | abstract fun episodeDao(): EpisodeDao
30 | abstract fun downLoadDao(): DownloadDao
31 | abstract fun downloadDetailDao(): DownloadDetailDao
32 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/lanlinju/animius/data/local/entity/DownloadDetailEntity.kt:
--------------------------------------------------------------------------------
1 | package com.lanlinju.animius.data.local.entity
2 |
3 | import androidx.room.ColumnInfo
4 | import androidx.room.Entity
5 | import androidx.room.ForeignKey
6 | import androidx.room.Index
7 | import androidx.room.PrimaryKey
8 | import com.lanlinju.animius.domain.model.DownloadDetail
9 | import com.lanlinju.animius.util.DOWNLOAD_DETAIL_TABLE
10 |
11 | @Entity(
12 | tableName = DOWNLOAD_DETAIL_TABLE,
13 | indices = [Index("download_id", unique = false), Index("download_url", unique = true)],
14 | foreignKeys = [
15 | ForeignKey(
16 | entity = DownloadEntity::class,
17 | parentColumns = ["download_id"],
18 | childColumns = ["download_id"],
19 | onDelete = ForeignKey.CASCADE,
20 | onUpdate = ForeignKey.CASCADE
21 | )
22 | ]
23 | )
24 | data class DownloadDetailEntity(
25 | @PrimaryKey(autoGenerate = true)
26 | @ColumnInfo(name = "download_detail_id") val downloadDetailId: Long = 0,
27 | @ColumnInfo(name = "download_id") val downloadId: Long,
28 | @ColumnInfo(name = "title") val title: String, /* 剧集名 eg: 第01集 */
29 | @ColumnInfo(name = "img_url") val imgUrl: String,
30 | @ColumnInfo(name = "drama_number") val dramaNumber: Int = 0, /* 用于集数排序 */
31 | @ColumnInfo(name = "download_url") val downloadUrl: String,
32 | @ColumnInfo(name = "path") val path: String, /* 保存的文件路径 */
33 | @ColumnInfo(name = "download_size") val downloadSize: Long = 0, /* 同下 */
34 | @ColumnInfo(name = "total_size") val totalSize: Long = 0, /* 如果是m3u8类型则是分片数量,其他文件表示字节数 */
35 | @ColumnInfo(name = "file_size") val fileSize: Long = 0, /* 在文件下载成功后写入其大小 */
36 | @ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis()
37 | ) {
38 | fun toDownloadDetail(): DownloadDetail {
39 | return DownloadDetail(
40 | title = title,
41 | imgUrl = imgUrl,
42 | dramaNumber = dramaNumber,
43 | downloadUrl = downloadUrl,
44 | path = path,
45 | downloadSize = downloadSize,
46 | totalSize = totalSize,
47 | fileSize = fileSize,
48 | )
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/app/src/main/java/com/lanlinju/animius/data/local/entity/DownloadEntity.kt:
--------------------------------------------------------------------------------
1 | package com.lanlinju.animius.data.local.entity
2 |
3 | import androidx.room.ColumnInfo
4 | import androidx.room.Entity
5 | import androidx.room.Index
6 | import androidx.room.PrimaryKey
7 | import com.lanlinju.animius.util.DOWNLOAD_TABLE
8 |
9 | @Entity(
10 | tableName = DOWNLOAD_TABLE,
11 | indices = [Index("detail_url", unique = true)]
12 | )
13 | data class DownloadEntity(
14 | @PrimaryKey(autoGenerate = true)
15 | @ColumnInfo(name = "download_id") val downloadId: Long = 0,
16 | @ColumnInfo(name = "title") val title: String,
17 | @ColumnInfo(name = "detail_url") val detailUrl: String,
18 | @ColumnInfo(name = "img_url") val imgUrl: String,
19 | @ColumnInfo(name = "source") val source: String,
20 | @ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis()
21 | )
22 |
--------------------------------------------------------------------------------
/app/src/main/java/com/lanlinju/animius/data/local/entity/EpisodeEntity.kt:
--------------------------------------------------------------------------------
1 | package com.lanlinju.animius.data.local.entity
2 |
3 | import androidx.room.ColumnInfo
4 | import androidx.room.Entity
5 | import androidx.room.ForeignKey
6 | import androidx.room.Index
7 | import androidx.room.PrimaryKey
8 | import com.lanlinju.animius.domain.model.Episode
9 | import com.lanlinju.animius.util.EPISODE_TABLE
10 |
11 | /**
12 | * [onDelete = ForeignKey.CASCADE] 当父表中的某一条记录被删除时,子表中所有引用该记录的行也会自动被删除。
13 | * [onUpdate = ForeignKey.CASCADE] 当父表中的某一条记录的主键被更新时,子表中所有引用该主键的外键也会自动更新为新的值。
14 | */
15 | @Entity(
16 | tableName = EPISODE_TABLE,
17 | indices = [Index("history_id", unique = false), Index("episode_url", unique = true)],
18 | foreignKeys = [
19 | ForeignKey(
20 | entity = HistoryEntity::class,
21 | parentColumns = ["history_id"],
22 | childColumns = ["history_id"],
23 | onDelete = ForeignKey.CASCADE,
24 | onUpdate = ForeignKey.CASCADE
25 | )
26 | ]
27 | )
28 | data class EpisodeEntity(
29 | @PrimaryKey(autoGenerate = true)
30 | @ColumnInfo(name = "episode_id") val episodeId: Long = 0L,
31 | @ColumnInfo(name = "history_id") val historyId: Long,
32 | @ColumnInfo(name = "name") val name: String,
33 | @ColumnInfo(name = "episode_url") val episodeUrl: String,
34 | @ColumnInfo(name = "last_position") val lastPosition: Long = 0L, /* 记录上次视频播放的位置 */
35 | @ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis()
36 | ) {
37 | fun toEpisode(): Episode {
38 | return Episode(
39 | name = name,
40 | url = episodeUrl,
41 | lastPlayPosition = lastPosition,
42 | isPlayed = false,
43 | historyId = historyId
44 | )
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/app/src/main/java/com/lanlinju/animius/data/local/entity/FavouriteEntity.kt:
--------------------------------------------------------------------------------
1 | package com.lanlinju.animius.data.local.entity
2 |
3 | import androidx.room.ColumnInfo
4 | import androidx.room.Entity
5 | import androidx.room.Index
6 | import androidx.room.PrimaryKey
7 | import com.lanlinju.animius.domain.model.Favourite
8 | import com.lanlinju.animius.util.FAVOURITE_TABLE
9 | import com.lanlinju.animius.util.SourceMode
10 |
11 | @Entity(
12 | tableName = FAVOURITE_TABLE,
13 | indices = [Index("detail_url", unique = true)]
14 | )
15 | data class FavouriteEntity(
16 | @PrimaryKey(autoGenerate = true)
17 | @ColumnInfo(name = "favourite_id") val favouriteId: Long = 0,
18 | @ColumnInfo(name = "title") val title: String,
19 | @ColumnInfo(name = "detail_url") val detailUrl: String,
20 | @ColumnInfo(name = "img_url") val imgUrl: String,
21 | @ColumnInfo(name = "source") val source: String, /* SourceMode 用于确定域名 */
22 | @ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis()
23 | ) {
24 | fun toFavourite(): Favourite {
25 | return Favourite(
26 | title = title,
27 | detailUrl = detailUrl,
28 | imgUrl = imgUrl,
29 | sourceMode = SourceMode.valueOf(source)
30 | )
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/app/src/main/java/com/lanlinju/animius/data/local/entity/HistoryEntity.kt:
--------------------------------------------------------------------------------
1 | package com.lanlinju.animius.data.local.entity
2 |
3 | import androidx.room.ColumnInfo
4 | import androidx.room.Entity
5 | import androidx.room.Index
6 | import androidx.room.PrimaryKey
7 | import com.lanlinju.animius.util.HISTORY_TABLE
8 |
9 | @Entity(
10 | tableName = HISTORY_TABLE,
11 | indices = [Index("detail_url", unique = true)]
12 | )
13 | data class HistoryEntity(
14 | @PrimaryKey(autoGenerate = true)
15 | @ColumnInfo(name = "history_id") val historyId: Long = 0L,
16 | @ColumnInfo(name = "title") val title: String,
17 | @ColumnInfo(name = "img_url") val imgUrl: String,
18 | @ColumnInfo(name = "detail_url") val detailUrl: String,
19 | @ColumnInfo(name = "source") val source: String, /* 用于确定域名 */
20 | @ColumnInfo(name = "updated_at") val updatedAt: Long = System.currentTimeMillis()
21 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/lanlinju/animius/data/local/relation/DownloadWithDownloadDetails.kt:
--------------------------------------------------------------------------------
1 | package com.lanlinju.animius.data.local.relation
2 |
3 | import androidx.room.Embedded
4 | import androidx.room.Relation
5 | import com.lanlinju.animius.data.local.entity.DownloadDetailEntity
6 | import com.lanlinju.animius.data.local.entity.DownloadEntity
7 | import com.lanlinju.animius.domain.model.Download
8 | import com.lanlinju.animius.util.SourceMode
9 |
10 | data class DownloadWithDownloadDetails(
11 | @Embedded val download: DownloadEntity,
12 | @Relation(
13 | parentColumn = "download_id",
14 | entityColumn = "download_id",
15 | )
16 | val downloadDetails: List
17 | ) {
18 | fun toDownload(): Download {
19 | val totalSize = downloadDetails.fold(0L) { acc, d -> acc + d.fileSize }
20 | return Download(
21 | title = download.title,
22 | detailUrl = download.detailUrl,
23 | imgUrl = download.imgUrl,
24 | sourceMode = SourceMode.valueOf(download.source),
25 | totalSize = totalSize,
26 | downloadDetails = downloadDetails.map { it.toDownloadDetail() }
27 | )
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/app/src/main/java/com/lanlinju/animius/data/local/relation/HistoryWithEpisodes.kt:
--------------------------------------------------------------------------------
1 | package com.lanlinju.animius.data.local.relation
2 |
3 | import androidx.room.Embedded
4 | import androidx.room.Relation
5 | import com.lanlinju.animius.data.local.entity.EpisodeEntity
6 | import com.lanlinju.animius.data.local.entity.HistoryEntity
7 | import com.lanlinju.animius.domain.model.History
8 | import com.lanlinju.animius.util.SourceMode
9 | import java.text.SimpleDateFormat
10 | import java.util.Locale
11 |
12 | data class HistoryWithEpisodes(
13 | @Embedded val history: HistoryEntity,
14 | @Relation(
15 | parentColumn = "history_id",
16 | entityColumn = "history_id"
17 | )
18 | val episodes: List
19 | ) {
20 | fun toHistory(): History {
21 | val sortedEpisodes = episodes.sortedByDescending { it.createdAt }
22 | val simpleDateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault())
23 |
24 | return History(
25 | title = history.title,
26 | imgUrl = history.imgUrl,
27 | detailUrl = history.detailUrl,
28 | lastEpisodeName = if (sortedEpisodes.isEmpty()) "" else sortedEpisodes.first().name,
29 | lastEpisodeUrl = if (sortedEpisodes.isEmpty()) "" else sortedEpisodes.first().episodeUrl,
30 | sourceMode = SourceMode.valueOf(history.source),
31 | time = simpleDateFormat.format(history.updatedAt),
32 | episodes = emptyList()
33 | )
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/app/src/main/java/com/lanlinju/animius/data/remote/api/AnimeApi.kt:
--------------------------------------------------------------------------------
1 | package com.lanlinju.animius.data.remote.api
2 |
3 | import com.lanlinju.animius.data.remote.dto.AnimeBean
4 | import com.lanlinju.animius.data.remote.dto.AnimeDetailBean
5 | import com.lanlinju.animius.data.remote.dto.HomeBean
6 | import com.lanlinju.animius.data.remote.dto.VideoBean
7 | import com.lanlinju.animius.util.SourceMode
8 |
9 | interface AnimeApi {
10 | suspend fun getHomeAllData(): List
11 |
12 | suspend fun getAnimeDetail(detailUrl: String, mode: SourceMode): AnimeDetailBean
13 |
14 | suspend fun getVideoData(episodeUrl: String, mode: SourceMode): VideoBean
15 |
16 | suspend fun getSearchData(query: String, page: Int, mode: SourceMode): List
17 |
18 | suspend fun getWeekDate(): Map>
19 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/lanlinju/animius/data/remote/api/AnimeApiImpl.kt:
--------------------------------------------------------------------------------
1 | package com.lanlinju.animius.data.remote.api
2 |
3 | import com.lanlinju.animius.data.remote.dto.AnimeBean
4 | import com.lanlinju.animius.data.remote.dto.AnimeDetailBean
5 | import com.lanlinju.animius.data.remote.dto.HomeBean
6 | import com.lanlinju.animius.data.remote.dto.VideoBean
7 | import com.lanlinju.animius.util.SourceHolder
8 | import com.lanlinju.animius.util.SourceMode
9 | import javax.inject.Inject
10 | import javax.inject.Singleton
11 |
12 | @Singleton
13 | class AnimeApiImpl @Inject constructor() : AnimeApi {
14 | override suspend fun getHomeAllData(): List {
15 | val animeSource = SourceHolder.currentSource
16 | return animeSource.getHomeData()
17 | }
18 |
19 | override suspend fun getAnimeDetail(detailUrl: String, mode: SourceMode): AnimeDetailBean {
20 | val animeSource = SourceHolder.getSource(mode)
21 | return animeSource.getAnimeDetail(detailUrl)
22 | }
23 |
24 | override suspend fun getVideoData(episodeUrl: String, mode: SourceMode): VideoBean {
25 | val animeSource = SourceHolder.getSource(mode)
26 | return animeSource.getVideoData(episodeUrl)
27 | }
28 |
29 | override suspend fun getSearchData(query: String, page: Int, mode: SourceMode): List {
30 | val animeSource = SourceHolder.getSource(mode)
31 | return animeSource.getSearchData(query, page)
32 | }
33 |
34 | override suspend fun getWeekDate(): Map> {
35 | val animeSource = SourceHolder.currentSource
36 | return animeSource.getWeekData()
37 | }
38 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/lanlinju/animius/data/remote/dandanplay/DandanplayClient.kt:
--------------------------------------------------------------------------------
1 | package com.lanlinju.animius.data.remote.dandanplay
2 |
3 | import com.lanlinju.animius.BuildConfig
4 | import com.lanlinju.animius.data.remote.dandanplay.dto.DandanplayDanmaku
5 | import com.lanlinju.animius.data.remote.dandanplay.dto.DandanplayDanmakuListResponse
6 | import com.lanlinju.animius.data.remote.dandanplay.dto.DandanplaySearchEpisodeResponse
7 | import io.ktor.client.HttpClient
8 | import io.ktor.client.call.body
9 | import io.ktor.client.plugins.timeout
10 | import io.ktor.client.request.HttpRequestBuilder
11 | import io.ktor.client.request.accept
12 | import io.ktor.client.request.get
13 | import io.ktor.client.request.header
14 | import io.ktor.client.request.parameter
15 | import io.ktor.http.ContentType
16 | import io.ktor.http.encodedPath
17 | import java.lang.System.currentTimeMillis
18 | import java.security.MessageDigest
19 | import kotlin.io.encoding.Base64
20 | import kotlin.io.encoding.ExperimentalEncodingApi
21 |
22 | class DandanplayClient(
23 | private val client: HttpClient,
24 | private val appId: String = BuildConfig.DANDANPLAY_APP_ID,
25 | private val appSecret: String = BuildConfig.DANDANPLAY_APP_SECRET,
26 | ) {
27 |
28 | suspend fun searchEpisode(
29 | subjectName: String,
30 | episodeName: String?,
31 | ): DandanplaySearchEpisodeResponse {
32 | val response = client.get("https://api.dandanplay.net/api/v2/search/episodes") {
33 | configureTimeout()
34 | accept(ContentType.Application.Json)
35 | addAuthorizationHeaders()
36 | parameter("anime", subjectName)
37 | parameter("episode", episodeName)
38 | }
39 |
40 | return response.body()
41 | }
42 |
43 | suspend fun getDanmakuList(episodeId: Long): List {
44 | val chConvert = 0
45 | val response =
46 | client.get("https://api.dandanplay.net/api/v2/comment/${episodeId}?chConvert=$chConvert&withRelated=true") {
47 | configureTimeout()
48 | accept(ContentType.Application.Json)
49 | addAuthorizationHeaders()
50 | }.body()
51 |
52 | return response.comments
53 | }
54 |
55 | private fun HttpRequestBuilder.addAuthorizationHeaders() {
56 | val timestamp = currentTimeMillis() / 1000
57 | header("X-AppId", appId)
58 | header("X-Timestamp", timestamp)
59 | header("X-Signature", generateSignature(appId, timestamp, url.encodedPath, appSecret))
60 | }
61 |
62 | private fun HttpRequestBuilder.configureTimeout() {
63 | timeout {
64 | requestTimeoutMillis = 60_000
65 | connectTimeoutMillis = 60_000
66 | socketTimeoutMillis = 60_000
67 | }
68 | }
69 |
70 | @OptIn(ExperimentalEncodingApi::class)
71 | private fun generateSignature(
72 | appId: String,
73 | timestamp: Long,
74 | path: String,
75 | appSecret: String
76 | ): String {
77 | val data = appId + timestamp + path + appSecret
78 | val hash = MessageDigest.getInstance("SHA-256").digest(data.toByteArray())
79 | return Base64.encode(hash)
80 | }
81 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/lanlinju/animius/data/remote/dandanplay/DandanplayDanmakuProvider.kt:
--------------------------------------------------------------------------------
1 | package com.lanlinju.animius.data.remote.dandanplay
2 |
3 | import com.anime.danmaku.api.DanmakuSession
4 | import com.anime.danmaku.api.TimeBasedDanmakuSession
5 | import com.lanlinju.animius.data.remote.dandanplay.DandanplayDanmakuProvider.Companion.ID
6 | import com.lanlinju.animius.data.remote.dandanplay.dto.toDanmakuOrNull
7 | import com.lanlinju.animius.util.createHttpClient
8 | import io.ktor.client.HttpClient
9 | import kotlinx.coroutines.Dispatchers
10 | import javax.inject.Inject
11 | import javax.inject.Singleton
12 |
13 | /**
14 | * A [DanmakuProvider] provides a stream of danmaku for a specific episode.
15 | *
16 | * @see DanmakuProviderFactory
17 | */
18 | interface DanmakuProvider : AutoCloseable {
19 | // 弹幕提供者的唯一标识符
20 | val id: String
21 |
22 | // 挂起函数,用于获取弹幕会话
23 | suspend fun fetch(subjectName: String, episodeName: String?): DanmakuSession?
24 | }
25 |
26 | interface DanmakuProviderFactory { // SPI 接口
27 | /**
28 | * @see DanmakuProvider.id
29 | * 获取弹幕提供者的唯一标识符
30 | */
31 | val id: String
32 |
33 | // 创建一个新的弹幕提供者实例
34 | fun create(): DanmakuProvider
35 | }
36 |
37 | @Singleton
38 | class DandanplayDanmakuProvider @Inject constructor(
39 | private val client: HttpClient
40 | ) : DanmakuProvider {
41 |
42 | companion object {
43 | const val ID = "弹弹play"
44 | }
45 |
46 | override val id: String get() = ID
47 |
48 | private val dandanplayClient = DandanplayClient(client)
49 | private val moviePattern = Regex("全集|HD|正片")
50 | private val nonDigitRegex = Regex("\\D")
51 |
52 | override suspend fun fetch(
53 | subjectName: String, episodeName: String?
54 | ): DanmakuSession? {
55 | if (episodeName.isNullOrBlank()) return null
56 | val formattedEpisodeName = episodeName.let { name ->
57 | when {
58 | moviePattern.containsMatchIn(name) -> "movie" // 剧场版
59 | name.contains("第") -> name.replace(nonDigitRegex, "") // tv 第01集 -> 01
60 | name.matches(Regex("\\d+")) -> name // girigiri tv的剧集只有数字
61 | else -> return null // 只获取TV版和剧场版弹幕
62 | }
63 | }
64 |
65 | val searchEpisodeResponse =
66 | dandanplayClient.searchEpisode(subjectName, formattedEpisodeName)
67 |
68 | if (!searchEpisodeResponse.success || searchEpisodeResponse.animes.isEmpty()) {
69 | return null
70 | }
71 | val firstAnime = searchEpisodeResponse.animes[0]
72 | val episodes = firstAnime.episodes
73 | if (episodes.isEmpty()) {
74 | return null
75 | }
76 | val firstEpisode = episodes[0]
77 | val episodeId = firstEpisode.episodeId.toLong()
78 |
79 | return createSession(episodeId)
80 | }
81 |
82 | private suspend fun createSession(
83 | episodeId: Long,
84 | ): DanmakuSession {
85 | val list = dandanplayClient.getDanmakuList(episodeId = episodeId)
86 | return TimeBasedDanmakuSession.create(
87 | list.asSequence().mapNotNull { it.toDanmakuOrNull() },
88 | coroutineContext = Dispatchers.Default,
89 | )
90 | }
91 |
92 | override fun close() {
93 | //client.close()
94 | }
95 | }
96 |
97 | class DandanplayDanmakuProviderFactory : DanmakuProviderFactory {
98 | override val id: String get() = ID
99 |
100 | override fun create(): DandanplayDanmakuProvider {
101 | return DandanplayDanmakuProvider(createHttpClient())
102 | }
103 | }
104 |
105 |
106 |
--------------------------------------------------------------------------------
/app/src/main/java/com/lanlinju/animius/data/remote/dandanplay/dto/DandanplayDanmaku.kt:
--------------------------------------------------------------------------------
1 | package com.lanlinju.animius.data.remote.dandanplay.dto
2 |
3 | import com.anime.danmaku.api.Danmaku
4 | import com.anime.danmaku.api.DanmakuLocation
5 | import com.lanlinju.animius.data.remote.dandanplay.DandanplayDanmakuProvider
6 | import kotlinx.serialization.Serializable
7 |
8 | @Serializable
9 | class DandanplayDanmaku(
10 | val cid: Long,
11 | val p: String,
12 | val m: String, // content
13 | )
14 |
15 | fun DandanplayDanmaku.toDanmakuOrNull(): Danmaku? {
16 | /*
17 | p参数格式为出现时间,模式,颜色,用户ID,各个参数之间使用英文逗号分隔
18 | 弹幕出现时间:格式为 0.00,单位为秒,精确到小数点后两位,例如12.34、445.6、789.01
19 | 弹幕模式:1-普通弹幕,4-底部弹幕,5-顶部弹幕
20 | 颜色:32位整数表示的颜色,算法为 Rx256x256+Gx256+B,R/G/B的范围应是0-255
21 | 用户ID:字符串形式表示的用户ID,通常为数字,不会包含特殊字符
22 | */
23 | val (time, mode, color, userId) = p.split(",").let {
24 | if (it.size < 4) return null else it
25 | }
26 |
27 | val timeSecs = time.toDoubleOrNull() ?: return null
28 |
29 | return Danmaku(
30 | id = cid.toString(),
31 | providerId = DandanplayDanmakuProvider.ID,
32 | playTimeMillis = (timeSecs * 1000).toLong(),
33 | senderId = userId,
34 | location = when (mode.toIntOrNull()) {
35 | 1 -> DanmakuLocation.NORMAL
36 | 4 -> DanmakuLocation.BOTTOM
37 | 5 -> DanmakuLocation.TOP
38 | else -> return null
39 | },
40 | text = m,
41 | color = color.toIntOrNull() ?: return null
42 | )
43 | }
44 |
45 | @Serializable
46 | class DandanplayDanmakuListResponse(
47 | val count: Int,
48 | val comments: List
49 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/lanlinju/animius/data/remote/dandanplay/dto/DandanplaySearchEpisodeResponse.kt:
--------------------------------------------------------------------------------
1 | package com.lanlinju.animius.data.remote.dandanplay.dto
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class DandanplaySearchEpisodeResponse(
7 | val hasMore: Boolean = false,
8 | val animes: List = listOf(),
9 | val errorCode: Int = 0,
10 | val success: Boolean = true,
11 | val errorMessage: String? = null,
12 | )
13 |
14 | @Serializable
15 | data class SearchAnimeEpisodes(
16 | val animeId: Int,
17 | val animeTitle: String,
18 | val type: String,
19 | val typeDescription: String,
20 | val episodes: List = listOf(),
21 | )
22 |
23 | @Serializable
24 | data class SearchEpisodeDetails(
25 | val episodeId: Int,
26 | val episodeTitle: String,
27 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/lanlinju/animius/data/remote/dto/AnimeBean.kt:
--------------------------------------------------------------------------------
1 | package com.lanlinju.animius.data.remote.dto
2 |
3 | import com.lanlinju.animius.domain.model.Anime
4 |
5 | /**
6 | * @param title 动漫名称
7 | * @param img 图片url /* 获取时间表时可为空 */
8 | * @param url 动漫详情url
9 | * @param episodeName 集数
10 | */
11 | data class AnimeBean(
12 | val title: String,
13 | val img: String,
14 | val url: String,
15 | val episodeName: String = ""
16 | ) {
17 | fun toAnime(): Anime {
18 | return Anime(
19 | title = title,
20 | img = img,
21 | detailUrl = url,
22 | episodeName = episodeName
23 | )
24 | }
25 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/lanlinju/animius/data/remote/dto/AnimeDetailBean.kt:
--------------------------------------------------------------------------------
1 | package com.lanlinju.animius.data.remote.dto
2 |
3 | import com.lanlinju.animius.domain.model.AnimeDetail
4 |
5 | data class AnimeDetailBean(
6 | val title: String,
7 | val imgUrl: String,
8 | val desc: String,
9 | val tags: List = emptyList(),
10 | val relatedAnimes: List,
11 | val episodes: List = emptyList(), /* 保持对旧的数据兼容, 如果支持多线路则需要置为空 */
12 | val channels: Map> = emptyMap(), /* 剧集多线路支持 */
13 | ) {
14 | fun toAnimeDetail(): AnimeDetail {
15 | val tempChannels = if (episodes.isNotEmpty()) { /* 保持对旧的不支持多线路的兼容 */
16 | mapOf(0 to episodes.map { it.toEpisode() })
17 | } else {
18 | channels.mapValues { it.value.map { it.toEpisode() } }
19 | }
20 | return AnimeDetail(
21 | title = title,
22 | img = imgUrl,
23 | desc = desc,
24 | tags = tags.map { it.uppercase() },
25 | lastPosition = 0,
26 | episodes = tempChannels[0] ?: emptyList(),
27 | relatedAnimes = relatedAnimes.map { it.toAnime() },
28 | channels = tempChannels,
29 | )
30 | }
31 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/lanlinju/animius/data/remote/dto/EpisodeBean.kt:
--------------------------------------------------------------------------------
1 | package com.lanlinju.animius.data.remote.dto
2 |
3 | import com.lanlinju.animius.domain.model.Episode
4 |
5 | data class EpisodeBean(
6 | val name: String,
7 | val url: String
8 | ) {
9 | fun toEpisode(): Episode {
10 | return Episode(name = name, url = url)
11 | }
12 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/lanlinju/animius/data/remote/dto/HomeBean.kt:
--------------------------------------------------------------------------------
1 | package com.lanlinju.animius.data.remote.dto
2 |
3 | import com.lanlinju.animius.domain.model.Home
4 |
5 | data class HomeBean(
6 | val title: String,
7 | val moreUrl: String = "", // 可为空
8 | val animes: List
9 | ) {
10 | fun toHome(): Home {
11 | val homeItems = animes.map { it.toAnime() }
12 | return Home(title = title, animeList = homeItems)
13 | }
14 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/lanlinju/animius/data/remote/dto/VideoBean.kt:
--------------------------------------------------------------------------------
1 | package com.lanlinju.animius.data.remote.dto
2 |
3 | import com.lanlinju.animius.domain.model.WebVideo
4 |
5 | data class VideoBean(
6 | val videoUrl: String, /* 视频播放地址 */
7 | val headers: Map = emptyMap()
8 | ) {
9 | fun toWebVideo(): WebVideo {
10 | return WebVideo(
11 | url = videoUrl,
12 | headers = headers
13 | )
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/app/src/main/java/com/lanlinju/animius/data/remote/parse/AnimeSource.kt:
--------------------------------------------------------------------------------
1 | package com.lanlinju.animius.data.remote.parse
2 |
3 | import com.lanlinju.animius.data.remote.dto.AnimeBean
4 | import com.lanlinju.animius.data.remote.dto.AnimeDetailBean
5 | import com.lanlinju.animius.data.remote.dto.HomeBean
6 | import com.lanlinju.animius.data.remote.dto.VideoBean
7 | import com.lanlinju.animius.util.preferences
8 |
9 | interface AnimeSource {
10 |
11 | /**
12 | * [preferences] 的Key值用于获取用户的自定义的域名
13 | */
14 | val KEY_SOURCE_DOMAIN: String
15 | get() = "${this.javaClass.simpleName}Domain"
16 |
17 | /**
18 | * 默认动漫域名
19 | */
20 | val DEFAULT_DOMAIN: String
21 |
22 | /**
23 | * 动漫域名,默认值为[DEFAULT_DOMAIN],
24 | * 且[DEFAULT_DOMAIN] 要先于 [baseUrl] 初始化
25 | */
26 | var baseUrl: String
27 |
28 | suspend fun getHomeData(): List
29 |
30 | suspend fun getAnimeDetail(detailUrl: String): AnimeDetailBean
31 |
32 | suspend fun getVideoData(episodeUrl: String): VideoBean
33 |
34 | suspend fun getSearchData(query: String, page: Int): List
35 |
36 | suspend fun getWeekData(): Map>
37 |
38 | /**
39 | * 当切换选中的数据源时调用,可以执行一些初始化操作
40 | */
41 | fun onEnter() {}
42 |
43 | /**
44 | * 当退出当前数据源时调用,可以执行一些清理操作
45 | */
46 | fun onExit() {}
47 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/lanlinju/animius/data/repository/AnimeRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package com.lanlinju.animius.data.repository
2 |
3 | import androidx.paging.Pager
4 | import androidx.paging.PagingConfig
5 | import androidx.paging.PagingData
6 | import com.lanlinju.animius.data.remote.api.AnimeApi
7 | import com.lanlinju.animius.data.repository.paging.SearchPagingSource
8 | import com.lanlinju.animius.domain.model.Anime
9 | import com.lanlinju.animius.domain.model.AnimeDetail
10 | import com.lanlinju.animius.domain.model.Home
11 | import com.lanlinju.animius.domain.model.WebVideo
12 | import com.lanlinju.animius.domain.repository.AnimeRepository
13 | import com.lanlinju.animius.util.Resource
14 | import com.lanlinju.animius.util.Result
15 | import com.lanlinju.animius.util.SEARCH_PAGE_SIZE
16 | import com.lanlinju.animius.util.SourceMode
17 | import com.lanlinju.animius.util.invokeApi
18 | import com.lanlinju.animius.util.map
19 | import com.lanlinju.animius.util.safeCall
20 | import kotlinx.coroutines.flow.Flow
21 | import javax.inject.Inject
22 | import javax.inject.Singleton
23 |
24 | @Singleton
25 | class AnimeRepositoryImpl @Inject constructor(
26 | private val animeApi: AnimeApi
27 | ) : AnimeRepository {
28 | override suspend fun getHomeData(): Resource> {
29 | val response = invokeApi {
30 | animeApi.getHomeAllData()
31 | }
32 | return when (response) {
33 | is Resource.Error -> Resource.Error(error = response.error)
34 | is Resource.Loading -> Resource.Loading
35 | is Resource.Success -> Resource.Success(
36 | data = response.data?.map { it.toHome() }.orEmpty()
37 | )
38 | }
39 | }
40 |
41 | override suspend fun getAnimeDetail(
42 | detailUrl: String,
43 | mode: SourceMode
44 | ): Resource {
45 | val response = invokeApi {
46 | animeApi.getAnimeDetail(detailUrl, mode)
47 | }
48 | return when (val response = response) {
49 | is Resource.Error -> Resource.Error(error = response.error)
50 | is Resource.Loading -> Resource.Loading
51 | is Resource.Success -> Resource.Success(
52 | data = response.data?.toAnimeDetail()
53 | )
54 | }
55 | }
56 |
57 | override suspend fun getVideoData(episodeUrl: String, mode: SourceMode): Result {
58 | return safeCall {
59 | animeApi.getVideoData(episodeUrl, mode)
60 | }.map { it.toWebVideo() }
61 | }
62 |
63 | override suspend fun getSearchData(query: String, mode: SourceMode): Flow> {
64 | return Pager(
65 | config = PagingConfig(pageSize = SEARCH_PAGE_SIZE),
66 | pagingSourceFactory = { SearchPagingSource(api = animeApi, query, mode) }
67 | ).flow
68 | }
69 |
70 | override suspend fun getWeekData(): Resource