Add Newgrounds.com #877

Merged
wasu-code merged 16 commits from newgrounds into main 2025-04-10 21:31:27 -05:00
2 changed files with 281 additions and 82 deletions
Showing only changes of commit a7a3c5db64 - Show all commits

View file

@ -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",
)
}
}

View file

@ -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",
)