CORE: Improve media paths and player reliability
Build and Publish / Build and Publish Docker Image (push) Successful in 3m3s
Build and Publish / Build and Publish Docker Image (push) Successful in 3m3s
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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 }}"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user