diff --git a/picard/mbxml.py b/picard/mbxml.py
index 31910e1a5..9e43171db 100644
--- a/picard/mbxml.py
+++ b/picard/mbxml.py
@@ -246,7 +246,7 @@ def media_formats_from_node(node):
def track_to_metadata(node, track):
m = track.metadata
- recording_to_metadata(node.recording[0], track)
+ recording_to_metadata(node.recording[0], m, track)
m.add_unique('musicbrainz_trackid', node.id)
# overwrite with data we have on the track
for name, nodes in node.children.iteritems():
@@ -265,8 +265,7 @@ def track_to_metadata(node, track):
m['~length'] = format_time(m.length)
-def recording_to_metadata(node, track):
- m = track.metadata
+def recording_to_metadata(node, m, track=None):
m.length = 0
m.add_unique('musicbrainz_recordingid', node.id)
for name, nodes in node.children.iteritems():
@@ -281,7 +280,7 @@ def recording_to_metadata(node, track):
m['~recordingcomment'] = nodes[0].text
elif name == 'artist_credit':
artist_credit_to_metadata(nodes[0], m)
- if 'name_credit' in nodes[0].children:
+ if 'name_credit' in nodes[0].children and track:
for name_credit in nodes[0].name_credit:
if 'artist' in name_credit.children:
for artist in name_credit.artist:
@@ -375,7 +374,7 @@ def release_to_metadata(node, m, album=None):
m['barcode'] = nodes[0].text
elif name == 'relation_list':
_relations_to_metadata(nodes, m)
- elif name == 'label_info_list' and nodes[0].count != '0':
+ elif name == 'label_info_list' and getattr(nodes[0], 'count', '0') != '0':
m['label'], m['catalognumber'] = label_info_from_node(nodes[0])
elif name == 'text_representation':
if 'language' in nodes[0].children:
diff --git a/picard/resources.py b/picard/resources.py
index 2c30635c9..cb58e55e2 100644
--- a/picard/resources.py
+++ b/picard/resources.py
@@ -1262,6 +1262,51 @@ qt_resource_data = "\
\xb3\xa9\xe4\x06\xaa\xfb\x62\x36\x86\x02\x46\x8a\x63\x13\x00\x29\
\x51\x09\x03\x00\x20\x62\x2f\x00\x00\x00\x00\x49\x45\x4e\x44\xae\
\x42\x60\x82\
+\x00\x00\x02\xa1\
+\x47\
+\x49\x46\x38\x39\x61\x10\x00\x10\x00\xf2\x00\x00\xff\xff\xff\x00\
+\x00\x00\xc2\xc2\xc2\x42\x42\x42\x00\x00\x00\x62\x62\x62\x82\x82\
+\x82\x92\x92\x92\x21\xff\x0b\x4e\x45\x54\x53\x43\x41\x50\x45\x32\
+\x2e\x30\x03\x01\x00\x00\x00\x21\xfe\x1a\x43\x72\x65\x61\x74\x65\
+\x64\x20\x77\x69\x74\x68\x20\x61\x6a\x61\x78\x6c\x6f\x61\x64\x2e\
+\x69\x6e\x66\x6f\x00\x21\xf9\x04\x09\x0a\x00\x00\x00\x2c\x00\x00\
+\x00\x00\x10\x00\x10\x00\x00\x03\x33\x08\xba\xdc\xfe\x30\xca\x49\
+\x6b\x13\x63\x08\x3a\x08\x19\x9c\x07\x4e\x98\x66\x09\x45\xb1\x31\
+\xc2\xba\x14\x99\xc1\xb6\x2e\x60\xc4\xc2\x71\xd0\x2d\x5b\x18\x39\
+\xdd\xa6\x07\x39\x18\x0c\x07\x4a\x6b\xe7\x48\x00\x00\x21\xf9\x04\
+\x09\x0a\x00\x00\x00\x2c\x00\x00\x00\x00\x10\x00\x10\x00\x00\x03\
+\x34\x08\xba\xdc\xfe\x4e\x8c\x21\x20\x1b\x84\x0c\xbb\xb0\xe6\x8a\
+\x44\x71\x42\x51\x54\x60\x31\x19\x20\x60\x4c\x45\x5b\x1a\xa8\x7c\
+\x1c\xb5\x75\xdf\xed\x61\x18\x07\x80\x20\xd7\x18\xe2\x86\x43\x19\
+\xb2\x25\x24\x2a\x12\x00\x21\xf9\x04\x09\x0a\x00\x00\x00\x2c\x00\
+\x00\x00\x00\x10\x00\x10\x00\x00\x03\x36\x08\xba\x32\x23\x2b\xca\
+\x41\xc8\x90\xcc\x94\x56\x2f\x06\x85\x63\x1c\x0e\xf4\x19\x4e\xf1\
+\x49\x42\x61\x98\xab\x70\x1c\xf0\x0a\xcc\xb3\xbd\x1c\xc6\xa8\x2b\
+\x02\x59\xed\x17\xfc\x01\x83\xc3\x0f\x32\xa9\x64\x1a\x9f\xbf\x04\
+\x00\x21\xf9\x04\x09\x0a\x00\x00\x00\x2c\x00\x00\x00\x00\x10\x00\
+\x10\x00\x00\x03\x33\x08\xba\x62\x25\x2b\xca\x32\x86\x91\xec\x9c\
+\x56\x5f\x85\x8b\xa6\x09\x85\x21\x0c\x04\x31\x44\x87\x61\x1c\x11\
+\xaa\x46\x82\xb0\xd1\x1f\x03\x62\x52\x5d\xf3\x3d\x1f\x30\x38\x2c\
+\x1a\x8f\xc8\xa4\x72\x39\x4c\x00\x00\x21\xf9\x04\x09\x0a\x00\x00\
+\x00\x2c\x00\x00\x00\x00\x10\x00\x10\x00\x00\x03\x32\x08\xba\x72\
+\x27\x2b\x4a\xe7\x64\x14\xf0\x18\xf3\x4c\x81\x0c\x26\x76\xc3\x60\
+\x5c\x62\x54\x94\x85\x84\xb9\x1e\x68\x59\x42\x29\xcf\xca\x40\x10\
+\x03\x1e\xe9\x3c\x1f\xc3\x26\x2c\x1a\x8f\xc8\xa4\x52\x92\x00\x00\
+\x21\xf9\x04\x09\x0a\x00\x00\x00\x2c\x00\x00\x00\x00\x10\x00\x10\
+\x00\x00\x03\x33\x08\xba\x20\xc2\x90\x39\x17\xe3\x74\xe7\xbc\xda\
+\x9e\x30\x19\xc7\x1c\xe0\x21\x2e\x42\xb6\x9d\xca\x57\xac\xa2\x31\
+\x0c\x06\x0b\x14\x73\x61\xbb\xb0\x35\xf7\x95\x01\x81\x30\xb0\x09\
+\x89\xbb\x9f\x6d\x29\x4a\x00\x00\x21\xf9\x04\x09\x0a\x00\x00\x00\
+\x2c\x00\x00\x00\x00\x10\x00\x10\x00\x00\x03\x32\x08\xba\xdc\xfe\
+\xf0\x09\x11\xd9\x9c\x55\x5d\x9a\x01\xee\xda\x71\x70\x95\x60\x88\
+\xdd\x61\x9c\xdd\x34\x96\x85\x41\x46\xc5\x30\x14\x90\x60\x9b\xb6\
+\x01\x0d\x04\xc2\x40\x10\x9b\x31\x80\xc2\xd6\xce\x91\x00\x00\x21\
+\xf9\x04\x09\x0a\x00\x00\x00\x2c\x00\x00\x00\x00\x10\x00\x10\x00\
+\x00\x03\x32\x08\xba\xdc\xfe\x30\xca\x49\xab\x65\x42\xd4\x9c\x29\
+\xd7\x1e\x08\x08\xc3\x20\x8e\xc7\x71\x0e\x04\x31\x30\xa9\xca\xb0\
+\xae\x50\x18\xc2\x61\x18\x07\x56\xda\xa5\x02\x20\x75\x62\x18\x82\
+\x9e\x5b\x11\x90\x00\x00\x3b\x00\x00\x00\x00\x00\x00\x00\x00\x00\
+\
\x00\x00\x00\x83\
\x89\
\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
@@ -3419,31 +3464,75 @@ qt_resource_data = "\
\x8f\xc4\xad\x06\x0f\xc4\xcd\x1e\x8f\x8e\x7f\x01\xd7\x2b\x79\xd4\
\xea\x76\x04\x5f\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\
\
-\x00\x00\x01\x62\
+\x00\x00\x04\x2d\
\x89\
\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
-\x00\x00\x20\x00\x00\x00\x20\x08\x06\x00\x00\x00\x73\x7a\x7a\xf4\
-\x00\x00\x01\x29\x49\x44\x41\x54\x78\x01\x63\x18\xa2\xe0\x3f\x63\
-\xb9\x74\x6b\x5c\x99\x74\x5b\xfc\x9b\x62\xeb\xf8\x37\xa5\x36\x71\
-\xff\x19\x18\x18\xe9\x62\x75\x85\x64\xbb\x71\x99\x74\xeb\x11\xa0\
-\x03\xfe\x57\xc8\xb4\x96\xbf\x29\xb1\x29\x7f\x5d\x62\xfd\x1f\x88\
-\xcf\xbc\x2c\xb2\xb4\xa2\x99\xc5\x95\xd2\x6d\xc2\x15\x52\x6d\x13\
-\x81\x16\xff\x01\x59\x8e\xee\x00\x28\xfe\xf7\xba\xc4\x6a\xd1\xab\
-\x52\x07\x09\xaa\x59\x9c\x66\x3c\x93\xb5\x5c\xa6\x35\x1f\x68\xe1\
-\x07\x98\xc5\x38\x1d\x80\xc0\x5f\x5e\x97\xd8\x34\xdc\xce\xf5\x64\
-\xa7\xc8\x72\x60\x50\x3b\x97\x4b\xb5\x5d\x81\x59\x48\xc8\x01\x98\
-\xd8\xea\xd6\xeb\x52\x1b\x1f\xd2\xe3\x59\xa6\x5d\xa5\x5c\xba\x6d\
-\x15\xcc\x22\xb2\x1d\x80\xc0\xbb\xdf\x16\xdb\x6a\x11\xb4\xb8\x44\
-\xbc\x9b\x1b\x68\x68\x03\xd0\xf0\x1f\x50\x4b\x28\x77\x00\x02\xff\
-\x02\xaa\x9d\xf8\x36\xd7\x9c\x0f\xab\xe5\xa5\xd2\xad\x06\x40\x43\
-\x9f\xc1\x0c\xa7\xba\x03\x10\xf8\xd9\xab\x52\x2b\x03\xcc\x60\x97\
-\x6e\x09\x05\x19\x4a\x07\x07\xfc\x7f\x55\x6a\x13\x3a\xe0\x0e\x18\
-\x75\xc0\xa8\x03\x46\x1d\x30\xea\x80\x51\x07\x8c\x3a\x60\xc0\xab\
-\x63\x42\x0d\x92\xef\xf4\x69\x90\xd0\xbf\x49\x46\xff\x46\x29\x10\
-\x7b\x0f\x68\xb3\x7c\xc8\x77\x4c\x30\x73\x8b\x54\xab\x11\x8e\xae\
-\xd9\xe9\xb7\x25\x56\x96\x43\xbd\x73\x4a\x7f\x00\x00\x00\x0b\xb8\
-\x4b\xac\x5f\x46\xcf\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\
-\x82\
+\x00\x00\x20\x00\x00\x00\x20\x08\x03\x00\x00\x00\x44\xa4\x8a\xc6\
+\x00\x00\x02\x1c\x50\x4c\x54\x45\x00\x00\x00\x00\x00\xff\xff\x00\
+\x00\x80\x00\x80\xff\x80\x40\x71\x1c\x8e\xea\x6a\x40\x78\x1e\x87\
+\x71\x1c\x80\xe8\x74\x3a\x7b\x1c\x84\xec\x71\x39\xe7\x78\x38\x75\
+\x1b\x85\xec\x76\x39\xec\x74\x3c\x75\x1c\x86\xe9\x73\x39\x77\x1a\
+\x84\x77\x1b\x85\xec\x75\x3c\xea\x73\x3c\x78\x1c\x85\x77\x1b\x84\
+\xec\x75\x3a\x78\x1b\x85\xec\x74\x3c\xea\x74\x3a\x78\x1a\x86\x76\
+\x1b\x85\xeb\x75\x3a\x78\x1b\x84\xeb\x74\x3a\xb3\x80\xac\xf6\xbe\
+\x92\xeb\x73\x3a\xcb\xa8\xbb\xf8\xcc\xa1\x77\x1a\x85\x78\x1c\x85\
+\xeb\x73\x3c\x78\x1b\x85\xeb\x74\x3b\x77\x1c\x85\xeb\x74\x3c\x77\
+\x1b\x85\xeb\x74\x3b\x77\x1b\x85\xeb\x74\x3b\x77\x1b\x85\xeb\x74\
+\x3b\x77\x1b\x85\x77\x1b\x85\x78\x1b\x85\x79\x1e\x87\x79\x1e\x88\
+\x79\x1f\x86\x7a\x20\x87\x7a\x20\x89\x7d\x22\x8a\x7d\x23\x8c\x7e\
+\x24\x8c\x7f\x25\x8c\x80\x26\x8d\x84\x2c\x92\x85\x2c\x92\x88\x30\
+\x95\x88\x31\x96\x8a\x33\x97\x8c\x34\x98\x8c\x37\x9a\x8e\x38\x9c\
+\x8f\x39\x9b\x90\x3a\x9d\x94\x40\xa2\x97\x51\x9a\x99\x46\xa6\x9a\
+\x49\xa7\x9c\x49\xa9\x9f\x4e\xac\xa0\x4f\xac\xa5\x55\xb2\xa8\x58\
+\xb4\xac\x5d\xb8\xac\x5e\xb8\xad\x60\xba\xaf\x62\xbb\xb0\x64\xbc\
+\xb3\x66\xbe\xb4\x68\xc0\xb4\x6a\xc1\xb5\x69\xc1\xb5\x6a\xc2\xb6\
+\x6b\xc2\xb9\x82\xb3\xbb\x75\xc3\xc0\x80\xc6\xc2\x84\xc6\xc7\x8e\
+\xc8\xc8\xa2\xb9\xca\xa2\xbb\xcd\x99\xca\xcf\x9d\xcb\xd1\xb1\xbe\
+\xd5\xaa\xcd\xd8\xb1\xce\xda\xb4\xce\xda\xbd\xc5\xe1\xc8\xcb\xe4\
+\xd0\xca\xe6\xca\xd3\xea\xd3\xd3\xeb\x74\x3b\xeb\x75\x3d\xeb\x76\
+\x3d\xec\x78\x40\xec\x79\x42\xec\x7b\x43\xec\x7c\x44\xec\x7e\x46\
+\xec\x7e\x47\xed\x7f\x47\xed\x7f\x48\xed\x81\x4a\xed\x82\x4a\xed\
+\x82\x4b\xed\x84\x4d\xee\x87\x51\xee\x88\x52\xee\x8b\x55\xee\x8b\
+\x56\xf0\x94\x60\xf0\x97\x64\xf0\x98\x65\xf0\x9a\x66\xf1\x9c\x69\
+\xf1\x9e\x6b\xf1\xe6\xd4\xf2\xa1\x6f\xf2\xa2\x70\xf2\xa4\x73\xf2\
+\xa7\x75\xf2\xe3\xd7\xf2\xe9\xd3\xf3\xae\x7d\xf5\xb8\x88\xf5\xea\
+\xd8\xf6\xbc\x8f\xf6\xee\xd6\xf7\xc9\x9e\xf8\xcb\xa0\xf8\xcf\xa4\
+\xf8\xd0\xa6\xf9\xd6\xac\xf9\xd7\xae\xf9\xf4\xd8\xfb\xe0\xb9\xfb\
+\xe2\xba\xfb\xe2\xbb\xfb\xe3\xbc\xfb\xf7\xda\xfc\xe9\xc3\xfc\xeb\
+\xc5\xfc\xed\xc7\xfc\xf8\xda\xfd\xef\xca\xfd\xf0\xcb\xfd\xf3\xce\
+\xfd\xfb\xda\xfe\xf4\xcf\xfe\xf6\xd1\xfe\xf6\xd2\xfe\xf7\xd3\xfe\
+\xf8\xd5\xfe\xf9\xd4\xff\xfb\xd7\xff\xfc\xd9\xff\xfd\xda\xff\xfd\
+\xdb\xff\xfe\xdb\xa6\x92\x1f\xb1\x00\x00\x00\x34\x74\x52\x4e\x53\
+\x00\x01\x01\x02\x04\x09\x0c\x11\x12\x16\x1b\x1b\x20\x30\x36\x37\
+\x3f\x47\x4d\x5e\x5e\x6f\x77\x83\x83\x84\x84\x88\x91\x99\x99\xa2\
+\xa2\xa8\xb1\xb3\xbb\xbd\xc1\xc2\xc9\xd1\xd7\xde\xe3\xea\xee\xf3\
+\xf6\xfc\xfd\xfe\x98\x06\x23\x55\x00\x00\x01\x8c\x49\x44\x41\x54\
+\x38\xcb\x63\x60\x40\x01\x8c\x5c\xdc\x4c\x0c\x78\x00\xaf\xba\xa4\
+\x94\x06\x3f\x4e\x69\x0e\x19\x43\x13\x49\xa9\x02\x23\x05\x4e\xac\
+\xd2\xcc\xc2\xba\x26\x26\x20\x05\x05\x05\x7a\xa2\x2c\x98\xf2\x02\
+\x9a\x26\x26\x30\x05\x05\x05\x5a\x82\x68\xd2\x5c\xf2\xc6\x26\xc8\
+\x0a\x0a\x0a\x94\x79\x90\xa4\x59\xc5\xf4\x4d\x4c\xd0\x14\x14\x18\
+\x48\xb3\xc1\xe4\x85\xb4\x4d\x4c\x30\x15\x14\x14\xe8\x88\x40\xe4\
+\xc5\x21\x52\x36\xae\x26\xde\xc9\xb9\x2a\x48\x0a\x0a\x0a\x24\xc0\
+\x0a\x64\x41\xd2\x76\xbe\x91\xf1\x39\x9b\xfb\xd3\x15\x21\x0a\x8a\
+\xda\x9a\x0b\x81\x94\x1c\x5c\x81\x47\x74\x5a\xfe\xa4\x25\x71\x16\
+\x30\x2b\x66\x6c\xde\x3c\x15\x45\x41\x40\xd2\xa6\xec\x29\x79\x51\
+\x0e\x50\x05\x75\x9b\xbb\x7a\x36\x56\x22\x2b\x08\xce\x98\x6d\x99\
+\xd2\x17\x0b\x53\x50\xba\x6e\xda\xf4\xd5\x25\x48\x0a\xcc\xa2\x13\
+\x16\x74\xae\xc8\x8c\x31\x87\x2a\xa8\x5f\xbe\x6e\x61\x07\xb2\x15\
+\x8e\xb1\xb1\x89\x59\xa9\xb1\x21\x50\x6f\x36\x2e\x5e\x5a\x0b\x76\
+\x0a\x5c\x81\x67\x2c\x18\x04\x80\x15\xd4\x4c\x5e\xbf\xac\xa1\x00\
+\x55\x81\x1f\x44\x81\x17\x50\x81\xd2\xdc\x0d\x9b\x37\x6f\x9c\xd5\
+\x84\xaa\x20\x08\xa2\xc0\x19\xa8\x40\x75\xde\xc4\xea\xf2\xde\x45\
+\x6b\x26\xb4\x17\x21\x14\x98\x46\x40\x14\x58\xc3\x83\xba\x62\xd5\
+\xc6\x0d\xd3\x11\x0a\xec\x21\xf2\xe1\x88\xb8\x68\xd8\xdc\xde\xbd\
+\xb9\x0a\xae\xc0\x35\x36\x36\x34\x2c\x36\x36\x10\xa1\xa0\x6c\xed\
+\x8c\x99\x2b\x8b\xe1\x0a\x9c\xdc\x6d\x4d\xac\xfc\x63\x7d\x90\x62\
+\xb3\x75\xce\xfc\x16\xe4\x90\x04\x01\x37\x17\xb4\xe8\x86\x29\x10\
+\x37\x31\xc1\x9e\x1e\x60\xd1\x4d\x30\xc1\x10\x4e\x72\x84\x13\x2d\
+\x11\xc9\x9e\x88\x8c\x03\x04\xec\xf8\xb3\x1e\x38\xf3\xaa\x01\x33\
+\x2f\x1f\xbe\xec\x8d\x99\xfd\x01\x12\xd5\xd3\xad\x82\xe8\xbe\xc1\
+\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\
\x00\x00\x33\x3f\
\x89\
\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
@@ -4759,6 +4848,10 @@ qt_resource_name = "\
\x00\x6d\
\x00\x61\x00\x74\x00\x63\x00\x68\x00\x2d\x00\x70\x00\x65\x00\x6e\x00\x64\x00\x69\x00\x6e\x00\x67\x00\x2d\x00\x36\x00\x30\x00\x2e\
\x00\x70\x00\x6e\x00\x67\
+\x00\x0a\
+\x0a\xcb\x27\x16\
+\x00\x6c\
+\x00\x6f\x00\x61\x00\x64\x00\x65\x00\x72\x00\x2e\x00\x67\x00\x69\x00\x66\
\x00\x05\
\x00\x35\x9b\x52\
\x00\x32\
@@ -4935,78 +5028,79 @@ qt_resource_name = "\
qt_resource_struct = "\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\
-\x00\x00\x00\x00\x00\x02\x00\x00\x00\x1d\x00\x00\x00\x02\
-\x00\x00\x02\x72\x00\x00\x00\x00\x00\x01\x00\x00\x4d\x8d\
-\x00\x00\x02\xf8\x00\x02\x00\x00\x00\x12\x00\x00\x00\x37\
-\x00\x00\x02\x16\x00\x02\x00\x00\x00\x13\x00\x00\x00\x24\
-\x00\x00\x00\x12\x00\x02\x00\x00\x00\x01\x00\x00\x00\x23\
-\x00\x00\x01\x1a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x22\
-\x00\x00\x01\x4e\x00\x02\x00\x00\x00\x01\x00\x00\x00\x21\
+\x00\x00\x00\x00\x00\x02\x00\x00\x00\x1e\x00\x00\x00\x02\
+\x00\x00\x02\x8c\x00\x00\x00\x00\x00\x01\x00\x00\x50\x32\
+\x00\x00\x03\x12\x00\x02\x00\x00\x00\x12\x00\x00\x00\x38\
+\x00\x00\x02\x30\x00\x02\x00\x00\x00\x13\x00\x00\x00\x25\
+\x00\x00\x00\x12\x00\x02\x00\x00\x00\x01\x00\x00\x00\x24\
+\x00\x00\x01\x1a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x23\
+\x00\x00\x01\x4e\x00\x02\x00\x00\x00\x01\x00\x00\x00\x22\
\x00\x00\x00\x22\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
-\x00\x00\x03\x4a\x00\x00\x00\x00\x00\x01\x00\x00\x54\x26\
-\x00\x00\x03\x26\x00\x00\x00\x00\x00\x01\x00\x00\x53\x90\
-\x00\x00\x00\x94\x00\x02\x00\x00\x00\x01\x00\x00\x00\x20\
-\x00\x00\x01\x06\x00\x02\x00\x00\x00\x01\x00\x00\x00\x1f\
-\x00\x00\x02\xd4\x00\x00\x00\x00\x00\x01\x00\x00\x50\xac\
+\x00\x00\x03\x64\x00\x00\x00\x00\x00\x01\x00\x00\x56\xcb\
+\x00\x00\x03\x40\x00\x00\x00\x00\x00\x01\x00\x00\x56\x35\
+\x00\x00\x00\x94\x00\x02\x00\x00\x00\x01\x00\x00\x00\x21\
+\x00\x00\x01\x06\x00\x02\x00\x00\x00\x01\x00\x00\x00\x20\
+\x00\x00\x02\xee\x00\x00\x00\x00\x00\x01\x00\x00\x53\x51\
\x00\x00\x01\x2a\x00\x00\x00\x00\x00\x01\x00\x00\x47\x52\
\x00\x00\x00\xd6\x00\x00\x00\x00\x00\x01\x00\x00\x46\xe5\
\x00\x00\x00\x48\x00\x00\x00\x00\x00\x01\x00\x00\x01\x3e\
\x00\x00\x00\x6a\x00\x00\x00\x00\x00\x01\x00\x00\x03\x8e\
\x00\x00\x00\xbe\x00\x00\x00\x00\x00\x01\x00\x00\x43\x17\
-\x00\x00\x02\xa6\x00\x00\x00\x00\x00\x01\x00\x00\x50\x23\
+\x00\x00\x02\xc0\x00\x00\x00\x00\x00\x01\x00\x00\x52\xc8\
\x00\x00\x00\xa8\x00\x00\x00\x00\x00\x01\x00\x00\x40\xf4\
-\x00\x00\x02\x44\x00\x00\x00\x00\x00\x01\x00\x00\x4d\x14\
+\x00\x00\x02\x5e\x00\x00\x00\x00\x00\x01\x00\x00\x4f\xb9\
\x00\x00\x01\xe8\x00\x00\x00\x00\x00\x01\x00\x00\x4c\x05\
\x00\x00\x01\x9c\x00\x00\x00\x00\x00\x01\x00\x00\x4a\xf5\
-\x00\x00\x03\x6a\x00\x00\x00\x00\x00\x01\x00\x00\x54\xa3\
+\x00\x00\x02\x16\x00\x00\x00\x00\x00\x01\x00\x00\x4c\x8d\
+\x00\x00\x03\x84\x00\x00\x00\x00\x00\x01\x00\x00\x57\x48\
\x00\x00\x01\x5e\x00\x00\x00\x00\x00\x01\x00\x00\x48\x77\
-\x00\x00\x03\x08\x00\x00\x00\x00\x00\x01\x00\x00\x52\xfc\
-\x00\x00\x02\x88\x00\x00\x00\x00\x00\x01\x00\x00\x4f\x8a\
-\x00\x00\x02\x26\x00\x00\x00\x00\x00\x01\x00\x00\x4c\x8d\
+\x00\x00\x03\x22\x00\x00\x00\x00\x00\x01\x00\x00\x55\xa1\
+\x00\x00\x02\xa2\x00\x00\x00\x00\x00\x01\x00\x00\x52\x2f\
+\x00\x00\x02\x40\x00\x00\x00\x00\x00\x01\x00\x00\x4f\x32\
\x00\x00\x01\xca\x00\x00\x00\x00\x00\x01\x00\x00\x4b\x6b\
\x00\x00\x01\x7e\x00\x00\x00\x00\x00\x01\x00\x00\x4a\x6e\
-\x00\x00\x04\x4a\x00\x00\x00\x00\x00\x01\x00\x00\xce\x2d\
-\x00\x00\x04\x4a\x00\x00\x00\x00\x00\x01\x00\x01\x01\x70\
-\x00\x00\x04\x4a\x00\x00\x00\x00\x00\x01\x00\x00\xc7\x62\
-\x00\x00\x04\x4a\x00\x00\x00\x00\x00\x01\x00\x00\xcc\xc7\
-\x00\x00\x04\x4a\x00\x00\x00\x00\x00\x01\x00\x01\x19\xca\
-\x00\x00\x06\x4a\x00\x00\x00\x00\x00\x01\x00\x00\xbb\x5b\
-\x00\x00\x04\x98\x00\x00\x00\x00\x00\x01\x00\x00\x92\x3c\
-\x00\x00\x06\xc0\x00\x00\x00\x00\x00\x01\x00\x00\x85\x4d\
-\x00\x00\x07\x44\x00\x00\x00\x00\x00\x01\x00\x00\xa2\xb9\
-\x00\x00\x05\xca\x00\x00\x00\x00\x00\x01\x00\x00\xae\x5d\
-\x00\x00\x07\x6e\x00\x00\x00\x00\x00\x01\x00\x00\xa4\xe2\
-\x00\x00\x06\x94\x00\x00\x00\x00\x00\x01\x00\x00\x7e\x20\
-\x00\x00\x03\xcc\x00\x00\x00\x00\x00\x01\x00\x00\x80\xde\
-\x00\x00\x04\x64\x00\x00\x00\x00\x00\x01\x00\x00\x87\x7b\
-\x00\x00\x06\xe4\x00\x00\x00\x00\x00\x01\x00\x00\x8c\xc7\
-\x00\x00\x07\xbe\x00\x00\x00\x00\x00\x01\x00\x00\xc0\x5d\
-\x00\x00\x04\xc0\x00\x00\x00\x00\x00\x01\x00\x00\x95\xcc\
-\x00\x00\x04\x00\x00\x00\x00\x00\x00\x01\x00\x00\x82\xad\
-\x00\x00\x05\x3a\x00\x00\x00\x00\x00\x01\x00\x00\xaa\xde\
-\x00\x00\x06\x72\x00\x00\x00\x00\x00\x01\x00\x00\xc3\x00\
-\x00\x00\x05\x06\x00\x00\x00\x00\x00\x01\x00\x00\x9d\xe9\
-\x00\x00\x07\x16\x00\x00\x00\x00\x00\x01\x00\x00\x9a\x4a\
-\x00\x00\x07\x96\x00\x00\x00\x00\x00\x01\x00\x00\xb7\x08\
-\x00\x00\x06\x22\x00\x00\x00\x00\x00\x01\x00\x00\xb3\x89\
-\x00\x00\x04\x1a\x00\x00\x00\x00\x00\x01\x00\x00\x59\x46\
-\x00\x00\x06\x4a\x00\x00\x00\x00\x00\x01\x00\x00\x78\xd0\
-\x00\x00\x04\x98\x00\x00\x00\x00\x00\x01\x00\x00\x5f\x84\
-\x00\x00\x05\xca\x00\x00\x00\x00\x00\x01\x00\x00\x70\xcf\
-\x00\x00\x03\xcc\x00\x00\x00\x00\x00\x01\x00\x00\x56\x1d\
-\x00\x00\x06\x04\x00\x00\x00\x00\x00\x01\x00\x00\x73\xd5\
-\x00\x00\x05\x5c\x00\x00\x00\x00\x00\x01\x00\x00\x69\xf6\
-\x00\x00\x04\x64\x00\x00\x00\x00\x00\x01\x00\x00\x5c\x7b\
-\x00\x00\x04\x4a\x00\x00\x00\x00\x00\x01\x00\x00\x5b\xa2\
-\x00\x00\x04\xc0\x00\x00\x00\x00\x00\x01\x00\x00\x61\x46\
-\x00\x00\x04\x00\x00\x00\x00\x00\x00\x01\x00\x00\x57\xa1\
-\x00\x00\x05\xa8\x00\x00\x00\x00\x00\x01\x00\x00\x6e\xf8\
-\x00\x00\x05\x3a\x00\x00\x00\x00\x00\x01\x00\x00\x67\xd3\
-\x00\x00\x03\x98\x00\x00\x00\x00\x00\x01\x00\x00\x55\x27\
-\x00\x00\x06\x72\x00\x00\x00\x00\x00\x01\x00\x00\x7c\x0e\
-\x00\x00\x05\x06\x00\x00\x00\x00\x00\x01\x00\x00\x64\xbf\
-\x00\x00\x05\x82\x00\x00\x00\x00\x00\x01\x00\x00\x6c\xd0\
-\x00\x00\x06\x22\x00\x00\x00\x00\x00\x01\x00\x00\x76\x36\
+\x00\x00\x04\x64\x00\x00\x00\x00\x00\x01\x00\x00\xd3\x9d\
+\x00\x00\x04\x64\x00\x00\x00\x00\x00\x01\x00\x01\x06\xe0\
+\x00\x00\x04\x64\x00\x00\x00\x00\x00\x01\x00\x00\xca\x07\
+\x00\x00\x04\x64\x00\x00\x00\x00\x00\x01\x00\x00\xcf\x6c\
+\x00\x00\x04\x64\x00\x00\x00\x00\x00\x01\x00\x01\x1f\x3a\
+\x00\x00\x06\x64\x00\x00\x00\x00\x00\x01\x00\x00\xbe\x00\
+\x00\x00\x04\xb2\x00\x00\x00\x00\x00\x01\x00\x00\x94\xe1\
+\x00\x00\x06\xda\x00\x00\x00\x00\x00\x01\x00\x00\x87\xf2\
+\x00\x00\x07\x5e\x00\x00\x00\x00\x00\x01\x00\x00\xa5\x5e\
+\x00\x00\x05\xe4\x00\x00\x00\x00\x00\x01\x00\x00\xb1\x02\
+\x00\x00\x07\x88\x00\x00\x00\x00\x00\x01\x00\x00\xa7\x87\
+\x00\x00\x06\xae\x00\x00\x00\x00\x00\x01\x00\x00\x80\xc5\
+\x00\x00\x03\xe6\x00\x00\x00\x00\x00\x01\x00\x00\x83\x83\
+\x00\x00\x04\x7e\x00\x00\x00\x00\x00\x01\x00\x00\x8a\x20\
+\x00\x00\x06\xfe\x00\x00\x00\x00\x00\x01\x00\x00\x8f\x6c\
+\x00\x00\x07\xd8\x00\x00\x00\x00\x00\x01\x00\x00\xc3\x02\
+\x00\x00\x04\xda\x00\x00\x00\x00\x00\x01\x00\x00\x98\x71\
+\x00\x00\x04\x1a\x00\x00\x00\x00\x00\x01\x00\x00\x85\x52\
+\x00\x00\x05\x54\x00\x00\x00\x00\x00\x01\x00\x00\xad\x83\
+\x00\x00\x06\x8c\x00\x00\x00\x00\x00\x01\x00\x00\xc5\xa5\
+\x00\x00\x05\x20\x00\x00\x00\x00\x00\x01\x00\x00\xa0\x8e\
+\x00\x00\x07\x30\x00\x00\x00\x00\x00\x01\x00\x00\x9c\xef\
+\x00\x00\x07\xb0\x00\x00\x00\x00\x00\x01\x00\x00\xb9\xad\
+\x00\x00\x06\x3c\x00\x00\x00\x00\x00\x01\x00\x00\xb6\x2e\
+\x00\x00\x04\x34\x00\x00\x00\x00\x00\x01\x00\x00\x5b\xeb\
+\x00\x00\x06\x64\x00\x00\x00\x00\x00\x01\x00\x00\x7b\x75\
+\x00\x00\x04\xb2\x00\x00\x00\x00\x00\x01\x00\x00\x62\x29\
+\x00\x00\x05\xe4\x00\x00\x00\x00\x00\x01\x00\x00\x73\x74\
+\x00\x00\x03\xe6\x00\x00\x00\x00\x00\x01\x00\x00\x58\xc2\
+\x00\x00\x06\x1e\x00\x00\x00\x00\x00\x01\x00\x00\x76\x7a\
+\x00\x00\x05\x76\x00\x00\x00\x00\x00\x01\x00\x00\x6c\x9b\
+\x00\x00\x04\x7e\x00\x00\x00\x00\x00\x01\x00\x00\x5f\x20\
+\x00\x00\x04\x64\x00\x00\x00\x00\x00\x01\x00\x00\x5e\x47\
+\x00\x00\x04\xda\x00\x00\x00\x00\x00\x01\x00\x00\x63\xeb\
+\x00\x00\x04\x1a\x00\x00\x00\x00\x00\x01\x00\x00\x5a\x46\
+\x00\x00\x05\xc2\x00\x00\x00\x00\x00\x01\x00\x00\x71\x9d\
+\x00\x00\x05\x54\x00\x00\x00\x00\x00\x01\x00\x00\x6a\x78\
+\x00\x00\x03\xb2\x00\x00\x00\x00\x00\x01\x00\x00\x57\xcc\
+\x00\x00\x06\x8c\x00\x00\x00\x00\x00\x01\x00\x00\x7e\xb3\
+\x00\x00\x05\x20\x00\x00\x00\x00\x00\x01\x00\x00\x67\x64\
+\x00\x00\x05\x9c\x00\x00\x00\x00\x00\x01\x00\x00\x6f\x75\
+\x00\x00\x06\x3c\x00\x00\x00\x00\x00\x01\x00\x00\x78\xdb\
"
def qInitResources():
diff --git a/picard/track.py b/picard/track.py
index 3a96d4eb2..faaef9cc8 100644
--- a/picard/track.py
+++ b/picard/track.py
@@ -271,9 +271,9 @@ class NonAlbumTrack(Track):
log.error(traceback.format_exc())
def _parse_recording(self, recording):
- recording_to_metadata(recording, self)
- self._customize_metadata()
m = self.metadata
+ recording_to_metadata(recording, m, self)
+ self._customize_metadata()
run_track_metadata_processors(self.album, m, None, recording)
if config.setting["enable_tagger_script"]:
script = config.setting["tagger_script"]
diff --git a/picard/ui/itemviews.py b/picard/ui/itemviews.py
index 32f169e75..93e388ab2 100644
--- a/picard/ui/itemviews.py
+++ b/picard/ui/itemviews.py
@@ -257,6 +257,7 @@ class BaseTreeView(QtGui.QTreeWidget):
if obj.num_linked_files == 1:
menu.addAction(self.window.play_file_action)
menu.addAction(self.window.open_folder_action)
+ menu.addAction(self.window.tracks_search_action)
plugin_actions.extend(_file_actions)
menu.addAction(self.window.browser_lookup_action)
menu.addSeparator()
@@ -285,6 +286,7 @@ class BaseTreeView(QtGui.QTreeWidget):
menu.addSeparator()
menu.addAction(self.window.autotag_action)
menu.addAction(self.window.analyze_action)
+ menu.addAction(self.window.tracks_search_action)
plugin_actions = list(_file_actions)
elif isinstance(obj, Album):
if can_view_info:
diff --git a/picard/ui/mainwindow.py b/picard/ui/mainwindow.py
index a45595bf3..c9e9d7bbe 100644
--- a/picard/ui/mainwindow.py
+++ b/picard/ui/mainwindow.py
@@ -34,6 +34,7 @@ from picard.ui.filebrowser import FileBrowser
from picard.ui.tagsfromfilenames import TagsFromFileNamesDialog
from picard.ui.options.dialog import OptionsDialog
from picard.ui.infodialog import FileInfoDialog, AlbumInfoDialog, ClusterInfoDialog
+from picard.ui.searchdialog import TrackSearchDialog
from picard.ui.infostatus import InfoStatus
from picard.ui.passworddialog import PasswordDialog
from picard.ui.logview import LogView, HistoryView
@@ -381,6 +382,10 @@ class MainWindow(QtGui.QMainWindow):
self.browser_lookup_action.setEnabled(False)
self.browser_lookup_action.triggered.connect(self.browser_lookup)
+ self.tracks_search_action = QtGui.QAction(icontheme.lookup('system-search'), _(u"Search similar tracks..."), self)
+ self.tracks_search_action.setStatusTip(_(u"View similar tracks and optionally choose a different release"))
+ self.tracks_search_action.triggered.connect(self.show_more_tracks)
+
self.show_file_browser_action = QtGui.QAction(_(u"File &Browser"), self)
self.show_file_browser_action.setCheckable(True)
if config.persist["view_file_browser"]:
@@ -674,7 +679,13 @@ class MainWindow(QtGui.QMainWindow):
"""Search for album, artist or track on the MusicBrainz website."""
text = self.search_edit.text()
type = self.search_combo.itemData(self.search_combo.currentIndex())
- self.tagger.search(text, type, config.setting["use_adv_search_syntax"])
+ if config.setting["builtin_search"]:
+ if type == "track":
+ dialog = TrackSearchDialog(self)
+ dialog.search(text)
+ dialog.exec_()
+ else:
+ self.tagger.search(text, type, config.setting["use_adv_search_syntax"])
def add_files(self):
"""Add files to the tagger."""
@@ -791,6 +802,14 @@ class MainWindow(QtGui.QMainWindow):
QtGui.QMessageBox.Yes)
return ret == QtGui.QMessageBox.Yes
+ def show_more_tracks(self):
+ obj = self.selected_objects[0]
+ if isinstance(obj, Track):
+ obj = obj.linked_files[0]
+ dialog = TrackSearchDialog(self)
+ dialog.load_similar_tracks(obj)
+ dialog.exec_()
+
def view_info(self):
if isinstance(self.selected_objects[0], Album):
album = self.selected_objects[0]
diff --git a/picard/ui/options/interface.py b/picard/ui/options/interface.py
index c603cb746..65dd7c723 100644
--- a/picard/ui/options/interface.py
+++ b/picard/ui/options/interface.py
@@ -40,6 +40,7 @@ class InterfaceOptionsPage(OptionsPage):
options = [
config.BoolOption("setting", "toolbar_show_labels", True),
config.BoolOption("setting", "toolbar_multiselect", False),
+ config.BoolOption("setting", "builtin_search", False),
config.BoolOption("setting", "use_adv_search_syntax", False),
config.BoolOption("setting", "quit_confirmation", True),
config.TextOption("setting", "ui_language", u""),
@@ -77,6 +78,7 @@ class InterfaceOptionsPage(OptionsPage):
def load(self):
self.ui.toolbar_show_labels.setChecked(config.setting["toolbar_show_labels"])
self.ui.toolbar_multiselect.setChecked(config.setting["toolbar_multiselect"])
+ self.ui.builtin_search.setChecked(config.setting["builtin_search"])
self.ui.use_adv_search_syntax.setChecked(config.setting["use_adv_search_syntax"])
self.ui.quit_confirmation.setChecked(config.setting["quit_confirmation"])
current_ui_language = config.setting["ui_language"]
@@ -87,6 +89,7 @@ class InterfaceOptionsPage(OptionsPage):
def save(self):
config.setting["toolbar_show_labels"] = self.ui.toolbar_show_labels.isChecked()
config.setting["toolbar_multiselect"] = self.ui.toolbar_multiselect.isChecked()
+ config.setting["builtin_search"] = self.ui.builtin_search.isChecked()
config.setting["use_adv_search_syntax"] = self.ui.use_adv_search_syntax.isChecked()
config.setting["quit_confirmation"] = self.ui.quit_confirmation.isChecked()
self.tagger.window.update_toolbar_style()
diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py
new file mode 100644
index 000000000..c1d837d6b
--- /dev/null
+++ b/picard/ui/searchdialog.py
@@ -0,0 +1,462 @@
+# -*- coding: utf-8 -*-
+#
+# Picard, the next-generation MusicBrainz tagger
+#
+# 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, QtCore, QtNetwork
+from operator import itemgetter
+from functools import partial
+from collections import namedtuple
+from picard import config
+from picard.file import File
+from picard.ui import PicardDialog
+from picard.ui.util import StandardButton, ButtonLineEdit
+from picard.util import format_time, icontheme
+from picard.mbxml import (
+ recording_to_metadata,
+ release_to_metadata,
+ release_group_to_metadata
+)
+from picard.i18n import ugettext_attr
+from picard.metadata import Metadata
+from picard.webservice import escape_lucene_query
+from picard.track import Track
+
+
+class ResultTable(QtGui.QTableWidget):
+
+ def __init__(self, column_titles):
+ QtGui.QTableWidget.__init__(self, 0, len(column_titles))
+ self.setHorizontalHeaderLabels(column_titles)
+ self.setSelectionMode(
+ QtGui.QAbstractItemView.SingleSelection)
+ self.setSelectionBehavior(
+ QtGui.QAbstractItemView.SelectRows)
+ self.setEditTriggers(
+ QtGui.QAbstractItemView.NoEditTriggers)
+ self.horizontalHeader().setStretchLastSection(True)
+ self.horizontalHeader().setResizeMode(
+ QtGui.QHeaderView.Stretch)
+ self.horizontalHeader().setResizeMode(
+ QtGui.QHeaderView.Interactive)
+
+class SearchBox(QtGui.QWidget):
+
+ def __init__(self, parent):
+ self.parent = parent
+ QtGui.QWidget.__init__(self, parent)
+ self.search_action = QtGui.QAction(icontheme.lookup('system-search'),
+ _(u"Search"), self)
+ self.search_action.triggered.connect(self.search)
+ self.setupUi()
+
+ def setupUi(self):
+ self.layout = QtGui.QVBoxLayout(self)
+ self.search_row_widget = QtGui.QWidget(self)
+ self.search_row_layout = QtGui.QHBoxLayout(self.search_row_widget)
+ self.search_row_layout.setContentsMargins(1, 1, 1, 1)
+ self.search_row_layout.setSpacing(1)
+ self.search_edit = ButtonLineEdit(self.search_row_widget)
+ self.search_row_layout.addWidget(self.search_edit)
+ self.search_button = QtGui.QToolButton(self.search_row_widget)
+ self.search_button.setAutoRaise(True)
+ self.search_button.setDefaultAction(self.search_action)
+ self.search_button.setIconSize(QtCore.QSize(22, 22))
+ self.search_row_layout.addWidget(self.search_button)
+ self.search_row_widget.setLayout(self.search_row_layout)
+ self.layout.addWidget(self.search_row_widget)
+ self.adv_opt_row_widget = QtGui.QWidget(self)
+ self.adv_opt_row_layout = QtGui.QHBoxLayout(self.adv_opt_row_widget)
+ self.adv_opt_row_layout.setAlignment(QtCore.Qt.AlignLeft)
+ self.adv_opt_row_layout.setContentsMargins(1, 1, 1, 1)
+ self.adv_opt_row_layout.setSpacing(1)
+ self.use_adv_search_syntax = QtGui.QCheckBox(self.adv_opt_row_widget)
+ self.use_adv_search_syntax.setText(_("Use advanced query syntax"))
+ self.adv_opt_row_layout.addWidget(self.use_adv_search_syntax)
+ self.adv_syntax_help = QtGui.QLabel(self.adv_opt_row_widget)
+ self.adv_syntax_help.setOpenExternalLinks(True)
+ self.adv_syntax_help.setText(_(
+ " ("
+ "Syntax Help)"))
+ self.adv_opt_row_layout.addWidget(self.adv_syntax_help)
+ self.adv_opt_row_widget.setLayout(self.adv_opt_row_layout)
+ self.layout.addWidget(self.adv_opt_row_widget)
+ self.layout.setContentsMargins(1, 1, 1, 1)
+ self.layout.setSpacing(1)
+ self.setMaximumHeight(60)
+
+ def search(self):
+ self.parent.search(self.search_edit.text())
+
+ def restore_checkbox_state(self):
+ self.use_adv_search_syntax.setChecked(config.setting["use_adv_search_syntax"])
+
+ def save_checkbox_state(self):
+ config.setting["use_adv_search_syntax"] = self.use_adv_search_syntax.isChecked()
+
+
+Retry = namedtuple("Retry", ["function", "query"])
+
+
+class SearchDialog(PicardDialog):
+
+ options = [
+ config.Option("persist", "searchdialog_window_size", QtCore.QSize(720, 360)),
+ config.Option("persist", "searchdialog_header_state", QtCore.QByteArray())
+ ]
+
+ def __init__(self, parent=None):
+ PicardDialog.__init__(self, parent)
+ self.search_results = []
+ self.table = None
+ self.setupUi()
+ self.restore_state()
+
+ def setupUi(self):
+ self.verticalLayout = QtGui.QVBoxLayout(self)
+ self.verticalLayout.setObjectName(_("vertical_layout"))
+ self.search_box = SearchBox(self)
+ self.search_box.setObjectName(_("search_box"))
+ self.verticalLayout.addWidget(self.search_box)
+ self.center_widget = QtGui.QWidget(self)
+ self.center_widget.setObjectName(_("center_widget"))
+ self.center_layout = QtGui.QVBoxLayout(self.center_widget)
+ self.center_layout.setObjectName(_("center_layout"))
+ self.center_layout.setContentsMargins(1, 1, 1, 1)
+ self.center_widget.setLayout(self.center_layout)
+ self.verticalLayout.addWidget(self.center_widget)
+ self.buttonBox = QtGui.QDialogButtonBox(self)
+ self.buttonBox.setObjectName(_("button_box"))
+ self.load_button = QtGui.QPushButton(_("Load into Picard"))
+ self.load_button.setEnabled(False)
+ self.buttonBox.addButton(
+ self.load_button,
+ QtGui.QDialogButtonBox.AcceptRole)
+ self.buttonBox.addButton(
+ StandardButton(StandardButton.CANCEL),
+ QtGui.QDialogButtonBox.RejectRole)
+ self.buttonBox.accepted.connect(self.accept)
+ self.buttonBox.rejected.connect(self.reject)
+ self.verticalLayout.addWidget(self.buttonBox)
+
+ def add_widget_to_center_layout(self, widget):
+ """Updates child widget of center_widget.
+
+ Child widgets represent dialog's current state, like progress,
+ error, and displaying fetched results.
+ """
+
+ wid = self.center_layout.takeAt(0)
+ if wid:
+ if wid.widget().objectName() == "results_table":
+ self.table = None
+ wid.widget().deleteLater()
+ self.center_layout.addWidget(widget)
+
+ def show_progress(self):
+ """Displays feedback while results are being fetched from server."""
+
+ self.progress_widget = QtGui.QWidget(self)
+ self.progress_widget.setObjectName("progress_widget")
+ layout = QtGui.QVBoxLayout(self.progress_widget)
+ text_label = QtGui.QLabel(_('Loading...'), self.progress_widget)
+ text_label.setAlignment(QtCore.Qt.AlignHCenter | QtCore.Qt.AlignBottom)
+ gif_label = QtGui.QLabel(self.progress_widget)
+ movie = QtGui.QMovie(":/images/loader.gif")
+ gif_label.setMovie(movie)
+ movie.start()
+ gif_label.setAlignment(QtCore.Qt.AlignHCenter | QtCore.Qt.AlignTop)
+ layout.addWidget(text_label)
+ layout.addWidget(gif_label)
+ layout.setContentsMargins(1, 1, 1, 1)
+ self.progress_widget.setLayout(layout)
+ self.add_widget_to_center_layout(self.progress_widget)
+
+ def show_error(self, error, show_retry_button=False):
+ """Displays error inside the dialog.
+
+ Args:
+ error -- Error string
+ show_retry_button -- Whether to display retry button or not
+ """
+
+ self.error_widget = QtGui.QWidget(self)
+ self.error_widget.setObjectName("error_widget")
+ layout = QtGui.QVBoxLayout(self.error_widget)
+ error_label = QtGui.QLabel(error, self.error_widget)
+ error_label.setWordWrap(True)
+ error_label.setAlignment(QtCore.Qt.AlignCenter)
+ error_label.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse)
+ layout.addWidget(error_label)
+ if show_retry_button:
+ retry_widget = QtGui.QWidget(self.error_widget)
+ retry_layout = QtGui.QHBoxLayout(retry_widget)
+ retry_button = QtGui.QPushButton(_("Retry"), self.error_widget)
+ retry_button.clicked.connect(self.retry)
+ retry_button.setSizePolicy(QtGui.QSizePolicy(QtGui.QSizePolicy.Maximum, QtGui.QSizePolicy.Fixed))
+ retry_layout.addWidget(retry_button)
+ retry_layout.setAlignment(QtCore.Qt.AlignHCenter | QtCore.Qt.AlignTop)
+ retry_widget.setLayout(retry_layout)
+ layout.addWidget(retry_widget)
+ self.error_widget.setLayout(layout)
+ self.add_widget_to_center_layout(self.error_widget)
+
+ def show_table(self, column_headers):
+ """Displays results table inside the dialog."""
+
+ self.table = ResultTable(self.table_headers)
+ self.table.setObjectName("results_table")
+ self.table.cellDoubleClicked.connect(self.row_double_clicked)
+ self.table.horizontalHeader().sectionResized.connect(
+ self.save_table_header_state)
+ self.restore_table_header_state()
+ self.add_widget_to_center_layout(self.table)
+ def enable_loading_button():
+ self.load_button.setEnabled(True)
+ self.table.itemSelectionChanged.connect(
+ enable_loading_button)
+
+ def row_double_clicked(self, row):
+ """Handle function for double click event inside the table."""
+
+ self.load_selection(row)
+ self.accept()
+
+ def network_error(self, reply, error):
+ error_msg = _("Following error occurred while fetching results:
"
+ "Network request error for %s:
%s (QT code %d, HTTP code %s)
" % (
+ reply.request().url().toString(QtCore.QUrl.RemoveUserInfo),
+ reply.errorString(),
+ error,
+ repr(reply.attribute(QtNetwork.QNetworkRequest.HttpStatusCodeAttribute)))
+ )
+ self.show_error(error_msg, show_retry_button=True)
+
+ def no_results_found(self):
+ error_msg = _("No results found. Please try a different search query.")
+ self.show_error(error_msg)
+
+ def accept(self):
+ if self.table:
+ sel_rows = self.table.selectionModel().selectedRows()
+ if sel_rows:
+ sel_row = sel_rows[0].row()
+ self.load_selection(sel_row)
+ self.save_state()
+ QtGui.QDialog.accept(self)
+
+ def reject(self):
+ self.save_state()
+ QtGui.QDialog.reject(self)
+
+ def restore_state(self):
+ size = config.persist["searchdialog_window_size"]
+ if size:
+ self.resize(size)
+ self.search_box.restore_checkbox_state()
+
+ def restore_table_header_state(self):
+ header = self.table.horizontalHeader()
+ state = config.persist["searchdialog_header_state"]
+ if state:
+ header.restoreState(state)
+ header.setResizeMode(QtGui.QHeaderView.Interactive)
+
+ def save_state(self):
+ """Saves dialog state i.e. window size, checkbox state, and table
+ header size.
+ """
+
+ if self.table:
+ self.save_table_header_state()
+ config.persist["searchdialog_window_size"] = self.size()
+ self.search_box.save_checkbox_state()
+
+ def save_table_header_state(self):
+ state = self.table.horizontalHeader().saveState()
+ config.persist["searchdialog_header_state"] = state
+
+
+class TrackSearchDialog(SearchDialog):
+
+ def __init__(self, parent):
+ super(TrackSearchDialog, self).__init__(parent)
+ self.file_ = None
+ self.setWindowTitle(_("Track Search Results"))
+ self.table_headers = [
+ _("Name"),
+ _("Length"),
+ _("Artist"),
+ _("Release"),
+ _("Date"),
+ _("Country"),
+ _("Type")
+ ]
+
+ def search(self, text):
+ """Performs search using query provided by the user."""
+
+ self.retry_params = Retry(self.search, text)
+ self.search_box.search_edit.setText(text)
+ self.show_progress()
+ self.tagger.xmlws.find_tracks(self.handle_reply,
+ query=text,
+ search=True,
+ limit=25)
+
+ def load_similar_tracks(self, file_):
+ """Performs search by using existing metadata information
+ from the file."""
+
+ self.retry_params = Retry(self.load_similar_tracks, file_)
+ self.file_ = file_
+ metadata = file_.orig_metadata
+ query = {
+ 'track': metadata['title'],
+ 'artist': metadata['artist'],
+ 'release': metadata['album'],
+ 'tnum': metadata['tracknumber'],
+ 'tracks': metadata['totaltracks'],
+ 'qdur': str(metadata.length / 2000),
+ 'isrc': metadata['isrc'],
+ }
+ if config.setting["use_adv_search_syntax"]:
+ # Display the query in advance syntax format.
+ query_str = ' '.join(['%s:(%s)' % (item, escape_lucene_query(value))
+ for item, value in query.iteritems() if value])
+ else:
+ # Display only the track title
+ query_str = query["track"]
+ # `query_str` is used only for presenting purpose. Actual query consists of all filters and follows
+ # advanced query syntax.
+ query["limit"] = 25
+ self.search_box.search_edit.setText(query_str)
+ self.show_progress()
+ self.tagger.xmlws.find_tracks(
+ self.handle_reply,
+ **query)
+
+ def retry(self):
+ """Retries the search using information from `retry_params`."""
+ self.retry_params.function(self.retry_params.query)
+
+ def handle_reply(self, document, http, error):
+ if error:
+ self.network_error(http, error)
+ return
+
+ try:
+ tracks = document.metadata[0].recording_list[0].recording
+ except (AttributeError, IndexError):
+ self.no_results_found()
+ return
+
+ if self.file_:
+ sorted_results = sorted(
+ (self.file_.orig_metadata.compare_to_track(
+ track,
+ File.comparison_weights)
+ for track in tracks),
+ reverse=True,
+ key=itemgetter(0))
+ tracks = [item[3] for item in sorted_results]
+
+ del self.search_results[:] # Clear existing data
+ self.parse_tracks_from_xml(tracks)
+ self.display_results()
+
+ def display_results(self):
+ self.show_table(self.table_headers)
+ for row, obj in enumerate(self.search_results):
+ track = obj[0]
+ table_item = QtGui.QTableWidgetItem
+ self.table.insertRow(row)
+ self.table.setItem(row, 0, table_item(track.get("title", "")))
+ self.table.setItem(row, 1, table_item(track.get("~length", "")))
+ self.table.setItem(row, 2, table_item(track.get("artist", "")))
+ self.table.setItem(row, 3, table_item(track.get("album", "")))
+ self.table.setItem(row, 4, table_item(track.get("date", "")))
+ self.table.setItem(row, 5, table_item(track.get("country", "")))
+ self.table.setItem(row, 6, table_item(track.get("releasetype", "")))
+
+ def parse_tracks_from_xml(self, tracks_xml):
+ """Extracts track information from XmlNode objects and stores that into Metadata objects.
+
+ Args:
+ tracks_xml -- list of XmlNode objects
+ """
+ for node in tracks_xml:
+ if "release_list" in node.children and "release" in node.release_list[0].children:
+ for rel_node in node.release_list[0].release:
+ track = Metadata()
+ recording_to_metadata(node, track)
+ release_to_metadata(rel_node, track)
+ rg_node = rel_node.release_group[0]
+ release_group_to_metadata(rg_node, track)
+ if "release_event_list" in rel_node.children:
+ # Extract contries list from `release_event_list` element
+ # Don't use `country` element as it contains information of a single release
+ # event and is basically for backward compatibility.
+ country = []
+ for re in rel_node.release_event_list[0].release_event:
+ try:
+ country.append(
+ re.area[0].iso_3166_1_code_list[0].iso_3166_1_code[0].text)
+ except AttributeError:
+ pass
+ track["country"] = ", ".join(country)
+ self.search_results.append((track, node))
+ else:
+ # This handles the case when no release is associated with a track
+ # i.e. the track is a NAT
+ track = Metadata()
+ recording_to_metadata(node, track)
+ track["album"] = _("Standalone Recording")
+ self.search_results.append((track, node))
+
+ def load_selection(self, row):
+ """Loads album corresponding to selected track.
+ If the search is performed for a file, also associates the file to
+ corresponding track in the album.
+ """
+ track, node = self.search_results[row]
+ if track.get("musicbrainz_albumid"):
+ # The track is not an NAT
+ self.tagger.get_release_group_by_id(track["musicbrainz_releasegroupid"]).loaded_albums.add(
+ track["musicbrainz_albumid"])
+ if self.file_:
+ # Search is performed for a file
+ # Have to move that file from its existing album to the new one
+ if isinstance(self.file_.parent, Track):
+ album = self.file_.parent.album
+ self.tagger.move_file_to_track(self.file_, track["musicbrainz_albumid"], track["musicbrainz_recordingid"])
+ if album._files == 0:
+ # Remove album if it has no more files associated
+ self.tagger.remove_album(album)
+ else:
+ self.tagger.move_file_to_track(self.file_, track["musicbrainz_albumid"], track["musicbrainz_recordingid"])
+ else:
+ # No files associated. Just a normal search.
+ self.tagger.load_album(track["musicbrainz_albumid"])
+ else:
+ # The track is a NAT
+ if self.file_:
+ album = self.file_.parent.album
+ self.tagger.move_file_to_nat(track["musicbrainz_recordingid"])
+ if album._files == 0:
+ self.tagger.remove_album(album)
+ else:
+ self.tagger.load_nat(track["musicbrainz_recordingid"], node)
diff --git a/picard/ui/ui_options_interface.py b/picard/ui/ui_options_interface.py
index 9f6bb3a1f..3e3d4f1c3 100644
--- a/picard/ui/ui_options_interface.py
+++ b/picard/ui/ui_options_interface.py
@@ -26,6 +26,9 @@ class Ui_InterfaceOptionsPage(object):
self.toolbar_multiselect = QtGui.QCheckBox(self.groupBox_2)
self.toolbar_multiselect.setObjectName(_fromUtf8("toolbar_multiselect"))
self.vboxlayout1.addWidget(self.toolbar_multiselect)
+ self.builtin_search = QtGui.QCheckBox(self.groupBox_2)
+ self.builtin_search.setObjectName("builtin_search")
+ self.vboxlayout1.addWidget(self.builtin_search)
self.use_adv_search_syntax = QtGui.QCheckBox(self.groupBox_2)
self.use_adv_search_syntax.setObjectName(_fromUtf8("use_adv_search_syntax"))
self.vboxlayout1.addWidget(self.use_adv_search_syntax)
@@ -69,6 +72,7 @@ class Ui_InterfaceOptionsPage(object):
self.groupBox_2.setTitle(_("Miscellaneous"))
self.toolbar_show_labels.setText(_("Show text labels under icons"))
self.toolbar_multiselect.setText(_("Allow selection of multiple directories"))
+ self.builtin_search.setText(_("Use builtin search rather than looking in browser"))
self.use_adv_search_syntax.setText(_("Use advanced query syntax"))
self.quit_confirmation.setText(_("Show a quit confirmation dialog for unsaved changes"))
self.starting_directory.setText(_("Begin browsing in the following directory:"))
diff --git a/picard/webservice.py b/picard/webservice.py
index 293fbac55..d174ae193 100644
--- a/picard/webservice.py
+++ b/picard/webservice.py
@@ -63,7 +63,7 @@ CLIENT_STRING = str(QUrl.toPercentEncoding('%s %s-%s' % (PICARD_ORG_NAME,
PICARD_VERSION_STR)))
-def _escape_lucene_query(text):
+def escape_lucene_query(text):
return re.sub(r'([+\-&|!(){}\[\]\^"~*?:\\/])', r'\\\1', text)
@@ -458,16 +458,28 @@ class XmlWebService(QtCore.QObject):
host = config.setting["server_host"]
port = config.setting["server_port"]
filters = []
- query = []
- for name, value in kwargs.items():
- if name == 'limit':
- filters.append((name, str(value)))
+
+ limit = kwargs.pop("limit")
+ if limit:
+ filters.append(("limit", limit))
+
+ is_search = kwargs.pop("search", False)
+ if is_search:
+ if config.setting["use_adv_search_syntax"]:
+ query = kwargs["query"]
else:
- value = _escape_lucene_query(value).strip().lower()
+ query = escape_lucene_query(kwargs["query"]).strip().lower()
+ filters.append(("dismax", 'true'))
+ else:
+ query = []
+ for name, value in kwargs.items():
+ value = escape_lucene_query(value).strip().lower()
if value:
query.append('%s:(%s)' % (name, value))
+ query = ' '.join(query)
+
if query:
- filters.append(('query', ' '.join(query)))
+ filters.append(("query", query))
queryargs = {}
for name, value in filters:
value = QUrl.toPercentEncoding(unicode(value))
diff --git a/resources/images/loader.gif b/resources/images/loader.gif
new file mode 100644
index 000000000..d0bce1542
Binary files /dev/null and b/resources/images/loader.gif differ
diff --git a/resources/makeqrc.py b/resources/makeqrc.py
index edcaffb71..9337a4cd3 100755
--- a/resources/makeqrc.py
+++ b/resources/makeqrc.py
@@ -21,14 +21,15 @@ def natsort_key(s):
return [ tryint(c) for c in re.split('(\d+)', s) ]
-def find_files(topdir, directory, pattern):
+def find_files(topdir, directory, patterns):
tdir = os.path.join(topdir, directory)
for root, dirs, files in os.walk(tdir):
for basename in files:
- if fnmatch.fnmatch(basename, pattern):
- filepath = os.path.join(root, basename)
- filename = os.path.relpath(filepath, topdir)
- yield filename
+ for pattern in patterns:
+ if fnmatch.fnmatch(basename, pattern):
+ filepath = os.path.join(root, basename)
+ filename = os.path.relpath(filepath, topdir)
+ yield filename
def main():
@@ -36,7 +37,7 @@ def main():
topdir = os.path.abspath(os.path.join(scriptdir, ".."))
resourcesdir = os.path.join(topdir, "resources")
qrcfile = os.path.join(resourcesdir, "picard.qrc")
- images = [i for i in find_files(resourcesdir, 'images', '*.png')]
+ images = [i for i in find_files(resourcesdir, 'images', ['*.gif', '*.png'])]
newimages = 0
for filename in images:
filepath = os.path.join(resourcesdir, filename)
diff --git a/resources/picard.qrc b/resources/picard.qrc
index 29a5c57b0..13e00f0a9 100644
--- a/resources/picard.qrc
+++ b/resources/picard.qrc
@@ -46,6 +46,7 @@
images/arrow.png
images/file-pending.png
images/file.png
+ images/loader.gif
images/match-50.png
images/match-60.png
images/match-70.png