Skip to content

Commit e18ddc4

Browse files
FiorenMasvetleledaal
authored andcommitted
Add Otakusic (#14252)
* Add Otakusic * change imgurl * Update src/vi/otakusic/src/eu/kanade/tachiyomi/extension/vi/otakusic/Otakusic.kt Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com> * Update src/vi/otakusic/src/eu/kanade/tachiyomi/extension/vi/otakusic/Otakusic.kt Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com> * Update src/vi/otakusic/src/eu/kanade/tachiyomi/extension/vi/otakusic/Otakusic.kt Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com> * Update src/vi/otakusic/src/eu/kanade/tachiyomi/extension/vi/otakusic/Otakusic.kt Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com> * Update src/vi/otakusic/src/eu/kanade/tachiyomi/extension/vi/otakusic/Otakusic.kt Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com> * Update src/vi/otakusic/src/eu/kanade/tachiyomi/extension/vi/otakusic/Otakusic.kt Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com> * Update src/vi/otakusic/src/eu/kanade/tachiyomi/extension/vi/otakusic/Otakusic.kt Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com> * fix build * fix date --------- Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
1 parent 26713bc commit e18ddc4

File tree

9 files changed

+443
-0
lines changed

9 files changed

+443
-0
lines changed

src/vi/otakusic/build.gradle

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
ext {
2+
extName = 'Otakusic'
3+
extClass = '.Otakusic'
4+
extVersionCode = 1
5+
isNsfw = true
6+
}
7+
8+
apply from: "$rootDir/common.gradle"
7.48 KB
Loading
3.65 KB
Loading
12.1 KB
Loading
24.9 KB
Loading
41.6 KB
Loading
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
package eu.kanade.tachiyomi.extension.vi.otakusic
2+
3+
import eu.kanade.tachiyomi.network.GET
4+
import eu.kanade.tachiyomi.network.interceptor.rateLimit
5+
import eu.kanade.tachiyomi.source.model.FilterList
6+
import eu.kanade.tachiyomi.source.model.MangasPage
7+
import eu.kanade.tachiyomi.source.model.Page
8+
import eu.kanade.tachiyomi.source.model.SChapter
9+
import eu.kanade.tachiyomi.source.model.SManga
10+
import eu.kanade.tachiyomi.source.online.HttpSource
11+
import eu.kanade.tachiyomi.util.asJsoup
12+
import keiyoushi.utils.parseAs
13+
import keiyoushi.utils.tryParse
14+
import kotlinx.serialization.json.Json
15+
import okhttp3.HttpUrl.Companion.toHttpUrl
16+
import okhttp3.Request
17+
import okhttp3.Response
18+
import uy.kohesive.injekt.injectLazy
19+
import java.text.SimpleDateFormat
20+
import java.util.Locale
21+
import java.util.TimeZone
22+
23+
class Otakusic : HttpSource() {
24+
override val name = "Otakusic"
25+
override val lang = "vi"
26+
override val baseUrl = "https://otakusic.com"
27+
override val supportsLatest = true
28+
29+
private val imgBaseUrl = baseUrl.replace("://", "://img.")
30+
31+
private val json: Json by injectLazy()
32+
33+
override val client = network.cloudflareClient.newBuilder()
34+
.rateLimit(3)
35+
.build()
36+
37+
override fun headersBuilder() = super.headersBuilder()
38+
.add("Referer", "$baseUrl/")
39+
40+
private fun apiHeaders() = headers.newBuilder()
41+
.add("X-Requested-With", "XMLHttpRequest")
42+
.add("Accept", "application/json")
43+
.build()
44+
45+
// ============================== Popular ===============================
46+
47+
override fun popularMangaRequest(page: Int): Request {
48+
val url = "$baseUrl/tim-kiem".toHttpUrl().newBuilder()
49+
.addQueryParameter("sort", "views")
50+
.addQueryParameter("page", page.toString())
51+
.build()
52+
return GET(url, headers)
53+
}
54+
55+
override fun popularMangaParse(response: Response): MangasPage = parseMangaListPage(response)
56+
57+
// =============================== Latest ===============================
58+
59+
override fun latestUpdatesRequest(page: Int): Request {
60+
val url = "$baseUrl/tim-kiem".toHttpUrl().newBuilder()
61+
.addQueryParameter("sort", "updated")
62+
.addQueryParameter("page", page.toString())
63+
.build()
64+
return GET(url, headers)
65+
}
66+
67+
override fun latestUpdatesParse(response: Response): MangasPage = parseMangaListPage(response)
68+
69+
// =============================== Search ===============================
70+
71+
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
72+
val url = "$baseUrl/tim-kiem".toHttpUrl().newBuilder()
73+
.addQueryParameter("page", page.toString())
74+
75+
if (query.isNotBlank()) {
76+
url.addQueryParameter("q", query)
77+
}
78+
79+
var sort = "updated"
80+
81+
filters.forEach { filter ->
82+
when (filter) {
83+
is SortFilter -> sort = filter.toUriPart()
84+
is StatusFilter -> filter.toUriPart()?.let { url.addQueryParameter("status", it) }
85+
is GenreFilter -> filter.toUriPart()?.let { url.addQueryParameter("category", it) }
86+
else -> {}
87+
}
88+
}
89+
90+
url.addQueryParameter("sort", sort)
91+
92+
return GET(url.build(), headers)
93+
}
94+
95+
override fun searchMangaParse(response: Response): MangasPage = parseMangaListPage(response)
96+
97+
override fun getFilterList(): FilterList = getFilters()
98+
99+
// =============================== Listing ==============================
100+
101+
private fun parseMangaListPage(response: Response): MangasPage {
102+
val document = response.asJsoup()
103+
104+
val mangaList = document.select("a[href*=/chi-tiet/]")
105+
.filter { it.selectFirst("img") != null }
106+
.map { element ->
107+
SManga.create().apply {
108+
setUrlWithoutDomain(element.absUrl("href"))
109+
title = element.selectFirst("img")!!.attr("alt")
110+
thumbnail_url = element.selectFirst("img")?.absUrl("src")
111+
}
112+
}
113+
.distinctBy { it.url }
114+
115+
val hasNextPage = document.selectFirst("a.pagination-btn:contains(Sau)") != null
116+
117+
return MangasPage(mangaList, hasNextPage)
118+
}
119+
120+
// =============================== Details ==============================
121+
122+
override fun mangaDetailsParse(response: Response): SManga {
123+
val document = response.asJsoup()
124+
125+
return SManga.create().apply {
126+
title = document.selectFirst("h1")!!.text()
127+
128+
author = document.select("h2:contains(Tác giả) + div a, h2:contains(Tác giả) ~ a")
129+
.joinToString { it.text() }
130+
.ifEmpty {
131+
document.selectFirst("h2:contains(Tác giả)")
132+
?.parent()
133+
?.ownText()
134+
?.replace(":", "")
135+
?.trim()
136+
?.takeIf { it.isNotEmpty() && it != "Đang cập nhật" }
137+
}
138+
139+
genre = document.select("div.flex.flex-wrap.gap-2 a")
140+
.joinToString { it.text() }
141+
142+
description = document.selectFirst("#description")?.text()
143+
144+
thumbnail_url = document.selectFirst("img[alt]")?.absUrl("src")
145+
146+
status = when {
147+
document.selectFirst("a[href*='status=ongoing']") != null -> SManga.ONGOING
148+
document.selectFirst("a[href*='status=completed']") != null -> SManga.COMPLETED
149+
else -> SManga.UNKNOWN
150+
}
151+
}
152+
}
153+
154+
override fun getMangaUrl(manga: SManga): String = "$baseUrl${manga.url}"
155+
156+
// ============================== Chapters ==============================
157+
158+
override fun chapterListRequest(manga: SManga): Request {
159+
val slug = manga.url.substringAfterLast("/chi-tiet/").trimEnd('/')
160+
return GET("$baseUrl/api/v1/manga/chapters/$slug", apiHeaders())
161+
}
162+
163+
override fun chapterListParse(response: Response): List<SChapter> {
164+
val chapters = response.parseAs<ChaptersResponse>().data
165+
val mangaSlug = response.request.url.pathSegments.last()
166+
167+
return chapters
168+
.filter { it.status != "inactive" }
169+
.map { dto ->
170+
SChapter.create().apply {
171+
// Store manga slug and chapter info for page list retrieval
172+
url = "$CHAPTER_URL_PREFIX$mangaSlug/${dto.chapterOriginalSlug}/${dto.chapterSlug}"
173+
name = "Chương ${dto.chapterName.content}"
174+
date_upload = (dto.publicAt ?: dto.updatedAt)?.let { dateFormat.tryParse(it) } ?: 0L
175+
}
176+
}
177+
}
178+
179+
override fun getChapterUrl(chapter: SChapter): String {
180+
val parts = chapter.url.removePrefix(CHAPTER_URL_PREFIX).split("/")
181+
return "$baseUrl/doc-truyen/${parts[0]}/${parts[2]}"
182+
}
183+
184+
// =============================== Pages ================================
185+
186+
override fun pageListRequest(chapter: SChapter): Request = throw UnsupportedOperationException()
187+
188+
override fun pageListParse(response: Response): List<Page> = throw UnsupportedOperationException()
189+
190+
override fun fetchPageList(chapter: SChapter): rx.Observable<List<Page>> {
191+
val parts = chapter.url.removePrefix(CHAPTER_URL_PREFIX).split("/")
192+
val mangaSlug = parts[0]
193+
val chapterOriginalSlug = parts[1]
194+
195+
val request = GET("$baseUrl/api/v1/manga/chapters/$mangaSlug", apiHeaders())
196+
val response = client.newCall(request).execute()
197+
val chapters = response.parseAs<ChaptersResponse>().data
198+
199+
val chapterDto = chapters.firstOrNull { it.chapterOriginalSlug == chapterOriginalSlug }
200+
?: throw Exception("Chapter not found")
201+
202+
val apiUrl = chapterDto.apiUrl
203+
?: throw Exception("No image data available for this chapter")
204+
205+
val imageFilenames: List<String> = json.decodeFromString(apiUrl)
206+
207+
val pages = imageFilenames.mapIndexed { index, filename ->
208+
val imageUrl = "$imgBaseUrl/manga/uploads/chapter/$mangaSlug/$chapterOriginalSlug/$filename"
209+
Page(index, imageUrl = imageUrl)
210+
}
211+
212+
return rx.Observable.just(pages)
213+
}
214+
215+
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
216+
217+
// ============================== Helpers ================================
218+
219+
private val dateFormat by lazy {
220+
SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ROOT).apply {
221+
timeZone = TimeZone.getTimeZone("Asia/Ho_Chi_Minh")
222+
}
223+
}
224+
225+
companion object {
226+
private const val CHAPTER_URL_PREFIX = "/api/chapter/"
227+
}
228+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package eu.kanade.tachiyomi.extension.vi.otakusic
2+
3+
import kotlinx.serialization.SerialName
4+
import kotlinx.serialization.Serializable
5+
import kotlinx.serialization.json.JsonPrimitive
6+
7+
@Serializable
8+
class ChaptersResponse(
9+
val data: List<ChapterDto> = emptyList(),
10+
)
11+
12+
@Serializable
13+
class ChapterDto(
14+
@SerialName("chapter_name") val chapterName: JsonPrimitive,
15+
@SerialName("chapter_slug") val chapterSlug: String,
16+
@SerialName("chapter_original_slug") val chapterOriginalSlug: String,
17+
@SerialName("manga_slug") val mangaSlug: String,
18+
@SerialName("is_locked") val isLocked: Boolean = false,
19+
@SerialName("api_url") val apiUrl: String? = null,
20+
@SerialName("updated_at") val updatedAt: String? = null,
21+
@SerialName("public_at") val publicAt: String? = null,
22+
val status: String? = null,
23+
)

0 commit comments

Comments
 (0)