New [RU] Newbie (#9394)

* New source Newbie

* mangaDetailsParse

* BETA save chapterList

* clean, pages, filters

* icon and "Open in browser"

* fix number formats

* fix number formats (againу)

* API_URL, structure pageListParse (needs fixing downloading chapters), limitation mangas pages

* date 0L

* limitation mangas pages (real)

* fix download

* Rename newbie dirs

* [ru]Newbiew. Fix image download

* capitalize genre

* change SManga url to get rid of conflict when making API changes

* only id (prev commit)

Co-authored-by: pavkazzz <me@pavkazzz.ru>
This commit is contained in:
e-shl 2021-10-12 15:34:24 +05:00 committed by GitHub
parent d8f1369583
commit abf7fe4284
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 462 additions and 0 deletions

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="eu.kanade.tachiyomi.extension" />

View File

@ -0,0 +1,16 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext {
extName = 'Newbie'
pkgNameSuffix = 'ru.newbie'
extClass = '.Newbie'
extVersionCode = 1
}
dependencies {
implementation project(':lib-dataimage')
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

View File

@ -0,0 +1,360 @@
package eu.kanade.tachiyomi.extension.ru.newbie
import BookDto
import LibraryDto
import MangaDetDto
import PageDto
import PageWrapperDto
import SeriesWrapperDto
import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.os.Build
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.model.Filter
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.toHttpUrlOrNull
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import org.jsoup.Jsoup
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.text.DecimalFormat
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class Newbie : HttpSource() {
override val name = "Newbie"
override val baseUrl = "https://newbie-tl.ru"
override val lang = "ru"
override val supportsLatest = true
override fun headersBuilder(): Headers.Builder = Headers.Builder()
.add("User-Agent", "Tachiyomi")
.add("Referer", baseUrl)
private fun imageContentTypeIntercept(chain: Interceptor.Chain): Response {
if (chain.request().url.queryParameter("slice").isNullOrEmpty()) {
return chain.proceed(chain.request())
}
val response = chain.proceed(chain.request())
val image = response.body?.byteString()?.toResponseBody("image/webp".toMediaType())
return response.newBuilder().body(image).build()
}
override val client: OkHttpClient =
network.client.newBuilder()
.addInterceptor { imageContentTypeIntercept(it) }
.build()
private val count = 30
override fun popularMangaRequest(page: Int) = GET("$API_URL/projects/popular?scale=week&size=$count&page=$page", headers)
override fun popularMangaParse(response: Response): MangasPage = searchMangaParse(response)
override fun latestUpdatesRequest(page: Int): Request = GET("$API_URL/projects/updates?only_bookmarks=false&size=$count&page=$page", headers)
override fun latestUpdatesParse(response: Response): MangasPage = searchMangaParse(response)
override fun searchMangaParse(response: Response): MangasPage {
val page = json.decodeFromString<PageWrapperDto<LibraryDto>>(response.body!!.string())
val mangas = page.items.map {
it.toSManga()
}
return MangasPage(mangas, mangas.size == count)
}
private fun LibraryDto.toSManga(): SManga {
val o = this
return SManga.create().apply {
// Do not change the title name to ensure work with a multilingual catalog!
title = o.title.en
url = "$id"
thumbnail_url = if (image.srcset.large.isNotEmpty()) {
"$IMAGE_URL/${image.srcset.large}"
} else "" +
"$IMAGE_URL/${image.srcset.small}"
}
}
private val simpleDateFormat by lazy { SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US) }
private fun parseDate(date: String?): Long {
date ?: return 0L
return try {
simpleDateFormat.parse(date)!!.time
} catch (_: Exception) {
Date().time
}
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
var url = "$API_URL/projects/catalog?size=$count&page=$page".toHttpUrlOrNull()!!.newBuilder()
if (query.isNotEmpty()) {
url = "$API_URL/projects/search?size=$count&page=$page".toHttpUrlOrNull()!!.newBuilder()
url.addQueryParameter("query", query)
}
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
when (filter) {
is OrderBy -> {
val ord = arrayOf("rating", "fresh")[filter.state!!.index]
url.addQueryParameter("sorting", ord)
}
is TypeList -> filter.state.forEach { type ->
if (type.state) {
url.addQueryParameter("types", type.id)
}
}
is StatusList -> filter.state.forEach { status ->
if (status.state) {
url.addQueryParameter("statuses", status.id)
}
}
is GenreList -> filter.state.forEach { genre ->
if (genre.state) {
url.addQueryParameter("genres", genre.id)
}
}
}
}
return GET(url.toString(), headers)
}
private fun parseStatus(status: String): Int {
return when (status) {
"completed" -> SManga.COMPLETED
"on_going" -> SManga.ONGOING
else -> SManga.UNKNOWN
}
}
private fun parseType(type: String): String {
return when (type) {
"manga" -> "Манга"
"manhwa" -> "Манхва"
"manhya" -> "Маньхуа"
"single" -> "Сингл"
"comics" -> "Комикс"
"russian" -> "Руманга"
else -> type
}
}
private fun MangaDetDto.toSManga(): SManga {
val ratingValue = DecimalFormat("#,###.##").format(rating * 2).replace(",", ".").toFloat()
val ratingStar = when {
ratingValue > 9.5 -> "★★★★★"
ratingValue > 8.5 -> "★★★★✬"
ratingValue > 7.5 -> "★★★★☆"
ratingValue > 6.5 -> "★★★✬☆"
ratingValue > 5.5 -> "★★★☆☆"
ratingValue > 4.5 -> "★★✬☆☆"
ratingValue > 3.5 -> "★★☆☆☆"
ratingValue > 2.5 -> "★✬☆☆☆"
ratingValue > 1.5 -> "★☆☆☆☆"
ratingValue > 0.5 -> "✬☆☆☆☆"
else -> "☆☆☆☆☆"
}
val o = this
return SManga.create().apply {
// Do not change the title name to ensure work with a multilingual catalog!
title = o.title.en
url = "$id"
thumbnail_url = "$IMAGE_URL/${image.srcset.large}"
author = o.author?.name
artist = o.artist?.name
description = o.title.ru + "\n" + ratingStar + " " + ratingValue + "\n" + Jsoup.parse(o.description).text()
genre = genres.joinToString { it.title.ru.capitalize() } + ", " + parseType(type) + ", " + "$adult+"
status = parseStatus(o.status)
}
}
private fun titleDetailsRequest(manga: SManga): Request {
return GET(API_URL + "/projects/" + manga.url, headers)
}
// Workaround to allow "Open in browser" use the real URL.
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return client.newCall(titleDetailsRequest(manga))
.asObservableSuccess()
.map { response ->
mangaDetailsParse(response).apply { initialized = true }
}
}
override fun mangaDetailsRequest(manga: SManga): Request {
return GET(baseUrl + "/p/" + manga.url, headers)
}
override fun mangaDetailsParse(response: Response): SManga {
val series = json.decodeFromString<MangaDetDto>(response.body!!.string())
return series.toSManga()
}
@SuppressLint("DefaultLocale")
private fun chapterName(book: BookDto): String {
var chapterName = "${book.tom}. Глава ${DecimalFormat("#,###.##").format(book.number).replace(",", ".")}"
if (book.name?.isNotBlank() == true) {
chapterName += " ${book.name.capitalize()}"
}
return chapterName
}
override fun chapterListParse(response: Response): List<SChapter> {
val chapters = json.decodeFromString<SeriesWrapperDto<List<BookDto>>>(response.body!!.string())
return chapters.items.filter { it.is_available == true }.map { chapter ->
SChapter.create().apply {
chapter_number = chapter.number
name = chapterName(chapter)
url = "/chapters/${chapter.id}/pages"
date_upload = parseDate(chapter.created_at)
scanlator = chapter.translator
}
}
}
override fun chapterListRequest(manga: SManga): Request {
return GET(API_URL + "/projects/" + manga.url + "/chapters?reverse=true&size=1000000", headers)
}
@TargetApi(Build.VERSION_CODES.N)
override fun pageListRequest(chapter: SChapter): Request {
return GET(API_URL + chapter.url, headers)
}
private fun pageListParse(response: Response, chapter: SChapter): List<Page> {
val body = response.body?.string()!!
val pages = json.decodeFromString<List<PageDto>>(body)
val result = mutableListOf<Page>()
pages.forEach { page ->
(1..page.slices!!).map { i ->
result.add(Page(result.size, "", API_URL + chapter.url + "/${page.id}?slice=$i"))
}
}
return result
}
override fun pageListParse(response: Response): List<Page> = throw Exception("Not used")
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
return client.newCall(pageListRequest(chapter))
.asObservableSuccess()
.map { response ->
pageListParse(response, chapter)
}
}
override fun fetchImageUrl(page: Page): Observable<String> = Observable.just(page.imageUrl!!)
override fun imageUrlRequest(page: Page): Request = throw NotImplementedError("Unused")
override fun imageUrlParse(response: Response): String = throw NotImplementedError("Unused")
override fun imageRequest(page: Page): Request {
val refererHeaders = headersBuilder().build()
return GET(page.imageUrl!!, refererHeaders)
}
private class CheckFilter(name: String, val id: String) : Filter.CheckBox(name)
private class TypeList(types: List<CheckFilter>) : Filter.Group<CheckFilter>("Типы", types)
private class StatusList(statuses: List<CheckFilter>) : Filter.Group<CheckFilter>("Статус", statuses)
private class GenreList(genres: List<CheckFilter>) : Filter.Group<CheckFilter>("Жанры", genres)
override fun getFilterList() = FilterList(
OrderBy(),
GenreList(getGenreList()),
TypeList(getTypeList()),
StatusList(getStatusList())
)
private class OrderBy : Filter.Sort(
"Сортировка",
arrayOf("По рейтенгу", "По новизне"),
Selection(0, false)
)
private fun getTypeList() = listOf(
CheckFilter("Манга", "manga"),
CheckFilter("Манхва", "manhwa"),
CheckFilter("Маньхуа", "manhya"),
CheckFilter("Сингл", "single"),
CheckFilter("OEL-манга", "oel"),
CheckFilter("Комикс", "comics"),
CheckFilter("Руманга", "russian")
)
private fun getStatusList() = listOf(
CheckFilter("Выпускается", "on_going"),
CheckFilter("Заброшен", "abandoned"),
CheckFilter("Завершён", "completed"),
CheckFilter("Приостановлен", "suspended")
)
private fun getGenreList() = listOf(
CheckFilter("cёнэн-ай", "28"),
CheckFilter("боевик", "17"),
CheckFilter("боевые искусства", "33"),
CheckFilter("гарем", "34"),
CheckFilter("гендерная интрига", "3"),
CheckFilter("героическое фэнтези", "19"),
CheckFilter("детектив", "35"),
CheckFilter("дзёсэй", "4"),
CheckFilter("додзинси", "20"),
CheckFilter("драма", "36"),
CheckFilter("ёнкома", "5"),
CheckFilter("игра", "21"),
CheckFilter("драма", "36"),
CheckFilter("ёнкома", "5"),
CheckFilter("игра", "21"),
CheckFilter("исекай", "37"),
CheckFilter("история", "6"),
CheckFilter("киберпанк", "22"),
CheckFilter("кодомо", "38"),
CheckFilter("комедия", "7"),
CheckFilter("махо-сёдзё", "23"),
CheckFilter("меха", "39"),
CheckFilter("мистика", "8"),
CheckFilter("научная фантастика", "24"),
CheckFilter("омегаверс", "40"),
CheckFilter("повседневность", "9"),
CheckFilter("постапокалиптика", "25"),
CheckFilter("приключения", "41"),
CheckFilter("психология", "10"),
CheckFilter("романтика", "26"),
CheckFilter("самурайский боевик", "42"),
CheckFilter("сверхъестественное", "11"),
CheckFilter("сёдзё", "27"),
CheckFilter("сёдзё-ай", "43"),
CheckFilter("сёнэн", "13"),
CheckFilter("спорт", "44"),
CheckFilter("сэйнэн", "12"),
CheckFilter("трагедия", "29"),
CheckFilter("триллер", "45"),
CheckFilter("ужасы", "14"),
CheckFilter("фантастика", "30"),
CheckFilter("фэнтези", "46"),
CheckFilter("школа", "15"),
CheckFilter("элементы юмора", "1"),
CheckFilter("эротика", "31"),
CheckFilter("этти", "47"),
CheckFilter("юри", "16"),
CheckFilter("яой", "32"),
)
companion object {
private const val API_URL = "https://api.newbie-tl.ru/v2"
private const val IMAGE_URL = "https://storage.newbie-tl.ru"
}
private val json: Json by injectLazy()
}

View File

@ -0,0 +1,84 @@
import kotlinx.serialization.Serializable
@Serializable
data class TagsDto(
val id: Int,
val title: TitleDto
)
@Serializable
data class BranchesDto(
val id: Long,
val count_chapters: Int
)
@Serializable
data class ImgsDto(
val large: String,
val small: String,
val thumbnail: String
)
@Serializable
data class ImgDto(
val srcset: ImgsDto,
)
@Serializable
data class TitleDto(
val en: String,
val ru: String
)
@Serializable
data class AuthorDto(
val name: String?
)
@Serializable
data class LibraryDto(
val id: Long,
val title: TitleDto,
val image: ImgDto
)
@Serializable
data class MangaDetDto(
val id: Long,
val title: TitleDto,
val author: AuthorDto?,
val artist: AuthorDto?,
val description: String,
val release_date: String,
val image: ImgDto,
val genres: List<TagsDto>,
val type: String,
val status: String,
val rating: Float,
val adult: String
)
@Serializable
data class PageWrapperDto<T>(
val items: List<T>,
)
@Serializable
data class SeriesWrapperDto<T>(
val items: T
)
@Serializable
data class BookDto(
val id: Long,
val tom: Int?,
val name: String?,
val number: Float,
val created_at: String,
val translator: String?,
val is_available: Boolean
)
@Serializable
data class PageDto(
val id: Int,
val slices: Int?
)