* Add encoding using Xiph.org 'flac' program. This adds a FlacEncodeTask that encodes wave files to flac files. This commit also replaces morituri's EncodeTask with FlacEncodeTask, however, in morituri, EncodeTask also does the tagging. FlacEncodeTask will not perform the tagging. So we will need an extra task for the tagging - this will be added soon. Meanwhile, do not merge this commit to master yet. * Add tagging using mutagen. Replace the gstreamer tagging code with mutagen tagging code. getTagList is rewritten to return a dictionary of tags, which are then simply passed to mutagen. The way it is set up right now is not the best - I don't think it makes sense for tagging to take place in program/cdparanoia.py ; but this is how the current code did it. I suggest that, when we rip out all the gstreamer code, we also move the tagging to a more sensible place; and then also make the tagging not be an actual 'task.Task'. * Add gstreamer-less CRC32 version Only works on wave files at this point. Should not be a problem, I think. * Use proper musicbrainz tags and ALBUM tag. * Add mutagen to .travis.yml
334 lines
10 KiB
Python
334 lines
10 KiB
Python
# -*- Mode: Python -*-
|
|
# vi:si:et:sw=4:sts=4:ts=4
|
|
|
|
# Morituri - for those about to RIP
|
|
|
|
# Copyright (C) 2009 Thomas Vander Stichele
|
|
|
|
# This file is part of morituri.
|
|
#
|
|
# morituri 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 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# morituri 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 morituri. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
import argparse
|
|
import sys
|
|
|
|
from morituri.command.basecommand import BaseCommand
|
|
from morituri.common import cache, task
|
|
from morituri.result import result
|
|
|
|
import logging
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class RCCue(BaseCommand):
|
|
summary = "write a cue file for the cached result"
|
|
description = summary
|
|
|
|
def do(self, args):
|
|
self._cache = cache.ResultCache()
|
|
|
|
try:
|
|
discid = args[0]
|
|
except IndexError:
|
|
sys.stderr.write(
|
|
'Please specify a cddb disc id\n')
|
|
return 3
|
|
|
|
persisted = self._cache.getRipResult(discid, create=False)
|
|
|
|
if not persisted:
|
|
sys.stderr.write(
|
|
'Could not find a result for cddb disc id %s\n' % discid)
|
|
return 3
|
|
|
|
sys.stdout.write(persisted.object.table.cue().encode('utf-8'))
|
|
|
|
|
|
class RCList(BaseCommand):
|
|
summary = "list cached results"
|
|
description = summary
|
|
|
|
def do(self, args):
|
|
self._cache = cache.ResultCache()
|
|
results = []
|
|
|
|
for i in self._cache.getIds():
|
|
r = self._cache.getRipResult(i, create=False)
|
|
results.append((r.object.artist, r.object.title, i))
|
|
|
|
results.sort()
|
|
|
|
for artist, title, cddbid in results:
|
|
if artist is None:
|
|
artist = '(None)'
|
|
if title is None:
|
|
title = '(None)'
|
|
|
|
sys.stdout.write('%s: %s - %s\n' % (
|
|
cddbid, artist.encode('utf-8'), title.encode('utf-8')))
|
|
|
|
|
|
class RCLog(BaseCommand):
|
|
summary = "write a log file for the cached result"
|
|
description = summary
|
|
formatter_class = argparse.ArgumentDefaultsHelpFormatter
|
|
|
|
def add_arguments(self):
|
|
loggers = result.getLoggers().keys()
|
|
|
|
self.parser.add_argument(
|
|
'-L', '--logger',
|
|
action="store", dest="logger",
|
|
default='morituri',
|
|
help="logger to use (choose from '" + "', '".join(loggers) + "')"
|
|
)
|
|
|
|
def do(self, args):
|
|
self._cache = cache.ResultCache()
|
|
|
|
persisted = self._cache.getRipResult(args[0], create=False)
|
|
|
|
if not persisted:
|
|
sys.stderr.write(
|
|
'Could not find a result for cddb disc id %s\n' % args[0])
|
|
return 3
|
|
|
|
try:
|
|
klazz = result.getLoggers()[self.options.logger]
|
|
except KeyError:
|
|
sys.stderr.write("No logger named %s found!\n" % (
|
|
self.options.logger))
|
|
return 3
|
|
|
|
logger = klazz()
|
|
sys.stdout.write(logger.log(persisted.object).encode('utf-8'))
|
|
|
|
|
|
class ResultCache(BaseCommand):
|
|
summary = "debug result cache"
|
|
description = summary
|
|
|
|
subcommands = {
|
|
'cue': RCCue,
|
|
'list': RCList,
|
|
'log': RCLog,
|
|
}
|
|
|
|
|
|
class Checksum(BaseCommand):
|
|
summary = "run a checksum task"
|
|
description = summary
|
|
|
|
def add_arguments(self):
|
|
self.parser.add_argument('files', nargs='+', action='store',
|
|
help="audio files to checksum")
|
|
|
|
def do(self):
|
|
runner = task.SyncRunner()
|
|
# here to avoid import gst eating our options
|
|
from morituri.common import checksum
|
|
|
|
for f in self.options.files:
|
|
fromPath = unicode(f)
|
|
checksumtask = checksum.CRC32Task(fromPath)
|
|
runner.run(checksumtask)
|
|
sys.stdout.write('Checksum: %08x\n' % checksumtask.checksum)
|
|
|
|
|
|
class Encode(BaseCommand):
|
|
summary = "run an encode task"
|
|
description = summary
|
|
|
|
def add_arguments(self):
|
|
# here to avoid import gst eating our options
|
|
from morituri.common import encode
|
|
|
|
default = 'flac'
|
|
# slated for deletion as flac will be the only encoder
|
|
self.parser.add_argument('--profile',
|
|
action="store",
|
|
dest="profile",
|
|
help="profile for encoding (default '%s', choices '%s')" % (
|
|
default, "', '".join(encode.ALL_PROFILES.keys())),
|
|
default=default)
|
|
self.parser.add_argument('input', action='store',
|
|
help="audio file to encode")
|
|
self.parser.add_argument('output', nargs='?', action='store',
|
|
help="output path")
|
|
|
|
def do(self):
|
|
from morituri.common import encode
|
|
profile = encode.ALL_PROFILES[self.options.profile]()
|
|
|
|
try:
|
|
fromPath = unicode(self.options.input)
|
|
except IndexError:
|
|
# unexercised after BaseCommand
|
|
sys.stdout.write('Please specify an input file.\n')
|
|
return 3
|
|
|
|
try:
|
|
toPath = unicode(self.options.output)
|
|
except IndexError:
|
|
toPath = fromPath + '.' + profile.extension
|
|
|
|
runner = task.SyncRunner()
|
|
|
|
logger.debug('Encoding %s to %s',
|
|
fromPath.encode('utf-8'),
|
|
toPath.encode('utf-8'))
|
|
encodetask = encode.FlacEncodeTask(fromPath, toPath)
|
|
|
|
runner.run(encodetask)
|
|
|
|
sys.stdout.write('Peak level: %r\n' % encodetask.peak)
|
|
sys.stdout.write('Encoded to %s\n' % toPath.encode('utf-8'))
|
|
|
|
|
|
class MaxSample(BaseCommand):
|
|
summary = "run a max sample task"
|
|
description = summary
|
|
|
|
def add_arguments(self):
|
|
self.parser.add_argument('files', nargs='+', action='store',
|
|
help="audio files to sample")
|
|
|
|
def do(self):
|
|
runner = task.SyncRunner()
|
|
# here to avoid import gst eating our options
|
|
from morituri.common import checksum
|
|
|
|
for arg in self.options.files:
|
|
fromPath = unicode(arg.decode('utf-8'))
|
|
|
|
checksumtask = checksum.MaxSampleTask(fromPath)
|
|
|
|
runner.run(checksumtask)
|
|
|
|
sys.stdout.write('%s\n' % arg)
|
|
sys.stdout.write('Biggest absolute sample: %04x\n' %
|
|
checksumtask.checksum)
|
|
|
|
|
|
class Tag(BaseCommand):
|
|
summary = "run a tag reading task"
|
|
description = summary
|
|
|
|
def add_arguments(self):
|
|
self.parser.add_argument('file', action='store',
|
|
help="audio file to tag")
|
|
|
|
def do(self):
|
|
try:
|
|
path = unicode(self.options.file)
|
|
except IndexError:
|
|
sys.stdout.write('Please specify an input file.\n')
|
|
return 3
|
|
|
|
runner = task.SyncRunner()
|
|
|
|
from morituri.common import encode
|
|
logger.debug('Reading tags from %s' % path.encode('utf-8'))
|
|
tagtask = encode.TagReadTask(path)
|
|
|
|
runner.run(tagtask)
|
|
|
|
for key in tagtask.taglist.keys():
|
|
sys.stdout.write('%s: %r\n' % (key, tagtask.taglist[key]))
|
|
|
|
|
|
class MusicBrainzNGS(BaseCommand):
|
|
summary = "examine MusicBrainz NGS info"
|
|
description = """Look up a MusicBrainz disc id and output information.
|
|
|
|
You can get the MusicBrainz disc id with whipper cd info.
|
|
|
|
Example disc id: KnpGsLhvH.lPrNc1PBL21lb9Bg4-"""
|
|
|
|
def add_arguments(self):
|
|
self.parser.add_argument('mbdiscid', action='store',
|
|
help="MB disc id to look up")
|
|
|
|
def do(self):
|
|
try:
|
|
discId = unicode(self.options.mbdiscid)
|
|
except IndexError:
|
|
sys.stdout.write('Please specify a MusicBrainz disc id.\n')
|
|
return 3
|
|
|
|
from morituri.common import mbngs
|
|
metadatas = mbngs.musicbrainz(discId, record=self.options.record)
|
|
|
|
sys.stdout.write('%d releases\n' % len(metadatas))
|
|
for i, md in enumerate(metadatas):
|
|
sys.stdout.write('- Release %d:\n' % (i + 1, ))
|
|
sys.stdout.write(' Artist: %s\n' % md.artist.encode('utf-8'))
|
|
sys.stdout.write(' Title: %s\n' % md.title.encode('utf-8'))
|
|
sys.stdout.write(' Type: %s\n' % md.releaseType.encode('utf-8'))
|
|
sys.stdout.write(' URL: %s\n' % md.url)
|
|
sys.stdout.write(' Tracks: %d\n' % len(md.tracks))
|
|
if md.catalogNumber:
|
|
sys.stdout.write(' Cat no: %s\n' % md.catalogNumber)
|
|
if md.barcode:
|
|
sys.stdout.write(' Barcode: %s\n' % md.barcode)
|
|
|
|
for j, track in enumerate(md.tracks):
|
|
sys.stdout.write(' Track %2d: %s - %s\n' % (
|
|
j + 1, track.artist.encode('utf-8'),
|
|
track.title.encode('utf-8')))
|
|
|
|
|
|
class CDParanoia(BaseCommand):
|
|
summary = "show cdparanoia version"
|
|
description = summary
|
|
|
|
def do(self):
|
|
from morituri.program import cdparanoia
|
|
version = cdparanoia.getCdParanoiaVersion()
|
|
sys.stdout.write("cdparanoia version: %s\n" % version)
|
|
|
|
|
|
class CDRDAO(BaseCommand):
|
|
summary = "show cdrdao version"
|
|
description = summary
|
|
|
|
def do(self):
|
|
from morituri.program import cdrdao
|
|
version = cdrdao.getCDRDAOVersion()
|
|
sys.stdout.write("cdrdao version: %s\n" % version)
|
|
|
|
|
|
class Version(BaseCommand):
|
|
summary = "debug version getting"
|
|
description = summary
|
|
|
|
subcommands = {
|
|
'cdparanoia': CDParanoia,
|
|
'cdrdao': CDRDAO,
|
|
}
|
|
|
|
|
|
class Debug(BaseCommand):
|
|
summary = "debug internals"
|
|
description = "debug internals"
|
|
|
|
subcommands = {
|
|
'checksum': Checksum,
|
|
'encode': Encode,
|
|
'maxsample': MaxSample,
|
|
'tag': Tag,
|
|
'musicbrainzngs': MusicBrainzNGS,
|
|
'resultcache': ResultCache,
|
|
'version': Version,
|
|
}
|