summaryrefslogtreecommitdiff
path: root/soundconverter
diff options
context:
space:
mode:
authorLars Wirzenius <liw@localhost>2014-06-03 13:09:32 +0000
committerLars Wirzenius <liw@localhost>2014-06-03 13:09:32 +0000
commit315db640f463613cae4de4c02cc52d2be6d5684a (patch)
tree3a922456821d1882c778267f04bbf0c4d0bfca68 /soundconverter
downloadsoundconverter-debian-packaging-master.tar.gz
Initial import for soundconverter 2.1.3HEADmaster
Diffstat (limited to 'soundconverter')
-rw-r--r--soundconverter/Makefile.am27
-rw-r--r--soundconverter/Makefile.in527
-rw-r--r--soundconverter/__init__.py23
-rw-r--r--soundconverter/batch.py130
-rw-r--r--soundconverter/error.py44
-rw-r--r--soundconverter/fileoperations.py154
-rw-r--r--soundconverter/gconfstore.py59
-rw-r--r--soundconverter/gstreamer.py823
-rw-r--r--soundconverter/messagearea.py219
-rw-r--r--soundconverter/namegenerator.py110
-rw-r--r--soundconverter/notify.py44
-rw-r--r--soundconverter/queue.py133
-rw-r--r--soundconverter/settings.py131
-rw-r--r--soundconverter/soundfile.py62
-rw-r--r--soundconverter/task.py93
-rw-r--r--soundconverter/ui.py1509
-rw-r--r--soundconverter/utils.py42
17 files changed, 4130 insertions, 0 deletions
diff --git a/soundconverter/Makefile.am b/soundconverter/Makefile.am
new file mode 100644
index 0000000..fa66353
--- /dev/null
+++ b/soundconverter/Makefile.am
@@ -0,0 +1,27 @@
+## Process this file with automake to produce Makefile.in
+
+soundconverterdir = $(libdir)/soundconverter/python/soundconverter
+
+soundconverter_PYTHON = \
+ __init__.py \
+ error.py \
+ gstreamer.py \
+ fileoperations.py \
+ namegenerator.py \
+ notify.py \
+ queue.py \
+ settings.py \
+ soundfile.py \
+ task.py \
+ ui.py \
+ utils.py \
+ messagearea.py \
+ gconfstore.py \
+ batch.py
+
+
+clean-local:
+ rm -rf *.pyc *.pyo
+
+
+
diff --git a/soundconverter/Makefile.in b/soundconverter/Makefile.in
new file mode 100644
index 0000000..9d8574c
--- /dev/null
+++ b/soundconverter/Makefile.in
@@ -0,0 +1,527 @@
+# Makefile.in generated by automake 1.14.1 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2013 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@
+VPATH = @srcdir@
+am__is_gnu_make = test -n '$(MAKEFILE_LIST)' && test -n '$(MAKELEVEL)'
+am__make_running_with_option = \
+ case $${target_option-} in \
+ ?) ;; \
+ *) echo "am__make_running_with_option: internal error: invalid" \
+ "target option '$${target_option-}' specified" >&2; \
+ exit 1;; \
+ esac; \
+ has_opt=no; \
+ sane_makeflags=$$MAKEFLAGS; \
+ if $(am__is_gnu_make); then \
+ sane_makeflags=$$MFLAGS; \
+ else \
+ case $$MAKEFLAGS in \
+ *\\[\ \ ]*) \
+ bs=\\; \
+ sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+ | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
+ esac; \
+ fi; \
+ skip_next=no; \
+ strip_trailopt () \
+ { \
+ flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+ }; \
+ for flg in $$sane_makeflags; do \
+ test $$skip_next = yes && { skip_next=no; continue; }; \
+ case $$flg in \
+ *=*|--*) continue;; \
+ -*I) strip_trailopt 'I'; skip_next=yes;; \
+ -*I?*) strip_trailopt 'I';; \
+ -*O) strip_trailopt 'O'; skip_next=yes;; \
+ -*O?*) strip_trailopt 'O';; \
+ -*l) strip_trailopt 'l'; skip_next=yes;; \
+ -*l?*) strip_trailopt 'l';; \
+ -[dEDm]) skip_next=yes;; \
+ -[JT]) skip_next=yes;; \
+ esac; \
+ case $$flg in \
+ *$$target_option*) has_opt=yes; break;; \
+ esac; \
+ done; \
+ test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+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 = :
+subdir = soundconverter
+DIST_COMMON = $(srcdir)/Makefile.in $(srcdir)/Makefile.am \
+ $(top_srcdir)/mkinstalldirs $(soundconverter_PYTHON) \
+ $(top_srcdir)/py-compile
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/configure.in
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+ $(ACLOCAL_M4)
+mkinstalldirs = $(SHELL) $(top_srcdir)/mkinstalldirs
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+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_GEN_1 =
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 =
+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
+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; }; \
+ }
+am__py_compile = PYTHON=$(PYTHON) $(SHELL) $(py_compile)
+am__installdirs = "$(DESTDIR)$(soundconverterdir)"
+am__pep3147_tweak = \
+ sed -e 's|\.py$$||' -e 's|[^/]*$$|__pycache__/&.*.py|'
+py_compile = $(top_srcdir)/py-compile
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+ACLOCAL = @ACLOCAL@
+ALL_LINGUAS = @ALL_LINGUAS@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+CATALOGS = @CATALOGS@
+CATOBJEXT = @CATOBJEXT@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CFLAGS = @CFLAGS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CYGPATH_W = @CYGPATH_W@
+DATADIRNAME = @DATADIRNAME@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+GETTEXT_PACKAGE = @GETTEXT_PACKAGE@
+GMOFILES = @GMOFILES@
+GMSGFMT = @GMSGFMT@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+INSTOBJEXT = @INSTOBJEXT@
+INTLLIBS = @INTLLIBS@
+INTLTOOL_EXTRACT = @INTLTOOL_EXTRACT@
+INTLTOOL_MERGE = @INTLTOOL_MERGE@
+INTLTOOL_PERL = @INTLTOOL_PERL@
+INTLTOOL_UPDATE = @INTLTOOL_UPDATE@
+INTLTOOL_V_MERGE = @INTLTOOL_V_MERGE@
+INTLTOOL_V_MERGE_OPTIONS = @INTLTOOL_V_MERGE_OPTIONS@
+INTLTOOL__v_MERGE_ = @INTLTOOL__v_MERGE_@
+INTLTOOL__v_MERGE_0 = @INTLTOOL__v_MERGE_0@
+LDFLAGS = @LDFLAGS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LTLIBOBJS = @LTLIBOBJS@
+MAKEINFO = @MAKEINFO@
+MKDIR_P = @MKDIR_P@
+MKINSTALLDIRS = @MKINSTALLDIRS@
+MSGFMT = @MSGFMT@
+MSGFMT_OPTS = @MSGFMT_OPTS@
+MSGMERGE = @MSGMERGE@
+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@
+POFILES = @POFILES@
+POSUB = @POSUB@
+PO_IN_DATADIR_FALSE = @PO_IN_DATADIR_FALSE@
+PO_IN_DATADIR_TRUE = @PO_IN_DATADIR_TRUE@
+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@
+USE_NLS = @USE_NLS@
+VERSION = @VERSION@
+XGETTEXT = @XGETTEXT@
+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@
+intltool__v_merge_options_ = @intltool__v_merge_options_@
+intltool__v_merge_options_0 = @intltool__v_merge_options_0@
+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@
+soundconverterdir = $(libdir)/soundconverter/python/soundconverter
+soundconverter_PYTHON = \
+ __init__.py \
+ error.py \
+ gstreamer.py \
+ fileoperations.py \
+ namegenerator.py \
+ notify.py \
+ queue.py \
+ settings.py \
+ soundfile.py \
+ task.py \
+ ui.py \
+ utils.py \
+ messagearea.py \
+ gconfstore.py \
+ batch.py
+
+all: all-am
+
+.SUFFIXES:
+$(srcdir)/Makefile.in: $(srcdir)/Makefile.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) --gnu soundconverter/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --gnu soundconverter/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_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-soundconverterPYTHON: $(soundconverter_PYTHON)
+ @$(NORMAL_INSTALL)
+ @list='$(soundconverter_PYTHON)'; dlist=; list2=; test -n "$(soundconverterdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(soundconverterdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(soundconverterdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then b=; else b="$(srcdir)/"; fi; \
+ if test -f $$b$$p; then \
+ $(am__strip_dir) \
+ dlist="$$dlist $$f"; \
+ list2="$$list2 $$b$$p"; \
+ else :; fi; \
+ done; \
+ for file in $$list2; do echo $$file; done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_DATA) $$files '$(DESTDIR)$(soundconverterdir)'"; \
+ $(INSTALL_DATA) $$files "$(DESTDIR)$(soundconverterdir)" || exit $$?; \
+ done || exit $$?; \
+ if test -n "$$dlist"; then \
+ $(am__py_compile) --destdir "$(DESTDIR)" \
+ --basedir "$(soundconverterdir)" $$dlist; \
+ else :; fi
+
+uninstall-soundconverterPYTHON:
+ @$(NORMAL_UNINSTALL)
+ @list='$(soundconverter_PYTHON)'; test -n "$(soundconverterdir)" || list=; \
+ py_files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ test -n "$$py_files" || exit 0; \
+ dir='$(DESTDIR)$(soundconverterdir)'; \
+ pyc_files=`echo "$$py_files" | sed 's|$$|c|'`; \
+ pyo_files=`echo "$$py_files" | sed 's|$$|o|'`; \
+ py_files_pep3147=`echo "$$py_files" | $(am__pep3147_tweak)`; \
+ echo "$$py_files_pep3147";\
+ pyc_files_pep3147=`echo "$$py_files_pep3147" | sed 's|$$|c|'`; \
+ pyo_files_pep3147=`echo "$$py_files_pep3147" | sed 's|$$|o|'`; \
+ st=0; \
+ for files in \
+ "$$py_files" \
+ "$$pyc_files" \
+ "$$pyo_files" \
+ "$$pyc_files_pep3147" \
+ "$$pyo_files_pep3147" \
+ ; do \
+ $(am__uninstall_files_from_dir) || st=$$?; \
+ done; \
+ exit $$st
+tags TAGS:
+
+ctags CTAGS:
+
+cscope cscopelist:
+
+
+distdir: $(DISTFILES)
+ @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
+installdirs:
+ for dir in "$(DESTDIR)$(soundconverterdir)"; 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:
+
+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)
+
+maintainer-clean-generic:
+ @echo "This command is intended for maintainers to use"
+ @echo "it deletes files that may require special tools to rebuild."
+clean: clean-am
+
+clean-am: clean-generic clean-local 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-soundconverterPYTHON
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am:
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man:
+
+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-soundconverterPYTHON
+
+.MAKE: install-am install-strip
+
+.PHONY: all all-am check check-am clean clean-generic clean-local \
+ cscopelist-am ctags-am distclean distclean-generic distdir dvi \
+ dvi-am html html-am info info-am install install-am \
+ 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-pdf \
+ install-pdf-am install-ps install-ps-am \
+ install-soundconverterPYTHON install-strip installcheck \
+ installcheck-am installdirs maintainer-clean \
+ maintainer-clean-generic mostlyclean mostlyclean-generic pdf \
+ pdf-am ps ps-am tags-am uninstall uninstall-am \
+ uninstall-soundconverterPYTHON
+
+
+clean-local:
+ rm -rf *.pyc *.pyo
+
+# 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/soundconverter/__init__.py b/soundconverter/__init__.py
new file mode 100644
index 0000000..dafd178
--- /dev/null
+++ b/soundconverter/__init__.py
@@ -0,0 +1,23 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# SoundConverter - GNOME application for converting between audio formats.
+# Copyright 2004 Lars Wirzenius
+# Copyright 2005-2012 Gautier Portet
+#
+# This program 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; version 3 of the License.
+#
+# This program 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 this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+
+
+
diff --git a/soundconverter/batch.py b/soundconverter/batch.py
new file mode 100644
index 0000000..8c9eabd
--- /dev/null
+++ b/soundconverter/batch.py
@@ -0,0 +1,130 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# SoundConverter - GNOME application for converting between audio formats.
+# Copyright 2004 Lars Wirzenius
+# Copyright 2005-2012 Gautier Portet
+#
+# This program 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; version 3 of the License.
+#
+# This program 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 this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+
+
+import sys
+import gobject
+import time
+from soundfile import SoundFile
+import error
+from soundconverter.settings import settings
+from gstreamer import TagReader
+from namegenerator import TargetNameGenerator
+from queue import TaskQueue
+from gstreamer import Converter
+from fileoperations import unquote_filename
+
+def cli_tags_main(input_files):
+ error.set_error_handler(error.ErrorPrinter())
+ loop = gobject.MainLoop()
+ gobject.threads_init()
+ context = loop.get_context()
+ for input_file in input_files:
+ input_file = SoundFile(input_file)
+ if not settings['quiet']:
+ print(input_file.filename)
+ t = TagReader(input_file)
+ t.start()
+ while t.running:
+ time.sleep(0.01)
+ context.iteration(True)
+
+ if not settings['quiet']:
+ for key in sorted(input_file.tags):
+ print(' %s: %s' % (key, input_file.tags[key]))
+
+
+class CliProgress:
+
+ def __init__(self):
+ self.current_text = ''
+
+ def show(self, new_text):
+ if new_text != self.current_text:
+ self.clear()
+ sys.stdout.write(new_text)
+ sys.stdout.flush()
+ self.current_text = new_text
+
+ def clear(self):
+ sys.stdout.write('\b \b' * len(self.current_text))
+ sys.stdout.flush()
+
+
+def cli_convert_main(input_files):
+ loop = gobject.MainLoop()
+ gobject.threads_init()
+ context = loop.get_context()
+ error.set_error_handler(error.ErrorPrinter())
+
+ output_type = settings['cli-output-type']
+ output_suffix = settings['cli-output-suffix']
+
+ generator = TargetNameGenerator()
+ generator.suffix = output_suffix
+
+ progress = CliProgress()
+
+ queue = TaskQueue()
+ for input_file in input_files:
+ input_file = SoundFile(input_file)
+ output_name = generator.get_target_name(input_file)
+ c = Converter(input_file, output_name, output_type)
+ c.init()
+ c.start()
+ while c.running:
+ if c.get_duration():
+ percent = min(100, 100.0* (c.get_position() / c.get_duration()))
+ percent = '%.1f %%' % percent
+ else:
+ percent = '/-\|' [int(time.time()) % 4]
+ progress.show('%s: %s' % (unquote_filename(c.sound_file.filename[-65:]), percent ))
+ time.sleep(0.01)
+ context.iteration(True)
+ print
+
+ previous_filename = None
+
+ '''
+ queue.start()
+
+ #running, progress = queue.get_progress(perfile)
+ while queue.running:
+ t = None #queue.get_current_task()
+ if t and not settings['quiet']:
+ if previous_filename != t.sound_file.get_filename_for_display():
+ if previous_filename:
+ print _('%s: OK') % previous_filename
+ previous_filename = t.sound_file.get_filename_for_display()
+
+ percent = 0
+ if t.get_duration():
+ percent = '%.1f %%' % ( 100.0* (t.get_position() / t.get_duration() ))
+ else:
+ percent = '/-\|' [int(time.time()) % 4]
+ progress.show('%s: %s' % (t.sound_file.get_filename_for_display()[-65:], percent ))
+ time.sleep(0.10)
+ context.iteration(True)
+ '''
+ if not settings['quiet']:
+ progress.clear()
+
+
diff --git a/soundconverter/error.py b/soundconverter/error.py
new file mode 100644
index 0000000..69e756c
--- /dev/null
+++ b/soundconverter/error.py
@@ -0,0 +1,44 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# SoundConverter - GNOME application for converting between audio formats.
+# Copyright 2004 Lars Wirzenius
+# Copyright 2005-2012 Gautier Portet
+#
+# This program 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; version 3 of the License.
+#
+# This program 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 this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+
+from gettext import gettext as _
+import sys
+
+
+class ErrorPrinter:
+
+ def show_error(self, primary, secondary):
+ try:
+ sys.stderr.write(_('\n\nError: %s\n%s\n') % (primary, secondary))
+ except:
+ pass
+ sys.exit(1)
+
+
+error_handler = ErrorPrinter()
+
+def set_error_handler(handler):
+ global error_handler
+ error_handler = handler
+
+def show_error(primary, secondary):
+ error_handler.show_error(primary, secondary)
+
diff --git a/soundconverter/fileoperations.py b/soundconverter/fileoperations.py
new file mode 100644
index 0000000..535cd01
--- /dev/null
+++ b/soundconverter/fileoperations.py
@@ -0,0 +1,154 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# SoundConverter - GNOME application for converting between audio formats.
+# Copyright 2004 Lars Wirzenius
+# Copyright 2005-2012 Gautier Portet
+#
+# This program 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; version 3 of the License.
+#
+# This program 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 this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+
+import os
+import urllib
+import urlparse
+import gnomevfs
+
+from utils import log
+from error import show_error
+
+use_gnomevfs = False
+
+def unquote_filename(filename):
+ return urllib.unquote(filename)
+
+
+def beautify_uri(uri):
+ return unquote_filename(uri).split('file://')[-1]
+
+
+def vfs_walk(uri):
+ """similar to os.path.walk, but with gnomevfs.
+
+ uri -- the base folder uri.
+ return a list of uri.
+
+ """
+ if str(uri)[-1] != '/':
+ uri = uri.append_string('/')
+
+ filelist = []
+
+ try:
+ dirlist = gnomevfs.open_directory(uri, gnomevfs.FILE_INFO_FOLLOW_LINKS)
+ for file_info in dirlist:
+ try:
+ if file_info.name[0] == '.':
+ continue
+
+ if file_info.type == gnomevfs.FILE_TYPE_DIRECTORY:
+ filelist.extend(
+ vfs_walk(uri.append_path(file_info.name)))
+
+ if file_info.type == gnomevfs.FILE_TYPE_REGULAR:
+ filelist.append(str(uri.append_file_name(file_info.name)))
+ except ValueError:
+ # this can happen when you do not have sufficent
+ # permissions to read file info.
+ log("skipping: \'%s\'" % file_info.name)
+ except:
+ log("skipping: '%s\'" % uri)
+ return filelist
+
+ return filelist
+
+
+def vfs_makedirs(path_to_create):
+ """Similar to os.makedirs, but with gnomevfs."""
+
+ uri = gnomevfs.URI(path_to_create)
+ path = uri.path
+
+ # start at root
+ uri = uri.resolve_relative('/')
+
+ for folder in path.split('/'):
+ if not folder:
+ continue
+ uri = uri.append_string(folder.replace('%2f', '/'))
+ try:
+ gnomevfs.make_directory(uri, 0777)
+ except gnomevfs.FileExistsError:
+ pass
+ except:
+ return False
+ return True
+
+
+def vfs_unlink(filename):
+ """Delete a gnomevfs file."""
+
+ gnomevfs.unlink(gnomevfs.URI(filename))
+
+
+def vfs_rename(original, newname):
+ """Rename a gnomevfs file"""
+
+ uri = gnomevfs.URI(newname)
+ dirname = uri.parent
+ if dirname and not gnomevfs.exists(dirname):
+ log('Creating folder: \'%s\'' % dirname)
+ if not vfs_makedirs(str(dirname)):
+ show_error(_('Cannot create folder!'), unquote_filename(dirname.path))
+ return 'cannot-create-folder'
+
+ try:
+ gnomevfs.xfer_uri(gnomevfs.URI(original), uri,
+ gnomevfs.XFER_REMOVESOURCE,
+ gnomevfs.XFER_ERROR_MODE_ABORT,
+ gnomevfs.XFER_OVERWRITE_MODE_ABORT
+ )
+ except Exception as error:
+ # TODO: maybe we need a special case here. If dest folder is unwritable. Just stop.
+ # or an option to stop all processing.
+ show_error(_('Error while renaming file!'), '%s: %s' % (beautify_uri(newname), error))
+ return 'cannot-rename-file'
+
+
+def vfs_exists(filename):
+ try:
+ return gnomevfs.exists(filename)
+ except:
+ return False
+
+
+def filename_to_uri(filename):
+ """Convert a filename to a valid uri.
+ Filename can be a relative or absolute path, or an uri.
+ """
+ if '://' not in filename:
+ # convert local filename to uri
+ filename = urllib.pathname2url(os.path.abspath(filename))
+ filename = str(gnomevfs.URI(filename))
+ return filename
+
+
+# GStreamer gnomevfssrc helpers
+
+def vfs_encode_filename(filename):
+ return filename_to_uri(filename)
+
+
+def file_encode_filename(filename):
+ return gnomevfs.get_local_path_from_uri(filename).replace(' ', '\ ')
+
diff --git a/soundconverter/gconfstore.py b/soundconverter/gconfstore.py
new file mode 100644
index 0000000..ebd2488
--- /dev/null
+++ b/soundconverter/gconfstore.py
@@ -0,0 +1,59 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# SoundConverter - GNOME application for converting between audio formats.
+# Copyright 2004 Lars Wirzenius
+# Copyright 2005-2012 Gautier Portet
+#
+# This program 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; version 3 of the License.
+#
+# This program 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 this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+
+import gconf
+
+
+class GConfStore(object):
+
+ def __init__(self, root, defaults):
+ self.gconf = gconf.client_get_default()
+ self.gconf.add_dir(root, gconf.CLIENT_PRELOAD_ONELEVEL)
+ self.root = root
+ self.defaults = defaults
+
+ def get_with_default(self, getter, key):
+ if self.gconf.get(self.path(key)) is None:
+ return self.defaults[key]
+ else:
+ return getter(self.path(key))
+
+ def get_int(self, key):
+ return self.get_with_default(self.gconf.get_int, key)
+
+ def set_int(self, key, value):
+ self.gconf.set_int(self.path(key), value)
+
+ def get_float(self, key):
+ return self.get_with_default(self.gconf.get_float, key)
+
+ def set_float(self, key, value):
+ self.gconf.set_float(self.path(key), value)
+
+ def get_string(self, key):
+ return self.get_with_default(self.gconf.get_string, key)
+
+ def set_string(self, key, value):
+ self.gconf.set_string(self.path(key), value)
+
+ def path(self, key):
+ assert key in self.defaults, 'missing gconf default:%s' % key
+ return '%s/%s' % (self.root, key)
diff --git a/soundconverter/gstreamer.py b/soundconverter/gstreamer.py
new file mode 100644
index 0000000..9ab0cf4
--- /dev/null
+++ b/soundconverter/gstreamer.py
@@ -0,0 +1,823 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# SoundConverter - GNOME application for converting between audio formats.
+# Copyright 2004 Lars Wirzenius
+# Copyright 2005-2012 Gautier Portet
+#
+# This program 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; version 3 of the License.
+#
+# This program 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 this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+
+import os
+import sys
+from urlparse import urlparse
+from gettext import gettext as _
+
+import gobject
+import gst
+import gst.pbutils
+import gnomevfs
+import gconf
+
+from fileoperations import vfs_encode_filename, file_encode_filename
+from fileoperations import unquote_filename, vfs_makedirs, vfs_unlink
+from fileoperations import vfs_rename
+from fileoperations import vfs_exists
+from fileoperations import beautify_uri
+from fileoperations import use_gnomevfs
+from task import BackgroundTask
+from queue import TaskQueue
+from utils import debug, log
+from settings import mime_whitelist, filename_blacklist
+from error import show_error
+try:
+ from notify import notification
+except:
+ def notification(msg):
+ pass
+
+from fnmatch import fnmatch
+
+import time
+import gtk
+def gtk_iteration():
+ while gtk.events_pending():
+ gtk.main_iteration(False)
+
+
+def gtk_sleep(duration):
+ start = time.time()
+ while time.time() < start + duration:
+ time.sleep(0.010)
+ gtk_iteration()
+
+import gconf
+# load gstreamer audio profiles
+_GCONF_PROFILE_PATH = "/system/gstreamer/0.10/audio/profiles/"
+_GCONF_PROFILE_LIST_PATH = "/system/gstreamer/0.10/audio/global/profile_list"
+audio_profiles_list = []
+audio_profiles_dict = {}
+
+_GCONF = gconf.client_get_default()
+profiles = _GCONF.get_list(_GCONF_PROFILE_LIST_PATH, 1)
+for name in profiles:
+ if _GCONF.get_bool(_GCONF_PROFILE_PATH + name + "/active"):
+ # get profile
+ description = _GCONF.get_string(_GCONF_PROFILE_PATH + name + "/name")
+ extension = _GCONF.get_string(_GCONF_PROFILE_PATH + name + "/extension")
+ pipeline = _GCONF.get_string(_GCONF_PROFILE_PATH + name + "/pipeline")
+ # check profile validity
+ if not extension or not pipeline:
+ continue
+ if not description:
+ description = extension
+ if description in audio_profiles_dict:
+ continue
+ # store
+ profile = description, extension, pipeline
+ audio_profiles_list.append(profile)
+ audio_profiles_dict[description] = profile
+
+required_elements = ('decodebin', 'fakesink', 'audioconvert', 'typefind', 'audiorate')
+for element in required_elements:
+ if not gst.element_factory_find(element):
+ print("required gstreamer element \'%s\' not found." % element)
+ sys.exit(1)
+
+if gst.element_factory_find('giosrc'):
+ gstreamer_source = 'giosrc'
+ gstreamer_sink = 'giosink'
+ encode_filename = vfs_encode_filename
+ use_gnomevfs = True
+ print(' using gio')
+elif gst.element_factory_find('gnomevfssrc'):
+ gstreamer_source = 'gnomevfssrc'
+ gstreamer_sink = 'gnomevfssink'
+ encode_filename = vfs_encode_filename
+ use_gnomevfs = True
+ print(' using deprecated gnomevfssrc')
+else:
+ gstreamer_source = 'filesrc'
+ gstreamer_sink = 'filesink'
+ encode_filename = file_encode_filename
+ print(' not using gnomevfssrc, look for a gnomevfs gstreamer package.')
+
+# used to dismiss codec installation if the user already canceled it
+user_canceled_codec_installation = False
+
+encoders = (
+ ('flacenc', 'FLAC'),
+ ('wavenc', 'WAV'),
+ ('vorbisenc', 'Ogg Vorbis'),
+ ('oggmux', 'Ogg Vorbis'),
+ ('id3v2mux', 'MP3 Tags'),
+ ('xingmux', 'Xing Header'),
+ ('lame', 'MP3'),
+ ('faac', 'AAC'),
+ ('mp4mux', 'AAC'),
+ ('opusenc', 'Opus'),
+ )
+
+available_elements = set()
+
+for encoder, name in encoders:
+ have_it = bool(gst.element_factory_find(encoder))
+ if have_it:
+ available_elements.add(encoder)
+ else:
+ print (' "%s" gstreamer element not found'
+ ', disabling %s output.' % (encoder, name))
+
+if 'oggmux' not in available_elements:
+ available_elements.discard('vorbisenc')
+
+
+class Pipeline(BackgroundTask):
+
+ """A background task for running a GstPipeline."""
+
+ def __init__(self):
+ BackgroundTask.__init__(self)
+ self.pipeline = None
+ self.sound_file = None
+ self.command = []
+ self.parsed = False
+ self.signals = []
+ self.processing = False
+ self.eos = False
+ self.error = None
+ self.connected_signals = []
+
+ def started(self):
+ self.play()
+
+ def cleanup(self):
+ for element, sid in self.connected_signals:
+ element.disconnect(sid)
+ self.connected_signals = []
+ self.stop_pipeline()
+
+ def aborted(self):
+ self.cleanup()
+
+ def finished(self):
+ self.cleanup()
+
+ def add_command(self, command):
+ self.command.append(command)
+
+ def add_signal(self, name, signal, callback):
+ self.signals.append((name, signal, callback,))
+
+ def toggle_pause(self, paused):
+ if not self.pipeline:
+ debug('toggle_pause(): pipeline is None !')
+ return
+
+ if paused:
+ self.pipeline.set_state(gst.STATE_PAUSED)
+ else:
+ self.pipeline.set_state(gst.STATE_PLAYING)
+
+ def found_tag(self, decoder, something, taglist):
+ pass
+
+ def restart(self):
+ self.parsed = False
+ self.duration = None
+ self.finished()
+ if vfs_exists(self.output_filename):
+ vfs_unlink(self.output_filename)
+ self.play()
+
+ def install_plugin_cb(self, result):
+ if result in (gst.pbutils.INSTALL_PLUGINS_SUCCESS,
+ gst.pbutils.INSTALL_PLUGINS_PARTIAL_SUCCESS):
+ gst.update_registry()
+ self.restart()
+ return
+ if result == gst.pbutils.INSTALL_PLUGINS_USER_ABORT:
+ self.error = _('Plugin installation aborted.')
+ global user_canceled_codec_installation
+ user_canceled_codec_installation = True
+ self.done()
+ return
+ self.done()
+ show_error('Error', 'failed to install plugins: %s' % gobject.markup_escape_text(str(result)))
+
+ def on_error(self, error):
+ self.error = error
+ log('error: %s (%s)' % (error, self.command))
+
+ def on_message(self, bus, message):
+ t = message.type
+ import gst
+ if t == gst.MESSAGE_ERROR:
+ error, _ = message.parse_error()
+ self.eos = True
+ self.error = error
+ self.on_error(error)
+ self.done()
+ elif gst.pbutils.is_missing_plugin_message(message):
+ global user_canceled_codec_installation
+ detail = gst.pbutils.missing_plugin_message_get_installer_detail(message)
+ debug('missing plugin:', detail.split('|')[3] , self.sound_file.uri)
+ self.pipeline.set_state(gst.STATE_NULL)
+ if gst.pbutils.install_plugins_installation_in_progress():
+ while gst.pbutils.install_plugins_installation_in_progress():
+ gtk_sleep(0.1)
+ self.restart()
+ return
+ if user_canceled_codec_installation:
+ self.error = 'Plugin installation cancelled'
+ debug(self.error)
+ self.done()
+ return
+ ctx = gst.pbutils.InstallPluginsContext()
+ gst.pbutils.install_plugins_async([detail], ctx, self.install_plugin_cb)
+
+ elif t == gst.MESSAGE_EOS:
+ self.eos = True
+ self.done()
+
+ elif t == gst.MESSAGE_TAG:
+ self.found_tag(self, '', message.parse_tag())
+ return True
+
+ def play(self):
+ if not self.parsed:
+ command = ' ! '.join(self.command)
+ debug('launching: \'%s\'' % command)
+ try:
+ self.pipeline = gst.parse_launch(command)
+ bus = self.pipeline.get_bus()
+ assert not self.connected_signals
+ self.connected_signals = []
+ for name, signal, callback in self.signals:
+ if name:
+ element = self.pipeline.get_by_name(name)
+ else:
+ element = bus
+ sid = element.connect(signal, callback)
+ self.connected_signals.append((element, sid,))
+
+ self.parsed = True
+
+ except gobject.GError, e:
+ show_error('GStreamer error when creating pipeline', str(e))
+ self.error = str(e)
+ self.eos = True
+ self.done()
+ return
+
+ bus.add_signal_watch()
+ watch_id = bus.connect('message', self.on_message)
+ self.watch_id = watch_id
+
+ self.pipeline.set_state(gst.STATE_PLAYING)
+
+ def stop_pipeline(self):
+ if not self.pipeline:
+ debug('pipeline already stopped!')
+ return
+ bus = self.pipeline.get_bus()
+ bus.disconnect(self.watch_id)
+ bus.remove_signal_watch()
+ self.pipeline.set_state(gst.STATE_NULL)
+ #self.pipeline = None
+
+ def get_position(self):
+ return NotImplementedError
+
+
+class TypeFinder(Pipeline):
+
+ def __init__(self, sound_file):
+ Pipeline.__init__(self)
+ self.sound_file = sound_file
+
+ command = '%s location="%s" ! typefind name=typefinder ! fakesink' % \
+ (gstreamer_source, encode_filename(self.sound_file.uri))
+ self.add_command(command)
+ self.add_signal('typefinder', 'have-type', self.have_type)
+
+ def on_error(self, error):
+ self.error = error
+ log('error: %s (%s)' % (error, self.sound_file.filename_for_display))
+
+ def set_found_type_hook(self, found_type_hook):
+ self.found_type_hook = found_type_hook
+
+ def have_type(self, typefind, probability, caps):
+ mime_type = caps.to_string()
+ debug('have_type:', mime_type,
+ self.sound_file.filename_for_display)
+ self.sound_file.mime_type = None
+ #self.sound_file.mime_type = mime_type
+ for t in mime_whitelist:
+ if t in mime_type:
+ self.sound_file.mime_type = mime_type
+ if not self.sound_file.mime_type:
+ log('mime type skipped: %s' % mime_type)
+ for t in filename_blacklist:
+ if fnmatch(self.sound_file.uri, t):
+ self.sound_file.mime_type = None
+ log('filename blacklisted (%s): %s' % (t,
+ self.sound_file.filename_for_display))
+
+ self.pipeline.set_state(gst.STATE_NULL)
+ self.done()
+
+ def finished(self):
+ Pipeline.finished(self)
+ if self.error:
+ return
+ if self.found_type_hook and self.sound_file.mime_type:
+ gobject.idle_add(self.found_type_hook, self.sound_file,
+ self.sound_file.mime_type)
+ self.sound_file.mime_type = True # remove string
+
+
+class Decoder(Pipeline):
+
+ """A GstPipeline background task that decodes data and finds tags."""
+
+ def __init__(self, sound_file):
+ Pipeline.__init__(self)
+ self.sound_file = sound_file
+ self.time = 0
+ self.position = 0
+
+ command = '%s location="%s" name=src ! decodebin name=decoder' % \
+ (gstreamer_source, encode_filename(self.sound_file.uri))
+ self.add_command(command)
+ self.add_signal('decoder', 'new-decoded-pad', self.new_decoded_pad)
+
+ def on_error(self, error):
+ self.error = error
+ log('error: %s (%s)' % (error,
+ self.sound_file.filename_for_display))
+
+ def have_type(self, typefind, probability, caps):
+ pass
+
+ def query_duration(self):
+ """
+ Ask for the duration of the current pipeline.
+ """
+ try:
+ if not self.sound_file.duration and self.pipeline:
+ self.sound_file.duration = self.pipeline.query_duration(
+ gst.FORMAT_TIME)[0] / gst.SECOND
+ debug('got file duration:', self.sound_file.duration)
+ if self.sound_file.duration < 0:
+ self.sound_file.duration = None
+ except gst.QueryError:
+ self.sound_file.duration = None
+
+ def query_position(self):
+ """
+ Ask for the stream position of the current pipeline.
+ """
+ try:
+ if self.pipeline:
+ self.position = self.pipeline.query_position(
+ gst.FORMAT_TIME)[0] / gst.SECOND
+ if self.position < 0:
+ self.position = 0
+ except gst.QueryError:
+ self.position = 0
+
+
+ def found_tag(self, decoder, something, taglist):
+ """
+ Called when the decoder reads a tag.
+ """
+ debug('found_tags:', self.sound_file.filename_for_display)
+ for k in taglist.keys():
+ if 'image' not in k:
+ debug('\t%s=%s' % (k, taglist[k]))
+ if isinstance(taglist[k], gst.Date):
+ taglist['year'] = taglist[k].year
+ taglist['date'] = '%04d-%02d-%02d' % (taglist[k].year,
+ taglist[k].month, taglist[k].day)
+ tag_whitelist = (
+ 'artist',
+ 'album',
+ 'title',
+ 'track-number',
+ 'track-count',
+ 'genre',
+ 'date',
+ 'year',
+ 'timestamp',
+ 'disc-number',
+ 'disc-count',
+ )
+ tags = {}
+ for k in taglist.keys():
+ if k in tag_whitelist:
+ tags[k] = taglist[k]
+
+ self.sound_file.tags.update(tags)
+ self.query_duration()
+
+ def new_decoded_pad(self, decoder, pad, is_last):
+ """ called when a decoded pad is created """
+ self.query_duration()
+ self.processing = True
+
+ def finished(self):
+ Pipeline.finished(self)
+
+ def get_sound_file(self):
+ return self.sound_file
+
+ def get_input_uri(self):
+ return self.sound_file.uri
+
+ def get_duration(self):
+ """ return the total duration of the sound file """
+ self.query_duration()
+ return self.sound_file.duration
+
+ def get_position(self):
+ """ return the current pipeline position in the stream """
+ self.query_position()
+ return self.position
+
+
+class TagReader(Decoder):
+
+ """A GstPipeline background task for finding meta tags in a file."""
+
+ def __init__(self, sound_file):
+ Decoder.__init__(self, sound_file)
+ self.found_tag_hook = None
+ self.found_tags = False
+ self.tagread = False
+ self.run_start_time = 0
+ self.add_command('fakesink')
+ self.add_signal(None, 'message::state-changed', self.on_state_changed)
+ self.tagread = False
+
+ def set_found_tag_hook(self, found_tag_hook):
+ self.found_tag_hook = found_tag_hook
+
+ def on_state_changed(self, bus, message):
+ prev, new, pending = message.parse_state_changed()
+ if new == gst.STATE_PLAYING and not self.tagread:
+ self.tagread = True
+ debug('TagReading done...')
+ self.done()
+
+ def finished(self):
+ Pipeline.finished(self)
+ self.sound_file.tags_read = True
+ if self.found_tag_hook:
+ gobject.idle_add(self.found_tag_hook, self)
+
+
+class Converter(Decoder):
+
+ """A background task for converting files to another format."""
+
+ def __init__(self, sound_file, output_filename, output_type,
+ delete_original=False, output_resample=False,
+ resample_rate=48000, force_mono=False):
+ Decoder.__init__(self, sound_file)
+
+ self.output_filename = output_filename
+ self.output_type = output_type
+ self.vorbis_quality = 0.6
+ self.aac_quality = 192
+ self.mp3_bitrate = 192
+ self.mp3_mode = 'vbr'
+ self.mp3_quality = 3
+ self.flac_compression = 8
+ self.wav_sample_width = 16
+
+ self.output_resample = output_resample
+ self.resample_rate = resample_rate
+ self.force_mono = force_mono
+
+ self.delete_original = delete_original
+
+ self.got_duration = False
+
+ def init(self):
+ self.encoders = {
+ 'audio/x-vorbis': self.add_oggvorbis_encoder,
+ 'audio/x-flac': self.add_flac_encoder,
+ 'audio/x-wav': self.add_wav_encoder,
+ 'audio/mpeg': self.add_mp3_encoder,
+ 'audio/x-m4a': self.add_aac_encoder,
+ 'audio/ogg; codecs=opus': self.add_opus_encoder,
+ 'gst-profile': self.add_audio_profile,
+ }
+ self.add_command('audiorate tolerance=10000000')
+ self.add_command('audioconvert')
+ self.add_command('audioresample')
+
+ # audio resampling support
+ if self.output_resample:
+ self.add_command('audio/x-raw-int,rate=%d' % self.resample_rate)
+ self.add_command('audioconvert')
+ self.add_command('audioresample')
+
+ if self.force_mono:
+ self.add_command('audio/x-raw-int,channels=1')
+ self.add_command('audioconvert')
+
+ encoder = self.encoders[self.output_type]()
+ if not encoder:
+ # TODO: is this used ?
+ # TODO: add proper error management when an encoder cannot be created
+ show_error(_('Error', "Cannot create a decoder for \'%s\' format.") %
+ self.output_type)
+ return
+
+ self.add_command(encoder)
+
+ uri = gnomevfs.URI(self.output_filename)
+ dirname = uri.parent
+ if dirname and not gnomevfs.exists(dirname):
+ log('Creating folder: \'%s\'' % dirname)
+ if not vfs_makedirs(str(dirname)):
+ show_error('Error', _("Cannot create \'%s\' folder.") % dirname)
+ return
+
+ self.add_command('%s location="%s"' % (
+ gstreamer_sink, encode_filename(self.output_filename)))
+
+ def aborted(self):
+ # remove partial file
+ try:
+ gnomevfs.unlink(self.output_filename)
+ except:
+ log('cannot delete: \'%s\'' % beautify_uri(self.output_filename))
+ return
+
+ def finished(self):
+ Pipeline.finished(self)
+
+ # Copy file permissions
+ try:
+ info = gnomevfs.get_file_info(self.sound_file.uri,
+ gnomevfs.FILE_INFO_FIELDS_PERMISSIONS)
+ gnomevfs.set_file_info(self.output_filename, info,
+ gnomevfs.SET_FILE_INFO_PERMISSIONS)
+ except:
+ log('Cannot set permission on \'%s\'' %
+ gnomevfs.format_uri_for_display(self.output_filename))
+
+ if self.delete_original and self.processing and not self.error:
+ log('deleting: \'%s\'' % self.sound_file.uri)
+ try:
+ vfs_unlink(self.sound_file.uri)
+ except:
+ log('Cannot remove \'%s\'' %
+ gnomevfs.format_uri_for_display(self.output_filename))
+
+ def on_error(self, err):
+ #pass
+ self.error = err
+ show_error('<b>%s</b>' % _('GStreamer Error:'), '%s\n<i>(%s)</i>' % (err,
+ self.sound_file.filename_for_display))
+
+ def set_vorbis_quality(self, quality):
+ self.vorbis_quality = quality
+
+ def set_aac_quality(self, quality):
+ self.aac_quality = quality
+
+ def set_opus_quality(self, quality):
+ self.opus_quality = quality
+
+ def set_mp3_mode(self, mode):
+ self.mp3_mode = mode
+
+ def set_mp3_quality(self, quality):
+ self.mp3_quality = quality
+
+ def set_flac_compression(self, compression):
+ self.flac_compression = compression
+
+ def set_wav_sample_width(self, sample_width):
+ self.wav_sample_width = sample_width
+
+ def set_audio_profile(self, audio_profile):
+ self.audio_profile = audio_profile
+
+ def add_flac_encoder(self):
+ s = 'flacenc mid-side-stereo=true quality=%s' % self.flac_compression
+ return s
+
+ def add_wav_encoder(self):
+ return 'audio/x-raw-int,width=%d ! wavenc' % (
+ self.wav_sample_width)
+
+ def add_oggvorbis_encoder(self):
+ cmd = 'vorbisenc'
+ if self.vorbis_quality is not None:
+ cmd += ' quality=%s' % self.vorbis_quality
+ cmd += ' ! oggmux '
+ return cmd
+
+ def add_mp3_encoder(self):
+ cmd = 'lamemp3enc encoding-engine-quality=2 '
+
+ if self.mp3_mode is not None:
+ properties = {
+ 'cbr' : 'target=bitrate cbr=true bitrate=%s ',
+ 'abr' : 'target=bitrate cbr=false bitrate=%s ',
+ 'vbr' : 'target=quality cbr=false quality=%s ',
+ }
+
+ cmd += properties[self.mp3_mode] % self.mp3_quality
+
+ if 'xingmux' in available_elements and properties[self.mp3_mode][0]:
+ # add xing header when creating VBR mp3
+ cmd += '! xingmux '
+
+ if 'id3v2mux' in available_elements:
+ # add tags
+ cmd += '! id3v2mux '
+
+ return cmd
+
+ def add_aac_encoder(self):
+ return 'faac bitrate=%s ! mp4mux' % (self.aac_quality * 1000)
+
+ def add_opus_encoder(self):
+ return 'opusenc bitrate=%s ! oggmux' % (self.opus_quality * 1000)
+
+ def add_audio_profile(self):
+ pipeline = audio_profiles_dict[self.audio_profile][2]
+ return pipeline
+
+
+class ConverterQueue(TaskQueue):
+
+ """Background task for converting many files."""
+
+ def __init__(self, window):
+ TaskQueue.__init__(self)
+ self.window = window
+ self.reset_counters()
+
+ def reset_counters(self):
+ self.total_duration = 0
+ self.duration_processed = 0
+ self.errors = []
+ self.error_count = 0
+ self.all_tasks = None
+ global user_canceled_codec_installation
+ user_canceled_codec_installation = True
+
+ def add(self, sound_file):
+ # generate a temporary filename from source name and output suffix
+ output_filename = self.window.prefs.generate_temp_filename(sound_file) + '~SC~'
+
+ if vfs_exists(output_filename):
+ # always overwrite temporary files
+ vfs_unlink(output_filename)
+
+ c = Converter(sound_file, output_filename,
+ self.window.prefs.get_string('output-mime-type'),
+ self.window.prefs.get_int('delete-original'),
+ self.window.prefs.get_int('output-resample'),
+ self.window.prefs.get_int('resample-rate'),
+ self.window.prefs.get_int('force-mono'),
+ )
+ c.set_vorbis_quality(self.window.prefs.get_float('vorbis-quality'))
+ c.set_aac_quality(self.window.prefs.get_int('aac-quality'))
+ c.set_opus_quality(self.window.prefs.get_int('opus-bitrate'))
+ c.set_flac_compression(self.window.prefs.get_int('flac-compression'))
+ c.set_wav_sample_width(self.window.prefs.get_int('wav-sample-width'))
+ c.set_audio_profile(self.window.prefs.get_string('audio-profile'))
+
+ quality = {
+ 'cbr': 'mp3-cbr-quality',
+ 'abr': 'mp3-abr-quality',
+ 'vbr': 'mp3-vbr-quality'
+ }
+ mode = self.window.prefs.get_string('mp3-mode')
+ c.set_mp3_mode(mode)
+ c.set_mp3_quality(self.window.prefs.get_int(quality[mode]))
+ c.init()
+ c.add_listener('finished', self.on_task_finished)
+ self.add_task(c)
+
+ def get_progress(self, per_file_progress):
+ tasks = self.running_tasks
+
+ # try to get all tasks durations
+ if not self.all_tasks:
+ self.all_tasks = []
+ self.all_tasks.extend(self.waiting_tasks)
+ self.all_tasks.extend(self.running_tasks)
+
+ for task in self.all_tasks:
+ if task.sound_file.duration is None:
+ duration = task.get_duration()
+ if duration:
+ self.total_duration += duration
+
+ position = 0.0
+ prolist = []
+ for task in range(self.finished_tasks): # TODO: use the add, luke
+ prolist.append(1.0)
+ for task in tasks:
+ if task.running:
+ position += task.get_position()
+ taskprogress = float(task.get_position()) / task.sound_file.duration if task.sound_file.duration else 0
+ taskprogress = min(max(taskprogress, 0.0), 1.0)
+ prolist.append(taskprogress)
+ per_file_progress[task.sound_file] = taskprogress
+ for task in self.waiting_tasks:
+ prolist.append(0.0)
+
+ progress = sum(prolist)/len(prolist) if prolist else 0
+ progress = min(max(progress, 0.0), 1.0)
+ return self.running or len(self.all_tasks), progress
+
+ def on_task_finished(self, task):
+ task.sound_file.progress = 1.0
+
+ if task.error:
+ debug('error in task, skipping rename:', task.output_filename)
+ if vfs_exists(task.output_filename):
+ vfs_unlink(task.output_filename)
+ self.errors.append(task.error)
+ self.error_count += 1
+ return
+
+ duration = task.get_duration()
+ if duration:
+ self.duration_processed += duration
+
+ # rename temporary file
+ newname = self.window.prefs.generate_filename(task.sound_file)
+ log(beautify_uri(task.output_filename), '->', beautify_uri(newname))
+
+ # safe mode. generate a filename until we find a free one
+ p,e = os.path.splitext(newname)
+ p = p.replace('%', '%%')
+ p = p + ' (%d)' + e
+ i = 1
+ while vfs_exists(newname):
+ newname = p % i
+ i += 1
+
+ task.error = vfs_rename(task.output_filename, newname)
+ if task.error:
+ self.errors.append(task.error)
+ self.error_count += 1
+
+ def finished(self):
+ # This must be called with emit_async
+ if self.running_tasks:
+ raise RuntimeError
+ TaskQueue.finished(self)
+ self.window.set_sensitive()
+ self.window.conversion_ended()
+ total_time = self.run_finish_time - self.run_start_time
+ msg = _('Conversion done in %s') % self.format_time(total_time)
+ if self.error_count:
+ msg += ', %d error(s)' % self.error_count
+ self.window.set_status(msg)
+ if not self.window.is_active():
+ notification(msg) # this must move
+ self.reset_counters()
+
+ def format_time(self, seconds):
+ units = [(86400, 'd'),
+ (3600, 'h'),
+ (60, 'm'),
+ (1, 's')]
+ seconds = round(seconds)
+ result = []
+ for factor, unity in units:
+ count = int(seconds / factor)
+ seconds -= count * factor
+ if count > 0 or (factor == 1 and not result):
+ result.append('%d %s' % (count, unity))
+ assert seconds == 0
+ return ' '.join(result)
+
+ def abort(self):
+ TaskQueue.abort(self)
+ self.window.set_sensitive()
+ self.reset_counters()
diff --git a/soundconverter/messagearea.py b/soundconverter/messagearea.py
new file mode 100644
index 0000000..9bb3d87
--- /dev/null
+++ b/soundconverter/messagearea.py
@@ -0,0 +1,219 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# SoundConverter - GNOME application for converting between audio formats.
+# Copyright 2004 Lars Wirzenius
+# Copyright 2005-2012 Gautier Portet
+#
+# This program 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; version 3 of the License.
+#
+# This program 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 this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+
+# THIS FILE WAS PART OF THE JOKOSHER PROJECT AND LICENSED UNDER THE GPL.
+
+import gtk
+import gobject
+
+
+class MessageArea(gtk.HBox):
+
+ __gsignals__ = {
+ "response" : ( gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_INT,) ),
+ "close" : ( gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, gobject.TYPE_NONE, () )
+ }
+
+ def __init__(self):
+ gtk.HBox.__init__(self)
+
+ self.contents = None
+ self.changing_style = False
+
+ self.main_hbox = gtk.HBox(False, 16) # FIXME: use style properties
+ self.main_hbox.show()
+ self.main_hbox.set_border_width(8) # FIXME: use style properties
+
+ self.action_area = gtk.VBox(True, 3) # FIXME: use style properties */
+ self.action_area.show()
+
+ self.main_hbox.pack_end(self.action_area, False, True)
+ self.pack_start(self.main_hbox, True, True)
+
+ self.set_app_paintable(True)
+
+ #self.connect("expose-event", self.paint_message_area)
+ self.connect("size-allocate", self.on_size_allocate)
+
+ # Note that we connect to style-set on one of the internal
+ # widgets, not on the message area itself, since gtk does
+ # not deliver any further style-set signals for a widget on
+ # which the style has been forced with gtk_widget_set_style()
+ self.main_hbox.connect("style-set", self.style_set)
+
+ def on_size_allocate(self, widget, rectangle):
+ # force a _complete_ redraw here or else in certain cases after resizing
+ # some border lines are left painted on top of the main content area.
+ self.queue_draw()
+
+ def style_set(self, widget, prev_style):
+ if self.changing_style:
+ return
+
+ # This is a hack needed to use the tooltip background color
+ window = gtk.Window(gtk.WINDOW_POPUP)
+ window.set_name("gtk-tooltip")
+ window.ensure_style()
+ style = window.get_style()
+
+ self.changing_style = True
+ self.set_style(style)
+ self.changing_style = False
+
+ window.destroy()
+
+ self.queue_draw()
+
+ def paint_message_area(self, widget, event):
+ a = widget.get_allocation()
+ x = a.x + 1
+ y = a.y + 1
+ width = a.width - 2
+ height = a.height - 2
+ widget.style.paint_flat_box(widget.window, gtk.STATE_NORMAL, gtk.SHADOW_OUT, None,
+ widget, "tooltip", x, y, width, height)
+ return False
+
+ def action_widget_activated(self, widget):
+ resp = self.get_response_data(widget)
+ if resp is None:
+ resp = gtk.RESPONSE_NONE
+ self.response(resp)
+
+ def get_response_data(self, widget):
+ return widget.get_data("gedit-message-area-response-data")
+
+ def set_response_data(self, widget, new_id):
+ widget.set_data("gedit-message-area-response-data", new_id)
+
+ def add_action_widget(self, child, response_id):
+ self.set_response_data(child, response_id)
+
+ try:
+ signal = child.get_activate_signal()
+ except ValueError:
+ signal = None
+
+ if isinstance(child, gtk.Button):
+ child.connect("clicked", self.action_widget_activated)
+ elif signal:
+ child.connect(signal, self.action_widget_activated)
+ else:
+ pass
+ #g_warning("Only 'activatable' widgets can be packed into the action area of a GeditMessageArea");
+
+ if response_id != gtk.RESPONSE_HELP:
+ self.action_area.pack_end(child, False, False)
+ else:
+ self.action_area.pack_start(child, False, False)
+
+
+ def add_button(self, text, response_id):
+ button = gtk.Button(stock=text)
+ button.set_flags(gtk.CAN_DEFAULT)
+ button.show()
+ self.add_action_widget(button, response_id)
+ return button
+
+ def add_buttons(self, *buttons):
+ for text, response_id in buttons:
+ self.add_button(text, response_id)
+
+ def set_response_sensitive(self, response_id, setting):
+ children = self.action_area.get_children()
+
+ for child in children:
+ rd = self.get_response_data(child)
+ if rd == response_id:
+ child.set_sensitive(setting)
+
+ def set_default_response(self, response_id):
+ children = self.action_area.get_children()
+
+ for child in children:
+ rd = self.get_response_data(child)
+ if rd == response_id:
+ child.grab_default()
+
+ def response(self, response_id):
+ self.emit("response", response_id)
+
+ def add_stock_button_with_text(self, text, stock_id, response_id):
+ button = gtk.Button(text, use_underline=True)
+ button.set_image(gtk.image_new_from_stock(stock_id,gtk.ICON_SIZE_BUTTON))
+ button.set_flags(gtk.CAN_DEFAULT)
+ button.show()
+ self.add_action_widget(button, response_id)
+
+ def set_contents(self, contents):
+ self.contents = contents;
+ self.main_hbox.pack_start(self.contents, True, True)
+
+ def set_text_and_icon(self, icon_stock_id, primary_text,
+ secondary_text=None, additionnal_widget=None):
+ hbox_content = gtk.HBox(False, 8)
+ hbox_content.show()
+
+ image = gtk.image_new_from_stock(icon_stock_id, gtk.ICON_SIZE_DIALOG)
+ image.show()
+ hbox_content.pack_start(image, False, False)
+ image.set_alignment(0.5, 0.5)
+
+ vbox = gtk.VBox(False, 6)
+ vbox.show()
+ hbox_content.pack_start(vbox, True, True)
+
+ primary_markup = "<b>%s</b>" % primary_text
+ primary_label = gtk.Label(primary_markup)
+ primary_label.set_use_markup(True)
+ primary_label.set_line_wrap(True)
+ primary_label.set_alignment(0, 0.5)
+ primary_label.set_flags(gtk.CAN_FOCUS)
+ primary_label.set_selectable(True)
+
+ textcolor = self.get_style().text[1]
+ self.get_style().text[0] = self.get_style().text[1]
+
+ primary_label.show()
+
+ vbox.pack_start(primary_label, True, True)
+
+ if secondary_text:
+ secondary_markup = "<small>%s</small>" % secondary_text
+ secondary_label = gtk.Label(secondary_markup)
+ secondary_label.set_flags(gtk.CAN_FOCUS)
+ secondary_label.set_use_markup(True)
+ secondary_label.set_line_wrap(True)
+ secondary_label.set_selectable(True)
+ secondary_label.set_alignment(0, 0.5)
+ secondary_label.show()
+
+ vbox.pack_start(secondary_label, True, True)
+
+ if additionnal_widget:
+ vbox.pack_start(additionnal_widget, True, True)
+
+ self.set_contents(hbox_content)
+
+
+gtk.binding_entry_add_signal(MessageArea, gtk.gdk.keyval_from_name("Escape"), 0, "close")
+
+
diff --git a/soundconverter/namegenerator.py b/soundconverter/namegenerator.py
new file mode 100644
index 0000000..7ec7e17
--- /dev/null
+++ b/soundconverter/namegenerator.py
@@ -0,0 +1,110 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# SoundConverter - GNOME application for converting between audio formats.
+# Copyright 2004 Lars Wirzenius
+# Copyright 2005-2012 Gautier Portet
+#
+# This program 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; version 3 of the License.
+#
+# This program 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 this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+
+import time
+import os
+import urllib
+import gnomevfs
+from fileoperations import vfs_exists
+
+
+class TargetNameGenerator:
+
+ """Generator for creating the target name from an input name."""
+
+ bad_chars = u'\\?%*:|"<>\ufffd'
+
+ def __init__(self):
+ self.folder = None
+ self.subfolders = ''
+ self.basename = '%(.inputname)s'
+ self.ext = '%(.ext)s'
+ self.suffix = None
+ self.replace_messy_chars = False
+ self.max_tries = 2
+ self.exists = vfs_exists
+
+ def get_target_name(self, sound_file):
+
+ assert self.suffix, 'you just forgot to call set_target_suffix()'
+
+ u = gnomevfs.URI(sound_file.uri)
+ root, ext = os.path.splitext(u.path)
+
+ root = sound_file.base_path
+ basename, ext = os.path.splitext(urllib.unquote(sound_file.filename))
+
+ # make sure basename contains only the filename
+ basefolder, basename = os.path.split(basename)
+
+ d = {
+ '.inputname': basename,
+ '.ext': ext,
+ '.target-ext': self.suffix[1:],
+ 'album': _('Unknown Album'),
+ 'artist': _('Unknown Artist'),
+ 'title': basename,
+ 'track-number': 0,
+ 'track-count': 0,
+ 'genre': '',
+ 'year': '',
+ 'date': '',
+ 'disc-number': 0,
+ 'disc-count': 0,
+ }
+ for key in sound_file.tags:
+ d[key] = sound_file.tags[key]
+ if isinstance(d[key], basestring):
+ # take care of tags containing slashes
+ d[key] = d[key].replace('/', '-')
+
+ # add timestamp to substitution dict -- this could be split into more
+ # entries for more fine-grained control over the string by the user...
+ timestamp_string = time.strftime('%Y%m%d_%H_%M_%S')
+ d['timestamp'] = timestamp_string
+
+ pattern = os.path.join(self.subfolders, self.basename + self.suffix)
+ result = pattern % d
+
+ if self.replace_messy_chars:
+ # convert to unicode object to manage filename letter by letter
+ if not isinstance(result, unicode):
+ result = unicode(result, 'utf-8', 'replace')
+
+ for char in self.bad_chars:
+ result = result.replace(char, '_')
+
+ # convert back to string so that urllib could cope with it
+ if isinstance(result, unicode):
+ result = result.encode('utf-8')
+
+ if self.folder is None:
+ folder = root
+ else:
+ folder = urllib.quote(self.folder, '/:%@')
+
+ if '/' in pattern:
+ # we are creating folders using tags, disable basefolder handling
+ basefolder = ''
+
+ result = os.path.join(folder, basefolder, urllib.quote(result))
+
+ return result
diff --git a/soundconverter/notify.py b/soundconverter/notify.py
new file mode 100644
index 0000000..4a1725e
--- /dev/null
+++ b/soundconverter/notify.py
@@ -0,0 +1,44 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# SoundConverter - GNOME application for converting between audio formats.
+# Copyright 2004 Lars Wirzenius
+# Copyright 2005-2012 Gautier Portet
+#
+# This program 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; version 3 of the License.
+#
+# This program 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 this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+
+
+def _notification_dummy(message):
+ pass
+
+
+notification = _notification_dummy
+
+try:
+ import pynotify
+
+
+ def _notification(message):
+ try:
+ n = pynotify.Notification('SoundConverter', message)
+ n.show()
+ except:
+ pass
+
+ if pynotify.init('Basics'):
+ notification = _notification
+
+except ImportError:
+ pass
diff --git a/soundconverter/queue.py b/soundconverter/queue.py
new file mode 100644
index 0000000..d0727e9
--- /dev/null
+++ b/soundconverter/queue.py
@@ -0,0 +1,133 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# SoundConverter - GNOME application for converting between audio formats.
+# Copyright 2004 Lars Wirzenius
+# Copyright 2005-2012 Gautier Portet
+#
+# This program 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; version 3 of the License.
+#
+# This program 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 this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+
+import time
+from task import BackgroundTask
+from settings import settings
+from utils import log
+
+
+class TaskQueue(BackgroundTask):
+
+ """A queue of tasks.
+
+ A task queue is a queue of other tasks. If you need, for example, to
+ do simple tasks A, B, and C, you can create a TaskQueue and add the
+ simple tasks to it:
+
+ q = TaskQueue()
+ q.add_task(A)
+ q.add_task(B)
+ q.add_task(C)
+ q.start()
+
+ The task queue behaves as a single task. It will execute the
+ tasks in order and start the next one when the previous finishes."""
+
+ def __init__(self):
+ BackgroundTask.__init__(self)
+ self.waiting_tasks = []
+ self.running_tasks = []
+ self.finished_tasks = 0
+ self.start_time = None
+ self.count = 0
+ self.paused = False
+
+ def add_task(self, task):
+ """Add a task to the queue."""
+ self.waiting_tasks.append(task)
+ #if self.start_time and not self.running_tasks:
+ if self.start_time:
+ # add a task to a stalled taskqueue, shake it!
+ self.start_next_task()
+
+ def start_next_task(self):
+ if not self.waiting_tasks:
+ if not self.running_tasks:
+ self.done()
+ return
+
+ to_start = settings['jobs'] - len(self.running_tasks)
+ for i in range(to_start):
+ try:
+ task = self.waiting_tasks.pop(0)
+ except IndexError:
+ return
+ self.running_tasks.append(task)
+ task.add_listener('finished', self.task_finished)
+ task.start()
+ if self.paused:
+ task.toggle_pause(True)
+ self.count += 1
+ total = len(self.waiting_tasks) + self.finished_tasks
+ self.progress = float(self.finished_tasks) / total if total else 0
+
+ def started(self):
+ """ BackgroundTask setup callback """
+ log('Queue start: %d tasks, %d thread(s).' % (
+ len(self.waiting_tasks) + len(self.running_tasks),
+ settings['jobs']))
+ self.count = 0
+ self.paused = False
+ self.finished_tasks = 0
+ self.start_time = time.time()
+ self.start_next_task()
+
+ def finished(self):
+ """ BackgroundTask finish callback """
+ log('Queue done in %.3fs (%s tasks)' % (time.time() - self.start_time,
+ self.count))
+ self.queue_ended()
+ self.count = 0
+ self.start_time = None
+ self.running_tasks = []
+ self.waiting_tasks = []
+ self.running = False
+
+ def task_finished(self, task=None):
+ if not self.running_tasks:
+ return
+ if task in self.running_tasks:
+ self.running_tasks.remove(task)
+ self.finished_tasks += 1
+ self.start_next_task()
+
+ def abort(self):
+ for task in self.running_tasks:
+ task.abort()
+ BackgroundTask.abort(self)
+ self.running_tasks = []
+ self.waiting_tasks = []
+ self.running = False
+ self.start_time = None
+
+ def toggle_pause(self, paused):
+ self.paused = paused
+ for task in self.running_tasks:
+ task.toggle_pause(self.paused)
+
+ # The following is called when the Queue is finished
+ def queue_ended(self):
+ pass
+
+ # The following when progress changed
+ def progress_hook(self, progress):
+ pass
diff --git a/soundconverter/settings.py b/soundconverter/settings.py
new file mode 100644
index 0000000..d6cb3ff
--- /dev/null
+++ b/soundconverter/settings.py
@@ -0,0 +1,131 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# SoundConverter - GNOME application for converting between audio formats.
+# Copyright 2004 Lars Wirzenius
+# Copyright 2005-2012 Gautier Portet
+#
+# This program 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; version 3 of the License.
+#
+# This program 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 this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+
+from gettext import gettext as _
+
+# add here any format you want to be read
+mime_whitelist = (
+ 'audio/',
+ 'video/',
+ 'application/ogg',
+ 'application/x-id3',
+ 'application/x-ape',
+ 'application/vnd.rn-realmedia',
+ 'application/x-pn-realaudio',
+ 'application/x-shockwave-flash',
+ 'application/x-3gp',
+)
+
+filename_blacklist = (
+ '*.iso',
+)
+
+# TODO: remove locale patterns...
+
+# custom filename patterns
+english_patterns = 'Artist Album Title Track Total Genre Date Year Timestamp DiscNumber DiscTotal Ext'
+
+# traductors: These are the custom filename patterns. Only if it makes sense.
+locale_patterns = _('Artist Album Title Track Total Genre Date Year Timestamp DiscNumber DiscTotal Ext')
+
+patterns_formats = (
+ '%(artist)s',
+ '%(album)s',
+ '%(title)s',
+ '%(track-number)02d',
+ '%(track-count)02d',
+ '%(genre)s',
+ '%(date)s',
+ '%(year)s',
+ '%(timestamp)s',
+ '%(disc-number)d',
+ '%(disc-count)d',
+ '%(.target-ext)s',
+)
+
+# add english and locale
+custom_patterns = english_patterns + ' ' + locale_patterns
+# convert to list
+custom_patterns = ['{%s}' % p for p in custom_patterns.split()]
+# and finally to dict, thus removing doubles
+custom_patterns = dict(zip(custom_patterns, patterns_formats * 2))
+
+locale_patterns_dict = dict(zip(
+ [p.lower() for p in english_patterns.split()],
+ ['{%s}' % p for p in locale_patterns.split()]))
+
+# add here the formats not containing tags
+# not to bother searching in them
+tag_blacklist = (
+ 'audio/x-wav',
+)
+
+
+# Name and pattern for CustomFileChooser
+filepattern = (
+ (_('All files'), '*.*'),
+ ('MP3', '*.mp3'),
+ ('Ogg Vorbis', '*.ogg;*.oga'),
+ ('iTunes AAC ', '*.m4a'),
+ ('Windows WAV', '*.wav'),
+ ('AAC', '*.aac'),
+ ('FLAC', '*.flac'),
+ ('AC3', '*.ac3')
+)
+
+
+def cpu_count():
+ '''
+ Returns the number of CPUs in the system.
+ (from pyprocessing)
+ '''
+ import sys
+ import os
+ if sys.platform == 'win32':
+ try:
+ num = int(os.environ['NUMBER_OF_PROCESSORS'])
+ except (ValueError, KeyError):
+ num = 0
+ elif sys.platform == 'darwin':
+ try:
+ num = int(os.popen('sysctl -n hw.ncpu').read())
+ except ValueError:
+ num = 0
+ else:
+ try:
+ num = os.sysconf('SC_NPROCESSORS_ONLN')
+ except (ValueError, OSError, AttributeError):
+ num = 0
+ if num >= 1:
+ return num
+ else:
+ return 1
+
+# application-wide settings
+settings = {
+ 'mode': 'gui',
+ 'quiet': False,
+ 'debug': False,
+ 'cli-output-type': 'audio/x-vorbis',
+ 'cli-output-suffix': '.ogg',
+ 'jobs': cpu_count(),
+ 'max-jobs': cpu_count(),
+}
diff --git a/soundconverter/soundfile.py b/soundconverter/soundfile.py
new file mode 100644
index 0000000..d196d29
--- /dev/null
+++ b/soundconverter/soundfile.py
@@ -0,0 +1,62 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# SoundConverter - GNOME application for converting between audio formats.
+# Copyright 2004 Lars Wirzenius
+# Copyright 2005-2012 Gautier Portet
+#
+# This program 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; version 3 of the License.
+#
+# This program 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 this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+
+import os
+import gobject
+
+from fileoperations import unquote_filename
+
+
+class SoundFile:
+ """Meta data information about a sound file (uri, tags)."""
+ __slots__ = ['uri','base_path','filename','tags','tags_read','duration','mime_type']
+
+ def __init__(self, uri, base_path=None):
+ """
+ Create a SoundFile object.
+ if base_path is set, the uri is cut in two parts,
+ - the base folder
+ - the remaining folder+filename.
+ """
+
+ self.uri = uri
+
+ if base_path:
+ self.base_path = base_path
+ self.filename = self.uri[len(self.base_path):]
+ else:
+ self.base_path, self.filename = os.path.split(self.uri)
+ self.base_path += '/'
+
+ self.tags = {}
+ self.tags_read = False
+ self.duration = None
+ self.mime_type = None
+
+ @property
+ def filename_for_display(self):
+ """
+ Returns the filename in a suitable for display form.
+ """
+ return gobject.filename_display_name(
+ unquote_filename(self.filename))
+
+
diff --git a/soundconverter/task.py b/soundconverter/task.py
new file mode 100644
index 0000000..af7abc2
--- /dev/null
+++ b/soundconverter/task.py
@@ -0,0 +1,93 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# SoundConverter - GNOME application for converting between audio formats.
+# Copyright 2004 Lars Wirzenius
+# Copyright 2005-2012 Gautier Portet
+#
+# This program 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; version 3 of the License.
+#
+# This program 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 this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+
+import time
+import gobject
+
+
+class BackgroundTask:
+
+ """A background task.
+
+ To use: derive a subclass and define the methods started, and
+ finished. Then call the start() method when you want to start the task.
+ You must call done() when the processing is finished.
+ Call the abort() method if you want to stop the task before it finishes
+ normally."""
+
+ def __init__(self):
+ self.running = False
+ self.listeners = {}
+ self.progress = None
+
+ def start(self):
+ """Start running the task. Call started()."""
+ self.emit('started')
+ self.running = True
+ self.run_start_time = time.time()
+
+ def add_listener(self, signal, listener):
+ """Add a custom listener to the given signal.
+ Signals are 'started' and 'finished'"""
+ if signal not in self.listeners:
+ self.listeners[signal] = []
+ self.listeners[signal].append(listener)
+
+ def emit(self, signal):
+ """Call the signal handlers.
+ Callbacks are called as gtk idle funcs to be sure
+ they are in the main thread."""
+ gobject.idle_add(getattr(self, signal))
+ if signal in self.listeners:
+ for listener in self.listeners[signal]:
+ gobject.idle_add(listener, self)
+
+ def emit_sync(self, signal):
+ """Call the signal handlers.
+ Callbacks are called synchronously."""
+ getattr(self, signal)()
+ if signal in self.listeners:
+ for listener in self.listeners[signal]:
+ listener(self)
+
+ def done(self):
+ """Call to end normally the task."""
+ self.run_finish_time = time.time()
+ if self.running:
+ self.emit_sync('finished')
+ self.running = False
+
+ def abort(self):
+ """Stop task processing. finished() is not called."""
+ self.emit('aborted')
+ self.running = False
+
+ def aborted(self):
+ """Called when the task is aborted."""
+ pass
+
+ def started(self):
+ """Called when the task starts."""
+ pass
+
+ def finished(self):
+ """Clean up the task after all work has been done."""
+ pass
diff --git a/soundconverter/ui.py b/soundconverter/ui.py
new file mode 100644
index 0000000..dbedefd
--- /dev/null
+++ b/soundconverter/ui.py
@@ -0,0 +1,1509 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# SoundConverter - GNOME application for converting between audio formats.
+# Copyright 2004 Lars Wirzenius
+# Copyright 2005-2012 Gautier Portet
+#
+# This program 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; version 3 of the License.
+#
+# This program 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 this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+
+import os
+from os.path import basename, dirname
+import time
+import sys
+import gtk
+import gobject
+import gnome
+import gnomevfs
+import urllib
+from gettext import gettext as _
+
+from gconfstore import GConfStore
+from fileoperations import filename_to_uri, beautify_uri
+from fileoperations import unquote_filename, vfs_walk
+from fileoperations import use_gnomevfs
+from gstreamer import ConverterQueue
+from gstreamer import available_elements, TypeFinder, TagReader
+from gstreamer import audio_profiles_list, audio_profiles_dict
+from soundfile import SoundFile
+from settings import locale_patterns_dict, custom_patterns, filepattern, settings
+from namegenerator import TargetNameGenerator
+from queue import TaskQueue
+from utils import log, debug
+from messagearea import MessageArea
+from error import show_error
+
+# Names of columns in the file list
+MODEL = [ gobject.TYPE_STRING, # visible filename
+ gobject.TYPE_PYOBJECT, # soundfile
+ gobject.TYPE_FLOAT, # progress
+ gobject.TYPE_STRING, # status
+ gobject.TYPE_STRING, # complete filename
+ ]
+
+COLUMNS = ['filename']
+
+#VISIBLE_COLUMNS = ['filename']
+#ALL_COLUMNS = VISIBLE_COLUMNS + ['META']
+
+MP3_CBR, MP3_ABR, MP3_VBR = range(3)
+
+
+def gtk_iteration():
+ while gtk.events_pending():
+ gtk.main_iteration(False)
+
+
+def gtk_sleep(duration):
+ start = time.time()
+ while time.time() < start + duration:
+ time.sleep(0.010)
+ gtk_iteration()
+
+
+class ErrorDialog:
+
+ def __init__(self, builder):
+ self.dialog = builder.get_object('error_dialog')
+ self.dialog.set_transient_for(builder.get_object('window'))
+ self.primary = builder.get_object('primary_error_label')
+ self.secondary = builder.get_object('secondary_error_label')
+
+ def show_error(self, primary, secondary):
+ self.primary.set_markup(primary)
+ self.secondary.set_markup(secondary)
+ try:
+ sys.stderr.write(_('\nError: %s\n%s\n') % (primary, secondary))
+ except:
+ pass
+ self.dialog.run()
+ self.dialog.hide()
+
+
+class MsgAreaErrorDialog_:
+
+ def __init__(self, builder):
+ self.dialog = builder.get_object('error_frame')
+ self.primary = builder.get_object('label_error')
+
+ def show_error(self, primary, secondary):
+ try:
+ sys.stderr.write(_('\nError: %s\n%s\n') % (primary, secondary))
+ except:
+ pass
+ #self.msg_area.set_text_and_icon(gtk.STOCK_DIALOG_ERROR, primary, secondary)
+ #self.msg_area.show()
+ self.primary.set_text(primary)
+ self.dialog.show()
+
+
+ def show_exception(self, exception):
+ self.show('<b>%s</b>' % gobject.markup_escape_text(exception.primary),
+ exception.secondary)
+
+
+class FileList:
+ """List of files added by the user."""
+
+ # List of MIME types which we accept for drops.
+ drop_mime_types = ['text/uri-list', 'text/plain', 'STRING']
+
+ def __init__(self, window, builder):
+ self.window = window
+ self.typefinders = TaskQueue()
+ self.filelist = set()
+
+ self.model = apply(gtk.ListStore, MODEL)
+
+ self.widget = builder.get_object('filelist')
+ self.sortedmodel = gtk.TreeModelSort(self.model)
+ self.widget.set_model(self.sortedmodel)
+ self.sortedmodel.set_sort_column_id(4, gtk.SORT_ASCENDING)
+ self.widget.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
+
+ self.widget.drag_dest_set(gtk.DEST_DEFAULT_ALL,
+ map(lambda i:
+ (self.drop_mime_types[i], 0, i),
+ range(len(self.drop_mime_types))),
+ gtk.gdk.ACTION_COPY)
+ self.widget.connect('drag_data_received', self.drag_data_received)
+
+ renderer = gtk.CellRendererProgress()
+ column = gtk.TreeViewColumn('progress',
+ renderer,
+ value=2,
+ text=3,
+ )
+ self.widget.append_column(column)
+ self.progress_column = column
+ self.progress_column.set_visible(False)
+
+ renderer = gtk.CellRendererText()
+ import pango
+ renderer.set_property('ellipsize', pango.ELLIPSIZE_MIDDLE)
+ column = gtk.TreeViewColumn('Filename',
+ renderer,
+ markup=0,
+ )
+ column.set_expand(True)
+ self.widget.append_column(column)
+
+ self.window.progressbarstatus.hide()
+
+ self.waiting_files = []
+ # add files to filelist in batches. Much faster, and suffisant.
+ gobject.timeout_add(100, self.commit_waiting_files)
+ self.waiting_files_last = 0
+
+ def drag_data_received(self, widget, context, x, y, selection,
+ mime_id, time):
+ widget.stop_emission('drag_data_received')
+ if mime_id >= 0 and mime_id < len(self.drop_mime_types):
+ self.add_uris([uri.strip() for uri in selection.data.split('\n')])
+ context.finish(True, False, time)
+
+ def get_files(self):
+ return [i[1] for i in self.sortedmodel]
+
+ def update_progress(self, queue):
+ if queue.running:
+ progress = queue.progress if queue.progress else 0
+ self.window.progressbarstatus.set_fraction(progress)
+ return True
+ return False
+
+ def found_type(self, sound_file, mime):
+ debug('found_type', sound_file.filename)
+
+ self.append_file(sound_file)
+ self.window.set_sensitive()
+
+ def add_uris(self, uris, base=None, extensions=None):
+ files = []
+ self.window.set_status(_('Scanning files...'))
+
+ base = None
+
+ for uri in uris:
+ if not uri:
+ continue
+ if uri.startswith('cdda:'):
+ show_error('Cannot read from Audio CD.',
+ 'Use SoundJuicer Audio CD Extractor instead.')
+ return
+ try:
+ info = gnomevfs.get_file_info(gnomevfs.URI(uri),
+ gnomevfs.FILE_INFO_FOLLOW_LINKS)
+ except gnomevfs.NotFoundError:
+ log('uri not found: \'%s\'' % uri)
+ continue
+ except gnomevfs.InvalidURIError:
+ log('invalid uri: \'%s\'' % uri)
+ continue
+ except gnomevfs.AccessDeniedError:
+ log('access denied: \'%s\'' % uri)
+ continue
+ except TypeError, e:
+ log('add error: %s (\'%s\')' % (e, uri))
+ continue
+ except:
+ log('error in get_file_info: %s' % (uri))
+ continue
+
+ if info.type == gnomevfs.FILE_TYPE_DIRECTORY:
+ log('walking: \'%s\'' % uri)
+ if len(uris) == 1:
+ # if only one folder is passed to the function,
+ # use its parent as base path.
+ base = os.path.dirname(uri)
+ filelist = vfs_walk(gnomevfs.URI(uri))
+ accepted = []
+ if extensions:
+ for f in filelist:
+ for extension in extensions:
+ if f.lower().endswith(extension):
+ accepted.append(f)
+ filelist = accepted
+ files.extend(filelist)
+ else:
+ files.append(uri)
+
+ files = [f for f in files if not f.endswith('~SC~')]
+
+ if not base:
+ base = os.path.commonprefix(files)
+ if base and not base.endswith('/'):
+ # we want a common folder
+ base = base[0:base.rfind('/')]
+ base += '/'
+ else:
+ base += '/'
+
+ for f in files:
+ sound_file = SoundFile(f, base)
+ if sound_file.uri in self.filelist:
+ log('file already present: \'%s\'' % sound_file.uri)
+ continue
+
+ typefinder = TypeFinder(sound_file)
+ typefinder.set_found_type_hook(self.found_type)
+ self.typefinders.add_task(typefinder)
+
+ for i in self.model:
+ i[0] = self.format_cell(i[1])
+
+ if files and not self.typefinders.running:
+ self.window.progressbarstatus.show()
+ self.typefinders.queue_ended = self.typefinder_queue_ended
+ self.typefinders.start()
+ gobject.timeout_add(100, self.update_progress, self.typefinders)
+ else:
+ self.window.set_status()
+
+ def typefinder_queue_ended(self):
+ if not self.waiting_files:
+ self.window.set_status()
+ self.window.progressbarstatus.hide()
+
+ def abort(self):
+ self.typefinders.abort()
+
+ def format_cell(self, sound_file):
+ return '%s' % gobject.markup_escape_text(unquote_filename(
+ sound_file.filename))
+
+ def set_row_progress(self, number, progress=None, text=None):
+ self.progress_column.set_visible(True)
+ if progress is not None:
+ if self.model[number][2] == 1.0:
+ return # already...
+ self.model[number][2] = progress * 100.0
+ if text is not None:
+ self.model[number][3] = text
+
+ def hide_row_progress(self):
+ self.progress_column.set_visible(False)
+
+ def append_file(self, sound_file):
+ self.waiting_files.append(sound_file)
+
+ def commit_waiting_files(self):
+ if self.waiting_files_last != len(self.waiting_files):
+ # still adding files
+ self.waiting_files_last = len(self.waiting_files)
+ return True
+
+ if self.waiting_files:
+ self.window.set_status(_('Adding files...'))
+ save = self.widget.get_model()
+ self.widget.set_model(None)
+ n = 0.0
+ next = time.time()
+ while self.waiting_files:
+ self._append_file(self.waiting_files.pop())
+ n += 1
+ if time.time() > next:
+ # keep UI responsive
+ gtk_iteration()
+ self.window.progressbarstatus.set_fraction(n/self.waiting_files_last)
+ next = time.time() + 0.01
+ self.widget.set_model(save)
+
+ self.window.set_status()
+ self.window.progressbarstatus.hide()
+ return True
+
+ def _append_file(self, sound_file):
+ self.model.append([self.format_cell(sound_file), sound_file, 0.0, '',
+ sound_file.uri])
+ self.filelist.add(sound_file.uri)
+ sound_file.filelist_row = len(self.model) - 1
+
+ def remove(self, iter):
+ uri = self.model.get(iter, 1)[0].uri
+ self.filelist.remove(uri)
+ self.model.remove(iter)
+
+ def is_nonempty(self):
+ try:
+ self.model.get_iter((0,))
+ except ValueError:
+ return False
+ return True
+
+
+class GladeWindow(object):
+
+ callbacks = {}
+ builder = None
+
+ def __init__(self, builder):
+ '''
+ Init GladeWindow, stores the objects's potential callbacks for later.
+ You have to call connect_signals() when all descendants are ready.'''
+ GladeWindow.builder = builder
+ GladeWindow.callbacks.update(dict([[x, getattr(self, x)]
+ for x in dir(self) if x.startswith('on_')]))
+
+ def __getattr__(self, attribute):
+ '''Allow direct use of window widget.'''
+ widget = GladeWindow.builder.get_object(attribute)
+ if widget is None:
+ raise AttributeError('Widget \'%s\' not found' % attribute)
+ self.__dict__[attribute] = widget # cache result
+ return widget
+
+ @staticmethod
+ def connect_signals():
+ '''Connect all GladeWindow objects to theirs respective signals'''
+ GladeWindow.builder.connect_signals(GladeWindow.callbacks)
+
+
+class PreferencesDialog(GladeWindow, GConfStore):
+
+ basename_patterns = [
+ ('%(.inputname)s', _('Same as input, but replacing the suffix')),
+ ('%(.inputname)s%(.ext)s',
+ _('Same as input, but with an additional suffix')),
+ ('%(track-number)02d-%(title)s', _('Track number - title')),
+ ('%(title)s', _('Track title')),
+ ('%(artist)s-%(title)s', _('Artist - title')),
+ ('Custom', _('Custom filename pattern')),
+ ]
+
+ subfolder_patterns = [
+ ('%(artist)s/%(album)s', _('artist/album')),
+ ('%(artist)s-%(album)s', _('artist-album')),
+ ('%(artist)s - %(album)s', _('artist - album')),
+ ]
+
+ defaults = {
+ 'same-folder-as-input': 1,
+ 'selected-folder': os.path.expanduser('~'),
+ 'create-subfolders': 0,
+ 'subfolder-pattern-index': 0,
+ 'name-pattern-index': 0,
+ 'custom-filename-pattern': '{Track} - {Title}',
+ 'replace-messy-chars': 0,
+ 'output-mime-type': 'audio/x-vorbis',
+ 'output-suffix': '.ogg',
+ 'vorbis-quality': 0.6,
+ 'vorbis-oga-extension': 0,
+ 'mp3-mode': 'vbr',
+ 'mp3-cbr-quality': 192,
+ 'mp3-abr-quality': 192,
+ 'mp3-vbr-quality': 3,
+ 'aac-quality': 192,
+ 'opus-bitrate': 96,
+ 'flac-compression': 8,
+ 'wav-sample-width': 16,
+ 'delete-original': 0,
+ 'output-resample': 0,
+ 'resample-rate': 48000,
+ 'flac-speed': 0, # TODO used ?
+ 'force-mono': 0,
+ 'last-used-folder': None,
+ 'audio-profile': None,
+ 'limit-jobs': 0,
+ 'number-of-jobs': 1,
+ }
+
+ sensitive_names = ['vorbis_quality', 'choose_folder', 'create_subfolders',
+ 'subfolder_pattern', 'jobs_spinbutton', 'resample_hbox',
+ 'force_mono']
+
+ def __init__(self, builder, parent):
+ GladeWindow.__init__(self, builder)
+ GConfStore.__init__(self, '/apps/SoundConverter', self.defaults)
+
+ self.dialog = builder.get_object('prefsdialog')
+ self.dialog.set_transient_for(parent)
+ self.example = builder.get_object('example_filename')
+ self.force_mono = builder.get_object('force_mono')
+
+ self.target_bitrate = None
+ self.convert_setting_from_old_version()
+
+ self.sensitive_widgets = {}
+ for name in self.sensitive_names:
+ self.sensitive_widgets[name] = builder.get_object(name)
+ assert self.sensitive_widgets[name] is not None
+ self.set_widget_initial_values(builder)
+ self.set_sensitive()
+
+ tip = [_('Available patterns:')]
+ for k in sorted(locale_patterns_dict.values()):
+ tip.append(k)
+ self.custom_filename.set_tooltip_text('\n'.join(tip))
+
+ #self.resample_rate.connect('changed', self._on_resample_rate_changed)
+
+ def convert_setting_from_old_version(self):
+ """ try to convert previous settings"""
+
+ # vorbis quality was once stored as an int enum
+ try:
+ self.get_float('vorbis-quality')
+ except gobject.GError:
+ log('deleting old settings...')
+ [self.gconf.unset(self.path(k)) for k in self.defaults.keys()]
+
+ self.gconf.clear_cache()
+
+ def set_widget_initial_values(self, builder):
+
+ self.quality_tabs.set_show_tabs(False)
+
+ if self.get_int('same-folder-as-input'):
+ w = self.same_folder_as_input
+ else:
+ w = self.into_selected_folder
+ w.set_active(True)
+
+ uri = filename_to_uri(self.get_string('selected-folder'))
+ self.target_folder_chooser.set_uri(uri)
+ self.update_selected_folder()
+
+ w = self.create_subfolders
+ w.set_active(self.get_int('create-subfolders'))
+
+ w = self.subfolder_pattern
+ active = self.get_int('subfolder-pattern-index')
+ model = w.get_model()
+ model.clear()
+ for pattern, desc in self.subfolder_patterns:
+ i = model.append()
+ model.set(i, 0, desc)
+ w.set_active(active)
+
+ if self.get_int('replace-messy-chars'):
+ w = self.replace_messy_chars
+ w.set_active(True)
+
+ if self.get_int('delete-original'):
+ self.delete_original.set_active(True)
+
+ mime_type = self.get_string('output-mime-type')
+
+ widgets = ( ('audio/x-vorbis', 'vorbisenc'),
+ ('audio/mpeg' , 'lame'),
+ ('audio/x-flac' , 'flacenc'),
+ ('audio/x-wav' , 'wavenc'),
+ ('audio/x-m4a' , 'faac'),
+ ('audio/ogg; codecs=opus' , 'opusenc'),
+ ('gst-profile' , None),
+ ) # must be in same order in output_mime_type
+
+ # desactivate output if encoder plugin is not present
+ widget = self.output_mime_type
+ model = widget.get_model()
+ assert len(model) == len(widgets), 'model:%d widgets:%d' % (len(model),
+ len(widgets))
+
+ if not self.gstprofile.get_model().get_n_columns():
+ self.gstprofile.set_model(gtk.ListStore(str))
+ cell = gtk.CellRendererText()
+ self.gstprofile.pack_start(cell)
+ self.gstprofile.add_attribute(cell,'text',0)
+ self.gstprofile.set_active(0)
+
+ # check if we can found the stored audio profile
+ found_profile = False
+ stored_profile = self.get_string('audio-profile')
+ for i, profile in enumerate(audio_profiles_list):
+ description, extension, pipeline = profile
+ self.gstprofile.get_model().append(['%s (.%s)' % (description, extension)])
+ if description == stored_profile:
+ self.gstprofile.set_active(i)
+ found_profile = True
+ if not found_profile and stored_profile:
+ # reset default output
+ log('Cannot find audio profile "%s", resetting to default output.'
+ % stored_profile)
+ self.set_string('audio-profile', '')
+ self.gstprofile.set_active(0)
+ mime_type = self.defaults['output-mime-type']
+
+ self.present_mime_types = []
+ i = 0
+ model = self.output_mime_type.get_model()
+ for b in widgets:
+ mime, encoder_name = b
+ # valid encoder?
+ encoder_present = encoder_name and encoder_name in available_elements
+ # valid profile?
+ profile_present = mime == 'gst-profile' and audio_profiles_list
+ if encoder_present or profile_present:
+ # add to supported outputs
+ self.present_mime_types.append(mime)
+ i += 1
+ else:
+ # remove it.
+ del model[i]
+ if mime_type == mime:
+ mime_type = self.defaults['output-mime-type']
+ for i, mime in enumerate(self.present_mime_types):
+ if mime_type == mime:
+ widget.set_active(i)
+ self.change_mime_type(mime_type)
+
+ # display information about mp3 encoding
+ if 'lame' not in available_elements:
+ w = self.lame_absent
+ w.show()
+
+ w = self.vorbis_quality
+ quality = self.get_float('vorbis-quality')
+ quality_setting = {0: 0, 0.2: 1, 0.4: 2, 0.6: 3, 0.8: 4, 1.0: 5}
+ w.set_active(-1)
+ for k, v in quality_setting.iteritems():
+ if abs(quality - k) < 0.01:
+ self.vorbis_quality.set_active(v)
+ if self.get_int('vorbis-oga-extension'):
+ self.vorbis_oga_extension.set_active(True)
+
+ w = self.aac_quality
+ quality = self.get_int('aac-quality')
+ quality_setting = {64: 0, 96: 1, 128: 2, 192: 3, 256: 4, 320: 5}
+ w.set_active(quality_setting.get(quality, -1))
+
+ w = self.opus_quality
+ quality = self.get_int('opus-bitrate')
+ quality_setting = {48: 0, 64: 1, 96: 2, 128: 3, 160: 4, 192: 5}
+ w.set_active(quality_setting.get(quality, -1))
+
+ w = self.flac_compression
+ quality = self.get_int('flac-compression')
+ quality_setting = {0: 0, 5: 1, 8: 2}
+ w.set_active(quality_setting.get(quality, -1))
+
+ w = self.wav_sample_width
+ quality = self.get_int('wav-sample-width')
+ quality_setting = {8: 0, 16: 1, 32: 2}
+ w.set_active(quality_setting.get(quality, -1))
+
+ self.mp3_quality = self.mp3_quality
+ self.mp3_mode = self.mp3_mode
+
+ mode = self.get_string('mp3-mode')
+ self.change_mp3_mode(mode)
+
+ w = self.basename_pattern
+ active = self.get_int('name-pattern-index')
+ model = w.get_model()
+ model.clear()
+ for pattern, desc in self.basename_patterns:
+ iter = model.append()
+ model.set(iter, 0, desc)
+ w.set_active(active)
+
+ self.custom_filename.set_text(self.get_string(
+ 'custom-filename-pattern'))
+ if self.basename_pattern.get_active() == len(self.basename_patterns)-1:
+ self.custom_filename_box.set_sensitive(True)
+ else:
+ self.custom_filename_box.set_sensitive(False)
+
+
+ self.resample_toggle.set_active(self.get_int('output-resample'))
+
+ cell = gtk.CellRendererText()
+ self.resample_rate.pack_start(cell, True)
+ self.resample_rate.add_attribute(cell, 'text', 0)
+ rates = [8000, 11025, 22050, 44100, 48000, 96000]
+ rate = self.get_int('resample-rate')
+ try:
+ idx = rates.index(rate)
+ except ValueError:
+ idx = -1
+ self.resample_rate.set_active(idx)
+
+ self.force_mono.set_active(self.get_int('force-mono'))
+
+ self.jobs.set_active(self.get_int('limit-jobs'))
+ self.jobs_spinbutton.set_value(self.get_int('number-of-jobs'))
+
+ self.update_jobs()
+ self.update_example()
+
+ def update_selected_folder(self):
+ self.into_selected_folder.set_use_underline(False)
+ self.into_selected_folder.set_label(_('Into folder %s') %
+ beautify_uri(self.get_string('selected-folder')))
+
+ def get_bitrate_from_settings(self):
+ bitrate = 0
+ aprox = True
+ mode = self.get_string('mp3-mode')
+
+ mime_type = self.get_string('output-mime-type')
+
+ if mime_type == 'audio/x-vorbis':
+ quality = self.get_float('vorbis-quality')*10
+ quality = int(quality)
+ bitrates = (64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 500)
+ bitrate = bitrates[quality]
+
+ elif mime_type == 'audio/x-m4a':
+ bitrate = self.get_int('aac-quality')
+
+ elif mime_type == 'audio/ogg; codecs=opus':
+ bitrate = self.get_int('opus-bitrate')
+
+ elif mime_type == 'audio/mpeg':
+ quality = {
+ 'cbr': 'mp3-cbr-quality',
+ 'abr': 'mp3-abr-quality',
+ 'vbr': 'mp3-vbr-quality'
+ }
+ bitrate = self.get_int(quality[mode])
+ if mode == 'vbr':
+ # hum, not really, but who cares? :)
+ bitrates = (320, 256, 224, 192, 160, 128, 112, 96, 80, 64)
+ bitrate = bitrates[bitrate]
+ if mode == 'cbr':
+ aprox = False
+
+ if bitrate:
+ if aprox:
+ return '~%d kbps' % bitrate
+ else:
+ return '%d kbps' % bitrate
+ else:
+ return 'N/A'
+
+ def update_example(self):
+ sound_file = SoundFile('foo/bar.flac')
+ sound_file.tags.update({'track-number': 1, 'track-count': 99})
+ sound_file.tags.update({'disc-number': 2, 'disc-count': 9})
+ sound_file.tags.update(locale_patterns_dict)
+
+ s = gobject.markup_escape_text(beautify_uri(
+ self.generate_filename(sound_file, for_display=True)))
+ p = 0
+ replaces = []
+
+ while 1:
+ b = s.find('{', p)
+ if b == -1:
+ break
+ e = s.find('}', b)
+
+ tag = s[b:e+1]
+ if tag.lower() in [
+ v.lower() for v in locale_patterns_dict.values()]:
+ k = tag
+ l = k.replace('{', '<b>{')
+ l = l.replace('}', '}</b>')
+ replaces.append([k, l])
+ else:
+ k = tag
+ l = k.replace('{', '<span foreground=\'red\'><i>{')
+ l = l.replace('}', '}</i></span>')
+ replaces.append([k, l])
+ p = b+1
+
+ for k, l in replaces:
+ s = s.replace(k, l)
+
+ self.example.set_markup(s)
+
+ markup = '<small>%s</small>' % (_('Target bitrate: %s') %
+ self.get_bitrate_from_settings())
+ self.aprox_bitrate.set_markup(markup)
+
+ def get_output_suffix(self):
+ self.gconf.clear_cache()
+ output_type = self.get_string('output-mime-type')
+ profile = self.get_string('audio-profile')
+ profile_ext = audio_profiles_dict[profile][1] if profile else ''
+ output_suffix = {
+ 'audio/x-vorbis': '.ogg',
+ 'audio/x-flac': '.flac',
+ 'audio/x-wav': '.wav',
+ 'audio/mpeg': '.mp3',
+ 'audio/x-m4a': '.m4a',
+ 'audio/ogg; codecs=opus': '.opus',
+ 'gst-profile': '.' + profile_ext,
+ }.get(output_type, '.?')
+ if output_suffix == '.ogg' and self.get_int('vorbis-oga-extension'):
+ output_suffix = '.oga'
+ return output_suffix
+
+ def generate_filename(self, sound_file, for_display=False):
+ generator = TargetNameGenerator()
+ generator.suffix = self.get_output_suffix()
+
+ if not self.get_int('same-folder-as-input'):
+ folder = self.get_string('selected-folder')
+ folder = filename_to_uri(folder)
+ generator.folder = folder
+
+ if self.get_int('create-subfolders'):
+ generator.subfolders = self.get_subfolder_pattern()
+
+ generator.basename = self.get_basename_pattern()
+
+ if for_display:
+ generator.replace_messy_chars = False
+ return unquote_filename(generator.get_target_name(sound_file))
+ else:
+ generator.replace_messy_chars = self.get_int('replace-messy-chars')
+ return generator.get_target_name(sound_file)
+
+ def generate_temp_filename(self, soundfile):
+ folder = dirname(soundfile.uri)
+ if not self.get_int('same-folder-as-input'):
+ folder = self.get_string('selected-folder')
+ folder = filename_to_uri(folder)
+ return folder + '/' + basename(soundfile.filename)
+
+ def process_custom_pattern(self, pattern):
+ for k in custom_patterns:
+ pattern = pattern.replace(k, custom_patterns[k])
+ return pattern
+
+ def set_sensitive(self):
+ for widget in self.sensitive_widgets.values():
+ widget.set_sensitive(False)
+
+ x = self.get_int('same-folder-as-input')
+ for name in ['choose_folder', 'create_subfolders',
+ 'subfolder_pattern']:
+ self.sensitive_widgets[name].set_sensitive(not x)
+
+ self.sensitive_widgets['vorbis_quality'].set_sensitive(
+ self.get_string('output-mime-type') == 'audio/x-vorbis')
+
+ self.sensitive_widgets['jobs_spinbutton'].set_sensitive(
+ self.get_int('limit-jobs'))
+
+ if self.get_string('output-mime-type') == 'gst-profile':
+ self.sensitive_widgets['resample_hbox'].set_sensitive(False)
+ self.sensitive_widgets['force_mono'].set_sensitive(False)
+ else:
+ self.sensitive_widgets['resample_hbox'].set_sensitive(True)
+ self.sensitive_widgets['force_mono'].set_sensitive(True)
+
+
+ def run(self):
+ self.dialog.run()
+ self.dialog.hide()
+
+ def on_delete_original_toggled(self, button):
+ if button.get_active():
+ self.set_int('delete-original', 1)
+ else:
+ self.set_int('delete-original', 0)
+
+ def on_same_folder_as_input_toggled(self, button):
+ if button.get_active():
+ self.set_int('same-folder-as-input', 1)
+ self.set_sensitive()
+ self.update_example()
+
+ def on_into_selected_folder_toggled(self, button):
+ if button.get_active():
+ self.set_int('same-folder-as-input', 0)
+ self.set_sensitive()
+ self.update_example()
+
+ def on_choose_folder_clicked(self, button):
+ ret = self.target_folder_chooser.run()
+ folder = self.target_folder_chooser.get_uri()
+ self.target_folder_chooser.hide()
+ if ret == gtk.RESPONSE_OK:
+ if folder:
+ self.set_string('selected-folder', urllib.unquote(folder))
+ self.update_selected_folder()
+ self.update_example()
+
+ def on_create_subfolders_toggled(self, button):
+ if button.get_active():
+ self.set_int('create-subfolders', 1)
+ else:
+ self.set_int('create-subfolders', 0)
+ self.update_example()
+
+ def on_subfolder_pattern_changed(self, combobox):
+ self.set_int('subfolder-pattern-index', combobox.get_active())
+ self.update_example()
+
+ def get_subfolder_pattern(self):
+ index = self.get_int('subfolder-pattern-index')
+ if index < 0 or index >= len(self.subfolder_patterns):
+ index = 0
+ return self.subfolder_patterns[index][0]
+
+ def on_basename_pattern_changed(self, combobox):
+ self.set_int('name-pattern-index', combobox.get_active())
+ if combobox.get_active() == len(self.basename_patterns)-1:
+ self.custom_filename_box.set_sensitive(True)
+ else:
+ self.custom_filename_box.set_sensitive(False)
+ self.update_example()
+
+ def get_basename_pattern(self):
+ index = self.get_int('name-pattern-index')
+ if index < 0 or index >= len(self.basename_patterns):
+ index = 0
+ if self.basename_pattern.get_active() == len(self.basename_patterns)-1:
+ return self.process_custom_pattern(self.custom_filename.get_text())
+ else:
+ return self.basename_patterns[index][0]
+
+ def on_custom_filename_changed(self, entry):
+ self.set_string('custom-filename-pattern', entry.get_text())
+ self.update_example()
+
+ def on_replace_messy_chars_toggled(self, button):
+ if button.get_active():
+ self.set_int('replace-messy-chars', 1)
+ else:
+ self.set_int('replace-messy-chars', 0)
+ self.update_example()
+
+ def change_mime_type(self, mime_type):
+ self.set_string('output-mime-type', mime_type)
+ self.set_sensitive()
+ self.update_example()
+ tabs = {
+ 'audio/x-vorbis': 0,
+ 'audio/mpeg': 1,
+ 'audio/x-flac': 2,
+ 'audio/x-wav': 3,
+ 'audio/x-m4a': 4,
+ 'audio/ogg; codecs=opus': 5,
+ 'gst-profile': 6,
+ }
+ self.quality_tabs.set_current_page(tabs[mime_type])
+
+ def on_output_mime_type_changed(self, combo):
+ self.change_mime_type(
+ self.present_mime_types[combo.get_active()]
+ )
+
+ def on_output_mime_type_ogg_vorbis_toggled(self, button):
+ if button.get_active():
+ self.change_mime_type('audio/x-vorbis')
+
+ def on_output_mime_type_flac_toggled(self, button):
+ if button.get_active():
+ self.change_mime_type('audio/x-flac')
+
+ def on_output_mime_type_wav_toggled(self, button):
+ if button.get_active():
+ self.change_mime_type('audio/x-wav')
+
+ def on_output_mime_type_mp3_toggled(self, button):
+ if button.get_active():
+ self.change_mime_type('audio/mpeg')
+
+ def on_output_mime_type_aac_toggled(self, button):
+ if button.get_active():
+ self.change_mime_type('audio/x-m4a')
+
+ def on_output_mime_type_opus_toggled(self, button):
+ if button.get_active():
+ self.change_mime_type('audio/ogg; codecs=opus')
+
+ def on_vorbis_quality_changed(self, combobox):
+ if combobox.get_active() == -1:
+ return # just de-selectionning
+ quality = (0, 0.2, 0.4, 0.6, 0.8, 1.0)
+ fquality = quality[combobox.get_active()]
+ self.set_float('vorbis-quality', fquality)
+ self.hscale_vorbis_quality.set_value(fquality*10)
+ self.update_example()
+
+ def on_hscale_vorbis_quality_value_changed(self, hscale):
+ fquality = hscale.get_value()
+ if abs(self.get_float('vorbis-quality') - fquality/10.0) < 0.001:
+ return # already at right value
+ self.set_float('vorbis-quality', fquality/10.0)
+ self.vorbis_quality.set_active(-1)
+ self.update_example()
+
+ def on_vorbis_oga_extension_toggled(self, toggle):
+ self.set_int('vorbis-oga-extension', toggle.get_active())
+ self.update_example()
+
+ def on_aac_quality_changed(self, combobox):
+ quality = (64, 96, 128, 192, 256, 320)
+ self.set_int('aac-quality', quality[combobox.get_active()])
+ self.update_example()
+
+ def on_opus_quality_changed(self, combobox):
+ quality = (48, 64, 96, 128, 160, 192)
+ self.set_int('opus-bitrate', quality[combobox.get_active()])
+ self.update_example()
+
+ def on_wav_sample_width_changed(self, combobox):
+ quality = (8, 16, 32)
+ self.set_int('wav-sample-width', quality[combobox.get_active()])
+ self.update_example()
+
+ def on_flac_compression_changed(self, combobox):
+ quality = (0, 5, 8)
+ self.set_int('flac-compression', quality[combobox.get_active()])
+ self.update_example()
+
+ def on_gstprofile_changed(self, combobox):
+ profile = audio_profiles_list[combobox.get_active()]
+ description, extension, pipeline = profile
+ self.set_string('audio-profile', description)
+ self.update_example()
+
+ def on_force_mono_toggle(self, button):
+ if button.get_active():
+ self.set_int('force-mono', 1)
+ else:
+ self.set_int('force-mono', 0)
+ self.update_example()
+
+ def change_mp3_mode(self, mode):
+
+ keys = {'cbr': 0, 'abr': 1, 'vbr': 2}
+ self.mp3_mode.set_active(keys[mode])
+
+ keys = {
+ 'cbr': 'mp3-cbr-quality',
+ 'abr': 'mp3-abr-quality',
+ 'vbr': 'mp3-vbr-quality',
+ }
+ quality = self.get_int(keys[mode])
+
+ quality_to_preset = {
+ 'cbr': {64: 0, 96: 1, 128: 2, 192: 3, 256: 4, 320: 5},
+ 'abr': {64: 0, 96: 1, 128: 2, 192: 3, 256: 4, 320: 5},
+ 'vbr': {9: 0, 7: 1, 5: 2, 3: 3, 1: 4, 0: 5}, # inverted !
+ }
+
+ range_ = {
+ 'cbr': 14,
+ 'abr': 14,
+ 'vbr': 10,
+ }
+ self.hscale_mp3.set_range(0, range_[mode])
+
+ if quality in quality_to_preset[mode]:
+ self.mp3_quality.set_active(quality_to_preset[mode][quality])
+ self.update_example()
+
+ def on_mp3_mode_changed(self, combobox):
+ mode = ('cbr', 'abr', 'vbr')[combobox.get_active()]
+ self.set_string('mp3-mode', mode)
+ self.change_mp3_mode(mode)
+
+ def on_mp3_quality_changed(self, combobox):
+ keys = {
+ 'cbr': 'mp3-cbr-quality',
+ 'abr': 'mp3-abr-quality',
+ 'vbr': 'mp3-vbr-quality'
+ }
+ quality = {
+ 'cbr': (64, 96, 128, 192, 256, 320),
+ 'abr': (64, 96, 128, 192, 256, 320),
+ 'vbr': (9, 7, 5, 3, 1, 0),
+ }
+ mode = self.get_string('mp3-mode')
+ self.set_int(keys[mode], quality[mode][combobox.get_active()])
+ self.update_example()
+
+ def on_hscale_mp3_value_changed(self, widget):
+ mode = self.get_string('mp3-mode')
+ keys = {
+ 'cbr': 'mp3-cbr-quality',
+ 'abr': 'mp3-abr-quality',
+ 'vbr': 'mp3-vbr-quality'
+ }
+ quality = {
+ 'cbr': (32, 40, 48, 56, 64, 80, 96, 112,
+ 128, 160, 192, 224, 256, 320),
+ 'abr': (32, 40, 48, 56, 64, 80, 96, 112,
+ 128, 160, 192, 224, 256, 320),
+ 'vbr': (9, 8, 7, 6, 5, 4, 3, 2, 1, 0),
+ }
+ self.set_int(keys[mode], quality[mode][int(widget.get_value())])
+ self.mp3_quality.set_active(-1)
+ self.update_example()
+
+ def on_resample_rate_changed(self, combobox):
+ model = combobox.get_model()
+ iter = combobox.get_active_iter()
+ changeto = model.get_value(iter, 0)
+ self.set_int('resample-rate', int(changeto))
+
+ def on_resample_toggle(self, rstoggle):
+ self.set_int('output-resample', rstoggle.get_active())
+ self.resample_rate.set_sensitive(rstoggle.get_active())
+
+ def on_jobs_toggled(self, jtoggle):
+ self.set_int('limit-jobs', jtoggle.get_active())
+ self.jobs_spinbutton.set_sensitive(jtoggle.get_active())
+ self.update_jobs()
+
+ def on_jobs_spinbutton_value_changed(self, jspinbutton):
+ self.set_int('number-of-jobs', int(jspinbutton.get_value()))
+ self.update_jobs()
+
+ def update_jobs(self):
+ if self.get_int('limit-jobs'):
+ settings['jobs'] = self.get_int('number-of-jobs')
+ else:
+ settings['jobs'] = settings['max-jobs']
+ self.set_sensitive()
+
+
+class CustomFileChooser:
+ """
+ Custom file chooser.\n
+ """
+
+ def __init__(self, builder, parent):
+ """
+ Constructor
+ Load glade object, create a combobox
+ """
+ self.dlg = builder.get_object('custom_file_chooser')
+ self.dlg.set_title(_('Open a file'))
+ self.dlg.set_transient_for(parent)
+
+ # setup
+ self.fcw = builder.get_object('filechooserwidget')
+ self.fcw.set_local_only(not use_gnomevfs)
+ self.fcw.set_select_multiple(True)
+
+ self.pattern = []
+
+ # Create combobox model
+ self.combo = builder.get_object('filtercombo')
+ self.combo.connect('changed', self.on_combo_changed)
+ self.store = gtk.ListStore(str)
+ self.combo.set_model(self.store)
+ combo_rend = gtk.CellRendererText()
+ self.combo.pack_start(combo_rend, True)
+ self.combo.add_attribute(combo_rend, 'text', 0)
+
+ # TODO: get all (gstreamer) knew files
+ for name, pattern in filepattern:
+ self.add_pattern(name, pattern)
+ self.combo.set_active(0)
+
+ def add_pattern(self, name, pat):
+ """
+ Add a new pattern to the combobox.
+ @param name: The pattern name.
+ @type name: string
+ @param pat: the pattern
+ @type pat: string
+ """
+ self.pattern.append(pat)
+ self.store.append(['%s (%s)' % (name, pat)])
+
+ def filter_cb(self, info, pattern):
+ filename = info[2]
+ return filename.lower().endswith(pattern[1:])
+
+ def on_combo_changed(self, w):
+ """
+ Callback for combobox 'changed' signal\n
+ Set a new filter for the filechooserwidget
+ """
+ filter = gtk.FileFilter()
+ active = self.combo.get_active()
+ if active:
+ filter.add_custom(gtk.FILE_FILTER_DISPLAY_NAME, self.filter_cb,
+ self.pattern[self.combo.get_active()])
+ else:
+ filter.add_pattern('*.*')
+ self.fcw.set_filter(filter)
+
+ def __getattr__(self, attr):
+ """
+ Redirect all missing attributes/methods
+ to dialog.
+ """
+ try:
+ # defaut to dialog attributes
+ return getattr(self.dlg, attr)
+ except AttributeError:
+ # fail back to inner file chooser widget
+ return getattr(self.fcw, attr)
+
+_old_progress = 0
+_old_total = 0
+
+class SoundConverterWindow(GladeWindow):
+
+ """Main application class."""
+
+ sensitive_names = ['remove', 'clearlist',
+ 'toolbutton_clearlist', 'convert_button']
+ unsensitive_when_converting = ['remove', 'clearlist', 'prefs_button',
+ 'toolbutton_addfile', 'toolbutton_addfolder', 'convert_button',
+ 'toolbutton_clearlist', 'filelist', 'menubar']
+
+ def __init__(self, builder):
+ self.paused_time = 0
+ GladeWindow.__init__(self, builder)
+
+ self.widget = builder.get_object('window')
+ self.prefs = PreferencesDialog(builder, self.widget)
+ self.addchooser = CustomFileChooser(builder, self.widget)
+ GladeWindow.connect_signals()
+
+ self.filelist = FileList(self, builder)
+ self.filelist_selection = self.filelist.widget.get_selection()
+ self.filelist_selection.connect('changed', self.selection_changed)
+ self.existsdialog = builder.get_object('existsdialog')
+ self.existsdialog.message = builder.get_object('exists_message')
+ self.existsdialog.apply_to_all = builder.get_object('apply_to_all')
+
+ self.addfolderchooser = gtk.FileChooserDialog(_('Add Folder...'),
+ self.widget, gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER,
+ (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN,
+ gtk.RESPONSE_OK))
+ self.addfolderchooser.set_select_multiple(True)
+ self.addfolderchooser.set_local_only(not use_gnomevfs)
+
+ self.combo = gtk.ComboBox()
+ self.store = gtk.ListStore(str)
+ self.combo.set_model(self.store)
+ combo_rend = gtk.CellRendererText()
+ self.combo.pack_start(combo_rend, True)
+ self.combo.add_attribute(combo_rend, 'text', 0)
+
+ # TODO: get all (gstreamer) knew files
+ for files in filepattern:
+ self.store.append(['%s (%s)' % (files[0], files[1])])
+
+ self.combo.set_active(0)
+ self.addfolderchooser.set_extra_widget(self.combo)
+
+ self.aboutdialog.set_property('name', NAME)
+ self.aboutdialog.set_property('version', VERSION)
+ self.aboutdialog.set_transient_for(self.widget)
+
+ self.converter = ConverterQueue(self)
+
+ self.sensitive_widgets = {}
+ for name in self.sensitive_names:
+ self.sensitive_widgets[name] = builder.get_object(name)
+ for name in self.unsensitive_when_converting:
+ self.sensitive_widgets[name] = builder.get_object(name)
+
+ self.set_sensitive()
+ self.set_status()
+
+
+ #msg = _('The output file <i>%s</i>\n exists already.\n '\
+ # 'Do you want to skip the file, overwrite it or'\
+ # ' cancel the conversion?\n') % '/foo/bar/baz'
+ vbox = self.vbox_status
+ self.msg_area = msg_area = MessageArea()
+ #msg_area.add_button('_Overwrite', 1)
+ #msg_area.add_button('_Skip', 2)
+ msg_area.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CLOSE)
+ #checkbox = gtk.CheckButton('Apply to _all queue')
+ #checkbox.show()
+ #msg_area.set_text_and_icon(gtk.STOCK_DIALOG_ERROR, 'Access Denied', msg, checkbox)
+
+ #msg_area.connect("response", self.OnMessageAreaReponse, msg_area)
+ #msg_area.connect("close", self.OnMessageAreaClose, msg_area)
+ vbox.pack_start(msg_area, False, False)
+ #msg_area.show()
+
+
+
+ # This bit of code constructs a list of methods for binding to Gtk+
+ # signals. This way, we don't have to maintain a list manually,
+ # saving editing effort. It's enough to add a method to the suitable
+ # class and give the same name in the .glade file.
+
+ def __getattr__(self, attribute):
+ """Allow direct use of window widget."""
+ widget = self.builder.get_object(attribute)
+ if widget is None:
+ raise AttributeError('Widget \'%s\' not found' % attribute)
+ self.__dict__[attribute] = widget # cache result
+ return widget
+
+ def close(self, *args):
+ debug('closing...')
+ self.filelist.abort()
+ self.converter.abort()
+ self.widget.hide_all()
+ self.widget.destroy()
+ # wait one second...
+ # yes, this sucks badly, but signals can still be called by gstreamer
+ # so wait a bit for things to calm down, and quit.
+ gtk_sleep(1)
+ gtk.main_quit()
+ return True
+
+ on_window_delete_event = close
+ on_quit_activate = close
+ on_quit_button_clicked = close
+
+ def on_add_activate(self, *args):
+ last_folder = self.prefs.get_string('last-used-folder')
+ if last_folder:
+ self.addchooser.set_current_folder_uri(last_folder)
+
+ ret = self.addchooser.run()
+ folder = self.addchooser.get_current_folder_uri()
+ self.addchooser.hide()
+ if ret == gtk.RESPONSE_OK and folder:
+ self.filelist.add_uris(self.addchooser.get_uris())
+ self.prefs.set_string('last-used-folder', folder)
+ self.set_sensitive()
+
+ def on_addfolder_activate(self, *args):
+ last_folder = self.prefs.get_string('last-used-folder')
+ if last_folder:
+ self.addfolderchooser.set_current_folder_uri(last_folder)
+
+ ret = self.addfolderchooser.run()
+ folders = self.addfolderchooser.get_uris()
+ folder = self.addfolderchooser.get_current_folder_uri()
+ self.addfolderchooser.hide()
+ if ret == gtk.RESPONSE_OK:
+ extensions = None
+ if self.combo.get_active():
+ patterns = filepattern[self.combo.get_active()][1].split(';')
+ extensions = [os.path.splitext(p)[1] for p in patterns]
+ self.filelist.add_uris(folders, extensions=extensions)
+ if folder:
+ self.prefs.set_string('last-used-folder', folder)
+
+ self.set_sensitive()
+
+ def on_remove_activate(self, *args):
+ model, paths = self.filelist_selection.get_selected_rows()
+ while paths:
+ # Remove files
+ childpath = model.convert_path_to_child_path(paths[0])
+ i = self.filelist.model.get_iter(childpath)
+ self.filelist.remove(i)
+ model, paths = self.filelist_selection.get_selected_rows()
+ # re-assign row numbers
+ files = self.filelist.get_files()
+ for i, sound_file in enumerate(files):
+ sound_file.filelist_row = i
+ self.set_sensitive()
+
+ def on_clearlist_activate(self, *args):
+ self.filelist.model.clear()
+ self.filelist.filelist.clear()
+ self.set_sensitive()
+ self.set_status()
+
+ def on_progress(self):
+ if self.pulse_progress > 0: # still waiting for tags
+ self.set_progress(self.pulse_progress, display_time=False)
+ return True
+ if self.pulse_progress == -1: # still waiting for add
+ self.set_progress()
+ return True
+ if self.pulse_progress == False: # conversion ended
+ return False
+
+ perfile = {}
+ for s in self.filelist.get_files():
+ perfile[s] = None
+ running, progress = self.converter.get_progress(perfile)
+
+ if running:
+ self.set_progress(progress)
+ for sound_file, taskprogress in perfile.iteritems():
+ if taskprogress > 0.0:
+ sound_file.progress = taskprogress
+ self.set_file_progress(sound_file, taskprogress)
+ if taskprogress is None and sound_file.progress:
+ self.set_file_progress(sound_file, 1.0)
+ sound_file.progress = None
+ return running
+
+ def do_convert(self):
+ self.pulse_progress = -1
+ gobject.timeout_add(100, self.on_progress)
+ self.progressbar.set_text(_('Preparing conversion...'))
+ files = self.filelist.get_files()
+ total = len(files)
+ for i, sound_file in enumerate(files):
+ gtk_iteration()
+ self.pulse_progress = float(i)/total # TODO: still needed?
+ sound_file.progress = None
+ self.converter.add(sound_file)
+ # all was OK
+ self.set_status('')
+ self.pulse_progress = None
+ self.converter.start()
+ self.set_sensitive()
+
+ def on_convert_button_clicked(self, *args):
+ # reset and show progress bar
+ self.set_progress(0)
+ self.progress_frame.show()
+ self.status_frame.hide()
+ self.progress_time = time.time()
+ self.set_progress()
+ self.set_status(_('Converting'))
+ for soundfile in self.filelist.get_files():
+ self.set_file_progress(soundfile, 0.0)
+ # start conversion
+ self.do_convert()
+ # update ui
+ self.set_sensitive()
+
+ def on_button_pause_clicked(self, *args):
+ self.converter.toggle_pause(not self.converter.paused)
+
+ if self.converter.paused:
+ self.current_pause_start = time.time()
+ else:
+ self.paused_time += time.time() - self.current_pause_start
+
+ def on_button_cancel_clicked(self, *args):
+ self.converter.abort()
+ self.set_status(_('Canceled'))
+ self.set_sensitive()
+ self.conversion_ended()
+
+ def on_select_all_activate(self, *args):
+ self.filelist.widget.get_selection().select_all()
+
+ def on_clear_activate(self, *args):
+ self.filelist.widget.get_selection().unselect_all()
+
+ def on_preferences_activate(self, *args):
+ self.prefs.run()
+
+ on_prefs_button_clicked = on_preferences_activate
+
+ def on_about_activate(self, *args):
+ about = self.aboutdialog
+ about.set_property('name', NAME)
+ about.set_property('version', VERSION)
+ about.set_transient_for(self.widget)
+ #TODO: about.set_property('translator_credits', TRANSLATORS)
+ about.show()
+
+ def on_aboutdialog_response(self, *args):
+ self.aboutdialog.hide()
+
+ def selection_changed(self, *args):
+ self.set_sensitive()
+
+ def conversion_ended(self):
+ self.pulse_progress = False
+ self.progress_frame.hide()
+ self.filelist.hide_row_progress()
+ self.status_frame.show()
+ self.widget.set_sensitive(True)
+ try:
+ from gi.repository import Unity
+ launcher = Unity.LauncherEntry.get_for_desktop_id ("soundconverter.desktop")
+ launcher.set_property("progress_visible", False)
+ except ImportError:
+ pass
+
+
+ def set_widget_sensitive(self, name, sensitivity):
+ self.sensitive_widgets[name].set_sensitive(sensitivity)
+
+ def set_sensitive(self):
+ """update the sensitive state of UI for the current state"""
+ for w in self.unsensitive_when_converting:
+ self.set_widget_sensitive(w, not self.converter.running)
+
+ if not self.converter.running:
+ self.set_widget_sensitive('remove',
+ self.filelist_selection.count_selected_rows() > 0)
+ self.set_widget_sensitive('convert_button',
+ self.filelist.is_nonempty())
+
+ def set_file_progress(self, sound_file, progress):
+ row = sound_file.filelist_row
+ self.filelist.set_row_progress(row, progress)
+
+ def set_progress(self, fraction=None, display_time=True):
+ if not fraction:
+ if fraction is None:
+ self.progressbar.pulse()
+ else:
+ self.progressbar.set_fraction(0)
+ self.progressbar.set_text('')
+ self.progressfile.set_markup('')
+ self.filelist.hide_row_progress()
+ return
+
+ if self.converter.paused:
+ self.progressbar.set_text(_('Paused'))
+ return
+
+ fraction = min(max(fraction, 0.0), 1.0)
+ self.progressbar.set_fraction(fraction)
+
+ if display_time:
+ t = time.time() - self.converter.run_start_time - \
+ self.paused_time
+ if (t < 1):
+ # wait a bit not to display crap
+ self.progressbar.pulse()
+ return
+
+ r = (t / fraction - t)
+ s = max(r % 60, 1)
+ m = r / 60
+
+ remaining = _('%d:%02d left') % (m, s)
+ self.progressbar.set_text(remaining)
+ self.progress_time = time.time()
+
+ def set_status(self, text=None):
+ if not text:
+ text = _('Ready')
+ self.statustext.set_markup(text)
+ gtk_iteration()
+
+ def is_active(self):
+ return self.widget.is_active()
+
+
+NAME = VERSION = None
+win = None
+
+def gui_main(name, version, gladefile, input_files):
+ global NAME, VERSION
+ NAME, VERSION = name, version
+ gnome.init(name, version)
+ builder = gtk.Builder()
+ builder.set_translation_domain(name.lower())
+ builder.add_from_file(gladefile)
+
+ global win
+ win = SoundConverterWindow(builder)
+ import error
+ error.set_error_handler(ErrorDialog(builder))
+
+ #error_dialog = MsgAreaErrorDialog(builder)
+ #error_dialog.msg_area = win.msg_area
+ #error.set_error_handler(error_dialog)
+
+ gobject.idle_add(win.filelist.add_uris, input_files)
+ win.set_sensitive()
+ gtk.main()
diff --git a/soundconverter/utils.py b/soundconverter/utils.py
new file mode 100644
index 0000000..473e6f4
--- /dev/null
+++ b/soundconverter/utils.py
@@ -0,0 +1,42 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# SoundConverter - GNOME application for converting between audio formats.
+# Copyright 2004 Lars Wirzenius
+# Copyright 2005-2012 Gautier Portet
+#
+# This program 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; version 3 of the License.
+#
+# This program 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 this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+
+# logging & debugging
+
+from settings import settings
+
+
+def log(*args):
+ """
+ Display a message.
+ Can be disabled with 'quiet' option
+ """
+ if not settings['quiet']:
+ print( ' '.join([str(msg) for msg in args]) )
+
+
+def debug(*args):
+ """
+ Display a debug message.
+ Only when activated by 'debug' option
+ """
+ if settings['debug']:
+ print( ' '.join([str(msg) for msg in args]) )