Add Newgrounds.com #877
2 changed files with 281 additions and 82 deletions
|
@ -6,22 +6,20 @@ import android.widget.Toast
|
|||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimesPage
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import eu.kanade.tachiyomi.util.parseAs
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.json.JSONObject
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
import tryParse
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
@ -29,6 +27,8 @@ import java.text.SimpleDateFormat
|
|||
import java.util.Locale
|
||||
import java.util.regex.Pattern
|
||||
|
||||
private const val SEARCH_PAGE_SIZE = 20
|
||||
|
||||
class NewGrounds : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
|
||||
|
||||
override val lang = "all"
|
||||
|
@ -50,81 +50,35 @@ class NewGrounds : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
|
|||
.build()
|
||||
}
|
||||
|
||||
private fun creatorUrl(username: String) = baseUrl.replaceFirst("www", username)
|
||||
|
||||
private fun animeFromElement(element: Element, section: String): SAnime {
|
||||
return if (section == PREF_SECTIONS["Your Feed"]) {
|
||||
SAnime.create().apply {
|
||||
title = element.selectFirst(".detail-title h4")!!.text()
|
||||
author = element.selectFirst(".detail-title strong")?.text()
|
||||
description = element.selectFirst(".detail-description")?.text()
|
||||
thumbnail_url = element.selectFirst(".item-icon img")?.absUrl("src")
|
||||
setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href"))
|
||||
}
|
||||
} else {
|
||||
SAnime.create().apply {
|
||||
title = element.selectFirst(".card-title h4")!!.text()
|
||||
author = element.selectFirst(".card-title span")?.text()?.replace("By ", "")
|
||||
description = element.selectFirst("a")?.attr("title")
|
||||
thumbnail_url = element.selectFirst("img")?.absUrl("src")
|
||||
setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Latest
|
||||
|
||||
private val latestSection = preferences.getString("LATEST", PREF_SECTIONS["Latest"])!!
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/$latestSection", headers)
|
||||
|
||||
override fun latestUpdatesNextPageSelector(): String? = null
|
||||
|
||||
override fun latestUpdatesSelector(): String {
|
||||
return if (latestSection == PREF_SECTIONS["Your Feed"]) {
|
||||
"a.item-portalsubmission"
|
||||
} else {
|
||||
"a.inline-card-portalsubmission"
|
||||
}
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
val offset = (page - 1) * SEARCH_PAGE_SIZE
|
||||
return GET("$baseUrl/$latestSection?offset=$offset", headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesNextPageSelector(): String = "#load-more-items a"
|
||||
|
||||
override fun latestUpdatesSelector(): String = animeSelector(latestSection)
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SAnime {
|
||||
return animeFromElement(element, latestSection)
|
||||
}
|
||||
|
||||
// override suspend fun getLatestUpdates(page: Int): AnimesPage {
|
||||
// val data = client.newCall(GET("$baseUrl")).awaitSuccess()
|
||||
// val document = data.parseAs<Document>()
|
||||
//
|
||||
// val animeList = document.select(latestUpdatesSelector()).map { element ->
|
||||
// animeFromElement(element, latestSection)
|
||||
// }
|
||||
//
|
||||
// return AnimesPage(animeList, hasNextPage = true)
|
||||
//
|
||||
// }
|
||||
|
||||
// Browse
|
||||
|
||||
private val popularSection = preferences.getString("POPULAR", PREF_SECTIONS["Popular"])!!
|
||||
|
||||
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/$popularSection", headers)
|
||||
|
||||
override fun popularAnimeNextPageSelector(): String? = null
|
||||
|
||||
override suspend fun getPopularAnime(page: Int): AnimesPage {
|
||||
val offset = 20
|
||||
//TODO
|
||||
return super.getPopularAnime(page)
|
||||
override fun popularAnimeRequest(page: Int): Request {
|
||||
val offset = (page - 1) * SEARCH_PAGE_SIZE
|
||||
return GET("$baseUrl/$popularSection?offset=$offset", headers)
|
||||
}
|
||||
|
||||
override fun popularAnimeSelector(): String {
|
||||
return if (latestSection == PREF_SECTIONS["Your Feed"]) {
|
||||
"a.item-portalsubmission"
|
||||
} else {
|
||||
"a.inline-card-portalsubmission"
|
||||
}
|
||||
}
|
||||
override fun popularAnimeNextPageSelector(): String = "#load-more-items a"
|
||||
|
||||
override fun popularAnimeSelector(): String = animeSelector(popularSection)
|
||||
|
||||
override fun popularAnimeFromElement(element: Element): SAnime {
|
||||
return animeFromElement(element, popularSection)
|
||||
|
@ -133,27 +87,108 @@ class NewGrounds : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
|
|||
// Search
|
||||
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
TODO("Not yet implemented")
|
||||
val searchUrl = "$baseUrl/search/conduct/movies".toHttpUrl().newBuilder()
|
||||
.addQueryParameter("page", page.toString())
|
||||
|
||||
if (query.isNotEmpty()) searchUrl.addQueryParameter("terms", query)
|
||||
|
||||
filters.findInstance<MatchAgainstFilter>().ifFilterSet {
|
||||
searchUrl.addQueryParameter("match", MATCH_AGAINST.values.elementAt(it.state))
|
||||
}
|
||||
filters.findInstance<TuningFilterGroup>()?.state
|
||||
?.findInstance<TuningExactFilter>().ifFilterSet {
|
||||
searchUrl.addQueryParameter("exact", "1")
|
||||
}
|
||||
filters.findInstance<TuningFilterGroup>()?.state
|
||||
?.findInstance<TuningAnyFilter>().ifFilterSet {
|
||||
searchUrl.addQueryParameter("any", "1")
|
||||
}
|
||||
filters.findInstance<AuthorFilter>().ifFilterSet {
|
||||
searchUrl.addQueryParameter("user", it.state)
|
||||
}
|
||||
filters.findInstance<GenreFilter>().ifFilterSet {
|
||||
searchUrl.addQueryParameter("genre", GENRE.values.elementAt(it.state))
|
||||
}
|
||||
filters.findInstance<LengthFilterGroup>()?.state
|
||||
?.findInstance<MinLengthFilter>().ifFilterSet {
|
||||
searchUrl.addQueryParameter("min_length", it.state)
|
||||
}
|
||||
filters.findInstance<LengthFilterGroup>()?.state
|
||||
?.findInstance<MaxLengthFilter>().ifFilterSet {
|
||||
searchUrl.addQueryParameter("max_length", it.state)
|
||||
}
|
||||
filters.findInstance<FrontpagedFilter>().ifFilterSet {
|
||||
searchUrl.addQueryParameter("frontpaged", "1")
|
||||
}
|
||||
filters.findInstance<DateFilterGroup>()?.state
|
||||
?.findInstance<AfterDateFilter>().ifFilterSet {
|
||||
searchUrl.addQueryParameter("after", it.state)
|
||||
}
|
||||
filters.findInstance<DateFilterGroup>()?.state
|
||||
?.findInstance<BeforeDateFilter>().ifFilterSet {
|
||||
searchUrl.addQueryParameter("before", it.state)
|
||||
}
|
||||
// filters.findInstance<RatingFilter>().ifFilterSet {
|
||||
// searchUrl.addQueryParameter("", "")
|
||||
// }
|
||||
filters.findInstance<SortingFilter>().ifFilterSet {
|
||||
searchUrl.addQueryParameter("sort", SORTING.values.elementAt(it.state))
|
||||
}
|
||||
filters.findInstance<TagsFilter>().ifFilterSet {
|
||||
searchUrl.addQueryParameter("tags", it.state)
|
||||
}
|
||||
|
||||
Log.d("Tst", "$searchUrl")
|
||||
|
||||
return GET(searchUrl.build(), headers)
|
||||
}
|
||||
|
||||
override fun searchAnimeNextPageSelector(): String? {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
override fun searchAnimeNextPageSelector(): String = "#results-load-more"
|
||||
|
||||
override fun searchAnimeSelector(): String {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
override fun searchAnimeSelector(): String = "ul.itemlist li a"
|
||||
|
||||
override fun searchAnimeFromElement(element: Element): SAnime {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
override fun searchAnimeFromElement(element: Element): SAnime = animeFromListElement(element)
|
||||
|
||||
// Etc.
|
||||
|
||||
override fun animeDetailsParse(document: Document): SAnime {
|
||||
fun getStarRating(): String {
|
||||
val score: Double = document.selectFirst("#score_number")?.text()?.toDouble() ?: 0.0
|
||||
val fullStars = score.toInt()
|
||||
val hasHalfStar = (score % 1) >= 0.5
|
||||
val totalStars = if (hasHalfStar) fullStars + 1 else fullStars
|
||||
val emptyStars = 5 - totalStars
|
||||
|
||||
return "✪".repeat(fullStars) + (if (hasHalfStar) "✪" else "") + "⬤".repeat(emptyStars) + " ($score)"
|
||||
}
|
||||
|
||||
fun getAdultRating(): String {
|
||||
val rating = document.selectFirst("#embed_header h2")!!.className().substringAfter("rated-")
|
||||
return when (rating) {
|
||||
"e" -> "🟩 Everyone"
|
||||
"t" -> "🟦 Ages 13+"
|
||||
"m" -> "🟪 Ages 17+"
|
||||
"a" -> "🟥 Adults Only"
|
||||
else -> "❓"
|
||||
}
|
||||
}
|
||||
|
||||
fun getStats(): String {
|
||||
val statsElement = document.selectFirst("#sidestats > dl:first-of-type")
|
||||
val views = statsElement?.selectFirst("dd:first-of-type")?.text() ?: "?"
|
||||
val faves = statsElement?.selectFirst("dd:nth-of-type(2)")?.text() ?: "?"
|
||||
val votes = statsElement?.selectFirst("dd:nth-of-type(3)")?.text() ?: "?"
|
||||
|
||||
return "👀 $views | ❤️ $faves | 👍 $votes"
|
||||
}
|
||||
|
||||
return SAnime.create().apply {
|
||||
title = document.selectFirst("h2[itemprop=\"name\"]")!!.text()
|
||||
description = document.selectFirst("meta[itemprop=\"description\"]")?.attr("content")
|
||||
description = """
|
||||
${document.selectFirst("meta[itemprop=\"description\"]")?.attr("content")}
|
||||
|
||||
${getAdultRating()} | ${getStarRating()} | ${getStats()}
|
||||
""".trimIndent()
|
||||
author = document.selectFirst(".authorlinks > div:first-of-type .item-details-main")?.text()
|
||||
artist = document.select(".authorlinks > div:not(:first-of-type) .item-details-main").joinToString {
|
||||
it.text()
|
||||
|
@ -170,7 +205,7 @@ class NewGrounds : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
|
|||
override fun episodeFromElement(element: Element): SEpisode = throw UnsupportedOperationException("Not Used")
|
||||
|
||||
private fun extractEpisodeIdFromScript(element: Element?): String? {
|
||||
val regex = """data-movie-id=\\\"(\d+)\\\""""
|
||||
val regex = """data-movie-id=\\"(\d+)\\""""
|
||||
val scriptContent = element!!.html().toString()
|
||||
|
||||
val pattern = Pattern.compile(regex, Pattern.MULTILINE)
|
||||
|
@ -199,13 +234,9 @@ class NewGrounds : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
|
|||
)
|
||||
}
|
||||
|
||||
override fun videoListRequest(episode: SEpisode): Request {
|
||||
Log.d("Tst", videoListHeaders.toString())
|
||||
return GET("$baseUrl${episode.url}", videoListHeaders)
|
||||
}
|
||||
override fun videoListRequest(episode: SEpisode): Request = GET("$baseUrl${episode.url}", videoListHeaders)
|
||||
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
Log.d("Tst", response.toString())
|
||||
val responseBody = response.body.string()
|
||||
val json = JSONObject(responseBody)
|
||||
val sources = json.getJSONObject("sources")
|
||||
|
@ -238,6 +269,21 @@ class NewGrounds : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
|
|||
|
||||
override fun videoUrlParse(document: Document): String = throw UnsupportedOperationException("Not Used")
|
||||
|
||||
// ============================== Filters ===============================
|
||||
|
||||
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
|
||||
MatchAgainstFilter(),
|
||||
TuningFilterGroup(),
|
||||
AuthorFilter(),
|
||||
GenreFilter(),
|
||||
LengthFilterGroup(),
|
||||
FrontpagedFilter(),
|
||||
DateFilterGroup(),
|
||||
TagsFilter(),
|
||||
// RatingFilter(),
|
||||
SortingFilter(),
|
||||
)
|
||||
|
||||
// ============================ Preferences =============================
|
||||
/*
|
||||
According to the labels on the website:
|
||||
|
@ -274,19 +320,91 @@ class NewGrounds : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
|
|||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val selected = newValue as String
|
||||
Toast.makeText(screen.context, "Restart Aniyomi to apply new setting.", Toast.LENGTH_LONG).show()
|
||||
Toast.makeText(screen.context, "Restart app to apply new setting.", Toast.LENGTH_LONG).show()
|
||||
preferences.edit().putString(key, selected).commit()
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
}
|
||||
|
||||
// ========================== Helper Functions ==========================
|
||||
|
||||
private fun creatorUrl(username: String) = baseUrl.replaceFirst("www", username)
|
||||
|
||||
/**
|
||||
* Chooses an extraction technique for anime information, based on section selected in Preferences
|
||||
*/
|
||||
private fun animeFromElement(element: Element, section: String): SAnime {
|
||||
return if (section == PREF_SECTIONS["Your Feed"]) {
|
||||
animeFromFeedElement(element)
|
||||
} else {
|
||||
animeFromGridElement(element)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts anime information from element of list typical for /popular, /browse or /featured
|
||||
*/
|
||||
private fun animeFromGridElement(element: Element): SAnime = SAnime.create().apply {
|
||||
title = element.selectFirst(".card-title h4")!!.text()
|
||||
author = element.selectFirst(".card-title span")?.text()?.replace("By ", "")
|
||||
description = element.selectFirst("a")?.attr("title")
|
||||
thumbnail_url = element.selectFirst("img")?.absUrl("src")
|
||||
setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href"))
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts anime information from element of list returned in Your Feed
|
||||
*/
|
||||
private fun animeFromFeedElement(element: Element): SAnime = SAnime.create().apply {
|
||||
title = element.selectFirst(".detail-title h4")!!.text()
|
||||
author = element.selectFirst(".detail-title strong")?.text()
|
||||
description = element.selectFirst(".detail-description")?.text()
|
||||
thumbnail_url = element.selectFirst(".item-icon img")?.absUrl("src")
|
||||
setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href"))
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts anime information from element of list typical for /search or /collection
|
||||
*/
|
||||
private fun animeFromListElement(element: Element): SAnime = SAnime.create().apply {
|
||||
title = element.selectFirst(".detail-title > h4")!!.text()
|
||||
author = element.selectFirst(".detail-title > span > strong")?.text()
|
||||
description = element.selectFirst(".detail-description")?.text()
|
||||
thumbnail_url = element.selectFirst(".item-icon img")?.absUrl("src")
|
||||
setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href"))
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns CSS selector for anime, based on the section selected in Preferences
|
||||
*/
|
||||
private fun animeSelector(section: String): String {
|
||||
return if (section == PREF_SECTIONS["Your Feed"]) {
|
||||
"a.item-portalsubmission"
|
||||
} else {
|
||||
"a.inline-card-portalsubmission"
|
||||
}
|
||||
}
|
||||
|
||||
private fun isAdultContentFiltered(document: Document): Boolean {
|
||||
return document.selectFirst("li:has(.suitable-a)")?.attr("data-disabled") != null
|
||||
}
|
||||
|
||||
private inline fun <reified T> Iterable<*>.findInstance() = find { it is T } as? T
|
||||
|
||||
inline fun <T> T?.ifFilterSet(action: (T) -> Unit) where T : AnimeFilter<*> {
|
||||
val state = this?.state
|
||||
if (this != null && state != null && state != "" && state != 0 && state != false) {
|
||||
action(this)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val PREF_SECTIONS = mapOf(
|
||||
"Featured" to "movies/featured",
|
||||
"Latest" to "movies/browse",
|
||||
"Popular" to "movies/popular",
|
||||
"Your Feed" to "social/feeds/show/favorite-artists-movies",
|
||||
"Under Judgment" to "movies/browse?interval=all&artist-type=unjudged",
|
||||
// "Under Judgment" to "movies/browse?interval=all&artist-type=unjudged",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
package eu.kanade.tachiyomi.animeextension.all.newgrounds
|
||||
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
||||
|
||||
class MatchAgainstFilter : AnimeFilter.Select<String>("Match against", MATCH_AGAINST.keys.toTypedArray(), 0)
|
||||
|
||||
class TuningExactFilter : AnimeFilter.CheckBox("exact matches", false)
|
||||
class TuningAnyFilter : AnimeFilter.CheckBox("match any words", false)
|
||||
class TuningFilterGroup : AnimeFilter.Group<AnimeFilter.CheckBox>(
|
||||
"Tuning",
|
||||
listOf(
|
||||
TuningExactFilter(),
|
||||
TuningAnyFilter(),
|
||||
),
|
||||
)
|
||||
|
||||
class AuthorFilter : AnimeFilter.Text("Author")
|
||||
|
||||
class GenreFilter : AnimeFilter.Select<String>("Genre", GENRE.keys.toTypedArray())
|
||||
|
||||
class MinLengthFilter : AnimeFilter.Text("Min Length")
|
||||
class MaxLengthFilter : AnimeFilter.Text("Max Length")
|
||||
class LengthFilterGroup : AnimeFilter.Group<AnimeFilter.Text>(
|
||||
"Length (00:00:00)",
|
||||
listOf(
|
||||
MinLengthFilter(),
|
||||
MaxLengthFilter(),
|
||||
),
|
||||
)
|
||||
|
||||
class FrontpagedFilter : AnimeFilter.CheckBox("Frontpaged?", false)
|
||||
|
||||
class AfterDateFilter : AnimeFilter.Text("On, or after")
|
||||
class BeforeDateFilter : AnimeFilter.Text("Before")
|
||||
class DateFilterGroup : AnimeFilter.Group<AnimeFilter.Text>(
|
||||
"Dates (YYYY-MM-DD)",
|
||||
listOf(
|
||||
AfterDateFilter(),
|
||||
BeforeDateFilter(),
|
||||
),
|
||||
)
|
||||
|
||||
// class RatingFilter() : AnimeFilter.Select<String>("Ratings (unused)", arrayOf("Everyone", "Ages 13+", "Ages 17+", "Adults Only"), 0)
|
||||
|
||||
class SortingFilter() : AnimeFilter.Select<String>("Sort by", SORTING.keys.toTypedArray())
|
||||
|
||||
class TagsFilter() : AnimeFilter.Text("Tags (comma separated)")
|
||||
|
||||
// ===================================================================
|
||||
val MATCH_AGAINST = mapOf(
|
||||
"Default" to "",
|
||||
"title / description / tags / author" to "tdtu",
|
||||
"title / description / tags" to "tdt",
|
||||
"title / description" to "td",
|
||||
"title" to "t",
|
||||
"description" to "d",
|
||||
)
|
||||
|
||||
val GENRE = mapOf(
|
||||
"All" to "",
|
||||
"Action" to "45",
|
||||
"Comedy - Original" to "60",
|
||||
"Comedy - Parody" to "61",
|
||||
"Drama" to "47",
|
||||
"Experimental" to "49",
|
||||
"Informative" to "48",
|
||||
"Music Video" to "50",
|
||||
"Other" to "51",
|
||||
"Spam" to "55",
|
||||
)
|
||||
|
||||
val SORTING = mapOf(
|
||||
"Default" to "",
|
||||
"Relevance" to "relevance",
|
||||
"Date (Descending)" to "date-desc",
|
||||
"Date (Ascending)" to "date-asc",
|
||||
"Score (Descending)" to "score-desc",
|
||||
"Score (Ascending)" to "score-asc",
|
||||
"Views (Descending)" to "views-desc",
|
||||
"Views (Ascending)" to "views-asc",
|
||||
)
|
Loading…
Add table
Add a link
Reference in a new issue