findFilms/app/templates/torrents.html
vrubelroman a5497eef26 fix(app): исправление скачивания торрентов
- generate_clean_magnet: убраны мёртвые трекеры (coppersurfer.tk, leechers-paradise.org),
  добавлены рабочие (tamersunion.org, exodus.desync.com, moeking.me),
  включено &dn= с URL-кодированием кириллицы
- extract_hash_from_result: новая единая функция извлечения хэша из 5 источников
  (Hash, InfoHash, Magnet, btih: в URL, Id)
- /api/add-torrent: убран ложный success — после Ok. от qBittorrent идёт реальная
  верификация (торрент появился в списке по хэшу или названию). Если не появился — error.
- /api/proxy-torrent-download: новый endpoint для скачивания .torrent файлов
  через NL-прокси (обходит DPI-блокировку)
- torrents.html: кнопка Копировать magnet (Clipboard API + fallback),
  proxy-ссылки для .torrent, disabled-состояния для пустых magnet/torrent_url
- tmdb-proxy: добавлен /proxy-torrent endpoint
- urlencode filter для Jinja2
- test_app.py: 47 тестов на чистые функции
2026-06-03 19:27:14 +00:00

332 lines
13 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Торренты: {{ movie_title }}</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 { color: #333; margin-bottom: 20px; }
.back-link {
display: inline-block;
margin-bottom: 20px;
color: #007bff;
text-decoration: none;
padding: 8px 16px;
border: 1px solid #007bff;
border-radius: 5px;
}
.back-link:hover { background-color: #007bff; color: white; }
.movie-info {
background-color: #e9ecef;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
}
.torrents-list { display: grid; gap: 15px; }
.torrent-item {
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
background: white;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
transition: box-shadow 0.3s ease;
}
.torrent-item:hover { box-shadow: 0 4px 10px rgba(0,0,0,0.15); }
.torrent-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 10px;
color: #333;
}
.torrent-details {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
margin-bottom: 15px;
}
.detail-item { display: flex; flex-direction: column; }
.detail-label {
font-size: 12px;
color: #666;
text-transform: uppercase;
margin-bottom: 4px;
}
.detail-value { font-size: 14px; font-weight: bold; color: #333; }
.resolution-1080p { color: #28a745; }
.resolution-720p { color: #ffc107; }
.resolution-480p { color: #dc3545; }
.resolution-2160p { color: #6f42c1; }
.torrent-actions {
display: flex;
gap: 10px;
margin-top: 15px;
flex-wrap: wrap;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 5px;
cursor: pointer;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 14px;
transition: background-color 0.3s ease;
white-space: nowrap;
}
.btn-primary { background-color: #007bff; color: white; }
.btn-primary:hover { background-color: #0056b3; }
.btn-success { background-color: #28a745; color: white; }
.btn-success:hover { background-color: #1e7e34; }
.btn-secondary { background-color: #6c757d; color: white; }
.btn-secondary:hover { background-color: #545b62; }
.btn-warning { background-color: #ffc107; color: #333; }
.btn-warning:hover { background-color: #e0a800; }
.btn-info { background-color: #17a2b8; color: white; }
.btn-info:hover { background-color: #138496; }
.btn:disabled { opacity: 0.6; cursor: not-allowed; }
.no-torrents {
text-align: center;
color: #666;
font-size: 18px;
padding: 40px;
}
.quality-badge {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
text-transform: uppercase;
}
.quality-bluray { background-color: #007bff; color: white; }
.quality-web-dl { background-color: #28a745; color: white; }
.quality-hdtv { background-color: #ffc107; color: #333; }
.toast {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: #333;
color: #fff;
padding: 12px 24px;
border-radius: 8px;
font-size: 14px;
z-index: 1000;
opacity: 0;
transition: opacity 0.3s ease;
pointer-events: none;
}
.toast.show { opacity: 1; }
.toast.success { background: #28a745; }
.toast.error { background: #dc3545; }
.magnet-link {
word-break: break-all;
font-size: 11px;
color: #666;
margin-top: 8px;
padding: 6px 8px;
background: #f8f9fa;
border-radius: 4px;
display: none;
}
.magnet-link.visible { display: block; }
</style>
</head>
<body>
<div class="container">
<a href="/" class="back-link">← Назад к поиску</a>
<h1>🔍 Торренты для: "{{ movie_title }}"</h1>
<div class="movie-info">
<strong>Фильм:</strong> {{ movie_title }}
{% if year %}
<br><strong>Год:</strong> {{ year }}
{% endif %}
</div>
{% if torrents %}
<div class="torrents-list">
{% for torrent in torrents %}
<div class="torrent-item">
<div class="torrent-title">{{ torrent.title }}</div>
<div class="torrent-details">
<div class="detail-item">
<div class="detail-label">Размер</div>
<div class="detail-value">{{ torrent.size_readable }}</div>
</div>
<div class="detail-item">
<div class="detail-label">Разрешение</div>
<div class="detail-value resolution-{{ torrent.resolution }}">{{ torrent.resolution }}</div>
</div>
<div class="detail-item">
<div class="detail-label">Качество</div>
<div class="detail-value">
<span class="quality-badge quality-{{ torrent.quality.lower() }}">{{ torrent.quality }}</span>
</div>
</div>
<div class="detail-item">
<div class="detail-label">Сиды</div>
<div class="detail-value">{{ torrent.seeds }}</div>
</div>
<div class="detail-item">
<div class="detail-label">Пиры</div>
<div class="detail-value">{{ torrent.peers }}</div>
</div>
{% if torrent.category %}
<div class="detail-item">
<div class="detail-label">Категория</div>
<div class="detail-value">{{ torrent.category }}</div>
</div>
{% endif %}
{% if torrent.date %}
<div class="detail-item">
<div class="detail-label">Дата</div>
<div class="detail-value">{{ torrent.date }}</div>
</div>
{% endif %}
{% if torrent.provider %}
<div class="detail-item">
<div class="detail-label">Провайдер</div>
<div class="detail-value">{{ torrent.provider }}</div>
</div>
{% endif %}
</div>
<div class="torrent-actions">
{% if torrent.magnet and torrent.magnet.startswith('magnet:') %}
<a href="{{ torrent.magnet }}" class="btn btn-primary" title="Открыть magnet-ссылку">🧲 Magnet</a>
<button onclick="copyMagnet('{{ torrent.magnet|e }}', this)" class="btn btn-info" title="Копировать magnet-ссылку">📋 Копировать</button>
{% else %}
<button class="btn btn-primary" disabled title="Magnet-ссылка недоступна">🧲 Magnet</button>
{% endif %}
{% if torrent.download_url and torrent.download_url.startswith('http') %}
<a href="/api/proxy-torrent-download?url={{ torrent.download_url|urlencode }}" class="btn btn-success" target="_blank">💾 Скачать .torrent</a>
{% else %}
<button class="btn btn-success" disabled title=".torrent файл недоступен">💾 Скачать .torrent</button>
{% endif %}
{% if torrent.magnet and torrent.magnet.startswith('magnet:') %}
<button onclick="addToTorrentClient('{{ torrent.id }}', '{{ torrent.magnet|e }}', '{{ torrent.title|e }}')" class="btn btn-secondary"> Добавить в клиент</button>
{% else %}
<button class="btn btn-secondary" disabled title="Нет magnet-ссылки для добавления"> Добавить в клиент</button>
{% endif %}
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="no-torrents">
Торренты не найдены. Попробуйте изменить поисковый запрос.
</div>
{% endif %}
</div>
<div id="toast" class="toast"></div>
<script>
function showToast(message, type) {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.className = 'toast ' + (type || '');
toast.classList.add('show');
setTimeout(() => toast.classList.remove('show'), 3000);
}
function copyMagnet(magnet, button) {
if (!magnet || !magnet.startsWith('magnet:')) {
showToast('❌ Magnet-ссылка недоступна', 'error');
return;
}
// Пробуем Clipboard API
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(magnet).then(() => {
const originalText = button.textContent;
button.textContent = '✅ Скопировано';
setTimeout(() => button.textContent = originalText, 2000);
showToast('✅ Magnet-ссылка скопирована в буфер', 'success');
}).catch(() => {
fallbackCopyMagnet(magnet, button);
});
} else {
fallbackCopyMagnet(magnet, button);
}
}
function fallbackCopyMagnet(magnet, button) {
const textarea = document.createElement('textarea');
textarea.value = magnet;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
const originalText = button.textContent;
button.textContent = '✅ Скопировано';
setTimeout(() => button.textContent = originalText, 2000);
showToast('✅ Magnet-ссылка скопирована в буфер', 'success');
} catch (e) {
showToast('❌ Не удалось скопировать. Выделите ссылку вручную.', 'error');
}
document.body.removeChild(textarea);
}
function addToTorrentClient(torrentId, magnet, title) {
const button = event.target;
const originalText = button.textContent;
button.textContent = '⏳ Добавляем...';
button.disabled = true;
const formData = new FormData();
formData.append('torrent_id', torrentId);
formData.append('magnet', magnet || '');
formData.append('torrent_title', title || '');
fetch('/api/add-torrent', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
showToast('✅ ' + data.message, 'success');
button.textContent = '✅ Добавлено';
button.style.backgroundColor = '#28a745';
} else {
showToast('❌ ' + data.message, 'error');
button.textContent = originalText;
button.disabled = false;
}
})
.catch(error => {
showToast('❌ Ошибка при добавлении торрента: ' + error, 'error');
button.textContent = originalText;
button.disabled = false;
});
}
</script>
</body>
</html>