Add extension: Netfilm (#1290)

This commit is contained in:
Secozzi
2023-02-18 22:40:31 +01:00
committed by GitHub
parent 8a5f0c18ba
commit d594aacadb
10 changed files with 349 additions and 0 deletions

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="eu.kanade.tachiyomi.animeextension" />

View File

@ -0,0 +1,13 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext {
extName = 'NetFilm'
pkgNameSuffix = 'all.netfilm'
extClass = '.NetFilm'
extVersionCode = 1
libVersion = '13'
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

@ -0,0 +1,84 @@
package eu.kanade.tachiyomi.animeextension.all.netfilm
import kotlinx.serialization.Serializable
@Serializable
data class CategoryResponse(
val data: List<CategoryData>
) {
@Serializable
data class CategoryData(
val coverVerticalUrl: String,
val domainType: Int,
val id: String,
val name: String,
val sort: String
)
}
@Serializable
data class AnimeInfoResponse(
val data: InfoData
) {
@Serializable
data class InfoData(
val coverVerticalUrl: String,
val episodeVo: List<EpisodeInfo>,
val id: String,
val introduction: String,
val name: String,
val category: Int,
val tagList: List<IdInfo>,
) {
@Serializable
data class EpisodeInfo(
val id: Int,
val seriesNo: Float
)
@Serializable
data class IdInfo(
val name: String
)
}
}
@Serializable
data class SearchResponse(
val data: InfoData
) {
@Serializable
data class InfoData(
val results: List<CategoryResponse.CategoryData>
)
}
@Serializable
data class EpisodeResponse(
val data: EpisodeData
) {
@Serializable
data class EpisodeData(
val qualities: List<Quality>,
val subtitles: List<Subtitle>
) {
@Serializable
data class Quality(
val quality: Int,
val url: String
)
@Serializable
data class Subtitle(
val language: String,
val url: String
)
}
}
@Serializable
data class LinkData(
val category: String,
val id: String,
val episodeId: String? = null
)

View File

@ -0,0 +1,250 @@
package eu.kanade.tachiyomi.animeextension.all.netfilm
import android.app.Application
import android.content.SharedPreferences
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.Track
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.network.GET
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import kotlin.math.ceil
import kotlin.math.floor
class NetFilm : ConfigurableAnimeSource, AnimeHttpSource() {
override val name = "NetFilm"
override val baseUrl = "https://net-film.vercel.app/api"
override val lang = "all"
private var sort = ""
override val supportsLatest = true
private val json: Json by injectLazy()
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override val client: OkHttpClient = network.cloudflareClient
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int): Request {
if (page == 1) sort = ""
return GET("$baseUrl/category?area=&category=1&order=count&params=COMIC&size=30&sort=$sort&subtitles=&year=")
}
override fun popularAnimeParse(response: Response): AnimesPage {
val parsed = json.decodeFromString<CategoryResponse>(response.body!!.string())
if (parsed.data.isEmpty()) {
return AnimesPage(emptyList(), false)
}
val animeList = parsed.data.map { ani ->
SAnime.create().apply {
title = ani.name
thumbnail_url = ani.coverVerticalUrl
setUrlWithoutDomain(
LinkData(
ani.domainType.toString(),
ani.id
).toJsonString()
)
}
}
sort = parsed.data.last().sort
return AnimesPage(animeList, animeList.size == 30)
}
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request {
if (page == 1) sort = ""
return GET("$baseUrl/category?area=&category=1&order=up&params=COMIC&size=30&sort=$sort&subtitles=&year=")
}
override fun latestUpdatesParse(response: Response): AnimesPage = popularAnimeParse(response)
// =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
if (page == 1) sort = ""
return if (query.isNotEmpty()) {
GET("$baseUrl/search?keyword=$query&size=30")
} else {
val pageList = filters.find { it is SubPageFilter } as SubPageFilter
GET("$baseUrl${pageList.toUriPart()}&sort=$sort&subtitles=&year=")
}
}
override fun searchAnimeParse(response: Response): AnimesPage {
val url = response.request.url.encodedPath
return if (url.startsWith("/api/category")) {
popularAnimeParse(response)
} else {
val parsed = json.decodeFromString<SearchResponse>(response.body!!.string())
if (parsed.data.results.isEmpty()) {
return AnimesPage(emptyList(), false)
}
val animeList = parsed.data.results.map { ani ->
SAnime.create().apply {
title = ani.name
thumbnail_url = ani.coverVerticalUrl
setUrlWithoutDomain(
LinkData(
ani.domainType.toString(),
ani.id
).toJsonString()
)
}
}
sort = parsed.data.results.last().sort
AnimesPage(animeList, animeList.size == 30)
}
}
// ============================== FILTERS ===============================
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
AnimeFilter.Header("Text search ignores filters"),
SubPageFilter(),
)
private class SubPageFilter : UriPartFilter(
"Sub Page",
arrayOf(
Pair("Popular Movie", "/category?area=&category=1&order=count&params=MOVIE,TVSPECIAL&size=30"),
Pair("Recent Movie", "/category?area=&category=1&order=up&params=MOVIE,TVSPECIAL&size=30"),
Pair("Popular TV Series", "/category?area=&category=1&order=count&params=TV,SETI,MINISERIES,VARIETY,TALK,DOCUMENTARY&size=30"),
Pair("Recent TV Series", "/category?area=&category=1&order=up&params=TV,SETI,MINISERIES,VARIETY,TALK,DOCUMENTARY&size=30"),
Pair("Popular Anime", "/category?area=&category=1&order=count&params=COMIC&size=30"),
Pair("Recent Anime", "/category?area=&category=1&order=up&params=COMIC&size=30"),
)
)
private open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) :
AnimeFilter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
fun toUriPart() = vals[state].second
}
// =========================== Anime Details ============================
override fun fetchAnimeDetails(anime: SAnime): Observable<SAnime> {
val parsed = json.decodeFromString<LinkData>(anime.url)
val resp = client.newCall(GET("$baseUrl/detail?category=${parsed.category}&id=${parsed.id}")).execute()
val data = json.decodeFromString<AnimeInfoResponse>(resp.body!!.string()).data
return Observable.just(
anime.apply {
title = data.name
thumbnail_url = data.coverVerticalUrl
description = data.introduction
genre = data.tagList.joinToString(", ") { it.name }
}
)
}
override fun animeDetailsParse(response: Response): SAnime = throw Exception("Not used")
// ============================== Episodes ==============================
override fun fetchEpisodeList(anime: SAnime): Observable<List<SEpisode>> {
val parsed = json.decodeFromString<LinkData>(anime.url)
val resp = client.newCall(GET("$baseUrl/detail?category=${parsed.category}&id=${parsed.id}")).execute()
val data = json.decodeFromString<AnimeInfoResponse>(resp.body!!.string()).data
val episodeList = data.episodeVo.map { ep ->
val formattedEpNum = if (floor(ep.seriesNo) == ceil(ep.seriesNo)) {
ep.seriesNo.toInt()
} else {
ep.seriesNo
}
SEpisode.create().apply {
episode_number = ep.seriesNo
setUrlWithoutDomain(
LinkData(
data.category.toString(),
data.id,
ep.id.toString()
).toJsonString()
)
name = "Episode $formattedEpNum"
}
}
return Observable.just(episodeList.reversed())
}
override fun episodeListParse(response: Response): List<SEpisode> = throw Exception("Not used")
// ============================ Video Links =============================
override fun fetchVideoList(episode: SEpisode): Observable<List<Video>> {
val parsed = json.decodeFromString<LinkData>(episode.url)
val resp = client.newCall(GET("$baseUrl/episode?category=${parsed.category}&id=${parsed.id}&episode=${parsed.episodeId!!}")).execute()
val episodeParsed = json.decodeFromString<EpisodeResponse>(resp.body!!.string())
val subtitleList = episodeParsed.data.subtitles.map { sub ->
Track(sub.url, sub.language)
}
val videoList = episodeParsed.data.qualities.map { quality ->
Video(quality.url, "${quality.quality}p", quality.url, subtitleTracks = subtitleList)
}
return Observable.just(videoList.sort())
}
// ============================= Utilities ==============================
private fun LinkData.toJsonString(): String {
return json.encodeToString(this)
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString("preferred_quality", "1080")!!
return this.sortedWith(
compareBy { it.quality.contains(quality) }
).reversed()
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val videoQualityPref = ListPreference(screen.context).apply {
key = "preferred_quality"
title = "Preferred quality"
entries = arrayOf("1080p", "720p", "480p", "360p", "240p", "80p")
entryValues = arrayOf("1080", "720", "480", "360", "240", "80")
setDefaultValue("1080")
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
screen.addPreference(videoQualityPref)
}
}