implement song addition/deletion

This commit is contained in:
Bui 2020-03-16 23:30:44 +00:00
parent c63b5eba4b
commit 5a68978ec4
9 changed files with 305 additions and 65 deletions

3
.gitignore vendored
View File

@ -49,6 +49,5 @@ public/api
taiko.db taiko.db
version.json version.json
public/index.html public/index.html
config.json config.py
public/assets/song_skins public/assets/song_skins
secret.txt

142
app.py
View File

@ -1,6 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import bcrypt import bcrypt
import config
import json import json
import re import re
import schema import schema
@ -14,27 +15,18 @@ from ffmpy import FFmpeg
from pymongo import MongoClient from pymongo import MongoClient
app = Flask(__name__) app = Flask(__name__)
client = MongoClient() client = MongoClient(host=config.MONGO['host'])
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()
app.secret_key = config.SECRET_KEY
app.config['SESSION_TYPE'] = 'redis' app.config['SESSION_TYPE'] = 'redis'
app.config['SESSION_COOKIE_HTTPONLY'] = False app.config['SESSION_COOKIE_HTTPONLY'] = False
app.cache = Cache(app, config={'CACHE_TYPE': 'redis'}) app.cache = Cache(app, config=config.REDIS)
sess = Session() sess = Session()
sess.init_app(app) sess.init_app(app)
db = client.taiko db = client[config.MONGO['database']]
db.users.create_index('username', unique=True) db.users.create_index('username', unique=True)
db.songs.create_index('id', unique=True)
DEFAULT_URL = 'https://github.com/bui/taiko-web/'
def api_error(message): def api_error(message):
return jsonify({'status': 'error', 'message': message}) return jsonify({'status': 'error', 'message': message})
@ -49,17 +41,19 @@ def login_required(f):
return decorated_function return decorated_function
def admin_required(f): def admin_required(level):
@wraps(f) def decorated_function(f):
def decorated_function(*args, **kwargs): @wraps(f)
if not session.get('username'): def wrapper(*args, **kwargs):
return abort(403) if not session.get('username'):
return abort(403)
user = db.users.find_one({'username': session.get('username')}) user = db.users.find_one({'username': session.get('username')})
if user['user_level'] < 50: if user['user_level'] < level:
return abort(403) return abort(403)
return f(*args, **kwargs) return f(*args, **kwargs)
return wrapper
return decorated_function return decorated_function
@ -71,27 +65,24 @@ def before_request_func():
def get_config(): def get_config():
if os.path.isfile('config.json'): config_out = {
try: 'songs_baseurl': config.SONGS_BASEURL,
config = json.load(open('config.json', 'r')) 'assets_baseurl': config.ASSETS_BASEURL,
except ValueError: 'email': config.EMAIL,
print('WARNING: Invalid config.json, using default values') 'accounts': config.ACCOUNTS
config = {} }
else:
print('WARNING: No config.json found, using default values')
config = {}
if not config.get('songs_baseurl'): if not config_out.get('songs_baseurl'):
config['songs_baseurl'] = ''.join([request.host_url, 'songs']) + '/' config_out['songs_baseurl'] = ''.join([request.host_url, 'songs']) + '/'
if not config.get('assets_baseurl'): if not config_out.get('assets_baseurl'):
config['assets_baseurl'] = ''.join([request.host_url, 'assets']) + '/' config_out['assets_baseurl'] = ''.join([request.host_url, 'assets']) + '/'
config['_version'] = get_version() config_out['_version'] = get_version()
return config return config_out
def get_version(): 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'): if os.path.isfile('version.json'):
try: try:
ver = json.load(open('version.json', 'r')) ver = json.load(open('version.json', 'r'))
@ -114,20 +105,21 @@ def route_index():
@app.route('/admin') @app.route('/admin')
@admin_required @admin_required(level=50)
def route_admin(): def route_admin():
return redirect('/admin/songs') return redirect('/admin/songs')
@app.route('/admin/songs') @app.route('/admin/songs')
@admin_required @admin_required(level=50)
def route_admin_songs(): def route_admin_songs():
songs = db.songs.find({}) 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/<int:id>') @app.route('/admin/songs/<int:id>')
@admin_required @admin_required(level=50)
def route_admin_songs_id(id): def route_admin_songs_id(id):
song = db.songs.find_one({'id': id}) song = db.songs.find_one({'id': id})
if not song: 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) 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/<int:id>', methods=['POST']) @app.route('/admin/songs/<int:id>', methods=['POST'])
@admin_required @admin_required(level=100)
def route_admin_songs_id_post(id): def route_admin_songs_id_post(id):
song = db.songs.find_one({'id': id}) song = db.songs.find_one({'id': id})
if not song: if not song:
@ -183,6 +225,18 @@ def route_admin_songs_id_post(id):
return redirect('/admin/songs/%s' % id) return redirect('/admin/songs/%s' % id)
@app.route('/admin/songs/<int:id>/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.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():

View File

@ -1,6 +0,0 @@
{
"songs_baseurl": "",
"assets_baseurl": "",
"email": "",
"accounts": true
}

32
config.example.py Normal file
View File

@ -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/'

View File

@ -130,3 +130,23 @@ h1 small {
margin-bottom: 10px; margin-bottom: 10px;
color: white; 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;
}

View File

@ -46,19 +46,19 @@
<div class="form-field"> <div class="form-field">
<p>Courses</p> <p>Courses</p>
<label for="course_easy">Easy</label> <label for="course_easy">Easy</label>
<input type="number" id="course_easy" value="{{song.courses.easy.stars}}" name="course_easy" min="1" max="10"> <input type="number" id="course_easy" value="{{song.courses.easy.stars}}" name="course_easy" min="0" 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> <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> <label for="course_normal">Normal</label>
<input type="number" id="course_normal" value="{{song.courses.normal.stars}}" name="course_normal" min="1" max="10"> <input type="number" id="course_normal" value="{{song.courses.normal.stars}}" name="course_normal" min="0" 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> <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> <label for="course_hard">Hard</label>
<input type="number" id="course_hard" value="{{song.courses.hard.stars}}" name="course_hard" min="1" max="10"> <input type="number" id="course_hard" value="{{song.courses.hard.stars}}" name="course_hard" min="0" 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> <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> <label for="course_oni">Oni</label>
<input type="number" id="course_oni" value="{{song.courses.oni.stars}}" name="course_oni" min="1" max="10"> <input type="number" id="course_oni" value="{{song.courses.oni.stars}}" name="course_oni" min="0" 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> <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> <label for="course_ura">Ura</label>
<input type="number" id="course_ura" value="{{song.courses.ura.stars}}" name="course_ura" min="1" max="10"> <input type="number" id="course_ura" value="{{song.courses.ura.stars}}" name="course_ura" min="0" 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> <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>
@ -115,7 +115,12 @@
</select> </select>
</div> </div>
<button type="submit">Save</button> <button type="submit" class="save-song">Save</button>
</form> </form>
{% if admin.user_level >= 100 %}
<form class="delete-song" method="post" action="/admin/songs/{{song.id}}/delete" onsubmit="return confirm('Are you sure you wish to delete this song?');">
<button type="submit">Delete song</button>
</form>
{% endif %}
</div> </div>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,121 @@
{% extends 'admin.html' %}
{% block content %}
<h1>New song</h1>
{% for message in get_flashed_messages() %}
<div class="message">{{ message }}</div>
{% endfor %}
<div class="song-form">
<form method="post">
<div class="form-field">
<span class="checkbox"><input type="checkbox" name="enabled" id="enabled" checked><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="" name="title" required>
<label for="title_ja">Japanese</label>
<input type="text" id="title_ja" value="" name="title_ja">
<label for="title_en">English</label>
<input type="text" id="title_en" value="" name="title_en">
<label for="title_cn">Chinese (Simplified)</label>
<input type="text" id="title_cn" value="" name="title_cn">
<label for="title_tw">Chinese (Traditional)</label>
<input type="text" id="title_tw" value="" name="title_tw">
<label for="title_ko">Korean</label>
<input type="text" id="title_ko" value="" name="title_ko">
</div>
<div class="form-field">
<p>Subtitle</p>
<label for="subtitle">Original</label>
<input type="text" id="subtitle" value="" name="subtitle">
<label for="subtitle_ja">Japanese</label>
<input type="text" id="subtitle_ja" value="" name="subtitle_ja">
<label for="subtitle_en">English</label>
<input type="text" id="subtitle_en" value="" name="subtitle_en">
<label for="subtitle_cn">Chinese (Simplified)</label>
<input type="text" id="subtitle_cn" value="" name="subtitle_cn">
<label for="subtitle_tw">Chinese (Traditional)</label>
<input type="text" id="subtitle_tw" value="" name="subtitle_tw">
<label for="subtitle_ko">Korean</label>
<input type="text" id="subtitle_ko" value="" name="subtitle_ko">
</div>
<div class="form-field">
<p>Courses</p>
<label for="course_easy">Easy</label>
<input type="number" id="course_easy" value="" name="course_easy" min="0" max="10">
<span class="checkbox"><input type="checkbox" name="branch_easy" id="branch_easy"><label for="branch_easy"> Diverge Notes</label></span>
<label for="course_normal">Normal</label>
<input type="number" id="course_normal" value="" name="course_normal" min="0" max="10">
<span class="checkbox"><input type="checkbox" name="branch_normal" id="branch_normal"><label for="branch_normal"> Diverge Notes</label></span>
<label for="course_hard">Hard</label>
<input type="number" id="course_hard" value="" name="course_hard" min="0" max="10">
<span class="checkbox"><input type="checkbox" name="branch_hard" id="branch_hard"><label for="branch_hard"> Diverge Notes</label></span>
<label for="course_oni">Oni</label>
<input type="number" id="course_oni" value="" name="course_oni" min="0" max="10">
<span class="checkbox"><input type="checkbox" name="branch_oni" id="branch_oni"><label for="branch_oni"> Diverge Notes</label></span>
<label for="course_ura">Ura</label>
<input type="number" id="course_ura" value="" name="course_ura" min="0" max="10">
<span class="checkbox"><input type="checkbox" name="branch_ura" id="branch_ura"><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">
<option value="0">(none)</option>
{% for category in categories %}
<option value="{{ category.id }}">{{ 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">TJA</option>
<option value="osu">osu!taiko</option>
</select>
</div>
<div class="form-field">
<p><label for="offset">Offset</label></p>
<input type="text" id="offset" value="" name="offset" required>
</div>
<div class="form-field">
<p><label for="skin_id">Skin</label></p>
<select name="skin_id" id="skin_id">
<option value="0">(none)</option>
{% for skin in song_skins %}
<option value="{{ skin.id }}">{{ skin.name }}</option>
{% endfor %}
</select>
</div>
<div class="form-field">
<p><label for="preview">Preview</label></p>
<input type="text" id="preview" value="" name="preview" required>
</div>
<div class="form-field">
<p><label for="volume">Volume</label></p>
<input type="text" id="volume" value="" name="volume" required>
</div>
<div class="form-field">
<p><label for="maker_id">Maker</label></p>
<select name="maker_id" id="maker_id">
<option value="0">(none)</option>
{% for maker in makers %}
<option value="{{ maker.id }}">{{ maker.name }}</option>
{% endfor %}
</select>
</div>
<button type="submit" class="save-song">Save</button>
</form>
</div>
{% endblock %}

View File

@ -1,6 +1,12 @@
{% extends 'admin.html' %} {% extends 'admin.html' %}
{% block content %} {% block content %}
{% if admin.user_level >= 100 %}
<a href="/admin/songs/new" class="side-button">New song</a>
{% endif %}
<h1>Songs</h1> <h1>Songs</h1>
{% for message in get_flashed_messages() %}
<div class="message">{{ message }}</div>
{% endfor %}
{% for song in songs %} {% for song in songs %}
<a href="/admin/songs/{{ song.id }}" class="song-link"> <a href="/admin/songs/{{ song.id }}" class="song-link">
<div class="song"> <div class="song">

View File

@ -4,24 +4,30 @@
import sqlite3 import sqlite3
from pymongo import MongoClient from pymongo import MongoClient
client = MongoClient() import os,sys,inspect
client.drop_database('taiko') current_dir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
db = client.taiko 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 = sqlite3.connect('taiko.db')
sqdb.row_factory = sqlite3.Row sqdb.row_factory = sqlite3.Row
curs = sqdb.cursor() curs = sqdb.cursor()
def migrate_songs(): def migrate_songs():
curs.execute('select * from songs') curs.execute('select * from songs order by id')
rows = curs.fetchall() rows = curs.fetchall()
for row in rows: for row in rows:
song = { song = {
'id': row['id'], 'id': row['id'],
'title': row['title'], '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': 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}, 'courses': {'easy': None, 'normal': None, 'hard': None, 'oni': None, 'ura': None},
'enabled': True if row['enabled'] else False, 'enabled': True if row['enabled'] else False,
'category_id': row['category'], 'category_id': row['category'],
@ -63,6 +69,9 @@ def migrate_songs():
song['subtitle_lang']['en'] = lang song['subtitle_lang']['en'] = lang
db.songs.insert_one(song) db.songs.insert_one(song)
last_song = song['id']
db.seq.insert_one({'name': 'songs', 'value': last_song})
def migrate_makers(): def migrate_makers():
curs.execute('select * from makers') curs.execute('select * from makers')