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