Compare commits

...

1 Commits
dev ... 5.3.19

Author SHA1 Message Date
tom5079
db928a168f fix loading 2025-03-23 09:44:53 -07:00
5 changed files with 58 additions and 39 deletions

View File

@@ -19,7 +19,7 @@ android {
minSdk = 21 minSdk = 21
targetSdk = 35 targetSdk = 35
versionCode = 70 versionCode = 70
versionName = "5.3.18" versionName = "5.3.19"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
} }

View File

@@ -12,7 +12,7 @@
"filters": [], "filters": [],
"attributes": [], "attributes": [],
"versionCode": 70, "versionCode": 70,
"versionName": "5.3.18", "versionName": "5.3.19",
"outputFile": "app-release.apk" "outputFile": "app-release.apk"
} }
], ],

View File

@@ -16,12 +16,14 @@
package xyz.quaver.pupil.hitomi package xyz.quaver.pupil.hitomi
import kotlinx.coroutines.* import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.datetime.Clock.System.now import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.Call import okhttp3.Call
import okhttp3.Callback import okhttp3.Callback
@@ -30,9 +32,7 @@ import okhttp3.Response
import xyz.quaver.pupil.client import xyz.quaver.pupil.client
import java.io.IOException import java.io.IOException
import java.net.URL import java.net.URL
import java.util.concurrent.Executors
import kotlin.coroutines.resumeWithException import kotlin.coroutines.resumeWithException
import kotlin.time.Duration.Companion.minutes
import kotlin.time.ExperimentalTime import kotlin.time.ExperimentalTime
const val protocol = "https:" const val protocol = "https:"
@@ -40,25 +40,25 @@ const val protocol = "https:"
@Serializable @Serializable
data class Artist( data class Artist(
val artist: String, val artist: String,
val url: String val url: String,
) )
@Serializable @Serializable
data class Group( data class Group(
val group: String, val group: String,
val url: String val url: String,
) )
@Serializable @Serializable
data class Parody( data class Parody(
val parody: String, val parody: String,
val url: String val url: String,
) )
@Serializable @Serializable
data class Character( data class Character(
val character: String, val character: String,
val url: String val url: String,
) )
@Serializable @Serializable
@@ -66,7 +66,7 @@ data class Tag(
val tag: String, val tag: String,
val url: String, val url: String,
val female: String? = null, val female: String? = null,
val male: String? = null val male: String? = null,
) )
@Serializable @Serializable
@@ -74,7 +74,7 @@ data class Language(
val galleryid: String, val galleryid: String,
val url: String, val url: String,
val language_localname: String, val language_localname: String,
val name: String val name: String,
) )
@Serializable @Serializable
@@ -93,7 +93,7 @@ data class GalleryInfo(
val languages: List<Language> = emptyList(), val languages: List<Language> = emptyList(),
val characters: List<Character>? = null, val characters: List<Character>? = null,
val scene_indexes: List<Int>? = emptyList(), val scene_indexes: List<Int>? = emptyList(),
val files: List<GalleryFiles> = emptyList() val files: List<GalleryFiles> = emptyList(),
) )
val json = Json { val json = Json {
@@ -104,13 +104,16 @@ val json = Json {
} }
typealias HeaderSetter = (Request.Builder) -> Request.Builder typealias HeaderSetter = (Request.Builder) -> Request.Builder
fun URL.readText(settings: HeaderSetter? = null): String { fun URL.readText(settings: HeaderSetter? = null): String {
val request = Request.Builder() val request = Request.Builder()
.url(this).let { .url(this).let {
settings?.invoke(it) ?: it settings?.invoke(it) ?: it
}.build() }.build()
return client.newCall(request).execute().also{ if (it.code() != 200) throw IOException("CODE ${it.code()}") }.body()?.use { it.string() } ?: throw IOException() return client.newCall(request).execute()
.also { if (it.code() != 200) throw IOException("CODE ${it.code()}") }.body()
?.use { it.string() } ?: throw IOException()
} }
fun URL.readBytes(settings: HeaderSetter? = null): ByteArray { fun URL.readBytes(settings: HeaderSetter? = null): ByteArray {
@@ -119,7 +122,9 @@ fun URL.readBytes(settings: HeaderSetter? = null): ByteArray {
settings?.invoke(it) ?: it settings?.invoke(it) ?: it
}.build() }.build()
return client.newCall(request).execute().also { if (it.code() != 200) throw IOException("CODE ${it.code()}") }.body()?.use { it.bytes() } ?: throw IOException() return client.newCall(request).execute()
.also { if (it.code() != 200) throw IOException("CODE ${it.code()}") }.body()
?.use { it.bytes() } ?: throw IOException()
} }
@Suppress("EXPERIMENTAL_API_USAGE") @Suppress("EXPERIMENTAL_API_USAGE")
@@ -130,7 +135,7 @@ fun getGalleryInfo(galleryID: Int) =
) )
//common.js //common.js
const val domain = "ltn.hitomi.la" const val domain = "ltn.gold-usergeneratedcontent.net"
const val galleryblockextension = ".html" const val galleryblockextension = ".html"
const val galleryblockdir = "galleryblock" const val galleryblockdir = "galleryblock"
const val nozomiextension = ".nozomi" const val nozomiextension = ".nozomi"
@@ -152,9 +157,13 @@ object gg {
mutex.withLock { mutex.withLock {
if (lastRetrieval == null || (lastRetrieval!! + 60000) < System.currentTimeMillis()) { if (lastRetrieval == null || (lastRetrieval!! + 60000) < System.currentTimeMillis()) {
val ggjs: String = suspendCancellableCoroutine { continuation -> val ggjs: String = suspendCancellableCoroutine { continuation ->
val call = client.newCall(Request.Builder().url("https://ltn.hitomi.la/gg.js").build()) val call =
client.newCall(
Request.Builder().url("https://ltn.gold-usergeneratedcontent.net/gg.js")
.build()
)
call.enqueue(object: Callback { call.enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) { override fun onFailure(call: Call, e: IOException) {
if (continuation.isCancelled) return if (continuation.isCancelled) return
continuation.resumeWithException(e) continuation.resumeWithException(e)
@@ -202,13 +211,14 @@ object gg {
refresh() refresh()
return b return b
} }
fun s(h: String): String { fun s(h: String): String {
val m = Regex("(..)(.)$").find(h) val m = Regex("(..)(.)$").find(h)
return m!!.groupValues.let { it[2]+it[1] }.toInt(16).toString(10) return m!!.groupValues.let { it[2] + it[1] }.toInt(16).toString(10)
} }
} }
suspend fun subdomainFromURL(url: String, base: String? = null) : String { suspend fun subdomainFromURL(url: String, base: String? = null): String {
var retval = "b" var retval = "b"
if (!base.isNullOrBlank()) if (!base.isNullOrBlank())
@@ -219,46 +229,55 @@ suspend fun subdomainFromURL(url: String, base: String? = null) : String {
val r = Regex("""/[0-9a-f]{61}([0-9a-f]{2})([0-9a-f])""") val r = Regex("""/[0-9a-f]{61}([0-9a-f]{2})([0-9a-f])""")
val m = r.find(url) ?: return "a" val m = r.find(url) ?: return "a"
val g = m.groupValues.let { it[2]+it[1] }.toIntOrNull(b) val g = m.groupValues.let { it[2] + it[1] }.toIntOrNull(b)
if (g != null) { if (g != null) {
retval = (97+ gg.m(g)).toChar().toString() + retval retval = (97 + gg.m(g)).toChar().toString() + retval
} }
return retval return retval
} }
suspend fun urlFromUrl(url: String, base: String? = null) : String { suspend fun urlFromUrl(url: String, base: String? = null): String {
return url.replace(Regex("""//..?\.hitomi\.la/"""), "//${subdomainFromURL(url, base)}.hitomi.la/") return url.replace(
Regex("""//..?\.(?:gold-usergeneratedcontent\.net|hitomi\.la)/"""),
"//${subdomainFromURL(url, base)}.gold-usergeneratedcontent.net/"
)
} }
suspend fun fullPathFromHash(hash: String) : String = suspend fun fullPathFromHash(hash: String): String =
"${gg.b()}${gg.s(hash)}/$hash" "${gg.b()}${gg.s(hash)}/$hash"
fun realFullPathFromHash(hash: String): String = fun realFullPathFromHash(hash: String): String =
hash.replace(Regex("""^.*(..)(.)$"""), "$2/$1/$hash") hash.replace(Regex("""^.*(..)(.)$"""), "$2/$1/$hash")
suspend fun urlFromHash(galleryID: Int, image: GalleryFiles, dir: String? = null, ext: String? = null) : String { suspend fun urlFromHash(
galleryID: Int,
image: GalleryFiles,
dir: String? = null,
ext: String? = null,
): String {
val ext = ext ?: dir ?: image.name.takeLastWhile { it != '.' } val ext = ext ?: dir ?: image.name.takeLastWhile { it != '.' }
val dir = dir ?: "images" val dir = dir ?: "images"
return "https://a.hitomi.la/$dir/${fullPathFromHash(image.hash)}.$ext" return "https://a.gold-usergeneratedcontent.net/$dir/${fullPathFromHash(image.hash)}.$ext"
} }
suspend fun urlFromUrlFromHash(galleryID: Int, image: GalleryFiles, dir: String? = null, ext: String? = null, base: String? = null) = suspend fun urlFromUrlFromHash(
galleryID: Int,
image: GalleryFiles,
dir: String? = null,
ext: String? = null,
base: String? = null,
) =
if (base == "tn") if (base == "tn")
urlFromUrl("https://a.hitomi.la/$dir/${realFullPathFromHash(image.hash)}.$ext", base) urlFromUrl(
"https://a.gold-usergeneratedcontent.net/$dir/${realFullPathFromHash(image.hash)}.$ext",
base
)
else else
urlFromUrl(urlFromHash(galleryID, image, dir, ext), base) urlFromUrl(urlFromHash(galleryID, image, dir, ext), base)
suspend fun rewriteTnPaths(html: String) { suspend fun imageUrlFromImage(galleryID: Int, image: GalleryFiles, noWebp: Boolean): String {
html.replace(Regex("""//tn\.hitomi\.la/[^/]+/[0-9a-f]/[0-9a-f]{2}/[0-9a-f]{64}""")) { url ->
runBlocking {
urlFromUrl(url.value, "tn")
}
}
}
suspend fun imageUrlFromImage(galleryID: Int, image: GalleryFiles, noWebp: Boolean) : String {
return urlFromUrlFromHash(galleryID, image, "webp", null, "a") return urlFromUrlFromHash(galleryID, image, "webp", null, "a")
// return when { // return when {
// noWebp -> // noWebp ->