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

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
import io.legado.app.constant.AppConst
import io.legado.app.constant.BookType
import io.legado.app.data.AppDatabase
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.model.analyzeRule.AnalyzeUrl
import io.legado.app.model.analyzeRule.CustomUrl
import io.legado.app.model.localBook.LocalBook
import io.legado.app.model.remote.RemoteBook
import io.legado.app.model.remote.RemoteBookWebDav
import io.legado.app.utils.isContentScheme
import kotlinx.coroutines.flow.Flow
class RemoteBookRepository(
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? {
return appDb.serverDao.get(serverId)
?.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.explore.ExploreShowViewModel
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.searchContent.SearchContentViewModel
import io.legado.app.ui.book.toc.TocViewModel
@@ -51,6 +52,7 @@ val appModule = module {
viewModelOf(::OtherConfigViewModel)
viewModelOf(::TocViewModel)
viewModelOf(::RemoteBookViewModel)
viewModelOf(::BookInfoViewModel)
viewModel { (route: ReplaceEditRoute) ->
ReplaceEditViewModel(
@@ -22,7 +22,6 @@ import android.view.View
import android.widget.CheckBox
import android.widget.LinearLayout
import androidx.activity.addCallback
import androidx.activity.viewModels
import androidx.core.app.ActivityCompat
import androidx.core.app.ActivityOptionsCompat
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.databinding.ActivityBookInfoBinding
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.getRemoteUrl
import io.legado.app.help.book.isAudio
import io.legado.app.help.book.isImage
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.model.BookCover
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.book.audio.AudioPlayActivity
import io.legado.app.ui.book.changecover.ChangeCoverDialog
@@ -101,6 +96,7 @@ import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.androidx.viewmodel.ext.android.viewModel
class BookInfoActivity :
VMBaseActivity<ActivityBookInfoBinding, BookInfoViewModel>(),
@@ -170,7 +166,7 @@ class BookInfoActivity :
private val book get() = viewModel.getBook(false)
override val binding by viewBinding(ActivityBookInfoBinding::inflate)
override val viewModel by viewModels<BookInfoViewModel>()
override val viewModel: BookInfoViewModel by viewModel()
@SuppressLint("PrivateResource")
override fun onCreate(savedInstanceState: Bundle?) {
@@ -333,14 +329,9 @@ class BookInfoActivity :
R.id.menu_delete_alert -> LocalConfig.bookInfoDeleteAlert = !item.isChecked
R.id.menu_upload -> {
viewModel.getBook()?.let { book ->
book.getRemoteUrl()?.let {
alert(R.string.draw, R.string.sure_upload) {
okButton {
upLoadBook(book)
}
cancelButton()
}
} ?: upLoadBook(book)
viewModel.uploadBook(book) {
toastOnUi("上传成功")
}
}
}
}
@@ -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() {
onBackPressedDispatcher.addCallback(this) {
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 {
showCover(book)
addColorScheme(binding.ivCover.drawable)
@@ -1172,4 +1130,4 @@ class BookInfoActivity :
toastOnUi("已添加到书架")
}
}
}
}
@@ -15,12 +15,11 @@ import io.legado.app.data.appDb
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter
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.NoStackTraceException
import io.legado.app.help.AppWebDav
import io.legado.app.help.book.BookHelp
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.isNotShelf
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.utils.ArchiveUtils
import io.legado.app.utils.UrlUtil
import io.legado.app.utils.isContentScheme
import io.legado.app.utils.postEvent
import io.legado.app.utils.toastOnUi
import kotlinx.coroutines.CoroutineScope
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 chapterListData = MutableLiveData<List<BookChapter>>()
val webFiles = mutableListOf<WebFile>()
@@ -122,18 +123,7 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
executeLazy(executeContext = IO) {
if (book.isLocal) {
book.tocUrl = ""
book.getRemoteUrl()?.let {
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
}
}
remoteBookRepository.refreshLocalBook(book)
} else {
val bs = bookSource ?: return@executeLazy
if (book.originName != bs.bookSourceName) {
@@ -159,25 +149,9 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
val book = bookData.value ?: return
if (!book.isLocal) return
val remoteUrl = book.getRemoteUrl() ?: return
execute {
waitDialogData.postValue(true)
val bookWebDav = AppWebDav.defaultBookWebDav
?: 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
val newBook = remoteBookRepository.syncBookFromRemote(book)
appDb.bookDao.delete(book)
appDb.bookDao.insert(newBook)
@@ -188,6 +162,21 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
context.toastOnUi("同步完成")
}.onFinally {
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)
}
}