Fixed autoplay after fed download

This commit is contained in:
Ultradesu
2026-07-27 17:33:48 +01:00
parent b0d8929b4c
commit 845df4e031
6 changed files with 135 additions and 16 deletions
Generated
+1 -1
View File
@@ -1845,7 +1845,7 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
[[package]] [[package]]
name = "furumusic" name = "furumusic"
version = "0.9.1" version = "0.9.3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-stream", "async-stream",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "furumusic" name = "furumusic"
version = "0.9.2" version = "0.9.3"
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"
+70 -8
View File
@@ -295,7 +295,60 @@ impl Federation {
let dir = storage_root.join("federation"); let dir = storage_root.join("federation");
tokio::fs::create_dir_all(&dir).await?; tokio::fs::create_dir_all(&dir).await?;
let downloaded = let downloaded =
download(&service, owner, item_id, &content_id, &dir, &mut progress).await?; match download(&service, owner, item_id, &content_id, &dir, &mut progress).await {
Ok(downloaded) => downloaded,
Err(primary_error) => {
progress(DownloadProgress {
phase: "discovering",
received: 0,
total: 0,
});
let outcome = service
.search_content_id(&content_id)
.await
.map_err(|err| {
anyhow::anyhow!("{primary_error:#}; content lookup also failed: {err}")
})?;
let mut last_error = primary_error;
let mut downloaded = None;
for item in outcome
.local_results
.into_iter()
.chain(outcome.network_results)
{
if item.kind != music_dht::ItemKind::Track
|| item.content_id.as_deref() != Some(content_id.as_str())
{
continue;
}
let candidate_item_id = hex_encode(item.id.as_bytes());
if item.owner == owner && candidate_item_id == item_id {
continue;
}
match download(
&service,
item.owner,
&candidate_item_id,
&content_id,
&dir,
&mut progress,
)
.await
{
Ok(candidate) => {
downloaded = Some(candidate);
break;
}
Err(err) => last_error = err,
}
}
downloaded.ok_or_else(|| {
anyhow::anyhow!(
"no reachable peer currently provides this track: {last_error:#}"
)
})?
}
};
if !save { if !save {
super::lock(&self.prepared_cache) super::lock(&self.prepared_cache)
.insert(token.clone(), (downloaded.path, downloaded.mime)); .insert(token.clone(), (downloaded.path, downloaded.mime));
@@ -306,15 +359,20 @@ impl Federation {
} }
progress(DownloadProgress { progress(DownloadProgress {
phase: "saving", phase: "saving",
received: 1, received: 0,
total: 1, total: 0,
}); });
let track_id = materialize(&pool, &storage_root, &content_id, downloaded).await?; let track_id = materialize(&pool, &storage_root, &content_id, downloaded).await?;
// The normal periodic sync will publish it; this immediate sync keeps // Materialization is the playback boundary: return the local track
// save-on-listen useful to the federation without waiting a minute. // immediately so the browser can replace the pending queue entry and
if let Err(err) = self.sync_now().await { // start it. Publishing must not keep the prepare stream stuck at 100%
tracing::warn!(track_id, "post-import federation publish failed: {err:#}"); // when a federation peer is slow or offline.
} let federation = std::sync::Arc::clone(self);
tokio::spawn(async move {
if let Err(err) = federation.sync_now().await {
tracing::warn!(track_id, "post-import federation publish failed: {err:#}");
}
});
Ok(PreparedTrack { Ok(PreparedTrack {
local_track_id: Some(track_id), local_track_id: Some(track_id),
stream_url: format!("/api/player/stream/{track_id}"), stream_url: format!("/api/player/stream/{track_id}"),
@@ -362,6 +420,10 @@ fn cache_artwork(federation: &Federation, key: String, artwork: &(Vec<u8>, Strin
); );
} }
fn hex_encode(bytes: &[u8]) -> String {
bytes.iter().map(|byte| format!("{byte:02x}")).collect()
}
async fn download( async fn download(
service: &music_dht::MusicDhtService, service: &music_dht::MusicDhtService,
owner: EndpointId, owner: EndpointId,
+18 -1
View File
@@ -2495,6 +2495,7 @@ document.addEventListener('alpine:init', () => {
federationSearch: { loading: false, error: '', artists: [], releases: [], tracks: [] }, federationSearch: { loading: false, error: '', artists: [], releases: [], tracks: [] },
artistFederation: { loading: false, error: '', releases: [], tracks: [] }, artistFederation: { loading: false, error: '', releases: [], tracks: [] },
federationPreparing: {}, federationPreparing: {},
federationFailures: {},
trackAvailability: {}, trackAvailability: {},
_federatedReleaseCache: {}, _federatedReleaseCache: {},
federationLiked: [], federationLiked: [],
@@ -3863,6 +3864,11 @@ document.addEventListener('alpine:init', () => {
return; return;
} }
if (!contentId || sources.length === 0) return; if (!contentId || sources.length === 0) return;
if (this.federationFailures[contentId]) {
const failures = { ...this.federationFailures };
delete failures[contentId];
this.federationFailures = failures;
}
if (this.federationPreparing[contentId]) { if (this.federationPreparing[contentId]) {
if (options.playAfterPrepare !== false) { if (options.playAfterPrepare !== false) {
this.federationPreparing = { this.federationPreparing = {
@@ -4023,7 +4029,13 @@ document.addEventListener('alpine:init', () => {
Alpine.store('queue').playRelease([playable], 0); Alpine.store('queue').playRelease([playable], 0);
} }
} catch (error) { } catch (error) {
window.alert(error?.message || 'Federated track download failed'); this.federationFailures = {
...this.federationFailures,
[contentId]: {
message: error?.message || 'Federated track is temporarily unavailable',
failedAt: Date.now(),
},
};
} finally { } finally {
const next = { ...this.federationPreparing }; const next = { ...this.federationPreparing };
delete next[contentId]; delete next[contentId];
@@ -4035,12 +4047,17 @@ document.addEventListener('alpine:init', () => {
return this.federationPreparing[contentId] || null; return this.federationPreparing[contentId] || null;
}, },
federationFailure(contentId) {
return this.federationFailures[contentId] || null;
},
federationDownloadTooltip(contentId) { federationDownloadTooltip(contentId) {
const progress = this.federationDownload(contentId); const progress = this.federationDownload(contentId);
if (!progress) return ''; if (!progress) return '';
const phases = { const phases = {
checking: 'Checking local library', checking: 'Checking local library',
connecting: 'Connecting to peer', connecting: 'Connecting to peer',
discovering: 'Looking for another peer',
downloading: 'Downloading', downloading: 'Downloading',
verifying: 'Verifying content ID', verifying: 'Verifying content ID',
saving: 'Adding to library', saving: 'Adding to library',
+25 -5
View File
@@ -562,13 +562,17 @@
<div class="track-row federation-track-row" <div class="track-row federation-track-row"
@dblclick="$store.library.playFederatedTrack(track)"> @dblclick="$store.library.playFederatedTrack(track)">
<span class="track-num federation-track-status"> <span class="track-num federation-track-status">
<template x-if="!$store.library.federationDownload(track.key.content_id) && !$store.library.isTrackLocal(track)"> <template x-if="!$store.library.federationDownload(track.key.content_id) && !$store.library.federationFailure(track.key.content_id) && !$store.library.isTrackLocal(track)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
<circle cx="12" cy="12" r="3"/> <circle cx="12" cy="12" r="3"/>
<path d="M5.6 8.5a7.5 7.5 0 000 7M18.4 8.5a7.5 7.5 0 010 7"/> <path d="M5.6 8.5a7.5 7.5 0 000 7M18.4 8.5a7.5 7.5 0 010 7"/>
<path d="M2.8 5.8a11 11 0 000 12.4M21.2 5.8a11 11 0 010 12.4"/> <path d="M2.8 5.8a11 11 0 000 12.4M21.2 5.8a11 11 0 010 12.4"/>
</svg> </svg>
</template> </template>
<template x-if="$store.library.federationFailure(track.key.content_id) && !$store.library.isTrackLocal(track)">
<span class="federation-error-badge"
:title="$store.library.federationFailure(track.key.content_id).message">!</span>
</template>
<template x-if="$store.library.federationDownload(track.key.content_id)"> <template x-if="$store.library.federationDownload(track.key.content_id)">
<span class="federation-download-progress" <span class="federation-download-progress"
:class="{ indeterminate: !$store.library.federationDownload(track.key.content_id).total }"> :class="{ indeterminate: !$store.library.federationDownload(track.key.content_id).total }">
@@ -905,12 +909,16 @@
<div class="track-row federation-track-row" <div class="track-row federation-track-row"
@dblclick="$store.library.playFederatedTrack(track)"> @dblclick="$store.library.playFederatedTrack(track)">
<span class="track-num federation-track-status"> <span class="track-num federation-track-status">
<template x-if="!$store.library.federationDownload(track.key.content_id) && !$store.library.isTrackLocal(track)"> <template x-if="!$store.library.federationDownload(track.key.content_id) && !$store.library.federationFailure(track.key.content_id) && !$store.library.isTrackLocal(track)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
<circle cx="12" cy="12" r="3"/> <circle cx="12" cy="12" r="3"/>
<path d="M5.6 8.5a7.5 7.5 0 000 7M18.4 8.5a7.5 7.5 0 010 7"/> <path d="M5.6 8.5a7.5 7.5 0 000 7M18.4 8.5a7.5 7.5 0 010 7"/>
</svg> </svg>
</template> </template>
<template x-if="$store.library.federationFailure(track.key.content_id) && !$store.library.isTrackLocal(track)">
<span class="federation-error-badge"
:title="$store.library.federationFailure(track.key.content_id).message">!</span>
</template>
<template x-if="$store.library.federationDownload(track.key.content_id)"> <template x-if="$store.library.federationDownload(track.key.content_id)">
<span class="federation-download-progress" <span class="federation-download-progress"
:class="{ indeterminate: !$store.library.federationDownload(track.key.content_id).total }"> :class="{ indeterminate: !$store.library.federationDownload(track.key.content_id).total }">
@@ -1158,13 +1166,17 @@
<template x-if="!track.federation_pending"> <template x-if="!track.federation_pending">
<span x-text="track.track_number || (idx + 1)"></span> <span x-text="track.track_number || (idx + 1)"></span>
</template> </template>
<template x-if="track.federation_pending && !$store.library.federationDownload(track.content_id) && !$store.library.isTrackLocal(track._federationTrack)"> <template x-if="track.federation_pending && !$store.library.federationDownload(track.content_id) && !$store.library.federationFailure(track.content_id) && !$store.library.isTrackLocal(track._federationTrack)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
<circle cx="12" cy="12" r="3"/> <circle cx="12" cy="12" r="3"/>
<path d="M5.6 8.5a7.5 7.5 0 000 7M18.4 8.5a7.5 7.5 0 010 7"/> <path d="M5.6 8.5a7.5 7.5 0 000 7M18.4 8.5a7.5 7.5 0 010 7"/>
<path d="M2.8 5.8a11 11 0 000 12.4M21.2 5.8a11 11 0 010 12.4"/> <path d="M2.8 5.8a11 11 0 000 12.4M21.2 5.8a11 11 0 010 12.4"/>
</svg> </svg>
</template> </template>
<template x-if="track.federation_pending && $store.library.federationFailure(track.content_id) && !$store.library.isTrackLocal(track._federationTrack)">
<span class="federation-error-badge"
:title="$store.library.federationFailure(track.content_id).message">!</span>
</template>
<template x-if="track.federation_pending && !$store.library.federationDownload(track.content_id) && $store.library.isTrackLocal(track._federationTrack)"> <template x-if="track.federation_pending && !$store.library.federationDownload(track.content_id) && $store.library.isTrackLocal(track._federationTrack)">
<span x-text="track.track_number || (idx + 1)"></span> <span x-text="track.track_number || (idx + 1)"></span>
</template> </template>
@@ -1312,13 +1324,17 @@
<template x-if="!track.federation_pending || $store.library.isTrackLocal(track._federationTrack)"> <template x-if="!track.federation_pending || $store.library.isTrackLocal(track._federationTrack)">
<span x-text="idx + 1"></span> <span x-text="idx + 1"></span>
</template> </template>
<template x-if="track.federation_pending && !$store.library.federationDownload(track.content_id) && !$store.library.isTrackLocal(track._federationTrack)"> <template x-if="track.federation_pending && !$store.library.federationDownload(track.content_id) && !$store.library.federationFailure(track.content_id) && !$store.library.isTrackLocal(track._federationTrack)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
<circle cx="12" cy="12" r="3"/> <circle cx="12" cy="12" r="3"/>
<path d="M5.6 8.5a7.5 7.5 0 000 7M18.4 8.5a7.5 7.5 0 010 7"/> <path d="M5.6 8.5a7.5 7.5 0 000 7M18.4 8.5a7.5 7.5 0 010 7"/>
<path d="M2.8 5.8a11 11 0 000 12.4M21.2 5.8a11 11 0 010 12.4"/> <path d="M2.8 5.8a11 11 0 000 12.4M21.2 5.8a11 11 0 010 12.4"/>
</svg> </svg>
</template> </template>
<template x-if="track.federation_pending && $store.library.federationFailure(track.content_id) && !$store.library.isTrackLocal(track._federationTrack)">
<span class="federation-error-badge"
:title="$store.library.federationFailure(track.content_id).message">!</span>
</template>
<template x-if="track.federation_pending && $store.library.federationDownload(track.content_id)"> <template x-if="track.federation_pending && $store.library.federationDownload(track.content_id)">
<span class="federation-download-progress" <span class="federation-download-progress"
:class="{ indeterminate: !$store.library.federationDownload(track.content_id).total }"> :class="{ indeterminate: !$store.library.federationDownload(track.content_id).total }">
@@ -1452,12 +1468,16 @@
<span class="queue-federation-status federation-track-status" <span class="queue-federation-status federation-track-status"
x-show="item.track.federation_pending" x-show="item.track.federation_pending"
x-cloak> x-cloak>
<template x-if="!$store.library.federationDownload(item.track.content_id)"> <template x-if="!$store.library.federationDownload(item.track.content_id) && !$store.library.federationFailure(item.track.content_id)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
<circle cx="12" cy="12" r="3"/> <circle cx="12" cy="12" r="3"/>
<path d="M5.6 8.5a7.5 7.5 0 000 7M18.4 8.5a7.5 7.5 0 010 7"/> <path d="M5.6 8.5a7.5 7.5 0 000 7M18.4 8.5a7.5 7.5 0 010 7"/>
</svg> </svg>
</template> </template>
<template x-if="$store.library.federationFailure(item.track.content_id)">
<span class="federation-error-badge"
:title="$store.library.federationFailure(item.track.content_id).message">!</span>
</template>
<template x-if="$store.library.federationDownload(item.track.content_id)"> <template x-if="$store.library.federationDownload(item.track.content_id)">
<span class="federation-download-progress" <span class="federation-download-progress"
:class="{ indeterminate: !$store.library.federationDownload(item.track.content_id).total }"> :class="{ indeterminate: !$store.library.federationDownload(item.track.content_id).total }">
+20
View File
@@ -2729,6 +2729,26 @@ button.user-stat:hover {
height: 18px; height: 18px;
color: var(--accent); color: var(--accent);
} }
.federation-error-badge {
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
border: 1px solid rgba(235, 105, 105, .7);
border-radius: 999px;
background: rgba(190, 55, 55, .18);
color: #ffb0b0;
font-size: 11px;
font-weight: 850;
line-height: 1;
cursor: help;
}
.queue-federation-status .federation-error-badge {
width: 16px;
height: 16px;
font-size: 10px;
}
.federation-download-progress { .federation-download-progress {
position: relative; position: relative;
display: inline-flex; display: inline-flex;