Skip to content

Commit

Permalink
fix loading
Browse files Browse the repository at this point in the history
  • Loading branch information
Him188 committed Sep 23, 2024
1 parent 4ed06c8 commit 484c4e7
Show file tree
Hide file tree
Showing 10 changed files with 108 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,10 @@ class AndroidWebViewVideoExtractor : WebViewVideoExtractor {
pageUrl: String,
resourceMatcher: (String) -> Instruction,
): WebResource? {
val deferred = CompletableDeferred<WebResource>()
withContext(Dispatchers.Main) {
// WebView requires same thread
// Executors.newSingleThreadExecutor().asCoroutineDispatcher().use { dispatcher ->
return withContext(Dispatchers.Main) {
val deferred = CompletableDeferred<WebResource>()
val loadedNestedUrls = ConcurrentSkipListSet<String>()

/**
Expand All @@ -134,14 +136,13 @@ class AndroidWebViewVideoExtractor : WebViewVideoExtractor {

Instruction.LoadPage -> {
logger.info { "WebView loading nested page: $url" }
launch {
withContext(Dispatchers.Main) {
@Suppress("LABEL_NAME_CLASH")
if (webView.url == url) return@withContext // avoid infinite loop
if (!loadedNestedUrls.add(url)) return@withContext
logger.info { "New webview created" }
createWebView(context, deferred, ::handleUrl).loadUrl(url)
}
launch(Dispatchers.Main) {
@Suppress("LABEL_NAME_CLASH")
if (webView.url == url) return@launch // avoid infinite loop
if (!loadedNestedUrls.add(url)) return@launch
logger.info { "WebView navigating to new url: $url" }
webView.loadUrl(url)
// createWebView(context, deferred, ::handleUrl).loadUrl(url)
}
return false
}
Expand All @@ -151,29 +152,30 @@ class AndroidWebViewVideoExtractor : WebViewVideoExtractor {
loadedNestedUrls.add(pageUrl)
createWebView(context, deferred, ::handleUrl).loadUrl(pageUrl)

// webView.webChromeClient = object : WebChromeClient() {
// override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean {
// consoleMessage ?: return false
// val message = consoleMessage.message() ?: return false
// // HTTPS 页面加载 HTTP 的视频时会有日志
// for (matchResult in consoleMessageUrlRegex.findAll(message)) {
// val url = matchResult.value.removeSurrounding("'")
// logger.info { "WebView console get url: $url" }
// handleUrl(url)
// }
// return false
// }
// }
}

return try {
deferred.await()
} catch (e: Throwable) {
if (deferred.isActive) {
deferred.cancel()
// webView.webChromeClient = object : WebChromeClient() {
// override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean {
// consoleMessage ?: return false
// val message = consoleMessage.message() ?: return false
// // HTTPS 页面加载 HTTP 的视频时会有日志
// for (matchResult in consoleMessageUrlRegex.findAll(message)) {
// val url = matchResult.value.removeSurrounding("'")
// logger.info { "WebView console get url: $url" }
// handleUrl(url)
// }
// return false
// }
// }

try {
deferred.await()
} catch (e: Throwable) {
if (deferred.isActive) {
deferred.cancel()
}
throw e
}
throw e
}
// }
}

@SuppressLint("SetJavaScriptEnabled")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,11 @@ class SelectorMediaSource(
query: SelectorSearchQuery,
mediaSourceId: String,
): ApiResponse<List<DefaultMedia>> {
return searchSubjects(searchConfig.searchUrl, query.subjectName).map { (_, document) ->
return searchSubjects(
searchConfig.searchUrl,
subjectName = query.subjectName,
useOnlyFirstWord = searchConfig.searchUseOnlyFirstWord,
).map { (_, document) ->
document ?: return@map emptyList()
val episodes = selectSubjects(document, searchConfig)
.orEmpty()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,11 @@ abstract class SelectorMediaSourceEngine {
suspend fun searchSubjects(
searchUrl: String,
subjectName: String,
useOnlyFirstWord: Boolean,
): ApiResponse<SearchSubjectResult> {
val encodedUrl = MediaSourceEngineHelpers.encodeUrlSegment(subjectName)
val encodedUrl = MediaSourceEngineHelpers.encodeUrlSegment(
if (useOnlyFirstWord) getFirstWord(subjectName) else subjectName,
)

val finalUrl = Url(
searchUrl.replace("{keyword}", encodedUrl),
Expand All @@ -93,6 +96,11 @@ abstract class SelectorMediaSourceEngine {
return searchImpl(finalUrl)
}

private fun getFirstWord(string: String): String {
if (!(string.contains(' '))) return string
return string.substringBefore(' ').ifBlank { string }
}

protected abstract suspend fun searchImpl(
finalUrl: Url,
): ApiResponse<SearchSubjectResult>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import org.intellij.lang.annotations.Language
data class SelectorSearchConfig(
// Phase 1, search
val searchUrl: String = "", // required
val searchUseOnlyFirstWord: Boolean = true,
val preferShortest: Boolean = true,
// Phase 2, for search result, select subjects
val subjectFormatId: SelectorFormatId = SelectorSubjectFormatA.id,
val selectorSubjectFormatA: SelectorSubjectFormatA.Config = SelectorSubjectFormatA.Config(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ data object SelectorSubjectFormatA : SelectorSubjectFormat<SelectorSubjectFormat
data class Config(
@Language("css")
val selectLists: String = "div.video-info-header > a",
val preferShorterName: Boolean = true,
) : SelectorFormatConfig {
override fun isValid(): Boolean {
return selectLists.isNotBlank()
Expand All @@ -65,7 +66,8 @@ data object SelectorSubjectFormatA : SelectorSubjectFormat<SelectorSubjectFormat
config: Config,
): List<WebSearchSubjectInfo>? {
val selectLists = QueryParser.parseSelectorOrNull(config.selectLists) ?: return null
return document.select(selectLists).map { a ->
val elements = document.select(selectLists)
return elements.mapTo(ArrayList(elements.size)) { a ->
val name = a.attr("title").takeIf { it.isNotBlank() } ?: a.text()
val href = a.attr("href")
val id = href.substringBeforeLast(".html").substringAfterLast("/")
Expand All @@ -75,6 +77,12 @@ data object SelectorSubjectFormatA : SelectorSubjectFormat<SelectorSubjectFormat
subjectDetailsPageUrl = SelectorHelpers.computeAbsoluteUrl(baseUrl, href),
origin = a,
)
}.apply {
if (config.preferShorterName) {
sortBy { info ->
info.name.length
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,6 @@ class EditSelectorMediaSourcePageState(
if (episodeNavController.currentDestination?.hasRoute<SelectorEpisodePaneRoutes.EPISODE>() != true) {
episodeNavController.navigate(SelectorEpisodePaneRoutes.EPISODE)
}
episodeNavController.navigate(SelectorEpisodePaneRoutes.EPISODE)
}

fun stopViewing() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,22 +36,28 @@ class SelectorConfigState(

var displayName by argumentsStorage.prop(
{ it.name }, { copy(name = it) },
"",
SelectorMediaSourceArguments.Default.name,
)

val displayNameIsError by derivedStateOf { displayName.isBlank() }

var iconUrl by argumentsStorage.prop(
{ it.iconUrl }, { copy(iconUrl = it) },
"",
SelectorMediaSourceArguments.Default.iconUrl,
)

var searchUrl by argumentsStorage.prop(
{ it.searchConfig.searchUrl }, { copy(searchConfig = searchConfig.copy(searchUrl = it)) },
"",
SelectorMediaSourceArguments.Default.searchConfig.searchUrl,
)
val searchUrlIsError by derivedStateOf { searchUrl.isBlank() }

var searchUseOnlyFirstWord by argumentsStorage.prop(
{ it.searchConfig.searchUseOnlyFirstWord },
{ copy(searchConfig = searchConfig.copy(searchUseOnlyFirstWord = it)) },
SelectorMediaSourceArguments.Default.searchConfig.searchUseOnlyFirstWord,
)

// region SubjectFormat

val subjectFormatA = SubjectFormatAConfig()
Expand All @@ -77,6 +83,7 @@ class SelectorConfigState(
val selectListsIsError by derivedStateOf {
QueryParser.parseSelectorOrNull(selectLists) == null
}
var preferShorterName by prop({ it.preferShorterName }, { copy(preferShorterName = it) })
}

// endregion
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ internal fun SelectorConfigurationDefaults.MatchVideoSection(
Modifier
.padding(bottom = (verticalSpacing - 8.dp).coerceAtLeast(0.dp))
.clickable { matchVideoConfig.enableNestedUrl = !matchVideoConfig.enableNestedUrl },
supportingContent = { Text("当遇到匹配的链接时,跳转到该链接,并继续匹配视频链接。支持任意次数嵌套") },
supportingContent = { Text("当遇到匹配的链接时,终止父页面加载并跳转到匹配的链接,在嵌套页面中继续查找视频链接。支持任意次数嵌套") },
trailingContent = {
Switch(matchVideoConfig.enableNestedUrl, { matchVideoConfig.enableNestedUrl = it })
},
Expand All @@ -79,7 +79,7 @@ internal fun SelectorConfigurationDefaults.MatchVideoSection(
.moveFocusOnEnter()
.padding(bottom = verticalSpacing),
label = { Text("匹配嵌套链接") },
supportingText = { Text("从播放页面中加载的所有资源链接中匹配出需要跳转进入的链接。若正则包含名为 v 的分组则使用该分组,否则使用整个 URL") },
supportingText = { Text("正则表达式,从播放页面中加载的所有资源链接中匹配出需要跳转进入的链接。若正则包含名为 v 的分组则使用该分组,否则使用整个 URL") },
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
shape = textFieldShape,
isError = matchVideoConfig.matchNestedUrlIsError,
Expand All @@ -90,7 +90,7 @@ internal fun SelectorConfigurationDefaults.MatchVideoSection(
matchVideoConfig.matchVideoUrl, { matchVideoConfig.matchVideoUrl = it },
Modifier.fillMaxWidth().moveFocusOnEnter(),
label = { Text("匹配视频链接") },
supportingText = { Text("从播放页面中加载的所有资源链接中匹配出视频链接的正则表达式。若正则包含名为 v 的分组则使用该分组,否则使用整个 URL") },
supportingText = { Text("正则表达式,从播放页面中加载的所有资源链接中匹配出视频链接。若正则包含名为 v 的分组则使用该分组,否则使用整个 URL") },
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
shape = textFieldShape,
isError = matchVideoConfig.matchVideoUrlIsError,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ internal fun SelectorConfigurationPane(
}
}

Column(verticalArrangement = Arrangement.spacedBy(verticalSpacing)) {
Column {
OutlinedTextField(
state.searchUrl, { state.searchUrl = it },
Modifier.fillMaxWidth().moveFocusOnEnter(),
Expand All @@ -130,16 +130,39 @@ internal fun SelectorConfigurationPane(
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
shape = textFieldShape,
)
ListItem(
headlineContent = { Text("仅使用第一个词") },
Modifier
.padding(top = (verticalSpacing - 8.dp).coerceAtLeast(0.dp))
.clickable { state.searchUseOnlyFirstWord = !state.searchUseOnlyFirstWord },
supportingContent = { Text("以空格分割,仅使用第一个词搜索。适用于搜索兼容性差的情况") },
trailingContent = {
Switch(state.searchUseOnlyFirstWord, { state.searchUseOnlyFirstWord = it })
},
colors = listItemColors,
)

val conf = state.subjectFormatA
OutlinedTextField(
conf.selectLists, { conf.selectLists = it },
Modifier.fillMaxWidth().moveFocusOnEnter(),
Modifier.fillMaxWidth().moveFocusOnEnter().padding(top = verticalSpacing),
label = { Text("提取条目列表") },
supportingText = { Text("CSS Selector 表达式。期望返回一些 <a>,每个对应一个条目,将会读取其 href 属性和 text") },
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
shape = textFieldShape,
isError = conf.selectListsIsError,
)
ListItem(
headlineContent = { Text("选择最短标题") },
Modifier
.padding(top = (verticalSpacing - 8.dp).coerceAtLeast(0.dp))
.clickable { conf.preferShorterName = !conf.preferShorterName },
supportingContent = { Text("选择满足匹配的标题最短的条目。可避免为第一季匹配到第二季") },
trailingContent = {
Switch(conf.preferShorterName, { conf.preferShorterName = it })
},
colors = listItemColors,
)
}

Row(Modifier.padding(top = verticalSpacing, bottom = 12.dp)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ class SelectorTestState(
private val searchUrl by derivedStateOf {
searchConfigState.value?.searchUrl
}
private val useOnlyFirstWord by derivedStateOf {
searchConfigState.value?.searchUseOnlyFirstWord
}

/**
* 用于查询条目列表, 每当编辑请求和 `searchUrl`, 会重新搜索, 但不会筛选.
Expand All @@ -67,20 +70,24 @@ class SelectorTestState(
val subjectSearcher = BackgroundSearcher(
backgroundScope,
derivedStateOf {
val url = searchUrl
url to searchKeyword
Triple(
searchConfigState.value?.searchUrl,
searchKeyword,
searchConfigState.value?.searchUseOnlyFirstWord,
)
},
search = { (url, searchKeyword) ->
search = { (url, searchKeyword, useOnlyFirstWord) ->
// 不清除 selectedSubjectIndex

launchRequestInBackground {
if (url.isNullOrBlank() || searchKeyword.isBlank()) {
if (url == null || url.isBlank() || searchKeyword.isBlank() || useOnlyFirstWord == null) {
null
} else {
try {
val res = engine.searchSubjects(
url,
searchUrl = url,
searchKeyword,
useOnlyFirstWord = useOnlyFirstWord,
)
Result.success(res)
} catch (e: CancellationException) {
Expand Down

0 comments on commit 484c4e7

Please sign in to comment.