[优化] 远程书籍同步逻辑

This commit is contained in:
HapeLee
2026-03-10 00:12:26 +08:00
parent 4c6f5f4348
commit f8b24b9588
4 changed files with 101 additions and 81 deletions
@@ -1,19 +1,90 @@
package io.legado.app.data.repository package io.legado.app.data.repository
import io.legado.app.constant.AppConst
import io.legado.app.constant.BookType import io.legado.app.constant.BookType
import io.legado.app.data.AppDatabase import io.legado.app.data.AppDatabase
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.exception.NoStackTraceException
import io.legado.app.help.AppWebDav
import io.legado.app.help.book.getRemoteUrl
import io.legado.app.help.book.isLocal
import io.legado.app.help.config.AppConfig
import io.legado.app.lib.webdav.Authorization import io.legado.app.lib.webdav.Authorization
import io.legado.app.model.analyzeRule.AnalyzeUrl
import io.legado.app.model.analyzeRule.CustomUrl import io.legado.app.model.analyzeRule.CustomUrl
import io.legado.app.model.localBook.LocalBook import io.legado.app.model.localBook.LocalBook
import io.legado.app.model.remote.RemoteBook import io.legado.app.model.remote.RemoteBook
import io.legado.app.model.remote.RemoteBookWebDav import io.legado.app.model.remote.RemoteBookWebDav
import io.legado.app.utils.isContentScheme
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
class RemoteBookRepository( class RemoteBookRepository(
private val appDb: AppDatabase private val appDb: AppDatabase
) { ) {
suspend fun getWebDav(book: Book): RemoteBookWebDav? {
val remoteUrl = book.getRemoteUrl() ?: return null
val analyzeUrl = AnalyzeUrl(remoteUrl)
// 优先从书籍 origin 中解析 serverID
val serverId = analyzeUrl.serverID
if (serverId != null && serverId != AppConst.DEFAULT_WEBDAV_ID) {
appDb.serverDao.get(serverId)?.getWebDavConfig()?.let {
return RemoteBookWebDav(it.url, Authorization(it), serverId)
}
}
// 如果没有指定 serverID,尝试使用目前选择的 WebDAV 服务器
val currentServerId = AppConfig.remoteServerId
if (currentServerId != AppConst.DEFAULT_WEBDAV_ID) {
appDb.serverDao.get(currentServerId)?.getWebDavConfig()?.let {
return RemoteBookWebDav(it.url, Authorization(it), currentServerId)
}
}
// 最后回退到默认的 WebDAV 配置
return AppWebDav.defaultBookWebDav
}
suspend fun refreshLocalBook(book: Book): Boolean {
if (!book.isLocal) return false
val remoteUrl = book.getRemoteUrl() ?: return false
val webDav = getWebDav(book) ?: throw NoStackTraceException("webDav没有配置")
val remoteBook = webDav.getRemoteBook(remoteUrl)
if (remoteBook == null) {
book.origin = BookType.localTag
return true
}
if (remoteBook.lastModify > book.lastCheckTime) {
val uri = webDav.downloadRemoteBook(remoteBook)
book.bookUrl = if (uri.isContentScheme()) uri.toString() else uri.path!!
book.lastCheckTime = remoteBook.lastModify
return true
}
return false
}
suspend fun syncBookFromRemote(book: Book): Book {
val remoteUrl = book.getRemoteUrl() ?: throw NoStackTraceException("不是远程书籍")
val webDav = getWebDav(book) ?: throw NoStackTraceException("webDav没有配置")
val remoteBook =
webDav.getRemoteBook(remoteUrl) ?: throw NoStackTraceException("远程文件不存在")
val downloadBookUri = webDav.downloadRemoteBook(remoteBook)
val importedBooks = LocalBook.importFiles(downloadBookUri)
val newBook = importedBooks.firstOrNull() ?: throw NoStackTraceException("导入失败")
newBook.durChapterIndex = book.durChapterIndex
newBook.durChapterPos = book.durChapterPos
newBook.order = book.order
newBook.group = book.group
return newBook
}
suspend fun uploadBook(book: Book) {
val webDav = getWebDav(book) ?: throw NoStackTraceException("未配置webDav")
webDav.upload(book)
book.lastCheckTime = System.currentTimeMillis()
}
suspend fun createWebDav(serverId: Long): RemoteBookWebDav? { suspend fun createWebDav(serverId: Long): RemoteBookWebDav? {
return appDb.serverDao.get(serverId) return appDb.serverDao.get(serverId)
?.getWebDavConfig() ?.getWebDavConfig()
@@ -12,6 +12,7 @@ import io.legado.app.data.repository.UploadRepository
import io.legado.app.ui.book.bookmark.AllBookmarkViewModel import io.legado.app.ui.book.bookmark.AllBookmarkViewModel
import io.legado.app.ui.book.explore.ExploreShowViewModel import io.legado.app.ui.book.explore.ExploreShowViewModel
import io.legado.app.ui.book.import.remote.RemoteBookViewModel import io.legado.app.ui.book.import.remote.RemoteBookViewModel
import io.legado.app.ui.book.info.BookInfoViewModel
import io.legado.app.ui.book.readRecord.ReadRecordViewModel import io.legado.app.ui.book.readRecord.ReadRecordViewModel
import io.legado.app.ui.book.searchContent.SearchContentViewModel import io.legado.app.ui.book.searchContent.SearchContentViewModel
import io.legado.app.ui.book.toc.TocViewModel import io.legado.app.ui.book.toc.TocViewModel
@@ -51,6 +52,7 @@ val appModule = module {
viewModelOf(::OtherConfigViewModel) viewModelOf(::OtherConfigViewModel)
viewModelOf(::TocViewModel) viewModelOf(::TocViewModel)
viewModelOf(::RemoteBookViewModel) viewModelOf(::RemoteBookViewModel)
viewModelOf(::BookInfoViewModel)
viewModel { (route: ReplaceEditRoute) -> viewModel { (route: ReplaceEditRoute) ->
ReplaceEditViewModel( ReplaceEditViewModel(
@@ -22,7 +22,6 @@ import android.view.View
import android.widget.CheckBox import android.widget.CheckBox
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.activity.addCallback import androidx.activity.addCallback
import androidx.activity.viewModels
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.app.ActivityOptionsCompat import androidx.core.app.ActivityOptionsCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
@@ -44,10 +43,7 @@ import io.legado.app.data.entities.BookChapter
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSource
import io.legado.app.databinding.ActivityBookInfoBinding import io.legado.app.databinding.ActivityBookInfoBinding
import io.legado.app.databinding.DialogEditTextBinding import io.legado.app.databinding.DialogEditTextBinding
import io.legado.app.exception.NoStackTraceException
import io.legado.app.help.AppWebDav
import io.legado.app.help.book.addType import io.legado.app.help.book.addType
import io.legado.app.help.book.getRemoteUrl
import io.legado.app.help.book.isAudio import io.legado.app.help.book.isAudio
import io.legado.app.help.book.isImage import io.legado.app.help.book.isImage
import io.legado.app.help.book.isLocal import io.legado.app.help.book.isLocal
@@ -60,7 +56,6 @@ import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.dialogs.selector import io.legado.app.lib.dialogs.selector
import io.legado.app.model.BookCover import io.legado.app.model.BookCover
import io.legado.app.model.SourceCallBack import io.legado.app.model.SourceCallBack
import io.legado.app.model.remote.RemoteBookWebDav
import io.legado.app.ui.about.AppLogDialog import io.legado.app.ui.about.AppLogDialog
import io.legado.app.ui.book.audio.AudioPlayActivity import io.legado.app.ui.book.audio.AudioPlayActivity
import io.legado.app.ui.book.changecover.ChangeCoverDialog import io.legado.app.ui.book.changecover.ChangeCoverDialog
@@ -101,6 +96,7 @@ import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koin.androidx.viewmodel.ext.android.viewModel
class BookInfoActivity : class BookInfoActivity :
VMBaseActivity<ActivityBookInfoBinding, BookInfoViewModel>(), VMBaseActivity<ActivityBookInfoBinding, BookInfoViewModel>(),
@@ -170,7 +166,7 @@ class BookInfoActivity :
private val book get() = viewModel.getBook(false) private val book get() = viewModel.getBook(false)
override val binding by viewBinding(ActivityBookInfoBinding::inflate) override val binding by viewBinding(ActivityBookInfoBinding::inflate)
override val viewModel by viewModels<BookInfoViewModel>() override val viewModel: BookInfoViewModel by viewModel()
@SuppressLint("PrivateResource") @SuppressLint("PrivateResource")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -333,14 +329,9 @@ class BookInfoActivity :
R.id.menu_delete_alert -> LocalConfig.bookInfoDeleteAlert = !item.isChecked R.id.menu_delete_alert -> LocalConfig.bookInfoDeleteAlert = !item.isChecked
R.id.menu_upload -> { R.id.menu_upload -> {
viewModel.getBook()?.let { book -> viewModel.getBook()?.let { book ->
book.getRemoteUrl()?.let { viewModel.uploadBook(book) {
alert(R.string.draw, R.string.sure_upload) { toastOnUi("上传成功")
okButton { }
upLoadBook(book)
}
cancelButton()
}
} ?: upLoadBook(book)
} }
} }
} }
@@ -374,17 +365,6 @@ class BookInfoActivity :
} }
} }
// override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
// if (ev.action == MotionEvent.ACTION_DOWN) {
// currentFocus?.let {
// if (it === binding.tvIntro && binding.tvIntro.hasSelection()) {
// it.clearFocus()
// }
// }
// }
// return super.dispatchTouchEvent(ev)
// }
private fun setupBackCallback() { private fun setupBackCallback() {
onBackPressedDispatcher.addCallback(this) { onBackPressedDispatcher.addCallback(this) {
if (!viewModel.inBookshelf && AppConfig.showAddToShelfAlert) { if (!viewModel.inBookshelf && AppConfig.showAddToShelfAlert) {
@@ -413,28 +393,6 @@ class BookInfoActivity :
} }
} }
private fun upLoadBook(
book: Book,
bookWebDav: RemoteBookWebDav? = AppWebDav.defaultBookWebDav,
) {
lifecycleScope.launch {
waitDialog.setText("上传中.....")
waitDialog.show()
try {
bookWebDav
?.upload(book)
?: throw NoStackTraceException("未配置webDav")
//更新书籍最后更新时间,使之比远程书籍的时间新
book.lastCheckTime = System.currentTimeMillis()
viewModel.saveBook(book)
} catch (e: Exception) {
toastOnUi(e.localizedMessage)
} finally {
waitDialog.dismiss()
}
}
}
private fun showBook(book: Book) = binding.run { private fun showBook(book: Book) = binding.run {
showCover(book) showCover(book)
addColorScheme(binding.ivCover.drawable) addColorScheme(binding.ivCover.drawable)
@@ -15,12 +15,11 @@ import io.legado.app.data.appDb
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookChapter
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSource
import io.legado.app.data.repository.RemoteBookRepository
import io.legado.app.exception.NoBooksDirException import io.legado.app.exception.NoBooksDirException
import io.legado.app.exception.NoStackTraceException import io.legado.app.exception.NoStackTraceException
import io.legado.app.help.AppWebDav
import io.legado.app.help.book.BookHelp import io.legado.app.help.book.BookHelp
import io.legado.app.help.book.getExportFileName import io.legado.app.help.book.getExportFileName
import io.legado.app.help.book.getRemoteUrl
import io.legado.app.help.book.isLocal import io.legado.app.help.book.isLocal
import io.legado.app.help.book.isNotShelf import io.legado.app.help.book.isNotShelf
import io.legado.app.help.book.isSameNameAuthor import io.legado.app.help.book.isSameNameAuthor
@@ -38,13 +37,15 @@ import io.legado.app.model.localBook.LocalBook
import io.legado.app.model.webBook.WebBook import io.legado.app.model.webBook.WebBook
import io.legado.app.utils.ArchiveUtils import io.legado.app.utils.ArchiveUtils
import io.legado.app.utils.UrlUtil import io.legado.app.utils.UrlUtil
import io.legado.app.utils.isContentScheme
import io.legado.app.utils.postEvent import io.legado.app.utils.postEvent
import io.legado.app.utils.toastOnUi import io.legado.app.utils.toastOnUi
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
class BookInfoViewModel(application: Application) : BaseViewModel(application) { class BookInfoViewModel(
application: Application,
private val remoteBookRepository: RemoteBookRepository
) : BaseViewModel(application) {
val bookData = MutableLiveData<Book>() val bookData = MutableLiveData<Book>()
val chapterListData = MutableLiveData<List<BookChapter>>() val chapterListData = MutableLiveData<List<BookChapter>>()
val webFiles = mutableListOf<WebFile>() val webFiles = mutableListOf<WebFile>()
@@ -122,18 +123,7 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
executeLazy(executeContext = IO) { executeLazy(executeContext = IO) {
if (book.isLocal) { if (book.isLocal) {
book.tocUrl = "" book.tocUrl = ""
book.getRemoteUrl()?.let { remoteBookRepository.refreshLocalBook(book)
val bookWebDav = AppWebDav.defaultBookWebDav
?: throw NoStackTraceException("webDav没有配置")
val remoteBook = bookWebDav.getRemoteBook(it)
if (remoteBook == null) {
book.origin = BookType.localTag
} else if (remoteBook.lastModify > book.lastCheckTime) {
val uri = bookWebDav.downloadRemoteBook(remoteBook)
book.bookUrl = if (uri.isContentScheme()) uri.toString() else uri.path!!
book.lastCheckTime = remoteBook.lastModify
}
}
} else { } else {
val bs = bookSource ?: return@executeLazy val bs = bookSource ?: return@executeLazy
if (book.originName != bs.bookSourceName) { if (book.originName != bs.bookSourceName) {
@@ -159,25 +149,9 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
val book = bookData.value ?: return val book = bookData.value ?: return
if (!book.isLocal) return if (!book.isLocal) return
val remoteUrl = book.getRemoteUrl() ?: return
execute { execute {
waitDialogData.postValue(true) waitDialogData.postValue(true)
val bookWebDav = AppWebDav.defaultBookWebDav val newBook = remoteBookRepository.syncBookFromRemote(book)
?: throw NoStackTraceException("webDav没有配置")
val remoteBook = bookWebDav.getRemoteBook(remoteUrl)
?: throw NoStackTraceException("远程文件不存在")
val downloadBookUri = bookWebDav.downloadRemoteBook(remoteBook)
val importedBooks = LocalBook.importFiles(downloadBookUri)
val newBook = importedBooks.firstOrNull() ?: throw NoStackTraceException("导入失败")
newBook.durChapterIndex = book.durChapterIndex
newBook.durChapterPos = book.durChapterPos
newBook.order = book.order
newBook.group = book.group
appDb.bookDao.delete(book) appDb.bookDao.delete(book)
appDb.bookDao.insert(newBook) appDb.bookDao.insert(newBook)
@@ -188,6 +162,21 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
context.toastOnUi("同步完成") context.toastOnUi("同步完成")
}.onFinally { }.onFinally {
waitDialogData.postValue(false) waitDialogData.postValue(false)
}.onError {
context.toastOnUi(it.localizedMessage)
}
}
fun uploadBook(book: Book, success: () -> Unit) {
execute {
waitDialogData.postValue(true)
remoteBookRepository.uploadBook(book)
saveBook(book)
success.invoke()
}.onFinally {
waitDialogData.postValue(false)
}.onError {
context.toastOnUi(it.localizedMessage)
} }
} }