refactor: delegate Miruro playback resolution to Terebi
This commit is contained in:
+33
-56
@@ -1,75 +1,52 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn source_projection_rejects_unsafe_or_unsupported_playback_contracts() {
|
||||
let sources = serde_json::json!({
|
||||
"episodeNumber": 1,
|
||||
"config": { "sun": { "player": "native", "proxy": false } },
|
||||
"tracks": [{ "track": "ssub", "providers": [{
|
||||
"provider": "sun",
|
||||
"servers": [{
|
||||
"headers": { "Origin": "https://ultracloud.cc", "Referer": "https://ultracloud.cc/" },
|
||||
"streams": [{ "url": "https://vault.keeply.top/master.m3u8?token=secret", "format": "hls", "embed": null }]
|
||||
}],
|
||||
"subtitles": [{
|
||||
"file": "https://vault.keeply.top/english.jpeg", "format": "vtt",
|
||||
"language": "en", "label": "English", "default": true
|
||||
}]
|
||||
}] }]
|
||||
});
|
||||
let identity = Identity::new("catalog".into(), 1).unwrap();
|
||||
let source = SourceDocument::from_sources(&sources, identity.clone(), "Episode 1").unwrap();
|
||||
fn watch_url_preserves_stable_identity_and_validates_mutable_fields() {
|
||||
let identity = Identity::from_key("miruro:OERe_a0YS4h-DlsBDOeXwX7HOO6316tw:1").unwrap();
|
||||
assert_eq!(identity.key(), "miruro:OERe_a0YS4h-DlsBDOeXwX7HOO6316tw:1");
|
||||
assert_eq!(
|
||||
identity.watch_url().unwrap(),
|
||||
"https://barelystarted.miruro.tv/watch/OERe_a0YS4h-DlsBDOeXwX7HOO6316tw/episode?ep=1"
|
||||
);
|
||||
assert_eq!(Identity::from_key(&identity.key()).unwrap(), identity);
|
||||
assert_ne!(
|
||||
Identity::new("catalog".into(), 2).unwrap().key(),
|
||||
source.identity.key()
|
||||
Identity::new(identity.media_id.clone(), 2).unwrap().key(),
|
||||
identity.key()
|
||||
);
|
||||
for key in [
|
||||
"miruro:catalog:01",
|
||||
"miruro:catalog:0",
|
||||
"miruro:catalog:2147483648",
|
||||
"miruro:catalog/:1",
|
||||
"miruro:catalog:+1",
|
||||
"miruro::1",
|
||||
] {
|
||||
assert!(Identity::from_key(key).is_err());
|
||||
}
|
||||
for (pointer, value) in [
|
||||
("/episodeNumber", serde_json::json!(49)),
|
||||
("/config/sun/proxy", serde_json::json!({"segments": true})),
|
||||
(
|
||||
"/tracks/0/providers/0/servers/0/streams/0/url",
|
||||
serde_json::json!("file:///secret"),
|
||||
),
|
||||
(
|
||||
"/tracks/0/providers/0/servers/0/streams/0/embed",
|
||||
serde_json::json!({"url": "https://embed.example"}),
|
||||
),
|
||||
(
|
||||
"/tracks/0/providers/0/servers/0/headers/origin",
|
||||
serde_json::json!("secret"),
|
||||
),
|
||||
(
|
||||
"/tracks/0/providers/0/servers/0/headers/Referer",
|
||||
serde_json::json!("secret\r\nX-Injected: true"),
|
||||
),
|
||||
for invalid in [
|
||||
Identity {
|
||||
provider: "youtube".into(),
|
||||
..identity.clone()
|
||||
},
|
||||
Identity {
|
||||
media_id: "catalog/".into(),
|
||||
..identity.clone()
|
||||
},
|
||||
Identity {
|
||||
media_id: "a".repeat(513),
|
||||
..identity.clone()
|
||||
},
|
||||
Identity {
|
||||
episode: 0,
|
||||
..identity.clone()
|
||||
},
|
||||
Identity {
|
||||
episode: i32::MAX as u32 + 1,
|
||||
..identity.clone()
|
||||
},
|
||||
] {
|
||||
let mut invalid_sources = sources.clone();
|
||||
if let Some(field) = invalid_sources.pointer_mut(pointer) {
|
||||
*field = value;
|
||||
} else {
|
||||
invalid_sources["tracks"][0]["providers"][0]["servers"][0]["headers"]["origin"] = value;
|
||||
}
|
||||
let error = SourceDocument::from_sources(&invalid_sources, identity.clone(), "Episode 1")
|
||||
.unwrap_err();
|
||||
assert!(!error.to_string().contains("secret"));
|
||||
assert!(invalid.watch_url().is_err());
|
||||
}
|
||||
let mut invalid_source = source;
|
||||
invalid_source
|
||||
.subtitles
|
||||
.push(invalid_source.subtitles[0].clone());
|
||||
assert!(invalid_source.validate().is_err());
|
||||
invalid_source.subtitles[1].default = false;
|
||||
invalid_source.subtitles[1].format = "srt".into();
|
||||
assert!(invalid_source.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+21
-128
@@ -1,12 +1,11 @@
|
||||
use super::*;
|
||||
use ratatui::crossterm::event::KeyModifiers;
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
fs,
|
||||
io::Write,
|
||||
sync::atomic::{AtomicU64, Ordering},
|
||||
};
|
||||
use tubular::miruro::{Detail, SourceDocument};
|
||||
use tubular::miruro::Detail;
|
||||
|
||||
struct Directory(PathBuf);
|
||||
impl Directory {
|
||||
@@ -53,16 +52,6 @@ fn series(media: &Media) -> miruro_browser::Series {
|
||||
serde_json::json!({"episode_number":7, "absolute_number":53, "title":null, "duration_seconds":null}),
|
||||
] }).unwrap()
|
||||
}
|
||||
fn source(video: &Video, token: &str) -> SourceDocument {
|
||||
SourceDocument {
|
||||
version: 1,
|
||||
url: format!("https://vault.keeply.top/master.m3u8?token={token}"),
|
||||
headers: BTreeMap::new(),
|
||||
identity: Identity::from_key(&video.id).unwrap(),
|
||||
title: Some(video.title.clone()),
|
||||
subtitles: Vec::new(),
|
||||
}
|
||||
}
|
||||
fn catalog_results(browser: &mut Browser) {
|
||||
browser.accept(
|
||||
browser.generation,
|
||||
@@ -77,19 +66,6 @@ fn enter_series(browser: &mut Browser) {
|
||||
let result = series(media);
|
||||
browser.accept(browser.generation, Ok(Response::Series(result)));
|
||||
}
|
||||
fn sources(directory: &Directory) -> Vec<Value> {
|
||||
let path = directory.0.join("playback-sessions");
|
||||
if !path.exists() {
|
||||
return Vec::new();
|
||||
}
|
||||
fs::read_dir(path)
|
||||
.unwrap()
|
||||
.map(|entry| {
|
||||
serde_json::from_slice(&fs::read(entry.unwrap().path().join("source.json")).unwrap())
|
||||
.unwrap()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_popup_scrolls_and_closes_without_moving_the_episode_selection() {
|
||||
@@ -188,7 +164,7 @@ fn catalog_series_back_forward_keep_native_query_selection_and_actual_episodes()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn latest_intent_and_navigation_reject_stale_catalog_series_and_sources() {
|
||||
fn latest_intent_and_navigation_reject_stale_catalog_and_series() {
|
||||
let directory = Directory::new();
|
||||
let mut browser = directory.browser(Some("old"));
|
||||
let old = browser.generation;
|
||||
@@ -214,30 +190,10 @@ fn latest_intent_and_navigation_reject_stale_catalog_series_and_sources() {
|
||||
};
|
||||
let data = series(media);
|
||||
browser.accept(browser.generation, Ok(Response::Series(data)));
|
||||
let video = browser.screen().selected_video().unwrap();
|
||||
browser.open();
|
||||
let stale = browser.generation;
|
||||
browser.history(false);
|
||||
browser.accept(
|
||||
stale,
|
||||
Ok(Response::Source(
|
||||
video.clone(),
|
||||
Box::new(source(&video, "stale")),
|
||||
)),
|
||||
assert_eq!(
|
||||
browser.screen().selected_video().unwrap().id,
|
||||
"miruro:first:3"
|
||||
);
|
||||
assert!(sources(&directory).is_empty());
|
||||
browser.history(true);
|
||||
browser.open();
|
||||
let stale = browser.generation;
|
||||
browser.refresh();
|
||||
browser.accept(
|
||||
stale,
|
||||
Ok(Response::Source(
|
||||
video.clone(),
|
||||
Box::new(source(&video, "refreshed-away")),
|
||||
)),
|
||||
);
|
||||
assert!(sources(&directory).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -274,12 +230,9 @@ fn manual_watch_status_refreshes_filters_and_preserves_other_providers() {
|
||||
assert!(!browser.state.is_watched(&episode.id));
|
||||
browser.preview = None;
|
||||
browser.collection(CollectionKind::Watching);
|
||||
browser.open();
|
||||
assert!(browser.pending.is_some());
|
||||
browser.key(KeyEvent::new(KeyCode::Char('M'), KeyModifiers::NONE));
|
||||
assert!(browser.state.is_watched(&episode.id));
|
||||
assert_eq!(browser.screen().len(), 0);
|
||||
assert!(browser.pending.is_none());
|
||||
browser.collection(CollectionKind::WatchLater);
|
||||
browser.key(KeyEvent::new(KeyCode::Char('f'), KeyModifiers::NONE));
|
||||
assert_eq!(browser.screen().len(), 0);
|
||||
@@ -336,8 +289,6 @@ fn offline_collections_and_filtered_bulk_removal_never_touch_youtube() {
|
||||
}
|
||||
browser.collection(CollectionKind::Saved);
|
||||
assert_eq!(browser.screen().len(), 1);
|
||||
browser.open();
|
||||
assert!(matches!(&browser.pending, Some(Request::Source(video)) if video.id == episode.id));
|
||||
browser.remove(true);
|
||||
assert_eq!(
|
||||
browser.removal.as_ref().unwrap().1.as_slice(),
|
||||
@@ -351,10 +302,8 @@ fn offline_collections_and_filtered_bulk_removal_never_touch_youtube() {
|
||||
.state
|
||||
.contains(CollectionKind::WatchLater, &episode.id)
|
||||
);
|
||||
let (_, report) = browser
|
||||
.state
|
||||
.miruro_session(&episode, &source(&episode, "progress"))
|
||||
.unwrap();
|
||||
let report = browser.state.progress_file(&episode).unwrap();
|
||||
crate::video_state::register_identity(&report, &episode.id).unwrap();
|
||||
let mut output = fs::OpenOptions::new();
|
||||
output.write(true).create_new(true);
|
||||
#[cfg(unix)]
|
||||
@@ -391,89 +340,33 @@ fn offline_collections_and_filtered_bulk_removal_never_touch_youtube() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selected_episode_and_offline_reopen_write_fresh_validated_source_sessions() {
|
||||
fn launcher_failure_does_not_mark_series_or_offline_episodes_as_started() {
|
||||
let directory = Directory::new();
|
||||
let mut browser = directory.browser(Some("anime"));
|
||||
catalog_results(&mut browser);
|
||||
enter_series(&mut browser);
|
||||
let first = browser.screen().selected_video().unwrap();
|
||||
browser.open();
|
||||
let stale = browser.generation;
|
||||
browser.select(Action::VideoNext);
|
||||
browser.accept(
|
||||
stale,
|
||||
Ok(Response::Source(
|
||||
first.clone(),
|
||||
Box::new(source(&first, "not-selected")),
|
||||
)),
|
||||
);
|
||||
assert!(sources(&directory).is_empty());
|
||||
let selected = browser.screen().selected_video().unwrap();
|
||||
assert_eq!(Identity::from_key(&selected.id).unwrap().episode, 7);
|
||||
browser.save(CollectionKind::Saved);
|
||||
browser.collection(CollectionKind::Saved);
|
||||
browser.open();
|
||||
browser.pending = None;
|
||||
browser.accept(
|
||||
browser.generation,
|
||||
Ok(Response::Source(
|
||||
selected.clone(),
|
||||
Box::new(source(&first, "wrong-identity")),
|
||||
)),
|
||||
);
|
||||
assert!(sources(&directory).is_empty());
|
||||
browser.open();
|
||||
browser.pending = None;
|
||||
let mut invalid = source(&selected, "private-token");
|
||||
invalid
|
||||
.headers
|
||||
.insert("X-Invalid".into(), "private-header\nvalue".into());
|
||||
browser.accept(
|
||||
browser.generation,
|
||||
Ok(Response::Source(selected.clone(), Box::new(invalid))),
|
||||
);
|
||||
assert!(sources(&directory).is_empty());
|
||||
assert!(!browser.status.contains("private-token"));
|
||||
assert!(!browser.status.contains("private-header"));
|
||||
for token in ["fresh-one", "fresh-two"] {
|
||||
browser.open();
|
||||
assert!(
|
||||
matches!(&browser.pending, Some(Request::Source(video)) if video.id == selected.id && video.title == selected.title)
|
||||
);
|
||||
browser.pending = None; // Deliver the real source response without a network request.
|
||||
browser.accept(
|
||||
browser.generation,
|
||||
Ok(Response::Source(
|
||||
selected.clone(),
|
||||
Box::new(source(&selected, token)),
|
||||
)),
|
||||
);
|
||||
}
|
||||
let written = sources(&directory);
|
||||
assert_eq!(written.len(), 2);
|
||||
assert!(
|
||||
written
|
||||
.iter()
|
||||
.all(|source| source["identity"]["episode"] == 7
|
||||
&& source["identity"]["media_id"] == "first")
|
||||
);
|
||||
assert!(
|
||||
written
|
||||
.iter()
|
||||
.any(|source| source["url"].as_str().unwrap().ends_with("fresh-one"))
|
||||
);
|
||||
assert!(
|
||||
written
|
||||
.iter()
|
||||
.any(|source| source["url"].as_str().unwrap().ends_with("fresh-two"))
|
||||
);
|
||||
assert!(!browser.state.is_watched(&selected.id)); // Spawn failure/CLI completion is not progress.
|
||||
assert!(browser.screen().error.is_some());
|
||||
assert!(!browser.state.is_watched(&selected.id));
|
||||
assert!(
|
||||
!browser
|
||||
.state
|
||||
.contains(CollectionKind::Watching, &selected.id)
|
||||
);
|
||||
assert!(browser.screen().error.is_some()); // Native launch errors remain visible.
|
||||
browser.save(CollectionKind::Saved);
|
||||
browser.collection(CollectionKind::Saved);
|
||||
browser.open();
|
||||
assert!(browser.screen().error.is_some());
|
||||
assert!(!browser.state.is_watched(&selected.id));
|
||||
assert!(
|
||||
!browser
|
||||
.state
|
||||
.contains(CollectionKind::Watching, &selected.id)
|
||||
);
|
||||
assert!(browser.state.contains(CollectionKind::Saved, &selected.id));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+17
-56
@@ -278,17 +278,6 @@ fn miruro_library_does_not_overwrite_external_creations_or_edits() {
|
||||
assert_eq!(fs::read(&path).unwrap(), changed);
|
||||
}
|
||||
|
||||
fn miruro_source(video: &Video) -> SourceDocument {
|
||||
SourceDocument {
|
||||
version: 1,
|
||||
url: "https://cdn.example/video.m3u8?token=private-token".into(),
|
||||
headers: [("Referer".into(), "https://player.example/".into())].into(),
|
||||
identity: Identity::from_key(&video.id).unwrap(),
|
||||
title: Some(video.title.clone()),
|
||||
subtitles: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn miruro_record(
|
||||
video: &Video,
|
||||
event: &str,
|
||||
@@ -1092,17 +1081,16 @@ fn watching_and_session_snapshots_reject_oversized_files() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn miruro_sessions_recover_collections_and_watching_without_persisting_sources_in_state() {
|
||||
fn miruro_sessions_recover_collections_and_watching_without_source_files() {
|
||||
let directory = TestDirectory::new();
|
||||
let video = miruro_video(1);
|
||||
let mut state = State::open(&directory.config()).unwrap();
|
||||
for kind in [CollectionKind::WatchLater, CollectionKind::Saved] {
|
||||
state.save(kind, &video).unwrap();
|
||||
}
|
||||
let source = miruro_source(&video);
|
||||
let (source_file, report) = state.miruro_session(&video, &source).unwrap();
|
||||
let source_bytes = fs::read(&source_file).unwrap();
|
||||
assert_eq!(source_bytes, serde_json::to_vec(&source).unwrap());
|
||||
let report = state.progress_file(&video).unwrap();
|
||||
register_identity(&report, &video.id).unwrap();
|
||||
assert!(!report.with_file_name("source.json").exists());
|
||||
assert!(!report.exists());
|
||||
assert_eq!(
|
||||
fs::read_to_string(report.with_file_name("video-id")).unwrap(),
|
||||
@@ -1112,7 +1100,6 @@ fn miruro_sessions_recover_collections_and_watching_without_persisting_sources_i
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
for path in [
|
||||
&source_file,
|
||||
&report.with_file_name("video-id"),
|
||||
&report.with_file_name("video.toml"),
|
||||
] {
|
||||
@@ -1172,18 +1159,6 @@ fn miruro_sessions_recover_collections_and_watching_without_persisting_sources_i
|
||||
assert_eq!(stored.timestamp, video.timestamp);
|
||||
assert_eq!(stored.publication_date, video.publication_date);
|
||||
}
|
||||
for path in [
|
||||
directory.0.join("watch-later.toml"),
|
||||
directory.0.join("saved-videos.toml"),
|
||||
directory.0.join("currently-watching.toml"),
|
||||
directory.0.join("watch-state.toml"),
|
||||
report.with_file_name("video.toml"),
|
||||
] {
|
||||
let contents = fs::read_to_string(path).unwrap();
|
||||
assert!(!contents.contains("private-token"));
|
||||
assert!(!contents.contains("cdn.example"));
|
||||
}
|
||||
assert_eq!(fs::read(source_file).unwrap(), source_bytes);
|
||||
assert!(report.exists());
|
||||
}
|
||||
|
||||
@@ -1193,9 +1168,8 @@ fn miruro_v2_requires_matching_episode_eligible_known_duration_and_threshold() {
|
||||
let directory = TestDirectory::new();
|
||||
let mut state = State::open(&directory.config()).unwrap();
|
||||
let video = miruro_video(1);
|
||||
let (_, report) = state
|
||||
.miruro_session(&video, &miruro_source(&video))
|
||||
.unwrap();
|
||||
let report = state.progress_file(&video).unwrap();
|
||||
register_identity(&report, &video.id).unwrap();
|
||||
append(
|
||||
&report,
|
||||
miruro_record(&miruro_video(2), event, true, 100.0, Some(100.0)).as_bytes(),
|
||||
@@ -1232,9 +1206,8 @@ fn miruro_v2_rejects_invalid_wire_schemas_numbers_and_namespaces() {
|
||||
let directory = TestDirectory::new();
|
||||
let mut state = State::open(&directory.config()).unwrap();
|
||||
let video = miruro_video(1);
|
||||
let (_, report) = state
|
||||
.miruro_session(&video, &miruro_source(&video))
|
||||
.unwrap();
|
||||
let report = state.progress_file(&video).unwrap();
|
||||
register_identity(&report, &video.id).unwrap();
|
||||
let valid = miruro_record(&video, "progress", true, 100.0, Some(100.0));
|
||||
for line in [
|
||||
valid.replace("\"version\":2", "\"version\":1"),
|
||||
@@ -1264,9 +1237,8 @@ fn miruro_partial_reports_and_bad_siblings_recover_after_restart() {
|
||||
let directory = TestDirectory::new();
|
||||
let mut state = State::open(&directory.config()).unwrap();
|
||||
let video = miruro_video(1);
|
||||
let (source, report) = state
|
||||
.miruro_session(&video, &miruro_source(&video))
|
||||
.unwrap();
|
||||
let report = state.progress_file(&video).unwrap();
|
||||
register_identity(&report, &video.id).unwrap();
|
||||
let completion = miruro_record(&video, "closed", true, 95.0, Some(100.0));
|
||||
append(&report, &completion.as_bytes()[..completion.len() - 1]);
|
||||
assert!(!state.poll_progress().unwrap());
|
||||
@@ -1274,9 +1246,8 @@ fn miruro_partial_reports_and_bad_siblings_recover_after_restart() {
|
||||
let mut state = State::open(&directory.config()).unwrap();
|
||||
assert!(!state.poll_progress().unwrap());
|
||||
let other = miruro_video(2);
|
||||
let (_, bad) = state
|
||||
.miruro_session(&other, &miruro_source(&other))
|
||||
.unwrap();
|
||||
let bad = state.progress_file(&other).unwrap();
|
||||
register_identity(&bad, &other.id).unwrap();
|
||||
append(&bad, b"not json\n");
|
||||
append(&report, b"\n");
|
||||
assert!(state.poll_progress().is_err());
|
||||
@@ -1285,7 +1256,6 @@ fn miruro_partial_reports_and_bad_siblings_recover_after_restart() {
|
||||
assert!(state.poll_progress().unwrap());
|
||||
assert!(!state.poll_progress().unwrap());
|
||||
drop(state);
|
||||
assert!(source.exists());
|
||||
assert!(report.exists());
|
||||
let state = State::open(&directory.config()).unwrap();
|
||||
assert!(state.is_watched(&video.id));
|
||||
@@ -1339,23 +1309,15 @@ fn miruro_state_rejects_catalog_rows_bad_titles_and_youtube_metadata() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn miruro_source_sessions_validate_binding_size_and_maximum_identity_marker() {
|
||||
fn miruro_sessions_recover_the_maximum_identity_marker() {
|
||||
let directory = TestDirectory::new();
|
||||
let mut state = State::open(&directory.config()).unwrap();
|
||||
let mut video = miruro_video(i32::MAX as u32);
|
||||
video.id = Identity::new("a".repeat(512), i32::MAX as u32)
|
||||
.unwrap()
|
||||
.key();
|
||||
let mut source = miruro_source(&video);
|
||||
source.identity.episode -= 1;
|
||||
assert!(state.miruro_session(&video, &source).is_err());
|
||||
assert!(state.streams.is_empty());
|
||||
source.identity = Identity::from_key(&video.id).unwrap();
|
||||
source.title = Some("x".repeat(MAX_SOURCE_BYTES));
|
||||
assert!(state.miruro_session(&video, &source).is_err());
|
||||
assert!(state.streams.is_empty());
|
||||
source.title = None;
|
||||
let (_, report) = state.miruro_session(&video, &source).unwrap();
|
||||
let report = state.progress_file(&video).unwrap();
|
||||
register_identity(&report, &video.id).unwrap();
|
||||
append(
|
||||
&report,
|
||||
miruro_record(&video, "ended", true, 95.0, Some(100.0)).as_bytes(),
|
||||
@@ -1375,9 +1337,8 @@ fn miruro_recovery_rejects_missing_snapshots_and_overlong_identity_markers() {
|
||||
let directory = TestDirectory::new();
|
||||
let mut state = State::open(&directory.config()).unwrap();
|
||||
let video = miruro_video(1);
|
||||
let (_, report) = state
|
||||
.miruro_session(&video, &miruro_source(&video))
|
||||
.unwrap();
|
||||
let report = state.progress_file(&video).unwrap();
|
||||
register_identity(&report, &video.id).unwrap();
|
||||
if missing_snapshot {
|
||||
fs::remove_file(report.with_file_name("video.toml")).unwrap();
|
||||
} else {
|
||||
|
||||
+13
-2
@@ -534,8 +534,19 @@ mod subprocesses {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn player_launch_rejects_unsafe_video_ids_before_spawning() {
|
||||
for id in ["", "--help", "jNQXAC9IVRw&list=other", "日本語"] {
|
||||
fn player_launch_rejects_invalid_provider_ids_before_registration_or_spawning() {
|
||||
for id in [
|
||||
"",
|
||||
"--help",
|
||||
"jNQXAC9IVRw&list=other",
|
||||
"日本語",
|
||||
"miruro:opaque_Show-1:0",
|
||||
"miruro:opaque_Show-1:01",
|
||||
"miruro:opaque_Show-1:2147483648",
|
||||
"miruro:../other:1",
|
||||
"miruro:opaque_Show-1:1&ep=2",
|
||||
"miruro-series:opaque_Show-1",
|
||||
] {
|
||||
assert!(matches!(
|
||||
PlayerLaunch::start(id, OsStr::new("terebi"), Path::new("progress.jsonl")),
|
||||
Err(error) if error.kind() == io::ErrorKind::InvalidInput
|
||||
|
||||
Reference in New Issue
Block a user