mirror of
https://github.com/FluxaMedia/fluxa-core.git
synced 2026-08-20 14:06:45 +00:00
Refine library history and recommendation requests
This commit is contained in:
parent
0179c0871d
commit
669f36f259
5 changed files with 93 additions and 19 deletions
|
|
@ -498,12 +498,14 @@ fn stream_dvcc_strip(upstream: &mut reqwest::blocking::Response, downstream: &mu
|
|||
/// Returns the number of four-character codes that were replaced.
|
||||
/// Parse the start offset from an HTTP `Content-Range: bytes START-END/TOTAL` header.
|
||||
// dvcC box parser
|
||||
#[cfg(test)]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct DvContainerInfo {
|
||||
profile: u8,
|
||||
compat_id: u8,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl DvContainerInfo {
|
||||
/// Mirrors Kodi's `notHasHDR10fallback` check
|
||||
/// (DVDVideoCodecAndroidMediaCodec.cpp:543-544 and 698-700):
|
||||
|
|
@ -520,6 +522,7 @@ impl DvContainerInfo {
|
|||
}
|
||||
|
||||
/// Scan `data` for a `dvcC` ISO-BMFF box and return the parsed DV profile info.
|
||||
#[cfg(test)]
|
||||
fn scan_dvcc_info(data: &[u8]) -> Option<DvContainerInfo> {
|
||||
for i in 0..data.len().saturating_sub(8) {
|
||||
if data[i..i + 4] == *b"dvcC" {
|
||||
|
|
@ -540,6 +543,7 @@ fn scan_dvcc_info(data: &[u8]) -> Option<DvContainerInfo> {
|
|||
/// byte[3][7:3] dv_level low 5 bits
|
||||
/// byte[3][2:0] rpu/el/bl_present_flags
|
||||
/// byte[4][7:4] dv_bl_signal_compatibility_id (4 bits)
|
||||
#[cfg(test)]
|
||||
fn parse_dvcc_payload(data: &[u8]) -> Option<DvContainerInfo> {
|
||||
if data.len() < 5 {
|
||||
return None;
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ pub(super) enum AppAction {
|
|||
id: String,
|
||||
language: Option<String>,
|
||||
profile: Option<Value>,
|
||||
similar_titles_source: Option<String>,
|
||||
},
|
||||
#[serde(rename = "detailPrefetchRequested")]
|
||||
DetailPrefetchRequested {
|
||||
|
|
|
|||
|
|
@ -128,6 +128,7 @@ struct FetchDetailSecondaryPayload {
|
|||
id: String,
|
||||
language: String,
|
||||
profile: Value,
|
||||
similar_titles_source: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
|
|
@ -260,6 +261,7 @@ pub(super) fn dispatch_secondary(
|
|||
id: String,
|
||||
language: Option<String>,
|
||||
profile: Option<Value>,
|
||||
similar_titles_source: Option<String>,
|
||||
) -> Vec<EffectEnvelope> {
|
||||
let generation = engine
|
||||
.state
|
||||
|
|
@ -274,6 +276,7 @@ pub(super) fn dispatch_secondary(
|
|||
id,
|
||||
language: language.unwrap_or_else(|| "en".to_string()),
|
||||
profile: profile.unwrap_or(Value::Null),
|
||||
similar_titles_source,
|
||||
},
|
||||
)]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -176,7 +176,8 @@ impl HeadlessEngine {
|
|||
id,
|
||||
language,
|
||||
profile,
|
||||
} => detail::dispatch_secondary(self, content_type, id, language, profile),
|
||||
similar_titles_source,
|
||||
} => detail::dispatch_secondary(self, content_type, id, language, profile, similar_titles_source),
|
||||
AppAction::DetailPrefetchRequested {
|
||||
content_type,
|
||||
id,
|
||||
|
|
|
|||
|
|
@ -306,9 +306,18 @@ pub(crate) fn library_view_plan_json(args_json: &str) -> Option<String> {
|
|||
.partial_cmp(&rating(a))
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
});
|
||||
let watching_ids: std::collections::HashSet<&str> = watching
|
||||
.iter()
|
||||
.filter_map(|item| item.get("id").and_then(Value::as_str))
|
||||
.collect();
|
||||
let mut history = all;
|
||||
history.retain(|item| activity_time(item) > 0);
|
||||
history.sort_by_key(|item| std::cmp::Reverse(activity_time(item)));
|
||||
history.retain(|item| {
|
||||
item.get("id")
|
||||
.and_then(Value::as_str)
|
||||
.is_none_or(|id| !watching_ids.contains(id))
|
||||
&& playback_time(item) > 0
|
||||
});
|
||||
history.sort_by_key(|item| std::cmp::Reverse(playback_time(item)));
|
||||
let tab = args.get("tab").and_then(Value::as_str).unwrap_or("");
|
||||
let mut items = match tab {
|
||||
"watchlist" => watchlist.clone(),
|
||||
|
|
@ -495,9 +504,23 @@ fn rating(item: &Value) -> f64 {
|
|||
}
|
||||
fn timestamp(item: &Value, key: &str) -> i64 {
|
||||
item.get(key)
|
||||
.and_then(Value::as_str)
|
||||
.and_then(|value| chrono::DateTime::parse_from_rfc3339(value).ok())
|
||||
.map(|value| value.timestamp_millis())
|
||||
.and_then(|value| {
|
||||
value
|
||||
.as_i64()
|
||||
.map(|value| {
|
||||
if value < 10_000_000_000 {
|
||||
value * 1_000
|
||||
} else {
|
||||
value
|
||||
}
|
||||
})
|
||||
.or_else(|| {
|
||||
value
|
||||
.as_str()
|
||||
.and_then(|value| chrono::DateTime::parse_from_rfc3339(value).ok())
|
||||
.map(|value| value.timestamp_millis())
|
||||
})
|
||||
})
|
||||
.unwrap_or(0)
|
||||
}
|
||||
fn status_changed_at(item: &Value) -> &str {
|
||||
|
|
@ -505,19 +528,61 @@ fn status_changed_at(item: &Value) -> &str {
|
|||
.and_then(Value::as_str)
|
||||
.unwrap_or("")
|
||||
}
|
||||
fn activity_time(item: &Value) -> i64 {
|
||||
[
|
||||
"savedAt",
|
||||
"lastWatchedAt",
|
||||
"statusChangedAt",
|
||||
"newEpisodeReleasedAt",
|
||||
"lastAirDateCheckedAt",
|
||||
"updatedAt",
|
||||
]
|
||||
.iter()
|
||||
.map(|key| timestamp(item, key))
|
||||
.find(|value| *value > 0)
|
||||
.unwrap_or(0)
|
||||
fn playback_time(item: &Value) -> i64 {
|
||||
let last_watched = timestamp(item, "lastWatchedAt");
|
||||
if last_watched > 0 {
|
||||
return last_watched;
|
||||
}
|
||||
let has_playback_state = item
|
||||
.get("timeOffset")
|
||||
.and_then(Value::as_i64)
|
||||
.is_some_and(|value| value > 0)
|
||||
|| item
|
||||
.get("lastVideoId")
|
||||
.and_then(Value::as_str)
|
||||
.is_some_and(|value| !value.is_empty());
|
||||
if has_playback_state {
|
||||
timestamp(item, "savedAt")
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn history_excludes_active_watching_and_non_playback_changes() {
|
||||
let plan = library_view_plan_json(
|
||||
&json!({
|
||||
"watchlist": [
|
||||
{"id": "saved", "savedAt": "2026-07-01T00:00:00Z"},
|
||||
{"id": "played", "lastVideoId": "played:1:1", "savedAt": "2026-07-02T00:00:00Z"}
|
||||
],
|
||||
"watching": [
|
||||
{"id": "active", "lastVideoId": "active:1:1", "savedAt": "2026-07-03T00:00:00Z"}
|
||||
],
|
||||
"completed": [],
|
||||
"dropped": [],
|
||||
"progress": {},
|
||||
"tab": "history"
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
.unwrap();
|
||||
let items = serde_json::from_str::<Value>(&plan).unwrap()["items"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.clone();
|
||||
|
||||
assert_eq!(
|
||||
items,
|
||||
vec![
|
||||
json!({"id": "played", "lastVideoId": "played:1:1", "savedAt": "2026-07-02T00:00:00Z"})
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
fn air_time(item: &Value) -> i64 {
|
||||
let value = timestamp(item, "nextEpisodeAirDate").max(timestamp(item, "newEpisodeReleasedAt"));
|
||||
|
|
|
|||
Loading…
Reference in a new issue