feat(remote): RemoteClientKind enum + kind-aware live remotes list
Both front-ends hold Vec<Arc<RemoteClientKind>> (web behind RwLock). Catacomb browse unchanged; PeerTube remotes report a phase-3 stopgap when browsed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
be3271ab32
commit
db0c74ffca
4 changed files with 130 additions and 27 deletions
26
src/app.rs
26
src/app.rs
|
|
@ -260,8 +260,8 @@ pub struct App {
|
|||
/// Auto-tag grouping suggestions, recomputed when the Maintenance screen
|
||||
/// opens and after a group is applied. Empty when there's nothing to suggest.
|
||||
autotag_suggestions: Vec<crate::autotag::GroupSuggestion>,
|
||||
// Federation (read-only remote libraries).
|
||||
remotes: Vec<std::sync::Arc<crate::remote::RemoteClient>>,
|
||||
// Federation (read-only remote libraries — catacomb + PeerTube).
|
||||
remotes: Vec<std::sync::Arc<crate::remote::RemoteClientKind>>,
|
||||
/// Currently-selected peer index, if the Remotes screen is showing one.
|
||||
remote_selected: Option<usize>,
|
||||
/// Last fetched remote library; `None` until a peer is loaded.
|
||||
|
|
@ -547,8 +547,8 @@ impl App {
|
|||
downloader.fetch_comments = config.backup.fetch_comments;
|
||||
downloader.dedup_enabled = config.backup.dedup_enabled;
|
||||
// Federation peers, built once from config (read-only remote libraries).
|
||||
let remotes: Vec<std::sync::Arc<crate::remote::RemoteClient>> = config.remotes.iter()
|
||||
.map(|r| std::sync::Arc::new(crate::remote::RemoteClient::new(r)))
|
||||
let remotes: Vec<std::sync::Arc<crate::remote::RemoteClientKind>> = config.remotes.iter()
|
||||
.map(|r| std::sync::Arc::new(crate::remote::RemoteClientKind::from_section(r)))
|
||||
.collect();
|
||||
downloader.convert_defaults = config.convert.clone();
|
||||
let config_bind = config.web.bind.clone();
|
||||
|
|
@ -2524,16 +2524,28 @@ impl App {
|
|||
let Some(client) = self.remotes.get(idx).cloned() else { return };
|
||||
self.remote_selected = Some(idx);
|
||||
self.remote_library = None;
|
||||
self.remote_status = format!("Connecting to {}…", client.name);
|
||||
match client.as_ref() {
|
||||
crate::remote::RemoteClientKind::Catacomb(_) => {
|
||||
self.remote_status = format!("Connecting to {}…", client.name());
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
self.remote_rx = Some(rx);
|
||||
let repaint_ctx = self.egui_ctx.clone();
|
||||
std::thread::spawn(move || {
|
||||
let _ = tx.send(client.library());
|
||||
let res = match client.as_ref() {
|
||||
crate::remote::RemoteClientKind::Catacomb(c) => c.library(),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let _ = tx.send(res);
|
||||
// Wake the egui loop so update() drains the result promptly.
|
||||
repaint_ctx.request_repaint();
|
||||
});
|
||||
}
|
||||
crate::remote::RemoteClientKind::Peertube(_) => {
|
||||
self.remote_status =
|
||||
"PeerTube browsing arrives in a later update".to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Launch the configured player on a remote (absolute, tokenized) URL.
|
||||
/// mpv streams it straight from the peer; no resume tracking (that's local).
|
||||
|
|
@ -2567,7 +2579,7 @@ impl App {
|
|||
ui.horizontal_wrapped(|ui| {
|
||||
for (i, r) in self.remotes.iter().enumerate() {
|
||||
let sel = self.remote_selected == Some(i);
|
||||
if ui.selectable_label(sel, format!("🌐 {}", r.name)).clicked() {
|
||||
if ui.selectable_label(sel, format!("🌐 {}", r.name())).clicked() {
|
||||
select_remote = Some(i);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,6 +96,17 @@ impl PeerTubeClient {
|
|||
}
|
||||
}
|
||||
|
||||
/// API base (`scheme://host`), for the editor's remote listing.
|
||||
pub fn base_url(&self) -> &str {
|
||||
&self.api_base
|
||||
}
|
||||
|
||||
/// Whether OAuth credentials are configured (editor `has_password`
|
||||
/// indicator; never echoes the secret).
|
||||
pub fn has_password(&self) -> bool {
|
||||
self.password.is_some()
|
||||
}
|
||||
|
||||
/// Canonical watch URL for a video, handed to the downloader (phase 3).
|
||||
pub fn watch_url(&self, uuid: &str) -> String {
|
||||
format!("{}/w/{}", self.api_base, uuid)
|
||||
|
|
|
|||
|
|
@ -151,6 +151,42 @@ impl RemoteClient {
|
|||
let v = self.library_json()?;
|
||||
Ok(parse_library(&v))
|
||||
}
|
||||
|
||||
/// Whether a password is configured (used by the editor's `has_password`
|
||||
/// indicator; never echoes the secret itself).
|
||||
pub fn has_password(&self) -> bool {
|
||||
self.password.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
/// A live federation client of either kind. Held by both front-ends' remotes
|
||||
/// lists so catacomb peers and PeerTube targets coexist.
|
||||
pub enum RemoteClientKind {
|
||||
Catacomb(RemoteClient),
|
||||
Peertube(crate::peertube::PeerTubeClient),
|
||||
}
|
||||
|
||||
impl RemoteClientKind {
|
||||
pub fn from_section(cfg: &crate::config::RemoteSection) -> Self {
|
||||
match cfg.kind {
|
||||
crate::config::RemoteKind::Catacomb => Self::Catacomb(RemoteClient::new(cfg)),
|
||||
crate::config::RemoteKind::Peertube => {
|
||||
Self::Peertube(crate::peertube::PeerTubeClient::new(cfg))
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn name(&self) -> &str {
|
||||
match self {
|
||||
Self::Catacomb(c) => &c.name,
|
||||
Self::Peertube(p) => &p.name,
|
||||
}
|
||||
}
|
||||
pub fn kind(&self) -> crate::config::RemoteKind {
|
||||
match self {
|
||||
Self::Catacomb(_) => crate::config::RemoteKind::Catacomb,
|
||||
Self::Peertube(_) => crate::config::RemoteKind::Peertube,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a relative URL points at peer media the read-only feed token grants
|
||||
|
|
@ -274,4 +310,23 @@ mod tests {
|
|||
assert_eq!(lib.channels[0].videos.len(), 2);
|
||||
assert_eq!(lib.channels[0].videos[1].id, "b");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn client_kind_from_section_dispatches() {
|
||||
use crate::config::{RemoteKind, RemoteSection};
|
||||
let cat = RemoteSection {
|
||||
name: "c".into(), url: "http://p:8081".into(),
|
||||
kind: RemoteKind::Catacomb, username: None, password: None,
|
||||
};
|
||||
let pt = RemoteSection {
|
||||
name: "p".into(), url: "https://framatube.org".into(),
|
||||
kind: RemoteKind::Peertube, username: None, password: None,
|
||||
};
|
||||
let a = RemoteClientKind::from_section(&cat);
|
||||
let b = RemoteClientKind::from_section(&pt);
|
||||
assert_eq!(a.kind(), RemoteKind::Catacomb);
|
||||
assert_eq!(a.name(), "c");
|
||||
assert_eq!(b.kind(), RemoteKind::Peertube);
|
||||
assert_eq!(b.name(), "p");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
45
src/web.rs
45
src/web.rs
|
|
@ -148,10 +148,10 @@ pub struct WebState {
|
|||
/// GET access to `/feed*` and the media mounts even when a password is
|
||||
/// set. Persisted in the `feed_token` setting; stable until regenerated.
|
||||
pub feed_token: String,
|
||||
/// Federation peers (read-only remote libraries), built once from
|
||||
/// `config.remotes` at startup. Indexed by position for the `/api/remotes`
|
||||
/// endpoints. Empty when no `[[remote]]` is configured.
|
||||
pub remotes: Vec<std::sync::Arc<crate::remote::RemoteClient>>,
|
||||
/// Federation peers (catacomb + PeerTube). Rebuilt wholesale by the editor's
|
||||
/// `PUT /api/remotes` (behind the `RwLock` for live-apply). Indexed by
|
||||
/// position for the `/api/remotes` endpoints.
|
||||
pub remotes: std::sync::RwLock<Vec<std::sync::Arc<crate::remote::RemoteClientKind>>>,
|
||||
}
|
||||
|
||||
/// Live state for the background perceptual-dedup job (see
|
||||
|
|
@ -2250,9 +2250,21 @@ async fn get_maintenance_scan(State(state): State<Arc<WebState>>) -> impl IntoRe
|
|||
async fn get_remotes(State(state): State<Arc<WebState>>) -> impl IntoResponse {
|
||||
let list: Vec<_> = state
|
||||
.remotes
|
||||
.read()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, r)| serde_json::json!({ "id": i, "name": r.name, "url": r.base_url() }))
|
||||
.map(|(i, r)| {
|
||||
let (url, has_password) = match r.as_ref() {
|
||||
crate::remote::RemoteClientKind::Catacomb(c) => (c.base_url().to_string(), c.has_password()),
|
||||
crate::remote::RemoteClientKind::Peertube(p) => (p.base_url().to_string(), p.has_password()),
|
||||
};
|
||||
let kind = match r.kind() {
|
||||
crate::config::RemoteKind::Catacomb => "catacomb",
|
||||
crate::config::RemoteKind::Peertube => "peertube",
|
||||
};
|
||||
serde_json::json!({ "id": i, "name": r.name(), "url": url, "kind": kind, "has_password": has_password })
|
||||
})
|
||||
.collect();
|
||||
Json(list)
|
||||
}
|
||||
|
|
@ -2265,14 +2277,27 @@ async fn get_remote_library(
|
|||
State(state): State<Arc<WebState>>,
|
||||
Path(id): Path<usize>,
|
||||
) -> Response {
|
||||
let Some(remote) = state.remotes.get(id).cloned() else {
|
||||
let remote = state.remotes.read().unwrap().get(id).cloned();
|
||||
let Some(remote) = remote else {
|
||||
return (StatusCode::NOT_FOUND, "no such remote").into_response();
|
||||
};
|
||||
match tokio::task::spawn_blocking(move || remote.library_json()).await {
|
||||
match remote.as_ref() {
|
||||
crate::remote::RemoteClientKind::Catacomb(_) => {
|
||||
match tokio::task::spawn_blocking(move || match remote.as_ref() {
|
||||
crate::remote::RemoteClientKind::Catacomb(c) => c.library_json(),
|
||||
_ => unreachable!(),
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(Ok(v)) => Json(v).into_response(),
|
||||
Ok(Err(e)) => (StatusCode::BAD_GATEWAY, format!("remote error: {e}")).into_response(),
|
||||
Err(_) => (StatusCode::INTERNAL_SERVER_ERROR, "remote task failed").into_response(),
|
||||
}
|
||||
}
|
||||
crate::remote::RemoteClientKind::Peertube(_) => {
|
||||
(StatusCode::NOT_IMPLEMENTED, "PeerTube browsing arrives in a later update").into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `GET /api/autotag/suggest` — heuristic folder-grouping suggestions for
|
||||
|
|
@ -3211,10 +3236,10 @@ async fn serve(config: Config, shutdown_rx: std::sync::mpsc::Receiver<()>) {
|
|||
// get a Lagged error and just resubscribe).
|
||||
let (progress_tx, _initial_rx) = tokio::sync::broadcast::channel::<String>(16);
|
||||
// Build the federation peers from config before `config` is moved in.
|
||||
let remotes: Vec<std::sync::Arc<crate::remote::RemoteClient>> = config
|
||||
let remotes: Vec<std::sync::Arc<crate::remote::RemoteClientKind>> = config
|
||||
.remotes
|
||||
.iter()
|
||||
.map(|r| std::sync::Arc::new(crate::remote::RemoteClient::new(r)))
|
||||
.map(|r| std::sync::Arc::new(crate::remote::RemoteClientKind::from_section(r)))
|
||||
.collect();
|
||||
let state = Arc::new(WebState {
|
||||
library: Mutex::new(library),
|
||||
|
|
@ -3237,7 +3262,7 @@ async fn serve(config: Config, shutdown_rx: std::sync::mpsc::Receiver<()>) {
|
|||
library_body_cache: Mutex::new(None),
|
||||
dedup: std::sync::Arc::new(DedupState::default()),
|
||||
feed_token,
|
||||
remotes,
|
||||
remotes: std::sync::RwLock::new(remotes),
|
||||
});
|
||||
|
||||
// Broadcast progress snapshots to WebSocket subscribers. Ticks fast
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue