New file selection page

This commit is contained in:
Juned KH 2024-12-04 14:01:12 +05:30
parent b58b633862
commit b58f7404f1
5 changed files with 967 additions and 727 deletions

1
.dockerignore Normal file
View File

@ -0,0 +1 @@
mltbenv/*

View File

@ -55,16 +55,14 @@ def create_help_buttons():
def bt_selection_buttons(id_): def bt_selection_buttons(id_):
gid = id_[:12] if len(id_) > 25 else id_ gid = id_[:12] if len(id_) > 25 else id_
pincode = "".join([n for n in id_ if n.isdigit()][:4]) pin = "".join([n for n in id_ if n.isdigit()][:4])
buttons = ButtonMaker() buttons = ButtonMaker()
BASE_URL = config_dict["BASE_URL"] BASE_URL = config_dict["BASE_URL"]
if config_dict["WEB_PINCODE"]: if config_dict["WEB_PINCODE"]:
buttons.url_button("Select Files", f"{BASE_URL}/app/files/{id_}") buttons.url_button("Select Files", f"{BASE_URL}/app/files?gid={id_}")
buttons.data_button("Pincode", f"sel pin {gid} {pincode}") buttons.data_button("Pincode", f"sel pin {gid} {pin}")
else: else:
buttons.url_button( buttons.url_button("Select Files", f"{BASE_URL}/app/files?gid={id_}&pin={pin}")
"Select Files", f"{BASE_URL}/app/files/{id_}?pin_code={pincode}"
)
buttons.data_button("Done Selecting", f"sel done {gid} {id_}") buttons.data_button("Done Selecting", f"sel done {gid} {id_}")
buttons.data_button("Cancel", f"sel cancel {gid}") buttons.data_button("Cancel", f"sel cancel {gid}")
return buttons.build_menu(2) return buttons.build_menu(2)

View File

@ -118,7 +118,7 @@ def make_tree(res, tool=False):
folders[-1], folders[-1],
is_file=True, is_file=True,
parent=previous_node, parent=previous_node,
size=i["length"], size=float(i["length"]),
priority=priority, priority=priority,
file_id=i["index"], file_id=i["index"],
progress=round( progress=round(
@ -130,7 +130,7 @@ def make_tree(res, tool=False):
folders[-1], folders[-1],
is_file=True, is_file=True,
parent=parent, parent=parent,
size=i["length"], size=float(i["length"]),
priority=priority, priority=priority,
file_id=i["index"], file_id=i["index"],
progress=round( progress=round(
@ -155,14 +155,14 @@ def create_list(parent, contents=None):
contents = [] contents = []
for i in parent.children: for i in parent.children:
if i.is_folder: if i.is_folder:
childrens = [] children = []
create_list(i, childrens) create_list(i, children)
contents.append( contents.append(
{ {
"id": f"folderNode_{i.file_id}", "id": f"folderNode_{i.file_id}",
"name": i.name, "name": i.name,
"type": "folder", "type": "folder",
"children": childrens, "children": children,
} }
) )
else: else:
@ -177,3 +177,19 @@ def create_list(parent, contents=None):
} }
) )
return contents return contents
def extract_file_ids(data):
selected_files = []
unselected_files = []
for item in data:
if item.get("type") == "file":
if item.get("selected"):
selected_files.append(str(item["id"]))
else:
unselected_files.append(str(item["id"]))
if item.get("children"):
child_selected, child_unselected = extract_file_ids(item["children"])
selected_files.extend(child_selected)
unselected_files.extend(child_unselected)
return selected_files, unselected_files

834
web/templates/page.html Normal file
View File

@ -0,0 +1,834 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Torrent Selector</title>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" rel="stylesheet">
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
body {
font-family: 'Inter', sans-serif;
transition: background-color 0.5s, color 0.5s;
display: flex;
flex-direction: column;
min-height: 100vh;
}
.dark {
background-color: hsl(224 71% 4%);
color: hsl(213 31% 91%);
}
.light {
background-color: hsl(0 0% 100%);
color: hsl(224 71% 4%);
}
.card {
background-color: hsl(224 71% 4%);
border-radius: 0.5rem;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
}
.dark .card {
background-color: hsl(224 71% 8%);
}
.light .card {
background-color: hsl(0 0% 100%);
color: hsl(224 71% 4%);
}
.btn {
padding: 0.5rem 1rem;
border-radius: 0.375rem;
font-weight: 500;
cursor: pointer;
display: inline-flex;
align-items: center;
transition: all 0.2s;
}
.btn:hover {
opacity: 0.9;
}
.btn-primary {
background-color: hsl(217.2 91.2% 59.8%);
color: hsl(0 0% 100%);
}
.btn-secondary {
background-color: hsl(215 20.2% 65.1%);
color: hsl(0 0% 100%);
}
.switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: hsl(215 20.2% 65.1%);
transition: .4s;
border-radius: 34px;
}
.slider:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked+.slider {
background-color: hsl(217.2 91.2% 59.8%);
}
input:checked+.slider:before {
transform: translateX(26px);
}
.file-tree-item {
display: flex;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid hsla(0, 0%, 100%, 0.1);
transition: background-color 0.3s ease;
}
.file-tree-item:hover {
background-color: hsla(0, 0%, 100%, 0.1);
}
.dark .file-tree-item:hover {
background-color: hsla(0, 0%, 0%, 0.1);
}
.icon {
margin-right: 8px;
font-size: 1.2em;
}
.folder {
cursor: pointer;
font-weight: 600;
}
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
}
.modal-content {
background-color: hsl(0 0% 100%);
color: hsl(224 71% 4%);
margin: 15% auto;
padding: 20px;
border-radius: 5px;
width: 90%;
max-width: 500px;
}
.dark .modal-content {
background-color: hsl(224 71% 4%);
color: hsl(213 31% 91%);
}
@keyframes day-night {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.day-night-animation {
animation: day-night 1s linear;
}
.edit-name-input {
background-color: transparent;
border: none;
border-bottom: 1px solid hsl(217.2 91.2% 59.8%);
color: inherit;
font-size: inherit;
padding: 2px 4px;
margin-right: 8px;
width: 100%;
}
.edit-name-input:focus {
outline: none;
border-bottom: 2px solid hsl(217.2 91.2% 59.8%);
}
.pin-entry {
animation: fadeIn 0.5s;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.pin-entry.fadeOut {
animation: fadeOut 0.5s;
}
@keyframes fadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
.size-info {
font-size: 0.8em;
color: hsl(215 20.2% 65.1%);
}
.dark .size-info {
color: hsl(213 31% 80%);
}
.light input[type="text"],
.light input[type="password"] {
background-color: hsl(0 0% 95%);
color: hsl(224 71% 4%);
}
header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 2rem 0;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
footer {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 2rem 0;
box-shadow: 0 -4px 6px rgba(0, 0, 0, 0.1);
margin-top: auto;
}
.social-button {
display: inline-flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.2);
color: white;
font-size: 1.2rem;
transition: all 0.3s ease;
}
.social-button:hover {
background-color: rgba(255, 255, 255, 0.3);
transform: scale(1.1);
}
.edit-buttons {
display: flex;
gap: 4px;
}
.edit-buttons button {
padding: 2px 4px;
font-size: 0.8em;
}
@media (max-width: 640px) {
.footer-content {
flex-direction: column-reverse;
align-items: center;
}
.footer-buttons {
margin-bottom: 1rem;
}
.file-tree-item .file-name,
.file-tree-item .folder-name {
font-size: 0.9em;
}
}
#pinEntry {
max-width: 300px;
margin: 0 auto;
}
#pinEntry input[type="password"] {
width: 100%;
margin-bottom: 1rem;
}
#pinEntry button {
width: 100%;
}
.file-info {
display: flex;
flex-direction: column;
flex-grow: 1;
}
.file-info-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.edit-btn {
background: none;
border: none;
cursor: pointer;
font-size: 0.9em;
color: inherit;
padding: 0;
margin-left: 8px;
}
.edit-btn:hover {
text-decoration: underline;
}
.checkbox-wrapper {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
}
</style>
</head>
<body class="dark">
<header>
<div class="container mx-auto px-4">
<div class="flex flex-col sm:flex-row justify-between items-center">
<h1 class="text-3xl font-bold text-white mb-4 sm:mb-0">Torrent
file selector</h1>
<div class="flex items-center space-x-4">
<div class="flex items-center">
<span id="themeIcon" class="mr-2 text-white">🌙</span>
<label class="switch">
<input type="checkbox" id="themeToggle" checked>
<span class="slider"></span>
</label>
</div>
</div>
</div>
</div>
</header>
<main class="flex-grow container mx-auto p-4">
<div id="pinEntry" class="card p-6 mb-4 pin-entry">
<h2 class="text-xl font-bold mb-4">Enter PIN</h2>
<input type="text" id="pinInput"
title="Enter the code that you have got from Telegram to access the Torrent"
class="bg-gray-700 text-white p-2 rounded mb-4 w-full" placeholder="Enter PIN">
<button id="submitPin" class="btn btn-primary">Submit</button>
</div>
<div id="fileManager" class="card p-6 hidden">
<div class="flex justify-between items-center mb-4">
<button id="selectAllBtn" class="btn btn-primary">Select
All</button>
<button id="submitBtn" class="btn btn-primary">Submit</button>
</div>
<div class="mb-4">
<p>Selected files: <span id="selectedCount">0</span> / <span id="totalCount">0</span></p>
<p>Total size: <span id="selectedSize">0 B</span> / <span id="totalSize">0 B</span></p>
</div>
<div id="fileTree" class="mb-4"></div>
</div>
</main>
<footer>
<div class="container mx-auto px-4">
<div class="footer-content flex flex-col sm:flex-row justify-between items-center">
<p class="text-white mb-4 sm:mb-0">&copy; 2024 Torrent file selector.
All rights reserved.</p>
<div class="footer-buttons flex space-x-4">
<a href="https://github.com/anasty17/mirror-leech-telegram-bot" target="_blank"
class="social-button">
<i class="fab fa-github"></i>
</a>
<a href="https://www.instagram.com" target="_blank" class="social-button">
<i class="fab fa-instagram"></i>
</a>
</div>
</div>
</div>
</footer>
<div id="reusableModal" class="modal">
<div class="modal-content">
<h2 id="modalTitle" class="text-xl font-bold mb-4"></h2>
<div id="modalBody"></div>
<div id="modalFooter" class="mt-4"></div>
</div>
</div>
<script>
const themeToggle = document.getElementById('themeToggle');
const themeIcon = document.getElementById('themeIcon');
const body = document.body;
const fileTree = document.getElementById('fileTree');
const selectedCount = document.getElementById('selectedCount');
const totalCount = document.getElementById('totalCount');
const selectedSize = document.getElementById('selectedSize');
const totalSize = document.getElementById('totalSize');
const selectAllBtn = document.getElementById('selectAllBtn');
const submitBtn = document.getElementById('submitBtn');
const pinEntry = document.getElementById('pinEntry');
const pinInput = document.getElementById('pinInput');
const submitPin = document.getElementById('submitPin');
const fileManager = document.getElementById('fileManager');
const reusableModal = document.getElementById('reusableModal');
const modalTitle = document.getElementById('modalTitle');
const modalBody = document.getElementById('modalBody');
const modalFooter = document.getElementById('modalFooter');
pinInput.focus();
const urlParams = new Proxy(new URLSearchParams(window.location.search), {
get: (searchParams, prop) => searchParams.get(prop),
});
if (urlParams.pin) {
pinInput.value = urlParams.pin
setTimeout(() => submitPin.click(), 0);
}
let currentFolder = null;
let files = [];
let allowEdit = false;
function loadThemePreference() {
const savedTheme = localStorage.getItem('darkMode');
if (savedTheme !== null) {
const isDark = savedTheme === 'true';
body.classList.toggle('dark', isDark);
body.classList.toggle('light', !isDark);
themeToggle.checked = isDark;
themeIcon.textContent = isDark ? '☀️' : '🌙';
}
}
function toggleTheme() {
body.classList.toggle('dark');
body.classList.toggle('light');
const isDark = body.classList.contains('dark');
themeIcon.textContent = isDark ? '☀️' : '🌙';
themeIcon.classList.add('day-night-animation');
setTimeout(() => themeIcon.classList.remove('day-night-animation'), 1000);
localStorage.setItem('darkMode', isDark);
}
function formatSize(size) {
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
let i = 0;
while (size >= 1024 && i < units.length - 1) {
size /= 1024;
i++;
}
return `${size.toFixed(2)} ${units[i]}`;
}
function calculateFolderSize(folder) {
let totalSize = 0;
const queue = [folder];
while (queue.length > 0) {
const node = queue.pop();
if (node.type === 'file') {
totalSize += node.size;
} else if (node.children) {
queue.push(...node.children);
}
}
return totalSize;
}
function renderFileTree(nodes) {
fileTree.innerHTML = '';
if (currentFolder) {
const backButton = document.createElement('div');
backButton.className = 'file-tree-item folder';
backButton.innerHTML = '<span class="icon">📁</span>...';
backButton.addEventListener('click', goBack);
fileTree.appendChild(backButton);
}
nodes.forEach(node => {
const div = document.createElement('div');
div.className = 'file-tree-item';
const checkboxWrapper = document.createElement('div');
checkboxWrapper.className = 'checkbox-wrapper';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.id = node.id;
checkbox.checked = node.selected;
checkbox.addEventListener('change', () => toggleFile(node));
checkboxWrapper.appendChild(checkbox);
const icon = document.createElement('span');
icon.className = 'icon';
icon.textContent = node.type === 'folder' ? '📁' : '📄';
const fileInfo = document.createElement('div');
fileInfo.className = 'file-info';
const nameElement = document.createElement('div');
nameElement.className = 'file-name cursor-pointer';
nameElement.textContent = node.name;
const sizeInfo = document.createElement('div');
sizeInfo.className = 'size-info';
const size = node.type === 'folder' ? calculateFolderSize(node) : node.size;
if (node.type === 'folder') {
sizeInfo.textContent = `${formatSize(size)}`;
} else {
sizeInfo.textContent = `${formatSize(size)}`;
if (allowEdit) {
const editBtn = document.createElement('span');
editBtn.textContent = ' | Edit ✏️';
editBtn.className = 'edit-btn';
sizeInfo.appendChild(editBtn);
}
if (node.progress !== undefined) {
const progressText = document.createElement('span');
progressText.textContent = ` | Progress: ${node.progress}%`;
sizeInfo.appendChild(progressText);
}
}
div.addEventListener('click', (event) => {
if (event.target.className === 'file-name cursor-pointer') {
toggleFile(node)
} else if (event.target.className === 'edit-btn') {
openEditFileNameModal(node)
}
})
fileInfo.appendChild(nameElement);
fileInfo.appendChild(sizeInfo);
div.appendChild(checkboxWrapper);
div.appendChild(icon);
div.appendChild(fileInfo);
if (node.type === 'folder') {
nameElement.classList.add('folder', 'folder-name');
nameElement.addEventListener('click', (e) => {
e.preventDefault();
openFolder(node);
});
if (areAllChildrenSelected(node)) {
checkbox.checked = true;
} else if (areSomeChildrenSelected(node)) {
checkbox.indeterminate = true;
}
}
fileTree.appendChild(div);
});
}
function areAllChildrenSelected(folder) {
return folder.children.every(child => child.type === 'folder' ? areAllChildrenSelected(child) : child.selected);
}
function areSomeChildrenSelected(folder) {
return folder.children.some(child => child.type === 'folder' ? areSomeChildrenSelected(child) : child.selected);
}
function toggleFile(node) {
if (node.type === 'folder') {
const isSelected = !areAllChildrenSelected(node);
toggleFolder(node, isSelected);
} else {
node.selected = !node.selected;
}
updateParentFolders(node);
updateStats();
renderFileTree(currentFolder ? currentFolder.children : files);
}
function toggleFolder(folder, isSelected) {
folder.selected = isSelected;
const queue = [folder];
while (queue.length > 0) {
const node = queue.pop();
if (node.type === 'file') {
node.selected = isSelected;
} else if (node.children) {
node.selected = isSelected;
queue.push(...node.children);
}
}
}
function updateParentFolders(node) {
let parent = findParent(files[0], node.id);
while (parent) {
parent.selected = areAllChildrenSelected(parent);
parent = findParent(files[0], parent.id);
}
}
function findParent(root, id) {
if (root.children) {
for (const child of root.children) {
if (child.id === id) {
return root;
}
const found = findParent(child, id);
if (found) return found;
}
}
return null;
}
function updateStats() {
const stats = calculateStats(files);
selectedCount.textContent = stats.selectedCount;
totalCount.textContent = stats.totalCount;
selectedSize.textContent = formatSize(stats.selectedSize);
totalSize.textContent = formatSize(stats.totalSize);
}
function calculateStats(nodes) {
let selectedCount = 0;
let totalCount = 0;
let selectedSize = 0;
let totalSize = 0;
const queue = [...nodes];
while (queue.length > 0) {
const node = queue.pop();
if (node.type === 'file') {
totalCount++;
totalSize += node.size;
if (node.selected) {
selectedCount++;
selectedSize += node.size;
}
}
if (node.children) {
queue.push(...node.children);
}
}
return { selectedCount, totalCount, selectedSize, totalSize };
}
function openFolder(folder) {
currentFolder = folder;
renderFileTree(folder.children);
updateStats();
}
function goBack() {
if (currentFolder) {
const parent = findParent(files[0], currentFolder.id);
if (parent) {
currentFolder = parent;
renderFileTree(parent.children);
} else {
currentFolder = null;
renderFileTree(files);
}
updateStats();
}
}
function selectAll() {
const nodes = currentFolder ? currentFolder.children : files;
nodes.forEach(node => {
if (node.type === 'folder') {
toggleFolder(node, true);
} else {
node.selected = true;
}
});
updateStats();
renderFileTree(nodes);
}
function openEditFileNameModal(node) {
modalTitle.textContent = 'Edit File Name';
modalBody.innerHTML = `
<input type="text" id="editNameInput" class="edit-name-input" value="${node.name}">
`;
modalFooter.innerHTML = `
<button id="saveNameBtn" class="btn btn-primary">Save</button>
<button class="btn btn-secondary" onClick="closeModal()">Cancel</button>
`;
const saveNameBtn = document.getElementById('saveNameBtn');
const editNameInput = document.getElementById('editNameInput');
saveNameBtn.addEventListener('click', () => {
const newName = editNameInput.value.trim();
if (newName && newName !== node.name) {
node.name = newName;
renderFileTree(currentFolder ? currentFolder.children : files);
}
closeModal();
});
editNameInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
saveNameBtn.click();
}
});
openModal();
editNameInput.setSelectionRange(editNameInput.value.length, editNameInput.value.length)
editNameInput.focus();
}
function openModal() {
reusableModal.style.display = 'block';
}
function closeModal() {
reusableModal.style.display = 'none';
}
function submitData() {
if (parseInt(selectedCount.innerText) === 0) {
modalTitle.textContent = 'Error';
modalBody.innerHTML = '<p>No files selected.</p>';
modalFooter.innerHTML = '<button class="btn btn-primary" onclick="closeModal()">Okay</button>';
openModal();
return;
}
modalTitle.textContent = 'Processing...';
modalBody.innerHTML = `<p>Submitting your selection ${selectedCount.innerText} ... </p>`;
modalFooter.innerHTML = '';
openModal();
const requestUrl = `/app/files/torrent?gid=${urlParams.gid}&pin=${pinInput.value}`;
fetch(requestUrl, { 'method': 'POST', 'body': JSON.stringify(files) }).then(response => {
if (reusableModal.style.display === 'block') {
closeModal();
}
if (response.ok) {
modalTitle.textContent = 'Success!';
modalBody.innerHTML = '<p>Your selection has been submitted successfully.</p>';
} else {
modalTitle.textContent = 'Error';
modalBody.innerHTML = '<p>There was an error submitting your selection. Please try again.</p>';
}
modalFooter.innerHTML = '<button class="btn btn-primary" onclick="closeModal()">Okay</button>';
openModal();
});
}
pinInput.addEventListener('keypress', ('keypress', (e) => {
if (e.key === 'Enter') {
submitPin.click();
}
}));
submitPin.addEventListener('click', () => {
pinInput.blur();
if (pinInput.value === '') {
modalTitle.textContent = 'Missing Pin';
modalBody.innerHTML = `<p>The <code>pin</code> is missing.</p>`;
modalFooter.innerHTML = '<button class="btn btn-primary" onClick="closeModal()">Try Again</button>';
openModal();
return false;
}
if (urlParams.gid == null || urlParams.gid === '') {
modalTitle.textContent = 'Missing parameter';
modalBody.innerHTML = `<p>The parameter <code>gid</code> is missing.</p>`;
modalFooter.innerHTML = '<button class="btn btn-primary" onClick="closeModal()">Try Again</button>';
openModal();
return false;
}
const requestUrl = `/app/files/torrent?gid=${urlParams.gid}&pin=${pinInput.value}`;
fetch(requestUrl).then(function (response) {
if (response.ok) {
return response.json().then(data => {
if (data.error) {
modalTitle.textContent = data.error;
modalBody.innerHTML = `<p>${data.message}. Please try again.</p>`;
modalFooter.innerHTML = '<button class="btn btn-primary" onclick="closeModal()">Retry</button>';
openModal();
} else {
files = data.files;
allowEdit = data.engine === "qbittorrent";
pinEntry.classList.add('fadeOut');
setTimeout(() => {
pinEntry.style.display = 'none';
fileManager.classList.remove('hidden');
renderFileTree(files);
updateStats();
}, 500)
}
});
} else {
modalTitle.textContent = 'Something Went Wrong';
modalBody.innerHTML = '<p>Please check console. Status Code: ' + response.status + '</p>';
modalFooter.innerHTML = '<button class="btn btn-primary" onclick="closeModal()">Retry</button>';
openModal();
}
}).catch(error => {
modalTitle.textContent = 'Server Error';
modalBody.innerHTML = '<p>There was an error connecting to the server. Please try again.</p><br>' + error.message;
modalFooter.innerHTML = '<button class="btn btn-primary" onclick="closeModal()">Retry</button>';
openModal();
});
});
themeToggle.addEventListener('change', toggleTheme);
selectAllBtn.addEventListener('click', selectAll);
submitBtn.addEventListener('click', submitData);
reusableModal.addEventListener('click', (event) => {
if (event.target === reusableModal) {
event.stopPropagation();
}
});
loadThemePreference();
</script>
</body>
</html>

View File

@ -1,11 +1,10 @@
from aria2p import API as ariaAPI, Client as ariaClient from aria2p import API as ariaAPI, Client as ariaClient
from flask import Flask, request from flask import Flask, request, render_template, jsonify
from logging import getLogger, FileHandler, StreamHandler, INFO, basicConfig from logging import getLogger, FileHandler, StreamHandler, INFO, basicConfig
from qbittorrentapi import NotFound404Error, Client as qbClient from qbittorrentapi import NotFound404Error, Client as qbClient
from time import sleep from time import sleep
from json import dumps
from web.nodes import make_tree from web.nodes import extract_file_ids, make_tree
app = Flask(__name__) app = Flask(__name__)
@ -27,640 +26,8 @@ basicConfig(
LOGGER = getLogger(__name__) LOGGER = getLogger(__name__)
page = """
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Torrent File Selector</title>
<link rel="icon" href="https://telegra.ph/file/43af672249c94053356c7.jpg" type="image/jpg">
<script
src="https://code.jquery.com/jquery-3.5.1.slim.min.js"
integrity="sha256-4+XzXVhsDmqanXGHaHvgh1gMQKX40OUvDEBTu8JcmNs="
crossorigin="anonymous"
></script>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Ubuntu:ital,wght@0,300;0,400;0,500;0,700;1,300;1,400;1,500;1,700&display=swap"
rel="stylesheet"
/>
<link
rel="stylesheet"
href="https://pro.fontawesome.com/releases/v5.10.0/css/all.css"
integrity="sha384-AYmEC3Yw5cVb3ZcuHtOA93w35dYTsvhLPVnYs9eStHfGJvOvKxVfELGroGkvsg+p"
crossorigin="anonymous"
/>
<style>
*{ def re_verify(paused, resumed, hash_id):
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: "Ubuntu", sans-serif;
list-style: none;
text-decoration: none;
outline: none !important;
color: white;
}
body{
background-color: #0D1117;
}
header{
margin: 3vh 1vw;
padding: 0.5rem 1rem 0.5rem 1rem;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: #161B22;
border-radius: 30px;
background-color: #161B22;
border: 2px solid rgba(255, 255, 255, 0.11);
}
header:hover, section:hover{
box-shadow: 0px 0px 15px black;
}
.brand{
display: flex;
align-items: center;
}
img{
width: 2.5rem;
height: 2.5rem;
border: 2px solid black;
border-radius: 50%;
}
.name{
margin-left: 1vw;
font-size: 1.5rem;
}
.intro{
text-align: center;
margin-bottom: 2vh;
margin-top: 1vh;
}
.social a{
font-size: 1.5rem;
padding-left: 1vw;
}
.social a:hover, .brand:hover{
filter: invert(0.3);
}
section{
margin: 0vh 1vw;
margin-bottom: 10vh;
padding: 1vh 3vw;
display: flex;
flex-direction: column;
border: 2px solid rgba(255, 255, 255, 0.11);
border-radius: 20px;
background-color: #161B22 ;
}
li:nth-child(1){
padding: 1rem 1rem 0.5rem 1rem;
}
li:nth-child(n+1){
padding-left: 1rem;
}
li label{
padding-left: 0.5rem;
}
li{
padding-bottom: 0.5rem;
}
span{
margin-right: 0.5rem;
cursor: pointer;
user-select: none;
transition: transform 200ms ease-out;
}
span.active{
transform: rotate(90deg);
-ms-transform: rotate(90deg); /* for IE */
-webkit-transform: rotate(90deg);/* for browsers supporting webkit (such as chrome, firefox, safari etc.). */
display: inline-block;
}
ul{
margin: 1vh 1vw 1vh 1vw;
padding: 0 0 0.5rem 0;
border: 2px solid black;
border-radius: 20px;
background-color: #1c2129;
overflow: hidden;
}
input[type="checkbox"]{
cursor: pointer;
user-select: none;
}
input[type="submit"] {
border-radius: 20px;
margin: 2vh auto 1vh auto;
width: 50%;
display: block;
height: 5.5vh;
border: 2px solid rgba(255, 255, 255, 0.11);
background-color: #0D1117;
font-size: 16px;
font-weight: 500;
}
input[type="submit"]:hover, input[type="submit"]:focus{
background-color: rgba(255, 255, 255, 0.068);
cursor: pointer;
}
@media (max-width: 768px){
input[type="submit"]{
width: 100%;
}
}
#treeview .parent {
position: relative;
}
#treeview .parent > ul {
display: none;
}
#sticks {
margin: 0vh 1vw;
margin-bottom: 1vh;
padding: 1vh 3vw;
display: flex;
flex-direction: column;
border: 2px solid rgba(255, 255, 255, 0.11);
border-radius: 20px;
background-color: #161b22;
align-items: center;
}
#sticks.stick {
position: sticky;
top: 0;
z-index: 10000;
}
</style>
<script>
function s_validate() {
if ($("input[name^='filenode_']:checked").length == 0) {
alert("Select one file at least!");
return false;
}
}
</script>
</head>
<body>
<!--© Designed and coded by @bipuldey19-Telegram-->
<header>
<div class="brand">
<img
src="https://telegra.ph/file/43af672249c94053356c7.jpg"
alt="logo"
/>
<a href="https://t.me/anas_tayyar">
<h2 class="name">Bittorrent Selection</h2>
</a>
</div>
<div class="social">
<a href="https://www.github.com/anasty17/mirror-leech-telegram-bot"><i class="fab fa-github"></i></a>
<a href="https://t.me/anas_tayyar"><i class="fab fa-telegram"></i></a>
</div>
</header>
<div id="sticks">
<h4>Selected files: <b id="checked_files">0</b> of <b id="total_files">0</b></h4>
<h4>Selected files size: <b id="checked_size">0</b> of <b id="total_size">0</b></h4>
</div>
<section>
<form action="{form_url}" onsubmit="return s_validate()" method="POST">
{My_content}
<input type="submit" name="Select these files ;)">
</form>
</section>
<script>
$(document).ready(function () {
docready();
var tags = $("li").filter(function () {
return $(this).find("ul").length !== 0;
});
tags.each(function () {
$(this).addClass("parent");
});
$("body").find("ul:first-child").attr("id", "treeview");
$(".parent").prepend("<span>▶</span>");
$("span").click(function (e) {
e.stopPropagation();
e.stopImmediatePropagation();
$(this).parent( ".parent" ).find(">ul").toggle("slow");
if ($(this).hasClass("active")) $(this).removeClass("active");
else $(this).addClass("active");
});
});
if(document.getElementsByTagName("ul").length >= 10){
var labels = document.querySelectorAll("label");
//Shorting the file/folder names
labels.forEach(function (label) {
if (label.innerText.toString().split(" ").length >= 9) {
let FirstPart = label.innerText
.toString()
.split(" ")
.slice(0, 5)
.join(" ");
let SecondPart = label.innerText
.toString()
.split(" ")
.splice(-5)
.join(" ");
label.innerText = `${FirstPart}... ${SecondPart}`;
}
if (label.innerText.toString().split(".").length >= 9) {
let first = label.innerText
.toString()
.split(".")
.slice(0, 5)
.join(" ");
let second = label.innerText
.toString()
.split(".")
.splice(-5)
.join(".");
label.innerText = `${first}... ${second}`;
}
});
}
</script>
<script>
$('input[type="checkbox"]').change(function(e) {
var checked = $(this).prop("checked"),
container = $(this).parent(),
siblings = container.siblings();
/*
$(this).attr('value', function(index, attr){
return attr == 'yes' ? 'noo' : 'yes';
});
*/
container.find('input[type="checkbox"]').prop({
indeterminate: false,
checked: checked
});
function checkSiblings(el) {
var parent = el.parent().parent(),
all = true;
el.siblings().each(function() {
let returnValue = all = ($(this).children('input[type="checkbox"]').prop("checked") === checked);
return returnValue;
});
if (all && checked) {
parent.children('input[type="checkbox"]').prop({
indeterminate: false,
checked: checked
});
checkSiblings(parent);
} else if (all && !checked) {
parent.children('input[type="checkbox"]').prop("checked", checked);
parent.children('input[type="checkbox"]').prop("indeterminate", (parent.find('input[type="checkbox"]:checked').length > 0));
checkSiblings(parent);
} else {
el.parents("li").children('input[type="checkbox"]').prop({
indeterminate: true,
checked: false
});
}
}
checkSiblings(container);
});
</script>
<script>
function docready () {
$("label[for^='filenode_']").css("cursor", "pointer");
$("label[for^='filenode_']").click(function () {
$(this).prev().click();
});
checked_size();
checkingfiles();
var total_files = $("label[for^='filenode_']").length;
$("#total_files").text(total_files);
var total_size = 0;
var ffilenode = $("label[for^='filenode_']");
ffilenode.each(function () {
var size = parseFloat($(this).data("size"));
total_size += size;
$(this).append(" - " + humanFileSize(size));
});
$("#total_size").text(humanFileSize(total_size));
};
function checked_size() {
var checked_size = 0;
var checkedboxes = $("input[name^='filenode_']:checked");
checkedboxes.each(function () {
var size = parseFloat($(this).data("size"));
checked_size += size;
});
$("#checked_size").text(humanFileSize(checked_size));
}
function checkingfiles() {
var checked_files = $("input[name^='filenode_']:checked").length;
$("#checked_files").text(checked_files);
}
$("input[name^='foldernode_']").change(function () {
checkingfiles();
checked_size();
});
$("input[name^='filenode_']").change(function () {
checkingfiles();
checked_size();
});
function humanFileSize(size) {
var i = -1;
var byteUnits = [' kB', ' MB', ' GB', ' TB', 'PB', 'EB', 'ZB', 'YB'];
do {
size = size / 1024;
i++;
} while (size > 1024);
return Math.max(size, 0).toFixed(1) + byteUnits[i];
}
function sticking() {
var window_top = $(window).scrollTop();
var div_top = $('.brand').offset().top;
if (window_top > div_top) {
$('#sticks').addClass('stick');
} else {
$('#sticks').removeClass('stick');
}
}
$(function () {
$(window).scroll(sticking);
sticking();
});
</script>
</body>
</html>
"""
code_page = """
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Torrent Code Checker</title>
<link rel="icon" href="https://telegra.ph/file/43af672249c94053356c7.jpg" type="image/jpg">
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Ubuntu:ital,wght@0,300;0,400;0,500;0,700;1,300;1,400;1,500;1,700&display=swap"
rel="stylesheet"
/>
<link
rel="stylesheet"
href="https://pro.fontawesome.com/releases/v5.10.0/css/all.css"
integrity="sha384-AYmEC3Yw5cVb3ZcuHtOA93w35dYTsvhLPVnYs9eStHfGJvOvKxVfELGroGkvsg+p"
crossorigin="anonymous"
/>
<style>
*{
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: "Ubuntu", sans-serif;
list-style: none;
text-decoration: none;
color: white;
}
body{
background-color: #0D1117;
}
header{
margin: 3vh 1vw;
padding: 0.5rem 1rem 0.5rem 1rem;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: #161B22;
border-radius: 30px;
background-color: #161B22;
border: 2px solid rgba(255, 255, 255, 0.11);
}
header:hover, section:hover{
box-shadow: 0px 0px 15px black;
}
.brand{
display: flex;
align-items: center;
}
img{
width: 2.5rem;
height: 2.5rem;
border: 2px solid black;
border-radius: 50%;
}
.name{
color: white;
margin-left: 1vw;
font-size: 1.5rem;
}
.intro{
text-align: center;
margin-bottom: 2vh;
margin-top: 1vh;
}
.social a{
font-size: 1.5rem;
color: white;
padding-left: 1vw;
}
.social a:hover, .brand:hover{
filter: invert(0.3);
}
section{
margin: 0vh 1vw;
margin-bottom: 10vh;
padding: 1vh 3vw;
display: flex;
flex-direction: column;
border: 2px solid rgba(255, 255, 255, 0.11);
border-radius: 20px;
background-color: #161B22 ;
color: white;
}
section form{
display: flex;
margin-left: auto;
margin-right: auto;
flex-direction: column;
}
section div{
background-color: #0D1117;
border-radius: 20px;
max-width: fit-content;
padding: 0.7rem;
margin-top: 2vh;
}
section label{
font-size: larger;
font-weight: 500;
margin: 0 0 0.5vh 1.5vw;
display: block;
}
section input[type="text"]{
border-radius: 20px;
outline: none;
width: 50vw;
height: 4vh;
padding: 1rem;
margin: 0.5vh;
border: 2px solid rgba(255, 255, 255, 0.11);
background-color: #3e475531;
box-shadow: inset 0px 0px 10px black;
}
section input[type="text"]:focus{
border-color: rgba(255, 255, 255, 0.404);
}
section button{
border-radius: 20px;
margin-top: 1vh;
width: 100%;
height: 5.5vh;
border: 2px solid rgba(255, 255, 255, 0.11);
background-color: #0D1117;
color: white;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: background-color 200ms ease;
}
section button:hover, section button:focus{
background-color: rgba(255, 255, 255, 0.068);
}
section span{
display: block;
font-size: x-small;
margin: 1vh;
font-weight: 100;
font-style: italic;
margin-left: 23%;
margin-right: auto;
margin-bottom: 2vh;
}
@media (max-width: 768px) {
section form{
flex-direction: column;
width: 90vw;
}
section div{
max-width: 100%;
margin-bottom: 1vh;
}
section label{
margin-left: 3vw;
margin-top: 1vh;
}
section input[type="text"]{
width: calc(100% - 0.3rem);
}
section button{
width: 100%;
height: 5vh;
display: block;
margin-left: auto;
margin-right: auto;
}
section span{
margin-left: 5%;
}
}
</style>
</head>
<body>
<!--© Designed and coded by @bipuldey19-Telegram-->
<header>
<div class="brand">
<img
src="https://telegra.ph/file/43af672249c94053356c7.jpg"
alt="logo"
/>
<a href="https://t.me/anas_tayyar">
<h2 class="name">Bittorrent Selection</h2>
</a>
</div>
<div class="social">
<a href="https://www.github.com/anasty17/mirror-leech-telegram-bot"><i class="fab fa-github"></i></a>
<a href="https://t.me/anas_tayyar"><i class="fab fa-telegram"></i></a>
</div>
</header>
<section>
<form action="{form_url}">
<div>
<label for="pin_code">Pin Code :</label>
<input
type="text"
name="pin_code"
placeholder="Enter the code that you have got from Telegram to access the Torrent"
/>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
<span
>* Dont mess around. Your download will get messed up.</
>
</section>
</body>
</html>
"""
def re_verfiy(paused, resumed, hash_id):
paused = paused.strip() paused = paused.strip()
resumed = resumed.strip() resumed = resumed.strip()
if paused: if paused:
@ -706,60 +73,90 @@ def re_verfiy(paused, resumed, hash_id):
return True return True
@app.route("/app/files/<string:id_>", methods=["GET"]) @app.route("/app/files")
def list_torrent_contents(id_): def files():
if "pin_code" not in request.args.keys(): return render_template("page.html")
return code_page.replace("{form_url}", f"/app/files/{id_}")
pincode = ""
for nbr in id_:
if nbr.isdigit():
pincode += str(nbr)
if len(pincode) == 4:
break
if request.args["pin_code"] != pincode:
return "<h1>Incorrect pin code</h1>"
if len(id_) > 20: @app.route("/app/files/torrent", methods=["GET", "POST"])
res = qbittorrent_client.torrents_files(torrent_hash=id_) def handle_torrent():
cont = make_tree(res, "qbittorrent") if not (gid := request.args.get("gid")):
else: return jsonify(
res = aria2.client.get_files(id_) {
cont = make_tree(res, "aria2") "files": [],
"engine": "",
try: "error": "GID is missing",
content = dumps(cont) "message": "GID not specified",
except Exception as e: }
LOGGER.error(str(e))
content = dumps({"files": [], "engine": str(e)})
return page.replace("{My_content}", content).replace(
"{form_url}", f"/app/files/{id_}?pin_code={pincode}"
) )
if not (pin := request.args.get("pin")):
@app.route("/app/files/<string:id_>", methods=["POST"]) return jsonify(
def set_priority(id_): {
data = dict(request.form) "files": [],
"engine": "",
resume = "" "error": "Pin is missing",
if len(id_) > 20: "message": "PIN not specified",
pause = "" }
for i, value in data.items(): )
if "filenode" in i: code = ""
node_no = i.split("_")[-1] for nbr in gid:
if nbr.isdigit():
if value == "on": code += str(nbr)
resume += f"{node_no}|" if len(code) == 4:
break
if code != pin:
return jsonify(
{
"files": [],
"engine": "",
"error": "Invalid pin",
"message": "The PIN you entered is incorrect",
}
)
if request.method == "POST":
data = request.get_json(cache=False, force=True)
selected_files, unselected_files = extract_file_ids(data)
if len(gid) > 20:
selected_files = "|".join(selected_files)
unselected_files = "|".join(unselected_files)
set_qbittorrent(gid, selected_files, unselected_files)
else: else:
pause += f"{node_no}|" selected_files = ",".join(selected_files)
set_aria2(gid, selected_files)
content = jsonify(
{
"files": [],
"engine": "",
"error": "",
"message": "Your selection has been submitted successfully.",
}
)
if request.method == "GET":
try:
if len(gid) > 20:
res = qbittorrent_client.torrents_files(torrent_hash=gid)
content = jsonify(make_tree(res, "qbittorrent"))
else:
res = aria2.client.get_files(gid)
content = jsonify(make_tree(res, "aria2"))
except Exception as e:
LOGGER.error(str(e))
content = jsonify(
{
"files": [],
"engine": "",
"error": "Error getting files",
"message": str(e),
}
)
return content
pause = pause.strip("|")
resume = resume.strip("|")
def set_qbittorrent(gid, selected_files, unselected_files):
try: try:
qbittorrent_client.torrents_file_priority( qbittorrent_client.torrents_file_priority(
torrent_hash=id_, file_ids=pause, priority=0 torrent_hash=gid, file_ids=unselected_files, priority=0
) )
except NotFound404Error as e: except NotFound404Error as e:
raise NotFound404Error from e raise NotFound404Error from e
@ -767,29 +164,23 @@ def set_priority(id_):
LOGGER.error(f"{e} Errored in paused") LOGGER.error(f"{e} Errored in paused")
try: try:
qbittorrent_client.torrents_file_priority( qbittorrent_client.torrents_file_priority(
torrent_hash=id_, file_ids=resume, priority=1 torrent_hash=gid, file_ids=selected_files, priority=1
) )
except NotFound404Error as e: except NotFound404Error as e:
raise NotFound404Error from e raise NotFound404Error from e
except Exception as e: except Exception as e:
LOGGER.error(f"{e} Errored in resumed") LOGGER.error(f"{e} Errored in resumed")
sleep(1) sleep(1)
if not re_verfiy(pause, resume, id_): if not re_verify(unselected_files, selected_files, gid):
LOGGER.error(f"Verification Failed! Hash: {id_}") LOGGER.error(f"Verification Failed! Hash: {gid}")
else:
for i, value in data.items():
if "filenode" in i and value == "on":
node_no = i.split("_")[-1]
resume += f"{node_no},"
resume = resume.strip(",")
res = aria2.client.change_option(id_, {"select-file": resume}) def set_aria2(gid, selected_files):
res = aria2.client.change_option(gid, {"select-file": selected_files})
if res == "OK": if res == "OK":
LOGGER.info(f"Verified! Gid: {id_}") LOGGER.info(f"Verified! Gid: {gid}")
else: else:
LOGGER.info(f"Verification Failed! Report! Gid: {id_}") LOGGER.info(f"Verification Failed! Report! Gid: {gid}")
return list_torrent_contents(id_)
@app.route("/") @app.route("/")