CORE: Improve media paths and player reliability
Build and Publish / Build and Publish Docker Image (push) Successful in 3m3s

This commit is contained in:
Ultradesu
2026-05-27 18:52:17 +03:00
parent fc6090d6a0
commit c43ee02b00
16 changed files with 639 additions and 185 deletions
+98
View File
@@ -38,6 +38,8 @@ const T = {
lastfmDisconnectConfirm: "{{ t.player_lastfm_disconnect_confirm }}",
lastfmConnectFailed: "{{ t.player_lastfm_connect_failed }}",
lastfmDisconnectFailed: "{{ t.player_lastfm_disconnect_failed }}",
connectionLost: "{{ t.player_connection_lost }}",
connectionLostDetail: "{{ t.player_connection_lost_detail }}",
trackWord: "{{ t.player_tracks_count }}",
clientIdle: "{{ t.player_client_idle }}",
active: "{{ t.player_active }}",
@@ -115,6 +117,42 @@ function coverVariantUrl(url, variant) {
}
document.addEventListener('alpine:init', () => {
// -----------------------------------------------------------------------
// Connection monitor
// -----------------------------------------------------------------------
Alpine.store('connection', {
failureCount: 0,
disconnected: false,
threshold: 2,
init() {
if (navigator.onLine === false) {
this.failureCount = this.threshold;
this.disconnected = true;
}
window.addEventListener('online', () => this.recordSuccess());
window.addEventListener('offline', () => this.recordFailure());
},
message() {
return T.connectionLostDetail;
},
recordSuccess() {
this.failureCount = 0;
this.disconnected = false;
},
recordFailure() {
this.failureCount += 1;
if (this.failureCount >= this.threshold) {
this.disconnected = true;
}
},
});
installConnectionFetchMonitor();
// -----------------------------------------------------------------------
// Audio element
// -----------------------------------------------------------------------
@@ -174,10 +212,19 @@ document.addEventListener('alpine:init', () => {
lastfmBusy: false,
init() {
this.cleanLastfmQuery();
this.load();
this.loadLastfm();
},
cleanLastfmQuery() {
const url = new URL(window.location.href);
if (!url.searchParams.has('lastfm')) return;
url.searchParams.delete('lastfm');
const clean = `${url.pathname}${url.search}${url.hash}`;
window.history.replaceState({}, document.title, clean || '/');
},
async load() {
try {
const res = await fetch('/api/player/me');
@@ -447,9 +494,13 @@ document.addEventListener('alpine:init', () => {
const queue = Alpine.store('queue');
if (queue.tracks.length === 0) return;
this._recordHistoryIfListenThresholdReached();
let nextIdx;
if (this.repeatMode === 'one') {
this.seek(0);
this._historyRecorded = false;
this._resetPlaybackTracking();
this.resume();
return;
} else if (this.shuffle) {
@@ -655,6 +706,18 @@ document.addEventListener('alpine:init', () => {
}).catch(() => {});
},
_recordHistoryIfListenThresholdReached() {
if (this._historyRecorded || !this.currentTrack) return false;
this._trackListenedDelta();
const duration = this._trackDuration();
if (duration <= 0) return false;
const listened = Math.floor(Number(this._listenedSeconds || 0));
const threshold = Math.ceil(duration / 2);
if (threshold <= 0 || listened < threshold) return false;
this._recordHistory(true);
return true;
},
_resetPlaybackTracking() {
this._nowPlayingSent = false;
this._playbackStartedAt = null;
@@ -2294,4 +2357,39 @@ document.addEventListener('alpine:init', () => {
},
});
});
function installConnectionFetchMonitor() {
if (window.__furumusicConnectionMonitorInstalled || !window.fetch) return;
window.__furumusicConnectionMonitorInstalled = true;
const nativeFetch = window.fetch.bind(window);
window.fetch = async (...args) => {
const tracked = isTrackedPlayerRequest(args[0]);
try {
const response = await nativeFetch(...args);
if (tracked) {
if (response.status >= 500) {
Alpine.store('connection')?.recordFailure();
} else {
Alpine.store('connection')?.recordSuccess();
}
}
return response;
} catch (error) {
if (tracked) Alpine.store('connection')?.recordFailure();
throw error;
}
};
}
function isTrackedPlayerRequest(input) {
const rawUrl = typeof input === 'string' ? input : input?.url;
if (!rawUrl) return false;
try {
const url = new URL(rawUrl, window.location.href);
return url.origin === window.location.origin && url.pathname.startsWith('/api/player/');
} catch {
return false;
}
}
</script>
+14
View File
@@ -275,6 +275,20 @@
<path d="M4 4.5A2.5 2.5 0 016.5 2H20v20H6.5A2.5 2.5 0 014 19.5z"/>
</svg>
</button>
<div class="connection-alert"
x-show="$store.connection.disconnected"
x-cloak
:title="$store.connection.message()"
role="status"
aria-live="polite">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M2 8.82a15 15 0 0120 0"/>
<path d="M5 12.86a10 10 0 0114 0"/>
<path d="M8.5 16.43a5 5 0 017 0"/>
<line x1="2" y1="2" x2="22" y2="22"/>
</svg>
<span class="connection-alert-text">{{ t.player_connection_lost }}</span>
</div>
<div class="search-bar">
<span class="search-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg></span>
<input id="search-input" type="text" placeholder="{{ t.player_search_placeholder }}"
+40
View File
@@ -1377,6 +1377,37 @@ button.user-stat:hover {
margin-bottom: 20px;
}
.connection-alert {
flex: 0 0 auto;
min-width: 42px;
height: 42px;
padding: 0 12px;
border: 1px solid rgba(248, 113, 113, 0.34);
border-radius: 8px;
background: rgba(127, 29, 29, 0.2);
color: #f87171;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.connection-alert svg {
width: 18px;
height: 18px;
flex: 0 0 auto;
}
.connection-alert-text {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: #fecaca;
font-size: 12px;
font-weight: 700;
}
/* Search bar */
.search-bar {
position: relative;
@@ -2676,6 +2707,15 @@ button.user-stat:hover {
flex: 1 1 auto;
}
.connection-alert {
width: 42px;
padding: 0;
}
.connection-alert-text {
display: none;
}
.mobile-library-btn {
display: flex;
}