Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ default-members = ["crates/*"]

[workspace.package]
edition = "2024"
version = "0.16.1"
version = "0.17.0"
license = "MIT"

[profile.release]
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ what you've watched: Settings → Sync with other devices, then pick a folder th
reach, like a folder on your NAS. Each Flex keeps its own libraries (pointing at wherever
the media is on that machine, as long as the libraries have the same names) and they
swap watched state, resume points, history, the watchlist, matches you fixed by hand and
YouTube subscriptions through that folder every 30 seconds or so.
YouTube subscriptions and what you've watched there through that folder every 30 seconds or so.

## YouTube

Expand All @@ -77,7 +77,8 @@ Add channels under Channels by pasting a link or an @handle, or import your exis
subscriptions from Google Takeout (`subscriptions.csv`), FreeTube, NewPipe or an OPML
file. New videos are picked up from each channel's feed every half an hour, so there's no
API key to set up. Search works too, either on the YouTube page or from the main search
(pick the YouTube scope at the top of the results).
(pick the YouTube scope at the top of the results). History lists the videos you've
played, most recent first.

Videos take a few seconds to start, since yt-dlp has to ask YouTube for them first, but
Flex starts on whichever video you have highlighted, so it's usually ready by the time you
Expand Down
6 changes: 3 additions & 3 deletions crates/flex-app/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ anyhow = "1.0.104"
chrono = { version = "0.4.45", default-features = false, features = ["clock"] }
eframe = { version = "0.36", default-features = false, features = ["glow", "default_fonts"] }
env_logger = "0.11.11"
flex-core = { version = "0.16.1", path = "../flex-core" }
flex-plugin = { version = "0.16.1", path = "../flex-plugin" }
flex-web = { version = "0.16.1", path = "../flex-web" }
flex-core = { version = "0.17.0", path = "../flex-core" }
flex-plugin = { version = "0.17.0", path = "../flex-plugin" }
flex-web = { version = "0.17.0", path = "../flex-web" }
image = { version = "0.25", default-features = false, features = ["jpeg", "png", "webp"] }
libloading = "0.9"
log = "0.4.34"
Expand Down
60 changes: 57 additions & 3 deletions crates/flex-app/src/screens/youtube.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,9 +235,9 @@ pub struct YouTubeHome {
loaded: Option<u64>,
}

/// Search, Channels, and which channels' videos to show.
fn home_buttons(sh: &Shared) -> [&'static str; 3] {
["Search…", "Channels", if sh.youtube_focused_only() { "Showing: focused" } else { "Showing: all channels" }]
/// Search, Channels, History, and which channels' videos to show.
fn home_buttons(sh: &Shared) -> [&'static str; 4] {
["Search…", "Channels", "History", if sh.youtube_focused_only() { "Showing: focused" } else { "Showing: all channels" }]
}

impl YouTubeHome {
Expand Down Expand Up @@ -275,6 +275,7 @@ impl Screen for YouTubeHome {
match self.view.handle(press, home_buttons(sh).len(), self.items.len()) {
GridInput::Button(0) => Nav::push(TextEntry::youtube_search()),
GridInput::Button(1) => Nav::push(ChannelList::default()),
GridInput::Button(2) => Nav::push(YouTubeHistory::default()),
GridInput::Button(_) => {
let on = !sh.youtube_focused_only();
sh.set_youtube_focused_only(on);
Expand Down Expand Up @@ -307,6 +308,59 @@ impl Screen for YouTubeHome {
}
}

const HISTORY: usize = 500;

/// Videos played, most recently first (synced like the rest of YouTube's progress).
#[derive(Default)]
pub struct YouTubeHistory {
view: VideoGrid,
items: Vec<VideoItem>,
loaded: Option<u64>,
}

impl YouTubeHistory {
fn refresh(&mut self, sh: &Shared) {
if self.loaded == Some(sh.youtube.version) {
return;
}
self.loaded = Some(sh.youtube.version);
let now = now_unix();
self.items = sh
.yt_history(HISTORY)
.into_iter()
.map(|(id, p)| {
let how = match (p.watched, p.position, p.duration) {
(true, _, _) => "watched".to_owned(),
(false, Some(pos), Some(dur)) => format!("{} left", theme::format_time(dur - pos)),
_ => "started".to_owned(),
};
let detail = format!("{} · {} · {how}", p.channel, ago(p.updated_at, now));
let item = VideoItem { id, title: p.title.clone(), channel: p.channel.clone(), channel_id: String::new(), detail, fraction: None, watched: false };
with_progress(item, Some(&p))
})
.collect();
}
}

impl Screen for YouTubeHistory {
fn handle(&mut self, sh: &mut Shared, press: Press) -> Nav {
if press.is(Action::Back) {
return Nav::Pop;
}
self.refresh(sh);
video_input(self.view.handle(press, 0, self.items.len()), &self.items).unwrap_or(Nav::None)
}

fn ui(&mut self, ui: &mut Ui, sh: &mut Shared, rect: Rect, focused: bool) -> Nav {
self.refresh(sh);
let body = widgets::header(ui.painter(), rect, "History", "YouTube", ui.style());
let items = std::mem::take(&mut self.items);
self.view.ui(ui, sh, body, &[], &items, "Nothing watched yet", focused);
self.items = items;
Nav::None
}
}

/// One channel's videos.
pub struct ChannelPage {
id: String,
Expand Down
4 changes: 4 additions & 0 deletions crates/flex-app/src/shared.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1744,6 +1744,10 @@ impl Shared {
self.db.yt_progress(id).ok().flatten()
}

pub fn yt_history(&self, limit: usize) -> Vec<(String, flex_core::youtube::store::WatchProgress)> {
self.db.yt_history(limit).unwrap_or_default()
}

pub fn yt_progress_all(&self) -> HashMap<String, flex_core::youtube::store::WatchProgress> {
self.db.yt_progress_all().unwrap_or_default()
}
Expand Down
2 changes: 1 addition & 1 deletion crates/flex-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ publish = false
[dependencies]
anyhow = "1.0.104"
directories = "6.0.0"
flex-plugin = { version = "0.16.1", path = "../flex-plugin" }
flex-plugin = { version = "0.17.0", path = "../flex-plugin" }
hunch = "2"
lofty = "0.25.4"
log = "0.4.34"
Expand Down
30 changes: 30 additions & 0 deletions crates/flex-core/src/youtube/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,22 @@ impl Db {
Ok(rows.collect::<rusqlite::Result<_>>()?)
}

/// Videos actually played (not only marked watched), most recently first: (id, progress).
/// One entry per video, at the last time it was played.
pub fn yt_history(&self, limit: usize) -> Result<Vec<(String, WatchProgress)>> {
let mut stmt = self.conn.prepare(
"SELECT id, title, channel, position, duration, watched, updated_at FROM yt_progress
WHERE duration IS NOT NULL ORDER BY updated_at DESC LIMIT ?1",
)?;
let rows = stmt.query_map([limit as i64], |r| {
Ok((
r.get::<_, String>(0)?,
WatchProgress { title: r.get(1)?, channel: r.get(2)?, position: r.get(3)?, duration: r.get(4)?, watched: r.get(5)?, updated_at: r.get(6)? },
))
})?;
Ok(rows.collect::<rusqlite::Result<_>>()?)
}

/// Records where playback got to. Past `threshold` (e.g. 0.9) of the way through, the
/// video counts as watched and won't resume. Returns the saved progress.
#[allow(clippy::too_many_arguments)]
Expand Down Expand Up @@ -269,4 +285,18 @@ mod tests {
assert!(!db.yt_progress("v").unwrap().unwrap().watched);
assert_eq!(db.yt_progress_all().unwrap().len(), 2);
}

#[test]
fn history_lists_played_videos_newest_first() {
let db = Db::open_in_memory().unwrap();
db.record_yt_progress("a", "A", "C", 60.0, 600.0, 0.9, 10).unwrap();
db.record_yt_progress("b", "B", "C", 590.0, 600.0, 0.9, 20).unwrap();
// Marked watched from a list without being played: not history.
db.set_yt_watched("c", "C", "C", true, 30).unwrap();
let ids = |db: &Db| db.yt_history(10).unwrap().into_iter().map(|(id, _)| id).collect::<Vec<_>>();
assert_eq!(ids(&db), ["b", "a"]);
db.record_yt_progress("a", "A", "C", 120.0, 600.0, 0.9, 40).unwrap();
assert_eq!(ids(&db), ["a", "b"], "playing again moves it to the top");
assert_eq!(db.yt_history(1).unwrap().len(), 1);
}
}
2 changes: 1 addition & 1 deletion crates/flex-web/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ publish = false
[dependencies]
anyhow = "1.0.104"
axum = "0.8.9"
flex-core = { version = "0.16.1", path = "../flex-core" }
flex-core = { version = "0.17.0", path = "../flex-core" }
log = "0.4.34"
serde = { version = "1.0.229", features = ["derive", "rc"] }
serde_json = "1.0.151"
Expand Down
2 changes: 1 addition & 1 deletion plugins/anilist/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ license.workspace = true
publish = false

[dependencies]
flex-plugin = { version = "0.16.1", path = "../../crates/flex-plugin" }
flex-plugin = { version = "0.17.0", path = "../../crates/flex-plugin" }
ureq = { version = "3", features = ["json"] }
2 changes: 1 addition & 1 deletion plugins/anilist/plugin.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name = "AniList"
description = "Updates your AniList progress as you watch anime in Flex"
version = "0.16.1"
version = "0.17.0"
command = "flex-anilist"
subscribe = ["playback"]

Expand Down
2 changes: 1 addition & 1 deletion plugins/lotus-lamp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ license.workspace = true
publish = false

[dependencies]
flex-plugin = { version = "0.16.1", path = "../../crates/flex-plugin" }
flex-plugin = { version = "0.17.0", path = "../../crates/flex-plugin" }
lotus-lamp = { git = "https://github.com/bombpersons/lotus-lamp-rs", rev = "59cf5ccaa0007f006f1812fd02cc5991aff11e72" }
tokio = { version = "1.53.2", features = ["rt", "time", "sync", "macros"] }
2 changes: 1 addition & 1 deletion plugins/lotus-lamp/plugin.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name = "LED strip (Lotus Lamp)"
description = "Lights a Bluetooth LED strip behind the TV with the colours on screen"
version = "0.16.1"
version = "0.17.0"
command = "flex-lotus-lamp"
subscribe = ["light"]

Expand Down
2 changes: 1 addition & 1 deletion plugins/trakt/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ license.workspace = true
publish = false

[dependencies]
flex-plugin = { version = "0.16.1", path = "../../crates/flex-plugin" }
flex-plugin = { version = "0.17.0", path = "../../crates/flex-plugin" }
ureq = { version = "3", features = ["json"] }
2 changes: 1 addition & 1 deletion plugins/trakt/plugin.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name = "Trakt"
description = "Keeps your Trakt history up to date with what you watch in Flex"
version = "0.16.1"
version = "0.17.0"
command = "flex-trakt"
subscribe = ["playback"]

Expand Down
Loading