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 abcdb4d7..f39ffed9 100644 --- a/.github/workflows/build_push.yml +++ b/.github/workflows/build_push.yml @@ -40,6 +40,14 @@ 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' 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/vidmoly-extractor/build.gradle.kts b/lib/vidmoly-extractor/build.gradle.kts new file mode 100644 index 00000000..a503203d --- /dev/null +++ b/lib/vidmoly-extractor/build.gradle.kts @@ -0,0 +1,7 @@ +plugins { + id("lib-android") +} + +dependencies { + implementation(project(":lib:playlist-utils")) +} \ No newline at end of file diff --git a/lib/vidmoly-extractor/src/main/java/eu/kanade/tachiyomi/lib/vidmolyextractor/VidMolyExtractor.kt b/lib/vidmoly-extractor/src/main/java/eu/kanade/tachiyomi/lib/vidmolyextractor/VidMolyExtractor.kt new file mode 100644 index 00000000..745f6b5b --- /dev/null +++ b/lib/vidmoly-extractor/src/main/java/eu/kanade/tachiyomi/lib/vidmolyextractor/VidMolyExtractor.kt @@ -0,0 +1,40 @@ +package eu.kanade.tachiyomi.lib.vidmolyextractor + +import eu.kanade.tachiyomi.animesource.model.Video +import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.util.asJsoup +import okhttp3.Headers +import okhttp3.OkHttpClient +import okhttp3.internal.EMPTY_HEADERS + +class VidMolyExtractor(private val client: OkHttpClient, headers: Headers = EMPTY_HEADERS) { + + private val baseUrl = "https://vidmoly.to" + + private val playlistUtils by lazy { PlaylistUtils(client) } + + private val headers: Headers = headers.newBuilder() + .set("Origin", baseUrl) + .set("Referer", "$baseUrl/") + .build() + + private val sourcesRegex = Regex("sources: (.*?]),") + private val urlsRegex = Regex("""file:"(.*?)"""") + + fun videosFromUrl(url: String, prefix: String = ""): List