refactored app structure

This commit is contained in:
Bryan Bailey
2022-04-05 23:12:42 -04:00
parent 56b4ec2670
commit 5ca91452ca
23 changed files with 317 additions and 138 deletions

29
app/__init__.py Normal file
View File

@@ -0,0 +1,29 @@
from distutils.command.config import config
from flask import Flask
from flask_mail import Mail
from flask_moment import Moment
from flask_sqlalchemy import SQLAlchemy
from config import configuration, DevelopmentConfig, TestingConfig, ProductionConfig
mail =Mail()
moment = Moment()
db = SQLAlchemy()
def create_app(config_name):
app = Flask(__name__)
app.config.from_object(configuration[config_name])
configuration[config_name].init_app(app)
mail.init_app(app)
moment.init_app(app)
db.init_app(app)
from .main import main as main_blueprint
app.register_blueprint(main_blueprint)
return app

23
app/email.py Normal file
View File

@@ -0,0 +1,23 @@
import threading
from flask import current_app, render_template
from flask_mail import Message
from app import mail, config
def send_async_email(app, msg):
with app.app_context():
mail.send(msg)
def send_email(to, subject, template, **kwargs):
msg = Message(
current_app.config['MAIL_PREFIX'] + subject,
sender=current_app.config['MAIL_SENDER'], recipients=to)
msg.body = render_template(template + '.txt', **kwargs)
msg.html = render_template(template + '.html', **kwargs)
mail_thread = threading.Thread(target=send_async_email, args=[current_app._get_current_object(), msg])
mail_thread.start()
return mail_thread

7
app/main/__init__.py Normal file
View File

@@ -0,0 +1,7 @@
from flask import Blueprint
main = Blueprint('main', __name__)
from . import views, errors

12
app/main/errors.py Normal file
View File

@@ -0,0 +1,12 @@
from flask import render_template
from . import main
@main.app_errorhandler(404)
def not_found(e):
return render_template('404.html'), 404
@main.errorhandler(500)
def server_error(e):
return render_template('500.html'), 500

9
app/main/forms.py Normal file
View File

@@ -0,0 +1,9 @@
from flask_wtf import FlaskForm
from wtforms import StringField, BooleanField, SubmitField
from wtforms.validators import DataRequired
class DownloadForm(FlaskForm):
url = StringField('Comic URL', validators=[DataRequired()])
series = BooleanField('Series? ')
download = SubmitField('Download')

104
app/main/views.py Normal file
View File

@@ -0,0 +1,104 @@
import os
from datetime import datetime
from flask import render_template, session, send_from_directory, redirect, url_for, flash
from app.main import main
from app.main.forms import DownloadForm
from app import db
from app.email import send_email
from app.models import User, Role, ComicMeta
from yoink.comic import Comic
from yoink.config import config
@main.route('/uploads/<path:filename>')
def download_file(filename):
return send_from_directory(os.path.join(config.library_path, 'comics'), filename, as_attachment=True)
def get_cover_path(comic):
return [image for image in os.listdir(os.path.join(config.library_path, 'comics', comic.title)) if image.endswith('000.jpg')][0]
def get_archive_path(comic):
return [image for image in os.listdir(os.path.join(config.library_path, 'comics', comic.title)) if image.endswith('.cbr')][0]
def get_comic_library_meta():
comic_meta = []
for comic in ComicMeta.query.order_by(ComicMeta.id.desc()).all():
comic_meta.append({
'cover': comic.cover_path,
'title': comic.title,
'archive': comic.archive_path
})
return comic_meta
def new_user(username, db, app, form):
user = User.query.filter_by(username=username).first()
if user is None:
user = User(username=form.username.data, role_id=3)
db.session.add(user)
if app.config['YOINK_ADMIN']:
send_email(app.config['YOINK_ADMIN'], 'New User', 'mail/new_user', user=user)
db.session.commit()
def check_setup():
if User.query.all() is None:
return redirect(url_for('setup'))
@main.route('/', methods=['post', 'get'])
def index():
url = None
series = False
form = DownloadForm()
latest = get_comic_library_meta()
if form.validate_on_submit():
url = form.url.data.strip()
series = form.series.data
comic = Comic(url)
comic_meta = ComicMeta.query.filter_by(title=comic.title).first()
if comic_meta is None:
comic.archiver.download()
comic.archiver.generate_archive()
comic_meta = ComicMeta()
comic_meta.title = comic.title
comic_meta.category = comic.category
comic_meta.issue = comic.issue_number
comic_meta.next_issue = comic.next
comic_meta.previous_issue = comic.prev
comic_meta.cover_path = os.path.join(comic.title, get_cover_path(comic))
comic_meta.archive_path = os.path.join(comic.title, get_archive_path(comic))
db.session.add(comic_meta)
db.session.commit()
else:
flash(f'Comic {comic.title} exists')
latest = get_comic_library_meta()
form.url.data = ''
return render_template('index.html', form=form, url=url, series=series, latest=latest), 200
if form.series.data:
print('Download the whole damn lot')
flash(f'{comic.title} downloaded to {os.path.join(config.library_path, "comics/" + comic.title)}')
latest = get_comic_library_meta()
comic.archiver.cleanup_worktree()
form.url.data = ''
return render_template('index.html', form=form, url=url, series=series, latest=latest), 200

31
app/models.py Normal file
View File

@@ -0,0 +1,31 @@
from app import db
class Role(db.Model):
__tablename__ = 'roles'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True)
users = db.relationship('User', backref='role', lazy='dynamic')
def __repr__(self):
return f'<Role {self.name!r}>'
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), unique=True, index=True)
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
def __repr__(self): return f'<User {self.username!r}>'
class ComicMeta(db.Model):
__tablename__ = 'comicmeta'
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(256), unique=True, index=True)
issue = db.Column(db.Integer, nullable=True, index=True)
category = db.Column(db.String(128), index=True, nullable=True)
previous_issue = db.Column(db.String(256), nullable=True)
next_issue = db.Column(db.String(256), nullable=True)
cover_path = db.Column(db.String(256))
archive_path = db.Column(db.String(256))

BIN
app/static/comic.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

3
app/static/css/yoink.css Normal file
View File

@@ -0,0 +1,3 @@
* {
box-sizing: border-box;
}

0
app/static/js/yoink.js Normal file
View File

14
app/templates/404.html Normal file
View File

@@ -0,0 +1,14 @@
{% extends "base.html" %}
{% block meta %}
{{ super() }}
<!-- Add additional meta tags here -->
{% endblock %}
{% block title %}Testing{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Opps! File not found</h1>
</div>
{% endblock %}

14
app/templates/500.html Normal file
View File

@@ -0,0 +1,14 @@
{% extends "base.html" %}
{% block meta %}
{{ super() }}
<!-- Add additional meta tags here -->
{% endblock %}
{% block title %}Testing{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Opps! Technical difficulties</h1>
</div>
{% endblock %}

61
app/templates/base.html Normal file
View File

@@ -0,0 +1,61 @@
<!DOCTYPE html>
<html lang="en">
<head>
{% block meta %}
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{% endblock %}
<title>{% block title %}Y!oink Web App{% endblock %}</title>
{% block styles %}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<link rel="stylesheet" href="{{ url_for('static', filename='css/yoink.css') }}">
{% endblock %}
{% block scripts %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.min.js" integrity="sha384-QJHtvGhmr9XOIpI6YVutG+2QOK9T+ZnN4kzFN1RtK3zEFEIsxhlmWl5/YESvpZ13" crossorigin="anonymous"></script>
{{ moment.include_moment() }}
{% endblock %}
<link rel="shortcut icon" href="{{ url_for('static', filename='images/comic.png') }}" type="image/x-icon">
</head>
<body>
{% block navbar %}
<nav class="navbar navbar-inverse">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle Navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a href="/" class="navbar-brand">Y!oink</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li>
<a href="/">Y!oink</a>
</li>
</ul>
</div>
</div>
</nav>
{% endblock %}
{% block content %}
<div class="container-fluid">
{% for message in get_flashed_messages() %}
<div class="alert alert-success alert-dismissible fade show" role="alert">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
{{message}}
</div>
{% endfor %}
{% block page_content %}
{% endblock %}
</div>
{% endblock %}
<script src="{{ url_for('static', filename='js/yoink.js') }}"></script>
</body>
</html>

31
app/templates/index.html Normal file
View File

@@ -0,0 +1,31 @@
{% extends "base.html" %}
{% import 'macros/library.html' as library %}
{% block meta %}
{{ super() }}
<!-- Add additional meta tags here -->
{% endblock %}
{% block title %}Testing{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Y!oink Web App</h1>
</div>
<form method="post" onsubmit="download.disabled = true; url.readOnly = true; return true;">
{{ form.hidden_tag() }}
<div class="input-group mb-3">
{{ form.series.label(class_='input-group-text') }}
<div class="input-group-text">
{{ form.series() }}
</div>
{{ form.url(class_='form-control') }}
{{ form.download(class_='btn btn-outline-primary') }}
</div>
</form>
{{ library.latest_downloads(latest) }}
{% endblock %}

View File

@@ -0,0 +1,3 @@
{% macro render_comment(comment) %}
<li>{{ comment }}</li>
{% endmacro %}

View File

@@ -0,0 +1,38 @@
<!-- latest downloads - limit to a max of 6 -->
{% macro latest_downloads(comics) %}
{% if comics %}
<div class="row">
<h2>Latest Downloads</h2>
{% for comic in comics %}
<div class="col col-md-3">
<div class="card">
<img src="{{ url_for('main.download_file', filename=comic['cover']) }}" alt="{{ comic['title'] }}" class="card-image-top position-relative">
<div class="card-body">
<h4 class="card-title">{{ comic['title'] }}</h4>
<p class="card-text">{{ comic['title'] }}</p>
</div>
<div class="card-footer text-center">
<a href="{{url_for('main.download_file', filename=comic.archive) }}" class="btn btn-info">Download Archive</a>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<p>Empty Comic Library</p>
{% endif %}
{% endmacro %}
<!-- all downloaded comics - TODO: pagination -->
{% macro all_downloads(comics) %}
{% if comics %}
{% for comic in comics %}
<h3>{{ comic['title'] }}</h3>
<img src="{{ url_for('main.download_file', filename=comic['cover']) }}">
{% endfor %}
{% else %}
<p>Empty Comic Library</p>
{% endif %}
{% endmacro %}