Merge branch 'master' into reports

This commit is contained in:
nyaazi
2017-05-29 16:20:48 +03:00
23 changed files with 958 additions and 337 deletions

View File

@@ -22,6 +22,15 @@ if app.config['DEBUG']:
app.config['DEBUG_TB_INTERCEPT_REDIRECTS'] = False
toolbar = DebugToolbarExtension(app)
app.logger.setLevel(logging.DEBUG)
# Forbid caching
@app.after_request
def forbid_cache(request):
request.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate, max-age=0'
request.headers['Pragma'] = 'no-cache'
request.headers['Expires'] = '0'
return request
else:
app.logger.setLevel(logging.WARNING)

View File

@@ -43,7 +43,7 @@ _username_validator = Regexp(
class LoginForm(FlaskForm):
username = StringField('Username or email address', [DataRequired(), _username_validator])
username = StringField('Username or email address', [DataRequired()])
password = PasswordField('Password', [DataRequired()])

View File

@@ -3,10 +3,13 @@ from enum import Enum, IntEnum
from datetime import datetime, timezone
from nyaa import app, db
from nyaa.torrents import create_magnet
from sqlalchemy import func, ForeignKeyConstraint, Index
from sqlalchemy.ext import declarative
from sqlalchemy_utils import ChoiceType, EmailType, PasswordType
from werkzeug.security import generate_password_hash, check_password_hash
from sqlalchemy_fulltext import FullText
from werkzeug.security import generate_password_hash, check_password_hash
from ipaddress import ip_address
import re
@@ -17,7 +20,6 @@ from hashlib import md5
if app.config['USE_MYSQL']:
from sqlalchemy.dialects import mysql
BinaryType = mysql.BINARY
DescriptionTextType = mysql.TEXT
MediumBlobType = mysql.MEDIUMBLOB
@@ -32,10 +34,36 @@ else:
COL_UTF8MB4_BIN = None
COL_ASCII_GENERAL_CI = 'NOCASE'
# For property timestamps
UTC_EPOCH = datetime.utcfromtimestamp(0)
class DeclarativeHelperBase(object):
''' This class eases our nyaa-sukebei shenanigans by automatically adjusting
__tablename__ and providing class methods for renaming references. '''
# See http://docs.sqlalchemy.org/en/latest/orm/extensions/declarative/api.html
__tablename_base__ = None
__flavor__ = None
@classmethod
def _table_prefix_string(cls):
return cls.__flavor__.lower() + '_'
@classmethod
def _table_prefix(cls, table_name):
return cls._table_prefix_string() + table_name
@classmethod
def _flavor_prefix(cls, table_name):
return cls.__flavor__ + table_name
@declarative.declared_attr
def __tablename__(cls):
return cls._table_prefix(cls.__tablename_base__)
class TorrentFlags(IntEnum):
NONE = 0
ANONYMOUS = 1
@@ -46,16 +74,13 @@ class TorrentFlags(IntEnum):
DELETED = 32
DB_TABLE_PREFIX = app.config['TABLE_PREFIX']
class Torrent(db.Model):
__tablename__ = DB_TABLE_PREFIX + 'torrents'
class TorrentBase(DeclarativeHelperBase):
__tablename_base__ = 'torrents'
id = db.Column(db.Integer, primary_key=True)
info_hash = db.Column(BinaryType(length=20), unique=True, nullable=False, index=True)
display_name = db.Column(
db.String(length=255, collation=COL_UTF8_GENERAL_CI), nullable=False, index=True)
display_name = db.Column(db.String(length=255, collation=COL_UTF8_GENERAL_CI),
nullable=False, index=True)
torrent_name = db.Column(db.String(length=255), nullable=False)
information = db.Column(db.String(length=255), nullable=False)
description = db.Column(DescriptionTextType(collation=COL_UTF8MB4_BIN), nullable=False)
@@ -63,50 +88,95 @@ class Torrent(db.Model):
filesize = db.Column(db.BIGINT, default=0, nullable=False, index=True)
encoding = db.Column(db.String(length=32), nullable=False)
flags = db.Column(db.Integer, default=0, nullable=False, index=True)
uploader_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
@declarative.declared_attr
def uploader_id(cls):
# Even though this is same for both tables, declarative requires this
return 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)
comment_count = db.Column(db.Integer, default=0, nullable=False, index=True)
created_time = db.Column(db.DateTime(timezone=False), default=datetime.utcnow, nullable=False)
updated_time = db.Column(db.DateTime(timezone=False),
default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
updated_time = db.Column(db.DateTime(timezone=False), default=datetime.utcnow,
onupdate=datetime.utcnow, nullable=False)
@declarative.declared_attr
def main_category_id(cls):
fk = db.ForeignKey(cls._table_prefix('main_categories.id'))
return db.Column(db.Integer, fk, nullable=False)
main_category_id = db.Column(db.Integer, db.ForeignKey(
DB_TABLE_PREFIX + 'main_categories.id'), nullable=False)
sub_category_id = db.Column(db.Integer, nullable=False)
redirect = db.Column(db.Integer, db.ForeignKey(
DB_TABLE_PREFIX + 'torrents.id'), nullable=True)
__table_args__ = (
Index('uploader_flag_idx', 'uploader_id', 'flags'),
ForeignKeyConstraint(
['main_category_id', 'sub_category_id'],
[DB_TABLE_PREFIX + 'sub_categories.main_category_id',
DB_TABLE_PREFIX + 'sub_categories.id']
), {}
)
@declarative.declared_attr
def redirect(cls):
fk = db.ForeignKey(cls._table_prefix('torrents.id'))
return db.Column(db.Integer, fk, nullable=True)
user = db.relationship('User', uselist=False, back_populates='torrents')
main_category = db.relationship('MainCategory', uselist=False,
back_populates='torrents', lazy="joined")
sub_category = db.relationship('SubCategory', uselist=False, backref='torrents', lazy="joined",
primaryjoin=(
"and_(SubCategory.id == foreign(Torrent.sub_category_id), "
"SubCategory.main_category_id == Torrent.main_category_id)"))
info = db.relationship('TorrentInfo', uselist=False,
cascade="all, delete-orphan", back_populates='torrent')
filelist = db.relationship('TorrentFilelist', uselist=False,
@declarative.declared_attr
def __table_args__(cls):
return (
Index(cls._table_prefix('uploader_flag_idx'), 'uploader_id', 'flags'),
ForeignKeyConstraint(
['main_category_id', 'sub_category_id'],
[cls._table_prefix('sub_categories.main_category_id'),
cls._table_prefix('sub_categories.id')]
), {}
)
@declarative.declared_attr
def user(cls):
return db.relationship('User', uselist=False, back_populates=cls._table_prefix('torrents'))
@declarative.declared_attr
def main_category(cls):
return db.relationship(cls._flavor_prefix('MainCategory'), uselist=False,
back_populates='torrents', lazy="joined")
@declarative.declared_attr
def sub_category(cls):
join_sql = ("and_({0}SubCategory.id == foreign({0}Torrent.sub_category_id), "
"{0}SubCategory.main_category_id == {0}Torrent.main_category_id)")
return db.relationship(cls._flavor_prefix('SubCategory'), uselist=False,
backref='torrents', lazy="joined",
primaryjoin=join_sql.format(cls.__flavor__))
@declarative.declared_attr
def info(cls):
return db.relationship(cls._flavor_prefix('TorrentInfo'), uselist=False,
cascade="all, delete-orphan", back_populates='torrent')
stats = db.relationship('Statistic', uselist=False,
cascade="all, delete-orphan", back_populates='torrent', lazy='joined')
trackers = db.relationship('TorrentTrackers', uselist=True, cascade="all, delete-orphan",
lazy='joined', order_by='TorrentTrackers.order')
comments = db.relationship('Comment', uselist=True,
@declarative.declared_attr
def filelist(cls):
return db.relationship(cls._flavor_prefix('TorrentFilelist'), uselist=False,
cascade="all, delete-orphan", back_populates='torrent')
@declarative.declared_attr
def stats(cls):
return db.relationship(cls._flavor_prefix('Statistic'), uselist=False,
cascade="all, delete-orphan", back_populates='torrent',
lazy='joined')
@declarative.declared_attr
def trackers(cls):
return db.relationship(cls._flavor_prefix('TorrentTrackers'), uselist=True,
cascade="all, delete-orphan", lazy='joined',
order_by=cls._flavor_prefix('TorrentTrackers.order'))
@declarative.declared_attr
def comments(cls):
return db.relationship(cls._flavor_prefix('Comment'), uselist=True,
cascade="all, delete-orphan")
def __repr__(self):
return '<{0} #{1.id} \'{1.display_name}\' {1.filesize}b>'.format(type(self).__name__, self)
def update_comment_count(self):
self.comment_count = Comment.query.filter_by(torrent_id=self.id).count()
return self.comment_count
@property
def created_utc_timestamp(self):
''' Returns a UTC POSIX timestamp, as seconds '''
@@ -149,6 +219,8 @@ class Torrent(db.Model):
if self.uploader_ip:
return str(ip_address(self.uploader_ip))
# Flag getters and setters below
@property
def anonymous(self):
return self.flags & TorrentFlags.ANONYMOUS
@@ -197,6 +269,8 @@ class Torrent(db.Model):
def complete(self, value):
self.flags = (self.flags & ~TorrentFlags.COMPLETE) | (value and TorrentFlags.COMPLETE)
# Class methods
@classmethod
def by_id(cls, id):
return cls.query.get(id)
@@ -211,44 +285,57 @@ class Torrent(db.Model):
return cls.by_info_hash(info_hash_bytes)
class TorrentNameSearch(FullText, Torrent):
__fulltext_columns__ = ('display_name',)
class TorrentFilelistBase(DeclarativeHelperBase):
__tablename_base__ = 'torrents_filelist'
class TorrentFilelist(db.Model):
__tablename__ = DB_TABLE_PREFIX + 'torrents_filelist'
__table_args__ = {'mysql_row_format': 'COMPRESSED'}
torrent_id = db.Column(db.Integer, db.ForeignKey(
DB_TABLE_PREFIX + 'torrents.id', ondelete="CASCADE"), primary_key=True)
@declarative.declared_attr
def torrent_id(cls):
fk = db.ForeignKey(cls._table_prefix('torrents.id'), ondelete="CASCADE")
return db.Column(db.Integer, fk, primary_key=True)
filelist_blob = db.Column(MediumBlobType, nullable=True)
torrent = db.relationship('Torrent', uselist=False, back_populates='filelist')
@declarative.declared_attr
def torrent(cls):
return db.relationship(cls._flavor_prefix('Torrent'), uselist=False,
back_populates='filelist')
class TorrentInfo(db.Model):
__tablename__ = DB_TABLE_PREFIX + 'torrents_info'
class TorrentInfoBase(DeclarativeHelperBase):
__tablename_base__ = 'torrents_info'
__table_args__ = {'mysql_row_format': 'COMPRESSED'}
torrent_id = db.Column(db.Integer, db.ForeignKey(
DB_TABLE_PREFIX + 'torrents.id', ondelete="CASCADE"), primary_key=True)
@declarative.declared_attr
def torrent_id(cls):
return db.Column(db.Integer, db.ForeignKey(
cls._table_prefix('torrents.id'), ondelete="CASCADE"), primary_key=True)
info_dict = db.Column(MediumBlobType, nullable=True)
torrent = db.relationship('Torrent', uselist=False, back_populates='info')
@declarative.declared_attr
def torrent(cls):
return db.relationship(cls._flavor_prefix('Torrent'), uselist=False, back_populates='info')
class Statistic(db.Model):
__tablename__ = DB_TABLE_PREFIX + 'statistics'
class StatisticBase(DeclarativeHelperBase):
__tablename_base__ = 'statistics'
torrent_id = db.Column(db.Integer, db.ForeignKey(
DB_TABLE_PREFIX + 'torrents.id', ondelete="CASCADE"), primary_key=True)
@declarative.declared_attr
def torrent_id(cls):
fk = db.ForeignKey(cls._table_prefix('torrents.id'), ondelete="CASCADE")
return db.Column(db.Integer, fk, primary_key=True)
seed_count = db.Column(db.Integer, default=0, nullable=False, index=True)
leech_count = db.Column(db.Integer, default=0, nullable=False, index=True)
download_count = db.Column(db.Integer, default=0, nullable=False, index=True)
last_updated = db.Column(db.DateTime(timezone=False))
torrent = db.relationship('Torrent', uselist=False, back_populates='stats')
@declarative.declared_attr
def torrent(cls):
return db.relationship(cls._flavor_prefix('Torrent'), uselist=False,
back_populates='stats')
class Trackers(db.Model):
@@ -264,30 +351,43 @@ class Trackers(db.Model):
return cls.query.filter_by(uri=uri).first()
class TorrentTrackers(db.Model):
__tablename__ = DB_TABLE_PREFIX + 'torrent_trackers'
class TorrentTrackersBase(DeclarativeHelperBase):
__tablename_base__ = 'torrent_trackers'
@declarative.declared_attr
def torrent_id(cls):
fk = db.ForeignKey(cls._table_prefix('torrents.id'), ondelete="CASCADE")
return db.Column(db.Integer, fk, primary_key=True)
@declarative.declared_attr
def tracker_id(cls):
fk = db.ForeignKey('trackers.id', ondelete="CASCADE")
return db.Column(db.Integer, fk, primary_key=True)
torrent_id = db.Column(db.Integer, db.ForeignKey(
DB_TABLE_PREFIX + 'torrents.id', ondelete="CASCADE"), primary_key=True)
tracker_id = db.Column(db.Integer, db.ForeignKey(
'trackers.id', ondelete="CASCADE"), primary_key=True)
order = db.Column(db.Integer, nullable=False, index=True)
tracker = db.relationship('Trackers', uselist=False, lazy='joined')
@declarative.declared_attr
def tracker(cls):
return db.relationship('Trackers', uselist=False, lazy='joined')
@classmethod
def by_torrent_id(cls, torrent_id):
return cls.query.filter_by(torrent_id=torrent_id).order_by(cls.order.desc())
class MainCategory(db.Model):
__tablename__ = DB_TABLE_PREFIX + 'main_categories'
class MainCategoryBase(DeclarativeHelperBase):
__tablename_base__ = 'main_categories'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(length=64), nullable=False)
sub_categories = db.relationship('SubCategory', back_populates='main_category')
torrents = db.relationship('Torrent', back_populates='main_category')
@declarative.declared_attr
def sub_categories(cls):
return db.relationship(cls._flavor_prefix('SubCategory'), back_populates='main_category')
@declarative.declared_attr
def torrents(cls):
return db.relationship(cls._flavor_prefix('Torrent'), back_populates='main_category')
def get_category_ids(self):
return (self.id, 0)
@@ -301,18 +401,22 @@ class MainCategory(db.Model):
return cls.query.get(id)
class SubCategory(db.Model):
__tablename__ = DB_TABLE_PREFIX + 'sub_categories'
class SubCategoryBase(DeclarativeHelperBase):
__tablename_base__ = 'sub_categories'
id = db.Column(db.Integer, primary_key=True)
main_category_id = db.Column(db.Integer, db.ForeignKey(
DB_TABLE_PREFIX + 'main_categories.id'), primary_key=True)
@declarative.declared_attr
def main_category_id(cls):
fk = db.ForeignKey(cls._table_prefix('main_categories.id'))
return db.Column(db.Integer, fk, primary_key=True)
name = db.Column(db.String(length=64), nullable=False)
main_category = db.relationship('MainCategory', uselist=False, back_populates='sub_categories')
# torrents = db.relationship('Torrent', back_populates='sub_category'),
# primaryjoin="and_(Torrent.sub_category_id == foreign(SubCategory.id), "
# "Torrent.main_category_id == SubCategory.main_category_id)")
@declarative.declared_attr
def main_category(cls):
return db.relationship(cls._flavor_prefix('MainCategory'), uselist=False,
back_populates='sub_categories')
def get_category_ids(self):
return (self.main_category_id, self.id)
@@ -326,17 +430,27 @@ class SubCategory(db.Model):
return cls.query.get((sub_cat_id, main_cat_id))
class Comment(db.Model):
__tablename__ = DB_TABLE_PREFIX + 'comments'
class CommentBase(DeclarativeHelperBase):
__tablename_base__ = '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'))
@declarative.declared_attr
def torrent_id(cls):
return db.Column(db.Integer, db.ForeignKey(
cls._table_prefix('torrents.id'), ondelete='CASCADE'), nullable=False)
@declarative.declared_attr
def user_id(cls):
return 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")
@declarative.declared_attr
def user(cls):
return db.relationship('User', uselist=False,
back_populates=cls._table_prefix('comments'), lazy="joined")
def __repr__(self):
return '<Comment %r>' % self.id
@@ -376,9 +490,11 @@ class User(db.Model):
last_login_date = db.Column(db.DateTime(timezone=False), default=None, nullable=True)
last_login_ip = db.Column(db.Binary(length=16), default=None, nullable=True)
torrents = db.relationship('Torrent', back_populates='user', lazy='dynamic')
comments = db.relationship('Comment', back_populates='user', lazy='dynamic')
# session = db.relationship('Session', uselist=False, back_populates='user')
nyaa_torrents = db.relationship('NyaaTorrent', back_populates='user', lazy='dynamic')
nyaa_comments = db.relationship('NyaaComment', back_populates='user', lazy='dynamic')
sukebei_torrents = db.relationship('SukebeiTorrent', back_populates='user', lazy='dynamic')
sukebei_comments = db.relationship('SukebeiComment', back_populates='user', lazy='dynamic')
def __init__(self, username, email, password):
self.username = username
@@ -470,20 +586,30 @@ class ReportStatus(IntEnum):
INVALID = 2
class Report(db.Model):
__tablename__ = DB_TABLE_PREFIX + 'reports'
class ReportBase(DeclarativeHelperBase):
__tablename_base__ = 'reports'
id = db.Column(db.Integer, primary_key=True)
torrent_id = db.Column(db.Integer, db.ForeignKey(
DB_TABLE_PREFIX + 'torrents.id', ondelete='CASCADE'))
user_id = db.Column(db.Integer, db.ForeignKey(
'users.id'))
created_time = db.Column(db.DateTime(timezone=False), default=datetime.utcnow)
reason = db.Column(db.String(length=255), nullable=False)
status = db.Column(ChoiceType(ReportStatus, impl=db.Integer()), nullable=False)
user = db.relationship('User', uselist=False, lazy="joined")
torrent = db.relationship('Torrent', uselist=False, lazy="joined")
@declarative.declared_attr
def torrent_id(cls):
return db.Column(db.Integer, db.ForeignKey(
cls._table_prefix('torrents.id'), ondelete='CASCADE'), nullable=False)
@declarative.declared_attr
def user_id(cls):
return db.Column(db.Integer, db.ForeignKey('users.id'))
@declarative.declared_attr
def user(cls):
return db.relationship('User', uselist=False, lazy="joined")
@declarative.declared_attr
def torrent(cls):
return db.relationship(cls._flavor_prefix('Torrent'), uselist=False, lazy="joined")
def __init__(self, torrent_id, user_id, reason):
self.torrent_id = torrent_id
@@ -512,12 +638,130 @@ class Report(db.Model):
def remove_reviewed(cls, id):
return cls.query.filter(cls.torrent_id == id, cls.status == 0).delete()
# class Session(db.Model):
# __tablename__ = 'sessions'
#
# session_id = db.Column(db.Integer, primary_key=True)
# user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
# login_ip = db.Column(db.Binary(length=16), nullable=True)
# login_date = db.Column(db.DateTime(timezone=False), nullable=True)
#
# user = db.relationship('User', back_populates='session')
# Actually declare our site-specific classes
# Torrent
class NyaaTorrent(TorrentBase, db.Model):
__flavor__ = 'Nyaa'
class SukebeiTorrent(TorrentBase, db.Model):
__flavor__ = 'Sukebei'
# Fulltext models for MySQL
if app.config['USE_MYSQL']:
class NyaaTorrentNameSearch(FullText, NyaaTorrent):
__fulltext_columns__ = ('display_name',)
__table_args__ = {'extend_existing': True}
class SukebeiTorrentNameSearch(FullText, SukebeiTorrent):
__fulltext_columns__ = ('display_name',)
__table_args__ = {'extend_existing': True}
else:
# Bogus classes for Sqlite
class NyaaTorrentNameSearch(object):
pass
class SukebeiTorrentNameSearch(object):
pass
# TorrentFilelist
class NyaaTorrentFilelist(TorrentFilelistBase, db.Model):
__flavor__ = 'Nyaa'
class SukebeiTorrentFilelist(TorrentFilelistBase, db.Model):
__flavor__ = 'Sukebei'
# TorrentInfo
class NyaaTorrentInfo(TorrentInfoBase, db.Model):
__flavor__ = 'Nyaa'
class SukebeiTorrentInfo(TorrentInfoBase, db.Model):
__flavor__ = 'Sukebei'
# Statistic
class NyaaStatistic(StatisticBase, db.Model):
__flavor__ = 'Nyaa'
class SukebeiStatistic(StatisticBase, db.Model):
__flavor__ = 'Sukebei'
# TorrentTrackers
class NyaaTorrentTrackers(TorrentTrackersBase, db.Model):
__flavor__ = 'Nyaa'
class SukebeiTorrentTrackers(TorrentTrackersBase, db.Model):
__flavor__ = 'Sukebei'
# MainCategory
class NyaaMainCategory(MainCategoryBase, db.Model):
__flavor__ = 'Nyaa'
class SukebeiMainCategory(MainCategoryBase, db.Model):
__flavor__ = 'Sukebei'
# SubCategory
class NyaaSubCategory(SubCategoryBase, db.Model):
__flavor__ = 'Nyaa'
class SukebeiSubCategory(SubCategoryBase, db.Model):
__flavor__ = 'Sukebei'
# Comment
class NyaaComment(CommentBase, db.Model):
__flavor__ = 'Nyaa'
class SukebeiComment(CommentBase, db.Model):
__flavor__ = 'Sukebei'
# Report
class NyaaReport(ReportBase, db.Model):
__flavor__ = 'Nyaa'
class SukebeiReport(ReportBase, db.Model):
__flavor__ = 'Sukebei'
# Choose our defaults for models.Torrent etc
if app.config['SITE_FLAVOR'] == 'nyaa':
Torrent = NyaaTorrent
TorrentFilelist = NyaaTorrentFilelist
TorrentInfo = NyaaTorrentInfo
Statistic = NyaaStatistic
TorrentTrackers = NyaaTorrentTrackers
MainCategory = NyaaMainCategory
SubCategory = NyaaSubCategory
Comment = NyaaComment
Report = NyaaReport
TorrentNameSearch = NyaaTorrentNameSearch
elif app.config['SITE_FLAVOR'] == 'sukebei':
Torrent = SukebeiTorrent
TorrentFilelist = SukebeiTorrentFilelist
TorrentInfo = SukebeiTorrentInfo
Statistic = SukebeiStatistic
TorrentTrackers = SukebeiTorrentTrackers
MainCategory = SukebeiMainCategory
SubCategory = SukebeiSubCategory
Comment = SukebeiComment
Report = SukebeiReport
TorrentNameSearch = SukebeiTorrentNameSearch

View File

@@ -51,7 +51,7 @@ def redirect_url():
@app.template_global()
def static_cachebuster(static_filename):
def static_cachebuster(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__)
@@ -60,19 +60,18 @@ def static_cachebuster(static_filename):
if app.debug:
# Do not bust cache on debug (helps debugging)
return static_filename
return flask.url_for('static', filename=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 filename not in _static_cache:
file_path = os.path.join(app.static_folder, filename)
file_mtime = None
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]
_static_cache[filename] = file_mtime
return flask.url_for('static', filename=filename, t=_static_cache[filename])
@app.template_global()
@@ -657,9 +656,10 @@ def view_torrent(torrent_id):
text=comment_text)
db.session.add(comment)
db.session.commit()
db.session.flush()
torrent_count = models.Comment.query.filter_by(torrent_id=torrent.id).count()
torrent_count = torrent.update_comment_count()
db.session.commit()
flask.flash('Comment successfully posted.', 'success')
@@ -687,6 +687,9 @@ def view_torrent(torrent_id):
def delete_comment(torrent_id, comment_id):
if not flask.g.user:
flask.abort(403)
torrent = models.Torrent.by_id(torrent_id)
if not torrent:
flask.abort(404)
comment = models.Comment.query.filter_by(id=comment_id).first()
if not comment:
@@ -696,6 +699,8 @@ def delete_comment(torrent_id, comment_id):
flask.abort(403)
db.session.delete(comment)
db.session.flush()
torrent.update_comment_count()
db.session.commit()
flask.flash('Comment successfully deleted.', 'success')

View File

@@ -25,6 +25,7 @@ def search_elastic(term='', user=None, sort='id', order='desc',
'id': 'id',
'size': 'filesize',
# 'name': 'display_name', # This is slow and buggy
'comments': 'comment_count',
'seeders': 'seed_count',
'leechers': 'leech_count',
'downloads': 'download_count'
@@ -190,6 +191,7 @@ def search_db(term='', user=None, sort='id', order='desc', category='0_0',
'size': models.Torrent.filesize,
# Disable this because we disabled this in search_elastic, for the sake of consistency:
# 'name': models.Torrent.display_name,
'comments': models.Torrent.comment_count,
'seeders': models.Statistic.seed_count,
'leechers': models.Statistic.leech_count,
'downloads': models.Statistic.download_count

View File

@@ -62,6 +62,30 @@ table.torrent-list tbody tr td a:visited {
color: #1d4568;
}
/* comments count */
table.torrent-list .hdr-comments {
border-left: hidden;
font-size: medium;
}
table.torrent-list .hdr-comments i {
margin-right: 6px;
}
table.torrent-list tbody .comments {
position: relative;
float: right;
border: 1px solid #d7d7d7;
border-radius: 3px;
color: #383838;
padding: 0 5px;
font-size: small;
background-color: #ffffff;
}
table.torrent-list tbody .comments i {
padding-right: 2px;
}
#torrent-description img {
max-width: 100%;
}

View File

@@ -6,9 +6,9 @@
<meta name="viewport" content="width=device-width">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link rel="shortcut icon" type="image/png" href="/static/favicon.png">
<link rel="icon" type="image/png" href="/static/favicon.png">
<link rel="mask-icon" href="/static/pinned-tab.svg" color="#3582F7">
<link rel="shortcut icon" type="image/png" href="{{ url_for('static', filename='favicon.png') }}">
<link rel="icon" type="image/png" href="{{ url_for('static', filename='favicon.png') }}">
<link rel="mask-icon" href="{{ url_for('static', filename='pinned-tab.svg') }}" color="#3582F7">
<link rel="alternate" type="application/rss+xml" href="{% if rss_filter %}{{ url_for('home', page='rss', _external=True, **rss_filter) }}{% else %}{{ url_for('home', page='rss', _external=True) }}{% endif %}" />
<meta property="og:site_name" content="{{ config.SITE_NAME }}">
@@ -25,10 +25,10 @@
make the navbar not look awful on tablets.
-->
{# These are extracted here for the dark mode toggle #}
{% set bootstrap_light = static_cachebuster('/static/css/bootstrap.min.css') %}
{% set bootstrap_dark = static_cachebuster('/static/css/bootstrap-dark.min.css') %}
{% set bootstrap_light = static_cachebuster('css/bootstrap.min.css') %}
{% set bootstrap_dark = static_cachebuster('css/bootstrap-dark.min.css') %}
<link href="{{ bootstrap_light }}" rel="stylesheet" id="bsThemeLink">
<link href="{{ static_cachebuster('/static/css/bootstrap-xl-mod.css') }}" rel="stylesheet">
<link href="{{ static_cachebuster('css/bootstrap-xl-mod.css') }}" rel="stylesheet">
<!--
This theme changer script needs to be inline and right under the above stylesheet link to prevent FOUC (Flash Of Unstyled Content)
Development version is commented out in static/js/main.js at the bottom of the file
@@ -38,15 +38,15 @@
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" integrity="sha256-eZrrJcwDc/3uDhsdt61sL2oOBY362qM3lon1gyExkL0=" crossorigin="anonymous" />
<!-- Custom styles for this template -->
<link href="{{ static_cachebuster('/static/css/main.css') }}" rel="stylesheet">
<link href="{{ static_cachebuster('css/main.css') }}" rel="stylesheet">
<!-- Core JavaScript -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js" integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha256-U5ZEeKfGNOja007MMD3YBI0A3OSZOQbeG6z2f2Y0hu8=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/commonmark/0.27.0/commonmark.min.js" integrity="sha256-10JreQhQG80GtKuzsioj0K46DlaB/CK/EG+NuG0q97E=" crossorigin="anonymous"></script>
<!-- Modified to not apply border-radius to selectpickers and stuff so our navbar looks cool -->
<script src="{{ static_cachebuster('/static/js/bootstrap-select.js') }}"></script>
<script src="{{ static_cachebuster('/static/js/main.js') }}"></script>
<script src="{{ static_cachebuster('js/bootstrap-select.js') }}"></script>
<script src="{{ static_cachebuster('js/main.js') }}"></script>
<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
<!--[if lt IE 9]>
@@ -65,21 +65,21 @@
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">{{ config.SITE_NAME }}</a>
<a class="navbar-brand" href="{{ url_for('home') }}">{{ config.SITE_NAME }}</a>
</div>
{% set search_username = (user.username + ("'" if user.username[-1] == 's' else "'s")) if user_page else None %}
{% set search_placeholder = 'Search {} torrents...'.format(search_username) if user_page else 'Search...' %}
<div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li {% if request.path == "/upload" %} class="active"{% endif %}><a href="/upload">Upload</a></li>
<li {% if request.path == url_for('upload') %}class="active"{% endif %}><a href="{{ url_for('upload') }}">Upload</a></li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
About
<span class="caret"></span>
</a>
<ul class="dropdown-menu">
<li {% if request.path == "/rules" %} class="active"{% endif %}><a href="/rules">Rules</a></li>
<li {% if request.path == "/help" %} class="active"{% endif %}><a href="/help">Help</a></li>
<li {% if request.path == url_for('site_rules') %}class="active"{% endif %}><a href="{{ url_for('site_rules') }}">Rules</a></li>
<li {% if request.path == url_for('site_help') %}class="active"{% endif %}><a href="{{ url_for('site_help') }}">Help</a></li>
</ul>
</li>
<li><a href="{% if rss_filter %}{{ url_for('home', page='rss', **rss_filter) }}{% else %}{{ url_for('home', page='rss') }}{% endif %}">RSS</a></li>
@@ -119,13 +119,13 @@
</a>
</li>
<li>
<a href="/profile">
<a href="{{ url_for('profile') }}">
<i class="fa fa-gear fa-fw"></i>
Profile
</a>
</li>
<li>
<a href="/logout">
<a href="{{ url_for('logout') }}">
<i class="fa fa-times fa-fw"></i>
Logout
</a>
@@ -145,13 +145,13 @@
</a>
<ul class="dropdown-menu">
<li>
<a href="/login">
<a href="{{ url_for('login') }}">
<i class="fa fa-sign-in fa-fw"></i>
Login
</a>
</li>
<li>
<a href="/register">
<a href="{{ url_for('register') }}">
<i class="fa fa-pencil fa-fw"></i>
Register
</a>
@@ -207,7 +207,7 @@
{% 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">
<form class="navbar-form navbar-right form" action="{{ url_for('home') }}" method="get">
{% endif %}
<input type="text" class="form-control" name="q" placeholder="{{ search_placeholder }}" value="{{ search["term"] if search is defined else '' }}">
@@ -243,7 +243,7 @@
{% 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">
<form class="navbar-form navbar-right form" action="{{ url_for('home') }}" 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 '' }}">

View File

@@ -12,7 +12,7 @@
{% 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>
<a href="{{ url_for('view_user', user_name=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 %}
@@ -28,6 +28,9 @@
{% call render_column_header("hdr-name", "width:auto;") %}
<div>Name</div>
{% endcall %}
{% call render_column_header("hdr-comments", "width:50px;", center_text=True, sort_key="comments", header_title="Comments") %}
<i class="fa fa-comments-o"></i>
{% endcall %}
{% call render_column_header("hdr-link", "width:70px;", center_text=True) %}
<div>Link</div>
{% endcall %}
@@ -44,12 +47,10 @@
{% endcall %}
{% call render_column_header("hdr-leechers", "width:50px;", center_text=True, sort_key="leechers", header_title="Leeches") %}
<i class="fa fa-arrow-down" aria-hidden="true"></i>
{% endcall %}
{% call render_column_header("hdr-downloads", "width:50px;", center_text=True, sort_key="downloads", header_title="Completed downloads") %}
<i class="fa fa-check" aria-hidden="true"></i>
{% endcall %}
{% endif %}
</tr>
</thead>
@@ -61,20 +62,29 @@
{% set icon_dir = config.SITE_FLAVOR %}
<td style="padding:0 4px;">
{% if use_elastic %}
<a href="/?c={{ cat_id }}" title="{{ category_name(cat_id) }}">
<a href="{{ url_for('home', c=cat_id) }}" title="{{ category_name(cat_id) }}">
{% else %}
<a href="/?c={{ cat_id }}" title="{{ torrent.main_category.name }} - {{ torrent.sub_category.name }}">
<a href="{{ url_for('home', c=cat_id) }}" title="{{ torrent.main_category.name }} - {{ torrent.sub_category.name }}">
{% endif %}
<img src="/static/img/icons/{{ icon_dir }}/{{ cat_id }}.png" alt="{{ category_name(cat_id) }}">
<img src="{{ url_for('static', filename='img/icons/%s/%s.png'|format(icon_dir, cat_id)) }}" alt="{{ category_name(cat_id) }}">
</a>
</td>
{% if use_elastic %}
<td><a href="{{ url_for('view_torrent', torrent_id=torrent.meta.id) }}" title="{{ torrent.display_name | escape }}">{%if "highlight" in torrent.meta %}{{ torrent.meta.highlight.display_name[0] | safe }}{% else %}{{torrent.display_name}}{%endif%}</a></td>
{% else %}
<td><a href="{{ url_for('view_torrent', torrent_id=torrent.id) }}" title="{{ torrent.display_name | escape }}">{{ torrent.display_name | escape }}</a></td>
{% endif %}
<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 %}
<td colspan="2">
{% set torrent_id = torrent.meta.id if use_elastic else torrent.id %}
{% set com_count = torrent.comment_count %}
{% if com_count %}
<a href="{{ url_for('view_torrent', torrent_id=torrent_id, _anchor='comments') }}" class="comments" title="{{ '{c} comment{s}'.format(c=com_count, s='s' if com_count > 1 else '') }}">
<i class="fa fa-comments-o"></i>{{ com_count -}}
</a>
{% endif %}
{% if use_elastic %}
<a href="{{ url_for('view_torrent', torrent_id=torrent_id) }}" title="{{ torrent.display_name | escape }}">{%if "highlight" in torrent.meta %}{{ torrent.meta.highlight.display_name[0] | safe }}{% else %}{{torrent.display_name}}{%endif%}</a>
{% else %}
<a href="{{ url_for('view_torrent', torrent_id=torrent_id) }}" title="{{ torrent.display_name | escape }}">{{ torrent.display_name | escape }}</a>
{% endif %}
</td>
<td class="text-center" style="white-space: nowrap;">
{% if torrent.has_torrent %}<a href="{{ url_for('download_torrent', torrent_id=torrent_id) }}"><i class="fa fa-fw fa-download"></i></a>{% endif %}
{% if use_elastic %}
<a href="{{ create_magnet_from_es_info(torrent.display_name, torrent.info_hash) }}"><i class="fa fa-fw fa-magnet"></i></a>
{% else %}

View File

@@ -10,7 +10,7 @@
<div class="panel-heading"{% if torrent.hidden %} style="background-color: darkgray;"{% endif %}>
<h3 class="panel-title">
{% if can_edit %}
<a href="{{ request.url }}/edit" title="Edit torrent"><i class="fa fa-fw fa-pencil"></i></a>
<a href="{{ url_for('edit_torrent', torrent_id=torrent.id) }}" title="Edit torrent"><i class="fa fa-fw fa-pencil"></i></a>
{% endif %}
{{ torrent.display_name }}
</h3>
@@ -132,8 +132,7 @@
</div>
{% endif %}
<div class="panel panel-default">
<div id="comments" class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">
Comments - {{ comments | length }}