saving before implementing ytdlp
This commit is contained in:
parent
a26b52c2fa
commit
cc9324fb65
|
|
@ -72,6 +72,17 @@
|
|||
<button id="btn-add-close" class="add-panel-close">Close</button>
|
||||
<div class="add-panel-content">
|
||||
<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>
|
||||
<button id="btn-add" class="add-btn">Add to library...</button>
|
||||
|
|
|
|||
|
|
@ -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:hover { background: #2a2a2a; border-color: #666; color: #aaa; }
|
||||
.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.hidden { display: none; }
|
||||
.add-panel.closing { animation: panelSlideDown 0.2s ease-in forwards; }
|
||||
|
|
|
|||
|
|
@ -49,6 +49,80 @@
|
|||
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
|
||||
fileInput.onchange = () => {
|
||||
if (fileInput.files.length > 0) {
|
||||
|
|
|
|||
45
server.ts
45
server.ts
|
|
@ -33,6 +33,7 @@ import {
|
|||
getClientInfo,
|
||||
} from "./auth";
|
||||
import { Library } from "./library";
|
||||
import { createFetchRequest, getUserRequests, startDownload } from "./ytdlp";
|
||||
|
||||
// Load 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
|
||||
if (path === "/api/auth/signup" && req.method === "POST") {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -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" });
|
||||
}
|
||||
Loading…
Reference in New Issue