v1.2.0 API 추가

This commit is contained in:
joyfuI
2020-02-14 00:49:47 +09:00
parent 6581394a6d
commit 40ce3ece03
6 changed files with 257 additions and 9 deletions

115
README.md
View File

@@ -2,12 +2,127 @@
[SJVA](https://sjva.me/) 용 [youtube-dl](https://ytdl-org.github.io/youtube-dl/) 플러그인입니다. [SJVA](https://sjva.me/) 용 [youtube-dl](https://ytdl-org.github.io/youtube-dl/) 플러그인입니다.
SJVA에서 유튜브, 네이버TV 등 동영상 사이트 영상을 다운로드할 수 있습니다. SJVA에서 유튜브, 네이버TV 등 동영상 사이트 영상을 다운로드할 수 있습니다.
## 설치
SJVA에서 "시스템 → 플러그인 → 플러그인 수동 설치" 칸에 저장소 주소를 넣고 설치 버튼을 누르면 됩니다.
`https://github.com/joyfuI/youtube-dl`
## 잡담 ## 잡담
시놀로지 docker 환경에서 테스트했습니다. 시놀로지 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 ## Changelog
v1.2.0
* API 추가
이제 다른 플러그인에서 동영상 정보 가져오기, 다운로드 요청이 가능합니다.
자세한 명세는 API 문단을 참고하세요.
v1.1.1 v1.1.1
* 플레이리스트 다운로드 중 국가차단 등의 이유로 다운로드 실패한 동영상이 있으면 건너뛰도록 개선 * 플레이리스트 다운로드 중 국가차단 등의 이유로 다운로드 실패한 동영상이 있으면 건너뛰도록 개선

View File

@@ -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"} {"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"}

View File

@@ -7,6 +7,7 @@ import platform
from datetime import datetime from datetime import datetime
# third-party # third-party
from flask import jsonify
# sjva 공용 # sjva 공용
from framework import db, path_data from framework import db, path_data
@@ -108,6 +109,7 @@ class Logic(object):
def get_data(youtube_dl): def get_data(youtube_dl):
try: try:
data = { } data = { }
data['plugin'] = youtube_dl.plugin
data['url'] = youtube_dl.url data['url'] = youtube_dl.url
data['filename'] = youtube_dl.filename data['filename'] = youtube_dl.filename
data['temp_path'] = youtube_dl.temp_path data['temp_path'] = youtube_dl.temp_path
@@ -153,3 +155,8 @@ class Logic(object):
return '%3.1f %s%s' % (size, unit, suffix) return '%3.1f %s%s' % (size, unit, suffix)
size /= 1024.0 size /= 1024.0
return '%.1f %s%s' % (size, 'YB', suffix) return '%.1f %s%s' % (size, 'YB', suffix)
@staticmethod
def abort(base, code):
base['errorCode'] = code
return jsonify(base)

View File

@@ -43,7 +43,8 @@ class Youtube_dl(object):
_index = 0 _index = 0
_last_msg = '' _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.url = url
self.filename = filename self.filename = filename
self.temp_path = tempfile.mkdtemp(prefix='youtube-dl_', dir=temp_path) self.temp_path = tempfile.mkdtemp(prefix='youtube-dl_', dir=temp_path)
@@ -53,6 +54,7 @@ class Youtube_dl(object):
Youtube_dl._index += 1 Youtube_dl._index += 1
self.status = Status.READY self.status = Status.READY
self._thread = None self._thread = None
self._key = None
self.start_time = None # 시작 시간 self.start_time = None # 시작 시간
self.end_time = None # 종료 시간 self.end_time = None # 종료 시간
# info_dict에서 얻는 정보 # info_dict에서 얻는 정보
@@ -74,8 +76,11 @@ class Youtube_dl(object):
self.speed = None # 다운로드 속도(bytes/s) self.speed = None # 다운로드 속도(bytes/s)
def start(self): def start(self):
if self.status != Status.READY:
return False
self._thread = Thread(target=self.run) self._thread = Thread(target=self.run)
self._thread.start() self._thread.start()
return True
def run(self): def run(self):
try: try:
@@ -113,8 +118,11 @@ class Youtube_dl(object):
self.end_time = datetime.now() self.end_time = datetime.now()
def stop(self): def stop(self):
if self.status in (Status.ERROR, Status.STOP, Status.COMPLETED):
return False
self.status = Status.STOP self.status = Status.STOP
self.end_time = datetime.now() self.end_time = datetime.now()
return True
@staticmethod @staticmethod
def get_version(): def get_version():

124
plugin.py
View File

@@ -5,7 +5,7 @@ import os
import traceback import traceback
# third-party # 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 from flask_login import login_required
# sjva 공용 # sjva 공용
@@ -33,7 +33,7 @@ def plugin_unload():
Logic.plugin_unload() Logic.plugin_unload()
plugin_info = { plugin_info = {
'version': '1.1.1', 'version': '1.2.0',
'name': 'youtube-dl', 'name': 'youtube-dl',
'category_name': 'vod', 'category_name': 'vod',
'icon': '', 'icon': '',
@@ -107,7 +107,7 @@ def ajax(sub):
temp_path = Logic.get_setting_value('temp_path') temp_path = Logic.get_setting_value('temp_path')
save_path = Logic.get_setting_value('save_path') save_path = Logic.get_setting_value('save_path')
format_code = request.form['format'] if request.form['format'] else None 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) # 리스트 추가 Logic.youtube_dl_list.append(youtube_dl) # 리스트 추가
youtube_dl.start() youtube_dl.start()
return jsonify([]) return jsonify([])
@@ -131,6 +131,122 @@ def ajax(sub):
######################################################### #########################################################
# API # API
######################################################### #########################################################
# API 명세는 https://github.com/joyfuI/youtube-dl#api
@blueprint.route('/api/<sub>', methods=['GET', 'POST']) @blueprint.route('/api/<sub>', methods=['GET', 'POST'])
def api(sub): 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 에러(페이지 없음)

View File

@@ -24,9 +24,10 @@
<thead> <thead>
<tr> <tr>
<th style="width:5%">IDX</th> <th style="width:5%">IDX</th>
<th style="width:8%">Plugin</th>
<th style="width:10%">시작시간</th> <th style="width:10%">시작시간</th>
<th style="width:11%">타입</th> <th style="width:10%">타입</th>
<th style="width:35%">제목</th> <th style="width:28%">제목</th>
<th style="width:8%">상태</th> <th style="width:8%">상태</th>
<th style="width:15%">진행률</th> <th style="width:15%">진행률</th>
<th style="width:8%">진행시간</th> <th style="width:8%">진행시간</th>
@@ -78,6 +79,7 @@
function make_item(data) { function make_item(data) {
var str = '<tr style="cursor: pointer;" data-toggle="collapse" data-target="#collapse_' + data.index + '" aria-expanded="true">'; var str = '<tr style="cursor: pointer;" data-toggle="collapse" data-target="#collapse_' + data.index + '" aria-expanded="true">';
str += '<td scope="col">' + (data.index + 1) + '</td>'; str += '<td scope="col">' + (data.index + 1) + '</td>';
str += '<td scope="col">' + data.plugin + '</td>';
str += '<td scope="col">' + data.start_time + '</td>'; str += '<td scope="col">' + data.start_time + '</td>';
str += '<td scope="col">' + data.extractor + '</td>'; str += '<td scope="col">' + data.extractor + '</td>';
str += '<td scope="col">' + data.title + '</td>'; str += '<td scope="col">' + data.title + '</td>';