diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 57a45987a7..752faaa1ec 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -276,6 +276,8 @@ dependencies {
implementation(libs.seeker)
// true type parser
implementation(libs.truetypeparser)
+ // torrserver
+ implementation(libs.torrentserver)
}
androidComponents {
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index 473a31a9b4..c163f62d39 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -28,6 +28,7 @@
-keep,allowoptimization class eu.kanade.tachiyomi.network.OkHttpExtensionsKt { public protected *; }
-keep,allowoptimization class eu.kanade.tachiyomi.network.RequestsKt { public protected *; }
-keep,allowoptimization class eu.kanade.tachiyomi.AppInfo { public protected *; }
+-keep,allowoptimization class eu.kanade.tachiyomi.torrentutils.** { public protected *; }
##---------------Begin: proguard configuration for RxJava 1.x ----------
-dontwarn sun.misc.**
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 9458212ceb..08f5b84c2f 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -274,6 +274,11 @@
android:foregroundServiceType="dataSync"
tools:node="merge" />
+
+
{
val playerPreferences = remember { Injekt.get() }
val basePreferences = remember { Injekt.get() }
+ val torrentServerPreferences = remember { Injekt.get() }
val deviceSupportsPip = basePreferences.deviceHasPip()
return listOfNotNull(
@@ -84,6 +88,7 @@ object SettingsPlayerScreen : SearchableSettings {
playerPreferences = playerPreferences,
basePreferences = basePreferences,
),
+ getTorrentServerGroup(torrentServerPreferences),
)
}
@@ -394,6 +399,55 @@ object SettingsPlayerScreen : SearchableSettings {
)
}
+ @Composable
+ private fun getTorrentServerGroup(
+ torrentServerPreferences: TorrentServerPreferences,
+ ): Preference.PreferenceGroup {
+ val scope = rememberCoroutineScope()
+ val trackersPref = torrentServerPreferences.trackers()
+ val trackers by trackersPref.collectAsState()
+
+ return Preference.PreferenceGroup(
+ title = stringResource(MR.strings.pref_category_torrentserver),
+ preferenceItems = persistentListOf(
+ Preference.PreferenceItem.EditTextPreference(
+ pref = torrentServerPreferences.port(),
+ title = stringResource(MR.strings.pref_torrentserver_port),
+ onValueChanged = {
+ try {
+ Integer.parseInt(it)
+ TorrentServerService.stop()
+ true
+ } catch (e: Exception) {
+ false
+ }
+ },
+ ),
+ Preference.PreferenceItem.MultiLineEditTextPreference(
+ pref = torrentServerPreferences.trackers(),
+ title = stringResource(MR.strings.pref_torrent_trackers),
+ subtitle = trackersPref.asState(scope).value
+ .lines().take(2)
+ .joinToString(
+ separator = "\n",
+ postfix = if (trackersPref.asState(scope).value.lines().size > 2) "\n..." else "",
+ ),
+ onValueChanged = {
+ TorrentServerService.stop()
+ true
+ },
+ ),
+ Preference.PreferenceItem.TextPreference(
+ title = stringResource(MR.strings.pref_reset_torrent_trackers_string),
+ enabled = remember(trackers) { trackers != trackersPref.defaultValue() },
+ onClick = {
+ trackersPref.delete()
+ },
+ ),
+ ),
+ )
+ }
+
@Composable
private fun SkipIntroLengthDialog(
initialSkipIntroLength: Int,
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/anime/AnimeDownloader.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/anime/AnimeDownloader.kt
index ecd27627db..05c37c5def 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/download/anime/AnimeDownloader.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/anime/AnimeDownloader.kt
@@ -20,6 +20,9 @@ import eu.kanade.tachiyomi.data.download.anime.model.AnimeDownload
import eu.kanade.tachiyomi.data.download.anime.model.AnimeDownloadPart
import eu.kanade.tachiyomi.data.library.anime.AnimeLibraryUpdateNotifier
import eu.kanade.tachiyomi.data.notification.NotificationHandler
+import eu.kanade.tachiyomi.data.torrentServer.TorrentServerApi
+import eu.kanade.tachiyomi.data.torrentServer.TorrentServerUtils
+import eu.kanade.tachiyomi.data.torrentServer.service.TorrentServerService
import eu.kanade.tachiyomi.network.ProgressListener
import eu.kanade.tachiyomi.util.size
import eu.kanade.tachiyomi.util.storage.DiskUtil
@@ -483,7 +486,9 @@ class AnimeDownloader(
if (downloadScope.isActive) {
file = try {
- if (isHls(download.video!!) || isMpd(download.video!!)) {
+ if (isTorrent(download.video!!)) {
+ torrentDownload(download, tmpDir, filename)
+ } else if (isHls(download.video!!) || isMpd(download.video!!)) {
ffmpegDownload(download, tmpDir, filename)
} else {
httpDownload(download, tmpDir, filename, newThreads, safe)
@@ -503,7 +508,9 @@ class AnimeDownloader(
// otherwise we attempt a final try forcing safe mode
return if (downloadScope.isActive) {
file ?: try {
- if (isHls(download.video!!) || isMpd(download.video!!)) {
+ if (isTorrent(download.video!!)) {
+ torrentDownload(download, tmpDir, filename)
+ } else if (isHls(download.video!!) || isMpd(download.video!!)) {
ffmpegDownload(download, tmpDir, filename)
} else {
httpDownload(download, tmpDir, filename, 1, true)
@@ -517,6 +524,11 @@ class AnimeDownloader(
}
}
+ private fun isTorrent(video: Video): Boolean {
+ val url = video.videoUrl ?: return false
+ return url.startsWith("magnet") || url.endsWith(".torrent") || url.startsWith(TorrentServerUtils.hostUrl)
+ }
+
private fun isMpd(video: Video): Boolean {
return video.videoUrl?.toHttpUrl()?.encodedPath?.endsWith(".mpd") ?: false
}
@@ -525,6 +537,35 @@ class AnimeDownloader(
return video.videoUrl?.toHttpUrl()?.encodedPath?.endsWith(".m3u8") ?: false
}
+ // this start the torrent server and get the url to download the video
+ private suspend fun torrentDownload(
+ download: AnimeDownload,
+ tmpDir: UniFile,
+ filename: String,
+ ): UniFile {
+ val video = download.video!!
+ TorrentServerService.start()
+ if (video.videoUrl!!.startsWith(TorrentServerUtils.hostUrl)) {
+ val hash = video.videoUrl!!.substringAfter("link=").substringBefore("&")
+ val index = video.videoUrl!!.substringAfter("index=").substringBefore("&").toInt()
+ val magnet = "magnet:?xt=urn:btih:$hash&index=$index"
+ video.videoUrl = magnet
+ }
+ val currentTorrent = TorrentServerApi.addTorrent(video.videoUrl!!, video.quality, "", "", false)
+ var index = 0
+ if (video.videoUrl!!.contains("index=")) {
+ index = try {
+ video.videoUrl?.substringAfter("index=")
+ ?.substringBefore("&")?.toInt() ?: 0
+ } catch (_: Exception) {
+ 0
+ }
+ }
+ val torrentUrl = TorrentServerUtils.getTorrentPlayLink(currentTorrent, index)
+ video.videoUrl = torrentUrl
+ return ffmpegDownload(download, tmpDir, filename)
+ }
+
// ffmpeg is always on safe mode
private fun ffmpegDownload(
download: AnimeDownload,
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt b/app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt
index 6a1a14b00d..72d05246d2 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt
@@ -68,6 +68,12 @@ object Notifications {
const val CHANNEL_INCOGNITO_MODE = "incognito_mode_channel"
const val ID_INCOGNITO_MODE = -701
+ /**
+ * Notification channel and ids used for torrent server
+ */
+ const val CHANNEL_TORRENT_SERVER = "torrent_server_channel"
+ const val ID_TORRENT_SERVER = -801
+
/**
* Notification channel and ids used for app and extension updates.
*/
@@ -162,6 +168,10 @@ object Notifications {
buildNotificationChannel(CHANNEL_INCOGNITO_MODE, IMPORTANCE_LOW) {
setName(context.stringResource(MR.strings.pref_incognito_mode))
},
+ buildNotificationChannel(CHANNEL_TORRENT_SERVER, IMPORTANCE_LOW) {
+ setName(context.stringResource(MR.strings.pref_category_torrentserver))
+ setShowBadge(false)
+ },
buildNotificationChannel(CHANNEL_APP_UPDATE, IMPORTANCE_DEFAULT) {
setGroup(GROUP_APK_UPDATES)
setName(context.stringResource(MR.strings.channel_app_updates))
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/torrentServer/TorrentServerApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/torrentServer/TorrentServerApi.kt
new file mode 100644
index 0000000000..1b346e20d7
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/torrentServer/TorrentServerApi.kt
@@ -0,0 +1,107 @@
+package eu.kanade.tachiyomi.data.torrentServer
+
+import eu.kanade.tachiyomi.BuildConfig
+import eu.kanade.tachiyomi.data.torrentServer.model.Torrent
+import eu.kanade.tachiyomi.data.torrentServer.model.TorrentRequest
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.network.NetworkHelper
+import eu.kanade.tachiyomi.network.POST
+import eu.kanade.tachiyomi.network.awaitSuccess
+import kotlinx.serialization.json.Json
+import okhttp3.MediaType.Companion.toMediaTypeOrNull
+import okhttp3.RequestBody.Companion.toRequestBody
+import org.jsoup.Jsoup
+import uy.kohesive.injekt.injectLazy
+import java.io.InputStream
+
+object TorrentServerApi {
+ private val network: NetworkHelper by injectLazy()
+ private val hostUrl = TorrentServerUtils.hostUrl
+
+ suspend fun echo(): String {
+ return try {
+ network.client.newCall(GET("$hostUrl/echo")).awaitSuccess().body.string()
+ } catch (e: Exception) {
+ if (BuildConfig.DEBUG) println(e.message)
+ ""
+ }
+ }
+
+ suspend fun shutdown(): String {
+ return try {
+ network.client.newCall(GET("$hostUrl/shutdown")).awaitSuccess().body.string()
+ } catch (e: Exception) {
+ if (BuildConfig.DEBUG) println(e.message)
+ ""
+ }
+ }
+
+ // / Torrents
+ suspend fun addTorrent(
+ link: String,
+ title: String,
+ poster: String = "",
+ data: String = "",
+ save: Boolean,
+ ): Torrent {
+ val req =
+ TorrentRequest(
+ "add",
+ link = link,
+ title = title,
+ poster = poster,
+ data = data,
+ save_to_db = save,
+ ).toString()
+ val resp =
+ network.client.newCall(
+ POST("$hostUrl/torrents", body = req.toRequestBody("application/json".toMediaTypeOrNull())),
+ ).awaitSuccess()
+ return Json.decodeFromString(Torrent.serializer(), resp.body.string())
+ }
+
+ suspend fun getTorrent(hash: String): Torrent {
+ val req = TorrentRequest("get", hash).toString()
+ val resp =
+ network.client.newCall(
+ POST("$hostUrl/torrents", body = req.toRequestBody("application/json".toMediaTypeOrNull())),
+ ).awaitSuccess()
+ return Json.decodeFromString(Torrent.serializer(), resp.body.string())
+ }
+
+ suspend fun remTorrent(hash: String) {
+ val req = TorrentRequest("rem", hash).toString()
+ network.client.newCall(
+ POST("$hostUrl/torrents", body = req.toRequestBody("application/json".toMediaTypeOrNull())),
+ ).awaitSuccess()
+ }
+
+ suspend fun listTorrent(): List {
+ val req = TorrentRequest("list").toString()
+ val resp =
+ network.client.newCall(
+ POST("$hostUrl/torrents", body = req.toRequestBody("application/json".toMediaTypeOrNull())),
+ ).awaitSuccess()
+ return Json.decodeFromString>(resp.body.string())
+ }
+
+ fun uploadTorrent(
+ file: InputStream,
+ title: String,
+ poster: String,
+ data: String,
+ save: Boolean,
+ ): Torrent {
+ val resp =
+ Jsoup.connect("$hostUrl/torrent/upload")
+ .data("title", title)
+ .data("poster", poster)
+ .data("data", data)
+ .data("save", save.toString())
+ .data("file1", "filename", file)
+ .ignoreContentType(true)
+ .ignoreHttpErrors(true)
+ .post()
+ return Json.decodeFromString(Torrent.serializer(), resp.body().text())
+ }
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/torrentServer/TorrentServerPreferences.kt b/app/src/main/java/eu/kanade/tachiyomi/data/torrentServer/TorrentServerPreferences.kt
new file mode 100644
index 0000000000..42e5bf3950
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/torrentServer/TorrentServerPreferences.kt
@@ -0,0 +1,39 @@
+package eu.kanade.tachiyomi.data.torrentServer
+
+import tachiyomi.core.common.preference.PreferenceStore
+
+class TorrentServerPreferences(
+ private val preferenceStore: PreferenceStore,
+) {
+ fun port() = preferenceStore.getString("pref_torrent_port", "8090")
+
+ fun trackers() = preferenceStore.getString(
+ "pref_torrent_trackers",
+ """http://nyaa.tracker.wf:7777/announce
+ http://anidex.moe:6969/announce
+ http://tracker.anirena.com:80/announce
+ udp://tracker.uw0.xyz:6969/announce
+ http://share.camoe.cn:8080/announce
+ http://t.nyaatracker.com:80/announce
+ udp://47.ip-51-68-199.eu:6969/announce
+ udp://9.rarbg.me:2940
+ udp://9.rarbg.to:2820
+ udp://exodus.desync.com:6969/announce
+ udp://explodie.org:6969/announce
+ udp://ipv4.tracker.harry.lu:80/announce
+ udp://open.stealth.si:80/announce
+ udp://opentor.org:2710/announce
+ udp://opentracker.i2p.rocks:6969/announce
+ udp://retracker.lanta-net.ru:2710/announce
+ udp://tracker.cyberia.is:6969/announce
+ udp://tracker.dler.org:6969/announce
+ udp://tracker.ds.is:6969/announce
+ udp://tracker.internetwarriors.net:1337
+ udp://tracker.openbittorrent.com:6969/announce
+ udp://tracker.opentrackr.org:1337/announce
+ udp://tracker.tiny-vps.com:6969/announce
+ udp://tracker.torrent.eu.org:451/announce
+ udp://valakas.rollo.dnsabr.com:2710/announce
+ udp://www.torrent.eu.org:451/announce""".replace(" ", ""),
+ )
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/torrentServer/TorrentServerUtils.kt b/app/src/main/java/eu/kanade/tachiyomi/data/torrentServer/TorrentServerUtils.kt
new file mode 100644
index 0000000000..358b077d7d
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/torrentServer/TorrentServerUtils.kt
@@ -0,0 +1,36 @@
+package eu.kanade.tachiyomi.data.torrentServer
+
+import eu.kanade.tachiyomi.data.torrentServer.model.FileStat
+import eu.kanade.tachiyomi.data.torrentServer.model.Torrent
+import uy.kohesive.injekt.injectLazy
+import java.io.File
+import java.net.URLEncoder
+
+object TorrentServerUtils {
+ private val preferences: TorrentServerPreferences by injectLazy()
+ val hostUrl = "http://127.0.0.1:${preferences.port().get()}"
+
+ // Is necessary separate the trackers by comma because is hardcoded in go-torrent-server
+ private val animeTrackers = preferences.trackers().get().split("\n").joinToString(",\n")
+
+ fun setTrackersList() {
+ torrServer.TorrServer.addTrackers(animeTrackers)
+ }
+
+ fun getTorrentPlayLink(torr: Torrent, index: Int): String {
+ val file = findFile(torr, index)
+ val name = file?.let { File(it.path).name } ?: torr.title
+ return "$hostUrl/stream/${name.urlEncode()}?link=${torr.hash}&index=$index&play"
+ }
+
+ private fun findFile(torrent: Torrent, index: Int): FileStat? {
+ torrent.file_stats?.forEach {
+ if (it.id == index) {
+ return it
+ }
+ }
+ return null
+ }
+
+ private fun String.urlEncode(): String = URLEncoder.encode(this, "utf8")
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/torrentServer/model/BTSets.kt b/app/src/main/java/eu/kanade/tachiyomi/data/torrentServer/model/BTSets.kt
new file mode 100644
index 0000000000..68b7405a70
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/torrentServer/model/BTSets.kt
@@ -0,0 +1,39 @@
+package eu.kanade.tachiyomi.data.torrentServer.model
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class BTSets(
+ // Cache
+ var CacheSize: Long,
+ var PreloadBuffer: Boolean,
+ var PreloadCache: Int,
+ var ReaderReadAHead: Int,
+ // Storage
+ var UseDisk: Boolean,
+ var TorrentsSavePath: String,
+ var RemoveCacheOnDrop: Boolean,
+ // Torrent
+ var ForceEncrypt: Boolean,
+ var RetrackersMode: Int,
+ var TorrentDisconnectTimeout: Int,
+ var EnableDebug: Boolean,
+ // DLNA
+ var EnableDLNA: Boolean,
+ var FriendlyName: String,
+ // Rutor search
+ var EnableRutorSearch: Boolean,
+ // BT Config
+ var EnableIPv6: Boolean,
+ var DisableTCP: Boolean,
+ var DisableUTP: Boolean,
+ var DisableUPNP: Boolean,
+ var DisableDHT: Boolean,
+ var DisablePEX: Boolean,
+ var DisableUpload: Boolean,
+ var DownloadRateLimit: Int,
+ var UploadRateLimit: Int,
+ var ConnectionsLimit: Int,
+ var DhtConnectionLimit: Int,
+ var PeersListenPort: Int,
+)
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/torrentServer/model/TorrServVersion.kt b/app/src/main/java/eu/kanade/tachiyomi/data/torrentServer/model/TorrServVersion.kt
new file mode 100644
index 0000000000..ddfaf206cd
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/torrentServer/model/TorrServVersion.kt
@@ -0,0 +1,9 @@
+package eu.kanade.tachiyomi.data.torrentServer.model
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class TorrServVersion(
+ val version: String,
+ val links: Map,
+)
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/torrentServer/model/Torrent.kt b/app/src/main/java/eu/kanade/tachiyomi/data/torrentServer/model/Torrent.kt
new file mode 100644
index 0000000000..c4380ba96a
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/torrentServer/model/Torrent.kt
@@ -0,0 +1,48 @@
+package eu.kanade.tachiyomi.data.torrentServer.model
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class Torrent(
+ var title: String,
+ var poster: String? = null,
+ var data: String? = null,
+ var timestamp: Long? = null,
+ var name: String? = null,
+ var hash: String? = null,
+ var stat: Int? = null,
+ var stat_string: String? = null,
+ var loaded_size: Long? = null,
+ var torrent_size: Long? = null,
+ var preloaded_bytes: Long? = null,
+ var preload_size: Long? = null,
+ var download_speed: Double? = null,
+ var upload_speed: Double? = null,
+ var total_peers: Int? = null,
+ var pending_peers: Int? = null,
+ var active_peers: Int? = null,
+ var connected_seeders: Int? = null,
+ var half_open_peers: Int? = null,
+ var bytes_written: Long? = null,
+ var bytes_written_data: Long? = null,
+ var bytes_read: Long? = null,
+ var bytes_read_data: Long? = null,
+ var bytes_read_useful_data: Long? = null,
+ var chunks_written: Long? = null,
+ var chunks_read: Long? = null,
+ var chunks_read_useful: Long? = null,
+ var chunks_read_wasted: Long? = null,
+ var pieces_dirtied_good: Long? = null,
+ var pieces_dirtied_bad: Long? = null,
+ var duration_seconds: Double? = null,
+ var bit_rate: String? = null,
+ var file_stats: List? = null,
+ var trackers: List? = null,
+)
+
+@Serializable
+data class FileStat(
+ var id: Int? = null,
+ var path: String,
+ var length: Long,
+)
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/torrentServer/model/TorrentDetails.kt b/app/src/main/java/eu/kanade/tachiyomi/data/torrentServer/model/TorrentDetails.kt
new file mode 100644
index 0000000000..14bb41faa1
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/torrentServer/model/TorrentDetails.kt
@@ -0,0 +1,23 @@
+package eu.kanade.tachiyomi.data.torrentServer.model
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class TorrentDetails(
+ val Title: String,
+ val Name: String,
+ val Names: List,
+ val Categories: String,
+ val Size: String,
+ val CreateDate: String,
+ val Tracker: String,
+ val Link: String,
+ val Year: Int,
+ val Peer: Int,
+ val Seed: Int,
+ val Magnet: String,
+ val Hash: String,
+ val IMDBID: String,
+ val VideoQuality: Int,
+ val AudioQuality: Int,
+)
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/torrentServer/model/TorrentRequest.kt b/app/src/main/java/eu/kanade/tachiyomi/data/torrentServer/model/TorrentRequest.kt
new file mode 100644
index 0000000000..09335f7b88
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/torrentServer/model/TorrentRequest.kt
@@ -0,0 +1,43 @@
+package eu.kanade.tachiyomi.data.torrentServer.model
+
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.serializer
+
+@Serializable
+data class TorrentRequest(
+ val action: String,
+ val hash: String = "",
+ val link: String = "",
+ val title: String = "",
+ val poster: String = "",
+ val data: String = "",
+ val save_to_db: Boolean = false,
+) {
+ override fun toString(): String {
+ return Json.encodeToString(serializer(), this)
+ }
+}
+
+@Serializable
+open class Request(val action: String) {
+ override fun toString(): String {
+ return Json.encodeToString(serializer(), this)
+ }
+}
+
+class SettingsReq(
+ action: String,
+ val Sets: BTSets,
+) : Request(action)
+
+class ViewedReq(
+ action: String,
+ val hash: String = "",
+ val file_index: Int = -1,
+) : Request(action)
+
+data class Viewed(
+ val hash: String,
+ val file_index: Int,
+)
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/torrentServer/service/TorrentServerService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/torrentServer/service/TorrentServerService.kt
new file mode 100644
index 0000000000..21417cd93c
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/torrentServer/service/TorrentServerService.kt
@@ -0,0 +1,171 @@
+package eu.kanade.tachiyomi.data.torrentServer.service
+
+import android.app.Application
+import android.app.PendingIntent
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ServiceInfo
+import android.os.Build
+import android.os.IBinder
+import android.util.Log
+import eu.kanade.tachiyomi.BuildConfig
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.notification.Notifications
+import eu.kanade.tachiyomi.data.torrentServer.TorrentServerApi
+import eu.kanade.tachiyomi.data.torrentServer.TorrentServerUtils
+import eu.kanade.tachiyomi.util.system.cancelNotification
+import eu.kanade.tachiyomi.util.system.notificationBuilder
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import tachiyomi.core.common.i18n.stringResource
+import tachiyomi.i18n.MR
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+import kotlin.coroutines.EmptyCoroutineContext
+
+class TorrentServerService : Service() {
+ private val serviceScope = CoroutineScope(EmptyCoroutineContext)
+ private val applicationContext = Injekt.get()
+
+ override fun onBind(intent: Intent?): IBinder? = null
+
+ override fun onStartCommand(
+ intent: Intent?,
+ flags: Int,
+ startId: Int,
+ ): Int {
+ intent?.let {
+ if (it.action != null) {
+ when (it.action) {
+ ACTION_START -> {
+ startServer()
+ notification(applicationContext)
+ return START_STICKY
+ }
+ ACTION_STOP -> {
+ stopServer()
+ return START_NOT_STICKY
+ }
+ }
+ }
+ }
+ return START_NOT_STICKY
+ }
+
+ private fun startServer() {
+ serviceScope.launch {
+ if (TorrentServerApi.echo() == "") {
+ if (BuildConfig.DEBUG) Log.d("TorrentService", "startServer()")
+ torrServer.TorrServer.startTorrentServer(filesDir.absolutePath)
+ wait(10)
+ TorrentServerUtils.setTrackersList()
+ }
+ }
+ }
+
+ private fun stopServer() {
+ serviceScope.launch {
+ if (BuildConfig.DEBUG) Log.d("TorrentService", "stopServer()")
+ torrServer.TorrServer.stopTorrentServer()
+ TorrentServerApi.shutdown()
+ applicationContext.cancelNotification(Notifications.ID_TORRENT_SERVER)
+ stopSelf()
+ }
+ }
+
+ private fun notification(context: Context) {
+ // fuck android 14
+ val startAgainIntent = PendingIntent.getService(
+ applicationContext,
+ 0,
+ Intent(applicationContext, TorrentServerService::class.java).apply {
+ action = ACTION_START
+ },
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
+ )
+ val exitPendingIntent =
+ PendingIntent.getService(
+ applicationContext,
+ 0,
+ Intent(applicationContext, TorrentServerService::class.java).apply {
+ action = ACTION_STOP
+ },
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
+ )
+
+ val builder = context.notificationBuilder(Notifications.CHANNEL_TORRENT_SERVER) {
+ setSmallIcon(R.drawable.ic_ani)
+ setContentText(stringResource(MR.strings.torrentserver_is_running))
+ setContentTitle(stringResource(MR.strings.app_name))
+ setAutoCancel(false)
+ setOngoing(true)
+ setDeleteIntent(startAgainIntent)
+ setUsesChronometer(true)
+ addAction(
+ R.drawable.ic_close_24dp,
+ "Stop",
+ exitPendingIntent,
+ )
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ startForeground(
+ Notifications.ID_TORRENT_SERVER,
+ builder.build(),
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
+ )
+ } else {
+ startForeground(Notifications.ID_TORRENT_SERVER, builder.build())
+ }
+ }
+
+ companion object {
+ const val ACTION_START = "start_torrent_server"
+ const val ACTION_STOP = "stop_torrent_server"
+ val applicationContext = Injekt.get()
+
+ suspend fun start() {
+ try {
+ val intent =
+ Intent(applicationContext, TorrentServerService::class.java).apply {
+ action = ACTION_START
+ }
+ applicationContext.startService(intent)
+ wait(10)
+ } catch (e: Exception) {
+ if (BuildConfig.DEBUG) Log.d("TorrentService", "start() error: ${e.message}")
+ e.printStackTrace()
+ }
+ }
+
+ fun stop() {
+ try {
+ val intent =
+ Intent(applicationContext, TorrentServerService::class.java).apply {
+ action = ACTION_STOP
+ }
+ applicationContext.startService(intent)
+ } catch (e: Exception) {
+ if (BuildConfig.DEBUG) Log.d("TorrentService", "stop() error: ${e.message}")
+ e.printStackTrace()
+ }
+ }
+
+ private suspend fun wait(timeout: Int = -1): Boolean {
+ var count = 0
+ if (timeout < 0) {
+ count = -20
+ }
+ while (TorrentServerApi.echo() == "") {
+ delay(1000)
+ count++
+ if (count > timeout) {
+ return false
+ }
+ }
+ return true
+ }
+ }
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/di/PreferenceModule.kt b/app/src/main/java/eu/kanade/tachiyomi/di/PreferenceModule.kt
index 85172316a4..9372e43b0d 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/di/PreferenceModule.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/di/PreferenceModule.kt
@@ -6,6 +6,7 @@ import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.domain.track.service.TrackPreferences
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.tachiyomi.core.security.SecurityPreferences
+import eu.kanade.tachiyomi.data.torrentServer.TorrentServerPreferences
import eu.kanade.tachiyomi.network.NetworkPreferences
import eu.kanade.tachiyomi.ui.player.settings.PlayerPreferences
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
@@ -69,5 +70,8 @@ class PreferenceModule(val app: Application) : InjektModule {
addSingletonFactory {
BasePreferences(app, get())
}
+ addSingletonFactory {
+ TorrentServerPreferences(get())
+ }
}
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/anime/api/AnimeExtensionApi.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/anime/api/AnimeExtensionApi.kt
index eba63e4a4e..88762f7d27 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/extension/anime/api/AnimeExtensionApi.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/extension/anime/api/AnimeExtensionApi.kt
@@ -117,6 +117,7 @@ internal class AnimeExtensionApi {
libVersion = it.extractLibVersion(),
lang = it.lang,
isNsfw = it.nsfw == 1,
+ isTorrent = it.torrent == 1,
sources = it.sources?.map(extensionAnimeSourceMapper).orEmpty(),
apkName = it.apk,
iconUrl = "$repoUrl/icon/${it.pkg}.png",
@@ -143,6 +144,7 @@ private data class AnimeExtensionJsonObject(
val code: Long,
val version: String,
val nsfw: Int,
+ val torrent: Int = 0,
val sources: List?,
)
diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/anime/model/AnimeExtension.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/anime/model/AnimeExtension.kt
index 2d8345cddc..79c4b8943a 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/extension/anime/model/AnimeExtension.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/extension/anime/model/AnimeExtension.kt
@@ -13,6 +13,7 @@ sealed class AnimeExtension {
abstract val libVersion: Double
abstract val lang: String?
abstract val isNsfw: Boolean
+ abstract val isTorrent: Boolean
data class Installed(
override val name: String,
@@ -22,6 +23,7 @@ sealed class AnimeExtension {
override val libVersion: Double,
override val lang: String,
override val isNsfw: Boolean,
+ override val isTorrent: Boolean,
val pkgFactory: String?,
val sources: List,
val icon: Drawable?,
@@ -39,6 +41,7 @@ sealed class AnimeExtension {
override val libVersion: Double,
override val lang: String,
override val isNsfw: Boolean,
+ override val isTorrent: Boolean,
val sources: List,
val apkName: String,
val iconUrl: String,
@@ -70,5 +73,6 @@ sealed class AnimeExtension {
val signatureHash: String,
override val lang: String? = null,
override val isNsfw: Boolean = false,
+ override val isTorrent: Boolean = false,
) : AnimeExtension()
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/anime/util/AnimeExtensionLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/anime/util/AnimeExtensionLoader.kt
index 17508d589e..a088d25acb 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/extension/anime/util/AnimeExtensionLoader.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/extension/anime/util/AnimeExtensionLoader.kt
@@ -43,6 +43,7 @@ internal object AnimeExtensionLoader {
private const val METADATA_NSFW = "tachiyomi.animeextension.nsfw"
private const val METADATA_HAS_README = "tachiyomi.animeextension.hasReadme"
private const val METADATA_HAS_CHANGELOG = "tachiyomi.animeextension.hasChangelog"
+ private const val METADATA_TORRENT = "tachiyomi.animeextension.torrent"
const val LIB_VERSION_MIN = 12
const val LIB_VERSION_MAX = 15
@@ -282,6 +283,8 @@ internal object AnimeExtensionLoader {
return AnimeLoadResult.Error
}
+ val isTorrent = appInfo.metaData.getInt(METADATA_TORRENT) == 1
+
val classLoader = try {
ChildFirstPathClassLoader(appInfo.sourceDir, null, context.classLoader)
} catch (e: Exception) {
@@ -329,6 +332,7 @@ internal object AnimeExtensionLoader {
libVersion = libVersion,
lang = lang,
isNsfw = isNsfw,
+ isTorrent = isTorrent,
sources = sources,
pkgFactory = appInfo.metaData.getString(METADATA_SOURCE_FACTORY),
icon = appInfo.loadIcon(pkgManager),
diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/anime/AnimeSourceExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/source/anime/AnimeSourceExtensions.kt
index 8d1b087c50..ceb13a7b66 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/source/anime/AnimeSourceExtensions.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/source/anime/AnimeSourceExtensions.kt
@@ -31,3 +31,10 @@ fun AnimeSource.getNameForAnimeInfo(): String {
}
fun AnimeSource.isLocalOrStub(): Boolean = isLocal() || this is StubAnimeSource
+
+fun AnimeSource?.isSourceForTorrents(): Boolean {
+ if (this == null || this.isLocalOrStub()) return false
+ val sourceUsed = Injekt.get().installedExtensionsFlow.value
+ .find { ext -> ext.sources.any { it.id == this.id } }!!
+ return sourceUsed.isTorrent
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/torrentutils/TorrentUtils.kt b/app/src/main/java/eu/kanade/tachiyomi/torrentutils/TorrentUtils.kt
new file mode 100644
index 0000000000..dc676b16ca
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/torrentutils/TorrentUtils.kt
@@ -0,0 +1,34 @@
+package eu.kanade.tachiyomi.torrentutils
+
+import eu.kanade.tachiyomi.data.torrentServer.TorrentServerApi
+import eu.kanade.tachiyomi.torrentutils.model.DeadTorrentException
+import eu.kanade.tachiyomi.torrentutils.model.TorrentFile
+import eu.kanade.tachiyomi.torrentutils.model.TorrentInfo
+import java.net.SocketTimeoutException
+
+/**
+ * Used by extensions.
+ */
+@Suppress("UNUSED")
+object TorrentUtils {
+ suspend fun getTorrentInfo(
+ url: String,
+ title: String,
+ ): TorrentInfo {
+ try {
+ val torrent = TorrentServerApi.addTorrent(url, title, "", "", false)
+ return TorrentInfo(
+ torrent.title,
+ torrent.file_stats?.map { file ->
+ TorrentFile(file.path, file.id ?: 0, file.length, torrent.hash!!, torrent.trackers ?: emptyList())
+ } ?: emptyList(),
+ torrent.hash!!,
+ torrent.torrent_size!!,
+ torrent.trackers ?: emptyList(),
+ )
+ } catch (e: SocketTimeoutException) {
+ throw DeadTorrentException()
+ }
+
+ }
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/torrentutils/model/TorrentFile.kt b/app/src/main/java/eu/kanade/tachiyomi/torrentutils/model/TorrentFile.kt
new file mode 100644
index 0000000000..d2ab0c6bba
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/torrentutils/model/TorrentFile.kt
@@ -0,0 +1,18 @@
+package eu.kanade.tachiyomi.torrentutils.model
+
+import eu.kanade.tachiyomi.data.torrentServer.TorrentServerUtils
+import java.io.File
+import java.net.URLEncoder
+
+data class TorrentFile(
+ val path: String,
+ val indexFile: Int,
+ val size: Long,
+ private val torrentHash: String,
+ private val trackers : List = emptyList(),
+) {
+ fun toMagnetURI(): String {
+ val trackers = trackers.joinToString("&tr=") { URLEncoder.encode(it, "UTF-8") }
+ return "magnet:?xt=urn:btih:$torrentHash${if (trackers.isNotEmpty()) "&tr=$trackers" else ""}&index=$indexFile"
+ }
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/torrentutils/model/TorrentInfo.kt b/app/src/main/java/eu/kanade/tachiyomi/torrentutils/model/TorrentInfo.kt
new file mode 100644
index 0000000000..f06bdf3772
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/torrentutils/model/TorrentInfo.kt
@@ -0,0 +1,15 @@
+package eu.kanade.tachiyomi.torrentutils.model
+
+data class TorrentInfo(
+ val title: String,
+ val files: List,
+ val hash: String,
+ val size: Long,
+ val trackers: List = emptyList(),
+){
+ fun setTrackers(trackers: List): TorrentInfo {
+ return TorrentInfo(title, files, hash, size, trackers)
+ }
+}
+
+class DeadTorrentException : Exception()
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/entries/anime/AnimeScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/entries/anime/AnimeScreen.kt
index f2067a38fa..5a3565c2b0 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/entries/anime/AnimeScreen.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/entries/anime/AnimeScreen.kt
@@ -38,7 +38,9 @@ import eu.kanade.presentation.util.formatEpisodeNumber
import eu.kanade.presentation.util.isTabletUi
import eu.kanade.tachiyomi.animesource.AnimeSource
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
+import eu.kanade.tachiyomi.data.torrentServer.service.TorrentServerService
import eu.kanade.tachiyomi.source.anime.isLocalOrStub
+import eu.kanade.tachiyomi.source.anime.isSourceForTorrents
import eu.kanade.tachiyomi.ui.browse.anime.migration.search.MigrateAnimeSearchScreen
import eu.kanade.tachiyomi.ui.browse.anime.source.browse.BrowseAnimeSourceScreen
import eu.kanade.tachiyomi.ui.browse.anime.source.globalsearch.GlobalAnimeSearchScreen
@@ -116,6 +118,9 @@ class AnimeScreen(
onBackClicked = navigator::pop,
onEpisodeClicked = { episode, alt ->
scope.launchIO {
+ if (successState.source.isSourceForTorrents()) {
+ TorrentServerService.start()
+ }
val extPlayer = screenModel.alwaysUseExternalPlayer != alt
openEpisode(context, episode, extPlayer)
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/entries/anime/AnimeScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/entries/anime/AnimeScreenModel.kt
index b217c438f8..66066a5c22 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/entries/anime/AnimeScreenModel.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/entries/anime/AnimeScreenModel.kt
@@ -25,10 +25,12 @@ import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadCache
import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadManager
import eu.kanade.tachiyomi.data.download.anime.model.AnimeDownload
+import eu.kanade.tachiyomi.data.torrentServer.service.TorrentServerService
import eu.kanade.tachiyomi.data.track.AnimeTracker
import eu.kanade.tachiyomi.data.track.EnhancedAnimeTracker
import eu.kanade.tachiyomi.data.track.TrackerManager
import eu.kanade.tachiyomi.network.HttpException
+import eu.kanade.tachiyomi.source.anime.isSourceForTorrents
import eu.kanade.tachiyomi.ui.entries.anime.track.AnimeTrackItem
import eu.kanade.tachiyomi.ui.player.settings.PlayerPreferences
import eu.kanade.tachiyomi.util.AniChartApi
@@ -191,11 +193,17 @@ class AnimeScreenModel(
val needRefreshInfo = !anime.initialized
val needRefreshEpisode = episodes.isEmpty()
+ val animeSource = Injekt.get().getOrStub(anime.source)
+
+ if (animeSource.isSourceForTorrents()) {
+ TorrentServerService.start()
+ }
+
// Show what we have earlier
mutableState.update {
State.Success(
anime = anime,
- source = Injekt.get().getOrStub(anime.source),
+ source = animeSource,
isFromSource = isFromSource,
episodes = episodes,
isRefreshingData = needRefreshInfo || needRefreshEpisode,
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerActivity.kt
index edc3fd2729..a5489c3893 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerActivity.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerActivity.kt
@@ -49,6 +49,9 @@ import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.notification.Notifications
+import eu.kanade.tachiyomi.data.torrentServer.TorrentServerApi
+import eu.kanade.tachiyomi.data.torrentServer.TorrentServerUtils
+import eu.kanade.tachiyomi.data.torrentServer.service.TorrentServerService
import eu.kanade.tachiyomi.databinding.PlayerActivityBinding
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
import eu.kanade.tachiyomi.ui.player.settings.PlayerPreferences
@@ -1619,11 +1622,49 @@ class PlayerActivity : BaseActivity() {
}
streams.subtitle.tracks = arrayOf(Track("nothing", "None")) + it.subtitleTracks.toTypedArray()
streams.audio.tracks = arrayOf(Track("nothing", "None")) + it.audioTracks.toTypedArray()
- MPVLib.command(arrayOf("loadfile", parseVideoUrl(it.videoUrl)))
+ if (it.videoUrl?.startsWith(TorrentServerUtils.hostUrl) == true ||
+ it.videoUrl?.startsWith("magnet") == true ||
+ it.videoUrl?.endsWith(".torrent") == true
+ ) {
+ launchIO {
+ TorrentServerService.start()
+ torrentLinkHandler(it.videoUrl!!, it.quality)
+ }
+ } else {
+ MPVLib.command(arrayOf("loadfile", parseVideoUrl(it.videoUrl)))
+ }
}
refreshUi()
}
+ private suspend fun torrentLinkHandler(videoUrl: String, quality: String) {
+ var index = 0
+
+ // check if link is from localSource
+ if (videoUrl.startsWith("content://")) {
+ val videoInputStream = applicationContext.contentResolver.openInputStream(Uri.parse(videoUrl))
+ val torrent = TorrentServerApi.uploadTorrent(videoInputStream!!, quality, "", "", false)
+ val torrentUrl = TorrentServerUtils.getTorrentPlayLink(torrent, 0)
+ MPVLib.command(arrayOf("loadfile", torrentUrl))
+ return
+ }
+
+ // check if link is from magnet, in that check if index is present
+ if (videoUrl.startsWith("magnet")) {
+ if (videoUrl.contains("index=")) {
+ index = try {
+ videoUrl.substringAfter("index=").substringBefore("&").toInt()
+ } catch (e: NumberFormatException) {
+ 0
+ }
+ }
+ }
+
+ val currentTorrent = TorrentServerApi.addTorrent(videoUrl, quality, "", "", false)
+ val videoTorrentUrl = TorrentServerUtils.getTorrentPlayLink(currentTorrent, index)
+ MPVLib.command(arrayOf("loadfile", videoTorrentUrl))
+ }
+
private fun parseVideoUrl(videoUrl: String?): String? {
val uri = Uri.parse(videoUrl)
return openContentFd(uri) ?: videoUrl
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 71ca302197..9efbeddb1f 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -106,6 +106,8 @@ seeker = "io.github.2307vivek:seeker:1.1.1"
truetypeparser = "io.github.yubyf:truetypeparser-light:2.1.4"
+torrentserver = "com.github.Diegopyl1209:torrentserver-aniyomi:c18f58e51b"
+
[bundles]
archive = ["common-compress", "junrar"]
acra = ["acra-http", "acra-scheduler"]
diff --git a/i18n/src/commonMain/resources/MR/base/strings.xml b/i18n/src/commonMain/resources/MR/base/strings.xml
index 65d7ca309a..dcf53fff52 100644
--- a/i18n/src/commonMain/resources/MR/base/strings.xml
+++ b/i18n/src/commonMain/resources/MR/base/strings.xml
@@ -1140,4 +1140,9 @@
Set to 0 to disable the speed limit.
Enable MPV scripts
Needs external storage permission.
+ Torrent Server is running
+ Torrent Server Preferences
+ Torrent Server Port
+ Torrent Trackers
+ Reset default torrent trackers string
\ No newline at end of file
diff --git a/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/Archive.kt b/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/Archive.kt
index 3a19de5a3a..debe15f759 100644
--- a/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/Archive.kt
+++ b/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/Archive.kt
@@ -5,7 +5,7 @@ import tachiyomi.core.common.storage.extension
object ArchiveAnime {
- private val SUPPORTED_ARCHIVE_TYPES = listOf("avi", "flv", "mkv", "mov", "mp4", "webm", "wmv")
+ private val SUPPORTED_ARCHIVE_TYPES = listOf("avi", "flv", "mkv", "mov", "mp4", "webm", "wmv", "torrent")
fun isSupported(file: UniFile): Boolean = with(file) {
return file.extension in SUPPORTED_ARCHIVE_TYPES