diff --git a/lib/dataimage/build.gradle b/lib/dataimage/build.gradle new file mode 100644 index 000000000..3fdb185de --- /dev/null +++ b/lib/dataimage/build.gradle @@ -0,0 +1,30 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 27 + buildToolsVersion '29.0.3' + + defaultConfig { + minSdkVersion 16 + targetSdkVersion 27 + versionCode 1 + versionName '1.0.0' + } + + buildTypes { + release { + minifyEnabled false + } + } +} + +repositories { + mavenCentral() +} + +dependencies { + compileOnly "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + compileOnly 'com.squareup.okhttp3:okhttp:3.10.0' + compileOnly 'org.jsoup:jsoup:1.13.1' +} diff --git a/lib/dataimage/src/main/AndroidManifest.xml b/lib/dataimage/src/main/AndroidManifest.xml new file mode 100644 index 000000000..11d9e3ff5 --- /dev/null +++ b/lib/dataimage/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/lib/dataimage/src/main/java/eu/kanade/tachiyomi/lib/dataimage/DataImageInterceptor.kt b/lib/dataimage/src/main/java/eu/kanade/tachiyomi/lib/dataimage/DataImageInterceptor.kt new file mode 100644 index 000000000..df1cc81f6 --- /dev/null +++ b/lib/dataimage/src/main/java/eu/kanade/tachiyomi/lib/dataimage/DataImageInterceptor.kt @@ -0,0 +1,65 @@ +package eu.kanade.tachiyomi.lib.dataimage + +import android.util.Base64 +import okhttp3.Interceptor +import okhttp3.MediaType +import okhttp3.Protocol +import okhttp3.Response +import okhttp3.ResponseBody +import org.jsoup.nodes.Element + +/** + * If a source provides images via a data:image string instead of a URL, use these functions and interceptor + */ + +/** + * Use if the attribute tag could have a data:image string or URL + * Transforms data:image in to a fake URL that OkHttp won't die on + */ +fun Element.dataImageAsUrl(attr: String): String { + return if (this.attr(attr).startsWith("data")) { + "https://127.0.0.1/?" + this.attr(attr).substringAfter(":") + } else { + this.attr("abs:$attr") + } +} + +/** + * Use if the attribute tag has a data:image string but real URLs are on a different attribute + */ +fun Element.dataImageAsUrlOrNull(attr: String): String? { + return if (this.attr(attr).startsWith("data")) { + "https://127.0.0.1/?" + this.attr(attr).substringAfter(":") + } else { + null + } +} + +/** + * Interceptor that detects the URLs we created with the above functions, base64 decodes the data if necessary, + * and builds a response with a valid image that Tachiyomi can display + */ +class DataImageInterceptor : Interceptor { + private val mediaTypePattern = Regex("""(^[^;,]*)[;,]""") + + override fun intercept(chain: Interceptor.Chain): Response { + val url = chain.request().url().toString() + return if (url.startsWith("https://127.0.0.1/?image")) { + val dataString = url.substringAfter("?") + val byteArray = if (dataString.contains("base64")) { + Base64.decode(dataString.substringAfter("base64,"), Base64.DEFAULT) + } else { + dataString.substringAfter(",").toByteArray() + } + val mediaType = MediaType.parse(mediaTypePattern.find(dataString)!!.value) + Response.Builder().body(ResponseBody.create(mediaType, byteArray)) + .request(chain.request()) + .protocol(Protocol.HTTP_1_0) + .code(200) + .message("") + .build() + } else { + chain.proceed(chain.request()) + } + } +} diff --git a/settings.gradle b/settings.gradle index 3cabe6210..dc61dc57a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -4,6 +4,9 @@ project(':lib-ratelimit').projectDir = new File("lib/ratelimit") include ':duktape-stub' project(':duktape-stub').projectDir = new File("lib/duktape-stub") +include ':lib-dataimage' +project(':lib-dataimage').projectDir = new File("lib/dataimage") + new File(rootDir, "src").eachDir { dir -> dir.eachDir { subdir -> String name = ":${dir.name}-${subdir.name}" diff --git a/src/tr/mangaship/build.gradle b/src/tr/mangaship/build.gradle new file mode 100644 index 000000000..75a7fad96 --- /dev/null +++ b/src/tr/mangaship/build.gradle @@ -0,0 +1,17 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +ext { + appName = 'Tachiyomi: Manga Ship' + pkgNameSuffix = 'tr.mangaship' + extClass = '.MangaShip' + extVersionCode = 1 + libVersion = '1.2' +} + +dependencies { + implementation project(':lib-dataimage') +} + +apply from: "$rootDir/common.gradle" + diff --git a/src/tr/mangaship/res/mipmap-hdpi/ic_launcher.png b/src/tr/mangaship/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..9dce78c31 Binary files /dev/null and b/src/tr/mangaship/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/tr/mangaship/res/mipmap-mdpi/ic_launcher.png b/src/tr/mangaship/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..e50537d01 Binary files /dev/null and b/src/tr/mangaship/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/tr/mangaship/res/mipmap-xhdpi/ic_launcher.png b/src/tr/mangaship/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..395cee294 Binary files /dev/null and b/src/tr/mangaship/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/tr/mangaship/res/mipmap-xxhdpi/ic_launcher.png b/src/tr/mangaship/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..fca129e10 Binary files /dev/null and b/src/tr/mangaship/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/tr/mangaship/res/mipmap-xxxhdpi/ic_launcher.png b/src/tr/mangaship/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..a92a4a389 Binary files /dev/null and b/src/tr/mangaship/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/tr/mangaship/res/web_hi_res_512.png b/src/tr/mangaship/res/web_hi_res_512.png new file mode 100644 index 000000000..9cb5ebcbc Binary files /dev/null and b/src/tr/mangaship/res/web_hi_res_512.png differ diff --git a/src/tr/mangaship/src/eu/kanade/tachiyomi/extension/tr/mangaship/MangaShip.kt b/src/tr/mangaship/src/eu/kanade/tachiyomi/extension/tr/mangaship/MangaShip.kt new file mode 100644 index 000000000..1e1cfdc58 --- /dev/null +++ b/src/tr/mangaship/src/eu/kanade/tachiyomi/extension/tr/mangaship/MangaShip.kt @@ -0,0 +1,107 @@ +package eu.kanade.tachiyomi.extension.tr.mangaship + +import eu.kanade.tachiyomi.lib.dataimage.DataImageInterceptor +import eu.kanade.tachiyomi.lib.dataimage.dataImageAsUrl +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.source.model.FilterList +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.ParsedHttpSource +import okhttp3.OkHttpClient +import okhttp3.Request +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element + +class MangaShip : ParsedHttpSource() { + + override val name = "Manga Ship" + + override val baseUrl = "https://www.mangaship.com" + + override val lang = "tr" + + override val supportsLatest = true + + override val client: OkHttpClient = network.cloudflareClient.newBuilder() + .addInterceptor(DataImageInterceptor()) + .build() + + // Popular + + override fun popularMangaRequest(page: Int): Request { + return GET("$baseUrl/Tr/PopulerMangalar?page=$page", headers) + } + + override fun popularMangaSelector() = "div.movie-item-contents" + + override fun popularMangaFromElement(element: Element): SManga { + return SManga.create().apply { + element.select("div.movie-item-title a").let { + title = it.text() + setUrlWithoutDomain(it.attr("href")) + } + thumbnail_url = element.select("img").attr("abs:src") + } + } + + override fun popularMangaNextPageSelector() = "li.active + li a:not(.lastpage)" + + // Latest + + override fun latestUpdatesRequest(page: Int): Request { + return GET("$baseUrl/Tr/YeniMangalar?page=$page", headers) + } + + override fun latestUpdatesSelector() = popularMangaSelector() + + override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element) + + override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector() + + // Search + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + return GET("$baseUrl/Tr/Search?kelime=$query&tur=Manga&page=$page", headers) + } + + override fun searchMangaSelector() = popularMangaSelector() + + override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element) + + override fun searchMangaNextPageSelector() = popularMangaNextPageSelector() + + // Details + + override fun mangaDetailsParse(document: Document): SManga { + return SManga.create().apply { + thumbnail_url = document.select("div.dec-review-img img").attr("abs:src") + genre = document.select("div.col-md-10 li:contains(Kategori) div a").joinToString { it.text() } + author = document.select("div.col-md-10 li:contains(Yazar) div a").text() + description = document.select("div.details-dectiontion p").joinToString("\n") { it.text() } + } + } + + // Chapters + + override fun chapterListSelector() = "div.item > div" + + override fun chapterFromElement(element: Element): SChapter { + return SChapter.create().apply { + element.select("div.plylist-single-content > a[title]").let { + name = it.text() + setUrlWithoutDomain(it.attr("href")) + } + } + } + + // Pages + + override fun pageListParse(document: Document): List { + return document.select("div.reading-content img").mapIndexed { i, img -> + Page(i, "", img.dataImageAsUrl("src")) + } + } + + override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used") +}