mirror of
https://gitlab.com/SIGBUS/nyaa.git
synced 2024-12-22 14:30:01 +00:00
commit
d7309884fe
|
@ -6,13 +6,39 @@ from nyaa import models, forms
|
|||
from nyaa import bencode, backend, utils
|
||||
from nyaa import torrents
|
||||
|
||||
import functools
|
||||
import json
|
||||
import os.path
|
||||
#from orderedset import OrderedSet
|
||||
#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):
|
||||
auth_info = None
|
||||
|
@ -90,3 +116,68 @@ def api_upload(upload_request, user):
|
|||
return_error_messages.extend(error_messages)
|
||||
|
||||
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
|
||||
|
|
|
@ -343,6 +343,16 @@ class User(db.Model):
|
|||
def __repr__(self):
|
||||
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
|
||||
def by_id(cls, id):
|
||||
return cls.query.get(id)
|
||||
|
@ -357,6 +367,10 @@ class User(db.Model):
|
|||
user = cls.query.filter_by(email=email).first()
|
||||
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
|
||||
def is_admin(self):
|
||||
return self.level is UserLevelType.ADMIN or self.level is UserLevelType.SUPERADMIN
|
||||
|
|
|
@ -110,6 +110,9 @@ def get_utc_timestamp(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')
|
||||
|
||||
# Routes start here #
|
||||
|
||||
app.register_blueprint(api_handler.api_blueprint, url_prefix='/api')
|
||||
|
||||
@app.route('/rss', defaults={'rss': True})
|
||||
@app.route('/', defaults={'rss': False})
|
||||
|
|
179
utils/api_uploader_v2.py
Executable file
179
utils/api_uploader_v2.py
Executable 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'])
|
Loading…
Reference in a new issue