Add Arc-Relight source (#636)

[DNM] Add Arc-Relight source
This commit is contained in:
ObserverOfTime 2018-11-26 16:15:14 +02:00 committed by Carlos
parent 99fce4c23e
commit 058d5905f0
10 changed files with 317 additions and 0 deletions

View File

@ -0,0 +1,15 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
ext {
appName = 'Tachiyomi: Arc-Relight'
pkgNameSuffix = 'en.arcrelight'
extClass = '.ArcRelight'
extVersionCode = 1
extVersionSuffix = 1
libVersion = '1.2'
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@ -0,0 +1,64 @@
package eu.kanade.tachiyomi.extension.en.arcrelight
import eu.kanade.tachiyomi.source.model.Filter
/** Array containing the possible statuses of a manga */
private val STATUSES = arrayOf("Any", "Completed", "Ongoing")
/** List containing the possible categories of a manga */
private val CATEGORIES = listOf(
Category("4-Koma"),
Category("Chaos;Head"),
Category("Comedy"),
Category("Drama"),
Category("Mystery"),
Category("Psychological"),
Category("Robotics;Notes"),
Category("Romance"),
Category("Sci-Fi"),
Category("Seinen"),
Category("Shounen"),
Category("Steins;Gate"),
Category("Supernatural"),
Category("Tragedy")
)
/**
* Filter representing the status of a manga.
*
* @constructor Creates a [Filter.Select] object with [STATUSES].
*/
class Status : Filter.Select<String>("Status", STATUSES) {
/** Returns the [state] as a string. */
fun string() = values[state].toLowerCase()
}
/**
* Filter representing a manga category.
*
* @property name The display name of the category.
* @constructor Creates a [Filter.TriState] object using [name].
*/
class Category(name: String) : Filter.TriState(name) {
/** Returns the [state] as a string, or null if [isIgnored]. */
fun stringOpt() = when(state) {
STATE_INCLUDE -> name.toLowerCase()
STATE_EXCLUDE -> "-" + name.toLowerCase()
else -> null
}
}
/**
* Filter representing the [categories][Category] of a manga.
*
* @constructor Creates a [Filter.Group] object with [CATEGORIES].
*/
class CategoryList : Filter.Group<Category>("Categories", CATEGORIES)
/**
* Filter representing the name of an author or artist.
*
* @constructor Creates a [Filter.Text] object.
*/
class Person : Filter.Text("Author/Artist")

View File

@ -0,0 +1,89 @@
package eu.kanade.tachiyomi.extension.en.arcrelight
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import org.json.JSONArray
import org.json.JSONObject
import java.lang.IllegalArgumentException
import java.text.DecimalFormat
import java.text.SimpleDateFormat
import java.util.Locale
/**
* The HTTP date format specified in
* [RFC 1123](https://tools.ietf.org/html/rfc1123#page-55).
*/
private const val HTTP_DATE = "EEE, dd MMM yyyy HH:mm:ss zzz"
/**
* Converts a date in the [HTTP_DATE] format to a Unix timestamp.
*
* @param date The date to convert.
* @return The timestamp of the date.
*/
fun httpDateToTimestamp(date: String) =
SimpleDateFormat(HTTP_DATE, Locale.US).parse(date).time
/**
* Joins each value of a given [field] of the array using [sep].
*
* @param field
* When its type is [Int], it is treated as the index of a [JSONArray].
* When its type is [String], it is treated as the key of a [JSONObject].
* @param sep The separator used to join the array.
* @return The joined string, or null if the array is empty.
* @throws IllegalArgumentException when [field] is of an invalid type.
*/
fun JSONArray.joinField(field: Any, sep: String = ", "): String? {
if (!(field is Int || field is String))
throw IllegalArgumentException("field must be a String or Int")
if (this.length() == 0) return null
val list = mutableListOf<String>()
for (i in 0 until this.length()) {
when (field) {
is Int -> list.add(this.getJSONArray(i).getString(field))
is String -> list.add(this.getJSONObject(i).getString(field))
}
}
return list.joinToString(sep)
}
/**
* Creates a [SManga] by parsing a [JSONObject].
*
* @param obj The object containing the manga info.
*/
fun SManga.fromJSON(obj: JSONObject) {
url = obj.getString("url")
title = obj.getString("title")
description = obj.getString("description")
thumbnail_url = obj.getString("cover")
author = obj.getJSONArray("authors")?.joinField(0)
artist = obj.getJSONArray("artists")?.joinField(0)
genre = obj.getJSONArray("categories")?.joinField("name")
status = when (obj.getBoolean("completed")) {
true -> SManga.COMPLETED
false -> SManga.ONGOING
}
}
/**
* Creates a [SChapter] by parsing a [JSONObject].
*
* @param obj The object containing the chapter info.
*/
fun SChapter.fromJSON(obj: JSONObject) {
url = obj.getString("url")
chapter_number = obj.getString("chapter").toFloat()
date_upload = httpDateToTimestamp(obj.getString("date"))
scanlator = obj.getJSONArray("groups")?.joinField("name", " & ")
val vol = obj.getString("volume")
val ch = DecimalFormat("0.#").format(chapter_number)
name = buildString {
if (vol != "0") append("Vol.$vol ")
append("Ch.$ch - ")
append(obj.getString("title"))
if (obj.getBoolean("final")) append(" [END]")
}
}

View File

@ -0,0 +1,149 @@
package eu.kanade.tachiyomi.extension.en.arcrelight
import android.net.Uri
import android.os.Build.VERSION
import eu.kanade.tachiyomi.extension.BuildConfig
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.*
import eu.kanade.tachiyomi.source.online.HttpSource
import org.json.JSONArray
import org.json.JSONObject
import okhttp3.Headers
import okhttp3.Request
import okhttp3.Response
/** Arc-Relight source */
class ArcRelight : HttpSource() {
override val versionId = 1
override val name = "Arc-Relight"
override val baseUrl = "https://arc-relight.site/api/v$versionId"
override val lang = "en"
override val supportsLatest = true
/**
* A user agent representing Tachiyomi.
* Includes the user's Android version
* and the current extension version.
*/
private val userAgent = "Mozilla/5.0 (" +
"Android ${VERSION.RELEASE}; Mobile) " +
"Tachiyomi/${BuildConfig.VERSION_NAME}"
override fun headersBuilder() = Headers.Builder().apply {
add("User-Agent", userAgent)
add("Referer", baseUrl)
}
override fun latestUpdatesRequest(page: Int) =
GET("$baseUrl/releases/", headers)
override fun pageListRequest(chapter: SChapter) =
GET(Uri.parse(chapter.url).path.replace(
"/reader/", "$baseUrl/series/"), headers)
override fun mangaDetailsRequest(manga: SManga) =
GET("$baseUrl/series/${manga.url.split("/")
.last { it != "" }}/", headers)
override fun chapterListRequest(manga: SManga) =
mangaDetailsRequest(manga)
override fun searchMangaRequest(page: Int, query: String,
filters: FilterList): Request {
val uri = Uri.parse("$baseUrl/series/").buildUpon()
uri.appendQueryParameter("q", query)
val cat = mutableListOf<String>()
filters.forEach {
when (it) {
is Person -> uri.appendQueryParameter("author", it.state)
is Status -> uri.appendQueryParameter("status", it.string())
is CategoryList -> cat.addAll(it.state.mapNotNull {
c -> Uri.encode(c.stringOpt())
})
}
}
return GET("$uri&categories=${cat.joinToString(",")}", headers)
}
override fun latestUpdatesParse(response: Response): MangasPage {
val arr = JSONArray(response.body()!!.string())
val ret = ArrayList<SManga>(arr.length())
for (i in 0 until arr.length()) {
val obj = arr.getJSONObject(i)
ret.add(SManga.create().apply {
url = obj.getString("url")
title = obj.getString("title")
thumbnail_url = obj.getString("cover")
})
}
return MangasPage(ret, false)
}
override fun chapterListParse(response: Response): List<SChapter> {
val res = JSONObject(response.body()!!.string())
val volumes = res.getJSONObject("volumes")
val ret = mutableListOf<SChapter>()
volumes.keys().forEach { vol ->
val chapters = volumes.getJSONObject(vol)
chapters.keys().forEach { ch ->
val obj = chapters.getJSONObject(ch)
obj.put("chapter", ch)
obj.put("volume", vol)
ret.add(SChapter.create().apply { fromJSON(obj) })
}
}
return ret.sortedByDescending { it.name }
}
override fun mangaDetailsParse(response: Response) =
SManga.create().apply {
fromJSON(JSONObject(response.body()!!.string()))
}
override fun pageListParse(response: Response): List<Page> {
val obj = JSONObject(response.body()!!.string())
val url = obj.getString("url")
val root = obj.getString("pages_root")
val arr = obj.getJSONArray("pages_list")
val ret = mutableListOf<Page>()
for (i in 0 until arr.length()) {
ret.add(Page(i, "$url${i + 1}", root + arr.getString(i)))
}
return ret
}
override fun searchMangaParse(response: Response): MangasPage {
val arr = JSONArray(response.body()!!.string())
val ret = mutableListOf<SManga>()
for (i in 0 until arr.length()) {
ret.add(SManga.create().apply {
fromJSON(arr.getJSONObject(i))
})
}
return MangasPage(ret.sortedBy { it.title }, false)
}
override fun getFilterList() = FilterList(
Person(), Status(), CategoryList()
)
override fun fetchPopularManga(page: Int) =
fetchSearchManga(page, "", FilterList())
override fun popularMangaRequest(page: Int) =
throw UnsupportedOperationException(
"This method should not be called!")
override fun popularMangaParse(response: Response) =
throw UnsupportedOperationException(
"This method should not be called!")
override fun imageUrlParse(response: Response) =
throw UnsupportedOperationException(
"This method should not be called!")
}