japanese-drum-game/app.py

380 lines
11 KiB
Python
Raw Normal View History

2020-03-13 10:45:42 +08:00
#!/usr/bin/env python3
2018-10-18 05:16:53 +08:00
2020-03-13 10:45:42 +08:00
import bcrypt
2018-10-11 04:57:29 +08:00
import json
2018-08-27 20:28:30 +08:00
import re
2020-03-13 10:45:42 +08:00
import schema
2018-08-27 20:28:30 +08:00
import os
2020-03-13 10:45:42 +08:00
from functools import wraps
from flask import Flask, g, jsonify, render_template, request, abort, redirect, session
2019-02-14 18:31:23 +08:00
from flask_caching import Cache
2020-03-13 10:45:42 +08:00
from flask_session import Session
2018-10-18 05:16:53 +08:00
from ffmpy import FFmpeg
2020-03-13 10:45:42 +08:00
from pymongo import MongoClient
2018-08-27 00:14:56 +08:00
app = Flask(__name__)
2020-03-13 10:45:42 +08:00
client = MongoClient()
2019-02-14 18:31:23 +08:00
try:
2020-03-13 10:45:42 +08:00
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()
app.config['SESSION_TYPE'] = 'redis'
2020-03-13 11:19:26 +08:00
app.config['SESSION_COOKIE_HTTPONLY'] = False
2020-03-13 10:45:42 +08:00
app.cache = Cache(app, config={'CACHE_TYPE': 'redis'})
sess = Session()
sess.init_app(app)
db = client.taiko
db.users.create_index('username', unique=True)
2019-02-14 18:31:23 +08:00
2019-02-07 02:49:25 +08:00
DEFAULT_URL = 'https://github.com/bui/taiko-web/'
2018-08-27 00:14:56 +08:00
2020-03-13 10:45:42 +08:00
def api_error(message):
return jsonify({'status': 'error', 'message': message})
def login_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if not session.get('username'):
return api_error('not_logged_in')
return f(*args, **kwargs)
return decorated_function
def admin_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if not session.get('username'):
return abort(403)
2018-08-27 00:14:56 +08:00
2020-03-13 10:45:42 +08:00
user = db.users.find_one({'username': session.get('username')})
if user['user_level'] < 100:
return abort(403)
2018-08-27 00:14:56 +08:00
2020-03-13 10:45:42 +08:00
return f(*args, **kwargs)
return decorated_function
2018-08-27 00:14:56 +08:00
2018-10-28 17:59:49 +08:00
def get_config():
if os.path.isfile('config.json'):
try:
config = json.load(open('config.json', 'r'))
except ValueError:
2019-02-21 06:02:44 +08:00
print('WARNING: Invalid config.json, using default values')
2018-10-28 17:59:49 +08:00
config = {}
else:
2019-02-21 06:02:44 +08:00
print('WARNING: No config.json found, using default values')
2018-10-28 17:59:49 +08:00
config = {}
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']) + '/'
2018-12-06 05:55:28 +08:00
config['_version'] = get_version()
2018-10-28 17:59:49 +08:00
return config
2018-12-06 05:47:35 +08:00
def get_version():
2019-02-07 02:49:25 +08:00
version = {'commit': None, 'commit_short': '', 'version': None, 'url': DEFAULT_URL}
2018-12-06 05:47:35 +08:00
if os.path.isfile('version.json'):
2019-02-07 02:49:25 +08:00
try:
ver = json.load(open('version.json', 'r'))
except ValueError:
print('Invalid version.json file')
return version
for key in version.keys():
if ver.get(key):
version[key] = ver.get(key)
2018-12-06 05:47:35 +08:00
return version
2018-10-11 04:57:29 +08:00
@app.route('/')
2019-02-14 18:36:39 +08:00
@app.cache.cached(timeout=15)
2018-10-11 04:57:29 +08:00
def route_index():
2018-12-06 05:47:35 +08:00
version = get_version()
2018-10-28 17:59:49 +08:00
return render_template('index.html', version=version, config=get_config())
2018-10-11 04:57:29 +08:00
2020-03-13 10:45:42 +08:00
@app.route('/admin')
@admin_required
def route_admin():
return redirect('/admin/songs')
@app.route('/admin/songs')
@admin_required
def route_admin_songs():
songs = db.songs.find({})
return render_template('admin_songs.html', songs=list(songs))
@app.route('/admin/songs/<int:id>')
@admin_required
def route_admin_songs_id(id):
song = db.songs.find_one({'id': id})
if not song:
return abort(404)
categories = list(db.categories.find({}))
song_skins = list(db.song_skins.find({}))
return render_template('admin_song_detail.html',
song=song, categories=categories, song_skins=song_skins)
2018-10-18 05:16:53 +08:00
@app.route('/api/preview')
2020-02-22 21:09:07 +08:00
@app.cache.cached(timeout=15, query_string=True)
2018-10-18 05:16:53 +08:00
def route_api_preview():
song_id = request.args.get('id', None)
if not song_id or not re.match('^[0-9]+$', song_id):
abort(400)
2020-03-13 10:45:42 +08:00
song = db.songs.find_one({'id': song_id})
if not song:
2018-10-18 05:16:53 +08:00
abort(400)
2020-03-13 10:45:42 +08:00
song_type = song['type']
prev_path = make_preview(song_id, song_type, song['preview'])
2018-10-18 05:16:53 +08:00
if not prev_path:
return redirect(get_config()['songs_baseurl'] + '%s/main.mp3' % song_id)
2018-10-18 05:16:53 +08:00
return redirect(get_config()['songs_baseurl'] + '%s/preview.mp3' % song_id)
2018-10-18 05:16:53 +08:00
2018-08-27 00:14:56 +08:00
@app.route('/api/songs')
2019-02-14 18:31:23 +08:00
@app.cache.cached(timeout=15)
2018-08-27 00:14:56 +08:00
def route_api_songs():
2020-03-13 10:45:42 +08:00
songs = list(db.songs.find({'enabled': True}, {'_id': False, 'enabled': False}))
2018-08-27 20:28:30 +08:00
for song in songs:
2020-03-13 10:45:42 +08:00
if song['maker_id']:
if song['maker_id'] == 0:
song['maker'] = 0
else:
song['maker'] = db.makers.find_one({'id': song['maker_id']}, {'_id': False})
else:
song['maker'] = None
del song['maker_id']
if song['category_id']:
song['category'] = db.categories.find_one({'id': song['category_id']})['title']
else:
song['category'] = None
del song['category_id']
if song['skin_id']:
song['song_skin'] = db.song_skins.find_one({'id': song['skin_id']}, {'_id': False, 'id': False})
else:
song['song_skin'] = None
del song['skin_id']
return jsonify(songs)
2018-08-27 00:14:56 +08:00
2018-10-28 05:42:28 +08:00
@app.route('/api/config')
2019-02-14 18:31:23 +08:00
@app.cache.cached(timeout=15)
2018-10-28 05:42:28 +08:00
def route_api_config():
2018-10-28 17:59:49 +08:00
config = get_config()
2018-10-28 05:42:28 +08:00
return jsonify(config)
2020-03-13 10:45:42 +08:00
@app.route('/api/register', methods=['POST'])
def route_api_register():
if session.get('username'):
return api_error('already_logged_in')
data = request.get_json()
if not schema.validate(data, schema.register):
return abort(400)
username = data.get('username', '')
if len(username) > 20 or not re.match('^[a-zA-Z0-9_]{1,20}$', username):
return api_error('invalid_username')
if db.users.find_one({'username_lower': username.lower()}):
return api_error('username_in_use')
password = data.get('password', '').encode('utf-8')
if not 8 <= len(password) <= 5000:
return api_error('invalid_password')
salt = bcrypt.gensalt()
hashed = bcrypt.hashpw(password, salt)
db.users.insert_one({
'username': username,
'username_lower': username.lower(),
'password': hashed,
'display_name': username,
'user_level': 1
})
session['username'] = username
session.permanent = True
return jsonify({'status': 'ok', 'username': username, 'display_name': username})
@app.route('/api/login', methods=['POST'])
def route_api_login():
if session.get('username'):
return api_error('already_logged_in')
data = request.get_json()
if not schema.validate(data, schema.login):
return abort(400)
username = data.get('username', '')
result = db.users.find_one({'username_lower': username.lower()})
if not result:
return api_error('invalid_username_password')
password = data.get('password', '').encode('utf-8')
if not bcrypt.checkpw(password, result['password']):
return api_error('invalid_username_password')
session['username'] = result['username']
if data.get('remember'):
session.permanent = True
return jsonify({'status': 'ok', 'username': result['username'], 'display_name': result['display_name']})
@app.route('/api/logout', methods=['POST'])
@login_required
def route_api_logout():
session.clear()
return jsonify({'status': 'ok'})
@app.route('/api/account/display_name', methods=['POST'])
@login_required
def route_api_account_display_name():
data = request.get_json()
if not schema.validate(data, schema.update_display_name):
return abort(400)
display_name = data.get('display_name', '')
if not display_name or len(display_name) > 20:
return api_error('invalid_display_name')
db.users.update_one({'username': session.get('username')}, {
'$set': {'display_name': display_name}
})
return jsonify({'status': 'ok'})
@app.route('/api/account/password', methods=['POST'])
@login_required
def route_api_account_password():
data = request.get_json()
if not schema.validate(data, schema.update_password):
return abort(400)
user = db.users.find_one({'username': session.get('username')})
current_password = data.get('current_password', '').encode('utf-8')
if not bcrypt.checkpw(current_password, user['password']):
return api_error('current_password_invalid')
new_password = data.get('new_password', '').encode('utf-8')
if not 8 <= len(new_password) <= 5000:
return api_error('invalid_password')
salt = bcrypt.gensalt()
hashed = bcrypt.hashpw(new_password, salt)
db.users.update_one({'username': session.get('username')}, {
'$set': {'password': hashed}
})
return jsonify({'status': 'ok'})
@app.route('/api/account/remove', methods=['POST'])
@login_required
def route_api_account_remove():
data = request.get_json()
if not schema.validate(data, schema.delete_account):
return abort(400)
user = db.users.find_one({'username': session.get('username')})
password = data.get('password', '').encode('utf-8')
if not bcrypt.checkpw(password, user['password']):
return api_error('current_password_invalid')
db.scores.delete_many({'username': session.get('username')})
db.users.delete_one({'username': session.get('username')})
session.clear()
return jsonify({'status': 'ok'})
@app.route('/api/scores/save', methods=['POST'])
@login_required
def route_api_scores_save():
data = request.get_json()
if not schema.validate(data, schema.scores_save):
return abort(400)
username = session.get('username')
if data.get('is_import'):
db.scores.delete_many({'username': username})
scores = data.get('scores', [])
for score in scores:
db.scores.update_one({'username': username, 'hash': score['hash']},
{'$set': {
'username': username,
'hash': score['hash'],
'score': score['score']
}}, upsert=True)
2020-03-13 11:54:27 +08:00
return jsonify({'status': 'ok'})
2020-03-13 10:45:42 +08:00
@app.route('/api/scores/get')
@login_required
def route_api_scores_get():
username = session.get('username')
scores = []
for score in db.scores.find({'username': username}):
scores.append({
'hash': score['hash'],
'score': score['score']
})
user = db.users.find_one({'username': username})
2020-03-13 11:19:26 +08:00
return jsonify({'status': 'ok', 'scores': scores, 'username': user['username'], 'display_name': user['display_name']})
2020-03-13 10:45:42 +08:00
2019-02-21 06:02:44 +08:00
def make_preview(song_id, song_type, preview):
2018-10-18 05:16:53 +08:00
song_path = 'public/songs/%s/main.mp3' % song_id
prev_path = 'public/songs/%s/preview.mp3' % song_id
if os.path.isfile(song_path) and not os.path.isfile(prev_path):
2019-02-21 06:42:18 +08:00
if not preview or preview <= 0:
2019-02-07 02:49:25 +08:00
print('Skipping #%s due to no preview' % song_id)
2018-10-18 05:16:53 +08:00
return False
2019-02-07 02:49:25 +08:00
print('Making preview.mp3 for song #%s' % song_id)
2018-10-18 05:16:53 +08:00
ff = FFmpeg(inputs={song_path: '-ss %s' % preview},
outputs={prev_path: '-codec:a libmp3lame -ar 32000 -b:a 92k -y -loglevel panic'})
2018-10-18 05:16:53 +08:00
ff.run()
return prev_path
2018-08-27 00:14:56 +08:00
if __name__ == '__main__':
2018-08-27 20:28:30 +08:00
app.run(port=34801)