Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ plugins {

// 支持通过 -PVERSION_NAME / -PVERSION_CODE 显式传入版本信息。
// 未显式传入 VERSION_NAME 时,默认把最后一段 patch 替换为 BUILD_NUMBER。
val baseAppVersionName = "0.11.0"
val baseAppVersionName = "1.0.0"

fun String?.nonBlankOrNull(): String? =
this?.trim()?.takeIf { it.isNotBlank() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,6 @@ object MediaSourceInfoConventions {
if (recognitionMode != MediaRecognitionMode.DIRECTORY) {
put(CONNECTION_RECOGNITION_MODE, recognitionMode.name)
}
if (recognitionMode == MediaRecognitionMode.MLIP && mlipMetadataMode != MlipMetadataMode.LIBRARY_DB_LOCAL_PRIORITY) {
put(CONNECTION_MLIP_METADATA_MODE, mlipMetadataMode.name)
}
}

fun sourceConnectionInfoFromPersistence(
Expand Down Expand Up @@ -233,23 +230,10 @@ fun MediaSourceInfo.withRecognitionMode(mode: MediaRecognitionMode): MediaSource
)

fun MediaSourceInfo.mlipMetadataMode(): MlipMetadataMode =
if (recognitionMode() != MediaRecognitionMode.MLIP) {
MlipMetadataMode.LIBRARY_DB_LOCAL_PRIORITY
} else {
connectionInfo[MediaSourceInfoConventions.CONNECTION_MLIP_METADATA_MODE]
?.trim()
?.let { value -> MlipMetadataMode.entries.firstOrNull { it.name.equals(value, ignoreCase = true) } }
?: MlipMetadataMode.LIBRARY_DB_LOCAL_PRIORITY
}
MlipMetadataMode.LIBRARY_DB_LOCAL_PRIORITY

fun MediaSourceInfo.withMlipMetadataMode(mode: MlipMetadataMode): MediaSourceInfo =
copy(
connectionInfo = if (recognitionMode() != MediaRecognitionMode.MLIP || mode == MlipMetadataMode.LIBRARY_DB_LOCAL_PRIORITY) {
connectionInfo - MediaSourceInfoConventions.CONNECTION_MLIP_METADATA_MODE
} else {
connectionInfo + (MediaSourceInfoConventions.CONNECTION_MLIP_METADATA_MODE to mode.name)
},
)
copy(connectionInfo = connectionInfo - MediaSourceInfoConventions.CONNECTION_MLIP_METADATA_MODE)

fun MediaSourceInfo.sourceLocation(): String? =
localRootPath() ?: remoteUrl()
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ class MediaSourceInfoConventionsTest {
}

@Test
fun `mlip metadata mode defaults to library db local priority and stores files only`() {
fun `legacy mlip metadata mode is ignored`() {
val defaultSource = source(connectionInfo = emptyMap())
val filesOnlyInfo = MediaSourceInfoConventions.sourceConnectionInfo(
type = MediaSourceType.WEBDAV,
Expand All @@ -242,8 +242,8 @@ class MediaSourceInfoConventionsTest {
val directorySource = filesOnlySource.withRecognitionMode(MediaRecognitionMode.DIRECTORY)

assertEquals(MlipMetadataMode.LIBRARY_DB_LOCAL_PRIORITY, defaultSource.mlipMetadataMode())
assertEquals(MlipMetadataMode.FILES_ONLY, filesOnlySource.mlipMetadataMode())
assertEquals("FILES_ONLY", filesOnlyInfo[MediaSourceInfoConventions.CONNECTION_MLIP_METADATA_MODE])
assertEquals(MlipMetadataMode.LIBRARY_DB_LOCAL_PRIORITY, filesOnlySource.mlipMetadataMode())
assertFalse(MediaSourceInfoConventions.CONNECTION_MLIP_METADATA_MODE in filesOnlyInfo)
assertFalse(MediaSourceInfoConventions.CONNECTION_MLIP_METADATA_MODE in defaultMlipInfo)
assertFalse(MediaSourceInfoConventions.CONNECTION_MLIP_METADATA_MODE in directorySource.connectionInfo)
assertFalse(
Expand Down
7 changes: 7 additions & 0 deletions data/src/main/kotlin/com/miruplay/tv/data/dao/EpisodeDao.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction
import com.miruplay.tv.data.entity.EpisodeEntity

private const val EPISODE_COLUMNS = "id, anime_id, season_number, episode_number, title, file_path, file_name, duration, thumbnail_path, bangumi_episode_id, bangumi_collection_type, last_updated"
Expand All @@ -13,6 +14,12 @@ interface EpisodeDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(episodes: List<EpisodeEntity>)

@Transaction
suspend fun replaceByAnimeId(animeId: String, episodes: List<EpisodeEntity>) {
deleteByAnimeId(animeId)
insertAll(episodes)
}

@Query("SELECT $EPISODE_COLUMNS FROM episode WHERE anime_id = :animeId ORDER BY season_number ASC, episode_number ASC")
suspend fun getByAnimeId(animeId: String): List<EpisodeEntity>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,8 @@ class MetadataRepositoryImpl @Inject constructor(

override suspend fun cacheEpisodes(animeId: String, episodes: List<Episode>): Result<Unit> = withContext(Dispatchers.IO) {
try {
episodeDao.deleteByAnimeId(animeId)
val entities = episodes.map { it.toEntity(animeId, it.seasonNumber) }
episodeDao.insertAll(entities)
episodeDao.replaceByAnimeId(animeId, entities)
// Also update episode count in cached anime metadata
animeDao.getById(animeId)?.let { animeEntity ->
animeDao.insert(animeEntity.copy(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package com.miruplay.tv.desktop

import com.miruplay.tv.core.common.AppError
import com.miruplay.tv.core.common.Result
import com.miruplay.tv.mediasource.desktop.desktopSourceFromInfo
import com.miruplay.tv.model.MediaRecognitionMode
import com.miruplay.tv.model.MediaSourceInfo
import com.miruplay.tv.model.MediaSourceType
import com.miruplay.tv.model.ScanResult
import com.miruplay.tv.model.cloudRssRescanStartedStatus
import com.miruplay.tv.model.localRootPath
import com.miruplay.tv.model.recognitionMode
import com.miruplay.tv.model.scanResultDisplayName
import com.miruplay.tv.model.sourcePickerTitle
import com.miruplay.tv.repository.MediaIndexEntry
Expand Down Expand Up @@ -47,6 +50,9 @@ internal suspend fun scanAndIndexDesktopSource(
metadataRepository: MetadataRepository,
scanner: DesktopMediaLibraryScanner = DesktopMediaLibraryScanner(),
): Result<DesktopSourceScanResult> {
if (sourceInfo.recognitionMode() == MediaRecognitionMode.MLIP) {
return Result.failure(AppError.LibraryIndexError.ReadFailed("桌面版暂不支持导入 MLIP library.db"))
}
val source = desktopSourceFromInfo(sourceInfo)
val rootPath = when (sourceInfo.type) {
MediaSourceType.LOCAL -> sourceInfo.localRootPath().orEmpty()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import com.miruplay.tv.model.CloudDriveLibraryMode
import com.miruplay.tv.model.PLAYBACK_SEEK_BACK_SECONDS
import com.miruplay.tv.model.PLAYBACK_SEEK_FORWARD_SECONDS
import com.miruplay.tv.model.PLAYBACK_SPEED_NORMAL
import com.miruplay.tv.model.MediaRecognitionMode
import com.miruplay.tv.model.MediaSourceInfo
import com.miruplay.tv.model.MediaSourceInfoConventions
import com.miruplay.tv.model.MediaPathConventions
Expand All @@ -68,6 +69,7 @@ import com.miruplay.tv.model.RssSubscriptionFormResult
import com.miruplay.tv.model.RssSubscriptionInfo
import com.miruplay.tv.model.ScraperResult
import com.miruplay.tv.model.cloudRssScheduledSyncCompleteStatus
import com.miruplay.tv.model.recognitionMode
import com.miruplay.tv.model.cloudRssInitialStatus
import com.miruplay.tv.model.cloudRssRescanStartedStatus
import com.miruplay.tv.model.cloudRssLinkedScanSourceStatus
Expand Down Expand Up @@ -480,6 +482,9 @@ internal fun MiruPlayDesktopComposeApp(
indexRepository = repositories.index,
metadataRepository = repositories.metadata,
bangumiScraper = bangumiScraper,
isSourceMetadataReadOnly = { sourceId ->
repositories.mediaSources.getSourceById(sourceId).getOrNull()?.recognitionMode() == MediaRecognitionMode.MLIP
},
)
}
val bangumiSyncCore = remember(bangumiScraper, repositories) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ package com.miruplay.tv.desktop

import com.miruplay.tv.core.common.Result
import com.miruplay.tv.mediasource.desktop.DesktopLocalMediaSource
import com.miruplay.tv.model.MediaRecognitionMode
import com.miruplay.tv.model.MediaSourceInfo
import com.miruplay.tv.model.MediaSourceInfoConventions
import com.miruplay.tv.model.MediaSourceType
import com.miruplay.tv.model.libraryScanCompleteStatus
import com.miruplay.tv.model.cloudRssScheduledSyncCompleteStatus
import com.miruplay.tv.model.libraryRescanCompleteStatus
Expand Down Expand Up @@ -62,6 +65,34 @@ class DesktopScanIndexIntegrationTest {
}
}

@Test
fun `desktop scan rejects MLIP instead of treating it as a directory source`() = runBlocking {
val storeRoot = Files.createTempDirectory("miruplay-desktop-store")
try {
val repositories = DesktopRepositories.fileBacked(storeRoot.resolve("store.json"))
val source = MediaSourceInfo(
id = 7L,
name = "MLIP WebDAV",
type = MediaSourceType.WEBDAV,
connectionInfo = MediaSourceInfoConventions.sourceConnectionInfo(
type = MediaSourceType.WEBDAV,
location = "https://example.test/anime",
recognitionMode = MediaRecognitionMode.MLIP,
),
)

val result = scanAndIndexDesktopSource(
sourceInfo = source,
indexRepository = repositories.index,
metadataRepository = repositories.metadata,
)

assertTrue(result is Result.Error)
} finally {
storeRoot.toFile().deleteRecursively()
}
}

@Test
fun `cloud rss linked source rescan refreshes repository index`() = runBlocking {
val mediaRoot = Files.createTempDirectory("miruplay-cloud-rss-rescan")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ fun MediaIndexEntry.clearExternalMetadata(sourceId: Long = this.sourceId): Media
scrapedAt = 0L,
)

fun MediaIndexEntry.isMlipManagedMetadata(): Boolean =
metadataSource.equals("MLIP", ignoreCase = true) ||
metadataId?.startsWith("mlip:") == true ||
localMetadataOverrideKey()?.startsWith("mlip:") == true

fun MediaIndexEntry.localMetadataOverrideKey(): String? =
scrapeMessage
?.takeIf { it.startsWith(LOCAL_METADATA_OVERRIDE_PREFIX) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,9 @@ fun metadataSearchResultStatus(
fun metadataSourceRequiredStatus(): String =
metadataSourceRequiredTvStatus()

fun mlipMetadataReadOnlyStatus(): String =
"MLIP 元数据由 library.db 管理,请在远端修正后重新扫描。"

fun MetadataBatchMatch.selectedReviewStatus(): String =
metadataSelectedBatchReviewTvStatus(query)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ internal class FileBackedMediaIndexRepository(
state.index
.asSequence()
.filter { it.sourceId == sourceId }
.mapNotNull { it.animeName?.takeIf(String::isNotBlank) }
.flatMap { entry -> sequenceOf(entry.metadataId, entry.animeName, entry.metadataTitle) }
.mapNotNull { it?.takeIf(String::isNotBlank) }
.distinct()
.sorted()
.toList()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,8 @@ class DesktopRepositoriesTest {
animeName = "Show B",
episodeTitle = "A Good Episode",
plot = "The indexed plot",
metadataId = "mlip:7:show-b",
metadataTitle = "节目 B",
),
),
)
Expand All @@ -384,7 +386,7 @@ class DesktopRepositoriesTest {
assertEquals("D:/Anime/B/01.mkv", plotQuery.data.single().path)

val names = repositories.index.getAnimeInIndex(7L) as Result.Success
assertEquals(listOf("Show A", "Show B"), names.data)
assertEquals(setOf("Show A", "Show B", "mlip:7:show-b", "节目 B"), names.data.toSet())

repositories.index.clearIndex(7L)
val cleared = repositories.index.queryIndex(7L, "") as Result.Success
Expand Down
Loading
Loading