From 75828e95203f2933089ad57bf6f527b45dd463be Mon Sep 17 00:00:00 2001 From: DiegoPYL1209 Date: Mon, 17 Jun 2024 21:05:29 -0400 Subject: [PATCH 01/14] basic implementation of torrent server --- .gitattributes | 3 + .github/workflows/build_push.yml | 54 ++ app/build.gradle.kts | 2 + app/src/main/AndroidManifest.xml | 5 + .../settings/screen/SettingsPlayerScreen.kt | 58 ++ .../data/notification/Notifications.kt | 10 + .../data/torrentServer/TorrentServerApi.kt | 107 ++++ .../torrentServer/TorrentServerPreferences.kt | 39 ++ .../data/torrentServer/TorrentServerUtils.kt | 36 ++ .../data/torrentServer/model/BTSets.kt | 39 ++ .../torrentServer/model/TorrServVersion.kt | 9 + .../data/torrentServer/model/Torrent.kt | 48 ++ .../torrentServer/model/TorrentDetails.kt | 23 + .../torrentServer/model/TorrentRequest.kt | 43 ++ .../service/TorrentServerService.kt | 170 ++++++ .../kanade/tachiyomi/di/PreferenceModule.kt | 4 + .../tachiyomi/ui/player/PlayerActivity.kt | 44 +- go/torrserver/bindings/main.go | 21 + go/torrserver/go.mod | 78 +++ go/torrserver/go.sum | 527 ++++++++++++++++++ go/torrserver/log/log.go | 121 ++++ go/torrserver/mimetype/mimetype.go | 144 +++++ go/torrserver/server.go | 106 ++++ go/torrserver/settings/btsets.go | 147 +++++ go/torrserver/settings/db.go | 171 ++++++ go/torrserver/settings/migrate.go | 102 ++++ go/torrserver/settings/settings.go | 36 ++ go/torrserver/settings/torrent.go | 81 +++ go/torrserver/settings/viewed.go | 100 ++++ go/torrserver/torr/apihelper.go | 253 +++++++++ go/torrserver/torr/btserver.go | 273 +++++++++ go/torrserver/torr/dbwrapper.go | 82 +++ go/torrserver/torr/preload.go | 168 ++++++ go/torrserver/torr/state/state.go | 75 +++ go/torrserver/torr/storage/state/state.go | 30 + go/torrserver/torr/storage/storage.go | 12 + go/torrserver/torr/storage/torrstor/cache.go | 370 ++++++++++++ .../torr/storage/torrstor/diskpiece.go | 85 +++ .../torr/storage/torrstor/mempiece.go | 70 +++ go/torrserver/torr/storage/torrstor/piece.go | 81 +++ .../torr/storage/torrstor/piecefake.go | 34 ++ go/torrserver/torr/storage/torrstor/ranges.go | 52 ++ go/torrserver/torr/storage/torrstor/reader.go | 206 +++++++ .../torr/storage/torrstor/storage.go | 72 +++ go/torrserver/torr/stream.go | 91 +++ go/torrserver/torr/torrent.go | 361 ++++++++++++ go/torrserver/torr/utils/blockedIP.go | 35 ++ go/torrserver/torr/utils/freemem.go | 15 + go/torrserver/torr/utils/torrent.go | 89 +++ go/torrserver/torr/utils/webImageChecker.go | 36 ++ go/torrserver/utils/filetypes.go | 99 ++++ go/torrserver/utils/location.go | 14 + go/torrserver/utils/prallel.go | 17 + go/torrserver/utils/strings.go | 48 ++ go/torrserver/version/version.go | 27 + go/torrserver/web/api/cache.go | 63 +++ go/torrserver/web/api/download.go | 64 +++ go/torrserver/web/api/m3u.go | 182 ++++++ go/torrserver/web/api/play.go | 86 +++ go/torrserver/web/api/route.go | 37 ++ go/torrserver/web/api/settings.go | 53 ++ go/torrserver/web/api/shutdown.go | 31 ++ go/torrserver/web/api/stream.go | 247 ++++++++ go/torrserver/web/api/torrents.go | 179 ++++++ go/torrserver/web/api/upload.go | 93 ++++ go/torrserver/web/api/utils/link.go | 137 +++++ go/torrserver/web/api/viewed.go | 71 +++ go/torrserver/web/server.go | 133 +++++ .../commonMain/resources/MR/base/strings.xml | 5 + 69 files changed, 6403 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/torrentServer/TorrentServerApi.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/torrentServer/TorrentServerPreferences.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/torrentServer/TorrentServerUtils.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/torrentServer/model/BTSets.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/torrentServer/model/TorrServVersion.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/torrentServer/model/Torrent.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/torrentServer/model/TorrentDetails.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/torrentServer/model/TorrentRequest.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/torrentServer/service/TorrentServerService.kt create mode 100644 go/torrserver/bindings/main.go create mode 100644 go/torrserver/go.mod create mode 100644 go/torrserver/go.sum create mode 100644 go/torrserver/log/log.go create mode 100644 go/torrserver/mimetype/mimetype.go create mode 100644 go/torrserver/server.go create mode 100644 go/torrserver/settings/btsets.go create mode 100644 go/torrserver/settings/db.go create mode 100644 go/torrserver/settings/migrate.go create mode 100644 go/torrserver/settings/settings.go create mode 100644 go/torrserver/settings/torrent.go create mode 100644 go/torrserver/settings/viewed.go create mode 100644 go/torrserver/torr/apihelper.go create mode 100644 go/torrserver/torr/btserver.go create mode 100644 go/torrserver/torr/dbwrapper.go create mode 100644 go/torrserver/torr/preload.go create mode 100644 go/torrserver/torr/state/state.go create mode 100644 go/torrserver/torr/storage/state/state.go create mode 100644 go/torrserver/torr/storage/storage.go create mode 100644 go/torrserver/torr/storage/torrstor/cache.go create mode 100644 go/torrserver/torr/storage/torrstor/diskpiece.go create mode 100644 go/torrserver/torr/storage/torrstor/mempiece.go create mode 100644 go/torrserver/torr/storage/torrstor/piece.go create mode 100644 go/torrserver/torr/storage/torrstor/piecefake.go create mode 100644 go/torrserver/torr/storage/torrstor/ranges.go create mode 100644 go/torrserver/torr/storage/torrstor/reader.go create mode 100644 go/torrserver/torr/storage/torrstor/storage.go create mode 100644 go/torrserver/torr/stream.go create mode 100644 go/torrserver/torr/torrent.go create mode 100644 go/torrserver/torr/utils/blockedIP.go create mode 100644 go/torrserver/torr/utils/freemem.go create mode 100644 go/torrserver/torr/utils/torrent.go create mode 100644 go/torrserver/torr/utils/webImageChecker.go create mode 100644 go/torrserver/utils/filetypes.go create mode 100644 go/torrserver/utils/location.go create mode 100644 go/torrserver/utils/prallel.go create mode 100644 go/torrserver/utils/strings.go create mode 100644 go/torrserver/version/version.go create mode 100644 go/torrserver/web/api/cache.go create mode 100644 go/torrserver/web/api/download.go create mode 100644 go/torrserver/web/api/m3u.go create mode 100644 go/torrserver/web/api/play.go create mode 100644 go/torrserver/web/api/route.go create mode 100644 go/torrserver/web/api/settings.go create mode 100644 go/torrserver/web/api/shutdown.go create mode 100644 go/torrserver/web/api/stream.go create mode 100644 go/torrserver/web/api/torrents.go create mode 100644 go/torrserver/web/api/upload.go create mode 100644 go/torrserver/web/api/utils/link.go create mode 100644 go/torrserver/web/api/viewed.go create mode 100644 go/torrserver/web/server.go diff --git a/.gitattributes b/.gitattributes index 1ca443da48..b106a97c24 100644 --- a/.gitattributes +++ b/.gitattributes @@ -22,3 +22,6 @@ *.woff binary *.pyc binary *.swp binary + +# Libs +*.aar binary diff --git a/.github/workflows/build_push.yml b/.github/workflows/build_push.yml index 4b473c5333..7dc76ad803 100644 --- a/.github/workflows/build_push.yml +++ b/.github/workflows/build_push.yml @@ -23,6 +23,60 @@ jobs: steps: - name: Clone repo uses: actions/checkout@v4 + + - uses: dorny/paths-filter@v2 + id: filter + with: + filters: | + gofiles: + - 'go/**' + + - name: Set up Go + if: steps.filter.outputs.gofiles == 'true' + uses: actions/setup-go@v2 + with: + go-version: '1.21' + + - name: Install Android SDK + if: steps.filter.outputs.gofiles == 'true' + uses: android-actions/setup-android@v2 + with: + sdk-version: '30' + + - name: Install Android NDK + if: steps.filter.outputs.gofiles == 'true' + uses: nttld/setup-ndk@v1 + with: + ndk-version: r25b + + - name: Install GoMobile + if: steps.filter.outputs.gofiles == 'true' + run: go install golang.org/x/mobile/cmd/gomobile@latest + + - name: Build GoMobile app + if: steps.filter.outputs.gofiles == 'true' + run: | + cd go/torrserver + go get golang.org/x/mobile/bind + gomobile init + cd bindings + gomobile bind -target=android -androidapi 23 -ldflags "-s -w" -o ../../../app/libs/server.aar + + - name: Commit build library + if: steps.filter.outputs.gofiles == 'true' + run: | + git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + git add app/libs/server.aar + git add app/libs/server-sources.jar + git commit -m "Add compiled torrserver library" + + - name: Push changes + if: steps.filter.outputs.gofiles == 'true' + uses: ad-m/github-push-action@master + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + branch: master - name: Validate Gradle Wrapper uses: gradle/wrapper-validation-action@v1 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 47fa9b9ecd..b7ce2759f4 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(files("libs/server.aar")) } androidComponents { 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 +90,7 @@ object SettingsPlayerScreen : SearchableSettings { playerPreferences = playerPreferences, basePreferences = basePreferences, ), + getTorrentServerGroup(torrentServerPreferences), ) } @@ -394,6 +401,57 @@ object SettingsPlayerScreen : SearchableSettings { ) } + @Composable + private fun getTorrentServerGroup( + torrentServerPreferences: TorrentServerPreferences, + ): Preference.PreferenceGroup { + val scope = rememberCoroutineScope() + val context = LocalContext.current + 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 = context.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() + context.stringResource(MR.strings.requires_app_restart) + }, + ), + ), + ) + } + @Composable private fun SkipIntroLengthDialog( initialSkipIntroLength: Int, 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 a04f872c35..bd04b6a2fc 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..edabec48b2 --- /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.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..000c7d3feb --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/torrentServer/service/TorrentServerService.kt @@ -0,0 +1,170 @@ +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.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() + + fun start() { + try { + val intent = + Intent(applicationContext, TorrentServerService::class.java).apply { + action = ACTION_START + } + applicationContext.startService(intent) + } 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() + } + } + + 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 fe6d2160d2..05c7cce7b1 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/ui/player/PlayerActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerActivity.kt index 4157e4fe8c..2b74f7784c 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 @@ -1617,11 +1620,50 @@ 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() + TorrentServerService.wait(10) + 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=").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/go/torrserver/bindings/main.go b/go/torrserver/bindings/main.go new file mode 100644 index 0000000000..af63369871 --- /dev/null +++ b/go/torrserver/bindings/main.go @@ -0,0 +1,21 @@ +package torrServer + +import ( + server "server" +) + +func StartTorrentServer(pathdb string) { + server.Start(pathdb, "", false, false) +} + +func WaitTorrentServer() { + server.WaitServer() +} + +func StopTorrentServer() { + server.Stop() +} + +func AddTrackers(trackers string) { + server.AddTrackers(trackers) +} diff --git a/go/torrserver/go.mod b/go/torrserver/go.mod new file mode 100644 index 0000000000..2fe322d332 --- /dev/null +++ b/go/torrserver/go.mod @@ -0,0 +1,78 @@ +module server + +go 1.20 + +replace github.com/anacrolix/torrent v1.53.3 => github.com/tsynik/torrent v1.2.13 + +require ( + github.com/anacrolix/dms v1.6.0 + github.com/anacrolix/log v0.14.5 // indirect + github.com/anacrolix/missinggo/v2 v2.7.3 + github.com/anacrolix/publicip v0.3.0 + github.com/anacrolix/torrent v1.53.3 + github.com/gin-contrib/cors v1.5.0 + github.com/gin-contrib/location v0.0.2 + github.com/gin-gonic/gin v1.9.1 + github.com/pkg/errors v0.9.1 + go.etcd.io/bbolt v1.3.8 + golang.org/x/image v0.16.0 + golang.org/x/time v0.5.0 +) + +require ( + github.com/RoaringBitmap/roaring v1.7.0 // indirect + github.com/alecthomas/atomic v0.1.0-alpha2 // indirect + github.com/anacrolix/chansync v0.4.0 // indirect + github.com/anacrolix/dht/v2 v2.21.0 // indirect + github.com/anacrolix/generics v0.0.0-20230911070922-5dd7545c6b13 // indirect + github.com/anacrolix/missinggo v1.3.0 // indirect + github.com/anacrolix/missinggo/perf v1.0.0 // indirect + github.com/anacrolix/multiless v0.3.1-0.20221221005021-2d12701f83f7 // indirect + github.com/anacrolix/stm v0.5.0 // indirect + github.com/anacrolix/sync v0.5.1 // indirect + github.com/anacrolix/upnp v0.1.3 // indirect + github.com/anacrolix/utp v0.2.0 // indirect + github.com/benbjohnson/immutable v0.4.3 // indirect + github.com/bits-and-blooms/bitset v1.13.0 // indirect + github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect + github.com/bytedance/sonic v1.10.2 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect + github.com/chenzhuoyu/iasm v0.9.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/edsrzf/mmap-go v1.1.0 // indirect + github.com/frankban/quicktest v1.14.6 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.17.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/google/btree v1.1.2 // indirect + github.com/huandu/xstrings v1.4.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.6 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mschoch/smat v0.2.0 // indirect + github.com/pelletier/go-toml/v2 v2.1.1 // indirect + github.com/rogpeppe/go-internal v1.11.0 // indirect + github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529 // indirect + github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + golang.org/x/arch v0.7.0 // indirect + golang.org/x/crypto v0.23.0 // indirect + golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect + golang.org/x/mobile v0.0.0-20240506190922-a1a533f289d3 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/text v0.15.0 // indirect + golang.org/x/tools v0.21.0 // indirect + google.golang.org/protobuf v1.32.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go/torrserver/go.sum b/go/torrserver/go.sum new file mode 100644 index 0000000000..c766e3cb7d --- /dev/null +++ b/go/torrserver/go.sum @@ -0,0 +1,527 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +crawshaw.io/iox v0.0.0-20181124134642-c51c3df30797/go.mod h1:sXBiorCo8c46JlQV3oXPKINnZ8mcqnye1EkVkqsectk= +crawshaw.io/sqlite v0.3.2/go.mod h1:igAO5JulrQ1DbdZdtVq48mnZUBAPOeFzer7VhDWNtW4= +filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/RoaringBitmap/roaring v0.4.7/go.mod h1:8khRDP4HmeXns4xIj9oGrKSz7XTQiJx2zgh7AcNke4w= +github.com/RoaringBitmap/roaring v0.4.17/go.mod h1:D3qVegWTmfCaX4Bl5CrBE9hfrSrrXIr8KVNvRsDi1NI= +github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo= +github.com/RoaringBitmap/roaring v1.7.0 h1:OZF303tJCER1Tj3x+aArx/S5X7hrT186ri6JjrGvG68= +github.com/RoaringBitmap/roaring v1.7.0/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90= +github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= +github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8= +github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= +github.com/alecthomas/assert/v2 v2.0.0-alpha3 h1:pcHeMvQ3OMstAWgaeaXIAL8uzB9xMm2zlxt+/4ml8lk= +github.com/alecthomas/atomic v0.1.0-alpha2 h1:dqwXmax66gXvHhsOS4pGPZKqYOlTkapELkLb3MNdlH8= +github.com/alecthomas/atomic v0.1.0-alpha2/go.mod h1:zD6QGEyw49HIq19caJDc2NMXAy8rNi9ROrxtMXATfyI= +github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142 h1:8Uy0oSf5co/NZXje7U1z8Mpep++QJOldL2hs/sBQf48= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alexflint/go-arg v1.4.3 h1:9rwwEBpMXfKQKceuZfYcwuc/7YY7tWJbFsgG5cAU/uo= +github.com/alexflint/go-arg v1.4.3/go.mod h1:3PZ/wp/8HuqRZMUUgu7I+e1qcpUbvmS258mRXkFH4IA= +github.com/alexflint/go-scalar v1.1.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o= +github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw= +github.com/alexflint/go-scalar v1.2.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o= +github.com/anacrolix/chansync v0.4.0 h1:Md0HM7zYCAO4KwNwgcIRgxNsMxiRuk7D1Ha0Uo+2y60= +github.com/anacrolix/chansync v0.4.0/go.mod h1:DZsatdsdXxD0WiwcGl0nJVwyjCKMDv+knl1q2iBjA2k= +github.com/anacrolix/dht/v2 v2.21.0 h1:8nzI+faaynY9jOKmVgdmBZVrTo8B7ZE/LKEgN3Vl/Bs= +github.com/anacrolix/dht/v2 v2.21.0/go.mod h1:SDGC+sEs1pnO2sJGYuhvIis7T8749dDHNfcjtdH4e3g= +github.com/anacrolix/dms v1.6.0 h1:v2g1Y+Fc/ICSEc+7M6B92oFcfcqa5LXYPhE4Hcm5tVA= +github.com/anacrolix/dms v1.6.0/go.mod h1:5fAMpBcPFG4WQFh91zhf2E7/KYZ3/WmmRAf/WMoL0Q0= +github.com/anacrolix/envpprof v0.0.0-20180404065416-323002cec2fa/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c= +github.com/anacrolix/envpprof v1.0.0/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c= +github.com/anacrolix/envpprof v1.1.0/go.mod h1:My7T5oSqVfEn4MD4Meczkw/f5lSIndGAKu/0SM/rkf4= +github.com/anacrolix/envpprof v1.3.0 h1:WJt9bpuT7A/CDCxPOv/eeZqHWlle/Y0keJUvc6tcJDk= +github.com/anacrolix/ffprobe v1.0.0/go.mod h1:BIw+Bjol6CWjm/CRWrVLk2Vy+UYlkgmBZ05vpSYqZPw= +github.com/anacrolix/ffprobe v1.1.0 h1:eKBudnERW9zRJ0+ge6FzkQ0pWLyq142+FJrwRwSRMT4= +github.com/anacrolix/ffprobe v1.1.0/go.mod h1:MXe+zG/RRa5OdIf5+VYYfS/CfsSqOH7RrvGIqJBzqhI= +github.com/anacrolix/generics v0.0.0-20230113004304-d6428d516633/go.mod h1:ff2rHB/joTV03aMSSn/AZNnaIpUw0h3njetGsaXcMy8= +github.com/anacrolix/generics v0.0.0-20230911070922-5dd7545c6b13 h1:qwOprPTDMM3BASJRf84mmZnTXRsPGGJ8xoHKQS7m3so= +github.com/anacrolix/generics v0.0.0-20230911070922-5dd7545c6b13/go.mod h1:ff2rHB/joTV03aMSSn/AZNnaIpUw0h3njetGsaXcMy8= +github.com/anacrolix/log v0.3.0/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU= +github.com/anacrolix/log v0.6.0/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU= +github.com/anacrolix/log v0.13.1/go.mod h1:D4+CvN8SnruK6zIFS/xPoRJmtvtnxs+CSfDQ+BFxZ68= +github.com/anacrolix/log v0.14.2/go.mod h1:1OmJESOtxQGNMlUO5rcv96Vpp9mfMqXXbe2RdinFLdY= +github.com/anacrolix/log v0.14.5 h1:OkMjBquVSRb742LkecSGDGaGpNoSrw4syRIm0eRdmrg= +github.com/anacrolix/log v0.14.5/go.mod h1:1OmJESOtxQGNMlUO5rcv96Vpp9mfMqXXbe2RdinFLdY= +github.com/anacrolix/missinggo v1.1.0/go.mod h1:MBJu3Sk/k3ZfGYcS7z18gwfu72Ey/xopPFJJbTi5yIo= +github.com/anacrolix/missinggo v1.1.2-0.20190815015349-b888af804467/go.mod h1:MBJu3Sk/k3ZfGYcS7z18gwfu72Ey/xopPFJJbTi5yIo= +github.com/anacrolix/missinggo v1.2.1/go.mod h1:J5cMhif8jPmFoC3+Uvob3OXXNIhOUikzMt+uUjeM21Y= +github.com/anacrolix/missinggo v1.3.0 h1:06HlMsudotL7BAELRZs0yDZ4yVXsHXGi323QBjAVASw= +github.com/anacrolix/missinggo v1.3.0/go.mod h1:bqHm8cE8xr+15uVfMG3BFui/TxyB6//H5fwlq/TeqMc= +github.com/anacrolix/missinggo/perf v1.0.0 h1:7ZOGYziGEBytW49+KmYGTaNfnwUqP1HBsy6BqESAJVw= +github.com/anacrolix/missinggo/perf v1.0.0/go.mod h1:ljAFWkBuzkO12MQclXzZrosP5urunoLS0Cbvb4V0uMQ= +github.com/anacrolix/missinggo/v2 v2.2.0/go.mod h1:o0jgJoYOyaoYQ4E2ZMISVa9c88BbUBVQQW4QeRkNCGY= +github.com/anacrolix/missinggo/v2 v2.5.1/go.mod h1:WEjqh2rmKECd0t1VhQkLGTdIWXO6f6NLjp5GlMZ+6FA= +github.com/anacrolix/missinggo/v2 v2.7.3 h1:Ee//CmZBMadeNiYB/hHo9ly2PFOEZ4Fhsbnug3rDAIE= +github.com/anacrolix/missinggo/v2 v2.7.3/go.mod h1:mIEtp9pgaXqt8VQ3NQxFOod/eQ1H0D1XsZzKUQfwtac= +github.com/anacrolix/multiless v0.3.1-0.20221221005021-2d12701f83f7 h1:lOtCD+LzoD1g7bowhYJNR++uV+FyY5bTZXKwnPex9S8= +github.com/anacrolix/multiless v0.3.1-0.20221221005021-2d12701f83f7/go.mod h1:zJv1JF9AqdZiHwxqPgjuOZDGWER6nyE48WBCi/OOrMM= +github.com/anacrolix/publicip v0.3.0 h1:QK+lvqNzZDznqWMe5lbnjdXsKb7Mvhqy6osV3J+HwPY= +github.com/anacrolix/publicip v0.3.0/go.mod h1:tF1kAG96Ao3t9Q8zyfA7Lso1wOEfHHEcZQTRI+PMm4k= +github.com/anacrolix/stm v0.2.0/go.mod h1:zoVQRvSiGjGoTmbM0vSLIiaKjWtNPeTvXUSdJQA4hsg= +github.com/anacrolix/stm v0.5.0 h1:9df1KBpttF0TzLgDq51Z+TEabZKMythqgx89f1FQJt8= +github.com/anacrolix/stm v0.5.0/go.mod h1:MOwrSy+jCm8Y7HYfMAwPj7qWVu7XoVvjOiYwJmpeB/M= +github.com/anacrolix/sync v0.3.0/go.mod h1:BbecHL6jDSExojhNtgTFSBcdGerzNc64tz3DCOj/I0g= +github.com/anacrolix/sync v0.5.1 h1:FbGju6GqSjzVoTgcXTUKkF041lnZkG5P0C3T5RL3SGc= +github.com/anacrolix/sync v0.5.1/go.mod h1:BbecHL6jDSExojhNtgTFSBcdGerzNc64tz3DCOj/I0g= +github.com/anacrolix/tagflag v0.0.0-20180109131632-2146c8d41bf0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw= +github.com/anacrolix/tagflag v1.0.0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw= +github.com/anacrolix/tagflag v1.1.0/go.mod h1:Scxs9CV10NQatSmbyjqmqmeQNwGzlNe0CMUMIxqHIG8= +github.com/anacrolix/upnp v0.1.3 h1:NlYEhE75adz2npEJKjbqyqnyW9qU4STookvSNXBJ5ao= +github.com/anacrolix/upnp v0.1.3/go.mod h1:Qyhbqo69gwNWvEk1xNTXsS5j7hMHef9hdr984+9fIic= +github.com/anacrolix/utp v0.2.0 h1:65Cdmr6q9WSw2KsM+rtJFu7rqDzLl2bdysf4KlNPcFI= +github.com/anacrolix/utp v0.2.0/go.mod h1:HGk4GYQw1O/3T1+yhqT/F6EcBd+AAwlo9dYErNy7mj8= +github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= +github.com/benbjohnson/immutable v0.2.0/go.mod h1:uc6OHo6PN2++n98KHLxW8ef4W42ylHiQSENghE1ezxI= +github.com/benbjohnson/immutable v0.4.3 h1:GYHcksoJ9K6HyAUpGxwZURrbTkXA0Dh4otXGqbhdrjA= +github.com/benbjohnson/immutable v0.4.3/go.mod h1:qJIKKSmdqz1tVzNtst1DZzvaqOU1onk1rc03IeM3Owk= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE= +github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/bradfitz/iter v0.0.0-20140124041915-454541ec3da2/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo= +github.com/bradfitz/iter v0.0.0-20190303215204-33e6a9893b0c/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo= +github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 h1:GKTyiRCL6zVf5wWaqKnf+7Qs6GbEPfd4iMOitWzXJx8= +github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8/go.mod h1:spo1JLcs67NmW1aVLEgtA8Yy1elc+X8y5SRW1sFW4Og= +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= +github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE= +github.com/bytedance/sonic v1.10.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0= +github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA= +github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= +github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0= +github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g= +github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/dustin/go-humanize v0.0.0-20180421182945-02af3965c54e/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ= +github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q= +github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/gin-contrib/cors v1.5.0 h1:DgGKV7DDoOn36DFkNtbHrjoRiT5ExCe+PC9/xp7aKvk= +github.com/gin-contrib/cors v1.5.0/go.mod h1:TvU7MAZ3EwrPLI2ztzTt3tqgvBCq+wn8WpZmfADjupI= +github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= +github.com/gin-contrib/location v0.0.2 h1:QZKh1+K/LLR4KG/61eIO3b7MLuKi8tytQhV6texLgP4= +github.com/gin-contrib/location v0.0.2/go.mod h1:NGoidiRlf0BlA/VKSVp+g3cuSMeTmip/63PhEjRhUAc= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= +github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= +github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= +github.com/glycerine/go-unsnap-stream v0.0.0-20190901134440-81cf024a9e0a/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= +github.com/glycerine/goconvey v0.0.0-20180728074245-46e3a41ad493/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= +github.com/glycerine/goconvey v0.0.0-20190315024820-982ee783a72e/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= +github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q= +github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs= +github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU= +github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4= +github.com/go-openapi/spec v0.20.14 h1:7CBlRnw+mtjFGlPDRZmAMnq35cRzI91xj03HVyUi/Do= +github.com/go-openapi/spec v0.20.14/go.mod h1:8EOhTpBoFiask8rrgwbLC3zmJfz4zsCUueRuPM6GNkw= +github.com/go-openapi/swag v0.22.8 h1:/9RjDSQ0vbFR+NyjGMkFTsA1IA0fmhKSThmfGZjicbw= +github.com/go-openapi/swag v0.22.8/go.mod h1:6QT22icPLEqAM/z/TChgb4WAveCHF92+2gF0CNjHpPI= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/go-playground/validator/v10 v10.17.0 h1:SmVVlfAOtlZncTxRuinDPomC2DkXJ4E5T9gDA0AIH74= +github.com/go-playground/validator/v10 v10.17.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180124185431-e89373fe6b4a/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= +github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20190309154008-847fc94819f9/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo= +github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4= +github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU= +github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= +github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/kljensen/snowball v0.9.0 h1:OpXkQBcic6vcPG+dChOGLIA/GNuVg47tbbIJ2s7Keas= +github.com/kljensen/snowball v0.9.0/go.mod h1:OGo5gFWjaeXqCu4iIrMl5OYip9XUJHGOU5eSkPjVg2A= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg= +github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM= +github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= +github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= +github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529 h1:18kd+8ZUlt/ARXhljq+14TwAoKa61q6dX8jtwOf6DH8= +github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529/go.mod h1:qe5TWALJ8/a1Lqznoc5BDHpYX/8HU60Hm2AwRmqzxqA= +github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 h1:GHRpF1pTW19a8tTFrMLUcfWwyC0pnifVo2ClaLq+hP8= +github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/assertions v0.0.0-20190215210624-980c5ac6f3ac/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= +github.com/smartystreets/goconvey v0.0.0-20190306220146-200a235640ff/go.mod h1:KSQcGKpxUMHk3nbYzs/tIBAM2iDooCn0BmttHOJEbLs= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= +github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= +github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M= +github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo= +github.com/swaggo/swag v1.16.2 h1:28Pp+8DkQoV+HLzLx8RGJZXNGKbFqnuvSbAAtoxiY04= +github.com/swaggo/swag v1.16.2/go.mod h1:6YzXnDcpr0767iOejs318CwYkCQqyGer6BizOg03f+E= +github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= +github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= +github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= +github.com/tsynik/torrent v1.2.13 h1:PyOTz6dpzsoIT0IhiEmg6H5VD+mFqF93Q2L9iFyqPtQ= +github.com/tsynik/torrent v1.2.13/go.mod h1:NDxg14AwVqi3PWt1oStYLnyUxHYHX3qGKBAZVh/6Jk8= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= +github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= +github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA= +go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= +go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc= +golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20220428152302-39d4317da171/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= +golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA= +golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= +golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8= +golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= +golang.org/x/image v0.16.0 h1:9kloLAKhUufZhA12l5fwnx2NZW39/we1UhBesW433jw= +golang.org/x/image v0.16.0/go.mod h1:ugSZItdV4nOxyqp56HmXwH0Ry0nBCpjnZdpDaIHdoPs= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mobile v0.0.0-20240112133503-c713f31d574b h1:kfWLZgb8iUBHdE9WydD5V5dHIS/F6HjlBZNyJfn2bs4= +golang.org/x/mobile v0.0.0-20240112133503-c713f31d574b/go.mod h1:4efzQnuA1nICq6h4kmZRMGzbPiP06lZvgADUu1VpJCE= +golang.org/x/mobile v0.0.0-20240404231514-09dbf07665ed h1:vZhAhVr5zF1IJaVKTawyTq78WSspLnK53iuMJ1fJgLc= +golang.org/x/mobile v0.0.0-20240404231514-09dbf07665ed/go.mod h1:z041I2NhLjANgIfD0XbB2AmUZ8sLUcSgyLaSNGEP50M= +golang.org/x/mobile v0.0.0-20240506190922-a1a533f289d3 h1:lXH7reX0gtet9FgdXR0WDs3t1nt0QTjDLt1rrBQ/Qgs= +golang.org/x/mobile v0.0.0-20240506190922-a1a533f289d3/go.mod h1:EiXZlVfUTaAyySFVJb9rsODuiO+WXu8HrUuySb7nYFw= +golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220524220425-1d687d428aca/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.8-0.20211029000441-d6a9af8af023/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= +golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= +golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= +golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= +golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw= +golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= +google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/vansante/go-ffprobe.v2 v2.1.1 h1:DIh5fMn+tlBvG7pXyUZdemVmLdERnf2xX6XOFF+0BBU= +gopkg.in/vansante/go-ffprobe.v2 v2.1.1/go.mod h1:qF0AlAjk7Nqzqf3y333Ly+KxN3cKF2JqA3JT5ZheUGE= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/go/torrserver/log/log.go b/go/torrserver/log/log.go new file mode 100644 index 0000000000..64d796e406 --- /dev/null +++ b/go/torrserver/log/log.go @@ -0,0 +1,121 @@ +package log + +import ( + "bytes" + "fmt" + "io" + "log" + "os" + "strings" + + "github.com/gin-gonic/gin" +) + +var ( + logPath = "" + webLogPath = "" +) + +var webLog *log.Logger + +var ( + logFile *os.File + webLogFile *os.File +) + +func Init(path, webpath string) { + webLogPath = webpath + logPath = path + + if webpath != "" { + ff, err := os.OpenFile(webLogPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0o666) + if err != nil { + TLogln("Error create web log file:", err) + } else { + webLogFile = ff + webLog = log.New(ff, " ", log.LstdFlags) + } + } + + if path != "" { + if fi, err := os.Lstat(path); err == nil { + if fi.Size() >= 100*1024*1024 { // 100MB + os.Remove(path) + } + } + ff, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0o666) + if err != nil { + TLogln("Error create log file:", err) + return + } + logFile = ff + os.Stdout = ff + os.Stderr = ff + // var timeFmt string + // var ok bool + // timeFmt, ok = os.LookupEnv("GO_LOG_TIME_FMT") + // if !ok { + // timeFmt = "2006-01-02T15:04:05-0700" + // } + // log.SetFlags(log.Lmsgprefix) + // log.SetPrefix(time.Now().Format(timeFmt) + " TSM ") + log.SetFlags(log.LstdFlags | log.LUTC | log.Lmsgprefix) + log.SetPrefix("UTC0 ") + log.SetOutput(ff) + } +} + +func Close() { + if logFile != nil { + logFile.Close() + } + if webLogFile != nil { + webLogFile.Close() + } +} + +func TLogln(v ...interface{}) { + log.Println(v...) +} + +func WebLogln(v ...interface{}) { + if webLog != nil { + webLog.Println(v...) + } +} + +func WebLogger() gin.HandlerFunc { + return func(c *gin.Context) { + if webLog == nil { + c.Next() + return + } + body := "" + // save body if not form or file + if !strings.HasPrefix(c.Request.Header.Get("Content-Type"), "multipart/form-data") { + body, _ := io.ReadAll(c.Request.Body) + c.Request.Body = io.NopCloser(bytes.NewBuffer(body)) + } else { + body = "body hidden, too large" + } + c.Next() + + statusCode := c.Writer.Status() + clientIP := c.ClientIP() + method := c.Request.Method + path := c.Request.URL.Path + raw := c.Request.URL.RawQuery + if raw != "" { + path = path + "?" + raw + } + + logStr := fmt.Sprintf("%3d | %12s | %-7s %#v %v", + statusCode, + clientIP, + method, + path, + string(body), + ) + WebLogln(logStr) + } +} diff --git a/go/torrserver/mimetype/mimetype.go b/go/torrserver/mimetype/mimetype.go new file mode 100644 index 0000000000..94126a5b07 --- /dev/null +++ b/go/torrserver/mimetype/mimetype.go @@ -0,0 +1,144 @@ +package mimetype + +import ( + "log" + "mime" + "net/http" + "os" + "path" + "strings" +) + +func init() { + // Add a minimal number of mime types to augment go's built in types + // for environments which don't have access to a mime.types file (e.g. + // Termux on android) + for _, t := range []struct { + mimeType string + extensions string + }{ + {"image/bmp", ".bmp"}, + {"image/gif", ".gif"}, + {"image/jpeg", ".jpg,.jpeg"}, + {"image/png", ".png"}, + {"image/tiff", ".tiff,.tif"}, + {"audio/x-aac", ".aac"}, + {"audio/dsd", ".dsd,.dsf,.dff"}, + {"audio/flac", ".flac"}, + {"audio/mpeg", ".mpga,.mpega,.mp2,.mp3,.m4a"}, + {"audio/ogg", ".oga,.ogg,.opus,.spx"}, + {"audio/opus", ".opus"}, + {"audio/weba", ".weba"}, + {"audio/x-ape", ".ape"}, + // {"audio/x-dsd", ".dsd"}, + // {"audio/x-dff", ".dff"}, + // {"audio/x-dsf", ".dsf"}, + {"audio/x-wav", ".wav"}, + {"video/dv", ".dif,.dv"}, + {"video/fli", ".fli"}, + {"video/mp4", ".mp4"}, + {"video/mpeg", ".mpeg,.mpg,.mpe"}, + {"video/x-matroska", ".mpv,.mkv"}, + {"video/mp2t", ".ts,.m2ts,.mts"}, + {"video/ogg", ".ogv"}, + {"video/webm", ".webm"}, + {"video/x-ms-vob", ".vob"}, + {"video/x-msvideo", ".avi"}, + {"video/x-quicktime", ".qt,.mov"}, + {"text/srt", ".srt"}, + {"text/smi", ".smi"}, + {"text/ssa", ".ssa"}, + } { + for _, ext := range strings.Split(t.extensions, ",") { + err := mime.AddExtensionType(ext, t.mimeType) + if err != nil { + panic(err) + } + } + } + if err := mime.AddExtensionType(".rmvb", "application/vnd.rn-realmedia-vbr"); err != nil { + log.Printf("Could not register application/vnd.rn-realmedia-vbr MIME type: %s", err) + } +} + +// Example: "video/mpeg" +type mimeType string + +// IsMedia returns true for media MIME-types +func (mt mimeType) IsMedia() bool { + return mt.IsVideo() || mt.IsAudio() || mt.IsImage() +} + +// IsVideo returns true for video MIME-types +func (mt mimeType) IsVideo() bool { + return strings.HasPrefix(string(mt), "video/") || mt == "application/vnd.rn-realmedia-vbr" +} + +// IsAudio returns true for audio MIME-types +func (mt mimeType) IsAudio() bool { + return strings.HasPrefix(string(mt), "audio/") +} + +// IsImage returns true for image MIME-types +func (mt mimeType) IsImage() bool { + return strings.HasPrefix(string(mt), "image/") +} + +// IsSub returns true for subtitles MIME-types +func (mt mimeType) IsSub() bool { + return strings.HasPrefix(string(mt), "text/srt") || strings.HasPrefix(string(mt), "text/smi") || strings.HasPrefix(string(mt), "text/ssa") +} + +// Returns the group "type", the part before the '/'. +func (mt mimeType) Type() string { + return strings.SplitN(string(mt), "/", 2)[0] +} + +// Returns the string representation of this MIME-type +func (mt mimeType) String() string { + return string(mt) +} + +// MimeTypeByPath determines the MIME-type of file at the given path +func MimeTypeByPath(filePath string) (ret mimeType, err error) { + ret = mimeTypeByBaseName(path.Base(filePath)) + if ret == "" { + ret, err = mimeTypeByContent(filePath) + } + // Custom DLNA-compat mime mappings + // TODO: make this depend on client headers / profile map + if ret == "video/mp2t" { + ret = "video/mpeg" + // } else if ret == "video/x-matroska" { + // ret = "video/mpeg" + } else if ret == "video/x-msvideo" { + ret = "video/avi" + } else if ret == "" { + ret = "application/octet-stream" + } + return +} + +// Guess MIME-type from the extension, ignoring ".part". +func mimeTypeByBaseName(name string) mimeType { + name = strings.TrimSuffix(name, ".part") + ext := path.Ext(name) + if ext != "" { + return mimeType(mime.TypeByExtension(ext)) + } + return mimeType("") +} + +// Guess the MIME-type by analysing the first 512 bytes of the file. +func mimeTypeByContent(path string) (ret mimeType, err error) { + file, err := os.Open(path) + if err != nil { + return + } + defer file.Close() + var data [512]byte + if n, err := file.Read(data[:]); err == nil { + ret = mimeType(http.DetectContentType(data[:n])) + } + return +} diff --git a/go/torrserver/server.go b/go/torrserver/server.go new file mode 100644 index 0000000000..fc49b37f31 --- /dev/null +++ b/go/torrserver/server.go @@ -0,0 +1,106 @@ +package server + +import ( + "net" + "os" + "path/filepath" + "strings" + + "server/log" + "server/settings" + "server/torr/utils" + "server/web" +) + +func Start(pathdb, port string, roSets, searchWA bool) { + settings.Path = pathdb + settings.InitSets(roSets, searchWA) + if roSets { + log.TLogln("Enabled Read-only DB mode!") + } + + // http checks + if port == "" { + port = "8090" + } + log.TLogln("Check web port", port) + l, err := net.Listen("tcp", ":"+port) + if l != nil { + l.Close() + } + if err != nil { + log.TLogln("Port", port, "already in use! Please set different sslport for HTTP. Abort") + os.Exit(1) + } + // remove old disk caches + go cleanCache() + // set settings http Start web server. + settings.Port = port + web.Start() +} + +func cleanCache() { + if !settings.BTsets.UseDisk || settings.BTsets.TorrentsSavePath == "/" || settings.BTsets.TorrentsSavePath == "" { + return + } + + dirs, err := os.ReadDir(settings.BTsets.TorrentsSavePath) + if err != nil { + return + } + + torrs := settings.ListTorrent() + + log.TLogln("Remove unused cache in dir:", settings.BTsets.TorrentsSavePath) + for _, d := range dirs { + if len(d.Name()) != 40 { + // Not a hash + continue + } + + if !settings.BTsets.RemoveCacheOnDrop { + for _, t := range torrs { + if d.IsDir() && d.Name() != t.InfoHash.HexString() { + log.TLogln("Remove unused cache:", d.Name()) + removeAllFiles(filepath.Join(settings.BTsets.TorrentsSavePath, d.Name())) + break + } + } + } else { + if d.IsDir() { + log.TLogln("Remove unused cache:", d.Name()) + removeAllFiles(filepath.Join(settings.BTsets.TorrentsSavePath, d.Name())) + } + } + } +} + +func removeAllFiles(path string) { + files, err := os.ReadDir(path) + if err != nil { + return + } + for _, f := range files { + name := filepath.Join(path, f.Name()) + os.Remove(name) + } + os.Remove(path) +} + +func WaitServer() string { + err := web.Wait() + if err != nil { + return err.Error() + } + return "" +} + +func Stop() { + web.Stop() + settings.CloseDB() +} + +func AddTrackers(trackers string) { + tracks := strings.Split(trackers, ",\n") + utils.SetDefTrackers(tracks) +} diff --git a/go/torrserver/settings/btsets.go b/go/torrserver/settings/btsets.go new file mode 100644 index 0000000000..1480556dab --- /dev/null +++ b/go/torrserver/settings/btsets.go @@ -0,0 +1,147 @@ +package settings + +import ( + "encoding/json" + "io" + "io/fs" + "path/filepath" + "strings" + + "server/log" +) + +type BTSets struct { + // Cache + CacheSize int64 // in byte, def 64 MB + ReaderReadAHead int // in percent, 5%-100%, [...S__X__E...] [S-E] not clean + PreloadCache int // in percent + + // Disk + UseDisk bool + TorrentsSavePath string + RemoveCacheOnDrop bool + + // Torrent + ForceEncrypt bool + RetrackersMode int // 0 - don`t add, 1 - add retrackers (def), 2 - remove retrackers 3 - replace retrackers + TorrentDisconnectTimeout int // in seconds + EnableDebug bool // debug logs + + // DLNA + EnableDLNA bool + FriendlyName string + + // BT Config + EnableIPv6 bool + DisableTCP bool + DisableUTP bool + DisableUPNP bool + DisableDHT bool + DisablePEX bool + DisableUpload bool + DownloadRateLimit int // in kb, 0 - inf + UploadRateLimit int // in kb, 0 - inf + ConnectionsLimit int + PeersListenPort int +} + +func (v *BTSets) String() string { + buf, _ := json.Marshal(v) + return string(buf) +} + +var BTsets *BTSets + +func SetBTSets(sets *BTSets) { + if ReadOnly { + return + } + // failsafe checks (use defaults) + if sets.CacheSize == 0 { + sets.CacheSize = 64 * 1024 * 1024 + } + if sets.ConnectionsLimit == 0 { + sets.ConnectionsLimit = 25 + } + if sets.TorrentDisconnectTimeout == 0 { + sets.TorrentDisconnectTimeout = 30 + } + + if sets.ReaderReadAHead < 5 { + sets.ReaderReadAHead = 5 + } + if sets.ReaderReadAHead > 100 { + sets.ReaderReadAHead = 100 + } + + if sets.PreloadCache < 0 { + sets.PreloadCache = 0 + } + if sets.PreloadCache > 100 { + sets.PreloadCache = 100 + } + + if sets.TorrentsSavePath == "" { + sets.UseDisk = false + } else if sets.UseDisk { + BTsets = sets + + go filepath.WalkDir(sets.TorrentsSavePath, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() && strings.ToLower(d.Name()) == ".tsc" { + BTsets.TorrentsSavePath = path + log.TLogln("Find directory \"" + BTsets.TorrentsSavePath + "\", use as cache dir") + return io.EOF + } + if d.IsDir() && strings.HasPrefix(d.Name(), ".") { + return filepath.SkipDir + } + return nil + }) + } + + BTsets = sets + buf, err := json.Marshal(BTsets) + if err != nil { + log.TLogln("Error marshal btsets", err) + return + } + tdb.Set("Settings", "BitTorr", buf) +} + +func SetDefaultConfig() { + sets := new(BTSets) + sets.CacheSize = 64 * 1024 * 1024 // 64 MB + sets.PreloadCache = 50 + sets.ConnectionsLimit = 25 + sets.RetrackersMode = 1 + sets.TorrentDisconnectTimeout = 30 + sets.ReaderReadAHead = 95 // 95% + BTsets = sets + if !ReadOnly { + buf, err := json.Marshal(BTsets) + if err != nil { + log.TLogln("Error marshal btsets", err) + return + } + tdb.Set("Settings", "BitTorr", buf) + } +} + +func loadBTSets() { + buf := tdb.Get("Settings", "BitTorr") + if len(buf) > 0 { + err := json.Unmarshal(buf, &BTsets) + if err == nil { + if BTsets.ReaderReadAHead < 5 { + BTsets.ReaderReadAHead = 5 + } + return + } + log.TLogln("Error unmarshal btsets", err) + } + + SetDefaultConfig() +} diff --git a/go/torrserver/settings/db.go b/go/torrserver/settings/db.go new file mode 100644 index 0000000000..03ec985082 --- /dev/null +++ b/go/torrserver/settings/db.go @@ -0,0 +1,171 @@ +package settings + +import ( + "path/filepath" + "strings" + "time" + + "server/log" + + bolt "go.etcd.io/bbolt" +) + +type TDB struct { + Path string + db *bolt.DB +} + +func NewTDB() *TDB { + db, err := bolt.Open(filepath.Join(Path, "config.db"), 0o666, &bolt.Options{Timeout: 5 * time.Second}) + if err != nil { + log.TLogln(err) + return nil + } + + tdb := new(TDB) + tdb.db = db + tdb.Path = Path + return tdb +} + +func (v *TDB) CloseDB() { + if v.db != nil { + v.db.Close() + v.db = nil + } +} + +func (v *TDB) Get(xpath, name string) []byte { + spath := strings.Split(xpath, "/") + if len(spath) == 0 { + return nil + } + var ret []byte + err := v.db.View(func(tx *bolt.Tx) error { + buckt := tx.Bucket([]byte(spath[0])) + if buckt == nil { + return nil + } + + for i, p := range spath { + if i == 0 { + continue + } + buckt = buckt.Bucket([]byte(p)) + if buckt == nil { + return nil + } + } + + ret = buckt.Get([]byte(name)) + return nil + }) + if err != nil { + log.TLogln("Error get sets", xpath+"/"+name, ", error:", err) + } + + return ret +} + +func (v *TDB) Set(xpath, name string, value []byte) { + if ReadOnly { + return + } + + spath := strings.Split(xpath, "/") + if len(spath) == 0 { + return + } + err := v.db.Update(func(tx *bolt.Tx) error { + buckt, err := tx.CreateBucketIfNotExists([]byte(spath[0])) + if err != nil { + return err + } + + for i, p := range spath { + if i == 0 { + continue + } + buckt, err = buckt.CreateBucketIfNotExists([]byte(p)) + if err != nil { + return err + } + } + + return buckt.Put([]byte(name), value) + }) + if err != nil { + log.TLogln("Error put sets", xpath+"/"+name, ", error:", err) + log.TLogln("value:", value) + } +} + +func (v *TDB) List(xpath string) []string { + spath := strings.Split(xpath, "/") + if len(spath) == 0 { + return nil + } + var ret []string + err := v.db.View(func(tx *bolt.Tx) error { + buckt := tx.Bucket([]byte(spath[0])) + if buckt == nil { + return nil + } + + for i, p := range spath { + if i == 0 { + continue + } + buckt = buckt.Bucket([]byte(p)) + if buckt == nil { + return nil + } + } + + buckt.ForEach(func(k, _ []byte) error { + if len(k) > 0 { + ret = append(ret, string(k)) + } + return nil + }) + + return nil + }) + if err != nil { + log.TLogln("Error list sets", xpath, ", error:", err) + } + + return ret +} + +func (v *TDB) Rem(xpath, name string) { + if ReadOnly { + return + } + + spath := strings.Split(xpath, "/") + if len(spath) == 0 { + return + } + err := v.db.Update(func(tx *bolt.Tx) error { + buckt := tx.Bucket([]byte(spath[0])) + if buckt == nil { + return nil + } + + for i, p := range spath { + if i == 0 { + continue + } + buckt = buckt.Bucket([]byte(p)) + if buckt == nil { + return nil + } + } + + return buckt.Delete([]byte(name)) + }) + if err != nil { + log.TLogln("Error rem sets", xpath+"/"+name, ", error:", err) + } +} diff --git a/go/torrserver/settings/migrate.go b/go/torrserver/settings/migrate.go new file mode 100644 index 0000000000..e0c6ce1436 --- /dev/null +++ b/go/torrserver/settings/migrate.go @@ -0,0 +1,102 @@ +package settings + +import ( + "encoding/binary" + "fmt" + "os" + "path/filepath" + + bolt "go.etcd.io/bbolt" + "server/log" + "server/web/api/utils" +) + +var dbTorrentsName = []byte("Torrents") + +type torrentOldDB struct { + Name string + Magnet string + InfoBytes []byte + Hash string + Size int64 + Timestamp int64 +} + +func Migrate() { + if _, err := os.Lstat(filepath.Join(Path, "torrserver.db")); os.IsNotExist(err) { + return + } + + db, err := bolt.Open(filepath.Join(Path, "torrserver.db"), 0o666, nil) + if err != nil { + return + } + + torrs := make([]*torrentOldDB, 0) + err = db.View(func(tx *bolt.Tx) error { + tdb := tx.Bucket(dbTorrentsName) + if tdb == nil { + return nil + } + c := tdb.Cursor() + for h, _ := c.First(); h != nil; h, _ = c.Next() { + hdb := tdb.Bucket(h) + if hdb != nil { + torr := new(torrentOldDB) + torr.Hash = string(h) + tmp := hdb.Get([]byte("Name")) + if tmp == nil { + return fmt.Errorf("error load torrent") + } + torr.Name = string(tmp) + + tmp = hdb.Get([]byte("Link")) + if tmp == nil { + return fmt.Errorf("error load torrent") + } + torr.Magnet = string(tmp) + + tmp = hdb.Get([]byte("Size")) + if tmp == nil { + return fmt.Errorf("error load torrent") + } + torr.Size = b2i(tmp) + + tmp = hdb.Get([]byte("Timestamp")) + if tmp == nil { + return fmt.Errorf("error load torrent") + } + torr.Timestamp = b2i(tmp) + + torrs = append(torrs, torr) + } + } + return nil + }) + db.Close() + if err == nil && len(torrs) > 0 { + for _, torr := range torrs { + spec, err := utils.ParseLink(torr.Magnet) + if err != nil { + continue + } + + title := torr.Name + if len(spec.DisplayName) > len(title) { + title = spec.DisplayName + } + log.TLogln("Migrate torrent", torr.Name, torr.Hash, torr.Size) + AddTorrent(&TorrentDB{ + TorrentSpec: spec, + Title: title, + Timestamp: torr.Timestamp, + Size: torr.Size, + }) + } + } + os.Remove(filepath.Join(Path, "torrserver.db")) +} + +func b2i(v []byte) int64 { + return int64(binary.BigEndian.Uint64(v)) +} diff --git a/go/torrserver/settings/settings.go b/go/torrserver/settings/settings.go new file mode 100644 index 0000000000..09f129e824 --- /dev/null +++ b/go/torrserver/settings/settings.go @@ -0,0 +1,36 @@ +package settings + +import ( + "os" + "path/filepath" + + "server/log" +) + +var ( + tdb *TDB + Path string + Port string + ReadOnly bool + HttpAuth bool + SearchWA bool + PubIPv4 string + PubIPv6 string + TorAddr string +) + +func InitSets(readOnly, searchWA bool) { + ReadOnly = readOnly + SearchWA = searchWA + tdb = NewTDB() + if tdb == nil { + log.TLogln("Error open db:", filepath.Join(Path, "config.db")) + os.Exit(1) + } + loadBTSets() + Migrate() +} + +func CloseDB() { + tdb.CloseDB() +} diff --git a/go/torrserver/settings/torrent.go b/go/torrserver/settings/torrent.go new file mode 100644 index 0000000000..40361cbbd7 --- /dev/null +++ b/go/torrserver/settings/torrent.go @@ -0,0 +1,81 @@ +package settings + +import ( + "encoding/json" + "sort" + "sync" + + "github.com/anacrolix/torrent" + "github.com/anacrolix/torrent/metainfo" +) + +type TorrentDB struct { + *torrent.TorrentSpec + + Title string `json:"title,omitempty"` + Poster string `json:"poster,omitempty"` + Data string `json:"data,omitempty"` + + Timestamp int64 `json:"timestamp,omitempty"` + Size int64 `json:"size,omitempty"` +} + +type File struct { + Name string `json:"name,omitempty"` + Id int `json:"id,omitempty"` + Size int64 `json:"size,omitempty"` +} + +var mu sync.Mutex + +func AddTorrent(torr *TorrentDB) { + list := ListTorrent() + mu.Lock() + find := -1 + for i, db := range list { + if db.InfoHash.HexString() == torr.InfoHash.HexString() { + find = i + break + } + } + if find != -1 { + list[find] = torr + } else { + list = append(list, torr) + } + for _, db := range list { + buf, err := json.Marshal(db) + if err == nil { + tdb.Set("Torrents", db.InfoHash.HexString(), buf) + } + } + mu.Unlock() +} + +func ListTorrent() []*TorrentDB { + mu.Lock() + defer mu.Unlock() + + var list []*TorrentDB + keys := tdb.List("Torrents") + for _, key := range keys { + buf := tdb.Get("Torrents", key) + if len(buf) > 0 { + var torr *TorrentDB + err := json.Unmarshal(buf, &torr) + if err == nil { + list = append(list, torr) + } + } + } + sort.Slice(list, func(i, j int) bool { + return list[i].Timestamp > list[j].Timestamp + }) + return list +} + +func RemTorrent(hash metainfo.Hash) { + mu.Lock() + tdb.Rem("Torrents", hash.HexString()) + mu.Unlock() +} diff --git a/go/torrserver/settings/viewed.go b/go/torrserver/settings/viewed.go new file mode 100644 index 0000000000..bf02562586 --- /dev/null +++ b/go/torrserver/settings/viewed.go @@ -0,0 +1,100 @@ +package settings + +import ( + "encoding/json" + + "server/log" +) + +type Viewed struct { + Hash string `json:"hash"` + FileIndex int `json:"file_index"` +} + +func SetViewed(vv *Viewed) { + var indexes map[int]struct{} + var err error + + buf := tdb.Get("Viewed", vv.Hash) + if len(buf) == 0 { + indexes = make(map[int]struct{}) + indexes[vv.FileIndex] = struct{}{} + buf, err = json.Marshal(indexes) + if err == nil { + tdb.Set("Viewed", vv.Hash, buf) + } + } else { + err = json.Unmarshal(buf, &indexes) + if err == nil { + indexes[vv.FileIndex] = struct{}{} + buf, err = json.Marshal(indexes) + if err == nil { + tdb.Set("Viewed", vv.Hash, buf) + } + } + } + if err != nil { + log.TLogln("Error set viewed:", err) + } +} + +func RemViewed(vv *Viewed) { + buf := tdb.Get("Viewed", vv.Hash) + var indeces map[int]struct{} + err := json.Unmarshal(buf, &indeces) + if err == nil { + if vv.FileIndex != -1 { + delete(indeces, vv.FileIndex) + buf, err = json.Marshal(indeces) + if err == nil { + tdb.Set("Viewed", vv.Hash, buf) + } + } else { + tdb.Rem("Viewed", vv.Hash) + } + } + if err != nil { + log.TLogln("Error rem viewed:", err) + } +} + +func ListViewed(hash string) []*Viewed { + var err error + if hash != "" { + buf := tdb.Get("Viewed", hash) + if len(buf) == 0 { + return []*Viewed{} + } + var indeces map[int]struct{} + err = json.Unmarshal(buf, &indeces) + if err == nil { + var ret []*Viewed + for i := range indeces { + ret = append(ret, &Viewed{hash, i}) + } + return ret + } + } else { + var ret []*Viewed + keys := tdb.List("Viewed") + for _, key := range keys { + buf := tdb.Get("Viewed", key) + if len(buf) == 0 { + return []*Viewed{} + } + var indeces map[int]struct{} + err = json.Unmarshal(buf, &indeces) + if err == nil { + for i := range indeces { + ret = append(ret, &Viewed{key, i}) + } + } + } + return ret + } + + if err != nil { + log.TLogln("Error list viewed:", err) + } + return []*Viewed{} +} diff --git a/go/torrserver/torr/apihelper.go b/go/torrserver/torr/apihelper.go new file mode 100644 index 0000000000..2043ce16e7 --- /dev/null +++ b/go/torrserver/torr/apihelper.go @@ -0,0 +1,253 @@ +package torr + +import ( + "io" + "os" + "path/filepath" + "sort" + "time" + + "github.com/anacrolix/torrent" + "github.com/anacrolix/torrent/metainfo" + + "server/log" + sets "server/settings" +) + +var bts *BTServer + +func InitApiHelper(bt *BTServer) { + bts = bt +} + +func LoadTorrent(tor *Torrent) *Torrent { + if tor.TorrentSpec == nil { + return nil + } + tr, err := NewTorrent(tor.TorrentSpec, bts) + if err != nil { + return nil + } + if !tr.WaitInfo() { + return nil + } + tr.Title = tor.Title + tr.Poster = tor.Poster + tr.Data = tor.Data + return tr +} + +func AddTorrent(spec *torrent.TorrentSpec, title, poster string, data string) (*Torrent, error) { + torr, err := NewTorrent(spec, bts) + if err != nil { + log.TLogln("error add torrent:", err) + return nil, err + } + + torDB := GetTorrentDB(spec.InfoHash) + + if torr.Title == "" { + torr.Title = title + if title == "" && torDB != nil { + torr.Title = torDB.Title + } + if torr.Title == "" && torr.Torrent != nil && torr.Torrent.Info() != nil { + torr.Title = torr.Info().Name + } + } + if torr.Poster == "" { + torr.Poster = poster + if torr.Poster == "" && torDB != nil { + torr.Poster = torDB.Poster + } + } + if torr.Data == "" { + torr.Data = data + if torr.Data == "" && torDB != nil { + torr.Data = torDB.Data + } + } + + return torr, nil +} + +func SaveTorrentToDB(torr *Torrent) { + log.TLogln("save to db:", torr.Hash()) + AddTorrentDB(torr) +} + +func GetTorrent(hashHex string) *Torrent { + hash := metainfo.NewHashFromHex(hashHex) + tor := bts.GetTorrent(hash) + if tor != nil { + tor.AddExpiredTime(time.Minute) + return tor + } + + tr := GetTorrentDB(hash) + if tr != nil { + tor = tr + go func() { + log.TLogln("New torrent", tor.Hash()) + tr, _ := NewTorrent(tor.TorrentSpec, bts) + if tr != nil { + tr.Title = tor.Title + tr.Poster = tor.Poster + tr.Data = tor.Data + tr.Size = tor.Size + tr.Timestamp = tor.Timestamp + tr.GotInfo() + } + }() + } + return tor +} + +func SetTorrent(hashHex, title, poster, data string) *Torrent { + hash := metainfo.NewHashFromHex(hashHex) + torr := bts.GetTorrent(hash) + torrDb := GetTorrentDB(hash) + + if title == "" && torr == nil && torrDb != nil { + torr = GetTorrent(hashHex) + torr.GotInfo() + if torr.Torrent != nil && torr.Torrent.Info() != nil { + title = torr.Info().Name + } + } + + if torr != nil { + if title == "" && torr.Torrent != nil && torr.Torrent.Info() != nil { + title = torr.Info().Name + } + torr.Title = title + torr.Poster = poster + torr.Data = data + } + + if torrDb != nil { + torrDb.Title = title + torrDb.Poster = poster + torrDb.Data = data + AddTorrentDB(torrDb) + } + if torr != nil { + return torr + } else { + return torrDb + } +} + +func RemTorrent(hashHex string) { + if sets.ReadOnly { + return + } + hash := metainfo.NewHashFromHex(hashHex) + if bts.RemoveTorrent(hash) { + if sets.BTsets.UseDisk && hashHex != "" && hashHex != "/" { + name := filepath.Join(sets.BTsets.TorrentsSavePath, hashHex) + ff, _ := os.ReadDir(name) + for _, f := range ff { + os.Remove(filepath.Join(name, f.Name())) + } + err := os.Remove(name) + if err != nil { + log.TLogln("Error remove cache:", err) + } + } + } + RemTorrentDB(hash) +} + +func ListTorrent() []*Torrent { + btlist := bts.ListTorrents() + dblist := ListTorrentsDB() + + for hash, t := range dblist { + if _, ok := btlist[hash]; !ok { + btlist[hash] = t + } + } + var ret []*Torrent + + for _, t := range btlist { + ret = append(ret, t) + } + + sort.Slice(ret, func(i, j int) bool { + if ret[i].Timestamp != ret[j].Timestamp { + return ret[i].Timestamp > ret[j].Timestamp + } else { + return ret[i].Title > ret[j].Title + } + }) + + return ret +} + +func DropTorrent(hashHex string) { + hash := metainfo.NewHashFromHex(hashHex) + bts.RemoveTorrent(hash) +} + +func SetSettings(set *sets.BTSets) { + if sets.ReadOnly { + return + } + sets.SetBTSets(set) + log.TLogln("drop all torrents") + dropAllTorrent() + time.Sleep(time.Second * 1) + log.TLogln("disconect") + bts.Disconnect() + log.TLogln("connect") + bts.Connect() + time.Sleep(time.Second * 1) + log.TLogln("end set settings") +} + +func SetDefSettings() { + if sets.ReadOnly { + return + } + sets.SetDefaultConfig() + log.TLogln("drop all torrents") + dropAllTorrent() + time.Sleep(time.Second * 1) + log.TLogln("disconect") + bts.Disconnect() + log.TLogln("connect") + bts.Connect() + time.Sleep(time.Second * 1) + log.TLogln("end set default settings") +} + +func dropAllTorrent() { + for _, torr := range bts.torrents { + torr.drop() + <-torr.closed + } +} + +func Shutdown() { + bts.Disconnect() + sets.CloseDB() + log.TLogln("Received shutdown. Quit") +} + +func WriteStatus(w io.Writer) { + bts.client.WriteStatus(w) +} + +func Preload(torr *Torrent, index int) { + cache := float32(sets.BTsets.CacheSize) + preload := float32(sets.BTsets.PreloadCache) + size := int64((cache / 100.0) * preload) + if size <= 0 { + return + } + if size > sets.BTsets.CacheSize { + size = sets.BTsets.CacheSize + } + torr.Preload(index, size) +} diff --git a/go/torrserver/torr/btserver.go b/go/torrserver/torr/btserver.go new file mode 100644 index 0000000000..1c73417c1c --- /dev/null +++ b/go/torrserver/torr/btserver.go @@ -0,0 +1,273 @@ +package torr + +import ( + "context" + "fmt" + "log" + "net" + "sync" + + "github.com/anacrolix/publicip" + "github.com/anacrolix/torrent" + "github.com/anacrolix/torrent/metainfo" + + "server/settings" + "server/torr/storage/torrstor" + "server/torr/utils" + "server/version" +) + +type BTServer struct { + config *torrent.ClientConfig + client *torrent.Client + + storage *torrstor.Storage + + torrents map[metainfo.Hash]*Torrent + + mu sync.Mutex +} + +var privateIPBlocks []*net.IPNet + +func init() { + for _, cidr := range []string{ + "127.0.0.0/8", // IPv4 loopback + "10.0.0.0/8", // RFC1918 + "172.16.0.0/12", // RFC1918 + "192.168.0.0/16", // RFC1918 + "169.254.0.0/16", // RFC3927 link-local + "::1/128", // IPv6 loopback + "fe80::/10", // IPv6 link-local + "fc00::/7", // IPv6 unique local addr + } { + _, block, err := net.ParseCIDR(cidr) + if err != nil { + panic(fmt.Errorf("parse error on %q: %v", cidr, err)) + } + privateIPBlocks = append(privateIPBlocks, block) + } +} + +func NewBTS() *BTServer { + bts := new(BTServer) + bts.torrents = make(map[metainfo.Hash]*Torrent) + return bts +} + +func (bt *BTServer) Connect() error { + bt.mu.Lock() + defer bt.mu.Unlock() + var err error + bt.configure(context.TODO()) + bt.client, err = torrent.NewClient(bt.config) + bt.torrents = make(map[metainfo.Hash]*Torrent) + InitApiHelper(bt) + return err +} + +func (bt *BTServer) Disconnect() { + bt.mu.Lock() + defer bt.mu.Unlock() + if bt.client != nil { + bt.client.Close() + bt.client = nil + utils.FreeOSMemGC() + } +} + +func (bt *BTServer) configure(ctx context.Context) { + blocklist, _ := utils.ReadBlockedIP() + bt.config = torrent.NewDefaultClientConfig() + + bt.storage = torrstor.NewStorage(settings.BTsets.CacheSize) + bt.config.DefaultStorage = bt.storage + + userAgent := "qBittorrent/4.3.9" + peerID := "-qB4390-" + upnpID := "TorrServer/" + version.Version + cliVers := userAgent + + // bt.config.AlwaysWantConns = true + bt.config.Debug = settings.BTsets.EnableDebug + bt.config.DisableIPv6 = !settings.BTsets.EnableIPv6 + bt.config.DisableTCP = settings.BTsets.DisableTCP + bt.config.DisableUTP = settings.BTsets.DisableUTP + // https://github.com/anacrolix/torrent/issues/703 + // bt.config.DisableWebtorrent = true // TODO: check memory usage + // bt.config.DisableWebseeds = false + bt.config.NoDefaultPortForwarding = settings.BTsets.DisableUPNP + bt.config.NoDHT = settings.BTsets.DisableDHT + bt.config.DisablePEX = settings.BTsets.DisablePEX + bt.config.NoUpload = settings.BTsets.DisableUpload + bt.config.IPBlocklist = blocklist + bt.config.Bep20 = peerID + bt.config.PeerID = utils.PeerIDRandom(peerID) + bt.config.UpnpID = upnpID + bt.config.HTTPUserAgent = userAgent + bt.config.ExtendedHandshakeClientVersion = cliVers + bt.config.EstablishedConnsPerTorrent = settings.BTsets.ConnectionsLimit + bt.config.TotalHalfOpenConns = 500 + // Encryption/Obfuscation + bt.config.EncryptionPolicy = torrent.EncryptionPolicy{ + ForceEncryption: settings.BTsets.ForceEncrypt, + } + // bt.config.HeaderObfuscationPolicy = torrent.HeaderObfuscationPolicy{ + // RequirePreferred: settings.BTsets.ForceEncrypt, + // Preferred: true, + // } + if settings.BTsets.DownloadRateLimit > 0 { + bt.config.DownloadRateLimiter = utils.Limit(settings.BTsets.DownloadRateLimit * 1024) + } + if settings.BTsets.UploadRateLimit > 0 { + bt.config.UploadRateLimiter = utils.Limit(settings.BTsets.UploadRateLimit * 1024) + } + if settings.TorAddr != "" { + log.Println("Set listen addr", settings.TorAddr) + bt.config.SetListenAddr(settings.TorAddr) + } else { + if settings.BTsets.PeersListenPort > 0 { + log.Println("Set listen port", settings.BTsets.PeersListenPort) + bt.config.ListenPort = settings.BTsets.PeersListenPort + } else { + // lport := 32000 + // for { + // log.Println("Check listen port", lport) + // l, err := net.Listen("tcp", ":"+strconv.Itoa(lport)) + // if l != nil { + // l.Close() + // } + // if err == nil { + // break + // } + // lport++ + // } + // log.Println("Set listen port", lport) + log.Println("Set listen port to random autoselect (0)") + bt.config.ListenPort = 0 // lport + } + } + + log.Println("Client config:", settings.BTsets) + + var err error + + // set public IPv4 + if settings.PubIPv4 != "" { + if ip4 := net.ParseIP(settings.PubIPv4); ip4.To4() != nil && !isPrivateIP(ip4) { + bt.config.PublicIp4 = ip4 + } + } + if bt.config.PublicIp4 == nil { + bt.config.PublicIp4, err = publicip.Get4(ctx) + if err != nil { + log.Printf("error getting public ipv4 address: %v", err) + } + } + if bt.config.PublicIp4 != nil { + log.Println("PublicIp4:", bt.config.PublicIp4) + } + + // set public IPv6 + if settings.PubIPv6 != "" { + if ip6 := net.ParseIP(settings.PubIPv6); ip6.To16() != nil && ip6.To4() == nil && !isPrivateIP(ip6) { + bt.config.PublicIp6 = ip6 + } + } + if bt.config.PublicIp6 == nil && settings.BTsets.EnableIPv6 { + bt.config.PublicIp6, err = publicip.Get6(ctx) + if err != nil { + log.Printf("error getting public ipv6 address: %v", err) + } + } + if bt.config.PublicIp6 != nil { + log.Println("PublicIp6:", bt.config.PublicIp6) + } +} + +func (bt *BTServer) GetTorrent(hash torrent.InfoHash) *Torrent { + if torr, ok := bt.torrents[hash]; ok { + return torr + } + return nil +} + +func (bt *BTServer) ListTorrents() map[metainfo.Hash]*Torrent { + list := make(map[metainfo.Hash]*Torrent) + for k, v := range bt.torrents { + list[k] = v + } + return list +} + +func (bt *BTServer) RemoveTorrent(hash torrent.InfoHash) bool { + if torr, ok := bt.torrents[hash]; ok { + return torr.Close() + } + return false +} + +func isPrivateIP(ip net.IP) bool { + if ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() { + return true + } + + for _, block := range privateIPBlocks { + if block.Contains(ip) { + return true + } + } + return false +} + +func getPublicIp4() net.IP { + ifaces, err := net.Interfaces() + if err != nil { + log.Println("Error get public IPv4") + return nil + } + for _, i := range ifaces { + addrs, _ := i.Addrs() + if i.Flags&net.FlagUp == net.FlagUp { + for _, addr := range addrs { + var ip net.IP + switch v := addr.(type) { + case *net.IPNet: + ip = v.IP + case *net.IPAddr: + ip = v.IP + } + if !isPrivateIP(ip) && ip.To4() != nil { + return ip + } + } + } + } + return nil +} + +func getPublicIp6() net.IP { + ifaces, err := net.Interfaces() + if err != nil { + log.Println("Error get public IPv6") + return nil + } + for _, i := range ifaces { + addrs, _ := i.Addrs() + if i.Flags&net.FlagUp == net.FlagUp { + for _, addr := range addrs { + var ip net.IP + switch v := addr.(type) { + case *net.IPNet: + ip = v.IP + case *net.IPAddr: + ip = v.IP + } + if !isPrivateIP(ip) && ip.To16() != nil && ip.To4() == nil { + return ip + } + } + } + } + return nil +} diff --git a/go/torrserver/torr/dbwrapper.go b/go/torrserver/torr/dbwrapper.go new file mode 100644 index 0000000000..4aca82f0a4 --- /dev/null +++ b/go/torrserver/torr/dbwrapper.go @@ -0,0 +1,82 @@ +package torr + +import ( + "encoding/json" + "time" + + "server/torr/utils" + + "server/settings" + "server/torr/state" + + "github.com/anacrolix/torrent/metainfo" +) + +type tsFiles struct { + TorrServer struct { + Files []*state.TorrentFileStat `json:"Files"` + } `json:"TorrServer"` +} + +func AddTorrentDB(torr *Torrent) { + t := new(settings.TorrentDB) + t.TorrentSpec = torr.TorrentSpec + t.Title = torr.Title + if torr.Data == "" { + files := new(tsFiles) + files.TorrServer.Files = torr.Status().FileStats + buf, _ := json.Marshal(files) + t.Data = string(buf) + torr.Data = t.Data + } else { + t.Data = torr.Data + } + if utils.CheckImgUrl(torr.Poster) { + t.Poster = torr.Poster + } + t.Size = torr.Size + if t.Size == 0 && torr.Torrent != nil { + t.Size = torr.Torrent.Length() + } + t.Timestamp = time.Now().Unix() + settings.AddTorrent(t) +} + +func GetTorrentDB(hash metainfo.Hash) *Torrent { + list := settings.ListTorrent() + for _, db := range list { + if hash == db.InfoHash { + torr := new(Torrent) + torr.TorrentSpec = db.TorrentSpec + torr.Title = db.Title + torr.Poster = db.Poster + torr.Timestamp = db.Timestamp + torr.Size = db.Size + torr.Data = db.Data + torr.Stat = state.TorrentInDB + return torr + } + } + return nil +} + +func RemTorrentDB(hash metainfo.Hash) { + settings.RemTorrent(hash) +} + +func ListTorrentsDB() map[metainfo.Hash]*Torrent { + ret := make(map[metainfo.Hash]*Torrent) + list := settings.ListTorrent() + for _, db := range list { + torr := new(Torrent) + torr.TorrentSpec = db.TorrentSpec + torr.Title = db.Title + torr.Poster = db.Poster + torr.Timestamp = db.Timestamp + torr.Size = db.Size + torr.Data = db.Data + torr.Stat = state.TorrentInDB + ret[torr.TorrentSpec.InfoHash] = torr + } + return ret +} diff --git a/go/torrserver/torr/preload.go b/go/torrserver/torr/preload.go new file mode 100644 index 0000000000..091151c421 --- /dev/null +++ b/go/torrserver/torr/preload.go @@ -0,0 +1,168 @@ +package torr + +import ( + "fmt" + "io" + "sync" + "time" + + "github.com/anacrolix/torrent" + + "server/log" + "server/settings" + "server/torr/state" + utils2 "server/utils" +) + +func (t *Torrent) Preload(index int, size int64) { + if size <= 0 { + return + } + t.PreloadSize = size + + if t.Stat == state.TorrentGettingInfo { + if !t.WaitInfo() { + return + } + // wait change status + time.Sleep(100 * time.Millisecond) + } + + t.muTorrent.Lock() + if t.Stat != state.TorrentWorking { + t.muTorrent.Unlock() + return + } + + t.Stat = state.TorrentPreload + t.muTorrent.Unlock() + + defer func() { + if t.Stat == state.TorrentPreload { + t.Stat = state.TorrentWorking + // Очистка по окончании прелоада + t.BitRate = "" + t.DurationSeconds = 0 + } + }() + + file := t.findFileIndex(index) + if file == nil { + file = t.Files()[0] + } + + if size > file.Length() { + size = file.Length() + } + + if t.Info() != nil { + // Запуск лога в отдельном потоке + go func() { + for t.Stat == state.TorrentPreload { + stat := fmt.Sprint(file.Torrent().InfoHash().HexString(), " ", utils2.Format(float64(t.PreloadedBytes)), "/", utils2.Format(float64(t.PreloadSize)), " Speed:", utils2.Format(t.DownloadSpeed), " Peers:[", t.Torrent.Stats().ConnectedSeeders, "]", t.Torrent.Stats().ActivePeers, "/", t.Torrent.Stats().TotalPeers) + log.TLogln("Preload:", stat) + t.AddExpiredTime(time.Second * time.Duration(settings.BTsets.TorrentDisconnectTimeout)) + time.Sleep(time.Second) + } + }() + + if t.Stat == state.TorrentClosed { + log.TLogln("End preload: torrent closed") + return + } + + // startend -> 8/16 MB + startend := t.Info().PieceLength + if startend < 8<<20 { + startend = 8 << 20 + } + + readerStart := file.NewReader() + defer readerStart.Close() + readerStart.SetResponsive() + readerStart.SetReadahead(0) + readerStartEnd := size - startend + + if readerStartEnd < 0 { + // Если конец начального ридера оказался за началом + readerStartEnd = size + } + if readerStartEnd > file.Length() { + // Если конец начального ридера оказался после конца файла + readerStartEnd = file.Length() + } + + readerEndStart := file.Length() - startend + readerEndEnd := file.Length() + + var wg sync.WaitGroup + go func() { + offset := int64(0) + if readerEndStart > readerStartEnd { + // Если конечный ридер не входит в диапозон начального + wg.Add(1) + defer wg.Done() + if t.Stat == state.TorrentPreload { + readerEnd := file.NewReader() + readerEnd.SetResponsive() + readerEnd.SetReadahead(0) + readerEnd.Seek(readerEndStart, io.SeekStart) + offset = readerEndStart + tmp := make([]byte, 32768) + for offset+int64(len(tmp)) < readerEndEnd { + n, err := readerEnd.Read(tmp) + if err != nil { + break + } + offset += int64(n) + } + readerEnd.Close() + } + } + }() + + pieceLength := t.Info().PieceLength + readahead := pieceLength * 4 + if readerStartEnd < readahead { + readahead = 0 + } + readerStart.SetReadahead(readahead) + offset := int64(0) + tmp := make([]byte, 32768) + for offset+int64(len(tmp)) < readerStartEnd { + n, err := readerStart.Read(tmp) + if err != nil { + log.TLogln("Error preload:", err) + return + } + offset += int64(n) + if readahead > 0 && readerStartEnd-(offset+int64(len(tmp))) < readahead { + readahead = 0 + readerStart.SetReadahead(0) + } + } + + wg.Wait() + } + log.TLogln("End preload:", file.Torrent().InfoHash().HexString(), "Peers:[", t.Torrent.Stats().ConnectedSeeders, "]", t.Torrent.Stats().ActivePeers, "/", t.Torrent.Stats().TotalPeers) +} + +func (t *Torrent) findFileIndex(index int) *torrent.File { + st := t.Status() + var stFile *state.TorrentFileStat + for _, f := range st.FileStats { + if index == f.Id { + stFile = f + break + } + } + if stFile == nil { + return nil + } + for _, file := range t.Files() { + if file.Path() == stFile.Path { + return file + } + } + return nil +} diff --git a/go/torrserver/torr/state/state.go b/go/torrserver/torr/state/state.go new file mode 100644 index 0000000000..b28b34e280 --- /dev/null +++ b/go/torrserver/torr/state/state.go @@ -0,0 +1,75 @@ +package state + +type TorrentStat int + +func (t TorrentStat) String() string { + switch t { + case TorrentAdded: + return "Torrent added" + case TorrentGettingInfo: + return "Torrent getting info" + case TorrentPreload: + return "Torrent preload" + case TorrentWorking: + return "Torrent working" + case TorrentClosed: + return "Torrent closed" + case TorrentInDB: + return "Torrent in db" + default: + return "Torrent unknown status" + } +} + +const ( + TorrentAdded = TorrentStat(iota) + TorrentGettingInfo + TorrentPreload + TorrentWorking + TorrentClosed + TorrentInDB +) + +type TorrentStatus struct { + Title string `json:"title"` + Poster string `json:"poster"` + Data string `json:"data,omitempty"` + Timestamp int64 `json:"timestamp"` + Name string `json:"name,omitempty"` + Hash string `json:"hash,omitempty"` + Stat TorrentStat `json:"stat"` + StatString string `json:"stat_string"` + LoadedSize int64 `json:"loaded_size,omitempty"` + TorrentSize int64 `json:"torrent_size,omitempty"` + PreloadedBytes int64 `json:"preloaded_bytes,omitempty"` + PreloadSize int64 `json:"preload_size,omitempty"` + DownloadSpeed float64 `json:"download_speed,omitempty"` + UploadSpeed float64 `json:"upload_speed,omitempty"` + TotalPeers int `json:"total_peers,omitempty"` + PendingPeers int `json:"pending_peers,omitempty"` + ActivePeers int `json:"active_peers,omitempty"` + ConnectedSeeders int `json:"connected_seeders,omitempty"` + HalfOpenPeers int `json:"half_open_peers,omitempty"` + BytesWritten int64 `json:"bytes_written,omitempty"` + BytesWrittenData int64 `json:"bytes_written_data,omitempty"` + BytesRead int64 `json:"bytes_read,omitempty"` + BytesReadData int64 `json:"bytes_read_data,omitempty"` + BytesReadUsefulData int64 `json:"bytes_read_useful_data,omitempty"` + ChunksWritten int64 `json:"chunks_written,omitempty"` + ChunksRead int64 `json:"chunks_read,omitempty"` + ChunksReadUseful int64 `json:"chunks_read_useful,omitempty"` + ChunksReadWasted int64 `json:"chunks_read_wasted,omitempty"` + PiecesDirtiedGood int64 `json:"pieces_dirtied_good,omitempty"` + PiecesDirtiedBad int64 `json:"pieces_dirtied_bad,omitempty"` + DurationSeconds float64 `json:"duration_seconds,omitempty"` + BitRate string `json:"bit_rate,omitempty"` + + FileStats []*TorrentFileStat `json:"file_stats,omitempty"` + Trackers []string `json:"trackers,omitempty"` +} + +type TorrentFileStat struct { + Id int `json:"id,omitempty"` + Path string `json:"path,omitempty"` + Length int64 `json:"length,omitempty"` +} diff --git a/go/torrserver/torr/storage/state/state.go b/go/torrserver/torr/storage/state/state.go new file mode 100644 index 0000000000..301c2edf08 --- /dev/null +++ b/go/torrserver/torr/storage/state/state.go @@ -0,0 +1,30 @@ +package state + +import ( + "server/torr/state" +) + +type CacheState struct { + Hash string + Capacity int64 + Filled int64 + PiecesLength int64 + PiecesCount int + Torrent *state.TorrentStatus + Pieces map[int]ItemState + Readers []*ReaderState +} + +type ItemState struct { + Id int + Length int64 + Size int64 + Completed bool + Priority int +} + +type ReaderState struct { + Start int + End int + Reader int +} diff --git a/go/torrserver/torr/storage/storage.go b/go/torrserver/torr/storage/storage.go new file mode 100644 index 0000000000..87dfbd5255 --- /dev/null +++ b/go/torrserver/torr/storage/storage.go @@ -0,0 +1,12 @@ +package storage + +import ( + "github.com/anacrolix/torrent/metainfo" + "github.com/anacrolix/torrent/storage" +) + +type Storage interface { + storage.ClientImpl + + CloseHash(hash metainfo.Hash) +} diff --git a/go/torrserver/torr/storage/torrstor/cache.go b/go/torrserver/torr/storage/torrstor/cache.go new file mode 100644 index 0000000000..5a16480724 --- /dev/null +++ b/go/torrserver/torr/storage/torrstor/cache.go @@ -0,0 +1,370 @@ +package torrstor + +import ( + "os" + "path/filepath" + "sort" + "sync" + "time" + + "github.com/anacrolix/torrent" + + "server/log" + "server/settings" + "server/torr/storage/state" + "server/torr/utils" + + "github.com/anacrolix/torrent/metainfo" + "github.com/anacrolix/torrent/storage" +) + +type Cache struct { + storage.TorrentImpl + storage *Storage + + capacity int64 + filled int64 + hash metainfo.Hash + + pieceLength int64 + pieceCount int + + pieces map[int]*Piece + + readers map[*Reader]struct{} + muReaders sync.Mutex + + isRemove bool + isClosed bool + muRemove sync.Mutex + torrent *torrent.Torrent +} + +func NewCache(capacity int64, storage *Storage) *Cache { + ret := &Cache{ + capacity: capacity, + filled: 0, + pieces: make(map[int]*Piece), + storage: storage, + readers: make(map[*Reader]struct{}), + } + + return ret +} + +func (c *Cache) Init(info *metainfo.Info, hash metainfo.Hash) { + log.TLogln("Create cache for:", info.Name, hash.HexString()) + if c.capacity == 0 { + c.capacity = info.PieceLength * 4 + } + + c.pieceLength = info.PieceLength + c.pieceCount = info.NumPieces() + c.hash = hash + + if settings.BTsets.UseDisk { + name := filepath.Join(settings.BTsets.TorrentsSavePath, hash.HexString()) + err := os.MkdirAll(name, 0o777) + if err != nil { + log.TLogln("Error create dir:", err) + } + } + + for i := 0; i < c.pieceCount; i++ { + c.pieces[i] = NewPiece(i, c) + } +} + +func (c *Cache) SetTorrent(torr *torrent.Torrent) { + c.torrent = torr +} + +func (c *Cache) Piece(m metainfo.Piece) storage.PieceImpl { + if val, ok := c.pieces[m.Index()]; ok { + return val + } + return &PieceFake{} +} + +func (c *Cache) Close() error { + log.TLogln("Close cache for:", c.hash) + c.isClosed = true + + delete(c.storage.caches, c.hash) + + if settings.BTsets.RemoveCacheOnDrop { + name := filepath.Join(settings.BTsets.TorrentsSavePath, c.hash.HexString()) + if name != "" && name != "/" { + for _, v := range c.pieces { + if v.dPiece != nil { + os.Remove(v.dPiece.name) + } + } + os.Remove(name) + } + } + + c.muReaders.Lock() + c.readers = nil + c.pieces = nil + c.muReaders.Unlock() + + utils.FreeOSMemGC() + return nil +} + +func (c *Cache) removePiece(piece *Piece) { + if !c.isClosed { + piece.Release() + } +} + +func (c *Cache) AdjustRA(readahead int64) { + if settings.BTsets.CacheSize == 0 { + c.capacity = readahead * 3 + } + if c.Readers() > 0 { + c.muReaders.Lock() + for r := range c.readers { + r.SetReadahead(readahead) + } + c.muReaders.Unlock() + } +} + +func (c *Cache) GetState() *state.CacheState { + cState := new(state.CacheState) + + piecesState := make(map[int]state.ItemState, 0) + var fill int64 = 0 + + if len(c.pieces) > 0 { + for _, p := range c.pieces { + if p.Size > 0 { + fill += p.Size + piecesState[p.Id] = state.ItemState{ + Id: p.Id, + Size: p.Size, + Length: c.pieceLength, + Completed: p.Complete, + Priority: int(c.torrent.PieceState(p.Id).Priority), + } + } + } + } + + readersState := make([]*state.ReaderState, 0) + + if c.Readers() > 0 { + c.muReaders.Lock() + for r := range c.readers { + rng := r.getPiecesRange() + pc := r.getReaderPiece() + readersState = append(readersState, &state.ReaderState{ + Start: rng.Start, + End: rng.End, + Reader: pc, + }) + } + c.muReaders.Unlock() + } + + c.filled = fill + cState.Capacity = c.capacity + cState.PiecesLength = c.pieceLength + cState.PiecesCount = c.pieceCount + cState.Hash = c.hash.HexString() + cState.Filled = fill + cState.Pieces = piecesState + cState.Readers = readersState + return cState +} + +func (c *Cache) cleanPieces() { + if c.isRemove || c.isClosed { + return + } + c.muRemove.Lock() + if c.isRemove { + c.muRemove.Unlock() + return + } + c.isRemove = true + defer func() { c.isRemove = false }() + c.muRemove.Unlock() + + remPieces := c.getRemPieces() + if c.filled > c.capacity { + rems := (c.filled-c.capacity)/c.pieceLength + 1 + for _, p := range remPieces { + c.removePiece(p) + rems-- + if rems <= 0 { + utils.FreeOSMemGC() + return + } + } + } +} + +func (c *Cache) getRemPieces() []*Piece { + piecesRemove := make([]*Piece, 0) + fill := int64(0) + + ranges := make([]Range, 0) + c.muReaders.Lock() + for r := range c.readers { + r.checkReader() + if r.isUse { + ranges = append(ranges, r.getPiecesRange()) + } + } + c.muReaders.Unlock() + ranges = mergeRange(ranges) + + for id, p := range c.pieces { + if p.Size > 0 { + fill += p.Size + } + if len(ranges) > 0 { + if !inRanges(ranges, id) { + if p.Size > 0 && !c.isIdInFileBE(ranges, id) { + piecesRemove = append(piecesRemove, p) + } + } + } else { + // on preload clean + if p.Size > 0 && !c.isIdInFileBE(ranges, id) { + piecesRemove = append(piecesRemove, p) + } + } + } + + c.clearPriority() + c.setLoadPriority(ranges) + + sort.Slice(piecesRemove, func(i, j int) bool { + return piecesRemove[i].Accessed < piecesRemove[j].Accessed + }) + + c.filled = fill + return piecesRemove +} + +func (c *Cache) setLoadPriority(ranges []Range) { + c.muReaders.Lock() + for r := range c.readers { + if !r.isUse { + continue + } + if c.isIdInFileBE(ranges, r.getReaderPiece()) { + continue + } + readerPos := r.getReaderPiece() + readerRAHPos := r.getReaderRAHPiece() + end := r.getPiecesRange().End + count := settings.BTsets.ConnectionsLimit / len(c.readers) // max concurrent loading blocks + limit := 0 + for i := readerPos; i < end && limit < count; i++ { + if !c.pieces[i].Complete { + if i == readerPos { + c.torrent.Piece(i).SetPriority(torrent.PiecePriorityNow) + } else if i == readerPos+1 { + c.torrent.Piece(i).SetPriority(torrent.PiecePriorityNext) + } else if i > readerPos && i <= readerRAHPos { + c.torrent.Piece(i).SetPriority(torrent.PiecePriorityReadahead) + } else if i > readerRAHPos && i <= readerRAHPos+5 && c.torrent.PieceState(i).Priority != torrent.PiecePriorityHigh { + c.torrent.Piece(i).SetPriority(torrent.PiecePriorityHigh) + } else if i > readerRAHPos+5 && c.torrent.PieceState(i).Priority != torrent.PiecePriorityNormal { + c.torrent.Piece(i).SetPriority(torrent.PiecePriorityNormal) + } + limit++ + } + } + } + c.muReaders.Unlock() +} + +func (c *Cache) isIdInFileBE(ranges []Range, id int) bool { + // keep 8/16 MB + FileRangeNotDelete := int64(c.pieceLength) + if FileRangeNotDelete < 8<<20 { + FileRangeNotDelete = 8 << 20 + } + + for _, rng := range ranges { + ss := int(rng.File.Offset() / c.pieceLength) + se := int((rng.File.Offset() + FileRangeNotDelete) / c.pieceLength) + + es := int((rng.File.Offset() + rng.File.Length() - FileRangeNotDelete) / c.pieceLength) + ee := int((rng.File.Offset() + rng.File.Length()) / c.pieceLength) + + if id >= ss && id < se || id > es && id <= ee { + return true + } + } + return false +} + +////////////////// +// Reader section +//////// + +func (c *Cache) NewReader(file *torrent.File) *Reader { + return newReader(file, c) +} + +func (c *Cache) Readers() int { + if c == nil { + return 0 + } + c.muReaders.Lock() + defer c.muReaders.Unlock() + if c.readers == nil { + return 0 + } + return len(c.readers) +} + +func (c *Cache) CloseReader(r *Reader) { + r.cache.muReaders.Lock() + r.Close() + delete(r.cache.readers, r) + r.cache.muReaders.Unlock() + go c.clearPriority() +} + +func (c *Cache) clearPriority() { + time.Sleep(time.Second) + ranges := make([]Range, 0) + c.muReaders.Lock() + for r := range c.readers { + r.checkReader() + if r.isUse { + ranges = append(ranges, r.getPiecesRange()) + } + } + c.muReaders.Unlock() + ranges = mergeRange(ranges) + + for id := range c.pieces { + if len(ranges) > 0 { + if !inRanges(ranges, id) { + if c.torrent.PieceState(id).Priority != torrent.PiecePriorityNone { + c.torrent.Piece(id).SetPriority(torrent.PiecePriorityNone) + } + } + } else { + if c.torrent.PieceState(id).Priority != torrent.PiecePriorityNone { + c.torrent.Piece(id).SetPriority(torrent.PiecePriorityNone) + } + } + } +} + +func (c *Cache) GetCapacity() int64 { + if c == nil { + return 0 + } + return c.capacity +} diff --git a/go/torrserver/torr/storage/torrstor/diskpiece.go b/go/torrserver/torr/storage/torrstor/diskpiece.go new file mode 100644 index 0000000000..822a68e0ff --- /dev/null +++ b/go/torrserver/torr/storage/torrstor/diskpiece.go @@ -0,0 +1,85 @@ +package torrstor + +import ( + "io" + "os" + "path/filepath" + "strconv" + "sync" + "time" + + "server/log" + "server/settings" +) + +type DiskPiece struct { + piece *Piece + + name string + + mu sync.RWMutex +} + +func NewDiskPiece(p *Piece) *DiskPiece { + name := filepath.Join(settings.BTsets.TorrentsSavePath, p.cache.hash.HexString(), strconv.Itoa(p.Id)) + ff, err := os.Stat(name) + if err == nil { + p.Size = ff.Size() + p.Complete = ff.Size() == p.cache.pieceLength + p.Accessed = ff.ModTime().Unix() + } + return &DiskPiece{piece: p, name: name} +} + +func (p *DiskPiece) WriteAt(b []byte, off int64) (n int, err error) { + p.mu.Lock() + defer p.mu.Unlock() + + ff, err := os.OpenFile(p.name, os.O_RDWR|os.O_CREATE, 0o666) + if err != nil { + log.TLogln("Error open file:", err) + return 0, err + } + defer ff.Close() + n, err = ff.WriteAt(b, off) + + p.piece.Size += int64(n) + if p.piece.Size > p.piece.cache.pieceLength { + p.piece.Size = p.piece.cache.pieceLength + } + p.piece.Accessed = time.Now().Unix() + return +} + +func (p *DiskPiece) ReadAt(b []byte, off int64) (n int, err error) { + p.mu.Lock() + defer p.mu.Unlock() + + ff, err := os.OpenFile(p.name, os.O_RDONLY, 0o666) + if os.IsNotExist(err) { + return 0, io.EOF + } + if err != nil { + log.TLogln("Error open file:", err) + return 0, err + } + defer ff.Close() + + n, err = ff.ReadAt(b, off) + + p.piece.Accessed = time.Now().Unix() + if int64(len(b))+off >= p.piece.Size { + go p.piece.cache.cleanPieces() + } + return n, nil +} + +func (p *DiskPiece) Release() { + p.mu.Lock() + defer p.mu.Unlock() + + p.piece.Size = 0 + p.piece.Complete = false + + os.Remove(p.name) +} diff --git a/go/torrserver/torr/storage/torrstor/mempiece.go b/go/torrserver/torr/storage/torrstor/mempiece.go new file mode 100644 index 0000000000..acb3074c7a --- /dev/null +++ b/go/torrserver/torr/storage/torrstor/mempiece.go @@ -0,0 +1,70 @@ +package torrstor + +import ( + "io" + "sync" + "time" +) + +type MemPiece struct { + piece *Piece + + buffer []byte + mu sync.RWMutex +} + +func NewMemPiece(p *Piece) *MemPiece { + return &MemPiece{piece: p} +} + +func (p *MemPiece) WriteAt(b []byte, off int64) (n int, err error) { + p.mu.Lock() + defer p.mu.Unlock() + + if p.buffer == nil { + go p.piece.cache.cleanPieces() + p.buffer = make([]byte, p.piece.cache.pieceLength, p.piece.cache.pieceLength) + } + n = copy(p.buffer[off:], b[:]) + p.piece.Size += int64(n) + if p.piece.Size > p.piece.cache.pieceLength { + p.piece.Size = p.piece.cache.pieceLength + } + p.piece.Accessed = time.Now().Unix() + return +} + +func (p *MemPiece) ReadAt(b []byte, off int64) (n int, err error) { + p.mu.RLock() + defer p.mu.RUnlock() + + size := len(b) + if size+int(off) > len(p.buffer) { + size = len(p.buffer) - int(off) + if size < 0 { + size = 0 + } + } + if len(p.buffer) < int(off) || len(p.buffer) < int(off)+size { + return 0, io.EOF + } + n = copy(b, p.buffer[int(off) : int(off)+size][:]) + p.piece.Accessed = time.Now().Unix() + if int64(len(b))+off >= p.piece.Size { + go p.piece.cache.cleanPieces() + } + if n == 0 { + return 0, io.EOF + } + return n, nil +} + +func (p *MemPiece) Release() { + p.mu.Lock() + defer p.mu.Unlock() + if p.buffer != nil { + p.buffer = nil + } + p.piece.Size = 0 + p.piece.Complete = false +} diff --git a/go/torrserver/torr/storage/torrstor/piece.go b/go/torrserver/torr/storage/torrstor/piece.go new file mode 100644 index 0000000000..f91aac40c7 --- /dev/null +++ b/go/torrserver/torr/storage/torrstor/piece.go @@ -0,0 +1,81 @@ +package torrstor + +import ( + "github.com/anacrolix/torrent" + "github.com/anacrolix/torrent/storage" + "server/settings" +) + +type Piece struct { + storage.PieceImpl `json:"-"` + + Id int `json:"-"` + Size int64 `json:"size"` + + Complete bool `json:"complete"` + Accessed int64 `json:"accessed"` + + mPiece *MemPiece `json:"-"` + dPiece *DiskPiece `json:"-"` + + cache *Cache `json:"-"` +} + +func NewPiece(id int, cache *Cache) *Piece { + p := &Piece{ + Id: id, + cache: cache, + } + + if !settings.BTsets.UseDisk { + p.mPiece = NewMemPiece(p) + } else { + p.dPiece = NewDiskPiece(p) + } + return p +} + +func (p *Piece) WriteAt(b []byte, off int64) (n int, err error) { + if !settings.BTsets.UseDisk { + return p.mPiece.WriteAt(b, off) + } else { + return p.dPiece.WriteAt(b, off) + } +} + +func (p *Piece) ReadAt(b []byte, off int64) (n int, err error) { + if !settings.BTsets.UseDisk { + return p.mPiece.ReadAt(b, off) + } else { + return p.dPiece.ReadAt(b, off) + } +} + +func (p *Piece) MarkComplete() error { + p.Complete = true + return nil +} + +func (p *Piece) MarkNotComplete() error { + p.Complete = false + return nil +} + +func (p *Piece) Completion() storage.Completion { + return storage.Completion{ + Complete: p.Complete, + Ok: true, + } +} + +func (p *Piece) Release() { + if !settings.BTsets.UseDisk { + p.mPiece.Release() + } else { + p.dPiece.Release() + } + if !p.cache.isClosed { + p.cache.torrent.Piece(p.Id).SetPriority(torrent.PiecePriorityNone) + p.cache.torrent.Piece(p.Id).UpdateCompletion() + } +} diff --git a/go/torrserver/torr/storage/torrstor/piecefake.go b/go/torrserver/torr/storage/torrstor/piecefake.go new file mode 100644 index 0000000000..7bcff21c7c --- /dev/null +++ b/go/torrserver/torr/storage/torrstor/piecefake.go @@ -0,0 +1,34 @@ +package torrstor + +import ( + "errors" + + "github.com/anacrolix/torrent/storage" +) + +type PieceFake struct{} + +func (PieceFake) ReadAt(p []byte, off int64) (n int, err error) { + err = errors.New("fake") + return +} + +func (PieceFake) WriteAt(p []byte, off int64) (n int, err error) { + err = errors.New("fake") + return +} + +func (PieceFake) MarkComplete() error { + return errors.New("fake") +} + +func (PieceFake) MarkNotComplete() error { + return errors.New("fake") +} + +func (PieceFake) Completion() storage.Completion { + return storage.Completion{ + Complete: false, + Ok: true, + } +} diff --git a/go/torrserver/torr/storage/torrstor/ranges.go b/go/torrserver/torr/storage/torrstor/ranges.go new file mode 100644 index 0000000000..4b28621e6c --- /dev/null +++ b/go/torrserver/torr/storage/torrstor/ranges.go @@ -0,0 +1,52 @@ +package torrstor + +import ( + "sort" + + "github.com/anacrolix/torrent" +) + +type Range struct { + Start, End int + File *torrent.File +} + +func inRanges(ranges []Range, ind int) bool { + for _, r := range ranges { + if ind >= r.Start && ind <= r.End { + return true + } + } + return false +} + +func mergeRange(ranges []Range) []Range { + if len(ranges) <= 1 { + return ranges + } + // copy ranges + merged := append([]Range(nil), ranges...) + + sort.Slice(merged, func(i, j int) bool { + if merged[i].Start < merged[j].Start { + return true + } + if merged[i].Start == merged[j].Start && merged[i].End < merged[j].End { + return true + } + return false + }) + + j := 0 + for i := 1; i < len(merged); i++ { + if merged[j].End >= merged[i].Start { + if merged[j].End < merged[i].End { + merged[j].End = merged[i].End + } + } else { + j++ + merged[j] = merged[i] + } + } + return merged[:j+1] +} diff --git a/go/torrserver/torr/storage/torrstor/reader.go b/go/torrserver/torr/storage/torrstor/reader.go new file mode 100644 index 0000000000..79f5770922 --- /dev/null +++ b/go/torrserver/torr/storage/torrstor/reader.go @@ -0,0 +1,206 @@ +package torrstor + +import ( + "io" + "strings" + "sync" + "time" + + "github.com/anacrolix/torrent" + + "server/log" + "server/settings" +) + +type Reader struct { + torrent.Reader + offset int64 + readahead int64 + file *torrent.File + + cache *Cache + isClosed bool + + ///Preload + lastAccess int64 + isUse bool + mu sync.Mutex +} + +func newReader(file *torrent.File, cache *Cache) *Reader { + r := new(Reader) + r.file = file + r.Reader = file.NewReader() + + r.SetReadahead(0) + r.cache = cache + r.isUse = true + + cache.muReaders.Lock() + cache.readers[r] = struct{}{} + cache.muReaders.Unlock() + return r +} + +func (r *Reader) Seek(offset int64, whence int) (n int64, err error) { + if r.isClosed { + return 0, io.EOF + } + switch whence { + case io.SeekStart: + r.offset = offset + case io.SeekCurrent: + r.offset += offset + case io.SeekEnd: + r.offset = r.file.Length() + offset + } + r.readerOn() + n, err = r.Reader.Seek(offset, whence) + r.offset = n + r.lastAccess = time.Now().Unix() + return +} + +func (r *Reader) Read(p []byte) (n int, err error) { + err = io.EOF + if r.isClosed { + return + } + if r.file.Torrent() != nil && r.file.Torrent().Info() != nil { + r.readerOn() + n, err = r.Reader.Read(p) + + // samsung tv fix xvid/divx + if r.offset == 0 && len(p) >= 192 { + str := strings.ToLower(string(p[112:116])) + if str == "xvid" || str == "divx" { + p[112] = 0x4D // M + p[113] = 0x50 // P + p[114] = 0x34 // 4 + p[115] = 0x56 // V + } + str = strings.ToLower(string(p[188:192])) + if str == "xvid" || str == "divx" { + p[188] = 0x4D // M + p[189] = 0x50 // P + p[190] = 0x34 // 4 + p[191] = 0x56 // V + } + } + + r.offset += int64(n) + r.lastAccess = time.Now().Unix() + } else { + log.TLogln("Torrent closed and readed") + } + return +} + +func (r *Reader) SetReadahead(length int64) { + if r.cache != nil && length > r.cache.capacity { + length = r.cache.capacity + } + if r.isUse { + r.Reader.SetReadahead(length) + } + r.readahead = length +} + +func (r *Reader) Offset() int64 { + return r.offset +} + +func (r *Reader) Readahead() int64 { + return r.readahead +} + +func (r *Reader) Close() { + // file reader close in gotorrent + // this struct close in cache + r.isClosed = true + if len(r.file.Torrent().Files()) > 0 { + r.Reader.Close() + } + go r.cache.getRemPieces() +} + +func (r *Reader) getPiecesRange() Range { + startOff, endOff := r.getOffsetRange() + return Range{r.getPieceNum(startOff), r.getPieceNum(endOff), r.file} +} + +func (r *Reader) getReaderPiece() int { + return r.getPieceNum(r.offset) +} + +func (r *Reader) getReaderRAHPiece() int { + return r.getPieceNum(r.offset + r.readahead) +} + +func (r *Reader) getPieceNum(offset int64) int { + return int((offset + r.file.Offset()) / r.cache.pieceLength) +} + +func (r *Reader) getOffsetRange() (int64, int64) { + prc := int64(settings.BTsets.ReaderReadAHead) + readers := int64(r.getUseReaders()) + if readers == 0 { + readers = 1 + } + + beginOffset := r.offset - (r.cache.capacity/readers)*(100-prc)/100 + endOffset := r.offset + (r.cache.capacity/readers)*prc/100 + + if beginOffset < 0 { + beginOffset = 0 + } + + if endOffset > r.file.Length() { + endOffset = r.file.Length() + } + return beginOffset, endOffset +} + +func (r *Reader) checkReader() { + if time.Now().Unix() > r.lastAccess+60 && len(r.cache.readers) > 1 { + r.readerOff() + } else { + r.readerOn() + } +} + +func (r *Reader) readerOn() { + r.mu.Lock() + defer r.mu.Unlock() + if !r.isUse { + if pos, err := r.Reader.Seek(0, io.SeekCurrent); err == nil && pos == 0 { + r.Reader.Seek(r.offset, io.SeekStart) + } + r.SetReadahead(r.readahead) + r.isUse = true + } +} + +func (r *Reader) readerOff() { + r.mu.Lock() + defer r.mu.Unlock() + if r.isUse { + r.SetReadahead(0) + r.isUse = false + if r.offset > 0 { + r.Reader.Seek(0, io.SeekStart) + } + } +} + +func (r *Reader) getUseReaders() int { + readers := 0 + if r.cache != nil { + for reader := range r.cache.readers { + if reader.isUse { + readers++ + } + } + } + return readers +} diff --git a/go/torrserver/torr/storage/torrstor/storage.go b/go/torrserver/torr/storage/torrstor/storage.go new file mode 100644 index 0000000000..31b23bb046 --- /dev/null +++ b/go/torrserver/torr/storage/torrstor/storage.go @@ -0,0 +1,72 @@ +package torrstor + +import ( + "sync" + + "server/torr/storage" + + "github.com/anacrolix/torrent/metainfo" + ts "github.com/anacrolix/torrent/storage" +) + +type Storage struct { + storage.Storage + + caches map[metainfo.Hash]*Cache + capacity int64 + mu sync.Mutex +} + +func NewStorage(capacity int64) *Storage { + stor := new(Storage) + stor.capacity = capacity + stor.caches = make(map[metainfo.Hash]*Cache) + return stor +} + +func (s *Storage) OpenTorrent(info *metainfo.Info, infoHash metainfo.Hash) (ts.TorrentImpl, error) { + // capFunc := func() (int64, bool) { + // return s.capacity, true + // } + s.mu.Lock() + defer s.mu.Unlock() + ch := NewCache(s.capacity, s) + ch.Init(info, infoHash) + s.caches[infoHash] = ch + return ch, nil + // return ts.TorrentImpl{ + // Piece: ch.Piece, + // Close: ch.Close, + // Capacity: &capFunc, + // }, nil +} + +func (s *Storage) CloseHash(hash metainfo.Hash) { + if s.caches == nil { + return + } + s.mu.Lock() + defer s.mu.Unlock() + if ch, ok := s.caches[hash]; ok { + ch.Close() + delete(s.caches, hash) + } +} + +func (s *Storage) Close() error { + s.mu.Lock() + defer s.mu.Unlock() + for _, ch := range s.caches { + ch.Close() + } + return nil +} + +func (s *Storage) GetCache(hash metainfo.Hash) *Cache { + s.mu.Lock() + defer s.mu.Unlock() + if cache, ok := s.caches[hash]; ok { + return cache + } + return nil +} diff --git a/go/torrserver/torr/stream.go b/go/torrserver/torr/stream.go new file mode 100644 index 0000000000..6ad9ffcc9a --- /dev/null +++ b/go/torrserver/torr/stream.go @@ -0,0 +1,91 @@ +package torr + +import ( + "encoding/hex" + "errors" + "fmt" + "log" + "net" + "net/http" + "time" + + "github.com/anacrolix/dms/dlna" + "github.com/anacrolix/missinggo/v2/httptoo" + "github.com/anacrolix/torrent" + + mt "server/mimetype" + sets "server/settings" + "server/torr/state" +) + +func (t *Torrent) Stream(fileID int, req *http.Request, resp http.ResponseWriter) error { + if !t.GotInfo() { + http.NotFound(resp, req) + return errors.New("torrent don't get info") + } + + st := t.Status() + var stFile *state.TorrentFileStat + for _, fileStat := range st.FileStats { + if fileStat.Id == fileID { + stFile = fileStat + break + } + } + if stFile == nil { + return fmt.Errorf("file with id %v not found", fileID) + } + + files := t.Files() + var file *torrent.File + for _, tfile := range files { + if tfile.Path() == stFile.Path { + file = tfile + break + } + } + if file == nil { + return fmt.Errorf("file with id %v not found", fileID) + } + + reader := t.NewReader(file) + + host, port, err := net.SplitHostPort(req.RemoteAddr) + if sets.BTsets.EnableDebug { + if err != nil { + log.Println("Connect client") + } else { + log.Println("Connect client", host, port) + } + } + + sets.SetViewed(&sets.Viewed{Hash: t.Hash().HexString(), FileIndex: fileID}) + + resp.Header().Set("Connection", "close") + etag := hex.EncodeToString([]byte(fmt.Sprintf("%s/%s", t.Hash().HexString(), file.Path()))) + resp.Header().Set("ETag", httptoo.EncodeQuotedString(etag)) + // DLNA headers + resp.Header().Set("transferMode.dlna.org", "Streaming") + mime, err := mt.MimeTypeByPath(file.Path()) + if err == nil && mime.IsMedia() { + resp.Header().Set("content-type", mime.String()) + } + if req.Header.Get("getContentFeatures.dlna.org") != "" { + resp.Header().Set("contentFeatures.dlna.org", dlna.ContentFeatures{ + SupportRange: true, + SupportTimeSeek: true, + }.String()) + } + + http.ServeContent(resp, req, file.Path(), time.Unix(t.Timestamp, 0), reader) + + t.CloseReader(reader) + if sets.BTsets.EnableDebug { + if err != nil { + log.Println("Disconnect client") + } else { + log.Println("Disconnect client", host, port) + } + } + return nil +} diff --git a/go/torrserver/torr/torrent.go b/go/torrserver/torr/torrent.go new file mode 100644 index 0000000000..e6db244e8f --- /dev/null +++ b/go/torrserver/torr/torrent.go @@ -0,0 +1,361 @@ +package torr + +import ( + "errors" + "sync" + "time" + + "github.com/anacrolix/torrent" + "github.com/anacrolix/torrent/metainfo" + + "server/log" + "server/settings" + "server/torr/state" + cacheSt "server/torr/storage/state" + "server/torr/storage/torrstor" + "server/torr/utils" +) + +type Torrent struct { + Title string + Poster string + Data string + *torrent.TorrentSpec + + Stat state.TorrentStat + Timestamp int64 + Size int64 + + *torrent.Torrent + muTorrent sync.Mutex + + bt *BTServer + cache *torrstor.Cache + + lastTimeSpeed time.Time + DownloadSpeed float64 + UploadSpeed float64 + BytesReadUsefulData int64 + BytesWrittenData int64 + + PreloadSize int64 + PreloadedBytes int64 + + DurationSeconds float64 + BitRate string + + expiredTime time.Time + + closed <-chan struct{} + + progressTicker *time.Ticker +} + +func NewTorrent(spec *torrent.TorrentSpec, bt *BTServer) (*Torrent, error) { + // https://github.com/anacrolix/torrent/issues/747 + if bt == nil || bt.client == nil { + return nil, errors.New("BT client not connected") + } + switch settings.BTsets.RetrackersMode { + case 1: + spec.Trackers = append(spec.Trackers, [][]string{utils.GetDefTrackers()}...) + case 2: + spec.Trackers = nil + case 3: + spec.Trackers = [][]string{utils.GetDefTrackers()} + } + + trackers := utils.GetTrackerFromFile() + if len(trackers) > 0 { + spec.Trackers = append(spec.Trackers, [][]string{trackers}...) + } + + goTorrent, _, err := bt.client.AddTorrentSpec(spec) + if err != nil { + return nil, err + } + + bt.mu.Lock() + defer bt.mu.Unlock() + if tor, ok := bt.torrents[spec.InfoHash]; ok { + return tor, nil + } + + torr := new(Torrent) + torr.Torrent = goTorrent + torr.Stat = state.TorrentAdded + torr.lastTimeSpeed = time.Now() + torr.bt = bt + torr.closed = goTorrent.Closed() + torr.TorrentSpec = spec + torr.AddExpiredTime(time.Minute) + torr.Timestamp = time.Now().Unix() + + go torr.watch() + + bt.torrents[spec.InfoHash] = torr + return torr, nil +} + +func (t *Torrent) WaitInfo() bool { + if t.Torrent == nil { + return false + } + + // Close torrent if not info while 5 minutes + tm := time.NewTimer(time.Minute * 5) + + select { + case <-t.Torrent.GotInfo(): + t.cache = t.bt.storage.GetCache(t.Hash()) + t.cache.SetTorrent(t.Torrent) + return true + case <-t.closed: + return false + case <-tm.C: + return false + } +} + +func (t *Torrent) GotInfo() bool { + // log.TLogln("GotInfo state:", t.Stat) + if t.Stat == state.TorrentClosed { + return false + } + // assume we have info in preload state + // and dont override with TorrentWorking + if t.Stat == state.TorrentPreload { + return true + } + t.Stat = state.TorrentGettingInfo + if t.WaitInfo() { + t.Stat = state.TorrentWorking + t.AddExpiredTime(time.Minute * 5) + return true + } else { + t.Close() + return false + } +} + +func (t *Torrent) AddExpiredTime(duration time.Duration) { + t.expiredTime = time.Now().Add(duration) +} + +func (t *Torrent) watch() { + t.progressTicker = time.NewTicker(time.Second) + defer t.progressTicker.Stop() + + for { + select { + case <-t.progressTicker.C: + go t.progressEvent() + case <-t.closed: + return + } + } +} + +func (t *Torrent) progressEvent() { + if t.expired() { + if t.TorrentSpec != nil { + log.TLogln("Torrent close by timeout", t.TorrentSpec.InfoHash.HexString()) + } + t.bt.RemoveTorrent(t.Hash()) + return + } + + t.muTorrent.Lock() + if t.Torrent != nil && t.Torrent.Info() != nil { + st := t.Torrent.Stats() + deltaDlBytes := st.BytesRead.Int64() - t.BytesReadUsefulData + deltaUpBytes := st.BytesWritten.Int64() - t.BytesWrittenData + deltaTime := time.Since(t.lastTimeSpeed).Seconds() + + t.DownloadSpeed = float64(deltaDlBytes) / deltaTime + t.UploadSpeed = float64(deltaUpBytes) / deltaTime + + t.BytesReadUsefulData = st.BytesRead.Int64() + t.BytesWrittenData = st.BytesWritten.Int64() + + if t.cache != nil { + t.PreloadedBytes = t.cache.GetState().Filled + } + } else { + t.DownloadSpeed = 0 + t.UploadSpeed = 0 + } + t.muTorrent.Unlock() + + t.lastTimeSpeed = time.Now() + t.updateRA() +} + +func (t *Torrent) updateRA() { + // t.muTorrent.Lock() + // defer t.muTorrent.Unlock() + // if t.Torrent != nil && t.Torrent.Info() != nil { + // pieceLen := t.Torrent.Info().PieceLength + // adj := pieceLen * int64(t.Torrent.Stats().ActivePeers) / int64(1+t.cache.Readers()) + // switch { + // case adj < pieceLen: + // adj = pieceLen + // case adj > pieceLen*4: + // adj = pieceLen * 4 + // } + // go t.cache.AdjustRA(adj) + // } + adj := int64(16 << 20) // 16 MB fixed RA + go t.cache.AdjustRA(adj) +} + +func (t *Torrent) expired() bool { + return t.cache.Readers() == 0 && t.expiredTime.Before(time.Now()) && (t.Stat == state.TorrentWorking || t.Stat == state.TorrentClosed) +} + +func (t *Torrent) Files() []*torrent.File { + if t.Torrent != nil && t.Torrent.Info() != nil { + files := t.Torrent.Files() + return files + } + return nil +} + +func (t *Torrent) Hash() metainfo.Hash { + if t.Torrent != nil { + t.Torrent.InfoHash() + } + if t.TorrentSpec != nil { + return t.TorrentSpec.InfoHash + } + return [20]byte{} +} + +func (t *Torrent) Length() int64 { + if t.Info() == nil { + return 0 + } + return t.Torrent.Length() +} + +func (t *Torrent) NewReader(file *torrent.File) *torrstor.Reader { + if t.Stat == state.TorrentClosed { + return nil + } + reader := t.cache.NewReader(file) + return reader +} + +func (t *Torrent) CloseReader(reader *torrstor.Reader) { + t.cache.CloseReader(reader) + t.AddExpiredTime(time.Second * time.Duration(settings.BTsets.TorrentDisconnectTimeout)) +} + +func (t *Torrent) GetCache() *torrstor.Cache { + return t.cache +} + +func (t *Torrent) drop() { + t.muTorrent.Lock() + defer t.muTorrent.Unlock() + if t.Torrent != nil { + t.Torrent.Drop() + t.Torrent = nil + } +} + +func (t *Torrent) Close() bool { + if t.cache != nil && t.cache.Readers() > 0 { + return false + } + t.Stat = state.TorrentClosed + + t.bt.mu.Lock() + delete(t.bt.torrents, t.Hash()) + t.bt.mu.Unlock() + + t.drop() + return true +} + +func (t *Torrent) Status() *state.TorrentStatus { + t.muTorrent.Lock() + defer t.muTorrent.Unlock() + + st := new(state.TorrentStatus) + + st.Stat = t.Stat + st.StatString = t.Stat.String() + st.Title = t.Title + st.Poster = t.Poster + st.Data = t.Data + st.Timestamp = t.Timestamp + st.TorrentSize = t.Size + st.BitRate = t.BitRate + st.DurationSeconds = t.DurationSeconds + + if t.TorrentSpec != nil { + st.Hash = t.TorrentSpec.InfoHash.HexString() + } + if t.Torrent != nil { + st.Name = t.Torrent.Name() + st.Hash = t.Torrent.InfoHash().HexString() + st.LoadedSize = t.Torrent.BytesCompleted() + + st.PreloadedBytes = t.PreloadedBytes + st.PreloadSize = t.PreloadSize + st.DownloadSpeed = t.DownloadSpeed + st.UploadSpeed = t.UploadSpeed + + tst := t.Torrent.Stats() + st.BytesWritten = tst.BytesWritten.Int64() + st.BytesWrittenData = tst.BytesWrittenData.Int64() + st.BytesRead = tst.BytesRead.Int64() + st.BytesReadData = tst.BytesReadData.Int64() + st.BytesReadUsefulData = tst.BytesReadUsefulData.Int64() + st.ChunksWritten = tst.ChunksWritten.Int64() + st.ChunksRead = tst.ChunksRead.Int64() + st.ChunksReadUseful = tst.ChunksReadUseful.Int64() + st.ChunksReadWasted = tst.ChunksReadWasted.Int64() + st.PiecesDirtiedGood = tst.PiecesDirtiedGood.Int64() + st.PiecesDirtiedBad = tst.PiecesDirtiedBad.Int64() + st.TotalPeers = tst.TotalPeers + st.PendingPeers = tst.PendingPeers + st.ActivePeers = tst.ActivePeers + st.ConnectedSeeders = tst.ConnectedSeeders + st.HalfOpenPeers = tst.HalfOpenPeers + st.Trackers = flattenStringArray(t.TorrentSpec.Trackers) + + if t.Torrent.Info() != nil { + st.TorrentSize = t.Torrent.Length() + + files := t.Files() + for i, f := range files { + st.FileStats = append(st.FileStats, &state.TorrentFileStat{ + Id: i, + Path: f.Path(), + Length: f.Length(), + }) + } + } + } + + return st +} + +func (t *Torrent) CacheState() *cacheSt.CacheState { + if t.Torrent != nil && t.cache != nil { + st := t.cache.GetState() + st.Torrent = t.Status() + return st + } + return nil +} + +func flattenStringArray(arr [][]string) []string { + var result []string + for _, subArray := range arr { + result = append(result, subArray...) + } + return result +} diff --git a/go/torrserver/torr/utils/blockedIP.go b/go/torrserver/torr/utils/blockedIP.go new file mode 100644 index 0000000000..fbe56b4b07 --- /dev/null +++ b/go/torrserver/torr/utils/blockedIP.go @@ -0,0 +1,35 @@ +package utils + +import ( + "bufio" + "os" + "path/filepath" + "strings" + + "server/settings" + + "github.com/anacrolix/torrent/iplist" +) + +func ReadBlockedIP() (ranger iplist.Ranger, err error) { + buf, err := os.ReadFile(filepath.Join(settings.Path, "blocklist")) + if err != nil { + return nil, err + } + scanner := bufio.NewScanner(strings.NewReader(string(buf))) + var ranges []iplist.Range + for scanner.Scan() { + r, ok, err := iplist.ParseBlocklistP2PLine(scanner.Bytes()) + if err != nil { + return nil, err + } + if ok { + ranges = append(ranges, r) + } + } + err = scanner.Err() + if len(ranges) > 0 { + ranger = iplist.New(ranges) + } + return +} diff --git a/go/torrserver/torr/utils/freemem.go b/go/torrserver/torr/utils/freemem.go new file mode 100644 index 0000000000..00b6a1ec1b --- /dev/null +++ b/go/torrserver/torr/utils/freemem.go @@ -0,0 +1,15 @@ +package utils + +import ( + "runtime" + "runtime/debug" +) + +func FreeOSMem() { + debug.FreeOSMemory() +} + +func FreeOSMemGC() { + runtime.GC() + debug.FreeOSMemory() +} diff --git a/go/torrserver/torr/utils/torrent.go b/go/torrserver/torr/utils/torrent.go new file mode 100644 index 0000000000..7698ab5f38 --- /dev/null +++ b/go/torrserver/torr/utils/torrent.go @@ -0,0 +1,89 @@ +package utils + +import ( + "encoding/base32" + "io" + "math/rand" + "net/http" + "os" + "path/filepath" + "strings" + + "server/settings" + + "golang.org/x/time/rate" +) + +var defTrackers = []string{} + +var loadedTrackers []string + +func SetDefTrackers(trackers []string) { + defTrackers = trackers +} + +func GetTrackerFromFile() []string { + name := filepath.Join(settings.Path, "trackers.txt") + buf, err := os.ReadFile(name) + if err == nil { + list := strings.Split(string(buf), "\n") + var ret []string + for _, l := range list { + if strings.HasPrefix(l, "udp") || strings.HasPrefix(l, "http") { + ret = append(ret, l) + } + } + return ret + } + return nil +} + +func GetDefTrackers() []string { + loadNewTracker() + if len(loadedTrackers) == 0 { + return defTrackers + } + return loadedTrackers +} + +func loadNewTracker() { + if len(loadedTrackers) > 0 { + return + } + resp, err := http.Get("https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_best_ip.txt") + if err == nil { + buf, err := io.ReadAll(resp.Body) + if err == nil { + arr := strings.Split(string(buf), "\n") + var ret []string + for _, s := range arr { + s = strings.TrimSpace(s) + if len(s) > 0 { + ret = append(ret, s) + } + } + loadedTrackers = append(ret, defTrackers...) + } + } +} + +func PeerIDRandom(peer string) string { + randomBytes := make([]byte, 32) + _, err := rand.Read(randomBytes) + if err != nil { + panic(err) + } + return peer + base32.StdEncoding.EncodeToString(randomBytes)[:20-len(peer)] +} + +func Limit(i int) *rate.Limiter { + l := rate.NewLimiter(rate.Inf, 0) + if i > 0 { + b := i + if b < 16*1024 { + b = 16 * 1024 + } + l = rate.NewLimiter(rate.Limit(i), b) + } + return l +} diff --git a/go/torrserver/torr/utils/webImageChecker.go b/go/torrserver/torr/utils/webImageChecker.go new file mode 100644 index 0000000000..4743195ac9 --- /dev/null +++ b/go/torrserver/torr/utils/webImageChecker.go @@ -0,0 +1,36 @@ +package utils + +import ( + "image" + _ "image/gif" + _ "image/jpeg" + _ "image/png" + "net/http" + "strings" + + "golang.org/x/image/webp" + + "server/log" +) + +func CheckImgUrl(link string) bool { + if link == "" { + return false + } + resp, err := http.Get(link) + if err != nil { + log.TLogln("Error check image:", err) + return false + } + defer resp.Body.Close() + if strings.HasSuffix(link, ".webp") { + _, err = webp.Decode(resp.Body) + } else { + _, _, err = image.Decode(resp.Body) + } + if err != nil { + log.TLogln("Error decode image:", err) + return false + } + return err == nil +} diff --git a/go/torrserver/utils/filetypes.go b/go/torrserver/utils/filetypes.go new file mode 100644 index 0000000000..10e3e1507d --- /dev/null +++ b/go/torrserver/utils/filetypes.go @@ -0,0 +1,99 @@ +package utils + +import ( + "path/filepath" + "strings" + + "server/torr/state" +) + +var extVideo = map[string]interface{}{ + ".3g2": nil, + ".3gp": nil, + ".aaf": nil, + ".asf": nil, + ".avchd": nil, + ".avi": nil, + ".drc": nil, + ".flv": nil, + ".m2ts": nil, + ".m2v": nil, + ".m4p": nil, + ".m4v": nil, + ".mkv": nil, + ".mng": nil, + ".mov": nil, + ".mp2": nil, + ".mp4": nil, + ".mpe": nil, + ".mpeg": nil, + ".mpg": nil, + ".mpv": nil, + ".mts": nil, + ".mxf": nil, + ".nsv": nil, + ".ogg": nil, + ".ogv": nil, + ".qt": nil, + ".rm": nil, + ".rmvb": nil, + ".roq": nil, + ".svi": nil, + ".ts": nil, + ".vob": nil, + ".webm": nil, + ".wmv": nil, + ".yuv": nil, +} + +var extAudio = map[string]interface{}{ + ".aac": nil, + ".aiff": nil, + ".ape": nil, + ".au": nil, + ".dff": nil, + ".dsd": nil, + ".dsf": nil, + ".flac": nil, + ".gsm": nil, + ".it": nil, + ".m3u": nil, + ".m4a": nil, + ".mid": nil, + ".mod": nil, + ".mp3": nil, + ".mpa": nil, + ".mpga": nil, + ".oga": nil, + ".ogg": nil, + ".opus": nil, + ".pls": nil, + ".ra": nil, + ".s3m": nil, + ".sid": nil, + ".spx": nil, + ".wav": nil, + ".wma": nil, + ".xm": nil, +} + +func GetMimeType(filename string) string { + ext := strings.ToLower(filepath.Ext(filename)) + if _, ok := extVideo[ext]; ok { + return "video/*" + } + if _, ok := extAudio[ext]; ok { + return "audio/*" + } + return "*/*" +} + +func GetPlayableFiles(st state.TorrentStatus) []*state.TorrentFileStat { + files := make([]*state.TorrentFileStat, 0) + for _, f := range st.FileStats { + if GetMimeType(f.Path) != "*/*" { + files = append(files, f) + } + } + return files +} diff --git a/go/torrserver/utils/location.go b/go/torrserver/utils/location.go new file mode 100644 index 0000000000..24f600e5e6 --- /dev/null +++ b/go/torrserver/utils/location.go @@ -0,0 +1,14 @@ +package utils + +import ( + "github.com/gin-contrib/location" + "github.com/gin-gonic/gin" +) + +func GetScheme(c *gin.Context) string { + url := location.Get(c) + if url == nil { + return "http" + } + return url.Scheme +} diff --git a/go/torrserver/utils/prallel.go b/go/torrserver/utils/prallel.go new file mode 100644 index 0000000000..bba43114df --- /dev/null +++ b/go/torrserver/utils/prallel.go @@ -0,0 +1,17 @@ +package utils + +import ( + "sync" +) + +func ParallelFor(begin, end int, fn func(i int)) { + var wg sync.WaitGroup + wg.Add(end - begin) + for i := begin; i < end; i++ { + go func(i int) { + fn(i) + wg.Done() + }(i) + } + wg.Wait() +} diff --git a/go/torrserver/utils/strings.go b/go/torrserver/utils/strings.go new file mode 100644 index 0000000000..066052068c --- /dev/null +++ b/go/torrserver/utils/strings.go @@ -0,0 +1,48 @@ +package utils + +import ( + "fmt" + "strconv" +) + +const ( + _ = 1.0 << (10 * iota) // ignore first value by assigning to blank identifier + KB + MB + GB + TB + PB + EB +) + +func Format(b float64) string { + multiple := "" + value := b + + switch { + case b >= EB: + value /= EB + multiple = "EB" + case b >= PB: + value /= PB + multiple = "PB" + case b >= TB: + value /= TB + multiple = "TB" + case b >= GB: + value /= GB + multiple = "GB" + case b >= MB: + value /= MB + multiple = "MB" + case b >= KB: + value /= KB + multiple = "KB" + case b == 0: + return "0" + default: + return strconv.FormatInt(int64(b), 10) + "B" + } + + return fmt.Sprintf("%.2f%s", value, multiple) +} diff --git a/go/torrserver/version/version.go b/go/torrserver/version/version.go new file mode 100644 index 0000000000..88c040dced --- /dev/null +++ b/go/torrserver/version/version.go @@ -0,0 +1,27 @@ +package version + +import ( + "log" + "runtime/debug" + // "github.com/anacrolix/torrent" +) + +const Version = "Kuukiyomi torrserver" + +func GetTorrentVersion() string { + bi, ok := debug.ReadBuildInfo() + if !ok { + log.Printf("Failed to read build info") + return "" + } + for _, dep := range bi.Deps { + if dep.Path == "github.com/anacrolix/torrent" { + if dep.Replace != nil { + return dep.Replace.Version + } else { + return dep.Version + } + } + } + return "" +} diff --git a/go/torrserver/web/api/cache.go b/go/torrserver/web/api/cache.go new file mode 100644 index 0000000000..021ad4b954 --- /dev/null +++ b/go/torrserver/web/api/cache.go @@ -0,0 +1,63 @@ +package api + +import ( + "net/http" + + "server/torr" + + "github.com/gin-gonic/gin" + "github.com/pkg/errors" +) + +// Action: get +type cacheReqJS struct { + requestI + Hash string `json:"hash,omitempty"` +} + +// cache godoc +// +// @Summary Return cache stats +// @Description Return cache stats. +// +// @Tags API +// +// @Param request body cacheReqJS true "Cache stats request" +// +// @Produce json +// @Success 200 {object} state.CacheState "Cache stats" +// @Router /cache [post] +func cache(c *gin.Context) { + var req cacheReqJS + err := c.ShouldBindJSON(&req) + if err != nil { + c.AbortWithError(http.StatusBadRequest, err) + return + } + c.Status(http.StatusBadRequest) + switch req.Action { + case "get": + { + getCache(req, c) + } + } +} + +func getCache(req cacheReqJS, c *gin.Context) { + if req.Hash == "" { + c.AbortWithError(http.StatusBadRequest, errors.New("hash is empty")) + return + } + tor := torr.GetTorrent(req.Hash) + + if tor != nil { + st := tor.CacheState() + if st == nil { + c.JSON(200, struct{}{}) + } else { + c.JSON(200, st) + } + } else { + c.Status(http.StatusNotFound) + } +} diff --git a/go/torrserver/web/api/download.go b/go/torrserver/web/api/download.go new file mode 100644 index 0000000000..961064346d --- /dev/null +++ b/go/torrserver/web/api/download.go @@ -0,0 +1,64 @@ +package api + +import ( + "fmt" + "io" + "net/http" + "strconv" + "time" + + "github.com/gin-gonic/gin" +) + +type fileReader struct { + pos int64 + size int64 + io.ReadSeeker +} + +func newFR(size int64) *fileReader { + return &fileReader{ + pos: 0, + size: size, + } +} + +func (f *fileReader) Read(p []byte) (n int, err error) { + f.pos = f.pos + int64(len(p)) + return len(p), nil +} + +func (f *fileReader) Seek(offset int64, whence int) (int64, error) { + switch whence { + case 0: + f.pos = offset + case 1: + f.pos += offset + case 2: + f.pos = f.size + offset + } + return f.pos, nil +} + +// download godoc +// +// @Summary Generates test file of given size +// @Description Download the test file of given size (for speed testing purpose). +// +// @Tags API +// +// @Param size path string true "Test file size" +// +// @Produce application/octet-stream +// @Success 200 {file} file +// @Router /download/{size} [get] +func download(c *gin.Context) { + szStr := c.Param("size") + sz, err := strconv.Atoi(szStr) + if err != nil { + c.Error(err) + return + } + + http.ServeContent(c.Writer, c.Request, fmt.Sprintln(szStr)+"mb.bin", time.Now(), newFR(int64(sz*1024*1024))) +} diff --git a/go/torrserver/web/api/m3u.go b/go/torrserver/web/api/m3u.go new file mode 100644 index 0000000000..726ad83aa0 --- /dev/null +++ b/go/torrserver/web/api/m3u.go @@ -0,0 +1,182 @@ +package api + +import ( + "bytes" + "encoding/hex" + "fmt" + "net/http" + "net/url" + "path/filepath" + "sort" + "strings" + "time" + + "github.com/anacrolix/missinggo/v2/httptoo" + + sets "server/settings" + "server/torr" + "server/torr/state" + "server/utils" + + "github.com/gin-gonic/gin" + "github.com/pkg/errors" +) + +// allPlayList godoc +// +// @Summary Get a M3U playlist with all torrents +// @Description Retrieve all torrents and generates a bundled M3U playlist. +// +// @Tags API +// +// @Produce audio/x-mpegurl +// @Success 200 {file} file +// @Router /playlistall/all.m3u [get] +func allPlayList(c *gin.Context) { + torrs := torr.ListTorrent() + + host := utils.GetScheme(c) + "://" + c.Request.Host + list := "#EXTM3U\n" + hash := "" + // fn=file.m3u fix forkplayer bug with end .m3u in link + for _, tr := range torrs { + list += "#EXTINF:0 type=\"playlist\"," + tr.Title + "\n" + list += host + "/stream/" + url.PathEscape(tr.Title) + ".m3u?link=" + tr.TorrentSpec.InfoHash.HexString() + "&m3u&fn=file.m3u\n" + hash += tr.Hash().HexString() + } + + sendM3U(c, "all.m3u", hash, list) +} + +// playList godoc +// +// @Summary Get HTTP link of torrent in M3U list +// @Description Get HTTP link of torrent in M3U list. +// +// @Tags API +// +// @Param hash query string true "Torrent hash" +// @Param fromlast query bool false "From last play file" +// +// @Produce audio/x-mpegurl +// @Success 200 {file} file +// @Router /playlist [get] +func playList(c *gin.Context) { + hash, _ := c.GetQuery("hash") + _, fromlast := c.GetQuery("fromlast") + if hash == "" { + c.AbortWithError(http.StatusBadRequest, errors.New("hash is empty")) + return + } + + tor := torr.GetTorrent(hash) + if tor == nil { + c.AbortWithStatus(http.StatusNotFound) + return + } + + if tor.Stat == state.TorrentInDB { + tor = torr.LoadTorrent(tor) + if tor == nil { + c.AbortWithError(http.StatusInternalServerError, errors.New("error get torrent info")) + return + } + } + + host := utils.GetScheme(c) + "://" + c.Request.Host + list := getM3uList(tor.Status(), host, fromlast) + list = "#EXTM3U\n" + list + name := strings.ReplaceAll(c.Param("fname"), `/`, "") // strip starting / from param + if name == "" { + name = tor.Name() + ".m3u" + } else if !strings.HasSuffix(strings.ToLower(name), ".m3u") && !strings.HasSuffix(strings.ToLower(name), ".m3u8") { + name += ".m3u" + } + + sendM3U(c, name, tor.Hash().HexString(), list) +} + +func sendM3U(c *gin.Context, name, hash string, m3u string) { + c.Header("Content-Type", "audio/x-mpegurl") + c.Header("Connection", "close") + if hash != "" { + etag := hex.EncodeToString([]byte(fmt.Sprintf("%s/%s", hash, name))) + c.Header("ETag", httptoo.EncodeQuotedString(etag)) + } + if name == "" { + name = "playlist.m3u" + } + c.Header("Content-Disposition", `attachment; filename="`+name+`"`) + http.ServeContent(c.Writer, c.Request, name, time.Now(), bytes.NewReader([]byte(m3u))) +} + +func getM3uList(tor *state.TorrentStatus, host string, fromLast bool) string { + m3u := "" + from := 0 + if fromLast { + pos := searchLastPlayed(tor) + if pos != -1 { + from = pos + } + } + for i, f := range tor.FileStats { + if i >= from { + if utils.GetMimeType(f.Path) != "*/*" { + fn := filepath.Base(f.Path) + if fn == "" { + fn = f.Path + } + m3u += "#EXTINF:0," + fn + "\n" + fileNamesakes := findFileNamesakes(tor.FileStats, f) // find external media with same name (audio/subtiles tracks) + if fileNamesakes != nil { + m3u += "#EXTVLCOPT:input-slave=" // include VLC option for external media + for _, namesake := range fileNamesakes { // include play-links to external media, with # splitter + sname := filepath.Base(namesake.Path) + m3u += host + "/stream/" + url.PathEscape(sname) + "?link=" + tor.Hash + "&index=" + fmt.Sprint(namesake.Id) + "&play#" + } + m3u += "\n" + } + name := filepath.Base(f.Path) + m3u += host + "/stream/" + url.PathEscape(name) + "?link=" + tor.Hash + "&index=" + fmt.Sprint(f.Id) + "&play\n" + } + } + } + return m3u +} + +func findFileNamesakes(files []*state.TorrentFileStat, file *state.TorrentFileStat) []*state.TorrentFileStat { + // find files with the same name in torrent + name := filepath.Base(strings.TrimSuffix(file.Path, filepath.Ext(file.Path))) + var namesakes []*state.TorrentFileStat + for _, f := range files { + if strings.Contains(f.Path, name) { // external tracks always include name of videofile + if f != file { // exclude itself + namesakes = append(namesakes, f) + } + } + } + return namesakes +} + +func searchLastPlayed(tor *state.TorrentStatus) int { + viewed := sets.ListViewed(tor.Hash) + if len(viewed) == 0 { + return -1 + } + sort.Slice(viewed, func(i, j int) bool { + return viewed[i].FileIndex > viewed[j].FileIndex + }) + + lastViewedIndex := viewed[0].FileIndex + + for i, stat := range tor.FileStats { + if stat.Id == lastViewedIndex { + if i >= len(tor.FileStats) { + return -1 + } + return i + } + } + + return -1 +} diff --git a/go/torrserver/web/api/play.go b/go/torrserver/web/api/play.go new file mode 100644 index 0000000000..5a573dc10a --- /dev/null +++ b/go/torrserver/web/api/play.go @@ -0,0 +1,86 @@ +package api + +import ( + "errors" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + + "server/torr" + "server/torr/state" + "server/web/api/utils" +) + +// play godoc +// +// @Summary Play given torrent referenced by hash +// @Description Play given torrent referenced by hash. +// +// @Tags API +// +// @Param hash query string true "Torrent hash" +// @Param id query string true "File index in torrent" +// @Param not_auth query bool false "Not authenticated" +// +// @Produce application/octet-stream +// @Success 200 "Torrent data" +// @Router /play [get] +func play(c *gin.Context) { + hash := c.Param("hash") + indexStr := c.Param("id") + notAuth := c.GetBool("not_auth") + + if hash == "" || indexStr == "" { + c.AbortWithError(http.StatusNotFound, errors.New("link should not be empty")) + return + } + + spec, err := utils.ParseLink(hash) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } + + tor := torr.GetTorrent(spec.InfoHash.HexString()) + if tor == nil && notAuth { + c.Header("WWW-Authenticate", "Basic realm=Authorization Required") + c.AbortWithStatus(http.StatusUnauthorized) + return + } + + if tor == nil { + c.AbortWithError(http.StatusInternalServerError, errors.New("error get torrent")) + return + } + + if tor.Stat == state.TorrentInDB { + tor, err = torr.AddTorrent(spec, tor.Title, tor.Poster, tor.Data) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } + } + + if !tor.GotInfo() { + c.AbortWithError(http.StatusInternalServerError, errors.New("timeout connection torrent")) + return + } + + // find file + index := -1 + if len(tor.Files()) == 1 { + index = 1 + } else { + ind, err := strconv.Atoi(indexStr) + if err == nil { + index = ind + } + } + if index == -1 { // if file index not set and play file exec + c.AbortWithError(http.StatusBadRequest, errors.New("\"index\" is wrong")) + return + } + + tor.Stream(index, c.Request, c.Writer) +} diff --git a/go/torrserver/web/api/route.go b/go/torrserver/web/api/route.go new file mode 100644 index 0000000000..bef65eee78 --- /dev/null +++ b/go/torrserver/web/api/route.go @@ -0,0 +1,37 @@ +package api + +import ( + "github.com/gin-gonic/gin" +) + +type requestI struct { + Action string `json:"action,omitempty"` +} + +func SetupRoute(route *gin.RouterGroup) { + route.GET("/shutdown", shutdown) + + route.POST("/settings", settings) + + route.POST("/torrents", torrents) + route.POST("/torrent/upload", torrentUpload) + + route.POST("/cache", cache) + + route.HEAD("/stream", stream) + route.HEAD("/stream/*fname", stream) + + route.GET("/stream", stream) + route.GET("/stream/*fname", stream) + + route.HEAD("/play/:hash/:id", play) + route.GET("/play/:hash/:id", play) + + route.POST("/viewed", viewed) + + route.GET("/playlistall/all.m3u", allPlayList) + route.GET("/playlist", playList) + route.GET("/playlist/*fname", playList) // Is this endpoint still needed ? `fname` is never used in handler + + route.GET("/download/:size", download) +} diff --git a/go/torrserver/web/api/settings.go b/go/torrserver/web/api/settings.go new file mode 100644 index 0000000000..9bfeede81f --- /dev/null +++ b/go/torrserver/web/api/settings.go @@ -0,0 +1,53 @@ +package api + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/pkg/errors" + + sets "server/settings" + "server/torr" +) + +// Action: get, set, def +type setsReqJS struct { + requestI + Sets *sets.BTSets `json:"sets,omitempty"` +} + +// settings godoc +// +// @Summary Get / Set server settings +// @Description Allow to get or set server settings. +// +// @Tags API +// +// @Param request body setsReqJS true "Settings request" +// +// @Accept json +// @Produce json +// @Success 200 {object} sets.BTSets "Depends on what action has been asked" +// @Router /settings [post] +func settings(c *gin.Context) { + var req setsReqJS + err := c.ShouldBindJSON(&req) + if err != nil { + c.AbortWithError(http.StatusBadRequest, err) + return + } + + if req.Action == "get" { + c.JSON(200, sets.BTsets) + return + } else if req.Action == "set" { + torr.SetSettings(req.Sets) + c.Status(200) + return + } else if req.Action == "def" { + torr.SetDefSettings() + c.Status(200) + return + } + c.AbortWithError(http.StatusBadRequest, errors.New("action is empty")) +} diff --git a/go/torrserver/web/api/shutdown.go b/go/torrserver/web/api/shutdown.go new file mode 100644 index 0000000000..6c3837ffb1 --- /dev/null +++ b/go/torrserver/web/api/shutdown.go @@ -0,0 +1,31 @@ +package api + +import ( + "net/http" + "time" + + sets "server/settings" + "server/torr" + + "github.com/gin-gonic/gin" +) + +// shutdown godoc +// @Summary Shuts down server +// @Description Gracefully shuts down server after 1 second. +// +// @Tags API +// +// @Success 200 +// @Router /shutdown [get] +func shutdown(c *gin.Context) { + if sets.ReadOnly { + c.Status(http.StatusForbidden) + return + } + c.Status(200) + go func() { + time.Sleep(1000) + torr.Shutdown() + }() +} diff --git a/go/torrserver/web/api/stream.go b/go/torrserver/web/api/stream.go new file mode 100644 index 0000000000..a7082e1b36 --- /dev/null +++ b/go/torrserver/web/api/stream.go @@ -0,0 +1,247 @@ +package api + +import ( + "net/http" + "net/url" + "strconv" + "strings" + + "server/torr" + "server/torr/state" + utils2 "server/utils" + "server/web/api/utils" + + "github.com/gin-gonic/gin" + "github.com/pkg/errors" +) + +// get stat +// http://127.0.0.1:8090/stream/fname?link=...&stat +// get m3u +// http://127.0.0.1:8090/stream/fname?link=...&index=1&m3u +// http://127.0.0.1:8090/stream/fname?link=...&index=1&m3u&fromlast +// stream torrent +// http://127.0.0.1:8090/stream/fname?link=...&index=1&play +// http://127.0.0.1:8090/stream/fname?link=...&index=1&play&preload +// http://127.0.0.1:8090/stream/fname?link=...&index=1&play&save +// http://127.0.0.1:8090/stream/fname?link=...&index=1&play&save&title=...&poster=... +// only save +// http://127.0.0.1:8090/stream/fname?link=...&save&title=...&poster=... + +// stream godoc +// +// @Summary Multi usage endpoint +// @Description Multi usage endpoint. +// +// @Tags API +// +// @Param link query string true "Magnet/hash/link to torrent" +// @Param index query string false "File index in torrent" +// @Param preload query string false "Should preload torrent" +// @Param stat query string false "Get statistics from torrent" +// @Param save query string false "Should save torrent" +// @Param m3u query string false "Get torrent as M3U playlist" +// @Param fromlast query string false "Get m3u from last play" +// @Param play query string false "Start stream torrent" +// @Param title query string true "Set title of torrent" +// @Param poster query string true "File index in torrent" +// @Param not_auth query string true "Set poster link of torrent" +// +// @Produce application/octet-stream +// @Success 200 "Data returned according to query" +// @Router /stream [get] +func stream(c *gin.Context) { + link := c.Query("link") + indexStr := c.Query("index") + _, preload := c.GetQuery("preload") + _, stat := c.GetQuery("stat") + _, save := c.GetQuery("save") + _, m3u := c.GetQuery("m3u") + _, fromlast := c.GetQuery("fromlast") + _, play := c.GetQuery("play") + title := c.Query("title") + poster := c.Query("poster") + data := "" + notAuth := c.GetBool("not_auth") + + if notAuth && (play || m3u) { + streamNoAuth(c) + return + } + if notAuth { + c.Header("WWW-Authenticate", "Basic realm=Authorization Required") + c.AbortWithStatus(http.StatusUnauthorized) + return + } + + if link == "" { + c.AbortWithError(http.StatusBadRequest, errors.New("link should not be empty")) + return + } + + title, _ = url.QueryUnescape(title) + link, _ = url.QueryUnescape(link) + poster, _ = url.QueryUnescape(poster) + + spec, err := utils.ParseLink(link) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } + + tor := torr.GetTorrent(spec.InfoHash.HexString()) + if tor != nil { + title = tor.Title + poster = tor.Poster + data = tor.Data + } + if tor == nil || tor.Stat == state.TorrentInDB { + tor, err = torr.AddTorrent(spec, title, poster, data) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } + } + + if !tor.GotInfo() { + c.AbortWithError(http.StatusInternalServerError, errors.New("timeout connection torrent")) + return + } + + if tor.Title == "" { + tor.Title = tor.Name() + } + + // save to db + if save { + torr.SaveTorrentToDB(tor) + c.Status(200) // only set status, not return + } + + // find file + index := -1 + if len(tor.Files()) == 1 { + index = 0 + } else { + ind, err := strconv.Atoi(indexStr) + if err == nil { + index = ind + } + } + if index == -1 && play { // if file index not set and play file exec + index = 0 + } + // preload torrent + if preload { + torr.Preload(tor, index) + } + // return stat if query + if stat { + c.JSON(200, tor.Status()) + return + } else + // return m3u if query + if m3u { + name := strings.ReplaceAll(c.Param("fname"), `/`, "") // strip starting / from param + if name == "" { + name = tor.Name() + ".m3u" + } else if !strings.HasSuffix(strings.ToLower(name), ".m3u") && !strings.HasSuffix(strings.ToLower(name), ".m3u8") { + name += ".m3u" + } + m3ulist := "#EXTM3U\n" + getM3uList(tor.Status(), utils2.GetScheme(c)+"://"+c.Request.Host, fromlast) + sendM3U(c, name, tor.Hash().HexString(), m3ulist) + return + } else + // return play if query + if play { + tor.Stream(index, c.Request, c.Writer) + return + } +} + +func streamNoAuth(c *gin.Context) { + link := c.Query("link") + indexStr := c.Query("index") + _, preload := c.GetQuery("preload") + _, m3u := c.GetQuery("m3u") + _, fromlast := c.GetQuery("fromlast") + _, play := c.GetQuery("play") + title := c.Query("title") + poster := c.Query("poster") + data := "" + + if link == "" { + c.AbortWithError(http.StatusBadRequest, errors.New("link should not be empty")) + return + } + + link, _ = url.QueryUnescape(link) + + spec, err := utils.ParseLink(link) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } + + tor := torr.GetTorrent(spec.InfoHash.HexString()) + if tor == nil { + c.Header("WWW-Authenticate", "Basic realm=Authorization Required") + c.AbortWithStatus(http.StatusUnauthorized) + return + } + + title = tor.Title + poster = tor.Poster + data = tor.Data + + if tor.Stat == state.TorrentInDB { + tor, err = torr.AddTorrent(spec, title, poster, data) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } + } + + if !tor.GotInfo() { + c.AbortWithError(http.StatusInternalServerError, errors.New("timeout connection torrent")) + return + } + + // find file + index := -1 + if len(tor.Files()) == 1 { + index = 0 + } else { + ind, err := strconv.Atoi(indexStr) + if err == nil { + index = ind + } + } + if index == -1 && play { // if file index not set and play file exec + index = 0 + } + // preload torrent + if preload { + torr.Preload(tor, index) + } + + // return m3u if query + if m3u { + name := strings.ReplaceAll(c.Param("fname"), `/`, "") // strip starting / from param + if name == "" { + name = tor.Name() + ".m3u" + } else if !strings.HasSuffix(strings.ToLower(name), ".m3u") && !strings.HasSuffix(strings.ToLower(name), ".m3u8") { + name += ".m3u" + } + m3ulist := "#EXTM3U\n" + getM3uList(tor.Status(), utils2.GetScheme(c)+"://"+c.Request.Host, fromlast) + sendM3U(c, name, tor.Hash().HexString(), m3ulist) + return + } else + // return play if query + if play { + tor.Stream(index, c.Request, c.Writer) + return + } + c.Header("WWW-Authenticate", "Basic realm=Authorization Required") + c.AbortWithStatus(http.StatusUnauthorized) +} diff --git a/go/torrserver/web/api/torrents.go b/go/torrserver/web/api/torrents.go new file mode 100644 index 0000000000..8ad08d0189 --- /dev/null +++ b/go/torrserver/web/api/torrents.go @@ -0,0 +1,179 @@ +package api + +import ( + "net/http" + "strings" + "time" + + "server/log" + "server/torr" + "server/torr/state" + "server/web/api/utils" + + "github.com/gin-gonic/gin" + "github.com/pkg/errors" +) + +// Action: add, get, set, rem, list, drop +type torrReqJS struct { + requestI + Link string `json:"link,omitempty"` + Hash string `json:"hash,omitempty"` + Title string `json:"title,omitempty"` + Poster string `json:"poster,omitempty"` + Data string `json:"data,omitempty"` + SaveToDB bool `json:"save_to_db,omitempty"` +} + +// torrents godoc +// +// @Summary Handle torrents informations +// @Description Allow to add, get or set torrents to server. The action depends of what has been asked. +// +// @Tags API +// +// @Param request body torrReqJS true "Torrent request" +// +// @Accept json +// @Produce json +// @Success 200 +// @Router /torrents [post] +func torrents(c *gin.Context) { + var req torrReqJS + err := c.ShouldBindJSON(&req) + if err != nil { + c.AbortWithError(http.StatusBadRequest, err) + return + } + c.Status(http.StatusBadRequest) + switch req.Action { + case "add": + { + addTorrent(req, c) + } + case "get": + { + getTorrent(req, c) + } + case "set": + { + setTorrent(req, c) + } + case "rem": + { + remTorrent(req, c) + } + case "list": + { + listTorrent(req, c) + } + case "drop": + { + dropTorrent(req, c) + } + } +} + +func addTorrent(req torrReqJS, c *gin.Context) { + if req.Link == "" { + c.AbortWithError(http.StatusBadRequest, errors.New("link is empty")) + return + } + + log.TLogln("add torrent", req.Link) + req.Link = strings.ReplaceAll(req.Link, "&", "&") + torrSpec, err := utils.ParseLink(req.Link) + if err != nil { + log.TLogln("error parse link:", err) + c.AbortWithError(http.StatusBadRequest, err) + return + } + + tor, err := torr.AddTorrent(torrSpec, req.Title, req.Poster, req.Data) + if err != nil { + log.TLogln("error add torrent:", err) + c.AbortWithError(http.StatusInternalServerError, err) + return + } + + go func() { + if !tor.GotInfo() { + log.TLogln("error add torrent:", "timeout connection get torrent info") + return + } + + if tor.Title == "" { + tor.Title = torrSpec.DisplayName // prefer dn over name + tor.Title = strings.ReplaceAll(tor.Title, "_", " ") + tor.Title = strings.Trim(tor.Title, " ") + if tor.Title == "" { + tor.Title = tor.Name() + } + } + + if req.SaveToDB { + torr.SaveTorrentToDB(tor) + } + }() + for tor.Status().FileStats == nil { + // wait for file stats to be set + time.Sleep(100 * time.Millisecond) + } + + c.JSON(200, tor.Status()) +} + +func getTorrent(req torrReqJS, c *gin.Context) { + if req.Hash == "" { + c.AbortWithError(http.StatusBadRequest, errors.New("hash is empty")) + return + } + tor := torr.GetTorrent(req.Hash) + + if tor != nil { + st := tor.Status() + c.JSON(200, st) + } else { + c.Status(http.StatusNotFound) + } +} + +func setTorrent(req torrReqJS, c *gin.Context) { + if req.Hash == "" { + c.AbortWithError(http.StatusBadRequest, errors.New("hash is empty")) + return + } + torr.SetTorrent(req.Hash, req.Title, req.Poster, req.Data) + c.Status(200) +} + +func remTorrent(req torrReqJS, c *gin.Context) { + if req.Hash == "" { + c.AbortWithError(http.StatusBadRequest, errors.New("hash is empty")) + return + } + torr.RemTorrent(req.Hash) + c.Status(200) +} + +func listTorrent(req torrReqJS, c *gin.Context) { + list := torr.ListTorrent() + if len(list) == 0 { + c.JSON(200, []*state.TorrentStatus{}) + return + } + var stats []*state.TorrentStatus + for _, tr := range list { + stats = append(stats, tr.Status()) + } + c.JSON(200, stats) +} + +func dropTorrent(req torrReqJS, c *gin.Context) { + if req.Hash == "" { + c.AbortWithError(http.StatusBadRequest, errors.New("hash is empty")) + return + } + torr.DropTorrent(req.Hash) + c.Status(200) +} diff --git a/go/torrserver/web/api/upload.go b/go/torrserver/web/api/upload.go new file mode 100644 index 0000000000..e0352ca2c9 --- /dev/null +++ b/go/torrserver/web/api/upload.go @@ -0,0 +1,93 @@ +package api + +import ( + "net/http" + + "server/log" + "server/torr" + "server/web/api/utils" + + "github.com/gin-gonic/gin" +) + +// torrentUpload godoc +// +// @Summary Only one file support +// @Description Only one file support. +// +// @Tags API +// +// @Param file formData file true "Torrent file to insert" +// @Param save formData string false "Save to DB" +// @Param title formData string false "Torrent title" +// @Param poster formData string false "Torrent poster" +// @Param data formData string false "Torrent data" +// +// @Accept multipart/form-data +// +// @Produce json +// @Success 200 {object} state.TorrentStatus "Torrent status" +// @Router /torrent/upload [post] +func torrentUpload(c *gin.Context) { + form, err := c.MultipartForm() + if err != nil { + c.AbortWithError(http.StatusBadRequest, err) + return + } + defer form.RemoveAll() + + save := len(form.Value["save"]) > 0 + title := "" + if len(form.Value["title"]) > 0 { + title = form.Value["title"][0] + } + poster := "" + if len(form.Value["poster"]) > 0 { + poster = form.Value["poster"][0] + } + data := "" + if len(form.Value["data"]) > 0 { + data = form.Value["data"][0] + } + var tor *torr.Torrent + for name, file := range form.File { + log.TLogln("add torrent file", name) + + torrFile, err := file[0].Open() + if err != nil { + log.TLogln("error upload torrent:", err) + continue + } + defer torrFile.Close() + + spec, err := utils.ParseFile(torrFile) + if err != nil { + log.TLogln("error upload torrent:", err) + continue + } + + tor, err = torr.AddTorrent(spec, title, poster, data) + if err != nil { + log.TLogln("error upload torrent:", err) + continue + } + + go func() { + if !tor.GotInfo() { + log.TLogln("error add torrent:", "timeout connection torrent") + return + } + + if tor.Title == "" { + tor.Title = tor.Name() + } + + if save { + torr.SaveTorrentToDB(tor) + } + }() + + break + } + c.JSON(200, tor.Status()) +} diff --git a/go/torrserver/web/api/utils/link.go b/go/torrserver/web/api/utils/link.go new file mode 100644 index 0000000000..8406ccd988 --- /dev/null +++ b/go/torrserver/web/api/utils/link.go @@ -0,0 +1,137 @@ +package utils + +import ( + "errors" + "fmt" + "mime/multipart" + "net/http" + "net/url" + "strings" + "time" + + "github.com/anacrolix/torrent" + "github.com/anacrolix/torrent/metainfo" +) + +func ParseFile(file multipart.File) (*torrent.TorrentSpec, error) { + minfo, err := metainfo.Load(file) + if err != nil { + return nil, err + } + info, err := minfo.UnmarshalInfo() + if err != nil { + return nil, err + } + + // mag := minfo.Magnet(info.Name, minfo.HashInfoBytes()) + mag := minfo.Magnet(nil, &info) + return &torrent.TorrentSpec{ + InfoBytes: minfo.InfoBytes, + Trackers: [][]string{mag.Trackers}, + DisplayName: info.Name, + InfoHash: minfo.HashInfoBytes(), + }, nil +} + +func ParseLink(link string) (*torrent.TorrentSpec, error) { + urlLink, err := url.Parse(link) + if err != nil { + return nil, err + } + + switch strings.ToLower(urlLink.Scheme) { + case "magnet": + return fromMagnet(urlLink.String()) + case "http", "https": + return fromHttp(urlLink.String()) + case "": + return fromMagnet("magnet:?xt=urn:btih:" + urlLink.Path) + case "file": + return fromFile(urlLink.Path) + default: + err = fmt.Errorf("unknown scheme:", urlLink, urlLink.Scheme) + } + return nil, err +} + +func fromMagnet(link string) (*torrent.TorrentSpec, error) { + mag, err := metainfo.ParseMagnetUri(link) + if err != nil { + return nil, err + } + + var trackers [][]string + if len(mag.Trackers) > 0 { + trackers = [][]string{mag.Trackers} + } + + return &torrent.TorrentSpec{ + InfoBytes: nil, + Trackers: trackers, + DisplayName: mag.DisplayName, + InfoHash: mag.InfoHash, + }, nil +} + +func fromHttp(link string) (*torrent.TorrentSpec, error) { + req, err := http.NewRequest("GET", link, nil) + if err != nil { + return nil, err + } + + client := new(http.Client) + client.Timeout = time.Duration(time.Second * 60) + req.Header.Set("User-Agent", "DWL/1.1.1 (Torrent)") + + resp, err := client.Do(req) + if er, ok := err.(*url.Error); ok { + if strings.HasPrefix(er.URL, "magnet:") { + return fromMagnet(er.URL) + } + } + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + return nil, errors.New(resp.Status) + } + + minfo, err := metainfo.Load(resp.Body) + if err != nil { + return nil, err + } + info, err := minfo.UnmarshalInfo() + if err != nil { + return nil, err + } + // mag := minfo.Magnet(info.Name, minfo.HashInfoBytes()) + mag := minfo.Magnet(nil, &info) + + return &torrent.TorrentSpec{ + InfoBytes: minfo.InfoBytes, + Trackers: [][]string{mag.Trackers}, + DisplayName: info.Name, + InfoHash: minfo.HashInfoBytes(), + }, nil +} + +func fromFile(path string) (*torrent.TorrentSpec, error) { + minfo, err := metainfo.LoadFromFile(path) + if err != nil { + return nil, err + } + info, err := minfo.UnmarshalInfo() + if err != nil { + return nil, err + } + + // mag := minfo.Magnet(info.Name, minfo.HashInfoBytes()) + mag := minfo.Magnet(nil, &info) + return &torrent.TorrentSpec{ + InfoBytes: minfo.InfoBytes, + Trackers: [][]string{mag.Trackers}, + DisplayName: info.Name, + InfoHash: minfo.HashInfoBytes(), + }, nil +} diff --git a/go/torrserver/web/api/viewed.go b/go/torrserver/web/api/viewed.go new file mode 100644 index 0000000000..fcacd5d34e --- /dev/null +++ b/go/torrserver/web/api/viewed.go @@ -0,0 +1,71 @@ +package api + +import ( + "net/http" + + sets "server/settings" + + "github.com/gin-gonic/gin" +) + +/* +file index starts from 1 +*/ + +// Action: set, rem, list +type viewedReqJS struct { + requestI + *sets.Viewed +} + +// viewed godoc +// +// @Summary Set / List / Remove viewed torrents +// @Description Allow to set, list or remove viewed torrents from server. +// +// @Tags API +// +// @Param request body viewedReqJS true "Viewed torrent request" +// +// @Accept json +// @Produce json +// @Success 200 {array} sets.Viewed +// @Router /viewed [post] +func viewed(c *gin.Context) { + var req viewedReqJS + err := c.ShouldBindJSON(&req) + if err != nil { + c.AbortWithError(http.StatusBadRequest, err) + return + } + + switch req.Action { + case "set": + { + setViewed(req, c) + } + case "rem": + { + remViewed(req, c) + } + case "list": + { + listViewed(req, c) + } + } +} + +func setViewed(req viewedReqJS, c *gin.Context) { + sets.SetViewed(req.Viewed) + c.Status(200) +} + +func remViewed(req viewedReqJS, c *gin.Context) { + sets.RemViewed(req.Viewed) + c.Status(200) +} + +func listViewed(req viewedReqJS, c *gin.Context) { + list := sets.ListViewed(req.Hash) + c.JSON(200, list) +} diff --git a/go/torrserver/web/server.go b/go/torrserver/web/server.go new file mode 100644 index 0000000000..3e60dfcfe3 --- /dev/null +++ b/go/torrserver/web/server.go @@ -0,0 +1,133 @@ +package web + +import ( + "net" + "net/http" + "os" + "sort" + + "github.com/gin-contrib/cors" + "github.com/gin-contrib/location" + "github.com/gin-gonic/gin" + + "server/settings" + + "server/log" + "server/torr" + "server/version" + "server/web/api" +) + +var ( + BTS = torr.NewBTS() + waitChan = make(chan error) + httpServer *http.Server +) + +// @title Swagger Torrserver API +// @version {version.Version} +// @description Torrent streaming server. + +// @license.name GPL 3.0 + +// @BasePath / + +// @securityDefinitions.basic BasicAuth + +// @externalDocs.description OpenAPI +// @externalDocs.url https://swagger.io/resources/open-api/ +func Start() { + log.TLogln("Start TorrServer " + version.Version + " torrent " + version.GetTorrentVersion()) + ips := getLocalIps() + if len(ips) > 0 { + log.TLogln("Local IPs:", ips) + } + err := BTS.Connect() + if err != nil { + log.TLogln("BTS.Connect() error!", err) // waitChan <- err + os.Exit(1) // return + } + + gin.SetMode(gin.ReleaseMode) + + // corsCfg := cors.DefaultConfig() + // corsCfg.AllowAllOrigins = true + // corsCfg.AllowHeaders = []string{"*"} + // corsCfg.AllowMethods = []string{"*"} + // corsCfg.AllowPrivateNetwork = true + corsCfg := cors.DefaultConfig() + corsCfg.AllowAllOrigins = true + corsCfg.AllowHeaders = []string{"Origin", "Content-Length", "Content-Type", "X-Requested-With", "Accept", "Authorization"} + + route := gin.New() + route.Use(log.WebLogger(), gin.Recovery(), cors.New(corsCfg), location.Default()) + + route.GET("/echo", echo) + + api.SetupRoute(&route.RouterGroup) + + httpServer = &http.Server{ + Addr: ":" + settings.Port, + Handler: route, + } + + go func() { + log.TLogln("Start http server at port", settings.Port) + httpServer.ListenAndServe() + //waitChan <- route.Run(" :" + settings.Port) + }() +} + +func Wait() error { + return <-waitChan +} + +func Stop() { + if httpServer != nil { + httpServer.Close() + } + BTS.Disconnect() + //waitChan <- nil +} + +// echo godoc +// +// @Summary Tests server status +// @Description Tests whether server is alive or not +// +// @Tags API +// +// @Produce plain +// @Success 200 {string} string "Server version" +// @Router /echo [get] +func echo(c *gin.Context) { + c.String(200, "%v", version.Version) +} + +func getLocalIps() []string { + ifaces, err := net.Interfaces() + if err != nil { + log.TLogln("Error get local IPs") + return nil + } + var list []string + for _, i := range ifaces { + addrs, _ := i.Addrs() + if i.Flags&net.FlagUp == net.FlagUp { + for _, addr := range addrs { + var ip net.IP + switch v := addr.(type) { + case *net.IPNet: + ip = v.IP + case *net.IPAddr: + ip = v.IP + } + if !ip.IsLoopback() && !ip.IsLinkLocalUnicast() && !ip.IsLinkLocalMulticast() { + list = append(list, ip.String()) + } + } + } + } + sort.Strings(list) + return list +} diff --git a/i18n/src/commonMain/resources/MR/base/strings.xml b/i18n/src/commonMain/resources/MR/base/strings.xml index 2e364c06d9..a00c8db7b1 100644 --- a/i18n/src/commonMain/resources/MR/base/strings.xml +++ b/i18n/src/commonMain/resources/MR/base/strings.xml @@ -1138,4 +1138,9 @@ Set to 0 to disable the speed limit. Enable MPV scripts Needs external storage permission. + Torrent Server is running + TorrentServer Preferences + TorrentServer Port + Torrent Trackers + Reset default torrent trackers string \ No newline at end of file From ad2b60c31edd2700d0d7adcf7e387326d91fa05f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Pe=C3=B1a=20y=20Lillo?= <80992641+Diegopyl1209@users.noreply.github.com> Date: Mon, 17 Jun 2024 21:07:49 -0400 Subject: [PATCH 02/14] Upload compiled libs --- app/libs/server-sources.jar | Bin 0 -> 5394 bytes app/libs/server.aar | Bin 0 -> 18110367 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 app/libs/server-sources.jar create mode 100644 app/libs/server.aar diff --git a/app/libs/server-sources.jar b/app/libs/server-sources.jar new file mode 100644 index 0000000000000000000000000000000000000000..b517e3e40457726c674a40c5fe0317bbb82b1b95 GIT binary patch literal 5394 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

;vx#MaJNdtJMj?Pi#*umXV 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

JW6{w0)#8KFO!s-?BP?rh%sR$6Wm)JWn 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­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