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