Files
2025-02-23 14:41:47 -05:00

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>