summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichaël Ball <michael.ball@gmail.com>2017-03-26 10:19:59 +0100
committerMichaël Ball <michael.ball@gmail.com>2017-06-04 07:37:53 +0100
commitd06f96388d754ed41876f7fccb63f84241d44963 (patch)
tree640a4f3eaf7e1f2b76a246a1977c27775d0b59a1
parentcaa1c3ccdf94ee20140b3964aab0ad3058e03699 (diff)
Works on python 2/pypy
-rw-r--r--app.dbbin4096 -> 2048 bytes
-rw-r--r--app.py24
-rw-r--r--common/security.py6
-rw-r--r--db/db_manager.py275
-rwxr-xr-xlibrary.py192
-rw-r--r--mach2.py291
-rw-r--r--models/album.py80
-rw-r--r--models/artist.py75
-rw-r--r--models/base.py13
-rw-r--r--models/track.py152
-rw-r--r--models/user.py17
-rw-r--r--tests/conftest.py8
-rw-r--r--tests/mach2_test.py11
-rw-r--r--tests/models/album_test.py16
-rw-r--r--tests/models/artist_test.py20
-rw-r--r--tests/models/track_test.py36
-rw-r--r--tests/test.dbbin19456 -> 19456 bytes
-rw-r--r--tests/testapp.dbbin4096 -> 2048 bytes
-rw-r--r--tests/testnew.oggbin0 -> 3929 bytes
-rw-r--r--tests/watcher_test.py96
-rw-r--r--watcher.py120
21 files changed, 845 insertions, 587 deletions
diff --git a/app.db b/app.db
index 7493558..2d93462 100644
--- a/app.db
+++ b/app.db
Binary files differ
diff --git a/app.py b/app.py
new file mode 100644
index 0000000..74d87d8
--- /dev/null
+++ b/app.py
@@ -0,0 +1,24 @@
+#!/usr/bin/env python
+"""Runs mach2."""
+from gevent import joinall, monkey, spawn
+from gevent.pywsgi import WSGIServer
+monkey.patch_all(thread=False)
+
+from six.moves import configparser # NOQA : E402
+
+from mach2 import create_app # NOQA : E402
+from watcher import LibraryWatcher # NOQA : E402
+
+APP = create_app()
+
+
+if __name__ == "__main__":
+ config = configparser.ConfigParser()
+ config.read("mach2.ini")
+ watcher = spawn(LibraryWatcher, config.get("DEFAULT", "media_dir"),
+ config.get("DEFAULT", "library"))
+
+ http_server = WSGIServer(('', 5000), APP, log=None)
+ server = spawn(http_server.serve_forever)
+
+ joinall([server, watcher])
diff --git a/common/security.py b/common/security.py
index 91acb2b..e3ab5de 100644
--- a/common/security.py
+++ b/common/security.py
@@ -1,12 +1,10 @@
-import configparser
-
from passlib.context import CryptContext
-
+from six.moves import configparser
config = configparser.ConfigParser()
config.read("mach2.ini")
-secret_key = config["DEFAULT"]["secret_key"]
+secret_key = config.get("DEFAULT", "secret_key")
pwd_context = CryptContext(
schemes=["pbkdf2_sha256", "des_crypt"],
diff --git a/db/db_manager.py b/db/db_manager.py
index ad2fd14..9c2dd53 100644
--- a/db/db_manager.py
+++ b/db/db_manager.py
@@ -1,9 +1,18 @@
-import configparser
+"""
+db_manager exposes a DbManager class to make interacting with sqlite
+databases easier.
+"""
+import logging
import os
import sqlite3
+import six
-class DbManager:
+
+_LOGGER = logging.getLogger(__name__)
+
+class DbManager(object):
+ """DBManager makes interacting with sqlite databases easier."""
create_album_table = "CREATE TABLE IF NOT EXISTS album (id INTEGER "\
"PRIMARY KEY, name TEXT, date TEXT, musicbrainz_albumid TEXT)"
@@ -38,141 +47,133 @@ class DbManager:
create_track_number_index = "CREATE INDEX IF NOT EXISTS "\
"track_tracknumber_IDX ON track(tracknumber)"
- class __DbManager:
- config = configparser.ConfigParser()
- config.read("mach2.ini")
-
- def iterdump(connection):
- cu = connection.cursor()
- yield("BEGIN TRANSACTION;")
-
- q = """
- SELECT "name", "type", "sql"
- FROM "sqlite_master"
- WHERE "sql" NOT NULL AND
- "type" == 'table'
- ORDER BY "name";
- """
- schema_res = cu.execute(q).fetchall()
- for table_name, type, sql in schema_res:
- if table_name == "sqlite_sequence":
- yield("DELETE FROM \"sqlite_sequence\";")
- elif table_name == "sqlite_stat1":
- yield("ANALYZE \"sqlite_master\";")
- elif table_name.startswith("sqlite_"):
- continue
- else:
- yield("{0};".format(sql))
-
- table_name_ident = table_name.replace("\"", "\"\"")
- res = cu.execute("PRAGMA table_info(\"{0}\")".format(
- table_name_ident))
- column_names = [
- str(table_info[1]) for table_info in res.fetchall()]
- q = """
- SELECT 'INSERT INTO "{0}" VALUES({1})' FROM "{0}";
- """.format(table_name_ident, ",".join(
+ @staticmethod
+ def iterdump(connection):
+ """Iterates through the database, creating commands to dump all the
+ tables line by line."""
+ cursor = connection.cursor()
+
+ query = """
+ SELECT "name", "type", "sql"
+ FROM "sqlite_master"
+ WHERE "sql" NOT NULL AND
+ "type" == 'table'
+ ORDER BY "name";
+ """
+ schema_res = cursor.execute(query).fetchall()
+ for table_name, dummy, sql in schema_res:
+ if table_name == "sqlite_sequence":
+ yield "DELETE FROM \"sqlite_sequence\";"
+ elif table_name == "sqlite_stat1":
+ yield "ANALYZE \"sqlite_master\";"
+ elif table_name.startswith("sqlite_"):
+ continue
+ else:
+ yield six.u("{0};").format(sql)
+
+ table_name_ident = table_name.replace("\"", "\"\"")
+ res = cursor.execute("PRAGMA table_info(\"{0}\")".format(
+ table_name_ident))
+ column_names = [
+ str(table_info[1]) for table_info in res.fetchall()]
+ query = """
+ SELECT 'INSERT INTO "{0}" VALUES({1})' FROM "{0}";
+ """.format(table_name_ident, ",".join(
"""'||quote("{0}")||'""".format(
col.replace(
"\"", "\"\"")) for col in column_names))
- query_res = cu.execute(q)
- for row in query_res:
- yield("{0};".format(row[0]))
-
- q = """
- SELECT "name", "type", "sql"
- FROM "sqlite_master"
- WHERE "sql" NOT NULL AND
- "type" IN ('index', 'trigger', 'view')
- """
- schema_res = cu.execute(q)
- for name, type, sql in schema_res.fetchall():
- yield("{0};".format(sql))
-
- yield("COMMIT;")
-
- def __init__(self, db=None):
- new_db = False
- cache_size_kb = 9766
-
- if db:
- self.conn = sqlite3.connect(db)
-
- else:
- if not os.path.isfile(self.config["DEFAULT"]["database"]):
- new_db = True
-
- if new_db:
- self.conn = sqlite3.connect(":memory:")
- self.create_tables()
- else:
- self.conn = sqlite3.connect(
- self.config["DEFAULT"]["database"])
- library_info = os.stat(self.config["DEFAULT"]["database"])
- cache_size_kb = round((library_info.st_size * 1.2) / 1024)
-
- cursor = self.conn.cursor()
- # Setting pragma with ? placeholder produces an error
- cursor.execute("pragma cache_size=-%s" % cache_size_kb)
- cursor.close()
-
- self.conn.row_factory = sqlite3.Row
-
- def export(self):
- if not os.path.isfile(self.config["DEFAULT"]["database"]):
- script = ""
-
- for line in DbManager.__DbManager.iterdump(self.conn):
- script = "\n".join((script, line))
-
- tempconn = sqlite3.connect(
- self.config["DEFAULT"]["database"])
- tempcur = tempconn.cursor()
-
- tempcur.executescript(script)
- tempcur.close()
-
- def __str__(self):
- return repr(self)
-
- def execute(self, script, parameters=None):
- if parameters:
- return self.conn.execute(script, parameters)
-
- return self.conn.execute(script)
-
- def commit(self):
- return self.conn.commit()
-
- def cursor(self):
- return self.conn.cursor()
-
- def close(self):
- return self.conn.close()
-
- def interrupt(self):
- return self.conn.interrupt()
-
- def create_tables(self):
- with self.conn:
- self.conn.execute(DbManager.create_album_table)
- self.conn.execute(DbManager.create_album_artist_table)
- self.conn.execute(DbManager.create_album_track_table)
- self.conn.execute(DbManager.create_artist_table)
- self.conn.execute(DbManager.create_artist_track_table)
- self.conn.execute(DbManager.create_track_table)
- self.conn.execute(DbManager.create_musicbrainz_artist_index)
- self.conn.execute(DbManager.create_track_filename_index)
- self.conn.execute(DbManager.create_track_grouping_index)
- self.conn.execute(DbManager.create_track_name_index)
- self.conn.execute(DbManager.create_track_number_index)
-
- instance = None
-
- def __new__(self, db=None):
- if db:
- return DbManager.__DbManager(db)
- elif not DbManager.instance:
- DbManager.instance = DbManager.__DbManager()
-
- return DbManager.instance
+ query_res = cursor.execute(query)
+ for row in query_res:
+ yield six.u("{0};").format(row[0])
+
+ query = """
+ SELECT "name", "type", "sql"
+ FROM "sqlite_master"
+ WHERE "sql" NOT NULL AND
+ "type" IN ('index', 'trigger', 'view')
+ """
+ schema_res = cursor.execute(query)
+ for dummy, dummy2, sql in schema_res.fetchall():
+ yield six.u("{0};").format(sql)
+
+ def __init__(self, db_file):
+ new_db = False
+ cache_size_kb = 9766
+ self.db_file = db_file
+
+ if not os.path.isfile(self.db_file):
+ new_db = True
+
+ if new_db:
+ self.conn = sqlite3.connect(":memory:")
+ self.create_tables()
+ else:
+ self.conn = sqlite3.connect(self.db_file)
+ library_info = os.stat(self.db_file)
+ cache_size_kb = round((library_info.st_size * 1.2) / 1024)
+
+ cursor = self.conn.cursor()
+ # Setting pragma with ? placeholder produces an error
+ cursor.execute("pragma cache_size=-%s" % cache_size_kb)
+ cursor.close()
+
+ self.conn.row_factory = sqlite3.Row
+
+ def export(self):
+ """Export the database."""
+ if not os.path.isfile(self.db_file):
+ tempconn = sqlite3.connect(self.db_file)
+ tempcur = tempconn.cursor()
+ tempcur.execute("PRAGMA journal_mode=WAL;")
+ tempcur.close()
+
+ try:
+ with tempconn:
+ for line in DbManager.iterdump(self.conn):
+ tempconn.execute(line)
+
+ except sqlite3.Error as exc:
+ _LOGGER.error(exc)
+
+ tempconn.close()
+
+ def __str__(self):
+ return repr(self)
+
+ def execute(self, script, parameters=None):
+ """Execute an sql statement"""
+ if parameters:
+ return self.conn.execute(script, parameters)
+
+ return self.conn.execute(script)
+
+ def commit(self):
+ """Commit the current transaction"""
+ return self.conn.commit()
+
+ def cursor(self):
+ """Create a cursor"""
+ return self.conn.cursor()
+
+ def close(self):
+ """Close the connection"""
+ return self.conn.close()
+
+ def interrupt(self):
+ """Interrupt the connection"""
+ return self.conn.interrupt()
+
+ def create_tables(self):
+ """Create the database tables"""
+ with self.conn:
+ self.conn.execute(DbManager.create_album_table)
+ self.conn.execute(DbManager.create_album_artist_table)
+ self.conn.execute(DbManager.create_album_track_table)
+ self.conn.execute(DbManager.create_artist_table)
+ self.conn.execute(DbManager.create_artist_track_table)
+ self.conn.execute(DbManager.create_track_table)
+ self.conn.execute(DbManager.create_musicbrainz_artist_index)
+ self.conn.execute(DbManager.create_track_filename_index)
+ self.conn.execute(DbManager.create_track_grouping_index)
+ self.conn.execute(DbManager.create_track_name_index)
+ self.conn.execute(DbManager.create_track_number_index)
diff --git a/library.py b/library.py
index 01924a2..272e9e5 100755
--- a/library.py
+++ b/library.py
@@ -1,99 +1,165 @@
#!/usr/bin/env python
-import configparser
-import gevent
-from gevent import monkey, queue
+"""This module implements a library for storing information on audio tracks."""
import logging
-import mutagen
import os
+from gevent import joinall, monkey, queue, sleep, spawn
+import mutagen
+import six
+from six.moves import configparser
+
from db.db_manager import DbManager
from models.track import Track
-file_store = queue.Queue()
-
logging.basicConfig(format="%(asctime)s %(message)s", level=logging.DEBUG)
+_LOGGER = logging.getLogger(__name__)
+
+if six.PY2:
+ def _u(string):
+ return unicode(string, encoding="utf_8")
+else:
+ def _u(string):
+ return string
+
+
+class MediaLibrary(object):
+ """Implements methods for storing and managing media in a library."""
+
+ def __init__(self, media_dir, database):
+ """Create a media library object.
+
+ Args:
+ media_dir (str): The path of the media directory
+ database (DatabaseManager): The media database
+
+ """
+ self.__media_dir = media_dir
+ self.__database = database
+
+ def store_track_task(self, file_queue):
+ """Store a track from the supplied queue.
+ Args:
+ file_queue (Queue[str]): A queue containing file paths.
-def store_track_task():
- while not file_store.empty():
- path = file_store.get()
- m = mutagen.File(path, easy=True)
- Track.store(path, m)
+ """
+ while not file_queue.empty():
+ path = file_queue.get()
+ metadata = mutagen.File(path, easy=True)
+ Track.store(_u(path), metadata, self.__database)
- gevent.sleep(0)
+ sleep(0)
+ def run(self, path=None):
+ """Store all tracks located in the supplied path.
-def run(path=None):
- db = DbManager()
- if path is not None:
- if os.path.isdir(path):
- store_dir(path)
+ Args:
+ path (str): The path to an audio file or directory containing audio
+ files.
+
+ """
+ if path is not None:
+ if os.path.isdir(path):
+ self.store_dir(path)
+ else:
+ self.store_file(path)
else:
- store_file(path)
- else:
- store_dir("/media/Music")
- db.export()
+ self.store_dir(self.__media_dir)
+
+ self.__database.export()
+
+ def store_file(self, path):
+ """Store an audio file.
+
+ Args:
+ path (str): The path to an audio file.
+
+ """
+ metadata = mutagen.File(path, easy=True)
+ if metadata:
+ if not Track.store(_u(path), metadata, self.__database):
+ _LOGGER.error("Problem saving %s", path)
+
+ def store_dir(self, path):
+ """Store all audio files in a directory.
+ Args:
+ path (str): The path to a directory.
-def store_file(path):
- logger = logging.getLogger("store_file")
- m = mutagen.File(path, easy=True)
- if m:
- if not Track.store(path, m):
- logger.error("Problem saving %s" % (path,))
+ """
+ _LOGGER.info("Scanning files")
+ file_queue = queue.Queue()
-def store_dir(path):
- logger = logging.getLogger("store_dir")
- logger.info("Scanning files")
+ allowed_extensions = [".mp3", ".ogg", ".flac", ".wav", ".aac", ".ape"]
+ for root, dummy, files in os.walk(path):
+ for name in files:
+ file_path = "".join([root, "/", name])
+ dummy, ext = os.path.splitext(file_path)
- allowed_extensions = [".mp3", ".ogg", ".flac", ".wav", ".aac", ".ape"]
- for root, dirs, files in os.walk(path):
- for name in files:
- file_path = "".join([root, "/", name])
- file, ext = os.path.splitext(file_path)
+ if ext.lower() in allowed_extensions:
+ file_queue.put(file_path)
- if ext.lower() in allowed_extensions:
- file_store.put(file_path)
+ _LOGGER.info("Storing tracks")
+ joinall([spawn(self.store_track_task, file_queue)] * 18)
+ _LOGGER.info("Done")
- logger.info("Storing tracks")
- gevent.joinall([gevent.spawn(store_track_task)] * 6)
- logger.info("Done")
+ def delete_file(self, path):
+ """Delete a file from the library.
+ Args:
+ path (str): The path for the file.
-def delete_file(path):
- track = Track.find_by_path(path)
+ """
+ track = Track.find_by_path(_u(path), self.__database)
- if track:
- track_album = track.album
- track_artists = track.artists
+ if track:
+ track_album = track.album
+ track_artists = track.artists
- track.delete()
+ track.delete()
- if track_album and len(track_album.tracks) == 0:
- track_album.delete()
+ if track_album and len(track_album.tracks) == 0:
+ track_album.delete()
- for artist in track_artists:
- if len(artist.tracks) == 0:
- artist.delete()
+ for artist in track_artists:
+ if len(artist.tracks) == 0:
+ artist.delete()
+ def update_file(self, path):
+ """Update a file in the library.
-def update_file(path):
- m = mutagen.File(path, easy=True)
- if m:
- track = Track.find_by_path(path)
- track.update(m)
+ Args:
+ path (str): The path for the file.
+ """
+ metadata = mutagen.File(path, easy=True)
+ if metadata:
+ track = Track.find_by_path(_u(path), self.__database)
+ track.update(metadata)
+
+ def update_track_filename(self, oldpath, newpath):
+ """Update a track's filename.
+
+ Args:
+ oldpath (str): The old path of the file.
+ newpath (str): The new path of the file.
+
+ """
+ track = Track.find_by_path(_u(oldpath), self.__database)
+ track.filename = _u(newpath)
+ track.save()
-def update_track_filename(oldpath, newpath):
- track = Track.find_by_path(oldpath)
- track.filename = newpath
- track.save()
if __name__ == "__main__":
monkey.patch_all(thread=False)
- config = configparser.ConfigParser()
- config.read("mach2.ini")
+ __CONFIG = configparser.ConfigParser()
+ __CONFIG.read("mach2.ini")
+
+ db = DbManager(__CONFIG.get("DEFAULT", "library"))
+ media_path = __CONFIG.get("DEFAULT", "media_dir")
+
+ media_library = MediaLibrary(media_path, db)
- run(config["DEFAULT"]["media_dir"])
+ media_library.run()
diff --git a/mach2.py b/mach2.py
index 412ac9d..e1f186c 100644
--- a/mach2.py
+++ b/mach2.py
@@ -1,70 +1,67 @@
import base64
-import configparser
import json
-import mimetypes
import os
+import subprocess
import sqlite3
import tempfile
-from flask import Blueprint, Flask, Response, current_app, g, redirect, \
- render_template, request, url_for
-from flask.ext.compress import Compress
-from flask.ext.login import LoginManager, current_user, login_required
-from flask.ext.login import login_user, logout_user
-from gevent import subprocess
+import mimetypes
+
+from flask import (Blueprint, Flask, Response, current_app, g, redirect,
+ render_template, request, url_for)
+from flask_compress import Compress
+from flask_login import (LoginManager, current_user, login_required,
+ login_user, logout_user)
+from six.moves import configparser
+from db.db_manager import DbManager
from models.album import Album
from models.artist import Artist
from models.track import Track
from models.user import User
-import builtins
-
-builtins.library_db = None
+_CONFIG = configparser.ConfigParser()
+_CONFIG.read("mach2.ini")
+MACH2 = Blueprint("mach2", __name__)
-config = configparser.ConfigParser()
-config.read("mach2.ini")
-
-mach2 = Blueprint("mach2", __name__)
-
-login_manager = LoginManager()
-login_manager.login_view = "mach2.login"
+_LOGIN_MANAGER = LoginManager()
+_LOGIN_MANAGER.login_view = "mach2.login"
def get_db():
- db = getattr(g, "_database", None)
- if db is None:
- db = sqlite3.connect(current_app.config["DATABASE"])
- db.row_factory = sqlite3.Row
- setattr(g, "_database", db)
+ database = getattr(g, "_database", None)
+ if database is None:
+ database = sqlite3.connect(current_app.config["DATABASE"])
+ database.row_factory = sqlite3.Row
+ setattr(g, "_database", database)
- return db
+ return database
-@mach2.teardown_app_request
+@MACH2.teardown_app_request
def close_connection(exception):
- db = getattr(g, "_database", None)
- if db is not None:
- db.close()
+ database = getattr(g, "_database", None)
+ if database is not None:
+ database.close()
def query_db(query, args=(), one=False):
cur = get_db().execute(query, args)
- rv = cur.fetchall()
+ result = cur.fetchall()
cur.close()
- return (rv[0] if rv else None) if one else rv
+ return (result[0] if result else None) if one else result
-@login_manager.request_loader
-def load_user_from_request(request):
+@_LOGIN_MANAGER.request_loader
+def load_user_from_request(req):
# first, try to login using the api_key url arg
- api_key = request.args.get('api_key', None)
+ api_key = req.args.get('api_key', None)
if not api_key:
# next, try to login using Basic Auth
- api_key = request.headers.get('Authorization', None)
+ api_key = req.headers.get('Authorization', None)
if api_key:
api_key = api_key.replace('Basic ', '', 1)
@@ -84,7 +81,8 @@ def load_user_from_request(request):
password_hash=result[2],
authenticated=0,
active=result[4],
- anonymous=result[5])
+ anonymous=result[5],
+ transcode_command=result[7])
if user:
return user
@@ -93,17 +91,17 @@ def load_user_from_request(request):
return None
-@mach2.route("/")
+@MACH2.route("/")
@login_required
def index():
return render_template("index.html", user=current_user)
-@mach2.route("/albums")
+@MACH2.route("/albums")
@login_required
def albums():
returned_albums = []
- albums = []
+ result_albums = []
order_by = request.args.get("order", None)
order_direction = request.args.get("direction", None)
@@ -138,61 +136,63 @@ def albums():
all_params.update(search_params)
if search_params:
- returned_albums = Album.search(db=builtins.library_db, **all_params)
+ returned_albums = Album.search(current_app.config["LIBRARY"],
+ **all_params)
else:
- returned_albums = Album.all(db=builtins.library_db, **params)
+ returned_albums = Album.all(current_app.config["LIBRARY"], **params)
- for album in returned_albums:
- albums.append(album.as_dict())
+ for returned_album in returned_albums:
+ result_albums.append(returned_album.as_dict())
- return json.dumps(albums)
+ return json.dumps(result_albums)
-@mach2.route("/albums/<int:album_id>/tracks")
+@MACH2.route("/albums/<int:album_id>/tracks")
@login_required
def album_tracks(album_id):
- tracks = []
- album = Album(db=builtins.library_db, id=album_id)
+ result_tracks = []
+ returned_album = Album(current_app.config["LIBRARY"], id=album_id)
- for track in album.tracks:
- tracks.append(track.as_dict())
+ for album_track in returned_album.tracks:
+ result_tracks.append(album_track.as_dict())
- return json.dumps(tracks)
+ return json.dumps(result_tracks)
-@mach2.route("/albums/<int:album_id>/artists")
+@MACH2.route("/albums/<int:album_id>/artists")
@login_required
def album_artists(album_id):
- artists = []
- album = Album(db=builtins.library_db, id=album_id)
+ result_artists = []
+ returned_album = Album(current_app.config["LIBRARY"], id=album_id)
- for artist in album.artists:
- artists.append(artist.as_dict())
+ for album_artist in returned_album.artists:
+ result_artists.append(album_artist.as_dict())
- return json.dumps(artists)
+ return json.dumps(result_artists)
-@mach2.route("/albums/<int:album_id>")
+@MACH2.route("/albums/<int:album_id>")
@login_required
def album(album_id):
- album = Album(db=builtins.library_db, id=album_id)
+ returned_album = Album(current_app.config["LIBRARY"], id=album_id)
- return json.dumps(album.as_dict())
+ return json.dumps(returned_album.as_dict())
-@mach2.route("/albums/<album_name>")
+@MACH2.route("/albums/<album_name>")
@login_required
def album_search(album_name):
- albums = []
+ result_albums = []
- for album in Album.search(db=builtins.library_db,
- name={"data": album_name, "operator": "LIKE"}):
- albums.append(album.as_dict())
+ for returned_album in Album.search(current_app.config["LIBRARY"],
+ name={"data": album_name,
+ "operator": "LIKE"}):
+ result_albums.append(returned_album.as_dict())
- return json.dumps(albums)
+ return json.dumps(result_albums)
-@mach2.route("/artists")
+@MACH2.route("/artists")
@login_required
def artists():
order_by = None
@@ -200,7 +200,7 @@ def artists():
lim = None
off = None
returned_artists = []
- artists = []
+ result_artists = []
if request.args.get("order"):
order_by = request.args.get("order")
@@ -215,66 +215,67 @@ def artists():
off = request.args.get("offset")
if order_by:
- returned_artists = Artist.all(db=builtins.library_db, order=order_by,
- direction=order_direction,
- limit=lim, offset=off)
+ returned_artists = Artist.all(current_app.config["LIBRARY"],
+ order=order_by,
+ direction=order_direction, limit=lim,
+ offset=off)
else:
- returned_artists = Artist.all(db=builtins.library_db, limit=lim,
+ returned_artists = Artist.all(current_app.config["LIBRARY"], limit=lim,
offset=off)
- for artist in returned_artists:
- artists.append(artist.as_dict())
+ for returned_artist in returned_artists:
+ result_artists.append(returned_artist.as_dict())
- return json.dumps(artists)
+ return json.dumps(result_artists)
-@mach2.route("/artists/<int:artist_id>/tracks")
+@MACH2.route("/artists/<int:artist_id>/tracks")
@login_required
def artist_tracks(artist_id):
- tracks = []
- artist = Artist(db=builtins.library_db, id=artist_id)
+ result_tracks = []
+ returned_artist = Artist(current_app.config["LIBRARY"], id=artist_id)
- for track in artist.tracks:
- tracks.append(track.as_dict())
+ for artist_track in returned_artist.tracks:
+ result_tracks.append(artist_track.as_dict())
- return json.dumps(tracks)
+ return json.dumps(result_tracks)
-@mach2.route("/artists/<int:artist_id>/albums")
+@MACH2.route("/artists/<int:artist_id>/albums")
@login_required
def artist_albums(artist_id):
- albums = []
- artist = Artist(db=builtins.library_db, id=artist_id)
+ result_albums = []
+ returned_artist = Artist(current_app.config["LIBRARY"], id=artist_id)
- for album in artist.albums:
- albums.append(album.as_dict())
+ for artist_album in returned_artist.albums:
+ result_albums.append(artist_album.as_dict())
- return json.dumps(albums)
+ return json.dumps(result_albums)
-@mach2.route("/artists/<int:artist_id>")
+@MACH2.route("/artists/<int:artist_id>")
@login_required
def artist_info(artist_id):
- artist = Artist(id=artist_id, db=builtins.library_db)
+ artist = Artist(current_app.config["LIBRARY"], id=artist_id)
return json.dumps(artist.as_dict())
-@mach2.route("/artists/<artist_name>")
+@MACH2.route("/artists/<artist_name>")
@login_required
def artist_search(artist_name):
- artists = []
- for artist in Artist.search(db=builtins.libary_db,
+ result_artists = []
+ for artist in Artist.search(current_app.config["LIBRARY"],
name={
"data": artist_name,
"operator": "LIKE"
- }):
- artists.append(artist.as_dict())
+ }):
+ result_artists.append(artist.as_dict())
return json.dumps(artists)
-@mach2.route("/tracks")
+@MACH2.route("/tracks")
@login_required
def tracks():
order_by = None
@@ -282,7 +283,7 @@ def tracks():
lim = None
off = None
returned_tracks = []
- tracks = []
+ result_tracks = []
if request.args.get("order"):
order_by = request.args.get("order")
@@ -297,50 +298,59 @@ def tracks():
off = request.args.get("offset")
if order_by:
- returned_tracks = Track.all(db=builtins.library_db, order=order_by,
- direction=order_direction, limit=lim,
- offset=off)
+ returned_tracks = Track.all(current_app.config["LIBRARY"],
+ order=order_by, direction=order_direction,
+ limit=lim, offset=off)
else:
- returned_tracks = Track.all(db=builtins.library_db,
+ returned_tracks = Track.all(current_app.config["LIBRARY"],
limit=lim, offset=off)
- for track in returned_tracks:
- tracks.append(track.as_dict())
+ for returned_track in returned_tracks:
+ result_tracks.append(returned_track.as_dict())
- return json.dumps(tracks)
+ return json.dumps(result_tracks)
-@mach2.route("/tracks/<int:track_id>/artists")
+@MACH2.route("/tracks/<int:track_id>/artists")
@login_required
def track_artists(track_id):
- artists = []
- track = Track(db=builtins.library_db, id=track_id)
+ result_artists = []
+ returned_track = Track(current_app.config["LIBRARY"], id=track_id)
- for artist in track.artists:
- artists.append(artist.as_dict())
+ for track_artist in returned_track.artists:
+ result_artists.append(track_artist.as_dict())
- return json.dumps(artists)
+ return json.dumps(result_artists)
-@mach2.route("/tracks/<int:track_id>")
+@MACH2.route("/tracks/<int:track_id>")
@login_required
def track(track_id):
- def stream_file(filename, chunksize=8192):
- with open(filename, "rb") as f:
+ def stream_file(filename, proc, chunksize=8192):
+ with open(filename, "rb") as streamed_file:
while True:
- chunk = f.read(chunksize)
- if chunk:
+ chunk = streamed_file.read(chunksize)
+ if proc.poll() is None or chunk:
yield chunk
- else:
+ elif not chunk:
os.remove(filename)
break
- local_track = Track(db=builtins.library_db, id=track_id)
+ local_track = Track(current_app.config["LIBRARY"], id=track_id)
- fd, temp_filename = tempfile.mkstemp()
+ dummy, temp_filename = tempfile.mkstemp()
- subprocess.call(["ffmpeg", "-y", "-i", local_track.filename, "-acodec",
- "libopus", "-b:a", "64000", "-f", "opus", temp_filename])
+ transcode_tokens = current_user.transcode_command.split()
+ transcode_command_items = []
+ for token in transcode_tokens:
+ if token == "{filename}":
+ transcode_command_items.append(local_track.filename)
+ elif token == "{output}":
+ transcode_command_items.append(temp_filename)
+ else:
+ transcode_command_items.append(token)
+
+ proc = subprocess.Popen(transcode_command_items)
mime_string = "application/octet-stream"
@@ -348,7 +358,7 @@ def track(track_id):
if mime[0]:
mime_string = mime[0]
- resp = Response(stream_file(temp_filename), mimetype=mime_string)
+ resp = Response(stream_file(temp_filename, proc), mimetype=mime_string)
if mime[1]:
resp.headers["Content-Encoding"] = mime[1]
@@ -356,30 +366,32 @@ def track(track_id):
return resp
-@mach2.route("/tracks/<track_name>")
+@MACH2.route("/tracks/<track_name>")
@login_required
def track_search(track_name):
- tracks = []
- for track in Track.search(db=builtins.library_db,
- name={"data": track_name, "operator": "LIKE"}):
- tracks.append(track.as_dict())
+ result_tracks = []
+ for returned_track in Track.search(current_app.config["LIBRARY"],
+ name={"data": track_name,
+ "operator": "LIKE"}):
+ result_tracks.append(returned_track.as_dict())
- return json.dumps(tracks)
+ return json.dumps(result_tracks)
-@login_manager.user_loader
+@_LOGIN_MANAGER.user_loader
def load_user(userid):
user = None
result = query_db("SELECT * FROM user WHERE id = ?", [userid], one=True)
if result:
user = User(id=result[0], username=result[1], password_hash=result[2],
- authenticated=1, active=result[4], anonymous=0)
+ authenticated=1, active=result[4], anonymous=0,
+ transcode_command=result[7])
return user
-@mach2.route("/login", methods=["GET", "POST"])
+@MACH2.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
user = None
@@ -391,7 +403,8 @@ def login():
password_hash=result[2],
authenticated=0,
active=result[4],
- anonymous=result[5])
+ anonymous=result[5],
+ transcode_command=result[7])
password = request.form["password"]
@@ -404,14 +417,14 @@ def login():
return render_template("login.html")
-@mach2.route("/logout")
+@MACH2.route("/logout")
@login_required
def logout():
logout_user()
return redirect("/")
-@mach2.before_app_first_request
+@MACH2.before_app_first_request
def setup_globals():
setattr(g, "_db_path", current_app.config["DATABASE"])
@@ -421,17 +434,19 @@ def create_app(database=None, library=None):
if database:
app.config["DATABASE"] = database
else:
- app.config["DATABASE"] = config["DEFAULT"]["app_db"]
+ app.config["DATABASE"] = _CONFIG.get("DEFAULT", "database")
if library:
- builtins.library_db = library
+ app.config["LIBRARY"] = library
+ else:
+ app.config["LIBRARY"] = DbManager(_CONFIG.get("DEFAULT", "library"))
- app.config["DEBUG"] = config["DEFAULT"]["debug"]
- app.config["SECRET_KEY"] = config["DEFAULT"]["secret_key"]
+ app.config["DEBUG"] = _CONFIG.get("DEFAULT", "debug")
+ app.config["SECRET_KEY"] = _CONFIG.get("DEFAULT", "secret_key")
- app.register_blueprint(mach2)
+ app.register_blueprint(MACH2)
- login_manager.init_app(app)
+ _LOGIN_MANAGER.init_app(app)
compress = Compress()
compress.init_app(app)
@@ -440,5 +455,5 @@ def create_app(database=None, library=None):
if __name__ == "__main__":
- app = create_app()
- app.run()
+ APP = create_app()
+ APP.run()
diff --git a/models/album.py b/models/album.py
index 96bea81..d55fb88 100644
--- a/models/album.py
+++ b/models/album.py
@@ -1,17 +1,16 @@
from common import utils
-from db.db_manager import DbManager
from models.base import BaseModel
class Album(BaseModel):
+ """Represents an album."""
- def __init__(self, id=None, db=None, **kwargs):
- if db:
- self.db = db
+ def __init__(self, db, id=None, **kwargs):
+ self._db = db
if id is not None:
- for row in self.db.execute("SELECT * FROM album WHERE id = ?",
- (id,)):
+ for row in self._db.execute("SELECT * FROM album WHERE id = ?",
+ (id,)):
setattr(self, "id", id)
setattr(self, "name", row[1])
setattr(self, "date", row[2])
@@ -23,25 +22,21 @@ class Album(BaseModel):
for track in self.tracks:
track.delete()
- with self.db.conn:
+ with self._db.conn:
delete_album = "DELETE FROM album WHERE id = ?"
- self.db.execute(delete_album, (self.id,))
+ self._db.execute(delete_album, (self.id,))
delete_track_rel = "DELETE FROM album_track WHERE album_id = ?"
- self.db.execute(delete_track_rel, (self.id,))
+ self._db.execute(delete_track_rel, (self.id,))
delete_artist_rel = "DELETE FROM album_artist WHERE album_id = ?"
- self.db.execute(delete_artist_rel, (self.id,))
+ self._db.execute(delete_artist_rel, (self.id,))
return True
@property
def db(self):
- try:
- return self._db
- except AttributeError:
- self._db = DbManager()
- return self._db
+ return self._db
@db.setter
def db(self, db):
@@ -54,12 +49,12 @@ class Album(BaseModel):
if not hasattr(self, "_artists"):
setattr(self, "_artists", [])
- for row in self.db.execute("SELECT artist.* FROM artist INNER "
- "JOIN album_artist ON artist.id = "
- "album_artist.artist_id WHERE "
- "album_id = ? ORDER BY name ASC",
- (self.id,)):
- artist = Artist(id=row[0], db=self.db, name=row[1],
+ for row in self._db.execute("SELECT artist.* FROM artist INNER "
+ "JOIN album_artist ON artist.id = "
+ "album_artist.artist_id WHERE "
+ "album_id = ? ORDER BY name ASC",
+ (self.id,)):
+ artist = Artist(id=row[0], db=self._db, name=row[1],
sortname=row[2], musicbrainz_artistid=row[3])
self._artists.append(artist)
@@ -72,13 +67,13 @@ class Album(BaseModel):
if not hasattr(self, "_tracks"):
setattr(self, "_tracks", [])
- for row in self.db.execute("SELECT track.* FROM track INNER "
- "JOIN album_track ON track.id = "
- "album_track.track_id WHERE "
- "album_id = ? ORDER BY tracknumber "
- "ASC", (self.id,)):
+ for row in self._db.execute("SELECT track.* FROM track INNER "
+ "JOIN album_track ON track.id = "
+ "album_track.track_id WHERE "
+ "album_id = ? ORDER BY tracknumber "
+ "ASC", (self.id,)):
- track = Track(id=row["id"], db=self.db,
+ track = Track(id=row["id"], db=self._db,
tracknumber=row["tracknumber"],
name=row["name"], grouping=row["grouping"],
filename=row["filename"])
@@ -97,14 +92,15 @@ class Album(BaseModel):
if len(dirty_attributes) > 0:
set_clause = utils.update_clause_from_dict(dirty_attributes)
- dirty_attributes[id] = self.id
+ dirty_attributes["id"] = self.id
- sql = " ".join(("UPDATE album"), set_clause, "WHERE id = :id")
+ sql = " ".join(("UPDATE album", set_clause, "WHERE id = :id"))
- with self.db.conn:
- self.db.execute(sql, dirty_attributes)
+ with self._db.conn:
+ self._db.execute(sql, dirty_attributes)
- def search(db=None, **search_params):
+ @classmethod
+ def search(cls, database, **search_params):
"""Find an album with the given params
Args:
@@ -114,9 +110,6 @@ class Album(BaseModel):
"""
albums = []
- if not db:
- db = DbManager()
-
# unpack search params
where_params = {}
value_params = {}
@@ -136,21 +129,21 @@ class Album(BaseModel):
result = None
if where_clause:
statement = " ".join(("SELECT * FROM album", where_clause))
- result = db.execute(statement, value_params)
+ result = database.execute(statement, value_params)
else:
- result = db.execute("SELECT * FROM album")
+ result = database.execute("SELECT * FROM album")
for row in result:
albums.append(
- Album(id=row["id"], db=db, name=row["name"], date=row["date"])
+ Album(id=row["id"], db=database, name=row["name"],
+ date=row["date"])
)
return albums
- def all(db=None, order="album.id", direction="ASC", limit=None,
+ @classmethod
+ def all(cls, database, order="album.id", direction="ASC", limit=None,
offset=None):
- if not db:
- db = DbManager()
albums = []
@@ -163,11 +156,12 @@ class Album(BaseModel):
select_string = " ".join((select_string,
"LIMIT %s OFFSET %s" % (limit, offset)))
- result = db.execute(select_string)
+ result = database.execute(select_string)
for row in result:
albums.append(
- Album(id=row["id"], db=db, name=row["name"], date=row["date"])
+ Album(id=row["id"], db=database, name=row["name"],
+ date=row["date"])
)
return albums
diff --git a/models/artist.py b/models/artist.py
index a76b2ee..dada665 100644
--- a/models/artist.py
+++ b/models/artist.py
@@ -1,17 +1,15 @@
from common import utils
-from db.db_manager import DbManager
from models.base import BaseModel
class Artist(BaseModel):
- def __init__(self, id=None, db=None, **kwargs):
- if db:
- self.db = db
+ def __init__(self, db, id=None, **kwargs):
+ self._db = db
if id is not None:
- for row in self.db.execute("SELECT * FROM artist WHERE id = ?",
- (id,)):
+ for row in self._db.execute("SELECT * FROM artist WHERE id = ?",
+ (id,)):
for key in ["id", "name", "sortname", "musicbrainz_artistid"]:
setattr(self, key, row[key])
else:
@@ -22,25 +20,21 @@ class Artist(BaseModel):
for album in self.albums:
album.delete()
- with self.db.conn:
+ with self._db.conn:
delete_artist = "DELETE FROM artist WHERE id = ?"
- self.db.execute(delete_artist, (self.id,))
+ self._db.execute(delete_artist, (self.id,))
delete_track_rel = "DELETE FROM artist_track WHERE artist_id = ?"
- self.db.execute(delete_track_rel, (self.id,))
+ self._db.execute(delete_track_rel, (self.id,))
delete_album_rel = "DELETE FROM album_artist WHERE artist_id = ?"
- self.db.execute(delete_album_rel, (self.id,))
+ self._db.execute(delete_album_rel, (self.id,))
return True
@property
def db(self):
- try:
- return self._db
- except AttributeError:
- self._db = DbManager()
- return self._db
+ return self._db
@db.setter
def db(self, db):
@@ -53,13 +47,13 @@ class Artist(BaseModel):
if not hasattr(self, "_tracks"):
setattr(self, "_tracks", [])
- for row in self.db.execute("SELECT track.* FROM track INNER "
- "JOIN artist_track ON track.id = "
- "artist_track.track_id WHERE "
- "artist_id = ? ORDER BY name ASC",
- (self.id,)):
+ for row in self._db.execute("SELECT track.* FROM track INNER "
+ "JOIN artist_track ON track.id = "
+ "artist_track.track_id WHERE "
+ "artist_id = ? ORDER BY name ASC",
+ (self.id,)):
- track = Track(id=row["id"], db=self.db,
+ track = Track(id=row["id"], db=self._db,
tracknumber=row["tracknumber"], name=row["name"],
grouping=row["grouping"],
filename=row["filename"])
@@ -74,12 +68,12 @@ class Artist(BaseModel):
if not hasattr(self, "_albums"):
setattr(self, "_albums", [])
- for row in self.db.execute("SELECT album.* FROM album INNER "
- "JOIN album_artist ON album.id = "
- "album_artist.album_id WHERE "
- "artist_id = ? ORDER BY date ASC",
- (self.id,)):
- album = Album(id=row["id"], db=self.db, name=row["name"],
+ for row in self._db.execute("SELECT album.* FROM album INNER "
+ "JOIN album_artist ON album.id = "
+ "album_artist.album_id WHERE "
+ "artist_id = ? ORDER BY date ASC",
+ (self.id,)):
+ album = Album(id=row["id"], db=self._db, name=row["name"],
date=row["date"])
self._albums.append(album)
@@ -98,12 +92,13 @@ class Artist(BaseModel):
dirty_attributes[id] = self.id
- sql = " ".join(("UPDATE artist"), set_clause, "WHERE id = :id")
+ sql = " ".join(("UPDATE artist", set_clause, "WHERE id = :id"))
- with self.db.conn:
- self.db.execute(sql, dirty_attributes)
+ with self._db.conn:
+ self._db.execute(sql, dirty_attributes)
- def search(db=None, **search_params):
+ @classmethod
+ def search(cls, database, **search_params):
"""Find an artist with the given params
Args:
@@ -113,9 +108,6 @@ class Artist(BaseModel):
"""
artists = []
- if not db:
- db = DbManager()
-
# unpack search params
where_params = {}
value_params = {}
@@ -128,23 +120,22 @@ class Artist(BaseModel):
result = []
if where_clause:
statement = " ".join(("SELECT * FROM artist", where_clause))
- result = db.execute(statement, value_params)
+ result = database.execute(statement, value_params)
else:
- result = db.execute("SELECT * FROM artist")
+ result = database.execute("SELECT * FROM artist")
for row in result:
artists.append(
- Artist(id=row["id"], db=db, name=row["name"],
+ Artist(id=row["id"], db=database, name=row["name"],
sortname=row["sortname"],
musicbrainz_artistid=row["musicbrainz_artistid"])
)
return artists
- def all(db=None, order="sortname", direction="ASC", limit=None,
+ @classmethod
+ def all(cls, database, order="sortname", direction="ASC", limit=None,
offset=None):
- if not db:
- db = DbManager()
artists = []
@@ -155,11 +146,11 @@ class Artist(BaseModel):
select_string = " ".join((select_string,
"LIMIT %s OFFSET %s" % (limit, offset)))
- result = db.execute(select_string)
+ result = database.execute(select_string)
for row in result:
artists.append(
- Artist(id=row["id"], db=db, name=row["name"],
+ Artist(id=row["id"], db=database, name=row["name"],
sortname=row["sortname"],
musicbrainz_artistid=row["musicbrainz_artistid"])
)
diff --git a/models/base.py b/models/base.py
index fd40001..8a6fc47 100644
--- a/models/base.py
+++ b/models/base.py
@@ -1,10 +1,15 @@
-class BaseModel():
+"""Implements a base model for other models to inherit."""
+from six import iteritems
+
+class BaseModel(object):
+ """BaseModel is meant to be inherited by other models."""
def as_dict(self):
+ """Exposes all the object's values as a dict."""
this_dict = {}
- for k in self.__dict__.keys():
- if k != "_db":
- this_dict[k] = getattr(self, k)
+ for key, val in iteritems(self.__dict__):
+ if key != "_db":
+ this_dict[key] = val
return this_dict
diff --git a/models/track.py b/models/track.py
index e0905e6..2a92558 100644
--- a/models/track.py
+++ b/models/track.py
@@ -2,7 +2,6 @@ import logging
import sqlite3
from common import utils
-from db.db_manager import DbManager
from models.artist import Artist
from models.album import Album
from models.base import BaseModel
@@ -13,15 +12,14 @@ logging.basicConfig(format="%(asctime)s %(message)s", level=logging.DEBUG)
class Track(BaseModel):
- def __init__(self, id=None, db=None, **kwargs):
- if db:
- self.db = db
+ def __init__(self, db, id=None, **kwargs):
+ self._db = db
self.__data = {}
if id is not None:
- for row in self.db.execute("SELECT * FROM track WHERE id = ?",
- (id,)):
+ for row in self._db.execute("SELECT * FROM track WHERE id = ?",
+ (id,)):
for key in ["id", "tracknumber", "name", "grouping",
"filename"]:
setattr(self, key, row[key])
@@ -32,11 +30,10 @@ class Track(BaseModel):
self.__data[key] = value
def delete(self):
-
delete_sql = "DELETE FROM track WHERE id = ?"
- with self.db.conn:
- self.db.execute(delete_sql, (self.id,))
+ with self._db.conn:
+ self._db.execute(delete_sql, (self.id,))
# If there is an old album, remove it if it no longer has any
# tracks
@@ -48,8 +45,8 @@ class Track(BaseModel):
old_album = self.album
if old_album:
- self.db.execute("DELETE FROM album_track WHERE track_id = ?",
- (self.id,))
+ self._db.execute("DELETE FROM album_track WHERE track_id = ?",
+ (self.id,))
if not old_album.tracks:
old_album.delete()
@@ -63,8 +60,8 @@ class Track(BaseModel):
old_artists = self.artists
for old_artist in old_artists:
- self.db.execute("DELETE FROM artist_track WHERE track_id = "
- "?", (self.id,))
+ self._db.execute("DELETE FROM artist_track WHERE track_id = "
+ "?", (self.id,))
if not old_artist.tracks:
old_artist.delete()
@@ -73,11 +70,8 @@ class Track(BaseModel):
@property
def db(self):
- try:
- return self._db
- except AttributeError:
- self._db = DbManager()
- return self._db
+ return self._db
+
@db.setter
def db(self, db):
@@ -88,11 +82,11 @@ class Track(BaseModel):
if not hasattr(self, "_album"):
setattr(self, "_album", None)
- for row in self.db.execute("SELECT album.* FROM album INNER "
- "JOIN album_track ON album.id = "
- "album_track.album_id WHERE "
- "track_id = ? LIMIT 1", (self.id,)):
- setattr(self, "_album", Album(id=row["id"], db=self.db,
+ for row in self._db.execute("SELECT album.* FROM album INNER "
+ "JOIN album_track ON album.id = "
+ "album_track.album_id WHERE "
+ "track_id = ? LIMIT 1", (self.id,)):
+ setattr(self, "_album", Album(id=row["id"], db=self._db,
name=row["name"],
date=row["date"]))
@@ -101,7 +95,7 @@ class Track(BaseModel):
@property
def artists(self):
if not hasattr(self, "_artists"):
- cursor = self.db.cursor()
+ cursor = self._db.cursor()
setattr(self, "_artists", [])
for row in cursor.execute("SELECT artist.* FROM artist INNER JOIN "
@@ -109,7 +103,7 @@ class Track(BaseModel):
"artist_track.artist_id WHERE "
"artist_track.track_id = ?",
(self.id,)):
- self._artists.append(Artist(id=row["id"], db=self.db,
+ self._artists.append(Artist(id=row["id"], db=self._db,
name=row["name"],
sortname=row["sortname"],
musicbrainz_artistid=row[
@@ -118,7 +112,10 @@ class Track(BaseModel):
return self._artists
def update(self, metadata):
- c = self.db.cursor()
+ c = self._db.cursor()
+
+ artist_changed = False
+ album_changed = False
artist_names = metadata["artist"]
musicbrainz_artist_ids = []
@@ -136,6 +133,7 @@ class Track(BaseModel):
artists = []
for artist_name in artist_names:
+ artist = None
musicbrainz_artistid = None
artistsort = None
@@ -161,7 +159,7 @@ class Track(BaseModel):
row = rows.fetchone()
if row:
- artist = Artist(id=row["id"], db=self.db, name=row["name"],
+ artist = Artist(id=row["id"], db=self._db, name=row["name"],
sortname=row["sortname"],
musicbrainz_artistid=row[
"musicbrainz_artistid"])
@@ -182,7 +180,7 @@ class Track(BaseModel):
(artist_name, artistsort, musicbrainz_artistid))
artist = Artist(
- id=c.lastrowid, db=self.db, name=artist_name,
+ id=c.lastrowid, db=self._db, name=artist_name,
sortname=artistsort,
musicbrainz_artistid=musicbrainz_artistid
)
@@ -219,7 +217,7 @@ class Track(BaseModel):
row = rows.fetchone()
if row:
- album = Album(id=row["id"], db=self.db, name=row["name"],
+ album = Album(id=row["id"], db=self._db, name=row["name"],
date=row["date"],
musicbrainz_albumid=row["musicbrainz_albumid"])
else:
@@ -227,7 +225,7 @@ class Track(BaseModel):
"musicbrainz_albumid) VALUES (?, ?, ?)",
(album_name, album_date, mb_albumid))
- album = Album(id=c.lastrowid, db=self.db, name=album_name,
+ album = Album(id=c.lastrowid, db=self._db, name=album_name,
date=album_date, musicbrainz_albumid=mb_albumid)
elif album_name:
@@ -239,14 +237,14 @@ class Track(BaseModel):
row = rows.fetchone()
if row:
- album = Album(id=row["id"], db=self.db, name=row["name"],
+ album = Album(id=row["id"], db=self._db, name=row["name"],
date=row["date"],
musicbrainz_albumid=row["musicbrainz_albumid"])
else:
c.execute("INSERT INTO album (name, `date`) VALUES (?, ?)",
(album_name, album_date))
- album = Album(id=c.lastrowid, db=self.db, name=album_name,
+ album = Album(id=c.lastrowid, db=self._db, name=album_name,
date=album_date)
if album:
@@ -290,12 +288,9 @@ class Track(BaseModel):
pass
old_album = self.album
-
if old_album:
- c.execute("DELETE FROM album_track WHERE track_id = ?", (self.id,))
-
- if not old_album.tracks:
- old_album.delete()
+ c.execute("DELETE FROM album_track WHERE track_id = ?",
+ (self.id,))
# If there are old artists, remove them if they no longer have
# any tracks
@@ -309,9 +304,6 @@ class Track(BaseModel):
c.execute("DELETE FROM artist_track WHERE track_id = ?",
(self.id,))
- if not old_artist.tracks:
- old_artist.delete()
-
if album:
try:
c.execute("INSERT INTO album_track (album_id, track_id) "
@@ -319,6 +311,9 @@ class Track(BaseModel):
except sqlite3.IntegrityError:
pass
+ if not old_album.tracks:
+ old_album.delete()
+
setattr(self, "_album", album)
for artist in artists:
@@ -328,6 +323,9 @@ class Track(BaseModel):
except sqlite3.IntegrityError:
pass
+ if not old_artist.tracks:
+ old_artist.delete()
+
if album:
try:
c.execute(
@@ -339,8 +337,8 @@ class Track(BaseModel):
if artists:
setattr(self, "_artists", artists)
- self.db.commit()
c.close()
+ self._db.commit()
return True
@@ -364,10 +362,11 @@ class Track(BaseModel):
sql = " ".join(("UPDATE track", set_clause, "WHERE id = :id"))
- with self.db.conn:
- self.db.execute(sql, dirty_attributes)
+ with self._db.conn:
+ self._db.execute(sql, dirty_attributes)
- def search(db=None, **search_params):
+ @classmethod
+ def search(cls, database, **search_params):
"""Find a track with the given params
Args:
@@ -376,10 +375,6 @@ class Track(BaseModel):
grouping: dict, with 'data' and 'operator' keys
filename: dict, with 'data' and 'operator' keys
"""
-
- if not db:
- db = DbManager()
-
tracks = []
# unpack search params
@@ -394,40 +389,37 @@ class Track(BaseModel):
result = None
if where_clause:
statement = " ".join(("SELECT * FROM track", where_clause))
- result = db.execute(statement, value_params)
+ result = database.execute(statement, value_params)
else:
- result = db.execute("SELECT * FROM track")
+ result = database.execute("SELECT * FROM track")
for row in result:
tracks.append(
- Track(id=row["id"], db=db, tracknumber=row["tracknumber"],
+ Track(id=row["id"], db=database, tracknumber=row["tracknumber"],
name=row["name"], grouping=row["grouping"],
filename=row["filename"])
)
return tracks
- def find_by_path(path, db=None):
- if not db:
- db = DbManager()
+ @classmethod
+ def find_by_path(cls, path, database):
track = None
- for row in db.execute("SELECT * FROM track WHERE filename = ? "
- "LIMIT 1", (path,)):
- track = Track(id=row["id"], db=db, tracknumber=row["tracknumber"],
- name=row["name"], grouping=row["grouping"],
- filename=row["filename"])
+ for row in database.execute("SELECT * FROM track WHERE filename = ? "
+ "LIMIT 1", (path,)):
+ track = Track(id=row["id"], db=database,
+ tracknumber=row["tracknumber"], name=row["name"],
+ grouping=row["grouping"], filename=row["filename"])
return track
- def store(filename, metadata, db=None):
- if Track.find_by_path(filename, db=db):
+ @classmethod
+ def store(cls, filename, metadata, database):
+ if Track.find_by_path(filename, database):
return True
- if not db:
- db = DbManager()
-
- c = db.cursor()
+ c = database.cursor()
artist_names = metadata["artist"]
musicbrainz_artist_ids = []
@@ -475,7 +467,7 @@ class Track(BaseModel):
row = rows.fetchone()
if row:
- artist = Artist(id=row["id"], db=db, name=row["name"],
+ artist = Artist(id=row["id"], db=database, name=row["name"],
sortname=row["sortname"],
musicbrainz_artistid=row[
"musicbrainz_artistid"])
@@ -499,7 +491,7 @@ class Track(BaseModel):
(artist_name, artistsort, musicbrainz_artistid))
artist = Artist(
- id=c.lastrowid, db=db, name=artist_name,
+ id=c.lastrowid, db=database, name=artist_name,
sortname=artistsort,
musicbrainz_artistid=musicbrainz_artistid
)
@@ -534,15 +526,15 @@ class Track(BaseModel):
row = rows.fetchone()
if row:
- album = Album(id=row["id"], db=db, name=row["name"],
+ album = Album(id=row["id"], db=database, name=row["name"],
date=row["date"], musicbrainz_albumid=row[
- "musicbrainz_albumid"])
+ "musicbrainz_albumid"])
else:
c.execute("INSERT INTO album (name, `date`, "
"musicbrainz_albumid) VALUES (?, ?, ?)",
(album_name, album_date, mb_albumid))
- album = Album(id=c.lastrowid, db=db, name=album_name,
+ album = Album(id=c.lastrowid, db=database, name=album_name,
date=album_date, musicbrainz_albumid=mb_albumid)
elif album_name:
@@ -554,13 +546,13 @@ class Track(BaseModel):
row = rows.fetchone()
if row:
- album = Album(id=row["id"], db=db, name=row["name"],
+ album = Album(id=row["id"], db=database, name=row["name"],
date=row["date"])
else:
c.execute("INSERT INTO album (name, `date`) VALUES(?, ?)",
(album_name, album_date))
- album = Album(id=c.lastrowid, db=db, name=album_name,
+ album = Album(id=c.lastrowid, db=database, name=album_name,
date=album_date)
for artist in artists:
@@ -591,7 +583,7 @@ class Track(BaseModel):
rows = c.execute("SELECT * FROM track WHERE filename = ?", (filename,))
row = rows.fetchone()
if row:
- track = Track(id=row["id"], db=db,
+ track = Track(id=row["id"], db=database,
tracknumber=row["tracknumber"], name=row["name"],
grouping=row["grouping"], filename=row["filename"])
else:
@@ -600,7 +592,7 @@ class Track(BaseModel):
(track_number, track_name, track_grouping,
filename))
- track = Track(id=c.lastrowid, db=db, tracknumber=track_number,
+ track = Track(id=c.lastrowid, db=database, tracknumber=track_number,
name=track_name, grouping=track_grouping,
filename=filename)
@@ -618,16 +610,14 @@ class Track(BaseModel):
except sqlite3.IntegrityError:
pass
- db.commit()
+ database.commit()
c.close()
return track
- def all(db=None, order="track.id", direction="ASC", limit=None,
+ @classmethod
+ def all(cls, database, order="track.id", direction="ASC", limit=None,
offset=None):
- if not db:
- db = DbManager()
-
tracks = []
select_string = "SELECT * FROM track LEFT JOIN artist_track ON " \
@@ -641,10 +631,10 @@ class Track(BaseModel):
select_string = " ".join((select_string,
"LIMIT %s OFFSET %s" % (limit, offset)))
- result = db.execute(select_string)
+ result = database.execute(select_string)
for row in result:
- tracks.append(Track(id=row["id"], db=db,
+ tracks.append(Track(id=row["id"], db=database,
tracknumber=row["tracknumber"],
name=row["name"], grouping=row["name"],
filename=row["filename"]))
diff --git a/models/user.py b/models/user.py
index 925df5e..ea90f39 100644
--- a/models/user.py
+++ b/models/user.py
@@ -1,11 +1,11 @@
-import os
+from os import urandom
-from flask.ext.login import make_secure_token
+from itsdangerous import URLSafeTimedSerializer
from common.security import pwd_context, secret_key
-class User:
+class User(object):
def __init__(self, **kwargs):
for (key, value) in kwargs.items():
setattr(self, key, value)
@@ -43,16 +43,17 @@ class User:
def new_password(self, password, category=None):
if self.id:
- hash = None
+ the_hash = None
if category:
- hash = pwd_context.encrypt(password, category=category)
+ the_hash = pwd_context.encrypt(password, category=category)
else:
- hash = pwd_context.encrypt(password)
+ the_hash = pwd_context.encrypt(password)
- api_key = make_secure_token(hash, os.urandom(64), key=secret_key)
+ serializer = URLSafeTimedSerializer(password, salt=urandom(64))
+ api_key = serializer.dumps(the_hash)
- return hash, api_key
+ return the_hash, api_key
else:
raise ValueError("No user")
diff --git a/tests/conftest.py b/tests/conftest.py
index 0028475..90364ce 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -8,8 +8,8 @@ from db.db_manager import DbManager
@pytest.fixture(scope="module")
def database(request):
database = DbManager(
- db=os.path.join(os.path.dirname(os.path.realpath(__file__)),
- "test.db"))
+ os.path.join(os.path.dirname(os.path.realpath(__file__)),
+ "test.db"))
def fin():
database.close()
@@ -32,8 +32,8 @@ def app(request):
db = os.path.join(os.path.dirname(os.path.realpath(__file__)),
"testapp.db")
library_db = DbManager(
- db=os.path.join(os.path.dirname(os.path.realpath(__file__)),
- "test.db"))
+ os.path.join(os.path.dirname(os.path.realpath(__file__)),
+ "test.db"))
def fin():
library_db.close()
diff --git a/tests/mach2_test.py b/tests/mach2_test.py
index c45ff2a..c33fd92 100644
--- a/tests/mach2_test.py
+++ b/tests/mach2_test.py
@@ -2,6 +2,7 @@ import json
import unittest
import pytest
+import six
from mach2 import create_app
@@ -25,17 +26,17 @@ class Mach2TestCase(unittest.TestCase):
def test_login(self):
rv = self.login("admin", "testpass")
- assert bytes("Log out", "utf-8") in rv.data
+ assert six.b("Log out") in rv.data
self.logout()
rv = self.login("wrong", "definitelywrong")
- assert bytes("Log out", "utf-8") not in rv.data
+ assert six.b("Log out") not in rv.data
self.logout()
def test_album(self):
self.login("admin", "testpass")
rv = self.app.get("/albums/1")
- assert bytes("Album 1", "utf-8") in rv.data
+ assert six.b("Album 1") in rv.data
self.logout()
@@ -43,8 +44,8 @@ class Mach2TestCase(unittest.TestCase):
self.login("admin", "testpass")
rv = self.app.get("/artists")
- assert bytes("Artist 1", "utf-8") in rv.data
- assert bytes("Artist 2", "utf-8") in rv.data
+ assert six.b("Artist 1") in rv.data
+ assert six.b("Artist 2") in rv.data
artists = json.loads(rv.data.decode("utf-8"))
assert artists
diff --git a/tests/models/album_test.py b/tests/models/album_test.py
index 680a5cb..5153626 100644
--- a/tests/models/album_test.py
+++ b/tests/models/album_test.py
@@ -2,20 +2,20 @@ from models.album import Album
def test_instance(database):
- album = Album(id=1, db=database)
+ album = Album(database, 1)
assert album.id == 1
assert album.name == "Album 1"
assert album.date == "1999-02-04"
def test_artists(database):
- album = Album(id=1, db=database)
+ album = Album(database, 1)
assert len(album.artists) == 1
assert album.artists[0].name == "Artist 2"
def test_tracks(database):
- album = Album(id=1, db=database)
+ album = Album(database, 1)
assert len(album.tracks) == 2
assert album.tracks[0].name == "Album track 1"
assert album.tracks[0].tracknumber == 1
@@ -36,28 +36,28 @@ def test_delete(database):
album_id = cursor.lastrowid
cursor.close()
- album = Album(album_id, db=database)
+ album = Album(database, album_id)
assert album.delete()
- test_album = Album(album_id, db=database)
+ test_album = Album(database, album_id)
assert not hasattr(test_album, "name")
def test_search(database):
search_payload = {"name": {"data": "Album 1", "operator": "="}}
- album_results = Album.search(db=database, **search_payload)
+ album_results = Album.search(database, **search_payload)
assert len(album_results) > 0
invalid_search_payload = {"name": {"data": "This album does not exist",
"operator": "="}}
- no_album_results = Album.search(db=database, **invalid_search_payload)
+ no_album_results = Album.search(database, **invalid_search_payload)
assert len(no_album_results) == 0
def test_all(database):
- album_results = Album.all(db=database)
+ album_results = Album.all(database)
assert len(album_results) > 0
diff --git a/tests/models/artist_test.py b/tests/models/artist_test.py
index bb66813..b3504a1 100644
--- a/tests/models/artist_test.py
+++ b/tests/models/artist_test.py
@@ -2,27 +2,27 @@ from models.artist import Artist
def test_instance(database):
- album = Artist(id=1, db=database)
+ album = Artist(database, 1)
assert album.id == 1
assert album.name == "Artist 1"
def test_albums(database):
- artist1 = Artist(id=1, db=database)
+ artist1 = Artist(database, 1)
assert len(artist1.albums) == 0
- artist2 = Artist(id=2, db=database)
+ artist2 = Artist(database, 2)
assert len(artist2.albums) == 1
assert artist2.albums[0].name == "Album 1"
assert artist2.albums[0].date == "1999-02-04"
def test_tracks(database):
- artist1 = Artist(id=1, db=database)
+ artist1 = Artist(database, 1)
assert len(artist1.tracks) == 1
assert artist1.tracks[0].name == "Non album track"
assert artist1.tracks[0].tracknumber is None
assert artist1.tracks[0].filename == "1.mp3"
- artist2 = Artist(id=2, db=database)
+ artist2 = Artist(database, 2)
assert artist2.tracks[0].name == "Album track 1"
assert artist2.tracks[0].tracknumber == 1
assert artist2.tracks[0].filename == "album/1.mp3"
@@ -40,28 +40,28 @@ def test_delete(database):
artist_id = cursor.lastrowid
- artist = Artist(artist_id, db=database)
+ artist = Artist(database, artist_id)
assert artist.delete()
- test_artist = Artist(artist_id, db=database)
+ test_artist = Artist(database, artist_id)
assert not hasattr(test_artist, "name")
def test_search(database):
search_payload = {"name": {"data": "Artist 1", "operator": "="}}
- artist_results = Artist.search(db=database, **search_payload)
+ artist_results = Artist.search(database, **search_payload)
assert len(artist_results) > 0
invalid_search_payload = {"name": {"data": "This artist does not exist",
"operator": "="}}
- no_artist_results = Artist.search(db=database, **invalid_search_payload)
+ no_artist_results = Artist.search(database, **invalid_search_payload)
assert len(no_artist_results) == 0
def test_all(database):
- artist_results = Artist.all(db=database)
+ artist_results = Artist.all(database)
assert len(artist_results) > 0
diff --git a/tests/models/track_test.py b/tests/models/track_test.py
index 56685fd..2bb4df1 100644
--- a/tests/models/track_test.py
+++ b/tests/models/track_test.py
@@ -4,14 +4,14 @@ from models.track import Track
def test_instance(database):
- track = Track(id=1, db=database)
+ track = Track(database, 1)
assert track.id == 1
assert track.name == "Non album track"
assert track.filename == "1.mp3"
-
+ assert track.artists
def test_as_dict(database):
- track = Track(id=1, db=database)
+ track = Track(database, 1)
track_dict = track.as_dict()
@@ -22,35 +22,35 @@ def test_as_dict(database):
def test_album(database):
- track1 = Track(id=1, db=database)
+ track1 = Track(database, 1)
assert track1.album is None
- track2 = Track(id=2, db=database)
+ track2 = Track(database, 2)
assert track2.album.name == "Album 1"
assert track2.album.date == "1999-02-04"
def test_artists(database):
- track = Track(id=1, db=database)
- assert track.artists is not None
- assert len(track.artists) > 0
+ track = Track(database, 1)
+ assert track.artists
assert track.artists[0].name == "Artist 1"
def test_find_by_path(database):
- track1 = Track.find_by_path("album/2.mp3", db=database)
+ track1 = Track.find_by_path("album/2.mp3", database)
assert track1.filename == "album/2.mp3"
assert track1.name == "Album track 2"
assert track1.grouping == "swing"
+ assert track1.artists
nonexistent_track = Track.find_by_path("path/does/not/exist.mp3",
- db=database)
+ database)
assert nonexistent_track is None
def test_search(database):
- tracks = Track.search(db=database, name={"data": "Album track %",
- "operator": "LIKE"})
+ tracks = Track.search(database, name={"data": "Album track %",
+ "operator": "LIKE"})
assert tracks is not None
assert len(tracks) == 2
@@ -59,7 +59,7 @@ def test_search(database):
def test_store(database, test_file):
metadata = mutagen.File(test_file, easy=True)
- test_track = Track.store(test_file, metadata, db=database)
+ test_track = Track.store(test_file, metadata, database)
assert test_track.filename == test_file
assert test_track.name == "Silence"
@@ -76,7 +76,7 @@ def test_store(database, test_file):
def test_update(database, test_file):
metadata = {"artist": ["New artist"], "title": ["New title"]}
- test_track = Track.find_by_path(test_file, db=database)
+ test_track = Track.find_by_path(test_file, database)
test_track.update(metadata)
assert test_track.artists
@@ -86,21 +86,21 @@ def test_update(database, test_file):
def test_save(database, test_file):
- test_track = Track.find_by_path(test_file, db=database)
+ test_track = Track.find_by_path(test_file, database)
test_track.name = "Totally new name"
test_track.save()
- new_track_to_test = Track.find_by_path(test_file, db=database)
+ new_track_to_test = Track.find_by_path(test_file, database)
assert new_track_to_test.name == "Totally new name"
def test_delete(database, test_file):
- test_track = Track.find_by_path(test_file, db=database)
+ test_track = Track.find_by_path(test_file, database)
test_track.delete()
- should_not_exist = Track.find_by_path(test_file, db=database)
+ should_not_exist = Track.find_by_path(test_file, database)
assert should_not_exist is None
diff --git a/tests/test.db b/tests/test.db
index ccb7825..1602d6f 100644
--- a/tests/test.db
+++ b/tests/test.db
Binary files differ
diff --git a/tests/testapp.db b/tests/testapp.db
index 2fc7b8c..3ccfa2f 100644
--- a/tests/testapp.db
+++ b/tests/testapp.db
Binary files differ
diff --git a/tests/testnew.ogg b/tests/testnew.ogg
new file mode 100644
index 0000000..64a9f49
--- /dev/null
+++ b/tests/testnew.ogg
Binary files differ
diff --git a/tests/watcher_test.py b/tests/watcher_test.py
new file mode 100644
index 0000000..5f92d36
--- /dev/null
+++ b/tests/watcher_test.py
@@ -0,0 +1,96 @@
+"""Tests for the watcher module."""
+from os import remove
+from os.path import dirname, join, realpath
+from shutil import copy, move, rmtree
+from tempfile import mkdtemp
+import unittest
+
+import mutagen
+import six
+
+from db.db_manager import DbManager
+from models.track import Track
+from watcher import LibraryWatcher
+
+
+if six.PY2:
+ def _u(string):
+ return unicode(string, encoding="utf_8")
+else:
+ def _u(string):
+ return string
+
+
+class WatcherTestCase(unittest.TestCase):
+ """Defines tests for the watcher module.
+
+ Extends:
+ unittest.Testcase
+
+ """
+
+ @classmethod
+ def setUpClass(cls):
+ """Set up fixtures for the tests."""
+ cls._tempdir = mkdtemp()
+ cls._tempdbdir = mkdtemp()
+ cls._db_path = join(cls._tempdbdir, "test.db")
+ copy(join(dirname(realpath(__file__)), "test.db"), cls._db_path)
+ cls._db = DbManager(cls._db_path)
+ cls._watcher = LibraryWatcher(cls._tempdir, cls._db_path)
+
+ @classmethod
+ def tearDownClass(cls):
+ """Remove test fixtures."""
+ cls._watcher.stop()
+ rmtree(cls._tempdbdir)
+ rmtree(cls._tempdir)
+
+ def test_watcher_actions(self):
+ """Test creating, moving, modifying and deleting a file."""
+ new_file = join(dirname(realpath(__file__)), "testnew.ogg")
+ copy(new_file, self._tempdir)
+
+ self._watcher.check_for_events()
+
+ found_track = Track.find_by_path(join(self._tempdir, "testnew.ogg"),
+ self._db)
+ assert found_track
+ assert "testnew.ogg" in found_track.filename
+ assert found_track.artists
+ found_artist = False
+ for artist in found_track.artists:
+ if _u("Art Ist") == artist.name:
+ found_artist = True
+ break
+
+ assert found_artist
+
+ original_file = join(self._tempdir, "testnew.ogg")
+ moved_file = join(self._tempdir, "testmoved.ogg")
+ move(original_file, moved_file)
+
+ self._watcher.check_for_events()
+
+ moved_track = Track.find_by_path(moved_file, self._db)
+
+ assert moved_track
+ assert "testmoved.ogg" in moved_track.filename
+
+ original_metadata = mutagen.File(moved_file, easy=True)
+
+ original_metadata["title"] = [_u("New title")]
+ original_metadata.save()
+
+ self._watcher.check_for_events()
+
+ modified_track = Track.find_by_path(moved_file, self._db)
+
+ assert modified_track
+ assert modified_track.name == "New title"
+
+ remove(moved_file)
+
+ self._watcher.check_for_events()
+
+ assert Track.find_by_path(moved_file, self._db) is None
diff --git a/watcher.py b/watcher.py
index 9af4a6b..353636a 100644
--- a/watcher.py
+++ b/watcher.py
@@ -1,41 +1,104 @@
-import atexit
-import configparser
+"""
+Defines a watcher service to update the media library.
+
+This module implements a monitoring service to update the library when files
+are added or removed from the media library directory.
+"""
+from atexit import register
+import logging
import os
import pyinotify
-import library
+from db.db_manager import DbManager
+from library import MediaLibrary
+
+
+_LOGGER = logging.getLogger(__name__)
class EventHandler(pyinotify.ProcessEvent):
+ """Event handler defining actions for adding/moving/removing files.
+
+ Extends:
+ pyinotify.ProcessEvent
+
+ """
+
+ def __init__(self, media_library, pevent=None, **kwargs):
+ """Create the event handler.
+
+ Args:
+ media_library (MediaLibrary): The media library.
+
+ """
+ self.__library = media_library
+
+ super(self.__class__, self).__init__(pevent=None, **kwargs)
+
def process_IN_CREATE(self, event):
- print("Creating:", event.pathname)
- library.run(event.pathname)
+ """Add a file to the library when it is created.
+
+ Args:
+ event (pynotify.Event) - the event raised by inotify.
+
+ """
+ _LOGGER.debug("Creating: %s", event.pathname)
+ self.__library.run(event.pathname)
def process_IN_DELETE(self, event):
- print("Removing:", event.pathname)
+ """Remove a file from the library when it is deleted.
+
+ Args:
+ event (pynotify.Event) - the event raised by inotify.
+
+ """
+ _LOGGER.debug("Removing: %s", event.pathname)
if not os.path.isdir(event.pathname):
- library.delete_file(event.pathname)
+ self.__library.delete_file(event.pathname)
def process_IN_MOVED_TO(self, event):
- print("Moved to:", event.pathname)
- library.update_track_filename(event.src_pathname, event.pathname)
+ """Update a file's information in the library when it is moved.
+
+ Args:
+ event (pynotify.Event) - the event raised by inotify.
+
+ """
+ _LOGGER.debug("Moved to: %s", event.pathname)
+ self.__library.update_track_filename(event.src_pathname,
+ event.pathname)
# moving the file may also hint that the metadata has changed
- library.update_file(event.pathname)
+ self.__library.update_file(event.pathname)
def process_IN_MODIFY(self, event):
- print("Modified:", event.pathname)
+ """Update a file's information in the library when it is modified.
+
+ Args:
+ event (pynotify.Event) - the event raised by inotify.
+
+ """
+ _LOGGER.debug("Modified: %s", event.pathname)
if not os.path.isdir(event.pathname):
- library.update_file(event.pathname)
+ self.__library.update_file(event.pathname)
-class LibraryWatcher:
+class LibraryWatcher(object):
+ """Watches the library."""
- def __init__(self, path):
- print("Setting up library watcher")
+ def __init__(self, path, database_path):
+ """Create the LibraryWatcher.
+
+ Args:
+ path (str): the patch of the directory to watch.
+
+ """
+ _LOGGER.info("Setting up library watcher")
+ database = DbManager(database_path)
+ library = MediaLibrary(path, database)
+ _LOGGER.info("Using %s", database)
if not hasattr(self, "path"):
setattr(self, "path", path)
@@ -50,25 +113,38 @@ class LibraryWatcher:
if not hasattr(self, "notifier"):
setattr(self,
"notifier",
- pyinotify.ThreadedNotifier(self.wm, EventHandler()))
-
- self.notifier.coalesce_events()
- self.notifier.start()
+ pyinotify.Notifier(self.wm, EventHandler(library),
+ timeout=10))
if not hasattr(self, "wdd"):
setattr(self, "wdd", self.wm.add_watch(path, mask, rec=True,
auto_add=True))
- print("Set up watch on ", path)
+ self.notifier.coalesce_events()
+
+ _LOGGER.info("Set up watch on %s", path)
- atexit.register(self.stop)
+ register(self.stop)
def stop(self):
+ """Remove all the watched paths."""
if self.wdd[self.path] > 0:
self.wm.rm_watch(self.wdd[self.path], rec=True)
+ def check_for_events(self):
+ """Check for any notification events."""
+ assert self.notifier._timeout is not None
+ self.notifier.process_events()
+ while self.notifier.check_events():
+ self.notifier.read_events()
+ self.notifier.process_events()
+
+
if __name__ == "__main__":
+ from six.moves import configparser
+
config = configparser.ConfigParser()
config.read("mach2.ini")
- watch = LibraryWatcher(config["DEFAULT"]["media_dir"])
+ watch = LibraryWatcher(config.get("DEFAULT", "media_dir"),
+ config.get("DEFAULT", "library"))