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