Upload API V2

This commit is contained in:
TheAMM 2017-05-18 15:30:03 +03:00
parent 6bfb65172c
commit e5fce168a0
3 changed files with 274 additions and 1 deletions

View File

@ -6,13 +6,39 @@ from nyaa import models, forms
from nyaa import bencode, backend, utils from nyaa import bencode, backend, utils
from nyaa import torrents from nyaa import torrents
import functools
import json import json
import os.path import os.path
#from orderedset import OrderedSet #from orderedset import OrderedSet
#from werkzeug import secure_filename #from werkzeug import secure_filename
# #################################### API HELPERS #################################### api_blueprint = flask.Blueprint('api', __name__)
# #################################### API HELPERS ####################################
def basic_auth_user(f):
''' A decorator that will try to validate the user into g.user from basic auth.
Note: this does not set user to None on failure, so users can also authorize
themselves with the cookie (handled in routes.before_request). '''
@functools.wraps(f)
def decorator(*args, **kwargs):
auth = flask.request.authorization
if auth:
user = models.User.by_username_or_email(auth.get('username'))
if user and user.validate_authorization(auth.get('password')):
flask.g.user = user
return f(*args, **kwargs)
return decorator
def api_require_user(f):
''' Returns an error message if flask.g.user is None.
Remember to put after basic_auth_user. '''
@functools.wraps(f)
def decorator(*args, **kwargs):
if flask.g.user is None:
return flask.jsonify({'errors':['Bad authorization']}), 403
return f(*args, **kwargs)
return decorator
def validate_user(upload_request): def validate_user(upload_request):
auth_info = None auth_info = None
@ -90,3 +116,68 @@ def api_upload(upload_request, user):
return_error_messages.extend(error_messages) return_error_messages.extend(error_messages)
return flask.make_response(flask.jsonify({'Failure': return_error_messages}), 400) return flask.make_response(flask.jsonify({'Failure': return_error_messages}), 400)
# V2 below
# Map UploadForm fields to API keys
UPLOAD_API_FORM_KEYMAP = {
'torrent_file' : 'torrent',
'display_name' : 'name',
'is_anonymous' : 'anonymous',
'is_hidden' : 'hidden',
'is_complete' : 'complete',
'is_remake' : 'remake'
}
UPLOAD_API_FORM_KEYMAP_REVERSE = {v:k for k,v in UPLOAD_API_FORM_KEYMAP.items()}
UPLOAD_API_KEYS = [
'name',
'category',
'anonymous',
'hidden',
'complete',
'remake',
'information',
'description'
]
@api_blueprint.route('/v2/upload', methods=['POST'])
@basic_auth_user
@api_require_user
def v2_api_upload():
mapped_dict = {
'torrent_file' : flask.request.files.get('torrent')
}
request_data_field = flask.request.form.get('torrent_data')
if request_data_field is None:
return flask.jsonify({'errors' : ['missing torrent_data field']}), 400
request_data = json.loads(request_data_field)
# Map api keys to upload form fields
for key in UPLOAD_API_KEYS:
mapped_key = UPLOAD_API_FORM_KEYMAP_REVERSE.get(key, key)
mapped_dict[mapped_key] = request_data.get(key)
# Flask-WTF (very helpfully!!) automatically grabs the request form, so force a None formdata
upload_form = forms.UploadForm(None, data=mapped_dict)
upload_form.category.choices = _create_upload_category_choices()
if upload_form.validate():
torrent = backend.handle_torrent_upload(upload_form, flask.g.user)
# Create a response dict with relevant data
torrent_metadata = {
'url' : flask.url_for('view_torrent', torrent_id=torrent.id, _external=True),
'id' : torrent.id,
'name' : torrent.display_name,
'hash' : torrent.info_hash.hex(),
'magnet' : torrent.magnet_uri
}
return flask.jsonify(torrent_metadata)
else:
# Map errors back from form fields into the api keys
mapped_errors = { UPLOAD_API_FORM_KEYMAP.get(k, k) : v for k,v in upload_form.errors.items() }
return flask.jsonify({'errors' : mapped_errors}), 400

View File

@ -110,6 +110,9 @@ def get_utc_timestamp(datetime_str):
def get_display_time(datetime_str): def get_display_time(datetime_str):
return datetime.strptime(datetime_str, '%Y-%m-%dT%H:%M:%S').strftime('%Y-%m-%d %H:%M') return datetime.strptime(datetime_str, '%Y-%m-%dT%H:%M:%S').strftime('%Y-%m-%d %H:%M')
# Routes start here #
app.register_blueprint(api_handler.api_blueprint, url_prefix='/api')
@app.route('/rss', defaults={'rss': True}) @app.route('/rss', defaults={'rss': True})
@app.route('/', defaults={'rss': False}) @app.route('/', defaults={'rss': False})

179
utils/api_uploader_v2.py Executable file
View File

@ -0,0 +1,179 @@
#!/usr/bin/env python3
import os
import requests
import json
import argparse
NYAA_HOST = 'https://nyaa.si'
SUKEBEI_HOST = 'https://sukebei.nyaa.si'
API_BASE = '/api'
API_UPLOAD = API_BASE + '/upload'
NYAA_CATS = '''1_1 - Anime - AMV
1_2 - Anime - English
1_3 - Anime - Non-English
1_4 - Anime - Raw
2_1 - Audio - Lossless
2_2 - Audio - Lossy
3_1 - Literature - English-translated
3_2 - Literature - Non-English
3_3 - Literature - Non-English-Translated
3_4 - Literature - Raw
4_1 - Live Action - English-translated
4_2 - Live Action - Idol/Promotional Video
4_3 - Live Action - Non-English-translated
4_4 - Live Action - Raw
5_1 - Pictures - Graphics
5_2 - Pictures - Photos
6_1 - Software - Applications
6_2 - Software - Games'''
SUKEBEI_CATS = '''1_1 - Art - Anime
1_2 - Art - Doujinshi
1_3 - Art - Games
1_4 - Art - Manga
1_5 - Art - Pictures
2_1 - Real Life - Photobooks / Pictures
2_2 - Real Life - Videos'''
class CategoryPrintAction(argparse.Action):
def __init__(self, option_strings, nargs='?', help=None, **kwargs):
super().__init__(option_strings=option_strings,
dest='site',
default=None,
nargs=nargs,
help=help)
def __call__(self, parser, namespace, values, option_string=None):
if values and values.lower() == 'sukebei':
print("Sukebei categories")
print(SUKEBEI_CATS)
else:
print("Nyaa categories")
print(NYAA_CATS)
parser.exit()
environment_epillog = '''You may also provide environment variables NYAA_API_HOST, NYAA_API_USERNAME and NYAA_API_PASSWORD for connection info.'''
parser = argparse.ArgumentParser(description='Upload torrents to Nyaa.si', epilog=environment_epillog)
parser.add_argument('--list-categories', default=False, action=CategoryPrintAction, nargs='?', help='List torrent categories. Include "sukebei" to show Sukebei categories')
conn_group = parser.add_argument_group('Connection options')
conn_group.add_argument('-s', '--sukebei', default=False, action='store_true', help='Upload to sukebei.nyaa.si')
conn_group.add_argument('-u', '--user', help='Username or email')
conn_group.add_argument('-p', '--password', help='Password')
conn_group.add_argument('--host', help='Select another api host (for debugging purposes)')
resp_group = parser.add_argument_group('Response options')
resp_group.add_argument('--raw', default=False, action='store_true', help='Print only raw response (JSON)')
resp_group.add_argument('-m', '--magnet', default=False, action='store_true', help='Print magnet uri')
tor_group = parser.add_argument_group('Torrent options')
tor_group.add_argument('-c', '--category', required=True, help='Torrent category (see ). Required.')
tor_group.add_argument('-n', '--name', help='Display name for the torrent (optional)')
tor_group.add_argument('-i', '--information', help='Information field (optional)')
tor_group.add_argument('-d', '--description', help='Description for the torrent (optional)')
tor_group.add_argument('-D', '--description-file', metavar='FILE', help='Read description from a file (optional)')
tor_group.add_argument('-A', '--anonymous', default=False, action='store_true', help='Upload torrent anonymously')
tor_group.add_argument('-H', '--hidden', default=False, action='store_true', help='Hide torrent from results')
tor_group.add_argument('-C', '--complete', default=False, action='store_true', help='Mark torrent as complete (eg. season batch)')
tor_group.add_argument('-R', '--remake', default=False, action='store_true', help='Mark torrent as remake (derivative work from another release)')
tor_group.add_argument('torrent', metavar='TORRENT_FILE', help='The .torrent file to upload')
def crude_torrent_check(file_object):
''' Does a simple check to weed out accidentally picking a wrong file '''
# Check if file seems to be a bencoded dictionary: starts with d and end with e
file_object.seek(0)
if file_object.read(1) != b'd':
return False
file_object.seek(-1, os.SEEK_END)
if file_object.read(1) != b'e':
return False
# Seek back to beginning
file_object.seek(0)
return True
if __name__ == "__main__":
args = parser.parse_args()
# Use debug host from args or environment, if set
debug_host = args.host or os.getenv('NYAA_API_HOST')
api_host = (debug_host or (args.sukebei and SUKEBEI_HOST or NYAA_HOST)).rstrip('/')
api_upload_url = api_host + API_UPLOAD
if args.description_file:
# Replace args.description with contents of the file
with open(args.description_file, 'r') as in_file:
args.description = in_file.read()
torrent_file = open(args.torrent, 'rb')
# Check if the file even seems like a torrent
if not crude_torrent_check(torrent_file):
raise Exception("File '{}' doesn't seem to be a torrent file".format(args.torrent))
api_username = args.user or os.getenv('NYAA_API_USERNAME')
api_password = args.password or os.getenv('NYAA_API_PASSWORD')
if not (api_username and api_password):
raise Exception('No authorization found from arguments or environment variables.')
auth = (api_username, api_password)
data = {
'name' : args.name,
'category' : args.category,
'information' : args.information,
'description' : args.description,
'anonymous' : args.anonymous,
'hidden' : args.hidden,
'complete' : args.complete,
'remake' : args.remake,
}
encoded_data = {
'torrent_data' : json.dumps(data)
}
files = {
'torrent' : torrent_file
}
# Go!
r = requests.post(api_upload_url, auth=auth, data=encoded_data, files=files)
torrent_file.close()
if args.raw:
print(r.text)
else:
try:
response = r.json()
except ValueError:
print('Bad response:')
print(r.text)
exit(1)
errors = response.get('errors')
if errors:
print('Upload failed', errors)
exit(1)
else:
print("[Uploaded] {url} - '{name}'".format(**response))
if args.magnet:
print(response['magnet'])