From 315db640f463613cae4de4c02cc52d2be6d5684a Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Tue, 3 Jun 2014 13:09:32 +0000 Subject: Initial import for soundconverter 2.1.3 --- soundconverter/Makefile.am | 27 + soundconverter/Makefile.in | 527 +++++++++++++ soundconverter/__init__.py | 23 + soundconverter/batch.py | 130 ++++ soundconverter/error.py | 44 ++ soundconverter/fileoperations.py | 154 ++++ soundconverter/gconfstore.py | 59 ++ soundconverter/gstreamer.py | 823 +++++++++++++++++++++ soundconverter/messagearea.py | 219 ++++++ soundconverter/namegenerator.py | 110 +++ soundconverter/notify.py | 44 ++ soundconverter/queue.py | 133 ++++ soundconverter/settings.py | 131 ++++ soundconverter/soundfile.py | 62 ++ soundconverter/task.py | 93 +++ soundconverter/ui.py | 1509 ++++++++++++++++++++++++++++++++++++++ soundconverter/utils.py | 42 ++ 17 files changed, 4130 insertions(+) create mode 100644 soundconverter/Makefile.am create mode 100644 soundconverter/Makefile.in create mode 100644 soundconverter/__init__.py create mode 100644 soundconverter/batch.py create mode 100644 soundconverter/error.py create mode 100644 soundconverter/fileoperations.py create mode 100644 soundconverter/gconfstore.py create mode 100644 soundconverter/gstreamer.py create mode 100644 soundconverter/messagearea.py create mode 100644 soundconverter/namegenerator.py create mode 100644 soundconverter/notify.py create mode 100644 soundconverter/queue.py create mode 100644 soundconverter/settings.py create mode 100644 soundconverter/soundfile.py create mode 100644 soundconverter/task.py create mode 100644 soundconverter/ui.py create mode 100644 soundconverter/utils.py (limited to 'soundconverter') 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('%s' % _('GStreamer Error:'), '%s\n(%s)' % (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 = "%s" % 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 = "%s" % 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('%s' % 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('{', '{') + l = l.replace('}', '}') + replaces.append([k, l]) + else: + k = tag + l = k.replace('{', '{') + l = l.replace('}', '}') + replaces.append([k, l]) + p = b+1 + + for k, l in replaces: + s = s.replace(k, l) + + self.example.set_markup(s) + + markup = '%s' % (_('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 %s\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]) ) -- cgit v1.2.1