saving before implementing ytdlp

This commit is contained in:
peterino2 2026-02-03 21:52:28 -08:00
parent a26b52c2fa
commit cc9324fb65
5 changed files with 221 additions and 0 deletions

View File

@ -72,6 +72,17 @@
<button id="btn-add-close" class="add-panel-close">Close</button> <button id="btn-add-close" class="add-panel-close">Close</button>
<div class="add-panel-content"> <div class="add-panel-content">
<button id="btn-upload-files" class="add-option">Upload files...</button> <button id="btn-upload-files" class="add-option">Upload files...</button>
<button id="btn-fetch-url" class="add-option">Fetch from website...</button>
</div>
</div>
<div id="fetch-dialog" class="fetch-dialog hidden">
<div class="fetch-dialog-header">
<span>Fetch from URL</span>
<button id="btn-fetch-close" class="fetch-dialog-close">×</button>
</div>
<div class="fetch-dialog-content">
<input type="text" id="fetch-url-input" class="fetch-url-input" placeholder="https://youtube.com/watch?v=...">
<button id="btn-fetch-submit" class="fetch-submit-btn">Fetch</button>
</div> </div>
</div> </div>
<button id="btn-add" class="add-btn">Add to library...</button> <button id="btn-add" class="add-btn">Add to library...</button>

View File

@ -70,6 +70,18 @@ h3 { font-size: 0.8rem; color: #666; margin-bottom: 0.3rem; text-transform: uppe
.add-btn { width: 100%; padding: 0.5rem; background: #252525; border: 1px solid #444; border-radius: 4px; color: #888; font-size: 0.85rem; cursor: pointer; margin-top: 0.3rem; flex-shrink: 0; transition: all 0.2s; } .add-btn { width: 100%; padding: 0.5rem; background: #252525; border: 1px solid #444; border-radius: 4px; color: #888; font-size: 0.85rem; cursor: pointer; margin-top: 0.3rem; flex-shrink: 0; transition: all 0.2s; }
.add-btn:hover { background: #2a2a2a; border-color: #666; color: #aaa; } .add-btn:hover { background: #2a2a2a; border-color: #666; color: #aaa; }
.add-btn.hidden { display: none; } .add-btn.hidden { display: none; }
.fetch-dialog { position: absolute; bottom: 0; left: 0; right: 0; background: #1a1a1a; border-top: 1px solid #333; border-radius: 0 0 6px 6px; display: flex; flex-direction: column; animation: panelSlideUp 0.2s ease-out; }
.fetch-dialog.hidden { display: none; }
.fetch-dialog.closing { animation: panelSlideDown 0.2s ease-in forwards; }
.fetch-dialog-header { display: flex; justify-content: space-between; align-items: center; padding: 0.4rem 0.5rem; border-bottom: 1px solid #333; }
.fetch-dialog-header span { font-size: 0.8rem; color: #aaa; }
.fetch-dialog-close { background: none; border: none; color: #666; font-size: 1.2rem; cursor: pointer; padding: 0 0.3rem; line-height: 1; }
.fetch-dialog-close:hover { color: #aaa; }
.fetch-dialog-content { padding: 0.5rem; display: flex; gap: 0.4rem; }
.fetch-url-input { flex: 1; padding: 0.4rem 0.6rem; background: #222; border: 1px solid #444; border-radius: 4px; color: #eee; font-size: 0.85rem; font-family: inherit; }
.fetch-url-input:focus { outline: none; border-color: #4e8; }
.fetch-submit-btn { padding: 0.4rem 0.8rem; background: #2a3a2a; border: 1px solid #4e8; border-radius: 4px; color: #4e8; font-size: 0.85rem; cursor: pointer; font-family: inherit; }
.fetch-submit-btn:hover { background: #3a4a3a; }
.add-panel { position: absolute; bottom: 0; left: 0; right: 0; background: #1a1a1a; border-top: 1px solid #333; border-radius: 0 0 6px 6px; display: flex; flex-direction: column; height: 50%; overflow: hidden; animation: panelSlideUp 0.2s ease-out; } .add-panel { position: absolute; bottom: 0; left: 0; right: 0; background: #1a1a1a; border-top: 1px solid #333; border-radius: 0 0 6px 6px; display: flex; flex-direction: column; height: 50%; overflow: hidden; animation: panelSlideUp 0.2s ease-out; }
.add-panel.hidden { display: none; } .add-panel.hidden { display: none; }
.add-panel.closing { animation: panelSlideDown 0.2s ease-in forwards; } .add-panel.closing { animation: panelSlideDown 0.2s ease-in forwards; }

View File

@ -49,6 +49,80 @@
fileInput.click(); fileInput.click();
}; };
// Fetch from URL option
const fetchUrlBtn = M.$("#btn-fetch-url");
const fetchDialog = M.$("#fetch-dialog");
const fetchCloseBtn = M.$("#btn-fetch-close");
const fetchUrlInput = M.$("#fetch-url-input");
const fetchSubmitBtn = M.$("#btn-fetch-submit");
function openFetchDialog() {
closePanel();
fetchDialog.classList.remove("hidden", "closing");
fetchUrlInput.value = "";
fetchUrlInput.focus();
}
function closeFetchDialog() {
fetchDialog.classList.add("closing");
fetchDialog.addEventListener("animationend", () => {
if (fetchDialog.classList.contains("closing")) {
fetchDialog.classList.add("hidden");
fetchDialog.classList.remove("closing");
}
}, { once: true });
}
if (fetchUrlBtn) {
fetchUrlBtn.onclick = openFetchDialog;
}
if (fetchCloseBtn) {
fetchCloseBtn.onclick = closeFetchDialog;
}
if (fetchSubmitBtn) {
fetchSubmitBtn.onclick = async () => {
const url = fetchUrlInput.value.trim();
if (!url) {
M.showToast("Please enter a URL");
return;
}
closeFetchDialog();
// Create a task for this fetch
const task = createTask(`Fetching: ${url.substring(0, 50)}...`);
try {
const res = await fetch("/api/fetch", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ url })
});
if (res.ok) {
const data = await res.json();
task.setComplete();
M.showToast(data.message || "Fetch started");
} else {
const err = await res.json().catch(() => ({}));
task.setError(err.error || "Failed");
M.showToast(err.error || "Fetch failed", "error");
}
} catch (e) {
task.setError("Error");
M.showToast("Fetch failed", "error");
}
};
}
if (fetchUrlInput) {
fetchUrlInput.onkeydown = (e) => {
if (e.key === "Enter") fetchSubmitBtn.click();
if (e.key === "Escape") closeFetchDialog();
};
}
// File input change // File input change
fileInput.onchange = () => { fileInput.onchange = () => {
if (fileInput.files.length > 0) { if (fileInput.files.length > 0) {

View File

@ -33,6 +33,7 @@ import {
getClientInfo, getClientInfo,
} from "./auth"; } from "./auth";
import { Library } from "./library"; import { Library } from "./library";
import { createFetchRequest, getUserRequests, startDownload } from "./ytdlp";
// Load config // Load config
interface Config { interface Config {
@ -571,6 +572,50 @@ serve({
} }
} }
// API: fetch from URL (yt-dlp)
if (path === "/api/fetch" && req.method === "POST") {
const { user, headers } = getOrCreateUser(req, server);
if (!user) {
return Response.json({ error: "Authentication required" }, { status: 401 });
}
if (user.is_guest) {
return Response.json({ error: "Guests cannot fetch from URLs" }, { status: 403 });
}
try {
const { url } = await req.json();
if (!url || typeof url !== "string") {
return Response.json({ error: "URL is required" }, { status: 400 });
}
// Create fetch request
const request = createFetchRequest(url, user.id);
console.log(`[Fetch] ${user.username} requested: ${url} (id=${request.id})`);
// Start download in background (don't await)
startDownload(request, config.musicDir);
return Response.json({
message: "Fetch started",
requestId: request.id
}, { headers });
} catch (e) {
console.error("[Fetch] Error:", e);
return Response.json({ error: "Invalid request" }, { status: 400 });
}
}
// API: get fetch requests for current user
if (path === "/api/fetch" && req.method === "GET") {
const { user, headers } = getOrCreateUser(req, server);
if (!user) {
return Response.json({ error: "Authentication required" }, { status: 401 });
}
const requests = getUserRequests(user.id);
return Response.json({ requests }, { headers });
}
// Auth: signup // Auth: signup
if (path === "/api/auth/signup" && req.method === "POST") { if (path === "/api/auth/signup" && req.method === "POST") {
try { try {

79
ytdlp.ts Normal file
View File

@ -0,0 +1,79 @@
// MusicRoom - yt-dlp integration module
// Handles fetching audio from URLs via yt-dlp
export interface FetchRequest {
id: string;
url: string;
status: "pending" | "downloading" | "processing" | "complete" | "error";
progress: number;
error?: string;
filename?: string;
createdAt: number;
completedAt?: number;
userId: number;
}
// In-memory store of active fetch requests
const fetchRequests = new Map<string, FetchRequest>();
// Generate unique request ID
function generateId(): string {
return Math.random().toString(36).substring(2, 10);
}
// Get all requests for a user
export function getUserRequests(userId: number): FetchRequest[] {
return [...fetchRequests.values()].filter(r => r.userId === userId);
}
// Get request by ID
export function getRequest(id: string): FetchRequest | undefined {
return fetchRequests.get(id);
}
// Create a new fetch request
export function createFetchRequest(url: string, userId: number): FetchRequest {
const request: FetchRequest = {
id: generateId(),
url,
status: "pending",
progress: 0,
createdAt: Date.now(),
userId,
};
fetchRequests.set(request.id, request);
return request;
}
// Update request status
export function updateRequest(id: string, updates: Partial<FetchRequest>): void {
const request = fetchRequests.get(id);
if (request) {
Object.assign(request, updates);
}
}
// Remove completed/failed requests older than given age (ms)
export function cleanupOldRequests(maxAge: number = 3600000): void {
const now = Date.now();
for (const [id, request] of fetchRequests) {
if (request.status === "complete" || request.status === "error") {
if (now - request.createdAt > maxAge) {
fetchRequests.delete(id);
}
}
}
}
// TODO: Implement actual yt-dlp download
// This will:
// 1. Spawn yt-dlp process with URL
// 2. Parse progress output
// 3. Update request status
// 4. Move completed file to music directory
// 5. Trigger library rescan
export async function startDownload(request: FetchRequest, musicDir: string): Promise<void> {
// Stub - to be implemented
console.log(`[ytdlp] Would download: ${request.url} to ${musicDir}`);
updateRequest(request.id, { status: "error", error: "Not implemented yet" });
}