[优化] 远程书籍同步逻辑
This commit is contained in:
@@ -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)
|
||||||
@@ -1172,4 +1130,4 @@ class BookInfoActivity :
|
|||||||
toastOnUi("已添加到书架")
|
toastOnUi("已添加到书架")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user