1
+ package com.otaku.kickassanime.work
2
+
3
+ import android.content.Context
4
+ import android.net.Uri
5
+ import android.util.Log
6
+ import androidx.hilt.work.HiltWorker
7
+ import androidx.media3.common.MediaItem
8
+ import androidx.media3.common.TrackSelectionParameters
9
+ import androidx.media3.common.util.UnstableApi
10
+ import androidx.media3.common.util.Util
11
+ import androidx.media3.exoplayer.DefaultRenderersFactory
12
+ import androidx.media3.exoplayer.offline.DownloadHelper
13
+ import androidx.media3.exoplayer.offline.DownloadHelper.Callback
14
+ import androidx.media3.exoplayer.offline.DownloadRequest
15
+ import androidx.media3.exoplayer.offline.DownloadService
16
+ import androidx.work.CoroutineWorker
17
+ import androidx.work.Data
18
+ import androidx.work.WorkerParameters
19
+ import com.google.gson.Gson
20
+ import com.otaku.fetch.ModuleRegistry
21
+ import com.otaku.fetch.base.download.DownloadUtils
22
+ import com.otaku.fetch.base.download.FetchDownloadService
23
+ import com.otaku.fetch.base.settings.Settings
24
+ import com.otaku.fetch.base.settings.dataStore
25
+ import com.otaku.kickassanime.Strings
26
+ import com.otaku.kickassanime.api.model.CommonSubtitle
27
+ import com.otaku.kickassanime.db.models.CommonVideoLink
28
+ import com.otaku.kickassanime.page.episodepage.CustomWebView
29
+ import com.otaku.kickassanime.page.episodepage.EpisodeViewModel
30
+ import com.otaku.kickassanime.pojo.PlayData
31
+ import com.otaku.kickassanime.utils.OfflineSubsHelper
32
+ import com.otaku.kickassanime.utils.Quality
33
+ import dagger.assisted.Assisted
34
+ import dagger.assisted.AssistedInject
35
+ import kotlinx.coroutines.flow.first
36
+ import kotlinx.coroutines.runBlocking
37
+ import kotlinx.coroutines.suspendCancellableCoroutine
38
+ import java.io.IOException
39
+ import kotlin.coroutines.resume
40
+
41
+
42
+ @HiltWorker
43
+ class DownloadAllEpisodeTask @AssistedInject constructor(
44
+ @Assisted val context : Context ,
45
+ @Assisted val workerParameters : WorkerParameters ,
46
+ private val gson : Gson ,
47
+ private val downloadUtils : DownloadUtils ,
48
+ private val offlineSubsHelper : OfflineSubsHelper
49
+ ) : CoroutineWorker(context, workerParameters) {
50
+
51
+
52
+ companion object {
53
+ fun createNewInput (
54
+ episodeUrls : Array <String >, episodeSlugs : Array <String >, animeSlug : String
55
+ ) = Data .Builder ().putStringArray(EPISODES_URLS , episodeUrls)
56
+ .putStringArray(EPISODES_SLUGS , episodeSlugs).putString(ANIME_SLUG , animeSlug).build()
57
+
58
+ fun getErrors (result : Data ): Array <out String >? {
59
+ return result.getStringArray(ERRORS )
60
+ }
61
+
62
+ fun getDownloadUrls (result : Data ): Array <out String >? {
63
+ return result.getStringArray(DOWNLOAD_URLS )
64
+ }
65
+
66
+
67
+ private const val EPISODES_URLS = " all_episodes"
68
+ private const val EPISODES_SLUGS = " all_episodes_slugs"
69
+ private const val ANIME_SLUG = " anime_slug"
70
+
71
+ private const val DOWNLOAD_URLS = " download_urls"
72
+ private const val ERRORS = " errors"
73
+ }
74
+
75
+ @androidx.annotation.OptIn (androidx.media3.common.util.UnstableApi ::class )
76
+ override suspend fun doWork (): Result {
77
+ return runBlocking {
78
+ val preferences = context.dataStore.data.first()
79
+ // get all episodes
80
+ val episodeUrls = workerParameters.inputData.getStringArray(EPISODES_URLS )
81
+ ? : return @runBlocking Result .failure()
82
+ val episodeSlugs = workerParameters.inputData.getStringArray(EPISODES_SLUGS )
83
+ ? : return @runBlocking Result .failure()
84
+ val animeSlug = workerParameters.inputData.getString(ANIME_SLUG )
85
+ ? : return @runBlocking Result .failure()
86
+ val webView: CustomWebView =
87
+ ModuleRegistry .modules[Strings .KICKASSANIME ]?.appModule?.webView as ? CustomWebView
88
+ ? : return @runBlocking Result .failure()
89
+
90
+ val allDownloads = ArrayList <Pair <String , PlayData >>()
91
+ loadNextEpisode(webView, episodeUrls, episodeSlugs, 0 , allDownloads)
92
+ val downloadRequestUri = ArrayList <String >()
93
+ val errors = ArrayList <String >()
94
+ allDownloads.forEach { (episodeSlug, episodePlayData) ->
95
+ try {
96
+ val downloadRequest = processEpisode(animeSlug,
97
+ episodeSlug,
98
+ episodePlayData,
99
+ preferences[Settings .DOWNLOADS_VIDEO_QUALITY ]?.let { quality ->
100
+ Quality .entries[quality.toIntOrNull() ? : 0 ].bitrate
101
+ } ? : Quality .MAX .bitrate
102
+ )
103
+ downloadRequestUri.add(downloadRequest.uri.toString())
104
+ } catch (e: Throwable ) {
105
+ errors.add(episodeSlug)
106
+ }
107
+ }
108
+ Result .success(
109
+ Data .Builder ()
110
+ .putStringArray(DOWNLOAD_URLS , downloadRequestUri.toTypedArray())
111
+ .putStringArray(ERRORS , errors.toTypedArray())
112
+ .build()
113
+ )
114
+ }
115
+ }
116
+
117
+ @androidx.annotation.OptIn (UnstableApi ::class )
118
+ private suspend fun processEpisode (
119
+ animeSlug : String ,
120
+ episodeSlug : String ,
121
+ playData : PlayData ,
122
+ downloadBitRate : Int
123
+ ): DownloadRequest {
124
+ val links = EpisodeViewModel .processPlayData(playData.allSources)
125
+ val subs = ArrayList <CommonSubtitle >()
126
+ val videoLinks = ArrayList <CommonVideoLink >()
127
+ links.forEach { (videoLink, subtitles) ->
128
+ run {
129
+ subs.addAll(subtitles)
130
+ videoLinks.add(videoLink)
131
+ }
132
+ }
133
+ val mediaLink = videoLinks.getOrNull(0 )?.getLink() ? : throw Exception (" No video link found" )
134
+ val trackSelectionParameters =
135
+ TrackSelectionParameters .Builder (context).setPreferredTextLanguage(" en" )
136
+ .setMinVideoBitrate(downloadBitRate - 1000 )
137
+ .setPreferredAudioLanguage(" en" ).setMaxVideoBitrate(downloadBitRate).build()
138
+ offlineSubsHelper.downloadSubs(animeSlug, episodeSlug, subs)
139
+ val link = Uri .parse(mediaLink)
140
+ val helper = DownloadHelper .forMediaItem(
141
+ context,
142
+ MediaItem .fromUri(link),
143
+ DefaultRenderersFactory (context),
144
+ downloadUtils.getHttpDataSourceFactory(context)
145
+ )
146
+
147
+ val downloadRequest = suspendCancellableCoroutine<DownloadRequest > { continuation ->
148
+ helper.prepare(object : Callback {
149
+ override fun onPrepared (helper : DownloadHelper ) {
150
+ for (periodIndex in 0 until helper.periodCount) {
151
+ helper.clearTrackSelections(periodIndex)
152
+ helper.addTrackSelection(periodIndex, trackSelectionParameters)
153
+ }
154
+ val downloadRequest = helper.getDownloadRequest(Util .getUtf8Bytes(episodeSlug))
155
+ DownloadService .sendAddDownload(
156
+ context, FetchDownloadService ::class .java,
157
+ downloadRequest, false
158
+ )
159
+ continuation.resume(downloadRequest)
160
+ }
161
+
162
+ override fun onPrepareError (helper : DownloadHelper , e : IOException ) {
163
+ Log .e(" Download Episode" , " Failed while preparing media" , e)
164
+ continuation.cancel(e)
165
+ }
166
+ })
167
+ }
168
+ return downloadRequest
169
+ }
170
+
171
+ private suspend fun loadNextEpisode (
172
+ webView : CustomWebView ,
173
+ episodeUrls : Array <String >,
174
+ episodeSlugs : Array <String >,
175
+ index : Int ,
176
+ playlist : ArrayList <Pair <String , PlayData >>
177
+ ) {
178
+ try {
179
+ val enqueue = webView.enqueue(episodeUrls[index])
180
+ val playData = gson.fromJson(enqueue, PlayData ::class .java)
181
+ playlist.add(episodeSlugs[index] to playData)
182
+ if (index + 1 < episodeUrls.size) {
183
+ loadNextEpisode(webView, episodeUrls, episodeSlugs, index + 1 , playlist)
184
+ }
185
+ } catch (e: Exception ) {
186
+ if (index + 1 < episodeUrls.size) {
187
+ loadNextEpisode(webView, episodeUrls, episodeSlugs, index + 1 , playlist)
188
+ }
189
+ }
190
+ }
191
+
192
+ }
0 commit comments