proof of concept
This commit is contained in:
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
env
|
||||||
|
*.zip
|
||||||
|
*.cbr
|
||||||
|
*.cbz
|
||||||
|
*.cfg
|
||||||
|
*.jpg
|
||||||
|
settings.json
|
||||||
|
.flaskenv
|
||||||
|
*.db
|
||||||
|
__pycache__
|
||||||
44
app/__init__.py
Normal file
44
app/__init__.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
from flask import Flask, render_template
|
||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
from flask_migrate import Migrate
|
||||||
|
from flask_mail import Mail
|
||||||
|
from flask_moment import Moment
|
||||||
|
from config import config
|
||||||
|
from flask_login import LoginManager
|
||||||
|
from flask_assets import Environment, Bundle
|
||||||
|
|
||||||
|
|
||||||
|
mail = Mail()
|
||||||
|
moment = Moment()
|
||||||
|
db = SQLAlchemy()
|
||||||
|
login_manager = LoginManager()
|
||||||
|
login_manager.login_view = 'auth.login'
|
||||||
|
|
||||||
|
|
||||||
|
def create_app(config_name):
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.config.from_object(config[config_name])
|
||||||
|
config[config_name].init_app(app)
|
||||||
|
|
||||||
|
mail.init_app(app)
|
||||||
|
moment.init_app(app)
|
||||||
|
db.init_app(app)
|
||||||
|
login_manager.init_app(app)
|
||||||
|
assets = Environment(app)
|
||||||
|
assets.url = app.static_url_path
|
||||||
|
assets.debug = True
|
||||||
|
scss = Bundle('scss/_variables.scss',
|
||||||
|
filters='pyscss',
|
||||||
|
output='gen/ripper.min.css')
|
||||||
|
assets.register('scss_all', scss)
|
||||||
|
|
||||||
|
|
||||||
|
from .main import main as main_blueprint
|
||||||
|
from .admin import admin as admin_blueprint
|
||||||
|
from .auth import auth as auth_blueprint
|
||||||
|
|
||||||
|
app.register_blueprint(main_blueprint)
|
||||||
|
app.register_blueprint(admin_blueprint)
|
||||||
|
app.register_blueprint(auth_blueprint)
|
||||||
|
|
||||||
|
return app
|
||||||
7
app/admin/__init__.py
Normal file
7
app/admin/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
from flask import Blueprint
|
||||||
|
|
||||||
|
|
||||||
|
admin = Blueprint('admin', __name__)
|
||||||
|
|
||||||
|
|
||||||
|
from . import views, errors, forms, models
|
||||||
1
app/admin/errors.py
Normal file
1
app/admin/errors.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from flask import Flask
|
||||||
0
app/admin/forms.py
Normal file
0
app/admin/forms.py
Normal file
0
app/admin/models.py
Normal file
0
app/admin/models.py
Normal file
0
app/admin/views.py
Normal file
0
app/admin/views.py
Normal file
7
app/auth/__init__.py
Normal file
7
app/auth/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
from flask import Blueprint
|
||||||
|
|
||||||
|
|
||||||
|
auth = Blueprint('auth', __name__)
|
||||||
|
|
||||||
|
|
||||||
|
from . import views, errors, forms, models
|
||||||
0
app/auth/errors.py
Normal file
0
app/auth/errors.py
Normal file
0
app/auth/forms.py
Normal file
0
app/auth/forms.py
Normal file
0
app/auth/models.py
Normal file
0
app/auth/models.py
Normal file
0
app/auth/views.py
Normal file
0
app/auth/views.py
Normal file
7
app/main/__init__.py
Normal file
7
app/main/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
from flask import Blueprint
|
||||||
|
|
||||||
|
|
||||||
|
main = Blueprint('main', __name__)
|
||||||
|
|
||||||
|
|
||||||
|
from . import views, errors, forms, models
|
||||||
0
app/main/errors.py
Normal file
0
app/main/errors.py
Normal file
9
app/main/forms.py
Normal file
9
app/main/forms.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
from flask_wtf import FlaskForm
|
||||||
|
from wtforms import StringField, SubmitField
|
||||||
|
import wtforms.validators
|
||||||
|
from wtforms.validators import Length, MacAddress, IPAddress, DataRequired, EqualTo
|
||||||
|
|
||||||
|
|
||||||
|
class Ripper(FlaskForm):
|
||||||
|
search = StringField('Search')
|
||||||
|
submit = SubmitField('Download')
|
||||||
0
app/main/models.py
Normal file
0
app/main/models.py
Normal file
42
app/main/views.py
Normal file
42
app/main/views.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import os
|
||||||
|
from flask import render_template, session, redirect, url_for, flash, request, current_app
|
||||||
|
from . import main
|
||||||
|
from .forms import Ripper
|
||||||
|
from .. import db
|
||||||
|
from flask_login import current_user, login_required
|
||||||
|
from ..panelrip import ripper, zipper
|
||||||
|
|
||||||
|
|
||||||
|
accepted_methods = ['GET', 'POST']
|
||||||
|
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64)'}
|
||||||
|
|
||||||
|
|
||||||
|
def download_comic_files(soup):
|
||||||
|
# create list for comic file links
|
||||||
|
comic_files = []
|
||||||
|
|
||||||
|
# set the image_folder name to the title of the comic
|
||||||
|
image_folder = os.path.join(current_app.config['STASH_FOLDER'], ripper.get_title(soup))
|
||||||
|
print(image_folder)
|
||||||
|
|
||||||
|
# append each comic link found in the bs4 response to the comic file list
|
||||||
|
for link in ripper.get_img_links(soup):
|
||||||
|
comic_files.append(link)
|
||||||
|
|
||||||
|
# download the completed comic list
|
||||||
|
ripper.download_img_links(comic_files, soup, current_app.config['STASH_FOLDER'])
|
||||||
|
|
||||||
|
# create a comic archive from all images in image_folder
|
||||||
|
zipper.create_comic_archive(ripper.get_title(soup), image_folder)
|
||||||
|
|
||||||
|
|
||||||
|
@main.route('/', methods=accepted_methods)
|
||||||
|
def index():
|
||||||
|
form = Ripper()
|
||||||
|
|
||||||
|
if form.validate_on_submit():
|
||||||
|
soup = ripper.get_soup_obj(form.search.data, headers)
|
||||||
|
download_comic_files(soup)
|
||||||
|
return render_template('success.html')
|
||||||
|
|
||||||
|
return render_template('index.html', form=form)
|
||||||
0
app/panelrip/__init__.py
Normal file
0
app/panelrip/__init__.py
Normal file
47
app/panelrip/ripper.py
Normal file
47
app/panelrip/ripper.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import os
|
||||||
|
import urllib.request
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
|
||||||
|
def get_soup_obj(url, headers):
|
||||||
|
req = urllib.request.Request(url, headers=headers)
|
||||||
|
raw = urllib.request.urlopen(req)
|
||||||
|
|
||||||
|
return BeautifulSoup(raw, 'html.parser')
|
||||||
|
|
||||||
|
|
||||||
|
def get_title(soup):
|
||||||
|
''' Grab the title element string from a Beautiful Soup object
|
||||||
|
removing the last 45 garbage characters'''
|
||||||
|
comic_title = soup.title.string.replace('Read All Comics Online For Free', '').replace('…', '').replace('|', '')
|
||||||
|
return comic_title.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def get_img_links(soup):
|
||||||
|
images = []
|
||||||
|
for link in soup.findAll('a'):
|
||||||
|
if not link.has_attr('href'):
|
||||||
|
img = link.img['src']
|
||||||
|
images.append(img)
|
||||||
|
|
||||||
|
return images
|
||||||
|
|
||||||
|
def download_img_links(links, soup, folder, title=None):
|
||||||
|
# test to see if the folder exists
|
||||||
|
if title is None:
|
||||||
|
print('No title specified\ncreating from reqest')
|
||||||
|
title = get_title(soup)
|
||||||
|
# if it doesn't, create it
|
||||||
|
if not os.path.exists(os.path.join(folder, title)):
|
||||||
|
os.makedirs(os.path.join(folder, title), mode=0o777)
|
||||||
|
print('Folder {} created...'.format(os.path.join(folder, title)))
|
||||||
|
# retrieve files from server
|
||||||
|
for link in links:
|
||||||
|
opener = urllib.request.build_opener()
|
||||||
|
opener.addheaders = [('User-agent', 'Mozilla/5.0')]
|
||||||
|
urllib.request.install_opener(opener)
|
||||||
|
print('Downloading file {} from {}'.format((link[-6:] if link[-11:-4] != '%2Bcopy' else (link[-13:-11] + link[-4:]) ), title))
|
||||||
|
urllib.request.urlretrieve(link, filename=os.path.join(folder + '/' + title, title + ' 0' + (link[-6:] if link[-11:-4] != '%2Bcopy' else (link[-13:-11] + link[-4:]) )))
|
||||||
|
|
||||||
|
print("Download Complete!")
|
||||||
|
|
||||||
15
app/panelrip/zipper.py
Normal file
15
app/panelrip/zipper.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
basedir = os.path.dirname(__file__)
|
||||||
|
|
||||||
|
def create_comic_archive(title, folder):
|
||||||
|
print('Creating comic archive: {}.cbr'.format(title))
|
||||||
|
|
||||||
|
output = shutil.make_archive(title, 'zip', folder)
|
||||||
|
os.rename(output, os.path.join(folder, title +'.cbr'))
|
||||||
|
print('Performing folder cleanup...')
|
||||||
|
for img in os.listdir(folder):
|
||||||
|
if not img.endswith('.cbr'):
|
||||||
|
os.remove(os.path.join(folder, img))
|
||||||
6
app/templates/index.html
Normal file
6
app/templates/index.html
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<h1>Save All Comics!</h1>
|
||||||
|
<form action="" method="POST" enctype=multipart/form-data>
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
{{ form.search(placeholder='Comic URL, eg: https://readallcomics.com/marauders-001-2019/') }}
|
||||||
|
{{ form.submit() }}
|
||||||
|
</form>
|
||||||
2
app/templates/success.html
Normal file
2
app/templates/success.html
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
<h1>Huzzah! It Worked!</h1>
|
||||||
|
<a href="{{ url_for('main.index') }}">« Back to Home</a>
|
||||||
53
config.py
Normal file
53
config.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
|
||||||
|
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
load_dotenv(os.path.join(basedir, '.flaskenv'))
|
||||||
|
|
||||||
|
class Config(object):
|
||||||
|
VERSION = '0.0.1'
|
||||||
|
SECRET_KEY = os.environ.get('SECRET_KEY') or 'snapekilledumbledore'
|
||||||
|
MAIL_SERVER = os.environ.get('MAIL_SERVER')
|
||||||
|
MAIL_PORT = 587
|
||||||
|
MAIL_USE_TLS = True
|
||||||
|
MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
|
||||||
|
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
|
||||||
|
MAIL_SUBJECT_PREFIX = os.environ.get('MAIL_SUBJECT_PREFIX')
|
||||||
|
ADMIN = os.environ.get('MAIL_SENDER') or 'bryan.bailey@brizzle.dev'
|
||||||
|
WTF_CSRF_ENABLED = True
|
||||||
|
SQLALCHEMY_TRACK_MODIFICATIONS = True
|
||||||
|
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URI') or \
|
||||||
|
'sqlite:///' + os.path.join(basedir, 'app.db')
|
||||||
|
UPLOAD_FOLDER = os.path.join(basedir, 'app/static/uploads')
|
||||||
|
STASH_FOLDER = os.path.join(UPLOAD_FOLDER, 'stash')
|
||||||
|
CLOUD_PLATFORM = 'aws'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def init_app(app):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DevConfig(Config):
|
||||||
|
DEBUG = True
|
||||||
|
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URI') or \
|
||||||
|
'sqlite:///' + os.path.join(basedir, 'dev.db')
|
||||||
|
|
||||||
|
|
||||||
|
class TestConfig(Config):
|
||||||
|
DEBUG = False
|
||||||
|
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URI') or \
|
||||||
|
'sqlite:///' + os.path.join(basedir, 'test.db')
|
||||||
|
|
||||||
|
|
||||||
|
# class ProductionConfig(Config):
|
||||||
|
# SQLALCHET_BINDS = {
|
||||||
|
# 'main': SQLALCHEMY_DATABASE_URI
|
||||||
|
# }
|
||||||
|
|
||||||
|
config = {
|
||||||
|
'development': DevConfig,
|
||||||
|
'testing': TestConfig,
|
||||||
|
'production': Config,
|
||||||
|
'default': DevConfig
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user