diff options
author | Michaël Ball <michael.ball@gmail.com> | 2014-12-28 12:24:22 +0000 |
---|---|---|
committer | Michaël Ball <michael.ball@gmail.com> | 2015-11-27 20:02:04 +0000 |
commit | 75beec91a8526fbbc0a90134140b9dff6af15c0c (patch) | |
tree | 02414e46da3e08000384c40c27b7aab9748de0fe | |
parent | a2964845e3c03b9cf5f01583f53f7553c7d67caf (diff) |
Initial frontend work
-rw-r--r-- | .gitignore | 7 | ||||
-rw-r--r-- | Gruntfile.js | 28 | ||||
-rw-r--r-- | app.db | bin | 0 -> 3072 bytes | |||
-rw-r--r-- | bower.json | 39 | ||||
-rw-r--r-- | common/security.py | 15 | ||||
-rw-r--r-- | common/utils.py | 14 | ||||
-rw-r--r-- | db/db_manager.py | 2 | ||||
-rw-r--r-- | mach2.py | 297 | ||||
-rw-r--r-- | models/album.py | 13 | ||||
-rw-r--r-- | models/track.py | 27 | ||||
-rw-r--r-- | models/user.py | 52 | ||||
-rw-r--r-- | package.json | 16 | ||||
-rw-r--r-- | static/partials/albums/list.html | 28 | ||||
-rw-r--r-- | static/partials/artists/detail.html | 12 | ||||
-rw-r--r-- | static/partials/artists/list.html | 28 | ||||
-rw-r--r-- | static/partials/artists/tracks.html | 7 | ||||
-rw-r--r-- | static/scripts/app/app.js | 63 | ||||
-rw-r--r-- | static/scripts/app/controllers.js | 148 | ||||
-rw-r--r-- | static/scripts/app/filters.js | 48 | ||||
-rw-r--r-- | static/scripts/app/services.js | 97 | ||||
-rw-r--r-- | templates/index.html | 64 | ||||
-rw-r--r-- | templates/login.html | 39 |
22 files changed, 1005 insertions, 39 deletions
@@ -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"]); +}; Binary files differdiff --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 @@ -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> |