├── .gitignore ├── Dockerfile ├── README.md ├── main.py ├── requirements.txt └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | psp_at3tool.exe 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11-slim AS builder 2 | WORKDIR /root 3 | ENV ARCH=x86_64 4 | RUN apt-get update && apt-get install -y yasm git curl lbzip2 build-essential 5 | RUN git clone https://github.com/acoustid/ffmpeg-build.git 6 | RUN echo "FFMPEG_CONFIGURE_FLAGS+=(--enable-encoder=pcm_s16le --enable-muxer=wav --enable-filter=loudnorm --enable-filter=aresample --enable-filter=replaygain --enable-filter=volume)" >> ffmpeg-build/common.sh 7 | RUN ffmpeg-build/build-linux.sh 8 | RUN mv ffmpeg-build/artifacts/ffmpeg-*-linux-gnu/bin/ffmpeg . 9 | 10 | FROM python:3.11-slim 11 | 12 | ENV WINEPREFIX="/wine32" 13 | ENV WINEARCH=win32 14 | ENV LOG_LEVEL= 15 | RUN dpkg --add-architecture i386 16 | RUN apt-get update && apt-get install -y wine32 wine:i386 --no-install-recommends 17 | RUN apt-get clean 18 | RUN /usr/bin/wine wineboot | true 19 | COPY --from=builder /root/ffmpeg /usr/bin/ffmpeg 20 | COPY psp_at3tool.exe . 21 | COPY requirements.txt . 22 | RUN pip install -r requirements.txt 23 | RUN mkdir /uploads 24 | COPY *.py ./ 25 | 26 | EXPOSE 5000 27 | ENTRYPOINT ["uvicorn"] 28 | CMD ["main:api", "--host", "0.0.0.0", "--port", "5000"] 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ATRAC Encode/Decode Server 2 | 3 | This is a basic [FastAPI](https://fastapi.tiangolo.com/) python application for running a windows executable atrac encoder as an API-driven service. It was originally designed for integration with [Web MiniDisc Pro](https://github.com/asivery/webminidisc). 4 | 5 | ## Installation 6 | 7 | No executables are provided in this repository. The current syntax of the script is designed to work with the ATRAC3 tool included with the Sony PSP SDK. Other executables such as [atracdenc](https://github.com/dcherednik/atracdenc) can be easily substituted 8 | 9 | To build atracapi as a docker container, provide an encoder executable in the root of this repository with the name `psp_at3tool.exe` and run `docker build .` 10 | 11 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import subprocess, logging, shutil 2 | from uuid import uuid4 3 | from fastapi import FastAPI, File, UploadFile, BackgroundTasks, HTTPException, Query 4 | from fastapi.responses import FileResponse, RedirectResponse 5 | from pathlib import Path 6 | from fastapi.middleware.cors import CORSMiddleware 7 | from tempfile import gettempdir, NamedTemporaryFile 8 | from utils import * 9 | from typing import Union 10 | 11 | api = FastAPI( 12 | title="ATRAC API" 13 | ) 14 | logger = logging.getLogger("uvicorn.info") 15 | @api.on_event("startup") 16 | async def startup_event(): 17 | api.add_middleware( 18 | CORSMiddleware, 19 | allow_origins=["*"], 20 | allow_credentials=True, 21 | allow_methods=["*"], 22 | allow_headers=["*"], 23 | ) 24 | subprocess.run(['/usr/bin/wineserver', '-p']) 25 | 26 | 27 | @api.get("/") 28 | async def root(): 29 | return RedirectResponse("/docs") 30 | 31 | @api.post('/encode') 32 | def encode_atrac(type: atracTypes, background_tasks: BackgroundTasks, file: UploadFile = File()): 33 | global logger 34 | filename = file.filename 35 | logger.info(f"Beginning encode for {filename}") 36 | with NamedTemporaryFile() as input: 37 | shutil.copyfileobj(file.file, input) 38 | output = do_encode(input.name, type, logger) 39 | background_tasks.add_task(remove_file, output, logger) 40 | return FileResponse(path=output, filename=Path(filename).stem + '.at3', media_type='audio/wav') 41 | 42 | @api.post('/transcode') 43 | def transcode_atrac(type: atracTypes, background_tasks: BackgroundTasks, applyReplaygain: bool = False, loudnessTarget: Union[float, None] = Query(default=None, ge=-70, le=-5), file: UploadFile = File()): 44 | global logger 45 | filename = file.filename 46 | logger.info(f"Beginning encode for {filename}") 47 | 48 | transcoderCommands = [] 49 | if loudnessTarget is not None: 50 | transcoderCommands.append(f'-filter_complex') 51 | transcoderCommands.append(f'-loudnorm=I={loudnessTarget}') 52 | elif applyReplaygain: 53 | transcoderCommands.append('-af') 54 | transcoderCommands.append('volume=replaygain=track') 55 | transcoderCommands += ['-ac', '2', '-ar', '44100', '-f', 'wav'] 56 | 57 | intermediary = Path(gettempdir(), str(uuid4())).absolute() 58 | with NamedTemporaryFile() as input: 59 | shutil.copyfileobj(file.file, input) 60 | logger.info("Starting ffmpeg...") 61 | transcoder = subprocess.run([ 62 | '/usr/bin/ffmpeg', '-i', 63 | Path(input.name), 64 | *transcoderCommands, 65 | intermediary], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 66 | logger.info(transcoder.stdout.decode('utf-8', errors='ignore')) 67 | 68 | logger.info("Starting at3tool...") 69 | output = do_encode(intermediary, type, logger) 70 | background_tasks.add_task(remove_file, output, logger) 71 | background_tasks.add_task(remove_file, intermediary, logger) 72 | return FileResponse(path=output, filename=Path(filename).stem + '.at3', media_type='audio/wav') 73 | 74 | @api.post('/decode') 75 | def decode_atrac(background_tasks: BackgroundTasks, file: UploadFile = File()): 76 | global logger 77 | filename = file.filename 78 | logger.info(f"Beginning decode for {filename}") 79 | output = Path(gettempdir(), str(uuid4())).absolute() 80 | with NamedTemporaryFile() as input: 81 | shutil.copyfileobj(file.file, input) 82 | encoder = subprocess.run(['/usr/bin/wine', 'psp_at3tool.exe', '-d', 83 | Path(input.name), 84 | output]) 85 | background_tasks.add_task(remove_file, output, logger) 86 | return FileResponse(path=output, filename=Path(filename).stem + '.wav', media_type='audio/wav') 87 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | anyio==3.6.2 2 | click==8.1.3 3 | fastapi==0.90.0 4 | h11==0.14.0 5 | httptools==0.5.0 6 | idna==3.4 7 | packaging==23.0 8 | pydantic==1.10.4 9 | python-dotenv==0.21.1 10 | python-multipart==0.0.5 11 | PyYAML==6.0 12 | six==1.16.0 13 | sniffio==1.3.0 14 | starlette==0.23.0 15 | typing_extensions==4.4.0 16 | uvicorn==0.20.0 17 | uvloop==0.17.0 18 | watchfiles==0.18.1 19 | websockets==10.4 20 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import os, shutil, subprocess 2 | from enum import Enum 3 | from tempfile import gettempdir, NamedTemporaryFile 4 | from pathlib import Path 5 | from uuid import uuid4 6 | 7 | 8 | class atracTypes(str, Enum): 9 | LP2 = 'LP2' 10 | LP4 = 'LP4' 11 | LP105 = 'LP105' 12 | PLUS48 = 'PLUS48' 13 | PLUS64 = 'PLUS64' 14 | PLUS96 = 'PLUS96' 15 | PLUS128 = 'PLUS128' 16 | PLUS160 = 'PLUS160' 17 | PLUS192 = 'PLUS192' 18 | PLUS256 = 'PLUS256' 19 | PLUS320 = 'PLUS320' 20 | PLUS352 = 'PLUS352' 21 | 22 | bitrates = { 23 | 'LP2': 132, 24 | 'LP4': 66, 25 | 'LP105': 105, 26 | 'PLUS48': 48, 27 | 'PLUS64': 64, 28 | 'PLUS96': 96, 29 | 'PLUS128': 128, 30 | 'PLUS160': 160, 31 | 'PLUS192': 192, 32 | 'PLUS256': 256, 33 | 'PLUS320': 320, 34 | 'PLUS352': 352 35 | } 36 | 37 | def allowed_file(filename): 38 | return '.' in filename and \ 39 | filename.rsplit('.', 1)[1].lower() in ['wav', 'at3'] 40 | 41 | 42 | def remove_file(filename, logger): 43 | os.remove(filename) 44 | logger.info(f"Removed {filename}") 45 | 46 | 47 | def do_encode(input, type, logger): 48 | output = Path(gettempdir(), str(uuid4())).absolute() 49 | subprocess.run(['/usr/bin/wine', 'psp_at3tool.exe', '-e', '-br', str(bitrates[type]), 50 | input, 51 | output]) 52 | return output 53 | --------------------------------------------------------------------------------