Skip to content

Commit

Permalink
fix matching video
Browse files Browse the repository at this point in the history
  • Loading branch information
Him188 committed Sep 23, 2024
1 parent 4d00df5 commit 4ed06c8
Show file tree
Hide file tree
Showing 17 changed files with 314 additions and 105 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import me.him188.ani.datasources.api.EpisodeSort
import me.him188.ani.datasources.api.MediaProperties
import me.him188.ani.datasources.api.SubtitleKind
import me.him188.ani.datasources.api.matcher.WebVideo
import me.him188.ani.datasources.api.matcher.WebVideoMatcher
import me.him188.ani.datasources.api.source.MediaSourceKind
import me.him188.ani.datasources.api.source.MediaSourceLocation
import me.him188.ani.datasources.api.topic.EpisodeRange
Expand Down Expand Up @@ -75,7 +76,9 @@ abstract class SelectorMediaSourceEngine {
* `null` means 404
*/
val document: Document?,
)
) {
override fun toString(): String = "SearchSubjectResult(url=$url, document=${document.toString().length}...)"
}

suspend fun searchSubjects(
searchUrl: String,
Expand Down Expand Up @@ -200,23 +203,39 @@ abstract class SelectorMediaSourceEngine {
}
}

fun matchWebVideo(url: String, searchConfig: SelectorSearchConfig.MatchVideoConfig): WebVideo? {
val result = searchConfig.matchVideoUrlRegex?.find(url) ?: return null
fun shouldLoadPage(url: String, config: SelectorSearchConfig.MatchVideoConfig): Boolean {
if (config.enableNestedUrl) {
config.matchNestedUrlRegex?.find(url)?.let {
return true
}
}
return false
}

fun matchWebVideo(url: String, searchConfig: SelectorSearchConfig.MatchVideoConfig): WebVideoMatcher.MatchResult {
if (shouldLoadPage(url, searchConfig)) {
return WebVideoMatcher.MatchResult.LoadPage
}

val result = searchConfig.matchVideoUrlRegex?.find(url) ?: return WebVideoMatcher.MatchResult.Continue
val videoUrl = try {
result.groups["v"]?.value ?: url
} catch (_: IllegalArgumentException) { // no group
url
}
return WebVideo(
videoUrl,
mapOf(
"User-Agent" to searchConfig.addHeadersToVideo.userAgent,
"Referer" to searchConfig.addHeadersToVideo.referer,
"Sec-Ch-Ua-Mobile" to "?0",
"Sec-Ch-Ua-Platform" to "macOS",
"Sec-Fetch-Dest" to "video",
"Sec-Fetch-Mode" to "no-cors",
"Sec-Fetch-Site" to "cross-site",

return WebVideoMatcher.MatchResult.Matched(
WebVideo(
videoUrl,
mapOf(
"User-Agent" to searchConfig.addHeadersToVideo.userAgent,
"Referer" to searchConfig.addHeadersToVideo.referer,
"Sec-Ch-Ua-Mobile" to "?0",
"Sec-Ch-Ua-Platform" to "macOS",
"Sec-Fetch-Dest" to "video",
"Sec-Fetch-Mode" to "no-cors",
"Sec-Fetch-Site" to "cross-site",
),
),
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import me.him188.ani.app.data.source.media.source.web.format.SelectorFormatId
import me.him188.ani.app.data.source.media.source.web.format.SelectorSubjectFormat
import me.him188.ani.app.data.source.media.source.web.format.SelectorSubjectFormatA
import me.him188.ani.app.data.source.media.source.web.format.parseOrNull
import org.intellij.lang.annotations.Language

@Immutable
@Serializable
Expand Down Expand Up @@ -81,11 +82,18 @@ data class SelectorSearchConfig(
}

@Serializable
@Suppress("RegExpRedundantEscape")
data class MatchVideoConfig(
@Suppress("RegExpRedundantEscape")
val matchVideoUrl: String = """^(?<v>http(s)?:\/\/(?!.*http(s)?:\/\/).+((\.mp4)|(\.mkv)|(m3u8)).*(\?.+)?)""",
val enableNestedUrl: Boolean = true,
@Language("regexp")
val matchNestedUrl: String = """^.+(m3u8|vip|xigua\.php).+\?""",
@Language("regexp")
val matchVideoUrl: String = """(^http(s)?:\/\/(?!.*http(s)?:\/\/).+((\.mp4)|(\.mkv)|(m3u8)).*(\?.+)?)|(akamaized)|(bilivideo.com)""",
val addHeadersToVideo: VideoHeaders = VideoHeaders(),
) {
val matchNestedUrlRegex by lazy {
Regex.parseOrNull(matchNestedUrl)
}
val matchVideoUrlRegex by lazy {
Regex.parseOrNull(matchVideoUrl)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import me.him188.ani.datasources.api.EpisodeSort
import me.him188.ani.utils.xml.Element
import me.him188.ani.utils.xml.QueryParser
import me.him188.ani.utils.xml.parseSelectorOrNull
import org.intellij.lang.annotations.Language

/**
* 决定如何匹配线路和剧集
Expand All @@ -43,7 +44,8 @@ sealed class SelectorChannelFormat<in Config : SelectorFormatConfig>(override va
return entries.find { it.id == id }
}

const val DEFAULT_MATCH_EPISODE_SORT_FROM_NAME = "第(?<ep>.+)(话|集)"
@Language("regexp")
const val DEFAULT_MATCH_EPISODE_SORT_FROM_NAME = """第\s*(?<ep>.+)\s*[话集]"""

fun isPossiblyMovie(title: String): Boolean {
return ("" in title || "" in title)
Expand Down Expand Up @@ -71,13 +73,17 @@ data object SelectorChannelFormatFlattened :
@Immutable
@Serializable
data class Config(
@Language("css")
val selectChannels: String = "body > div.box-width.cor5 > div.anthology.wow.fadeInUp.animated > div.anthology-tab.nav-swiper.b-b.br div.swiper-wrapper a.swiper-slide",
@Language("css")
val selectLists: String = "body > div.box-width.cor5 > div.anthology.wow.fadeInUp.animated > a",
@Language("css")
val selectElements: String = "a",
@Language("regexp")
val matchEpisodeSortFromName: String = DEFAULT_MATCH_EPISODE_SORT_FROM_NAME,
) : SelectorFormatConfig {
override fun isValid(): Boolean {
return selectChannels.isNotBlank() && selectLists.isNotBlank() && selectElements.isNotBlank()
return selectChannels.isNotBlank() && selectLists.isNotBlank() && selectElements.isNotBlank() && matchEpisodeSortFromName.isNotBlank()
}
}

Expand Down Expand Up @@ -130,15 +136,13 @@ data object SelectorChannelFormatNoChannel :
@Immutable
@Serializable
data class Config(
val selectEpisodes: String = "",
@Language("css")
val selectEpisodes: String = "#glist-1 > div.module-blocklist.scroll-box.scroll-box-y > div > a",
@Language("regexp")
val matchEpisodeSortFromName: String = DEFAULT_MATCH_EPISODE_SORT_FROM_NAME,
) : SelectorFormatConfig {
val matchEpisodeSortFromNameRegex by lazy(LazyThreadSafetyMode.PUBLICATION) {
try {
matchEpisodeSortFromName.toRegex()
} catch (e: Exception) {
null
}
Regex.parseOrNull(matchEpisodeSortFromName)
}

override fun isValid(): Boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ interface SelectorFormat {
@Immutable
@JvmInline
value class SelectorFormatId(
// in case we want to change type
val value: String,
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,26 @@ import me.him188.ani.app.data.source.media.source.web.WebSearchSubjectInfo
import me.him188.ani.utils.xml.Element
import me.him188.ani.utils.xml.QueryParser
import me.him188.ani.utils.xml.parseSelectorOrNull
import org.intellij.lang.annotations.Language

/**
* 决定如何匹配条目
*/
sealed class SelectorSubjectFormat<in Config : SelectorFormatConfig>(override val id: SelectorFormatId) :
SelectorFormat { // 方便改名

/**
* `null` means invalid config
*/
abstract fun select(
document: Element,
baseUrl: String,
config: Config,
): List<WebSearchSubjectInfo>
): List<WebSearchSubjectInfo>?

companion object {
val entries by lazy { // 必须 lazy, 否则可能获取到 null
listOf(SelectorSubjectFormatA)
listOf(checkNotNull(SelectorSubjectFormatA)) // checkNotNull is needed to be fail-fast
}

fun findById(id: SelectorFormatId): SelectorSubjectFormat<*>? {
Expand All @@ -46,7 +51,8 @@ data object SelectorSubjectFormatA : SelectorSubjectFormat<SelectorSubjectFormat
@Immutable
@Serializable
data class Config(
val selectLists: String = "",
@Language("css")
val selectLists: String = "div.video-info-header > a",
) : SelectorFormatConfig {
override fun isValid(): Boolean {
return selectLists.isNotBlank()
Expand All @@ -57,8 +63,8 @@ data object SelectorSubjectFormatA : SelectorSubjectFormat<SelectorSubjectFormat
document: Element,
baseUrl: String,
config: Config,
): List<WebSearchSubjectInfo> {
val selectLists = QueryParser.parseSelectorOrNull(config.selectLists) ?: return emptyList()
): List<WebSearchSubjectInfo>? {
val selectLists = QueryParser.parseSelectorOrNull(config.selectLists) ?: return null
return document.select(selectLists).map { a ->
val name = a.attr("title").takeIf { it.isNotBlank() } ?: a.text()
val href = a.attr("href")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ package me.him188.ani.app.ui.settings.mediasource.selector.episode

import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
Expand All @@ -35,13 +36,16 @@ import kotlin.coroutines.EmptyCoroutineContext
@Preview
fun PreviewSelectorEpisodePaneCompact() = ProvideFoundationCompositionLocalsForPreview {
Surface {
val state = rememberTestEditSelectorMediaSourceState(
SelectorSearchConfig.MatchVideoConfig(),
)
SelectorTestAndEpisodePane(
state = rememberTestEditSelectorMediaSourceState(
TestSelectorTestEpisodePresentations[0],
SelectorSearchConfig.MatchVideoConfig(),
),
state = state,
layout = SelectorEpisodePaneLayout.Compact,
)
SideEffect {
state.viewEpisode(TestSelectorTestEpisodePresentations[0])
}
}
}

Expand Down Expand Up @@ -112,7 +116,6 @@ internal fun rememberTestSelectorEpisodeState(
@TestOnly
@Composable
internal fun rememberTestEditSelectorMediaSourceState(
viewing: SelectorTestEpisodePresentation? = TestSelectorTestEpisodePresentations[0],
matchVideoConfig: SelectorSearchConfig.MatchVideoConfig = SelectorSearchConfig.MatchVideoConfig(),
urls: (pageUrl: String) -> List<String> = {
listOf("https://example.com/a.mkv")
Expand All @@ -134,10 +137,6 @@ internal fun rememberTestEditSelectorMediaSourceState(
backgroundScope = scope,
context,
flowDispatcher = EmptyCoroutineContext,
).apply {
viewing?.let { presentation ->
this.viewEpisode(presentation)
}
}
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavDestination.Companion.hasRoute
import androidx.navigation.NavHostController
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import me.him188.ani.app.data.source.media.resolver.WebViewVideoExtractor
Expand All @@ -53,6 +55,7 @@ import me.him188.ani.app.ui.settings.mediasource.selector.edit.SelectorConfigSta
import me.him188.ani.app.ui.settings.mediasource.selector.edit.SelectorConfigurationPane
import me.him188.ani.app.ui.settings.mediasource.selector.episode.SelectorEpisodePaneDefaults
import me.him188.ani.app.ui.settings.mediasource.selector.episode.SelectorEpisodePaneLayout
import me.him188.ani.app.ui.settings.mediasource.selector.episode.SelectorEpisodePaneRoutes
import me.him188.ani.app.ui.settings.mediasource.selector.episode.SelectorEpisodeState
import me.him188.ani.app.ui.settings.mediasource.selector.episode.SelectorTestAndEpisodePane
import me.him188.ani.app.ui.settings.mediasource.selector.test.SelectorTestEpisodePresentation
Expand All @@ -74,20 +77,27 @@ class EditSelectorMediaSourcePageState(

private val viewingItemState = mutableStateOf<SelectorTestEpisodePresentation?>(null)

// lateinit var episodeNavController: NavHostController
var viewingItem by viewingItemState
private set

lateinit var episodeNavController: NavHostController
internal set // set from ui

fun viewEpisode(
episode: SelectorTestEpisodePresentation,
) {
this.viewingItem = episode
// episodeNavController.navigate("details")
if (episodeNavController.currentDestination?.hasRoute<SelectorEpisodePaneRoutes.EPISODE>() != true) {
episodeNavController.navigate(SelectorEpisodePaneRoutes.EPISODE)
}
episodeNavController.navigate(SelectorEpisodePaneRoutes.EPISODE)
}

fun stopViewing() {
this.viewingItem = null
// episodeNavController.navigate("list")
if (episodeNavController.currentDestination?.hasRoute<SelectorEpisodePaneRoutes.TEST>() != true) {
episodeNavController.navigate(SelectorEpisodePaneRoutes.TEST)
}
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,16 @@ class SelectorConfigState(
matchVideoUrl.isBlank() || !isValidRegex(matchVideoUrl)
}

var enableNestedUrl by prop(
{ it.enableNestedUrl }, { copy(enableNestedUrl = it) },
)
var matchNestedUrl by prop(
{ it.matchNestedUrl }, { copy(matchNestedUrl = it) },
)
val matchNestedUrlIsError by derivedStateOf {
matchNestedUrl.isBlank() || !isValidRegex(matchNestedUrl)
}

val videoHeaders = HeadersConfig()

@Stable
Expand Down
Loading

0 comments on commit 4ed06c8

Please sign in to comment.