This commit is contained in:
Filipinos 2025-07-28 13:58:30 +02:00
parent e614cb387a
commit eb4bc982de
14 changed files with 127 additions and 54 deletions

View File

@ -346,5 +346,7 @@
"m3uInstruction1": { "message": "Wählen Sie einen Server aus der Liste aus." },
"m3uInstruction2": { "message": "Wählen Sie eine oder mehrere Bibliotheken aus, die Sie einschließen möchten." },
"m3uInstruction3": { "message": "Klicken Sie auf die Download-Schaltfläche." },
"m3uInstruction4": { "message": "Importieren Sie die .m3u-Datei in Ihren kompatiblen Player." }
"m3uInstruction4": { "message": "Importieren Sie die .m3u-Datei in Ihren kompatiblen Player." },
"settingsRegionLabel": { "message": "Region für die Inhaltsentdeckung" },
"allRegions": { "message": "Alle Regionen" }
}

View File

@ -343,5 +343,7 @@
"m3uInstruction1": { "message": "Choose a server from the list." },
"m3uInstruction2": { "message": "Select one or more libraries to include." },
"m3uInstruction3": { "message": "Click the download button." },
"m3uInstruction4": { "message": "Import the .m3u file into your compatible player." }
"m3uInstruction4": { "message": "Import the .m3u file into your compatible player." },
"settingsRegionLabel": { "message": "Region for content discovery" },
"allRegions": { "message": "All regions" }
}

View File

@ -343,5 +343,7 @@
"m3uInstruction1": { "message": "Elige un servidor de la lista." },
"m3uInstruction2": { "message": "Selecciona una o más bibliotecas para incluir." },
"m3uInstruction3": { "message": "Haz clic en el botón de descarga." },
"m3uInstruction4": { "message": "Importa el archivo .m3u en tu reproductor compatible." }
"m3uInstruction4": { "message": "Importa el archivo .m3u en tu reproductor compatible." },
"settingsRegionLabel": { "message": "Región para descubrimiento de contenido" },
"allRegions": { "message": "Todas las regiones" }
}

View File

@ -343,5 +343,7 @@
"m3uInstruction1": { "message": "Choisissez un serveur dans la liste." },
"m3uInstruction2": { "message": "Sélectionnez une ou plusieurs bibliothèques à inclure." },
"m3uInstruction3": { "message": "Cliquez sur le bouton de téléchargement." },
"m3uInstruction4": { "message": "Importez le fichier .m3u dans votre lecteur compatible." }
"m3uInstruction4": { "message": "Importez le fichier .m3u dans votre lecteur compatible." },
"settingsRegionLabel": { "message": "Région pour la découverte de contenu" },
"allRegions": { "message": "Toutes les régions" }
}

View File

@ -343,5 +343,7 @@
"m3uInstruction1": { "message": "Scegli un server dalla lista." },
"m3uInstruction2": { "message": "Seleziona una o più librerie da includere." },
"m3uInstruction3": { "message": "Clicca sul pulsante di download." },
"m3uInstruction4": { "message": "Importa il file .m3u nel tuo lettore compatibile." }
"m3uInstruction4": { "message": "Importa il file .m3u nel tuo lettore compatibile." },
"settingsRegionLabel": { "message": "Regione per la scoperta di contenuti" },
"allRegions": { "message": "Tutte le regioni" }
}

View File

@ -343,5 +343,7 @@
"m3uInstruction1": { "message": "Escolha um servidor da lista." },
"m3uInstruction2": { "message": "Selecione uma ou mais bibliotecas para incluir." },
"m3uInstruction3": { "message": "Clique no botão de download." },
"m3uInstruction4": { "message": "Importe o arquivo .m3u para o seu reprodutor compatível." }
"m3uInstruction4": { "message": "Importe o arquivo .m3u para o seu reprodutor compatível." },
"settingsRegionLabel": { "message": "Região para descoberta de conteúdo" },
"allRegions": { "message": "Todas as regiões" }
}

View File

@ -5,21 +5,25 @@ import { getFromDB } from './db.js';
import { _ } from './utils.js';
export async function fetchTMDB(endpoint, params = {}, signal) {
const langMap = {
'es': 'es-ES',
'en': 'en-US',
'fr': 'fr-FR',
'de': 'de-DE',
'it': 'it-IT',
'pt': 'pt-BR'
const region = state.settings.watchRegion || 'US';
const regionToLangMap = {
'ES': 'es-ES', 'MX': 'es-MX',
'FR': 'fr-FR', 'CA': 'fr-CA',
'DE': 'de-DE',
'IT': 'it-IT',
'PT': 'pt-PT', 'BR': 'pt-BR',
'US': 'en-US', 'GB': 'en-GB'
};
const tmdbLang = langMap[state.settings.language] || 'en-US';
const tmdbLang = regionToLangMap[region] || 'en-US';
const [path, existingQuery] = endpoint.split('?');
const finalParams = new URLSearchParams(existingQuery);
finalParams.set('api_key', state.settings.apiKey);
finalParams.set('language', tmdbLang);
finalParams.set('watch_region', region);
for (const [key, value] of Object.entries(params)) {
if (value) {
@ -27,10 +31,6 @@ export async function fetchTMDB(endpoint, params = {}, signal) {
}
}
if (state.settings.watchRegion && !finalParams.has('watch_region')) {
finalParams.set('watch_region', state.settings.watchRegion);
}
const url = `https://api.themoviedb.org/3/${path}?${finalParams.toString()}`;
const response = await fetch(url, { signal });

View File

@ -1,5 +1,5 @@
export const config = {
defaultApiKey: '4e44d9029b1270a757cddc766a1bcb63',
dbName: 'PlexDB',
dbVersion: 7,
dbVersion: 9,
};

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

@ -13,7 +13,15 @@ async function loadSettings() {
const settingsData = await getFromDB('settings');
if (settingsData && settingsData.length > 0) {
state.settings = { ...state.settings, ...settingsData[0] };
} else {
}
// Ensure a default region is set if none exists
if (!state.settings.watchRegion) {
state.settings.watchRegion = 'US';
}
// Ensure language is always set, fallback to UI language
if (!state.settings.language) {
state.settings.language = chrome.i18n.getUILanguage().split('-')[0];
}
@ -21,19 +29,6 @@ async function loadSettings() {
state.settings.apiKey = config.defaultApiKey;
}
if (!state.settings.watchRegion) {
const tmdbLangMap = {
'es': 'es-ES',
'en': 'en-US',
'fr': 'fr-FR',
'de': 'de-DE',
'it': 'it-IT',
'pt': 'pt-BR'
};
const fullLangCode = tmdbLangMap[state.settings.language] || 'en-US';
state.settings.watchRegion = fullLangCode.split('-')[1] || 'US';
}
const jellyfinSettingsData = await getFromDB('jellyfin_settings');
if (jellyfinSettingsData && jellyfinSettingsData.length > 0) {
state.jellyfinSettings = { ...state.jellyfinSettings, ...jellyfinSettingsData[0] };
@ -41,7 +36,8 @@ async function loadSettings() {
} catch (error) {
console.error("Could not load settings from DB, using defaults.", error);
state.settings.language = chrome.i18n.getUILanguage().split('-')[0];
// Fallback to defaults in case of any error
state.settings.watchRegion = 'US';
}
}

View File

@ -0,0 +1,59 @@
import { showNotification, _ } from './utils.js';
export const phpScriptGenerator = (() => {
let dom = {};
function cacheDom() {
const settingsModal = document.getElementById('settingsModal');
if (!settingsModal) return false;
dom.secretKeyCheck = settingsModal.querySelector('#phpSecretKeyCheck');
dom.secretKey = settingsModal.querySelector('#phpSecretKey');
dom.savePath = settingsModal.querySelector('#phpSavePath');
dom.filename = settingsModal.querySelector('#phpFilename');
dom.fileActionAppendRadio = settingsModal.querySelector('#phpFileActionAppend');
dom.generatedCode = settingsModal.querySelector('#generatedPhpCode');
dom.generateBtn = settingsModal.querySelector('#generatePhpScriptBtn');
dom.copyBtn = settingsModal.querySelector('#copyPhpScriptBtn');
return dom.generateBtn && dom.copyBtn;
}
function init() {
if (!cacheDom()) {
return;
}
dom.generateBtn.addEventListener('click', generatePhpScript);
dom.copyBtn.addEventListener('click', copyScript);
}
function generatePhpScript() {
const useSecretKey = dom.secretKeyCheck.checked;
const secretKey = dom.secretKey.value.trim();
const savePath = dom.savePath.value.trim();
const filename = dom.filename.value.trim() || 'CinePlex_Playlist.m3u';
const appendToFile = dom.fileActionAppendRadio.checked;
let script = `<?php\nheader('Content-Type: application/json');\nheader('Access-Control-Allow-Origin: *');\nheader('Access-Control-Allow-Methods: POST, OPTIONS');\nheader('Access-Control-Allow-Headers: Content-Type, Origin, X-Secret-Key');\n\nif ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {\n http_response_code(200);\n exit(0);\n}\n\ndefine('SAVE_DIRECTORY', '${savePath.replace(/'/g, "\\'")}');\ndefine('FILENAME', '${filename.replace(/'/g, "\\'")}');\ndefine('FILE_ACTION_APPEND', ${appendToFile ? 'true' : 'false'});\n${useSecretKey ? `define('SECRET_KEY', '${secretKey.replace(/'/g, "\\'")}');` : ''}\n\nfunction sendResponse($success, $message, $filename = '', $http_code = 200) {\n if (!$success && $http_code === 200) {\n $http_code = 400;\n }\n http_response_code($http_code);\n echo json_encode(['success' => $success, 'message' => $message, 'filename' => $filename]);\n exit;\n}\n`;
if (useSecretKey) {
script += `\n$auth_key = isset($_SERVER['HTTP_X_SECRET_KEY']) ? $_SERVER['HTTP_X_SECRET_KEY'] : '';\nif (!defined('SECRET_KEY') || SECRET_KEY === '' || $auth_key !== SECRET_KEY) {\n sendResponse(false, 'Acceso no autorizado. Clave secreta inválida o no proporcionada.', '', 403);\n}\n`;
}
script += `\n$json_data = file_get_contents('php://input');\n$data = json_decode($json_data, true);\n\nif (json_last_error() !== JSON_ERROR_NONE) {\n sendResponse(false, 'Error: Datos JSON inválidos.');\n}\n\nif (!isset($data['streams']) || !is_array($data['streams']) || empty($data['streams'])) {\n sendResponse(false, 'Error: El JSON debe contener un array "streams" no vacío.');\n}\n\n$save_dir = SAVE_DIRECTORY !== '' ? rtrim(SAVE_DIRECTORY, '/\\') : __DIR__;\n\nif (!is_dir($save_dir) || !is_writable($save_dir)) {\n sendResponse(false, 'Error del servidor: El directorio de destino no existe o no tiene permisos de escritura.', '', 500);\n}\n\n$safe_filename = preg_replace('/[^\\w\\s._-]/', '', basename(FILENAME));\n$safe_filename = preg_replace('/\\s+/', '_', $safe_filename);\n$target_path = $save_dir . DIRECTORY_SEPARATOR . $safe_filename;\n\n$content_to_write = "";\n\nif (FILE_ACTION_APPEND) {\n $file_exists = file_exists($target_path);\n if (!$file_exists) {\n $content_to_write .= "#EXTM3U\\n";\n }\n foreach ($data['streams'] as $stream) {\n if (isset($stream['extinf'], $stream['url'])) {\n $content_to_write .= trim($stream['extinf']) . "\\n";\n $content_to_write .= trim($stream['url']) . "\\n";\n }\n }\n if (file_put_contents($target_path, $content_to_write, FILE_APPEND | LOCK_EX) !== false) {\n sendResponse(true, 'Streams añadidos correctamente al archivo.', $safe_filename, 200);\n } else {\n sendResponse(false, 'Error del servidor: No se pudo añadir contenido al archivo.', '', 500);\n }\n} else { // Overwrite mode\n $content_to_write = "#EXTM3U\\n";\n foreach ($data['streams'] as $stream) {\n if (isset($stream['extinf'], $stream['url'])) {\n $content_to_write .= trim($stream['extinf']) . "\\n";\n $content_to_write .= trim($stream['url']) . "\\n";\n }\n }\n if (file_put_contents($target_path, $content_to_write, LOCK_EX) !== false) {\n sendResponse(true, 'Archivo de streams sobrescrito correctamente.', $safe_filename, 201);\n } else {\n sendResponse(false, 'Error del servidor: No se pudo escribir el archivo.', '', 500);\n }\n}\n?>`;
dom.generatedCode.value = script;
showNotification(_("scriptGenerated"), "success");
}
function copyScript() {
if (!dom.generatedCode.value || dom.generatedCode.value.trim() === '') {
showNotification(_("errorGeneratingScript"), "warning");
return;
}
navigator.clipboard.writeText(dom.generatedCode.value).then(() => {
showNotification(_("scriptCopied"), "success");
}).catch(err => {
showNotification(_("errorCopyingScript"), "error");
});
}
return { init };
})();

View File

@ -104,13 +104,11 @@ export function renderProviders(providers) {
export async function getProviderItems(providerId, page = 1) {
try {
const watchRegion = state.settings.watchRegion || 'US';
const language = state.settings.language || 'en-US';
const params = {
with_watch_providers: providerId,
watch_region: watchRegion,
page: page,
language: language
};
const moviesResponse = await fetchTMDB('discover/movie', params);

View File

@ -1557,7 +1557,6 @@ export function activateSettingsTab(tabId) {
export function openSettingsModal() {
document.getElementById('tmdbApiKey').value = state.settings.apiKey;
document.getElementById('appLanguage').value = state.settings.language;
document.getElementById('phpScriptUrl').value = state.settings.phpScriptUrl || '';
document.getElementById('lightModeToggle').checked = state.settings.theme === 'light';
document.getElementById('showHeroToggle').checked = state.settings.showHero;
@ -1580,11 +1579,10 @@ export function openSettingsModal() {
}
export async function saveSettings() {
const oldLanguage = state.settings.language;
const oldRegion = state.settings.watchRegion;
const newSettings = {
id: 'user_settings',
apiKey: document.getElementById('tmdbApiKey').value.trim(),
language: document.getElementById('appLanguage').value,
theme: document.getElementById('lightModeToggle').checked ? 'light' : 'dark',
showHero: document.getElementById('showHeroToggle').checked,
phpScriptUrl: document.getElementById('phpScriptUrl').value.trim(),
@ -1615,11 +1613,12 @@ export async function saveSettings() {
showNotification(_('settingsSavedSuccess'), 'success');
applyTheme(state.settings.theme);
applyHeroVisibility(state.settings.showHero);
bootstrap.Modal.getInstance(document.getElementById('settingsModal'))?.hide();
if (newSettings.language !== oldLanguage) {
showNotification(_('languageChangeReload'), 'info', 4000);
setTimeout(() => window.location.reload(), 4000);
if (newSettings.watchRegion !== oldRegion) {
loadInitialContent();
}
bootstrap.Modal.getInstance(document.getElementById('settingsModal'))?.hide();
} catch (error) {
showNotification(_('errorSavingSettings'), 'error');
}

View File

@ -400,17 +400,6 @@
<label for="tmdbApiKey" class="form-label">__MSG_settingsTmdbApiLabel__</label>
<input type="password" class="form-control" id="tmdbApiKey" placeholder="__MSG_settingsTmdbApiPlaceholder__">
</div>
<div class="mb-3">
<label for="appLanguage" class="form-label">__MSG_settingsTmdbLangLabel__</label>
<select class="form-control filter-select" id="appLanguage">
<option value="es">__MSG_lang_es__</option>
<option value="en">__MSG_lang_en__</option>
<option value="fr">__MSG_lang_fr__</option>
<option value="de">__MSG_lang_de__</option>
<option value="it">__MSG_lang_it__</option>
<option value="pt">__MSG_lang_pt__</option>
</select>
</div>
<div class="mb-3">
<label for="region-filter" class="form-label">__MSG_settingsRegionLabel__</label>
<select class="form-control filter-select" id="region-filter">