Manga cover updates (#3101)
* cover caching overhaul * add ui for removing custom cover * skip some loading work * minor cleanup * allow refresh library metadata to refresh local manga * rename metadata_date to cover_last_modified * rearrange removeMangaFromLibrary * change custom cover directory add setting for updating cover when refreshing library * remove toggle and explicit action for updating covers
This commit is contained in:
@@ -12,7 +12,7 @@ import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import timber.log.Timber
|
||||
|
||||
open class FileFetcher(private val file: File) : DataFetcher<InputStream> {
|
||||
open class FileFetcher(private val filePath: String = "") : DataFetcher<InputStream> {
|
||||
|
||||
private var data: InputStream? = null
|
||||
|
||||
@@ -20,7 +20,11 @@ open class FileFetcher(private val file: File) : DataFetcher<InputStream> {
|
||||
loadFromFile(callback)
|
||||
}
|
||||
|
||||
protected fun loadFromFile(callback: DataFetcher.DataCallback<in InputStream>) {
|
||||
private fun loadFromFile(callback: DataFetcher.DataCallback<in InputStream>) {
|
||||
loadFromFile(File(filePath), callback)
|
||||
}
|
||||
|
||||
protected fun loadFromFile(file: File, callback: DataFetcher.DataCallback<in InputStream>) {
|
||||
try {
|
||||
data = FileInputStream(file)
|
||||
} catch (e: FileNotFoundException) {
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package eu.kanade.tachiyomi.data.glide
|
||||
|
||||
import com.bumptech.glide.Priority
|
||||
import com.bumptech.glide.load.data.DataFetcher
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.lang.Exception
|
||||
|
||||
open class LibraryMangaCustomCoverFetcher(
|
||||
private val manga: Manga,
|
||||
private val coverCache: CoverCache
|
||||
) : FileFetcher() {
|
||||
|
||||
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
|
||||
getCustomCoverFile()?.let {
|
||||
loadFromFile(it, callback)
|
||||
} ?: callback.onLoadFailed(Exception("Custom cover file not found"))
|
||||
}
|
||||
|
||||
protected fun getCustomCoverFile(): File? {
|
||||
return coverCache.getCustomCoverFile(manga).takeIf { it.exists() }
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.data.glide
|
||||
|
||||
import com.bumptech.glide.Priority
|
||||
import com.bumptech.glide.load.data.DataFetcher
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
@@ -19,31 +20,41 @@ import java.io.InputStream
|
||||
class LibraryMangaUrlFetcher(
|
||||
private val networkFetcher: DataFetcher<InputStream>,
|
||||
private val manga: Manga,
|
||||
private val file: File
|
||||
) :
|
||||
FileFetcher(file) {
|
||||
private val coverCache: CoverCache
|
||||
) : LibraryMangaCustomCoverFetcher(manga, coverCache) {
|
||||
|
||||
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
|
||||
if (!file.exists()) {
|
||||
getCustomCoverFile()?.let {
|
||||
loadFromFile(it, callback)
|
||||
return
|
||||
}
|
||||
|
||||
val cover = coverCache.getCoverFile(manga)
|
||||
if (cover == null) {
|
||||
callback.onLoadFailed(Exception("Null thumbnail url"))
|
||||
return
|
||||
}
|
||||
|
||||
if (!cover.exists()) {
|
||||
networkFetcher.loadData(
|
||||
priority,
|
||||
object : DataFetcher.DataCallback<InputStream> {
|
||||
override fun onDataReady(data: InputStream?) {
|
||||
if (data != null) {
|
||||
val tmpFile = File(file.path + ".tmp")
|
||||
val tmpFile = File(cover.path + ".tmp")
|
||||
try {
|
||||
// Retrieve destination stream, create parent folders if needed.
|
||||
val output = try {
|
||||
tmpFile.outputStream()
|
||||
} catch (e: FileNotFoundException) {
|
||||
tmpFile.parentFile.mkdirs()
|
||||
tmpFile.parentFile!!.mkdirs()
|
||||
tmpFile.outputStream()
|
||||
}
|
||||
|
||||
// Copy the file and rename to the original.
|
||||
data.use { output.use { data.copyTo(output) } }
|
||||
tmpFile.renameTo(file)
|
||||
loadFromFile(callback)
|
||||
tmpFile.renameTo(cover)
|
||||
loadFromFile(cover, callback)
|
||||
} catch (e: Exception) {
|
||||
tmpFile.delete()
|
||||
callback.onLoadFailed(e)
|
||||
@@ -59,7 +70,7 @@ class LibraryMangaUrlFetcher(
|
||||
}
|
||||
)
|
||||
} else {
|
||||
loadFromFile(callback)
|
||||
loadFromFile(cover, callback)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.glide
|
||||
|
||||
import com.bumptech.glide.load.Key
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import java.io.File
|
||||
import java.security.MessageDigest
|
||||
|
||||
class MangaSignature(manga: Manga, file: File) : Key {
|
||||
|
||||
private val key = manga.thumbnail_url + file.lastModified()
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return if (other is MangaSignature) {
|
||||
key == other.key
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return key.hashCode()
|
||||
}
|
||||
|
||||
override fun updateDiskCacheKey(md: MessageDigest) {
|
||||
md.update(key.toByteArray(Key.CHARSET))
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,15 @@
|
||||
package eu.kanade.tachiyomi.data.glide
|
||||
|
||||
import com.bumptech.glide.load.Key
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import java.security.MessageDigest
|
||||
|
||||
data class MangaThumbnail(val manga: Manga, val url: String?)
|
||||
data class MangaThumbnail(val manga: Manga, val coverLastModified: Long) : Key {
|
||||
val key = manga.url + coverLastModified
|
||||
|
||||
fun Manga.toMangaThumbnail() = MangaThumbnail(this, this.thumbnail_url)
|
||||
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
|
||||
messageDigest.update(key.toByteArray(Key.CHARSET))
|
||||
}
|
||||
}
|
||||
|
||||
fun Manga.toMangaThumbnail() = MangaThumbnail(this, cover_last_modified)
|
||||
|
||||
@@ -14,7 +14,7 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import java.io.File
|
||||
import eu.kanade.tachiyomi.util.isLocal
|
||||
import java.io.InputStream
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
@@ -48,12 +48,6 @@ class MangaThumbnailModelLoader : ModelLoader<MangaThumbnail, InputStream> {
|
||||
*/
|
||||
private val defaultClient = Injekt.get<NetworkHelper>().client
|
||||
|
||||
/**
|
||||
* LRU cache whose key is the thumbnail url of the manga, and the value contains the request url
|
||||
* and the file where it should be stored in case the manga is a favorite.
|
||||
*/
|
||||
private val lruCache = LruCache<GlideUrl, File>(100)
|
||||
|
||||
/**
|
||||
* Map where request headers are stored for a source.
|
||||
*/
|
||||
@@ -78,7 +72,7 @@ class MangaThumbnailModelLoader : ModelLoader<MangaThumbnail, InputStream> {
|
||||
/**
|
||||
* Returns a fetcher for the given manga or null if the url is empty.
|
||||
*
|
||||
* @param manga the model.
|
||||
* @param mangaThumbnail the model.
|
||||
* @param width the width of the view where the resource will be loaded.
|
||||
* @param height the height of the view where the resource will be loaded.
|
||||
*/
|
||||
@@ -88,13 +82,16 @@ class MangaThumbnailModelLoader : ModelLoader<MangaThumbnail, InputStream> {
|
||||
height: Int,
|
||||
options: Options
|
||||
): ModelLoader.LoadData<InputStream>? {
|
||||
// Check thumbnail is not null or empty
|
||||
val url = mangaThumbnail.url
|
||||
if (url == null || url.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
|
||||
val manga = mangaThumbnail.manga
|
||||
val url = manga.thumbnail_url
|
||||
|
||||
if (url.isNullOrEmpty()) {
|
||||
return if (!manga.favorite || manga.isLocal()) {
|
||||
null
|
||||
} else {
|
||||
ModelLoader.LoadData(mangaThumbnail, LibraryMangaCustomCoverFetcher(manga, coverCache))
|
||||
}
|
||||
}
|
||||
|
||||
if (url.startsWith("http", true)) {
|
||||
val source = sourceManager.get(manga.source) as? HttpSource
|
||||
@@ -107,19 +104,13 @@ class MangaThumbnailModelLoader : ModelLoader<MangaThumbnail, InputStream> {
|
||||
return ModelLoader.LoadData(glideUrl, networkFetcher)
|
||||
}
|
||||
|
||||
// Obtain the file for this url from the LRU cache, or retrieve and add it to the cache.
|
||||
val file = lruCache.getOrPut(glideUrl) { coverCache.getCoverFile(url) }
|
||||
|
||||
val libraryFetcher = LibraryMangaUrlFetcher(networkFetcher, manga, file)
|
||||
val libraryFetcher = LibraryMangaUrlFetcher(networkFetcher, manga, coverCache)
|
||||
|
||||
// Return an instance of the fetcher providing the needed elements.
|
||||
return ModelLoader.LoadData(MangaSignature(manga, file), libraryFetcher)
|
||||
return ModelLoader.LoadData(mangaThumbnail, libraryFetcher)
|
||||
} else {
|
||||
// Get the file from the url, removing the scheme if present.
|
||||
val file = File(url.substringAfter("file://"))
|
||||
|
||||
// Return an instance of the fetcher providing the needed elements.
|
||||
return ModelLoader.LoadData(MangaSignature(manga, file), FileFetcher(file))
|
||||
return ModelLoader.LoadData(mangaThumbnail, FileFetcher(url.removePrefix("file://")))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user