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.ListPreference
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource 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.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage
import eu.kanade.tachiyomi.animesource.model.SAnime import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parseAs import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.json.JSONObject import org.json.JSONObject
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import rx.Observable
import tryParse import tryParse
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -29,6 +27,8 @@ import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
import java.util.regex.Pattern import java.util.regex.Pattern
private const val SEARCH_PAGE_SIZE = 20
class NewGrounds : ParsedAnimeHttpSource(), ConfigurableAnimeSource { class NewGrounds : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
override val lang = "all" override val lang = "all"
@ -50,81 +50,35 @@ class NewGrounds : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
.build() .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 // Latest
private val latestSection = preferences.getString("LATEST", PREF_SECTIONS["Latest"])!! private val latestSection = preferences.getString("LATEST", PREF_SECTIONS["Latest"])!!
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/$latestSection", headers) override fun latestUpdatesRequest(page: Int): Request {
val offset = (page - 1) * SEARCH_PAGE_SIZE
override fun latestUpdatesNextPageSelector(): String? = null return GET("$baseUrl/$latestSection?offset=$offset", headers)
override fun latestUpdatesSelector(): String {
return if (latestSection == PREF_SECTIONS["Your Feed"]) {
"a.item-portalsubmission"
} else {
"a.inline-card-portalsubmission"
}
} }
override fun latestUpdatesNextPageSelector(): String = "#load-more-items a"
override fun latestUpdatesSelector(): String = animeSelector(latestSection)
override fun latestUpdatesFromElement(element: Element): SAnime { override fun latestUpdatesFromElement(element: Element): SAnime {
return animeFromElement(element, latestSection) 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 // Browse
private val popularSection = preferences.getString("POPULAR", PREF_SECTIONS["Popular"])!! private val popularSection = preferences.getString("POPULAR", PREF_SECTIONS["Popular"])!!
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/$popularSection", headers) override fun popularAnimeRequest(page: Int): Request {
val offset = (page - 1) * SEARCH_PAGE_SIZE
override fun popularAnimeNextPageSelector(): String? = null return GET("$baseUrl/$popularSection?offset=$offset", headers)
override suspend fun getPopularAnime(page: Int): AnimesPage {
val offset = 20
//TODO
return super.getPopularAnime(page)
} }
override fun popularAnimeSelector(): String { override fun popularAnimeNextPageSelector(): String = "#load-more-items a"
return if (latestSection == PREF_SECTIONS["Your Feed"]) {
"a.item-portalsubmission" override fun popularAnimeSelector(): String = animeSelector(popularSection)
} else {
"a.inline-card-portalsubmission"
}
}
override fun popularAnimeFromElement(element: Element): SAnime { override fun popularAnimeFromElement(element: Element): SAnime {
return animeFromElement(element, popularSection) return animeFromElement(element, popularSection)
@ -133,27 +87,108 @@ class NewGrounds : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
// Search // Search
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request { 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? { override fun searchAnimeNextPageSelector(): String = "#results-load-more"
TODO("Not yet implemented")
}
override fun searchAnimeSelector(): String { override fun searchAnimeSelector(): String = "ul.itemlist li a"
TODO("Not yet implemented")
}
override fun searchAnimeFromElement(element: Element): SAnime { override fun searchAnimeFromElement(element: Element): SAnime = animeFromListElement(element)
TODO("Not yet implemented")
}
// Etc. // Etc.
override fun animeDetailsParse(document: Document): SAnime { 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 { return SAnime.create().apply {
title = document.selectFirst("h2[itemprop=\"name\"]")!!.text() 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() 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 { artist = document.select(".authorlinks > div:not(:first-of-type) .item-details-main").joinToString {
it.text() it.text()
@ -170,7 +205,7 @@ class NewGrounds : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
override fun episodeFromElement(element: Element): SEpisode = throw UnsupportedOperationException("Not Used") override fun episodeFromElement(element: Element): SEpisode = throw UnsupportedOperationException("Not Used")
private fun extractEpisodeIdFromScript(element: Element?): String? { private fun extractEpisodeIdFromScript(element: Element?): String? {
val regex = """data-movie-id=\\\"(\d+)\\\"""" val regex = """data-movie-id=\\"(\d+)\\""""
val scriptContent = element!!.html().toString() val scriptContent = element!!.html().toString()
val pattern = Pattern.compile(regex, Pattern.MULTILINE) val pattern = Pattern.compile(regex, Pattern.MULTILINE)
@ -199,13 +234,9 @@ class NewGrounds : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
) )
} }
override fun videoListRequest(episode: SEpisode): Request { override fun videoListRequest(episode: SEpisode): Request = GET("$baseUrl${episode.url}", videoListHeaders)
Log.d("Tst", videoListHeaders.toString())
return GET("$baseUrl${episode.url}", videoListHeaders)
}
override fun videoListParse(response: Response): List<Video> { override fun videoListParse(response: Response): List<Video> {
Log.d("Tst", response.toString())
val responseBody = response.body.string() val responseBody = response.body.string()
val json = JSONObject(responseBody) val json = JSONObject(responseBody)
val sources = json.getJSONObject("sources") val sources = json.getJSONObject("sources")
@ -238,6 +269,21 @@ class NewGrounds : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
override fun videoUrlParse(document: Document): String = throw UnsupportedOperationException("Not Used") 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 ============================= // ============================ Preferences =============================
/* /*
According to the labels on the website: According to the labels on the website:
@ -274,19 +320,91 @@ class NewGrounds : ParsedAnimeHttpSource(), ConfigurableAnimeSource {
setOnPreferenceChangeListener { _, newValue -> setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String 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() preferences.edit().putString(key, selected).commit()
} }
}.also(screen::addPreference) }.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 { companion object {
private val PREF_SECTIONS = mapOf( private val PREF_SECTIONS = mapOf(
"Featured" to "movies/featured", "Featured" to "movies/featured",
"Latest" to "movies/browse", "Latest" to "movies/browse",
"Popular" to "movies/popular", "Popular" to "movies/popular",
"Your Feed" to "social/feeds/show/favorite-artists-movies", "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",
)