Compare commits

...

4 Commits

Author SHA1 Message Date
Hell13Cat 800c9cb651 0.0.9s 2026-06-02 00:58:11 +03:00
Hell13Cat 4cddd19c76 0.0.8s 2026-06-02 00:44:02 +03:00
Hell13Cat 4a88e4c491 0.0.7s 2026-06-02 00:26:24 +03:00
Hell13Cat 627b374b7f 0.0.6s 2026-06-02 00:15:38 +03:00
8 changed files with 515 additions and 91 deletions
+128
View File
@@ -0,0 +1,128 @@
// ==UserScript==
// @name gelbooru.com Button DW ADD
// @namespace nyakonya_gelbooruocdw_dev
// @match https://gelbooru.com/index.php*
// @grant GM_xmlhttpRequest
// @grant GM_registerMenuCommand
// @grant GM_getValue
// @grant GM_setValue
// @version 1.0r
// @author https://t.me/Nyako_TW
// @downloadURL https://git.nekono.su/Nyako/Local-Booru/raw/branch/main/button_script.user.js
// @updateURL https://git.nekono.su/Nyako/Local-Booru/raw/branch/main/button_script.user.js
// @description Adds a one-click button to download the original image on Gelbooru post pages.
// ==/UserScript==
'use strict'; // Рекомендуется оставить для более строгого и безопасного выполнения кода.
// Функция безопасного запуска функций
function safeRun(fn, name) {
try {
fn();
} catch (e) {
console.error(`Ошибка в ${name}:`, e);
}
}
// Функция генерации текстовой конфигурации
function registerTextInput(key, defaultValue, title, promptMessage) {
const currentValue = GM_getValue(key, defaultValue);
const menuText = `⚙️ ${title}: ${currentValue}`;
GM_registerMenuCommand(menuText, () => {
const newValue = prompt(promptMessage, currentValue);
if (newValue !== null) {
GM_setValue(key, newValue.trim());
alert(
`Настройка изменена!\n${title}: ${newValue.trim()}\nОбновите страницу для применения настроек.`
);
location.reload();
}
});
}
// Генерация конфигураций
function config_runner() {
registerTextInput(
'nyakonya_gb_api_root',
'http://127.0.2.1',
'Адрес API',
'Введите базовый URL локального API (например, http://127.0.2.1 или https://booru.loc):'
);
}
// Запуск конфигуратора
safeRun(config_runner, "config_runner");
// 1. Находим ссылку на оригинальное изображение по тексту
const params = new URLSearchParams(window.location.search);
const imageID = params.get("id");
const xpathResult = document.evaluate(
'//a[text()="Original image"]',
document,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
null
);
const originalImageLinkElement = xpathResult.singleNodeValue;
if (originalImageLinkElement) {
const imageUrl = originalImageLinkElement.href;
const existingAnchor = document.querySelector('a[onclick*="saveTagSearch"]');
if (existingAnchor) {
const downloadSvgIcon = `
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="2em" height="2em" viewBox="0 0 122.433 122.88" enable-background="new 0 0 122.433 122.88" xml:space="preserve"><g><polygon fill-rule="evenodd" clip-rule="evenodd" points="61.216,122.88 0,59.207 39.403,59.207 39.403,0 83.033,0 83.033,59.207 122.433,59.207 61.216,122.88"/></g></svg>
`;
const newDownloadButton = document.createElement('a');
newDownloadButton.id = "nyakonya-gelbooruocdw-geter";
newDownloadButton.title = "Download original image";
newDownloadButton.innerHTML = downloadSvgIcon;
newDownloadButton.className = existingAnchor.className;
newDownloadButton.style.cssText = existingAnchor.style.cssText;
existingAnchor.parentNode.insertBefore(newDownloadButton, existingAnchor.nextSibling);
const searchBox = document.getElementById("tags-search");
if (searchBox) {
searchBox.style.width = "calc(100% - 380px)";
}
console.log('Кнопка для скачивания добавлена:', newDownloadButton);
newDownloadButton.addEventListener('click', (event) => {
event.preventDefault(); // Отменяем стандартное действие ссылки
// Получаем настроенный пользователем корневой путь
const apiRoot = GM_getValue('nyakonya_gb_api_root', 'http://127.0.2.1');
// Формируем корректный URL для запроса к dw_api
const apiUrl = apiRoot.endsWith('/') ? `${apiRoot}dw_api` : `${apiRoot}/dw_api`;
console.log(`Отправка URL на локальный API: ${imageUrl} (Endpoint: ${apiUrl})`);
const originalOutline = newDownloadButton.style.outline;
newDownloadButton.style.outline = "2px solid yellow"; // Статус "в процессе"
GM_xmlhttpRequest({
method: "POST",
url: apiUrl,
headers: {
"Content-Type": "application/json"
},
data: JSON.stringify({ id: imageID }),
onload: function(response) {
console.log('Ответ от API:', response.responseText);
newDownloadButton.style.outline = "2px solid green"; // Успех
setTimeout(() => { newDownloadButton.style.outline = originalOutline; }, 2000);
},
onerror: function(response) {
console.error('Ошибка при отправке на API:', response);
newDownloadButton.style.outline = "2px solid red"; // Ошибка
alert('Не удалось отправить на локальный загрузчик. Убедитесь, что Python-скрипт запущен и вы приняли самоподписанный сертификат в браузере. Подробности в консоли (F12).');
setTimeout(() => { newDownloadButton.style.outline = originalOutline; }, 5000);
}
});
});
} else {
console.warn('Исходный элемент для вставки кнопки (saveTagSearch) не найден.');
}
}
+7 -3
View File
@@ -1,7 +1,9 @@
{ {
"server":{ "server":{
"port":8084, "host":"127.0.2.1",
"count_page":25 "port":80,
"count_page":25,
"thumb_size":250
}, },
"gb":{ "gb":{
"id":"", "id":"",
@@ -12,6 +14,8 @@
} }
}, },
"file":{ "file":{
"repos":["data/files", "E:/local_booru/img"] "repos":["data/files"],
"dw":"data/files",
"thumb":"data/thumb"
} }
} }
+24 -10
View File
@@ -1,21 +1,31 @@
from system_module import db, gelbooru, logger from system_module import db, gelbooru, logger, image_processor
from flask import Flask, render_template, jsonify, send_from_directory, render_template_string, request from flask import Flask, render_template, jsonify, send_from_directory, render_template_string, request
import json import json
import requests import requests
from pathlib import Path from pathlib import Path
import subprocess
import traceback
import os import os
CONFIG_ROOT = json.load(open("config.json", "r", encoding="utf-8")) if Path("config_gb.json").exists():
CONFIG_ROOT_S = json.load(open("config_gb.json", "r", encoding="utf-8")) CONFIG_FILE = "config_gb.json"
else:
CONFIG_FILE = "config.json"
CONFIG_ROOT = json.load(open(CONFIG_FILE, "r", encoding="utf-8"))
CONFIG_HOST = CONFIG_ROOT["server"]["host"]
CONFIG_PORT = CONFIG_ROOT["server"]["port"] CONFIG_PORT = CONFIG_ROOT["server"]["port"]
PAGINATION_COUNT = CONFIG_ROOT["server"]["count_page"] PAGINATION_COUNT = CONFIG_ROOT["server"]["count_page"]
GB_ID = CONFIG_ROOT_S["gb"]["id"] TARGET_MIN_SIZE = CONFIG_ROOT["server"]["thumb_size"]
GB_HASH = CONFIG_ROOT_S["gb"]["hash"] GB_ID = CONFIG_ROOT["gb"]["id"]
GB_HASH = CONFIG_ROOT["gb"]["hash"]
GB_HEADERS = CONFIG_ROOT["gb"]["headers"] GB_HEADERS = CONFIG_ROOT["gb"]["headers"]
FILE_FOLDERS = CONFIG_ROOT["file"]["repos"] FILE_FOLDERS = CONFIG_ROOT["file"]["repos"]
THUMB_FOLDER = "data_test/thumb" FILE_DW_FOLDERS = CONFIG_ROOT["file"]["dw"]
THUMB_FOLDER = CONFIG_ROOT["file"]["thumb"]
gb = gelbooru.GB(GB_ID, GB_HASH, GB_HEADERS)
gb = gelbooru.GB(GB_ID, GB_HASH, GB_HEADERS, FILE_DW_FOLDERS)
log = logger.Logger() log = logger.Logger()
database = db.DB(True) database = db.DB(True)
app = Flask(__name__) app = Flask(__name__)
@@ -29,6 +39,8 @@ def find_file(filename, file_folders):
return folder_path return folder_path
return None return None
if not image_processor.check_ffmpeg(): print("WARNING!\n\nУ вас не установлен ffmpeg. Генерация превью для видео не доступна!")
@app.route("/") @app.route("/")
def index(): def index():
return render_template("index.html") return render_template("index.html")
@@ -58,7 +70,7 @@ def dw_api():
image_id = data["id"] image_id = data["id"]
try: try:
data_get, url_file, file_name = gb.get_from_id(image_id) data_get, url_file, file_name = gb.get_from_id(image_id)
if os.path.exists("dw/"+file_name): if database.check_uni(data_get["md5"]):
message = f"File '{file_name}' already exists. Skipping." message = f"File '{file_name}' already exists. Skipping."
log.send("warning", "all", message) log.send("warning", "all", message)
return jsonify({'status': 'skipped', 'message': message}) return jsonify({'status': 'skipped', 'message': message})
@@ -68,16 +80,18 @@ def dw_api():
if 'text/html' in content_type: if 'text/html' in content_type:
log.send("error", "all", "The server returned HTML instead of an image. Bot protection.") log.send("error", "all", "The server returned HTML instead of an image. Bot protection.")
return False return False
with open("dw/"+file_name, 'wb') as f: with open(FILE_DW_FOLDERS+"/"+file_name, 'wb') as f:
for chunk in r.iter_content(chunk_size=8192): for chunk in r.iter_content(chunk_size=8192):
f.write(chunk) f.write(chunk)
database.add_raw(data_get) database.add_raw(data_get)
image_processor.generate_thumb(FILE_DW_FOLDERS+"/"+file_name, THUMB_FOLDER, TARGET_MIN_SIZE)
return jsonify({'status': 'success'}) return jsonify({'status': 'success'})
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
error_message = f"Network error {image_id}: {e}" error_message = f"Network error {image_id}: {e}"
log.send("error", "all", error_message) log.send("error", "all", error_message)
return jsonify({'status': 'error', 'message': error_message}), 500 return jsonify({'status': 'error', 'message': error_message}), 500
except Exception as e: except Exception as e:
traceback.print_exc()
error_message = f"Unknown error: {e}" error_message = f"Unknown error: {e}"
log.send("error", "all", error_message) log.send("error", "all", error_message)
return jsonify({'status': 'error', 'message': error_message}), 500 return jsonify({'status': 'error', 'message': error_message}), 500
@@ -115,4 +129,4 @@ def api():
if __name__ == "__main__": if __name__ == "__main__":
app.run(host="0.0.0.0", port=CONFIG_PORT) app.run(host=CONFIG_HOST, port=CONFIG_PORT)
+176 -65
View File
@@ -1,31 +1,112 @@
from tinydb import TinyDB, Query import sqlite3
import os import os
import json import json
from collections import Counter
from system_module import gelbooru
from datetime import datetime from datetime import datetime
from system_module import gelbooru
class DB: class DB:
def __init__(self, debug): def __init__(self, debug: bool):
if debug: if debug:
db_path = "data_test" db_path = "data_test"
else: else:
db_path = "data" db_path = "data"
if not os.path.exists(os.path.join(db_path, "db.json")):
json.dump({}, open(os.path.join(db_path, "db.json"), "w", encoding="utf-8")) if not os.path.exists(db_path):
self.db = TinyDB(db_path+"/db.json") os.makedirs(db_path)
self.File = Query()
self.conn = sqlite3.connect(os.path.join(db_path, "db.sqlite"), check_same_thread=False)
self.conn.row_factory = sqlite3.Row
self._create_table()
def _create_table(self):
with self.conn:
self.conn.execute("""
CREATE TABLE IF NOT EXISTS files (
md5 TEXT PRIMARY KEY,
local_filename TEXT,
is_video INTEGER,
is_gif INTEGER,
tags TEXT,
rating TEXT,
score INTEGER,
timestamp TEXT,
parsed_time INTEGER,
display_date TEXT,
source TEXT,
width INTEGER,
height INTEGER
)
""")
def _row_to_dict(self, row: sqlite3.Row) -> dict:
if not row:
return None
d = dict(row)
d["is_video"] = bool(d["is_video"])
d["is_gif"] = bool(d["is_gif"])
def add(self, data_add): # Парсинг тегов из JSON-строки обратно в массив
self.db.insert(data_add) d["tags"] = json.loads(d["tags"]) if d["tags"] else []
# Восстанавливаем другие поля (например, display_date и source),
# если в них были переданы кортежи или списки
for field in ["display_date", "source"]:
val = d.get(field)
if isinstance(val, str) and (val.startswith('[') or val.startswith('{')):
try:
d[field] = json.loads(val)
except ValueError:
pass
return d
def add(self, data_add: dict):
parsed_time = 0
if "timestamp" in data_add:
try:
parsed_time = int(datetime.strptime(data_add["timestamp"], "%a %b %d %H:%M:%S %z %Y").timestamp())
except ValueError:
pass
row_data = {
"md5": data_add.get("md5"),
"local_filename": data_add.get("local_filename", data_add.get("image")),
"is_video": int(data_add.get("is_video", False)),
"is_gif": int(data_add.get("is_gif", False)),
"tags": data_add.get("tags", []),
"rating": data_add.get("rating"),
"score": data_add.get("score"),
"timestamp": data_add.get("timestamp", data_add.get("created_at")),
"parsed_time": parsed_time,
"display_date": data_add.get("display_date"),
"source": data_add.get("source"),
"width": data_add.get("width"),
"height": data_add.get("height")
}
# БЕЗОПАСНАЯ СЕРИАЛИЗАЦИЯ ДЛЯ SQLITE:
# SQLite принимает только числа, строки и байты.
# Любые списки (tags) или кортежи (display_date) мы перегоняем в JSON.
for key, value in row_data.items():
if isinstance(value, (list, tuple, dict)):
row_data[key] = json.dumps(value)
columns = ", ".join(row_data.keys())
placeholders = ", ".join(["?"] * len(row_data))
with self.conn:
self.conn.execute(
f"INSERT OR REPLACE INTO files ({columns}) VALUES ({placeholders})",
tuple(row_data.values())
)
def add_raw(self, data_add): def add_raw(self, data_add: dict):
is_video = False is_video = data_add["image"].split(".")[-1] in ["webm", "mp4", "avi"]
is_gif = False is_gif = data_add["image"].split(".")[-1] == "gif"
if data_add["image"].split(".")[-1] in ["webm", "mp4", "avi"]:
is_video = True dt_obj = datetime.strptime(data_add["created_at"], "%a %b %d %H:%M:%S %z %Y")
if data_add["image"].split(".")[-1] == "gif": parsed_time = int(dt_obj.timestamp())
is_gif = True
add_data = { add_data = {
"md5": data_add["md5"], "md5": data_add["md5"],
"local_filename": data_add["image"], "local_filename": data_add["image"],
@@ -35,70 +116,100 @@ class DB:
"rating": data_add["rating"], "rating": data_add["rating"],
"score": data_add["score"], "score": data_add["score"],
"timestamp": data_add["created_at"], "timestamp": data_add["created_at"],
"parsed_time": parsed_time,
"display_date": gelbooru.gelbooru_date_parse(data_add["created_at"]), "display_date": gelbooru.gelbooru_date_parse(data_add["created_at"]),
"source": data_add["source"], "source": data_add["source"],
"width": data_add["width"], "width": data_add["width"],
"height": data_add["height"] "height": data_add["height"]
} }
self.db.insert(add_data) self.add(add_data)
def get_all(self): def get_all(self):
sorted_records = sorted( cursor = self.conn.execute("SELECT * FROM files ORDER BY parsed_time DESC")
self.db.all(), return [self._row_to_dict(row) for row in cursor.fetchall()]
key=lambda x: datetime.strptime(
x["timestamp"],
"%a %b %d %H:%M:%S %z %Y"
),
reverse=True
)
return sorted_records
def get_search(self, rating_tmp, tags): def get_search(self, rating_tmp: list[str], tags: list[str]):
mapping_rating = { mapping_rating = {
"e": "explicit", "e": "explicit",
"g": "general", "g": "general",
"s": "sensitive", "s": "sensitive",
"q": "questionable" "q": "questionable"
} }
rating = [mapping_rating.get(x, x) for x in rating_tmp]
if rating: query = "SELECT * FROM files WHERE 1=1"
query = self.File.rating.one_of(rating) params = []
if tags != [""]:
tag_query = self.File.tags.test(lambda t: set(tags).issubset(set(t or []))) ratings = [mapping_rating.get(x, x) for x in rating_tmp if x]
query = tag_query if query is None else query & tag_query if ratings:
results = self.db.search(query) if query else self.db.all() placeholders = ",".join("?" * len(ratings))
sorted_records = sorted( query += f" AND rating IN ({placeholders})"
results, params.extend(ratings)
key=lambda x: datetime.strptime(
x["timestamp"], tags = [t for t in tags if t]
"%a %b %d %H:%M:%S %z %Y" if tags:
), for tag in tags:
reverse=True query += " AND EXISTS (SELECT 1 FROM json_each(files.tags) WHERE value = ?)"
) params.append(tag)
return sorted_records
query += " ORDER BY parsed_time DESC"
cursor = self.conn.execute(query, tuple(params))
return [self._row_to_dict(row) for row in cursor.fetchall()]
def get_unknow(self): def get_unknow(self):
results = self.db.search(self.File.tags == []) cursor = self.conn.execute("SELECT * FROM files WHERE tags = '[]' OR tags IS NULL")
return results return [self._row_to_dict(row) for row in cursor.fetchall()]
def get_autocomplete(self, tag_slise): def check_uni(self, md5: str):
counter = Counter() query = "SELECT 1 FROM files WHERE md5 = ? LIMIT 1"
for record in self.db.all(): cursor = self.conn.execute(query, (md5,))
counter.update(record.get("tags", [])) return cursor.fetchone() is not None
results = [
{
"tag": tag,
"count": count
}
for tag, count in counter.items()
if tag.lower().startswith(tag_slise)
]
results.sort(key=lambda x: x["count"], reverse=True)
top_10 = results[:10]
return top_10
def edit_data(self, id_file, edit_data): def get_autocomplete(self, tag_slise: str):
self.db.update(self.edit_data, self.File.md5 == self.id_file) query = """
SELECT value AS tag, COUNT(*) AS count
FROM files, json_each(files.tags)
WHERE value LIKE ?
GROUP BY value
ORDER BY count DESC
LIMIT 10
"""
cursor = self.conn.execute(query, (f"{tag_slise.lower()}%",))
return [{"tag": row["tag"], "count": row["count"]} for row in cursor.fetchall()]
def get_from_id(self, id_file): def edit_data(self, id_file: str, edit_data: dict):
return self.db.get(self.File.md5 == id_file) if not edit_data:
return
set_clauses = []
params = []
for key, value in edit_data.items():
# Если передаем списки/кортежи, пакуем в JSON
if isinstance(value, (list, tuple, dict)):
value = json.dumps(value)
set_clauses.append(f"{key} = ?")
if key == "timestamp":
params.append(value)
try:
set_clauses.append("parsed_time = ?")
params.append(int(datetime.strptime(value, "%a %b %d %H:%M:%S %z %Y").timestamp()))
except ValueError:
pass
else:
params.append(value)
params.append(id_file)
query = f"UPDATE files SET {', '.join(set_clauses)} WHERE md5 = ?"
with self.conn:
self.conn.execute(query, tuple(params))
def get_from_id(self, id_file: str):
cursor = self.conn.execute("SELECT * FROM files WHERE md5 = ?", (id_file,))
return self._row_to_dict(cursor.fetchone())
def close(self):
self.conn.close()
+104
View File
@@ -0,0 +1,104 @@
from tinydb import TinyDB, Query
import os
import json
from collections import Counter
from system_module import gelbooru
from datetime import datetime
class DB:
def __init__(self, debug: bool):
if debug:
db_path = "data_test"
else:
db_path = "data"
if not os.path.exists(os.path.join(db_path, "db.json")):
json.dump({}, open(os.path.join(db_path, "db.json"), "w", encoding="utf-8"))
self.db = TinyDB(db_path+"/db.json")
self.File = Query()
def add(self, data_add: dict):
self.db.insert(data_add)
def add_raw(self, data_add: dict):
is_video = False
is_gif = False
if data_add["image"].split(".")[-1] in ["webm", "mp4", "avi"]:
is_video = True
if data_add["image"].split(".")[-1] == "gif":
is_gif = True
add_data = {
"md5": data_add["md5"],
"local_filename": data_add["image"],
"is_video": is_video,
"is_gif": is_gif,
"tags": data_add["tags"].split(" "),
"rating": data_add["rating"],
"score": data_add["score"],
"timestamp": data_add["created_at"],
"display_date": gelbooru.gelbooru_date_parse(data_add["created_at"]),
"source": data_add["source"],
"width": data_add["width"],
"height": data_add["height"]
}
self.db.insert(add_data)
def get_all(self):
sorted_records = sorted(
self.db.all(),
key=lambda x: datetime.strptime(
x["timestamp"],
"%a %b %d %H:%M:%S %z %Y"
),
reverse=True
)
return sorted_records
def get_search(self, rating_tmp: list[str], tags: list[str]):
mapping_rating = {
"e": "explicit",
"g": "general",
"s": "sensitive",
"q": "questionable"
}
rating = [mapping_rating.get(x, x) for x in rating_tmp]
if rating:
query = self.File.rating.one_of(rating)
if tags != [""]:
tag_query = self.File.tags.test(lambda t: set(tags).issubset(set(t or [])))
query = tag_query if query is None else query & tag_query
results = self.db.search(query) if query else self.db.all()
sorted_records = sorted(
results,
key=lambda x: datetime.strptime(
x["timestamp"],
"%a %b %d %H:%M:%S %z %Y"
),
reverse=True
)
return sorted_records
def get_unknow(self):
results = self.db.search(self.File.tags == [])
return results
def get_autocomplete(self, tag_slise: str):
counter = Counter()
for record in self.db.all():
counter.update(record.get("tags", []))
results = [
{
"tag": tag,
"count": count
}
for tag, count in counter.items()
if tag.lower().startswith(tag_slise)
]
results.sort(key=lambda x: x["count"], reverse=True)
top_10 = results[:10]
return top_10
def edit_data(self, id_file: str, edit_data: dict):
self.db.update(edit_data, self.File.md5 == id_file)
def get_from_id(self, id_file: str):
return self.db.get(self.File.md5 == id_file)
+6 -7
View File
@@ -1,5 +1,6 @@
import requests import requests
from datetime import datetime from datetime import datetime
import os
def gelbooru_date_parse(date_str): def gelbooru_date_parse(date_str):
if not date_str: if not date_str:
@@ -12,22 +13,20 @@ def gelbooru_date_parse(date_str):
return 0, "Unknown" return 0, "Unknown"
class GB: class GB:
def __init__(self, api_id, api_hash, headers): def __init__(self, api_id, api_hash, headers, dw_folder):
self.api_id = api_id self.api_id = api_id
self.api_hash = api_hash self.api_hash = api_hash
self.dw_folder = dw_folder
self.root_url = "https://gelbooru.com/index.php" self.root_url = "https://gelbooru.com/index.php"
self.HEADERS = headers self.HEADERS = headers
def get_from_id(self, id_post): def get_from_id(self, id_post):
params = {"page": "dapi", "s": "post", "q": "index", "json": 1, "id": id_post, "api_key":self.api_id, "user_id":self.api_hash} params = {"page": "dapi", "s": "post", "q": "index", "json": 1, "id": id_post, "api_key":self.api_hash, "user_id":self.api_id}
response = requests.get(self.root_url, params=params, headers=self.HEADERS, timeout=15) response = requests.get(self.root_url, params=params, headers=self.HEADERS, timeout=15)
print(response.url)
url_file = response.json()["post"][0]["file_url"] url_file = response.json()["post"][0]["file_url"]
file_name = response.json()["post"][0]["image"] file_name = response.json()["post"][0]["image"]
if os.path.exists("dw/"+file_name): return response.json()["post"][0], url_file, file_name
message = f"Файл '{file_name}' уже существует. Пропускаем."
print(message)
return response.json()["post"][0], url_file, file_name
pass
def get_from_md5(self, md5): def get_from_md5(self, md5):
pass pass
+64
View File
@@ -0,0 +1,64 @@
import subprocess
from PIL import Image
import os
EXTENSIONS_IMG = ('.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp')
EXTENSIONS_VID = ('.mp4', '.webm')
VALID_EXTENSIONS = EXTENSIONS_IMG + EXTENSIONS_VID
def check_ffmpeg():
try:
if os.name == 'nt': creationflags = subprocess.CREATE_NO_WINDOW
else: creationflags = 0
subprocess.run(["ffmpeg", "-version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True, creationflags=creationflags)
return True
except (FileNotFoundError, subprocess.CalledProcessError):
return False
def video_gen(input_path_file, output_path_file, TARGET_MIN_SIZE):
vf_scale = f"scale='if(gt(iw,ih),-1,{TARGET_MIN_SIZE})':'if(gt(iw,ih),{TARGET_MIN_SIZE},-1)'"
cmd = ["ffmpeg", "-y", "-i", input_path_file, "-ss", "00:00:00", "-vframes", "1", "-vf", vf_scale, "-q:v", "2", output_path_file]
if os.name == 'nt': creationflags = subprocess.CREATE_NO_WINDOW
else: creationflags = 0
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, creationflags=creationflags)
if result.returncode != 0:
raise Exception(f"FFmpeg error: {result.stderr.decode('utf-8', errors='ignore')}")
def image_and_gif_gen(input_path_file, output_path_file, TARGET_MIN_SIZE):
with Image.open(input_path_file) as img:
img_rgba = img.convert("RGBA")
background = Image.new("RGB", img_rgba.size, (255, 255, 255))
background.paste(img_rgba, mask=img_rgba)
final_img = background
width, height = final_img.size
if width <= TARGET_MIN_SIZE and height <= TARGET_MIN_SIZE:
final_img.save(output_path_file, "JPEG", quality=90)
else:
if width < height:
new_width = TARGET_MIN_SIZE
new_height = int(TARGET_MIN_SIZE * (height / width))
else:
new_height = TARGET_MIN_SIZE
new_width = int(TARGET_MIN_SIZE * (width / height))
resized_img = final_img.resize((new_width, new_height), Image.Resampling.LANCZOS)
resized_img.save(output_path_file, "JPEG", quality=85)
def generate_thumb(input_path_file, THUMB_FOLDER, TARGET_MIN_SIZE):
ext_file = input_path_file.split(".")[-1]
if ext_file not in VALID_EXTENSIONS:
return {"status":False, "message":"Расширение не поддерживаеться!"}
thumb_filename = f"{input_path_file.split(".")[-2]}.jpg"
output_path_file = os.path.join(THUMB_FOLDER, thumb_filename)
if ext_file in EXTENSIONS_IMG:
image_and_gif_gen(input_path_file, output_path_file, TARGET_MIN_SIZE)
return {"status":True}
else:
if not check_ffmpeg(): return {"status":False, "message":"ffmpeg не установлен!"}
video_gen(input_path_file, output_path_file, TARGET_MIN_SIZE)
return {"status":True}
+6 -6
View File
@@ -19,10 +19,10 @@ class Logger:
self.logger_cli.addHandler(console_handler) self.logger_cli.addHandler(console_handler)
def send(self, level_log, type_log, message): def send(self, level_log, type_log, message):
if self.type_log == "file": if type_log == "file":
getattr(self.logger_file, level_log)(self.message) getattr(self.logger_file, level_log)(message)
elif self.type_log == "cli": elif type_log == "cli":
getattr(self.logger_cli, level_log)(self.message) getattr(self.logger_cli, level_log)(message)
else: else:
getattr(self.logger_file, level_log)(self.message) getattr(self.logger_file, level_log)(message)
getattr(self.logger_cli, level_log)(self.message) getattr(self.logger_cli, level_log)(message)