Luscious conversion to multisrc (#6384)

* Create Luscious.kt

* Added icons and Factorys for multilang

* Create LusciousGenerator.kt

* Deleted singel source
This commit is contained in:
Johannes Joens
2021-04-02 04:11:06 +13:00
committed by GitHub
parent a942bdc87a
commit 5e5cdf9418
14 changed files with 152 additions and 49 deletions

View File

@ -0,0 +1,606 @@
package eu.kanade.tachiyomi.multisrc.luscious
import com.github.salomonbrys.kotson.addProperty
import com.github.salomonbrys.kotson.fromJson
import com.github.salomonbrys.kotson.get
import com.github.salomonbrys.kotson.set
import com.google.gson.Gson
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.model.Filter
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.HttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import rx.Observable
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
abstract class Luscious(
override val name: String,
override val baseUrl: String,
override val lang: String ) : HttpSource() {
//Based on Luscios single source extension form https://github.com/tachiyomiorg/tachiyomi-extensions/commit/aacf56d0c0ddb173372aac69d798ae998f178377
//with modifiaction to make it support multisrc
override val supportsLatest: Boolean = true
private val apiBaseUrl: String = "$baseUrl/graphql/nobatch/"
private val gson = Gson()
override val client: OkHttpClient = network.cloudflareClient
private val lusLang: String = lusLang(lang)
private fun lusLang(lang: String): String {
return when (lang) {
"en" -> "1"
"ja" -> "2"
"es" -> "3"
"it" -> "4"
"de" -> "5"
"fr" -> "6"
"zh" -> "8"
"ko" -> "9"
"pt" -> "100"
"th" -> "101"
else -> "99"
}
}
// Common
private fun buildAlbumListRequestInput(page: Int, filters: FilterList, query: String = ""): JsonObject {
val sortByFilter = filters.findInstance<SortBySelectFilter>()!!
val albumTypeFilter = filters.findInstance<AlbumTypeSelectFilter>()!!
val interestsFilter = filters.findInstance<InterestGroupFilter>()!!
val languagesFilter = filters.findInstance<LanguageGroupFilter>()!!
val tagsFilter = filters.findInstance<TagGroupFilter>()!!
val genreFilter = filters.findInstance<GenreGroupFilter>()!!
val contentTypeFilter = filters.findInstance<ContentTypeSelectFilter>()!!
return JsonObject().apply {
add(
"input",
JsonObject().apply {
addProperty("display", sortByFilter.selected)
addProperty("page", page)
add(
"filters",
JsonArray().apply {
if (contentTypeFilter.selected != FILTER_VALUE_IGNORE)
add(contentTypeFilter.toJsonObject("content_id"))
if (albumTypeFilter.selected != FILTER_VALUE_IGNORE)
add(albumTypeFilter.toJsonObject("album_type"))
with(interestsFilter) {
if (this.selected.isEmpty()) {
throw Exception("Please select an Interest")
}
add(this.toJsonObject("audience_ids"))
}
add(
languagesFilter.toJsonObject("language_ids").apply {
set("value", "+$lusLang${get("value").asString}")
}
)
if (tagsFilter.anyNotIgnored()) {
add(tagsFilter.toJsonObject("tagged"))
}
if (genreFilter.anyNotIgnored()) {
add(genreFilter.toJsonObject("genre_ids"))
}
if (query != "") {
add(
JsonObject().apply {
addProperty("name", "search_query")
addProperty("value", query)
}
)
}
}
)
}
)
}
}
private fun buildAlbumListRequest(page: Int, filters: FilterList, query: String = ""): Request {
val input = buildAlbumListRequestInput(page, filters, query)
val url = HttpUrl.parse(apiBaseUrl)!!.newBuilder()
.addQueryParameter("operationName", "AlbumList")
.addQueryParameter("query", ALBUM_LIST_REQUEST_GQL)
.addQueryParameter("variables", input.toString())
.toString()
return GET(url, headers)
}
private fun parseAlbumListResponse(response: Response): MangasPage {
val data = gson.fromJson<JsonObject>(response.body()!!.string())
with(data["data"]["album"]["list"]) {
return MangasPage(
this["items"].asJsonArray.map {
SManga.create().apply {
url = it["url"].asString
title = it["title"].asString
thumbnail_url = it["cover"]["url"].asString
}
},
this["info"]["has_next_page"].asBoolean
)
}
}
// Latest
override fun latestUpdatesRequest(page: Int): Request = buildAlbumListRequest(page, getSortFilters(LATEST_DEFAULT_SORT_STATE))
override fun latestUpdatesParse(response: Response): MangasPage = parseAlbumListResponse(response)
// Chapters
override fun chapterListParse(response: Response): List<SChapter> {
val document = response.asJsoup()
return listOf(
SChapter.create().apply {
url = response.request().url().toString()
name = "Chapter"
date_upload = document.select(".album-info-item:contains(Created:)")?.first()?.ownText()?.trim()?.let {
DATE_FORMATS_WITH_ORDINAL_SUFFIXES.mapNotNull { format -> format.parseOrNull(it) }.firstOrNull()?.time
} ?: 0L
chapter_number = 1f
}
)
}
// Pages
private fun buildAlbumPicturesRequestInput(id: String, page: Int, sortPagesByOption: String): JsonObject {
return JsonObject().apply {
addProperty(
"input",
JsonObject().apply {
addProperty(
"filters",
JsonArray().apply {
add(
JsonObject().apply {
addProperty("name", "album_id")
addProperty("value", id)
}
)
}
)
addProperty("display", sortPagesByOption)
addProperty("page", page)
}
)
}
}
private fun buildAlbumPicturesPageUrl(id: String, page: Int, sortPagesByOption: String): String {
val input = buildAlbumPicturesRequestInput(id, page, sortPagesByOption)
return HttpUrl.parse(apiBaseUrl)!!.newBuilder()
.addQueryParameter("operationName", "AlbumListOwnPictures")
.addQueryParameter("query", ALBUM_PICTURES_REQUEST_GQL)
.addQueryParameter("variables", input.toString())
.toString()
}
private fun parseAlbumPicturesResponse(response: Response, sortPagesByOption: String): List<Page> {
val id = response.request().url().queryParameter("variables").toString()
.let { gson.fromJson<JsonObject>(it)["input"]["filters"].asJsonArray }
.let { it.first { f -> f["name"].asString == "album_id" } }
.let { it["value"].asString }
val data = gson.fromJson<JsonObject>(response.body()!!.string())
.let { it["data"]["picture"]["list"].asJsonObject }
return data["items"].asJsonArray.mapIndexed { index, it ->
Page(index, imageUrl = it["url_to_original"].asString)
} + if (data["info"]["total_pages"].asInt > 1) { // get 2nd page onwards
(ITEMS_PER_PAGE until data["info"]["total_items"].asInt).chunked(ITEMS_PER_PAGE).mapIndexed { page, indices ->
indices.map { Page(it, url = buildAlbumPicturesPageUrl(id, page + 2, sortPagesByOption)) }
}.flatten()
} else emptyList()
}
private fun getAlbumSortPagesOption(chapter: SChapter): Observable<String> {
return client.newCall(GET(chapter.url))
.asObservableSuccess()
.map {
val sortByKey = it.asJsoup().select(".o-input-select:contains(Sorted By) .o-select-value")?.text() ?: ""
ALBUM_PICTURES_SORT_OPTIONS.getValue(sortByKey)
}
}
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
val id = chapter.url.substringAfterLast("_").removeSuffix("/")
return getAlbumSortPagesOption(chapter)
.concatMap { sortPagesByOption ->
client.newCall(GET(buildAlbumPicturesPageUrl(id, 1, sortPagesByOption)))
.asObservableSuccess()
.map { parseAlbumPicturesResponse(it, sortPagesByOption) }
}
}
override fun pageListParse(response: Response): List<Page> = throw UnsupportedOperationException("Not used")
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException("Not used")
override fun fetchImageUrl(page: Page): Observable<String> {
if (page.imageUrl != null) {
return Observable.just(page.imageUrl)
}
return client.newCall(GET(page.url, headers))
.asObservableSuccess()
.map {
val data = gson.fromJson<JsonObject>(it.body()!!.string()).let { data ->
data["data"]["picture"]["list"].asJsonObject
}
data["items"].asJsonArray[page.index % 50].asJsonObject["url_to_original"].asString
}
}
// Details
private fun parseMangaGenre(document: Document): String {
return listOf(
document.select(".o-tag--secondary").map { it.text().substringBefore("(").trim() },
document.select(".o-tag:not([href *= /tags/artist])").map { it.text() },
document.select(".album-info-item:contains(Content:) .o-tag").map { it.text() }
).flatten().joinToString()
}
private fun parseMangaDescription(document: Document): String {
val pageCount: String? = (
document.select(".album-info-item:contains(pictures)").firstOrNull()
?: document.select(".album-info-item:contains(gifs)").firstOrNull()
)?.text()
return listOf(
Pair("Description", document.select(".album-description:last-of-type")?.text()),
Pair("Pages", pageCount)
).let {
it + listOf("Parody", "Character", "Ethnicity")
.map { key -> key to document.select(".o-tag--category:contains($key) .o-tag").joinToString { t -> t.text() } }
}.filter { desc -> !desc.second.isNullOrBlank() }
.joinToString("\n\n") { "${it.first}:\n${it.second}" }
}
override fun mangaDetailsParse(response: Response): SManga {
val document = response.asJsoup()
return SManga.create().apply {
artist = document.select(".o-tag--category:contains(Artist:) .o-tag")?.joinToString { it.text() }
author = artist
genre = parseMangaGenre(document)
title = document.select("a[title]").text()
status = when {
title.contains("ongoing", true) -> SManga.ONGOING
else -> SManga.COMPLETED
}
description = parseMangaDescription(document)
}
}
// Popular
override fun popularMangaParse(response: Response): MangasPage = parseAlbumListResponse(response)
override fun popularMangaRequest(page: Int): Request = buildAlbumListRequest(page, getSortFilters(POPULAR_DEFAULT_SORT_STATE))
// Search
override fun searchMangaParse(response: Response): MangasPage = parseAlbumListResponse(response)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = buildAlbumListRequest(
page,
filters.let {
if (it.isEmpty()) getSortFilters(SEARCH_DEFAULT_SORT_STATE)
else it
},
query
)
class TriStateFilterOption(name: String, val value: String) : Filter.TriState(name)
abstract class TriStateGroupFilter(name: String, options: List<TriStateFilterOption>) : Filter.Group<TriStateFilterOption>(name, options) {
val included: List<String>
get() = state.filter { it.isIncluded() }.map { it.value }
val excluded: List<String>
get() = state.filter { it.isExcluded() }.map { it.value }
fun anyNotIgnored(): Boolean = state.any { !it.isIgnored() }
override fun toString(): String = (included.map { "+$it" } + excluded.map { "-$it" }).joinToString("")
}
private fun Filter<*>.toJsonObject(key: String): JsonObject {
val value = this.toString()
return JsonObject().apply {
addProperty("name", key)
addProperty("value", value)
}
}
private class TagGroupFilter(filters: List<TriStateFilterOption>) : TriStateGroupFilter("Tags", filters)
private class GenreGroupFilter(filters: List<TriStateFilterOption>) : TriStateGroupFilter("Genres", filters)
class CheckboxFilterOption(name: String, val value: String, default: Boolean = true) : Filter.CheckBox(name, default)
abstract class CheckboxGroupFilter(name: String, options: List<CheckboxFilterOption>) : Filter.Group<CheckboxFilterOption>(name, options) {
val selected: List<String>
get() = state.filter { it.state }.map { it.value }
override fun toString(): String = selected.joinToString("") { "+$it" }
}
private class InterestGroupFilter(options: List<CheckboxFilterOption>) : CheckboxGroupFilter("Interests", options)
private class LanguageGroupFilter(options: List<CheckboxFilterOption>) : CheckboxGroupFilter("Languages", options)
class SelectFilterOption(val name: String, val value: String)
abstract class SelectFilter(name: String, private val options: List<SelectFilterOption>, default: Int = 0) : Filter.Select<String>(name, options.map { it.name }.toTypedArray(), default) {
val selected: String
get() = options[state].value
override fun toString(): String = selected
}
class SortBySelectFilter(options: List<SelectFilterOption>, default: Int) : SelectFilter("Sort By", options, default)
class AlbumTypeSelectFilter(options: List<SelectFilterOption>) : SelectFilter("Album Type", options)
class ContentTypeSelectFilter(options: List<SelectFilterOption>) : SelectFilter("Content Type", options)
override fun getFilterList(): FilterList = getSortFilters(POPULAR_DEFAULT_SORT_STATE)
private fun getSortFilters(sortState: Int) = FilterList(
SortBySelectFilter(getSortFilters(), sortState),
AlbumTypeSelectFilter(getAlbumTypeFilters()),
ContentTypeSelectFilter(getContentTypeFilters()),
InterestGroupFilter(getInterestFilters()),
LanguageGroupFilter(getLanguageFilters()),
TagGroupFilter(getTagFilters()),
GenreGroupFilter(getGenreFilters())
)
fun getSortFilters() = listOf(
SelectFilterOption("Rating - All Time", "rating_all_time"),
SelectFilterOption("Rating - Last 7 Days", "rating_7_days"),
SelectFilterOption("Rating - Last 14 Days", "rating_14_days"),
SelectFilterOption("Rating - Last 30 Days", "rating_30_days"),
SelectFilterOption("Rating - Last 90 Days", "rating_90_days"),
SelectFilterOption("Rating - Last Year", "rating_1_year"),
SelectFilterOption("Rating - Last Year", "rating_1_year"),
SelectFilterOption("Date - Newest First", "date_newest"),
SelectFilterOption("Date - 2020", "date_2020"),
SelectFilterOption("Date - 2019", "date_2019"),
SelectFilterOption("Date - 2018", "date_2018"),
SelectFilterOption("Date - 2017", "date_2017"),
SelectFilterOption("Date - 2016", "date_2016"),
SelectFilterOption("Date - 2015", "date_2015"),
SelectFilterOption("Date - 2014", "date_2014"),
SelectFilterOption("Date - 2013", "date_2013"),
SelectFilterOption("Date - Oldest First", "date_oldest"),
SelectFilterOption("Date - Upcoming", "date_upcoming"),
SelectFilterOption("Date - Trending", "date_trending"),
SelectFilterOption("Date - Featured", "date_featured"),
SelectFilterOption("Date - Last Viewed", "date_last_interaction")
)
fun getAlbumTypeFilters() = listOf(
SelectFilterOption("Manga", "manga"),
SelectFilterOption("All", FILTER_VALUE_IGNORE),
SelectFilterOption("Pictures", "pictures")
)
fun getContentTypeFilters() = listOf(
SelectFilterOption("All", FILTER_VALUE_IGNORE),
SelectFilterOption("Hentai", "0"),
SelectFilterOption("Non-Erotic", "5"),
SelectFilterOption("Real People", "6")
)
fun getInterestFilters() = listOf(
CheckboxFilterOption("Straight Sex", "1"),
CheckboxFilterOption("Trans x Girl", "10", false),
CheckboxFilterOption("Gay / Yaoi", "2"),
CheckboxFilterOption("Lesbian / Yuri", "3"),
CheckboxFilterOption("Trans", "5"),
CheckboxFilterOption("Solo Girl", "6"),
CheckboxFilterOption("Trans x Trans", "8"),
CheckboxFilterOption("Trans x Guy", "9")
)
fun getLanguageFilters() = listOf(
CheckboxFilterOption("English", ENGLISH_LUS_LANG_VAL, false),
CheckboxFilterOption("Japanese", JAPANESE_LUS_LANG_VAL, false),
CheckboxFilterOption("Spanish", SPANISH_LUS_LANG_VAL, false),
CheckboxFilterOption("Italian", ITALIAN_LUS_LANG_VAL, false),
CheckboxFilterOption("German", GERMAN_LUS_LANG_VAL, false),
CheckboxFilterOption("French", FRENCH_LUS_LANG_VAL, false),
CheckboxFilterOption("Chinese", CHINESE_LUS_LANG_VAL, false),
CheckboxFilterOption("Korean", KOREAN_LUS_LANG_VAL, false),
CheckboxFilterOption("Others", OTHERS_LUS_LANG_VAL, false),
CheckboxFilterOption("Portugese", PORTUGESE_LUS_LANG_VAL, false),
CheckboxFilterOption("Thai", THAI_LUS_LANG_VAL, false)
).filterNot { it.value == lusLang }
fun getTagFilters() = listOf(
TriStateFilterOption("Big Breasts", "big_breasts"),
TriStateFilterOption("Blowjob", "blowjob"),
TriStateFilterOption("Anal", "anal"),
TriStateFilterOption("Group", "group"),
TriStateFilterOption("Big Ass", "big_ass"),
TriStateFilterOption("Full Color", "full_color"),
TriStateFilterOption("Schoolgirl", "schoolgirl"),
TriStateFilterOption("Rape", "rape"),
TriStateFilterOption("Glasses", "glasses"),
TriStateFilterOption("Nakadashi", "nakadashi"),
TriStateFilterOption("Yuri", "yuri"),
TriStateFilterOption("Paizuri", "paizuri"),
TriStateFilterOption("Ahegao", "ahegao"),
TriStateFilterOption("Group: metart", "group%3A_metart"),
TriStateFilterOption("Brunette", "brunette"),
TriStateFilterOption("Solo", "solo"),
TriStateFilterOption("Blonde", "blonde"),
TriStateFilterOption("Shaved Pussy", "shaved_pussy"),
TriStateFilterOption("Small Breasts", "small_breasts"),
TriStateFilterOption("Cum", "cum"),
TriStateFilterOption("Stockings", "stockings"),
TriStateFilterOption("Yuri", "yuri"),
TriStateFilterOption("Ass", "ass"),
TriStateFilterOption("Creampie", "creampie"),
TriStateFilterOption("Rape", "rape"),
TriStateFilterOption("Oral Sex", "oral_sex"),
TriStateFilterOption("Bondage", "bondage"),
TriStateFilterOption("Futanari", "futanari"),
TriStateFilterOption("Double Penetration", "double_penetration"),
TriStateFilterOption("Threesome", "threesome"),
TriStateFilterOption("Anal Sex", "anal_sex"),
TriStateFilterOption("Big Cock", "big_cock"),
TriStateFilterOption("Straight Sex", "straight_sex"),
TriStateFilterOption("Yaoi", "yaoi")
)
fun getGenreFilters() = listOf(
TriStateFilterOption("3D / Digital Art", "25"),
TriStateFilterOption("Amateurs", "20"),
TriStateFilterOption("Artist Collection", "19"),
TriStateFilterOption("Asian Girls", "12"),
TriStateFilterOption("Cosplay", "22"),
TriStateFilterOption("BDSM", "27"),
TriStateFilterOption("Cross-Dressing", "30"),
TriStateFilterOption("Defloration / First Time", "59"),
TriStateFilterOption("Ebony Girls", "32"),
TriStateFilterOption("European Girls", "46"),
TriStateFilterOption("Fantasy / Monster Girls", "10"),
TriStateFilterOption("Fetish", "2"),
TriStateFilterOption("Furries", "8"),
TriStateFilterOption("Futanari", "31"),
TriStateFilterOption("Group Sex", "36"),
TriStateFilterOption("Harem", "56"),
TriStateFilterOption("Humor", "41"),
TriStateFilterOption("Interracial", "28"),
TriStateFilterOption("Kemonomimi / Animal Ears", "39"),
TriStateFilterOption("Latina Girls", "33"),
TriStateFilterOption("Mature", "13"),
TriStateFilterOption("Members: Original Art", "18"),
TriStateFilterOption("Members: Verified Selfies", "21"),
TriStateFilterOption("Military", "48"),
TriStateFilterOption("Mind Control", "34"),
TriStateFilterOption("Monsters & Tentacles", "38"),
TriStateFilterOption("Netorare / Cheating", "40"),
TriStateFilterOption("No Genre Given", "1"),
TriStateFilterOption("Nonconsent / Reluctance", "37"),
TriStateFilterOption("Other Ethnicity Girls", "57"),
TriStateFilterOption("Public Sex", "43"),
TriStateFilterOption("Romance", "42"),
TriStateFilterOption("School / College", "35"),
TriStateFilterOption("Sex Workers", "47"),
TriStateFilterOption("Softcore / Ecchi", "9"),
TriStateFilterOption("Superheroes", "17"),
TriStateFilterOption("Tankobon", "45"),
TriStateFilterOption("TV / Movies", "51"),
TriStateFilterOption("Trans", "14"),
TriStateFilterOption("Video Games", "15"),
TriStateFilterOption("Vintage", "58"),
TriStateFilterOption("Western", "11"),
TriStateFilterOption("Workplace Sex", "50")
)
private inline fun <reified T> Iterable<*>.findInstance() = find { it is T } as? T
private fun SimpleDateFormat.parseOrNull(string: String): Date? {
return try {
parse(string)
} catch (e: ParseException) {
null
}
}
companion object {
private val ALBUM_PICTURES_SORT_OPTIONS = hashMapOf(
Pair("Sort By Newest", "date_newest"),
Pair("Sort By Rating", "rating_all_time")
).withDefault { "position" }
private const val ITEMS_PER_PAGE = 50
private val ORDINAL_SUFFIXES = listOf("st", "nd", "rd", "th")
private val DATE_FORMATS_WITH_ORDINAL_SUFFIXES = ORDINAL_SUFFIXES.map {
SimpleDateFormat("MMMM dd'$it', yyyy", Locale.US)
}
const val ENGLISH_LUS_LANG_VAL = "1"
const val JAPANESE_LUS_LANG_VAL = "2"
const val SPANISH_LUS_LANG_VAL = "3"
const val ITALIAN_LUS_LANG_VAL = "4"
const val GERMAN_LUS_LANG_VAL = "5"
const val FRENCH_LUS_LANG_VAL = "6"
const val CHINESE_LUS_LANG_VAL = "8"
const val KOREAN_LUS_LANG_VAL = "9"
const val OTHERS_LUS_LANG_VAL = "99"
const val PORTUGESE_LUS_LANG_VAL = "100"
const val THAI_LUS_LANG_VAL = "101"
private const val POPULAR_DEFAULT_SORT_STATE = 0
private const val LATEST_DEFAULT_SORT_STATE = 7
private const val SEARCH_DEFAULT_SORT_STATE = 0
private const val FILTER_VALUE_IGNORE = "<ignore>"
private val ALBUM_LIST_REQUEST_GQL = """
query AlbumList(${'$'}input: AlbumListInput!) {
album {
list(input: ${'$'}input) {
info {
page
has_next_page
}
items
}
}
}
""".replace("\n", " ").replace("\\s+".toRegex(), " ")
private val ALBUM_PICTURES_REQUEST_GQL = """
query AlbumListOwnPictures(${'$'}input: PictureListInput!) {
picture {
list(input: ${'$'}input) {
info {
total_items
total_pages
page
has_next_page
}
items {
url_to_original
}
}
}
}
""".replace("\n", " ").replace("\\s+".toRegex(), " ")
}
}

View File

@ -0,0 +1,27 @@
package eu.kanade.tachiyomi.multisrc.luscious
import generator.ThemeSourceData.MultiLang
import generator.ThemeSourceData.SingleLang
import generator.ThemeSourceGenerator
class LusciousGenerator : ThemeSourceGenerator {
override val themePkg = "luscious"
override val themeClass = "Luscious"
override val baseVersionCode: Int = 1
override val sources = listOf(
MultiLang("Luscious", "https://www.luscious.net", listOf("en","ja", "es", "it", "de", "fr", "zh", "ko", "other", "pt", "th"), isNsfw = true, className = "LusciousFactory", overrideVersionCode = 2),
MultiLang("Luscious (Members)", "https://members.luscious.net", listOf("en","ja", "es", "it", "de", "fr", "zh", "ko", "other", "pt", "th"), isNsfw = true, className = "LusciousMembersFactory", pkgName = "lusciousmembers"),//Requires Account
MultiLang("Luscious (API)", "https://api.luscious.net", listOf("en","ja", "es", "it", "de", "fr", "zh", "ko", "other", "pt", "th"), isNsfw = true, className = "LusciousAPIFactory", pkgName = "lusciousapi")
)
companion object {
@JvmStatic
fun main(args: Array<String>) {
LusciousGenerator().createAll()
}
}
}