mirror of
https://github.com/jiojciojsioe3/a3cjroijsiojiorj.git
synced 2024-11-15 15:31:51 +08:00
account system backend, db rewrite
This commit is contained in:
parent
a899fd5cfe
commit
7519b1c4c2
2
.gitignore
vendored
2
.gitignore
vendored
@ -36,6 +36,7 @@ $RECYCLE.BIN/
|
|||||||
.Trashes
|
.Trashes
|
||||||
|
|
||||||
.vscode
|
.vscode
|
||||||
|
*.pyc
|
||||||
|
|
||||||
# Directories potentially created on remote AFP share
|
# Directories potentially created on remote AFP share
|
||||||
.AppleDB
|
.AppleDB
|
||||||
@ -50,3 +51,4 @@ version.json
|
|||||||
public/index.html
|
public/index.html
|
||||||
config.json
|
config.json
|
||||||
public/assets/song_skins
|
public/assets/song_skins
|
||||||
|
secret.txt
|
||||||
|
344
app.py
344
app.py
@ -1,39 +1,65 @@
|
|||||||
#!/usr/bin/env python2
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
from __future__ import division
|
|
||||||
|
|
||||||
|
import bcrypt
|
||||||
import json
|
import json
|
||||||
import sqlite3
|
|
||||||
import re
|
import re
|
||||||
|
import schema
|
||||||
import os
|
import os
|
||||||
from flask import Flask, g, jsonify, render_template, request, abort, redirect
|
|
||||||
|
from functools import wraps
|
||||||
|
from flask import Flask, g, jsonify, render_template, request, abort, redirect, session
|
||||||
from flask_caching import Cache
|
from flask_caching import Cache
|
||||||
|
from flask_session import Session
|
||||||
from ffmpy import FFmpeg
|
from ffmpy import FFmpeg
|
||||||
|
from pymongo import MongoClient
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
try:
|
client = MongoClient()
|
||||||
app.cache = Cache(app, config={'CACHE_TYPE': 'redis'})
|
|
||||||
except RuntimeError:
|
try:
|
||||||
import tempfile
|
app.secret_key = open('secret.txt').read().strip()
|
||||||
app.cache = Cache(app, config={'CACHE_TYPE': 'filesystem', 'CACHE_DIR': tempfile.gettempdir()})
|
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'
|
||||||
|
app.cache = Cache(app, config={'CACHE_TYPE': 'redis'})
|
||||||
|
sess = Session()
|
||||||
|
sess.init_app(app)
|
||||||
|
|
||||||
|
db = client.taiko
|
||||||
|
db.users.create_index('username', unique=True)
|
||||||
|
|
||||||
DATABASE = 'taiko.db'
|
|
||||||
DEFAULT_URL = 'https://github.com/bui/taiko-web/'
|
DEFAULT_URL = 'https://github.com/bui/taiko-web/'
|
||||||
|
|
||||||
|
|
||||||
def get_db():
|
def api_error(message):
|
||||||
db = getattr(g, '_database', None)
|
return jsonify({'status': 'error', 'message': message})
|
||||||
if db is None:
|
|
||||||
db = g._database = sqlite3.connect(DATABASE)
|
|
||||||
db.row_factory = sqlite3.Row
|
|
||||||
return db
|
|
||||||
|
|
||||||
|
|
||||||
def query_db(query, args=(), one=False):
|
def login_required(f):
|
||||||
cur = get_db().execute(query, args)
|
@wraps(f)
|
||||||
rv = cur.fetchall()
|
def decorated_function(*args, **kwargs):
|
||||||
cur.close()
|
if not session.get('username'):
|
||||||
return (rv[0] if rv else None) if one else rv
|
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)
|
||||||
|
|
||||||
|
user = db.users.find_one({'username': session.get('username')})
|
||||||
|
if user['user_level'] < 100:
|
||||||
|
return abort(403)
|
||||||
|
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
return decorated_function
|
||||||
|
|
||||||
|
|
||||||
def get_config():
|
def get_config():
|
||||||
@ -72,13 +98,6 @@ def get_version():
|
|||||||
return version
|
return version
|
||||||
|
|
||||||
|
|
||||||
@app.teardown_appcontext
|
|
||||||
def close_connection(exception):
|
|
||||||
db = getattr(g, '_database', None)
|
|
||||||
if db is not None:
|
|
||||||
db.close()
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
@app.cache.cached(timeout=15)
|
@app.cache.cached(timeout=15)
|
||||||
def route_index():
|
def route_index():
|
||||||
@ -86,6 +105,33 @@ def route_index():
|
|||||||
return render_template('index.html', version=version, config=get_config())
|
return render_template('index.html', version=version, config=get_config())
|
||||||
|
|
||||||
|
|
||||||
|
@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)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/preview')
|
@app.route('/api/preview')
|
||||||
@app.cache.cached(timeout=15, query_string=True)
|
@app.cache.cached(timeout=15, query_string=True)
|
||||||
def route_api_preview():
|
def route_api_preview():
|
||||||
@ -93,12 +139,12 @@ def route_api_preview():
|
|||||||
if not song_id or not re.match('^[0-9]+$', song_id):
|
if not song_id or not re.match('^[0-9]+$', song_id):
|
||||||
abort(400)
|
abort(400)
|
||||||
|
|
||||||
song_row = query_db('select * from songs where id = ? and enabled = 1', (song_id,))
|
song = db.songs.find_one({'id': song_id})
|
||||||
if not song_row:
|
if not song:
|
||||||
abort(400)
|
abort(400)
|
||||||
|
|
||||||
song_type = song_row[0]['type']
|
song_type = song['type']
|
||||||
prev_path = make_preview(song_id, song_type, song_row[0]['preview'])
|
prev_path = make_preview(song_id, song_type, song['preview'])
|
||||||
if not prev_path:
|
if not prev_path:
|
||||||
return redirect(get_config()['songs_baseurl'] + '%s/main.mp3' % song_id)
|
return redirect(get_config()['songs_baseurl'] + '%s/main.mp3' % song_id)
|
||||||
|
|
||||||
@ -108,52 +154,30 @@ def route_api_preview():
|
|||||||
@app.route('/api/songs')
|
@app.route('/api/songs')
|
||||||
@app.cache.cached(timeout=15)
|
@app.cache.cached(timeout=15)
|
||||||
def route_api_songs():
|
def route_api_songs():
|
||||||
songs = query_db('select s.*, m.name, m.url from songs s left join makers m on s.maker_id = m.maker_id where enabled = 1')
|
songs = list(db.songs.find({'enabled': True}, {'_id': False, 'enabled': False}))
|
||||||
|
|
||||||
raw_categories = query_db('select * from categories')
|
|
||||||
categories = {}
|
|
||||||
for cat in raw_categories:
|
|
||||||
categories[cat['id']] = cat['title']
|
|
||||||
|
|
||||||
raw_song_skins = query_db('select * from song_skins')
|
|
||||||
song_skins = {}
|
|
||||||
for skin in raw_song_skins:
|
|
||||||
song_skins[skin[0]] = {'name': skin['name'], 'song': skin['song'], 'stage': skin['stage'], 'don': skin['don']}
|
|
||||||
|
|
||||||
songs_out = []
|
|
||||||
for song in songs:
|
for song in songs:
|
||||||
song_id = song['id']
|
if song['maker_id']:
|
||||||
song_type = song['type']
|
|
||||||
preview = song['preview']
|
|
||||||
|
|
||||||
category_out = categories[song['category']] if song['category'] in categories else ''
|
|
||||||
song_skin_out = song_skins[song['skin_id']] if song['skin_id'] in song_skins else None
|
|
||||||
maker = None
|
|
||||||
if song['maker_id'] == 0:
|
if song['maker_id'] == 0:
|
||||||
maker = 0
|
song['maker'] = 0
|
||||||
elif song['maker_id'] and song['maker_id'] > 0:
|
else:
|
||||||
maker = {'name': song['name'], 'url': song['url'], 'id': song['maker_id']}
|
song['maker'] = db.makers.find_one({'id': song['maker_id']}, {'_id': False})
|
||||||
|
else:
|
||||||
|
song['maker'] = None
|
||||||
|
del song['maker_id']
|
||||||
|
|
||||||
songs_out.append({
|
if song['category_id']:
|
||||||
'id': song_id,
|
song['category'] = db.categories.find_one({'id': song['category_id']})['title']
|
||||||
'title': song['title'],
|
else:
|
||||||
'title_lang': song['title_lang'],
|
song['category'] = None
|
||||||
'subtitle': song['subtitle'],
|
del song['category_id']
|
||||||
'subtitle_lang': song['subtitle_lang'],
|
|
||||||
'stars': [
|
|
||||||
song['easy'], song['normal'], song['hard'], song['oni'], song['ura']
|
|
||||||
],
|
|
||||||
'preview': preview,
|
|
||||||
'category': category_out,
|
|
||||||
'type': song_type,
|
|
||||||
'offset': song['offset'],
|
|
||||||
'song_skin': song_skin_out,
|
|
||||||
'volume': song['volume'],
|
|
||||||
'maker': maker,
|
|
||||||
'hash': song['hash']
|
|
||||||
})
|
|
||||||
|
|
||||||
return jsonify(songs_out)
|
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)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/config')
|
@app.route('/api/config')
|
||||||
@ -163,6 +187,176 @@ def route_api_config():
|
|||||||
return jsonify(config)
|
return jsonify(config)
|
||||||
|
|
||||||
|
|
||||||
|
@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)
|
||||||
|
|
||||||
|
return jsonify({'success': True})
|
||||||
|
|
||||||
|
|
||||||
|
@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})
|
||||||
|
return jsonify({'scores': scores, 'username': user['username'], 'display_name': user['display_name']})
|
||||||
|
|
||||||
|
|
||||||
def make_preview(song_id, song_type, preview):
|
def make_preview(song_id, song_type, preview):
|
||||||
song_path = 'public/songs/%s/main.mp3' % song_id
|
song_path = 'public/songs/%s/main.mp3' % song_id
|
||||||
prev_path = 'public/songs/%s/preview.mp3' % song_id
|
prev_path = 'public/songs/%s/preview.mp3' % song_id
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
{
|
{
|
||||||
"songs_baseurl": "",
|
"songs_baseurl": "",
|
||||||
"assets_baseurl": ""
|
"assets_baseurl": "",
|
||||||
|
"email": "",
|
||||||
|
"_accounts": true
|
||||||
}
|
}
|
||||||
|
125
public/src/css/admin.css
Normal file
125
public/src/css/admin.css
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: 'Noto Sans JP', sans-serif;
|
||||||
|
background: #FF7F00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
width: 200px;
|
||||||
|
background-color: #A01300;
|
||||||
|
position: fixed;
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav a {
|
||||||
|
display: block;
|
||||||
|
color: #FFF;
|
||||||
|
padding: 16px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav a.active {
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav a:hover:not(.active) {
|
||||||
|
background-color: #555;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
margin-left: 200px;
|
||||||
|
padding: 1px 16px;
|
||||||
|
height: 1000px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 700px) {
|
||||||
|
.nav {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.nav a {float: left;}
|
||||||
|
main {margin-left: 0;}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 400px) {
|
||||||
|
.sidebar a {
|
||||||
|
text-align: center;
|
||||||
|
float: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.song {
|
||||||
|
background: #F84828;
|
||||||
|
color: white;
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 14pt;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.song p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.song-link {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.song-form {
|
||||||
|
background: #ff5333;
|
||||||
|
color: #FFF;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field {
|
||||||
|
background: #555555;
|
||||||
|
padding: 15px 20px 20px 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field > label {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field input {
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field input[type="text"] {
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field input[type="number"] {
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 small {
|
||||||
|
color: #4a4a4a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field-indent {
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox input {
|
||||||
|
margin-right: 3px;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
73
schema.py
Normal file
73
schema.py
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import jsonschema
|
||||||
|
|
||||||
|
def validate(data, schema):
|
||||||
|
try:
|
||||||
|
jsonschema.validate(data, schema)
|
||||||
|
return True
|
||||||
|
except jsonschema.exceptions.ValidationError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
register = {
|
||||||
|
'$schema': 'http://json-schema.org/schema#',
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'username': {'type': 'string'},
|
||||||
|
'password': {'type': 'string'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
login = {
|
||||||
|
'$schema': 'http://json-schema.org/schema#',
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'username': {'type': 'string'},
|
||||||
|
'password': {'type': 'string'},
|
||||||
|
'remember': {'type': 'boolean'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update_display_name = {
|
||||||
|
'$schema': 'http://json-schema.org/schema#',
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'display_name': {'type': 'string'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update_password = {
|
||||||
|
'$schema': 'http://json-schema.org/schema#',
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'current_password': {'type': 'string'},
|
||||||
|
'new_password': {'type': 'string'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delete_account = {
|
||||||
|
'$schema': 'http://json-schema.org/schema#',
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'password': {'type': 'string'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scores_save = {
|
||||||
|
'$schema': 'http://json-schema.org/schema#',
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'scores': {
|
||||||
|
'type': 'array',
|
||||||
|
'items': {'$ref': '#/definitions/score'}
|
||||||
|
},
|
||||||
|
'is_import': {'type': 'boolean'}
|
||||||
|
},
|
||||||
|
'definitions': {
|
||||||
|
'score': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'hash': {'type': 'string'},
|
||||||
|
'score': {'type': 'string'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
23
templates/admin.html
Normal file
23
templates/admin.html
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Taiko Web Admin</title>
|
||||||
|
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;700&display=swap" rel="stylesheet">
|
||||||
|
<link href="/src/css/admin.css" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<div class="nav">
|
||||||
|
<a href="/admin/songs">Songs</a>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<div class="container">
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
87
templates/admin_song_detail.html
Normal file
87
templates/admin_song_detail.html
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
{% extends 'admin.html' %}
|
||||||
|
{% block content %}
|
||||||
|
<h1>{{ song.title }} <small>(ID: {{ song.id }})</small></h1>
|
||||||
|
<div class="song-form">
|
||||||
|
<form method="post">
|
||||||
|
|
||||||
|
<div class="form-field">
|
||||||
|
<span class="checkbox"><input type="checkbox" name="enabled" id="enabled"{% if song.enabled %} checked{% endif %}><label for="enabled"> Enabled</label></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-field">
|
||||||
|
<p>Title</p>
|
||||||
|
<label for="title">Original</label>
|
||||||
|
<input type="text" id="title" value="{{song.title}}" name="title">
|
||||||
|
<label for="title_ja">Japanese</label>
|
||||||
|
<input type="text" id="title_ja" value="{{song.title_lang.ja}}" name="title_ja">
|
||||||
|
<label for="title_en">English</label>
|
||||||
|
<input type="text" id="title_en" value="{{song.title_lang.en}}" name="title_en">
|
||||||
|
<label for="title_cn">Chinese (Simplified)</label>
|
||||||
|
<input type="text" id="title_cn" value="{{song.title_lang.cn}}" name="title_cn">
|
||||||
|
<label for="title_tw">Chinese (Traditional)</label>
|
||||||
|
<input type="text" id="title_tw" value="{{song.title_lang.tw}}" name="title_tw">
|
||||||
|
<label for="title_ko">Korean</label>
|
||||||
|
<input type="text" id="title_ko" value="{{song.title_lang.ko}}" name="title_ko">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-field">
|
||||||
|
<p>Subtitle</p>
|
||||||
|
<label for="subtitle">Original</label>
|
||||||
|
<input type="text" id="subtitle" value="{{song.subtitle}}" name="subtitle">
|
||||||
|
<label for="subtitle_ja">Japanese</label>
|
||||||
|
<input type="text" id="subtitle_ja" value="{{song.subtitle_lang.ja}}" name="subtitle_ja">
|
||||||
|
<label for="subtitle_en">English</label>
|
||||||
|
<input type="text" id="subtitle_en" value="{{song.subtitle_lang.en}}" name="subtitle_en">
|
||||||
|
<label for="subtitle_cn">Chinese (Simplified)</label>
|
||||||
|
<input type="text" id="subtitle_cn" value="{{song.subtitle_lang.cn}}" name="subtitle_cn">
|
||||||
|
<label for="subtitle_tw">Chinese (Traditional)</label>
|
||||||
|
<input type="text" id="subtitle_tw" value="{{song.subtitle_lang.tw}}" name="subtitle_tw">
|
||||||
|
<label for="subtitle_ko">Korean</label>
|
||||||
|
<input type="text" id="subtitle_ko" value="{{song.subtitle_lang.ko}}" name="subtitle_ko">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-field">
|
||||||
|
<p>Courses</p>
|
||||||
|
<label for="course_easy">Easy</label>
|
||||||
|
<input type="number" id="course_easy" value="{{song.courses.easy.stars}}" name="course_easy" min="1" max="10">
|
||||||
|
<span class="checkbox"><input type="checkbox" name="branch_easy" id="branch_easy"{% if song.courses.easy.branch %} checked{% endif %}><label for="branch_easy"> Diverge Notes</label></span>
|
||||||
|
<label for="course_normal">Normal</label>
|
||||||
|
<input type="number" id="course_normal" value="{{song.courses.normal.stars}}" name="course_normal" min="1" max="10">
|
||||||
|
<span class="checkbox"><input type="checkbox" name="branch_normal" id="branch_normal"{% if song.courses.normal.branch %} checked{% endif %}><label for="branch_normal"> Diverge Notes</label></span>
|
||||||
|
<label for="course_hard">Hard</label>
|
||||||
|
<input type="number" id="course_hard" value="{{song.courses.hard.stars}}" name="course_hard" min="1" max="10">
|
||||||
|
<span class="checkbox"><input type="checkbox" name="branch_hard" id="branch_hard"{% if song.courses.hard.branch %} checked{% endif %}><label for="branch_hard"> Diverge Notes</label></span>
|
||||||
|
<label for="course_oni">Oni</label>
|
||||||
|
<input type="number" id="course_oni" value="{{song.courses.oni.stars}}" name="course_oni" min="1" max="10">
|
||||||
|
<span class="checkbox"><input type="checkbox" name="branch_oni" id="branch_oni"{% if song.courses.oni.branch %} checked{% endif %}><label for="branch_oni"> Diverge Notes</label></span>
|
||||||
|
<label for="course_ura">Ura</label>
|
||||||
|
<input type="number" id="course_ura" value="{{song.courses.ura.stars}}" name="course_ura" min="1" max="10">
|
||||||
|
<span class="checkbox"><input type="checkbox" name="branch_ura" id="branch_ura"{% if song.courses.ura.branch %} checked{% endif %}><label for="branch_ura"> Diverge Notes</label></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-field">
|
||||||
|
<p><label for="category_id">Category</label></p>
|
||||||
|
<select name="category_id" id="category_id">
|
||||||
|
{% for category in categories %}
|
||||||
|
<option value="{{ category.id }}"{% if song.category_id == category.id %} selected{% endif %}>{{ category.title }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-field">
|
||||||
|
<p><label for="type">Type</label></p>
|
||||||
|
<select name="type" id="type">
|
||||||
|
<option value="tja"{% if song.type == 'tja' %} selected{% endif %}>TJA</option>
|
||||||
|
<option value="osu"{% if song.type == 'osu' %} selected{% endif %}>osu!taiko</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-field">
|
||||||
|
<p><label for="offset">Offset</label></p>
|
||||||
|
<input type="text" id="offset" value="{{song.offset}}" name="offset">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit">Save</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
11
templates/admin_songs.html
Normal file
11
templates/admin_songs.html
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{% extends 'admin.html' %}
|
||||||
|
{% block content %}
|
||||||
|
<h1>Songs</h1>
|
||||||
|
{% for song in songs %}
|
||||||
|
<a href="/admin/songs/{{ song.id }}" class="song-link">
|
||||||
|
<div class="song">
|
||||||
|
<p>{{ song.title }}</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
{% endblock %}
|
105
tools/migrate_db.py
Normal file
105
tools/migrate_db.py
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# Migrate old SQLite taiko.db to MongoDB
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
from pymongo import MongoClient
|
||||||
|
|
||||||
|
client = MongoClient()
|
||||||
|
#client.drop_database('taiko')
|
||||||
|
db = client.taiko
|
||||||
|
sqdb = sqlite3.connect('taiko.db')
|
||||||
|
sqdb.row_factory = sqlite3.Row
|
||||||
|
curs = sqdb.cursor()
|
||||||
|
|
||||||
|
def migrate_songs():
|
||||||
|
curs.execute('select * from songs')
|
||||||
|
rows = curs.fetchall()
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
song = {
|
||||||
|
'id': row['id'],
|
||||||
|
'title': row['title'],
|
||||||
|
'title_lang': {'ja': row['title']},
|
||||||
|
'subtitle': row['subtitle'],
|
||||||
|
'subtitle_lang': {'ja': row['subtitle']},
|
||||||
|
'courses': {'easy': None, 'normal': None, 'hard': None, 'oni': None, 'ura': None},
|
||||||
|
'enabled': True if row['enabled'] else False,
|
||||||
|
'category_id': row['category'],
|
||||||
|
'type': row['type'],
|
||||||
|
'offset': row['offset'],
|
||||||
|
'skin_id': row['skin_id'],
|
||||||
|
'preview': row['preview'],
|
||||||
|
'volume': row['volume'],
|
||||||
|
'maker_id': row['maker_id'],
|
||||||
|
'hash': row['hash'],
|
||||||
|
'order': row['id']
|
||||||
|
}
|
||||||
|
|
||||||
|
for diff in ['easy', 'normal', 'hard', 'oni', 'ura']:
|
||||||
|
if row[diff]:
|
||||||
|
spl = row[diff].split(' ')
|
||||||
|
branch = False
|
||||||
|
if len(spl) > 1 and spl[1] == 'B':
|
||||||
|
branch = True
|
||||||
|
|
||||||
|
song['courses'][diff] = {'stars': int(spl[0]), 'branch': branch}
|
||||||
|
|
||||||
|
if row['title_lang']:
|
||||||
|
langs = row['title_lang'].splitlines()
|
||||||
|
for lang in langs:
|
||||||
|
spl = lang.split(' ', 1)
|
||||||
|
if spl[0] in ['ja', 'en', 'cn', 'tw', 'ko']:
|
||||||
|
song['title_lang'][spl[0]] = spl[1]
|
||||||
|
else:
|
||||||
|
song['title_lang']['en'] = lang
|
||||||
|
|
||||||
|
if row['subtitle_lang']:
|
||||||
|
langs = row['subtitle_lang'].splitlines()
|
||||||
|
for lang in langs:
|
||||||
|
spl = lang.split(' ', 1)
|
||||||
|
if spl[0] in ['ja', 'en', 'cn', 'tw', 'ko']:
|
||||||
|
song['subtitle_lang'][spl[0]] = spl[1]
|
||||||
|
else:
|
||||||
|
song['subtitle_lang']['en'] = lang
|
||||||
|
|
||||||
|
db.songs.insert_one(song)
|
||||||
|
|
||||||
|
def migrate_makers():
|
||||||
|
curs.execute('select * from makers')
|
||||||
|
rows = curs.fetchall()
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
db.makers.insert_one({
|
||||||
|
'id': row['maker_id'],
|
||||||
|
'name': row['name'],
|
||||||
|
'url': row['url']
|
||||||
|
})
|
||||||
|
|
||||||
|
def migrate_categories():
|
||||||
|
curs.execute('select * from categories')
|
||||||
|
rows = curs.fetchall()
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
db.categories.insert_one({
|
||||||
|
'id': row['id'],
|
||||||
|
'title': row['title']
|
||||||
|
})
|
||||||
|
|
||||||
|
def migrate_song_skins():
|
||||||
|
curs.execute('select * from song_skins')
|
||||||
|
rows = curs.fetchall()
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
db.song_skins.insert_one({
|
||||||
|
'id': row['id'],
|
||||||
|
'name': row['name'],
|
||||||
|
'song': row['song'],
|
||||||
|
'stage': row['stage'],
|
||||||
|
'don': row['don']
|
||||||
|
})
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
migrate_songs()
|
||||||
|
migrate_makers()
|
||||||
|
migrate_categories()
|
||||||
|
migrate_song_skins()
|
Loading…
Reference in New Issue
Block a user