Big Update ! PyNyaaTa doesn't require MySQL anymore /o/
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
9b6cae5dc2
commit
e5e1ebf875
@ -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
|
||||
|
||||
|
50
app.py
50
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,6 +41,7 @@ def verify_password(username, password):
|
||||
def boldify(name):
|
||||
query = request.args.get('q', '')
|
||||
name = Connector.boldify(name, query)
|
||||
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])
|
||||
@ -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/<url_filters>')
|
||||
@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/<int:link_id>', methods=['GET', 'POST'])
|
||||
@mysql_required
|
||||
@auth.login_required
|
||||
def admin_edit(link_id=None):
|
||||
folders = AnimeFolder.query.all()
|
||||
|
32
config.py
32
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)
|
||||
|
@ -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
|
||||
|
26
forms.py
Normal file
26
forms.py
Normal file
@ -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')
|
27
models.py
27
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()
|
||||
|
@ -2,7 +2,8 @@
|
||||
{% block title %}- Admin List{% endblock %}
|
||||
{% block add_button %}
|
||||
<a class="navbar-item has-tooltip-bottom" data-tooltip="Add entry" href="{{ url_for('admin_edit') }}">
|
||||
<i class="fa fa-plus"></i><i> </i><span class="is-hidden-mobile">Add entry</span>
|
||||
<i class="fa fa-plus"></i><i> </i>
|
||||
<span class="is-hidden-mobile">Add entry</span>
|
||||
</a>
|
||||
{% endblock %}
|
||||
{% block body %}
|
||||
|
@ -20,22 +20,28 @@
|
||||
𝛑 😼 た
|
||||
</a>
|
||||
<a class="navbar-item has-tooltip-bottom" data-tooltip="Latest torrents" href="{{ url_for('latest') }}">
|
||||
<i class="fa fa-newspaper-o"></i><i> </i><span class="is-hidden-mobile">Latest torrents</span>
|
||||
<i class="fa fa-newspaper-o"></i><i> </i>
|
||||
<span class="is-hidden-mobile">Latest torrents</span>
|
||||
</a>
|
||||
{% if mysql_disabled %}
|
||||
<a class="navbar-item has-tooltip-bottom" data-tooltip="My seeded torrents"
|
||||
href="{{ url_for('list_animes') }}">
|
||||
<i class="fa fa-cloud-download"></i><i> </i><span class="is-hidden-mobile">My seeded torrents</span>
|
||||
<i class="fa fa-cloud-download"></i><i> </i>
|
||||
<span class="is-hidden-mobile">My seeded torrents</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% block add_button %}{% endblock %}
|
||||
{% if request.args.get('q') %}
|
||||
<a class="navbar-item has-tooltip-bottom" data-tooltip="TVDB"
|
||||
href="https://www.thetvdb.com/search?menu%5Btype%5D=TV&query={{ request.args.get('q') }}"
|
||||
target="_blank">
|
||||
<i class="fa fa-television"></i><i> </i><span class="is-hidden-mobile">TVDB</span>
|
||||
<i class="fa fa-television"></i><i> </i>
|
||||
<span class="is-hidden-mobile">TVDB</span>
|
||||
</a>
|
||||
<a class="navbar-item has-tooltip-bottom" data-tooltip="Nautiljon"
|
||||
href="https://www.nautiljon.com/search.php?q={{ request.args.get('q') }}" target="_blank">
|
||||
<i class="fa fa-rss"></i><i> </i><span class="is-hidden-mobile">Nautiljon</span>
|
||||
<i class="fa fa-rss"></i><i> </i>
|
||||
<span class="is-hidden-mobile">Nautiljon</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
Reference in New Issue
Block a user