A Pair Of 2+ : move to individual extension (#15757)
* Remove from Madara * APairOf2: rewrite for new site
@ -38,7 +38,6 @@ class MadaraGenerator : ThemeSourceGenerator {
 | 
			
		||||
        SingleLang("Anikiga", "https://anikiga.com", "tr"),
 | 
			
		||||
        SingleLang("Anisa Manga", "https://anisamanga.com", "tr"),
 | 
			
		||||
        SingleLang("Ansh Scans", "https://anshscans.org", "en"),
 | 
			
		||||
        SingleLang("A Pair of 2+", "https://po2scans.com", "en", className = "APairOf2"),
 | 
			
		||||
        SingleLang("ApollComics", "https://apollcomics.xyz", "es", isNsfw = true, overrideVersionCode = 2),
 | 
			
		||||
        SingleLang("Apolltoons", "https://apolltoons.xyz", "es", isNsfw = true),
 | 
			
		||||
        SingleLang("Aqua Manga", "https://aquamanga.com", "en", overrideVersionCode = 3),
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										24
									
								
								src/en/apairof2/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,24 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    package="eu.kanade.tachiyomi.extension">
 | 
			
		||||
 | 
			
		||||
    <application>
 | 
			
		||||
        <activity
 | 
			
		||||
            android:name=".en.apairof2.APairOf2UrlActivity"
 | 
			
		||||
            android:excludeFromRecents="true"
 | 
			
		||||
            android:exported="true"
 | 
			
		||||
            android:theme="@android:style/Theme.NoDisplay">
 | 
			
		||||
            <intent-filter>
 | 
			
		||||
                <action android:name="android.intent.action.VIEW" />
 | 
			
		||||
 | 
			
		||||
                <category android:name="android.intent.category.DEFAULT" />
 | 
			
		||||
                <category android:name="android.intent.category.BROWSABLE" />
 | 
			
		||||
 | 
			
		||||
                <data
 | 
			
		||||
                    android:host="po2scans.com"
 | 
			
		||||
                    android:pathPattern="/series/..*"
 | 
			
		||||
                    android:scheme="https" />
 | 
			
		||||
            </intent-filter>
 | 
			
		||||
        </activity>
 | 
			
		||||
    </application>
 | 
			
		||||
</manifest>
 | 
			
		||||
							
								
								
									
										11
									
								
								src/en/apairof2/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,11 @@
 | 
			
		||||
apply plugin: 'com.android.application'
 | 
			
		||||
apply plugin: 'kotlin-android'
 | 
			
		||||
 | 
			
		||||
ext {
 | 
			
		||||
    extName = 'A Pair of 2+'
 | 
			
		||||
    pkgNameSuffix = 'en.apairof2'
 | 
			
		||||
    extClass = '.APairOf2'
 | 
			
		||||
    extVersionCode = 29
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
apply from: "$rootDir/common.gradle"
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB  | 
| 
		 Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB  | 
| 
		 Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB  | 
| 
		 Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 7.4 KiB  | 
| 
		 Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB  | 
| 
		 Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB  | 
@ -0,0 +1,155 @@
 | 
			
		||||
package eu.kanade.tachiyomi.extension.en.apairof2
 | 
			
		||||
 | 
			
		||||
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.ParsedHttpSource
 | 
			
		||||
import okhttp3.Headers
 | 
			
		||||
import okhttp3.OkHttpClient
 | 
			
		||||
import okhttp3.Request
 | 
			
		||||
import org.jsoup.nodes.Document
 | 
			
		||||
import org.jsoup.nodes.Element
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import java.text.SimpleDateFormat
 | 
			
		||||
import java.util.Locale
 | 
			
		||||
 | 
			
		||||
class APairOf2 : ParsedHttpSource() {
 | 
			
		||||
 | 
			
		||||
    override val name = "A Pair of 2+"
 | 
			
		||||
 | 
			
		||||
    override val baseUrl = "https://po2scans.com"
 | 
			
		||||
 | 
			
		||||
    override val lang = "en"
 | 
			
		||||
 | 
			
		||||
    override val supportsLatest = true
 | 
			
		||||
 | 
			
		||||
    override val versionId = 2
 | 
			
		||||
 | 
			
		||||
    override val client: OkHttpClient = network.cloudflareClient
 | 
			
		||||
 | 
			
		||||
    override fun headersBuilder(): Headers.Builder = super.headersBuilder()
 | 
			
		||||
        .add("Referer", "$baseUrl/")
 | 
			
		||||
 | 
			
		||||
    // popular
 | 
			
		||||
    override fun popularMangaRequest(page: Int): Request {
 | 
			
		||||
        return GET("$baseUrl/series", headers)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun popularMangaSelector() = "div.series-list"
 | 
			
		||||
 | 
			
		||||
    override fun popularMangaFromElement(element: Element): SManga {
 | 
			
		||||
        return SManga.create().apply {
 | 
			
		||||
            title = element.select("div > h2").text()
 | 
			
		||||
            url = element.select("div > a").attr("href").let { "/$it" }
 | 
			
		||||
            thumbnail_url = element.select("img").attr("data-src").let { "$baseUrl/$it" }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // TODO: add page selectors & url parameters when site have enough series for pagination
 | 
			
		||||
    override fun popularMangaNextPageSelector() = null
 | 
			
		||||
 | 
			
		||||
    // latest
 | 
			
		||||
    override fun latestUpdatesRequest(page: Int): Request {
 | 
			
		||||
        return GET(baseUrl, headers)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun latestUpdatesSelector() = "div.chap"
 | 
			
		||||
 | 
			
		||||
    override fun latestUpdatesFromElement(element: Element): SManga {
 | 
			
		||||
        return SManga.create().apply {
 | 
			
		||||
            element.select("div.chap-title a").let { it ->
 | 
			
		||||
                url = it.attr("href").let { "/$it" }
 | 
			
		||||
                title = it.text()
 | 
			
		||||
            }
 | 
			
		||||
            thumbnail_url = element.select("img").attr("data-src").let { "$baseUrl/$it" }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun latestUpdatesNextPageSelector() = popularMangaSelector()
 | 
			
		||||
 | 
			
		||||
    // search
 | 
			
		||||
    override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
 | 
			
		||||
        if (!query.startsWith(SLUG_SEARCH_PREFIX)) {
 | 
			
		||||
            return super.fetchSearchManga(page, query, filters)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val url = "/series/${query.substringAfter(SLUG_SEARCH_PREFIX)}"
 | 
			
		||||
        return fetchMangaDetails(SManga.create().apply { this.url = url }).map {
 | 
			
		||||
            it.url = url
 | 
			
		||||
            MangasPage(listOf(it), false)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
 | 
			
		||||
        return GET("$baseUrl/series?search=$query", headers)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun searchMangaSelector() = popularMangaSelector()
 | 
			
		||||
 | 
			
		||||
    override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
 | 
			
		||||
 | 
			
		||||
    override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
 | 
			
		||||
 | 
			
		||||
    // manga details
 | 
			
		||||
    override fun mangaDetailsParse(document: Document): SManga {
 | 
			
		||||
        return SManga.create().apply {
 | 
			
		||||
            title = document.selectFirst(".title")!!.text()
 | 
			
		||||
            author = document.select(".author > span:nth-child(2)").text()
 | 
			
		||||
            artist = author
 | 
			
		||||
            status = document.select(".status > span:nth-child(2)").text().parseStatus()
 | 
			
		||||
            description = document.select(".summary p").text()
 | 
			
		||||
            thumbnail_url = document.select("div.series-image img").attr("src").let { "$baseUrl/$it" }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun String.parseStatus(): Int {
 | 
			
		||||
        return when {
 | 
			
		||||
            this.contains("ongoing", true) -> SManga.ONGOING
 | 
			
		||||
            this.contains("complete", true) -> SManga.COMPLETED
 | 
			
		||||
            else -> SManga.UNKNOWN
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // chapter list
 | 
			
		||||
    override fun chapterListSelector() = "div.chap"
 | 
			
		||||
 | 
			
		||||
    override fun chapterFromElement(element: Element): SChapter {
 | 
			
		||||
        return SChapter.create().apply {
 | 
			
		||||
            element.select("a").let { a ->
 | 
			
		||||
                url = a.attr("href").let { "/$it" }
 | 
			
		||||
                name = a.text()
 | 
			
		||||
            }
 | 
			
		||||
            date_upload = parseDate(element.select("div > div > span:nth-child(2)").text())
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun parseDate(dateStr: String): Long {
 | 
			
		||||
        return runCatching { DATE_FORMATTER.parse(dateStr)?.time }
 | 
			
		||||
            .getOrNull() ?: 0L
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // page list
 | 
			
		||||
    override fun pageListParse(document: Document): List<Page> {
 | 
			
		||||
        return document.select(".swiper-slide img").mapIndexed { index, img ->
 | 
			
		||||
            Page(
 | 
			
		||||
                index = index,
 | 
			
		||||
                imageUrl = img.attr("src").let { "$baseUrl/$it" },
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun imageUrlParse(document: Document): String {
 | 
			
		||||
        throw UnsupportedOperationException("not used")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        private val DATE_FORMATTER by lazy {
 | 
			
		||||
            SimpleDateFormat("dd MMMM, yy", Locale.ENGLISH)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const val SLUG_SEARCH_PREFIX = "slug:"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,34 @@
 | 
			
		||||
package eu.kanade.tachiyomi.extension.en.apairof2
 | 
			
		||||
 | 
			
		||||
import android.app.Activity
 | 
			
		||||
import android.content.ActivityNotFoundException
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.util.Log
 | 
			
		||||
import kotlin.system.exitProcess
 | 
			
		||||
 | 
			
		||||
class APairOf2UrlActivity : Activity() {
 | 
			
		||||
    override fun onCreate(savedInstanceState: Bundle?) {
 | 
			
		||||
        super.onCreate(savedInstanceState)
 | 
			
		||||
        val pathSegments = intent?.data?.pathSegments
 | 
			
		||||
        if (pathSegments != null && pathSegments.size > 1) {
 | 
			
		||||
            val slug = pathSegments[1]
 | 
			
		||||
            val mainIntent = Intent().apply {
 | 
			
		||||
                action = "eu.kanade.tachiyomi.SEARCH"
 | 
			
		||||
                putExtra("query", "${APairOf2.SLUG_SEARCH_PREFIX}$slug")
 | 
			
		||||
                putExtra("filter", packageName)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            try {
 | 
			
		||||
                startActivity(mainIntent)
 | 
			
		||||
            } catch (e: ActivityNotFoundException) {
 | 
			
		||||
                Log.e("APairOf2UrlActivity", e.toString())
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            Log.e("APairOf2UrlActivity", "could not parse uri from intent $intent")
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        finish()
 | 
			
		||||
        exitProcess(0)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||