mirror of
https://github.com/FluxaMedia/fluxa-core.git
synced 2026-08-18 21:18:16 +00:00
centralize desktop domain policy in core
This commit is contained in:
parent
25b3ca73a0
commit
d6c3bd72be
4 changed files with 260 additions and 1 deletions
|
|
@ -1,6 +1,9 @@
|
|||
use crate::stream_policy;
|
||||
#[cfg(any(feature = "full-api", not(feature = "streaming-shared")))]
|
||||
use crate::{cast_protocol, headless_engine, offline_download, player_policy, subtitle_sync};
|
||||
use crate::{
|
||||
cast_protocol, desktop_playback, headless_engine, library_persistence, offline_download,
|
||||
player_policy, subtitle_sync,
|
||||
};
|
||||
|
||||
pub struct FluxaCore;
|
||||
|
||||
|
|
@ -60,6 +63,45 @@ impl FluxaCore {
|
|||
})
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "full-api", not(feature = "streaming-shared")))]
|
||||
pub fn should_play_next_episode(has_next_episode: bool, auto_play: bool) -> bool {
|
||||
guard(false, || {
|
||||
desktop_playback::should_play_next_episode(has_next_episode, auto_play)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "full-api", not(feature = "streaming-shared")))]
|
||||
pub fn chapter_skip_segments_json(chapters_json: &str) -> String {
|
||||
guard("[]".to_string(), || {
|
||||
desktop_playback::chapter_skip_segments_json(chapters_json)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "full-api", not(feature = "streaming-shared")))]
|
||||
pub fn library_progress_entries_json(document_json: &str) -> String {
|
||||
guard("[]".to_string(), || library_persistence::progress_entries_json(document_json))
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "full-api", not(feature = "streaming-shared")))]
|
||||
pub fn library_items_json(document_json: &str) -> String {
|
||||
guard("[]".to_string(), || library_persistence::library_items_json(document_json))
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "full-api", not(feature = "streaming-shared")))]
|
||||
pub fn library_watched_video_ids_json(document_json: &str) -> String {
|
||||
guard("[]".to_string(), || library_persistence::watched_video_ids_json(document_json))
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "full-api", not(feature = "streaming-shared")))]
|
||||
pub fn library_last_watched_entries_json(document_json: &str) -> String {
|
||||
guard("[]".to_string(), || library_persistence::last_watched_entries_json(document_json))
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "full-api", not(feature = "streaming-shared")))]
|
||||
pub fn library_continue_watching_entries_json(document_json: &str) -> String {
|
||||
guard("[]".to_string(), || library_persistence::continue_watching_entries_json(document_json))
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "full-api", not(feature = "streaming-shared")))]
|
||||
pub fn subtitle_sync_estimate_json(request_json: &str) -> Option<String> {
|
||||
guard(None, || {
|
||||
|
|
|
|||
90
src/desktop_playback.rs
Normal file
90
src/desktop_playback.rs
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Chapter {
|
||||
title: String,
|
||||
start_time: i64,
|
||||
}
|
||||
|
||||
pub(crate) fn should_play_next_episode(has_next_episode: bool, auto_play: bool) -> bool {
|
||||
has_next_episode && auto_play
|
||||
}
|
||||
|
||||
pub(crate) fn chapter_skip_segments_json(chapters_json: &str) -> String {
|
||||
let chapters: Vec<Chapter> = match serde_json::from_str(chapters_json) {
|
||||
Ok(chapters) => chapters,
|
||||
Err(_) => return "[]".to_string(),
|
||||
};
|
||||
let segments = chapters
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, chapter)| {
|
||||
let segment_type = classify_chapter(&chapter.title)?;
|
||||
let end_time = chapters.get(index + 1)?.start_time;
|
||||
(end_time > chapter.start_time).then(|| {
|
||||
serde_json::json!({
|
||||
"type": segment_type,
|
||||
"startTime": chapter.start_time,
|
||||
"endTime": end_time,
|
||||
})
|
||||
})
|
||||
})
|
||||
.collect::<Vec<Value>>();
|
||||
serde_json::to_string(&segments).unwrap_or_else(|_| "[]".to_string())
|
||||
}
|
||||
|
||||
fn classify_chapter(title: &str) -> Option<&'static str> {
|
||||
let normalized = title.trim().to_lowercase();
|
||||
match normalized.as_str() {
|
||||
"op" | "opening" | "intro" | "introduction" | "op sequence" | "mixed-intro"
|
||||
| "opening sequence" | "opening theme" => return Some("intro"),
|
||||
"ed" | "ending" | "outro" | "credits" | "end credits" | "closing"
|
||||
| "ending theme" | "ending sequence" => return Some("outro"),
|
||||
"recap" | "previously" | "previously on" | "cold open" => return Some("recap"),
|
||||
_ => {}
|
||||
}
|
||||
if normalized.starts_with("op ")
|
||||
|| normalized.starts_with("opening ")
|
||||
|| normalized.contains("intro")
|
||||
|| normalized.contains("opening")
|
||||
{
|
||||
return Some("intro");
|
||||
}
|
||||
if normalized.starts_with("ed ")
|
||||
|| normalized.starts_with("ending ")
|
||||
|| normalized.contains("ending")
|
||||
|| normalized.contains("outro")
|
||||
|| normalized.contains("credits")
|
||||
{
|
||||
return Some("outro");
|
||||
}
|
||||
if normalized.contains("recap") || normalized.contains("previously") {
|
||||
return Some("recap");
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn next_episode_requires_both_a_successor_and_autoplay() {
|
||||
assert!(should_play_next_episode(true, true));
|
||||
assert!(!should_play_next_episode(true, false));
|
||||
assert!(!should_play_next_episode(false, true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derives_skip_segments_from_named_chapters() {
|
||||
let result = chapter_skip_segments_json(
|
||||
r#"[{"title":"Opening Theme","startTime":0},{"title":"Episode","startTime":90000},{"title":"Credits","startTime":120000},{"title":"End","startTime":140000}]"#,
|
||||
);
|
||||
let value: Value = serde_json::from_str(&result).unwrap();
|
||||
assert_eq!(value[0]["type"], "intro");
|
||||
assert_eq!(value[0]["endTime"], 90000);
|
||||
assert_eq!(value[1]["type"], "outro");
|
||||
}
|
||||
}
|
||||
|
|
@ -45,6 +45,8 @@ mod core_error;
|
|||
mod data_policy;
|
||||
#[cfg(any(feature = "full-api", not(feature = "streaming-shared")))]
|
||||
mod discovery_plan;
|
||||
#[cfg(any(feature = "full-api", not(feature = "streaming-shared")))]
|
||||
mod desktop_playback;
|
||||
#[cfg(feature = "native")]
|
||||
mod dolby_vision_rpu;
|
||||
#[cfg(any(feature = "full-api", not(feature = "streaming-shared")))]
|
||||
|
|
@ -59,6 +61,8 @@ mod home_ranking;
|
|||
mod intro_segments;
|
||||
#[cfg(any(feature = "full-api", not(feature = "streaming-shared")))]
|
||||
mod library_state;
|
||||
#[cfg(any(feature = "full-api", not(feature = "streaming-shared")))]
|
||||
mod library_persistence;
|
||||
pub mod log_sink;
|
||||
#[cfg(any(feature = "full-api", not(feature = "streaming-shared")))]
|
||||
mod nuvio_sync;
|
||||
|
|
|
|||
123
src/library_persistence.rs
Normal file
123
src/library_persistence.rs
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct LibraryItem<'a> {
|
||||
media_id: &'a str,
|
||||
status: &'static str,
|
||||
value: &'a Value,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct KeyedValue<'a> {
|
||||
key: &'a str,
|
||||
value: &'a Value,
|
||||
}
|
||||
|
||||
pub(crate) fn progress_entries_json(document_json: &str) -> String {
|
||||
object_entries(document_json, "progress")
|
||||
}
|
||||
|
||||
pub(crate) fn library_items_json(document_json: &str) -> String {
|
||||
let document: Value = match serde_json::from_str(document_json) {
|
||||
Ok(document) => document,
|
||||
Err(_) => return "[]".to_string(),
|
||||
};
|
||||
let mut entries = Vec::new();
|
||||
for status in ["watchlist", "completed", "dropped"] {
|
||||
for item in document
|
||||
.get(status)
|
||||
.and_then(Value::as_array)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
{
|
||||
if let Some(media_id) = item.get("id").and_then(Value::as_str) {
|
||||
entries.push(LibraryItem {
|
||||
media_id,
|
||||
status,
|
||||
value: item,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
serde_json::to_string(&entries).unwrap_or_else(|_| "[]".to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn watched_video_ids_json(document_json: &str) -> String {
|
||||
let document: Value = match serde_json::from_str(document_json) {
|
||||
Ok(document) => document,
|
||||
Err(_) => return "[]".to_string(),
|
||||
};
|
||||
let ids = document
|
||||
.get("watched")
|
||||
.and_then(Value::as_object)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(|(video_id, watched)| (watched.as_bool() == Some(true)).then_some(video_id))
|
||||
.collect::<Vec<_>>();
|
||||
serde_json::to_string(&ids).unwrap_or_else(|_| "[]".to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn last_watched_entries_json(document_json: &str) -> String {
|
||||
object_entries(document_json, "lastWatchedEpisodes")
|
||||
}
|
||||
|
||||
pub(crate) fn continue_watching_entries_json(document_json: &str) -> String {
|
||||
let document: Value = match serde_json::from_str(document_json) {
|
||||
Ok(document) => document,
|
||||
Err(_) => return "[]".to_string(),
|
||||
};
|
||||
let entries = document
|
||||
.get("externalContinueWatching")
|
||||
.and_then(Value::as_array)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(|item| {
|
||||
Some(KeyedValue {
|
||||
key: item.get("id")?.as_str()?,
|
||||
value: item,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
serde_json::to_string(&entries).unwrap_or_else(|_| "[]".to_string())
|
||||
}
|
||||
|
||||
fn object_entries(document_json: &str, field: &str) -> String {
|
||||
let document: Value = match serde_json::from_str(document_json) {
|
||||
Ok(document) => document,
|
||||
Err(_) => return "[]".to_string(),
|
||||
};
|
||||
let entries = document
|
||||
.get(field)
|
||||
.and_then(Value::as_object)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|(key, value)| KeyedValue { key, value })
|
||||
.collect::<Vec<_>>();
|
||||
serde_json::to_string(&entries).unwrap_or_else(|_| "[]".to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn extracts_all_legacy_library_domains() {
|
||||
let document = r#"{
|
||||
"progress":{"movie:1":{"position":12}},
|
||||
"watchlist":[{"id":"movie:1"}],
|
||||
"completed":[{"id":"movie:2"}],
|
||||
"dropped":[],
|
||||
"watched":{"video:1":true,"video:2":false},
|
||||
"lastWatchedEpisodes":{"series:1":{"id":"episode:2"}},
|
||||
"externalContinueWatching":[{"id":"movie:3"}]
|
||||
}"#;
|
||||
assert!(progress_entries_json(document).contains("movie:1"));
|
||||
assert!(library_items_json(document).contains("completed"));
|
||||
assert_eq!(watched_video_ids_json(document), r#"["video:1"]"#);
|
||||
assert!(last_watched_entries_json(document).contains("series:1"));
|
||||
assert!(continue_watching_entries_json(document).contains("movie:3"));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue