diff --git a/README.md b/README.md index 741f8d3..e4617c6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # youtube-dl_sjva [SJVA](https://sjva.me/) 용 [youtube-dl](https://ytdl-org.github.io/youtube-dl/) 플러그인입니다. -SJVA에서 유튜브 등 동영상 사이트 영상을 다운로드할 수 있습니다. +SJVA에서 유튜브, 네이버TV 등 동영상 사이트 영상을 다운로드할 수 있습니다. ## 잡담 시놀로지 docker 환경에서 테스트했습니다. @@ -9,8 +9,13 @@ SJVA에서 유튜브 등 동영상 사이트 영상을 다운로드할 수 있 일단 어느 정도 코드가 정리되면 그때 화질 선택 등 옵션을 추가할 예정 ## Changelog +v1.0.0 +* 바이너리 실행 방식에서 파이썬 임베딩 방식으로 변경 +* SJVA 시작 시 자동으로 youtube-dl 업데이트 +* 목록에서 진행률 표시 추가 + v0.1.1 -* 윈도우 환경 지원 추가 +* ~~윈도우 환경 지원 추가~~ * 다운로드 실패 시 임시파일 삭제가 안 되는 문제 수정 v0.1.0 diff --git a/info.json b/info.json index 5326809..13cf548 100644 --- a/info.json +++ b/info.json @@ -1 +1 @@ -{"more": "", "version": "0.1.1", "name": "youtube-dl", "developer": "joyfuI", "home": "https://github.com/joyfuI/youtube-dl", "description": "\uc720\ud29c\ube0c, \ub124\uc774\ubc84TV \ub4f1 \ub3d9\uc601\uc0c1 \uc0ac\uc774\ud2b8\uc5d0\uc11c \ub3d9\uc601\uc0c1 \ub2e4\uc6b4\ub85c\ub4dc", "icon": "", "category_name": "vod"} \ No newline at end of file +{"more": "", "version": "1.0.0", "name": "youtube-dl", "developer": "joyfuI", "home": "https://github.com/joyfuI/youtube-dl", "description": "\uc720\ud29c\ube0c, \ub124\uc774\ubc84TV \ub4f1 \ub3d9\uc601\uc0c1 \uc0ac\uc774\ud2b8\uc5d0\uc11c \ub3d9\uc601\uc0c1 \ub2e4\uc6b4\ub85c\ub4dc", "icon": "", "category_name": "vod"} \ No newline at end of file diff --git a/logic.py b/logic.py index 87bcd45..8976aaf 100644 --- a/logic.py +++ b/logic.py @@ -4,17 +4,18 @@ import os import traceback import platform -import subprocess +from datetime import datetime # third-party # sjva 공용 -from framework import db, path_app_root, path_data +from framework import db, path_data from framework.util import Util # 패키지 from .plugin import logger, package_name from .model import ModelSetting +from .my_youtube_dl import Status ######################################################### @@ -31,7 +32,7 @@ class Logic(object): if db.session.query(ModelSetting).filter_by(key=key).count() == 0: db.session.add(ModelSetting(key, value)) db.session.commit() - except Exception as e: + except Exception as e: logger.error('Exception:%s', e) logger.error(traceback.format_exc()) @@ -40,10 +41,12 @@ class Logic(object): try: logger.debug('%s plugin_load', package_name) Logic.db_init() # DB 초기화 + + # youtube-dl 업데이트 if platform.system() == 'Windows': # 윈도우일 때 - Logic.youtube_dl_path = os.path.join(path_app_root, 'bin', 'Windows', 'youtube-dl.exe') - if not os.path.isfile(Logic.youtube_dl_path): # youtube-dl.exe가 없으면 - Logic.youtube_dl_update() + os.system('pip.exe install --upgrade youtube-dl') + else: + os.system('pip install --upgrade youtube-dl') # 편의를 위해 json 파일 생성 from plugin import plugin_info @@ -56,7 +59,7 @@ class Logic(object): def plugin_unload(): try: logger.debug('%s plugin_unload', package_name) - except Exception as e: + except Exception as e: logger.error('Exception:%s', e) logger.error(traceback.format_exc()) @@ -69,7 +72,7 @@ class Logic(object): entity.value = value db.session.commit() return True - except Exception as e: + except Exception as e: logger.error('Exception:%s', e) logger.error(traceback.format_exc()) return False @@ -78,19 +81,60 @@ class Logic(object): def get_setting_value(key): try: return db.session.query(ModelSetting).filter_by(key=key).first().value - except Exception as e: + except Exception as e: logger.error('Exception:%s', e) logger.error(traceback.format_exc()) ######################################################### - youtube_dl_path = 'youtube-dl' youtube_dl_list = [] @staticmethod - def youtube_dl_update(): - if platform.system() == 'Windows': # 윈도우일 때 - subprocess.call(['powershell', "(new-Object System.Net.WebClient).DownloadFile('https://yt-dl.org/latest/youtube-dl.exe', '%s')" % os.path.join(path_app_root, 'bin', 'Windows', 'youtube-dl.exe')]) - else: # 나머지 Unix-like - 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']) + def get_data(youtube_dl): + try: + data = { } + data['url'] = youtube_dl.url + data['filename'] = youtube_dl.filename + data['temp_path'] = youtube_dl.temp_path + data['save_path'] = youtube_dl.save_path + data['index'] = youtube_dl.index + data['status_str'] = youtube_dl.status.name + data['status_ko'] = str(youtube_dl.status) + data['end_time'] = '' + data['extractor'] = youtube_dl.extractor if youtube_dl.extractor is not None else '' + data['title'] = youtube_dl.title if youtube_dl.title is not None else youtube_dl.url + data['uploader'] = youtube_dl.uploader if youtube_dl.uploader is not None else '' + data['uploader_url'] = youtube_dl.uploader_url if youtube_dl.uploader_url is not None else '' + data['downloaded_bytes_str'] = '' + data['total_bytes_str'] = '' + data['percent'] = '0' + data['eta'] = youtube_dl.eta if youtube_dl.eta is not None else '' + data['speed_str'] = Logic.human_readable_size(youtube_dl.speed, '/s') if youtube_dl.speed is not None else '' + if youtube_dl.status == Status.READY: # 다운로드 전 + data['start_time'] = '' + data['download_time'] = '' + else: + if youtube_dl.end_time is None: # 완료 전 + download_time = datetime.now() - youtube_dl.start_time + else: + download_time = youtube_dl.end_time - youtube_dl.start_time + data['end_time'] = youtube_dl.end_time.strftime('%m-%d %H:%M:%S') + if None not in (youtube_dl.downloaded_bytes, youtube_dl.total_bytes): # 둘 다 값이 있으면 + data['downloaded_bytes_str'] = Logic.human_readable_size(youtube_dl.downloaded_bytes) + data['total_bytes_str'] = Logic.human_readable_size(youtube_dl.total_bytes) + data['percent'] = '%.2f' % (float(youtube_dl.downloaded_bytes) / float(youtube_dl.total_bytes) * 100) + data['start_time'] = youtube_dl.start_time.strftime('%m-%d %H:%M:%S') + data['download_time'] = '%02d:%02d' % (download_time.seconds / 60, download_time.seconds % 60) + return data + except Exception as e: + logger.error('Exception:%s', e) + logger.error(traceback.format_exc()) + return None + + @staticmethod + def human_readable_size(size, suffix=''): + for unit in ['Bytes','KB','MB','GB','TB','PB','EB','ZB']: + if size < 1024.0: + return '%3.1f %s%s' % (size, unit, suffix) + size /= 1024.0 + return '%.1f %s%s' % (size, 'YB', suffix) diff --git a/my_youtube_dl.py b/my_youtube_dl.py new file mode 100644 index 0000000..57283fb --- /dev/null +++ b/my_youtube_dl.py @@ -0,0 +1,158 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +# python +import os +import shutil +import tempfile +import glob +from threading import Thread +import json +from datetime import datetime +from enum import Enum + +# third-party +import youtube_dl + +# 패키지 +from .plugin import logger + +class Status(Enum): + READY = 0 + START = 1 + DOWNLOADING = 2 + ERROR = 3 + FINISHED = 4 + STOP = 5 + COMPLETED = 6 + + def __str__(self): + str_list = [ + '준비', + '분석중', + '다운로드중', + '실패', + '변환중', + '중지', + '완료' + ] + return str_list[self.value] + +class Youtube_dl(object): + _index = 0 + _last_msg = '' + + def __init__(self, url, filename, temp_path, save_path): + self.url = url + self.filename = filename + self.temp_path = tempfile.mkdtemp(prefix='youtube-dl_', dir=temp_path) + self.save_path = save_path + self.index = Youtube_dl._index + Youtube_dl._index += 1 + self.status = Status.READY + self._thread = None + self.start_time = None # 시작 시간 + self.end_time = None # 종료 시간 + # info_dict에서 얻는 정보 + self.extractor = None # 타입 + self.title = None # 제목 + self.uploader = None # 업로더 + self.uploader_url = None # 업로더 주소 + # info_dict에서 얻는 정보(entries) + """ + self.playlist_index = None + self.duration = None # 길이 + self.format = None # 포맷 + self.thumbnail = None # 썸네일 + """ + # progress_hooks에서 얻는 정보 + self.downloaded_bytes = None # 다운로드한 크기 + self.total_bytes = None # 전체 크기 + self.eta = None # 예상 시간(s) + self.speed = None # 다운로드 속도(bytes/s) + + def start(self): + self._thread = Thread(target=self.run) + self.start_time = datetime.now() + self._thread.start() + self.status = Status.START + + def run(self): + info_dict = Youtube_dl.get_info_dict(self.url) # 동영상 정보 가져오기 + if info_dict is None: # 가져오기 실패 + self.status = Status.ERROR + return + self.extractor = info_dict['extractor'] + self.title = info_dict['title'] + self.uploader = info_dict['uploader'] + self.uploader_url = info_dict['uploader_url'] + ydl_opts = { + 'logger': MyLogger(), + 'progress_hooks': [self.my_hook], + # 'match_filter': self.match_filter_func, + 'outtmpl': os.path.join(self.temp_path, self.filename) + } + with youtube_dl.YoutubeDL(ydl_opts) as ydl: + ydl.download([self.url]) + if self.status == Status.FINISHED: # 다운로드 성공 + for i in glob.glob(self.temp_path + '/*'): + shutil.move(i, self.save_path) # 파일 이동 + self.status = Status.COMPLETED + shutil.rmtree(self.temp_path) # 임시폴더 삭제 + self.end_time = datetime.now() + + def stop(self): + self.status = Status.STOP + self.end_time = datetime.now() + + @staticmethod + def get_version(): + return youtube_dl.version.__version__ + + @staticmethod + def get_info_dict(url): + try: + ydl_opts = { + 'simulate': True, + 'dump_single_json': True, + 'logger': MyLogger() + } + with youtube_dl.YoutubeDL(ydl_opts) as ydl: + ydl.download([url]) + except Exception as e: + return None + return json.loads(Youtube_dl._last_msg) + + def my_hook(self, d): + if self.status != Status.STOP: + self.status = { + 'downloading': Status.DOWNLOADING, + 'error': Status.ERROR, + 'finished': Status.FINISHED # 다운로드 완료. 변환 시작 + }[d['status']] + if d['status'] != 'error': + self.filename = os.path.basename(d.get('filename')) + self.downloaded_bytes = d.get('downloaded_bytes') + self.total_bytes = d.get('total_bytes') + self.eta = d.get('eta') + self.speed = d.get('speed') + + def match_filter_func(self, info_dict): + self.playlist_index = info_dict['playlist_index'] + self.duration = info_dict['duration'] + self.format = info_dict['format'] + self.thumbnail = info_dict['thumbnail'] + return None + +class MyLogger(object): + def debug(self, msg): + Youtube_dl._last_msg = msg + if msg.find('') != -1 or msg.find('{') != -1: + return # 과도한 로그 방지 + logger.debug(msg) + + def warning(self, msg): + logger.warning(msg) + + def error(self, msg): + logger.error(msg) diff --git a/plugin.py b/plugin.py index f9d127c..ef61def 100644 --- a/plugin.py +++ b/plugin.py @@ -3,8 +3,6 @@ # python import os import traceback -import subprocess -from datetime import datetime # third-party from flask import Blueprint, request, render_template, redirect, jsonify @@ -22,7 +20,7 @@ logger = get_logger(package_name) # 패키지 from .logic import Logic from .model import ModelSetting -from .youtube_dl import Youtube_dl, Status +from .my_youtube_dl import Youtube_dl ######################################################### @@ -35,7 +33,7 @@ def plugin_unload(): Logic.plugin_unload() plugin_info = { - 'version': '0.1.1', + 'version': '1.0.0', 'name': 'youtube-dl', 'category_name': 'vod', 'icon': '', @@ -69,7 +67,7 @@ def detail(sub): setting_list = db.session.query(ModelSetting).all() arg = Util.db_list_to_dict(setting_list) arg['package_name'] = package_name - arg['youtube_dl_path'] = Logic.youtube_dl_path + arg['youtube_dl_version'] = Youtube_dl.get_version() return render_template('%s_setting.html' % package_name, arg=arg) elif sub == 'download': @@ -101,14 +99,6 @@ def ajax(sub): ret = Logic.setting_save(request) return jsonify(ret) - elif sub == 'youtube_dl_version': - ret = subprocess.check_output([Logic.youtube_dl_path, '--version']) - return jsonify(ret) - - elif sub == 'youtube_dl_update': - Logic.youtube_dl_update() - return jsonify([]) - elif sub == 'download': url = request.form['url'] filename = request.form['filename'] @@ -122,30 +112,9 @@ def ajax(sub): elif sub == 'list': ret = [] for i in Logic.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) + data = Logic.get_data(i) + if data is not None: + ret.append(data) return jsonify(ret) elif sub == 'stop': diff --git a/templates/youtube-dl_list.html b/templates/youtube-dl_list.html index b398236..c7cd5f9 100644 --- a/templates/youtube-dl_list.html +++ b/templates/youtube-dl_list.html @@ -25,9 +25,10 @@