summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichaël Ball <michael.ball@gmail.com>2014-12-28 12:24:22 +0000
committerMichaël Ball <michael.ball@gmail.com>2015-11-27 20:02:04 +0000
commit75beec91a8526fbbc0a90134140b9dff6af15c0c (patch)
tree02414e46da3e08000384c40c27b7aab9748de0fe
parenta2964845e3c03b9cf5f01583f53f7553c7d67caf (diff)
Initial frontend work
-rw-r--r--.gitignore7
-rw-r--r--Gruntfile.js28
-rw-r--r--app.dbbin0 -> 3072 bytes
-rw-r--r--bower.json39
-rw-r--r--common/security.py15
-rw-r--r--common/utils.py14
-rw-r--r--db/db_manager.py2
-rw-r--r--mach2.py297
-rw-r--r--models/album.py13
-rw-r--r--models/track.py27
-rw-r--r--models/user.py52
-rw-r--r--package.json16
-rw-r--r--static/partials/albums/list.html28
-rw-r--r--static/partials/artists/detail.html12
-rw-r--r--static/partials/artists/list.html28
-rw-r--r--static/partials/artists/tracks.html7
-rw-r--r--static/scripts/app/app.js63
-rw-r--r--static/scripts/app/controllers.js148
-rw-r--r--static/scripts/app/filters.js48
-rw-r--r--static/scripts/app/services.js97
-rw-r--r--templates/index.html64
-rw-r--r--templates/login.html39
22 files changed, 1005 insertions, 39 deletions
diff --git a/.gitignore b/.gitignore
index a76c615..def8954 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,4 +12,9 @@ library.db-journal
public
tmp
library.db
-cscope.* \ No newline at end of file
+bower_components
+node_modules
+library.db
+cscope.*
+static/scripts/libs/
+.jshintrc \ No newline at end of file
diff --git a/Gruntfile.js b/Gruntfile.js
new file mode 100644
index 0000000..a220434
--- /dev/null
+++ b/Gruntfile.js
@@ -0,0 +1,28 @@
+module.exports = function(grunt) {
+ var path = require("path");
+
+ grunt.initConfig({
+ pkg: grunt.file.readJSON("package.json"),
+ bower: {
+ install: {
+ options: {
+ targetDir: "static/scripts/libs",
+ install: true,
+ cleanup: true,
+ layout: "byComponent"
+ }
+ }
+ },
+ run: {
+ mach2: {
+ cmd: "python",
+ args: ["mach2.py"]
+ }
+ }
+ });
+
+ grunt.loadNpmTasks("grunt-bower-task");
+ grunt.loadNpmTasks("grunt-run");
+
+ grunt.task.registerTask("default", ["bower:install", "run:mach2"]);
+};
diff --git a/app.db b/app.db
new file mode 100644
index 0000000..8078901
--- /dev/null
+++ b/app.db
Binary files differ
diff --git a/bower.json b/bower.json
new file mode 100644
index 0000000..024e6f3
--- /dev/null
+++ b/bower.json
@@ -0,0 +1,39 @@
+{
+ "name": "mach2",
+ "version": "0.0.1",
+ "homepage": "https://github.com/michael-ball/mach2",
+ "authors": [
+ "Michaël Ball"
+ ],
+ "description": "Lightweight media server",
+ "main": "mach2.py",
+ "license": "MIT",
+ "private": true,
+ "ignore": [
+ "**/.*",
+ "node_modules",
+ "bower_components",
+ "test",
+ "tests"
+ ],
+ "dependencies": {
+ "angular": "1.4.8",
+ "bootstrap": "3.3.5",
+ "moment": "~2.10.6",
+ "angular-moment": "~0.10.3",
+ "angular-ui-router": "~0.2.15",
+ "angular-bootstrap": "~0.14.3",
+ "angular-resource": "~1.4.8",
+ "moment-timezone": "~0.4.1"
+ },
+ "resolutions": {
+ "angular": "1.3.10"
+ },
+ "exportsOverride": {
+ "bootstrap": {
+ "js": "**/bootstrap*.js",
+ "css": "**/*.css*",
+ "fonts": "**/fonts"
+ }
+ }
+}
diff --git a/common/security.py b/common/security.py
new file mode 100644
index 0000000..af1e8b9
--- /dev/null
+++ b/common/security.py
@@ -0,0 +1,15 @@
+from passlib.context import CryptContext
+
+
+pwd_context = CryptContext(
+ schemes=["pbkdf2_sha256", "des_crypt"],
+ default="pbkdf2_sha256",
+
+ # vary rounds parameter randomly when creating new hashes...
+ all__vary_rounds=0.1,
+
+ # set the number of rounds that should be used...
+ # (appropriate values may vary for different schemes,
+ # and the amount of time you wish it to take)
+ pbkdf2_sha256__default_rounds=8000,
+ )
diff --git a/common/utils.py b/common/utils.py
index 288673e..38ad5ed 100644
--- a/common/utils.py
+++ b/common/utils.py
@@ -12,8 +12,18 @@ def make_where_clause(params, join_operator="AND"):
try:
for (column, operator) in params.items():
- condition_subphrase = " ".join(("%s", operator, ":%s"))
- where_items.append(condition_subphrase % (column, column))
+ condition_subphrase = ""
+
+ if operator == "BETWEEN":
+ condition_subphrase = " ".join(("%s", operator,
+ ":%s1 AND :%s2"))
+
+ where_items.append(condition_subphrase % (column, column,
+ column))
+ else:
+ condition_subphrase = " ".join(("%s", operator, ":%s"))
+
+ where_items.append(condition_subphrase % (column, column))
where_statement = None
if len(where_items) > 1:
diff --git a/db/db_manager.py b/db/db_manager.py
index 82168d0..c55ac39 100644
--- a/db/db_manager.py
+++ b/db/db_manager.py
@@ -172,5 +172,5 @@ class DbManager:
def __new__(self):
if not DbManager.instance:
DbManager.instance = DbManager.__DbManager()
-
+
return DbManager.instance
diff --git a/mach2.py b/mach2.py
index ffa1c1e..7d4ff68 100644
--- a/mach2.py
+++ b/mach2.py
@@ -13,10 +13,10 @@ 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 models.album import Album
from models.artist import Artist
from models.track import Track
+from models.user import User
DATABASE = "app.db"
@@ -24,6 +24,8 @@ DATABASE = "app.db"
compress = Compress()
app = Flask(__name__)
+app.secret_key = """\xfc[\x16\x9d\x0f\x86;;\x9e_\x96\x01\xb7\xeay^\x8b\xa0E\x84
+ \x91;\x18\xc2"""
app.config.from_object(__name__)
config = configparser.ConfigParser()
@@ -60,42 +62,173 @@ def query_db(query, args=(), one=False):
return (rv[0] if rv else None) if one else rv
+config = configparser.ConfigParser()
+config.read("mach2.ini")
+
+login_manager = LoginManager()
+login_manager.login_view = "login"
+login_manager.session_protection = "strong"
+
+
+def get_db():
+ db = getattr(g, "_database", None)
+ if db is None:
+ db = sqlite3.connect(DATABASE)
+ db.row_factory = sqlite3.Row
+ setattr(g, "_database", db)
+
+ return db
+
+
+@app.teardown_appcontext
+def close_connection(exception):
+ db = getattr(g, "_database", None)
+ if db is not None:
+ db.close()
+
+
+def query_db(query, args=(), one=False):
+ cur = get_db().execute(query, args)
+ rv = cur.fetchall()
+ cur.close()
+ return (rv[0] if rv else None) if one else rv
+
+
@app.route("/")
-def hello():
- return "Hello world!"
+@login_required
+def index():
+ return render_template("index.html", user=current_user)
-@app.route("/search/album/<album_name>")
-def album_search(album_name):
+@app.route("/albums")
+@login_required
+def albums():
+ returned_albums = []
albums = []
- for album in Album.search(name={'data': album_name, 'operator': 'LIKE'}):
+
+ order_by = request.args.get("order", None)
+ order_direction = request.args.get("direction", None)
+ lim = request.args.get("limit", None)
+ off = request.args.get("offset", None)
+ conditions = request.args.getlist("conditions")
+
+ search_params = {}
+
+ if conditions:
+ field = conditions[0]
+ operator = conditions[1]
+ value = conditions[2]
+
+ search_params[field] = {"data": value, "operator": operator}
+
+ params = {}
+
+ if order_by:
+ params["order"] = order_by
+
+ if order_direction:
+ params["direction"] = order_direction
+
+ if lim:
+ params["limit"] = lim
+
+ if off:
+ params["offset"] = off
+
+ all_params = params.copy()
+ all_params.update(search_params)
+
+ if search_params:
+ returned_albums = Album.search(**all_params)
+ else:
+ returned_albums = Album.all(**params)
+
+ for album in returned_albums:
albums.append(album.__dict__)
return json.dumps(albums)
-@app.route("/search/artist/<artist_name>")
-def artist_search(artist_name):
+@app.route("/albums/<int:album_id>/tracks")
+@login_required
+def album_tracks(album_id):
+ tracks = []
+ album = Album(id=album_id)
+
+ for track in album.tracks:
+ tracks.append(track.__dict__)
+
+ return json.dumps(tracks)
+
+
+@app.route("/albums/<int:album_id>/artists")
+@login_required
+def album_artists(album_id):
artists = []
- for artist in Artist.search(name={
- 'data': artist_name,
- 'operator': 'LIKE'
- }):
+ album = Album(id=album_id)
+
+ for artist in album.artists:
artists.append(artist.__dict__)
return json.dumps(artists)
-@app.route("/search/track/<track_name>")
-def track_search(track_name):
- tracks = []
- for track in Track.search(name={'data': track_name, 'operator': 'LIKE'}):
- tracks.append(track.__dict__)
+@app.route("/albums/<int:album_id>")
+@login_required
+def album(album_id):
+ album = Album(id=album_id)
- return json.dumps(tracks)
+ return json.dumps(album.__dict__)
+
+
+@app.route("/albums/<album_name>")
+@login_required
+def album_search(album_name):
+ albums = []
+
+ for album in Album.search(name={"data": album_name, "operator": "LIKE"}):
+ albums.append(album.__dict__)
+
+ return json.dumps(albums)
-@app.route("/artist/<int:artist_id>/tracks")
+@app.route("/artists")
+@login_required
+def artists():
+ order_by = None
+ order_direction = None
+ lim = None
+ off = None
+ returned_artists = []
+ artists = []
+
+ if request.args.get("order"):
+ order_by = request.args.get("order")
+
+ if request.args.get("direction"):
+ order_direction = request.args.get("direction")
+
+ if request.args.get("limit"):
+ lim = request.args.get("limit")
+
+ if request.args.get("offset"):
+ off = request.args.get("offset")
+
+ if order_by:
+ returned_artists = Artist.all(order=order_by,
+ direction=order_direction,
+ limit=lim, offset=off)
+ else:
+ returned_artists = Artist.all(limit=lim, offset=off)
+
+ for artist in returned_artists:
+ artists.append(artist.__dict__)
+
+ return json.dumps(artists)
+
+
+@app.route("/artists/<int:artist_id>/tracks")
+@login_required
def artist_tracks(artist_id):
tracks = []
artist = Artist(id=artist_id)
@@ -106,7 +239,8 @@ def artist_tracks(artist_id):
return json.dumps(tracks)
-@app.route("/artist/<int:artist_id>/albums")
+@app.route("/artists/<int:artist_id>/albums")
+@login_required
def artist_albums(artist_id):
albums = []
artist = Artist(id=artist_id)
@@ -117,29 +251,75 @@ def artist_albums(artist_id):
return json.dumps(albums)
-@app.route("/album/<int:album_id>/tracks")
-def album_tracks(album_id):
+@app.route("/artists/<int:artist_id>")
+@login_required
+def artist_info(artist_id):
+ artist = Artist(id=artist_id)
+
+ return json.dumps(artist.__dict__)
+
+
+@app.route("/artists/<artist_name>")
+@login_required
+def artist_search(artist_name):
+ artists = []
+ for artist in Artist.search(name={
+ "data": artist_name,
+ "operator": "LIKE"
+ }):
+ artists.append(artist.__dict__)
+
+ return json.dumps(artists)
+
+
+@app.route("/tracks")
+@login_required
+def tracks():
+ order_by = None
+ order_direction = None
+ lim = None
+ off = None
+ returned_tracks = []
tracks = []
- album = Album(id=album_id)
- for track in album.tracks:
+ if request.args.get("order"):
+ order_by = request.args.get("order")
+
+ if request.args.get("direction"):
+ order_direction = request.args.get("direction")
+
+ if request.args.get("limit"):
+ lim = request.args.get("limit")
+
+ if request.args.get("offset"):
+ off = request.args.get("offset")
+
+ if order_by:
+ returned_tracks = Track.all(order=order_by, direction=order_direction,
+ limit=lim, offset=off)
+ else:
+ returned_tracks = Track.all(limit=lim, offset=off)
+
+ for track in returned_tracks:
tracks.append(track.__dict__)
return json.dumps(tracks)
-@app.route("/album/<int:album_id>/artists")
-def album_artists(album_id):
+@app.route("/tracks/<int:track_id>/artists")
+@login_required
+def track_artists(track_id):
artists = []
- album = Album(id=album_id)
+ track = Track(id=track_id)
- for artist in album.artists:
+ for artist in track.artists:
artists.append(artist.__dict__)
return json.dumps(artists)
-@app.route("/track/<int:track_id>")
+@app.route("/tracks/<int:track_id>")
+@login_required
def track(track_id):
def stream_file(filename, chunksize=8192):
with open(filename, "rb") as f:
@@ -171,10 +351,63 @@ def track(track_id):
return resp
-if __name__ == "__main__":
- config = configparser.ConfigParser()
- config.read("mach2.ini")
+@app.route("/tracks/<track_name>")
+@login_required
+def track_search(track_name):
+ tracks = []
+ for track in Track.search(name={"data": track_name, "operator": "LIKE"}):
+ tracks.append(track.__dict__)
+
+ return json.dumps(tracks)
+
+
+@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)
+
+ return user
+
+
+@app.route("/login", methods=["GET", "POST"])
+def login():
+ if request.method == "POST":
+ user = None
+ result = query_db("SELECT * FROM user WHERE username = ?",
+ [request.form["username"]], one=True)
+
+ if result:
+ user = User(id=result[0],
+ username=result[1],
+ password_hash=result[2],
+ authenticated=0,
+ active=result[4],
+ anonymous=result[5])
+
+ password = request.form["password"]
+
+ if user and user.verify(password):
+ login_user(user)
+ return redirect(request.args.get("next") or url_for("index"))
+ else:
+ user = None
+
+ return render_template("login.html")
+
+
+@app.route("/logout")
+@login_required
+def logout():
+ logout_user()
+ return redirect("/")
+
+
+if __name__ == "__main__":
login_manager.init_app(app)
compress.init_app(app)
diff --git a/models/album.py b/models/album.py
index 9ca3798..0d7cd54 100644
--- a/models/album.py
+++ b/models/album.py
@@ -99,7 +99,8 @@ class Album():
sql = " ".join(("UPDATE album"), set_clause, "WHERE id = :id")
cursor.execute(sql, dirty_attributes)
- def search(**search_params):
+ def search(order="album.id", direction="ASC", limit=None,
+ offset=None, **search_params):
"""Find an album with the given params
Args:
@@ -117,7 +118,14 @@ class Album():
value_params = {}
for (attr, value) in search_params.items():
where_params[attr] = value["operator"]
- value_params[attr] = value["data"]
+
+ if value["operator"] == "BETWEEN":
+ items = value["data"].split(" ")
+
+ value_params["".join((attr, "1"))] = items[0]
+ value_params["".join((attr, "2"))] = items[2]
+ else:
+ value_params[attr] = value["data"]
where_clause = utils.make_where_clause(where_params)
@@ -138,6 +146,7 @@ class Album():
def all(order="album.id", direction="ASC", limit=None, offset=None):
db = DbManager()
cursor = db.cursor()
+
albums = []
select_string = """SELECT * FROM album LEFT JOIN album_artist ON
diff --git a/models/track.py b/models/track.py
index dead1f8..688f6ff 100644
--- a/models/track.py
+++ b/models/track.py
@@ -361,7 +361,7 @@ class Track:
for artist_name in artist_names:
musicbrainz_artistid = None
- artistsort = None
+ artistsort = artist_name
try:
musicbrainz_artistid = musicbrainz_artist_ids[i]
except IndexError:
@@ -543,3 +543,28 @@ class Track:
db.commit()
return True
+
+ def all(order="track.id", direction="ASC", limit=None, offset=None):
+ db = DbManager()
+ tracks = []
+
+ select_string = """SELECT * FROM track LEFT JOIN artist_track ON
+ artist_track.track_id = track.id LEFT JOIN artist ON
+ artist_track.artist_id = artist.id LEFT JOIN album_track ON
+ album_track.track_id = track.id LEFT JOIN album ON
+ album_track.album_id = album.id ORDER BY %s %s""" % (order,
+ direction)
+
+ if limit is not None and offset is not None:
+ select_string = " ".join((select_string,
+ "LIMIT %s OFFSET %s" % (limit, offset)))
+
+ result = db.execute(select_string)
+
+ for row in result:
+ tracks.append(
+ Track(id=row[0], tracknumber=row[1], name=row[3],
+ grouping=row[3], filename=row[4])
+ )
+
+ return tracks
diff --git a/models/user.py b/models/user.py
new file mode 100644
index 0000000..f912e43
--- /dev/null
+++ b/models/user.py
@@ -0,0 +1,52 @@
+from common.security import pwd_context
+
+
+class User:
+ def __init__(self, **kwargs):
+ for (key, value) in kwargs.items():
+ setattr(self, key, value)
+
+ def get_id(self):
+ if self.id:
+ return str(self.id)
+ else:
+ raise ValueError("No user")
+
+ def is_authenticated(self):
+ if self.authenticated > 0:
+ return True
+ else:
+ return False
+
+ def is_active(self):
+ if self.active > 0:
+ return True
+ else:
+ return False
+
+ def is_anonymous(self):
+ if self.anonymous > 0:
+ return True
+ else:
+ return False
+
+ def verify(self, password):
+ if self.id and pwd_context.verify(password, self.password_hash):
+ self.authenticated = 1
+ return True
+ else:
+ return False
+
+ def new_password(self, password, category=None):
+ if self.id:
+ hash = None
+
+ if category:
+ hash = pwd_context.encrypt(password, category=category)
+ else:
+ hash = pwd_context.encrypt(password)
+
+ return hash
+
+ else:
+ raise ValueError("No user")
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..aa0ff4d
--- /dev/null
+++ b/package.json
@@ -0,0 +1,16 @@
+{
+ "name": "mach2",
+ "version": "0.0.1",
+ "description": "Lightweight media server",
+ "homepage": "https://github.com/michael-ball/mach2",
+ "license": "MIT",
+ "author": {
+ "name": "Michaël Ball",
+ "url": "https://www.github.com/michael-ball"
+ },
+ "devDependencies": {
+ "grunt": "~0.4.5",
+ "grunt-bower-task": "^0.4.0",
+ "grunt-run": "^0.3.0"
+ }
+}
diff --git a/static/partials/albums/list.html b/static/partials/albums/list.html
new file mode 100644
index 0000000..01ac2a8
--- /dev/null
+++ b/static/partials/albums/list.html
@@ -0,0 +1,28 @@
+<div class="row">
+ <div class="col-sm-offset-1 col-sm-11 col-xs-12">
+ <h2>Albums</h2>
+ </div>
+</div>
+<div class="row">
+ <div class="col-sm-offset-1 col-sm-6 col-xs-12">
+ <input type="text" class="form-control" data-ng-model="search" placeholder="Search for...">
+ </div>
+</div>
+<div class="row" data-ng-hide="search">
+ <div class="col-sm-1 hidden-xs text-center">
+ <div class="btn-group-vertical" role="group">
+ <label data-ng-repeat="i in indices" data-ng-model="$parent.selectedIndex" class="btn btn-default btn-sm" uib-btn-radio="'{{ i }}'" uncheckable>{{ i }}</label>
+ </div>
+ </div>
+ <div class="visible-xs-block col-xs-12">
+ <select data-ng-model="selectedIndex">
+ <option data-ng-repeat="i in indices" value="{{ i }}">{{ i }}</option>
+ </select>
+ </div>
+ <div class="col-sm-11 col-xs-12">
+ <p data-ng-repeat="album in albums | dateFilter:selectedIndex"><a data-ui-sref="albums.detail({ albumId: album.id })">{{ album.name }}<span ng-show="album.date"> ({{ album.date | amDateFormat: 'YYYY':'':'':'YYYY-MM-DD' }})</span></a></p>
+ </div>
+</div>
+<div class="row" data-ng-show="search">
+ <p data-ng-repeat="album in albums | filter:search"><a data-ui-sref="albums.detail({ albumId: album.id })">{{ album.name }}<span ng-show="album.date"> ({{ album.date | amDateFormat: 'YYYY':'':'':'YYYY-MM-DD' }})</span></a></p>
+</div>
diff --git a/static/partials/artists/detail.html b/static/partials/artists/detail.html
new file mode 100644
index 0000000..991633a
--- /dev/null
+++ b/static/partials/artists/detail.html
@@ -0,0 +1,12 @@
+<div class="col-xs-12">
+ <h3>{{ artist.name }}</h3>
+
+ <h4>Albums</h4>
+ <ul>
+ <li data-ng-repeat="album in albums">{{album.name}} ({{ album.date | amDateFormat: 'YYYY':'':'':'YYYY-MM-DD' }})</li>
+ </ul>
+
+ <p><a data-ui-sref="artistdetail.tracks({ artistId: artist.id })">View artist tracks</a></p>
+</div>
+<div class="row" data-ui-view>
+</div> \ No newline at end of file
diff --git a/static/partials/artists/list.html b/static/partials/artists/list.html
new file mode 100644
index 0000000..7c48e57
--- /dev/null
+++ b/static/partials/artists/list.html
@@ -0,0 +1,28 @@
+<div class="row">
+ <div class="col-sm-offset-1 col-sm-11 col-xs-12">
+ <h2>Artists</h2>
+ </div>
+</div>
+<div class="row">
+ <div class="col-sm-offset-1 col-sm-6 col-xs-12">
+ <input type="text" class="form-control" data-ng-model="search" placeholder="Search for...">
+ </div>
+</div>
+<div class="row" data-ng-hide="search">
+ <div class="col-sm-1 hidden-xs text-center">
+ <div class="btn-group-vertical" role="group">
+ <label data-ng-repeat="i in indices" data-ng-model="$parent.selectedIndex" class="btn btn-default btn-sm" uib-btn-radio="'{{ i }}'" uncheckable>{{ i }}</label>
+ </div>
+ </div>
+ <div class="visible-xs-block col-xs-12">
+ <select data-ng-model="selectedIndex">
+ <option data-ng-repeat="i in indices" value="{{ i }}">{{ i }}</option>
+ </select>
+ </div>
+ <div class="col-sm-11 col-xs-12">
+ <p data-ng-repeat="artist in totalArtists | alphabetFilter:{attrs: ['sortname', 'name'], param: selectedIndex }"><a data-ui-sref="artistdetail({ artistId: artist.id })">{{ artist.name }}</a></p>
+ </div>
+</div>
+<div class="row" data-ng-show="search">
+ <p data-ng-repeat="artist in totalArtists | filter:search"><a data-ui-sref="artistdetail({ artistId: artist.id })">{{ artist.name }}</a></p>
+</div> \ No newline at end of file
diff --git a/static/partials/artists/tracks.html b/static/partials/artists/tracks.html
new file mode 100644
index 0000000..8d05b1d
--- /dev/null
+++ b/static/partials/artists/tracks.html
@@ -0,0 +1,7 @@
+<div class="col-xs-12">
+ <h3>{{ artist.name }}'s Tracks</h3>
+
+ <ul>
+ <li data-ng-repeat="track in tracks"><a href="/tracks/{{track.id}}">{{ track.name }}</a></li>
+ </ul>
+</div> \ No newline at end of file
diff --git a/static/scripts/app/app.js b/static/scripts/app/app.js
new file mode 100644
index 0000000..b0d3738
--- /dev/null
+++ b/static/scripts/app/app.js
@@ -0,0 +1,63 @@
+var mach2App = angular.module(
+ 'mach2App',
+ [
+ 'ui.router',
+ 'mach2Services',
+ 'mach2Controllers',
+ 'mach2Filters',
+ 'ui.bootstrap',
+ 'angularMoment'
+ ]
+);
+
+mach2App.config(
+ [
+ '$stateProvider',
+ '$urlRouterProvider',
+ function($stateProvider, $urlRouterProvider) {
+ $stateProvider.state('artists', {
+ url: '/artists',
+ templateUrl: 'static/partials/artists/list.html',
+ controller: 'ArtistCtrl',
+ });
+
+ $stateProvider.state('artistdetail', {
+ url: '/artists/{artistId:int}',
+ templateUrl: 'static/partials/artists/detail.html',
+ controller: 'ArtistDetailCtrl',
+ resolve: {
+ artistId: ['$stateParams', function($stateParams) {
+ return $stateParams.artistId;
+ }]
+ },
+ });
+
+ $stateProvider.state('artistdetail.tracks', {
+ url: '/tracks',
+ templateUrl: 'static/partials/artists/tracks.html',
+ controller: 'ArtistTracksCtrl'
+ });
+
+ $stateProvider.state('albums', {
+ url: '/albums',
+ templateUrl: 'static/partials/albums/list.html',
+ controller: 'AlbumCtrl'
+ });
+
+ $stateProvider.state('albums.detail', {
+ url: '/{albumId:int}',
+ templateUrl: 'static/partials/album/detail.html',
+ controller: 'AlbumDetailCtrl'
+ });
+
+ $urlRouterProvider.otherwise('/artists');
+ }
+ ]
+);
+
+mach2App.constant(
+ 'angularMomentConfig',
+ {
+ timezone: 'utc'
+ }
+); \ No newline at end of file
diff --git a/static/scripts/app/controllers.js b/static/scripts/app/controllers.js
new file mode 100644
index 0000000..35340e6
--- /dev/null
+++ b/static/scripts/app/controllers.js
@@ -0,0 +1,148 @@
+var mach2Controllers = angular.module(
+ 'mach2Controllers',
+ [
+ 'ui.bootstrap',
+ 'angularMoment'
+ ]
+);
+
+mach2Controllers.controller('NavCtrl', ['$scope', function($scope) {
+}]);
+
+mach2Controllers.controller(
+ 'ArtistCtrl',
+ [
+ '$scope',
+ 'ArtistSearch',
+ function($scope, ArtistSearch) {
+ $scope.totalArtists = ArtistSearch.query();
+ $scope.indices = [
+ 'A',
+ 'B',
+ 'C',
+ 'D',
+ 'E',
+ 'F',
+ 'G',
+ 'H',
+ 'I',
+ 'J',
+ 'K',
+ 'L',
+ 'M',
+ 'N',
+ 'O',
+ 'P',
+ 'Q',
+ 'R',
+ 'S',
+ 'T',
+ 'U',
+ 'V',
+ 'W',
+ 'X',
+ 'Y',
+ 'Z',
+ '0-9',
+ 'Other'
+ ];
+ $scope.selectedIndex = $scope.indices[0];
+ }
+ ]
+);
+
+mach2Controllers.controller(
+ 'ArtistDetailCtrl',
+ [
+ '$scope',
+ '$stateParams',
+ 'Artist',
+ 'ArtistAlbums',
+ 'ArtistTracks',
+ function(
+ $scope,
+ $stateParams,
+ Artist,
+ ArtistAlbums,
+ ArtistTracks
+ ) {
+ console.log('Am I here?');
+ $scope.artist = Artist.query({
+ artistId: $stateParams.artistId
+ });
+
+ $scope.albums = ArtistAlbums.query({
+ artistId: $stateParams.artistId
+ });
+
+ $scope.tracks = ArtistTracks.query({
+ artistId: $stateParams.artistId
+ });
+ }
+ ]
+);
+
+mach2Controllers.controller(
+ 'ArtistTracksCtrl',
+ [
+ '$scope',
+ '$stateParams',
+ 'Artist',
+ 'ArtistTracks',
+ function($scope, $stateParams, Artist, ArtistTracks) {
+ $scope.artist = Artist.query({
+ artistId: $stateParams.artistId
+ });
+
+ $scope.tracks = ArtistTracks.query({
+ artistId: $stateParams.artistId
+ });
+ }
+ ]
+);
+
+mach2Controllers.controller(
+ 'AlbumCtrl',
+ [
+ '$scope',
+ 'AlbumArtists',
+ 'AlbumSearch',
+ function($scope, AlbumArtists, AlbumSearch) {
+ // horrible way of calculating decades
+ var currentYear = moment().format('YYYY');
+ var startDecade = 1940;
+
+ var decades = [];
+
+ for (i = (startDecade/10); i <= (currentYear/10); i++) {
+ decades.push(i * 10);
+ }
+
+ $scope.albums = AlbumSearch.query();
+ $scope.indices = decades;
+ $scope.selectedIndex = $scope.indices[0];
+ $scope.albumArtists = AlbumArtists;
+ }
+ ]
+);
+
+mach2Controllers.controller(
+ 'AlbumDetailCtrl',
+ [
+ '$scope',
+ '$stateParams',
+ 'Album',
+ 'AlbumTracks',
+ function(
+ $scope,
+ $stateParams,
+ Album,
+ AlbumTracks
+ ) {
+
+ }
+ ]
+);
+
+mach2Controllers.controller('TrackCtrl', ['$scope', function($scope) {
+}]);
diff --git a/static/scripts/app/filters.js b/static/scripts/app/filters.js
new file mode 100644
index 0000000..ed1f537
--- /dev/null
+++ b/static/scripts/app/filters.js
@@ -0,0 +1,48 @@
+var mach2Filters = angular.module('mach2Filters',[]);
+
+mach2Filters.filter('alphabetFilter', function() {
+ return function(items, search) {
+ if (!search) {
+ return items;
+ }
+
+ return items.filter(function(element, index, array) {
+ var searchTerm = search.param;
+ var searchAttrs = search.attrs;
+ var regexp = new RegExp(searchTerm, 'i');
+
+ var searchString = null;
+
+ for(i = 0; i < searchAttrs.length; i++) {
+ if (element[searchAttrs[i]]) {
+ searchString = element[searchAttrs[i]];
+ break;
+ }
+ }
+
+ if (searchTerm === '0-9') {
+ regexp = /[0-9]/;
+ } else if (searchTerm === 'Other') {
+ regexp = /\W/;
+ }
+
+ if (searchString.charAt(0).match(regexp) !== null) {
+ return true;
+ } else {
+ return false;
+ }
+ });
+ };
+});
+
+mach2Filters.filter('dateFilter', function() {
+ return function(items, search) {
+ return items.filter(function(element, index, array) {
+ var albumDate = moment(element.date, 'YYYY-MM-DD');
+ var compDate = moment(search, 'YYYY');
+ var compNextDate = moment((parseInt(search) + 10), 'YYYY');
+
+ return (albumDate.isAfter(compDate) && albumDate.isBefore(compNextDate));
+ });
+ };
+}); \ No newline at end of file
diff --git a/static/scripts/app/services.js b/static/scripts/app/services.js
new file mode 100644
index 0000000..c7cd92b
--- /dev/null
+++ b/static/scripts/app/services.js
@@ -0,0 +1,97 @@
+var mach2Services = angular.module('mach2Services', ['ngResource']);
+
+mach2Services.factory('Artist', ['$resource', function($resource) {
+ return $resource('artists/:artistId', {}, {
+ query: {
+ method: 'GET'
+ }
+ });
+}]);
+
+mach2Services.factory('ArtistAlbums', ['$resource', function($resource) {
+ return $resource('artists/:artistId/albums', {}, {
+ query: {
+ method: 'GET',
+ isArray: true
+ }
+ });
+}]);
+
+mach2Services.factory('ArtistSearch', ['$resource', function($resource) {
+ return $resource('artists/:name', {}, {
+ query: {
+ method: 'GET',
+ isArray: true
+ }
+ });
+}]);
+
+mach2Services.factory('ArtistTracks', ['$resource', function($resource) {
+ return $resource('artists/:artistId/tracks', {}, {
+ query: {
+ method: 'GET',
+ isArray: true
+ }
+ });
+}]);
+
+mach2Services.factory('Album', ['$resource', function($resource) {
+ return $resource('albums/:albumId', {}, {
+ query: {
+ method: 'GET'
+ }
+ });
+}]);
+
+mach2Services.factory('AlbumArtists', ['$resource', function($resource) {
+ return $resource('albums/:albumId/artists', {}, {
+ query: {
+ method: 'GET',
+ isArray: true
+ }
+ });
+}]);
+
+mach2Services.factory('AlbumSearch', ['$resource', function($resource) {
+ return $resource('albums/:name', {}, {
+ query: {
+ method: 'GET',
+ isArray: true,
+ }
+ });
+}]);
+
+mach2Services.factory('AlbumTracks', ['$resource', function($resource) {
+ return $resource('albums/:albumId/tracks', {}, {
+ query: {
+ method: 'GET',
+ isArray: true
+ }
+ });
+}]);
+
+mach2Services.factory('Track', ['$resource', function($resource) {
+ return $resource('tracks/:trackId', {}, {
+ query: {
+ method: 'GET'
+ }
+ });
+}]);
+
+mach2Services.factory('TrackArtists', ['$resource', function($resource) {
+ return $resource('tracks/:trackId/artists', {}, {
+ query: {
+ method: 'GET'
+ }
+ });
+}]);
+
+
+mach2Services.factory('TrackSearch', ['$resource', function($resource) {
+ return $resource('tracks/:name', {}, {
+ query: {
+ method: 'GET',
+ isArray: true
+ }
+ });
+}]);
diff --git a/templates/index.html b/templates/index.html
new file mode 100644
index 0000000..9f3d8b6
--- /dev/null
+++ b/templates/index.html
@@ -0,0 +1,64 @@
+<!doctype html>
+<html lang="en" data-ng-app="mach2App">
+ <head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>Mach2</title>
+ <link rel="stylesheet" href="{{ url_for('static', filename='scripts/libs/bootstrap/css/bootstrap.css') }}">
+ </head>
+ <body>
+ <div class="container">
+ <nav class="navbar navbar-inverse navbar" data-ng-controller="NavCtrl">
+ <div class="container-fluid">
+ <div class="navbar-header">
+ <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar-collapse">
+ <span class="sr-only">Toggle navigation</span>
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ </button>
+ <a class="navbar-brand" href="#">mach2</a>
+ </div>
+ <div class="collapse navbar-collapse" id="navbar-collapse">
+ <ul class="nav navbar-nav">
+ <li class="dropdown">
+ <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">Music <span class="caret"></span></a>
+ <ul class="dropdown-menu" role="menu">
+ <li><a href="#/artists">Artists</a></li>
+ <li><a href="#/albums">Albums</a></li>
+ <li><a href="#/tracks">Tracks</a></li>
+ </ul>
+ </li>
+ <li><a href="#">Settings</a></li>
+ </ul>
+ <ul class="nav navbar-nav navbar-right">
+ <li class="dropdown">
+ <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">{{ user.username }}<span class="caret"></span></a>
+ <ul class="dropdown-menu" role="menu">
+ <li><a href="#">Change password</a></li>
+ <li class="divider"></li>
+ <li><a href="/logout">Log out</a></li>
+ </ul>
+ </li>
+ </ul>
+ </div>
+ </div>
+ </nav>
+ <div class="row" data-ui-view>
+ </div>
+ </div>
+ <script src="{{ url_for('static', filename='scripts/libs/jquery/jquery.js') }}"></script>
+ <script src="{{ url_for('static', filename='scripts/libs/bootstrap/js/bootstrap.js') }}"></script>
+ <script src="{{ url_for('static', filename='scripts/libs/moment/moment.js') }}"></script>
+ <script src="{{ url_for('static', filename='scripts/libs/moment-timezone/moment-timezone-with-data-2010-2020.js') }}"></script>
+ <script src="{{ url_for('static', filename='scripts/libs/angular/angular.js') }}"></script>
+ <script src="{{ url_for('static', filename='scripts/libs/angular-resource/angular-resource.js') }}"></script>
+ <script src="{{ url_for('static', filename='scripts/libs/angular-ui-router/angular-ui-router.js') }}"></script>
+ <script src="{{ url_for('static', filename='scripts/libs/angular-bootstrap/ui-bootstrap-tpls.js') }}"></script>
+ <script src="{{ url_for('static', filename='scripts/libs/angular-moment/angular-moment.js') }}"></script>
+ <script src="{{ url_for('static', filename='scripts/app/filters.js') }}"></script>
+ <script src="{{ url_for('static', filename='scripts/app/services.js') }}"></script>
+ <script src="{{ url_for('static', filename='scripts/app/controllers.js') }}"></script>
+ <script src="{{ url_for('static', filename='scripts/app/app.js') }}"></script>
+ </body>
+</html>
diff --git a/templates/login.html b/templates/login.html
new file mode 100644
index 0000000..0f3d529
--- /dev/null
+++ b/templates/login.html
@@ -0,0 +1,39 @@
+<!doctype html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>Mach2</title>
+ <script src="{{ url_for('static', filename='scripts/lib/jquery/jquery.js') }}"></script>
+ <script src="{{ url_for('static', filename='scripts/lib/bootstrap/js/bootstrap.js') }}"></script>
+ <link rel="stylesheet" href="{{ url_for('static', filename='scripts/lib/bootstrap/css/bootstrap.css') }}">
+ </head>
+ <body>
+ <div class="container">
+ <div class="col-xs-12 text-center">
+ <h1>Mach2</h1>
+ </div>
+ <div class="col-xs-offset-2 col-xs-8">
+ <form class="form-horizontal" action="/login" method="POST">
+ <div class="form-group">
+ <label for="username" class="col-xs-12 col-sm-3 control-label">Username</label>
+ <div class="col-xs-12 col-sm-9">
+ <input type="text" class="form-control" id="username" name="username" placeholder="Username" />
+ </div>
+ </div>
+ <div class="form-group">
+ <label for="password" class="col-xs-12 col-sm-3 control-label">Password</label>
+ <div class="col-xs-12 col-sm-9">
+ <input type="password" class="form-control" id="password" name="password" placeholder="Password" />
+ </div>
+ </div>
+ <div class="form-group">
+ <div class="col-xs-12 col-sm-offset-3 col-sm-9">
+ <button type="submit" class="btn btn-primary btn-block">Sign in</button>
+ </div>
+ </div>
+ </form>
+ </div>
+ </div>
+ </body>
+</html>