Compare commits
2 Commits
3e701c99c9
...
78b0c92e9b
| Author | SHA1 | Date | |
|---|---|---|---|
| 78b0c92e9b | |||
| 493edfcf3a |
@@ -43,7 +43,8 @@ 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. 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 two-line title/episode rows rather than YouTube's video table.
|
||||
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:
|
||||
|
||||
|
||||
+52
-25
@@ -101,7 +101,7 @@
|
||||
//! Switching views cancels in-flight extraction; returning retains the unaccepted request and per-channel offsets.
|
||||
//! Errors keep loaded rows and stop automatic retries; r reloads config and restarts the current view.
|
||||
//! Deep pages may slow down because yt-dlp offsets re-traverse earlier continuation pages.
|
||||
//! Video tables show Published as YYYY-MM-DD; ~ marks estimated UTC dates, Unknown missing metadata.
|
||||
//! Video entries show the title above channel · YYYY-MM-DD; ~ marks estimated UTC dates, Unknown missing metadata.
|
||||
//! Prefer exact upload dates and cache date labels during extraction, not on every redraw.
|
||||
//! Terebi receives `--new-instance --url` and a private `--progress-file`; each selection gets its own player.
|
||||
//! `--player` overrides its CLI path; otherwise `terebi` is resolved through PATH.
|
||||
@@ -2986,39 +2986,66 @@ impl App {
|
||||
.categories
|
||||
.values()
|
||||
.any(|channels| video_in_channels(video, channels, &self.config));
|
||||
let channel = Cell::from(video.channel.as_deref().unwrap_or("Unknown"));
|
||||
let title = Span::raw(video.title.as_str());
|
||||
let channel = Span::raw(video.channel.as_deref().unwrap_or("Unknown"));
|
||||
Row::new([
|
||||
Cell::from(if self.video_state.is_watched(&video.id) {
|
||||
"✓"
|
||||
} else {
|
||||
""
|
||||
}),
|
||||
if self.video_state.contains(CollectionKind::Saved, &video.id) {
|
||||
Cell::from(video.title.as_str()).fg(Color::Green)
|
||||
} else if self
|
||||
.video_state
|
||||
.contains(CollectionKind::WatchLater, &video.id)
|
||||
{
|
||||
Cell::from(video.title.as_str()).fg(Color::Magenta)
|
||||
} else {
|
||||
Cell::from(video.title.as_str())
|
||||
},
|
||||
if tagged {
|
||||
channel.fg(Color::Cyan)
|
||||
} else {
|
||||
channel
|
||||
},
|
||||
Cell::from(video.publication_date.as_deref().unwrap_or("Unknown")),
|
||||
Cell::from(Text::from(vec![
|
||||
Line::from(
|
||||
if self.video_state.contains(CollectionKind::Saved, &video.id) {
|
||||
title.fg(Color::Green)
|
||||
} else if self
|
||||
.video_state
|
||||
.contains(CollectionKind::WatchLater, &video.id)
|
||||
{
|
||||
title.fg(Color::Magenta)
|
||||
} else {
|
||||
title
|
||||
},
|
||||
),
|
||||
Line::from(vec![
|
||||
if tagged {
|
||||
channel.fg(Color::Cyan)
|
||||
} else {
|
||||
channel
|
||||
},
|
||||
" · ".into(),
|
||||
video
|
||||
.publication_date
|
||||
.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(),
|
||||
])),
|
||||
])
|
||||
.height(2)
|
||||
}),
|
||||
[
|
||||
Constraint::Length(1),
|
||||
Constraint::Fill(3),
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(11),
|
||||
],
|
||||
[Constraint::Length(1), Constraint::Fill(1)],
|
||||
)
|
||||
.header(Row::new(["", "Video", "Channel", "Published"]).bold())
|
||||
.block(Block::bordered().title(loading_title(
|
||||
if view.source == ViewSource::Search {
|
||||
" Results "
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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>,
|
||||
|
||||
+21
-5
@@ -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(),
|
||||
@@ -3010,9 +3011,9 @@ fn video_channel_colors_follow_category_membership_and_preserve_selection_contra
|
||||
.draw(|frame| app.draw(frame, Duration::ZERO))
|
||||
.unwrap();
|
||||
let buffer = terminal.backend().buffer();
|
||||
// One tab row, three search rows, then the table border and header.
|
||||
// One tab row, three search rows, then the results border and two-line entries.
|
||||
for (index, (label, _, _, tagged)) in cases.iter().enumerate() {
|
||||
let y = 6 + index as u16;
|
||||
let y = 6 + 2 * index as u16;
|
||||
let row: String = (0..120).map(|x| buffer[(x, y)].symbol()).collect();
|
||||
let x = row.find(label).expect("channel label is visible") as u16;
|
||||
assert_eq!(
|
||||
@@ -3024,8 +3025,21 @@ fn video_channel_colors_follow_category_membership_and_preserve_selection_contra
|
||||
},
|
||||
"{label} in row {index}, cleared={cleared}"
|
||||
);
|
||||
assert_eq!(buffer[(3, y)].fg, Color::Reset, "title stays uncolored");
|
||||
assert_eq!(buffer[(108, y)].fg, Color::Reset, "date stays uncolored");
|
||||
let title: String = (0..120).map(|x| buffer[(x, y - 1)].symbol()).collect();
|
||||
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!(
|
||||
buffer[(x, y)]
|
||||
.modifier
|
||||
.contains(ratatui::style::Modifier::DIM)
|
||||
);
|
||||
}
|
||||
if !cleared {
|
||||
app.views[0].selection.select(Some(0));
|
||||
@@ -5143,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,
|
||||
},
|
||||
@@ -5153,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,
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user