From 3b157491c649f79f4c6d05373a9acbf411b0d1a9 Mon Sep 17 00:00:00 2001 From: joyfuI Date: Wed, 5 Feb 2020 20:54:30 +0900 Subject: [PATCH] =?UTF-8?q?v0.1.0=20=EC=B5=9C=EC=B4=88=20=EA=B3=B5?= =?UTF-8?q?=EA=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 15 ++- __init__.py | 2 + info.json | 1 + logic.py | 80 ++++++++++++++ model.py | 36 ++++++ plugin.py | 169 +++++++++++++++++++++++++++++ templates/youtube-dl_download.html | 43 ++++++++ templates/youtube-dl_list.html | 130 ++++++++++++++++++++++ templates/youtube-dl_setting.html | 80 ++++++++++++++ youtube_dl.py | 81 ++++++++++++++ 10 files changed, 635 insertions(+), 2 deletions(-) create mode 100644 __init__.py create mode 100644 info.json create mode 100644 logic.py create mode 100644 model.py create mode 100644 plugin.py create mode 100644 templates/youtube-dl_download.html create mode 100644 templates/youtube-dl_list.html create mode 100644 templates/youtube-dl_setting.html create mode 100644 youtube_dl.py diff --git a/README.md b/README.md index cd36202..0d25642 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,13 @@ -# youtube-dl - SJVA용 youtube-dl 플러그인 +# youtube-dl_sjva +[SJVA](https://sjva.me/) 용 [youtube-dl](https://ytdl-org.github.io/youtube-dl/) 플러그인입니다. +SJVA에서 유튜브 등 동영상 사이트 영상을 다운로드할 수 있습니다. + +## 잡담 +시놀로지 docker 환경에서 테스트했습니다. + +다른 분들이 만든 플러그인을 참고하며 주먹구구식으로 만들었기 때문에 손볼 곳이 많습니다. +일단 어느 정도 코드가 정리되면 그때 화질 선택 등 옵션을 추가할 예정 + +## Changelog +v0.1.0 +* 최초 공개 diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..f7cd4a1 --- /dev/null +++ b/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from plugin import blueprint, menu, plugin_load, plugin_unload, plugin_info diff --git a/info.json b/info.json new file mode 100644 index 0000000..1ecf634 --- /dev/null +++ b/info.json @@ -0,0 +1 @@ +{"more": "", "version": "0.1.0", "name": "youtube-dl", "developer": "joyfuI", "home": "https://github.com/joyfuI/youtube-dl", "description": "youtube-dl", "icon": "", "category_name": "vod"} \ No newline at end of file diff --git a/logic.py b/logic.py new file mode 100644 index 0000000..021adf0 --- /dev/null +++ b/logic.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +######################################################### +# python +import os +import traceback + +# third-party + +# sjva 공용 +from framework import db, path_data +from framework.util import Util + +# 패키지 +from .plugin import logger, package_name +from .model import ModelSetting + +######################################################### + +class Logic(object): + db_default = { + 'temp_path': os.path.join(path_data, 'download_tmp'), + 'save_path': os.path.join(path_data, 'download') + } + + @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() + 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) + # DB 초기화 + Logic.db_init() + + # 편의를 위해 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 setting_save(req): + try: + for key, value in req.form.items(): + 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_setting_value(key): + try: + return db.session.query(ModelSetting).filter_by(key=key).first().value + except Exception as e: + logger.error('Exception:%s', e) + logger.error(traceback.format_exc()) + +######################################################### diff --git a/model.py b/model.py new file mode 100644 index 0000000..13749a1 --- /dev/null +++ b/model.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +######################################################### +# python +import os + +# third-party + +# sjva 공용 +from framework import db, app, path_app_root + +# 패키지 +from .plugin import package_name + +db_file = os.path.join(path_app_root, 'data', 'db', '%s.db' % package_name) +app.config['SQLALCHEMY_BINDS'][package_name] = 'sqlite:///%s' % (db_file) + +class ModelSetting(db.Model): + __tablename__ = 'plugin_%s_setting' % package_name + __table_args__ = { 'mysql_collate': 'utf8_general_ci' } + __bind_key__ = package_name + + 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 } + +######################################################### diff --git a/plugin.py b/plugin.py new file mode 100644 index 0000000..8ca79be --- /dev/null +++ b/plugin.py @@ -0,0 +1,169 @@ +# -*- coding: utf-8 -*- +######################################################### +# python +import os +import traceback +import subprocess +from datetime import datetime + +# third-party +from flask import Blueprint, request, render_template, redirect, jsonify +from flask_login import login_required + +# sjva 공용 +from framework.logger import get_logger +from framework import db +from framework.util import Util + +# 로그 +package_name = __name__.split('.')[0] +logger = get_logger(package_name) + +# 패키지 +from .logic import Logic +from .model import ModelSetting +from .youtube_dl import Youtube_dl, Status + +######################################################### + +blueprint = Blueprint(package_name, package_name, url_prefix='/%s' % package_name, template_folder=os.path.join(os.path.dirname(__file__), 'templates')) + +def plugin_load(): + Logic.plugin_load() + +def plugin_unload(): + Logic.plugin_unload() + +plugin_info = { + 'version': '0.1.0', + 'name': 'youtube-dl', + 'category_name': 'vod', + 'icon': '', + 'developer': 'joyfuI', + 'description': 'youtube-dl', + 'home': 'https://github.com/joyfuI/youtube-dl', + 'more': '' +} + +# 메뉴 구성 +menu = { + 'main': [package_name, 'youtube-dl'], + 'sub': [ + ['setting', '설정'], ['download', '다운로드'], ['list', '목록'], ['log', '로그'] + ], + 'category': 'vod' +} + +######################################################### + +youtube_dl_list = [] + +######################################################### +# WEB Menu +######################################################### +@blueprint.route('/') +def home(): + return redirect('/%s/list' % package_name) + +@blueprint.route('/') +@login_required +def detail(sub): + try: + if sub == 'setting': + setting_list = db.session.query(ModelSetting).all() + arg = Util.db_list_to_dict(setting_list) + arg['package_name'] = package_name + arg['youtube_dl_path'] = 'youtube-dl' + return render_template('%s_setting.html' % (package_name), arg=arg) + + elif sub == 'download': + arg = { } + arg['package_name'] = package_name + arg['file_name'] = '%(title)s-%(id)s.%(ext)s' + return render_template('%s_download.html' % (package_name), arg=arg) + + elif sub == 'list': + arg = { } + arg['package_name'] = package_name + return render_template('%s_list.html' % (package_name), 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='%s - %s' % (package_name, sub)) + +######################################################### +# For UI +######################################################### +@blueprint.route('/ajax/', methods=['GET', 'POST']) +def ajax(sub): + logger.debug('AJAX %s %s', package_name, sub) + try: + if sub == 'setting_save': + ret = Logic.setting_save(request) + return jsonify(ret) + + elif sub == 'youtube_dl_version': + ret = subprocess.check_output(['youtube-dl', '--version']) + return jsonify(ret) + + elif sub == 'youtube_dl_update': + subprocess.call(['curl', '-L', 'https://yt-dl.org/downloads/latest/youtube-dl', '-o', '/usr/local/bin/youtube-dl']) + subprocess.call(['chmod', 'a+rx', '/usr/local/bin/youtube-dl']) + return jsonify([]) + + elif sub == 'download': + url = request.form['url'] + filename = request.form['filename'] + temp_path = Logic.get_setting_value('temp_path') + save_path = Logic.get_setting_value('save_path') + youtube_dl = Youtube_dl(url, filename, temp_path, save_path) + youtube_dl_list.append(youtube_dl) # 리스트 추가 + youtube_dl.start() + return jsonify([]) + + elif sub == 'list': + ret = [] + for i in youtube_dl_list: + data = { } + data['url'] = i.url + data['filename'] = i.filename + data['temp_path'] = i.temp_path + data['save_path'] = i.save_path + data['index'] = i.index + data['status_str'] = i.status.name + data['status_ko'] = str(i.status) + data['format'] = i.format + data['end_time'] = '' + if i.status == Status.READY: # 다운로드 전 + data['duration_str'] = '' + data['download_time'] = '' + data['start_time'] = '' + else: + data['duration_str'] = '%02d:%02d:%02d' % (i.duration / 60 / 60, i.duration / 60 % 60, i.duration % 60) + if i.end_time == None: # 완료 전 + download_time = datetime.now() - i.start_time + else: + download_time = i.end_time - i.start_time + data['end_time'] = i.end_time.strftime('%m-%d %H:%M:%S') + data['download_time'] = '%02d:%02d' % (download_time.seconds / 60, download_time.seconds % 60) + data['start_time'] = i.start_time.strftime('%m-%d %H:%M:%S') + ret.append(data) + return jsonify(ret) + + elif sub == 'stop': + index = int(request.form['index']) + youtube_dl_list[index].stop() + return jsonify([]) + except Exception as e: + logger.error('Exception:%s', e) + logger.error(traceback.format_exc()) + +######################################################### +# API +######################################################### +@blueprint.route('/api/', methods=['GET', 'POST']) +def api(sub): + logger.debug('api %s %s', package_name, sub) diff --git a/templates/youtube-dl_download.html b/templates/youtube-dl_download.html new file mode 100644 index 0000000..897d416 --- /dev/null +++ b/templates/youtube-dl_download.html @@ -0,0 +1,43 @@ +{% extends "base.html" %} +{% block content %} + +
+ {{ macros.setting_input_text('url', 'URL', placeholder='http:// 주소', desc='유튜브, 네이버TV 등 동영상 주소') }} + {{ macros.setting_input_text('filename', '파일명', value=arg['file_name']) }} + {{ macros.setting_button([['download_start', '다운로드']]) }} +
+ + + +{% endblock %} \ No newline at end of file diff --git a/templates/youtube-dl_list.html b/templates/youtube-dl_list.html new file mode 100644 index 0000000..b398236 --- /dev/null +++ b/templates/youtube-dl_list.html @@ -0,0 +1,130 @@ +{% extends "base.html" %} +{% block content %} + + + + + + + + + + + + + + + + +
IDX시작시간파일명상태길이진행시간Action
+ + + +{% endblock %} \ No newline at end of file diff --git a/templates/youtube-dl_setting.html b/templates/youtube-dl_setting.html new file mode 100644 index 0000000..c736ef0 --- /dev/null +++ b/templates/youtube-dl_setting.html @@ -0,0 +1,80 @@ +{% extends "base.html" %} +{% block content %} + +
+ {{ macros.setting_input_text_and_buttons('youtube_dl_path', 'youtube-dl 경로', [['youtube_dl_version', '버전확인']], value=arg['youtube_dl_path']) }} +
+ {{ macros.setting_input_text('temp_path', '임시 폴더', value=arg['temp_path'], placeholder='임시 폴더 경로', desc='다운로드 파일이 임시로 저장될 폴더 입니다.') }} + {{ macros.setting_input_text('save_path', '저장 폴더', value=arg['save_path'], placeholder='저장 폴더 경로', desc='정상적으로 완료된 파일이 이동할 폴더 입니다.') }} + {{ macros.setting_button([['setting_save', '저장']]) }} + {{ macros.m_hr() }} + {{ macros.setting_button([['youtube_dl_update', '업데이트']], left='youtube-dl 업데이트', desc=['혹시 정상적으로 동영상 주소를 입력했는데 다운로드에 실패한다면 업데이트를 해보세요.']) }} +
+
+ + + +{% endblock %} \ No newline at end of file diff --git a/youtube_dl.py b/youtube_dl.py new file mode 100644 index 0000000..4c75ea8 --- /dev/null +++ b/youtube_dl.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- +# python +import os +from threading import Thread +import subprocess +import json +from datetime import datetime +from enum import Enum + +# 패키지 +from .plugin import logger + +class Status(Enum): + READY = 0 + START = 1 + STOP = 2 + SUCCESS = 3 + FAILURE = 4 + + def __str__(self): + str_list = [ + '준비', + '다운로드중', + '중지', + '완료', + '실패' + ] + return str_list[self.value] + +class Youtube_dl(object): + _index = 0 + + def __init__(self, url, filename, temp_path, save_path): + self.url = url + self.filename = filename + self.temp_path = temp_path + self.save_path = save_path + self.index = Youtube_dl._index + Youtube_dl._index += 1 + self.status = Status.READY + self._thread = None + self._process = None + self.start_time = None + self.end_time = None + self.duration = None + self.format = None + self.errorlevel = None + + def start(self): + self._thread = Thread(target=self.run) + self.start_time = datetime.now() + self._thread.start() + + def run(self): + command = [ + 'youtube-dl', + '--print-json', + '-o', self.temp_path + '/' + self.filename, + '--exec', 'mv {} ' + self.save_path + '/', + self.url + ] + self._process = subprocess.Popen(command, stdout=subprocess.PIPE, universal_newlines=True) # youtube-dl 실행 + data = json.loads(self._process.stdout.readline()) # 파일 정보 + self.filename = data['_filename'].split('/')[-1] + self.duration = data['duration'] + self.format = data['format'] + self.status = Status.START + self.errorlevel = self._process.wait() # 실행 결과 + self.end_time = datetime.now() + if self.errorlevel == 0: # 다운로드 성공 + self.status = Status.SUCCESS + else: # 다운로드 실패 + logger.debug('returncode %d', self.errorlevel) + if self.status != Status.STOP: + self.status = Status.FAILURE + logger.debug('rm -f ' + self.temp_path + '/' + ''.join(str.split('.')[:-1]) + '*') + os.system('rm -f ' + self.temp_path + '/' + ''.join(str.split('.')[:-1]) + '*') # 임시 파일 삭제 + + def stop(self): + self.status = Status.STOP + self._process.terminate()