parent
1ece5958b8
commit
21264da348
@ -0,0 +1,184 @@ |
|||||||
|
#!/usr/bin/env python |
||||||
|
# -*- coding: utf-8 -*- |
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- IMPORTS |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
""" --- PYTHON IMPORTS --- """ |
||||||
|
import os, pathlib |
||||||
|
from os.path import dirname, abspath |
||||||
|
|
||||||
|
""" --- STASHR CORE IMPORTS --- """ |
||||||
|
from stashr import database as stashr_database |
||||||
|
from stashr.stashr import stashr_image_downloaded |
||||||
|
from stashr.api import create_json_return, api |
||||||
|
|
||||||
|
""" --- STASHR PLUGIN IMPORTS --- """ |
||||||
|
from . import thumbnailer, tasks, forms |
||||||
|
from .config import thumbconfig |
||||||
|
|
||||||
|
""" --- FLASK EXTENSION IMPORTS --- """ |
||||||
|
from flask_login import current_user |
||||||
|
|
||||||
|
""" --- STASHR DEPENDENCY IMPORTS --- """ |
||||||
|
from flask import Blueprint, render_template, request, flash, redirect, url_for |
||||||
|
from flask_login import login_required, current_user |
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- PLUGIN |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
# PLUGIN DETAILS |
||||||
|
__plugin_name__ = "Image Thumbnailer" |
||||||
|
__version__ = "0.1.0" |
||||||
|
__author__ = "Stashr" |
||||||
|
__description__ = "Shrink image filesize" |
||||||
|
|
||||||
|
"""------------------------------ |
||||||
|
- PLUGIN ROUTES |
||||||
|
------------------------------""" |
||||||
|
|
||||||
|
""" --- DEFINE BLUEPRINT --- """ |
||||||
|
bp = Blueprint('thumbnailer', __name__, root_path=dirname(abspath(__file__)), template_folder='templates', static_folder='static') |
||||||
|
|
||||||
|
""" --- PAGES --- """ |
||||||
|
|
||||||
|
@bp.route('/settings', methods=['GET', 'POST']) |
||||||
|
@login_required |
||||||
|
def thumbnailer_settings_page(): |
||||||
|
|
||||||
|
if current_user.role != 'admin': |
||||||
|
flash('Permission Denied', 'error') |
||||||
|
return redirect(url_for('index_page')) |
||||||
|
|
||||||
|
settings_form = forms.settings_form() |
||||||
|
|
||||||
|
if request.method == 'POST': |
||||||
|
if settings_form.validate(): |
||||||
|
thumbconfig['THUMBNAILER']['max_width'] = settings_form.max_width.data |
||||||
|
thumbconfig['THUMBNAILER']['quality'] = settings_form.quality.data |
||||||
|
thumbconfig['THUMBNAILER']['save_original_file'] = settings_form.save_original_file.data |
||||||
|
thumbconfig.write() |
||||||
|
flash('Thumbnailer Settings Updated', 'success') |
||||||
|
else: |
||||||
|
for error in settings_form.errors.items(): |
||||||
|
flash(f'{error[0]}: {error[1]}', 'error') |
||||||
|
|
||||||
|
return render_template( |
||||||
|
'thumbnailer_settings.html', |
||||||
|
settings_form=settings_form, |
||||||
|
title='Thumbnailer Settings' |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
""" --- API --- """ |
||||||
|
|
||||||
|
# SCAN IMAGES |
||||||
|
@bp.route('/api/scan', methods=['POST']) |
||||||
|
def api_post_scan_images(): |
||||||
|
|
||||||
|
user = current_user |
||||||
|
|
||||||
|
if not user.is_authenticated: |
||||||
|
if not request.json: |
||||||
|
return create_json_return('400') |
||||||
|
if "api_key" not in request.json: |
||||||
|
return create_json_return('400') |
||||||
|
if request.json['api_key'] == "": |
||||||
|
return create_json_return('100') |
||||||
|
|
||||||
|
user = stashr_database.session \ |
||||||
|
.query(stashr_database.Users) \ |
||||||
|
.filter(stashr_database.Users.api_key == request.json['api_key']) \ |
||||||
|
.first() |
||||||
|
|
||||||
|
if user is None: |
||||||
|
return create_json_return('100') |
||||||
|
|
||||||
|
if user.role.lower() != 'admin': |
||||||
|
return create_json_return('401') |
||||||
|
|
||||||
|
tasks.scan_images_task() |
||||||
|
|
||||||
|
return create_json_return('200') |
||||||
|
|
||||||
|
|
||||||
|
# PROCESS IMAGES |
||||||
|
@bp.route('/api/process/images', methods=['POST']) |
||||||
|
def api_post_process_images(): |
||||||
|
|
||||||
|
user = current_user |
||||||
|
|
||||||
|
if not user.is_authenticated: |
||||||
|
if not request.json: |
||||||
|
return create_json_return('400') |
||||||
|
if "api_key" not in request.json: |
||||||
|
return create_json_return('400') |
||||||
|
if request.json['api_key'] == "": |
||||||
|
return create_json_return('100') |
||||||
|
|
||||||
|
user = stashr_database.session \ |
||||||
|
.query(stashr_database.Users) \ |
||||||
|
.filter(stashr_database.Users.api_key == request.json['api_key']) \ |
||||||
|
.first() |
||||||
|
|
||||||
|
if user is None: |
||||||
|
return create_json_return('100') |
||||||
|
|
||||||
|
if user.role.lower() != 'admin': |
||||||
|
return create_json_return('401') |
||||||
|
|
||||||
|
tasks.process_images_task() |
||||||
|
|
||||||
|
return create_json_return('200') |
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/api/settings', methods=['GET']) |
||||||
|
def api_get_settings(): |
||||||
|
|
||||||
|
user = current_user |
||||||
|
|
||||||
|
if not user.is_authenticated: |
||||||
|
api_key = request.args.get('api_key') |
||||||
|
if api_key == "": |
||||||
|
return create_json_return('100') |
||||||
|
user = stashr_database.session \ |
||||||
|
.query(stashr_database.Users) \ |
||||||
|
.filter(stashr_database.Users.api_key == api_key) \ |
||||||
|
.first() |
||||||
|
|
||||||
|
if user is None: |
||||||
|
return create_json_return('100') |
||||||
|
|
||||||
|
if user.role.lower() != 'admin': |
||||||
|
return create_json_return('401') |
||||||
|
|
||||||
|
settings = thumbconfig |
||||||
|
|
||||||
|
return create_json_return('200', results=settings) |
||||||
|
|
||||||
|
|
||||||
|
"""------------------------------ |
||||||
|
- PLUGIN FUNCTIONS |
||||||
|
------------------------------""" |
||||||
|
|
||||||
|
@stashr_image_downloaded.connect |
||||||
|
def do_something(*args, **kwargs): |
||||||
|
pass |
||||||
|
|
||||||
|
|
||||||
|
"""------------------------------ |
||||||
|
- REGISTER PLUGIN |
||||||
|
------------------------------""" |
||||||
|
|
||||||
|
def register(): |
||||||
|
return dict( |
||||||
|
bep=dict( |
||||||
|
blueprint=bp, |
||||||
|
prefix='/thumbnailer' |
||||||
|
), |
||||||
|
tep = dict( |
||||||
|
settings_menu='thumbnailer_tep_settings_menu.html', |
||||||
|
) |
||||||
|
) |
@ -0,0 +1,61 @@ |
|||||||
|
#!/usr/bin/env python |
||||||
|
# -*- coding: utf-8 -*- |
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- IMPORTS |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
""" --- PYTHON IMPORTS --- """ |
||||||
|
import os, io, shutil |
||||||
|
from os.path import dirname, abspath |
||||||
|
|
||||||
|
from configobj import ConfigObj |
||||||
|
from validate import Validator |
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- THUMBNAILER CONFIG |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
class ThumbConfig(ConfigObj): |
||||||
|
|
||||||
|
configspec = u""" |
||||||
|
[THUMBNAILER] |
||||||
|
max_width = integer(default=160) |
||||||
|
quality = integer(default=50) |
||||||
|
save_original_file = boolean(default=False) |
||||||
|
""" |
||||||
|
|
||||||
|
def __init__(self): |
||||||
|
super(ThumbConfig, self).__init__() |
||||||
|
|
||||||
|
configspecfile = os.path.join( |
||||||
|
dirname(abspath(__file__)), |
||||||
|
'configspec.ini' |
||||||
|
) |
||||||
|
|
||||||
|
if not os.path.exists(configspecfile): |
||||||
|
with open(configspecfile, 'w') as fd: |
||||||
|
shutil.copyfileobj(io.StringIO(ThumbConfig.configspec), fd) |
||||||
|
|
||||||
|
self.filename = os.path.join( |
||||||
|
dirname(abspath(__file__)), |
||||||
|
'config.ini' |
||||||
|
) |
||||||
|
self.configspec = configspecfile |
||||||
|
self.encoding = "UTF8" |
||||||
|
|
||||||
|
tmp = ConfigObj(self.filename, configspec=self.configspec, encoding=self.encoding) |
||||||
|
validator = Validator() |
||||||
|
tmp.validate(validator, copy=True) |
||||||
|
|
||||||
|
self.merge(tmp) |
||||||
|
|
||||||
|
if not os.path.exists(self.filename): |
||||||
|
self.write() |
||||||
|
|
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- CONFIGURATION DEFINITION |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
thumbconfig = ThumbConfig() |
@ -0,0 +1,100 @@ |
|||||||
|
#!/usr/bin/env python |
||||||
|
# -*- coding: utf-8 -*- |
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- IMPORTS |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
""" --- HUEY IMPORT --- """ |
||||||
|
|
||||||
|
""" --- PYTHON IMPORTS --- """ |
||||||
|
import os |
||||||
|
from os.path import dirname, abspath |
||||||
|
|
||||||
|
""" --- STASHR DEPENDENCY IMPORTS --- """ |
||||||
|
|
||||||
|
""" --- STASHR CORE IMPORTS --- """ |
||||||
|
from stashr import log |
||||||
|
from stashr import database as stashrdb |
||||||
|
|
||||||
|
""" --- FLASK EXTENSION IMPORTS --- """ |
||||||
|
|
||||||
|
""" --- SQLALCHEMY IMPORTS --- """ |
||||||
|
from sqlalchemy import * |
||||||
|
from sqlalchemy.ext.declarative import declarative_base |
||||||
|
from sqlalchemy.orm import * |
||||||
|
|
||||||
|
""" --- CREATE LOGGER --- """ |
||||||
|
|
||||||
|
""" --- STASHR PLUGIN IMPORTS --- """ |
||||||
|
|
||||||
|
""" --- CREATE LOGGER --- """ |
||||||
|
logger = log.stashr_logger(__name__) |
||||||
|
|
||||||
|
""" --- CREATE SQLITE ENGINE --- """ |
||||||
|
db_path = os.path.join(dirname(abspath(__file__)), 'database.db') |
||||||
|
engine = create_engine('sqlite:///{0}'.format(db_path), |
||||||
|
connect_args={'check_same_thread': False, 'timeout': 15}, |
||||||
|
echo=False) |
||||||
|
Base = declarative_base() |
||||||
|
|
||||||
|
from marshmallow_sqlalchemy import ModelSchema, SQLAlchemyAutoSchema, SQLAlchemySchema, auto_field |
||||||
|
from marshmallow import fields |
||||||
|
from marshmallow_sqlalchemy.fields import Nested |
||||||
|
|
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- DATABASE |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
class Images(Base): |
||||||
|
__tablename__ = 'images' |
||||||
|
|
||||||
|
image_id = Column(Integer, primary_key=True) |
||||||
|
|
||||||
|
image_type = Column(String) |
||||||
|
image_type_id = Column(Integer) |
||||||
|
|
||||||
|
image_filename = Column(String) |
||||||
|
image_path = Column(String) |
||||||
|
|
||||||
|
image_original_size = Column(Integer) |
||||||
|
image_processed_size = Column(Integer) |
||||||
|
|
||||||
|
image_original_width = Column(Integer) |
||||||
|
image_original_height = Column(Integer) |
||||||
|
|
||||||
|
image_processed = Column(Boolean, server_default='0') |
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- DATABASE SCHEMAS |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
class ImagesSchema(SQLAlchemyAutoSchema): |
||||||
|
class Meta: |
||||||
|
model = Images |
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- DATABASE DEFINITION |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
Session = sessionmaker(bind=engine) |
||||||
|
session = Session() |
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- DATABASE FUNCTIONS |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
""" --- DATABASE MIGRATION --- """ |
||||||
|
def migrate_database(): |
||||||
|
# Check for new database tables |
||||||
|
# Check for new table columns |
||||||
|
pass |
||||||
|
|
||||||
|
""" --- CHECK DATABASE --- """ |
||||||
|
if not os.path.exists(db_path): |
||||||
|
logger.info('Creating Thumbnailer Database') |
||||||
|
Base.metadata.create_all(engine) |
||||||
|
else: |
||||||
|
logger.info('Thumbnailer Database Exists') |
||||||
|
migrate_database() |
@ -0,0 +1,47 @@ |
|||||||
|
#!/usr/bin/env python |
||||||
|
# -*- coding: utf-8 -*- |
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- IMPORTS |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
""" --- HUEY IMPORT --- """ |
||||||
|
""" --- PYTHON IMPORTS --- """ |
||||||
|
""" --- STASHR DEPENDENCY IMPORTS --- """ |
||||||
|
""" --- STASHR CORE IMPORTS --- """ |
||||||
|
from stashr import log, database |
||||||
|
|
||||||
|
""" --- FLASK EXTENSION IMPORTS --- """ |
||||||
|
from flask_wtf import FlaskForm |
||||||
|
|
||||||
|
from wtforms import StringField, BooleanField, SelectField, IntegerField, HiddenField, TextAreaField, SubmitField |
||||||
|
from wtforms.validators import DataRequired, Length, Email, EqualTo, ValidationError, NumberRange |
||||||
|
|
||||||
|
""" --- CREATE LOGGER --- """ |
||||||
|
logger = log.stashr_logger(__name__) |
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- FORMS |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
class settings_form(FlaskForm): |
||||||
|
|
||||||
|
max_width = IntegerField( |
||||||
|
'Max Image Width', |
||||||
|
validators = [ |
||||||
|
] |
||||||
|
) |
||||||
|
quality = IntegerField( |
||||||
|
'Image Quality', |
||||||
|
validators = [ |
||||||
|
NumberRange(min=0, max=100, message='Enter a number between 0 and 100') |
||||||
|
] |
||||||
|
) |
||||||
|
save_original_file = BooleanField( |
||||||
|
'Backup Original Image Files', |
||||||
|
validators = [ |
||||||
|
] |
||||||
|
) |
||||||
|
settings_button = SubmitField( |
||||||
|
'Save', |
||||||
|
) |
@ -0,0 +1,38 @@ |
|||||||
|
#!/usr/bin/env python |
||||||
|
# -*- coding: utf-8 -*- |
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- IMPORTS |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
""" --- HUEY IMPORT --- """ |
||||||
|
from stashr.stashr import huey |
||||||
|
from huey import crontab |
||||||
|
|
||||||
|
""" --- PYTHON IMPORTS --- """ |
||||||
|
|
||||||
|
""" --- STASHR DEPENDENCY IMPORTS --- """ |
||||||
|
|
||||||
|
""" --- STASHR CORE IMPORTS --- """ |
||||||
|
from stashr import log |
||||||
|
|
||||||
|
""" --- STASHR PLUGIN IMPORTS --- """ |
||||||
|
from . import thumbnailer |
||||||
|
|
||||||
|
""" --- CREATE LOGGER --- """ |
||||||
|
logger = log.stashr_logger(__name__) |
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- THUMBNAILER TASKS |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
|
||||||
|
@huey.task() |
||||||
|
def scan_images_task(): |
||||||
|
thumbnailer.get_images() |
||||||
|
thumbnailer.get_image_stats() |
||||||
|
|
||||||
|
|
||||||
|
@huey.task() |
||||||
|
def process_images_task(): |
||||||
|
thumbnailer.process_images() |
@ -0,0 +1,180 @@ |
|||||||
|
{% extends "settings_page.html" %} |
||||||
|
|
||||||
|
{% block header_script_files %} |
||||||
|
<script src="https://unpkg.com/axios/dist/axios.min.js"></script> |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block settings_pane %} |
||||||
|
|
||||||
|
<div id="app"> |
||||||
|
<settings ref="settings" v-bind:settings="settings"></settings> |
||||||
|
<modals ref="modal" v-bind:settings="settings"></modals> |
||||||
|
</div> |
||||||
|
|
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block script %} |
||||||
|
|
||||||
|
Vue.component('settings', { |
||||||
|
props: ['settings'], |
||||||
|
template: ` |
||||||
|
<div class="row r-10 m-2"> |
||||||
|
<div class="col col-12"> |
||||||
|
<div class="row"> |
||||||
|
<div class="col-sm-12 col-md-6 text-center text-md-start"> |
||||||
|
<h2>Thumbnailer</h2> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<hr /> |
||||||
|
<div class="row"> |
||||||
|
<ul class="nav nav-tabs" id="myTab" role="tablist"> |
||||||
|
<li class="nav-item" role="presentation"> |
||||||
|
<button class="nav-link active" id="settings-tab" data-bs-toggle="tab" data-bs-target="#settings" type="button" role="tab" aria-controls="settings" aria-selected="true">Settings</button> |
||||||
|
</li> |
||||||
|
<li class="nav-item" role="presentation"> |
||||||
|
<button class="nav-link" id="actions-tab" data-bs-toggle="tab" data-bs-target="#actions" type="button" role="tab" aria-controls="actions" aria-selected="false">Actions</button> |
||||||
|
</li> |
||||||
|
</ul> |
||||||
|
<div class="tab-content py-2" id="myTabContent"> |
||||||
|
<div class="tab-pane fade show active" id="settings" role="tabpanel" aria-labelledby="settings-tab"> |
||||||
|
<div class="row"> |
||||||
|
<table class="table table-striped"> |
||||||
|
<tbody> |
||||||
|
<tr> |
||||||
|
<th scope="row">{{ settings_form.max_width.label.text }}</th><td>[[ settings.THUMBNAILER.max_width ]]</td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<th scope="row">{{ settings_form.quality.label.text }}</th><td>[[ settings.THUMBNAILER.quality ]]</td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<th scope="row">{{ settings_form.save_original_file.label.text }}</th><td>[[ settings.THUMBNAILER.save_original_file ]]</td> |
||||||
|
</tr> |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
<div class="row w-100"> |
||||||
|
<div class="col-12 text-end my-2"> |
||||||
|
<button type="button" class="btn btn-info" data-bs-toggle="modal" data-bs-target="#settingsModal">Modify Settings</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="tab-pane fade" id="actions" role="tabpanel" aria-labelledby="actions-tab"> |
||||||
|
<table class="table table-striped"> |
||||||
|
<thead> |
||||||
|
<tr> |
||||||
|
<th scope="col">Actions</th> |
||||||
|
<th scope="col"></th> |
||||||
|
</tr> |
||||||
|
</thead> |
||||||
|
<tbody> |
||||||
|
<tr> |
||||||
|
<th scope="row">Scan New Images</th><td><a onclick="scanImages()"><i class="fas fa-play"></i></a></td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<th scope="row">Process All Images</th><td><a onclick="processImages()"><i class="fas fa-play"></i></a></td> |
||||||
|
</tr> |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
`, |
||||||
|
methods: {}, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}) |
||||||
|
|
||||||
|
Vue.component('modals',{ |
||||||
|
props: ['settings'], |
||||||
|
template: ` |
||||||
|
<div> |
||||||
|
<div class="modal" id="settingsModal" role="dialog" aria-labelledby="settingsModal" aria-hidden="true"> |
||||||
|
<div class="modal-dialog modal-dialog-centered" role="document"> |
||||||
|
<div class="modal-content"> |
||||||
|
<div class="modal-header"> |
||||||
|
<h5 class="modal-title" id="notesModalTitle"> |
||||||
|
Modify App Settings |
||||||
|
</h5> |
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> |
||||||
|
</div> |
||||||
|
|
||||||
|
<form method="POST"> |
||||||
|
<div class="modal-body"> |
||||||
|
|
||||||
|
{{ settings_form.csrf_token }} |
||||||
|
|
||||||
|
<div class="mb-3"> |
||||||
|
{{ settings_form.max_width.label }} |
||||||
|
{{ settings_form.max_width(class_='form-control', placeholder=settings_form.max_width.label.text) }} |
||||||
|
</div> |
||||||
|
<div class="mb-3"> |
||||||
|
{{ settings_form.quality.label }} |
||||||
|
{{ settings_form.quality(class_='form-control', placeholder=settings_form.quality.label.text) }} |
||||||
|
</div> |
||||||
|
<div class="mb-3"> |
||||||
|
{{ settings_form.save_original_file }} |
||||||
|
{{ settings_form.save_original_file.label }} |
||||||
|
</div> |
||||||
|
|
||||||
|
</div> |
||||||
|
<div class="modal-footer"> |
||||||
|
{{ settings_form.settings_button(class_='btn btn-success') }} |
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> |
||||||
|
</div> |
||||||
|
</form> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
`, |
||||||
|
methods: {}, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}) |
||||||
|
|
||||||
|
var app = new Vue({ |
||||||
|
el: '#app', |
||||||
|
data: { |
||||||
|
settings: [] |
||||||
|
}, |
||||||
|
created() { |
||||||
|
this.getSettings() |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
getSettings() { |
||||||
|
axios.get('{{ url_for('thumbnailer.api_get_settings') }}') |
||||||
|
.then(res => { |
||||||
|
this.settings = res.data.results; |
||||||
|
document.getElementById('max_width').value = res.data.results.THUMBNAILER.max_width; |
||||||
|
document.getElementById('quality').value = res.data.results.THUMBNAILER.quality; |
||||||
|
document.getElementById('quality').checked = res.data.results.THUMBNAILER.settings_button; |
||||||
|
}) |
||||||
|
.catch(err => console.log(err)) |
||||||
|
} |
||||||
|
}, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}) |
||||||
|
|
||||||
|
function scanImages() { |
||||||
|
axios.post('{{ url_for('thumbnailer.api_post_scan_images') }}') |
||||||
|
.then(res => { |
||||||
|
if(res.data.status_code == 200) { |
||||||
|
stashrToast('Scanning Images', 'success') |
||||||
|
} else { |
||||||
|
stashrToast('Error', 'error') |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
function processImages() { |
||||||
|
axios.post('{{ url_for('thumbnailer.api_post_process_images') }}') |
||||||
|
.then(res => { |
||||||
|
if(res.data.status_code == 200) { |
||||||
|
stashrToast('Processing Images', 'success') |
||||||
|
} else { |
||||||
|
stashrToast('Error', 'error') |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
{% endblock %} |
@ -0,0 +1,6 @@ |
|||||||
|
<li class="nav-item"> |
||||||
|
<a class="nav-link{% if request.path == url_for('thumbnailer.thumbnailer_settings_page') %} active{% endif %}" href="{{ url_for('thumbnailer.thumbnailer_settings_page') }}"> |
||||||
|
<i class="far fa-images"></i> |
||||||
|
Thumbnailer |
||||||
|
</a> |
||||||
|
</li> |
@ -0,0 +1,153 @@ |
|||||||
|
#!/usr/bin/env python |
||||||
|
# -*- coding: utf-8 -*- |
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- IMPORTS |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
""" --- PYTHON IMPORTS --- """ |
||||||
|
import os, pathlib, shutil |
||||||
|
|
||||||
|
""" --- STASHR CORE IMPORTS --- """ |
||||||
|
from stashr import log, folders, utils |
||||||
|
|
||||||
|
""" --- STASHR PLUGIN IMPORTS --- """ |
||||||
|
from . import database |
||||||
|
from .config import thumbconfig |
||||||
|
|
||||||
|
""" --- STASHR PLUGIN DEPENDENCY IMPORTS --- """ |
||||||
|
try: |
||||||
|
from PIL import Image |
||||||
|
except: |
||||||
|
utils.install_package('pillow') |
||||||
|
from PIL import Image |
||||||
|
|
||||||
|
""" --- CREATE LOGGER --- """ |
||||||
|
logger = log.stashr_logger(__name__) |
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- THUMBNAILER FUNCTIONS |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
|
||||||
|
def get_images(): |
||||||
|
|
||||||
|
stored_images = database.session \ |
||||||
|
.query(database.Images) \ |
||||||
|
.all() |
||||||
|
|
||||||
|
stored_images = database.ImagesSchema(many=True).dump(stored_images) |
||||||
|
|
||||||
|
new_images = [] |
||||||
|
existing_images = [] |
||||||
|
|
||||||
|
images_folder = folders.StashrFolders().images_folder() |
||||||
|
|
||||||
|
subdirs = os.scandir(images_folder) |
||||||
|
for subdir in subdirs: |
||||||
|
files = os.scandir(os.path.join(images_folder, subdir)) |
||||||
|
for image in files: |
||||||
|
existing_images.append(image) |
||||||
|
for item in existing_images: |
||||||
|
if not any(path['image_path'] == item.path for path in stored_images): |
||||||
|
new_image = database.Images( |
||||||
|
image_type = pathlib.Path(item.path).parent.name, |
||||||
|
image_type_id = pathlib.Path(item.path).stem, |
||||||
|
image_filename = item.name, |
||||||
|
image_path = item.path, |
||||||
|
) |
||||||
|
new_images.append(new_image) |
||||||
|
|
||||||
|
database.session.bulk_save_objects(new_images) |
||||||
|
database.session.commit() |
||||||
|
|
||||||
|
|
||||||
|
def get_image_stats(): |
||||||
|
|
||||||
|
missing_stats = database.session \ |
||||||
|
.query(database.Images) \ |
||||||
|
.filter(database.Images.image_original_size == None) \ |
||||||
|
.all() |
||||||
|
|
||||||
|
try: |
||||||
|
for image in missing_stats: |
||||||
|
with Image.open(image.image_path) as img: |
||||||
|
width, height = img.size |
||||||
|
|
||||||
|
image.image_original_width = width |
||||||
|
image.image_original_height = height |
||||||
|
image.image_original_size = pathlib.Path(image.image_path).stat().st_size |
||||||
|
|
||||||
|
database.session.merge(image) |
||||||
|
except Exception as e: |
||||||
|
logger.error(e) |
||||||
|
|
||||||
|
database.session.commit() |
||||||
|
|
||||||
|
|
||||||
|
def process_images(): |
||||||
|
|
||||||
|
non_processed = database.session \ |
||||||
|
.query(database.Images) \ |
||||||
|
.filter(database.Images.image_processed == False) \ |
||||||
|
.all() |
||||||
|
|
||||||
|
for image in non_processed: |
||||||
|
|
||||||
|
thumbnail = pathlib.Path(new_process_image(image)) |
||||||
|
|
||||||
|
if thumbnail.stat().st_size == 0: |
||||||
|
thumbnail.unlink() |
||||||
|
continue |
||||||
|
|
||||||
|
if thumbconfig['THUMBNAILER']['save_original_file']: |
||||||
|
backup_original_image(image.image_path) |
||||||
|
|
||||||
|
thumbnail.replace(image.image_path) |
||||||
|
|
||||||
|
image.image_processed = True |
||||||
|
image.image_processed_size = pathlib.Path(image.image_path).stat().st_size |
||||||
|
database.session.merge(image) |
||||||
|
|
||||||
|
database.session.commit() |
||||||
|
|
||||||
|
|
||||||
|
def new_process_image(image): |
||||||
|
|
||||||
|
max_width = thumbconfig['THUMBNAILER']['max_width'] |
||||||
|
quality = thumbconfig['THUMBNAILER']['quality'] |
||||||
|
|
||||||
|
ratio = image.image_original_height/image.image_original_width |
||||||
|
size = (max_width, int(max_width*ratio)) |
||||||
|
|
||||||
|
thumbnail_file = os.path.join( |
||||||
|
pathlib.Path(image.image_path).parent, |
||||||
|
f'{pathlib.Path(image.image_path).stem}-thumbnail.jpg' |
||||||
|
) |
||||||
|
try: |
||||||
|
with Image.open(image.image_path) as img: |
||||||
|
img.thumbnail(size) |
||||||
|
img.save(thumbnail_file, quality=quality) |
||||||
|
finally: |
||||||
|
return thumbnail_file |
||||||
|
|
||||||
|
|
||||||
|
def backup_original_image(original_file_path): |
||||||
|
|
||||||
|
original_file_path = pathlib.Path(original_file_path) |
||||||
|
|
||||||
|
backup_folder = os.path.join( |
||||||
|
folders.StashrFolders().backup_folder(), |
||||||
|
'images', |
||||||
|
original_file_path.parent.name |
||||||
|
) |
||||||
|
|
||||||
|
if not os.path.isdir(backup_folder): |
||||||
|
os.makedirs(backup_folder) |
||||||
|
|
||||||
|
backup_file = pathlib.Path(os.path.join( |
||||||
|
backup_folder, |
||||||
|
original_file_path.name |
||||||
|
)) |
||||||
|
|
||||||
|
shutil.copy(original_file_path, backup_file) |
Loading…
Reference in new issue