diff --git a/src/tr/animeler/AndroidManifest.xml b/src/tr/animeler/AndroidManifest.xml new file mode 100644 index 000000000..1460a1343 --- /dev/null +++ b/src/tr/animeler/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + diff --git a/src/tr/animeler/build.gradle b/src/tr/animeler/build.gradle new file mode 100644 index 000000000..82062f7d6 --- /dev/null +++ b/src/tr/animeler/build.gradle @@ -0,0 +1,28 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.serialization) +} + +ext { + extName = 'Animeler' + pkgNameSuffix = 'tr.animeler' + extClass = '.Animeler' + extVersionCode = 1 + libVersion = '13' +} + +dependencies { + implementation(project(":lib-dood-extractor")) + implementation(project(":lib-filemoon-extractor")) + implementation(project(":lib-gdriveplayer-extractor")) + implementation(project(":lib-sibnet-extractor")) + implementation(project(":lib-streamlare-extractor")) + implementation(project(":lib-okru-extractor")) + implementation(project(":lib-streamtape-extractor")) + implementation(project(":lib-uqload-extractor")) + implementation(project(":lib-voe-extractor")) + implementation(project(":lib-vudeo-extractor")) +} + +apply from: "$rootDir/common.gradle" diff --git a/src/tr/animeler/res/mipmap-hdpi/ic_launcher.png b/src/tr/animeler/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..2204aaea1 Binary files /dev/null and b/src/tr/animeler/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/tr/animeler/res/mipmap-mdpi/ic_launcher.png b/src/tr/animeler/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..601314a56 Binary files /dev/null and b/src/tr/animeler/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/tr/animeler/res/mipmap-xhdpi/ic_launcher.png b/src/tr/animeler/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..18c643327 Binary files /dev/null and b/src/tr/animeler/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/tr/animeler/res/mipmap-xxhdpi/ic_launcher.png b/src/tr/animeler/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..95b7899bb Binary files /dev/null and b/src/tr/animeler/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/tr/animeler/res/mipmap-xxxhdpi/ic_launcher.png b/src/tr/animeler/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..33afbcd26 Binary files /dev/null and b/src/tr/animeler/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/tr/animeler/src/eu/kanade/tachiyomi/animeextension/tr/animeler/Animeler.kt b/src/tr/animeler/src/eu/kanade/tachiyomi/animeextension/tr/animeler/Animeler.kt new file mode 100644 index 000000000..adb7a33e5 --- /dev/null +++ b/src/tr/animeler/src/eu/kanade/tachiyomi/animeextension/tr/animeler/Animeler.kt @@ -0,0 +1,376 @@ +package eu.kanade.tachiyomi.animeextension.tr.animeler + +import android.app.Application +import androidx.preference.ListPreference +import androidx.preference.MultiSelectListPreference +import androidx.preference.PreferenceScreen +import eu.kanade.tachiyomi.animeextension.tr.animeler.dto.EpisodeDto +import eu.kanade.tachiyomi.animeextension.tr.animeler.dto.FullAnimeDto +import eu.kanade.tachiyomi.animeextension.tr.animeler.dto.SearchRequestDto +import eu.kanade.tachiyomi.animeextension.tr.animeler.dto.SearchResponseDto +import eu.kanade.tachiyomi.animeextension.tr.animeler.dto.SingleDto +import eu.kanade.tachiyomi.animeextension.tr.animeler.dto.SourcesDto +import eu.kanade.tachiyomi.animeextension.tr.animeler.dto.VideoDto +import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource +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.AnimeHttpSource +import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor +import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor +import eu.kanade.tachiyomi.lib.gdriveplayerextractor.GdrivePlayerExtractor +import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor +import eu.kanade.tachiyomi.lib.sibnetextractor.SibnetExtractor +import eu.kanade.tachiyomi.lib.streamlareextractor.StreamlareExtractor +import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor +import eu.kanade.tachiyomi.lib.uqloadextractor.UqloadExtractor +import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor +import eu.kanade.tachiyomi.lib.vudeoextractor.VudeoExtractor +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.POST +import eu.kanade.tachiyomi.network.asObservableSuccess +import eu.kanade.tachiyomi.util.asJsoup +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import okhttp3.FormBody +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response +import rx.Observable +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import uy.kohesive.injekt.injectLazy +import java.text.SimpleDateFormat +import java.util.Locale + +class Animeler : AnimeHttpSource(), ConfigurableAnimeSource { + + override val name = "Animeler" + + override val baseUrl = "https://animeler.me" + + override val lang = "tr" + + override val supportsLatest = true + + private val json: Json by injectLazy() + + private val preferences by lazy { + Injekt.get().getSharedPreferences("source_$id", 0x0000) + } + + // ============================== Popular =============================== + override fun popularAnimeRequest(page: Int) = searchOrderBy("total_kiranime_views", page) + + override fun popularAnimeParse(response: Response): AnimesPage { + val results = response.parseAs() + val animes = results.data.map { + SAnime.create().apply { + setUrlWithoutDomain(it.url) + thumbnail_url = it.image + title = it.title + } + } + val page = response.request.url.queryParameter("page")?.toIntOrNull() ?: 1 + val hasNextPage = page < results.pages + return AnimesPage(animes, hasNextPage) + } + + // =============================== Latest =============================== + override fun latestUpdatesRequest(page: Int) = searchOrderBy("kiranime_anime_updated", page) + + override fun latestUpdatesParse(response: Response) = popularAnimeParse(response) + + // =============================== Search =============================== + override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable { + return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler + val id = query.removePrefix(PREFIX_SEARCH) + client.newCall(GET("$baseUrl/anime/$id")) + .asObservableSuccess() + .map(::searchAnimeByIdParse) + } else { + super.fetchSearchAnime(page, query, filters) + } + } + + private fun searchAnimeByIdParse(response: Response): AnimesPage { + val details = animeDetailsParse(response) + return AnimesPage(listOf(details), false) + } + + override fun getFilterList() = AnimelerFilters.FILTER_LIST + + override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request { + val params = AnimelerFilters.getSearchParameters(filters) + val (meta, orderBy) = when (params.orderBy) { + "date", "title" -> Pair(null, params.orderBy) + else -> Pair(params.orderBy, "meta_value_num") + } + + val single = SingleDto( + paged = page, + key = meta, + order = params.order, + orderBy = orderBy, + season = params.season.ifEmpty { null }, + year = params.year.ifEmpty { null }, + ) + + val taxonomies = with(params) { + listOf(genres, status, producers, studios, types).filter { + it.terms.isNotEmpty() + } + } + + val requestDto = SearchRequestDto(single, query, query, taxonomies) + val requestData = json.encodeToString(requestDto) + return searchRequest(requestData, page) + } + + override fun searchAnimeParse(response: Response) = popularAnimeParse(response) + + private fun searchOrderBy(order: String, page: Int): Request { + val body = """ + { + "keyword": "", + "query": "", + "single": { + "paged": $page, + "orderby": "meta_value_num", + "meta_key": "$order", + "order": "desc" + }, + "tax": [] + } + """.trimIndent() + return searchRequest(body, page) + } + + private fun searchRequest(data: String, page: Int): Request { + val body = data.toRequestBody("application/json".toMediaType()) + return POST("$baseUrl/wp-json/kiranime/v1/anime/advancedsearch?_locale=user&page=$page", headers, body) + } + + // =========================== Anime Details ============================ + override fun animeDetailsParse(response: Response) = SAnime.create().apply { + val body = response.use { it.body.string() } + .substringAfter("var anime = ") + .substringBefore("}<") + "}" + val animeDto = json.decodeFromString(body) + + setUrlWithoutDomain(animeDto.url) + thumbnail_url = animeDto.image + title = animeDto.title + artist = animeDto.studios + author = animeDto.producers + genre = animeDto.genres + status = when { + animeDto.meta.aired.orEmpty().contains(" to ") -> SAnime.COMPLETED + else -> SAnime.UNKNOWN + } + + description = buildString { + animeDto.post.post_content?.also { append(it + "\n") } + + with(animeDto.meta) { + score?.takeIf(String::isNotBlank)?.also { append("\nScore: $it") } + native?.takeIf(String::isNotBlank)?.also { append("\nNative: $it") } + synonyms?.takeIf(String::isNotBlank)?.also { append("\nDiğer İsimleri: $it") } + rate?.takeIf(String::isNotBlank)?.also { append("\nRate: $it") } + premiered?.takeIf(String::isNotBlank)?.also { append("\nPremiered: $it") } + aired?.takeIf(String::isNotBlank)?.also { append("\nYayınlandı: $it") } + duration?.takeIf(String::isNotBlank)?.also { append("\nSüre: $it") } + } + } + } + + // ============================== Episodes ============================== + override fun episodeListParse(response: Response): List { + val body = response.use { it.body.string() } + .substringAfter("var episodes = ") + .substringBefore("];") + "]" + + val episodes = json.decodeFromString>(body) + + return episodes.reversed().map { + SEpisode.create().apply { + setUrlWithoutDomain(it.url) + name = "Bölüm " + it.meta.number + episode_number = it.meta.number.toFloat() + date_upload = it.date.toDate() + } + } + } + + // ============================ Video Links ============================= + private val doodExtractor by lazy { DoodExtractor(client) } + private val filemoonExtractor by lazy { FilemoonExtractor(client) } + private val gdrivePlayerExtractor by lazy { GdrivePlayerExtractor(client) } + private val okruExtractor by lazy { OkruExtractor(client) } + private val sibnetExtractor by lazy { SibnetExtractor(client) } + private val streamlareExtractor by lazy { StreamlareExtractor(client) } + private val streamtapeExtractor by lazy { StreamTapeExtractor(client) } + private val uqloadExtractor by lazy { UqloadExtractor(client) } + private val voeExtractor by lazy { VoeExtractor(client) } + private val vudeoExtractor by lazy { VudeoExtractor(client) } + + override fun videoListParse(response: Response): List