mirror of
https://github.com/mihonapp/mihon.git
synced 2025-11-14 04:58:56 +01:00
Local manga in zip/cbz/folder format (#648)
* add local source * small fixes * change Chapter to SChapter and and Manga to SManga in ChapterRecognition. Use ChapterRecognition.parseChapterNumber() to recognize chapter numbers. * use thread poll * update isImage() * add isImage() function to DiskUtil * improve cover handling * Support external SD cards * use R.string.app_name as root folder name
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
package eu.kanade.tachiyomi.util
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
|
||||
/**
|
||||
* -R> = regex conversion.
|
||||
@@ -37,7 +37,7 @@ object ChapterRecognition {
|
||||
*/
|
||||
private val unwantedWhiteSpace = Regex("""(\s)(extra|special|omake)""")
|
||||
|
||||
fun parseChapterNumber(chapter: Chapter, manga: Manga) {
|
||||
fun parseChapterNumber(chapter: SChapter, manga: SManga) {
|
||||
// If chapter number is known return.
|
||||
if (chapter.chapter_number == -2f || chapter.chapter_number > -1f)
|
||||
return
|
||||
@@ -91,7 +91,7 @@ object ChapterRecognition {
|
||||
* @param chapter chapter object
|
||||
* @return true if volume is found
|
||||
*/
|
||||
fun updateChapter(match: MatchResult?, chapter: Chapter): Boolean {
|
||||
fun updateChapter(match: MatchResult?, chapter: SChapter): Boolean {
|
||||
match?.let {
|
||||
val initial = it.groups[1]?.value?.toFloat()!!
|
||||
val subChapterDecimal = it.groups[2]?.value
|
||||
|
||||
@@ -1,11 +1,53 @@
|
||||
package eu.kanade.tachiyomi.util
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Environment
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.support.v4.os.EnvironmentCompat
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.net.URLConnection
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
|
||||
object DiskUtil {
|
||||
|
||||
fun isImage(name: String, openStream: (() -> InputStream)? = null): Boolean {
|
||||
val contentType = URLConnection.guessContentTypeFromName(name)
|
||||
if (contentType != null)
|
||||
return contentType.startsWith("image/")
|
||||
|
||||
if (openStream != null) try {
|
||||
openStream.invoke().buffered().use {
|
||||
var bytes = ByteArray(11)
|
||||
it.mark(bytes.size)
|
||||
var length = it.read(bytes, 0, bytes.size)
|
||||
it.reset()
|
||||
if (length == -1)
|
||||
return false
|
||||
if (bytes[0] == 'G'.toByte() && bytes[1] == 'I'.toByte() && bytes[2] == 'F'.toByte() && bytes[3] == '8'.toByte()) {
|
||||
return true // image/gif
|
||||
} else if (bytes[0] == 0x89.toByte() && bytes[1] == 0x50.toByte() && bytes[2] == 0x4E.toByte()
|
||||
&& bytes[3] == 0x47.toByte() && bytes[4] == 0x0D.toByte() && bytes[5] == 0x0A.toByte()
|
||||
&& bytes[6] == 0x1A.toByte() && bytes[7] == 0x0A.toByte()) {
|
||||
return true // image/png
|
||||
} else if (bytes[0] == 0xFF.toByte() && bytes[1] == 0xD8.toByte() && bytes[2] == 0xFF.toByte()) {
|
||||
if (bytes[3] == 0xE0.toByte() || bytes[3] == 0xE1.toByte() && bytes[6] == 'E'.toByte()
|
||||
&& bytes[7] == 'x'.toByte() && bytes[8] == 'i'.toByte()
|
||||
&& bytes[9] == 'f'.toByte() && bytes[10] == 0.toByte()) {
|
||||
return true // image/jpeg
|
||||
} else if (bytes[3] == 0xEE.toByte()) {
|
||||
return true // image/jpg
|
||||
}
|
||||
} else if (bytes[0] == 'W'.toByte() && bytes[1] == 'E'.toByte() && bytes[2] == 'B'.toByte() && bytes[3] == 'P'.toByte()) {
|
||||
return true // image/webp
|
||||
}
|
||||
}
|
||||
} catch(e: Exception) {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun hashKeyForDisk(key: String): String {
|
||||
return try {
|
||||
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
|
||||
@@ -31,9 +73,26 @@ object DiskUtil {
|
||||
return size
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the root folders of all the available external storages.
|
||||
*/
|
||||
fun getExternalStorages(context: Context): List<File> {
|
||||
return ContextCompat.getExternalFilesDirs(context, null)
|
||||
.filterNotNull()
|
||||
.mapNotNull {
|
||||
val file = File(it.absolutePath.substringBefore("/Android/"))
|
||||
val state = EnvironmentCompat.getStorageState(file)
|
||||
if (state == Environment.MEDIA_MOUNTED || state == Environment.MEDIA_MOUNTED_READ_ONLY) {
|
||||
file
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutate the given filename to make it valid for a FAT filesystem,
|
||||
* replacing any invalid characters with "_". This method doesn't allow private files (starting
|
||||
* replacing any invalid characters with "_". This method doesn't allow hidden files (starting
|
||||
* with a dot), but you can manually add it later.
|
||||
*/
|
||||
fun buildValidFilename(origName: String): String {
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
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 timber.log.Timber
|
||||
import java.io.IOException
|
||||
import java.net.URL
|
||||
import java.net.URLConnection
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
class ZipContentProvider : ContentProvider() {
|
||||
|
||||
private val pool by lazy { Executors.newCachedThreadPool() }
|
||||
|
||||
companion object {
|
||||
const val PROVIDER = "${BuildConfig.APPLICATION_ID}.zip-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 url = "jar:file://" + uri.toString().substringAfter("content://$PROVIDER")
|
||||
val input = URL(url).openStream()
|
||||
val pipe = ParcelFileDescriptor.createPipe()
|
||||
pool.execute {
|
||||
try {
|
||||
val output = ParcelFileDescriptor.AutoCloseOutputStream(pipe[1])
|
||||
input.use {
|
||||
output.use {
|
||||
input.copyTo(output)
|
||||
output.flush()
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user