Added FED devices
Build and Publish / Build and Publish Docker Image (push) Successful in 4m1s

This commit is contained in:
Ultradesu
2026-07-24 16:54:41 +03:00
parent c2bdd62a51
commit b737ced3fc
3 changed files with 92 additions and 33 deletions
Generated
+1 -1
View File
@@ -1845,7 +1845,7 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
[[package]] [[package]]
name = "furumusic" name = "furumusic"
version = "0.7.1" version = "0.8.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "furumusic" name = "furumusic"
version = "0.8.0" version = "0.8.1"
edition = "2024" edition = "2024"
description = "Reusable web-app boilerplate: auth, OIDC/SSO, admin panel, user management, i18n, PostgreSQL" description = "Reusable web-app boilerplate: auth, OIDC/SSO, admin panel, user management, i18n, PostgreSQL"
+90 -31
View File
@@ -27,6 +27,7 @@ const PAIRING_WAIT_MS: i64 = 5 * 60 * 1000;
const PAIRING_RETRY_DELAY: Duration = Duration::from_secs(1); const PAIRING_RETRY_DELAY: Duration = Duration::from_secs(1);
const RESPONSE_DRAIN_TIMEOUT: Duration = Duration::from_secs(2); const RESPONSE_DRAIN_TIMEOUT: Duration = Duration::from_secs(2);
const DEVICE_SYNC_INTERVAL: Duration = Duration::from_secs(2); const DEVICE_SYNC_INTERVAL: Duration = Duration::from_secs(2);
const LOCAL_SEED_RECHECK_MS: i64 = 60 * 1000;
const MAX_LINE: usize = 8 * 1024 * 1024; const MAX_LINE: usize = 8 * 1024 * 1024;
const MAX_OPS_PER_BATCH: i64 = 1000; const MAX_OPS_PER_BATCH: i64 = 1000;
@@ -596,6 +597,7 @@ pub async fn revoke_device(pool: &sqlx::PgPool, user_id: i64, device_id: &str) -
pub async fn status(pool: &sqlx::PgPool, user_id: i64, user_name: &str) -> Result<FedDeviceStatus> { pub async fn status(pool: &sqlx::PgPool, user_id: i64, user_name: &str) -> Result<FedDeviceStatus> {
let identity = ensure_identity(pool, user_id, user_name).await?; let identity = ensure_identity(pool, user_id, user_name).await?;
maybe_seed_local_user_state(pool, user_id).await?;
let active_devices: i64 = sqlx::query_scalar( let active_devices: i64 = sqlx::query_scalar(
"SELECT COUNT(*) FROM furumusic__fed_device "SELECT COUNT(*) FROM furumusic__fed_device
WHERE user_id = $1 AND trusted_at_ms IS NOT NULL AND revoked_at_ms IS NULL", WHERE user_id = $1 AND trusted_at_ms IS NOT NULL AND revoked_at_ms IS NULL",
@@ -1467,6 +1469,24 @@ async fn ensure_identity(pool: &sqlx::PgPool, user_id: i64, user_name: &str) ->
}) })
} }
async fn maybe_seed_local_user_state(pool: &sqlx::PgPool, user_id: i64) -> Result<()> {
let last_seeded_at_ms: Option<i64> = sqlx::query_scalar(
"SELECT local_seeded_at_ms
FROM furumusic__fed_device_identity
WHERE user_id = $1",
)
.bind(user_id)
.fetch_optional(pool)
.await?;
let Some(last_seeded_at_ms) = last_seeded_at_ms else {
return Ok(());
};
if now_ms().saturating_sub(last_seeded_at_ms) < LOCAL_SEED_RECHECK_MS {
return Ok(());
}
seed_local_user_state(pool, user_id).await
}
async fn seed_local_user_state(pool: &sqlx::PgPool, user_id: i64) -> Result<()> { async fn seed_local_user_state(pool: &sqlx::PgPool, user_id: i64) -> Result<()> {
let now = now_ms(); let now = now_ms();
let like_rows = sqlx::query( let like_rows = sqlx::query(
@@ -1512,22 +1532,8 @@ async fn seed_local_user_state(pool: &sqlx::PgPool, user_id: i64) -> Result<()>
.await?; .await?;
for playlist in playlists { for playlist in playlists {
let playlist_id: i64 = playlist.get("id"); let playlist_id: i64 = playlist.get("id");
let sync_id = format!("webpl_{user_id}_{playlist_id}");
let title: String = playlist.get("title"); let title: String = playlist.get("title");
sqlx::query( let sync_id = ensure_seed_playlist_sync_id(pool, user_id, playlist_id, &title, now).await?;
"INSERT INTO furumusic__fed_state_playlist
(user_id, playlist_id, local_playlist_id, title, deleted, hlc_ms, op_id)
VALUES ($1, $2, $3, $4, false, $5, $6)
ON CONFLICT (user_id, playlist_id) DO NOTHING",
)
.bind(user_id)
.bind(&sync_id)
.bind(playlist_id)
.bind(&title)
.bind(now)
.bind(format!("local_seed:playlist:{playlist_id}"))
.execute(pool)
.await?;
let items = sqlx::query( let items = sqlx::query(
"SELECT pt.track_id, pt.position, c.content_id "SELECT pt.track_id, pt.position, c.content_id
@@ -1593,17 +1599,16 @@ async fn seed_local_user_state(pool: &sqlx::PgPool, user_id: i64) -> Result<()>
.bind(user_id) .bind(user_id)
.fetch_one(pool) .fetch_one(pool)
.await?; .await?;
if missing_likes + missing_playlist_items == 0 { sqlx::query(
sqlx::query( "UPDATE furumusic__fed_device_identity
"UPDATE furumusic__fed_device_identity SET local_seeded_at_ms = $2
SET local_seeded_at_ms = $2 WHERE user_id = $1",
WHERE user_id = $1 AND local_seeded_at_ms = 0", )
) .bind(user_id)
.bind(user_id) .bind(now)
.bind(now) .execute(pool)
.execute(pool) .await?;
.await?; if missing_likes + missing_playlist_items != 0 {
} else {
tracing::debug!( tracing::debug!(
user_id, user_id,
missing_likes, missing_likes,
@@ -3414,17 +3419,36 @@ async fn ensure_materialized_playlist(
.bind(&now) .bind(&now)
.fetch_one(pool) .fetch_one(pool)
.await?; .await?;
upsert_materialized_playlist_link(pool, user_id, playlist_sync_id, id, title).await?;
Ok(id)
}
async fn upsert_materialized_playlist_link(
pool: &sqlx::PgPool,
user_id: i64,
playlist_sync_id: &str,
local_playlist_id: i64,
title: &str,
) -> Result<()> {
sqlx::query( sqlx::query(
"UPDATE furumusic__fed_state_playlist "INSERT INTO furumusic__fed_state_playlist
SET local_playlist_id = $3 (user_id, playlist_id, local_playlist_id, title, deleted, hlc_ms, op_id)
WHERE user_id = $1 AND playlist_id = $2", VALUES ($1, $2, $3, $4, false, 0, $5)
ON CONFLICT (user_id, playlist_id) DO UPDATE SET
local_playlist_id = COALESCE(furumusic__fed_state_playlist.local_playlist_id, EXCLUDED.local_playlist_id),
title = CASE
WHEN furumusic__fed_state_playlist.hlc_ms = 0 THEN EXCLUDED.title
ELSE furumusic__fed_state_playlist.title
END",
) )
.bind(user_id) .bind(user_id)
.bind(playlist_sync_id) .bind(playlist_sync_id)
.bind(id) .bind(local_playlist_id)
.bind(title)
.bind(format!("local_materialized:{local_playlist_id}"))
.execute(pool) .execute(pool)
.await?; .await?;
Ok(id) Ok(())
} }
async fn ensure_playlist_for_item( async fn ensure_playlist_for_item(
@@ -3449,6 +3473,40 @@ async fn ensure_playlist_for_item(
ensure_materialized_playlist(pool, user_id, playlist_sync_id, None, &title).await ensure_materialized_playlist(pool, user_id, playlist_sync_id, None, &title).await
} }
async fn ensure_seed_playlist_sync_id(
pool: &sqlx::PgPool,
user_id: i64,
playlist_id: i64,
title: &str,
hlc_ms: i64,
) -> Result<String> {
if let Some(sync_id) = local_playlist_sync_id(pool, user_id, playlist_id).await? {
return Ok(sync_id);
}
let sync_id = format!("webpl_{user_id}_{playlist_id}");
sqlx::query(
"INSERT INTO furumusic__fed_state_playlist
(user_id, playlist_id, local_playlist_id, title, deleted, hlc_ms, op_id)
VALUES ($1, $2, $3, $4, false, $5, $6)
ON CONFLICT (user_id, playlist_id) DO UPDATE SET
local_playlist_id = COALESCE(furumusic__fed_state_playlist.local_playlist_id, EXCLUDED.local_playlist_id),
title = CASE
WHEN furumusic__fed_state_playlist.hlc_ms = 0 THEN EXCLUDED.title
ELSE furumusic__fed_state_playlist.title
END,
deleted = false",
)
.bind(user_id)
.bind(&sync_id)
.bind(playlist_id)
.bind(title)
.bind(hlc_ms)
.bind(format!("local_seed:playlist:{playlist_id}"))
.execute(pool)
.await?;
Ok(sync_id)
}
async fn ensure_local_playlist_sync_id( async fn ensure_local_playlist_sync_id(
pool: &sqlx::PgPool, pool: &sqlx::PgPool,
user_id: i64, user_id: i64,
@@ -3707,6 +3765,7 @@ async fn ops_for_peer(
} }
async fn snapshot(pool: &sqlx::PgPool, user_id: i64) -> Result<SyncSnapshot> { async fn snapshot(pool: &sqlx::PgPool, user_id: i64) -> Result<SyncSnapshot> {
maybe_seed_local_user_state(pool, user_id).await?;
let like_rows = sqlx::query( let like_rows = sqlx::query(
"SELECT content_id, liked, hlc_ms, op_id, fed_json "SELECT content_id, liked, hlc_ms, op_id, fed_json
FROM furumusic__fed_state_like FROM furumusic__fed_state_like