diff --git a/app/src/main/kotlin/com/hippo/ehviewer/client/EhCookieStore.kt b/app/src/main/kotlin/com/hippo/ehviewer/client/EhCookieStore.kt index a15331bb6..7d719e526 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/client/EhCookieStore.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/client/EhCookieStore.kt @@ -33,6 +33,8 @@ object EhCookieStore : CookiesStorage { ) } + fun getUserId() = getCookies(EhUrl.HOST_E)?.get(KEY_IPB_MEMBER_ID) + fun getIdentityCookies(): List> { val eCookies = getCookies(EhUrl.HOST_E) val exCookies = getCookies(EhUrl.HOST_EX) @@ -48,8 +50,6 @@ object EhCookieStore : CookiesStorage { fun flush() = manager.flush() - fun getCookieHeader(url: String): String? = manager.getCookie(url) - // See https://github.com/Ehviewer-Overhauled/Ehviewer/issues/873 override suspend fun addCookie(requestUrl: Url, cookie: Cookie) { if (cookie.name != KEY_UTMP_NAME) { diff --git a/app/src/main/kotlin/com/hippo/ehviewer/client/EhEngine.kt b/app/src/main/kotlin/com/hippo/ehviewer/client/EhEngine.kt index 4e84cfbf4..8cc940c91 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/client/EhEngine.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/client/EhEngine.kt @@ -180,6 +180,11 @@ object EhEngine { return ehRequest(url, referer).fetchUsingAsByteBuffer(TorrentParser::parse) } + suspend fun getTorrentKey() = EhCookieStore.getUserId()?.let { id -> + val key = ehRequest(EhUrl.URL_HOME).fetchUsingAsText(HomeParser::parseTorrentKey) + "${id}x$key" + } + suspend fun getArchiveList(url: String, gid: Long, token: String): ArchiveParser.Result { val funds = if (EhUtils.isExHentai) getFunds() else null return ehRequest(url, EhUrl.getGalleryDetailUrl(gid, token)) diff --git a/app/src/main/kotlin/com/hippo/ehviewer/client/EhUrl.kt b/app/src/main/kotlin/com/hippo/ehviewer/client/EhUrl.kt index 56a7a6f8f..419b68152 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/client/EhUrl.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/client/EhUrl.kt @@ -133,6 +133,11 @@ object EhUrl { }.buildString() fun getTagDefinitionUrl(tag: String) = "https://ehwiki.org/wiki/" + tag.replace(' ', '_') + + fun getTrackerUrl(gid: Long, key: String?) = ehUrl( + path = listOfNotNull("$gid", key, "announce"), + host = "ehtracker.org", + ).buildString() } inline fun ehUrl(path: String? = null, host: String = EhUrl.domain, builder: ParametersBuilder.() -> Unit) = ehUrl( diff --git a/app/src/main/kotlin/com/hippo/ehviewer/client/parser/HomeParser.kt b/app/src/main/kotlin/com/hippo/ehviewer/client/parser/HomeParser.kt index 92b1d9a65..048cc7785 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/client/parser/HomeParser.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/client/parser/HomeParser.kt @@ -8,7 +8,8 @@ import kotlinx.parcelize.Parcelize import kotlinx.serialization.Serializable object HomeParser { - private val PATTERN_FUNDS = Regex("Available: ([\\d,]+) Credits.*Available: ([\\d,]+) kGP", RegexOption.DOT_MATCHES_ALL) + private val TorrentKeyRegex = Regex("Your current key is: <[^>]*>([^<]*)<") + private val FundsRegex = Regex("Available: ([\\d,]+) Credits.*Available: ([\\d,]+) kGP", RegexOption.DOT_MATCHES_ALL) private const val INSUFFICIENT_FUNDS = "Insufficient funds." fun parse(body: ByteBuffer) = runCatching { @@ -21,8 +22,11 @@ object HomeParser { } } + fun parseTorrentKey(body: String) = TorrentKeyRegex.find(body)?.run { groupValues[1] } + ?: throw ParseException("Parse torrent key error") + fun parseFunds(body: String): Funds { - PATTERN_FUNDS.find(body)?.groupValues?.run { + FundsRegex.find(body)?.groupValues?.run { val fundsC = ParserUtils.parseInt(get(1), 0) val fundsGP = ParserUtils.parseInt(get(2), 0) * 1000 return Funds("%,d+".format(fundsGP), "%,d".format(fundsC)) diff --git a/app/src/main/kotlin/com/hippo/ehviewer/icons/filled/Magnet.kt b/app/src/main/kotlin/com/hippo/ehviewer/icons/filled/Magnet.kt new file mode 100644 index 000000000..06ea8ccb4 --- /dev/null +++ b/app/src/main/kotlin/com/hippo/ehviewer/icons/filled/Magnet.kt @@ -0,0 +1,42 @@ +package com.hippo.ehviewer.icons.filled + +import androidx.compose.foundation.Image +import androidx.compose.material.icons.materialIcon +import androidx.compose.material.icons.materialPath +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import com.hippo.ehviewer.icons.EhIcons +import com.hippo.ehviewer.util.unsafeLazy + +val EhIcons.Filled.Magnet by unsafeLazy { + materialIcon(name = "Magnet") { + materialPath { + moveTo(3.0F, 7.0F) + verticalLineTo(13.0F) + arcTo(9.0F, 9.0F, 0.0F, false, false, 12.0F, 22.0F) + arcTo(9.0F, 9.0F, 0.0F, false, false, 21.0F, 13.0F) + verticalLineTo(7.0F) + horizontalLineTo(17.0F) + verticalLineTo(13.0F) + arcTo(5.0F, 5.0F, 0.0F, false, true, 12.0F, 18.0F) + arcTo(5.0F, 5.0F, 0.0F, false, true, 7.0F, 13.0F) + verticalLineTo(7.0F) + moveTo(17.0F, 5.0F) + horizontalLineTo(21.0F) + verticalLineTo(2.0F) + horizontalLineTo(17.0F) + moveTo(3.0F, 5.0F) + horizontalLineTo(7.0F) + verticalLineTo(2.0F) + horizontalLineTo(3.0F) + close() + } + } +} + +@Preview +@Composable +@Suppress("UnusedPrivateMember") +private fun IconMagnetPreview() { + Image(imageVector = EhIcons.Default.Magnet, contentDescription = null) +} diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryDetailContent.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryDetailContent.kt index 36073878c..f1bf39810 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryDetailContent.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryDetailContent.kt @@ -1,8 +1,9 @@ package com.hippo.ehviewer.ui.screen import android.Manifest +import android.content.ActivityNotFoundException import android.content.Context -import android.os.Environment +import android.content.Intent import android.text.TextUtils.TruncateAt.END import androidx.annotation.StringRes import androidx.compose.animation.SharedTransitionScope @@ -29,7 +30,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ImageSearch import androidx.compose.material.icons.filled.Search -import androidx.compose.material.icons.filled.SwapVerticalCircle import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.FilledTonalButton @@ -68,10 +68,10 @@ import androidx.paging.compose.itemKey import androidx.window.core.layout.WindowWidthSizeClass import arrow.core.partially1 import arrow.fx.coroutines.parMap +import arrow.fx.coroutines.parZip import com.hippo.ehviewer.EhDB import com.hippo.ehviewer.R import com.hippo.ehviewer.Settings -import com.hippo.ehviewer.client.EhCookieStore import com.hippo.ehviewer.client.EhEngine import com.hippo.ehviewer.client.EhFilter.remember import com.hippo.ehviewer.client.EhUrl @@ -92,6 +92,8 @@ import com.hippo.ehviewer.dao.DownloadInfo import com.hippo.ehviewer.dao.Filter import com.hippo.ehviewer.dao.FilterMode import com.hippo.ehviewer.download.DownloadManager +import com.hippo.ehviewer.icons.EhIcons +import com.hippo.ehviewer.icons.filled.Magnet import com.hippo.ehviewer.ktbuilder.imageRequest import com.hippo.ehviewer.ktbuilder.launchIn import com.hippo.ehviewer.ui.GalleryInfoBottomSheet @@ -126,11 +128,10 @@ import com.hippo.ehviewer.ui.tools.getClippedRefreshKey import com.hippo.ehviewer.ui.tools.getLimit import com.hippo.ehviewer.ui.tools.getOffset import com.hippo.ehviewer.ui.tools.rememberInVM -import com.hippo.ehviewer.util.AppConfig import com.hippo.ehviewer.util.FavouriteStatusRouter -import com.hippo.ehviewer.util.FileUtils import com.hippo.ehviewer.util.addTextToClipboard import com.hippo.ehviewer.util.bgWork +import com.hippo.ehviewer.util.displayString import com.hippo.ehviewer.util.flattenForEach import com.hippo.ehviewer.util.isAtLeastQ import com.hippo.ehviewer.util.requestPermission @@ -140,6 +141,7 @@ import eu.kanade.tachiyomi.util.lang.launchUI import eu.kanade.tachiyomi.util.lang.withIOContext import eu.kanade.tachiyomi.util.lang.withUIContext import eu.kanade.tachiyomi.util.system.logcat +import io.ktor.http.encodeURLParameter import kotlin.math.roundToInt import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineStart @@ -148,7 +150,6 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.async import kotlinx.coroutines.launch import moe.tarsin.coroutines.runSuspendCatching -import splitties.systemservices.downloadManager context(CoroutineScope, DestinationsNavigator, DialogState, MainActivity, SnackbarHostState, SharedTransitionScope, TransitionsVisibilityScope) @Composable @@ -542,16 +543,18 @@ fun BelowHeader(galleryDetail: GalleryDetail) { ) val torrentText = stringResource(R.string.torrent_count, galleryDetail.torrentCount) val permissionDenied = stringResource(R.string.permission_denied) - val downloadTorrentFailed = stringResource(R.string.download_torrent_failure) - val downloadTorrentStarted = stringResource(R.string.download_torrent_started) val noTorrents = stringResource(R.string.no_torrents) val torrentResult = remember(galleryDetail) { async(Dispatchers.IO + Job(), CoroutineStart.LAZY) { - EhEngine.getTorrentList(galleryDetail.torrentUrl!!, galleryDetail.gid, galleryDetail.token) + parZip( + { EhEngine.getTorrentList(galleryDetail.torrentUrl!!, galleryDetail.gid, galleryDetail.token) }, + { EhEngine.getTorrentKey() }, + { list, key -> list to key }, + ) } } suspend fun showTorrentDialog() { - val torrentList = bgWork { torrentResult.await() } + val (torrentList, key) = bgWork { torrentResult.await() } if (torrentList.isEmpty()) { showSnackbar(noTorrents) } else { @@ -561,21 +564,20 @@ fun BelowHeader(galleryDetail: GalleryDetail) { onItemClick = { dismissWith(it) }, ) } - val url = selected.url - val name = "${selected.name}.torrent" - val r = android.app.DownloadManager.Request(url.toUri()) - r.setDestinationInExternalPublicDir( - Environment.DIRECTORY_DOWNLOADS, - AppConfig.APP_DIRNAME + "/" + FileUtils.sanitizeFilename(name), - ) - r.setNotificationVisibility(android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) - EhCookieStore.getCookieHeader(url)?.let { r.addRequestHeader("Cookie", it) } - downloadManager.enqueue(r) - showSnackbar(downloadTorrentStarted) + val hash = selected.url.dropLast(8).takeLast(40) + val name = selected.name.encodeURLParameter() + val tracker = EhUrl.getTrackerUrl(galleryDetail.gid, key).encodeURLParameter() + val link = "magnet:?xt=urn:btih:$hash&dn=$name&tr=$tracker" + val intent = Intent(Intent.ACTION_VIEW, link.toUri()) + try { + startActivity(intent) + } catch (_: ActivityNotFoundException) { + withUIContext { addTextToClipboard(link, true) } + } } } EhIconButton( - icon = Icons.Default.SwapVerticalCircle, + icon = EhIcons.Default.Magnet, text = torrentText, onClick = { launchIO { @@ -585,8 +587,7 @@ fun BelowHeader(galleryDetail: GalleryDetail) { runSuspendCatching { showTorrentDialog() }.onFailure { - logcat(it) - showSnackbar(downloadTorrentFailed) + showSnackbar(it.displayString()) } } else { showSnackbar(permissionDenied) diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index 6e17a0921..067b2f568 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -60,7 +60,6 @@ Stem opp Gråskala Del - Nedlasting av torrent startet Blokker etiketten «%s»\? Underbar Ugyldig nettadresse diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 3a80b13d7..f44d60887 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -126,8 +126,6 @@ 评分成功 评分失败 没有种子 - 开始下载种子 - 无法下载种子 种子 未收藏 添加收藏 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 0bc79b194..f19acddec 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -126,8 +126,6 @@ 評分成功 評分失敗 沒有種子 - 開始下載種子 - 無法下載種子 種子 未收藏 新增到收藏裡 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6ce05bc08..ca2bb8656 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -142,8 +142,6 @@ Rate successfully Rate failed No torrents - Torrent download started - Failed to download torrent Torrents Not favorited Add to favorites