Add DownloadStation (#62)

* Add DownloadStation

* Add test

* Add resume, fix create

* Add get_config, create destination

* Added readme
This commit is contained in:
Quentame
2020-09-07 23:26:41 +02:00
committed by GitHub
parent 27379e7cd6
commit ca56dccee9
10 changed files with 589 additions and 32 deletions

View File

@ -126,6 +126,34 @@ The ``SynologyDSM`` class can also ``update()`` all APIs at once.
print("Space used: " + str(api.share.share_size(share_uuid, human_readable=True)))
print("Recycle Bin Enabled: " + str(api.share.share_recycle_bin(share_uuid)))
print("--")
Download Station usage
--------------------------
.. code-block:: python
from synology_dsm import SynologyDSM
api = SynologyDSM("<IP/DNS>", "<port>", "<username>", "<password>")
if "SYNO.DownloadStation.Info" in api.apis:
api.download_station.get_info()
api.download_station.get_config()
# The download list will be updated after each of the following functions:
# You should have the right on the (default) directory that the download will be saved, or you will get a 403 or 406 error
api.download_station.create("http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4")
api.download_station.pause("dbid_1")
# Like the other function, you can eather pass a str or a list
api.download_station.resume(["dbid_1", "dbid_2"])
api.download_station.delete("dbid_3")
# Manual update
api.download_station.update()
Surveillance Station usage
--------------------------

View File

@ -0,0 +1,95 @@
"""Synology DownloadStation API wrapper."""
from .task import SynoDownloadTask
class SynoDownloadStation(object):
"""An implementation of a Synology DownloadStation."""
API_KEY = "SYNO.DownloadStation.*"
INFO_API_KEY = "SYNO.DownloadStation.Info"
TASK_API_KEY = "SYNO.DownloadStation.Task"
def __init__(self, dsm):
"""Initialize a Download Station."""
self._dsm = dsm
self._tasks_by_id = {}
self.additionals = [
"detail",
"file",
] # Can contain: detail, transfer, file, tracker, peer
def update(self):
"""Update tasks from API."""
list_data = self._dsm.get(
self.TASK_API_KEY, "List", {"additional": ",".join(self.additionals)}
)["data"]
for task_data in list_data["tasks"]:
if task_data["id"] in self._tasks_by_id:
self._tasks_by_id[task_data["id"]].update(task_data)
else:
self._tasks_by_id[task_data["id"]] = SynoDownloadTask(task_data)
# Global
def get_info(self):
"""Return general informations about the Download Station instance."""
return self._dsm.get(self.INFO_API_KEY, "GetInfo")
def get_config(self):
"""Return configuration about the Download Station instance."""
return self._dsm.get(self.INFO_API_KEY, "GetConfig")
# Downloads
def get_all_tasks(self):
"""Return a list of tasks."""
return self._tasks_by_id.values()
def get_task(self, task_id):
"""Return task matching task_id."""
return self._tasks_by_id[task_id]
def create(self, uri, unzip_password=None, destination=None):
"""Create a new task (uri accepts HTTP/FTP/magnet/ED2K links)."""
res = self._dsm.post(
self.TASK_API_KEY,
"Create",
{
"uri": ",".join(uri) if isinstance(uri, list) else uri,
"unzip_password": unzip_password,
"destination": destination,
},
)
self.update()
return res
def pause(self, task_id):
"""Pause a download task."""
res = self._dsm.get(
self.TASK_API_KEY,
"Pause",
{"id": ",".join(task_id) if isinstance(task_id, list) else task_id},
)
self.update()
return res
def resume(self, task_id):
"""Resume a paused download task."""
res = self._dsm.get(
self.TASK_API_KEY,
"Resume",
{"id": ",".join(task_id) if isinstance(task_id, list) else task_id},
)
self.update()
return res
def delete(self, task_id, force_complete=False):
"""Delete a download task."""
res = self._dsm.get(
self.TASK_API_KEY,
"Delete",
{
"id": ",".join(task_id) if isinstance(task_id, list) else task_id,
"force_complete": force_complete,
},
)
self.update()
return res

View File

@ -0,0 +1,53 @@
"""DownloadStation task."""
class SynoDownloadTask(object):
"""An representation of a Synology DownloadStation task."""
def __init__(self, data):
"""Initialize a Download Station task."""
self._data = data
def update(self, data):
"""Update the task."""
self._data = data
@property
def id(self):
"""Return id of the task."""
return self._data["id"]
@property
def title(self):
"""Return title of the task."""
return self._data["title"]
@property
def type(self):
"""Return type of the task (bt, nzb, http(s), ftp, emule)."""
return self._data["type"]
@property
def username(self):
"""Return username of the task."""
return self._data["username"]
@property
def size(self):
"""Return size of the task."""
return self._data["size"]
@property
def status(self):
"""Return status of the task (waiting, downloading, paused, finishing, finished, hash_checking, seeding, filehosting_waiting, extracting, error)."""
return self._data["status"]
@property
def status_extra(self):
"""Return status_extra of the task."""
return self._data.get("status_extra")
@property
def additional(self):
"""Return additional data of the task."""
return self._data["additional"]

View File

@ -22,6 +22,7 @@ from .exceptions import (
from .api.core.security import SynoCoreSecurity
from .api.core.utilization import SynoCoreUtilization
from .api.core.share import SynoCoreShare
from .api.download_station import SynoDownloadStation
from .api.dsm.information import SynoDSMInformation
from .api.dsm.network import SynoDSMNetwork
from .api.storage.storage import SynoStorage
@ -70,6 +71,7 @@ class SynologyDSM(object):
self._apis = {
"SYNO.API.Info": {"maxVersion": 1, "minVersion": 1, "path": "query.cgi"}
}
self._download = None
self._information = None
self._network = None
self._security = None
@ -183,11 +185,9 @@ class SynologyDSM(object):
"""Handles API GET request."""
return self._request("GET", api, method, params, **kwargs)
def post(self, api, method, params=None, data=None, json=None, **kwargs):
def post(self, api, method, params=None, **kwargs):
"""Handles API POST request."""
return self._request(
"POST", api, method, params, data=data, json=json, **kwargs
)
return self._request("POST", api, method, params, **kwargs)
def _request(
self, request_method, api, method, params=None, retry_once=True, **kwargs
@ -227,20 +227,6 @@ class SynologyDSM(object):
url = self._build_url(api)
# If the request method is POST and the API is SynoCoreShare the params
# to the request body. Used to support the weird Syno use of POST
# to choose what fields to return. See ./api/core/share.py
# for an example.
if request_method == "POST" and api == SynoCoreShare.API_KEY:
body = {}
body.update(params)
body.update(kwargs.pop("data"))
body["mimeType"] = "application/json"
# Request data via POST (excluding FileStation file uploads)
self._debuglog("POST BODY: " + str(body))
kwargs["data"] = body
# Request data
response = self._execute_request(request_method, url, params, **kwargs)
self._debuglog("Request Method: " + request_method)
@ -279,6 +265,13 @@ class SynologyDSM(object):
url, params=encoded_params, timeout=self._timeout, **kwargs
)
elif method == "POST":
data = {}
data.update(params)
data.update(kwargs.pop("data", {}))
data["mimeType"] = "application/json"
kwargs["data"] = data
self._debuglog("POST data: " + str(data))
response = self._session.post(
url, params=params, timeout=self._timeout, **kwargs
)
@ -308,6 +301,9 @@ class SynologyDSM(object):
def update(self, with_information=False, with_network=False):
"""Updates the various instanced modules."""
if self._download:
self._download.update()
if self._information and with_information:
self._information.update()
@ -337,6 +333,9 @@ class SynologyDSM(object):
if hasattr(self, "_" + api):
setattr(self, "_" + api, None)
return True
if api == SynoDownloadStation.API_KEY:
self._download = None
return True
if api == SynoCoreSecurity.API_KEY:
self._security = None
return True
@ -352,6 +351,9 @@ class SynologyDSM(object):
if api == SynoSurveillanceStation.API_KEY:
self._surveillance = None
return True
if isinstance(api, SynoDownloadStation):
self._download = None
return True
if isinstance(api, SynoCoreSecurity):
self._security = None
return True
@ -370,6 +372,13 @@ class SynologyDSM(object):
return True
return False
@property
def download_station(self):
"""Gets NAS DownloadStation."""
if not self._download:
self._download = SynoDownloadStation(self)
return self._download
@property
def information(self):
"""Gets NAS informations."""

View File

@ -10,6 +10,7 @@ from synology_dsm.api.core.security import SynoCoreSecurity
from synology_dsm.api.core.utilization import SynoCoreUtilization
from synology_dsm.api.dsm.information import SynoDSMInformation
from synology_dsm.api.dsm.network import SynoDSMNetwork
from synology_dsm.api.download_station import SynoDownloadStation
from synology_dsm.api.storage.storage import SynoStorage
from synology_dsm.api.core.share import SynoCoreShare
from synology_dsm.api.surveillance_station import SynoSurveillanceStation
@ -44,6 +45,9 @@ from .api_data.dsm_6 import (
DSM_6_SURVEILLANCE_STATION_CAMERA_LIST,
DSM_6_SURVEILLANCE_STATION_HOME_MODE_GET_INFO,
DSM_6_SURVEILLANCE_STATION_HOME_MODE_SWITCH,
DSM_6_DOWNLOAD_STATION_INFO_INFO,
DSM_6_DOWNLOAD_STATION_INFO_CONFIG,
DSM_6_DOWNLOAD_STATION_TASK_LIST,
)
from .api_data.dsm_5 import (
DSM_5_API_INFO,
@ -193,24 +197,32 @@ class SynologyDSMMock(SynologyDSM):
if not self._session_id:
return ERROR_INSUFFICIENT_USER_PRIVILEGE
if SynoCoreSecurity.API_KEY in url:
if self.error:
return DSM_6_CORE_SECURITY_UPDATE_OUTOFDATE
return API_SWITCHER[self.dsm_version]["CORE_SECURITY"]
if SynoCoreShare.API_KEY in url:
return API_SWITCHER[self.dsm_version]["CORE_SHARE"]
if SynoCoreUtilization.API_KEY in url:
if self.error:
return DSM_6_CORE_UTILIZATION_ERROR_1055
return API_SWITCHER[self.dsm_version]["CORE_UTILIZATION"]
if SynoDSMInformation.API_KEY in url:
return API_SWITCHER[self.dsm_version]["DSM_INFORMATION"]
if SynoDSMNetwork.API_KEY in url:
return API_SWITCHER[self.dsm_version]["DSM_NETWORK"]
if SynoCoreShare.API_KEY in url:
return API_SWITCHER[self.dsm_version]["CORE_SHARE"]
if SynoCoreSecurity.API_KEY in url:
if self.error:
return DSM_6_CORE_SECURITY_UPDATE_OUTOFDATE
return API_SWITCHER[self.dsm_version]["CORE_SECURITY"]
if SynoCoreUtilization.API_KEY in url:
if self.error:
return DSM_6_CORE_UTILIZATION_ERROR_1055
return API_SWITCHER[self.dsm_version]["CORE_UTILIZATION"]
if SynoDownloadStation.TASK_API_KEY in url:
if "GetInfo" in url:
return DSM_6_DOWNLOAD_STATION_INFO_INFO
if "GetConfig" in url:
return DSM_6_DOWNLOAD_STATION_INFO_CONFIG
if "List" in url:
return DSM_6_DOWNLOAD_STATION_TASK_LIST
if SynoStorage.API_KEY in url:
return API_SWITCHER[self.dsm_version]["STORAGE_STORAGE"][

View File

@ -13,6 +13,13 @@ from .core.const_6_core_security import (
DSM_6_CORE_SECURITY,
DSM_6_CORE_SECURITY_UPDATE_OUTOFDATE,
)
from .download_station.const_6_download_station_info import (
DSM_6_DOWNLOAD_STATION_INFO_INFO,
DSM_6_DOWNLOAD_STATION_INFO_CONFIG,
)
from .download_station.const_6_download_station_task import (
DSM_6_DOWNLOAD_STATION_TASK_LIST,
)
from .dsm.const_6_dsm_info import DSM_6_DSM_INFORMATION
from .dsm.const_6_dsm_network import DSM_6_DSM_NETWORK
from .storage.const_6_storage_storage import (

View File

@ -0,0 +1 @@
"""DSM 6 SYNO.DownloadStation.* datas."""

View File

@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
"""DSM 6 SYNO.DownloadStation.Info data."""
DSM_6_DOWNLOAD_STATION_INFO_INFO = {
"data": {"is_manager": True, "version": 3543, "version_string": "3.8-3543"},
"success": True,
}
DSM_6_DOWNLOAD_STATION_INFO_CONFIG = {
"data": {
"bt_max_download": 0,
"bt_max_upload": 800,
"default_destination": "downloads",
"emule_default_destination": None,
"emule_enabled": False,
"emule_max_download": 0,
"emule_max_upload": 20,
"ftp_max_download": 0,
"http_max_download": 0,
"nzb_max_download": 0,
"unzip_service_enabled": False,
},
"success": True,
}

View File

@ -0,0 +1,299 @@
# -*- coding: utf-8 -*-
"""DSM 6 SYNO.DownloadStation.Task data."""
DSM_6_DOWNLOAD_STATION_TASK_LIST = {
"data": {
"offset": 0,
"tasks": [
{
"additional": {
"detail": {
"completed_time": 0,
"connected_leechers": 0,
"connected_peers": 0,
"connected_seeders": 0,
"create_time": 1550089068,
"destination": "Folder/containing/downloads",
"seedelapsed": 0,
"started_time": 1592549339,
"total_peers": 0,
"total_pieces": 1239,
"unzip_password": "",
"uri": "magnet:?xt=urn:btih:1234ABCD1234ABCD1234ABCD1234ABCD1234ABCD&dn=1234",
"waiting_seconds": 0,
},
"file": [
{
"filename": "INFO.nfo",
"index": 0,
"priority": "low",
"size": 1335,
"size_downloaded": 0,
"wanted": True,
},
{
"filename": "My super movie 2 2022.mkv",
"index": 1,
"priority": "normal",
"size": 1591087515,
"size_downloaded": 0,
"wanted": True,
},
{
"filename": "My super movie 2 sample.mkv",
"index": 2,
"priority": "normal",
"size": 2754524,
"size_downloaded": 0,
"wanted": False,
},
{
"filename": "My super movie 2021.mkv",
"index": 3,
"priority": "normal",
"size": 1155085341,
"size_downloaded": 0,
"wanted": True,
},
{
"filename": "My super movie 2021 sample.mkv",
"index": 4,
"priority": "normal",
"size": 4359701,
"size_downloaded": 0,
"wanted": False,
},
{
"filename": "My super movie 3 2023.mkv",
"index": 5,
"priority": "normal",
"size": 1288819263,
"size_downloaded": 0,
"wanted": True,
},
{
"filename": "My super movie 3 sample.mkv",
"index": 6,
"priority": "normal",
"size": 3077684,
"size_downloaded": 0,
"wanted": False,
},
{
"filename": "My super movie 4 2031.mkv",
"index": 7,
"priority": "normal",
"size": 1149397942,
"size_downloaded": 0,
"wanted": True,
},
{
"filename": "My super movie 4 sample.mkv",
"index": 8,
"priority": "normal",
"size": 2023179,
"size_downloaded": 0,
"wanted": False,
},
],
},
"id": "dbid_86",
"size": 5196586484,
"status": "downloading",
"title": "My super movie Complete 2021-2031",
"type": "bt",
"username": "Test_useR",
},
{
"additional": {
"detail": {
"completed_time": 0,
"connected_leechers": 0,
"connected_peers": 0,
"connected_seeders": 0,
"create_time": 1551214114,
"destination": "Folder/containing/downloads",
"seedelapsed": 0,
"started_time": 1592549348,
"total_peers": 0,
"total_pieces": 948,
"unzip_password": "",
"uri": "magnet:?xt=urn:btih:1234ABCD1234ABCD1234ABCD1234ABCD1234ABCD&dn=1234",
"waiting_seconds": 0,
},
"file": [
{
"filename": "Blade Swipper 1984.mkv",
"index": 0,
"priority": "normal",
"size": 1986298376,
"size_downloaded": 1602519560,
"wanted": True,
}
],
},
"id": "dbid_164",
"size": 1986298376,
"status": "downloading",
"title": "Blade Swipper 1984.mkv",
"type": "bt",
"username": "Test_useR",
},
{
"additional": {
"detail": {
"completed_time": 0,
"connected_leechers": 0,
"connected_peers": 50,
"connected_seeders": 50,
"create_time": 1585435581,
"destination": "Folder/containing/downloads",
"seedelapsed": 0,
"started_time": 1592549349,
"total_peers": 0,
"total_pieces": 0,
"unzip_password": "",
"uri": "magnet:?xt=urn:btih:1234ABCD1234ABCD1234ABCD1234ABCD1234ABCD&dn=1234",
"waiting_seconds": 0,
},
"file": [],
},
"id": "dbid_486",
"size": 0,
"status": "downloading",
"title": "The falling State",
"type": "bt",
"username": "Test_useR",
},
{
"additional": {
"detail": {
"completed_time": 0,
"connected_leechers": 0,
"connected_peers": 1,
"connected_seeders": 1,
"create_time": 1591562665,
"destination": "Folder/containing/downloads",
"seedelapsed": 0,
"started_time": 1591563597,
"total_peers": 0,
"total_pieces": 1494,
"unzip_password": "",
"uri": "magnet:?xt=urn:btih:1234ABCD1234ABCD1234ABCD1234ABCD1234ABCD&dn=1234",
"waiting_seconds": 0,
}
},
"id": "dbid_518",
"size": 391580448,
"status": "paused",
"title": "Welcome to the North.mkv",
"type": "bt",
"username": "Test_useR",
},
{
"additional": {
"detail": {
"completed_time": 1591565351,
"connected_leechers": 0,
"connected_peers": 0,
"connected_seeders": 0,
"create_time": 1591563606,
"destination": "Folder/containing/downloads",
"seedelapsed": 172800,
"started_time": 1592601577,
"total_peers": 0,
"total_pieces": 5466,
"unzip_password": "",
"uri": "magnet:?xt=urn:btih:1234ABCD1234ABCD1234ABCD1234ABCD1234ABCD&dn=1234",
"waiting_seconds": 0,
}
},
"id": "dbid_522",
"size": 5731285821,
"status": "finished",
"title": "Birds of Pokémon.mkv",
"type": "bt",
"username": "Test_useR",
},
{
"additional": {
"detail": {
"completed_time": 1591566799,
"connected_leechers": 0,
"connected_peers": 0,
"connected_seeders": 0,
"create_time": 1591566523,
"destination": "Folder/containing/downloads",
"seedelapsed": 0,
"started_time": 1591566696,
"total_peers": 0,
"total_pieces": 0,
"unzip_password": "",
"uri": "https://1fichier.com/?1234ABCD1234ABCD1234&af=22123",
"waiting_seconds": 0,
}
},
"id": "dbid_531",
"size": 2811892495,
"status": "finished",
"title": "1234ABCD1234ABCD1234",
"type": "https",
"username": "Test_useR",
},
{
"additional": {
"detail": {
"completed_time": 0,
"connected_leechers": 0,
"connected_peers": 0,
"connected_seeders": 0,
"create_time": 1591566903,
"destination": "Folder/containing/downloads",
"seedelapsed": 0,
"started_time": 0,
"total_peers": 0,
"total_pieces": 0,
"unzip_password": "",
"uri": "https://1fichier.com/?123ABC123ABC123ABC12",
"waiting_seconds": 0,
}
},
"id": "dbid_533",
"size": 0,
"status": "error",
"status_extra": {"error_detail": "unknown"},
"title": "?123ABC123ABC123ABC12",
"type": "https",
"username": "Test_useR",
},
{
"additional": {
"detail": {
"completed_time": 0,
"connected_leechers": 0,
"connected_peers": 0,
"connected_seeders": 0,
"create_time": 1592605687,
"destination": "Folder/containing/downloads",
"seedelapsed": 0,
"started_time": 1592605731,
"total_peers": 0,
"total_pieces": 0,
"unzip_password": "",
"uri": "https://1fichier.com/?123ABC123ABC123ABC12",
"waiting_seconds": 0,
}
},
"id": "dbid_549",
"size": 0,
"status": "error",
"status_extra": {"error_detail": "broken_link"},
"title": "123ABC123ABC123ABC12",
"type": "https",
"username": "Test_useR",
},
],
"total": 8,
},
"success": True,
}

View File

@ -698,8 +698,37 @@ class TestSynologyDSM(TestCase):
assert self.api.storage.disk_below_remain_life_thr("test_disk") is None
assert self.api.storage.disk_temp("test_disk") is None
def test_surveillance_camera(self):
"""Test surveillance."""
def test_download_station(self):
"""Test DownloadStation."""
assert self.api.download_station
assert not self.api.download_station.get_all_tasks()
assert self.api.download_station.get_info()
assert self.api.download_station.get_config()
self.api.download_station.update()
assert self.api.download_station.get_all_tasks()
assert len(self.api.download_station.get_all_tasks()) == 8
# BT DL
assert self.api.download_station.get_task("dbid_86").status == "downloading"
assert not self.api.download_station.get_task("dbid_86").status_extra
assert self.api.download_station.get_task("dbid_86").type == "bt"
assert self.api.download_station.get_task("dbid_86").additional.get("file")
assert (
len(self.api.download_station.get_task("dbid_86").additional.get("file"))
== 9
)
# HTTPS error
assert self.api.download_station.get_task("dbid_549").status == "error"
assert (
self.api.download_station.get_task("dbid_549").status_extra["error_detail"]
== "broken_link"
)
assert self.api.download_station.get_task("dbid_549").type == "https"
def test_surveillance_station(self):
"""Test SurveillanceStation."""
self.api.with_surveillance = True
assert self.api.surveillance_station
assert not self.api.surveillance_station.get_all_cameras()