From 570a06bd9e0a08f3265b7ac5f44853f2e6b76a6f Mon Sep 17 00:00:00 2001 From: kyamiko Date: Sat, 3 Jun 2017 20:57:53 +0000 Subject: [PATCH] API Info (#157) Squashing 11 commits into one. --- .../3001f79b7722_add_torrents.uploader_ip.py | 3 +- nyaa/api_handler.py | 75 ++++++++++++++ utils/api_info.py | 97 +++++++++++++++++++ 3 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 utils/api_info.py diff --git a/migrations/versions/3001f79b7722_add_torrents.uploader_ip.py b/migrations/versions/3001f79b7722_add_torrents.uploader_ip.py index 03ed87c..52fe0ba 100644 --- a/migrations/versions/3001f79b7722_add_torrents.uploader_ip.py +++ b/migrations/versions/3001f79b7722_add_torrents.uploader_ip.py @@ -1,7 +1,7 @@ """Add uploader_ip column to torrents table. Revision ID: 3001f79b7722 -Revises: +Revises: Create Date: 2017-05-21 18:01:35.472717 """ @@ -19,6 +19,7 @@ TABLE_PREFIXES = ('nyaa', 'sukebei') def upgrade(): + for prefix in TABLE_PREFIXES: op.add_column(prefix + '_torrents', sa.Column('uploader_ip', sa.Binary(), nullable=True)) # ### end Alembic commands ### diff --git a/nyaa/api_handler.py b/nyaa/api_handler.py index 65049b7..93bd13d 100644 --- a/nyaa/api_handler.py +++ b/nyaa/api_handler.py @@ -12,6 +12,9 @@ from nyaa import routes import functools import json import os.path +import re + +import binascii api_blueprint = flask.Blueprint('api', __name__) @@ -254,3 +257,75 @@ def ghetto_import(): db.session.commit() return 'success' + + +# ####################################### INFO ####################################### +ID_PATTERN = '^[1-9][0-9]*$' +INFO_HASH_PATTERN = '^[0-9a-fA-F]{40}$' # INFO_HASH as string + + +@api_blueprint.route('/info/', methods=['GET']) +@basic_auth_user +@api_require_user +def v2_api_info(torrent_id_or_hash): + torrent_id_or_hash = torrent_id_or_hash.lower().strip() + + matchID = re.match(ID_PATTERN, torrent_id_or_hash) + matchHASH = re.match(INFO_HASH_PATTERN, torrent_id_or_hash) + + torrent = None + + if matchID: + torrent = models.Torrent.by_id(int(torrent_id_or_hash)) + elif matchHASH: + # Convert the string representation of a torrent hash back into a binary representation + a2b_hash = binascii.unhexlify(torrent_id_or_hash) + torrent = models.Torrent.by_info_hash(a2b_hash) + else: + return flask.jsonify({'errors': ['Query was not a valid id or hash.']}), 400 + + viewer = flask.g.user + + if not torrent: + return flask.jsonify({'errors': ['Query was not a valid id or hash.']}), 400 + + # Only allow admins see deleted torrents + if torrent.deleted and not (viewer and viewer.is_superadmin): + return flask.jsonify({'errors': ['Query was not a valid id or hash.']}), 400 + + submitter = None + if not torrent.anonymous and torrent.user: + submitter = torrent.user.username + if torrent.user and (viewer == torrent.user or viewer.is_moderator): + submitter = torrent.user.username + + files = {} + if torrent.filelist: + files = json.loads(torrent.filelist.filelist_blob.decode('utf-8')) + + # Create a response dict with relevant data + torrent_metadata = { + 'submitter': submitter, + 'url': flask.url_for('view_torrent', torrent_id=torrent.id, _external=True), + 'id': torrent.id, + 'name': torrent.display_name, + 'creation_date': torrent.created_time.strftime('%Y-%m-%d %H:%M UTC'), + 'hash_b32': torrent.info_hash_as_b32, # as used in magnet uri + 'hash_hex': torrent.info_hash_as_hex, # .hex(), #as shown in torrent client + 'magnet': torrent.magnet_uri, + 'main_category': torrent.main_category.name, + 'main_category_id': torrent.main_category.id, + 'sub_category': torrent.sub_category.name, + 'sub_category_id': torrent.sub_category.id, + 'information': torrent.information, + 'description': torrent.description, + 'stats': {'seeders': torrent.stats.seed_count, 'leechers': torrent.stats.leech_count, 'downloads': torrent.stats.download_count}, + 'filesize': torrent.filesize, + 'files': files, + # reduce torrent flags to True/False + 'is_trusted': torrent.trusted, + 'is_complete': torrent.complete, + 'is_remake': torrent.remake + } + + return flask.jsonify(torrent_metadata), 200 diff --git a/utils/api_info.py b/utils/api_info.py new file mode 100644 index 0000000..f37d5d3 --- /dev/null +++ b/utils/api_info.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 + +import re +import os +import argparse +import requests +import json +from pprint import pprint + +NYAA_HOST = 'https://nyaa.si' +SUKEBEI_HOST = 'https://sukebei.nyaa.si' + +API_BASE = '/api' +API_INFO = API_BASE + '/info' + +ID_PATTERN = '^[1-9][0-9]*$' +INFO_HASH_PATTERN = '^[0-9a-fA-F]{40}$' + +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='Query torrent info on Nyaa.si', epilog=environment_epillog) + +conn_group = parser.add_argument_group('Connection options') + +conn_group.add_argument('-s', '--sukebei', default=False, + action='store_true', help='Query torrent info on 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)') + +parser.add_argument('hash_or_id', help='Torrent by id or hash Required.') + +parser.add_argument('--raw', default=False, action='store_true', + help='Print only raw response (JSON)') + + +def easy_file_size(filesize): + for prefix in ['B', 'KiB', 'MiB', 'GiB', 'TiB']: + if filesize < 1024.0: + return '{0:.1f} {1}'.format(filesize, prefix) + filesize = filesize / 1024.0 + return '{0:.1f} {1}'.format(filesize, prefix) + + +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_query = args.hash_or_id.lower().strip() + + # Verify query is either a valid id or valid hash + matchID = re.match(ID_PATTERN, api_query) + matchHASH = re.match(INFO_HASH_PATTERN, api_query) + + if not (matchID or matchHASH): + raise Exception('Query was not a valid id or valid hash.') + + api_info_url = api_host + API_INFO + '/' + api_query + + 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) + + # Go! + r = requests.get(api_info_url, auth=auth) + + if args.raw: + print(r.text) + else: + try: + rj = r.json() + except ValueError: + print('Bad response:') + print(r.text) + exit(1) + + errors = rj.get('errors') + + if errors: + print('Info request failed:', errors) + exit(1) + else: + rj['filesize'] = easy_file_size(rj['filesize']) + rj['is_trusted'] = 'Yes' if rj['is_trusted'] else 'No' + rj['is_complete'] = 'Yes' if rj['is_complete'] else 'No' + rj['is_remake'] = 'Yes' if rj['is_remake'] else 'No' + print("Torrent #{} '{}' uploaded by '{}' ({}) (Created on: {}) ({} - {}) (Trusted: {}, Complete: {}, Remake: {})\n{}".format( + rj['id'], rj['name'], rj['submitter'], rj['filesize'], rj['creation_date'], rj['main_category'], rj['sub_category'], rj['is_trusted'], rj['is_complete'], rj['is_remake'], rj['magnet']))