Jellyfin integration in M3U generator and statistics
This commit is contained in:
parent
e96e4eb255
commit
f6748ab113
20
js/constants.js
Normal file
20
js/constants.js
Normal 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'
|
||||||
|
};
|
19
js/db.js
19
js/db.js
@ -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];
|
||||||
}
|
}
|
@ -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 };
|
||||||
|
@ -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);
|
||||||
});
|
});
|
5
js/ui.js
5
js/ui.js
@ -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);
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user