diff --git a/README.md b/README.md index 6278eab..d735c29 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,127 @@ [SJVA](https://sjva.me/) 용 [youtube-dl](https://ytdl-org.github.io/youtube-dl/) 플러그인입니다. SJVA에서 유튜브, 네이버TV 등 동영상 사이트 영상을 다운로드할 수 있습니다. +## 설치 +SJVA에서 "시스템 → 플러그인 → 플러그인 수동 설치" 칸에 저장소 주소를 넣고 설치 버튼을 누르면 됩니다. +`https://github.com/joyfuI/youtube-dl` + ## 잡담 시놀로지 docker 환경에서 테스트했습니다. -다른 분들이 만든 플러그인을 참고하며 주먹구구식으로 만들었습니다;; +다른 분들이 만든 플러그인을 참고하며 주먹구구식으로 만들었습니다;; + +드디어! API를 추가했습니다. 다른 플러그인에서 동영상 정보나 다운로드를 요청할 수 있습니다. +다른 플러그인이 멋대로 다운로드를 중지할 수 없도록 다운로드를 요청할 때 임의의 키를 넘겨 받습니다. 이 중지 요청 시 키가 일치해야 요청이 실행됩니다. +이걸로 뭔갈 만드실 분이 계실지... + +## API +### 공통사항 +모든 요청은 `POST`로만 받습니다. 그리고 응답은 `JSON` 형식입니다. +모든 요청엔 *플러그인 이름* 정보가 있어야 합니다. `plugin` 키에 담아서 보내면 됩니다. 만약 *플러그인 이름* 정보가 없으면 **403 에러**를 반환합니다. +요청을 처리하는 과정에서 예외가 발생하면 **500 에러**를 반환합니다. 이건 저한테 로그와 함께 알려주시면 됩니다. +모든 응답에는 `errorCode` 키가 있습니다. 코드의 의미는 아래 문단 참고 +#### 에러 코드 (errorCode) +* `0` - 성공. 문제없음 +* `1` - 필수 요청 변수가 없음 +* `2` - 잘못된 주소 +* `3` - 인덱스 범위를 벗어남 +* `4` - 키가 일치하지 않음 +* `10` - 실패. 요청은 성공하였으나 실행 결과가 실패 +#### Status 타입 +상태를 나타냄 +* "`READY`" - 준비 +* "`START`" - 분석중 +* "`DOWNLOADING`" - 다운로드중 +* "`ERROR`" - 실패 +* "`FINISHED`" - 변환중 +* "`STOP`" - 중지 +* "`COMPLETED`" - 완료 + +### /youtube-dl/api/info_dict +동영상 정보를 반환하는 API +#### Request +키 | 설명 | 필수 | 타입 +--- | --- | --- | --- +`plugin` | 플러그인 이름 | O | String +`url` | 동영상 주소 | O | String +#### Response +키 | 설명 | 타입 +--- | --- | --- +`errorCode` | 에러 코드 | Integer +`info_dict` | 동영상 정보 | Object +동영상 정보(`info_dict` 키)에는 youtube-dl에서 생성한 info_dict 정보가 그대로 들어있습니다. 따라서 이 부분은 직접 주소를 넣어가며 반환되는 정보를 확인해보는게 좋습니다. +간단한 예로 `thumbnail` 키엔 썸네일 주소, `uploader` 키엔 업로더 이름, `title` 키엔 동영상 제목, `duration` 키엔 동영상 길이 등이 들어 있습니다. +그리고 만약 주소가 플레이리스트라면 `_type` 키에 "`playlist`"라는 값이 들어 있습니다. 이때는 `entries` 키에 리스트가 들어있어 동영상들의 제목과 ID를 확인할 수 있습니다. + +### /youtube-dl/api/download +다운로드 준비를 요청하는 API +#### Request +키 | 설명 | 필수 | 타입 +--- | --- | --- | --- +`plugin` | 플러그인 이름 | O | String +`key` | 임의의 키. 이후 다운로드를 제어할 때 이 키가 필요함 | O | String +`url` | 동영상 주소 | O | String +`filename` | 파일명. 템플릿 규칙은 https://github.com/ytdl-org/youtube-dl/blob/master/README.md#output-template 참고 | O | String +`temp_path` | 임시 폴더 경로 | O | String +`save_path` | 저장 폴더 경로 | O | String +`format_code` | 동영상 포맷. 포맷 지정은 https://github.com/ytdl-org/youtube-dl/blob/master/README.md#format-selection 참고. 지정하지 않으면 최고 화질로 다운로드됨 | X | String +`start` | 다운로드 준비 후 바로 다운로드를 시작할지 여부. 기본값: false | X | Boolean +#### Response +키 | 설명 | 타입 +--- | --- | --- +`errorCode` | 에러 코드 | Integer +`index` | 동영상 인덱스. 이후 다운로드를 제어할 때 이 값이 필요함 | Integer + +### /youtube-dl/api/start +다운로드 시작을 요청하는 API +#### Request +키 | 설명 | 필수 | 타입 +--- | --- | --- | --- +`plugin` | 플러그인 이름 | O | String +`index` | 제어할 동영상의 인덱스 | O | Integer +`key` | 제어할 동영상에게 넘겨준 키. 이 값이 일치해야 요청이 실행됨 | O | String +#### Response +키 | 설명 | 타입 +--- | --- | --- +`errorCode` | 에러 코드 | Integer +`status` | 요청을 받았을 당시의 상태 | Status + +### /youtube-dl/api/stop +다운로드 중지를 요청하는 API +#### Request +키 | 설명 | 필수 | 타입 +--- | --- | --- | --- +`plugin` | 플러그인 이름 | O | String +`index` | 제어할 동영상의 인덱스 | O | Integer +`key` | 제어할 동영상에게 넘겨준 키. 이 값이 일치해야 요청이 실행됨 | O | String +#### Response +키 | 설명 | 타입 +--- | --- | --- +`errorCode` | 에러 코드 | Integer +`status` | 요청을 받았을 당시의 상태 | Status + +### /youtube-dl/api/status +현재 상태를 반환하는 API +#### Request +키 | 설명 | 필수 | 타입 +--- | --- | --- | --- +`plugin` | 플러그인 이름 | O | String +`index` | 제어할 동영상의 인덱스 | O | Integer +`key` | 제어할 동영상에게 넘겨준 키. 이 값이 일치해야 요청이 실행됨 | O | String +#### Response +키 | 설명 | 타입 +--- | --- | --- +`errorCode` | 에러 코드 | Integer +`status` | 요청을 받았을 당시의 상태 | Status +`start_time` | 다운로드 시작 시간 | Boolean +`end_time` | 다운로드 종료 시간 | Status or null ## Changelog +v1.2.0 +* API 추가 + 이제 다른 플러그인에서 동영상 정보 가져오기, 다운로드 요청이 가능합니다. + 자세한 명세는 API 문단을 참고하세요. + v1.1.1 * 플레이리스트 다운로드 중 국가차단 등의 이유로 다운로드 실패한 동영상이 있으면 건너뛰도록 개선 diff --git a/info.json b/info.json index 04fc340..f224e8d 100644 --- a/info.json +++ b/info.json @@ -1 +1 @@ -{"more": "", "version": "1.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.2.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 3216845..29eaaca 100644 --- a/logic.py +++ b/logic.py @@ -7,6 +7,7 @@ import platform from datetime import datetime # third-party +from flask import jsonify # sjva 공용 from framework import db, path_data @@ -108,6 +109,7 @@ class Logic(object): def get_data(youtube_dl): try: data = { } + data['plugin'] = youtube_dl.plugin data['url'] = youtube_dl.url data['filename'] = youtube_dl.filename data['temp_path'] = youtube_dl.temp_path @@ -153,3 +155,8 @@ class Logic(object): return '%3.1f %s%s' % (size, unit, suffix) size /= 1024.0 return '%.1f %s%s' % (size, 'YB', suffix) + + @staticmethod + def abort(base, code): + base['errorCode'] = code + return jsonify(base) diff --git a/my_youtube_dl.py b/my_youtube_dl.py index a1e437a..2d0085c 100644 --- a/my_youtube_dl.py +++ b/my_youtube_dl.py @@ -43,7 +43,8 @@ class Youtube_dl(object): _index = 0 _last_msg = '' - def __init__(self, url, filename, temp_path, save_path, format_code=None): + def __init__(self, plugin, url, filename, temp_path, save_path, format_code=None): + self.plugin = plugin self.url = url self.filename = filename self.temp_path = tempfile.mkdtemp(prefix='youtube-dl_', dir=temp_path) @@ -53,6 +54,7 @@ class Youtube_dl(object): Youtube_dl._index += 1 self.status = Status.READY self._thread = None + self._key = None self.start_time = None # 시작 시간 self.end_time = None # 종료 시간 # info_dict에서 얻는 정보 @@ -74,8 +76,11 @@ class Youtube_dl(object): self.speed = None # 다운로드 속도(bytes/s) def start(self): + if self.status != Status.READY: + return False self._thread = Thread(target=self.run) self._thread.start() + return True def run(self): try: @@ -113,8 +118,11 @@ class Youtube_dl(object): self.end_time = datetime.now() def stop(self): + if self.status in (Status.ERROR, Status.STOP, Status.COMPLETED): + return False self.status = Status.STOP self.end_time = datetime.now() + return True @staticmethod def get_version(): diff --git a/plugin.py b/plugin.py index 327928d..0abf30f 100644 --- a/plugin.py +++ b/plugin.py @@ -5,7 +5,7 @@ import os import traceback # third-party -from flask import Blueprint, request, render_template, redirect, jsonify +from flask import Blueprint, request, render_template, redirect, jsonify, abort from flask_login import login_required # sjva 공용 @@ -33,7 +33,7 @@ def plugin_unload(): Logic.plugin_unload() plugin_info = { - 'version': '1.1.1', + 'version': '1.2.0', 'name': 'youtube-dl', 'category_name': 'vod', 'icon': '', @@ -107,7 +107,7 @@ def ajax(sub): temp_path = Logic.get_setting_value('temp_path') save_path = Logic.get_setting_value('save_path') format_code = request.form['format'] if request.form['format'] else None - youtube_dl = Youtube_dl(url, filename, temp_path, save_path, format_code) + youtube_dl = Youtube_dl(package_name, url, filename, temp_path, save_path, format_code) Logic.youtube_dl_list.append(youtube_dl) # 리스트 추가 youtube_dl.start() return jsonify([]) @@ -131,6 +131,122 @@ def ajax(sub): ######################################################### # API ######################################################### +# API 명세는 https://github.com/joyfuI/youtube-dl#api @blueprint.route('/api/', methods=['GET', 'POST']) def api(sub): - logger.debug('api %s %s', package_name, sub) + plugin = request.form.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.form.get('url') + ret = { + 'errorCode': 0, + 'info_dict': None + } + if None == url: + return Logic.abort(ret, 1) # 필수 요청 변수가 없음 + if not url.startswith('http'): + return Logic.abort(ret, 2) # 잘못된 주소 + info_dict = Youtube_dl.get_info_dict(url) + if info_dict is None: + return Logic.abort(ret, 10) # 실패 + ret['info_dict'] = info_dict + return jsonify(ret) + + # 다운로드 준비를 요청하는 API + elif sub == 'download': + key = request.form.get('key') + url = request.form.get('url') + filename = request.form.get('filename') + temp_path = request.form.get('temp_path') + save_path = request.form.get('save_path') + format_code = request.form.get('format_code', None) + start = request.form.get('start', False) + ret = { + 'errorCode': 0, + 'index': None + } + if None in (key, url, filename, temp_path, save_path): + return Logic.abort(ret, 1) # 필수 요청 변수가 없음 + if not url.startswith('http'): + return Logic.abort(ret, 2) # 잘못된 주소 + youtube_dl = Youtube_dl(plugin, url, filename, temp_path, save_path, format_code) + youtube_dl._key = key + Logic.youtube_dl_list.append(youtube_dl) # 리스트 추가 + ret['index'] = youtube_dl.index + if start: + youtube_dl.start() + return jsonify(ret) + + # 다운로드 시작을 요청하는 API + elif sub == 'start': + index = request.form.get('index') + key = request.form.get('key') + ret = { + 'errorCode': 0, + 'status': None + } + if None in (index, key): + return Logic.abort(ret, 1) # 필수 요청 변수가 없음 + index = int(index) + if not (0 <= index and index < Youtube_dl._index): + return Logic.abort(ret, 3) # 인덱스 범위를 벗어남 + youtube_dl = Logic.youtube_dl_list[index] + if youtube_dl._key != key: + return Logic.abort(ret, 4) # 키가 일치하지 않음 + ret['status'] = youtube_dl.status.name + if not youtube_dl.start(): + return Logic.abort(ret, 10) # 실패 + return jsonify(ret) + + # 다운로드 중지를 요청하는 API + elif sub == 'stop': + index = request.form.get('index') + key = request.form.get('key') + ret = { + 'errorCode': 0, + 'status': None + } + if None in (index, key): + return Logic.abort(ret, 1) # 필수 요청 변수가 없음 + index = int(index) + if not (0 <= index and index < Youtube_dl._index): + return Logic.abort(ret, 3) # 인덱스 범위를 벗어남 + youtube_dl = Logic.youtube_dl_list[index] + if youtube_dl._key != key: + return Logic.abort(ret, 4) # 키가 일치하지 않음 + ret['status'] = youtube_dl.status.name + if not youtube_dl.stop(): + return Logic.abort(ret, 10) # 실패 + return jsonify(ret) + + # 현재 상태를 반환하는 API + elif sub == 'status': + index = request.form.get('index') + key = request.form.get('key') + ret = { + 'errorCode': 0, + 'status': None, + 'start_time': None, + 'end_time': None + } + if None in (index, key): + return Logic.abort(ret, 1) # 필수 요청 변수가 없음 + index = int(index) + if not (0 <= index and index < Youtube_dl._index): + return Logic.abort(ret, 3) # 인덱스 범위를 벗어남 + youtube_dl = Logic.youtube_dl_list[index] + if youtube_dl._key != key: + return Logic.abort(ret, 4) # 키가 일치하지 않음 + ret['status'] = youtube_dl.status.name + ret['start_time'] = youtube_dl.start_time + ret['end_time'] = youtube_dl.end_time + return jsonify(ret) + except Exception as e: + logger.error('Exception:%s', e) + logger.error(traceback.format_exc()) + abort(500) # 500 에러(서버 오류) + abort(404) # 404 에러(페이지 없음) diff --git a/templates/youtube-dl_list.html b/templates/youtube-dl_list.html index c7cd5f9..aa07530 100644 --- a/templates/youtube-dl_list.html +++ b/templates/youtube-dl_list.html @@ -24,9 +24,10 @@ IDX + Plugin 시작시간 - 타입 - 제목 + 타입 + 제목 상태 진행률 진행시간 @@ -78,6 +79,7 @@ function make_item(data) { var str = ''; str += '' + (data.index + 1) + ''; + str += '' + data.plugin + ''; str += '' + data.start_time + ''; str += '' + data.extractor + ''; str += '' + data.title + '';