feat: include duration in YouTube entry metadata

This commit is contained in:
2026-10-18 12:21:17 +02:00
parent 493edfcf3a
commit 78b0c92e9b
7 changed files with 41 additions and 1 deletions
+1
View File
@@ -44,6 +44,7 @@ Startup shows a centered **YouTube / Miruro** selector. Use `j` / `k` or arrows
After selection, the browser starts in **normal mode**. Press `i` to type a search; Enter submits a changed query and returns to normal mode. Esc returns without submitting.
Both providers use the same layout: a cyan view heading, bordered search and results, a bordered status line, and a dim one-line key hint bar. Entries use two lines: the title above dimmed metadata. YouTube shows the channel and publication date beneath each video title, without separate channel/date columns; tagged channels remain cyan. The selected row uses black text on cyan with a `> ` marker; saved titles are green, watch-later titles magenta, and `✓` marks watched entries. Metadata and help open in centered cyan-bordered popups; bulk-removal confirmations use yellow borders. Miruro keeps its catalog navigation and title/episode metadata.
YouTube's dimmed metadata row includes duration as `M:SS` or `H:MM:SS`, or `Unknown duration` when unavailable. Duration is retained in saved collections; older entries without duration remain readable.
Selecting a YouTube video launches:
+19
View File
@@ -3019,6 +3019,25 @@ impl App {
.as_deref()
.unwrap_or("Unknown")
.into(),
" · ".into(),
video
.duration
.filter(|seconds| seconds.is_finite() && *seconds >= 0.0)
.map(|seconds| {
let seconds = seconds as u64;
if seconds >= 3600 {
format!(
"{}:{:02}:{:02}",
seconds / 3600,
seconds / 60 % 60,
seconds % 60
)
} else {
format!("{}:{:02}", seconds / 60, seconds % 60)
}
})
.unwrap_or_else(|| "Unknown duration".into())
.into(),
])
.dim(),
])),
+1
View File
@@ -199,6 +199,7 @@ fn video(id: String, title: String, date: Option<String>) -> Video {
channel_id: None,
uploader_id: None,
timestamp: None,
duration: None,
publication_date: date,
availability: None,
}
+9
View File
@@ -716,6 +716,12 @@ fn load_videos(
item.as_integer()
.ok_or_else(|| invalid("Video timestamp must be an integer"))?,
),
"duration" => serde_json::Value::from(
item.as_float()
.or_else(|| item.as_integer().map(|n| n as f64))
.filter(|n| n.is_finite() && *n >= 0.0)
.ok_or_else(|| invalid("Video duration must be a nonnegative number"))?,
),
_ => return Err(invalid(format!("Unknown saved video field {key:?}"))),
};
fields.insert(key.into(), field);
@@ -774,6 +780,9 @@ fn video_document<'a>(videos: impl Iterator<Item = &'a Video>) -> DocumentMut {
if let Some(timestamp) = video.timestamp {
table["timestamp"] = value(timestamp);
}
if let Some(duration) = video.duration {
table["duration"] = value(duration);
}
tables.push(table);
}
document["videos"] = if tables.is_empty() {
+1
View File
@@ -137,6 +137,7 @@ pub struct Video {
pub channel_id: Option<String>,
pub uploader_id: Option<String>,
pub timestamp: Option<i64>,
pub duration: Option<f64>,
/// Cached YYYY-MM-DD label; a leading ~ marks an estimated UTC date.
#[serde(default, rename = "upload_date")]
pub publication_date: Option<String>,
+8 -1
View File
@@ -2993,7 +2993,8 @@ fn video_channel_colors_follow_category_membership_and_preserve_selection_contra
&serde_json::json!({
"entries": cases.iter().map(|(channel, id, handle, _)| serde_json::json!({
"id": "jNQXAC9IVRw", "title": "Video title", "channel": channel,
"channel_id": id, "uploader_id": handle, "upload_date": "20261011"
"channel_id": id, "uploader_id": handle, "upload_date": "20261011",
"duration": 3723.5
})).collect::<Vec<_>>()
})
.to_string(),
@@ -3028,6 +3029,10 @@ fn video_channel_colors_follow_category_membership_and_preserve_selection_contra
assert!(title.contains("Video title"));
let date_x = row.find("2026-10-11").expect("date follows channel") as u16;
assert!(row.contains(&format!("{label} · 2026-10-11")));
assert!(
row.contains(" · 1:02:03"),
"duration is visible in metadata"
);
assert_eq!(buffer[(3, y - 1)].fg, Color::Reset, "title stays uncolored");
assert_eq!(buffer[(date_x, y)].fg, Color::Reset, "date stays uncolored");
assert!(
@@ -5152,6 +5157,7 @@ fn modal_keys_edit_or_navigate_without_submitting_on_escape() {
channel_id: None,
uploader_id: None,
timestamp: None,
duration: None,
publication_date: None,
availability: None,
},
@@ -5162,6 +5168,7 @@ fn modal_keys_edit_or_navigate_without_submitting_on_escape() {
channel_id: None,
uploader_id: None,
timestamp: None,
duration: None,
publication_date: None,
availability: None,
},
+2
View File
@@ -34,6 +34,7 @@ fn video() -> Video {
channel_id: Some("UC4QobU6STFB0P71PMvOGN5A".into()),
uploader_id: Some("@jawed".into()),
timestamp: Some(1_114_300_000),
duration: Some(19.0),
publication_date: Some("2005-04-23".into()),
availability: None,
}
@@ -66,6 +67,7 @@ fn miruro_video(episode: u32) -> Video {
channel_id: None,
uploader_id: None,
timestamp: Some(1_114_300_000),
duration: None,
publication_date: Some("2005-04-23".into()),
availability: None,
}