mirror of
https://github.com/PR0M3TH3AN/bitvid-seeder.git
synced 2026-10-27 07:51:10 +00:00
536 lines
20 KiB
HTML
536 lines
20 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>bitvid Torrent Seeder</title>
|
|
<!-- Favicon -->
|
|
<link rel="icon" type="image/x-icon" href="assets/favicon.ico" />
|
|
<!-- Bootstrap CSS -->
|
|
<link
|
|
href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css"
|
|
rel="stylesheet"
|
|
/>
|
|
<!-- Font Awesome -->
|
|
<link
|
|
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css"
|
|
rel="stylesheet"
|
|
/>
|
|
<!-- Custom CSS -->
|
|
<link rel="stylesheet" href="style.css" />
|
|
<!-- Load nostr.js module -->
|
|
<script type="module" src="js/nostr.js"></script>
|
|
</head>
|
|
<body>
|
|
<div class="container my-4">
|
|
<!-- Updated Header with Logo and Text -->
|
|
<div class="text-center mb-4">
|
|
<img
|
|
src="assets/svg/bitvid-logo-light-mode.svg"
|
|
alt="Bitvid Logo"
|
|
class="img-fluid mb-2"
|
|
style="max-width: 200px"
|
|
/>
|
|
<h2 class="mb-0">Seeder Server</h2>
|
|
</div>
|
|
<!-- Circular Login/Profile Button -->
|
|
<div class="d-flex justify-content-end mb-4">
|
|
<button
|
|
id="loginButton"
|
|
class="btn p-0 rounded-circle me-3"
|
|
style="
|
|
width: 48px;
|
|
height: 48px;
|
|
background-color: #1e293b;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
"
|
|
>
|
|
<img
|
|
id="loginButtonAvatar"
|
|
src="assets/svg/default-profile.svg"
|
|
alt="Profile Icon"
|
|
style="width: 40px; height: 40px; border-radius: 50%"
|
|
/>
|
|
</button>
|
|
</div>
|
|
<!-- Add Torrent Form -->
|
|
<div class="card mb-4">
|
|
<div class="card-header">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<h5 class="mb-0">Add New Torrent</h5>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
<form id="addTorrentForm" class="row g-3" enctype="multipart/form-data">
|
|
<div class="col-md-9">
|
|
<input type="text" id="magnet" class="form-control" placeholder="Enter magnet link" />
|
|
<label for="videoFile" class="form-label mt-2">Or upload a video file:</label>
|
|
<input type="file" id="videoFile" class="form-control" accept="video/*" />
|
|
</div>
|
|
<div class="col-md-3 align-self-end">
|
|
<button type="submit" class="btn btn-primary w-100">Add Torrent</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<!-- Active Torrents Table -->
|
|
<div class="card mb-4">
|
|
<div class="card-header">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<h5 class="mb-0">Active Torrents</h5>
|
|
<div class="d-flex align-items-center">
|
|
<div id="paginationContainer" class="me-3">
|
|
<!-- Pagination will be inserted here -->
|
|
</div>
|
|
<button
|
|
class="btn btn-link"
|
|
data-bs-toggle="modal"
|
|
data-bs-target="#settingsModal"
|
|
>
|
|
<i class="fas fa-cog"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="table-responsive">
|
|
<table id="torrentTable" class="table table-striped">
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>Progress</th>
|
|
<th>Peers</th>
|
|
<th>Downloaded</th>
|
|
<th>Uploaded</th>
|
|
<th>Status</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="torrentList"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Settings Modal -->
|
|
<div
|
|
class="modal fade"
|
|
id="settingsModal"
|
|
tabindex="-1"
|
|
aria-labelledby="settingsModalLabel"
|
|
aria-hidden="true"
|
|
>
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content custom-modal-content">
|
|
<div class="modal-header custom-modal-header">
|
|
<h5 class="modal-title" id="settingsModalLabel">Settings</h5>
|
|
<button
|
|
type="button"
|
|
class="btn-close"
|
|
data-bs-dismiss="modal"
|
|
aria-label="Close"
|
|
></button>
|
|
</div>
|
|
<div class="modal-body custom-modal-body">
|
|
<form id="configForm">
|
|
<div class="mb-3">
|
|
<label for="storageDir" class="form-label"
|
|
>Storage Directory</label
|
|
>
|
|
<input
|
|
type="text"
|
|
id="storageDir"
|
|
class="form-control"
|
|
placeholder="Storage directory"
|
|
required
|
|
/>
|
|
</div>
|
|
<button type="submit" class="btn custom-btn-primary w-100">
|
|
Update Storage Directory
|
|
</button>
|
|
</form>
|
|
<p id="currentStorage" class="mt-3 text-center"></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Profile Modal Container -->
|
|
<div id="profileModalContainer"></div>
|
|
<!-- Helper to format bytes -->
|
|
<script>
|
|
function formatBytes(bytes) {
|
|
if (bytes === 0) return "0 B";
|
|
const k = 1024;
|
|
const sizes = ["B", "KB", "MB", "GB", "TB"];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return (bytes / Math.pow(k, i)).toFixed(2) + " " + sizes[i];
|
|
}
|
|
</script>
|
|
<!-- Main UI Script -->
|
|
<script>
|
|
window.addEventListener("nostrClientReady", () => {
|
|
// Pagination variables
|
|
let currentPage = 1;
|
|
const limit = 20;
|
|
|
|
// Profile-related functions
|
|
async function updateProfileUI(pubkey) {
|
|
const btnAvatar = document.getElementById("loginButtonAvatar");
|
|
const profileAvatar = document.getElementById("profileModalAvatar");
|
|
const profileName = document.getElementById("profileModalName");
|
|
|
|
if (!pubkey) {
|
|
btnAvatar.src = "assets/svg/default-profile.svg";
|
|
if (profileAvatar)
|
|
profileAvatar.src = "assets/svg/default-profile.svg";
|
|
if (profileName) profileName.textContent = "Not logged in";
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const events = await window.nostrClient.pool.list(
|
|
window.nostrClient.relays,
|
|
[{ kinds: [0], authors: [pubkey], limit: 1 }]
|
|
);
|
|
let displayName = pubkey;
|
|
let picture = "assets/svg/default-profile.svg";
|
|
if (events.length && events[0].content) {
|
|
const data = JSON.parse(events[0].content);
|
|
displayName = data.name || data.display_name || pubkey;
|
|
picture = data.picture || "assets/svg/default-profile.svg";
|
|
}
|
|
btnAvatar.src = picture;
|
|
if (profileAvatar) profileAvatar.src = picture;
|
|
if (profileName) profileName.textContent = displayName;
|
|
} catch (error) {
|
|
console.error("Error fetching profile data:", error);
|
|
btnAvatar.src = "assets/svg/default-profile.svg";
|
|
if (profileAvatar)
|
|
profileAvatar.src = "assets/svg/default-profile.svg";
|
|
if (profileName) profileName.textContent = pubkey;
|
|
}
|
|
}
|
|
|
|
async function login() {
|
|
try {
|
|
const pubkey = await window.nostrClient.login();
|
|
if (pubkey) {
|
|
localStorage.setItem("userPubKey", pubkey);
|
|
await updateProfileUI(pubkey);
|
|
}
|
|
} catch (error) {
|
|
console.error("Login failed:", error);
|
|
alert("Login failed. Please try again.");
|
|
}
|
|
}
|
|
|
|
function logout() {
|
|
window.nostrClient.logout();
|
|
localStorage.removeItem("userPubKey");
|
|
updateProfileUI(null);
|
|
}
|
|
|
|
async function loadProfileModal() {
|
|
try {
|
|
const response = await fetch("components/profile-modal.html");
|
|
const html = await response.text();
|
|
document.getElementById("profileModalContainer").innerHTML = html;
|
|
const closeBtn = document.getElementById("closeProfileModal");
|
|
const logoutBtn = document.getElementById("profileLogoutBtn");
|
|
if (closeBtn) {
|
|
closeBtn.addEventListener("click", () => {
|
|
toggleProfileModal(false);
|
|
});
|
|
}
|
|
if (logoutBtn) {
|
|
logoutBtn.addEventListener("click", () => {
|
|
logout();
|
|
toggleProfileModal(false);
|
|
});
|
|
}
|
|
} catch (err) {
|
|
console.error("Error loading profile modal:", err);
|
|
}
|
|
}
|
|
|
|
function toggleProfileModal(show) {
|
|
const modal = document.getElementById("profileModal");
|
|
if (!modal) return;
|
|
modal.classList.toggle("hidden", !show);
|
|
}
|
|
|
|
// Profile initialization
|
|
document
|
|
.getElementById("loginButton")
|
|
.addEventListener("click", async () => {
|
|
const userPubKey = localStorage.getItem("userPubKey");
|
|
if (!userPubKey) {
|
|
await login();
|
|
toggleProfileModal(true);
|
|
await updateProfileUI(localStorage.getItem("userPubKey"));
|
|
} else {
|
|
toggleProfileModal(true);
|
|
await updateProfileUI(userPubKey);
|
|
}
|
|
});
|
|
|
|
const savedPubKey = localStorage.getItem("userPubKey") || null;
|
|
if (savedPubKey) {
|
|
updateProfileUI(savedPubKey);
|
|
}
|
|
|
|
loadProfileModal();
|
|
|
|
// Torrent management functions
|
|
document
|
|
.getElementById("addTorrentForm")
|
|
.addEventListener("submit", async (e) => {
|
|
e.preventDefault();
|
|
if (!localStorage.getItem("userPubKey")) {
|
|
alert("Please login to add a torrent.");
|
|
return;
|
|
}
|
|
|
|
const magnet = document.getElementById("magnet").value;
|
|
const videoFile = document.getElementById("videoFile").files[0];
|
|
|
|
if (magnet && videoFile) {
|
|
alert(
|
|
"Please provide either a magnet link or a video file, not both."
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (magnet) {
|
|
// Add torrent via magnet link
|
|
await fetch("/add", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ magnet }),
|
|
});
|
|
document.getElementById("magnet").value = "";
|
|
fetchTorrents(currentPage);
|
|
} else if (videoFile) {
|
|
// Upload video file
|
|
const formData = new FormData();
|
|
formData.append("video", videoFile);
|
|
await fetch("/upload", {
|
|
method: "POST",
|
|
body: formData,
|
|
});
|
|
document.getElementById("videoFile").value = "";
|
|
fetchTorrents(currentPage);
|
|
} else {
|
|
alert("Please provide a magnet link or a video file.");
|
|
}
|
|
});
|
|
|
|
async function fetchTorrents(page) {
|
|
if (!localStorage.getItem("userPubKey")) return;
|
|
try {
|
|
const res = await fetch(`/torrents?page=${page}&limit=${limit}`);
|
|
if (!res.ok) {
|
|
throw new Error(`Server error: ${res.status} ${res.statusText}`);
|
|
}
|
|
const data = await res.json();
|
|
if (!data.torrents) {
|
|
console.error("No torrents found in response:", data);
|
|
return;
|
|
}
|
|
const { torrents, total, page: current } = data;
|
|
const list = document.getElementById("torrentList");
|
|
list.innerHTML = "";
|
|
torrents.forEach((torrent) => {
|
|
const tr = document.createElement("tr");
|
|
const tdName = document.createElement("td");
|
|
tdName.textContent = torrent.name;
|
|
tr.appendChild(tdName);
|
|
const tdProgress = document.createElement("td");
|
|
tdProgress.textContent =
|
|
(torrent.progress * 100).toFixed(2) + "%";
|
|
tr.appendChild(tdProgress);
|
|
const tdPeers = document.createElement("td");
|
|
tdPeers.textContent = torrent.numPeers;
|
|
tr.appendChild(tdPeers);
|
|
const tdDownloaded = document.createElement("td");
|
|
tdDownloaded.textContent = formatBytes(torrent.downloaded);
|
|
tr.appendChild(tdDownloaded);
|
|
const tdUploaded = document.createElement("td");
|
|
tdUploaded.textContent = formatBytes(torrent.uploaded);
|
|
tr.appendChild(tdUploaded);
|
|
const tdStatus = document.createElement("td");
|
|
tdStatus.textContent = torrent.paused ? "Paused" : "Active";
|
|
tr.appendChild(tdStatus);
|
|
const tdActions = document.createElement("td");
|
|
|
|
// Pause/Resume button
|
|
const toggleBtn = document.createElement("button");
|
|
toggleBtn.className = "btn btn-warning btn-sm me-2";
|
|
if (torrent.paused) {
|
|
toggleBtn.textContent = "Resume";
|
|
toggleBtn.onclick = async () => {
|
|
await fetch("/resume/" + torrent.infoHash, {
|
|
method: "POST",
|
|
});
|
|
fetchTorrents(currentPage);
|
|
};
|
|
} else {
|
|
toggleBtn.textContent = "Pause";
|
|
toggleBtn.onclick = async () => {
|
|
await fetch("/pause/" + torrent.infoHash, { method: "POST" });
|
|
fetchTorrents(currentPage);
|
|
};
|
|
}
|
|
tdActions.appendChild(toggleBtn);
|
|
|
|
// Remove button
|
|
const removeBtn = document.createElement("button");
|
|
removeBtn.textContent = "Remove";
|
|
removeBtn.className = "btn btn-danger btn-sm me-2";
|
|
removeBtn.onclick = async () => {
|
|
await fetch("/remove/" + torrent.infoHash, {
|
|
method: "DELETE",
|
|
});
|
|
fetchTorrents(currentPage);
|
|
};
|
|
tdActions.appendChild(removeBtn);
|
|
|
|
// Copy Magnet button
|
|
const copyMagnetBtn = document.createElement("button");
|
|
copyMagnetBtn.textContent = "Copy Magnet";
|
|
copyMagnetBtn.className = "btn btn-secondary btn-sm me-2";
|
|
copyMagnetBtn.onclick = async () => {
|
|
try {
|
|
if (torrent.magnet) {
|
|
await navigator.clipboard.writeText(torrent.magnet);
|
|
alert("Magnet link copied to clipboard!");
|
|
} else {
|
|
alert("Magnet link not found.");
|
|
}
|
|
} catch (err) {
|
|
console.error("Error copying magnet link:", err);
|
|
alert("Failed to copy magnet link.");
|
|
}
|
|
};
|
|
tdActions.appendChild(copyMagnetBtn);
|
|
|
|
// New Download button
|
|
const downloadBtn = document.createElement("button");
|
|
downloadBtn.textContent = "Download";
|
|
downloadBtn.className = "btn btn-primary btn-sm";
|
|
downloadBtn.onclick = () => {
|
|
window.location.href = `/download/${torrent.infoHash}`;
|
|
};
|
|
tdActions.appendChild(downloadBtn);
|
|
|
|
tr.appendChild(tdActions);
|
|
list.appendChild(tr);
|
|
});
|
|
updatePaginationControls(total, current);
|
|
} catch (error) {
|
|
console.error("Error fetching torrents:", error);
|
|
}
|
|
}
|
|
|
|
function updatePaginationControls(total, page) {
|
|
const totalPages = Math.ceil(total / limit);
|
|
const pagination = document.createElement("div");
|
|
pagination.className = "d-flex justify-content-center mt-3";
|
|
|
|
// Previous button
|
|
const prevBtn = document.createElement("button");
|
|
prevBtn.className = "btn btn-secondary me-2";
|
|
prevBtn.textContent = "Previous";
|
|
prevBtn.disabled = page === 1;
|
|
prevBtn.onclick = () => {
|
|
if (page > 1) {
|
|
currentPage = page - 1;
|
|
fetchTorrents(currentPage);
|
|
}
|
|
};
|
|
pagination.appendChild(prevBtn);
|
|
|
|
// Page number buttons
|
|
for (let i = 1; i <= totalPages; i++) {
|
|
const pageBtn = document.createElement("button");
|
|
pageBtn.className =
|
|
i === page ? "btn btn-primary me-1" : "btn btn-secondary me-1";
|
|
pageBtn.textContent = i;
|
|
pageBtn.onclick = () => {
|
|
currentPage = i;
|
|
fetchTorrents(currentPage);
|
|
};
|
|
pagination.appendChild(pageBtn);
|
|
}
|
|
|
|
// Next button
|
|
const nextBtn = document.createElement("button");
|
|
nextBtn.className = "btn btn-secondary";
|
|
nextBtn.textContent = "Next";
|
|
nextBtn.disabled = page === totalPages;
|
|
nextBtn.onclick = () => {
|
|
if (page < totalPages) {
|
|
currentPage = page + 1;
|
|
fetchTorrents(currentPage);
|
|
}
|
|
};
|
|
pagination.appendChild(nextBtn);
|
|
|
|
// Insert pagination into the designated container in Active Torrents header
|
|
const paginationContainer = document.getElementById(
|
|
"paginationContainer"
|
|
);
|
|
const existingPagination =
|
|
paginationContainer.querySelector(".d-flex");
|
|
if (existingPagination) {
|
|
existingPagination.remove(); // Remove any existing pagination to avoid duplicates
|
|
}
|
|
paginationContainer.appendChild(pagination);
|
|
}
|
|
async function fetchConfig() {
|
|
if (!localStorage.getItem("userPubKey")) return;
|
|
try {
|
|
const res = await fetch("/config");
|
|
const config = await res.json();
|
|
document.getElementById("currentStorage").textContent =
|
|
"Current storage directory: " + config.storageDir;
|
|
document.getElementById("storageDir").value = config.storageDir;
|
|
} catch (error) {
|
|
console.error("Error fetching config:", error);
|
|
}
|
|
}
|
|
|
|
document
|
|
.getElementById("configForm")
|
|
.addEventListener("submit", async (e) => {
|
|
e.preventDefault();
|
|
const storageDir = document.getElementById("storageDir").value;
|
|
const res = await fetch("/config", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ storageDir }),
|
|
});
|
|
const result = await res.json();
|
|
document.getElementById("currentStorage").textContent =
|
|
"Current storage directory: " + result.storageDir;
|
|
bootstrap.Modal.getInstance(
|
|
document.getElementById("settingsModal")
|
|
).hide();
|
|
});
|
|
|
|
// Initial fetch and periodic updates
|
|
fetchTorrents(currentPage);
|
|
fetchConfig();
|
|
|
|
setInterval(() => {
|
|
fetchTorrents(currentPage);
|
|
fetchConfig();
|
|
}, 5000);
|
|
});
|
|
</script>
|
|
<!-- Bootstrap Bundle JS -->
|
|
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
|
|
</body>
|
|
</html>
|