Permalink
Browse files

Initial commit

  • Loading branch information...
0 parents commit 0b37151e55d328dcf5b0f8aef6a38d552355ae1d @lalinsky lalinsky committed Mar 20, 2011
@@ -0,0 +1,20 @@
+Copyright (c) 2011 Lukas Lalinsky
+
+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
+AUTHORS OR COPYRIGHT HOLDERS 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.
+
@@ -0,0 +1,11 @@
+[database]
+user=acoustid_ro
+password=Acoustid12345
+superuser=pgsql
+name=acoustid
+host=acoustid.org
+port=5432
+
+[logging]
+#echo_queries=yes
+
@@ -0,0 +1,48 @@
+# Copyright (C) 2011 Lukas Lalinsky
+# Distributed under the MIT license, see the LICENSE file for details.
+
+from werkzeug.exceptions import HTTPException
+from werkzeug.routing import Map, Rule, Submount
+from werkzeug.wrappers import Request, Response
+import sqlalchemy
+from acoustid.config import Config
+import acoustid.handlers.admin
+from acoustid import api, website, handlers
+
+
+api_url_rules = [
+ Submount('/ws', [
+ Rule('/lookup', endpoint=api.LookupHandler),
+ Rule('/submit', endpoint=api.SubmitHandler),
+ ])
+]
+
+admin_url_rules = [
+ Submount('/admin', [
+ Rule('/merge-missing-mbids', endpoint=handlers.admin.MergeMissingMBIDsHandler),
+ ])
+]
+
+website_url_rules = [
+ Rule('/', endpoint=website.IndexHandler),
+]
+
+
+class Server(object):
+
+ def __init__(self, config_path):
+ url_rules = website_url_rules + api_url_rules + admin_url_rules
+ self.url_map = Map(url_rules, strict_slashes=False)
+ self.config = Config(config_path)
+ self.engine = sqlalchemy.create_engine(self.config.database.create_url(), echo=self.config.logging.echo_queries)
+
+ def __call__(self, environ, start_response):
+ urls = self.url_map.bind_to_environ(environ)
+ try:
+ handler_class, args = urls.match()
+ handler = handler_class.create_from_server(self)
+ response = handler.handle(Request(environ))
+ except HTTPException, e:
+ return e(environ, start_response)
+ return response(environ, start_response)
+
@@ -0,0 +1,104 @@
+# Copyright (C) 2011 Lukas Lalinsky
+# Distributed under the MIT license, see the LICENSE file for details.
+
+from acoustid.handler import Handler, Response
+from acoustid.track import lookup_mbids
+from acoustid.musicbrainz import lookup_metadata
+from acoustid.fingerprintdata import FingerprintData
+from werkzeug.exceptions import HTTPException
+import xml.etree.cElementTree as etree
+import chromaprint
+
+
+def xml_response(elem, **kwargs):
+ xml = etree.tostring(elem, encoding="UTF-8")
+ return Response(xml, content_type='text/xml')
+
+
+def error_response(error):
+ root = etree.Element('response', status='error')
+ etree.SubElement(root, 'error').text = error
+ return xml_response(root, status=400)
+
+
+class BadRequest(HTTPException):
+
+ code = 400
+
+ def get_response(self, environ):
+ return error_response(self.description)
+
+
+class MissingArgument(BadRequest):
+
+ def __init__(self, name):
+ description = "Missing argument '%s'" % (name,)
+ BadRequest.__init__(self, description)
+
+
+class LookupHandler(Handler):
+
+ def __init__(self, conn, fingerprint_data):
+ self.conn = conn
+ self.fingerprint_data = fingerprint_data
+
+ def _inject_metadata(self, meta, result_map):
+ track_mbid_map = lookup_mbids(self.conn, result_map.keys())
+ if meta > 1:
+ all_mbids = []
+ for track_id, mbids in track_mbid_map.iteritems():
+ all_mbids.extend(mbids)
+ track_meta_map = lookup_metadata(self.conn, all_mbids)
+ for track_id, mbids in track_mbid_map.iteritems():
+ result = result_map[track_id]
+ tracks = etree.SubElement(result, 'tracks')
+ for mbid in mbids:
+ track = etree.SubElement(tracks, 'track')
+ etree.SubElement(track, 'id').text = str(mbid)
+ if meta == 1:
+ continue
+ track_meta = track_meta_map[mbid]
+ etree.SubElement(track, 'name').text = track_meta['name']
+ artist = etree.SubElement(track, 'artist')
+ etree.SubElement(artist, 'id').text = track_meta['artist_id']
+ etree.SubElement(artist, 'name').text = track_meta['artist_name']
+
+ def handle(self, req):
+ fingerprint_string = req.args.get('fingerprint')
+ if not fingerprint_string:
+ raise MissingArgument('fingerprint')
+ fingerprint, version = chromaprint.decode_fingerprint(fingerprint_string)
+ if version != 1:
+ raise BadRequest('Unsupported fingerprint version')
+ length = req.args.get('length', type=int)
+ if not length:
+ raise MissingArgument('length')
+ meta = req.args.get('meta', type=int, default=0)
+ root = etree.Element('response', status='ok')
+ results = etree.SubElement(root, 'results')
+ matches = self.fingerprint_data.search(fingerprint, length, 0.7, 0.3)
+ result_map = {}
+ for fingerprint_id, track_id, score in matches:
+ if track_id in result_map:
+ continue
+ result_map[track_id] = result = etree.SubElement(results, 'result')
+ etree.SubElement(result, 'id').text = str(track_id)
+ etree.SubElement(result, 'score').text = '%.2f' % score
+ if meta and result_map:
+ self._inject_metadata(meta, result_map)
+ return xml_response(root)
+
+ @classmethod
+ def create_from_server(cls, server):
+ conn = server.engine.connect()
+ return cls(conn, FingerprintData(conn))
+
+
+class SubmitHandler(Handler):
+ def __init__(self):
+ pass
+
+ @classmethod
+ def create_from_server(cls, server):
+ return cls()
+
@@ -0,0 +1,67 @@
+# Copyright (C) 2011 Lukas Lalinsky
+# Distributed under the MIT license, see the LICENSE file for details.
+
+import logging
+import ConfigParser
+from sqlalchemy.engine.url import URL
+
+logger = logging.getLogger(__name__)
+
+
+class DatabaseConfig(object):
+
+ def __init__(self):
+ self.user = None
+ self.superuser = 'postgres'
+ self.name = None
+ self.host = None
+ self.port = None
+ self.password = None
+
+ def create_url(self, superuser=False):
+ kwargs = {}
+ if superuser:
+ kwargs['username'] = self.superuser
+ else:
+ kwargs['username'] = self.user
+ kwargs['database'] = self.name
+ if self.host is not None:
+ kwargs['host'] = self.host
+ if self.port is not None:
+ kwargs['port'] = self.port
+ if self.password is not None:
+ kwargs['password'] = self.password
+ return URL('postgresql+psycopg2', **kwargs)
+
+ def read(self, parser, section):
+ self.user = parser.get(section, 'user')
+ self.name = parser.get(section, 'name')
+ if parser.has_option(section, 'host'):
+ self.host = parser.get(section, 'host')
+ if parser.has_option(section, 'port'):
+ self.port = parser.getint(section, 'port')
+ if parser.has_option(section, 'password'):
+ self.password = parser.get(section, 'password')
+
+
+class LoggingConfig(object):
+
+ def __init__(self):
+ self.echo_queries = False
+
+ def read(self, parser, section):
+ if parser.has_option(section, 'echo_queries'):
+ self.echo_queries = parser.getboolean(section, 'echo_queries')
+
+
+class Config(object):
+
+ def __init__(self, path):
+ logger.info("Loading configuration file %s", path)
+ parser = ConfigParser.RawConfigParser()
+ parser.read(path)
+ self.database = DatabaseConfig()
+ self.database.read(parser, 'database')
+ self.logging = LoggingConfig()
+ self.logging.read(parser, 'logging')
+
@@ -0,0 +1,52 @@
+# Copyright (C) 2011 Lukas Lalinsky
+# Distributed under the MIT license, see the LICENSE file for details.
+
+import logging
+import chromaprint
+from sqlalchemy import sql
+from acoustid import tables as schema
+
+logger = logging.getLogger(__name__)
+
+
+class FingerprintData(object):
+
+ MAX_LENGTH_DIFF = 10
+ QUERY_SIZE = 120
+ PARTS = ((100, 20), (1, 100))
+ PART_SEARCH_SQL = """
+ SELECT id, track_id, acoustid_compare(fingerprint, query) AS score
+ FROM fingerprint, (SELECT %(fp)s AS query) q
+ WHERE
+ length BETWEEN %(length)s - %(max_length_diff)s AND %(length)s + %(max_length_diff)s AND (
+ (%(length)s >= 34 AND subarray(extract_fp_query(query), %(part_start)s, %(part_length)s)
+ && extract_fp_query(fingerprint)) OR
+ (%(length)s <= 50 AND subarray(extract_short_fp_query(query), %(part_start)s, %(part_length)s)
+ && extract_short_fp_query(fingerprint))
+ )
+ """
+
+ def __init__(self, db):
+ self._db = db
+
+ def search(self, fp, length, good_enough_score, min_score):
+ matched = []
+ best_score = 0.0
+ for part_start, part_length in self.PARTS:
+ logger.info("Searching for %i:%i", part_start, part_start + part_length - 1)
+ result = self._db.execute(self.PART_SEARCH_SQL, dict(fp=fp, length=length,
+ part_start=part_start, part_length=part_length,
+ max_length_diff=self.MAX_LENGTH_DIFF))
+ try:
+ for row in result:
+ if row['score'] >= min_score:
+ matched.append(row)
+ if row['score'] >= best_score:
+ best_score = row['score']
+ finally:
+ result.close()
+ if best_score > good_enough_score:
+ break
+ return matched
+
+
@@ -0,0 +1,12 @@
+from werkzeug.wrappers import Response
+
+
+class Handler(object):
+
+ @classmethod
+ def create_from_server(cls, server):
+ return cls()
+
+ def handle(self, req):
+ raise NotImplementedError(self.handle)
+
No changes.
@@ -0,0 +1,20 @@
+# Copyright (C) 2011 Lukas Lalinsky
+# Distributed under the MIT license, see the LICENSE file for details.
+
+from acoustid.handler import Handler, Response
+from acoustid.track import merge_missing_mbids
+
+
+class MergeMissingMBIDsHandler(Handler):
+
+ def __init__(self, conn):
+ self.conn = conn
+
+ @classmethod
+ def create_from_server(cls, server):
+ return cls(server.engine.connect())
+
+ def handle(self, req):
+ merge_missing_mbids(self.conn)
+ return Response('OK')
+
@@ -0,0 +1,35 @@
+# Copyright (C) 2011 Lukas Lalinsky
+# Distributed under the MIT license, see the LICENSE file for details.
+
+import logging
+from sqlalchemy import sql
+from acoustid import tables as schema
+
+logger = logging.getLogger(__name__)
+
+
+def lookup_metadata(conn, mbids):
+ """
+ Lookup MusicBrainz metadata for the specified MBIDs.
+ """
+ query = sql.select(
+ [
+ schema.mb_track.c.gid,
+ schema.mb_track.c.name,
+ schema.mb_track.c.length,
+ schema.mb_artist.c.gid.label('artist_gid'),
+ schema.mb_artist.c.name.label('artist_name')
+ ],
+ schema.mb_track.c.gid.in_(mbids),
+ from_obj=schema.mb_track.join(schema.mb_artist))
+ results = {}
+ for row in conn.execute(query):
+ results[row['gid']] = {
+ 'id': row['gid'],
+ 'name': row['name'],
+ 'length': row['length'] / 1000,
+ 'artist_id': row['artist_gid'],
+ 'artist_name': row['artist_name'],
+ }
+ return results
+
@@ -0,0 +1,16 @@
+# Copyright (C) 2011 Lukas Lalinsky
+# Distributed under the MIT license, see the LICENSE file for details.
+
+import logging
+import sqlalchemy
+from acoustid.config import Config
+
+logger = logging.getLogger(__name__)
+
+
+class Script(object):
+
+ def __init__(self, config_path):
+ self.config = Config(config_path)
+ self.engine = sqlalchemy.create_engine(self.config.database.create_url())
+
Oops, something went wrong.

0 comments on commit 0b37151

Please sign in to comment.