Add MangaHosted (#7259)

* Add MangaHosted

* Fix search request
This commit is contained in:
Chopper 2025-01-20 03:06:44 -03:00 committed by Draff
parent 24772e3262
commit c063dda9f0
No known key found for this signature in database
GPG Key ID: E8A89F3211677653
11 changed files with 366 additions and 0 deletions

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".all.mangahosted.MangaHostedUrlActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https"
android:host="mangahosted.org"
android:pathPattern="/.*/..*" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -0,0 +1,8 @@
ext {
extName = 'Manga Hosted'
extClass = '.MangaHostedFactory'
extVersionCode = 1
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,202 @@
package eu.kanade.tachiyomi.extension.all.mangahosted
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.interceptor.rateLimit
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.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
class MangaHosted(private val langOption: LanguageOption) : HttpSource() {
override val lang = langOption.lang
override val name: String = "Manga Hosted${langOption.nameSuffix}"
override val baseUrl: String = "https://mangahosted.org"
override val supportsLatest = true
private val json: Json by injectLazy()
override val client = network.client.newBuilder()
.rateLimit(2)
.build()
override fun headersBuilder(): Headers.Builder = super.headersBuilder()
.set("Referer", "$baseUrl/")
// ================================= Popular ==========================================
override fun popularMangaRequest(page: Int): Request {
val maxResult = 24
return GET("$apiUrl/${langOption.infix}/HomeTopFllow/$maxResult/${page - 1}")
}
override fun popularMangaParse(response: Response): MangasPage {
val dto = response.parseAs<Pageable<MangaDto>>()
val mangas = dto.data.map(::mangaParse)
return MangasPage(
mangas = mangas,
hasNextPage = dto.hasNextPage(),
)
}
// ================================= Latest ===========================================
override fun latestUpdatesRequest(page: Int): Request {
val maxResult = 24
val url = "$apiUrl/${langOption.infix}/HomeLastUpdate".toHttpUrl().newBuilder()
.addPathSegment("$maxResult")
.addPathSegment("${page - 1}")
.build()
return GET(url, headers)
}
override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
// ================================= Search ===========================================
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val maxResult = 20
val url = "$apiUrl/${langOption.infix}/SeachPage/$maxResult/${page - 1}".toHttpUrl().newBuilder()
.addPathSegment(query)
.build()
return GET(url, headers)
}
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
if (query.startsWith(SEARCH_PREFIX)) {
val url = "$baseUrl/${langOption.infix}/${query.substringAfter(SEARCH_PREFIX)}"
return client.newCall(GET(url, headers))
.asObservableSuccess().map { response ->
val mangas = try { listOf(mangaDetailsParse(response)) } catch (_: Exception) { emptyList() }
MangasPage(mangas, false)
}
}
return super.fetchSearchManga(page, query, filters)
}
override fun searchMangaParse(response: Response): MangasPage {
val dto = response.parseAs<SearchDto>()
return MangasPage(
dto.mangas.map(::mangaParse),
false,
)
}
// ================================= Details ==========================================
override fun mangaDetailsRequest(manga: SManga): Request {
val url = "$apiUrl/${langOption.infix}/getInfoManga".toHttpUrl().newBuilder()
.addPathSegment(manga.slug())
.build()
return GET(url, headers)
}
override fun mangaDetailsParse(response: Response): SManga {
val dto = response.parseAs<MangaDetailsDto>()
return mangaParse(dto.details)
}
override fun getMangaUrl(manga: SManga): String {
return baseUrl + manga.url.replace(langOption.infix, langOption.mangaSubstring)
}
// ================================= Chapter ==========================================
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
val chapters = mutableListOf<SChapter>()
var currentPage = 0
do {
val chaptersDto = fetchChapterListPageable(manga, currentPage++)
chapters += chaptersDto.data.map { chapter ->
SChapter.create().apply {
name = chapter.name
date_upload = chapter.date.toDate()
url = chapter.toChapterUrl(langOption.infix)
}
}
} while (chaptersDto.hasNextPage())
return Observable.just(chapters)
}
private fun fetchChapterListPageable(manga: SManga, page: Int): Pageable<ChapterDto> {
val maxResult = 100
val url = "$apiUrl/${langOption.infix}/GetChapterListFilter/${manga.slug()}/$maxResult/$page/all/${langOption.orderBy}"
return client.newCall(GET(url, headers)).execute()
.parseAs<Pageable<ChapterDto>>()
}
override fun chapterListParse(response: Response) = throw UnsupportedOperationException()
// ================================= Pages ============================================
override fun pageListRequest(chapter: SChapter): Request {
val chapterSlug = chapter.url.substringAfter(langOption.infix)
val url = "$apiUrl/${langOption.infix}/GetImageChapter$chapterSlug"
return GET(url, headers)
}
override fun imageRequest(page: Page): Request {
val imageHeaders = headers.newBuilder()
.set("Accept", "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8")
.removeAll("Referer")
.build()
return super.imageRequest(page).newBuilder()
.headers(imageHeaders)
.build()
}
override fun pageListParse(response: Response): List<Page> {
val location = response.request.url.toString()
val dto = response.parseAs<PageDto>()
return dto.pages.mapIndexed { index, url ->
Page(index, location, imageUrl = url)
}
}
override fun imageUrlParse(response: Response): String = ""
// ================================= Utilities =======================================
private inline fun <reified T> Response.parseAs(): T {
return json.decodeFromString(body.string())
}
private fun SManga.slug() = this.url.split("/").last()
private fun mangaParse(dto: MangaDto): SManga {
return SManga.create().apply {
title = dto.title
thumbnail_url = dto.thumbnailUrl
status = dto.status
url = "/${langOption.infix}/${dto.slug}"
genre = dto.genres
initialized = true
}
}
private fun String.toDate(): Long =
try { dateFormat.parse(trim())!!.time } catch (_: Exception) { 0L }
companion object {
const val SEARCH_PREFIX = "slug:"
val baseApiUrl = "https://api.novelfull.us"
val apiUrl = "$baseApiUrl/api"
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS", Locale.ENGLISH)
}
}

View File

@ -0,0 +1,68 @@
package eu.kanade.tachiyomi.extension.all.mangahosted
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
class MangaDetailsDto(private val data: Props) {
val details: MangaDto get() = data.details
@Serializable
class Props(
@SerialName("infoDoc") val details: MangaDto,
)
}
@Serializable
open class Pageable<T>(
var currentPage: Int,
var totalPage: Int,
val data: List<T>,
) {
fun hasNextPage() = (currentPage + 1) <= totalPage
}
@Serializable
class ChapterDto(
val date: String,
@SerialName("idDoc") val slugManga: String,
@SerialName("idDetail") val id: String,
@SerialName("nameChapter") val name: String,
) {
fun toChapterUrl(lang: String) = "/$lang/${this.slugManga}/$id"
}
@Serializable
class MangaDto(
@SerialName("name") val title: String,
@SerialName("image") private val _thumbnailUrl: String,
@SerialName("idDoc") val slug: String,
@SerialName("genresName") val genres: String,
@SerialName("status") val _status: String,
) {
val thumbnailUrl get() = "${MangaHosted.baseApiUrl}$_thumbnailUrl"
val status get() = when (_status) {
"ongoing" -> SManga.ONGOING
"completed" -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
}
@Serializable
class SearchDto(
@SerialName("data")
val mangas: List<MangaDto>,
)
@Serializable
class PageDto(val `data`: Data) {
val pages: List<String> get() = `data`.detailDocuments.source.split("#")
@Serializable
class Data(@SerialName("detail_documents") val detailDocuments: DetailDocuments)
@Serializable
class DetailDocuments(val source: String)
}

View File

@ -0,0 +1,30 @@
package eu.kanade.tachiyomi.extension.all.mangahosted
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
class MangaHostedFactory : SourceFactory {
override fun createSources(): List<Source> = languages.map { MangaHosted(it) }
}
class LanguageOption(
val lang: String,
val infix: String = lang,
val mangaSubstring: String = infix,
val nameSuffix: String = "",
val orderBy: String = "DESC",
)
val languages = listOf(
LanguageOption("en", "manga", "scan"),
LanguageOption("en", "manga-v2", "kaka", " v2"),
LanguageOption("en", "comic", "comic-dc", " Comics"),
LanguageOption("es", "manga-spanish", "manga-es"),
LanguageOption("id", "manga-indo", "id"),
LanguageOption("it", "manga-italia", "manga-it"),
LanguageOption("ja", "mangaraw", "raw"),
LanguageOption("pt-BR", "manga-br", orderBy = "ASC"),
LanguageOption("ru", "manga-ru", "mangaru"),
LanguageOption("ru", "manga-ru-hentai", "hentai", " +18"),
LanguageOption("ru", "manga-ru-yaoi", "yaoi", " +18 Yaoi"),
)

View File

@ -0,0 +1,36 @@
package eu.kanade.tachiyomi.extension.all.mangahosted
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
class MangaHostedUrlActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 1) {
val intent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", slug(pathSegments))
putExtra("filter", packageName)
}
try {
startActivity(intent)
} catch (e: ActivityNotFoundException) {
Log.e("UnionMangasUrlActivity", e.toString())
}
}
finish()
exitProcess(0)
}
private fun slug(pathSegments: List<String>) =
"${MangaHosted.SEARCH_PREFIX}${pathSegments[1]}"
}