mirror of
https://github.com/Viren070/MediaFusion.git
synced 2025-12-01 23:21:11 +01:00
Improve Contribution torrents import workflow and add Telegram notifications
Implemented support for sports content, including metadata collection, validation, and episode file annotation logic with modal dialogs for detailed input. Added a Telegram bot for notifications regarding new contributions.
This commit is contained in:
+151
-21
@@ -41,17 +41,89 @@
|
||||
<!-- Torrent Upload Parameters -->
|
||||
<div id="torrentUploadParameters" style="display: none;">
|
||||
<div class="mb-3">
|
||||
<label for="metaType" class="form-label">Content Type</label>
|
||||
<select class="form-select" id="metaType" name="metaType" onchange="updateContentType()" required>
|
||||
<option value="movie" {% if prefill_data.meta_type == 'movie' %}selected{% endif %}>Movie</option>
|
||||
<option value="series" {% if prefill_data.meta_type == 'series' %}selected{% endif %}>Series</option>
|
||||
<option value="sports">Sports Content</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3" id="torrentImdbIdContainer">
|
||||
<label for="torrentImdbId" class="form-label">IMDb ID</label>
|
||||
<input type="text" class="form-control" id="torrentImdbId" name="torrentImdbId" maxlength="10" placeholder="Enter IMDb ID (e.g., tt1234567)"
|
||||
value="{% if prefill_data.meta_id %}{{ prefill_data.meta_id }}{% endif %}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="metaType" class="form-label">Meta Type</label>
|
||||
<select class="form-select" id="metaType" name="metaType" onchange="updateMetaType()" required>
|
||||
<option value="movie" {% if prefill_data.meta_type == 'movie' %}selected{% endif %}>Movie</option>
|
||||
<option value="series" {% if prefill_data.meta_type == 'series' %}selected{% endif %}>Series</option>
|
||||
</select>
|
||||
|
||||
<!-- Sports Metadata Section -->
|
||||
<div id="sportsMetadata" style="display: none;" class="mb-4">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Sports Content Details</h5>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="sportsCatalog" class="form-label">Sports Category *</label>
|
||||
<select class="form-select" id="sportsCatalog" name="sportsCatalog" required>
|
||||
<option value="american_football">American Football</option>
|
||||
<option value="baseball">Baseball</option>
|
||||
<option value="basketball">Basketball</option>
|
||||
<option value="football">Football</option>
|
||||
<option value="formula_racing">Formula Racing</option>
|
||||
<option value="hockey">Hockey</option>
|
||||
<option value="motogp_racing">MotoGP Racing</option>
|
||||
<option value="other_sports">Other Sports</option>
|
||||
<option value="rugby">Rugby/AFL</option>
|
||||
<option value="fighting">Fighting (WWE, UFC)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="sportsTitle" class="form-label">Title *</label>
|
||||
<input type="text" class="form-control" id="sportsTitle" name="sportsTitle" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="sportsYear" class="form-label">Year</label>
|
||||
<input type="text" class="form-control" id="sportsYear" name="sportsYear" placeholder="YYYY or YYYY-YYYY">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="sportsPoster" class="form-label">Poster URL *</label>
|
||||
<input type="url" class="form-control" id="sportsPoster" name="sportsPoster" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="sportsBackground" class="form-label">Background URL</label>
|
||||
<input type="url" class="form-control" id="sportsBackground" name="sportsBackground" placeholder="Leave empty to use poster">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="sportsLogo" class="form-label">Logo URL</label>
|
||||
<input type="url" class="form-control" id="sportsLogo" name="sportsLogo">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="addTitleToPoster" name="addTitleToPoster">
|
||||
<label class="form-check-label" for="addTitleToPoster">
|
||||
Add title to poster
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="sportsDescription" class="form-label">Description</label>
|
||||
<textarea class="form-control" id="sportsDescription" name="sportsDescription" rows="3"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="sportsWebsite" class="form-label">Website</label>
|
||||
<input type="url" class="form-control" id="sportsWebsite" name="sportsWebsite">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="createdAt" class="form-label">Created At</label>
|
||||
<input type="date" class="form-control" id="createdAt" name="createdAt" required>
|
||||
@@ -77,7 +149,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Catalogs Selection -->
|
||||
<div class="mb-3">
|
||||
<div class="mb-3" id="catalogsSelection">
|
||||
<label class="form-label">Catalogs (Optional)</label>
|
||||
<div class="alert alert-info" role="alert">
|
||||
<i class="bi bi-info-circle"></i> Selecting catalogs helps organize content but is optional
|
||||
@@ -114,24 +186,81 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Series Parameters -->
|
||||
<div id="seriesParameters" class="mb-3" style="display: none;">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<label for="season" class="form-label">Season</label>
|
||||
<input type="number" class="form-control" id="season" name="season" min="1" value="{% if prefill_data.season %}{{ prefill_data.season }}{% endif %}">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="episodes" class="form-label">Episodes</label>
|
||||
<input type="text" class="form-control" id="episodes" name="episodes" value="{% if prefill_data.episode %}{{ prefill_data.episode }}{% endif %}"
|
||||
placeholder="1,2,3 or 1-10">
|
||||
<div class="alert alert-info" role="alert">
|
||||
<i class="bi bi-info-circle"></i> Enter episode numbers separated by commas or a range (e.g., 1,2,3 or 1-10)
|
||||
<!-- File Annotation Modal -->
|
||||
<div class="modal fade" id="fileAnnotationModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Annotate Video Files</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="alert alert-info">
|
||||
<i class="bi bi-info-circle"></i> Please specify season and episode numbers for each video file
|
||||
</div>
|
||||
|
||||
<!-- Bulk Season Assignment -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title">Bulk Season Assignment</h6>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Season</label>
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control" id="bulkSeason" min="1" placeholder="Season number">
|
||||
<button class="btn btn-outline-secondary" type="button" id="applyBulkSeason">Apply to All</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Multiple Seasons</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="multipleSeasons" placeholder="e.g., 1-3 or 1,2,4">
|
||||
<button class="btn btn-outline-secondary" type="button" id="applyMultiSeasons">Apply Sequence</button>
|
||||
</div>
|
||||
<small class="text-muted">For multi-season packs. Will assign seasons sequentially to files.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- File grouping options -->
|
||||
<div id="fileGroupingOptions" style="display: none;">
|
||||
<h6 class="mt-3">Season Distribution</h6>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="seasonDistribution" id="autoDistribute" value="auto" checked>
|
||||
<label class="form-check-label" for="autoDistribute">
|
||||
Auto-distribute episodes across seasons
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="seasonDistribution" id="manualGroup" value="manual">
|
||||
<label class="form-check-label" for="manualGroup">
|
||||
Manually group episodes per season
|
||||
</label>
|
||||
</div>
|
||||
<div id="episodesPerSeason" class="mt-2" style="display: none;">
|
||||
<label class="form-label">Episodes per season</label>
|
||||
<input type="number" class="form-control" id="episodeCount" min="1" placeholder="Number of episodes per season">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- File list for annotation -->
|
||||
<div id="fileAnnotationList"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" id="confirmAnnotation">Confirm</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Uploader Information -->
|
||||
<div class="mb-3">
|
||||
<label for="uploaderName" class="form-label">Uploader Name (Optional)</label>
|
||||
<input type="text" class="form-control" id="uploaderName" name="uploaderName" placeholder="Enter your name or leave blank for Anonymous">
|
||||
</div>
|
||||
|
||||
<!-- Upload Methods -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label">Upload Method</label>
|
||||
@@ -315,7 +444,8 @@
|
||||
<div id="blockTorrentParameters" style="display: none;">
|
||||
<div class="mb-3">
|
||||
<label for="blockTorrentInfoHash" class="form-label">Torrent Info Hash</label>
|
||||
<input type="text" class="form-control" id="blockTorrentInfoHash" name="blockTorrentInfoHash" required>
|
||||
<input type="text" class="form-control" id="blockTorrentInfoHash" name="blockTorrentInfoHash" required
|
||||
value="{% if prefill_data.info_hash %}{{ prefill_data.info_hash }}{% endif %}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+330
-67
@@ -255,19 +255,234 @@ function showConfirmationDialog(validationErrors, torrentData, infoHash) {
|
||||
});
|
||||
}
|
||||
|
||||
function updateMetaType() {
|
||||
function updateContentType() {
|
||||
const metaType = document.getElementById('metaType').value;
|
||||
if (metaType === 'movie') {
|
||||
setElementDisplay('catalogsSeries', 'none');
|
||||
setElementDisplay('catalogsMovie', 'block');
|
||||
setElementDisplay('seriesParameters', 'none');
|
||||
|
||||
if (metaType === 'sports') {
|
||||
setElementDisplay('sportsMetadata', 'block');
|
||||
setElementDisplay('torrentImdbIdContainer', 'none');
|
||||
setElementDisplay('catalogsSelection', 'none');
|
||||
} else {
|
||||
setElementDisplay('catalogsMovie', 'none');
|
||||
setElementDisplay('catalogsSeries', 'block');
|
||||
setElementDisplay('seriesParameters', 'block');
|
||||
setElementDisplay('sportsMetadata', 'none');
|
||||
setElementDisplay('torrentImdbIdContainer', 'block');
|
||||
setElementDisplay('catalogsSelection', 'block');
|
||||
if (metaType === 'movie') {
|
||||
setElementDisplay('catalogsSeries', 'none');
|
||||
setElementDisplay('catalogsMovie', 'block');
|
||||
} else {
|
||||
setElementDisplay('catalogsMovie', 'none');
|
||||
setElementDisplay('catalogsSeries', 'block');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function collectSportsMetadata() {
|
||||
return {
|
||||
title: document.getElementById('sportsTitle').value,
|
||||
year: document.getElementById('sportsYear').value,
|
||||
poster: document.getElementById('sportsPoster').value,
|
||||
background: document.getElementById('sportsBackground').value,
|
||||
logo: document.getElementById('sportsLogo').value,
|
||||
description: document.getElementById('sportsDescription').value,
|
||||
website: document.getElementById('sportsWebsite').value,
|
||||
is_add_title_to_poster: document.getElementById('addTitleToPoster').checked,
|
||||
catalogs: document.getElementById('sportsCatalog').value
|
||||
};
|
||||
}
|
||||
|
||||
function parseSeasonsInput(input) {
|
||||
const seasons = [];
|
||||
const parts = input.split(',');
|
||||
|
||||
for (const part of parts) {
|
||||
if (part.includes('-')) {
|
||||
const [start, end] = part.split('-').map(num => parseInt(num.trim()));
|
||||
for (let i = start; i <= end; i++) {
|
||||
seasons.push(i);
|
||||
}
|
||||
} else {
|
||||
const season = parseInt(part.trim());
|
||||
if (!isNaN(season)) {
|
||||
seasons.push(season);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return seasons;
|
||||
}
|
||||
|
||||
function showFileAnnotationModal(files) {
|
||||
const modal = document.getElementById('fileAnnotationModal');
|
||||
const fileList = document.getElementById('fileAnnotationList');
|
||||
fileList.innerHTML = '';
|
||||
|
||||
const isSportsContent = document.getElementById('metaType').value === 'sports';
|
||||
|
||||
// Sort files by filename
|
||||
files.sort((a, b) => {
|
||||
return a.filename.localeCompare(b.filename, undefined, {
|
||||
numeric: true,
|
||||
sensitivity: 'base'
|
||||
});
|
||||
});
|
||||
|
||||
files.forEach((file, index) => {
|
||||
const fileRow = `
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<h6 class="card-subtitle mb-2 text-muted">${file.filename}</h6>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Season</label>
|
||||
<input type="number" class="form-control season-input"
|
||||
id="season-${index}"
|
||||
data-index="${index}"
|
||||
value="${file.season_number || ''}"
|
||||
min="1">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Episode</label>
|
||||
<input type="number" class="form-control"
|
||||
id="episode-${index}"
|
||||
value="${file.episode_number || ''}"
|
||||
min="1">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${isSportsContent ? `
|
||||
<div class="episode-metadata mt-3">
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Episode Title</label>
|
||||
<input type="text" class="form-control"
|
||||
id="title-${index}"
|
||||
placeholder="Optional">
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Episode Overview</label>
|
||||
<textarea class="form-control"
|
||||
id="overview-${index}"
|
||||
rows="2"
|
||||
placeholder="Optional"></textarea>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Thumbnail URL</label>
|
||||
<input type="url" class="form-control"
|
||||
id="thumbnail-${index}"
|
||||
placeholder="Optional">
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Release Date</label>
|
||||
<input type="date" class="form-control"
|
||||
id="release-${index}">
|
||||
</div>
|
||||
</div>
|
||||
` : `
|
||||
`}
|
||||
</div>
|
||||
</div>`;
|
||||
fileList.insertAdjacentHTML('beforeend', fileRow);
|
||||
});
|
||||
|
||||
// Set up bulk season assignment handler
|
||||
document.getElementById('applyBulkSeason').onclick = () => {
|
||||
const season = document.getElementById('bulkSeason').value;
|
||||
if (season) {
|
||||
document.querySelectorAll('.season-input').forEach(input => {
|
||||
input.value = season;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Set up multiple seasons handler
|
||||
document.getElementById('applyMultiSeasons').onclick = () => {
|
||||
const seasonsInput = document.getElementById('multipleSeasons').value;
|
||||
if (!seasonsInput) return;
|
||||
|
||||
const seasons = parseSeasonsInput(seasonsInput);
|
||||
if (seasons.length === 0) return;
|
||||
|
||||
const fileGroupingOptions = document.getElementById('fileGroupingOptions');
|
||||
fileGroupingOptions.style.display = 'block';
|
||||
|
||||
const applySeasons = () => {
|
||||
const distribution = document.querySelector('input[name="seasonDistribution"]:checked').value;
|
||||
const episodesPerSeason = parseInt(document.getElementById('episodeCount').value) || 0;
|
||||
|
||||
const seasonInputs = document.querySelectorAll('.season-input');
|
||||
if (distribution === 'auto') {
|
||||
// Distribute episodes evenly across seasons
|
||||
const filesPerSeason = Math.ceil(seasonInputs.length / seasons.length);
|
||||
seasonInputs.forEach((input, index) => {
|
||||
const seasonIndex = Math.floor(index / filesPerSeason);
|
||||
input.value = seasons[Math.min(seasonIndex, seasons.length - 1)];
|
||||
});
|
||||
} else {
|
||||
// Manual distribution based on episodes per season
|
||||
if (episodesPerSeason > 0) {
|
||||
seasonInputs.forEach((input, index) => {
|
||||
const seasonIndex = Math.floor(index / episodesPerSeason);
|
||||
input.value = seasons[Math.min(seasonIndex, seasons.length - 1)];
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Set up distribution method handlers
|
||||
document.querySelectorAll('input[name="seasonDistribution"]').forEach(radio => {
|
||||
radio.onchange = () => {
|
||||
document.getElementById('episodesPerSeason').style.display =
|
||||
radio.value === 'manual' ? 'block' : 'none';
|
||||
if (radio.value === 'auto') {
|
||||
applySeasons();
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
document.getElementById('episodeCount').onchange = () => {
|
||||
if (document.getElementById('manualGroup').checked) {
|
||||
applySeasons();
|
||||
}
|
||||
};
|
||||
|
||||
// Initial application
|
||||
applySeasons();
|
||||
};
|
||||
|
||||
const bsModal = new bootstrap.Modal(modal);
|
||||
bsModal.show();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
document.getElementById('confirmAnnotation').onclick = () => {
|
||||
const annotatedFiles = files.map((file, index) => {
|
||||
const baseData = {
|
||||
...file,
|
||||
season_number: parseInt(document.getElementById(`season-${index}`).value) || null,
|
||||
episode_number: parseInt(document.getElementById(`episode-${index}`).value) || null,
|
||||
};
|
||||
|
||||
if (isSportsContent) {
|
||||
return {
|
||||
...baseData,
|
||||
title: document.getElementById(`title-${index}`).value || null,
|
||||
overview: document.getElementById(`overview-${index}`).value || null,
|
||||
thumbnail: document.getElementById(`thumbnail-${index}`).value || null,
|
||||
release_date: document.getElementById(`release-${index}`).value || null,
|
||||
|
||||
};
|
||||
}
|
||||
return baseData;
|
||||
});
|
||||
bsModal.hide();
|
||||
resolve(annotatedFiles);
|
||||
};
|
||||
|
||||
modal.addEventListener('hidden.bs.modal', () => {
|
||||
reject(new Error('Annotation cancelled'));
|
||||
}, {once: true});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function toggleInput(disableId, input) {
|
||||
document.getElementById(disableId).disabled = !!input.value;
|
||||
}
|
||||
@@ -293,7 +508,7 @@ function updateFormFields() {
|
||||
case 'add_torrent':
|
||||
setElementDisplay("torrentUploadParameters", "block");
|
||||
authRequired = false;
|
||||
updateMetaType();
|
||||
updateContentType();
|
||||
break;
|
||||
case 'scrapy':
|
||||
// Show Scrapy-specific parameters
|
||||
@@ -389,40 +604,62 @@ function constructTvMetadata() {
|
||||
return tvMetaData;
|
||||
}
|
||||
|
||||
|
||||
async function handleAddTorrent(submitBtn, loadingSpinner, forceImport = false) {
|
||||
async function handleAddTorrent(submitBtn, loadingSpinner, forceImport = false, annotatedFiles = null) {
|
||||
let formData = new FormData();
|
||||
const imdbId = document.getElementById('torrentImdbId').value;
|
||||
const metaType = document.getElementById('metaType').value;
|
||||
const isSportsContent = metaType === 'sports';
|
||||
|
||||
// Basic validation
|
||||
const imdbIdNumeric = parseInt(imdbId.slice(2), 10);
|
||||
if (!imdbId.startsWith('tt') || imdbId.length < 3 || imdbId.length > 10 || isNaN(imdbIdNumeric)) {
|
||||
showNotification('Invalid IMDb ID', 'error');
|
||||
resetButton(submitBtn, loadingSpinner);
|
||||
return;
|
||||
}
|
||||
|
||||
formData.append('meta_id', imdbId);
|
||||
formData.append('meta_type', metaType);
|
||||
|
||||
// Handle optional catalogs
|
||||
const catalogInputs = metaType === 'movie'
|
||||
? document.querySelectorAll('#catalogsMovie input[name="catalogs"]:checked')
|
||||
: document.querySelectorAll('#catalogsSeries input[name="catalogs"]:checked');
|
||||
|
||||
const catalogs = Array.from(catalogInputs).map(el => el.value);
|
||||
if (catalogs.length > 0) {
|
||||
formData.append('catalogs', catalogs.join(','));
|
||||
}
|
||||
|
||||
// Handle languages
|
||||
const selectedLanguages = Array.from(document.querySelectorAll('input[name="languages"]:checked'))
|
||||
.map(el => el.value);
|
||||
if (selectedLanguages.length > 0) {
|
||||
formData.append('languages', selectedLanguages.join(','));
|
||||
// Handle sports content metadata
|
||||
if (isSportsContent) {
|
||||
const sportsMetadata = collectSportsMetadata();
|
||||
|
||||
// Validate required fields
|
||||
if (!sportsMetadata.title || !sportsMetadata.poster || !sportsMetadata.catalogs) {
|
||||
showNotification('Title, poster, and sports category are required.', 'error');
|
||||
resetButton(submitBtn, loadingSpinner);
|
||||
return;
|
||||
}
|
||||
|
||||
// Add sports metadata to formData
|
||||
Object.entries(sportsMetadata).forEach(([key, value]) => {
|
||||
if (value !== null && value !== '') {
|
||||
formData.append(key, value);
|
||||
}
|
||||
});
|
||||
|
||||
// Set meta_type based on catalog
|
||||
const isSeriesType = ['formula_racing', 'motogp_racing'].includes(sportsMetadata.catalog);
|
||||
formData.append('meta_type', isSeriesType ? 'series' : 'movie');
|
||||
formData.append('is_sports_content', 'true');
|
||||
} else {
|
||||
// Handle regular IMDb content
|
||||
const imdbId = document.getElementById('torrentImdbId').value;
|
||||
const imdbIdNumeric = parseInt(imdbId.slice(2), 10);
|
||||
if (!imdbId.startsWith('tt') || imdbId.length < 3 || imdbId.length > 10 || isNaN(imdbIdNumeric)) {
|
||||
showNotification('Invalid IMDb ID', 'error');
|
||||
resetButton(submitBtn, loadingSpinner);
|
||||
return;
|
||||
}
|
||||
formData.append('meta_id', imdbId);
|
||||
formData.append('meta_type', metaType);
|
||||
|
||||
// Handle optional catalogs
|
||||
const catalogInputs = metaType === 'movie'
|
||||
? document.querySelectorAll('#catalogsMovie input[name="catalogs"]:checked')
|
||||
: document.querySelectorAll('#catalogsSeries input[name="catalogs"]:checked');
|
||||
|
||||
const catalogs = Array.from(catalogInputs).map(el => el.value);
|
||||
if (catalogs.length > 0) {
|
||||
formData.append('catalogs', catalogs.join(','));
|
||||
}
|
||||
const selectedLanguages = Array.from(document.querySelectorAll('input[name="languages"]:checked'))
|
||||
.map(el => el.value);
|
||||
if (selectedLanguages.length > 0) {
|
||||
formData.append('languages', selectedLanguages.join(','));
|
||||
}
|
||||
}
|
||||
|
||||
// Handle common form fields
|
||||
const createdAt = document.getElementById('createdAt').value;
|
||||
if (!createdAt) {
|
||||
showNotification('Created At is required.', 'error');
|
||||
@@ -431,28 +668,11 @@ async function handleAddTorrent(submitBtn, loadingSpinner, forceImport = false)
|
||||
}
|
||||
formData.append('created_at', createdAt);
|
||||
|
||||
if (metaType === 'series') {
|
||||
const season = document.getElementById('season').value;
|
||||
let episodes = document.getElementById('episodes').value;
|
||||
if (!season || !episodes) {
|
||||
showNotification('Season and Episodes are required for TV Series.', 'error');
|
||||
resetButton(submitBtn, loadingSpinner);
|
||||
return;
|
||||
}
|
||||
if (episodes.includes('-')) {
|
||||
const [start, end] = episodes.split('-');
|
||||
episodes = Array.from({length: end - start + 1}, (_, i) => parseInt(start) + i).join(',');
|
||||
} else {
|
||||
episodes = episodes.split(',').map(e => e.trim());
|
||||
}
|
||||
|
||||
formData.append('season', season);
|
||||
formData.append('episodes', episodes);
|
||||
}
|
||||
|
||||
// Handle torrent file/magnet
|
||||
const magnetLink = document.getElementById('magnetLink').value;
|
||||
const torrentFile = document.getElementById('torrentFile').files[0];
|
||||
const torrentType = document.getElementById('torrentType').value;
|
||||
|
||||
if (!magnetLink && !torrentFile) {
|
||||
showNotification('Either Magnet Link or Torrent File is required.', 'error');
|
||||
resetButton(submitBtn, loadingSpinner);
|
||||
@@ -464,8 +684,8 @@ async function handleAddTorrent(submitBtn, loadingSpinner, forceImport = false)
|
||||
resetButton(submitBtn, loadingSpinner);
|
||||
return;
|
||||
}
|
||||
formData.append('torrent_type', torrentType);
|
||||
|
||||
formData.append('torrent_type', torrentType);
|
||||
if (magnetLink) {
|
||||
formData.append('magnet_link', magnetLink);
|
||||
} else {
|
||||
@@ -477,6 +697,15 @@ async function handleAddTorrent(submitBtn, loadingSpinner, forceImport = false)
|
||||
formData.append('force_import', 'true');
|
||||
}
|
||||
|
||||
// Add uploader name
|
||||
const uploaderName = document.getElementById('uploaderName').value.trim() || 'Anonymous';
|
||||
formData.append('uploader', uploaderName);
|
||||
|
||||
// Add annotated files if available
|
||||
if (annotatedFiles) {
|
||||
formData.append('file_data', JSON.stringify(annotatedFiles));
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/scraper/torrent', {
|
||||
method: 'POST',
|
||||
@@ -485,8 +714,19 @@ async function handleAddTorrent(submitBtn, loadingSpinner, forceImport = false)
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === 'validation_failed' && !forceImport) {
|
||||
// Show confirmation dialog
|
||||
if (data.status === 'needs_annotation') {
|
||||
try {
|
||||
// Show modal for file annotation
|
||||
const newAnnotatedFiles = await showFileAnnotationModal(data.files);
|
||||
// Retry with the annotated files
|
||||
await handleAddTorrent(submitBtn, loadingSpinner, forceImport, newAnnotatedFiles);
|
||||
return;
|
||||
} catch (annotationError) {
|
||||
console.error('Error annotating files:', annotationError);
|
||||
showNotification('File annotation was cancelled', 'warning');
|
||||
}
|
||||
} else if (data.status === 'validation_failed' && !forceImport) {
|
||||
// Show confirmation dialog but preserve the annotated files
|
||||
const shouldForceImport = await showConfirmationDialog(
|
||||
data.errors,
|
||||
data.torrent_data,
|
||||
@@ -494,25 +734,46 @@ async function handleAddTorrent(submitBtn, loadingSpinner, forceImport = false)
|
||||
);
|
||||
|
||||
if (shouldForceImport) {
|
||||
// Retry with force import
|
||||
submitBtn.disabled = true;
|
||||
loadingSpinner.style.display = 'inline-block';
|
||||
await handleAddTorrent(submitBtn, loadingSpinner, true);
|
||||
// Pass the existing annotated files to the next attempt
|
||||
await handleAddTorrent(submitBtn, loadingSpinner, true, annotatedFiles);
|
||||
return;
|
||||
}
|
||||
} else if (data.status === 'validation_failed' && forceImport) {
|
||||
showNotification(`Validation failed: ${JSON.stringify(data.errors)}`, 'error');
|
||||
} else if (data.detail) {
|
||||
showNotification(data.detail, 'error');
|
||||
} else {
|
||||
showNotification(data.status, 'success');
|
||||
// Clear the form on success
|
||||
resetForm();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error submitting scraper form:', error);
|
||||
showNotification(`Error submitting scraper form. Error: ${error.toString()}`, 'error');
|
||||
console.error('Error submitting torrent:', error);
|
||||
showNotification(`Error submitting torrent: ${error.toString()}`, 'error');
|
||||
} finally {
|
||||
resetButton(submitBtn, loadingSpinner);
|
||||
}
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
document.getElementById('torrentImdbId').value = '';
|
||||
document.getElementById('magnetLink').value = '';
|
||||
document.getElementById('torrentFile').value = '';
|
||||
document.getElementById('uploaderName').value = '';
|
||||
|
||||
// Reset sports metadata if present
|
||||
if (document.getElementById('sportsMetadata')) {
|
||||
document.getElementById('sportsTitle').value = '';
|
||||
document.getElementById('sportsYear').value = '';
|
||||
document.getElementById('sportsPoster').value = '';
|
||||
document.getElementById('sportsBackground').value = '';
|
||||
document.getElementById('sportsLogo').value = '';
|
||||
document.getElementById('sportsDescription').value = '';
|
||||
document.getElementById('sportsWebsite').value = '';
|
||||
document.getElementById('addTitleToPoster').checked = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAddTvMetadata(payload, submitBtn, loadingSpinner) {
|
||||
try {
|
||||
payload['tv_metadata'] = constructTvMetadata();
|
||||
@@ -714,5 +975,7 @@ async function submitScraperForm() {
|
||||
|
||||
|
||||
// Initial update for form fields on page load
|
||||
document.addEventListener('DOMContentLoaded', updateFormFields);
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
updateFormFields();
|
||||
});
|
||||
document.getElementById('spiderName').addEventListener('change', toggleSpiderSpecificFields);
|
||||
+139
-33
@@ -1,6 +1,7 @@
|
||||
import json
|
||||
import logging
|
||||
from datetime import date, datetime
|
||||
from typing import Literal
|
||||
from typing import Literal, Optional
|
||||
from uuid import uuid4
|
||||
|
||||
from fastapi import HTTPException, UploadFile, File, Form, APIRouter, BackgroundTasks
|
||||
@@ -14,17 +15,19 @@ from db.crud import (
|
||||
get_series_data_by_id,
|
||||
get_stream_by_info_hash,
|
||||
store_new_torrent_streams,
|
||||
update_metadata,
|
||||
get_or_create_metadata,
|
||||
)
|
||||
from db.enums import TorrentType
|
||||
from db.models import TorrentStreams, EpisodeFile
|
||||
from db.redis_database import REDIS_ASYNC_CLIENT
|
||||
from mediafusion_scrapy.task import run_spider
|
||||
from scrapers import imdb_data
|
||||
from scrapers.tv import add_tv_metadata, parse_m3u_playlist
|
||||
from utils import const, torrent
|
||||
from utils.network import get_request_namespace
|
||||
from utils.parser import calculate_max_similarity_ratio
|
||||
from utils.parser import calculate_max_similarity_ratio, convert_bytes_to_readable
|
||||
from utils.runtime_const import TEMPLATES
|
||||
from utils.telegram_bot import telegram_notifier
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -36,7 +39,7 @@ def validate_api_password(api_password: str):
|
||||
|
||||
|
||||
def raise_error(error_msg: str):
|
||||
error_msg = f"User upload Failed due to: {error_msg}"
|
||||
error_msg = f"Failed due to: {error_msg}"
|
||||
logging.warning(error_msg)
|
||||
raise HTTPException(status_code=200, detail=error_msg)
|
||||
|
||||
@@ -44,16 +47,20 @@ def raise_error(error_msg: str):
|
||||
@router.get("/", tags=["scraper"])
|
||||
async def get_scraper(
|
||||
request: Request,
|
||||
action: str = None,
|
||||
meta_id: str = None,
|
||||
meta_type: str = None,
|
||||
season: int = None,
|
||||
episode: str = None,
|
||||
info_hash: str = None,
|
||||
):
|
||||
prefill_data = {
|
||||
"meta_id": meta_id,
|
||||
"meta_type": meta_type,
|
||||
"season": season,
|
||||
"episode": episode,
|
||||
"action": action,
|
||||
"info_hash": info_hash,
|
||||
}
|
||||
return TEMPLATES.TemplateResponse(
|
||||
"html/scraper.html",
|
||||
@@ -157,7 +164,7 @@ async def update_imdb_data(
|
||||
if not movie:
|
||||
raise_error(f"Movie with ID {meta_id} not found.")
|
||||
|
||||
await imdb_data.process_imdb_data([meta_id], media_type)
|
||||
await update_metadata([meta_id], media_type)
|
||||
|
||||
if redirect_video:
|
||||
return RedirectResponse(
|
||||
@@ -169,17 +176,28 @@ async def update_imdb_data(
|
||||
|
||||
@router.post("/torrent", tags=["scraper"])
|
||||
async def add_torrent(
|
||||
meta_id: str = Form(...),
|
||||
meta_type: Literal["movie", "series"] = Form(...),
|
||||
catalogs: str = Form(None),
|
||||
languages: str = Form(None),
|
||||
meta_id: Optional[str] = Form(None),
|
||||
# Sports metadata fields
|
||||
title: Optional[str] = Form(None),
|
||||
year: Optional[int] = Form(None),
|
||||
poster: Optional[str] = Form(None),
|
||||
background: Optional[str] = Form(None),
|
||||
logo: Optional[str] = Form(None),
|
||||
description: Optional[str] = Form(None),
|
||||
website: Optional[str] = Form(None),
|
||||
is_add_title_to_poster: bool = Form(False),
|
||||
catalogs: Optional[str] = Form(None),
|
||||
is_sports_content: bool = Form(False),
|
||||
# Common fields
|
||||
languages: Optional[str] = Form(None),
|
||||
created_at: date = Form(...),
|
||||
season: int | None = Form(None),
|
||||
episodes: str | None = Form(None),
|
||||
torrent_type: TorrentType = Form(TorrentType.PUBLIC),
|
||||
magnet_link: str = Form(None),
|
||||
torrent_file: UploadFile = File(None),
|
||||
magnet_link: Optional[str] = Form(None),
|
||||
torrent_file: Optional[UploadFile] = File(None),
|
||||
force_import: bool = Form(False),
|
||||
file_data: Optional[str] = Form(None),
|
||||
uploader: Optional[str] = Form("Anonymous"),
|
||||
):
|
||||
torrent_data: dict = {}
|
||||
info_hash = None
|
||||
@@ -189,8 +207,6 @@ async def add_torrent(
|
||||
raise_error("Either magnet link or torrent file must be provided.")
|
||||
if torrent_type != TorrentType.PUBLIC and not torrent_file:
|
||||
raise_error(f"Torrent file must be provided for {torrent_type} torrents.")
|
||||
if not meta_id.startswith("tt") or not meta_id[2:].isdigit():
|
||||
raise_error("Invalid IMDb ID. Must start with 'tt'.")
|
||||
|
||||
# Convert catalogs string to list if provided
|
||||
catalog_list = catalogs.split(",") if catalogs else []
|
||||
@@ -198,9 +214,6 @@ async def add_torrent(
|
||||
# Convert languages string to list if provided
|
||||
language_list = languages.split(",") if languages else []
|
||||
|
||||
if meta_type == "series" and not (season and episodes):
|
||||
raise_error("Season and episode number must be provided for series.")
|
||||
|
||||
# Process magnet link or torrent file
|
||||
if magnet_link:
|
||||
info_hash, trackers = torrent.parse_magnet(magnet_link)
|
||||
@@ -233,6 +246,84 @@ async def add_torrent(
|
||||
"status": f"Torrent already exists and attached to meta id: {torrent_stream.meta_id}"
|
||||
}
|
||||
|
||||
# Handle sports content metadata
|
||||
if is_sports_content:
|
||||
if not title or not poster or not catalog_list:
|
||||
raise_error("Title, poster, and sports catalog are required.")
|
||||
|
||||
# Validate catalog is in CATALOG_DATA
|
||||
if catalog_list[0] not in const.CATALOG_DATA:
|
||||
raise_error(
|
||||
f"Invalid sports catalog. Must be one of: {', '.join(const.CATALOG_DATA.keys())}"
|
||||
)
|
||||
|
||||
# Determine if it's a series type catalog
|
||||
is_series_catalog = catalog_list[0] in ["formula_racing", "motogp_racing"]
|
||||
if is_series_catalog:
|
||||
meta_type = "series"
|
||||
else:
|
||||
meta_type = "movie"
|
||||
|
||||
# Create metadata for sports content
|
||||
sports_metadata = {
|
||||
"title": title,
|
||||
"year": year,
|
||||
"poster": poster,
|
||||
"background": background or poster, # Use poster as fallback
|
||||
"logo": logo,
|
||||
"description": description,
|
||||
"website": website,
|
||||
"is_add_title_to_poster": is_add_title_to_poster,
|
||||
"catalogs": catalog_list,
|
||||
"created_at": created_at,
|
||||
}
|
||||
|
||||
# Get or create metadata
|
||||
metadata_result = await get_or_create_metadata(
|
||||
sports_metadata, meta_type, is_search_imdb_title=False, is_imdb_only=False
|
||||
)
|
||||
|
||||
if not metadata_result:
|
||||
raise_error("Failed to create metadata for sports content.")
|
||||
|
||||
meta_id = metadata_result["id"]
|
||||
else:
|
||||
# Regular IMDb content validation
|
||||
if not meta_id or not meta_id.startswith("tt") or not meta_id[2:].isdigit():
|
||||
raise_error("Invalid IMDb ID. Must start with 'tt'.")
|
||||
|
||||
# For series, check if we need file annotation
|
||||
if meta_type == "series":
|
||||
has_all_metadata = all(
|
||||
file.get("season_number") and file.get("episode_number")
|
||||
for file in torrent_data.get("file_data", [])
|
||||
)
|
||||
|
||||
# If we received annotated file data, update the torrent_data
|
||||
if file_data:
|
||||
annotated_files = json.loads(file_data)
|
||||
torrent_data["file_data"] = annotated_files
|
||||
# Update seasons and episodes based on annotations
|
||||
seasons = {
|
||||
file["season_number"]
|
||||
for file in annotated_files
|
||||
if file["season_number"]
|
||||
}
|
||||
episodes = {
|
||||
file["episode_number"]
|
||||
for file in annotated_files
|
||||
if file["episode_number"]
|
||||
}
|
||||
torrent_data["seasons"] = list(seasons)
|
||||
torrent_data["episodes"] = list(episodes)
|
||||
|
||||
# If automatic detection failed and we don't have annotations yet
|
||||
elif not has_all_metadata:
|
||||
return {
|
||||
"status": "needs_annotation",
|
||||
"files": torrent_data.get("file_data", []),
|
||||
}
|
||||
|
||||
source = "Contribution Stream"
|
||||
languages = set(language_list).union(torrent_data.get("languages", []))
|
||||
torrent_data.update(
|
||||
@@ -241,6 +332,7 @@ async def add_torrent(
|
||||
"created_at": created_at,
|
||||
"languages": languages,
|
||||
"torrent_type": torrent_type,
|
||||
"uploader": uploader,
|
||||
}
|
||||
)
|
||||
if torrent_type == TorrentType.PUBLIC:
|
||||
@@ -254,6 +346,8 @@ async def add_torrent(
|
||||
torrent_data["catalog"] = ["contribution_stream"]
|
||||
|
||||
validation_errors = []
|
||||
seasons_and_episodes = {}
|
||||
stream_cache_keys = []
|
||||
|
||||
if meta_type == "movie":
|
||||
movie_data = await get_movie_data_by_id(meta_id)
|
||||
@@ -314,19 +408,7 @@ async def add_torrent(
|
||||
)
|
||||
|
||||
# Episode validation
|
||||
episodes = [int(ep) for ep in episodes.split(",")]
|
||||
if torrent_data.get("episodes"):
|
||||
if (
|
||||
not set(torrent_data.get("episodes")).issubset(set(episodes))
|
||||
and not force_import
|
||||
):
|
||||
validation_errors.append(
|
||||
{
|
||||
"type": "episode_mismatch",
|
||||
"message": f"Episode mismatch: '{torrent_data.get('episodes')}' != '{episodes}'",
|
||||
}
|
||||
)
|
||||
else:
|
||||
if not torrent_data.get("episodes"):
|
||||
validation_errors.append(
|
||||
{
|
||||
"type": "episodes_not_found",
|
||||
@@ -355,9 +437,14 @@ async def add_torrent(
|
||||
info_hash, torrent_data, meta_id
|
||||
)
|
||||
|
||||
stream_cache_keys = [
|
||||
f"torrent_streams:{meta_id}:{season}:{ep}" for ep in episodes
|
||||
]
|
||||
# Prepare seasons and episodes for cache cleanup
|
||||
for ep in torrent_stream.episode_files:
|
||||
if ep.season_number not in seasons_and_episodes:
|
||||
seasons_and_episodes[ep.season_number] = []
|
||||
seasons_and_episodes[ep.season_number].append(ep.episode_number)
|
||||
stream_cache_keys.append(
|
||||
f"torrent_streams:{meta_id}:{ep.season_number}:{ep.episode_number}"
|
||||
)
|
||||
|
||||
if not torrent_stream:
|
||||
raise_error("Failed to store torrent data. Contact support.")
|
||||
@@ -366,6 +453,21 @@ async def add_torrent(
|
||||
for key in stream_cache_keys:
|
||||
await REDIS_ASYNC_CLIENT.delete(key)
|
||||
|
||||
# Send Telegram notification
|
||||
if settings.telegram_bot_token:
|
||||
await telegram_notifier.send_contribution_notification(
|
||||
meta_id=meta_id,
|
||||
title=title,
|
||||
meta_type=meta_type,
|
||||
poster=f"{settings.poster_host_url}/poster/{meta_type}/{meta_id}.jpg",
|
||||
uploader=uploader,
|
||||
info_hash=info_hash,
|
||||
torrent_type=torrent_type,
|
||||
size=convert_bytes_to_readable(torrent_stream.size),
|
||||
torrent_name=torrent_stream.torrent_name,
|
||||
seasons_and_episodes=seasons_and_episodes,
|
||||
)
|
||||
|
||||
return {
|
||||
"status": f"Successfully added torrent: {torrent_stream.id} for {title}. Thanks for your contribution."
|
||||
}
|
||||
@@ -388,7 +490,9 @@ async def handle_movie_stream_store(info_hash, parsed_data, video_id):
|
||||
codec=parsed_data.get("codec"),
|
||||
quality=parsed_data.get("quality"),
|
||||
audio=parsed_data.get("audio"),
|
||||
hdr=parsed_data.get("hdr"),
|
||||
source=parsed_data.get("source"),
|
||||
uploader=parsed_data.get("uploader"),
|
||||
catalog=parsed_data.get("catalog"),
|
||||
updated_at=datetime.now(),
|
||||
seeders=parsed_data.get("seeders"),
|
||||
@@ -440,7 +544,9 @@ async def handle_series_stream_store(info_hash, parsed_data, video_id):
|
||||
codec=parsed_data.get("codec"),
|
||||
quality=parsed_data.get("quality"),
|
||||
audio=parsed_data.get("audio"),
|
||||
hdr=parsed_data.get("hdr"),
|
||||
source=parsed_data.get("source"),
|
||||
uploader=parsed_data.get("uploader"),
|
||||
catalog=parsed_data.get("catalog"),
|
||||
updated_at=datetime.now(),
|
||||
seeders=parsed_data.get("seeders"),
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
import aiohttp
|
||||
|
||||
from db.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TelegramNotifier:
|
||||
def __init__(self):
|
||||
self.bot_token = settings.telegram_bot_token
|
||||
self.chat_id = settings.telegram_chat_id
|
||||
self.base_url = f"https://api.telegram.org/bot{self.bot_token}"
|
||||
self.enabled = bool(self.bot_token and self.chat_id)
|
||||
|
||||
async def send_contribution_notification(
|
||||
self,
|
||||
meta_id: str,
|
||||
title: str,
|
||||
meta_type: str,
|
||||
poster: str,
|
||||
uploader: str,
|
||||
info_hash: str,
|
||||
torrent_type: str,
|
||||
size: str,
|
||||
torrent_name: str,
|
||||
seasons_and_episodes: Optional[dict] = None,
|
||||
):
|
||||
"""Send notification about new contribution to Telegram channel"""
|
||||
if not self.enabled:
|
||||
logger.warning("Telegram notifications are disabled. Check bot token.")
|
||||
return
|
||||
|
||||
# Create block URL - will open scraper page with block_torrent action and info hash
|
||||
block_url = (
|
||||
f"{settings.host_url}/scraper?"
|
||||
f"action=block_torrent&"
|
||||
f"info_hash={info_hash}"
|
||||
)
|
||||
meta_id_data = (
|
||||
f"*IMDb*: [{meta_id}](https://www.imdb.com/title/{meta_id}/)\n"
|
||||
if meta_id.startswith("tt")
|
||||
else f"Meta ID: {meta_id}\n"
|
||||
)
|
||||
|
||||
# Build the message
|
||||
message = (
|
||||
f"🎬 New Contribution\n\n"
|
||||
f"*Title*: {title}\n"
|
||||
f"*Type*: {meta_type.title()}\n"
|
||||
f"{meta_id_data}"
|
||||
f"*Uploader*: {uploader}\n"
|
||||
f"*Size*: {size}\n"
|
||||
f"*Torrent Name*: `{torrent_name}`\n"
|
||||
f"*Info Hash*: `{info_hash}`\n"
|
||||
f"*Type*: {torrent_type}\n"
|
||||
f"*Poster*: [View]({poster})"
|
||||
)
|
||||
|
||||
# Add season/episode info for series
|
||||
if meta_type == "series" and seasons_and_episodes:
|
||||
message += "\n*Seasons*: "
|
||||
for season, episodes in seasons_and_episodes.items():
|
||||
message += f"\n- Season {season}: "
|
||||
if len(episodes) == 1:
|
||||
message += f"{episodes[0]}"
|
||||
else:
|
||||
message += f"{min(episodes)} - {max(episodes)}"
|
||||
message += "\n"
|
||||
|
||||
# Add block link
|
||||
message += f"\n\n[🚫 Block Torrent]({block_url})"
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
# Send a single message with photo and caption
|
||||
async with session.post(
|
||||
f"{self.base_url}/sendPhoto",
|
||||
json={
|
||||
"chat_id": self.chat_id,
|
||||
"photo": poster,
|
||||
"caption": message,
|
||||
"parse_mode": "Markdown",
|
||||
},
|
||||
) as response:
|
||||
if not response.ok:
|
||||
error_data = await response.json()
|
||||
logger.error(
|
||||
f"Failed to send Telegram notification: {error_data}"
|
||||
)
|
||||
# Fallback to text-only message if photo fails
|
||||
await self._send_text_only_message(message)
|
||||
return await response.json()
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending Telegram notification: {e}")
|
||||
# Fallback to text-only message if there's an error
|
||||
await self._send_text_only_message(message)
|
||||
|
||||
async def _send_text_only_message(self, message: str):
|
||||
"""Fallback method to send text-only message if photo sending fails"""
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
await session.post(
|
||||
f"{self.base_url}/sendMessage",
|
||||
json={
|
||||
"chat_id": self.chat_id,
|
||||
"text": message,
|
||||
"parse_mode": "Markdown",
|
||||
"disable_web_page_preview": False,
|
||||
},
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending fallback text message: {e}")
|
||||
|
||||
|
||||
# Create a singleton instance
|
||||
telegram_notifier = TelegramNotifier()
|
||||
Reference in New Issue
Block a user