255 lines
10 KiB
JavaScript
255 lines
10 KiB
JavaScript
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 => {
|
|
const baseItem = {
|
|
id: item.Id,
|
|
title: item.Name,
|
|
type: item.Type,
|
|
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 };
|
|
|
|
} 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;
|
|
} |