Add Random Scans source (#8289)
* Add Random Scans source * Reverse Comikey typo fix; will put in another PR
This commit is contained in:
parent
e0e24f149f
commit
914d23395b
|
@ -0,0 +1,36 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="eu.kanade.tachiyomi.extension"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application>
|
||||
<activity
|
||||
android:name=".en.randomscans.RandomScansURLActivity"
|
||||
android:excludeFromRecents="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:host="wearerandomscans.wordpress.com"
|
||||
android:pathPattern="/projects/..*"
|
||||
android:scheme="https" />
|
||||
<data
|
||||
android:host="wearerandomscans.wordpress.com"
|
||||
android:pathPattern="/..*-..*"
|
||||
android:scheme="https" />
|
||||
|
||||
<data
|
||||
android:host="www.wearerandomscans.wordpress.com"
|
||||
android:pathPattern="/projects/..*"
|
||||
android:scheme="https" />
|
||||
<data
|
||||
android:host="www.wearerandomscans.wordpress.com"
|
||||
android:pathPattern="/..*-..*"
|
||||
android:scheme="https" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
|
@ -0,0 +1,12 @@
|
|||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
ext {
|
||||
extName = 'Random Scans'
|
||||
pkgNameSuffix = 'en.randomscans'
|
||||
extClass = '.RandomScans'
|
||||
extVersionCode = 1
|
||||
libVersion = '1.2'
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
Binary file not shown.
After Width: | Height: | Size: 4.6 KiB |
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
Binary file not shown.
After Width: | Height: | Size: 35 KiB |
Binary file not shown.
After Width: | Height: | Size: 59 KiB |
Binary file not shown.
After Width: | Height: | Size: 529 KiB |
|
@ -0,0 +1,279 @@
|
|||
package eu.kanade.tachiyomi.extension.en.randomscans
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
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 eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
|
||||
class RandomScans : HttpSource() {
|
||||
override val name = "Random Scans"
|
||||
|
||||
override val baseUrl = "https://wearerandomscans.wordpress.com"
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
override val supportsLatest = false
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient
|
||||
|
||||
companion object {
|
||||
const val SLUG_SEARCH_PREFIX = "slug:"
|
||||
}
|
||||
|
||||
// == Main site functions ==
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request = throw(UnsupportedOperationException("Not used"))
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage = throw(UnsupportedOperationException("Not used"))
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request = GET("$baseUrl/projects", headers)
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val mangaList: List<SManga> = response.asJsoup()
|
||||
.select("main#main div.wp-block-media-text")
|
||||
.map { element ->
|
||||
SManga.create().apply {
|
||||
// url is absolute
|
||||
url = element.selectFirst("a[href]:containsOwn(Chapter List), a[href]:containsOwn(Read Here)").attr("href")
|
||||
|
||||
// thumbnail_url is absolute
|
||||
thumbnail_url = element.selectFirst("img[src]").attr("src")
|
||||
|
||||
title = element.selectFirst("p.has-large-font-size > strong").text()
|
||||
|
||||
description = element.selectFirst("p:containsOwn(Author/Artist) + p")?.text()
|
||||
|
||||
val details = element.selectFirst("p:containsOwn(Author/Artist)").html().split("<br>")
|
||||
|
||||
author = details.find { it.contains("Author/Artist: ") }
|
||||
?.substringAfterLast("Author/Artist: ")
|
||||
|
||||
artist = author
|
||||
|
||||
genre = details.find { it.contains("Genres: ") }
|
||||
?.substringAfterLast("Genres: ")
|
||||
?.replace(",", ", ")
|
||||
|
||||
status = when (element.selectFirst("p.has-large-font-size:containsOwn(Status: )")?.text()?.substringAfterLast("Status: ")) {
|
||||
"Ongoing" -> SManga.ONGOING
|
||||
"Completed" -> SManga.COMPLETED
|
||||
"Hiatus" -> {
|
||||
description = "Status: Hiatus\n\n$description"
|
||||
SManga.ONGOING
|
||||
}
|
||||
"Dropped" -> {
|
||||
description = "Status: Dropped\n\n$description"
|
||||
SManga.COMPLETED // Not sure what the best status is for "Dropped"
|
||||
}
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
initialized = true // we have all of the fields
|
||||
}
|
||||
}
|
||||
return MangasPage(mangaList, false)
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = throw UnsupportedOperationException("Not used")
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage = throw UnsupportedOperationException("Not used")
|
||||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
if (query.startsWith(SLUG_SEARCH_PREFIX)) {
|
||||
var slug = query.removePrefix(SLUG_SEARCH_PREFIX)
|
||||
|
||||
// remove trailing digits from chapter links
|
||||
while (slug.substringAfterLast('-', "").toIntOrNull() != null) {
|
||||
slug = slug.substringBeforeLast('-')
|
||||
}
|
||||
|
||||
val manga = SManga.create().apply {
|
||||
url = "$baseUrl/projects/$slug"
|
||||
}
|
||||
|
||||
return fetchMangaDetails(manga).map {
|
||||
MangasPage(listOf(it), false)
|
||||
}
|
||||
}
|
||||
return fetchPopularManga(page).map {
|
||||
mangasPage ->
|
||||
MangasPage(
|
||||
mangasPage.mangas.filter {
|
||||
it.title.contains(query, true)
|
||||
},
|
||||
mangasPage.hasNextPage
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// == Manga functions ==
|
||||
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
return client.newCall(mangaDetailsRequest(manga))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
if (isOneshot(manga)) {
|
||||
oneshotDetailsParse(response).copyFromCustom(manga)
|
||||
} else {
|
||||
mangaDetailsParse(response)
|
||||
}.apply {
|
||||
initialized = true
|
||||
url = manga.url
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga): Request = GET(manga.url, headers)
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
return SManga.create().apply {
|
||||
val element = response.asJsoup().selectFirst("div.wp-block-media-text")
|
||||
|
||||
// thumbnail_url is absolute
|
||||
thumbnail_url = element.selectFirst("figure > img[src]").attr("src")
|
||||
|
||||
title = element.selectFirst("p.has-huge-font-size").text()
|
||||
|
||||
genre = element.selectFirst("p.has-huge-font-size + p")?.text()?.replace(" • ", ", ")
|
||||
|
||||
description = element.selectFirst("p.has-huge-font-size + p + p")?.text()
|
||||
|
||||
author = element.selectFirst("p:containsOwn(Author/Artist: )").text().removePrefix("Author/Artist: ")
|
||||
|
||||
artist = author
|
||||
|
||||
status = when (element.selectFirst("p:containsOwn(Format: ) + p")?.text()?.substringAfterLast("• ")) {
|
||||
"Ongoing" -> SManga.ONGOING
|
||||
"Completed" -> SManga.COMPLETED
|
||||
"Hiatus" -> {
|
||||
description = "Status: Hiatus\n\n$description"
|
||||
SManga.ONGOING
|
||||
}
|
||||
"Dropped" -> {
|
||||
description = "Status: Dropped\n\n$description"
|
||||
SManga.COMPLETED // Not sure what the best status is for "Dropped"
|
||||
}
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// oneshots have manga pages which directly link to the chapter page, and have dates in their url like a chapter page
|
||||
private fun isOneshot(manga: SManga): Boolean = manga.url.contains(Regex("/20\\d\\d/\\d\\d/\\d\\d"))
|
||||
|
||||
// We can only get the title
|
||||
private fun oneshotDetailsParse(response: Response): SManga {
|
||||
return SManga.create().apply {
|
||||
val responseJson = response.asJsoup()
|
||||
title = responseJson.selectFirst("h1.entry-title").text()
|
||||
thumbnail_url = responseJson.selectFirst("img[src]").attr("src")
|
||||
status = SManga.COMPLETED
|
||||
description = "Add this manga from search to fetch details properly"
|
||||
}
|
||||
}
|
||||
|
||||
// the same as SManga.copyFrom, which we can't access, except it doesn't copy status
|
||||
private fun SManga.copyFromCustom(other: SManga): SManga {
|
||||
return this.apply {
|
||||
|
||||
if (other.author != null) {
|
||||
author = other.author
|
||||
}
|
||||
|
||||
if (other.artist != null) {
|
||||
artist = other.artist
|
||||
}
|
||||
|
||||
if (other.description != null) {
|
||||
description = other.description
|
||||
}
|
||||
|
||||
if (other.genre != null) {
|
||||
genre = other.genre
|
||||
}
|
||||
|
||||
if (other.thumbnail_url != null) {
|
||||
thumbnail_url = other.thumbnail_url
|
||||
}
|
||||
|
||||
if (!initialized) {
|
||||
initialized = other.initialized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
if (isOneshot(manga)) {
|
||||
return Observable.just(
|
||||
listOf(
|
||||
SChapter.create().apply {
|
||||
// the "manga" page for a oneshot is really more like a chapter page
|
||||
url = manga.url
|
||||
|
||||
name = "Oneshot"
|
||||
|
||||
date_upload = System.currentTimeMillis() - 1000
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return super.fetchChapterList(manga)
|
||||
}
|
||||
|
||||
override fun chapterListRequest(manga: SManga): Request = GET(manga.url, headers)
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
return response.asJsoup()
|
||||
.select("main#main li > a[href]")
|
||||
.mapIndexed { index, element ->
|
||||
SChapter.create().apply {
|
||||
// url is absolute
|
||||
url = element.attr("href")
|
||||
|
||||
name = element.text()
|
||||
|
||||
// chapters are set as uploaded 1 millisecond apart,
|
||||
// so that users can still sort by date uploaded
|
||||
date_upload = System.currentTimeMillis() - 1000 + index
|
||||
}
|
||||
}.reversed()
|
||||
}
|
||||
|
||||
// == Chapter functions ==
|
||||
|
||||
override fun pageListRequest(chapter: SChapter): Request = GET(chapter.url, headers)
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val reponseJson = response.asJsoup()
|
||||
|
||||
val list = reponseJson.select("figure > img").mapIndexed { index, element ->
|
||||
Page(index, "", element.attr("src"))
|
||||
}.toMutableList()
|
||||
|
||||
if (reponseJson.selectFirst("iframe[src*=drive.google.com/file]") != null) {
|
||||
list.add(Page(list.last().index + 1, "", "https://fakeimg.pl/1800x2252/FFFFFF/000000/?font_size=110&text=Some%20images%20in%20this%20chapter%20could%20not%20be%20loaded%20%0Abecause%20they%20are%20stored%20in%20a%20Google%20Drive%20PDF.%20%0APlease%20open%20the%20chapter%20in%20web%20view%20to%20view%20them."))
|
||||
}
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
// == Page functions ==
|
||||
|
||||
override fun fetchImageUrl(page: Page): Observable<String> {
|
||||
return Observable.just(page.imageUrl)
|
||||
}
|
||||
|
||||
override fun imageUrlRequest(page: Page): Request = throw UnsupportedOperationException("Not used")
|
||||
|
||||
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException("Not used")
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package eu.kanade.tachiyomi.extension.en.randomscans
|
||||
|
||||
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 RandomScansURLActivity : Activity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val pathSegments: List<String>? = intent?.data?.pathSegments
|
||||
if (!pathSegments.isNullOrEmpty()) {
|
||||
|
||||
val mainIntent = Intent().apply {
|
||||
action = "eu.kanade.tachiyomi.SEARCH"
|
||||
// pathSegments.last() seemed to be crashing, it's beyond me why
|
||||
putExtra("query", RandomScans.SLUG_SEARCH_PREFIX + pathSegments[pathSegments.size - 1])
|
||||
putExtra("filter", packageName)
|
||||
}
|
||||
|
||||
try {
|
||||
startActivity(mainIntent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Log.e("RandomScansURLActivity", "failed to start activity with error: $e")
|
||||
}
|
||||
} else {
|
||||
Log.e("RandomScansURLActivity", "could not parse uri from intent $intent")
|
||||
}
|
||||
|
||||
finish()
|
||||
exitProcess(0)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue