Initial commit.
48
README.md
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
# NyaaV2
|
||||||
|
|
||||||
|
## Setup:
|
||||||
|
|
||||||
|
- Create your virtualenv, for example with `pyvenv venv`
|
||||||
|
- Enter your virtualenv with `source venv/bin/activate`
|
||||||
|
- Install dependencies with `pip install -r requirements.txt`
|
||||||
|
- Run `python db_create.py` to create the database
|
||||||
|
- Start the dev server with `python run.py`
|
||||||
|
|
||||||
|
## Updated Setup (python 3.6.1):
|
||||||
|
|
||||||
|
- Install dependencies https://github.com/pyenv/pyenv/wiki/Common-build-problems
|
||||||
|
- Install `pyenv` https://github.com/pyenv/pyenv/blob/master/README.md#installation
|
||||||
|
- Install `pyenv-virtualenv` https://github.com/pyenv/pyenv-virtualenv/blob/master/README.md
|
||||||
|
- `pyenv install 3.6.1`
|
||||||
|
- `pyenv virtualenv 3.6.1 nyaa`
|
||||||
|
- `pyenv activate nyaa`
|
||||||
|
- Install dependencies with `pip install -r requirements.txt`
|
||||||
|
- Copy `config.example.py` into `config.py`
|
||||||
|
- Change TALBE_PREFIX to `nyaa_` or `sukebei_` depending on the site
|
||||||
|
|
||||||
|
## Setting up MySQL/MariaDB database for advanced functionality
|
||||||
|
- Enable `USE_MYSQL` flag in config.py
|
||||||
|
- Install latest mariadb by following instructions here https://downloads.mariadb.org/mariadb/repositories/
|
||||||
|
- Tested versions: `mysql Ver 15.1 Distrib 10.0.30-MariaDB, for debian-linux-gnu (x86_64) using readline 5.2`
|
||||||
|
- Run the following commands logged in as your root db user:
|
||||||
|
- `CREATE USER 'test'@'localhost' IDENTIFIED BY 'test123';`
|
||||||
|
- `GRANT ALL PRIVILEGES ON * . * TO 'test'@'localhost';`
|
||||||
|
- `FLUSH PRIVILEGES;`
|
||||||
|
- `CREATE DATABASE nyaav2 DEFAULT CHARACTER SET utf8 COLLATE utf8_bin;`
|
||||||
|
- To setup and import nyaa_maria_vx.sql:
|
||||||
|
- `mysql -u <user> -p nyaav2`
|
||||||
|
- `DROP DATABASE nyaav2;`
|
||||||
|
- `CREATE DATABASE nyaav2 DEFAULT CHARACTER SET utf8 COLLATE utf8_bin;`
|
||||||
|
- `SOURCE ~/path/to/database/nyaa_maria_vx.sql`
|
||||||
|
|
||||||
|
## Finishing up
|
||||||
|
- Run `python db_create.py` to create the database
|
||||||
|
- Load the .sql file
|
||||||
|
- `mysql -u user -p nyaav2`
|
||||||
|
- `SOURCE cocks.sql`
|
||||||
|
- Remember to change the default user password to an empty string to disable logging in
|
||||||
|
- Start the dev server with `python run.py`
|
||||||
|
- Deactivate `source deactivate`
|
||||||
|
|
||||||
|
## Code Quality:
|
||||||
|
- Remember to follow PEP8 style guidelines and run `./lint.sh` before committing.
|
15
WSGI.py
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
#!/usr/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import gevent.monkey
|
||||||
|
gevent.monkey.patch_all()
|
||||||
|
|
||||||
|
from nyaa import app
|
||||||
|
|
||||||
|
if app.config["DEBUG"]:
|
||||||
|
from werkzeug.debug import DebuggedApplication
|
||||||
|
app.wsgi_app = DebuggedApplication(app.wsgi_app, True)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import gevent.pywsgi
|
||||||
|
gevent_server = gevent.pywsgi.WSGIServer(("localhost", 5000), app.wsgi_app)
|
||||||
|
gevent_server.serve_forever()
|
55
api_uploader.py
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
# api_uploader.py
|
||||||
|
|
||||||
|
|
||||||
|
# Uploads a single file
|
||||||
|
# I will create another script for batch uploading
|
||||||
|
|
||||||
|
import json
|
||||||
|
import requests
|
||||||
|
|
||||||
|
url = "http://127.0.0.1:5500/api/upload"
|
||||||
|
|
||||||
|
# Required for Auth
|
||||||
|
username = ""
|
||||||
|
password = ""
|
||||||
|
|
||||||
|
# Required
|
||||||
|
torrent_name = ""
|
||||||
|
|
||||||
|
# Required
|
||||||
|
main_cat = ""
|
||||||
|
# Required
|
||||||
|
sub_cat = ""
|
||||||
|
|
||||||
|
# May be blank
|
||||||
|
information = ""
|
||||||
|
# May be blank
|
||||||
|
description = ""
|
||||||
|
# flags = [Hidden, Remake, Complete, Anonymous]
|
||||||
|
# 0 for NOT SET / 1 for SET
|
||||||
|
# Required
|
||||||
|
flags = [0, 0, 0, 0]
|
||||||
|
|
||||||
|
metadata={
|
||||||
|
"username": username,
|
||||||
|
"password": password,
|
||||||
|
"display_name": torrent_name,
|
||||||
|
"main_cat": main_cat,
|
||||||
|
"sub_cat": sub_cat,
|
||||||
|
"information": information,
|
||||||
|
"description": description,
|
||||||
|
"flags": flags
|
||||||
|
}
|
||||||
|
|
||||||
|
# Required
|
||||||
|
file_name = ""
|
||||||
|
|
||||||
|
files = {
|
||||||
|
'json': (json.dumps(metadata)),
|
||||||
|
'torrent': ('{0}'.format(file_name), open(file_name, 'rb'), 'application/octet-stream')}
|
||||||
|
|
||||||
|
response = requests.post(url, files=files)
|
||||||
|
|
||||||
|
json_response = response.json()
|
||||||
|
|
||||||
|
print(json_response)
|
2
batch_upload_torrent.sh
Executable file
|
@ -0,0 +1,2 @@
|
||||||
|
up_t() { curl -F "category=1_2" -F "torrent_file=@$1" 'http://localhost:5500/upload'; }
|
||||||
|
for x in test_torrent_batch/*; do up_t "$x"; done
|
51
config.example.py
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
DEBUG = True
|
||||||
|
USE_RECAPTCHA = False
|
||||||
|
USE_EMAIL_VERIFICATION = False
|
||||||
|
USE_MYSQL = True
|
||||||
|
|
||||||
|
# Enable this once stat integration is done
|
||||||
|
ENABLE_SHOW_STATS = False
|
||||||
|
|
||||||
|
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
if USE_MYSQL:
|
||||||
|
SQLALCHEMY_DATABASE_URI = ('mysql://test:test123@localhost/nyaav2')
|
||||||
|
else:
|
||||||
|
SQLALCHEMY_DATABASE_URI = (
|
||||||
|
'sqlite:///' + os.path.join(BASE_DIR, 'test.db') + '?check_same_thread=False')
|
||||||
|
|
||||||
|
CSRF_SESSION_KEY = '***'
|
||||||
|
SECRET_KEY = '***'
|
||||||
|
|
||||||
|
# Prefix for running multiple sites, user table will not be prefixed.
|
||||||
|
# For sukebei, change 'nyaa_' to 'sukebei_'
|
||||||
|
TABLE_PREFIX = 'nyaa_'
|
||||||
|
|
||||||
|
# for recaptcha and email verification:
|
||||||
|
# keys for localhost. Change as appropriate when actual domain is registered.
|
||||||
|
RECAPTCHA_PUBLIC_KEY = '***'
|
||||||
|
RECAPTCHA_PRIVATE_KEY = '***'
|
||||||
|
SMTP_SERVER = '***'
|
||||||
|
SMTP_PORT = 587
|
||||||
|
MAIL_FROM_ADDRESS = '***'
|
||||||
|
SMTP_USERNAME = '***'
|
||||||
|
SMTP_PASSWORD = '***'
|
||||||
|
|
||||||
|
RESULTS_PER_PAGE = 75
|
||||||
|
|
||||||
|
# What the site identifies itself as.
|
||||||
|
SITE_NAME = 'Nyaa'
|
||||||
|
|
||||||
|
# The maximum number of files a torrent can contain
|
||||||
|
# until the site says "Too many files to display."
|
||||||
|
MAX_FILES_VIEW = 1000
|
||||||
|
|
||||||
|
# """
|
||||||
|
# Setting to make sure main announce url is present in torrent
|
||||||
|
# """
|
||||||
|
ENFORCE_MAIN_ANNOUNCE_URL = False
|
||||||
|
MAIN_ANNOUNCE_URL = ''
|
||||||
|
|
||||||
|
BACKUP_TORRENT_FOLDER = 'torrents'
|
40
db_create.py
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import sys
|
||||||
|
from nyaa import app, db, models
|
||||||
|
|
||||||
|
# Create tables
|
||||||
|
|
||||||
|
db.create_all()
|
||||||
|
|
||||||
|
# Insert categories
|
||||||
|
|
||||||
|
if app.config['TABLE_PREFIX'] == 'nyaa_':
|
||||||
|
CATEGORIES = [
|
||||||
|
('Anime', ['Anime Music Video', 'English-translated', 'Non-English-translated', 'Raw']),
|
||||||
|
('Audio', ['Lossless', 'Lossy']),
|
||||||
|
('Literature', ['English-translated', 'Non-English-translated', 'Raw']),
|
||||||
|
('Live Action', ['English-translated', 'Idol/Promotional Video', 'Non-English-translated', 'Raw']),
|
||||||
|
('Pictures', ['Graphics', 'Photos']),
|
||||||
|
('Software', ['Applications', 'Games']),
|
||||||
|
]
|
||||||
|
elif app.config['TABLE_PREFIX'] == 'sukebei_':
|
||||||
|
CATEGORIES = [
|
||||||
|
('Art', ['Anime', 'Doujinshi', 'Games', 'Manga', 'Pictures']),
|
||||||
|
('Real Life', ['Photobooks / Pictures', 'Videos']),
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
CATEGORIES = []
|
||||||
|
|
||||||
|
for main_cat_name, sub_cat_names in CATEGORIES:
|
||||||
|
main_cat = models.MainCategory(name=main_cat_name)
|
||||||
|
for i, sub_cat_name in enumerate(sub_cat_names):
|
||||||
|
# Composite keys can't autoincrement, set sub_cat id manually (1-index)
|
||||||
|
sub_cat = models.SubCategory(id=i+1, name=sub_cat_name, main_category=main_cat)
|
||||||
|
db.session.add(main_cat)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# Create fulltext index
|
||||||
|
|
||||||
|
if app.config['USE_MYSQL']:
|
||||||
|
db.engine.execute('ALTER TABLE ' + app.config['TABLE_PREFIX'] + 'torrents ADD FULLTEXT KEY (display_name)')
|
||||||
|
|
6
lint.sh
Executable file
|
@ -0,0 +1,6 @@
|
||||||
|
autopep8 nyaa/ \
|
||||||
|
--recursive \
|
||||||
|
--in-place \
|
||||||
|
--pep8-passes 2000 \
|
||||||
|
--max-line-length 100 \
|
||||||
|
--verbose
|
6
my.cnf
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
[mysqld]
|
||||||
|
innodb_ft_min_token_size=2
|
||||||
|
ft_min_word_len=2
|
||||||
|
innodb_ft_cache_size = 80000000
|
||||||
|
innodb_ft_total_cache_size = 1600000000
|
||||||
|
max_allowed_packet = 100M
|
53
nyaa/__init__.py
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
import flask
|
||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
from flask_assets import Environment, Bundle
|
||||||
|
from flask_debugtoolbar import DebugToolbarExtension
|
||||||
|
from nyaa import fix_paginate
|
||||||
|
|
||||||
|
|
||||||
|
app = flask.Flask(__name__)
|
||||||
|
app.config.from_object('config')
|
||||||
|
|
||||||
|
# Database
|
||||||
|
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||||
|
|
||||||
|
# Debugging
|
||||||
|
if app.config['DEBUG']:
|
||||||
|
app.config['DEBUG_TB_INTERCEPT_REDIRECTS'] = False
|
||||||
|
toolbar = DebugToolbarExtension(app)
|
||||||
|
app.logger.setLevel(logging.DEBUG)
|
||||||
|
else:
|
||||||
|
app.logger.setLevel(logging.WARNING)
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
if 'LOG_FILE' in app.config:
|
||||||
|
from logging.handlers import RotatingFileHandler
|
||||||
|
app.log_handler = RotatingFileHandler(
|
||||||
|
app.config['LOG_FILE'], maxBytes=10000, backupCount=1)
|
||||||
|
app.logger.addHandler(app.log_handler)
|
||||||
|
|
||||||
|
# Log errors and display a message to the user in production mdode
|
||||||
|
if not app.config['DEBUG']:
|
||||||
|
@app.errorhandler(500)
|
||||||
|
def internal_error(exception):
|
||||||
|
app.logger.error(exception)
|
||||||
|
flask.flash(flask.Markup(
|
||||||
|
'<strong>An error occured!</strong> Debugging information has been logged.'), 'danger')
|
||||||
|
return flask.redirect('/')
|
||||||
|
|
||||||
|
# Enable the jinja2 do extension.
|
||||||
|
app.jinja_env.add_extension('jinja2.ext.do')
|
||||||
|
app.jinja_env.lstrip_blocks = True
|
||||||
|
app.jinja_env.trim_blocks = True
|
||||||
|
|
||||||
|
db = SQLAlchemy(app)
|
||||||
|
|
||||||
|
assets = Environment(app)
|
||||||
|
|
||||||
|
# css = Bundle('style.scss', filters='libsass',
|
||||||
|
# output='style.css', depends='**/*.scss')
|
||||||
|
# assets.register('style_all', css)
|
||||||
|
|
||||||
|
from nyaa import routes
|
317
nyaa/api_handler.py
Normal file
|
@ -0,0 +1,317 @@
|
||||||
|
import flask
|
||||||
|
from nyaa import app, db
|
||||||
|
from nyaa import models, forms
|
||||||
|
from nyaa import bencode, utils
|
||||||
|
from nyaa import torrents
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os.path
|
||||||
|
from orderedset import OrderedSet
|
||||||
|
from werkzeug import secure_filename
|
||||||
|
|
||||||
|
DEBUG_API = False
|
||||||
|
#################################### API ROUTES ####################################
|
||||||
|
CATEGORIES = [
|
||||||
|
('Anime', ['Anime Music Video', 'English-translated', 'Non-English-translated', 'Raw']),
|
||||||
|
('Audio', ['Lossless', 'Lossy']),
|
||||||
|
('Literature', ['English-translated', 'Non-English-translated', 'Raw']),
|
||||||
|
('Live Action', ['English-translated',
|
||||||
|
'Idol/Promotional Video', 'Non-English-translated', 'Raw']),
|
||||||
|
('Pictures', ['Graphics', 'Photos']),
|
||||||
|
('Software', ['Applications', 'Games']),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def validate_main_sub_cat(main_cat_name, sub_cat_name):
|
||||||
|
for main_cat in models.MainCategory.query.order_by(models.MainCategory.id):
|
||||||
|
if main_cat_name == main_cat.name:
|
||||||
|
for sub_cat in main_cat.sub_categories:
|
||||||
|
if sub_cat_name == sub_cat.name:
|
||||||
|
cat_id = main_cat.id_as_string
|
||||||
|
sub_cat_id = sub_cat.id_as_string
|
||||||
|
cat_sub_cat = sub_cat_id.split('_')
|
||||||
|
#print('cat: {0} sub_cat: {1}'.format(cat_sub_cat[0], cat_sub_cat[1]))
|
||||||
|
|
||||||
|
return True, cat_sub_cat[0], cat_sub_cat[1]
|
||||||
|
|
||||||
|
return False, 0, 0
|
||||||
|
|
||||||
|
|
||||||
|
def _replace_utf8_values(dict_or_list):
|
||||||
|
''' Will replace 'property' with 'property.utf-8' and remove latter if it exists.
|
||||||
|
Thanks, bitcomet! :/ '''
|
||||||
|
did_change = False
|
||||||
|
if isinstance(dict_or_list, dict):
|
||||||
|
for key in [key for key in dict_or_list.keys() if key.endswith('.utf-8')]:
|
||||||
|
dict_or_list[key.replace('.utf-8', '')] = dict_or_list.pop(key)
|
||||||
|
did_change = True
|
||||||
|
for value in dict_or_list.values():
|
||||||
|
did_change = _replace_utf8_values(value) or did_change
|
||||||
|
elif isinstance(dict_or_list, list):
|
||||||
|
for item in dict_or_list:
|
||||||
|
did_change = _replace_utf8_values(item) or did_change
|
||||||
|
return did_change
|
||||||
|
|
||||||
|
|
||||||
|
def validate_torrent_flags(torrent_flags):
|
||||||
|
_torrent_flags = ['hidden', 'remake', 'complete', 'anonymous']
|
||||||
|
|
||||||
|
if len(torrent_flags) != 4:
|
||||||
|
return False
|
||||||
|
|
||||||
|
for flag in torrent_flags:
|
||||||
|
if int(flag) not in [0, 1]:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
# It might be good to factor this out of forms UploadForm because the same code is
|
||||||
|
# used in both files.
|
||||||
|
|
||||||
|
|
||||||
|
def validate_torrent_file(torrent_file_name, torrent_file):
|
||||||
|
# Decode and ensure data is bencoded data
|
||||||
|
try:
|
||||||
|
torrent_dict = bencode.decode(torrent_file)
|
||||||
|
except (bencode.MalformedBencodeException, UnicodeError):
|
||||||
|
return False, 'Malformed torrent file'
|
||||||
|
|
||||||
|
# Uncomment for debug print of the torrent
|
||||||
|
# forms._debug_print_torrent_metadata(torrent_dict)
|
||||||
|
|
||||||
|
try:
|
||||||
|
forms._validate_torrent_metadata(torrent_dict)
|
||||||
|
except AssertionError as e:
|
||||||
|
return False, 'Malformed torrent metadata ({})'.format(e.args[0])
|
||||||
|
|
||||||
|
# Note! bencode will sort dict keys, as per the spec
|
||||||
|
# This may result in a different hash if the uploaded torrent does not match the
|
||||||
|
# spec, but it's their own fault for using broken software! Right?
|
||||||
|
bencoded_info_dict = bencode.encode(torrent_dict['info'])
|
||||||
|
info_hash = utils.sha1_hash(bencoded_info_dict)
|
||||||
|
|
||||||
|
# Check if the info_hash exists already in the database
|
||||||
|
existing_torrent = models.Torrent.by_info_hash(info_hash)
|
||||||
|
if existing_torrent:
|
||||||
|
return False, 'That torrent already exists (#{})'.format(existing_torrent.id)
|
||||||
|
|
||||||
|
# Torrent is legit, pass original filename and dict along
|
||||||
|
return True, forms.TorrentFileData(filename=os.path.basename(torrent_file_name),
|
||||||
|
torrent_dict=torrent_dict,
|
||||||
|
info_hash=info_hash,
|
||||||
|
bencoded_info_dict=bencoded_info_dict)
|
||||||
|
|
||||||
|
|
||||||
|
def api_upload(upload_request):
|
||||||
|
if upload_request.method == 'POST':
|
||||||
|
j = None
|
||||||
|
torrent_file = None
|
||||||
|
try:
|
||||||
|
if 'json' in upload_request.files:
|
||||||
|
f = upload_request.files['json']
|
||||||
|
j = json.loads(f.read().decode('utf-8'))
|
||||||
|
if DEBUG_API:
|
||||||
|
print(json.dumps(j, indent=4))
|
||||||
|
|
||||||
|
_json_keys = ['username', 'password',
|
||||||
|
'display_name', 'main_cat', 'sub_cat', 'flags'] # 'information' and 'description' are not required
|
||||||
|
# Check that required fields are present
|
||||||
|
for _k in _json_keys:
|
||||||
|
if _k not in j.keys():
|
||||||
|
return flask.make_response(flask.jsonify({"Error": "Missing JSON field: {0}.".format(_k)}), 400)
|
||||||
|
# Check that no extra fields are present
|
||||||
|
for k in j.keys():
|
||||||
|
if k not in ['username', 'password',
|
||||||
|
'display_name', 'main_cat', 'sub_cat', 'information', 'description', 'flags']:
|
||||||
|
return flask.make_response(flask.jsonify({"Error": "Incorrect JSON field(s)."}), 400)
|
||||||
|
else:
|
||||||
|
return flask.make_response(flask.jsonify({"Error": "No metadata."}), 400)
|
||||||
|
if 'torrent' in upload_request.files:
|
||||||
|
f = upload_request.files['torrent']
|
||||||
|
if DEBUG_API:
|
||||||
|
print(f.filename)
|
||||||
|
torrent_file = f
|
||||||
|
# print(f.read())
|
||||||
|
else:
|
||||||
|
return flask.make_response(flask.jsonify({"Error": "No torrent file."}), 400)
|
||||||
|
|
||||||
|
# 'username' and 'password' must have been provided as they are part of j.keys()
|
||||||
|
username = j['username']
|
||||||
|
password = j['password']
|
||||||
|
# Validate that the provided username and password belong to a valid user
|
||||||
|
user = models.User.by_username(username)
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
user = models.User.by_email(username)
|
||||||
|
|
||||||
|
if not user or password != user.password_hash or user.status == models.UserStatusType.INACTIVE:
|
||||||
|
return flask.make_response(flask.jsonify({"Error": "Incorrect username or password"}), 403)
|
||||||
|
|
||||||
|
current_user = user
|
||||||
|
|
||||||
|
display_name = j['display_name']
|
||||||
|
if (len(display_name) < 3) or (len(display_name) > 1024):
|
||||||
|
return flask.make_response(flask.jsonify({"Error": "Torrent name must be between 3 and 1024 characters."}), 400)
|
||||||
|
|
||||||
|
main_cat_name = j['main_cat']
|
||||||
|
sub_cat_name = j['sub_cat']
|
||||||
|
|
||||||
|
cat_subcat_status, cat_id, sub_cat_id = validate_main_sub_cat(
|
||||||
|
main_cat_name, sub_cat_name)
|
||||||
|
if not cat_subcat_status:
|
||||||
|
return flask.make_response(flask.jsonify({"Error": "Incorrect Category / Sub-Category."}), 400)
|
||||||
|
|
||||||
|
# TODO Sanitize information
|
||||||
|
information = None
|
||||||
|
try:
|
||||||
|
information = j['information']
|
||||||
|
if len(information) > 255:
|
||||||
|
return flask.make_response(flask.jsonify({"Error": "Information is limited to 255 characters."}), 400)
|
||||||
|
except Exception as e:
|
||||||
|
information = ''
|
||||||
|
|
||||||
|
# TODO Sanitize description
|
||||||
|
description = None
|
||||||
|
try:
|
||||||
|
description = j['description']
|
||||||
|
if len(description) > (10 * 1024):
|
||||||
|
return flask.make_response(flask.jsonify({"Error": "Description is limited to {0} characters.".format(10 * 1024)}), 403)
|
||||||
|
except Exception as e:
|
||||||
|
description = ''
|
||||||
|
|
||||||
|
v_flags = validate_torrent_flags(j['flags'])
|
||||||
|
if v_flags:
|
||||||
|
torrent_flags = j['flags']
|
||||||
|
else:
|
||||||
|
return flask.make_response(flask.jsonify({"Error": "Incorrect torrent flags."}), 400)
|
||||||
|
|
||||||
|
torrent_status, torrent_data = validate_torrent_file(
|
||||||
|
torrent_file.filename, torrent_file.read()) # Needs validation
|
||||||
|
|
||||||
|
if not torrent_status:
|
||||||
|
return flask.make_response(flask.jsonify({"Error": "Invalid or Duplicate torrent file."}), 400)
|
||||||
|
|
||||||
|
# The torrent has been validated and is safe to access with ['foo'] etc - all relevant
|
||||||
|
# keys and values have been checked for (see UploadForm in forms.py for details)
|
||||||
|
info_dict = torrent_data.torrent_dict['info']
|
||||||
|
|
||||||
|
changed_to_utf8 = _replace_utf8_values(torrent_data.torrent_dict)
|
||||||
|
|
||||||
|
torrent_filesize = info_dict.get('length') or sum(
|
||||||
|
f['length'] for f in info_dict.get('files'))
|
||||||
|
|
||||||
|
# In case no encoding, assume UTF-8.
|
||||||
|
torrent_encoding = torrent_data.torrent_dict.get('encoding', b'utf-8').decode('utf-8')
|
||||||
|
|
||||||
|
torrent = models.Torrent(info_hash=torrent_data.info_hash,
|
||||||
|
display_name=display_name,
|
||||||
|
torrent_name=torrent_data.filename,
|
||||||
|
information=information,
|
||||||
|
description=description,
|
||||||
|
encoding=torrent_encoding,
|
||||||
|
filesize=torrent_filesize,
|
||||||
|
user=current_user)
|
||||||
|
|
||||||
|
# Store bencoded info_dict
|
||||||
|
torrent.info = models.TorrentInfo(info_dict=torrent_data.bencoded_info_dict)
|
||||||
|
torrent.stats = models.Statistic()
|
||||||
|
torrent.has_torrent = True
|
||||||
|
|
||||||
|
# Fields with default value will be None before first commit, so set .flags
|
||||||
|
torrent.flags = 0
|
||||||
|
|
||||||
|
torrent.anonymous = True if torrent_flags[0] else False
|
||||||
|
torrent.hidden = True if torrent_flags[1] else False
|
||||||
|
torrent.remake = True if torrent_flags[2] else False
|
||||||
|
torrent.complete = True if torrent_flags[3] else False
|
||||||
|
# Copy trusted status from user if possible
|
||||||
|
torrent.trusted = (current_user.level >=
|
||||||
|
models.UserLevelType.TRUSTED) if current_user else False
|
||||||
|
|
||||||
|
# Set category ids
|
||||||
|
torrent.main_category_id = cat_id
|
||||||
|
torrent.sub_category_id = sub_cat_id
|
||||||
|
# To simplify parsing the filelist, turn single-file torrent into a list
|
||||||
|
torrent_filelist = info_dict.get('files')
|
||||||
|
|
||||||
|
used_path_encoding = changed_to_utf8 and 'utf-8' or torrent_encoding
|
||||||
|
|
||||||
|
parsed_file_tree = dict()
|
||||||
|
if not torrent_filelist:
|
||||||
|
# If single-file, the root will be the file-tree (no directory)
|
||||||
|
file_tree_root = parsed_file_tree
|
||||||
|
torrent_filelist = [{'length': torrent_filesize, 'path': [info_dict['name']]}]
|
||||||
|
else:
|
||||||
|
# If multi-file, use the directory name as root for files
|
||||||
|
file_tree_root = parsed_file_tree.setdefault(
|
||||||
|
info_dict['name'].decode(used_path_encoding), {})
|
||||||
|
|
||||||
|
# Parse file dicts into a tree
|
||||||
|
for file_dict in torrent_filelist:
|
||||||
|
# Decode path parts from utf8-bytes
|
||||||
|
path_parts = [path_part.decode(used_path_encoding)
|
||||||
|
for path_part in file_dict['path']]
|
||||||
|
|
||||||
|
filename = path_parts.pop()
|
||||||
|
current_directory = file_tree_root
|
||||||
|
|
||||||
|
for directory in path_parts:
|
||||||
|
current_directory = current_directory.setdefault(directory, {})
|
||||||
|
|
||||||
|
current_directory[filename] = file_dict['length']
|
||||||
|
|
||||||
|
parsed_file_tree = utils.sorted_pathdict(parsed_file_tree)
|
||||||
|
|
||||||
|
json_bytes = json.dumps(parsed_file_tree, separators=(',', ':')).encode('utf8')
|
||||||
|
torrent.filelist = models.TorrentFilelist(filelist_blob=json_bytes)
|
||||||
|
|
||||||
|
db.session.add(torrent)
|
||||||
|
db.session.flush()
|
||||||
|
|
||||||
|
# Store the users trackers
|
||||||
|
trackers = OrderedSet()
|
||||||
|
announce = torrent_data.torrent_dict.get('announce', b'').decode('ascii')
|
||||||
|
if announce:
|
||||||
|
trackers.add(announce)
|
||||||
|
|
||||||
|
# List of lists with single item
|
||||||
|
announce_list = torrent_data.torrent_dict.get('announce-list', [])
|
||||||
|
for announce in announce_list:
|
||||||
|
trackers.add(announce[0].decode('ascii'))
|
||||||
|
|
||||||
|
# Remove our trackers, maybe? TODO ?
|
||||||
|
|
||||||
|
# Search for/Add trackers in DB
|
||||||
|
db_trackers = OrderedSet()
|
||||||
|
for announce in trackers:
|
||||||
|
tracker = models.Trackers.by_uri(announce)
|
||||||
|
|
||||||
|
# Insert new tracker if not found
|
||||||
|
if not tracker:
|
||||||
|
tracker = models.Trackers(uri=announce)
|
||||||
|
db.session.add(tracker)
|
||||||
|
|
||||||
|
db_trackers.add(tracker)
|
||||||
|
|
||||||
|
db.session.flush()
|
||||||
|
|
||||||
|
# Store tracker refs in DB
|
||||||
|
for order, tracker in enumerate(db_trackers):
|
||||||
|
torrent_tracker = models.TorrentTrackers(torrent_id=torrent.id,
|
||||||
|
tracker_id=tracker.id, order=order)
|
||||||
|
db.session.add(torrent_tracker)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
if app.config.get('BACKUP_TORRENT_FOLDER'):
|
||||||
|
torrent_file.seek(0, 0)
|
||||||
|
torrent_path = os.path.join(app.config['BACKUP_TORRENT_FOLDER'], '{}.{}'.format(torrent.id, secure_filename(torrent_file.filename)))
|
||||||
|
torrent_file.save(torrent_path)
|
||||||
|
torrent_file.close()
|
||||||
|
|
||||||
|
#print('Success? {0}'.format(torrent.id))
|
||||||
|
return flask.make_response(flask.jsonify({"Success": "Request was processed {0}".format(torrent.id)}), 200)
|
||||||
|
except Exception as e:
|
||||||
|
print('Exception: {0}'.format(e))
|
||||||
|
return flask.make_response(flask.jsonify({"Error": "Incorrect JSON. Please see HELP page for examples."}), 400)
|
||||||
|
else:
|
||||||
|
return flask.make_response(flask.jsonify({"Error": "Bad request"}), 400)
|
163
nyaa/bencode.py
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
|
||||||
|
def _pairwise(iterable):
|
||||||
|
""" Returns items from an iterable two at a time, ala
|
||||||
|
[0, 1, 2, 3, ...] -> [(0, 1), (2, 3), ...] """
|
||||||
|
iterable = iter(iterable)
|
||||||
|
return zip(iterable, iterable)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ['encode', 'decode', 'BencodeException', 'MalformedBencodeException']
|
||||||
|
|
||||||
|
# https://wiki.theory.org/BitTorrentSpecification#Bencoding
|
||||||
|
|
||||||
|
|
||||||
|
class BencodeException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MalformedBencodeException(BencodeException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# bencode types
|
||||||
|
_DIGITS = b'0123456789'
|
||||||
|
_B_INT = b'i'
|
||||||
|
_B_LIST = b'l'
|
||||||
|
_B_DICT = b'd'
|
||||||
|
_B_END = b'e'
|
||||||
|
|
||||||
|
|
||||||
|
# Decoding of bencoded data
|
||||||
|
|
||||||
|
def _bencode_decode(file_object, decode_keys_as_utf8=True):
|
||||||
|
""" Decodes a bencoded value, raising a MalformedBencodeException on errors.
|
||||||
|
decode_keys_as_utf8 controls decoding dict keys as utf8 (which they
|
||||||
|
almost always are) """
|
||||||
|
if isinstance(file_object, str):
|
||||||
|
file_object = file_object.encode('utf8')
|
||||||
|
if isinstance(file_object, bytes):
|
||||||
|
file_object = BytesIO(file_object)
|
||||||
|
|
||||||
|
def create_ex(msg):
|
||||||
|
return MalformedBencodeException(
|
||||||
|
'{0} at position {1} (0x{1:02X} hex)'.format(msg, file_object.tell()))
|
||||||
|
|
||||||
|
def _read_list():
|
||||||
|
""" Decodes values from stream until a None is returned ('e') """
|
||||||
|
items = []
|
||||||
|
while True:
|
||||||
|
value = _bencode_decode(file_object, decode_keys_as_utf8=decode_keys_as_utf8)
|
||||||
|
if value is None:
|
||||||
|
break
|
||||||
|
items.append(value)
|
||||||
|
return items
|
||||||
|
|
||||||
|
kind = file_object.read(1)
|
||||||
|
|
||||||
|
if kind == _B_INT: # Integer
|
||||||
|
int_bytes = b''
|
||||||
|
while True:
|
||||||
|
c = file_object.read(1)
|
||||||
|
if not c:
|
||||||
|
raise create_ex('Unexpected end while reading an integer')
|
||||||
|
elif c == _B_END:
|
||||||
|
try:
|
||||||
|
return int(int_bytes.decode('utf8'))
|
||||||
|
except Exception as e:
|
||||||
|
raise create_ex('Unable to parse int')
|
||||||
|
|
||||||
|
# not a digit OR '-' in the middle of the int
|
||||||
|
if (c not in _DIGITS + b'-') or (c == b'-' and int_bytes):
|
||||||
|
raise create_ex('Unexpected input while reading an integer: ' + repr(c))
|
||||||
|
else:
|
||||||
|
int_bytes += c
|
||||||
|
|
||||||
|
elif kind == _B_LIST: # List
|
||||||
|
return _read_list()
|
||||||
|
|
||||||
|
elif kind == _B_DICT: # Dictionary
|
||||||
|
keys_and_values = _read_list()
|
||||||
|
if len(keys_and_values) % 2 != 0:
|
||||||
|
raise MalformedBencodeException('Uneven amount of key/value pairs')
|
||||||
|
|
||||||
|
# "Technically" the bencode dictionary keys are bytestrings,
|
||||||
|
# but real-world they're always(?) UTF-8.
|
||||||
|
decoded_dict = dict((decode_keys_as_utf8 and k.decode('utf8') or k, v)
|
||||||
|
for k, v in _pairwise(keys_and_values))
|
||||||
|
return decoded_dict
|
||||||
|
|
||||||
|
# List/dict end, but make sure input is not just 'e'
|
||||||
|
elif kind == _B_END and file_object.tell() > 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
elif kind in _DIGITS: # Bytestring
|
||||||
|
str_len_bytes = kind # keep first digit
|
||||||
|
# Read string length until a ':'
|
||||||
|
while True:
|
||||||
|
c = file_object.read(1)
|
||||||
|
if c in _DIGITS:
|
||||||
|
str_len_bytes += c
|
||||||
|
elif c == b':':
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise create_ex('Unexpected input while reading string length: ' + repr(c))
|
||||||
|
try:
|
||||||
|
str_len = int(str_len_bytes.decode())
|
||||||
|
except Exception as e:
|
||||||
|
raise create_ex('Unable to parse bytestring length')
|
||||||
|
|
||||||
|
bytestring = file_object.read(str_len)
|
||||||
|
if len(bytestring) != str_len:
|
||||||
|
raise create_ex('Read only {} bytes, {} wanted'.format(len(bytestring), str_len))
|
||||||
|
|
||||||
|
return bytestring
|
||||||
|
else:
|
||||||
|
raise create_ex('Unexpected data type ({})'.format(repr(kind)))
|
||||||
|
|
||||||
|
|
||||||
|
# Bencoding
|
||||||
|
|
||||||
|
def _bencode_int(value):
|
||||||
|
""" Encode an integer, eg 64 -> i64e """
|
||||||
|
return _B_INT + str(value).encode('utf8') + _B_END
|
||||||
|
|
||||||
|
|
||||||
|
def _bencode_bytes(value):
|
||||||
|
""" Encode a bytestring (strings as UTF-8), eg 'hello' -> 5:hello """
|
||||||
|
if isinstance(value, str):
|
||||||
|
value = value.encode('utf8')
|
||||||
|
return str(len(value)).encode('utf8') + b':' + value
|
||||||
|
|
||||||
|
|
||||||
|
def _bencode_list(value):
|
||||||
|
""" Encode a list, eg [64, "hello"] -> li64e5:helloe """
|
||||||
|
return _B_LIST + b''.join(_bencode(item) for item in value) + _B_END
|
||||||
|
|
||||||
|
|
||||||
|
def _bencode_dict(value):
|
||||||
|
""" Encode a dict, which is keys and values interleaved as a list,
|
||||||
|
eg {"hello":123}-> d5:helloi123ee """
|
||||||
|
dict_keys = sorted(value.keys()) # Sort keys as per spec
|
||||||
|
return _B_DICT + b''.join(
|
||||||
|
_bencode_bytes(key) + _bencode(value[key]) for key in dict_keys) + _B_END
|
||||||
|
|
||||||
|
|
||||||
|
def _bencode(value):
|
||||||
|
""" Bencode any supported value (int, bytes, str, list, dict) """
|
||||||
|
if isinstance(value, int):
|
||||||
|
return _bencode_int(value)
|
||||||
|
elif isinstance(value, (str, bytes)):
|
||||||
|
return _bencode_bytes(value)
|
||||||
|
elif isinstance(value, list):
|
||||||
|
return _bencode_list(value)
|
||||||
|
elif isinstance(value, dict):
|
||||||
|
return _bencode_dict(value)
|
||||||
|
|
||||||
|
raise BencodeException('Unsupported type ' + str(type(value)))
|
||||||
|
|
||||||
|
|
||||||
|
# The functions call themselves
|
||||||
|
encode = _bencode
|
||||||
|
decode = _bencode_decode
|
28
nyaa/fix_paginate.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
from flask_sqlalchemy import Pagination, BaseQuery
|
||||||
|
from flask import abort
|
||||||
|
|
||||||
|
def paginate_faste(self, page=1, per_page=50, max_page=None, step=5):
|
||||||
|
if page < 1:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
if max_page and page > max_page:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
items = self.limit(per_page).offset((page - 1) * per_page).all()
|
||||||
|
|
||||||
|
if not items and page != 1:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
# No need to count if we're on the first page and there are fewer
|
||||||
|
# items than we expected.
|
||||||
|
if page == 1 and len(items) < per_page:
|
||||||
|
total = len(items)
|
||||||
|
else:
|
||||||
|
if max_page:
|
||||||
|
total = self.order_by(None).limit(per_page * min((page + step), max_page)).count()
|
||||||
|
else:
|
||||||
|
total = self.order_by(None).limit(per_page * (page + step)).count()
|
||||||
|
|
||||||
|
return Pagination(self, page, per_page, total, items)
|
||||||
|
|
||||||
|
BaseQuery.paginate_faste = paginate_faste
|
352
nyaa/forms.py
Normal file
|
@ -0,0 +1,352 @@
|
||||||
|
from nyaa import db, app
|
||||||
|
from nyaa.models import User
|
||||||
|
from nyaa import bencode, utils, models
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from flask_wtf import FlaskForm
|
||||||
|
from flask_wtf.file import FileField, FileRequired
|
||||||
|
from wtforms import TextField, PasswordField, BooleanField, TextAreaField, SelectField
|
||||||
|
from wtforms.validators import Required, Optional, Email, Length, EqualTo, ValidationError, Regexp
|
||||||
|
|
||||||
|
# For DisabledSelectField
|
||||||
|
from wtforms.widgets import Select as SelectWidget
|
||||||
|
from wtforms.widgets import html_params, HTMLString
|
||||||
|
|
||||||
|
from flask_wtf.recaptcha import RecaptchaField
|
||||||
|
|
||||||
|
|
||||||
|
class Unique(object):
|
||||||
|
|
||||||
|
""" validator that checks field uniqueness """
|
||||||
|
|
||||||
|
def __init__(self, model, field, message=None):
|
||||||
|
self.model = model
|
||||||
|
self.field = field
|
||||||
|
if not message:
|
||||||
|
message = 'This element already exists'
|
||||||
|
self.message = message
|
||||||
|
|
||||||
|
def __call__(self, form, field):
|
||||||
|
check = self.model.query.filter(self.field == field.data).first()
|
||||||
|
if check:
|
||||||
|
raise ValidationError(self.message)
|
||||||
|
|
||||||
|
|
||||||
|
_username_validator = Regexp(
|
||||||
|
r'[a-zA-Z0-9_\-]+',
|
||||||
|
message='Your username must only consist of alphanumerics and _- (a-zA-Z0-9_-)')
|
||||||
|
|
||||||
|
|
||||||
|
class LoginForm(FlaskForm):
|
||||||
|
username = TextField('Username or email address', [Required(), _username_validator])
|
||||||
|
password = PasswordField('Password', [Required()])
|
||||||
|
|
||||||
|
|
||||||
|
class RegisterForm(FlaskForm):
|
||||||
|
username = TextField('Username', [
|
||||||
|
Required(),
|
||||||
|
Length(min=3, max=32),
|
||||||
|
_username_validator,
|
||||||
|
Unique(User, User.username, 'Username not availiable')
|
||||||
|
])
|
||||||
|
|
||||||
|
email = TextField('Email address', [
|
||||||
|
Email(),
|
||||||
|
Required(),
|
||||||
|
Length(min=5, max=128),
|
||||||
|
Unique(User, User.email, 'Email already in use by another account')
|
||||||
|
])
|
||||||
|
|
||||||
|
password = PasswordField('Password', [
|
||||||
|
Required(),
|
||||||
|
EqualTo('password_confirm', message='Passwords must match'),
|
||||||
|
Length(min=6, max=1024,
|
||||||
|
message='Password must be at least %(min)d characters long.')
|
||||||
|
])
|
||||||
|
|
||||||
|
password_confirm = PasswordField('Password (confirm)')
|
||||||
|
|
||||||
|
if app.config['USE_RECAPTCHA']:
|
||||||
|
recaptcha = RecaptchaField()
|
||||||
|
|
||||||
|
|
||||||
|
class ProfileForm(FlaskForm):
|
||||||
|
email = TextField('New email address', [
|
||||||
|
Email(),
|
||||||
|
Optional(),
|
||||||
|
Length(min=5, max=128),
|
||||||
|
Unique(User, User.email, 'Email is taken')
|
||||||
|
])
|
||||||
|
|
||||||
|
current_password = PasswordField('Current password', [Optional()])
|
||||||
|
|
||||||
|
new_password = PasswordField('New password (confirm)', [
|
||||||
|
Optional(),
|
||||||
|
EqualTo('password_confirm', message='Passwords must match'),
|
||||||
|
Length(min=6, max=1024,
|
||||||
|
message='Password must be at least %(min)d characters long.')
|
||||||
|
])
|
||||||
|
|
||||||
|
password_confirm = PasswordField('Repeat Password')
|
||||||
|
|
||||||
|
|
||||||
|
# Classes for a SelectField that can be set to disable options (id, name, disabled)
|
||||||
|
# TODO: Move to another file for cleaner look
|
||||||
|
class DisabledSelectWidget(SelectWidget):
|
||||||
|
def __call__(self, field, **kwargs):
|
||||||
|
kwargs.setdefault('id', field.id)
|
||||||
|
if self.multiple:
|
||||||
|
kwargs['multiple'] = True
|
||||||
|
html = ['<select %s>' % html_params(name=field.name, **kwargs)]
|
||||||
|
for val, label, selected, disabled in field.iter_choices():
|
||||||
|
extra = disabled and {'disabled': ''} or {}
|
||||||
|
html.append(self.render_option(val, label, selected, **extra))
|
||||||
|
html.append('</select>')
|
||||||
|
return HTMLString(''.join(html))
|
||||||
|
|
||||||
|
|
||||||
|
class DisabledSelectField(SelectField):
|
||||||
|
widget = DisabledSelectWidget()
|
||||||
|
|
||||||
|
def iter_choices(self):
|
||||||
|
for choice_tuple in self.choices:
|
||||||
|
value, label = choice_tuple[:2]
|
||||||
|
disabled = len(choice_tuple) == 3 and choice_tuple[2] or False
|
||||||
|
yield (value, label, self.coerce(value) == self.data, disabled)
|
||||||
|
|
||||||
|
def pre_validate(self, form):
|
||||||
|
for v in self.choices:
|
||||||
|
if self.data == v[0]:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise ValueError(self.gettext('Not a valid choice'))
|
||||||
|
|
||||||
|
|
||||||
|
class EditForm(FlaskForm):
|
||||||
|
display_name = TextField('Display name', [
|
||||||
|
Length(min=3, max=255,
|
||||||
|
message='Torrent name must be at least %(min)d characters and %(max)d at most.')
|
||||||
|
])
|
||||||
|
|
||||||
|
category = DisabledSelectField('Category')
|
||||||
|
|
||||||
|
def validate_category(form, field):
|
||||||
|
cat_match = re.match(r'^(\d+)_(\d+)$', field.data)
|
||||||
|
if not cat_match:
|
||||||
|
raise ValidationError('You must select a category')
|
||||||
|
|
||||||
|
main_cat_id = int(cat_match.group(1))
|
||||||
|
sub_cat_id = int(cat_match.group(2))
|
||||||
|
|
||||||
|
cat = models.SubCategory.by_category_ids(main_cat_id, sub_cat_id)
|
||||||
|
|
||||||
|
if not cat:
|
||||||
|
raise ValidationError('You must select a proper category')
|
||||||
|
|
||||||
|
field.parsed_data = cat
|
||||||
|
|
||||||
|
is_hidden = BooleanField('Hidden')
|
||||||
|
is_deleted = BooleanField('Deleted')
|
||||||
|
is_remake = BooleanField('Remake')
|
||||||
|
is_anonymous = BooleanField('Anonymous')
|
||||||
|
is_complete = BooleanField('Complete')
|
||||||
|
|
||||||
|
information = TextField('Information', [
|
||||||
|
Length(max=255, message='Information must be at most %(max)d characters long.')
|
||||||
|
])
|
||||||
|
description = TextAreaField('Description (markdown supported)', [
|
||||||
|
Length(max=10 * 1024, message='Description must be at most %(max)d characters long.')
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
class UploadForm(FlaskForm):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
csrf = False
|
||||||
|
|
||||||
|
torrent_file = FileField('Torrent file', [
|
||||||
|
FileRequired()
|
||||||
|
])
|
||||||
|
|
||||||
|
display_name = TextField('Display name (optional)', [
|
||||||
|
Optional(),
|
||||||
|
Length(min=3, max=255,
|
||||||
|
message='Torrent name must be at least %(min)d characters long and %(max)d at most.')
|
||||||
|
])
|
||||||
|
|
||||||
|
# category = SelectField('Category')
|
||||||
|
category = DisabledSelectField('Category')
|
||||||
|
|
||||||
|
def validate_category(form, field):
|
||||||
|
cat_match = re.match(r'^(\d+)_(\d+)$', field.data)
|
||||||
|
if not cat_match:
|
||||||
|
raise ValidationError('You must select a category')
|
||||||
|
|
||||||
|
main_cat_id = int(cat_match.group(1))
|
||||||
|
sub_cat_id = int(cat_match.group(2))
|
||||||
|
|
||||||
|
cat = models.SubCategory.by_category_ids(main_cat_id, sub_cat_id)
|
||||||
|
|
||||||
|
if not cat:
|
||||||
|
raise ValidationError('You must select a proper category')
|
||||||
|
|
||||||
|
field.parsed_data = cat
|
||||||
|
|
||||||
|
is_hidden = BooleanField('Hidden')
|
||||||
|
is_remake = BooleanField('Remake')
|
||||||
|
is_anonymous = BooleanField('Anonymous')
|
||||||
|
is_complete = BooleanField('Complete')
|
||||||
|
|
||||||
|
information = TextField('Information', [
|
||||||
|
Length(max=255, message='Information must be at most %(max)d characters long.')
|
||||||
|
])
|
||||||
|
description = TextAreaField('Description (markdown supported)', [
|
||||||
|
Length(max=10 * 1024, message='Description must be at most %(max)d characters long.')
|
||||||
|
])
|
||||||
|
|
||||||
|
def validate_torrent_file(form, field):
|
||||||
|
# Decode and ensure data is bencoded data
|
||||||
|
try:
|
||||||
|
torrent_dict = bencode.decode(field.data)
|
||||||
|
#field.data.close()
|
||||||
|
except (bencode.MalformedBencodeException, UnicodeError):
|
||||||
|
raise ValidationError('Malformed torrent file')
|
||||||
|
|
||||||
|
# Uncomment for debug print of the torrent
|
||||||
|
# _debug_print_torrent_metadata(torrent_dict)
|
||||||
|
|
||||||
|
try:
|
||||||
|
_validate_torrent_metadata(torrent_dict)
|
||||||
|
except AssertionError as e:
|
||||||
|
raise ValidationError('Malformed torrent metadata ({})'.format(e.args[0]))
|
||||||
|
|
||||||
|
try:
|
||||||
|
_validate_trackers(torrent_dict)
|
||||||
|
except AssertionError as e:
|
||||||
|
raise ValidationError('Malformed torrent trackers ({})'.format(e.args[0]))
|
||||||
|
|
||||||
|
if app.config.get('ENFORCE_MAIN_ANNOUNCE_URL'):
|
||||||
|
main_announce_url = app.config.get('MAIN_ANNOUNCE_URL')
|
||||||
|
if not main_announce_url:
|
||||||
|
raise Exception('Config MAIN_ANNOUNCE_URL not set!')
|
||||||
|
|
||||||
|
announce = torrent_dict.get('announce', b'').decode('utf-8')
|
||||||
|
if announce != main_announce_url:
|
||||||
|
raise ValidationError('Please set {} as the first tracker in the torrent'.format(main_announce_url))
|
||||||
|
|
||||||
|
# Note! bencode will sort dict keys, as per the spec
|
||||||
|
# This may result in a different hash if the uploaded torrent does not match the
|
||||||
|
# spec, but it's their own fault for using broken software! Right?
|
||||||
|
bencoded_info_dict = bencode.encode(torrent_dict['info'])
|
||||||
|
info_hash = utils.sha1_hash(bencoded_info_dict)
|
||||||
|
|
||||||
|
# Check if the info_hash exists already in the database
|
||||||
|
existing_torrent = models.Torrent.by_info_hash(info_hash)
|
||||||
|
if existing_torrent:
|
||||||
|
raise ValidationError('That torrent already exists (#{})'.format(existing_torrent.id))
|
||||||
|
|
||||||
|
# Torrent is legit, pass original filename and dict along
|
||||||
|
field.parsed_data = TorrentFileData(filename=os.path.basename(field.data.filename),
|
||||||
|
torrent_dict=torrent_dict,
|
||||||
|
info_hash=info_hash,
|
||||||
|
bencoded_info_dict=bencoded_info_dict)
|
||||||
|
|
||||||
|
|
||||||
|
class TorrentFileData(object):
|
||||||
|
"""Quick and dirty class to pass data from the validator"""
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
for k, v in kwargs.items():
|
||||||
|
setattr(self, k, v)
|
||||||
|
|
||||||
|
# https://wiki.theory.org/BitTorrentSpecification#Metainfo_File_Structure
|
||||||
|
|
||||||
|
def _validate_trackers(torrent_dict):
|
||||||
|
announce = torrent_dict.get('announce')
|
||||||
|
_validate_bytes(announce)
|
||||||
|
|
||||||
|
announce_list = torrent_dict.get('announce-list')
|
||||||
|
if announce_list is not None:
|
||||||
|
_validate_list(announce_list, 'announce-list')
|
||||||
|
|
||||||
|
for announce in announce_list:
|
||||||
|
_validate_list(announce, 'announce-list item')
|
||||||
|
|
||||||
|
_validate_bytes(announce[0], 'announce-list item item')
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_torrent_metadata(torrent_dict):
|
||||||
|
''' Validates a torrent metadata dict, raising AssertionError on errors '''
|
||||||
|
assert isinstance(torrent_dict, dict), 'torrent metadata is not a dict'
|
||||||
|
|
||||||
|
info_dict = torrent_dict.get('info')
|
||||||
|
assert info_dict is not None, 'no info_dict in torrent'
|
||||||
|
assert isinstance(info_dict, dict), 'info is not a dict'
|
||||||
|
|
||||||
|
encoding_bytes = torrent_dict.get('encoding', b'utf-8')
|
||||||
|
encoding = _validate_bytes(encoding_bytes, 'encoding', 'utf-8').lower()
|
||||||
|
|
||||||
|
name = info_dict.get('name')
|
||||||
|
_validate_bytes(name, 'name', test_decode=encoding)
|
||||||
|
|
||||||
|
piece_length = info_dict.get('piece length')
|
||||||
|
_validate_number(piece_length, 'piece length', check_positive=True)
|
||||||
|
|
||||||
|
pieces = info_dict.get('pieces')
|
||||||
|
_validate_bytes(pieces, 'pieces')
|
||||||
|
assert len(pieces) % 20 == 0, 'pieces length is not a multiple of 20'
|
||||||
|
|
||||||
|
files = info_dict.get('files')
|
||||||
|
if files is not None:
|
||||||
|
_validate_list(files, 'filelist')
|
||||||
|
|
||||||
|
for file_dict in files:
|
||||||
|
file_length = file_dict.get('length')
|
||||||
|
_validate_number(file_length, 'file length', check_positive_or_zero=True)
|
||||||
|
|
||||||
|
path_list = file_dict.get('path')
|
||||||
|
_validate_list(path_list, 'path')
|
||||||
|
for path_part in path_list:
|
||||||
|
_validate_bytes(path_part, 'path part', test_decode=encoding)
|
||||||
|
|
||||||
|
else:
|
||||||
|
length = info_dict.get('length')
|
||||||
|
_validate_number(length, 'length', check_positive=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_bytes(value, name='value', test_decode=None):
|
||||||
|
assert isinstance(value, bytes), name + ' is not bytes'
|
||||||
|
assert len(value) > 0, name + ' is empty'
|
||||||
|
if test_decode:
|
||||||
|
try:
|
||||||
|
return value.decode(test_decode)
|
||||||
|
except UnicodeError:
|
||||||
|
raise AssertionError(name + ' could not be decoded from ' + repr(test_decode))
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_number(value, name='value', check_positive=False, check_positive_or_zero=False):
|
||||||
|
assert isinstance(value, int), name + ' is not an int'
|
||||||
|
if check_positive_or_zero:
|
||||||
|
assert value >= 0, name + ' is less than 0'
|
||||||
|
elif check_positive:
|
||||||
|
assert value > 0, name + ' is not positive'
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_list(value, name='value', check_empty=False):
|
||||||
|
assert isinstance(value, list), name + ' is not a list'
|
||||||
|
if check_empty:
|
||||||
|
assert len(value) > 0, name + ' is empty'
|
||||||
|
|
||||||
|
|
||||||
|
def _debug_print_torrent_metadata(torrent_dict):
|
||||||
|
from pprint import pprint
|
||||||
|
|
||||||
|
# Temporarily remove 'pieces' from infodict for clean debug prints
|
||||||
|
info_dict = torrent_dict.get('info', {})
|
||||||
|
orig_pieces = info_dict.get('pieces')
|
||||||
|
|
||||||
|
info_dict['pieces'] = '<piece data>'
|
||||||
|
pprint(torrent_dict)
|
||||||
|
|
||||||
|
info_dict['pieces'] = orig_pieces
|
332
nyaa/models.py
Normal file
|
@ -0,0 +1,332 @@
|
||||||
|
from enum import Enum, IntEnum
|
||||||
|
from datetime import datetime
|
||||||
|
from nyaa import app, db
|
||||||
|
from nyaa.torrents import create_magnet
|
||||||
|
from sqlalchemy import func, ForeignKeyConstraint, Index
|
||||||
|
from sqlalchemy_utils import ChoiceType, EmailType, PasswordType
|
||||||
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
|
from sqlalchemy_fulltext import FullText
|
||||||
|
|
||||||
|
if app.config['USE_MYSQL']:
|
||||||
|
from sqlalchemy.dialects import mysql
|
||||||
|
BinaryType = mysql.BINARY
|
||||||
|
DescriptionTextType = mysql.TEXT
|
||||||
|
MediumBlobType = mysql.MEDIUMBLOB
|
||||||
|
COL_UTF8_GENERAL_CI = 'utf8_general_ci'
|
||||||
|
COL_UTF8MB4_BIN = 'utf8mb4_bin'
|
||||||
|
COL_ASCII_GENERAL_CI = 'ascii_general_ci'
|
||||||
|
else:
|
||||||
|
BinaryType = db.Binary
|
||||||
|
DescriptionTextType = db.String
|
||||||
|
MediumBlobType = db.BLOB
|
||||||
|
COL_UTF8_GENERAL_CI = 'NOCASE'
|
||||||
|
COL_UTF8MB4_BIN = None
|
||||||
|
COL_ASCII_GENERAL_CI = 'NOCASE'
|
||||||
|
|
||||||
|
|
||||||
|
class TorrentFlags(IntEnum):
|
||||||
|
NONE = 0
|
||||||
|
ANONYMOUS = 1
|
||||||
|
HIDDEN = 2
|
||||||
|
TRUSTED = 4
|
||||||
|
REMAKE = 8
|
||||||
|
COMPLETE = 16
|
||||||
|
DELETED = 32
|
||||||
|
|
||||||
|
|
||||||
|
class Torrent(db.Model):
|
||||||
|
__tablename__ = app.config['TABLE_PREFIX'] + 'torrents'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
info_hash = db.Column(BinaryType(length=20), unique=True, nullable=False, index=True)
|
||||||
|
display_name = db.Column(
|
||||||
|
db.String(length=255, collation=COL_UTF8_GENERAL_CI), nullable=False, index=True)
|
||||||
|
torrent_name = db.Column(db.String(length=255), nullable=False)
|
||||||
|
information = db.Column(db.String(length=255), nullable=False)
|
||||||
|
description = db.Column(DescriptionTextType(collation=COL_UTF8MB4_BIN), nullable=False)
|
||||||
|
|
||||||
|
filesize = db.Column(db.BIGINT, default=0, nullable=False, index=True)
|
||||||
|
encoding = db.Column(db.String(length=32), nullable=False)
|
||||||
|
flags = db.Column(db.Integer, default=0, nullable=False, index=True)
|
||||||
|
uploader_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
|
||||||
|
has_torrent = db.Column(db.Boolean, nullable=False, default=False)
|
||||||
|
|
||||||
|
created_time = db.Column(db.DateTime(timezone=False), default=datetime.utcnow, nullable=False)
|
||||||
|
updated_time = db.Column(db.DateTime(timezone=False),
|
||||||
|
default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||||
|
|
||||||
|
main_category_id = db.Column(db.Integer, db.ForeignKey(
|
||||||
|
app.config['TABLE_PREFIX'] + 'main_categories.id'), nullable=False)
|
||||||
|
sub_category_id = db.Column(db.Integer, nullable=False)
|
||||||
|
redirect = db.Column(db.Integer, db.ForeignKey(
|
||||||
|
app.config['TABLE_PREFIX'] + 'torrents.id'), nullable=True)
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
Index('uploader_flag_idx', 'uploader_id', 'flags'),
|
||||||
|
ForeignKeyConstraint(
|
||||||
|
['main_category_id', 'sub_category_id'],
|
||||||
|
[app.config['TABLE_PREFIX'] + 'sub_categories.main_category_id',
|
||||||
|
app.config['TABLE_PREFIX'] + 'sub_categories.id']
|
||||||
|
), {}
|
||||||
|
)
|
||||||
|
|
||||||
|
user = db.relationship('User', uselist=False, back_populates='torrents')
|
||||||
|
main_category = db.relationship('MainCategory', uselist=False,
|
||||||
|
back_populates='torrents', lazy="joined")
|
||||||
|
sub_category = db.relationship('SubCategory', uselist=False, backref='torrents', lazy="joined",
|
||||||
|
primaryjoin="and_(SubCategory.id == foreign(Torrent.sub_category_id), "
|
||||||
|
"SubCategory.main_category_id == Torrent.main_category_id)")
|
||||||
|
info = db.relationship('TorrentInfo', uselist=False, back_populates='torrent')
|
||||||
|
filelist = db.relationship('TorrentFilelist', uselist=False, back_populates='torrent')
|
||||||
|
stats = db.relationship('Statistic', uselist=False, back_populates='torrent', lazy='joined')
|
||||||
|
trackers = db.relationship('TorrentTrackers', uselist=True, lazy='joined')
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '<{0} #{1.id} \'{1.display_name}\' {1.filesize}b>'.format(type(self).__name__, self)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def magnet_uri(self):
|
||||||
|
return create_magnet(self)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def anonymous(self):
|
||||||
|
return self.flags & TorrentFlags.ANONYMOUS
|
||||||
|
|
||||||
|
@anonymous.setter
|
||||||
|
def anonymous(self, value):
|
||||||
|
self.flags = (self.flags & ~TorrentFlags.ANONYMOUS) | (value and TorrentFlags.ANONYMOUS)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hidden(self):
|
||||||
|
return self.flags & TorrentFlags.HIDDEN
|
||||||
|
|
||||||
|
@hidden.setter
|
||||||
|
def hidden(self, value):
|
||||||
|
self.flags = (self.flags & ~TorrentFlags.HIDDEN) | (value and TorrentFlags.HIDDEN)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def deleted(self):
|
||||||
|
return self.flags & TorrentFlags.DELETED
|
||||||
|
|
||||||
|
@deleted.setter
|
||||||
|
def deleted(self, value):
|
||||||
|
self.flags = (self.flags & ~TorrentFlags.DELETED) | (value and TorrentFlags.DELETED)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def trusted(self):
|
||||||
|
return self.flags & TorrentFlags.TRUSTED
|
||||||
|
|
||||||
|
@trusted.setter
|
||||||
|
def trusted(self, value):
|
||||||
|
self.flags = (self.flags & ~TorrentFlags.TRUSTED) | (value and TorrentFlags.TRUSTED)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def remake(self):
|
||||||
|
return self.flags & TorrentFlags.REMAKE
|
||||||
|
|
||||||
|
@remake.setter
|
||||||
|
def remake(self, value):
|
||||||
|
self.flags = (self.flags & ~TorrentFlags.REMAKE) | (value and TorrentFlags.REMAKE)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def complete(self):
|
||||||
|
return self.flags & TorrentFlags.COMPLETE
|
||||||
|
|
||||||
|
@complete.setter
|
||||||
|
def complete(self, value):
|
||||||
|
self.flags = (self.flags & ~TorrentFlags.COMPLETE) | (value and TorrentFlags.COMPLETE)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def by_id(cls, id):
|
||||||
|
return cls.query.get(id)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def by_info_hash(cls, info_hash):
|
||||||
|
return cls.query.filter_by(info_hash=info_hash).first()
|
||||||
|
|
||||||
|
|
||||||
|
class TorrentNameSearch(FullText, Torrent):
|
||||||
|
__fulltext_columns__ = ('display_name',)
|
||||||
|
|
||||||
|
|
||||||
|
class TorrentFilelist(db.Model):
|
||||||
|
__tablename__ = app.config['TABLE_PREFIX'] + 'torrents_filelist'
|
||||||
|
__table_args__ = {'mysql_row_format': 'COMPRESSED'}
|
||||||
|
|
||||||
|
torrent_id = db.Column(db.Integer, db.ForeignKey(
|
||||||
|
app.config['TABLE_PREFIX'] + 'torrents.id', ondelete="CASCADE"), primary_key=True)
|
||||||
|
filelist_blob = db.Column(MediumBlobType, nullable=True)
|
||||||
|
|
||||||
|
torrent = db.relationship('Torrent', uselist=False, back_populates='filelist')
|
||||||
|
|
||||||
|
|
||||||
|
class TorrentInfo(db.Model):
|
||||||
|
__tablename__ = app.config['TABLE_PREFIX'] + 'torrents_info'
|
||||||
|
__table_args__ = {'mysql_row_format': 'COMPRESSED'}
|
||||||
|
|
||||||
|
torrent_id = db.Column(db.Integer, db.ForeignKey(
|
||||||
|
app.config['TABLE_PREFIX'] + 'torrents.id', ondelete="CASCADE"), primary_key=True)
|
||||||
|
info_dict = db.Column(MediumBlobType, nullable=True)
|
||||||
|
|
||||||
|
torrent = db.relationship('Torrent', uselist=False, back_populates='info')
|
||||||
|
|
||||||
|
|
||||||
|
class Statistic(db.Model):
|
||||||
|
__tablename__ = app.config['TABLE_PREFIX'] + 'statistics'
|
||||||
|
|
||||||
|
torrent_id = db.Column(db.Integer, db.ForeignKey(
|
||||||
|
app.config['TABLE_PREFIX'] + 'torrents.id', ondelete="CASCADE"), primary_key=True)
|
||||||
|
|
||||||
|
seed_count = db.Column(db.Integer, default=0, nullable=False, index=True)
|
||||||
|
leech_count = db.Column(db.Integer, default=0, nullable=False, index=True)
|
||||||
|
download_count = db.Column(db.Integer, default=0, nullable=False, index=True)
|
||||||
|
last_updated = db.Column(db.DateTime(timezone=False))
|
||||||
|
|
||||||
|
torrent = db.relationship('Torrent', uselist=False, back_populates='stats')
|
||||||
|
|
||||||
|
|
||||||
|
class Trackers(db.Model):
|
||||||
|
__tablename__ = 'trackers'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
uri = db.Column(db.String(length=255, collation=COL_UTF8_GENERAL_CI), nullable=False, unique=True)
|
||||||
|
disabled = db.Column(db.Boolean, nullable=False, default=False)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def by_uri(cls, uri):
|
||||||
|
return cls.query.filter_by(uri=uri).first()
|
||||||
|
|
||||||
|
|
||||||
|
class TorrentTrackers(db.Model):
|
||||||
|
__tablename__ = app.config['TABLE_PREFIX'] + 'torrent_trackers'
|
||||||
|
|
||||||
|
torrent_id = db.Column(db.Integer, db.ForeignKey(app.config['TABLE_PREFIX'] + 'torrents.id', ondelete="CASCADE"), primary_key=True)
|
||||||
|
tracker_id = db.Column(db.Integer, db.ForeignKey('trackers.id', ondelete="CASCADE"), primary_key=True)
|
||||||
|
order = db.Column(db.Integer, nullable=False, index=True)
|
||||||
|
|
||||||
|
tracker = db.relationship('Trackers', uselist=False, lazy='joined')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def by_torrent_id(cls, torrent_id):
|
||||||
|
return cls.query.filter_by(torrent_id=torrent_id).order_by(cls.order.desc())
|
||||||
|
|
||||||
|
|
||||||
|
class MainCategory(db.Model):
|
||||||
|
__tablename__ = app.config['TABLE_PREFIX'] + 'main_categories'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
name = db.Column(db.String(length=64), nullable=False)
|
||||||
|
|
||||||
|
sub_categories = db.relationship('SubCategory', back_populates='main_category')
|
||||||
|
torrents = db.relationship('Torrent', back_populates='main_category')
|
||||||
|
|
||||||
|
def get_category_ids(self):
|
||||||
|
return (self.id, 0)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def id_as_string(self):
|
||||||
|
return '_'.join(str(x) for x in self.get_category_ids())
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def by_id(cls, id):
|
||||||
|
return cls.query.get(id)
|
||||||
|
|
||||||
|
|
||||||
|
class SubCategory(db.Model):
|
||||||
|
__tablename__ = app.config['TABLE_PREFIX'] + 'sub_categories'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
main_category_id = db.Column(db.Integer, db.ForeignKey(
|
||||||
|
app.config['TABLE_PREFIX'] + 'main_categories.id'), primary_key=True)
|
||||||
|
name = db.Column(db.String(length=64), nullable=False)
|
||||||
|
|
||||||
|
main_category = db.relationship('MainCategory', uselist=False, back_populates='sub_categories')
|
||||||
|
# torrents = db.relationship('Torrent', back_populates='sub_category'),
|
||||||
|
# primaryjoin="and_(Torrent.sub_category_id == foreign(SubCategory.id), "
|
||||||
|
# "Torrent.main_category_id == SubCategory.main_category_id)")
|
||||||
|
|
||||||
|
def get_category_ids(self):
|
||||||
|
return (self.main_category_id, self.id)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def id_as_string(self):
|
||||||
|
return '_'.join(str(x) for x in self.get_category_ids())
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def by_id(cls, id):
|
||||||
|
return cls.query.get(id)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def by_category_ids(cls, main_cat_id, sub_cat_id):
|
||||||
|
return cls.query.filter(cls.id == sub_cat_id, cls.main_category_id == main_cat_id).first()
|
||||||
|
|
||||||
|
|
||||||
|
class UserLevelType(IntEnum):
|
||||||
|
REGULAR = 0
|
||||||
|
TRUSTED = 1
|
||||||
|
ADMIN = 2
|
||||||
|
SUPERADMIN = 3
|
||||||
|
|
||||||
|
|
||||||
|
class UserStatusType(Enum):
|
||||||
|
INACTIVE = 0
|
||||||
|
ACTIVE = 1
|
||||||
|
BANNED = 2
|
||||||
|
|
||||||
|
|
||||||
|
class User(db.Model):
|
||||||
|
__tablename__ = 'users'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
username = db.Column(db.String(length=32, collation=COL_ASCII_GENERAL_CI),
|
||||||
|
unique=True, nullable=False)
|
||||||
|
email = db.Column(EmailType(length=255, collation=COL_ASCII_GENERAL_CI),
|
||||||
|
unique=True, nullable=True)
|
||||||
|
password_hash = db.Column(PasswordType(max_length=255, schemes=['argon2']), nullable=False)
|
||||||
|
status = db.Column(ChoiceType(UserStatusType, impl=db.Integer()), nullable=False)
|
||||||
|
level = db.Column(ChoiceType(UserLevelType, impl=db.Integer()), nullable=False)
|
||||||
|
|
||||||
|
created_time = db.Column(db.DateTime(timezone=False), default=datetime.utcnow)
|
||||||
|
last_login_date = db.Column(db.DateTime(timezone=False), default=None, nullable=True)
|
||||||
|
last_login_ip = db.Column(db.Binary(length=16), default=None, nullable=True)
|
||||||
|
|
||||||
|
torrents = db.relationship('Torrent', back_populates='user', lazy="dynamic")
|
||||||
|
# session = db.relationship('Session', uselist=False, back_populates='user')
|
||||||
|
|
||||||
|
def __init__(self, username, email, password):
|
||||||
|
self.username = username
|
||||||
|
self.email = email
|
||||||
|
self.password_hash = password
|
||||||
|
self.status = UserStatusType.INACTIVE
|
||||||
|
self.level = UserLevelType.REGULAR
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '<User %r>' % self.username
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def by_id(cls, id):
|
||||||
|
return cls.query.get(id)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def by_username(cls, username):
|
||||||
|
user = cls.query.filter_by(username=username).first()
|
||||||
|
return user
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def by_email(cls, email):
|
||||||
|
user = cls.query.filter_by(email=email).first()
|
||||||
|
return user
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_admin(self):
|
||||||
|
return self.level is UserLevelType.ADMIN or self.level is UserLevelType.SUPERADMIN
|
||||||
|
|
||||||
|
# class Session(db.Model):
|
||||||
|
# __tablename__ = 'sessions'
|
||||||
|
#
|
||||||
|
# session_id = db.Column(db.Integer, primary_key=True)
|
||||||
|
# user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||||
|
# login_ip = db.Column(db.Binary(length=16), nullable=True)
|
||||||
|
# login_date = db.Column(db.DateTime(timezone=False), nullable=True)
|
||||||
|
#
|
||||||
|
# user = db.relationship('User', back_populates='session')
|
767
nyaa/routes.py
Normal file
|
@ -0,0 +1,767 @@
|
||||||
|
import flask
|
||||||
|
from werkzeug.datastructures import CombinedMultiDict
|
||||||
|
from nyaa import app, db
|
||||||
|
from nyaa import models, forms
|
||||||
|
from nyaa import bencode, utils
|
||||||
|
from nyaa import torrents
|
||||||
|
from nyaa import api_handler
|
||||||
|
import config
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
from datetime import datetime
|
||||||
|
from collections import OrderedDict
|
||||||
|
import ipaddress
|
||||||
|
import os.path
|
||||||
|
import base64
|
||||||
|
from urllib.parse import quote
|
||||||
|
import sqlalchemy_fulltext.modes as FullTextMode
|
||||||
|
from sqlalchemy_fulltext import FullTextSearch
|
||||||
|
import shlex
|
||||||
|
from werkzeug import url_encode, secure_filename
|
||||||
|
from orderedset import OrderedSet
|
||||||
|
|
||||||
|
from itsdangerous import URLSafeSerializer, BadSignature
|
||||||
|
|
||||||
|
import smtplib
|
||||||
|
from email.mime.multipart import MIMEMultipart
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
from email.utils import formatdate
|
||||||
|
|
||||||
|
DEBUG_API = False
|
||||||
|
|
||||||
|
|
||||||
|
def redirect_url():
|
||||||
|
url = flask.request.args.get('next') or \
|
||||||
|
flask.request.referrer or \
|
||||||
|
'/'
|
||||||
|
if url == flask.request.url:
|
||||||
|
return '/'
|
||||||
|
return url
|
||||||
|
|
||||||
|
|
||||||
|
@app.template_global()
|
||||||
|
def modify_query(**new_values):
|
||||||
|
args = flask.request.args.copy()
|
||||||
|
|
||||||
|
for key, value in new_values.items():
|
||||||
|
args[key] = value
|
||||||
|
|
||||||
|
return '{}?{}'.format(flask.request.path, url_encode(args))
|
||||||
|
|
||||||
|
|
||||||
|
def search(term='', user=None, sort='id', order='desc', category='0_0', filter='0', page=1, rss=False, admin=False):
|
||||||
|
sort_keys = {
|
||||||
|
'id': models.Torrent.id,
|
||||||
|
'size': models.Torrent.filesize,
|
||||||
|
'name': models.Torrent.display_name,
|
||||||
|
'seeders': models.Statistic.seed_count,
|
||||||
|
'leechers': models.Statistic.leech_count,
|
||||||
|
'downloads': models.Statistic.download_count
|
||||||
|
}
|
||||||
|
|
||||||
|
sort_ = sort.lower()
|
||||||
|
if sort_ not in sort_keys:
|
||||||
|
flask.abort(400)
|
||||||
|
sort = sort_keys[sort]
|
||||||
|
|
||||||
|
order_keys = {
|
||||||
|
'desc': 'desc',
|
||||||
|
'asc': 'asc'
|
||||||
|
}
|
||||||
|
|
||||||
|
order_ = order.lower()
|
||||||
|
if order_ not in order_keys:
|
||||||
|
flask.abort(400)
|
||||||
|
|
||||||
|
filter_keys = {
|
||||||
|
'0': None,
|
||||||
|
'1': (models.TorrentFlags.REMAKE, False),
|
||||||
|
'2': (models.TorrentFlags.TRUSTED, True),
|
||||||
|
'3': (models.TorrentFlags.COMPLETE, True)
|
||||||
|
}
|
||||||
|
|
||||||
|
filter_ = filter.lower()
|
||||||
|
if filter_ not in filter_keys:
|
||||||
|
flask.abort(400)
|
||||||
|
filter = filter_keys[filter_]
|
||||||
|
|
||||||
|
if user:
|
||||||
|
user = models.User.by_id(user)
|
||||||
|
if not user:
|
||||||
|
flask.abort(404)
|
||||||
|
user = user.id
|
||||||
|
|
||||||
|
main_category = None
|
||||||
|
sub_category = None
|
||||||
|
main_cat_id = 0
|
||||||
|
sub_cat_id = 0
|
||||||
|
if category:
|
||||||
|
cat_match = re.match(r'^(\d+)_(\d+)$', category)
|
||||||
|
if not cat_match:
|
||||||
|
flask.abort(400)
|
||||||
|
|
||||||
|
main_cat_id = int(cat_match.group(1))
|
||||||
|
sub_cat_id = int(cat_match.group(2))
|
||||||
|
|
||||||
|
if main_cat_id > 0:
|
||||||
|
if sub_cat_id > 0:
|
||||||
|
sub_category = models.SubCategory.by_category_ids(main_cat_id, sub_cat_id)
|
||||||
|
else:
|
||||||
|
main_category = models.MainCategory.by_id(main_cat_id)
|
||||||
|
|
||||||
|
if not category:
|
||||||
|
flask.abort(400)
|
||||||
|
|
||||||
|
# Force sort by id desc if rss
|
||||||
|
if rss:
|
||||||
|
sort = sort_keys['id']
|
||||||
|
order = 'desc'
|
||||||
|
page = 1
|
||||||
|
|
||||||
|
same_user = False
|
||||||
|
if flask.g.user:
|
||||||
|
same_user = flask.g.user.id == user
|
||||||
|
|
||||||
|
if term:
|
||||||
|
query = db.session.query(models.TorrentNameSearch)
|
||||||
|
else:
|
||||||
|
query = models.Torrent.query
|
||||||
|
|
||||||
|
# Filter by user
|
||||||
|
if user:
|
||||||
|
query = query.filter(models.Torrent.uploader_id == user)
|
||||||
|
# If admin, show everything
|
||||||
|
if not admin:
|
||||||
|
# If user is not logged in or the accessed feed doesn't belong to user,
|
||||||
|
# hide anonymous torrents belonging to the queried user
|
||||||
|
if not same_user:
|
||||||
|
query = query.filter(models.Torrent.flags.op('&')(
|
||||||
|
int(models.TorrentFlags.ANONYMOUS | models.TorrentFlags.DELETED)).is_(False))
|
||||||
|
|
||||||
|
if main_category:
|
||||||
|
query = query.filter(models.Torrent.main_category_id == main_cat_id)
|
||||||
|
elif sub_category:
|
||||||
|
query = query.filter((models.Torrent.main_category_id == main_cat_id) &
|
||||||
|
(models.Torrent.sub_category_id == sub_cat_id))
|
||||||
|
|
||||||
|
if filter:
|
||||||
|
query = query.filter(models.Torrent.flags.op('&')(int(filter[0])).is_(filter[1]))
|
||||||
|
|
||||||
|
# If admin, show everything
|
||||||
|
if not admin:
|
||||||
|
query = query.filter(models.Torrent.flags.op('&')(
|
||||||
|
int(models.TorrentFlags.HIDDEN | models.TorrentFlags.DELETED)).is_(False))
|
||||||
|
|
||||||
|
if term:
|
||||||
|
for item in shlex.split(term, posix=False):
|
||||||
|
if len(item) >= 2:
|
||||||
|
query = query.filter(FullTextSearch(
|
||||||
|
item, models.TorrentNameSearch, FullTextMode.NATURAL))
|
||||||
|
|
||||||
|
# Sort and order
|
||||||
|
if sort.class_ != models.Torrent:
|
||||||
|
query = query.join(sort.class_)
|
||||||
|
|
||||||
|
query = query.order_by(getattr(sort, order)())
|
||||||
|
|
||||||
|
if rss:
|
||||||
|
query = query.limit(app.config['RESULTS_PER_PAGE'])
|
||||||
|
else:
|
||||||
|
query = query.paginate_faste(page, per_page=app.config['RESULTS_PER_PAGE'], step=5)
|
||||||
|
|
||||||
|
return query
|
||||||
|
|
||||||
|
|
||||||
|
@app.errorhandler(404)
|
||||||
|
def not_found(error):
|
||||||
|
return flask.render_template('404.html'), 404
|
||||||
|
|
||||||
|
|
||||||
|
@app.before_request
|
||||||
|
def before_request():
|
||||||
|
flask.g.user = None
|
||||||
|
if 'user_id' in flask.session:
|
||||||
|
user = models.User.by_id(flask.session['user_id'])
|
||||||
|
if not user:
|
||||||
|
return logout()
|
||||||
|
|
||||||
|
flask.g.user = user
|
||||||
|
flask.session.permanent = True
|
||||||
|
flask.session.modified = True
|
||||||
|
|
||||||
|
if flask.g.user.status == models.UserStatusType.BANNED:
|
||||||
|
return 'You are banned.', 403
|
||||||
|
|
||||||
|
|
||||||
|
def _generate_query_string(term, category, filter, user):
|
||||||
|
params = {}
|
||||||
|
if term:
|
||||||
|
params['q'] = str(term)
|
||||||
|
if category:
|
||||||
|
params['c'] = str(category)
|
||||||
|
if filter:
|
||||||
|
params['f'] = str(filter)
|
||||||
|
if user:
|
||||||
|
params['u'] = str(user)
|
||||||
|
return params
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/rss', defaults={'rss': True})
|
||||||
|
@app.route('/', defaults={'rss': False})
|
||||||
|
def home(rss):
|
||||||
|
if flask.request.args.get('page') == 'rss':
|
||||||
|
rss = True
|
||||||
|
|
||||||
|
term = flask.request.args.get('q')
|
||||||
|
sort = flask.request.args.get('s')
|
||||||
|
order = flask.request.args.get('o')
|
||||||
|
category = flask.request.args.get('c')
|
||||||
|
filter = flask.request.args.get('f')
|
||||||
|
user_name = flask.request.args.get('u')
|
||||||
|
page = flask.request.args.get('p')
|
||||||
|
if page:
|
||||||
|
page = int(page)
|
||||||
|
|
||||||
|
user_id = None
|
||||||
|
if user_name:
|
||||||
|
user = models.User.by_username(user_name)
|
||||||
|
if not user:
|
||||||
|
flask.abort(404)
|
||||||
|
user_id = user.id
|
||||||
|
|
||||||
|
query_args = {
|
||||||
|
'term': term or '',
|
||||||
|
'user': user_id,
|
||||||
|
'sort': sort or 'id',
|
||||||
|
'order': order or 'desc',
|
||||||
|
'category': category or '0_0',
|
||||||
|
'filter': filter or '0',
|
||||||
|
'page': page or 1,
|
||||||
|
'rss': rss
|
||||||
|
}
|
||||||
|
|
||||||
|
# God mode
|
||||||
|
if flask.g.user and flask.g.user.is_admin:
|
||||||
|
query_args['admin'] = True
|
||||||
|
|
||||||
|
query = search(**query_args)
|
||||||
|
|
||||||
|
if rss:
|
||||||
|
return render_rss('/', query)
|
||||||
|
else:
|
||||||
|
rss_query_string = _generate_query_string(term, category, filter, user_name)
|
||||||
|
return flask.render_template('home.html',
|
||||||
|
torrent_query=query,
|
||||||
|
search=query_args,
|
||||||
|
rss_filter=rss_query_string)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/user/<user_name>')
|
||||||
|
def view_user(user_name):
|
||||||
|
user = models.User.by_username(user_name)
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
flask.abort(404)
|
||||||
|
|
||||||
|
term = flask.request.args.get('q')
|
||||||
|
sort = flask.request.args.get('s')
|
||||||
|
order = flask.request.args.get('o')
|
||||||
|
category = flask.request.args.get('c')
|
||||||
|
filter = flask.request.args.get('f')
|
||||||
|
page = flask.request.args.get('p')
|
||||||
|
if page:
|
||||||
|
page = int(page)
|
||||||
|
|
||||||
|
query_args = {
|
||||||
|
'term': term or '',
|
||||||
|
'user': user.id,
|
||||||
|
'sort': sort or 'id',
|
||||||
|
'order': order or 'desc',
|
||||||
|
'category': category or '0_0',
|
||||||
|
'filter': filter or '0',
|
||||||
|
'page': page or 1,
|
||||||
|
'rss': False
|
||||||
|
}
|
||||||
|
|
||||||
|
# God mode
|
||||||
|
if flask.g.user and flask.g.user.is_admin:
|
||||||
|
query_args['admin'] = True
|
||||||
|
|
||||||
|
query = search(**query_args)
|
||||||
|
|
||||||
|
rss_query_string = _generate_query_string(term, category, filter, user_name)
|
||||||
|
return flask.render_template('user.html',
|
||||||
|
torrent_query=query,
|
||||||
|
search=query_args,
|
||||||
|
user=user,
|
||||||
|
user_page=True,
|
||||||
|
rss_filter=rss_query_string)
|
||||||
|
|
||||||
|
|
||||||
|
@app.template_filter('rfc822')
|
||||||
|
def _jinja2_filter_rfc822(date, fmt=None):
|
||||||
|
return formatdate(float(date.strftime('%s')))
|
||||||
|
|
||||||
|
|
||||||
|
def render_rss(label, query):
|
||||||
|
rss_xml = flask.render_template('rss.xml',
|
||||||
|
term=label,
|
||||||
|
site_url=flask.request.url_root,
|
||||||
|
query=query)
|
||||||
|
response = flask.make_response(rss_xml)
|
||||||
|
response.headers['Content-Type'] = 'application/xml'
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
#@app.route('/about', methods=['GET'])
|
||||||
|
# def about():
|
||||||
|
# return flask.render_template('about.html')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/login', methods=['GET', 'POST'])
|
||||||
|
def login():
|
||||||
|
if flask.g.user:
|
||||||
|
return flask.redirect(redirect_url())
|
||||||
|
|
||||||
|
form = forms.LoginForm(flask.request.form)
|
||||||
|
if flask.request.method == 'POST' and form.validate():
|
||||||
|
username = form.username.data.strip()
|
||||||
|
password = form.password.data
|
||||||
|
user = models.User.by_username(username)
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
user = models.User.by_email(username)
|
||||||
|
|
||||||
|
if not user or password != user.password_hash or user.status == models.UserStatusType.INACTIVE:
|
||||||
|
flask.flash(flask.Markup(
|
||||||
|
'<strong>Login failed!</strong> Incorrect username or password.'), 'danger')
|
||||||
|
return flask.redirect(flask.url_for('login'))
|
||||||
|
|
||||||
|
user.last_login_date = datetime.utcnow()
|
||||||
|
user.last_login_ip = ipaddress.ip_address(flask.request.remote_addr).packed
|
||||||
|
db.session.add(user)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
flask.g.user = user
|
||||||
|
flask.session['user_id'] = user.id
|
||||||
|
|
||||||
|
return flask.redirect(redirect_url())
|
||||||
|
|
||||||
|
return flask.render_template('login.html', form=form)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/logout')
|
||||||
|
def logout():
|
||||||
|
flask.g.user = None
|
||||||
|
flask.session.permanent = False
|
||||||
|
flask.session.modified = False
|
||||||
|
|
||||||
|
response = flask.make_response(flask.redirect(redirect_url()))
|
||||||
|
response.set_cookie(app.session_cookie_name, expires=0)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/register', methods=['GET', 'POST'])
|
||||||
|
def register():
|
||||||
|
if flask.g.user:
|
||||||
|
return flask.redirect(redirect_url())
|
||||||
|
|
||||||
|
form = forms.RegisterForm(flask.request.form)
|
||||||
|
if flask.request.method == 'POST' and form.validate():
|
||||||
|
user = models.User(username=form.username.data.strip(),
|
||||||
|
email=form.email.data.strip(), password=form.password.data)
|
||||||
|
user.last_login_ip = ipaddress.ip_address(flask.request.remote_addr).packed
|
||||||
|
db.session.add(user)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
if config.USE_EMAIL_VERIFICATION: # force verification, enable email
|
||||||
|
activ_link = get_activation_link(user)
|
||||||
|
send_verification_email(user.email, activ_link)
|
||||||
|
return flask.render_template('waiting.html')
|
||||||
|
else: # disable verification, set user as active and auto log in
|
||||||
|
user.status = models.UserStatusType.ACTIVE
|
||||||
|
db.session.add(user)
|
||||||
|
db.session.commit()
|
||||||
|
flask.g.user = user
|
||||||
|
flask.session['user_id'] = user.id
|
||||||
|
return flask.redirect(redirect_url())
|
||||||
|
|
||||||
|
return flask.render_template('register.html', form=form)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/profile', methods=['GET', 'POST'])
|
||||||
|
def profile():
|
||||||
|
if not flask.g.user:
|
||||||
|
return flask.redirect('/') # so we dont get stuck in infinite loop when signing out
|
||||||
|
|
||||||
|
form = forms.ProfileForm(flask.request.form)
|
||||||
|
if flask.request.method == 'POST' and form.validate():
|
||||||
|
user = flask.g.user
|
||||||
|
new_email = form.email.data
|
||||||
|
new_password = form.new_password.data
|
||||||
|
|
||||||
|
if new_email:
|
||||||
|
user.email = form.email.data
|
||||||
|
|
||||||
|
if new_password:
|
||||||
|
if form.current_password.data != user.password_hash:
|
||||||
|
flask.flash(flask.Markup(
|
||||||
|
'<strong>Password change failed!</strong> Incorrect password.'), 'danger')
|
||||||
|
return flask.redirect('/profile')
|
||||||
|
user.password_hash = form.new_password.data
|
||||||
|
|
||||||
|
db.session.add(user)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
flask.g.user = user
|
||||||
|
flask.session['user_id'] = user.id
|
||||||
|
|
||||||
|
return flask.render_template('profile.html', form=form)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/user/activate/<payload>')
|
||||||
|
def activate_user(payload):
|
||||||
|
s = get_serializer()
|
||||||
|
try:
|
||||||
|
user_id = s.loads(payload)
|
||||||
|
except BadSignature:
|
||||||
|
flask.abort(404)
|
||||||
|
|
||||||
|
user = models.User.by_id(user_id)
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
flask.abort(404)
|
||||||
|
|
||||||
|
user.status = models.UserStatusType.ACTIVE
|
||||||
|
|
||||||
|
db.session.add(user)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return flask.redirect('/login')
|
||||||
|
|
||||||
|
|
||||||
|
@utils.cached_function
|
||||||
|
def _create_upload_category_choices():
|
||||||
|
''' Turns categories in the database into a list of (id, name)s '''
|
||||||
|
choices = [('', '[Select a category]')]
|
||||||
|
for main_cat in models.MainCategory.query.order_by(models.MainCategory.id):
|
||||||
|
choices.append((main_cat.id_as_string, main_cat.name, True))
|
||||||
|
for sub_cat in main_cat.sub_categories:
|
||||||
|
choices.append((sub_cat.id_as_string, ' - ' + sub_cat.name))
|
||||||
|
return choices
|
||||||
|
|
||||||
|
|
||||||
|
def _replace_utf8_values(dict_or_list):
|
||||||
|
''' Will replace 'property' with 'property.utf-8' and remove latter if it exists.
|
||||||
|
Thanks, bitcomet! :/ '''
|
||||||
|
did_change = False
|
||||||
|
if isinstance(dict_or_list, dict):
|
||||||
|
for key in [key for key in dict_or_list.keys() if key.endswith('.utf-8')]:
|
||||||
|
dict_or_list[key.replace('.utf-8', '')] = dict_or_list.pop(key)
|
||||||
|
did_change = True
|
||||||
|
for value in dict_or_list.values():
|
||||||
|
did_change = _replace_utf8_values(value) or did_change
|
||||||
|
elif isinstance(dict_or_list, list):
|
||||||
|
for item in dict_or_list:
|
||||||
|
did_change = _replace_utf8_values(item) or did_change
|
||||||
|
return did_change
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/upload', methods=['GET', 'POST'])
|
||||||
|
def upload():
|
||||||
|
current_user = flask.g.user
|
||||||
|
form = forms.UploadForm(CombinedMultiDict((flask.request.files, flask.request.form)))
|
||||||
|
form.category.choices = _create_upload_category_choices()
|
||||||
|
if flask.request.method == 'POST' and form.validate():
|
||||||
|
torrent_data = form.torrent_file.parsed_data
|
||||||
|
|
||||||
|
# The torrent has been validated and is safe to access with ['foo'] etc - all relevant
|
||||||
|
# keys and values have been checked for (see UploadForm in forms.py for details)
|
||||||
|
info_dict = torrent_data.torrent_dict['info']
|
||||||
|
|
||||||
|
changed_to_utf8 = _replace_utf8_values(torrent_data.torrent_dict)
|
||||||
|
|
||||||
|
# Use uploader-given name or grab it from the torrent
|
||||||
|
display_name = form.display_name.data.strip() or info_dict['name'].decode('utf8').strip()
|
||||||
|
information = (form.information.data or '').strip()
|
||||||
|
description = (form.description.data or '').strip()
|
||||||
|
|
||||||
|
torrent_filesize = info_dict.get('length') or sum(
|
||||||
|
f['length'] for f in info_dict.get('files'))
|
||||||
|
|
||||||
|
# In case no encoding, assume UTF-8.
|
||||||
|
torrent_encoding = torrent_data.torrent_dict.get('encoding', b'utf-8').decode('utf-8')
|
||||||
|
|
||||||
|
torrent = models.Torrent(info_hash=torrent_data.info_hash,
|
||||||
|
display_name=display_name,
|
||||||
|
torrent_name=torrent_data.filename,
|
||||||
|
information=information,
|
||||||
|
description=description,
|
||||||
|
encoding=torrent_encoding,
|
||||||
|
filesize=torrent_filesize,
|
||||||
|
user=current_user)
|
||||||
|
|
||||||
|
# Store bencoded info_dict
|
||||||
|
torrent.info = models.TorrentInfo(info_dict=torrent_data.bencoded_info_dict)
|
||||||
|
torrent.stats = models.Statistic()
|
||||||
|
torrent.has_torrent = True
|
||||||
|
|
||||||
|
# Fields with default value will be None before first commit, so set .flags
|
||||||
|
torrent.flags = 0
|
||||||
|
|
||||||
|
torrent.anonymous = form.is_anonymous.data if current_user else True
|
||||||
|
torrent.hidden = form.is_hidden.data
|
||||||
|
torrent.remake = form.is_remake.data
|
||||||
|
torrent.complete = form.is_complete.data
|
||||||
|
# Copy trusted status from user if possible
|
||||||
|
torrent.trusted = (current_user.level >=
|
||||||
|
models.UserLevelType.TRUSTED) if current_user else False
|
||||||
|
|
||||||
|
# Set category ids
|
||||||
|
torrent.main_category_id, torrent.sub_category_id = form.category.parsed_data.get_category_ids()
|
||||||
|
# print('Main cat id: {0}, Sub cat id: {1}'.format(
|
||||||
|
# torrent.main_category_id, torrent.sub_category_id))
|
||||||
|
|
||||||
|
# To simplify parsing the filelist, turn single-file torrent into a list
|
||||||
|
torrent_filelist = info_dict.get('files')
|
||||||
|
|
||||||
|
used_path_encoding = changed_to_utf8 and 'utf-8' or torrent_encoding
|
||||||
|
|
||||||
|
parsed_file_tree = dict()
|
||||||
|
if not torrent_filelist:
|
||||||
|
# If single-file, the root will be the file-tree (no directory)
|
||||||
|
file_tree_root = parsed_file_tree
|
||||||
|
torrent_filelist = [{'length': torrent_filesize, 'path': [info_dict['name']]}]
|
||||||
|
else:
|
||||||
|
# If multi-file, use the directory name as root for files
|
||||||
|
file_tree_root = parsed_file_tree.setdefault(
|
||||||
|
info_dict['name'].decode(used_path_encoding), {})
|
||||||
|
|
||||||
|
# Parse file dicts into a tree
|
||||||
|
for file_dict in torrent_filelist:
|
||||||
|
# Decode path parts from utf8-bytes
|
||||||
|
path_parts = [path_part.decode(used_path_encoding) for path_part in file_dict['path']]
|
||||||
|
|
||||||
|
filename = path_parts.pop()
|
||||||
|
current_directory = file_tree_root
|
||||||
|
|
||||||
|
for directory in path_parts:
|
||||||
|
current_directory = current_directory.setdefault(directory, {})
|
||||||
|
|
||||||
|
current_directory[filename] = file_dict['length']
|
||||||
|
|
||||||
|
parsed_file_tree = utils.sorted_pathdict(parsed_file_tree)
|
||||||
|
|
||||||
|
json_bytes = json.dumps(parsed_file_tree, separators=(',', ':')).encode('utf8')
|
||||||
|
torrent.filelist = models.TorrentFilelist(filelist_blob=json_bytes)
|
||||||
|
|
||||||
|
db.session.add(torrent)
|
||||||
|
db.session.flush()
|
||||||
|
|
||||||
|
# Store the users trackers
|
||||||
|
trackers = OrderedSet()
|
||||||
|
announce = torrent_data.torrent_dict.get('announce', b'').decode('ascii')
|
||||||
|
if announce:
|
||||||
|
trackers.add(announce)
|
||||||
|
|
||||||
|
# List of lists with single item
|
||||||
|
announce_list = torrent_data.torrent_dict.get('announce-list', [])
|
||||||
|
for announce in announce_list:
|
||||||
|
trackers.add(announce[0].decode('ascii'))
|
||||||
|
|
||||||
|
# Remove our trackers, maybe? TODO ?
|
||||||
|
|
||||||
|
# Search for/Add trackers in DB
|
||||||
|
db_trackers = OrderedSet()
|
||||||
|
for announce in trackers:
|
||||||
|
tracker = models.Trackers.by_uri(announce)
|
||||||
|
|
||||||
|
# Insert new tracker if not found
|
||||||
|
if not tracker:
|
||||||
|
tracker = models.Trackers(uri=announce)
|
||||||
|
db.session.add(tracker)
|
||||||
|
|
||||||
|
db_trackers.add(tracker)
|
||||||
|
|
||||||
|
db.session.flush()
|
||||||
|
|
||||||
|
# Store tracker refs in DB
|
||||||
|
for order, tracker in enumerate(db_trackers):
|
||||||
|
torrent_tracker = models.TorrentTrackers(torrent_id=torrent.id,
|
||||||
|
tracker_id=tracker.id, order=order)
|
||||||
|
db.session.add(torrent_tracker)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
torrent_file = form.torrent_file.data
|
||||||
|
if app.config.get('BACKUP_TORRENT_FOLDER'):
|
||||||
|
torrent_file.seek(0, 0)
|
||||||
|
torrent_path = os.path.join(app.config['BACKUP_TORRENT_FOLDER'], '{}.{}'.format(torrent.id, secure_filename(torrent_file.filename)))
|
||||||
|
torrent_file.save(torrent_path)
|
||||||
|
torrent_file.close()
|
||||||
|
|
||||||
|
return flask.redirect('/view/' + str(torrent.id))
|
||||||
|
else:
|
||||||
|
return flask.render_template('upload.html', form=form, user=flask.g.user)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/view/<int:torrent_id>')
|
||||||
|
def view_torrent(torrent_id):
|
||||||
|
torrent = models.Torrent.by_id(torrent_id)
|
||||||
|
|
||||||
|
if not torrent:
|
||||||
|
flask.abort(404)
|
||||||
|
|
||||||
|
if torrent.deleted and (not flask.g.user or not flask.g.user.is_admin):
|
||||||
|
flask.abort(404)
|
||||||
|
|
||||||
|
if flask.g.user:
|
||||||
|
can_edit = flask.g.user is torrent.user or flask.g.user.is_admin
|
||||||
|
else:
|
||||||
|
can_edit = False
|
||||||
|
|
||||||
|
files = None
|
||||||
|
if torrent.filelist:
|
||||||
|
files = utils.flattenDict(json.loads(torrent.filelist.filelist_blob.decode('utf-8')))
|
||||||
|
|
||||||
|
return flask.render_template('view.html', torrent=torrent,
|
||||||
|
files=files,
|
||||||
|
can_edit=can_edit)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/view/<int:torrent_id>/edit', methods=['GET', 'POST'])
|
||||||
|
def edit_torrent(torrent_id):
|
||||||
|
torrent = models.Torrent.by_id(torrent_id)
|
||||||
|
form = forms.EditForm(flask.request.form)
|
||||||
|
form.category.choices = _create_upload_category_choices()
|
||||||
|
category = str(torrent.main_category_id) + "_" + str(torrent.sub_category_id)
|
||||||
|
|
||||||
|
if not torrent:
|
||||||
|
flask.abort(404)
|
||||||
|
|
||||||
|
if torrent.deleted and (not flask.g.user or not flask.g.user.is_admin):
|
||||||
|
flask.abort(404)
|
||||||
|
|
||||||
|
if not flask.g.user or (flask.g.user is not torrent.user and not flask.g.user.is_admin):
|
||||||
|
flask.abort(403)
|
||||||
|
|
||||||
|
if flask.request.method == 'POST' and form.validate():
|
||||||
|
# Form has been sent, edit torrent with data.
|
||||||
|
torrent.main_category_id, torrent.sub_category_id = form.category.parsed_data.get_category_ids()
|
||||||
|
torrent.display_name = (form.display_name.data or '').strip()
|
||||||
|
torrent.information = (form.information.data or '').strip()
|
||||||
|
torrent.description = (form.description.data or '').strip()
|
||||||
|
if flask.g.user.is_admin:
|
||||||
|
torrent.deleted = form.is_deleted.data
|
||||||
|
torrent.hidden = form.is_hidden.data
|
||||||
|
torrent.remake = form.is_remake.data
|
||||||
|
torrent.complete = form.is_complete.data
|
||||||
|
torrent.anonymous = form.is_anonymous.data
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return flask.redirect('/view/' + str(torrent_id))
|
||||||
|
else:
|
||||||
|
# Setup form with pre-formatted form.
|
||||||
|
form.category.data = category
|
||||||
|
form.display_name.data = torrent.display_name
|
||||||
|
form.information.data = torrent.information
|
||||||
|
form.description.data = torrent.description
|
||||||
|
form.is_hidden.data = torrent.hidden
|
||||||
|
if flask.g.user.is_admin:
|
||||||
|
form.is_deleted.data = torrent.deleted
|
||||||
|
form.is_remake.data = torrent.remake
|
||||||
|
form.is_complete.data = torrent.complete
|
||||||
|
form.is_anonymous.data = torrent.anonymous
|
||||||
|
|
||||||
|
return flask.render_template('edit.html', form=form, torrent=torrent, admin=flask.g.user.is_admin)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/view/<int:torrent_id>/magnet')
|
||||||
|
def redirect_magnet(torrent_id):
|
||||||
|
torrent = models.Torrent.by_id(torrent_id)
|
||||||
|
|
||||||
|
if not torrent:
|
||||||
|
flask.abort(404)
|
||||||
|
|
||||||
|
return flask.redirect(torrents.create_magnet(torrent))
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/view/<int:torrent_id>/torrent')
|
||||||
|
def download_torrent(torrent_id):
|
||||||
|
torrent = models.Torrent.by_id(torrent_id)
|
||||||
|
|
||||||
|
if not torrent:
|
||||||
|
flask.abort(404)
|
||||||
|
|
||||||
|
resp = flask.Response(_get_cached_torrent_file(torrent))
|
||||||
|
resp.headers['Content-Type'] = 'application/x-bittorrent'
|
||||||
|
resp.headers['Content-Disposition'] = 'inline; filename*=UTF-8\'\'{}'.format(
|
||||||
|
quote(torrent.torrent_name.encode('utf-8')))
|
||||||
|
|
||||||
|
return resp
|
||||||
|
|
||||||
|
|
||||||
|
def _get_cached_torrent_file(torrent):
|
||||||
|
# Note: obviously temporary
|
||||||
|
cached_torrent = os.path.join(app.config['BASE_DIR'],
|
||||||
|
'torrent_cache', str(torrent.id) + '.torrent')
|
||||||
|
if not os.path.exists(cached_torrent):
|
||||||
|
with open(cached_torrent, 'wb') as out_file:
|
||||||
|
out_file.write(torrents.create_bencoded_torrent(torrent))
|
||||||
|
|
||||||
|
return open(cached_torrent, 'rb')
|
||||||
|
|
||||||
|
|
||||||
|
def get_serializer(secret_key=None):
|
||||||
|
if secret_key is None:
|
||||||
|
secret_key = app.secret_key
|
||||||
|
return URLSafeSerializer(secret_key)
|
||||||
|
|
||||||
|
|
||||||
|
def get_activation_link(user):
|
||||||
|
s = get_serializer()
|
||||||
|
payload = s.dumps(user.id)
|
||||||
|
return flask.url_for('activate_user', payload=payload, _external=True)
|
||||||
|
|
||||||
|
|
||||||
|
def send_verification_email(to_address, activ_link):
|
||||||
|
''' this is until we have our own mail server, obviously. This can be greatly cut down if on same machine.
|
||||||
|
probably can get rid of all but msg formatting/building, init line and sendmail line if local SMTP server '''
|
||||||
|
|
||||||
|
msg_body = 'Please click on: ' + activ_link + ' to activate your account.\n\n\nUnsubscribe:'
|
||||||
|
|
||||||
|
msg = MIMEMultipart()
|
||||||
|
msg['Subject'] = 'Verification Link'
|
||||||
|
msg['From'] = config.MAIL_FROM_ADDRESS
|
||||||
|
msg['To'] = to_address
|
||||||
|
msg.attach(MIMEText(msg_body, 'plain'))
|
||||||
|
|
||||||
|
server = smtplib.SMTP(config.SMTP_SERVER, config.SMTP_PORT)
|
||||||
|
server.set_debuglevel(1)
|
||||||
|
server.ehlo()
|
||||||
|
server.starttls()
|
||||||
|
server.ehlo()
|
||||||
|
server.login(config.SMTP_USERNAME, config.SMTP_PASSWORD)
|
||||||
|
server.sendmail(config.SMTP_USERNAME, to_address, msg.as_string())
|
||||||
|
server.quit()
|
||||||
|
|
||||||
|
|
||||||
|
#################################### STATIC PAGES ####################################
|
||||||
|
@app.route('/rules', methods=['GET'])
|
||||||
|
def site_rules():
|
||||||
|
return flask.render_template('rules.html')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/help', methods=['GET'])
|
||||||
|
def site_help():
|
||||||
|
return flask.render_template('help.html')
|
||||||
|
|
||||||
|
|
||||||
|
#################################### API ROUTES ####################################
|
||||||
|
# DISABLED FOR NOW
|
||||||
|
@app.route('/api/upload', methods = ['POST'])
|
||||||
|
def api_upload():
|
||||||
|
api_response = api_handler.api_upload(flask.request)
|
||||||
|
return api_response
|
BIN
nyaa/static/css/bootstrap-dark.min.css
vendored
Normal file
BIN
nyaa/static/css/bootstrap-select.min.css
vendored
Normal file
BIN
nyaa/static/css/bootstrap-theme.min.css
vendored
Normal file
BIN
nyaa/static/css/bootstrap.min.css
vendored
Normal file
BIN
nyaa/static/css/font-awesome.min.css
vendored
Normal file
85
nyaa/static/css/main.css
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
.panel-heading-collapse a:after {
|
||||||
|
font-family:'Glyphicons Halflings';
|
||||||
|
content:"\e114";
|
||||||
|
float: right;
|
||||||
|
color: grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-heading-collapse a.collapsed:after {
|
||||||
|
content:"\e080";
|
||||||
|
}
|
||||||
|
|
||||||
|
.torrent-list > tbody > tr > td {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.torrent-list thead th {
|
||||||
|
position: relative;
|
||||||
|
background-image: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.torrent-list thead th a {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
text-decoration: none;
|
||||||
|
z-index: 10;
|
||||||
|
/* IE Workaround */
|
||||||
|
background-color: white;
|
||||||
|
opacity: 0;
|
||||||
|
filter: alpha(opacity=1);
|
||||||
|
}
|
||||||
|
|
||||||
|
table.torrent-list thead th.sorting:after,
|
||||||
|
table.torrent-list thead th.sorting_asc:after,
|
||||||
|
table.torrent-list thead th.sorting_desc:after {
|
||||||
|
position: absolute;
|
||||||
|
top: 12px;
|
||||||
|
right: 8px;
|
||||||
|
display: block;
|
||||||
|
font-family: FontAwesome;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.torrent-list thead th.sorting:after {
|
||||||
|
content: "\f0dc";
|
||||||
|
color: #808080;
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
table.torrent-list thead th.sorting_asc:after {
|
||||||
|
content: "\f0de";
|
||||||
|
}
|
||||||
|
table.torrent-list thead th.sorting_desc:after {
|
||||||
|
content: "\f0dd";
|
||||||
|
}
|
||||||
|
|
||||||
|
#torrent-description img {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table > tbody > tr.deleted > td, .table > tbody > tr.deleted > th, .table > tbody > tr > td.deleted, .table > tbody > tr > th.deleted, .table > tfoot > tr.deleted > td, .table > tfoot > tr.deleted > th, .table > tfoot > tr > td.deleted, .table > tfoot > tr > th.deleted, .table > thead > tr.deleted > td, .table > thead > tr.deleted > th, .table > thead > tr > td.deleted, .table > thead > tr > th.deleted {
|
||||||
|
background-color:#9e9e9e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-hover > tbody > tr.deleted:hover > td, .table-hover > tbody > tr.deleted:hover > th, .table-hover > tbody > tr:hover > .deleted, .table-hover > tbody > tr > td.deleted:hover, .table-hover > tbody > tr > th.deleted:hover {
|
||||||
|
background-color:#bdbdbd;
|
||||||
|
}
|
||||||
|
.panel-deleted {
|
||||||
|
border-color:#757575;
|
||||||
|
}
|
||||||
|
.panel-deleted > .panel-heading {
|
||||||
|
color:#212121;
|
||||||
|
background-color:#9e9e9e;
|
||||||
|
border-color:#757575;
|
||||||
|
}
|
||||||
|
.panel-deleted > .panel-heading + .panel-collapse > .panel-body {
|
||||||
|
border-top-color:#757575;
|
||||||
|
}
|
||||||
|
.panel-deleted > .panel-heading .badge {
|
||||||
|
color:#9e9e9e;
|
||||||
|
background-color:#212121;
|
||||||
|
}
|
||||||
|
.panel-deleted > .panel-footer + .panel-collapse > .panel-body {
|
||||||
|
border-bottom-color:#757575;
|
||||||
|
}
|
BIN
nyaa/static/favicon.png
Normal file
After Width: | Height: | Size: 2 KiB |
BIN
nyaa/static/fonts/FontAwesome.otf
Normal file
BIN
nyaa/static/fonts/fontawesome-webfont.eot
Normal file
BIN
nyaa/static/fonts/fontawesome-webfont.svg
Normal file
After Width: | Height: | Size: 434 KiB |
BIN
nyaa/static/fonts/fontawesome-webfont.ttf
Normal file
BIN
nyaa/static/fonts/fontawesome-webfont.woff
Normal file
BIN
nyaa/static/fonts/fontawesome-webfont.woff2
Normal file
BIN
nyaa/static/fonts/glyphicons-halflings-regular.eot
Normal file
BIN
nyaa/static/fonts/glyphicons-halflings-regular.svg
Normal file
After Width: | Height: | Size: 106 KiB |
BIN
nyaa/static/fonts/glyphicons-halflings-regular.ttf
Normal file
BIN
nyaa/static/fonts/glyphicons-halflings-regular.woff
Normal file
BIN
nyaa/static/fonts/glyphicons-halflings-regular.woff2
Normal file
BIN
nyaa/static/img/icons/nyaa/1_1.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
nyaa/static/img/icons/nyaa/1_2.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
nyaa/static/img/icons/nyaa/1_3.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
nyaa/static/img/icons/nyaa/1_4.png
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
nyaa/static/img/icons/nyaa/2_1.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
nyaa/static/img/icons/nyaa/2_2.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
nyaa/static/img/icons/nyaa/3_1.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
nyaa/static/img/icons/nyaa/3_2.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
nyaa/static/img/icons/nyaa/3_3.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
BIN
nyaa/static/img/icons/nyaa/4_1.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
BIN
nyaa/static/img/icons/nyaa/4_2.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
nyaa/static/img/icons/nyaa/4_3.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
nyaa/static/img/icons/nyaa/4_4.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
nyaa/static/img/icons/nyaa/5_1.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
nyaa/static/img/icons/nyaa/5_2.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
nyaa/static/img/icons/nyaa/6_1.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
nyaa/static/img/icons/nyaa/6_2.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
nyaa/static/img/icons/sukebei/1_1.png
Normal file
After Width: | Height: | Size: 4 KiB |
BIN
nyaa/static/img/icons/sukebei/1_2.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
nyaa/static/img/icons/sukebei/1_3.png
Normal file
After Width: | Height: | Size: 3.7 KiB |
BIN
nyaa/static/img/icons/sukebei/1_4.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
nyaa/static/img/icons/sukebei/1_5.png
Normal file
After Width: | Height: | Size: 4 KiB |
BIN
nyaa/static/img/icons/sukebei/2_1.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
nyaa/static/img/icons/sukebei/2_2.png
Normal file
After Width: | Height: | Size: 4 KiB |
1895
nyaa/static/js/bootstrap-select.js
vendored
Normal file
BIN
nyaa/static/js/bootstrap.min.js
vendored
Normal file
BIN
nyaa/static/js/jquery.min.js
vendored
Normal file
62
nyaa/static/js/main.js
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
document.addEventListener("DOMContentLoaded", function(event) { // wait for content to load because this script is above the link
|
||||||
|
document.getElementById('themeToggle').addEventListener('click', function(e) { // listen for click event
|
||||||
|
e.preventDefault(); // keep link from default action, which going to top of the page
|
||||||
|
toggleDarkMode(); // toggle theme
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Credit: https://www.abeautifulsite.net/whipping-file-inputs-into-shape-with-bootstrap-3
|
||||||
|
// We can attach the `fileselect` event to all file inputs on the page
|
||||||
|
$(document).on('change', ':file', function() {
|
||||||
|
var input = $(this),
|
||||||
|
numFiles = input.get(0).files ? input.get(0).files.length : 1,
|
||||||
|
label = input.val().replace(/\\/g, '/').replace(/.*\//, '');
|
||||||
|
input.trigger('fileselect', [numFiles, label]);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// We can watch for our custom `fileselect` event like this
|
||||||
|
$(document).ready(function() {
|
||||||
|
$(':file').on('fileselect', function(event, numFiles, label) {
|
||||||
|
|
||||||
|
var input = $(this).parent().prev().find(':text'),
|
||||||
|
log = numFiles > 1 ? numFiles + ' files selected' : label;
|
||||||
|
|
||||||
|
if (input.length) {
|
||||||
|
input.val(log);
|
||||||
|
} else {
|
||||||
|
if (log) alert(log);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
// This is the unminified version of the theme changer script in the layout.html @ line: 21
|
||||||
|
// ===========================================================
|
||||||
|
// if (typeof(Storage) !== 'undefined') {
|
||||||
|
// var bsThemeLink = document.getElementById('bsThemeLink');
|
||||||
|
|
||||||
|
// if (localStorage.getItem('theme') === 'dark') {
|
||||||
|
// setThemeDark();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// function toggleDarkMode() {
|
||||||
|
// if (localStorage.getItem('theme') === 'dark') {
|
||||||
|
// setThemeLight();
|
||||||
|
// } else {
|
||||||
|
// setThemeDark();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// function setThemeDark() {
|
||||||
|
// bsThemeLink.href = '/static/css/bootstrap-dark.min.css';
|
||||||
|
// localStorage.setItem('theme', 'dark');
|
||||||
|
// }
|
||||||
|
|
||||||
|
// function setThemeLight() {
|
||||||
|
// bsThemeLink.href = '/static/css/bootstrap.min.css';
|
||||||
|
// localStorage.setItem('theme', 'light');
|
||||||
|
// }
|
||||||
|
// }
|
13
nyaa/static/js/npm.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
// This file is autogenerated via the `commonjs` Grunt task. You can require() this file in a CommonJS environment.
|
||||||
|
require('../../js/transition.js')
|
||||||
|
require('../../js/alert.js')
|
||||||
|
require('../../js/button.js')
|
||||||
|
require('../../js/carousel.js')
|
||||||
|
require('../../js/collapse.js')
|
||||||
|
require('../../js/dropdown.js')
|
||||||
|
require('../../js/modal.js')
|
||||||
|
require('../../js/tooltip.js')
|
||||||
|
require('../../js/popover.js')
|
||||||
|
require('../../js/scrollspy.js')
|
||||||
|
require('../../js/tab.js')
|
||||||
|
require('../../js/affix.js')
|
46
nyaa/static/style.css
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
/* http://meyerweb.com/eric/tools/css/reset/
|
||||||
|
v2.0 | 20110126
|
||||||
|
License: none (public domain)
|
||||||
|
*/
|
||||||
|
html, body, div, span, applet, object, iframe,
|
||||||
|
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
|
||||||
|
a, abbr, acronym, address, big, cite, code,
|
||||||
|
del, dfn, em, img, ins, kbd, q, s, samp,
|
||||||
|
small, strike, strong, sub, sup, tt, var,
|
||||||
|
b, u, i, center,
|
||||||
|
dl, dt, dd, ol, ul, li,
|
||||||
|
fieldset, form, label, legend,
|
||||||
|
table, caption, tbody, tfoot, thead, tr, th, td,
|
||||||
|
article, aside, canvas, details, embed,
|
||||||
|
figure, figcaption, footer, header, hgroup,
|
||||||
|
menu, nav, output, ruby, section, summary,
|
||||||
|
time, mark, audio, video {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
font-size: 100%;
|
||||||
|
font: inherit;
|
||||||
|
vertical-align: baseline; }
|
||||||
|
|
||||||
|
/* HTML5 display-role reset for older browsers */
|
||||||
|
article, aside, details, figcaption, figure,
|
||||||
|
footer, header, hgroup, menu, nav, section {
|
||||||
|
display: block; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
line-height: 1; }
|
||||||
|
|
||||||
|
ol, ul {
|
||||||
|
list-style: none; }
|
||||||
|
|
||||||
|
blockquote, q {
|
||||||
|
quotes: none; }
|
||||||
|
|
||||||
|
blockquote:before, blockquote:after,
|
||||||
|
q:before, q:after {
|
||||||
|
content: '';
|
||||||
|
content: none; }
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-spacing: 0; }
|
6
nyaa/templates/404.html
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
{% block title %}404 Not Found :: {{ config.SITE_NAME }}{% endblock %}
|
||||||
|
{% block body %}
|
||||||
|
<h1>404 Not Found</h1>
|
||||||
|
<p>The path you requested does not exist on this server.</p>
|
||||||
|
{% endblock %}
|
59
nyaa/templates/_formhelpers.html
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
{% macro render_field(field) %}
|
||||||
|
{% if field.errors %}
|
||||||
|
<div class="form-group has-error">
|
||||||
|
{% else %}
|
||||||
|
<div class="form-group">
|
||||||
|
{% endif %}
|
||||||
|
{{ field.label(class='control-label') }}
|
||||||
|
{{ field(title=field.description,**kwargs) | safe }}
|
||||||
|
{% if field.errors %}
|
||||||
|
<div class="help-block">
|
||||||
|
{% if field.errors|length < 2 %}
|
||||||
|
{% for error in field.errors %}
|
||||||
|
{{ error }}
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<ul>
|
||||||
|
{% for error in field.errors %}
|
||||||
|
<li>{{ error }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% macro render_upload(field) %}
|
||||||
|
{% if field.errors %}
|
||||||
|
<div class="form-group has-error">
|
||||||
|
{% else %}
|
||||||
|
<div class="form-group">
|
||||||
|
{% endif %}
|
||||||
|
<label class="control-label" for="torrent_file">Torrent file</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="{{ field.id }}" class="input-group-btn">
|
||||||
|
<span class="btn btn-default">Browse…</span>
|
||||||
|
</label>
|
||||||
|
<input type="text" class="form-control" readonly>
|
||||||
|
</div>
|
||||||
|
<div class="sr-only">
|
||||||
|
{{ field(title=field.description,**kwargs) | safe }}
|
||||||
|
</div>
|
||||||
|
{% if field.errors %}
|
||||||
|
<div class="help-block">
|
||||||
|
{% if field.errors|length < 2 %}
|
||||||
|
{% for error in field.errors %}
|
||||||
|
{{ error }}
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<ul>
|
||||||
|
{% for error in field.errors %}
|
||||||
|
<li>{{ error }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endmacro %}
|
52
nyaa/templates/bootstrap/pagination.html
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
## https://github.com/mbr/flask-bootstrap/blob/master/flask_bootstrap/templates/bootstrap/pagination.html
|
||||||
|
{% macro _arg_url_for(endpoint, base) %}
|
||||||
|
{# calls url_for() with a given endpoint and **base as the parameters,
|
||||||
|
additionally passing on all keyword_arguments (may overwrite existing ones)
|
||||||
|
#}
|
||||||
|
{%- with kargs = base.copy() -%}
|
||||||
|
{%- do kargs.update(kwargs) -%}
|
||||||
|
{{url_for(endpoint, **kargs)}}
|
||||||
|
{%- endwith %}
|
||||||
|
{%- endmacro %}
|
||||||
|
|
||||||
|
{% macro render_pagination(pagination,
|
||||||
|
endpoint=None,
|
||||||
|
prev=('«')|safe,
|
||||||
|
next=('»')|safe,
|
||||||
|
size=None,
|
||||||
|
ellipses='…',
|
||||||
|
args={}
|
||||||
|
)
|
||||||
|
-%}
|
||||||
|
{% with url_args = {} %}
|
||||||
|
{%- do url_args.update(request.view_args if not endpoint else {}),
|
||||||
|
url_args.update(request.args if not endpoint else {}),
|
||||||
|
url_args.update(args) -%}
|
||||||
|
{% with endpoint = endpoint or request.endpoint %}
|
||||||
|
<nav>
|
||||||
|
<ul class="pagination{% if size %} pagination-{{size}}{% endif %}"{{kwargs|xmlattr}}>
|
||||||
|
{# prev and next are only show if a symbol has been passed. #}
|
||||||
|
{% if prev != None -%}
|
||||||
|
<li{% if not pagination.has_prev %} class="disabled"{% endif %}><a href="{{_arg_url_for(endpoint, url_args, p=pagination.prev_num) if pagination.has_prev else '#'}}">{{prev}}</a></li>
|
||||||
|
{%- endif -%}
|
||||||
|
|
||||||
|
{%- for page in pagination.iter_pages(left_edge=2, left_current=6, right_current=6, right_edge=2) %}
|
||||||
|
{% if page %}
|
||||||
|
{% if page != pagination.page %}
|
||||||
|
<li><a href="{{_arg_url_for(endpoint, url_args, p=page)}}">{{page}}</a></li>
|
||||||
|
{% else %}
|
||||||
|
<li class="active"><a href="#">{{page}} <span class="sr-only">(current)</span></a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% elif ellipses != None %}
|
||||||
|
<li class="disabled"><a href="#">{{ellipses}}</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{%- endfor %}
|
||||||
|
|
||||||
|
{% if next != None -%}
|
||||||
|
<li{% if not pagination.has_next %} class="disabled"{% endif %}><a href="{{_arg_url_for(endpoint, url_args, p=pagination.next_num) if pagination.has_next else '#'}}">{{next}}</a></li>
|
||||||
|
{%- endif -%}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
{% endwith %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endmacro %}
|
88
nyaa/templates/edit.html
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
{% block title %}Edit {{ torrent.display_name }} :: {{ config.SITE_NAME }}{% endblock %}
|
||||||
|
{% block body %}
|
||||||
|
{% from "_formhelpers.html" import render_field %}
|
||||||
|
|
||||||
|
<h1>Edit Torrent</h1>
|
||||||
|
|
||||||
|
<form method="POST" enctype="multipart/form-data">
|
||||||
|
{{ form.csrf_token }}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="form-group col-md-6">
|
||||||
|
{{ render_field(form.category, class_='form-control')}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="form-group col-md-6">
|
||||||
|
{{ render_field(form.display_name, class_='form-control', placeholder='Display name') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="form-group col-md-6">
|
||||||
|
{{ render_field(form.information, class_='form-control', placeholder='Your website or IRC channel') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="form-group col-md-6">
|
||||||
|
{{ render_field(form.description, class_='form-control') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if admin %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="form-group col-md-6">
|
||||||
|
<label>
|
||||||
|
{{ form.is_deleted }}
|
||||||
|
Deleted
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="form-group col-md-6">
|
||||||
|
<label>
|
||||||
|
{{ form.is_hidden }}
|
||||||
|
Hidden
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="form-group col-md-6">
|
||||||
|
<label>
|
||||||
|
{{ form.is_remake }}
|
||||||
|
Remake
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="form-group col-md-6">
|
||||||
|
<label>
|
||||||
|
{{ form.is_complete }}
|
||||||
|
Complete
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="form-group col-md-6">
|
||||||
|
<label>
|
||||||
|
{{ form.is_anonymous }}
|
||||||
|
Anonymous
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="form-group col-md-6">
|
||||||
|
<input type="submit" value="Edit" class="btn btn-primary">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
10
nyaa/templates/flashes.html
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="alert alert-dismissable alert-{{ category }}" role="alert">
|
||||||
|
<button type="button" class="close" data-dismiss="alert">×</button>
|
||||||
|
{{ message }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
109
nyaa/templates/help.html
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
{% block title %}Help :: {{ config.SITE_NAME }}{% endblock %}
|
||||||
|
{% block body %}
|
||||||
|
|
||||||
|
<h1>Coming soon (tm)</h1>
|
||||||
|
{# <div class="content">
|
||||||
|
<h1>Help</h1>
|
||||||
|
<p><b>The search engine</b> is located at the top right, and it allows users to search through the torrent titles available on the site. Results matching either word A or B can be included by typing a vertical bar between them (|). Results matching a certain word can be excluded by prefixing that word with a hyphen-minus (-). Phrases can be matched by surrounding them with double-quotes (). Search results can be filtered by category, remake, trusted, and/or A+ status, and then narrowed down further by age and size ranges as well as excluding specific users. Sorting can be done in ascending or descending order by date, amount of seeders/leechers/downloads, size, or name. The search engine adapts to the current view and makes it possible to search for specific torrents in a specific subcategory from a specific user.</p>
|
||||||
|
<p><b>Blue entries:</b></p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<p>Torrents marked as A+ quality are blue in the torrent lists.</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>These are exclusive torrents picked by us.</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>They represent the best available version of this content.</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>There will be no duplicates.</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>Only versions that we actually consider worthy of a seal of approval will be listed.</p>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p>Uploaders are encouraged to leave a complete description of the release on the torrent information page. This is especially true for batches.<br></p>
|
||||||
|
<p><b>Green entries:</b> Torrents uploaded by trusted users are green in the torrent lists.<br></p>
|
||||||
|
<p><b>Orange entries:</b> Torrents must be marked as remakes if any of the following applies to the release:</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<p>Reencode of original release.</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>Remux of another uploader's original release for hardsubbing and/or fixing purposes.</p>
|
||||||
|
</li><!--<li>Non-v2 (or non-v3, etc.) remux of original release using a similar source.</li>-->
|
||||||
|
<li>
|
||||||
|
<p>Reupload of original release using non-original file names.</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>Reupload of original release with missing and/or unrelated additional files.</p>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p><b>Red entries:</b> Torrents containing completed series or other complete sets are red.<br></p>
|
||||||
|
<p><b>Grey entries:</b> Hidden torrents are grey.<br></p>
|
||||||
|
<p><b>The tools to manage your torrents</b> are located right above the torrent's details on the information page. Editable fields are the torrent's title, category, description, information link, metadata, and the alias and key fields which are explained on the page. It is also possible to hide the torrent which prevents it from being displayed in lists or even delete it altogether.<br></p>
|
||||||
|
<p><b>Pseudo-anonymous uploads</b> are torrents that will appear to be anonymous, but you can still manage them through your account.<br></p>
|
||||||
|
<p><b>Flagging torrents</b> points them out for moderator review. Torrents can be flagged by clicking on the link located in the upper right corner of their information pages.<br></p>
|
||||||
|
<p><b>RSS</b> is a useful Web feed that automatically updates when a torrent is added by a user. Many programs such as popular BitTorrent clients, which can be set up for automatic downloading, can make use of RSS feeds. The RSS feed link is dynamic which means that it will - like the search function - adapt to the current view, search results included.<br></p>
|
||||||
|
<p><b>BBCode user input</b> is parsed by the torrent descriptions, information links, and torrent comments, and they all support basic BBCode like [b], [i], [s], [u], [left], [center], [right], [code], [email], [img], [url], [color], [font], [size], [quote], and [spoiler].<br></p>
|
||||||
|
<p><b>The upload page</b> returns various HTTP status codes in order to simplify automated uploads. The following details the custom codes used:</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<p>200: The ID of the uploaded torrent can be found in the Record-ID header.</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>418: You're doing it wrong.</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>460: You forgot to include a valid announce URL. Torrents using only DHT are not allowed, because this is most often just a mistake on behalf of the uploader.</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>461: This torrent already exists in the database.</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>462: The file you uploaded or linked to does not seem to be a torrent.</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>463: The form is missing required data like the category and/or the checkbox which confirms that you have read the rules.</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>520: Server-side error. Wait for a few minutes, and then notify Nyaa if the problem did not go away.</p>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<h1>IRC help channel</h1><a href="irc://irc-server:port/channel?key">
|
||||||
|
<h1>NyaaV2 IRC</h1></a>
|
||||||
|
<p>The IRC channel is only for site support.<br></p>
|
||||||
|
<p><b>Read this to avoid getting banned:</b></p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<p>Do not sit around if you do not need site support unless you have voice/+ access.</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>Requests are not allowed. We only manage the site; we do not necessarily have the material you want on hand.</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>We do not know when A or B will be released, if it's authentic, or anything about a particular release. Do not ask.</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>XDCC, similar services, and their triggers are not allowed.</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>Use English only. Even though we aren't all from English-speaking countries, we need level ground to communicate on.</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>Do not send private messages to the staff. Ask your question in the channel on joining and wait; a staff member will respond in due time.</p>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p><b>Keep these things in mind when asking for help:</b></p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<p>We are not interested in your user name. Paste a link to your account if you want us to do something with it.</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>Provide as many details as possible. If you are having trouble submitting any kind of entry, we want to know everything about you and what (except any passwords) you supply to the form in question.</p>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div> #}
|
||||||
|
{% endblock %}
|
13
nyaa/templates/home.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
{% block title %}Browse :: {{ config.SITE_NAME }}{% endblock %}
|
||||||
|
{% block body %}
|
||||||
|
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<p><strong>Hello!</strong> This site is still a work in progress and new features (faster search, open source™, etc.) will be added soon.</p>
|
||||||
|
<p>We welcome you to provide feedback at <a href="irc://irc.rizon.net/nyaa-dev">#nyaa-dev@irc.rizon.net</a></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{% include "search_results.html" %}
|
||||||
|
|
||||||
|
{% endblock %}
|
289
nyaa/templates/layout.html
Normal file
|
@ -0,0 +1,289 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>{% block title %}{{ config.SITE_NAME }}{% endblock %}</title>
|
||||||
|
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<link rel="shortcut icon" type="image/png" href="/static/favicon.png">
|
||||||
|
<link rel="icon" type="image/png" href="/static/favicon.png">
|
||||||
|
|
||||||
|
<!-- Bootstrap core CSS -->
|
||||||
|
<!--
|
||||||
|
Note: This has been customized at http://getbootstrap.com/customize/ to
|
||||||
|
set the column breakpoint to tablet mode, instead of mobile. This is to
|
||||||
|
make the navbar not look awful on tablets.
|
||||||
|
-->
|
||||||
|
<link href="/static/css/bootstrap.min.css" rel="stylesheet" id="bsThemeLink">
|
||||||
|
<!--
|
||||||
|
This theme changer script needs to be inline and right under the above stylesheet link to prevent FOUC (Flash Of Unstyled Content)
|
||||||
|
Development version is commented out in static/js/main.js at the bottom of the file
|
||||||
|
-->
|
||||||
|
<script>function toggleDarkMode(){"dark"===localStorage.getItem("theme")?setThemeLight():setThemeDark()}function setThemeDark(){bsThemeLink.href="/static/css/bootstrap-dark.min.css",localStorage.setItem("theme","dark")}function setThemeLight(){bsThemeLink.href="/static/css/bootstrap.min.css",localStorage.setItem("theme","light")}if("undefined"!=typeof Storage){var bsThemeLink=document.getElementById("bsThemeLink");"dark"===localStorage.getItem("theme")&&setThemeDark()}</script>
|
||||||
|
<link href="/static/css/bootstrap-select.min.css" rel="stylesheet">
|
||||||
|
<link href="/static/css/font-awesome.min.css" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- Custom styles for this template -->
|
||||||
|
<link href="/static/css/main.css" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- Bootstrap core JavaScript -->
|
||||||
|
<script src="/static/js/jquery.min.js"></script>
|
||||||
|
<script src="/static/js/bootstrap.min.js"></script>
|
||||||
|
<!-- Modified to not apply border-radius to selectpickers and stuff so our navbar looks cool -->
|
||||||
|
<script src="/static/js/bootstrap-select.js"></script>
|
||||||
|
<script src="/static/js/main.js"></script>
|
||||||
|
<script src="//cdnjs.cloudflare.com/ajax/libs/marked/0.3.6/marked.min.js"></script>
|
||||||
|
|
||||||
|
<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
|
||||||
|
<!--[if lt IE 9]>
|
||||||
|
<script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
|
||||||
|
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
|
||||||
|
<![endif]-->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Fixed navbar -->
|
||||||
|
<nav class="navbar navbar-default navbar-static-top navbar-inverse">
|
||||||
|
<div class="container">
|
||||||
|
<div class="navbar-header">
|
||||||
|
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
|
||||||
|
<span class="sr-only">Toggle navigation</span>
|
||||||
|
<span class="icon-bar"></span>
|
||||||
|
<span class="icon-bar"></span>
|
||||||
|
<span class="icon-bar"></span>
|
||||||
|
</button>
|
||||||
|
<a class="navbar-brand" href="/">{{ config.SITE_NAME }}</a>
|
||||||
|
</div>
|
||||||
|
<div id="navbar" class="navbar-collapse collapse">
|
||||||
|
<ul class="nav navbar-nav">
|
||||||
|
<li {% if request.path == "/upload" %} class="active"{% endif %}><a href="/upload">Upload</a></li>
|
||||||
|
<li class="dropdown">
|
||||||
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
|
||||||
|
About
|
||||||
|
<span class="caret"></span>
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li {% if request.path == "/rules" %} class="active"{% endif %}><a href="/rules">Rules</a></li>
|
||||||
|
<li {% if request.path == "/help" %} class="active"{% endif %}><a href="/help">Help</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li><a href="{% if rss_filter %}{{ url_for('home', page='rss', **rss_filter) }}{% else %}{{ url_for('home', page='rss') }}{% endif %}">RSS</a></li>
|
||||||
|
{% if config.TABLE_PREFIX == 'nyaa_' %}
|
||||||
|
<li><a href="https://sukebei.nyaa.si/">R-18</a></li>
|
||||||
|
{% elif config.TABLE_PREFIX == 'sukebei_' %}
|
||||||
|
<li><a href="https://nyaa.si/">SFW</a></li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<ul class="nav navbar-nav navbar-right">
|
||||||
|
{% if g.user %}
|
||||||
|
<li class="dropdown">
|
||||||
|
<a href="#" class="dropdown-toggle visible-lg visible-sm visible-xs" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
|
||||||
|
<i class="fa fa-user fa-fw"></i>
|
||||||
|
{{g.user.username}}
|
||||||
|
<span class="caret"></span>
|
||||||
|
</a>
|
||||||
|
<a href="#" class="dropdown-toggle hidden-lg hidden-sm hidden-xs" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
|
||||||
|
<i class="fa fa-user fa-fw"></i>
|
||||||
|
<span class="caret"></span>
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li class="hidden-lg hidden-sm hidden-xs">
|
||||||
|
<a><i class="fa fa-user fa-fw"></i>Logged in as {{ g.user.username }}</a>
|
||||||
|
</li>
|
||||||
|
<li class="hidden-lg hidden-sm hidden-xs divider" role="separator">
|
||||||
|
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{{ url_for('view_user', user_name=g.user.username) }}">
|
||||||
|
<i class="fa fa-user fa-fw"></i>
|
||||||
|
Torrents
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/profile">
|
||||||
|
<i class="fa fa-gear fa-fw"></i>
|
||||||
|
Profile
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/logout">
|
||||||
|
<i class="fa fa-times fa-fw"></i>
|
||||||
|
Logout
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="dropdown">
|
||||||
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
|
||||||
|
<i class="fa fa-user-times fa-fw"></i>
|
||||||
|
Guest
|
||||||
|
<span class="caret"></span>
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li>
|
||||||
|
<a href="/login">
|
||||||
|
<i class="fa fa-sign-in fa-fw"></i>
|
||||||
|
Login
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/register">
|
||||||
|
<i class="fa fa-pencil fa-fw"></i>
|
||||||
|
Register
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
{% if user_page %}
|
||||||
|
<form class="navbar-form navbar-right form" action="{{ url_for('view_user', user_name=user.username) }}" method="get">
|
||||||
|
{% else %}
|
||||||
|
<form class="navbar-form navbar-right form" action="/" method="get">
|
||||||
|
{% endif %}
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" class="form-control" name="q" placeholder="Search..." value="{{ search["term"] if search is defined else '' }}">
|
||||||
|
<div class="input-group-btn" id="navFilter">
|
||||||
|
<select class="selectpicker show-tick" title="Filter" data-width="120px" name="f">
|
||||||
|
<option value="0" title="Show all" {% if search is defined and search["filter"] == "0" %}selected{% else %}selected{% endif %}>Show all</option>
|
||||||
|
<option value="1" title="No remakes" {% if search is defined and search["filter"] == "1" %}selected{% endif %}>No remakes</option>
|
||||||
|
<option value="2" title="Trusted only" {% if search is defined and search["filter"] == "2" %}selected{% endif %}>Trusted only</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="input-group-btn" id="navFilter">
|
||||||
|
{% if config.TABLE_PREFIX == 'nyaa_' %}
|
||||||
|
<select class="selectpicker show-tick" title="Category" data-width="170px" name="c">
|
||||||
|
<option value="0_0" title="Show all" {% if search is defined and search["category"] == "0_0" %}selected{% else %}selected{% endif %}>
|
||||||
|
Show all
|
||||||
|
</option>
|
||||||
|
<option value="1_0" title="Anime" {% if search is defined and search["category"] == "1_0" %}selected{% endif %}>
|
||||||
|
Anime
|
||||||
|
</option>
|
||||||
|
<option value="1_1" title="Anime - AMV" {% if search is defined and search["category"] == "1_1" %}selected{% endif %}>
|
||||||
|
- Anime Music Video
|
||||||
|
</option>
|
||||||
|
<option value="1_2" title="Anime - English" {% if search is defined and search["category"] == "1_2" %}selected{% endif %}>
|
||||||
|
- English-translated
|
||||||
|
</option>
|
||||||
|
<option value="1_3" title="Anime - Non-English" {% if search is defined and search["category"] == "1_3" %}selected{% endif %}>
|
||||||
|
- Non-English-translated
|
||||||
|
</option>
|
||||||
|
<option value="1_4" title="Anime - Raw" {% if search is defined and search["category"] == "1_4" %}selected{% endif %}>
|
||||||
|
- Raw
|
||||||
|
</option>
|
||||||
|
<option value="2_0" title="Audio" {% if search is defined and search["category"] == "2_0" %}selected{% endif %}>
|
||||||
|
Audio
|
||||||
|
</option>
|
||||||
|
<option value="2_1" title="Audio - Lossless" {% if search is defined and search["category"] == "2_1" %}selected{% endif %}>
|
||||||
|
- Lossless
|
||||||
|
</option>
|
||||||
|
<option value="2_2" title="Audio - Lossy" {% if search is defined and search["category"] == "2_2" %}selected{% endif %}>
|
||||||
|
- Lossy
|
||||||
|
</option>
|
||||||
|
<option value="3_0" title="Literature" {% if search is defined and search["category"] == "3_0" %}selected{% endif %}>
|
||||||
|
Literature
|
||||||
|
</option>
|
||||||
|
<option value="3_1" title="Literature - English" {% if search is defined and search["category"] == "3_1" %}selected{% endif %}>
|
||||||
|
- English-translated
|
||||||
|
</option>
|
||||||
|
<option value="3_2" title="Literature - Non-English" {% if search is defined and search["category"] == "3_2" %}selected{% endif %}>
|
||||||
|
- Non-English-translated
|
||||||
|
</option>
|
||||||
|
<option value="3_3" title="Literature - Raw" {% if search is defined and search["category"] == "3_3" %}selected{% endif %}>
|
||||||
|
- Raw
|
||||||
|
</option>
|
||||||
|
<option value="4_0" title="Live Action" {% if search is defined and search["category"] == "4_0" %}selected{% endif %}>
|
||||||
|
Live Action
|
||||||
|
</option>
|
||||||
|
<option value="4_1" title="Live Action - English" {% if search is defined and search["category"] == "4_1" %}selected{% endif %}>
|
||||||
|
- English-translated
|
||||||
|
</option>
|
||||||
|
<option value="4_2" title="Live Action - Idol/PV" {% if search is defined and search["category"] == "4_2" %}selected{% endif %}>
|
||||||
|
- Idol/Promotional Video
|
||||||
|
</option>
|
||||||
|
<option value="4_3" title="Live Action - Non-English" {% if search is defined and search["category"] == "4_3" %}selected{% endif %}>
|
||||||
|
- Non-English-translated
|
||||||
|
</option>
|
||||||
|
<option value="4_4" title="Live Action - Raw" {% if search is defined and search["category"] == "4_4" %}selected{% endif %}>
|
||||||
|
- Raw
|
||||||
|
</option>
|
||||||
|
<option value="5_0" title="Pictures" {% if search is defined and search["category"] == "5_0" %}selected{% endif %}>
|
||||||
|
Pictures
|
||||||
|
</option>
|
||||||
|
<option value="5_1" title="Pictures - Graphics" {% if search is defined and search["category"] == "5_1" %}selected{% endif %}>
|
||||||
|
- Graphics
|
||||||
|
</option>
|
||||||
|
<option value="5_2" title="Pictures - Photos" {% if search is defined and search["category"] == "5_2" %}selected{% endif %}>
|
||||||
|
- Photos
|
||||||
|
</option>
|
||||||
|
<option value="6_0" title="Software" {% if search is defined and search["category"] == "6_0" %}selected{% endif %}>
|
||||||
|
Software
|
||||||
|
</option>
|
||||||
|
<option value="6_1" title="Software - Apps" {% if search is defined and search["category"] == "6_1" %}selected{% endif %}>
|
||||||
|
- Applications
|
||||||
|
</option>
|
||||||
|
<option value="6_2" title="Software - Games" {% if search is defined and search["category"] == "6_2" %}selected{% endif %}>
|
||||||
|
- Games
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
{% elif config.TABLE_PREFIX == 'sukebei_' %}
|
||||||
|
<select class="selectpicker show-tick" title="Category" data-width="170px" name="c">
|
||||||
|
<option value="0_0" title="Show all" {% if search is defined and search["category"] == "0_0" %}selected{% else %}selected{% endif %}>
|
||||||
|
Show all
|
||||||
|
</option>
|
||||||
|
<option value="1_0" title="Art" {% if search is defined and search["category"] == "1_0" %}selected{% endif %}>
|
||||||
|
Art
|
||||||
|
</option>
|
||||||
|
<option value="1_1" title="Art - Anime" {% if search is defined and search["category"] == "1_1" %}selected{% endif %}>
|
||||||
|
- Anime
|
||||||
|
</option>
|
||||||
|
<option value="1_2" title="Art - Doujinshi" {% if search is defined and search["category"] == "1_2" %}selected{% endif %}>
|
||||||
|
- Doujinshi
|
||||||
|
</option>
|
||||||
|
<option value="1_3" title="Art - Games" {% if search is defined and search["category"] == "1_3" %}selected{% endif %}>
|
||||||
|
- Games
|
||||||
|
</option>
|
||||||
|
<option value="1_4" title="Art - Manga" {% if search is defined and search["category"] == "1_4" %}selected{% endif %}>
|
||||||
|
- Manga
|
||||||
|
</option>
|
||||||
|
<option value="1_5" title="Art - Pictures" {% if search is defined and search["category"] == "1_5" %}selected{% endif %}>
|
||||||
|
- Pictures
|
||||||
|
</option>
|
||||||
|
<option value="2_0" title="Real Life" {% if search is defined and search["category"] == "2_0" %}selected{% endif %}>
|
||||||
|
Real Life
|
||||||
|
</option>
|
||||||
|
<option value="2_1" title="Real Life - Pictures" {% if search is defined and search["category"] == "2_1" %}selected{% endif %}>
|
||||||
|
- Photobooks and Pictures
|
||||||
|
</option>
|
||||||
|
<option value="2_2" title="Real Life - Videos" {% if search is defined and search["category"] == "2_2" %}selected{% endif %}>
|
||||||
|
- Videos
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="input-group-btn">
|
||||||
|
<button class="btn btn-primary" type="submit">
|
||||||
|
<i class="fa fa-search fa-fw"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div><!--/.nav-collapse -->
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
{% include "flashes.html" %}
|
||||||
|
|
||||||
|
{% block body %}{% endblock %}
|
||||||
|
</div> <!-- /container -->
|
||||||
|
|
||||||
|
<footer style="text-align: center;">
|
||||||
|
<p>Dark Mode: <a href="#" id="themeToggle">Toggle</a></p>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
|
28
nyaa/templates/login.html
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
{% block title %}Login :: {{ config.SITE_NAME }}{% endblock %}
|
||||||
|
{% block body %}
|
||||||
|
{% from "_formhelpers.html" import render_field %}
|
||||||
|
|
||||||
|
<h1>Login</h1>
|
||||||
|
<form method="POST">
|
||||||
|
{{ form.csrf_token }}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="form-group col-md-4">
|
||||||
|
{{ render_field(form.username, class_='form-control', placeholder='Username') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="form-group col-md-4">
|
||||||
|
{{ render_field(form.password, class_='form-control', placeholder='Password') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<input type="submit" value="Login" class="btn btn-primary">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
43
nyaa/templates/profile.html
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
{% block title %}Edit Profile :: {{ config.SITE_NAME }}{% endblock %}
|
||||||
|
{% block body %}
|
||||||
|
{% from "_formhelpers.html" import render_field %}
|
||||||
|
|
||||||
|
<h1>Edit Profile</h1>
|
||||||
|
<form method="POST">
|
||||||
|
{{ form.csrf_token }}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="form-group col-md-4">
|
||||||
|
{{ render_field(form.email, class_='form-control', placeholder='New email address') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="form-group col-md-4">
|
||||||
|
{{ render_field(form.current_password, class_='form-control', placeholder='Current password') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="form-group col-md-4">
|
||||||
|
{{ render_field(form.new_password, class_='form-control', placeholder='New password') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="form-group col-md-4">
|
||||||
|
{{ render_field(form.password_confirm, class_='form-control', placeholder='New password (confirm)') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<input type="submit" value="Update" class="btn btn-primary">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
|
|
54
nyaa/templates/register.html
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
{% block title %}Register :: {{ config.SITE_NAME }}{% endblock %}
|
||||||
|
{% block body %}
|
||||||
|
{% from "_formhelpers.html" import render_field %}
|
||||||
|
|
||||||
|
<h1>Register</h1>
|
||||||
|
<form method="POST">
|
||||||
|
{{ form.csrf_token }}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="form-group col-md-4">
|
||||||
|
{{ render_field(form.username, class_='form-control', placeholder='Username') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="form-group col-md-4">
|
||||||
|
{{ render_field(form.email, class_='form-control', placeholder='Email address') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="form-group col-md-4">
|
||||||
|
{{ render_field(form.password, class_='form-control', placeholder='Password') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="form-group col-md-4">
|
||||||
|
{{ render_field(form.password_confirm, class_='form-control', placeholder='Password (confirm)') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if config.USE_RECAPTCHA %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
{% for error in form.recaptcha.errors %}
|
||||||
|
{{ error }}
|
||||||
|
{% endfor %}
|
||||||
|
{{ form.recaptcha }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<input type="submit" value="Register" class="btn btn-primary">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
|
|
22
nyaa/templates/rss.xml
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
|
||||||
|
<channel>
|
||||||
|
<title>{{ config.SITE_NAME }} Torrent File RSS (No magnets)</title>
|
||||||
|
<description>
|
||||||
|
RSS Feed for {{ term }}
|
||||||
|
</description>
|
||||||
|
<link>{{ site_url }}</link>
|
||||||
|
<atom:link href="{{ site_url }}rss" rel="self" type="application/rss+xml" />
|
||||||
|
{% for torrent in query %}
|
||||||
|
{% if torrent.has_torrent %}
|
||||||
|
<item>
|
||||||
|
<title>{{ torrent.display_name }}</title>
|
||||||
|
<link>
|
||||||
|
{{ site_url }}view/{{ torrent.id }}/torrent
|
||||||
|
</link>
|
||||||
|
<guid isPermaLink="true">{{ site_url }}view/{{ torrent.id }}</guid>
|
||||||
|
<pubDate>{{ torrent.created_time|rfc822 }}</pubDate>
|
||||||
|
</item>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</channel>
|
||||||
|
</rss>
|
90
nyaa/templates/rules.html
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
{% block title %}Rules :: {{ config.SITE_NAME }}{% endblock %}
|
||||||
|
{% block body %}
|
||||||
|
|
||||||
|
<h1>Coming soon (tm)</h1>
|
||||||
|
{# <div class="content">
|
||||||
|
<h1>Site Rules</h1><!-- <br> -->
|
||||||
|
<!-- <b>Spoilers:</b> Your account will be banned if you repeatedly post these without using the [spoiler] tag properly. -->
|
||||||
|
<h1>Breaking any of the rules on this page may result in being banned</h1>
|
||||||
|
<p><b>Shitposting and Trolling:</b> Your account will be banned if you keep this up.</p>
|
||||||
|
<p><b>Bumping:</b> Your account will be banned if you keep deleting and reposting your torrents.</p>
|
||||||
|
<p><b>Flooding:</b> If you have five or more releases of the same type to release in one go, make a batch torrent containing all of them.</p>
|
||||||
|
<p><b>URL redirection services:</b> These are removed on sight along with their torrents.</p>
|
||||||
|
<p><b>Advertising:</b> No.</p>
|
||||||
|
<p><b>Content restrictions:</b>This site is for content that originates from and/or is specific to China, Japan, and/or Korea.</p>
|
||||||
|
<p>Other content is not allowed without exceptions and will be removed.</p><br>
|
||||||
|
<p><a href="https://%3CURL%3E/"><b>NAME</b></a> is for <b>work-safe</b> content only. The following rules apply:</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<p>No pornography of any kind.</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>No extreme visual content. This means no scat, gore, or any other of such things.</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>Troll torrents are not allowed. These will be removed on sight.</p>
|
||||||
|
</li>
|
||||||
|
</ul><br>
|
||||||
|
<p><a href="https://%3CURL%3E"><b>NAME</b></a> is the place for <b>non-work-safe</b> content only. Still, the following rules apply:</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<p>No extreme real life visual content. This means no scat, gore, bestiality, or any other of such things.</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>Absolutely no real life child pornography of any kind.</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>Troll torrents are not allowed. These will be removed on sight.</p>
|
||||||
|
</li>
|
||||||
|
</ul><br>
|
||||||
|
<p><b>Torrent information:</b> Text files (.txt) or info files (.nfo) for torrent or release group information are preferred.</p>
|
||||||
|
<p>Torrents containing (.chm) or (.url) files may be removed.</p><br>
|
||||||
|
<p><b>Upper limits on video resolution based on source:</b></p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<p>DVD source video is limited to 1024x576p.</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>Web source video is limited to 1920x1080p or source resolution, whichever is lower.</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>TV source video is by default limited to 1920x1080p.<!-- The BS11, BS-NTV, NHK-BS Premium, and WOWOW channels are limited to 1920x1080p.--> SD channels, however, are limited to 480p.</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>Blu-ray source video is limited to 1920x1080p.</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>UHD source video is limited to 3840x2160p.</p>
|
||||||
|
</li>
|
||||||
|
</ul><br>
|
||||||
|
<p>Naturally, untouched sources are not bound by these limits.</p><br>
|
||||||
|
<p><b>Finally, a few notes concerning tagging and using other people's work:</b></p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<p>Do not add your own tag(s) when reuploading an original release.</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>Unless you are reuploading an original release, you should either avoid using tags that are not your own or make it extremely clear to everyone that you are the one responsible for the upload.</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>If these policies are not obeyed, then those torrents will be removed if reported by a group or person commonly seen as the owner of the tag(s). This especially applies to remake torrents.</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>Although only hinted at above, we will of course remove any troll torrents tagged with A-sucks, B-is-slow, or such if reported by A or B.</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>Remakes which are utterly bit rate-starved are not allowed.</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>Remakes which add watermarks or such are not allowed.</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>Remakes which reencode video to XviD or worse are not allowed.</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>Remakes of JPG/PNG-based releases are not allowed without exceptions since there is most often no point in making such.</p>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div> #}
|
||||||
|
{% endblock %}
|
70
nyaa/templates/search_results.html
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
{% if torrent_query.items %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-bordered table-hover table-striped torrent-list">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width:80px;text-align:center;">Category</th>
|
||||||
|
<th class="sorting{% if search["sort"] == "name" %}{% if search["order"] == "desc" %}_desc{% else %}_asc{% endif %}{% endif %}" style="width:auto;">
|
||||||
|
<a href="{% if search["sort"] == "name" and search["order"] == "desc" %}{{ modify_query(s="name", o="asc") }}{% else %}{{ modify_query(s="name", o="desc") }}{% endif %}"></a>
|
||||||
|
Name
|
||||||
|
</th>
|
||||||
|
<th style="width:0;text-align:center;">Link</th>
|
||||||
|
<th class="sorting{% if search["sort"] == "size" %}{% if search["order"] == "desc" %}_desc{% else %}_asc{% endif %}{% endif %}" style="width:100px;text-align:center;">
|
||||||
|
<a href="{% if search["sort"] == "size" and search["order"] == "desc" %}{{ modify_query(s="size", o="asc") }}{% else %}{{ modify_query(s="size", o="desc") }}{% endif %}"></a>
|
||||||
|
Size
|
||||||
|
</th>
|
||||||
|
{# <th style="width:170px;text-align:center;">Date Uploaded</th> #}
|
||||||
|
{% if config.ENABLE_SHOW_STATS %}
|
||||||
|
<th class="sorting{% if search["sort"] == "seeders" %}{% if search["order"] == "desc" %}_desc{% else %}_asc{% endif %}{% endif %} text-center" style="width:65px;">
|
||||||
|
<a href="{% if search["sort"] == "seeders" and search["order"] == "desc" %}{{ modify_query(s="seeders", o="asc") }}{% else %}{{ modify_query(s="seeders", o="desc") }}{% endif %}"></a>
|
||||||
|
<i class="fa fa-arrow-up" aria-hidden="true"></i></a>
|
||||||
|
</th>
|
||||||
|
<th class="sorting{% if search["sort"] == "leechers" %}{% if search["order"] == "desc" %}_desc{% else %}_asc{% endif %}{% endif %} text-center" style="width:65px;">
|
||||||
|
<a href="{% if search["sort"] == "leechers" and search["order"] == "desc" %}{{ modify_query(s="leechers", o="asc") }}{% else %}{{ modify_query(s="leechers", o="desc") }}{% endif %}"></a>
|
||||||
|
<i class="fa fa-arrow-down" aria-hidden="true"></i>
|
||||||
|
</th>
|
||||||
|
<th class="sorting{% if search["sort"] == "downloads" %}{% if search["order"] == "desc" %}_desc{% else %}_asc{% endif %}{% endif %} text-center" style="width:65px;">
|
||||||
|
<a href="{% if search["sort"] == "downloads" and search["order"] == "desc" %}{{ modify_query(s="downloads", o="asc") }}{% else %}{{ modify_query(s="downloads", o="desc") }}{% endif %}"></a>
|
||||||
|
<i class="fa fa-check" aria-hidden="true"></i>
|
||||||
|
</th>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for torrent in torrent_query.items %}
|
||||||
|
<tr class="{% if torrent.deleted %}deleted{% elif torrent.hidden %}warning{% elif torrent.remake %}danger{% elif torrent.trusted %}success{% else %}default{% endif %}">
|
||||||
|
{% if config.TABLE_PREFIX == 'nyaa_' %}
|
||||||
|
<td style="padding:0 4px;">
|
||||||
|
<a href="/?c={{ torrent.main_category.id }}_{{ torrent.sub_category.id }}">
|
||||||
|
<img src="/static/img/icons/nyaa/{{ torrent.main_category.id }}_{{ torrent.sub_category.id }}.png">
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
{% elif config.TABLE_PREFIX == 'sukebei_' %}
|
||||||
|
<td style="padding:0 4px;">
|
||||||
|
<a href="/?c={{ torrent.main_category.id }}_{{ torrent.sub_category.id }}">
|
||||||
|
<img src="/static/img/icons/sukebei/{{ torrent.main_category.id }}_{{ torrent.sub_category.id }}.png">
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
|
<td><a href="/view/{{ torrent.id }}">{{ torrent.display_name | escape }}</a></td>
|
||||||
|
<td style="white-space: nowrap;text-align: center;">{% if torrent.has_torrent %}<a href="/view/{{ torrent.id }}/torrent"><i class="fa fa-fw fa-download"></i></a> {% endif %}<a href="{{ torrent.magnet_uri }}"><i class="fa fa-fw fa-magnet"></i></a></td>
|
||||||
|
<td>{{ torrent.filesize | filesizeformat(True) }}</td>
|
||||||
|
{# <td>{{ torrent.created_time.strftime('%Y-%m-%d %H:%M') }}</td> #}
|
||||||
|
{% if config.ENABLE_SHOW_STATS %}
|
||||||
|
<td class="text-center" style="color: green;">{{ torrent.stats.seed_count }}</td>
|
||||||
|
<td class="text-center" style="color: red;">{{ torrent.stats.leech_count }}</td>
|
||||||
|
<td class="text-center">{{ torrent.stats.download_count }}</td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<h3>No results found</h3>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<center>
|
||||||
|
{% from "bootstrap/pagination.html" import render_pagination %}
|
||||||
|
{{ render_pagination(torrent_query) }}
|
||||||
|
</center>
|
88
nyaa/templates/upload.html
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
{% block title %}Upload Torrent :: {{ config.SITE_NAME }}{% endblock %}
|
||||||
|
{% block body %}
|
||||||
|
{% from "_formhelpers.html" import render_field %}
|
||||||
|
{% from "_formhelpers.html" import render_upload %}
|
||||||
|
|
||||||
|
<h1>Upload Torrent</h1>
|
||||||
|
|
||||||
|
{% if not user %}
|
||||||
|
<p>You are not logged in, and are uploading anonymously.</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
<form method="POST" enctype="multipart/form-data">
|
||||||
|
{% if config.ENFORCE_MAIN_ANNOUNCE_URL %}<p><strong>Important:</strong> Please put <i>{{config.MAIN_ANNOUNCE_URL}}</i> as your first tracker</p>{% endif %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="form-group col-md-6">
|
||||||
|
{{ render_upload(form.torrent_file, accept=".torrent") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="form-group col-md-6">
|
||||||
|
{{ render_field(form.category, class_='form-control')}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="form-group col-md-6">
|
||||||
|
{{ render_field(form.display_name, class_='form-control', placeholder='Display name') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="form-group col-md-6">
|
||||||
|
{{ render_field(form.information, class_='form-control', placeholder='Your website or IRC channel') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="form-group col-md-6">
|
||||||
|
{{ render_field(form.description, class_='form-control') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="form-group col-md-6">
|
||||||
|
<label>
|
||||||
|
{{ form.is_hidden }}
|
||||||
|
Hidden
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="form-group col-md-6">
|
||||||
|
<label>
|
||||||
|
{{ form.is_remake }}
|
||||||
|
Remake
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="form-group col-md-6">
|
||||||
|
<label>
|
||||||
|
{{ form.is_complete }}
|
||||||
|
Complete
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="form-group col-md-6">
|
||||||
|
<label>
|
||||||
|
{{ form.is_anonymous }}
|
||||||
|
Anonymous
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="form-group col-md-6">
|
||||||
|
<input type="submit" value="Upload" class="btn btn-primary">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
10
nyaa/templates/user.html
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
{% block title %}{{ user.username }} :: {{ config.SITE_NAME }}{% endblock %}
|
||||||
|
{% block body %}
|
||||||
|
<h3>
|
||||||
|
Browsing {{user.username}}'s torrents
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{% include "search_results.html" %}
|
||||||
|
|
||||||
|
{% endblock %}
|
120
nyaa/templates/view.html
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
{% block title %}{{ torrent.display_name }} :: {{ config.SITE_NAME }}{% endblock %}
|
||||||
|
{% block body %}
|
||||||
|
<div class="panel panel-{% if torrent.deleted %}deleted{% elif torrent.remake %}danger{% elif torrent.trusted %}success{% else %}default{% endif %}">
|
||||||
|
<div class="panel-heading"{% if torrent.hidden %} style="background-color: darkgray;"{% endif %}>
|
||||||
|
<h3 class="panel-title">
|
||||||
|
{% if can_edit %}
|
||||||
|
<a href="{{ request.url }}/edit"><i class="fa fa-fw fa-pencil"></i></a>
|
||||||
|
{% endif %}
|
||||||
|
{{ torrent.display_name }}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-1">Category:</div>
|
||||||
|
<div class="col-md-5">{{ torrent.main_category.name }} - {{ torrent.sub_category.name }}</div>
|
||||||
|
|
||||||
|
<div class="col-md-1">Date:</div>
|
||||||
|
<div class="col-md-5">{{ torrent.created_time.strftime('%Y-%m-%d, %H:%M UTC') }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-1">Submitter:</div>
|
||||||
|
<div class="col-md-5">{% if not torrent.anonymous and torrent.user %}<a href="{{ url_for('view_user', user_name=torrent.user.username) }}">{{ torrent.user.username }}</a>{% else %}Anonymous{% endif %}</div>
|
||||||
|
|
||||||
|
<div class="col-md-1">Seeders:</div>
|
||||||
|
<div class="col-md-5"><span style="color: green;">{% if config.ENABLE_SHOW_STATS %}{{ torrent.stats.seed_count }}{% else %}Coming soon{% endif %}</span></div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-1">Information:</div>
|
||||||
|
<div class="col-md-5">
|
||||||
|
{% if torrent.information %}
|
||||||
|
{{ torrent.information | escape }}
|
||||||
|
{% else %}
|
||||||
|
No information.
|
||||||
|
{% endif%}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-1">Leechers:</div>
|
||||||
|
<div class="col-md-5"><span style="color: red;">{% if config.ENABLE_SHOW_STATS %}{{ torrent.stats.leech_count }}{% else %}Coming soon{% endif %}</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-1">File size:</div>
|
||||||
|
<div class="col-md-5">{{ torrent.filesize | filesizeformat(True) }}</div>
|
||||||
|
|
||||||
|
<div class="col-md-1">Downloads:</div>
|
||||||
|
<div class="col-md-5">{% if config.ENABLE_SHOW_STATS %}{{ torrent.stats.download_count }}{% else %}Coming soon{% endif %}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel-footer">
|
||||||
|
{% if torrent.has_torrent %}<a href="/view/{{ torrent.id }}/torrent"><i class="fa fa-download fa-fw"></i>Download Torrent</a> or {% endif %}<a href="{{ torrent.magnet_uri }}" class="card-footer-item"><i class="fa fa-magnet fa-fw"></i>Magnet</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-body" id="torrent-description">
|
||||||
|
{% if torrent.description %}
|
||||||
|
{{ torrent.description | escape }}
|
||||||
|
{% else %}
|
||||||
|
#### No description.
|
||||||
|
{% endif%}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if files and files.__len__() <= config.MAX_FILES_VIEW %}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading panel-heading-collapse">
|
||||||
|
<h3 class="panel-title">
|
||||||
|
<div class="row">
|
||||||
|
<a class="collapsed col-md-12" data-target="#collapseFileList" data-toggle="collapse" style="color:inherit;text-decoration:none;">File list</a>
|
||||||
|
</div>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel-collapse collapse" id="collapseFileList">
|
||||||
|
<table class="table table-bordered table-hover table-striped">
|
||||||
|
<thead>
|
||||||
|
<th style="width:auto;">Path</th>
|
||||||
|
<th style="width:auto;">Size</th>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{%- for key, value in files.items() %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ key }}</td>
|
||||||
|
<td class="col-md-2">{{ value | filesizeformat(True) }}</td>
|
||||||
|
</tr>
|
||||||
|
{%- endfor %}
|
||||||
|
<tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% elif files %}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading panel-heading-collapse">
|
||||||
|
<h3 class="panel-title">
|
||||||
|
<div class="row"><div class="col-md-12">Too many files to display.</div></div>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading panel-heading-collapse">
|
||||||
|
<h3 class="panel-title">
|
||||||
|
<div class="row"><div class="col-md-12">File list is not available for this torrent.</div></div>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var target = document.getElementById('torrent-description');
|
||||||
|
var text = target.innerHTML;
|
||||||
|
var html = marked(text.trim(), { sanitize: true });
|
||||||
|
target.innerHTML = html;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% endblock %}
|
6
nyaa/templates/waiting.html
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
{% block title %}Awaiting Verification :: {{ config.SITE_NAME }}{% endblock %}
|
||||||
|
{% block body %}
|
||||||
|
<h1>Awaiting Verification</h1>
|
||||||
|
<p>Your account been registered. Please check your email for the verification link to activate your account.</p>
|
||||||
|
{% endblock %}
|
0
nyaa/tests/test_models.py
Normal file
102
nyaa/torrents.py
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
import os
|
||||||
|
import base64
|
||||||
|
import time
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
from orderedset import OrderedSet
|
||||||
|
|
||||||
|
from nyaa import bencode
|
||||||
|
from nyaa import app
|
||||||
|
from nyaa import models
|
||||||
|
|
||||||
|
USED_TRACKERS = OrderedSet()
|
||||||
|
|
||||||
|
|
||||||
|
def read_trackers_from_file(file_object):
|
||||||
|
USED_TRACKERS.clear()
|
||||||
|
|
||||||
|
for line in file_object:
|
||||||
|
line = line.strip()
|
||||||
|
if line:
|
||||||
|
USED_TRACKERS.add(line)
|
||||||
|
return USED_TRACKERS
|
||||||
|
|
||||||
|
|
||||||
|
def read_trackers():
|
||||||
|
tracker_list_file = os.path.join(app.config['BASE_DIR'], 'trackers.txt')
|
||||||
|
|
||||||
|
if os.path.exists(tracker_list_file):
|
||||||
|
with open(tracker_list_file, 'r') as in_file:
|
||||||
|
return read_trackers_from_file(in_file)
|
||||||
|
|
||||||
|
|
||||||
|
def default_trackers():
|
||||||
|
if not USED_TRACKERS:
|
||||||
|
read_trackers()
|
||||||
|
return USED_TRACKERS[:]
|
||||||
|
|
||||||
|
|
||||||
|
def get_trackers(torrent):
|
||||||
|
trackers = default_trackers()
|
||||||
|
torrent_trackers = torrent.trackers
|
||||||
|
|
||||||
|
for torrent_tracker in torrent_trackers:
|
||||||
|
trackers.add(torrent_tracker.tracker.uri)
|
||||||
|
|
||||||
|
return list(trackers)
|
||||||
|
|
||||||
|
|
||||||
|
def create_magnet(torrent, max_trackers=5, trackers=None):
|
||||||
|
if trackers is None:
|
||||||
|
trackers = get_trackers(torrent)
|
||||||
|
|
||||||
|
magnet_parts = [
|
||||||
|
('dn', torrent.display_name)
|
||||||
|
]
|
||||||
|
for tracker in trackers[:max_trackers]:
|
||||||
|
magnet_parts.append(('tr', tracker))
|
||||||
|
|
||||||
|
b32_info_hash = base64.b32encode(torrent.info_hash).decode('utf-8')
|
||||||
|
return 'magnet:?xt=urn:btih:' + b32_info_hash + '&' + urlencode(magnet_parts)
|
||||||
|
|
||||||
|
|
||||||
|
def create_default_metadata_base(torrent, trackers=None):
|
||||||
|
if trackers is None:
|
||||||
|
trackers = get_trackers(torrent)
|
||||||
|
|
||||||
|
metadata_base = {
|
||||||
|
'created by': 'NyaaV2',
|
||||||
|
'creation date': int(time.time()),
|
||||||
|
'comment': 'NyaaV2 Torrent #' + str(torrent.id), # Throw the url here or something neat
|
||||||
|
# 'encoding' : 'UTF-8' # It's almost always UTF-8 and expected, but if it isn't...
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(trackers) > 0:
|
||||||
|
metadata_base['announce'] = trackers[0]
|
||||||
|
if len(trackers) > 1:
|
||||||
|
# Yes, it's a list of lists with a single element inside.
|
||||||
|
metadata_base['announce-list'] = [[tracker] for tracker in trackers]
|
||||||
|
|
||||||
|
return metadata_base
|
||||||
|
|
||||||
|
|
||||||
|
def create_bencoded_torrent(torrent, metadata_base=None):
|
||||||
|
''' Creates a bencoded torrent metadata for a given torrent,
|
||||||
|
optionally using a given metadata_base dict (note: 'info' key will be
|
||||||
|
popped off the dict) '''
|
||||||
|
if metadata_base is None:
|
||||||
|
metadata_base = create_default_metadata_base(torrent)
|
||||||
|
|
||||||
|
metadata_base['encoding'] = torrent.encoding
|
||||||
|
|
||||||
|
# Make sure info doesn't exist on the base
|
||||||
|
metadata_base.pop('info', None)
|
||||||
|
prefixed_dict = {key: metadata_base[key] for key in metadata_base if key < 'info'}
|
||||||
|
suffixed_dict = {key: metadata_base[key] for key in metadata_base if key > 'info'}
|
||||||
|
|
||||||
|
prefix = bencode.encode(prefixed_dict)
|
||||||
|
suffix = bencode.encode(suffixed_dict)
|
||||||
|
|
||||||
|
bencoded_info = torrent.info.info_dict
|
||||||
|
bencoded_torrent = prefix[:-1] + b'4:info' + bencoded_info + b'e' + suffix[1:]
|
||||||
|
|
||||||
|
return bencoded_torrent
|
61
nyaa/utils.py
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
import hashlib
|
||||||
|
import functools
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
|
||||||
|
def sha1_hash(input_bytes):
|
||||||
|
""" Hash given bytes with hashlib.sha1 and return the digest (as bytes) """
|
||||||
|
return hashlib.sha1(input_bytes).digest()
|
||||||
|
|
||||||
|
|
||||||
|
def sorted_pathdict(input_dict):
|
||||||
|
""" Sorts a parsed torrent filelist dict by alphabat, directories first """
|
||||||
|
directories = OrderedDict()
|
||||||
|
files = OrderedDict()
|
||||||
|
|
||||||
|
for key, value in input_dict.items():
|
||||||
|
if isinstance(value, dict):
|
||||||
|
directories[key] = sorted_pathdict(value)
|
||||||
|
else:
|
||||||
|
files[key] = value
|
||||||
|
|
||||||
|
return OrderedDict(sorted(directories.items()) + sorted(files.items()))
|
||||||
|
|
||||||
|
|
||||||
|
def cached_function(f):
|
||||||
|
sentinel = object()
|
||||||
|
f._cached_value = sentinel
|
||||||
|
|
||||||
|
@functools.wraps(f)
|
||||||
|
def decorator(*args, **kwargs):
|
||||||
|
if f._cached_value is sentinel:
|
||||||
|
print('Evaluating', f, args, kwargs)
|
||||||
|
f._cached_value = f(*args, **kwargs)
|
||||||
|
return f._cached_value
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def flattenDict(d, result=None):
|
||||||
|
if result is None:
|
||||||
|
result = {}
|
||||||
|
for key in d:
|
||||||
|
value = d[key]
|
||||||
|
if isinstance(value, dict):
|
||||||
|
value1 = {}
|
||||||
|
for keyIn in value:
|
||||||
|
value1["/".join([key, keyIn])] = value[keyIn]
|
||||||
|
flattenDict(value1, result)
|
||||||
|
elif isinstance(value, (list, tuple)):
|
||||||
|
for indexB, element in enumerate(value):
|
||||||
|
if isinstance(element, dict):
|
||||||
|
value1 = {}
|
||||||
|
index = 0
|
||||||
|
for keyIn in element:
|
||||||
|
newkey = "/".join([key, keyIn])
|
||||||
|
value1["/".join([key, keyIn])] = value[indexB][keyIn]
|
||||||
|
index += 1
|
||||||
|
for keyA in value1:
|
||||||
|
flattenDict(value1, result)
|
||||||
|
else:
|
||||||
|
result[key] = value
|
||||||
|
return result
|
34
requirements.txt
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
appdirs==1.4.3
|
||||||
|
argon2-cffi==16.3.0
|
||||||
|
autopep8==1.3.1
|
||||||
|
blinker==1.4
|
||||||
|
cffi==1.10.0
|
||||||
|
click==6.7
|
||||||
|
dominate==2.3.1
|
||||||
|
Flask==0.12.1
|
||||||
|
Flask-Assets==0.12
|
||||||
|
Flask-DebugToolbar==0.10.1
|
||||||
|
Flask-SQLAlchemy==2.2
|
||||||
|
Flask-WTF==0.14.2
|
||||||
|
gevent==1.2.1
|
||||||
|
greenlet==0.4.12
|
||||||
|
itsdangerous==0.24
|
||||||
|
Jinja2==2.9.6
|
||||||
|
libsass==0.12.3
|
||||||
|
MarkupSafe==1.0
|
||||||
|
mysqlclient==1.3.10
|
||||||
|
orderedset==2.0
|
||||||
|
packaging==16.8
|
||||||
|
passlib==1.7.1
|
||||||
|
pycodestyle==2.3.1
|
||||||
|
pycparser==2.17
|
||||||
|
pyparsing==2.2.0
|
||||||
|
six==1.10.0
|
||||||
|
SQLAlchemy>=1.1.9
|
||||||
|
SQLAlchemy-FullText-Search==0.2.3
|
||||||
|
SQLAlchemy-Utils>=0.32.14
|
||||||
|
uWSGI==2.0.15
|
||||||
|
visitor==0.1.3
|
||||||
|
webassets==0.12.1
|
||||||
|
Werkzeug==0.12.1
|
||||||
|
WTForms==2.1
|
2
run.py
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
from nyaa import app
|
||||||
|
app.run(host='0.0.0.0', port=5500, debug=True)
|
1
torrent_cache/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
*.torrent
|
10
trackers.txt
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
udp://tracker.internetwarriors.net:1337/announce
|
||||||
|
udp://tracker.leechers-paradise.org:6969/announce
|
||||||
|
udp://tracker.coppersurfer.tk:6969/announce
|
||||||
|
udp://exodus.desync.com:6969/announce
|
||||||
|
udp://tracker.openbittorrent.com:80/announce
|
||||||
|
udp://tracker.sktorrent.net:6969/announce
|
||||||
|
udp://tracker.zer0day.to:1337/announce
|
||||||
|
udp://tracker.pirateparty.gr:6969/announce
|
||||||
|
udp://oscar.reyesleon.xyz:6969/announce
|
||||||
|
|
30
uwsgi.ini
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
[uwsgi]
|
||||||
|
# socket = [addr:port]
|
||||||
|
socket = uwsgi.sock
|
||||||
|
chmod-socket = 664
|
||||||
|
|
||||||
|
# logging
|
||||||
|
disable-logging = True
|
||||||
|
#logger = file:uwsgi.log
|
||||||
|
|
||||||
|
# Base application directory
|
||||||
|
#chdir = .
|
||||||
|
|
||||||
|
# WSGI module and callable
|
||||||
|
# module = [wsgi_module_name]:[application_callable_name]
|
||||||
|
module = WSGI:app
|
||||||
|
|
||||||
|
# master = [master process (true of false)]
|
||||||
|
master = true
|
||||||
|
|
||||||
|
# debugging
|
||||||
|
catch-exceptions = True
|
||||||
|
|
||||||
|
# performance
|
||||||
|
processes = 4
|
||||||
|
buffer-size = 8192
|
||||||
|
|
||||||
|
loop = gevent
|
||||||
|
socket-timeout = 10
|
||||||
|
gevent = 1000
|
||||||
|
gevent-monkey-patch = true
|