diff --git a/ChangeLog b/ChangeLog index c091830..d7f4243 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,22 @@ +2011-08-14 Thomas Vander Stichele + + * morituri/test/release.c7d919f4-3ea0-4c4b-a230-b3605f069440.xml: + Add release data for Bettie Serveert, Lamprey + * morituri/test/test_common_program.py: + Add a test for parsing and getting the whole duration. + * morituri/common/common.py: + Add a method to format time. + * morituri/common/program.py: + Add duration to tracks and release metadatas. + When there are multiple matches, look up the closest in duration. + Make sure that multiple matches closest in duration contain same + artist and title. + Complain about the other ones. + * morituri/image/table.py: + Add a method to calculate a duration from the table. + * morituri/test/test_image_table.py: + Add a test for it. + 2011-08-13 Thomas Vander Stichele * morituri/rip/cd.py: diff --git a/morituri/common/common.py b/morituri/common/common.py index e6612ca..406df96 100644 --- a/morituri/common/common.py +++ b/morituri/common/common.py @@ -67,6 +67,46 @@ def framesToHMSF(frames): return "%02d:%02d:%02d.%02d" % (h, m, s, f) +def formatTime(seconds, fractional=3): + """ + Nicely format time in a human-readable format, like + HH:MM:SS.mmm + + If fractional is zero, no seconds will be shown. + If it is greater than 0, we will show seconds and fractions of seconds. + As a side consequence, there is no way to show seconds without fractions. + + @param seconds: the time in seconds to format. + @type seconds: int or float + @param fractional: how many digits to show for the fractional part of + seconds. + @type fractional: int + + @rtype: string + @returns: a nicely formatted time string. + """ + chunks = [] + + if seconds < 0: + chunks.append(('-')) + seconds = -seconds + + hour = 60 * 60 + hours = seconds / hour + seconds %= hour + + minute = 60 + minutes = seconds / minute + seconds %= minute + + chunk = '%02d:%02d' % (hours, minutes) + if fractional > 0: + chunk += ':%0*.*f' % (fractional + 3, fractional, seconds) + + chunks.append(chunk) + + return " ".join(chunks) + class Persister(object): """ I wrap an optional pickle to persist an object to disk. diff --git a/morituri/common/program.py b/morituri/common/program.py index 7ca846f..791f6a7 100644 --- a/morituri/common/program.py +++ b/morituri/common/program.py @@ -26,6 +26,7 @@ Common functionality and class for all programs using morituri. import os import urlparse +import time from morituri.common import common, log from morituri.result import result @@ -40,6 +41,7 @@ class MusicBrainzException(Exception): class TrackMetadata(object): artist = None title = None + duration = None # in ms class DiscMetadata(object): """ @@ -90,8 +92,12 @@ def getMetadata(release): metadata.mbidArtist = urlparse.urlparse(release.artist.id)[2].split("/")[-1] + duration = 0 + for t in release.tracks: track = TrackMetadata() + track.duration = t.duration + duration += t.duration if isSingleArtist or t.artist == None: track.artist = metadata.artist track.sortName = metadata.sortName @@ -106,6 +112,8 @@ def getMetadata(release): track.mbid = urlparse.urlparse(t.id)[2].split("/")[-1] metadata.tracks.append(track) + metadata.duration = duration + return metadata @@ -159,8 +167,10 @@ def musicbrainz(discid): md = getMetadata(release) if md: + log.debug('program', 'duration %r', md.duration) ret.append(md) + return ret class Program(log.Loggable): @@ -345,18 +355,54 @@ class Program(log.Loggable): ret = None metadatas = None - try: - metadatas = musicbrainz(mbdiscid) - except MusicBrainzException, e: + for i in range(0, 4): + try: + metadatas = musicbrainz(mbdiscid) + except MusicBrainzException, e: + print "Warning:", e + time.sleep(5) + continue + + if not metadatas: print "Error:", e print 'Continuing without metadata' if metadatas: + print 'Disc duration: %s' % common.formatTime( + ittoc.duration() / 1000.0) + print print 'Matching releases:' + deltas = {} for metadata in metadatas: print 'Artist : %s' % metadata.artist.encode('utf-8') print 'Title : %s' % metadata.title.encode('utf-8') + print 'Duration: %s' % common.formatTime( + metadata.duration / 1000.0) + + delta = abs(metadata.duration - ittoc.duration()) + if not delta in deltas: + deltas[delta] = [] + deltas[delta].append(metadata) + + # Select the release that most closely matches the duration. + lowest = min(deltas.keys()) + + # If we have multiple, make sure they match + metadatas = deltas[lowest] + if len(metadatas) > 1: + artist = metadatas[0].artist + title = metadatas[0].title + for metadata in metadatas: + assert artist == metadata.artist + assert title == metadata.title + + if (len(deltas.keys()) > 1): + print + print 'Picked closest match in duration.' + print 'Others may be wrong in musicbrainz, please correct.' + print 'Artist : %s' % artist + print 'Title : %s' % title # Select one of the returned releases. We just pick the first one. ret = metadatas[0] diff --git a/morituri/image/table.py b/morituri/image/table.py index 9b6522b..725fa7f 100644 --- a/morituri/image/table.py +++ b/morituri/image/table.py @@ -427,6 +427,14 @@ class Table(object, log.Loggable): return urlparse.urlunparse(( 'http', host, '/bare/cdlookup.html', '', query, '')) + def duration(self): + """ + Get an estimate of the duration in ms. + """ + values = self._getMusicBrainzValues() + leadout = values[2] + first = values[3] + return ((leadout - first) * 1000) / common.FRAMES_PER_SECOND def _getMusicBrainzValues(self): """ diff --git a/morituri/test/release.c7d919f4-3ea0-4c4b-a230-b3605f069440.xml b/morituri/test/release.c7d919f4-3ea0-4c4b-a230-b3605f069440.xml new file mode 100644 index 0000000..346c677 --- /dev/null +++ b/morituri/test/release.c7d919f4-3ea0-4c4b-a230-b3605f069440.xml @@ -0,0 +1,50 @@ + + + + + Lamprey + B00000581T + + Bettie ServeertBettie Serveert + + + + Keepsake378693 + + + Ray Ray Rain262106 + + + D. Feathers332626 + + + Re-Feel-It238240 + + + 21 Days203826 + + + Cybor*D241800 + + + Tell Me, Sad318333 + + + Crutches292373 + + + Something So Wild171466 + + + Totally Freaked Out250893 + + + Silent Spring272533 + + + + + + + diff --git a/morituri/test/test_common_program.py b/morituri/test/test_common_program.py index 1c72b01..3a5d0b3 100644 --- a/morituri/test/test_common_program.py +++ b/morituri/test/test_common_program.py @@ -110,3 +110,17 @@ class PathTestCase(unittest.TestCase): self.assertEquals(path, u'/tmp/Jeff Buckley/Grace') +class MetadataLengthTestCase(unittest.TestCase): + def testLamprey(self): + from musicbrainz2 import wsxml + + path = os.path.join(os.path.dirname(__file__), + 'release.c7d919f4-3ea0-4c4b-a230-b3605f069440.xml') + handle = open(path, "rb") + + reader = wsxml.MbXmlParser() + wsMetadata = reader.parse(handle) + release = wsMetadata.getRelease() + metadata = progam.getMetadata(release) + + self.assertEquals(metadata.duration, 2962889) diff --git a/morituri/test/test_image_table.py b/morituri/test/test_image_table.py index 7c810f3..43e59b2 100644 --- a/morituri/test/test_image_table.py +++ b/morituri/test/test_image_table.py @@ -52,6 +52,9 @@ class LadyhawkeTestCase(tcommon.TestCase): self.assertEquals(self.table.getAccurateRipURL(), "http://www.accuraterip.com/accuraterip/a/5/d/dBAR-012-0013bd5a-00b8d489-c60af50d.bin") + def testDuration(self): + self.assertEquals(self.table.duration(), 2609413) + class MusicBrainzTestCase(tcommon.TestCase): # example taken from http://musicbrainz.org/doc/DiscIDCalculation # disc is Ettella Diamant