瀏覽代碼

Rar/cbr support

len 8 年之前
父節點
當前提交
c8e3375248

+ 1 - 0
app/build.gradle

@@ -99,6 +99,7 @@ dependencies {
 
     // Modified dependencies
     compile 'com.github.inorichi:subsampling-scale-image-view:44aa442'
+    compile 'com.github.inorichi:junrar-android:634c1f5'
 
     // Android support library
     final support_library_version = '25.1.1'

+ 5 - 0
app/src/main/AndroidManifest.xml

@@ -81,6 +81,11 @@
             android:authorities="${applicationId}.zip-provider"
             android:exported="false" />
 
+        <provider
+            android:name="eu.kanade.tachiyomi.util.RarContentProvider"
+            android:authorities="${applicationId}.rar-provider"
+            android:exported="false" />
+
         <receiver
             android:name=".data.notification.NotificationReceiver"
             android:exported="false" />

+ 23 - 3
app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt

@@ -6,7 +6,10 @@ import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.source.model.*
 import eu.kanade.tachiyomi.util.ChapterRecognition
 import eu.kanade.tachiyomi.util.DiskUtil
+import eu.kanade.tachiyomi.util.RarContentProvider
 import eu.kanade.tachiyomi.util.ZipContentProvider
+import junrar.Archive
+import junrar.rarfile.FileHeader
 import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator
 import org.jsoup.Jsoup
 import org.jsoup.nodes.Document
@@ -169,7 +172,9 @@ class LocalSource(private val context: Context) : CatalogueSource {
     }
 
     private fun isSupportedFormat(extension: String): Boolean {
-        return extension.equals("zip", true) || extension.equals("cbz", true) || extension.equals("epub", true)
+        return extension.equals("zip", true) || extension.equals("cbz", true)
+                || extension.equals("rar", true) || extension.equals("cbr", true)
+                || extension.equals("epub", true)
     }
 
     private fun getLoader(file: File): Loader {
@@ -180,6 +185,8 @@ class LocalSource(private val context: Context) : CatalogueSource {
             ZipLoader(file)
         } else if (extension.equals("epub", true)) {
             EpubLoader(file)
+        } else if (extension.equals("rar", true) || extension.equals("cbr", true)) {
+            RarLoader(file)
         } else {
             throw Exception("Invalid chapter format")
         }
@@ -207,8 +214,8 @@ class LocalSource(private val context: Context) : CatalogueSource {
     class ZipLoader(val file: File) : Loader {
         override fun load(): List<Page> {
             val comparator = CaseInsensitiveSimpleNaturalComparator.getInstance<String>()
-            ZipFile(file).use { zip ->
-                return zip.entries().toList()
+            return ZipFile(file).use { zip ->
+                zip.entries().toList()
                         .filter { !it.isDirectory && DiskUtil.isImage(it.name, { zip.getInputStream(it) }) }
                         .sortedWith(Comparator<ZipEntry> { f1, f2 -> comparator.compare(f1.name, f2.name) })
                         .map { Uri.parse("content://${ZipContentProvider.PROVIDER}${file.absolutePath}!/${it.name}") }
@@ -217,6 +224,19 @@ class LocalSource(private val context: Context) : CatalogueSource {
         }
     }
 
+    class RarLoader(val file: File) : Loader {
+        override fun load(): List<Page> {
+            val comparator = CaseInsensitiveSimpleNaturalComparator.getInstance<String>()
+            return Archive(file).use { archive ->
+                archive.fileHeaders
+                        .filter { !it.isDirectory && DiskUtil.isImage(it.fileNameString, { archive.getInputStream(it) }) }
+                        .sortedWith(Comparator<FileHeader> { f1, f2 -> comparator.compare(f1.fileNameString, f2.fileNameString) })
+                        .map { Uri.parse("content://${RarContentProvider.PROVIDER}${file.absolutePath}!-/${it.fileNameString}") }
+                        .mapIndexed { i, uri -> Page(i, uri = uri).apply { status = Page.READY } }
+            }
+        }
+    }
+
     class EpubLoader(val file: File) : Loader {
 
         override fun load(): List<Page> {

+ 73 - 0
app/src/main/java/eu/kanade/tachiyomi/util/RarContentProvider.kt

@@ -0,0 +1,73 @@
+package eu.kanade.tachiyomi.util
+
+import android.content.ContentProvider
+import android.content.ContentValues
+import android.content.res.AssetFileDescriptor
+import android.database.Cursor
+import android.net.Uri
+import android.os.ParcelFileDescriptor
+import eu.kanade.tachiyomi.BuildConfig
+import junrar.Archive
+import java.io.File
+import java.io.IOException
+import java.net.URLConnection
+import java.util.concurrent.Executors
+
+class RarContentProvider : ContentProvider() {
+
+    private val pool by lazy { Executors.newCachedThreadPool() }
+
+    companion object {
+        const val PROVIDER = "${BuildConfig.APPLICATION_ID}.rar-provider"
+    }
+
+    override fun onCreate(): Boolean {
+        return true
+    }
+
+    override fun getType(uri: Uri): String? {
+        return URLConnection.guessContentTypeFromName(uri.toString())
+    }
+
+    override fun openAssetFile(uri: Uri, mode: String): AssetFileDescriptor? {
+        try {
+            val pipe = ParcelFileDescriptor.createPipe()
+            pool.execute {
+                try {
+                    val (rar, file) = uri.toString()
+                            .substringAfter("content://$PROVIDER")
+                            .split("!-/", limit = 2)
+
+                    Archive(File(rar)).use { archive ->
+                        val fileHeader = archive.fileHeaders.first { it.fileNameString == file }
+
+                        ParcelFileDescriptor.AutoCloseOutputStream(pipe[1]).use { output ->
+                            archive.extractFile(fileHeader, output)
+                        }
+                    }
+                } catch (e: Exception) {
+                    // Ignore
+                }
+            }
+            return AssetFileDescriptor(pipe[0], 0, -1)
+        } catch (e: IOException) {
+            return null
+        }
+    }
+
+    override fun query(p0: Uri?, p1: Array<out String>?, p2: String?, p3: Array<out String>?, p4: String?): Cursor? {
+        return null
+    }
+
+    override fun insert(p0: Uri?, p1: ContentValues?): Uri {
+        throw UnsupportedOperationException("not implemented")
+    }
+
+    override fun update(p0: Uri?, p1: ContentValues?, p2: String?, p3: Array<out String>?): Int {
+        throw UnsupportedOperationException("not implemented")
+    }
+
+    override fun delete(p0: Uri?, p1: String?, p2: Array<out String>?): Int {
+        throw UnsupportedOperationException("not implemented")
+    }
+}