mirror of
https://gitlab.com/SIGBUS/nyaa.git
synced 2024-12-22 14:10: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
|
# NyaaV2
|
||||||
|
|
||||||
## Setup:
|
## Setup
|
||||||
|
|
||||||
- Create your virtualenv, for example with `pyvenv venv`
|
|
||||||
- Enter your virtualenv with `source venv/bin/activate`
|
|
||||||
- Install dependencies with `pip install -r requirements.txt`
|
|
||||||
- Run `python db_create.py` to create the database
|
|
||||||
- Start the dev server with `python run.py`
|
|
||||||
|
|
||||||
## Updated Setup (python 3.6.1):
|
|
||||||
|
|
||||||
- Install dependencies https://github.com/pyenv/pyenv/wiki/Common-build-problems
|
- Install dependencies https://github.com/pyenv/pyenv/wiki/Common-build-problems
|
||||||
- Install `pyenv` https://github.com/pyenv/pyenv/blob/master/README.md#installation
|
- Install `pyenv` https://github.com/pyenv/pyenv/blob/master/README.md#installation
|
||||||
|
@ -20,7 +12,7 @@
|
||||||
- Copy `config.example.py` into `config.py`
|
- Copy `config.example.py` into `config.py`
|
||||||
- Change TABLE_PREFIX to `nyaa_` or `sukebei_` depending on the site
|
- 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
|
- Enable `USE_MYSQL` flag in config.py
|
||||||
- Install latest mariadb by following instructions here https://downloads.mariadb.org/mariadb/repositories/
|
- 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`
|
- 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;`
|
- `CREATE DATABASE nyaav2 DEFAULT CHARACTER SET utf8 COLLATE utf8_bin;`
|
||||||
- `SOURCE ~/path/to/database/nyaa_maria_vx.sql`
|
- `SOURCE ~/path/to/database/nyaa_maria_vx.sql`
|
||||||
|
|
||||||
## Finishing up
|
### Finishing up
|
||||||
- Run `python db_create.py` to create the database
|
- Run `python db_create.py` to create the database
|
||||||
- Load the .sql file
|
- Load the .sql file
|
||||||
- `mysql -u user -p nyaav2`
|
- `mysql -u user -p nyaav2`
|
||||||
- `SOURCE cocks.sql`
|
- `SOURCE cocks.sql`
|
||||||
- Remember to change the default user password to an empty string to disable logging in
|
- Remember to change the default user password to an empty string to disable logging in
|
||||||
- Start the dev server with `python run.py`
|
- 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 jdk `sudo apt-get install openjdk-8-jdk`
|
||||||
- Install elasticsearch https://www.elastic.co/guide/en/elasticsearch/reference/current/deb.html
|
- Install elasticsearch https://www.elastic.co/guide/en/elasticsearch/reference/current/deb.html
|
||||||
- `sudo systemctl enable elasticsearch.service`
|
- `sudo systemctl enable elasticsearch.service`
|
||||||
|
@ -54,7 +46,7 @@
|
||||||
- Run `curl -XGET 'localhost:9200'` and make sure ES is running
|
- Run `curl -XGET 'localhost:9200'` and make sure ES is running
|
||||||
- Optional: install Kabana as a search frontend for ES
|
- 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
|
- Add the `[mariadb]` bin-log section to my.cnf and reload mysql server
|
||||||
- Connect to mysql
|
- Connect to mysql
|
||||||
- `SHOW VARIABLES LIKE 'binlog_format';`
|
- `SHOW VARIABLES LIKE 'binlog_format';`
|
||||||
|
@ -62,7 +54,7 @@
|
||||||
- Connect to root user
|
- Connect to root user
|
||||||
- `GRANT REPLICATION SLAVE ON *.* TO 'test'@'localhost';` where test is the user you will be running `sync_es.py` with
|
- `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`
|
- Run `./create_es.sh` and this creates two indicies: `nyaa` and `sukebei`
|
||||||
- The output should show `acknowledged: true` twice
|
- The output should show `acknowledged: true` twice
|
||||||
- The safest bet is to disable the webapp here to ensure there's no database writes
|
- 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`
|
- Run `python import_to_es.py` with `SITE_FLAVOR` set to `sukebei`
|
||||||
- These will take some time to run as it's indexing
|
- 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
|
- 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
|
- Configure the MySQL options with the user where you granted the REPLICATION permissions
|
||||||
- Connect to MySQL, run `SHOW MASTER STATUS;`.
|
- 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
|
- 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
|
- Make sure `sync_es.py` runs within venv with the right dependencies
|
||||||
|
|
||||||
## Good to go!
|
Enable the `USE_ELASTIC_SEARCH` flag in `config.py`, restart the application, and you're good to go.
|
||||||
- After that, enable the `USE_ELASTIC_SEARCH` flag and restart the webapp 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:
|
## Code Quality:
|
||||||
- Remember to follow PEP8 style guidelines and run `./lint.sh` before committing.
|
- 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__))
|
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
|
||||||
if USE_MYSQL:
|
if USE_MYSQL:
|
||||||
SQLALCHEMY_DATABASE_URI = ('mysql://test:test123@localhost/nyaav2')
|
SQLALCHEMY_DATABASE_URI = ('mysql://test:test123@localhost/nyaav2?charset=utf8mb4')
|
||||||
else:
|
else:
|
||||||
SQLALCHEMY_DATABASE_URI = (
|
SQLALCHEMY_DATABASE_URI = (
|
||||||
'sqlite:///' + os.path.join(BASE_DIR, 'test.db') + '?check_same_thread=False')
|
'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.add(main_cat)
|
||||||
|
|
||||||
db.session.commit()
|
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
|
- resolution
|
||||||
- lowercase
|
- lowercase
|
||||||
- my_ngram
|
- my_ngram
|
||||||
|
- word_delimit
|
||||||
filter:
|
filter:
|
||||||
my_ngram:
|
my_ngram:
|
||||||
type: edgeNGram
|
type: edgeNGram
|
||||||
|
@ -28,7 +29,11 @@ settings:
|
||||||
max_gram: 15
|
max_gram: 15
|
||||||
resolution:
|
resolution:
|
||||||
type: pattern_capture
|
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:
|
char_filter:
|
||||||
my_char_filter:
|
my_char_filter:
|
||||||
type: mapping
|
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
|
# Database
|
||||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||||
|
app.config['MYSQL_DATABASE_CHARSET'] = 'utf8mb4'
|
||||||
|
|
||||||
# Don't refresh cookie each request
|
# Don't refresh cookie each request
|
||||||
app.config['SESSION_REFRESH_EACH_REQUEST'] = False
|
app.config['SESSION_REFRESH_EACH_REQUEST'] = False
|
||||||
|
@ -37,7 +38,7 @@ if not app.config['DEBUG']:
|
||||||
def internal_error(exception):
|
def internal_error(exception):
|
||||||
app.logger.error(exception)
|
app.logger.error(exception)
|
||||||
flask.flash(flask.Markup(
|
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('/')
|
return flask.redirect('/')
|
||||||
|
|
||||||
# Get git commit hash
|
# Get git commit hash
|
||||||
|
|
|
@ -6,6 +6,9 @@ from nyaa import models, forms
|
||||||
from nyaa import bencode, backend, utils
|
from nyaa import bencode, backend, utils
|
||||||
from nyaa import torrents
|
from nyaa import torrents
|
||||||
|
|
||||||
|
# For _create_upload_category_choices
|
||||||
|
from nyaa import routes
|
||||||
|
|
||||||
import functools
|
import functools
|
||||||
import json
|
import json
|
||||||
import os.path
|
import os.path
|
||||||
|
@ -42,84 +45,7 @@ def api_require_user(f):
|
||||||
return decorator
|
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 ####################################
|
# #################################### 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
|
# Map UploadForm fields to API keys
|
||||||
UPLOAD_API_FORM_KEYMAP = {
|
UPLOAD_API_FORM_KEYMAP = {
|
||||||
|
@ -134,19 +60,20 @@ UPLOAD_API_FORM_KEYMAP = {
|
||||||
'is_trusted': 'trusted'
|
'is_trusted': 'trusted'
|
||||||
}
|
}
|
||||||
UPLOAD_API_FORM_KEYMAP_REVERSE = {v: k for k, v in UPLOAD_API_FORM_KEYMAP.items()}
|
UPLOAD_API_FORM_KEYMAP_REVERSE = {v: k for k, v in UPLOAD_API_FORM_KEYMAP.items()}
|
||||||
UPLOAD_API_KEYS = [
|
UPLOAD_API_DEFAULTS = {
|
||||||
'name',
|
'name': '',
|
||||||
'category',
|
'category': '',
|
||||||
'anonymous',
|
'anonymous': False,
|
||||||
'hidden',
|
'hidden': False,
|
||||||
'complete',
|
'complete': False,
|
||||||
'remake',
|
'remake': False,
|
||||||
'trusted',
|
'trusted': True,
|
||||||
'information',
|
'information': '',
|
||||||
'description'
|
'description': ''
|
||||||
]
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@api_blueprint.route('/upload', methods=['POST'])
|
||||||
@api_blueprint.route('/v2/upload', methods=['POST'])
|
@api_blueprint.route('/v2/upload', methods=['POST'])
|
||||||
@basic_auth_user
|
@basic_auth_user
|
||||||
@api_require_user
|
@api_require_user
|
||||||
|
@ -158,16 +85,21 @@ def v2_api_upload():
|
||||||
request_data_field = flask.request.form.get('torrent_data')
|
request_data_field = flask.request.form.get('torrent_data')
|
||||||
if request_data_field is None:
|
if request_data_field is None:
|
||||||
return flask.jsonify({'errors': ['missing torrent_data field']}), 400
|
return flask.jsonify({'errors': ['missing torrent_data field']}), 400
|
||||||
request_data = json.loads(request_data_field)
|
|
||||||
|
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
|
# 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_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
|
# Flask-WTF (very helpfully!!) automatically grabs the request form, so force a None formdata
|
||||||
upload_form = forms.UploadForm(None, data=mapped_dict)
|
upload_form = forms.UploadForm(None, data=mapped_dict, meta={'csrf': False})
|
||||||
upload_form.category.choices = _create_upload_category_choices()
|
upload_form.category.choices = routes._create_upload_category_choices()
|
||||||
|
|
||||||
if upload_form.validate():
|
if upload_form.validate():
|
||||||
torrent = backend.handle_torrent_upload(upload_form, flask.g.user)
|
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
|
# 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()}
|
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
|
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 app, db
|
||||||
from nyaa import models, forms
|
from nyaa import models, forms
|
||||||
from nyaa import bencode, utils
|
from nyaa import bencode, utils
|
||||||
|
@ -8,6 +9,7 @@ import json
|
||||||
from werkzeug import secure_filename
|
from werkzeug import secure_filename
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from orderedset import OrderedSet
|
from orderedset import OrderedSet
|
||||||
|
from ipaddress import ip_address
|
||||||
|
|
||||||
|
|
||||||
def _replace_utf8_values(dict_or_list):
|
def _replace_utf8_values(dict_or_list):
|
||||||
|
@ -53,7 +55,8 @@ def handle_torrent_upload(upload_form, uploading_user=None, fromAPI=False):
|
||||||
description=description,
|
description=description,
|
||||||
encoding=torrent_encoding,
|
encoding=torrent_encoding,
|
||||||
filesize=torrent_filesize,
|
filesize=torrent_filesize,
|
||||||
user=uploading_user)
|
user=uploading_user,
|
||||||
|
uploader_ip=ip_address(flask.request.remote_addr).packed)
|
||||||
|
|
||||||
# Store bencoded info_dict
|
# Store bencoded info_dict
|
||||||
torrent.info = models.TorrentInfo(info_dict=torrent_data.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
|
torrent.complete = upload_form.is_complete.data
|
||||||
# Copy trusted status from user if possible
|
# Copy trusted status from user if possible
|
||||||
can_mark_trusted = uploading_user and uploading_user.is_trusted
|
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
|
torrent.trusted = upload_form.is_trusted.data if can_mark_trusted else False
|
||||||
|
|
||||||
# Set category ids
|
# Set category ids
|
||||||
torrent.main_category_id, torrent.sub_category_id = \
|
torrent.main_category_id, torrent.sub_category_id = \
|
||||||
upload_form.category.parsed_data.get_category_ids()
|
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:
|
if not tracker:
|
||||||
tracker = models.Trackers(uri=announce)
|
tracker = models.Trackers(uri=announce)
|
||||||
db.session.add(tracker)
|
db.session.add(tracker)
|
||||||
|
db.session.flush()
|
||||||
|
|
||||||
db_trackers.add(tracker)
|
db_trackers.add(tracker)
|
||||||
|
|
||||||
db.session.flush()
|
|
||||||
|
|
||||||
# Store tracker refs in DB
|
# Store tracker refs in DB
|
||||||
for order, tracker in enumerate(db_trackers):
|
for order, tracker in enumerate(db_trackers):
|
||||||
torrent_tracker = models.TorrentTrackers(torrent_id=torrent.id,
|
torrent_tracker = models.TorrentTrackers(torrent_id=torrent.id,
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import flask
|
||||||
from nyaa import db, app
|
from nyaa import db, app
|
||||||
from nyaa.models import User
|
from nyaa.models import User
|
||||||
from nyaa import bencode, utils, models
|
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 wtforms.widgets import html_params, HTMLString
|
||||||
|
|
||||||
from flask_wtf.recaptcha import RecaptchaField
|
from flask_wtf.recaptcha import RecaptchaField
|
||||||
|
from flask_wtf.recaptcha.validators import Recaptcha as RecaptchaValidator
|
||||||
|
|
||||||
|
|
||||||
class Unique(object):
|
class Unique(object):
|
||||||
|
@ -35,7 +37,7 @@ class Unique(object):
|
||||||
|
|
||||||
|
|
||||||
_username_validator = Regexp(
|
_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_-)')
|
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'))
|
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):
|
class EditForm(FlaskForm):
|
||||||
display_name = StringField('Torrent display name', [
|
display_name = StringField('Torrent display name', [
|
||||||
Length(min=3, max=255,
|
Length(min=3, max=255, message='Torrent display name must be at least %(min)d characters '
|
||||||
message='Torrent display name must be at least %(min)d characters long '
|
'long and %(max)d at most.')
|
||||||
'and %(max)d at most.')
|
|
||||||
])
|
])
|
||||||
|
|
||||||
category = DisabledSelectField('Category')
|
category = DisabledSelectField('Category')
|
||||||
|
@ -164,10 +173,6 @@ class EditForm(FlaskForm):
|
||||||
|
|
||||||
|
|
||||||
class UploadForm(FlaskForm):
|
class UploadForm(FlaskForm):
|
||||||
|
|
||||||
class Meta:
|
|
||||||
csrf = False
|
|
||||||
|
|
||||||
torrent_file = FileField('Torrent file', [
|
torrent_file = FileField('Torrent file', [
|
||||||
FileRequired()
|
FileRequired()
|
||||||
])
|
])
|
||||||
|
@ -179,6 +184,16 @@ class UploadForm(FlaskForm):
|
||||||
'%(max)d at most.')
|
'%(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 = SelectField('Category')
|
||||||
category = DisabledSelectField('Category')
|
category = DisabledSelectField('Category')
|
||||||
|
|
||||||
|
@ -263,7 +278,7 @@ class UploadForm(FlaskForm):
|
||||||
|
|
||||||
|
|
||||||
class UserForm(FlaskForm):
|
class UserForm(FlaskForm):
|
||||||
user_class = DisabledSelectField('Change User Class')
|
user_class = SelectField('Change User Class')
|
||||||
|
|
||||||
def validate_user_class(form, field):
|
def validate_user_class(form, field):
|
||||||
if not field.data:
|
if not field.data:
|
||||||
|
@ -309,7 +324,8 @@ def _validate_trackers(torrent_dict, tracker_to_check_for=None):
|
||||||
for announce in announce_list:
|
for announce in announce_list:
|
||||||
_validate_list(announce, 'announce-list item')
|
_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():
|
if tracker_to_check_for and announce_string.lower() == tracker_to_check_for.lower():
|
||||||
tracker_found = True
|
tracker_found = True
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import flask
|
||||||
from enum import Enum, IntEnum
|
from enum import Enum, IntEnum
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from nyaa import app, db
|
from nyaa import app, db
|
||||||
|
@ -6,11 +7,13 @@ from sqlalchemy import func, ForeignKeyConstraint, Index
|
||||||
from sqlalchemy_utils import ChoiceType, EmailType, PasswordType
|
from sqlalchemy_utils import ChoiceType, EmailType, PasswordType
|
||||||
from werkzeug.security import generate_password_hash, check_password_hash
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
from sqlalchemy_fulltext import FullText
|
from sqlalchemy_fulltext import FullText
|
||||||
|
from ipaddress import ip_address
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import base64
|
import base64
|
||||||
from markupsafe import escape as escape_markup
|
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']:
|
if app.config['USE_MYSQL']:
|
||||||
from sqlalchemy.dialects import mysql
|
from sqlalchemy.dialects import mysql
|
||||||
|
@ -61,6 +64,7 @@ class Torrent(db.Model):
|
||||||
encoding = db.Column(db.String(length=32), nullable=False)
|
encoding = db.Column(db.String(length=32), nullable=False)
|
||||||
flags = db.Column(db.Integer, default=0, nullable=False, index=True)
|
flags = db.Column(db.Integer, default=0, nullable=False, index=True)
|
||||||
uploader_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=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)
|
has_torrent = db.Column(db.Boolean, nullable=False, default=False)
|
||||||
|
|
||||||
created_time = db.Column(db.DateTime(timezone=False), default=datetime.utcnow, nullable=False)
|
created_time = db.Column(db.DateTime(timezone=False), default=datetime.utcnow, nullable=False)
|
||||||
|
@ -92,11 +96,13 @@ class Torrent(db.Model):
|
||||||
info = db.relationship('TorrentInfo', uselist=False,
|
info = db.relationship('TorrentInfo', uselist=False,
|
||||||
cascade="all, delete-orphan", back_populates='torrent')
|
cascade="all, delete-orphan", back_populates='torrent')
|
||||||
filelist = db.relationship('TorrentFilelist', uselist=False,
|
filelist = db.relationship('TorrentFilelist', uselist=False,
|
||||||
cascade="all, delete-orphan", back_populates='torrent')
|
cascade="all, delete-orphan", back_populates='torrent')
|
||||||
stats = db.relationship('Statistic', uselist=False,
|
stats = db.relationship('Statistic', uselist=False,
|
||||||
cascade="all, delete-orphan", back_populates='torrent', lazy='joined')
|
cascade="all, delete-orphan", back_populates='torrent', lazy='joined')
|
||||||
trackers = db.relationship('TorrentTrackers', uselist=True,
|
trackers = db.relationship('TorrentTrackers', uselist=True, cascade="all, delete-orphan",
|
||||||
cascade="all, delete-orphan", lazy='joined')
|
lazy='joined', order_by='TorrentTrackers.order')
|
||||||
|
comments = db.relationship('Comment', uselist=True,
|
||||||
|
cascade="all, delete-orphan")
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<{0} #{1.id} \'{1.display_name}\' {1.filesize}b>'.format(type(self).__name__, 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):
|
def magnet_uri(self):
|
||||||
return create_magnet(self)
|
return create_magnet(self)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def uploader_ip_string(self):
|
||||||
|
if self.uploader_ip:
|
||||||
|
return str(ip_address(self.uploader_ip))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def anonymous(self):
|
def anonymous(self):
|
||||||
return self.flags & TorrentFlags.ANONYMOUS
|
return self.flags & TorrentFlags.ANONYMOUS
|
||||||
|
@ -194,6 +205,11 @@ class Torrent(db.Model):
|
||||||
def by_info_hash(cls, info_hash):
|
def by_info_hash(cls, info_hash):
|
||||||
return cls.query.filter_by(info_hash=info_hash).first()
|
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):
|
class TorrentNameSearch(FullText, Torrent):
|
||||||
__fulltext_columns__ = ('display_name',)
|
__fulltext_columns__ = ('display_name',)
|
||||||
|
@ -310,10 +326,31 @@ class SubCategory(db.Model):
|
||||||
return cls.query.get((sub_cat_id, main_cat_id))
|
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):
|
class UserLevelType(IntEnum):
|
||||||
REGULAR = 0
|
REGULAR = 0
|
||||||
TRUSTED = 1
|
TRUSTED = 1
|
||||||
ADMIN = 2
|
MODERATOR = 2
|
||||||
SUPERADMIN = 3
|
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_date = db.Column(db.DateTime(timezone=False), default=None, nullable=True)
|
||||||
last_login_ip = db.Column(db.Binary(length=16), 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')
|
# session = db.relationship('Session', uselist=False, back_populates='user')
|
||||||
|
|
||||||
def __init__(self, username, email, password):
|
def __init__(self, username, email, password):
|
||||||
|
@ -363,6 +400,39 @@ class User(db.Model):
|
||||||
]
|
]
|
||||||
return all(checks)
|
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
|
@classmethod
|
||||||
def by_id(cls, id):
|
def by_id(cls, id):
|
||||||
return cls.query.get(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)
|
return cls.by_username(username_or_email) or cls.by_email(username_or_email)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_admin(self):
|
def is_moderator(self):
|
||||||
return self.level >= UserLevelType.ADMIN
|
return self.level >= UserLevelType.MODERATOR
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_superadmin(self):
|
def is_superadmin(self):
|
||||||
|
|
294
nyaa/routes.py
294
nyaa/routes.py
|
@ -7,11 +7,13 @@ from nyaa import torrents
|
||||||
from nyaa import backend
|
from nyaa import backend
|
||||||
from nyaa import api_handler
|
from nyaa import api_handler
|
||||||
from nyaa.search import search_elastic, search_db
|
from nyaa.search import search_elastic, search_db
|
||||||
|
from sqlalchemy.orm import joinedload
|
||||||
import config
|
import config
|
||||||
|
|
||||||
|
import re
|
||||||
import json
|
import json
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import ipaddress
|
from ipaddress import ip_address
|
||||||
import os.path
|
import os.path
|
||||||
import base64
|
import base64
|
||||||
from urllib.parse import quote
|
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.')
|
'what you were looking for.')
|
||||||
|
|
||||||
|
|
||||||
|
# For static_cachebuster
|
||||||
|
_static_cache = {}
|
||||||
|
|
||||||
|
|
||||||
def redirect_url():
|
def redirect_url():
|
||||||
url = flask.request.args.get('next') or \
|
url = flask.request.args.get('next') or \
|
||||||
flask.request.referrer or \
|
flask.request.referrer or \
|
||||||
|
@ -44,6 +50,31 @@ def redirect_url():
|
||||||
return 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()
|
@app.template_global()
|
||||||
def modify_query(**new_values):
|
def modify_query(**new_values):
|
||||||
args = flask.request.args.copy()
|
args = flask.request.args.copy()
|
||||||
|
@ -132,8 +163,6 @@ def get_category_id_map():
|
||||||
# Routes start here #
|
# Routes start here #
|
||||||
|
|
||||||
|
|
||||||
app.register_blueprint(api_handler.api_blueprint, url_prefix='/api')
|
|
||||||
|
|
||||||
def chain_get(source, *args):
|
def chain_get(source, *args):
|
||||||
''' Tries to return values from source by the given keys.
|
''' Tries to return values from source by the given keys.
|
||||||
Returns None if none match.
|
Returns None if none match.
|
||||||
|
@ -145,6 +174,7 @@ def chain_get(source, *args):
|
||||||
return value
|
return value
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@app.route('/rss', defaults={'rss': True})
|
@app.route('/rss', defaults={'rss': True})
|
||||||
@app.route('/', defaults={'rss': False})
|
@app.route('/', defaults={'rss': False})
|
||||||
def home(rss):
|
def home(rss):
|
||||||
|
@ -180,6 +210,26 @@ def home(rss):
|
||||||
flask.abort(404)
|
flask.abort(404)
|
||||||
user_id = user.id
|
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 = {
|
query_args = {
|
||||||
'user': user_id,
|
'user': user_id,
|
||||||
'sort': sort_key or 'id',
|
'sort': sort_key or 'id',
|
||||||
|
@ -193,9 +243,17 @@ def home(rss):
|
||||||
|
|
||||||
if flask.g.user:
|
if flask.g.user:
|
||||||
query_args['logged_in_user'] = 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
|
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
|
# If searching, we get results from elastic search
|
||||||
use_elastic = app.config.get('USE_ELASTIC_SEARCH')
|
use_elastic = app.config.get('USE_ELASTIC_SEARCH')
|
||||||
if use_elastic and search_term:
|
if use_elastic and search_term:
|
||||||
|
@ -212,9 +270,12 @@ def home(rss):
|
||||||
query_results = search_elastic(**query_args)
|
query_results = search_elastic(**query_args)
|
||||||
|
|
||||||
if render_as_rss:
|
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:
|
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'])
|
max_results = min(max_search_results, query_results['hits']['total'])
|
||||||
# change p= argument to whatever you change page_parameter to or pagination breaks
|
# change p= argument to whatever you change page_parameter to or pagination breaks
|
||||||
pagination = Pagination(p=query_args['page'], per_page=results_per_page,
|
pagination = Pagination(p=query_args['page'], per_page=results_per_page,
|
||||||
|
@ -225,7 +286,8 @@ def home(rss):
|
||||||
pagination=pagination,
|
pagination=pagination,
|
||||||
torrent_query=query_results,
|
torrent_query=query_results,
|
||||||
search=query_args,
|
search=query_args,
|
||||||
rss_filter=rss_query_string)
|
rss_filter=rss_query_string,
|
||||||
|
special_results=special_results)
|
||||||
else:
|
else:
|
||||||
# If ES is enabled, default to db search for browsing
|
# If ES is enabled, default to db search for browsing
|
||||||
if use_elastic:
|
if use_elastic:
|
||||||
|
@ -237,7 +299,8 @@ def home(rss):
|
||||||
if render_as_rss:
|
if render_as_rss:
|
||||||
return render_rss('Home', query, use_elastic=False, magnet_links=use_magnet_links)
|
return render_rss('Home', query, use_elastic=False, magnet_links=use_magnet_links)
|
||||||
else:
|
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
|
# 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)
|
# if we're browsing without a search term (which means we default to DB)
|
||||||
# or if ES is disabled
|
# or if ES is disabled
|
||||||
|
@ -245,7 +308,8 @@ def home(rss):
|
||||||
use_elastic=False,
|
use_elastic=False,
|
||||||
torrent_query=query,
|
torrent_query=query,
|
||||||
search=query_args,
|
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'])
|
@app.route('/user/<user_name>', methods=['GET', 'POST'])
|
||||||
|
@ -255,22 +319,23 @@ def view_user(user_name):
|
||||||
if not user:
|
if not user:
|
||||||
flask.abort(404)
|
flask.abort(404)
|
||||||
|
|
||||||
if flask.g.user and flask.g.user.id != user.id:
|
admin_form = None
|
||||||
admin = flask.g.user.is_admin
|
if flask.g.user and flask.g.user.is_moderator and flask.g.user.level > user.level:
|
||||||
superadmin = flask.g.user.is_superadmin
|
admin_form = forms.UserForm()
|
||||||
else:
|
default, admin_form.user_class.choices = _create_user_class_choices(user)
|
||||||
admin = False
|
if flask.request.method == 'GET':
|
||||||
superadmin = False
|
admin_form.user_class.data = default
|
||||||
|
|
||||||
form = forms.UserForm()
|
if flask.request.method == 'POST' and admin_form and admin_form.validate():
|
||||||
form.user_class.choices = _create_user_class_choices()
|
selection = admin_form.user_class.data
|
||||||
if flask.request.method == 'POST' and form.validate():
|
|
||||||
selection = form.user_class.data
|
|
||||||
|
|
||||||
if selection == 'regular':
|
if selection == 'regular':
|
||||||
user.level = models.UserLevelType.REGULAR
|
user.level = models.UserLevelType.REGULAR
|
||||||
elif selection == 'trusted':
|
elif selection == 'trusted':
|
||||||
user.level = models.UserLevelType.TRUSTED
|
user.level = models.UserLevelType.TRUSTED
|
||||||
|
elif selection == 'moderator':
|
||||||
|
user.level = models.UserLevelType.MODERATOR
|
||||||
|
|
||||||
db.session.add(user)
|
db.session.add(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
@ -310,7 +375,7 @@ def view_user(user_name):
|
||||||
|
|
||||||
if flask.g.user:
|
if flask.g.user:
|
||||||
query_args['logged_in_user'] = 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
|
query_args['admin'] = True
|
||||||
|
|
||||||
# Use elastic search for term searching
|
# Use elastic search for term searching
|
||||||
|
@ -343,9 +408,7 @@ def view_user(user_name):
|
||||||
user_page=True,
|
user_page=True,
|
||||||
rss_filter=rss_query_string,
|
rss_filter=rss_query_string,
|
||||||
level=user_level,
|
level=user_level,
|
||||||
admin=admin,
|
admin_form=admin_form)
|
||||||
superadmin=superadmin,
|
|
||||||
form=form)
|
|
||||||
# Similar logic as home page
|
# Similar logic as home page
|
||||||
else:
|
else:
|
||||||
if use_elastic:
|
if use_elastic:
|
||||||
|
@ -361,9 +424,7 @@ def view_user(user_name):
|
||||||
user_page=True,
|
user_page=True,
|
||||||
rss_filter=rss_query_string,
|
rss_filter=rss_query_string,
|
||||||
level=user_level,
|
level=user_level,
|
||||||
admin=admin,
|
admin_form=admin_form)
|
||||||
superadmin=superadmin,
|
|
||||||
form=form)
|
|
||||||
|
|
||||||
|
|
||||||
@app.template_filter('rfc822')
|
@app.template_filter('rfc822')
|
||||||
|
@ -416,7 +477,7 @@ def login():
|
||||||
return flask.redirect(flask.url_for('login'))
|
return flask.redirect(flask.url_for('login'))
|
||||||
|
|
||||||
user.last_login_date = datetime.utcnow()
|
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.add(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
@ -450,7 +511,7 @@ def register():
|
||||||
if flask.request.method == 'POST' and form.validate():
|
if flask.request.method == 'POST' and form.validate():
|
||||||
user = models.User(username=form.username.data.strip(),
|
user = models.User(username=form.username.data.strip(),
|
||||||
email=form.email.data.strip(), password=form.password.data)
|
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.add(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
@ -478,14 +539,6 @@ def profile():
|
||||||
|
|
||||||
form = forms.ProfileForm(flask.request.form)
|
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():
|
if flask.request.method == 'POST' and form.validate():
|
||||||
user = flask.g.user
|
user = flask.g.user
|
||||||
new_email = form.email.data.strip()
|
new_email = form.email.data.strip()
|
||||||
|
@ -515,12 +568,7 @@ def profile():
|
||||||
flask.g.user = user
|
flask.g.user = user
|
||||||
return flask.redirect('/profile')
|
return flask.redirect('/profile')
|
||||||
|
|
||||||
_user = models.User.by_id(flask.g.user.id)
|
return flask.render_template('profile.html', form=form)
|
||||||
username = _user.username
|
|
||||||
current_email = _user.email
|
|
||||||
|
|
||||||
return flask.render_template('profile.html', form=form, name=username, email=current_email,
|
|
||||||
level=level)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/user/activate/<payload>')
|
@app.route('/user/activate/<payload>')
|
||||||
|
@ -562,34 +610,65 @@ def _create_upload_category_choices():
|
||||||
|
|
||||||
@app.route('/upload', methods=['GET', 'POST'])
|
@app.route('/upload', methods=['GET', 'POST'])
|
||||||
def upload():
|
def upload():
|
||||||
form = forms.UploadForm(CombinedMultiDict((flask.request.files, flask.request.form)))
|
upload_form = forms.UploadForm(CombinedMultiDict((flask.request.files, flask.request.form)))
|
||||||
form.category.choices = _create_upload_category_choices()
|
upload_form.category.choices = _create_upload_category_choices()
|
||||||
|
|
||||||
if flask.request.method == 'POST' and form.validate():
|
if flask.request.method == 'POST' and upload_form.validate():
|
||||||
torrent = backend.handle_torrent_upload(form, flask.g.user)
|
torrent = backend.handle_torrent_upload(upload_form, flask.g.user)
|
||||||
|
|
||||||
return flask.redirect('/view/' + str(torrent.id))
|
return flask.redirect('/view/' + str(torrent.id))
|
||||||
else:
|
else:
|
||||||
# If we get here with a POST, it means the form data was invalid: return a non-okay status
|
# 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
|
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):
|
def view_torrent(torrent_id):
|
||||||
torrent = models.Torrent.by_id(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:
|
if not torrent:
|
||||||
flask.abort(404)
|
flask.abort(404)
|
||||||
|
|
||||||
# Only allow admins see deleted torrents
|
# 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)
|
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
|
# 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
|
files = None
|
||||||
if torrent.filelist:
|
if torrent.filelist:
|
||||||
|
@ -598,11 +677,32 @@ def view_torrent(torrent_id):
|
||||||
report_form = forms.ReportForm()
|
report_form = forms.ReportForm()
|
||||||
return flask.render_template('view.html', torrent=torrent,
|
return flask.render_template('view.html', torrent=torrent,
|
||||||
files=files,
|
files=files,
|
||||||
viewer=viewer,
|
comment_form=comment_form,
|
||||||
|
comments=torrent.comments,
|
||||||
can_edit=can_edit,
|
can_edit=can_edit,
|
||||||
report_form=report_form)
|
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'])
|
@app.route('/view/<int:torrent_id>/edit', methods=['GET', 'POST'])
|
||||||
def edit_torrent(torrent_id):
|
def edit_torrent(torrent_id):
|
||||||
torrent = models.Torrent.by_id(torrent_id)
|
torrent = models.Torrent.by_id(torrent_id)
|
||||||
|
@ -615,11 +715,11 @@ def edit_torrent(torrent_id):
|
||||||
flask.abort(404)
|
flask.abort(404)
|
||||||
|
|
||||||
# Only allow admins edit deleted torrents
|
# 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)
|
flask.abort(404)
|
||||||
|
|
||||||
# Only allow torrent owners or admins edit torrents
|
# 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)
|
flask.abort(403)
|
||||||
|
|
||||||
if flask.request.method == 'POST' and form.validate():
|
if flask.request.method == 'POST' and form.validate():
|
||||||
|
@ -635,15 +735,16 @@ def edit_torrent(torrent_id):
|
||||||
torrent.complete = form.is_complete.data
|
torrent.complete = form.is_complete.data
|
||||||
torrent.anonymous = form.is_anonymous.data
|
torrent.anonymous = form.is_anonymous.data
|
||||||
|
|
||||||
if editor.is_trusted:
|
if flask.g.user.is_trusted:
|
||||||
torrent.trusted = form.is_trusted.data
|
torrent.trusted = form.is_trusted.data
|
||||||
if editor.is_admin:
|
if flask.g.user.is_moderator:
|
||||||
torrent.deleted = form.is_deleted.data
|
torrent.deleted = form.is_deleted.data
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
flask.flash(flask.Markup(
|
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))
|
return flask.redirect(flask.url_for('view_torrent', torrent_id=torrent.id))
|
||||||
else:
|
else:
|
||||||
|
@ -664,8 +765,7 @@ def edit_torrent(torrent_id):
|
||||||
|
|
||||||
return flask.render_template('edit.html',
|
return flask.render_template('edit.html',
|
||||||
form=form,
|
form=form,
|
||||||
torrent=torrent,
|
torrent=torrent)
|
||||||
editor=editor)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/view/<int:torrent_id>/magnet')
|
@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('/view/<int:torrent_id>/torrent')
|
||||||
|
@app.route('/download/<int:torrent_id>.torrent')
|
||||||
def download_torrent(torrent_id):
|
def download_torrent(torrent_id):
|
||||||
torrent = models.Torrent.by_id(torrent_id)
|
torrent = models.Torrent.by_id(torrent_id)
|
||||||
|
|
||||||
if not torrent:
|
if not torrent or not torrent.has_torrent:
|
||||||
flask.abort(404)
|
flask.abort(404)
|
||||||
|
|
||||||
resp = flask.Response(_get_cached_torrent_file(torrent))
|
resp = flask.Response(_get_cached_torrent_file(torrent))
|
||||||
resp.headers['Content-Type'] = 'application/x-bittorrent'
|
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')))
|
quote(torrent.torrent_name.encode('utf-8')))
|
||||||
|
|
||||||
return resp
|
return resp
|
||||||
|
@ -717,7 +818,7 @@ def submit_report(torrent_id):
|
||||||
|
|
||||||
@app.route('/reports', methods=['GET', 'POST'])
|
@app.route('/reports', methods=['GET', 'POST'])
|
||||||
def view_reports():
|
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)
|
flask.abort(403)
|
||||||
|
|
||||||
page = flask.request.args.get('p', flask.request.args.get('offset', 1, int), int)
|
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()
|
server.quit()
|
||||||
|
|
||||||
|
|
||||||
def _create_user_class_choices():
|
def _create_user_class_choices(user):
|
||||||
choices = [('regular', 'Regular')]
|
choices = [('regular', 'Regular')]
|
||||||
if flask.g.user and flask.g.user.is_superadmin:
|
default = 'regular'
|
||||||
choices.append(('trusted', 'Trusted'))
|
if flask.g.user:
|
||||||
return choices
|
if flask.g.user.is_moderator:
|
||||||
|
choices.append(('trusted', 'Trusted'))
|
||||||
|
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 ####################################
|
# #################################### STATIC PAGES ####################################
|
||||||
|
|
||||||
|
|
||||||
@app.route('/rules', methods=['GET'])
|
@app.route('/rules', methods=['GET'])
|
||||||
def site_rules():
|
def site_rules():
|
||||||
return flask.render_template('rules.html')
|
return flask.render_template('rules.html')
|
||||||
|
@ -818,11 +960,11 @@ def site_help():
|
||||||
return flask.render_template('help.html')
|
return flask.render_template('help.html')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/xmlns/nyaa', methods=['GET'])
|
||||||
|
def xmlns_nyaa():
|
||||||
|
return flask.render_template('xmlns.html')
|
||||||
|
|
||||||
|
|
||||||
# #################################### API ROUTES ####################################
|
# #################################### API ROUTES ####################################
|
||||||
@app.route('/api/upload', methods=['POST'])
|
|
||||||
def api_upload():
|
app.register_blueprint(api_handler.api_blueprint, url_prefix='/api')
|
||||||
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
|
|
||||||
|
|
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;
|
padding: 1em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.markdown-source {
|
||||||
|
min-height: 360px;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 991px){
|
@media (max-width: 991px){
|
||||||
.panel-body .col-md-5 {
|
.panel-body .col-md-5 {
|
||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
|
@ -218,3 +222,106 @@ table.torrent-list tbody tr td a:visited {
|
||||||
ul.nav-tabs#profileTabs {
|
ul.nav-tabs#profileTabs {
|
||||||
margin-bottom: 15px;
|
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) {
|
$('body').on('dragenter', function(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
dropZone.css({ 'visibility': 'visible', 'opacity': 1 });
|
dropZone.css({ 'visibility': 'visible', 'opacity': 1 });
|
||||||
|
@ -63,6 +64,13 @@ $(document).ready(function() {
|
||||||
$('#torrent_file')[0].files = files;
|
$('#torrent_file')[0].files = files;
|
||||||
$(this).css({ 'visibility': 'hidden', 'opacity': 0 });
|
$(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) {
|
function _format_time_difference(seconds) {
|
||||||
|
@ -80,6 +88,8 @@ function _format_time_difference(seconds) {
|
||||||
if (seconds < 0) {
|
if (seconds < 0) {
|
||||||
suffix = "";
|
suffix = "";
|
||||||
prefix = "After ";
|
prefix = "After ";
|
||||||
|
} else if (seconds == 0) {
|
||||||
|
return "Just now"
|
||||||
}
|
}
|
||||||
|
|
||||||
var parts = [];
|
var parts = [];
|
||||||
|
@ -96,11 +106,12 @@ function _format_time_difference(seconds) {
|
||||||
}
|
}
|
||||||
return prefix + parts.join(" ") + suffix;
|
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 pad = function (n) { return ("00" + n).slice(-2); }
|
||||||
var ymd = date.getFullYear() + "-" + pad(date.getMonth()+1) + "-" + pad(date.getDate());
|
var ymd = date.getFullYear() + "-" + pad(date.getMonth()+1) + "-" + pad(date.getDate());
|
||||||
var hm = pad(date.getHours()) + ":" + pad(date.getMinutes());
|
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
|
// 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++) {
|
for (var i = 0; i < timestamp_targets.length; i++) {
|
||||||
var target = timestamp_targets[i];
|
var target = timestamp_targets[i];
|
||||||
var torrent_timestamp = parseInt(target.getAttribute('data-timestamp'));
|
var torrent_timestamp = parseInt(target.getAttribute('data-timestamp'));
|
||||||
|
var swap_flag = target.getAttribute('data-timestamp-swap') != null;
|
||||||
|
|
||||||
if (torrent_timestamp) {
|
if (torrent_timestamp) {
|
||||||
var timedelta = now_timestamp - 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" %}
|
{% extends "layout.html" %}
|
||||||
{% block title %}404 Not Found :: {{ config.SITE_NAME }}{% endblock %}
|
{% block title %}404 Not Found :: {{ config.SITE_NAME }}{% endblock %}
|
||||||
|
{% block metatags %}
|
||||||
|
<meta property="og:description" content="Nothing here.">
|
||||||
|
{% endblock %}
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<h1>404 Not Found</h1>
|
<h1>404 Not Found</h1>
|
||||||
<p>The path you requested does not exist on this server.</p>
|
<p>The path you requested does not exist on this server.</p>
|
||||||
|
|
|
@ -37,12 +37,12 @@
|
||||||
{{ field.label(class='control-label') }}
|
{{ field.label(class='control-label') }}
|
||||||
<ul class="nav nav-tabs" role="tablist">
|
<ul class="nav nav-tabs" role="tablist">
|
||||||
<li role="presentation" class="active">
|
<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
|
Write
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li role="presentation">
|
<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
|
Preview
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -119,4 +119,4 @@
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
{% set torrent_url = url_for('view_torrent', torrent_id=torrent.id) %}
|
{% set torrent_url = url_for('view_torrent', torrent_id=torrent.id) %}
|
||||||
<h1>
|
<h1>
|
||||||
Edit Torrent <a href="{{ torrent_url }}">#{{torrent.id}}</a>
|
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>)
|
(by <a href="{{ url_for('view_user', user_name=torrent.user.username) }}">{{ torrent.user.username }}</a>)
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</h1>
|
</h1>
|
||||||
|
@ -29,41 +29,54 @@
|
||||||
{{ render_field(form.information, class_='form-control', placeholder='Your website or IRC channel') }}
|
{{ render_field(form.information, class_='form-control', placeholder='Your website or IRC channel') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="control-label">Torrent flags</label>
|
<label class="control-label">Torrent flags</label><br>
|
||||||
<div>
|
<div class="btn-group" data-toggle="buttons">
|
||||||
{% if editor.is_admin %}
|
{# Only allow changing anonymous status when an uploader exists #}
|
||||||
<label class="btn btn-primary">
|
{% if torrent.uploader_id %}
|
||||||
{{ form.is_deleted }}
|
<label class="btn btn-default {% if torrent.anonymous %}active{% endif %}" title="Upload torrent anonymously (don't display your username)">
|
||||||
Deleted
|
{{ form.is_anonymous }}
|
||||||
</label>
|
<span class="glyphicon glyphicon-check"></span>
|
||||||
{% endif %}
|
<span class="glyphicon glyphicon-unchecked"></span>
|
||||||
|
Anonymous
|
||||||
<label class="btn btn-default" style="background-color: darkgray; border-color: #ccc;" title="Hide torrent from listing">
|
</label>
|
||||||
{{ form.is_hidden }}
|
{% endif %}
|
||||||
Hidden
|
<label class="btn btn-grey {% if torrent.hidden %}active{% endif %}" title="Hide torrent from listing">
|
||||||
</label>
|
{{ form.is_hidden }}
|
||||||
<label class="btn btn-danger" title="This torrent is derived from another release">
|
<span class="glyphicon glyphicon-check"></span>
|
||||||
{{ form.is_remake }}
|
<span class="glyphicon glyphicon-unchecked"></span>
|
||||||
Remake
|
Hidden
|
||||||
</label>
|
</label>
|
||||||
<label class="btn btn-primary" title="This torrent is a complete batch (eg. season)">
|
{% if g.user.is_moderator %}
|
||||||
{{ form.is_complete }}
|
<label class="btn btn-primary {% if torrent.deleted %}active{% endif %}">
|
||||||
Complete
|
{{ form.is_deleted }}
|
||||||
</label>
|
<span class="glyphicon glyphicon-check"></span>
|
||||||
|
<span class="glyphicon glyphicon-unchecked"></span>
|
||||||
{# Only allow changing anonymous status when an uploader exists #}
|
Deleted
|
||||||
{% if torrent.uploader_id %}
|
</label>
|
||||||
<label class="btn btn-primary" title="Upload torrent anonymously (don't display your username)">
|
{% endif %}
|
||||||
{{ form.is_anonymous }}
|
</div>
|
||||||
Anonymous
|
<div class="hidden-xl"><br></div>
|
||||||
</label>
|
<div class="btn-group" data-toggle="buttons">
|
||||||
{% endif %}
|
<label class="btn btn-danger {% if torrent.remake %}active{% endif %}" title="This torrent is derived from another release">
|
||||||
{% if editor.is_trusted %}
|
{{ form.is_remake }}
|
||||||
<label class="btn btn-success" title="Mark torrent trusted">
|
<span class="glyphicon glyphicon-check"></span>
|
||||||
{{ form.is_trusted }}
|
<span class="glyphicon glyphicon-unchecked"></span>
|
||||||
Trusted
|
Remake
|
||||||
</label>
|
</label>
|
||||||
{% endif %}
|
<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 %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,15 +1,22 @@
|
||||||
{% extends "layout.html" %}
|
{% extends "layout.html" %}
|
||||||
{% block title %}{% if search.term %}{{ search.term | e}}{% else %}Browse{% endif %} :: {{ config.SITE_NAME }}{% endblock %}
|
{% 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 %}
|
{% block body %}
|
||||||
|
|
||||||
|
{% if search["term"] == '' %}
|
||||||
<div class="alert alert-info">
|
<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>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>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
|
<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>
|
||||||
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>We welcome you to provide feedback at <a href="irc://irc.rizon.net/nyaa-dev">#nyaa-dev@irc.rizon.net</a></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>
|
<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>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% include "search_results.html" %}
|
{% include "search_results.html" %}
|
||||||
|
|
||||||
|
|
|
@ -1,235 +1,309 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>{% block title %}{{ config.SITE_NAME }}{% endblock %}</title>
|
<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">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<link rel="shortcut icon" type="image/png" href="/static/favicon.png">
|
<link rel="shortcut icon" type="image/png" href="/static/favicon.png">
|
||||||
<link rel="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="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 %}" />
|
<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 %}" />
|
||||||
|
|
||||||
<!-- Bootstrap core CSS -->
|
<meta property="og:site_name" content="{{ config.SITE_NAME }}">
|
||||||
<!--
|
<meta property="og:title" content="{{ self.title() }}">
|
||||||
Note: This has been customized at http://getbootstrap.com/customize/ to
|
<meta property="og:image" content="{% block meta_image %}/static/img/avatar/default.png{% endblock %}">
|
||||||
set the column breakpoint to tablet mode, instead of mobile. This is to
|
{% block metatags %}
|
||||||
make the navbar not look awful on tablets.
|
{# Filled by children #}
|
||||||
-->
|
{% endblock %}
|
||||||
<link href="/static/css/bootstrap.min.css" rel="stylesheet" id="bsThemeLink">
|
|
||||||
<!--
|
<!-- Bootstrap core CSS -->
|
||||||
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
|
Note: This has been customized at http://getbootstrap.com/customize/ to
|
||||||
-->
|
set the column breakpoint to tablet mode, instead of mobile. This is to
|
||||||
<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>
|
make the navbar not look awful on tablets.
|
||||||
<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" />
|
{# These are extracted here for the dark mode toggle #}
|
||||||
|
{% set bootstrap_light = static_cachebuster('/static/css/bootstrap.min.css') %}
|
||||||
<!-- Custom styles for this template -->
|
{% set bootstrap_dark = static_cachebuster('/static/css/bootstrap-dark.min.css') %}
|
||||||
<link href="/static/css/main.css" rel="stylesheet">
|
<link href="{{ bootstrap_light }}" rel="stylesheet" id="bsThemeLink">
|
||||||
|
<link href="{{ static_cachebuster('/static/css/bootstrap-xl-mod.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>
|
This theme changer script needs to be inline and right under the above stylesheet link to prevent FOUC (Flash Of Unstyled Content)
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha256-U5ZEeKfGNOja007MMD3YBI0A3OSZOQbeG6z2f2Y0hu8=" crossorigin="anonymous"></script>
|
Development version is commented out in static/js/main.js at the bottom of the file
|
||||||
<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>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>
|
||||||
<script src="/static/js/bootstrap-select.js"></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" />
|
||||||
<script src="/static/js/main.js"></script>
|
<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" />
|
||||||
|
|
||||||
<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
|
<!-- Custom styles for this template -->
|
||||||
<!--[if lt IE 9]>
|
<link href="{{ static_cachebuster('/static/css/main.css') }}" rel="stylesheet">
|
||||||
<script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
|
|
||||||
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
|
<!-- Core JavaScript -->
|
||||||
<![endif]-->
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js" integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4=" crossorigin="anonymous"></script>
|
||||||
</head>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha256-U5ZEeKfGNOja007MMD3YBI0A3OSZOQbeG6z2f2Y0hu8=" crossorigin="anonymous"></script>
|
||||||
<body>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/commonmark/0.27.0/commonmark.min.js" integrity="sha256-10JreQhQG80GtKuzsioj0K46DlaB/CK/EG+NuG0q97E=" crossorigin="anonymous"></script>
|
||||||
<!-- Fixed navbar -->
|
<!-- Modified to not apply border-radius to selectpickers and stuff so our navbar looks cool -->
|
||||||
<nav class="navbar navbar-default navbar-static-top navbar-inverse">
|
<script src="{{ static_cachebuster('/static/js/bootstrap-select.js') }}"></script>
|
||||||
<div class="container">
|
<script src="{{ static_cachebuster('/static/js/main.js') }}"></script>
|
||||||
<div class="navbar-header">
|
|
||||||
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
|
<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
|
||||||
<span class="sr-only">Toggle navigation</span>
|
<!--[if lt IE 9]>
|
||||||
<span class="icon-bar"></span>
|
<script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
|
||||||
<span class="icon-bar"></span>
|
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
|
||||||
<span class="icon-bar"></span>
|
<![endif]-->
|
||||||
</button>
|
</head>
|
||||||
<a class="navbar-brand" href="/">{{ config.SITE_NAME }}</a>
|
<body>
|
||||||
</div>
|
<!-- Fixed navbar -->
|
||||||
<div id="navbar" class="navbar-collapse collapse">
|
<nav class="navbar navbar-default navbar-static-top navbar-inverse">
|
||||||
<ul class="nav navbar-nav">
|
<div class="container">
|
||||||
<li {% if request.path == "/upload" %} class="active"{% endif %}><a href="/upload">Upload</a></li>
|
<div class="navbar-header">
|
||||||
<li class="dropdown">
|
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
|
||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
|
<span class="sr-only">Toggle navigation</span>
|
||||||
About
|
<span class="icon-bar"></span>
|
||||||
<span class="caret"></span>
|
<span class="icon-bar"></span>
|
||||||
</a>
|
<span class="icon-bar"></span>
|
||||||
<ul class="dropdown-menu">
|
</button>
|
||||||
<li {% if request.path == "/rules" %} class="active"{% endif %}><a href="/rules">Rules</a></li>
|
<a class="navbar-brand" href="/">{{ config.SITE_NAME }}</a>
|
||||||
<li {% if request.path == "/help" %} class="active"{% endif %}><a href="/help">Help</a></li>
|
</div>
|
||||||
</ul>
|
{% set search_username = (user.username + ("'" if user.username[-1] == 's' else "'s")) if user_page else None %}
|
||||||
</li>
|
{% set search_placeholder = 'Search {} torrents...'.format(search_username) if user_page else 'Search...' %}
|
||||||
<li><a href="{% if rss_filter %}{{ url_for('home', page='rss', **rss_filter) }}{% else %}{{ url_for('home', page='rss') }}{% endif %}">RSS</a></li>
|
<div id="navbar" class="navbar-collapse collapse">
|
||||||
{% if config.SITE_FLAVOR == 'nyaa' %}
|
<ul class="nav navbar-nav">
|
||||||
<li><a href="https://sukebei.nyaa.si/">Fap</a></li>
|
<li {% if request.path == "/upload" %} class="active"{% endif %}><a href="/upload">Upload</a></li>
|
||||||
{% elif config.SITE_FLAVOR == 'sukebei' %}
|
<li class="dropdown">
|
||||||
<li><a href="https://nyaa.si/">Fun</a></li>
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
|
||||||
{% endif %}
|
About
|
||||||
{% if g.user.is_admin %}
|
<span class="caret"></span>
|
||||||
<li><a href="{{ url_for('view_reports') }}">Reports</a></li>
|
</a>
|
||||||
{% endif %}
|
<ul class="dropdown-menu">
|
||||||
</ul>
|
<li {% if request.path == "/rules" %} class="active"{% endif %}><a href="/rules">Rules</a></li>
|
||||||
|
<li {% if request.path == "/help" %} class="active"{% endif %}><a href="/help">Help</a></li>
|
||||||
<ul class="nav navbar-nav navbar-right">
|
</ul>
|
||||||
{% if g.user %}
|
</li>
|
||||||
<li class="dropdown">
|
<li><a href="{% if rss_filter %}{{ url_for('home', page='rss', **rss_filter) }}{% else %}{{ url_for('home', page='rss') }}{% endif %}">RSS</a></li>
|
||||||
<a href="#" class="dropdown-toggle visible-lg visible-sm visible-xs" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
|
{% if config.SITE_FLAVOR == 'nyaa' %}
|
||||||
<i class="fa fa-user fa-fw"></i>
|
<li><a href="https://sukebei.nyaa.si/">Fap</a></li>
|
||||||
{{g.user.username}}
|
{% elif config.SITE_FLAVOR == 'sukebei' %}
|
||||||
<span class="caret"></span>
|
<li><a href="https://nyaa.si/">Fun</a></li>
|
||||||
</a>
|
{% endif %}
|
||||||
<a href="#" class="dropdown-toggle hidden-lg hidden-sm hidden-xs" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
|
{% if g.user.is_moderator %}
|
||||||
<i class="fa fa-user fa-fw"></i>
|
<li><a href="{{ url_for('view_reports') }}">Reports</a> </li>
|
||||||
<span class="caret"></span>
|
{% endif %}
|
||||||
</a>
|
</ul>
|
||||||
<ul class="dropdown-menu">
|
|
||||||
<li class="hidden-lg hidden-sm hidden-xs">
|
<ul class="nav navbar-nav navbar-right">
|
||||||
<a><i class="fa fa-user fa-fw"></i>Logged in as {{ g.user.username }}</a>
|
{% if g.user %}
|
||||||
</li>
|
<li class="dropdown">
|
||||||
<li class="hidden-lg hidden-sm hidden-xs divider" role="separator">
|
<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>
|
||||||
</li>
|
{{ g.user.username }}
|
||||||
<li>
|
<span class="caret"></span>
|
||||||
<a href="{{ url_for('view_user', user_name=g.user.username) }}">
|
</a>
|
||||||
<i class="fa fa-user fa-fw"></i>
|
<a href="#" class="dropdown-toggle hidden-lg hidden-sm hidden-xs" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
|
||||||
Torrents
|
<i class="fa fa-user fa-fw"></i>
|
||||||
</a>
|
<span class="caret"></span>
|
||||||
</li>
|
</a>
|
||||||
<li>
|
<ul class="dropdown-menu">
|
||||||
<a href="/profile">
|
<li class="hidden-lg hidden-sm hidden-xs">
|
||||||
<i class="fa fa-gear fa-fw"></i>
|
<a><i class="fa fa-user fa-fw"></i>Logged in as {{ g.user.username }}</a>
|
||||||
Profile
|
</li>
|
||||||
</a>
|
<li class="hidden-lg hidden-sm hidden-xs divider" role="separator">
|
||||||
</li>
|
|
||||||
<li>
|
</li>
|
||||||
<a href="/logout">
|
<li>
|
||||||
<i class="fa fa-times fa-fw"></i>
|
<a href="{{ url_for('view_user', user_name=g.user.username) }}">
|
||||||
Logout
|
<i class="fa fa-user fa-fw"></i>
|
||||||
</a>
|
Torrents
|
||||||
</li>
|
</a>
|
||||||
</ul>
|
</li>
|
||||||
</li>
|
<li>
|
||||||
{% else %}
|
<a href="/profile">
|
||||||
<li class="dropdown">
|
<i class="fa fa-gear fa-fw"></i>
|
||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
|
Profile
|
||||||
<i class="fa fa-user-times fa-fw"></i>
|
</a>
|
||||||
Guest
|
</li>
|
||||||
<span class="caret"></span>
|
<li>
|
||||||
</a>
|
<a href="/logout">
|
||||||
<ul class="dropdown-menu">
|
<i class="fa fa-times fa-fw"></i>
|
||||||
<li>
|
Logout
|
||||||
<a href="/login">
|
</a>
|
||||||
<i class="fa fa-sign-in fa-fw"></i>
|
</li>
|
||||||
Login
|
</ul>
|
||||||
</a>
|
</li>
|
||||||
</li>
|
{% else %}
|
||||||
<li>
|
<li class="dropdown">
|
||||||
<a href="/register">
|
<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-pencil fa-fw"></i>
|
<i class="fa fa-user fa-fw"></i>
|
||||||
Register
|
Guest
|
||||||
</a>
|
<span class="caret"></span>
|
||||||
</li>
|
</a>
|
||||||
</ul>
|
<a href="#" class="dropdown-toggle hidden-lg hidden-sm hidden-xs" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
|
||||||
</li>
|
<i class="fa fa-user fa-fw"></i>
|
||||||
{% endif %}
|
<span class="caret"></span>
|
||||||
</ul>
|
</a>
|
||||||
{% if user_page %}
|
<ul class="dropdown-menu">
|
||||||
<form class="navbar-form navbar-right form" action="{{ url_for('view_user', user_name=user.username) }}" method="get">
|
<li>
|
||||||
{% else %}
|
<a href="/login">
|
||||||
<form class="navbar-form navbar-right form" action="/" method="get">
|
<i class="fa fa-sign-in fa-fw"></i>
|
||||||
{% endif %}
|
Login
|
||||||
<div class="input-group search-container">
|
</a>
|
||||||
<input type="text" class="form-control search-bar" name="q" placeholder="Search..." value="{{ search["term"] if search is defined else '' }}">
|
</li>
|
||||||
<div class="input-group-btn nav-filter" id="navFilter-criteria">
|
<li>
|
||||||
<select class="selectpicker show-tick" title="Filter" data-width="120px" name="f">
|
<a href="/register">
|
||||||
<option value="0" title="No filter" {% if search is defined and search["quality_filter"] == "0" %}selected{% else %}selected{% endif %}>No filter</option>
|
<i class="fa fa-pencil fa-fw"></i>
|
||||||
<option value="1" title="No remakes" {% if search is defined and search["quality_filter"] == "1" %}selected{% endif %}>No remakes</option>
|
Register
|
||||||
<option value="2" title="Trusted only" {% if search is defined and search["quality_filter"] == "2" %}selected{% endif %}>Trusted only</option>
|
</a>
|
||||||
</select>
|
</li>
|
||||||
</div>
|
</ul>
|
||||||
<div class="input-group-btn nav-filter" id="navFilter-category">
|
</li>
|
||||||
{% set nyaa_cats = [('1_0', 'Anime', 'Anime'),
|
{% endif %}
|
||||||
('1_1', '- Anime Music Video', 'Anime - AMV'),
|
</ul>
|
||||||
('1_2', '- English-translated', 'Anime - English'),
|
{% set nyaa_cats = [('1_0', 'Anime', 'Anime'),
|
||||||
('1_3', '- Non-English-translated', 'Anime - Non-English'),
|
('1_1', '- Anime Music Video', 'Anime - AMV'),
|
||||||
('1_4', '- Raw', 'Anime - Raw'),
|
('1_2', '- English-translated', 'Anime - English'),
|
||||||
('2_0', 'Audio', 'Audio'),
|
('1_3', '- Non-English-translated', 'Anime - Non-English'),
|
||||||
('2_1', '- Lossless', 'Audio - Lossless'),
|
('1_4', '- Raw', 'Anime - Raw'),
|
||||||
('2_2', '- Lossy', 'Audio - Lossy'),
|
('2_0', 'Audio', 'Audio'),
|
||||||
('3_0', 'Literature', 'Literature'),
|
('2_1', '- Lossless', 'Audio - Lossless'),
|
||||||
('3_1', '- English-translated', 'Literature - English'),
|
('2_2', '- Lossy', 'Audio - Lossy'),
|
||||||
('3_2', '- Non-English-translated', 'Literature - Non-English'),
|
('3_0', 'Literature', 'Literature'),
|
||||||
('3_3', '- Raw', 'Literature - Raw'),
|
('3_1', '- English-translated', 'Literature - English'),
|
||||||
('4_0', 'Live Action', 'Live Action'),
|
('3_2', '- Non-English-translated', 'Literature - Non-English'),
|
||||||
('4_1', '- English-translated', 'Live Action - English'),
|
('3_3', '- Raw', 'Literature - Raw'),
|
||||||
('4_2', '- Idol/Promotional Video', 'Live Action - Idol/PV'),
|
('4_0', 'Live Action', 'Live Action'),
|
||||||
('4_3', '- Non-English-translated', 'Live Action - Non-English'),
|
('4_1', '- English-translated', 'Live Action - English'),
|
||||||
('4_4', '- Raw', 'Live Action - Raw'),
|
('4_2', '- Idol/Promotional Video', 'Live Action - Idol/PV'),
|
||||||
('5_0', 'Pictures', 'Pictures'),
|
('4_3', '- Non-English-translated', 'Live Action - Non-English'),
|
||||||
('5_1', '- Graphics', 'Pictures - Graphics'),
|
('4_4', '- Raw', 'Live Action - Raw'),
|
||||||
('5_2', '- Photos', 'Pictures - Photos'),
|
('5_0', 'Pictures', 'Pictures'),
|
||||||
('6_0', 'Software', 'Software'),
|
('5_1', '- Graphics', 'Pictures - Graphics'),
|
||||||
('6_1', '- Applications', 'Software - Apps'),
|
('5_2', '- Photos', 'Pictures - Photos'),
|
||||||
('6_2', '- Games', 'Software - Games')] %}
|
('6_0', 'Software', 'Software'),
|
||||||
{% set suke_cats = [('1_0', 'Art', 'Art'),
|
('6_1', '- Applications', 'Software - Apps'),
|
||||||
('1_1', '- Anime', 'Art - Anime'),
|
('6_2', '- Games', 'Software - Games')]
|
||||||
('1_2', '- Doujinshi', 'Art - Doujinshi'),
|
%}
|
||||||
('1_3', '- Games', 'Art - Games'),
|
|
||||||
('1_4', '- Manga', 'Art - Manga'),
|
{% set suke_cats = [('1_0', 'Art', 'Art'),
|
||||||
('1_5', '- Pictures', 'Art - Pictures'),
|
('1_1', '- Anime', 'Art - Anime'),
|
||||||
('2_0', 'Real Life', 'Real Life'),
|
('1_2', '- Doujinshi', 'Art - Doujinshi'),
|
||||||
('2_1', '- Photobooks and Pictures', 'Real Life - Pictures'),
|
('1_3', '- Games', 'Art - Games'),
|
||||||
('2_2', '- Videos', 'Real Life - Videos')] %}
|
('1_4', '- Manga', 'Art - Manga'),
|
||||||
{% if config.SITE_FLAVOR == 'nyaa' %}
|
('1_5', '- Pictures', 'Art - Pictures'),
|
||||||
{% set used_cats = nyaa_cats %}
|
('2_0', 'Real Life', 'Real Life'),
|
||||||
{% elif config.SITE_FLAVOR == 'sukebei' %}
|
('2_1', '- Photobooks and Pictures', 'Real Life - Pictures'),
|
||||||
{% set used_cats = suke_cats %}
|
('2_2', '- Videos', 'Real Life - Videos')]
|
||||||
{% endif %}
|
%}
|
||||||
<select class="selectpicker show-tick" title="Category" data-width="170px" name="c">
|
|
||||||
<option value="0_0" title="All categories" {% if search is defined and search["category"] == "0_0" %}selected{% else %}selected{% endif %}>
|
{% if config.SITE_FLAVOR == 'nyaa' %}
|
||||||
All categories
|
{% set used_cats = nyaa_cats %}
|
||||||
</option>
|
{% elif config.SITE_FLAVOR == 'sukebei' %}
|
||||||
{% for cat_id, cat_name, cat_title in used_cats %}
|
{% set used_cats = suke_cats %}
|
||||||
<option value="{{ cat_id }}" title="{{ cat_title }}" {% if search is defined and search.category == cat_id %}selected{% endif %}>
|
{% endif %}
|
||||||
{{ cat_name }}
|
|
||||||
</option>
|
<div class="search-container visible-xs visible-sm">
|
||||||
{% endfor %}
|
{# The mobile menu #}
|
||||||
</select>
|
{% if user_page %}
|
||||||
</div>
|
<form class="navbar-form navbar-right form" action="{{ url_for('view_user', user_name=user.username) }}" method="get">
|
||||||
<div class="input-group-btn search-btn">
|
{% else %}
|
||||||
<button class="btn btn-primary" type="submit">
|
<form class="navbar-form navbar-right form" action="/" method="get">
|
||||||
<i class="fa fa-search fa-fw"></i>
|
{% endif %}
|
||||||
</button>
|
|
||||||
</div>
|
<input type="text" class="form-control" name="q" placeholder="{{ search_placeholder }}" value="{{ search["term"] if search is defined else '' }}">
|
||||||
</div>
|
<br>
|
||||||
</form>
|
|
||||||
</div><!--/.nav-collapse -->
|
<select class="form-control" title="Filter" data-width="120px" name="f">
|
||||||
</div>
|
<option value="0" title="No filter" {% if search is defined and search["quality_filter"] == "0" %}selected{% else %}selected{% endif %}>No filter</option>
|
||||||
</nav>
|
<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>
|
||||||
<div class="container">
|
</select>
|
||||||
{% include "flashes.html" %}
|
|
||||||
|
<br>
|
||||||
{% block body %}{% endblock %}
|
|
||||||
</div> <!-- /container -->
|
<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 %}>
|
||||||
<footer style="text-align: center;">
|
All categories
|
||||||
<p>Dark Mode: <a href="#" id="themeToggle">Toggle</a></p>
|
</option>
|
||||||
{% if config.COMMIT_HASH %}
|
{% for cat_id, cat_name, cat_title in used_cats %}
|
||||||
<p>Commit: <a href="https://github.com/nyaadevs/nyaa/tree/{{ config.COMMIT_HASH }}">{{ config.COMMIT_HASH[:7] }}</a></p>
|
<option value="{{ cat_id }}" title="{{ cat_title }}" {% if search is defined and search.category == cat_id %}selected{% endif %}>
|
||||||
{% endif %}
|
{{ cat_name }}
|
||||||
</footer>
|
</option>
|
||||||
</body>
|
{% endfor %}
|
||||||
</html>
|
</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>
|
||||||
|
{% 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>
|
||||||
|
</div>
|
||||||
|
<div class="input-group-btn search-btn">
|
||||||
|
<button class="btn btn-primary" type="submit">
|
||||||
|
<i class="fa fa-search fa-fw"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div><!--/.nav-collapse -->
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
{% include "flashes.html" %}
|
||||||
|
|
||||||
|
{% block body %}{% endblock %}
|
||||||
|
</div> <!-- /container -->
|
||||||
|
|
||||||
|
<footer style="text-align: center;">
|
||||||
|
<p>Dark Mode: <a href="#" id="themeToggle">Toggle</a></p>
|
||||||
|
{% if config.COMMIT_HASH %}
|
||||||
|
<p>Commit: <a href="https://github.com/nyaadevs/nyaa/tree/{{ config.COMMIT_HASH }}">{{ config.COMMIT_HASH[:7] }}</a></p>
|
||||||
|
{% endif %}
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
{% extends "layout.html" %}
|
{% extends "layout.html" %}
|
||||||
{% block title %}Login :: {{ config.SITE_NAME }}{% endblock %}
|
{% block title %}Login :: {{ config.SITE_NAME }}{% endblock %}
|
||||||
|
{% block metatags %}
|
||||||
|
<meta property="og:description" content="Log in to {{ config.SITE_NAME }}!">
|
||||||
|
{% endblock %}
|
||||||
{% block body %}
|
{% block body %}
|
||||||
{% from "_formhelpers.html" import render_field %}
|
{% from "_formhelpers.html" import render_field %}
|
||||||
|
|
||||||
|
|
|
@ -3,17 +3,17 @@
|
||||||
{% block body %}
|
{% block body %}
|
||||||
{% from "_formhelpers.html" import render_field %}
|
{% 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="row" style="margin-bottom: 20px;">
|
||||||
<div class="col-sm-4 avatar" style="display: none;">
|
<div class="col-sm-2" style="max-width: 150px;">
|
||||||
<!-- TO BE IMPLEMENTED -->
|
<img class="avatar" src="{{ g.user.gravatar_url() }}">
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-8">
|
<div class="col-sm-10">
|
||||||
<dl class="row" style="margin: 20px 0 15px 0;">
|
<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-2">User ID:</dt><dd class="col-sm-10">{{ g.user.id }}</dd>
|
||||||
<dt class="col-sm-3">User Class:</dt><dd class="col-sm-9">{{ level }}</dd>
|
<dt class="col-sm-2">User Class:</dt><dd class="col-sm-10">{{ g.user.userlevel_str }}</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 Created on:</dt><dd class="col-sm-10">{{ g.user.created_time }}</dd>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -59,7 +59,7 @@
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="form-group col-md-4">
|
<div class="form-group col-md-4">
|
||||||
<label class="control-label" for="current_email">Current Email</label>
|
<label class="control-label" for="current_email">Current Email</label>
|
||||||
<div>{{email}}</div>
|
<div>{{ g.user.email }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
{% extends "layout.html" %}
|
{% extends "layout.html" %}
|
||||||
{% block title %}Register :: {{ config.SITE_NAME }}{% endblock %}
|
{% block title %}Register :: {{ config.SITE_NAME }}{% endblock %}
|
||||||
|
{% block metatags %}
|
||||||
|
<meta property="og:description" content="Register to {{ config.SITE_NAME }}!">
|
||||||
|
{% endblock %}
|
||||||
{% block body %}
|
{% block body %}
|
||||||
{% from "_formhelpers.html" import render_field %}
|
{% 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>
|
<channel>
|
||||||
<title>{{ config.SITE_NAME }} Torrent File RSS</title>
|
<title>{{ config.SITE_NAME }} Torrent File RSS</title>
|
||||||
<description>RSS Feed for {{ term }}</description>
|
<description>RSS Feed for {{ term }}</description>
|
||||||
|
@ -12,15 +12,15 @@
|
||||||
{% if torrent.has_torrent and not magnet_links %}
|
{% if torrent.has_torrent and not magnet_links %}
|
||||||
<link>{{ url_for('download_torrent', torrent_id=torrent.meta.id, _external=True) }}</link>
|
<link>{{ url_for('download_torrent', torrent_id=torrent.meta.id, _external=True) }}</link>
|
||||||
{% else %}
|
{% 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 %}
|
{% endif %}
|
||||||
<guid isPermaLink="true">{{ url_for('view_torrent', torrent_id=torrent.meta.id, _external=True) }}</guid>
|
<guid isPermaLink="true">{{ url_for('view_torrent', torrent_id=torrent.meta.id, _external=True) }}</guid>
|
||||||
<pubDate>{{ torrent.created_time|rfc822_es }}</pubDate>
|
<pubDate>{{ torrent.created_time|rfc822_es }}</pubDate>
|
||||||
|
|
||||||
<seeders> {{- torrent.seed_count }}</seeders>
|
<nyaa:seeders> {{- torrent.seed_count }}</nyaa:seeders>
|
||||||
<leechers> {{- torrent.leech_count }}</leechers>
|
<nyaa:leechers> {{- torrent.leech_count }}</nyaa:leechers>
|
||||||
<downloads>{{- torrent.download_count }}</downloads>
|
<nyaa:downloads>{{- torrent.download_count }}</nyaa:downloads>
|
||||||
<infoHash> {{- torrent.info_hash }}</infoHash>
|
<nyaa:infoHash> {{- torrent.info_hash }}</nyaa:infoHash>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% if torrent.has_torrent and not magnet_links %}
|
{% if torrent.has_torrent and not magnet_links %}
|
||||||
<link>{{ url_for('download_torrent', torrent_id=torrent.id, _external=True) }}</link>
|
<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>
|
<guid isPermaLink="true">{{ url_for('view_torrent', torrent_id=torrent.id, _external=True) }}</guid>
|
||||||
<pubDate>{{ torrent.created_time|rfc822 }}</pubDate>
|
<pubDate>{{ torrent.created_time|rfc822 }}</pubDate>
|
||||||
|
|
||||||
<seeders> {{- torrent.stats.seed_count }}</seeders>
|
<nyaa:seeders> {{- torrent.stats.seed_count }}</nyaa:seeders>
|
||||||
<leechers> {{- torrent.stats.leech_count }}</leechers>
|
<nyaa:leechers> {{- torrent.stats.leech_count }}</nyaa:leechers>
|
||||||
<downloads>{{- torrent.stats.download_count }}</downloads>
|
<nyaa:downloads>{{- torrent.stats.download_count }}</nyaa:downloads>
|
||||||
<infoHash> {{- torrent.info_hash_as_hex }}</infoHash>
|
<nyaa:infoHash> {{- torrent.info_hash_as_hex }}</nyaa:infoHash>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% set cat_id = use_elastic and ((torrent.main_category_id|string) + '_' + (torrent.sub_category_id|string)) or torrent.sub_category.id_as_string %}
|
{% 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>
|
<nyaa:categoryId>{{- cat_id }}</nyaa:categoryId>
|
||||||
<category> {{- category_name(cat_id) }}</category>
|
<nyaa:category> {{- category_name(cat_id) }}</nyaa:category>
|
||||||
<size> {{- torrent.filesize | filesizeformat(True) }}</size>
|
<nyaa:size> {{- torrent.filesize | filesizeformat(True) }}</nyaa:size>
|
||||||
</item>
|
</item>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</channel>
|
</channel>
|
||||||
|
|
|
@ -8,6 +8,15 @@
|
||||||
{{ caller() }}
|
{{ caller() }}
|
||||||
</th>
|
</th>
|
||||||
{% endmacro %}
|
{% 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) %}
|
{% if (use_elastic and torrent_query.hits.total > 0) or (torrent_query.items) %}
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-bordered table-hover table-striped torrent-list">
|
<table class="table table-bordered table-hover table-striped torrent-list">
|
||||||
|
@ -56,7 +65,7 @@
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="/?c={{ cat_id }}" title="{{ torrent.main_category.name }} - {{ torrent.sub_category.name }}">
|
<a href="/?c={{ cat_id }}" title="{{ torrent.main_category.name }} - {{ torrent.sub_category.name }}">
|
||||||
{% endif %}
|
{% 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>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
{% if use_elastic %}
|
{% if use_elastic %}
|
||||||
|
@ -67,7 +76,7 @@
|
||||||
<td style="white-space: nowrap;text-align: center;">
|
<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 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 %}
|
{% 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 %}
|
{% else %}
|
||||||
<a href="{{ torrent.magnet_uri }}"><i class="fa fa-fw fa-magnet"></i></a>
|
<a href="{{ torrent.magnet_uri }}"><i class="fa fa-fw fa-magnet"></i></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -76,7 +85,7 @@
|
||||||
{% if use_elastic %}
|
{% if use_elastic %}
|
||||||
<td class="text-center" data-timestamp="{{ torrent.created_time | utc_time }}">{{ torrent.created_time | display_time }}</td>
|
<td class="text-center" data-timestamp="{{ torrent.created_time | utc_time }}">{{ torrent.created_time | display_time }}</td>
|
||||||
{% else %}
|
{% else %}
|
||||||
<td class="text-center" data-timestamp="{{ torrent.created_utc_timestamp|int }}">{{ torrent.created_time.strftime('%Y-%m-%d %H:%M') }}</td>
|
<td class="text-center" data-timestamp="{{ torrent.created_utc_timestamp | int }}">{{ torrent.created_time.strftime('%Y-%m-%d %H:%M') }}</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if config.ENABLE_SHOW_STATS %}
|
{% if config.ENABLE_SHOW_STATS %}
|
||||||
|
@ -99,7 +108,7 @@
|
||||||
<h3>No results found</h3>
|
<h3>No results found</h3>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<center>
|
<div class="center">
|
||||||
{% if use_elastic %}
|
{% if use_elastic %}
|
||||||
{{ pagination.info }}
|
{{ pagination.info }}
|
||||||
{{ pagination.links }}
|
{{ pagination.links }}
|
||||||
|
@ -107,4 +116,4 @@
|
||||||
{% from "bootstrap/pagination.html" import render_pagination %}
|
{% from "bootstrap/pagination.html" import render_pagination %}
|
||||||
{{ render_pagination(torrent_query) }}
|
{{ render_pagination(torrent_query) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</center>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
{% extends "layout.html" %}
|
{% extends "layout.html" %}
|
||||||
{% block title %}Upload Torrent :: {{ config.SITE_NAME }}{% endblock %}
|
{% 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 %}
|
{% block body %}
|
||||||
{% from "_formhelpers.html" import render_field %}
|
{% from "_formhelpers.html" import render_field %}
|
||||||
{% from "_formhelpers.html" import render_upload %}
|
{% from "_formhelpers.html" import render_upload %}
|
||||||
|
@ -7,69 +10,97 @@
|
||||||
|
|
||||||
<h1>Upload Torrent</h1>
|
<h1>Upload Torrent</h1>
|
||||||
|
|
||||||
{% if not user %}
|
{% if not g.user %}
|
||||||
<p>You are not logged in, and are uploading anonymously.</p>
|
<p>You are not logged in, and are uploading anonymously.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
<div id="upload-drop-zone"><span>Drop here!</span></div>
|
<div id="upload-drop-zone"><span>Drop here!</span></div>
|
||||||
<form method="POST" enctype="multipart/form-data">
|
<form method="POST" enctype="multipart/form-data">
|
||||||
{% if config.ENFORCE_MAIN_ANNOUNCE_URL %}<p><strong>Important:</strong> Please include <kbd>{{config.MAIN_ANNOUNCE_URL}}</kbd> in your trackers</p>{% endif %}
|
{{ 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="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-10">
|
||||||
{{ render_upload(form.torrent_file, accept=".torrent") }}
|
{{ render_upload(upload_form.torrent_file, accept=".torrent") }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<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>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
{{ render_field(form.category, class_='form-control')}}
|
{{ render_field(upload_form.category, class_='form-control')}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row"></div>
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="row form-group">
|
<div class="row form-group">
|
||||||
<div class="col-md-6">
|
<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>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="control-label">Torrent flags</label>
|
<label class="control-label">Torrent flags</label><br>
|
||||||
<div>
|
|
||||||
<label class="btn btn-primary" title="Upload torrent anonymously (don't display your username)">
|
<div class="btn-group" data-toggle="buttons">
|
||||||
{{ form.is_anonymous(disabled=(False if user else ""), checked=(False if user else "")) }}
|
<label class="btn btn-default {% if not g.user %}active disabled{% endif %}" title="Upload torrent anonymously (don't display your username)">
|
||||||
Anonymous
|
{{ upload_form.is_anonymous(disabled=(False if g.user else ""), checked=(False if g.user else "")) }}
|
||||||
</label>
|
{% if not g.user %}<span class="glyphicon glyphicon-ban-circle"></span>{% endif %}
|
||||||
<label class="btn btn-default" style="background-color: darkgray; border-color: #ccc;" title="Hide torrent from listing">
|
{% if g.user %}<span class="glyphicon glyphicon-check"></span>{% endif %}
|
||||||
{{ form.is_hidden }}
|
{% if g.user %}<span class="glyphicon glyphicon-unchecked"></span>{% endif %}
|
||||||
Hidden
|
Anonymous
|
||||||
</label>
|
</label>
|
||||||
<label class="btn btn-danger" title="This torrent is derived from another release">
|
<label class="btn btn-grey" title="Hide torrent from listing">
|
||||||
{{ form.is_remake }}
|
{{ upload_form.is_hidden }}
|
||||||
Remake
|
<span class="glyphicon glyphicon-check"></span>
|
||||||
</label>
|
<span class="glyphicon glyphicon-unchecked"></span>
|
||||||
<label class="btn btn-primary" title="This torrent is a complete batch (eg. season)">
|
Hidden
|
||||||
{{ form.is_complete }}
|
</label>
|
||||||
Complete
|
</div>
|
||||||
</label>
|
<div class="hidden-xl hidden-lg"><br></div>
|
||||||
{% if user.is_trusted %}
|
<div class="btn-group" data-toggle="buttons">
|
||||||
<label class="btn btn-success" title="Mark torrent trusted">
|
|
||||||
{{ form.is_trusted(checked="") }}
|
<label class="btn btn-danger" title="This torrent is derived from another release">
|
||||||
Trusted
|
{{ upload_form.is_remake }}
|
||||||
</label>
|
<span class="glyphicon glyphicon-check"></span>
|
||||||
{% endif %}
|
<span class="glyphicon glyphicon-unchecked"></span>
|
||||||
|
Remake
|
||||||
|
</label>
|
||||||
|
<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 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>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
{{ render_markdown_editor(form.description, field_name='description') }}
|
{{ render_markdown_editor(upload_form.description, field_name='description') }}
|
||||||
</div>
|
</div>
|
||||||
</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="row">
|
||||||
<div class="form-group col-md-6">
|
<div class="form-group col-md-6">
|
||||||
<input type="submit" value="Upload" class="btn btn-primary">
|
<input type="submit" value="Upload" class="btn btn-primary">
|
||||||
|
|
|
@ -1,34 +1,56 @@
|
||||||
{% extends "layout.html" %}
|
{% extends "layout.html" %}
|
||||||
{% block title %}{{ user.username }} :: {{ config.SITE_NAME }}{% endblock %}
|
{% 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 %}
|
{% block body %}
|
||||||
{% from "_formhelpers.html" import render_menu_with_button %}
|
{% from "_formhelpers.html" import render_menu_with_button %}
|
||||||
|
|
||||||
{% if superadmin %}
|
{% if g.user and g.user.is_moderator %}
|
||||||
<h2>User Information</h2><br>
|
<h2>User Information</h2><br>
|
||||||
<dl class="dl-horizontal">
|
<div class="row" style="margin-bottom: 20px;">
|
||||||
<dt>User ID:</dt>
|
<div class="col-sm-2" style="max-width: 150px;">
|
||||||
<dd>{{user.id}}</dd>
|
<img class="avatar" src="{{ user.gravatar_url() }}">
|
||||||
<dt>Account created on:</dt>
|
</div>
|
||||||
<dd>{{user.created_time}}</dd>
|
<div class="col-sm-10">
|
||||||
<dt>Email address:</dt>
|
<dl class="dl-horizontal">
|
||||||
<dd>{{user.email}}</dd>
|
<dt>User ID:</dt>
|
||||||
<dt>User class:</dt>
|
<dd>{{ user.id }}</dd>
|
||||||
<dd>{{level}}</dd><br>
|
<dt>Account created on:</dt>
|
||||||
</dl>
|
<dd>{{ user.created_time }}</dd>
|
||||||
|
<dt>Email address:</dt>
|
||||||
|
<dd>{{ user.email }}</dd>
|
||||||
|
<dt>User class:</dt>
|
||||||
|
<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 method="POST">
|
||||||
{{ form.csrf_token }}
|
{{ admin_form.csrf_token }}
|
||||||
|
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
{{ render_menu_with_button(form.user_class)}}
|
{{ render_menu_with_button(admin_form.user_class) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<br>
|
<br>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<h3>
|
<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>
|
</h3>
|
||||||
|
|
||||||
{% include "search_results.html" %}
|
{% include "search_results.html" %}
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
{% extends "layout.html" %}
|
{% extends "layout.html" %}
|
||||||
{% block title %}{{ torrent.display_name }} :: {{ config.SITE_NAME }}{% endblock %}
|
{% 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 %}
|
{% block body %}
|
||||||
{% from "_formhelpers.html" import render_field %}
|
{% 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 %}">
|
<div class="panel panel-{% if torrent.deleted %}deleted{% elif torrent.remake %}danger{% elif torrent.trusted %}success{% else %}default{% endif %}">
|
||||||
|
@ -25,12 +29,15 @@
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-1">Submitter:</div>
|
<div class="col-md-1">Submitter:</div>
|
||||||
<div class="col-md-5">
|
<div class="col-md-5">
|
||||||
{% set user_url = torrent.user and url_for('view_user', user_name=torrent.user.username) %}
|
{% set user_url = torrent.user and url_for('view_user', user_name=torrent.user.username) %}
|
||||||
{%- if not torrent.anonymous and torrent.user -%}
|
{%- 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 -%}
|
{%- 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 -%}
|
{%- endif -%}
|
||||||
|
{% if g.user and g.user.is_superadmin and torrent.uploader_ip %}
|
||||||
|
({{ torrent.uploader_ip_string }})
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-1">Seeders:</div>
|
<div class="col-md-1">Seeders:</div>
|
||||||
|
@ -56,16 +63,20 @@
|
||||||
<div class="col-md-1">File size:</div>
|
<div class="col-md-1">File size:</div>
|
||||||
<div class="col-md-5">{{ torrent.filesize | filesizeformat(True) }}</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 class="col-md-5">{% if config.ENABLE_SHOW_STATS %}{{ torrent.stats.download_count }}{% else %}Coming soon{% endif %}</div>
|
||||||
</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>
|
</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 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">
|
<button type="button" class="btn btn-danger pull-right" data-toggle="modal" data-target="#reportModal">
|
||||||
Report
|
Report
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -82,42 +93,32 @@
|
||||||
|
|
||||||
{% if files and files.__len__() <= config.MAX_FILES_VIEW %}
|
{% if files and files.__len__() <= config.MAX_FILES_VIEW %}
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading panel-heading-collapse">
|
<div class="panel-heading">
|
||||||
<h3 class="panel-title">
|
<h3 class="panel-title">File list</h3>
|
||||||
<div class="row">
|
|
||||||
<a class="collapsed col-md-12" data-target="#collapseFileList" data-toggle="collapse" style="color:inherit;text-decoration:none;">File list</a>
|
|
||||||
</div>
|
|
||||||
</h3>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel-collapse collapse" id="collapseFileList">
|
<div class="torrent-file-list panel-body">
|
||||||
<table class="table table-bordered table-hover table-striped">
|
<ul>
|
||||||
<thead>
|
{% for key, value in files.items() recursive -%}
|
||||||
<th style="width:auto;">Path</th>
|
{% if value is iterable %}
|
||||||
<th style="width:auto;">Size</th>
|
{% set pre_expanded = not loop.depth0 and value.items()|length <= 20 %}
|
||||||
</thead>
|
<li>
|
||||||
<tbody>
|
<a href="" class="folder"><i class="fa fa-folder{% if pre_expanded %}-open{% endif %}"></i>{{ key }}</a>
|
||||||
{%- for key, value in files.items() recursive %}
|
<ul{% if pre_expanded %} data-show="yes"{% endif %}>{{ '\n' + loop(value.items()) }}
|
||||||
<tr>
|
</ul>
|
||||||
{%- if value is iterable %}
|
</li>
|
||||||
<td colspan="2" {% if loop.depth0 is greaterthan 0 %}style="padding-left: {{ loop.depth0 * 20 }}px"{% endif %}>
|
{% else %}
|
||||||
<i class="glyphicon glyphicon-folder-open"></i> <b>{{ key }}</b></td>
|
<li><i class="fa fa-file"></i>{{ key }} <span class="file-size">({{ value | filesizeformat(True) }})</span></a></li>
|
||||||
{{ loop(value.items()) }}
|
{% endif %}
|
||||||
{%- else %}
|
{% endfor %}
|
||||||
<td{% if loop.depth0 is greaterthan 0 %} style="padding-left: {{ loop.depth0 * 20 }}px"{% endif %}>
|
</ul>
|
||||||
<i class="glyphicon glyphicon-file"></i> {{ key }}</td>
|
|
||||||
<td class="col-md-2">{{ value | filesizeformat(True) }}</td>
|
|
||||||
{%- endif %}
|
|
||||||
</tr>
|
|
||||||
{%- endfor %}
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% elif files %}
|
{% elif files %}
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading panel-heading-collapse">
|
<div class="panel-heading panel-heading-collapse">
|
||||||
<h3 class="panel-title">
|
<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>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -125,13 +126,66 @@
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading panel-heading-collapse">
|
<div class="panel-heading panel-heading-collapse">
|
||||||
<h3 class="panel-title">
|
<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>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="modal fade" id="reportModal" tabindex="-1" role="dialog" aria-labelledby="reportModalLabel">
|
|
||||||
|
<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-dialog" role="document">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
|
@ -153,7 +207,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
var target = document.getElementById('torrent-description');
|
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()
|
USED_TRACKERS = OrderedSet()
|
||||||
|
|
||||||
|
# Limit the amount of trackers added into .torrent files
|
||||||
|
MAX_TRACKERS = 5
|
||||||
|
|
||||||
|
|
||||||
def read_trackers_from_file(file_object):
|
def read_trackers_from_file(file_object):
|
||||||
USED_TRACKERS.clear()
|
USED_TRACKERS.clear()
|
||||||
|
@ -55,7 +58,7 @@ def get_trackers(torrent):
|
||||||
return list(trackers)
|
return list(trackers)
|
||||||
|
|
||||||
|
|
||||||
def get_trackers_magnet():
|
def get_default_trackers():
|
||||||
trackers = OrderedSet()
|
trackers = OrderedSet()
|
||||||
|
|
||||||
# Our main one first
|
# Our main one first
|
||||||
|
@ -70,8 +73,9 @@ def get_trackers_magnet():
|
||||||
|
|
||||||
|
|
||||||
def create_magnet(torrent, max_trackers=5, trackers=None):
|
def create_magnet(torrent, max_trackers=5, trackers=None):
|
||||||
|
# Unless specified, we just use default trackers
|
||||||
if trackers is None:
|
if trackers is None:
|
||||||
trackers = get_trackers_magnet()
|
trackers = get_default_trackers()
|
||||||
|
|
||||||
magnet_parts = [
|
magnet_parts = [
|
||||||
('dn', torrent.display_name)
|
('dn', torrent.display_name)
|
||||||
|
@ -85,10 +89,10 @@ def create_magnet(torrent, max_trackers=5, trackers=None):
|
||||||
|
|
||||||
# For processing ES links
|
# For processing ES links
|
||||||
@app.context_processor
|
@app.context_processor
|
||||||
def create_magnet_from_info():
|
def create_magnet_from_es_info():
|
||||||
def _create_magnet_from_info(display_name, info_hash, max_trackers=5, trackers=None):
|
def _create_magnet_from_es_info(display_name, info_hash, max_trackers=5, trackers=None):
|
||||||
if trackers is None:
|
if trackers is None:
|
||||||
trackers = get_trackers_magnet()
|
trackers = get_default_trackers()
|
||||||
|
|
||||||
magnet_parts = [
|
magnet_parts = [
|
||||||
('dn', display_name)
|
('dn', display_name)
|
||||||
|
@ -98,7 +102,7 @@ def create_magnet_from_info():
|
||||||
|
|
||||||
b32_info_hash = base64.b32encode(bytes.fromhex(info_hash)).decode('utf-8')
|
b32_info_hash = base64.b32encode(bytes.fromhex(info_hash)).decode('utf-8')
|
||||||
return 'magnet:?xt=urn:btih:' + b32_info_hash + '&' + urlencode(magnet_parts)
|
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):
|
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]
|
metadata_base['announce'] = trackers[0]
|
||||||
if len(trackers) > 1:
|
if len(trackers) > 1:
|
||||||
# Yes, it's a list of lists with a single element inside.
|
# 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
|
return metadata_base
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
alembic==0.9.2
|
||||||
appdirs==1.4.3
|
appdirs==1.4.3
|
||||||
argon2-cffi==16.3.0
|
argon2-cffi==16.3.0
|
||||||
autopep8==1.3.1
|
autopep8==1.3.1
|
||||||
|
@ -5,9 +6,14 @@ blinker==1.4
|
||||||
cffi==1.10.0
|
cffi==1.10.0
|
||||||
click==6.7
|
click==6.7
|
||||||
dominate==2.3.1
|
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-Assets==0.12
|
||||||
Flask-DebugToolbar==0.10.1
|
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-SQLAlchemy==2.2
|
||||||
Flask-WTF==0.14.2
|
Flask-WTF==0.14.2
|
||||||
gevent==1.2.1
|
gevent==1.2.1
|
||||||
|
@ -15,27 +21,29 @@ greenlet==0.4.12
|
||||||
itsdangerous==0.24
|
itsdangerous==0.24
|
||||||
Jinja2==2.9.6
|
Jinja2==2.9.6
|
||||||
libsass==0.12.3
|
libsass==0.12.3
|
||||||
|
Mako==1.0.6
|
||||||
MarkupSafe==1.0
|
MarkupSafe==1.0
|
||||||
|
mysql-replication==0.13
|
||||||
mysqlclient==1.3.10
|
mysqlclient==1.3.10
|
||||||
orderedset==2.0
|
orderedset==2.0
|
||||||
packaging==16.8
|
packaging==16.8
|
||||||
passlib==1.7.1
|
passlib==1.7.1
|
||||||
|
progressbar2==3.20.0
|
||||||
pycodestyle==2.3.1
|
pycodestyle==2.3.1
|
||||||
pycparser==2.17
|
pycparser==2.17
|
||||||
|
PyMySQL==0.7.11
|
||||||
pyparsing==2.2.0
|
pyparsing==2.2.0
|
||||||
|
python-dateutil==2.6.0
|
||||||
|
python-editor==1.0.3
|
||||||
|
python-utils==2.1.0
|
||||||
six==1.10.0
|
six==1.10.0
|
||||||
SQLAlchemy==1.1.9
|
SQLAlchemy==1.1.10
|
||||||
SQLAlchemy-FullText-Search==0.2.3
|
SQLAlchemy-FullText-Search==0.2.3
|
||||||
SQLAlchemy-Utils==0.32.14
|
SQLAlchemy-Utils==0.32.14
|
||||||
|
statsd==3.2.1
|
||||||
|
urllib3==1.21.1
|
||||||
uWSGI==2.0.15
|
uWSGI==2.0.15
|
||||||
visitor==0.1.3
|
visitor==0.1.3
|
||||||
webassets==0.12.1
|
webassets==0.12.1
|
||||||
Werkzeug==0.12.1
|
Werkzeug==0.12.2
|
||||||
WTForms==2.1
|
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.opentrackr.org:1337/announce
|
||||||
|
udp://tracker.coppersurfer.tk:6969/announce
|
||||||
|
udp://tracker.doko.moe:6969
|
||||||
udp://tracker.zer0day.to:1337/announce
|
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'
|
SUKEBEI_HOST = 'https://sukebei.nyaa.si'
|
||||||
|
|
||||||
API_BASE = '/api'
|
API_BASE = '/api'
|
||||||
API_UPLOAD = API_BASE + '/v2/upload'
|
API_UPLOAD = API_BASE + '/upload'
|
||||||
|
|
||||||
NYAA_CATS = '''1_1 - Anime - AMV
|
NYAA_CATS = '''1_1 - Anime - AMV
|
||||||
1_2 - Anime - English
|
1_2 - Anime - English
|
||||||
|
|
Loading…
Reference in a new issue