Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0d8929b4c |
Generated
+1
-1
@@ -1845,7 +1845,7 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "furumusic"
|
name = "furumusic"
|
||||||
version = "0.9.0"
|
version = "0.9.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-stream",
|
"async-stream",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "furumusic"
|
name = "furumusic"
|
||||||
version = "0.9.1"
|
version = "0.9.2"
|
||||||
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"
|
||||||
|
|
||||||
|
|||||||
@@ -1712,7 +1712,7 @@ async fn local_seed_needs_metadata_backfill(pool: &sqlx::PgPool, user_id: i64) -
|
|||||||
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(
|
||||||
"SELECT ult.track_id, c.content_id
|
"SELECT ult.track_id, ult.created_at, c.content_id
|
||||||
FROM furumusic__user_liked_track ult
|
FROM furumusic__user_liked_track ult
|
||||||
JOIN furumusic__track t ON t.id = ult.track_id
|
JOIN furumusic__track t ON t.id = ult.track_id
|
||||||
JOIN furumusic__media_file m ON m.id = t.audio_file_id
|
JOIN furumusic__media_file m ON m.id = t.audio_file_id
|
||||||
@@ -1729,6 +1729,7 @@ async fn seed_local_user_state(pool: &sqlx::PgPool, user_id: i64) -> Result<()>
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let track_id: i64 = row.get("track_id");
|
let track_id: i64 = row.get("track_id");
|
||||||
|
let liked_hlc_ms = timestamp_from_iso(&row.get::<String, _>("created_at")).unwrap_or(now);
|
||||||
let fed_json = synced_fed_track_for_track(pool, track_id, &content_id)
|
let fed_json = synced_fed_track_for_track(pool, track_id, &content_id)
|
||||||
.await?
|
.await?
|
||||||
.map(serde_json::to_value)
|
.map(serde_json::to_value)
|
||||||
@@ -1743,7 +1744,7 @@ async fn seed_local_user_state(pool: &sqlx::PgPool, user_id: i64) -> Result<()>
|
|||||||
)
|
)
|
||||||
.bind(user_id)
|
.bind(user_id)
|
||||||
.bind(&content_id)
|
.bind(&content_id)
|
||||||
.bind(now)
|
.bind(liked_hlc_ms)
|
||||||
.bind(format!("local_seed:like:{track_id}"))
|
.bind(format!("local_seed:like:{track_id}"))
|
||||||
.bind(track_id)
|
.bind(track_id)
|
||||||
.bind(fed_json)
|
.bind(fed_json)
|
||||||
@@ -4536,6 +4537,12 @@ fn iso_from_ms(ms: i64) -> String {
|
|||||||
.to_string()
|
.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn timestamp_from_iso(value: &str) -> Option<i64> {
|
||||||
|
chrono::DateTime::parse_from_rfc3339(value)
|
||||||
|
.ok()
|
||||||
|
.map(|value| value.timestamp_millis())
|
||||||
|
}
|
||||||
|
|
||||||
fn now_label() -> String {
|
fn now_label() -> String {
|
||||||
let secs = (now_ms() / 1000).max(0);
|
let secs = (now_ms() / 1000).max(0);
|
||||||
format!(
|
format!(
|
||||||
|
|||||||
@@ -78,6 +78,8 @@ pub(super) struct TrackItem {
|
|||||||
#[derive(Debug, Serialize, JsonSchema)]
|
#[derive(Debug, Serialize, JsonSchema)]
|
||||||
pub(super) struct PlaylistTrackItem {
|
pub(super) struct PlaylistTrackItem {
|
||||||
pub(super) playlist_track_id: Option<i64>,
|
pub(super) playlist_track_id: Option<i64>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub(super) sort_key: Option<i64>,
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub(super) track: TrackItem,
|
pub(super) track: TrackItem,
|
||||||
}
|
}
|
||||||
|
|||||||
+40
-3
@@ -4111,6 +4111,7 @@ async fn build_playlist_track_items(
|
|||||||
.zip(playlist_track_ids)
|
.zip(playlist_track_ids)
|
||||||
.map(|(track, playlist_track_id)| PlaylistTrackItem {
|
.map(|(track, playlist_track_id)| PlaylistTrackItem {
|
||||||
playlist_track_id,
|
playlist_track_id,
|
||||||
|
sort_key: None,
|
||||||
track,
|
track,
|
||||||
})
|
})
|
||||||
.collect())
|
.collect())
|
||||||
@@ -4309,14 +4310,50 @@ async fn likes_playlist_handler(
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| cot::Error::internal(e.to_string()))?;
|
.map_err(|e| cot::Error::internal(e.to_string()))?;
|
||||||
|
|
||||||
let track_items = build_track_items(tracks, pool)
|
let order_rows = sqlx::query(
|
||||||
|
"SELECT ult.track_id,
|
||||||
|
COALESCE(
|
||||||
|
fsl.hlc_ms,
|
||||||
|
(EXTRACT(EPOCH FROM ult.created_at::timestamptz) * 1000)::bigint,
|
||||||
|
0
|
||||||
|
) AS sort_key
|
||||||
|
FROM furumusic__user_liked_track ult
|
||||||
|
JOIN furumusic__track t ON t.id = ult.track_id
|
||||||
|
LEFT JOIN furumusic__media_file m ON m.id = t.audio_file_id
|
||||||
|
LEFT JOIN furumusic__federation_content_id_cache c
|
||||||
|
ON c.media_file_id = m.id AND c.sha256_hash = m.sha256_hash
|
||||||
|
LEFT JOIN furumusic__fed_state_like fsl
|
||||||
|
ON fsl.user_id = ult.user_id
|
||||||
|
AND fsl.content_id = c.content_id
|
||||||
|
AND fsl.liked = true
|
||||||
|
WHERE ult.user_id = $1",
|
||||||
|
)
|
||||||
|
.bind(user_id)
|
||||||
|
.fetch_all(pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| cot::Error::internal(e.to_string()))?;
|
||||||
|
let order_by_track = order_rows
|
||||||
|
.into_iter()
|
||||||
|
.map(|row| (row.get::<i64, _>("track_id"), row.get::<i64, _>("sort_key")))
|
||||||
|
.collect::<HashMap<_, _>>();
|
||||||
|
|
||||||
|
let mut track_items = build_track_items(tracks, pool)
|
||||||
.await?
|
.await?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|track| PlaylistTrackItem {
|
.map(|track| PlaylistTrackItem {
|
||||||
playlist_track_id: None,
|
playlist_track_id: None,
|
||||||
|
sort_key: order_by_track.get(&track.id).copied(),
|
||||||
track,
|
track,
|
||||||
})
|
})
|
||||||
.collect();
|
.collect::<Vec<_>>();
|
||||||
|
track_items.sort_by(|left, right| {
|
||||||
|
right
|
||||||
|
.sort_key
|
||||||
|
.cmp(&left.sort_key)
|
||||||
|
.then_with(|| left.track.content_id.cmp(&right.track.content_id))
|
||||||
|
.then_with(|| left.track.title.cmp(&right.track.title))
|
||||||
|
.then_with(|| left.track.id.cmp(&right.track.id))
|
||||||
|
});
|
||||||
|
|
||||||
Json(PlaylistDetail {
|
Json(PlaylistDetail {
|
||||||
id: -1,
|
id: -1,
|
||||||
@@ -6293,7 +6330,7 @@ async fn federation_playlist_tracks_handler(
|
|||||||
};
|
};
|
||||||
let rows = if path.id == -1 {
|
let rows = if path.id == -1 {
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
"SELECT content_id, 0::bigint AS position, fed_json
|
"SELECT content_id, hlc_ms AS position, fed_json
|
||||||
FROM furumusic__fed_state_like
|
FROM furumusic__fed_state_like
|
||||||
WHERE user_id = $1 AND liked = true
|
WHERE user_id = $1 AND liked = true
|
||||||
AND local_track_id IS NULL AND fed_json IS NOT NULL
|
AND local_track_id IS NULL AND fed_json IS NOT NULL
|
||||||
|
|||||||
@@ -3288,10 +3288,17 @@ document.addEventListener('alpine:init', () => {
|
|||||||
if (federationResponse.ok) {
|
if (federationResponse.ok) {
|
||||||
const references = await federationResponse.json();
|
const references = await federationResponse.json();
|
||||||
const federated = (references || [])
|
const federated = (references || [])
|
||||||
.map(reference => this.federationTrackFromWire(reference.federation))
|
.map(reference => {
|
||||||
.filter(Boolean)
|
const track = this.federationTrackFromWire(reference.federation);
|
||||||
.map(track => this.federationQueueTrack(track));
|
if (!track) return null;
|
||||||
|
return {
|
||||||
|
...this.federationQueueTrack(track),
|
||||||
|
sort_key: Number(reference.position || 0),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
playlist.tracks = [...(playlist.tracks || []), ...federated];
|
playlist.tracks = [...(playlist.tracks || []), ...federated];
|
||||||
|
if (Number(id) === -1) playlist.tracks = this.sortLikeTracks(playlist.tracks);
|
||||||
}
|
}
|
||||||
this.currentPlaylist = playlist;
|
this.currentPlaylist = playlist;
|
||||||
} catch {}
|
} catch {}
|
||||||
@@ -3777,7 +3784,13 @@ document.addEventListener('alpine:init', () => {
|
|||||||
if (liked && !this.currentPlaylist.tracks.some(item => item.content_id === contentId)) {
|
if (liked && !this.currentPlaylist.tracks.some(item => item.content_id === contentId)) {
|
||||||
this.currentPlaylist = {
|
this.currentPlaylist = {
|
||||||
...this.currentPlaylist,
|
...this.currentPlaylist,
|
||||||
tracks: [...this.currentPlaylist.tracks, this.federationQueueTrack(track)],
|
tracks: this.sortLikeTracks([
|
||||||
|
...this.currentPlaylist.tracks,
|
||||||
|
{
|
||||||
|
...this.federationQueueTrack(track),
|
||||||
|
sort_key: Date.now(),
|
||||||
|
},
|
||||||
|
]),
|
||||||
};
|
};
|
||||||
} else if (!liked) {
|
} else if (!liked) {
|
||||||
this.currentPlaylist = {
|
this.currentPlaylist = {
|
||||||
@@ -3788,6 +3801,22 @@ document.addEventListener('alpine:init', () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
sortLikeTracks(tracks) {
|
||||||
|
return [...(tracks || [])].sort((left, right) => {
|
||||||
|
const order = Number(right.sort_key || 0) - Number(left.sort_key || 0);
|
||||||
|
if (order) return order;
|
||||||
|
const stable = String(left.content_id || left.id || '')
|
||||||
|
.localeCompare(String(right.content_id || right.id || ''));
|
||||||
|
if (stable) return stable;
|
||||||
|
const title = String(left.title || '').localeCompare(
|
||||||
|
String(right.title || ''),
|
||||||
|
undefined,
|
||||||
|
{ sensitivity: 'base' },
|
||||||
|
);
|
||||||
|
return title;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
isMixedReleaseLiked(release) {
|
isMixedReleaseLiked(release) {
|
||||||
const tracks = release?.tracks || [];
|
const tracks = release?.tracks || [];
|
||||||
if (!tracks.length) return false;
|
if (!tracks.length) return false;
|
||||||
|
|||||||
Reference in New Issue
Block a user