diff --git a/src/ru/newbie/AndroidManifest.xml b/src/ru/newbie/AndroidManifest.xml
new file mode 100644
index 000000000..30deb7f79
--- /dev/null
+++ b/src/ru/newbie/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/src/ru/newbie/build.gradle b/src/ru/newbie/build.gradle
new file mode 100644
index 000000000..d5fc142a0
--- /dev/null
+++ b/src/ru/newbie/build.gradle
@@ -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"
diff --git a/src/ru/newbie/res/mipmap-hdpi/ic_launcher.png b/src/ru/newbie/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..7ff397956
Binary files /dev/null and b/src/ru/newbie/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/ru/newbie/res/mipmap-mdpi/ic_launcher.png b/src/ru/newbie/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..baced1263
Binary files /dev/null and b/src/ru/newbie/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/ru/newbie/res/mipmap-xhdpi/ic_launcher.png b/src/ru/newbie/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..771d2c3c6
Binary files /dev/null and b/src/ru/newbie/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/ru/newbie/res/mipmap-xxhdpi/ic_launcher.png b/src/ru/newbie/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..223b58183
Binary files /dev/null and b/src/ru/newbie/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/ru/newbie/res/mipmap-xxxhdpi/ic_launcher.png b/src/ru/newbie/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..7b67a0974
Binary files /dev/null and b/src/ru/newbie/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/ru/newbie/res/web_hi_res_512.png b/src/ru/newbie/res/web_hi_res_512.png
new file mode 100644
index 000000000..f0556b7be
Binary files /dev/null and b/src/ru/newbie/res/web_hi_res_512.png differ
diff --git a/src/ru/newbie/src/eu/kanade/tachiyomi/extension/ru/newbie/Newbie.kt b/src/ru/newbie/src/eu/kanade/tachiyomi/extension/ru/newbie/Newbie.kt
new file mode 100644
index 000000000..d30202225
--- /dev/null
+++ b/src/ru/newbie/src/eu/kanade/tachiyomi/extension/ru/newbie/Newbie.kt
@@ -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>(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 {
+ 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(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 {
+ val chapters = json.decodeFromString>>(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 {
+ val body = response.body?.string()!!
+ val pages = json.decodeFromString>(body)
+ val result = mutableListOf()
+ 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 = throw Exception("Not used")
+ override fun fetchPageList(chapter: SChapter): Observable> {
+ return client.newCall(pageListRequest(chapter))
+ .asObservableSuccess()
+ .map { response ->
+ pageListParse(response, chapter)
+ }
+ }
+ override fun fetchImageUrl(page: Page): Observable = 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) : Filter.Group("Типы", types)
+ private class StatusList(statuses: List) : Filter.Group("Статус", statuses)
+ private class GenreList(genres: List) : Filter.Group("Жанры", 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()
+}
diff --git a/src/ru/newbie/src/eu/kanade/tachiyomi/extension/ru/newbie/dto/Dto.kt b/src/ru/newbie/src/eu/kanade/tachiyomi/extension/ru/newbie/dto/Dto.kt
new file mode 100644
index 000000000..0133ebe9f
--- /dev/null
+++ b/src/ru/newbie/src/eu/kanade/tachiyomi/extension/ru/newbie/dto/Dto.kt
@@ -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,
+ val type: String,
+ val status: String,
+ val rating: Float,
+ val adult: String
+)
+
+@Serializable
+data class PageWrapperDto(
+ val items: List,
+)
+
+@Serializable
+data class SeriesWrapperDto(
+ 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?
+)