From 429242f1c8767e41ea2da18266ed8840589d4a14 Mon Sep 17 00:00:00 2001 From: Laurent Monin Date: Sun, 20 Jan 2013 23:26:19 +0100 Subject: [PATCH 1/8] Modify and move setup_gettext() in its own file. Unit tests will need to use gettext, so i modified a bit setup_gettext() and it can now be easily imported. I add to change the code a bit to make it work, i also fixed an issue with locale under linux that was occuring when one imported this module. --- picard/i18n.py | 65 ++++++++++++++++++++++++++++++++++++++++++++++++ picard/tagger.py | 50 ++----------------------------------- 2 files changed, 67 insertions(+), 48 deletions(-) create mode 100644 picard/i18n.py diff --git a/picard/i18n.py b/picard/i18n.py new file mode 100644 index 000000000..453e99497 --- /dev/null +++ b/picard/i18n.py @@ -0,0 +1,65 @@ +import gettext +import locale +import os.path +import sys +import __builtin__ + +__builtin__.__dict__['N_'] = lambda a: a + + +def setup_gettext(localedir, ui_language=None, logdebug=None): + """Setup locales, load translations, install gettext functions.""" + current_locale = '' + if ui_language: + os.environ['LANGUAGE'] = '' + os.environ['LANG'] = ui_language + try: + current_locale = locale.normalize(ui_language) + locale.setlocale(locale.LC_ALL, current_locale) + except: + pass + if sys.platform == "win32": + try: + locale.setlocale(locale.LC_ALL, os.environ["LANG"]) + except KeyError: + os.environ["LANG"] = locale.getdefaultlocale()[0] + try: + current_locale = locale.setlocale(locale.LC_ALL, "") + except: + pass + except: + pass + elif not ui_language: + if sys.platform == "darwin": + try: + import Foundation + defaults = Foundation.NSUserDefaults.standardUserDefaults() + os.environ["LANG"] = \ + defaults.objectForKey_("AppleLanguages")[0] + except: + pass + try: + current_locale = locale.setlocale(locale.LC_ALL, "") + except: + pass + if logdebug: + logdebug("Using locale %r", current_locale) + try: + if logdebug: + logdebug("Loading gettext translation, localedir=%r", localedir) + trans = gettext.translation("picard", localedir) + trans.install(True) + _ungettext = trans.ungettext + except IOError: + __builtin__.__dict__['_'] = lambda a: a + + def _ungettext(a, b, c): + if c == 1: + return a + else: + return b + __builtin__.__dict__['ungettext'] = _ungettext + if logdebug: + logdebug("_ = %r", _) + logdebug("N_ = %r", N_) + logdebug("ungettext = %r", ungettext) diff --git a/picard/tagger.py b/picard/tagger.py index 1826c4578..779c3f632 100644 --- a/picard/tagger.py +++ b/picard/tagger.py @@ -20,8 +20,6 @@ from PyQt4 import QtGui, QtCore -import gettext -import locale import getopt import os.path import shutil @@ -30,10 +28,6 @@ import sys from collections import deque from itertools import chain -# Install gettext "noop" function. -import __builtin__ -__builtin__.__dict__['N_'] = lambda a: a - # Py2exe 0.6.6 has broken fake_getline which doesn't work with Python 2.5 if hasattr(sys, "frozen"): import linecache @@ -51,6 +45,7 @@ shutil.copystat = _patched_shutil_copystat import picard.resources import picard.plugins +from picard.i18n import setup_gettext from picard import version_string, log, acoustid, config from picard.album import Album, NatAlbum @@ -140,7 +135,7 @@ class Tagger(QtGui.QApplication): check_io_encoding() - self.setup_gettext(localedir) + setup_gettext(localedir, config.setting["ui_language"], log.debug) self.xmlws = XmlWebService() @@ -198,47 +193,6 @@ class Tagger(QtGui.QApplication): # default format, disabled remove_va_file_naming_format(merge=False) - def setup_gettext(self, localedir): - """Setup locales, load translations, install gettext functions.""" - ui_language = config.setting["ui_language"] - if ui_language: - os.environ['LANGUAGE'] = '' - os.environ['LANG'] = ui_language - if sys.platform == "win32": - try: - locale.setlocale(locale.LC_ALL, os.environ["LANG"]) - except KeyError: - os.environ["LANG"] = locale.getdefaultlocale()[0] - try: - locale.setlocale(locale.LC_ALL, "") - except: - pass - except: - pass - else: - if sys.platform == "darwin" and not ui_language: - try: - import Foundation - defaults = Foundation.NSUserDefaults.standardUserDefaults() - os.environ["LANG"] = defaults.objectForKey_("AppleLanguages")[0] - except: - pass - try: - locale.setlocale(locale.LC_ALL, "") - except: - pass - try: - log.debug("Loading gettext translation, localedir=%r", localedir) - self.translation = gettext.translation("picard", localedir) - self.translation.install(True) - ungettext = self.translation.ungettext - except IOError: - __builtin__.__dict__['_'] = lambda a: a - def ungettext(a, b, c): - if c == 1: return a - else: return b - __builtin__.__dict__['ungettext'] = ungettext - def move_files_to_album(self, files, albumid=None, album=None): """Move `files` to tracks on album `albumid`.""" if album is None: From 6de59d8ee98e5cd7b2a9029697f136de58ba3b2c Mon Sep 17 00:00:00 2001 From: Laurent Monin Date: Mon, 27 May 2013 12:50:16 +0200 Subject: [PATCH 2/8] Add functions to convert bytes to human readable form. Binary and decimal modes are supported (MB and MiB ie.) It supports i18n using gettext and locale. Precision can be modified if needed, by default it is using 1 digit (if needed). Extensive tests were written, the toughest was to make them work for both default C locale and fr_FR.UTF-8 locale (ofc it is possible to test for more locales...). If one locale isn't available on testing system, test is skipped. fr locale was chosen because decimal point is replaced by a comma and byte units becomes "octet" units (1.5 MB in english -> 1,5 Mo in french). --- picard/util/bytes2human.py | 125 +++++++++++++++++++++++++++++ test/data/b2h_test_C.dat | 74 +++++++++++++++++ test/data/b2h_test_fr_FR.UTF-8.dat | 74 +++++++++++++++++ test/po/fr.po | 69 ++++++++++++++++ test/test_bytes2human.py | 99 +++++++++++++++++++++++ 5 files changed, 441 insertions(+) create mode 100644 picard/util/bytes2human.py create mode 100644 test/data/b2h_test_C.dat create mode 100644 test/data/b2h_test_fr_FR.UTF-8.dat create mode 100644 test/po/fr.po create mode 100644 test/test_bytes2human.py diff --git a/picard/util/bytes2human.py b/picard/util/bytes2human.py new file mode 100644 index 000000000..7bf1011ad --- /dev/null +++ b/picard/util/bytes2human.py @@ -0,0 +1,125 @@ +# -*- coding: utf-8 -*- +# +# Picard, the next-generation MusicBrainz tagger +# Copyright (C) 2013 Laurent Monin +# +# 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; either version 2 +# of the License, or (at your option) any later version. +# +# 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +import locale +import picard.i18n + + +""" +Helper class to convert bytes to human-readable form +It supports i18n through gettext, decimal and binary units. + +>>> n = 1572864 +>>> [binary(n), decimal(n)] +['1.5 MiB', '1.6 MB'] +""" + +#used to force gettextization +_BYTES_STRINGS_I18N = ( + N_('%s B'), + N_('%s kB'), + N_('%s KiB'), + N_('%s MB'), + N_('%s MiB'), + N_('%s GB'), + N_('%s GiB'), + N_('%s TB'), + N_('%s TiB'), + N_('%s PB'), + N_('%s PiB'), +) + + +def decimal(number, prec=1): + """ + Convert bytes to short human-readable string, decimal mode + + >>> [decimal(n) for n in [1000, 1024, 15500]] + ['1 kB', '1 kB', '15.5 kB'] + """ + return short_string(int(number), 1000) + + +def binary(number, prec=1): + """ + Convert bytes to short human-readable string, binary mode + >>> [binary(n) for n in [1000, 1024, 15500]] + ['1000 B', '1 KiB', '15.1 KiB'] + """ + return short_string(int(number), 1024, prec) + + +def short_string(number, multiple, prec=1): + """ + Returns short human-readable string for `number` bytes + >>> [short_string(n, 1024, 2) for n in [1000, 1100, 15500]] + ['1000 B', '1.07 KiB', '15.14 KiB'] + >>> [short_string(n, 1000, 1) for n in [10000, 11000, 1550000]] + ['10 kB', '11 kB', '1.6 MB'] + """ + num, unit = calc_unit(number, multiple) + n = int(num) + nr = round(num, prec) + if n == nr or unit == 'B': + fmt = '%d' + num = n + else: + fmt = '%%0.%df' % prec + num = nr + fmtnum = locale.format(fmt, num) + return _("%s " + unit) % fmtnum + + +def calc_unit(number, multiple=1000): + """ + Calculate rounded number of multiple * bytes, finding best unit + + >>> calc_unit(12456, 1024) + (12.1640625, 'KiB') + >>> calc_unit(-12456, 1000) + (-12.456, 'kB') + >>> calc_unit(0, 1001) + Traceback (most recent call last): + ... + ValueError: multiple parameter has to be 1000 or 1024 + """ + if number < 0: + sign = -1 + number = -number + else: + sign = 1 + n = float(number) + if multiple == 1000: + k, b = 'k', 'B' + elif multiple == 1024: + k, b = 'K', 'iB' + else: + raise ValueError('multiple parameter has to be 1000 or 1024') + + suffixes = ["B"] + [i + b for i in k + "MGTP"] + for suffix in suffixes: + if n < multiple or suffix == suffixes[-1]: + return (sign*n, suffix) + else: + n /= multiple + + +if __name__ == "__main__": + import doctest + doctest.testmod() diff --git a/test/data/b2h_test_C.dat b/test/data/b2h_test_C.dat new file mode 100644 index 000000000..5693d8825 --- /dev/null +++ b/test/data/b2h_test_C.dat @@ -0,0 +1,74 @@ +0;0 B;0 B;0 B +1;1 B;1 B;1 B +100;100 B;100 B;100 B +102;102 B;102 B;102 B +500;500 B;500 B;500 B +512;512 B;512 B;512 B +990;990 B;990 B;990 B +999;999 B;999 B;999 B +1000;1 kB;1000 B;1000 B +1013;1 kB;1013 B;1013 B +1023;1 kB;1023 B;1023 B +1024;1 kB;1 KiB;1 KiB +1500;1.5 kB;1.5 KiB;1.46 KiB +1536;1.5 kB;1.5 KiB;1.50 KiB +100000;100 kB;97.7 KiB;97.66 KiB +104857;104.9 kB;102.4 KiB;102.40 KiB +500000;500 kB;488.3 KiB;488.28 KiB +524288;524.3 kB;512 KiB;512 KiB +990000;990 kB;966.8 KiB;966.80 KiB +999900;999.9 kB;976.5 KiB;976.46 KiB +1000000;1 MB;976.6 KiB;976.56 KiB +1038090;1 MB;1013.8 KiB;1013.76 KiB +1048471;1 MB;1023.9 KiB;1023.90 KiB +1048576;1 MB;1 MiB;1 MiB +1500000;1.5 MB;1.4 MiB;1.43 MiB +1572864;1.6 MB;1.5 MiB;1.50 MiB +100000000;100 MB;95.4 MiB;95.37 MiB +107374182;107.4 MB;102.4 MiB;102.40 MiB +500000000;500 MB;476.8 MiB;476.84 MiB +536870912;536.9 MB;512 MiB;512 MiB +990000000;990 MB;944.1 MiB;944.14 MiB +999900000;999.9 MB;953.6 MiB;953.58 MiB +1000000000;1 GB;953.7 MiB;953.67 MiB +1063004405;1.1 GB;1013.8 MiB;1013.76 MiB +1073634449;1.1 GB;1023.9 MiB;1023.90 MiB +1073741824;1.1 GB;1 GiB;1 GiB +1500000000;1.5 GB;1.4 GiB;1.40 GiB +1610612736;1.6 GB;1.5 GiB;1.50 GiB +100000000000;100 GB;93.1 GiB;93.13 GiB +109951162777;110.0 GB;102.4 GiB;102.40 GiB +500000000000;500 GB;465.7 GiB;465.66 GiB +549755813888;549.8 GB;512 GiB;512 GiB +990000000000;990 GB;922 GiB;922.01 GiB +999900000000;999.9 GB;931.2 GiB;931.23 GiB +1000000000000;1 TB;931.3 GiB;931.32 GiB +1088516511498;1.1 TB;1013.8 GiB;1013.76 GiB +1099401676613;1.1 TB;1023.9 GiB;1023.90 GiB +1099511627776;1.1 TB;1 TiB;1 TiB +1500000000000;1.5 TB;1.4 TiB;1.36 TiB +1649267441664;1.6 TB;1.5 TiB;1.50 TiB +100000000000000;100 TB;90.9 TiB;90.95 TiB +112589990684262;112.6 TB;102.4 TiB;102.40 TiB +500000000000000;500 TB;454.7 TiB;454.75 TiB +562949953421312;562.9 TB;512 TiB;512 TiB +990000000000000;990 TB;900.4 TiB;900.40 TiB +999900000000000;999.9 TB;909.4 TiB;909.40 TiB +1000000000000000;1 PB;909.5 TiB;909.49 TiB +1114640907774197;1.1 PB;1013.8 TiB;1013.76 TiB +1125787316851939;1.1 PB;1023.9 TiB;1023.90 TiB +1125899906842624;1.1 PB;1 PiB;1 PiB +1500000000000000;1.5 PB;1.3 PiB;1.33 PiB +1688849860263936;1.7 PB;1.5 PiB;1.50 PiB +100000000000000000;100 PB;88.8 PiB;88.82 PiB +115292150460684704;115.3 PB;102.4 PiB;102.40 PiB +500000000000000000;500 PB;444.1 PiB;444.09 PiB +576460752303423488;576.5 PB;512 PiB;512 PiB +990000000000000000;990 PB;879.3 PiB;879.30 PiB +999900000000000000;999.9 PB;888.1 PiB;888.09 PiB +1000000000000000000;1000 PB;888.2 PiB;888.18 PiB +1141392289560778496;1141.4 PB;1013.8 PiB;1013.76 PiB +1152806212456386304;1152.8 PB;1023.9 PiB;1023.90 PiB +1152921504606846976;1152.9 PB;1024 PiB;1024 PiB +1500000000000000000;1500 PB;1332.3 PiB;1332.27 PiB +1729382256910270464;1729.4 PB;1536 PiB;1536 PiB diff --git a/test/data/b2h_test_fr_FR.UTF-8.dat b/test/data/b2h_test_fr_FR.UTF-8.dat new file mode 100644 index 000000000..f781da84e --- /dev/null +++ b/test/data/b2h_test_fr_FR.UTF-8.dat @@ -0,0 +1,74 @@ +0;0 o;0 o;0 o +1;1 o;1 o;1 o +100;100 o;100 o;100 o +102;102 o;102 o;102 o +500;500 o;500 o;500 o +512;512 o;512 o;512 o +990;990 o;990 o;990 o +999;999 o;999 o;999 o +1000;1 ko;1000 o;1000 o +1013;1 ko;1013 o;1013 o +1023;1 ko;1023 o;1023 o +1024;1 ko;1 Kio;1 Kio +1500;1,5 ko;1,5 Kio;1,46 Kio +1536;1,5 ko;1,5 Kio;1,50 Kio +100000;100 ko;97,7 Kio;97,66 Kio +104857;104,9 ko;102,4 Kio;102,40 Kio +500000;500 ko;488,3 Kio;488,28 Kio +524288;524,3 ko;512 Kio;512 Kio +990000;990 ko;966,8 Kio;966,80 Kio +999900;999,9 ko;976,5 Kio;976,46 Kio +1000000;1 Mo;976,6 Kio;976,56 Kio +1038090;1 Mo;1013,8 Kio;1013,76 Kio +1048471;1 Mo;1023,9 Kio;1023,90 Kio +1048576;1 Mo;1 Mio;1 Mio +1500000;1,5 Mo;1,4 Mio;1,43 Mio +1572864;1,6 Mo;1,5 Mio;1,50 Mio +100000000;100 Mo;95,4 Mio;95,37 Mio +107374182;107,4 Mo;102,4 Mio;102,40 Mio +500000000;500 Mo;476,8 Mio;476,84 Mio +536870912;536,9 Mo;512 Mio;512 Mio +990000000;990 Mo;944,1 Mio;944,14 Mio +999900000;999,9 Mo;953,6 Mio;953,58 Mio +1000000000;1 Go;953,7 Mio;953,67 Mio +1063004405;1,1 Go;1013,8 Mio;1013,76 Mio +1073634449;1,1 Go;1023,9 Mio;1023,90 Mio +1073741824;1,1 Go;1 Gio;1 Gio +1500000000;1,5 Go;1,4 Gio;1,40 Gio +1610612736;1,6 Go;1,5 Gio;1,50 Gio +100000000000;100 Go;93,1 Gio;93,13 Gio +109951162777;110,0 Go;102,4 Gio;102,40 Gio +500000000000;500 Go;465,7 Gio;465,66 Gio +549755813888;549,8 Go;512 Gio;512 Gio +990000000000;990 Go;922 Gio;922,01 Gio +999900000000;999,9 Go;931,2 Gio;931,23 Gio +1000000000000;1 To;931,3 Gio;931,32 Gio +1088516511498;1,1 To;1013,8 Gio;1013,76 Gio +1099401676613;1,1 To;1023,9 Gio;1023,90 Gio +1099511627776;1,1 To;1 Tio;1 Tio +1500000000000;1,5 To;1,4 Tio;1,36 Tio +1649267441664;1,6 To;1,5 Tio;1,50 Tio +100000000000000;100 To;90,9 Tio;90,95 Tio +112589990684262;112,6 To;102,4 Tio;102,40 Tio +500000000000000;500 To;454,7 Tio;454,75 Tio +562949953421312;562,9 To;512 Tio;512 Tio +990000000000000;990 To;900,4 Tio;900,40 Tio +999900000000000;999,9 To;909,4 Tio;909,40 Tio +1000000000000000;1 Po;909,5 Tio;909,49 Tio +1114640907774197;1,1 Po;1013,8 Tio;1013,76 Tio +1125787316851939;1,1 Po;1023,9 Tio;1023,90 Tio +1125899906842624;1,1 Po;1 Pio;1 Pio +1500000000000000;1,5 Po;1,3 Pio;1,33 Pio +1688849860263936;1,7 Po;1,5 Pio;1,50 Pio +100000000000000000;100 Po;88,8 Pio;88,82 Pio +115292150460684704;115,3 Po;102,4 Pio;102,40 Pio +500000000000000000;500 Po;444,1 Pio;444,09 Pio +576460752303423488;576,5 Po;512 Pio;512 Pio +990000000000000000;990 Po;879,3 Pio;879,30 Pio +999900000000000000;999,9 Po;888,1 Pio;888,09 Pio +1000000000000000000;1000 Po;888,2 Pio;888,18 Pio +1141392289560778496;1141,4 Po;1013,8 Pio;1013,76 Pio +1152806212456386304;1152,8 Po;1023,9 Pio;1023,90 Pio +1152921504606846976;1152,9 Po;1024 Pio;1024 Pio +1500000000000000000;1500 Po;1332,3 Pio;1332,27 Pio +1729382256910270464;1729,4 Po;1536 Pio;1536 Pio diff --git a/test/po/fr.po b/test/po/fr.po new file mode 100644 index 000000000..1f58b585b --- /dev/null +++ b/test/po/fr.po @@ -0,0 +1,69 @@ +msgid "" +msgstr "" +"Project-Id-Version: MusicBrainz\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2013-01-20 13:23+0100\n" +"PO-Revision-Date: 2013-01-20 13:26+0100\n" +"Last-Translator: Laurent Monin \n" +"Language-Team: French (http://www.transifex.com/projects/p/musicbrainz/language/fr/)\n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 0.9.6\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: picard/util/bytes2human.py:47 +#, python-format +msgid "%s B" +msgstr "%s o" + +#: picard/util/bytes2human.py:48 +#, python-format +msgid "%s kB" +msgstr "%s ko" + +#: picard/util/bytes2human.py:49 +#, python-format +msgid "%s KiB" +msgstr "%s Kio" + +#: picard/util/bytes2human.py:50 +#, python-format +msgid "%s MB" +msgstr "%s Mo" + +#: picard/util/bytes2human.py:51 +#, python-format +msgid "%s MiB" +msgstr "%s Mio" + +#: picard/util/bytes2human.py:52 +#, python-format +msgid "%s GB" +msgstr "%s Go" + +#: picard/util/bytes2human.py:53 +#, python-format +msgid "%s GiB" +msgstr "%s Gio" + +#: picard/util/bytes2human.py:54 +#, python-format +msgid "%s TB" +msgstr "%s To" + +#: picard/util/bytes2human.py:55 +#, python-format +msgid "%s TiB" +msgstr "%s Tio" + +#: picard/util/bytes2human.py:56 +#, python-format +msgid "%s PB" +msgstr "%s Po" + +#: picard/util/bytes2human.py:57 +#, python-format +msgid "%s PiB" +msgstr "%s Pio" diff --git a/test/test_bytes2human.py b/test/test_bytes2human.py new file mode 100644 index 000000000..06038bb41 --- /dev/null +++ b/test/test_bytes2human.py @@ -0,0 +1,99 @@ +import locale +import os.path +import shutil +import subprocess +import sys +import tempfile +import unittest + +from picard.i18n import setup_gettext +from picard.util import bytes2human + +class Testbytes2human(unittest.TestCase): + def setUp(self): + # we are using temporary locales for tests + self.tmp_path = tempfile.mkdtemp().decode("utf-8") + if sys.hexversion >= 0x020700F0: + self.addCleanup(shutil.rmtree, self.tmp_path) + self.localedir = os.path.join(self.tmp_path, 'locale') + + test_locales = [('picard', 'fr', 'test/po/fr.po')] + for domain, locale, po in test_locales: + path = os.path.join(self.localedir, locale, 'LC_MESSAGES') + os.makedirs(path) + mo = os.path.join(path, '%s.mo' % domain) + assert(subprocess.call(['msgfmt', '-o', mo, po]) == 0) + + def tearDown(self): + if sys.hexversion < 0x020700F0: + shutil.rmtree(self.tmp_path) + + def test_00(self): + # testing with default C locale, english + lang = 'C' + setup_gettext(self.localedir, lang) + self.run_test(lang) + + self.assertEqual(bytes2human.binary(45682), '44.6 KiB') + self.assertEqual(bytes2human.binary(-45682), '-44.6 KiB') + self.assertEqual(bytes2human.decimal(45682), '45.7 kB') + self.assertEqual(bytes2human.decimal(9223372036854775807), '9223.4 PB') + self.assertEqual(bytes2human.decimal(123.6), '123 B') + self.assertRaises(ValueError, bytes2human.decimal, 'xxx') + self.assertRaises(ValueError, bytes2human.decimal, '123.6') + self.assertRaises(ValueError, bytes2human.binary, 'yyy') + self.assertRaises(ValueError, bytes2human.binary, '456yyy') + try: + bytes2human.decimal(u'123') + except Exception as e: + self.fail('Unexpected exception: %s' % e) + + def test_05(self): + # testing with french locale and translation + # 1.5 MiB -> 1,5 Mio + lang = 'fr_FR.UTF-8' + setup_gettext(self.localedir, lang) + self.run_test(lang) + + def run_test(self, lang = 'C', create_test_data=False): + """ + Compare data generated with sample files + Setting create_test_data to True will generated sample files + from code execution (developper-only, check carefully) + """ + filename = os.path.join('test', 'data', 'b2h_test_%s.dat' % lang) + testlist = self._create_testlist() + if create_test_data: + self._save_expected_to(filename, testlist) + expected = self._read_expected_from(filename) + #self.maxDiff = None + self.assertEqual(testlist, expected) + if create_test_data: + # be sure it is disabled + self.fail('!!! UNSET create_test_data mode !!! (%s)' % filename) + + def _create_testlist(self): + values = [0, 1] + for n in [1000, 1024]: + p = 1 + for e in range(0,6): + p *= n + for x in [0.1, 0.5, 0.99, 0.9999, 1, 1.5]: + values.append(int(p*x)) + l = [] + for x in sorted(values): + l.append(";".join([str(x), bytes2human.decimal(x), + bytes2human.binary(x), + bytes2human.short_string(x, 1024, 2)])) + return l + + def _save_expected_to(self, path, a_list): + with open(path, 'wb') as f: + f.writelines([l + "\n" for l in a_list]) + f.close() + + def _read_expected_from(self, path): + with open(path, 'rb') as f: + lines = [l.rstrip("\n") for l in f.readlines()] + f.close() + return lines From d2005ddd4c272120b23d5deb68edfd0ac1e8e978 Mon Sep 17 00:00:00 2001 From: Laurent Monin Date: Sun, 20 Jan 2013 23:40:05 +0100 Subject: [PATCH 3/8] Use bytes2human to display file size in Info dialog. --- picard/ui/infodialog.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/picard/ui/infodialog.py b/picard/ui/infodialog.py index 7289911a4..f5dc058f2 100644 --- a/picard/ui/infodialog.py +++ b/picard/ui/infodialog.py @@ -19,7 +19,7 @@ import os.path from PyQt4 import QtGui -from picard.util import format_time, encode_filename +from picard.util import format_time, encode_filename, bytes2human from picard.ui.ui_infodialog import Ui_InfoDialog @@ -75,13 +75,8 @@ class FileInfoDialog(InfoDialog): info.append((_('Format:'), file.orig_metadata['~format'])) try: size = os.path.getsize(encode_filename(file.filename)) - if size < 1024: - size = '%d B' % size - elif size < 1024 * 1024: - size = '%0.1f kB' % (size / 1024.0) - else: - size = '%0.1f MB' % (size / 1024.0 / 1024.0) - info.append((_('Size:'), size)) + sizestr = "%s (%s)" % (bytes2human.decimal(size), bytes2human.binary(size)) + info.append((_('Size:'), sizestr)) except: pass if file.orig_metadata.length: From 9f213b15fc0a535645648559e0b79196afdeae99 Mon Sep 17 00:00:00 2001 From: Laurent Monin Date: Sun, 20 Jan 2013 23:40:41 +0100 Subject: [PATCH 4/8] Display cover art images sizes and dimensions in Info dialog --- picard/ui/infodialog.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/picard/ui/infodialog.py b/picard/ui/infodialog.py index f5dc058f2..eff24d105 100644 --- a/picard/ui/infodialog.py +++ b/picard/ui/infodialog.py @@ -48,11 +48,17 @@ class InfoDialog(QtGui.QDialog): for image in images: data = image["data"] + size = len(data) item = QtGui.QListWidgetItem() pixmap = QtGui.QPixmap() pixmap.loadFromData(data) icon = QtGui.QIcon(pixmap) item.setIcon(icon) + s = "%s (%s)\n%d x %d" % (bytes2human.decimal(size), + bytes2human.binary(size), + pixmap.width(), + pixmap.height()) + item.setText(s) self.ui.artwork_list.addItem(item) def tab_hide(self, widget): From 86e8db315b35235931b8d44393c4ce0a0cba0c4e Mon Sep 17 00:00:00 2001 From: Laurent Monin Date: Mon, 10 Jun 2013 14:38:57 +0200 Subject: [PATCH 5/8] Make Picard search box smart enough to detect mbids. If a release mbid is given (search type = Album) it will try to load it as when using web browser tagger link. If string looks like a known entity (artist, release-group, label, track) it will open browser on corresponding url. Examples: Select box on Album + "abb7caf1-ac93-48e1-a066-60caad07e1a7" -> load release (http://musicbrainz.org/release/abb7caf1-ac93-48e1-a066-60caad07e1a7) Select box on anything + "http://musicbrainz.org/artist/c19ff12b-058f-44a8-b245-b0efb4752925" -> open browser on Artist page "ARTIST := c19ff12b-058f-44a8-b245-b0efb4752925 !!" -> open Artist page --- picard/browser/filelookup.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/picard/browser/filelookup.py b/picard/browser/filelookup.py index 096bf3984..93408d4f3 100644 --- a/picard/browser/filelookup.py +++ b/picard/browser/filelookup.py @@ -20,6 +20,8 @@ from PyQt4 import QtCore import os.path +import re +from picard import log from picard.util import webbrowser2 class FileLookup(object): @@ -33,7 +35,9 @@ class FileLookup(object): return str(QtCore.QUrl.toPercentEncoding(text)) def launch(self, url): + log.debug("webbrowser2: %s" % url) webbrowser2.open(url) + return True def discLookup(self, url): return self.launch("%s&tport=%d" % (url, self.localPort)) @@ -56,11 +60,34 @@ class FileLookup(object): def artistLookup(self, artist_id): return self._lookup('artist', artist_id) + def mbidLookup(self, string, type_): + """Parses string for known entity type and mbid, open browser for it + If entity type is 'release', it will load corresponding release if + possible. + """ + uuid = '[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}' + entity_type = '(?:artist|release-group|release|track|label)' + regex = r"\b(%s)?\W*(%s)" % (entity_type, uuid) + m = re.search(regex, string, re.IGNORECASE) + if m is None: + return False + if m.group(1) is None: + entity = type_ + else: + entity = m.group(1).lower() + mbid = m.group(2).lower() + if entity == 'release': + QtCore.QObject.tagger.load_album(mbid) + return True + return self._lookup(entity, mbid) + def _search(self, type_, query, adv=False): + if self.mbidLookup(query, type_): + return True url = "http://%s:%d/search/textsearch?limit=25&type=%s&query=%s&tport=%d" % ( self._encode(self.server), self.port, - type_, + type_, self._encode(query), self.localPort) if adv: From 51ea26f8cc5a095af6f9bfa2fae45fa96c314dd6 Mon Sep 17 00:00:00 2001 From: Laurent Monin Date: Tue, 11 Jun 2013 12:43:29 +0200 Subject: [PATCH 6/8] Complete the list of known entities. --- picard/browser/filelookup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/picard/browser/filelookup.py b/picard/browser/filelookup.py index 93408d4f3..d99c60a65 100644 --- a/picard/browser/filelookup.py +++ b/picard/browser/filelookup.py @@ -66,7 +66,7 @@ class FileLookup(object): possible. """ uuid = '[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}' - entity_type = '(?:artist|release-group|release|track|label)' + entity_type = '(?:release-group|release|recording|work|artist|label|url|area|track)' regex = r"\b(%s)?\W*(%s)" % (entity_type, uuid) m = re.search(regex, string, re.IGNORECASE) if m is None: From 513b51d73025c8b0347c8fe55e81a8ba53f5f12f Mon Sep 17 00:00:00 2001 From: Calvin Walton Date: Sat, 15 Jun 2013 11:32:55 -0400 Subject: [PATCH 7/8] Fix performed vocals attribute with no vocal type set This fixes a regression since e16b2533c where picard no saves vocal ARs with no type set as generic "performer" ars; appearing in the UI like Performer []: Some Vocalist Performer [guest]: Some Guest Vocalist Add special handling for the vocal relationship type to use a fallback string if there is no vocal type set. The fallback string is "vocals" (earlier versions of picard used "vocal"), for consistancy with the current vocal type strings, which look like "lead vocals", "background vocals", etc. --- picard/mbxml.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/picard/mbxml.py b/picard/mbxml.py index f607e384e..705690623 100644 --- a/picard/mbxml.py +++ b/picard/mbxml.py @@ -55,7 +55,8 @@ def _decamelcase(text): _REPLACE_MAP = {} _EXTRA_ATTRS = ['guest', 'additional', 'minor'] -def _parse_attributes(attrs): +_BLANK_SPECIAL_RELTYPES = {'vocal': 'vocals'} +def _parse_attributes(attrs, reltype): attrs = [_decamelcase(_REPLACE_MAP.get(a, a)) for a in attrs] prefix = ' '.join([a for a in attrs if a in _EXTRA_ATTRS]) attrs = [a for a in attrs if a not in _EXTRA_ATTRS] @@ -64,7 +65,7 @@ def _parse_attributes(attrs): elif len(attrs) == 1: attrs = attrs[0] else: - attrs = '' + attrs = _BLANK_SPECIAL_RELTYPES.get(reltype, '') return ' '.join([prefix, attrs]).strip().lower() @@ -79,7 +80,7 @@ def _relations_to_metadata(relation_lists, m): if 'attribute_list' in relation.children: attribs = [a.text for a in relation.attribute_list[0].attribute] if reltype in ('vocal', 'instrument', 'performer'): - name = 'performer:' + _parse_attributes(attribs) + name = 'performer:' + _parse_attributes(attribs, reltype) elif reltype == 'mix-DJ' and len(attribs) > 0: if not hasattr(m, "_djmix_ars"): m._djmix_ars = {} From dfc790226d29dd1bc65773f5c0688bed2d2610a2 Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Sun, 16 Jun 2013 21:49:51 -0500 Subject: [PATCH 8/8] Remove some obsolete code Python 2.5 is no longer supported. --- picard/tagger.py | 8 -------- picard/util/queue.py | 22 +++++----------------- 2 files changed, 5 insertions(+), 25 deletions(-) diff --git a/picard/tagger.py b/picard/tagger.py index a0b54d54e..33af04469 100644 --- a/picard/tagger.py +++ b/picard/tagger.py @@ -34,14 +34,6 @@ from itertools import chain import __builtin__ __builtin__.__dict__['N_'] = lambda a: a -# Py2exe 0.6.6 has broken fake_getline which doesn't work with Python 2.5 -if hasattr(sys, "frozen"): - import linecache - def fake_getline(filename, lineno, module_globals = None): - return '' - linecache.getline = fake_getline - del linecache, fake_getline - # A "fix" for http://python.org/sf/1438480 def _patched_shutil_copystat(src, dst): try: _orig_shutil_copystat(src, dst) diff --git a/picard/util/queue.py b/picard/util/queue.py index 1955b8c7e..7203775d1 100644 --- a/picard/util/queue.py +++ b/picard/util/queue.py @@ -51,10 +51,13 @@ class Queue: self.mutex.unlock() def remove(self,item): - """Remove an item into the queue.""" + """Remove an item from the queue.""" self.mutex.lock() try: - self._remove(item) + self.queue.remove(item) + except ValueError: + pass + else: self.not_full.wakeOne() finally: self.mutex.unlock() @@ -91,21 +94,6 @@ class Queue: def _put(self, item): self.queue.append(item) - # Remove an item from the queue - def _remove(self, item): - if item in self.queue: - try: - # remove is only availible in python 2.5 - self.queue.remove(item) - except AttributeError: - # remove items this way in older versions of python. - for i in range(0, len(self.queue)): - if self.queue[i] == item: - self.queue.rotate(-i) - self.queue.popleft() - self.queue.rotate(i) - break - # Get an item from the queue def _get(self): return self.queue.popleft()