Compare commits
24 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1593dcc662 | ||
|
|
e38a6b3cff | ||
|
|
947f343919 | ||
|
|
9a0a41d6cb | ||
|
|
011502a94f | ||
|
|
322f5f19e4 | ||
|
|
a384077c28 | ||
|
|
7804bfc735 | ||
|
|
47a3774e28 | ||
|
|
6af194843f | ||
|
|
600c2c8895 | ||
|
|
4ac806d326 | ||
|
|
76ae1ecfb7 | ||
|
|
1c667d3ba7 | ||
|
|
1452483bfd | ||
|
|
3e84b8dc09 | ||
|
|
5e05d61f14 | ||
|
|
97d68ca8eb | ||
|
|
d7d8c89eec | ||
|
|
7a7fe8c773 | ||
|
|
83636a71e9 | ||
|
|
042c89d195 | ||
|
|
91c7ec05d4 | ||
|
|
e30ce50718 |
16 changed files with 1688 additions and 346 deletions
|
|
@ -3,7 +3,7 @@ name: Build and Push Docker image
|
|||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- dev
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
|
@ -1,12 +1,29 @@
|
|||
const sorts = ['quality', 'sizedesc', 'sizeasc', 'qualitythensize'];
|
||||
const qualityExclusions = ['2160p', '1080p', '720p', '480p', 'rips', 'cam', 'hevc', 'unknown'];
|
||||
const languages = ['en', 'fr', 'multi'];
|
||||
const languages = ['en', 'fr', 'multi', 'vfq'];
|
||||
|
||||
|
||||
const implementedDebrids = ['debrid_rd', 'debrid_ad', 'debrid_tb', 'debrid_pm', 'sharewood', 'yggflix'];
|
||||
|
||||
const unimplementedDebrids = ['debrid_dl', 'debrid_ed', 'debrid_oc', 'debrid_pk'];
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
loadData();
|
||||
handleUniqueAccounts();
|
||||
updateProviderFields();
|
||||
updateDebridOrderList();
|
||||
toggleStremThruFields();
|
||||
|
||||
const apiKeyInput = document.getElementById('ApiKey');
|
||||
if (apiKeyInput) {
|
||||
apiKeyInput.addEventListener('blur', function() {
|
||||
validateApiKeyWithoutAlert(this.value);
|
||||
});
|
||||
|
||||
if (apiKeyInput.value && apiKeyInput.value.trim() !== '') {
|
||||
validateApiKeyWithoutAlert(apiKeyInput.value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function setElementDisplay(elementId, displayStatus) {
|
||||
|
|
@ -181,7 +198,7 @@ function pollForADCredentials(check, pin, expiresIn) {
|
|||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data === null) return; // Skip processing if user hasn't entered PIN yet
|
||||
if (data === null) return;
|
||||
if (data.data && data.data.activated && data.data.apikey) {
|
||||
clearInterval(pollInterval);
|
||||
clearTimeout(timeoutId);
|
||||
|
|
@ -253,26 +270,42 @@ function updateDebridOrderList() {
|
|||
const adEnabled = document.getElementById('debrid_ad').checked || document.getElementById('debrid_ad').disabled;
|
||||
const tbEnabled = document.getElementById('debrid_tb').checked || document.getElementById('debrid_tb').disabled;
|
||||
const pmEnabled = document.getElementById('debrid_pm').checked || document.getElementById('debrid_pm').disabled;
|
||||
const dlEnabled = document.getElementById('debrid_dl')?.checked || document.getElementById('debrid_dl')?.disabled;
|
||||
const edEnabled = document.getElementById('debrid_ed')?.checked || document.getElementById('debrid_ed')?.disabled;
|
||||
const ocEnabled = document.getElementById('debrid_oc')?.checked || document.getElementById('debrid_oc')?.disabled;
|
||||
const pkEnabled = document.getElementById('debrid_pk')?.checked || document.getElementById('debrid_pk')?.disabled;
|
||||
|
||||
if (debridOrder.length === 0 ||
|
||||
!debridOrder.every(service =>
|
||||
(service === 'Real-Debrid' && rdEnabled) ||
|
||||
(service === 'AllDebrid' && adEnabled) ||
|
||||
(service === 'TorBox' && tbEnabled) ||
|
||||
(service === 'Premiumize' && pmEnabled)
|
||||
(service === 'Premiumize' && pmEnabled) ||
|
||||
(service === 'Debrid-Link' && dlEnabled) ||
|
||||
(service === 'EasyDebrid' && edEnabled) ||
|
||||
(service === 'Offcloud' && ocEnabled) ||
|
||||
(service === 'PikPak' && pkEnabled)
|
||||
)) {
|
||||
debridOrder = [];
|
||||
if (rdEnabled) debridOrder.push('Real-Debrid');
|
||||
if (adEnabled) debridOrder.push('AllDebrid');
|
||||
if (tbEnabled) debridOrder.push('TorBox');
|
||||
if (pmEnabled) debridOrder.push('Premiumize');
|
||||
if (dlEnabled) debridOrder.push('Debrid-Link');
|
||||
if (edEnabled) debridOrder.push('EasyDebrid');
|
||||
if (ocEnabled) debridOrder.push('Offcloud');
|
||||
if (pkEnabled) debridOrder.push('PikPak');
|
||||
}
|
||||
|
||||
debridOrder.forEach(serviceName => {
|
||||
if ((serviceName === 'Real-Debrid' && rdEnabled) ||
|
||||
(serviceName === 'AllDebrid' && adEnabled) ||
|
||||
(serviceName === 'Premiumize' && pmEnabled) ||
|
||||
(serviceName === 'TorBox' && tbEnabled)) {
|
||||
(serviceName === 'TorBox' && tbEnabled) ||
|
||||
(serviceName === 'Debrid-Link' && dlEnabled) ||
|
||||
(serviceName === 'EasyDebrid' && edEnabled) ||
|
||||
(serviceName === 'Offcloud' && ocEnabled) ||
|
||||
(serviceName === 'PikPak' && pkEnabled)) {
|
||||
addDebridToList(serviceName);
|
||||
}
|
||||
});
|
||||
|
|
@ -289,6 +322,18 @@ function updateDebridOrderList() {
|
|||
if (pmEnabled && !debridOrder.includes('Premiumize')) {
|
||||
addDebridToList('Premiumize');
|
||||
}
|
||||
if (dlEnabled && !debridOrder.includes('Debrid-Link')) {
|
||||
addDebridToList('Debrid-Link');
|
||||
}
|
||||
if (edEnabled && !debridOrder.includes('EasyDebrid')) {
|
||||
addDebridToList('EasyDebrid');
|
||||
}
|
||||
if (ocEnabled && !debridOrder.includes('Offcloud')) {
|
||||
addDebridToList('Offcloud');
|
||||
}
|
||||
if (pkEnabled && !debridOrder.includes('PikPak')) {
|
||||
addDebridToList('PikPak');
|
||||
}
|
||||
|
||||
Sortable.create(debridOrderList, {
|
||||
animation: 150,
|
||||
|
|
@ -330,40 +375,103 @@ function toggleDebridOrderList() {
|
|||
}
|
||||
}
|
||||
|
||||
function toggleStremThruFields() {
|
||||
const stremthruEnabledCheckbox = document.getElementById('stremthru_enabled');
|
||||
if (!stremthruEnabledCheckbox) return;
|
||||
|
||||
const isEnabled = stremthruEnabledCheckbox.checked;
|
||||
const urlDiv = document.getElementById('stremthru_url_div');
|
||||
const authDiv = document.getElementById('stremthru_auth_div');
|
||||
const urlInput = document.getElementById('stremthru_url');
|
||||
const defaultUrl = 'https://stremthru.13377001.xyz/';
|
||||
|
||||
if (isEnabled) {
|
||||
setElementDisplay('stremthru_url_div', 'block');
|
||||
if (authDiv) setElementDisplay('stremthru_auth_div', 'block');
|
||||
|
||||
// Set default URL if empty or placeholder
|
||||
if (urlInput && (!urlInput.value || urlInput.value === urlInput.placeholder)) {
|
||||
urlInput.value = defaultUrl;
|
||||
}
|
||||
} else {
|
||||
setElementDisplay('stremthru_url_div', 'none');
|
||||
if (authDiv) setElementDisplay('stremthru_auth_div', 'none');
|
||||
|
||||
// Si StremThru est désactivé, désactiver et décocher les services non implémentés
|
||||
unimplementedDebrids.forEach(id => {
|
||||
const checkbox = document.getElementById(id);
|
||||
if (checkbox && checkbox.checked) {
|
||||
checkbox.checked = false;
|
||||
// Déclencher manuellement la mise à jour pour masquer les champs
|
||||
updateProviderFields();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function updateDebridDownloaderOptions() {
|
||||
const debridDownloaderOptions = document.getElementById('debridDownloaderOptions');
|
||||
if (!debridDownloaderOptions) return;
|
||||
|
||||
debridDownloaderOptions.innerHTML = '';
|
||||
|
||||
const rdEnabled = document.getElementById('debrid_rd').checked || document.getElementById('debrid_rd').disabled;
|
||||
const adEnabled = document.getElementById('debrid_ad').checked || document.getElementById('debrid_ad').disabled;
|
||||
const tbEnabled = document.getElementById('debrid_tb').checked || document.getElementById('debrid_tb').disabled;
|
||||
const pmEnabled = document.getElementById('debrid_pm').checked || document.getElementById('debrid_pm').disabled;
|
||||
// --- Vérifier les services de débridage standard ---
|
||||
const rdEnabled = document.getElementById('debrid_rd')?.checked || document.getElementById('debrid_rd')?.disabled;
|
||||
const adEnabled = document.getElementById('debrid_ad')?.checked || document.getElementById('debrid_ad')?.disabled;
|
||||
const tbEnabled = document.getElementById('debrid_tb')?.checked || document.getElementById('debrid_tb')?.disabled;
|
||||
const pmEnabled = document.getElementById('debrid_pm')?.checked || document.getElementById('debrid_pm')?.disabled;
|
||||
const dlEnabled = document.getElementById('debrid_dl')?.checked || document.getElementById('debrid_dl')?.disabled;
|
||||
const edEnabled = document.getElementById('debrid_ed')?.checked || document.getElementById('debrid_ed')?.disabled;
|
||||
const ocEnabled = document.getElementById('debrid_oc')?.checked || document.getElementById('debrid_oc')?.disabled;
|
||||
const pkEnabled = document.getElementById('debrid_pk')?.checked || document.getElementById('debrid_pk')?.disabled;
|
||||
|
||||
// --- Vérifier StremThru ---
|
||||
const stremthruEnabledCheckbox = document.getElementById('stremthru_enabled');
|
||||
const stremthruEnabled = stremthruEnabledCheckbox ? stremthruEnabledCheckbox.checked : false;
|
||||
|
||||
let firstOption = null;
|
||||
|
||||
// --- Ajouter des options en fonction des services activés ---
|
||||
if (rdEnabled) {
|
||||
firstOption = addDebridDownloaderOption('Real-Debrid');
|
||||
}
|
||||
if (adEnabled) {
|
||||
firstOption = addDebridDownloaderOption('AllDebrid');
|
||||
// Utiliser l'opérateur ternaire pour une attribution plus propre
|
||||
firstOption = firstOption ? firstOption : addDebridDownloaderOption('AllDebrid');
|
||||
if (firstOption.value !== 'AllDebrid') addDebridDownloaderOption('AllDebrid');
|
||||
}
|
||||
if (tbEnabled) {
|
||||
if (!firstOption) {
|
||||
firstOption = addDebridDownloaderOption('TorBox');
|
||||
} else {
|
||||
addDebridDownloaderOption('TorBox');
|
||||
}
|
||||
firstOption = firstOption ? firstOption : addDebridDownloaderOption('TorBox');
|
||||
if (firstOption.value !== 'TorBox') addDebridDownloaderOption('TorBox');
|
||||
}
|
||||
if (pmEnabled) {
|
||||
if (!firstOption) {
|
||||
firstOption = addDebridDownloaderOption('Premiumize');
|
||||
} else {
|
||||
addDebridDownloaderOption('Premiumize');
|
||||
}
|
||||
firstOption = firstOption ? firstOption : addDebridDownloaderOption('Premiumize');
|
||||
if (firstOption.value !== 'Premiumize') addDebridDownloaderOption('Premiumize');
|
||||
}
|
||||
if (dlEnabled) {
|
||||
firstOption = firstOption ? firstOption : addDebridDownloaderOption('Debrid-Link');
|
||||
if (firstOption.value !== 'Debrid-Link') addDebridDownloaderOption('Debrid-Link');
|
||||
}
|
||||
if (edEnabled) {
|
||||
firstOption = firstOption ? firstOption : addDebridDownloaderOption('EasyDebrid');
|
||||
if (firstOption.value !== 'EasyDebrid') addDebridDownloaderOption('EasyDebrid');
|
||||
}
|
||||
if (ocEnabled) {
|
||||
firstOption = firstOption ? firstOption : addDebridDownloaderOption('Offcloud');
|
||||
if (firstOption.value !== 'Offcloud') addDebridDownloaderOption('Offcloud');
|
||||
}
|
||||
if (pkEnabled) {
|
||||
firstOption = firstOption ? firstOption : addDebridDownloaderOption('PikPak');
|
||||
if (firstOption.value !== 'PikPak') addDebridDownloaderOption('PikPak');
|
||||
}
|
||||
|
||||
// --- Ajouter StremThru si activé ---
|
||||
if (stremthruEnabled) {
|
||||
firstOption = firstOption ? firstOption : addDebridDownloaderOption('StremThru');
|
||||
if (firstOption.value !== 'StremThru') addDebridDownloaderOption('StremThru');
|
||||
}
|
||||
|
||||
// Sélectionner la première option ajoutée par défaut si aucune n'est sélectionnée
|
||||
if (firstOption && !document.querySelector('input[name="debrid_downloader"]:checked')) {
|
||||
firstOption.checked = true;
|
||||
}
|
||||
|
|
@ -397,33 +505,91 @@ function addDebridDownloaderOption(serviceName) {
|
|||
|
||||
|
||||
function updateProviderFields() {
|
||||
const RDdebridChecked = document.getElementById('debrid_rd').checked ||
|
||||
document.getElementById('debrid_rd').disabled;
|
||||
const ADdebridChecked = document.getElementById('debrid_ad').checked ||
|
||||
document.getElementById('debrid_ad').disabled;
|
||||
const TBdebridChecked = document.getElementById('debrid_tb').checked ||
|
||||
document.getElementById('debrid_tb').disabled;
|
||||
const PMdebridChecked = document.getElementById('debrid_pm').checked ||
|
||||
document.getElementById('debrid_pm').disabled;
|
||||
const cacheChecked = document.getElementById('cache')?.checked;
|
||||
const yggflixChecked = document.getElementById('yggflix')?.checked ||
|
||||
document.getElementById('yggflix')?.disabled;
|
||||
const sharewoodChecked = document.getElementById('sharewood')?.checked ||
|
||||
document.getElementById('sharewood')?.disabled;
|
||||
console.log("--- Running updateProviderFields ---"); // Debug start
|
||||
const stremthruEnabledCheckbox = document.getElementById('stremthru_enabled');
|
||||
let stremthruWasEnabled = stremthruEnabledCheckbox ? stremthruEnabledCheckbox.checked : false; // Track initial state
|
||||
let stremthruForcedEnable = false;
|
||||
let anyUnimplementedChecked = false; // Flag to track if any unimplemented service is checked
|
||||
|
||||
setElementDisplay('rd_debrid-fields', RDdebridChecked ? 'block' : 'none');
|
||||
setElementDisplay('ad_debrid-fields', ADdebridChecked ? 'block' : 'none');
|
||||
setElementDisplay('tb_debrid-fields', TBdebridChecked ? 'block' : 'none');
|
||||
setElementDisplay('pm_debrid-fields', PMdebridChecked ? 'block' : 'none');
|
||||
const serviceStates = {};
|
||||
const allDebrids = [...implementedDebrids, ...unimplementedDebrids];
|
||||
|
||||
// Vérifier l'état des autres éléments de l'interface
|
||||
const cacheChecked = document.getElementById('cache')?.checked;
|
||||
const yggflixChecked = document.getElementById('yggflix')?.checked || document.getElementById('yggflix')?.disabled;
|
||||
const sharewoodChecked = document.getElementById('sharewood')?.checked || document.getElementById('sharewood')?.disabled;
|
||||
const torboxChecked = document.getElementById('debrid_tb')?.checked || document.getElementById('debrid_tb')?.disabled;
|
||||
|
||||
// Afficher/masquer les champs spécifiques
|
||||
setElementDisplay('cache-fields', cacheChecked ? 'block' : 'none');
|
||||
setElementDisplay('ygg-fields', yggflixChecked ? 'block' : 'none');
|
||||
setElementDisplay('sharewood-fields', sharewoodChecked ? 'block' : 'none');
|
||||
setElementDisplay('tb_debrid-fields', torboxChecked ? 'block' : 'none');
|
||||
|
||||
// Traiter tous les débrideurs
|
||||
allDebrids.forEach(id => {
|
||||
const checkbox = document.getElementById(id);
|
||||
if (!checkbox) return;
|
||||
const isChecked = checkbox.checked || checkbox.disabled;
|
||||
serviceStates[id] = isChecked;
|
||||
|
||||
// Déterminer l'ID du div de credentials correspondant
|
||||
let credDivId = '';
|
||||
switch (id) {
|
||||
case 'debrid_rd': credDivId = 'rd_token_info_div'; break;
|
||||
case 'debrid_ad': credDivId = 'ad_token_info_div'; break;
|
||||
case 'debrid_pm': credDivId = 'pm_token_info_div'; break;
|
||||
case 'debrid_tb': credDivId = 'tb_token_info_div'; break;
|
||||
case 'debrid_dl': credDivId = 'debridlink_api_key_div'; break;
|
||||
case 'debrid_ed': credDivId = 'easydebrid_api_key_div'; break;
|
||||
case 'debrid_oc': credDivId = 'offcloud_credentials_div'; break;
|
||||
case 'debrid_pk': credDivId = 'pikpak_credentials_div'; break;
|
||||
}
|
||||
|
||||
// Afficher/masquer le div de credentials
|
||||
if (credDivId) {
|
||||
setElementDisplay(credDivId, isChecked ? 'block' : 'none');
|
||||
}
|
||||
|
||||
// Logique pour forcer l'activation de StremThru avec les débrideurs non implémentés
|
||||
if (unimplementedDebrids.includes(id) && isChecked) {
|
||||
if (stremthruEnabledCheckbox && !stremthruEnabledCheckbox.checked) {
|
||||
stremthruEnabledCheckbox.checked = true;
|
||||
stremthruForcedEnable = true; // Marquer qu'on l'a forcé
|
||||
}
|
||||
anyUnimplementedChecked = true; // Définir le drapeau
|
||||
}
|
||||
});
|
||||
|
||||
// Gérer l'état de la case à cocher StremThru: désactiver si un service non implémenté est coché
|
||||
if (stremthruEnabledCheckbox) {
|
||||
if (anyUnimplementedChecked) {
|
||||
stremthruEnabledCheckbox.checked = true; // S'assurer qu'elle est cochée
|
||||
stremthruEnabledCheckbox.disabled = true; // Désactiver la case à cocher
|
||||
} else {
|
||||
stremthruEnabledCheckbox.disabled = false; // Réactiver si aucun service non implémenté n'est coché
|
||||
}
|
||||
|
||||
// Afficher/masquer les champs StremThru
|
||||
setElementDisplay('stremthru_url_div', stremthruEnabledCheckbox.checked ? 'block' : 'none');
|
||||
const authDiv = document.getElementById('stremthru_auth_div');
|
||||
if (authDiv) {
|
||||
setElementDisplay('stremthru_auth_div', stremthruEnabledCheckbox.checked ? 'block' : 'none');
|
||||
}
|
||||
}
|
||||
|
||||
// Si on a forcé l'activation de StremThru OU si son état a changé, mettre à jour la visibilité de ses champs
|
||||
if (stremthruEnabledCheckbox && (stremthruForcedEnable || stremthruEnabledCheckbox.checked !== stremthruWasEnabled || anyUnimplementedChecked)) {
|
||||
toggleStremThruFields();
|
||||
}
|
||||
|
||||
// Gérer l'ordre des débrideurs
|
||||
const debridOrderCheckbox = document.getElementById('debrid_order');
|
||||
const debridOrderList = document.getElementById('debridOrderList');
|
||||
|
||||
if (debridOrderCheckbox && debridOrderList) {
|
||||
const anyDebridEnabled = RDdebridChecked || ADdebridChecked || TBdebridChecked || PMdebridChecked;
|
||||
// Vérifier si au moins un débrideur est activé
|
||||
const anyDebridEnabled = Object.values(serviceStates).some(state => state);
|
||||
|
||||
debridOrderCheckbox.disabled = !anyDebridEnabled;
|
||||
|
||||
|
|
@ -434,26 +600,57 @@ function updateProviderFields() {
|
|||
debridOrderList.classList.toggle('hidden', !(anyDebridEnabled && debridOrderCheckbox.checked));
|
||||
}
|
||||
|
||||
// Mettre à jour les listes et options
|
||||
updateDebridOrderList();
|
||||
updateDebridDownloaderOptions();
|
||||
|
||||
ensureDebridConsistency();
|
||||
console.log("--- Finished updateProviderFields ---"); // Debug end
|
||||
}
|
||||
|
||||
function ensureDebridConsistency() {
|
||||
const RDdebridChecked = document.getElementById('debrid_rd').checked;
|
||||
const ADdebridChecked = document.getElementById('debrid_ad').checked;
|
||||
const TBdebridChecked = document.getElementById('debrid_tb').checked;
|
||||
const PMdebridChecked = document.getElementById('debrid_pm').checked;
|
||||
const debridOrderChecked = document.getElementById('debrid_order').checked;
|
||||
// Récupérer l'état de tous les débrideurs
|
||||
const serviceStates = {};
|
||||
const allDebrids = [...implementedDebrids, ...unimplementedDebrids];
|
||||
let anyDebridChecked = false;
|
||||
let anyUnimplementedChecked = false;
|
||||
|
||||
if (!RDdebridChecked && !ADdebridChecked && !TBdebridChecked && !PMdebridChecked) {
|
||||
document.getElementById('debrid_order').checked = false;
|
||||
document.getElementById('debridOrderList').classList.add('hidden');
|
||||
allDebrids.forEach(id => {
|
||||
const checkbox = document.getElementById(id);
|
||||
if (!checkbox) return;
|
||||
const isChecked = checkbox.checked;
|
||||
serviceStates[id] = isChecked;
|
||||
if (isChecked) {
|
||||
anyDebridChecked = true;
|
||||
if (unimplementedDebrids.includes(id)) {
|
||||
anyUnimplementedChecked = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Gérer l'état de la case à cocher d'ordre des débrideurs
|
||||
const debridOrderCheckbox = document.getElementById('debrid_order');
|
||||
const debridOrderList = document.getElementById('debridOrderList');
|
||||
|
||||
if (debridOrderCheckbox && debridOrderList) {
|
||||
if (!anyDebridChecked) {
|
||||
debridOrderCheckbox.checked = false;
|
||||
debridOrderList.classList.add('hidden');
|
||||
}
|
||||
|
||||
if (debridOrderCheckbox.checked && !anyDebridChecked) {
|
||||
debridOrderCheckbox.checked = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (debridOrderChecked && !RDdebridChecked && !ADdebridChecked && !TBdebridChecked && !PMdebridChecked) {
|
||||
document.getElementById('debrid_order').checked = false;
|
||||
// Gérer l'état de la case à cocher StremThru
|
||||
const stremthruEnabledCheckbox = document.getElementById('stremthru_enabled');
|
||||
if (stremthruEnabledCheckbox) {
|
||||
if (anyUnimplementedChecked) {
|
||||
stremthruEnabledCheckbox.checked = true;
|
||||
stremthruEnabledCheckbox.disabled = true;
|
||||
} else {
|
||||
stremthruEnabledCheckbox.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
updateDebridDownloaderOptions();
|
||||
|
|
@ -497,8 +694,8 @@ function loadData() {
|
|||
ctg_yggtorrent: true,
|
||||
ctg_yggflix: false,
|
||||
metadataProvider: 'tmdb',
|
||||
sort: 'qualitythensize',
|
||||
exclusion: ['cam', '2160p'],
|
||||
sort: 'quality',
|
||||
exclusion: ['cam'],
|
||||
languages: ['fr', 'multi'],
|
||||
debrid_rd: false,
|
||||
debrid_ad: false,
|
||||
|
|
@ -569,10 +766,50 @@ function loadData() {
|
|||
ensureDebridConsistency();
|
||||
}
|
||||
|
||||
// Fonction pour valider l'API key
|
||||
function validateApiKey(apiKey) {
|
||||
// Référence à l'élément d'erreur
|
||||
const apiKeyErrorElement = document.getElementById('apiKeyError');
|
||||
|
||||
// Si aucune API key n'est fournie
|
||||
if (!apiKey || apiKey.trim() === '') {
|
||||
if (apiKeyErrorElement) {
|
||||
apiKeyErrorElement.classList.remove('hidden');
|
||||
}
|
||||
alert('Veuillez fournir une API Key Stream Fusion. Utilisez le bot Telegram : t.me/Stremiofr_bot - Envoyez la commande /generate');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Vérification du format UUID v4
|
||||
const isValidFormat = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(apiKey);
|
||||
|
||||
if (!isValidFormat) {
|
||||
if (apiKeyErrorElement) {
|
||||
apiKeyErrorElement.classList.remove('hidden');
|
||||
}
|
||||
alert('API Key invalide. Utilisez le bot Telegram : t.me/Stremiofr_bot - Envoyez la commande /generate');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Si l'API key est valide, masquer le message d'erreur
|
||||
if (apiKeyErrorElement) {
|
||||
apiKeyErrorElement.classList.add('hidden');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function getLink(method) {
|
||||
const apiKey = document.getElementById('ApiKey').value;
|
||||
|
||||
// Vérifier l'API key en premier
|
||||
if (!validateApiKey(apiKey)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const data = {
|
||||
addonHost: new URL(window.location.href).origin,
|
||||
apiKey: document.getElementById('ApiKey').value,
|
||||
apiKey: apiKey,
|
||||
service: [],
|
||||
RDToken: document.getElementById('rd_token_info')?.value,
|
||||
ADToken: document.getElementById('ad_token_info')?.value,
|
||||
|
|
@ -601,7 +838,15 @@ function getLink(method) {
|
|||
torrenting: document.getElementById('torrenting').checked,
|
||||
debrid: false,
|
||||
metadataProvider: document.getElementById('tmdb').checked ? 'tmdb' : 'cinemeta',
|
||||
debridDownloader: document.querySelector('input[name="debrid_downloader"]:checked')?.value
|
||||
debridDownloader: document.querySelector('input[name="debrid_downloader"]:checked')?.value,
|
||||
// StremThru configuration
|
||||
stremthru: document.getElementById('stremthru_enabled')?.checked || false,
|
||||
stremthruUrl: document.getElementById('stremthru_url')?.value || 'https://stremthru.13377001.xyz',
|
||||
// Nouveaux débrideurs
|
||||
debridlinkApiKey: document.getElementById('debridlink_api_key')?.value || '',
|
||||
easydebridApiKey: document.getElementById('easydebrid_api_key')?.value || '',
|
||||
offcloudCredentials: document.getElementById('offcloud_credentials')?.value || '',
|
||||
pikpakCredentials: document.getElementById('pikpak_credentials')?.value || ''
|
||||
};
|
||||
|
||||
data.service = Array.from(document.getElementById('debridOrderList').children).map(li => li.dataset.serviceName);
|
||||
|
|
@ -614,9 +859,14 @@ function getLink(method) {
|
|||
if (data.service.includes('AllDebrid') && document.getElementById('ad_token_info') && !data.ADToken) missingRequiredFields.push("AllDebrid Account Connection");
|
||||
if (data.service.includes('TorBox') && document.getElementById('tb_token_info') && !data.TBToken) missingRequiredFields.push("TorBox Account Connection");
|
||||
if (data.service.includes('Premiumize') && document.getElementById('pm_token_info') && !data.PMToken) missingRequiredFields.push("Premiumize Account Connection");
|
||||
if (data.service.includes('Debrid-Link') && document.getElementById('debridlink_api_key') && !data.debridlinkApiKey) missingRequiredFields.push("Debrid-Link API Key");
|
||||
if (data.service.includes('EasyDebrid') && document.getElementById('easydebrid_api_key') && !data.easydebridApiKey) missingRequiredFields.push("EasyDebrid API Key");
|
||||
if (data.service.includes('Offcloud') && document.getElementById('offcloud_credentials') && !data.offcloudCredentials) missingRequiredFields.push("Offcloud Credentials");
|
||||
if (data.service.includes('PikPak') && document.getElementById('pikpak_credentials') && !data.pikpakCredentials) missingRequiredFields.push("PikPak Credentials");
|
||||
if (data.languages.length === 0) missingRequiredFields.push("Languages");
|
||||
if (data.yggflix && document.getElementById('yggPasskey') && !data.yggPasskey) missingRequiredFields.push("Ygg Passkey");
|
||||
if (data.sharewood && document.getElementById('sharewoodPasskey') && !data.sharewoodPasskey) missingRequiredFields.push("Sharewood Passkey");
|
||||
if (data.stremthru && !data.stremthruUrl) missingRequiredFields.push("StremThru URL");
|
||||
|
||||
if (missingRequiredFields.length > 0) {
|
||||
alert(`Please fill all required fields: ${missingRequiredFields.join(", ")}`);
|
||||
|
|
@ -627,10 +877,6 @@ function getLink(method) {
|
|||
return /^[a-zA-Z0-9]{32}$/.test(passkey);
|
||||
}
|
||||
|
||||
function validateApiKey(apiKey) {
|
||||
return /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(apiKey);
|
||||
}
|
||||
|
||||
if (data.yggflix && data.yggPasskey && !validatePasskey(data.yggPasskey)) {
|
||||
alert('Ygg Passkey doit contenir exactement 32 caractères alphanumériques');
|
||||
return false;
|
||||
|
|
@ -641,11 +887,6 @@ function getLink(method) {
|
|||
return false;
|
||||
}
|
||||
|
||||
if (data.apiKey && !validateApiKey(data.apiKey)) {
|
||||
alert('APIKEY doit être un UUID v4 valide');
|
||||
return false;
|
||||
}
|
||||
|
||||
const encodedData = btoa(JSON.stringify(data));
|
||||
const stremio_link = `${window.location.host}/${encodedData}/manifest.json`;
|
||||
|
||||
|
|
@ -667,3 +908,33 @@ function showCheckboxes() {
|
|||
checkboxes.style.display = showLanguageCheckBoxes ? "block" : "none";
|
||||
showLanguageCheckBoxes = !showLanguageCheckBoxes;
|
||||
}
|
||||
|
||||
// Fonction pour valider l'API key sans afficher d'alert
|
||||
function validateApiKeyWithoutAlert(apiKey) {
|
||||
const apiKeyErrorElement = document.getElementById('apiKeyError');
|
||||
|
||||
// Si aucune API key n'est fournie
|
||||
if (!apiKey || apiKey.trim() === '') {
|
||||
if (apiKeyErrorElement) {
|
||||
apiKeyErrorElement.classList.remove('hidden');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Vérification du format UUID v4
|
||||
const isValidFormat = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(apiKey);
|
||||
|
||||
if (!isValidFormat) {
|
||||
if (apiKeyErrorElement) {
|
||||
apiKeyErrorElement.classList.remove('hidden');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Si l'API key est valide, masquer le message d'erreur
|
||||
if (apiKeyErrorElement) {
|
||||
apiKeyErrorElement.classList.add('hidden');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,8 +51,18 @@
|
|||
<label for="ApiKey" class="block text-sm font-medium leading-6 text-red-600 text-center">API Key</label>
|
||||
<input type="text" name="ApiKey" id="ApiKey"
|
||||
class="mt-2 block w-full rounded-md border-0 py-1.5 px-3 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6">
|
||||
<div id="apiKeyError" class="hidden mt-2 text-sm text-center text-red-600">
|
||||
<p>API Key manquante ou invalide</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 text-center">
|
||||
<p class="text-sm text-gray-200 bg-gray-800 rounded-lg p-3 shadow-md">
|
||||
<span class="font-bold text-white">Comment obtenir votre API Key ?</span><br>
|
||||
1. Ajoutez le Bot Telegram : <a href="https://t.me/Stremiofr_bot" target="_blank" class="text-blue-400 hover:text-blue-300 underline font-medium">t.me/Stremiofr_bot</a><br>
|
||||
2. Envoyez la commande <span class="bg-blue-900 text-white px-2 py-1 rounded font-mono">/generate</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -70,13 +80,27 @@
|
|||
<input id="debrid_rd" name="debrid_rd" type="checkbox"
|
||||
data-unique-account="{{ rd_unique_account|lower }}"
|
||||
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600"
|
||||
onchange="updateProviderFields(true)">
|
||||
onchange="updateProviderFields()">
|
||||
</div>
|
||||
<div class="text-sm leading-6">
|
||||
<label for="debrid_rd" class="font-medium text-white">Enable Real-Debrid Service</label>
|
||||
<p class="text-gray-500 dark:text-gray-300">Activate Real-Débrid service for streaming.
|
||||
</p>
|
||||
</div>
|
||||
<!-- RealDebrid Token Info -->
|
||||
<div id="rd_token_info_div" class="mt-4 space-y-3" style="display: none;">
|
||||
<p class="text-sm text-gray-400">Real-Debrid nécessite une authentification OAuth.</p>
|
||||
<button type="button" id="rd-auth-button" onclick="startRealDebridAuth()"
|
||||
class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 transition ease-in-out duration-150">
|
||||
S'authentifier avec Real-Debrid
|
||||
</button>
|
||||
<div id="auth-instructions" style="display: none;" class="text-sm text-gray-300">
|
||||
<p>Veuillez visiter <a id="verification-url" href="#" target="_blank"
|
||||
class="font-medium text-indigo-400 hover:text-indigo-300">l'URL de vérification</a>
|
||||
et entrer le code: <strong id="user-code" class="text-white"></strong></p>
|
||||
</div>
|
||||
<input type="hidden" id="rd_token_info" name="rd_token_info">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative flex gap-x-3">
|
||||
|
|
@ -84,13 +108,27 @@
|
|||
<input id="debrid_ad" name="debrid_ad" type="checkbox"
|
||||
data-unique-account="{{ ad_unique_account|lower }}"
|
||||
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600"
|
||||
onchange="updateProviderFields(true)">
|
||||
onchange="updateProviderFields()">
|
||||
</div>
|
||||
<div class="text-sm leading-6">
|
||||
<label for="debrid_ad" class="font-medium text-white">Enable AllDebrid Service</label>
|
||||
<p class="text-gray-500 dark:text-gray-300">Activate AllDebrid service for streaming.
|
||||
</p>
|
||||
</div>
|
||||
<!-- AllDebrid Token Info -->
|
||||
<div id="ad_token_info_div" class="mt-4 space-y-3" style="display: none;">
|
||||
<p class="text-sm text-gray-400">AllDebrid nécessite une authentification OAuth.</p>
|
||||
<button type="button" id="ad-auth-button" onclick="startADAuth()"
|
||||
class="rounded-md bg-orange-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-orange-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-orange-600 transition ease-in-out duration-150">
|
||||
Connect with AllDebrid
|
||||
</button>
|
||||
<div id="ad-auth-instructions" style="display: none;" class="text-sm text-gray-300">
|
||||
<p>Please visit <a id="ad-verification-url" href="#" target="_blank"
|
||||
class="font-medium text-orange-400 hover:text-orange-300">verification URL</a>
|
||||
and enter the code: <strong id="ad-user-code" class="text-white"></strong></p>
|
||||
</div>
|
||||
<input type="hidden" id="ad_token_info" name="ad_token_info">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative flex gap-x-3">
|
||||
|
|
@ -98,12 +136,22 @@
|
|||
<input id="debrid_pm" name="debrid_pm" type="checkbox"
|
||||
data-unique-account="{{ pm_unique_account|lower }}"
|
||||
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600"
|
||||
onchange="updateProviderFields(true)">
|
||||
onchange="updateProviderFields()">
|
||||
</div>
|
||||
<div class="text-sm leading-6">
|
||||
<label for="debrid_pm" class="font-medium text-white">Enable Premiumize Service</label>
|
||||
<p class="text-gray-500 dark:text-gray-300">Activate Premiumize service for streaming.</p>
|
||||
</div>
|
||||
<!-- Premiumize Token Info -->
|
||||
<div id="pm_token_info_div" class="mt-2" style="display: none;">
|
||||
<label for="pm_token_info" class="block text-sm font-medium leading-6 text-white">Premiumize
|
||||
API Key</label>
|
||||
<input type="text" name="pm_token_info" id="pm_token_info"
|
||||
placeholder="Votre clé API Premiumize"
|
||||
class="mt-1 block w-full rounded-md border-0 py-1.5 px-3 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6">
|
||||
<p class="mt-1 text-xs text-gray-400">Trouvez votre clé API dans les paramètres de votre compte
|
||||
Premiumize.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative flex gap-x-3">
|
||||
|
|
@ -111,13 +159,93 @@
|
|||
<input id="debrid_tb" name="debrid_tb" type="checkbox"
|
||||
data-unique-account="{{ tb_unique_account|lower }}"
|
||||
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600"
|
||||
onchange="updateProviderFields(true)">
|
||||
onchange="updateProviderFields()">
|
||||
</div>
|
||||
<div class="text-sm leading-6">
|
||||
<label for="debrid_tb" class="font-medium text-white">Enable TorBox Service</label>
|
||||
<p class="text-gray-500 dark:text-gray-300">Activate TorBox service for streaming.
|
||||
</p>
|
||||
</div>
|
||||
<!-- TorBox Token Info -->
|
||||
<div id="tb_token_info_div" class="mt-2" style="display: none;">
|
||||
<label for="tb_token_info" class="block text-sm font-medium leading-6 text-white">TorBox API
|
||||
Key</label>
|
||||
<input type="text" name="tb_token_info" id="tb_token_info" placeholder="Votre clé API TorBox"
|
||||
class="mt-1 block w-full rounded-md border-0 py-1.5 px-3 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6">
|
||||
<p class="mt-1 text-xs text-gray-400">Disponible dans les paramètres de votre compte TorBox.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative flex gap-x-3">
|
||||
<div class="flex h-6 items-center">
|
||||
<input id="debrid_dl" name="debrid_dl" type="checkbox"
|
||||
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600"
|
||||
onchange="updateProviderFields()">
|
||||
</div>
|
||||
<div class="text-sm leading-6">
|
||||
<label for="debrid_dl" class="font-medium text-white">Enable Debrid-Link Service</label>
|
||||
<p class="text-gray-500 dark:text-gray-300">Activate Debrid-Link service for streaming.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="debridlink_api_key_div" class="mt-2 hidden">
|
||||
<label for="debridlink_api_key" class="block text-sm font-medium leading-6 text-white">Debrid-Link API Key</label>
|
||||
<input type="text" name="debridlink_api_key" id="debridlink_api_key"
|
||||
class="mt-2 block w-full rounded-md border-0 py-1.5 px-3 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
|
||||
placeholder="Enter your Debrid-Link API Key">
|
||||
</div>
|
||||
|
||||
<div class="relative flex gap-x-3">
|
||||
<div class="flex h-6 items-center">
|
||||
<input id="debrid_ed" name="debrid_ed" type="checkbox"
|
||||
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600"
|
||||
onchange="updateProviderFields()">
|
||||
</div>
|
||||
<div class="text-sm leading-6">
|
||||
<label for="debrid_ed" class="font-medium text-white">Enable EasyDebrid Service</label>
|
||||
<p class="text-gray-500 dark:text-gray-300">Activate EasyDebrid service for streaming.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="easydebrid_api_key_div" class="mt-2 hidden">
|
||||
<label for="easydebrid_api_key" class="block text-sm font-medium leading-6 text-white">EasyDebrid API Key</label>
|
||||
<input type="text" name="easydebrid_api_key" id="easydebrid_api_key"
|
||||
class="mt-2 block w-full rounded-md border-0 py-1.5 px-3 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
|
||||
placeholder="Enter your EasyDebrid API Key">
|
||||
</div>
|
||||
|
||||
<div class="relative flex gap-x-3">
|
||||
<div class="flex h-6 items-center">
|
||||
<input id="debrid_oc" name="debrid_oc" type="checkbox"
|
||||
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600"
|
||||
onchange="updateProviderFields()">
|
||||
</div>
|
||||
<div class="text-sm leading-6">
|
||||
<label for="debrid_oc" class="font-medium text-white">Enable Offcloud Service</label>
|
||||
<p class="text-gray-500 dark:text-gray-300">Activate Offcloud service for streaming.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="offcloud_credentials_div" class="mt-2 hidden">
|
||||
<label for="offcloud_credentials" class="block text-sm font-medium leading-6 text-white">Offcloud Credentials (email:password)</label>
|
||||
<input type="text" name="offcloud_credentials" id="offcloud_credentials"
|
||||
class="mt-2 block w-full rounded-md border-0 py-1.5 px-3 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
|
||||
placeholder="Enter your Offcloud email:password">
|
||||
</div>
|
||||
|
||||
<div class="relative flex gap-x-3">
|
||||
<div class="flex h-6 items-center">
|
||||
<input id="debrid_pk" name="debrid_pk" type="checkbox"
|
||||
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600"
|
||||
onchange="updateProviderFields()">
|
||||
</div>
|
||||
<div class="text-sm leading-6">
|
||||
<label for="debrid_pk" class="font-medium text-white">Enable PikPak Service</label>
|
||||
<p class="text-gray-500 dark:text-gray-300">Activate PikPak service for streaming.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="pikpak_credentials_div" class="mt-2 hidden">
|
||||
<label for="pikpak_credentials" class="block text-sm font-medium leading-6 text-white">PikPak Credentials (email:password)</label>
|
||||
<input type="text" name="pikpak_credentials" id="pikpak_credentials"
|
||||
class="mt-2 block w-full rounded-md border-0 py-1.5 px-3 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
|
||||
placeholder="Enter your PikPak email:password">
|
||||
</div>
|
||||
|
||||
<div class="relative flex gap-x-3">
|
||||
|
|
@ -126,15 +254,52 @@
|
|||
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600">
|
||||
</div>
|
||||
<div class="text-sm leading-6">
|
||||
<label for="torrenting" class="font-medium text-white">Enable Torrenting</label>
|
||||
<p class="text-gray-500 dark:text-gray-300"><span
|
||||
class="font-bold text-red-600 dark:text-red-500">Warning: This may be illegal in
|
||||
<label for="torrenting" class="font-medium text-white">Enable Torrenting <span
|
||||
class="text-red-500">(Risky)</span></label>
|
||||
<p class="text-gray-500 dark:text-gray-300">Activate direct torrent streaming. <span
|
||||
class="text-red-500">This may be illegal in
|
||||
some countries. Use at your own risk.</span></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- StremThru Section -->
|
||||
<div class="border-b border-gray-900/10 dark:border-white/50 pb-12">
|
||||
<div class="flex flex-col items-center">
|
||||
<h2 class="text-white font-semibold leading-7">StremThru Settings</h2>
|
||||
<p class="mt-1 text-sm leading-6 text-gray-600 dark:text-gray-400">Configure StremThru as an optional interface.</p>
|
||||
</div>
|
||||
<div class="mt-10 space-y-10">
|
||||
<!-- Enable StremThru Checkbox -->
|
||||
<div class="relative flex gap-x-3">
|
||||
<div class="flex h-6 items-center">
|
||||
<input id="stremthru_enabled" name="stremthru_enabled" type="checkbox" class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600" onchange="toggleStremThruFields()">
|
||||
</div>
|
||||
<div class="text-sm leading-6">
|
||||
<label for="stremthru_enabled" class="font-medium text-white">Enable StremThru</label>
|
||||
<p class="text-gray-500 dark:text-gray-300">Use StremThru to manage debrid services.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- StremThru URL Input -->
|
||||
<div id="stremthru_url_div" class="hidden">
|
||||
<label for="stremthru_url" class="block text-sm font-medium leading-6 text-white">StremThru URL</label>
|
||||
<input type="text" name="stremthru_url" id="stremthru_url"
|
||||
class="mt-2 block w-full rounded-md border-0 py-1.5 px-3 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
|
||||
placeholder="https://stremthru.13377001.xyz/">
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Enter the URL of your StremThru instance.</p>
|
||||
</div>
|
||||
<!-- StremThru Store Auth Input -->
|
||||
<div id="stremthru_auth_div" class="hidden">
|
||||
<label for="stremthru_store_auth" class="block text-sm font-medium leading-6 text-white">StremThru Store Authorization (Optional)</label>
|
||||
<input type="text" name="stremthru_store_auth" id="stremthru_store_auth"
|
||||
class="mt-2 block w-full rounded-md border-0 py-1.5 px-3 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
|
||||
placeholder="Optional global auth key">
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Global key if required by your StremThru instance.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-b border-gray-900/10 dark:border-white/50 pb-12">
|
||||
<div class="flex flex-col items-center">
|
||||
<h2 class="text-white font-semibold leading-7">Torrent Providers</h2>
|
||||
|
|
@ -161,7 +326,7 @@
|
|||
<div class="flex h-6 items-center">
|
||||
<input id="cache" name="cache" type="checkbox" checked
|
||||
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600"
|
||||
onchange="updateProviderFields(true)">
|
||||
onchange="updateProviderFields()">
|
||||
</div>
|
||||
<div class="text-sm leading-6">
|
||||
<label for="cache" class="font-medium text-white">Enable Public Caching</label>
|
||||
|
|
@ -191,7 +356,7 @@
|
|||
<input id="yggflix" name="yggflix" type="checkbox"
|
||||
data-unique-account="{{ ygg_unique_account|lower }}"
|
||||
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600"
|
||||
onchange="updateProviderFields(true)">
|
||||
onchange="updateProviderFields()">
|
||||
</div>
|
||||
<div class="text-sm leading-6">
|
||||
<label for="yggflix" class="font-medium text-white">Enable YggTorrent API</label>
|
||||
|
|
@ -205,7 +370,7 @@
|
|||
<input id="sharewood" name="sharewood" type="checkbox"
|
||||
data-unique-account="{{ sharewood_unique_account|lower }}"
|
||||
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600"
|
||||
onchange="updateProviderFields(true)">
|
||||
onchange="updateProviderFields()">
|
||||
</div>
|
||||
<div class="text-sm leading-6">
|
||||
<label for="sharewood" class="font-medium text-white">Enable Sharewood API</label>
|
||||
|
|
@ -326,20 +491,11 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if not tb_unique_account %}
|
||||
<div id="tb_debrid-fields" style="display: none;">
|
||||
<div class="border-b border-gray-900/10 dark:border-white/50 pb-12">
|
||||
<div class="flex flex-col items-center">
|
||||
<h2 class="text-white font-semibold leading-7">Torbox Configuration</h2>
|
||||
</div>
|
||||
<div class="mt-10 grid grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
|
||||
<div class="sm:col-span-4">
|
||||
<label for="tb_token_info" class="block text-sm font-medium leading-6 text-white">Torbox
|
||||
Token</label>
|
||||
<input type="text" name="tb_token_info" id="tb_token_info"
|
||||
class="mt-2 block w-full rounded-md border-0 py-1.5 px-3 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6 space-y-6">
|
||||
<div class="relative flex gap-x-3">
|
||||
<div class="flex h-6 items-center">
|
||||
|
|
@ -367,7 +523,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if not pm_unique_account %}
|
||||
<div id="pm_debrid-fields" style="display: none;">
|
||||
|
|
@ -526,7 +681,7 @@
|
|||
<legend class="text-sm font-semibold leading-6 text-white">Sorting Options</legend>
|
||||
<div class="mt-6 space-y-6">
|
||||
<div class="flex items-center gap-x-3">
|
||||
<input id="quality" name="sorting" type="radio"
|
||||
<input id="quality" name="sorting" type="radio" checked
|
||||
class="h-4 w-4 border-gray-300 text-indigo-600 focus:ring-indigo-600">
|
||||
<label for="quality" class="block text-sm font-medium leading-6 text-white">Get the
|
||||
best
|
||||
|
|
@ -547,7 +702,7 @@
|
|||
size ascending</label>
|
||||
</div>
|
||||
<div class="flex items-center gap-x-3">
|
||||
<input id="qualitythensize" name="sorting" type="radio" checked
|
||||
<input id="qualitythensize" name="sorting" type="radio"
|
||||
class="h-4 w-4 border-gray-300 text-indigo-600 focus:ring-indigo-600">
|
||||
<label for="qualitythensize"
|
||||
class="block text-sm font-medium leading-6 text-white">Filter
|
||||
|
|
@ -600,7 +755,7 @@
|
|||
want to exclude</p>
|
||||
<div class="mt-6 space-y-6">
|
||||
<div class="flex items-center gap-x-3">
|
||||
<input id="2160p" name="2160p" type="checkbox" checked
|
||||
<input id="2160p" name="2160p" type="checkbox"
|
||||
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600">
|
||||
<label for="2160p" class="block text-sm font-medium leading-6 text-white">Exclude
|
||||
2160p
|
||||
|
|
@ -621,7 +776,7 @@
|
|||
resolution</label>
|
||||
</div>
|
||||
<div class="flex items-center gap-x-3">
|
||||
<input id="480p" name="480p" type="checkbox"
|
||||
<input id="480p" name="480p" type="checkbox" checked
|
||||
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600">
|
||||
<label for="480p" class="block text-sm font-medium leading-6 text-white">Exclude
|
||||
480p
|
||||
|
|
@ -701,6 +856,12 @@
|
|||
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50">
|
||||
<span class="ml-2 text-white">MULTi</span>
|
||||
</label>
|
||||
<label class="inline-flex items-center">
|
||||
<input id="vfq" type="checkbox" name="language" value="vfq"
|
||||
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50">
|
||||
<span class="ml-2 text-white">Français québécois (VFQ)</span>
|
||||
<span class="ml-2 text-xs text-gray-400">(Juste pour les québécois - VFQ en priorité)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -754,14 +915,13 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex items-center justify-center gap-x-6">
|
||||
<a id="install" onclick="return getLink('link')"
|
||||
class="cursor-pointer rounded-md bg-black px-3 py-2 text-sm font-semibold text-white dark:text-black dark:bg-white shadow-sm hover:bg--500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">Install</a>
|
||||
<a id="copy" onclick="return getLink('copy')"
|
||||
class="cursor-pointer rounded-md bg-black px-3 py-2 text-sm font-semibold text-white dark:text-black dark:bg-white shadow-sm hover:bg--500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">Copy</a>
|
||||
</div>
|
||||
<div class="mt-6 flex items-center justify-center gap-x-6">
|
||||
<a id="install" onclick="return getLink('link');"
|
||||
class="cursor-pointer rounded-md bg-black px-3 py-2 text-sm font-semibold text-white dark:text-black dark:bg-white shadow-sm hover:bg--500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">Install</a>
|
||||
<a id="copy" onclick="return getLink('copy');"
|
||||
class="cursor-pointer rounded-md bg-black px-3 py-2 text-sm font-semibold text-white dark:text-black dark:bg-white shadow-sm hover:bg--500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">Copy</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
14
stream_fusion/utils/cache/local_redis.py
vendored
14
stream_fusion/utils/cache/local_redis.py
vendored
|
|
@ -187,11 +187,21 @@ class RedisCache(CacheBase):
|
|||
await self.execute_with_retry(set_operation)
|
||||
|
||||
async def delete(self, key: str) -> bool:
|
||||
# Skip delete if Redis not available
|
||||
if not await self.can_cache():
|
||||
self.logger.debug(f"RedisCache.delete: cache indisponible, suppression ignorée pour {key}")
|
||||
return False
|
||||
|
||||
async def delete_operation():
|
||||
client = await self.get_redis_client()
|
||||
return bool(await client.delete(key))
|
||||
|
||||
return await self.execute_with_retry(delete_operation)
|
||||
try:
|
||||
return await self.execute_with_retry(delete_operation)
|
||||
except Exception as e:
|
||||
# Log at debug to avoid polluting logs when deletion fails
|
||||
self.logger.debug(f"RedisCache.delete: suppression impossible pour {key}: {e}")
|
||||
return False
|
||||
|
||||
async def exists(self, key: str) -> bool:
|
||||
async def exists_operation():
|
||||
|
|
@ -222,4 +232,4 @@ class RedisCache(CacheBase):
|
|||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
await self.close()
|
||||
await self.close()
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from urllib.parse import unquote
|
|||
from fastapi import HTTPException
|
||||
|
||||
from stream_fusion.utils.debrid.base_debrid import BaseDebrid
|
||||
from stream_fusion.utils.general import season_episode_in_filename
|
||||
from stream_fusion.utils.general import season_episode_in_filename, get_info_hash_from_magnet
|
||||
from stream_fusion.logging_config import logger
|
||||
from stream_fusion.settings import settings
|
||||
|
||||
|
|
@ -53,7 +53,12 @@ class AllDebrid(BaseDebrid):
|
|||
stream_type = query['type']
|
||||
torrent_download = unquote(query["torrent_download"]) if query["torrent_download"] is not None else None
|
||||
|
||||
torrent_id = self.add_magnet_or_torrent(magnet, torrent_download, ip)
|
||||
if torrent_download and (settings.yggflix_url and settings.yggflix_url in torrent_download):
|
||||
logger.info("AllDebrid: Using .torrent file for private tracker")
|
||||
torrent_id = self.add_magnet_or_torrent(magnet, torrent_download, ip)
|
||||
else:
|
||||
torrent_id = self.add_magnet_or_torrent(magnet, torrent_download, ip)
|
||||
|
||||
logger.info(f"AllDebrid: Torrent ID: {torrent_id}")
|
||||
|
||||
if not self.wait_for_ready_status(
|
||||
|
|
@ -81,10 +86,11 @@ class AllDebrid(BaseDebrid):
|
|||
matching_files.append(file)
|
||||
|
||||
if len(matching_files) == 0:
|
||||
logger.error(f"AllDebrid: No matching files for S{numeric_season:02d}E{numeric_episode:02d} in torrent.")
|
||||
raise HTTPException(status_code=404, detail=f"No matching files for S{numeric_season:02d}E{numeric_episode:02d} in torrent.")
|
||||
|
||||
link = max(matching_files, key=lambda x: x["size"])["link"]
|
||||
logger.warning(f"AllDebrid: No matching files found for S{numeric_season:02d}E{numeric_episode:02d}")
|
||||
return None
|
||||
else:
|
||||
# Correspondance trouvée via filename parsing
|
||||
link = max(matching_files, key=lambda x: x["size"])["link"]
|
||||
else:
|
||||
logger.error("AllDebrid: Unsupported stream type.")
|
||||
raise HTTPException(status_code=500, detail="Unsupported stream type.")
|
||||
|
|
@ -110,7 +116,8 @@ class AllDebrid(BaseDebrid):
|
|||
logger.info("AllDebrid: No hashes to be sent.")
|
||||
return {"status": "success", "data": {"magnets": []}}
|
||||
|
||||
result_magnets = []
|
||||
# Construire les hashes pour la requête
|
||||
hashes_to_check = []
|
||||
for hash_or_magnet in hashes_or_magnets:
|
||||
try:
|
||||
# Marquer tous les magnets comme disponibles immédiatement
|
||||
|
|
@ -121,7 +128,6 @@ class AllDebrid(BaseDebrid):
|
|||
})
|
||||
except Exception as e:
|
||||
logger.error(f"AllDebrid: Error processing hash {hash_or_magnet}: {str(e)}")
|
||||
# Même en cas d'erreur, on marque comme disponible
|
||||
result_magnets.append({
|
||||
"hash": hash_or_magnet,
|
||||
"instant": True,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
from collections import deque
|
||||
import json
|
||||
import time
|
||||
import asyncio
|
||||
|
||||
import aiohttp
|
||||
import requests
|
||||
|
||||
from stream_fusion.logging_config import logger
|
||||
|
|
@ -9,10 +11,11 @@ from stream_fusion.settings import settings
|
|||
|
||||
|
||||
class BaseDebrid:
|
||||
def __init__(self, config):
|
||||
def __init__(self, config, session: aiohttp.ClientSession = None):
|
||||
self.config = config
|
||||
self.logger = logger
|
||||
self.__session = self._create_session()
|
||||
# Use provided session or create a new one
|
||||
self.__session = session if session else self._create_session()
|
||||
|
||||
# Rate limiters
|
||||
self.global_limit = 250
|
||||
|
|
@ -24,13 +27,10 @@ class BaseDebrid:
|
|||
self.torrent_requests = deque()
|
||||
|
||||
def _create_session(self):
|
||||
session = requests.Session()
|
||||
session = aiohttp.ClientSession()
|
||||
if settings.proxy_url:
|
||||
self.logger.info(f"BaseDebrid: Using proxy: {settings.proxy_url}")
|
||||
session.proxies = {
|
||||
"http": str(settings.proxy_url),
|
||||
"https": str(settings.proxy_url),
|
||||
}
|
||||
session.connector = aiohttp.TCPConnector(proxy=str(settings.proxy_url))
|
||||
return session
|
||||
|
||||
def _rate_limit(self, requests_queue, limit, period):
|
||||
|
|
@ -52,58 +52,54 @@ class BaseDebrid:
|
|||
def _torrent_rate_limit(self):
|
||||
self._rate_limit(self.torrent_requests, self.torrent_limit, self.torrent_period)
|
||||
|
||||
def json_response(self, url, method="get", data=None, headers=None, files=None):
|
||||
self._global_rate_limit()
|
||||
async def json_response(self, url, method="get", data=None, headers=None, files=None):
|
||||
self._global_rate_limit() # Assuming these are quick checks
|
||||
if "torrents" in url:
|
||||
self._torrent_rate_limit()
|
||||
self._torrent_rate_limit() # Assuming these are quick checks
|
||||
|
||||
max_attempts = 5
|
||||
for attempt in range(max_attempts):
|
||||
try:
|
||||
if method == "get":
|
||||
response = self.__session.get(url, headers=headers)
|
||||
elif method == "post":
|
||||
response = self.__session.post(
|
||||
url, data=data, headers=headers, files=files
|
||||
)
|
||||
elif method == "put":
|
||||
response = self.__session.put(url, data=data, headers=headers)
|
||||
elif method == "delete":
|
||||
response = self.__session.delete(url, headers=headers)
|
||||
else:
|
||||
raise ValueError(f"BaseDebrid: Unsupported HTTP method: {method}")
|
||||
# Use aiohttp session for async requests
|
||||
async with self.__session.request(
|
||||
method, url, data=data, headers=headers # Files might need different handling with aiohttp
|
||||
# If 'files' are used, it typically involves FormData which needs setup
|
||||
# For simplicity, assuming 'files' is not commonly used or handled correctly elsewhere
|
||||
) as response:
|
||||
response.raise_for_status() # Check for HTTP errors (4xx, 5xx)
|
||||
|
||||
response.raise_for_status()
|
||||
|
||||
try:
|
||||
return response.json()
|
||||
except json.JSONDecodeError as json_err:
|
||||
self.logger.error(f"BaseDebrid: Invalid JSON response: {json_err}")
|
||||
self.logger.debug(
|
||||
f"BaseDebrid: Response content: {response.text[:200]}..."
|
||||
)
|
||||
if attempt < max_attempts - 1:
|
||||
wait_time = 2**attempt + 1
|
||||
self.logger.info(
|
||||
f"BaseDebrid: Retrying in {wait_time} seconds..."
|
||||
try:
|
||||
# Await the json parsing
|
||||
json_data = await response.json()
|
||||
return json_data
|
||||
except aiohttp.ContentTypeError as json_err: # Catch aiohttp's specific error
|
||||
self.logger.error(f"BaseDebrid: Invalid JSON response: {json_err}")
|
||||
resp_text = await response.text()
|
||||
self.logger.debug(
|
||||
f"BaseDebrid: Response content: {resp_text[:200]}..."
|
||||
)
|
||||
time.sleep(wait_time)
|
||||
else:
|
||||
return None
|
||||
if attempt < max_attempts - 1:
|
||||
wait_time = 2**attempt + 1
|
||||
self.logger.info(
|
||||
f"BaseDebrid: Retrying in {wait_time} seconds..."
|
||||
)
|
||||
await asyncio.sleep(wait_time) # Use asyncio.sleep
|
||||
else:
|
||||
return None
|
||||
|
||||
except requests.exceptions.HTTPError as e:
|
||||
status_code = e.response.status_code
|
||||
except aiohttp.ClientResponseError as e: # Catch aiohttp HTTP errors
|
||||
status_code = e.status
|
||||
if status_code == 429:
|
||||
wait_time = 2**attempt + 1
|
||||
self.logger.warning(
|
||||
f"BaseDebrid: Rate limit exceeded. Attempt {attempt + 1}/{max_attempts}. Waiting for {wait_time} seconds."
|
||||
)
|
||||
time.sleep(wait_time)
|
||||
await asyncio.sleep(wait_time) # Use asyncio.sleep
|
||||
elif 400 <= status_code < 500:
|
||||
self.logger.error(
|
||||
f"BaseDebrid: Client error occurred: {e}. Status code: {status_code}"
|
||||
)
|
||||
return None
|
||||
return None # Stop retrying on client errors (except 429)
|
||||
elif 500 <= status_code < 600:
|
||||
self.logger.error(
|
||||
f"BaseDebrid: Server error occurred: {e}. Status code: {status_code}"
|
||||
|
|
@ -113,32 +109,35 @@ class BaseDebrid:
|
|||
self.logger.info(
|
||||
f"BaseDebrid: Retrying in {wait_time} seconds..."
|
||||
)
|
||||
time.sleep(wait_time)
|
||||
await asyncio.sleep(wait_time) # Use asyncio.sleep
|
||||
else:
|
||||
return None
|
||||
return None # Stop after max attempts for server errors
|
||||
else:
|
||||
self.logger.error(
|
||||
f"BaseDebrid: Unexpected HTTP error occurred: {e}. Status code: {status_code}"
|
||||
)
|
||||
return None
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
except aiohttp.ClientConnectionError as e: # Catch connection errors
|
||||
self.logger.error(f"BaseDebrid: Connection error occurred: {e}")
|
||||
if attempt < max_attempts - 1:
|
||||
wait_time = 2**attempt + 1
|
||||
self.logger.info(f"BaseDebrid: Retrying in {wait_time} seconds...")
|
||||
time.sleep(wait_time)
|
||||
await asyncio.sleep(wait_time) # Use asyncio.sleep
|
||||
else:
|
||||
return None
|
||||
except requests.exceptions.Timeout as e:
|
||||
except asyncio.TimeoutError as e: # Catch timeouts (if configured in session)
|
||||
self.logger.error(f"BaseDebrid: Request timed out: {e}")
|
||||
if attempt < max_attempts - 1:
|
||||
wait_time = 2**attempt + 1
|
||||
self.logger.info(f"BaseDebrid: Retrying in {wait_time} seconds...")
|
||||
time.sleep(wait_time)
|
||||
await asyncio.sleep(wait_time) # Use asyncio.sleep
|
||||
else:
|
||||
return None
|
||||
except requests.exceptions.RequestException as e:
|
||||
self.logger.error(f"BaseDebrid: An unexpected error occurred: {e}")
|
||||
except aiohttp.ClientError as e: # Catch other aiohttp client errors
|
||||
self.logger.error(f"BaseDebrid: An unexpected aiohttp error occurred: {e}")
|
||||
return None
|
||||
except Exception as e: # Catch any other unexpected error
|
||||
self.logger.error(f"BaseDebrid: An unexpected general error occurred: {e}", exc_info=True)
|
||||
return None
|
||||
|
||||
self.logger.error(
|
||||
|
|
@ -146,21 +145,35 @@ class BaseDebrid:
|
|||
)
|
||||
return None
|
||||
|
||||
def wait_for_ready_status(self, check_status_func, timeout=30, interval=5):
|
||||
async def wait_for_ready_status(self, check_status_func, timeout=30, interval=5):
|
||||
"""Waits for a torrent to be ready by periodically calling check_status_func."""
|
||||
self.logger.info(f"BaseDebrid: Waiting for {timeout} seconds for caching.")
|
||||
start_time = time.time()
|
||||
while time.time() - start_time < timeout:
|
||||
if check_status_func():
|
||||
# Assume check_status_func might become async if it involves IO
|
||||
status_ready = await check_status_func()
|
||||
if status_ready:
|
||||
self.logger.info("BaseDebrid: File is ready!")
|
||||
return True
|
||||
time.sleep(interval)
|
||||
await asyncio.sleep(interval)
|
||||
self.logger.info(f"BaseDebrid: Waiting timed out.")
|
||||
return False
|
||||
|
||||
def download_torrent_file(self, download_url):
|
||||
response = requests.get(download_url)
|
||||
response.raise_for_status()
|
||||
return response.content
|
||||
async def download_torrent_file(self, download_url):
|
||||
"""Downloads a torrent file from a URL asynchronously."""
|
||||
try:
|
||||
async with self.__session.get(download_url) as response:
|
||||
response.raise_for_status() # Check for HTTP errors
|
||||
# Read the content asynchronously
|
||||
content = await response.read()
|
||||
return content
|
||||
except aiohttp.ClientError as e:
|
||||
self.logger.error(f"BaseDebrid: Failed to download torrent file from {download_url}: {e}")
|
||||
# Optionally re-raise or return None/empty bytes based on desired error handling
|
||||
return None
|
||||
except Exception as e:
|
||||
self.logger.error(f"BaseDebrid: Unexpected error downloading torrent file: {e}", exc_info=True)
|
||||
return None
|
||||
|
||||
def get_stream_link(self, query, ip=None):
|
||||
raise NotImplementedError
|
||||
|
|
|
|||
|
|
@ -4,81 +4,158 @@ from stream_fusion.utils.debrid.alldebrid import AllDebrid
|
|||
from stream_fusion.utils.debrid.realdebrid import RealDebrid
|
||||
from stream_fusion.utils.debrid.torbox import Torbox
|
||||
from stream_fusion.utils.debrid.premiumize import Premiumize
|
||||
from stream_fusion.utils.debrid.stremthrudebrid import StremThruDebrid
|
||||
from stream_fusion.logging_config import logger
|
||||
from stream_fusion.settings import settings
|
||||
from fastapi import Request
|
||||
import aiohttp
|
||||
|
||||
def normalize_service_name(service_name):
|
||||
return service_name.lower().replace('-', '')
|
||||
|
||||
def get_all_debrid_services(config):
|
||||
services = config['service']
|
||||
debrid_service = []
|
||||
if not services:
|
||||
services_config = config.get('service', [])
|
||||
debrid_services = []
|
||||
stremthru_enabled = config.get('stremthru_enabled', False)
|
||||
|
||||
if not services_config:
|
||||
logger.error("No service configuration found in the config file.")
|
||||
return []
|
||||
for service in services:
|
||||
if service == "Real-Debrid":
|
||||
debrid_service.append(RealDebrid(config))
|
||||
logger.debug("Real-Debrid: service added to be use")
|
||||
if service == "AllDebrid":
|
||||
debrid_service.append(AllDebrid(config))
|
||||
logger.debug("AllDebrid: service added to be use")
|
||||
if service == "TorBox":
|
||||
debrid_service.append(Torbox(config))
|
||||
logger.debug("TorBox: service added to be use")
|
||||
if service == "Premiumize":
|
||||
debrid_service.append(Premiumize(config))
|
||||
logger.debug("Premiumize: service added to be use")
|
||||
if not debrid_service:
|
||||
raise HTTPException(status_code=500, detail="Invalid service configuration.")
|
||||
|
||||
return debrid_service
|
||||
|
||||
if stremthru_enabled:
|
||||
logger.info("StremThru est activé (config requête). Tentative d'instanciation du client StremThru.")
|
||||
if config.get('stremthru_url'):
|
||||
try:
|
||||
debrid_services.append(StremThruDebrid(config))
|
||||
logger.info("Client StremThruDebrid instancié avec succès.")
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de l'instanciation de StremThruDebrid: {e}")
|
||||
else:
|
||||
logger.error("StremThru est activé mais stremthru_url manque dans la config.")
|
||||
|
||||
# Instantiate direct Debrid services based on config['service']
|
||||
logger.debug("Instantiating direct Debrid services based on config['service']")
|
||||
service_map = {
|
||||
"RD": (RealDebrid, config.get('RDToken')),
|
||||
"Real-Debrid": (RealDebrid, config.get('RDToken')),
|
||||
"AD": (AllDebrid, config.get('ADToken')),
|
||||
"AllDebrid": (AllDebrid, config.get('ADToken')),
|
||||
"PM": (Premiumize, config.get('PMToken')),
|
||||
"Premiumize": (Premiumize, config.get('PMToken')),
|
||||
"TB": (Torbox, config.get('TBToken')),
|
||||
"TorBox": (Torbox, config.get('TBToken'))
|
||||
}
|
||||
direct_services_to_instantiate = [s for s in services_config if s in service_map]
|
||||
if not direct_services_to_instantiate and not debrid_services:
|
||||
logger.error("No valid direct Debrid services found and StremThru not used.")
|
||||
return []
|
||||
for service_name in direct_services_to_instantiate:
|
||||
service_class, token = service_map[service_name]
|
||||
if token:
|
||||
try:
|
||||
debrid_services.append(service_class(config))
|
||||
logger.info(f"Service {service_name} instantiated successfully.")
|
||||
except Exception as e:
|
||||
logger.error(f"Error instantiating {service_name}: {e}")
|
||||
else:
|
||||
logger.warning(f"Missing token for {service_name}; service skipped.")
|
||||
|
||||
if not debrid_services:
|
||||
logger.error("Finalement, aucun service Debrid valide n'a pu être instancié.")
|
||||
return []
|
||||
|
||||
unique_services = []
|
||||
seen_types = set()
|
||||
for service in debrid_services:
|
||||
if type(service) not in seen_types:
|
||||
unique_services.append(service)
|
||||
seen_types.add(type(service))
|
||||
|
||||
logger.info(f"Services Debrid instanciés : {[type(s).__name__ for s in unique_services]}")
|
||||
return unique_services
|
||||
|
||||
def get_download_service(config):
|
||||
target_service_name = None
|
||||
if not settings.download_service:
|
||||
service = config.get('debridDownloader')
|
||||
if not service:
|
||||
# Si aucun service n'est spécifié, utiliser le service activé
|
||||
services = config.get('service', [])
|
||||
if len(services) == 1:
|
||||
service = services[0]
|
||||
logger.info(f"Using active service as download service: {service}")
|
||||
service_config_name = config.get('debridDownloader')
|
||||
if not service_config_name:
|
||||
enabled_services = config.get('service', [])
|
||||
if len(enabled_services) == 1:
|
||||
target_service_name = enabled_services[0]
|
||||
logger.info(f"Utilisation du seul service actif comme service de téléchargement: {target_service_name}")
|
||||
else:
|
||||
logger.error("Multiple services enabled. Please select a download service in the web interface.")
|
||||
logger.error("Plusieurs services activés. Veuillez sélectionner un service de téléchargement.")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="Multiple services enabled. Please select a download service in the web interface."
|
||||
)
|
||||
else:
|
||||
target_service_name = service_config_name
|
||||
else:
|
||||
service = settings.download_service
|
||||
|
||||
if service == "Real-Debrid":
|
||||
target_service_name = settings.download_service
|
||||
|
||||
if not target_service_name:
|
||||
logger.error("Impossible de déterminer le service de téléchargement cible.")
|
||||
raise HTTPException(status_code=500, detail="Impossible de déterminer le service de téléchargement.")
|
||||
|
||||
logger.debug(f"Service de téléchargement cible déterminé: {target_service_name}")
|
||||
|
||||
# Use StremThru if enabled, otherwise fall back to direct service
|
||||
if config.get("stremthru_enabled", False):
|
||||
logger.info(f"StremThru activé. Utilisation de StremThruDebrid pour '{target_service_name}'.")
|
||||
return StremThruDebrid(config)
|
||||
|
||||
logger.info(f"StremThru désactivé. Utilisation du client direct pour '{target_service_name}'.")
|
||||
if target_service_name == "Real-Debrid":
|
||||
return RealDebrid(config)
|
||||
elif service == "AllDebrid":
|
||||
elif target_service_name == "AllDebrid":
|
||||
return AllDebrid(config)
|
||||
elif service == "TorBox":
|
||||
elif target_service_name == "TorBox":
|
||||
return Torbox(config)
|
||||
elif service == "Premiumize":
|
||||
elif target_service_name == "Premiumize":
|
||||
return Premiumize(config)
|
||||
else:
|
||||
logger.error(f"Invalid download service: {service}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Invalid download service: {service}. Please select a valid download service in the web interface."
|
||||
)
|
||||
logger.error(f"Service de téléchargement invalide spécifié: {target_service_name}")
|
||||
raise HTTPException(status_code=500, detail=f"Invalid download service: {target_service_name}.")
|
||||
|
||||
SERVICE_MAP = {
|
||||
"RD": "Real-Debrid",
|
||||
"AD": "AllDebrid",
|
||||
"TB": "TorBox",
|
||||
"PM": "Premiumize",
|
||||
"DL": "DOWNLOAD_SERVICE"
|
||||
}
|
||||
|
||||
def get_debrid_service(config, service):
|
||||
if not service:
|
||||
service = settings.download_service
|
||||
if service == "RD":
|
||||
return RealDebrid(config)
|
||||
elif service == "AD":
|
||||
return AllDebrid(config)
|
||||
elif service == "TB":
|
||||
return Torbox(config)
|
||||
elif service == "PM":
|
||||
return Premiumize(config)
|
||||
elif service == "DL":
|
||||
return get_download_service(config)
|
||||
def get_debrid_service(config, service_short_code, request: Request):
|
||||
target_service_name = None
|
||||
http_session = request.app.state.http_session
|
||||
|
||||
if service_short_code == "DL":
|
||||
logger.debug("get_debrid_service: Délégation à get_download_service pour 'DL'.")
|
||||
return get_download_service(config)
|
||||
elif service_short_code in SERVICE_MAP:
|
||||
target_service_name = SERVICE_MAP[service_short_code]
|
||||
logger.debug(f"get_debrid_service: Service demandé '{service_short_code}' mappé à '{target_service_name}'.")
|
||||
else:
|
||||
logger.error("Invalid service configuration return by stremio in the query.")
|
||||
raise HTTPException(status_code=500, detail="Invalid service configuration return by stremio.")
|
||||
logger.warning(f"get_debrid_service: Code de service inconnu '{service_short_code}'.")
|
||||
raise HTTPException(status_code=400, detail=f"Unknown service code: {service_short_code}")
|
||||
|
||||
# Use StremThru proxy for all services if enabled
|
||||
if config.get('stremthru_enabled', False):
|
||||
logger.info(f"StremThru activé. Délégation de '{target_service_name}' au client StremThru.")
|
||||
return StremThruDebrid(config, session=http_session)
|
||||
|
||||
# Fallback: direct service instantiation
|
||||
service_map = {
|
||||
"real-debrid": RealDebrid,
|
||||
"alldebrid": AllDebrid,
|
||||
"torbox": Torbox,
|
||||
"premiumize": Premiumize
|
||||
}
|
||||
normalized = normalize_service_name(target_service_name)
|
||||
if normalized in service_map:
|
||||
cls = service_map[normalized]
|
||||
return cls(config, session=http_session)
|
||||
else:
|
||||
logger.error(f"Service Debrid invalide spécifié: {target_service_name}")
|
||||
raise HTTPException(status_code=400, detail=f"Unsupported debrid service: {target_service_name}")
|
||||
|
|
|
|||
337
stream_fusion/utils/debrid/stremtrudebrid.py
Normal file
337
stream_fusion/utils/debrid/stremtrudebrid.py
Normal file
|
|
@ -0,0 +1,337 @@
|
|||
import re
|
||||
import time
|
||||
import asyncio
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
from urllib.parse import urljoin, quote_plus
|
||||
import aiohttp
|
||||
import logging
|
||||
import json
|
||||
from fastapi import HTTPException
|
||||
|
||||
from stream_fusion.utils.debrid.base_debrid import BaseDebrid
|
||||
from stream_fusion.settings import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class StremThruDebrid(BaseDebrid):
|
||||
"""Debrid proxy via StremThru for multiple stores."""
|
||||
STORE_CODE_TO_NAME = {
|
||||
'rd': 'realdebrid',
|
||||
'ad': 'alldebrid',
|
||||
'pz': 'premiumize',
|
||||
'tb': 'torbox',
|
||||
'dl': 'debridlink',
|
||||
'en': 'easydebrid',
|
||||
'oc': 'offcloud',
|
||||
'pp': 'pikpak',
|
||||
}
|
||||
STORE_NAME_TO_TOKEN_KEY = {
|
||||
'realdebrid': 'RDToken',
|
||||
'alldebrid': 'ADToken',
|
||||
'premiumize': 'PMToken',
|
||||
'torbox': 'TBToken',
|
||||
'debridlink': 'DLToken',
|
||||
'easydebrid': 'EDToken',
|
||||
'offcloud': 'OCToken',
|
||||
'pikpak': 'PKToken',
|
||||
}
|
||||
|
||||
def __init__(self, config: Dict[str, Any], session: aiohttp.ClientSession = None):
|
||||
super().__init__(config, session=session)
|
||||
# Determine base URL: try settings first, then config
|
||||
base = getattr(settings, 'stremthru_base_url', None) or config.get('stremthru_url')
|
||||
if not base:
|
||||
logger.warning("StremThruDebrid: URL not specified; proxy calls may fail.")
|
||||
self.base_url = ''
|
||||
else:
|
||||
self.base_url = base.rstrip('/')
|
||||
logger.info(f"StremThruDebrid initialized with base URL {self.base_url or '<none>'}")
|
||||
|
||||
async def _request(
|
||||
self,
|
||||
method: str,
|
||||
path: str,
|
||||
store_name: str,
|
||||
params: Optional[Dict[str, Any]] = None,
|
||||
json: Optional[Dict[str, Any]] = None,
|
||||
timeout: int = 20,
|
||||
max_retries: int = 3,
|
||||
) -> Any:
|
||||
"""Central async HTTP request to StremThru API."""
|
||||
token_key = self.STORE_NAME_TO_TOKEN_KEY.get(store_name)
|
||||
if not token_key:
|
||||
logger.error(f"_request: unknown store_name '{store_name}'")
|
||||
raise HTTPException(status_code=400, detail=f"Unknown store: {store_name}")
|
||||
token = self.config.get(token_key)
|
||||
url = urljoin(self.base_url + '/', path.lstrip('/'))
|
||||
headers = {"Accept": "application/json"}
|
||||
if token:
|
||||
headers.update({
|
||||
"X-StremThru-Store-Name": store_name,
|
||||
"X-StremThru-Store-Authorization": f"Bearer {token}"
|
||||
})
|
||||
if json:
|
||||
headers["Content-Type"] = "application/json"
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
async with aiohttp.ClientSession(headers=headers) as session:
|
||||
resp = await session.request(method, url, params=params, json=json, timeout=timeout)
|
||||
text = await resp.text()
|
||||
# Return empty dict for missing torrent (404) on GET magnet info
|
||||
if resp.status == 404 and method.upper() == 'GET' and '/magnets/' in path:
|
||||
return {}
|
||||
# Handle error status
|
||||
if resp.status >= 400:
|
||||
# Try parse error payload
|
||||
try:
|
||||
err = await resp.json(content_type=None)
|
||||
code = err.get('error', {}).get('code', resp.status)
|
||||
msg = err.get('error', {}).get('message', text)
|
||||
except:
|
||||
code = resp.status; msg = text
|
||||
raise aiohttp.ClientResponseError(
|
||||
status=resp.status, request_info=resp.request_info,
|
||||
history=(), message=msg
|
||||
)
|
||||
# Return raw text for unrestrict links
|
||||
if 'unrestrict' in path:
|
||||
return text
|
||||
# Parse JSON response
|
||||
try:
|
||||
return await resp.json(content_type=None)
|
||||
except Exception:
|
||||
# Fallback: extract JSON substring
|
||||
try:
|
||||
idx = text.find('{')
|
||||
return json.loads(text[idx:]) if idx >= 0 else {}
|
||||
except Exception as e2:
|
||||
logger.warning(f"_request: JSON fallback failed: {e2}; response text: {text[:100]}")
|
||||
return {}
|
||||
except Exception as e:
|
||||
logger.warning(f"Attempt {attempt+1}/{max_retries} failed: {e}")
|
||||
await asyncio.sleep(1)
|
||||
raise RuntimeError(f"Failed request {method} {url}")
|
||||
|
||||
def extract_hash_from_magnet(self, magnet: str) -> Optional[str]:
|
||||
m = re.search(r'btih:([A-Fa-f0-9]{40})', magnet)
|
||||
return m.group(1).lower() if m else None
|
||||
|
||||
async def is_already_added(self, magnet: str, store_name: str, ip: Optional[str] = None) -> Optional[str]:
|
||||
h = self.extract_hash_from_magnet(magnet)
|
||||
if not h:
|
||||
return None
|
||||
# Liste les magnets existants et cherche l'ID associé au hash
|
||||
try:
|
||||
res = await self._request('GET', '/v0/store/magnets', store_name)
|
||||
items = res.get('data', {}).get('items', [])
|
||||
for item in items:
|
||||
if item.get('hash') == h:
|
||||
return item.get('id')
|
||||
return None
|
||||
except aiohttp.ClientResponseError as e:
|
||||
# 404 ou 400 = non ajouté
|
||||
if e.status in (404, 400):
|
||||
return None
|
||||
raise
|
||||
|
||||
async def add_magnet(self, magnet: str, store_name: str, ip: Optional[str] = None) -> str:
|
||||
h = self.extract_hash_from_magnet(magnet)
|
||||
if not h: raise ValueError("Invalid magnet")
|
||||
data = {'magnet': magnet}
|
||||
# Ajoute le magnet via /v0/store/magnets
|
||||
res = await self._request('POST', '/v0/store/magnets', store_name, json=data)
|
||||
logger.debug(f"StremThruDebrid.add_magnet raw response: {res}")
|
||||
# Extrait l'ID retourné (soit en top-level, soit sous 'data')
|
||||
id_ = None
|
||||
if isinstance(res, dict):
|
||||
id_ = res.get('id') or res.get('data', {}).get('id')
|
||||
return id_ or h
|
||||
|
||||
async def get_torrent_info(self, torrent_id: str, store_name: str, ip: Optional[str] = None) -> Optional[Dict[str, Any]]:
|
||||
try:
|
||||
# Récupère le statut via /v0/store/magnets/{id}
|
||||
res = await self._request('GET', f'/v0/store/magnets/{torrent_id}', store_name)
|
||||
# Déroule le champ 'data'
|
||||
data = res.get('data') if isinstance(res, dict) else None
|
||||
return data if data and data.get('hash') else None
|
||||
except aiohttp.ClientResponseError as e:
|
||||
if e.status == 404:
|
||||
return None
|
||||
raise
|
||||
|
||||
async def delete_torrent(self, torrent_id: str, store_name: str, ip: Optional[str] = None) -> bool:
|
||||
# Supprime le magnet par l'id StremThru
|
||||
await self._request('DELETE', f'/v0/store/magnets/{torrent_id}', store_name)
|
||||
return True
|
||||
|
||||
async def unrestrict_link(self, link: str, store_name: str, ip: Optional[str] = None) -> Optional[str]:
|
||||
# Génère un lien direct via /v0/store/link/generate
|
||||
res = await self._request('POST', '/v0/store/link/generate', store_name, json={'link': link})
|
||||
# La réponse est dans data.link
|
||||
return res.get('data', {}).get('link')
|
||||
|
||||
async def wait_for_link(self, torrent_id: str, store_name: str, timeout: int = 60, interval: int = 1) -> bool:
|
||||
start = time.time()
|
||||
while time.time()-start < timeout:
|
||||
info = await self.get_torrent_info(torrent_id, store_name)
|
||||
if info and info.get('status') in ['cached','ready','downloaded','completed']:
|
||||
return True
|
||||
await asyncio.sleep(interval)
|
||||
return False
|
||||
|
||||
def _find_appropriate_link(self, files: List[Dict[str, Any]], file_index: Union[str,int], season: Optional[int], episode: Optional[int]) -> Tuple[Optional[str], Optional[int]]:
|
||||
# Gère file_index None ou invalide
|
||||
try:
|
||||
idx = int(file_index) if file_index is not None else -1
|
||||
except (TypeError, ValueError):
|
||||
idx = -1
|
||||
# Si pas de file_index valide, choisir le fichier avec lien et taille maximale
|
||||
if idx < 0:
|
||||
valid_files = [f for f in files if f.get('link')]
|
||||
if valid_files:
|
||||
largest = max(valid_files, key=lambda x: x.get('size', 0))
|
||||
return largest.get('link'), largest.get('index')
|
||||
for f in files:
|
||||
if f.get('index') == idx:
|
||||
return f.get('link'), idx
|
||||
return None, None
|
||||
|
||||
async def get_stream_link(self, query: Dict[str, Any], config: Dict[str, Any], ip: Optional[str] = None) -> str:
|
||||
magnet = query['magnet']
|
||||
# Si aucun file_index fourni, on prend -1 (pour la vignettes ou premier lien)
|
||||
raw_idx = query.get('file_index')
|
||||
idx = raw_idx if raw_idx is not None else -1
|
||||
code_raw = query.get('store_code')
|
||||
if not code_raw:
|
||||
# Infer store_code from config debridDownloader
|
||||
default = config.get('debridDownloader')
|
||||
inv = {v: k for k, v in self.STORE_CODE_TO_NAME.items()}
|
||||
if default:
|
||||
code_raw = inv.get(default.lower())
|
||||
if code_raw:
|
||||
logger.debug(f"get_stream_link: inferred store_code '{code_raw}' from config.debridDownloader")
|
||||
else:
|
||||
logger.error("get_stream_link: cannot infer store_code from config.debridDownloader")
|
||||
raise HTTPException(status_code=400, detail="Missing or invalid store_code")
|
||||
else:
|
||||
logger.error("get_stream_link: missing store_code and no debridDownloader in config")
|
||||
raise HTTPException(status_code=400, detail="Missing store_code")
|
||||
code = str(code_raw).lower()
|
||||
name = self.STORE_CODE_TO_NAME.get(code)
|
||||
if not name:
|
||||
logger.error(f"get_stream_link: invalid store_code '{code_raw}'")
|
||||
raise HTTPException(status_code=400, detail=f"Invalid store code: {code_raw}")
|
||||
# Check if magnet already added
|
||||
id_ = await self.is_already_added(magnet, name, ip)
|
||||
info: Optional[Dict[str, Any]] = None
|
||||
if id_:
|
||||
h = id_
|
||||
# Get initial torrent info
|
||||
info = await self.get_torrent_info(h, name, ip)
|
||||
else:
|
||||
# Add magnet and capture raw response to skip extra GET
|
||||
res = await self._request('POST', '/v0/store/magnets', name, json={'magnet': magnet})
|
||||
logger.debug(f"StremThruDebrid.add_magnet raw response: {res}")
|
||||
# Extract ID
|
||||
h = res.get('id') or res.get('data', {}).get('id') or self.extract_hash_from_magnet(magnet)
|
||||
# Use returned data if ready
|
||||
info = res.get('data') if isinstance(res, dict) else None
|
||||
# Wait for readiness if not already ready
|
||||
if not info or info.get('status') not in ['cached','ready','downloaded','completed']:
|
||||
if not await self.wait_for_link(h, name):
|
||||
raise RuntimeError("Timeout waiting for torrent")
|
||||
info = await self.get_torrent_info(h, name, ip)
|
||||
link, fid = self._find_appropriate_link(info.get('files',[]), idx, query.get('season'), query.get('episode'))
|
||||
if fid is None:
|
||||
raise RuntimeError("File not found")
|
||||
return await self.unrestrict_link(link, name, ip)
|
||||
|
||||
def _find_appropriate_link(self, files: List[Dict[str, Any]], file_index: Union[str,int], season: Optional[int], episode: Optional[int]) -> Tuple[Optional[str], Optional[int]]:
|
||||
# Gère file_index None ou invalide
|
||||
try:
|
||||
idx = int(file_index) if file_index is not None else -1
|
||||
except (TypeError, ValueError):
|
||||
idx = -1
|
||||
# Si pas de file_index valide, choisir le fichier avec lien et taille maximale
|
||||
if idx < 0:
|
||||
valid_files = [f for f in files if f.get('link')]
|
||||
if valid_files:
|
||||
largest = max(valid_files, key=lambda x: x.get('size', 0))
|
||||
return largest.get('link'), largest.get('index')
|
||||
for f in files:
|
||||
if f.get('index') == idx:
|
||||
return f.get('link'), idx
|
||||
return None, None
|
||||
|
||||
async def get_stream_link(self, query: Dict[str, Any], config: Dict[str, Any], ip: Optional[str] = None) -> str:
|
||||
magnet = query['magnet']
|
||||
# Si aucun file_index fourni, on prend -1 (pour la vignettes ou premier lien)
|
||||
raw_idx = query.get('file_index')
|
||||
idx = raw_idx if raw_idx is not None else -1
|
||||
code_raw = query.get('store_code')
|
||||
if not code_raw:
|
||||
# Infer store_code from config debridDownloader
|
||||
default = config.get('debridDownloader')
|
||||
inv = {v: k for k, v in self.STORE_CODE_TO_NAME.items()}
|
||||
if default:
|
||||
code_raw = inv.get(default.lower())
|
||||
if code_raw:
|
||||
logger.debug(f"get_stream_link: inferred store_code '{code_raw}' from config.debridDownloader")
|
||||
else:
|
||||
logger.error("get_stream_link: cannot infer store_code from config.debridDownloader")
|
||||
raise HTTPException(status_code=400, detail="Missing or invalid store_code")
|
||||
else:
|
||||
logger.error("get_stream_link: missing store_code and no debridDownloader in config")
|
||||
raise HTTPException(status_code=400, detail="Missing store_code")
|
||||
code = str(code_raw).lower()
|
||||
name = self.STORE_CODE_TO_NAME.get(code)
|
||||
if not name:
|
||||
logger.error(f"get_stream_link: invalid store_code '{code_raw}'")
|
||||
raise HTTPException(status_code=400, detail=f"Invalid store code: {code_raw}")
|
||||
# Check if magnet already added
|
||||
id_ = await self.is_already_added(magnet, name, ip)
|
||||
info: Optional[Dict[str, Any]] = None
|
||||
if id_:
|
||||
h = id_
|
||||
# Get initial torrent info
|
||||
info = await self.get_torrent_info(h, name, ip)
|
||||
else:
|
||||
# Add magnet and capture raw response to skip extra GET
|
||||
res = await self._request('POST', '/v0/store/magnets', name, json={'magnet': magnet})
|
||||
logger.debug(f"StremThruDebrid.add_magnet raw response: {res}")
|
||||
# Extract ID
|
||||
h = res.get('id') or res.get('data', {}).get('id') or self.extract_hash_from_magnet(magnet)
|
||||
# Use returned data if ready
|
||||
info = res.get('data') if isinstance(res, dict) else None
|
||||
# Wait for readiness if not already ready
|
||||
if not info or info.get('status') not in ['cached','ready','downloaded','completed']:
|
||||
if not await self.wait_for_link(h, name):
|
||||
raise RuntimeError("Timeout waiting for torrent")
|
||||
info = await self.get_torrent_info(h, name, ip)
|
||||
link, fid = self._find_appropriate_link(info.get('files',[]), idx, query.get('season'), query.get('episode'))
|
||||
if fid is None:
|
||||
raise RuntimeError("File not found")
|
||||
return await self.unrestrict_link(link, name, ip)
|
||||
|
||||
async def get_cached_files_async(self, torrent_items: List[Any], ip: Optional[str] = None, sid: Optional[str] = None) -> Tuple[Dict[str,List[Dict]], str]:
|
||||
return await self.get_cached_files(torrent_items, ip, sid)
|
||||
|
||||
async def get_cached_files(self, torrent_items: List[Any], ip: Optional[str] = None, sid: Optional[str] = None) -> Tuple[Dict[str,List[Dict]], str]:
|
||||
hashes = [i.info_hash for i in torrent_items if getattr(i,'info_hash',None)]
|
||||
if not hashes: return {}, ''
|
||||
magnets = [f"magnet:?xt=urn:btih:{h}" for h in hashes]
|
||||
params={'magnet':','.join(magnets)}
|
||||
if sid:
|
||||
params['sid'] = sid
|
||||
# Select a store with available token, fallback to first
|
||||
store_name = next((sn for sn, tk in self.STORE_NAME_TO_TOKEN_KEY.items() if self.config.get(tk)), list(self.STORE_NAME_TO_TOKEN_KEY.keys())[0])
|
||||
# Use global check endpoint; store_name passed via header
|
||||
res = await self._request('GET', '/v0/store/magnets/check', store_name, params=params)
|
||||
items = res.get('data',{}).get('items',[])
|
||||
out={h:[] for h in hashes}
|
||||
for it in items:
|
||||
h = it.get('hash'); fs = it.get('files', [])
|
||||
for f in fs:
|
||||
# Use 'title' key to represent file name for consistency
|
||||
out[h].append({'file_index': f.get('index'), 'title': f.get('name'), 'size': f.get('size')})
|
||||
return out, store_name
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
from itertools import islice
|
||||
import uuid
|
||||
import tenacity
|
||||
import re
|
||||
from urllib.parse import unquote
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
|
@ -105,11 +106,7 @@ class Torbox(BaseDebrid):
|
|||
|
||||
if existing_torrent:
|
||||
logger.info(f"Torbox: Found existing torrent with ID: {existing_torrent['id']}")
|
||||
torrent_info = existing_torrent
|
||||
if not torrent_info or "id" not in torrent_info:
|
||||
logger.error("Torbox: Failed to add or find torrent.")
|
||||
return None
|
||||
torrent_id = torrent_info["id"]
|
||||
torrent_id = existing_torrent["id"]
|
||||
else:
|
||||
# Add the magnet or torrent file
|
||||
torrent_info = self.add_magnet_or_torrent(magnet, torrent_download)
|
||||
|
|
@ -125,8 +122,14 @@ class Torbox(BaseDebrid):
|
|||
logger.warning("Torbox: Torrent not ready, caching in progress.")
|
||||
return settings.no_cache_video_url
|
||||
|
||||
# Always get fresh torrent info with complete file list
|
||||
fresh_torrent_info = self.get_torrent_info(torrent_id)
|
||||
if not fresh_torrent_info or "data" not in fresh_torrent_info:
|
||||
logger.error("Torbox: Failed to get torrent info.")
|
||||
return settings.no_cache_video_url
|
||||
|
||||
# Select the appropriate file
|
||||
file_id = self._select_file(torrent_info, stream_type, file_index, season, episode)
|
||||
file_id = self._select_file(fresh_torrent_info["data"], stream_type, file_index, season, episode)
|
||||
|
||||
if file_id == None:
|
||||
logger.error("Torbox: No matching file found.")
|
||||
|
|
@ -225,21 +228,64 @@ class Torbox(BaseDebrid):
|
|||
return largest_file["id"]
|
||||
|
||||
elif stream_type == "series":
|
||||
# Always use intelligent selection for series (ignore file_index)
|
||||
if file_index is not None:
|
||||
logger.info(f"Torbox: Selected file index {file_index} for series")
|
||||
return file_index
|
||||
logger.debug(f"Torbox: Ignoring file_index {file_index} for series, using intelligent selection")
|
||||
|
||||
try:
|
||||
numeric_season = int(season.replace("S", ""))
|
||||
numeric_episode = int(episode.replace("E", ""))
|
||||
except (ValueError, TypeError):
|
||||
logger.error(f"Torbox: Invalid season/episode format: {season}/{episode}")
|
||||
return None
|
||||
|
||||
matching_files = [
|
||||
file for file in files
|
||||
if season_episode_in_filename(file["short_name"], season, episode) and is_video_file(file["short_name"])
|
||||
matching_files = []
|
||||
logger.debug(f"Torbox: Searching for S{numeric_season:02d}E{numeric_episode:02d} among {len(files)} files")
|
||||
|
||||
# Pre-compile patterns for better performance
|
||||
episode_patterns = [
|
||||
rf"[Ss]{numeric_season:02d}[Ee]{numeric_episode:02d}", # S01E01
|
||||
rf"[Ss]{numeric_season}[Ee]{numeric_episode:02d}", # S1E01
|
||||
rf"{numeric_season:02d}x{numeric_episode:02d}", # 01x01
|
||||
rf"{numeric_season}x{numeric_episode:02d}", # 1x01
|
||||
rf"[Ss]eason.{numeric_season:02d}.*[Ee]{numeric_episode:02d}", # Season 01 Episode 01
|
||||
rf"[Ss]eason.{numeric_season}.*[Ee]{numeric_episode:02d}", # Season 1 Episode 01
|
||||
rf"[Ss]{numeric_season:02d}.*[Ee]pisode.{numeric_episode:02d}", # S01 Episode 01
|
||||
rf"[Ss]{numeric_season}.*[Ee]pisode.{numeric_episode:02d}", # S1 Episode 01
|
||||
]
|
||||
|
||||
if matching_files:
|
||||
largest_matching_file = max(matching_files, key=lambda x: x["size"])
|
||||
logger.info(f"Torbox: Selected largest matching file (ID: {largest_matching_file['id']}, Name: {largest_matching_file['name']}, Size: {largest_matching_file['size']}) for series")
|
||||
return largest_matching_file["id"]
|
||||
else:
|
||||
logger.warning(f"Torbox: No matching files found for S{season}E{episode}")
|
||||
|
||||
logger.error(f"Torbox: Failed to select appropriate file for {stream_type}")
|
||||
return None
|
||||
for file in files:
|
||||
if not is_video_file(file["short_name"]):
|
||||
continue
|
||||
|
||||
filename = file["short_name"]
|
||||
logger.debug(f"Torbox: Checking file: {filename}")
|
||||
|
||||
# Test RTN parser first
|
||||
if season_episode_in_filename(filename, numeric_season, numeric_episode):
|
||||
logger.debug(f"Torbox: ✓ RTN MATCH: {filename}")
|
||||
matching_files.append(file)
|
||||
else:
|
||||
# Immediate fallback with improved patterns
|
||||
match_found = False
|
||||
for pattern in episode_patterns:
|
||||
if re.search(pattern, filename, re.IGNORECASE):
|
||||
logger.debug(f"Torbox: ✓ REGEX MATCH with pattern '{pattern}': {filename}")
|
||||
matching_files.append(file)
|
||||
match_found = True
|
||||
break
|
||||
|
||||
if not match_found:
|
||||
logger.debug(f"Torbox: ✗ NO MATCH: {filename}")
|
||||
|
||||
if len(matching_files) == 0:
|
||||
logger.warning(f"Torbox: No matching files found for S{numeric_season:02d}E{numeric_episode:02d}")
|
||||
return None
|
||||
|
||||
largest_matching_file = max(matching_files, key=lambda x: x["size"])
|
||||
logger.info(f"Torbox: Selected {largest_matching_file['short_name']} (ID: {largest_matching_file['id']}) from {len(matching_files)} matches")
|
||||
|
||||
if len(matching_files) > 1:
|
||||
logger.debug(f"Torbox: Other matches: {[f['short_name'] for f in matching_files if f != largest_matching_file]}")
|
||||
|
||||
return largest_matching_file["id"]
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ from typing import List
|
|||
from RTN import title_match
|
||||
|
||||
from stream_fusion.utils.filter.language_filter import LanguageFilter
|
||||
from stream_fusion.utils.filter.language_priority_filter import LanguagePriorityFilter
|
||||
from stream_fusion.utils.filter.max_size_filter import MaxSizeFilter
|
||||
from stream_fusion.utils.filter.quality_exclusion_filter import QualityExclusionFilter
|
||||
from stream_fusion.utils.filter.title_exclusion_filter import TitleExclusionFilter
|
||||
|
|
@ -22,16 +23,30 @@ def sort_quality(item: TorrentItem):
|
|||
return priority, item.parsed_data.resolution is None
|
||||
|
||||
|
||||
def get_indexer_priority_for_sort(indexer):
|
||||
"""Fonction pour obtenir la priorité de l'indexer lors du tri"""
|
||||
indexer_priority = {
|
||||
"Yggtorrent": 1, # "Yggtorrent - API"
|
||||
"DMM": 2, # "DMM - API"
|
||||
"Public": 3, # "Public - Cache"
|
||||
"Sharewood": 4, # "Sharewood - API"
|
||||
"Jackett": 5, # Jackett indexers
|
||||
}
|
||||
indexer_name = indexer.split(' ')[0] if indexer and ' ' in indexer else indexer
|
||||
priority = indexer_priority.get(indexer_name, 999)
|
||||
logger.trace(f"Filters: Indexer '{indexer}' -> extracted '{indexer_name}' -> priority {priority}")
|
||||
return priority
|
||||
|
||||
def items_sort(items, config):
|
||||
logger.info(f"Filters: Sorting items by method: {config['sort']}")
|
||||
logger.info(f"Filters: Sorting items by method: {config['sort']} (YggFlix priority at equal quality)")
|
||||
if config["sort"] == "quality":
|
||||
sorted_items = sorted(items, key=sort_quality)
|
||||
sorted_items = sorted(items, key=lambda x: (sort_quality(x), get_indexer_priority_for_sort(x.indexer)))
|
||||
elif config["sort"] == "sizeasc":
|
||||
sorted_items = sorted(items, key=lambda x: int(x.size))
|
||||
sorted_items = sorted(items, key=lambda x: (int(x.size), get_indexer_priority_for_sort(x.indexer)))
|
||||
elif config["sort"] == "sizedesc":
|
||||
sorted_items = sorted(items, key=lambda x: int(x.size), reverse=True)
|
||||
sorted_items = sorted(items, key=lambda x: (-int(x.size), get_indexer_priority_for_sort(x.indexer)))
|
||||
elif config["sort"] == "qualitythensize":
|
||||
sorted_items = sorted(items, key=lambda x: (sort_quality(x), -int(x.size)))
|
||||
sorted_items = sorted(items, key=lambda x: (sort_quality(x), -int(x.size), get_indexer_priority_for_sort(x.indexer)))
|
||||
else:
|
||||
logger.warning(
|
||||
f"Filters: Unrecognized sort method: {config['sort']}. No sorting applied."
|
||||
|
|
@ -39,7 +54,7 @@ def items_sort(items, config):
|
|||
sorted_items = items
|
||||
|
||||
logger.success(
|
||||
f"Filters: Sorting complete. Number of sorted items: {len(sorted_items)}"
|
||||
f"Filters: Sorting complete - Quality first, YggFlix priority at equal quality. Number of sorted items: {len(sorted_items)}"
|
||||
)
|
||||
return sorted_items
|
||||
|
||||
|
|
@ -199,6 +214,9 @@ def filter_items(items, media, config):
|
|||
"exclusion": QualityExclusionFilter(config),
|
||||
# "resultsPerQuality": ResultsPerQualityFilter(config),
|
||||
}
|
||||
|
||||
# Filtre de priorité de langue à appliquer en dernier
|
||||
language_priority_filter = LanguagePriorityFilter(config)
|
||||
|
||||
logger.info(f"Filters: Initial item count: {len(items)}")
|
||||
|
||||
|
|
@ -232,6 +250,34 @@ def filter_items(items, media, config):
|
|||
f"Filters: Error while applying {filter_name} filter", exc_info=e
|
||||
)
|
||||
|
||||
# Appliquer le filtre de priorité de langue en dernier pour trier les résultats
|
||||
try:
|
||||
logger.info(f"Filters: Applying language priority filter")
|
||||
items = language_priority_filter(items)
|
||||
logger.success(f"Filters: Items sorted by language priority")
|
||||
|
||||
# Tri secondaire par qualité au sein de chaque groupe linguistique
|
||||
# Regrouper les torrents par priorité de langue
|
||||
language_groups = {}
|
||||
for item in items:
|
||||
priority = getattr(item, 'language_priority', 999)
|
||||
if priority not in language_groups:
|
||||
language_groups[priority] = []
|
||||
language_groups[priority].append(item)
|
||||
|
||||
# Trier chaque groupe par qualité et taille selon la méthode de tri configurée
|
||||
sorted_items = []
|
||||
for priority in sorted(language_groups.keys()):
|
||||
group_items = language_groups[priority]
|
||||
# Appliquer le même tri que celui configuré globalement
|
||||
sorted_group = items_sort(group_items, config)
|
||||
sorted_items.extend(sorted_group)
|
||||
|
||||
items = sorted_items
|
||||
logger.success(f"Filters: Items sorted by language priority and then by quality")
|
||||
except Exception as e:
|
||||
logger.error(f"Filters: Error while applying language priority filter", exc_info=e)
|
||||
|
||||
logger.success(f"Filters: Filtering complete. Final item count: {len(items)}")
|
||||
return items
|
||||
|
||||
|
|
@ -255,8 +301,14 @@ def merge_items(
|
|||
|
||||
def add_to_merged(item: TorrentItem):
|
||||
key = (item.raw_title, item.size)
|
||||
if key not in merged_dict or item.seeders > merged_dict[key].seeders:
|
||||
if key not in merged_dict:
|
||||
merged_dict[key] = item
|
||||
else:
|
||||
existing_priority = get_indexer_priority_for_sort(merged_dict[key].indexer)
|
||||
new_priority = get_indexer_priority_for_sort(item.indexer)
|
||||
|
||||
if new_priority < existing_priority or (new_priority == existing_priority and item.seeders > merged_dict[key].seeders):
|
||||
merged_dict[key] = item
|
||||
|
||||
for item in cache_items:
|
||||
add_to_merged(item)
|
||||
|
|
|
|||
|
|
@ -29,7 +29,9 @@ class TorrentItem:
|
|||
self.trackers = [] # Trackers of the torrent
|
||||
self.file_index = None # Index of the file inside of the torrent - it may be updated durring __process_torrent() and update_availability(). If the index is None and torrent is not None, it means that the series episode is not inside of the torrent.
|
||||
self.full_index = None # Case where we cannot call RD to get the full index. Else None
|
||||
self.availability = False # If it's instantly available on the debrid service
|
||||
|
||||
# Initialize availability as an empty string to match get_unaviable_hashes check
|
||||
self.availability = "" # Stores service codes (e.g., 'AD', 'RD') or remains empty until checked
|
||||
|
||||
self.parsed_data: ParsedData = parsed_data # Ranked result
|
||||
|
||||
|
|
@ -41,6 +43,7 @@ class TorrentItem:
|
|||
"season": media.season if isinstance(media, Series) else None,
|
||||
"episode": media.episode if isinstance(media, Series) else None,
|
||||
"torrent_download": quote(self.torrent_download) if self.torrent_download is not None else None,
|
||||
# Utiliser la chaîne de disponibilité complète comme code de service
|
||||
"service": self.availability if self.availability else "DL",
|
||||
"privacy": self.privacy if self.privacy else "private",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,19 @@ class TorrentService:
|
|||
try:
|
||||
cached_item = await self.torrent_dao.get_torrent_item_by_id(unique_id)
|
||||
if cached_item:
|
||||
return cached_item.to_torrent_item()
|
||||
torrent_item = cached_item.to_torrent_item()
|
||||
|
||||
# Vérifier si c'est un torrent YggFlix mal mis en cache (sans torrent_download)
|
||||
if (indexer == "Yggtorrent - API" and
|
||||
(not torrent_item.torrent_download or
|
||||
not settings.yggflix_url or
|
||||
settings.yggflix_url not in torrent_item.torrent_download)):
|
||||
|
||||
logger.info(f"TorrentService: YggFlix torrent {raw_title} mal mis en cache, suppression pour retraitement")
|
||||
await self.torrent_dao.delete_torrent_item(unique_id)
|
||||
return None
|
||||
|
||||
return torrent_item
|
||||
return None
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting cached torrent: {e}")
|
||||
|
|
@ -96,6 +108,7 @@ class TorrentService:
|
|||
def __process_ygg_api_url(self, result: TorrentItem):
|
||||
if not self.config["yggflix"]:
|
||||
logger.error("Yggflix is not enabled in the config. Skipping processing of Yggflix URL.")
|
||||
return result
|
||||
try:
|
||||
response = self.__session.get(result.link, timeout=10)
|
||||
time.sleep(0.1) # Add a delay of 0.1 seconds between requests faire usage for small VPS
|
||||
|
|
@ -107,7 +120,11 @@ class TorrentService:
|
|||
return result
|
||||
|
||||
if response.status_code == 200:
|
||||
return self.__process_torrent(result, response.content)
|
||||
# Pour YggFlix, on garde le lien .torrent original pour les trackers privés
|
||||
processed_result = self.__process_torrent(result, response.content)
|
||||
# Conserver le lien .torrent pour AllDebrid
|
||||
processed_result.torrent_download = result.link
|
||||
return processed_result
|
||||
elif response.status_code == 422:
|
||||
self.logger.info(f"Not aviable torrent on yggflix: {result.file_name}")
|
||||
else:
|
||||
|
|
@ -137,12 +154,38 @@ class TorrentService:
|
|||
return result
|
||||
|
||||
def __process_torrent(self, result: TorrentItem, torrent_file):
|
||||
metadata = bencode.bdecode(torrent_file)
|
||||
try:
|
||||
metadata = bencode.bdecode(torrent_file)
|
||||
except Exception as e:
|
||||
try:
|
||||
from bencodepy import Decoder
|
||||
decoder = Decoder(encoding='latin-1')
|
||||
metadata = decoder.decode(torrent_file)
|
||||
except Exception as inner_e:
|
||||
logger.error(f"Impossible de décoder le fichier torrent: {str(e)} puis {str(inner_e)}")
|
||||
result.torrent_download = result.link
|
||||
result.trackers = []
|
||||
result.info_hash = ""
|
||||
result.magnet = ""
|
||||
return result
|
||||
|
||||
result.torrent_download = result.link
|
||||
result.trackers = self.__get_trackers_from_torrent(metadata)
|
||||
result.info_hash = self.__convert_torrent_to_hash(metadata["info"])
|
||||
result.magnet = self.__build_magnet(result.info_hash, metadata["info"]["name"], result.trackers)
|
||||
# Conserver le lien .torrent original si ce n'est pas déjà défini
|
||||
if not result.torrent_download:
|
||||
result.torrent_download = result.link
|
||||
|
||||
try:
|
||||
result.trackers = self.__get_trackers_from_torrent(metadata)
|
||||
result.info_hash = self.__convert_torrent_to_hash(metadata["info"])
|
||||
|
||||
# Pour tous les torrents, construire le magnet complet normalement
|
||||
# La distinction AllDebrid vs autres services se fait au niveau debrid
|
||||
result.magnet = self.__build_magnet(result.info_hash, metadata["info"]["name"], result.trackers)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors du traitement des métadonnées du torrent: {str(e)}")
|
||||
result.trackers = []
|
||||
result.info_hash = ""
|
||||
result.magnet = ""
|
||||
|
||||
if "files" not in metadata["info"]:
|
||||
result.file_index = 1
|
||||
|
|
|
|||
|
|
@ -26,14 +26,11 @@ class TorrentSmartContainer:
|
|||
self.__media = media
|
||||
|
||||
def get_unaviable_hashes(self):
|
||||
hashes = []
|
||||
for hash, item in self.__itemsDict.items():
|
||||
if item.availability is False:
|
||||
hashes.append(hash)
|
||||
self.logger.debug(
|
||||
f"TorrentSmartContainer: Retrieved {len(hashes)} hashes to process"
|
||||
)
|
||||
return hashes
|
||||
return [item.info_hash for item in self.get_items() if item.availability == ""]
|
||||
|
||||
def get_unavailable_magnets(self):
|
||||
"""Retourne les liens magnet pour les items qui n'ont pas encore de disponibilité marquée."""
|
||||
return [item.magnet for item in self.get_items() if item.availability == "" and item.magnet]
|
||||
|
||||
def get_items(self):
|
||||
items = list(self.__itemsDict.values())
|
||||
|
|
@ -261,21 +258,22 @@ class TorrentSmartContainer:
|
|||
for data in response["data"]["magnets"]:
|
||||
torrent_item: TorrentItem = self.__itemsDict[data["hash"]]
|
||||
|
||||
# Set availability to AD immediately for all files
|
||||
torrent_item.availability = "AD"
|
||||
|
||||
# Process files if they exist
|
||||
# Only mark AD availability if we have files from AllDebrid
|
||||
if "files" in data and data["files"]:
|
||||
files = []
|
||||
self._explore_folders_alldebrid(
|
||||
data["files"], files, 1, torrent_item.type, media
|
||||
)
|
||||
if files: # If we found matching files
|
||||
if files:
|
||||
self._update_file_details(torrent_item, files, debrid="AD")
|
||||
else:
|
||||
self.logger.debug(
|
||||
f"No matching AD files for hash {data['hash']}; skipping availability update."
|
||||
)
|
||||
else:
|
||||
# If no files data, still mark as available
|
||||
self.logger.debug(f"No files data for hash {data['hash']}, but marking as available")
|
||||
torrent_item.availability = "AD"
|
||||
self.logger.debug(
|
||||
f"No files data for hash {data['hash']}; skipping AD availability update."
|
||||
)
|
||||
|
||||
self.logger.info(
|
||||
"TorrentSmartContainer: AllDebrid availability update completed"
|
||||
|
|
@ -410,9 +408,101 @@ class TorrentSmartContainer:
|
|||
)
|
||||
|
||||
self.logger.info(
|
||||
"TorrentSmartContainer: Premiumize availability update completed"
|
||||
f"TorrentSmartContainer: Premiumize availability update completed. {len([item for item in torrent_items if item.availability == 'PM'])}/{len(torrent_items)} items marked as instant."
|
||||
)
|
||||
|
||||
def update_availability_stremthru(self, cached_files, store_name, media):
|
||||
"""
|
||||
Met à jour la disponibilité des items basée sur les fichiers retournés par StremThru (via get_cached_files).
|
||||
'cached_files' est maintenant un dictionnaire {info_hash: [file_dict, ...], ...}
|
||||
'store_name' est le nom du store interne StremThru (ex: 'alldebrid', 'realdebrid').
|
||||
"""
|
||||
if not cached_files or not isinstance(cached_files, dict):
|
||||
self.logger.debug(f"update_availability_stremthru: No cached files provided or not a dict: {type(cached_files)}")
|
||||
return
|
||||
|
||||
# --- MODIFICATION: Calculer le nombre total de fichiers pour le log ---
|
||||
total_files_count = sum(len(files) for files in cached_files.values())
|
||||
self.logger.info(f"TorrentSmartContainer: Updating availability from Stremthru cached files dict for store '{store_name}' ({len(cached_files)} hashes, {total_files_count} files total)")
|
||||
|
||||
# --- MODIFICATION: Revenir à la logique précédente pour générer le code ---
|
||||
# Déterminer le code de disponibilité basé sur le store StremThru
|
||||
# store_availability_code = config.get_stremthru_availability_code(store_name) # Utilise une méthode de config
|
||||
# Générer le code court pour la disponibilité (comme avant)
|
||||
availability_code = store_name[:2].upper() if store_name else "ST" # Utiliser 'ST' si store_name est None/vide
|
||||
if store_name == "alldebrid": availability_code = "AD"
|
||||
elif store_name == "easydebrid": availability_code = "ED"
|
||||
elif store_name == "realdebrid": availability_code = "RD"
|
||||
elif store_name == "premiumize": availability_code = "PM"
|
||||
elif store_name == "debridlink": availability_code = "DL"
|
||||
elif store_name == "pikpak": availability_code = "PK"
|
||||
elif store_name == "offcloud": availability_code = "OC"
|
||||
elif store_name == "torbox": availability_code = "TB"
|
||||
# Ajouter d'autres si besoin
|
||||
|
||||
# IMPORTANT: Préfixer le code pour indiquer la gestion par Stremthru
|
||||
stremthru_availability_code = f"ST:{availability_code}"
|
||||
|
||||
self.logger.info(f"Using availability code '{stremthru_availability_code}' for store '{store_name}' from Stremthru cached files.")
|
||||
|
||||
# Log the raw data received from StremThru
|
||||
self.logger.debug(f"TorrentSmartContainer: Received raw cached_files data from Stremthru for store '{store_name}': {cached_files}")
|
||||
|
||||
updated_hashes = set() # Garder trace des hashes uniques mis à jour
|
||||
updated_hashes_count = 0
|
||||
skipped_non_matching = 0
|
||||
|
||||
# --- MODIFICATION: Itérer sur les valeurs (listes de fichiers), puis les fichiers ---
|
||||
for info_hash_key, files_list in cached_files.items(): # Itérer sur les paires clé(hash)/valeur(liste de fichiers)
|
||||
item = self.__itemsDict.get(info_hash_key) # Récupérer l'item TorrentItem correspondant au hash
|
||||
if not item:
|
||||
# Ce cas ne devrait pas arriver si get_cached_files a bien utilisé les hashes du container
|
||||
self.logger.warning(f"update_availability_stremthru: Infohash {info_hash_key} from StremThru response not found in container.")
|
||||
continue
|
||||
|
||||
# Marquer que ce hash a été mis à jour (au moins un fichier trouvé)
|
||||
if info_hash_key not in updated_hashes:
|
||||
updated_hashes_count += 1
|
||||
updated_hashes.add(info_hash_key)
|
||||
|
||||
# Traiter chaque fichier trouvé pour ce hash
|
||||
for file_info in files_list: # Maintenant, file_info est bien un dictionnaire
|
||||
# Validation basique de file_info (pourrait être renforcée)
|
||||
if not isinstance(file_info, dict):
|
||||
self.logger.warning(f"update_availability_stremthru: Expected dict for file_info, got {type(file_info)} for hash {info_hash_key}. Skipping this file.")
|
||||
continue
|
||||
|
||||
# info_hash = file_info.get("info_hash") # On a déjà info_hash_key
|
||||
file_index = file_info.get("file_index")
|
||||
file_title = file_info.get("title")
|
||||
file_size = file_info.get("size")
|
||||
|
||||
# Skip invalid or unknown indices (e.g., None or -1)
|
||||
if file_index is None or file_index < 0:
|
||||
self.logger.debug(
|
||||
f"update_availability_stremthru: Skipping file '{file_title}' for hash {info_hash_key} due to invalid file_index {file_index}."
|
||||
)
|
||||
skipped_non_matching += 1
|
||||
continue
|
||||
# Update first valid file_info using base method (use availability_code without ST: prefix)
|
||||
if item:
|
||||
self._update_file_details(
|
||||
item,
|
||||
[{'file_index': file_index, 'title': file_title, 'size': file_size}],
|
||||
debrid=f"ST:{availability_code}"
|
||||
)
|
||||
self.logger.debug(
|
||||
f"StremThru: Updated file details for hash {info_hash_key}, file_index {file_index}, title '{file_title}', availability '{availability_code}'"
|
||||
)
|
||||
# Only use the first matching file
|
||||
break
|
||||
# Count skips due to missing file_index
|
||||
if file_index is None:
|
||||
skipped_non_matching += 1
|
||||
|
||||
# Log final
|
||||
self.logger.info(f"TorrentSmartContainer: Availability update from Stremthru completed. {updated_hashes_count} items potentially updated. Skipped {skipped_non_matching} files due to missing index.")
|
||||
|
||||
def _update_file_details(self, torrent_item, files, debrid: str = "??"):
|
||||
if not files:
|
||||
self.logger.debug(
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import json
|
|||
from urllib.parse import unquote
|
||||
import redis.asyncio as redis
|
||||
import asyncio
|
||||
import time
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, Response, status
|
||||
from redis.exceptions import LockError
|
||||
from fastapi.responses import RedirectResponse, StreamingResponse
|
||||
|
|
@ -21,6 +22,7 @@ from stream_fusion.utils.debrid.get_debrid_service import (
|
|||
)
|
||||
from stream_fusion.utils.debrid.realdebrid import RealDebrid
|
||||
from stream_fusion.utils.debrid.torbox import Torbox
|
||||
from stream_fusion.utils.debrid.stremthrudebrid import StremThruDebrid
|
||||
from stream_fusion.utils.parse_config import parse_config
|
||||
from stream_fusion.utils.string_encoding import decodeb64
|
||||
from stream_fusion.utils.security import check_api_key
|
||||
|
|
@ -91,37 +93,46 @@ class ProxyStreamer:
|
|||
logger.debug("Playback: Streaming connection closed")
|
||||
|
||||
|
||||
# class ProxyStreamer:
|
||||
# def __init__(self, request: Request, url: str, headers: dict):
|
||||
# self.request = request
|
||||
# self.url = url
|
||||
# self.headers = headers
|
||||
# self.response = None
|
||||
|
||||
# async def stream_content(self):
|
||||
# async with self.request.app.state.http_session.get(
|
||||
# self.url, headers=self.headers
|
||||
# ) as self.response:
|
||||
# async for chunk in self.response.content.iter_any():
|
||||
# yield chunk
|
||||
|
||||
# async def close(self):
|
||||
# if self.response:
|
||||
# await self.response.release()
|
||||
# logger.debug("Streaming connection closed")
|
||||
|
||||
|
||||
async def handle_download(
|
||||
query: dict, config: dict, ip: str, redis_cache: RedisCache
|
||||
) -> str:
|
||||
api_key = config.get("apiKey")
|
||||
cache_key = f"download:{api_key}:{json.dumps(query)}_{ip}"
|
||||
cache_key_params = {k: query[k] for k in sorted(query.keys())}
|
||||
cache_key = f"download:{api_key}:{json.dumps(cache_key_params)}_{ip}"
|
||||
|
||||
# Check if a download is already in progress
|
||||
if await redis_cache.get(cache_key) == DOWNLOAD_IN_PROGRESS_FLAG:
|
||||
logger.info("Playback: Download already in progress")
|
||||
return settings.no_cache_video_url
|
||||
|
||||
# Determine the actual download service (could be StremThru even for 'DL' request)
|
||||
download_service = get_download_service(config)
|
||||
|
||||
# If StremThru is the configured download handler
|
||||
if isinstance(download_service, StremThruDebrid):
|
||||
logger.info("Playback (handle_download): StremThru service detected as download handler. Attempting direct stream link retrieval.")
|
||||
# Ensure store_code present for StremThruDebrid
|
||||
if not query.get('store_code'):
|
||||
default_service = config.get('debridDownloader')
|
||||
inv = {v: k for k, v in StremThruDebrid.STORE_CODE_TO_NAME.items()}
|
||||
code = inv.get(default_service.lower()) if default_service else None
|
||||
if code:
|
||||
logger.debug(f"Playback: inferred store_code '{code}' for StremThruDebrid based on debridDownloader '{default_service}'")
|
||||
query['store_code'] = code
|
||||
else:
|
||||
logger.warning(f"Playback: cannot infer store_code for StremThruDebrid from config.debridDownloader '{default_service}'")
|
||||
try:
|
||||
direct_link = await download_service.get_stream_link(query, config, ip)
|
||||
if direct_link:
|
||||
logger.success(f"Playback (handle_download): StremThru provided direct link: {direct_link[:60]}...")
|
||||
# Return the direct link immediately, skip Redis flag and status page
|
||||
return direct_link
|
||||
else:
|
||||
logger.warning("Playback (handle_download): StremThru did not provide an immediate link. Proceeding with background process indication.")
|
||||
except Exception as e:
|
||||
logger.error(f"Playback (handle_download): Error getting StremThru link: {e}. Proceeding with background process indication.")
|
||||
# Fall through to the standard download status logic
|
||||
|
||||
# Mark the start of the download
|
||||
await redis_cache.set(
|
||||
cache_key, DOWNLOAD_IN_PROGRESS_FLAG, expiration=600 # 10 minute expiration
|
||||
|
|
@ -163,39 +174,44 @@ async def handle_download(
|
|||
status_code=500,
|
||||
detail="Failed to add magnet or torrent to TorBox",
|
||||
)
|
||||
elif isinstance(debrid_service, StremThruDebrid):
|
||||
# StremThru handles caching/downloading via its get_stream_link/add_magnet logic.
|
||||
# No explicit background caching call needed here.
|
||||
logger.info("Playback: StremThru service detected. Skipping explicit background caching call.")
|
||||
pass # Nothing to do here for StremThru in this specific block
|
||||
else:
|
||||
magnet = query["magnet"]
|
||||
torrent_download = (
|
||||
unquote(query["torrent_download"])
|
||||
if query["torrent_download"] is not None
|
||||
else None
|
||||
)
|
||||
try:
|
||||
# Start background caching
|
||||
if debrid_service.start_background_caching(magnet, query):
|
||||
logger.success(
|
||||
f"Playback: Started background caching for magnet: {magnet[:50]}"
|
||||
)
|
||||
# Check if the service supports background caching before attempting to call it
|
||||
if hasattr(debrid_service, 'start_background_caching'):
|
||||
if debrid_service.start_background_caching(query):
|
||||
logger.success(
|
||||
f"Playback: Started background caching for magnet: {query['magnet'][:50]}"
|
||||
)
|
||||
else:
|
||||
# If the method exists but returns False, it failed
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="Failed to start background caching (service returned false)"
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="Failed to start background caching"
|
||||
)
|
||||
# Log a warning if the service doesn't support this method
|
||||
logger.warning(f"Playback: Service {type(debrid_service).__name__} does not support start_background_caching. Skipping.")
|
||||
# Since caching isn't started/needed, clear the progress flag immediately.
|
||||
await redis_cache.delete(cache_key)
|
||||
except Exception as e:
|
||||
logger.error(f"Error starting background caching: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Failed to start background caching: {str(e)}"
|
||||
)
|
||||
# Ensure the flag is deleted on any exception during the caching attempt
|
||||
await redis_cache.delete(cache_key)
|
||||
return settings.no_cache_video_url
|
||||
except Exception as e:
|
||||
# Ensure the flag is deleted on any other exception within handle_download
|
||||
await redis_cache.delete(cache_key)
|
||||
logger.error(f"Playback: Error handling download: {str(e)}", exc_info=True)
|
||||
raise e
|
||||
|
||||
|
||||
async def get_stream_link(
|
||||
decoded_query: str, config: dict, ip: str, redis_cache: RedisCache, cache_user_identifier: str
|
||||
decoded_query: str, config: dict, ip: str, redis_cache: RedisCache, cache_user_identifier: str, request: Request
|
||||
) -> str:
|
||||
logger.debug(f"Playback: Getting stream link for query: {decoded_query}, IP: {ip}")
|
||||
cache_key = f"stream_link:{cache_user_identifier}:{decoded_query}"
|
||||
|
|
@ -213,8 +229,10 @@ async def get_stream_link(
|
|||
if service == "DL":
|
||||
link = await handle_download(query, config, ip, redis_cache)
|
||||
elif service:
|
||||
debrid_service = get_debrid_service(config, service)
|
||||
link = debrid_service.get_stream_link(query, config, ip)
|
||||
debrid_service = get_debrid_service(config, service, request)
|
||||
logger.info(f"Playback: Attempting get_stream_link for service '{type(debrid_service).__name__}' and query {decoded_query}...")
|
||||
link = await debrid_service.get_stream_link(query, config, ip)
|
||||
logger.info(f"Playback: Service '{type(debrid_service).__name__}' returned link: {link}")
|
||||
else:
|
||||
logger.error("Playback: Service not found in query")
|
||||
raise HTTPException(status_code=500, detail="Service not found in query")
|
||||
|
|
@ -228,6 +246,154 @@ async def get_stream_link(
|
|||
return link
|
||||
|
||||
|
||||
# Nouvelle route pour le playback via Stremthru
|
||||
@router.get("/stremthru/{store_code}/{config}/{query}")
|
||||
@rate_limiter(limit=settings.playback_limit_requests, seconds=settings.playback_limit_seconds)
|
||||
async def get_stremthru_playback(
|
||||
store_code: str,
|
||||
config: str,
|
||||
query: str,
|
||||
request: Request,
|
||||
redis_cache: RedisCache = Depends(get_redis_cache_dependency),
|
||||
apikey_dao: APIKeyDAO = Depends(),
|
||||
):
|
||||
start_time = time.time()
|
||||
ip = request.client.host
|
||||
logger.info(f"Playback GET Stremthru/{store_code}: Request received from {ip}")
|
||||
|
||||
try:
|
||||
config_dict = parse_config(config)
|
||||
api_key = config_dict.get("apiKey")
|
||||
cache_user_identifier = api_key if api_key else ip
|
||||
|
||||
# Valider la clé API si elle existe
|
||||
if api_key:
|
||||
try:
|
||||
await check_api_key(api_key, apikey_dao)
|
||||
logger.info(f"Playback GET Stremthru/{store_code}: Valid API key provided by {ip}")
|
||||
except HTTPException as e:
|
||||
logger.warning(f"Playback GET Stremthru/{store_code}: Invalid API key provided by {ip}. Error: {e.detail}")
|
||||
raise e
|
||||
else:
|
||||
logger.info(f"Playback GET Stremthru/{store_code}: No API key provided by {ip}. Proceeding without validation.")
|
||||
|
||||
if not query:
|
||||
raise HTTPException(status_code=400, detail="Query required.")
|
||||
|
||||
decoded_query = decodeb64(query)
|
||||
query_dict = json.loads(decoded_query)
|
||||
|
||||
# Récupérer l'instance StremThruDebrid
|
||||
# Assurez-vous que StremThruDebrid peut être initialisé correctement avec config_dict
|
||||
# et qu'il utilise le bon store_name basé sur la config ou store_code?
|
||||
# Pour l'instant, supposons que l'instance StremThru est le gestionnaire principal.
|
||||
try:
|
||||
stremthru_service = StremThruDebrid(config_dict)
|
||||
# TODO: Vérifier si StremThruDebrid utilise bien le store_code implicitement
|
||||
# ou s'il faut le passer/configurer spécifiquement.
|
||||
# Il lit `store_name` et `store_auth` de config_dict, donc la config URL doit les contenir.
|
||||
except Exception as e:
|
||||
logger.error(f"Playback GET Stremthru/{store_code}: Failed to initialize StremThruDebrid: {e}")
|
||||
raise HTTPException(status_code=500, detail="Failed to initialize Stremthru service")
|
||||
|
||||
logger.info(f"Playback GET Stremthru/{store_code}: Attempting to get stream link via Stremthru service.")
|
||||
logger.debug(f"Playback GET Stremthru/{store_code}: Query details: {query_dict}")
|
||||
|
||||
# Add store_code to query_dict before passing it
|
||||
query_dict['store_code'] = store_code
|
||||
|
||||
# Appeler get_stream_link qui gère tout le processus (add, wait, unrestrict)
|
||||
# Elle prend query (dict), config (dict), ip (str)
|
||||
stream_link = await stremthru_service.get_stream_link(query=query_dict, config=config_dict, ip=ip)
|
||||
|
||||
if not stream_link:
|
||||
logger.error(f"Playback GET Stremthru/{store_code}: Failed to get stream link from Stremthru service.")
|
||||
raise HTTPException(status_code=502, detail="Failed to get stream link from Stremthru service")
|
||||
|
||||
logger.info(f"Playback GET Stremthru/{store_code}: Stream link obtained: {stream_link[:60]}...")
|
||||
|
||||
# Rediriger ou Proxy
|
||||
if settings.proxied_link:
|
||||
logger.info(f"Playback GET Stremthru/{store_code}: Proxying stream from {stream_link[:60]}...")
|
||||
headers = {key: value for key, value in request.headers.items() if key.lower() in ["range", "accept"]}
|
||||
streamer = ProxyStreamer(request, stream_link, headers)
|
||||
return StreamingResponse(
|
||||
streamer.stream_content(),
|
||||
headers={"Accept-Ranges": "bytes", "Content-Range": request.headers.get("range", "bytes 0-")},
|
||||
background=BackgroundTask(streamer.close),
|
||||
)
|
||||
else:
|
||||
logger.info(f"Playback GET Stremthru/{store_code}: Redirecting to {stream_link[:60]}...")
|
||||
return RedirectResponse(url=stream_link, status_code=302)
|
||||
|
||||
except HTTPException: # Re-raise HTTP exceptions
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Playback GET Stremthru/{store_code}: Unexpected error: {e}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=f"Internal server error during Stremthru playback: {e}")
|
||||
finally:
|
||||
end_time = time.time()
|
||||
logger.info(f"Playback GET Stremthru/{store_code}: Request processing time: {end_time - start_time:.2f} seconds")
|
||||
|
||||
# Nouvelle route HEAD pour Stremthru
|
||||
@router.head("/stremthru/{store_code}/{config}/{query}")
|
||||
async def head_stremthru_playback(
|
||||
store_code: str,
|
||||
config: str,
|
||||
query: str,
|
||||
request: Request,
|
||||
apikey_dao: APIKeyDAO = Depends(), # Ajouter dépendances nécessaires
|
||||
):
|
||||
ip = request.client.host
|
||||
logger.info(f"Playback HEAD Stremthru/{store_code}: Request received from {ip}")
|
||||
|
||||
try:
|
||||
config_dict = parse_config(config)
|
||||
api_key = config_dict.get("apiKey")
|
||||
|
||||
# Valider la clé API si elle existe
|
||||
if api_key:
|
||||
try:
|
||||
await check_api_key(api_key, apikey_dao)
|
||||
logger.info(f"Playback HEAD Stremthru/{store_code}: Valid API key provided by {ip}")
|
||||
except HTTPException as e:
|
||||
logger.warning(f"Playback HEAD Stremthru/{store_code}: Invalid API key provided by {ip}. Error: {e.detail}")
|
||||
raise e
|
||||
else:
|
||||
logger.info(f"Playback HEAD Stremthru/{store_code}: No API key provided by {ip}. Proceeding without validation.")
|
||||
|
||||
if not query:
|
||||
raise HTTPException(status_code=400, detail="Query required.")
|
||||
|
||||
# La logique HEAD est simple: renvoyer OK avec les bons en-têtes.
|
||||
# On ne vérifie PAS le lien distant ici pour Stremthru pour l'instant.
|
||||
# Le client (Stremio) fera la requête GET si le HEAD réussit.
|
||||
headers = {
|
||||
"Content-Type": "video/mp4",
|
||||
"Accept-Ranges": "bytes",
|
||||
"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0",
|
||||
"Pragma": "no-cache",
|
||||
"Expires": "0",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "GET, HEAD, OPTIONS",
|
||||
"Content-Length": "0" # Indiquer une taille 0 car on ne connaît pas la vraie taille
|
||||
}
|
||||
logger.info(f"Playback HEAD Stremthru/{store_code}: Returning 200 OK for HEAD request.")
|
||||
return Response(status_code=status.HTTP_200_OK, headers=headers)
|
||||
|
||||
except HTTPException: # Re-raise HTTP exceptions
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Playback HEAD Stremthru/{store_code}: Unexpected error: {e}", exc_info=True)
|
||||
return Response(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
content=ErrorResponse(
|
||||
detail="An error occurred while processing the HEAD request."
|
||||
).model_dump_json(),
|
||||
media_type="application/json",
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{config}/{query}", responses={500: {"model": ErrorResponse}})
|
||||
@rate_limiter(limit=20, seconds=60, redis=redis_session)
|
||||
async def get_playback(
|
||||
|
|
@ -277,8 +443,8 @@ async def get_playback(
|
|||
try:
|
||||
if await lock.acquire(blocking=False):
|
||||
logger.debug("Playback: Lock acquired, getting stream link")
|
||||
# Pass cache_user_identifier to get_stream_link
|
||||
link = await get_stream_link(decoded_query, config, ip, redis_cache, cache_user_identifier)
|
||||
# Pass cache_user_identifier and request to get_stream_link
|
||||
link = await get_stream_link(decoded_query, config, ip, redis_cache, cache_user_identifier, request)
|
||||
else:
|
||||
logger.debug("Playback: Lock not acquired, waiting for cached link")
|
||||
# Use cache_user_identifier for cache key lookup
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import json
|
|||
import queue
|
||||
import re
|
||||
import threading
|
||||
import json
|
||||
from typing import List
|
||||
|
||||
from RTN import ParsedData
|
||||
|
|
@ -71,10 +72,30 @@ def parse_to_debrid_stream(
|
|||
results: queue.Queue,
|
||||
media: Media,
|
||||
):
|
||||
if torrent_item.availability:
|
||||
name = f"{INSTANTLY_AVAILABLE}|–{torrent_item.availability}-|{INSTANTLY_AVAILABLE}"
|
||||
# Determine if AllDebrid cache is available (direct or via StremThru)
|
||||
avail = torrent_item.availability
|
||||
is_ad_cached = False
|
||||
if isinstance(avail, dict):
|
||||
# check codes like 'ST:AD' in dict values
|
||||
for code in avail.values():
|
||||
if 'AD' in code:
|
||||
is_ad_cached = True
|
||||
break
|
||||
elif isinstance(avail, str) and 'AD' in avail:
|
||||
is_ad_cached = True
|
||||
# Determine display name: show only service code when cached
|
||||
if is_ad_cached:
|
||||
# extract code from availability value
|
||||
if isinstance(avail, dict):
|
||||
first = next(iter(avail.values()), None)
|
||||
code = first if isinstance(first, str) else None
|
||||
else:
|
||||
code = avail if isinstance(avail, str) else None
|
||||
code = code or "AD"
|
||||
name = f"{INSTANTLY_AVAILABLE}{code}+"
|
||||
else:
|
||||
name = f"{DOWNLOAD_REQUIRED}|–DL-|{DOWNLOAD_REQUIRED}"
|
||||
# show file title when not cached
|
||||
name = torrent_item.file_name or torrent_item.raw_title
|
||||
|
||||
parsed_data: ParsedData = torrent_item.parsed_data
|
||||
|
||||
|
|
@ -120,21 +141,19 @@ def parse_to_debrid_stream(
|
|||
json.dumps(torrent_item.to_debrid_stream_query(media))
|
||||
).replace("=", "%3D")
|
||||
|
||||
results.put(
|
||||
{
|
||||
"name": name,
|
||||
"description": title,
|
||||
"url": f"{host}/playback/{configb64}/{queryb64}",
|
||||
"behaviorHints": {
|
||||
"bingeGroup": f"stremio-jackett-{torrent_item.info_hash}",
|
||||
"filename": (
|
||||
torrent_item.file_name
|
||||
if torrent_item.file_name is not None
|
||||
else torrent_item.raw_title
|
||||
),
|
||||
},
|
||||
results.put({
|
||||
"name": name,
|
||||
"description": title,
|
||||
"url": f"{host}/playback/{configb64}/{queryb64}",
|
||||
"behaviorHints": {
|
||||
"bingeGroup": f"stremio-jackett-{torrent_item.info_hash}",
|
||||
"filename": (
|
||||
torrent_item.file_name
|
||||
if torrent_item.file_name is not None
|
||||
else torrent_item.raw_title
|
||||
),
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
if torrenting and torrent_item.privacy == "public":
|
||||
name = f"{DIRECT_TORRENT}\n{parsed_data.quality}\n"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import hashlib
|
||||
import time
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
|
||||
import asyncio
|
||||
|
||||
from stream_fusion.services.postgresql.dao.apikey_dao import APIKeyDAO
|
||||
from stream_fusion.services.postgresql.dao.torrentitem_dao import TorrentItemDAO
|
||||
|
|
@ -37,12 +37,14 @@ from stream_fusion.utils.torrent.torrent_smart_container import TorrentSmartCont
|
|||
from stream_fusion.utils.zilean.zilean_result import ZileanResult
|
||||
from stream_fusion.utils.zilean.zilean_service import ZileanService
|
||||
from stream_fusion.settings import settings
|
||||
from stream_fusion.utils.debrid.stremthrudebrid import StremThruDebrid
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/{config}/stream/{stream_type}/{stream_id}", response_model=SearchResponse)
|
||||
@router.get("/{config}/stream/{stream_type}/{stream_id:path}", response_model=SearchResponse)
|
||||
async def get_results(
|
||||
request: Request,
|
||||
config: str,
|
||||
|
|
@ -288,25 +290,72 @@ async def get_results(
|
|||
search_results = ResultsPerQualityFilter(config).filter(raw_search_results)
|
||||
logger.info(f"Search: Filtered search results per quality: {len(search_results)}")
|
||||
|
||||
def stream_processing(search_results, media, config):
|
||||
async def stream_processing(search_results, media, config) -> list[dict]:
|
||||
torrent_smart_container = TorrentSmartContainer(search_results, media)
|
||||
|
||||
if config["debrid"]:
|
||||
for debrid in debrid_services:
|
||||
hashes = torrent_smart_container.get_unaviable_hashes()
|
||||
ip = request.client.host
|
||||
result = debrid.get_availability_bulk(hashes, ip)
|
||||
tasks = []
|
||||
|
||||
for service in debrid_services:
|
||||
service_name = service.__class__.__name__
|
||||
logger.debug(f"Processing service: {service_name}")
|
||||
|
||||
# Check if StremThruDebrid is the service instance
|
||||
if isinstance(service, StremThruDebrid):
|
||||
logger.debug(f"Creating StremThru task for {service_name}.get_cached_files_async")
|
||||
# StremThru uses get_cached_files_async with all items
|
||||
task = asyncio.create_task(service.get_cached_files_async(torrent_smart_container.get_items()))
|
||||
tasks.append((service_name, task, service))
|
||||
else:
|
||||
# Direct services use get_availability_bulk with unavailable hashes
|
||||
hashes_to_check = torrent_smart_container.get_unaviable_hashes()
|
||||
if not hashes_to_check:
|
||||
logger.debug(f"No unavailable hashes to check with direct service {service_name}. Skipping.")
|
||||
continue
|
||||
logger.debug(f"Creating Direct Debrid task for {service_name}.get_availability_bulk with {len(hashes_to_check)} hashes")
|
||||
# Pass unavailable hashes and media context
|
||||
task = asyncio.create_task(service.get_availability_bulk(hashes_to_check, media))
|
||||
tasks.append((service_name, task, service))
|
||||
|
||||
# Gather results from all tasks (StremThru or Direct)
|
||||
if not tasks:
|
||||
logger.debug("No availability check tasks were created.")
|
||||
results = []
|
||||
else:
|
||||
results = await asyncio.gather(*[task for _, task, _ in tasks])
|
||||
logger.debug(f"Gathered {len(results)} results from availability checks.")
|
||||
|
||||
for i, (service_name, _, service_instance) in enumerate(tasks):
|
||||
result = results[i]
|
||||
logger.debug(f"Processing result from {service_name} (instance type: {type(service_instance).__name__})")
|
||||
|
||||
if isinstance(service_instance, StremThruDebrid):
|
||||
if result:
|
||||
torrent_smart_container.update_availability(
|
||||
result, type(debrid), media
|
||||
)
|
||||
logger.info(
|
||||
f"Search: Checked availability for {len(result.items())} items with {type(debrid).__name__}"
|
||||
)
|
||||
result_dict, used_store_name = result
|
||||
if result_dict:
|
||||
logger.debug(f"Updating availability from StremThru store '{used_store_name}'")
|
||||
torrent_smart_container.update_availability_stremthru(cached_files=result_dict, store_name=used_store_name, media=media)
|
||||
else:
|
||||
logger.info(f"No cached files found by StremThru for store '{used_store_name}'.")
|
||||
else:
|
||||
logger.warning(
|
||||
"Search: No availability results found in debrid service"
|
||||
)
|
||||
logger.warning(f"Invalid or empty result received from StremThru {service_name}.")
|
||||
elif isinstance(result, dict):
|
||||
logger.debug(f"Updating availability from direct service {service_name}")
|
||||
torrent_smart_container.update_availability(debrid_response=result, debrid_type=type(service_instance), media=media)
|
||||
else:
|
||||
logger.warning(f"Received unexpected result type ({type(result)}) from {service_name}. Skipping update.")
|
||||
|
||||
logger.debug("--- Entering Availability Check Block ---")
|
||||
logger.debug("--- Container Items Availability Check (After Updates) ---")
|
||||
container_items = torrent_smart_container.get_items()
|
||||
if container_items:
|
||||
for i, item in enumerate(container_items[:5]):
|
||||
filename_log = item.file_name[:60] if item.file_name else "[No Filename]"
|
||||
logger.debug(f"Item {i} Hash: {item.info_hash}, Filename: {filename_log}..., Availability: {item.availability}")
|
||||
if len(container_items) > 5:
|
||||
logger.debug(f"... (logged first 5 out of {len(container_items)} items)")
|
||||
else:
|
||||
logger.debug("Container is empty after updates.")
|
||||
logger.debug("--- End Availability Check ---")
|
||||
|
||||
if config["cache"]:
|
||||
logger.info("Search: Caching public container items")
|
||||
|
|
@ -322,7 +371,7 @@ async def get_results(
|
|||
|
||||
return stream_list
|
||||
|
||||
stream_list = stream_processing(search_results, media, config)
|
||||
stream_list = await stream_processing(search_results, media, config)
|
||||
streams = [Stream(**stream) for stream in stream_list]
|
||||
await redis_cache.set(stream_cache_key(media), streams, expiration=1200)
|
||||
total_time = time.time() - start
|
||||
|
|
|
|||
Loading…
Reference in a new issue