diff --git a/.gitignore b/.gitignore index 3b79d0f..e7ddeff 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,5 @@ public/api taiko.db version.json public/index.html -config.json +config.py public/assets/song_skins -secret.txt diff --git a/app.py b/app.py index 76a0dcc..0726ef9 100644 --- a/app.py +++ b/app.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import bcrypt +import config import json import re import schema @@ -14,27 +15,18 @@ from ffmpy import FFmpeg from pymongo import MongoClient app = Flask(__name__) -client = MongoClient() - -try: - app.secret_key = open('secret.txt').read().strip() -except FileNotFoundError: - app.secret_key = os.urandom(24).hex() - with open('secret.txt', 'w') as fp: - fp.write(app.secret_key) - fp.close() +client = MongoClient(host=config.MONGO['host']) +app.secret_key = config.SECRET_KEY app.config['SESSION_TYPE'] = 'redis' app.config['SESSION_COOKIE_HTTPONLY'] = False -app.cache = Cache(app, config={'CACHE_TYPE': 'redis'}) +app.cache = Cache(app, config=config.REDIS) sess = Session() sess.init_app(app) -db = client.taiko +db = client[config.MONGO['database']] db.users.create_index('username', unique=True) - -DEFAULT_URL = 'https://github.com/bui/taiko-web/' - +db.songs.create_index('id', unique=True) def api_error(message): return jsonify({'status': 'error', 'message': message}) @@ -49,17 +41,19 @@ def login_required(f): return decorated_function -def admin_required(f): - @wraps(f) - def decorated_function(*args, **kwargs): - if not session.get('username'): - return abort(403) +def admin_required(level): + def decorated_function(f): + @wraps(f) + def wrapper(*args, **kwargs): + if not session.get('username'): + return abort(403) + + user = db.users.find_one({'username': session.get('username')}) + if user['user_level'] < level: + return abort(403) - user = db.users.find_one({'username': session.get('username')}) - if user['user_level'] < 50: - return abort(403) - - return f(*args, **kwargs) + return f(*args, **kwargs) + return wrapper return decorated_function @@ -71,27 +65,24 @@ def before_request_func(): def get_config(): - if os.path.isfile('config.json'): - try: - config = json.load(open('config.json', 'r')) - except ValueError: - print('WARNING: Invalid config.json, using default values') - config = {} - else: - print('WARNING: No config.json found, using default values') - config = {} + config_out = { + 'songs_baseurl': config.SONGS_BASEURL, + 'assets_baseurl': config.ASSETS_BASEURL, + 'email': config.EMAIL, + 'accounts': config.ACCOUNTS + } - if not config.get('songs_baseurl'): - config['songs_baseurl'] = ''.join([request.host_url, 'songs']) + '/' - if not config.get('assets_baseurl'): - config['assets_baseurl'] = ''.join([request.host_url, 'assets']) + '/' + if not config_out.get('songs_baseurl'): + config_out['songs_baseurl'] = ''.join([request.host_url, 'songs']) + '/' + if not config_out.get('assets_baseurl'): + config_out['assets_baseurl'] = ''.join([request.host_url, 'assets']) + '/' - config['_version'] = get_version() - return config + config_out['_version'] = get_version() + return config_out def get_version(): - version = {'commit': None, 'commit_short': '', 'version': None, 'url': DEFAULT_URL} + version = {'commit': None, 'commit_short': '', 'version': None, 'url': config.URL} if os.path.isfile('version.json'): try: ver = json.load(open('version.json', 'r')) @@ -114,20 +105,21 @@ def route_index(): @app.route('/admin') -@admin_required +@admin_required(level=50) def route_admin(): return redirect('/admin/songs') @app.route('/admin/songs') -@admin_required +@admin_required(level=50) def route_admin_songs(): songs = db.songs.find({}) - return render_template('admin_songs.html', songs=list(songs)) + user = db.users.find_one({'username': session['username']}) + return render_template('admin_songs.html', songs=list(songs), admin=user) @app.route('/admin/songs/') -@admin_required +@admin_required(level=50) def route_admin_songs_id(id): song = db.songs.find_one({'id': id}) if not song: @@ -142,8 +134,58 @@ def route_admin_songs_id(id): song=song, categories=categories, song_skins=song_skins, makers=makers, admin=user) +@app.route('/admin/songs/new') +@admin_required(level=100) +def route_admin_songs_new(): + categories = list(db.categories.find({})) + song_skins = list(db.song_skins.find({})) + makers = list(db.makers.find({})) + + return render_template('admin_song_new.html', categories=categories, song_skins=song_skins, makers=makers) + + +@app.route('/admin/songs/new', methods=['POST']) +@admin_required(level=100) +def route_admin_songs_new_post(): + output = {'title_lang': {}, 'subtitle_lang': {}, 'courses': {}} + output['enabled'] = True if request.form.get('enabled') else False + output['title'] = request.form.get('title') or None + output['subtitle'] = request.form.get('subtitle') or None + for lang in ['ja', 'en', 'cn', 'tw', 'ko']: + output['title_lang'][lang] = request.form.get('title_%s' % lang) or None + output['subtitle_lang'][lang] = request.form.get('subtitle_%s' % lang) or None + + for course in ['easy', 'normal', 'hard', 'oni', 'ura']: + if request.form.get('course_%s' % course): + output['courses'][course] = {'stars': int(request.form.get('course_%s' % course)), + 'branch': True if request.form.get('branch_%s' % course) else False} + else: + output['courses'][course] = None + + output['category_id'] = int(request.form.get('category_id')) or None + output['type'] = request.form.get('type') + output['offset'] = float(request.form.get('offset')) or None + output['skin_id'] = int(request.form.get('skin_id')) or None + output['preview'] = float(request.form.get('preview')) or None + output['volume'] = float(request.form.get('volume')) or None + output['maker_id'] = int(request.form.get('maker_id')) or None + output['hash'] = None + + seq = db.seq.find_one({'name': 'songs'}) + seq_new = seq['value'] + 1 if seq else 1 + output['id'] = seq_new + output['order'] = seq_new + + db.songs.insert_one(output) + flash('Song created.') + + db.seq.update_one({'name': 'songs'}, {'$set': {'value': seq_new}}, upsert=True) + + return redirect('/admin/songs/%s' % str(seq_new)) + + @app.route('/admin/songs/', methods=['POST']) -@admin_required +@admin_required(level=100) def route_admin_songs_id_post(id): song = db.songs.find_one({'id': id}) if not song: @@ -183,6 +225,18 @@ def route_admin_songs_id_post(id): return redirect('/admin/songs/%s' % id) +@app.route('/admin/songs//delete', methods=['POST']) +@admin_required(level=100) +def route_admin_songs_id_delete(id): + song = db.songs.find_one({'id': id}) + if not song: + return abort(404) + + db.songs.delete_one({'id': id}) + flash('Song deleted.') + return redirect('/admin/songs') + + @app.route('/api/preview') @app.cache.cached(timeout=15, query_string=True) def route_api_preview(): diff --git a/config.example.json b/config.example.json deleted file mode 100644 index f81c1b7..0000000 --- a/config.example.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "songs_baseurl": "", - "assets_baseurl": "", - "email": "", - "accounts": true -} diff --git a/config.example.py b/config.example.py new file mode 100644 index 0000000..1fa4fc7 --- /dev/null +++ b/config.example.py @@ -0,0 +1,32 @@ +# The full URL base asset URL, with trailing slash. +ASSETS_BASEURL = '' + +# The full URL base song URL, with trailing slash. +SONGS_BASEURL = '' + +# The email address to display in the "About Simulator" menu. +EMAIL = 'taiko@example.com' + +# Whether to use the user account system. +ACCOUNTS = True + +# MongoDB server settings. +MONGO = { + 'host': ['localhost:27017'], + 'database': 'taiko' +} + +# Redis server settings, used for sessions + cache. +REDIS = { + 'CACHE_TYPE': 'redis', + 'CACHE_REDIS_HOST': '127.0.0.1', + 'CACHE_REDIS_PORT': 6379, + 'CACHE_REDIS_PASSWORD': None, + 'CACHE_REDIS_DB': None +} + +# Secret key used for sessions. +SECRET_KEY = 'change-me' + +# Git repository base URL. +URL = 'https://github.com/bui/taiko-web/' diff --git a/public/src/css/admin.css b/public/src/css/admin.css index 7445630..69bb27c 100644 --- a/public/src/css/admin.css +++ b/public/src/css/admin.css @@ -130,3 +130,23 @@ h1 small { margin-bottom: 10px; color: white; } + +.save-song { + font-size: 22pt; + width: 120px; +} + +.delete-song button { + float: right; + margin-top: -25px; + font-size: 12pt; +} + +.side-button { + float: right; + background: green; + padding: 5px 20px; + color: white; + text-decoration: none; + margin-top: 25px; +} diff --git a/templates/admin_song_detail.html b/templates/admin_song_detail.html index 9389f2d..27132f3 100644 --- a/templates/admin_song_detail.html +++ b/templates/admin_song_detail.html @@ -46,19 +46,19 @@

Courses

- + - + - + - + - +
@@ -115,7 +115,12 @@ - + + {% if admin.user_level >= 100 %} +
+ +
+ {% endif %} {% endblock %} diff --git a/templates/admin_song_new.html b/templates/admin_song_new.html new file mode 100644 index 0000000..9fc1361 --- /dev/null +++ b/templates/admin_song_new.html @@ -0,0 +1,121 @@ +{% extends 'admin.html' %} +{% block content %} +

New song

+{% for message in get_flashed_messages() %} +
{{ message }}
+{% endfor %} +
+
+ +
+ +
+ +
+

Title

+ + + + + + + + + + + + +
+ +
+

Subtitle

+ + + + + + + + + + + + +
+ +
+

Courses

+ + + + + + + + + + + + + + + +
+ +
+

+ +
+ +
+

+ +
+ +
+

+ +
+ +
+

+ +
+ +
+

+ +
+ +
+

+ +
+ +
+

+ +
+ + +
+
+{% endblock %} diff --git a/templates/admin_songs.html b/templates/admin_songs.html index eb55425..1c384a4 100644 --- a/templates/admin_songs.html +++ b/templates/admin_songs.html @@ -1,6 +1,12 @@ {% extends 'admin.html' %} {% block content %} +{% if admin.user_level >= 100 %} +New song +{% endif %}

Songs

+{% for message in get_flashed_messages() %} +
{{ message }}
+{% endfor %} {% for song in songs %}
diff --git a/tools/migrate_db.py b/tools/migrate_db.py index 9f88bfd..e04ea9f 100644 --- a/tools/migrate_db.py +++ b/tools/migrate_db.py @@ -4,24 +4,30 @@ import sqlite3 from pymongo import MongoClient -client = MongoClient() -client.drop_database('taiko') -db = client.taiko +import os,sys,inspect +current_dir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) +parent_dir = os.path.dirname(current_dir) +sys.path.insert(0, parent_dir) +import config + +client = MongoClient(config.MONGO['host']) +client.drop_database(config.MONGO['database']) +db = client[config.MONGO['database']] sqdb = sqlite3.connect('taiko.db') sqdb.row_factory = sqlite3.Row curs = sqdb.cursor() def migrate_songs(): - curs.execute('select * from songs') + curs.execute('select * from songs order by id') rows = curs.fetchall() for row in rows: song = { 'id': row['id'], 'title': row['title'], - 'title_lang': {'ja': row['title']}, + 'title_lang': {'ja': row['title'], 'en': None, 'cn': None, 'tw': None, 'ko': None}, 'subtitle': row['subtitle'], - 'subtitle_lang': {'ja': row['subtitle']}, + 'subtitle_lang': {'ja': row['subtitle'], 'en': None, 'cn': None, 'tw': None, 'ko': None}, 'courses': {'easy': None, 'normal': None, 'hard': None, 'oni': None, 'ura': None}, 'enabled': True if row['enabled'] else False, 'category_id': row['category'], @@ -63,6 +69,9 @@ def migrate_songs(): song['subtitle_lang']['en'] = lang db.songs.insert_one(song) + last_song = song['id'] + + db.seq.insert_one({'name': 'songs', 'value': last_song}) def migrate_makers(): curs.execute('select * from makers')