Support opds (#133)
* Add OPDS catalog support for book discovery and downloading * Implement OPDS search, improved book discovery, and enhanced UI details. * Improve OPDS search and catalog management * Add authentication support and extended metadata to the OPDS reader. * Add support for OPDS 2.0 (JSON) feeds * Enhance OPDS book downloading with progress tracking and multi-format support. * Enhance OPDS book management and UI * Add support for OPDS-PSE (Page Streaming Extension) streaming * Improve OPDS streaming and catalog management
This commit is contained in:
parent
355664fbcc
commit
15264a31ae
12 changed files with 2208 additions and 33 deletions
|
|
@ -3089,14 +3089,17 @@ fun PdfViewerScreen(
|
|||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
Timber.d("Opening ParcelFileDescriptor for URI: $pdfUri")
|
||||
currentPfdOpened = context.contentResolver.openFileDescriptor(pdfUri, "r")
|
||||
if (currentPfdOpened == null) throw Exception("Failed to open ParcelFileDescriptor")
|
||||
|
||||
if (pdfUri.scheme != "opds-pse") {
|
||||
currentPfdOpened = context.contentResolver.openFileDescriptor(pdfUri, "r")
|
||||
if (currentPfdOpened == null) throw Exception("Failed to open ParcelFileDescriptor")
|
||||
}
|
||||
|
||||
val doc = DocumentFactory.loadDocument(context, pdfUri, uiState.selectedFileType ?: FileType.PDF, documentPassword, pdfiumCore)
|
||||
|
||||
if (!isActive) {
|
||||
doc.close()
|
||||
currentPfdOpened.close()
|
||||
currentPfdOpened?.close()
|
||||
return@withContext
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,9 +24,11 @@ import java.io.File
|
|||
import me.zhanghai.android.libarchive.Archive
|
||||
import me.zhanghai.android.libarchive.ArchiveEntry
|
||||
import me.zhanghai.android.libarchive.ArchiveException
|
||||
import okhttp3.Request
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
import java.util.zip.ZipFile
|
||||
import androidx.core.graphics.createBitmap
|
||||
|
||||
interface ReaderDocument : AutoCloseable {
|
||||
suspend fun getPageCount(): Int
|
||||
|
|
@ -68,6 +70,13 @@ interface ReaderWebLinks : AutoCloseable {
|
|||
|
||||
object DocumentFactory {
|
||||
suspend fun loadDocument(context: Context, uri: Uri, type: FileType, password: String?, pdfiumCore: PdfiumCoreKt): ReaderDocument {
|
||||
if (uri.scheme == "opds-pse") {
|
||||
val bookId = uri.getQueryParameter("id") ?: UUID.randomUUID().toString()
|
||||
val urlTemplate = uri.getQueryParameter("url") ?: ""
|
||||
val count = uri.getQueryParameter("count")?.toIntOrNull() ?: 0
|
||||
val catalogId = uri.getQueryParameter("catalogId")
|
||||
return OpdsStreamDocumentWrapper(context, bookId, urlTemplate, count, catalogId)
|
||||
}
|
||||
return if (type == FileType.CBZ || type == FileType.CBR || type == FileType.CB7) {
|
||||
val cacheFile = File(context.cacheDir, "temp_comic_${System.currentTimeMillis()}.${type.name.lowercase()}")
|
||||
withContext(Dispatchers.IO) {
|
||||
|
|
@ -117,13 +126,36 @@ class PdfPageWrapper(val pdfPage: PdfPageKt) : ReaderPage {
|
|||
}
|
||||
|
||||
override fun getNativePointer(): Long {
|
||||
return try {
|
||||
val field = pdfPage.javaClass.getDeclaredField("mNativePagePtr")
|
||||
field.isAccessible = true
|
||||
field.get(pdfPage) as? Long ?: 0L
|
||||
} catch (_: Exception) {
|
||||
0L
|
||||
return extractNativePointer(pdfPage)
|
||||
}
|
||||
|
||||
private fun extractNativePointer(obj: Any): Long {
|
||||
val priorityFields = listOf("page", "mNativePagePtr", "pagePtr", "mNativePage")
|
||||
|
||||
for (name in priorityFields) {
|
||||
try {
|
||||
val field = obj.javaClass.getDeclaredField(name)
|
||||
field.isAccessible = true
|
||||
val value = field.get(obj)
|
||||
if (value is Long && value != 0L) return value
|
||||
if (value != null && value !is Long) {
|
||||
val nestedPtr = extractNativePointer(value)
|
||||
if (nestedPtr != 0L) return nestedPtr
|
||||
}
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
|
||||
try {
|
||||
for (field in obj.javaClass.declaredFields) {
|
||||
if (field.type == Long::class.java || field.type == Long::class.javaPrimitiveType) {
|
||||
field.isAccessible = true
|
||||
val value = field.get(obj) as Long
|
||||
if (value > 0xFFFFFFFFL) return value
|
||||
}
|
||||
}
|
||||
} catch (_: Exception) {}
|
||||
|
||||
return 0L
|
||||
}
|
||||
|
||||
override fun close() { pdfPage.close() }
|
||||
|
|
@ -388,4 +420,93 @@ class ArchivePageWrapper(imageBytes: ByteArray) : ReaderPage {
|
|||
override fun close() {
|
||||
decoder?.recycle()
|
||||
}
|
||||
}
|
||||
|
||||
class OpdsStreamDocumentWrapper(
|
||||
private val context: Context,
|
||||
private val bookId: String,
|
||||
private val urlTemplate: String,
|
||||
private val pageCount: Int,
|
||||
private val catalogId: String?
|
||||
) : ReaderDocument {
|
||||
private val cacheDir = File(context.cacheDir, "opds_stream_${bookId.hashCode()}").apply { mkdirs() }
|
||||
|
||||
private val catalog = catalogId?.let {
|
||||
com.aryan.reader.opds.OpdsRepository(context).getCatalogs().find { c -> c.id == it }
|
||||
}
|
||||
|
||||
private val client = com.aryan.reader.opds.OpdsRepository.sharedHttpClient.newBuilder()
|
||||
.apply {
|
||||
if (!catalog?.username.isNullOrBlank() && !catalog.password.isNullOrBlank()) {
|
||||
authenticator(com.aryan.reader.opds.OpdsRepository.OpdsAuthenticator(catalog.username, catalog.password))
|
||||
}
|
||||
}
|
||||
.build()
|
||||
|
||||
private fun createErrorPageBytes(): ByteArray {
|
||||
val bitmap = createBitmap(800, 1200)
|
||||
val canvas = Canvas(bitmap)
|
||||
canvas.drawColor(android.graphics.Color.DKGRAY)
|
||||
val paint = Paint().apply {
|
||||
color = android.graphics.Color.WHITE
|
||||
textSize = 40f
|
||||
textAlign = Paint.Align.CENTER
|
||||
}
|
||||
canvas.drawText("Page Unavailable", 400f, 600f, paint)
|
||||
val stream = java.io.ByteArrayOutputStream()
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, stream)
|
||||
return stream.toByteArray()
|
||||
}
|
||||
|
||||
override suspend fun getPageCount() = pageCount
|
||||
|
||||
override suspend fun openPage(pageIndex: Int): ReaderPage? = withContext(Dispatchers.IO) {
|
||||
if (pageIndex !in 0 until pageCount) return@withContext null
|
||||
|
||||
val cachedFile = File(cacheDir, "page_$pageIndex.jpg")
|
||||
if (cachedFile.exists() && cachedFile.length() > 0) {
|
||||
try {
|
||||
return@withContext ArchivePageWrapper(cachedFile.readBytes())
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Failed to read cached stream page")
|
||||
}
|
||||
}
|
||||
|
||||
val finalUrlTemplate = if (catalog != null && urlTemplate.startsWith("http")) {
|
||||
try {
|
||||
val oldUrl = java.net.URL(urlTemplate)
|
||||
val newUrl = java.net.URL(catalog.url)
|
||||
val oldBase = "${oldUrl.protocol}://${oldUrl.authority}"
|
||||
val newBase = "${newUrl.protocol}://${newUrl.authority}"
|
||||
urlTemplate.replace(oldBase, newBase)
|
||||
} catch (_: Exception) {
|
||||
urlTemplate
|
||||
}
|
||||
} else urlTemplate
|
||||
|
||||
val url = finalUrlTemplate.replace("{pageNumber}", pageIndex.toString())
|
||||
.replace("{maxWidth}", "1600")
|
||||
|
||||
val request = Request.Builder().url(url).build()
|
||||
try {
|
||||
val response = client.newCall(request).execute()
|
||||
if (response.isSuccessful) {
|
||||
val bytes = response.body?.bytes()
|
||||
if (bytes != null && bytes.isNotEmpty()) {
|
||||
cachedFile.writeBytes(bytes)
|
||||
return@withContext ArchivePageWrapper(bytes)
|
||||
}
|
||||
} else {
|
||||
Timber.e("Stream page failed with HTTP ${response.code}")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Failed to fetch stream page $pageIndex")
|
||||
}
|
||||
|
||||
return@withContext ArchivePageWrapper(createErrorPageBytes())
|
||||
}
|
||||
|
||||
override suspend fun getTableOfContents() = emptyList<Bookmark>()
|
||||
|
||||
override fun close() {}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue