From 52d01f1bcd30fb4dc8cad94d00ea7558ad7bc786 Mon Sep 17 00:00:00 2001 From: Maxim Vershinin Date: Wed, 19 Aug 2026 17:26:07 +0500 Subject: [PATCH] FEATURE: allows the backend to take a tracklis text file --- web/backend/api/__init__.py | 3 +- web/backend/api/split_file.py | 140 +++++++++++++++++++++++++++++++ web/backend/main.py | 7 +- web/backend/services/splitter.py | 9 +- 4 files changed, 149 insertions(+), 10 deletions(-) create mode 100644 web/backend/api/split_file.py diff --git a/web/backend/api/__init__.py b/web/backend/api/__init__.py index c38d5c4..ae73d86 100644 --- a/web/backend/api/__init__.py +++ b/web/backend/api/__init__.py @@ -1,4 +1,3 @@ """API route handlers.""" -from . import upload, split, status, download -# websocket is imported directly in main.py to avoid circular import \ No newline at end of file +from . import upload, split, split_file, status, download, formats, info diff --git a/web/backend/api/split_file.py b/web/backend/api/split_file.py new file mode 100644 index 0000000..1c0a354 --- /dev/null +++ b/web/backend/api/split_file.py @@ -0,0 +1,140 @@ +"""Endpoint for splitting with a tracklist file upload.""" + +import json +from pathlib import Path + +from fastapi import APIRouter, UploadFile, File, Form, HTTPException, BackgroundTasks + +from backend.config import settings +from backend.services.task_manager import task_manager +from backend.services.splitter import run_split_task +from backend.models.request import TracklistEntry +from backend.models.response import SplitResponse, TaskStatus + +router = APIRouter(prefix="/api", tags=["split"]) + +# Import CLI tracklist parsing functions +from audio_splitter.tracklist import parse_format, read_tracklist + + +@router.post("/split-file", response_model=SplitResponse) +async def split_from_file( + background_tasks: BackgroundTasks, + task_id: str = Form(...), + tracklist_file: UploadFile = File(...), + options: str = Form("{}"), + tracklist_format: str = Form("%ts %tn - %an"), +): + """ + Start a splitting task using a tracklist file. + + This endpoint accepts a plain text tracklist file (like the CLI does) + and parses it using the same logic. + + Args: + task_id: Task ID from upload. + tracklist_file: Tracklist file (text/plain). + options: JSON string of all CLI options. + tracklist_format: Format string for parsing (default: "%ts %tn - %an"). + + Returns: + SplitResponse with task_id and status. + """ + # Validate task exists + if not task_manager.has_task(task_id): + raise HTTPException(status_code=404, detail=f"Task {task_id} not found") + + # Check if task is already processing + status = task_manager.get_status(task_id) + if status and status["status"] in (TaskStatus.PROCESSING, TaskStatus.DONE): + raise HTTPException( + status_code=409, + detail=f"Task {task_id} is already {status['status']}" + ) + + # Validate file type + if not tracklist_file.filename.endswith(('.txt', '.text')): + raise HTTPException( + status_code=400, + detail="Tracklist file must be a text file (.txt or .text)" + ) + + # Read and parse the tracklist file + try: + content = await tracklist_file.read() + text = content.decode('utf-8') + except UnicodeDecodeError: + raise HTTPException( + status_code=400, + detail="Tracklist file must be UTF-8 encoded" + ) + + if not text.strip(): + raise HTTPException( + status_code=400, + detail="Tracklist file is empty" + ) + + # Parse the tracklist using CLI logic + try: + # Parse the format string into tokens + tokens = parse_format(tracklist_format) + + # Write the content to a temporary file (read_tracklist expects a file path) + temp_tracklist_path = settings.temp_dir / task_id / "tracklist.txt" + temp_tracklist_path.parent.mkdir(parents=True, exist_ok=True) + temp_tracklist_path.write_text(text, encoding='utf-8') + + # Parse the tracklist file using CLI logic + tracklist_dicts = read_tracklist(str(temp_tracklist_path), tokens) + + except Exception as e: + raise HTTPException( + status_code=400, + detail=f"Failed to parse tracklist: {str(e)}" + ) + + if not tracklist_dicts: + raise HTTPException( + status_code=400, + detail="No valid tracks found in tracklist file" + ) + + # Convert dicts to TracklistEntry objects (same as JSON endpoint) + try: + tracklist_entries = [TracklistEntry(**entry) for entry in tracklist_dicts] + except Exception as e: + raise HTTPException( + status_code=400, + detail=f"Invalid tracklist data: {str(e)}" + ) + + # Parse options JSON + try: + options_dict = json.loads(options) + except json.JSONDecodeError: + raise HTTPException( + status_code=400, + detail="Invalid JSON in options field" + ) + + # Update task status + task_manager.update_task( + task_id, + status=TaskStatus.PROCESSING, + progress=0, + message="Preparing to split..." + ) + + # Start background task + background_tasks.add_task( + run_split_task, + task_id, + tracklist_entries, # now TracklistEntry objects + options_dict + ) + + return SplitResponse( + task_id=task_id, + status=TaskStatus.PROCESSING + ) diff --git a/web/backend/main.py b/web/backend/main.py index 638051b..21d25f0 100644 --- a/web/backend/main.py +++ b/web/backend/main.py @@ -5,7 +5,7 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from backend.config import settings -from backend.api import upload, split, status, download, websocket, formats, info +from backend.api import upload, split, split_file, status, download, websocket, formats, info from backend.services import progress_publisher app = FastAPI( @@ -33,11 +33,12 @@ async def startup_event(): # Include routers app.include_router(upload.router) app.include_router(split.router) +app.include_router(split_file.router) # NEW app.include_router(status.router) app.include_router(download.router) app.include_router(websocket.router) -app.include_router(formats.router) # new -app.include_router(info.router) # new +app.include_router(formats.router) +app.include_router(info.router) @app.get("/") diff --git a/web/backend/services/splitter.py b/web/backend/services/splitter.py index 61e99ee..23be35b 100644 --- a/web/backend/services/splitter.py +++ b/web/backend/services/splitter.py @@ -40,8 +40,10 @@ def run_split_task(task_id: str, tracklist: List[TracklistEntry], options: Dict[ from types import SimpleNamespace + # Build args namespace from options. + # IMPORTANT: format is None if not specified -> core will auto-detect. args = SimpleNamespace( - format=options.get("format", "mp3"), + format=options.get("format"), transcode_to=options.get("transcode_to", None), drop_video=options.get("drop_video", False), drop_subs=options.get("drop_subs", False), @@ -70,13 +72,10 @@ def run_split_task(task_id: str, tracklist: List[TracklistEntry], options: Dict[ task_id, progress=10, message="Starting split..." ) - # Run the split split_audio(str(input_path), str(output_dir), tracks, args) - # Get output files output_files = FileManager.get_output_files(task_id) - # Final status update task_manager.update_task_with_progress( task_id, progress=100, @@ -84,7 +83,7 @@ def run_split_task(task_id: str, tracklist: List[TracklistEntry], options: Dict[ status=TaskStatus.DONE ) - # Add tracks to the task state + # Add tracks to the task state (for download and status queries) task_manager.update_task( task_id, tracks=output_files -- 2.52.0