diff --git a/README.md b/README.md index a584b72..16ff647 100644 --- a/README.md +++ b/README.md @@ -13,20 +13,16 @@ After a good rewrite in Python, it's time to show it to the public, and here it ### With Docker - Install Docker: https://hub.docker.com/search/?type=edition&offering=community -- Be sure to have a MySQL Server installed and running -- Create a .env like [this one](.env.dist) -- Run `docker run --env-file .env -p 5000 xefir/pynyaata` +- Run `docker run -p 5000 xefir/pynyaata` - The app is accessible at http://localhost:5000 ### Without Docker - Install Python 3: https://www.python.org/downloads/ - Install Pip: https://pip.pypa.io/en/stable/installing/ -- Install MariaDB (or any MySQL server): https://mariadb.com/downloads/ - Clone this repository - Launch a terminal and move into the root of the cloned repository - Run `pip install -r requirements.txt` -- Copy the `.env.dist` file to `.env` and ajust values to point to your MySQL server - Run `python3 app.py` - The app is accessible at http://localhost:5000 @@ -43,6 +39,7 @@ After a good rewrite in Python, it's time to show it to the public, and here it All is managed by environment variables. Please look into the `.env.dist` file to list all env variables possible. +You have to install MariaDB (or any MySQL server) to be able to access the admin panel. ## Links diff --git a/app.py b/app.py index 61a5088..5825a4d 100644 --- a/app.py +++ b/app.py @@ -1,11 +1,35 @@ from operator import attrgetter, itemgetter from time import sleep -from flask import redirect, render_template, request, url_for +from flask import redirect, render_template, request, url_for, abort -from config import app, auth, db, ADMIN_USERNAME, ADMIN_PASSWORD, APP_PORT +from config import app, auth, ADMIN_USERNAME, ADMIN_PASSWORD, APP_PORT from connectors import * -from models import AnimeFolder, AnimeTitle, DeleteForm, SearchForm, EditForm +from forms import SearchForm, DeleteForm, EditForm + +if MYSQL_ENABLED: + from config import db + from models import AnimeFolder, AnimeTitle, AnimeLink + + +def clean_model(obj): + for attr in dir(obj): + if not attr.startswith('_') and getattr(obj, attr) is None: + try: + setattr(obj, attr, '') + except AttributeError: + pass + return obj + + +def mysql_required(f): + @wraps(f) + def decorated_function(*args, **kwargs): + if not MYSQL_ENABLED: + return abort(404) + return f(*args, **kwargs) + + return decorated_function @auth.verify_password @@ -17,9 +41,10 @@ def verify_password(username, password): def boldify(name): query = request.args.get('q', '') name = Connector.boldify(name, query) - for keyword in db.session.query(AnimeTitle.keyword.distinct()).all(): - if keyword[0].lower() != query.lower(): - name = Connector.boldify(name, keyword[0]) + if MYSQL_ENABLED: + for keyword in db.session.query(AnimeTitle.keyword.distinct()).all(): + if keyword[0].lower() != query.lower(): + name = Connector.boldify(name, keyword[0]) return name @@ -52,7 +77,8 @@ def search(): AnimeUltime(query).run(), ] - return render_template('search.html', search_form=SearchForm(), connectors=results) + return render_template('search.html', search_form=SearchForm(), connectors=results, + mysql_disabled=not MYSQL_ENABLED) @app.route('/latest') @@ -73,11 +99,13 @@ def latest(page=1): result['self'] = Connector.get_instance(result['href'], '') results.sort(key=itemgetter('date'), reverse=True) - return render_template('latest.html', search_form=SearchForm(), torrents=results, page=page) + return render_template('latest.html', search_form=SearchForm(), torrents=results, page=page, + mysql_disabled=not MYSQL_ENABLED) @app.route('/list') @app.route('/list/') +@mysql_required def list_animes(url_filters='nyaa,yggtorrent'): filters = None for i, to_filter in enumerate(url_filters.split(',')): @@ -99,6 +127,7 @@ def list_animes(url_filters='nyaa,yggtorrent'): @app.route('/admin', methods=['GET', 'POST']) +@mysql_required @auth.login_required def admin(): folders = AnimeFolder.query.all() @@ -128,18 +157,9 @@ def admin(): return render_template('admin/list.html', search_form=SearchForm(), folders=folders, action_form=form) -def clean_model(obj): - for attr in dir(obj): - if not attr.startswith('_') and getattr(obj, attr) is None: - try: - setattr(obj, attr, '') - except AttributeError: - pass - return obj - - @app.route('/admin/edit', methods=['GET', 'POST']) @app.route('/admin/edit/', methods=['GET', 'POST']) +@mysql_required @auth.login_required def admin_edit(link_id=None): folders = AnimeFolder.query.all() diff --git a/config.py b/config.py index 1842f0c..d4fb3dd 100644 --- a/config.py +++ b/config.py @@ -5,32 +5,34 @@ from flask.cli import load_dotenv from flask_httpauth import HTTPBasicAuth from flask_sqlalchemy import SQLAlchemy -# init DB and migration load_dotenv() -db_user = environ.get('MYSQL_USER') -db_password = environ.get('MYSQL_PASSWORD') -db_name = environ.get('MYSQL_DATABASE') -db_host = environ.get('MYSQL_SERVER') -if not db_host or not db_user or not db_password or not db_name: - print('Missing connection environment variables') - exit() -# load app constants IS_DEBUG = environ.get('FLASK_ENV', 'production') == 'development' ADMIN_USERNAME = environ.get('ADMIN_USERNAME', 'admin') ADMIN_PASSWORD = environ.get('ADMIN_PASSWORD', 'secret') APP_PORT = environ.get('FLASK_PORT', 5000) CACHE_TIMEOUT = environ.get('CACHE_TIMEOUT', 60 * 60) BLACKLIST_WORDS = environ.get('BLACKLIST_WORDS', '').split(',') +MYSQL_ENABLED = False app = Flask(__name__) app.name = 'PyNyaaTa' app.secret_key = urandom(24).hex() -app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://%s:%s@%s/%s?charset=utf8mb4' % ( - db_user, db_password, db_host, db_name -) -app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True -app.config['SQLALCHEMY_ECHO'] = IS_DEBUG app.url_map.strict_slashes = False auth = HTTPBasicAuth() -db = SQLAlchemy(app) + +db_host = environ.get('MYSQL_SERVER') +if db_host: + MYSQL_ENABLED = True + db_user = environ.get('MYSQL_USER') + db_password = environ.get('MYSQL_PASSWORD') + db_name = environ.get('MYSQL_DATABASE') + if not db_user or not db_password or not db_name: + print('Missing connection environment variables') + exit() + app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://%s:%s@%s/%s?charset=utf8mb4' % ( + db_user, db_password, db_host, db_name + ) + app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True + app.config['SQLALCHEMY_ECHO'] = IS_DEBUG + db = SQLAlchemy(app) diff --git a/connectors.py b/connectors.py index 54be678..69fcd50 100644 --- a/connectors.py +++ b/connectors.py @@ -11,8 +11,7 @@ from bs4 import BeautifulSoup from cloudscraper import create_scraper from requests import RequestException -from config import IS_DEBUG, CACHE_TIMEOUT, BLACKLIST_WORDS -from models import AnimeLink +from config import IS_DEBUG, MYSQL_ENABLED, CACHE_TIMEOUT, BLACKLIST_WORDS scraper = create_scraper() @@ -102,6 +101,13 @@ def curl_content(url, params=None, ajax=False): return {'http_code': http_code, 'output': output} +def link_exist_in_db(href): + if MYSQL_ENABLED: + from models import AnimeLink + return AnimeLink.query.filter_by(link=href).first() + return False + + class Connector(ABC): @property @abstractmethod @@ -250,8 +256,7 @@ class Nyaa(Connector): 'seeds': check_seeds, 'leechs': tds[6].string, 'downloads': check_downloads, - 'class': self.color if AnimeLink.query.filter_by(link=href).first() else 'is-%s' % - tr['class'][0] + 'class': self.color if link_exist_in_db(href) else 'is-%s' % tr['class'][0] }) self.on_error = False @@ -331,8 +336,7 @@ class Pantsu(Connector): 'seeds': check_seeds, 'leechs': tds[5].string, 'downloads': check_downloads, - 'class': self.color if AnimeLink.query.filter_by(link=href).first() else 'is-%s' % - tr['class'][0] + 'class': self.color if link_exist_in_db(href) else 'is-%s' % tr['class'][0] }) self.on_error = False @@ -401,9 +405,7 @@ class YggTorrent(Connector): 'seeds': check_seeds, 'leechs': tds[8].string, 'downloads': check_downloads, - 'class': self.color if AnimeLink.query.filter_by( - link=quote(url['href'], '/+:') - ).first() else '' + 'class': self.color if link_exist_in_db(quote(url['href'], '/+:')) else '' }) self.on_error = False @@ -465,7 +467,7 @@ class AnimeUltime(Connector): 'name': url.get_text(), 'type': tds[1].string, 'date': datetime.fromtimestamp(0), - 'class': self.color if AnimeLink.query.filter_by(link=href).first() else '' + 'class': self.color if link_exist_in_db(href) else '' }) else: player = html.select('div.AUVideoPlayer') @@ -479,7 +481,7 @@ class AnimeUltime(Connector): 'name': name[0].string, 'type': ani_type[0].string.replace(':', ''), 'date': datetime.fromtimestamp(0), - 'class': self.color if AnimeLink.query.filter_by(link=href).first() else '' + 'class': self.color if link_exist_in_db(href) else '' }) self.on_error = False @@ -513,7 +515,7 @@ class AnimeUltime(Connector): 'name': link.string, 'type': tds[4].string, 'date': release_date, - 'class': self.color if AnimeLink.query.filter_by(link=href).first() else '' + 'class': self.color if link_exist_in_db(href) else '' }) self.on_error = False diff --git a/forms.py b/forms.py new file mode 100644 index 0000000..c858b81 --- /dev/null +++ b/forms.py @@ -0,0 +1,26 @@ +from flask_wtf import FlaskForm +from wtforms import BooleanField, HiddenField, StringField +from wtforms.fields.html5 import SearchField, URLField +from wtforms.validators import DataRequired + + +class SearchForm(FlaskForm): + q = SearchField('search', validators=[DataRequired()]) + + +class DeleteForm(FlaskForm): + class Meta: + csrf = False + + id = HiddenField('id', validators=[DataRequired()]) + + +class EditForm(FlaskForm): + id = HiddenField('id') + folder = StringField('folder', validators=[DataRequired()]) + name = StringField('name', validators=[DataRequired()]) + link = URLField('link', validators=[DataRequired()]) + season = StringField('season', validators=[DataRequired()]) + comment = StringField('comment') + keyword = StringField('keyword') + is_vf = BooleanField('is_vf') diff --git a/models.py b/models.py index 2e6f72a..6ea14ea 100644 --- a/models.py +++ b/models.py @@ -1,8 +1,3 @@ -from flask_wtf import FlaskForm -from wtforms import BooleanField, HiddenField, StringField -from wtforms.fields.html5 import SearchField, URLField -from wtforms.validators import DataRequired - from config import db @@ -29,26 +24,4 @@ class AnimeLink(db.Model): title_id = db.Column(db.Integer, db.ForeignKey('anime_title.id')) -class SearchForm(FlaskForm): - q = SearchField('search', validators=[DataRequired()]) - - -class DeleteForm(FlaskForm): - class Meta: - csrf = False - - id = HiddenField('id', validators=[DataRequired()]) - - -class EditForm(FlaskForm): - id = HiddenField('id') - folder = StringField('folder', validators=[DataRequired()]) - name = StringField('name', validators=[DataRequired()]) - link = URLField('link', validators=[DataRequired()]) - season = StringField('season', validators=[DataRequired()]) - comment = StringField('comment') - keyword = StringField('keyword') - is_vf = BooleanField('is_vf') - - db.create_all() diff --git a/templates/admin/list.html b/templates/admin/list.html index 8e3c961..1146be1 100644 --- a/templates/admin/list.html +++ b/templates/admin/list.html @@ -2,7 +2,8 @@ {% block title %}- Admin List{% endblock %} {% block add_button %} -  Add entry +   + Add entry {% endblock %} {% block body %} diff --git a/templates/layout.html b/templates/layout.html index 5f4a2ad..babe390 100644 --- a/templates/layout.html +++ b/templates/layout.html @@ -20,22 +20,28 @@ 𝛑 😼 た -  Latest torrents - - -  My seeded torrents +   + Latest torrents + {% if mysql_disabled %} + +   + My seeded torrents + + {% endif %} {% block add_button %}{% endblock %} {% if request.args.get('q') %} -  TVDB +   + TVDB -  Nautiljon +   + Nautiljon {% endif %}