Skip to content

New source: Procomic (replaces MangaPro/ProChan) - #18067

Open
bakali1 wants to merge 8 commits into
keiyoushi:mainfrom
bakali1:procomic
Open

New source: Procomic (replaces MangaPro/ProChan)#18067
bakali1 wants to merge 8 commits into
keiyoushi:mainfrom
bakali1:procomic

Conversation

@bakali1

@bakali1 bakali1 commented Jul 29, 2026

Copy link
Copy Markdown

Complete rewrite of the former MangaPro/ProChan extension as a new KeiSource (libVersion = "1.6"). The site and its API changed significantly — the old codebase was no longer salvageable, so this was built from scratch against the current API.

Changes

  • Added src/ar/procomic/ with full implementation:
  • REST API integration (/api/public/content, /api/public/series/search, /api/public/{type}/{id})
  • Search with filters (type, sort, year, status)
  • Popular / Latest updates
  • Manga details and chapter listing
  • Page list via Next.js RSC extraction + AVIF-to-JPEG transcoding via ImageDecoder
  • Mirror support (procomic.pro / procomic.net) via baseUrl { mirrors(...) }
  • Cookie interceptor for language/safety preferences
  • Deeplink handling for /series/... URLs

In relations:

open #14571 ( Source request (AR): ProChan )
Closes #16185 ( remove mangapro / prochan)

Checklist:

  • Updated versionCode value in build.gradle.kts
  • Updated baseVersionCode in build.gradle.kts (if updated multisrc theme code)
  • Referenced all related issues in the PR body (e.g. "Closes #xyz")
  • Set the contentWarning configuration in build.gradle.kts appropriately
  • Have not changed source names
  • 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
  • This PR is AI-assisted, I have reviewed the changes manually and confirmed they are not slop

Comment thread src/ar/procomic/src/eu/kanade/tachiyomi/extension/ar/procomic/Procomic.kt Outdated
Comment thread src/ar/procomic/src/eu/kanade/tachiyomi/extension/ar/procomic/Procomic.kt Outdated
Comment thread src/ar/procomic/src/eu/kanade/tachiyomi/extension/ar/procomic/Procomic.kt Outdated
Comment thread src/ar/procomic/src/eu/kanade/tachiyomi/extension/ar/procomic/Procomic.kt Outdated
return SManga.create().also {
it.url = "/series/$type/$id/$slug"
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check domain too

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review!

I'll add the domain check. Quick question though — I was planning to use this:
kotlin

if (url.host != baseUrl.toHttpUrl().host) {
return null
}

But Prochan has mirrors, so I'm not sure how to handle those. Is it worth adding a list of allowed mirror domains, or should I just stick with the main one?

Honestly, it's probably not a big deal since the main website is what users actually use — the mirrors are just redirects anyway. But I added it as a mirror because the API requests work through it too.

What do you think? Should I keep it simple with just the main domain, or build out a mirror list?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's fine, or if the mirror isn't really used, you can remove the mirror from extension entirely

Comment thread src/ar/procomic/src/eu/kanade/tachiyomi/extension/ar/procomic/Procomic.kt Outdated
Comment thread src/ar/procomic/src/eu/kanade/tachiyomi/extension/ar/procomic/Procomic.kt Outdated
Comment thread src/ar/procomic/src/eu/kanade/tachiyomi/extension/ar/procomic/Procomic.kt Outdated
Comment thread src/ar/procomic/src/eu/kanade/tachiyomi/extension/ar/procomic/Procomic.kt Outdated
bakali1 added 6 commits July 29, 2026 18:32
- Use client.get()/parseAs helpers instead of GET/await
- Remove local Json instance, use shared parseAs
- Replace SimpleDateFormat/tryParse with Instant.parseOrNull
- Use firstInstanceOrNull instead of filterIsInstance
- Remove broad try-catch in searchApi
- Convert data class DTOs to regular class
- Replace Calendar with Year.now() in Filters
- Capitalize extension name in build.gradle.kts
- Remove unused import
- Use baseUrl mirrors DSL with versionId
- Fix ambiguous it-shadowing in genre builder
Add network interceptor that decodes image/avif responses via
tachiyomi.decoder.ImageDecoder and re-encodes as JPEG, preventing
BitmapRegionDecoder crash during tall image splitting on pre-API30.
- Drop `supportsLatest = true` (default in KeiSource)
- Remove AVIF-to-JPEG interceptor and ImageDecoder dependency —
  app handles AVIF natively
- Remove empty `configureHeaders` override
- Remove `CacheControl.FORCE_NETWORK` from search requests
- Add host check in `getMangaByUrl` to reject cross-domain URLs
- Store type/slug/seriesId/chapterNumber in `memo` instead of
  encoding them in URL paths; use `/$id` as the canonical URL
- Add `getChapterUrl()` to reconstruct full chapter URLs from memo
- Return both manga details and chapters unconditionally in
  `getMangaDetails`
@bakali1

bakali1 commented Jul 30, 2026

Copy link
Copy Markdown
Author

I used the extention for some time reading some Mangas I discovered some bugs I will try to fix
changes to be made :

  • chapters missing images
  • the manga url should lead to the manga when clicking open in web view

dear reviewer am sorry for wasting your time dealing with this source

I will try my best to fix the issue

return SManga.create().also {
it.url = "/series/$type/$id/$slug"
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's fine, or if the mirror isn't really used, you can remove the mirror from extension entirely

val id = segs[off + 2]
val slug = segs[off + 3]
return SManga.create().also {
it.url = "/$id"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need to prefix with "/"

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’ll remove the prefix as well, and I won’t change the manga URL structure (/series/$type/$id/$slug) since that’s how it’s set on the website. That way, when you open it in the WebView, you can see your manga.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

override getMangaUrl and getChapterUrl for webview urls

override suspend fun getPageList(chapter: SChapter): List<Page> {
val response = client.get(getChapterUrl(chapter), headersBuilder().set("rsc", "1").build())
return response.extractNextJs<ChapterImages>()?.appImages?.mapIndexed { i, img ->
Page(i, imageUrl = img.mobile ?: img.desktop ?: "")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Page(i, imageUrl = img.mobile ?: img.desktop ?: "")
Page(i, imageUrl = img.mobile ?: img.desktop!!)

shouldn't be empty

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks, i will apply it the change

}
override fun getChapterUrl(chapter: SChapter): String {
val m = chapter.memo
return "$baseUrl/series/${m["type"]!!.jsonPrimitive.content}/${m["seriesId"]!!.jsonPrimitive.content}/${m["slug"]!!.jsonPrimitive.content}/${chapter.url.removePrefix("/")}/${m["chapterNumber"]!!.jsonPrimitive.content}"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return "$baseUrl/series/${m["type"]!!.jsonPrimitive.content}/${m["seriesId"]!!.jsonPrimitive.content}/${m["slug"]!!.jsonPrimitive.content}/${chapter.url.removePrefix("/")}/${m["chapterNumber"]!!.jsonPrimitive.content}"
return "$baseUrl/series/${m["type"]!!.string}/${m["seriesId"]!!.string}/${m["slug"]!!.string}/${chapter.url.removePrefix("/")}/${m["chapterNumber"]!!.string}"

use JsonElement helper utils

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank u for the review , i didn't know that exist i will use it instead of the current method

@bakali1

bakali1 commented Aug 1, 2026

Copy link
Copy Markdown
Author

Correction to my earlier reply where I said the site no longer uses scrambled pages.

That was incorrect. Some chapters are still served as scrambled pages, which is what caused the "chapters missing images" bug I reported. This has been fixed in the latest commits.

I've verified the fix on a real device, and the affected chapters now load every page correctly.

I also discovered that some series are hidden behind the site's "Safe Browsing" setting. Guests can't disable this setting, so getPageList now throws an error for those series instead of returning an empty page list, making the issue much easier to identify.

That said, the site seems to change every couple of weeks. I suspect they're preparing or promoting their official app and are intentionally making the website harder to scrape. At this point, I'm considering marking this source as a draft because it may not be worth constantly chasing their frequent changes and debugging their ever-changing anti-scraping measures.

and thank you AwkwardPeak7 so much for giving me advises and being friendly and patient with me
i have learned a lot of thing building this extension after it i will try doing some more of the issues

)
}

val rscHeaders by lazy { headersBuilder().add("rsc", "1").build() }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
val rscHeaders by lazy { headersBuilder().add("rsc", "1").build() }
val rscHeaders get() = headersBuilder().add("rsc", "1").build()

Comment on lines +279 to +280
val payload = buildJsonObject { put("url", cdnUrl) }.toJsonString()
.toRequestBody("application/json".toMediaType())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
val payload = buildJsonObject { put("url", cdnUrl) }.toJsonString()
.toRequestBody("application/json".toMediaType())
val payload = buildJsonObject { put("url", cdnUrl) }.toJsonRequestBody()

Comment on lines +286 to +289
val response = client.post("$baseUrl/api/cdn-image/sign", rscheaders, payload)

if (!response.isSuccessful) {
response.close()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

client.post ensures http success

@AwkwardPeak7

Copy link
Copy Markdown
Contributor

promoting their official app

perhaps you can sniff the api of the app and use it ;)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants