From: Mark Wooding Date: Sun, 14 Feb 2016 02:20:26 +0000 (+0000) Subject: Update automatically managed build utilities. X-Git-Url: https://git.distorted.org.uk/~mdw/autoys/commitdiff_plain/18c464e896e1960bda77c3454512a75876084c6c?hp=479773e6779f387298e36a1804cf39d5a5404c92 Update automatically managed build utilities. --- diff --git a/.gitignore b/.gitignore index f9d77cc..61708fd 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ /autom4te.cache/ /config/ /configure +/gremlin/Makefile.in diff --git a/Makefile.am b/Makefile.am index 5b4a513..c5cc0b8 100644 --- a/Makefile.am +++ b/Makefile.am @@ -30,6 +30,8 @@ SUBDIRS = ###-------------------------------------------------------------------------- ### Subdirectories. +SUBDIRS += gremlin + ###-------------------------------------------------------------------------- ### Release tweaking. @@ -37,9 +39,6 @@ SUBDIRS = dist-hook:: echo $(VERSION) >$(distdir)/RELEASE -## Bodge for now. -EXTRA_DIST += gremlin/gremlin - ## Additional build tools. EXTRA_DIST += config/auto-version EXTRA_DIST += config/confsubst @@ -54,4 +53,6 @@ EXTRA_DIST += debian/copyright EXTRA_DIST += debian/compat EXTRA_DIST += debian/source/format +EXTRA_DIST += debian/gremlin.install + ###----- That's all, folks -------------------------------------------------- diff --git a/Makefile.in b/Makefile.in index 44590ef..6adb43f 100644 --- a/Makefile.in +++ b/Makefile.in @@ -313,9 +313,9 @@ top_srcdir = @top_srcdir@ ###-------------------------------------------------------------------------- ### Debian. -EXTRA_DIST = gremlin/gremlin config/auto-version config/confsubst \ - debian/rules debian/control debian/changelog debian/copyright \ - debian/compat debian/source/format +EXTRA_DIST = config/auto-version config/confsubst debian/rules \ + debian/control debian/changelog debian/copyright debian/compat \ + debian/source/format debian/gremlin.install CLEANFILES = DISTCLEANFILES = MAINTAINERCLEANFILES = @@ -336,7 +336,10 @@ V_SUBST = $(V_SUBST_$(V)) V_SUBST_ = $(V_SUBST_$(AM_DEFAULT_VERBOSITY)) V_SUBST_0 = @echo " SUBST $@"; SUBST = $(V_SUBST)$(confsubst) -SUBDIRS = + +###-------------------------------------------------------------------------- +### Subdirectories. +SUBDIRS = gremlin all: all-recursive .SUFFIXES: @@ -910,9 +913,6 @@ uninstall-am: uninstall-binPROGRAMS uninstall-binSCRIPTS ###----- That's all, folks -------------------------------------------------- ###-------------------------------------------------------------------------- -### Subdirectories. - -###-------------------------------------------------------------------------- ### Release tweaking. dist-hook:: diff --git a/configure b/configure index 5250c7e..b6e6350 100755 --- a/configure +++ b/configure @@ -1,6 +1,6 @@ #! /bin/sh # Guess values for system-dependent variables and create Makefiles. -# Generated by GNU Autoconf 2.69 for autoys 0.1.0-pre0. +# Generated by GNU Autoconf 2.69 for autoys 0.1.0. # # Report bugs to . # @@ -579,12 +579,12 @@ MAKEFLAGS= # Identity of this package. PACKAGE_NAME='autoys' PACKAGE_TARNAME='autoys' -PACKAGE_VERSION='0.1.0-pre0' -PACKAGE_STRING='autoys 0.1.0-pre0' +PACKAGE_VERSION='0.1.0' +PACKAGE_STRING='autoys 0.1.0' PACKAGE_BUGREPORT='mdw@distorted.org.uk' PACKAGE_URL='' -ac_unique_file="gremlin/gremlin" +ac_unique_file="gremlin/gremlin.in" ac_subst_vars='am__EXEEXT_FALSE am__EXEEXT_TRUE LTLIBOBJS @@ -1241,7 +1241,7 @@ if test "$ac_init_help" = "long"; then # Omit some internal or obsolete options to make the list less imposing. # This message is too long to be a string in the A/UX 3.1 sh. cat <<_ACEOF -\`configure' configures autoys 0.1.0-pre0 to adapt to many kinds of systems. +\`configure' configures autoys 0.1.0 to adapt to many kinds of systems. Usage: $0 [OPTION]... [VAR=VALUE]... @@ -1307,7 +1307,7 @@ fi if test -n "$ac_init_help"; then case $ac_init_help in - short | recursive ) echo "Configuration of autoys 0.1.0-pre0:";; + short | recursive ) echo "Configuration of autoys 0.1.0:";; esac cat <<\_ACEOF @@ -1397,7 +1397,7 @@ fi test -n "$ac_init_help" && exit $ac_status if $ac_init_version; then cat <<\_ACEOF -autoys configure 0.1.0-pre0 +autoys configure 0.1.0 generated by GNU Autoconf 2.69 Copyright (C) 2012 Free Software Foundation, Inc. @@ -1452,7 +1452,7 @@ cat >config.log <<_ACEOF This file contains any messages produced by compilers while running configure, to aid debugging if configure makes a mistake. -It was created by autoys $as_me 0.1.0-pre0, which was +It was created by autoys $as_me 0.1.0, which was generated by GNU Autoconf 2.69. Invocation command line was $ $0 $@ @@ -2268,7 +2268,7 @@ fi # Define the identity of the package. PACKAGE='autoys' - VERSION='0.1.0-pre0' + VERSION='0.1.0' cat >>confdefs.h <<_ACEOF @@ -3695,7 +3695,7 @@ fi -ac_config_files="$ac_config_files Makefile" +ac_config_files="$ac_config_files Makefile gremlin/Makefile" cat >confcache <<\_ACEOF # This file is a shell script that caches the results of configure @@ -4263,7 +4263,7 @@ cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 # report actual input values of CONFIG_FILES etc. instead of their # values after options handling. ac_log=" -This file was extended by autoys $as_me 0.1.0-pre0, which was +This file was extended by autoys $as_me 0.1.0, which was generated by GNU Autoconf 2.69. Invocation command line was CONFIG_FILES = $CONFIG_FILES @@ -4320,7 +4320,7 @@ _ACEOF cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 ac_cs_config="`$as_echo "$ac_configure_args" | sed 's/^ //; s/[\\""\`\$]/\\\\&/g'`" ac_cs_version="\\ -autoys config.status 0.1.0-pre0 +autoys config.status 0.1.0 configured by $0, generated by GNU Autoconf 2.69, with options \\"\$ac_cs_config\\" @@ -4440,6 +4440,7 @@ do case $ac_config_target in "depfiles") CONFIG_COMMANDS="$CONFIG_COMMANDS depfiles" ;; "Makefile") CONFIG_FILES="$CONFIG_FILES Makefile" ;; + "gremlin/Makefile") CONFIG_FILES="$CONFIG_FILES gremlin/Makefile" ;; *) as_fn_error $? "invalid argument: \`$ac_config_target'" "$LINENO" 5;; esac diff --git a/configure.ac b/configure.ac index 16813ce..b20a56d 100644 --- a/configure.ac +++ b/configure.ac @@ -28,7 +28,7 @@ dnl Initialization. mdw_AUTO_VERSION AC_INIT([autoys], AUTO_VERSION, [mdw@distorted.org.uk]) -AC_CONFIG_SRCDIR([gremlin/gremlin]) +AC_CONFIG_SRCDIR([gremlin/gremlin.in]) AC_CONFIG_AUX_DIR([config]) AM_INIT_AUTOMAKE([foreign]) mdw_SILENT_RULES @@ -64,7 +64,9 @@ AM_CONDITIONAL([HAVE_BASH], [test $have_bash = yes]) dnl-------------------------------------------------------------------------- dnl Output. -AC_CONFIG_FILES([Makefile]) +AC_CONFIG_FILES( + [Makefile] + [gremlin/Makefile]) AC_OUTPUT dnl----- That's all, folks -------------------------------------------------- diff --git a/debian/changelog b/debian/changelog index 9152fd0..f43df84 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +autoys (0.1.0) experimental; urgency=low + + * gremlin: A batch audio conversion tool. + + -- Mark Wooding Sun, 14 Feb 2016 02:01:20 +0000 + autoys (0.1.0~pre0) experimental; urgency=low * Preliminary prerelease. diff --git a/debian/control b/debian/control index c1edd5e..9181280 100644 --- a/debian/control +++ b/debian/control @@ -8,4 +8,34 @@ Standards-Version: 3.1.1 Package: autoys Architecture: all Section: sound +Depends: + gremlin Description: A convenience package which depends on the other `autoys' packages. + +Package: gremlin +Architecture: all +Section: sound +Depends: ${python:Depends}, + python-pyparsing, + python-gst0.10, python-gobject-2, + python-eyed3, python-imaging +Description: Maintain converted trees of audio files. + The `gremlin' program converts audio files in an input `master' directory + tree, which presumably contains high-quality (ideally lossless) encodings of + interesting audio, writing corresponding converted files to a collection of + output directory trees. It's non-interactive, idempotent, and restartable; + it never modifies its master tree. It's exactly the sort of thing you want + to install as a daily cron job. + . + The gremlin reads a configuration file which describes the conversion policy + for each of the output trees. The policy can say things like: copy MP3 + files up to 160kb/s, or Ogg Vorbis files up to 128kb/s; and convert + everything else to 128kb/s Ogg Vorbis. + . + The gremlin can also convert image files, such as cover art. + . + Input files can be anything which GStreamer and/or the Python Imaging + Library can understand; output files are more constrained, because the + gremlin has to be able to understand their relevant properties. The + currently supported audio formats are Ogg Vorbis and MP3; image formats are + JPEG, PNG, and BMP. diff --git a/debian/gremlin.install b/debian/gremlin.install new file mode 100644 index 0000000..df1f8d9 --- /dev/null +++ b/debian/gremlin.install @@ -0,0 +1,2 @@ +usr/bin/gremlin +usr/share/man/man1/gremlin.1 diff --git a/gremlin/Makefile.am b/gremlin/Makefile.am new file mode 100644 index 0000000..46fb7d4 --- /dev/null +++ b/gremlin/Makefile.am @@ -0,0 +1,45 @@ +### -*-makefile-*- +### +### Build script for the conversion gremlin +### +### (c) 2016 Mark Wooding +### + +###----- Licensing notice --------------------------------------------------- +### +### This file is part of the `autoys' audio tools collection. +### +### `autoys' is free software; you can redistribute it and/or modify +### it under the terms of the GNU General Public License as published by +### the Free Software Foundation; either version 2 of the License, or +### (at your option) any later version. +### +### `autoys' is distributed in the hope that it will be useful, +### but WITHOUT ANY WARRANTY; without even the implied warranty of +### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +### GNU General Public License for more details. +### +### You should have received a copy of the GNU General Public License +### along with `autoys'; if not, write to the Free Software Foundation, +### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +include $(top_srcdir)/vars.am + +###-------------------------------------------------------------------------- +### The gremlin. + +if HAVE_PYTHON + +bin_SCRIPTS += gremlin +CLEANFILES += gremlin +EXTRA_DIST += gremlin.in + +gremlin: gremlin.in + $(SUBST) $(srcdir)/gremlin.in >$@.new $(SUBSTITUTIONS) && \ + chmod +x $@.new && mv $@.new $@ + +dist_man_MANS += gremlin.1 + +endif + +###----- That's all, folks -------------------------------------------------- diff --git a/gremlin/Makefile.in b/gremlin/Makefile.in new file mode 100644 index 0000000..932b82a --- /dev/null +++ b/gremlin/Makefile.in @@ -0,0 +1,623 @@ +# Makefile.in generated by automake 1.11.6 from Makefile.am. +# @configure_input@ + +# Copyright (C) 1994, 1995, 1996, 1997, 1998, 1999, 2000, 2001, 2002, +# 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011 Free Software +# Foundation, Inc. +# This Makefile.in is free software; the Free Software Foundation +# gives unlimited permission to copy and/or distribute it, +# with or without modifications, as long as this notice is preserved. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY, to the extent permitted by law; without +# even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. + +@SET_MAKE@ + +### -*-makefile-*- +### +### Build script for the conversion gremlin +### +### (c) 2016 Mark Wooding +### + +###----- Licensing notice --------------------------------------------------- +### +### This file is part of the `autoys' audio tools collection. +### +### `autoys' is free software; you can redistribute it and/or modify +### it under the terms of the GNU General Public License as published by +### the Free Software Foundation; either version 2 of the License, or +### (at your option) any later version. +### +### `autoys' is distributed in the hope that it will be useful, +### but WITHOUT ANY WARRANTY; without even the implied warranty of +### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +### GNU General Public License for more details. +### +### You should have received a copy of the GNU General Public License +### along with `autoys'; if not, write to the Free Software Foundation, +### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +### -*-makefile-*- +### +### Common definitions +### +### (c) 2016 Mark Wooding +### + +###----- Licensing notice --------------------------------------------------- +### +### This file is part of the `autoys' audio tools collection. +### +### `autoys' is free software; you can redistribute it and/or modify +### it under the terms of the GNU General Public License as published by +### the Free Software Foundation; either version 2 of the License, or +### (at your option) any later version. +### +### `autoys' is distributed in the hope that it will be useful, +### but WITHOUT ANY WARRANTY; without even the implied warranty of +### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +### GNU General Public License for more details. +### +### You should have received a copy of the GNU General Public License +### along with `autoys'; if not, write to the Free Software Foundation, +### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +###-------------------------------------------------------------------------- +### Initial values of common variables. + + +VPATH = @srcdir@ +am__make_dryrun = \ + { \ + am__dry=no; \ + case $$MAKEFLAGS in \ + *\\[\ \ ]*) \ + echo 'am--echo: ; @echo "AM" OK' | $(MAKE) -f - 2>/dev/null \ + | grep '^AM OK$$' >/dev/null || am__dry=yes;; \ + *) \ + for am__flg in $$MAKEFLAGS; do \ + case $$am__flg in \ + *=*|--*) ;; \ + *n*) am__dry=yes; break;; \ + esac; \ + done;; \ + esac; \ + test $$am__dry = yes; \ + } +pkgdatadir = $(datadir)/@PACKAGE@ +pkgincludedir = $(includedir)/@PACKAGE@ +pkglibdir = $(libdir)/@PACKAGE@ +pkglibexecdir = $(libexecdir)/@PACKAGE@ +am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd +install_sh_DATA = $(install_sh) -c -m 644 +install_sh_PROGRAM = $(install_sh) -c +install_sh_SCRIPT = $(install_sh) -c +INSTALL_HEADER = $(INSTALL_DATA) +transform = $(program_transform_name) +NORMAL_INSTALL = : +PRE_INSTALL = : +POST_INSTALL = : +NORMAL_UNINSTALL = : +PRE_UNINSTALL = : +POST_UNINSTALL = : +DIST_COMMON = $(dist_man_MANS) $(srcdir)/Makefile.am \ + $(srcdir)/Makefile.in $(top_srcdir)/vars.am +bin_PROGRAMS = + +###-------------------------------------------------------------------------- +### The gremlin. +@HAVE_PYTHON_TRUE@am__append_1 = gremlin +@HAVE_PYTHON_TRUE@am__append_2 = gremlin +@HAVE_PYTHON_TRUE@am__append_3 = gremlin.in +@HAVE_PYTHON_TRUE@am__append_4 = gremlin.1 +subdir = gremlin +ACLOCAL_M4 = $(top_srcdir)/aclocal.m4 +am__aclocal_m4_deps = $(top_srcdir)/configure.ac +am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \ + $(ACLOCAL_M4) +mkinstalldirs = $(install_sh) -d +CONFIG_CLEAN_FILES = +CONFIG_CLEAN_VPATH_FILES = +am__installdirs = "$(DESTDIR)$(bindir)" "$(DESTDIR)$(bindir)" \ + "$(DESTDIR)$(man1dir)" +PROGRAMS = $(bin_PROGRAMS) +am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`; +am__vpath_adj = case $$p in \ + $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \ + *) f=$$p;; \ + esac; +am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`; +am__install_max = 40 +am__nobase_strip_setup = \ + srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'` +am__nobase_strip = \ + for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||" +am__nobase_list = $(am__nobase_strip_setup); \ + for p in $$list; do echo "$$p $$p"; done | \ + sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \ + $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \ + if (++n[$$2] == $(am__install_max)) \ + { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \ + END { for (dir in files) print dir, files[dir] }' +am__base_list = \ + sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \ + sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g' +am__uninstall_files_from_dir = { \ + test -z "$$files" \ + || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \ + || { echo " ( cd '$$dir' && rm -f" $$files ")"; \ + $(am__cd) "$$dir" && rm -f $$files; }; \ + } +SCRIPTS = $(bin_SCRIPTS) +AM_V_GEN = $(am__v_GEN_@AM_V@) +am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@) +am__v_GEN_0 = @echo " GEN " $@; +AM_V_at = $(am__v_at_@AM_V@) +am__v_at_ = $(am__v_at_@AM_DEFAULT_V@) +am__v_at_0 = @ +SOURCES = +DIST_SOURCES = +am__can_run_installinfo = \ + case $$AM_UPDATE_INFO_DIR in \ + n|no|NO) false;; \ + *) (install-info --version) >/dev/null 2>&1;; \ + esac +man1dir = $(mandir)/man1 +NROFF = nroff +MANS = $(dist_man_MANS) +DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST) +ACLOCAL = @ACLOCAL@ +AMTAR = @AMTAR@ +AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@ +AUTOCONF = @AUTOCONF@ +AUTOHEADER = @AUTOHEADER@ +AUTOMAKE = @AUTOMAKE@ +AWK = @AWK@ +BASH = @BASH@ +CC = @CC@ +CCDEPMODE = @CCDEPMODE@ +CFLAGS = @CFLAGS@ +CPPFLAGS = @CPPFLAGS@ +CYGPATH_W = @CYGPATH_W@ +DEFS = @DEFS@ +DEPDIR = @DEPDIR@ +ECHO_C = @ECHO_C@ +ECHO_N = @ECHO_N@ +ECHO_T = @ECHO_T@ +EXEEXT = @EXEEXT@ +INSTALL = @INSTALL@ +INSTALL_DATA = @INSTALL_DATA@ +INSTALL_PROGRAM = @INSTALL_PROGRAM@ +INSTALL_SCRIPT = @INSTALL_SCRIPT@ +INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@ +LDFLAGS = @LDFLAGS@ +LIBOBJS = @LIBOBJS@ +LIBS = @LIBS@ +LTLIBOBJS = @LTLIBOBJS@ +MAKEINFO = @MAKEINFO@ +MKDIR_P = @MKDIR_P@ +OBJEXT = @OBJEXT@ +PACKAGE = @PACKAGE@ +PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@ +PACKAGE_NAME = @PACKAGE_NAME@ +PACKAGE_STRING = @PACKAGE_STRING@ +PACKAGE_TARNAME = @PACKAGE_TARNAME@ +PACKAGE_URL = @PACKAGE_URL@ +PACKAGE_VERSION = @PACKAGE_VERSION@ +PATH_SEPARATOR = @PATH_SEPARATOR@ +PYTHON = @PYTHON@ +PYTHON_EXEC_PREFIX = @PYTHON_EXEC_PREFIX@ +PYTHON_PLATFORM = @PYTHON_PLATFORM@ +PYTHON_PREFIX = @PYTHON_PREFIX@ +PYTHON_VERSION = @PYTHON_VERSION@ +SET_MAKE = @SET_MAKE@ +SHELL = @SHELL@ +STRIP = @STRIP@ +VERSION = @VERSION@ +abs_builddir = @abs_builddir@ +abs_srcdir = @abs_srcdir@ +abs_top_builddir = @abs_top_builddir@ +abs_top_srcdir = @abs_top_srcdir@ +ac_ct_CC = @ac_ct_CC@ +am__include = @am__include@ +am__leading_dot = @am__leading_dot@ +am__quote = @am__quote@ +am__tar = @am__tar@ +am__untar = @am__untar@ +bindir = @bindir@ +build_alias = @build_alias@ +builddir = @builddir@ +datadir = @datadir@ +datarootdir = @datarootdir@ +docdir = @docdir@ +dvidir = @dvidir@ +exec_prefix = @exec_prefix@ +host_alias = @host_alias@ +htmldir = @htmldir@ +includedir = @includedir@ +infodir = @infodir@ +install_sh = @install_sh@ +libdir = @libdir@ +libexecdir = @libexecdir@ +localedir = @localedir@ +localstatedir = @localstatedir@ +mandir = @mandir@ +mkdir_p = @mkdir_p@ +oldincludedir = @oldincludedir@ +pdfdir = @pdfdir@ +pkgpyexecdir = @pkgpyexecdir@ +pkgpythondir = @pkgpythondir@ +prefix = @prefix@ +program_transform_name = @program_transform_name@ +psdir = @psdir@ +pyexecdir = @pyexecdir@ +pythondir = @pythondir@ +sbindir = @sbindir@ +sharedstatedir = @sharedstatedir@ +srcdir = @srcdir@ +sysconfdir = @sysconfdir@ +target_alias = @target_alias@ +top_build_prefix = @top_build_prefix@ +top_builddir = @top_builddir@ +top_srcdir = @top_srcdir@ +EXTRA_DIST = $(am__append_3) +CLEANFILES = $(am__append_2) +DISTCLEANFILES = +MAINTAINERCLEANFILES = +bin_SCRIPTS = $(am__append_1) +dist_man_MANS = $(am__append_4) + +###-------------------------------------------------------------------------- +### Standard configuration substitutions. +confsubst = $(top_srcdir)/config/confsubst +SUBSTITUTIONS = \ + prefix=$(prefix) exec_prefix=$(exec_prefix) \ + libdir=$(libdir) includedir=$(includedir) \ + bindir=$(bindir) sbindir=$(sbindir) \ + PACKAGE=$(PACKAGE) VERSION=$(VERSION) \ + PYTHON=$(PYTHON) BASH=$(BASH) + +V_SUBST = $(V_SUBST_$(V)) +V_SUBST_ = $(V_SUBST_$(AM_DEFAULT_VERBOSITY)) +V_SUBST_0 = @echo " SUBST $@"; +SUBST = $(V_SUBST)$(confsubst) +all: all-am + +.SUFFIXES: +$(srcdir)/Makefile.in: $(srcdir)/Makefile.am $(top_srcdir)/vars.am $(am__configure_deps) + @for dep in $?; do \ + case '$(am__configure_deps)' in \ + *$$dep*) \ + ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \ + && { if test -f $@; then exit 0; else break; fi; }; \ + exit 1;; \ + esac; \ + done; \ + echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign gremlin/Makefile'; \ + $(am__cd) $(top_srcdir) && \ + $(AUTOMAKE) --foreign gremlin/Makefile +.PRECIOUS: Makefile +Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status + @case '$?' in \ + *config.status*) \ + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \ + *) \ + echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__depfiles_maybe)'; \ + cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__depfiles_maybe);; \ + esac; +$(top_srcdir)/vars.am: + +$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES) + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh + +$(top_srcdir)/configure: $(am__configure_deps) + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh +$(ACLOCAL_M4): $(am__aclocal_m4_deps) + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh +$(am__aclocal_m4_deps): +install-binPROGRAMS: $(bin_PROGRAMS) + @$(NORMAL_INSTALL) + @list='$(bin_PROGRAMS)'; test -n "$(bindir)" || list=; \ + if test -n "$$list"; then \ + echo " $(MKDIR_P) '$(DESTDIR)$(bindir)'"; \ + $(MKDIR_P) "$(DESTDIR)$(bindir)" || exit 1; \ + fi; \ + for p in $$list; do echo "$$p $$p"; done | \ + sed 's/$(EXEEXT)$$//' | \ + while read p p1; do if test -f $$p; \ + then echo "$$p"; echo "$$p"; else :; fi; \ + done | \ + sed -e 'p;s,.*/,,;n;h' -e 's|.*|.|' \ + -e 'p;x;s,.*/,,;s/$(EXEEXT)$$//;$(transform);s/$$/$(EXEEXT)/' | \ + sed 'N;N;N;s,\n, ,g' | \ + $(AWK) 'BEGIN { files["."] = ""; dirs["."] = 1 } \ + { d=$$3; if (dirs[d] != 1) { print "d", d; dirs[d] = 1 } \ + if ($$2 == $$4) files[d] = files[d] " " $$1; \ + else { print "f", $$3 "/" $$4, $$1; } } \ + END { for (d in files) print "f", d, files[d] }' | \ + while read type dir files; do \ + if test "$$dir" = .; then dir=; else dir=/$$dir; fi; \ + test -z "$$files" || { \ + echo " $(INSTALL_PROGRAM_ENV) $(INSTALL_PROGRAM) $$files '$(DESTDIR)$(bindir)$$dir'"; \ + $(INSTALL_PROGRAM_ENV) $(INSTALL_PROGRAM) $$files "$(DESTDIR)$(bindir)$$dir" || exit $$?; \ + } \ + ; done + +uninstall-binPROGRAMS: + @$(NORMAL_UNINSTALL) + @list='$(bin_PROGRAMS)'; test -n "$(bindir)" || list=; \ + files=`for p in $$list; do echo "$$p"; done | \ + sed -e 'h;s,^.*/,,;s/$(EXEEXT)$$//;$(transform)' \ + -e 's/$$/$(EXEEXT)/' `; \ + test -n "$$list" || exit 0; \ + echo " ( cd '$(DESTDIR)$(bindir)' && rm -f" $$files ")"; \ + cd "$(DESTDIR)$(bindir)" && rm -f $$files + +clean-binPROGRAMS: + -test -z "$(bin_PROGRAMS)" || rm -f $(bin_PROGRAMS) +install-binSCRIPTS: $(bin_SCRIPTS) + @$(NORMAL_INSTALL) + @list='$(bin_SCRIPTS)'; test -n "$(bindir)" || list=; \ + if test -n "$$list"; then \ + echo " $(MKDIR_P) '$(DESTDIR)$(bindir)'"; \ + $(MKDIR_P) "$(DESTDIR)$(bindir)" || exit 1; \ + fi; \ + for p in $$list; do \ + if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \ + if test -f "$$d$$p"; then echo "$$d$$p"; echo "$$p"; else :; fi; \ + done | \ + sed -e 'p;s,.*/,,;n' \ + -e 'h;s|.*|.|' \ + -e 'p;x;s,.*/,,;$(transform)' | sed 'N;N;N;s,\n, ,g' | \ + $(AWK) 'BEGIN { files["."] = ""; dirs["."] = 1; } \ + { d=$$3; if (dirs[d] != 1) { print "d", d; dirs[d] = 1 } \ + if ($$2 == $$4) { files[d] = files[d] " " $$1; \ + if (++n[d] == $(am__install_max)) { \ + print "f", d, files[d]; n[d] = 0; files[d] = "" } } \ + else { print "f", d "/" $$4, $$1 } } \ + END { for (d in files) print "f", d, files[d] }' | \ + while read type dir files; do \ + if test "$$dir" = .; then dir=; else dir=/$$dir; fi; \ + test -z "$$files" || { \ + echo " $(INSTALL_SCRIPT) $$files '$(DESTDIR)$(bindir)$$dir'"; \ + $(INSTALL_SCRIPT) $$files "$(DESTDIR)$(bindir)$$dir" || exit $$?; \ + } \ + ; done + +uninstall-binSCRIPTS: + @$(NORMAL_UNINSTALL) + @list='$(bin_SCRIPTS)'; test -n "$(bindir)" || exit 0; \ + files=`for p in $$list; do echo "$$p"; done | \ + sed -e 's,.*/,,;$(transform)'`; \ + dir='$(DESTDIR)$(bindir)'; $(am__uninstall_files_from_dir) +install-man1: $(dist_man_MANS) + @$(NORMAL_INSTALL) + @list1=''; \ + list2='$(dist_man_MANS)'; \ + test -n "$(man1dir)" \ + && test -n "`echo $$list1$$list2`" \ + || exit 0; \ + echo " $(MKDIR_P) '$(DESTDIR)$(man1dir)'"; \ + $(MKDIR_P) "$(DESTDIR)$(man1dir)" || exit 1; \ + { for i in $$list1; do echo "$$i"; done; \ + if test -n "$$list2"; then \ + for i in $$list2; do echo "$$i"; done \ + | sed -n '/\.1[a-z]*$$/p'; \ + fi; \ + } | while read p; do \ + if test -f $$p; then d=; else d="$(srcdir)/"; fi; \ + echo "$$d$$p"; echo "$$p"; \ + done | \ + sed -e 'n;s,.*/,,;p;h;s,.*\.,,;s,^[^1][0-9a-z]*$$,1,;x' \ + -e 's,\.[0-9a-z]*$$,,;$(transform);G;s,\n,.,' | \ + sed 'N;N;s,\n, ,g' | { \ + list=; while read file base inst; do \ + if test "$$base" = "$$inst"; then list="$$list $$file"; else \ + echo " $(INSTALL_DATA) '$$file' '$(DESTDIR)$(man1dir)/$$inst'"; \ + $(INSTALL_DATA) "$$file" "$(DESTDIR)$(man1dir)/$$inst" || exit $$?; \ + fi; \ + done; \ + for i in $$list; do echo "$$i"; done | $(am__base_list) | \ + while read files; do \ + test -z "$$files" || { \ + echo " $(INSTALL_DATA) $$files '$(DESTDIR)$(man1dir)'"; \ + $(INSTALL_DATA) $$files "$(DESTDIR)$(man1dir)" || exit $$?; }; \ + done; } + +uninstall-man1: + @$(NORMAL_UNINSTALL) + @list=''; test -n "$(man1dir)" || exit 0; \ + files=`{ for i in $$list; do echo "$$i"; done; \ + l2='$(dist_man_MANS)'; for i in $$l2; do echo "$$i"; done | \ + sed -n '/\.1[a-z]*$$/p'; \ + } | sed -e 's,.*/,,;h;s,.*\.,,;s,^[^1][0-9a-z]*$$,1,;x' \ + -e 's,\.[0-9a-z]*$$,,;$(transform);G;s,\n,.,'`; \ + dir='$(DESTDIR)$(man1dir)'; $(am__uninstall_files_from_dir) +tags: TAGS +TAGS: + +ctags: CTAGS +CTAGS: + + +distdir: $(DISTFILES) + @list='$(MANS)'; if test -n "$$list"; then \ + list=`for p in $$list; do \ + if test -f $$p; then d=; else d="$(srcdir)/"; fi; \ + if test -f "$$d$$p"; then echo "$$d$$p"; else :; fi; done`; \ + if test -n "$$list" && \ + grep 'ab help2man is required to generate this page' $$list >/dev/null; then \ + echo "error: found man pages containing the \`missing help2man' replacement text:" >&2; \ + grep -l 'ab help2man is required to generate this page' $$list | sed 's/^/ /' >&2; \ + echo " to fix them, install help2man, remove and regenerate the man pages;" >&2; \ + echo " typically \`make maintainer-clean' will remove them" >&2; \ + exit 1; \ + else :; fi; \ + else :; fi + @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \ + topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \ + list='$(DISTFILES)'; \ + dist_files=`for file in $$list; do echo $$file; done | \ + sed -e "s|^$$srcdirstrip/||;t" \ + -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \ + case $$dist_files in \ + */*) $(MKDIR_P) `echo "$$dist_files" | \ + sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \ + sort -u` ;; \ + esac; \ + for file in $$dist_files; do \ + if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \ + if test -d $$d/$$file; then \ + dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \ + if test -d "$(distdir)/$$file"; then \ + find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \ + fi; \ + if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \ + cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \ + find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \ + fi; \ + cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \ + else \ + test -f "$(distdir)/$$file" \ + || cp -p $$d/$$file "$(distdir)/$$file" \ + || exit 1; \ + fi; \ + done +check-am: all-am +check: check-am +all-am: Makefile $(PROGRAMS) $(SCRIPTS) $(MANS) +installdirs: + for dir in "$(DESTDIR)$(bindir)" "$(DESTDIR)$(bindir)" "$(DESTDIR)$(man1dir)"; do \ + test -z "$$dir" || $(MKDIR_P) "$$dir"; \ + done +install: install-am +install-exec: install-exec-am +install-data: install-data-am +uninstall: uninstall-am + +install-am: all-am + @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am + +installcheck: installcheck-am +install-strip: + if test -z '$(STRIP)'; then \ + $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \ + install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \ + install; \ + else \ + $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \ + install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \ + "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \ + fi +mostlyclean-generic: + +clean-generic: + -test -z "$(CLEANFILES)" || rm -f $(CLEANFILES) + +distclean-generic: + -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES) + -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES) + -test -z "$(DISTCLEANFILES)" || rm -f $(DISTCLEANFILES) + +maintainer-clean-generic: + @echo "This command is intended for maintainers to use" + @echo "it deletes files that may require special tools to rebuild." + -test -z "$(MAINTAINERCLEANFILES)" || rm -f $(MAINTAINERCLEANFILES) +clean: clean-am + +clean-am: clean-binPROGRAMS clean-generic mostlyclean-am + +distclean: distclean-am + -rm -f Makefile +distclean-am: clean-am distclean-generic + +dvi: dvi-am + +dvi-am: + +html: html-am + +html-am: + +info: info-am + +info-am: + +install-data-am: install-man + +install-dvi: install-dvi-am + +install-dvi-am: + +install-exec-am: install-binPROGRAMS install-binSCRIPTS + +install-html: install-html-am + +install-html-am: + +install-info: install-info-am + +install-info-am: + +install-man: install-man1 + +install-pdf: install-pdf-am + +install-pdf-am: + +install-ps: install-ps-am + +install-ps-am: + +installcheck-am: + +maintainer-clean: maintainer-clean-am + -rm -f Makefile +maintainer-clean-am: distclean-am maintainer-clean-generic + +mostlyclean: mostlyclean-am + +mostlyclean-am: mostlyclean-generic + +pdf: pdf-am + +pdf-am: + +ps: ps-am + +ps-am: + +uninstall-am: uninstall-binPROGRAMS uninstall-binSCRIPTS uninstall-man + +uninstall-man: uninstall-man1 + +.MAKE: install-am install-strip + +.PHONY: all all-am check check-am clean clean-binPROGRAMS \ + clean-generic distclean distclean-generic distdir dvi dvi-am \ + html html-am info info-am install install-am \ + install-binPROGRAMS install-binSCRIPTS install-data \ + install-data-am install-dvi install-dvi-am install-exec \ + install-exec-am install-html install-html-am install-info \ + install-info-am install-man install-man1 install-pdf \ + install-pdf-am install-ps install-ps-am install-strip \ + installcheck installcheck-am installdirs maintainer-clean \ + maintainer-clean-generic mostlyclean mostlyclean-generic pdf \ + pdf-am ps ps-am uninstall uninstall-am uninstall-binPROGRAMS \ + uninstall-binSCRIPTS uninstall-man uninstall-man1 + + +###----- That's all, folks -------------------------------------------------- + +@HAVE_PYTHON_TRUE@gremlin: gremlin.in +@HAVE_PYTHON_TRUE@ $(SUBST) $(srcdir)/gremlin.in >$@.new $(SUBSTITUTIONS) && \ +@HAVE_PYTHON_TRUE@ chmod +x $@.new && mv $@.new $@ + +###----- That's all, folks -------------------------------------------------- + +# Tell versions [3.59,3.63) of GNU make to not export all variables. +# Otherwise a system limit (for SysV at least) may be exceeded. +.NOEXPORT: diff --git a/gremlin/gremlin.1 b/gremlin/gremlin.1 new file mode 100644 index 0000000..e6538cf --- /dev/null +++ b/gremlin/gremlin.1 @@ -0,0 +1,725 @@ +.\" -*-nroff-*- +.\" +.\" Manual for the audio conversion gremlin +.\" +.\" (c) 2016 Mark Wooding +.\" +. +.\"----- Licensing notice --------------------------------------------------- +.\" +.\" This file is part of the `autoys' audio tools collection. +.\" +.\" `autoys' is free software; you can redistribute it and/or modify +.\" it under the terms of the GNU General Public License as published by +.\" the Free Software Foundation; either version 2 of the License, or +.\" (at your option) any later version. +.\" +.\" `autoys' is distributed in the hope that it will be useful, +.\" but WITHOUT ANY WARRANTY; without even the implied warranty of +.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +.\" GNU General Public License for more details. +.\" +.\" You should have received a copy of the GNU General Public License +.\" along with `autoys'; if not, write to the Free Software Foundation, +.\" Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +. +.TH gremlin 1 "13 February 2016" "Mark Wooding" "autoys" +. +.\"-------------------------------------------------------------------------- +.SH NAME +gremlin \- batch audio file converter +. +.SH SYNOPSIS +.B gremlin +.RB [ \-in ] +.RB [ \-T +.IR timeout ] +.RB [ \-t +.IR timeout ] +.I config +. +.\"-------------------------------------------------------------------------- +.SH DESCRIPTION +. +The +.B gremlin +program converts audio files +in an input `master' directory tree, +which presumably contains +high-quality (ideally lossless) encodings +of interesting audio, +writing corresponding converted files +to a collection of output directory trees. +It's non-interactive, idempotent, and restartable; +it never modifies its master tree. +It's exactly the sort of thing you want to +install as a daily cron job. +.PP +The +.B gremlin +reads a configuration file +which describes the conversion policy for each of the output trees. +The policy can say things like: +copy MP3 files up to 160kb/s, +or Ogg Vorbis files up to 128kb/s; +and convert everything else to 128kb/s Ogg Vorbis. +.PP +The +.B gremlin +can also convert image files, such as cover art. +.PP +Input files can be anything which +GStreamer and/or the Python Imaging Library can understand; +output files are more constrained, +because the +.B gremlin +has to be able to understand +their relevant properties. +The currently supported audio formats are +Ogg Vorbis and +MP3; +image formats are +JPEG, +PNG, and +BMP. +.PP +In a little more detail: +the +.B gremlin +works through its input master tree, +one directory at a time. +For each master directory, +it tries to write a converted version +to a corresponding output directory +in each of the output trees. +For each file in the master directory, +it determines which files should be made +in each output directory: +if those files exist, +and are not older than the master file, +then they're left alone on the assumption that they're up-to-date; +otherwise, the +.B gremlin +will make the output files by converting the master file. +.PP +Any other files or directories in the output directory +will be +.IR deleted . +The +.B gremlin +assumes that its output trees belong entirely to it, +to maintain according to its configuration, +and that unexpected files are either +debris left over from an earlier failure +or a result of a policy change, +and in either case the right thing to do is +to delete the offending files. +. +.SS "Command line syntax" +The following options are recognized. +.TP +.B "\-h, \-\-help" +Write a help message to standard output +describing the +.BR gremlin 's +command-line options, +and exit with status zero. +.TP +.B "\-\-version" +Write the +.BR gremlin 's +version number to standard output +and exit with status zero. +.TP +.B "\-i, \-\-interactive" +Write progress eyecandy to standard output while running. +While walking the master tree, +the +.B gremlin +shows which directory it's currently examining. +While converting audio files, +it shows a progress meter showing +a bar chart of the job in progress, +the percentage of the job which is complete, +and an estimated time to completion. +(This last starts out rather inaccurate, +but seems to be pretty good after a couple of seconds.) +All this is done automatically if standard output is a terminal; +this option can be used to turn it on under other circumstances. +.TP +.B "\-n, \-\-no-act" +Don't actually modify the filesystem. +No files will be created or removed. +.TP +.BI "\-t, \-\-timeout=" timeout +Only run for about +.I timeout +seconds. +Once the timeout has expired, +.B gremlin +will try to finish what it's doing +and then exit with status zero. +.IP +(This might seem a surprising choice of exit status. +The idea is that the +.B gremlin +was asked to spend some amount of time converting files, +and it has done that successfully.) +.TP +.BI "\-T, \-\-timeout-nasty=" timeout +If the timeout set by the +.B \-t +option (above) has expired, +and a further +.I timeout +seconds have elapsed +but the +.B gremlin +still hasn't managed to wrap things up, +then exit immediately with status 3, +possibly leaving files partially converted, +or other kinds of incompleteness. +(A future run of the +.B gremlin +will notice this wreckage and clean it up.) +. +.\"-------------------------------------------------------------------------- +.SH CONFIGURATION FILE +. +.SS "Lexical syntax" +The +.BR gremlin 's +configuration file has a simple token-oriented lexical syntax. +Whitespace acts to separate tokens but has no other meaning. +A hash sign +.RB ` # ' +outside of a quoted string introduces a comment +which extends to the end of the line; +newlines otherwise just separate tokens, just like other whitespace. +There are no `reserved words', +but some names have special meanings, +depending on the context. +.PP +Integers are written in decimal. +(There is no provision for entering numbers in hex or octal.) +.IP +.I int +::= +.I digit +\&... +.br +.I digit +::= +.B 0 +| +.B 1 +| +.B 2 +| +.B 3 +| +.B 4 +| +.B 5 +| +.B 6 +| +.B 7 +| +.B 8 +| +.B 9 +.PP +Strings (mostly used for pathnames and suchlike) +are enclosed in double quotes +.RB ` """" '; +quotes and backslashes to be included in the string +must be escaped by preceding them with a backslash +.RB ` \e '. +.IP +.I string +::= +.B """" +.IR string-char ...\& +.B """" +.br +.I string-char +::= +any character other than +.B """" +or +.B \e +.br +\h'4m'| +.B "\e""" +| +.B \e\e +. +.SS "Top-level syntax" +At a high level, +the configuration consists of a sequence of +.IR "top-level items" . +.IP +.I config +::= +.I toplevel-item +\&... +. +.SS "Global settings" +Miscellaneous configuration for the whole program +goes in a top-level +.B vars +section. +.IP +.I toplevel-item +::= +.I vars-section +.br +.I vars-section +::= +.B vars +.B { +.IR var-setting +\&...\& +.B } +.PP +There may be multiple such sections. +The same variable may be set more than once; +if that happens, +only the last such setting has affect. +.IP +.I var-setting +::= +.B master +.B = +.I path +.br +.I path +::= +.I string +.PP +The +.B master +variable holds the pathname of the top of the master tree. +.PP +There are, at present, no other global settings. +. +.SS "Target definitions" +The other kind of top-level configuration item +defines a target directory +to be constructed or updated +by the +.BR gremlin . +.IP +.I toplevel-item +::= +.I target-def +.br +.I target-def +::= +.B target +.I path +.B { +.I type-clause +\&...\& +.B } +.br +.I type-clause +::= +.B type +.I type +.B { +.I policy +\&...\& +.B } +.PP +A +.B target +definition tells the +.B gremlin +to populate a directory tree, +named rooted at the given +.IR path . +The body of the target definition consists of +a sequence of +.B type +clauses +which explain what to do with different kinds of file. +The possible +.I type +tokens are as follows. +.TP +.B audio +Encoded audio files, +which can be decoded by the GStreamer library. +.TP +.B image +Image files, +which can be decoded by the Python Imaging Library. +.PP +The body of the type clause defines a +.I policy +for converting files of that type. +. +.SS "Policy descriptions" +There are two kinds of +.I primitive +policies, +which are described in full below: +.BR accept , +which copies (or links) a master file +if its format is appropriate, +or does nothing; +and +.BR convert , +which converts a master file into a chosen format, +and (in principle) should always succeed. +There are also two ways to build up +.I compound +policies from simpler ones. +.IP +.I policy +::= +.B and +.B { +.I policy +\&...\& +.B } +.br +\h'4m'| +.B or +.B { +.I policy +\&...\& +.B } +.PP +The +.B and +policy applies +.I all +of its operand policies, +potentially producing multiple output files. +.PP +The body of a +.I type-clause +consists of a sequence of policies +which are implicitly combined together in this way. +.PP +The +.B or +policy +tries its operand policies in turn, +in the order specified, +until one of them succeeds; +no more policies are tried after this. +.IP +.I policy +::= +.B accept +.I format-spec +.br +\h'4m'| +.B convert +.I format-spec +.IP +.I format-spec +::= +.I format-name +.br +\h'4m'| +.I format-name +.B { +.I format-prop +\&...\& +.B } +.PP +(The possible +.IR format-name s +and the corresponding +.IR format-spec s +are described in the section below.) +.PP +The +.B convert +policy converts a file to the specified format. +More specifically: +if the file's format already matches the +.I format-spec +then it is copied to the target directory. +(Indeed, if possible, +the file is hard linked into the target directory.) +If the file's format doesn't match, +then the +.B gremlin +converts it, +producing an output file of the requested format. +.PP +The +.B accept +policy copies or links a file if its format matches the +.IR format-spec , +just as +.B convert +does. +However, if the file doesn't match then +.B accept +fails. +.PP +The usual use of +.B accept +is within an +.B or +block. +For example, suppose that the master tree mostly contains +losslessly encoded files, such as FLAC, +and we usually want to produce Ogg Vorbis +for use on devices with limited storage capacity; +but some of the master files are only available as MP3, +and re-encoding MP3 as Ogg Vorbis won't be good for sound quality. +Therefore, you can say something like +.IP +.nf +.ft B +or { + accept mp3 { bitrate = 160 } + convert ogg-vorbis { bitrate = 128 } +} +.fi +.ft P +.PP +which means: +if a master file is an MP3 file with bitrate approximately 160kb/s or less, +then copy it; +otherwise, convert the file to Ogg Vorbis, at about 128kb/s. +.PP +It's possible that even a simple policy +acting on the files in a master directory +will come up with multiple ways +to produce the same output file. +The rule used to decide is as follows: +if the +.B gremlin +can make the output file by copying one of the master files +then it does that; +otherwise it converts one of the inputs chosen arbitrarily. +For example, +suppose that a policy for +.B audio +files says +.IP +.B convert ogg-vorbis +.PP +and the master directory contains +.B foo.flac +and +.BR foo.ogg ; +then it will copy +.B foo.ogg +and ignore +.BR foo.flac . +If, instead, the master contains +.B foo.flac +and +.BR foo.mp3 , +then one of these will be converted, +but it's hard to predict which. +. +.SS "Audio formats" +Two audio +.IR format-type s +are defined. +.PP +All audio formats support a +.B bitrate +property. +.IP +.I format-prop +::= +.B bitrate +.B = +.I int +.PP +The bitrate is expressed in kilobits per second. +For an existing file to match a +.I format-spec +containing a +.B bitrate +property, +the file's bitrate must be less than +the specified bitrate times a fudge factor +(currently sqrt(2)). +(The +.B bitrate +property is notionally the desired +.I output +bitrate; +the +.B gremlin +assumes that it's better to make output files a bit larger +than to re-encode an already lossily compressed master file.) +.PP +At present, the audio formats define no other properties. +.TP +.B mp3 +The MP3 format that everyone knows and loves. +For encoding, the +.B gremlin +uses Lame, +and stores metadata in an ID3v2 tag; +it also tries to store an ID3v1.1 tag, +but this can fail for a number of reasons +(e.g., if the genre can't be represented, +or text contains characters outside of the ISO 8859-1 character set +used in ID3v1 tags). +.TP +.B ogg-vorbis +Vorbis-encoded audio in an Ogg container, +as defined by the Xiph.Org Foundation. +On encoding, the +.B bitrate +parameter is actually mapped to a quality setting +chosen to produce approximately the right bitrate. +. +.SS "Image formats" +Three image +.IR format-type s +are defined. +.PP +All image formats support a +.B size +property. +.IP +.I format-prop +::= +.B size +.B = +.I int +.PP +The size provides an upper bound on the width and height of the image. +A master file will only match if +both its width and height are +less than the stated size. +On output, the image will be scaled to the right size, +preserving its aspect ratio. +.TP +.B jpeg +The JFIF format, defined by the Joint Photographic Experts Group. +The following additional properties can be set; +they affect output only. +.RS +.TP +.B optimize +Spend longer to select optimal encoder settings. +.TP +.B progressive +Make a progressively-rendering output file. +This isn't usually a good idea. +.TP +.BI "quality = " int +Set the image quality (at the expense of file size). +This is a percentage; the default is 75. +.RE +.TP +.B png +The Portable Network Graphics format, +originally defined in RFC2083. +The following additional properties can be set; +they affect output only. +.RS +.TP +.B optimize +Spend longer to try to make the output file smaller. +.RE +.TP +.B bmp +The Windows BMP format. +There are no additional properties. +. +.SS "Example file" +The following is the author's configuration file. +I have an archive which mostly consists of FLAC files, +with a few MP3 files where I've been unable to obtain physical CDs. +I generate two output trees. +One mostly contains Ogg Vorbis files, +but tolerates occasional MP3 +rather than suffer the quality loss of re-encoding. +It also generates small BMP-format images from cover art, +because I have an old portable audio player +which runs the free RockBox firmware, +whose player is only capable of displaying such images. +.IP +.nf +.ft B +### -*-conf-*- + +vars { + master = "/mnt/jb/master" +} + +target "/mnt/jb/gremlin/ogg-vorbis-128" { + type audio { + or { + accept mp3 { bitrate = 160 } + convert ogg-vorbis { bitrate = 128 } + } + } + type image { + or { + accept png + convert jpeg { quality = 7 } + } + convert bmp { size = 75 } + } +} + +target "/mnt/jb/gremlin/mp3-160" { + type audio { + convert mp3 { bitrate = 160 } + } + type image { + or { + accept png + convert jpeg { quality = 7 } + } + } +} +.fi +.ft P +. +.\"-------------------------------------------------------------------------- +.SH BUGS +. +The +.B gremlin +makes no effort to process more than one file at a time. +.PP +It should probably support more audio formats. +They're quite easy to add, +but I don't have a good feel for which formats are good. +Patches and advice are welcome. +.PP +The +.B and +and +.B or +policy names are possibly confusing. +They suggest that they work like the standard logical operators; +while +.B or +sort of does, if you squint a bit, +.B and +certainly doesn't; +on the other hand, it does try to do all of the things you ask of it. +.PP +.B gremlin +is a very unhelpful name for the program. +. +.\"-------------------------------------------------------------------------- +.SH AUTHOR +Mark Wooding, +. +.SH SEE ALSO +.BR hush (1), +.BR rsync (1). +. +.\"----- That's all, folks -------------------------------------------------- diff --git a/gremlin/gremlin b/gremlin/gremlin.in similarity index 86% rename from gremlin/gremlin rename to gremlin/gremlin.in index c9734d5..e32d084 100755 --- a/gremlin/gremlin +++ b/gremlin/gremlin.in @@ -1,4 +1,4 @@ -#! /usr/bin/python +#! @PYTHON@ ### ### Convert a directory tree of audio files ### @@ -63,7 +63,7 @@ import pyparsing as P ###-------------------------------------------------------------------------- ### Special initialization. -VERSION = '1.0.0~pre' +VERSION = '@VERSION@' ## GLib. G.threads_init() @@ -103,7 +103,6 @@ def charwidth(s): else: w += 1 ## Done. - #print ';; %r -> %d' % (s, w) return w class StatusLine (object): @@ -136,8 +135,6 @@ class StatusLine (object): ## Eyecandy update. if me.eyecandyp: - #print - #print ';; new status %r' % line ## If the old line was longer, we need to clobber its tail, so work out ## what that involves. @@ -159,7 +156,6 @@ class StatusLine (object): ## Actually do the output, all in one syscall. b = charwidth(me._last[i:]) SYS.stdout.write(pre + '\b'*b + line[i:]) - #print ';; => %r' % (pre + '\b'*b + line[i:]) SYS.stdout.flush() ## Update our idea of what's gone on. @@ -301,7 +297,6 @@ String = P.QuotedString('"', '\\') ## Handy abbreviations for constructed parser elements. def K(k): return P.Keyword(k).suppress() def D(d): return P.Literal(d).suppress() -##R = P.ZeroOrMore def R(p): return P.ZeroOrMore(p).setParseAction(lambda s, l, t: [t]) O = P.Optional @@ -1094,7 +1089,7 @@ class AudioFormat (BaseFormat): class OggVorbisFormat (AudioFormat): "AudioFormat object for Ogg Vorbis." - ## From http://en.wikipedia.org/wiki/Vorbis + ## From https://en.wikipedia.org/wiki/Vorbis QMAP = [(-1, 45), ( 0, 64), ( 1, 80), ( 2, 96), ( 3, 112), ( 4, 128), ( 5, 160), ( 6, 192), ( 7, 224), ( 8, 256), ( 9, 320), (10, 500)] @@ -1105,13 +1100,15 @@ class OggVorbisFormat (AudioFormat): EXT = 'ogg' def encoder_chain(me): - for q, br in me.QMAP: - if br >= me.bitrate: - break - else: - raise ValueError, 'no suitable quality setting found' - return [make_element('vorbisenc', - quality = q/10.0), + encprops = {} + if me.bitrate is not None: + for q, br in me.QMAP: + if br >= me.bitrate: + break + else: + raise ValueError, 'no suitable quality setting found' + encprops['quality'] = q/10.0 + return [make_element('vorbisenc', **encprops), make_element('oggmux')] defformat('ogg-vorbis', OggVorbisFormat) @@ -1124,9 +1121,9 @@ class MP3Format (AudioFormat): EXT = 'mp3' def encoder_chain(me): - return [make_element('lame', - vbr_mean_bitrate = me.bitrate, - vbr = 4), + encprops = {} + if me.bitrate is not None: encprops['vbr_mean_bitrate'] = me.bitrate + return [make_element('lame', vbr = 4, **encprops), make_element('xingmux'), make_element('id3v2mux')] @@ -1241,7 +1238,7 @@ class JPEGFormat (ImageFormat): optimize If present, take a second pass to select optimal encoder settings. - progression + progressive If present, make a progressive file. quality Integer from 1--100 (worst to best); default is 75. @@ -1281,33 +1278,87 @@ class BMPFormat (ImageFormat): defformat('bmp', BMPFormat) ###-------------------------------------------------------------------------- +### Remaining parsing machinery. + +Type = K('type') - Name - D('{') - R(Policy) - D('}') +def build_type(s, l, t): + try: + cat = CATEGORYMAP[t[0]] + except KeyError: + raise P.ParseException(s, loc, "Unknown category `%s'" % t[0]) + pols = t[1] + if len(pols) == 1: pol = pols[0] + else: pol = AndPolicy(pols) + pol.setcategory(cat) + return pol +Type.setParseAction(build_type) + +TARGETS = [] +class TargetJob (object): + def __init__(me, targetdir, policies): + me.targetdir = targetdir + me.policies = policies + def perform(me): + TARGETS.append(me) + +Target = K('target') - String - D('{') - R(Type) - D('}') +def build_target(s, l, t): + return TargetJob(t[0], t[1]) +Target.setParseAction(build_target) + +VARS = { 'master': None } +class VarsJob (object): + def __init__(me, vars): + me.vars = vars + def perform(me): + for k, v in me.vars: + VARS[k] = v + +Var = prop('master', String) +Vars = K('vars') - D('{') - R(Var) - D('}') +def build_vars(s, l, t): + return VarsJob(t[0]) +Vars.setParseAction(build_vars) + +TopLevel = Vars | Target +Config = R(TopLevel) +Config.ignore(P.pythonStyleComment) + +###-------------------------------------------------------------------------- ### The directory grobbler. -class Grobbler (object): +def grobble(master, targets, noact = False): """ - The directory grobbler copies a directory tree, converting files. + Work through the MASTER directory, writing converted files to TARGETS. + + The TARGETS are a list of `TargetJob' objects, each describing a target + directory and a policy to apply to it. + + If NOACT is true, then don't actually do anything permanent to the + filesystem. """ - def __init__(me, policies, noact = False): - """ - Create a new Grobbler, working with the given POLICIES. - """ - me._pmap = {} - me._noact = noact - for p in policies: - me._pmap.setdefault(p.cat, []).append(p) - me._dirs = [] + ## Transform the targets into a more convenient data structure. + tpolmap = [] + for t in targets: + pmap = {} + tpolmap.append(pmap) + for p in t.policies: pmap.setdefault(p.cat, []).append(p) - def _grobble_file(me, master, targetdir, cohorts): - """ - Convert MASTER, writing the result to TARGETDIR. + ## Keep track of the current position in the master tree. + dirs = [] - The COHORTS are actually (CAT, ID, COHORT) triples, where a COHORT is a - list of (FILENAME, ID) pairs. + ## And the files which haven't worked. + broken = [] - Since this function might convert the MASTER file, the caller doesn't - know the name of the output files, so we return then as a list. - """ + def grobble_file(master, pmap, targetdir, cohorts): + ## Convert MASTER, writing the result to TARGETDIR. + ## + ## The COHORTS are actually (CAT, ID, COHORT) triples, where a COHORT is + ## a list of (FILENAME, ID) pairs. + ## + ## Since this function might convert the MASTER file, the caller doesn't + ## know the name of the output files, so we return then as a list. done = set() st_m = OS.stat(master) @@ -1317,7 +1368,7 @@ class Grobbler (object): ## Go through the category's policies and see if any match. If we fail ## here, see if there are more categories to try. - for pol in me._pmap[cat]: + for pol in pmap[cat]: acts = pol.actions(master, targetdir, id, cohort) if acts: break else: @@ -1346,7 +1397,7 @@ class Grobbler (object): ## Remove the target. (A hardlink will fail if the target already ## exists.) - if not me._noact: + if not noact: try: OS.unlink(a.target) except OSError, err: @@ -1354,7 +1405,7 @@ class Grobbler (object): raise ## Do whatever it is we decided to do. - if me._noact: + if noact: STATUS.commit(filestatus(master, a)) else: a.perform() @@ -1363,11 +1414,9 @@ class Grobbler (object): return list(done) @contextmanager - def _wrap(me, masterfile): - """ - Handle exceptions found while trying to convert a particular file or - directory. - """ + def wrap(masterfile): + ## Handle exceptions found while trying to convert a particular file or + ## directory. try: yield masterfile @@ -1377,179 +1426,134 @@ class Grobbler (object): except (IOError, OSError), exc: STATUS.clear() STATUS.commit(filestatus(masterfile, 'failed (%s)' % exc)) - me._broken.append((masterfile, exc)) + broken.append((masterfile, exc)) - def _grobble_dir(me, master, target): - """ - Recursively convert files in MASTER, writing them to TARGET. - """ + def grobble_dir(master, targets): + ## Recursively convert files in MASTER, writing them to the TARGETS. - ## Make sure the TARGET exists and is a directory. It's a fundamental - ## assumption of this program that the entire TARGET tree is disposable, - ## so if something exists but isn't a directory, we should kill it. - if OS.path.isdir(target): - pass - else: - if OS.path.exists(target): - STATUS.commit(filestatus(target, 'clear nondirectory')) - if not me._noact: - OS.unlink(target) - STATUS.commit(filestatus(target, 'create directory')) - if not me._noact: - OS.mkdir(target) - - ## Keep a list of things in the target. As we convert files, we'll check - ## them off. Anything left over is rubbish and needs to be deleted. - checklist = {} - try: - for i in OS.listdir(target): - checklist[i] = False - except OSError, err: - if err.errno not in (E.ENOENT, E.ENOTDIR): - raise - - ## Keep track of the files in each category. - catmap = {} - todo = [] - done = [] - - ## Work through the master files. - for f in sorted(OS.listdir(master)): - - ## If the killswitch has been pulled then stop. The whole idea is that - ## we want to cause a clean shutdown if possible, so we don't want to - ## do it in the middle of encoding because the encoding effort will - ## have been wasted. This is the only place we need to check. If - ## we've exited the loop, then clearing old files will probably be - ## fast, and we'll either end up here when the recursive call returns - ## or we'll be in the same boat as before, clearing old files, only up - ## a level. If worst comes to worst, we'll be killed forcibly - ## somewhere inside `SH.rmtree', and that can continue where it left - ## off. - if KILLSWITCH.is_set(): - return - - ## Do something with the file. - with me._wrap(OS.path.join(master, f)) as masterfile: - - ## If it's a directory then grobble it recursively. Keep the user - ## amused by telling him where we are in the tree. - if OS.path.isdir(masterfile): - me._dirs.append(f) - STATUS.set('/'.join(me._dirs)) - try: - done += me._grobble_dir(masterfile, OS.path.join(target, f)) - finally: - me._dirs.pop() - STATUS.set('/'.join(me._dirs)) - - ## Otherwise it's a file. Work out what kind, and stash it under - ## the appropriate categories. Later, we'll apply policy to the - ## files, by category, and work out what to do with them all. - else: - gf = GIO.File(masterfile) - mime = gf.query_info('standard::content-type').get_content_type() - cats = [] - for cat in me._pmap.iterkeys(): - id = cat.identify(masterfile, mime) - if id is None: continue - catmap.setdefault(cat, []).append((masterfile, id)) - cats.append((cat, id)) - if not cats: - catmap.setdefault(None, []).append((masterfile, id)) - todo.append((masterfile, cats)) - - ## Work through the categorized files to see what actions to do for - ## them. - for masterfile, cats in todo: - with me._wrap(masterfile): - done += me._grobble_file(masterfile, target, - [(cat, id, catmap[cat]) - for cat, id in cats]) - - ## Check the results off the list so that we don't clear it later. - for f in done: - checklist[OS.path.basename(f)] = True - - ## Maybe there's stuff in the target which isn't accounted for. Delete - ## it: either the master has changed, or the policy for this target has - ## changed. Either way, the old files aren't wanted. - for f in checklist: - if not checklist[f]: - STATUS.commit(filestatus(f, 'clear bogus file')) - if not me._noact: - bogus = OS.path.join(target, f) - try: - if OS.path.isdir(bogus): - SH.rmtree(bogus) - else: - OS.unlink(bogus) - except OSError, err: - if err.errno != E.ENOENT: - raise - - ## Return the target name, so that it can be checked off. - return [target] - - def grobble(me, master, target): - """ - Convert MASTER, writing a directory tree TARGET. - - Returns a list of files which couldn't be converted. - """ - try: - me._broken = [] - me._grobble_dir(master, target) - return me._broken - finally: - del me._broken - -###-------------------------------------------------------------------------- -### Remaining parsing machinery. - -Type = K('type') - Name - D('{') - R(Policy) - D('}') -def build_type(s, l, t): - try: - cat = CATEGORYMAP[t[0]] - except KeyError: - raise P.ParseException(s, loc, "Unknown category `%s'" % t[0]) - pols = t[1] - if len(pols) == 1: pol = pols[0] - else: pol = AndPolicy(pols) - pol.setcategory(cat) - return pol -Type.setParseAction(build_type) - -TARGETS = [] -class TargetJob (object): - def __init__(me, targetdir, policies): - me.targetdir = targetdir - me.policies = policies - def perform(me): - TARGETS.append(me) - -Target = K('target') - String - D('{') - R(Type) - D('}') -def build_target(s, l, t): - return TargetJob(t[0], t[1]) -Target.setParseAction(build_target) + ## Keep track of the subdirectories we encounter, because we'll need to + ## do all of those in one go at the end. + subdirs = set() -VARS = { 'master': None } -class VarsJob (object): - def __init__(me, vars): - me.vars = vars - def perform(me): - for k, v in me.vars: - VARS[k] = v + ## Work through each target directory in turn. + for target, pmap in zip(targets, tpolmap): -Var = prop('master', String) -Vars = K('vars') - D('{') - R(Var) - D('}') -def build_vars(s, l, t): - return VarsJob(t[0]) -Vars.setParseAction(build_vars) + ## Make sure the TARGET exists and is a directory. It's a fundamental + ## assumption of this program that the entire TARGET tree is + ## disposable, so if something exists but isn't a directory, we should + ## kill it. + if OS.path.isdir(target): + pass + else: + if OS.path.exists(target): + STATUS.commit(filestatus(target, 'clear nondirectory')) + if not noact: + OS.unlink(target) + STATUS.commit(filestatus(target, 'create directory')) + if not noact: + OS.mkdir(target) + + ## Keep a list of things in the target. As we convert files, we'll + ## check them off. Anything left over is rubbish and needs to be + ## deleted. + checklist = {} + try: + for i in OS.listdir(target): + checklist[i] = False + except OSError, err: + if err.errno not in (E.ENOENT, E.ENOTDIR): + raise + + ## Keep track of the files in each category. + catmap = {} + todo = [] + done = [] + + ## Work through the master files. + for f in sorted(OS.listdir(master)): + + ## If the killswitch has been pulled then stop. The whole idea is + ## that we want to cause a clean shutdown if possible, so we don't + ## want to do it in the middle of encoding because the encoding + ## effort will have been wasted. This is the only place we need to + ## check. If we've exited the loop, then clearing old files will + ## probably be fast, and we'll either end up here when the recursive + ## call returns or we'll be in the same boat as before, clearing old + ## files, only up a level. If worst comes to worst, we'll be killed + ## forcibly somewhere inside `SH.rmtree', and that can continue where + ## it left off. + if KILLSWITCH.is_set(): + return + + ## Do something with the file. + with wrap(OS.path.join(master, f)) as masterfile: + + ## If it's a directory then prepare to grobble it recursively, but + ## don't do that yet. + if OS.path.isdir(masterfile): + subdirs.add(f) + done.append(OS.path.join(target, f)) + + ## Otherwise it's a file. Work out what kind, and stash it under + ## the appropriate categories. Later, we'll apply policy to the + ## files, by category, and work out what to do with them all. + else: + gf = GIO.File(masterfile) + mime = gf.query_info('standard::content-type').get_content_type() + cats = [] + for cat in pmap.iterkeys(): + id = cat.identify(masterfile, mime) + if id is None: continue + catmap.setdefault(cat, []).append((masterfile, id)) + cats.append((cat, id)) + if not cats: + catmap.setdefault(None, []).append((masterfile, id)) + todo.append((masterfile, cats)) + + ## Work through the categorized files to see what actions to do for + ## them. + for masterfile, cats in todo: + with wrap(masterfile): + done += grobble_file(masterfile, pmap, target, + [(cat, id, catmap[cat]) for cat, id in cats]) + + ## Check the results off the list so that we don't clear it later. + for f in done: + checklist[OS.path.basename(f)] = True + + ## Maybe there's stuff in the target which isn't accounted for. Delete + ## it: either the master has changed, or the policy for this target has + ## changed. Either way, the old files aren't wanted. + for f in checklist: + if not checklist[f]: + STATUS.commit(filestatus(f, 'clear bogus file')) + if not noact: + bogus = OS.path.join(target, f) + try: + if OS.path.isdir(bogus): + SH.rmtree(bogus) + else: + OS.unlink(bogus) + except OSError, err: + if err.errno != E.ENOENT: + raise + + ## If there are subdirectories which want processing then do those. + ## Keep the user amused by telling him where we are in the tree. + for d in sorted(subdirs): + dirs.append(d) + STATUS.set('/'.join(dirs)) + with wrap(OS.path.join(master, d)) as masterdir: + try: + grobble_dir(masterdir, + [OS.path.join(target, d) for target in targets]) + finally: + dirs.pop() + STATUS.set('/'.join(dirs)) -TopLevel = Vars | Target -Config = R(TopLevel) -Config.ignore(P.pythonStyleComment) + ## Right. We're ready to go. + grobble_dir(master, [t.targetdir for t in targets]) + return broken ###-------------------------------------------------------------------------- ### Command-line interface. @@ -1575,7 +1579,8 @@ def parse_opts(args): ## Build the option parser object. op = OP.OptionParser(prog = QUIS, version = VERSION, - usage = '%prog [-t TIMEOUT] CONFIG', + usage = '%prog [-in] [-t TIMEOUT] [-T TIMEOUT] ' + 'CONFIG', description = """\ Convert a directory tree of files according to the configuration file CONFIG. @@ -1634,11 +1639,7 @@ if __name__ == '__main__': opts = parse_opts(SYS.argv[1:]) if 'master' not in VARS: die("no master directory set") - broken = [] - for t in TARGETS: - g = Grobbler(t.policies, opts.noact) - b = g.grobble(VARS['master'], t.targetdir) - broken += b + broken = grobble(VARS['master'], TARGETS, opts.noact) if broken: moan('failed to convert some files:') for file, exc in broken: