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"),
|
||||
genre: Array.from(track.querySelectorAll("Genre")).map(g => g.getAttribute('tag')).join(', ') || '',
|
||||
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);
|
||||
|
||||
@ -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) {
|
||||
if (!busqueda || !tipoContenido) return { success: false, streams: [], message: _('invalidStreamInfo') };
|
||||
if (!state.db) return { success: false, streams: [], message: _('dbUnavailableForStreams') };
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { state } from './state.js';
|
||||
import { addItemsToStore, clearStore } from './db.js';
|
||||
import { addItemsToStore, clearStore, getFromDB } from './db.js';
|
||||
import { showNotification, _, emitirEventoActualizacion } from './utils.js';
|
||||
|
||||
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) {
|
||||
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 {
|
||||
const response = await fetch(itemsUrl, {
|
||||
headers: {
|
||||
'X-Emby-Token': apiKey
|
||||
}
|
||||
headers: { 'X-Emby-Token': apiKey }
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error(`Error ${response.status}`);
|
||||
|
||||
const data = await response.json();
|
||||
const items = data.Items.map(item => ({
|
||||
const items = data.Items.map(item => {
|
||||
const baseItem = {
|
||||
id: item.Id,
|
||||
title: item.Name,
|
||||
year: item.ProductionYear,
|
||||
type: item.Type,
|
||||
duration: item.RunTimeTicks,
|
||||
seriesTitle: item.SeriesName,
|
||||
seasonNumber: item.ParentIndexNumber,
|
||||
thumb: item.ImageTags?.Primary ? `${url}/Items/${item.Id}/Images/Primary?tag=${item.ImageTags.Primary}` : ''
|
||||
}));
|
||||
};
|
||||
if (!isMusic) {
|
||||
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 };
|
||||
|
||||
@ -135,7 +141,7 @@ export async function startJellyfinScan() {
|
||||
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) {
|
||||
statusDiv.innerHTML += `<div class="text-warning">${_('jellyfinNoMediaLibraries')}</div>`;
|
||||
@ -150,33 +156,70 @@ export async function startJellyfinScan() {
|
||||
await clearStore('jellyfin_movies');
|
||||
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 totalSeries = 0;
|
||||
let totalMusic = 0;
|
||||
|
||||
const scanPromises = mediaLibraries.map(library =>
|
||||
fetchItemsFromLibrary(url, authResult.userId, authResult.token, library)
|
||||
);
|
||||
|
||||
const results = await Promise.allSettled(scanPromises);
|
||||
const urlObject = new URL(url);
|
||||
|
||||
for (const result of results) {
|
||||
if (result.status === 'fulfilled' && result.value.success) {
|
||||
const library = mediaLibraries.find(lib => lib.Id === result.value.libraryId);
|
||||
if (library) {
|
||||
const storeName = library.CollectionType === 'movies' ? 'jellyfin_movies' : 'jellyfin_series';
|
||||
const collectionType = library.CollectionType;
|
||||
let storeName;
|
||||
if (collectionType === 'movies') storeName = 'jellyfin_movies';
|
||||
else if (collectionType === 'tvshows') storeName = 'jellyfin_series';
|
||||
else if (collectionType === 'music') storeName = 'artists';
|
||||
|
||||
if (storeName) {
|
||||
const titulos = result.value.items;
|
||||
if (storeName === 'artists') {
|
||||
const jellyfinArtists = {
|
||||
ip: urlObject.hostname,
|
||||
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: result.value.items,
|
||||
titulos: titulos,
|
||||
};
|
||||
if (titulos.length > 0) {
|
||||
await addItemsToStore(storeName, [dbEntry]);
|
||||
if (storeName === 'jellyfin_movies') {
|
||||
totalMovies += result.value.items.length;
|
||||
} else {
|
||||
totalSeries += result.value.items.length;
|
||||
}
|
||||
statusDiv.innerHTML += `<div class="text-success-secondary">${_('jellyfinLibraryScanSuccess', [library.Name, String(result.value.items.length)])}</div>`;
|
||||
}
|
||||
|
||||
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>`;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const libraryName = result.reason?.libraryName || result.value?.libraryName || 'Unknown';
|
||||
@ -196,7 +239,7 @@ export async function startJellyfinScan() {
|
||||
await addItemsToStore('jellyfin_settings', [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>`;
|
||||
showNotification(message, 'success');
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { getFromDB } from './db.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 {
|
||||
constructor() {
|
||||
@ -264,7 +265,13 @@ export class MusicPlayer {
|
||||
const artistCard = document.querySelector(`.artist-card[data-id='${wasArtistId}']`);
|
||||
if (artistCard) {
|
||||
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.markCurrentSong();
|
||||
|
||||
@ -333,12 +340,25 @@ export class MusicPlayer {
|
||||
if (servidor && Array.isArray(servidor.titulos)) {
|
||||
servidor.titulos.forEach(artista => {
|
||||
if (artista && typeof artista.id !== 'undefined' && artista.id !== null && !artistMap.has(artista.id)) {
|
||||
artistMap.set(artista.id, {
|
||||
id: artista.id, title: artista.title || 'Artista Desconocido',
|
||||
token: servidor.token, ip: servidor.ip, puerto: servidor.puerto,
|
||||
protocolo: servidor.protocolo, serverName: servidor.serverName || servidor.ip || 'Servidor Desconocido',
|
||||
thumb: artista.thumb
|
||||
});
|
||||
const artistEntry = {
|
||||
id: artista.id,
|
||||
title: artista.title || 'Artista Desconocido',
|
||||
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');
|
||||
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 = `
|
||||
<div class="artist-thumb-wrapper">
|
||||
@ -380,12 +405,21 @@ export class MusicPlayer {
|
||||
<div class="artist-card-title">${artista.title}</div>
|
||||
`;
|
||||
card.dataset.id = artista.id;
|
||||
card.dataset.thumb = artista.thumb || '';
|
||||
card.dataset.isjellyfin = artista.isJellyfin;
|
||||
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;
|
||||
card.dataset.thumb = artista.thumb || '';
|
||||
card.title = `${artista.title} en ${artista.serverName || 'Servidor Desconocido'}`;
|
||||
}
|
||||
|
||||
artistGrid.appendChild(card);
|
||||
}
|
||||
});
|
||||
@ -432,12 +466,19 @@ export class MusicPlayer {
|
||||
|
||||
async loadArtistSongs(selectedCard) {
|
||||
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.currentArtistId = artistaId;
|
||||
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-title').textContent = this.currentArtist;
|
||||
|
||||
@ -449,7 +490,12 @@ export class MusicPlayer {
|
||||
document.getElementById('listaCanciones').innerHTML = '';
|
||||
|
||||
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);
|
||||
} catch (error) {
|
||||
showNotification(_('errorFetchingArtistSongs'), "error");
|
||||
@ -789,7 +835,14 @@ export class MusicPlayer {
|
||||
filteredArtists.forEach((artista) => {
|
||||
const card = document.createElement('div');
|
||||
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 = `
|
||||
<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>'}
|
||||
@ -797,12 +850,20 @@ export class MusicPlayer {
|
||||
<div class="artist-card-title">${artista.title}</div>
|
||||
`;
|
||||
card.dataset.id = artista.id;
|
||||
card.dataset.thumb = artista.thumb || '';
|
||||
card.dataset.isjellyfin = artista.isJellyfin;
|
||||
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;
|
||||
card.dataset.thumb = artista.thumb || '';
|
||||
card.title = `${artista.title} en ${artista.serverName || 'Servidor Desconocido'}`;
|
||||
}
|
||||
artistGrid.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
52
plex.html
52
plex.html
@ -4,6 +4,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="__MSG_appDescription__">
|
||||
<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://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"
|
||||
rel="stylesheet">
|
||||
<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>
|
||||
|
||||
<body class="unlocalized">
|
||||
@ -22,7 +20,7 @@
|
||||
<header class="top-bar">
|
||||
<div class="top-bar-left">
|
||||
<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 class="top-bar-center">
|
||||
<div class="search-bar">
|
||||
@ -43,22 +41,22 @@
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<nav class="sidebar-nav" id="sidebar-nav">
|
||||
<nav class="sidebar-nav" id="sidebar-nav" role="navigation">
|
||||
<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" href="#" id="nav-series"><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-photos"><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-favorites"><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-recommendations"><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" href="#" id="nav-m3u-generator"><i class="fas fa-list-ul"></i><span>__MSG_navM3uGenerator__</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" role="button"><i class="fas fa-tv"></i><span>__MSG_navSeries__</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" role="button"><i class="fas fa-images"></i><span>__MSG_navPhotos__</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" role="button"><i class="fas fa-heart"></i><span>__MSG_navFavorites__</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" 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" role="button"><i class="fas fa-music"></i><span>__MSG_navMusic__</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>
|
||||
</nav>
|
||||
|
||||
<div id="main-container">
|
||||
<div id="main-container" role="main">
|
||||
<div id="main-view">
|
||||
<section class="hero" id="hero-section">
|
||||
<div class="hero-background-container">
|
||||
@ -287,15 +285,15 @@
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<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>
|
||||
</a>
|
||||
<div class="footer-links">
|
||||
<a href="#" class="footer-link" id="footer-movies">__MSG_navMovies__</a>
|
||||
<a href="#" class="footer-link" id="footer-series">__MSG_navSeries__</a>
|
||||
<a href="#" class="footer-link" id="footer-providers">__MSG_navProviders__</a>
|
||||
<a href="#" class="footer-link" id="footer-stats">__MSG_navStats__</a>
|
||||
<a href="#" class="footer-link" id="footer-favorites">__MSG_navFavorites__</a>
|
||||
<a href="#" class="footer-link" id="footer-movies" role="button">__MSG_navMovies__</a>
|
||||
<a href="#" class="footer-link" id="footer-series" role="button">__MSG_navSeries__</a>
|
||||
<a href="#" class="footer-link" id="footer-providers" role="button">__MSG_navProviders__</a>
|
||||
<a href="#" class="footer-link" id="footer-stats" role="button">__MSG_navStats__</a>
|
||||
<a href="#" class="footer-link" id="footer-favorites" role="button">__MSG_navFavorites__</a>
|
||||
</div>
|
||||
<p class="footer-credit">__MSG_footerCredit__</p>
|
||||
</div>
|
||||
@ -308,7 +306,7 @@
|
||||
<div id="item-details-content"></div>
|
||||
</section>
|
||||
|
||||
<div id="video-lightbox" class="lightbox">
|
||||
<div id="video-lightbox" class="lightbox" role="dialog" aria-modal="true">
|
||||
<div class="lightbox-content">
|
||||
<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"
|
||||
@ -316,7 +314,7 @@
|
||||
</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">
|
||||
<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>
|
||||
@ -337,7 +335,7 @@
|
||||
|
||||
<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-content">
|
||||
<div class="modal-header">
|
||||
@ -360,7 +358,7 @@
|
||||
</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-content">
|
||||
<div class="modal-header">
|
||||
@ -736,7 +734,7 @@
|
||||
</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-content">
|
||||
<div class="modal-header">
|
||||
|
Loading…
x
Reference in New Issue
Block a user