Compare commits

..

10 Commits

Author SHA1 Message Date
1167e60c36 Update repository URL to Gitea and sync structural changes 2026-01-05 15:57:24 +09:00
joyfuI
51ef1ee459 다운로드 요청 오류 수정 2022-08-07 14:50:55 +09:00
joyfuI
3410ddd73c download, thumbnail, sub API에 headers 키 추가 2022-07-24 21:56:46 +09:00
joyfuI
26e4f96d49 자바스크립트 리펙토링 2022-07-24 19:48:33 +09:00
joyfuI
3ef17ad0b3 최신 플러그인 구조로 변경 2022-07-10 23:22:28 +09:00
joyfuI
44b792bc28 js 캐시 문제 해결 2022-06-27 23:57:01 +09:00
joyfuI
3d1e12d081 디폴트 youtube_dl 패키지를 yt-dlp로 변경 2022-05-05 22:04:46 +09:00
joyfuI
8cd3c769b0 과도한 로그 방지 2022-05-05 21:59:24 +09:00
joyfuI
28dcded7ce Unknown format code 'd' for object of type 'float' 오류 수정 2022-05-05 21:43:57 +09:00
joyfuI
c74ad4bc78 파일 인코딩 문제 수정 2022-05-05 21:34:12 +09:00
24 changed files with 1019 additions and 1134 deletions

View File

@@ -15,7 +15,7 @@ SJVA에서 "시스템 → 플러그인 → 플러그인 수동 설치" 칸에
API를 제공합니다. 다른 플러그인에서 동영상 정보나 다운로드를 요청할 수 있습니다. API를 제공합니다. 다른 플러그인에서 동영상 정보나 다운로드를 요청할 수 있습니다.
다른 플러그인이 멋대로 다운로드를 중지할 수 없도록 다운로드를 요청할 때 임의의 키를 넘겨받습니다. 중지 요청 시 키가 일치해야 요청이 실행됩니다. 다른 플러그인이 멋대로 다운로드를 중지할 수 없도록 다운로드를 요청할 때 임의의 키를 넘겨받습니다. 중지 요청 시 키가 일치해야 요청이 실행됩니다.
[youtube-dl](https://github.com/ytdl-org/youtube-dl)의 DMCA 테이크다운 사태 이후, 비슷한 상황을 대비하기 위해 youtube-dl의 포크 프로젝트 [youtube-dlc](https://github.com/blackjack4494/yt-dlc)의 포크 프로젝트(...)인 [yt-dlp](https://github.com/pukkandan/yt-dlp)를 추가했습니다. [youtube-dl](https://github.com/ytdl-org/youtube-dl)의 DMCA 테이크다운 사태 이후, 비슷한 상황을 대비하기 위해 youtube-dl의 포크 프로젝트 [youtube-dlc](https://github.com/blackjack4494/yt-dlc)의 포크 프로젝트(...)인 [yt-dlp](https://github.com/yt-dlp/yt-dlp)를 추가했습니다.
설정에서 취향껏 골라서 사용하시면 되며 youtube-dl가 우선 지원됩니다. 설정에서 취향껏 골라서 사용하시면 되며 youtube-dl가 우선 지원됩니다.
## API ## API
@@ -93,8 +93,10 @@ API를 제공합니다. 다른 플러그인에서 동영상 정보나 다운로
| `archive` | 다운로드한 동영상의 ID를 기록할 파일 경로. 파일이 이미 있으면 이미 다운로드한 동영상은 다운로드 하지 않음. 미지정 시 기록하지 않음 | X | String | | `archive` | 다운로드한 동영상의 ID를 기록할 파일 경로. 파일이 이미 있으면 이미 다운로드한 동영상은 다운로드 하지 않음. 미지정 시 기록하지 않음 | X | String |
| `start` | 다운로드 준비 후 바로 다운로드를 시작할지 여부. 기본값: `false` | X | Boolean | | `start` | 다운로드 준비 후 바로 다운로드를 시작할지 여부. 기본값: `false` | X | Boolean |
| `cookiefile` | 다운로드 시 필요한 쿠키 파일 경로 | X | String | | `cookiefile` | 다운로드 시 필요한 쿠키 파일 경로 | X | String |
| `headers` | 다운로드 시 사용할 HTTP 헤더 정보 | X | String |
`dateafter` 키에 넣을 수 있는 날짜는 `"YYYYMMDD"` 또는 `"(now|today)[+-][0-9](day|week|month|year)(s)?"` 형식의 문자열입니다. `dateafter` 키에 넣을 수 있는 날짜는 `"YYYYMMDD"` 또는 `"(now|today)[+-][0-9](day|week|month|year)(s)?"` 형식의 문자열입니다.
`headers` 키에 넣는 값은 `json` 형식의 문자열입니다.
`playlist` 키에 넣을 수 있는 값은 `"1-3,7,10-13"` 형식의 범위 또는 `"reverse"`, `"random"`입니다. `playlist` 키에 넣을 수 있는 값은 `"1-3,7,10-13"` 형식의 범위 또는 `"reverse"`, `"random"`입니다.
범위를 넣으면 플레이리스트에서 선택한 것만 다운로드합니다. 범위를 넣으면 플레이리스트에서 선택한 것만 다운로드합니다.
@@ -127,8 +129,10 @@ API를 제공합니다. 다른 플러그인에서 동영상 정보나 다운로
| `archive` | 다운로드한 동영상의 ID를 기록할 파일 경로. 파일이 이미 있으면 이미 다운로드한 동영상은 다운로드 하지 않음. 미지정 시 기록하지 않음 | X | String | | `archive` | 다운로드한 동영상의 ID를 기록할 파일 경로. 파일이 이미 있으면 이미 다운로드한 동영상은 다운로드 하지 않음. 미지정 시 기록하지 않음 | X | String |
| `start` | 다운로드 준비 후 바로 다운로드를 시작할지 여부. 기본값: `false` | X | Boolean | | `start` | 다운로드 준비 후 바로 다운로드를 시작할지 여부. 기본값: `false` | X | Boolean |
| `cookiefile` | 다운로드 시 필요한 쿠키 파일 경로 | X | String | | `cookiefile` | 다운로드 시 필요한 쿠키 파일 경로 | X | String |
| `headers` | 다운로드 시 사용할 HTTP 헤더 정보 | X | String |
`dateafter` 키에 넣을 수 있는 날짜는 `"YYYYMMDD"` 또는 `"(now|today)[+-][0-9](day|week|month|year)(s)?"` 형식의 문자열입니다. `dateafter` 키에 넣을 수 있는 날짜는 `"YYYYMMDD"` 또는 `"(now|today)[+-][0-9](day|week|month|year)(s)?"` 형식의 문자열입니다.
`headers` 키에 넣는 값은 `json` 형식의 문자열입니다.
`playlist` 키에 넣을 수 있는 값은 `"1-3,7,10-13"` 형식의 범위 또는 `"reverse"`, `"random"`입니다. `playlist` 키에 넣을 수 있는 값은 `"1-3,7,10-13"` 형식의 범위 또는 `"reverse"`, `"random"`입니다.
범위를 넣으면 플레이리스트에서 선택한 것만 다운로드합니다. 범위를 넣으면 플레이리스트에서 선택한 것만 다운로드합니다.
@@ -163,8 +167,10 @@ API를 제공합니다. 다른 플러그인에서 동영상 정보나 다운로
| `archive` | 다운로드한 동영상의 ID를 기록할 파일 경로. 파일이 이미 있으면 이미 다운로드한 동영상은 다운로드 하지 않음. 미지정 시 기록하지 않음 | X | String | | `archive` | 다운로드한 동영상의 ID를 기록할 파일 경로. 파일이 이미 있으면 이미 다운로드한 동영상은 다운로드 하지 않음. 미지정 시 기록하지 않음 | X | String |
| `start` | 다운로드 준비 후 바로 다운로드를 시작할지 여부. 기본값: `false` | X | Boolean | | `start` | 다운로드 준비 후 바로 다운로드를 시작할지 여부. 기본값: `false` | X | Boolean |
| `cookiefile` | 다운로드 시 필요한 쿠키 파일 경로 | X | String | | `cookiefile` | 다운로드 시 필요한 쿠키 파일 경로 | X | String |
| `headers` | 다운로드 시 사용할 HTTP 헤더 정보 | X | String |
`dateafter` 키에 넣을 수 있는 날짜는 `"YYYYMMDD"` 또는 `"(now|today)[+-][0-9](day|week|month|year)(s)?"` 형식의 문자열입니다. `dateafter` 키에 넣을 수 있는 날짜는 `"YYYYMMDD"` 또는 `"(now|today)[+-][0-9](day|week|month|year)(s)?"` 형식의 문자열입니다.
`headers` 키에 넣는 값은 `json` 형식의 문자열입니다.
`playlist` 키에 넣을 수 있는 값은 `"1-3,7,10-13"` 형식의 범위 또는 `"reverse"`, `"random"`입니다. `playlist` 키에 넣을 수 있는 값은 `"1-3,7,10-13"` 형식의 범위 또는 `"reverse"`, `"random"`입니다.
범위를 넣으면 플레이리스트에서 선택한 것만 다운로드합니다. 범위를 넣으면 플레이리스트에서 선택한 것만 다운로드합니다.
@@ -245,6 +251,20 @@ API를 제공합니다. 다른 플러그인에서 동영상 정보나 다운로
## Changelog ## Changelog
v4.0.1
- 다운로드 요청 오류 수정
v4.0.0
- 최신 플러그인 구조로 변경
- download, thumbnail, sub API에 headers 키 추가
http 헤더 수정이 필요한 경우 활용할 수 있습니다.
v3.1.1
- 디폴트 youtube_dl 패키지를 yt-dlp로 변경
v3.1.0 v3.1.0
- yt-dlp일 때 작동 안하는 문제 수정 - yt-dlp일 때 작동 안하는 문제 수정

View File

@@ -1 +0,0 @@
from .plugin import blueprint, menu, plugin_info, plugin_load, plugin_unload

8
abort.py Normal file
View File

@@ -0,0 +1,8 @@
from flask import jsonify
class LogicAbort:
@staticmethod
def abort(base, code):
base["errorCode"] = code
return jsonify(base)

View File

@@ -1,10 +0,0 @@
{
"version": "3.1.0",
"name": "youtube-dl",
"category_name": "vod",
"developer": "joyfuI",
"description": "유튜브, 네이버TV 등 동영상 사이트에서 동영상 다운로드",
"home": "https://github.com/joyfuI/youtube-dl",
"more": "",
"category": "vod"
}

7
info.yaml Normal file
View File

@@ -0,0 +1,7 @@
title: "유튜브 다운로더"
version: "0.1.0"
package_name: "youtube-dl"
developer: "flaskfarm"
description: "유튜브 다운로드"
home: "https://gitea.yommi.duckdns.org/ff_plugins/youtube-dl"
more: "https://gitea.yommi.duckdns.org/ff_plugins/youtube-dl/raw/branch/main/README.md"

103
logic.py
View File

@@ -1,103 +0,0 @@
import os
import sys
import platform
import traceback
import subprocess
import sqlite3
from framework import db, path_app_root, path_data
from framework.logger import get_logger
from framework.util import Util
from .logic_normal import LogicNormal
from .model import ModelSetting
package_name = __name__.split(".", maxsplit=1)[0]
logger = get_logger(package_name)
class Logic(object):
db_default = {
"db_version": "2",
"youtube_dl_package": "0",
"ffmpeg_path": ""
if platform.system() != "Windows"
else os.path.join(path_app_root, "bin", "Windows", "ffmpeg.exe"),
"temp_path": os.path.join(path_data, "download_tmp"),
"save_path": os.path.join(path_data, "download"),
"default_filename": "",
"proxy": "",
}
@staticmethod
def db_init():
try:
for key, value in Logic.db_default.items():
if db.session.query(ModelSetting).filter_by(key=key).count() == 0:
db.session.add(ModelSetting(key, value))
db.session.commit()
Logic.migration()
except Exception as e:
logger.error("Exception:%s", e)
logger.error(traceback.format_exc())
@staticmethod
def plugin_load():
try:
logger.debug("%s plugin_load", package_name)
Logic.db_init()
# youtube-dl 업데이트
youtube_dl = LogicNormal.get_youtube_dl_package(
ModelSetting.get("youtube_dl_package")
)
logger.debug(f"{youtube_dl} upgrade")
logger.debug(
subprocess.check_output(
[sys.executable, "-m", "pip", "install", "--upgrade", youtube_dl],
universal_newlines=True,
)
)
# 편의를 위해 json 파일 생성
from .plugin import plugin_info
Util.save_from_dict_to_json(
plugin_info, os.path.join(os.path.dirname(__file__), "info.json")
)
except Exception as e:
logger.error("Exception:%s", e)
logger.error(traceback.format_exc())
@staticmethod
def plugin_unload():
try:
logger.debug("%s plugin_unload", package_name)
except Exception as e:
logger.error("Exception:%s", e)
logger.error(traceback.format_exc())
@staticmethod
def migration():
try:
db_version = ModelSetting.get_int("db_version")
connect = sqlite3.connect(
os.path.join(path_data, "db", f"{package_name}.db")
)
if db_version < 2:
logger.debug("youtube-dlc uninstall")
logger.debug(
subprocess.check_output(
[sys.executable, "-m", "pip", "uninstall", "-y", "youtube-dlc"],
universal_newlines=True,
)
)
connect.commit()
connect.close()
ModelSetting.set("db_version", Logic.db_default["db_version"])
db.session.flush()
except Exception as e:
logger.error("Exception:%s", e)
logger.error(traceback.format_exc())

View File

@@ -1,28 +1,226 @@
import os
import sys
import platform
import traceback import traceback
import subprocess
import sqlite3
from datetime import datetime from datetime import datetime
from flask import jsonify from flask import render_template, jsonify
from framework.logger import get_logger
from .my_youtube_dl import MyYoutubeDL, Status from .my_youtube_dl import MyYoutubeDL, Status
package_name = __name__.split(".", maxsplit=1)[0] logger = P.logger
logger = get_logger(package_name) package_name = P.package_name
ModelSetting = P.ModelSetting
class LogicNormal(object): class LogicMain(LogicModuleBase):
db_default = {
"db_version": "2",
"youtube_dl_package": "1",
"ffmpeg_path": ""
if platform.system() != "Windows"
else os.path.join(path_app_root, "bin", "Windows", "ffmpeg.exe"),
"temp_path": os.path.join(path_data, "download_tmp"),
"save_path": os.path.join(path_data, "download"),
"default_filename": "",
"proxy": "",
}
def __init__(self, plugin):
super(LogicMain, self).__init__(plugin, None)
self.name = package_name # 모듈명
default_route_socketio(plugin, self)
def plugin_load(self):
try:
# youtube-dl 업데이트
youtube_dl = Plugin.youtube_dl_packages[
int(ModelSetting.get("youtube_dl_package"))
]
logger.debug(f"{youtube_dl} upgrade")
logger.debug(
subprocess.check_output(
[sys.executable, "-m", "pip", "install", "--upgrade", youtube_dl],
universal_newlines=True,
)
)
except Exception as error:
logger.error("Exception:%s", error)
logger.error(traceback.format_exc())
def process_menu(self, sub, req):
try:
arg = {
"package_name": package_name,
"sub": sub,
"template_name": f"{package_name}_{sub}",
"package_version": Plugin.plugin_info["version"],
}
if sub == "setting":
arg.update(ModelSetting.to_dict())
arg["package_list"] = Plugin.youtube_dl_packages
arg["youtube_dl_version"] = LogicMain.get_youtube_dl_version()
arg["DEFAULT_FILENAME"] = LogicMain.get_default_filename()
elif sub == "download":
default_filename = ModelSetting.get("default_filename")
arg["filename"] = (
default_filename
if default_filename
else LogicMain.get_default_filename()
)
arg["preset_list"] = LogicMain.get_preset_list()
arg["postprocessor_list"] = LogicMain.get_postprocessor_list()
elif sub == "thumbnail":
default_filename = ModelSetting.get("default_filename")
arg["filename"] = (
default_filename
if default_filename
else LogicMain.get_default_filename()
)
elif sub == "sub":
default_filename = ModelSetting.get("default_filename")
arg["filename"] = (
default_filename
if default_filename
else LogicMain.get_default_filename()
)
elif sub == "list":
pass
return render_template(f"{package_name}_{sub}.html", arg=arg)
except Exception as error:
logger.error("Exception:%s", error)
logger.error(traceback.format_exc())
return render_template("sample.html", title=f"{package_name} - {sub}")
def process_ajax(self, sub, req):
try:
logger.debug("AJAX: %s, %s", sub, req.values)
ret = {"ret": "success"}
if sub == "ffmpeg_version":
path = req.form["path"]
output = subprocess.check_output([path, "-version"])
output = output.decode().replace("\n", "<br>")
ret["data"] = output
elif sub == "download":
postprocessor = req.form["postprocessor"]
video_convertor, extract_audio = LogicMain.get_postprocessor()
preferedformat = None
preferredcodec = None
preferredquality = None
if postprocessor in video_convertor:
preferedformat = postprocessor
elif postprocessor in extract_audio:
preferredcodec = postprocessor
preferredquality = 192
youtube_dl = LogicMain.download(
plugin=package_name,
url=req.form["url"],
filename=req.form["filename"],
temp_path=ModelSetting.get("temp_path"),
save_path=ModelSetting.get("save_path"),
format=req.form["format"],
preferedformat=preferedformat,
preferredcodec=preferredcodec,
preferredquality=preferredquality,
proxy=ModelSetting.get("proxy"),
ffmpeg_path=ModelSetting.get("ffmpeg_path"),
)
youtube_dl.start()
LogicMain.socketio_emit("add", youtube_dl)
ret["ret"] = "info"
ret["msg"] = "분석중..."
elif sub == "thumbnail":
youtube_dl = LogicMain.thumbnail(
plugin=package_name,
url=req.form["url"],
filename=req.form["filename"],
temp_path=ModelSetting.get("temp_path"),
save_path=ModelSetting.get("save_path"),
all_thumbnails=req.form["all_thumbnails"],
proxy=ModelSetting.get("proxy"),
ffmpeg_path=ModelSetting.get("ffmpeg_path"),
)
youtube_dl.start()
LogicMain.socketio_emit("add", youtube_dl)
ret["ret"] = "info"
ret["msg"] = "분석중..."
elif sub == "sub":
youtube_dl = LogicMain.sub(
plugin=package_name,
url=req.form["url"],
filename=req.form["filename"],
temp_path=ModelSetting.get("temp_path"),
save_path=ModelSetting.get("save_path"),
all_subs=req.form["all_subs"],
sub_lang=req.form["sub_lang"],
auto_sub=req.form["auto_sub"],
proxy=ModelSetting.get("proxy"),
ffmpeg_path=ModelSetting.get("ffmpeg_path"),
)
youtube_dl.start()
LogicMain.socketio_emit("add", youtube_dl)
ret["ret"] = "info"
ret["msg"] = "분석중..."
elif sub == "list":
ret["data"] = []
for i in LogicMain.youtube_dl_list:
data = LogicMain.get_data(i)
if data is not None:
ret["data"].append(data)
elif sub == "all_stop":
for i in LogicMain.youtube_dl_list:
i.stop()
elif sub == "stop":
index = int(req.form["index"])
LogicMain.youtube_dl_list[index].stop()
return jsonify(ret)
except Exception as error:
logger.error("Exception:%s", error)
logger.error(traceback.format_exc())
return jsonify({"ret": "danger", "msg": str(error)})
def migration(self):
try:
db_version = ModelSetting.get_int("db_version")
connect = sqlite3.connect(
os.path.join(path_data, "db", f"{package_name}.db")
)
if db_version < 2:
logger.debug("youtube-dlc uninstall")
logger.debug(
subprocess.check_output(
[sys.executable, "-m", "pip", "uninstall", "-y", "youtube-dlc"],
universal_newlines=True,
)
)
connect.commit()
connect.close()
ModelSetting.set("db_version", LogicMain.db_default["db_version"])
db.session.flush()
except Exception as error:
logger.error("Exception:%s", error)
logger.error(traceback.format_exc())
youtube_dl_list = [] youtube_dl_list = []
@staticmethod
def get_youtube_dl_package(index=None, import_pkg=False):
packages = ["youtube-dl", "yt-dlp"]
import_name = ["youtube_dl", "yt_dlp"]
if import_pkg:
return import_name if index is None else import_name[int(index)]
else:
return packages if index is None else packages[int(index)]
@staticmethod @staticmethod
def get_youtube_dl_version(): def get_youtube_dl_version():
try: try:
@@ -79,7 +277,7 @@ class LogicNormal(object):
def get_postprocessor(): def get_postprocessor():
video_convertor = [] video_convertor = []
extract_audio = [] extract_audio = []
for i in LogicNormal.get_postprocessor_list(): for i in LogicMain.get_postprocessor_list():
if i[2] == "비디오 변환": if i[2] == "비디오 변환":
video_convertor.append(i[0]) video_convertor.append(i[0])
elif i[2] == "오디오 추출": elif i[2] == "오디오 추출":
@@ -131,12 +329,14 @@ class LogicNormal(object):
opts["ffmpeg_location"] = kwagrs["ffmpeg_path"] opts["ffmpeg_location"] = kwagrs["ffmpeg_path"]
if "cookiefile" in kwagrs and kwagrs["cookiefile"]: if "cookiefile" in kwagrs and kwagrs["cookiefile"]:
opts["cookiefile"] = kwagrs["cookiefile"] opts["cookiefile"] = kwagrs["cookiefile"]
if "headers" in kwagrs and kwagrs["headers"]:
opts["http_headers"] = kwagrs["headers"]
dateafter = kwagrs.get("dateafter") dateafter = kwagrs.get("dateafter")
youtube_dl = MyYoutubeDL( youtube_dl = MyYoutubeDL(
plugin, "video", url, filename, temp_path, save_path, opts, dateafter plugin, "video", url, filename, temp_path, save_path, opts, dateafter
) )
youtube_dl.key = kwagrs.get("key") youtube_dl.key = kwagrs.get("key")
LogicNormal.youtube_dl_list.append(youtube_dl) # 리스트 추가 LogicMain.youtube_dl_list.append(youtube_dl) # 리스트 추가
return youtube_dl return youtube_dl
except Exception as error: except Exception as error:
logger.error("Exception:%s", error) logger.error("Exception:%s", error)
@@ -175,6 +375,8 @@ class LogicNormal(object):
opts["ffmpeg_location"] = kwagrs["ffmpeg_path"] opts["ffmpeg_location"] = kwagrs["ffmpeg_path"]
if "cookiefile" in kwagrs and kwagrs["cookiefile"]: if "cookiefile" in kwagrs and kwagrs["cookiefile"]:
opts["cookiefile"] = kwagrs["cookiefile"] opts["cookiefile"] = kwagrs["cookiefile"]
if "headers" in kwagrs and kwagrs["headers"]:
opts["http_headers"] = kwagrs["headers"]
dateafter = kwagrs.get("dateafter") dateafter = kwagrs.get("dateafter")
youtube_dl = MyYoutubeDL( youtube_dl = MyYoutubeDL(
plugin, plugin,
@@ -187,7 +389,7 @@ class LogicNormal(object):
dateafter, dateafter,
) )
youtube_dl.key = kwagrs.get("key") youtube_dl.key = kwagrs.get("key")
LogicNormal.youtube_dl_list.append(youtube_dl) # 리스트 추가 LogicMain.youtube_dl_list.append(youtube_dl) # 리스트 추가
return youtube_dl return youtube_dl
except Exception as error: except Exception as error:
logger.error("Exception:%s", error) logger.error("Exception:%s", error)
@@ -230,12 +432,14 @@ class LogicNormal(object):
opts["ffmpeg_location"] = kwagrs["ffmpeg_path"] opts["ffmpeg_location"] = kwagrs["ffmpeg_path"]
if "cookiefile" in kwagrs and kwagrs["cookiefile"]: if "cookiefile" in kwagrs and kwagrs["cookiefile"]:
opts["cookiefile"] = kwagrs["cookiefile"] opts["cookiefile"] = kwagrs["cookiefile"]
if "headers" in kwagrs and kwagrs["headers"]:
opts["http_headers"] = kwagrs["headers"]
dateafter = kwagrs.get("dateafter") dateafter = kwagrs.get("dateafter")
youtube_dl = MyYoutubeDL( youtube_dl = MyYoutubeDL(
plugin, "subtitle", url, filename, temp_path, save_path, opts, dateafter plugin, "subtitle", url, filename, temp_path, save_path, opts, dateafter
) )
youtube_dl.key = kwagrs.get("key") youtube_dl.key = kwagrs.get("key")
LogicNormal.youtube_dl_list.append(youtube_dl) # 리스트 추가 LogicMain.youtube_dl_list.append(youtube_dl) # 리스트 추가
return youtube_dl return youtube_dl
except Exception as error: except Exception as error:
logger.error("Exception:%s", error) logger.error("Exception:%s", error)
@@ -284,9 +488,7 @@ class LogicNormal(object):
else "" else ""
) )
data["speed_str"] = ( data["speed_str"] = (
LogicNormal.human_readable_size( LogicMain.human_readable_size(youtube_dl.progress_hooks["speed"], "/s")
youtube_dl.progress_hooks["speed"], "/s"
)
if youtube_dl.progress_hooks["speed"] is not None if youtube_dl.progress_hooks["speed"] is not None
else "" else ""
) )
@@ -303,19 +505,19 @@ class LogicNormal(object):
youtube_dl.progress_hooks["downloaded_bytes"], youtube_dl.progress_hooks["downloaded_bytes"],
youtube_dl.progress_hooks["total_bytes"], youtube_dl.progress_hooks["total_bytes"],
): # 둘 다 값이 있으면 ): # 둘 다 값이 있으면
data["downloaded_bytes_str"] = LogicNormal.human_readable_size( data["downloaded_bytes_str"] = LogicMain.human_readable_size(
youtube_dl.progress_hooks["downloaded_bytes"] youtube_dl.progress_hooks["downloaded_bytes"]
) )
data["total_bytes_str"] = LogicNormal.human_readable_size( data["total_bytes_str"] = LogicMain.human_readable_size(
youtube_dl.progress_hooks["total_bytes"] youtube_dl.progress_hooks["total_bytes"]
) )
data[ data[
"percent" "percent"
] = f"{float(youtube_dl.progress_hooks['downloaded_bytes']) / float(youtube_dl.progress_hooks['total_bytes']) * 100:.2f}" ] = f"{(float(youtube_dl.progress_hooks['downloaded_bytes']) / float(youtube_dl.progress_hooks['total_bytes']) * 100):.2f}"
data["start_time"] = youtube_dl.start_time.strftime("%m-%d %H:%M:%S") data["start_time"] = youtube_dl.start_time.strftime("%m-%d %H:%M:%S")
data[ data[
"download_time" "download_time"
] = f"{download_time.seconds / 60:02f}:{download_time.seconds % 60:02f}" ] = f"{int(download_time.seconds / 60):02d}:{int(download_time.seconds % 60):02d}"
return data return data
except Exception as error: except Exception as error:
logger.error("Exception:%s", error) logger.error("Exception:%s", error)
@@ -335,6 +537,7 @@ class LogicNormal(object):
return f"{size:.1f} YB{suffix}" return f"{size:.1f} YB{suffix}"
@staticmethod @staticmethod
def abort(base, code): def socketio_emit(cmd, data):
base["errorCode"] = code socketio.emit(
return jsonify(base) cmd, LogicMain.get_data(data), namespace=f"/{package_name}", broadcast=True
)

118
mod_basic.py Normal file
View File

@@ -0,0 +1,118 @@
# -*- coding: utf-8 -*-
# @Time : 2023/02/25 7:29 PM
# @Author : yommi
# @Site :
# @File : mod_basic
# @Software: PyCharm
# @Path : youtube-dl/mod_basic.py
from support import SupportYaml
from tool import ToolUtil
from .setup import *
# from .main import LogicMain
from .my_youtube_dl import MyYoutubeDL, Status
import platform
import os
from .model import ModelYoutubeDlItem
from loguru import logger
class ModuleBasic(PluginModuleBase):
def __init__(self, P):
super(ModuleBasic, self).__init__(
P, name="basic", first_menu="setting", scheduler_desc="유튜브 다운로더"
)
self.db_default = {
"db_version": "2",
"youtube_dl_package": "1",
"ffmpeg_path": ""
if platform.system() != "Windows"
else os.path.join("PATH_APP_ROOT", "bin", "Windows", "ffmpeg.exe"),
"temp_path": os.path.join("{PATH_DATA}", "download_tmp"),
"save_path": os.path.join("{PATH_DATA}", "download"),
"default_filename": "",
"proxy": "",
}
self.web_list_model = ModelYoutubeDlItem
def process_menu(self, sub, req):
logger.debug(f"sub: {sub}")
arg = P.ModelSetting.to_dict()
logger.debug(f"arg:: {arg}")
if sub == "setting":
arg["is_include"] = F.scheduler.is_include(self.get_scheduler_name())
arg["is_running"] = F.scheduler.is_running(self.get_scheduler_name())
elif sub == "download":
default_filename = P.ModelSetting.get("default_filename")
arg["filename"] = (
default_filename
if default_filename
else ModuleBasic.get_default_filename()
)
arg["preset_list"] = ModuleBasic.get_preset_list()
arg["postprocessor_list"] = ModuleBasic.get_postprocessor_list()
return render_template(f"{P.package_name}_{self.name}_{sub}.html", arg=arg)
def process_command(self, command, arg1, arg2, arg3, req):
ret = {"ret": "success"}
return jsonify(ret)
# def plugin_load(self):
# if (
# os.path.exists(
# ToolUtil.make_path(P.ModelSetting.get(f"{self.name}_path_config"))
# )
# is False
# ):
# shutil.copyfile(
# os.path.join(
# os.path.dirname(__file__), "files", f"config_{self.name}.yaml"
# ),
# ToolUtil.make_path(P.ModelSetting.get(f"{self.name}_path_config")),
# )
@staticmethod
def get_default_filename():
return MyYoutubeDL.DEFAULT_FILENAME
@staticmethod
def get_preset_list():
return [
["bestvideo+bestaudio/best", "최고 화질"],
["bestvideo[height<=1080]+bestaudio/best[height<=1080]", "1080p"],
["worstvideo+worstaudio/worst", "최저 화질"],
["bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]", "최고 화질(mp4)"],
[
"bestvideo[ext=mp4][height<=1080]+bestaudio[ext=m4a]/best[ext=mp4][height<=1080]",
"1080p(mp4)",
],
["bestvideo[filesize<50M]+bestaudio/best[filesize<50M]", "50MB 미만"],
["bestaudio/best", "오디오만"],
["_custom", "사용자 정의"],
]
@staticmethod
def get_postprocessor_list():
return [
["", "후처리 안함", None],
["mp4", "MP4", "비디오 변환"],
["flv", "FLV", "비디오 변환"],
["webm", "WebM", "비디오 변환"],
["ogg", "Ogg", "비디오 변환"],
["mkv", "MKV", "비디오 변환"],
["ts", "TS", "비디오 변환"],
["avi", "AVI", "비디오 변환"],
["wmv", "WMV", "비디오 변환"],
["mov", "MOV", "비디오 변환"],
["gif", "GIF", "비디오 변환"],
["mp3", "MP3", "오디오 추출"],
["aac", "AAC", "오디오 추출"],
["flac", "FLAC", "오디오 추출"],
["m4a", "M4A", "오디오 추출"],
["opus", "Opus", "오디오 추출"],
["vorbis", "Vorbis", "오디오 추출"],
["wav", "WAV", "오디오 추출"],
]

125
model.py
View File

@@ -1,119 +1,18 @@
import os # -*- coding: utf-8 -*-
import traceback # @Time : 2023/02/25 7:55 PM
# @Author : yommi
# @Site :
# @File : model
# @Software: PyCharm
# @Path : youtube-dl/model.py
from framework import app, db, path_data from .setup import *
from framework.logger import get_logger
from framework.util import Util
package_name = __name__.split(".", maxsplit=1)[0]
logger = get_logger(package_name)
app.config["SQLALCHEMY_BINDS"][package_name] = "sqlite:///%s" % (
os.path.join(path_data, "db", f"{package_name}.db")
)
class ModelSetting(db.Model): class ModelYoutubeDlItem(ModelBase):
__tablename__ = f"{package_name}_setting" P = P
__tablename__ = "youtube_dl_item"
__table_args__ = {"mysql_collate": "utf8_general_ci"} __table_args__ = {"mysql_collate": "utf8_general_ci"}
__bind_key__ = package_name __bind_key__ = P.package_name
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
key = db.Column(db.String(100), unique=True, nullable=False)
value = db.Column(db.String, nullable=False)
def __init__(self, key, value):
self.key = key
self.value = value
def __repr__(self):
return repr(self.as_dict())
def as_dict(self):
return {x.name: getattr(self, x.name) for x in self.__table__.columns}
@staticmethod
def get(key):
try:
return (
db.session.query(ModelSetting).filter_by(key=key).first().value.strip()
)
except Exception as e:
logger.error("Exception:%s %s", e, key)
logger.error(traceback.format_exc())
@staticmethod
def get_int(key):
try:
return int(ModelSetting.get(key))
except Exception as e:
logger.error("Exception:%s %s", e, key)
logger.error(traceback.format_exc())
@staticmethod
def get_bool(key):
try:
return ModelSetting.get(key) == "True"
except Exception as e:
logger.error("Exception:%s %s", e, key)
logger.error(traceback.format_exc())
@staticmethod
def set(key, value):
try:
item = (
db.session.query(ModelSetting)
.filter_by(key=key)
.with_for_update()
.first()
)
if item is not None:
item.value = value.strip()
db.session.commit()
else:
db.session.add(ModelSetting(key, value.strip()))
except Exception as e:
logger.error("Exception:%s", e)
logger.error(traceback.format_exc())
logger.error("Error Key:%s Value:%s", key, value)
@staticmethod
def to_dict():
try:
return Util.db_list_to_dict(db.session.query(ModelSetting).all())
except Exception as e:
logger.error("Exception:%s", e)
logger.error(traceback.format_exc())
@staticmethod
def setting_save(req):
try:
for key, value in req.form.items():
if key in ["scheduler", "is_running"]:
continue
if key.startswith("tmp_"):
continue
logger.debug("Key:%s Value:%s", key, value)
entity = (
db.session.query(ModelSetting)
.filter_by(key=key)
.with_for_update()
.first()
)
entity.value = value
db.session.commit()
return True
except Exception as e:
logger.error("Exception:%s", e)
logger.error(traceback.format_exc())
return False
@staticmethod
def get_list(key):
try:
value = ModelSetting.get(key)
values = [x.strip().strip() for x in value.replace("\n", "|").split("|")]
values = Util.get_list_except_empty(values)
return values
except Exception as e:
logger.error("Exception:%s %s", e, key)
logger.error(traceback.format_exc())

View File

@@ -8,11 +8,20 @@ from datetime import datetime
from threading import Thread from threading import Thread
from enum import Enum from enum import Enum
from framework.logger import get_logger from framework import celery as celery_shutil
import framework.common.celery as celery_shutil
package_name = __name__.split(".", maxsplit=1)[0] # from .plugin import Plugin
logger = get_logger(package_name)
from .setup import P
logger = P.logger
ModelSetting = P.ModelSetting
youtube_dl_package = P.youtube_dl_packages[
int(ModelSetting.get("youtube_dl_package"))
if ModelSetting.get("youtube_dl_package")
else 1 # LogicMain.db_default["youtube_dl_package"]
].replace("-", "_")
class Status(Enum): class Status(Enum):
@@ -25,11 +34,11 @@ class Status(Enum):
COMPLETED = 6 COMPLETED = 6
def __str__(self): def __str__(self):
str_list = ["??", "???", "?????", "??", "???", "??", "??"] str_list = ["준비", "분석중", "다운로드중", "실패", "변환중", "중지", "완료"]
return str_list[self.value] return str_list[self.value]
class MyYoutubeDL(object): class MyYoutubeDL:
DEFAULT_FILENAME = "%(title)s-%(id)s.%(ext)s" DEFAULT_FILENAME = "%(title)s-%(id)s.%(ext)s"
_index = 0 _index = 0
@@ -47,8 +56,6 @@ class MyYoutubeDL(object):
datebefore=None, datebefore=None,
): ):
# from youtube_dl.utils import DateRange # from youtube_dl.utils import DateRange
from .plugin import youtube_dl_package
DateRange = __import__( DateRange = __import__(
f"{youtube_dl_package}.utils", fromlist=["DateRange"] f"{youtube_dl_package}.utils", fromlist=["DateRange"]
).DateRange ).DateRange
@@ -79,10 +86,10 @@ class MyYoutubeDL(object):
self.end_time = None # 종료 시간 self.end_time = None # 종료 시간
# info_dict에서 얻는 정보 # info_dict에서 얻는 정보
self.info_dict = { self.info_dict = {
"extractor": None, # ?? "extractor": None, # 타입
"title": None, # ?? "title": None, # 제목
"uploader": None, # ??? "uploader": None, # 업로더
"uploader_url": None, # ??? ?? "uploader_url": None, # 업로더 주소
} }
# info_dict에서 얻는 정보(entries) # info_dict에서 얻는 정보(entries)
# self.info_dict['playlist_index'] = None # self.info_dict['playlist_index'] = None
@@ -91,10 +98,10 @@ class MyYoutubeDL(object):
# self.info_dict['thumbnail'] = None # 썸네일 # self.info_dict['thumbnail'] = None # 썸네일
# progress_hooks에서 얻는 정보 # progress_hooks에서 얻는 정보
self.progress_hooks = { self.progress_hooks = {
"downloaded_bytes": None, # ????? ?? "downloaded_bytes": None, # 다운로드한 크기
"total_bytes": None, # ?? ?? "total_bytes": None, # 전체 크기
"eta": None, # ?? ??(s) "eta": None, # 예상 시간(s)
"speed": None, # ???? ??(bytes/s) "speed": None, # 다운로드 속도(bytes/s)
} }
def start(self): def start(self):
@@ -106,8 +113,6 @@ class MyYoutubeDL(object):
def run(self): def run(self):
# import youtube_dl # import youtube_dl
from .plugin import youtube_dl_package
youtube_dl = __import__(youtube_dl_package) youtube_dl = __import__(youtube_dl_package)
try: try:
@@ -115,7 +120,10 @@ class MyYoutubeDL(object):
self.status = Status.START self.status = Status.START
# 동영상 정보 가져오기 # 동영상 정보 가져오기
info_dict = MyYoutubeDL.get_info_dict( info_dict = MyYoutubeDL.get_info_dict(
self.url, self.opts.get("proxy"), self.opts.get("cookiefile") self.url,
self.opts.get("proxy"),
self.opts.get("cookiefile"),
self.opts.get("http_headers"),
) )
if info_dict is None: if info_dict is None:
self.status = Status.ERROR self.status = Status.ERROR
@@ -134,7 +142,9 @@ class MyYoutubeDL(object):
} }
ydl_opts.update(self.opts) ydl_opts.update(self.opts)
with youtube_dl.YoutubeDL(ydl_opts) as ydl: with youtube_dl.YoutubeDL(ydl_opts) as ydl:
ydl.download([self.url]) logger.debug(self.url)
error_code = ydl.download([self.url])
logger.debug(error_code)
if self.status in (Status.START, Status.FINISHED): # 다운로드 성공 if self.status in (Status.START, Status.FINISHED): # 다운로드 성공
for i in glob(self.temp_path + "/**/*", recursive=True): for i in glob(self.temp_path + "/**/*", recursive=True):
path = i.replace(self.temp_path, self.save_path, 1) path = i.replace(self.temp_path, self.save_path, 1)
@@ -144,9 +154,9 @@ class MyYoutubeDL(object):
continue continue
celery_shutil.move(i, path) celery_shutil.move(i, path)
self.status = Status.COMPLETED self.status = Status.COMPLETED
except Exception as e: except Exception as error:
self.status = Status.ERROR self.status = Status.ERROR
logger.error("Exception:%s", e) logger.error("Exception:%s", error)
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
finally: finally:
# 임시폴더 삭제 # 임시폴더 삭제
@@ -164,8 +174,6 @@ class MyYoutubeDL(object):
@staticmethod @staticmethod
def get_version(): def get_version():
# from youtube_dl.version import __version__ # from youtube_dl.version import __version__
from .plugin import youtube_dl_package
__version__ = __import__( __version__ = __import__(
f"{youtube_dl_package}.version", fromlist=["__version__"] f"{youtube_dl_package}.version", fromlist=["__version__"]
).__version__ ).__version__
@@ -173,10 +181,8 @@ class MyYoutubeDL(object):
return __version__ return __version__
@staticmethod @staticmethod
def get_info_dict(url, proxy=None, cookiefile=None): def get_info_dict(url, proxy=None, cookiefile=None, http_headers=None):
# import youtube_dl # import youtube_dl
from .plugin import youtube_dl_package
youtube_dl = __import__(youtube_dl_package) youtube_dl = __import__(youtube_dl_package)
try: try:
@@ -185,27 +191,30 @@ class MyYoutubeDL(object):
ydl_opts["proxy"] = proxy ydl_opts["proxy"] = proxy
if cookiefile: if cookiefile:
ydl_opts["cookiefile"] = cookiefile ydl_opts["cookiefile"] = cookiefile
if http_headers:
ydl_opts["http_headers"] = http_headers
with youtube_dl.YoutubeDL(ydl_opts) as ydl: with youtube_dl.YoutubeDL(ydl_opts) as ydl:
# info = ydl.extract_info(url, download=False)
info = ydl.extract_info(url, download=False) info = ydl.extract_info(url, download=False)
except Exception as e: except Exception as error:
logger.error("Exception:%s", e) logger.error("Exception:%s", error)
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
return None return None
return info return ydl.sanitize_info(info)
def my_hook(self, d): def my_hook(self, data):
if self.status != Status.STOP: if self.status != Status.STOP:
self.status = { self.status = {
"downloading": Status.DOWNLOADING, "downloading": Status.DOWNLOADING,
"error": Status.ERROR, "error": Status.ERROR,
"finished": Status.FINISHED, # ???? ??. ?? ?? "finished": Status.FINISHED, # 다운로드 완료. 변환 시작
}[d["status"]] }[data["status"]]
if d["status"] != "error": if data["status"] != "error":
self.filename = os.path.basename(d.get("filename")) self.filename = os.path.basename(data.get("filename"))
self.progress_hooks["downloaded_bytes"] = d.get("downloaded_bytes") self.progress_hooks["downloaded_bytes"] = data.get("downloaded_bytes")
self.progress_hooks["total_bytes"] = d.get("total_bytes") self.progress_hooks["total_bytes"] = data.get("total_bytes")
self.progress_hooks["eta"] = d.get("eta") self.progress_hooks["eta"] = data.get("eta")
self.progress_hooks["speed"] = d.get("speed") self.progress_hooks["speed"] = data.get("speed")
def match_filter_func(self, info_dict): def match_filter_func(self, info_dict):
self.info_dict["playlist_index"] = info_dict["playlist_index"] self.info_dict["playlist_index"] = info_dict["playlist_index"]
@@ -220,15 +229,15 @@ class MyYoutubeDL(object):
@status.setter @status.setter
def status(self, value): def status(self, value):
from .plugin import socketio_emit from .main import LogicMain
self._status = value self._status = value
socketio_emit("status", self) LogicMain.socketio_emit("status", self)
class MyLogger(object): class MyLogger:
def debug(self, msg): def debug(self, msg):
if msg.find("\x1B") != -1 or msg.find("{") != -1: if msg.find(" ETA ") != -1:
# 과도한 로그 방지 # 과도한 로그 방지
return return
logger.debug(msg) logger.debug(msg)

502
plugin.py
View File

@@ -1,502 +0,0 @@
import os
import traceback
import subprocess
from flask import Blueprint, request, render_template, redirect, jsonify, abort
from flask_login import login_required
from flask_cors import cross_origin
from framework import check_api, socketio
from framework.logger import get_logger
from .logic import Logic
from .logic_normal import LogicNormal
from .model import ModelSetting
package_name = __name__.split(".", maxsplit=1)[0]
logger = get_logger(package_name)
youtube_dl_package = LogicNormal.get_youtube_dl_package(
ModelSetting.get("youtube_dl_package")
if ModelSetting.get("youtube_dl_package")
else Logic.db_default["youtube_dl_package"],
import_pkg=True,
)
#########################################################
# 플러그인 공용
#########################################################
blueprint = Blueprint(
package_name,
package_name,
url_prefix=f"/{package_name}",
template_folder=os.path.join(os.path.dirname(__file__), "templates"),
static_folder=os.path.join(os.path.dirname(__file__), "static"),
)
menu = {
"main": [package_name, "youtube-dl"],
"sub": [
["setting", "설정"],
["download", "다운로드"],
["thumbnail", "썸네일 다운로드"],
["sub", "자막 다운로드"],
["list", "목록"],
["log", "로그"],
],
"category": "vod",
}
plugin_info = {
"version": "3.1.0",
"name": "youtube-dl",
"category_name": "vod",
"developer": "joyfuI",
"description": "유튜브, 네이버TV 등 동영상 사이트에서 동영상 다운로드",
"home": "https://github.com/joyfuI/youtube-dl",
"more": "",
}
def plugin_load():
Logic.plugin_load()
def plugin_unload():
Logic.plugin_unload()
#########################################################
# WEB Menu
#########################################################
@blueprint.route("/")
def home():
return redirect(f"/{package_name}/list")
@blueprint.route("/<sub>")
@login_required
def first_menu(sub):
try:
arg = {
"package_name": package_name,
"template_name": f"{package_name}_{sub}",
}
if sub == "setting":
arg.update(ModelSetting.to_dict())
arg["package_list"] = LogicNormal.get_youtube_dl_package()
arg["youtube_dl_version"] = LogicNormal.get_youtube_dl_version()
arg["DEFAULT_FILENAME"] = LogicNormal.get_default_filename()
return render_template(f"{package_name}_{sub}.html", arg=arg)
elif sub == "download":
default_filename = ModelSetting.get("default_filename")
arg["filename"] = (
default_filename
if default_filename
else LogicNormal.get_default_filename()
)
arg["preset_list"] = LogicNormal.get_preset_list()
arg["postprocessor_list"] = LogicNormal.get_postprocessor_list()
return render_template(f"{package_name}_{sub}.html", arg=arg)
elif sub == "thumbnail":
default_filename = ModelSetting.get("default_filename")
arg["filename"] = (
default_filename
if default_filename
else LogicNormal.get_default_filename()
)
return render_template(f"{package_name}_{sub}.html", arg=arg)
elif sub == "sub":
default_filename = ModelSetting.get("default_filename")
arg["filename"] = (
default_filename
if default_filename
else LogicNormal.get_default_filename()
)
return render_template(f"{package_name}_{sub}.html", arg=arg)
elif sub == "list":
return render_template(f"{package_name}_{sub}.html", arg=arg)
elif sub == "log":
return render_template("log.html", package=package_name)
except Exception as e:
logger.error("Exception:%s", e)
logger.error(traceback.format_exc())
return render_template("sample.html", title=f"{package_name} - {sub}")
#########################################################
# For UI
#########################################################
@blueprint.route("/ajax/<sub>", methods=["POST"])
@login_required
def ajax(sub):
logger.debug("AJAX %s %s", package_name, sub)
try:
# 공통 요청
if sub == "setting_save":
ret = ModelSetting.setting_save(request)
if request.form["ffmpeg_path"] == "ffmpeg":
ModelSetting.set("ffmpeg_path", "")
return jsonify(ret)
# UI 요청
elif sub == "ffmpeg_version":
path = request.form["path"]
ret = subprocess.check_output([path, "-version"])
ret = ret.decode().replace("\n", "<br>")
return jsonify(ret)
elif sub == "download":
postprocessor = request.form["postprocessor"]
video_convertor, extract_audio = LogicNormal.get_postprocessor()
preferedformat = None
preferredcodec = None
preferredquality = None
if postprocessor in video_convertor:
preferedformat = postprocessor
elif postprocessor in extract_audio:
preferredcodec = postprocessor
preferredquality = 192
youtube_dl = LogicNormal.download(
plugin=package_name,
url=request.form["url"],
filename=request.form["filename"],
temp_path=ModelSetting.get("temp_path"),
save_path=ModelSetting.get("save_path"),
format=request.form["format"],
preferedformat=preferedformat,
preferredcodec=preferredcodec,
preferredquality=preferredquality,
proxy=ModelSetting.get("proxy"),
ffmpeg_path=ModelSetting.get("ffmpeg_path"),
)
youtube_dl.start()
socketio_emit("add", youtube_dl)
return jsonify([])
elif sub == "thumbnail":
youtube_dl = LogicNormal.thumbnail(
plugin=package_name,
url=request.form["url"],
filename=request.form["filename"],
temp_path=ModelSetting.get("temp_path"),
save_path=ModelSetting.get("save_path"),
all_thumbnails=request.form["all_thumbnails"],
proxy=ModelSetting.get("proxy"),
ffmpeg_path=ModelSetting.get("ffmpeg_path"),
)
youtube_dl.start()
socketio_emit("add", youtube_dl)
return jsonify([])
elif sub == "sub":
youtube_dl = LogicNormal.sub(
plugin=package_name,
url=request.form["url"],
filename=request.form["filename"],
temp_path=ModelSetting.get("temp_path"),
save_path=ModelSetting.get("save_path"),
all_subs=request.form["all_subs"],
sub_lang=request.form["sub_lang"],
auto_sub=request.form["auto_sub"],
proxy=ModelSetting.get("proxy"),
ffmpeg_path=ModelSetting.get("ffmpeg_path"),
)
youtube_dl.start()
socketio_emit("add", youtube_dl)
return jsonify([])
elif sub == "list":
ret = []
for i in LogicNormal.youtube_dl_list:
data = LogicNormal.get_data(i)
if data is not None:
ret.append(data)
return jsonify(ret)
elif sub == "all_stop":
for i in LogicNormal.youtube_dl_list:
i.stop()
return jsonify([])
elif sub == "stop":
index = int(request.form["index"])
LogicNormal.youtube_dl_list[index].stop()
return jsonify([])
except Exception as e:
logger.error("Exception:%s", e)
logger.error(traceback.format_exc())
#########################################################
# API
#########################################################
# API 명세는 https://github.com/joyfuI/youtube-dl#api
@blueprint.route("/api/<sub>", methods=["GET", "POST"])
@cross_origin()
@check_api
def api(sub):
plugin = request.values.get("plugin")
logger.debug("API %s %s: %s", package_name, sub, plugin)
if not plugin: # 요청한 플러그인명이 빈문자열이거나 None면
abort(403) # 403 에러(거부)
try:
# 동영상 정보를 반환하는 API
if sub == "info_dict":
url = request.values.get("url")
ret = {"errorCode": 0, "info_dict": None}
if None in (url,):
return LogicNormal.abort(ret, 1) # 필수 요청 변수가 없음
if not url.startswith("http"):
return LogicNormal.abort(ret, 2) # 잘못된 동영상 주소
info_dict = LogicNormal.get_info_dict(url, ModelSetting.get("proxy"))
if info_dict is None:
return LogicNormal.abort(ret, 10) # 실패
ret["info_dict"] = info_dict
return jsonify(ret)
# 비디오 다운로드 준비를 요청하는 API
elif sub == "download":
key = request.values.get("key")
url = request.values.get("url")
filename = request.values.get(
"filename", ModelSetting.get("default_filename")
)
save_path = request.values.get("save_path", ModelSetting.get("save_path"))
format_code = request.values.get("format", None)
preferedformat = request.values.get("preferedformat", None)
preferredcodec = request.values.get("preferredcodec", None)
preferredquality = request.values.get("preferredquality", 192)
dateafter = request.values.get("dateafter", None)
playlist = request.values.get("playlist", None)
archive = request.values.get("archive", None)
start = request.values.get("start", False)
cookiefile = request.values.get("cookiefile", None)
ret = {"errorCode": 0, "index": None}
if None in (key, url):
return LogicNormal.abort(ret, 1) # 필수 요청 변수가 없음
if not url.startswith("http"):
return LogicNormal.abort(ret, 2) # 잘못된 동영상 주소
if preferredcodec not in (
None,
"best",
"mp3",
"aac",
"flac",
"m4a",
"opus",
"vorbis",
"wav",
):
return LogicNormal.abort(ret, 5) # 허용되지 않은 값이 있음
if not filename:
filename = LogicNormal.get_default_filename()
youtube_dl = LogicNormal.download(
plugin=plugin,
url=url,
filename=filename,
temp_path=ModelSetting.get("temp_path"),
save_path=save_path,
format=format_code,
preferedformat=preferedformat,
preferredcodec=preferredcodec,
preferredquality=preferredquality,
dateafter=dateafter,
playlist=playlist,
archive=archive,
proxy=ModelSetting.get("proxy"),
ffmpeg_path=ModelSetting.get("ffmpeg_path"),
key=key,
cookiefile=cookiefile,
)
if youtube_dl is None:
return LogicNormal.abort(ret, 10) # 실패
ret["index"] = youtube_dl.index
if start:
youtube_dl.start()
socketio_emit("add", youtube_dl)
return jsonify(ret)
# 썸네일 다운로드 준비를 요청하는 API
elif sub == "thumbnail":
key = request.values.get("key")
url = request.values.get("url")
filename = request.values.get(
"filename", ModelSetting.get("default_filename")
)
save_path = request.values.get("save_path", ModelSetting.get("save_path"))
all_thumbnails = request.values.get("all_thumbnails", False)
dateafter = request.values.get("dateafter", None)
playlist = request.values.get("playlist", None)
archive = request.values.get("archive", None)
start = request.values.get("start", False)
cookiefile = request.values.get("cookiefile", None)
ret = {"errorCode": 0, "index": None}
if None in (key, url):
return LogicNormal.abort(ret, 1) # 필수 요청 변수가 없음
if not url.startswith("http"):
return LogicNormal.abort(ret, 2) # 잘못된 동영상 주소
if not filename:
filename = LogicNormal.get_default_filename()
youtube_dl = LogicNormal.thumbnail(
plugin=plugin,
url=url,
filename=filename,
temp_path=ModelSetting.get("temp_path"),
save_path=save_path,
all_thumbnails=all_thumbnails,
dateafter=dateafter,
playlist=playlist,
archive=archive,
proxy=ModelSetting.get("proxy"),
ffmpeg_path=ModelSetting.get("ffmpeg_path"),
key=key,
cookiefile=cookiefile,
)
if youtube_dl is None:
return LogicNormal.abort(ret, 10) # 실패
ret["index"] = youtube_dl.index
if start:
youtube_dl.start()
socketio_emit("add", youtube_dl)
return jsonify(ret)
# 자막 다운로드 준비를 요청하는 API
elif sub == "sub":
key = request.values.get("key")
url = request.values.get("url")
filename = request.values.get(
"filename", ModelSetting.get("default_filename")
)
save_path = request.values.get("save_path", ModelSetting.get("save_path"))
all_subs = request.values.get("all_subs", False)
sub_lang = request.values.get("sub_lang", "ko")
auto_sub = request.values.get("all_subs", False)
dateafter = request.values.get("dateafter", None)
playlist = request.values.get("playlist", None)
archive = request.values.get("archive", None)
start = request.values.get("start", False)
cookiefile = request.values.get("cookiefile", None)
ret = {"errorCode": 0, "index": None}
if None in (key, url):
return LogicNormal.abort(ret, 1) # 필수 요청 변수가 없음
if not url.startswith("http"):
return LogicNormal.abort(ret, 2) # 잘못된 동영상 주소
if not filename:
filename = LogicNormal.get_default_filename()
youtube_dl = LogicNormal.sub(
plugin=plugin,
url=url,
filename=filename,
temp_path=ModelSetting.get("temp_path"),
save_path=save_path,
all_subs=all_subs,
sub_lang=sub_lang,
auto_sub=auto_sub,
dateafter=dateafter,
playlist=playlist,
archive=archive,
proxy=ModelSetting.get("proxy"),
ffmpeg_path=ModelSetting.get("ffmpeg_path"),
key=key,
cookiefile=cookiefile,
)
if youtube_dl is None:
return LogicNormal.abort(ret, 10) # 실패
ret["index"] = youtube_dl.index
if start:
youtube_dl.start()
socketio_emit("add", youtube_dl)
return jsonify(ret)
# 다운로드 시작을 요청하는 API
elif sub == "start":
index = request.values.get("index")
key = request.values.get("key")
ret = {"errorCode": 0, "status": None}
if None in (index, key):
return LogicNormal.abort(ret, 1) # 필수 요청 변수가 없음
index = int(index)
if not 0 <= index < len(LogicNormal.youtube_dl_list):
return LogicNormal.abort(ret, 3) # 인덱스 범위를 벗어남
youtube_dl = LogicNormal.youtube_dl_list[index]
if youtube_dl.key != key:
return LogicNormal.abort(ret, 4) # 키가 일치하지 않음
ret["status"] = youtube_dl.status.name
if not youtube_dl.start():
return LogicNormal.abort(ret, 10) # 실패
return jsonify(ret)
# 다운로드 중지를 요청하는 API
elif sub == "stop":
index = request.values.get("index")
key = request.values.get("key")
ret = {"errorCode": 0, "status": None}
if None in (index, key):
return LogicNormal.abort(ret, 1) # 필수 요청 변수가 없음
index = int(index)
if not 0 <= index < len(LogicNormal.youtube_dl_list):
return LogicNormal.abort(ret, 3) # 인덱스 범위를 벗어남
youtube_dl = LogicNormal.youtube_dl_list[index]
if youtube_dl.key != key:
return LogicNormal.abort(ret, 4) # 키가 일치하지 않음
ret["status"] = youtube_dl.status.name
if not youtube_dl.stop():
return LogicNormal.abort(ret, 10) # 실패
return jsonify(ret)
# 현재 상태를 반환하는 API
elif sub == "status":
index = request.values.get("index")
key = request.values.get("key")
ret = {
"errorCode": 0,
"status": None,
"type": None,
"start_time": None,
"end_time": None,
"temp_path": None,
"save_path": None,
}
if None in (index, key):
return LogicNormal.abort(ret, 1) # 필수 요청 변수가 없음
index = int(index)
if not 0 <= index < len(LogicNormal.youtube_dl_list):
return LogicNormal.abort(ret, 3) # 인덱스 범위를 벗어남
youtube_dl = LogicNormal.youtube_dl_list[index]
if youtube_dl.key != key:
return LogicNormal.abort(ret, 4) # 키가 일치하지 않음
ret["status"] = youtube_dl.status.name
ret["type"] = youtube_dl.type
ret["start_time"] = (
youtube_dl.start_time.strftime("%Y-%m-%dT%H:%M:%S")
if youtube_dl.start_time is not None
else None
)
ret["end_time"] = (
youtube_dl.end_time.strftime("%Y-%m-%dT%H:%M:%S")
if youtube_dl.end_time is not None
else None
)
ret["temp_path"] = youtube_dl.temp_path
ret["save_path"] = youtube_dl.save_path
return jsonify(ret)
except Exception as e:
logger.error("Exception:%s", e)
logger.error(traceback.format_exc())
abort(500) # 500 에러(서버 오류)
abort(404) # 404 에러(페이지 없음)
#########################################################
# socketio
#########################################################
def socketio_emit(cmd, data):
socketio.emit(
cmd, LogicNormal.get_data(data), namespace=f"/{package_name}", broadcast=True
)

77
setup.py Normal file
View File

@@ -0,0 +1,77 @@
# -*- coding: utf-8 -*-
# @Time : 2023/02/25 7:20 PM
# @Author : yommi
# @Site :
# @File : setup.py
# @Software: PyCharm
# @Path : youtube-dl/setup.py
__menu = {
"uri": __package__,
"name": "유튜브 다운로더",
"list": [
{
"uri": "basic",
"name": "기본 처리",
"list": [
{
"uri": "setting",
"name": "설정",
},
],
},
{
"uri": "basic/download",
"name": "다운로드",
},
{
"uri": "download",
"name": "다운로드",
"list": [
{
"uri": "basic",
"name": "다운로드",
},
],
},
{"uri": "thumbnail", "name": "썸네일 다운로드"},
{"uri": "sub", "name": "자막 다운로드"},
{
"uri": "manual",
"name": "매뉴얼",
"list": [
{"uri": "README.md", "name": "README.md"},
],
},
{
"uri": "log",
"name": "로그",
},
],
}
setting = {
"filepath": __file__,
"use_db": True,
"use_default_setting": True,
"home_module": None,
"menu": __menu,
"setting_menu": None,
"default_route": "normal",
}
from plugin import *
P = create_plugin_instance(setting)
P.youtube_dl_packages = ["youtube-dl", "yt-dlp", "youtube-dlc"]
try:
from .mod_basic import ModuleBasic
P.set_module_list([ModuleBasic])
except Exception as e:
P.logger.error(f"Exception:{str(e)}")
P.logger.error(traceback.format_exc())
logger = P.logger

View File

@@ -1,51 +1,69 @@
'use strict'; 'use strict';
const url = document.getElementById('url'); (() => {
const preset = document.getElementById('preset'); const post_ajax = (url, data) => {
const format = document.getElementById('format'); const loading = document.getElementById('loading');
const postprocessor = document.getElementById('postprocessor'); if (loading) {
const download_btn = document.getElementById('download_btn'); loading.style.display = 'block';
}
// 프리셋 변경 return fetch(`/${package_name}/ajax${url}`, {
preset.addEventListener('change', () => { method: 'POST',
if (preset.value !== '_custom') { cache: 'no-cache',
format.value = preset.value; headers: {
} 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
}); },
format.addEventListener('input', () => { body: new URLSearchParams(data),
preset.value = '_custom';
});
// 후처리 변경
postprocessor.addEventListener('change', () => {
const select = postprocessor.selectedOptions[0];
if (select.parentElement.label === '오디오 추출') {
preset.value = 'bestaudio/best';
format.value = preset.value;
}
});
// 다운로드
download_btn.addEventListener('click', (event) => {
event.preventDefault();
if (!url.value.startsWith('http')) {
notify('URL을 입력하세요.', 'warning');
return;
}
fetch(`/${package_name}/ajax/download`, {
method: 'POST',
cache: 'no-cache',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
},
body: get_formdata('#download'),
})
.then((response) => response.json())
.then(() => {
notify('분석중..', 'info');
}) })
.catch(() => { .then((response) => response.json())
notify('다운로드 요청 실패', 'danger'); .then((ret) => {
}); if (ret.msg) {
}); notify(ret.msg, ret.ret);
}
return ret;
})
.catch(() => {
notify('요청 실패', 'danger');
})
.finally(() => {
if (loading) {
loading.style.display = 'none';
}
});
};
const url = document.getElementById('url');
const preset = document.getElementById('preset');
const format = document.getElementById('format');
const postprocessor = document.getElementById('postprocessor');
const download_btn = document.getElementById('download_btn');
// 프리셋 변경
preset.addEventListener('change', () => {
if (preset.value !== '_custom') {
format.value = preset.value;
}
});
format.addEventListener('input', () => {
preset.value = '_custom';
});
// 후처리 변경
postprocessor.addEventListener('change', () => {
const select = postprocessor.selectedOptions[0];
if (select.parentElement.label === '오디오 추출') {
preset.value = 'bestaudio/best';
format.value = preset.value;
}
});
// 다운로드
download_btn.addEventListener('click', (event) => {
event.preventDefault();
if (!url.value.startsWith('http')) {
notify('URL을 입력하세요.', 'warning');
return;
}
post_ajax('/download', get_formdata('#download'));
});
})();

View File

@@ -1,152 +1,155 @@
'use strict'; 'use strict';
const all_stop_btn = document.getElementById('all_stop_btn'); (() => {
const list_tbody = document.getElementById('list_tbody'); const post_ajax = (url, data) => {
const loading = document.getElementById('loading');
// 소켓 if (loading) {
const socket = io.connect(`${location.origin}/${package_name}`); loading.style.display = 'block';
socket.on('add', (data) => {
list_tbody.innerHTML += make_item(data);
});
socket.on('status', (data) => {
status_html(data);
});
// 목록 불러오기
fetch(`/${package_name}/ajax/list`, {
method: 'POST',
cache: 'no-cache',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
},
})
.then((response) => response.json())
.then((data) => {
let str = '';
for (const item of data) {
str += make_item(item);
} }
list_tbody.innerHTML = str; return fetch(`/${package_name}/ajax${url}`, {
method: 'POST',
cache: 'no-cache',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
},
body: new URLSearchParams(data),
})
.then((response) => response.json())
.then((ret) => {
if (ret.msg) {
notify(ret.msg, ret.ret);
}
return ret;
})
.catch(() => {
notify('요청 실패', 'danger');
})
.finally(() => {
if (loading) {
loading.style.display = 'none';
}
});
};
const all_stop_btn = document.getElementById('all_stop_btn');
const list_tbody = document.getElementById('list_tbody');
const get_item = (data) => {
let str = `<td>${data.index + 1}</td>`;
str += `<td>${data.plugin}</td>`;
str += `<td>${data.start_time}</td>`;
str += `<td>${data.extractor}</td>`;
str += `<td>${data.title}</td>`;
str += `<td>${data.status_ko}</td>`;
let visi = 'hidden';
if (parseInt(data.percent) > 0 && data.status_str !== 'STOP') {
visi = 'visible';
}
str += `<td><div class="progress"><div class="progress-bar" style="visibility: ${visi}; width: ${data.percent}%">${data.percent}%</div></div></td>`;
str += `<td>${data.download_time}</td>`;
str += '<td class="tableRowHoverOff">';
if (
data.status_str === 'START' ||
data.status_str === 'DOWNLOADING' ||
data.status_str === 'FINISHED'
) {
str += `<button class="align-middle btn btn-outline-danger btn-sm youtubeDl-stop" data-index="${data.index}">중지</button>`;
}
str += '</td>';
return str;
};
const info_html = (left, right, option) => {
let str = '<div class="row">';
const link = left === 'URL' || left === '업로더';
str += '<div class="col-sm-2">';
str += `<b>${left}</b>`;
str += '</div>';
str += '<div class="col-sm-10">';
str += '<div class="input-group col-sm-9">';
str += '<span class="text-left info-padding">';
if (link) {
str += `<a href="${option}" target="_blank">`;
}
str += right;
if (link) {
str += '</a>';
}
str += '</span></div></div></div>';
return str;
};
const get_detail = (data) => {
let str = info_html('URL', data.url, data.url);
str += info_html('업로더', data.uploader, data.uploader_url);
str += info_html('임시폴더', data.temp_path);
str += info_html('저장폴더', data.save_path);
str += info_html('종료시간', data.end_time);
if (data.status_str === 'DOWNLOADING') {
str += info_html('', '<b>현재 다운로드 중인 파일에 대한 정보</b>');
str += info_html('파일명', data.filename);
str += info_html(
'진행률(current/total)',
`${data.percent}% (${data.downloaded_bytes_str} / ${data.total_bytes_str})`
);
str += info_html('남은 시간', `${data.eta}`);
str += info_html('다운 속도', data.speed_str);
}
return str;
};
const make_item = (data) => {
let str = `<tr id="item_${data.index}" class="cursor-pointer" aria-expanded="true" data-toggle="collapse" data-target="#collapse_${data.index}">`;
str += get_item(data);
str += '</tr>';
str += `<tr id="collapse_${data.index}" class="collapse tableRowHoverOff">`;
str += '<td colspan="9">';
str += `<div id="detail_${data.index}">`;
str += get_detail(data);
str += '</div>';
str += '</td>';
str += '</tr>';
return str;
};
const status_html = (data) => {
document.getElementById(`item_${data.index}`).innerHTML = get_item(data);
document.getElementById(`detail_${data.index}`).innerHTML =
get_detail(data);
};
// 소켓
const socket = io.connect(`${location.origin}/${package_name}`);
socket.on('add', (data) => {
list_tbody.innerHTML += make_item(data);
});
socket.on('status', (data) => {
status_html(data);
}); });
// 전체 중지 const reload_list = async () => {
all_stop_btn.addEventListener('click', (event) => { const { data } = await post_ajax('/list');
event.preventDefault(); list_tbody.innerHTML = data.map((item) => make_item(item)).join('');
fetch(`/${package_name}/ajax/all_stop`, { };
method: 'POST',
cache: 'no-cache',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
},
})
.then((response) => response.json())
.then(() => {
location.reload();
});
});
// 중지 // 전체 중지
list_tbody.addEventListener('click', (event) => { all_stop_btn.addEventListener('click', (event) => {
event.preventDefault(); event.preventDefault();
const target = event.target; post_ajax('/all_stop').then(reload_list);
if (!target.classList.contains('youtubeDl-stop')) { });
return;
} // 중지
fetch(`/${package_name}/ajax/stop`, { list_tbody.addEventListener('click', (event) => {
method: 'POST', event.preventDefault();
cache: 'no-cache', const target = event.target;
headers: { if (!target.classList.contains('youtubeDl-stop')) {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', return;
}, }
body: new URLSearchParams({ post_ajax('/stop', {
index: target.dataset.index, index: target.dataset.index,
}), }).then(reload_list);
}) });
.then((response) => response.json())
.then(() => {
location.reload();
});
});
function make_item(data) { // 목록 불러오기
let str = `<tr id="item_${data.index}" class="cursor-pointer" aria-expanded="true" data-toggle="collapse" data-target="#collapse_${data.index}">`; reload_list();
str += get_item(data); })();
str += '</tr>';
str += `<tr id="collapse_${data.index}" class="collapse tableRowHoverOff">`;
str += '<td colspan="9">';
str += `<div id="detail_${data.index}">`;
str += get_detail(data);
str += '</div>';
str += '</td>';
str += '</tr>';
return str;
}
function get_item(data) {
let str = `<td>${data.index + 1}</td>`;
str += `<td>${data.plugin}</td>`;
str += `<td>${data.start_time}</td>`;
str += `<td>${data.extractor}</td>`;
str += `<td>${data.title}</td>`;
str += `<td>${data.status_ko}</td>`;
let visi = 'hidden';
if (parseInt(data.percent) > 0 && data.status_str !== 'STOP') {
visi = 'visible';
}
str += `<td><div class="progress"><div class="progress-bar" style="visibility: ${visi}; width: ${data.percent}%">${data.percent}%</div></div></td>`;
str += `<td>${data.download_time}</td>`;
str += '<td class="tableRowHoverOff">';
if (
data.status_str === 'START' ||
data.status_str === 'DOWNLOADING' ||
data.status_str === 'FINISHED'
) {
str += `<button class="align-middle btn btn-outline-danger btn-sm youtubeDl-stop" data-index="${data.index}">중지</button>`;
}
str += '</td>';
return str;
}
function get_detail(data) {
let str = info_html('URL', data.url, data.url);
str += info_html('업로더', data.uploader, data.uploader_url);
str += info_html('임시폴더', data.temp_path);
str += info_html('저장폴더', data.save_path);
str += info_html('종료시간', data.end_time);
if (data.status_str === 'DOWNLOADING') {
str += info_html('', '<b>현재 다운로드 중인 파일에 대한 정보</b>');
str += info_html('파일명', data.filename);
str += info_html(
'진행률(current/total)',
`${data.percent}% (${data.downloaded_bytes_str} / ${data.total_bytes_str})`
);
str += info_html('남은 시간', `${data.eta}`);
str += info_html('다운 속도', data.speed_str);
}
return str;
}
function info_html(left, right, option) {
let str = '<div class="row">';
const link = left === 'URL' || left === '업로더';
str += '<div class="col-sm-2">';
str += `<b>${left}</b>`;
str += '</div>';
str += '<div class="col-sm-10">';
str += '<div class="input-group col-sm-9">';
str += '<span class="text-left info-padding">';
if (link) {
str += `<a href="${option}" target="_blank">`;
}
str += right;
if (link) {
str += '</a>';
}
str += '</span></div></div></div>';
return str;
}
function status_html(data) {
document.getElementById(`item_${data.index}`).innerHTML = get_item(data);
document.getElementById(`detail_${data.index}`).innerHTML = get_detail(data);
}

View File

@@ -1,74 +1,94 @@
'use strict'; 'use strict';
const ffmpeg_path = document.getElementById('ffmpeg_path'); (() => {
const ffmpeg_version_btn = document.getElementById('ffmpeg_version_btn'); const post_ajax = (url, data) => {
const ffmpeg_path_btn = document.getElementById('ffmpeg_path_btn'); const loading = document.getElementById('loading');
const temp_path = document.getElementById('temp_path'); if (loading) {
const temp_path_btn = document.getElementById('temp_path_btn'); loading.style.display = 'block';
const save_path = document.getElementById('save_path'); }
const save_path_btn = document.getElementById('save_path_btn'); return fetch(`/${package_name}/ajax${url}`, {
const modal_title = document.getElementById('modal_title'); method: 'POST',
const modal_body = document.getElementById('modal_body'); cache: 'no-cache',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
},
body: new URLSearchParams(data),
})
.then((response) => response.json())
.then((ret) => {
if (ret.msg) {
notify(ret.msg, ret.ret);
}
return ret;
})
.catch(() => {
notify('요청 실패', 'danger');
})
.finally(() => {
if (loading) {
loading.style.display = 'none';
}
});
};
// FFmpeg 버전확인 const ffmpeg_path = document.getElementById('ffmpeg_path');
ffmpeg_version_btn.addEventListener('click', (event) => { const ffmpeg_version_btn = document.getElementById('ffmpeg_version_btn');
event.preventDefault(); const ffmpeg_path_btn = document.getElementById('ffmpeg_path_btn');
let ffmpeg = ffmpeg_path.value; const temp_path = document.getElementById('temp_path');
if (ffmpeg.length === 0) { const temp_path_btn = document.getElementById('temp_path_btn');
ffmpeg = 'ffmpeg'; const save_path = document.getElementById('save_path');
} const save_path_btn = document.getElementById('save_path_btn');
const modal_title = document.getElementById('modal_title');
const modal_body = document.getElementById('modal_body');
fetch(`/${package_name}/ajax/ffmpeg_version`, { // FFmpeg 버전확인
method: 'POST', ffmpeg_version_btn.addEventListener('click', (event) => {
cache: 'no-cache', event.preventDefault();
headers: { let ffmpeg = ffmpeg_path.value;
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', if (ffmpeg.length === 0) {
}, ffmpeg = 'ffmpeg';
body: new URLSearchParams({ }
post_ajax('/ffmpeg_version', {
path: ffmpeg, path: ffmpeg,
}), }).then(({ data }) => {
})
.then((response) => response.json())
.then((data) => {
modal_title.innerHTML = `${ffmpeg} -version`; modal_title.innerHTML = `${ffmpeg} -version`;
modal_body.innerHTML = data; modal_body.innerHTML = data;
$('#large_modal').modal(); $('#large_modal').modal();
})
.catch(() => {
notify('버전확인 실패', 'danger');
}); });
});
// FFmpeg 파일 선택
ffmpeg_path_btn.addEventListener('click', (event) => {
event.preventDefault();
m_select_local_file_modal('실행 파일 선택', '/', false, (result) => {
ffmpeg_path.value = result;
}); });
});
// 임시 폴더 경로 선택 // FFmpeg 파일 선택
temp_path_btn.addEventListener('click', (event) => { ffmpeg_path_btn.addEventListener('click', (event) => {
event.preventDefault(); event.preventDefault();
m_select_local_file_modal( m_select_local_file_modal('실행 파일 선택', '/', false, (result) => {
'저장 경로 선택', ffmpeg_path.value = result;
temp_path.value, });
true, });
(result) => {
temp_path.value = result;
}
);
});
// 저장 폴더 경로 선택 // 임시 폴더 경로 선택
save_path_btn.addEventListener('click', (event) => { temp_path_btn.addEventListener('click', (event) => {
event.preventDefault(); event.preventDefault();
m_select_local_file_modal( m_select_local_file_modal(
'저장 경로 선택', '저장 경로 선택',
save_path.value, temp_path.value,
true, true,
(result) => { (result) => {
save_path.value = result; temp_path.value = result;
} }
); );
}); });
// 저장 폴더 경로 선택
save_path_btn.addEventListener('click', (event) => {
event.preventDefault();
m_select_local_file_modal(
'저장 경로 선택',
save_path.value,
true,
(result) => {
save_path.value = result;
}
);
});
})();

View File

@@ -1,34 +1,52 @@
'use strict'; 'use strict';
const url = document.getElementById('url'); (() => {
const download_btn = document.getElementById('download_btn'); const post_ajax = (url, data) => {
const loading = document.getElementById('loading');
// 모든 자막 다운로드 if (loading) {
$('#all_subs').change(() => { loading.style.display = 'block';
use_collapse('all_subs', true); }
}); return fetch(`/${package_name}/ajax${url}`, {
method: 'POST',
// 다운로드 cache: 'no-cache',
download_btn.addEventListener('click', (event) => { headers: {
event.preventDefault(); 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
if (!url.value.startsWith('http')) { },
notify('URL을 입력하세요.', 'warning'); body: new URLSearchParams(data),
return;
}
fetch(`/${package_name}/ajax/sub`, {
method: 'POST',
cache: 'no-cache',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
},
body: get_formdata('#download'),
})
.then((response) => response.json())
.then(() => {
notify('분석중..', 'info');
}) })
.catch(() => { .then((response) => response.json())
notify('다운로드 요청 실패', 'danger'); .then((ret) => {
}); if (ret.msg) {
}); notify(ret.msg, ret.ret);
}
return ret;
})
.catch(() => {
notify('요청 실패', 'danger');
})
.finally(() => {
if (loading) {
loading.style.display = 'none';
}
});
};
const url = document.getElementById('url');
const download_btn = document.getElementById('download_btn');
// 모든 자막 다운로드
$('#all_subs').change(() => {
use_collapse('all_subs', true);
});
// 다운로드
download_btn.addEventListener('click', (event) => {
event.preventDefault();
if (!url.value.startsWith('http')) {
notify('URL을 입력하세요.', 'warning');
return;
}
post_ajax('/sub', get_formdata('#download'));
});
})();

View File

@@ -1,29 +1,47 @@
'use strict'; 'use strict';
const url = document.getElementById('url'); (() => {
const download_btn = document.getElementById('download_btn'); const post_ajax = (url, data) => {
const loading = document.getElementById('loading');
// 다운로드 if (loading) {
download_btn.addEventListener('click', (event) => { loading.style.display = 'block';
event.preventDefault(); }
if (!url.value.startsWith('http')) { return fetch(`/${package_name}/ajax${url}`, {
notify('URL을 입력하세요.', 'warning'); method: 'POST',
return; cache: 'no-cache',
} headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
fetch(`/${package_name}/ajax/thumbnail`, { },
method: 'POST', body: new URLSearchParams(data),
cache: 'no-cache',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
},
body: get_formdata('#download'),
})
.then((response) => response.json())
.then(() => {
notify('분석중..', 'info');
}) })
.catch(() => { .then((response) => response.json())
notify('다운로드 요청 실패', 'danger'); .then((ret) => {
}); if (ret.msg) {
}); notify(ret.msg, ret.ret);
}
return ret;
})
.catch(() => {
notify('요청 실패', 'danger');
})
.finally(() => {
if (loading) {
loading.style.display = 'none';
}
});
};
const url = document.getElementById('url');
const download_btn = document.getElementById('download_btn');
// 다운로드
download_btn.addEventListener('click', (event) => {
event.preventDefault();
if (!url.value.startsWith('http')) {
notify('URL을 입력하세요.', 'warning');
return;
}
post_ajax('/thumbnail', get_formdata('#download'));
});
})();

View File

@@ -31,19 +31,19 @@
{% block content %} {% block content %}
<form id="download"> <form id="download">
{{ macros.setting_input_text('url', 'URL', placeholder='http:// 주소', desc='유튜브, 네이버TV 등 동영상 주소') }} {{ macros.setting_input_text('url', 'URL', placeholder='http:// 주소', desc='유튜브, 네이버TV 등 동영상 주소') }}
{{ macros.setting_input_text('filename', '파일명', value=arg['filename'], desc='템플릿 규칙은 https://github.com/ytdl-org/youtube-dl/#output-template 참고') }} {{ macros.setting_input_text('filename', '파일명', value=arg['filename'], desc='템플릿 규칙은 https://github.com/ytdl-org/youtube-dl/#output-template 참고') }}
{{ macros.setting_select('preset', '동영상 포맷 프리셋', arg['preset_list'], col='3') }} {{ macros.setting_select('preset', '동영상 포맷 프리셋', arg['preset_list'], col='3') }}
{{ macros.setting_input_text('format', '동영상 포맷', desc=['포맷 지정은 https://github.com/ytdl-org/youtube-dl/#format-selection 참고', '빈칸으로 두면 최고 화질로 다운로드합니다.']) }} {{ macros.setting_input_text('format', '동영상 포맷', desc=['포맷 지정은 https://github.com/ytdl-org/youtube-dl/#format-selection 참고', '빈칸으로 두면 최고 화질로 다운로드합니다.']) }}
{{ my_setting_select('postprocessor', '후처리', arg['postprocessor_list'], col='3', desc='다운로드 후 FFmpeg로 후처리합니다.') }} {{ my_setting_select('postprocessor', '후처리', arg['postprocessor_list'], col='3', desc='다운로드 후 FFmpeg로 후처리합니다.') }}
{{ macros.setting_button([['download_btn', '다운로드']]) }} {{ macros.setting_buttons([['download_btn', '다운로드']]) }}
</form> </form>
<script> <script>
"use strict"; "use strict";
const package_name = '{{ arg["package_name"] }}'; const package_name = '{{ arg["package_name"] }}';
</script> </script>
<script src="{{ url_for('.static', filename='%s.js' % arg['template_name']) }}"></script> <script src="{{ url_for('.static', filename='%s.js' % arg['template_name']) }}?ver={{ arg['package_version'] }}"></script>
{% endblock %} {% endblock %}

View File

@@ -0,0 +1,34 @@
{% extends "base.html" %} {% block content %}
{{ macros.m_button_group([['globalSettingSaveBtn', '설정 저장'], ['globalOneExecuteBtn', '1회 실행'], ['globalImmediatelyExecuteBtn', '즉시 실행']])}}
{{ macros.m_row_start('5') }}
{{ macros.m_row_end() }}
<form id="setting">
{{ macros.setting_radio_with_value('youtube_dl_package', 'youtube-dl', arg['package_list'],
value=arg['youtube_dl_package'], desc='사용할 youtube-dl 패키지를 선택합니다. 설정 저장 후
재시작이 필요합니다.') }}
{{ macros.setting_input_text('youtube_dl_version', 'youtube-dl 버전',
value=arg['youtube_dl_version'], disabled=True) }}
{{
macros.setting_input_text_and_buttons('ffmpeg_path', 'FFmpeg 경로', [['ffmpeg_version_btn',
'버전확인'], ['ffmpeg_path_btn', '파일 선택']], value=arg['ffmpeg_path'], placeholder='ffmpeg',
desc='SJVA에 내장된 버전 말고 원하는 버전을 사용할 수 있습니다.') }} {{
macros.setting_input_text_and_buttons('temp_path', '임시 폴더', [['temp_path_btn', '경로 선택']],
value=arg['temp_path'], desc='다운로드 파일이 임시로 저장될 폴더입니다.') }} {{
macros.setting_input_text_and_buttons('save_path', '저장 폴더', [['save_path_btn', '경로 선택']],
value=arg['save_path'], desc='정상적으로 완료된 파일이 이동할 폴더입니다.') }} {{
macros.setting_input_text('default_filename', '기본 파일명', value=arg['default_filename'],
placeholder=arg['DEFAULT_FILENAME'], desc='템플릿 규칙은
https://github.com/ytdl-org/youtube-dl/#output-template 참고') }} {{
macros.setting_input_text('proxy', '프록시', value=arg['proxy'], desc=['HTTP/HTTPS/SOCKS를
지원합니다. 예) socks5://127.0.0.1:1080/', '빈칸으로 두면 프록시를 사용하지 않습니다.']) }}
{# {{ macros.setting_button([['global_setting_save_btn', '저장']]) }}#}
</form>
<script>
"use strict"
const package_name = '{{ arg["package_name"] }}'
</script>
<script src="{{ url_for('.static', filename='%s.js' % arg['template_name']) }}?ver={{ arg['package_version'] }}"></script>
{% endblock %}

View File

@@ -0,0 +1,49 @@
{% extends "base.html" %}
{% macro my_setting_select(id, title, options, col='9', desc=None, value=None) %}
{{ macros.setting_top(title) }}
<div class="input-group col-sm-{{ col }}">
<select id="{{ id }}" name="{{ id }}" class="form-control form-control-sm">
{% set ns = namespace(optgroup=none) %}
{% for item in options %}
{% if ns.optgroup != item[2] %}
{% if ns.optgroup is not none %}
</optgroup>
{% endif %}
{% if item[2] is not none %}
<optgroup label="{{ item[2] }}">
{% endif %}
{% set ns.optgroup = item[2] %}
{% endif %}
{% 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 %}
{% if ns.optgroup is not none %}
</optgroup>
{% endif %}
</select>
</div>
{{ macros.setting_bottom(desc) }}
{% endmacro %}
{% block content %}
<form id="download">
{{ macros.setting_input_text('url', 'URL', placeholder='http:// 주소', desc='유튜브, 네이버TV 등 동영상 주소') }}
{{ macros.setting_input_text('filename', '파일명', value=arg['filename'], desc='템플릿 규칙은 https://github.com/ytdl-org/youtube-dl/#output-template 참고') }}
{{ macros.setting_select('preset', '동영상 포맷 프리셋', arg['preset_list'], col='3') }}
{{ macros.setting_input_text('format', '동영상 포맷', desc=['포맷 지정은 https://github.com/ytdl-org/youtube-dl/#format-selection 참고', '빈칸으로 두면 최고 화질로 다운로드합니다.']) }}
{{ my_setting_select('postprocessor', '후처리', arg['postprocessor_list'], col='3', desc='다운로드 후 FFmpeg로 후처리합니다.') }}
{{ macros.setting_button([['download_btn', '다운로드']]) }}
</form>
<script>
"use strict";
const package_name = '{{ arg["package_name"] }}';
</script>
<script src="{{ url_for('.static', filename='%s.js' % arg['template_name']) }}?ver={{ arg['package_version'] }}"></script>
{% endblock %}

View File

@@ -2,7 +2,7 @@
<link <link
rel="stylesheet" rel="stylesheet"
href="{{ url_for('.static', filename='%s.css' % arg['template_name']) }}" href="{{ url_for('.static', filename='%s.css' % arg['template_name']) }}?ver={{ arg['package_version'] }}"
/> />
{{ macros.m_row_start() }} {{ macros.m_button('all_stop_btn', '전체 중지') }} {{ {{ macros.m_row_start() }} {{ macros.m_button('all_stop_btn', '전체 중지') }} {{
@@ -30,6 +30,6 @@ macros.m_row_end() }}
'use strict'; 'use strict';
const package_name = '{{ arg["package_name"] }}'; const package_name = '{{ arg["package_name"] }}';
</script> </script>
<script src="{{ url_for('.static', filename='%s.js' % arg['template_name']) }}"></script> <script src="{{ url_for('.static', filename='%s.js' % arg['template_name']) }}?ver={{ arg['package_version'] }}"></script>
{% endblock %} {% endblock %}

View File

@@ -29,6 +29,6 @@
'use strict'; 'use strict';
const package_name = '{{ arg["package_name"] }}'; const package_name = '{{ arg["package_name"] }}';
</script> </script>
<script src="{{ url_for('.static', filename='%s.js' % arg['template_name']) }}"></script> <script src="{{ url_for('.static', filename='%s.js' % arg['template_name']) }}?ver={{ arg['package_version'] }}"></script>
{% endblock %} {% endblock %}

View File

@@ -20,6 +20,6 @@
'use strict'; 'use strict';
const package_name = '{{ arg["package_name"] }}'; const package_name = '{{ arg["package_name"] }}';
</script> </script>
<script src="{{ url_for('.static', filename='%s.js' % arg['template_name']) }}"></script> <script src="{{ url_for('.static', filename='%s.js' % arg['template_name']) }}?ver={{ arg['package_version'] }}"></script>
{% endblock %} {% endblock %}

View File

@@ -13,6 +13,6 @@
'use strict'; 'use strict';
const package_name = '{{ arg["package_name"] }}'; const package_name = '{{ arg["package_name"] }}';
</script> </script>
<script src="{{ url_for('.static', filename='%s.js' % arg['template_name']) }}"></script> <script src="{{ url_for('.static', filename='%s.js' % arg['template_name']) }}?ver={{ arg['package_version'] }}"></script>
{% endblock %} {% endblock %}