mirror of
https://github.com/fergalmoran/picard.git
synced 2026-02-21 15:14:13 +00:00
Merge branch 'master' into artists-tag
Conflicts: NEWS.txt
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
[main]
|
||||
host = https://www.transifex.net
|
||||
host = https://www.transifex.com
|
||||
|
||||
[musicbrainz.picard]
|
||||
file_filter = po/<lang>.po
|
||||
source_file = po/picard.pot
|
||||
source_lang = en
|
||||
|
||||
type = PO
|
||||
|
||||
2
NEWS.txt
2
NEWS.txt
@@ -16,6 +16,8 @@
|
||||
* Main window is now emitting a "selection_updated" signal, plugin api version bumps to 1.3.0
|
||||
* Append system information to user-agent string
|
||||
* Compilation tag/variable aligned with iTunes, set only for Various Artists type compilations.
|
||||
* Ignore directories and files while indexing when show_hidden_files option is set to False (PICARD-528)
|
||||
* Add ignore_regex option which allows one to ignore matching paths, can be set in Options > Advanced (PICARD-528)
|
||||
* Added an "artists" tag to track metadata, based on the one in Jaikoz, which
|
||||
contains the individual artist names from the artist credit.
|
||||
|
||||
|
||||
14
README.md
Normal file
14
README.md
Normal file
@@ -0,0 +1,14 @@
|
||||
MusicBrainz Picard
|
||||
==================
|
||||
|
||||
[MusicBrainz Picard](http://musicbrainz.org/doc/MusicBrainz_Picard) is a cross-platform (Linux/Mac OS X/Windows) application written in Python and is the official [MusicBrainz](http://musicbrainz.org) tagger.
|
||||
|
||||
Picard supports the majority of audio file formats, is capable of using audio fingerprints ([AcoustIDs](http://musicbrainz.org/doc/AcoustID)), performing CD lookups and [disc ID](http://musicbrainz.org/doc/Disc_ID) submissions, and it has excellent Unicode support. Additionally, there are several plugins available that extend Picard's features.
|
||||
|
||||
When tagging files, Picard uses an album-oriented approach. This approach allows it to utilize the MusicBrainz data as effectively as possible and correctly tag your music. For more information, [see the illustrated quick start guide to tagging](http://musicbrainz.org/doc/How_To_Tag_Files_With_Picard).
|
||||
|
||||
Picard is named after Captain Jean-Luc Picard from the TV series Star Trek: The Next Generation.
|
||||
|
||||
Binary downloads are available [here](http://musicbrainz.org/doc/MusicBrainz_Picard).
|
||||
|
||||
To submit bugs or improvements, please use [Picard bug tracker](http://tickets.musicbrainz.org/browse/PICARD).
|
||||
@@ -25,22 +25,42 @@ PICARD_ORG_NAME = "MusicBrainz"
|
||||
PICARD_VERSION = (1, 3, 0, 'dev', 2)
|
||||
|
||||
|
||||
def version_to_string(version_tuple, short=False):
|
||||
assert len(version_tuple) == 5
|
||||
assert version_tuple[3] in ('final', 'dev')
|
||||
if short and version_tuple[3] == 'final':
|
||||
if version_tuple[2] == 0:
|
||||
version_str = '%d.%d' % version_tuple[:2]
|
||||
class VersionError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def version_to_string(version, short=False):
|
||||
if len(version) != 5:
|
||||
raise VersionError("Length != 5")
|
||||
if version[3] not in ('final', 'dev'):
|
||||
raise VersionError("Should be either 'final' or 'dev'")
|
||||
_version = []
|
||||
for p in version:
|
||||
try:
|
||||
n = int(p)
|
||||
except ValueError:
|
||||
n = p
|
||||
pass
|
||||
_version.append(n)
|
||||
version = tuple(_version)
|
||||
if short and version[3] == 'final':
|
||||
if version[2] == 0:
|
||||
version_str = '%d.%d' % version[:2]
|
||||
else:
|
||||
version_str = '%d.%d.%d' % version_tuple[:3]
|
||||
version_str = '%d.%d.%d' % version[:3]
|
||||
else:
|
||||
version_str = '%d.%d.%d%s%d' % version_tuple
|
||||
version_str = '%d.%d.%d%s%d' % version
|
||||
return version_str
|
||||
|
||||
|
||||
_version_re = re.compile("(\d+)[._](\d+)[._](\d+)[._]?(dev|final)[._]?(\d+)$")
|
||||
def version_from_string(version_str):
|
||||
g = re.match(r"^(\d+).(\d+).(\d+)(dev|final)(\d+)$", version_str).groups()
|
||||
return (int(g[0]), int(g[1]), int(g[2]), g[3], int(g[4]))
|
||||
m = _version_re.search(version_str)
|
||||
if m:
|
||||
g = m.groups()
|
||||
return (int(g[0]), int(g[1]), int(g[2]), g[3], int(g[4]))
|
||||
raise VersionError("String '%s' do not match regex '%s'" % (version_str,
|
||||
_version_re.pattern))
|
||||
|
||||
|
||||
__version__ = PICARD_VERSION_STR = version_to_string(PICARD_VERSION)
|
||||
|
||||
@@ -21,7 +21,7 @@ from collections import deque
|
||||
from functools import partial
|
||||
from PyQt4 import QtCore
|
||||
from picard import config, log
|
||||
from picard.const import ACOUSTID_KEY, FPCALC_NAMES
|
||||
from picard.const import FPCALC_NAMES
|
||||
from picard.util import find_executable
|
||||
from picard.webservice import XmlNode
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ from heapq import heappush, heappop
|
||||
from PyQt4 import QtCore
|
||||
from picard import config
|
||||
from picard.metadata import Metadata
|
||||
from picard.similarity import similarity2, similarity
|
||||
from picard.similarity import similarity
|
||||
from picard.ui.item import Item
|
||||
from picard.util import format_time
|
||||
|
||||
@@ -189,10 +189,10 @@ class Cluster(QtCore.QObject, Item):
|
||||
albumDict.add(album)))
|
||||
|
||||
artist_cluster_engine = ClusterEngine(artistDict)
|
||||
artist_cluster = artist_cluster_engine.cluster(threshold)
|
||||
artist_cluster_engine.cluster(threshold)
|
||||
|
||||
album_cluster_engine = ClusterEngine(albumDict)
|
||||
album_cluster = album_cluster_engine.cluster(threshold)
|
||||
album_cluster_engine.cluster(threshold)
|
||||
|
||||
# Arrange tracks into albums
|
||||
albums = {}
|
||||
@@ -458,7 +458,5 @@ class ClusterEngine(object):
|
||||
self.idClusterIndex[match] = match0
|
||||
del self.clusterBins[match1]
|
||||
|
||||
return self.clusterBins
|
||||
|
||||
def can_refresh(self):
|
||||
return False
|
||||
|
||||
@@ -17,10 +17,11 @@
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
import re
|
||||
from operator import itemgetter
|
||||
from PyQt4 import QtCore
|
||||
from picard import (PICARD_APP_NAME, PICARD_ORG_NAME, PICARD_VERSION,
|
||||
version_to_string, version_from_string, log)
|
||||
version_to_string, version_from_string)
|
||||
from picard.util import LockableObject, rot13
|
||||
|
||||
|
||||
@@ -84,7 +85,7 @@ class Config(QtCore.QSettings):
|
||||
|
||||
TextOption("application", "version", '0.0.0dev0')
|
||||
self._version = version_from_string(self.application["version"])
|
||||
self._upgrade_hooks = []
|
||||
self._upgrade_hooks = dict()
|
||||
|
||||
def switchProfile(self, profilename):
|
||||
"""Sets the current profile."""
|
||||
@@ -94,17 +95,15 @@ class Config(QtCore.QSettings):
|
||||
else:
|
||||
raise KeyError("Unknown profile '%s'" % (profilename,))
|
||||
|
||||
def register_upgrade_hook(self, to_version_str, func, *args):
|
||||
def register_upgrade_hook(self, func, *args):
|
||||
"""Register a function to upgrade from one config version to another"""
|
||||
to_version = version_from_string(to_version_str)
|
||||
to_version = version_from_string(func.__name__)
|
||||
assert to_version <= PICARD_VERSION, "%r > %r !!!" % (to_version, PICARD_VERSION)
|
||||
hook = {
|
||||
'to': to_version,
|
||||
self._upgrade_hooks[to_version] = {
|
||||
'func': func,
|
||||
'args': args,
|
||||
'done': False
|
||||
}
|
||||
self._upgrade_hooks.append(hook)
|
||||
|
||||
def run_upgrade_hooks(self):
|
||||
"""Executes registered functions to upgrade config version to the latest"""
|
||||
@@ -118,29 +117,30 @@ class Config(QtCore.QSettings):
|
||||
version_to_string(PICARD_VERSION)
|
||||
))
|
||||
return
|
||||
# sort upgrade hooks by version
|
||||
self._upgrade_hooks.sort(key=itemgetter("to"))
|
||||
for hook in self._upgrade_hooks:
|
||||
if self._version < hook['to']:
|
||||
for version in sorted(self._upgrade_hooks):
|
||||
hook = self._upgrade_hooks[version]
|
||||
if self._version < version:
|
||||
try:
|
||||
hook['func'](*hook['args'])
|
||||
except Exception as e:
|
||||
except:
|
||||
import traceback
|
||||
raise ConfigUpgradeError(
|
||||
"Error during config upgrade from version %s to %s "
|
||||
"using %s(): %s" % (
|
||||
"using %s():\n%s" % (
|
||||
version_to_string(self._version),
|
||||
version_to_string(hook['to']),
|
||||
hook['func'].__name__, e
|
||||
version_to_string(version),
|
||||
hook['func'].__name__,
|
||||
traceback.format_exc()
|
||||
))
|
||||
else:
|
||||
hook['done'] = True
|
||||
self._version = hook['to']
|
||||
self._version = version
|
||||
self._write_version()
|
||||
else:
|
||||
# hook is not applicable, mark as done
|
||||
hook['done'] = True
|
||||
|
||||
if all(map(itemgetter("done"), self._upgrade_hooks)):
|
||||
if all(map(itemgetter("done"), self._upgrade_hooks.values())):
|
||||
# all hooks were executed, ensure config is marked with latest version
|
||||
self._version = PICARD_VERSION
|
||||
self._write_version()
|
||||
|
||||
110
picard/config_upgrade.py
Normal file
110
picard/config_upgrade.py
Normal file
@@ -0,0 +1,110 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Picard, the next-generation MusicBrainz tagger
|
||||
# Copyright (C) 2013-2014 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.
|
||||
|
||||
|
||||
from PyQt4 import QtGui
|
||||
|
||||
import re
|
||||
from picard import (log, config)
|
||||
|
||||
|
||||
# TO ADD AN UPGRADE HOOK:
|
||||
# ----------------------
|
||||
# add a function here, named after the version you want upgrade to
|
||||
# ie. upgrade_to_v1_0_0_dev_1() for 1.0.0dev1
|
||||
# register it in upgrade_config()
|
||||
# and modify PICARD_VERSION to match it
|
||||
#
|
||||
|
||||
_s = config.setting
|
||||
|
||||
|
||||
# In version 1.0, the file naming formats for single and various
|
||||
# artist releases were merged.
|
||||
def upgrade_to_v1_0_0_final_0():
|
||||
def remove_va_file_naming_format(merge=True):
|
||||
if merge:
|
||||
_s["file_naming_format"] = (
|
||||
"$if($eq(%%compilation%%,1),\n$noop(Various Artist "
|
||||
"albums)\n%s,\n$noop(Single Artist Albums)\n%s)" % (
|
||||
_s["va_file_naming_format"].toString(),
|
||||
_s["file_naming_format"]
|
||||
))
|
||||
_s.remove("va_file_naming_format")
|
||||
_s.remove("use_va_format")
|
||||
|
||||
if ("va_file_naming_format" in _s and
|
||||
"use_va_format" in _s):
|
||||
|
||||
msgbox = QtGui.QMessageBox()
|
||||
if _s["use_va_format"].toBool():
|
||||
remove_va_file_naming_format()
|
||||
msgbox.information(msgbox,
|
||||
_("Various Artists file naming scheme removal"),
|
||||
_("The separate file naming scheme for various artists "
|
||||
"albums has been removed in this version of Picard.\n"
|
||||
"Your file naming scheme has automatically been "
|
||||
"merged with that of single artist albums."),
|
||||
QtGui.QMessageBox.Ok)
|
||||
|
||||
elif (_s["va_file_naming_format"].toString() !=
|
||||
r"$if2(%albumartist%,%artist%)/%album%/$if($gt(%totaldis"
|
||||
"cs%,1),%discnumber%-,)$num(%tracknumber%,2) %artist% - "
|
||||
"%title%"):
|
||||
|
||||
answer = msgbox.question(msgbox,
|
||||
_("Various Artists file naming scheme removal"),
|
||||
_("The separate file naming scheme for various artists "
|
||||
"albums has been removed in this version of Picard.\n"
|
||||
"You currently do not use this option, but have a "
|
||||
"separate file naming scheme defined.\n"
|
||||
"Do you want to remove it or merge it with your file "
|
||||
"naming scheme for single artist albums?"),
|
||||
_("Merge"), _("Remove"))
|
||||
|
||||
if answer:
|
||||
remove_va_file_naming_format(merge=False)
|
||||
else:
|
||||
remove_va_file_naming_format()
|
||||
else:
|
||||
# default format, disabled
|
||||
remove_va_file_naming_format(merge=False)
|
||||
|
||||
|
||||
def upgrade_to_v1_3_0_dev_1():
|
||||
if "windows_compatible_filenames" in _s:
|
||||
_s["windows_compatibility"] = _s["windows_compatible_filenames"]
|
||||
_s.remove("windows_compatible_filenames")
|
||||
log.info(_('Config upgrade: option "windows_compatible_filenames" '
|
||||
' was renamed "windows_compatibility" (PICARD-110).'))
|
||||
|
||||
|
||||
def upgrade_to_v1_3_0_dev_2():
|
||||
if "preserved_tags" in _s:
|
||||
_s["preserved_tags"] = re.sub(r"\s+", ",", _s["preserved_tags"].strip())
|
||||
log.info(_('Config upgrade: option "preserved_tags" is now using '
|
||||
'comma instead of spaces as tag separator (PICARD-536).'))
|
||||
|
||||
|
||||
def upgrade_config():
|
||||
cfg = config._config
|
||||
cfg.register_upgrade_hook(upgrade_to_v1_0_0_final_0)
|
||||
cfg.register_upgrade_hook(upgrade_to_v1_3_0_dev_1)
|
||||
cfg.register_upgrade_hook(upgrade_to_v1_3_0_dev_2)
|
||||
cfg.run_upgrade_hooks()
|
||||
@@ -19,7 +19,6 @@
|
||||
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
|
||||
# Install gettext "noop" function in case const.py gets imported directly.
|
||||
import __builtin__
|
||||
|
||||
@@ -24,10 +24,9 @@
|
||||
import json
|
||||
import re
|
||||
import traceback
|
||||
import picard.webservice
|
||||
from functools import partial
|
||||
from picard import config, log
|
||||
from picard.metadata import Metadata, is_front_image
|
||||
from picard.metadata import is_front_image
|
||||
from picard.util import mimetype, parse_amazon_url
|
||||
from picard.const import CAA_HOST, CAA_PORT
|
||||
from PyQt4.QtCore import QUrl, QObject
|
||||
|
||||
@@ -29,11 +29,9 @@ from operator import itemgetter
|
||||
from collections import defaultdict
|
||||
from PyQt4 import QtCore
|
||||
from picard import config, log
|
||||
from picard.track import Track
|
||||
from picard.metadata import Metadata
|
||||
from picard.ui.item import Item
|
||||
from picard.script import ScriptParser
|
||||
from picard.similarity import similarity2
|
||||
from picard.util import (
|
||||
decode_filename,
|
||||
encode_filename,
|
||||
@@ -144,7 +142,7 @@ class File(QtCore.QObject, Item):
|
||||
def has_error(self):
|
||||
return self.state == File.ERROR
|
||||
|
||||
def _load(self):
|
||||
def _load(self, filename):
|
||||
"""Load metadata from the file."""
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -281,6 +279,8 @@ class File(QtCore.QObject, Item):
|
||||
new_filename = os.path.basename(new_filename)
|
||||
new_filename = make_short_filename(new_dirname, new_filename,
|
||||
config.setting['windows_compatibility'], config.setting['windows_compatibility_drive_root'])
|
||||
# TODO: move following logic under util.filenaming
|
||||
# (and reconsider its necessity)
|
||||
# win32 compatibility fixes
|
||||
if settings['windows_compatibility'] or sys.platform == 'win32':
|
||||
new_filename = new_filename.replace('./', '_/').replace('.\\', '_\\')
|
||||
@@ -359,8 +359,8 @@ class File(QtCore.QObject, Item):
|
||||
# image multiple times
|
||||
if (os.path.exists(new_filename) and
|
||||
os.path.getsize(new_filename) == len(data)):
|
||||
log.debug("Identical file size, not saving %r", image_filename)
|
||||
continue
|
||||
log.debug("Identical file size, not saving %r", image_filename)
|
||||
continue
|
||||
log.debug("Saving cover images to %r", image_filename)
|
||||
new_dirname = os.path.dirname(image_filename)
|
||||
if not os.path.isdir(new_dirname):
|
||||
|
||||
@@ -22,7 +22,7 @@ import struct
|
||||
from struct import pack, unpack
|
||||
import mutagen
|
||||
from mutagen._util import insert_bytes
|
||||
from mutagen.id3 import ID3, Frame, Frames, Frames_2_2, TextFrame, TORY, \
|
||||
from mutagen.id3 import ID3, Frames, Frames_2_2, TextFrame, TORY, \
|
||||
TYER, TIME, APIC, IPLS, TDAT, BitPaddedInt, MakeID3v1
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
# -*- 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 gettext
|
||||
import locale
|
||||
import os.path
|
||||
|
||||
@@ -62,7 +62,7 @@ class Logger(object):
|
||||
for func in self._receivers:
|
||||
try:
|
||||
thread.to_main(func, level, time, message)
|
||||
except Exception as e:
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
@@ -21,8 +21,7 @@ import traceback
|
||||
from collections import defaultdict
|
||||
from functools import partial
|
||||
from itertools import combinations
|
||||
from PyQt4 import QtCore
|
||||
from picard import config, log
|
||||
from picard import log
|
||||
from picard.metadata import Metadata
|
||||
from picard.dataobj import DataObject
|
||||
from picard.mbxml import media_formats_from_node, label_info_from_node
|
||||
|
||||
@@ -22,11 +22,9 @@ from PyQt4 import QtGui, QtCore
|
||||
|
||||
import getopt
|
||||
import os.path
|
||||
import re
|
||||
import shutil
|
||||
import signal
|
||||
import sys
|
||||
from collections import deque
|
||||
from functools import partial
|
||||
from itertools import chain
|
||||
|
||||
@@ -61,16 +59,17 @@ from picard.track import Track, NonAlbumTrack
|
||||
from picard.releasegroup import ReleaseGroup
|
||||
from picard.collection import load_user_collections
|
||||
from picard.ui.mainwindow import MainWindow
|
||||
from picard.ui.itemviews import BaseTreeView
|
||||
from picard.plugin import PluginManager
|
||||
from picard.acoustidmanager import AcoustIDManager
|
||||
from picard.config_upgrade import upgrade_config
|
||||
from picard.util import (
|
||||
decode_filename,
|
||||
encode_filename,
|
||||
thread,
|
||||
mbid_validate,
|
||||
check_io_encoding,
|
||||
uniqify
|
||||
uniqify,
|
||||
is_hidden_path,
|
||||
)
|
||||
from picard.webservice import XmlWebService
|
||||
|
||||
@@ -127,10 +126,12 @@ class Tagger(QtGui.QApplication):
|
||||
|
||||
check_io_encoding()
|
||||
|
||||
self._upgrade_config()
|
||||
|
||||
# Must be before config upgrade because upgrade dialogs need to be
|
||||
# translated
|
||||
setup_gettext(localedir, config.setting["ui_language"], log.debug)
|
||||
|
||||
upgrade_config()
|
||||
|
||||
self.xmlws = XmlWebService()
|
||||
|
||||
load_user_collections()
|
||||
@@ -162,64 +163,6 @@ class Tagger(QtGui.QApplication):
|
||||
self.nats = None
|
||||
self.window = MainWindow()
|
||||
|
||||
def _upgrade_config(self):
|
||||
cfg = config._config
|
||||
|
||||
# In version 1.0, the file naming formats for single and various
|
||||
# artist releases were merged.
|
||||
def upgrade_to_v1_0():
|
||||
def remove_va_file_naming_format(merge=True):
|
||||
if merge:
|
||||
config.setting["file_naming_format"] = (
|
||||
"$if($eq(%compilation%,1),\n$noop(Various Artist "
|
||||
"albums)\n%s,\n$noop(Single Artist Albums)\n%s)" % (
|
||||
config.setting["va_file_naming_format"].toString(),
|
||||
config.setting["file_naming_format"]
|
||||
))
|
||||
config.setting.remove("va_file_naming_format")
|
||||
config.setting.remove("use_va_format")
|
||||
|
||||
if ("va_file_naming_format" in config.setting and
|
||||
"use_va_format" in config.setting):
|
||||
|
||||
if config.setting["use_va_format"].toBool():
|
||||
remove_va_file_naming_format()
|
||||
self.window.show_va_removal_notice()
|
||||
|
||||
elif (config.setting["va_file_naming_format"].toString() !=
|
||||
r"$if2(%albumartist%,%artist%)/%album%/$if($gt(%totaldis"
|
||||
"cs%,1),%discnumber%-,)$num(%tracknumber%,2) %artist% - "
|
||||
"%title%"):
|
||||
|
||||
if self.window.confirm_va_removal():
|
||||
remove_va_file_naming_format(merge=False)
|
||||
else:
|
||||
remove_va_file_naming_format()
|
||||
else:
|
||||
# default format, disabled
|
||||
remove_va_file_naming_format(merge=False)
|
||||
|
||||
def upgrade_to_v1_3():
|
||||
_s = config.setting
|
||||
# the setting `windows_compatible_filenames` has been renamed
|
||||
# to `windows_compatibility`
|
||||
if "windows_compatible_filenames" in _s:
|
||||
_s["windows_compatibility"] = _s["windows_compatible_filenames"]
|
||||
_s.remove("windows_compatible_filenames")
|
||||
log.debug("Config upgrade: windows_compatible_filenames "
|
||||
"renamed windows_compatibility")
|
||||
|
||||
# preserved_tags spaces to comma separator, PICARD-536
|
||||
if "preserved_tags" in _s:
|
||||
_s["preserved_tags"] = re.sub(r"\s+", ",", _s["preserved_tags"].strip())
|
||||
log.debug("Config upgrade: convert preserved_tags separator "
|
||||
"from spaces to comma")
|
||||
|
||||
cfg.register_upgrade_hook("1.0.0final0", upgrade_to_v1_0)
|
||||
cfg.register_upgrade_hook("1.3.0dev2", upgrade_to_v1_3)
|
||||
|
||||
cfg.run_upgrade_hooks()
|
||||
|
||||
def move_files_to_album(self, files, albumid=None, album=None):
|
||||
"""Move `files` to tracks on album `albumid`."""
|
||||
if album is None:
|
||||
@@ -329,9 +272,20 @@ class Tagger(QtGui.QApplication):
|
||||
def add_files(self, filenames, target=None):
|
||||
"""Add files to the tagger."""
|
||||
log.debug("Adding files %r", filenames)
|
||||
ignoreregex = None
|
||||
pattern = config.setting['ignore_regex']
|
||||
if pattern:
|
||||
ignoreregex = re.compile(pattern)
|
||||
ignore_hidden = not config.persist["show_hidden_files"]
|
||||
new_files = []
|
||||
for filename in filenames:
|
||||
filename = os.path.normpath(os.path.realpath(filename))
|
||||
if ignore_hidden and is_hidden_path(filename):
|
||||
log.debug("File ignored (hidden): %s" % (filename))
|
||||
continue
|
||||
if ignoreregex is not None and ignoreregex.search(filename):
|
||||
log.info("File ignored (matching %s): %s" % (pattern, filename))
|
||||
continue
|
||||
if filename not in self.files:
|
||||
file = open_file(filename)
|
||||
if file:
|
||||
|
||||
@@ -50,7 +50,8 @@ class CDLookupDialog(QtGui.QDialog):
|
||||
item.setData(0, QtCore.Qt.UserRole, QtCore.QVariant(release.id))
|
||||
self.ui.release_list.setCurrentItem(self.ui.release_list.topLevelItem(0))
|
||||
self.ui.ok_button.setEnabled(True)
|
||||
[self.ui.release_list.resizeColumnToContents(i) for i in range(self.ui.release_list.columnCount() - 1)]
|
||||
for i in range(self.ui.release_list.columnCount() - 1):
|
||||
self.ui.release_list.resizeColumnToContents(i)
|
||||
# Sort by descending date, then ascending country
|
||||
self.ui.release_list.sortByColumn(3, QtCore.Qt.AscendingOrder)
|
||||
self.ui.release_list.sortByColumn(2, QtCore.Qt.DescendingOrder)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# -*- coding: UTF-8 -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Picard, the next-generation MusicBrainz tagger
|
||||
# Copyright (C) 2006-2007 Lukáš Lalinský
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
|
||||
from PyQt4 import QtCore, QtGui
|
||||
from PyQt4.QtGui import QIcon
|
||||
from picard import config
|
||||
from picard.util import icontheme
|
||||
from picard.ui.ui_infostatus import Ui_InfoStatus
|
||||
|
||||
|
||||
@@ -308,7 +308,9 @@ class BaseTreeView(QtGui.QTreeWidget):
|
||||
action.setChecked(True)
|
||||
action.triggered.connect(partial(obj.switch_release_version, version["id"]))
|
||||
|
||||
_add_other_versions() if obj.release_group.loaded else \
|
||||
if obj.release_group.loaded:
|
||||
_add_other_versions()
|
||||
else:
|
||||
obj.release_group.load_versions(_add_other_versions)
|
||||
releases_menu.setEnabled(True)
|
||||
else:
|
||||
|
||||
@@ -22,7 +22,6 @@ from PyQt4 import QtCore, QtGui
|
||||
import sys
|
||||
import os.path
|
||||
|
||||
from functools import partial
|
||||
from picard import config, log
|
||||
from picard.file import File
|
||||
from picard.track import Track
|
||||
@@ -669,23 +668,6 @@ class MainWindow(QtGui.QMainWindow):
|
||||
from picard.ui.logview import HistoryView
|
||||
HistoryView(self).show()
|
||||
|
||||
def confirm_va_removal(self):
|
||||
return QtGui.QMessageBox.question(self,
|
||||
_("Various Artists file naming scheme removal"),
|
||||
_("""The separate file naming scheme for various artists albums has been
|
||||
removed in this version of Picard. You currently do not use the this option,
|
||||
but have a separate file naming scheme defined. Do you want to remove it or
|
||||
merge it with your file naming scheme for single artist albums?"""),
|
||||
_("Merge"), _("Remove"))
|
||||
|
||||
def show_va_removal_notice(self):
|
||||
QtGui.QMessageBox.information(self,
|
||||
_("Various Artists file naming scheme removal"),
|
||||
_("""The separate file naming scheme for various artists albums has been
|
||||
removed in this version of Picard. Your file naming scheme has automatically
|
||||
been merged with that of single artist albums."""),
|
||||
QtGui.QMessageBox.Ok)
|
||||
|
||||
def open_bug_report(self):
|
||||
webbrowser2.goto('troubleshooting')
|
||||
|
||||
|
||||
@@ -290,7 +290,9 @@ class MetadataBox(QtGui.QTableWidget):
|
||||
self.set_tag_values(tag, [""])
|
||||
|
||||
def remove_selected_tags(self):
|
||||
(self.remove_tag(tag) for tag in self.selected_tags() if self.tag_is_removable(tag))
|
||||
for tag in self.selected_tags():
|
||||
if self.tag_is_removable(tag):
|
||||
self.remove_tag(tag)
|
||||
|
||||
def tag_is_removable(self, tag):
|
||||
return self.tag_diff.status[tag] & TagStatus.NotRemovable == 0
|
||||
|
||||
@@ -33,6 +33,7 @@ class OptionsPage(QtGui.QWidget):
|
||||
PARENT = None
|
||||
SORT_ORDER = 1000
|
||||
ACTIVE = True
|
||||
STYLESHEET_ERROR = "QWidget { background-color: #f55; color: white; font-weight:bold }"
|
||||
|
||||
def info(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Picard, the next-generation MusicBrainz tagger
|
||||
# Copyright (C) 2006 Lukáš Lalinský
|
||||
# Copyright (C) 2006-2014 Lukáš Lalinský
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
@@ -75,7 +75,7 @@ Discid %(discid-version)s
|
||||
Thank you for using Picard. Picard relies on the MusicBrainz database, which is operated by the MetaBrainz Foundation with the help of thousands of volunteers. If you like this application please consider donating to the MetaBrainz Foundation to keep the service running.</p>
|
||||
<p align="center"><a href="%(picard-donate-url)s">Donate now!</a></p>
|
||||
<p align="center"><strong>Credits</strong><br/>
|
||||
<small>Copyright © 2004-2011 Robert Kaye, Lukáš Lalinský and others%(translator-credits)s</small></p>
|
||||
<small>Copyright © 2004-2014 Robert Kaye, Lukáš Lalinský and others%(translator-credits)s</small></p>
|
||||
<p align="center"><a href="%(picard-doc-url)s">%(picard-doc-url)s</a></p>
|
||||
""") % args
|
||||
self.ui.label.setOpenExternalLinks(True)
|
||||
|
||||
@@ -17,7 +17,12 @@
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
from picard.ui.options import OptionsPage, register_options_page
|
||||
from PyQt4.QtGui import QPalette, QColor
|
||||
import re
|
||||
|
||||
from picard import config
|
||||
from picard.ui.options import OptionsPage, OptionsCheckError, register_options_page
|
||||
from picard.ui.ui_options_advanced import Ui_AdvancedOptionsPage
|
||||
|
||||
|
||||
class AdvancedOptionsPage(OptionsPage):
|
||||
@@ -26,7 +31,39 @@ class AdvancedOptionsPage(OptionsPage):
|
||||
TITLE = N_("Advanced")
|
||||
PARENT = None
|
||||
SORT_ORDER = 90
|
||||
ACTIVE = False
|
||||
ACTIVE = True
|
||||
|
||||
options = [
|
||||
config.TextOption("setting", "ignore_regex", ""),
|
||||
]
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(AdvancedOptionsPage, self).__init__(parent)
|
||||
self.ui = Ui_AdvancedOptionsPage()
|
||||
self.ui.setupUi(self)
|
||||
self.ui.ignore_regex.textChanged.connect(self.live_checker)
|
||||
|
||||
def load(self):
|
||||
self.ui.ignore_regex.setText(config.setting["ignore_regex"])
|
||||
|
||||
def save(self):
|
||||
config.setting["ignore_regex"] = unicode(self.ui.ignore_regex.text())
|
||||
|
||||
def live_checker(self, text):
|
||||
self.ui.regex_error.setStyleSheet("")
|
||||
self.ui.regex_error.setText("")
|
||||
try:
|
||||
self.check()
|
||||
except OptionsCheckError as e:
|
||||
self.ui.regex_error.setStyleSheet(self.STYLESHEET_ERROR)
|
||||
self.ui.regex_error.setText(e.info)
|
||||
return
|
||||
|
||||
def check(self):
|
||||
try:
|
||||
re.compile(unicode(self.ui.ignore_regex.text()))
|
||||
except re.error as e:
|
||||
raise OptionsCheckError(_("Regex Error"), str(e))
|
||||
|
||||
|
||||
register_options_page(AdvancedOptionsPage)
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
|
||||
from PyQt4 import QtCore, QtGui
|
||||
from picard import config
|
||||
from picard.plugin import ExtensionPoint
|
||||
from picard.util import webbrowser2
|
||||
from picard.ui.util import StandardButton
|
||||
from picard.ui.options import (
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
import os
|
||||
from PyQt4 import QtCore, QtGui
|
||||
from PyQt4 import QtGui
|
||||
from picard import config
|
||||
from picard.util import webbrowser2, find_executable
|
||||
from picard.const import FPCALC_NAMES
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
from PyQt4 import QtCore
|
||||
from picard import config
|
||||
from picard.ui.options import OptionsPage, register_options_page
|
||||
from picard.ui.ui_options_metadata import Ui_MetadataOptionsPage
|
||||
|
||||
@@ -270,8 +270,6 @@ class RenamingOptionsPage(OptionsPage):
|
||||
file.metadata['musicbrainz_releasetrackid'] = 'eac99807-93d4-3668-9714-fa0c1b487ccf'
|
||||
return file
|
||||
|
||||
STYLESHEET_ERROR = "QWidget { background-color: #f55; color: white; font-weight:bold }"
|
||||
|
||||
def move_files_to_browse(self):
|
||||
path = QtGui.QFileDialog.getExistingDirectory(self, "", self.ui.move_files_to.text())
|
||||
if path:
|
||||
|
||||
@@ -70,8 +70,6 @@ class ScriptingOptionsPage(OptionsPage):
|
||||
config.TextOption("setting", "tagger_script", ""),
|
||||
]
|
||||
|
||||
STYLESHEET_ERROR = "QWidget { background-color: #f55; color: white; font-weight:bold }"
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(ScriptingOptionsPage, self).__init__(parent)
|
||||
self.ui = Ui_ScriptingOptionsPage()
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
from PyQt4 import QtCore, QtGui
|
||||
from PyQt4 import QtGui
|
||||
from picard import config
|
||||
from picard.ui.ui_passworddialog import Ui_PasswordDialog
|
||||
from picard.util import rot13
|
||||
|
||||
@@ -66,6 +66,6 @@ class Ui_Dialog(object):
|
||||
Dialog.setWindowTitle(_("CD Lookup"))
|
||||
self.label.setText(_("The following releases on MusicBrainz match the CD:"))
|
||||
self.ok_button.setText(_("OK"))
|
||||
self.lookup_button.setText(_(" Lookup manually "))
|
||||
self.lookup_button.setText(_("Lookup manually"))
|
||||
self.cancel_button.setText(_("Cancel"))
|
||||
|
||||
|
||||
48
picard/ui/ui_options_advanced.py
Normal file
48
picard/ui/ui_options_advanced.py
Normal file
@@ -0,0 +1,48 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Form implementation generated from reading ui file 'ui/options_advanced.ui'
|
||||
#
|
||||
# Created: Wed Dec 25 02:35:20 2013
|
||||
# by: PyQt4 UI code generator 4.9.3
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
from PyQt4 import QtCore, QtGui
|
||||
|
||||
try:
|
||||
_fromUtf8 = QtCore.QString.fromUtf8
|
||||
except AttributeError:
|
||||
_fromUtf8 = lambda s: s
|
||||
|
||||
class Ui_AdvancedOptionsPage(object):
|
||||
def setupUi(self, AdvancedOptionsPage):
|
||||
AdvancedOptionsPage.setObjectName(_fromUtf8("AdvancedOptionsPage"))
|
||||
AdvancedOptionsPage.resize(338, 435)
|
||||
self.vboxlayout = QtGui.QVBoxLayout(AdvancedOptionsPage)
|
||||
self.vboxlayout.setObjectName(_fromUtf8("vboxlayout"))
|
||||
self.groupBox = QtGui.QGroupBox(AdvancedOptionsPage)
|
||||
self.groupBox.setObjectName(_fromUtf8("groupBox"))
|
||||
self.gridlayout = QtGui.QGridLayout(self.groupBox)
|
||||
self.gridlayout.setSpacing(2)
|
||||
self.gridlayout.setObjectName(_fromUtf8("gridlayout"))
|
||||
self.label_ignore_regex = QtGui.QLabel(self.groupBox)
|
||||
self.label_ignore_regex.setObjectName(_fromUtf8("label_ignore_regex"))
|
||||
self.gridlayout.addWidget(self.label_ignore_regex, 1, 0, 1, 1)
|
||||
self.ignore_regex = QtGui.QLineEdit(self.groupBox)
|
||||
self.ignore_regex.setObjectName(_fromUtf8("ignore_regex"))
|
||||
self.gridlayout.addWidget(self.ignore_regex, 2, 0, 1, 1)
|
||||
self.regex_error = QtGui.QLabel(self.groupBox)
|
||||
self.regex_error.setText(_fromUtf8(""))
|
||||
self.regex_error.setObjectName(_fromUtf8("regex_error"))
|
||||
self.gridlayout.addWidget(self.regex_error, 3, 0, 1, 1)
|
||||
self.vboxlayout.addWidget(self.groupBox)
|
||||
spacerItem = QtGui.QSpacerItem(181, 21, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
|
||||
self.vboxlayout.addItem(spacerItem)
|
||||
|
||||
self.retranslateUi(AdvancedOptionsPage)
|
||||
QtCore.QMetaObject.connectSlotsByName(AdvancedOptionsPage)
|
||||
|
||||
def retranslateUi(self, AdvancedOptionsPage):
|
||||
self.groupBox.setTitle(_("Advanced options"))
|
||||
self.label_ignore_regex.setText(_("Ignore file paths matching the following regular expression:"))
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
import sys
|
||||
from PyQt4 import QtGui
|
||||
from PyQt4 import QtCore, QtGui
|
||||
from picard import config
|
||||
from picard.util import find_existing_path
|
||||
|
||||
|
||||
@@ -365,3 +365,9 @@ if sys.platform == 'win32':
|
||||
return ap1 == ap2
|
||||
else:
|
||||
os_path_samefile = os.path.samefile
|
||||
|
||||
|
||||
def is_hidden_path(path):
|
||||
"""Returns true if at least one element of the path starts with a dot"""
|
||||
path = os.path.normpath(path) # we need to ignore /./ and /a/../ cases
|
||||
return any(s.startswith('.') for s in path.split(os.sep))
|
||||
|
||||
@@ -18,8 +18,6 @@
|
||||
# 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
|
||||
@@ -46,26 +44,26 @@ _BYTES_STRINGS_I18N = (
|
||||
)
|
||||
|
||||
|
||||
def decimal(number, prec=1):
|
||||
def decimal(number, scale=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)
|
||||
return short_string(int(number), 1000, scale)
|
||||
|
||||
|
||||
def binary(number, prec=1):
|
||||
def binary(number, scale=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)
|
||||
return short_string(int(number), 1024, scale)
|
||||
|
||||
|
||||
def short_string(number, multiple, prec=1):
|
||||
def short_string(number, multiple, scale=1):
|
||||
"""
|
||||
Returns short human-readable string for `number` bytes
|
||||
>>> [short_string(n, 1024, 2) for n in [1000, 1100, 15500]]
|
||||
@@ -75,12 +73,12 @@ def short_string(number, multiple, prec=1):
|
||||
"""
|
||||
num, unit = calc_unit(number, multiple)
|
||||
n = int(num)
|
||||
nr = round(num, prec)
|
||||
nr = round(num, scale)
|
||||
if n == nr or unit == 'B':
|
||||
fmt = '%d'
|
||||
num = n
|
||||
else:
|
||||
fmt = '%%0.%df' % prec
|
||||
fmt = '%%0.%df' % scale
|
||||
num = nr
|
||||
fmtnum = locale.format(fmt, num)
|
||||
return _("%s " + unit) % fmtnum
|
||||
@@ -118,8 +116,3 @@ def calc_unit(number, multiple=1000):
|
||||
return (sign * n, suffix)
|
||||
else:
|
||||
n /= multiple
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import doctest
|
||||
doctest.testmod()
|
||||
|
||||
@@ -79,10 +79,11 @@ elif sys.platform == 'linux2' and QFile.exists(LINUX_CDROM_INFO):
|
||||
elif key == 'Can play audio':
|
||||
drive_audio_caps = [v == '1' for v in
|
||||
QString(values).trimmed().split(QRegExp("\\s+"), QString.SkipEmptyParts)]
|
||||
break # no need to continue passed this line
|
||||
line = cdinfo.readLine()
|
||||
# Show only drives that are capable of playing audio
|
||||
for drive in drive_names:
|
||||
if drive_audio_caps[drive_names.indexOf(drive)]:
|
||||
for index, drive in enumerate(drive_names):
|
||||
if drive_audio_caps[index]:
|
||||
device = u'/dev/%s' % drive
|
||||
symlink_target = QFile.symLinkTarget(device)
|
||||
if symlink_target != '':
|
||||
|
||||
@@ -311,7 +311,7 @@ def make_short_filename(basedir, relpath, win_compat=False, relative_to=""):
|
||||
reserved = len(basedir)
|
||||
if not basedir.endswith(os.path.sep):
|
||||
reserved += 1
|
||||
return os.path.join(basedir, _make_win_short_filename(relpath, reserved))
|
||||
return _make_win_short_filename(relpath, reserved)
|
||||
# if we're being windows compatible, figure out how much
|
||||
# needs to be reserved for the basedir part
|
||||
if win_compat:
|
||||
@@ -337,4 +337,4 @@ def make_short_filename(basedir, relpath, win_compat=False, relative_to=""):
|
||||
# and filesystem-dependent
|
||||
limit = _get_filename_limit(basedir)
|
||||
relpath = shorten_path(relpath, limit, mode=SHORTEN_BYTES)
|
||||
return os.path.join(basedir, relpath)
|
||||
return relpath
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
import sys
|
||||
import traceback
|
||||
from PyQt4.QtCore import QThreadPool, QRunnable, QCoreApplication, QEvent
|
||||
from PyQt4.QtCore import QRunnable, QCoreApplication, QEvent
|
||||
|
||||
|
||||
class ProxyToMainEvent(QEvent):
|
||||
|
||||
@@ -28,7 +28,6 @@ import re
|
||||
import time
|
||||
import os.path
|
||||
import platform
|
||||
import urllib
|
||||
from collections import deque, defaultdict
|
||||
from functools import partial
|
||||
from PyQt4 import QtCore, QtNetwork
|
||||
@@ -55,8 +54,9 @@ USER_AGENT_STRING = '%s-%s/%s (%s;%s-%s)' % (PICARD_ORG_NAME, PICARD_APP_NAME,
|
||||
platform.platform(),
|
||||
platform.python_implementation(),
|
||||
platform.python_version())
|
||||
CLIENT_STRING = urllib.quote('%s %s-%s' % (PICARD_ORG_NAME, PICARD_APP_NAME,
|
||||
PICARD_VERSION_STR))
|
||||
CLIENT_STRING = str(QUrl.toPercentEncoding('%s %s-%s' % (PICARD_ORG_NAME,
|
||||
PICARD_APP_NAME,
|
||||
PICARD_VERSION_STR)))
|
||||
|
||||
|
||||
def _escape_lucene_query(text):
|
||||
@@ -392,7 +392,7 @@ class XmlWebService(QtCore.QObject):
|
||||
query = []
|
||||
for name, value in kwargs.items():
|
||||
if name == 'limit':
|
||||
filters.append((name, value))
|
||||
filters.append((name, str(value)))
|
||||
else:
|
||||
value = _escape_lucene_query(value).strip().lower()
|
||||
if value:
|
||||
|
||||
975
po/picard.pot
975
po/picard.pot
File diff suppressed because it is too large
Load Diff
2
setup.py
2
setup.py
@@ -89,8 +89,6 @@ class picard_test(Command):
|
||||
self.verbosity = int(self.verbosity)
|
||||
|
||||
def run(self):
|
||||
import os.path
|
||||
import glob
|
||||
import unittest
|
||||
|
||||
names = []
|
||||
|
||||
@@ -30,8 +30,11 @@ class Testbytes2human(unittest.TestCase):
|
||||
|
||||
self.assertEqual(bytes2human.binary(45682), '44.6 KiB')
|
||||
self.assertEqual(bytes2human.binary(-45682), '-44.6 KiB')
|
||||
self.assertEqual(bytes2human.binary(-45682, 2), '-44.61 KiB')
|
||||
self.assertEqual(bytes2human.decimal(45682), '45.7 kB')
|
||||
self.assertEqual(bytes2human.decimal(45682, 2), '45.68 kB')
|
||||
self.assertEqual(bytes2human.decimal(9223372036854775807), '9223.4 PB')
|
||||
self.assertEqual(bytes2human.decimal(9223372036854775807, 3), '9223.372 PB')
|
||||
self.assertEqual(bytes2human.decimal(123.6), '123 B')
|
||||
self.assertRaises(ValueError, bytes2human.decimal, 'xxx')
|
||||
self.assertRaises(ValueError, bytes2human.decimal, '123.6')
|
||||
|
||||
@@ -17,7 +17,7 @@ class ShortFilenameTest(unittest.TestCase):
|
||||
def test_bmp_unicode_on_unicode_fs(self):
|
||||
char = u"\N{LATIN SMALL LETTER SHARP S}"
|
||||
fn = make_short_filename(self.root, os.path.join(*[char * 120] * 2))
|
||||
self.assertEqual(fn, os.path.join(self.root, *[char * 120] * 2))
|
||||
self.assertEqual(fn, os.path.join(*[char * 120] * 2))
|
||||
|
||||
@unittest.skipUnless(sys.platform not in ("win32", "darwin"), "non-windows, non-osx test")
|
||||
def test_bmp_unicode_on_nix(self):
|
||||
@@ -25,28 +25,28 @@ class ShortFilenameTest(unittest.TestCase):
|
||||
max_len = 255
|
||||
divisor = len(char.encode(sys.getfilesystemencoding()))
|
||||
fn = make_short_filename(self.root, os.path.join(*[char * 200] * 2))
|
||||
self.assertEqual(fn, os.path.join(self.root, *[char * (max_len // divisor)] * 2))
|
||||
self.assertEqual(fn, os.path.join(*[char * (max_len // divisor)] * 2))
|
||||
|
||||
@unittest.skipUnless(sys.platform == "darwin", "os x test")
|
||||
def test_precomposed_unicode_on_osx(self):
|
||||
char = u"\N{LATIN SMALL LETTER A WITH BREVE}"
|
||||
max_len = 255
|
||||
fn = make_short_filename(self.root, os.path.join(*[char * 200] * 2))
|
||||
self.assertEqual(fn, os.path.join(self.root, *[char * (max_len // 2)] * 2))
|
||||
self.assertEqual(fn, os.path.join(*[char * (max_len // 2)] * 2))
|
||||
|
||||
@unittest.skipUnless(sys.platform == "win32", "windows test")
|
||||
def test_nonbmp_unicode_on_windows(self):
|
||||
char = u"\N{MUSICAL SYMBOL G CLEF}"
|
||||
remaining = 259 - (3 + 10 + 1 + 200 + 1)
|
||||
fn = make_short_filename(self.root, os.path.join(*[char * 100] * 2))
|
||||
self.assertEqual(fn, os.path.join(self.root, char * 100, char * (remaining // 2)))
|
||||
self.assertEqual(fn, os.path.join(char * 100, char * (remaining // 2)))
|
||||
|
||||
@unittest.skipUnless(sys.platform == "darwin", "os x test")
|
||||
def test_nonbmp_unicode_on_osx(self):
|
||||
char = u"\N{MUSICAL SYMBOL G CLEF}"
|
||||
max_len = 255
|
||||
fn = make_short_filename(self.root, os.path.join(*[char * 200] * 2))
|
||||
self.assertEqual(fn, os.path.join(self.root, *[char * (max_len // 2)] * 2))
|
||||
self.assertEqual(fn, os.path.join(*[char * (max_len // 2)] * 2))
|
||||
|
||||
@unittest.skipUnless(sys.platform not in ("win32", "darwin"), "non-windows, non-osx test")
|
||||
def test_nonbmp_unicode_on_nix(self):
|
||||
@@ -54,7 +54,7 @@ class ShortFilenameTest(unittest.TestCase):
|
||||
max_len = 255
|
||||
divisor = len(char.encode(sys.getfilesystemencoding()))
|
||||
fn = make_short_filename(self.root, os.path.join(*[char * 100] * 2))
|
||||
self.assertEqual(fn, os.path.join(self.root, *[char * (max_len // divisor)] * 2))
|
||||
self.assertEqual(fn, os.path.join(*[char * (max_len // divisor)] * 2))
|
||||
|
||||
@unittest.skipUnless(sys.platform not in ("win32", "darwin"), "non-windows, non-osx test")
|
||||
def test_nonbmp_unicode_on_nix_with_windows_compat(self):
|
||||
@@ -63,45 +63,49 @@ class ShortFilenameTest(unittest.TestCase):
|
||||
remaining = 259 - (3 + 10 + 1 + 200 + 1)
|
||||
divisor = len(char.encode(sys.getfilesystemencoding()))
|
||||
fn = make_short_filename(self.root, os.path.join(*[char * 100] * 2), win_compat=True)
|
||||
self.assertEqual(fn, os.path.join(self.root, char * (max_len // divisor), char * (remaining // 2)))
|
||||
self.assertEqual(fn, os.path.join(char * (max_len // divisor), char * (remaining // 2)))
|
||||
|
||||
def test_windows_shortening(self):
|
||||
fn = make_short_filename(self.root, os.path.join("a" * 200, "b" * 200, "c" * 200 + ".ext"), win_compat=True)
|
||||
self.assertEqual(fn, os.path.join(self.root, "a" * 116, "b" * 116, "c" * 7 + ".ext"))
|
||||
self.assertEqual(fn, os.path.join("a" * 116, "b" * 116, "c" * 7 + ".ext"))
|
||||
|
||||
@unittest.skipUnless(sys.platform != "win32", "non-windows test")
|
||||
def test_windows_shortening_with_ancestor_on_nix(self):
|
||||
root = os.path.join(self.root, "w" * 10, "x" * 10, "y" * 9, "z" * 9)
|
||||
fn = make_short_filename(
|
||||
os.path.join(self.root, "w" * 10, "x" * 10, "y" * 9, "z" * 9), os.path.join("b" * 200, "c" * 200, "d" * 200 + ".ext"),
|
||||
root, os.path.join("b" * 200, "c" * 200, "d" * 200 + ".ext"),
|
||||
win_compat=True, relative_to = self.root)
|
||||
self.assertEqual(fn, os.path.join(self.root, "w" * 10, "x" * 10, "y" * 9, "z" * 9, "b" * 100, "c" * 100, "d" * 7 + ".ext"))
|
||||
self.assertEqual(fn, os.path.join("b" * 100, "c" * 100, "d" * 7 + ".ext"))
|
||||
|
||||
def test_windows_node_maxlength_shortening(self):
|
||||
max_len = 226
|
||||
remaining = 259 - (3 + 10 + 1 + max_len + 1)
|
||||
fn = make_short_filename(self.root, os.path.join("a" * 300, "b" * 100 + ".ext"), win_compat=True)
|
||||
self.assertEqual(fn, os.path.join(self.root, "a" * max_len, "b" * (remaining - 4) + ".ext"))
|
||||
self.assertEqual(fn, os.path.join("a" * max_len, "b" * (remaining - 4) + ".ext"))
|
||||
|
||||
def test_windows_selective_shortening(self):
|
||||
root = self.root + "x" * (44 - 10 - 3)
|
||||
fn = make_short_filename(root, os.path.join(
|
||||
os.path.join(*["a" * 9] * 10 + ["b" * 15] * 10), "c" * 10), win_compat=True)
|
||||
self.assertEqual(fn, os.path.join(root, os.path.join(*["a" * 9] * 10 + ["b" * 9] * 10), "c" * 10))
|
||||
self.assertEqual(fn, os.path.join(os.path.join(*["a" * 9] * 10 + ["b" * 9] * 10), "c" * 10))
|
||||
|
||||
def test_windows_shortening_not_needed(self):
|
||||
fn = make_short_filename(self.root + "x" * 33, os.path.join(
|
||||
root = self.root + "x" * 33
|
||||
fn = make_short_filename(root, os.path.join(
|
||||
os.path.join(*["a" * 9] * 20), "b" * 10), win_compat=True)
|
||||
self.assertEqual(fn, os.path.join(self.root + "x" * 33, os.path.join(*["a" * 9] * 20), "b" * 10))
|
||||
self.assertEqual(fn, os.path.join(os.path.join(*["a" * 9] * 20), "b" * 10))
|
||||
|
||||
def test_windows_path_too_long(self):
|
||||
root = self.root + "x" * 230
|
||||
self.assertRaises(IOError, make_short_filename,
|
||||
self.root + "x" * 230, os.path.join("a", "b", "c", "d"), win_compat=True)
|
||||
root, os.path.join("a", "b", "c", "d"), win_compat=True)
|
||||
|
||||
def test_windows_path_not_too_long(self):
|
||||
fn = make_short_filename(self.root + "x" * 230, os.path.join("a", "b", "c"), win_compat=True)
|
||||
self.assertEqual(fn, os.path.join(self.root + "x" * 230, "a", "b", "c"))
|
||||
root = self.root + "x" * 230
|
||||
fn = make_short_filename(root, os.path.join("a", "b", "c"), win_compat=True)
|
||||
self.assertEqual(fn, os.path.join("a", "b", "c"))
|
||||
|
||||
def test_whitespace(self):
|
||||
fn = make_short_filename(self.root, os.path.join("a1234567890 ", " b1234567890 "))
|
||||
self.assertEqual(fn, os.path.join(self.root, "a1234567890", "b1234567890"))
|
||||
self.assertEqual(fn, os.path.join("a1234567890", "b1234567890"))
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os.path
|
||||
import unittest
|
||||
from picard import util
|
||||
|
||||
@@ -119,3 +120,18 @@ class SaveReleaseTypeScoresTest(unittest.TestCase):
|
||||
self.assertTrue("Single 0.50" in saved_scores)
|
||||
self.assertTrue("Other 0.00" in saved_scores)
|
||||
self.assertEqual(6, len(saved_scores.split()))
|
||||
|
||||
|
||||
class HiddenPathTest(unittest.TestCase):
|
||||
|
||||
def test(self):
|
||||
self.assertEqual(util.is_hidden_path('/a/.b/c.mp3'), True)
|
||||
self.assertEqual(util.is_hidden_path('/a/b/c.mp3'), False)
|
||||
self.assertEqual(util.is_hidden_path('/a/.b/.c.mp3'), True)
|
||||
self.assertEqual(util.is_hidden_path('/a/b/.c.mp3'), True)
|
||||
self.assertEqual(util.is_hidden_path('c.mp3'), False)
|
||||
self.assertEqual(util.is_hidden_path('.c.mp3'), True)
|
||||
self.assertEqual(util.is_hidden_path('/a/./c.mp3'), False)
|
||||
self.assertEqual(util.is_hidden_path('/a/./.c.mp3'), True)
|
||||
self.assertEqual(util.is_hidden_path('/a/../c.mp3'), False)
|
||||
self.assertEqual(util.is_hidden_path('/a/../.c.mp3'), True)
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import unittest
|
||||
from picard import version_to_string, version_from_string
|
||||
from picard import (version_to_string,
|
||||
version_from_string,
|
||||
VersionError)
|
||||
|
||||
|
||||
class VersionsTest(unittest.TestCase):
|
||||
@@ -23,8 +25,8 @@ class VersionsTest(unittest.TestCase):
|
||||
|
||||
def test_version_conv_4(self):
|
||||
l, s = (1, 0, 2, '', 0), '1.0.2'
|
||||
self.assertRaises(AssertionError, version_to_string, (l))
|
||||
self.assertRaises(AttributeError, version_from_string, (s))
|
||||
self.assertRaises(VersionError, version_to_string, (l))
|
||||
self.assertRaises(VersionError, version_from_string, (s))
|
||||
|
||||
def test_version_conv_5(self):
|
||||
l, s = (999, 999, 999, 'dev', 999), '999.999.999dev999'
|
||||
@@ -32,9 +34,8 @@ class VersionsTest(unittest.TestCase):
|
||||
self.assertEqual(l, version_from_string(s))
|
||||
|
||||
def test_version_conv_6(self):
|
||||
self.assertRaises(TypeError, version_to_string, ('1', 0, 2, 'final', 0))
|
||||
self.assertRaises(AssertionError, version_to_string, (1, 0))
|
||||
self.assertRaises(TypeError, version_from_string, 1)
|
||||
l = (1, 0, 2, 'xx', 0)
|
||||
self.assertRaises(VersionError, version_to_string, (l))
|
||||
|
||||
def test_version_conv_7(self):
|
||||
l, s = (1, 1, 0, 'final', 0), '1.1'
|
||||
@@ -51,3 +52,19 @@ class VersionsTest(unittest.TestCase):
|
||||
def test_version_conv_10(self):
|
||||
l, s = (1, 1, 0, 'dev', 0), '1.1.0dev0'
|
||||
self.assertEqual(version_to_string(l, short=True), s)
|
||||
|
||||
def test_version_conv_11(self):
|
||||
l, s = ('1', '1', '0', 'dev', '0'), '1.1.0dev0'
|
||||
self.assertEqual(version_to_string(l), s)
|
||||
|
||||
def test_version_conv_12(self):
|
||||
l, s = (1, 1, 0, 'dev', 0), '1_1_0_dev_0'
|
||||
self.assertEqual(l, version_from_string(s))
|
||||
|
||||
def test_version_conv_13(self):
|
||||
l, s = (1, 1, 0, 'dev', 0), 'anything_28_1_1_0_dev_0'
|
||||
self.assertEqual(l, version_from_string(s))
|
||||
|
||||
def test_version_conv_14(self):
|
||||
l = 'anything_28x_1_0_dev_0'
|
||||
self.assertRaises(VersionError, version_to_string, (l))
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
<item>
|
||||
<widget class="QPushButton" name="lookup_button">
|
||||
<property name="text">
|
||||
<string> Lookup manually </string>
|
||||
<string>Lookup manually</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
|
||||
63
ui/options_advanced.ui
Normal file
63
ui/options_advanced.ui
Normal file
@@ -0,0 +1,63 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>AdvancedOptionsPage</class>
|
||||
<widget class="QWidget" name="AdvancedOptionsPage">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>338</width>
|
||||
<height>435</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QVBoxLayout">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="title">
|
||||
<string>Advanced options</string>
|
||||
</property>
|
||||
<layout class="QGridLayout">
|
||||
<property name="spacing">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_ignore_regex">
|
||||
<property name="text">
|
||||
<string>Ignore file paths matching the following regular expression:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLineEdit" name="ignore_regex"/>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="regex_error" >
|
||||
<property name="text" >
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>181</width>
|
||||
<height>21</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<tabstops>
|
||||
<tabstop>ignore_regex</tabstop>
|
||||
</tabstops>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
Reference in New Issue
Block a user