feat(web+android): Künstlern folgen, Alben merken, Top Songs per Künstler-ID
Folgen ist kein eigener Speicher, sondern Navidromes Stern auf dem Künstler (star&id=<ndArtistId>) — Navidrome wirft id, albumId und artistId beim Markieren in eine Liste und behandelt sie gleich, der vorhandene Aufruf trug also ohne Änderung. Zustand aus `starred` in getArtist/getAlbum, beide Plattformen schalten optimistisch um. Android nutzt den vorhandenen StarState, der ohnehin über IDs arbeitet. Dazu: getTopSongs bekommt seit Navidrome 0.64 zusätzlich `id`. Der Namensweg sucht mit LIKE und Max:1, bei gleichnamigen Künstlern gewann der erste Treffer. Jetzt id + artist zusammen; in Erkunden bleibt es beim Namen, weil die artistId dort Deezers ist. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
de674dbe3f
commit
5f043beb5d
11 files changed
+177
-11
No files matched your search
+68
-2
@@ -1,6 +1,6 @@
|
||||
import { S } from '../state.js';
|
||||
import { esc, toast, fmtCount } from '../utils.js';
|
||||
import { api, coverUrl, deezerSearch, deezerLookupAlbum, deezerLookupArtistSongs, deezerLookupArtistAlbums, deezerArtistImage, deezerRelatedArtists, fetchRomajiIndex, dispArtistById, dispAlbumName, cacheArtistSortNames, ensureArtistSortMap, getArtistOverride } from '../api.js';
|
||||
import { api, invalidateApiCache, coverUrl, deezerSearch, deezerLookupAlbum, deezerLookupArtistSongs, deezerLookupArtistAlbums, deezerArtistImage, deezerRelatedArtists, fetchRomajiIndex, dispArtistById, dispAlbumName, cacheArtistSortNames, ensureArtistSortMap, getArtistOverride } from '../api.js';
|
||||
import { openArtistPin } from '../ui/artistPin.js';
|
||||
import { makeCard, makeAlbumCard, makeGlobalTrackItem, makeSongItem } from '../ui/components.js';
|
||||
import { playQueue, dlSongLocal, toggleStar, parseArtists } from '../player/player.js';
|
||||
@@ -112,7 +112,11 @@ export async function loadArtistDetail(artist) {
|
||||
: hasND ? getArtistImage(artist.id) : Promise.resolve(null);
|
||||
const albumP = hasND ? api('getArtist', `&id=${artist.id}`) : Promise.resolve(null);
|
||||
const infoP = hasND ? api('getArtistInfo2',`&id=${artist.id}&count=8`) : Promise.resolve(null);
|
||||
const topP = hasND ? api('getTopSongs', `&artist=${encodeURIComponent(artist.name)}&count=10`) : Promise.resolve(null);
|
||||
// id + artist zusammen: Navidrome nimmt seit 0.64 die Kuenstler-ID und faellt nur dann auf den
|
||||
// Namen zurueck, wenn es sie nicht kennt (aeltere Versionen ignorieren id einfach). Der Namensweg
|
||||
// sucht mit LIKE und Max:1 — bei zwei gleichnamigen Kuenstlern in der Bibliothek gewinnt also der
|
||||
// erste Treffer, und das war nicht zwingend der gemeinte.
|
||||
const topP = hasND ? api('getTopSongs', `&id=${encodeURIComponent(artist.id)}&artist=${encodeURIComponent(artist.name)}&count=10`) : Promise.resolve(null);
|
||||
const iArtistP = knownDeezer ? Promise.resolve([{ artistId: knownDeezer }]) : deezerSearch(artist.name, 'musicArtist', 5);
|
||||
const eventsP = fetchEvents(artist.name);
|
||||
|
||||
@@ -331,6 +335,30 @@ export async function loadArtistDetail(artist) {
|
||||
evBtn.innerHTML = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg> Konzerte${events.length ? ` <span style="font-size:11px;opacity:.7">(${events.length})</span>` : ''}`;
|
||||
evBtn.onclick = () => openEventsModal(events, artist.name);
|
||||
actionBar.appendChild(evBtn);
|
||||
|
||||
// Folgen = Navidromes Stern auf dem Künstler. Kein eigener Speicher, sofort auf allen Geräten
|
||||
// und in Navidromes eigener Oberfläche sichtbar.
|
||||
if (hasND && artist.id) {
|
||||
const folgeBtn = document.createElement('button');
|
||||
folgeBtn.className = 'artist-action-btn';
|
||||
actionBar.appendChild(folgeBtn);
|
||||
_verdrahteMerken(
|
||||
folgeBtn, artist.id, _ndArtist?.starred,
|
||||
'Folge ich', 'Folgen',
|
||||
`❤ Du folgst ${artist.name}`, `Folgst ${artist.name} nicht mehr`,
|
||||
);
|
||||
// Ohne Pinnung läuft die Deezer-Zuordnung über den Namen — bei gleichnamigen Künstlern
|
||||
// bekäme man dann dauerhaft die Neuheiten des Falschen gemeldet.
|
||||
if (!_pin && !_ndArtist?.starred) {
|
||||
folgeBtn.addEventListener('click', () => {
|
||||
setTimeout(() => {
|
||||
if (folgeBtn.classList.contains('is-starred')) {
|
||||
toast('Tipp: Über ⚙ im Kopfbereich den richtigen Deezer-Künstler festpinnen');
|
||||
}
|
||||
}, 900);
|
||||
}, { once: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(albEl) _renderArtistAlbums(albEl, ndAlbums, iAlbums, artist);
|
||||
@@ -479,6 +507,40 @@ export async function loadArtistAllSongs({ albums, artist }) {
|
||||
});
|
||||
}
|
||||
|
||||
// ─── MARKIEREN (Künstler + Album) ───
|
||||
// Navidrome wirft beim Markieren `id`, `albumId` und `artistId` in eine Liste und behandelt sie
|
||||
// gleich (media_annotation.go) — derselbe Aufruf taugt also für Songs, Alben und Künstler. Der
|
||||
// Zustand kommt aus `starred` in getArtist/getAlbum.
|
||||
const _HERZ = (voll) => `<svg width="13" height="13" viewBox="0 0 24 24" fill="${voll ? 'currentColor' : 'none'}" stroke="currentColor" stroke-width="2"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>`;
|
||||
|
||||
/**
|
||||
* Verdrahtet einen Knopf als Markieren-Umschalter. Optimistisch: der Knopf springt sofort um und
|
||||
* kehrt zurück, wenn der Aufruf scheitert — sonst fühlt sich das Markieren träge an.
|
||||
*/
|
||||
function _verdrahteMerken(btn, id, markiert, textAn, textAus, meldungAn, meldungAus) {
|
||||
if (!btn || !id) return;
|
||||
let an = !!markiert;
|
||||
const malen = () => {
|
||||
const label = btn.querySelector('span');
|
||||
btn.innerHTML = _HERZ(an) + `<span>${an ? textAn : textAus}</span>`;
|
||||
btn.classList.toggle('is-starred', an);
|
||||
void label;
|
||||
};
|
||||
malen();
|
||||
btn.onclick = async () => {
|
||||
const vorher = an;
|
||||
an = !an; malen();
|
||||
try {
|
||||
await api(vorher ? 'unstar' : 'star', `&id=${encodeURIComponent(id)}`);
|
||||
invalidateApiCache('getStarred2');
|
||||
toast(an ? meldungAn : meldungAus);
|
||||
} catch (e) {
|
||||
an = vorher; malen();
|
||||
toast('Fehler: ' + e.message);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export async function loadDeezerAlbumDetail(album) {
|
||||
document.getElementById('deezer-album-back-btn').onclick = () => _navGoBack();
|
||||
document.getElementById('deezer-album-detail-name').textContent = album.collectionName || '—';
|
||||
@@ -604,6 +666,10 @@ export async function loadAlbumDetail(album) {
|
||||
const songs = r.album?.song || [];
|
||||
const totalMin = Math.round(songs.reduce((s,t) => s+(t.duration||0), 0) / 60);
|
||||
document.getElementById('album-detail-stats').textContent = `${songs.length} Titel · ${r.album.year||''} · ${totalMin} Min.`;
|
||||
_verdrahteMerken(
|
||||
document.getElementById('btn-star-album'), album.id, r.album?.starred,
|
||||
'Gemerkt', 'Merken', '❤ Album gemerkt', 'Aus den Favoriten entfernt',
|
||||
);
|
||||
// r.album has sortName (album sort tag) — use it; nav album may not have sortName
|
||||
document.getElementById('album-detail-name').textContent = dispAlbumName(r.album);
|
||||
// Album artist comes from the album's own album-artist tags (r.album.artists / .artist), NOT the
|
||||
|
||||
Reference in new issue
Block a user