Skip to content

Commit 9877607

Browse files
authored
Merge pull request #58 from hfr/master
Direct device access for myjdapi.
2 parents 7e0a290 + f53f47d commit 9877607

File tree

4 files changed

+157
-34
lines changed

4 files changed

+157
-34
lines changed

DIRECT.md

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Direct device access using My.JDownloader-API
2+
3+
This is an extension of myjdapi to allow a direct device access without My.JDownloader.
4+
5+
## Theory of operation
6+
7+
My.JDowloader manages JDownloader devices and myjdapi provides a python interface to access them.
8+
If possible My.JDowloader will push a direct connection to myjdapi which will be used consequently.
9+
If enabled this JDownloader api will be available locally. The direct access modification will make
10+
it possible to use the local api without any interaction with My.Downloader at all.
11+
12+
## Implemetation
13+
14+
In myjdapi all connetions will be managed by an instance of Myjdapi. To use myjdapi locally Myjdapi needs to be extended to allow local connextions.
15+
16+
`Myjdapi().connect_device(ip, port, _type='jd', username=None, password=None, timeout=None)` will create
17+
a local connection to a given device and it will make all modifications to the myjdapi instance to support
18+
the direct communication with the local device.
19+
20+
Even so the interface supports a user name and password it is not implemented yet.
21+
22+
The call to connect_device() will ping the device to make sure it exists and it will create a device with the name given by the ip-parameter and the id 'direct'.
23+
A direct device connection has the status connected but it has no session id since the session id is supported be My.Downloader only.
24+
25+
## Usage
26+
27+
After myjdapi has established the connection the device can be querried as usually be calling:
28+
29+
```python
30+
import myjdapi
31+
host = 'localhost'
32+
port = 3128 # Optional, by default is 3128
33+
# We need an instance of Myjdapi() but no application key is required
34+
jd = myjdapi.Myjdapi()
35+
# connect directly to the device
36+
jd.connect_device(host,port,timeout=10) # Timeout is optional.
37+
# The device can be accessed using the host name or ip address.
38+
device = jd.get_device(host)
39+
# Or the device can be accessed by using the device id "direct".
40+
device = jd.get_device(device_id="direct")
41+
# Or the device can be accessed by using it without parameters to obtain the single device.
42+
device = jd.get_device()
43+
```
44+
45+
Once the connection to the local device is established myjdapi will not connect to My.JDownloader any more.
46+
The connection is permanent and will not be refreshed any more.

myjdapi/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,4 @@
3434
MYJDUnknownException,
3535
)
3636

37-
__version__ = "1.1.8"
37+
__version__ = "1.1.9"

myjdapi/myjdapi.py

+109-32
Original file line numberDiff line numberDiff line change
@@ -966,7 +966,6 @@ def __init__(self, jd, device_dict):
966966
self.extensions = Extension(self)
967967
self.dialogs = Dialog(self)
968968
self.update = Update(self)
969-
self.jd = Jd(self)
970969
self.system = System(self)
971970
self.__direct_connection_info = None
972971
self.__refresh_direct_connections()
@@ -975,6 +974,8 @@ def __init__(self, jd, device_dict):
975974
self.__direct_connection_consecutive_failures = 0
976975

977976
def __refresh_direct_connections(self):
977+
if self.myjd.get_connection_type() == "remoteapi":
978+
return
978979
response = self.myjd.request_api("/device/getDirectConnectionInfos",
979980
"POST", None, self.__action_url())
980981
if response is not None \
@@ -1021,7 +1022,11 @@ def action(self, path, params=(), http_action="POST"):
10211022
/example?param1=ex&param2=ex2 [("param1","ex"),("param2","ex2")]
10221023
:param postparams: List of Params that are send in the post.
10231024
"""
1024-
action_url = self.__action_url()
1025+
1026+
if self.myjd.get_connection_type() == "remoteapi":
1027+
action_url = None
1028+
else:
1029+
action_url = self.__action_url()
10251030
if not self.__direct_connection_enabled or self.__direct_connection_info is None \
10261031
or time.time() < self.__direct_connection_cooldown:
10271032
# No direct connection available, we use My.JDownloader api.
@@ -1076,7 +1081,6 @@ def action(self, path, params=(), http_action="POST"):
10761081
def __action_url(self):
10771082
return "/t_" + self.myjd.get_session_token() + "_" + self.device_id
10781083

1079-
10801084
class Myjdapi:
10811085
"""
10821086
Main class for connecting to JD API.
@@ -1091,6 +1095,7 @@ def __init__(self):
10911095
self.__request_id = int(time.time() * 1000)
10921096
self.__api_url = "https://api.jdownloader.org"
10931097
self.__app_key = "http://git.io/vmcsk"
1098+
self.__content_type = "application/aesjson-jd; charset=utf-8"
10941099
self.__api_version = 1
10951100
self.__devices = None
10961101
self.__login_secret = None
@@ -1100,6 +1105,8 @@ def __init__(self):
11001105
self.__server_encryption_token = None
11011106
self.__device_encryption_token = None
11021107
self.__connected = False
1108+
self.__timeout = 3
1109+
self.__connection_type = "myjd" # myjd -> MyJdownloader API, remoteapi -> Deprecated Direct RemoteAPI connection.
11031110

11041111
def get_session_token(self):
11051112
return self.__session_token
@@ -1137,6 +1144,8 @@ def __update_encryption_tokens(self):
11371144
Updates the server_encryption_token and device_encryption_token
11381145
11391146
"""
1147+
if self.__connection_type == "remoteapi":
1148+
return
11401149
if self.__server_encryption_token is None:
11411150
old_token = self.__login_secret
11421151
else:
@@ -1166,6 +1175,8 @@ def __decrypt(self, secret_token, data):
11661175
:param secret_token:
11671176
:param data:
11681177
"""
1178+
if self.__connection_type == "remoteapi":
1179+
return data.encode('utf-8')
11691180
init_vector = secret_token[:len(secret_token) // 2]
11701181
key = secret_token[len(secret_token) // 2:]
11711182
decryptor = AES.new(key, AES.MODE_CBC, init_vector)
@@ -1179,6 +1190,8 @@ def __encrypt(self, secret_token, data):
11791190
:param secret_token:
11801191
:param data:
11811192
"""
1193+
if self.__connection_type == "remoteapi":
1194+
return data
11821195
data = PAD(data.encode('utf-8'))
11831196
init_vector = secret_token[:len(secret_token) // 2]
11841197
key = secret_token[len(secret_token) // 2:]
@@ -1209,6 +1222,7 @@ def connect(self, email, password):
12091222
self.__device_encryption_token = None
12101223
self.__devices = None
12111224
self.__connected = False
1225+
self.__connection_type = "myjd"
12121226

12131227
self.__login_secret = self.__secret_create(email, password, "server")
12141228
self.__device_secret = self.__secret_create(email, password, "device")
@@ -1223,13 +1237,52 @@ def connect(self, email, password):
12231237
self.update_devices()
12241238
return response
12251239

1240+
def direct_connect(self, ip, port=3128, timeout=3):
1241+
"""
1242+
Direct connect to a single device/app instance using the deprecated RemoteAPI.
1243+
This RemoteAPI has to be enabled on JDownloader beforehand.
1244+
Beaware this connection is not authenticated nor encrypted, so do not enable
1245+
it publicly.
1246+
1247+
:param ip: ip of the device
1248+
:param port: port of the device, 3128 by default.
1249+
:param port: optional timeout of the connection, 3 seconds by default.
1250+
:returns: boolean -- True if succesful, False if there was any error.
1251+
1252+
"""
1253+
self.update_request_id()
1254+
# This direct connection doesn't use auth nor encryption so all secrets and tokens are invalid.
1255+
self.__login_secret = None
1256+
self.__device_secret = None
1257+
self.__session_token = None
1258+
self.__regain_token = None
1259+
self.__server_encryption_token = None
1260+
self.__device_encryption_token = None
1261+
self.__devices = [{
1262+
'name': ip,
1263+
'id': 'direct',
1264+
'type': 'jd'
1265+
}]
1266+
self.__connection_type = "remoteapi"
1267+
self.__api_url="http://" + ip + ":" + str(port)
1268+
self.__content_type = "application/json; charset=utf-8"
1269+
self.__timeout=timeout
1270+
self.__connected = True # Set as already connected to use the request_api to ping the instance. Will set correct after that if the connection works.
1271+
response = self.request_api("/device/ping", "GET", [])['data']
1272+
self.__connected = response
1273+
self.update_request_id()
1274+
return response
1275+
12261276
def reconnect(self):
12271277
"""
12281278
Reestablish connection to API.
12291279
12301280
:returns: boolean -- True if successful, False if there was any error.
12311281
12321282
"""
1283+
if self.__connection_type == "remoteapi":
1284+
return True
1285+
12331286
response = self.request_api("/my/reconnect", "GET",
12341287
[("sessiontoken", self.__session_token),
12351288
("regaintoken", self.__regain_token)])
@@ -1246,7 +1299,10 @@ def disconnect(self):
12461299
:returns: boolean -- True if successful, False if there was any error.
12471300
12481301
"""
1249-
response = self.request_api("/my/disconnect", "GET",
1302+
if self.__connection_type == "remoteapi":
1303+
response=True
1304+
else:
1305+
response = self.request_api("/my/disconnect", "GET",
12501306
[("sessiontoken", self.__session_token)])
12511307
self.update_request_id()
12521308
self.__login_secret = None
@@ -1265,6 +1321,8 @@ def update_devices(self):
12651321
12661322
:returns: boolean -- True if successful, False if there was any error.
12671323
"""
1324+
if self.__connection_type == "remoteapi":
1325+
return
12681326
response = self.request_api("/my/listdevices", "GET",
12691327
[("sessiontoken", self.__session_token)])
12701328
self.update_request_id()
@@ -1301,8 +1359,11 @@ def get_device(self, device_name=None, device_id=None):
13011359
for device in self.__devices:
13021360
if device["name"] == device_name:
13031361
return Jddevice(self, device)
1362+
elif len(self.__devices) > 0:
1363+
return Jddevice(self, self.__devices[0])
13041364
raise (MYJDDeviceNotFoundException("Device not found\n"))
13051365

1366+
13061367
def request_api(self,
13071368
path,
13081369
http_method="GET",
@@ -1332,34 +1393,26 @@ def request_api(self,
13321393
else:
13331394
query += ["&%s=%s" % (param[0], param[1])]
13341395
query += ["rid=" + str(self.__request_id)]
1335-
if self.__server_encryption_token is None:
1336-
query += [
1337-
"signature=" \
1338-
+ str(self.__signature_create(self.__login_secret,
1339-
query[0] + "&".join(query[1:])))
1340-
]
1341-
else:
1342-
query += [
1343-
"signature=" \
1344-
+ str(self.__signature_create(self.__server_encryption_token,
1345-
query[0] + "&".join(query[1:])))
1346-
]
1396+
if self.__connection_type == "myjd":
1397+
if self.__server_encryption_token is None: # Requests pre-auth.
1398+
query += [
1399+
"signature=" \
1400+
+ str(self.__signature_create(self.__login_secret,
1401+
query[0] + "&".join(query[1:])))
1402+
]
1403+
else:
1404+
query += [
1405+
"signature=" \
1406+
+ str(self.__signature_create(self.__server_encryption_token,
1407+
query[0] + "&".join(query[1:])))
1408+
]
13471409
query = query[0] + "&".join(query[1:])
1348-
encrypted_response = requests.get(api + query, timeout=3)
1410+
encrypted_response = requests.get(api + query, timeout=self.__timeout)
13491411
else:
1350-
params_request = []
1351-
if params is not None:
1352-
for param in params:
1353-
if isinstance(param, str) or isinstance(param, list):
1354-
params_request += [param]
1355-
elif isinstance(param, dict) or isinstance(param, bool):
1356-
params_request += [json.dumps(param)]
1357-
else:
1358-
params_request += [str(param)]
13591412
params_request = {
13601413
"apiVer": self.__api_version,
13611414
"url": path,
1362-
"params": params_request,
1415+
"params": self.__adapt_params_for_request(params),
13631416
"rid": self.__request_id
13641417
}
13651418
data = json.dumps(params_request)
@@ -1376,10 +1429,10 @@ def request_api(self,
13761429
encrypted_response = requests.post(
13771430
request_url,
13781431
headers={
1379-
"Content-Type": "application/aesjson-jd; charset=utf-8"
1432+
"Content-Type": self.__content_type
13801433
},
13811434
data=encrypted_data,
1382-
timeout=3)
1435+
timeout=self.__timeout)
13831436
except requests.exceptions.RequestException as e:
13841437
return None
13851438
if encrypted_response.status_code != 200:
@@ -1410,8 +1463,32 @@ def request_api(self,
14101463
response = self.__decrypt(self.__device_encryption_token,
14111464
encrypted_response.text)
14121465
jsondata = json.loads(response.decode('utf-8'))
1413-
if jsondata['rid'] != self.__request_id:
1414-
self.update_request_id()
1415-
return None
1466+
if 'rid' in jsondata.keys():
1467+
if jsondata['rid'] != self.__request_id:
1468+
self.update_request_id()
1469+
return None
14161470
self.update_request_id()
14171471
return jsondata
1472+
1473+
def get_connection_type(self):
1474+
return self.__connection_type
1475+
1476+
def __adapt_params_for_request(self, params):
1477+
if params is None:
1478+
return None
1479+
params_request = []
1480+
for param in params:
1481+
if isinstance(param, str):
1482+
params_request += [param]
1483+
elif isinstance(param, list):
1484+
params_request += [self.__adapt_params_for_request(param)]
1485+
elif isinstance(param, dict) and self.__connection_type == "remoteapi":
1486+
params_request += [param]
1487+
elif isinstance(param, dict):
1488+
params_request += [json.dumps(param)]
1489+
elif isinstance(param, bool) or isinstance(param, object):
1490+
params_request += [json.dumps(param)]
1491+
else:
1492+
params_request += [str(param)]
1493+
return params_request
1494+

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
long_description = f.read()
1313
setup(
1414
name="myjdapi",
15-
version="1.1.8",
15+
version="1.1.9",
1616
description="Library to use My.Jdownloader API in an easy way.",
1717
long_description=long_description,
1818
url="https://github.com/mmarquezs/My.Jdownloader-API-Python-Library/",

0 commit comments

Comments
 (0)