generalize convert-to-mp3 into convert-audio with selectable target format
- rename task to convert-audio, support mp3/flac/wav/ogg/aac/opus targets - add options input type with selectable pill buttons in UI - extend seed script parser to handle YAML list properties (options) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,13 @@
|
|||||||
---
|
---
|
||||||
name: Convert To MP3
|
name: Convert Audio
|
||||||
description: Convert audio files to MP3 320kbps, preserving metadata.
|
description: Convert audio files between formats, preserving metadata.
|
||||||
version: 1
|
version: 1
|
||||||
mode: script
|
mode: script
|
||||||
language: bash
|
language: bash
|
||||||
triggers:
|
triggers:
|
||||||
- type: file
|
- type: file
|
||||||
extensions:
|
extensions:
|
||||||
|
- mp3
|
||||||
- flac
|
- flac
|
||||||
- wav
|
- wav
|
||||||
- ogg
|
- ogg
|
||||||
@@ -26,6 +27,17 @@ inputs:
|
|||||||
file_path:
|
file_path:
|
||||||
type: string
|
type: string
|
||||||
description: Path to an audio file or directory to convert.
|
description: Path to an audio file or directory to convert.
|
||||||
|
target_format:
|
||||||
|
type: string
|
||||||
|
description: Target format
|
||||||
|
default: mp3
|
||||||
|
options:
|
||||||
|
- mp3
|
||||||
|
- flac
|
||||||
|
- wav
|
||||||
|
- ogg
|
||||||
|
- aac
|
||||||
|
- opus
|
||||||
delete_source:
|
delete_source:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: Delete source files after successful conversion.
|
description: Delete source files after successful conversion.
|
||||||
@@ -33,7 +45,7 @@ inputs:
|
|||||||
args: [file_path]
|
args: [file_path]
|
||||||
---
|
---
|
||||||
|
|
||||||
# Convert To MP3
|
# Convert Audio
|
||||||
|
|
||||||
Convert audio files to MP3 320kbps using ffmpeg, preserving metadata.
|
Convert audio files between formats using ffmpeg, preserving metadata.
|
||||||
Supports single file conversion and batch directory conversion.
|
Supports single file conversion and batch directory conversion.
|
||||||
@@ -1,11 +1,26 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# ── Task: Convert To MP3 ──
|
# ── Task: Convert Audio ──
|
||||||
|
|
||||||
EXTENSIONS="flac|wav|ogg|wma|aac|m4a|opus|aiff|aif|ape|wv|alac|dsf|dff"
|
EXTENSIONS="mp3|flac|wav|ogg|wma|aac|m4a|opus|aiff|aif|ape|wv|alac|dsf|dff"
|
||||||
|
TARGET_FORMAT="${INPUT_TARGET_FORMAT:-mp3}"
|
||||||
DELETE_SOURCE="${INPUT_DELETE_SOURCE:-false}"
|
DELETE_SOURCE="${INPUT_DELETE_SOURCE:-false}"
|
||||||
|
|
||||||
|
ffmpeg_args_for_format() {
|
||||||
|
case "$1" in
|
||||||
|
mp3) echo "-codec:a libmp3lame -b:a 320k -id3v2_version 3" ;;
|
||||||
|
flac) echo "-codec:a flac" ;;
|
||||||
|
wav) echo "-codec:a pcm_s16le" ;;
|
||||||
|
ogg) echo "-codec:a libvorbis -q:a 6" ;;
|
||||||
|
aac) echo "-codec:a aac -b:a 256k" ;;
|
||||||
|
opus) echo "-codec:a libopus -b:a 192k" ;;
|
||||||
|
*) echo "Unsupported format: $1" >&2; return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
FFMPEG_ARGS=$(ffmpeg_args_for_format "$TARGET_FORMAT") || exit 1
|
||||||
|
|
||||||
process_file() {
|
process_file() {
|
||||||
local input="$1"
|
local input="$1"
|
||||||
local ext="${input##*.}"
|
local ext="${input##*.}"
|
||||||
@@ -13,10 +28,10 @@ process_file() {
|
|||||||
dir="$(dirname "$input")"
|
dir="$(dirname "$input")"
|
||||||
local base
|
local base
|
||||||
base="$(basename "$input" ".$ext")"
|
base="$(basename "$input" ".$ext")"
|
||||||
local output="$dir/$base.mp3"
|
local output="$dir/$base.$TARGET_FORMAT"
|
||||||
|
|
||||||
if [[ "${ext,,}" == "mp3" ]]; then
|
if [[ "${ext,,}" == "$TARGET_FORMAT" ]]; then
|
||||||
echo "Skipping (already MP3): $input"
|
echo "Skipping (already $TARGET_FORMAT): $input"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -26,7 +41,8 @@ process_file() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Converting: $input → $output"
|
echo "Converting: $input → $output"
|
||||||
if ffmpeg -nostdin -i "$input" -codec:a libmp3lame -b:a 320k -map_metadata 0 -id3v2_version 3 -y "$output" 2>/dev/null; then
|
# shellcheck disable=SC2086
|
||||||
|
if ffmpeg -nostdin -i "$input" $FFMPEG_ARGS -map_metadata 0 -y "$output" 2>/dev/null; then
|
||||||
echo " Done"
|
echo " Done"
|
||||||
if [[ "$DELETE_SOURCE" == "true" ]]; then
|
if [[ "$DELETE_SOURCE" == "true" ]]; then
|
||||||
rm "$input"
|
rm "$input"
|
||||||
@@ -48,24 +48,52 @@ function parseFrontmatter(content: string) {
|
|||||||
}
|
}
|
||||||
meta.trigger = triggers.length > 0 ? triggers : undefined;
|
meta.trigger = triggers.length > 0 ? triggers : undefined;
|
||||||
|
|
||||||
// Parse inputs (simplified — store as-is for now)
|
// Parse inputs
|
||||||
const inputsMatch = yaml.match(/^inputs:\s*\n((?:[ \t]+.+\n?)*)/m);
|
const inputsMatch = yaml.match(/^inputs:\s*\n((?:[ \t]+.+\n?)*)/m);
|
||||||
if (inputsMatch) {
|
if (inputsMatch) {
|
||||||
const inputBlock = inputsMatch[1]!;
|
const inputBlock = inputsMatch[1]!;
|
||||||
const inputEntries: Record<string, Record<string, string>> = {};
|
const inputEntries: Record<string, Record<string, string | string[]>> = {};
|
||||||
let currentInput: string | null = null;
|
let currentInput: string | null = null;
|
||||||
|
let currentListKey: string | null = null;
|
||||||
|
let currentList: string[] = [];
|
||||||
|
|
||||||
|
const flushList = () => {
|
||||||
|
if (currentInput && currentListKey && currentList.length > 0) {
|
||||||
|
inputEntries[currentInput]![currentListKey] = currentList;
|
||||||
|
}
|
||||||
|
currentListKey = null;
|
||||||
|
currentList = [];
|
||||||
|
};
|
||||||
|
|
||||||
for (const line of inputBlock.split('\n')) {
|
for (const line of inputBlock.split('\n')) {
|
||||||
const topMatch = line.match(/^\s{2}(\w[\w_-]*):\s*$/);
|
const topMatch = line.match(/^\s{2}(\w[\w_-]*):\s*$/);
|
||||||
if (topMatch) {
|
if (topMatch) {
|
||||||
|
flushList();
|
||||||
currentInput = topMatch[1]!;
|
currentInput = topMatch[1]!;
|
||||||
inputEntries[currentInput] = {};
|
inputEntries[currentInput] = {};
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const propMatch = line.match(/^\s{4}(\w[\w_-]*):\s*(.+)$/);
|
// List item (6 spaces + dash)
|
||||||
|
const listItemMatch = line.match(/^\s{6}-\s*(.+)$/);
|
||||||
|
if (listItemMatch && currentInput && currentListKey) {
|
||||||
|
currentList.push(listItemMatch[1]!.trim());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Property with value or start of list
|
||||||
|
const propMatch = line.match(/^\s{4}(\w[\w_-]*):\s*(.*)$/);
|
||||||
if (propMatch && currentInput) {
|
if (propMatch && currentInput) {
|
||||||
inputEntries[currentInput]![propMatch[1]!] = propMatch[2]!.trim();
|
flushList();
|
||||||
|
const value = propMatch[2]!.trim();
|
||||||
|
if (value === '') {
|
||||||
|
// Start of a list (e.g. "options:")
|
||||||
|
currentListKey = propMatch[1]!;
|
||||||
|
currentList = [];
|
||||||
|
} else {
|
||||||
|
inputEntries[currentInput]![propMatch[1]!] = value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
flushList();
|
||||||
if (Object.keys(inputEntries).length > 0) {
|
if (Object.keys(inputEntries).length > 0) {
|
||||||
meta.inputs = inputEntries;
|
meta.inputs = inputEntries;
|
||||||
}
|
}
|
||||||
|
|||||||
+21
@@ -198,6 +198,7 @@ type TaskInputDef = {
|
|||||||
type: string;
|
type: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
default?: string;
|
default?: string;
|
||||||
|
options?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type TaskInputFormProps = {
|
type TaskInputFormProps = {
|
||||||
@@ -239,6 +240,26 @@ const TaskInputForm = ({ inputDefs, values, onChange, autoFilledKeys }: TaskInpu
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (def.options && def.options.length > 0) {
|
||||||
|
const selected = values[key] ?? def.default ?? def.options[0]!;
|
||||||
|
return (
|
||||||
|
<div key={key} className="flex flex-col gap-1.5">
|
||||||
|
<span className="text-sm font-medium text-duck-dark dark:text-foreground">{def.description ?? key}</span>
|
||||||
|
<div className="flex flex-wrap gap-1.5">
|
||||||
|
{def.options.map((opt) => (
|
||||||
|
<button
|
||||||
|
key={opt}
|
||||||
|
onClick={() => onChange(key, opt)}
|
||||||
|
className={`px-3 py-1 text-xs font-medium rounded-md transition-colors cursor-pointer ${selected === opt ? 'bg-duck-teal text-white' : 'bg-duck-dark/5 dark:bg-foreground/5 text-duck-dark/60 dark:text-foreground/60 hover:text-duck-dark dark:hover:text-foreground hover:bg-duck-dark/10 dark:hover:bg-foreground/10'}`}
|
||||||
|
>
|
||||||
|
{opt}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Default: text input for string/number
|
// Default: text input for string/number
|
||||||
return (
|
return (
|
||||||
<div key={key} className="flex flex-col gap-1">
|
<div key={key} className="flex flex-col gap-1">
|
||||||
|
|||||||
Reference in New Issue
Block a user