diff --git a/src/fr/bigsolo/AndroidManifest.xml b/src/fr/bigsolo/AndroidManifest.xml new file mode 100644 index 000000000..03a4d85ef --- /dev/null +++ b/src/fr/bigsolo/AndroidManifest.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + diff --git a/src/fr/bigsolo/build.gradle b/src/fr/bigsolo/build.gradle index 485e9b810..c68cc1018 100644 --- a/src/fr/bigsolo/build.gradle +++ b/src/fr/bigsolo/build.gradle @@ -1,9 +1,7 @@ ext { extName = 'BigSolo' extClass = '.BigSolo' - themePkg = 'scanr' - baseUrl = 'https://www.bigsolo.org' - overrideVersionCode = 1 + extVersionCode = 3 isNsfw = false } diff --git a/src/fr/bigsolo/src/eu/kanade/tachiyomi/extension/fr/bigsolo/BigSolo.kt b/src/fr/bigsolo/src/eu/kanade/tachiyomi/extension/fr/bigsolo/BigSolo.kt index 8ce0c995f..36b1ea8a1 100644 --- a/src/fr/bigsolo/src/eu/kanade/tachiyomi/extension/fr/bigsolo/BigSolo.kt +++ b/src/fr/bigsolo/src/eu/kanade/tachiyomi/extension/fr/bigsolo/BigSolo.kt @@ -1,10 +1,164 @@ package eu.kanade.tachiyomi.extension.fr.bigsolo -import eu.kanade.tachiyomi.multisrc.scanr.ScanR +import eu.kanade.tachiyomi.network.GET +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 keiyoushi.utils.parseAs +import okhttp3.Request +import okhttp3.Response +import java.net.URI -class BigSolo : ScanR( - name = "Big Solo", - baseUrl = "https://bigsolo.org", - lang = "fr", - useHighLowQualityCover = true, -) +class BigSolo : HttpSource() { + + override val name = "BigSolo" + override val baseUrl = "https://bigsolo.org" + override val lang = "fr" + override val supportsLatest = true + override val id = 4410528266393104437 + + // Popular + override fun popularMangaRequest(page: Int): Request { + return GET("$baseUrl/data/series", headers) + } + + override fun popularMangaParse(response: Response): MangasPage { + val series = response.parseAs() + val mangaList = mutableListOf() + + for (serie in series.reco) { + mangaList.add(serie.toDetailedSManga()) + } + + return MangasPage(mangaList, false) + } + + // Latest + override fun latestUpdatesRequest(page: Int): Request { + return GET("$baseUrl/data/series", headers) + } + + override fun latestUpdatesParse(response: Response): MangasPage = searchMangaParse(response) + + // Search + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val url = if (query.isNotBlank()) { + "$baseUrl/data/series#$query" + } else { + "$baseUrl/data/series" + } + return GET(url, headers) + } + + override fun searchMangaParse(response: Response): MangasPage { + val series = response.parseAs() + val allSeries = (series.series + series.os).sortedByDescending { it.lastChapter?.timestamp } + val mangaList = mutableListOf() + + val fragment = response.request.url.fragment + val searchQuery = fragment ?: "" + + if (searchQuery.startsWith("SLUG:")) { + val serie = allSeries.find { it.slug == searchQuery.removePrefix("SLUG:") } + if (serie != null) { + mangaList.add(serie.toDetailedSManga()) + } + return MangasPage(mangaList, false) + } + + for (serie in allSeries) { + if (searchQuery.isBlank() || + serie.title.contains(searchQuery, ignoreCase = true) || + serie.alternativeTitles.any { it.contains(searchQuery, ignoreCase = true) } || + serie.jaTitle.contains(searchQuery, ignoreCase = true) + ) { + mangaList.add(serie.toDetailedSManga()) + } + } + + return MangasPage(mangaList, false) + } + + // Details + override fun mangaDetailsRequest(manga: SManga): Request { + val splitedPath = URI(manga.url).path.split("/") + val slug = splitedPath[1] + return GET("$baseUrl/data/series/$slug", headers) + } + + override fun getMangaUrl(manga: SManga): String { + return "$baseUrl${manga.url}" + } + + override fun mangaDetailsParse(response: Response): SManga { + val serie = response.parseAs() + return serie.toDetailedSManga() + } + + // Pages + override fun pageListRequest(chapter: SChapter): Request { + val splitedPath = URI(chapter.url).path.split("/") + val slug = splitedPath[1] + val chapterId = splitedPath[2] + return GET("$baseUrl/data/series/$slug/$chapterId", headers) + } + + override fun pageListParse(response: Response): List { + val chapterDetails = response.parseAs() + return chapterDetails.images.mapIndexed { index, pageData -> + Page(index, imageUrl = pageData) + } + } + + // Chapters + override fun chapterListRequest(manga: SManga): Request { + val slug = URI(manga.url).path.split("/")[1] + return GET("$baseUrl/data/series/$slug", headers) + } + + override fun chapterListParse(response: Response): List { + val seriesData = response.parseAs() + return buildChapterList(seriesData) + } + private fun buildChapterList(serie: Serie): List { + val chapters = serie.chapters + val chapterList = mutableListOf() + val multipleChapters = chapters.size > 1 + + for ((chapterNumber, chapterData) in chapters) { + if (chapterData.licencied) continue + + val title = chapterData.title + val volumeNumber = chapterData.volume + + val baseName = if (multipleChapters) { + buildString { + if (volumeNumber.isNotBlank()) append("Vol. $volumeNumber ") + append("Ch. $chapterNumber") + if (title.isNotBlank()) append(" – $title") + } + } else { + if (title.isNotBlank()) "One Shot – $title" else "One Shot" + } + + val chapter = SChapter.create().apply { + name = baseName + setUrlWithoutDomain("$baseUrl/${serie.slug}/$chapterNumber") + chapter_number = chapterNumber.toFloatOrNull() ?: -1f + scanlator = chapterData.teams.joinToString(" & ") + date_upload = chapterData.timestamp * 1000L + } + chapterList.add(chapter) + } + + return chapterList.sortedByDescending { it.chapter_number } + } + + // Unsupported Stuff + override fun imageUrlParse(response: Response): String { + throw UnsupportedOperationException() + } +} diff --git a/src/fr/bigsolo/src/eu/kanade/tachiyomi/extension/fr/bigsolo/BigSoloDto.kt b/src/fr/bigsolo/src/eu/kanade/tachiyomi/extension/fr/bigsolo/BigSoloDto.kt new file mode 100644 index 000000000..d5bda3f30 --- /dev/null +++ b/src/fr/bigsolo/src/eu/kanade/tachiyomi/extension/fr/bigsolo/BigSoloDto.kt @@ -0,0 +1,77 @@ +package eu.kanade.tachiyomi.extension.fr.bigsolo + +import eu.kanade.tachiyomi.source.model.SManga +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Data Transfer Objects for BigSolo extension + */ + +@Serializable +class SeriesResponse( + val series: List, + val os: List, + val reco: List, +) + +@Serializable +class Serie( + val slug: String = "", + val title: String, + val description: String, + val artist: String, + val author: String, + val tags: List, + @SerialName("ja_title") + val jaTitle: String, + @SerialName("alternative_titles") + val alternativeTitles: List, + val status: String, + val cover: Cover? = null, + val chapters: Map, + @SerialName("last_chapter") + val lastChapter: lastChapter? = null, +) + +@Serializable +class lastChapter( + val timestamp: Int, +) + +@Serializable +class Chapter( + val title: String, + val volume: String = "", + val timestamp: Int, + val teams: List, + @SerialName("licensed") + val licencied: Boolean = false, +) + +@Serializable +class ChapterDetails( + val images: List, +) + +@Serializable +class Cover( + @SerialName("url_hq") + val urlHq: String, +) + +// DTO to SManga extension functions +fun Serie.toDetailedSManga(): SManga = SManga.create().apply { + title = this@toDetailedSManga.title + description = this@toDetailedSManga.description + artist = this@toDetailedSManga.artist + author = this@toDetailedSManga.author + genre = this@toDetailedSManga.tags.joinToString() + status = when (this@toDetailedSManga.status) { + "En cours" -> SManga.ONGOING + "Finis", "Fini" -> SManga.COMPLETED + else -> SManga.UNKNOWN + } + thumbnail_url = this@toDetailedSManga.cover?.urlHq + url = "/${this@toDetailedSManga.slug}" +} diff --git a/src/fr/bigsolo/src/eu/kanade/tachiyomi/extension/fr/bigsolo/BigSoloUrlActivity.kt b/src/fr/bigsolo/src/eu/kanade/tachiyomi/extension/fr/bigsolo/BigSoloUrlActivity.kt new file mode 100644 index 000000000..6a3fbf9d1 --- /dev/null +++ b/src/fr/bigsolo/src/eu/kanade/tachiyomi/extension/fr/bigsolo/BigSoloUrlActivity.kt @@ -0,0 +1,38 @@ +package eu.kanade.tachiyomi.extension.fr.bigsolo + +import android.app.Activity +import android.content.ActivityNotFoundException +import android.content.Intent +import android.os.Bundle +import android.util.Log +import kotlin.system.exitProcess + +/** + * Springboard that accepts https://bigsolo.org/xxxxxx intents and redirects them to + * the main Tachiyomi process. + */ +class BigSoloUrlActivity : Activity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val pathSegments = intent?.data?.pathSegments + if (pathSegments != null && pathSegments.size >= 1) { + val slug = pathSegments[0] + val mainIntent = Intent().apply { + action = "eu.kanade.tachiyomi.SEARCH" + putExtra("query", "SLUG:$slug") + putExtra("filter", packageName) + } + + try { + startActivity(mainIntent) + } catch (e: ActivityNotFoundException) { + Log.e("BigSoloUrlActivity", e.toString()) + } + } else { + Log.e("BigSoloUrlActivity", "could not parse uri from intent $intent") + } + + finish() + exitProcess(0) + } +}