mirror of
https://github.com/fergalmoran/picard.git
synced 2026-01-06 16:44:06 +00:00
Thread-less album loading.
This commit is contained in:
188
picard/album.py
188
picard/album.py
@@ -18,6 +18,7 @@
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
import traceback
|
||||
from PyQt4 import QtCore
|
||||
from musicbrainz2.model import Relation
|
||||
from musicbrainz2.utils import extractUuid, extractFragment
|
||||
@@ -27,32 +28,90 @@ from picard.dataobj import DataObject
|
||||
from picard.track import Track
|
||||
from picard.script import ScriptParser
|
||||
from picard.ui.item import Item
|
||||
from picard.util import needs_read_lock, needs_write_lock, format_time
|
||||
|
||||
|
||||
_AMAZON_IMAGE_URL = "http://images.amazon.com/images/P/%s.01.LZZZZZZZ.jpg"
|
||||
|
||||
|
||||
class AlbumLoadError(Exception):
|
||||
pass
|
||||
from picard.util import format_time
|
||||
from picard.cluster import Cluster
|
||||
from picard.mbxml import release_to_metadata, track_to_metadata
|
||||
|
||||
|
||||
class Album(DataObject, Item):
|
||||
|
||||
def __init__(self, id, title=None):
|
||||
def __init__(self, id):
|
||||
DataObject.__init__(self, id)
|
||||
self.metadata = Metadata()
|
||||
if title:
|
||||
self.metadata[u"album"] = title
|
||||
self.unmatched_files = []
|
||||
self.files = []
|
||||
self.unmatched_files = Cluster(_("Unmatched Files"), special=2)
|
||||
self.tracks = []
|
||||
self.loaded = False
|
||||
self._files = 0
|
||||
self._requests = 0
|
||||
|
||||
def __str__(self):
|
||||
return '<Album %s %r>' % (self.id, self.metadata[u"album"])
|
||||
|
||||
def _parse_release(self, document):
|
||||
"""Make album object from a parsed XML document."""
|
||||
m = self._new_metadata
|
||||
|
||||
release_node = document.metadata[0].release[0]
|
||||
release_to_metadata(release_node, m)
|
||||
run_album_metadata_processors(self, m, release_node)
|
||||
|
||||
for i, node in enumerate(release_node.track_list[0].track):
|
||||
t = Track(node.attribs['id'], self)
|
||||
self._new_tracks.append(t)
|
||||
tm = t.metadata
|
||||
tm.copy(m)
|
||||
track_to_metadata(node, tm)
|
||||
tm['tracknumber'] = str(i + 1)
|
||||
run_track_metadata_processors(self, m, release_node, node)
|
||||
|
||||
def _release_request_finished(self, document, http, error):
|
||||
try:
|
||||
if error:
|
||||
self.log.error(unicode(http.errorString()))
|
||||
else:
|
||||
try:
|
||||
self._parse_release(document)
|
||||
except:
|
||||
error = True
|
||||
self.log.error(traceback.format_exc())
|
||||
finally:
|
||||
self._requests -= 1
|
||||
self._finalize_loading(error)
|
||||
|
||||
def _finalize_loading(self, error):
|
||||
if error:
|
||||
self.metadata.clear()
|
||||
self.metadata['album'] = _("[couldn't load album %s]") % self.id
|
||||
del self._new_metadata
|
||||
del self._new_tracks
|
||||
self.update()
|
||||
else:
|
||||
if not self._requests:
|
||||
self.metadata = self._new_metadata
|
||||
self.tracks = self._new_tracks
|
||||
del self._new_metadata
|
||||
del self._new_tracks
|
||||
self.loaded = True
|
||||
self.update()
|
||||
self.match_files(self.unmatched_files.files)
|
||||
|
||||
def load(self, force=False):
|
||||
if self._requests:
|
||||
self.log.info("Not reloading, some requests are still active.")
|
||||
return
|
||||
self.loaded = False
|
||||
self.metadata.clear()
|
||||
self.metadata['album'] = _("[loading album information]")
|
||||
self.update()
|
||||
self._new_metadata = Metadata()
|
||||
self._new_tracks = []
|
||||
self._requests = 1
|
||||
self.tagger.xmlws.get_release_by_id(self.id, self._release_request_finished, inc=('tracks','artist'))
|
||||
|
||||
def update(self, update_tracks=True):
|
||||
self.tagger.emit(QtCore.SIGNAL("album_updated"), self, update_tracks)
|
||||
|
||||
def load_(self, force=False):
|
||||
self.tagger.window.set_statusbar_message('Loading release %s...', self.id)
|
||||
|
||||
ws = self.tagger.get_web_service(cached=not force)
|
||||
@@ -125,82 +184,13 @@ class Album(DataObject, Item):
|
||||
self.metadata["~#length"] = duration
|
||||
self.metadata["~length"] = format_time(duration)
|
||||
|
||||
@needs_read_lock
|
||||
def getNumTracks(self):
|
||||
return len(self.tracks)
|
||||
def _add_file(self, track, file):
|
||||
self._files += 1
|
||||
self.update(False)
|
||||
|
||||
@needs_write_lock
|
||||
def addUnmatchedFile(self, file):
|
||||
self.unmatched_files.append(file)
|
||||
self.emit(QtCore.SIGNAL("fileAdded(int)"), file.id)
|
||||
|
||||
@needs_write_lock
|
||||
def addLinkedFile(self, track, file):
|
||||
index = self.tracks.index(track)
|
||||
self.files.append(file)
|
||||
self.emit(QtCore.SIGNAL("track_updated"), track)
|
||||
|
||||
@needs_write_lock
|
||||
def removeLinkedFile(self, track, file):
|
||||
self.emit(QtCore.SIGNAL("track_updated"), track)
|
||||
|
||||
@needs_read_lock
|
||||
def getNumUnmatchedFiles(self):
|
||||
return len(self.unmatched_files)
|
||||
|
||||
@needs_read_lock
|
||||
def getNumTracks(self):
|
||||
return len(self.tracks)
|
||||
|
||||
@needs_read_lock
|
||||
def getNumLinkedFiles(self):
|
||||
count = 0
|
||||
for track in self.tracks:
|
||||
if track.is_linked():
|
||||
count += 1
|
||||
return count
|
||||
|
||||
@needs_write_lock
|
||||
def remove_file(self, file):
|
||||
index = self.unmatched_files.index(file)
|
||||
self.emit(QtCore.SIGNAL("fileAboutToBeRemoved"), index)
|
||||
# self.test = self.unmatched_files[index]
|
||||
del self.unmatched_files[index]
|
||||
print self.unmatched_files
|
||||
self.emit(QtCore.SIGNAL("fileRemoved"), index)
|
||||
|
||||
def matchFile(self, file):
|
||||
bestMatch = 0.0, None
|
||||
for track in self.tracks:
|
||||
sim = file.orig_metadata.compare(track.metadata)
|
||||
if sim > bestMatch[0]:
|
||||
bestMatch = sim, track
|
||||
|
||||
if bestMatch[1]:
|
||||
file.move_to_track(bestMatch[1])
|
||||
|
||||
@needs_read_lock
|
||||
def can_save(self):
|
||||
"""Return if this object can be saved."""
|
||||
if self.files:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def can_remove(self):
|
||||
"""Return if this object can be removed."""
|
||||
return True
|
||||
|
||||
def can_edit_tags(self):
|
||||
"""Return if this object supports tag editing."""
|
||||
return False
|
||||
|
||||
def can_analyze(self):
|
||||
"""Return if this object can be fingerprinted."""
|
||||
return False
|
||||
|
||||
def can_refresh(self):
|
||||
return True
|
||||
def _remove_file(self, track, file):
|
||||
self._files -= 1
|
||||
self.update(False)
|
||||
|
||||
def match_files(self, files):
|
||||
"""Match files on tracks on this album, based on metadata similarity."""
|
||||
@@ -219,8 +209,11 @@ class Album(DataObject, Item):
|
||||
if track.linked_file and sim < track.linked_file.similarity:
|
||||
continue
|
||||
matched[file] = track
|
||||
unmatched = [f for f in files if f not in matched]
|
||||
for file, track in matched.items():
|
||||
file.move(track)
|
||||
for file in unmatched:
|
||||
file.move(self.unmatched_files)
|
||||
|
||||
def match_file(self, file, trackid=None):
|
||||
"""Match the file on a track on this album, based on trackid or metadata similarity."""
|
||||
@@ -231,10 +224,25 @@ class Album(DataObject, Item):
|
||||
return
|
||||
self.match_files([file])
|
||||
|
||||
def can_save(self):
|
||||
return self._files > 0
|
||||
|
||||
def can_remove(self):
|
||||
return True
|
||||
|
||||
def can_edit_tags(self):
|
||||
return False
|
||||
|
||||
def can_analyze(self):
|
||||
return False
|
||||
|
||||
def can_refresh(self):
|
||||
return True
|
||||
|
||||
def column(self, column):
|
||||
if column == 'title':
|
||||
if self.getNumTracks():
|
||||
return '%s (%d/%d)' % (self.metadata['album'], self.getNumTracks(), self.getNumLinkedFiles())
|
||||
if self.tracks:
|
||||
return '%s (%d/%d)' % (self.metadata['album'], len(self.tracks), self._files)
|
||||
else:
|
||||
return self.metadata['album']
|
||||
elif column == '~length':
|
||||
|
||||
@@ -43,6 +43,9 @@ class Cluster(QtCore.QObject, Item):
|
||||
def __repr__(self):
|
||||
return '<Cluster %r>' % self.metadata['album']
|
||||
|
||||
def __len__(self):
|
||||
return len(self.files)
|
||||
|
||||
def add_file(self, file):
|
||||
self.metadata['totaltracks'] += 1
|
||||
self.metadata['~#length'] += file.metadata['~#length']
|
||||
|
||||
66
picard/mbxml.py
Normal file
66
picard/mbxml.py
Normal file
@@ -0,0 +1,66 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Picard, the next-generation MusicBrainz tagger
|
||||
# Copyright (C) 2007 Lukáš Lalinský
|
||||
#
|
||||
# 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 picard.util import format_time
|
||||
|
||||
|
||||
def artist_to_metadata(node, m, release=False):
|
||||
m['musicbrainz_artistid'] = node.attribs['id']
|
||||
if release:
|
||||
m['musicbrainz_albumartistid'] = m['musicbrainz_artistid']
|
||||
for name, nodes in node.children.iteritems():
|
||||
if not nodes:
|
||||
continue
|
||||
if name == 'name':
|
||||
m['artist'] = nodes[0].text
|
||||
if release:
|
||||
m['albumartist'] = m['artist']
|
||||
elif name == 'sort_name':
|
||||
m['artistsort'] = nodes[0].text
|
||||
if release:
|
||||
m['albumartistsort'] = m['artistsort']
|
||||
|
||||
|
||||
def track_to_metadata(node, m):
|
||||
m['musicbrainz_trackid'] = node.attribs['id']
|
||||
for name, nodes in node.children.iteritems():
|
||||
if not nodes:
|
||||
continue
|
||||
if name == 'title':
|
||||
m['title'] = nodes[0].text
|
||||
elif name == 'duration':
|
||||
m['~#length'] = int(nodes[0].text)
|
||||
elif name == 'artist':
|
||||
artist_to_metadata(nodes[0], m)
|
||||
if '~#length' not in m:
|
||||
m['~#length'] = 0
|
||||
m['~length'] = format_time(m['~#length'])
|
||||
|
||||
|
||||
def release_to_metadata(node, m):
|
||||
m['musicbrainz_albumid'] = node.attribs['id']
|
||||
for name, nodes in node.children.iteritems():
|
||||
if not nodes:
|
||||
continue
|
||||
if name == 'title':
|
||||
m['album'] = nodes[0].text
|
||||
elif name == 'asin':
|
||||
m['asin'] = nodes[0].text
|
||||
elif name == 'artist':
|
||||
artist_to_metadata(nodes[0], m, True)
|
||||
@@ -74,6 +74,7 @@ from picard.util import (
|
||||
from picard.util.cachedws import CachedWebService
|
||||
from picard.util.search import LuceneQueryFilter
|
||||
from picard.util.thread import ThreadAssist
|
||||
from picard.webservice import XmlWebService
|
||||
|
||||
from musicbrainz2.disc import readDisc, getSubmissionUrl, DiscError
|
||||
from musicbrainz2.utils import extractUuid
|
||||
@@ -102,7 +103,7 @@ class Tagger(QtGui.QApplication):
|
||||
- cluster_removed(album, index)
|
||||
|
||||
- album_added(album)
|
||||
- album_updated(album)
|
||||
- album_updated(album, update_tracks)
|
||||
- album_removed(album, index)
|
||||
|
||||
- track_updated(track)
|
||||
@@ -140,6 +141,8 @@ class Tagger(QtGui.QApplication):
|
||||
self.thread_assist = ThreadAssist(self)
|
||||
self.load_thread = self.thread_assist.allocate()
|
||||
|
||||
self.xmlws = XmlWebService(self.cachedir)
|
||||
|
||||
# Initialize fingerprinting
|
||||
self._ofa = musicdns.OFA()
|
||||
self._analyze_thread = self.thread_assist.allocate()
|
||||
@@ -152,8 +155,6 @@ class Tagger(QtGui.QApplication):
|
||||
|
||||
self.puidmanager = PUIDManager()
|
||||
|
||||
self.__files_to_be_moved = []
|
||||
|
||||
self.browser_integration = BrowserIntegration()
|
||||
|
||||
self.files = {}
|
||||
@@ -239,8 +240,8 @@ class Tagger(QtGui.QApplication):
|
||||
if album.loaded:
|
||||
album.match_files(files)
|
||||
else:
|
||||
for file in files:
|
||||
self.__files_to_be_moved.append((file, album))
|
||||
for file in [file for file in files]:
|
||||
file.move(album.unmatched_files)
|
||||
|
||||
def move_file_to_album(self, file, albumid=None, album=None):
|
||||
"""Move `file` to a track on album `albumid`."""
|
||||
@@ -252,7 +253,7 @@ class Tagger(QtGui.QApplication):
|
||||
if album.loaded:
|
||||
album.match_file(file, trackid)
|
||||
else:
|
||||
self.__files_to_be_moved.append((file, album, trackid))
|
||||
file.move(album.unmatched_files)
|
||||
|
||||
def exit(self):
|
||||
self.stopping = True
|
||||
@@ -486,34 +487,19 @@ class Tagger(QtGui.QApplication):
|
||||
self.remove_files(files)
|
||||
|
||||
def load_album(self, id):
|
||||
"""Load an album specified by MusicBrainz ID."""
|
||||
album = self.get_album_by_id(id)
|
||||
if album:
|
||||
return album
|
||||
album = Album(id, _("[loading album information]"))
|
||||
album = Album(id)
|
||||
self.albums.append(album)
|
||||
self.emit(QtCore.SIGNAL("album_added"), album)
|
||||
self.thread_assist.spawn(self.__load_album_thread, album)
|
||||
album.load()
|
||||
return album
|
||||
|
||||
def reload_album(self, album):
|
||||
album.name = _("[loading album information]")
|
||||
self.emit(QtCore.SIGNAL("album_updated"), album)
|
||||
self.thread_assist.spawn(self.__load_album_thread, album, True)
|
||||
album.load(force=True)
|
||||
|
||||
def __load_album_thread(self, album, force=False):
|
||||
try:
|
||||
album.load(force)
|
||||
except Exception, e:
|
||||
self.log.error(traceback.format_exc())
|
||||
self.window.set_statusbar_message('Loading release failed: %s', e, timeout=3000)
|
||||
self.thread_assist.proxy_to_main(self.__load_album_failed, album)
|
||||
else:
|
||||
self.thread_assist.proxy_to_main(self.__load_album_finished, album)
|
||||
|
||||
def __load_album_finished(self, album):
|
||||
self.emit(QtCore.SIGNAL("album_updated"), album)
|
||||
album.loaded = True
|
||||
def finalize_album_loading(self, album):
|
||||
for item in self.__files_to_be_moved:
|
||||
if item[1] == album:
|
||||
if len(item) == 3:
|
||||
@@ -521,10 +507,6 @@ class Tagger(QtGui.QApplication):
|
||||
else:
|
||||
item[1].match_file(item[0])
|
||||
|
||||
def __load_album_failed(self, album):
|
||||
album.metadata['album'] = _("[couldn't load release %s]") % album.id
|
||||
self.emit(QtCore.SIGNAL("album_updated"), album)
|
||||
|
||||
def get_album_by_id(self, id):
|
||||
for album in self.albums:
|
||||
if album.id == id:
|
||||
|
||||
@@ -33,31 +33,33 @@ class Track(DataObject):
|
||||
self.metadata = Metadata()
|
||||
|
||||
def __str__(self):
|
||||
return '<Track %s %r>' % (self.id, self.metadata[u"title"])
|
||||
return '<Track %s %r>' % (self.id, self.metadata["title"])
|
||||
|
||||
def add_file(self, file):
|
||||
if self.linked_file:
|
||||
self.linked_file.move(self.tagger.unmatched_files)
|
||||
self.linked_file.move(self.album.unmatched_files)
|
||||
self.linked_file = file
|
||||
file.saved_metadata.copy(file.metadata)
|
||||
file.metadata.copy(self.metadata)
|
||||
if 'musicip_puid' in file.saved_metadata:
|
||||
file.metadata['musicip_puid'] = file.saved_metadata['musicip_puid']
|
||||
file.metadata.changed = True
|
||||
self.album.addLinkedFile(self, file)
|
||||
self.album._add_file(self, file)
|
||||
file.update(signal=False)
|
||||
self.tagger.emit(QtCore.SIGNAL("track_updated"), self)
|
||||
self.update()
|
||||
|
||||
def remove_file(self, file):
|
||||
file = self.linked_file
|
||||
file.metadata.copy(file.saved_metadata)
|
||||
self.linked_file = None
|
||||
self.album.removeLinkedFile(self, file)
|
||||
self.tagger.emit(QtCore.SIGNAL("track_updated"), self)
|
||||
self.album._remove_file(self, file)
|
||||
self.update()
|
||||
return file
|
||||
|
||||
def update_file(self, file):
|
||||
assert file == self.linked_file
|
||||
self.update()
|
||||
|
||||
def update(self):
|
||||
self.tagger.emit(QtCore.SIGNAL("track_updated"), self)
|
||||
|
||||
def is_linked(self):
|
||||
|
||||
@@ -109,7 +109,7 @@ class MainPanel(QtGui.QSplitter):
|
||||
|
||||
def unregister_object(self, obj=None, item=None):
|
||||
if obj is None and item is not None:
|
||||
obj = self.item_from_object(item)
|
||||
obj = self.object_from_item(item)
|
||||
if obj is not None and item is None:
|
||||
item = self.item_from_object(obj)
|
||||
del self._object_to_item[obj]
|
||||
@@ -147,12 +147,16 @@ class MainPanel(QtGui.QSplitter):
|
||||
self.register_object(file, item)
|
||||
self.update_file(file, item)
|
||||
self.update_cluster(cluster, cluster_item)
|
||||
if cluster.special == 2 and cluster.files:
|
||||
cluster_item.setHidden(False)
|
||||
|
||||
def remove_file_from_cluster(self, cluster, file, index):
|
||||
cluster_item = self.item_from_object(cluster)
|
||||
cluster_item.takeChild(index)
|
||||
self.unregister_object(file)
|
||||
self.update_cluster(cluster, cluster_item)
|
||||
if cluster.special == 2 and not cluster.files:
|
||||
cluster_item.setHidden(True)
|
||||
|
||||
|
||||
class BaseTreeView(QtGui.QTreeWidget):
|
||||
@@ -317,6 +321,20 @@ class BaseTreeView(QtGui.QTreeWidget):
|
||||
if obj.can_edit_tags():
|
||||
self.window.edit_tags(obj)
|
||||
|
||||
def add_cluster(self, cluster, parent_item=None):
|
||||
if parent_item is None:
|
||||
parent_item = self.clusters
|
||||
cluster_item = QtGui.QTreeWidgetItem(parent_item)
|
||||
cluster_item.setIcon(0, self.panel.icon_dir)
|
||||
self.panel.update_cluster(cluster, cluster_item)
|
||||
self.panel.register_object(cluster, cluster_item)
|
||||
for file in cluster.files:
|
||||
item = QtGui.QTreeWidgetItem(cluster_item)
|
||||
self.panel.register_object(file, item)
|
||||
self.panel.update_file(file, item)
|
||||
if cluster.special == 2 and not cluster.files:
|
||||
cluster_item.setHidden(True)
|
||||
|
||||
|
||||
class FileTreeView(BaseTreeView):
|
||||
|
||||
@@ -335,16 +353,6 @@ class FileTreeView(BaseTreeView):
|
||||
self.connect(self.tagger, QtCore.SIGNAL("cluster_added"), self.add_cluster)
|
||||
self.connect(self.tagger, QtCore.SIGNAL("cluster_removed"), self.remove_cluster)
|
||||
|
||||
def add_cluster(self, cluster):
|
||||
cluster_item = QtGui.QTreeWidgetItem(self.clusters)
|
||||
cluster_item.setIcon(0, self.panel.icon_dir)
|
||||
self.panel.update_cluster(cluster, cluster_item)
|
||||
self.panel.register_object(cluster, cluster_item)
|
||||
for file in cluster.files:
|
||||
item = QtGui.QTreeWidgetItem(cluster_item)
|
||||
self.panel.register_object(file, item)
|
||||
self.update_file(file, item)
|
||||
|
||||
def remove_cluster(self, cluster, index):
|
||||
for file in cluster.files:
|
||||
self.panel.unregister_object(file)
|
||||
@@ -406,6 +414,7 @@ class AlbumTreeView(BaseTreeView):
|
||||
font.setBold(True)
|
||||
item.setFont(i, font)
|
||||
item.setText(i, album.column(column[1]))
|
||||
self.add_cluster(album.unmatched_files, item)
|
||||
|
||||
def update_album(self, album, update_tracks=True):
|
||||
album_item = self.panel.item_from_object(album)
|
||||
@@ -419,6 +428,7 @@ class AlbumTreeView(BaseTreeView):
|
||||
item = QtGui.QTreeWidgetItem(album_item)
|
||||
self.panel.register_object(track, item)
|
||||
self.update_track(track, item)
|
||||
self.add_cluster(album.unmatched_files, album_item)
|
||||
|
||||
def remove_album(self, album, index):
|
||||
self.panel.unregister_object(album)
|
||||
|
||||
164
picard/webservice.py
Normal file
164
picard/webservice.py
Normal file
@@ -0,0 +1,164 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Picard, the next-generation MusicBrainz tagger
|
||||
# Copyright (C) 2007 Lukáš Lalinský
|
||||
#
|
||||
# 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.
|
||||
|
||||
|
||||
"""
|
||||
Asynchronous XML web service.
|
||||
"""
|
||||
|
||||
import re
|
||||
import sha
|
||||
from PyQt4 import QtCore, QtNetwork, QtXml
|
||||
from picard import version_string
|
||||
|
||||
|
||||
def _node_name(name):
|
||||
return re.sub('[^a-zA-Z0-9]', '_', unicode(name))
|
||||
|
||||
|
||||
class XmlNode(object):
|
||||
|
||||
def __init__(self):
|
||||
self.text = u''
|
||||
self.children = {}
|
||||
self.attribs = {}
|
||||
|
||||
def __repr__(self):
|
||||
return repr(self.__dict__)
|
||||
|
||||
def __getattr__(self, name):
|
||||
try:
|
||||
return self.children[name]
|
||||
except KeyError:
|
||||
try:
|
||||
return self.attribs[name]
|
||||
except KeyError:
|
||||
raise AttributeError, name
|
||||
|
||||
|
||||
class XmlHandler(QtXml.QXmlDefaultHandler):
|
||||
|
||||
def init(self):
|
||||
self.document = XmlNode()
|
||||
self.node = self.document
|
||||
self.path = []
|
||||
|
||||
def startElement(self, namespace, name, qname, attrs):
|
||||
node = XmlNode()
|
||||
for i in xrange(attrs.count()):
|
||||
node.attribs[_node_name(attrs.localName(i))] = unicode(attrs.value(i))
|
||||
self.node.children.setdefault(_node_name(name), []).append(node)
|
||||
self.path.append(self.node)
|
||||
self.node = node
|
||||
return True
|
||||
|
||||
def endElement(self, namespace, name, qname):
|
||||
self.node = self.path.pop()
|
||||
return True
|
||||
|
||||
def characters(self, text):
|
||||
self.node.text += unicode(text)
|
||||
return True
|
||||
|
||||
|
||||
class XmlWebService(QtNetwork.QHttp):
|
||||
|
||||
def __init__(self, cachedir, parent=None):
|
||||
QtNetwork.QHttp.__init__(self, parent)
|
||||
self.connect(self, QtCore.SIGNAL("requestStarted(int)"), self._start_request)
|
||||
self.connect(self, QtCore.SIGNAL("requestFinished(int, bool)"), self._finish_request)
|
||||
self.connect(self, QtCore.SIGNAL("readyRead(const QHttpResponseHeader &)"), self._read_data)
|
||||
self._cachedir = cachedir
|
||||
self._request_handlers = {}
|
||||
self._xml_handler = XmlHandler()
|
||||
self._xml_reader = QtXml.QXmlSimpleReader()
|
||||
self._xml_reader.setContentHandler(self._xml_handler)
|
||||
self._xml_input = QtXml.QXmlInputSource()
|
||||
self._using_proxy = False
|
||||
|
||||
def _make_cache_filename(self, host, port, path):
|
||||
url = "%s:%d%s" % (host, port, path)
|
||||
filename = sha.new(url).hexdigest()
|
||||
m = re.search(r"\.([a-z]{2,3})(?:\?|$)", url)
|
||||
if m:
|
||||
filename += "." + m.group(1)
|
||||
return os.path.join(self._cachedir, filename)
|
||||
|
||||
def _start_request(self, request_id):
|
||||
if request_id in self._request_handlers:
|
||||
self._xml_handler.init()
|
||||
self._new_request = True
|
||||
|
||||
def _finish_request(self, request_id, error):
|
||||
try:
|
||||
handler = self._request_handlers[request_id]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
if handler is not None:
|
||||
handler(self._xml_handler.document, self, error)
|
||||
del self._request_handlers[request_id]
|
||||
|
||||
def _read_data(self, response):
|
||||
if response.statusCode() != 200:
|
||||
self.abort()
|
||||
else:
|
||||
self._xml_input.setData(self.readAll())
|
||||
if self._new_request:
|
||||
ok = self._xml_reader.parse(self._xml_input, True)
|
||||
self._new_request = False
|
||||
else:
|
||||
ok = self._xml_reader.parseContinue()
|
||||
if not ok:
|
||||
self.abort()
|
||||
|
||||
def _prepare(self, method, host, port, path):
|
||||
self.log.debug("%s http://%s:%d%s", method, host, port, path)
|
||||
header = QtNetwork.QHttpRequestHeader(method, path)
|
||||
if port == 80:
|
||||
header.setValue("Host", "%s" % host)
|
||||
else:
|
||||
header.setValue("Host", "%s:%d" % (host, port))
|
||||
header.setValue("User-Agent", "MusicBrainz Picard/%s" % version_string)
|
||||
if method == "POST":
|
||||
header.setContentType("application/x-www-form-urlencoded")
|
||||
if self.config.setting["use_proxy"]:
|
||||
self.setProxy(self.config.setting["proxy_server_host"], self.config.setting["proxy_server_port"], self.config.setting["proxy_username"], self.config.setting["proxy_password"])
|
||||
self._using_proxy = True
|
||||
elif self._using_proxy:
|
||||
self.setProxy(QtCore.QString(), QtCore.QString())
|
||||
self._using_proxy = False
|
||||
self.setHost(host, port)
|
||||
return header
|
||||
|
||||
def get(self, host, port, path, handler):
|
||||
header = self._prepare("GET", host, port, path)
|
||||
requestid = self.request(header)
|
||||
self._request_handlers[requestid] = handler
|
||||
|
||||
def post(self, host, port, path, data, handler):
|
||||
header = self._prepare("POST", host, port, path)
|
||||
requestid = self.request(header, data)
|
||||
self._request_handlers[requestid] = handler
|
||||
|
||||
def get_release_by_id(self, releaseid, handler, inc=[]):
|
||||
host = self.config.setting["server_host"]
|
||||
port = self.config.setting["server_port"]
|
||||
path = "/ws/1/release/%s?type=xml&inc=%s" % (releaseid, "+".join(inc))
|
||||
self.get(host, port, path, handler)
|
||||
Reference in New Issue
Block a user