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>
|
<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>
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
45
server.ts
45
server.ts
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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