#!/usr/bin/env bash # conved – file conversion via conved.com from the command line (Linux, macOS; needs only bash and curl). # Documentation: https://www.conved.com/docs # # conved login store the API token (account at https://www.conved.com/account) # conved convert -t [-o ] # conved formats [] conved status conved get [-o ] conved me # # Token: env CONVED_TOKEN, otherwise the file ${XDG_CONFIG_HOME:-~/.config}/conved/token (CONVED_TOKEN_FILE). # stdout gets only the path of the result (for scripts), messages go to stderr. Exit codes: # 0 done, 1 usage, 2 conversion failed / unsupported, 3 limit reached, 4 bad token, 5 network / server, 6 timeout set -u -o pipefail VERSION="1.0.8" API="${CONVED_API:-https://www.conved.com/api/v1}" TOKEN_FILE="${CONVED_TOKEN_FILE:-${XDG_CONFIG_HOME:-$HOME/.config}/conved/token}" QUIET=0 say() { [ "$QUIET" = 1 ] || printf '%s\n' "$*" >&2; } die() { printf 'conved: %s\n' "$2" >&2; exit "$1"; } usage() { cat >&2 < -t [-o ] [--no-wait] [--timeout ] [-q] convert and download the result (prints its path) conved status [--wait ] job status conved get [-o ] download result of a finished job conved formats [] supported conversions (all, or targets for ) conved me account and limits conved version | help Examples: conved convert slides.ppt -t pdf conved convert report.docx -t pdf -o /var/www/report.pdf conved convert 'https://www.youtube.com/watch?v=3RmQTYLD398' -t mp3 Token: env CONVED_TOKEN, or file $TOKEN_FILE Exit codes: 0 done, 1 usage, 2 conversion failed/unsupported, 3 limit reached, 4 bad token, 5 network/server, 6 timeout HELP } command -v curl >/dev/null 2>&1 || die 5 "curl is required" token() { if [ -n "${CONVED_TOKEN:-}" ]; then printf '%s' "$CONVED_TOKEN"; return; fi [ -r "$TOKEN_FILE" ] || die 4 "no API token – run 'conved login' (create a token at https://www.conved.com/account)" tr -d ' \r\n' < "$TOKEN_FILE" } # value from a flat JSON API response (string or number); no jq/python needed json() { local v v=$(printf '%s' "$1" | sed -nE 's/.*"'"$2"'":"((\\.|[^"\\])*)".*/\1/p' | head -n1) [ -z "$v" ] && v=$(printf '%s' "$1" | sed -nE 's/.*"'"$2"'":(-?[0-9]+|true|false|null).*/\1/p' | head -n1) printf '%s' "$v" | sed -e 's#\\/#/#g' -e 's/\\"/"/g' -e 's/\\\\/\\/g' } # API call: api METHOD PATH [curl arguments…] → sets BODY and CODE api() { local method=$1 path=$2 out shift 2 # token goes to curl on stdin (printf is a shell builtin) – never on the command line, where `ps` would show it out=$(printf 'Authorization: Bearer %s\n' "$(token)" | curl -sS --proto =https,http --connect-timeout 15 -X "$method" -H @- \ -H "Accept: application/json" -H "User-Agent: conved-cli/$VERSION" -w $'\n%{http_code}' "$@" "$API$path") || die 5 "cannot reach $API" CODE=${out##*$'\n'} BODY=${out%$'\n'*} } # API error → message and exit code by error type api_fail() { local err msg err=$(json "$BODY" error); msg=$(json "$BODY" message) case "$err" in unauthorized) die 4 "${msg:-invalid token}" ;; limit_reached) die 3 "${msg:-limit reached}" ;; unsupported_format|unsupported_target|file_too_large|download_failed) die 2 "${msg:-$err}" ;; bad_request|method_not_allowed|not_found|upload_failed) die 1 "${msg:-$err}" ;; *) # not an API JSON response → rejected by the web server / firewall before reaching the application [ -z "$err" ] && [ "$CODE" = 403 -o "$CODE" = 500 -o "$CODE" = 413 ] && die 5 "rejected by the server firewall (HTTP $CODE) – contact info@conved.com" die 5 "server error (HTTP $CODE)${msg:+: $msg}" ;; esac } download() { # download local url out=$2 api GET "/jobs/$1" [ "$CODE" = 200 ] || api_fail url=$(json "$BODY" download_url) [ -n "$url" ] || die 2 "job $1 is not finished (status: $(json "$BODY" status))" if [ -z "$out" ]; then # name from the server: no directories, no leading dot or dash out=$(json "$BODY" name); out=${out##*/}; out=${out#[.-]} fi out=${out:-conved-$1} curl -fsS --proto =https --connect-timeout 15 -o "$out.part" "$url" || { rm -f "$out.part"; die 5 "download failed"; } mv -f "$out.part" "$out" say "saved $out" printf '%s\n' "$out" } cmd_login() { local t=${1:-} if [ -z "$t" ] && [ -t 0 ]; then printf 'API token (https://www.conved.com/account): ' >&2 read -rs t; echo >&2 elif [ -z "$t" ] || [ "$t" = - ]; then # from stdin (scripts): echo "$TOKEN" | conved login read -r t else say "note: a token on the command line is visible to other users (ps) – prefer 'conved login' or stdin" fi t=$(printf '%s' "$t" | tr -d ' \r\n') case "$t" in cvd_*) ;; *) die 1 "a token starts with cvd_" ;; esac CONVED_TOKEN=$t api GET /me [ "$CODE" = 200 ] || api_fail mkdir -p "$(dirname "$TOKEN_FILE")" ( umask 077; printf '%s\n' "$t" > "$TOKEN_FILE" ) say "signed in as $(json "$BODY" email), token saved to $TOKEN_FILE" } cmd_convert() { local src="" to="" out="" wait=1 timeout=900 id status started while [ $# -gt 0 ]; do case "$1" in -t|--to) to=${2:-}; shift ;; -o|--output) out=${2:-}; shift ;; --no-wait) wait=0 ;; --timeout) timeout=${2:-900}; shift ;; -q|--quiet) QUIET=1 ;; -*) die 1 "unknown option $1" ;; *) [ -z "$src" ] && src=$1 || die 1 "only one input file" ;; esac shift done [ -n "$src" ] && [ -n "$to" ] || { usage; exit 1; } to=$(printf '%s' "$to" | tr '[:upper:]' '[:lower:]') case "$src" in http://*|https://*) say "converting $src → $to"; api POST /convert --form-string "url=$src" --form-string "to=$to" -F "wait=40" ;; *) [ -f "$src" ] && [ -r "$src" ] || die 1 "file not found: $src" say "uploading $src → $to" api POST /convert -F "file=@\"$src\"" --form-string "to=$to" -F wait=40 [ -n "$out" ] || out="$(dirname "$src")/$(basename "${src%.*}").$to" ;; esac [ "$CODE" = 200 ] || [ "$CODE" = 202 ] || api_fail id=$(json "$BODY" id); status=$(json "$BODY" status) [ -n "$id" ] || die 5 "unexpected server response" if [ "$wait" = 0 ]; then printf '%s\n' "$id"; return; fi started=$(date +%s) while [ "$status" = waiting ]; do [ $(( $(date +%s) - started )) -lt "$timeout" ] || die 6 "timeout – the job $id is still running (conved status $id)" say "converting… (job $id)" api GET "/jobs/$id?wait=30" [ "$CODE" = 200 ] || api_fail status=$(json "$BODY" status) done [ "$status" = done ] || die 2 "conversion failed: $(json "$BODY" error)" download "$id" "$out" } cmd_status() { local id=${1:-} w=0 [ -n "$id" ] || die 1 "usage: conved status [--wait ]" [ "${2:-}" = "--wait" ] && w=${3:-30} api GET "/jobs/$id?wait=$w" [ "$CODE" = 200 ] || api_fail printf '%s\n' "$(json "$BODY" status)" } cmd_formats() { local list if [ -n "${1:-}" ]; then api GET "/formats?from=$1" [ "$CODE" = 200 ] || api_fail list=$(printf '%s' "$BODY" | sed -nE 's/.*:\[([^]]*)\].*/\1/p' | tr -d '"' | tr ',' ' ') [ -n "$list" ] || die 2 "format '$1' is not supported" printf '%s\n' "$list" else api GET /formats [ "$CODE" = 200 ] || api_fail printf '%s' "$BODY" | sed -E 's/^\{"formats":\{//; s/\}\}$//; s/\],"/]\n"/g' | sed -E 's/"([^"]+)":\[(.*)\]/\1: \2/; s/"//g; s/,/ /g' echo fi } case "${1:-help}" in login) shift; cmd_login "$@" ;; logout) rm -f "$TOKEN_FILE"; say "token removed" ;; convert|c) shift; cmd_convert "$@" ;; status) shift; cmd_status "$@" ;; get) shift; id=${1:-}; [ -n "$id" ] || die 1 "usage: conved get [-o ]"; [ "${2:-}" = "-o" ] && o=${3:-} || o=""; download "$id" "$o" ;; formats) shift; cmd_formats "$@" ;; me) api GET /me; [ "$CODE" = 200 ] || api_fail; printf '%s\n' "$BODY" ;; version|-v|--version) echo "conved $VERSION" ;; help|-h|--help) usage ;; *) usage; exit 1 ;; esac