PLAYER: Added generated playlists feature
Build and Publish / Build and Publish Docker Image (push) Successful in 3m5s

This commit is contained in:
2026-05-29 17:04:30 +03:00
parent 496c501076
commit e1a4b6267f
18 changed files with 2628 additions and 446 deletions
+61 -5
View File
@@ -42,6 +42,8 @@ const T = {
lastfmDisconnectConfirm: "{{ t.player_lastfm_disconnect_confirm }}",
lastfmConnectFailed: "{{ t.player_lastfm_connect_failed }}",
lastfmDisconnectFailed: "{{ t.player_lastfm_disconnect_failed }}",
startRadio: "{{ t.player_start_radio }}",
radioFailed: "{{ t.player_radio_failed }}",
connectionLost: "{{ t.player_connection_lost }}",
connectionLostDetail: "{{ t.player_connection_lost_detail }}",
trackWord: "{{ t.player_tracks_count }}",
@@ -304,27 +306,46 @@ document.addEventListener('alpine:init', () => {
Alpine.store('info', {
modal: null,
open(title, body) {
open(title, body, actions = []) {
if (Array.isArray(body)) {
this.openRows(title, body);
this.openRows(title, body, actions);
return;
}
this.modal = {
title: title || T.info,
body: body || T.noDetails,
rows: null,
actions: actions || [],
};
},
openRows(title, rows) {
openRows(title, rows, actions = []) {
this.modal = {
title: title || T.info,
body: '',
rows: (rows || []).filter(row => row && ((row.value !== undefined && row.value !== null && row.value !== '') || (row.links && row.links.length))),
actions: actions || [],
};
},
close() {
this.modal = null;
},
async runAction(actionOrIndex) {
const index = Number.isInteger(actionOrIndex) ? actionOrIndex : this.modal?.actions?.indexOf(actionOrIndex);
const action = index >= 0 ? this.modal?.actions?.[index] : actionOrIndex;
if (!action || action.busy === true || typeof action.run !== 'function') return;
if (index >= 0 && this.modal?.actions) {
this.modal.actions[index] = { ...action, busy: true };
this.modal.actions = [...this.modal.actions];
}
try {
await action.run();
} finally {
if (index >= 0 && this.modal?.actions?.[index]) {
this.modal.actions[index] = { ...this.modal.actions[index], busy: false };
this.modal.actions = [...this.modal.actions];
}
}
},
navigate(link) {
if (!link || !link.id) return;
this.close();
@@ -488,6 +509,16 @@ document.addEventListener('alpine:init', () => {
return Math.max(1, Math.ceil(this.total / this.perPage));
},
tracks() {
return (this.items || []).map(item => item.track).filter(Boolean);
},
playFrom(index) {
const tracks = this.tracks();
if (!tracks.length || index < 0 || index >= tracks.length) return;
Alpine.store('queue').playRelease(tracks, index);
},
async load(page) {
page = Math.max(1, page || 1);
this.loading = true;
@@ -2117,8 +2148,25 @@ document.addEventListener('alpine:init', () => {
return `${T.lastfmRating}: ${Math.round(value)}\n${this.trackInfo(track)}`;
},
async startRadio(kind, id) {
if (!kind || !id) return;
try {
const res = await fetch(`/api/player/radio/${encodeURIComponent(kind)}/${encodeURIComponent(id)}`);
if (!res.ok) throw new Error('radio failed');
const tracks = await res.json();
if (!Array.isArray(tracks) || tracks.length === 0) throw new Error('radio empty');
Alpine.store('queue').playRelease(tracks, 0);
Alpine.store('info').close();
} catch (err) {
console.warn(err);
window.alert(T.radioFailed);
}
},
openTrackInfo(track) {
Alpine.store('info').openRows(T.trackInfoTitle, this.trackInfoRows(track));
Alpine.store('info').openRows(T.trackInfoTitle, this.trackInfoRows(track), [
{ label: T.startRadio, run: () => this.startRadio('track', track?.id) },
]);
},
uploadersInfo(uploaders) {
@@ -2142,7 +2190,9 @@ document.addEventListener('alpine:init', () => {
},
openReleaseInfo(release) {
Alpine.store('info').openRows(T.releaseInfoTitle, this.releaseInfoRows(release));
Alpine.store('info').openRows(T.releaseInfoTitle, this.releaseInfoRows(release), [
{ label: T.startRadio, run: () => this.startRadio('release', release?.id) },
]);
},
infoLinks(items, type) {
@@ -2230,6 +2280,12 @@ document.addEventListener('alpine:init', () => {
return rows;
},
playArtistTopTracks() {
const tracks = this.currentArtist?.top_tracks || [];
if (!tracks.length) return;
Alpine.store('queue').playRelease(tracks, 0);
},
async openRelease(id, options = {}) {
this._beginNavigation('#release/' + id, options);
this.searchQuery = '';