Jellyfin music integration in MusicPlayer
This commit is contained in:
parent
2a8c3611a4
commit
e614cb387a
3922
css/main.css
3922
css/main.css
File diff suppressed because it is too large
Load Diff
59
js/api.js
59
js/api.js
@ -95,7 +95,8 @@ export async function getMusicUrlsFromPlex(token, protocolo, ip, puerto, artista
|
|||||||
year: track.getAttribute("parentYear") || track.getAttribute("year"),
|
year: track.getAttribute("parentYear") || track.getAttribute("year"),
|
||||||
genre: Array.from(track.querySelectorAll("Genre")).map(g => g.getAttribute('tag')).join(', ') || '',
|
genre: Array.from(track.querySelectorAll("Genre")).map(g => g.getAttribute('tag')).join(', ') || '',
|
||||||
index: parseInt(track.getAttribute("index") || 0, 10),
|
index: parseInt(track.getAttribute("index") || 0, 10),
|
||||||
albumIndex: parseInt(track.getAttribute("parentIndex") || 0, 10)
|
albumIndex: parseInt(track.getAttribute("parentIndex") || 0, 10),
|
||||||
|
trackIndex: parseInt(track.getAttribute("index") || 0, 10)
|
||||||
};
|
};
|
||||||
}).filter(track => track !== null);
|
}).filter(track => track !== null);
|
||||||
|
|
||||||
@ -114,6 +115,62 @@ export async function getMusicUrlsFromPlex(token, protocolo, ip, puerto, artista
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getMusicUrlsFromJellyfin(serverUrl, userId, token, artistId) {
|
||||||
|
try {
|
||||||
|
const albumsUrl = `${serverUrl}/Users/${userId}/Items?ParentId=${artistId}&IncludeItemTypes=MusicAlbum&Recursive=true&Fields=ImageTags`;
|
||||||
|
const albumsResponse = await fetch(albumsUrl, { headers: { 'X-Emby-Token': token } });
|
||||||
|
if (!albumsResponse.ok) throw new Error(`Failed to fetch albums: ${albumsResponse.status}`);
|
||||||
|
const albumsData = await albumsResponse.json();
|
||||||
|
|
||||||
|
let allTracks = [];
|
||||||
|
|
||||||
|
for (const album of albumsData.Items) {
|
||||||
|
const songsUrl = `${serverUrl}/Users/${userId}/Items?ParentId=${album.Id}&IncludeItemTypes=Audio&Recursive=true&Fields=MediaSources,ImageTags`;
|
||||||
|
const songsResponse = await fetch(songsUrl, { headers: { 'X-Emby-Token': token } });
|
||||||
|
if (!songsResponse.ok) continue;
|
||||||
|
const songsData = await songsResponse.json();
|
||||||
|
|
||||||
|
const albumTracks = songsData.Items.map(track => {
|
||||||
|
const source = track.MediaSources?.[0];
|
||||||
|
if (!source) return null;
|
||||||
|
|
||||||
|
const streamUrl = `${serverUrl}/Audio/${track.Id}/stream.${source.Container || 'mp3'}?api_key=${token}&static=true`;
|
||||||
|
const coverUrl = album.ImageTags?.Primary ? `${serverUrl}/Items/${album.Id}/Images/Primary?tag=${album.ImageTags.Primary}` : 'img/no-poster.png';
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: streamUrl,
|
||||||
|
titulo: track.Name || 'Pista desconocida',
|
||||||
|
album: album.Name || 'Álbum desconocido',
|
||||||
|
artista: track.AlbumArtist || 'Artista desconocido',
|
||||||
|
cover: coverUrl,
|
||||||
|
extension: source.Container || "mp3",
|
||||||
|
id: track.Id,
|
||||||
|
artistId: artistId,
|
||||||
|
year: album.ProductionYear,
|
||||||
|
genre: track.Genres?.join(', ') || '',
|
||||||
|
index: track.IndexNumber || 0,
|
||||||
|
albumIndex: album.IndexNumber || 0,
|
||||||
|
trackIndex: track.IndexNumber || 0
|
||||||
|
};
|
||||||
|
}).filter(track => track !== null);
|
||||||
|
allTracks.push(...albumTracks);
|
||||||
|
}
|
||||||
|
|
||||||
|
allTracks.sort((a, b) => {
|
||||||
|
if (a.albumIndex !== b.albumIndex) {
|
||||||
|
return a.albumIndex - b.albumIndex;
|
||||||
|
}
|
||||||
|
return a.index - b.index;
|
||||||
|
});
|
||||||
|
|
||||||
|
return allTracks;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in getMusicUrlsFromJellyfin:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchAllStreamsFromPlex(busqueda, tipoContenido) {
|
export async function fetchAllStreamsFromPlex(busqueda, tipoContenido) {
|
||||||
if (!busqueda || !tipoContenido) return { success: false, streams: [], message: _('invalidStreamInfo') };
|
if (!busqueda || !tipoContenido) return { success: false, streams: [], message: _('invalidStreamInfo') };
|
||||||
if (!state.db) return { success: false, streams: [], message: _('dbUnavailableForStreams') };
|
if (!state.db) return { success: false, streams: [], message: _('dbUnavailableForStreams') };
|
||||||
|
105
js/jellyfin.js
105
js/jellyfin.js
@ -1,5 +1,5 @@
|
|||||||
import { state } from './state.js';
|
import { state } from './state.js';
|
||||||
import { addItemsToStore, clearStore } from './db.js';
|
import { addItemsToStore, clearStore, getFromDB } from './db.js';
|
||||||
import { showNotification, _, emitirEventoActualizacion } from './utils.js';
|
import { showNotification, _, emitirEventoActualizacion } from './utils.js';
|
||||||
|
|
||||||
async function authenticateJellyfin(url, username, password) {
|
async function authenticateJellyfin(url, username, password) {
|
||||||
@ -50,28 +50,34 @@ export async function fetchLibraryViews(url, userId, apiKey) {
|
|||||||
|
|
||||||
|
|
||||||
export async function fetchItemsFromLibrary(url, userId, apiKey, library) {
|
export async function fetchItemsFromLibrary(url, userId, apiKey, library) {
|
||||||
const itemsUrl = `${url}/Users/${userId}/Items?ParentId=${library.Id}&recursive=true&fields=ProductionYear,RunTimeTicks,SeriesName,ParentIndexNumber,ImageTags&includeItemTypes=Movie,Series`;
|
const isMusic = library.CollectionType === 'music';
|
||||||
|
const itemTypes = isMusic ? 'MusicArtist' : 'Movie,Series';
|
||||||
|
const fields = isMusic ? 'ImageTags' : 'ProductionYear,RunTimeTicks,SeriesName,ParentIndexNumber,ImageTags';
|
||||||
|
const itemsUrl = `${url}/Users/${userId}/Items?ParentId=${library.Id}&recursive=true&fields=${fields}&includeItemTypes=${itemTypes}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(itemsUrl, {
|
const response = await fetch(itemsUrl, {
|
||||||
headers: {
|
headers: { 'X-Emby-Token': apiKey }
|
||||||
'X-Emby-Token': apiKey
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) throw new Error(`Error ${response.status}`);
|
if (!response.ok) throw new Error(`Error ${response.status}`);
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const items = data.Items.map(item => ({
|
const items = data.Items.map(item => {
|
||||||
id: item.Id,
|
const baseItem = {
|
||||||
title: item.Name,
|
id: item.Id,
|
||||||
year: item.ProductionYear,
|
title: item.Name,
|
||||||
type: item.Type,
|
type: item.Type,
|
||||||
duration: item.RunTimeTicks,
|
thumb: item.ImageTags?.Primary ? `${url}/Items/${item.Id}/Images/Primary?tag=${item.ImageTags.Primary}` : ''
|
||||||
seriesTitle: item.SeriesName,
|
};
|
||||||
seasonNumber: item.ParentIndexNumber,
|
if (!isMusic) {
|
||||||
thumb: item.ImageTags?.Primary ? `${url}/Items/${item.Id}/Images/Primary?tag=${item.ImageTags.Primary}` : ''
|
baseItem.year = item.ProductionYear;
|
||||||
}));
|
baseItem.duration = item.RunTimeTicks;
|
||||||
|
baseItem.seriesTitle = item.SeriesName;
|
||||||
|
baseItem.seasonNumber = item.ParentIndexNumber;
|
||||||
|
}
|
||||||
|
return baseItem;
|
||||||
|
});
|
||||||
|
|
||||||
return { success: true, items, libraryName: library.Name, libraryId: library.Id };
|
return { success: true, items, libraryName: library.Name, libraryId: library.Id };
|
||||||
|
|
||||||
@ -135,7 +141,7 @@ export async function startJellyfinScan() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mediaLibraries = viewsResult.views.filter(v => v.CollectionType === 'movies' || v.CollectionType === 'tvshows');
|
const mediaLibraries = viewsResult.views.filter(v => v.CollectionType === 'movies' || v.CollectionType === 'tvshows' || v.CollectionType === 'music');
|
||||||
|
|
||||||
if (mediaLibraries.length === 0) {
|
if (mediaLibraries.length === 0) {
|
||||||
statusDiv.innerHTML += `<div class="text-warning">${_('jellyfinNoMediaLibraries')}</div>`;
|
statusDiv.innerHTML += `<div class="text-warning">${_('jellyfinNoMediaLibraries')}</div>`;
|
||||||
@ -149,34 +155,71 @@ export async function startJellyfinScan() {
|
|||||||
|
|
||||||
await clearStore('jellyfin_movies');
|
await clearStore('jellyfin_movies');
|
||||||
await clearStore('jellyfin_series');
|
await clearStore('jellyfin_series');
|
||||||
|
|
||||||
|
const allArtists = await getFromDB('artists') || [];
|
||||||
|
const plexArtists = allArtists.filter(a => !a.isJellyfin);
|
||||||
|
await clearStore('artists');
|
||||||
|
if (plexArtists.length > 0) {
|
||||||
|
await addItemsToStore('artists', plexArtists);
|
||||||
|
}
|
||||||
|
|
||||||
let totalMovies = 0;
|
let totalMovies = 0;
|
||||||
let totalSeries = 0;
|
let totalSeries = 0;
|
||||||
|
let totalMusic = 0;
|
||||||
|
|
||||||
const scanPromises = mediaLibraries.map(library =>
|
const scanPromises = mediaLibraries.map(library =>
|
||||||
fetchItemsFromLibrary(url, authResult.userId, authResult.token, library)
|
fetchItemsFromLibrary(url, authResult.userId, authResult.token, library)
|
||||||
);
|
);
|
||||||
|
|
||||||
const results = await Promise.allSettled(scanPromises);
|
const results = await Promise.allSettled(scanPromises);
|
||||||
|
const urlObject = new URL(url);
|
||||||
|
|
||||||
for (const result of results) {
|
for (const result of results) {
|
||||||
if (result.status === 'fulfilled' && result.value.success) {
|
if (result.status === 'fulfilled' && result.value.success) {
|
||||||
const library = mediaLibraries.find(lib => lib.Id === result.value.libraryId);
|
const library = mediaLibraries.find(lib => lib.Id === result.value.libraryId);
|
||||||
if (library) {
|
if (library) {
|
||||||
const storeName = library.CollectionType === 'movies' ? 'jellyfin_movies' : 'jellyfin_series';
|
const collectionType = library.CollectionType;
|
||||||
const dbEntry = {
|
let storeName;
|
||||||
serverUrl: url,
|
if (collectionType === 'movies') storeName = 'jellyfin_movies';
|
||||||
libraryId: library.Id,
|
else if (collectionType === 'tvshows') storeName = 'jellyfin_series';
|
||||||
libraryName: library.Name,
|
else if (collectionType === 'music') storeName = 'artists';
|
||||||
titulos: result.value.items,
|
|
||||||
};
|
if (storeName) {
|
||||||
await addItemsToStore(storeName, [dbEntry]);
|
const titulos = result.value.items;
|
||||||
if (storeName === 'jellyfin_movies') {
|
if (storeName === 'artists') {
|
||||||
totalMovies += result.value.items.length;
|
const jellyfinArtists = {
|
||||||
} else {
|
ip: urlObject.hostname,
|
||||||
totalSeries += result.value.items.length;
|
puerto: urlObject.port || (urlObject.protocol === 'https:' ? '443' : '80'),
|
||||||
|
token: authResult.token,
|
||||||
|
protocolo: urlObject.protocol.replace(':', ''),
|
||||||
|
serverName: `Jellyfin - ${library.Name}`,
|
||||||
|
titulos: titulos.map(t => ({...t, isJellyfin: true, userId: authResult.userId, serverUrl: url})),
|
||||||
|
tokenPrincipal: authResult.token,
|
||||||
|
isJellyfin: true,
|
||||||
|
userId: authResult.userId,
|
||||||
|
serverUrl: url
|
||||||
|
};
|
||||||
|
if (titulos.length > 0) {
|
||||||
|
await addItemsToStore(storeName, [jellyfinArtists]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const dbEntry = {
|
||||||
|
serverUrl: url,
|
||||||
|
libraryId: library.Id,
|
||||||
|
libraryName: library.Name,
|
||||||
|
titulos: titulos,
|
||||||
|
};
|
||||||
|
if (titulos.length > 0) {
|
||||||
|
await addItemsToStore(storeName, [dbEntry]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (collectionType === 'movies') totalMovies += titulos.length;
|
||||||
|
else if (collectionType === 'tvshows') totalSeries += titulos.length;
|
||||||
|
else if (collectionType === 'music') totalMusic += titulos.length;
|
||||||
|
|
||||||
|
statusDiv.innerHTML += `<div class="text-success-secondary">${_('jellyfinLibraryScanSuccess', [library.Name, String(titulos.length)])}</div>`;
|
||||||
}
|
}
|
||||||
statusDiv.innerHTML += `<div class="text-success-secondary">${_('jellyfinLibraryScanSuccess', [library.Name, String(result.value.items.length)])}</div>`;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const libraryName = result.reason?.libraryName || result.value?.libraryName || 'Unknown';
|
const libraryName = result.reason?.libraryName || result.value?.libraryName || 'Unknown';
|
||||||
@ -196,7 +239,7 @@ export async function startJellyfinScan() {
|
|||||||
await addItemsToStore('jellyfin_settings', [newSettings]);
|
await addItemsToStore('jellyfin_settings', [newSettings]);
|
||||||
state.jellyfinSettings = newSettings;
|
state.jellyfinSettings = newSettings;
|
||||||
|
|
||||||
const message = _('jellyfinScanSuccess', [String(totalMovies), String(totalSeries)]);
|
const message = `Scan finished. Found ${totalMovies} movies, ${totalSeries} series, and ${totalMusic} music artists.`;
|
||||||
statusDiv.innerHTML += `<div class="text-success mt-2">${message}</div>`;
|
statusDiv.innerHTML += `<div class="text-success mt-2">${message}</div>`;
|
||||||
showNotification(message, 'success');
|
showNotification(message, 'success');
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { getFromDB } from './db.js';
|
import { getFromDB } from './db.js';
|
||||||
import { debounce, showNotification, _ } from './utils.js';
|
import { debounce, showNotification, _ } from './utils.js';
|
||||||
import { getMusicUrlsFromPlex } from './api.js';
|
import { getMusicUrlsFromPlex, getMusicUrlsFromJellyfin } from './api.js';
|
||||||
|
import { state } from './state.js';
|
||||||
|
|
||||||
export class MusicPlayer {
|
export class MusicPlayer {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -264,7 +265,13 @@ export class MusicPlayer {
|
|||||||
const artistCard = document.querySelector(`.artist-card[data-id='${wasArtistId}']`);
|
const artistCard = document.querySelector(`.artist-card[data-id='${wasArtistId}']`);
|
||||||
if (artistCard) {
|
if (artistCard) {
|
||||||
try {
|
try {
|
||||||
const canciones = await getMusicUrlsFromPlex(artistCard.dataset.token, artistCard.dataset.protocolo, artistCard.dataset.ip, artistCard.dataset.puerto, wasArtistId);
|
const { token, protocolo, ip, puerto, id: artistaId, isjellyfin, serverurl, userid } = artistCard.dataset;
|
||||||
|
let canciones;
|
||||||
|
if (isjellyfin === 'true') {
|
||||||
|
canciones = await getMusicUrlsFromJellyfin(serverurl, userid, token, artistaId);
|
||||||
|
} else {
|
||||||
|
canciones = await getMusicUrlsFromPlex(token, protocolo, ip, puerto, artistaId);
|
||||||
|
}
|
||||||
this.handleSongsLoaded(canciones, wasArtistId);
|
this.handleSongsLoaded(canciones, wasArtistId);
|
||||||
this.markCurrentSong();
|
this.markCurrentSong();
|
||||||
|
|
||||||
@ -333,12 +340,25 @@ export class MusicPlayer {
|
|||||||
if (servidor && Array.isArray(servidor.titulos)) {
|
if (servidor && Array.isArray(servidor.titulos)) {
|
||||||
servidor.titulos.forEach(artista => {
|
servidor.titulos.forEach(artista => {
|
||||||
if (artista && typeof artista.id !== 'undefined' && artista.id !== null && !artistMap.has(artista.id)) {
|
if (artista && typeof artista.id !== 'undefined' && artista.id !== null && !artistMap.has(artista.id)) {
|
||||||
artistMap.set(artista.id, {
|
const artistEntry = {
|
||||||
id: artista.id, title: artista.title || 'Artista Desconocido',
|
id: artista.id,
|
||||||
token: servidor.token, ip: servidor.ip, puerto: servidor.puerto,
|
title: artista.title || 'Artista Desconocido',
|
||||||
protocolo: servidor.protocolo, serverName: servidor.serverName || servidor.ip || 'Servidor Desconocido',
|
thumb: artista.thumb,
|
||||||
thumb: artista.thumb
|
isJellyfin: servidor.isJellyfin || false,
|
||||||
});
|
serverName: servidor.serverName || 'Servidor Desconocido',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (servidor.isJellyfin) {
|
||||||
|
artistEntry.serverUrl = servidor.serverUrl;
|
||||||
|
artistEntry.userId = servidor.userId;
|
||||||
|
artistEntry.token = servidor.token;
|
||||||
|
} else {
|
||||||
|
artistEntry.token = servidor.token;
|
||||||
|
artistEntry.ip = servidor.ip;
|
||||||
|
artistEntry.puerto = servidor.puerto;
|
||||||
|
artistEntry.protocolo = servidor.protocolo;
|
||||||
|
}
|
||||||
|
artistMap.set(artista.id, artistEntry);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -367,9 +387,14 @@ export class MusicPlayer {
|
|||||||
const card = document.createElement('div');
|
const card = document.createElement('div');
|
||||||
card.className = 'artist-card';
|
card.className = 'artist-card';
|
||||||
|
|
||||||
const thumbUrl = artista.thumb
|
let thumbUrl = null;
|
||||||
? `${artista.protocolo}://${artista.ip}:${artista.puerto}${artista.thumb}?X-Plex-Token=${artista.token}`
|
if (artista.thumb) {
|
||||||
: null;
|
if (artista.isJellyfin) {
|
||||||
|
thumbUrl = artista.thumb;
|
||||||
|
} else {
|
||||||
|
thumbUrl = `${artista.protocolo}://${artista.ip}:${artista.puerto}${artista.thumb}?X-Plex-Token=${artista.token}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
card.innerHTML = `
|
card.innerHTML = `
|
||||||
<div class="artist-thumb-wrapper">
|
<div class="artist-thumb-wrapper">
|
||||||
@ -380,12 +405,21 @@ export class MusicPlayer {
|
|||||||
<div class="artist-card-title">${artista.title}</div>
|
<div class="artist-card-title">${artista.title}</div>
|
||||||
`;
|
`;
|
||||||
card.dataset.id = artista.id;
|
card.dataset.id = artista.id;
|
||||||
card.dataset.token = artista.token;
|
|
||||||
card.dataset.ip = artista.ip;
|
|
||||||
card.dataset.puerto = artista.puerto;
|
|
||||||
card.dataset.protocolo = artista.protocolo;
|
|
||||||
card.dataset.thumb = artista.thumb || '';
|
card.dataset.thumb = artista.thumb || '';
|
||||||
|
card.dataset.isjellyfin = artista.isJellyfin;
|
||||||
card.title = `${artista.title} en ${artista.serverName || 'Servidor Desconocido'}`;
|
card.title = `${artista.title} en ${artista.serverName || 'Servidor Desconocido'}`;
|
||||||
|
|
||||||
|
if (artista.isJellyfin) {
|
||||||
|
card.dataset.serverurl = artista.serverUrl;
|
||||||
|
card.dataset.userid = artista.userId;
|
||||||
|
card.dataset.token = artista.token;
|
||||||
|
} else {
|
||||||
|
card.dataset.token = artista.token;
|
||||||
|
card.dataset.ip = artista.ip;
|
||||||
|
card.dataset.puerto = artista.puerto;
|
||||||
|
card.dataset.protocolo = artista.protocolo;
|
||||||
|
}
|
||||||
|
|
||||||
artistGrid.appendChild(card);
|
artistGrid.appendChild(card);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -432,12 +466,19 @@ export class MusicPlayer {
|
|||||||
|
|
||||||
async loadArtistSongs(selectedCard) {
|
async loadArtistSongs(selectedCard) {
|
||||||
if (!this.isReady) return;
|
if (!this.isReady) return;
|
||||||
const { token, protocolo, ip, puerto, id: artistaId, thumb } = selectedCard.dataset;
|
const { id: artistaId, thumb, isjellyfin, serverurl, userid, token, protocolo, ip, puerto } = selectedCard.dataset;
|
||||||
this.currentArtist = selectedCard.querySelector('.artist-card-title').textContent;
|
this.currentArtist = selectedCard.querySelector('.artist-card-title').textContent;
|
||||||
this.currentArtistId = artistaId;
|
this.currentArtistId = artistaId;
|
||||||
if (!artistaId) return;
|
if (!artistaId) return;
|
||||||
|
|
||||||
const thumbUrl = thumb ? `${protocolo}://${ip}:${puerto}${thumb}?X-Plex-Token=${token}` : 'img/no-profile.png';
|
let thumbUrl = 'img/no-profile.png';
|
||||||
|
if (thumb) {
|
||||||
|
if (isjellyfin === 'true') {
|
||||||
|
thumbUrl = thumb;
|
||||||
|
} else {
|
||||||
|
thumbUrl = `${protocolo}://${ip}:${puerto}${thumb}?X-Plex-Token=${token}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
document.getElementById('artist-header-thumb').src = thumbUrl;
|
document.getElementById('artist-header-thumb').src = thumbUrl;
|
||||||
document.getElementById('artist-header-title').textContent = this.currentArtist;
|
document.getElementById('artist-header-title').textContent = this.currentArtist;
|
||||||
|
|
||||||
@ -449,7 +490,12 @@ export class MusicPlayer {
|
|||||||
document.getElementById('listaCanciones').innerHTML = '';
|
document.getElementById('listaCanciones').innerHTML = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let canciones = await getMusicUrlsFromPlex(token, protocolo, ip, puerto, artistaId);
|
let canciones;
|
||||||
|
if (isjellyfin === 'true') {
|
||||||
|
canciones = await getMusicUrlsFromJellyfin(serverurl, userid, token, artistaId);
|
||||||
|
} else {
|
||||||
|
canciones = await getMusicUrlsFromPlex(token, protocolo, ip, puerto, artistaId);
|
||||||
|
}
|
||||||
this.handleSongsLoaded(canciones, artistaId);
|
this.handleSongsLoaded(canciones, artistaId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showNotification(_('errorFetchingArtistSongs'), "error");
|
showNotification(_('errorFetchingArtistSongs'), "error");
|
||||||
@ -789,7 +835,14 @@ export class MusicPlayer {
|
|||||||
filteredArtists.forEach((artista) => {
|
filteredArtists.forEach((artista) => {
|
||||||
const card = document.createElement('div');
|
const card = document.createElement('div');
|
||||||
card.className = 'artist-card';
|
card.className = 'artist-card';
|
||||||
const thumbUrl = artista.thumb ? `${artista.protocolo}://${artista.ip}:${artista.puerto}${artista.thumb}?X-Plex-Token=${artista.token}` : null;
|
let thumbUrl = null;
|
||||||
|
if (artista.thumb) {
|
||||||
|
if (artista.isJellyfin) {
|
||||||
|
thumbUrl = artista.thumb;
|
||||||
|
} else {
|
||||||
|
thumbUrl = `${artista.protocolo}://${artista.ip}:${artista.puerto}${artista.thumb}?X-Plex-Token=${artista.token}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
card.innerHTML = `
|
card.innerHTML = `
|
||||||
<div class="artist-thumb-wrapper">
|
<div class="artist-thumb-wrapper">
|
||||||
${thumbUrl ? `<img src="${thumbUrl}" class="artist-thumb" alt="${artista.title}" loading="lazy">` : '<i class="fas fa-user-music artist-thumb-placeholder"></i>'}
|
${thumbUrl ? `<img src="${thumbUrl}" class="artist-thumb" alt="${artista.title}" loading="lazy">` : '<i class="fas fa-user-music artist-thumb-placeholder"></i>'}
|
||||||
@ -797,12 +850,20 @@ export class MusicPlayer {
|
|||||||
<div class="artist-card-title">${artista.title}</div>
|
<div class="artist-card-title">${artista.title}</div>
|
||||||
`;
|
`;
|
||||||
card.dataset.id = artista.id;
|
card.dataset.id = artista.id;
|
||||||
card.dataset.token = artista.token;
|
|
||||||
card.dataset.ip = artista.ip;
|
|
||||||
card.dataset.puerto = artista.puerto;
|
|
||||||
card.dataset.protocolo = artista.protocolo;
|
|
||||||
card.dataset.thumb = artista.thumb || '';
|
card.dataset.thumb = artista.thumb || '';
|
||||||
|
card.dataset.isjellyfin = artista.isJellyfin;
|
||||||
card.title = `${artista.title} en ${artista.serverName || 'Servidor Desconocido'}`;
|
card.title = `${artista.title} en ${artista.serverName || 'Servidor Desconocido'}`;
|
||||||
|
|
||||||
|
if (artista.isJellyfin) {
|
||||||
|
card.dataset.serverurl = artista.serverUrl;
|
||||||
|
card.dataset.userid = artista.userId;
|
||||||
|
card.dataset.token = artista.token;
|
||||||
|
} else {
|
||||||
|
card.dataset.token = artista.token;
|
||||||
|
card.dataset.ip = artista.ip;
|
||||||
|
card.dataset.puerto = artista.puerto;
|
||||||
|
card.dataset.protocolo = artista.protocolo;
|
||||||
|
}
|
||||||
artistGrid.appendChild(card);
|
artistGrid.appendChild(card);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
54
plex.html
54
plex.html
@ -4,6 +4,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="description" content="__MSG_appDescription__">
|
||||||
<title>__MSG_appName__ - __MSG_appTagline__</title>
|
<title>__MSG_appName__ - __MSG_appTagline__</title>
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
|
||||||
@ -11,9 +12,6 @@
|
|||||||
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700&family=Orbitron:wght@500;600;700&display=swap"
|
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700&family=Orbitron:wght@500;600;700&display=swap"
|
||||||
rel="stylesheet">
|
rel="stylesheet">
|
||||||
<link rel="stylesheet" href="css/main.css">
|
<link rel="stylesheet" href="css/main.css">
|
||||||
<link rel="stylesheet" href="css/equalizer.css">
|
|
||||||
<link rel="stylesheet" href="css/photos.css">
|
|
||||||
<link rel="stylesheet" href="css/providers.css">
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="unlocalized">
|
<body class="unlocalized">
|
||||||
@ -22,7 +20,7 @@
|
|||||||
<header class="top-bar">
|
<header class="top-bar">
|
||||||
<div class="top-bar-left">
|
<div class="top-bar-left">
|
||||||
<button id="sidebar-toggle" class="btn-icon" aria-label="__MSG_toggleNavigation__"><i class="fas fa-bars"></i></button>
|
<button id="sidebar-toggle" class="btn-icon" aria-label="__MSG_toggleNavigation__"><i class="fas fa-bars"></i></button>
|
||||||
<a class="navbar-brand logo" href="#" id="reset-view-btn">__MSG_appName__</a>
|
<a class="navbar-brand logo" href="#" id="reset-view-btn" role="button">__MSG_appName__</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="top-bar-center">
|
<div class="top-bar-center">
|
||||||
<div class="search-bar">
|
<div class="search-bar">
|
||||||
@ -43,22 +41,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<nav class="sidebar-nav" id="sidebar-nav">
|
<nav class="sidebar-nav" id="sidebar-nav" role="navigation">
|
||||||
<ul class="sidebar-menu">
|
<ul class="sidebar-menu">
|
||||||
<li><a class="nav-link active" href="#" id="nav-movies"><i class="fas fa-film"></i><span>__MSG_navMovies__</span></a></li>
|
<li><a class="nav-link active" href="#" id="nav-movies" role="button"><i class="fas fa-film"></i><span>__MSG_navMovies__</span></a></li>
|
||||||
<li><a class="nav-link" href="#" id="nav-series"><i class="fas fa-tv"></i><span>__MSG_navSeries__</span></a></li>
|
<li><a class="nav-link" href="#" id="nav-series" role="button"><i class="fas fa-tv"></i><span>__MSG_navSeries__</span></a></li>
|
||||||
<li><a class="nav-link" href="#" id="nav-providers"><i class="fas fa-broadcast-tower"></i><span>__MSG_navProviders__</span></a></li>
|
<li><a class="nav-link" href="#" id="nav-providers" role="button"><i class="fas fa-broadcast-tower"></i><span>__MSG_navProviders__</span></a></li>
|
||||||
<li><a class="nav-link" href="#" id="nav-photos"><i class="fas fa-images"></i><span>__MSG_navPhotos__</span></a></li>
|
<li><a class="nav-link" href="#" id="nav-photos" role="button"><i class="fas fa-images"></i><span>__MSG_navPhotos__</span></a></li>
|
||||||
<li><a class="nav-link" href="#" id="nav-stats"><i class="fas fa-chart-pie"></i><span>__MSG_navStats__</span></a></li>
|
<li><a class="nav-link" href="#" id="nav-stats" role="button"><i class="fas fa-chart-pie"></i><span>__MSG_navStats__</span></a></li>
|
||||||
<li><a class="nav-link" href="#" id="nav-favorites"><i class="fas fa-heart"></i><span>__MSG_navFavorites__</span></a></li>
|
<li><a class="nav-link" href="#" id="nav-favorites" role="button"><i class="fas fa-heart"></i><span>__MSG_navFavorites__</span></a></li>
|
||||||
<li><a class="nav-link" href="#" id="nav-history"><i class="fas fa-history"></i><span>__MSG_navHistory__</span></a></li>
|
<li><a class="nav-link" href="#" id="nav-history" role="button"><i class="fas fa-history"></i><span>__MSG_navHistory__</span></a></li>
|
||||||
<li><a class="nav-link" href="#" id="nav-recommendations"><i class="fas fa-magic"></i><span>__MSG_navRecommendations__</span></a></li>
|
<li><a class="nav-link" href="#" id="nav-recommendations" role="button"><i class="fas fa-magic"></i><span>__MSG_navRecommendations__</span></a></li>
|
||||||
<li><a class="nav-link d-lg-none" href="#" id="openMusicPlayerMobile"><i class="fas fa-music"></i><span>__MSG_navMusic__</span></a></li>
|
<li><a class="nav-link d-lg-none" href="#" id="openMusicPlayerMobile" role="button"><i class="fas fa-music"></i><span>__MSG_navMusic__</span></a></li>
|
||||||
<li><a class="nav-link" href="#" id="nav-m3u-generator"><i class="fas fa-list-ul"></i><span>__MSG_navM3uGenerator__</span></a></li>
|
<li><a class="nav-link" href="#" id="nav-m3u-generator" role="button"><i class="fas fa-list-ul"></i><span>__MSG_navM3uGenerator__</span></a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div id="main-container">
|
<div id="main-container" role="main">
|
||||||
<div id="main-view">
|
<div id="main-view">
|
||||||
<section class="hero" id="hero-section">
|
<section class="hero" id="hero-section">
|
||||||
<div class="hero-background-container">
|
<div class="hero-background-container">
|
||||||
@ -287,15 +285,15 @@
|
|||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="footer-content">
|
<div class="footer-content">
|
||||||
<a class="footer-logo-link" href="#" id="footer-logo-btn">
|
<a class="footer-logo-link" href="#" id="footer-logo-btn" role="button">
|
||||||
<span class="footer-logo-text">__MSG_appName__</span>
|
<span class="footer-logo-text">__MSG_appName__</span>
|
||||||
</a>
|
</a>
|
||||||
<div class="footer-links">
|
<div class="footer-links">
|
||||||
<a href="#" class="footer-link" id="footer-movies">__MSG_navMovies__</a>
|
<a href="#" class="footer-link" id="footer-movies" role="button">__MSG_navMovies__</a>
|
||||||
<a href="#" class="footer-link" id="footer-series">__MSG_navSeries__</a>
|
<a href="#" class="footer-link" id="footer-series" role="button">__MSG_navSeries__</a>
|
||||||
<a href="#" class="footer-link" id="footer-providers">__MSG_navProviders__</a>
|
<a href="#" class="footer-link" id="footer-providers" role="button">__MSG_navProviders__</a>
|
||||||
<a href="#" class="footer-link" id="footer-stats">__MSG_navStats__</a>
|
<a href="#" class="footer-link" id="footer-stats" role="button">__MSG_navStats__</a>
|
||||||
<a href="#" class="footer-link" id="footer-favorites">__MSG_navFavorites__</a>
|
<a href="#" class="footer-link" id="footer-favorites" role="button">__MSG_navFavorites__</a>
|
||||||
</div>
|
</div>
|
||||||
<p class="footer-credit">__MSG_footerCredit__</p>
|
<p class="footer-credit">__MSG_footerCredit__</p>
|
||||||
</div>
|
</div>
|
||||||
@ -308,7 +306,7 @@
|
|||||||
<div id="item-details-content"></div>
|
<div id="item-details-content"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div id="video-lightbox" class="lightbox">
|
<div id="video-lightbox" class="lightbox" role="dialog" aria-modal="true">
|
||||||
<div class="lightbox-content">
|
<div class="lightbox-content">
|
||||||
<button class="lightbox-close" aria-label="__MSG_closeTrailer__"><i class="fas fa-times"></i></button>
|
<button class="lightbox-close" aria-label="__MSG_closeTrailer__"><i class="fas fa-times"></i></button>
|
||||||
<div class="video-container"><iframe id="video-iframe" frameborder="0" allow="autoplay; encrypted-media"
|
<div class="video-container"><iframe id="video-iframe" frameborder="0" allow="autoplay; encrypted-media"
|
||||||
@ -316,7 +314,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="photo-lightbox" style="display: none;">
|
<div id="photo-lightbox" style="display: none;" role="dialog" aria-modal="true">
|
||||||
<div class="photo-lightbox-container">
|
<div class="photo-lightbox-container">
|
||||||
<button class="photo-lightbox-btn" id="photo-lightbox-close" aria-label="__MSG_close__"><i class="fas fa-times"></i></button>
|
<button class="photo-lightbox-btn" id="photo-lightbox-close" aria-label="__MSG_close__"><i class="fas fa-times"></i></button>
|
||||||
<button class="photo-lightbox-btn" id="photo-lightbox-prev" aria-label="__MSG_previous__"><i class="fas fa-chevron-left"></i></button>
|
<button class="photo-lightbox-btn" id="photo-lightbox-prev" aria-label="__MSG_previous__"><i class="fas fa-chevron-left"></i></button>
|
||||||
@ -337,7 +335,7 @@
|
|||||||
|
|
||||||
<div class="spinner" id="spinner"></div>
|
<div class="spinner" id="spinner"></div>
|
||||||
|
|
||||||
<div class="modal fade" id="activityViewerModal" tabindex="-1" aria-labelledby="activityViewerModalLabel" aria-hidden="true">
|
<div class="modal fade" id="activityViewerModal" tabindex="-1" aria-labelledby="activityViewerModalLabel" aria-hidden="true" role="dialog" aria-modal="true">
|
||||||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
@ -360,7 +358,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal fade" id="settingsModal" tabindex="-1" aria-labelledby="settingsModalLabel" aria-hidden="true">
|
<div class="modal fade" id="settingsModal" tabindex="-1" aria-labelledby="settingsModalLabel" aria-hidden="true" role="dialog" aria-modal="true">
|
||||||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
@ -736,7 +734,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal fade" id="infoModal" tabindex="-1" aria-labelledby="infoModalLabel" aria-hidden="true">
|
<div class="modal fade" id="infoModal" tabindex="-1" aria-labelledby="infoModalLabel" aria-hidden="true" role="dialog" aria-modal="true">
|
||||||
<div class="modal-dialog modal-dialog-centered">
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
@ -768,4 +766,4 @@
|
|||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user