CinePlex/js/jellyfin.js

266 lines
11 KiB
JavaScript
Raw Permalink Normal View History

import { state } from './state.js';
import { addItemsToStore, clearStore, getFromDB } from './db.js';
import { showNotification, _, emitirEventoActualizacion } from './utils.js';
async function authenticateJellyfin(url, username, password) {
const authUrl = `${url}/Users/AuthenticateByName`;
const body = JSON.stringify({
Username: username,
Pw: password
});
try {
const response = await fetch(authUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Emby-Authorization': 'MediaBrowser Client="CinePlex", Device="Chrome", DeviceId="cineplex-jellyfin-integration", Version="1.0"'
},
body: body
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.AuthenticationResult?.ErrorMessage || `Error ${response.status}`);
}
const data = await response.json();
return { success: true, token: data.AccessToken, userId: data.User.Id };
} catch (error) {
return { success: false, message: error.message };
}
}
export async function fetchLibraryViews(url, userId, apiKey) {
const viewsUrl = `${url}/Users/${userId}/Views`;
try {
const response = await fetch(viewsUrl, {
headers: {
'X-Emby-Token': apiKey
}
});
if (!response.ok) throw new Error(`Error ${response.status} fetching library views`);
const data = await response.json();
return { success: true, views: data.Items };
} catch (error) {
return { success: false, message: error.message };
}
}
export async function fetchItemsFromLibrary(url, userId, apiKey, library) {
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 }
});
if (!response.ok) throw new Error(`Error ${response.status}`);
const data = await response.json();
const items = data.Items.map(item => {
2025-08-02 09:57:03 +02:00
const mediaStream = item.MediaStreams?.find(s => s.Type === 'Video');
const container = item.Container;
let resolution = null;
if (mediaStream && mediaStream.Height) {
if (mediaStream.Height >= 2160) resolution = '4k';
else if (mediaStream.Height >= 1080) resolution = '1080p';
else if (mediaStream.Height >= 720) resolution = '720p';
}
const baseItem = {
id: item.Id,
title: item.Name,
type: item.Type,
2025-08-02 09:57:03 +02:00
thumb: item.ImageTags?.Primary ? `${url}/Items/${item.Id}/Images/Primary?tag=${item.ImageTags.Primary}` : '',
container: container,
resolution: resolution
};
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 };
} catch (error) {
return { success: false, message: error.message, libraryName: library.Name, libraryId: library.Id };
}
}
export async function startJellyfinScan() {
if (state.isScanningJellyfin) {
showNotification(_('jellyfinScanInProgress'), 'warning');
return;
}
state.isScanningJellyfin = true;
const statusDiv = document.getElementById('jellyfinScanStatus');
const scanBtn = document.getElementById('jellyfinScanBtn');
const originalBtnText = scanBtn.innerHTML;
scanBtn.innerHTML = `<span class="spinner-border spinner-border-sm me-2"></span>${_('jellyfinScanning')}`;
scanBtn.disabled = true;
const urlInput = document.getElementById('jellyfinServerUrl');
const usernameInput = document.getElementById('jellyfinUsername');
const passwordInput = document.getElementById('jellyfinPassword');
let url = urlInput.value.trim();
const username = usernameInput.value.trim();
const password = passwordInput.value;
if (!url || !username) {
showNotification(_('jellyfinMissingCredentials'), 'error');
state.isScanningJellyfin = false;
scanBtn.innerHTML = originalBtnText;
scanBtn.disabled = false;
return;
}
url = url.replace(/\/web\/.*$/, '').replace(/\/$/, '');
statusDiv.innerHTML = `<div class="text-info">${_('jellyfinConnecting', url)}</div>`;
const authResult = await authenticateJellyfin(url, username, password);
if (!authResult.success) {
statusDiv.innerHTML = `<div class="text-danger">${_('jellyfinAuthFailed', authResult.message)}</div>`;
showNotification(_('jellyfinAuthFailed', authResult.message), 'error');
state.isScanningJellyfin = false;
scanBtn.innerHTML = originalBtnText;
scanBtn.disabled = false;
return;
}
statusDiv.innerHTML = `<div class="text-success">${_('jellyfinAuthSuccess')}</div><div class="text-info">${_('jellyfinFetchingLibraries')}</div>`;
const viewsResult = await fetchLibraryViews(url, authResult.userId, authResult.token);
if (!viewsResult.success) {
statusDiv.innerHTML += `<div class="text-danger">${_('jellyfinFetchFailed', viewsResult.message)}</div>`;
state.isScanningJellyfin = false;
scanBtn.innerHTML = originalBtnText;
scanBtn.disabled = false;
return;
}
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>`;
state.isScanningJellyfin = false;
scanBtn.innerHTML = originalBtnText;
scanBtn.disabled = false;
return;
}
statusDiv.innerHTML += `<div class="text-info">${_('jellyfinLibrariesFound', String(mediaLibraries.length))}</div>`;
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 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: 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>`;
}
}
} else {
const libraryName = result.reason?.libraryName || result.value?.libraryName || 'Unknown';
statusDiv.innerHTML += `<div class="text-warning">${_('jellyfinLibraryScanFailed', libraryName)}</div>`;
}
}
const newSettings = {
id: 'jellyfin_credentials',
url: url,
username: username,
password: password,
apiKey: authResult.token,
userId: authResult.userId
};
await addItemsToStore('jellyfin_settings', [newSettings]);
state.jellyfinSettings = newSettings;
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');
setTimeout(() => {
const modalInstance = bootstrap.Modal.getInstance(document.getElementById('settingsModal'));
if(modalInstance) modalInstance.hide();
emitirEventoActualizacion();
}, 2000);
state.isScanningJellyfin = false;
scanBtn.innerHTML = originalBtnText;
scanBtn.disabled = false;
}