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
config.yaml
# config.yaml
lib2/
.vscode/
memo.txt
@@ -141,7 +141,7 @@ pre_start.sh
*.code-workspace
false
*copy.py
*.sh
# *.sh
data/
tmp/
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 값은 대체
#- name: "토렌트"
# list:
# - uri: "rss"
#- name: "기본 기능"
# list:
# - uri: "terminal"
# - uri: "command"
# - uri: "flaskfilemanager"
# - uri: "flaskcode"
# - uri: "number_baseball"
#- name: "링크"
@@ -32,6 +26,8 @@
name: "확장 설정"
- uri: "system/plugin"
name: "플러그인 관리"
- uri: "system/tool/command"
name: "Command 관리"
- uri: "-"
- uri: "system/logout"
name: "로그아웃"

View File

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

View File

@@ -3,11 +3,11 @@ Flask
Flask-SQLAlchemy
Flask-Login
Flask-Cors
Flask-Markdown
#Flask-Markdown
Flask-SocketIO
python-engineio
python-socketio
Werkzeug
python-socketio<5.8.0
Werkzeug<3.0
Jinja2
# common util
@@ -25,4 +25,5 @@ pillow
gevent
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 .version import VERSION
frame = Framework.get_instance()
F = frame
logger = frame.logger
app = frame.app
celery = frame.celery
db = frame.db
scheduler = frame.scheduler
socketio = frame.socketio
path_app_root = frame.path_app_root
path_data = frame.path_data
get_logger = frame.get_logger
# 2024.06.13
# 잘못된 설계로 인해 import 만으로 초기화 되버려 lib을 사용할 수 없다.
# 분리.
F = None
frame = None
logger = None
app = None
celery = None
db = None
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 support import d
from .init_declare import User, check_api
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.form)
try:
if F.SystemModelSetting.get_bool('auth_use_apikey'):
if request.method == 'POST':
apikey = request.form['apikey']
else:
apikey = request.args.get('apikey')
#apikey = request.args.get('apikey')
if apikey is None or apikey != F.SystemModelSetting.get('auth_apikey'):
if F.SystemModelSetting.get_bool('use_apikey'):
try:
d = request.get_json()
except Exception:
d = request.form.to_dict() if request.method == 'POST' else request.args.to_dict()
apikey = d.get('apikey')
if apikey is None or apikey != F.SystemModelSetting.get('apikey'):
F.logger.warning('CHECK API : ABORT no match ({})'.format(apikey))
F.logger.warning(request.environ.get('HTTP_X_REAL_IP', request.remote_addr))
abort(403)
@@ -31,7 +31,7 @@ def check_api(original_function):
return original_function(*args, **kwargs) #2
return wrapper_function
# Suuport를 logger 생성전에 쓰지 않기 위해 중복 선언
# Support를 logger 생성전에 쓰지 않기 위해 중복 선언
import logging
@@ -47,7 +47,7 @@ class CustomFormatter(logging.Formatter):
# pathname filename
#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 = {
logging.DEBUG: __format.format(color=grey, reset=reset, yellow=yellow, green=green),

View File

@@ -8,14 +8,15 @@ import time
import traceback
from datetime import datetime
import redis
import yaml
from flask import Flask
from flask_cors import CORS
from flask_login import LoginManager, login_required
from flask_socketio import SocketIO
from flask_sqlalchemy import SQLAlchemy
from flaskext.markdown import Markdown
from pytz import timezone, utc
from werkzeug.middleware.proxy_fix import ProxyFix
from .init_declare import CustomFormatter, check_api
@@ -37,12 +38,15 @@ class Framework:
self.db = None
self.scheduler = None
self.socketio = None
self.rd = None
self.path_app_root = None
self.path_data = None
self.users = {}
self.get_cache = None
self.__level_unset_logger_list = []
self.__logger_list = []
self.all_log_filehandler = None
self.__exit_code = -1
self.login_manager = None
#self.plugin_instance_list = {}
@@ -59,14 +63,17 @@ class Framework:
def __initialize(self):
os.environ["PYTHONUNBUFFERED"] = "1"
os.environ['FF'] = "true"
os.environ['FF_PYTHON'] = sys.executable
self.__config_initialize("first")
self.__make_default_dir()
self.logger = self.get_logger(__package__)
self.get_logger('support')
import support
self.__prepare_starting()
self.app = Flask(__name__)
self.app.wsgi_app = ProxyFix(self.app.wsgi_app, x_proto=1)
self.__config_initialize('flask')
self.__init_db()
@@ -82,7 +89,6 @@ class Framework:
self.socketio = SocketIO(self.app, cors_allowed_origins="*", async_mode='threading')
CORS(self.app)
Markdown(self.app)
self.login_manager = LoginManager()
self.login_manager.init_app(self.app)
@@ -94,10 +100,11 @@ class Framework:
self.app.config.update(
DROPZONE_MAX_FILE_SIZE = 102400,
DROPZONE_TIMEOUT = 5*60*1000,
#DROPZONE_ALLOWED_FILE_CUSTOM = True,
#DROPZONE_ALLOWED_FILE_TYPE = 'default, image, audio, video, text, app, *.*',
DROPZONE_ALLOWED_FILE_CUSTOM = True,
DROPZONE_ALLOWED_FILE_TYPE = "image/*, audio/*, video/*, text/*, application/*, *.*",
)
self.dropzone = Dropzone(self.app)
def __init_db(self):
@@ -131,19 +138,20 @@ class Framework:
def __init_celery(self):
redis_port = 6379
try:
from celery import Celery
#if frame.config['use_celery'] == False or platform.system() == 'Windows':
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)
if redis_port == None:
redis_port = self.config.get('redis_port', None)
if redis_port == None:
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_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: {options}")
celery.steps['worker'].add(CustomArgs)
except Exception as e:
if self.config['use_celery']:
self.logger.error('CELERY!!!')
@@ -187,6 +196,14 @@ class Framework:
if len(args) > 0 and type(args[0]) == type(dummy_func):
return args[0]
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
@@ -201,11 +218,13 @@ class Framework:
self.logger.error(f'Exception:{str(e)}')
self.logger.error(traceback.format_exc())
self.SystemModelSetting = SystemInstance.ModelSetting
SystemInstance.plugin_load()
if self.config['run_flask']:
SystemInstance.plugin_load()
self.app.register_blueprint(SystemInstance.blueprint)
self.config['flag_system_loading'] = True
self.__config_initialize('member')
self.__config_initialize('system_loading_after')
self.set_level(self.SystemModelSetting.get_int('log_level'))
def initialize_plugin(self):
@@ -232,6 +251,7 @@ class Framework:
self.__make_default_logger()
self.__config_initialize("last")
self.config['loading_completed'] = True
self.logger.info('### LAST')
self.logger.info(f"### PORT: {self.config.get('port')}")
self.logger.info('### Now you can access App by webbrowser!!')
@@ -248,6 +268,7 @@ class Framework:
def __config_initialize(self, mode):
if mode == "first":
self.config = {}
self.config['loading_completed'] = False
self.config['os'] = platform.system()
self.config['flag_system_loading'] = 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['exist_export'] = os.path.exists(self.config['export_filepath'])
self.config['recent_version'] = '--'
from .version import VERSION
self.config['version'] = VERSION
self.__process_args()
self.__load_config()
self.__init_define()
@@ -270,7 +293,7 @@ class Framework:
self.config['notify_yaml_filepath'] = os.path.join(self.config['path_data'], 'db', 'notify.yaml')
if 'running_type' not in self.config:
self.config['running_type'] = 'native'
self.pip_install()
elif mode == "flask":
self.app.secret_key = os.urandom(24)
self.app.config['TEMPLATES_AUTO_RELOAD'] = True
@@ -295,9 +318,7 @@ class Framework:
self.config['DEFINE'] = {}
# 이건 필요 없음
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):
# celery 에서 args 처리시 문제 발생.
@@ -363,6 +384,9 @@ class Framework:
self.config['debug'] = False
if self.config.get('plugin_update') == None:
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:
self.config['plugin_loading_only_devpath'] = False
if self.config.get('plugin_loading_list') == None:
@@ -402,8 +426,8 @@ class Framework:
try:
if self.config['flag_system_loading']:
try:
from system import SystemModelSetting
level = SystemModelSetting.get_int('log_level')
#from system import SystemModelSetting
level = self.SystemModelSetting.get_int('log_level')
except:
level = logging.DEBUG
if self.__level_unset_logger_list is not None:
@@ -426,7 +450,7 @@ class Framework:
return converted.timetuple()
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:
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.setFormatter(file_formatter)
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:
streamHandler = logging.StreamHandler()
streamHandler.setFormatter(CustomFormatter())
logger.addHandler(streamHandler)
if self.all_log_filehandler != None:
logger.addHandler(self.all_log_filehandler)
return logger
@@ -459,7 +491,7 @@ class Framework:
def set_level(self, level):
try:
for l in self.__logger_list:
l.setLevel(level)
l.setLevel(int(level))
self.__make_default_logger()
except:
pass
@@ -468,7 +500,7 @@ class Framework:
def start(self):
host = '0.0.0.0'
for i in range(5):
for i in range(5):
try:
#self.logger.debug(d(self.config))
# allow_unsafe_werkzeug=True termux nohup 실행시 필요함
@@ -517,8 +549,8 @@ class Framework:
PluginManager.plugin_unload()
with self.app.test_request_context():
self.socketio.stop()
except Exception as exception:
self.logger.error('Exception:%s', exception)
except Exception as e:
self.logger.error(f"Exception:{str(e)}")
self.logger.error(traceback.format_exc())
def get_recent_version(self):
@@ -532,3 +564,11 @@ class Framework:
self.logger.error(traceback.format_exc())
self.config['recent_version'] = '확인 실패'
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 shutil
import traceback
from framework import F, logger
from support import SupportYaml, d
from framework import F
class MenuManager:
menu_map = None
@classmethod
def __load_menu_yaml(cls):
menu_yaml_filepath = os.path.join(F.config['path_data'], 'db', 'menu.yaml')
if os.path.exists(menu_yaml_filepath) == False:
shutil.copy(
os.path.join(F.config['path_app'], 'files', 'menu.yaml.template'),
menu_yaml_filepath
)
cls.menu_map = SupportYaml.read_yaml(menu_yaml_filepath)
try:
menu_yaml_filepath = os.path.join(F.config['path_data'], 'db', 'menu.yaml')
if os.path.exists(menu_yaml_filepath) == False:
shutil.copy(
os.path.join(F.config['path_app'], 'files', 'menu.yaml.template'),
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
def init_menu(cls):
cls.__load_menu_yaml()
from .init_plugin import PluginManager
plugin_menus = PluginManager.plugin_menus
copy_map = []
if cls.__init_menu() == False:
cls.menu_map = SupportYaml.read_yaml(os.path.join(F.config['path_app'], 'files', 'menu.yaml.template'))
cls.__init_menu()
for category in cls.menu_map:
if 'uri' in category:
copy_map.append(category)
continue
cate_count = 0
@classmethod
def __init_menu(cls):
try:
from .init_plugin import PluginManager
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 = []
for item in category['list']:
if item['uri'] in plugin_menus:
plugin_menus[item['uri']]['match'] = True
tmp_cate_list.append(plugin_menus[item['uri']]['menu'])
cate_count += 1
elif item['uri'].startswith('http'):
tmp_cate_list.append({
'uri': item['uri'],
'name': item['name'],
'target': item.get('target', '_blank')
})
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 ['-']:
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 = []
for item in category['list']:
if item['uri'] in plugin_menus:
plugin_menus[item['uri']]['match'] = True
tmp_cate_list.append(plugin_menus[item['uri']]['menu'])
cate_count += 1
elif item['uri'].startswith('http'):
tmp_cate_list.append({
'uri': item['uri'],
'name': item['name'],
'target': item.get('target', '_blank')
})
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 ['-']:
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':[]
cate_count += 1
elif item['uri'] == 'setting':
# 2024.06.04
# 확장설정도 메뉴 구성
if len(PluginManager.setting_menus) > 0:
set_tmp = item.get('list')
if set_tmp:
cp = PluginManager.setting_menus.copy()
include = []
for set_ch in set_tmp:
if set_ch.get('uri') and (set_ch.get('uri') == '-' or set_ch.get('uri').startswith('http')):
include.append(set_ch)
continue
for i, ps in enumerate(cp):
if set_ch.get('plugin') != None and set_ch.get('plugin') == ps.get('plugin'):
include.append(ps)
del cp[i]
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['count'] += 1
c['list'].append(plugin_menu['menu'])
c = cls.menu_map[-2]
c['count'] += 1
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))
@classmethod
def get_menu_map(cls):
#F.logger.warning(d(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 sys
import threading
import time
import traceback
import zipfile
import requests
from support import SupportFile, SupportSubprocess, SupportYaml
from framework import F
from support import SupportFile, SupportSubprocess, SupportYaml
class PluginManager:
@@ -30,13 +30,13 @@ class PluginManager:
tmps = os.listdir(plugin_path)
add_plugin_list = []
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)
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
except Exception as exception:
F.logger.error('Exception:%s', exception)
except Exception as e:
F.logger.error(f"Exception:{str(e)}")
F.logger.error(traceback.format_exc())
if F.config.get('plugin_loading_only_devpath', None) == True:
@@ -59,12 +59,12 @@ class PluginManager:
tmps = os.listdir(__)
add_plugin_list = []
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)
cls.all_package_list[t] = {'pos':'dev', 'path':os.path.join(__, t), 'loading':True}
plugins = plugins + add_plugin_list
except Exception as exception:
F.logger.error('Exception:%s', exception)
except Exception as e:
F.logger.error(f"Exception:{str(e)}")
F.logger.error(traceback.format_exc())
# plugin_loading_list
@@ -79,8 +79,8 @@ class PluginManager:
cls.all_package_list[_]['loading'] = False
cls.all_package_list[_]['status'] = 'not_include_loading_list'
plugins = new_plugins
except Exception as exception:
F.logger.error('Exception:%s', exception)
except Exception as e:
F.logger.error(f"Exception:{str(e)}")
F.logger.error(traceback.format_exc())
# plugin_except_list
@@ -95,8 +95,8 @@ class PluginManager:
cls.all_package_list[_]['loading'] = False
cls.all_package_list[_]['status'] = 'include_except_list'
plugins = new_plugins
except Exception as exception:
F.logger.error('Exception:%s', exception)
except Exception as e:
F.logger.error(f"Exception:{str(e)}")
F.logger.error(traceback.format_exc())
return plugins
@@ -111,45 +111,32 @@ class PluginManager:
F.logger.debug(plugins)
for plugin_name in plugins:
F.logger.debug(f'[+] PLUGIN LOADING Start.. [{plugin_name}]')
F.logger.info(f'[+] PLUGIN IMPORT Start.. [{plugin_name}]')
import_start_time = time.time()
entity = cls.all_package_list[plugin_name]
entity['version'] = '3'
try:
mod = __import__('%s' % (plugin_name), fromlist=[])
mod_plugin_info = None
try:
mod_plugin_info = getattr(mod, 'plugin_info')
entity['module'] = mod
except Exception as exception:
F.logger.info(f'[!] PLUGIN_INFO not exist : [{plugin_name}] - is FF')
if mod_plugin_info == None:
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}]')
mod = __import__(f'{plugin_name}.setup', fromlist=['setup'])
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}]')
continue
try:
if entity['version'] != '4':
mod_blue_print = getattr(mod, 'blueprint')
else:
entity['setup_mod'] = mod
entity['P'] = getattr(mod, 'P')
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:
F.app.register_blueprint(mod_blue_print)
except Exception as exception:
#logger.error('Exception:%s', exception)
#logger.error(traceback.format_exc())
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
#system.LogicPlugin.current_loading_plugin_list[plugin_name]['status'] = 'success'
#system.LogicPlugin.current_loading_plugin_list[plugin_name]['info'] = mod_plugin_info
except Exception as exception:
F.logger.error('Exception:%s', exception)
except Exception as e:
F.logger.error(f"Exception:{str(e)}")
F.logger.error(traceback.format_exc())
F.logger.debug('no blueprint')
cls.all_package_list[plugin_name]['loading'] = False
@@ -157,36 +144,44 @@ class PluginManager:
cls.all_package_list[plugin_name]['log'] = traceback.format_exc()
if not F.config['run_celery']:
try:
with F.app.app_context():
F.db.create_all()
except Exception as exception:
F.logger.error('Exception:%s', exception)
except Exception as e:
F.logger.error(f"Exception:{str(e)}")
F.logger.error(traceback.format_exc())
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']:
return
for key, entity in cls.plugin_list.items():
try:
mod_plugin_load = None
if entity['version'] == '3':
mod_plugin_load = getattr(entity['module'], 'plugin_load')
elif entity['version'] == '4':
mod_plugin_load = getattr(entity['P'], 'plugin_load')
mod_plugin_load = getattr(entity['P'], 'plugin_load')
if mod_plugin_load:
def func(mod_plugin_load, key):
try:
F.logger.debug(f'[!] plugin_load threading start : [{key}]')
#mod.plugin_load()
load_start_time = time.time()
F.logger.info(f'[!] plugin_load threading start : [{key}]')
mod_plugin_load()
F.logger.debug(f'[!] plugin_load threading end : [{key}]')
except Exception as exception:
load_elapsed_time = time.time() - load_start_time
F.logger.info(f'[!] plugin_load threading end : [{key}] ({load_elapsed_time:.3f}s)')
except Exception as e:
F.logger.error('### plugin_load exception : %s', key)
F.logger.error('Exception:%s', exception)
F.logger.error(f"Exception:{str(e)}")
F.logger.error(traceback.format_exc())
cls.all_package_list[key]['loading'] = False
cls.all_package_list[key]['status'] = 'plugin_load error'
@@ -199,42 +194,29 @@ class PluginManager:
MenuManager.init_menu()
F.logger.info(f"플러그인 로딩 실패로 메뉴 삭제2 : {key}")
t = threading.Thread(target=func, args=(mod_plugin_load, key))
t.setDaemon(True)
t.start()
# mod는 위에서 로딩
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:
except Exception as e:
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:
mod_menu = None
if entity['version'] == '3':
mod_menu = getattr(entity['module'], 'menu')
elif entity['version'] == '4':
mod_menu = getattr(entity['P'], 'menu')
mod_menu = getattr(entity['P'], 'menu')
if mod_menu and cls.all_package_list[key]['loading'] != False:
cls.plugin_menus[key]= {'menu':mod_menu, 'match':False}
if entity['version'] == '4':
setting_menu = getattr(entity['P'], 'setting_menu')
if setting_menu != None and cls.all_package_list[key]['loading'] != False:
F.logger.info(f"메뉴 포함 : {key}")
cls.setting_menus.append(setting_menu)
setting_menu = getattr(entity['P'], 'setting_menu')
if setting_menu != None and cls.all_package_list[key]['loading'] != False:
setting_menu['plugin'] = entity['P'].package_name
F.logger.info(f"확장 설정 : {key}")
cls.setting_menus.append(setting_menu)
except Exception as exception:
F.logger.debug('no menu')
F.logger.debug('### plugin_load threading all start.. : %s ', len(cls.plugin_list))
# 모든 모듈을 로드한 이후에 app 등록, table 생성, start
except Exception as exception:
F.logger.error('Exception:%s', exception)
except Exception as e:
F.logger.error(f"Exception:{str(e)}")
F.logger.error(traceback.format_exc())
@@ -243,17 +225,9 @@ class PluginManager:
def plugin_unload(cls):
for key, entity in cls.plugin_list.items():
try:
if entity['version'] == '3':
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')
mod_plugin_unload = getattr(entity['P'], 'plugin_unload')
if mod_plugin_unload:
mod_plugin_unload()
#mod.plugin_unload()
except Exception as e:
F.logger.error('module:%s', key)
F.logger.error(f'Exception:{str(e)}')
@@ -267,6 +241,7 @@ class PluginManager:
@classmethod
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
ret = {}
try:
@@ -381,7 +356,7 @@ class PluginManager:
tmps = os.listdir(plugins_path)
for t in tmps:
plugin_path = os.path.join(plugins_path, t)
if t.startswith('_'):
if t.startswith('_') or t.startswith('.'):
continue
if os.path.exists(os.path.join(plugin_path, '.git')):
command = ['git', '-C', plugin_path, 'reset', '--hard', 'HEAD']
@@ -392,14 +367,15 @@ class PluginManager:
F.logger.debug(ret)
else:
F.logger.debug(f"{plugin_path} not git repo")
except Exception as exception:
F.logger.error('Exception:%s', exception)
except Exception as e:
F.logger.error(f"Exception:{str(e)}")
F.logger.error(traceback.format_exc())
@classmethod
def get_plugin_instance(cls, package_name):
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:
pass

View File

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

View File

@@ -4,6 +4,10 @@ from framework import F
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)
if match:
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_theme=get_theme)
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(dropzone=F.dropzone)
app.jinja_env.filters['get_menu'] = get_menu
app.jinja_env.filters['get_theme'] = get_theme
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.auto_reload = True

View File

@@ -4,17 +4,18 @@ import time
import traceback
from flask import request
from support import SingletonClass
from framework import F
from support import SingletonClass
namespace = 'log'
@F.socketio.on('connect', namespace='/%s' % namespace)
@F.login_required
def socket_connect():
F.logger.debug('log connect')
@F.socketio.on('start', namespace='/%s' % namespace)
@F.login_required
def socket_file(data):
try:
package = filename = None
@@ -24,8 +25,8 @@ def socket_file(data):
filename = data['filename']
LogViewer.instance().start(package, filename, request.sid)
F.logger.debug('start package:%s filename:%s sid:%s', package, filename, request.sid)
except Exception as exception:
F.logger.error('Exception:%s', exception)
except Exception as e:
F.logger.error(f"Exception:{str(e)}")
F.logger.error(traceback.format_exc())
@F.socketio.on('disconnect', namespace='/%s' % namespace)
@@ -33,8 +34,8 @@ def disconnect():
try:
LogViewer.instance().disconnect(request.sid)
F.logger.debug('disconnect sid:%s', request.sid)
except Exception as exception:
F.logger.error('Exception:%s', exception)
except Exception as e:
F.logger.error(f"Exception:{str(e)}")
F.logger.error(traceback.format_exc())
@@ -62,18 +63,17 @@ class WatchThread(threading.Thread):
key = 'filename'
value = self.filename
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)
while not self.stop_flag:
line = f.readline()
if not line:
time.sleep(0.1) # Sleep briefly
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)
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):

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:
self.remove_job("scheduler_check")
#time.sleep(30)
except Exception as exception:
self.logger.error('Exception:%s', exception)
except Exception as e:
self.logger.error(f"Exception:{str(e)}")
self.logger.error(traceback.format_exc())
def shutdown(self):
@@ -233,21 +233,21 @@ class Job(object):
if self.args is None:
self.thread = threading.Thread(target=self.target_function, args=())
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.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()
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.running_timedelta = self.end_time - self.start_time
self.status = 'success'
if not F.scheduler.is_include(self.job_id):
F.scheduler.remove_job_instance(self.job_id)
self.count += 1
except Exception as exception:
except Exception as e:
self.status = 'exception'
F.logger.error('Exception:%s', exception)
F.logger.error(f"Exception:{str(e)}")
F.logger.error(traceback.format_exc())
finally:
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 {
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 MODULE_NAME = tmp[2];
var PAGE_NAME = "";
} else if (tmp.length == 4){
} else if (tmp.length > 3){
var PACKAGE_NAME = tmp[1];
var MODULE_NAME = tmp[2];
var PAGE_NAME = tmp[3];
@@ -23,8 +23,6 @@ $(window).on("load resize", 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);
}
document.getElementById("modal_body").innerHTML = '<pre style="white-space: pre-wrap;">' +data + '</pre>';
$("#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;
}
// 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 frameSocket = io.connect(protocol + "//" + document.domain + ":" + location.port + "/framework");
console.log(frameSocket);
frameSocket.on('notify', function(data){
$.notify({
message : data['msg'],
message : '<strong>' + data['msg'] + '</strong>',
url: data['url'],
target: '_self'
},{
@@ -29,7 +28,7 @@ frameSocket.on('notify', 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){
@@ -37,14 +36,12 @@ frameSocket.on('loading_hide', function(data){
});
frameSocket.on('refresh', function(data){
console.log('data')
window.location.reload();
});
$('#command_modal').on('hide.bs.modal', function (e) {
//e.preventDefault(); 있으면 동작 안함.
console.log("ff global command_modal hide.bs.modal CATCH")
$.ajax({
url: `/global/ajax/command_modal_hide`,
type: 'POST',
@@ -74,13 +71,27 @@ $("body").on('click', '#globalLinkBtn', function(e) {
window.location.href = url;
});
$("body").on('click', '#globalReloadBtn', function(e) {
e.preventDefault();
location.reload();
});
// global_link_btn 모두 찾아 변경
$("body").on('click', '#globalSettingSaveBtn', function(e){
e.preventDefault();
globalSettingSave();
if (globalSettingSaveBefore()) {
globalSettingSave();
}
});
function globalSettingSaveBefore() {
return true;
}
function globalSettingSaveAfter() {
return true;
}
function globalSettingSave() {
var formData = getFormdata('#setting');
$.ajax({
@@ -94,6 +105,7 @@ function globalSettingSave() {
$.notify('<strong>설정을 저장하였습니다.</strong>', {
type: 'success'
});
globalSettingSaveAfter();
} else {
$.notify('<strong>설정 저장에 실패하였습니다.</strong>', {
type: 'warning'
@@ -106,7 +118,10 @@ function globalSettingSave() {
$("body").on('click', '#globalEditBtn', function(e) {
e.preventDefault();
file = $(this).data('file');
console.log(file);
if (file == null) {
var tag = $(this).data('tag');
file = $('#' + tag).val();
}
$.ajax({
url: '/global/ajax/is_available_edit',
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){
e.preventDefault();
document.getElementById("confirm_title").innerHTML = "DB 삭제";
document.getElementById("confirm_body").innerHTML = "전체 목록을 삭제 하시겠습니까?";
$('#confirm_button').attr('onclick', "globalDbDelete();");
$("#confirm_modal").modal();
return;
globalConfirmModal('DB 삭제', "전체 목록을 삭제 하시겠습니까?", function() {
globalDbDelete(0);
});
});
function globalDbDelete() {
function globalDbDelete(day) {
$.ajax({
url: '/'+PACKAGE_NAME+'/ajax/' + MODULE_NAME + '/reset_db',
url: '/'+PACKAGE_NAME+'/ajax/' + MODULE_NAME + '/db_delete',
type: "POST",
cache: false,
data: {},
data: {day:day},
dataType: "json",
success: function (data) {
if (data) {
$.notify('<strong>삭제하였습니다.</strong>', {
type: 'success'
});
} else {
if (data == -1) {
$.notify('<strong>삭제에 실패하였습니다.</strong>',{
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){
e.preventDefault();
document.getElementById("confirm_title").innerHTML = "DB 삭제";
document.getElementById("confirm_body").innerHTML = "전체 목록을 삭제 하시겠습니까?";
$('#confirm_button').attr('onclick', "globalDbDeletePage();");
$("#confirm_modal").modal();
return;
globalConfirmModal('DB 삭제', "최근 " + day + "일 이내 데이터를 제외하고 삭제 하시겠습니까?", function() {
globalDbDeletePage(0);
});
});
function globalDbDeletePage() {
function globalDbDeletePage(day) {
$.ajax({
url: '/'+PACKAGE_NAME+'/ajax/' + MODULE_NAME + '/' + PAGE_NAME + '/reset_db',
type: "POST",
cache: false,
data: {sub:sub},
data: {day:day},
dataType: "json",
success: function (data) {
if (data) {
$.notify('<strong>삭제하였습니다.</strong>', {
type: 'success'
});
} else {
if (data == -1) {
$.notify('<strong>삭제에 실패하였습니다.</strong>',{
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 - 함수
///////////////////////////////////////
function globalSendCommand(command, arg1, arg2, arg3, modal_title, callback) {
console.log("globalSendCommand [" + command + '] [' + arg1 + '] [' + arg2 + '] [' + arg3 + '] [' + modal_title + '] [' + callback + ']');
console.log('/' + PACKAGE_NAME + '/ajax/' + MODULE_NAME + '/command');
function globalSendCommand(command, arg1, arg2, arg3, callback) {
var url = '/' + PACKAGE_NAME + '/ajax/' + MODULE_NAME + '/command';
return globalSendCommandByUrl(url, command, arg1, arg2, arg3, callback);
}
function globalSendCommandByUrl(url, command, arg1, arg2, arg3, callback) {
$.ajax({
url: '/' + PACKAGE_NAME + '/ajax/' + MODULE_NAME + '/command',
url: url,
type: "POST",
cache: false,
data:{command:command, arg1:arg1, arg2:arg2, arg3},
dataType: "json",
success: function (ret) {
console.log(ret)
if (ret.msg != null) notify(ret.msg, ret.ret);
if (ret.modal != null) showModal(ret.modal, modal_title, false);
if (ret.json != null) showModal(ret.json, modal_title, true);
if (ret.modal != null) showModal(ret.modal, ret.title, false);
if (ret.json != null) showModal(ret.json, ret.title, true);
if (callback != null) callback(ret);
if (ret.reload) location.reload();
}
});
}
function globalSendCommandPage(command, arg1, arg2, arg3, modal_title, callback) {
console.log("globalSendCommandPage [" + command + '] [' + arg1 + '] [' + arg2 + '] [' + arg3 + '] [' + modal_title + '] [' + callback + ']');
console.log('/' + PACKAGE_NAME + '/ajax/' + MODULE_NAME + '/command');
function globalSendCommandPage(command, arg1, arg2, arg3, callback) {
var url = '/' + PACKAGE_NAME + '/ajax/' + MODULE_NAME + '/' + PAGE_NAME + '/command';
return globalSendCommandPageByUrl(url, command, arg1, arg2, arg3, callback);
}
function globalSendCommandPageByUrl(url, command, arg1, arg2, arg3, callback) {
$.ajax({
url: '/' + PACKAGE_NAME + '/ajax/' + MODULE_NAME + '/' + PAGE_NAME + '/command',
url: url,
type: "POST",
cache: false,
data:{command:command, arg1:arg1, arg2:arg2, arg3},
dataType: "json",
success: function (ret) {
if (ret.msg != null) notify(ret.msg, ret.ret);
if (ret.modal != null) m_modal(ret.modal, modal_title, false);
if (ret.json != null) m_modal(ret.json, modal_title, true);
if (ret.modal != null) showModal(ret.modal, ret.title, false);
if (ret.json != null) showModal(ret.json, ret.title, true);
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>'
}
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> \
</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'
}).done((datas) => {
console.log(datas)
if (datas.length == 0) {
return false;
}
@@ -510,8 +625,6 @@ let listdir = (path = '/', only_dir = true) => {
} else {
//new_path = (path !== path_spliter) ? path + path_spliter + $(evt.currentTarget).text() : path + $(evt.currentTarget).text();
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
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';
if (outline) {
str += '-outline';
}
str += '-' + color+'';
str += ' ' + _class;
if (small) {
str += ' py-0" style="font-size: 0.8em;"';
} else {
@@ -35,9 +36,14 @@ function j_button_small(id, text, data={}, color='primary', outline=true) {
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;
}
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') {
var str = '<div class="col-sm-' + w + ' " style="text-align: '+align+'; word-break:break-all;">';
str += h;
@@ -45,6 +51,13 @@ function j_col(w, h, align='left') {
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') {
var str = '<div class="col-sm-' + w + ' " style="padding:0px; margin:0px; text-align: '+align+'; word-break:break-all;">';
str += h;
@@ -87,57 +100,101 @@ function j_row_info(left, right, l=2, r=8) {
function j_progress(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 " style="margin-top:2px">'+label+'</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 position-absolute" style="margin-top:2px">'+label+'</div>';
str += '</div>'
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 make_log(key, value, left=2, right=10) {
row = m_col(left, key, aligh='right');
row += m_col(right, value, aligh='left');
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();
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에서 화면 생성
function text_color(text, color='red') {
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') {
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>
<!-- 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>
<body class="body ">
@@ -50,7 +56,8 @@
</div>
</main>
<div class="loading" id="loading">
<img src="/static/img/loading.gif" />
<!-- <img src="/static/img/loading.gif" /> -->
<img src="/static/img/loader.svg" />
</div>
{{ modals() }}
</body>
@@ -59,3 +66,5 @@
<!-- 글로벌 버튼이 모두 나오고 처리-->
<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_global_plugin.js') }}"></script>

View File

@@ -1,81 +1,208 @@
{% extends "base.html" %}
{% block content %}
<div>
<nav>
{{ macros.m_tab_head_start() }}
{{ macros.m_tab_head('old', '이전', true) }}
{{ macros.m_tab_head('new', '실시간', false) }}
{{ macros.m_tab_head_end() }}
</nav>
<div class="tab-content" id="nav-tabContent">
{{ macros.m_tab_content_start('이전', true) }}
<div>
<textarea id="log" class="col-md-12" rows="30" charswidth="23" disabled style="background-color:#ffffff;visibility:hidden"></textarea>
</div>
{{ macros.m_tab_content_end() }}
<style>
/* Unified Log Page Design (matches gds_dviewer) */
.log-wrapper {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
{{ macros.m_tab_content_start('실시간', false) }}
<div>
<textarea id="add" class="col-md-12" rows="30" charswidth="23" disabled style="background-color:#ffffff;visibility:visible"></textarea>
</div>
<div class="form-inline">
<label class="form-check-label" for="auto_scroll">자동 스크롤</label>
<input id="auto_scroll" name="auto_scroll" class="form-control form-control-sm" type="checkbox" data-toggle="toggle" checked>
<span class='text-left' style="padding-left:25px; padding-top:0px">
<button id="clear" class="btn btn-sm btn-outline-success">리셋</button>
</span>
</div>
{{ macros.m_tab_content_end() }}
.log-card {
background: linear-gradient(145deg, rgba(20, 30, 48, 0.95), rgba(36, 59, 85, 0.9));
border: 1px solid rgba(100, 150, 180, 0.25);
border-radius: 12px;
overflow: hidden;
}
</div>
.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>
<script type="text/javascript">
$(document).ready(function() {
setWide();
$('#loading').show();
ResizeTextAreaLog()
})
function ResizeTextAreaLog() {
ClientHeight = window.innerHeight
$("#log").height(ClientHeight-240);
$("#add").height(ClientHeight-260);
function escapeHtml(text) {
var div = document.createElement('div');
div.appendChild(document.createTextNode(text));
return div.innerHTML;
}
$(window).resize(function() {
ResizeTextAreaLog();
function formatLogLine(line) {
var className = '';
if (line.indexOf('ERROR') !== -1) className = 'log-line-error';
else if (line.indexOf('WARNING') !== -1) className = 'log-line-warning';
else if (line.indexOf('INFO') !== -1) className = 'log-line-info';
else if (line.indexOf('DEBUG') !== -1) className = 'log-line-debug';
return '<div class="' + className + '">' + escapeHtml(line) + '</div>';
}
// Tab switching
document.querySelectorAll('.log-tab').forEach(function(tab) {
tab.addEventListener('click', function() {
document.querySelectorAll('.log-tab').forEach(function(t) { t.classList.remove('active'); });
document.querySelectorAll('.log-content').forEach(function(c) { c.classList.remove('active'); });
this.classList.add('active');
document.getElementById('tab-' + this.dataset.tab).classList.add('active');
});
});
$(document).ready(function() {
setWide();
$('#loading').show();
});
var protocol = window.location.protocol;
var socket = io.connect(protocol + "//" + document.domain + ":" + location.port + "/log");
socket.emit("start", {'package':'{{package}}'} );
socket.on('on_start', function(data){
document.getElementById("log").innerHTML += data.data;
document.getElementById("log").scrollTop = document.getElementById("log").scrollHeight;
document.getElementById("log").style.visibility = 'visible';
$('#loading').hide();
socket.emit("start", {'package':'{{package}}'});
socket.on('on_start', function(data) {
var container = document.getElementById("log-history");
var lines = data.data.split('\n');
var html = '';
for (var i = 0; i < lines.length; i++) {
html += formatLogLine(lines[i]);
}
container.innerHTML = html || '<div style="text-align:center;color:#64748b;">로그가 비어 있습니다.</div>';
container.scrollTop = container.scrollHeight;
$('#loading').hide();
});
socket.on('add', function(data){
if (data.package == "{{package}}") {
var chk = $('#auto_scroll').is(":checked");
document.getElementById("add").innerHTML += data.data;
if (chk) document.getElementById("add").scrollTop = document.getElementById("add").scrollHeight;
}
socket.on('add', function(data) {
if (data.package == "{{package}}") {
var chk = $('#auto_scroll').is(":checked");
var container = document.getElementById("log-realtime");
container.innerHTML += formatLogLine(data.data);
if (chk) container.scrollTop = container.scrollHeight;
}
});
$("#clear").click(function(e) {
e.preventDefault();
document.getElementById("add").innerHTML = '';
e.preventDefault();
document.getElementById("log-realtime").innerHTML = '';
});
$("#auto_scroll").click(function(){
var chk = $(this).is(":checked");//.attr('checked');
});
</script>
</script>
{% endblock %}

View File

@@ -25,11 +25,11 @@
<!---->
{% 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 %}
{% macro m_tab_head_end() %}
</div>
</div>
{% endmacro %}
{% 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>
{% endif %}
{% endmacro %}
<!----------------------------------------------------------------->
<!-- SETTING -->
<!-- SETTING -->
<!-- SETTING -->
<!------------------------------------------------------------------>
<!-- 설정 -->
<!-- SETTING 기본 틀-->
{% macro setting_top(left='', padding='10') %}
@@ -57,6 +70,16 @@
<div class='col-sm-9'>
{% 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') %}
{% if desc is not none %}
<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) }}
<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>
{% else %}
<input id="globalSchedulerSwitchBtn" name="globalSchedulerSwitchBtn" class="form-control form-control-sm" type="checkbox" data-toggle="toggle">
{% 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>
{% 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>
{% endif %}
{% endif %}
@@ -268,13 +312,116 @@
setting_gole
<!-- NOT SETTING -->
<!-- NOT SETTING -->
<!-- NOT SETTING -->
<!-- SELECT Dummy
option을 script로 넣을 때 사용
예: 시스템 - 전체로그
-->
{% 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) %}
<div class="btn-group btn-group-sm flex-wrap mr-2" role="group">
@@ -304,6 +451,14 @@
{{ setting_bottom(desc, padding_top='-5') }}
{% 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) %}
{{ setting_top(left, padding) }}
<div style="padding-left:20px; padding-top:-5px;">
@@ -354,219 +509,208 @@
<!-- SELECT Dummy
option을 script로 넣을 때 사용
예: 시스템 - 전체로그
-->
{% 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) }}
{% 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="{{id}}_modal_body" style="word-break:break-all;">
{% endmacro %}
<!-- 삭제해야함 --------------------------------------------------------->
<!--
{% 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>
{% 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" />
</div>
{% endfor %}
</div>
{{ setting_bottom(desc) }}
</div>
</div>
<!-- Modal end -->
{% 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 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 }}">
{% macro m_modal_end_with_button(buttons) %}
</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 == '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 class="modal-footer">
<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] %}
data-{{d[0]}}="{{d[1]}}""
{% endfor %}
{% endif %}
>{{b[1]}}</button>
{% endfor %}
<button type="button" class="btn btn-sm btn-warning" data-dismiss="modal">닫기 (취소)</button>
</div>
</div>
{{ setting_bottom(desc) }}
<div class="loading" id="modal_loading">
<img src="/static/img/loading.gif" />
</div>
</div>
</div>
</div>
<!-- Modal end -->
{% 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='') %}
@@ -584,31 +728,6 @@ option을 script로 넣을 때 사용
{{ setting_bottom(desc) }}
{% 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 -->
{% macro setting_select(id, title, options, col='9', desc=None, value=None) %}
{{ setting_top(title) }}
@@ -655,21 +794,6 @@ macros.setting_button_with_info([['toggle_btn', 'Toggle', [{'key':'category', 'v
{{ setting_bottom(desc) }}
{% 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 + 버튼 -->
@@ -703,15 +827,6 @@ macros.setting_button_with_info([['toggle_btn', 'Toggle', [{'key':'category', 'v
<!--progress-bar-striped progress-bar-animated-->
{% macro setting_progress(id, left='', desc='') %}
{{ setting_top(left) }}
@@ -725,66 +840,6 @@ macros.setting_button_with_info([['toggle_btn', 'Toggle', [{'key':'category', 'v
{% 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 %}
{% 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) %}
<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') %}
<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>
<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>-->
</div>
</div>
@@ -70,7 +70,7 @@
<div class="modal-footer">
<button type="button" id='select_local_file_modal_confirm_btn' class="btn btn-success" data-dismiss="modal">선택
</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>
</div>
</div>

View File

@@ -19,7 +19,7 @@
{% 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>
{% 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 %}
<!--{{ category }}-->
<li class="nav-item dropdown">
@@ -134,10 +134,11 @@
{% if current_menu[0] == plugin['uri'] and 'list' in plugin %}
{% for module in plugin['list'] %}
{% if module['uri'] == current_menu[1] and 'list' in module%}
<!--{{ module }}-->
<!-- {{ module }} -->
<ul class="nav nav-pills bg-light shadow text-dark">
{% 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>
{% else %}
<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" %}
{% block content %}
{% filter markdown %}
{{ data }}
{% endfilter %}
<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>
<style type="text/css">
img{
<script src="{{ url_for('static', filename='js/showdown_2.1.0.js') }}"></script>
<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">
$(document).ready(function(){
//$('#main_container').attr('class', 'container-fluid');
});
$(document).ready(function(){
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>
{% 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>
<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" />
@@ -63,3 +65,4 @@ player.ready(function(){
player.play();
</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
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
except Exception as exception:
F.logger.debug('Exception:%s', exception)
except Exception as e:
F.logger.debug(f"Exception:{str(e)}")
F.logger.debug(traceback.format_exc())
@@ -60,8 +60,8 @@ class Util(object):
ret['dirname'] = max_filename.replace('/%s' % ret['filename'], '')
ret['max_size'] = max_size
return ret
except Exception as exception:
F.logger.error('Exception:%s', exception)
except Exception as e:
F.logger.error(f"Exception:{str(e)}")
F.logger.error(traceback.format_exc())
@@ -81,8 +81,8 @@ class Util(object):
import shutil
shutil.rmtree(zip_path)
return True
except Exception as exception:
F.logger.error('Exception:%s', exception)
except Exception as e:
F.logger.error(f"Exception:{str(e)}")
F.logger.error(traceback.format_exc())
return False
@@ -92,12 +92,12 @@ class Util(object):
def make_apikey(url):
from framework import SystemModelSetting
url = url.format(ddns=SystemModelSetting.get('ddns'))
if SystemModelSetting.get_bool('auth_use_apikey'):
if SystemModelSetting.get_bool('use_apikey'):
if url.find('?') == -1:
url += '?'
else:
url += '&'
url += 'apikey=%s' % SystemModelSetting.get('auth_apikey')
url += 'apikey=%s' % SystemModelSetting.get('apikey')
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 flask import Blueprint, render_template, jsonify, redirect, request
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.daemon = True
self.download_thread.start()
except Exception as exception:
self.P.logger.error('Exception:%s', exception)
except Exception as e:
self.P.logger.error(f"Exception:{str(e)}")
self.P.logger.error(traceback.format_exc())
@@ -107,8 +107,8 @@ class FfmpegQueue(object):
if self.current_ffmpeg_count < self.max_ffmpeg_count:
break
time.sleep(5)
except Exception as exception:
self.P.logger.error('Exception:%s', exception)
except Exception as e:
self.P.logger.error(f"Exception:{str(e)}")
self.P.logger.error(traceback.format_exc())
self.P.logger.error('current_ffmpeg_count : %s', self.current_ffmpeg_count)
self.P.logger.error('max_ffmpeg_count : %s', self.max_ffmpeg_count)
@@ -153,8 +153,8 @@ class FfmpegQueue(object):
f.start()
self.current_ffmpeg_count += 1
self.download_queue.task_done()
except Exception as exception:
self.P.logger.error('Exception:%s', exception)
except Exception as e:
self.P.logger.error(f"Exception:{str(e)}")
self.P.logger.error(traceback.format_exc())
def ffmpeg_listener(self, **arg):
@@ -203,8 +203,8 @@ class FfmpegQueue(object):
self.entity_list.append(entity)
self.download_queue.put(entity)
return True
except Exception as exception:
self.P.logger.error('Exception:%s', exception)
except Exception as e:
self.P.logger.error(f"Exception:{str(e)}")
self.P.logger.error(traceback.format_exc())
return False
@@ -270,8 +270,8 @@ class FfmpegQueue(object):
self.entity_list = new_list
ret['ret'] = 'refresh'
return ret
except Exception as exception:
self.P.logger.error('Exception:%s', exception)
except Exception as e:
self.P.logger.error(f"Exception:{str(e)}")
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,
get_model_setting)
from loguru import logger as logger1
class PluginBase(object):
package_name = None
@@ -23,6 +25,7 @@ class PluginBase(object):
def __init__(self, setting):
try:
logger1.debug(f"[DEBUG] PluginBase init for {setting.get('filepath')}")
is_system = ('system' == os.path.basename(os.path.dirname(setting['filepath'])))
self.status = ""
self.setting = setting
@@ -39,6 +42,7 @@ class PluginBase(object):
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.menu = setting['menu']
logger1.debug(f"[DEBUG] Menu set for {self.package_name}: {self.menu}")
self.setting_menu = setting.get('setting_menu', None)
self.ModelSetting = None
@@ -52,6 +56,7 @@ class PluginBase(object):
self.home_module = setting.get('home_module')
self.status = "init_success"
self.config = {}
self.recent_menu_plugin_except_list = setting.get('recent_menu_plugin_except_list', [])
except Exception as e:
self.logger.error(f'Exception:{str(e)}')
self.logger.error(traceback.format_exc())
@@ -59,10 +64,16 @@ class PluginBase(object):
def set_module_list(self, mod_list):
try:
# self.module_list = []
for mod in mod_list:
logger1.debug(mod)
mod_ins = mod(self)
# self.logger.debug(mod_ins)
logger1.debug(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:
F.logger.error(f'[{self.package_name}] Exception:{str(e)}')
F.logger.error(traceback.format_exc())
@@ -77,16 +88,31 @@ class PluginBase(object):
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):
self.logic.plugin_unload()
if self.logic:
self.logic.plugin_unload()
def get_first_manual_path(self):
for __ in self.menu['list']:
if __['uri'] == 'manual' and len(__['list']) > 0:
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):
ins = PluginBase(config)

View File

@@ -32,14 +32,39 @@ class Logic(object):
if self.P.ModelSetting is not None:
for module in self.P.module_list:
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)
if module.page_list is not None:
for page_instance in module.page_list:
key = f'{module.name}_{page_instance.name}_auto_start'
if self.P.ModelSetting.has_key(key) and self.P.ModelSetting.get_bool(key):
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):
try:
@@ -88,7 +113,7 @@ class Logic(object):
try:
job_id = '%s_%s' % (self.P.package_name, 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)
except Exception as 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(traceback.format_exc())
def reset_db(self, module_name):
def db_delete(self, module_name, page_name, day):
try:
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:
self.P.logger.error(f'Exception:{str(e)}')
self.P.logger.error(traceback.format_exc())
def one_execute(self, module_name):
self.P.logger.debug('one_execute :%s', module_name)
try:
@@ -166,6 +201,7 @@ class Logic(object):
self.P.logger.error(f'Exception:{str(e)}')
self.P.logger.error(traceback.format_exc())
"""
def process_telegram_data(self, data, target=None):
try:
for module in self.P.module_list:
@@ -174,7 +210,7 @@ class Logic(object):
except Exception as e:
self.P.logger.error(f'Exception:{str(e)}')
self.P.logger.error(traceback.format_exc())
"""
@@ -303,12 +339,23 @@ class Logic(object):
def arg_to_dict(self, arg):
"""
import urllib.parse
tmp = urllib.parse.unquote(arg)
tmps = tmp.split('&')
ret = {}
for tmp in tmps:
_ = tmp.split('=')
_ = tmp.split('=', 1)
ret[_[0]] = _[1]
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):
try:
if self.page_list == None:
return
for page in self.page_list:
if page_name == page.name:
return page
@@ -48,10 +50,22 @@ class PluginModuleBase(object):
def process_menu(self, page, req):
if self.page_list is not None:
page_ins = self.get_page(page)
if page_ins != None:
return page_ins.process_menu(req)
from framework import F
try:
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}")
def process_ajax(self, sub, req):
@@ -69,12 +83,18 @@ class PluginModuleBase(object):
def scheduler_function(self):
pass
def reset_db(self):
pass
def db_delete(self, day):
if self.web_list_model != None:
return self.web_list_model.delete_all(day)
def plugin_load(self):
pass
def plugin_load_celery(self):
pass
def plugin_unload(self):
pass
@@ -115,12 +135,37 @@ class PluginModuleBase(object):
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()
return render_template(f'{self.P.package_name}_{self.parent.name}_{self.name}.html', arg=arg)
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}")
@@ -176,6 +222,9 @@ class PluginPageBase(object):
def plugin_load(self):
pass
def plugin_load_celery(self):
pass
# logic
def plugin_unload(self):
pass
@@ -207,3 +256,41 @@ class PluginPageBase(object):
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
from datetime import datetime, timedelta
@@ -45,7 +46,7 @@ class ModelBase(F.db.Model):
paging['next_page'] = False
paging['current_page'] = current_page
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
except Exception as e:
cls.P.logger.error(f'Exception:{str(e)}')
@@ -89,24 +90,30 @@ class ModelBase(F.db.Model):
return False
@classmethod
def delete_all(cls, days=None):
def delete_all(cls, day=None):
count = -1
try:
with F.app.app_context():
if days == None:
F.db.session.query(cls).delete()
F.db.session.commit()
if day == None or day in [0, '0']:
count = F.db.session.query(cls).delete()
else:
now = datetime.now()
ago = now - timedelta(days=int(days))
ago = now - timedelta(days=int(day))
#ago.hour = 0
#ago.minute = 0
count = F.db.session.query(cls).filter(cls.created_time > ago).delete()
cls.P.logger.info(f"delete_all {days=} {count=}")
return True
count = F.db.session.query(cls).filter(cls.created_time < ago).delete()
cls.P.logger.info(f"delete_all {day=} {count=}")
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:
cls.P.logger.error(f'Exception:{str(e)}')
cls.P.logger.error(traceback.format_exc())
return False
return count
@classmethod
@@ -135,12 +142,12 @@ class ModelBase(F.db.Model):
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}')
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(f'{cls.__tablename__}_last_list_option ERROR!' )
return ret
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())
@@ -149,6 +156,10 @@ class ModelBase(F.db.Model):
def make_query(cls, req, order='desc', search='', option1='all', option2='all'):
with F.app.app_context():
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
@@ -171,3 +182,16 @@ class ModelBase(F.db.Model):
query = query.filter(field.like('%'+search+'%'))
#query = query1.union(query2)
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:
change_list.append(key)
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
F.db.session.commit()
return True, change_list
@@ -117,7 +119,8 @@ def get_model_setting(package_name, logger, table_name=None):
return False, []
@staticmethod
def get_list(key, delimeter='\n', comment=' #'):
def get_list(key, delimeter='\n', comment='#'):
value = None
try:
value = ModelSetting.get(key).replace('\n', delimeter)
if comment is None:

View File

@@ -27,7 +27,12 @@ def default_route(P):
def first_menu(sub):
try:
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:
if sub == module.name:
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 render_template('sample.html', title='%s - %s' % (P.package_name, sub))
except Exception as exception:
P.logger.error('Exception:%s', exception)
except Exception as e:
P.logger.error(f"Exception:{str(e)}")
P.logger.error(traceback.format_exc())
@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('/'))
from support import SupportFile
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)
except Exception as exception:
P.logger.error('Exception:%s', exception)
except Exception as e:
P.logger.error(f"Exception:{str(e)}")
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'])
@login_required
def second_menu(sub, sub2):
if sub2 == 'null':
return
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:
for module in P.module_list:
if sub == module.name:
@@ -74,8 +106,8 @@ def default_route(P):
if sub == 'log':
return render_template('log.html', package=P.package_name)
return render_template('sample.html', title='%s - %s' % (P.package_name, sub))
except Exception as exception:
P.logger.error('Exception:%s', exception)
except Exception as e:
P.logger.error(f"Exception:{str(e)}")
P.logger.error(traceback.format_exc())
#########################################################
@@ -84,7 +116,7 @@ def default_route(P):
@P.blueprint.route('/ajax/<sub>', methods=['GET', 'POST'])
@login_required
def ajax(sub):
P.logger.debug('AJAX %s %s', P.package_name, sub)
#P.logger.debug('AJAX %s %s', P.package_name, sub)
try:
# global
if sub == 'setting_save':
@@ -93,24 +125,26 @@ def default_route(P):
module.setting_save_after(change_list)
return jsonify(ret)
except Exception as exception:
P.logger.error('Exception:%s', exception)
except Exception as e:
P.logger.error(f"Exception:{str(e)}")
P.logger.error(traceback.format_exc())
@P.blueprint.route('/ajax/<module_name>/<cmd>', methods=['GET', 'POST'])
@login_required
def second_ajax(module_name, cmd):
# P.logger.debug(f"[CORE-DEBUG] second_ajax: package={P.package_name}, module={module_name}, cmd={cmd}")
try:
for module in P.module_list:
if cmd == 'scheduler':
go = request.form['scheduler']
if go == 'true':
P.logic.scheduler_start(module_name)
P.logic.scheduler_start(module_name)
else:
P.logic.scheduler_stop(module_name)
P.logic.scheduler_stop(module_name)
return jsonify(go)
elif cmd == 'reset_db':
ret = P.logic.reset_db(module_name)
elif cmd == 'db_delete':
day = request.form['day']
ret = P.logic.db_delete(module_name, None, day)
return jsonify(ret)
elif cmd == 'one_execute':
ret = P.logic.one_execute(module_name)
@@ -118,18 +152,25 @@ def default_route(P):
elif cmd == 'immediately_execute':
ret = P.logic.immediately_execute(module_name)
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 cmd == 'command':
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:
return module.process_ajax(cmd, request)
except Exception as exception:
P.logger.error('Exception:%s', exception)
except Exception as e:
P.logger.error(f"Exception:{str(e)}")
P.logger.error(traceback.format_exc())
@P.blueprint.route('/ajax/<module_name>/<page_name>/<command>', methods=['GET', 'POST'])
@@ -148,10 +189,10 @@ def default_route(P):
else:
P.logic.scheduler_stop_sub(module_name, page_name)
return jsonify(go)
#elif command == 'reset_db':
# sub = request.form['sub']
# ret = P.logic.reset_db(sub)
# return jsonify(ret)
elif command == 'db_delete':
day = request.form['day']
ret = P.logic.db_delete(module_name, page_name, day)
return jsonify(ret)
elif command == 'one_execute':
ret = P.logic.one_execute_sub(module_name, page_name)
return jsonify(ret)
@@ -160,25 +201,30 @@ def default_route(P):
return jsonify(ret)
elif command == 'command':
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:
return ins_page.process_ajax(command, request)
P.logger.error(f"not process ajax : {P.package_name} {module_name} {page_name} {command}")
except Exception as exception:
P.logger.error('Exception:%s', exception)
except Exception as e:
P.logger.error(f"Exception:{str(e)}")
P.logger.error(traceback.format_exc())
#########################################################
# API - 외부
#########################################################
# 단일 모듈인 경우 모듈이름을 붙이기 불편하여 추가.
@P.blueprint.route('/api/<sub2>', methods=['GET', 'POST'])
@P.blueprint.route('/api/<sub>', methods=['GET', 'POST'])
@F.check_api
def api_first(sub2):
def api_first(sub):
try:
for module in P.module_list:
return module.process_api(sub2, request)
except Exception as exception:
P.logger.error('Exception:%s', exception)
return P.module_list[0].process_api(sub, request)
except Exception as e:
P.logger.error(f"Exception:{str(e)}")
P.logger.error(traceback.format_exc())
@P.blueprint.route('/api/<sub>/<sub2>', methods=['GET', 'POST'])
@@ -188,8 +234,16 @@ def default_route(P):
for module in P.module_list:
if sub == module.name:
return module.process_api(sub2, request)
except Exception as exception:
P.logger.error('Exception:%s', exception)
except Exception as e:
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.blueprint.route('/normal/<sub>/<sub2>', methods=['GET', 'POST'])
@@ -198,13 +252,13 @@ def default_route(P):
for module in P.module_list:
if sub == module.name:
return module.process_normal(sub2, request)
except Exception as exception:
P.logger.error('Exception:%s', exception)
except Exception as e:
P.logger.error(f"Exception:{str(e)}")
P.logger.error(traceback.format_exc())
# default_route 끝
@@ -266,18 +320,14 @@ def default_route_single_module(P):
else:
P.logic.scheduler_stop(sub)
return jsonify(go)
elif sub == 'reset_db':
sub = request.form['sub']
ret = P.logic.reset_db(sub)
return jsonify(ret)
elif sub == 'one_execute':
sub = request.form['sub']
ret = P.logic.one_execute(sub)
return jsonify(ret)
else:
return P.module_list[0].process_ajax(sub, request)
except Exception as exception:
P.logger.error('Exception:%s', exception)
except Exception as e:
P.logger.error(f"Exception:{str(e)}")
P.logger.error(traceback.format_exc())
@P.blueprint.route('/api/<sub>', methods=['GET', 'POST'])
@@ -285,16 +335,16 @@ def default_route_single_module(P):
def api(sub):
try:
return P.module_list[0].process_api(sub, request)
except Exception as exception:
P.logger.error('Exception:%s', exception)
except Exception as e:
P.logger.error(f"Exception:{str(e)}")
P.logger.error(traceback.format_exc())
@P.blueprint.route('/normal/<sub>', methods=['GET', 'POST'])
def normal(sub):
try:
return P.module_list[0].process_normal(sub, request)
except Exception as exception:
P.logger.error('Exception:%s', exception)
except Exception as e:
P.logger.error(f"Exception:{str(e)}")
P.logger.error(traceback.format_exc())
@@ -330,14 +380,15 @@ def default_route_socketio_module(module, attach=''):
module.socketio_list = []
@F.socketio.on('connect', namespace=f'/{P.package_name}/{module.name}{attach}')
@F.login_required
def connect():
try:
P.logger.debug(f'socket_connect : {P.package_name} - {module.name}{attach}')
module.socketio_list.append(request.sid)
socketio_callback('start', '')
module.socketio_connect()
except Exception as exception:
P.logger.error('Exception:%s', exception)
except Exception as e:
P.logger.error(f"Exception:{str(e)}")
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}')
module.socketio_list.remove(request.sid)
module.socketio_disconnect()
except Exception as exception:
P.logger.error('Exception:%s', exception)
except Exception as e:
P.logger.error(f"Exception:{str(e)}")
P.logger.error(traceback.format_exc())
@@ -357,7 +408,7 @@ def default_route_socketio_module(module, attach=''):
if encoding:
data = json.dumps(data, cls=AlchemyEncoder)
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
@@ -392,9 +443,10 @@ def default_route_socketio_page(page):
page.socketio_list = []
@F.socketio.on('connect', namespace=f'/{P.package_name}/{module.name}/{page.name}')
@F.login_required
def page_socketio_connect():
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_socketio_callback('start', '')
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}')
def page_socketio_disconnect():
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)
except Exception as e:
P.logger.error(f'Exception:{str(e)}')
@@ -417,6 +469,6 @@ def default_route_socketio_page(page):
if encoding:
data = json.dumps(data, cls=AlchemyEncoder)
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

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
logger = get_logger()
#logger = get_logger()
import logging
logger = logging.getLogger('support')
from .base.aes import SupportAES
from .base.discord import SupportDiscord
from .base.file import SupportFile
from .base.os_command import SupportOSCommand
from .base.string import SupportString
from .base.sub_process import SupportSubprocess
from .base.support_sc import SupportSC
from .base.telegram import SupportTelegram
from .base.slack import SupportSlack
from .base.util import (AlchemyEncoder, SingletonClass, SupportUtil,
default_headers, pt)
from .base.yaml import SupportYaml

View File

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

View File

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

View File

@@ -3,6 +3,7 @@ import json
import os
import re
import traceback
import zipfile
from . import logger
@@ -16,8 +17,8 @@ class SupportFile(object):
data = ifp.read()
ifp.close()
return data
except Exception as exception:
logger.error('Exception:%s', exception)
except Exception as e:
logger.error(f"Exception:{str(e)}")
logger.error(traceback.format_exc())
@classmethod
@@ -27,8 +28,8 @@ class SupportFile(object):
ofp = codecs.open(filename, mode, encoding='utf8')
ofp.write(data)
ofp.close()
except Exception as exception:
logger.error('Exception:%s', exception)
except Exception as e:
logger.error(f"Exception:{str(e)}")
logger.error(traceback.format_exc())
@classmethod
@@ -37,8 +38,8 @@ class SupportFile(object):
with open(filepath, "r", encoding='utf8') as json_file:
data = json.load(json_file)
return data
except Exception as exception:
logger.error('Exception:%s', exception)
except Exception as e:
logger.error(f"Exception:{str(e)}")
logger.error(traceback.format_exc())
@classmethod
@@ -48,8 +49,8 @@ class SupportFile(object):
os.makedirs(os.path.dirname(filepath), exist_ok=True)
with open(filepath, "w", encoding='utf8') as json_file:
json.dump(data, json_file, indent=4, ensure_ascii=False)
except Exception as exception:
logger.error('Exception:%s', exception)
except Exception as e:
logger.error(f"Exception:{str(e)}")
logger.error(traceback.format_exc())
@@ -71,8 +72,8 @@ class SupportFile(object):
response = requests.get(url, headers=headers) # get request
file_is.write(response.content) # write to file
return True
except Exception as exception:
logger.debug('Exception:%s', exception)
except Exception as e:
logger.debug(f"Exception:{str(e)}")
logger.debug(traceback.format_exc())
return False
@@ -82,118 +83,18 @@ class SupportFile(object):
#text = text.replace('/', '')
# 2021-07-31 X:X
#text = text.replace(':', ' ')
text = re.sub('[\\/:*?\"<>|]', ' ', text).strip()
text = re.sub('[\\/:*?\"<>|]', ' ', text).strip()
text = re.sub("\s{2,}", ' ', text)
return text
@classmethod
def size(cls, 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
def unzip(cls, zip_filepath, extract_folderpath):
with zipfile.ZipFile(zip_filepath, 'r') as zip_ref:
zip_ref.extractall(extract_folderpath)
# 파일처리에서 사용. 중복이면 시간값
@classmethod
def file_move(cls, source_path, target_dir, target_filename):
try:
@@ -208,9 +109,160 @@ class SupportFile(object):
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 exception:
logger.debug('Exception:%s', exception)
except Exception as e:
logger.debug(f"Exception:{str(e)}")
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
@@ -234,13 +286,13 @@ class SupportFile(object):
import shutil
shutil.rmtree(zip_path)
return zipfilepath
except Exception as exception:
logger.error('Exception:%s', exception)
except Exception as e:
logger.error(f"Exception:{str(e)}")
logger.error(traceback.format_exc())
return
"""
"""
@classmethod
def rmtree(cls, folderpath):
import shutil
@@ -253,19 +305,9 @@ class SupportFile(object):
return True
except:
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:
with open(filename, 'wb') as f:
f.write(data)
except Exception as exception:
logger.error('Exception:%s', exception)
except Exception as e:
logger.error(f"Exception:{str(e)}")
logger.error(traceback.format_exc())
@@ -361,8 +403,8 @@ class SupportFile(object):
if isinstance(data, bytes):
data = data.decode('utf-8')
return data
except Exception as exception:
logger.error('Exception:%s', exception)
except Exception as e:
logger.error(f"Exception:{str(e)}")
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
import requests
from . import logger
class SupportImage(object):
@classmethod
@@ -20,6 +26,6 @@ class SupportImage(object):
from . import SupportDiscord
return SupportDiscord.discord_proxy_image_bytes(img_byte_arr)
except Exception as e:
logger.error('Exception:%s', e)
logger.error(f"Exception:{str(e)}")
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
@@ -5,22 +8,78 @@ class SupportString(object):
@classmethod
def get_cate_char_by_first(cls, title): # get_first
value = ord(title[0].upper())
if value >= ord('0') and value <= ord('9'): return '0Z'
elif value >= ord('A') and value <= ord('Z'): return '0Z'
elif value >= ord('') and value < ord(''): return ''
elif value < ord(''): return ''
elif value < ord(''): return ''
elif value < ord(''): return ''
elif value < ord(''): return ''
elif value < ord(''): return ''
elif value < ord(''): return ''
elif value < ord(''): return ''
elif value < ord(''): return ''
elif value < ord(''): return ''
elif value < ord(''): return ''
elif value < ord(''): return ''
elif value < ord(''): return ''
elif value <= ord(''): return ''
else: return '0Z'
if ord('') <= value < ord(''): return ''
if ord('') <= value < ord(''): return ''
if ord('') <= value < ord(''): return ''
if ord('') <= value < ord(''): return ''
if ord('') <= value < ord(''): return ''
if ord('') <= value < ord(''): return ''
if ord('') <= value < ord(''): return ''
if ord('') <= value < ord(''): return ''
if ord('') <= value < ord(''): return ''
if ord('') <= value < ord(''): return ''
if ord('') <= value < ord(''): return ''
if ord('') <= value < ord(''): return ''
if ord('') <= value < ord(''): return ''
if ord('') <= value < ord(''): return ''
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 json
import locale
import os
import platform
import queue
@@ -20,7 +21,7 @@ def demote(user_uid, user_gid):
class SupportSubprocess(object):
@classmethod
def command_for_windows(cls, command: list) -> str or list:
def command_for_windows(cls, command: list):
if platform.system() == 'Windows':
tmp = []
if type(command) == type([]):
@@ -43,17 +44,32 @@ class SupportSubprocess(object):
iter_arg = ''
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:
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')
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')
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:
#process.communicate()
process_ret = process.wait(timeout=timeout) # wait for the subprocess to exit
except:
import psutil
@@ -62,14 +78,17 @@ class SupportSubprocess(object):
proc.kill()
process.kill()
new_ret['status'] = "timeout"
ret = []
with process.stdout:
for line in iter(process.stdout.readline, iter_arg):
ret.append(line.strip())
if log:
logger.debug(ret[-1])
#logger.error(process_ret)
thread.join()
#ret = []
#with process.stdout:
# for line in iter(process.stdout.readline, iter_arg):
# ret.append(line.strip())
# if log:
# logger.debug(ret[-1])
ret = result
#logger.error(ret)
if format is None:
ret2 = '\n'.join(ret)
elif format == 'json':
@@ -82,20 +101,29 @@ class SupportSubprocess(object):
break
ret2 = json.loads(''.join(ret[index:]))
except:
ret2 = None
ret2 = ret
new_ret['log'] = ret2
return new_ret
except Exception as exception:
logger.error('Exception:%s', exception)
except Exception as e:
logger.error(f"Exception:{str(e)}")
logger.error(traceback.format_exc())
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 = []
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.print_log = print_log
self.shell = shell
@@ -108,6 +136,7 @@ class SupportSubprocess(object):
self.stdout_queue = None
self.call_id = call_id
self.timestamp = time.time()
self.callback_line = callback_line
def start(self, join=True):
@@ -127,13 +156,15 @@ class SupportSubprocess(object):
self.command = self.command_for_windows(self.command)
logger.debug(f"{self.command=}")
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:
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:
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)
self.send_stdout_callback(self.call_id, 'START', None)
self.__start_communicate()
self.__start_send_callback()
if self.process is not None:
@@ -142,17 +173,18 @@ class SupportSubprocess(object):
self.process_close()
else:
self.process.wait()
self.remove_instance(self)
logger.info(f"{self.command} END")
except Exception as e:
logger.error(f'Exception:{str(e)}')
logger.error(traceback.format_exc())
logger.warning(self.command)
if self.stdout_callback != None:
self.stdout_callback('error', str(e))
self.stdout_callback('error', str(traceback.format_exc()))
self.send_stdout_callback(self.call_id, 'ERROR', str(e))
self.send_stdout_callback(self.call_id, 'ERROR', str(traceback.format_exc()))
finally:
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):
@@ -164,7 +196,11 @@ class SupportSubprocess(object):
def rdr():
while True:
buf = self.process.stdout.read(1)
try:
buf = self.process.stdout.read(1)
except:
continue
#print(buf)
if buf:
_queue.put( buf )
else:
@@ -192,7 +228,9 @@ class SupportSubprocess(object):
if r is not None:
#print(f"{r=}")
self.stdout_queue.put(r)
self.stdout_queue.put('\n')
self.stdout_queue.put('<END>')
self.stdout_queue.put('\n')
for tgt in [rdr, clct]:
th = threading.Thread(target=tgt)
th.setDaemon(True)
@@ -204,16 +242,36 @@ class SupportSubprocess(object):
def func():
while self.stdout_queue:
line = self.stdout_queue.get()
#logger.error(line)
if line == '<END>':
if self.stdout_callback != None:
self.stdout_callback('end', None)
self.send_stdout_callback(self.call_id, 'END', None)
break
else:
if self.stdout_callback != None:
self.stdout_callback('log', line)
self.send_stdout_callback(self.call_id, 'LOG', line)
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.start()
@@ -243,6 +301,15 @@ class SupportSubprocess(object):
self.process.stdin.write(f'{cmd}\n')
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
def all_process_close(cls):
@@ -271,4 +338,7 @@ class SupportSubprocess(object):
for instance in cls.__instance_list:
if instance.call_id == call_id:
return instance
@classmethod
def get_list(cls):
return cls.__instance_list

View File

@@ -1,5 +1,7 @@
import time
import traceback
import requests
from telepot_mod import Bot
from . import logger
@@ -12,11 +14,21 @@ class SupportTelegram:
try:
bot = Bot(bot_token)
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)
return True
except Exception as exception:
logger.error('Exception:%s', exception)
except Exception as e:
logger.error(f"Exception:{str(e)}")
logger.error(traceback.format_exc())
logger.debug('Chatid:%s', chat_id)
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 functools import wraps
import time
def pt(f):
@wraps(f)
def wrapper(*args, **kwds):
@@ -26,9 +29,9 @@ class SupportUtil(object):
def sizeof_fmt(cls, num, suffix='Bytes'):
for unit in ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z']:
if abs(num) < 1024.0:
return "%3.1f%s%s" % (num, unit, suffix)
return "%3.1f %s%s" % (num, unit, suffix)
num /= 1024.0
return "%.1f%s%s" % (num, 'Y', suffix)
return "%.1f %s%s" % (num, 'Y', suffix)
@classmethod
def is_arm(cls):

View File

@@ -1,5 +1,11 @@
import os
import traceback
import yaml
from . import logger
class SupportYaml(object):
@classmethod
def write_yaml(cls, filepath, data):
@@ -11,3 +17,41 @@ class SupportYaml(object):
with open(filepath, encoding='utf8') as file:
data = yaml.load(file, Loader=yaml.FullLoader)
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
if self.proxy 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:
headers_command = []
tmp = ""
@@ -136,9 +140,15 @@ class SupportFfmpeg(object):
if len(tmp) > 0:
headers_command.append('-headers')
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:
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':
@@ -159,7 +169,7 @@ class SupportFfmpeg(object):
return
except:
pass
#logger.error(' '.join(command))
logger.error(' '.join(command))
command = SupportSubprocess.command_for_windows(command)
if platform.system() == 'Windows' and header_count > 1:
@@ -216,8 +226,8 @@ SET CRLF=^
else:
if os.path.exists(self.temp_fullpath):
os.remove(self.temp_fullpath)
except Exception as exception:
logger.error('Exception:%s', exception)
except Exception as e:
logger.error(f"Exception:{str(e)}")
logger.error(traceback.format_exc())
arg = {'type':'last', 'status':self.status, 'data' : self.get_data()}
@@ -347,6 +357,7 @@ SET CRLF=^
return data
def send_to_listener(self, **arg):
print(arg)
if self.total_callback_function != None:
self.total_callback_function(**arg)
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
except gspread.exceptions.APIError:
self.sleep_exception()
except Exception as exception:
except Exception as e:
logger.error(f"{key} - {value}")
logger.error('Exception:%s', exception)
logger.error(f"Exception:{str(e)}")
logger.error(traceback.format_exc())
logger.error(self.header_info)
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