mirror of
https://gitlab.com/SIGBUS/nyaa.git
synced 2024-12-22 14:00:00 +00:00
Merge branch 'master' into reports
This commit is contained in:
commit
5332ba1a49
18
.travis.yml
Normal file
18
.travis.yml
Normal file
|
@ -0,0 +1,18 @@
|
|||
language: python
|
||||
|
||||
python:
|
||||
- 3.6
|
||||
|
||||
dist: xenial
|
||||
sudo: false
|
||||
|
||||
cache: pip
|
||||
|
||||
install:
|
||||
- pip install --upgrade pycodestyle
|
||||
|
||||
script:
|
||||
- pycodestyle nyaa/ --show-source --max-line-length=100
|
||||
|
||||
notifications:
|
||||
email: false
|
34
README.md
34
README.md
|
@ -1,14 +1,6 @@
|
|||
# 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):
|
||||
## Setup
|
||||
|
||||
- Install dependencies https://github.com/pyenv/pyenv/wiki/Common-build-problems
|
||||
- Install `pyenv` https://github.com/pyenv/pyenv/blob/master/README.md#installation
|
||||
|
@ -20,7 +12,7 @@
|
|||
- Copy `config.example.py` into `config.py`
|
||||
- Change TABLE_PREFIX to `nyaa_` or `sukebei_` depending on the site
|
||||
|
||||
## Setting up MySQL/MariaDB database for advanced functionality
|
||||
### 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`
|
||||
|
@ -35,18 +27,18 @@
|
|||
- `CREATE DATABASE nyaav2 DEFAULT CHARACTER SET utf8 COLLATE utf8_bin;`
|
||||
- `SOURCE ~/path/to/database/nyaa_maria_vx.sql`
|
||||
|
||||
## Finishing up
|
||||
### 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`
|
||||
- When you are finished developing, deactivate your virtualenv with `source deactivate`
|
||||
|
||||
# Enabling ElasticSearch
|
||||
## Enabling ElasticSearch
|
||||
|
||||
## Basics
|
||||
### Basics
|
||||
- Install jdk `sudo apt-get install openjdk-8-jdk`
|
||||
- Install elasticsearch https://www.elastic.co/guide/en/elasticsearch/reference/current/deb.html
|
||||
- `sudo systemctl enable elasticsearch.service`
|
||||
|
@ -54,7 +46,7 @@
|
|||
- Run `curl -XGET 'localhost:9200'` and make sure ES is running
|
||||
- Optional: install Kabana as a search frontend for ES
|
||||
|
||||
## Enable MySQL Binlogging
|
||||
### Enable MySQL Binlogging
|
||||
- Add the `[mariadb]` bin-log section to my.cnf and reload mysql server
|
||||
- Connect to mysql
|
||||
- `SHOW VARIABLES LIKE 'binlog_format';`
|
||||
|
@ -62,7 +54,7 @@
|
|||
- Connect to root user
|
||||
- `GRANT REPLICATION SLAVE ON *.* TO 'test'@'localhost';` where test is the user you will be running `sync_es.py` with
|
||||
|
||||
## Setting up ES
|
||||
### Setting up ES
|
||||
- Run `./create_es.sh` and this creates two indicies: `nyaa` and `sukebei`
|
||||
- The output should show `acknowledged: true` twice
|
||||
- The safest bet is to disable the webapp here to ensure there's no database writes
|
||||
|
@ -70,7 +62,7 @@
|
|||
- Run `python import_to_es.py` with `SITE_FLAVOR` set to `sukebei`
|
||||
- These will take some time to run as it's indexing
|
||||
|
||||
## Setting up sync_es.py
|
||||
### Setting up sync_es.py
|
||||
- Sync_es.py keeps the ElasticSearch index updated by reading the BinLog
|
||||
- Configure the MySQL options with the user where you granted the REPLICATION permissions
|
||||
- Connect to MySQL, run `SHOW MASTER STATUS;`.
|
||||
|
@ -78,9 +70,13 @@
|
|||
- Set up `sync_es.py` as a service and run it, preferably as the system/root
|
||||
- Make sure `sync_es.py` runs within venv with the right dependencies
|
||||
|
||||
## Good to go!
|
||||
- After that, enable the `USE_ELASTIC_SEARCH` flag and restart the webapp and you're good to go
|
||||
Enable the `USE_ELASTIC_SEARCH` flag in `config.py`, restart the application, and you're good to go.
|
||||
|
||||
## Database migrations
|
||||
- Uses [flask-Migrate](https://flask-migrate.readthedocs.io/)
|
||||
- Run `./db_migrate.py db migrate` to generate the migration script after database model changes.
|
||||
- Take a look at the result in `migrations/versions/...` to make sure nothing went wrong.
|
||||
- Run `./db_migrate.py db upgrade` to upgrade your database.
|
||||
|
||||
## Code Quality:
|
||||
- Remember to follow PEP8 style guidelines and run `./lint.sh` before committing.
|
||||
|
|
|
@ -11,7 +11,7 @@ ENABLE_SHOW_STATS = False
|
|||
|
||||
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
|
||||
if USE_MYSQL:
|
||||
SQLALCHEMY_DATABASE_URI = ('mysql://test:test123@localhost/nyaav2')
|
||||
SQLALCHEMY_DATABASE_URI = ('mysql://test:test123@localhost/nyaav2?charset=utf8mb4')
|
||||
else:
|
||||
SQLALCHEMY_DATABASE_URI = (
|
||||
'sqlite:///' + os.path.join(BASE_DIR, 'test.db') + '?check_same_thread=False')
|
||||
|
|
|
@ -33,9 +33,3 @@ if not existing_cats:
|
|||
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)')
|
||||
|
||||
|
|
13
db_migrate.py
Normal file
13
db_migrate.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
#!/usr/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from nyaa import app, db
|
||||
from flask_script import Manager
|
||||
from flask_migrate import Migrate, MigrateCommand
|
||||
|
||||
migrate = Migrate(app, db)
|
||||
|
||||
manager = Manager(app)
|
||||
manager.add_command("db", MigrateCommand)
|
||||
|
||||
if __name__ == "__main__":
|
||||
manager.run()
|
|
@ -21,6 +21,7 @@ settings:
|
|||
- resolution
|
||||
- lowercase
|
||||
- my_ngram
|
||||
- word_delimit
|
||||
filter:
|
||||
my_ngram:
|
||||
type: edgeNGram
|
||||
|
@ -28,7 +29,11 @@ settings:
|
|||
max_gram: 15
|
||||
resolution:
|
||||
type: pattern_capture
|
||||
patterns: ["(\\d+)x(\\d+)"]
|
||||
patterns: ["(\\d+)[xX](\\d+)"]
|
||||
word_delimit:
|
||||
type: word_delimiter
|
||||
preserve_original: true
|
||||
split_on_numerics: false
|
||||
char_filter:
|
||||
my_char_filter:
|
||||
type: mapping
|
||||
|
|
1
migrations/README
Normal file
1
migrations/README
Normal file
|
@ -0,0 +1 @@
|
|||
Generic single-database configuration.
|
45
migrations/alembic.ini
Normal file
45
migrations/alembic.ini
Normal file
|
@ -0,0 +1,45 @@
|
|||
# A generic, single database configuration.
|
||||
|
||||
[alembic]
|
||||
# template used to generate migration files
|
||||
# file_template = %%(rev)s_%%(slug)s
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# revision_environment = false
|
||||
|
||||
|
||||
# Logging configuration
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARN
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
87
migrations/env.py
Normal file
87
migrations/env.py
Normal file
|
@ -0,0 +1,87 @@
|
|||
from __future__ import with_statement
|
||||
from alembic import context
|
||||
from sqlalchemy import engine_from_config, pool
|
||||
from logging.config import fileConfig
|
||||
import logging
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
config = context.config
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
fileConfig(config.config_file_name)
|
||||
logger = logging.getLogger('alembic.env')
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
# from myapp import mymodel
|
||||
# target_metadata = mymodel.Base.metadata
|
||||
from flask import current_app
|
||||
config.set_main_option('sqlalchemy.url',
|
||||
current_app.config.get('SQLALCHEMY_DATABASE_URI'))
|
||||
target_metadata = current_app.extensions['migrate'].db.metadata
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
# my_important_option = config.get_main_option("my_important_option")
|
||||
# ... etc.
|
||||
|
||||
|
||||
def run_migrations_offline():
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
|
||||
"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(url=url)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online():
|
||||
"""Run migrations in 'online' mode.
|
||||
|
||||
In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
|
||||
# this callback is used to prevent an auto-migration from being generated
|
||||
# when there are no changes to the schema
|
||||
# reference: http://alembic.readthedocs.org/en/latest/cookbook.html
|
||||
def process_revision_directives(context, revision, directives):
|
||||
if getattr(config.cmd_opts, 'autogenerate', False):
|
||||
script = directives[0]
|
||||
if script.upgrade_ops.is_empty():
|
||||
directives[:] = []
|
||||
logger.info('No changes in schema detected.')
|
||||
|
||||
engine = engine_from_config(config.get_section(config.config_ini_section),
|
||||
prefix='sqlalchemy.',
|
||||
poolclass=pool.NullPool)
|
||||
|
||||
connection = engine.connect()
|
||||
context.configure(connection=connection,
|
||||
target_metadata=target_metadata,
|
||||
process_revision_directives=process_revision_directives,
|
||||
**current_app.extensions['migrate'].configure_args)
|
||||
|
||||
try:
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
24
migrations/script.py.mako
Normal file
24
migrations/script.py.mako
Normal file
|
@ -0,0 +1,24 @@
|
|||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = ${repr(up_revision)}
|
||||
down_revision = ${repr(down_revision)}
|
||||
branch_labels = ${repr(branch_labels)}
|
||||
depends_on = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade():
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade():
|
||||
${downgrades if downgrades else "pass"}
|
30
migrations/versions/3001f79b7722_add_torrents.uploader_ip.py
Normal file
30
migrations/versions/3001f79b7722_add_torrents.uploader_ip.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
"""Add uploader_ip column to torrents table.
|
||||
|
||||
Revision ID: 3001f79b7722
|
||||
Revises:
|
||||
Create Date: 2017-05-21 18:01:35.472717
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '3001f79b7722'
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('nyaa_torrents', sa.Column('uploader_ip', sa.Binary(), nullable=True))
|
||||
op.add_column('sukebei_torrents', sa.Column('uploader_ip', sa.Binary(), nullable=True))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('nyaa_torrents', 'uploader_ip')
|
||||
op.drop_column('sukebei_torrents', 'uploader_ip')
|
||||
# ### end Alembic commands ###
|
48
migrations/versions/d0eeb8049623_add_comments.py
Normal file
48
migrations/versions/d0eeb8049623_add_comments.py
Normal file
|
@ -0,0 +1,48 @@
|
|||
"""Add comments table.
|
||||
|
||||
Revision ID: d0eeb8049623
|
||||
Revises: 3001f79b7722
|
||||
Create Date: 2017-05-22 22:58:12.039149
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'd0eeb8049623'
|
||||
down_revision = '3001f79b7722'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('nyaa_comments',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('torrent_id', sa.Integer(), nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||
sa.Column('created_time', sa.DateTime(), nullable=True),
|
||||
sa.Column('text', sa.String(length=255, collation='utf8mb4_bin'), nullable=False),
|
||||
sa.ForeignKeyConstraint(['torrent_id'], ['nyaa_torrents.id'], ondelete='CASCADE'),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('sukebei_comments',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('torrent_id', sa.Integer(), nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||
sa.Column('created_time', sa.DateTime(), nullable=True),
|
||||
sa.Column('text', sa.String(length=255, collation='utf8mb4_bin'), nullable=False),
|
||||
sa.ForeignKeyConstraint(['torrent_id'], ['sukebei_torrents.id'], ondelete='CASCADE'),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('nyaa_comments')
|
||||
op.drop_table('sukebei_comments')
|
||||
# ### end Alembic commands ###
|
|
@ -12,6 +12,7 @@ app.config.from_object('config')
|
|||
|
||||
# Database
|
||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
app.config['MYSQL_DATABASE_CHARSET'] = 'utf8mb4'
|
||||
|
||||
# Don't refresh cookie each request
|
||||
app.config['SESSION_REFRESH_EACH_REQUEST'] = False
|
||||
|
@ -37,7 +38,7 @@ if not app.config['DEBUG']:
|
|||
def internal_error(exception):
|
||||
app.logger.error(exception)
|
||||
flask.flash(flask.Markup(
|
||||
'<strong>An error occured!</strong> Debugging information has been logged.'), 'danger')
|
||||
'<strong>An error occurred!</strong> Debug information has been logged.'), 'danger')
|
||||
return flask.redirect('/')
|
||||
|
||||
# Get git commit hash
|
||||
|
|
|
@ -6,6 +6,9 @@ from nyaa import models, forms
|
|||
from nyaa import bencode, backend, utils
|
||||
from nyaa import torrents
|
||||
|
||||
# For _create_upload_category_choices
|
||||
from nyaa import routes
|
||||
|
||||
import functools
|
||||
import json
|
||||
import os.path
|
||||
|
@ -42,84 +45,7 @@ def api_require_user(f):
|
|||
return decorator
|
||||
|
||||
|
||||
def validate_user(upload_request):
|
||||
auth_info = None
|
||||
try:
|
||||
if 'auth_info' in upload_request.files:
|
||||
auth_info = json.loads(upload_request.files['auth_info'].read().decode('utf-8'))
|
||||
if 'username' not in auth_info.keys() or 'password' not in auth_info.keys():
|
||||
return False, None, None
|
||||
|
||||
username = auth_info['username']
|
||||
password = auth_info['password']
|
||||
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 False, None, None
|
||||
|
||||
return True, user, None
|
||||
else:
|
||||
return False, None, None
|
||||
|
||||
except Exception as e:
|
||||
return False, None, e
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
# #################################### API ROUTES ####################################
|
||||
def api_upload(upload_request, user):
|
||||
form_info = None
|
||||
try:
|
||||
form_info = json.loads(upload_request.files['torrent_info'].read().decode('utf-8'))
|
||||
|
||||
form_info_as_dict = []
|
||||
for k, v in form_info.items():
|
||||
if k in ['is_anonymous', 'is_hidden', 'is_remake', 'is_complete']:
|
||||
if v:
|
||||
form_info_as_dict.append((k, v))
|
||||
else:
|
||||
form_info_as_dict.append((k, v))
|
||||
form_info = ImmutableMultiDict(form_info_as_dict)
|
||||
except Exception as e:
|
||||
return flask.make_response(flask.jsonify(
|
||||
{'Failure': ['Invalid data. See HELP in api_uploader.py']}), 400)
|
||||
|
||||
try:
|
||||
torrent_file = upload_request.files['torrent_file']
|
||||
torrent_file = ImmutableMultiDict([('torrent_file', torrent_file)])
|
||||
except Exception as e:
|
||||
return flask.make_response(flask.jsonify(
|
||||
{'Failure': ['No torrent file was attached.']}), 400)
|
||||
|
||||
form = forms.UploadForm(CombinedMultiDict((torrent_file, form_info)))
|
||||
form.category.choices = _create_upload_category_choices()
|
||||
|
||||
if upload_request.method == 'POST' and form.validate():
|
||||
torrent = backend.handle_torrent_upload(form, user, True)
|
||||
|
||||
return flask.make_response(flask.jsonify({'Success': int('{0}'.format(torrent.id))}), 200)
|
||||
else:
|
||||
return_error_messages = []
|
||||
for error_name, error_messages in form.errors.items():
|
||||
return_error_messages.extend(error_messages)
|
||||
|
||||
return flask.make_response(flask.jsonify({'Failure': return_error_messages}), 400)
|
||||
|
||||
# V2 below
|
||||
|
||||
|
||||
# Map UploadForm fields to API keys
|
||||
UPLOAD_API_FORM_KEYMAP = {
|
||||
|
@ -134,19 +60,20 @@ UPLOAD_API_FORM_KEYMAP = {
|
|||
'is_trusted': 'trusted'
|
||||
}
|
||||
UPLOAD_API_FORM_KEYMAP_REVERSE = {v: k for k, v in UPLOAD_API_FORM_KEYMAP.items()}
|
||||
UPLOAD_API_KEYS = [
|
||||
'name',
|
||||
'category',
|
||||
'anonymous',
|
||||
'hidden',
|
||||
'complete',
|
||||
'remake',
|
||||
'trusted',
|
||||
'information',
|
||||
'description'
|
||||
]
|
||||
UPLOAD_API_DEFAULTS = {
|
||||
'name': '',
|
||||
'category': '',
|
||||
'anonymous': False,
|
||||
'hidden': False,
|
||||
'complete': False,
|
||||
'remake': False,
|
||||
'trusted': True,
|
||||
'information': '',
|
||||
'description': ''
|
||||
}
|
||||
|
||||
|
||||
@api_blueprint.route('/upload', methods=['POST'])
|
||||
@api_blueprint.route('/v2/upload', methods=['POST'])
|
||||
@basic_auth_user
|
||||
@api_require_user
|
||||
|
@ -158,16 +85,21 @@ def v2_api_upload():
|
|||
request_data_field = flask.request.form.get('torrent_data')
|
||||
if request_data_field is None:
|
||||
return flask.jsonify({'errors': ['missing torrent_data field']}), 400
|
||||
|
||||
try:
|
||||
request_data = json.loads(request_data_field)
|
||||
except json.decoder.JSONDecodeError:
|
||||
return flask.jsonify({'errors': ['unable to parse valid JSON in torrent_data']}), 400
|
||||
|
||||
# Map api keys to upload form fields
|
||||
for key in UPLOAD_API_KEYS:
|
||||
for key, default in UPLOAD_API_DEFAULTS.items():
|
||||
mapped_key = UPLOAD_API_FORM_KEYMAP_REVERSE.get(key, key)
|
||||
mapped_dict[mapped_key] = request_data.get(key) or ''
|
||||
value = request_data.get(key, default)
|
||||
mapped_dict[mapped_key] = value if value is not None else default
|
||||
|
||||
# Flask-WTF (very helpfully!!) automatically grabs the request form, so force a None formdata
|
||||
upload_form = forms.UploadForm(None, data=mapped_dict)
|
||||
upload_form.category.choices = _create_upload_category_choices()
|
||||
upload_form = forms.UploadForm(None, data=mapped_dict, meta={'csrf': False})
|
||||
upload_form.category.choices = routes._create_upload_category_choices()
|
||||
|
||||
if upload_form.validate():
|
||||
torrent = backend.handle_torrent_upload(upload_form, flask.g.user)
|
||||
|
@ -186,3 +118,139 @@ def v2_api_upload():
|
|||
# Map errors back from form fields into the api keys
|
||||
mapped_errors = {UPLOAD_API_FORM_KEYMAP.get(k, k): v for k, v in upload_form.errors.items()}
|
||||
return flask.jsonify({'errors': mapped_errors}), 400
|
||||
|
||||
|
||||
# #################################### TEMPORARY ####################################
|
||||
|
||||
from orderedset import OrderedSet # noqa: E402
|
||||
|
||||
|
||||
@api_blueprint.route('/ghetto_import', methods=['POST'])
|
||||
def ghetto_import():
|
||||
if flask.request.remote_addr != '127.0.0.1':
|
||||
return flask.error(403)
|
||||
|
||||
torrent_file = flask.request.files.get('torrent')
|
||||
|
||||
try:
|
||||
torrent_dict = bencode.decode(torrent_file)
|
||||
# field.data.close()
|
||||
except (bencode.MalformedBencodeException, UnicodeError):
|
||||
return 'Malformed torrent file', 500
|
||||
|
||||
try:
|
||||
forms._validate_torrent_metadata(torrent_dict)
|
||||
except AssertionError as e:
|
||||
return 'Malformed torrent metadata ({})'.format(e.args[0]), 500
|
||||
|
||||
try:
|
||||
tracker_found = forms._validate_trackers(torrent_dict)
|
||||
except AssertionError as e:
|
||||
return 'Malformed torrent trackers ({})'.format(e.args[0]), 500
|
||||
|
||||
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
|
||||
torrent = models.Torrent.by_info_hash(info_hash)
|
||||
if not torrent:
|
||||
return 'This torrent does not exists', 500
|
||||
|
||||
if torrent.has_torrent:
|
||||
return 'This torrent already has_torrent', 500
|
||||
|
||||
# Torrent is legit, pass original filename and dict along
|
||||
torrent_data = forms.TorrentFileData(filename=os.path.basename(torrent_file.filename),
|
||||
torrent_dict=torrent_dict,
|
||||
info_hash=info_hash,
|
||||
bencoded_info_dict=bencoded_info_dict)
|
||||
|
||||
# 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 = backend._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')
|
||||
|
||||
# Store bencoded info_dict
|
||||
torrent.info = models.TorrentInfo(info_dict=torrent_data.bencoded_info_dict)
|
||||
torrent.has_torrent = True
|
||||
|
||||
# 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, {})
|
||||
|
||||
# Don't add empty filenames (BitComet directory)
|
||||
if filename:
|
||||
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.session.flush()
|
||||
|
||||
db_trackers.add(tracker)
|
||||
|
||||
# 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()
|
||||
|
||||
return 'success'
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import flask
|
||||
from nyaa import app, db
|
||||
from nyaa import models, forms
|
||||
from nyaa import bencode, utils
|
||||
|
@ -8,6 +9,7 @@ import json
|
|||
from werkzeug import secure_filename
|
||||
from collections import OrderedDict
|
||||
from orderedset import OrderedSet
|
||||
from ipaddress import ip_address
|
||||
|
||||
|
||||
def _replace_utf8_values(dict_or_list):
|
||||
|
@ -53,7 +55,8 @@ def handle_torrent_upload(upload_form, uploading_user=None, fromAPI=False):
|
|||
description=description,
|
||||
encoding=torrent_encoding,
|
||||
filesize=torrent_filesize,
|
||||
user=uploading_user)
|
||||
user=uploading_user,
|
||||
uploader_ip=ip_address(flask.request.remote_addr).packed)
|
||||
|
||||
# Store bencoded info_dict
|
||||
torrent.info = models.TorrentInfo(info_dict=torrent_data.bencoded_info_dict)
|
||||
|
@ -69,7 +72,9 @@ def handle_torrent_upload(upload_form, uploading_user=None, fromAPI=False):
|
|||
torrent.complete = upload_form.is_complete.data
|
||||
# Copy trusted status from user if possible
|
||||
can_mark_trusted = uploading_user and uploading_user.is_trusted
|
||||
# To do, automatically mark trusted if user is trusted unless user specifies otherwise
|
||||
torrent.trusted = upload_form.is_trusted.data if can_mark_trusted else False
|
||||
|
||||
# Set category ids
|
||||
torrent.main_category_id, torrent.sub_category_id = \
|
||||
upload_form.category.parsed_data.get_category_ids()
|
||||
|
@ -134,11 +139,10 @@ def handle_torrent_upload(upload_form, uploading_user=None, fromAPI=False):
|
|||
if not tracker:
|
||||
tracker = models.Trackers(uri=announce)
|
||||
db.session.add(tracker)
|
||||
db.session.flush()
|
||||
|
||||
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,
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import flask
|
||||
from nyaa import db, app
|
||||
from nyaa.models import User
|
||||
from nyaa import bencode, utils, models
|
||||
|
@ -15,6 +16,7 @@ from wtforms.widgets import Select as SelectWidget
|
|||
from wtforms.widgets import html_params, HTMLString
|
||||
|
||||
from flask_wtf.recaptcha import RecaptchaField
|
||||
from flask_wtf.recaptcha.validators import Recaptcha as RecaptchaValidator
|
||||
|
||||
|
||||
class Unique(object):
|
||||
|
@ -35,7 +37,7 @@ class Unique(object):
|
|||
|
||||
|
||||
_username_validator = Regexp(
|
||||
r'[a-zA-Z0-9_\-]+',
|
||||
r'^[a-zA-Z0-9_\-]+$',
|
||||
message='Your username must only consist of alphanumerics and _- (a-zA-Z0-9_-)')
|
||||
|
||||
|
||||
|
@ -124,11 +126,18 @@ class DisabledSelectField(SelectField):
|
|||
raise ValueError(self.gettext('Not a valid choice'))
|
||||
|
||||
|
||||
class CommentForm(FlaskForm):
|
||||
comment = TextAreaField('Make a comment', [
|
||||
Length(min=3, max=255, message='Comment must be at least %(min)d characters '
|
||||
'long and %(max)d at most.'),
|
||||
DataRequired()
|
||||
])
|
||||
|
||||
|
||||
class EditForm(FlaskForm):
|
||||
display_name = StringField('Torrent display name', [
|
||||
Length(min=3, max=255,
|
||||
message='Torrent display name must be at least %(min)d characters long '
|
||||
'and %(max)d at most.')
|
||||
Length(min=3, max=255, message='Torrent display name must be at least %(min)d characters '
|
||||
'long and %(max)d at most.')
|
||||
])
|
||||
|
||||
category = DisabledSelectField('Category')
|
||||
|
@ -164,10 +173,6 @@ class EditForm(FlaskForm):
|
|||
|
||||
|
||||
class UploadForm(FlaskForm):
|
||||
|
||||
class Meta:
|
||||
csrf = False
|
||||
|
||||
torrent_file = FileField('Torrent file', [
|
||||
FileRequired()
|
||||
])
|
||||
|
@ -179,6 +184,16 @@ class UploadForm(FlaskForm):
|
|||
'%(max)d at most.')
|
||||
])
|
||||
|
||||
if app.config['USE_RECAPTCHA']:
|
||||
# Captcha only for not logged in users
|
||||
_recaptcha_validator = RecaptchaValidator()
|
||||
|
||||
def _validate_recaptcha(form, field):
|
||||
if not flask.g.user:
|
||||
return UploadForm._recaptcha_validator(form, field)
|
||||
|
||||
recaptcha = RecaptchaField(validators=[_validate_recaptcha])
|
||||
|
||||
# category = SelectField('Category')
|
||||
category = DisabledSelectField('Category')
|
||||
|
||||
|
@ -263,7 +278,7 @@ class UploadForm(FlaskForm):
|
|||
|
||||
|
||||
class UserForm(FlaskForm):
|
||||
user_class = DisabledSelectField('Change User Class')
|
||||
user_class = SelectField('Change User Class')
|
||||
|
||||
def validate_user_class(form, field):
|
||||
if not field.data:
|
||||
|
@ -309,7 +324,8 @@ def _validate_trackers(torrent_dict, tracker_to_check_for=None):
|
|||
for announce in announce_list:
|
||||
_validate_list(announce, 'announce-list item')
|
||||
|
||||
announce_string = _validate_bytes(announce[0], 'announce-list item url', test_decode='utf-8')
|
||||
announce_string = _validate_bytes(
|
||||
announce[0], 'announce-list item url', test_decode='utf-8')
|
||||
if tracker_to_check_for and announce_string.lower() == tracker_to_check_for.lower():
|
||||
tracker_found = True
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import flask
|
||||
from enum import Enum, IntEnum
|
||||
from datetime import datetime, timezone
|
||||
from nyaa import app, db
|
||||
|
@ -6,11 +7,13 @@ 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
|
||||
from ipaddress import ip_address
|
||||
|
||||
import re
|
||||
import base64
|
||||
from markupsafe import escape as escape_markup
|
||||
from urllib.parse import unquote as unquote_url
|
||||
from urllib.parse import urlencode, unquote as unquote_url
|
||||
from hashlib import md5
|
||||
|
||||
if app.config['USE_MYSQL']:
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
@ -61,6 +64,7 @@ class Torrent(db.Model):
|
|||
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)
|
||||
uploader_ip = db.Column(db.Binary(length=16), default=None, 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)
|
||||
|
@ -95,8 +99,10 @@ class Torrent(db.Model):
|
|||
cascade="all, delete-orphan", back_populates='torrent')
|
||||
stats = db.relationship('Statistic', uselist=False,
|
||||
cascade="all, delete-orphan", back_populates='torrent', lazy='joined')
|
||||
trackers = db.relationship('TorrentTrackers', uselist=True,
|
||||
cascade="all, delete-orphan", lazy='joined')
|
||||
trackers = db.relationship('TorrentTrackers', uselist=True, cascade="all, delete-orphan",
|
||||
lazy='joined', order_by='TorrentTrackers.order')
|
||||
comments = db.relationship('Comment', uselist=True,
|
||||
cascade="all, delete-orphan")
|
||||
|
||||
def __repr__(self):
|
||||
return '<{0} #{1.id} \'{1.display_name}\' {1.filesize}b>'.format(type(self).__name__, self)
|
||||
|
@ -138,6 +144,11 @@ class Torrent(db.Model):
|
|||
def magnet_uri(self):
|
||||
return create_magnet(self)
|
||||
|
||||
@property
|
||||
def uploader_ip_string(self):
|
||||
if self.uploader_ip:
|
||||
return str(ip_address(self.uploader_ip))
|
||||
|
||||
@property
|
||||
def anonymous(self):
|
||||
return self.flags & TorrentFlags.ANONYMOUS
|
||||
|
@ -194,6 +205,11 @@ class Torrent(db.Model):
|
|||
def by_info_hash(cls, info_hash):
|
||||
return cls.query.filter_by(info_hash=info_hash).first()
|
||||
|
||||
@classmethod
|
||||
def by_info_hash_hex(cls, info_hash_hex):
|
||||
info_hash_bytes = bytearray.fromhex(info_hash_hex)
|
||||
return cls.by_info_hash(info_hash_bytes)
|
||||
|
||||
|
||||
class TorrentNameSearch(FullText, Torrent):
|
||||
__fulltext_columns__ = ('display_name',)
|
||||
|
@ -310,10 +326,31 @@ class SubCategory(db.Model):
|
|||
return cls.query.get((sub_cat_id, main_cat_id))
|
||||
|
||||
|
||||
class Comment(db.Model):
|
||||
__tablename__ = DB_TABLE_PREFIX + 'comments'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
torrent_id = db.Column(db.Integer, db.ForeignKey(
|
||||
DB_TABLE_PREFIX + 'torrents.id', ondelete='CASCADE'), nullable=False)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'))
|
||||
created_time = db.Column(db.DateTime(timezone=False), default=datetime.utcnow)
|
||||
text = db.Column(db.String(length=255, collation=COL_UTF8MB4_BIN), nullable=False)
|
||||
|
||||
user = db.relationship('User', uselist=False, back_populates='comments', lazy="joined")
|
||||
|
||||
def __repr__(self):
|
||||
return '<Comment %r>' % self.id
|
||||
|
||||
@property
|
||||
def created_utc_timestamp(self):
|
||||
''' Returns a UTC POSIX timestamp, as seconds '''
|
||||
return (self.created_time - UTC_EPOCH).total_seconds()
|
||||
|
||||
|
||||
class UserLevelType(IntEnum):
|
||||
REGULAR = 0
|
||||
TRUSTED = 1
|
||||
ADMIN = 2
|
||||
MODERATOR = 2
|
||||
SUPERADMIN = 3
|
||||
|
||||
|
||||
|
@ -339,8 +376,8 @@ class User(db.Model):
|
|||
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")
|
||||
|
||||
torrents = db.relationship('Torrent', back_populates='user', lazy='dynamic')
|
||||
comments = db.relationship('Comment', back_populates='user', lazy='dynamic')
|
||||
# session = db.relationship('Session', uselist=False, back_populates='user')
|
||||
|
||||
def __init__(self, username, email, password):
|
||||
|
@ -363,6 +400,39 @@ class User(db.Model):
|
|||
]
|
||||
return all(checks)
|
||||
|
||||
def gravatar_url(self):
|
||||
# from http://en.gravatar.com/site/implement/images/python/
|
||||
size = 120
|
||||
# construct the url
|
||||
default_avatar = flask.url_for('static', filename='img/avatar/default.png', _external=True)
|
||||
gravatar_url = 'https://www.gravatar.com/avatar/{}?{}'.format(
|
||||
md5(self.email.encode('utf-8').lower()).hexdigest(),
|
||||
urlencode({'d': default_avatar, 's': str(size)}))
|
||||
return gravatar_url
|
||||
|
||||
@property
|
||||
def userlevel_str(self):
|
||||
if self.level == UserLevelType.REGULAR:
|
||||
return 'User'
|
||||
elif self.level == UserLevelType.TRUSTED:
|
||||
return 'Trusted'
|
||||
elif self.level >= UserLevelType.MODERATOR:
|
||||
return 'Moderator'
|
||||
|
||||
@property
|
||||
def userlevel_color(self):
|
||||
if self.level == UserLevelType.REGULAR:
|
||||
return 'default'
|
||||
elif self.level == UserLevelType.TRUSTED:
|
||||
return 'success'
|
||||
elif self.level >= UserLevelType.MODERATOR:
|
||||
return 'purple'
|
||||
|
||||
@property
|
||||
def ip_string(self):
|
||||
if self.last_login_ip:
|
||||
return str(ip_address(self.last_login_ip))
|
||||
|
||||
@classmethod
|
||||
def by_id(cls, id):
|
||||
return cls.query.get(id)
|
||||
|
@ -382,8 +452,8 @@ class User(db.Model):
|
|||
return cls.by_username(username_or_email) or cls.by_email(username_or_email)
|
||||
|
||||
@property
|
||||
def is_admin(self):
|
||||
return self.level >= UserLevelType.ADMIN
|
||||
def is_moderator(self):
|
||||
return self.level >= UserLevelType.MODERATOR
|
||||
|
||||
@property
|
||||
def is_superadmin(self):
|
||||
|
|
290
nyaa/routes.py
290
nyaa/routes.py
|
@ -7,11 +7,13 @@ from nyaa import torrents
|
|||
from nyaa import backend
|
||||
from nyaa import api_handler
|
||||
from nyaa.search import search_elastic, search_db
|
||||
from sqlalchemy.orm import joinedload
|
||||
import config
|
||||
|
||||
import re
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
import ipaddress
|
||||
from ipaddress import ip_address
|
||||
import os.path
|
||||
import base64
|
||||
from urllib.parse import quote
|
||||
|
@ -35,6 +37,10 @@ SERACH_PAGINATE_DISPLAY_MSG = ('Displaying results {start}-{end} out of {total}
|
|||
'what you were looking for.')
|
||||
|
||||
|
||||
# For static_cachebuster
|
||||
_static_cache = {}
|
||||
|
||||
|
||||
def redirect_url():
|
||||
url = flask.request.args.get('next') or \
|
||||
flask.request.referrer or \
|
||||
|
@ -44,6 +50,31 @@ def redirect_url():
|
|||
return url
|
||||
|
||||
|
||||
@app.template_global()
|
||||
def static_cachebuster(static_filename):
|
||||
''' Adds a ?t=<mtime> cachebuster to the given path, if the file exists.
|
||||
Results are cached in memory and persist until app restart! '''
|
||||
# Instead of timestamps, we could use commit hashes (we already load it in __init__)
|
||||
# But that'd mean every static resource would get cache busted. This lets unchanged items
|
||||
# stay in the cache.
|
||||
|
||||
if app.debug:
|
||||
# Do not bust cache on debug (helps debugging)
|
||||
return static_filename
|
||||
|
||||
# Get file mtime if not already cached.
|
||||
if static_filename not in _static_cache:
|
||||
file_path = os.path.join(app.config['BASE_DIR'], 'nyaa', static_filename[1:])
|
||||
if os.path.exists(file_path):
|
||||
file_mtime = int(os.path.getmtime(file_path))
|
||||
_static_cache[static_filename] = static_filename + '?t=' + str(file_mtime)
|
||||
else:
|
||||
# Throw a warning?
|
||||
_static_cache[static_filename] = static_filename
|
||||
|
||||
return _static_cache[static_filename]
|
||||
|
||||
|
||||
@app.template_global()
|
||||
def modify_query(**new_values):
|
||||
args = flask.request.args.copy()
|
||||
|
@ -132,8 +163,6 @@ def get_category_id_map():
|
|||
# Routes start here #
|
||||
|
||||
|
||||
app.register_blueprint(api_handler.api_blueprint, url_prefix='/api')
|
||||
|
||||
def chain_get(source, *args):
|
||||
''' Tries to return values from source by the given keys.
|
||||
Returns None if none match.
|
||||
|
@ -145,6 +174,7 @@ def chain_get(source, *args):
|
|||
return value
|
||||
return None
|
||||
|
||||
|
||||
@app.route('/rss', defaults={'rss': True})
|
||||
@app.route('/', defaults={'rss': False})
|
||||
def home(rss):
|
||||
|
@ -180,6 +210,26 @@ def home(rss):
|
|||
flask.abort(404)
|
||||
user_id = user.id
|
||||
|
||||
special_results = {
|
||||
'first_word_user': None,
|
||||
'query_sans_user': None,
|
||||
'infohash_torrent': None
|
||||
}
|
||||
# Add advanced features to searches (but not RSS or user searches)
|
||||
if search_term and not render_as_rss and not user_id:
|
||||
# Check if the first word of the search is an existing user
|
||||
user_word_match = re.match(r'^([a-zA-Z0-9_-]+) *(.*|$)', search_term)
|
||||
if user_word_match:
|
||||
special_results['first_word_user'] = models.User.by_username(user_word_match.group(1))
|
||||
special_results['query_sans_user'] = user_word_match.group(2)
|
||||
|
||||
# Check if search is a 40-char torrent hash
|
||||
infohash_match = re.match(r'(?i)^([a-f0-9]{40})$', search_term)
|
||||
if infohash_match:
|
||||
# Check for info hash in database
|
||||
matched_torrent = models.Torrent.by_info_hash_hex(infohash_match.group(1))
|
||||
special_results['infohash_torrent'] = matched_torrent
|
||||
|
||||
query_args = {
|
||||
'user': user_id,
|
||||
'sort': sort_key or 'id',
|
||||
|
@ -193,9 +243,17 @@ def home(rss):
|
|||
|
||||
if flask.g.user:
|
||||
query_args['logged_in_user'] = flask.g.user
|
||||
if flask.g.user.is_admin: # God mode
|
||||
if flask.g.user.is_moderator: # God mode
|
||||
query_args['admin'] = True
|
||||
|
||||
infohash_torrent = special_results.get('infohash_torrent')
|
||||
if infohash_torrent:
|
||||
# infohash_torrent is only set if this is not RSS or userpage search
|
||||
flask.flash(flask.Markup('You were redirected here because '
|
||||
'the given hash matched this torrent.'), 'info')
|
||||
# Redirect user from search to the torrent if we found one with the specific info_hash
|
||||
return flask.redirect(flask.url_for('view_torrent', torrent_id=infohash_torrent.id))
|
||||
|
||||
# If searching, we get results from elastic search
|
||||
use_elastic = app.config.get('USE_ELASTIC_SEARCH')
|
||||
if use_elastic and search_term:
|
||||
|
@ -212,9 +270,12 @@ def home(rss):
|
|||
query_results = search_elastic(**query_args)
|
||||
|
||||
if render_as_rss:
|
||||
return render_rss('"{}"'.format(search_term), query_results, use_elastic=True, magnet_links=use_magnet_links)
|
||||
return render_rss(
|
||||
'"{}"'.format(search_term), query_results,
|
||||
use_elastic=True, magnet_links=use_magnet_links)
|
||||
else:
|
||||
rss_query_string = _generate_query_string(search_term, category, quality_filter, user_name)
|
||||
rss_query_string = _generate_query_string(
|
||||
search_term, category, quality_filter, user_name)
|
||||
max_results = min(max_search_results, query_results['hits']['total'])
|
||||
# change p= argument to whatever you change page_parameter to or pagination breaks
|
||||
pagination = Pagination(p=query_args['page'], per_page=results_per_page,
|
||||
|
@ -225,7 +286,8 @@ def home(rss):
|
|||
pagination=pagination,
|
||||
torrent_query=query_results,
|
||||
search=query_args,
|
||||
rss_filter=rss_query_string)
|
||||
rss_filter=rss_query_string,
|
||||
special_results=special_results)
|
||||
else:
|
||||
# If ES is enabled, default to db search for browsing
|
||||
if use_elastic:
|
||||
|
@ -237,7 +299,8 @@ def home(rss):
|
|||
if render_as_rss:
|
||||
return render_rss('Home', query, use_elastic=False, magnet_links=use_magnet_links)
|
||||
else:
|
||||
rss_query_string = _generate_query_string(search_term, category, quality_filter, user_name)
|
||||
rss_query_string = _generate_query_string(
|
||||
search_term, category, quality_filter, user_name)
|
||||
# Use elastic is always false here because we only hit this section
|
||||
# if we're browsing without a search term (which means we default to DB)
|
||||
# or if ES is disabled
|
||||
|
@ -245,7 +308,8 @@ def home(rss):
|
|||
use_elastic=False,
|
||||
torrent_query=query,
|
||||
search=query_args,
|
||||
rss_filter=rss_query_string)
|
||||
rss_filter=rss_query_string,
|
||||
special_results=special_results)
|
||||
|
||||
|
||||
@app.route('/user/<user_name>', methods=['GET', 'POST'])
|
||||
|
@ -255,22 +319,23 @@ def view_user(user_name):
|
|||
if not user:
|
||||
flask.abort(404)
|
||||
|
||||
if flask.g.user and flask.g.user.id != user.id:
|
||||
admin = flask.g.user.is_admin
|
||||
superadmin = flask.g.user.is_superadmin
|
||||
else:
|
||||
admin = False
|
||||
superadmin = False
|
||||
admin_form = None
|
||||
if flask.g.user and flask.g.user.is_moderator and flask.g.user.level > user.level:
|
||||
admin_form = forms.UserForm()
|
||||
default, admin_form.user_class.choices = _create_user_class_choices(user)
|
||||
if flask.request.method == 'GET':
|
||||
admin_form.user_class.data = default
|
||||
|
||||
form = forms.UserForm()
|
||||
form.user_class.choices = _create_user_class_choices()
|
||||
if flask.request.method == 'POST' and form.validate():
|
||||
selection = form.user_class.data
|
||||
if flask.request.method == 'POST' and admin_form and admin_form.validate():
|
||||
selection = admin_form.user_class.data
|
||||
|
||||
if selection == 'regular':
|
||||
user.level = models.UserLevelType.REGULAR
|
||||
elif selection == 'trusted':
|
||||
user.level = models.UserLevelType.TRUSTED
|
||||
elif selection == 'moderator':
|
||||
user.level = models.UserLevelType.MODERATOR
|
||||
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
|
||||
|
@ -310,7 +375,7 @@ def view_user(user_name):
|
|||
|
||||
if flask.g.user:
|
||||
query_args['logged_in_user'] = flask.g.user
|
||||
if flask.g.user.is_admin: # God mode
|
||||
if flask.g.user.is_moderator: # God mode
|
||||
query_args['admin'] = True
|
||||
|
||||
# Use elastic search for term searching
|
||||
|
@ -343,9 +408,7 @@ def view_user(user_name):
|
|||
user_page=True,
|
||||
rss_filter=rss_query_string,
|
||||
level=user_level,
|
||||
admin=admin,
|
||||
superadmin=superadmin,
|
||||
form=form)
|
||||
admin_form=admin_form)
|
||||
# Similar logic as home page
|
||||
else:
|
||||
if use_elastic:
|
||||
|
@ -361,9 +424,7 @@ def view_user(user_name):
|
|||
user_page=True,
|
||||
rss_filter=rss_query_string,
|
||||
level=user_level,
|
||||
admin=admin,
|
||||
superadmin=superadmin,
|
||||
form=form)
|
||||
admin_form=admin_form)
|
||||
|
||||
|
||||
@app.template_filter('rfc822')
|
||||
|
@ -416,7 +477,7 @@ def login():
|
|||
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
|
||||
user.last_login_ip = ip_address(flask.request.remote_addr).packed
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
|
||||
|
@ -450,7 +511,7 @@ def register():
|
|||
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
|
||||
user.last_login_ip = ip_address(flask.request.remote_addr).packed
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
|
||||
|
@ -478,14 +539,6 @@ def profile():
|
|||
|
||||
form = forms.ProfileForm(flask.request.form)
|
||||
|
||||
level = 'Regular'
|
||||
if flask.g.user.is_admin:
|
||||
level = 'Moderator'
|
||||
if flask.g.user.is_superadmin: # check this second because we can be admin AND superadmin
|
||||
level = 'Administrator'
|
||||
elif flask.g.user.is_trusted:
|
||||
level = 'Trusted'
|
||||
|
||||
if flask.request.method == 'POST' and form.validate():
|
||||
user = flask.g.user
|
||||
new_email = form.email.data.strip()
|
||||
|
@ -515,12 +568,7 @@ def profile():
|
|||
flask.g.user = user
|
||||
return flask.redirect('/profile')
|
||||
|
||||
_user = models.User.by_id(flask.g.user.id)
|
||||
username = _user.username
|
||||
current_email = _user.email
|
||||
|
||||
return flask.render_template('profile.html', form=form, name=username, email=current_email,
|
||||
level=level)
|
||||
return flask.render_template('profile.html', form=form)
|
||||
|
||||
|
||||
@app.route('/user/activate/<payload>')
|
||||
|
@ -562,34 +610,65 @@ def _create_upload_category_choices():
|
|||
|
||||
@app.route('/upload', methods=['GET', 'POST'])
|
||||
def upload():
|
||||
form = forms.UploadForm(CombinedMultiDict((flask.request.files, flask.request.form)))
|
||||
form.category.choices = _create_upload_category_choices()
|
||||
upload_form = forms.UploadForm(CombinedMultiDict((flask.request.files, flask.request.form)))
|
||||
upload_form.category.choices = _create_upload_category_choices()
|
||||
|
||||
if flask.request.method == 'POST' and form.validate():
|
||||
torrent = backend.handle_torrent_upload(form, flask.g.user)
|
||||
if flask.request.method == 'POST' and upload_form.validate():
|
||||
torrent = backend.handle_torrent_upload(upload_form, flask.g.user)
|
||||
|
||||
return flask.redirect('/view/' + str(torrent.id))
|
||||
else:
|
||||
# If we get here with a POST, it means the form data was invalid: return a non-okay status
|
||||
status_code = 400 if flask.request.method == 'POST' else 200
|
||||
return flask.render_template('upload.html', form=form, user=flask.g.user), status_code
|
||||
return flask.render_template('upload.html', upload_form=upload_form), status_code
|
||||
|
||||
|
||||
@app.route('/view/<int:torrent_id>')
|
||||
@app.route('/view/<int:torrent_id>', methods=['GET', 'POST'])
|
||||
def view_torrent(torrent_id):
|
||||
if flask.request.method == 'POST':
|
||||
torrent = models.Torrent.by_id(torrent_id)
|
||||
|
||||
viewer = flask.g.user
|
||||
|
||||
else:
|
||||
torrent = models.Torrent.query \
|
||||
.options(joinedload('filelist'),
|
||||
joinedload('comments')) \
|
||||
.filter_by(id=torrent_id) \
|
||||
.first()
|
||||
if not torrent:
|
||||
flask.abort(404)
|
||||
|
||||
# Only allow admins see deleted torrents
|
||||
if torrent.deleted and not (viewer and viewer.is_admin):
|
||||
if torrent.deleted and not (flask.g.user and flask.g.user.is_moderator):
|
||||
flask.abort(404)
|
||||
|
||||
comment_form = None
|
||||
if flask.g.user:
|
||||
comment_form = forms.CommentForm()
|
||||
|
||||
if flask.request.method == 'POST':
|
||||
if not flask.g.user:
|
||||
flask.abort(403)
|
||||
|
||||
if comment_form.validate():
|
||||
comment_text = (comment_form.comment.data or '').strip()
|
||||
|
||||
comment = models.Comment(
|
||||
torrent_id=torrent_id,
|
||||
user_id=flask.g.user.id,
|
||||
text=comment_text)
|
||||
|
||||
db.session.add(comment)
|
||||
db.session.commit()
|
||||
|
||||
torrent_count = models.Comment.query.filter_by(torrent_id=torrent.id).count()
|
||||
|
||||
flask.flash('Comment successfully posted.', 'success')
|
||||
|
||||
return flask.redirect(flask.url_for('view_torrent',
|
||||
torrent_id=torrent_id,
|
||||
_anchor='com-' + str(torrent_count)))
|
||||
|
||||
# Only allow owners and admins to edit torrents
|
||||
can_edit = viewer and (viewer is torrent.user or viewer.is_admin)
|
||||
can_edit = flask.g.user and (flask.g.user is torrent.user or flask.g.user.is_moderator)
|
||||
|
||||
files = None
|
||||
if torrent.filelist:
|
||||
|
@ -598,11 +677,32 @@ def view_torrent(torrent_id):
|
|||
report_form = forms.ReportForm()
|
||||
return flask.render_template('view.html', torrent=torrent,
|
||||
files=files,
|
||||
viewer=viewer,
|
||||
comment_form=comment_form,
|
||||
comments=torrent.comments,
|
||||
can_edit=can_edit,
|
||||
report_form=report_form)
|
||||
|
||||
|
||||
@app.route('/view/<int:torrent_id>/comment/<int:comment_id>/delete', methods=['POST'])
|
||||
def delete_comment(torrent_id, comment_id):
|
||||
if not flask.g.user:
|
||||
flask.abort(403)
|
||||
|
||||
comment = models.Comment.query.filter_by(id=comment_id).first()
|
||||
if not comment:
|
||||
flask.abort(404)
|
||||
|
||||
if not (comment.user.id == flask.g.user.id or flask.g.user.is_moderator):
|
||||
flask.abort(403)
|
||||
|
||||
db.session.delete(comment)
|
||||
db.session.commit()
|
||||
|
||||
flask.flash('Comment successfully deleted.', 'success')
|
||||
|
||||
return flask.redirect(flask.url_for('view_torrent', torrent_id=torrent_id))
|
||||
|
||||
|
||||
@app.route('/view/<int:torrent_id>/edit', methods=['GET', 'POST'])
|
||||
def edit_torrent(torrent_id):
|
||||
torrent = models.Torrent.by_id(torrent_id)
|
||||
|
@ -615,11 +715,11 @@ def edit_torrent(torrent_id):
|
|||
flask.abort(404)
|
||||
|
||||
# Only allow admins edit deleted torrents
|
||||
if torrent.deleted and not (editor and editor.is_admin):
|
||||
if torrent.deleted and not (flask.g.user and flask.g.user.is_moderator):
|
||||
flask.abort(404)
|
||||
|
||||
# Only allow torrent owners or admins edit torrents
|
||||
if not editor or not (editor is torrent.user or editor.is_admin):
|
||||
if not flask.g.user or not (flask.g.user is torrent.user or flask.g.user.is_moderator):
|
||||
flask.abort(403)
|
||||
|
||||
if flask.request.method == 'POST' and form.validate():
|
||||
|
@ -635,15 +735,16 @@ def edit_torrent(torrent_id):
|
|||
torrent.complete = form.is_complete.data
|
||||
torrent.anonymous = form.is_anonymous.data
|
||||
|
||||
if editor.is_trusted:
|
||||
if flask.g.user.is_trusted:
|
||||
torrent.trusted = form.is_trusted.data
|
||||
if editor.is_admin:
|
||||
if flask.g.user.is_moderator:
|
||||
torrent.deleted = form.is_deleted.data
|
||||
|
||||
db.session.commit()
|
||||
|
||||
flask.flash(flask.Markup(
|
||||
'Torrent has been successfully edited! Changes might take a few minutes to show up.'), 'info')
|
||||
'Torrent has been successfully edited! Changes might take a few minutes to show up.'),
|
||||
'info')
|
||||
|
||||
return flask.redirect(flask.url_for('view_torrent', torrent_id=torrent.id))
|
||||
else:
|
||||
|
@ -664,8 +765,7 @@ def edit_torrent(torrent_id):
|
|||
|
||||
return flask.render_template('edit.html',
|
||||
form=form,
|
||||
torrent=torrent,
|
||||
editor=editor)
|
||||
torrent=torrent)
|
||||
|
||||
|
||||
@app.route('/view/<int:torrent_id>/magnet')
|
||||
|
@ -679,15 +779,16 @@ def redirect_magnet(torrent_id):
|
|||
|
||||
|
||||
@app.route('/view/<int:torrent_id>/torrent')
|
||||
@app.route('/download/<int:torrent_id>.torrent')
|
||||
def download_torrent(torrent_id):
|
||||
torrent = models.Torrent.by_id(torrent_id)
|
||||
|
||||
if not torrent:
|
||||
if not torrent or not torrent.has_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(
|
||||
resp.headers['Content-Disposition'] = 'inline; filename="{0}"; filename*=UTF-8\'\'{0}'.format(
|
||||
quote(torrent.torrent_name.encode('utf-8')))
|
||||
|
||||
return resp
|
||||
|
@ -717,7 +818,7 @@ def submit_report(torrent_id):
|
|||
|
||||
@app.route('/reports', methods=['GET', 'POST'])
|
||||
def view_reports():
|
||||
if not flask.g.user or not flask.g.user.is_admin:
|
||||
if not flask.g.user or not flask.g.user.is_moderator:
|
||||
flask.abort(403)
|
||||
|
||||
page = flask.request.args.get('p', flask.request.args.get('offset', 1, int), int)
|
||||
|
@ -800,14 +901,55 @@ def send_verification_email(to_address, activ_link):
|
|||
server.quit()
|
||||
|
||||
|
||||
def _create_user_class_choices():
|
||||
def _create_user_class_choices(user):
|
||||
choices = [('regular', 'Regular')]
|
||||
if flask.g.user and flask.g.user.is_superadmin:
|
||||
default = 'regular'
|
||||
if flask.g.user:
|
||||
if flask.g.user.is_moderator:
|
||||
choices.append(('trusted', 'Trusted'))
|
||||
return choices
|
||||
if flask.g.user.is_superadmin:
|
||||
choices.append(('moderator', 'Moderator'))
|
||||
|
||||
if user:
|
||||
if user.is_moderator:
|
||||
default = 'moderator'
|
||||
elif user.is_trusted:
|
||||
default = 'trusted'
|
||||
|
||||
return default, choices
|
||||
|
||||
|
||||
@app.template_filter()
|
||||
def timesince(dt, default='just now'):
|
||||
"""
|
||||
Returns string representing "time since" e.g.
|
||||
3 minutes ago, 5 hours ago etc.
|
||||
Date and time (UTC) are returned if older than 1 day.
|
||||
"""
|
||||
|
||||
now = datetime.utcnow()
|
||||
diff = now - dt
|
||||
|
||||
periods = (
|
||||
(diff.days, 'day', 'days'),
|
||||
(diff.seconds / 3600, 'hour', 'hours'),
|
||||
(diff.seconds / 60, 'minute', 'minutes'),
|
||||
(diff.seconds, 'second', 'seconds'),
|
||||
)
|
||||
|
||||
if diff.days >= 1:
|
||||
return dt.strftime('%Y-%m-%d %H:%M UTC')
|
||||
else:
|
||||
for period, singular, plural in periods:
|
||||
|
||||
if period >= 1:
|
||||
return '%d %s ago' % (period, singular if int(period) == 1 else plural)
|
||||
|
||||
return default
|
||||
|
||||
# #################################### STATIC PAGES ####################################
|
||||
|
||||
|
||||
@app.route('/rules', methods=['GET'])
|
||||
def site_rules():
|
||||
return flask.render_template('rules.html')
|
||||
|
@ -818,11 +960,11 @@ def site_help():
|
|||
return flask.render_template('help.html')
|
||||
|
||||
|
||||
@app.route('/xmlns/nyaa', methods=['GET'])
|
||||
def xmlns_nyaa():
|
||||
return flask.render_template('xmlns.html')
|
||||
|
||||
|
||||
# #################################### API ROUTES ####################################
|
||||
@app.route('/api/upload', methods=['POST'])
|
||||
def api_upload():
|
||||
is_valid_user, user, debug = api_handler.validate_user(flask.request)
|
||||
if not is_valid_user:
|
||||
return flask.make_response(flask.jsonify({"Failure": "Invalid username or password."}), 400)
|
||||
api_response = api_handler.api_upload(flask.request, user)
|
||||
return api_response
|
||||
|
||||
app.register_blueprint(api_handler.api_blueprint, url_prefix='/api')
|
||||
|
|
277
nyaa/static/css/bootstrap-xl-mod.css
vendored
Normal file
277
nyaa/static/css/bootstrap-xl-mod.css
vendored
Normal file
|
@ -0,0 +1,277 @@
|
|||
/*
|
||||
* CSS file with Bootstrap grid classes for screens bigger than 1600px. Just add this file after the Bootstrap CSS file and you will be able to juse col-xl, col-xl-push, hidden-xl, etc.
|
||||
*
|
||||
* Author: Marc van Nieuwenhuijzen
|
||||
* Company: WebVakman
|
||||
* Site: WebVakman.nl
|
||||
*
|
||||
* Edited to be for >=1480px with container width of 1400px for Nyaa.si
|
||||
* Also edited to not fuck up column gutters.
|
||||
*/
|
||||
|
||||
@media (min-width: 1200px) and (max-width: 1479px) {
|
||||
.hidden-lg {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.visible-xl-block,
|
||||
.visible-xl-inline,
|
||||
.visible-xl-inline-block,
|
||||
.visible-xl{
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@media (min-width: 1480px) {
|
||||
.container {
|
||||
width: 1400px;
|
||||
}
|
||||
|
||||
.col-xl-1, .col-xl-2, .col-xl-3, .col-xl-4, .col-xl-5, .col-xl-6, .col-xl-7, .col-xl-8, .col-xl-9, .col-xl-10, .col-xl-11, .col-xl-12 {
|
||||
position: relative;
|
||||
min-height: 1px;
|
||||
padding-right: 15px;
|
||||
padding-left: 15px;
|
||||
|
||||
float: left;
|
||||
}
|
||||
|
||||
.col-xl-12 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.col-xl-11 {
|
||||
width: 91.66666667%;
|
||||
}
|
||||
|
||||
.col-xl-10 {
|
||||
width: 83.33333333%;
|
||||
}
|
||||
|
||||
.col-xl-9 {
|
||||
width: 75%;
|
||||
}
|
||||
|
||||
.col-xl-8 {
|
||||
width: 66.66666667%;
|
||||
}
|
||||
|
||||
.col-xl-7 {
|
||||
width: 58.33333333%;
|
||||
}
|
||||
|
||||
.col-xl-6 {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.col-xl-5 {
|
||||
width: 41.66666667%;
|
||||
}
|
||||
|
||||
.col-xl-4 {
|
||||
width: 33.33333333%;
|
||||
}
|
||||
|
||||
.col-xl-3 {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
.col-xl-2 {
|
||||
width: 16.66666667%;
|
||||
}
|
||||
|
||||
.col-xl-1 {
|
||||
width: 8.33333333%;
|
||||
}
|
||||
|
||||
.col-xl-pull-12 {
|
||||
right: 100%;
|
||||
}
|
||||
|
||||
.col-xl-pull-11 {
|
||||
right: 91.66666667%;
|
||||
}
|
||||
|
||||
.col-xl-pull-10 {
|
||||
right: 83.33333333%;
|
||||
}
|
||||
|
||||
.col-xl-pull-9 {
|
||||
right: 75%;
|
||||
}
|
||||
|
||||
.col-xl-pull-8 {
|
||||
right: 66.66666667%;
|
||||
}
|
||||
|
||||
.col-xl-pull-7 {
|
||||
right: 58.33333333%;
|
||||
}
|
||||
|
||||
.col-xl-pull-6 {
|
||||
right: 50%;
|
||||
}
|
||||
|
||||
.col-xl-pull-5 {
|
||||
right: 41.66666667%;
|
||||
}
|
||||
|
||||
.col-xl-pull-4 {
|
||||
right: 33.33333333%;
|
||||
}
|
||||
|
||||
.col-xl-pull-3 {
|
||||
right: 25%;
|
||||
}
|
||||
|
||||
.col-xl-pull-2 {
|
||||
right: 16.66666667%;
|
||||
}
|
||||
|
||||
.col-xl-pull-1 {
|
||||
right: 8.33333333%;
|
||||
}
|
||||
|
||||
.col-xl-pull-0 {
|
||||
right: auto;
|
||||
}
|
||||
|
||||
.col-xl-push-12 {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.col-xl-push-11 {
|
||||
left: 91.66666667%;
|
||||
}
|
||||
|
||||
.col-xl-push-10 {
|
||||
left: 83.33333333%;
|
||||
}
|
||||
|
||||
.col-xl-push-9 {
|
||||
left: 75%;
|
||||
}
|
||||
|
||||
.col-xl-push-8 {
|
||||
left: 66.66666667%;
|
||||
}
|
||||
|
||||
.col-xl-push-7 {
|
||||
left: 58.33333333%;
|
||||
}
|
||||
|
||||
.col-xl-push-6 {
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
.col-xl-push-5 {
|
||||
left: 41.66666667%;
|
||||
}
|
||||
|
||||
.col-xl-push-4 {
|
||||
left: 33.33333333%;
|
||||
}
|
||||
|
||||
.col-xl-push-3 {
|
||||
left: 25%;
|
||||
}
|
||||
|
||||
.col-xl-push-2 {
|
||||
left: 16.66666667%;
|
||||
}
|
||||
|
||||
.col-xl-push-1 {
|
||||
left: 8.33333333%;
|
||||
}
|
||||
|
||||
.col-xl-push-0 {
|
||||
left: auto;
|
||||
}
|
||||
|
||||
.col-xl-offset-12 {
|
||||
margin-left: 100%;
|
||||
}
|
||||
|
||||
.col-xl-offset-11 {
|
||||
margin-left: 91.66666667%;
|
||||
}
|
||||
|
||||
.col-xl-offset-10 {
|
||||
margin-left: 83.33333333%;
|
||||
}
|
||||
|
||||
.col-xl-offset-9 {
|
||||
margin-left: 75%;
|
||||
}
|
||||
|
||||
.col-xl-offset-8 {
|
||||
margin-left: 66.66666667%;
|
||||
}
|
||||
|
||||
.col-xl-offset-7 {
|
||||
margin-left: 58.33333333%;
|
||||
}
|
||||
|
||||
.col-xl-offset-6 {
|
||||
margin-left: 50%;
|
||||
}
|
||||
|
||||
.col-xl-offset-5 {
|
||||
margin-left: 41.66666667%;
|
||||
}
|
||||
|
||||
.col-xl-offset-4 {
|
||||
margin-left: 33.33333333%;
|
||||
}
|
||||
|
||||
.col-xl-offset-3 {
|
||||
margin-left: 25%;
|
||||
}
|
||||
|
||||
.col-xl-offset-2 {
|
||||
margin-left: 16.66666667%;
|
||||
}
|
||||
|
||||
.col-xl-offset-1 {
|
||||
margin-left: 8.33333333%;
|
||||
}
|
||||
|
||||
.col-xl-offset-0 {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.visible-xl {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
table.visible-xl {
|
||||
display: table;
|
||||
}
|
||||
|
||||
tr.visible-xl {
|
||||
display: table-row !important;
|
||||
}
|
||||
|
||||
th.visible-xl, td.visible-xl {
|
||||
display: table-cell !important;
|
||||
}
|
||||
|
||||
.visible-xl-block {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.visible-xl-inline {
|
||||
display: inline !important;
|
||||
}
|
||||
|
||||
.visible-xl-inline-block {
|
||||
display: inline-block !important;
|
||||
}
|
||||
|
||||
.hidden-xl {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
|
@ -96,6 +96,10 @@ table.torrent-list tbody tr td a:visited {
|
|||
padding: 1em 0;
|
||||
}
|
||||
|
||||
.markdown-source {
|
||||
min-height: 360px;
|
||||
}
|
||||
|
||||
@media (max-width: 991px){
|
||||
.panel-body .col-md-5 {
|
||||
margin-left: 20px;
|
||||
|
@ -218,3 +222,106 @@ table.torrent-list tbody tr td a:visited {
|
|||
ul.nav-tabs#profileTabs {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.comment-panel {
|
||||
width: 99%;
|
||||
margin: 0 auto;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.comment-panel:target {
|
||||
border-color: black;
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
.text-purple, a.text-purple:visited { color: #a760bc; }
|
||||
a.text-purple:hover, a.text-purple:active, a.text-purple:focus { color: #a760e0; }
|
||||
|
||||
.comment-content {
|
||||
word-break: break-all;
|
||||
}
|
||||
.comment-content img {
|
||||
max-width: 100%;
|
||||
max-height: 600px;
|
||||
}
|
||||
|
||||
.comment-box {
|
||||
width: 95%;
|
||||
margin: 0 auto;
|
||||
margin-top: 30px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.delete-comment-form {
|
||||
position: relative;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
max-width: 120px;
|
||||
}
|
||||
|
||||
.btn-grey {
|
||||
color: #000000;
|
||||
background-color: #cccfd2;
|
||||
border-color: #ccc;
|
||||
}
|
||||
.btn-grey:hover, .btn-grey:focus, .btn-grey:active, .btn-grey.active, .open > .dropdown-toggle.btn-grey {
|
||||
background-color: #aaaaaa;
|
||||
}
|
||||
.btn span.glyphicon-check {
|
||||
display: none;
|
||||
}
|
||||
.btn.active span.glyphicon-check {
|
||||
display: inline;
|
||||
}
|
||||
.btn span.glyphicon-unchecked {
|
||||
display: inline;
|
||||
}
|
||||
.btn.active span.glyphicon-unchecked {
|
||||
display: none;
|
||||
}
|
||||
.center {
|
||||
float: none;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Torrent file list */
|
||||
.torrent-file-list ul {
|
||||
padding: 5px 20px 0px 20px;
|
||||
list-style: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.torrent-file-list > ul {
|
||||
display: block; /* First level always visible */
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.torrent-file-list ul[data-show] {
|
||||
/* Used to show first level's items based on amount */
|
||||
display: block;
|
||||
}
|
||||
|
||||
.torrent-file-list li:not(:last-of-type) {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.torrent-file-list i.fa {
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.torrent-file-list i.fa-folder-open {
|
||||
padding-right: 3px;
|
||||
}
|
||||
|
||||
.torrent-file-list a.folder {
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.torrent-file-list .file-size {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
|
BIN
nyaa/static/img/avatar/default.png
Normal file
BIN
nyaa/static/img/avatar/default.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.8 KiB |
|
@ -43,6 +43,7 @@ $(document).ready(function() {
|
|||
}
|
||||
});
|
||||
|
||||
// Drag & Drop zone for upload page
|
||||
$('body').on('dragenter', function(event) {
|
||||
event.preventDefault();
|
||||
dropZone.css({ 'visibility': 'visible', 'opacity': 1 });
|
||||
|
@ -63,6 +64,13 @@ $(document).ready(function() {
|
|||
$('#torrent_file')[0].files = files;
|
||||
$(this).css({ 'visibility': 'hidden', 'opacity': 0 });
|
||||
});
|
||||
|
||||
// Collapsible file lists
|
||||
$('.torrent-file-list a.folder').click(function(e) {
|
||||
e.preventDefault();
|
||||
$(this).blur().children('i').toggleClass('fa-folder-open fa-folder');
|
||||
$(this).next().stop().slideToggle(250);
|
||||
});
|
||||
});
|
||||
|
||||
function _format_time_difference(seconds) {
|
||||
|
@ -80,6 +88,8 @@ function _format_time_difference(seconds) {
|
|||
if (seconds < 0) {
|
||||
suffix = "";
|
||||
prefix = "After ";
|
||||
} else if (seconds == 0) {
|
||||
return "Just now"
|
||||
}
|
||||
|
||||
var parts = [];
|
||||
|
@ -96,11 +106,12 @@ function _format_time_difference(seconds) {
|
|||
}
|
||||
return prefix + parts.join(" ") + suffix;
|
||||
}
|
||||
function _format_date(date) {
|
||||
function _format_date(date, show_seconds) {
|
||||
var pad = function (n) { return ("00" + n).slice(-2); }
|
||||
var ymd = date.getFullYear() + "-" + pad(date.getMonth()+1) + "-" + pad(date.getDate());
|
||||
var hm = pad(date.getHours()) + ":" + pad(date.getMinutes());
|
||||
return ymd + " " + hm;
|
||||
var s = show_seconds ? ":" + pad(date.getSeconds()) : ""
|
||||
return ymd + " " + hm + s;
|
||||
}
|
||||
|
||||
// Add title text to elements with data-timestamp attribute
|
||||
|
@ -111,11 +122,20 @@ document.addEventListener("DOMContentLoaded", function(event) {
|
|||
for (var i = 0; i < timestamp_targets.length; i++) {
|
||||
var target = timestamp_targets[i];
|
||||
var torrent_timestamp = parseInt(target.getAttribute('data-timestamp'));
|
||||
var swap_flag = target.getAttribute('data-timestamp-swap') != null;
|
||||
|
||||
if (torrent_timestamp) {
|
||||
var timedelta = now_timestamp - torrent_timestamp;
|
||||
target.setAttribute('title', _format_time_difference(timedelta));
|
||||
|
||||
target.innerText = _format_date(new Date(torrent_timestamp*1000));
|
||||
var formatted_date = _format_date(new Date(torrent_timestamp*1000), swap_flag);
|
||||
var formatted_timedelta = _format_time_difference(timedelta);
|
||||
if (swap_flag) {
|
||||
target.setAttribute('title', formatted_date);
|
||||
target.innerText = formatted_timedelta;
|
||||
} else {
|
||||
target.setAttribute('title', formatted_timedelta);
|
||||
target.innerText = formatted_date;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
{% extends "layout.html" %}
|
||||
{% block title %}404 Not Found :: {{ config.SITE_NAME }}{% endblock %}
|
||||
{% block metatags %}
|
||||
<meta property="og:description" content="Nothing here.">
|
||||
{% endblock %}
|
||||
{% block body %}
|
||||
<h1>404 Not Found</h1>
|
||||
<p>The path you requested does not exist on this server.</p>
|
||||
|
|
|
@ -37,12 +37,12 @@
|
|||
{{ field.label(class='control-label') }}
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li role="presentation" class="active">
|
||||
<a href="#{{ field_name }}-tab" aria-controls="" role="tab" data-toggle="tab">
|
||||
<a href="#{{ field_name }}-tab" role="tab" data-toggle="tab">
|
||||
Write
|
||||
</a>
|
||||
</li>
|
||||
<li role="presentation">
|
||||
<a href="#{{ field_name }}-preview" id="{{ field_name }}-preview-tab" aria-controls="preview" role="tab" data-toggle="tab">
|
||||
<a href="#{{ field_name }}-preview" id="{{ field_name }}-preview-tab" role="tab" data-toggle="tab">
|
||||
Preview
|
||||
</a>
|
||||
</li>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
{% set torrent_url = url_for('view_torrent', torrent_id=torrent.id) %}
|
||||
<h1>
|
||||
Edit Torrent <a href="{{ torrent_url }}">#{{torrent.id}}</a>
|
||||
{% if (torrent.user != None) and (torrent.user != editor) %}
|
||||
{% if (torrent.user != None) and (torrent.user != g.user) %}
|
||||
(by <a href="{{ url_for('view_user', user_name=torrent.user.username) }}">{{ torrent.user.username }}</a>)
|
||||
{% endif %}
|
||||
</h1>
|
||||
|
@ -29,38 +29,51 @@
|
|||
{{ render_field(form.information, class_='form-control', placeholder='Your website or IRC channel') }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="control-label">Torrent flags</label>
|
||||
<div>
|
||||
{% if editor.is_admin %}
|
||||
<label class="btn btn-primary">
|
||||
{{ form.is_deleted }}
|
||||
Deleted
|
||||
</label>
|
||||
{% endif %}
|
||||
|
||||
<label class="btn btn-default" style="background-color: darkgray; border-color: #ccc;" title="Hide torrent from listing">
|
||||
{{ form.is_hidden }}
|
||||
Hidden
|
||||
</label>
|
||||
<label class="btn btn-danger" title="This torrent is derived from another release">
|
||||
{{ form.is_remake }}
|
||||
Remake
|
||||
</label>
|
||||
<label class="btn btn-primary" title="This torrent is a complete batch (eg. season)">
|
||||
{{ form.is_complete }}
|
||||
Complete
|
||||
</label>
|
||||
|
||||
<label class="control-label">Torrent flags</label><br>
|
||||
<div class="btn-group" data-toggle="buttons">
|
||||
{# Only allow changing anonymous status when an uploader exists #}
|
||||
{% if torrent.uploader_id %}
|
||||
<label class="btn btn-primary" title="Upload torrent anonymously (don't display your username)">
|
||||
<label class="btn btn-default {% if torrent.anonymous %}active{% endif %}" title="Upload torrent anonymously (don't display your username)">
|
||||
{{ form.is_anonymous }}
|
||||
<span class="glyphicon glyphicon-check"></span>
|
||||
<span class="glyphicon glyphicon-unchecked"></span>
|
||||
Anonymous
|
||||
</label>
|
||||
{% endif %}
|
||||
{% if editor.is_trusted %}
|
||||
<label class="btn btn-success" title="Mark torrent trusted">
|
||||
<label class="btn btn-grey {% if torrent.hidden %}active{% endif %}" title="Hide torrent from listing">
|
||||
{{ form.is_hidden }}
|
||||
<span class="glyphicon glyphicon-check"></span>
|
||||
<span class="glyphicon glyphicon-unchecked"></span>
|
||||
Hidden
|
||||
</label>
|
||||
{% if g.user.is_moderator %}
|
||||
<label class="btn btn-primary {% if torrent.deleted %}active{% endif %}">
|
||||
{{ form.is_deleted }}
|
||||
<span class="glyphicon glyphicon-check"></span>
|
||||
<span class="glyphicon glyphicon-unchecked"></span>
|
||||
Deleted
|
||||
</label>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="hidden-xl"><br></div>
|
||||
<div class="btn-group" data-toggle="buttons">
|
||||
<label class="btn btn-danger {% if torrent.remake %}active{% endif %}" title="This torrent is derived from another release">
|
||||
{{ form.is_remake }}
|
||||
<span class="glyphicon glyphicon-check"></span>
|
||||
<span class="glyphicon glyphicon-unchecked"></span>
|
||||
Remake
|
||||
</label>
|
||||
<label class="btn btn-warning {% if torrent.complete %}active{% endif %}" title="This torrent is a complete batch (eg. season)">
|
||||
{{ form.is_complete }}
|
||||
<span class="glyphicon glyphicon-check"></span>
|
||||
<span class="glyphicon glyphicon-unchecked"></span>
|
||||
Complete
|
||||
</label>
|
||||
{% if g.user.is_trusted %}
|
||||
<label class="btn btn-success {% if torrent.trusted %}active{% endif %}" title="Mark torrent trusted">
|
||||
{{ form.is_trusted }}
|
||||
<span class="glyphicon glyphicon-check"></span>
|
||||
<span class="glyphicon glyphicon-unchecked"></span>
|
||||
Trusted
|
||||
</label>
|
||||
{% endif %}
|
||||
|
|
|
@ -1,15 +1,22 @@
|
|||
{% extends "layout.html" %}
|
||||
{% block title %}{% if search.term %}{{ search.term | e}}{% else %}Browse{% endif %} :: {{ config.SITE_NAME }}{% endblock %}
|
||||
{% block metatags %}
|
||||
{% if search.term %}
|
||||
<meta property="og:description" content="Search for '{{ search.term }}'">
|
||||
{% else %}
|
||||
<meta property="og:description" content="{{ config.SITE_NAME }} homepage">
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block body %}
|
||||
|
||||
{% if search["term"] == '' %}
|
||||
<div class="alert alert-info">
|
||||
<p><strong>5/18 Update:</strong> We've added an upload api for ease of uploading. See documentation <a href="https://github.com/nyaadevs/nyaa/blob/master/utils/api_uploader.py">here</a>.</p>
|
||||
<p><strong>5/17 Update:</strong> We've added faster and more accurate search! In addition to your typical keyword search in both English and other languages, you can also now use powerful operators
|
||||
like <kbd>clockwork planet -horrible</kbd> or <kbd>commie|horrible|cartel yowamushi</kbd> to search. For all supported operators, please click <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-simple-query-string-query.html#_simple_query_string_syntax">here</a>. More features are coming soon!</p><br>
|
||||
<p><strong>2017-05-22 Update:</strong> We've added comments. You can change your avatar using <a href="http://en.gravatar.com">Gravatar</a> or if you don't like gravatar you can just stick with our spify default avatar.</p>
|
||||
<p><strong>2017-05-22 Update:</strong> We've updated our upload API to v2 (v1 <b>is now disabled!</b>). See documentation <b><a href="https://github.com/nyaadevs/nyaa/blob/master/utils/api_uploader_v2.py">here</a></b>.</p>
|
||||
<p>We welcome you to provide feedback at <a href="irc://irc.rizon.net/nyaa-dev">#nyaa-dev@irc.rizon.net</a></p>
|
||||
<p>Our GitHub: <a href="https://github.com/nyaadevs" target="_blank">https://github.com/nyaadevs</a> - creating <a href="https://github.com/nyaadevs/nyaa/issues">issues</a> for features and faults is recommendable!</p>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% include "search_results.html" %}
|
||||
|
||||
|
|
|
@ -4,38 +4,49 @@
|
|||
<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 name="viewport" content="width=device-width">
|
||||
<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">
|
||||
<link rel="mask-icon" href="/static/pinned-tab.svg" color="#3582F7">
|
||||
<link rel="alternate" type="application/rss+xml" href="{% if rss_filter %}{{ url_for('home', page='rss', _external=True, **rss_filter) }}{% else %}{{ url_for('home', page='rss', _external=True) }}{% endif %}" />
|
||||
|
||||
<meta property="og:site_name" content="{{ config.SITE_NAME }}">
|
||||
<meta property="og:title" content="{{ self.title() }}">
|
||||
<meta property="og:image" content="{% block meta_image %}/static/img/avatar/default.png{% endblock %}">
|
||||
{% block metatags %}
|
||||
{# Filled by children #}
|
||||
{% endblock %}
|
||||
|
||||
<!-- 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">
|
||||
{# These are extracted here for the dark mode toggle #}
|
||||
{% set bootstrap_light = static_cachebuster('/static/css/bootstrap.min.css') %}
|
||||
{% set bootstrap_dark = static_cachebuster('/static/css/bootstrap-dark.min.css') %}
|
||||
<link href="{{ bootstrap_light }}" rel="stylesheet" id="bsThemeLink">
|
||||
<link href="{{ static_cachebuster('/static/css/bootstrap-xl-mod.css') }}" rel="stylesheet">
|
||||
<!--
|
||||
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>
|
||||
<script>function toggleDarkMode(){"dark"===localStorage.getItem("theme")?setThemeLight():setThemeDark()}function setThemeDark(){bsThemeLink.href="{{ bootstrap_dark }}",localStorage.setItem("theme","dark")}function setThemeLight(){bsThemeLink.href="{{ bootstrap_light }}",localStorage.setItem("theme","light")}if("undefined"!=typeof Storage){var bsThemeLink=document.getElementById("bsThemeLink");"dark"===localStorage.getItem("theme")&&setThemeDark()}</script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.12.2/css/bootstrap-select.min.css" integrity="sha256-an4uqLnVJ2flr7w0U74xiF4PJjO2N5Df91R2CUmCLCA=" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" integrity="sha256-eZrrJcwDc/3uDhsdt61sL2oOBY362qM3lon1gyExkL0=" crossorigin="anonymous" />
|
||||
|
||||
<!-- Custom styles for this template -->
|
||||
<link href="/static/css/main.css" rel="stylesheet">
|
||||
<link href="{{ static_cachebuster('/static/css/main.css') }}" rel="stylesheet">
|
||||
|
||||
<!-- Core JavaScript -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js" integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha256-U5ZEeKfGNOja007MMD3YBI0A3OSZOQbeG6z2f2Y0hu8=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/commonmark/0.27.0/commonmark.min.js" integrity="sha256-10JreQhQG80GtKuzsioj0K46DlaB/CK/EG+NuG0q97E=" crossorigin="anonymous"></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="{{ static_cachebuster('/static/js/bootstrap-select.js') }}"></script>
|
||||
<script src="{{ static_cachebuster('/static/js/main.js') }}"></script>
|
||||
|
||||
<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
|
||||
<!--[if lt IE 9]>
|
||||
|
@ -56,6 +67,8 @@
|
|||
</button>
|
||||
<a class="navbar-brand" href="/">{{ config.SITE_NAME }}</a>
|
||||
</div>
|
||||
{% set search_username = (user.username + ("'" if user.username[-1] == 's' else "'s")) if user_page else None %}
|
||||
{% set search_placeholder = 'Search {} torrents...'.format(search_username) if user_page else 'Search...' %}
|
||||
<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>
|
||||
|
@ -75,7 +88,7 @@
|
|||
{% elif config.SITE_FLAVOR == 'sukebei' %}
|
||||
<li><a href="https://nyaa.si/">Fun</a></li>
|
||||
{% endif %}
|
||||
{% if g.user.is_admin %}
|
||||
{% if g.user.is_moderator %}
|
||||
<li><a href="{{ url_for('view_reports') }}">Reports</a> </li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
@ -121,11 +134,15 @@
|
|||
</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>
|
||||
<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>
|
||||
Guest
|
||||
<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>
|
||||
<a href="/login">
|
||||
|
@ -143,21 +160,6 @@
|
|||
</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 search-container">
|
||||
<input type="text" class="form-control search-bar" name="q" placeholder="Search..." value="{{ search["term"] if search is defined else '' }}">
|
||||
<div class="input-group-btn nav-filter" id="navFilter-criteria">
|
||||
<select class="selectpicker show-tick" title="Filter" data-width="120px" name="f">
|
||||
<option value="0" title="No filter" {% if search is defined and search["quality_filter"] == "0" %}selected{% else %}selected{% endif %}>No filter</option>
|
||||
<option value="1" title="No remakes" {% if search is defined and search["quality_filter"] == "1" %}selected{% endif %}>No remakes</option>
|
||||
<option value="2" title="Trusted only" {% if search is defined and search["quality_filter"] == "2" %}selected{% endif %}>Trusted only</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="input-group-btn nav-filter" id="navFilter-category">
|
||||
{% set nyaa_cats = [('1_0', 'Anime', 'Anime'),
|
||||
('1_1', '- Anime Music Video', 'Anime - AMV'),
|
||||
('1_2', '- English-translated', 'Anime - English'),
|
||||
|
@ -180,7 +182,9 @@
|
|||
('5_2', '- Photos', 'Pictures - Photos'),
|
||||
('6_0', 'Software', 'Software'),
|
||||
('6_1', '- Applications', 'Software - Apps'),
|
||||
('6_2', '- Games', 'Software - Games')] %}
|
||||
('6_2', '- Games', 'Software - Games')]
|
||||
%}
|
||||
|
||||
{% set suke_cats = [('1_0', 'Art', 'Art'),
|
||||
('1_1', '- Anime', 'Art - Anime'),
|
||||
('1_2', '- Doujinshi', 'Art - Doujinshi'),
|
||||
|
@ -189,13 +193,85 @@
|
|||
('1_5', '- Pictures', 'Art - Pictures'),
|
||||
('2_0', 'Real Life', 'Real Life'),
|
||||
('2_1', '- Photobooks and Pictures', 'Real Life - Pictures'),
|
||||
('2_2', '- Videos', 'Real Life - Videos')] %}
|
||||
('2_2', '- Videos', 'Real Life - Videos')]
|
||||
%}
|
||||
|
||||
{% if config.SITE_FLAVOR == 'nyaa' %}
|
||||
{% set used_cats = nyaa_cats %}
|
||||
{% elif config.SITE_FLAVOR == 'sukebei' %}
|
||||
{% set used_cats = suke_cats %}
|
||||
{% endif %}
|
||||
<select class="selectpicker show-tick" title="Category" data-width="170px" name="c">
|
||||
|
||||
<div class="search-container visible-xs visible-sm">
|
||||
{# The mobile menu #}
|
||||
{% 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 %}
|
||||
|
||||
<input type="text" class="form-control" name="q" placeholder="{{ search_placeholder }}" value="{{ search["term"] if search is defined else '' }}">
|
||||
<br>
|
||||
|
||||
<select class="form-control" title="Filter" data-width="120px" name="f">
|
||||
<option value="0" title="No filter" {% if search is defined and search["quality_filter"] == "0" %}selected{% else %}selected{% endif %}>No filter</option>
|
||||
<option value="1" title="No remakes" {% if search is defined and search["quality_filter"] == "1" %}selected{% endif %}>No remakes</option>
|
||||
<option value="2" title="Trusted only" {% if search is defined and search["quality_filter"] == "2" %}selected{% endif %}>Trusted only</option>
|
||||
</select>
|
||||
|
||||
<br>
|
||||
|
||||
<select class="form-control" title="Category" data-width="200px" name="c">
|
||||
<option value="0_0" title="All categories" {% if search is defined and search["category"] == "0_0" %}selected{% else %}selected{% endif %}>
|
||||
All categories
|
||||
</option>
|
||||
{% for cat_id, cat_name, cat_title in used_cats %}
|
||||
<option value="{{ cat_id }}" title="{{ cat_title }}" {% if search is defined and search.category == cat_id %}selected{% endif %}>
|
||||
{{ cat_name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<br>
|
||||
|
||||
<button class="btn btn-primary form-control" type="submit">
|
||||
<i class="fa fa-search fa-fw"></i> Search
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% 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 search-container hidden-xs hidden-sm">
|
||||
<input type="text" class="form-control search-bar" name="q" placeholder="{{ search_placeholder }}" value="{{ search["term"] if search is defined else '' }}">
|
||||
|
||||
<div class="input-group-btn nav-filter" id="navFilter-criteria">
|
||||
<select class="selectpicker show-tick" title="Filter" data-width="120px" name="f">
|
||||
<option value="0" title="No filter" {% if search is defined and search["quality_filter"] == "0" %}selected{% else %}selected{% endif %}>No filter</option>
|
||||
<option value="1" title="No remakes" {% if search is defined and search["quality_filter"] == "1" %}selected{% endif %}>No remakes</option>
|
||||
<option value="2" title="Trusted only" {% if search is defined and search["quality_filter"] == "2" %}selected{% endif %}>Trusted only</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="input-group-btn nav-filter" id="navFilter-category">
|
||||
<!--
|
||||
On narrow viewports, there isn't enough room to fit the full stuff in the selectpicker, so we show a full-width one on wide viewports, but squish it on narrow ones.
|
||||
-->
|
||||
{# XXX Search breaks with multiple fields with the same name: default to the shorter one so we don't break visuals. This is a hack! #}
|
||||
{# <select class="selectpicker show-tick visible-lg" title="Category" data-width="200px" name="c">
|
||||
<option value="0_0" title="All categories" {% if search is defined and search["category"] == "0_0" %}selected{% else %}selected{% endif %}>
|
||||
All categories
|
||||
</option>
|
||||
{% for cat_id, cat_name, cat_title in used_cats %}
|
||||
<option value="{{ cat_id }}" title="{{ cat_title }}" {% if search is defined and search.category == cat_id %}selected{% endif %}>
|
||||
{{ cat_name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select> #}
|
||||
<select class="selectpicker show-tick" title="Category" data-width="130px" name="c">
|
||||
<option value="0_0" title="All categories" {% if search is defined and search["category"] == "0_0" %}selected{% else %}selected{% endif %}>
|
||||
All categories
|
||||
</option>
|
||||
|
@ -213,8 +289,8 @@
|
|||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div><!--/.nav-collapse -->
|
||||
</div>
|
||||
</div><!--/.nav-collapse -->
|
||||
</nav>
|
||||
|
||||
<div class="container">
|
||||
|
@ -231,5 +307,3 @@
|
|||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
{% extends "layout.html" %}
|
||||
{% block title %}Login :: {{ config.SITE_NAME }}{% endblock %}
|
||||
{% block metatags %}
|
||||
<meta property="og:description" content="Log in to {{ config.SITE_NAME }}!">
|
||||
{% endblock %}
|
||||
{% block body %}
|
||||
{% from "_formhelpers.html" import render_field %}
|
||||
|
||||
|
|
|
@ -3,17 +3,17 @@
|
|||
{% block body %}
|
||||
{% from "_formhelpers.html" import render_field %}
|
||||
|
||||
<h2 style="margin-bottom: 20px;">Profile of <strong>{{ name }}</strong></h2>
|
||||
<h2 style="margin-bottom: 20px;">Profile of <strong class="text-{{ g.user.userlevel_color }}">{{ g.user.username }}</strong></h2>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-4 avatar" style="display: none;">
|
||||
<!-- TO BE IMPLEMENTED -->
|
||||
<div class="row" style="margin-bottom: 20px;">
|
||||
<div class="col-sm-2" style="max-width: 150px;">
|
||||
<img class="avatar" src="{{ g.user.gravatar_url() }}">
|
||||
</div>
|
||||
<div class="col-sm-8">
|
||||
<div class="col-sm-10">
|
||||
<dl class="row" style="margin: 20px 0 15px 0;">
|
||||
<dt class="col-sm-3">User ID:</dt><dd class="col-sm-9">{{ g.user.id }}</dd>
|
||||
<dt class="col-sm-3">User Class:</dt><dd class="col-sm-9">{{ level }}</dd>
|
||||
<dt class="col-sm-3">User Created on:</dt><dd class="col-sm-9">{{ g.user.created_time }}</dd>
|
||||
<dt class="col-sm-2">User ID:</dt><dd class="col-sm-10">{{ g.user.id }}</dd>
|
||||
<dt class="col-sm-2">User Class:</dt><dd class="col-sm-10">{{ g.user.userlevel_str }}</dd>
|
||||
<dt class="col-sm-2">User Created on:</dt><dd class="col-sm-10">{{ g.user.created_time }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -59,7 +59,7 @@
|
|||
<div class="row">
|
||||
<div class="form-group col-md-4">
|
||||
<label class="control-label" for="current_email">Current Email</label>
|
||||
<div>{{email}}</div>
|
||||
<div>{{ g.user.email }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
{% extends "layout.html" %}
|
||||
{% block title %}Register :: {{ config.SITE_NAME }}{% endblock %}
|
||||
{% block metatags %}
|
||||
<meta property="og:description" content="Register to {{ config.SITE_NAME }}!">
|
||||
{% endblock %}
|
||||
{% block body %}
|
||||
{% from "_formhelpers.html" import render_field %}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
|
||||
<rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:nyaa="{{ url_for('xmlns_nyaa', _external=True) }}" version="2.0">
|
||||
<channel>
|
||||
<title>{{ config.SITE_NAME }} Torrent File RSS</title>
|
||||
<description>RSS Feed for {{ term }}</description>
|
||||
|
@ -12,15 +12,15 @@
|
|||
{% if torrent.has_torrent and not magnet_links %}
|
||||
<link>{{ url_for('download_torrent', torrent_id=torrent.meta.id, _external=True) }}</link>
|
||||
{% else %}
|
||||
<link>{{ create_magnet_from_info(torrent.display_name, torrent.info_hash) }}</link>
|
||||
<link>{{ create_magnet_from_es_info(torrent.display_name, torrent.info_hash) }}</link>
|
||||
{% endif %}
|
||||
<guid isPermaLink="true">{{ url_for('view_torrent', torrent_id=torrent.meta.id, _external=True) }}</guid>
|
||||
<pubDate>{{ torrent.created_time|rfc822_es }}</pubDate>
|
||||
|
||||
<seeders> {{- torrent.seed_count }}</seeders>
|
||||
<leechers> {{- torrent.leech_count }}</leechers>
|
||||
<downloads>{{- torrent.download_count }}</downloads>
|
||||
<infoHash> {{- torrent.info_hash }}</infoHash>
|
||||
<nyaa:seeders> {{- torrent.seed_count }}</nyaa:seeders>
|
||||
<nyaa:leechers> {{- torrent.leech_count }}</nyaa:leechers>
|
||||
<nyaa:downloads>{{- torrent.download_count }}</nyaa:downloads>
|
||||
<nyaa:infoHash> {{- torrent.info_hash }}</nyaa:infoHash>
|
||||
{% else %}
|
||||
{% if torrent.has_torrent and not magnet_links %}
|
||||
<link>{{ url_for('download_torrent', torrent_id=torrent.id, _external=True) }}</link>
|
||||
|
@ -30,15 +30,15 @@
|
|||
<guid isPermaLink="true">{{ url_for('view_torrent', torrent_id=torrent.id, _external=True) }}</guid>
|
||||
<pubDate>{{ torrent.created_time|rfc822 }}</pubDate>
|
||||
|
||||
<seeders> {{- torrent.stats.seed_count }}</seeders>
|
||||
<leechers> {{- torrent.stats.leech_count }}</leechers>
|
||||
<downloads>{{- torrent.stats.download_count }}</downloads>
|
||||
<infoHash> {{- torrent.info_hash_as_hex }}</infoHash>
|
||||
<nyaa:seeders> {{- torrent.stats.seed_count }}</nyaa:seeders>
|
||||
<nyaa:leechers> {{- torrent.stats.leech_count }}</nyaa:leechers>
|
||||
<nyaa:downloads>{{- torrent.stats.download_count }}</nyaa:downloads>
|
||||
<nyaa:infoHash> {{- torrent.info_hash_as_hex }}</nyaa:infoHash>
|
||||
{% endif %}
|
||||
{% set cat_id = use_elastic and ((torrent.main_category_id|string) + '_' + (torrent.sub_category_id|string)) or torrent.sub_category.id_as_string %}
|
||||
<categoryId>{{- cat_id }}</categoryId>
|
||||
<category> {{- category_name(cat_id) }}</category>
|
||||
<size> {{- torrent.filesize | filesizeformat(True) }}</size>
|
||||
<nyaa:categoryId>{{- cat_id }}</nyaa:categoryId>
|
||||
<nyaa:category> {{- category_name(cat_id) }}</nyaa:category>
|
||||
<nyaa:size> {{- torrent.filesize | filesizeformat(True) }}</nyaa:size>
|
||||
</item>
|
||||
{% endfor %}
|
||||
</channel>
|
||||
|
|
|
@ -8,6 +8,15 @@
|
|||
{{ caller() }}
|
||||
</th>
|
||||
{% endmacro %}
|
||||
|
||||
{% if special_results is defined and not search.user %}
|
||||
{% if special_results.first_word_user %}
|
||||
<div class="alert alert-info">
|
||||
<a href="/user/{{ special_results.first_word_user.username }}{{ modify_query(q=special_results.query_sans_user)[1:] }}">Click here to see only results uploaded by {{ special_results.first_word_user.username }}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if (use_elastic and torrent_query.hits.total > 0) or (torrent_query.items) %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-hover table-striped torrent-list">
|
||||
|
@ -56,7 +65,7 @@
|
|||
{% else %}
|
||||
<a href="/?c={{ cat_id }}" title="{{ torrent.main_category.name }} - {{ torrent.sub_category.name }}">
|
||||
{% endif %}
|
||||
<img src="/static/img/icons/{{ icon_dir }}/{{ cat_id }}.png">
|
||||
<img src="/static/img/icons/{{ icon_dir }}/{{ cat_id }}.png" alt="{{ category_name(cat_id) }}">
|
||||
</a>
|
||||
</td>
|
||||
{% if use_elastic %}
|
||||
|
@ -67,7 +76,7 @@
|
|||
<td style="white-space: nowrap;text-align: center;">
|
||||
{% if torrent.has_torrent %}<a href="{{ url_for('download_torrent', torrent_id=torrent.id) }}"><i class="fa fa-fw fa-download"></i></a>{% endif %}
|
||||
{% if use_elastic %}
|
||||
<a href="{{ create_magnet_from_info(torrent.display_name, torrent.info_hash) }}"><i class="fa fa-fw fa-magnet"></i></a>
|
||||
<a href="{{ create_magnet_from_es_info(torrent.display_name, torrent.info_hash) }}"><i class="fa fa-fw fa-magnet"></i></a>
|
||||
{% else %}
|
||||
<a href="{{ torrent.magnet_uri }}"><i class="fa fa-fw fa-magnet"></i></a>
|
||||
{% endif %}
|
||||
|
@ -99,7 +108,7 @@
|
|||
<h3>No results found</h3>
|
||||
{% endif %}
|
||||
|
||||
<center>
|
||||
<div class="center">
|
||||
{% if use_elastic %}
|
||||
{{ pagination.info }}
|
||||
{{ pagination.links }}
|
||||
|
@ -107,4 +116,4 @@
|
|||
{% from "bootstrap/pagination.html" import render_pagination %}
|
||||
{{ render_pagination(torrent_query) }}
|
||||
{% endif %}
|
||||
</center>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
{% extends "layout.html" %}
|
||||
{% block title %}Upload Torrent :: {{ config.SITE_NAME }}{% endblock %}
|
||||
{% block metatags %}
|
||||
<meta property="og:description" content="Upload a torrent to {{ config.SITE_NAME }}">
|
||||
{% endblock %}
|
||||
{% block body %}
|
||||
{% from "_formhelpers.html" import render_field %}
|
||||
{% from "_formhelpers.html" import render_upload %}
|
||||
|
@ -7,69 +10,97 @@
|
|||
|
||||
<h1>Upload Torrent</h1>
|
||||
|
||||
{% if not user %}
|
||||
{% if not g.user %}
|
||||
<p>You are not logged in, and are uploading anonymously.</p>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<div id="upload-drop-zone"><span>Drop here!</span></div>
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
{{ upload_form.csrf_token }}
|
||||
|
||||
{% if config.ENFORCE_MAIN_ANNOUNCE_URL %}<p><strong>Important:</strong> Please include <kbd>{{ config.MAIN_ANNOUNCE_URL }}</kbd> in your trackers</p>{% endif %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
{{ render_upload(form.torrent_file, accept=".torrent") }}
|
||||
<div class="col-md-10">
|
||||
{{ render_upload(upload_form.torrent_file, accept=".torrent") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
{{ render_field(form.display_name, class_='form-control', placeholder='Display name') }}
|
||||
{{ render_field(upload_form.display_name, class_='form-control', placeholder='Display name') }}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
{{ render_field(form.category, class_='form-control')}}
|
||||
{{ render_field(upload_form.category, class_='form-control')}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
|
||||
</div>
|
||||
<div class="row"></div>
|
||||
<div class="row form-group">
|
||||
<div class="col-md-6">
|
||||
{{ render_field(form.information, class_='form-control', placeholder='Your website or IRC channel') }}
|
||||
{{ render_field(upload_form.information, class_='form-control', placeholder='Your website or IRC channel') }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="control-label">Torrent flags</label>
|
||||
<div>
|
||||
<label class="btn btn-primary" title="Upload torrent anonymously (don't display your username)">
|
||||
{{ form.is_anonymous(disabled=(False if user else ""), checked=(False if user else "")) }}
|
||||
<label class="control-label">Torrent flags</label><br>
|
||||
|
||||
<div class="btn-group" data-toggle="buttons">
|
||||
<label class="btn btn-default {% if not g.user %}active disabled{% endif %}" title="Upload torrent anonymously (don't display your username)">
|
||||
{{ upload_form.is_anonymous(disabled=(False if g.user else ""), checked=(False if g.user else "")) }}
|
||||
{% if not g.user %}<span class="glyphicon glyphicon-ban-circle"></span>{% endif %}
|
||||
{% if g.user %}<span class="glyphicon glyphicon-check"></span>{% endif %}
|
||||
{% if g.user %}<span class="glyphicon glyphicon-unchecked"></span>{% endif %}
|
||||
Anonymous
|
||||
</label>
|
||||
<label class="btn btn-default" style="background-color: darkgray; border-color: #ccc;" title="Hide torrent from listing">
|
||||
{{ form.is_hidden }}
|
||||
<label class="btn btn-grey" title="Hide torrent from listing">
|
||||
{{ upload_form.is_hidden }}
|
||||
<span class="glyphicon glyphicon-check"></span>
|
||||
<span class="glyphicon glyphicon-unchecked"></span>
|
||||
Hidden
|
||||
</label>
|
||||
</div>
|
||||
<div class="hidden-xl hidden-lg"><br></div>
|
||||
<div class="btn-group" data-toggle="buttons">
|
||||
|
||||
<label class="btn btn-danger" title="This torrent is derived from another release">
|
||||
{{ form.is_remake }}
|
||||
{{ upload_form.is_remake }}
|
||||
<span class="glyphicon glyphicon-check"></span>
|
||||
<span class="glyphicon glyphicon-unchecked"></span>
|
||||
Remake
|
||||
</label>
|
||||
<label class="btn btn-primary" title="This torrent is a complete batch (eg. season)">
|
||||
{{ form.is_complete }}
|
||||
<label class="btn btn-warning" title="This torrent is a complete batch (eg. season)">
|
||||
{{ upload_form.is_complete }}
|
||||
<span class="glyphicon glyphicon-check"></span>
|
||||
<span class="glyphicon glyphicon-unchecked"></span>
|
||||
Complete
|
||||
</label>
|
||||
{% if user.is_trusted %}
|
||||
<label class="btn btn-success" title="Mark torrent trusted">
|
||||
{{ form.is_trusted(checked="") }}
|
||||
{% if g.user.is_trusted %}
|
||||
<label class="btn btn-success active" title="Mark torrent trusted">
|
||||
{{ upload_form.is_trusted(checked="") }}
|
||||
<span class="glyphicon glyphicon-check"></span>
|
||||
<span class="glyphicon glyphicon-unchecked"></span>
|
||||
Trusted
|
||||
</label>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{{ render_markdown_editor(form.description, field_name='description') }}
|
||||
{{ render_markdown_editor(upload_form.description, field_name='description') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if config.USE_RECAPTCHA and not g.user %}
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
{% for error in upload_form.recaptcha.errors %}
|
||||
{{ error }}
|
||||
{% endfor %}
|
||||
{{ upload_form.recaptcha }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<br>
|
||||
|
||||
<div class="row">
|
||||
<div class="form-group col-md-6">
|
||||
<input type="submit" value="Upload" class="btn btn-primary">
|
||||
|
|
|
@ -1,10 +1,24 @@
|
|||
{% extends "layout.html" %}
|
||||
{% block title %}{{ user.username }} :: {{ config.SITE_NAME }}{% endblock %}
|
||||
{% block meta_image %}{{ user.gravatar_url() }}{% endblock %}
|
||||
{% block metatags %}
|
||||
{% if search.term %}
|
||||
<meta property="og:description" content="Search for '{{ search.term }}' in torrents uploaded by {{ user.username }}">
|
||||
{% else %}
|
||||
<meta property="og:description" content="Torrents uploaded by {{ user.username }}">
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% from "_formhelpers.html" import render_menu_with_button %}
|
||||
|
||||
{% if superadmin %}
|
||||
{% if g.user and g.user.is_moderator %}
|
||||
<h2>User Information</h2><br>
|
||||
<div class="row" style="margin-bottom: 20px;">
|
||||
<div class="col-sm-2" style="max-width: 150px;">
|
||||
<img class="avatar" src="{{ user.gravatar_url() }}">
|
||||
</div>
|
||||
<div class="col-sm-10">
|
||||
<dl class="dl-horizontal">
|
||||
<dt>User ID:</dt>
|
||||
<dd>{{ user.id }}</dd>
|
||||
|
@ -13,22 +27,30 @@
|
|||
<dt>Email address:</dt>
|
||||
<dd>{{ user.email }}</dd>
|
||||
<dt>User class:</dt>
|
||||
<dd>{{level}}</dd><br>
|
||||
<dd>{{ level }}</dd>
|
||||
{%- if g.user.is_superadmin -%}
|
||||
<dt>Last login IP:</dt>
|
||||
<dd>{{ user.ip_string }}</dd><br>
|
||||
{%- endif -%}
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
{% if admin_form %}
|
||||
<form method="POST">
|
||||
{{ form.csrf_token }}
|
||||
{{ admin_form.csrf_token }}
|
||||
|
||||
<div class="form-group row">
|
||||
<div class="col-md-6">
|
||||
{{ render_menu_with_button(form.user_class)}}
|
||||
{{ render_menu_with_button(admin_form.user_class) }}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<br>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<h3>
|
||||
Browsing {{user.username}}'s torrents
|
||||
Browsing <span class="text-{{ user.userlevel_color }}">{{ user.username }}</span>'{{ '' if user.username[-1] == 's' else 's' }} torrents
|
||||
</h3>
|
||||
|
||||
{% include "search_results.html" %}
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
{% extends "layout.html" %}
|
||||
{% block title %}{{ torrent.display_name }} :: {{ config.SITE_NAME }}{% endblock %}
|
||||
{% block metatags %}
|
||||
{% set uploader_name = torrent.user.username if (torrent.user and not torrent.anonymous) else 'Anonymous' %}
|
||||
<meta property="og:description" content="{{ category_name(torrent.sub_category.id_as_string) }} | {{ torrent.filesize | filesizeformat(True) }} | Uploaded by {{ uploader_name }} on {{ torrent.created_time.strftime('%Y-%m-%d') }}">
|
||||
{% endblock %}
|
||||
{% block body %}
|
||||
{% from "_formhelpers.html" import render_field %}
|
||||
<div class="panel panel-{% if torrent.deleted %}deleted{% elif torrent.remake %}danger{% elif torrent.trusted %}success{% else %}default{% endif %}">
|
||||
|
@ -27,10 +31,13 @@
|
|||
<div class="col-md-5">
|
||||
{% set user_url = torrent.user and url_for('view_user', user_name=torrent.user.username) %}
|
||||
{%- if not torrent.anonymous and torrent.user -%}
|
||||
<a href="{{ user_url }}">{{ torrent.user.username }}</a>
|
||||
<a class="text-{{ torrent.user.userlevel_color }}" href="{{ user_url }}">{{ torrent.user.username }}</a>
|
||||
{%- else -%}
|
||||
Anonymous {% if torrent.user and (viewer == torrent.user or viewer.is_admin) %}(<a href="{{ user_url }}">{{ torrent.user.username }}</a>){% endif %}
|
||||
Anonymous {% if torrent.user and (g.user == torrent.user or g.user.is_moderator) %}(<a href="{{ user_url }}">{{ torrent.user.username }}</a>){% endif %}
|
||||
{%- endif -%}
|
||||
{% if g.user and g.user.is_superadmin and torrent.uploader_ip %}
|
||||
({{ torrent.uploader_ip_string }})
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-md-1">Seeders:</div>
|
||||
|
@ -56,16 +63,20 @@
|
|||
<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-1">Completed:</div>
|
||||
<div class="col-md-5">{% if config.ENABLE_SHOW_STATS %}{{ torrent.stats.download_count }}{% else %}Coming soon{% endif %}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-offset-6 col-md-1">Info hash:</div>
|
||||
<div class="col-md-5"><kbd>{{ torrent.info_hash_as_hex }}</kbd></div>
|
||||
</div>
|
||||
<div class="panel-footer clearfix" style="font-size: large">
|
||||
{% 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 class="panel-footer clearfix">
|
||||
{% if torrent.has_torrent %}<a href="{{ url_for('download_torrent', torrent_id=torrent.id )}}"><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>
|
||||
<button type="button" class="btn btn-danger pull-right" data-toggle="modal" data-target="#reportModal">
|
||||
Report
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -82,42 +93,32 @@
|
|||
|
||||
{% 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 class="panel-heading">
|
||||
<h3 class="panel-title">File list</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() recursive %}
|
||||
<tr>
|
||||
{%- if value is iterable %}
|
||||
<td colspan="2" {% if loop.depth0 is greaterthan 0 %}style="padding-left: {{ loop.depth0 * 20 }}px"{% endif %}>
|
||||
<i class="glyphicon glyphicon-folder-open"></i> <b>{{ key }}</b></td>
|
||||
{{ loop(value.items()) }}
|
||||
{%- else %}
|
||||
<td{% if loop.depth0 is greaterthan 0 %} style="padding-left: {{ loop.depth0 * 20 }}px"{% endif %}>
|
||||
<i class="glyphicon glyphicon-file"></i> {{ key }}</td>
|
||||
<td class="col-md-2">{{ value | filesizeformat(True) }}</td>
|
||||
{%- endif %}
|
||||
</tr>
|
||||
{%- endfor %}
|
||||
</table>
|
||||
<div class="torrent-file-list panel-body">
|
||||
<ul>
|
||||
{% for key, value in files.items() recursive -%}
|
||||
{% if value is iterable %}
|
||||
{% set pre_expanded = not loop.depth0 and value.items()|length <= 20 %}
|
||||
<li>
|
||||
<a href="" class="folder"><i class="fa fa-folder{% if pre_expanded %}-open{% endif %}"></i>{{ key }}</a>
|
||||
<ul{% if pre_expanded %} data-show="yes"{% endif %}>{{ '\n' + loop(value.items()) }}
|
||||
</ul>
|
||||
</li>
|
||||
{% else %}
|
||||
<li><i class="fa fa-file"></i>{{ key }} <span class="file-size">({{ value | filesizeformat(True) }})</span></a></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</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>
|
||||
Too many files to display.
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -125,12 +126,65 @@
|
|||
<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>
|
||||
File list is not available for this torrent.
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
Comments - {{ comments | length }}
|
||||
</h3>
|
||||
</div>
|
||||
{% for comment in comments %}
|
||||
<div class="panel panel-default comment-panel" id="com-{{ loop.index }}">
|
||||
<div class="panel-body">
|
||||
<div class="col-md-2">
|
||||
<p>
|
||||
<a class="text-{{ comment.user.userlevel_color }}" href="{{ url_for('view_user', user_name=comment.user.username) }}">{{ comment.user.username }}</a>
|
||||
{% if comment.user.id == torrent.uploader_id and not torrent.anonymous %}
|
||||
(uploader)
|
||||
{% endif %}
|
||||
</p>
|
||||
<p><img class="avatar" src="{{ comment.user.gravatar_url() }}" alt="{{ comment.user.userlevel_str }}"></p>
|
||||
</div>
|
||||
<div class="col-md-10">
|
||||
<div class="row">
|
||||
<a href="#com-{{ loop.index }}"><small data-timestamp-swap data-timestamp="{{ comment.created_utc_timestamp|int }}">{{ comment.created_time.strftime('%Y-%m-%d %H:%M UTC') }}</small></a>
|
||||
{% if g.user.is_moderator or g.user.id == comment.user_id %}
|
||||
<form class="delete-comment-form" action="{{ url_for('delete_comment', torrent_id=torrent.id, comment_id=comment.id) }}" method="POST">
|
||||
<button name="submit" type="submit" class="btn btn-danger btn-xs" title="Delete">Delete</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="row">
|
||||
{# Escape newlines into html entities because CF strips blank newlines #}
|
||||
<div class="comment-content" id="torrent-comment{{ comment.id }}">{{ comment.text }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
var target = document.getElementById('torrent-comment{{ comment.id }}');
|
||||
var text = target.innerHTML;
|
||||
var reader = new commonmark.Parser({safe: true});
|
||||
var writer = new commonmark.HtmlRenderer({safe: true, softbreak: '<br />'});
|
||||
var parsed = reader.parse(text.trim());
|
||||
target.innerHTML = writer.render(parsed);
|
||||
</script>
|
||||
{% endfor %}
|
||||
{% if comment_form %}
|
||||
<form class="comment-box" method="POST">
|
||||
{{ comment_form.csrf_token }}
|
||||
{{ render_field(comment_form.comment, class_='form-control') }}
|
||||
<input type="submit" value="Submit" class="btn btn-success btn-sm">
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="reportModal" tabindex="-1" role="dialog" aria-labelledby="reportModalLabel">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
|
@ -153,7 +207,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var target = document.getElementById('torrent-description');
|
||||
|
|
32
nyaa/templates/xmlns.html
Normal file
32
nyaa/templates/xmlns.html
Normal file
|
@ -0,0 +1,32 @@
|
|||
{% extends "layout.html" %}
|
||||
{% block title %}XML Namespace :: {{ config.SITE_NAME }}{% endblock %}
|
||||
{% block body %}
|
||||
<div class="content">
|
||||
<h1>Nyaa XML Namespace</h1>
|
||||
<p>You found this page because our RSS feeds contain an URL that links here. Said URL is not an actual page but rather a unique identifier used to prevent name collisions with other XML namespaces.</p>
|
||||
<p>The namespace contains the following additional, informational <b>tags</b>:</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p><code><nyaa:seeders></code> holds the current amount of seeders on the respective torrent.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><code><nyaa:leechers></code> holds the current amount of leechers on the respective torrent.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><code><nyaa:downloads></code> counts the downloads the torrent got up to the point the feed was refreshed.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><code><nyaa:infoHash></code> is the torrent's infohash, a unique identifier, in hexadecimal.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><code><nyaa:categoryId></code> contains the ID of the category containing the upload in the form <code>category_subcategory</code>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><code><nyaa:category></code> contains the written name of the torrent's category in the form <code>Category - Subcategory</code>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><code><nyaa:size></code> indicates the torrent's download size to one decimal place, using a magnitude prefix according to ISO/IEC 80000-13.</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -11,6 +11,9 @@ from nyaa import models
|
|||
|
||||
USED_TRACKERS = OrderedSet()
|
||||
|
||||
# Limit the amount of trackers added into .torrent files
|
||||
MAX_TRACKERS = 5
|
||||
|
||||
|
||||
def read_trackers_from_file(file_object):
|
||||
USED_TRACKERS.clear()
|
||||
|
@ -55,7 +58,7 @@ def get_trackers(torrent):
|
|||
return list(trackers)
|
||||
|
||||
|
||||
def get_trackers_magnet():
|
||||
def get_default_trackers():
|
||||
trackers = OrderedSet()
|
||||
|
||||
# Our main one first
|
||||
|
@ -70,8 +73,9 @@ def get_trackers_magnet():
|
|||
|
||||
|
||||
def create_magnet(torrent, max_trackers=5, trackers=None):
|
||||
# Unless specified, we just use default trackers
|
||||
if trackers is None:
|
||||
trackers = get_trackers_magnet()
|
||||
trackers = get_default_trackers()
|
||||
|
||||
magnet_parts = [
|
||||
('dn', torrent.display_name)
|
||||
|
@ -85,10 +89,10 @@ def create_magnet(torrent, max_trackers=5, trackers=None):
|
|||
|
||||
# For processing ES links
|
||||
@app.context_processor
|
||||
def create_magnet_from_info():
|
||||
def _create_magnet_from_info(display_name, info_hash, max_trackers=5, trackers=None):
|
||||
def create_magnet_from_es_info():
|
||||
def _create_magnet_from_es_info(display_name, info_hash, max_trackers=5, trackers=None):
|
||||
if trackers is None:
|
||||
trackers = get_trackers_magnet()
|
||||
trackers = get_default_trackers()
|
||||
|
||||
magnet_parts = [
|
||||
('dn', display_name)
|
||||
|
@ -98,7 +102,7 @@ def create_magnet_from_info():
|
|||
|
||||
b32_info_hash = base64.b32encode(bytes.fromhex(info_hash)).decode('utf-8')
|
||||
return 'magnet:?xt=urn:btih:' + b32_info_hash + '&' + urlencode(magnet_parts)
|
||||
return dict(create_magnet_from_info=_create_magnet_from_info)
|
||||
return dict(create_magnet_from_es_info=_create_magnet_from_es_info)
|
||||
|
||||
|
||||
def create_default_metadata_base(torrent, trackers=None):
|
||||
|
@ -116,7 +120,7 @@ def create_default_metadata_base(torrent, trackers=None):
|
|||
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]
|
||||
metadata_base['announce-list'] = [[tracker] for tracker in trackers[:MAX_TRACKERS]]
|
||||
|
||||
return metadata_base
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
alembic==0.9.2
|
||||
appdirs==1.4.3
|
||||
argon2-cffi==16.3.0
|
||||
autopep8==1.3.1
|
||||
|
@ -5,9 +6,14 @@ blinker==1.4
|
|||
cffi==1.10.0
|
||||
click==6.7
|
||||
dominate==2.3.1
|
||||
Flask==0.12.1
|
||||
elasticsearch==5.3.0
|
||||
elasticsearch-dsl==5.2.0
|
||||
Flask==0.12.2
|
||||
Flask-Assets==0.12
|
||||
Flask-DebugToolbar==0.10.1
|
||||
Flask-Migrate==2.0.3
|
||||
flask-paginate==0.4.5
|
||||
Flask-Script==2.0.5
|
||||
Flask-SQLAlchemy==2.2
|
||||
Flask-WTF==0.14.2
|
||||
gevent==1.2.1
|
||||
|
@ -15,27 +21,29 @@ greenlet==0.4.12
|
|||
itsdangerous==0.24
|
||||
Jinja2==2.9.6
|
||||
libsass==0.12.3
|
||||
Mako==1.0.6
|
||||
MarkupSafe==1.0
|
||||
mysql-replication==0.13
|
||||
mysqlclient==1.3.10
|
||||
orderedset==2.0
|
||||
packaging==16.8
|
||||
passlib==1.7.1
|
||||
progressbar2==3.20.0
|
||||
pycodestyle==2.3.1
|
||||
pycparser==2.17
|
||||
PyMySQL==0.7.11
|
||||
pyparsing==2.2.0
|
||||
python-dateutil==2.6.0
|
||||
python-editor==1.0.3
|
||||
python-utils==2.1.0
|
||||
six==1.10.0
|
||||
SQLAlchemy==1.1.9
|
||||
SQLAlchemy==1.1.10
|
||||
SQLAlchemy-FullText-Search==0.2.3
|
||||
SQLAlchemy-Utils==0.32.14
|
||||
statsd==3.2.1
|
||||
urllib3==1.21.1
|
||||
uWSGI==2.0.15
|
||||
visitor==0.1.3
|
||||
webassets==0.12.1
|
||||
Werkzeug==0.12.1
|
||||
Werkzeug==0.12.2
|
||||
WTForms==2.1
|
||||
## elasticsearch dependencies
|
||||
elasticsearch==5.3.0
|
||||
elasticsearch-dsl==5.2.0
|
||||
progressbar2==3.20.0
|
||||
mysql-replication==0.13
|
||||
flask-paginate==0.4.5
|
||||
statsd==3.2.1
|
||||
|
|
13
trackers.txt
13
trackers.txt
|
@ -1,13 +1,4 @@
|
|||
udp://oscar.reyesleon.xyz:6969/announce
|
||||
udp://tracker.cyberia.is:6969/announce
|
||||
udp://tracker.doko.moe:6969
|
||||
http://tracker.baka-sub.cf:80/announce
|
||||
udp://tracker.coppersurfer.tk:6969/announce
|
||||
udp://tracker.torrent.eu.org:451
|
||||
udp://tracker.opentrackr.org:1337/announce
|
||||
udp://tracker.coppersurfer.tk:6969/announce
|
||||
udp://tracker.doko.moe:6969
|
||||
udp://tracker.zer0day.to:1337/announce
|
||||
http://t.nyaatracker.com:80/announce
|
||||
https://open.kickasstracker.com:443/announce
|
||||
udp://tracker.safe.moe:6969/announce
|
||||
udp://p4p.arenabg.ch:1337/announce
|
||||
udp://tracker.justseed.it:1337/announce
|
||||
|
|
|
@ -1,134 +0,0 @@
|
|||
# Uploads a single torrent file
|
||||
# Works on nyaa.si and sukebei.nyaa.si
|
||||
|
||||
# Consider using api_uploader_v2.py instead
|
||||
# It has a nice command line interface
|
||||
|
||||
import json
|
||||
import requests
|
||||
|
||||
'''
|
||||
The POST payload to the api endpoint (/api/upload) should be multipart/form-data containing three fields
|
||||
|
||||
'auth_info': file containing "{
|
||||
'username': str,
|
||||
'password': str
|
||||
}",
|
||||
|
||||
'torrent_info': {
|
||||
'category': str, # see below
|
||||
'display_name': str, # optional
|
||||
'information': str,
|
||||
'description': str,
|
||||
'is_anonymous': boolean,
|
||||
'is_hidden': boolean,
|
||||
'is_remake': boolean,
|
||||
'is_complete': boolean
|
||||
},
|
||||
|
||||
'torrent_file': multi part file format
|
||||
|
||||
|
||||
A successful request should return {'Success': int(torrent_id)}
|
||||
A failed request should return {'Failure': ["Failure 1", "Failure 2"...]]}
|
||||
|
||||
'''
|
||||
|
||||
# ########################################### HELP ############################################
|
||||
# ################################# CATEGORIES MUST BE EXACT ##################################
|
||||
'''
|
||||
# Nyaa categories only for now, but api still works for sukebei
|
||||
|
||||
Anime
|
||||
Anime - AMV : '1_1'
|
||||
Anime - English : '1_2'
|
||||
Anime - Non-English : '1_3'
|
||||
Anime - Raw : '1_4'
|
||||
Audio
|
||||
Lossless : '2_1'
|
||||
Lossy : '2_2'
|
||||
Literature
|
||||
Literature - English-translated : '3_1'
|
||||
Literature - Non-English : '3_2'
|
||||
Literature - Non-English-Translated : '3_3'
|
||||
Literature - Raw : '3_4'
|
||||
Live Action
|
||||
Live Action - English-translated : '4_1'
|
||||
Live Action - Idol/Promotional Video : '4_2'
|
||||
Live Action - Non-English-translated : '4_3'
|
||||
Live Action - Raw : '4_4'
|
||||
Pictures
|
||||
Pictures - Graphics : '5_1'
|
||||
Pictures - Photos : '5_2'
|
||||
Software
|
||||
Software - Applications : '6_1'
|
||||
Software - Games : '6_2'
|
||||
'''
|
||||
# ################################# CATEGORIES MUST BE EXACT ##################################
|
||||
|
||||
# ###################################### EXAMPLE REQUEST ######################################
|
||||
'''
|
||||
# Required
|
||||
username = ''
|
||||
password = ''
|
||||
torrent_file = '/path/to/my.torrent'
|
||||
category = '1_2'
|
||||
|
||||
#Optional
|
||||
display_name = ''
|
||||
information = 'API HOWTO'
|
||||
description = 'Visit #nyaa-dev@irc.rizon.net'
|
||||
|
||||
# Defaults to False, change to True to set
|
||||
is_anonymous : False,
|
||||
is_hidden : False,
|
||||
is_remake : False,
|
||||
is_complete : False
|
||||
'''
|
||||
|
||||
|
||||
# ######################################## CHANGE HERE ########################################
|
||||
|
||||
url = 'https://nyaa.si/api/upload' # or 'https://sukebei.nyaa.si/api/upload' or 'http://127.0.0.1:5500/api/upload'
|
||||
|
||||
# Required
|
||||
username = ''
|
||||
password = ''
|
||||
torrent_file = ''
|
||||
category = ''
|
||||
|
||||
# Optional
|
||||
display_name = ''
|
||||
information = ''
|
||||
description = ''
|
||||
is_anonymous = False
|
||||
is_hidden = False
|
||||
is_remake = False
|
||||
is_complete = False
|
||||
|
||||
auth_info = {
|
||||
'username' : username,
|
||||
'password' : password
|
||||
}
|
||||
|
||||
metadata={
|
||||
'category' : category,
|
||||
'display_name' : display_name,
|
||||
'information' : information,
|
||||
'description' : description,
|
||||
'is_anonymous' : is_anonymous,
|
||||
'is_hidden' : is_hidden,
|
||||
'is_remake' : is_remake,
|
||||
'is_complete' : is_complete
|
||||
}
|
||||
|
||||
files = {
|
||||
'auth_info' : (json.dumps(auth_info)),
|
||||
'torrent_info' : (json.dumps(metadata)),
|
||||
'torrent_file' : ('{0}'.format(torrent_file), open(torrent_file, 'rb'), 'application/octet-stream')
|
||||
}
|
||||
|
||||
response = requests.post(url, files=files)
|
||||
json_response = response.json()
|
||||
print(json_response)
|
||||
# A successful request should print {'Success': int(torrent_id)}
|
|
@ -9,7 +9,7 @@ NYAA_HOST = 'https://nyaa.si'
|
|||
SUKEBEI_HOST = 'https://sukebei.nyaa.si'
|
||||
|
||||
API_BASE = '/api'
|
||||
API_UPLOAD = API_BASE + '/v2/upload'
|
||||
API_UPLOAD = API_BASE + '/upload'
|
||||
|
||||
NYAA_CATS = '''1_1 - Anime - AMV
|
||||
1_2 - Anime - English
|
||||
|
|
Loading…
Reference in a new issue