Jellyfin integration in M3U generator and statistics

This commit is contained in:
Filipinos 2025-07-27 12:56:26 +02:00
parent e96e4eb255
commit f6748ab113
5 changed files with 95 additions and 12 deletions

20
js/constants.js Normal file
View File

@ -0,0 +1,20 @@
export const API_URLS = {
TMDB_BASE: 'https://api.themoviedb.org/3',
TMDB_IMAGE_BASE: 'https://image.tmdb.org/t/p',
PLEX_TV: 'https://plex.tv/api/resources',
YOUTUBE_EMBED: 'https://www.youtube.com/embed/',
IMDB_TITLE: 'https://www.imdb.com/title/'
};
export const CONFIG = {
DEFAULT_API_KEY: '4e44d9029b1270a757cddc766a1bcb63',
DB_NAME: 'PlexDB',
DB_VERSION: 6
};
export const STORAGE_KEYS = {
USER_HISTORY: 'cineplex_userHistory',
USER_PREFERENCES: 'cineplex_userPreferences',
FAVORITES: 'cineplex_favorites',
RECOMMENDATIONS_CACHE: 'cineplex_recommendations'
};

View File

@ -268,12 +268,27 @@ export async function importDatabase(file) {
} }
export async function getServers() { export async function getServers() {
const connections = await getFromDB('conexiones_locales'); const plexConnections = await getFromDB('conexiones_locales');
return connections.map(conn => ({ const jellyfinConnections = await getFromDB('jellyfin_settings');
const plexServers = plexConnections.map(conn => ({
id: conn.id, id: conn.id,
name: conn.nombre, name: conn.nombre,
type: 'plex',
accessToken: conn.token, accessToken: conn.token,
publicUrl: conn.protocolo + '://' + conn.ip + ':' + conn.puerto, publicUrl: conn.protocolo + '://' + conn.ip + ':' + conn.puerto,
localUrl: conn.protocolo + '://' + conn.ip + ':' + conn.puerto, localUrl: conn.protocolo + '://' + conn.ip + ':' + conn.puerto,
})); }));
const jellyfinServers = jellyfinConnections.map(conn => ({
id: conn.id,
name: conn.username || 'Jellyfin Server',
type: 'jellyfin',
accessToken: conn.apiKey,
userId: conn.userId,
publicUrl: conn.url,
localUrl: conn.url,
}));
return [...plexServers, ...jellyfinServers];
} }

View File

@ -32,7 +32,7 @@ async function authenticateJellyfin(url, username, password) {
} }
} }
async function fetchLibraryViews(url, userId, apiKey) { export async function fetchLibraryViews(url, userId, apiKey) {
const viewsUrl = `${url}/Users/${userId}/Views`; const viewsUrl = `${url}/Users/${userId}/Views`;
try { try {
const response = await fetch(viewsUrl, { const response = await fetch(viewsUrl, {
@ -49,8 +49,8 @@ async function fetchLibraryViews(url, userId, apiKey) {
} }
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&includeItemTypes=Movie,Series`; const itemsUrl = `${url}/Users/${userId}/Items?ParentId=${library.Id}&recursive=true&fields=ProductionYear,RunTimeTicks,SeriesName,ParentIndexNumber,ImageTags&includeItemTypes=Movie,Series`;
try { try {
const response = await fetch(itemsUrl, { const response = await fetch(itemsUrl, {
@ -67,7 +67,10 @@ async function fetchItemsFromLibrary(url, userId, apiKey, library) {
title: item.Name, title: item.Name,
year: item.ProductionYear, year: item.ProductionYear,
type: item.Type, type: item.Type,
posterTag: item.ImageTags?.Primary, duration: item.RunTimeTicks,
seriesTitle: item.SeriesName,
seasonNumber: item.ParentIndexNumber,
thumb: item.ImageTags?.Primary ? `${url}/Items/${item.Id}/Images/Primary?tag=${item.ImageTags.Primary}` : ''
})); }));
return { success: true, items, libraryName: library.Name, libraryId: library.Id }; return { success: true, items, libraryName: library.Name, libraryId: library.Id };

View File

@ -1,5 +1,6 @@
import { getServers } from './db.js'; import { getServers } from './db.js';
import { fetchLibraries, fetchLibraryContents } from './plex.js'; import { fetchLibraries as fetchPlexLibraries, fetchLibraryContents as fetchPlexLibraryContents } from './plex.js';
import { fetchLibraryViews as fetchJellyfinLibraries, fetchItemsFromLibrary as fetchJellyfinLibraryContents } from './jellyfin.js';
import { showNotification, _ } from './utils.js'; import { showNotification, _ } from './utils.js';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
@ -22,6 +23,10 @@ document.addEventListener('DOMContentLoaded', () => {
gsap.to(m3uConfigPanel, { autoAlpha: 1, y: 0, duration: 0.8, ease: "power3.out" }); gsap.to(m3uConfigPanel, { autoAlpha: 1, y: 0, duration: 0.8, ease: "power3.out" });
gsap.to(m3uInfoPanel, { autoAlpha: 1, y: 0, duration: 0.8, ease: "power3.out", delay: 0.2 }); gsap.to(m3uInfoPanel, { autoAlpha: 1, y: 0, duration: 0.8, ease: "power3.out", delay: 0.2 });
await loadServers();
}
async function loadServers() {
try { try {
const servers = await getServers(); const servers = await getServers();
m3uServerSelect.innerHTML = `<option value="">${_('selectAServer')}</option>`; m3uServerSelect.innerHTML = `<option value="">${_('selectAServer')}</option>`;
@ -29,6 +34,7 @@ document.addEventListener('DOMContentLoaded', () => {
const option = document.createElement('option'); const option = document.createElement('option');
option.value = server.id; option.value = server.id;
option.textContent = server.name; option.textContent = server.name;
option.dataset.type = server.type;
m3uServerSelect.appendChild(option); m3uServerSelect.appendChild(option);
}); });
} catch (error) { } catch (error) {
@ -37,7 +43,9 @@ document.addEventListener('DOMContentLoaded', () => {
} }
m3uServerSelect.addEventListener('change', async () => { m3uServerSelect.addEventListener('change', async () => {
const serverId = m3uServerSelect.value; const selectedOption = m3uServerSelect.options[m3uServerSelect.selectedIndex];
const serverId = selectedOption.value;
const serverType = selectedOption.dataset.type;
m3uLibrariesContainer.innerHTML = ''; m3uLibrariesContainer.innerHTML = '';
downloadM3uBtn.disabled = true; downloadM3uBtn.disabled = true;
@ -57,7 +65,20 @@ document.addEventListener('DOMContentLoaded', () => {
const server = servers.find(s => s.id == serverId); const server = servers.find(s => s.id == serverId);
if (!server) return; if (!server) return;
const libraries = await fetchLibraries(server.accessToken, server.publicUrl, server.localUrl); let libraries = [];
if (serverType === 'plex') {
libraries = await fetchPlexLibraries(server.accessToken, server.publicUrl, server.localUrl);
} else if (serverType === 'jellyfin') {
const jellyfinLibraries = await fetchJellyfinLibraries(server.publicUrl, server.userId, server.accessToken);
if(jellyfinLibraries.success) {
libraries = jellyfinLibraries.views.map(lib => ({
key: lib.Id,
title: lib.Name,
type: lib.CollectionType === 'movies' ? 'movie' : (lib.CollectionType === 'tvshows' ? 'show' : 'music')
}));
}
}
const checkboxes = []; const checkboxes = [];
libraries.forEach(library => { libraries.forEach(library => {
if (library.type === 'movie' || library.type === 'show' || library.type === 'music') { if (library.type === 'movie' || library.type === 'show' || library.type === 'music') {
@ -93,7 +114,9 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
downloadM3uBtn.addEventListener('click', async () => { downloadM3uBtn.addEventListener('click', async () => {
const serverId = m3uServerSelect.value; const selectedOption = m3uServerSelect.options[m3uServerSelect.selectedIndex];
const serverId = selectedOption.value;
const serverType = selectedOption.dataset.type;
const selectedLibraries = Array.from(m3uLibrariesContainer.querySelectorAll('input:checked')).map(input => input.value); const selectedLibraries = Array.from(m3uLibrariesContainer.querySelectorAll('input:checked')).map(input => input.value);
if (!serverId || selectedLibraries.length === 0) { if (!serverId || selectedLibraries.length === 0) {
@ -117,7 +140,24 @@ document.addEventListener('DOMContentLoaded', () => {
const libraryElement = m3uLibrariesContainer.querySelector(`#library-${libraryKey}`); const libraryElement = m3uLibrariesContainer.querySelector(`#library-${libraryKey}`);
const libraryType = libraryElement.getAttribute('data-type'); const libraryType = libraryElement.getAttribute('data-type');
const libraryTitle = libraryElement ? libraryElement.nextElementSibling.textContent.trim() : ''; const libraryTitle = libraryElement ? libraryElement.nextElementSibling.textContent.trim() : '';
const items = await fetchLibraryContents(server.accessToken, server.publicUrl, server.localUrl, libraryKey, libraryType, 60000);
let items = [];
if (serverType === 'plex') {
items = await fetchPlexLibraryContents(server.accessToken, server.publicUrl, server.localUrl, libraryKey, libraryType, 60000);
} else if (serverType === 'jellyfin') {
const jellyfinItems = await fetchJellyfinLibraryContents(server.publicUrl, server.userId, server.accessToken, { Id: libraryKey });
if(jellyfinItems.success) {
items = jellyfinItems.items.map(item => ({
title: item.title,
duration: item.duration ? item.duration / 10000 : -1,
seriesTitle: item.seriesTitle,
seasonNumber: item.seasonNumber,
thumb: item.thumb,
url: `${server.publicUrl}/Videos/${item.id}/stream?static=true&api_key=${server.accessToken}`
}));
}
}
items.forEach(item => { items.forEach(item => {
const duration = item.duration ? Math.round(item.duration / 1000) : -1; const duration = item.duration ? Math.round(item.duration / 1000) : -1;
const groupTitle = item.seriesTitle && item.seasonNumber const groupTitle = item.seriesTitle && item.seasonNumber
@ -178,4 +218,6 @@ document.addEventListener('DOMContentLoaded', () => {
initializeM3uGenerator(); initializeM3uGenerator();
} }
} }
document.addEventListener('actualizacion', loadServers);
}); });

View File

@ -1079,6 +1079,7 @@ export async function generateStatistics() {
const allTokens = await getFromDB('tokens'); const allTokens = await getFromDB('tokens');
const allConnections = await getFromDB('conexiones_locales'); const allConnections = await getFromDB('conexiones_locales');
const allJellyfinConnections = await getFromDB('jellyfin_settings');
const filteredTokens = selectedToken === 'all' const filteredTokens = selectedToken === 'all'
? allTokens ? allTokens
@ -1088,8 +1089,10 @@ export async function generateStatistics() {
? allConnections ? allConnections
: allConnections.filter(c => c.tokenPrincipal === selectedToken); : allConnections.filter(c => c.tokenPrincipal === selectedToken);
const totalServers = filteredConnections.length + (selectedToken === 'all' ? allJellyfinConnections.length : 0);
animateValue('total-tokens', 0, filteredTokens.length, 1000); animateValue('total-tokens', 0, filteredTokens.length, 1000);
animateValue('total-servers', 0, filteredConnections.length, 1000); animateValue('total-servers', 0, totalServers, 1000);
updateTokenDetailsCard(selectedToken, filteredConnections); updateTokenDetailsCard(selectedToken, filteredConnections);