Compare commits

...

10 Commits

Author SHA1 Message Date
526bd36c5a Gunicorn Transition: add wsgi entrypoint and gunicorn config, bump version to 4.1.41 2026-01-27 16:01:56 +09:00
def4e9eeb8 feat: 유튜브 검색 정렬 기능 추가 및 인피니티 스크롤 개선
- 검색 정렬 옵션 추가 (최신순, 조회수순, 관련성순)
- 기본 정렬을 최신순으로 변경
- 인피니티 스크롤 최적화 (첫 20개 빠르게 로드, 스크롤 시 추가)
- extract_flat 파라미터 추가로 검색 성능 개선
- 서버 캐시로 중복 요청 방지
- UI 개선: 검색 결과 종료 메시지 추가
- 버전 업데이트: 0.1.2 → 0.2.0
2026-01-26 16:47:23 +09:00
6e747abf86 Add framework.logger compatibility shim 2026-01-19 21:14:55 +09:00
2512161203 Fix plugin loading sync and resolve regression in setting_menu; bump version 2026-01-17 14:41:12 +09:00
2681f5a096 fix: system_all_log.html ReferenceError and other updates 2026-01-17 14:06:27 +09:00
cf19d79ef8 feat: Install gevent and its dependencies, and add environment variables to suppress gevent fork warnings in gommi.sh. 2026-01-03 20:26:38 +09:00
3a9765f7ea feat: Add Docker support and debug logging for plugin initialization. 2026-01-01 22:57:40 +09:00
97d203cb86 feat: add restart script and update configuration 2025-12-29 02:17:49 +09:00
0c53e1d2f2 수정사항 2025-12-26 22:22:08 +09:00
af9a38a973 linkkf 로직수정중 2025-12-25 19:42:32 +09:00
155 changed files with 10149 additions and 1549 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

21
.dockerignore Normal file
View File

@@ -0,0 +1,21 @@
# Docker 무시 파일
# 빌드 시 컨테이너에 복사하지 않을 파일들
.git
.gitignore
.idea
.vscode
.venv
__pycache__
*.pyc
*.pyo
*.pyd
.DS_Store
.python-version
# 데이터 폴더는 볼륨으로 마운트
data/
# 개발용 파일
*.md
*.log

4
.gitignore vendored
View File

@@ -129,7 +129,7 @@ dmypy.json
# FlaksFarm # FlaksFarm
config.yaml # config.yaml
lib2/ lib2/
.vscode/ .vscode/
memo.txt memo.txt
@@ -141,7 +141,7 @@ pre_start.sh
*.code-workspace *.code-workspace
false false
*copy.py *copy.py
*.sh # *.sh
data/ data/
tmp/ tmp/
lib/support/site/tving.py lib/support/site/tving.py

8
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

12
.idea/dataSources.xml generated Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="linkkf" uuid="8b6bf041-ffab-472b-b603-18b3316bc628">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/data/db/linkkf.db</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>

20
.idea/flaskfarm.iml generated Normal file
View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="jdk" jdkName="Python 3.9 (FF)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">
<option name="format" value="PLAIN" />
<option name="myDocStringFormat" value="Plain" />
</component>
<component name="TemplatesService">
<option name="TEMPLATE_CONFIGURATION" value="Jinja2" />
<option name="TEMPLATE_FOLDERS">
<list>
<option value="$MODULE_DIR$/lib/framework/templates" />
</list>
</option>
</component>
</module>

View File

@@ -0,0 +1,76 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="HttpUrlsUsage" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="ignoredUrls">
<list>
<option value="http://localhost" />
<option value="http://127.0.0.1" />
<option value="http://0.0.0.0" />
<option value="http://www.w3.org/" />
<option value="http://json-schema.org/draft" />
<option value="http://java.sun.com/" />
<option value="http://xmlns.jcp.org/" />
<option value="http://javafx.com/javafx/" />
<option value="http://javafx.com/fxml" />
<option value="http://maven.apache.org/xsd/" />
<option value="http://maven.apache.org/POM/" />
<option value="http://www.springframework.org/schema/" />
<option value="http://www.springframework.org/tags" />
<option value="http://www.springframework.org/security/tags" />
<option value="http://www.thymeleaf.org" />
<option value="http://www.jboss.org/j2ee/schema/" />
<option value="http://www.jboss.com/xml/ns/" />
<option value="http://www.ibm.com/webservices/xsd" />
<option value="http://activemq.apache.org/schema/" />
<option value="http://schema.cloudfoundry.org/spring/" />
<option value="http://schemas.xmlsoap.org/" />
<option value="http://cxf.apache.org/schemas/" />
<option value="http://primefaces.org/ui" />
<option value="http://tiles.apache.org/" />
<option value="http://yommi.duckdns.org" />
</list>
</option>
</inspection_tool>
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoredPackages">
<value>
<list size="17">
<item index="0" class="java.lang.String" itemvalue="trio-websocket" />
<item index="1" class="java.lang.String" itemvalue="h11" />
<item index="2" class="java.lang.String" itemvalue="loguru" />
<item index="3" class="java.lang.String" itemvalue="sniffio" />
<item index="4" class="java.lang.String" itemvalue="sqlalchemy" />
<item index="5" class="java.lang.String" itemvalue="wsproto" />
<item index="6" class="java.lang.String" itemvalue="attrs" />
<item index="7" class="java.lang.String" itemvalue="sortedcontainers" />
<item index="8" class="java.lang.String" itemvalue="exceptiongroup" />
<item index="9" class="java.lang.String" itemvalue="trio" />
<item index="10" class="java.lang.String" itemvalue="selenium" />
<item index="11" class="java.lang.String" itemvalue="certifi" />
<item index="12" class="java.lang.String" itemvalue="pysocks" />
<item index="13" class="java.lang.String" itemvalue="urllib3" />
<item index="14" class="java.lang.String" itemvalue="async-generator" />
<item index="15" class="java.lang.String" itemvalue="outcome" />
<item index="16" class="java.lang.String" itemvalue="idna" />
</list>
</value>
</option>
</inspection_tool>
<inspection_tool class="PyPep8NamingInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="ignoredErrors">
<list>
<option value="N802" />
<option value="N803" />
</list>
</option>
</inspection_tool>
<inspection_tool class="PyUnresolvedReferencesInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoredIdentifiers">
<list>
<option value="sqlalchemy.engine.result.Result.__await__" />
</list>
</option>
</inspection_tool>
</profile>
</component>

View File

@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

4
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.9 (FF)" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/flaskfarm.iml" filepath="$PROJECT_DIR$/.idea/flaskfarm.iml" />
</modules>
</component>
</project>

View File

@@ -0,0 +1,9 @@
c python:S6019"FFix this reluctant quantifier that will only ever match 0 repetitions.(¨†…™üÿÿÿÿ
c python:S6019 "FFix this reluctant quantifier that will only ever match 0 repetitions.(Ìé©èùÿÿÿÿ
c python:S6019"FFix this reluctant quantifier that will only ever match 0 repetitions.(–…‡éþÿÿÿÿ
^ python:S6019"FFix this reluctant quantifier that will only ever match 0 repetitions.(ëÅÑš
^ python:S6019"FFix this reluctant quantifier that will only ever match 0 repetitions.(‹Ò•–
6 python:S125!"Remove this commented out code.(¡‚ÙÈ
T python:S5754)"<Specify an exception class to catch or reraise the exception(ˆÊÉ·
6 python:S1252"Remove this commented out code.(ý<>™Ë

View File

@@ -0,0 +1,6 @@
q python:S3776
"TRefactor this function to reduce its Cognitive Complexity from 22 to the 15 allowed.(úÈœ‚ÿÿÿÿÿ
6 python:S125"Remove this commented out code.(ÑêÏà
T python:S5754/"<Specify an exception class to catch or reraise the exception(ˆÊÉ·
6 python:S125("Remove this commented out code.(£ÌÎæ

View File

@@ -0,0 +1,2 @@
l python:S3776"TRefactor this function to reduce its Cognitive Complexity from 25 to the 15 allowed.(”Ò­Ñ

View File

@@ -0,0 +1,10 @@
e python:S1192'"MDefine a constant instead of duplicating this literal 'Exception:%s' 9 times.(ï°Ð½
L python:S10669"/Merge this if statement with the enclosing one.(Íìáöüÿÿÿÿ
l python:S3776"TRefactor this function to reduce its Cognitive Complexity from 50 to the 15 allowed.(<28>Øê­
6 python:S125/"Remove this commented out code.(“Ä¡–
l python:S3776k"TRefactor this function to reduce its Cognitive Complexity from 39 to the 15 allowed.(ûãô”
r python:S3776á"TRefactor this function to reduce its Cognitive Complexity from 58 to the 15 allowed.(ó‚‚åýÿÿÿÿ
U python:S5754ü"<Specify an exception class to catch or reraise the exception(ˆÊÉ·
< python:S125¼"Remove this commented out code.(ÁêÕúûÿÿÿÿ
U python:S5754é"<Specify an exception class to catch or reraise the exception(ˆÊÉ·

View File

@@ -0,0 +1,7 @@
B python:S1481"%Remove the unused local variable "e".(¼–¯¸ÿÿÿÿÿ
6 python:S125 "Remove this commented out code.(™èÇÅ
; python:S125"Remove this commented out code.(à髈üÿÿÿÿ
6 python:S125T"Remove this commented out code.(»—Ö
C python:S5806Y"+Rename this variable; it shadows a builtin.(Ç­¡¡
6 python:S125Z"Remove this commented out code.(¾§Ç¡

View File

@@ -0,0 +1,33 @@
e python:S5797I"HReplace this expression; used as a condition it will always be constant.(™¼ï€üÿÿÿÿ
M python:S1066ã"/Merge this if statement with the enclosing one.(„Õý“þÿÿÿÿ
v python:S1163"ZRename this field "SystemModelSetting" to match the regular expression ^[_a-z][_a-z0-9]*$.(òªÆÓýÿÿÿÿ
b python:S1164"KRename this field "Job" to match the regular expression ^[_a-z][_a-z0-9]*$.(„è´’
r python:S116Ù"URename this field "PluginManager" to match the regular expression ^[_a-z][_a-z0-9]*$.(æ¥Ë÷ùÿÿÿÿ
6 python:S125/"Remove this commented out code.(´ÔÔí
6 python:S125_"Remove this commented out code.(„ÿ’È
6 python:S125"Remove this commented out code.(æ‰Èì
[ python:S112">Replace this generic exception class with a more specific one.(¢…›°þÿÿÿÿ
y python:S1186¯"[Add a nested comment explaining why this function is empty, or complete the implementation.(³‡Êºþÿÿÿÿ
| python:S117Á"_Rename this local variable "SystemInstance" to match the regular expression ^[_a-z][a-z0-9_]*$.(çÉÍ¥úÿÿÿÿ
r python:S3776÷"TRefactor this function to reduce its Cognitive Complexity from 26 to the 15 allowed.(<28><>†øÿÿÿÿ
7 python:S125ü"Remove this commented out code.(ðû<C3B0>
A python:S108")Either remove or fill this block of code.(ã‘Û¾
U python:S5754 "<Specify an exception class to catch or reraise the exception(ˆÊÉ·
< python:S125¼"Remove this commented out code.(ÛÑ“¹üÿÿÿÿ
r python:S3776Ç"TRefactor this function to reduce its Cognitive Complexity from 21 to the 15 allowed.(û´´¨ýÿÿÿÿ
< python:S125Ê"Remove this commented out code.(ˆà¡×ÿÿÿÿÿ
U python:S5754þ"<Specify an exception class to catch or reraise the exception(ˆÊÉ·
t python:S117²"\Rename this local variable "fileHandler" to match the regular expression ^[_a-z][a-z0-9_]*$.(ºŽ™…
v python:S117"^Rename this local variable "streamHandler" to match the regular expression ^[_a-z][a-z0-9_]*$.(<28>½ÝÀ
m python:S3776<18>"TRefactor this function to reduce its Cognitive Complexity from 24 to the 15 allowed.(¸ì¶Ê
U python:S5754¢"<Specify an exception class to catch or reraise the exception(ˆÊÉ·
U python:S5754"<Specify an exception class to catch or reraise the exception(ˆÊÉ·
n python:S1542¥"PRename function "customTime" to match the regular expression ^[a-z_][a-z0-9_]*$.(ŠˆíÖúÿÿÿÿ
U python:S5754À"<Specify an exception class to catch or reraise the exception(Ê<E28099>
U python:S5754Â"<Specify an exception class to catch or reraise the exception(Ê<E28099>
U python:S5754Ä"<Specify an exception class to catch or reraise the exception(Ê<E28099>
U python:S5754Æ"<Specify an exception class to catch or reraise the exception(Ê<E28099>
U python:S5754È"<Specify an exception class to catch or reraise the exception(Ê<E28099>
U python:S5754Ï"<Specify an exception class to catch or reraise the exception(ˆÊÉ·
U python:S5754<18>"<Specify an exception class to catch or reraise the exception(ˆÊÉ·

17
.idea/sonarlint/issuestore/index.pb generated Normal file
View File

@@ -0,0 +1,17 @@
I
lib/framework/__init__.py,0/3/03875fef6dc33ed50c8bc5f25df52c02e352a134
M
lib/framework/init_declare.py,9/5/95519e06d92ec11e26cf873c3c30e2fcedf78892
J
lib/framework/init_menu.py,6/2/627433fe5c5c7210e3062642e7963227a319d5c6
L
lib/framework/init_plugin.py,8/2/82544e7bcd3de23afaf278a62d1180d65a1ef456
K
lib/framework/init_route.py,5/8/58836750c643ef469da17133f44914292a82f3b3
I
lib/framework/init_web.py,0/e/0e68783ed60c8d2f67617374a92c1652fb6bdaee
K
lib/framework/log_viewer.py,0/c/0c528d2f014ab7c32dd27d4e5d79396e74f3e62c
J
lib/framework/init_main.py,e/e/eeb13886aba87bd6947610e1cba3283d308baf0c

13
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$/data/plugins/ffmpeg" vcs="Git" />
<mapping directory="$PROJECT_DIR$/data/plugins/flaskcode" vcs="Git" />
<mapping directory="$PROJECT_DIR$/data/plugins/klive_plus" vcs="Git" />
<mapping directory="$PROJECT_DIR$/data/plugins/number_baseball" vcs="Git" />
<mapping directory="$PROJECT_DIR$/data/plugins/sjva" vcs="Git" />
<mapping directory="$PROJECT_DIR$/data/plugins/terminal" vcs="Git" />
<mapping directory="$PROJECT_DIR$/data/plugins/trans" vcs="Git" />
</component>
</project>

89
Dockerfile Normal file
View File

@@ -0,0 +1,89 @@
# FlaskFarm Docker Image v3.16
# Ubuntu/Debian + Python 3.14 for maximum performance
# Python 3.14.2 stable release
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/*
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_14_requirements.txt .
# 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 . .
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 (Matching EXPOSE port 9999)
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:9999/ || exit 1
# 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"]

74
all_files.txt Normal file
View File

@@ -0,0 +1,74 @@
너 따위가 마왕을 이길 수 있다고 생각하지 마 라며 용사 파티에서 추방되었으니 왕도에서 멋대로 살고 싶다 (2026)
내가 연인이 될 수 있을 리 없잖아, 무리무리! (※무리가 아니었다 !) 속편 (2026)
소꿉친구와는 러브 코미디를 할 수 없어 (2026)
어느 날 공주가 되어 버렸다 - 중국어 (2025)
타몬 군 지금 어느 쪽 ! (2026)
치토세 군은 라무네 병 속에 (2025)
친구의 여동생이 나한테만 짜증나게 군다 (2025)
토지마 탄자부로는 가면라이더가 되고 싶어 (2025)
이세계 사정은 사축 하기 나름 (2026)
무사태평 영주의 즐거운 영지 방어 ~생산계 마법으로 이름 없는 마을을 최강의 성채 도시로~ (2026)
용사 파티에 귀여운 애가 있어서, 고백해봤다. (2026)
내가 너무 귀여운 걸 어쩌겠어! (2025)
더 파이팅 뉴 챌린저 (2009)
볼룸에 오신 것을 환영합니다 (2017)
아르마는 가족이 되고 싶어 (2025)
미소가 끊이지 않는 직장입니다 (2025)
악식 영애와 광혈 공작 (2025)
온화한 귀족의 휴가의 권장 (2026)
용사 파티에서 쫓겨난 다재무능 (2026)
밤은 고양이와 함께 시즌 3 (2024)
미남 고교 지구방위부 하이칼라! (2025)
마왕의 딸은 너무 친절해!! (2026)
사망 유희로 밥을 먹는다. (2026)
정반대의 너와 나 (2026)
마술사 쿠논은 보인다 (2026)
고문 아르바이트의 일상 (2026)
카야는 무섭지 않아 (2026)
페이트 스트레인지 페이크 (2026)
전생했더니 드래곤의 알이었다 ~최강이 아니면 목표로 하지 않아~ (2026)
공주님 “고문“의 시간입니다 2기 (2026)
나를 먹고 싶은, 괴물 (2025)
비밀의 아이프리 (2024)
마루는 강쥐 (2025)
울트라맨 오메가 (2025)
트라이건 스타게이즈 (2026)
아름다운 그대에게 (2026)
아름다운 초저녁달 (2026)
용사형에 처함 (2026)
데드 어카운트 (2026)
에리스의 성배 (2026)
한밤중 하트튠 (2026)
헬 모드 ~파고들기 좋아하는 게이머는 폐급 설정 이세계에서 무쌍한다~ (2026)
귀족 전생 ~축복받은 태생으로 최강의 힘을 손에 넣다~ (2026)
투명남과 인간녀 ~곧 부부가 될 두 사람~ (2026)
라디앙 시즌 2 (2019)
마도정병의 슬레이브 2 (2026)
【최애의 아이】 3기 (2026)
불꽃 소방대 3기 part 2 (2026)
무한의 주인 IMMORTAL (2019)
푸른 오케스트라 Season 2 (2025)
불멸의 그대에게 Season 3 (2025)
와타리 군의 XX가 붕괴 직전 (2025)
비질랜티 -나의 히어로 아카데미아 ILLEGALS- Season 2 (2026)
아빠는 영웅, 엄마는 정령, 딸인 나는 전생자. (2025)
그노시아 (2025)
가치아쿠타 (2025)
위국일기 (2026)
주술회전 3기 사멸회유 전편 (2025)
주술회전 3기 사멸회유 전편 (2026)
원펀맨 3기 (2025)
용족 Ⅱ-The Mourner's Eyes- - 중국어 (2025)
용족 Ⅱ-The Mourner's Eyes- - 일본어 (2025)
어차피, 사랑하고 만다. 2기 (2026)
안녕! 틴틴팅클 (2025)
(자막) 달의 요정 세일러 문 (2021)
(더빙) 카쿠리요의 여관밥 (2018)
(더빙) 디지몬 비트브레이크 (2025)
(자막) 장송의 프리렌 2기 (2026)
(자막) 원피스 33기 (2024)
(더빙) 하나Doll (2025)
DARK MOON 달의 제단 (2026)
SI-VIS The Sound of Heroes (2025)
WORKING!!! (3기) (2015)
WWW.WORKING!! (2016)

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/방송중/라프텔 애니메이션')

43
cli/encode.py Normal file
View File

@@ -0,0 +1,43 @@
import argparse
import os
import sys
sys.path.append(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'lib'))
from support import SupportFile, SupportSC, logger
class Encode:
def start_folder(self, folderpath):
for name in os.listdir(folderpath):
filepath = os.path.join(folderpath, name)
if os.path.isfile(filepath) and name not in ['setup.py', '__init__.py']:
self.encode_file(filepath)
def encode_file(self, filepath):
text = SupportFile.read_file(filepath)
data = SupportSC.encode(text, 0)
SupportFile.write_file(filepath + 'f', data)
logger.info(f"Create {os.path.basename(filepath + 'f')}")
def process_args(self):
parser = argparse.ArgumentParser()
parser.add_argument('--mode', default='encode')
parser.add_argument('--source', required=True, help=u'absolute path. folder or file')
args = parser.parse_args()
if SupportSC.LIBRARY_LOADING == False:
logger.error("sc import fail")
return
if os.path.exists(args.source):
if os.path.isdir(args.source):
self.start_folder(args.source)
elif os.path.isfile(args.source):
self.encode_file(args.source)
else:
logger.error("wrong source path!!")
if __name__== "__main__":
Encode().process_args()

51
config.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'
# gevent 사용여부
# 플러그인 개발이나 termux 환경에서의 실행 같이 특수한 경우에만 false로 사용.
# 실행환경에 gevent 관련 패키지가 설치되어 있지 않는다면 값과 상관 없이 false로 동작.
use_gevent: true
# celery 사용 여부
use_celery: true
# redis port
# celery를 사용하는 경우 사용하는 redis 포트
# 환경변수 REDIS_PORT 값이 있는 경우 무시됨.
#redis_port: 6379
# 포트
# 생략시 DB 값을 사용.
port: 9999
# 소스 수정시 재로딩
# 두번 로딩되는 것을 감안하여 코딩해야 함.
# debug: true
debug: false
# 플러그인 업데이트 여부
# - true인 경우 로딩시 플러그인을 업데이트 함.
# /data/plugins 폴더에 있는 플러그인 만을 대상으로 함.
# - debug 값이 true인 경우에는 항상 false
plugin_update: false
# running_type
# termux, entware 인 경우 입력 함.
running_type: 'docker'
# 개발용 폴더만 로딩할 경우 사용
# plugin_loading_only_devpath: true
# 로딩할 플러그인 package 명
# 타 플러그인과 연동되는 플러그인 개발시 사용.
# import 로 런타임에 로딩할 수 있지만 타 패키지 메뉴 등은 표시되지 않음.
#plugin_loading_list: ['command', 'flaskcode']
# 로딩 제외할 플러그인 package 명
plugin_except_list: ['.idea', '.git', '.vscode', '.nova', '.mypy_cache']

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

23
docker-compose.yml Normal file
View File

@@ -0,0 +1,23 @@
# FlaskFarm Docker Compose
# Usage:
# docker compose up -d # 시작
# docker compose down # 중지
# docker compose logs -f # 로그 보기
services:
flaskfarm:
build: .
container_name: flaskfarm
restart: unless-stopped
ports:
- "9099:9099"
volumes:
# FlaskFarm data 폴더 (DB, 설정, 다운로드 등)
- ./data:/data
# 플러그인 폴더 (외부 마운트)
- ../ff_dev_plugins:/data/plugins
environment:
- TZ=Asia/Seoul
- PYTHONUNBUFFERED=1
# M1/M2 Mac에서 ARM64 Linux 이미지 사용
platform: linux/arm64

90
ff_3_10_requirements.txt Normal file
View File

@@ -0,0 +1,90 @@
aiohttp==3.8.3
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.2.7
certifi==2022.9.24
charset-normalizer==2.1.1
click==8.1.3
click-didyoumean==0.3.0
click-plugins==1.1.1
click-repl==0.2.0
cloudscraper==1.2.64
Deprecated==1.2.13
discord-webhook==0.17.0
EditorConfig==0.12.3
exceptiongroup==1.0.0rc9
Flask==2.2.2
Flask-Cors==3.0.10
Flask-Dropzone==1.6.0
Flask-Login==0.6.2
Flask-Markdown==0.3
Flask-SocketIO==5.3.1
Flask-SQLAlchemy==3.0.2
FlaskFarm==4.0.47
frozenlist==1.3.1
gevent==22.10.1
gevent-websocket==0.10.1
greenlet==1.1.3.post0
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.2.4
lxml==4.9.1
Markdown==3.4.1
MarkupSafe==2.1.1
multidict==6.0.2
outcome==1.2.0
packaging==21.3
Pillow==9.2.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.3.4
python-socketio==5.7.2
pytz==2022.5
pytz-deprecation-shim==0.1.0.post0
PyYAML==6.0
redis==4.3.4
requests==2.28.1
requests-cache==0.9.6
requests-toolbelt==0.10.1
selenium==4.5.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.0.0
wcwidth==0.2.5
webdriver-manager==3.8.4
Werkzeug==2.2.2
wrapt==1.14.1
wsproto==1.2.0
yarl==1.8.1
zipp==3.10.0
zope.event==4.5.0
zope.interface==5.5.0

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

74
files.txt Normal file
View File

@@ -0,0 +1,74 @@
너 따위가 마왕을 이길 수 있다고 생각하지 마 라며 용사 파티에서 추방되었으니 왕도에서 멋대로 살고 싶다 (2026)
내가 연인이 될 수 있을 리 없잖아, 무리무리! (※무리가 아니었다 !) 속편 (2026)
소꿉친구와는 러브 코미디를 할 수 없어 (2026)
어느 날 공주가 되어 버렸다 - 중국어 (2025)
타몬 군 지금 어느 쪽 ! (2026)
치토세 군은 라무네 병 속에 (2025)
친구의 여동생이 나한테만 짜증나게 군다 (2025)
토지마 탄자부로는 가면라이더가 되고 싶어 (2025)
이세계 사정은 사축 하기 나름 (2026)
무사태평 영주의 즐거운 영지 방어 ~생산계 마법으로 이름 없는 마을을 최강의 성채 도시로~ (2026)
용사 파티에 귀여운 애가 있어서, 고백해봤다. (2026)
내가 너무 귀여운 걸 어쩌겠어! (2025)
더 파이팅 뉴 챌린저 (2009)
볼룸에 오신 것을 환영합니다 (2017)
아르마는 가족이 되고 싶어 (2025)
미소가 끊이지 않는 직장입니다 (2025)
악식 영애와 광혈 공작 (2025)
온화한 귀족의 휴가의 권장 (2026)
용사 파티에서 쫓겨난 다재무능 (2026)
밤은 고양이와 함께 시즌 3 (2024)
미남 고교 지구방위부 하이칼라! (2025)
마왕의 딸은 너무 친절해!! (2026)
사망 유희로 밥을 먹는다. (2026)
정반대의 너와 나 (2026)
마술사 쿠논은 보인다 (2026)
고문 아르바이트의 일상 (2026)
카야는 무섭지 않아 (2026)
페이트 스트레인지 페이크 (2026)
전생했더니 드래곤의 알이었다 ~최강이 아니면 목표로 하지 않아~ (2026)
공주님 “고문“의 시간입니다 2기 (2026)
나를 먹고 싶은, 괴물 (2025)
비밀의 아이프리 (2024)
마루는 강쥐 (2025)
울트라맨 오메가 (2025)
트라이건 스타게이즈 (2026)
아름다운 그대에게 (2026)
아름다운 초저녁달 (2026)
용사형에 처함 (2026)
데드 어카운트 (2026)
에리스의 성배 (2026)
한밤중 하트튠 (2026)
헬 모드 ~파고들기 좋아하는 게이머는 폐급 설정 이세계에서 무쌍한다~ (2026)
귀족 전생 ~축복받은 태생으로 최강의 힘을 손에 넣다~ (2026)
투명남과 인간녀 ~곧 부부가 될 두 사람~ (2026)
라디앙 시즌 2 (2019)
마도정병의 슬레이브 2 (2026)
【최애의 아이】 3기 (2026)
불꽃 소방대 3기 part 2 (2026)
무한의 주인 IMMORTAL (2019)
푸른 오케스트라 Season 2 (2025)
불멸의 그대에게 Season 3 (2025)
와타리 군의 XX가 붕괴 직전 (2025)
비질랜티 -나의 히어로 아카데미아 ILLEGALS- Season 2 (2026)
아빠는 영웅, 엄마는 정령, 딸인 나는 전생자. (2025)
그노시아 (2025)
가치아쿠타 (2025)
위국일기 (2026)
주술회전 3기 사멸회유 전편 (2025)
주술회전 3기 사멸회유 전편 (2026)
원펀맨 3기 (2025)
용족 Ⅱ-The Mourner's Eyes- - 중국어 (2025)
용족 Ⅱ-The Mourner's Eyes- - 일본어 (2025)
어차피, 사랑하고 만다. 2기 (2026)
안녕! 틴틴팅클 (2025)
(자막) 달의 요정 세일러 문 (2021)
(더빙) 카쿠리요의 여관밥 (2018)
(더빙) 디지몬 비트브레이크 (2025)
(자막) 장송의 프리렌 2기 (2026)
(자막) 원피스 33기 (2024)
(더빙) 하나Doll (2025)
DARK MOON 달의 제단 (2026)
SI-VIS The Sound of Heroes (2025)
WORKING!!! (3기) (2015)
WWW.WORKING!! (2016)

View File

@@ -1,18 +1,12 @@
# 카테고리 # 카테고리
# uri 가 plugin인 경우 name 값은 대체 # uri 가 plugin인 경우 name 값은 대체
#- name: "토렌트"
# list:
# - uri: "rss"
#- name: "기본 기능" #- name: "기본 기능"
# list: # list:
# - uri: "terminal" # - uri: "terminal"
# - uri: "command"
# - uri: "flaskfilemanager" # - uri: "flaskfilemanager"
# - uri: "flaskcode" # - uri: "flaskcode"
# - uri: "number_baseball"
#- name: "링크" #- name: "링크"
@@ -32,6 +26,8 @@
name: "확장 설정" name: "확장 설정"
- uri: "system/plugin" - uri: "system/plugin"
name: "플러그인 관리" name: "플러그인 관리"
- uri: "system/tool/command"
name: "Command 관리"
- uri: "-" - uri: "-"
- uri: "system/logout" - uri: "system/logout"
name: "로그아웃" name: "로그아웃"

View File

@@ -3,7 +3,7 @@
# message id 가 없을 경우 DEFAULT 값 사용 # message id 가 없을 경우 DEFAULT 값 사용
# 공통: type, enable_time (시작시간-종료시간. 항상 받을 경우 생략) # 공통: type, enable_time (시작시간-종료시간. 항상 받을 경우 생략)
- DEFAULT: DEFAULT:
- type: 'telegram' - type: 'telegram'
token: '' token: ''
chat_id: '' chat_id: ''
@@ -14,7 +14,7 @@
enable_time: '09-23' enable_time: '09-23'
- system_start: system_start:
- type: 'telegram' - type: 'telegram'
token: '' token: ''
chat_id: '' chat_id: ''

View File

@@ -3,11 +3,11 @@ Flask
Flask-SQLAlchemy Flask-SQLAlchemy
Flask-Login Flask-Login
Flask-Cors Flask-Cors
Flask-Markdown #Flask-Markdown
Flask-SocketIO Flask-SocketIO
python-engineio python-engineio
python-socketio python-socketio<5.8.0
Werkzeug Werkzeug<3.0
Jinja2 Jinja2
# common util # common util
@@ -25,4 +25,5 @@ pillow
gevent gevent
gevent-websocket gevent-websocket
pycryptodome pycryptodome
json_fix

12
flower.sh Executable file
View File

@@ -0,0 +1,12 @@
#!/bin/bash
# GDS-DViewer Flower Dashboard Startup Script
# This script bypasses main.py to avoid gevent monkey-patch conflicts (infinite loading).
cd /Volumes/WD/Users/Work/python/flaskfarm
# Kill existing processes
pkill -f flower
# Execute integrated startup command
/Users/yommi/.pyenv/versions/FF_3.10/bin/python3 -c "import sys, os; sys.path.append(os.path.join(os.getcwd(), 'lib')); sys.argv=['celery', '--config_filepath=data/config_mac.yaml', '--running_type=local']; from framework import initiaize; celery=initiaize().celery; celery.start(['flower', '--address=0.0.0.0', '--port=5556'])"

60
gommi.sh Executable file
View File

@@ -0,0 +1,60 @@
#!/bin/bash
# Python 3.14 + gevent fork 경고 억제
export GEVENT_NOWAITPID=1
export PYTHONWARNINGS="ignore::DeprecationWarning"
CONFIGFILE="/data/config.yaml"
COUNT=0
# 🌐 Camoufox 브라우저 캐시 경로 설정 (마운트된 data 폴더 사용으로 이미지 용량 절감)
export CAMOUFOX_CACHE_DIR="/data/.camoufox"
# 🔧 서버 시작 전에 플러그인 업데이트 및 브라우저 확인
update_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
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}
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

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

32
gunicorn_config.py Normal file
View File

@@ -0,0 +1,32 @@
# Gunicorn 최적화 설정 (Bottleproof v2)
import multiprocessing
import os
# 1. 네트워크 설정
bind = "0.0.0.0:9099"
backlog = 2048
# 2. 프로세스 관리
# GeventWebSocketWorker: SocketIO(WebSocket) 지원을 위해 필수
worker_class = "geventwebsocket.gunicorn.workers.GeventWebSocketWorker"
workers = 1 # SocketIO 세션 일관성을 위해 1개 권장 (Redis message queue 미사용 시)
worker_connections = 1000
timeout = 600
keepalive = 5
# 3. PID 관리 (정밀한 프로세스 제어를 위해 추가)
pidfile = "data/gunicorn.pid"
# 4. 로깅 설정
# data/log 폴더 자동 생성 보장
log_dir = "data/log"
if not os.path.exists(log_dir):
os.makedirs(log_dir, exist_ok=True)
accesslog = os.path.join(log_dir, "gunicorn_access.log")
errorlog = os.path.join(log_dir, "gunicorn_error.log")
loglevel = "info"
capture_output = True
# 5. 개발 편의성
reload = False # 운영 환경에서는 False

BIN
lib/.DS_Store vendored Normal file

Binary file not shown.

BIN
lib/framework/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -1,25 +1,70 @@
try:
import yaml
except:
import os
try:
os.system("pip install pyyaml")
except:
pass
from .init_main import Framework from .init_main import Framework
from .version import VERSION from .version import VERSION
frame = Framework.get_instance() # 2024.06.13
F = frame # 잘못된 설계로 인해 import 만으로 초기화 되버려 lib을 사용할 수 없다.
logger = frame.logger # 분리.
app = frame.app
celery = frame.celery F = None
db = frame.db frame = None
scheduler = frame.scheduler logger = None
socketio = frame.socketio app = None
path_app_root = frame.path_app_root celery = None
path_data = frame.path_data db = None
get_logger = frame.get_logger scheduler = None
socketio = None
rd = None
path_app_root = None
path_data = None
get_logger = None
SystemModelSetting = None
get_cache = None
def initiaize():
global F
global frame
global logger
global app
global celery
global db
global scheduler
global socketio
global path_app_root
global path_data
global get_logger
global SystemModelSetting
global get_cache
F = Framework.get_instance()
frame = F
logger = frame.logger
app = frame.app
celery = frame.celery
db = frame.db
scheduler = frame.scheduler
socketio = frame.socketio
rd = frame.rd
path_app_root = frame.path_app_root
path_data = frame.path_data
get_logger = frame.get_logger
frame.initialize_system()
from system.setup import SystemModelSetting as SS
SystemModelSetting = SS
frame.initialize_plugin()
return frame
from flask_login import login_required from flask_login import login_required
from support import d from support import d
from .init_declare import User, check_api from .init_declare import User, check_api
from .scheduler import Job from .scheduler import Job
frame.initialize_system()
from system.setup import SystemModelSetting
frame.initialize_plugin()

View File

@@ -0,0 +1,73 @@
import redis
class _RedisManager:
_instance = None
def __new__(cls, *args, **kwargs):
if not cls._instance:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self, host='localhost', port=6379):
if hasattr(self, 'redis_client'):
return
try:
self.redis_client = redis.Redis(host=host, port=port, db=1, decode_responses=True)
self.redis_client.ping()
self.is_redis = True
except redis.exceptions.ConnectionError:
self.is_redis = False
self.cache_backend = {} # Redis 실패 시 메모리 캐시 사용
def set(self, key, value, ex=None):
if self.is_redis:
self.redis_client.set(key, value, ex=ex)
else:
self.cache_backend[key] = value
def get(self, key):
if self.is_redis:
return self.redis_client.get(key)
else:
return self.cache_backend.get(key)
def delete(self, key):
if self.is_redis:
self.redis_client.delete(key)
else:
if key in self.cache_backend:
del self.cache_backend[key]
#_redis_manager_instance = _RedisManager()
class NamespacedCache:
def __init__(self, namespace):
self._manager = _RedisManager._instance
self.namespace = namespace
def _make_key(self, key):
# 'plugin_name:key' 형식으로 실제 키를 생성
return f"{self.namespace}:{key}"
def set(self, key, value, ex=None):
full_key = self._make_key(key)
self._manager.set(full_key, value, ex=ex)
def get(self, key):
full_key = self._make_key(key)
return self._manager.get(full_key)
def delete(self, key):
full_key = self._make_key(key)
self._manager.delete(full_key)
def get_cache(plugin_name: str) -> NamespacedCache:
"""
플러그인 이름을 기반으로 네임스페이스가 적용된 캐시 객체를 반환합니다.
"""
if not plugin_name:
raise ValueError("플러그인 이름은 필수입니다.")
return NamespacedCache(plugin_name)

View File

@@ -13,13 +13,13 @@ def check_api(original_function):
#logger.warning(request.url) #logger.warning(request.url)
#logger.warning(request.form) #logger.warning(request.form)
try: try:
if F.SystemModelSetting.get_bool('auth_use_apikey'): if F.SystemModelSetting.get_bool('use_apikey'):
if request.method == 'POST': try:
apikey = request.form['apikey'] d = request.get_json()
else: except Exception:
apikey = request.args.get('apikey') d = request.form.to_dict() if request.method == 'POST' else request.args.to_dict()
#apikey = request.args.get('apikey') apikey = d.get('apikey')
if apikey is None or apikey != F.SystemModelSetting.get('auth_apikey'): if apikey is None or apikey != F.SystemModelSetting.get('apikey'):
F.logger.warning('CHECK API : ABORT no match ({})'.format(apikey)) F.logger.warning('CHECK API : ABORT no match ({})'.format(apikey))
F.logger.warning(request.environ.get('HTTP_X_REAL_IP', request.remote_addr)) F.logger.warning(request.environ.get('HTTP_X_REAL_IP', request.remote_addr))
abort(403) abort(403)
@@ -31,7 +31,7 @@ def check_api(original_function):
return original_function(*args, **kwargs) #2 return original_function(*args, **kwargs) #2
return wrapper_function return wrapper_function
# Suuport를 logger 생성전에 쓰지 않기 위해 중복 선언 # Support를 logger 생성전에 쓰지 않기 위해 중복 선언
import logging import logging
@@ -47,7 +47,7 @@ class CustomFormatter(logging.Formatter):
# pathname filename # pathname filename
#format = "[%(asctime)s|%(name)s|%(levelname)s - %(message)s (%(filename)s:%(lineno)d)" #format = "[%(asctime)s|%(name)s|%(levelname)s - %(message)s (%(filename)s:%(lineno)d)"
__format = '[{yellow}%(asctime)s{reset}|{color}%(levelname)s{reset}|{green}%(name)s{reset} %(pathname)s:%(lineno)s] {color}%(message)s{reset}' if os.environ.get('LOGGER_PATHNAME', "False") == "True" else '[{yellow}%(asctime)s{reset}|{color}%(levelname)s{reset}|{green}%(name)s{reset} %(filename)s:%(lineno)s] {color}%(message)s{reset}' __format = '[{yellow}%(asctime)s{reset}|{color}%(levelname)s{reset}|{green}%(name)s{reset}|%(pathname)s:%(lineno)s] {color}%(message)s{reset}' if os.environ.get('LOGGER_PATHNAME', "False") == "True" else '[{yellow}%(asctime)s{reset}|{color}%(levelname)s{reset}|{green}%(name)s{reset}|%(filename)s:%(lineno)s] {color}%(message)s{reset}'
FORMATS = { FORMATS = {
logging.DEBUG: __format.format(color=grey, reset=reset, yellow=yellow, green=green), logging.DEBUG: __format.format(color=grey, reset=reset, yellow=yellow, green=green),

View File

@@ -8,14 +8,15 @@ import time
import traceback import traceback
from datetime import datetime from datetime import datetime
import redis
import yaml import yaml
from flask import Flask from flask import Flask
from flask_cors import CORS from flask_cors import CORS
from flask_login import LoginManager, login_required from flask_login import LoginManager, login_required
from flask_socketio import SocketIO from flask_socketio import SocketIO
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from flaskext.markdown import Markdown
from pytz import timezone, utc from pytz import timezone, utc
from werkzeug.middleware.proxy_fix import ProxyFix
from .init_declare import CustomFormatter, check_api from .init_declare import CustomFormatter, check_api
@@ -37,12 +38,15 @@ class Framework:
self.db = None self.db = None
self.scheduler = None self.scheduler = None
self.socketio = None self.socketio = None
self.rd = None
self.path_app_root = None self.path_app_root = None
self.path_data = None self.path_data = None
self.users = {} self.users = {}
self.get_cache = None
self.__level_unset_logger_list = [] self.__level_unset_logger_list = []
self.__logger_list = [] self.__logger_list = []
self.all_log_filehandler = None
self.__exit_code = -1 self.__exit_code = -1
self.login_manager = None self.login_manager = None
#self.plugin_instance_list = {} #self.plugin_instance_list = {}
@@ -59,14 +63,17 @@ class Framework:
def __initialize(self): def __initialize(self):
os.environ["PYTHONUNBUFFERED"] = "1" os.environ["PYTHONUNBUFFERED"] = "1"
os.environ['FF'] = "true" os.environ['FF'] = "true"
os.environ['FF_PYTHON'] = sys.executable
self.__config_initialize("first") self.__config_initialize("first")
self.__make_default_dir() self.__make_default_dir()
self.logger = self.get_logger(__package__) self.logger = self.get_logger(__package__)
self.get_logger('support')
import support import support
self.__prepare_starting() self.__prepare_starting()
self.app = Flask(__name__) self.app = Flask(__name__)
self.app.wsgi_app = ProxyFix(self.app.wsgi_app, x_proto=1)
self.__config_initialize('flask') self.__config_initialize('flask')
self.__init_db() self.__init_db()
@@ -82,7 +89,6 @@ class Framework:
self.socketio = SocketIO(self.app, cors_allowed_origins="*", async_mode='threading') self.socketio = SocketIO(self.app, cors_allowed_origins="*", async_mode='threading')
CORS(self.app) CORS(self.app)
Markdown(self.app)
self.login_manager = LoginManager() self.login_manager = LoginManager()
self.login_manager.init_app(self.app) self.login_manager.init_app(self.app)
@@ -94,10 +100,11 @@ class Framework:
self.app.config.update( self.app.config.update(
DROPZONE_MAX_FILE_SIZE = 102400, DROPZONE_MAX_FILE_SIZE = 102400,
DROPZONE_TIMEOUT = 5*60*1000, DROPZONE_TIMEOUT = 5*60*1000,
#DROPZONE_ALLOWED_FILE_CUSTOM = True, DROPZONE_ALLOWED_FILE_CUSTOM = True,
#DROPZONE_ALLOWED_FILE_TYPE = 'default, image, audio, video, text, app, *.*', DROPZONE_ALLOWED_FILE_TYPE = "image/*, audio/*, video/*, text/*, application/*, *.*",
) )
self.dropzone = Dropzone(self.app) self.dropzone = Dropzone(self.app)
def __init_db(self): def __init_db(self):
@@ -131,19 +138,20 @@ class Framework:
def __init_celery(self): def __init_celery(self):
redis_port = 6379
try: try:
from celery import Celery
#if frame.config['use_celery'] == False or platform.system() == 'Windows':
if self.config['use_celery'] == False: if self.config['use_celery'] == False:
raise Exception('no celery') raise Exception('use_celery=False')
from celery import Celery
redis_port = os.environ.get('REDIS_PORT', None) redis_port = os.environ.get('REDIS_PORT', None)
if redis_port == None: if redis_port == None:
redis_port = self.config.get('redis_port', None) redis_port = self.config.get('redis_port', None)
if redis_port == None: if redis_port == None:
redis_port = '6379' redis_port = '6379'
self.config['redis_port'] = redis_port
self.rd = redis.StrictRedis(host='localhost', port=redis_port, db=0)
if self.config['use_celery'] == False:
raise Exception('no celery')
self.app.config['CELERY_BROKER_URL'] = 'redis://localhost:%s/0' % redis_port self.app.config['CELERY_BROKER_URL'] = 'redis://localhost:%s/0' % redis_port
self.app.config['CELERY_RESULT_BACKEND'] = 'redis://localhost:%s/0' % redis_port self.app.config['CELERY_RESULT_BACKEND'] = 'redis://localhost:%s/0' % redis_port
@@ -166,6 +174,7 @@ class Framework:
F.logger.info(f"celery running_type: {running_type}") F.logger.info(f"celery running_type: {running_type}")
#F.logger.info(f"celery running_type: {options}") #F.logger.info(f"celery running_type: {options}")
celery.steps['worker'].add(CustomArgs) celery.steps['worker'].add(CustomArgs)
except Exception as e: except Exception as e:
if self.config['use_celery']: if self.config['use_celery']:
self.logger.error('CELERY!!!') self.logger.error('CELERY!!!')
@@ -187,6 +196,14 @@ class Framework:
if len(args) > 0 and type(args[0]) == type(dummy_func): if len(args) > 0 and type(args[0]) == type(dummy_func):
return args[0] return args[0]
self.f(*args, **kwargs) self.f(*args, **kwargs)
try:
from .init_cache_manager import _RedisManager, get_cache
_RedisManager(host='localhost', port=redis_port)
self.get_cache = get_cache
except Exception as e:
self.logger.error(f"get_cache import error: {str(e)}")
self.get_cache = None
return celery return celery
@@ -201,11 +218,13 @@ class Framework:
self.logger.error(f'Exception:{str(e)}') self.logger.error(f'Exception:{str(e)}')
self.logger.error(traceback.format_exc()) self.logger.error(traceback.format_exc())
self.SystemModelSetting = SystemInstance.ModelSetting self.SystemModelSetting = SystemInstance.ModelSetting
SystemInstance.plugin_load() if self.config['run_flask']:
SystemInstance.plugin_load()
self.app.register_blueprint(SystemInstance.blueprint) self.app.register_blueprint(SystemInstance.blueprint)
self.config['flag_system_loading'] = True self.config['flag_system_loading'] = True
self.__config_initialize('member') self.__config_initialize('member')
self.__config_initialize('system_loading_after') self.__config_initialize('system_loading_after')
self.set_level(self.SystemModelSetting.get_int('log_level'))
def initialize_plugin(self): def initialize_plugin(self):
@@ -232,6 +251,7 @@ class Framework:
self.__make_default_logger() self.__make_default_logger()
self.__config_initialize("last") self.__config_initialize("last")
self.config['loading_completed'] = True
self.logger.info('### LAST') self.logger.info('### LAST')
self.logger.info(f"### PORT: {self.config.get('port')}") self.logger.info(f"### PORT: {self.config.get('port')}")
self.logger.info('### Now you can access App by webbrowser!!') self.logger.info('### Now you can access App by webbrowser!!')
@@ -248,6 +268,7 @@ class Framework:
def __config_initialize(self, mode): def __config_initialize(self, mode):
if mode == "first": if mode == "first":
self.config = {} self.config = {}
self.config['loading_completed'] = False
self.config['os'] = platform.system() self.config['os'] = platform.system()
self.config['flag_system_loading'] = False self.config['flag_system_loading'] = False
#self.config['run_flask'] = True if sys.argv[0].endswith('main.py') else False #self.config['run_flask'] = True if sys.argv[0].endswith('main.py') else False
@@ -263,6 +284,8 @@ class Framework:
self.config['export_filepath'] = os.path.join(self.config['path_app'], 'export.sh') self.config['export_filepath'] = os.path.join(self.config['path_app'], 'export.sh')
self.config['exist_export'] = os.path.exists(self.config['export_filepath']) self.config['exist_export'] = os.path.exists(self.config['export_filepath'])
self.config['recent_version'] = '--' self.config['recent_version'] = '--'
from .version import VERSION
self.config['version'] = VERSION
self.__process_args() self.__process_args()
self.__load_config() self.__load_config()
self.__init_define() self.__init_define()
@@ -270,7 +293,7 @@ class Framework:
self.config['notify_yaml_filepath'] = os.path.join(self.config['path_data'], 'db', 'notify.yaml') self.config['notify_yaml_filepath'] = os.path.join(self.config['path_data'], 'db', 'notify.yaml')
if 'running_type' not in self.config: if 'running_type' not in self.config:
self.config['running_type'] = 'native' self.config['running_type'] = 'native'
self.pip_install()
elif mode == "flask": elif mode == "flask":
self.app.secret_key = os.urandom(24) self.app.secret_key = os.urandom(24)
self.app.config['TEMPLATES_AUTO_RELOAD'] = True self.app.config['TEMPLATES_AUTO_RELOAD'] = True
@@ -295,9 +318,7 @@ class Framework:
self.config['DEFINE'] = {} self.config['DEFINE'] = {}
# 이건 필요 없음 # 이건 필요 없음
self.config['DEFINE']['GIT_VERSION_URL'] = 'https://raw.githubusercontent.com/flaskfarm/flaskfarm/main/lib/framework/version.py' self.config['DEFINE']['GIT_VERSION_URL'] = 'https://raw.githubusercontent.com/flaskfarm/flaskfarm/main/lib/framework/version.py'
self.config['DEFINE']['CHANGELOG'] = 'https://flaskfarm.github.io/posts/changelog' self.config['DEFINE']['CHANGELOG'] = 'https://github.com/flaskfarm/flaskfarm'
def __process_args(self): def __process_args(self):
# celery 에서 args 처리시 문제 발생. # celery 에서 args 처리시 문제 발생.
@@ -363,6 +384,9 @@ class Framework:
self.config['debug'] = False self.config['debug'] = False
if self.config.get('plugin_update') == None: if self.config.get('plugin_update') == None:
self.config['plugin_update'] = True self.config['plugin_update'] = True
# 2022-11-20
if self.config['debug']:
self.config['plugin_update'] = False
if self.config.get('plugin_loading_only_devpath') == None: if self.config.get('plugin_loading_only_devpath') == None:
self.config['plugin_loading_only_devpath'] = False self.config['plugin_loading_only_devpath'] = False
if self.config.get('plugin_loading_list') == None: if self.config.get('plugin_loading_list') == None:
@@ -402,8 +426,8 @@ class Framework:
try: try:
if self.config['flag_system_loading']: if self.config['flag_system_loading']:
try: try:
from system import SystemModelSetting #from system import SystemModelSetting
level = SystemModelSetting.get_int('log_level') level = self.SystemModelSetting.get_int('log_level')
except: except:
level = logging.DEBUG level = logging.DEBUG
if self.__level_unset_logger_list is not None: if self.__level_unset_logger_list is not None:
@@ -426,7 +450,7 @@ class Framework:
return converted.timetuple() return converted.timetuple()
if from_command == False: if from_command == False:
file_formatter = logging.Formatter(u'[%(asctime)s|%(levelname)s|%(filename)s:%(lineno)s] %(message)s') file_formatter = logging.Formatter(u'[%(asctime)s|%(levelname)s|%(name)s|%(filename)s:%(lineno)s] %(message)s')
else: else:
file_formatter = logging.Formatter(u'[%(asctime)s] %(message)s') file_formatter = logging.Formatter(u'[%(asctime)s] %(message)s')
@@ -435,10 +459,18 @@ class Framework:
fileHandler = logging.handlers.RotatingFileHandler(filename=os.path.join(self.path_data, 'log', f'{name}.log'), maxBytes=file_max_bytes, backupCount=5, encoding='utf8', delay=True) fileHandler = logging.handlers.RotatingFileHandler(filename=os.path.join(self.path_data, 'log', f'{name}.log'), maxBytes=file_max_bytes, backupCount=5, encoding='utf8', delay=True)
fileHandler.setFormatter(file_formatter) fileHandler.setFormatter(file_formatter)
logger.addHandler(fileHandler) logger.addHandler(fileHandler)
if name == 'framework' and self.all_log_filehandler == None:
self.all_log_filehandler = logging.handlers.RotatingFileHandler(filename=os.path.join(self.path_data, 'log', f'all.log'), maxBytes=5*1024*1024, backupCount=5, encoding='utf8', delay=True)
self.all_log_filehandler.setFormatter(file_formatter)
if from_command == False: if from_command == False:
streamHandler = logging.StreamHandler() streamHandler = logging.StreamHandler()
streamHandler.setFormatter(CustomFormatter()) streamHandler.setFormatter(CustomFormatter())
logger.addHandler(streamHandler) logger.addHandler(streamHandler)
if self.all_log_filehandler != None:
logger.addHandler(self.all_log_filehandler)
return logger return logger
@@ -459,7 +491,7 @@ class Framework:
def set_level(self, level): def set_level(self, level):
try: try:
for l in self.__logger_list: for l in self.__logger_list:
l.setLevel(level) l.setLevel(int(level))
self.__make_default_logger() self.__make_default_logger()
except: except:
pass pass
@@ -468,7 +500,7 @@ class Framework:
def start(self): def start(self):
host = '0.0.0.0' host = '0.0.0.0'
for i in range(5): for i in range(5):
try: try:
#self.logger.debug(d(self.config)) #self.logger.debug(d(self.config))
# allow_unsafe_werkzeug=True termux nohup 실행시 필요함 # allow_unsafe_werkzeug=True termux nohup 실행시 필요함
@@ -517,8 +549,8 @@ class Framework:
PluginManager.plugin_unload() PluginManager.plugin_unload()
with self.app.test_request_context(): with self.app.test_request_context():
self.socketio.stop() self.socketio.stop()
except Exception as exception: except Exception as e:
self.logger.error('Exception:%s', exception) self.logger.error(f"Exception:{str(e)}")
self.logger.error(traceback.format_exc()) self.logger.error(traceback.format_exc())
def get_recent_version(self): def get_recent_version(self):
@@ -532,3 +564,11 @@ class Framework:
self.logger.error(traceback.format_exc()) self.logger.error(traceback.format_exc())
self.config['recent_version'] = '확인 실패' self.config['recent_version'] = '확인 실패'
return False return False
# dev 도커용. package는 setup에 포함.
def pip_install(self):
try:
import json_fix
except:
os.system('pip install json_fix')

View File

@@ -1,93 +1,144 @@
import os import os
import shutil import shutil
import traceback
from framework import F, logger
from support import SupportYaml, d from support import SupportYaml, d
from framework import F
class MenuManager: class MenuManager:
menu_map = None menu_map = None
@classmethod @classmethod
def __load_menu_yaml(cls): def __load_menu_yaml(cls):
menu_yaml_filepath = os.path.join(F.config['path_data'], 'db', 'menu.yaml') try:
if os.path.exists(menu_yaml_filepath) == False: menu_yaml_filepath = os.path.join(F.config['path_data'], 'db', 'menu.yaml')
shutil.copy( if os.path.exists(menu_yaml_filepath) == False:
os.path.join(F.config['path_app'], 'files', 'menu.yaml.template'), shutil.copy(
menu_yaml_filepath os.path.join(F.config['path_app'], 'files', 'menu.yaml.template'),
) menu_yaml_filepath
cls.menu_map = SupportYaml.read_yaml(menu_yaml_filepath) )
cls.menu_map = SupportYaml.read_yaml(menu_yaml_filepath)
except Exception as e:
logger.error(f"Exception:{str(e)}")
logger.error(traceback.format_exc())
cls.menu_map = SupportYaml.read_yaml(os.path.join(F.config['path_app'], 'files', 'menu.yaml.template'))
@classmethod @classmethod
def init_menu(cls): def init_menu(cls):
cls.__load_menu_yaml() cls.__load_menu_yaml()
from .init_plugin import PluginManager if cls.__init_menu() == False:
plugin_menus = PluginManager.plugin_menus cls.menu_map = SupportYaml.read_yaml(os.path.join(F.config['path_app'], 'files', 'menu.yaml.template'))
copy_map = [] cls.__init_menu()
for category in cls.menu_map: @classmethod
if 'uri' in category: def __init_menu(cls):
copy_map.append(category) try:
continue from .init_plugin import PluginManager
cate_count = 0 plugin_menus = PluginManager.plugin_menus
copy_map = []
for category in cls.menu_map:
if 'uri' in category:
if category['uri'] in plugin_menus:
plugin_menus[category['uri']]['match'] = True
copy_map.append(plugin_menus[category['uri']]['menu'])
else:
copy_map.append(category)
continue
cate_count = 0
tmp_cate_list = [] tmp_cate_list = []
for item in category['list']: for item in category['list']:
if item['uri'] in plugin_menus: if item['uri'] in plugin_menus:
plugin_menus[item['uri']]['match'] = True plugin_menus[item['uri']]['match'] = True
tmp_cate_list.append(plugin_menus[item['uri']]['menu']) tmp_cate_list.append(plugin_menus[item['uri']]['menu'])
cate_count += 1 cate_count += 1
elif item['uri'].startswith('http'): elif item['uri'].startswith('http'):
tmp_cate_list.append({ tmp_cate_list.append({
'uri': item['uri'], 'uri': item['uri'],
'name': item['name'], 'name': item['name'],
'target': item.get('target', '_blank') 'target': item.get('target', '_blank')
}) })
cate_count += 1 cate_count += 1
elif (len(item['uri'].split('/')) > 1 and item['uri'].split('/')[0] in plugin_menus) or item['uri'].startswith('javascript') or item['uri'] in ['-']: elif (len(item['uri'].split('/')) > 1 and item['uri'].split('/')[0] in plugin_menus) or item['uri'].startswith('javascript') or item['uri'] in ['-']:
tmp_cate_list.append({
'uri': item['uri'],
'name': item.get('name', ''),
})
cate_count += 1
elif item['uri'] == 'setting':
if len(PluginManager.setting_menus) > 0:
tmp_cate_list.append({ tmp_cate_list.append({
'uri': item['uri'], 'uri': item['uri'],
'name': item.get('name', ''), 'name': item.get('name', ''),
'list': PluginManager.setting_menus
}) })
cate_count += 1
if cate_count > 0: elif item['uri'] == 'setting':
copy_map.append({ # 2024.06.04
'name': category['name'], # 확장설정도 메뉴 구성
'list': tmp_cate_list, if len(PluginManager.setting_menus) > 0:
'count': cate_count set_tmp = item.get('list')
}) if set_tmp:
cls.menu_map = copy_map cp = PluginManager.setting_menus.copy()
include = []
make_dummy_cate = False for set_ch in set_tmp:
for name, plugin_menu in plugin_menus.items(): if set_ch.get('uri') and (set_ch.get('uri') == '-' or set_ch.get('uri').startswith('http')):
#F.logger.info(d(plugin_menu)) include.append(set_ch)
#if 'uri' not in plugin_menu['menu']: continue
# continue
if plugin_menu['match'] == False: for i, ps in enumerate(cp):
if make_dummy_cate == False: if set_ch.get('plugin') != None and set_ch.get('plugin') == ps.get('plugin'):
make_dummy_cate = True include.append(ps)
cls.menu_map.insert(len(cls.menu_map)-1, { del cp[i]
'name':'미분류', 'count':0, 'list':[] break
tmp_cate_list.append({
'uri': item['uri'],
'name': item.get('name', ''),
'list': include + cp
})
else:
tmp_cate_list.append({
'uri': item['uri'],
'name': item.get('name', ''),
'list': PluginManager.setting_menus
})
if cate_count > 0:
copy_map.append({
'name': category['name'],
'list': tmp_cate_list,
'count': cate_count
}) })
cls.menu_map = copy_map
make_dummy_cate = False
for name, plugin_menu in plugin_menus.items():
#F.logger.info(d(plugin_menu))
#if 'uri' not in plugin_menu['menu']:
# continue
if plugin_menu['match'] == False:
if make_dummy_cate == False:
make_dummy_cate = True
cls.menu_map.insert(len(cls.menu_map)-1, {
'name':'미분류', 'count':0, 'list':[]
})
c = cls.menu_map[-2] c = cls.menu_map[-2]
c['count'] += 1 c['count'] += 1
c['list'].append(plugin_menu['menu']) c['list'].append(plugin_menu['menu'])
return True
except Exception as e:
logger.error(f"Exception:{str(e)}")
logger.error(traceback.format_exc())
return False
#F.logger.warning(d(cls.menu_map)) #F.logger.warning(d(cls.menu_map))
@classmethod @classmethod
def get_menu_map(cls): def get_menu_map(cls):
#F.logger.warning(d(cls.menu_map))
return cls.menu_map return cls.menu_map
@classmethod
def get_setting_menu(cls, plugin):
from .init_plugin import PluginManager
for tmp in PluginManager.setting_menus:
if tmp['plugin'] == plugin:
return tmp

View File

@@ -3,13 +3,13 @@ import platform
import shutil import shutil
import sys import sys
import threading import threading
import time
import traceback import traceback
import zipfile import zipfile
import requests import requests
from support import SupportFile, SupportSubprocess, SupportYaml
from framework import F from framework import F
from support import SupportFile, SupportSubprocess, SupportYaml
class PluginManager: class PluginManager:
@@ -30,13 +30,13 @@ class PluginManager:
tmps = os.listdir(plugin_path) tmps = os.listdir(plugin_path)
add_plugin_list = [] add_plugin_list = []
for t in tmps: for t in tmps:
if not t.startswith('_') and os.path.isdir(os.path.join(plugin_path, t)): if t.startswith('_') == False and t.startswith('.') == False and os.path.isdir(os.path.join(plugin_path, t)) and t != 'false' and t != 'tmp':
add_plugin_list.append(t) add_plugin_list.append(t)
cls.all_package_list[t] = {'pos':'normal', 'path':os.path.join(plugin_path, t), 'loading':(F.config.get('plugin_loading_only_devpath', None) != True)} cls.all_package_list[t] = {'pos':'normal', 'path':os.path.join(plugin_path, t), 'loading':(F.config.get('plugin_loading_only_devpath', None) != True)}
plugins = plugins + add_plugin_list plugins = plugins + add_plugin_list
except Exception as exception: except Exception as e:
F.logger.error('Exception:%s', exception) F.logger.error(f"Exception:{str(e)}")
F.logger.error(traceback.format_exc()) F.logger.error(traceback.format_exc())
if F.config.get('plugin_loading_only_devpath', None) == True: if F.config.get('plugin_loading_only_devpath', None) == True:
@@ -59,12 +59,12 @@ class PluginManager:
tmps = os.listdir(__) tmps = os.listdir(__)
add_plugin_list = [] add_plugin_list = []
for t in tmps: for t in tmps:
if not t.startswith('_') and os.path.isdir(os.path.join(__, t)): if t.startswith('_') == False and t.startswith('.') == False and os.path.isdir(os.path.join(__, t)) and t != 'false' and t != 'tmp':
add_plugin_list.append(t) add_plugin_list.append(t)
cls.all_package_list[t] = {'pos':'dev', 'path':os.path.join(__, t), 'loading':True} cls.all_package_list[t] = {'pos':'dev', 'path':os.path.join(__, t), 'loading':True}
plugins = plugins + add_plugin_list plugins = plugins + add_plugin_list
except Exception as exception: except Exception as e:
F.logger.error('Exception:%s', exception) F.logger.error(f"Exception:{str(e)}")
F.logger.error(traceback.format_exc()) F.logger.error(traceback.format_exc())
# plugin_loading_list # plugin_loading_list
@@ -79,8 +79,8 @@ class PluginManager:
cls.all_package_list[_]['loading'] = False cls.all_package_list[_]['loading'] = False
cls.all_package_list[_]['status'] = 'not_include_loading_list' cls.all_package_list[_]['status'] = 'not_include_loading_list'
plugins = new_plugins plugins = new_plugins
except Exception as exception: except Exception as e:
F.logger.error('Exception:%s', exception) F.logger.error(f"Exception:{str(e)}")
F.logger.error(traceback.format_exc()) F.logger.error(traceback.format_exc())
# plugin_except_list # plugin_except_list
@@ -95,8 +95,8 @@ class PluginManager:
cls.all_package_list[_]['loading'] = False cls.all_package_list[_]['loading'] = False
cls.all_package_list[_]['status'] = 'include_except_list' cls.all_package_list[_]['status'] = 'include_except_list'
plugins = new_plugins plugins = new_plugins
except Exception as exception: except Exception as e:
F.logger.error('Exception:%s', exception) F.logger.error(f"Exception:{str(e)}")
F.logger.error(traceback.format_exc()) F.logger.error(traceback.format_exc())
return plugins return plugins
@@ -111,45 +111,32 @@ class PluginManager:
F.logger.debug(plugins) F.logger.debug(plugins)
for plugin_name in 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] entity = cls.all_package_list[plugin_name]
entity['version'] = '3'
try: try:
mod = __import__('%s' % (plugin_name), fromlist=[])
mod_plugin_info = None
try: try:
mod_plugin_info = getattr(mod, 'plugin_info') mod = __import__(f'{plugin_name}.setup', fromlist=['setup'])
entity['module'] = mod except Exception as e:
except Exception as exception: F.logger.error(f'Exception:{str(e)}')
F.logger.info(f'[!] PLUGIN_INFO not exist : [{plugin_name}] - is FF') F.logger.error(traceback.format_exc())
F.logger.warning(f'[!] NOT normal plugin : [{plugin_name}]')
if mod_plugin_info == None: continue
try:
mod = __import__(f'{plugin_name}.setup', fromlist=['setup'])
entity['version'] = '4'
except Exception as e:
F.logger.error(f'Exception:{str(e)}')
F.logger.error(traceback.format_exc())
F.logger.warning(f'[!] NOT normal plugin : [{plugin_name}]')
try: try:
if entity['version'] != '4': entity['setup_mod'] = mod
mod_blue_print = getattr(mod, 'blueprint') entity['P'] = getattr(mod, 'P')
else: mod_blue_print = getattr(entity['P'], 'blueprint')
entity['setup_mod'] = mod
entity['P'] = getattr(mod, 'P')
mod_blue_print = getattr(entity['P'], 'blueprint')
if mod_blue_print: if mod_blue_print:
F.app.register_blueprint(mod_blue_print) F.app.register_blueprint(mod_blue_print)
except Exception as exception: except Exception as exception:
#logger.error('Exception:%s', exception)
#logger.error(traceback.format_exc())
F.logger.warning(f'[!] BLUEPRINT not exist : [{plugin_name}]') 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 cls.plugin_list[plugin_name] = entity
#system.LogicPlugin.current_loading_plugin_list[plugin_name]['status'] = 'success' except Exception as e:
#system.LogicPlugin.current_loading_plugin_list[plugin_name]['info'] = mod_plugin_info F.logger.error(f"Exception:{str(e)}")
except Exception as exception:
F.logger.error('Exception:%s', exception)
F.logger.error(traceback.format_exc()) F.logger.error(traceback.format_exc())
F.logger.debug('no blueprint') F.logger.debug('no blueprint')
cls.all_package_list[plugin_name]['loading'] = False cls.all_package_list[plugin_name]['loading'] = False
@@ -157,36 +144,44 @@ class PluginManager:
cls.all_package_list[plugin_name]['log'] = traceback.format_exc() cls.all_package_list[plugin_name]['log'] = traceback.format_exc()
if not F.config['run_celery']: if not F.config['run_celery']:
try: try:
with F.app.app_context(): with F.app.app_context():
F.db.create_all() F.db.create_all()
except Exception as exception: except Exception as e:
F.logger.error('Exception:%s', exception) F.logger.error(f"Exception:{str(e)}")
F.logger.error(traceback.format_exc()) F.logger.error(traceback.format_exc())
F.logger.debug('db.create_all error') F.logger.debug('db.create_all error')
if F.config['run_celery']:
for key, entity in cls.plugin_list.items():
try:
mod_plugin_load = getattr(entity['P'], 'plugin_load_celery')
if mod_plugin_load:
F.logger.info(f'[!] plugin_load_celery start : [{key}]')
mod_plugin_load()
F.logger.info(f'[!] plugin_load_celery end : [{key}]')
except Exception as e:
F.logger.error(f"Exception:{str(e)}")
F.logger.error(traceback.format_exc())
if not F.config['run_flask']: if not F.config['run_flask']:
return return
for key, entity in cls.plugin_list.items(): for key, entity in cls.plugin_list.items():
try: try:
mod_plugin_load = None mod_plugin_load = getattr(entity['P'], 'plugin_load')
if entity['version'] == '3':
mod_plugin_load = getattr(entity['module'], 'plugin_load')
elif entity['version'] == '4':
mod_plugin_load = getattr(entity['P'], 'plugin_load')
if mod_plugin_load: if mod_plugin_load:
def func(mod_plugin_load, key): def func(mod_plugin_load, key):
try: try:
F.logger.debug(f'[!] plugin_load threading start : [{key}]') load_start_time = time.time()
#mod.plugin_load() F.logger.info(f'[!] plugin_load threading start : [{key}]')
mod_plugin_load() mod_plugin_load()
F.logger.debug(f'[!] plugin_load threading end : [{key}]') load_elapsed_time = time.time() - load_start_time
except Exception as exception: 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('### plugin_load exception : %s', key)
F.logger.error('Exception:%s', exception) F.logger.error(f"Exception:{str(e)}")
F.logger.error(traceback.format_exc()) F.logger.error(traceback.format_exc())
cls.all_package_list[key]['loading'] = False cls.all_package_list[key]['loading'] = False
cls.all_package_list[key]['status'] = 'plugin_load error' cls.all_package_list[key]['status'] = 'plugin_load error'
@@ -199,42 +194,29 @@ class PluginManager:
MenuManager.init_menu() MenuManager.init_menu()
F.logger.info(f"플러그인 로딩 실패로 메뉴 삭제2 : {key}") F.logger.info(f"플러그인 로딩 실패로 메뉴 삭제2 : {key}")
t = threading.Thread(target=func, args=(mod_plugin_load, key))
t.setDaemon(True)
t.start()
# mod는 위에서 로딩 except Exception as e:
if key != 'mod':
t = threading.Thread(target=func, args=(mod_plugin_load, key))
t.setDaemon(True)
t.start()
#if key == 'mod':
# t.join()
except Exception as exception:
F.logger.debug(f'[!] PLUGIN_LOAD function not exist : [{key}]') F.logger.debug(f'[!] PLUGIN_LOAD function not exist : [{key}]')
#logger.error('Exception:%s', exception)
#logger.error(traceback.format_exc())
#logger.debug('no init_scheduler')
try: try:
mod_menu = None mod_menu = getattr(entity['P'], 'menu')
if entity['version'] == '3':
mod_menu = getattr(entity['module'], 'menu')
elif entity['version'] == '4':
mod_menu = getattr(entity['P'], 'menu')
if mod_menu and cls.all_package_list[key]['loading'] != False: if mod_menu and cls.all_package_list[key]['loading'] != False:
cls.plugin_menus[key]= {'menu':mod_menu, 'match':False} cls.plugin_menus[key]= {'menu':mod_menu, 'match':False}
if entity['version'] == '4': setting_menu = getattr(entity['P'], 'setting_menu')
setting_menu = getattr(entity['P'], 'setting_menu') if setting_menu != None and cls.all_package_list[key]['loading'] != False:
if setting_menu != None and cls.all_package_list[key]['loading'] != False: setting_menu['plugin'] = entity['P'].package_name
F.logger.info(f"메뉴 포함 : {key}") F.logger.info(f"확장 설정 : {key}")
cls.setting_menus.append(setting_menu) cls.setting_menus.append(setting_menu)
except Exception as exception: except Exception as exception:
F.logger.debug('no menu') F.logger.debug('no menu')
F.logger.debug('### plugin_load threading all start.. : %s ', len(cls.plugin_list)) F.logger.debug('### plugin_load threading all start.. : %s ', len(cls.plugin_list))
# 모든 모듈을 로드한 이후에 app 등록, table 생성, start # 모든 모듈을 로드한 이후에 app 등록, table 생성, start
except Exception as exception: except Exception as e:
F.logger.error('Exception:%s', exception) F.logger.error(f"Exception:{str(e)}")
F.logger.error(traceback.format_exc()) F.logger.error(traceback.format_exc())
@@ -243,17 +225,9 @@ class PluginManager:
def plugin_unload(cls): def plugin_unload(cls):
for key, entity in cls.plugin_list.items(): for key, entity in cls.plugin_list.items():
try: try:
if entity['version'] == '3': mod_plugin_unload = getattr(entity['P'], 'plugin_unload')
mod_plugin_unload = getattr(entity['module'], 'plugin_unload')
elif entity['version'] == '4':
mod_plugin_unload = getattr(entity['P'], 'plugin_unload')
#if plugin_name == 'rss':
# continue
#mod_plugin_unload = getattr(mod, 'plugin_unload')
if mod_plugin_unload: if mod_plugin_unload:
mod_plugin_unload() mod_plugin_unload()
#mod.plugin_unload()
except Exception as e: except Exception as e:
F.logger.error('module:%s', key) F.logger.error('module:%s', key)
F.logger.error(f'Exception:{str(e)}') F.logger.error(f'Exception:{str(e)}')
@@ -267,6 +241,7 @@ class PluginManager:
@classmethod @classmethod
def plugin_install(cls, plugin_git, zip_url=None, zip_filename=None): def plugin_install(cls, plugin_git, zip_url=None, zip_filename=None):
plugin_git = plugin_git.strip()
is_git = True if plugin_git != None and plugin_git != '' else False is_git = True if plugin_git != None and plugin_git != '' else False
ret = {} ret = {}
try: try:
@@ -381,7 +356,7 @@ class PluginManager:
tmps = os.listdir(plugins_path) tmps = os.listdir(plugins_path)
for t in tmps: for t in tmps:
plugin_path = os.path.join(plugins_path, t) plugin_path = os.path.join(plugins_path, t)
if t.startswith('_'): if t.startswith('_') or t.startswith('.'):
continue continue
if os.path.exists(os.path.join(plugin_path, '.git')): if os.path.exists(os.path.join(plugin_path, '.git')):
command = ['git', '-C', plugin_path, 'reset', '--hard', 'HEAD'] command = ['git', '-C', plugin_path, 'reset', '--hard', 'HEAD']
@@ -392,14 +367,15 @@ class PluginManager:
F.logger.debug(ret) F.logger.debug(ret)
else: else:
F.logger.debug(f"{plugin_path} not git repo") F.logger.debug(f"{plugin_path} not git repo")
except Exception as exception: except Exception as e:
F.logger.error('Exception:%s', exception) F.logger.error(f"Exception:{str(e)}")
F.logger.error(traceback.format_exc()) F.logger.error(traceback.format_exc())
@classmethod @classmethod
def get_plugin_instance(cls, package_name): def get_plugin_instance(cls, package_name):
try: try:
return cls.all_package_list[package_name]['P'] if cls.all_package_list[package_name]['loading']:
return cls.all_package_list[package_name]['P']
except: except:
pass pass

View File

@@ -4,7 +4,6 @@ import traceback
from flask import (jsonify, redirect, render_template, request, from flask import (jsonify, redirect, render_template, request,
send_from_directory) send_from_directory)
from flask_login import login_required from flask_login import login_required
from framework import F from framework import F
@@ -86,27 +85,31 @@ def open_file(path):
@F.app.route("/file/<path:path>") @F.app.route("/file/<path:path>")
@F.check_api @F.check_api
def file2(path): def file2(path):
# 윈도우 drive 필요 없음 import platform
if platform.system() == 'Windows':
path = os.path.splitdrive(path)[1][1:]
return send_from_directory('/', path, as_attachment=True) return send_from_directory('/', path, as_attachment=True)
@F.app.route("/upload", methods=['GET', 'POST']) @F.app.route("/upload", methods=['GET', 'POST'])
@login_required
def upload(): def upload():
try: try:
if request.method == 'POST': if request.method == 'POST':
f = request.files['file'] f = request.files['file']
from werkzeug import secure_filename from werkzeug.utils import secure_filename
upload_path = F.SystemModelSetting.get('path_upload') upload_path = F.SystemModelSetting.get('path_upload')
os.makedirs(upload_path, exist_ok=True) os.makedirs(upload_path, exist_ok=True)
f.save(os.path.join(upload_path, secure_filename(f.filename))) f.save(os.path.join(upload_path, secure_filename(f.filename)))
return jsonify('success') return jsonify('success')
except Exception as exception: except Exception as e:
F.logger.error('Exception:%s', exception) F.logger.error(f"Exception:{str(e)}")
F.logger.error(traceback.format_exc()) F.logger.error(traceback.format_exc())
return jsonify('fail') return jsonify('fail')
@F.app.route("/videojs", methods=['GET', 'POST']) @F.app.route("/videojs", methods=['GET', 'POST'])
@login_required
def videojs(): def videojs():
data = {} data = {}
data['play_title'] = request.form['play_title'] data['play_title'] = request.form['play_title']
@@ -116,9 +119,33 @@ def videojs():
data['play_subtitle_src'] = request.form['play_subtitle_src'] data['play_subtitle_src'] = request.form['play_subtitle_src']
return render_template('videojs.html', data=data) return render_template('videojs.html', data=data)
@F.app.route("/videojs_drm", methods=['GET', 'POST'])
@login_required
def videojs_drm():
data = {}
data['play_title'] = request.form['play_title']
data['play_source_src'] = request.form['play_source_src']
data['play_source_type'] = request.form['play_source_type']
if 'play_subtitle_src' in request.form:
data['play_subtitle_src'] = request.form['play_subtitle_src']
return render_template('videojs_drm.html', data=data)
@F.app.route("/videojs_discord", methods=['GET', 'POST'])
@login_required
def videojs_og():
data = {}
"""
data['play_title'] = request.form['play_title']
data['play_source_src'] = request.form['play_source_src']
data['play_source_type'] = request.form['play_source_type']
if 'play_subtitle_src' in request.form:
data['play_subtitle_src'] = request.form['play_subtitle_src']
"""
return render_template('videojs_discord.html', data=data)
@F.app.route("/headers", methods=['GET', 'POST']) @F.app.route("/headers", methods=['GET', 'POST'])
@login_required
def headers(): def headers():
from support import d from support import d
F.logger.info(d(request.headers)) F.logger.info(d(request.headers))
@@ -127,6 +154,7 @@ def headers():
# 3.10에서 이거 필수 # 3.10에서 이거 필수
@F.socketio.on('connect', namespace=f'/framework') @F.socketio.on('connect', namespace=f'/framework')
@login_required
def connect(): def connect():
pass pass

View File

@@ -4,6 +4,10 @@ from framework import F
def get_menu(full_query): def get_menu(full_query):
match = re.compile(r'\/(?P<package_name>.*?)\/(?P<module_name>.*?)\/manual\/(?P<sub2>.*?)($|\?)').match(full_query)
if match:
return match.group('package_name'), match.group('module_name'), f"manual/{match.group('sub2')}"
match = re.compile(r'\/(?P<menu>.*?)\/manual\/(?P<sub2>.*?)($|\?)').match(full_query) match = re.compile(r'\/(?P<menu>.*?)\/manual\/(?P<sub2>.*?)($|\?)').match(full_query)
if match: if match:
return match.group('menu'), 'manual', match.group('sub2') return match.group('menu'), 'manual', match.group('sub2')
@@ -48,12 +52,14 @@ def jinja_initialize(app):
app.jinja_env.globals.update(get_menu=get_menu) app.jinja_env.globals.update(get_menu=get_menu)
app.jinja_env.globals.update(get_theme=get_theme) app.jinja_env.globals.update(get_theme=get_theme)
app.jinja_env.globals.update(get_menu_map=MenuManager.get_menu_map) app.jinja_env.globals.update(get_menu_map=MenuManager.get_menu_map)
app.jinja_env.globals.update(get_setting_menu=MenuManager.get_setting_menu)
app.jinja_env.globals.update(get_web_title=get_web_title) app.jinja_env.globals.update(get_web_title=get_web_title)
app.jinja_env.globals.update(dropzone=F.dropzone) app.jinja_env.globals.update(dropzone=F.dropzone)
app.jinja_env.filters['get_menu'] = get_menu app.jinja_env.filters['get_menu'] = get_menu
app.jinja_env.filters['get_theme'] = get_theme app.jinja_env.filters['get_theme'] = get_theme
app.jinja_env.filters['get_menu_map'] = MenuManager.get_menu_map app.jinja_env.filters['get_menu_map'] = MenuManager.get_menu_map
app.jinja_env.filters['get_setting_menu'] = MenuManager.get_setting_menu
app.jinja_env.filters['get_web_title'] = get_web_title app.jinja_env.filters['get_web_title'] = get_web_title
app.jinja_env.auto_reload = True app.jinja_env.auto_reload = True

View File

@@ -4,17 +4,18 @@ import time
import traceback import traceback
from flask import request from flask import request
from support import SingletonClass
from framework import F from framework import F
from support import SingletonClass
namespace = 'log' namespace = 'log'
@F.socketio.on('connect', namespace='/%s' % namespace) @F.socketio.on('connect', namespace='/%s' % namespace)
@F.login_required
def socket_connect(): def socket_connect():
F.logger.debug('log connect') F.logger.debug('log connect')
@F.socketio.on('start', namespace='/%s' % namespace) @F.socketio.on('start', namespace='/%s' % namespace)
@F.login_required
def socket_file(data): def socket_file(data):
try: try:
package = filename = None package = filename = None
@@ -24,8 +25,8 @@ def socket_file(data):
filename = data['filename'] filename = data['filename']
LogViewer.instance().start(package, filename, request.sid) LogViewer.instance().start(package, filename, request.sid)
F.logger.debug('start package:%s filename:%s sid:%s', package, filename, request.sid) F.logger.debug('start package:%s filename:%s sid:%s', package, filename, request.sid)
except Exception as exception: except Exception as e:
F.logger.error('Exception:%s', exception) F.logger.error(f"Exception:{str(e)}")
F.logger.error(traceback.format_exc()) F.logger.error(traceback.format_exc())
@F.socketio.on('disconnect', namespace='/%s' % namespace) @F.socketio.on('disconnect', namespace='/%s' % namespace)
@@ -33,8 +34,8 @@ def disconnect():
try: try:
LogViewer.instance().disconnect(request.sid) LogViewer.instance().disconnect(request.sid)
F.logger.debug('disconnect sid:%s', request.sid) F.logger.debug('disconnect sid:%s', request.sid)
except Exception as exception: except Exception as e:
F.logger.error('Exception:%s', exception) F.logger.error(f"Exception:{str(e)}")
F.logger.error(traceback.format_exc()) F.logger.error(traceback.format_exc())
@@ -62,18 +63,17 @@ class WatchThread(threading.Thread):
key = 'filename' key = 'filename'
value = self.filename value = self.filename
if os.path.exists(logfile): if os.path.exists(logfile):
with open(logfile, 'r') as f: with open(logfile, 'r', encoding='utf8') as f:
f.seek(0, os.SEEK_END) f.seek(0, os.SEEK_END)
while not self.stop_flag: while not self.stop_flag:
line = f.readline() line = f.readline()
if not line: if not line:
time.sleep(0.1) # Sleep briefly time.sleep(0.1) # Sleep briefly
continue continue
F.socketio.emit("add", {key : value, 'data': line}, namespace='/log', broadcast=True) F.socketio.emit("add", {key : value, 'data': line}, namespace='/log')
F.logger.debug('WatchThread.. End %s', value) F.logger.debug('WatchThread.. End %s', value)
else: else:
F.socketio.emit("add", {key : value, 'data': 'not exist logfile'}, namespace='/log', broadcast=True) F.socketio.emit("add", {key : value, 'data': 'not exist logfile'}, namespace='/log')
class LogViewer(SingletonClass): class LogViewer(SingletonClass):

1
lib/framework/logger.py Normal file
View File

@@ -0,0 +1 @@
from support import get_logger

View File

@@ -49,8 +49,8 @@ class Scheduler(object):
if flag_exit: if flag_exit:
self.remove_job("scheduler_check") self.remove_job("scheduler_check")
#time.sleep(30) #time.sleep(30)
except Exception as exception: except Exception as e:
self.logger.error('Exception:%s', exception) self.logger.error(f"Exception:{str(e)}")
self.logger.error(traceback.format_exc()) self.logger.error(traceback.format_exc())
def shutdown(self): def shutdown(self):
@@ -233,21 +233,21 @@ class Job(object):
if self.args is None: if self.args is None:
self.thread = threading.Thread(target=self.target_function, args=()) self.thread = threading.Thread(target=self.target_function, args=())
else: else:
self.thread = threading.Thread(target=self.target_function, args=(self.args,)) self.thread = threading.Thread(target=self.target_function, args=self.args)
self.thread.daemon = True self.thread.daemon = True
self.thread.start() self.thread.start()
F.socketio.emit('notify', {'type':'success', 'msg':f"{self.description}<br>작업을 시작합니다." }, namespace='/framework', broadcast=True) F.socketio.emit('notify', {'type':'success', 'msg':f"{self.description}<br>작업을 시작합니다." }, namespace='/framework')
self.thread.join() self.thread.join()
F.socketio.emit('notify', {'type':'success', 'msg':f"{self.description}<br>작업이 종료되었습니다." }, namespace='/framework', broadcast=True) F.socketio.emit('notify', {'type':'success', 'msg':f"{self.description}<br>작업이 종료되었습니다." }, namespace='/framework')
self.end_time = datetime.now(timezone('Asia/Seoul')) self.end_time = datetime.now(timezone('Asia/Seoul'))
self.running_timedelta = self.end_time - self.start_time self.running_timedelta = self.end_time - self.start_time
self.status = 'success' self.status = 'success'
if not F.scheduler.is_include(self.job_id): if not F.scheduler.is_include(self.job_id):
F.scheduler.remove_job_instance(self.job_id) F.scheduler.remove_job_instance(self.job_id)
self.count += 1 self.count += 1
except Exception as exception: except Exception as e:
self.status = 'exception' self.status = 'exception'
F.logger.error('Exception:%s', exception) F.logger.error(f"Exception:{str(e)}")
F.logger.error(traceback.format_exc()) F.logger.error(traceback.format_exc())
finally: finally:
self.is_running = False self.is_running = False

BIN
lib/framework/static/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -106,3 +106,30 @@ background-color: #ffff0080 !important;
.dropdown-menu { .dropdown-menu {
margin:-2px; margin:-2px;
} }
.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

@@ -0,0 +1,160 @@
h3 {
border-bottom: 1px solid #ddd;
}
.top-bar {
height: 45px;
min-height: 45px;
position: absolute;
top: 0;
right: 0;
left: 0;
}
.bars-lnk {
color: #fff;
}
.bars-lnk i {
display: inline-block;
margin-left: 10px;
margin-top: 7px;
}
.bars-lnk img {
display: inline-block;
margin-left: 10px;
margin-top: -15px;
margin-right: 15px;
height: 35px;
}
.lateral-menu {
background-color: #333;
color: rgb(144, 144, 144);
width: 300px;
}
.lateral-menu label {
color: rgb(144, 144, 144);
}
.lateral-menu-content {
padding-left: 10px;
height: 100%;
font-size: 12px;
font-style: normal;
font-variant: normal;
font-weight: bold;
line-height: 16px;
}
.lateral-menu-content .title{
padding-top: 15px;
font-size: 2em;
height: 45px;
}
.lateral-menu-content-inner {
overflow-y: auto;
height: 100%;
padding-top: 10px;
padding-bottom: 50px;
padding-right: 10px;
font-size: 0.9em;
}
#preview {
height: 97%;
max-height: 97%;
border: 1px solid #eee;
overflow-y: scroll;
width: 55%;
padding: 10px;
}
pre {
white-space: pre-wrap; /* css-3 */
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
white-space: -pre-wrap; /* Opera 4-6 */
white-space: -o-pre-wrap; /* Opera 7 */
word-wrap: break-word; /* Internet Explorer 5.5+ */
background-color: #f8f8f8;
border: 1px solid #dfdfdf;
margin-top: 1.5em;
margin-bottom: 1.5em;
padding: 0.125rem 0.3125rem 0.0625rem;
}
pre code {
background-color: transparent;
border: 0;
padding: 0;
}
.modal-wrapper {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: 999;
background-color: rgba(51,51,51,0.5);
}
.modal-inner {
margin-top: 200px;
margin-left: auto;
margin-right: auto;
width: 600px;
height: 225px;
background-color: #fff;
opacity: 1;
z-index: 1000;
}
.modal-close-btn {
float: right;
display: inline-block;
margin-right: 5px;
color: #ff4336;
}
.modal-close-btn:hover {
float: right;
display: inline-block;
margin-right: 5px;
color: #8d0002;
}
.modal-topbar {
clear: both;
height: 25px;
}
.modal-inner .link-area {
margin: 10px;
height: 170px;
}
.modal-inner textarea {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
.version {
color: white;
font-size: 0.8em !important;
}

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="margin: auto; background: none; display: block; shape-rendering: auto;" width="100px" height="100px" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
<circle cx="50" cy="50" r="31" stroke-width="4" stroke="#e15b64" stroke-dasharray="48.69468613064179 48.69468613064179" fill="none" stroke-linecap="round">
<animateTransform attributeName="transform" type="rotate" dur="2.6315789473684212s" repeatCount="indefinite" keyTimes="0;1" values="0 50 50;360 50 50"></animateTransform>
</circle>
<circle cx="50" cy="50" r="26" stroke-width="4" stroke="#f8b26a" stroke-dasharray="40.840704496667314 40.840704496667314" stroke-dashoffset="40.840704496667314" fill="none" stroke-linecap="round">
<animateTransform attributeName="transform" type="rotate" dur="2.6315789473684212s" repeatCount="indefinite" keyTimes="0;1" values="0 50 50;-360 50 50"></animateTransform>
</circle>
<!-- [ldio] generated by https://loading.io/ --></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ if (tmp.length == 2) {
var PACKAGE_NAME = tmp[1]; var PACKAGE_NAME = tmp[1];
var MODULE_NAME = tmp[2]; var MODULE_NAME = tmp[2];
var PAGE_NAME = ""; var PAGE_NAME = "";
} else if (tmp.length == 4){ } else if (tmp.length > 3){
var PACKAGE_NAME = tmp[1]; var PACKAGE_NAME = tmp[1];
var MODULE_NAME = tmp[2]; var MODULE_NAME = tmp[2];
var PAGE_NAME = tmp[3]; var PAGE_NAME = tmp[3];
@@ -23,8 +23,6 @@ $(window).on("load resize", function (event) {
}); });
$('#command_modal').on('show.bs.modal', function (event) { $('#command_modal').on('show.bs.modal', function (event) {
console.log('111111111')
console.log(event);
}) })
/////////////////////////////////////// ///////////////////////////////////////
@@ -113,7 +111,6 @@ function showModal(data='EMPTY', title='JSON', json=true) {
data = JSON.stringify(data, null, 2); data = JSON.stringify(data, null, 2);
} }
document.getElementById("modal_body").innerHTML = '<pre style="white-space: pre-wrap;">' +data + '</pre>'; document.getElementById("modal_body").innerHTML = '<pre style="white-space: pre-wrap;">' +data + '</pre>';
$("#large_modal").modal(); $("#large_modal").modal();
} }
@@ -168,7 +165,22 @@ function use_collapse(div, reverse=false) {
} }
} }
// jquery extend function
// post로 요청하면서 리다이렉트
$.extend(
{
redirectPost: function(location, args)
{
var form = '';
$.each( args, function( key, value ) {
console.log(key);
console.log(value);
value = value.split('"').join('\"')
form += '<input type="hidden" name="'+key+'" value="'+value+'">';
});
$('<form action="' + location + '" method="POST">' + form + '</form>').appendTo($(document.body)).submit();
}
});
@@ -282,20 +294,3 @@ function pad(n, width) {
return n.length >= width ? n : new Array(width - n.length + 1).join('0') + n; return n.length >= width ? n : new Array(width - n.length + 1).join('0') + n;
} }
// jquery extend function
// post로 요청하면서 리다이렉트
// 푹 자동에서 푹 기본 검색할때 사용
$.extend(
{
redirectPost: function(location, args)
{
var form = '';
$.each( args, function( key, value ) {
console.log(key);
console.log(value);
value = value.split('"').join('\"')
form += '<input type="hidden" name="'+key+'" value="'+value+'">';
});
$('<form action="' + location + '" method="POST">' + form + '</form>').appendTo($(document.body)).submit();
}
});

View File

@@ -15,11 +15,10 @@ $(document).ready(function(){
var protocol = window.location.protocol; var protocol = window.location.protocol;
var frameSocket = io.connect(protocol + "//" + document.domain + ":" + location.port + "/framework"); var frameSocket = io.connect(protocol + "//" + document.domain + ":" + location.port + "/framework");
console.log(frameSocket);
frameSocket.on('notify', function(data){ frameSocket.on('notify', function(data){
$.notify({ $.notify({
message : data['msg'], message : '<strong>' + data['msg'] + '</strong>',
url: data['url'], url: data['url'],
target: '_self' target: '_self'
},{ },{
@@ -29,7 +28,7 @@ frameSocket.on('notify', function(data){
}); });
frameSocket.on('modal', function(data){ frameSocket.on('modal', function(data){
m_modal(data.data, data.title, false); showModal(data.data, data.title, false);
}); });
frameSocket.on('loading_hide', function(data){ frameSocket.on('loading_hide', function(data){
@@ -37,14 +36,12 @@ frameSocket.on('loading_hide', function(data){
}); });
frameSocket.on('refresh', function(data){ frameSocket.on('refresh', function(data){
console.log('data')
window.location.reload(); window.location.reload();
}); });
$('#command_modal').on('hide.bs.modal', function (e) { $('#command_modal').on('hide.bs.modal', function (e) {
//e.preventDefault(); 있으면 동작 안함. //e.preventDefault(); 있으면 동작 안함.
console.log("ff global command_modal hide.bs.modal CATCH")
$.ajax({ $.ajax({
url: `/global/ajax/command_modal_hide`, url: `/global/ajax/command_modal_hide`,
type: 'POST', type: 'POST',
@@ -74,13 +71,27 @@ $("body").on('click', '#globalLinkBtn', function(e) {
window.location.href = url; window.location.href = url;
}); });
$("body").on('click', '#globalReloadBtn', function(e) {
e.preventDefault();
location.reload();
});
// global_link_btn 모두 찾아 변경 // global_link_btn 모두 찾아 변경
$("body").on('click', '#globalSettingSaveBtn', function(e){ $("body").on('click', '#globalSettingSaveBtn', function(e){
e.preventDefault(); e.preventDefault();
globalSettingSave(); if (globalSettingSaveBefore()) {
globalSettingSave();
}
}); });
function globalSettingSaveBefore() {
return true;
}
function globalSettingSaveAfter() {
return true;
}
function globalSettingSave() { function globalSettingSave() {
var formData = getFormdata('#setting'); var formData = getFormdata('#setting');
$.ajax({ $.ajax({
@@ -94,6 +105,7 @@ function globalSettingSave() {
$.notify('<strong>설정을 저장하였습니다.</strong>', { $.notify('<strong>설정을 저장하였습니다.</strong>', {
type: 'success' type: 'success'
}); });
globalSettingSaveAfter();
} else { } else {
$.notify('<strong>설정 저장에 실패하였습니다.</strong>', { $.notify('<strong>설정 저장에 실패하였습니다.</strong>', {
type: 'warning' type: 'warning'
@@ -106,7 +118,10 @@ function globalSettingSave() {
$("body").on('click', '#globalEditBtn', function(e) { $("body").on('click', '#globalEditBtn', function(e) {
e.preventDefault(); e.preventDefault();
file = $(this).data('file'); file = $(this).data('file');
console.log(file); if (file == null) {
var tag = $(this).data('tag');
file = $('#' + tag).val();
}
$.ajax({ $.ajax({
url: '/global/ajax/is_available_edit', url: '/global/ajax/is_available_edit',
type: "POST", type: "POST",
@@ -236,107 +251,188 @@ $("body").on('click', '#globalImmediatelyExecutePageBtn', function(e){
}); });
}); });
$("body").on('click', '#globalDbDeleteDayBtn', function(e){
e.preventDefault();
var tag_id = $(this).data('tag_id');
var day = $('#' + tag_id).val();
globalConfirmModal('DB 삭제', "최근 " + day + "일 이내 데이터를 제외하고 삭제 하시겠습니까?", function() {
globalDbDelete(day);
});
});
$("body").on('click', '#globalDbDeleteBtn', function(e){ $("body").on('click', '#globalDbDeleteBtn', function(e){
e.preventDefault(); e.preventDefault();
document.getElementById("confirm_title").innerHTML = "DB 삭제"; globalConfirmModal('DB 삭제', "전체 목록을 삭제 하시겠습니까?", function() {
document.getElementById("confirm_body").innerHTML = "전체 목록을 삭제 하시겠습니까?"; globalDbDelete(0);
$('#confirm_button').attr('onclick', "globalDbDelete();"); });
$("#confirm_modal").modal();
return;
}); });
function globalDbDelete() { function globalDbDelete(day) {
$.ajax({ $.ajax({
url: '/'+PACKAGE_NAME+'/ajax/' + MODULE_NAME + '/reset_db', url: '/'+PACKAGE_NAME+'/ajax/' + MODULE_NAME + '/db_delete',
type: "POST", type: "POST",
cache: false, cache: false,
data: {}, data: {day:day},
dataType: "json", dataType: "json",
success: function (data) { success: function (data) {
if (data) { if (data == -1) {
$.notify('<strong>삭제하였습니다.</strong>', {
type: 'success'
});
} else {
$.notify('<strong>삭제에 실패하였습니다.</strong>',{ $.notify('<strong>삭제에 실패하였습니다.</strong>',{
type: 'warning' type: 'warning'
}); });
} else {
$.notify('<strong>'+data+'개를 삭제하였습니다.</strong>', {
type: 'success'
});
globalRequestSearch('1');
} }
} }
}); });
} }
///////////////////////////////////////////////////
$("body").on('click', '#globalDbDeleteDayPageBtn', function(e){
e.preventDefault();
var tag_id = $(this).data('tag_id');
var day = $('#' + tag_id).val();
globalConfirmModal('DB 삭제', day + "일 제외 목록을 삭제 하시겠습니까?", function() {
globalDbDeletePage(day);
});
});
$("body").on('click', '#globalDbDeletePageBtn', function(e){ $("body").on('click', '#globalDbDeletePageBtn', function(e){
e.preventDefault(); e.preventDefault();
document.getElementById("confirm_title").innerHTML = "DB 삭제"; globalConfirmModal('DB 삭제', "최근 " + day + "일 이내 데이터를 제외하고 삭제 하시겠습니까?", function() {
document.getElementById("confirm_body").innerHTML = "전체 목록을 삭제 하시겠습니까?"; globalDbDeletePage(0);
$('#confirm_button').attr('onclick', "globalDbDeletePage();"); });
$("#confirm_modal").modal();
return;
}); });
function globalDbDeletePage() { function globalDbDeletePage(day) {
$.ajax({ $.ajax({
url: '/'+PACKAGE_NAME+'/ajax/' + MODULE_NAME + '/' + PAGE_NAME + '/reset_db', url: '/'+PACKAGE_NAME+'/ajax/' + MODULE_NAME + '/' + PAGE_NAME + '/reset_db',
type: "POST", type: "POST",
cache: false, cache: false,
data: {sub:sub}, data: {day:day},
dataType: "json", dataType: "json",
success: function (data) { success: function (data) {
if (data) { if (data == -1) {
$.notify('<strong>삭제하였습니다.</strong>', {
type: 'success'
});
} else {
$.notify('<strong>삭제에 실패하였습니다.</strong>',{ $.notify('<strong>삭제에 실패하였습니다.</strong>',{
type: 'warning' type: 'warning'
}); });
} else {
$.notify('<strong>'+data+'개를 삭제하였습니다.</strong>', {
type: 'success'
});
globalRequestSearch('1');
} }
} }
}); });
} }
$("body").on('click', '#globalDbDeleteItemBtn', function(e){
e.preventDefault();
var db_id = $(this).data('id');
$.ajax({
url: '/'+PACKAGE_NAME+'/ajax/' + MODULE_NAME + '/db_delete_item',
type: "POST",
cache: false,
data: {db_id:db_id},
dataType: "json",
success: function (ret) {
if (ret) {
notify('삭제하였습니다.', 'success');
globalRequestSearch(current_page);
} else {
notify('삭제에 실패하였습니다.', 'warning');
}
}
});
});
$("body").on('click', '#globalDbDeleteItemPageBtn', function(e){
e.preventDefault();
var db_id = $(this).data('id');
$.ajax({
url: '/'+PACKAGE_NAME+'/ajax/' + MODULE_NAME + '/' + PAGE_NAME + '/db_delete_item',
type: "POST",
cache: false,
data: {db_id:db_id},
dataType: "json",
success: function (ret) {
if (ret) {
notify('삭제하였습니다.', 'success');
globalRequestSearch(current_page);
} else {
notify('삭제에 실패하였습니다.', 'warning');
}
}
});
});
$("body").on('click', '#globalJsonBtn', function(e){
e.preventDefault();
showModal(current_data.list[$(this).data('idx')]);
});
/////////////////////////////////////// ///////////////////////////////////////
// Global - 함수 // Global - 함수
/////////////////////////////////////// ///////////////////////////////////////
function globalSendCommand(command, arg1, arg2, arg3, modal_title, callback) { function globalSendCommand(command, arg1, arg2, arg3, callback) {
console.log("globalSendCommand [" + command + '] [' + arg1 + '] [' + arg2 + '] [' + arg3 + '] [' + modal_title + '] [' + callback + ']'); var url = '/' + PACKAGE_NAME + '/ajax/' + MODULE_NAME + '/command';
console.log('/' + PACKAGE_NAME + '/ajax/' + MODULE_NAME + '/command'); return globalSendCommandByUrl(url, command, arg1, arg2, arg3, callback);
}
function globalSendCommandByUrl(url, command, arg1, arg2, arg3, callback) {
$.ajax({ $.ajax({
url: '/' + PACKAGE_NAME + '/ajax/' + MODULE_NAME + '/command', url: url,
type: "POST", type: "POST",
cache: false, cache: false,
data:{command:command, arg1:arg1, arg2:arg2, arg3}, data:{command:command, arg1:arg1, arg2:arg2, arg3},
dataType: "json", dataType: "json",
success: function (ret) { success: function (ret) {
console.log(ret)
if (ret.msg != null) notify(ret.msg, ret.ret); if (ret.msg != null) notify(ret.msg, ret.ret);
if (ret.modal != null) showModal(ret.modal, modal_title, false); if (ret.modal != null) showModal(ret.modal, ret.title, false);
if (ret.json != null) showModal(ret.json, modal_title, true); if (ret.json != null) showModal(ret.json, ret.title, true);
if (callback != null) callback(ret); if (callback != null) callback(ret);
if (ret.reload) location.reload();
} }
}); });
} }
function globalSendCommandPage(command, arg1, arg2, arg3, modal_title, callback) { function globalSendCommandPage(command, arg1, arg2, arg3, callback) {
console.log("globalSendCommandPage [" + command + '] [' + arg1 + '] [' + arg2 + '] [' + arg3 + '] [' + modal_title + '] [' + callback + ']'); var url = '/' + PACKAGE_NAME + '/ajax/' + MODULE_NAME + '/' + PAGE_NAME + '/command';
console.log('/' + PACKAGE_NAME + '/ajax/' + MODULE_NAME + '/command'); return globalSendCommandPageByUrl(url, command, arg1, arg2, arg3, callback);
}
function globalSendCommandPageByUrl(url, command, arg1, arg2, arg3, callback) {
$.ajax({ $.ajax({
url: '/' + PACKAGE_NAME + '/ajax/' + MODULE_NAME + '/' + PAGE_NAME + '/command', url: url,
type: "POST", type: "POST",
cache: false, cache: false,
data:{command:command, arg1:arg1, arg2:arg2, arg3}, data:{command:command, arg1:arg1, arg2:arg2, arg3},
dataType: "json", dataType: "json",
success: function (ret) { success: function (ret) {
if (ret.msg != null) notify(ret.msg, ret.ret); if (ret.msg != null) notify(ret.msg, ret.ret);
if (ret.modal != null) m_modal(ret.modal, modal_title, false); if (ret.modal != null) showModal(ret.modal, ret.title, false);
if (ret.json != null) m_modal(ret.json, modal_title, true); if (ret.json != null) showModal(ret.json, ret.title, true);
if (callback != null) callback(ret); if (callback != null) callback(ret);
if (ret.reload) location.reload();
} }
}); });
} }
@@ -400,6 +496,10 @@ function make_page_html(data) {
str += '<button id="gloablSearchPageBtn" data-page="' + (data.last_page+1) + '" type="button" class="btn btn-secondary">&raquo;</button>' str += '<button id="gloablSearchPageBtn" data-page="' + (data.last_page+1) + '" type="button" class="btn btn-secondary">&raquo;</button>'
} }
if (data.last_page != data.total_page) {
str += '<button id="gloablSearchPageBtn" data-page="' + (data.total_page) + '" type="button" class="btn btn-secondary">'+data.total_page+'</button>'
}
str += '</div> \ str += '</div> \
</div> \ </div> \
</div> \ </div> \
@@ -431,6 +531,22 @@ $("body").on('click', '#globalSearchResetBtn', function(e){
}); });
$("body").on('change', '#option1', function(e){
e.preventDefault();
globalRequestSearch(1);
});
$("body").on('change', '#option2', function(e){
e.preventDefault();
globalRequestSearch(1);
});
$("body").on('change', '#order', function(e){
e.preventDefault();
globalRequestSearch(1);
});
/////////////////////////////////////// ///////////////////////////////////////
// 파일 선택 모달 // 파일 선택 모달
@@ -483,7 +599,6 @@ let listdir = (path = '/', only_dir = true) => {
}, },
dataType: 'json' dataType: 'json'
}).done((datas) => { }).done((datas) => {
console.log(datas)
if (datas.length == 0) { if (datas.length == 0) {
return false; return false;
} }
@@ -510,8 +625,6 @@ let listdir = (path = '/', only_dir = true) => {
} else { } else {
//new_path = (path !== path_spliter) ? path + path_spliter + $(evt.currentTarget).text() : path + $(evt.currentTarget).text(); //new_path = (path !== path_spliter) ? path + path_spliter + $(evt.currentTarget).text() : path + $(evt.currentTarget).text();
new_path = $(evt.currentTarget).data('value'); new_path = $(evt.currentTarget).data('value');
console.log(new_path)
console.log(evt)
} }
*/ */
@@ -587,3 +700,23 @@ function ResizeTextArea() {
/////////////////////////////////////// ///////////////////////////////////////
///////////////////////////////////////
// Confirm MODAL
///////////////////////////////////////
function globalConfirmModal(title, body, func) {
$("#confirm_title").html(title);
$("#confirm_body").html(body);
//$('#confirm_button').attr('onclick', func);
$("body").on('click', '#confirm_button', function(e){
e.stopImmediatePropagation();
e.preventDefault();
func();
});
$("#confirm_modal").modal();
}

View File

@@ -0,0 +1,14 @@
///////////////////////////////////////
// 자주 사용하는 플러그인에 전용 명령
function pluginRcloneLs(remote_path) {
var url = '/rclone/ajax/config/command';
globalSendCommandByUrl(url, "ls", remote_path);
}
function pluginRcloneSize(remote_path) {
var url = '/rclone/ajax/config/command';
globalSendCommandByUrl(url, "size", remote_path);
}

View File

@@ -10,12 +10,13 @@ function j_button_group(h) {
} }
// primary, secondary, success, danger, warning, info, light, dark, white // primary, secondary, success, danger, warning, info, light, dark, white
function j_button(id, text, data={}, color='primary', outline=true, small=false) { function j_button(id, text, data={}, color='primary', outline=true, small=false, _class='') {
var str = '<button id="'+id+'" name="'+id+'" class="btn btn-sm btn'; var str = '<button id="'+id+'" name="'+id+'" class="btn btn-sm btn';
if (outline) { if (outline) {
str += '-outline'; str += '-outline';
} }
str += '-' + color+''; str += '-' + color+'';
str += ' ' + _class;
if (small) { if (small) {
str += ' py-0" style="font-size: 0.8em;"'; str += ' py-0" style="font-size: 0.8em;"';
} else { } else {
@@ -35,9 +36,14 @@ function j_button_small(id, text, data={}, color='primary', outline=true) {
function j_row_start(padding='10', align='center') { function j_row_start(padding='10', align='center') {
var str = '<div class="row" style="padding-top: '+padding+'px; padding-bottom:'+padding+'px; align-items:'+align+';">'; var str = '<div class="row chover" style="padding-top: '+padding+'px; padding-bottom:'+padding+'px; align-items:'+align+';">';
return str; return str;
} }
function j_row_start_hover(padding='10', align='center') {
var str = '<div class="row my_hover" style="padding-top: '+padding+'px; padding-bottom:'+padding+'px; align-items:'+align+';">';
return str;
}
function j_col(w, h, align='left') { function j_col(w, h, align='left') {
var str = '<div class="col-sm-' + w + ' " style="text-align: '+align+'; word-break:break-all;">'; var str = '<div class="col-sm-' + w + ' " style="text-align: '+align+'; word-break:break-all;">';
str += h; str += h;
@@ -45,6 +51,13 @@ function j_col(w, h, align='left') {
return str; return str;
} }
function j_col_with_class(w, h, align='left', _class='context_menu') {
var str = '<div class="col-sm-' + w + ' '+_class+'" style="text-align: '+align+'; word-break:break-all;">';
str += h;
str += '</div>';
return str;
}
function j_col_wide(w, h, align='left') { function j_col_wide(w, h, align='left') {
var str = '<div class="col-sm-' + w + ' " style="padding:0px; margin:0px; text-align: '+align+'; word-break:break-all;">'; var str = '<div class="col-sm-' + w + ' " style="padding:0px; margin:0px; text-align: '+align+'; word-break:break-all;">';
str += h; str += h;
@@ -87,57 +100,101 @@ function j_row_info(left, right, l=2, r=8) {
function j_progress(id, width, label) { function j_progress(id, width, label) {
var str = ''; var str = '';
str += '<div class="progress" style="height: 25px;">' str += '<div class="progress" style="height: 25px;">'
str += '<div id="'+id+'" class="progress-bar" style="background-color:yellow;width:'+width+'%"></div>'; str += '<div id="'+id+'" class="progress-bar bg-success" style="width:'+width+'%"></div>';
str += '<div id="'+id+'_label" class="justify-content-center d-flex w-100 " style="margin-top:2px">'+label+'</div>'; str += '<div id="'+id+'_label" class="justify-content-center d-flex w-100 position-absolute" style="margin-top:2px">'+label+'</div>';
str += '</div>' str += '</div>'
return str; return str;
} }
function j_td(text, width='10', align='center', colspan='1') {
str = '<td scope="col" colspan="'+colspan+'" style="width:'+width+'%; text-align:'+align+';">'+ text + '</td>';
return str;
}
function j_th(text, width='10', align='center', colspan='1') {
str = '<th scope="col" colspan="'+colspan+'" style="width:'+width+'%; text-align:'+align+';">'+ text + '</td>';
return str;
}
function j_info_text(key, value, left=2, right=10) {
row = j_row_start(0);
row += j_col(left, '<strong>' + key + '</strong>', aligh='right');
row += j_col(right, value, aligh='left');
row += j_row_end();
function make_log(key, value, left=2, right=10) {
row = m_col(left, key, aligh='right');
row += m_col(right, value, aligh='left');
return row; return row;
} }
function j_info_text_left(key, value, left=3, right=9) {
row = j_row_start(0);
row += j_col(left, '<strong>' + key + '</strong>', aligh='left');
row += j_col(right, value, aligh='left');
row += j_row_end();
return row;
}
function j_tab_make(data) {
str = '<nav><div class="nav nav-tabs" id="nav-tab" role="tablist">';
for (i in data) {
if (data[i][2]) {
str += '<a class="nav-item nav-link active" id="tab_head_'+data[i][0]+'" data-toggle="tab" href="#tab_content_'+data[i][0]+'" role="tab">'+data[i][1]+'</a>';
} else {
str += '<a class="nav-item nav-link" id="tab_head_'+data[i][0]+'" data-toggle="tab" href="#tab_content_'+data[i][0]+'" role="tab">'+data[i][1]+'</a>';
}
}
str += '</div></nav>';
str += '<div class="tab-content" id="nav-tabContent">';
for (i in data) {
if (data[i][2]) {
str += '<div class="tab-pane fade show active" id="tab_content_'+data[i][0]+'" role="tabpanel" ></div>';
} else {
str += '<div class="tab-pane fade show" id="tab_content_'+data[i][0]+'" role="tabpanel" ></div>';
}
}
str += '</div>';
return str;
}
// javascript에서 화면 생성 // javascript에서 화면 생성
function text_color(text, color='red') { function text_color(text, color='red') {
return '<span style="color:'+color+'; font-weight:bold">' + text + '</span>'; return '<span style="color:'+color+'; font-weight:bold">' + text + '</span>';
} }
function j_pre(text) {
return '<pre style="word-wrap: break-word;white-space: pre-wrap;white-space: -moz-pre-wrap;white-space: -pre-wrap;white-space: -o-pre-wrap;word-break:break-all;">'+text+'</pre>';
}
@@ -277,10 +334,7 @@ document.addEventListener("DOMContentLoaded", function(){
function m_row_start_hover(padding='10', align='center') {
var str = '<div class="row my_hover" style="padding-top: '+padding+'px; padding-bottom:'+padding+'px; align-items:'+align+';">';
return str;
}
function m_row_start_top(padding='10') { function m_row_start_top(padding='10') {
return m_row_start(padding, 'top'); return m_row_start(padding, 'top');
} }
@@ -309,46 +363,5 @@ function m_row_start_color2(padding='10', align='center') {
function m_tab_head(name, active) {
if (active) {
var str = '<a class="nav-item nav-link active" id="id_'+name+'" data-toggle="tab" href="#'+name+'" role="tab">'+name+'</a>';
} else {
var str = '<a class="nav-item nav-link" id="id_'+name+'" data-toggle="tab" href="#'+name+'" role="tab">'+name+'</a>';
}
return str;
}
function m_tab_content(name, content, active) {
if (active) {
var str = '<div class="tab-pane fade show active" id="'+name+'" role="tabpanel" >';
} else {
var str = '<div class="tab-pane fade show" id="'+name+'" role="tabpanel" >';
}
str += content;
str += '</div>'
return str;
}
function m_progress2(id, width, label) {
var str = '';
str += '<div class="progress" style="height: 25px;">'
str += '<div id="'+id+'" class="progress-bar" style="background-color:yellow;width:'+width+'%"></div>';
str += '<div id="'+id+'_label" class="justify-content-center d-flex w-100 position-absolute" style="margin:0px; margin-top:2px">'+label+'</div>';
str += '</div>'
return str;
}

View File

@@ -0,0 +1,35 @@
//
// Google Prettify
// A showdown extension to add Google Prettify (http://code.google.com/p/google-code-prettify/)
// hints to showdown's HTML output.
//
(function () {
var prettify = function () {
return [
{
type: 'output',
filter: function (source) {
return source.replace(/(<pre[^>]*>)?[\n\s]?<code([^>]*)>/gi, function (match, pre, codeClass) {
if (pre) {
return '<pre class="prettyprint linenums"><code' + codeClass + '>';
} else {
return ' <code class="prettyprint">';
}
});
}
}
];
};
// Client-side export
if (typeof window !== 'undefined' && window.showdown && window.showdown.extensions) {
window.showdown.extensions.prettify = prettify;
}
// Server-side export
if (typeof module !== 'undefined') {
module.exports = prettify;
}
}());

File diff suppressed because one or more lines are too long

View File

@@ -35,6 +35,12 @@
<script src="https://cdn.jsdelivr.net/gh/gitbrent/bootstrap4-toggle@3.4.0/js/bootstrap4-toggle.min.js"></script> <script src="https://cdn.jsdelivr.net/gh/gitbrent/bootstrap4-toggle@3.4.0/js/bootstrap4-toggle.min.js"></script>
<!-- end 토글 --> <!-- end 토글 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jquery-contextmenu/2.8.0/jquery.contextMenu.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-contextmenu/2.8.0/jquery.contextMenu.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-contextmenu/2.8.0/jquery.ui.position.js"></script>
</head> </head>
<body class="body "> <body class="body ">
@@ -50,7 +56,8 @@
</div> </div>
</main> </main>
<div class="loading" id="loading"> <div class="loading" id="loading">
<img src="/static/img/loading.gif" /> <!-- <img src="/static/img/loading.gif" /> -->
<img src="/static/img/loader.svg" />
</div> </div>
{{ modals() }} {{ modals() }}
</body> </body>
@@ -59,3 +66,5 @@
<!-- 글로벌 버튼이 모두 나오고 처리--> <!-- 글로벌 버튼이 모두 나오고 처리-->
<script src="{{ url_for('static', filename='js/sjva_global1.js') }}"></script> <script src="{{ url_for('static', filename='js/sjva_global1.js') }}"></script>
<script src="{{ url_for('static', filename='js/ff_global1.js') }}"></script> <script src="{{ url_for('static', filename='js/ff_global1.js') }}"></script>
<script src="{{ url_for('static', filename='js/ff_global_plugin.js') }}"></script>

View File

@@ -1,81 +1,208 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<div> <style>
<nav> /* Unified Log Page Design (matches gds_dviewer) */
{{ macros.m_tab_head_start() }} .log-wrapper {
{{ macros.m_tab_head('old', '이전', true) }} max-width: 1400px;
{{ macros.m_tab_head('new', '실시간', false) }} margin: 0 auto;
{{ macros.m_tab_head_end() }} padding: 20px;
</nav> }
<div class="tab-content" id="nav-tabContent">
{{ macros.m_tab_content_start('이전', 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() }}
{{ macros.m_tab_content_start('실시간', false) }} .log-card {
<div> background: linear-gradient(145deg, rgba(20, 30, 48, 0.95), rgba(36, 59, 85, 0.9));
<textarea id="add" class="col-md-12" rows="30" charswidth="23" disabled style="background-color:#ffffff;visibility:visible"></textarea> border: 1px solid rgba(100, 150, 180, 0.25);
</div> 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() }}
</div> .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;
}
.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> </div>
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function() { function escapeHtml(text) {
setWide(); var div = document.createElement('div');
$('#loading').show(); div.appendChild(document.createTextNode(text));
ResizeTextAreaLog() return div.innerHTML;
})
function ResizeTextAreaLog() {
ClientHeight = window.innerHeight
$("#log").height(ClientHeight-240);
$("#add").height(ClientHeight-260);
} }
$(window).resize(function() { function formatLogLine(line) {
ResizeTextAreaLog(); 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 protocol = window.location.protocol;
var socket = io.connect(protocol + "//" + document.domain + ":" + location.port + "/log"); var socket = io.connect(protocol + "//" + document.domain + ":" + location.port + "/log");
socket.emit("start", {'package':'{{package}}'} );
socket.on('on_start', function(data){ socket.emit("start", {'package':'{{package}}'});
document.getElementById("log").innerHTML += data.data;
document.getElementById("log").scrollTop = document.getElementById("log").scrollHeight; socket.on('on_start', function(data) {
document.getElementById("log").style.visibility = 'visible'; var container = document.getElementById("log-history");
$('#loading').hide(); 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){ socket.on('add', function(data) {
if (data.package == "{{package}}") { if (data.package == "{{package}}") {
var chk = $('#auto_scroll').is(":checked"); var chk = $('#auto_scroll').is(":checked");
document.getElementById("add").innerHTML += data.data; var container = document.getElementById("log-realtime");
if (chk) document.getElementById("add").scrollTop = document.getElementById("add").scrollHeight; container.innerHTML += formatLogLine(data.data);
} if (chk) container.scrollTop = container.scrollHeight;
}
}); });
$("#clear").click(function(e) { $("#clear").click(function(e) {
e.preventDefault(); e.preventDefault();
document.getElementById("add").innerHTML = ''; document.getElementById("log-realtime").innerHTML = '';
}); });
</script>
$("#auto_scroll").click(function(){
var chk = $(this).is(":checked");//.attr('checked');
});
</script>
{% endblock %} {% endblock %}

View File

@@ -25,11 +25,11 @@
<!----> <!---->
{% macro m_tab_head_start() %} {% macro m_tab_head_start() %}
<div class="nav nav-tabs" id="nav-tab" role="tablist"> <div class="nav nav-tabs" id="nav-tab" role="tablist">
{% endmacro %} {% endmacro %}
{% macro m_tab_head_end() %} {% macro m_tab_head_end() %}
</div> </div>
{% endmacro %} {% endmacro %}
{% macro m_tab_head(name, title, active) %} {% macro m_tab_head(name, title, active) %}
@@ -39,12 +39,25 @@
<a class="nav-item nav-link" id="tab_{{name}}" data-toggle="tab" href="#{{name}}" role="tab">{{title}}</a> <a class="nav-item nav-link" id="tab_{{name}}" data-toggle="tab" href="#{{name}}" role="tab">{{title}}</a>
{% endif %} {% endif %}
{% endmacro %} {% endmacro %}
<!----------------------------------------------------------------->
<!-- SETTING -->
<!-- SETTING -->
<!-- SETTING -->
<!------------------------------------------------------------------>
<!-- 설정 -->
<!-- SETTING 기본 틀--> <!-- SETTING 기본 틀-->
{% macro setting_top(left='', padding='10') %} {% macro setting_top(left='', padding='10') %}
@@ -57,6 +70,16 @@
<div class='col-sm-9'> <div class='col-sm-9'>
{% endmacro %} {% endmacro %}
{% macro setting_top_big(left='', padding='10') %}
<div class='row' style="padding-top: {{padding}}px; padding-bottom:{{padding}}px; align-items: center;">
<div class='col-sm-3 set-left'>
{% if left != '' %}
<strong><h4>{{ left }}</h4></strong>
{% endif %}
</div>
<div class='col-sm-9'>
{% endmacro %}
{% macro setting_bottom(desc=None, padding_top='5') %} {% macro setting_bottom(desc=None, padding_top='5') %}
{% if desc is not none %} {% if desc is not none %}
<div style="padding-left:20px; padding-top:{{padding_top}}px;"> <div style="padding-left:20px; padding-top:{{padding_top}}px;">
@@ -247,18 +270,39 @@
<!-- 스케쥴링 작동 버튼--> <!-- 스케쥴링 작동 버튼-->
{% macro setting_global_scheduler_button(is_include, is_running, id='scheduler', left='스케쥴링 작동', desc=['On : 스케쥴링 시작','Off : 스케쥴링 중지']) %} {% macro global_setting_scheduler_button(is_include, is_running, id='scheduler', left='스케쥴링 작동', desc=['On : 스케쥴링 시작','Off : 스케쥴링 중지']) %}
{{ setting_top(left) }} {{ setting_top(left) }}
<div class="input-group col-sm-3"> <div class="input-group col-sm-3">
{% if is_include == True %} {% if is_include == True or is_include == "True" %}
<input id="globalSchedulerSwitchBtn" name="globalSchedulerSwitchBtn" class="form-control form-control-sm" type="checkbox" data-toggle="toggle" checked> <input id="globalSchedulerSwitchBtn" name="globalSchedulerSwitchBtn" class="form-control form-control-sm" type="checkbox" data-toggle="toggle" checked>
{% else %} {% else %}
<input id="globalSchedulerSwitchBtn" name="globalSchedulerSwitchBtn" class="form-control form-control-sm" type="checkbox" data-toggle="toggle"> <input id="globalSchedulerSwitchBtn" name="globalSchedulerSwitchBtn" class="form-control form-control-sm" type="checkbox" data-toggle="toggle">
{% endif %} {% endif %}
{% if is_running == True %} {% if is_running == True or is_running == "True" %}
<span style="padding-left:10px; padding-top: 8px; font-weight: bold;">실행중</span> <span style="padding-left:10px; padding-top: 8px; font-weight: bold;">실행중</span>
{% else %} {% else %}
{% if is_include == True %} {% if is_include == True or is_include == "True" %}
<span style="padding-left:10px; padding-top: 8px; ">대기중</span>
{% endif %}
{% endif %}
</div>
{{ setting_bottom(desc) }}
{% endmacro %}
<!-- 스케쥴링 작동 버튼 페이지 -->
{% macro global_setting_scheduler_button_page(is_include, is_running, id='scheduler', left='스케쥴링 작동', desc=['On : 스케쥴링 시작','Off : 스케쥴링 중지']) %}
{{ setting_top(left) }}
<div class="input-group col-sm-3">
{% if is_include == True or is_include == "True" %}
<input id="globalSchedulerSwitchPageBtn" name="globalSchedulerSwitchPageBtn" class="form-control form-control-sm" type="checkbox" data-toggle="toggle" checked>
{% else %}
<input id="globalSchedulerSwitchPageBtn" name="globalSchedulerSwitchPageBtn" class="form-control form-control-sm" type="checkbox" data-toggle="toggle">
{% endif %}
{% if is_running == True or is_running == "True" %}
<span style="padding-left:10px; padding-top: 8px; font-weight: bold;">실행중</span>
{% else %}
{% if is_include == True or is_include == "True" %}
<span style="padding-left:10px; padding-top: 8px; ">대기중</span> <span style="padding-left:10px; padding-top: 8px; ">대기중</span>
{% endif %} {% endif %}
{% endif %} {% endif %}
@@ -268,13 +312,116 @@
setting_gole
<!-- NOT SETTING --> <!-- SELECT Dummy
<!-- NOT SETTING --> option을 script로 넣을 때 사용
<!-- NOT SETTING --> 예: 시스템 - 전체로그
-->
{% macro setting_select_empty(id, title, col='9', desc=None, value=None) %}
{{ setting_top(title) }}
<div class="input-group col-sm-{{col}}">
<div id="{{id}}_div" name="{{id}}_div"></div>
</div>
{{ setting_bottom(desc) }}
{% endmacro %}
{% macro setting_input_int(id, left, value='', min='', max='', placeholder='', desc=None) %}
{{ setting_top(left) }}
<div class="input-group col-sm-3">
<input id="{{ id }}" name="{{ id }}" type="number" class="form-control form-control-sm"
{% if min != '' %}
min="{{ min }}"
{% endif %}
{% if max != '' %}
max="{{ max }}"
{% endif %}
{% if placeholder != '' %}
placeholder="{{ placeholder }}"
{% endif %}
value="{{ value }}">
</div>
{{ setting_bottom(desc) }}
{% endmacro %}
<!-- 토글버튼형식 -->
{% macro setting_checkbox(id, left, value, desc='') %}
{{ setting_top(left) }}
<div class="input-group col-sm-3">
{% if value == True or value == 'True' or value == 'true' or value == 'On' %}
<input id="{{ id }}" name="{{ id }}" class="form-control form-control-sm" type="checkbox" data-toggle="toggle" checked>
{% else %}
<input id="{{ id }}" name="{{ id }}" class="form-control form-control-sm" type="checkbox" data-toggle="toggle">
{% endif %}
</div>
{{ setting_bottom(desc) }}
{% endmacro %}
<!------------------------------------------------------------------>
<!-- 설정 외 -->
<!-- 리스트 div로 꾸밀때 헤드 -->
{% macro m_hr_head_top() %}
<div class="d-inline-block"></div>
<hr style="width: 100%; margin:0px; background-color:#808080;">
{% endmacro %}
{% macro m_hr_head_bottom() %}
<hr style="width: 100%; margin:0px; margin-bottom:10px; margin-top:2px; background-color:#808080; height:2px" />
{% endmacro %}
<!-- 버튼 그룹 --> <!-- 버튼 그룹 -->
{% macro m_button_group(buttons) %} {% macro m_button_group(buttons) %}
<div class="btn-group btn-group-sm flex-wrap mr-2" role="group"> <div class="btn-group btn-group-sm flex-wrap mr-2" role="group">
@@ -304,6 +451,14 @@
{{ setting_bottom(desc, padding_top='-5') }} {{ setting_bottom(desc, padding_top='-5') }}
{% endmacro %} {% endmacro %}
{% macro info_text_big(id, left, value='', desc=None) %}
{{ setting_top_big(left) }}
<div style="padding-left:20px; padding-top:-5px;">
<span id={{id}}><h4>{{value}}</h4></span>
</div>
{{ setting_bottom(desc, padding_top='-5') }}
{% endmacro %}
{% macro info_text_go(id, left, value='', desc=None, padding=10) %} {% macro info_text_go(id, left, value='', desc=None, padding=10) %}
{{ setting_top(left, padding) }} {{ setting_top(left, padding) }}
<div style="padding-left:20px; padding-top:-5px;"> <div style="padding-left:20px; padding-top:-5px;">
@@ -354,219 +509,208 @@
<!-- SELECT Dummy
option을 script로 넣을 때 사용
예: 시스템 - 전체로그 {% macro m_modal_start(id, title, size) %}
--> <!-- Modal -->
{% macro setting_select_empty(id, title, col='9', desc=None, value=None) %} <div class="modal fade" id="{{id}}" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
{{ setting_top(title) }} <div class="modal-dialog {{size}}">
<div class="input-group col-sm-{{col}}"> <div class="modal-content">
<div id="{{id}}_div" name="{{id}}_div"></div>
</div> <div class="modal-header">
{{ setting_bottom(desc) }} <h4 class="modal-title" id="{{id}}_title">{{title}}</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
</div>
<div class="modal-body" id="{{id}}_modal_body" style="word-break:break-all;">
{% endmacro %} {% endmacro %}
{% macro m_modal_end() %}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-warning" data-dismiss="modal">닫기 (취소)</button>
</div>
<div class="loading" id="modal_loading">
<img src="/static/img/loading.gif" />
<!-- 삭제해야함 --------------------------------------------------------->
<!--
{% macro setting_radio(id, title, radios, value=None, desc=None, disabled=False) %}
{{ setting_top(title) }}
<div class="input-group col-sm-9">
{% for r in radios %}
<div class="custom-control custom-radio custom-control-inline">
{% if value|int == loop.index0 %}
<input id="{{id}}{{loop.index0}}" type="radio" class="custom-control-input" name="{{id}}" value="{{loop.index0}}" checked {% if disabled %} disabled {% endif %}>
{% else %}
<input id="{{id}}{{loop.index0}}" type="radio" class="custom-control-input" name="{{id}}" value="{{loop.index0}}" {% if disabled %} disabled {% endif %}>
{% endif %}
<label class="custom-control-label" for="{{id}}{{loop.index0}}">{{r}}</label>
</div> </div>
{% endfor %}
</div> </div>
{{ setting_bottom(desc) }} </div>
</div>
<!-- Modal end -->
{% endmacro %} {% endmacro %}
-->
<!-- 그룹화 하지 않음.. 삭제-->
<!--
{% macro setting_button(buttons, left='', desc='') %}
{{ setting_top(left) }}
<div class="input-group col-sm-9">
{% for b in buttons %}
{% if not loop.first %}
<span class='text-left' style="padding-left:5px; padding-top:0px">
{% endif %}
<button id="{{b[0]}}" class="btn btn-sm btn-outline-primary">{{b[1]}}</button>
</span>
{% endfor %}
</div>
{{ setting_bottom(desc) }}
{% endmacro %}
-->
<!----------------------------------------------------------------->
{% macro m_modal_end_with_button(buttons) %}
{% macro setting_input_int(id, left, value='', min='', max='', placeholder='', desc=None) %}
{{ setting_top(left) }}
<div class="input-group col-sm-3">
<input id="{{ id }}" name="{{ id }}" type="number" class="form-control form-control-sm"
{% if min != '' %}
min="{{ min }}"
{% endif %}
{% if max != '' %}
max="{{ max }}"
{% endif %}
{% if placeholder != '' %}
placeholder="{{ placeholder }}"
{% endif %}
value="{{ value }}">
</div> </div>
{{ setting_bottom(desc) }} <div class="modal-footer">
{% endmacro %} <div class="btn-group btn-group-sm flex-wrap mr-2" role="group">
{% for b in buttons %}
<button id="{{b[0]}}" class="btn btn-sm btn-outline-primary"
{% if b|length > 2 %}
<!-- 토글버튼형식 --> {% for d in b[2] %}
{% macro setting_checkbox(id, left, value, desc='') %} data-{{d[0]}}="{{d[1]}}""
{{ setting_top(left) }} {% endfor %}
<div class="input-group col-sm-3"> {% endif %}
{% if value == 'True' or value == 'true' or value == 'On' %} >{{b[1]}}</button>
<input id="{{ id }}" name="{{ id }}" class="form-control form-control-sm" type="checkbox" data-toggle="toggle" checked> {% endfor %}
{% else %} <button type="button" class="btn btn-sm btn-warning" data-dismiss="modal">닫기 (취소)</button>
<input id="{{ id }}" name="{{ id }}" class="form-control form-control-sm" type="checkbox" data-toggle="toggle"> </div>
{% endif %}
</div> </div>
{{ setting_bottom(desc) }} <div class="loading" id="modal_loading">
<img src="/static/img/loading.gif" />
</div>
</div>
</div>
</div>
<!-- Modal end -->
{% endmacro %} {% endmacro %}
{% macro print_md(id, text) %}
<div id="{{id}}_div" data-text="{{text}}"></div>
<script type="text/javascript">
ret = converter.makeHtml($('#{{id}}_div').data('text'));
$('#{{id}}_div').html(ret);
</script>
{% endmacro %}
{% macro print_md1(id, text) %}
<script type="text/javascript">
ret = converter.makeHtml($('#{{id}}_div').data('text'));
</script>
{% endmacro %}
<!----------------------------------------------------------->
<!----------------------------------------------------------->
<!----------------------------------------------------------->
<!----------------------------------------------------------->
<!----------------------------------------------------------->
<!--이하 정리 필요------------------------>
<!-- 일반적인 체크박스 --> <!-- 일반적인 체크박스 -->
{% macro setting_default_checkbox(id, left, label, value, desc='') %} {% macro setting_default_checkbox(id, left, label, value, desc='') %}
@@ -584,31 +728,6 @@ option을 script로 넣을 때 사용
{{ setting_bottom(desc) }} {{ setting_bottom(desc) }}
{% endmacro %} {% endmacro %}
<!-- 스케쥴러 스위치 체크박스 전용-->
{% macro setting_scheduler_switch(left='스케쥴링 작동', desc=['On : 스케쥴링 시작','Off : 스케쥴링 중지'], is_include='False', is_running='False') %}
{{ setting_top(left) }}
<div class="input-group col-sm-3">
{% if is_include == 'True' %}
<input id="scheduler_swtich_btn" name="scheduler_swtich_btn" class="form-control form-control-sm" type="checkbox" data-toggle="toggle" checked>
{% else %}
<input id="scheduler_swtich_btn" name="scheduler_swtich_btn" class="form-control form-control-sm" type="checkbox" data-toggle="toggle">
{% endif %}
{% if is_running == 'True' %}
<span style="padding-left:10px; padding-top: 8px;">동작중</span>
{% else %}
{% if is_include == 'True' %}
<span style="padding-left:10px; padding-top: 8px;">대기중</span>
{% endif %}
{% endif %}
</div>
{{ setting_bottom(desc) }}
{% endmacro %}
<!-- <!--
@@ -637,6 +756,26 @@ macros.setting_button_with_info([['toggle_btn', 'Toggle', [{'key':'category', 'v
{% macro select(id, options, col='3', value=None) %}
<div class="input-group col-sm-{{col}}" style="padding-left:0px; padding-top:0px">
<select id="{{id}}" name="{{id}}" class="form-control form-control-sm">
{% for item in options %}
{% if value is not none and value == item[0] %}
<option value="{{ item[0] }}" selected>{{item[1]}}</option>
{% else %}
<option value="{{ item[0] }}">{{item[1]}}</option>
{% endif %}
{% endfor %}
</select>
</div>
{% endmacro %}
<!-- select --> <!-- select -->
{% macro setting_select(id, title, options, col='9', desc=None, value=None) %} {% macro setting_select(id, title, options, col='9', desc=None, value=None) %}
{{ setting_top(title) }} {{ setting_top(title) }}
@@ -655,21 +794,6 @@ macros.setting_button_with_info([['toggle_btn', 'Toggle', [{'key':'category', 'v
{{ setting_bottom(desc) }} {{ setting_bottom(desc) }}
{% endmacro %} {% endmacro %}
{% macro select(id, options, col='3', value=None) %}
<div class="input-group col-sm-{{col}}" style="padding-left:0px; padding-top:0px">
<select id="{{id}}" name="{{id}}" class="form-control form-control-sm">
{% for item in options %}
{% if value is not none and value == item[0] %}
<option value="{{ item[0] }}" selected>{{item[1]}}</option>
{% else %}
<option value="{{ item[0] }}">{{item[1]}}</option>
{% endif %}
{% endfor %}
</select>
</div>
{% endmacro %}
<!-- select + 버튼 --> <!-- select + 버튼 -->
@@ -703,15 +827,6 @@ macros.setting_button_with_info([['toggle_btn', 'Toggle', [{'key':'category', 'v
<!--progress-bar-striped progress-bar-animated--> <!--progress-bar-striped progress-bar-animated-->
{% macro setting_progress(id, left='', desc='') %} {% macro setting_progress(id, left='', desc='') %}
{{ setting_top(left) }} {{ setting_top(left) }}
@@ -725,66 +840,6 @@ macros.setting_button_with_info([['toggle_btn', 'Toggle', [{'key':'category', 'v
{% endmacro %} {% endmacro %}
<!-- 스케쥴링 작동 버튼-->
{% macro setting_scheduler_button(is_include, is_running, id='scheduler', left='스케쥴링 작동', desc=['On : 스케쥴링 시작','Off : 스케쥴링 중지']) %}
{{ setting_top(left) }}
<div class="input-group col-sm-3">
{% if is_include == 'True' %}
<input id="scheduler" name="scheduler" class="form-control form-control-sm" type="checkbox" data-toggle="toggle" checked>
{% else %}
<input id="scheduler" name="scheduler" class="form-control form-control-sm" type="checkbox" data-toggle="toggle">
{% endif %}
{% if is_running == 'True' %}
<span style="padding-left:10px; padding-top: 8px;">동작중</span>
{% else %}
{% if is_include == 'True' %}
<span style="padding-left:10px; padding-top: 8px;">대기중</span>
{% endif %}
{% endif %}
</div>
{{ setting_bottom(desc) }}
{% endmacro %}
{% macro setting_global_scheduler_sub_button(is_include, is_running, id='scheduler', left='스케쥴링 작동', desc=['On : 스케쥴링 시작','Off : 스케쥴링 중지']) %}
{{ setting_top(left) }}
<div class="input-group col-sm-3">
{% if is_include == 'True' %}
<input id="global_scheduler_sub" name="global_scheduler_sub" class="form-control form-control-sm" type="checkbox" data-toggle="toggle" checked>
{% else %}
<input id="global_scheduler_sub" name="global_scheduler_sub" class="form-control form-control-sm" type="checkbox" data-toggle="toggle">
{% endif %}
{% if is_running == 'True' %}
<span style="padding-left:10px; padding-top: 8px;">동작중</span>
{% else %}
{% if is_include == 'True' %}
<span style="padding-left:10px; padding-top: 8px;">대기중</span>
{% endif %}
{% endif %}
</div>
{{ setting_bottom(desc) }}
{% endmacro %}
{% macro setting_global_scheduler_sublogic_button(is_include, is_running, id='scheduler', left='스케쥴링 작동', desc=['On : 스케쥴링 시작','Off : 스케쥴링 중지']) %}
{{ setting_top(left) }}
<div class="input-group col-sm-3">
{% if is_include == 'True' %}
<input id="global_scheduler_sublogic" name="global_scheduler_sublogic" class="form-control form-control-sm" type="checkbox" data-toggle="toggle" checked>
{% else %}
<input id="global_scheduler_sublogic" name="global_scheduler_sublogic" class="form-control form-control-sm" type="checkbox" data-toggle="toggle">
{% endif %}
{% if is_running == 'True' %}
<span style="padding-left:10px; padding-top: 8px;">동작중</span>
{% else %}
{% if is_include == 'True' %}
<span style="padding-left:10px; padding-top: 8px;">대기중</span>
{% endif %}
{% endif %}
</div>
{{ setting_bottom(desc) }}
{% endmacro %}
@@ -803,14 +858,7 @@ macros.setting_button_with_info([['toggle_btn', 'Toggle', [{'key':'category', 'v
{% endmacro %} {% endmacro %}
{% macro m_hr_head_top() %}
<div class="d-inline-block"></div>
<hr style="width: 100%; margin:0px; background-color:#808080;">
{% endmacro %}
{% macro m_hr_head_bottom() %}
<hr style="width: 100%; margin:0px; margin-bottom:10px; margin-top:2px; background-color:#808080; height:2px" />
{% endmacro %}
{% macro m_button(id, text) %} {% macro m_button(id, text) %}
<button id="{{id}}" name="{{id}}" class="btn btn-sm btn-outline-primary">{{text}}</button> <button id="{{id}}" name="{{id}}" class="btn btn-sm btn-outline-primary">{{text}}</button>
@@ -836,59 +884,6 @@ macros.setting_button_with_info([['toggle_btn', 'Toggle', [{'key':'category', 'v
{% macro m_modal_start(id, title, size) %}
<!-- Modal -->
<div class="modal fade" id="{{id}}" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-dialog {{size}}">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="{{id}}_title">{{title}}</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
</div>
<div class="modal-body" id="modal_body" style="word-break:break-all;">
{% endmacro %}
{% macro m_modal_end() %}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">닫기</button>
</div>
<div class="loading" id="modal_loading">
<img src="/static/img/loading.gif" />
</div>
</div>
</div>
</div>
<!-- Modal end -->
{% endmacro %}
{% macro m_modal_start2(id, title, size) %}
<!-- Modal -->
<div class="modal fade" id="{{id}}" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-dialog {{size}}">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="{{id}}_title">{{title}}</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
</div>
<div class="loading" id="modal_loading">
<img src="/static/img/loading.gif" />
</div>
{% endmacro %}
{% macro m_modal_end2() %}
</div>
</div>
</div>
<!-- Modal end -->
{% endmacro %}
{% macro row_start(padding='10') %} {% macro row_start(padding='10') %}
<div class='row' style="padding-top: {{padding}}px; padding-bottom:{{padding}}px; align-items: center;"> <div class='row' style="padding-top: {{padding}}px; padding-bottom:{{padding}}px; align-items: center;">
@@ -1002,16 +997,3 @@ macros.setting_button_with_info([['toggle_btn', 'Toggle', [{'key':'category', 'v
<!-- 다른이름으로 정의함. 나중에 삭제 -->
{% macro buttons(buttons, left='', desc='') %}
{{ setting_top(left) }}
<div class="input-group col-sm-9">
<div class="btn-group btn-group-sm flex-wrap mr-2" role="group">
{% for b in buttons %}
<button id="{{b[0]}}" class="btn btn-sm btn-outline-primary">{{b[1]}}</button>
{% endfor %}
</div>
</div>
{{ setting_bottom(desc) }}
{% endmacro %}

View File

@@ -14,7 +14,7 @@
<div class="modal-body" id="modal_body" style="word-break:break-all;"> <div class="modal-body" id="modal_body" style="word-break:break-all;">
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">닫기</button> <button type="button" class="btn btn-warning" data-dismiss="modal">닫기</button>
<!--<button type="button" class="btn btn-primary">Save changes</button>--> <!--<button type="button" class="btn btn-primary">Save changes</button>-->
</div> </div>
</div> </div>
@@ -70,7 +70,7 @@
<div class="modal-footer"> <div class="modal-footer">
<button type="button" id='select_local_file_modal_confirm_btn' class="btn btn-success" data-dismiss="modal">선택 <button type="button" id='select_local_file_modal_confirm_btn' class="btn btn-success" data-dismiss="modal">선택
</button> </button>
<button type="button" id='select_local_file_modal_cancel_btn' class="btn btn-default" data-dismiss="modal">닫기 <button type="button" id='select_local_file_modal_cancel_btn' class="btn btn-warning" data-dismiss="modal">닫기
</button> </button>
</div> </div>
</div> </div>

View File

@@ -19,7 +19,7 @@
{% if 'uri' in category and category['uri'].startswith('http') %} {% if 'uri' in category and category['uri'].startswith('http') %}
<li class="nav-item"> <a class="nav-link" href="{{ category['uri']}}" target="_blank">{{category['name']}}</a></li> <li class="nav-item"> <a class="nav-link" href="{{ category['uri']}}" target="_blank">{{category['name']}}</a></li>
{% elif 'uri' in category and category['uri'].startswith('http') == False %} {% elif 'uri' in category and category['uri'].startswith('http') == False %}
<li class="nav-item"> <a class="nav-link" href="{{ category['uri']}}">{{category['name']}}</a></li> <li class="nav-item"> <a class="nav-link" href="/{{ category['uri']}}">{{category['name']}}</a></li>
{% else %} {% else %}
<!--{{ category }}--> <!--{{ category }}-->
<li class="nav-item dropdown"> <li class="nav-item dropdown">
@@ -134,10 +134,11 @@
{% if current_menu[0] == plugin['uri'] and 'list' in plugin %} {% if current_menu[0] == plugin['uri'] and 'list' in plugin %}
{% for module in plugin['list'] %} {% for module in plugin['list'] %}
{% if module['uri'] == current_menu[1] and 'list' in module%} {% if module['uri'] == current_menu[1] and 'list' in module%}
<!--{{ module }}--> <!-- {{ module }} -->
<ul class="nav nav-pills bg-light shadow text-dark"> <ul class="nav nav-pills bg-light shadow text-dark">
{% for page in module['list'] %} {% for page in module['list'] %}
{% if current_menu[2] == page['uri'] %} <!--{{ current_menu }}-->
{% if current_menu[2]!= None and page['uri'].startswith(current_menu[2]) %}
<li class="nav-item"><a class="nav-link active" href="/{{ current_menu[0] }}/{{ current_menu[1] }}/{{ page['uri'] }}">{{page['name']}}</a></li> <li class="nav-item"><a class="nav-link active" href="/{{ current_menu[0] }}/{{ current_menu[1] }}/{{ page['uri'] }}">{{page['name']}}</a></li>
{% else %} {% else %}
<li class="nav-item"><a class="nav-link" href="/{{ current_menu[0] }}/{{ current_menu[1] }}/{{ page['uri'] }}">{{page['name']}}</a></li> <li class="nav-item"><a class="nav-link" href="/{{ current_menu[0] }}/{{ current_menu[1] }}/{{ page['uri'] }}">{{page['name']}}</a></li>

View File

@@ -1,30 +1,32 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
{% filter markdown %} <script src="https://cdn.jsdelivr.net/gh/google/code-prettify@master/loader/run_prettify.js?autorun=true&amp;lang=css&lang=python&skin=sunburst"></script>
{{ data }}
{% endfilter %}
<style type="text/css"> <script src="{{ url_for('static', filename='js/showdown_2.1.0.js') }}"></script>
img{ <script src="{{ url_for('static', filename='js/showdown-prettify.js') }}"></script>
<link href="{{ url_for('static', filename='css/showdown.css') }}" rel="stylesheet">
display: block;
max-width: 100%;
margin-right: auto;
}
</style>
<div id="md_div" data-url="{{ arg }}"></div>
<div id="content_div" data-url="{{ arg }}"></div>
<meta id="text" data-text="{{data}}">
<div id="text_div"></div>
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function(){ $(document).ready(function(){
//$('#main_container').attr('class', 'container-fluid'); var converter = new showdown.Converter({extensions: ['prettify']});
}); converter.setOption('tables', true);
converter.setOption('strikethrough', true);
converter.setOption('ghCodeBlocks',true);
text = $('#text').data('text');
if (window.location.href.endsWith('.yaml')) {
text = "```" + text + "```";
}
html = converter.makeHtml(text);
$('#text_div').html(html);
});
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -0,0 +1,30 @@
{% extends "base.html" %}
{% block content %}
{% filter markdown %}
{{ data }}
{% endfilter %}
<style type="text/css">
img{
display: block;
max-width: 100%;
margin-right: auto;
}
</style>
<div id="md_div" data-url="{{ arg }}"></div>
<div id="content_div" data-url="{{ arg }}"></div>
<script type="text/javascript">
$(document).ready(function(){
//$('#main_container').attr('class', 'container-fluid');
});
</script>
{% endblock %}

View File

@@ -1,3 +1,5 @@
<html>
<title>{{data['play_title']}}</title> <title>{{data['play_title']}}</title>
<script src="https://vjs.zencdn.net/7.11.4/video.min.js"></script> <script src="https://vjs.zencdn.net/7.11.4/video.min.js"></script>
<link href="https://vjs.zencdn.net/7.11.4/video-js.css" rel="stylesheet" /> <link href="https://vjs.zencdn.net/7.11.4/video-js.css" rel="stylesheet" />
@@ -63,3 +65,4 @@ player.ready(function(){
player.play(); player.play();
</script> </script>
</html>

View File

@@ -0,0 +1,90 @@
<title>aaaa</title>
<meta charset="UTF-8">
<meta content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0" name="viewport">
<meta content="ie=edge" http-equiv="X-UA-Compatible">
<meta content="width=device-width, initial-scale=1" name="viewport">
<link href="/media/avatar.png" rel="icon" type="image/jpeg">
<link crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.1/dist/css/bootstrap.min.css" integrity="sha384-F3w7mX95PdgyTmZZMECAngseQB83DfGTowi0iMjiWaeVhAn4FJkqJByhZMI3AhiU" rel="stylesheet" />
<link href="https://unpkg.com/aos@2.3.1/dist/aos.css" rel="stylesheet" />
<link href="https://unpkg.com/swiper@7/swiper-bundle.min.css" rel="stylesheet" />
<link href='https://unpkg.com/boxicons@2.1.2/css/boxicons.min.css' rel='stylesheet'>
<link href="/css/style.css" rel="stylesheet" />
<link href="/css/style_dark.css" rel="stylesheet" />
<link href="/media/favicon.png" rel="icon" type="image/jpeg">
<meta property="og:site_name" content="aaaaaaaaaaaaaaaaa" />
<meta property="og:url" content="https://ff.soju6jan.synology.me/gds_tool/api/route/streaming.mp4?apikey=ooo5298ooo&type=file&id=1gtpG7CAUKTWu6wxWtCKx-XN01PMz70v8" />
<meta property="og:type" content="video.other" />
<meta property="og:title" content="Mini rengar xD" />
<meta property="og:image" content="https://outplays.eu/Q5THkfY3/thumbnail.png" />
<meta property="og:video" content="https://ff.soju6jan.synology.me/gds_tool/api/route/streaming.mp4?apikey=ooo5298ooo&type=file&id=1gtpG7CAUKTWu6wxWtCKx-XN01PMz70v8" />
<meta property="og:video:type" content="video/mp4" />
<meta property="og:video:secure_url" content="https://ff.soju6jan.synology.me/gds_tool/api/route/streaming.mp4?apikey=ooo5298ooo&type=file&id=1gtpG7CAUKTWu6wxWtCKx-XN01PMz70v8" />
<meta property="og:video:height" content="1080" />
<meta property="og:video:width" content="1920" />
<meta property="og:image:height" content="1080" />
<meta property="og:image:width" content="1920" />
<script src="https://vjs.zencdn.net/7.11.4/video.min.js"></script>
<link href="https://vjs.zencdn.net/7.11.4/video-js.css" rel="stylesheet" />
<body bgcolor='black'>
<video id=player width=960 height=540 class="video-js vjs-default-skin vjs-16-9" autoplay controls>
<source
src="https://ff.soju6jan.synology.me/gds_tool/api/route/streaming.mp4?apikey=ooo5298ooo&type=file&id=1gtpG7CAUKTWu6wxWtCKx-XN01PMz70v8"
type="application/x-mpegURL" />
</video>
</body>
<script>
var subtitle_src = "aaaa";
let options = {
html5: {
nativeTextTracks: false
},
playbackRates: [.5, .75, 1, 1.5, 2],
controls: true,
preload: "auto",
controlBar: {
playToggle: false,
pictureInPictureToggle: false,
remainingTimeDisplay: true,
qualitySelector: true,
}
};
let player = videojs('player', options);
player.ready(function(){
// set subtitle track
if (subtitle_src != "") {
var suburl = subtitle_src.replace(/&amp;/g, '&');
let captionOption = {
kind: 'captions',
srclang: 'ko',
label: 'Korean',
src: suburl,
mode: 'showing'
};
player.addRemoteTextTrack(captionOption);
var settings = this.textTrackSettings;
settings.setValues({
"backgroundColor": "#000",
"backgroundOpacity": "0",
"edgeStyle": "uniform",
});
settings.updateDisplay();
}
else {
var tracks = player.textTracks();
for (var i = 0; i < tracks.length; i++) {
var track = tracks[i];
}
}
});
player.play();
</script>

View File

@@ -34,8 +34,8 @@ class Util(object):
paging['count'] = count paging['count'] = count
F.logger.debug('paging : c:%s %s %s %s %s %s', count, paging['total_page'], paging['prev_page'], paging['next_page'] , paging['start_page'], paging['last_page']) F.logger.debug('paging : c:%s %s %s %s %s %s', count, paging['total_page'], paging['prev_page'], paging['next_page'] , paging['start_page'], paging['last_page'])
return paging return paging
except Exception as exception: except Exception as e:
F.logger.debug('Exception:%s', exception) F.logger.debug(f"Exception:{str(e)}")
F.logger.debug(traceback.format_exc()) F.logger.debug(traceback.format_exc())
@@ -60,8 +60,8 @@ class Util(object):
ret['dirname'] = max_filename.replace('/%s' % ret['filename'], '') ret['dirname'] = max_filename.replace('/%s' % ret['filename'], '')
ret['max_size'] = max_size ret['max_size'] = max_size
return ret return ret
except Exception as exception: except Exception as e:
F.logger.error('Exception:%s', exception) F.logger.error(f"Exception:{str(e)}")
F.logger.error(traceback.format_exc()) F.logger.error(traceback.format_exc())
@@ -81,8 +81,8 @@ class Util(object):
import shutil import shutil
shutil.rmtree(zip_path) shutil.rmtree(zip_path)
return True return True
except Exception as exception: except Exception as e:
F.logger.error('Exception:%s', exception) F.logger.error(f"Exception:{str(e)}")
F.logger.error(traceback.format_exc()) F.logger.error(traceback.format_exc())
return False return False
@@ -92,12 +92,12 @@ class Util(object):
def make_apikey(url): def make_apikey(url):
from framework import SystemModelSetting from framework import SystemModelSetting
url = url.format(ddns=SystemModelSetting.get('ddns')) url = url.format(ddns=SystemModelSetting.get('ddns'))
if SystemModelSetting.get_bool('auth_use_apikey'): if SystemModelSetting.get_bool('use_apikey'):
if url.find('?') == -1: if url.find('?') == -1:
url += '?' url += '?'
else: else:
url += '&' url += '&'
url += 'apikey=%s' % SystemModelSetting.get('auth_apikey') url += 'apikey=%s' % SystemModelSetting.get('apikey')
return url return url

View File

@@ -1 +1 @@
VERSION="4.0.47" VERSION="4.1.41"

View File

@@ -1,5 +1,6 @@
# 순서 바꾸지 말 것 # 순서 바꾸지 말 것
import os, sys, traceback, re, threading, time, queue import os, sys, traceback, re, threading, time, queue, json, shutil, yaml
import requests
from datetime import datetime, timedelta from datetime import datetime, timedelta
from flask import Blueprint, render_template, jsonify, redirect, request from flask import Blueprint, render_template, jsonify, redirect, request
from sqlalchemy import desc, or_ from sqlalchemy import desc, or_

View File

@@ -94,8 +94,8 @@ class FfmpegQueue(object):
self.download_thread = threading.Thread(target=self.download_thread_function, args=()) self.download_thread = threading.Thread(target=self.download_thread_function, args=())
self.download_thread.daemon = True self.download_thread.daemon = True
self.download_thread.start() self.download_thread.start()
except Exception as exception: except Exception as e:
self.P.logger.error('Exception:%s', exception) self.P.logger.error(f"Exception:{str(e)}")
self.P.logger.error(traceback.format_exc()) self.P.logger.error(traceback.format_exc())
@@ -107,8 +107,8 @@ class FfmpegQueue(object):
if self.current_ffmpeg_count < self.max_ffmpeg_count: if self.current_ffmpeg_count < self.max_ffmpeg_count:
break break
time.sleep(5) time.sleep(5)
except Exception as exception: except Exception as e:
self.P.logger.error('Exception:%s', exception) self.P.logger.error(f"Exception:{str(e)}")
self.P.logger.error(traceback.format_exc()) self.P.logger.error(traceback.format_exc())
self.P.logger.error('current_ffmpeg_count : %s', self.current_ffmpeg_count) self.P.logger.error('current_ffmpeg_count : %s', self.current_ffmpeg_count)
self.P.logger.error('max_ffmpeg_count : %s', self.max_ffmpeg_count) self.P.logger.error('max_ffmpeg_count : %s', self.max_ffmpeg_count)
@@ -153,8 +153,8 @@ class FfmpegQueue(object):
f.start() f.start()
self.current_ffmpeg_count += 1 self.current_ffmpeg_count += 1
self.download_queue.task_done() self.download_queue.task_done()
except Exception as exception: except Exception as e:
self.P.logger.error('Exception:%s', exception) self.P.logger.error(f"Exception:{str(e)}")
self.P.logger.error(traceback.format_exc()) self.P.logger.error(traceback.format_exc())
def ffmpeg_listener(self, **arg): def ffmpeg_listener(self, **arg):
@@ -203,8 +203,8 @@ class FfmpegQueue(object):
self.entity_list.append(entity) self.entity_list.append(entity)
self.download_queue.put(entity) self.download_queue.put(entity)
return True return True
except Exception as exception: except Exception as e:
self.P.logger.error('Exception:%s', exception) self.P.logger.error(f"Exception:{str(e)}")
self.P.logger.error(traceback.format_exc()) self.P.logger.error(traceback.format_exc())
return False return False
@@ -270,8 +270,8 @@ class FfmpegQueue(object):
self.entity_list = new_list self.entity_list = new_list
ret['ret'] = 'refresh' ret['ret'] = 'refresh'
return ret return ret
except Exception as exception: except Exception as e:
self.P.logger.error('Exception:%s', exception) self.P.logger.error(f"Exception:{str(e)}")
self.P.logger.error(traceback.format_exc()) self.P.logger.error(traceback.format_exc())

View File

@@ -8,6 +8,8 @@ from support import SupportYaml
from . import (Logic, default_route, default_route_single_module, from . import (Logic, default_route, default_route_single_module,
get_model_setting) get_model_setting)
from loguru import logger as logger1
class PluginBase(object): class PluginBase(object):
package_name = None package_name = None
@@ -23,6 +25,7 @@ class PluginBase(object):
def __init__(self, setting): def __init__(self, setting):
try: try:
logger1.debug(f"[DEBUG] PluginBase init for {setting.get('filepath')}")
is_system = ('system' == os.path.basename(os.path.dirname(setting['filepath']))) is_system = ('system' == os.path.basename(os.path.dirname(setting['filepath'])))
self.status = "" self.status = ""
self.setting = setting self.setting = setting
@@ -39,6 +42,7 @@ class PluginBase(object):
self.logger = F.get_logger(self.package_name) self.logger = F.get_logger(self.package_name)
self.blueprint = Blueprint(self.package_name, self.package_name, url_prefix=f'/{self.package_name}', template_folder=os.path.join(os.path.dirname(setting['filepath']), 'templates'), static_folder=os.path.join(os.path.dirname(setting['filepath']), 'static')) self.blueprint = Blueprint(self.package_name, self.package_name, url_prefix=f'/{self.package_name}', template_folder=os.path.join(os.path.dirname(setting['filepath']), 'templates'), static_folder=os.path.join(os.path.dirname(setting['filepath']), 'static'))
self.menu = setting['menu'] self.menu = setting['menu']
logger1.debug(f"[DEBUG] Menu set for {self.package_name}: {self.menu}")
self.setting_menu = setting.get('setting_menu', None) self.setting_menu = setting.get('setting_menu', None)
self.ModelSetting = None self.ModelSetting = None
@@ -52,6 +56,7 @@ class PluginBase(object):
self.home_module = setting.get('home_module') self.home_module = setting.get('home_module')
self.status = "init_success" self.status = "init_success"
self.config = {} self.config = {}
self.recent_menu_plugin_except_list = setting.get('recent_menu_plugin_except_list', [])
except Exception as e: except Exception as e:
self.logger.error(f'Exception:{str(e)}') self.logger.error(f'Exception:{str(e)}')
self.logger.error(traceback.format_exc()) self.logger.error(traceback.format_exc())
@@ -59,10 +64,16 @@ class PluginBase(object):
def set_module_list(self, mod_list): def set_module_list(self, mod_list):
try: try:
# self.module_list = []
for mod in mod_list: for mod in mod_list:
logger1.debug(mod)
mod_ins = mod(self) mod_ins = mod(self)
# self.logger.debug(mod_ins)
logger1.debug(mod_ins)
self.module_list.append(mod_ins) self.module_list.append(mod_ins)
if self.home_module == None:
self.home_module = self.module_list[0].name
except Exception as e: except Exception as e:
F.logger.error(f'[{self.package_name}] Exception:{str(e)}') F.logger.error(f'[{self.package_name}] Exception:{str(e)}')
F.logger.error(traceback.format_exc()) F.logger.error(traceback.format_exc())
@@ -77,16 +88,31 @@ class PluginBase(object):
def plugin_load(self): def plugin_load(self):
self.logic.plugin_load() if self.logic:
self.logic.plugin_load()
def plugin_load_celery(self):
if self.logic:
self.logic.plugin_load_celery()
def plugin_unload(self): def plugin_unload(self):
self.logic.plugin_unload() if self.logic:
self.logic.plugin_unload()
def get_first_manual_path(self): def get_first_manual_path(self):
for __ in self.menu['list']: for __ in self.menu['list']:
if __['uri'] == 'manual' and len(__['list']) > 0: if __['uri'] == 'manual' and len(__['list']) > 0:
return __['list'][0]['uri'] return __['list'][0]['uri']
def get_module(self, sub):
try:
for module in self.module_list:
if module.name == sub:
return module
except Exception as e:
self.logger.error(f'Exception:{str(e)}')
#self.P.logger.error(traceback.format_exc())
def create_plugin_instance(config): def create_plugin_instance(config):
ins = PluginBase(config) ins = PluginBase(config)

View File

@@ -32,14 +32,39 @@ class Logic(object):
if self.P.ModelSetting is not None: if self.P.ModelSetting is not None:
for module in self.P.module_list: for module in self.P.module_list:
key = f'{module.name}_auto_start' key = f'{module.name}_auto_start'
if self.P.ModelSetting.has_key(key) and self.P.ModelSetting.get_bool(key): key2 = f'{module.name}_interval'
if self.P.ModelSetting.has_key(key) and self.P.ModelSetting.get_bool(key) and self.P.ModelSetting.has_key(key2):
self.scheduler_start(module.name) self.scheduler_start(module.name)
if module.page_list is not None: if module.page_list is not None:
for page_instance in module.page_list: for page_instance in module.page_list:
key = f'{module.name}_{page_instance.name}_auto_start' key = f'{module.name}_{page_instance.name}_auto_start'
if self.P.ModelSetting.has_key(key) and self.P.ModelSetting.get_bool(key): if self.P.ModelSetting.has_key(key) and self.P.ModelSetting.get_bool(key):
self.scheduler_start_sub(module.name, page_instance.name) self.scheduler_start_sub(module.name, page_instance.name)
key1 = f'{module.name}_db_auto_delete'
key2 = f'{module.name}_db_delete_day'
if self.P.ModelSetting.has_key(key1) and self.P.ModelSetting.has_key(key2) and self.P.ModelSetting.get_bool(key1):
try: module.db_delete(self.P.ModelSetting.get_int(key2))
except: pass
if module.page_list == None:
continue
for page_instance in module.page_list:
key1 = f'{module.name}_{page_instance.name}_db_auto_delete'
key2 = f'{module.name}_{page_instance.name}_db_delete_day'
if self.P.ModelSetting.has_key(key1) and self.P.ModelSetting.has_key(key2) and self.P.ModelSetting.get_bool(key1):
try: page_instance.db_delete(self.P.ModelSetting.get_int(key2))
except: pass
def plugin_load_celery(self):
self.P.logger.debug('%s plugin_load_celery', self.P.package_name)
for module in self.P.module_list:
module.plugin_load_celery()
if module.page_list is not None:
for page_instance in module.page_list:
page_instance.plugin_load_celery()
def db_init(self): def db_init(self):
try: try:
@@ -88,7 +113,7 @@ class Logic(object):
try: try:
job_id = '%s_%s' % (self.P.package_name, module_name) job_id = '%s_%s' % (self.P.package_name, module_name)
module = self.get_module(module_name) module = self.get_module(module_name)
job = Job(self.P.package_name, job_id, module.get_scheduler_interval(), self.scheduler_function, module.get_scheduler_desc(), args=module_name) job = Job(self.P.package_name, job_id, module.get_scheduler_interval(), self.scheduler_function, module.get_scheduler_desc(), args=(module_name,))
F.scheduler.add_job_instance(job) F.scheduler.add_job_instance(job)
except Exception as e: except Exception as e:
self.P.logger.error(f'Exception:{str(e)}') self.P.logger.error(f'Exception:{str(e)}')
@@ -112,15 +137,25 @@ class Logic(object):
self.P.logger.error(f'Exception:{str(e)}') self.P.logger.error(f'Exception:{str(e)}')
self.P.logger.error(traceback.format_exc()) self.P.logger.error(traceback.format_exc())
def reset_db(self, module_name):
def db_delete(self, module_name, page_name, day):
try: try:
module = self.get_module(module_name) module = self.get_module(module_name)
return module.reset_db() if module == None:
return False
if page_name != None:
page = module.get_page(page_name)
if page != None:
return page.db_delete(day)
else:
return module.db_delete(day)
except Exception as e: except Exception as e:
self.P.logger.error(f'Exception:{str(e)}') self.P.logger.error(f'Exception:{str(e)}')
self.P.logger.error(traceback.format_exc()) self.P.logger.error(traceback.format_exc())
def one_execute(self, module_name): def one_execute(self, module_name):
self.P.logger.debug('one_execute :%s', module_name) self.P.logger.debug('one_execute :%s', module_name)
try: try:
@@ -166,6 +201,7 @@ class Logic(object):
self.P.logger.error(f'Exception:{str(e)}') self.P.logger.error(f'Exception:{str(e)}')
self.P.logger.error(traceback.format_exc()) self.P.logger.error(traceback.format_exc())
"""
def process_telegram_data(self, data, target=None): def process_telegram_data(self, data, target=None):
try: try:
for module in self.P.module_list: for module in self.P.module_list:
@@ -174,7 +210,7 @@ class Logic(object):
except Exception as e: except Exception as e:
self.P.logger.error(f'Exception:{str(e)}') self.P.logger.error(f'Exception:{str(e)}')
self.P.logger.error(traceback.format_exc()) self.P.logger.error(traceback.format_exc())
"""
@@ -303,12 +339,23 @@ class Logic(object):
def arg_to_dict(self, arg): def arg_to_dict(self, arg):
"""
import urllib.parse import urllib.parse
tmp = urllib.parse.unquote(arg) tmp = urllib.parse.unquote(arg)
tmps = tmp.split('&') tmps = tmp.split('&')
ret = {} ret = {}
for tmp in tmps: for tmp in tmps:
_ = tmp.split('=') _ = tmp.split('=', 1)
ret[_[0]] = _[1] ret[_[0]] = _[1]
return ret return ret
"""
import html
import urllib.parse
char = '||!||'
arg = arg.replace('&amp;', char)
tmp = html.unescape(arg)
tmp = urllib.parse.unquote(tmp)
tmp = dict(urllib.parse.parse_qs(tmp, keep_blank_values=True))
ret = {k: v[0].replace(char, '&') for k, v in tmp.items()}
return ret

View File

@@ -39,6 +39,8 @@ class PluginModuleBase(object):
def get_page(self, page_name): def get_page(self, page_name):
try: try:
if self.page_list == None:
return
for page in self.page_list: for page in self.page_list:
if page_name == page.name: if page_name == page.name:
return page return page
@@ -48,10 +50,22 @@ class PluginModuleBase(object):
def process_menu(self, page, req): def process_menu(self, page, req):
if self.page_list is not None: from framework import F
page_ins = self.get_page(page)
if page_ins != None: try:
return page_ins.process_menu(req) if self.page_list is not None:
page_ins = self.get_page(page)
if page_ins != None:
return page_ins.process_menu(req)
arg = self.P.ModelSetting.to_dict() if self.P.ModelSetting != None else {}
arg['path_data'] = F.config['path_data']
arg['is_include'] = F.scheduler.is_include(self.get_scheduler_name())
arg['is_running'] = F.scheduler.is_running(self.get_scheduler_name())
return render_template(f'{self.P.package_name}_{self.name}_{page}.html', arg=arg)
except Exception as e:
self.P.logger.error(f'Exception:{str(e)}')
self.P.logger.error(traceback.format_exc())
return render_template('sample.html', title=f"PluginModuleBase-process_menu{self.P.package_name}/{self.name}/{page}") return render_template('sample.html', title=f"PluginModuleBase-process_menu{self.P.package_name}/{self.name}/{page}")
def process_ajax(self, sub, req): def process_ajax(self, sub, req):
@@ -69,12 +83,18 @@ class PluginModuleBase(object):
def scheduler_function(self): def scheduler_function(self):
pass pass
def reset_db(self): def db_delete(self, day):
pass if self.web_list_model != None:
return self.web_list_model.delete_all(day)
def plugin_load(self): def plugin_load(self):
pass pass
def plugin_load_celery(self):
pass
def plugin_unload(self): def plugin_unload(self):
pass pass
@@ -115,12 +135,37 @@ class PluginModuleBase(object):
pass pass
def arg_to_dict(self, arg):
return self.P.logic.arg_to_dict(arg)
def get_scheduler_name(self):
return f'{self.P.package_name}_{self.name}'
def process_discord_data(self, data):
pass
def start_celery(self, func, on_message=None, *args, page=None):
from framework import F
if F.config['use_celery']:
result = func.apply_async(args)
try:
if on_message != None:
ret = result.get(on_message=on_message, propagate=True)
else:
ret = result.get()
except:
ret = result.get()
else:
if on_message == None:
ret = func(*args)
else:
if page == None:
ret = func(self, *args)
else:
ret = func(page, *args)
return ret
@@ -153,7 +198,8 @@ class PluginPageBase(object):
arg = self.P.ModelSetting.to_dict() arg = self.P.ModelSetting.to_dict()
return render_template(f'{self.P.package_name}_{self.parent.name}_{self.name}.html', arg=arg) return render_template(f'{self.P.package_name}_{self.parent.name}_{self.name}.html', arg=arg)
except Exception as e: except Exception as e:
pass self.P.logger.error(f'Exception:{str(e)}')
self.P.logger.error(traceback.format_exc())
return render_template('sample.html', title=f"PluginPageBase-process_menu --- {self.P.package_name}/{self.parent.name}/{self.name}") return render_template('sample.html', title=f"PluginPageBase-process_menu --- {self.P.package_name}/{self.parent.name}/{self.name}")
@@ -176,6 +222,9 @@ class PluginPageBase(object):
def plugin_load(self): def plugin_load(self):
pass pass
def plugin_load_celery(self):
pass
# logic # logic
def plugin_unload(self): def plugin_unload(self):
pass pass
@@ -207,3 +256,41 @@ class PluginPageBase(object):
pass pass
def arg_to_dict(self, arg):
return self.P.logic.arg_to_dict(arg)
def get_page(self, page_name):
return self.parent.get_page(page_name)
def get_module(self, module_name):
return self.parent.get_module(module_name)
def process_discord_data(self, data):
pass
def db_delete(self, day):
if self.web_list_model != None:
return self.web_list_model.delete_all(day)
def start_celery(self, func, on_message, *args):
return self.parent.start_celery(func, on_message, *args, page=self)
"""
def start_celery(self, func, on_message=None, *args):
from framework import F
if F.config['use_celery']:
result = func.apply_async(args)
try:
if on_message != None:
ret = result.get(on_message=on_message, propagate=True)
else:
ret = result.get()
except:
ret = result.get()
else:
ret = func(*args)
return ret
"""

View File

@@ -1,3 +1,4 @@
import sqlite3
import traceback import traceback
from datetime import datetime, timedelta from datetime import datetime, timedelta
@@ -45,7 +46,7 @@ class ModelBase(F.db.Model):
paging['next_page'] = False paging['next_page'] = False
paging['current_page'] = current_page paging['current_page'] = current_page
paging['count'] = count paging['count'] = count
F.logger.debug('paging : c:%s %s %s %s %s %s', count, paging['total_page'], paging['prev_page'], paging['next_page'] , paging['start_page'], paging['last_page']) #F.logger.debug('paging : c:%s %s %s %s %s %s', count, paging['total_page'], paging['prev_page'], paging['next_page'] , paging['start_page'], paging['last_page'])
return paging return paging
except Exception as e: except Exception as e:
cls.P.logger.error(f'Exception:{str(e)}') cls.P.logger.error(f'Exception:{str(e)}')
@@ -89,24 +90,30 @@ class ModelBase(F.db.Model):
return False return False
@classmethod @classmethod
def delete_all(cls, days=None): def delete_all(cls, day=None):
count = -1
try: try:
with F.app.app_context(): with F.app.app_context():
if days == None: if day == None or day in [0, '0']:
F.db.session.query(cls).delete() count = F.db.session.query(cls).delete()
F.db.session.commit()
else: else:
now = datetime.now() now = datetime.now()
ago = now - timedelta(days=int(days)) ago = now - timedelta(days=int(day))
#ago.hour = 0 #ago.hour = 0
#ago.minute = 0 #ago.minute = 0
count = F.db.session.query(cls).filter(cls.created_time > ago).delete() count = F.db.session.query(cls).filter(cls.created_time < ago).delete()
cls.P.logger.info(f"delete_all {days=} {count=}") cls.P.logger.info(f"delete_all {day=} {count=}")
return True F.db.session.commit()
db_file = F.app.config['SQLALCHEMY_BINDS'][cls.P.package_name].replace('sqlite:///', '').split('?')[0]
connection = sqlite3.connect(db_file)
cursor = connection.cursor()
cursor.execute('VACUUM;')
connection.close()
except Exception as e: except Exception as e:
cls.P.logger.error(f'Exception:{str(e)}') cls.P.logger.error(f'Exception:{str(e)}')
cls.P.logger.error(traceback.format_exc()) cls.P.logger.error(traceback.format_exc())
return False return count
@classmethod @classmethod
@@ -135,12 +142,12 @@ class ModelBase(F.db.Model):
if cls.P.ModelSetting is not None and cls.__tablename__ is not None: if cls.P.ModelSetting is not None and cls.__tablename__ is not None:
cls.P.ModelSetting.set(f'{cls.__tablename__}_last_list_option', f'{order}|{page}|{search}|{option1}|{option2}') cls.P.ModelSetting.set(f'{cls.__tablename__}_last_list_option', f'{order}|{page}|{search}|{option1}|{option2}')
except Exception as e: except Exception as e:
F.logger.error('Exception:%s', e) F.logger.error(f"Exception:{str(e)}")
F.logger.error(traceback.format_exc()) F.logger.error(traceback.format_exc())
F.logger.error(f'{cls.__tablename__}_last_list_option ERROR!' ) F.logger.error(f'{cls.__tablename__}_last_list_option ERROR!' )
return ret return ret
except Exception as e: except Exception as e:
cls.P.logger.error('Exception:%s', e) cls.P.logger.error(f"Exception:{str(e)}")
cls.P.logger.error(traceback.format_exc()) cls.P.logger.error(traceback.format_exc())
@@ -149,6 +156,10 @@ class ModelBase(F.db.Model):
def make_query(cls, req, order='desc', search='', option1='all', option2='all'): def make_query(cls, req, order='desc', search='', option1='all', option2='all'):
with F.app.app_context(): with F.app.app_context():
query = F.db.session.query(cls) query = F.db.session.query(cls)
if order == 'desc':
query = query.order_by(desc(cls.id))
else:
query = query.order_by(cls.id)
return query return query
@@ -171,3 +182,16 @@ class ModelBase(F.db.Model):
query = query.filter(field.like('%'+search+'%')) query = query.filter(field.like('%'+search+'%'))
#query = query1.union(query2) #query = query1.union(query2)
return query return query
@classmethod
def get_list_by_status(cls, status):
try:
with F.app.app_context():
query = F.db.session.query(cls).filter(
cls.status == status,
)
query = query.order_by(cls.id)
return query.all()
except:
pass

View File

@@ -107,6 +107,8 @@ def get_model_setting(package_name, logger, table_name=None):
if ModelSetting.get(key) != value: if ModelSetting.get(key) != value:
change_list.append(key) change_list.append(key)
entity = F.db.session.query(ModelSetting).filter_by(key=key).with_for_update().first() entity = F.db.session.query(ModelSetting).filter_by(key=key).with_for_update().first()
if entity == None:
logger.warning(f"NOT exist setting key: {key}")
entity.value = value entity.value = value
F.db.session.commit() F.db.session.commit()
return True, change_list return True, change_list
@@ -117,7 +119,8 @@ def get_model_setting(package_name, logger, table_name=None):
return False, [] return False, []
@staticmethod @staticmethod
def get_list(key, delimeter='\n', comment=' #'): def get_list(key, delimeter='\n', comment='#'):
value = None
try: try:
value = ModelSetting.get(key).replace('\n', delimeter) value = ModelSetting.get(key).replace('\n', delimeter)
if comment is None: if comment is None:

View File

@@ -27,7 +27,12 @@ def default_route(P):
def first_menu(sub): def first_menu(sub):
try: try:
if P.ModelSetting is not None and (P.package_name == 'system' and sub != 'home'): if P.ModelSetting is not None and (P.package_name == 'system' and sub != 'home'):
P.ModelSetting.set('recent_menu_plugin', '{}'.format(sub)) current_menu = sub
for except_menu in P.recent_menu_plugin_except_list:
if current_menu.startswith(except_menu) or current_menu == except_menu:
break
else:
P.ModelSetting.set('recent_menu_plugin', current_menu)
for module in P.module_list: for module in P.module_list:
if sub == module.name: if sub == module.name:
first_menu = module.get_first_menu() first_menu = module.get_first_menu()
@@ -45,8 +50,8 @@ def default_route(P):
return redirect(f"/{P.package_name}/manual/{P.get_first_manual_path()}") return redirect(f"/{P.package_name}/manual/{P.get_first_manual_path()}")
return render_template('sample.html', title='%s - %s' % (P.package_name, sub)) return render_template('sample.html', title='%s - %s' % (P.package_name, sub))
except Exception as exception: except Exception as e:
P.logger.error('Exception:%s', exception) P.logger.error(f"Exception:{str(e)}")
P.logger.error(traceback.format_exc()) P.logger.error(traceback.format_exc())
@P.blueprint.route('/manual/<path:path>', methods=['GET', 'POST']) @P.blueprint.route('/manual/<path:path>', methods=['GET', 'POST'])
@@ -57,16 +62,43 @@ def default_route(P):
filepath = os.path.join(plugin_root, *path.split('/')) filepath = os.path.join(plugin_root, *path.split('/'))
from support import SupportFile from support import SupportFile
data = SupportFile.read_file(filepath) data = SupportFile.read_file(filepath)
if filepath.endswith('.mdf'):
try:
from support import SupportSC
data = SupportSC.decode(data)
except:
pass
return render_template('manual.html', data=data) return render_template('manual.html', data=data)
except Exception as exception: except Exception as e:
P.logger.error('Exception:%s', exception) P.logger.error(f"Exception:{str(e)}")
P.logger.error(traceback.format_exc()) P.logger.error(traceback.format_exc())
@P.blueprint.route('/<module_name>/manual/<path:path>', methods=['GET', 'POST'])
@login_required
def module_manual(module_name, path):
try:
plugin_root = os.path.dirname(P.blueprint.template_folder)
filepath = os.path.join(plugin_root, *path.split('/'))
from support import SupportFile
data = SupportFile.read_file(filepath)
return render_template('manual.html', data=data)
except Exception as e:
P.logger.error(f"Exception:{str(e)}")
P.logger.error(traceback.format_exc())
@P.blueprint.route('/<sub>/<sub2>', methods=['GET', 'POST']) @P.blueprint.route('/<sub>/<sub2>', methods=['GET', 'POST'])
@login_required @login_required
def second_menu(sub, sub2): def second_menu(sub, sub2):
if sub2 == 'null':
return
if P.ModelSetting is not None: if P.ModelSetting is not None:
P.ModelSetting.set('recent_menu_plugin', '{}|{}'.format(sub, sub2)) current_menu = f"{sub}|{sub2}"
for except_menu in P.recent_menu_plugin_except_list:
if current_menu.startswith(except_menu) or current_menu == except_menu:
break
else:
P.ModelSetting.set('recent_menu_plugin', current_menu)
try: try:
for module in P.module_list: for module in P.module_list:
if sub == module.name: if sub == module.name:
@@ -74,8 +106,8 @@ def default_route(P):
if sub == 'log': if sub == 'log':
return render_template('log.html', package=P.package_name) return render_template('log.html', package=P.package_name)
return render_template('sample.html', title='%s - %s' % (P.package_name, sub)) return render_template('sample.html', title='%s - %s' % (P.package_name, sub))
except Exception as exception: except Exception as e:
P.logger.error('Exception:%s', exception) P.logger.error(f"Exception:{str(e)}")
P.logger.error(traceback.format_exc()) P.logger.error(traceback.format_exc())
######################################################### #########################################################
@@ -84,7 +116,7 @@ def default_route(P):
@P.blueprint.route('/ajax/<sub>', methods=['GET', 'POST']) @P.blueprint.route('/ajax/<sub>', methods=['GET', 'POST'])
@login_required @login_required
def ajax(sub): def ajax(sub):
P.logger.debug('AJAX %s %s', P.package_name, sub) #P.logger.debug('AJAX %s %s', P.package_name, sub)
try: try:
# global # global
if sub == 'setting_save': if sub == 'setting_save':
@@ -93,24 +125,26 @@ def default_route(P):
module.setting_save_after(change_list) module.setting_save_after(change_list)
return jsonify(ret) return jsonify(ret)
except Exception as exception: except Exception as e:
P.logger.error('Exception:%s', exception) P.logger.error(f"Exception:{str(e)}")
P.logger.error(traceback.format_exc()) P.logger.error(traceback.format_exc())
@P.blueprint.route('/ajax/<module_name>/<cmd>', methods=['GET', 'POST']) @P.blueprint.route('/ajax/<module_name>/<cmd>', methods=['GET', 'POST'])
@login_required @login_required
def second_ajax(module_name, cmd): def second_ajax(module_name, cmd):
# P.logger.debug(f"[CORE-DEBUG] second_ajax: package={P.package_name}, module={module_name}, cmd={cmd}")
try: try:
for module in P.module_list: for module in P.module_list:
if cmd == 'scheduler': if cmd == 'scheduler':
go = request.form['scheduler'] go = request.form['scheduler']
if go == 'true': if go == 'true':
P.logic.scheduler_start(module_name) P.logic.scheduler_start(module_name)
else: else:
P.logic.scheduler_stop(module_name) P.logic.scheduler_stop(module_name)
return jsonify(go) return jsonify(go)
elif cmd == 'reset_db': elif cmd == 'db_delete':
ret = P.logic.reset_db(module_name) day = request.form['day']
ret = P.logic.db_delete(module_name, None, day)
return jsonify(ret) return jsonify(ret)
elif cmd == 'one_execute': elif cmd == 'one_execute':
ret = P.logic.one_execute(module_name) ret = P.logic.one_execute(module_name)
@@ -118,18 +152,25 @@ def default_route(P):
elif cmd == 'immediately_execute': elif cmd == 'immediately_execute':
ret = P.logic.immediately_execute(module_name) ret = P.logic.immediately_execute(module_name)
return jsonify(ret) return jsonify(ret)
elif cmd == 'web_list':
model = P.logic.get_module(module_name).web_list_model
if model != None:
return jsonify(model.web_list(request))
if module_name == module.name: if module_name == module.name:
if cmd == 'command': if cmd == 'command':
return module.process_command(request.form['command'], request.form.get('arg1'), request.form.get('arg2'), request.form.get('arg3'), request) return module.process_command(request.form['command'], request.form.get('arg1'), request.form.get('arg2'), request.form.get('arg3'), request)
elif cmd == 'web_list':
model = P.logic.get_module(module_name).web_list_model
if model != None:
return jsonify(model.web_list(request))
elif cmd == 'db_delete_item':
db_id = request.form['db_id']
ret = False
if module.web_list_model != None:
ret = module.web_list_model.delete_by_id(db_id)
return jsonify(ret)
else: else:
return module.process_ajax(cmd, request) return module.process_ajax(cmd, request)
except Exception as exception: except Exception as e:
P.logger.error('Exception:%s', exception) P.logger.error(f"Exception:{str(e)}")
P.logger.error(traceback.format_exc()) P.logger.error(traceback.format_exc())
@P.blueprint.route('/ajax/<module_name>/<page_name>/<command>', methods=['GET', 'POST']) @P.blueprint.route('/ajax/<module_name>/<page_name>/<command>', methods=['GET', 'POST'])
@@ -148,10 +189,10 @@ def default_route(P):
else: else:
P.logic.scheduler_stop_sub(module_name, page_name) P.logic.scheduler_stop_sub(module_name, page_name)
return jsonify(go) return jsonify(go)
#elif command == 'reset_db': elif command == 'db_delete':
# sub = request.form['sub'] day = request.form['day']
# ret = P.logic.reset_db(sub) ret = P.logic.db_delete(module_name, page_name, day)
# return jsonify(ret) return jsonify(ret)
elif command == 'one_execute': elif command == 'one_execute':
ret = P.logic.one_execute_sub(module_name, page_name) ret = P.logic.one_execute_sub(module_name, page_name)
return jsonify(ret) return jsonify(ret)
@@ -160,25 +201,30 @@ def default_route(P):
return jsonify(ret) return jsonify(ret)
elif command == 'command': elif command == 'command':
return ins_page.process_command(request.form['command'], request.form.get('arg1'), request.form.get('arg2'), request.form.get('arg3'), request) return ins_page.process_command(request.form['command'], request.form.get('arg1'), request.form.get('arg2'), request.form.get('arg3'), request)
elif command == 'db_delete_item':
db_id = request.form['db_id']
ret = False
if ins_page.web_list_model != None:
ret = ins_page.web_list_model.delete_by_id(db_id)
return jsonify(ret)
else: else:
return ins_page.process_ajax(command, request) return ins_page.process_ajax(command, request)
P.logger.error(f"not process ajax : {P.package_name} {module_name} {page_name} {command}") P.logger.error(f"not process ajax : {P.package_name} {module_name} {page_name} {command}")
except Exception as exception: except Exception as e:
P.logger.error('Exception:%s', exception) P.logger.error(f"Exception:{str(e)}")
P.logger.error(traceback.format_exc()) P.logger.error(traceback.format_exc())
######################################################### #########################################################
# API - 외부 # API - 외부
######################################################### #########################################################
# 단일 모듈인 경우 모듈이름을 붙이기 불편하여 추가. # 단일 모듈인 경우 모듈이름을 붙이기 불편하여 추가.
@P.blueprint.route('/api/<sub2>', methods=['GET', 'POST']) @P.blueprint.route('/api/<sub>', methods=['GET', 'POST'])
@F.check_api @F.check_api
def api_first(sub2): def api_first(sub):
try: try:
for module in P.module_list: return P.module_list[0].process_api(sub, request)
return module.process_api(sub2, request) except Exception as e:
except Exception as exception: P.logger.error(f"Exception:{str(e)}")
P.logger.error('Exception:%s', exception)
P.logger.error(traceback.format_exc()) P.logger.error(traceback.format_exc())
@P.blueprint.route('/api/<sub>/<sub2>', methods=['GET', 'POST']) @P.blueprint.route('/api/<sub>/<sub2>', methods=['GET', 'POST'])
@@ -188,8 +234,16 @@ def default_route(P):
for module in P.module_list: for module in P.module_list:
if sub == module.name: if sub == module.name:
return module.process_api(sub2, request) return module.process_api(sub2, request)
except Exception as exception: except Exception as e:
P.logger.error('Exception:%s', exception) P.logger.error(f"Exception:{str(e)}")
P.logger.error(traceback.format_exc())
@P.blueprint.route('/normal/<sub>', methods=['GET', 'POST'])
def normal_first(sub):
try:
return P.module_list[0].process_normal(sub, request)
except Exception as e:
P.logger.error(f"Exception:{str(e)}")
P.logger.error(traceback.format_exc()) P.logger.error(traceback.format_exc())
@P.blueprint.route('/normal/<sub>/<sub2>', methods=['GET', 'POST']) @P.blueprint.route('/normal/<sub>/<sub2>', methods=['GET', 'POST'])
@@ -198,13 +252,13 @@ def default_route(P):
for module in P.module_list: for module in P.module_list:
if sub == module.name: if sub == module.name:
return module.process_normal(sub2, request) return module.process_normal(sub2, request)
except Exception as exception: except Exception as e:
P.logger.error('Exception:%s', exception) P.logger.error(f"Exception:{str(e)}")
P.logger.error(traceback.format_exc()) P.logger.error(traceback.format_exc())
# default_route 끝
@@ -266,18 +320,14 @@ def default_route_single_module(P):
else: else:
P.logic.scheduler_stop(sub) P.logic.scheduler_stop(sub)
return jsonify(go) return jsonify(go)
elif sub == 'reset_db':
sub = request.form['sub']
ret = P.logic.reset_db(sub)
return jsonify(ret)
elif sub == 'one_execute': elif sub == 'one_execute':
sub = request.form['sub'] sub = request.form['sub']
ret = P.logic.one_execute(sub) ret = P.logic.one_execute(sub)
return jsonify(ret) return jsonify(ret)
else: else:
return P.module_list[0].process_ajax(sub, request) return P.module_list[0].process_ajax(sub, request)
except Exception as exception: except Exception as e:
P.logger.error('Exception:%s', exception) P.logger.error(f"Exception:{str(e)}")
P.logger.error(traceback.format_exc()) P.logger.error(traceback.format_exc())
@P.blueprint.route('/api/<sub>', methods=['GET', 'POST']) @P.blueprint.route('/api/<sub>', methods=['GET', 'POST'])
@@ -285,16 +335,16 @@ def default_route_single_module(P):
def api(sub): def api(sub):
try: try:
return P.module_list[0].process_api(sub, request) return P.module_list[0].process_api(sub, request)
except Exception as exception: except Exception as e:
P.logger.error('Exception:%s', exception) P.logger.error(f"Exception:{str(e)}")
P.logger.error(traceback.format_exc()) P.logger.error(traceback.format_exc())
@P.blueprint.route('/normal/<sub>', methods=['GET', 'POST']) @P.blueprint.route('/normal/<sub>', methods=['GET', 'POST'])
def normal(sub): def normal(sub):
try: try:
return P.module_list[0].process_normal(sub, request) return P.module_list[0].process_normal(sub, request)
except Exception as exception: except Exception as e:
P.logger.error('Exception:%s', exception) P.logger.error(f"Exception:{str(e)}")
P.logger.error(traceback.format_exc()) P.logger.error(traceback.format_exc())
@@ -330,14 +380,15 @@ def default_route_socketio_module(module, attach=''):
module.socketio_list = [] module.socketio_list = []
@F.socketio.on('connect', namespace=f'/{P.package_name}/{module.name}{attach}') @F.socketio.on('connect', namespace=f'/{P.package_name}/{module.name}{attach}')
@F.login_required
def connect(): def connect():
try: try:
P.logger.debug(f'socket_connect : {P.package_name} - {module.name}{attach}') P.logger.debug(f'socket_connect : {P.package_name} - {module.name}{attach}')
module.socketio_list.append(request.sid) module.socketio_list.append(request.sid)
socketio_callback('start', '') socketio_callback('start', '')
module.socketio_connect() module.socketio_connect()
except Exception as exception: except Exception as e:
P.logger.error('Exception:%s', exception) P.logger.error(f"Exception:{str(e)}")
P.logger.error(traceback.format_exc()) P.logger.error(traceback.format_exc())
@@ -347,8 +398,8 @@ def default_route_socketio_module(module, attach=''):
P.logger.debug(f'socket_disconnect : {P.package_name} - {module.name}{attach}') P.logger.debug(f'socket_disconnect : {P.package_name} - {module.name}{attach}')
module.socketio_list.remove(request.sid) module.socketio_list.remove(request.sid)
module.socketio_disconnect() module.socketio_disconnect()
except Exception as exception: except Exception as e:
P.logger.error('Exception:%s', exception) P.logger.error(f"Exception:{str(e)}")
P.logger.error(traceback.format_exc()) P.logger.error(traceback.format_exc())
@@ -357,7 +408,7 @@ def default_route_socketio_module(module, attach=''):
if encoding: if encoding:
data = json.dumps(data, cls=AlchemyEncoder) data = json.dumps(data, cls=AlchemyEncoder)
data = json.loads(data) data = json.loads(data)
F.socketio.emit(cmd, data, namespace=f'/{P.package_name}/{module.name}{attach}', broadcast=True) F.socketio.emit(cmd, data, namespace=f'/{P.package_name}/{module.name}{attach}')
module.socketio_callback = socketio_callback module.socketio_callback = socketio_callback
@@ -392,9 +443,10 @@ def default_route_socketio_page(page):
page.socketio_list = [] page.socketio_list = []
@F.socketio.on('connect', namespace=f'/{P.package_name}/{module.name}/{page.name}') @F.socketio.on('connect', namespace=f'/{P.package_name}/{module.name}/{page.name}')
@F.login_required
def page_socketio_connect(): def page_socketio_connect():
try: try:
P.logger.debug(f'socket_connect : {P.package_name}/{module.name}/{page.name}') #P.logger.debug(f'socket_connect : {P.package_name}/{module.name}/{page.name}')
page.socketio_list.append(request.sid) page.socketio_list.append(request.sid)
page_socketio_socketio_callback('start', '') page_socketio_socketio_callback('start', '')
except Exception as e: except Exception as e:
@@ -405,7 +457,7 @@ def default_route_socketio_page(page):
@F.socketio.on('disconnect', namespace=f'/{P.package_name}/{module.name}/{page.name}') @F.socketio.on('disconnect', namespace=f'/{P.package_name}/{module.name}/{page.name}')
def page_socketio_disconnect(): def page_socketio_disconnect():
try: try:
P.logger.debug(f'socket_disconnect : {P.package_name}/{module.name}/{page.name}') #P.logger.debug(f'socket_disconnect : {P.package_name}/{module.name}/{page.name}')
page.socketio_list.remove(request.sid) page.socketio_list.remove(request.sid)
except Exception as e: except Exception as e:
P.logger.error(f'Exception:{str(e)}') P.logger.error(f'Exception:{str(e)}')
@@ -417,6 +469,6 @@ def default_route_socketio_page(page):
if encoding: if encoding:
data = json.dumps(data, cls=AlchemyEncoder) data = json.dumps(data, cls=AlchemyEncoder)
data = json.loads(data) data = json.loads(data)
F.socketio.emit(cmd, data, namespace=f'/{P.package_name}/{module.name}/{page.name}', broadcast=True) F.socketio.emit(cmd, data, namespace=f'/{P.package_name}/{module.name}/{page.name}')
page.socketio_callback = page_socketio_socketio_callback page.socketio_callback = page_socketio_socketio_callback

BIN
lib/support/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -10,15 +10,19 @@ def d(data):
from .logger import get_logger from .logger import get_logger
logger = get_logger() #logger = get_logger()
import logging
logger = logging.getLogger('support')
from .base.aes import SupportAES from .base.aes import SupportAES
from .base.discord import SupportDiscord from .base.discord import SupportDiscord
from .base.file import SupportFile from .base.file import SupportFile
from .base.os_command import SupportOSCommand
from .base.string import SupportString from .base.string import SupportString
from .base.sub_process import SupportSubprocess from .base.sub_process import SupportSubprocess
from .base.support_sc import SupportSC from .base.support_sc import SupportSC
from .base.telegram import SupportTelegram from .base.telegram import SupportTelegram
from .base.slack import SupportSlack
from .base.util import (AlchemyEncoder, SingletonClass, SupportUtil, from .base.util import (AlchemyEncoder, SingletonClass, SupportUtil,
default_headers, pt) default_headers, pt)
from .base.yaml import SupportYaml from .base.yaml import SupportYaml

View File

@@ -18,8 +18,8 @@ class SupportAES(object):
def encrypt(cls, raw, mykey=None): def encrypt(cls, raw, mykey=None):
try: try:
Random.atfork() Random.atfork()
except Exception as exception: except Exception as e:
logger.error('Exception:%s', exception) logger.error(f"Exception:{str(e)}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
raw = pad(raw) raw = pad(raw)
@@ -31,8 +31,8 @@ class SupportAES(object):
cipher = AES.new(key if mykey is None else mykey, AES.MODE_CBC, iv ) cipher = AES.new(key if mykey is None else mykey, AES.MODE_CBC, iv )
try: try:
tmp = cipher.encrypt( raw ) tmp = cipher.encrypt( raw )
except Exception as exception: except Exception as e:
logger.error('Exception:%s', exception) logger.error(f"Exception:{str(e)}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
tmp = cipher.encrypt( raw.encode() ) tmp = cipher.encrypt( raw.encode() )
ret = base64.b64encode( iv + tmp ) ret = base64.b64encode( iv + tmp )
@@ -64,8 +64,8 @@ class SupportAES(object):
def encrypt_(cls, raw, mykey=None, iv=None): def encrypt_(cls, raw, mykey=None, iv=None):
try: try:
Random.atfork() Random.atfork()
except Exception as exception: except Exception as e:
logger.error('Exception:%s', exception) logger.error(f"Exception:{str(e)}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
raw = pad(raw) raw = pad(raw)
if type(raw) == type(''): if type(raw) == type(''):
@@ -79,8 +79,8 @@ class SupportAES(object):
cipher = AES.new(key if mykey is None else mykey, AES.MODE_CBC, iv ) cipher = AES.new(key if mykey is None else mykey, AES.MODE_CBC, iv )
try: try:
tmp = cipher.encrypt( raw ) tmp = cipher.encrypt( raw )
except Exception as exception: except Exception as e:
logger.error('Exception:%s', exception) logger.error(f"Exception:{str(e)}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
tmp = cipher.encrypt( raw.encode() ) tmp = cipher.encrypt( raw.encode() )
ret = base64.b64encode( tmp ) ret = base64.b64encode( tmp )

View File

@@ -1,4 +1,3 @@
import io
import os import os
import random import random
import time import time
@@ -11,31 +10,36 @@ try:
except: except:
os.system('pip3 install discord-webhook') os.system('pip3 install discord-webhook')
# 2023-10-13 by flaskfarm
# 웹훅 URL이 git에 노출되면 중단.
# base64로 인코딩.
import base64
from discord_webhook import DiscordEmbed, DiscordWebhook from discord_webhook import DiscordEmbed, DiscordWebhook
from . import logger from . import logger
webhook_list = [ webhook_list = [
#'https://discord.com/api/webhooks/933908493612744705/DGPWBQN8LiMnt2cnCSNVy6rCc5Gi_vj98QpJ3ZEeihohzsfOsCWvcixJU1A2fQuepGFq', # 1 base64.b64decode(b'aHR0cHM6Ly9kaXNjb3JkLmNvbS9hcGkvd2ViaG9va3MvMTM5NDk1MTkwMTk2NzE1NTMwNS9uY01aaWZVVDY3ZTRISXdPeG8xM0dLdFBTNFBnVjZZSDBZaU1SQ2FMQkNfMU0yMHo3WmNFRjExM2xnY0NpRTFFdnhEZQ==').decode('utf-8'), # 1
#'https://discord.com/api/webhooks/932754078839234731/R2iFzQ7P8IKV-MGWp820ToWX07s5q8X-st-QsUJs7j3JInUj6ZlI4uDYKeR_cwIi98mf', # 2 base64.b64decode(b'aHR0cHM6Ly9kaXNjb3JkLmNvbS9hcGkvd2ViaG9va3MvMTM5NDk1MjY1NjQwOTA3MTY0Ni9zUjlHZFJMbERrQV9Cc243UkdvQXQ3TmVSMU9SVFRxczVqUEc5UU9PYTJCbjllTTI1YnctV0FXZ2pYT1pYa183U0V4Wg==').decode('utf-8'), # 2
#'https://discord.com/api/webhooks/932754171835351131/50RLrYa_B69ybk4BWoLruNqU7YlZ3pl3gpPr9bwuankWyTIGtRGbgf0CJ9ExJWJmvXwo', # 3 base64.b64decode(b'aHR0cHM6Ly9kaXNjb3JkLmNvbS9hcGkvd2ViaG9va3MvMTM5NDk1MjkyNDExOTA0NDE4OC9wX3ZMN211eElKUmFWOXRDVG56S3c4LVJjY0R5V1JaSjdER2dYc1YwaXlLVGFjZEM4MVBiYmctWHFzY0NDTk5jdXpWeQ==').decode('utf-8'), # 3
'https://discord.com/api/webhooks/794661043863027752/A9O-vZSHIgfQ3KX7wO5_e2xisqpLw5TJxg2Qs1stBHxyd5PK-Zx0IJbAQXmyDN1ixZ-n', # 4 base64.b64decode(b'aHR0cHM6Ly9kaXNjb3JkLmNvbS9hcGkvd2ViaG9va3MvMTM5NDk1MzE2MjE1ODI0MzkwMS9KdDMwZjlTTTR6dWNfVmYwSVlmbzdZNTluOFI5T2RQazNXdTFtNG93MHZxZFJERXlneVZvb25Rdm1QbVRya1lOVTRyag==').decode('utf-8'), # 4
'https://discord.com/api/webhooks/810373348776476683/h_uJLBBlHzD0w_CG0nUajFO-XEh3fvy-vQofQt1_8TMD7zHiR7a28t3jF-xBCP6EVlow', # 5 base64.b64decode(b'aHR0cHM6Ly9kaXNjb3JkLmNvbS9hcGkvd2ViaG9va3MvMTM5NDk1MzMyNzY1MzE1ODk4Mi82Nk0zZVFyRGpSZG1UTzExaXZlMGhlTFFpNGwtckZUN1lRYTJ3elpmMjNBOGZPYm1CYjJSRmhxR2dNSHNlNUdHSFNLTA==').decode('utf-8'), # 5
'https://discord.com/api/webhooks/810373405508501534/wovhf-1pqcxW5h9xy7iwkYaf8KMDjHU49cMWuLKtBWjAnj-tzS1_j8RJ7tsMyViDbZCE', # 6 base64.b64decode(b'aHR0cHM6Ly9kaXNjb3JkLmNvbS9hcGkvd2ViaG9va3MvMTM5NDk1MzU1ODAzNzg5MzIxNC84aWFNLTJIdXJFOW1XM0RqY293dm9tUVhUeUxLOElrbWR5SnhsY1BFRzJ4MjBqOTNIN0FWNnY0dVJIak5XeGprcjg4Tw==').decode('utf-8'), # 6
'https://discord.com/api/webhooks/796558388326039552/k2VV356S1gKQa9ht-JuAs5Dqw5eVkxgZsLUzFoxmFG5lW6jqKl7zCBbbKVhs3pcLOetm', # 7 base64.b64decode(b'aHR0cHM6Ly9kaXNjb3JkLmNvbS9hcGkvd2ViaG9va3MvMTM5NDk1MzczMTQzNDYxMDc0OS9xRktGX0hSWDRYVHFYMVFPRzM5YWNJVkp6dmdRZXBzZjM2TEFEUlpNOWtiZ0pNUHVfd091OXZ4bXdZVVBRMUpkUjhhRg==').decode('utf-8'), # 7
'https://discord.com/api/webhooks/810373566452858920/Qf2V8BoLOy2kQzlZGHy5HZ1nTj7lK72ol_UFrR3_eHKEOK5fyR_fQ8Yw8YzVh9EQG54o', # 8 base64.b64decode(b'aHR0cHM6Ly9kaXNjb3JkLmNvbS9hcGkvd2ViaG9va3MvMTM5NDk1Mzg1NTI0NjI3NDYwMS9vWGVxYVdhWENNZktkM19iZktEVjB0Ti1XQzUyLUxpVjU0VjQxWE1jNWd3XzJmQnpnekp4MzJNYS1wOWlvQkFpd1I3Mw==').decode('utf-8'), # 8
'https://discord.com/api/webhooks/810373654411739157/SGgdO49OCkTNIlc_BSMSy7IXQwwXVonG3DsVfvBVE6luTCwvgCqEBpEk30WBeMMieCyI', # 9 base64.b64decode(b'aHR0cHM6Ly9kaXNjb3JkLmNvbS9hcGkvd2ViaG9va3MvMTM5NDk1Mzk5MDgyNzE1MTQ2MS85a0xXTXZCY1FaNzZRcnRpZmVJck9DOXo5SXl1WGl4YnRmbldocHVjSlFRVUJqcGxSd0tIdzdDc0h3THJhQkRQM1h5ag==').decode('utf-8'), # 9
'https://discord.com/api/webhooks/810373722341900288/FwcRJ4YxYjpyHpnRwF5f2an0ltEm8JPqcWeZqQi3Qz4QnhEY-kR2sjF9fo_n6stMGnf_', # 10 base64.b64decode(b'aHR0cHM6Ly9kaXNjb3JkLmNvbS9hcGkvd2ViaG9va3MvMTM5NDk1NDExMjQ1NzYzNzg5OC9ZVC1qblZTeWFxcjAxMjFtWUtVZFU1SjJaVFZHS0NOM2djUDI2RXEwWm5hR3RWeFllM3NZa0kyUG81RWhPd211WDd6aw==').decode('utf-8'), # 10
'https://discord.com/api/webhooks/931779811691626536/vvwCm1YQvE5tW4QJ4SNKRmXhQQrmOQxbjsgRjbTMMXOSiclB66qipiZaax5giAqqu2IB', # 11 base64.b64decode(b'aHR0cHM6Ly9kaXNjb3JkLmNvbS9hcGkvd2ViaG9va3MvMTM5NDk1NDI1Mzg1MTk1NTMxMS9RVUt1cU5uWFFiaWkwU01FMWxkU0lEakxhZXh5RDRUZEZuLWdXejFuSXRlYy1mSFVCU3dxUDd3WHNBbDB1dXd2VVJTRw==').decode('utf-8'), # 11
'https://discord.com/api/webhooks/931779905631420416/VKlDwfxWQPJfIaj94-ww_hM1MNEayRKoMq0adMffCC4WQS60yoAub_nqPbpnfFRR3VU5', # 12 base64.b64decode(b'aHR0cHM6Ly9kaXNjb3JkLmNvbS9hcGkvd2ViaG9va3MvMTM5NDk1NDM3NDMyNDgxMzkyNS9VR1Jsc3liY2dPQ3hoMVQ1Z0J0QVc2RGQyZ0dPaGVOXzcydy15QTBvZzU5aU1BcnB3WWxVRzhka0ZXTUxSVUZpaHFScw==').decode('utf-8'), # 12
'https://discord.com/api/webhooks/931779947914231840/22amQuHSOI7wPijSt3U01mXwd5hTo_WHfVkeaowDQMawCo5tXVfeEMd6wAWf1n7CseiG', # 13 base64.b64decode(b'aHR0cHM6Ly9kaXNjb3JkLmNvbS9hcGkvd2ViaG9va3MvMTM5NDk1NDUxNjE5NzI3Nzc2Ny9iOEFIN1FtY2JPSl9XcUVHZmtMOVNPbXBJMWluVThvcDF4amQwWGFjRXFFZW82ZURzbS0yYkpZYllmQ1RYclMxbHhUdQ==').decode('utf-8'), # 13
'https://discord.com/api/webhooks/810374294416654346/T3-TEdKIg7rwMZeDzNr46KPDvO7ZF8pRdJ3lfl39lJw2XEZamAG8uACIXagbNMX_B0YN', # 14 base64.b64decode(b'aHR0cHM6Ly9kaXNjb3JkLmNvbS9hcGkvd2ViaG9va3MvMTM5NDk1NDY0MDIzMTIzOTcwMS90bkFSTzFvYWo1SWRmb0U4UEVJejRZUVMxNFhKXzdpc0I5Q1otdzVyaXdDN0U0cVVzQ1B6V2pLRnM3WE9OazBvVEo5Qg==').decode('utf-8'), # 14
'https://discord.com/api/webhooks/810374337403289641/_esFkQXwlPlhxJWtlqDAdLg2Nujo-LjGPEG3mUmjiRZto69NQpkBJ0F2xtSNrCH4VAgb', # 15 base64.b64decode(b'aHR0cHM6Ly9kaXNjb3JkLmNvbS9hcGkvd2ViaG9va3MvMTM5NDk1NDc1NTcxNzIwMTk4MS9WLWQwc0hvNl9QakJTdFpLVmtuSTdDS0RuQks1QzRhS2dPZUZ4azEwam41VE5oZk1PdFNOSFNHN3BpaGNWLVh6Y0kxZg==').decode('utf-8'), # 15
'https://discord.com/api/webhooks/810374384736534568/mH5-OkBVpi7XqJioaQ8Ma-NiL-bOx7B5nYJpL1gZ03JaJaUaIW4bCHeCt5O_VGLJwAtj', # 16 base64.b64decode(b'aHR0cHM6Ly9kaXNjb3JkLmNvbS9hcGkvd2ViaG9va3MvMTM5NDk1NDg4NDc4NDMyNDYxOS9XVEpHWWVjcjVKOHhtN0hTaUpCbmdnU01Uc3JkMUxiaDVwQzB2Vm5tYVptZWlvd2RRZWZQRHRuZHowRmViWE9xYkNoeA==').decode('utf-8'), # 16
'https://discord.com/api/webhooks/810374428604104724/Z1Tdxz3mb0ytWq5LHWi4rG5CeJnr9KWXy5aO_waeD0NcImQnhRXe7h7ra7UrIDRQ2jOg', # 17 base64.b64decode(b'aHR0cHM6Ly9kaXNjb3JkLmNvbS9hcGkvd2ViaG9va3MvMTM5NDk1NTAxMTIxMzA5OTEyOS9neHVVenpsMTBpMUV4NWZtdU5jZGlOQ2FocHBEM3liQlpxaTR3Y3phdlpGeG1OUGx2VFRadU9CalZCMTBOZzJ2QWpLcA==').decode('utf-8'), # 17
'https://discord.com/api/webhooks/810374475773509643/QCPPN4djNzhuOmbS3DlrGBunK0SVR5Py9vMyCiPL-0T2VPgitFZS4YM6GCLfM2fkrn4-', # 18 base64.b64decode(b'aHR0cHM6Ly9kaXNjb3JkLmNvbS9hcGkvd2ViaG9va3MvMTM5NDk1NTEzMjg4OTczMTE1My9YcTU4cXdCTGlOOEF4S1djQTl0MFJERkhIT0NDNjg4MlQ1aXBKbkJxY3VSOFVxMGowSzF4Rko3dUZWaGhRR0RFTjc3bw==').decode('utf-8'), # 18
'https://discord.com/api/webhooks/810374527652855819/5ypaKI_r-hYzwmdDlVmgAU6xNgU833L9tFlPnf3nw4ZDaPMSppjt77aYOiFks4KLGQk8', # 19 base64.b64decode(b'aHR0cHM6Ly9kaXNjb3JkLmNvbS9hcGkvd2ViaG9va3MvMTM5NDk1NTI5NzYzNzc5MzgxMy9pV3hoZkxRN190dHhkNENIVnNPWjA2ZHFOUjlkVTZUdlNfdHA2OHVnNlI2WmRIa2dESzJKb28xUVNSa3NrRDhLUXRyTg==').decode('utf-8'), # 19
'https://discord.com/api/webhooks/810374587917402162/lHrG7CEysGUM_41DMnrxL2Q8eh1-xPjJXstYE68WWfLQbuUAV3rOfsNB9adncJzinYKi', # 20 base64.b64decode(b'aHR0cHM6Ly9kaXNjb3JkLmNvbS9hcGkvd2ViaG9va3MvMTM5NDk1NTQ0NDk0MjAxMjQ4OC9zandtaFNDYjI0ZElYbjBVMWhwMmdJRzZDV2REcC1Kb3M0OW1Oc05jQllGenNDNm1KYVZJOVpoQm11dGt4cXd1bDc1ZA==').decode('utf-8'), # 20
] ]
@@ -44,20 +48,60 @@ class SupportDiscord(object):
@classmethod @classmethod
def send_discord_message(cls, text, image_url=None, webhook_url=None): def send_discord_message(cls, text, image_url=None, webhook_url=None):
try: try:
"""
webhook = DiscordWebhook(url=webhook_url, content=text) webhook = DiscordWebhook(url=webhook_url, content=text)
if image_url is not None: if image_url is not None:
embed = DiscordEmbed() embed = DiscordEmbed()
embed.set_timestamp() embed.set_timestamp()
embed.set_image(url=image_url) embed.set_image(url=image_url)
webhook.add_embed(embed) webhook.add_embed(embed)
response = webhook.execute() """
return True try:
except Exception as exception:
logger.error('Exception:%s', exception) if image_url is not None:
webhook = DiscordWebhook(url=webhook_url)
embed = DiscordEmbed()
embed.set_timestamp()
embed.set_image(url=image_url)
tmp = text.split('\n', 1)
embed.set_title(tmp[0])
embed.set_description(tmp[1])
webhook.add_embed(embed)
else:
if 'http://' in text or 'https://' in text:
webhook = DiscordWebhook(url=webhook_url, content= text)
else:
webhook = DiscordWebhook(url=webhook_url, content='```' + text + '```')
webhook.execute()
return True
except:
webhook = DiscordWebhook(url=webhook_url, content=text)
if image_url is not None:
embed = DiscordEmbed()
embed.set_timestamp()
embed.set_image(url=image_url)
webhook.add_embed(embed)
webhook.execute()
return True
except Exception as e:
logger.error(f"Exception:{str(e)}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
return False return False
@classmethod
def send_discord_bot_message(cls, text, webhook_url, encryped=True):
try:
from support import SupportAES
if encryped:
text = '^' + SupportAES.encrypt(text)
return cls.send_discord_message(text, webhook_url=webhook_url)
except Exception as e:
logger.error(f"Exception:{str(e)}")
logger.error(traceback.format_exc())
return False
@classmethod @classmethod
def discord_proxy_image(cls, image_url, webhook_url=None, retry=True): def discord_proxy_image(cls, image_url, webhook_url=None, retry=True):
@@ -95,21 +139,21 @@ class SupportDiscord(object):
return image_url return image_url
else: else:
raise Exception(str(data)) raise Exception(str(data))
except Exception as exception: except Exception as e:
logger.error('Exception:%s', exception) logger.error(f"Exception:{str(e)}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
if retry: if retry:
time.sleep(1) time.sleep(1)
return cls.discord_proxy_image(image_url, webhook_url=None, retry=False) return cls.discord_proxy_image(image_url, webhook_url=webhook_url, retry=False)
else: else:
return image_url return image_url
@classmethod @classmethod
def discord_proxy_image_localfile(cls, filepath, retry=True): def discord_proxy_image_localfile(cls, filepath, webhook_url=None, retry=True):
data = None data = None
webhook_url = webhook_list[random.randint(0,len(webhook_list)-1)] if webhook_url is None or webhook_url == '':
webhook_url = webhook_list[random.randint(0,len(webhook_list)-1)]
try: try:
webhook = DiscordWebhook(url=webhook_url, content='') webhook = DiscordWebhook(url=webhook_url, content='')
import io import io
@@ -133,8 +177,8 @@ class SupportDiscord(object):
if retry: if retry:
time.sleep(1) time.sleep(1)
return cls.discord_proxy_image_localfile(filepath, retry=False) return cls.discord_proxy_image_localfile(filepath, retry=False)
except Exception as exception: except Exception as e:
logger.error('Exception:%s', exception) logger.error(f"Exception:{str(e)}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
if retry: if retry:
@@ -143,15 +187,15 @@ class SupportDiscord(object):
@classmethod @classmethod
def discord_proxy_image_bytes(cls, bytes, retry=True): def discord_proxy_image_bytes(cls, bytes, retry=True, format='jpg', webhook_url=None):
data = None data = None
idx = random.randint(0,len(webhook_list)-1) if webhook_url is None or webhook_url == '':
webhook_url = webhook_list[idx] webhook_url = webhook_list[random.randint(0,len(webhook_list)-1)]
try: try:
webhook = DiscordWebhook(url=webhook_url, content='') webhook = DiscordWebhook(url=webhook_url, content='')
webhook.add_file(file=bytes, filename='image.jpg') webhook.add_file(file=bytes, filename=f'image.{format}')
embed = DiscordEmbed() embed = DiscordEmbed()
embed.set_image(url="attachment://image.jpg") embed.set_image(url=f"attachment://image.{format}")
response = webhook.execute() response = webhook.execute()
data = None data = None
if type(response) == type([]): if type(response) == type([]):
@@ -168,8 +212,8 @@ class SupportDiscord(object):
if retry: if retry:
time.sleep(1) time.sleep(1)
return cls.discord_proxy_image_bytes(bytes, retry=False) return cls.discord_proxy_image_bytes(bytes, retry=False)
except Exception as exception: except Exception as e:
logger.error('Exception:%s', exception) logger.error(f"Exception:{str(e)}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
if retry: if retry:
@@ -181,7 +225,7 @@ class SupportDiscord(object):
# RSS에서 자막 올린거 # RSS에서 자막 올린거
@classmethod @classmethod
def discord_cdn(cls, byteio=None, filepath=None, filename=None, webhook_url=None, content='', retry=True): def discord_cdn(cls, byteio=None, filepath=None, filename=None, webhook_url="https://discord.com/api/webhooks/1050549730964410470/ttge1ggOfIxrCSeTmYbIIsUWyMGAQj-nN6QBgwZTqLcHtUKcqjZ8wFWSWAhHmZne57t7", content='', retry=True):
data = None data = None
if webhook_url is None: if webhook_url is None:
webhook_url = webhook_list[random.randint(0,9)] webhook_url = webhook_list[random.randint(0,9)]
@@ -210,8 +254,8 @@ class SupportDiscord(object):
if retry: if retry:
time.sleep(1) time.sleep(1)
return cls.discord_proxy_image_localfile(filepath, retry=False) return cls.discord_proxy_image_localfile(filepath, retry=False)
except Exception as exception: except Exception as e:
logger.error('Exception:%s', exception) logger.error(f"Exception:{str(e)}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
if retry: if retry:
time.sleep(1) time.sleep(1)

View File

@@ -3,6 +3,7 @@ import json
import os import os
import re import re
import traceback import traceback
import zipfile
from . import logger from . import logger
@@ -16,8 +17,8 @@ class SupportFile(object):
data = ifp.read() data = ifp.read()
ifp.close() ifp.close()
return data return data
except Exception as exception: except Exception as e:
logger.error('Exception:%s', exception) logger.error(f"Exception:{str(e)}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
@classmethod @classmethod
@@ -27,8 +28,8 @@ class SupportFile(object):
ofp = codecs.open(filename, mode, encoding='utf8') ofp = codecs.open(filename, mode, encoding='utf8')
ofp.write(data) ofp.write(data)
ofp.close() ofp.close()
except Exception as exception: except Exception as e:
logger.error('Exception:%s', exception) logger.error(f"Exception:{str(e)}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
@classmethod @classmethod
@@ -37,8 +38,8 @@ class SupportFile(object):
with open(filepath, "r", encoding='utf8') as json_file: with open(filepath, "r", encoding='utf8') as json_file:
data = json.load(json_file) data = json.load(json_file)
return data return data
except Exception as exception: except Exception as e:
logger.error('Exception:%s', exception) logger.error(f"Exception:{str(e)}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
@classmethod @classmethod
@@ -48,8 +49,8 @@ class SupportFile(object):
os.makedirs(os.path.dirname(filepath), exist_ok=True) os.makedirs(os.path.dirname(filepath), exist_ok=True)
with open(filepath, "w", encoding='utf8') as json_file: with open(filepath, "w", encoding='utf8') as json_file:
json.dump(data, json_file, indent=4, ensure_ascii=False) json.dump(data, json_file, indent=4, ensure_ascii=False)
except Exception as exception: except Exception as e:
logger.error('Exception:%s', exception) logger.error(f"Exception:{str(e)}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
@@ -71,8 +72,8 @@ class SupportFile(object):
response = requests.get(url, headers=headers) # get request response = requests.get(url, headers=headers) # get request
file_is.write(response.content) # write to file file_is.write(response.content) # write to file
return True return True
except Exception as exception: except Exception as e:
logger.debug('Exception:%s', exception) logger.debug(f"Exception:{str(e)}")
logger.debug(traceback.format_exc()) logger.debug(traceback.format_exc())
return False return False
@@ -82,118 +83,18 @@ class SupportFile(object):
#text = text.replace('/', '') #text = text.replace('/', '')
# 2021-07-31 X:X # 2021-07-31 X:X
#text = text.replace(':', ' ') #text = text.replace(':', ' ')
text = re.sub('[\\/:*?\"<>|]', ' ', text).strip() text = re.sub('[\\/:*?\"<>|]', ' ', text).strip()
text = re.sub("\s{2,}", ' ', text) text = re.sub("\s{2,}", ' ', text)
return text return text
@classmethod @classmethod
def size(cls, start_path = '.'): def unzip(cls, zip_filepath, extract_folderpath):
total_size = 0 with zipfile.ZipFile(zip_filepath, 'r') as zip_ref:
for dirpath, dirnames, filenames in os.walk(start_path): zip_ref.extractall(extract_folderpath)
for f in filenames:
fp = os.path.join(dirpath, f)
if not os.path.islink(fp):
total_size += os.path.getsize(fp)
return total_size
# 파일처리에서 사용. 중복이면 시간값
@classmethod @classmethod
def file_move(cls, source_path, target_dir, target_filename): def file_move(cls, source_path, target_dir, target_filename):
try: try:
@@ -208,9 +109,160 @@ class SupportFile(object):
new_target_filename = f"{tmp[0]} {str(time.time()).split('.')[0]}{tmp[1]}" new_target_filename = f"{tmp[0]} {str(time.time()).split('.')[0]}{tmp[1]}"
target_path = os.path.join(target_dir, new_target_filename) target_path = os.path.join(target_dir, new_target_filename)
shutil.move(source_path, target_path) shutil.move(source_path, target_path)
except Exception as exception: except Exception as e:
logger.debug('Exception:%s', exception) logger.debug(f"Exception:{str(e)}")
logger.debug(traceback.format_exc()) logger.debug(traceback.format_exc())
@classmethod
def size(cls, start_path = '.'):
if os.path.exists(start_path):
if os.path.isdir(start_path):
total_size = 0
for dirpath, dirnames, filenames in os.walk(start_path):
for f in filenames:
fp = os.path.join(dirpath, f)
if not os.path.islink(fp):
total_size += os.path.getsize(fp)
return total_size
else:
return os.path.getsize(start_path)
return 0
@classmethod
def size_info(cls, start_path = '.'):
ret = {
'size':0,
'file_count':0,
'folder_count':0.
}
for dirpath, dirnames, filenames in os.walk(start_path):
for f in filenames:
fp = os.path.join(dirpath, f)
if not os.path.islink(fp):
ret['size'] += os.path.getsize(fp)
ret['folder_count'] += len(dirnames)
ret['file_count'] += len(filenames)
return ret
@classmethod
def rmtree(cls, folderpath):
import shutil
try:
for root, dirs, files in os.walk(folderpath):
for name in files:
os.remove(os.path.join(root, name))
for name in dirs:
shutil.rmtree(os.path.join(root, name))
shutil.rmtree(folderpath)
except Exception as e:
logger.debug(f"Exception:{str(e)}")
logger.debug(traceback.format_exc())
return False
@classmethod
def file_move(cls, source_path, target_dir, target_filename):
try:
import shutil
import time
os.makedirs(target_dir, exist_ok=True)
target_path = os.path.join(target_dir, target_filename)
if source_path != target_path:
if os.path.exists(target_path):
tmp = os.path.splitext(target_filename)
new_target_filename = f"{tmp[0]} {str(time.time()).split('.')[0]}{tmp[1]}"
target_path = os.path.join(target_dir, new_target_filename)
shutil.move(source_path, target_path)
except Exception as e:
logger.debug(f"Exception:{str(e)}")
logger.debug(traceback.format_exc())
""" """
@classmethod @classmethod
@@ -234,13 +286,13 @@ class SupportFile(object):
import shutil import shutil
shutil.rmtree(zip_path) shutil.rmtree(zip_path)
return zipfilepath return zipfilepath
except Exception as exception: except Exception as e:
logger.error('Exception:%s', exception) logger.error(f"Exception:{str(e)}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
return return
""" """
"""
@classmethod @classmethod
def rmtree(cls, folderpath): def rmtree(cls, folderpath):
import shutil import shutil
@@ -253,19 +305,9 @@ class SupportFile(object):
return True return True
except: except:
return False return False
"""
@classmethod
def rmtree2(cls, folderpath):
import shutil
try:
for root, dirs, files in os.walk(folderpath):
for name in files:
os.remove(os.path.join(root, name))
for name in dirs:
shutil.rmtree(os.path.join(root, name))
except:
return False
@@ -279,8 +321,8 @@ class SupportFile(object):
try: try:
with open(filename, 'wb') as f: with open(filename, 'wb') as f:
f.write(data) f.write(data)
except Exception as exception: except Exception as e:
logger.error('Exception:%s', exception) logger.error(f"Exception:{str(e)}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
@@ -361,8 +403,8 @@ class SupportFile(object):
if isinstance(data, bytes): if isinstance(data, bytes):
data = data.decode('utf-8') data = data.decode('utf-8')
return data return data
except Exception as exception: except Exception as e:
logger.error('Exception:%s', exception) logger.error(f"Exception:{str(e)}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
""" """

View File

@@ -1,7 +1,13 @@
import os, sys, traceback, requests import os
import sys
import traceback
from io import BytesIO from io import BytesIO
import requests
from . import logger from . import logger
class SupportImage(object): class SupportImage(object):
@classmethod @classmethod
@@ -20,6 +26,6 @@ class SupportImage(object):
from . import SupportDiscord from . import SupportDiscord
return SupportDiscord.discord_proxy_image_bytes(img_byte_arr) return SupportDiscord.discord_proxy_image_bytes(img_byte_arr)
except Exception as e: except Exception as e:
logger.error('Exception:%s', e) logger.error(f"Exception:{str(e)}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())

View File

@@ -0,0 +1,44 @@
import os
import platform
from . import logger
class SupportOSCommand(object):
@classmethod
def get_size(cls, path):
from support import SupportFile, SupportSubprocess, SupportUtil
if platform.system() == 'Windows':
#https://docs.microsoft.com/en-us/sysinternals/downloads/du
"""
bin = r'C:\SJVA3\data\bin\du64.exe'
command = [bin, '-c', '-nobanner', f'"{path}"']
data = ToolSubprocess.execute_command_return(command, force_log=True)
logger.warning(data)
ret = {}
tmp = data.split('\t')
ret['target'] = tmp[1].strip()
ret['size'] = int(tmp[0].strip())
ret['sizeh'] = ToolUtil.sizeof_fmt(ret['size'])
"""
ret = {}
ret['target'] = path
if os.path.exists(path):
if os.path.isdir(path):
ret['size'] = SupportFile.size(start_path=path)
else:
ret['size'] = os.stat(path).st_size
ret['sizeh'] = SupportUtil.sizeof_fmt(ret['size'])
return ret
else:
command = ['du', '-bs', path]
data = SupportSubprocess.execute_command_return(command)
ret = {}
tmp = data['log'].split('\t')
ret['target'] = tmp[1].strip()
ret['size'] = int(tmp[0].strip())
ret['sizeh'] = SupportUtil.sizeof_fmt(ret['size'])
return ret

25
lib/support/base/slack.py Normal file
View File

@@ -0,0 +1,25 @@
import os
import traceback
try:
from slack_sdk.webhook import WebhookClient
except:
os.system('pip3 install slack-sdk')
from slack_sdk.webhook import WebhookClient
from . import logger
class SupportSlack:
@classmethod
def send_slack_message(cls, text, webhook_url=None, image_url=None, disable_notification=None):
try:
if webhook_url is None:
return False
webhook = WebhookClient(webhook_url)
if image_url is not None:
webhook.send(text=text, blocks=[{"type": "image", "title": {"type": "plain_text", "text": "Image", "emoji": True}, "image_url": image_url, "alt_text": "Image"}])
webhook.send(text=text)
return True
except Exception as e:
logger.error(f"Exception:{str(e)}")
logger.error(traceback.format_exc())
return False

View File

@@ -1,3 +1,6 @@
import re
import traceback
from . import logger from . import logger
@@ -5,22 +8,78 @@ class SupportString(object):
@classmethod @classmethod
def get_cate_char_by_first(cls, title): # get_first def get_cate_char_by_first(cls, title): # get_first
value = ord(title[0].upper()) value = ord(title[0].upper())
if value >= ord('0') and value <= ord('9'): return '0Z' if ord('') <= value < ord(''): return ''
elif value >= ord('A') and value <= ord('Z'): return '0Z' if ord('') <= value < ord(''): return ''
elif value >= ord('') and value < ord(''): return '' if ord('') <= value < ord(''): return ''
elif value < ord(''): return '' if ord('') <= value < ord(''): return ''
elif value < ord(''): return '' if ord('') <= value < ord(''): return ''
elif value < ord(''): return '' if ord('') <= value < ord(''): return ''
elif value < ord(''): return '' if ord('') <= value < ord(''): return ''
elif value < ord(''): return '' if ord('') <= value < ord(''): return ''
elif value < ord(''): return '' if ord('') <= value < ord(''): return ''
elif value < ord(''): return '' if ord('') <= value < ord(''): return ''
elif value < ord(''): return '' if ord('') <= value < ord(''): return ''
elif value < ord(''): return '' if ord('') <= value < ord(''): return ''
elif value < ord(''): return '' if ord('') <= value < ord(''): return ''
elif value < ord(''): return '' if ord('') <= value < ord(''): return ''
elif value < ord(''): return '' return '0Z'
elif value <= ord(''): return ''
else: return '0Z'
@classmethod
def is_include_hangul(cls, text):
try:
hanCount = len(re.findall(u'[\u3130-\u318F\uAC00-\uD7A3]+', text))
return hanCount > 0
except:
return False
@classmethod
def language_info(cls, text):
try:
text = text.strip().replace(' ', '')
all_count = len(text)
han_count = len(re.findall('[\u3130-\u318F\uAC00-\uD7A3]', text))
eng_count = len(re.findall('[a-zA-Z]', text))
etc_count = len(re.findall('[0-9]', text))
etc_count += len(re.findall('[-=+,#/\?:^$.@*\"※~&%ㆍ!』\\|\(\)\[\]\<\>`\'…》:]', text))
if all_count == etc_count:
return (0,0)
han_percent = int(han_count * 100 / (all_count-etc_count))
eng_percent = int(eng_count * 100 / (all_count-etc_count))
return (han_percent, eng_percent)
except Exception as e:
logger.error(f"Exception:{str(e)}")
logger.error(traceback.format_exc())
return False
@classmethod
def remove_special_char(cls, text):
return re.sub('[-=+,#/\?:^$.@*\"※~&%ㆍ!』\\|\(\)\[\]\<\>`\'…》:]', '', text)
@classmethod
def remove_emoji(cls, text, char=''):
import re
emoji_pattern = re.compile("["
u"\U0001F600-\U0001F64F" # emoticons
u"\U0001F300-\U0001F5FF" # symbols & pictographs
u"\U0001F680-\U0001F6FF" # transport & map symbols
u"\U0001F1E0-\U0001F1FF" # flags (iOS)
u"\U00002500-\U00002BEF" # chinese char
u"\U00002702-\U000027B0"
u"\U00002702-\U000027B0"
#u"\U000024C2-\U0001F251"
u"\U0001f926-\U0001f937"
u"\U00010000-\U0010ffff"
u"\u2640-\u2642"
u"\u2600-\u2B55"
u"\u200d"
u"\u23cf"
u"\u23e9"
u"\u231a"
u"\ufe0f" # dingbats
u"\u3030"
"]+", flags=re.UNICODE)
# Remove emojis from the text
text = emoji_pattern.sub(char, text)
return text

View File

@@ -1,5 +1,6 @@
import io import io
import json import json
import locale
import os import os
import platform import platform
import queue import queue
@@ -20,7 +21,7 @@ def demote(user_uid, user_gid):
class SupportSubprocess(object): class SupportSubprocess(object):
@classmethod @classmethod
def command_for_windows(cls, command: list) -> str or list: def command_for_windows(cls, command: list):
if platform.system() == 'Windows': if platform.system() == 'Windows':
tmp = [] tmp = []
if type(command) == type([]): if type(command) == type([]):
@@ -43,17 +44,32 @@ class SupportSubprocess(object):
iter_arg = '' iter_arg = ''
if platform.system() == 'Windows': if platform.system() == 'Windows':
process = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, shell=shell, env=env, encoding='utf8') process = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, shell=shell, env=env, encoding='utf8', bufsize=0)
else: else:
if uid == None: if uid == None:
process = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, shell=shell, env=env, encoding='utf8') process = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, shell=shell, env=env, encoding='utf8')
else: else:
process = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, shell=shell, env=env, preexec_fn=demote(uid, gid), encoding='utf8') process = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, shell=shell, env=env, preexec_fn=demote(uid, gid), encoding='utf8')
new_ret = {'status':'finish', 'log':None} new_ret = {'status':'finish', 'log':None}
def func(ret):
with process.stdout:
try:
for line in iter(process.stdout.readline, iter_arg):
ret.append(line.strip())
if log:
logger.debug(ret[-1])
except:
pass
result = []
thread = threading.Thread(target=func, args=(result,))
thread.setDaemon(True)
thread.start()
#thread.join()
try: try:
#process.communicate()
process_ret = process.wait(timeout=timeout) # wait for the subprocess to exit process_ret = process.wait(timeout=timeout) # wait for the subprocess to exit
except: except:
import psutil import psutil
@@ -62,14 +78,17 @@ class SupportSubprocess(object):
proc.kill() proc.kill()
process.kill() process.kill()
new_ret['status'] = "timeout" new_ret['status'] = "timeout"
#logger.error(process_ret)
ret = [] thread.join()
with process.stdout: #ret = []
for line in iter(process.stdout.readline, iter_arg): #with process.stdout:
ret.append(line.strip()) # for line in iter(process.stdout.readline, iter_arg):
if log: # ret.append(line.strip())
logger.debug(ret[-1]) # if log:
# logger.debug(ret[-1])
ret = result
#logger.error(ret)
if format is None: if format is None:
ret2 = '\n'.join(ret) ret2 = '\n'.join(ret)
elif format == 'json': elif format == 'json':
@@ -82,20 +101,29 @@ class SupportSubprocess(object):
break break
ret2 = json.loads(''.join(ret[index:])) ret2 = json.loads(''.join(ret[index:]))
except: except:
ret2 = None ret2 = ret
new_ret['log'] = ret2 new_ret['log'] = ret2
return new_ret return new_ret
except Exception as exception: except Exception as e:
logger.error('Exception:%s', exception) logger.error(f"Exception:{str(e)}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
logger.error('command : %s', command) logger.error('command : %s', command)
finally:
try:
if process.stdout:
process.stdout.close()
if process.stdin:
process.stdin.close()
except Exception as e:
pass
__instance_list = [] __instance_list = []
def __init__(self, command, print_log=False, shell=False, env=None, timeout=None, uid=None, gid=None, stdout_callback=None, call_id=None): def __init__(self, command, print_log=False, shell=False, env=None, timeout=None, uid=None, gid=None, stdout_callback=None, call_id=None, callback_line=True):
self.command = command self.command = command
self.print_log = print_log self.print_log = print_log
self.shell = shell self.shell = shell
@@ -108,6 +136,7 @@ class SupportSubprocess(object):
self.stdout_queue = None self.stdout_queue = None
self.call_id = call_id self.call_id = call_id
self.timestamp = time.time() self.timestamp = time.time()
self.callback_line = callback_line
def start(self, join=True): def start(self, join=True):
@@ -127,13 +156,15 @@ class SupportSubprocess(object):
self.command = self.command_for_windows(self.command) self.command = self.command_for_windows(self.command)
logger.debug(f"{self.command=}") logger.debug(f"{self.command=}")
if platform.system() == 'Windows': if platform.system() == 'Windows':
self.process = subprocess.Popen(self.command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, shell=self.shell, env=self.env, encoding='utf8', bufsize=0) self.process = subprocess.Popen(self.command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, shell=self.shell, env=self.env, encoding='utf8', bufsize=0)
else: else:
if self.uid == None: if self.uid == None:
self.process = subprocess.Popen(self.command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, shell=self.shell, env=self.env, encoding='utf8', bufsize=0) self.process = subprocess.Popen(self.command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, shell=self.shell, env=self.env, encoding='utf8', bufsize=0)
else: else:
self.process = subprocess.Popen(self.command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, shell=self.shell, env=self.env, preexec_fn=demote(self.uid, self.gid), encoding='utf8', bufsize=0) self.process = subprocess.Popen(self.command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, shell=self.shell, env=self.env, preexec_fn=demote(self.uid, self.gid), encoding='utf8', bufsize=0)
SupportSubprocess.__instance_list.append(self) SupportSubprocess.__instance_list.append(self)
self.send_stdout_callback(self.call_id, 'START', None)
self.__start_communicate() self.__start_communicate()
self.__start_send_callback() self.__start_send_callback()
if self.process is not None: if self.process is not None:
@@ -142,17 +173,18 @@ class SupportSubprocess(object):
self.process_close() self.process_close()
else: else:
self.process.wait() self.process.wait()
self.remove_instance(self)
logger.info(f"{self.command} END") logger.info(f"{self.command} END")
except Exception as e: except Exception as e:
logger.error(f'Exception:{str(e)}') logger.error(f'Exception:{str(e)}')
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
logger.warning(self.command) logger.warning(self.command)
if self.stdout_callback != None: self.send_stdout_callback(self.call_id, 'ERROR', str(e))
self.stdout_callback('error', str(e)) self.send_stdout_callback(self.call_id, 'ERROR', str(traceback.format_exc()))
self.stdout_callback('error', str(traceback.format_exc()))
finally: finally:
if self.stdout_callback != None: if self.stdout_callback != None:
self.stdout_callback('thread_end', None) #self.stdout_callback(self.call_id, 'thread_end', None)
pass
def __start_communicate(self): def __start_communicate(self):
@@ -164,7 +196,11 @@ class SupportSubprocess(object):
def rdr(): def rdr():
while True: while True:
buf = self.process.stdout.read(1) try:
buf = self.process.stdout.read(1)
except:
continue
#print(buf)
if buf: if buf:
_queue.put( buf ) _queue.put( buf )
else: else:
@@ -192,7 +228,9 @@ class SupportSubprocess(object):
if r is not None: if r is not None:
#print(f"{r=}") #print(f"{r=}")
self.stdout_queue.put(r) self.stdout_queue.put(r)
self.stdout_queue.put('\n')
self.stdout_queue.put('<END>') self.stdout_queue.put('<END>')
self.stdout_queue.put('\n')
for tgt in [rdr, clct]: for tgt in [rdr, clct]:
th = threading.Thread(target=tgt) th = threading.Thread(target=tgt)
th.setDaemon(True) th.setDaemon(True)
@@ -204,16 +242,36 @@ class SupportSubprocess(object):
def func(): def func():
while self.stdout_queue: while self.stdout_queue:
line = self.stdout_queue.get() line = self.stdout_queue.get()
#logger.error(line)
if line == '<END>': if line == '<END>':
if self.stdout_callback != None: self.send_stdout_callback(self.call_id, 'END', None)
self.stdout_callback('end', None)
break break
else: else:
if self.stdout_callback != None: self.send_stdout_callback(self.call_id, 'LOG', line)
self.stdout_callback('log', line)
self.remove_instance(self) self.remove_instance(self)
th = threading.Thread(target=func, args=()) def func_callback_line():
previous = ''
while self.stdout_queue:
receive = previous + self.stdout_queue.get()
lines = receive.split('\n')
previous = lines[-1]
for line in lines[:-1]:
line = line.strip()
# TODO
#logger.error(line)
if line == '<END>':
self.send_stdout_callback(self.call_id, 'END', None)
break
else:
self.send_stdout_callback(self.call_id, 'LOG', line)
self.remove_instance(self)
if self.callback_line:
th = threading.Thread(target=func_callback_line, args=())
else:
th = threading.Thread(target=func, args=())
th.setDaemon(True) th.setDaemon(True)
th.start() th.start()
@@ -243,6 +301,15 @@ class SupportSubprocess(object):
self.process.stdin.write(f'{cmd}\n') self.process.stdin.write(f'{cmd}\n')
self.process.stdin.flush() self.process.stdin.flush()
def send_stdout_callback(self, call_id, mode, data):
try:
if self.stdout_callback != None:
self.stdout_callback(self.call_id, mode, data)
except Exception as e:
logger.error(f'Exception:{str(e)}')
logger.error(f"[{call_id}] [{mode}] [{data}]")
#logger.error(traceback.format_exc())
@classmethod @classmethod
def all_process_close(cls): def all_process_close(cls):
@@ -271,4 +338,7 @@ class SupportSubprocess(object):
for instance in cls.__instance_list: for instance in cls.__instance_list:
if instance.call_id == call_id: if instance.call_id == call_id:
return instance return instance
@classmethod
def get_list(cls):
return cls.__instance_list

View File

@@ -1,5 +1,7 @@
import time
import traceback import traceback
import requests
from telepot_mod import Bot from telepot_mod import Bot
from . import logger from . import logger
@@ -12,11 +14,21 @@ class SupportTelegram:
try: try:
bot = Bot(bot_token) bot = Bot(bot_token)
if image_url is not None: if image_url is not None:
bot.sendPhoto(chat_id, image_url, disable_notification=disable_notification) logger.debug(image_url)
for i in range(5):
if requests.get(image_url).status_code == 200:
break
else:
time.sleep(3)
try:
bot.sendPhoto(chat_id, image_url, disable_notification=disable_notification)
except Exception as e:
logger.error(f"Exception:{str(e)}")
logger.error(traceback.format_exc())
bot.sendMessage(chat_id, text, disable_web_page_preview=True, disable_notification=disable_notification) bot.sendMessage(chat_id, text, disable_web_page_preview=True, disable_notification=disable_notification)
return True return True
except Exception as exception: except Exception as e:
logger.error('Exception:%s', exception) logger.error(f"Exception:{str(e)}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
logger.debug('Chatid:%s', chat_id) logger.debug('Chatid:%s', chat_id)
return False return False

View File

@@ -1,8 +1,11 @@
import os, traceback, io, re, json, codecs import json
import time
import traceback
from functools import wraps
from . import logger from . import logger
from functools import wraps
import time
def pt(f): def pt(f):
@wraps(f) @wraps(f)
def wrapper(*args, **kwds): def wrapper(*args, **kwds):
@@ -26,9 +29,9 @@ class SupportUtil(object):
def sizeof_fmt(cls, num, suffix='Bytes'): def sizeof_fmt(cls, num, suffix='Bytes'):
for unit in ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z']: for unit in ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z']:
if abs(num) < 1024.0: if abs(num) < 1024.0:
return "%3.1f%s%s" % (num, unit, suffix) return "%3.1f %s%s" % (num, unit, suffix)
num /= 1024.0 num /= 1024.0
return "%.1f%s%s" % (num, 'Y', suffix) return "%.1f %s%s" % (num, 'Y', suffix)
@classmethod @classmethod
def is_arm(cls): def is_arm(cls):

View File

@@ -1,5 +1,11 @@
import os
import traceback
import yaml import yaml
from . import logger
class SupportYaml(object): class SupportYaml(object):
@classmethod @classmethod
def write_yaml(cls, filepath, data): def write_yaml(cls, filepath, data):
@@ -11,3 +17,41 @@ class SupportYaml(object):
with open(filepath, encoding='utf8') as file: with open(filepath, encoding='utf8') as file:
data = yaml.load(file, Loader=yaml.FullLoader) data = yaml.load(file, Loader=yaml.FullLoader)
return data return data
@classmethod
def copy_section(cls, source_file, target_file, section_name):
from support import SupportFile
try:
if os.path.exists(source_file) == False:
return 'not_exist_source_file'
if os.path.exists(target_file) == False:
return 'not_exist_target_file'
lines = SupportFile.read_file(source_file).split('\n')
section = {}
current_section_name = None
current_section_data = None
for line in lines:
line = line.strip()
if line.startswith('# SECTION START : '):
current_section_name = line.split(':')[1].strip()
current_section_data = []
if current_section_data is not None:
current_section_data.append(line)
if line.startswith('# SECTION END'):
section[current_section_name] = current_section_data
current_section_name = current_section_data = None
if section_name not in section:
return 'not_include_section'
data = '\n'.join(section[section_name])
source_data = SupportFile.read_file(target_file)
source_data = source_data + f"\n{data}\n"
SupportFile.write_file(target_file, source_data)
return 'success'
except Exception as e:
logger.error(f"Exception:{str(e)}")
logger.error(traceback.format_exc())
return 'exception'

View File

@@ -116,7 +116,11 @@ class SupportFfmpeg(object):
header_count = 0 header_count = 0
if self.proxy is None: if self.proxy is None:
if self.headers is None: if self.headers is None:
command = [self.__ffmpeg_path, '-y', '-i', self.url, '-c', 'copy', '-bsf:a', 'aac_adtstoasc'] if platform.system() == 'Windows':
command = [self.__ffmpeg_path, '-y', '-i', f'"{self.url}"', '-c', 'copy', '-bsf:a', 'aac_adtstoasc']
else:
command = [self.__ffmpeg_path, '-y', '-i', self.url, '-c', 'copy', '-bsf:a', 'aac_adtstoasc']
else: else:
headers_command = [] headers_command = []
tmp = "" tmp = ""
@@ -136,9 +140,15 @@ class SupportFfmpeg(object):
if len(tmp) > 0: if len(tmp) > 0:
headers_command.append('-headers') headers_command.append('-headers')
headers_command.append(f'{tmp}') headers_command.append(f'{tmp}')
command = [self.__ffmpeg_path, '-y'] + headers_command + ['-i', self.url, '-c', 'copy', '-bsf:a', 'aac_adtstoasc'] if platform.system() == 'Windows':
command = [self.__ffmpeg_path, '-y'] + headers_command + ['-i', f'"{self.url}"', '-c', 'copy', '-bsf:a', 'aac_adtstoasc']
else:
command = [self.__ffmpeg_path, '-y'] + headers_command + ['-i', self.url, '-c', 'copy', '-bsf:a', 'aac_adtstoasc']
else: else:
command = [self.__ffmpeg_path, '-y', '-http_proxy', self.proxy, '-i', self.url, '-c', 'copy', '-bsf:a', 'aac_adtstoasc'] if platform.system() == 'Windows':
command = [self.__ffmpeg_path, '-y', '-http_proxy', self.proxy, '-i', f'"{self.url}"', '-c', 'copy', '-bsf:a', 'aac_adtstoasc']
else:
command = [self.__ffmpeg_path, '-y', '-http_proxy', self.proxy, '-i', self.url, '-c', 'copy', '-bsf:a', 'aac_adtstoasc']
if platform.system() == 'Windows': if platform.system() == 'Windows':
@@ -159,7 +169,7 @@ class SupportFfmpeg(object):
return return
except: except:
pass pass
#logger.error(' '.join(command)) logger.error(' '.join(command))
command = SupportSubprocess.command_for_windows(command) command = SupportSubprocess.command_for_windows(command)
if platform.system() == 'Windows' and header_count > 1: if platform.system() == 'Windows' and header_count > 1:
@@ -216,8 +226,8 @@ SET CRLF=^
else: else:
if os.path.exists(self.temp_fullpath): if os.path.exists(self.temp_fullpath):
os.remove(self.temp_fullpath) os.remove(self.temp_fullpath)
except Exception as exception: except Exception as e:
logger.error('Exception:%s', exception) logger.error(f"Exception:{str(e)}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
arg = {'type':'last', 'status':self.status, 'data' : self.get_data()} arg = {'type':'last', 'status':self.status, 'data' : self.get_data()}
@@ -347,6 +357,7 @@ SET CRLF=^
return data return data
def send_to_listener(self, **arg): def send_to_listener(self, **arg):
print(arg)
if self.total_callback_function != None: if self.total_callback_function != None:
self.total_callback_function(**arg) self.total_callback_function(**arg)
if self.callback_function is not None and self.callback_function != self.total_callback_function: if self.callback_function is not None and self.callback_function != self.total_callback_function:

View File

@@ -0,0 +1,27 @@
import traceback
from support import SupportSubprocess, logger
class SupportFfprobe:
__ffprobe_path = 'ffprobe'
@classmethod
def initialize(cls, __ffprobe_path):
cls.__ffprobe_path = __ffprobe_path
@classmethod
def ffprobe(cls, filepath, ffprobe_path=None, option=None):
try:
if ffprobe_path == None:
ffprobe_path = cls.__ffprobe_path
command = [ffprobe_path, '-v', 'quiet', '-print_format', 'json', '-show_format', '-show_streams', filepath]
if option is not None:
command += option
logger.warning(' '.join(command))
ret = SupportSubprocess.execute_command_return(command, format='json')
return ret['log']
except Exception as e:
logger.error(f"Exception:{str(e)}")
logger.error(traceback.format_exc())

View File

@@ -204,9 +204,9 @@ class GoogleSheetBase:
break break
except gspread.exceptions.APIError: except gspread.exceptions.APIError:
self.sleep_exception() self.sleep_exception()
except Exception as exception: except Exception as e:
logger.error(f"{key} - {value}") logger.error(f"{key} - {value}")
logger.error('Exception:%s', exception) logger.error(f"Exception:{str(e)}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
logger.error(self.header_info) logger.error(self.header_info)
self.sleep_exception() self.sleep_exception()

View File

@@ -0,0 +1,197 @@
import json
import os
import traceback
from support import SupportSubprocess, d, logger
class SupportRclone(object):
__instance_list = []
__rclone_path = 'rclone'
__rclone_config_path = 'rclone.conf'
@classmethod
def initialize(cls, __rclone_path, __rclone_config_path):
cls.__rclone_path = __rclone_path
cls.__rclone_config_path = __rclone_config_path
@classmethod
def get_rclone_path(cls):
return cls.__rclone_path
@classmethod
def __get_cmd(cls, config_path=None):
command = [cls.__rclone_path]
if config_path == None:
command += ['--config', cls.__rclone_config_path]
else:
command += ['--config', config_path]
return command
@classmethod
def rclone_cmd(cls):
return [cls.__rclone_path, '--config', cls.__rclone_config_path]
@classmethod
def get_version(cls, rclone_path=None):
try:
if rclone_path == None:
rclone_path = cls.__rclone_path
cmd = [rclone_path, '--version']
result = SupportSubprocess.execute_command_return(cmd)
if result != None and result['status'] == 'finish':
return result['log']
except Exception as e:
logger.error(f'Exception:{str(e)}')
logger.error(traceback.format_exc())
@classmethod
def config_list(cls, rclone_path=None, rclone_config_path=None, option=None):
try:
if rclone_path == None:
rclone_path = cls.__rclone_path
if rclone_config_path == None:
rclone_config_path = cls.__rclone_config_path
if os.path.exists(rclone_config_path) == False:
return
command = [rclone_path, '--config', rclone_config_path, 'config', 'dump']
if option is not None:
command += option
result = SupportSubprocess.execute_command_return(command, format='json')
for key, value in result['log'].items():
if 'token' in value and value['token'].startswith('{'):
value['token'] = json.loads(value['token'])
return result['log']
except Exception as e:
logger.error(f'Exception:{str(e)}')
logger.error(traceback.format_exc())
@classmethod
def get_config(cls, remote_name, rclone_path=None, rclone_config_path=None, option=None):
try:
data = cls.config_list(rclone_path=rclone_path, rclone_config_path=rclone_config_path, option=option)
return data.get(remote_name, None)
except Exception as e:
logger.error(f"Exception:{str(e)}")
logger.error(traceback.format_exc())
@classmethod
def lsjson(cls, remote_path, config_path=None, option=None):
return cls.__execute_one_param('lsjson', remote_path, config_path=config_path, option=option, format='json')
@classmethod
def lsf(cls, remote_path, config_path=None, option=None):
if option == None:
option = ['--max-depth=1']
return cls.__execute_one_param('lsf', remote_path, config_path=config_path, option=option, format='json')
@classmethod
def size(cls, remote_path, config_path=None, option=None):
if option == None:
option = ['--json']
return cls.__execute_one_param('size', remote_path, config_path=config_path, option=option, format='json')
@classmethod
def mkdir(cls, remote_path, config_path=None, option=None):
return cls.__execute_one_param('mkdir', remote_path, config_path=config_path, option=option, format='json')
@classmethod
def purge(cls, remote_path, config_path=None, option=None):
return cls.__execute_one_param('purge', remote_path, config_path=config_path, option=option, format='json')
@classmethod
def __execute_one_param(cls, command, remote_path, config_path=None, option=None, format=None):
try:
command = cls.__get_cmd(config_path) + [command, remote_path]
if option is not None:
command += option
result = SupportSubprocess.execute_command_return(command, format=format)
ret = None
if result != None and result['status'] == 'finish':
ret = result['log']
return ret
except Exception as e:
logger.error(f'Exception:{str(e)}')
logger.error(traceback.format_exc())
@classmethod
def copy(cls, src, tar, config_path=None, option=None):
return cls.__execute_two_param('copy', src, tar, config_path=config_path, option=option)
@classmethod
def copy_server_side(cls, src, tar, config_path=None, option=None):
if option == None:
option = ['--drive-server-side-across-configs=true', '--delete-empty-src-dirs']
return cls.__execute_two_param('copy', src, tar, config_path=config_path, option=option)
@classmethod
def move(cls, src, tar, config_path=None, option=None):
return cls.__execute_two_param('move', src, tar, config_path=config_path, option=option)
@classmethod
def move_server_side(cls, src, tar, config_path=None, option=None):
if option == None:
option = ['--drive-server-side-across-configs=true', '--delete-empty-src-dirs']
return cls.__execute_two_param('move', src, tar, config_path=config_path, option=option)
@classmethod
def __execute_two_param(cls, command, src, tar, config_path=None, option=None, format=None):
try:
command = cls.__get_cmd(config_path) + [command, src, tar]
if option is not None:
command += option
result = SupportSubprocess.execute_command_return(command, format=format)
ret = None
if result != None and result['status'] == 'finish':
ret = result['log']
return ret
except Exception as e:
logger.error(f'Exception:{str(e)}')
logger.error(traceback.format_exc())
@classmethod
def getid(cls, remote_path, config_path=None, option=None):
try:
command = cls.__get_cmd(config_path) + ['backend', 'getid', remote_path]
if option is not None:
command += option
result = SupportSubprocess.execute_command_return(command)
ret = None
if result != None and result['status'] == 'finish':
ret = result['log']
if ret is not None and (len(ret.split(' ')) > 1 or ret == ''):
ret = None
return ret
except Exception as e:
logger.error(f"Exception:{str(e)}")
logger.error(traceback.format_exc())
@classmethod
def chpar(cls, src, tar, config_path=None, option=None):
try:
command = cls.__get_cmd(config_path) + ['backend', 'chpar', src, tar, '-o', 'depth=1', '-o', 'delete-empty-src-dir', '--drive-use-trash=false']
if option is not None:
command += option
result = SupportSubprocess.execute_command_return(command)
ret = None
if result != None and result['status'] == 'finish':
ret = result['log']
return True
except Exception as e:
logger.error(f"Exception:{str(e)}")
logger.error(traceback.format_exc())
return False

Some files were not shown because too many files have changed in this diff Show More