We leverage some cool libraries:
$ ciel -s ffmpeg delux flac
[…]
TITLE : Cria de Favela
track : 12
TRACKTOTAL : 12
Duration: 00:02:51.60, start: 0.000000, bitrate: 954 kb/s
Stream #0:0: Audio: flac, 44100 Hz, stereo, s16
Stream #0:1: Video: mjpeg (Progressive), yuvj420p(pc, bt470bg/unknown/unknown), 1280x1280 [SAR 120:120 DAR 1:1], 90k tbr, 90k tbn, 90k tbc (attached pic)
Metadata:
comment : Cover (front)
title : D:\Música em alta resolução\Album\Criolo\[2017] Espiral de Ilusão - Deluxe Edition\Espiral de Ilusão - Deluxe Edition.jpg
Stream mapping:
Stream #0:1 -> #0:0 (mjpeg (native) -> png (native))
Stream #0:0 -> #0:1 (flac (native) -> mp3 (libmp3lame))
Press [q] to stop, [?] for help
done for files:
"/home/vince/zique/[2017] Espiral de Ilusão - Deluxe Edition/01 - Criolo - Lá Vem Você.flac"
"/home/vince/zique/[2017] Espiral de Ilusão - Deluxe Edition/02 - Criolo - Dilúvio de Solidão.flac"
"/home/vince/zique/[2017] Espiral de Ilusão - Deluxe Edition/03 - Criolo - Menino Mimado.flac"
Easy!
We find files with file-finder:
(finder:finder*
:root root
;; This "and"s the params:
:predicates (apply #'finder:every-path~ params)
;; This would do a "or":
;; :predicates (apply #'finder:path~ params)
)
This searches for files recursively on a root directory and it "ands" our list of search terms.
We could be more precise in asking it to check that "flac" is the file extension, but it's ok for us here.
This gives us a list of "file-finder:file" objects, actually not lisp pathnames, not strings. To get file names as strings, we use finder:path file
.
To get a file name with the mp3 extension, we simply replace in a string:
(let ((extension (pathname-type file)))
(when extension
(values
(str:replace-all extension "mp3" file)
extension)))
However, TIL. We have directory names that give the year in between […]
characters, and uiop:file-exists-p
chokes at it (it returns nil). We escape those characters:
(defun escape-file-name (name)
"Escape [ and ] with double \\,
otherwise uiop:file-exists-p returns NIL for an existing file."
;; This works on upstream file-finder <2025-09-09>
;; (when (finder:file? name)
;; (setf name (finder:path name)))
(str:replace-using '("[" "\\["
"]" "\\]")
name))
(edit) OK, those are wildcard characters. This pattern correctly returns T when a file exists:
(probe-file (make-pathname :name name :type extension))
so I might adapt the script.
And that's pretty it. We run ffmpeg
with
(uiop:run-program (list "ffmpeg"
"-i"
(finder:path file)
target)
:output :interactive
:error-output t)
Here's the full listing.
Find it also on https://github.com/ciel-lang/CIEL/discussions/89 and in CIEL's scripts directory.
- https://github.com/ciel-lang/CIEL/ - a batteries-included Common Lisp. Here we only rely on the built-in file-finder library, and cl-str. And the easy way to run a script from the terminal.
(in-package :ciel-user)
;;;
;;; Search files and transform them to mp3 with ffmpeg.
;;;
;;; Usage:
;;;
;;; ciel src/scripts/ffmpeg search terms
;;;
;;; Todo:
;;; - choose search directories (do we even search on the current directory?)
;;; - choose output format, etc.
;;;
;;; TIL: we need to escape file names if they contain characters such as [ ].
;;;
(defparameter *directories* '("~/Music/" "~/Downloads/" "~/zique/"))
(defvar *music-type-extensions* '("aac" "ac3" "aiff" "amr" "ape" "dts" "f4a" "f4b" "flac" "gsm"
"m3u" "m4a" "midi" "mlp" "mka" "mp2" "mp3" "oga" "ogg" "opus" "pva"
"ra" "ram" "raw" "rf64" "spx" "tta" "wav" "wavpack" "wma" "wv")
"A bunch of audio extensions. Should be supported by ffmpeg. Thanks ready-player.el.")
(defun music-file-p (file)
"Return non-NIL if this file (pathname) has a music file extension as of *music-type-extensions*."
(find (pathname-type file) *music-type-extensions* :test #'equalp))
(defun find-on-directory (root params)
"Search on default directories.
PARAMS (list)"
(finder:finder*
:root root
;; This "and"s the params:
:predicates (apply #'finder:every-path~ params)
;; This would do a "or":
;; :predicates (apply #'finder:path~ params)
))
(defun find-files (&optional params)
"Find files matching PARAMS (a list of strings) on the default directories.
PARAMS is an 'OR'. I would prefer to 'and' the matches actually (see find-on-directory)."
(unless params
(format *error-output* "No search terms supplied.~&Usage: finder.lisp search terms.~&")
(return-from find-files))
(let ((str:*ignore-case* t)
(params (ensure-list params)))
(flatten
(loop for root in *directories*
collect
(find-on-directory root params)))))
(defun pprint-for-shell (list)
"Pretty-print this list of files (with full path), one per line."
(mapcar (lambda (p)
(format t "~s~&" (finder:path p)))
list)
(terpri))
(defun change-extension (file)
(when (finder:file? file)
(setf file (finder:path file)))
(let ((extension (pathname-type file)))
(when extension
(values
(str:replace-all extension "mp3" file)
extension))))
;; warn!
(defun escape-file-name (name)
"Escape [ and ] with double \\,
otherwise uiop:file-exists-p returns NIL for an existing file."
;; This works on upstream file-finder <2025-09-09>
;; (when (finder:file? name)
;; (setf name (finder:path name)))
(str:replace-using '("[" "\\["
"]" "\\]")
name))
(defun run-ffmpeg (file)
"Run ffmpeg on FILE, transform to mp3."
(let ((target (change-extension file)))
(if (uiop:file-exists-p (escape-file-name target))
(progn
(format t "~&mp3 already exists: ~a~&" target)
target)
(uiop:run-program (list "ffmpeg"
"-i"
(finder:path file)
target)
:output :interactive
:error-output t))))
(defun ffmpeg-on-files (files)
"Transform files to mp3 with ffmpeg.
Search on the default directories by AND-ing the search terms."
(loop for file in files
for path = (finder:path file)
when (and (music-file-p path)
(uiop:file-exists-p (escape-file-name path)))
do (format t "~&transforming: ~a~&" file)
(run-ffmpeg file)
and collect file into processed
finally
(format t "~&~%done for files: ~&")
(pprint-for-shell processed)))
#+ciel
(ffmpeg-on-files (find-files (rest *script-args*)))
Top comments (1)
oh, we also use
flatten
from Alexandria.