3 Commits

Author SHA1 Message Date
max 539ce3a801 feat (git): refactoring issue template added
Build and Push Docker Image / build-and-push-backend (pull_request) Successful in 54s
Build and Push Docker Image / build-and-push-frontend (pull_request) Successful in 58s
Build and Push Docker Image / notify-deployment-server (pull_request) Successful in 13s
2026-09-02 12:45:38 +05:00
max 3abb66fbdf feat (git): adds feature request issue template 2026-09-02 12:37:08 +05:00
max 8fb68e76ab feat (git): adds a new issue template
Build and Push Docker Image / build-and-push-backend (pull_request) Successful in 20m33s
Build and Push Docker Image / build-and-push-frontend (pull_request) Successful in 2m5s
Build and Push Docker Image / notify-deployment-server (pull_request) Successful in 17s
2026-09-02 12:21:47 +05:00
17 changed files with 716 additions and 432 deletions
+126
View File
@@ -0,0 +1,126 @@
name: "Bug Report"
description: "Report a bug or unexpected behavior to help us improve"
title: "[BUG]: "
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
## ⚠️ Before You Begin
Please ensure you have:
- [ ] Searched existing issues to avoid duplicates
- [ ] Confirmed this is a bug, not a question or configuration problem
- type: textarea
id: description
attributes:
label: "📋 Description"
description: "Provide a clear and concise description of the bug"
placeholder: "What happened? What did you expect to happen instead?"
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: "🔁 Steps to Reproduce"
description: "Step-by-step instructions to reproduce the issue"
placeholder: |
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
validations:
required: true
- type: textarea
id: expected
attributes:
label: "✅ Expected Behavior"
description: "What you expected to happen"
placeholder: "A clear description of what should happen..."
validations:
required: true
- type: textarea
id: actual
attributes:
label: "❌ Actual Behavior"
description: "What actually happened"
placeholder: "Include error messages, stack traces, or unexpected outcomes..."
validations:
required: true
- type: textarea
id: logs
attributes:
label: "📄 Logs / Screenshots"
description: |
Provide relevant logs, error messages, or screenshots.
For logs, please use a pastebin and share the URL.
**Remember to remove any sensitive information (API keys, passwords, etc.).**[reference:13]
placeholder: "Paste logs here or provide a Gist URL..."
validations:
required: false
- type: markdown
attributes:
value: |
---
## 🖥️ Environment Details
Please fill out the relevant information below.
- type: input
id: version
attributes:
label: "📦 Audio Splitter Version"
description: "The version you are using (or commit reference)"
placeholder: "e.g., v1.21.7"
validations:
required: true
- type: input
id: os
attributes:
label: "💻 Operating System"
description: "Your OS and version"
placeholder: "e.g., Ubuntu 22.04, macOS Sonoma 14.5, Windows"
validations:
required: true
- type: input
id: browser
attributes:
label: "🌍 Browser (if applicable)"
description: "Browser name and version"
placeholder: "e.g., Chrome 120, Firefox 121, Safari 17"
validations:
required: false
- type: textarea
id: additional
attributes:
label: "📎 Additional Context"
description: "Any other information that might be relevant"
placeholder: |
- Database type and version (e.g., PostgreSQL 15, SQLite)
- Reverse proxy/CDN in use (e.g., Nginx, Cloudflare)[reference:15]
- Any custom configuration
- Related issues or PRs
validations:
required: false
- type: checkboxes
id: checklist
attributes:
label: "✅ Submission Checklist"
description: "Please confirm the following before submitting"
options:
- label: "I have searched for existing issues (open and closed) that report the same problem"
required: true
- label: "I am using the latest stable release of Audio Splitter"
required: true
- label: "I have provided clear steps to reproduce the issue"
required: true
- label: "I have included relevant logs or error messages (with sensitive info removed)"
required: false
+129
View File
@@ -0,0 +1,129 @@
name: "Feature Request"
description: "Suggest a new feature or enhancement for this project"
title: "[FEATURE]: "
labels: ["enhancement"]
body:
- type: markdown
attributes:
value: |
## ⚠️ Before You Begin
Please ensure you have:
- [ ] Searched existing issues (open and closed) to avoid duplicates
- [ ] Reviewed the [project documentation](https://git.vmn.su/max/audio_splitter/wiki) for existing functionality
- [ ] Confirmed this is a feature request, not a bug or configuration question
- type: textarea
id: problem
attributes:
label: "🎯 Problem Statement"
description: "What problem does this feature solve? What can't you do today?"
placeholder: |
I'm always frustrated when...
Currently, it's hard/impossible to...
This feature would help me because...
validations:
required: true
- type: textarea
id: solution
attributes:
label: "💡 Proposed Solution"
description: "Describe the solution you'd like to see"
placeholder: |
A clear description of what you want to happen.
If you have a specific API, UI, or implementation in mind, describe it here.
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: "🔄 Alternatives Considered"
description: "What alternative solutions or workarounds have you considered?"
placeholder: |
- Alternative A: ...
- Workaround B: ...
- Why these don't fully solve the problem
validations:
required: false
- type: textarea
id: usecase
attributes:
label: "👤 Use Case / Persona"
description: "Who benefits from this feature and in what context?"
placeholder: |
As a [type of user], I want to [do something] so that [I achieve some benefit].
Example: "As a project maintainer, I want to bulk-close issues by label so that I can clean up stale tickets faster."
validations:
required: false
- type: textarea
id: screenshots
attributes:
label: "📸 Screenshots"
description: "If you can, provide screenshots or diagrams of the proposed feature"
placeholder: "Drag and drop images or paste links..."
validations:
required: false
- type: markdown
attributes:
value: |
---
## 🔧 Implementation Details (Optional)
The following sections are for contributors who want to implement this feature.
- type: textarea
id: technical
attributes:
label: "⚙️ Technical Approach"
description: "If you have ideas about how to implement this, share them here"
placeholder: |
- Which components/modules would be affected?
- Any database schema changes?
- API design considerations?
validations:
required: false
- type: textarea
id: acceptance
attributes:
label: "✅ Acceptance Criteria"
description: "What does 'done' look like for this feature?"
placeholder: |
- [ ] Feature works in the UI
- [ ] API endpoints are documented
- [ ] Tests are added
- [ ] Documentation is updated
validations:
required: false
- type: dropdown
id: contribution
attributes:
label: "🤝 Are you willing to contribute this feature?"
description: "Knowing if you can help implement this helps us prioritize"
options:
- "Yes, I can contribute"
- "Yes, but I need guidance"
- "No, but I can test/review"
- "No, I'm just requesting"
validations:
required: true
- type: checkboxes
id: checklist
attributes:
label: "✅ Submission Checklist"
description: "Please confirm the following before submitting"
options:
- label: "I have searched for existing issues (open and closed) that request the same feature"
required: true
- label: "I have explained the problem this feature solves"
required: true
- label: "I have described a concrete solution or direction"
required: true
- label: "I have considered alternatives and explained why they fall short"
required: false
+160
View File
@@ -0,0 +1,160 @@
name: "Refactor Request"
description: "Propose a code restructuring or cleanup with no behavior change"
title: "[REFACTOR]: "
labels: ["refactoring"]
body:
- type: markdown
attributes:
value: |
## ⚠️ Before You Begin
Please ensure you have:
- [ ] Searched existing issues (open and closed) to avoid duplicates
- [ ] Reviewed the [project documentation](https://git.vmn.su/max/audio_splitter/wiki) for architecture guidelines
- [ ] Confirmed this is a refactor (code restructuring with NO behavior change), not a bug fix or new feature
- type: textarea
id: problem
attributes:
label: "🎯 Problem Statement"
description: "What pain points does the current code cause? Why is this refactor needed?"
placeholder: |
The current implementation is difficult to maintain because...
This code has accumulated technical debt due to...
I'm constantly frustrated when working with this module because...
validations:
required: true
- type: textarea
id: scope
attributes:
label: "📂 Scope"
description: "Which files, modules, or components need refactoring? Be specific."
placeholder: |
- File paths: `src/auth/`, `internal/handler/`
- Component: `UserService` class
validations:
required: true
- type: textarea
id: current-state
attributes:
label: "🔍 Current State"
description: "Describe the current code structure, its issues, and why it's problematic"
placeholder: |
- The function `processUser()` is 500+ lines long
- Duplicated logic across 3 different files
- Mixed concerns (business logic + HTTP handling)
- Poor test coverage makes changes risky
- Naming is unclear and inconsistent
validations:
required: true
- type: textarea
id: proposed-changes
attributes:
label: "💡 Proposed Solution"
description: "What should the code look like after the refactor?"
placeholder: |
- Extract `processUser()` into smaller, focused functions
- Consolidate duplicated logic into a shared utility
- Separate business logic from HTTP handlers
- Introduce interfaces for better testability
- Rename variables and functions for clarity
validations:
required: true
- type: textarea
id: constraints
attributes:
label: "🚫 Constraints"
description: "What must NOT change? Public APIs, behavior, backward compatibility, performance characteristics?"
placeholder: |
- All public APIs must remain identical
- Database schema must not change
- External behavior must be identical
- Response formats must stay the same
- Performance must not degrade
validations:
required: true
- type: textarea
id: benefits
attributes:
label: "📈 Expected Benefits"
description: "What improvements will this refactor bring?"
placeholder: |
- Improved maintainability
- Better testability
- Reduced code duplication
- Clearer separation of concerns
- Easier onboarding for new contributors
validations:
required: false
- type: textarea
id: risks
attributes:
label: "⚠️ Risks & Mitigations"
description: "What could go wrong, and how will you mitigate it?"
placeholder: |
- Risk: Regression bugs
Mitigation: Comprehensive test coverage before and after
- Risk: Large diff making code review difficult
Mitigation: Break into smaller, incremental PRs
validations:
required: false
- type: textarea
id: testing
attributes:
label: "🧪 Testing Strategy"
description: "How will you ensure the refactor doesn't break existing functionality?"
placeholder: |
- Existing test suite must pass
- Add regression tests before refactoring
- Run benchmarks to ensure no performance regression
- Manual testing of critical paths
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: "🔄 Alternatives Considered"
description: "What other approaches did you consider, and why did you reject them?"
placeholder: |
- Alternative A: Complete rewrite — rejected because too risky
- Alternative B: Gradual deprecation — rejected because too slow
validations:
required: false
- type: dropdown
id: contribution
attributes:
label: "🤝 Are you willing to contribute this refactor?"
description: "Knowing if you can help implement this helps us prioritize"
options:
- "Yes, I can contribute"
- "Yes, but I need guidance"
- "No, but I can review"
- "No, I'm just suggesting"
validations:
required: true
- type: checkboxes
id: checklist
attributes:
label: "✅ Submission Checklist"
description: "Please confirm the following before submitting"
options:
- label: "I have searched for existing issues (open and closed) that request the same refactor"
required: true
- label: "I have clearly explained the problem this refactor solves"
required: true
- label: "I have described the scope of the refactor (specific files/modules)"
required: true
- label: "I have identified what must NOT change (APIs, behavior, backward compatibility)"
required: true
- label: "I have described how the refactor will be tested"
required: true
+57
View File
@@ -0,0 +1,57 @@
FROM python:3.13-slim
# Install FFmpeg and dependencies required for gosu installation
RUN apt-get update && \
apt-get install -y --no-install-recommends \
ffmpeg \
ca-certificates \
wget \
gnupg \
dirmngr \
gnupg-agent && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Install gosu (lightweight tool for dropping privileges)
RUN set -eux; \
dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')"; \
wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/1.17/gosu-$dpkgArch"; \
wget -O /usr/local/bin/gosu.asc "https://github.com/tianon/gosu/releases/download/1.17/gosu-$dpkgArch.asc"; \
export GNUPGHOME="$(mktemp -d)"; \
gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4; \
gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu; \
gpgconf --kill all; \
rm -rf "$GNUPGHOME" /usr/local/bin/gosu.asc; \
chmod +x /usr/local/bin/gosu; \
gosu --version
# Set Python environment variables
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
# Set working directory
WORKDIR /app
# Copy package metadata and source code
COPY setup.py pyproject.toml README.md ./
COPY audio_splitter/ ./audio_splitter/
# Install the package
RUN pip install --no-cache-dir .
# Create a non-root user with UID 1000
RUN addgroup --system --gid 1000 appgroup && \
adduser --system --uid 1000 --ingroup appgroup appuser
# Change ownership of /app to the container user (so it can write there if needed)
RUN chown -R appuser:appgroup /app
# Copy the entrypoint script
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
# Set the entrypoint
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
# Default command (shows help if no arguments provided)
CMD ["--help"]
-35
View File
@@ -1,35 +0,0 @@
"""Default values for all configurable options shared between CLI, web backend, and frontend."""
# Output container format (used when user doesn't specify one; auto-detection overrides this)
DEFAULT_FORMAT = "mp3"
# Filename template
DEFAULT_OUTPUT_TEMPLATE = "%an-%tn.%ext"
# Character replacement
DEFAULT_REPLACEMENT_CHAR = "_"
DEFAULT_BAD_CHARS = r'!@#№$;:%^&?*(){}[]\/<>+=~`\' '
# Tracklist parsing
DEFAULT_TRACKLIST_FORMAT = "%ts %tn - %an"
# Metadata defaults
DEFAULT_ALBUM = None
DEFAULT_COMMENT = None
DEFAULT_COMMENT_STREAM = None
DEFAULT_COMMENT_SEPARATOR = "; "
DEFAULT_NO_COMMENT = False
DEFAULT_MERGE_COMMENTS = False
# Stream handling
DEFAULT_DROP_VIDEO = False
DEFAULT_DROP_SUBS = False
# Filename/export options
DEFAULT_NUMBER_TRACKS = False
DEFAULT_REPLACE_BAD_CHARS = True
DEFAULT_SKIP_EXISTING = False
DEFAULT_DELETE_ORIGINAL = False
# Transcoding (None means copy codec)
DEFAULT_TRANSCODE_TO = None
+116 -50
View File
@@ -1,67 +1,133 @@
"""Commandline interface and entry point."""
import argparse
import os
import sys
import subprocess
import argparse
from .constants import DEFAULT_BAD_CHARS
from .defaults import (
DEFAULT_FORMAT,
DEFAULT_OUTPUT_TEMPLATE,
DEFAULT_REPLACEMENT_CHAR,
DEFAULT_BAD_CHARS,
DEFAULT_TRACKLIST_FORMAT,
DEFAULT_ALBUM,
DEFAULT_COMMENT,
DEFAULT_COMMENT_STREAM,
DEFAULT_COMMENT_SEPARATOR,
DEFAULT_NO_COMMENT,
DEFAULT_MERGE_COMMENTS,
DEFAULT_DROP_VIDEO,
DEFAULT_DROP_SUBS,
DEFAULT_NUMBER_TRACKS,
DEFAULT_REPLACE_BAD_CHARS,
DEFAULT_SKIP_EXISTING,
DEFAULT_DELETE_ORIGINAL,
DEFAULT_TRANSCODE_TO,
)
from .core import split_audio
from .tracklist import read_tracklist, parse_format
from .core import split_audio
def main():
parser = argparse.ArgumentParser(description="Split audio file using a tracklist.")
parser.add_argument('input_file', help='Input audio file')
"""Parse arguments and start the splitting process."""
parser = argparse.ArgumentParser(
description="Split an audio file into tracks using a tracklist.",
epilog="Tracklist format: mm:ss track_name - author_name"
)
# Positional arguments.
parser.add_argument('input_file', help='Input audio/video file')
parser.add_argument('tracklist_file', help='Tracklist file')
# Output options
parser.add_argument('--format', default=DEFAULT_FORMAT, help=f"Output container format (default: {DEFAULT_FORMAT})")
parser.add_argument('--transcode-to', default=DEFAULT_TRANSCODE_TO, help="Audio codec to transcode to (default: copy)")
parser.add_argument('--drop-video', action='store_true', default=DEFAULT_DROP_VIDEO, help="Drop video streams")
parser.add_argument('--drop-subs', action='store_true', default=DEFAULT_DROP_SUBS, help="Drop subtitle streams")
# Optional arguments.
parser.add_argument(
'--output-dir', '-o',
help='Output directory for split tracks (default: <input_basename>_splits)'
)
parser.add_argument(
'--format',
help='Output container format (e.g., mp3, m4a, mkv, mp4, ogg, opus)'
)
parser.add_argument(
'--transcode-to',
help='Re-encode audio to this codec (e.g., libmp3lame, aac, libopus)'
)
parser.add_argument(
'--drop-video',
action='store_true',
help='Remove video streams from output'
)
parser.add_argument(
'--drop-subs',
action='store_true',
help='Remove subtitle streams from output'
)
parser.add_argument(
'--number-tracks',
action='store_true',
help='Prepend track number to output filenames (convenience; use %%num in template for full control)'
)
parser.add_argument(
'--replace-bad-chars',
action='store_true',
help='Replace problematic characters in filenames (default: off)'
)
parser.add_argument(
'--replacement-char',
default='_',
help='Character used as replacement (default: "_")'
)
parser.add_argument(
'--bad-chars',
default=DEFAULT_BAD_CHARS,
help='String of characters to replace (default includes space and single quote)'
)
parser.add_argument(
'--skip-existing',
action='store_true',
help='Skip extraction if output file already exists (default: overwrite)'
)
parser.add_argument(
'--tracklist-format',
default='%ts %tn - %an',
help='Format of each line in the tracklist using placeholders: '
'%%ts (timestamp), %%tn (track name), %%an (author), %%al (album), '
'%%date (date/year), %%ext (file extension). '
'Default: "%%ts %%tn - %%an"'
)
parser.add_argument(
'--output-template',
default='%an-%tn.%ext',
help='Template for output filenames using placeholders: '
'%%tn (track name), %%an (author), %%al (album), '
'%%date (date/year), %%ext (file extension), %%num (track number). '
'Default: "%%an-%%tn.%%ext"'
)
parser.add_argument(
'--dry-run',
action='store_true',
help='Parse and display the tracklist without splitting any files'
)
# Filename options
parser.add_argument('--number-tracks', action='store_true', default=DEFAULT_NUMBER_TRACKS, help="Prepend track numbers")
parser.add_argument('--output-template', default=DEFAULT_OUTPUT_TEMPLATE, help=f"Output filename template (default: {DEFAULT_OUTPUT_TEMPLATE})")
parser.add_argument('--replace-bad-chars', action='store_true', default=DEFAULT_REPLACE_BAD_CHARS, help="Replace bad characters")
parser.add_argument('--replacement-char', default=DEFAULT_REPLACEMENT_CHAR, help=f"Replacement character (default: {DEFAULT_REPLACEMENT_CHAR})")
parser.add_argument('--bad-chars', default=DEFAULT_BAD_CHARS, help=f"Bad characters to replace (default: {DEFAULT_BAD_CHARS})")
parser.add_argument('--skip-existing', action='store_true', default=DEFAULT_SKIP_EXISTING, help="Skip existing output files")
# Metadata options.
parser.add_argument(
'--album',
help='Set album name in output metadata (overrides parsed %%al and original album)'
)
parser.add_argument(
'--comment',
help='Explicit comment text (overrides all other comment settings)'
)
parser.add_argument(
'--no-comment',
action='store_true',
help='Explicitly ignore any comment (no comment written)'
)
parser.add_argument(
'--comment-stream',
type=int,
default=None,
help='Select comment from a specific stream index (0based). Default: first stream with a comment.'
)
parser.add_argument(
'--merge-comments',
action='store_true',
help='Merge all comments from all streams into one (separated by --comment-separator)'
)
parser.add_argument(
'--comment-separator',
default='; ',
help='Separator used when merging comments (default: "; ")'
)
# Metadata options
parser.add_argument('--album', default=DEFAULT_ALBUM, help="Album name")
parser.add_argument('--comment', default=DEFAULT_COMMENT, help="Comment")
parser.add_argument('--no-comment', action='store_true', default=DEFAULT_NO_COMMENT, help="Ignore comment")
parser.add_argument('--comment-stream', type=int, default=DEFAULT_COMMENT_STREAM, help="Comment stream index")
parser.add_argument('--merge-comments', action='store_true', default=DEFAULT_MERGE_COMMENTS, help="Merge all comments")
parser.add_argument('--comment-separator', default=DEFAULT_COMMENT_SEPARATOR, help=f"Separator for merged comments (default: {DEFAULT_COMMENT_SEPARATOR})")
# Tracklist format
parser.add_argument('--tracklist-format', default=DEFAULT_TRACKLIST_FORMAT, help=f"Tracklist format (default: {DEFAULT_TRACKLIST_FORMAT})")
# Other
parser.add_argument('--delete-original', action='store_true', default=DEFAULT_DELETE_ORIGINAL, help="Delete original file after split")
parser.add_argument('--dry-run', action='store_true', help="Parse and display tracklist without splitting")
# NEW: Delete original after successful split.
parser.add_argument(
'--delete-original',
action='store_true',
help='Delete the original input file after successful splitting (default: keep)'
)
args = parser.parse_args()
+37
View File
@@ -0,0 +1,37 @@
services:
backend:
build:
context: .
dockerfile: web/backend/Dockerfile
volumes:
# Bind mount for source code (hot-reload for Python)
- ./web/backend:/app/backend
- ./audio_splitter:/app/audio_splitter
- ./setup.py:/app/setup.py
- ./pyproject.toml:/app/pyproject.toml
# Development data directory (overrides production)
- "./dev_data:/tmp/audio_splitter_web"
environment:
- DEBUG=1
- PYTHONUNBUFFERED=1
ports:
- "8000:8000"
frontend:
build:
context: ./web/frontend
# Use the same Dockerfile, but override CMD for development
dockerfile: Dockerfile
command: ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
volumes:
# Bind mount for source code (hot-reload for Vite)
- ./web/frontend:/app
- node_modules:/app/node_modules
environment:
- BACKEND_URL=http://backend:8000
- VITE_BACKEND_URL=http://backend:8000
ports:
- "5173:5173"
volumes:
node_modules:
+5 -10
View File
@@ -1,24 +1,19 @@
services:
backend:
build:
context: .
dockerfile: web/backend/Dockerfile
#image: max/audio_splitter_backend:latest
image: git.vmn.su/max/audio_splitter_backend:latest
ports:
- "${BACKEND_PORT:-8000}:8000"
volumes:
#- "${BACKEND_DATA_DIR:-/var/lib/audio_splitter_data}:/tmp/audio_splitter_web"
- "/tmp/test:/tmp/audio_splitter_web"
# Bind mount for persistent data storage.
# Set BACKEND_DATA_DIR in .env to point to your data directory.
- "${BACKEND_DATA_DIR:-/var/lib/audio_splitter_data}:/tmp/audio_splitter_web"
environment:
- PYTHONUNBUFFERED=1
- DEBUG=${DEBUG:-0}
restart: unless-stopped
frontend:
build:
context: .
dockerfile: web/frontend/Dockerfile
#image: max/audio_splitter_frontend:latest
image: git.vmn.su/max/audio_splitter_frontend:latest
ports:
- "${FRONTEND_PORT:-5173}:80"
depends_on:
+18
View File
@@ -0,0 +1,18 @@
#!/bin/bash
set -e
# Check if we are running as root (default)
if [ "$(id -u)" = "0" ]; then
# If /data is a directory, change its ownership to the container user
if [ -d "/data" ]; then
echo "Setting ownership of /data to appuser:appgroup"
chown -R appuser:appgroup /data
fi
# Drop privileges and run audio_splitter with the provided arguments
exec gosu appuser audio_splitter "$@"
else
# If not root, just run audio_splitter directly
# It's not supposed to be called
exec audio_splitter "$@"
fi
-79
View File
@@ -1,79 +0,0 @@
#!/usr/bin/env python3
"""
Generate TypeScript constants from audio_splitter/defaults.py.
"""
import importlib.util
import json
import sys
from pathlib import Path
# Get the directory where this script is located
SCRIPT_DIR = Path(__file__).parent.absolute()
# The defaults.py is in the same directory as the script
DEFAULTS_PATH = SCRIPT_DIR / "defaults.py"
# The frontend source is at SCRIPT_DIR.parent (i.e., /app)
# The generated file should be at /app/src/constants/generated.ts
FRONTEND_ROOT = SCRIPT_DIR.parent
OUTPUT_PATH = FRONTEND_ROOT / "src" / "constants" / "generated.ts"
def load_defaults_module():
spec = importlib.util.spec_from_file_location("defaults", DEFAULTS_PATH)
if spec is None:
raise RuntimeError(f"Could not load spec for {DEFAULTS_PATH}")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def format_value(value):
"""Convert Python value to TypeScript literal."""
if value is None:
return "null"
if isinstance(value, bool):
return str(value).lower()
if isinstance(value, str):
# Use json.dumps to produce a properly escaped string literal
return json.dumps(value)
if isinstance(value, (int, float)):
return str(value)
if isinstance(value, list):
return f"[{', '.join(format_value(v) for v in value)}]"
return repr(value)
def main():
print(f"Generating TypeScript defaults from {DEFAULTS_PATH}")
defaults = load_defaults_module()
constants = {name: value for name, value in vars(defaults).items() if name.startswith("DEFAULT_")}
if not constants:
print("No DEFAULT_* constants found.")
sys.exit(1)
lines = [
"// ============================================================================",
"// GENERATED FILE DO NOT EDIT MANUALLY.",
"// This file is generated from audio_splitter/defaults.py.",
"// Run `npm run generate` or `python scripts/generate_ts_defaults.py` to update.",
"// ============================================================================",
"",
]
for name, value in sorted(constants.items()):
ts_value = format_value(value)
lines.append(f"export const {name} = {ts_value};")
OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True)
OUTPUT_PATH.write_text("\n".join(lines) + "\n")
print(f"✅ Generated {OUTPUT_PATH}")
print(f" {len(constants)} constants exported.")
if __name__ == "__main__":
main()
+2 -1
View File
@@ -1,3 +1,4 @@
"""API route handlers."""
from . import upload, split, split_file, status, download, formats, info
from . import upload, split, status, download
# websocket is imported directly in main.py to avoid circular import
-140
View File
@@ -1,140 +0,0 @@
"""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
)
+3 -4
View File
@@ -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, split_file, status, download, websocket, formats, info
from backend.api import upload, split, status, download, websocket, formats, info
from backend.services import progress_publisher
app = FastAPI(
@@ -33,12 +33,11 @@ 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)
app.include_router(info.router)
app.include_router(formats.router) # new
app.include_router(info.router) # new
@app.get("/")
+33 -45
View File
@@ -2,10 +2,10 @@
import os
import sys
import time
import shutil
from pathlib import Path
from types import SimpleNamespace
from typing import Dict, List, Any
from typing import List, Dict, Any
import time
from backend.config import settings
from backend.services.task_manager import task_manager
@@ -13,31 +13,12 @@ from backend.services.file_manager import FileManager
from backend.models.request import TracklistEntry
from backend.models.response import TaskStatus
# Import central defaults
from audio_splitter.defaults import (
DEFAULT_FORMAT,
DEFAULT_OUTPUT_TEMPLATE,
DEFAULT_REPLACEMENT_CHAR,
DEFAULT_BAD_CHARS,
DEFAULT_ALBUM,
DEFAULT_COMMENT,
DEFAULT_NO_COMMENT,
DEFAULT_COMMENT_STREAM,
DEFAULT_MERGE_COMMENTS,
DEFAULT_COMMENT_SEPARATOR,
DEFAULT_DROP_VIDEO,
DEFAULT_DROP_SUBS,
DEFAULT_NUMBER_TRACKS,
DEFAULT_REPLACE_BAD_CHARS,
DEFAULT_SKIP_EXISTING,
DEFAULT_DELETE_ORIGINAL,
DEFAULT_TRANSCODE_TO,
)
def run_split_task(task_id: str, tracklist: List[TracklistEntry], options: Dict[str, Any]) -> None:
try:
task_manager.update_task_with_progress(task_id, progress=5, message="Initializing...")
task_manager.update_task_with_progress(
task_id, progress=5, message="Initializing..."
)
input_path = FileManager.get_input_path(task_id)
if not input_path or not input_path.exists():
@@ -45,7 +26,6 @@ def run_split_task(task_id: str, tracklist: List[TracklistEntry], options: Dict[
output_dir = FileManager.ensure_output_dir(task_id)
# Convert tracklist to dicts (CLI format)
tracks = []
for entry in tracklist:
track_dict = {
@@ -58,25 +38,26 @@ def run_split_task(task_id: str, tracklist: List[TracklistEntry], options: Dict[
}
tracks.append(track_dict)
# Build args namespace using defaults where options not provided
from types import SimpleNamespace
args = SimpleNamespace(
format=options.get("format", DEFAULT_FORMAT),
transcode_to=options.get("transcode_to", DEFAULT_TRANSCODE_TO),
drop_video=options.get("drop_video", DEFAULT_DROP_VIDEO),
drop_subs=options.get("drop_subs", DEFAULT_DROP_SUBS),
number_tracks=options.get("number_tracks", DEFAULT_NUMBER_TRACKS),
replace_bad_chars=options.get("replace_bad_chars", DEFAULT_REPLACE_BAD_CHARS),
replacement_char=options.get("replacement_char", DEFAULT_REPLACEMENT_CHAR),
bad_chars=options.get("bad_chars", DEFAULT_BAD_CHARS),
skip_existing=options.get("skip_existing", DEFAULT_SKIP_EXISTING),
output_template=options.get("output_template", DEFAULT_OUTPUT_TEMPLATE),
album=options.get("album", DEFAULT_ALBUM),
comment=options.get("comment", DEFAULT_COMMENT),
no_comment=options.get("no_comment", DEFAULT_NO_COMMENT),
comment_stream=options.get("comment_stream", DEFAULT_COMMENT_STREAM),
merge_comments=options.get("merge_comments", DEFAULT_MERGE_COMMENTS),
comment_separator=options.get("comment_separator", DEFAULT_COMMENT_SEPARATOR),
delete_original=DEFAULT_DELETE_ORIGINAL, # never delete in web
format=options.get("format", "mp3"),
transcode_to=options.get("transcode_to", None),
drop_video=options.get("drop_video", False),
drop_subs=options.get("drop_subs", False),
number_tracks=options.get("number_tracks", False),
replace_bad_chars=options.get("replace_bad_chars", False),
replacement_char=options.get("replacement_char", "_"),
bad_chars=options.get("bad_chars", r'!@#№$;:%^&?*(){}[]\/<>+=~`\' '),
skip_existing=options.get("skip_existing", False),
output_template=options.get("output_template", "%an-%tn.%ext"),
album=options.get("album", None),
comment=options.get("comment", None),
no_comment=options.get("no_comment", False),
comment_stream=options.get("comment_stream", None),
merge_comments=options.get("merge_comments", False),
comment_separator=options.get("comment_separator", "; "),
delete_original=False,
)
project_root = Path(__file__).parent.parent.parent.parent
@@ -85,12 +66,17 @@ def run_split_task(task_id: str, tracklist: List[TracklistEntry], options: Dict[
from audio_splitter.core import split_audio
task_manager.update_task_with_progress(task_id, progress=10, message="Starting split...")
task_manager.update_task_with_progress(
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,
@@ -98,11 +84,13 @@ def run_split_task(task_id: str, tracklist: List[TracklistEntry], options: Dict[
status=TaskStatus.DONE
)
# Add tracks to the task state
task_manager.update_task(
task_id,
tracks=output_files
)
# Give WebSocket time to send the final message
time.sleep(0.5)
except Exception as e:
+12 -27
View File
@@ -1,42 +1,27 @@
# web/frontend/Dockerfile
# Build context must be the project root.
# Stage 1: Build
FROM node:20-alpine AS builder
# Install Python for the generation script
RUN apk add --no-cache python3 py3-pip
WORKDIR /app
# Copy frontend package files and install dependencies
COPY web/frontend/package.json web/frontend/package-lock.json* ./
# Install dependencies
COPY package.json package-lock.json* ./
RUN npm install
# Copy the frontend source code
COPY web/frontend/ .
# Copy the Python defaults and the generation script into the expected location
# The generate script in package.json expects ../../scripts/generate_ts_defaults.py
# So we must place it at /app/../../scripts/ which is /scripts/
# But we can't COPY to a parent directory. Instead, we'll copy to /app/scripts/
# and adjust the package.json script to use ./scripts/generate_ts_defaults.py
# Actually, the simplest fix is to copy to /app/scripts/ and then adjust package.json.
#
# Let's use a different approach: copy to /app/scripts/ and update the generate script.
COPY audio_splitter/defaults.py /app/scripts/defaults.py
COPY scripts/generate_ts_defaults.py /app/scripts/generate_ts_defaults.py
# Run the generation script
RUN python /app/scripts/generate_ts_defaults.py
# Build the frontend
# Copy the application code and build
COPY . .
RUN npm run build
# Stage 2: Production (nginx)
FROM nginx:alpine
# Copy built assets from builder
COPY --from=builder /app/dist /usr/share/nginx/html
COPY web/frontend/nginx/nginx.conf /etc/nginx/conf.d/default.conf
# Copy nginx configuration
COPY nginx/nginx.conf /etc/nginx/conf.d/default.conf
# Expose the port
EXPOSE 80
# Start nginx
CMD ["nginx", "-g", "daemon off;"]
+1 -4
View File
@@ -4,9 +4,6 @@
"version": "0.1.0",
"type": "module",
"scripts": {
"generate": "python scripts/generate_ts_defaults.py",
"predev": "npm run generate",
"prebuild": "npm run generate",
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
@@ -36,4 +33,4 @@
"typescript": "^5.2.2",
"vite": "^5.0.8"
}
}
}
+17 -37
View File
@@ -1,44 +1,24 @@
// web/frontend/src/stores/optionsStore.ts
import { create } from 'zustand'
import { SplitOptions } from '../types'
import {
DEFAULT_FORMAT,
DEFAULT_OUTPUT_TEMPLATE,
DEFAULT_REPLACEMENT_CHAR,
DEFAULT_BAD_CHARS,
DEFAULT_ALBUM,
DEFAULT_COMMENT,
DEFAULT_NO_COMMENT,
DEFAULT_COMMENT_STREAM,
DEFAULT_MERGE_COMMENTS,
DEFAULT_COMMENT_SEPARATOR,
DEFAULT_DROP_VIDEO,
DEFAULT_DROP_SUBS,
DEFAULT_NUMBER_TRACKS,
DEFAULT_REPLACE_BAD_CHARS,
DEFAULT_SKIP_EXISTING,
DEFAULT_TRANSCODE_TO,
DEFAULT_TRACKLIST_FORMAT,
} from '../constants/generated'
const DEFAULT_OPTIONS: SplitOptions = {
format: DEFAULT_FORMAT,
transcode_to: DEFAULT_TRANSCODE_TO ?? '',
drop_video: DEFAULT_DROP_VIDEO,
drop_subs: DEFAULT_DROP_SUBS,
number_tracks: DEFAULT_NUMBER_TRACKS,
replace_bad_chars: DEFAULT_REPLACE_BAD_CHARS,
replacement_char: DEFAULT_REPLACEMENT_CHAR,
bad_chars: DEFAULT_BAD_CHARS,
skip_existing: DEFAULT_SKIP_EXISTING,
output_template: DEFAULT_OUTPUT_TEMPLATE,
album: DEFAULT_ALBUM ?? '',
comment: DEFAULT_COMMENT ?? '',
no_comment: DEFAULT_NO_COMMENT,
comment_stream: DEFAULT_COMMENT_STREAM,
merge_comments: DEFAULT_MERGE_COMMENTS,
comment_separator: DEFAULT_COMMENT_SEPARATOR,
tracklist_format: DEFAULT_TRACKLIST_FORMAT,
format: 'mp3',
transcode_to: '',
drop_video: false,
drop_subs: false,
number_tracks: false,
replace_bad_chars: false,
replacement_char: '_',
bad_chars: '!@#№$;:%^&?*(){}[]\\/<>+=~`\' ',
skip_existing: false,
output_template: '%an-%tn.%ext',
album: '',
comment: '',
no_comment: false,
comment_stream: null,
merge_comments: false,
comment_separator: '; ',
tracklist_format: '%ts %tn - %an', // NEW
}
interface OptionsState {