def config(self, set_option=None, get_option=None, value=None): logger.debug("%s (%s)\tconfig()\tHeaders: %s" % (utils.find_originating_host(cherrypy.request.headers), cherrypy.request.login, cherrypy.request.headers)) if cfg['REQUIRE_LOGIN'] and cherrypy.request.login not in cfg['GROUPS']['admin']: logger.warn("%(user)s (%(ip)s) requested configuration data, but was denied because %(user)s is not a member of the admin group." % {'user': cherrypy.request.login, 'ip': utils.find_originating_host(cherrypy.request.headers)}) raise cherrypy.HTTPError(401,'Not Authorized') if set_option: if not value: raise cherrypy.HTTPError(501,'No value provided for the requested option') if set_option not in cfg.keys(): raise cherrypy.HTTPError(501,'The requested option does not exist') try: if type(cfg[set_option]) is types.ListType: value = value.split(',') if type(cfg[set_option]) is types.BooleanType: value = json.loads(value) if type(cfg[set_option]) is types.StringType: value = str(value) except: raise cherrypy.HTTPError(501, 'The value provided was the wrong type. Expected a %s' % type(cfg[set_option])) try: cfg[set_option] = value cfg.save_config() cherrypy.response.headers['Content-Type'] = 'application/json' return json.dumps({'config': {set_option: cfg[set_option]}}) except Exception as x: logger.error("Could not save configuration. The error was: %s" % str(x)) if get_option: if get_option not in cfg.keys(): raise cherrypy.HTTPError(501,'The requested option does not exist') cherrypy.response.headers['Content-Type'] = 'application/json' return json.dumps({'config': {get_option: cfg[get_option]}}) cherrypy.response.headers['Content-Type'] = 'application/json' return json.dumps({'config': cfg})
def _process_read_queue(self): """Spawns a pool of worker processes to handle reading the metadata from all of the files in the read_queue. """ self.reading.set() status = 'Importing songs to the library' self.jobs[self.current_job]['status'] = status # Create a pool of processes to handle the actual reading of tags. # Using processes instead of threads lets us take full advantage of # multi-core CPUs so this operation doesn't take as long. self.pool = Pool() queue_size = self.read_queue.qsize() while self.read_queue.qsize() > 0 and not self.stopping.is_set(): args_list = [] # Get 100 songs from the queue for x in range(100): try: args_list.append(self.read_queue.get(timeout=1)) except: break # Read the tags from the 100 songs and then stick the results in # the database queue. try: self.db_queue.put(self.pool.map(read_metadata, args_list)) n_items = len(args_list) self.jobs[self.current_job]['processed_items'] += n_items except: logger.error("Error processing read queue.") logger.debug("Processed %d items in %0.2f seconds" % (queue_size - self.read_queue.qsize(), time() - self.start_time)) self.pool.close() self.pool.join() self.reading.clear()
def __init__(self, db_url=cfg['COUCHDB_URL'], db_username=cfg['COUCHDB_USER'], db_password=cfg['COUCHDB_PASSWORD']): """Sets up the database connection and starts loading songs.""" # Initiate a connection to the database server self.shutting_down = threading.Event() self.building_cache = threading.Event() logger.debug("Initiating the database connection.") filters = [] if db_username or db_password: filters.append(BasicAuth(db_username, db_password)) self._server = Server(db_url, filters=filters) connect_retries = 0 while True: try: # Get a reference to our database self.db = self._server.get_or_create_db(cfg['DATABASE_NAME']) break except Exception as e: logger.error(str(e)) if connect_retries < 3: logger.error("Error connecting to CouchDB.") connect_retries += 1 sleep(3) else: logger.error("Could not connect to CouchDB. Quitting.") sys.exit(1) logger.debug("Loading database design documents.") # Load our database views from the filesystem loader = FileSystemDocsLoader(os.path.join(cfg['ASSETS_DIR'], 'views/_design')) try: loader.sync(self.db, verbose=True) except: pass logger.debug("Initializing the database cache.") self.cache = BlofeldCache(self.db) self.scanner = Scanner(cfg['MUSIC_PATH'], self.db) self.update()
def get_song(self, songid=None, format=False, bitrate=False): logger.debug("%s (%s)\tget_song(songid=%s, format=%s, bitrate=%s)\tHeaders: %s" % (utils.find_originating_host(cherrypy.request.headers), cherrypy.request.login, songid, format, bitrate, cherrypy.request.headers)) log_message = "%s (%s) is listening to " % (cherrypy.request.login, utils.find_originating_host(cherrypy.request.headers)) last = self.multi_requests.get(songid, None) show_log = False if not last or (last and time.time() > (last + 30)): show_log = True self.multi_requests[songid] = time.time() try: range_request = cherrypy.request.headers['Range'] except: range_request = "bytes=0-" try: song = self.library.db[songid] path = song['location'] except: log_message += "a song ID which could not be found: %s" % str(songid) logger.error(log_message) raise cherrypy.HTTPError(404) log_message += '"%s" by %s from %s ' % (song['title'].encode(cfg['ENCODING']), song['artist'].encode(cfg['ENCODING']), song['album'].encode(cfg['ENCODING'])) try: client_os, client_browser = httpagentparser.simple_detect(cherrypy.request.headers['User-Agent']) # b = self.bc(cherrypy.request.headers['User-Agent']) # if b: # browser = "%s %s.%s on %s" % (b.name(), b.version()[0], b.version()[1], b.get("platform")) # else: # browser = cherrypy.request.headers['User-Agent'] # log_message += "using %s." % browser except: client_os = 'an OS' client_browser = 'a browser' try: if bitrate: bitrate = str(bitrate) force_transcode = False if bitrate and \ (int(bitrate) in [8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320]) and \ (song['bitrate'] / 1024 > int(bitrate)): force_transcode = True except: pass try: song_format = [song['mimetype'].split('/')[1], os.path.splitext(path)[1].lower()[1:]] except: song_format = [os.path.splitext(path)[1].lower()[1:]] if True in [True for x in song_format if x in ['mp3']]: song_mime = 'audio/mpeg' song_format = ['mp3'] elif True in [True for x in song_format if x in ['ogg', 'vorbis', 'oga']]: song_mime = 'audio/ogg' song_format = ['ogg', 'vorbis', 'oga'] elif True in [True for x in song_format if x in ['m4a', 'aac', 'mp4']]: song_mime = 'audio/x-m4a' song_format = ['m4a', 'aac', 'mp4'] else: song_mime = 'application/octet-stream' if not (format or bitrate): #log_message += " The client did not request any specific format or bitrate so the file is being sent as-is (%s kbps %s)." % (str(song['bitrate'] / 1000), str(song_format)) log_message += "(%skbps %s)" % (str(song['bitrate']), song_format[0]) if client_os and client_browser: log_message += " using %s on %s." % (client_browser, client_os) else: log_message += "." logger.info(log_message) if not os.name == 'nt': path = path.encode(cfg['ENCODING']) return serve_file(path, song_mime, "inline", os.path.split(path)[1]) if format: format = str(format).split(',') else: format = song_format logger.debug("The client wants %s and the file is %s" % (format, song_format)) if True in [True for x in format if x in song_format] and not force_transcode: #if bitrate: # log_message += " The client requested %s kbps %s, but the file is already %s kbps %s, so the file is being sent as-is." % (bitrate, format, str(song['bitrate'] / 1000), str(song_format)) #else: # log_message += " The client requested %s, but the file is already %s, so the file is being sent as-is." % (format, str(song_format)) log_message += "(%skbps %s)" % (str(song['bitrate'] / 1000), song_format[0]) if client_os and client_browser: log_message += " using %s on %s." % (client_browser, client_os) else: log_message += "." if show_log: logger.info(log_message) if not os.name == 'nt': path = path.encode(cfg['ENCODING']) return serve_file(path, song_mime, "inline", os.path.split(path)[1]) else: #if bitrate: # log_message = " The client requested %s kbps %s, but the file is %s kbps %s, so we're transcoding the file for them." % (bitrate, format, str(song['bitrate'] / 1000), str(song_format)) #else: # log_message += " The client requested %s, but the file %s, so we're transcoding the file for them." % (format, str(song_format)) log_message += "(transcoded from %skbps %s to %skbps %s)" % (str(song['bitrate'] / 1000), song_format[0], str(bitrate), format[0]) if client_os and client_browser: log_message += " using %s on %s." % (client_browser, client_os) else: log_message += "." if show_log: logger.info(log_message) # If we're transcoding audio and the client is trying to make range # requests, we have to throw an error 416. This sucks because it breaks # <audio> in all the WebKit browsers I've tried, but at least it stops # them from spawning a zillion transcoder threads (I'm looking at you, # Chromium). if True in [True for x in format if x in ['mp3']]: # cherrypy.response.headers['Content-Length'] = '-1' if range_request != 'bytes=0-': logger.debug("Got a range request for a file that needs transcoded: %s" % range_request) raise cherrypy.HTTPError(416) else: cherrypy.response.headers['Content-Type'] = 'audio/mpeg' try: if cherrypy.request.headers['Referer'].lower().endswith('jplayer.swf'): cherrypy.response.headers['Content-Type'] = 'audio/mp3' except: pass #cherrypy.response.headers['Content-Type'] = 'application/octet-stream' return self.transcoder.transcode(path, 'mp3', bitrate) elif True in [True for x in format if x in ['ogg', 'vorbis', 'oga']]: # cherrypy.response.headers['Content-Length'] = '-1' if range_request != 'bytes=0-': logger.debug("Got a range request for a file that needs transcoded: %s" % range_request) raise cherrypy.HTTPError(416) else: cherrypy.response.headers['Content-Type'] = 'audio/ogg' #cherrypy.response.headers['Content-Type'] = 'application/octet-stream' return self.transcoder.transcode(path, 'ogg', bitrate) elif True in [True for x in format if x in ['m4a', 'aac', 'mp4']]: # cherrypy.response.headers['Content-Length'] = '-1' if range_request != 'bytes=0-': logger.debug("Got a range request for a file that needs transcoded: %s" % range_request) raise cherrypy.HTTPError(416) else: cherrypy.response.headers['Content-Type'] = 'audio/x-m4a' #cherrypy.response.headers['Content-Type'] = 'application/octet-stream' return self.transcoder.transcode(path, 'm4a', bitrate) else: raise cherrypy.HTTPError(501)
def transcode_process(conn, path, stop, format='mp3', bitrate=False): import gi gi.require_version('Gst', '1.0') from gi.repository import GObject, Gst GObject.threads_init() Gst.init(None) # If we were passed a bitrate argument, make sure it's actually a number try: bitrate = int(bitrate) except: bitrate = False log_message = "Transcoding %s to %s" % (path.encode(cfg['ENCODING']), format) # Create our transcoding pipeline using one of the strings at the end of # this module. transcoder = Gst.parse_launch(pipeline[format]) # Set the bitrate we were asked for if bitrate in [8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320]: log_message += " at %d kbps" % bitrate encoder = transcoder.get_by_name('encoder') muxer = transcoder.get_by_name('muxer') if format is 'mp3': encoder.set_property("target", "bitrate") encoder.set_property("bitrate", bitrate) elif format is 'ogg': encoder.set_property("max-bitrate", bitrate * 1024) elif format is 'm4a': encoder.set_property("bitrate", bitrate * 1000) #encoder.set_property("outputformat", 1) #encoder.set_property("profile", 1) #muxer.set_property("faststart", True) # Load our file into the transcoder log_message += "." logger.info(log_message) source = transcoder.get_by_name('source') try: source.set_property("location", path.encode('utf-8')) # Set the output to be asynchronous so the transcoding happens as quickly # as possible rather than real time. output = transcoder.get_by_name('output') output.set_property("sync", False) # Start the pipeline running so we can start grabbing data out of it transcoder.set_state(Gst.State.PLAYING) #transcoder.get_state() try: # Grab a bit of encoded data and yield it to the client while True: sample = output.emit('pull-sample') if sample and not stop.is_set(): buf = sample.get_buffer() data = buf.extract_dup(0, buf.get_size()) conn.send(data) else: break except Exception as e: logger.warn(str(e)) logger.warn("Some type of error occured during transcoding.") except Exception as e: logger.error(str(e)) logger.error("Could not open the file for transcoding. This is probably happening because there are non-ASCII characters in the filename.") finally: try: # I think this is supposed to free the memory used by the transcoder transcoder.set_state(Gst.State.NULL) except: pass if not stop.is_set(): conn.send(False) conn.close()
def read_metadata((location, id, mtime, revision)): """Read the metadata of a file and return it as a dict.""" # Try to open the file and read its tags try: metadata = mutagen.File(location, None, True) except: logger.error("%s made Mutagen explode." % location) return None # Create the metadata object we're going to return with some default # values filled in. This is just in case there aren't tags for these # things so we don't run into problems elsewhere with this data not # being there. song = { '_id': id, 'location': location, 'title': os.path.split(location)[1], 'mtime': mtime, 'artist': 'Unknown Artist', 'album': 'Unknown Album', 'genre': [], 'date': '', 'tracknumber': 0, 'discnumber': 1, 'bitrate': 0, 'length': 0, 'subtitle': '', 'discsubtitle': '', 'compilation': False, 'replaygain_track_gain': None, 'replaygain_album_gain': None, 'replaygain_track_peak': None, 'replaygain_album_peak': None, 'type': 'song' } if revision: song['_rev'] = revision # Try to set the length of the song in seconds, the bitrate in bps and # the mimetype and a default track number of the file. try: song['length'] = metadata.info.length except: pass try: song['bitrate'] = metadata.info.bitrate except: pass try: song['mimetype'] = metadata.mime[0] except: pass # Now we parse all the metadata and transfer it to our song object. if os.path.splitext(location)[1].lower()[1:] in ('wma', 'asf'): song = parse_wma(song, metadata) else: song = parse_metadata(song, metadata) # Fill in some needed tags if they're not present if not song.has_key('albumartist'): song['albumartist'] = song['artist'] for field in ('title', 'artist', 'album', 'albumartist'): if not song.has_key('%ssort' % field): song['%ssort' % field] = song[field] # Create artist and album IDs which are just SHA-1 hashes of each field for field in ('artist', 'album'): value = song[field].encode('utf-8') song['%s_hash' % field] = hashlib.sha1(value).hexdigest() return song