1
0
Fork 0
mirror of https://gitlab.com/SIGBUS/nyaa.git synced 2024-12-22 19:49:59 +00:00

Merge pull request #119 from nyaadevs/upload_api_v2

Upload API V2
This commit is contained in:
Anna-Maria Meriniemi 2017-05-18 22:26:38 +03:00 committed by GitHub
commit d7309884fe
4 changed files with 288 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

@ -343,6 +343,16 @@ class User(db.Model):
def __repr__(self): def __repr__(self):
return '<User %r>' % self.username return '<User %r>' % self.username
def validate_authorization(self, password):
''' Returns a boolean for whether the user can be logged in '''
checks = [
# Password must match
password == self.password_hash,
# Reject inactive and banned users
self.status == UserStatusType.ACTIVE
]
return all(checks)
@classmethod @classmethod
def by_id(cls, id): def by_id(cls, id):
return cls.query.get(id) return cls.query.get(id)
@ -357,6 +367,10 @@ class User(db.Model):
user = cls.query.filter_by(email=email).first() user = cls.query.filter_by(email=email).first()
return user return user
@classmethod
def by_username_or_email(cls, username_or_email):
return cls.by_username(username_or_email) or cls.by_email(username_or_email)
@property @property
def is_admin(self): def is_admin(self):
return self.level is UserLevelType.ADMIN or self.level is UserLevelType.SUPERADMIN return self.level is UserLevelType.ADMIN or self.level is UserLevelType.SUPERADMIN

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'])