Preserve plugin-sourced subtitles and scraper overrides across re-fetch

plugin_result_to_stream only populated subtitle_tracks, not the subtitles
field Kotlin's Stream model actually reads, silently dropping plugin
subtitles. Also add pluginScraperSettingsUpdated and make repository
re-fetch (on manual refresh or app-start replay) carry over a scraper's
existing enabled/settings instead of resetting to manifest defaults.
This commit is contained in:
KhooLy 2026-07-20 02:40:12 +03:00
parent 80657b75bf
commit f9000b4dbd
4 changed files with 132 additions and 0 deletions

View file

@ -320,6 +320,11 @@ pub(super) enum AppAction {
PluginRepositoryRemoveRequested { manifest_url: String },
#[serde(rename = "pluginScraperToggled")]
PluginScraperToggled { scraper_id: String, enabled: bool },
#[serde(rename = "pluginScraperSettingsUpdated")]
PluginScraperSettingsUpdated {
scraper_id: String,
settings: Value,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]

View file

@ -550,6 +550,10 @@ impl HeadlessEngine {
scraper_id,
enabled,
} => plugins::dispatch_toggle_scraper(self, scraper_id, enabled),
AppAction::PluginScraperSettingsUpdated {
scraper_id,
settings,
} => plugins::dispatch_update_scraper_settings(self, scraper_id, settings),
}
}
@ -1770,6 +1774,78 @@ mod tests {
assert!(destroy_headless_engine(handle));
}
#[test]
fn plugin_repository_refetch_preserves_disabled_state_and_settings() {
let handle = create_headless_engine("{}");
let manifest_value = json!({
"manifestUrl": "https://example.com/manifest.json",
"manifest": {
"name": "Phisher's Repo",
"version": "1.0.0",
"scrapers": [
{"id": "MoviesDrive", "name": "MoviesDrive", "version": "1.1.1", "filename": "src/providers/moviesdrive.js"}
]
}
});
let requested: Value = serde_json::from_str(
&headless_engine_dispatch_json(
handle,
r#"{"type":"pluginRepositoryAddRequested","manifestUrl":"https://example.com/manifest.json"}"#,
)
.unwrap(),
)
.unwrap();
headless_engine_complete_effect_json(
handle,
&json!({
"effectId": requested["effects"][0]["id"].as_str().unwrap(),
"status": "ok",
"value": manifest_value
})
.to_string(),
)
.unwrap();
headless_engine_dispatch_json(
handle,
r#"{"type":"pluginScraperToggled","scraperId":"MoviesDrive","enabled":false}"#,
)
.unwrap();
headless_engine_dispatch_json(
handle,
r#"{"type":"pluginScraperSettingsUpdated","scraperId":"MoviesDrive","settings":{"quality":"1080p"}}"#,
)
.unwrap();
let refetch_requested: Value = serde_json::from_str(
&headless_engine_dispatch_json(
handle,
r#"{"type":"pluginRepositoryAddRequested","manifestUrl":"https://example.com/manifest.json"}"#,
)
.unwrap(),
)
.unwrap();
let refetched: Value = serde_json::from_str(
&headless_engine_complete_effect_json(
handle,
&json!({
"effectId": refetch_requested["effects"][0]["id"].as_str().unwrap(),
"status": "ok",
"value": manifest_value
})
.to_string(),
)
.unwrap(),
)
.unwrap();
let scraper = &refetched["state"]["plugins"]["scrapers"][0];
assert_eq!(scraper["enabled"], false);
assert_eq!(scraper["settings"]["quality"], "1080p");
assert!(destroy_headless_engine(handle));
}
#[test]
fn calendar_completion_plans_os_side_effects_in_core() {
let handle = create_headless_engine("{}");

View file

@ -77,6 +77,22 @@ pub(super) fn dispatch_toggle_scraper(
vec![]
}
pub(super) fn dispatch_update_scraper_settings(
engine: &mut HeadlessEngine,
scraper_id: String,
settings: Value,
) -> Vec<EffectEnvelope> {
if let Some(items) = engine.state.plugins.scrapers.as_array_mut() {
if let Some(scraper) = items
.iter_mut()
.find(|scraper| scraper["id"].as_str() == Some(scraper_id.as_str()))
{
scraper["settings"] = settings;
}
}
vec![]
}
pub(super) fn complete(
engine: &mut HeadlessEngine,
generation: u64,
@ -114,11 +130,43 @@ pub(super) fn complete(
repository_entry,
);
let previous_by_id: std::collections::HashMap<String, Value> = engine
.state
.plugins
.scrapers
.as_array()
.into_iter()
.flatten()
.filter(|scraper| scraper["repositoryUrl"].as_str() == Some(manifest_url))
.filter_map(|scraper| {
scraper["id"]
.as_str()
.map(|id| (id.to_string(), scraper.clone()))
})
.collect();
if let Some(items) = engine.state.plugins.scrapers.as_array_mut() {
items.retain(|scraper| scraper["repositoryUrl"].as_str() != Some(manifest_url));
}
for mut scraper in scrapers {
scraper["repositoryUrl"] = Value::String(manifest_url.to_string());
let manifest_enabled = scraper["enabled"].as_bool().unwrap_or(true);
if let Some(previous) = scraper["id"]
.as_str()
.and_then(|id| previous_by_id.get(id))
{
if manifest_enabled {
if let Some(previous_enabled) = previous["enabled"].as_bool() {
scraper["enabled"] = Value::Bool(previous_enabled);
}
}
if let Some(previous_settings) = previous.get("settings") {
scraper["settings"] = previous_settings.clone();
}
}
if scraper.get("settings").is_none() {
scraper["settings"] = json!({});
}
if let Some(items) = engine.state.plugins.scrapers.as_array_mut() {
items.push(scraper);
}

View file

@ -166,6 +166,7 @@ fn plugin_result_to_stream(result: PluginStreamResult) -> Stream {
title: Some(result.title),
info_hash: result.info_hash,
headers: result.headers,
subtitles: subtitle_tracks.clone(),
subtitle_tracks,
extra,
..Default::default()
@ -257,6 +258,8 @@ mod tests {
let subs = stream.subtitle_tracks.as_ref().unwrap();
assert_eq!(subs[0].lang, "en");
assert_eq!(subs[0].url, "https://example.com/sub.srt");
let subs_alias = stream.subtitles.as_ref().unwrap();
assert_eq!(subs_alias[0].url, "https://example.com/sub.srt");
}
#[test]