diff --git a/src/ja/micmicidol/build.gradle b/src/ja/micmicidol/build.gradle new file mode 100644 index 000000000..ff8fe12cc --- /dev/null +++ b/src/ja/micmicidol/build.gradle @@ -0,0 +1,8 @@ +ext { + extName = "MIC MIC IDOL" + extClass = ".MicMicIdol" + extVersionCode = 1 + isNsfw = true +} + +apply from: "$rootDir/common.gradle" diff --git a/src/ja/micmicidol/res/mipmap-hdpi/ic_launcher.png b/src/ja/micmicidol/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..bf122a8d2 Binary files /dev/null and b/src/ja/micmicidol/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/ja/micmicidol/res/mipmap-mdpi/ic_launcher.png b/src/ja/micmicidol/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..d40159169 Binary files /dev/null and b/src/ja/micmicidol/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/ja/micmicidol/res/mipmap-xhdpi/ic_launcher.png b/src/ja/micmicidol/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..06e9e7ee4 Binary files /dev/null and b/src/ja/micmicidol/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/ja/micmicidol/res/mipmap-xxhdpi/ic_launcher.png b/src/ja/micmicidol/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..92357edc4 Binary files /dev/null and b/src/ja/micmicidol/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/ja/micmicidol/res/mipmap-xxxhdpi/ic_launcher.png b/src/ja/micmicidol/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..22e1852f1 Binary files /dev/null and b/src/ja/micmicidol/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/ja/micmicidol/src/eu/kanade/tachiyomi/extension/ja/micmicidol/BloggerDto.kt b/src/ja/micmicidol/src/eu/kanade/tachiyomi/extension/ja/micmicidol/BloggerDto.kt new file mode 100644 index 000000000..1f4bce077 --- /dev/null +++ b/src/ja/micmicidol/src/eu/kanade/tachiyomi/extension/ja/micmicidol/BloggerDto.kt @@ -0,0 +1,49 @@ +package eu.kanade.tachiyomi.extension.ja.micmicidol + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class BloggerDto( + val feed: BloggerFeedDto, +) + +@Serializable +data class BloggerFeedDto( + @SerialName("openSearch\$totalResults") val totalResults: BloggerTextDto, + @SerialName("openSearch\$startIndex") val startIndex: BloggerTextDto, + @SerialName("openSearch\$itemsPerPage") val itemsPerPage: BloggerTextDto, + val category: List = emptyList(), + val entry: List = emptyList(), +) + +@Serializable +data class BloggerFeedEntryDto( + val published: BloggerTextDto, + val category: List, + val title: BloggerTextDto, + val content: BloggerTextDto, + val link: List, + val author: List, +) + +@Serializable +data class BloggerLinkDto( + val rel: String, + val href: String, +) + +@Serializable +data class BloggerCategoryDto( + val term: String, +) + +@Serializable +data class BloggerAuthorDto( + val name: BloggerTextDto, +) + +@Serializable +data class BloggerTextDto( + @SerialName("\$t") val t: String, +) diff --git a/src/ja/micmicidol/src/eu/kanade/tachiyomi/extension/ja/micmicidol/MicMicIdol.kt b/src/ja/micmicidol/src/eu/kanade/tachiyomi/extension/ja/micmicidol/MicMicIdol.kt new file mode 100644 index 000000000..4baa08330 --- /dev/null +++ b/src/ja/micmicidol/src/eu/kanade/tachiyomi/extension/ja/micmicidol/MicMicIdol.kt @@ -0,0 +1,182 @@ +package eu.kanade.tachiyomi.extension.ja.micmicidol + +import android.os.Build +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.source.model.Filter +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.model.UpdateStrategy +import eu.kanade.tachiyomi.source.online.HttpSource +import eu.kanade.tachiyomi.util.asJsoup +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Request +import okhttp3.Response +import org.jsoup.Jsoup +import rx.Observable +import uy.kohesive.injekt.injectLazy +import java.text.SimpleDateFormat +import java.util.Locale + +class MicMicIdol : HttpSource() { + + override val name = "MIC MIC IDOL" + + override val baseUrl = "https://www.micmicidol.club" + + override val lang = "ja" + + override val supportsLatest = false + + override val client = network.cloudflareClient + + override fun headersBuilder() = super.headersBuilder() + .add("Referer", "$baseUrl/") + + private val json: Json by injectLazy() + + private val dateFormat by lazy { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX", Locale.getDefault()) + } else { + SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) + } + } + + override fun popularMangaRequest(page: Int) = GET(apiUrlBuilder(page).build(), headers) + + override fun popularMangaParse(response: Response): MangasPage { + val data = json.decodeFromString(response.body.string()) + + categories = data.feed.category.map { it.term } + + val manga = data.feed.entry.map { entry -> + val content = Jsoup.parseBodyFragment(entry.content.t, baseUrl) + + SManga.create().apply { + setUrlWithoutDomain(entry.link.first { it.rel == "alternate" }.href + "#${entry.published.t}") + title = entry.title.t + thumbnail_url = content.selectFirst("img")?.absUrl("src") + genre = entry.category.joinToString { it.term } + status = SManga.COMPLETED + update_strategy = UpdateStrategy.ONLY_FETCH_ONCE + initialized = true + } + } + val hasNextPage = (data.feed.startIndex.t.toInt() + data.feed.itemsPerPage.t.toInt()) <= data.feed.totalResults.t.toInt() + + return MangasPage(manga, hasNextPage) + } + + override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException() + + override fun latestUpdatesParse(response: Response) = throw UnsupportedOperationException() + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val filterList = filters.ifEmpty { getFilterList() } + val searchQuery = buildString { + filterList.filterIsInstance().forEach { + it.state + .filter { f -> f.state } + .forEach { f -> + append(" label:\"") + append(f.name) + append("\"") + } + } + + if (query.isNotEmpty()) { + append(" ") + append(query) + } + }.trim() + val url = apiUrlBuilder(page) + .addQueryParameter("q", searchQuery) + .build() + + return GET(url, headers) + } + + override fun searchMangaParse(response: Response) = popularMangaParse(response) + + override fun fetchMangaDetails(manga: SManga): Observable = Observable.just(manga) + + override fun getMangaUrl(manga: SManga) = "$baseUrl${manga.url}".substringBefore("#") + + override fun mangaDetailsRequest(manga: SManga) = throw UnsupportedOperationException() + + override fun mangaDetailsParse(response: Response) = throw UnsupportedOperationException() + + override fun fetchChapterList(manga: SManga): Observable> { + val date = manga.url.substringAfter("#") + + return Observable.just( + listOf( + SChapter.create().apply { + url = manga.url.substringBefore("#") + name = "Gallery" + date_upload = runCatching { + dateFormat.parse(date)!!.time + }.getOrDefault(0L) + }, + ), + ) + } + + override fun chapterListParse(response: Response) = throw UnsupportedOperationException() + + override fun pageListParse(response: Response): List { + val document = response.asJsoup() + + return document.select("div.post-body img").mapIndexed { i, it -> + Page(i, imageUrl = it.absUrl("src")) + } + } + + override fun imageUrlParse(response: Response) = throw UnsupportedOperationException() + + override fun getFilterList(): FilterList { + val types = getTypes() + val japanMagazines = getJapanMagazines() + val japanFashion = getJapanFashion() + + val filters = mutableListOf>( + LabelFilter("Type", types.map { Label(it) }), + LabelFilter("Japan Magazine", japanMagazines.map { Label(it) }), + LabelFilter("Japan Fashion", japanFashion.map { Label(it) }), + ).apply { + if (categories.isEmpty()) { + add(0, Filter.Header("Press 'Reset' to show extra filters")) + add(1, Filter.Separator()) + return@apply + } + + val others = categories + .filterNot { types.contains(it) || japanMagazines.contains(it) || japanFashion.contains(it) } + + add(LabelFilter("Other", others.map { Label(it) })) + } + + return FilterList(filters) + } + + private var categories = emptyList() + + private fun apiUrlBuilder(page: Int) = baseUrl.toHttpUrl().newBuilder().apply { + // Blogger indices start from 1 + val startIndex = MAX_RESULTS * (page - 1) + 1 + + addPathSegments("feeds/posts/default") + addQueryParameter("alt", "json") + addQueryParameter("max-results", MAX_RESULTS.toString()) + addQueryParameter("start-index", startIndex.toString()) + } + + companion object { + private const val MAX_RESULTS = 25 + } +} diff --git a/src/ja/micmicidol/src/eu/kanade/tachiyomi/extension/ja/micmicidol/MicMicIdolFilters.kt b/src/ja/micmicidol/src/eu/kanade/tachiyomi/extension/ja/micmicidol/MicMicIdolFilters.kt new file mode 100644 index 000000000..f1c8c46b9 --- /dev/null +++ b/src/ja/micmicidol/src/eu/kanade/tachiyomi/extension/ja/micmicidol/MicMicIdolFilters.kt @@ -0,0 +1,59 @@ +package eu.kanade.tachiyomi.extension.ja.micmicidol + +import eu.kanade.tachiyomi.source.model.Filter + +class LabelFilter(name: String, labels: List