fix: system_all_log.html ReferenceError and other updates

This commit is contained in:
2026-01-17 14:06:27 +09:00
parent cf19d79ef8
commit 2681f5a096
24 changed files with 820 additions and 141 deletions

BIN
.DS_Store vendored

Binary file not shown.

17
=24.0.0
View File

@@ -1,17 +0,0 @@
Collecting greenlet
Downloading greenlet-3.3.0-cp314-cp314-macosx_11_0_universal2.whl.metadata (4.1 kB)
Collecting gevent
Downloading gevent-25.9.1-cp314-cp314-macosx_11_0_universal2.whl.metadata (14 kB)
Collecting zope.event (from gevent)
Downloading zope_event-6.1-py3-none-any.whl.metadata (5.1 kB)
Collecting zope.interface (from gevent)
Downloading zope_interface-8.1.1-cp314-cp314-macosx_11_0_arm64.whl.metadata (45 kB)
Downloading greenlet-3.3.0-cp314-cp314-macosx_11_0_universal2.whl (275 kB)
Downloading gevent-25.9.1-cp314-cp314-macosx_11_0_universal2.whl (3.0 MB)
━━━━━━━━━ 3.0/3.0 11.3 0:00:00
MB MB/s
Downloading zope_event-6.1-py3-none-any.whl (6.4 kB)
Downloading zope_interface-8.1.1-cp314-cp314-macosx_11_0_arm64.whl (209 kB)
Installing collected packages: zope.interface, zope.event, greenlet, gevent
Successfully installed gevent-25.9.1 greenlet-3.3.0 zope.event-6.1 zope.interface-8.1.1

17
=3.2.2
View File

@@ -1,17 +0,0 @@
Collecting greenlet
Downloading greenlet-3.3.0-cp314-cp314-macosx_11_0_universal2.whl.metadata (4.1 kB)
Collecting gevent
Downloading gevent-25.9.1-cp314-cp314-macosx_11_0_universal2.whl.metadata (14 kB)
Collecting zope.event (from gevent)
Downloading zope_event-6.1-py3-none-any.whl.metadata (5.1 kB)
Collecting zope.interface (from gevent)
Downloading zope_interface-8.1.1-cp314-cp314-macosx_11_0_arm64.whl.metadata (45 kB)
Downloading greenlet-3.3.0-cp314-cp314-macosx_11_0_universal2.whl (275 kB)
Downloading gevent-25.9.1-cp314-cp314-macosx_11_0_universal2.whl (3.0 MB)
━━━━━━━━━ 3.0/3.0 11.3 0:00:00
MB MB/s
Downloading zope_event-6.1-py3-none-any.whl (6.4 kB)
Downloading zope_interface-8.1.1-cp314-cp314-macosx_11_0_arm64.whl (209 kB)
Installing collected packages: zope.interface, zope.event, greenlet, gevent
Successfully installed gevent-25.9.1 greenlet-3.3.0 zope.event-6.1 zope.interface-8.1.1

View File

@@ -1,44 +1,89 @@
# FlaskFarm Docker Image
# Ubuntu 22.04 + Python 3.10 for sc module support on ARM64/x86_64 Linux
# FlaskFarm Docker Image v3.16
# Ubuntu/Debian + Python 3.14 for maximum performance
# Python 3.14.2 stable release
FROM python:3.10-slim-bullseye
FROM python:3.14-slim
LABEL maintainer="yommi"
LABEL description="FlaskFarm with sc module support"
# Install system dependencies
# Install system dependencies and Korean locales
RUN apt-get update && apt-get install -y --no-install-recommends \
ffmpeg \
git \
curl \
gcc \
python3-dev \
wget \
gnupg \
libxml2-dev \
libxslt1-dev \
zlib1g-dev \
libjpeg-dev \
libnss3 \
libatk-bridge2.0-0 \
libxcomposite1 \
libxdamage1 \
libxrandr2 \
libgbm1 \
libasound2 \
libasound2-dev \
libpangocairo-1.0-0 \
libgtk-3-0 \
pkg-config \
libbz2-dev \
libreadline-dev \
libffi-dev \
libssl-dev \
build-essential \
locales \
&& sed -i -e 's/# ko_KR.UTF-8 UTF-8/ko_KR.UTF-8 UTF-8/' /etc/locale.gen \
&& locale-gen \
&& rm -rf /var/lib/apt/lists/*
# Set working directory
WORKDIR /app
ENV LC_ALL=ko_KR.UTF-8 \
LANG=ko_KR.UTF-8 \
LANGUAGE=ko_KR.UTF-8
# Install Google Chrome Stable (amd64) or Chromium (arm64)
ARG TARGETARCH
RUN if [ "$TARGETARCH" = "amd64" ]; then \
wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | gpg --dearmor -o /usr/share/keyrings/google-chrome.gpg && \
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/google-chrome.gpg] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list && \
apt-get update && apt-get install -y google-chrome-stable; \
else \
apt-get update && apt-get install -y chromium chromium-driver; \
fi && \
rm -rf /var/lib/apt/lists/*
# Set working directory to /root
WORKDIR /root
# Copy requirements first for layer caching
COPY ff_3_10_requirements.txt .
COPY ff_3_14_requirements.txt .
# Install Python dependencies (skip FlaskFarm package - running from source)
RUN grep -v "FlaskFarm" ff_3_10_requirements.txt > requirements_docker.txt \
&& pip install --no-cache-dir -r requirements_docker.txt \
&& pip install --no-cache-dir curl_cffi yt-dlp loguru
# Install Python dependencies (including camoufox/zendriver)
RUN grep -v "FlaskFarm" ff_3_14_requirements.txt > requirements_docker.txt \
&& pip install --no-cache-dir -r requirements_docker.txt
# Copy FlaskFarm application
COPY . .
# Expose port
EXPOSE 9099
RUN mkdir -p /data/plugins /data/db
COPY gommi.sh /root/gommi.sh
COPY config.yaml /data/config.yaml
RUN chmod +x /root/gommi.sh
# Environment variables
ENV PYTHONUNBUFFERED=1
ENV TZ=Asia/Seoul
# Health check
# Health check (Matching EXPOSE port 9999)
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:9099/ || exit 1
CMD curl -f http://localhost:9999/ || exit 1
# Run FlaskFarm
CMD ["python", "main.py"]
# Expose port
EXPOSE 9999/tcp
# Run FlaskFarm via gommi.sh in /root
ENTRYPOINT ["/root/gommi.sh"]

44
Dockerfile.3.10.bak Normal file
View File

@@ -0,0 +1,44 @@
# FlaskFarm Docker Image
# Ubuntu 22.04 + Python 3.10 for sc module support on ARM64/x86_64 Linux
FROM python:3.10-slim-bullseye
LABEL maintainer="yommi"
LABEL description="FlaskFarm with sc module support"
# Install system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
ffmpeg \
git \
curl \
gcc \
python3-dev \
&& rm -rf /var/lib/apt/lists/*
# Set working directory
WORKDIR /app
# Copy requirements first for layer caching
COPY ff_3_10_requirements.txt .
# Install Python dependencies (skip FlaskFarm package - running from source)
RUN grep -v "FlaskFarm" ff_3_10_requirements.txt > requirements_docker.txt \
&& pip install --no-cache-dir -r requirements_docker.txt \
&& pip install --no-cache-dir curl_cffi yt-dlp loguru
# Copy FlaskFarm application
COPY . .
# Expose port
EXPOSE 9099
# Environment variables
ENV PYTHONUNBUFFERED=1
ENV TZ=Asia/Seoul
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:9099/ || exit 1
# Run FlaskFarm
CMD ["python", "main.py"]

24
check_status.py Normal file
View File

@@ -0,0 +1,24 @@
import sys
import os
# FF 경로 설정
sys.path.append('/Volumes/WD/Users/Work/python/flaskfarm')
from framework import F
from gds_dviewer.logic import LogicExplorer
def check():
try:
logic = LogicExplorer(None) # 인스턴스 생성 (싱글톤 패턴일 경우 기존 인스턴스 접근 필요할 수도)
# 실제로는 LogicExplorer.instance 같은 게 있는지 확인 필요
# 하지만 gds_dviewer는 보통 P.logic에 저장됨.
# P instance 가져오기
from gds_dviewer.plugin import P
indexer = P.logic.explorer.indexer
print(f"Is Running: {indexer.is_running}")
print(f"Progress: {indexer.progress}")
except Exception as e:
print(f"Error: {e}")
if __name__ == "__main__":
check()

52
cleanup_duplicates.py Normal file
View File

@@ -0,0 +1,52 @@
import sys
import os
import unicodedata
sys.path.append('/Volumes/WD/Users/Work/python/flaskfarm')
from framework import app, db
from system.logic import SystemLogic
# 플러그인 모듈 로드
from data.plugins.gds_dviewer.model_file_index import FileIndex
def cleanup_duplicates(parent_path):
with app.app_context():
# 해당 폴더의 모든 항목 조회
items = FileIndex.query.filter_by(parent_path=parent_path).all()
print(f"Total items in {parent_path}: {len(items)}")
# NFC 이름 기준으로 그룹화
groups = {}
for item in items:
nfc_name = unicodedata.normalize('NFC', item.name)
if nfc_name not in groups:
groups[nfc_name] = []
groups[nfc_name].append(item)
deleted_count = 0
for name, group in groups.items():
if len(group) > 1:
print(f"Found duplicate: {name} (Count: {len(group)})")
# 우선순위: 메타데이터 있는 것 > ID가 작은 것(오래된 것)
# 정렬: 메타데이터 있나? (내림차순 True=1, False=0), ID (오름차순)
group.sort(key=lambda x: (1 if x.meta_id else 0, -x.id), reverse=True)
# 첫 번째(가장 좋은 것)를 남기고 나머지 삭제
keep = group[0]
remove_list = group[1:]
print(f" Keep: ID={keep.id}, Meta={keep.meta_id}, Name={keep.name}")
for rm in remove_list:
print(f" REMOVE: ID={rm.id}, Meta={rm.meta_id}, Name={rm.name}")
db.session.delete(rm)
deleted_count += 1
if deleted_count > 0:
db.session.commit()
print(f"Deleted {deleted_count} duplicate items.")
else:
print("No duplicates found to delete.")
if __name__ == "__main__":
cleanup_duplicates('VIDEO/방송중/라프텔 애니메이션')

View File

@@ -1,4 +1,4 @@
path_data: '.'
path_data: '/data'
##########################################################################
# 데이터 폴더 루트 경로
# 윈도우의 경우 폴더 구분 기호 \ 를 두개 사용
@@ -7,7 +7,7 @@ path_data: '.'
#path_data: "."
# 개발용 플러그인 경로
path_dev: '/Volumes/WD/Users/Work/python/ff_dev_plugins'
path_dev: '/data/plugins'
# gevent 사용여부
# 플러그인 개발이나 termux 환경에서의 실행 같이 특수한 경우에만 false로 사용.
@@ -24,12 +24,12 @@ use_celery: true
# 포트
# 생략시 DB 값을 사용.
port: 9099
port: 9999
# 소스 수정시 재로딩
# 두번 로딩되는 것을 감안하여 코딩해야 함.
debug: true
# debug: false
# debug: true
debug: false
# 플러그인 업데이트 여부
# - true인 경우 로딩시 플러그인을 업데이트 함.
@@ -39,7 +39,7 @@ plugin_update: false
# running_type
# termux, entware 인 경우 입력 함.
running_type: 'native'
running_type: 'docker'
# 개발용 폴더만 로딩할 경우 사용
# plugin_loading_only_devpath: true
# 로딩할 플러그인 package 명

51
config_mac.yaml Normal file
View File

@@ -0,0 +1,51 @@
path_data: 'data'
##########################################################################
# 데이터 폴더 루트 경로
# 윈도우의 경우 폴더 구분 기호 \ 를 두개 사용
# 예) data_folder: "C:\\work\\data"
# 현재 폴더인 경우 .
#path_data: "."
# 개발용 플러그인 경로
path_dev: '/Volumes/WD/Users/Work/python/ff_dev_plugins/anime_downloader'
# gevent 사용여부
# 플러그인 개발이나 termux 환경에서의 실행 같이 특수한 경우에만 false로 사용.
# 실행환경에 gevent 관련 패키지가 설치되어 있지 않는다면 값과 상관 없이 false로 동작.
use_gevent: true
# celery 사용 여부
use_celery: true
# redis port
# celery를 사용하는 경우 사용하는 redis 포트
# 환경변수 REDIS_PORT 값이 있는 경우 무시됨.
#redis_port: 6379
# 포트
# 생략시 DB 값을 사용.
port: 9099
# 소스 수정시 재로딩
# 두번 로딩되는 것을 감안하여 코딩해야 함.
# debug: true
debug: true
# 플러그인 업데이트 여부
# - true인 경우 로딩시 플러그인을 업데이트 함.
# /data/plugins 폴더에 있는 플러그인 만을 대상으로 함.
# - debug 값이 true인 경우에는 항상 false
plugin_update: false
# running_type
# termux, entware 인 경우 입력 함.
running_type: 'native'
# 개발용 폴더만 로딩할 경우 사용
# plugin_loading_only_devpath: true
# 로딩할 플러그인 package 명
# 타 플러그인과 연동되는 플러그인 개발시 사용.
# import 로 런타임에 로딩할 수 있지만 타 패키지 메뉴 등은 표시되지 않음.
#plugin_loading_list: ['command', 'flaskcode']
# 로딩 제외할 플러그인 package 명
plugin_except_list: ['.idea', '.git', '.vscode', '.nova', '.mypy_cache']

0
db.sqlite Normal file
View File

View File

@@ -13,9 +13,9 @@ services:
- "9099:9099"
volumes:
# FlaskFarm data 폴더 (DB, 설정, 다운로드 등)
- ./data:/app/data
- ./data:/data
# 플러그인 폴더 (외부 마운트)
- ../ff_dev_plugins:/app/plugins
- ../ff_dev_plugins:/data/plugins
environment:
- TZ=Asia/Seoul
- PYTHONUNBUFFERED=1

97
ff_3_14_requirements.txt Normal file
View File

@@ -0,0 +1,97 @@
aiohttp>=3.9.0
aiosignal==1.2.0
amqp>=5.1.1
appdirs==1.4.4
APScheduler==3.9.1
async-generator==1.10
async-timeout==4.0.2
attrs==22.1.0
beautifulsoup4==4.11.1
bidict==0.22.0
billiard>=3.6.4.0
cattrs==22.2.0
celery>=5.4.0
certifi>=2022.9.24
charset-normalizer==2.1.1
click>=8.1.7
click-didyoumean==0.3.0
click-plugins==1.1.1
click-repl==0.2.0
cloudscraper==1.2.64
Deprecated>=1.2.14
discord-webhook==0.17.0
EditorConfig==0.12.3
exceptiongroup==1.0.0rc9
Flask>=3.0.0
Flask-Cors>=3.0.10
Flask-Dropzone>=1.6.0
Flask-Login>=0.6.3
Flask-Markdown==0.3
Flask-SocketIO>=5.3.6
Flask-SQLAlchemy>=3.0.2
FlaskFarm==4.0.47
frozenlist>=1.4.1
gevent>=24.2.1
gevent-websocket==0.10.1
greenlet>=3.2.2
h11==0.14.0
idna==3.4
importlib-metadata==5.0.0
itsdangerous>=2.1.2
Jinja2>=3.1.2
jsbeautifier==1.14.7
kombu>=5.3.0
lxml>=4.9.4
Markdown==3.4.1
MarkupSafe>=2.1.4
multidict>=6.0.5
outcome==1.2.0
packaging==21.3
Pillow>=10.0.0
prompt-toolkit==3.0.31
psutil==5.9.3
pycryptodome==3.15.0
pyparsing==3.0.9
PySocks==1.7.1
python-dotenv==0.21.0
python-engineio>=4.8.0
python-socketio>=5.10.0
pytz==2022.5
pytz-deprecation-shim==0.1.0.post0
PyYAML>=6.0.2
redis>=4.3.4
requests==2.28.1
requests-cache==0.9.6
requests-toolbelt==0.10.1
selenium>=4.20.0
selenium-stealth==1.0.6
six==1.16.0
sniffio==1.3.0
sortedcontainers==2.4.0
soupsieve==2.3.2.post1
SQLAlchemy==1.4.42
telepot-mod==0.0.1
tqdm==4.64.1
trio==0.22.0
trio-websocket==0.9.2
tzdata>=2022.5
tzlocal==4.2
url-normalize==1.4.3
urllib3==1.26.12
vine>=5.1.0
wcwidth==0.2.5
webdriver-manager>=4.0.0
Werkzeug>=3.0.0
wrapt==1.14.1
wsproto==1.2.0
yarl>=1.9.4
zipp==3.10.0
zope.event>=5.0
zope.interface>=7.0
zendriver
camoufox
curl_cffi
yt-dlp
loguru
shazamio
shazamio-core

View File

@@ -4,12 +4,21 @@
export GEVENT_NOWAITPID=1
export PYTHONWARNINGS="ignore::DeprecationWarning"
CONFIGFILE="./config.yaml"
CONFIGFILE="/data/config.yaml"
COUNT=0
# 🔧 서버 시작 전에 플러그인 업데이트
# 🌐 Camoufox 브라우저 캐시 경로 설정 (마운트된 data 폴더 사용으로 이미지 용량 절감)
export CAMOUFOX_CACHE_DIR="/data/.camoufox"
# 🔧 서버 시작 전에 플러그인 업데이트 및 브라우저 확인
update_plugins() {
PLUGINS_DIR="./data/plugins"
# Camoufox 브라우저 체크 및 다운로드 (컨테이너 최초 실행 시 1회)
if [ ! -d "$CAMOUFOX_CACHE_DIR" ]; then
echo "Fetching Camoufox binaries to $CAMOUFOX_CACHE_DIR..."
camoufox fetch
fi
PLUGINS_DIR="/data/plugins"
if [ -d "$PLUGINS_DIR" ]; then
for dir in "$PLUGINS_DIR"/*/; do
if [ -d "$dir/.git" ]; then
@@ -28,14 +37,23 @@ fi
while true;
do
python -m flaskfarm.main --repeat ${COUNT} --config ${CONFIGFILE}
echo "------------------------------------------------"
echo "Starting FlaskFarm Python Process (COUNT: ${COUNT})"
echo "Config: ${CONFIGFILE}"
echo "------------------------------------------------"
python main.py --repeat ${COUNT} --config ${CONFIGFILE}
RESULT=$?
echo "PYTHON EXIT CODE : ${RESULT}.............."
echo "------------------------------------------------"
echo "PYTHON EXIT CODE : ${RESULT}"
echo "------------------------------------------------"
if [ "$RESULT" = "1" ]; then
echo 'REPEAT....'
update_plugins # 재시작 시에도 업데이트
echo 'Restarting... (RESULT=1)'
update_plugins
else
echo 'FINISH....'
echo "Exiting... (RESULT=${RESULT})"
break
fi
COUNT=`expr $COUNT + 1`

101
gommi_mac.sh Executable file
View File

@@ -0,0 +1,101 @@
#!/bin/bash
# pyenv 초기화 (Warp/iTerm 등 비-인터랙티브 셸에서도 작동하도록)
export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
if command -v pyenv &> /dev/null; then
eval "$(pyenv init -)"
eval "$(pyenv virtualenv-init -)" 2>/dev/null
fi
# Python 3.14 + gevent fork 경고 억제
export GEVENT_NOWAITPID=1
export PYTHONWARNINGS="ignore::DeprecationWarning"
# Ctrl+C (SIGINT) 한 번에 종료되도록 설정
cleanup() {
echo ""
echo "Stopping FlaskFarm..."
# Python 프로세스 종료
if [ -n "$PYTHON_PID" ]; then
kill -TERM $PYTHON_PID 2>/dev/null
sleep 0.5
kill -9 $PYTHON_PID 2>/dev/null
fi
# 모든 자식 프로세스 종료
pkill -P $$ 2>/dev/null
exit 0
}
trap cleanup SIGINT SIGTERM
CONFIGFILE="data/config_mac.yaml"
COUNT=0
# 🌐 Camoufox 브라우저 캐시 경로 설정 및 유지
# macOS에서는 /Users/yommi/Library/Caches/camoufox가 기본값이나, 프로젝트 data 폴더로 강제 리다이렉션
CAMOUFOX_DEFAULT_DIR="$HOME/Library/Caches/camoufox"
CAMOUFOX_PERSISTENT_DIR="$(pwd)/data/.camoufox"
# 심볼릭 링크를 통해 data 폴더와 동기화
if [ ! -L "$CAMOUFOX_DEFAULT_DIR" ]; then
echo "Configuring Camoufox persistence link..."
mkdir -p "$CAMOUFOX_PERSISTENT_DIR"
if [ -d "$CAMOUFOX_DEFAULT_DIR" ] && [ ! -L "$CAMOUFOX_DEFAULT_DIR" ]; then
cp -R "$CAMOUFOX_DEFAULT_DIR/" "$CAMOUFOX_PERSISTENT_DIR/" 2>/dev/null
rm -rf "$CAMOUFOX_DEFAULT_DIR"
fi
mkdir -p "$(dirname "$CAMOUFOX_DEFAULT_DIR")"
ln -s "$CAMOUFOX_PERSISTENT_DIR" "$CAMOUFOX_DEFAULT_DIR"
fi
# 🔧 서버 시작 전에 플러그인 업데이트 및 브라우저 확인
update_plugins() {
# Camoufox 브라우저 체크 (실제 설치된 폴더 확인)
if [ ! -d "$CAMOUFOX_DEFAULT_DIR/Camoufox.app" ]; then
echo "Fetching Camoufox binaries to $CAMOUFOX_PERSISTENT_DIR..."
camoufox fetch
fi
PLUGINS_DIR="data/plugins"
if [ -d "$PLUGINS_DIR" ]; then
for dir in "$PLUGINS_DIR"/*/; do
if [ -d "$dir/.git" ]; then
echo "Updating plugin: $dir"
git -C "$dir" reset --hard HEAD 2>/dev/null
git -C "$dir" pull 2>/dev/null & # 병렬 실행
fi
done
wait # 모든 git pull 완료 대기
fi
}
# 첫 실행 시 또는 --update 옵션일 때만
if [ "$COUNT" = "0" ]; then
update_plugins
fi
while true;
do
echo "------------------------------------------------"
echo "Starting FlaskFarm Python Process (COUNT: ${COUNT})"
echo "Config: ${CONFIGFILE}"
echo "------------------------------------------------"
python main.py --repeat ${COUNT} --config ${CONFIGFILE} &
PYTHON_PID=$!
wait $PYTHON_PID
RESULT=$?
echo "------------------------------------------------"
echo "PYTHON EXIT CODE : ${RESULT}"
echo "------------------------------------------------"
if [ "$RESULT" = "1" ]; then
echo 'Restarting... (RESULT=1)'
update_plugins
else
echo "Exiting... (RESULT=${RESULT})"
break
fi
COUNT=$(expr $COUNT + 1)
done

View File

@@ -3,6 +3,7 @@ import platform
import shutil
import sys
import threading
import time
import traceback
import zipfile
@@ -110,7 +111,8 @@ class PluginManager:
F.logger.debug(plugins)
for plugin_name in plugins:
F.logger.debug(f'[+] PLUGIN LOADING Start.. [{plugin_name}]')
F.logger.info(f'[+] PLUGIN IMPORT Start.. [{plugin_name}]')
import_start_time = time.time()
entity = cls.all_package_list[plugin_name]
try:
try:
@@ -129,6 +131,9 @@ class PluginManager:
F.app.register_blueprint(mod_blue_print)
except Exception as exception:
F.logger.warning(f'[!] BLUEPRINT not exist : [{plugin_name}]')
import_elapsed_time = time.time() - import_start_time
F.logger.info(f'[+] PLUGIN IMPORT End.. [{plugin_name}] ({import_elapsed_time:.3f}s)')
cls.plugin_list[plugin_name] = entity
except Exception as e:
F.logger.error(f"Exception:{str(e)}")
@@ -155,9 +160,11 @@ class PluginManager:
if mod_plugin_load:
def func(mod_plugin_load, key):
try:
#F.logger.debug(f'[!] plugin_load_celery threading start : [{key}]')
load_start_time = time.time()
F.logger.info(f'[!] plugin_load_celery threading start : [{key}]')
mod_plugin_load()
#F.logger.debug(f'[!] plugin_load_celery threading end : [{key}]')
load_elapsed_time = time.time() - load_start_time
F.logger.info(f'[!] plugin_load_celery threading end : [{key}] ({load_elapsed_time:.3f}s)')
except Exception as e:
F.logger.error(f"Exception:{str(e)}")
F.logger.error(traceback.format_exc())
@@ -177,9 +184,11 @@ class PluginManager:
if mod_plugin_load:
def func(mod_plugin_load, key):
try:
load_start_time = time.time()
F.logger.info(f'[!] plugin_load threading start : [{key}]')
mod_plugin_load()
F.logger.debug(f'[!] plugin_load threading end : [{key}]')
load_elapsed_time = time.time() - load_start_time
F.logger.info(f'[!] plugin_load threading end : [{key}] ({load_elapsed_time:.3f}s)')
except Exception as e:
F.logger.error('### plugin_load exception : %s', key)
F.logger.error(f"Exception:{str(e)}")

View File

@@ -108,3 +108,28 @@ background-color: #ffff0080 !important;
}
.modal { overflow: scroll !important; }
/* Mobile Navigation Tightening */
@media (max-width: 768px) {
#menu_module_div {
margin-bottom: 0 !important;
}
#menu_module_div .nav-pills {
margin-bottom: 0 !important;
border-bottom: 1px solid #dee2e6;
border-radius: 0 !important;
box-shadow: none !important;
}
#menu_page_div .nav-pills {
margin-top: 0 !important;
border-radius: 0 !important;
box-shadow: 0 .125rem .25rem rgba(0,0,0,.075) !important;
}
#main_container {
padding-top: 0 !important;
margin-top: 0 !important;
}
#main_container > .d-inline-block {
display: none !important;
}
}

View File

@@ -1,82 +1,208 @@
{% extends "base.html" %}
{% block content %}
<div>
<nav>
{{ macros.m_tab_head_start() }}
{{ macros.m_tab_head('old', '이전', true) }}
{{ macros.m_tab_head('new', '실시간', false) }}
{{ macros.m_tab_head_end() }}
</nav>
<div class="tab-content" id="nav-tabContent">
{{ macros.m_tab_content_start('old', true) }}
<div>
<textarea id="log" class="col-md-12" rows="30" charswidth="23" disabled style="background-color:#ffffff;visibility:hidden"></textarea>
</div>
{{ macros.m_tab_content_end() }}
<style>
/* Unified Log Page Design (matches gds_dviewer) */
.log-wrapper {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
{{ macros.m_tab_content_start('new', false) }}
<div>
<textarea id="add" class="col-md-12" rows="30" charswidth="23" disabled style="background-color:#ffffff;visibility:visible"></textarea>
</div>
.log-card {
background: linear-gradient(145deg, rgba(20, 30, 48, 0.95), rgba(36, 59, 85, 0.9));
border: 1px solid rgba(100, 150, 180, 0.25);
border-radius: 12px;
overflow: hidden;
}
<div class="form-inline">
<label class="form-check-label" for="auto_scroll">자동 스크롤</label>
<input id="auto_scroll" name="auto_scroll" class="form-control form-control-sm" type="checkbox" data-toggle="toggle" checked>
<span class='text-left' style="padding-left:25px; padding-top:0px">
<button id="clear" class="btn btn-sm btn-outline-success">리셋</button>
</span>
</div>
{{ macros.m_tab_content_end() }}
.log-tabs {
border-bottom: 1px solid rgba(100, 150, 180, 0.2);
background: rgba(0, 0, 0, 0.2);
padding: 10px 10px 0 10px;
display: flex;
gap: 6px;
}
</div>
.log-tab {
color: #94a3b8;
border: none;
border-radius: 8px 8px 0 0;
padding: 10px 20px;
font-weight: 500;
background: transparent;
cursor: pointer;
transition: all 0.2s;
}
.log-tab:hover {
color: #e2e8f0;
background: rgba(255, 255, 255, 0.05);
}
.log-tab.active {
color: #7dd3fc;
background: rgba(20, 30, 48, 0.95);
border-bottom: 2px solid #7dd3fc;
}
.log-content {
display: none;
}
.log-content.active {
display: block;
}
.log-container {
height: calc(100vh - 200px);
min-height: 400px;
overflow-y: auto;
padding: 16px;
font-family: 'SF Mono', 'Consolas', 'Monaco', monospace;
font-size: 12px;
line-height: 1.6;
background: rgba(0, 0, 0, 0.3);
color: #94a3b8;
}
.log-line-error { color: #f87171; }
.log-line-warning { color: #fbbf24; }
.log-line-info { color: #5eead4; }
.log-line-debug { color: #94a3b8; }
.controls-bar {
display: flex;
align-items: center;
justify-content: flex-end;
padding: 12px 20px;
background: rgba(0, 0, 0, 0.2);
border-top: 1px solid rgba(100, 150, 180, 0.2);
gap: 12px;
}
.btn-log {
background: linear-gradient(180deg, rgba(45, 55, 72, 0.95), rgba(35, 45, 60, 0.98));
border: 1px solid rgba(100, 150, 180, 0.25);
color: #7dd3fc;
padding: 6px 14px;
font-size: 12px;
cursor: pointer;
border-radius: 6px;
}
.btn-log:hover {
background: linear-gradient(180deg, rgba(55, 65, 82, 0.95), rgba(45, 55, 70, 0.98));
color: #fff;
}
.log-switch {
display: flex;
align-items: center;
gap: 8px;
}
.log-switch label {
color: #94a3b8;
font-size: 12px;
font-weight: 500;
}
@media (max-width: 768px) {
.log-wrapper {
padding: 5px;
}
.log-container {
height: calc(100vh - 180px);
min-height: 300px;
padding: 12px;
}
}
</style>
<div class="log-wrapper">
<div class="log-card">
<div class="log-tabs">
<button class="log-tab active" data-tab="old">이전</button>
<button class="log-tab" data-tab="new">실시간</button>
</div>
<div class="log-content active" id="tab-old">
<div class="log-container" id="log-history"></div>
</div>
<div class="log-content" id="tab-new">
<div class="log-container" id="log-realtime"></div>
<div class="controls-bar">
<div class="log-switch">
<label for="auto_scroll">자동 스크롤</label>
<input id="auto_scroll" name="auto_scroll" type="checkbox" checked>
</div>
<button id="clear" class="btn-log">리셋</button>
</div>
</div>
</div>
</div>
<script type="text/javascript">
$(document).ready(function() {
setWide();
$('#loading').show();
ResizeTextAreaLog()
})
function ResizeTextAreaLog() {
ClientHeight = window.innerHeight
$("#log").height(ClientHeight-240);
$("#add").height(ClientHeight-260);
function escapeHtml(text) {
var div = document.createElement('div');
div.appendChild(document.createTextNode(text));
return div.innerHTML;
}
$(window).resize(function() {
ResizeTextAreaLog();
function formatLogLine(line) {
var className = '';
if (line.indexOf('ERROR') !== -1) className = 'log-line-error';
else if (line.indexOf('WARNING') !== -1) className = 'log-line-warning';
else if (line.indexOf('INFO') !== -1) className = 'log-line-info';
else if (line.indexOf('DEBUG') !== -1) className = 'log-line-debug';
return '<div class="' + className + '">' + escapeHtml(line) + '</div>';
}
// Tab switching
document.querySelectorAll('.log-tab').forEach(function(tab) {
tab.addEventListener('click', function() {
document.querySelectorAll('.log-tab').forEach(function(t) { t.classList.remove('active'); });
document.querySelectorAll('.log-content').forEach(function(c) { c.classList.remove('active'); });
this.classList.add('active');
document.getElementById('tab-' + this.dataset.tab).classList.add('active');
});
});
$(document).ready(function() {
setWide();
$('#loading').show();
});
var protocol = window.location.protocol;
var socket = io.connect(protocol + "//" + document.domain + ":" + location.port + "/log");
socket.emit("start", {'package':'{{package}}'} );
socket.on('on_start', function(data){
document.getElementById("log").innerHTML += data.data;
document.getElementById("log").scrollTop = document.getElementById("log").scrollHeight;
document.getElementById("log").style.visibility = 'visible';
$('#loading').hide();
socket.emit("start", {'package':'{{package}}'});
socket.on('on_start', function(data) {
var container = document.getElementById("log-history");
var lines = data.data.split('\n');
var html = '';
for (var i = 0; i < lines.length; i++) {
html += formatLogLine(lines[i]);
}
container.innerHTML = html || '<div style="text-align:center;color:#64748b;">로그가 비어 있습니다.</div>';
container.scrollTop = container.scrollHeight;
$('#loading').hide();
});
socket.on('add', function(data){
if (data.package == "{{package}}") {
var chk = $('#auto_scroll').is(":checked");
document.getElementById("add").innerHTML += data.data;
if (chk) document.getElementById("add").scrollTop = document.getElementById("add").scrollHeight;
}
socket.on('add', function(data) {
if (data.package == "{{package}}") {
var chk = $('#auto_scroll').is(":checked");
var container = document.getElementById("log-realtime");
container.innerHTML += formatLogLine(data.data);
if (chk) container.scrollTop = container.scrollHeight;
}
});
$("#clear").click(function(e) {
e.preventDefault();
document.getElementById("add").innerHTML = '';
e.preventDefault();
document.getElementById("log-realtime").innerHTML = '';
});
$("#auto_scroll").click(function(){
var chk = $(this).is(":checked");//.attr('checked');
});
</script>
{% endblock %}

View File

@@ -132,14 +132,15 @@ def default_route(P):
@P.blueprint.route('/ajax/<module_name>/<cmd>', methods=['GET', 'POST'])
@login_required
def second_ajax(module_name, cmd):
# P.logger.debug(f"[CORE-DEBUG] second_ajax: package={P.package_name}, module={module_name}, cmd={cmd}")
try:
for module in P.module_list:
if cmd == 'scheduler':
go = request.form['scheduler']
if go == 'true':
P.logic.scheduler_start(module_name)
P.logic.scheduler_start(module_name)
else:
P.logic.scheduler_stop(module_name)
P.logic.scheduler_stop(module_name)
return jsonify(go)
elif cmd == 'db_delete':
day = request.form['day']

View File

@@ -53,11 +53,26 @@ $(window).resize(function() {
});
function escapeHtml(text) {
var div = document.createElement('div');
div.appendChild(document.createTextNode(text));
return div.innerHTML;
}
function formatLogLine(line) {
var className = '';
if (line.indexOf('ERROR') !== -1) className = 'text-danger';
else if (line.indexOf('WARNING') !== -1) className = 'text-warning';
else if (line.indexOf('INFO') !== -1) className = 'text-info';
else if (line.indexOf('DEBUG') !== -1) className = 'text-muted';
return '<div class="' + className + '" style="white-space: pre-wrap; font-family: monospace; font-size: 13px;">' + escapeHtml(line) + '</div>';
}
socket.on('on_start', function(data){
lines = splitLines(data.data);
var lines = data.data.split('\n');
var html = '';
for (i in lines) {
html += logline(lines[i]);
for (var i in lines) {
html += formatLogLine(lines[i]);
}
$('#log_div').html(html)
document.getElementById("log_div").scrollTop = document.getElementById("log_div").scrollHeight;
@@ -66,7 +81,7 @@ socket.on('on_start', function(data){
socket.on('add', function(data){
if (data.filename == current_filename) {
$('#log_div').append(logline(data.data.trim()));
$('#log_div').append(formatLogLine(data.data.trim()));
document.getElementById("log_div").scrollTop = document.getElementById("log_div").scrollHeight;
}
});

13
main.py
View File

@@ -3,12 +3,21 @@
monkey.patch_all()
print("[MAIN] gevent mokey patch!!")
except Exception:
print("[MAIN] gevent not installed!!")
except Exception as e:
print(f"[MAIN] gevent not installed or failed to load: {e}")
# import traceback; traceback.print_exc()
import os
import sys
import traceback
import flask
import markupsafe
flask.Markup = markupsafe.Markup
sys.modules['flask.Markup'] = markupsafe.Markup # Backup for some import styles
# Injecting into flask module members for 'from flask import Markup'
if not hasattr(flask, 'Markup'):
setattr(flask, 'Markup', markupsafe.Markup)
try:
import platform

43
manual_migration.py Normal file
View File

@@ -0,0 +1,43 @@
import sqlite3
import os
db_path = '/Volumes/WD/Users/Work/python/flaskfarm/data/db/gds_dviewer.db'
table_name = 'gds_dviewer_file_index'
print(f"Checking DB: {db_path}")
if not os.path.exists(db_path):
print("Error: DB file not found")
exit(1)
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
# Check columns
cursor.execute(f"PRAGMA table_info({table_name})")
columns = [row[1] for row in cursor.fetchall()]
print(f"Current columns: {columns}")
# Add columns if missing
changed = False
if 'meta_id' not in columns:
print(f"Adding meta_id to {table_name}")
cursor.execute(f"ALTER TABLE {table_name} ADD COLUMN meta_id VARCHAR(64)")
try:
cursor.execute(f"CREATE INDEX ix_{table_name}_meta_id ON {table_name} (meta_id)")
print("Created index for meta_id")
except Exception as e:
print(f"Index creation hint/error: {e}")
changed = True
if 'meta_poster' not in columns:
print(f"Adding meta_poster to {table_name}")
cursor.execute(f"ALTER TABLE {table_name} ADD COLUMN meta_poster VARCHAR(1024)")
changed = True
if changed:
conn.commit()
print("Migration successful")
else:
print("No changes needed")
conn.close()

16
restart_mac.sh Executable file
View File

@@ -0,0 +1,16 @@
#!/bin/bash
# FlaskFarm 재시작 스크립트
# 사용법: ./restart.sh
cd /Volumes/WD/Users/Work/python/flaskfarm
# 기존 FlaskFarm 프로세스 종료
pkill -9 -f "python main.py" 2>/dev/null
lsof -ti :9099 | xargs kill -9 2>/dev/null
# 잠시 대기
sleep 1
# 백그라운드 없이 재시작 (터미널에서 직접 실행용)
echo "FlaskFarm 시작 중..."
./gommi_mac.sh

37
test_metadata_search.py Normal file
View File

@@ -0,0 +1,37 @@
import sys
import os
import traceback
# Add dev plugins path to sys.path
dev_path = '/Volumes/WD/Users/Work/python/ff_dev_plugins'
if dev_path not in sys.path:
sys.path.insert(0, dev_path)
# Mocking Logger and F for direct import test
class MockLogger:
def debug(self, msg, *args): print(f"DEBUG: {msg % args if args else msg}")
def info(self, msg, *args): print(f"INFO: {msg % args if args else msg}")
def error(self, msg, *args): print(f"ERROR: {msg % args if args else msg}")
def warning(self, msg, *args): print(f"WARNING: {msg % args if args else msg}")
import logging
logging.basicConfig(level=logging.DEBUG)
# We need to set up the environment so metadata can be imported
os.environ['PYTHONPATH'] = dev_path
try:
# Try importing directly from the plugin
# metadata/mod_ftv.py contains ModuleFtv
from metadata.mod_ftv import ModuleFtv
from support_site import SiteTmdbFtv
print("Searching for '비밀의 아이프리' with year 2024...")
# SiteTmdbFtv.search is a classmethod
result = SiteTmdbFtv.search('비밀의 아이프리', year=2024)
print(f"Result: {result}")
except Exception as e:
print(f"Error: {e}")
traceback.print_exc()