ears-1.0.1/NEWS0000644000000000000000000000056111077433473013317 0ustar00usergroup000000000000001.0.1 (2008-09-24) ================== * Split ourselves off from the lastfmsubmitd package. Older history can be found in NEWS over there (before 1.0). Since peel is optionally a lastfmsubmitd client and uses its Python API, you'll still need to have it installed locally if you want to submit to Last.fm. * Fix sox invocation (-w is deprecated). ears-1.0.1/README0000644000000000000000000000473311077433473013505 0ustar00usergroup00000000000000Ears ==== This is a collection of programs that originally accumulated within my "lastfmsubmitd" package. They gather data from MusicBrainz and use it to rip CDs and/or submit to Last.fm. Querying ======== mbget will read the CD in your computer's CD-ROM drive, query MusicBrainz for its album information, and output it in YAML. This format is like the one lastfmsubmitd uses, but it does not contain the time of submission. Instead of reading from the CD-ROM drive, you can also specify an album with --discid or --albumid. If a list of track numbers is also specified, only those tracks will be printed. If you are really lazy and don't want to put the CD in, you can use mbfind, specify the album name with --album, and choose the album's MBID from that. (You can also try specifying --artist, but at the moment it does not really work and cannot be used in combination with --album.) Ripping CDs =========== peel, the CD ripper, reads a description of tracks to rip from standard input. So, here's the trivial case of ripping a CD: $ mbget | peel Replaying tracks from a CD ========================== lastcd will take a list of songs encoded in this format, add submission times to them as if they had all just been played in order, and write them to lastfmsubmitd's spool or stdout. Requirements ============ * python-musicbrainz, available from http://musicbrainz.org/doc/PythonMusicbrainz. Author and License ================== Copyright © 2006-2008 Decklin Foster . Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ears-1.0.1/doc/mbget.10000644000000000000000000000230711077433473014545 0ustar00usergroup00000000000000.TH lastfm 1 .SH NAME mbfind, mbget, mbsubmit \- interface to the MusicBrainz database .SH SYNOPSIS .B mbget .RI [ --device\ DEV ] .RI [ --discid\ ID ] .RI [ --albumid\ ID ] .RI [ --freedb ] .br .B mbfind .RI [ --artist\ ARTIST ] .RI [ --album\ ALBUM ] .RI [ --verbose ] .br .B mbsubmit .SH DESCRIPTION .B mbget prints a listing of the specified album (by default, the disc in /dev/cdrom), in either a serialized format compatible with .IR lastfmsubmitd (1) or a format compatible with FreeDB. .B mbfind lists albums in the MusicBrainz database matching the given criteria. .B mbsubmit opens a web browser window to submit the disc ID of the disc in /dev/cdrom to MusicBrainz. .SH OPTIONS .TP .B \-a, \-\-artist Specify the artist name to search for. .TP .B \-b, \-\-album Specify the album name to search for. .TP .B \-v, \-\-verbose Print all RDF responses read from the server (for debugging purposes). .TP .B \-d, \-\-device Specify the device to read a disc ID from. .TP .B \-i, \-\-discid Specify a disc ID; do not read from any devices. .TP .B \-a, \-\-albumid Specify an album ID; do not read from any devices. .TP .B \-f, \-\-freedb Print output in FreeDB format. .SH AUTHOR Decklin Foster . ears-1.0.1/lastcd0000755000000000000000000000367011077433473014024 0ustar00usergroup00000000000000#!/usr/bin/python import sys import os import time import getopt import lastfm import lastfm.client import lastfm.marshaller USAGE = """\ usage: lastcd [--stdout] [--debug] [--help]""" if __name__ == '__main__': shortopts = 'sdh' longopts = [ 'stdout', 'debug', 'help', ] try: opts, args = getopt.getopt(sys.argv[1:], shortopts, longopts) except getopt.GetoptError, e: print >>sys.stderr, 'lastcd: %s' % e print >>sys.stderr, USAGE sys.exit(1) debug = False stdout = False for opt, arg in opts: if opt in ('--stdout', '-s'): stdout = True elif opt in ('--debug', '-d'): debug = True elif opt in ('--help', '-h'): print USAGE sys.exit(0) cli = lastfm.client.Client('lastcd') cli.open_log(debug) songs = [] for f in [file(name) for name in args] or [sys.stdin]: songs += lastfm.marshaller.load_documents(f) total_len = 0 for s in songs: total_len += s['length'] # We will simulate things as if the last track in our input just finished # playing right now. pos = time.time() - total_len subs = [] for s in songs: l = s['length'] if l: if l >= lastfm.MIN_LEN and l <= lastfm.MAX_LEN: date = pos + l / 2 s['time'] = time.gmtime(pos) subs.append(s) pos = pos + l else: print >>sys.stderr, 'lastcd: track has zero length' sys.exit(1) if not subs: print >>sys.stderr, 'lastcd: no usable tracks found' sys.exit(1) if stdout: lastfm.marshaller.dump_documents(subs, sys.stdout) else: try: cli.log.info('Sending %s song(s) to daemon' % len(subs)) cli.submit_many(subs) except IOError, e: print >>sys.stderr, 'lastcd: error writing: %s' % e ears-1.0.1/mbfind0000755000000000000000000000557311077433473014015 0ustar00usergroup00000000000000#!/usr/bin/python import sys import getopt try: import musicbrainz except ImportError, e: print >>sys.stderr, 'import: %s' % e sys.exit(1) def print_selected_album(mb): id = mb.GetIDFromURL(mb.GetResultData(musicbrainz.MBE_AlbumGetAlbumId)) album = mb.GetResultData(musicbrainz.MBE_AlbumGetAlbumName) n = mb.GetResultInt(musicbrainz.MBE_AlbumGetNumTracks) artistid = mb.GetIDFromURL( mb.GetResultData(musicbrainz.MBE_AlbumGetAlbumArtistId)) if artistid == musicbrainz.MBI_VARIOUS_ARTIST_ID: artist = '[Various Artists]' else: try: artist = mb.GetResultData1(musicbrainz.MBE_AlbumGetArtistName, 1) except musicbrainz.MusicBrainzError: artist = '[Unknown]' dur = 0 for i in range(1, n+1): dur += mb.GetResultInt1(musicbrainz.MBE_AlbumGetTrackDuration, i) m, s = divmod(dur/1000, 60) print '%s %s - %s [%d:%02d, %d tracks]' % (id, artist, album, m, s, n) if __name__ == '__main__': shortopts = 'a:b:v' longopts = ['artist=', 'album=', 'verbose'] mb = musicbrainz.mb() mb.SetDepth(4) try: opts, args = getopt.getopt(sys.argv[1:], shortopts, longopts) except getopt.GetoptError, e: print >>sys.stderr, 'getopt: %s' % e sys.exit(1) artistspec = None albumspec = None verbose = False for opt, arg in opts: if opt in ('--artist', '-a'): artistspec = arg if opt in ('--album', '-b'): albumspec = arg if opt in ('--verbose', '-v'): verbose = True if albumspec: try: mb.QueryWithArgs(musicbrainz.MBQ_FindAlbumByName, [albumspec]) except musicbrainz.MusicBrainzError, e: print >>sys.stderr, 'musicbrainz: %s' % e sys.exit(1) if verbose: print mb.GetResultRDF() n = mb.GetResultInt(musicbrainz.MBE_GetNumAlbums) for i in range(1, n+1): mb.Select1(musicbrainz.MBS_SelectAlbum, i) print_selected_album(mb) mb.Select(musicbrainz.MBS_Rewind) elif artistspec: try: mb.QueryWithArgs(musicbrainz.MBQ_FindArtistByName, [artistspec]) except musicbrainz.MusicBrainzError, e: print >>sys.stderr, 'musicbrainz: %s' % e sys.exit(1) if verbose: print mb.GetResultRDF() n = mb.GetResultInt(musicbrainz.MBE_GetNumArtists) for i in range(1, n+1): mb.Select1(musicbrainz.MBS_SelectArtist, i) m = mb.GetResultInt(musicbrainz.MBE_GetNumAlbums) for j in range(1, m+1): mb.Select1(musicbrainz.MBS_SelectAlbum, j) print_selected_album(mb) mb.Select(musicbrainz.MBS_Back) mb.Select(musicbrainz.MBS_Rewind) else: print >>sys.stderr, 'args: must specify an album or artist name' sys.exit(1) ears-1.0.1/mbget0000755000000000000000000001303111077433473013640 0ustar00usergroup00000000000000#!/usr/bin/python import sys import getopt try: import musicbrainz except ImportError, e: print >>sys.stderr, 'import: %s' % e sys.exit(1) class Abby(musicbrainz.mb): def __init__(self): musicbrainz.mb.__init__(self) def readdisc(self, dev='/dev/cdrom'): self.SetDevice(dev) try: self.Query(musicbrainz.MBQ_GetCDTOC) return self.GetResultData(musicbrainz.MBE_TOCGetCDIndexId) except musicbrainz.MusicBrainzError: print >>sys.stderr, ("cdrom: can't read TOC from disc") sys.exit(1) def query(self, discid=None, albumid=None): try: if discid: self.SetDepth(2) self.QueryWithArgs(musicbrainz.MBQ_GetCDInfoFromCDIndexId, [discid]) else: self.SetDepth(4) self.QueryWithArgs(musicbrainz.MBQ_GetAlbumById, [albumid]) return self.querieddiscinfo() except musicbrainz.MusicBrainzError: return self.localdiscinfo() def querieddiscinfo(self): matches = self.GetResultInt(musicbrainz.MBE_GetNumAlbums) if matches == 0: return self.localdiscinfo() self.Select1(musicbrainz.MBS_SelectAlbum, 1) n = self.GetResultInt(musicbrainz.MBE_AlbumGetNumTracks) album = self.GetResultData(musicbrainz.MBE_AlbumGetAlbumName) artistid = self.GetIDFromURL(self.GetResultData( musicbrainz.MBE_AlbumGetAlbumArtistId)) if artistid == musicbrainz.MBI_VARIOUS_ARTIST_ID: albumartist = 'Various Artists' else: # XXX: This is wrong! SG5DR. albumartist = self.GetResultData1( musicbrainz.MBE_AlbumGetArtistName, 1) tracks = [] for i in range(1, n+1): tracks.append({ 'number': i, 'album': album, 'artist': self.GetResultData1( musicbrainz.MBE_AlbumGetArtistName, i), 'title': self.GetResultData1( musicbrainz.MBE_AlbumGetTrackName, i), 'length': self.GetResultInt1( musicbrainz.MBE_AlbumGetTrackDuration, i) / 1000, 'mbid': self.GetIDFromURL(self.GetResultData1( musicbrainz.MBE_AlbumGetTrackId, i)), }) return album, albumartist, tracks def localdiscinfo(self): # Ensure that previous query was local TOC (ugh) self.readdisc() first = self.GetResultInt(musicbrainz.MBE_TOCGetFirstTrack) last = self.GetResultInt(musicbrainz.MBE_TOCGetLastTrack) tracks = [] for i in range(first, last+1): tracks.append({ 'number': i, 'artist': '', 'album': '', 'title': '', 'length': self.GetResultInt1( musicbrainz.MBE_TOCGetTrackNumSectors, i+1) / 75, }) return None, None, tracks def freedb_dump(albumartist, album, discid, tracks): print '# fake CD database file generated by mbget' print '#' print '# Track frame offsets:' # Assume standard pregap total_len = 2 for t in tracks: print '# %d' % (total_len * 75) total_len += t['length'] if albumartist and album: title = '%s / %s' % (albumartist, album) else: title = '' print '#' print '# Disc length: %d seconds' % total_len print '#' print '# Revision: 0' print '# Processed by: MusicBrainz' print '# Submitted by: MusicBrainz' print 'DISCID=%s' % discid print 'DTITLE=%s' % title for i, t in enumerate(tracks): if albumartist: if albumartist == t['artist']: title = t['title'] else: title = '%s / %s' % (t['artist'], t['title']) else: title = '' print 'TTITLE%d=%s' % (i, title) print 'EXTD=' for i, t in enumerate(tracks): print 'EXTD%d=' % i print 'PLAYORDER=' print '.' if __name__ == '__main__': shortopts = 'd:pi:a:ft' longopts = ['device=', 'print-discid', 'discid=', 'albumid=', 'freedb', 'template'] try: opts, args = getopt.getopt(sys.argv[1:], shortopts, longopts) except getopt.GetoptError, e: print >>sys.stderr, 'getopt: %s' % e sys.exit(1) abby = Abby() discid = None albumid = None freedb = False template = False for opt, arg in opts: if opt in ('--device', '-d'): discid = abby.readdisc(arg) elif opt in ('--print-discid', '-p'): print abby.readdisc(arg) sys.exit(0) elif opt in ('--discid', '-i'): discid = arg elif opt in ('--albumid', '-a'): albumid = arg elif opt in ('--freedb', '-f'): freedb = True elif opt in ('--template', '-t'): template = True if not (discid or albumid): discid = abby.readdisc() album, albumartist, tracks = abby.query(discid, albumid) if (album and albumartist) or template: if freedb: freedb_dump(albumartist, album, discid, tracks) else: import lastfm.marshaller if args: tracks = [tracks[int(a)-1] for a in args] for t in tracks: # The serialized format is UTF-8, but when printing, we need # to encode to stdout instead... this is ugly. print lastfm.marshaller.dump(t).decode('utf-8') else: sys.exit(1) ears-1.0.1/mbsubmit0000755000000000000000000000135611077433473014373 0ustar00usergroup00000000000000#!/usr/bin/python import sys import os import webbrowser try: import musicbrainz except ImportError, e: print >>sys.stderr, 'import: %s' % e sys.exit(1) if __name__ == '__main__': mb = musicbrainz.mb() mb.Query(musicbrainz.MBQ_GetCDTOC) cdid = mb.GetResultData(musicbrainz.MBE_TOCGetCDIndexId) mb.QueryWithArgs(musicbrainz.MBQ_GetCDInfoFromCDIndexId, [cdid]) if mb.GetResultInt(musicbrainz.MBE_GetNumAlbums) == 0: url = mb.GetWebSubmitURL() if url: webbrowser.open_new(url) else: print >>sys.stderr, 'musicbrainz: no url received from server' sys.exit(1) else: print >>sys.stderr, 'musicbrainz: discid is already entered' sys.exit(2) ears-1.0.1/peel0000755000000000000000000001260711077433473013477 0ustar00usergroup00000000000000#!/usr/bin/python import os import sys import time import getopt import select import fcntl import lastfm.client import lastfm.marshaller from lastfm.config import SaneConfParser BUF_CHUNK = 4096 BUF_MAX = 2048 BUF_INIT = 512 def popen(argv, mode='r'): command = ' '.join(argv) return os.popen(command.encode(sys.getfilesystemencoding()), mode) def quotemeta(s): for meta in ('\\', '$', '`', '"', '\n'): s = s.replace(meta, '\\' + meta) return '"%s"' % s class Command: def __init__(self, user_opts): self.user_opts = user_opts.split() class CdParanoia(Command): def open(self, device, number): return popen(['cdparanoia', '-r'] + self.user_opts + ['-d', device, '%d' % number, '-']) class APlay(Command): def open(self): return popen(['aplay', '-q', '-t', 'raw', '-c', '2', '-r', '44100', '-f', 'S16_LE'] + self.user_opts, 'w') class Bfp(Command): def open(self): return popen(['bfp'] + self.user_opts, 'w') class OggEnc(Command): def open(self, song, path): argv = ['oggenc', '-Q', '-r'] + self.user_opts try: argv += ['-a', quotemeta(song['artist'])] except KeyError: pass try: argv += ['-t', quotemeta(song['title'])] except KeyError: pass try: argv += ['-l', quotemeta(song['album'])] except KeyError: pass try: argv += ['-N', '%d' % song['number']] except KeyError: pass try: argv += ['-c', quotemeta('musicbrainz_trackid=%s' % song['mbid'])] except KeyError: pass argv += ['-o', quotemeta('%s.ogg' % path), '-'] return popen(argv, 'w') class Lame(Command): def open(self, song, path): argv = ['lame', '--quiet', '-rx'] + self.user_opts try: argv += ['--ta', quotemeta(song['artist'])] except KeyError: pass try: argv += ['--tt', quotemeta(song['title'])] except KeyError: pass try: argv += ['--tl', quotemeta(song['album'])] except KeyError: pass try: argv += ['--tn', '%d' % song['number']] # XXX except KeyError: pass argv += ['-', quotemeta('%s.mp3' % path)] return popen(argv, 'w') def sox_open(path): return popen(['sox', path, '-t', 'raw', '-r', '44100', '-c', '2', '-s', '-']) rippers = {'cdparanoia': CdParanoia} players = {'aplay': APlay, 'bfp': Bfp} encoders = {'oggenc': OggEnc, 'lame': Lame} def_path = '%(artist)s/%(album)s/%(number)02d - %(title)s' if __name__ == '__main__': shortopts = 'd:e:qc' longopts = ['device=', 'encoder', 'quiet', 'continue'] try: opts, args = getopt.getopt(sys.argv[1:], shortopts, longopts) except getopt.GetoptError, e: print >>sys.stderr, 'peel: %s' % e sys.exit(1) device = '/dev/cdrom' quiet = False cp = SaneConfParser() cp.read([os.path.expanduser('~/.peelrc')]) rip_cmd = cp.get('commands', 'rip', 'cdparanoia') play_cmd = cp.get('commands', 'play', 'aplay') enc_cmd = cp.get('commands', 'encode', 'oggenc') rip_opts = cp.get('options', rip_cmd, '') play_opts = cp.get('options', play_cmd, '') enc_opts = cp.get('options', enc_cmd, '') path_tmpl = cp.get('output', 'path', def_path) for opt, arg in opts: if opt in ('--device', '-d'): device = arg elif opt in ('--quiet', '-q'): quiet = True elif opt in ('--continue', '-c'): device = None elif opt in ('--encoder', '-e'): enc_cmd = arg try: ripper = rippers[rip_cmd](rip_opts) player = players[play_cmd](play_opts) encoder = encoders[enc_cmd](enc_opts) except KeyError, e: print >>sys.stderr, 'unknown command: %s' % e.args[0] if not quiet: play = player.open() cli = lastfm.client.Client('peel') cli.open_log() for song in lastfm.marshaller.load_documents(sys.stdin): print "Track %(number)s: %(title)s..." % song safe_song = {} for k, v in song.iteritems(): try: safe_song[k] = v.replace('/', '_') except AttributeError: safe_song[k] = v path = path_tmpl % safe_song dir = os.path.dirname(path) if dir and not os.path.isdir(dir): os.makedirs(dir) enc = encoder.open(song, path) if device: rip = ripper.open(device, song['number']) else: rip = sox_open('track%02d.cdda.wav' % song['number']) buf = [rip.read(BUF_CHUNK) for i in range(0, BUF_INIT)] flags = fcntl.fcntl(rip, fcntl.F_GETFL) fcntl.fcntl(rip, fcntl.F_SETFL, flags|os.O_NONBLOCK) while len(buf) > 0 or not rip.closed: while len(buf) < BUF_MAX and not rip.closed: rd, wr, ex = select.select([rip], [], [], 0) if rip in rd: raw = rip.read(BUF_CHUNK) if len(raw) > 0: buf.append(raw) else: rip.close() else: break if len(buf) > 0: raw = buf.pop(0) enc.write(raw) if not quiet: play.write(raw) if not quiet: if song['length'] >= 30: song['time'] = time.gmtime() cli.submit(song) cli.log.info('Sent %s to daemon' % lastfm.repr(song)) ears-1.0.1/setup.py0000644000000000000000000000152711077433473014335 0ustar00usergroup00000000000000#!/usr/bin/python from distutils.core import setup setup( name='ears', version='1.0.1', description='MusicBrainz querying, CD-ripping, and Last.fm tools', author='Decklin Foster', author_email='decklin@red-bean.com', url='http://www.red-bean.com/decklin/ears/', classifiers=[ 'Intended Audience :: End Users/Desktop', 'Development Status :: 4 - Beta', 'License :: MIT/X Consortium License', 'Topic :: Multimedia :: Sound :: Players', 'Operating System :: POSIX', 'Environment :: Console (Text Based)', 'Programming Language :: Python', ], scripts = [ 'lastcd', 'mbfind', 'mbget', 'mbsubmit', 'peel', ], data_files=[ ('share/man/man1', [ 'doc/mbget.1', ]), ], )