diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index e0276c30..0d06aae0 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -8,3 +8,4 @@ Checklist: - [ ] Have explicitly kept the `id` if a source's name or language were changed - [ ] Have tested the modifications by compiling and running the extension through Android Studio - [ ] Have removed `web_hi_res_512.png` when adding a new extension +- [ ] Have made sure all the icons are in png format diff --git a/.github/workflows/build_push.yml b/.github/workflows/build_push.yml index d984a6c6..ecfccbf0 100644 --- a/.github/workflows/build_push.yml +++ b/.github/workflows/build_push.yml @@ -40,12 +40,26 @@ jobs: files_separator: " " safe_output: false + - name: Import GPG key + uses: crazy-max/ghaction-import-gpg@v6 # v6.1.0 + with: + gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} + passphrase: ${{ secrets.GPG_PASSPHRASE }} + git_user_signingkey: true + git_commit_gpgsign: true + # This step is going to commit, but this will not trigger another workflow. - name: Bump extensions that uses a modified lib if: steps.modified-libs.outputs.any_changed == 'true' run: | ./.github/scripts/bump-versions.py ${{ steps.modified-libs.outputs.all_changed_files }} + # This step is going to commit, but this will not trigger another workflow. + - name: Bump extensions that uses a modified lib + if: steps.modified-libs.outputs.any_changed == 'true' + run: | + chmod +x ./.github/scripts/bump-versions.py ${{ steps.modified-libs.outputs.all_changed_files }} + - name: Validate Gradle Wrapper uses: gradle/wrapper-validation-action@a494d935f4b56874c4a5a87d19af7afcf3a163d0 # v2 diff --git a/README.md b/README.md index fb997af6..68811214 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,20 @@ -# Aniyomi-extensions +## Guide -The source code for the extensions +just paste this into your anime repo +``` +https://raw.githubusercontent.com/almightyhak/aniyomi-anime-repo/main/index.min.json +``` +If your interested in installing just the apks they can be found [Here](https://github.com/almightyhak/aniyomi-anime-repo) ## Support Server -[Discord](https://discord.gg/vut4mmXQzU) +Join the [Discord](https://discord.gg/vut4mmXQzU) for updates and announcements + and please check the discord BEFORE making an issue -## Guide +## Contributing -just paste this into your anime repo https://raw.githubusercontent.com/almightyhak/aniyomi-anime-repo/main/index.min.json - -i am maintaining this but keep in mind that i'm NOT a developer so expect issues to take a while to fix - -If your interested in installing just the apks they can be found [Here](https://github.com/almightyhak/aniyomi-anime-repo) +[Template](https://github.com/aniyomiorg/aniyomi-extensions/blob/master/CONTRIBUTING.md) ## Contact diff --git a/lib-multisrc/anilist/build.gradle.kts b/lib-multisrc/anilist/build.gradle.kts new file mode 100644 index 00000000..9dce2478 --- /dev/null +++ b/lib-multisrc/anilist/build.gradle.kts @@ -0,0 +1,5 @@ +plugins { + id("lib-multisrc") +} + +baseVersionCode = 2 diff --git a/lib-multisrc/anilist/src/eu/kanade/tachiyomi/multisrc/anilist/AniListAnimeHttpSource.kt b/lib-multisrc/anilist/src/eu/kanade/tachiyomi/multisrc/anilist/AniListAnimeHttpSource.kt new file mode 100644 index 00000000..a2c8780d --- /dev/null +++ b/lib-multisrc/anilist/src/eu/kanade/tachiyomi/multisrc/anilist/AniListAnimeHttpSource.kt @@ -0,0 +1,166 @@ +package eu.kanade.tachiyomi.multisrc.anilist + +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.online.AnimeHttpSource +import eu.kanade.tachiyomi.network.POST +import eu.kanade.tachiyomi.util.parseAs +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import okhttp3.FormBody +import okhttp3.Request +import okhttp3.Response +import uy.kohesive.injekt.injectLazy + +abstract class AniListAnimeHttpSource : AnimeHttpSource() { + override val supportsLatest = true + val json by injectLazy() + + /* =============================== Mapping AniList <> Source =============================== */ + abstract fun mapAnimeDetailUrl(animeId: Int): String + + abstract fun mapAnimeId(animeDetailUrl: String): Int + + open fun getPreferredTitleLanguage(): TitleLanguage { + return TitleLanguage.ROMAJI + } + + /* ===================================== Popular Anime ===================================== */ + override fun popularAnimeRequest(page: Int): Request { + return buildAnimeListRequest( + query = ANIME_LIST_QUERY, + variables = AnimeListVariables( + page = page, + sort = AnimeListVariables.MediaSort.POPULARITY_DESC, + ), + ) + } + + override fun popularAnimeParse(response: Response): AnimesPage { + return parseAnimeListResponse(response) + } + + /* ===================================== Latest Anime ===================================== */ + override fun latestUpdatesRequest(page: Int): Request { + return buildAnimeListRequest( + query = LATEST_ANIME_LIST_QUERY, + variables = AnimeListVariables( + page = page, + sort = AnimeListVariables.MediaSort.START_DATE_DESC, + ), + ) + } + + override fun latestUpdatesParse(response: Response): AnimesPage { + return parseAnimeListResponse(response) + } + + /* ===================================== Search Anime ===================================== */ + override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request { + return buildAnimeListRequest( + query = ANIME_LIST_QUERY, + variables = AnimeListVariables( + page = page, + sort = AnimeListVariables.MediaSort.SEARCH_MATCH, + search = query.ifBlank { null }, + ), + ) + } + + override fun searchAnimeParse(response: Response): AnimesPage { + return parseAnimeListResponse(response) + } + + /* ===================================== Anime Details ===================================== */ + override fun animeDetailsRequest(anime: SAnime): Request { + return buildRequest( + query = ANIME_DETAILS_QUERY, + variables = json.encodeToString(AnimeDetailsVariables(mapAnimeId(anime.url))), + ) + } + + override fun animeDetailsParse(response: Response): SAnime { + val media = response.parseAs().data.media + + return media.toSAnime() + } + + override fun getAnimeUrl(anime: SAnime): String { + return anime.url + } + + /* ==================================== AniList Utility ==================================== */ + private fun buildAnimeListRequest( + query: String, + variables: AnimeListVariables, + ): Request { + return buildRequest(query, json.encodeToString(variables)) + } + + private fun buildRequest(query: String, variables: String): Request { + val requestBody = FormBody.Builder() + .add("query", query) + .add("variables", variables) + .build() + + return POST(url = "https://graphql.anilist.co", body = requestBody) + } + + private fun parseAnimeListResponse(response: Response): AnimesPage { + val page = response.parseAs().data.page + + return AnimesPage( + animes = page.media.map { it.toSAnime() }, + hasNextPage = page.pageInfo.hasNextPage, + ) + } + + private fun AniListMedia.toSAnime(): SAnime { + val otherNames = when (getPreferredTitleLanguage()) { + TitleLanguage.ROMAJI -> listOfNotNull(title.english, title.native) + TitleLanguage.ENGLISH -> listOfNotNull(title.romaji, title.native) + TitleLanguage.NATIVE -> listOfNotNull(title.romaji, title.english) + } + val newDescription = buildString { + append( + description + ?.replace("
\n
", "\n") + ?.replace("<.*?>".toRegex(), ""), + ) + if (otherNames.isNotEmpty()) { + appendLine() + appendLine() + append("Other name(s): ${otherNames.joinToString(", ")}") + } + } + val media = this + + return SAnime.create().apply { + url = mapAnimeDetailUrl(media.id) + title = parseTitle(media.title) + author = media.studios.nodes.joinToString(", ") { it.name } + description = newDescription + genre = media.genres.joinToString(", ") + status = when (media.status) { + AniListMedia.Status.RELEASING -> SAnime.ONGOING + AniListMedia.Status.FINISHED -> SAnime.COMPLETED + } + thumbnail_url = media.coverImage.large + } + } + + private fun parseTitle(title: AniListMedia.Title): String { + return when (getPreferredTitleLanguage()) { + TitleLanguage.ROMAJI -> title.romaji + TitleLanguage.ENGLISH -> title.english ?: title.romaji + TitleLanguage.NATIVE -> title.native ?: title.romaji + } + } + + enum class TitleLanguage { + ROMAJI, + ENGLISH, + NATIVE, + } +} diff --git a/lib-multisrc/anilist/src/eu/kanade/tachiyomi/multisrc/anilist/AniListRequest.kt b/lib-multisrc/anilist/src/eu/kanade/tachiyomi/multisrc/anilist/AniListRequest.kt new file mode 100644 index 00000000..00fcfac0 --- /dev/null +++ b/lib-multisrc/anilist/src/eu/kanade/tachiyomi/multisrc/anilist/AniListRequest.kt @@ -0,0 +1,73 @@ +package eu.kanade.tachiyomi.multisrc.anilist + +import kotlinx.serialization.Serializable + +internal const val MEDIA_QUERY = """ + id + title { + romaji + english + native + } + coverImage { + large + } + description + status + genres + studios(isMain: true) { + nodes { + name + } + } +""" + +internal const val ANIME_LIST_QUERY = """ + query (${"$"}page: Int, ${"$"}sort: [MediaSort], ${"$"}search: String) { + Page(page: ${"$"}page, perPage: 30) { + pageInfo { + hasNextPage + } + media(type: ANIME, sort: ${"$"}sort, search: ${"$"}search, status_in: [RELEASING, FINISHED], countryOfOrigin: "JP", isAdult: false) { + $MEDIA_QUERY + } + } + } +""" + +internal const val LATEST_ANIME_LIST_QUERY = """ + query (${"$"}page: Int, ${"$"}sort: [MediaSort], ${"$"}search: String) { + Page(page: ${"$"}page, perPage: 30) { + pageInfo { + hasNextPage + } + media(type: ANIME, sort: ${"$"}sort, search: ${"$"}search, status_in: [RELEASING, FINISHED], countryOfOrigin: "JP", isAdult: false, startDate_greater: 1, episodes_greater: 1) { + $MEDIA_QUERY + } + } + } +""" + +internal const val ANIME_DETAILS_QUERY = """ + query (${"$"}id: Int) { + Media(id: ${"$"}id) { + $MEDIA_QUERY + } + } +""" + +@Serializable +internal data class AnimeListVariables( + val page: Int, + val sort: MediaSort, + val search: String? = null, +) { + enum class MediaSort { + POPULARITY_DESC, + SEARCH_MATCH, + START_DATE_DESC, + } +} + +@Serializable +internal data class AnimeDetailsVariables(val id: Int) diff --git a/lib-multisrc/anilist/src/eu/kanade/tachiyomi/multisrc/anilist/AniListResponse.kt b/lib-multisrc/anilist/src/eu/kanade/tachiyomi/multisrc/anilist/AniListResponse.kt new file mode 100644 index 00000000..d445e8e9 --- /dev/null +++ b/lib-multisrc/anilist/src/eu/kanade/tachiyomi/multisrc/anilist/AniListResponse.kt @@ -0,0 +1,57 @@ +package eu.kanade.tachiyomi.multisrc.anilist + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +internal data class AniListAnimeListResponse(val data: Data) { + @Serializable + data class Data(@SerialName("Page") val page: Page) { + @Serializable + data class Page( + val pageInfo: PageInfo, + val media: List, + ) { + @Serializable + data class PageInfo(val hasNextPage: Boolean) + } + } +} + +@Serializable +internal data class AniListAnimeDetailsResponse(val data: Data) { + @Serializable + data class Data(@SerialName("Media") val media: AniListMedia) +} + +@Serializable +internal data class AniListMedia( + val id: Int, + val title: Title, + val coverImage: CoverImage, + val description: String?, + val status: Status, + val genres: List, + val studios: Studios, +) { + @Serializable + data class Title( + val romaji: String, + val english: String?, + val native: String?, + ) + + @Serializable + data class CoverImage(val large: String) + + enum class Status { + RELEASING, + FINISHED, + } + + @Serializable + data class Studios(val nodes: List) { + @Serializable + data class Node(val name: String) + } +} diff --git a/lib/vidsrc-extractor/src/main/java/eu/kanade/tachiyomi/lib/vidsrcextractor/VidSrcExtractor.kt b/lib/vidsrc-extractor/src/main/java/eu/kanade/tachiyomi/lib/vidsrcextractor/VidSrcExtractor.kt index aecda1f6..c6cf9db4 100644 --- a/lib/vidsrc-extractor/src/main/java/eu/kanade/tachiyomi/lib/vidsrcextractor/VidSrcExtractor.kt +++ b/lib/vidsrc-extractor/src/main/java/eu/kanade/tachiyomi/lib/vidsrcextractor/VidSrcExtractor.kt @@ -1,18 +1,19 @@ package eu.kanade.tachiyomi.lib.vidsrcextractor import android.util.Base64 -import app.cash.quickjs.QuickJs import eu.kanade.tachiyomi.animesource.model.Track import eu.kanade.tachiyomi.animesource.model.Video import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils +import eu.kanade.tachiyomi.lib.vidsrcextractor.MediaResponseBody.Result import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.util.parseAs import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable -import okhttp3.CacheControl +import kotlinx.serialization.json.Json import okhttp3.Headers import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.OkHttpClient +import uy.kohesive.injekt.injectLazy import java.net.URLDecoder import javax.crypto.Cipher import javax.crypto.spec.SecretKeySpec @@ -21,54 +22,32 @@ import javax.crypto.spec.SecretKeySpec class VidsrcExtractor(private val client: OkHttpClient, private val headers: Headers) { private val playlistUtils by lazy { PlaylistUtils(client, headers) } + private val json: Json by injectLazy() - private val cacheControl = CacheControl.Builder().noStore().build() - private val noCacheClient = client.newBuilder() - .cache(null) - .build() - - private val keys by lazy { - noCacheClient.newCall( - GET("https://raw.githubusercontent.com/KillerDogeEmpire/vidplay-keys/keys/keys.json", cache = cacheControl), - ).execute().parseAs>() - } - - fun videosFromUrl(embedLink: String, hosterName: String, type: String = "", subtitleList: List = emptyList()): List