diff --git a/db_create.py b/db_create.py index d881377..06a1783 100755 --- a/db_create.py +++ b/db_create.py @@ -1,35 +1,60 @@ +#!/usr/bin/env python3 import sys +import sqlalchemy from nyaa import app, db, models -# Create tables +NYAA_CATEGORIES = [ + ('Anime', ['Anime Music Video', 'English-translated', 'Non-English-translated', 'Raw']), + ('Audio', ['Lossless', 'Lossy']), + ('Literature', ['English-translated', 'Non-English-translated', 'Raw']), + ('Live Action', ['English-translated', 'Idol/Promotional Video', 'Non-English-translated', 'Raw']), + ('Pictures', ['Graphics', 'Photos']), + ('Software', ['Applications', 'Games']), +] -db.create_all() -# Insert categories and insert if it doesn't eixst -existing_cats = models.MainCategory.query.all() -if not existing_cats: - if app.config['SITE_FLAVOR'] == 'nyaa': - CATEGORIES = [ - ('Anime', ['Anime Music Video', 'English-translated', 'Non-English-translated', 'Raw']), - ('Audio', ['Lossless', 'Lossy']), - ('Literature', ['English-translated', 'Non-English-translated', 'Raw']), - ('Live Action', ['English-translated', 'Idol/Promotional Video', 'Non-English-translated', 'Raw']), - ('Pictures', ['Graphics', 'Photos']), - ('Software', ['Applications', 'Games']), - ] - elif app.config['SITE_FLAVOR'] == 'sukebei': - CATEGORIES = [ - ('Art', ['Anime', 'Doujinshi', 'Games', 'Manga', 'Pictures']), - ('Real Life', ['Photobooks / Pictures', 'Videos']), - ] - else: - CATEGORIES = [] +SUKEBEI_CATEGORIES = [ + ('Art', ['Anime', 'Doujinshi', 'Games', 'Manga', 'Pictures']), + ('Real Life', ['Photobooks / Pictures', 'Videos']), +] - for main_cat_name, sub_cat_names in CATEGORIES: - main_cat = models.MainCategory(name=main_cat_name) + +def add_categories(categories, main_class, sub_class): + for main_cat_name, sub_cat_names in categories: + main_cat = main_class(name=main_cat_name) for i, sub_cat_name in enumerate(sub_cat_names): # Composite keys can't autoincrement, set sub_cat id manually (1-index) - sub_cat = models.SubCategory(id=i+1, name=sub_cat_name, main_category=main_cat) + sub_cat = sub_class(id=i+1, name=sub_cat_name, main_category=main_cat) db.session.add(main_cat) + +if __name__ == '__main__': + # Test for the user table, assume db is empty if it's not created + database_empty = False + try: + models.User.query.first() + except (sqlalchemy.exc.ProgrammingError, sqlalchemy.exc.OperationalError): + database_empty = True + + + print('Creating all tables...') + db.create_all() + + + nyaa_category_test = models.NyaaMainCategory.query.first() + if not nyaa_category_test: + print('Adding Nyaa categories...') + add_categories(NYAA_CATEGORIES, models.NyaaMainCategory, models.NyaaSubCategory) + + sukebei_category_test = models.SukebeiMainCategory.query.first() + if not sukebei_category_test: + print('Adding Sukebei categories...') + add_categories(SUKEBEI_CATEGORIES, models.SukebeiMainCategory, models.SukebeiSubCategory) + db.session.commit() + + if database_empty: + print('Remember to run the following to mark the database up-to-date for Alembic:') + print('./db_migrate.py db stamp head') + # Technically we should be able to do this here, but when you have + # Flask-Migrate and Flask-SQA and everything... I didn't get it working. \ No newline at end of file diff --git a/db_migrate.py b/db_migrate.py index 8d4f8f0..1f480e7 100755 --- a/db_migrate.py +++ b/db_migrate.py @@ -1,4 +1,4 @@ -#!/usr/bin/python3 +#!/usr/bin/env python3 # -*- coding: utf-8 -*- from nyaa import app, db from flask_script import Manager diff --git a/migrations/versions/3001f79b7722_add_torrents.uploader_ip.py b/migrations/versions/3001f79b7722_add_torrents.uploader_ip.py index 152c440..03ed87c 100644 --- a/migrations/versions/3001f79b7722_add_torrents.uploader_ip.py +++ b/migrations/versions/3001f79b7722_add_torrents.uploader_ip.py @@ -11,20 +11,19 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. revision = '3001f79b7722' -down_revision = None +down_revision = '97ddefed1834' branch_labels = None depends_on = None +TABLE_PREFIXES = ('nyaa', 'sukebei') + 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 ### + for prefix in TABLE_PREFIXES: + op.add_column(prefix + '_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 ### + for prefix in TABLE_PREFIXES: + op.drop_column(prefix + '_torrents', 'uploader_ip') diff --git a/migrations/versions/97ddefed1834_initial_database_state.py b/migrations/versions/97ddefed1834_initial_database_state.py new file mode 100644 index 0000000..fcb1dcb --- /dev/null +++ b/migrations/versions/97ddefed1834_initial_database_state.py @@ -0,0 +1,166 @@ +"""Initial database state + +Revision ID: 97ddefed1834 +Revises: +Create Date: 2017-05-26 18:46:14.440040 + +""" +from alembic import op +import sqlalchemy as sa +import sqlalchemy_utils +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = '97ddefed1834' +down_revision = None +branch_labels = None +depends_on = None + +TABLE_PREFIXES = ('nyaa', 'sukebei') + +def upgrade(): + # Shared tables + op.create_table('users', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('username', sa.String(length=32, collation='ascii_general_ci'), nullable=False), + sa.Column('email', sqlalchemy_utils.types.email.EmailType(length=255), nullable=True), + + # These are actually PasswordType, UserStatusType and UserLevelType, + # but database-wise binary and integers are what's being used + sa.Column('password_hash', sa.Binary(length=255), nullable=False), + sa.Column('status', sa.Integer(), nullable=False), + sa.Column('level', sa.Integer(), nullable=False), + + sa.Column('created_time', sa.DateTime(), nullable=True), + sa.Column('last_login_date', sa.DateTime(), nullable=True), + sa.Column('last_login_ip', sa.Binary(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('email'), + sa.UniqueConstraint('username') + ) + + op.create_table('trackers', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('uri', sa.String(length=255, collation='utf8_general_ci'), nullable=False), + sa.Column('disabled', sa.Boolean(), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('uri') + ) + + # Nyaa and Sukebei + for prefix in TABLE_PREFIXES: + # Main categories + op.create_table(prefix + '_main_categories', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=64), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + # Sub categories + op.create_table(prefix + '_sub_categories', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('main_category_id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=64), nullable=False), + sa.ForeignKeyConstraint(['main_category_id'], [prefix + '_main_categories.id'], ), + sa.PrimaryKeyConstraint('id', 'main_category_id') + ) + # Main torrent table + op.create_table(prefix + '_torrents', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('info_hash', sa.BINARY(length=20), nullable=False), + sa.Column('display_name', sa.String(length=255, collation='utf8_general_ci'), nullable=False), + sa.Column('torrent_name', sa.String(length=255), nullable=False), + sa.Column('information', sa.String(length=255), nullable=False), + sa.Column('description', mysql.TEXT(collation='utf8mb4_bin'), nullable=False), + sa.Column('filesize', sa.BIGINT(), nullable=False), + sa.Column('encoding', sa.String(length=32), nullable=False), + sa.Column('flags', sa.Integer(), nullable=False), + sa.Column('uploader_id', sa.Integer(), nullable=True), + sa.Column('has_torrent', sa.Boolean(), nullable=False), + sa.Column('created_time', sa.DateTime(), nullable=False), + sa.Column('updated_time', sa.DateTime(), nullable=False), + sa.Column('main_category_id', sa.Integer(), nullable=False), + sa.Column('sub_category_id', sa.Integer(), nullable=False), + sa.Column('redirect', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['main_category_id', 'sub_category_id'], [prefix + '_sub_categories.main_category_id', prefix + '_sub_categories.id'], ), + sa.ForeignKeyConstraint(['main_category_id'], [prefix + '_main_categories.id'], ), + sa.ForeignKeyConstraint(['redirect'], [prefix + '_torrents.id'], ), + sa.ForeignKeyConstraint(['uploader_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_' + prefix + '_torrents_display_name'), prefix + '_torrents', ['display_name'], unique=False) + op.create_index(op.f('ix_' + prefix + '_torrents_filesize'), prefix + '_torrents', ['filesize'], unique=False) + op.create_index(op.f('ix_' + prefix + '_torrents_flags'), prefix + '_torrents', ['flags'], unique=False) + op.create_index(op.f('ix_' + prefix + '_torrents_info_hash'), prefix + '_torrents', ['info_hash'], unique=True) + op.create_index(prefix + '_uploader_flag_idx', prefix + '_torrents', ['uploader_id', 'flags'], unique=False) + + # Statistics for torrents + op.create_table(prefix + '_statistics', + sa.Column('torrent_id', sa.Integer(), nullable=False), + sa.Column('seed_count', sa.Integer(), nullable=False), + sa.Column('leech_count', sa.Integer(), nullable=False), + sa.Column('download_count', sa.Integer(), nullable=False), + sa.Column('last_updated', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['torrent_id'], [prefix + '_torrents.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('torrent_id') + ) + op.create_index(op.f('ix_' + prefix + '_statistics_download_count'), prefix + '_statistics', ['download_count'], unique=False) + op.create_index(op.f('ix_' + prefix + '_statistics_leech_count'), prefix + '_statistics', ['leech_count'], unique=False) + op.create_index(op.f('ix_' + prefix + '_statistics_seed_count'), prefix + '_statistics', ['seed_count'], unique=False) + + # Trackers relationships for torrents + op.create_table(prefix + '_torrent_trackers', + sa.Column('torrent_id', sa.Integer(), nullable=False), + sa.Column('tracker_id', sa.Integer(), nullable=False), + sa.Column('order', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['torrent_id'], [prefix + '_torrents.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['tracker_id'], ['trackers.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('torrent_id', 'tracker_id') + ) + op.create_index(op.f('ix_' + prefix + '_torrent_trackers_order'), prefix + '_torrent_trackers', ['order'], unique=False) + + # Torrent filelists + op.create_table(prefix + '_torrents_filelist', + sa.Column('torrent_id', sa.Integer(), nullable=False), + sa.Column('filelist_blob', mysql.MEDIUMBLOB(), nullable=True), + sa.ForeignKeyConstraint(['torrent_id'], [prefix + '_torrents.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('torrent_id'), + mysql_row_format='COMPRESSED' + ) + + # Torrent info_dicts + op.create_table(prefix + '_torrents_info', + sa.Column('torrent_id', sa.Integer(), nullable=False), + sa.Column('info_dict', mysql.MEDIUMBLOB(), nullable=True), + sa.ForeignKeyConstraint(['torrent_id'], [prefix + '_torrents.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('torrent_id'), + mysql_row_format='COMPRESSED' + ) + # ### end Alembic commands ### + + +def downgrade(): + # Note: this may fail. It's better to just drop all tables instead (or reset the database) + + # Nyaa and Sukebei + for prefix in TABLE_PREFIXES: + op.drop_table(prefix + '_torrents_info') + op.drop_table(prefix + '_torrents_filelist') + op.drop_index(op.f('ix_' + prefix + '_torrent_trackers_order'), table_name=prefix + '_torrent_trackers') + op.drop_table(prefix + '_torrent_trackers') + op.drop_index(op.f('ix_' + prefix + '_statistics_seed_count'), table_name=prefix + '_statistics') + op.drop_index(op.f('ix_' + prefix + '_statistics_leech_count'), table_name=prefix + '_statistics') + op.drop_index(op.f('ix_' + prefix + '_statistics_download_count'), table_name=prefix + '_statistics') + op.drop_table(prefix + '_statistics') + op.drop_table(prefix + '_torrents') + op.drop_index(prefix + '_uploader_flag_idx', table_name=prefix + '_torrents') + op.drop_index(op.f('ix_' + prefix + '_torrents_info_hash'), table_name=prefix + '_torrents') + op.drop_index(op.f('ix_' + prefix + '_torrents_flags'), table_name=prefix + '_torrents') + op.drop_index(op.f('ix_' + prefix + '_torrents_filesize'), table_name=prefix + '_torrents') + op.drop_index(op.f('ix_' + prefix + '_torrents_display_name'), table_name=prefix + '_torrents') + op.drop_table(prefix + '_sub_categories') + op.drop_table(prefix + '_main_categories') + + # Shared tables + op.drop_table('users') + op.drop_table('trackers') + # ### end Alembic commands ### diff --git a/migrations/versions/d0eeb8049623_add_comments.py b/migrations/versions/d0eeb8049623_add_comments.py index f6fab5b..47e2a7a 100644 --- a/migrations/versions/d0eeb8049623_add_comments.py +++ b/migrations/versions/d0eeb8049623_add_comments.py @@ -15,34 +15,23 @@ down_revision = '3001f79b7722' branch_labels = None depends_on = None +TABLE_PREFIXES = ('nyaa', 'sukebei') + 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 ### + for prefix in TABLE_PREFIXES: + op.create_table(prefix + '_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'], [prefix + '_torrents.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('nyaa_comments') - op.drop_table('sukebei_comments') - # ### end Alembic commands ### + for prefix in TABLE_PREFIXES: + op.drop_table(prefix + '_comments') diff --git a/nyaa/models.py b/nyaa/models.py index 8b27679..e34c767 100755 --- a/nyaa/models.py +++ b/nyaa/models.py @@ -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 @@ -36,6 +39,31 @@ else: 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,45 +88,84 @@ 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) 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): @@ -211,44 +275,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 +341,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 +391,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 +420,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 '' % self.id @@ -376,9 +480,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 @@ -464,12 +570,118 @@ class User(db.Model): return self.level >= UserLevelType.TRUSTED -# 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' + + +# 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 + + TorrentNameSearch = NyaaTorrentNameSearch +elif app.config['SITE_FLAVOR'] == 'sukebei': + Torrent = SukebeiTorrent + TorrentFilelist = SukebeiTorrentFilelist + TorrentInfo = SukebeiTorrentInfo + Statistic = SukebeiStatistic + TorrentTrackers = SukebeiTorrentTrackers + MainCategory = SukebeiMainCategory + SubCategory = SukebeiSubCategory + Comment = SukebeiComment + + TorrentNameSearch = SukebeiTorrentNameSearch