#!/usr/bin/python # Web interface to my MP3 database import asyncore import base64 import cStringIO import datetime import mad import os import re import socket import sys import time import types import urllib import uuid import MySQLdb import coverhunt import business import database import gflags import track FLAGS = gflags.FLAGS gflags.DEFINE_string('ip', '192.168.1.99', 'Bind to this IP') gflags.DEFINE_integer('port', 12345, 'Bind to this port') gflags.DEFINE_boolean('showrequest', False, 'Show the content of HTTP request') gflags.DEFINE_boolean('showpost', False, 'Show the content of POST operations') gflags.DEFINE_boolean('showresponse', False, 'Show the content of response headers') running = True uuid = uuid.uuid4() requests = {} skips = {} bytes = 0 _CONTENT_TYPES = {'css': 'text/css', 'html': 'text/html', 'mp3': 'audo/x-mpeg-3', 'png': 'image/png', 'swf': 'application/x-shockwave-flash', 'xml': 'text/xml' } _SUBST_RE = re.compile('(.*){{([^}]+)}}(.*)', re.MULTILINE | re.DOTALL) def htc(m): return chr(int(m.group(1),16)) def urldecode(url): url = url.replace('+', ' ') rex = re.compile('%([0-9a-hA-H][0-9a-hA-H])',re.M) return rex.sub(htc,url) class http_server(asyncore.dispatcher): """Listen for new client connections, which are then handed off to another class """ def __init__(self, ip, port, db): self.ip= ip self.port = port self.db = db self.logfile = open('log', 'a') self.business = business.BusinessLogic(self.db, self.log) asyncore.dispatcher.__init__(self) self.create_socket(socket.AF_INET, socket.SOCK_STREAM) self.bind((ip, port)) self.listen(5) def log(self, msg, console=True): """Write a log line.""" l = '%s %s %s\n' %(datetime.datetime.now(), repr(self.addr), msg) if console: sys.stdout.write(l) self.logfile.write(l) self.logfile.flush() def writable(self): return 0 def handle_read(self): pass def readable(self): return self.accepting def handle_connect(self): pass def handle_accept(self): conn, addr = self.accept() handler = http_handler(conn, addr, self.db, self.business, self.log) class http_handler(asyncore.dispatcher): """Handle a single connection""" def __init__(self, conn, addr, db, business, log): asyncore.dispatcher.__init__(self, sock=conn) self.addr = addr self.db = db self.business = business self.log = log self.buffer = '' # Used to track uPnP plays self.is_mp3 = False self.is_tracked = False self.streamed_at = None self.id = None self.cookie = None self.extra_headers = [] self.client_id = 0 def handle_read(self): rq = self.recv(32 * 1024) file = '' method = None post_data = None chunk = None for line in rq.split('\n'): line = line.rstrip('\r') self.log('REQUEST %s' % line, console=FLAGS.showrequest) if line.startswith('GET'): (_, file, _) = line.split(' ') method = 'GET' if line.startswith('POST'): (_, file, _) = line.split(' ') method = 'POST' if line.startswith('Range: '): chunk = line[7:] if line.startswith('Cookie: '): self.cookie = line[8:] if post_data is not None: post_data += '%s\r\n' % line if len(line) == 0 and method == 'POST' and post_data is None: post_data = '' if file: self.log('%s %s' %(method, file)) if method == 'POST' and post_data: for l in post_data.split('\r\n'): self.log('DATA %s' % l, console=FLAGS.showpost) # Implementation of uPnP -- must come before cookie set if file.startswith('/getDeviceDesc'): self.handleurl_getdevicedesc(file) elif file.startswith('/uPnP_Control'): self.handleurl_cdscontrol(file, post_data) elif file.startswith('/trackedmp3/'): # We use the uPnP client's streaming behaviour to track plays self.is_tracked = True self.streamed_at = datetime.datetime.now() self.handleurl_mp3(file[12:], chunk, tracked=True) else: # Normal clients can handle cookies if not self.cookie: self.db.ExecuteSql('insert into clients(createtime) values(now());') row = self.db.GetOneRow('select last_insert_id();') self.cookie = 'id=%s' % row['last_insert_id()'] self.extra_headers.append('Set-Cookie: id=%s; ' 'expires=Sun, 17 Jan 2038 09:00:00 GMT' % row['last_insert_id()']) # Extract the id for elem in self.cookie.split('; '): if elem.startswith('id='): self.client_id = int(elem[3:]) self.db.ExecuteSql('update clients set requests=0 ' 'where requests is null;') self.db.ExecuteSql('update clients set requests=requests + 1 ' 'where id=%d;' % self.client_id) self.db.ExecuteSql('commit;') # Top URL if file == '/': self.handleurl_root(post_data) # Implementation of the HTTP player elif file == '/play': self.handleurl_play() elif file.startswith('/mp3/'): self.handleurl_mp3(file[5:], chunk) elif file.startswith('/local/'): self.handleurl_local(file) elif file.startswith('/done'): self.handleurl_done(file) elif file.startswith('/skipped'): self.handleurl_skipped(file) elif file.startswith('/art'): self.handleurl_art(file) elif file.startswith('/merge'): self.handleurl_merge(file) elif file.startswith('/unmerge'): self.handleurl_unmerge(file) elif file.startswith('/art'): self.handleurl_art(file) elif file.startswith('/merge'): self.handleurl_merge(file) # User interface for the HTTP player elif file.startswith('/graph'): self.handleurl_graph() elif file.startswith('/browse'): self.handleurl_browse(file, post_data) elif file.startswith('/tags'): self.handleurl_tags(file) elif file.startswith('/tag/'): self.handleurl_tag(file) elif file.startswith('/addtag/'): self.handleurl_addtag(file) elif file.startswith('/deletetag/'): self.handleurl_deletetag(file) elif file.startswith('/events'): self.handleurl_events(file) else: self.senderror(404, '%s file not found' % file) self.close() self.log('%d bytes queued' % len(self.buffer)) def writable(self): return len(self.buffer) > 0 def handle_write(self): global bytes try: sent = self.send(self.buffer) bytes += sent self.buffer = self.buffer[sent:] if len(self.buffer) == 0: if self.is_mp3 and self.is_tracked and self.id: self.log('MP3 request complete') delta = datetime.datetime.now() - self.streamed_at if delta.seconds < 30: self.log('MP3 streamed too fast') else: self.markplayed(self.id) self.close() except: pass def handle_close(self): pass def handleurl_root(self, post_data): """The top level page.""" if post_data: for l in post_data.split('\r\n'): if l: (name, value) = l.split('=') value = value.replace('%2F', '/').replace('%3A', ':') if value == '': value = None if name == 'mp3_source': self.log('Updating MP3 source to %s' % value) self.db.ExecuteSql('update clients set mp3_source="%s" ' 'where id=%s;' %(value, self.client_id)) self.db.ExecuteSql('commit;') row = self.db.GetOneRow('select * from clients where id=%s;' % self.client_id) if not row or not row.has_key('mp3_source'): row = {'mp3_source': ''} self.sendfile('index.html', subst={'mp3_source': row['mp3_source'] }) def handleurl_play(self): """The HTTP playback user interface.""" self.markskipped() skips.setdefault(self.addr[0], 0) rendered = self.business.picktrack(client_id=self.client_id, skips=skips[self.addr[0]]) requests[self.addr[0]] = (datetime.datetime.now(), rendered['id']) self.log('MP3 url is %s' % rendered['mp3_url']) if not rendered: self.senderror(501, 'Failed to select a track') return rendered['graph'] = self.playgraph() self.sendfile('play.html', subst=rendered) def handleurl_graph(self): """Return just a page with the history graph on it.""" rendered = {} rendered['graph'] = self.playgraph() sql = ('select * from events inner join tracks on ' 'events.track_id = tracks.id where event in ("play", "skip") ' 'order by events.timestamp desc limit 30;') results = self.renderbrowseresults(sql) rendered['results'] = '\n'.join(results) self.sendfile('graph.html', subst=rendered) def handleurl_browse(self, file, post_data): """Browse the database.""" # Parse filters filters = {'artist_filter': '', 'artist_filter_compiled': '.*', 'album_filter': '', 'album_filter_compiled': '.*', 'track_filter': '', 'track_filter_compiled': '.*', 'recent_filter': '', 'recent_filter_compiled': '', 'recent_checked': '', 'unplayed_filter': '', 'unplayed_filter_compiled': '', 'unplayed_checked': '', 'random_filter': '', 'random_filter_compiled': '', 'random_checked': '', } # I am sure there is a better way than this if post_data: for l in post_data.split('\r\n'): if len(l) > 0: for arg in l.split('&'): (name, value) = arg.split('=') if value: value = urldecode(value.replace('+', ' ')) filters['%s_filter' % name] = value filters['%s_filter_compiled' % name] = value.replace(' ', '[ _+]+') recent_sql = '' if filters['recent_filter'] == 'Recent': recent_sql = 'and (to_days(now()) - to_days(creation_time)) < 15' filters['recent_checked'] = 'checked' unplayed_sql = '' if filters['unplayed_filter'] == 'Unplayed': unplayed_sql = ('and last_played=makedate(1970,1) and ' 'last_skipped=makedate(1970,1)') filters['unplayed_checked'] = 'checked' random_sql_cols = '' random_sql_order = '' if filters['random_filter'] == 'Random': random_sql_cols = ', %s' % business.GenerateRankSql(2) random_sql_order = 'idx desc,' filters['random_checked'] = 'checked' if (filters['artist_filter'] or filters['album_filter'] or filters['track_filter']): limit_sql = '' else: limit_sql = 'limit 100' sql = ('select *%s from tracks ' 'where artist rlike "%s" and album rlike "%s" and song rlike "%s" ' '%s %s order by %s artist, song, album, number %s;' %(random_sql_cols, filters['artist_filter_compiled'], filters['album_filter_compiled'], filters['track_filter_compiled'], unplayed_sql, recent_sql, random_sql_order, limit_sql)) self.log('Browse SQL = %s' % sql) results = self.renderbrowseresults(sql) filters['results'] = '\n'.join(results) self.sendfile('browse.html', subst=filters) def handleurl_tags(self, file): """Serve a list of the tags available.""" tags = [] for row in self.db.GetRows('select distinct(tag), count(*) from tags ' 'group by tag order by tag;'): self.log('Found tag: %s (%d tracks)' %(row['tag'], row['count(*)'])) tags.append('
  • %s (%d tracks)' %(urllib.urlencode({'id': row['tag']}), row['tag'], row['count(*)'])) self.sendfile('tags.html', subst={'tags': '\n'.join(tags)}) def handleurl_events(self, file): """Show recent events.""" events = [] merge_master = None merge_slave = None merge_slave_tags = None merge_slave_paths = None # This select is like this because timestamps aren't accurate enough to be # ordered correctly unless we take the default sort order. I should fix this # by adding a unique counter to these rows for row in self.db.GetRows('select * from events;')[-1000:]: if row['event'] == 'merge: before': merge_master = row['track_id'] elif row['event'] == 'merge: deleted': merge_slave = row['track_id'] elif row['event'].startswith('merge: tags'): merge_slave_tags = eval(row['details']) elif row['event'].startswith('merge: paths'): merge_slave_paths = eval(row['details']) elif row['event'] == 'merge: after': events.append('%s%s and %sMerge' 'Track %s and %s were merged. %s owned tags %s and ' 'paths %s before the merge' %(row['timestamp'], merge_master, merge_slave, merge_master, merge_slave, merge_slave, merge_slave_tags, merge_slave_paths)) else: events.append('%s%s%s%s' %(row['timestamp'], row['track_id'], row['event'], row['details'])) self.sendfile('events.html', subst={'events': ('' '' '%s
    TimestampTrack IdEventDetails
    ' % '\n'.join(events))}) def handleurl_tag(self, file): """Show songs with a given tag.""" (_, _, tag_encoded) = file.split('/') tag = urldecode(tag_encoded.split('=')[1]) sql = ('select * from tags inner join tracks on tags.track_id = tracks.id ' 'where tag="%s";' % tag) self.log('Tag browse SQL = %s' % sql) results = self.renderbrowseresults(sql, includemissing=True) tags = {} tags['results'] = '\n'.join(results) tags['tag'] = tag tags['tag_encoded'] = tag_encoded self.sendfile('tag.html', subst=tags) def handleurl_addtag(self, file): """Add this tag.""" (_, _, tag_encoded, tracks) = file.split('/') tag = urldecode(tag_encoded.split('=')[1]) tracks = tracks.split(',') for track in tracks: self.db.ExecuteSql('insert ignore into tags(tag, track_id) ' 'values("%s", %s);' %(tag, track)) self.db.ExecuteSql('commit;') self.sendfile('done.html') def handleurl_deletetag(self, file): """Delete this tag.""" (_, _, tag_encoded, tracks) = file.split('/') tag = urldecode(tag_encoded.split('=')[1]) tracks = tracks.split(',') self.log('Deleting tag %s from %s' %(tag, repr(tracks))) self.db.ExecuteSql('delete from tags where tag="%s" and ' 'track_id in (%s);' %(tag, ','.join(tracks))) self.db.ExecuteSql('commit;') self.sendfile('done.html') def handleurl_art(self, file): """Serve an image for a given album, if we have one.""" (_, _, artist, album) = file.replace('%20', ' ').split('/') self.log('Fetching art for "%s" "%s"' %(artist, album)) row = self.db.GetOneRow('select art from art where artist=%s and ' 'album=%s;' %(self.db.FormatSqlValue('artist', artist), self.db.FormatSqlValue('album', album))) if not row or not row['art']: self.senderror(404, 'No such art') return data = base64.decodestring(row['art']) self.sendheaders('HTTP/1.1 200 OK\r\n' 'Content-Type: image/jpeg\r\n' 'Content-Length: %s\r\n' '%s\r\n' %(len(data), '\r\n'.join(self.extra_headers))) self.buffer += data def handleurl_merge(self, file): """Merge the specified list of tracks.""" (_, _, tracks) = file.split('/') tracks = tracks.split(',') self.log('Merging %s' % repr(tracks)) first_track = track.Track(self.db) first_track.FromId(int(tracks[0])) self.log('Before: %s' % repr(first_track.persistant)) cleanup = [] for second_track_id in tracks[1:]: second_track = track.Track(self.db) second_track.FromId(int(second_track_id)) first_track.Merge(second_track) self.log('After: %s' % repr(first_track.persistant)) cleanup.append(second_track) first_track.Store() for second_track in cleanup: second_track.Delete() self.log('Merge finished') self.sendfile('done.html') def handleurl_unmerge(self, file): """Undo a merge.""" (_, _, tracks) = file.split('/') tracks = tracks.split(',') self.log('Unmerging %s' % repr(tracks)) first_track = track.Track(self.db) first_track.persistant = eval(self.db.GetOneRow('select * from events where ' 'event = "merge: before" and ' 'track_id = %s order by ' 'timestamp asc limit 1;' % tracks[0])['details']) self.log('Previous state of %s: %s' %(tracks[0], repr(first_track.persistant))) first_track.Store() for t_id in tracks[1:]: t = track.Track(self.db) t.persistant = eval(self.db.GetOneRow('select * from events where ' 'event = "merge: deleted" and ' 'track_id = %s order by ' 'timestamp desc limit 1;' % t_id)['details']) tags = eval(self.db.GetOneRow('select * from events where ' 'event = "merge: tags: %s" ' 'order by timestamp desc limit 1;' % t_id)['details']) paths = eval(self.db.GetOneRow('select * from events where ' 'event = "merge: paths: %s" ' 'order by timestamp desc limit 1;' % t_id)['details']) self.log('Previous state of %s: %s. Tags %s, Paths %s.' %(t_id, repr(t.persistant), tags, paths)) t.Store() for tag in tags: try: self.db.ExecuteSql('update tags set track_id=%s where tag="%s";' % (t_id, tag['tag'])) except: pass for path in paths: try: self.db.ExecuteSql('update paths set track_id=%s where path=%s;' % (t_id, self.db.FormatSqlValue('path', path['path']))) except: pass self.db.ExecuteSql('commit;') def renderbrowseresults(self, sql, includemissing=False): """Paint a table of the results of a SQL statement.""" f = open('browse_result.html') results_template = f.read() f.close() f = open('missing_result.html') missing_template = f.read() f.close() results = [] bgcolor = '' for row in self.db.GetRows(sql): this_track = track.Track(self.db) this_track.FromId(row['id']) rendered = this_track.RenderValues() (rendered['mp3_url'], rendered['mp3_file']) = self.business.findMP3(row['id'], client_id=self.client_id) if not 'creation_time' in rendered: rendered['creation_time'] = '' if rendered['mp3_url']: if bgcolor == 'bgcolor="#DDDDDD"': bgcolor = '' else: bgcolor = 'bgcolor="#DDDDDD"' rendered['bgcolor'] = bgcolor results.append(self.substitute(results_template, rendered)) else: if includemissing: results.append(self.substitute(missing_template, rendered)) self.log('Skipping row with no MP3 URL: %s' % repr(rendered)) return results def markskipped(self): """Mark skipped tracks, if any.""" if self.addr[0] in requests: skips.setdefault(self.addr[0], 0) skips[self.addr[0]] += 1 self.business.markskipped(requests[self.addr[0]][1], skips[self.addr[0]]) del requests[self.addr[0]] def markplayed(self, id): """Mark a track as played.""" self.business.markplayed(id) if self.addr[0] in requests: skips[self.addr[0]] = 0 del requests[self.addr[0]] def playgraph(self): """Generate a Google chart API graph of recent play history.""" play_graph = {} skip_graph = {} now = datetime.datetime.now() one_hour = datetime.timedelta(minutes=60) one_hour_ago = now - one_hour # Collect data from MySQL for row in self.db.GetRows('select song, plays, skips, last_action, ' 'last_played, last_skipped from tracks ' 'where last_action is not null and ' 'last_action > %s ' 'order by last_action desc;' % self.db.FormatSqlValue('date', one_hour_ago)): if row['last_played']: delta = now - row['last_played'] secs = delta.seconds / 60 if secs < 3600: play_graph.setdefault(secs, 0) play_graph[secs] += 1 if row['last_skipped']: delta = now - row['last_skipped'] secs = delta.seconds / 60 if secs < 3600: skip_graph.setdefault(secs, 0) skip_graph[secs] += 1 play = '' skip = '' for i in range(60): play += '%d,' % play_graph.get(i, 0) skip += '%d,' % skip_graph.get(i, 0) return ('cht=bvg&' 'chbh=a&chds=0,10,0,10&chd=t:%s|%s&' 'chco=00FF00,FF0000' %(play[:-1], skip[:-1])) def handleurl_done(self, file): """Mark an MP3 as played.""" id = file.split('/')[-1] if id and id != 'nosuch': self.markplayed(id) if self.addr[0] in requests: del requests[self.addr[0]] self.sendfile('done.html') def handleurl_skipped(self, file): """Mark an MP3 as skipped.""" # I use the business layer here, because its possible that the server # doesn't know the track currently being played (think browse interface). # I also don't do skip length tracking, as it makes no sense to the browse # interface id = file.split('/')[-1] if id and id != 'nosuch': self.business.markskipped(id, -1) if self.addr[0] in requests: del requests[self.addr[0]] self.sendfile('skipped.html') def handleurl_mp3(self, file, chunk, tracked=False): """Serve MP3 files.""" self.id = int(file) if self.addr[0] in requests: # A uPnP pause can look like a skip, but its requesting the same ID self.log('Comparing %s(%s) and %s(%s)' %(type(requests[self.addr[0]][1]), requests[self.addr[0]][1], type(self.id), self.id)) if int(requests[self.addr[0]][1]) == self.id and tracked: self.log('This is a resume') else: self.markskipped() for row in self.db.GetRows('select path from paths where track_id=%s;' % self.id): if row['path'].endswith('.mp3') and os.path.exists(row['path']): requests[self.addr[0]] = (datetime.datetime.now(), self.id) self.sendfile(row['path'], chunk=chunk) self.is_mp3 = True return self.senderror(500, 'MP3 %s missing' % file) def handleurl_local(self, file): """Return a local file needed by the user interface.""" ent = file.split('/')[-1] self.sendfile(ent) def handleurl_getdevicedesc(self, file): """uPnP device discovery.""" global uuid self.sendfile('upnp_devicedesc.xml', subst={'ip': FLAGS.ip, 'port': FLAGS.port, 'uuid': uuid }) def handleurl_cdscontrol(self, file, post_data): """uPnP CDS endpoint control.""" object_id = None object_id_re = re.compile('(.*)') for l in post_data.split('\r\n'): m = object_id_re.match(l) if m: object_id = m.group(1) if object_id: self.log('uPnP request for object id %s' % object_id) if object_id == '0': f = open('upnp_results.xml') results_template = f.read() f.close() results = [] for title in ['All', 'Recent']: result = self.substitute(results_template, {'title': title}) self.log('XML %s' % result, console=FLAGS.showresponse) results.append(self.xmlsafe(result)) self.sendfile('upnp_browseresponse.xml', subst={'result': '\n'.join(results), 'num_returned': len(results), 'num_matches': len(results) }) elif object_id == 'All' or object_id == 'Recent': skips.setdefault(self.addr[0], 0) rendered = self.business.picktrack(recent=(object_id == 'Recent'), client_id=self.client_id, skips=skips[self.addr[0]]) self.log('uPnP track selection %s' % rendered['id']) rendered['ip'] = FLAGS.ip rendered['port'] = FLAGS.port rendered['objectid'] = object_id f = open('upnp_song.xml') results = f.read() f.close() results = self.substitute(results, rendered) for l in results.split('\n'): self.log('XML %s' % l, console=FLAGS.showresponse) results = self.xmlsafe(results) self.sendfile('upnp_browseresponse.xml', subst={'result': results, 'num_returned': 1, 'num_matches': 1000 }) else: self.senderror(501, 'Unknown object ID') def xmlsafe(self, s): """Return an XML safe version of a string.""" for repl in [('&', '&'), ('"', '"'), ('<', '<'), ('>', '>'), ("'", '')]: (i, o) = repl s = s.replace(i, o) return s def sendredirect(self, path): """Send a HTTP 302 redirect.""" self.sendheaders('HTTP/1.1 302 Found\r\n' 'Location: %s\r\n' % path) def getstats_ever(self): """Return some playback statistics for all time.""" retval = {} row = self.db.GetOneRow('select count(*), max(plays), sum(plays), ' 'max(skips), sum(skips) from tracks;') for key in row: retval['ever_%s' %(key.replace('(', '').\ replace(')', '').\ replace('*', ''))] = row[key] return retval def getstats_today(self): """Return some playback statistics for today.""" retval = {} row = self.db.GetOneRow('select count(*) from events ' 'where date(timestamp) = date(now()) ' 'and event = "play";') retval['today_countplays'] = row['count(*)'] row = self.db.GetOneRow('select count(*) from events ' 'where date(timestamp) = date(now()) ' 'and event = "skip";') retval['today_countskips'] = row['count(*)'] return retval def sendfile(self, path, subst=None, chunk=None): """Send a file to the client, including doing the MIME type properly.""" inset = 0 if chunk: # Format is "bytes=6600100-" inset = int(chunk.split('=')[1].split('-')[0]) self.log('Skipping the first %d bytes' % inset) data = '' try: f = open(path) f.read(inset) data += f.read() f.close() except Exception, e: self.senderror(404, 'File read error: %s (%s)' % (path, e)) return extn = path.split('.')[-1] mime_type = _CONTENT_TYPES.get(extn, 'application/octet-stream') if mime_type.find('ml') != -1: if not subst: subst = {} subst.update(self.getstats_ever()) subst.update(self.getstats_today()) data = self.substitute(data, subst) self.sendheaders('HTTP/1.1 200 OK\r\n' 'Content-Type: %s\r\n' 'Content-Length: %s\r\n' '%s\r\n' %(mime_type, len(data), '\r\n'.join(self.extra_headers))) if mime_type == 'text/xml': for l in data.split('\n'): self.log('REPLY %s' % l, console=FLAGS.showresponse) self.buffer += data def substitute(self, data, subst): """Perform template substitution.""" m = _SUBST_RE.match(data) while m: data = '%s%s%s' %(m.group(1), subst.get(m.group(2), '%s missing' % m.group(2)), m.group(3)) m = _SUBST_RE.match(data) return data def senderror(self, number, msg): self.sendheaders('HTTP/1.1 %d %s\r\n' 'Content-Type: text/html\r\n\r\n' %(number, msg)) self.buffer += ('MP3 server' '%s' % msg) self.log('Sent %d error' % number) def sendheaders(self, headers): """Send HTTP response headers.""" for l in headers.split('\r\n'): self.log('RESPONSE %s' % l, console=FLAGS.showresponse) self.buffer += headers def DisplayFriendlySize(bytes): """Turn a number of bytes into a nice string""" t = type(bytes) if t != types.LongType and t != types.IntType and t != decimal.Decimal: return 'NotANumber(%s=%s)' %(t, bytes) if bytes < 1024: return '%d bytes' % bytes if bytes < 1024 * 1024: return '%d kb (%d bytes)' %((bytes / 1024), bytes) if bytes < 1024 * 1024 * 1024: return '%d mb (%d bytes)' %((bytes / (1024 * 1024)), bytes) return '%d gb (%d bytes)' %((bytes / (1024 * 1024 * 1024)), bytes) def main(argv): global running global bytes # Parse flags try: argv = FLAGS(argv) except gflags.FlagsError, e: print FLAGS db = database.Database() server = http_server(FLAGS.ip, FLAGS.port, db) # Start the web server, which takes over this thread print '%s Started listening on port %s' %(datetime.datetime.now(), FLAGS.port) last_summary = time.time() while running: last_event = time.time() asyncore.loop(timeout=10.0, count=1) if time.time() - last_event > 9.0: # We are idle print '%s ...' % datetime.datetime.now() remove = [] for ent in requests: (t, _) = requests[ent] delta = datetime.datetime.now() - t if delta.seconds > 3600: print '%s Entry %s is too old' %(datetime.datetime.now(), ent) remove.append(ent) for ent in remove: del requests[ent] db.ExecuteSql('update tracks set last_played=makedate(1970,1) where ' 'last_played is null;') db.ExecuteSql('update tracks set last_skipped=makedate(1970,1) where ' 'last_skipped is null;') db.ExecuteSql('commit;') for row in db.GetRows('select path from paths where error is null ' 'and duration is null limit 1;'): try: duration = mad.MadFile(row['path']).total_time() print '%s MP3 length %s: %f ms' %(datetime.datetime.now(), row['path'], duration) db.ExecuteSql('update paths set duration=%f where path=%s;' %(duration, db.FormatSqlValue('path', row['path']))) except Exception, e: db.ExecuteSql('update paths set error=%s where path=%s;' %(db.FormatSqlValue('error', str(e)), db.FormatSqlValue('path', row['path']))) for row in db.GetRows('select distinct tracks.artist, tracks.album, ' 'art.art from tracks left join art on ' 'tracks.artist = art.artist and ' 'tracks.album = art.album where ' 'art.art is null and art.error is null and ' 'tracks.artist is not null ' 'and tracks.album is not null ' 'group by tracks.album, tracks.artist limit 1;'): print '%s Fetching art for "%s" "%s"' %(datetime.datetime.now(), row['artist'], row['album']) a = coverhunt.Art(row['artist'], row['album']) art = a.Search() if not art: db.ExecuteSql('insert into art(artist, album, error) values ' '(%s, %s, "No art found");' %(db.FormatSqlValue('artist', row['artist']), db.FormatSqlValue('album', row['album']))) else: db.ExecuteSql('insert into art(artist, album, art) values ' '(%s, %s, "%s");' %(db.FormatSqlValue('artist', row['artist']), db.FormatSqlValue('album', row['album']), art)) if time.time() - last_summary > 60.0: print '%s TOTAL BYTES SERVED: %s' %(datetime.datetime.now(), DisplayFriendlySize(bytes)) last_summary = time.time() if __name__ == "__main__": main(sys.argv)