add Comicfuz (#3600)
* ComicFuz * points info * search and payed chapter indicator * save full urls * client side cache with search and latest * final cleanup and icons * image quality and isNsfw * tag search
This commit is contained in:
		
							parent
							
								
									47b60ed24d
								
							
						
					
					
						commit
						02ddcb00e6
					
				
							
								
								
									
										8
									
								
								src/ja/comicfuz/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/ja/comicfuz/build.gradle
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,8 @@
 | 
			
		||||
ext {
 | 
			
		||||
    extName = 'COMIC FUZ'
 | 
			
		||||
    extClass = '.ComicFuz'
 | 
			
		||||
    extVersionCode = 1
 | 
			
		||||
    isNsfw = true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
apply from: "$rootDir/common.gradle"
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								src/ja/comicfuz/res/mipmap-hdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/ja/comicfuz/res/mipmap-hdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 3.7 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/ja/comicfuz/res/mipmap-mdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/ja/comicfuz/res/mipmap-mdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 2.0 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/ja/comicfuz/res/mipmap-xhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/ja/comicfuz/res/mipmap-xhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 4.9 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/ja/comicfuz/res/mipmap-xxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/ja/comicfuz/res/mipmap-xxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 8.0 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/ja/comicfuz/res/mipmap-xxxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/ja/comicfuz/res/mipmap-xxxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 12 KiB  | 
@ -0,0 +1,222 @@
 | 
			
		||||
package eu.kanade.tachiyomi.extension.ja.comicfuz
 | 
			
		||||
 | 
			
		||||
import eu.kanade.tachiyomi.network.POST
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.FilterList
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.MangasPage
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.Page
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.SChapter
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.SManga
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.HttpSource
 | 
			
		||||
import kotlinx.serialization.decodeFromByteArray
 | 
			
		||||
import kotlinx.serialization.encodeToByteArray
 | 
			
		||||
import kotlinx.serialization.protobuf.ProtoBuf
 | 
			
		||||
import okhttp3.HttpUrl.Companion.toHttpUrl
 | 
			
		||||
import okhttp3.MediaType.Companion.toMediaType
 | 
			
		||||
import okhttp3.Request
 | 
			
		||||
import okhttp3.RequestBody
 | 
			
		||||
import okhttp3.RequestBody.Companion.toRequestBody
 | 
			
		||||
import okhttp3.Response
 | 
			
		||||
import okio.IOException
 | 
			
		||||
 | 
			
		||||
class ComicFuz : HttpSource() {
 | 
			
		||||
 | 
			
		||||
    override val name = "COMIC FUZ"
 | 
			
		||||
 | 
			
		||||
    private val domain = "comic-fuz.com"
 | 
			
		||||
    override val baseUrl = "https://$domain"
 | 
			
		||||
    private val apiUrl = "https://api.$domain/v1"
 | 
			
		||||
    private val cdnUrl = "https://img.$domain"
 | 
			
		||||
 | 
			
		||||
    override val lang = "ja"
 | 
			
		||||
 | 
			
		||||
    override val supportsLatest = true
 | 
			
		||||
 | 
			
		||||
    override val client = network.cloudflareClient.newBuilder()
 | 
			
		||||
        .addInterceptor(ImageInterceptor)
 | 
			
		||||
        .addNetworkInterceptor { chain ->
 | 
			
		||||
            val response = chain.proceed(chain.request())
 | 
			
		||||
 | 
			
		||||
            if (!response.isSuccessful) {
 | 
			
		||||
                val exception = when (response.code) {
 | 
			
		||||
                    401 -> "Unauthorized"
 | 
			
		||||
                    402 -> "Payment Required"
 | 
			
		||||
                    else -> "HTTP error ${response.code}"
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                throw IOException(exception)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return@addNetworkInterceptor response
 | 
			
		||||
        }
 | 
			
		||||
        .build()
 | 
			
		||||
 | 
			
		||||
    override fun headersBuilder() = super.headersBuilder()
 | 
			
		||||
        .set("Referer", "$baseUrl/")
 | 
			
		||||
        .set("Origin", baseUrl)
 | 
			
		||||
 | 
			
		||||
    override fun popularMangaRequest(page: Int): Request {
 | 
			
		||||
        return searchMangaRequest(page, "", getFilterList())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun popularMangaParse(response: Response): MangasPage {
 | 
			
		||||
        return searchMangaParse(response)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun latestUpdatesRequest(page: Int): Request {
 | 
			
		||||
        val payload = DayOfWeekRequest(
 | 
			
		||||
            deviceInfo = DeviceInfo(
 | 
			
		||||
                deviceType = DeviceType.BROWSER,
 | 
			
		||||
            ),
 | 
			
		||||
            dayOfWeek = DayOfWeek.today(),
 | 
			
		||||
        ).toRequestBody()
 | 
			
		||||
 | 
			
		||||
        return POST("$apiUrl/mangas_by_day_of_week", headers, payload)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun latestUpdatesParse(response: Response): MangasPage {
 | 
			
		||||
        val data = response.parseAs<MangaListResponse>()
 | 
			
		||||
        val entries = data.mangas.map {
 | 
			
		||||
            it.toSManga(cdnUrl)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return MangasPage(entries, false)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
 | 
			
		||||
        val tag = filters.filterIsInstance<TagFilter>().first()
 | 
			
		||||
 | 
			
		||||
        return if (query.isNotBlank() || tag.selected == null) {
 | 
			
		||||
            val payload = SearchRequest(
 | 
			
		||||
                deviceInfo = DeviceInfo(
 | 
			
		||||
                    deviceType = DeviceType.BROWSER,
 | 
			
		||||
                ),
 | 
			
		||||
                query = query.trim(),
 | 
			
		||||
                pageIndexOfMangas = page,
 | 
			
		||||
                pageIndexOfBooks = 1,
 | 
			
		||||
            ).toRequestBody()
 | 
			
		||||
 | 
			
		||||
            POST("$apiUrl/search#$page", headers, payload)
 | 
			
		||||
        } else {
 | 
			
		||||
            val payload = MangaListRequest(
 | 
			
		||||
                deviceInfo = DeviceInfo(
 | 
			
		||||
                    deviceType = DeviceType.BROWSER,
 | 
			
		||||
                ),
 | 
			
		||||
                tagId = tag.selected!!,
 | 
			
		||||
            ).toRequestBody()
 | 
			
		||||
 | 
			
		||||
            POST("$apiUrl/manga_list", headers, payload)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun searchMangaParse(response: Response): MangasPage {
 | 
			
		||||
        return if (response.request.url.pathSegments.last() == "search") {
 | 
			
		||||
            val data = response.parseAs<SearchResponse>()
 | 
			
		||||
            val page = response.request.url.fragment!!.toInt()
 | 
			
		||||
            val entries = data.mangas.map {
 | 
			
		||||
                it.toSManga(cdnUrl)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            MangasPage(entries, data.pageCountOfMangas > page)
 | 
			
		||||
        } else {
 | 
			
		||||
            val data = response.parseAs<MangaListResponse>()
 | 
			
		||||
            val entries = data.mangas.map {
 | 
			
		||||
                it.toSManga(cdnUrl)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return MangasPage(entries, false)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getFilterList() = getFilters()
 | 
			
		||||
 | 
			
		||||
    override fun mangaDetailsRequest(manga: SManga): Request {
 | 
			
		||||
        val payload = MangaDetailsRequest(
 | 
			
		||||
            deviceInfo = DeviceInfo(
 | 
			
		||||
                deviceType = DeviceType.BROWSER,
 | 
			
		||||
            ),
 | 
			
		||||
            mangaId = manga.url.substringAfterLast("/").toInt(),
 | 
			
		||||
        ).toRequestBody()
 | 
			
		||||
 | 
			
		||||
        return POST("$apiUrl/manga_detail", headers, payload)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getMangaUrl(manga: SManga): String {
 | 
			
		||||
        return "$baseUrl${manga.url}"
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun mangaDetailsParse(response: Response): SManga {
 | 
			
		||||
        val data = response.parseAs<MangaDetailsResponse>()
 | 
			
		||||
 | 
			
		||||
        return data.toSManga(cdnUrl)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun chapterListRequest(manga: SManga) = mangaDetailsRequest(manga)
 | 
			
		||||
 | 
			
		||||
    override fun chapterListParse(response: Response): List<SChapter> {
 | 
			
		||||
        val data = response.parseAs<MangaDetailsResponse>()
 | 
			
		||||
 | 
			
		||||
        return data.chapterGroups.flatMap { group ->
 | 
			
		||||
            group.chapters.map { chapter ->
 | 
			
		||||
                chapter.toSChapter()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getChapterUrl(chapter: SChapter): String {
 | 
			
		||||
        return "$baseUrl${chapter.url}"
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun pageListRequest(chapter: SChapter): Request {
 | 
			
		||||
        val payload = MangaViewerRequest(
 | 
			
		||||
            deviceInfo = DeviceInfo(
 | 
			
		||||
                deviceType = DeviceType.BROWSER,
 | 
			
		||||
            ),
 | 
			
		||||
            chapterId = chapter.url.substringAfterLast("/").toInt(),
 | 
			
		||||
            useTicket = false,
 | 
			
		||||
            consumePoint = UserPoint(
 | 
			
		||||
                event = 0,
 | 
			
		||||
                paid = 0,
 | 
			
		||||
            ),
 | 
			
		||||
            viewerMode = ViewerMode(
 | 
			
		||||
                imageQuality = ImageQuality.HIGH,
 | 
			
		||||
            ),
 | 
			
		||||
        ).toRequestBody()
 | 
			
		||||
 | 
			
		||||
        return POST("$apiUrl/manga_viewer", headers, payload)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun pageListParse(response: Response): List<Page> {
 | 
			
		||||
        val data = response.parseAs<MangaViewerResponse>()
 | 
			
		||||
 | 
			
		||||
        val pages = data.pages
 | 
			
		||||
            .filter { it.image?.isExtraPage == false }
 | 
			
		||||
            .mapNotNull { it.image }
 | 
			
		||||
 | 
			
		||||
        return pages.mapIndexed { idx, page ->
 | 
			
		||||
            Page(
 | 
			
		||||
                index = idx,
 | 
			
		||||
                imageUrl = if (page.encryptionKey.isEmpty() && page.iv.isEmpty()) {
 | 
			
		||||
                    cdnUrl + page.imageUrl
 | 
			
		||||
                } else {
 | 
			
		||||
                    "$cdnUrl${page.imageUrl}".toHttpUrl().newBuilder()
 | 
			
		||||
                        .addQueryParameter("key", page.encryptionKey)
 | 
			
		||||
                        .addQueryParameter("iv", page.iv)
 | 
			
		||||
                        .toString()
 | 
			
		||||
                },
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun imageUrlParse(response: Response): String {
 | 
			
		||||
        throw UnsupportedOperationException()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private inline fun <reified T> Response.parseAs(): T {
 | 
			
		||||
        return ProtoBuf.decodeFromByteArray(body.bytes())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private inline fun <reified T : Any> T.toRequestBody(): RequestBody {
 | 
			
		||||
        return ProtoBuf.encodeToByteArray(this)
 | 
			
		||||
            .toRequestBody("application/protobuf".toMediaType())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,110 @@
 | 
			
		||||
package eu.kanade.tachiyomi.extension.ja.comicfuz
 | 
			
		||||
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.SChapter
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.SManga
 | 
			
		||||
import kotlinx.serialization.Serializable
 | 
			
		||||
import kotlinx.serialization.protobuf.ProtoNumber
 | 
			
		||||
import java.text.ParseException
 | 
			
		||||
import java.text.SimpleDateFormat
 | 
			
		||||
import java.util.Locale
 | 
			
		||||
 | 
			
		||||
@Serializable
 | 
			
		||||
class MangaListResponse(
 | 
			
		||||
    @ProtoNumber(1) val mangas: List<Manga>,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@Serializable
 | 
			
		||||
class SearchResponse(
 | 
			
		||||
    @ProtoNumber(2) val mangas: List<Manga>,
 | 
			
		||||
    @ProtoNumber(6) val pageCountOfMangas: Int = 0,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@Serializable
 | 
			
		||||
class Manga(
 | 
			
		||||
    @ProtoNumber(1) private val id: Int,
 | 
			
		||||
    @ProtoNumber(2) private val title: String,
 | 
			
		||||
    @ProtoNumber(4) private val cover: String,
 | 
			
		||||
    @ProtoNumber(14) private val description: String,
 | 
			
		||||
) {
 | 
			
		||||
    fun toSManga(cdnUrl: String): SManga = SManga.create().apply {
 | 
			
		||||
        url = "/manga/$id"
 | 
			
		||||
        title = this@Manga.title
 | 
			
		||||
        thumbnail_url = cdnUrl + cover
 | 
			
		||||
        description = this@Manga.description
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Serializable
 | 
			
		||||
class MangaDetailsResponse(
 | 
			
		||||
    @ProtoNumber(2) private val manga: Manga,
 | 
			
		||||
    @ProtoNumber(3) val chapterGroups: List<ChapterGroup>,
 | 
			
		||||
    @ProtoNumber(4) private val authors: List<Author>,
 | 
			
		||||
    @ProtoNumber(7) private val tags: List<Name>,
 | 
			
		||||
) {
 | 
			
		||||
    fun toSManga(cdnUrl: String) = manga.toSManga(cdnUrl).apply {
 | 
			
		||||
        genre = tags.joinToString { it.name }
 | 
			
		||||
        author = authors.joinToString { it.author.name }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Serializable
 | 
			
		||||
class Author(
 | 
			
		||||
    @ProtoNumber(1) val author: Name,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@Serializable
 | 
			
		||||
class Name(
 | 
			
		||||
    @ProtoNumber(2) val name: String,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@Serializable
 | 
			
		||||
class ChapterGroup(
 | 
			
		||||
    @ProtoNumber(2) val chapters: List<Chapter>,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@Serializable
 | 
			
		||||
class Chapter(
 | 
			
		||||
    @ProtoNumber(1) private val id: Int,
 | 
			
		||||
    @ProtoNumber(2) private val title: String,
 | 
			
		||||
    @ProtoNumber(5) private val points: Point,
 | 
			
		||||
    @ProtoNumber(8) private val date: String = "",
 | 
			
		||||
) {
 | 
			
		||||
    fun toSChapter() = SChapter.create().apply {
 | 
			
		||||
        url = "/manga/viewer/$id"
 | 
			
		||||
        name = if (points.amount > 0) {
 | 
			
		||||
            "\uD83D\uDD12 $title" // lock emoji
 | 
			
		||||
        } else {
 | 
			
		||||
            title
 | 
			
		||||
        }
 | 
			
		||||
        date_upload = try {
 | 
			
		||||
            dateFormat.parse(date)!!.time
 | 
			
		||||
        } catch (_: ParseException) {
 | 
			
		||||
            0L
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private val dateFormat = SimpleDateFormat("yyyy/MM/dd", Locale.ENGLISH)
 | 
			
		||||
 | 
			
		||||
@Serializable
 | 
			
		||||
class Point(
 | 
			
		||||
    @ProtoNumber(2) val amount: Int = 0,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@Serializable
 | 
			
		||||
class MangaViewerResponse(
 | 
			
		||||
    @ProtoNumber(3) val pages: List<ViewerPage>,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@Serializable
 | 
			
		||||
class ViewerPage(
 | 
			
		||||
    @ProtoNumber(1) val image: Image? = null,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@Serializable
 | 
			
		||||
class Image(
 | 
			
		||||
    @ProtoNumber(1) val imageUrl: String,
 | 
			
		||||
    @ProtoNumber(3) val iv: String = "",
 | 
			
		||||
    @ProtoNumber(4) val encryptionKey: String = "",
 | 
			
		||||
    @ProtoNumber(7) val isExtraPage: Boolean = false,
 | 
			
		||||
)
 | 
			
		||||
@ -0,0 +1,78 @@
 | 
			
		||||
package eu.kanade.tachiyomi.extension.ja.comicfuz
 | 
			
		||||
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.Filter
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.FilterList
 | 
			
		||||
 | 
			
		||||
fun getFilters() = FilterList(
 | 
			
		||||
    TagFilter(),
 | 
			
		||||
    Filter.Separator(),
 | 
			
		||||
    Filter.Header("Doesn't work with text search"),
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
class TagFilter : Filter.Select<String>("Tags", tags.map { it.name }.toTypedArray()) {
 | 
			
		||||
    val selected get() = when (state) {
 | 
			
		||||
        0 -> null
 | 
			
		||||
        else -> tags[state].id
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class Tag(
 | 
			
		||||
    val id: Int,
 | 
			
		||||
    val name: String,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
private val tags = listOf(
 | 
			
		||||
    Tag(-1, ""),
 | 
			
		||||
    Tag(7, "日曜日"),
 | 
			
		||||
    Tag(12, "オリジナル"),
 | 
			
		||||
    Tag(38, "グルメ"),
 | 
			
		||||
    Tag(138, "FUZコミックス"),
 | 
			
		||||
    Tag(288, "広告で人気の作品"),
 | 
			
		||||
    Tag(462, "オリジナル作品の最新話が無料化!"),
 | 
			
		||||
    Tag(540, "ギャグ・コメディ"),
 | 
			
		||||
    Tag(552, "日常"),
 | 
			
		||||
    Tag(23, "学園"),
 | 
			
		||||
    Tag(26, "SF・ファンタジー"),
 | 
			
		||||
    Tag(29, "恋愛"),
 | 
			
		||||
    Tag(13, "男性向け"),
 | 
			
		||||
    Tag(549, "百合"),
 | 
			
		||||
    Tag(41, "お仕事・趣味"),
 | 
			
		||||
    Tag(56, "週刊漫画TIMES"),
 | 
			
		||||
    Tag(150, "芳文社コミックス"),
 | 
			
		||||
    Tag(537, "スポーツ"),
 | 
			
		||||
    Tag(68, "まんがタイムきららフォワード"),
 | 
			
		||||
    Tag(141, "まんがタイムKRコミックス"),
 | 
			
		||||
    Tag(291, "新規連載作品"),
 | 
			
		||||
    Tag(204, "まんがタイムオリジナル"),
 | 
			
		||||
    Tag(6, "土曜日"),
 | 
			
		||||
    Tag(1274, "6/3発売 FUZオリジナル作品新刊"),
 | 
			
		||||
    Tag(2, "火曜日"),
 | 
			
		||||
    Tag(14, "女性向け"),
 | 
			
		||||
    Tag(44, "バトル・アクション"),
 | 
			
		||||
    Tag(47, "ミステリー・サスペンス"),
 | 
			
		||||
    Tag(83, "BL"),
 | 
			
		||||
    Tag(32, "メディア化"),
 | 
			
		||||
    Tag(50, "歴史・時代"),
 | 
			
		||||
    Tag(20, "4コマ"),
 | 
			
		||||
    Tag(147, "まんがタイムコミックス"),
 | 
			
		||||
    Tag(5, "金曜日"),
 | 
			
		||||
    Tag(543, "異世界"),
 | 
			
		||||
    Tag(35, "ヒューマンドラマ"),
 | 
			
		||||
    Tag(65, "まんがタイムきららキャラット"),
 | 
			
		||||
    Tag(4, "木曜日"),
 | 
			
		||||
    Tag(59, "まんがタイムきらら"),
 | 
			
		||||
    Tag(153, "ラバココミックス"),
 | 
			
		||||
    Tag(201, "まんがタイム"),
 | 
			
		||||
    Tag(3, "水曜日"),
 | 
			
		||||
    Tag(62, "まんがタイムきららMAX"),
 | 
			
		||||
    Tag(17, "読切"),
 | 
			
		||||
    Tag(1, "月曜日"),
 | 
			
		||||
    Tag(74, "ゆるキャン△"),
 | 
			
		||||
    Tag(207, "コミックトレイル"),
 | 
			
		||||
    Tag(77, "城下町のダンデライオン"),
 | 
			
		||||
    Tag(156, "トレイルコミックス"),
 | 
			
		||||
    Tag(198, "まんがホーム"),
 | 
			
		||||
    Tag(71, "魔法少女まどか☆マギカ"),
 | 
			
		||||
    Tag(177, "花音コミックス"),
 | 
			
		||||
    Tag(1175, "価格改定対象作品"),
 | 
			
		||||
)
 | 
			
		||||
@ -0,0 +1,52 @@
 | 
			
		||||
package eu.kanade.tachiyomi.extension.ja.comicfuz
 | 
			
		||||
 | 
			
		||||
import okhttp3.Interceptor
 | 
			
		||||
import okhttp3.MediaType.Companion.toMediaType
 | 
			
		||||
import okhttp3.Response
 | 
			
		||||
import okhttp3.ResponseBody.Companion.toResponseBody
 | 
			
		||||
import javax.crypto.Cipher
 | 
			
		||||
import javax.crypto.spec.IvParameterSpec
 | 
			
		||||
import javax.crypto.spec.SecretKeySpec
 | 
			
		||||
 | 
			
		||||
object ImageInterceptor : Interceptor {
 | 
			
		||||
    private val mediaType = "image/jpeg".toMediaType()
 | 
			
		||||
 | 
			
		||||
    private inline val AES: Cipher
 | 
			
		||||
        get() = Cipher.getInstance("AES/CBC/PKCS7Padding")
 | 
			
		||||
 | 
			
		||||
    override fun intercept(chain: Interceptor.Chain): Response {
 | 
			
		||||
        val url = chain.request().url
 | 
			
		||||
        val key = url.queryParameter("key")
 | 
			
		||||
            ?: return chain.proceed(chain.request())
 | 
			
		||||
        val iv = url.queryParameter("iv")!!
 | 
			
		||||
 | 
			
		||||
        val response = chain.proceed(
 | 
			
		||||
            chain.request().newBuilder().url(
 | 
			
		||||
                url.newBuilder()
 | 
			
		||||
                    .removeAllQueryParameters("key")
 | 
			
		||||
                    .removeAllQueryParameters("iv")
 | 
			
		||||
                    .build(),
 | 
			
		||||
            ).build(),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        val body = response.body.bytes()
 | 
			
		||||
            .decode(key.decodeHex(), iv.decodeHex())
 | 
			
		||||
 | 
			
		||||
        return response.newBuilder()
 | 
			
		||||
            .body(body)
 | 
			
		||||
            .build()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun ByteArray.decode(key: ByteArray, iv: ByteArray) = AES.let {
 | 
			
		||||
        it.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
 | 
			
		||||
        it.doFinal(this).toResponseBody(mediaType)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun String.decodeHex(): ByteArray {
 | 
			
		||||
        check(length % 2 == 0) { "Must have an even length" }
 | 
			
		||||
 | 
			
		||||
        return chunked(2)
 | 
			
		||||
            .map { it.toInt(16).toByte() }
 | 
			
		||||
            .toByteArray()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,89 @@
 | 
			
		||||
package eu.kanade.tachiyomi.extension.ja.comicfuz
 | 
			
		||||
 | 
			
		||||
import kotlinx.serialization.Serializable
 | 
			
		||||
import kotlinx.serialization.protobuf.ProtoNumber
 | 
			
		||||
import java.util.Calendar
 | 
			
		||||
 | 
			
		||||
@Serializable
 | 
			
		||||
class DeviceInfo(
 | 
			
		||||
    @ProtoNumber(3) private val deviceType: DeviceType,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
enum class DeviceType {
 | 
			
		||||
    IOS,
 | 
			
		||||
    ANDROID,
 | 
			
		||||
    BROWSER,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Serializable
 | 
			
		||||
class DayOfWeekRequest(
 | 
			
		||||
    @ProtoNumber(1) private val deviceInfo: DeviceInfo,
 | 
			
		||||
    @ProtoNumber(2) private val dayOfWeek: DayOfWeek,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
enum class DayOfWeek(private val dayNum: Int) {
 | 
			
		||||
    ALL(0),
 | 
			
		||||
    MONDAY(1),
 | 
			
		||||
    TUESDAY(2),
 | 
			
		||||
    WEDNESDAY(3),
 | 
			
		||||
    THURSDAY(4),
 | 
			
		||||
    FRIDAY(5),
 | 
			
		||||
    SATURDAY(6),
 | 
			
		||||
    SUNDAY(7),
 | 
			
		||||
    ;
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        fun today(): DayOfWeek {
 | 
			
		||||
            val calendar = Calendar.getInstance()
 | 
			
		||||
            val dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK)
 | 
			
		||||
            val adjustedDayOfWeek = if (dayOfWeek == Calendar.SUNDAY) 7 else dayOfWeek - 1
 | 
			
		||||
 | 
			
		||||
            return values().first { it.dayNum == adjustedDayOfWeek }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Serializable
 | 
			
		||||
class SearchRequest(
 | 
			
		||||
    @ProtoNumber(1) private val deviceInfo: DeviceInfo,
 | 
			
		||||
    @ProtoNumber(2) private val query: String,
 | 
			
		||||
    @ProtoNumber(3) private val pageIndexOfMangas: Int,
 | 
			
		||||
    @ProtoNumber(4) private val pageIndexOfBooks: Int,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@Serializable
 | 
			
		||||
class MangaListRequest(
 | 
			
		||||
    @ProtoNumber(1) private val deviceInfo: DeviceInfo,
 | 
			
		||||
    @ProtoNumber(2) private val tagId: Int,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@Serializable
 | 
			
		||||
class MangaDetailsRequest(
 | 
			
		||||
    @ProtoNumber(1) private val deviceInfo: DeviceInfo,
 | 
			
		||||
    @ProtoNumber(2) private val mangaId: Int,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@Serializable
 | 
			
		||||
class MangaViewerRequest(
 | 
			
		||||
    @ProtoNumber(1) private val deviceInfo: DeviceInfo,
 | 
			
		||||
    @ProtoNumber(2) private val chapterId: Int,
 | 
			
		||||
    @ProtoNumber(3) private val useTicket: Boolean,
 | 
			
		||||
    @ProtoNumber(4) private val consumePoint: UserPoint,
 | 
			
		||||
    @ProtoNumber(5) private val viewerMode: ViewerMode,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@Serializable
 | 
			
		||||
class UserPoint(
 | 
			
		||||
    @ProtoNumber(1) private val event: Int,
 | 
			
		||||
    @ProtoNumber(2) private val paid: Int,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@Serializable
 | 
			
		||||
class ViewerMode(
 | 
			
		||||
    @ProtoNumber(1) private val imageQuality: ImageQuality,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
enum class ImageQuality {
 | 
			
		||||
    NORMAL,
 | 
			
		||||
    HIGH,
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user