diff --git a/Kha/Backends/Krom/Krom.hx b/Kha/Backends/Krom/Krom.hx index c189f8d5..3a6db643 100644 --- a/Kha/Backends/Krom/Krom.hx +++ b/Kha/Backends/Krom/Krom.hx @@ -130,6 +130,11 @@ extern class Krom { static function readStorage(name: String): haxe.io.BytesData; static function fileSaveBytes(path: String, bytes: haxe.io.BytesData): Void; + static function fileWriteBytes(path: String, bytes: haxe.io.BytesData, ?offset: Float, ?byteLength: Int): Void; + static function fileReadBytes(path: String, ?offset: Float, ?length: Int): haxe.io.BytesData; + static function fileSize(path: String): Float; + static function fileExists(path: String): Bool; + static function deleteFile(path: String): Void; static function sysCommand(cmd: String, ?args: Array): Int; static function savePath(): String; static function getArgCount(): Int; diff --git a/leenkx/Sources/leenkx/logicnode/AddTorrentNode.hx b/leenkx/Sources/leenkx/logicnode/AddTorrentNode.hx deleted file mode 100644 index b5bbdfd0..00000000 --- a/leenkx/Sources/leenkx/logicnode/AddTorrentNode.hx +++ /dev/null @@ -1,79 +0,0 @@ -package leenkx.logicnode; -import leenkx.system.Event; -#if js -import leenkx.network.Leenkx; -#end -import iron.object.Object; - -class AddTorrentNode extends LogicNode { - public var title: String; - public var values: Array; - public var net_Url: String; - public var data: Dynamic; - public var runner: Bool = false; - - - public function new(tree:LogicTree) { - super(tree); - } - - public function promiseResult(connection,torrentid,net_Url){ - #if js - if (runner == false){ - values = []; - var script = 'var torrentId = "' + torrentid + '"; - lx_' + net_Url +'.wt.add(torrentId, function (torrent) { - leenkx.network.Leenkx.torrent.set("' + net_Url +'", torrent.files); - torrent.on("done", function () { - leenkx.network.Leenkx.connections.h["' + net_Url +'"].ontorrentdone(); - }) - }); - '; - js.Syntax.code('(1, eval)({0})', script.toString()); - runner = true; - promiseResult(connection,torrentid,net_Url); - return; - }else{ - try{ - values = leenkx.network.Leenkx.torrent.get(net_Url); - if(values.length > 0){ - runOutput(0); - runner = false; - return; - } - } - catch(error){ - haxe.Timer.delay(function () { - promiseResult(connection,torrentid,net_Url); - return; - }, 100); - } - } - #end - } - - - override function run(from:Int) { - var connection = inputs[1].get(); - if (connection == null) return; - var torrentid = inputs[2].get(); - if (torrentid == null) return; - #if js - net_Url = connection._url; - promiseResult(connection,torrentid,net_Url); - #end - } - - override function get(from: Int): Dynamic { - #if js - return switch (from) { - case 1: Leenkx.id.get(net_Url); - case 2: leenkx.network.Leenkx.torrent.get(net_Url); - default: throw "Unreachable"; - } - #else - return null; - #end - } - -} diff --git a/leenkx/Sources/leenkx/logicnode/CreateLeenkxNode.hx b/leenkx/Sources/leenkx/logicnode/CreateLeenkxNode.hx index 903a2ac7..e9bd96f8 100644 --- a/leenkx/Sources/leenkx/logicnode/CreateLeenkxNode.hx +++ b/leenkx/Sources/leenkx/logicnode/CreateLeenkxNode.hx @@ -1,131 +1,168 @@ package leenkx.logicnode; import iron.object.Object; -#if js import leenkx.network.Types; -import leenkx.network.Util; +#if lnx_torrent import leenkx.network.Leenkx; +import leenkx.network.torrent.LeenkxClient; +import leenkx.network.torrent.TorrentClient; +import leenkx.network.torrent.peer.RtcPeerPool; #end import leenkx.system.Event; class CreateLeenkxNode extends LogicNode { + public var property0: String; + public var property1: Bool; + public var property2: String; + public var property3: String; public var net_Url: String; - public var data: Dynamic; - public var client: Dynamic; public var value: String; public var listener: TEvent = null; public function new(tree:LogicTree) { - super(tree); + super(tree); } override function run(from:Int) { - #if js - net_Url = inputs[1].get(); - if (net_Url == null) return; - if(Leenkx.connections[net_Url] == null){ - var object = tree.object; - value = Leenkx.onLoadEvent; - listener = Event.add(value, onEvent, tree.object.uid); - client = new leenkx.network.Leenkx(net_Url, object); - - } else { - return; - } - #end - } - - function onEvent() { - #if js - var lnxjs:Dynamic = js.Lib.global; - var lnxCxNew = lnxjs.lnxNew; - - if (lnxCxNew == null) { - trace("ERROR: lnxNew not available"); - return; + #if lnx_torrent + net_Url = inputs[1].get(); + if (net_Url == null) return; + if (Leenkx.connections[net_Url] == null) { + value = Leenkx.onLoadEvent; + listener = Event.add(value, onEvent, tree.object.uid); + new leenkx.network.Leenkx(net_Url, tree.object); } - - var lnxCn:Dynamic = lnxCxNew(net_Url); - - lnxCn.on("connections", function(c) { - leenkx.network.Leenkx.data.set(net_Url, c + 1); - leenkx.network.Leenkx.connections[net_Url].onconnections(); - }); - lnxCn.on("message", function(address, message) { - leenkx.network.Leenkx.data.set(net_Url, message); - leenkx.network.Leenkx.id.set(net_Url, address); - leenkx.network.Leenkx.connections[net_Url].onmessage(); - }); - lnxCn.on("seen", function(address) { - leenkx.network.Leenkx.id.set(net_Url, address); - leenkx.network.Leenkx.connections[net_Url].onseen(); - }); - lnxCn.on("left", function(address) { - leenkx.network.Leenkx.id.set(net_Url, address); - leenkx.network.Leenkx.connections[net_Url].onleft(); - }); - lnxCn.on("server", function(address) { - leenkx.network.Leenkx.id.set(net_Url, address); - leenkx.network.Leenkx.connections[net_Url].onserver(); - }); - lnxCn.on("ping", function(address) { - leenkx.network.Leenkx.id.set(net_Url, address); - leenkx.network.Leenkx.connections[net_Url].onping(); - }); - lnxCn.on("timeout", function(address) { - leenkx.network.Leenkx.id.set(net_Url, address); - leenkx.network.Leenkx.connections[net_Url].ontimeout(); - }); - lnxCn.on("rpc", function(address, call, args, nonce) { - leenkx.network.Leenkx.data.set(net_Url, call); - leenkx.network.Leenkx.id.set(net_Url, address); - call(args); - leenkx.network.Leenkx.connections[net_Url].onrpc(); - }); - lnxCn.on("rpc-response", function(address, nonce, response) { - leenkx.network.Leenkx.id.set(net_Url, address); - leenkx.network.Leenkx.connections[net_Url].onrpcresponse(); - }); - lnxCn.on("wireleft", function(wirecount, wire) { - leenkx.network.Leenkx.data.set(net_Url, wirecount); - leenkx.network.Leenkx.id.set(net_Url, wire.peerId); - leenkx.network.Leenkx.connections[net_Url].onwireleft(); - }); - lnxCn.on("wireseen", function(wirecount, wire) { - leenkx.network.Leenkx.data.set(net_Url, wirecount); - leenkx.network.Leenkx.id.set(net_Url, wire.peerId); - leenkx.network.Leenkx.connections[net_Url].onwireseen(); - }); - lnxCn.on("torrent", function(identifier, torrent) { - leenkx.network.Leenkx.data.set(net_Url, torrent); - leenkx.network.Leenkx.id.set(net_Url, identifier); - leenkx.network.Leenkx.connections[net_Url].ontorrent(); - }); - lnxCn.on("tracker", function(identifier) { - leenkx.network.Leenkx.id.set(net_Url, identifier); - leenkx.network.Leenkx.connections[net_Url].ontracker(); - }); - lnxCn.on("announce", function(identifier) { - leenkx.network.Leenkx.id.set(net_Url, identifier); - leenkx.network.Leenkx.connections[net_Url].onannounce(); - }); - - Reflect.setField(lnxjs, "lnx_" + net_Url, lnxCn); - Leenkx.connections[net_Url].client = lnxCn; - - var script = 'globalThis.addEventListener("beforeunload", function (e) { - leenkx.network.Leenkx.connections.h["' + net_Url + '"].client.destroy(); - delete e["returnValue"]; - });'; - js.Syntax.code('(1, eval)({0})', script); - - runOutput(0); #end } + function onEvent() { + if (listener != null) { + Event.removeListener(listener); + listener = null; + } + #if lnx_torrent + var sock = Leenkx.connections[net_Url]; + if (sock == null) return; + if (sock.client != null) { + runOutput(0); + return; + } + var opts:leenkx.network.torrent.Types.LeenkxOptions = {deferJoin: true}; + var nextInput = 2; + if (property0 == "datachannel") { + opts.transport = "datachannel"; + var imode = property2 != null ? property2 : "inherit"; + if (imode != "inherit") { + if (inputs.length > nextInput) { + var ice = TorrentClient.trackerUrls(inputs[nextInput].get()); + if (ice != null) { + opts.iceServers = imode == "override" ? ice : RtcPeerPool.defaultIceServers.concat(ice); + } + } + nextInput++; + } + } + var tmode = property3 != null ? property3 : "inherit"; + var trackers:Array = null; + if (tmode != "inherit" && inputs.length > nextInput) { + trackers = TorrentClient.trackerUrls(inputs[nextInput].get()); + } + opts.announce = tmode == "override" && trackers != null ? trackers + : trackers != null ? TorrentClient.defaultTrackers.concat(trackers) + : TorrentClient.defaultTrackers; + var lnxCn = new LeenkxClient(net_Url, opts); + lnxCn.isRelay = property1 == true; + + lnxCn.onOpen = function() { + Leenkx.data.set(net_Url, lnxCn.lastPeerCount); + Leenkx.id.set(net_Url, lnxCn.lastPeerId); + sock.onopen(); + }; + lnxCn.onConnections = function(c) { + Leenkx.data.set(net_Url, c + 1); + sock.onconnections(); + }; + lnxCn.onMessage = function(address, message, nonce) { + Leenkx.data.set(net_Url, message); + Leenkx.id.set(net_Url, address); + sock.onmessage(); + }; + lnxCn.onSeen = function(address) { + Leenkx.id.set(net_Url, address); + sock.onseen(); + }; + lnxCn.onLeft = function(address) { + Leenkx.id.set(net_Url, address); + sock.onleft(); + }; + lnxCn.onServer = function(address) { + Leenkx.id.set(net_Url, address); + sock.onserver(); + }; + lnxCn.onPing = function(address) { + Leenkx.id.set(net_Url, address); + sock.onping(); + }; + lnxCn.onTimeout = function(address) { + Leenkx.id.set(net_Url, address); + sock.ontimeout(); + }; + lnxCn.onWireLeft = function(wirecount, wire) { + Leenkx.data.set(net_Url, wirecount); + if (wire != null && wire.peerId != null) { + Leenkx.id.set(net_Url, wire.peerId); + } + sock.onwireleft(); + }; + lnxCn.onWireSeen = function(wirecount, wire) { + Leenkx.data.set(net_Url, wirecount); + if (wire != null && wire.peerId != null) { + Leenkx.id.set(net_Url, wire.peerId); + } + sock.onwireseen(); + }; + lnxCn.onTorrent = function(identifier, torrent) { + Leenkx.data.set(net_Url, torrent); + Leenkx.id.set(net_Url, identifier); + sock.ontorrent(); + }; + lnxCn.onTracker = function(identifier, tracker) { + Leenkx.id.set(net_Url, identifier); + sock.ontracker(); + }; + lnxCn.onAnnounce = function(identifier) { + Leenkx.id.set(net_Url, identifier); + sock.onannounce(); + }; + lnxCn.onError = function(msg) { + Leenkx.data.set(net_Url, msg); + sock.onerror(); + }; + lnxCn.onTorrentAdded = function(t) { + Leenkx.torrent.set(net_Url, t); + Leenkx.file.set(net_Url, t.files); + Leenkx.data.set(net_Url, t); + sock.ontorrentadded(); + }; + lnxCn.onTorrentDone = function(t) { + Leenkx.data.set(net_Url, t); + sock.ontorrentdone(); + }; + + sock.client = lnxCn; + + lnxCn.connect(); + #if (js && !kha_krom) + js.Browser.window.addEventListener("beforeunload", function(e) { + lnxCn.destroy(); + }); + #end + #end + runOutput(0); + } override function get(from: Int): Dynamic { - #if js + #if lnx_torrent return switch (from) { case 1: Leenkx.connections[net_Url]; case 2: Leenkx.id.get(net_Url); diff --git a/leenkx/Sources/leenkx/logicnode/CreateTorrentNode.hx b/leenkx/Sources/leenkx/logicnode/CreateTorrentNode.hx index 518f40a9..2de96089 100644 --- a/leenkx/Sources/leenkx/logicnode/CreateTorrentNode.hx +++ b/leenkx/Sources/leenkx/logicnode/CreateTorrentNode.hx @@ -1,43 +1,100 @@ package leenkx.logicnode; -import leenkx.system.Event; +#if lnx_torrent import leenkx.network.Leenkx; -import iron.object.Object; +import leenkx.network.torrent.Magnet; +import leenkx.network.torrent.TorrentClient; +#end + class CreateTorrentNode extends LogicNode { - public var torrent: Dynamic; - public var net_Url: String; + public var meta:Dynamic; + public var metaBytes:Dynamic; + public var magnetUri:Dynamic; + public var infoHashHex:String; + public var property0:String; + public var property1:String; public function new(tree:LogicTree) { super(tree); - } + } override function run(from:Int) { + #if lnx_torrent + meta = null; + metaBytes = null; + magnetUri = null; + infoHashHex = null; - var connection = inputs[1].get(); - if (connection == null) return; - var data = inputs[2].get(); - if (data == null) return; - net_Url = connection._url; + var data = inputs[1].get(); + if (data == null) return; + var mode = property1 != null ? property1 : "file"; + var opts:Dynamic = {}; + var mv = property0 != null ? Std.parseInt(property0) : null; + if (mv != null) Reflect.setField(opts, "metaVersion", mv); + field(2, opts, "name"); + field(3, opts, "comment"); + field(4, opts, "createdBy"); + field(5, opts, "source"); + field(6, opts, "announce"); + field(7, opts, "private"); + field(8, opts, "pieceLength"); + field(9, opts, "urlList"); - var script = 'var file = ' + data + '; - lx_' + net_Url +'.wt.add(file, function (torrent) { - leenkx.network.Leenkx.file.set("' + net_Url +'", torrent.files); - torrent.on("done", function () { - leenkx.network.Leenkx.connections.h["' + net_Url +'"].ontorrentdone(); - }); - }); - '; + if (mode == "folder") { + #if sys + data = TorrentClient.readFolder(Std.string(data)); + var first:Dynamic = data[0]; + if (first != null && Reflect.field(opts, "name") == null) { + var p = Std.string(first.path); + var i = p.indexOf("/"); + Reflect.setField(opts, "name", p.substring(0, i >= 0 ? i : p.length)); + } + #else + trace("Folder mode requires a sys target"; + #end + } - js.Syntax.code('(1, eval)({0})', script.toString()); + var m; + try { + m = TorrentClient.buildMeta(data, opts); + } catch(e:Dynamic) { + trace(e); + return; + } + meta = m; + metaBytes = m.toTorrentBytes(); + infoHashHex = m.infoHashHex != null ? m.infoHashHex : m.infoHashV2Hex; + magnetUri = Magnet.build(m.infoHash, m.name, m.announce, m.urlList, m.infoHashV2); + #end runOutput(0); } + #if lnx_torrent + function field(i:Int, opts:Dynamic, name:String):Void { + if (i >= inputs.length) return; + var v:Dynamic = inputs[i].get(); + if (v == null || Std.string(v) == "") { + return; + } + if (name == "announce") { + v = TorrentClient.trackerUrls(v); + if (v == null) return; + } + Reflect.setField(opts, name, v); + } + #end - override function get(from: Int): Dynamic { - return switch (from) { - case 1: Leenkx.file.get(net_Url); - default: throw "Unreachable"; - } - } - + override function get(from:Int):Dynamic { + #if lnx_torrent + return switch (from) { + case 1: meta; + case 2: metaBytes; + case 3: infoHashHex; + case 4: magnetUri; + default: throw "Unreachable"; + } + #else + return null; + #end + } } diff --git a/leenkx/Sources/leenkx/logicnode/CreateTrackerNode.hx b/leenkx/Sources/leenkx/logicnode/CreateTrackerNode.hx new file mode 100644 index 00000000..25bf5b96 --- /dev/null +++ b/leenkx/Sources/leenkx/logicnode/CreateTrackerNode.hx @@ -0,0 +1,88 @@ +package leenkx.logicnode; +#if (lnx_torrent && (sys || kha_krom)) +import leenkx.network.torrent.tracker.HttpTracker.HttpTrackerServer; +import leenkx.network.torrent.tracker.UdpTracker.UdpTrackerServer; +import leenkx.network.torrent.transport.WsTrackerClient.WsTrackerServer; +#end +#if (lnx_torrent && sys) +import leenkx.network.torrent.transport.WsTrackerClient.WssTrackerServer; +#end + +class CreateTrackerNode extends LogicNode { + public var property0:String; + public var tracker:Dynamic; + public var url:String; + + public function new(tree:LogicTree) { + super(tree); + } + + override function run(from:Int) { + tracker = null; + url = null; + startServer(); + runOutput(0); + } + + function startServer():Void { + #if (lnx_torrent && (sys || kha_krom)) + var hv = inputs[1].get(); + var host = hv != null ? Std.string(hv) : "127.0.0.1"; + if (host == "") host = "127.0.0.1"; + var pv = inputs[2].get(); + var port = pv != null ? Std.int(Std.parseFloat(Std.string(pv))) : 0; + if (port <= 0) port = 6969; + var scheme = property0 != null ? property0 : "udp"; + switch (scheme) { + case "http": + var s = new HttpTrackerServer(); + s.onError = function(e) trace(e); + s.start(port); + tracker = s; + url = "http://" + host + ":" + port + "/announce"; + case "ws": + var s = new WsTrackerServer(host, port); + s.onError = function(e) trace(e); + s.start(); + tracker = s; + url = "ws://" + host + ":" + port; + case "wss": + #if sys + var cert = inputs.length > 3 ? Std.string(inputs[3].get()) : null; + var key = inputs.length > 4 ? Std.string(inputs[4].get()) : null; + if (cert == null || cert == "" || key == null || key == "") { + trace("wss server needs Cert File and Key File"); + return; + } + var s = new WssTrackerServer(host, port, cert, key); + s.onError = function(e) trace(e); + s.start(); + tracker = s; + url = "wss://" + host + ":" + port; + #else + trace("wss server requires a sys target"); + #end + default: + var s = new UdpTrackerServer(); + s.onError = function(e) trace(e); + s.start(port); + tracker = s; + url = "udp://" + host + ":" + port; + } + #else + trace("server mode needs a sys or krom target"); + #end + } + + override function get(from:Int):Dynamic { + #if lnx_torrent + return switch (from) { + case 1: tracker; + case 2: url; + default: throw "Unreachable"; + } + #else + return null; + #end + } +} diff --git a/leenkx/Sources/leenkx/logicnode/DownloadTorrentNode.hx b/leenkx/Sources/leenkx/logicnode/DownloadTorrentNode.hx new file mode 100644 index 00000000..00f88f1c --- /dev/null +++ b/leenkx/Sources/leenkx/logicnode/DownloadTorrentNode.hx @@ -0,0 +1,101 @@ +package leenkx.logicnode; +import leenkx.system.Event; +#if lnx_torrent +import leenkx.network.Leenkx; +import leenkx.network.torrent.TorrentClient; +#end + +class DownloadTorrentNode extends LogicNode { + public var net_Url:String; + public var torrent:Dynamic; + public var property0:String; + public var property1:String; + var listener:TEvent = null; + + public function new(tree:LogicTree) { + super(tree); + } + + public function promiseResult(connection, torrentid, net_Url, ?opts:Dynamic) { + #if lnx_torrent + var socket = Leenkx.connections[net_Url]; + if (socket == null) { + if (listener == null) { + var self = this; + listener = Event.add(Leenkx.onLoadEvent, function() { + if (Leenkx.connections[net_Url] == null) return; + if (self.listener != null) { + Event.removeListener(self.listener); + self.listener = null; + } + self.promiseResult(connection, torrentid, net_Url, opts); + }); + } + return; + } + var client = socket.client; + if (client == null) return; + var self = this; + client.add(torrentid, opts, function(torrent) { + self.torrent = torrent; + var onReady = function() { + self.runOutput(0); + }; + var onDone = function() { + self.runOutput(4); + }; + if (torrent.ready) onReady(); + else torrent.on("ready", onReady); + if (torrent.done) onDone(); + else torrent.on("done", onDone); + }); + #end + } + + function onRemove() { + if (listener != null) { + Event.removeListener(listener); + listener = null; + } + } + + override function run(from:Int) { + var connection = inputs[1].get(); + if (connection == null) return; + var v = inputs[2].get(); + if (v == null) return; + #if lnx_torrent + net_Url = connection._url; + var torrentId:Dynamic = switch (property0 != null ? property0 : "magnet") { + case "file": v; + case "id": TorrentClient.torrentIdFor(v); + default: Std.string(v); + }; + var opts:Dynamic = null; + var tmode = property1 != null ? property1 : "inherit"; + if (tmode != "inherit" && inputs.length > 3) { + var announce = TorrentClient.trackerUrls(inputs[3].get()); + opts = {}; + if (announce != null) opts.announce = announce; + if (tmode == "override") opts.announceOverride = true; + } + promiseResult(connection, torrentId, net_Url, opts); + #end + } + + override function get(from:Int):Dynamic { + #if lnx_torrent + var t:leenkx.network.torrent.Torrent = torrent; + return switch (from) { + case 1: torrent; + case 2: t != null && t.meta != null ? t.meta.name : null; + case 3: t != null ? t.files : null; + case 5: t != null ? t.infoHashHex : null; + case 6: t != null ? t.magnetURI : null; + default: throw "Unreachable"; + } + #else + return null; + #end + } +} diff --git a/leenkx/Sources/leenkx/logicnode/GetTorrentFileNode.hx b/leenkx/Sources/leenkx/logicnode/GetTorrentFileNode.hx new file mode 100644 index 00000000..bf1529ce --- /dev/null +++ b/leenkx/Sources/leenkx/logicnode/GetTorrentFileNode.hx @@ -0,0 +1,83 @@ +package leenkx.logicnode; +#if lnx_torrent +import leenkx.network.Leenkx; +import leenkx.network.torrent.Torrent; +import leenkx.network.torrent.TorrentFile; +#end + +class GetTorrentFileNode extends LogicNode { + public var property0:String; + public var file:Dynamic; + var net_Url:String; + + public function new(tree:LogicTree) { + super(tree); + } + + override function run(from:Int) { + #if lnx_torrent + file = null; + var id = inputs[2].get(); + var match = inputs[3].get(); + if (id != null && match != null) { + var connection = inputs[1].get(); + if (connection != null) net_Url = connection._url; + var socket = net_Url != null ? Leenkx.connections[net_Url] : null; + var client = socket != null ? socket.client : null; + var t = client != null ? client.get(id) : (Std.isOfType(id, Torrent) ? id : null); + if (t != null) file = findFile(t, match); + } + #end + runOutput(0); + } + + #if lnx_torrent + function findFile(t:Torrent, match:Dynamic):TorrentFile { + var files = t.files; + switch (property0) { + case "index": + var i = Std.parseInt(Std.string(match)); + if (i != null && i >= 0 && i < files.length) { + return files[i]; + } + case "name": + var s = Std.string(match); + for (f in files) { + if (f.name == s) return f; + } + case "path": + var s = Std.string(match); + for (f in files) { + if (f.path == s) return f; + } + default: + var s = Std.string(match); + if (!StringTools.startsWith(s, ".")) s = "." + s; + for (f in files) { + if (StringTools.endsWith(f.name, s)) return f; + } + } + return null; + } + #end + + override function get(from:Int):Dynamic { + #if lnx_torrent + var f:TorrentFile = file; + return switch (from) { + case 1: file; + case 2: f != null ? f.name : null; + case 3: f != null ? f.path : null; + case 4: f != null ? f.length : 0; + case 5: f != null ? f.type : null; + case 6: f != null ? f.getBytes() : null; + case 7: f != null; + case 8: f != null ? f.downloaded : 0.0; + case 9: f != null ? f.progress : 0.0; + default: throw "Unreachable"; + } + #else + return null; + #end + } +} diff --git a/leenkx/Sources/leenkx/logicnode/LeenkxCloseConnectionNode.hx b/leenkx/Sources/leenkx/logicnode/LeenkxCloseConnectionNode.hx index 6dbd68c1..b2262467 100644 --- a/leenkx/Sources/leenkx/logicnode/LeenkxCloseConnectionNode.hx +++ b/leenkx/Sources/leenkx/logicnode/LeenkxCloseConnectionNode.hx @@ -1,8 +1,8 @@ package leenkx.logicnode; -import leenkx.system.Event; -import leenkx.network.Connect; -import iron.object.Object; - +#if lnx_torrent +import leenkx.network.Leenkx; +import leenkx.network.torrent.LeenkxClient; +#end class LeenkxCloseConnectionNode extends LogicNode { public var property0: String; @@ -12,41 +12,29 @@ class LeenkxCloseConnectionNode extends LogicNode { } override function run(from:Int) { + #if lnx_torrent var connection = inputs[1].get(); - if (connection == null) return; + if (connection == null) return; + var socket = Leenkx.connections[connection._url]; + if (socket == null) return; - if(property0 == "client") { - var script = ' - for (p in leenkx.network.Leenkx.connections.h["' + connection._url + '"].client.torrent._peers){ - try{ - leenkx.network.Leenkx.id.set("' + connection._url + '",p); - leenkx.network.Leenkx.connections.h["' + connection._url + '"].client.torrent._peers[p].conn._pc.close(); - leenkx.network.Leenkx.connections.h["' + connection._url + '"].client.torrent._peers[p].conn.destroy(); - leenkx.network.Leenkx.connections.h["' + connection._url + '"].onclose(); - }catch(error){ - console.log("Error: " + error); - } - } - '; - js.Syntax.code('(1, eval)({0})', script.toString()); - } else { - var script = ' - try{ - var lnxConn = leenkx.network.Leenkx.connections.h["' + connection._url + '"]; - if (lnxConn && lnxConn.client && lnxConn.client.torrent && lnxConn.client.torrent._peers) { - for (var p in lnxConn.client.torrent._peers) { - lnxConn.client.torrent._peers[p].conn._pc.close(); - lnxConn.client.torrent._peers[p].conn.destroy(); - leenkx.network.Leenkx.id.set("' + connection._url + '", p); - lnxConn.onclose(); - } - } - }catch(error){ - console.log("Error: " + error); - } - '; - js.Syntax.code('(1, eval)({0})', script.toString()); + var lnxCn:LeenkxClient = socket.client; + if (property0 == "peer") { + var id:String = inputs.length > 2 ? inputs[2].get() : null; + if (id != null && lnxCn != null) { + lnxCn.peers.remove(id); + lnxCn.seenFired.remove(id); + } + runOutput(0); + return; } + if (lnxCn != null) { + lnxCn.destroy(); + socket.client = null; + } + Leenkx.connections.remove(connection._url); + socket.onclose(); + #end runOutput(0); } } diff --git a/leenkx/Sources/leenkx/logicnode/LeenkxEventNode.hx b/leenkx/Sources/leenkx/logicnode/LeenkxEventNode.hx index c94ded30..10aa580c 100644 --- a/leenkx/Sources/leenkx/logicnode/LeenkxEventNode.hx +++ b/leenkx/Sources/leenkx/logicnode/LeenkxEventNode.hx @@ -1,10 +1,10 @@ package leenkx.logicnode; import leenkx.system.Event; +#if lnx_torrent import leenkx.network.Leenkx; -import iron.object.Object; +#end class LeenkxEventNode extends LogicNode { - public var property0: String; public var property1: String; public var value: String; @@ -14,90 +14,63 @@ class LeenkxEventNode extends LogicNode { public function new(tree:LogicTree) { super(tree); tree.notifyOnInit(init); + tree.notifyOnRemove(onRemove); } function init() { - if (property0 == "client") { - net_Url = inputs[0].get(); - switch (property1) { - case "onopen": value = Leenkx.onOpenEvent;listener = Event.add(value, onEvent, tree.object.uid); - case "onmessage": value = Leenkx.onMessageEvent;listener = Event.add(value, onEvent, tree.object.uid); - case "onerror": value = Leenkx.onErrorEvent;listener = Event.add(value, onEvent, tree.object.uid); - case "onclose": value = Leenkx.onCloseEvent;listener = Event.add(value, onEvent, tree.object.uid); - case "onseen": value = Leenkx.onSeenEvent;listener = Event.add(value, onEvent, tree.object.uid); - case "onserver": value = Leenkx.onServerEvent;listener = Event.add(value, onEvent, tree.object.uid); - case "onconnections": value = Leenkx.onConnectionsEvent;listener = Event.add(value, onEvent, tree.object.uid); - case "onping": value = Leenkx.onPingEvent;listener = Event.add(value, onEvent, tree.object.uid); - case "onleft": value = Leenkx.onLeftEvent;listener = Event.add(value, onEvent, tree.object.uid); - case "ontimeout": value = Leenkx.onTimeOutEvent;listener = Event.add(value, onEvent, tree.object.uid); - case "onrpc": value = Leenkx.onRpcEvent;listener = Event.add(value, onEvent, tree.object.uid); - case "onrpcresponse": value = Leenkx.onRpcResponseEvent;listener = Event.add(value, onEvent, tree.object.uid); - case "onwireleft": value = Leenkx.onWireLeftEvent;listener = Event.add(value, onEvent, tree.object.uid); - case "onwireseen": value = Leenkx.onWireSeenEvent;listener = Event.add(value, onEvent, tree.object.uid); - case "ontorrent": value = Leenkx.onTorrentEvent;listener = Event.add(value, onEvent, tree.object.uid); - case "ontracker": value = Leenkx.onTrackerEvent;listener = Event.add(value, onEvent, tree.object.uid); - case "onannounce": value = Leenkx.onAnnounceEvent;listener = Event.add(value, onEvent, tree.object.uid); - default: throw "Failed to set client event type."; - } - } else if (property0 == "host") { - #if (sys || kha_krom) - var net_Domain = inputs[0].get(); - var net_Port = inputs[1].get(); - net_Url = "ws://" + net_Domain + ":" + Std.string(net_Port); - switch (property1) { - case "onopen": value = Leenkx.onOpenEvent;listener = Event.add(value, onEvent, tree.object.uid); - case "onmessage": value = Leenkx.onMessageEvent;listener = Event.add(value, onEvent, tree.object.uid); - case "onerror": value = Leenkx.onErrorEvent;listener = Event.add(value, onEvent, tree.object.uid); - case "onclose": value = Leenkx.onCloseEvent;listener = Event.add(value, onEvent, tree.object.uid); - default: throw "Failed to set host event type."; - } - #end - } else if (property0 == "securehost"){ - #if (sys || kha_krom) - var net_Domain = inputs[0].get(); - var net_Port = inputs[1].get(); - net_Url = "wss://" + net_Domain + ":" + Std.string(net_Port); - switch (property1) { - case "onopen": value = Leenkx.onOpenEvent;listener = Event.add(value, onEvent, tree.object.uid); - case "onmessage": value = Leenkx.onMessageEvent;listener = Event.add(value, onEvent, tree.object.uid); - case "onerror": value = Leenkx.onErrorEvent;listener = Event.add(value, onEvent, tree.object.uid); - case "onclose": value = Leenkx.onCloseEvent;listener = Event.add(value, onEvent, tree.object.uid); - default: throw "Failed to set host event type."; - } - #end + #if lnx_torrent + var u:Dynamic = inputs[0].get(); + net_Url = Std.isOfType(u, String) ? u : (u != null ? u._url : null); + switch (property1) { + case "onopen": value = Leenkx.onOpenEvent; + case "onmessage": value = Leenkx.onMessageEvent; + case "onerror": value = Leenkx.onErrorEvent; + case "onclose": value = Leenkx.onCloseEvent; + case "onseen": value = Leenkx.onSeenEvent; + case "onserver": value = Leenkx.onServerEvent; + case "onconnections": value = Leenkx.onConnectionsEvent; + case "onping": value = Leenkx.onPingEvent; + case "onleft": value = Leenkx.onLeftEvent; + case "ontimeout": value = Leenkx.onTimeOutEvent; + case "onwireleft": value = Leenkx.onWireLeftEvent; + case "onwireseen": value = Leenkx.onWireSeenEvent; + case "ontorrent": value = Leenkx.onTorrentEvent; + case "ontorrentadded": value = Leenkx.onTorrentAddedEvent; + case "ontracker": value = Leenkx.onTrackerEvent; + case "onannounce": value = Leenkx.onAnnounceEvent; + case "ontorrentdone": value = Leenkx.onTorrentDoneEvent; + case "onload": value = Leenkx.onLoadEvent; + default: + trace("Failed to set client event type"); } - - + listener = Event.add(value, onEvent, tree.object.uid); + #end } function onEvent() { runOutput(0); } - override function get(from: Int): Dynamic { - if (property0 == "host") { - return switch (from) { - case 1: Leenkx.id.get(net_Url); - case 2: Leenkx.data.get(net_Url); - default: throw "Unreachable"; - } - } - else if (property0 == "securehost") { - return switch (from) { - case 1: Leenkx.id.get(net_Url); - case 2: Leenkx.data.get(net_Url); - default: throw "Unreachable"; - } - } - else { - return switch (from) { - case 1: Leenkx.id.get(net_Url); - case 2: Leenkx.data.get(net_Url); - default: throw "Unreachable"; - } + function onRemove() { + if (listener != null) { + Event.removeListener(listener); + listener = null; } } + override function get(from: Int): Dynamic { + #if lnx_torrent + return switch (from) { + case 1: Leenkx.id.get(net_Url); + case 2: Leenkx.data.get(net_Url); + default: throw "Unreachable"; + } + #else + return null; + #end + } + } + diff --git a/leenkx/Sources/leenkx/logicnode/LeenkxMessageParserNode.hx b/leenkx/Sources/leenkx/logicnode/LeenkxMessageParserNode.hx index aff8622b..ad1cdb31 100644 --- a/leenkx/Sources/leenkx/logicnode/LeenkxMessageParserNode.hx +++ b/leenkx/Sources/leenkx/logicnode/LeenkxMessageParserNode.hx @@ -1,8 +1,6 @@ package leenkx.logicnode; -import leenkx.network.Connect; import leenkx.network.Buffer; import haxe.io.Bytes; -import iron.object.Object; import iron.math.Vec4; import iron.math.Quat; import iron.math.Mat4; @@ -22,140 +20,107 @@ class LeenkxMessageParserNode extends LogicNode { if (data == null) return; api = inputs[1].get(); + if (api == null) api = ""; + + var buffer:Buffer; + if (Std.isOfType(data, Buffer)) { + buffer = cast data; + } else { + var rawBytes:Bytes; + try { + rawBytes = haxe.crypto.Base64.decode(Std.string(data)); + } catch(e) { + trace("Base64 decode failed: " + e); + return; + } + buffer = new Buffer(); + buffer.writeBytes(rawBytes); + } - /** - var buffer = cast(data, Buffer); - api = inputs[1].get(); if(buffer.endsWith(api) == false){ return; } - **/ - var buffer = new Buffer(); - if(!StringTools.startsWith(data,api)){ - return; - } - switch (property0) { - case "string": - parsed = data.substr(api.length, data.length); - runOutput(0); - case "vector": - var vec = data.substr(api.length, data.length); - vec = StringTools.replace(vec, "(", ""); - vec = StringTools.replace(vec, ")", ""); - vec = StringTools.replace(vec, " ", ""); - vec = StringTools.trim(vec); - var array = vec.split(",").map(Std.parseFloat); - parsed = new Vec4( array[0], array[1], array[2], array[3] ); - runOutput(0); - case "float": - var flt:String = data.substr(api.length, data.length); - var float = StringTools.trim(flt); - parsed = Std.parseFloat(float); - runOutput(0); - case "integer": - var int:String = data.substr(api.length, data.length); - var integer = StringTools.trim(int); - parsed = Std.parseInt(integer); - runOutput(0); - case "boolean": - var boolean:String = data.substr(api.length, data.length); - if(boolean == "true"){ - parsed = true; - } else { - parsed = false; - } - runOutput(0); - case "transform": - var tsfm = data.substr(api.length, data.length); - tsfm = StringTools.replace(tsfm, "[", ""); - tsfm = StringTools.replace(tsfm, "]", ""); - tsfm = StringTools.replace(tsfm, " ", ""); - tsfm = StringTools.trim(tsfm); - var array = tsfm.split(",").map(Std.parseFloat); - parsed = new Mat4( - array[0], array[1], array[2], array[3], - array[4], array[5], array[6], array[7], - array[8], array[9], array[10], array[11], - array[12], array[13], array[14], array[15] - ); - runOutput(0); - case "rotation": - var bytes = new haxe.io.BytesInput(buffer.readUntil(api)); - var rot = new Quat(); - for (f in Reflect.fields(rot)) { - Reflect.setField(rot, f, bytes.readFloat()); - } - parsed = rot; - runOutput(0); - default: throw "Failed to parse data."; - } - return; - - switch (property0) { - case "string": + case "string": var bytes = buffer.readAllAvailableBytes(); parsed = bytes.toString().substr(0, bytes.length - api.length); - trace(parsed); runOutput(0); - case "vector": - var bytes = new haxe.io.BytesInput(buffer.readUntil(api)); + case "vector": + var readBytes = buffer.readUntil(api); + if (readBytes == null) return; + var bytes = new haxe.io.BytesInput(readBytes); var vec = new Vec4(); - for (f in Reflect.fields(vec)) { - Reflect.setField(vec, f, bytes.readFloat()); - } + vec.x = bytes.readFloat(); + vec.y = bytes.readFloat(); + vec.z = bytes.readFloat(); + vec.w = bytes.readFloat(); parsed = vec; runOutput(0); - case "float": - var bytes = new haxe.io.BytesInput(buffer.readUntil(api)); + case "float": + var readBytes = buffer.readUntil(api); + if (readBytes == null) return; + var bytes = new haxe.io.BytesInput(readBytes); var float: Float = bytes.readFloat(); parsed = float; runOutput(0); - case "integer": - var bytes = new haxe.io.BytesInput(buffer.readUntil(api)); + case "integer": + var readBytes = buffer.readUntil(api); + if (readBytes == null) return; + var bytes = new haxe.io.BytesInput(readBytes); var integer: Int = bytes.readInt32(); parsed = integer; runOutput(0); - case "boolean": + case "boolean": var bytes = buffer.readAllAvailableBytes(); var boolean = bytes.toString().substr(0, bytes.length - api.length); - if(boolean == "true"){ + if (boolean == "true") { parsed = true; } else { parsed = false; } runOutput(0); case "transform": - var bytes = new haxe.io.BytesInput(buffer.readUntil(api)); + var readBytes = buffer.readUntil(api); + if (readBytes == null) return; + var bytes = new haxe.io.BytesInput(readBytes); var loc: Vec4 = new Vec4(); var rot: Quat = new Quat(); var scl: Vec4 = new Vec4(); - for (f in Reflect.fields(loc)) { - Reflect.setField(loc, f, bytes.readFloat()); + try { + loc.x = bytes.readFloat(); + loc.y = bytes.readFloat(); + loc.z = bytes.readFloat(); + loc.w = bytes.readFloat(); + rot.x = bytes.readFloat(); + rot.y = bytes.readFloat(); + rot.z = bytes.readFloat(); + rot.w = bytes.readFloat(); + scl.x = bytes.readFloat(); + scl.y = bytes.readFloat(); + scl.z = bytes.readFloat(); + scl.w = bytes.readFloat(); + } catch(e: Dynamic) { + trace("readFloat error: " + e); + return; } - for (f in Reflect.fields(rot)) { - Reflect.setField(rot, f, bytes.readFloat()); - } - for (f in Reflect.fields(scl)) { - Reflect.setField(scl, f, bytes.readFloat()); - } - var transform = Mat4.identity(); + var transform = Mat4.identity(); parsed = transform.compose(loc, rot, scl); runOutput(0); - case "rotation": - var bytes = new haxe.io.BytesInput(buffer.readUntil(api)); + case "rotation": + var readBytes = buffer.readUntil(api); + if (readBytes == null) return; + var bytes = new haxe.io.BytesInput(readBytes); var rot = new Quat(); - for (f in Reflect.fields(rot)) { - Reflect.setField(rot, f, bytes.readFloat()); - } + rot.x = bytes.readFloat(); + rot.y = bytes.readFloat(); + rot.z = bytes.readFloat(); + rot.w = bytes.readFloat(); parsed = rot; runOutput(0); default: throw "Failed to parse data."; } - - - } + } override function get(from: Int): Dynamic { return switch (from) { diff --git a/leenkx/Sources/leenkx/logicnode/LeenkxSendMessageNode.hx b/leenkx/Sources/leenkx/logicnode/LeenkxSendMessageNode.hx index 17da2ff4..e90b5661 100644 --- a/leenkx/Sources/leenkx/logicnode/LeenkxSendMessageNode.hx +++ b/leenkx/Sources/leenkx/logicnode/LeenkxSendMessageNode.hx @@ -1,401 +1,124 @@ package leenkx.logicnode; -import leenkx.network.Connect; -import leenkx.network.Leenkx; import leenkx.network.Buffer; import haxe.io.Bytes; -import iron.object.Object; +#if lnx_torrent +import leenkx.network.Leenkx; +import leenkx.network.torrent.LeenkxClient; +#end import iron.math.Vec4; import iron.math.Quat; import iron.math.Mat4; class LeenkxSendMessageNode extends LogicNode { - public var property0: String; public var property1: String; - public var id: String; - public var all: Bool; + public var property2: Bool; public function new(tree:LogicTree) { super(tree); } override function run(from:Int) { - #if js var connection = inputs[1].get(); - if (connection == null) return; - var api: String = inputs[2].get(); + if (connection == null) return; + var api: String = inputs[2].get(); var message: Dynamic = inputs[3].get(); + if (message == null) return; + var broadcast: Bool = inputs.length > 4 && inputs[4].get() == true; + var targetAddress: String = inputs.length > 5 ? inputs[5].get() : null; + if (!broadcast && (targetAddress == null || targetAddress == "")) { + return; + } + + #if lnx_torrent + var socket = Leenkx.connections[connection._url]; + if (socket == null) return; + var lnxCn:LeenkxClient = socket.client; + if (lnxCn == null) return; + + var buffer = new Buffer(); + + switch (property1) { + case "string": + buffer.writeBytes(Bytes.ofString(message)); + case "vector": + var bytesOut = new haxe.io.BytesOutput(); + var f:Null; + if ((f = message.x) != null) bytesOut.writeFloat(f); + if ((f = message.y) != null) bytesOut.writeFloat(f); + if ((f = message.z) != null) bytesOut.writeFloat(f); + if ((f = message.w) != null) bytesOut.writeFloat(f); + buffer.writeBytes(bytesOut.getBytes()); + case "float": + var bytesOut = new haxe.io.BytesOutput(); + bytesOut.writeFloat(message); + buffer.writeBytes(bytesOut.getBytes()); + case "integer": + var bytesOut = new haxe.io.BytesOutput(); + bytesOut.writeInt32(message); + buffer.writeBytes(bytesOut.getBytes()); + case "boolean": + if (message == true) { + buffer.writeBytes(Bytes.ofString("true")); + } else { + buffer.writeBytes(Bytes.ofString("false")); + } + case "transform": + var bytesOut = new haxe.io.BytesOutput(); + var transform:Mat4 = message; + var loc = new Vec4(); + var rot = new Quat(); + var scale = new Vec4(); + transform.decompose(loc, rot, scale); + bytesOut.writeFloat(loc.x); + bytesOut.writeFloat(loc.y); + bytesOut.writeFloat(loc.z); + bytesOut.writeFloat(loc.w); + bytesOut.writeFloat(rot.x); + bytesOut.writeFloat(rot.y); + bytesOut.writeFloat(rot.z); + bytesOut.writeFloat(rot.w); + bytesOut.writeFloat(scale.x); + bytesOut.writeFloat(scale.y); + bytesOut.writeFloat(scale.z); + bytesOut.writeFloat(scale.w); + buffer.writeBytes(bytesOut.getBytes()); + case "rotation": + var bytesOut = new haxe.io.BytesOutput(); + var f:Null; + if ((f = message.x) != null) bytesOut.writeFloat(f); + if ((f = message.y) != null) bytesOut.writeFloat(f); + if ((f = message.z) != null) bytesOut.writeFloat(f); + if ((f = message.w) != null) bytesOut.writeFloat(f); + buffer.writeBytes(bytesOut.getBytes()); + default: trace("Failed to send data."); + } + + if (api != null && api != "") { + buffer.writeBytes(Bytes.ofString(api)); + } + + var rawBytes = buffer.readAllAvailableBytes(); try { - if(inputs[5].get() == true){ - var script = 'for (p in lnx_' + connection._url +'.torrent._peers){ - try{ - lnx_' + connection._url +'.torrent._peers[p].conn.send(`'+ api + message + '`); - }catch(error){ - console.log("Error: " + error); - } - }'; - js.Syntax.code('(1, eval)({0})', script.toString()); - runOutput(0); + if (property2) { + var payload = haxe.crypto.Base64.encode(rawBytes); + if (broadcast) { + lnxCn.send(null, payload); + } else { + lnxCn.send(targetAddress, payload); + } } else { - var script = 'lnx_' + connection._url +'.send(`' + inputs[4].get() + '`, `'+ api + message + '` );'; - js.Syntax.code('(1, eval)({0})', script.toString()); - runOutput(0); + lnxCn.sendRawMessage(rawBytes, broadcast ? null : targetAddress); } - } catch(error) { trace("Error: " + error); } - return; + runOutput(0); - switch (property1) { - case "string": - if(property0 == "client"){ - var buffer = new Buffer(); - buffer.writeBytes(Bytes.ofString(message)); - if(api != "" || api != null){ - buffer.writeBytes(Bytes.ofString(api)); - - try { - connection.buffer = buffer; - if(inputs[5].get() == true){ - var script = 'for (p in lnx_' + connection._url +'.torrent._peers){ - try{ - lnx_' + connection._url +'.torrent._peers[p].conn.send(leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer ); - }catch(error){ - console.log("Error: " + error); - } - }'; - js.Syntax.code('(1, eval)({0})', script.toString()); - runOutput(0); - } else { - var script = 'lnx_' + connection._url +'.send("' + inputs[4].get() + '",leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer );'; - js.Syntax.code('(1, eval)({0})', script.toString()); - runOutput(0); - } - } catch(error) { - trace("Error: " + error); - } - } - } else { - if(inputs[5].get() == true){ - var script = 'lnx_' + connection._url + '.send(`' + api + message + '`);'; - js.Syntax.code('(1, eval)({0})', script); - }else{ - var script = 'lnx_' + connection._url + '.send(`' + inputs[4].get() + '`, `' + api + message + '`);'; - js.Syntax.code('(1, eval)({0})', script); - } - runOutput(0); - return; - } - case "vector": - if(property0 == "client"){ - var bytesOut = new haxe.io.BytesOutput(); - var i = Reflect.fields(message); - for (b in i) { - bytesOut.writeFloat(Reflect.field(message, b)); - } - var buffer = new Buffer(); - buffer.writeBytes(bytesOut.getBytes()); - if(api != "" || api != null){ - buffer.writeBytes(Bytes.ofString(api)); - } + #else + runOutput(0); - try { - connection.buffer = buffer; - if(inputs[5].get() == true){ - var script = 'for (p in lnx_' + connection._url +'.torrent._peers){ - try{ - lnx_' + connection._url +'.torrent._peers[p].conn.send("' + api + message + '"); - }catch(error){ - console.log("Error: " + error); - } - }'; - js.Syntax.code('(1, eval)({0})', script.toString()); - runOutput(0); - } else { - var script = 'lnx_' + connection._url +'.send("' + inputs[4].get() + '",leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer );'; - js.Syntax.code('(1, eval)({0})', script.toString()); - runOutput(0); - } - } catch(error) { - trace("Error: " + error); - } - } else { - if(inputs[5].get() == true){ - var script = 'lnx_' + connection._url + '.send(`' + api + message + '`);'; - js.Syntax.code('(1, eval)({0})', script); - }else{ - var script = 'lnx_' + connection._url + '.send(`' + inputs[4].get() + '`, `' + api + message + '`);'; - js.Syntax.code('(1, eval)({0})', script); - } - runOutput(0); - return; - } - case "float": - if(property0 == "client"){ - var bytesOut = new haxe.io.BytesOutput(); - bytesOut.writeFloat(message); - var buffer = new Buffer(); - buffer.writeBytes(bytesOut.getBytes()); - if(api != "" || api != null){ - buffer.writeBytes(Bytes.ofString(api)); - } - try { - connection.buffer = buffer; - if(inputs[5].get() == true){ - var script = 'for (p in lnx_' + connection._url +'.torrent._peers){ - try{ - lnx_' + connection._url +'.torrent._peers[p].conn.send(leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer ); - }catch(error){ - console.log("Error: " + error); - } - }'; - js.Syntax.code('(1, eval)({0})', script.toString()); - runOutput(0); - } else { - var script = 'lnx_' + connection._url +'.send("' + inputs[4].get() + '",leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer );'; - js.Syntax.code('(1, eval)({0})', script.toString()); - runOutput(0); - } - } catch(error) { - trace("Error: " + error); - } - } else { - if(inputs[5].get() == true){ - var script = 'lnx_' + connection._url + '.send(`' + api + message + '`);'; - js.Syntax.code('(1, eval)({0})', script); - }else{ - var script = 'lnx_' + connection._url + '.send(`' + inputs[4].get() + '`, `' + api + message + '`);'; - js.Syntax.code('(1, eval)({0})', script); - } - runOutput(0); - return; - } - case "integer": - if(property0 == "client"){ - var bytesOut = new haxe.io.BytesOutput(); - bytesOut.writeInt32(message); - var buffer = new Buffer(); - buffer.writeBytes(bytesOut.getBytes()); - if(api != "" || api != null){ - buffer.writeBytes(Bytes.ofString(api)); - } - try { - connection.buffer = buffer; - if(inputs[5].get() == true){ - var script = 'for (p in lnx_' + connection._url +'.torrent._peers){ - try{ - lnx_' + connection._url +'.torrent._peers[p].conn.send(leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer ); - }catch(error){ - console.log("Error: " + error); - } - }'; - js.Syntax.code('(1, eval)({0})', script.toString()); - runOutput(0); - } else { - var script = 'lnx_' + connection._url +'.send("' + inputs[4].get() + '",leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer );'; - js.Syntax.code('(1, eval)({0})', script.toString()); - runOutput(0); - } - } catch(error) { - trace("Error: " + error); - } - } else { - if(inputs[5].get() == true){ - var script = 'lnx_' + connection._url + '.send(`' + api + message + '`);'; - js.Syntax.code('(1, eval)({0})', script); - }else{ - var script = 'lnx_' + connection._url + '.send(`' + inputs[4].get() + '`, `' + api + message + '`);'; - js.Syntax.code('(1, eval)({0})', script); - } - runOutput(0); - return; - } - case "boolean": - if(property0 == "client"){ - var buffer = new Buffer(); - if(message == true){ - buffer.writeBytes(Bytes.ofString("true")); - } else { - buffer.writeBytes(Bytes.ofString("false")); - } - if(api != "" || api != null){ - buffer.writeBytes(Bytes.ofString(api)); - } - try { - connection.buffer = buffer; - if(inputs[5].get() == true){ - var script = 'for (p in lnx_' + connection._url +'.torrent._peers){ - try{ - lnx_' + connection._url +'.torrent._peers[p].conn.send(leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer ); - }catch(error){ - console.log("Error: " + error); - } - }'; - js.Syntax.code('(1, eval)({0})', script.toString()); - runOutput(0); - } else { - var script = 'lnx_' + connection._url +'.send("' + inputs[4].get() + '",leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer );'; - js.Syntax.code('(1, eval)({0})', script.toString()); - runOutput(0); - } - } catch(error) { - trace("Error: " + error); - } - } else { - if(inputs[5].get() == true){ - var script = 'lnx_' + connection._url + '.send(`' + api + message + '`);'; - js.Syntax.code('(1, eval)({0})', script); - }else{ - var script = 'lnx_' + connection._url + '.send(`' + inputs[4].get() + '`, `' + api + message + '`);'; - js.Syntax.code('(1, eval)({0})', script); - } - runOutput(0); - return; - } - case "transform": - if(property0 == "client"){ - var bytesOut = new haxe.io.BytesOutput(); - var transform:Mat4 = message; - var loc = new Vec4(); - var rot = new Quat(); - var scale = new Vec4(); - transform.decompose(loc, rot, scale); - var l = Reflect.fields(loc); - for (b in l) { - bytesOut.writeFloat(Reflect.field(loc, b)); - } - var r = Reflect.fields(rot); - for (b in r) { - bytesOut.writeFloat(Reflect.field(rot, b)); - } - var s = Reflect.fields(scale); - for (b in s) { - bytesOut.writeFloat(Reflect.field(scale, b)); - } - var buffer = new Buffer(); - buffer.writeBytes(bytesOut.getBytes()); - if(api != "" || api != null){ - buffer.writeBytes(Bytes.ofString(api)); - } - try { - - connection.buffer = buffer; - - if(inputs[5].get() == true){ - var script = 'for (p in lnx_' + connection._url +'.torrent._peers){ - try{ - //console.log("Mine: " + lx_8001.torrent.discovery.peerId + " || Incomming: " + p); - lnx_' + connection._url +'.torrent._peers[p].conn.send(leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer ); - }catch(error){ - console.log("Error: " + error); - } - }'; - js.Syntax.code('(1, eval)({0})', script.toString()); - runOutput(0); - } else { - //var script = 'lnx_' + connection._url +'.send("' + inputs[4].get() + '",leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer );'; - var script = 'lnx_' + connection._url +'.send(leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer );'; - js.Syntax.code('(1, eval)({0})', script.toString()); - runOutput(0); - } - - } catch(error) { - trace("Error: " + error); - } - } - case "rotation": - if(property0 == "client"){ - var bytesOut = new haxe.io.BytesOutput(); - var i = Reflect.fields(message); - for (b in i) { - bytesOut.writeFloat(Reflect.field(message, b)); - } - var buffer = new Buffer(); - buffer.writeBytes(bytesOut.getBytes()); - if(api != "" || api != null){ - buffer.writeBytes(Bytes.ofString(api)); - } - try { - connection.buffer = buffer; - if(inputs[5].get() == true){ - var script = 'for (p in lnx_' + connection._url +'.torrent._peers){ - try{ - lnx_' + connection._url +'.torrent._peers[p].conn.send(leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer ); - }catch(error){ - console.log("Error: " + error); - } - }'; - js.Syntax.code('(1, eval)({0})', script.toString()); - runOutput(0); - } else { - var script = 'lnx_' + connection._url +'.send("' + inputs[4].get() + '",leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer );'; - js.Syntax.code('(1, eval)({0})', script.toString()); - runOutput(0); - } - } catch(error) { - trace("Error: " + error); - } - } else { - if(inputs[5].get() == true){ - var script = 'lnx_' + connection._url + '.send(`' + api + message + '`);'; - js.Syntax.code('(1, eval)({0})', script); - }else{ - var script = 'lnx_' + connection._url + '.send(`' + inputs[4].get() + '`, `' + api + message + '`);'; - js.Syntax.code('(1, eval)({0})', script); - } - runOutput(0); - return; - } - } #end } - } - - - - -/*******_________________________________________ - - try { - - if(inputs[5].get() == true){ - var script = 'for (p in lnx_' + connection._url +'.torrent._peers){ - try{ - lnx_' + connection._url +'.torrent._peers[p].conn.send("' + api + message + '"); - }catch(error){ - console.log("Error: " + error); - } - }'; - js.Syntax.code('(1, eval)({0})', script.toString()); - runOutput(0); - } else { - var script = 'for (p in lnx_' + connection._url +'.torrent._peers){ - try{ - - lx_' + connection._url +'.torrent._peers[' + inputs[4].get() + '].conn.send("' + api + message + '"); - }catch(error){ - console.log("Error: " + error); - } - }'; - js.Syntax.code('(1, eval)({0})', script.toString()); - runOutput(0); - } - } catch(error) { - trace("Error: " + error); - } - return; - -_________________________________________*****/ - - - - - - - - - - diff --git a/leenkx/Sources/leenkx/logicnode/NetworkMessageParserNode.hx b/leenkx/Sources/leenkx/logicnode/NetworkMessageParserNode.hx index eda33bb2..91dfc39b 100644 --- a/leenkx/Sources/leenkx/logicnode/NetworkMessageParserNode.hx +++ b/leenkx/Sources/leenkx/logicnode/NetworkMessageParserNode.hx @@ -23,6 +23,7 @@ class NetworkMessageParserNode extends LogicNode { var buffer = cast(data, Buffer); api = inputs[1].get(); + if (api == null) api = ""; if(buffer.endsWith(api) == false){ return; } diff --git a/leenkx/Sources/leenkx/logicnode/OnTorrentEventNode.hx b/leenkx/Sources/leenkx/logicnode/OnTorrentEventNode.hx new file mode 100644 index 00000000..92a77c09 --- /dev/null +++ b/leenkx/Sources/leenkx/logicnode/OnTorrentEventNode.hx @@ -0,0 +1,65 @@ +package leenkx.logicnode; +#if lnx_torrent +import leenkx.network.Leenkx; +import leenkx.network.torrent.Torrent; +#end + +class OnTorrentEventNode extends LogicNode { + public var property0:String; + public var eventValue:Dynamic; + var net_Url:String; + var torrent:Dynamic; + var registeredTorrents:Map = new Map(); + + public function new(tree:LogicTree) { + super(tree); + } + + override function run(from:Int) { + #if lnx_torrent + var connection = inputs[1].get(); + if (connection == null) return; + var id = inputs[2].get(); + if (id == null) return; + net_Url = connection._url; + + var socket = Leenkx.connections[connection._url]; + var client = socket != null ? socket.client : null; + var t = client != null ? client.get(id) : (Std.isOfType(id, Torrent) ? id : null); + if (t == null) return; + + var event = property0 != null ? property0 : "done"; + var self = this; + var fire = function(v:Dynamic) { + self.eventValue = v; + self.runOutput(0); + }; + var key = t.infoHashHex != null ? t.infoHashHex : "v2:" + t.infoHashV2Hex; + var alreadyRegistered = registeredTorrents.exists(key); + if (!alreadyRegistered) registeredTorrents.set(key, true); + switch (event) { + case "ready": + if (t.ready) fire(null); + else if (!alreadyRegistered) t.on("ready", function() fire(null)); + case "piece" | "wire" | "metadata" | "error": + if (!alreadyRegistered) t.on(event, function(v) fire(v)); + default: + if (t.done) fire(null); + else if (!alreadyRegistered) t.on("done", function() fire(null)); + } + torrent = t; + #end + } + + override function get(from:Int):Dynamic { + #if lnx_torrent + return switch (from) { + case 1: torrent; + case 2: eventValue; + default: throw "Unreachable"; + } + #else + return null; + #end + } +} diff --git a/leenkx/Sources/leenkx/logicnode/ParseTorrentNode.hx b/leenkx/Sources/leenkx/logicnode/ParseTorrentNode.hx new file mode 100644 index 00000000..ee608e41 --- /dev/null +++ b/leenkx/Sources/leenkx/logicnode/ParseTorrentNode.hx @@ -0,0 +1,73 @@ +package leenkx.logicnode; +#if lnx_torrent +import haxe.io.Bytes; +import leenkx.network.torrent.Magnet; +import leenkx.network.torrent.MetaInfo; +#end + +class ParseTorrentNode extends LogicNode { + var meta:Dynamic; + var magnet:Dynamic; + var idHex:String; + public var property0:String; + + public function new(tree:LogicTree) { + super(tree); + } + + override function run(from:Int) { + #if lnx_torrent + meta = null; + magnet = null; + idHex = null; + var src = inputs[1].get(); + if (src == null) return; + + switch (property0 != null ? property0 : "magnet") { + case "file": + if (Std.isOfType(src, MetaInfo)) { + meta = src; + } else { + var b = Std.isOfType(src, Bytes) ? src : Bytes.ofString(Std.string(src)); + try { + meta = MetaInfo.fromTorrentBytes(b); + } catch(e:Dynamic) { + trace(e); + return; + } + } + case "id": + var s = Std.string(src); + idHex = s.length == 64 ? "v2:" + s : s; + default: + var m = Magnet.parse(Std.string(src)); + if (m == null) { + return; + } + magnet = m; + idHex = m.infoHashHex != null ? m.infoHashHex : m.infoHashV2Hex; + } + #end + runOutput(0); + } + + override function get(from:Int):Dynamic { + #if lnx_torrent + var m:MetaInfo = meta; + var mg:Magnet = magnet; + return switch (from) { + case 1: m; + case 2: m != null ? (m.infoHashHex != null ? m.infoHashHex : m.infoHashV2Hex) : idHex; + case 3: m != null ? m.name : (mg != null ? mg.name : null); + case 4: m != null ? m.announce : (mg != null ? mg.trackers : null); + case 5: m != null ? m.urlList : (mg != null ? mg.webSeeds : null); + case 6: m != null ? m.files : null; + case 7: m != null ? m.totalLength : 0; + case 8: m != null ? m.toTorrentBytes() : null; + default: throw "Unreachable"; + } + #else + return null; + #end + } +} diff --git a/leenkx/Sources/leenkx/logicnode/SeedTorrentNode.hx b/leenkx/Sources/leenkx/logicnode/SeedTorrentNode.hx index 5874d3a2..1d7df577 100644 --- a/leenkx/Sources/leenkx/logicnode/SeedTorrentNode.hx +++ b/leenkx/Sources/leenkx/logicnode/SeedTorrentNode.hx @@ -1,52 +1,124 @@ package leenkx.logicnode; -import leenkx.system.Event; -import iron.object.Object; - +#if lnx_torrent +import haxe.io.Bytes; +import leenkx.network.Leenkx; +import leenkx.network.torrent.MetaInfo; +import leenkx.network.torrent.Torrent; +#end class SeedTorrentNode extends LogicNode { - public var torrent: Dynamic; - public var net_Url: Dynamic; - + public var torrent:Dynamic; + public var net_Url:String; public function new(tree:LogicTree) { super(tree); - } - + } override function run(from:Int) { - #if js + #if lnx_torrent var connection = inputs[1].get(); - if (connection == null) return; - trace("got connection"); - var data = inputs[3].get(); - if (data == null) return; - trace("got data"); - var opts = inputs[2].get(); + if (connection == null) return; + var src = inputs[2].get(); net_Url = connection._url; - var cx = "lx_" + net_Url; - var lx:Dynamic = untyped js.Browser.window[cx]; - var args = opts.toString(); - trace(args); - var func = Reflect.field(lx.wt, "seed"); - if (func != null) { - torrent = func(data, function (torrent) { - trace('Client is seeding ' + torrent.magnetURI); - }); - //trace(torrent); + var socket = Leenkx.connections[connection._url]; + if (socket == null || socket.client == null) return; + var client = socket.client; + var self = this; + + var onSeeded = function(t:Torrent) { + if (t == null) return; + self.torrent = t; + var onDone = function() { + self.runOutput(2); + }; + if (t.done) onDone(); + else t.on("done", onDone); + }; + + if (Std.isOfType(src, Torrent)) { + var t:Torrent = src; + t.start(); + torrent = t; + runOutput(0); + return; } + var meta:MetaInfo = null; + if (Std.isOfType(src, MetaInfo)) { + meta = cast src; + } else if (Std.isOfType(src, Bytes)) { + try { + meta = MetaInfo.fromTorrentBytes(cast src); + } catch(e:Dynamic) { + return; + } + } + if (meta == null) { + return; + } + var files = filesFor(meta, inputs.length > 3 ? inputs[3].get() : null); + if (files == null) { + return; + } + client.seedMeta(meta, files, null, onSeeded); runOutput(0); #end } - override function get(from: Int): Dynamic { - return switch (from) { - case 1: torrent; - default: throw "Unreachable"; - } - } + #if lnx_torrent + function filesFor(meta:MetaInfo, data:Dynamic):Array<{path:String, data:Bytes}> { + if (data == null) return null; + var entries = meta.files; + if (Std.isOfType(data, Bytes)) { + var blob:Bytes = data; + if (blob.length != meta.totalLength) return null; + var files:Array<{path:String, data:Bytes}> = []; + var cursor = 0; + for (f in entries) { + files.push({path: f.path, data: blob.sub(cursor, f.length)}); + cursor += f.length; + } + return files; + } + if (Std.isOfType(data, Array)) { + var items:Array = data; + var files:Array<{path:String, data:Bytes}> = []; + for (it in items) { + var p = Reflect.field(it, "path"); + var d:Bytes = Reflect.field(it, "data"); + if (d == null) d = Reflect.field(it, "content"); + if (p == null || d == null) continue; + var sp = Std.string(p); + var matched = false; + for (f in entries) { + if (f.path == sp || StringTools.endsWith(f.path, "/" + sp)) { + files.push({path: f.path, data: d}); + matched = true; + break; + } + } + if (!matched) files.push({path: sp, data: d}); + } + if (files.length == entries.length) return files; + return null; + } + return null; + } + #end + override function get(from:Int):Dynamic { + #if lnx_torrent + var t:Torrent = torrent; + return switch (from) { + case 1: torrent; + case 3: t != null ? t.infoHashHex : null; + case 4: t != null ? t.magnetURI : null; + default: throw "Unreachable"; + } + #else + return null; + #end + } } - diff --git a/leenkx/Sources/leenkx/logicnode/SetTrackersNode.hx b/leenkx/Sources/leenkx/logicnode/SetTrackersNode.hx new file mode 100644 index 00000000..c007cb43 --- /dev/null +++ b/leenkx/Sources/leenkx/logicnode/SetTrackersNode.hx @@ -0,0 +1,90 @@ +package leenkx.logicnode; +#if lnx_torrent +import leenkx.network.Leenkx; +import leenkx.network.torrent.Torrent; +import leenkx.network.torrent.TorrentClient; +#end + +class SetTrackersNode extends LogicNode { + public var property0:String; + public var target:Dynamic; + + public function new(tree:LogicTree) { + super(tree); + } + + override function run(from:Int) { + #if lnx_torrent + var p = property0 != null ? property0 : "add"; + switch (p) { + case "add" | "override": + setTorrentTrackers(p == "override"); + default: + setTrackerProperty(p); + } + #end + runOutput(0); + } + + #if lnx_torrent + function setTorrentTrackers(trackerOverride:Bool):Void { + var id = inputs[2].get(); + var urls = TorrentClient.trackerUrls(inputs[3].get()); + if (urls == null) return; + var t:Torrent = Std.isOfType(id, Torrent) ? id : null; + if (t == null && id != null) { + var connection = inputs[1].get(); + var socket = connection != null ? Leenkx.connections[connection._url] : null; + var client = socket != null ? socket.client : null; + if (client != null) t = client.get(id); + } + if (t != null) { + target = t; + if (trackerOverride) { + t.setTrackers(urls); + } else { + for (u in urls) t.addTracker(u); + } + } else if (id == null) { + target = null; + if (trackerOverride) { + TorrentClient.defaultTrackers = urls; + } else { + TorrentClient.mergeTrackers(TorrentClient.defaultTrackers, urls); + } + } + } + + function setTrackerProperty(prop:String):Void { + var tracker:Dynamic = inputs[1].get(); + if (tracker == null) return; + var v = inputs[2].get(); + if (v == null) return; + var field = prop == "interval" ? "intervalSec" : prop; + var val:Dynamic = field == "trackerId" ? Std.string(v) + : Std.int(Std.parseFloat(Std.string(v))); + var setter:Dynamic = field == "intervalSec" + && Reflect.field(tracker, "intervalSec") == null + ? Reflect.field(tracker, "setAnnounceInterval") : null; + if (setter != null) { + try { + Reflect.callMethod(tracker, setter, [(val:Int) * 1000]); + } catch(e:Dynamic) {} + } else { + Reflect.setField(tracker, field, val); + } + target = tracker; + } + #end + + override function get(from:Int):Dynamic { + #if lnx_torrent + return switch (from) { + case 1: target; + default: throw "Unreachable"; + } + #else + return null; + #end + } +} diff --git a/leenkx/Sources/leenkx/logicnode/TorrentClientInfoNode.hx b/leenkx/Sources/leenkx/logicnode/TorrentClientInfoNode.hx new file mode 100644 index 00000000..59d45f83 --- /dev/null +++ b/leenkx/Sources/leenkx/logicnode/TorrentClientInfoNode.hx @@ -0,0 +1,74 @@ +package leenkx.logicnode; +#if lnx_torrent +import leenkx.network.Leenkx; +import leenkx.network.torrent.Torrent; +#end + +class TorrentClientInfoNode extends LogicNode { + var stats:Dynamic; + var list:Array; + + public function new(tree:LogicTree) { + super(tree); + } + + override function run(from:Int) { + #if lnx_torrent + list = []; + stats = {downRate: 0.0, upRate: 0.0, progress: 0.0, ratio: 0.0}; + var connection = inputs[1].get(); + if (connection != null) { + var socket = Leenkx.connections[connection._url]; + var client = socket != null ? socket.client : null; + if (client != null) collect(client); + } + #end + runOutput(0); + } + + #if lnx_torrent + function collect(client:Dynamic) { + var down = 0.0; + var up = 0.0; + var received = 0.0; + var sent = 0.0; + var total = 0.0; + var left = 0.0; + var torrents:Map = client.torrents; + for (t in torrents) { + list.push(t); + var s = t.getStats(); + down += s.downRate; + up += s.upRate; + received += s.downloaded; + sent += s.uploaded; + if (t.meta != null) { + total += t.meta.totalLength; + left += s.left; + } + } + stats = { + downRate: down, + upRate: up, + progress: total > 0 ? 1.0 - left / total : 0.0, + ratio: received > 0 ? sent / received : 0.0 + }; + } + #end + + override function get(from:Int):Dynamic { + #if lnx_torrent + return switch (from) { + case 1: list; + case 2: list.length; + case 3: stats.downRate; + case 4: stats.upRate; + case 5: stats.progress; + case 6: stats.ratio; + default: throw "Unreachable"; + } + #else + return null; + #end + } +} diff --git a/leenkx/Sources/leenkx/logicnode/TorrentControlNode.hx b/leenkx/Sources/leenkx/logicnode/TorrentControlNode.hx new file mode 100644 index 00000000..ac5b78ee --- /dev/null +++ b/leenkx/Sources/leenkx/logicnode/TorrentControlNode.hx @@ -0,0 +1,93 @@ +package leenkx.logicnode; +#if lnx_torrent +import leenkx.network.Leenkx; +import leenkx.network.torrent.Torrent; +#end + +class TorrentControlNode extends LogicNode { + public var property0:String; + var net_Url:String; + + public function new(tree:LogicTree) { + super(tree); + } + + override function run(from:Int) { + #if lnx_torrent + var connection = inputs[1].get(); + if (connection == null) return; + var id = inputs[2].get(); + if (id == null) return; + net_Url = connection._url; + + var socket = Leenkx.connections[connection._url]; + if (socket == null) return; + var client = socket.client; + var t = client != null ? client.get(id) : (Std.isOfType(id, Torrent) ? id : null); + var action = property0 != null ? property0 : "remove"; + + var file:Dynamic = inputs.length > 3 ? inputs[3].get() : null; + var t2 = t; + var startB = 0; + var endB = -1; + if (file != null) { + startB = file.offset; + endB = file.offset + file.length - 1; + if (t2 == null) t2 = file.torrent; + } else { + var sv = inputs.length > 4 ? inputs[4].get() : null; + var ev = inputs.length > 5 ? inputs[5].get() : null; + if (sv != null && Std.string(sv) != "") { + startB = Std.int(Std.parseFloat(Std.string(sv))); + } + if (ev != null && Std.string(ev) != "") { + endB = Std.int(Std.parseFloat(Std.string(ev))); + } + } + var pv = inputs.length > 6 ? inputs[6].get() : null; + var priority = pv != null && Std.string(pv) != "" ? Std.int(Std.parseFloat(Std.string(pv))) : null; + + switch (action) { + case "start": + if (t != null) t.start(); + case "stop": + if (t != null) t.stop(); + case "select", "critical", "deselect": + if (t2 != null) { + if (endB < 0) { + endB = t2.meta != null ? t2.meta.totalLength - 1 : 0; + } + if (action == "critical") { + t2.critical(startB, endB); + } else if (action == "select") { + t2.select(startB, endB, priority); + } else { + t2.deselect(startB, endB, priority); + } + } + case "add-peer": + var peer = inputs.length > 3 ? inputs[3].get() : null; + if (t != null && peer != null) { + t.addPeer(Std.string(peer)); + } + case "add-web-seed": + var url = inputs.length > 3 ? inputs[3].get() : null; + if (t != null && url != null) { + t.addWebSeed(Std.string(url)); + } + case "remove", "remove-data": + var hex = t != null ? t.infoHashHex : Std.string(id); + if (hex == null) hex = Std.string(id); + if (client != null) { + client.remove(hex, action == "remove-data"); + } + if (t != null && Leenkx.torrent.get(net_Url) == t) { + Leenkx.torrent.remove(net_Url); + Leenkx.file.remove(net_Url); + } + default: + } + #end + runOutput(0); + } +} diff --git a/leenkx/Sources/leenkx/logicnode/TorrentInfoNode.hx b/leenkx/Sources/leenkx/logicnode/TorrentInfoNode.hx new file mode 100644 index 00000000..89b8f0a8 --- /dev/null +++ b/leenkx/Sources/leenkx/logicnode/TorrentInfoNode.hx @@ -0,0 +1,74 @@ +package leenkx.logicnode; +#if lnx_torrent +import leenkx.network.Leenkx; +import leenkx.network.torrent.Torrent; +import leenkx.network.torrent.TorrentClient; +#end + +class TorrentInfoNode extends LogicNode { + public var torrent:Dynamic; + public var net_Url:String; + + public function new(tree:LogicTree) { + super(tree); + } + + override function run(from:Int) { + #if lnx_torrent + var id = inputs[2].get(); + if (id == null) return; + torrent = null; + + if (Std.isOfType(id, Torrent)) { + torrent = id; + } else { + var connection = inputs[1].get(); + if (connection == null) return; + net_Url = connection._url; + var socket = Leenkx.connections[net_Url]; + if (socket == null || socket.client == null) return; + torrent = socket.client.get(id); + } + #end + runOutput(0); + } + + override function get(from:Int):Dynamic { + #if lnx_torrent + var t:Torrent = torrent; + return switch (from) { + case 1: t != null ? t.infoHashHex : null; + case 2: t != null && t.meta != null ? t.meta.name : null; + case 3: t != null ? t.numPeers : 0; + case 4: progress(t); + case 5: t != null ? t.getStats().downloaded : 0.0; + case 6: t != null ? t.getStats().uploaded : 0.0; + case 7: t != null ? t.done : false; + case 8: t != null ? t.magnetURI : null; + case 9: t != null ? t.files : null; + case 10: t != null && t.meta != null ? t.meta.numPieces : 0; + case 11: t != null && t.meta != null ? t.meta.pieceLength : 0; + case 12: t != null && t.meta != null ? t.meta.totalLength : 0; + case 13: t != null && t.meta != null ? t.meta.comment : null; + case 14: t != null && t.meta != null ? t.meta.createdBy : null; + case 15: t != null && t.meta != null ? t.meta.protocolVersion : 1; + case 16: t != null ? t.infoHashV2Hex : null; + case 17: t != null ? t.getStats().downRate : 0.0; + case 18: t != null ? t.getStats().upRate : 0.0; + case 19: t != null ? t.stopped : false; + case 20: t; + default: throw "Unreachable"; + } + #else + return null; + #end + } + + #if lnx_torrent + function progress(t:Torrent):Float { + if (t == null || t.meta == null || t.meta.totalLength == 0) return 0; + var stats = t.getStats(); + return 1.0 - stats.left / t.meta.totalLength; + } + #end +} diff --git a/leenkx/Sources/leenkx/network/Leenkx.hx b/leenkx/Sources/leenkx/network/Leenkx.hx index 65412591..2ed27796 100644 --- a/leenkx/Sources/leenkx/network/Leenkx.hx +++ b/leenkx/Sources/leenkx/network/Leenkx.hx @@ -121,7 +121,6 @@ class Leenkx { class LeenkxSocket { public var _url:String; public var client:LeenkxClient; - public var torrentClient:leenkx.network.torrent.TorrentClient; public var buffer:Dynamic; public var onopen:Void->Void; public var onclose:Void->Void; diff --git a/leenkx/Sources/leenkx/network/torrent/LeenkxClient.hx b/leenkx/Sources/leenkx/network/torrent/LeenkxClient.hx index 6dbd9655..3243031b 100644 --- a/leenkx/Sources/leenkx/network/torrent/LeenkxClient.hx +++ b/leenkx/Sources/leenkx/network/torrent/LeenkxClient.hx @@ -9,7 +9,8 @@ import leenkx.network.torrent.TorrentInfo; class LeenkxClient { - static var PEERTIMEOUT:Float = 5 * 60 * 1000; + public static var PEERTIMEOUT:Float = 5 * 60 * 1000; + public static var defaultHeartbeat:Int = 30000; public var identifier:String; public var seed:String; @@ -53,6 +54,54 @@ class LeenkxClient { private var deferredOpts:TransportOpts = null; private var heartbeatInterval:Int = 30000; + public var torrents(get, never):Map; + public var onTorrentAdded:Torrent -> Void; + public var onTorrentDone:Torrent -> Void; + var swarm:TorrentClient = null; + + function get_torrents():Map { + return swarm != null ? swarm.torrents : []; + } + + function getSwarm():TorrentClient { + if (swarm == null) { + swarm = new TorrentClient(); + swarm.onError = function(e) { + if (onError != null) onError(e); + }; + swarm.onTorrentAdded = function(t) { + if (onTorrentAdded != null) onTorrentAdded(t); + }; + swarm.onTorrentDone = function(t) { + if (onTorrentDone != null) onTorrentDone(t); + }; + } + return swarm; + } + + public function add(torrentId:Dynamic, ?opts:Dynamic, ?cb:Torrent -> Void):Torrent { + return getSwarm().add(torrentId, opts, cb); + } + + public function seedMeta(meta:MetaInfo, + files:Array<{path:String, data:Bytes}>, + ?opts:Dynamic, ?cb:Torrent -> Void):Torrent { + return getSwarm().seedMeta(meta, files, opts, cb); + } + + public function get(id:Dynamic):Torrent { + if (Std.isOfType(id, Torrent)) return id; + return swarm != null ? swarm.get(id) : null; + } + + public function remove(infoHashHex:String, ?deleteData:Bool):Void { + if (swarm != null) swarm.remove(infoHashHex, deleteData); + } + + public function listen(port:Int):Void { + getSwarm().listen(port); + } + public function new(?identifier:String, ?opts:LeenkxOptions) { if (identifier != null && !Std.isOfType(identifier, String)) { opts = cast identifier; @@ -60,7 +109,7 @@ class LeenkxClient { } opts = opts != null ? opts : {}; - this.announce = opts.announce != null ? opts.announce : ["wss://ws1.leenkx.com"]; + this.announce = opts.announce != null ? opts.announce : TorrentClient.defaultTrackers; this.timeout = opts.timeout != null ? opts.timeout : PEERTIMEOUT; if (opts.seed != null) { @@ -90,7 +139,7 @@ class LeenkxClient { createTransport(opts, opts.deferJoin == true); - this.heartbeatInterval = opts.heartbeat != null ? opts.heartbeat : 30000; + this.heartbeatInterval = opts.heartbeat != null ? opts.heartbeat : defaultHeartbeat; } function createTransport(opts:LeenkxOptions, defer:Bool):Void { @@ -281,6 +330,10 @@ class LeenkxClient { } var packet = makePacket({y: PacketType.Leave}); sendRaw(packet); + if (swarm != null) { + swarm.destroy(); + swarm = null; + } if (transport != null) { transport.destroy(cb); } else if (cb != null) { diff --git a/leenkx/Sources/leenkx/network/torrent/Torrent.hx b/leenkx/Sources/leenkx/network/torrent/Torrent.hx index 8507bf66..9bb070b9 100644 --- a/leenkx/Sources/leenkx/network/torrent/Torrent.hx +++ b/leenkx/Sources/leenkx/network/torrent/Torrent.hx @@ -39,12 +39,9 @@ class Torrent { public var announcePort:Int = 0; public var downloadDir:String = null; - public function getStats():{downloaded:Float, uploaded:Float, - left:Float, downRate:Float, upRate:Float} { + public function getStats():{downloaded:Float, uploaded:Float, left:Float, downRate:Float, upRate:Float} { var total = meta != null ? meta.totalLength : 0; - var downloaded = pieces != null - ? Math.min(pieces.numHave * (meta.pieceLength:Float), total) - : 0; + var downloaded = pieces != null ? Math.min(pieces.numHave * (meta.pieceLength:Float), total) : 0; var now = haxe.Timer.stamp(); var dt = now - lastStatT; var downRate:Float = 0; @@ -93,8 +90,7 @@ class Torrent { public function new(infoHash:Bytes, peerId:Bytes, trackers:Array, ?meta:MetaInfo, ?storage:IStorage, ?downloadDir:String) { TorrentClient.initMain(); this.infoHash = infoHash; - this.infoHashHex = infoHash != null - ? Crypto.toHex(infoHash) : null; + this.infoHashHex = infoHash != null ? Crypto.toHex(infoHash) : null; this.peerId = peerId; this.trackers = trackers; this.storage = storage; @@ -121,12 +117,9 @@ class Torrent { function setMeta(m:MetaInfo):Void { meta = m; if (storage == null) { - var sz = m.storageLength > 0 ? m.storageLength - : m.totalLength; + var sz = m.storageLength > 0 ? m.storageLength : m.totalLength; #if (sys || kha_krom) - storage = downloadDir != null - ? new FileTreeStorage(downloadDir, m.files) - : new MemoryStorage(sz); + storage = downloadDir != null ? new FileTreeStorage(downloadDir, m.files) : new MemoryStorage(sz); #else storage = new MemoryStorage(sz); #end @@ -139,8 +132,7 @@ class Torrent { if (m.fileHashes == null && m.files != null) { m.fileHashes = [for (f in m.files) null]; } - files = [for (f in m.files) - new TorrentFile(f, storage, this)]; + files = [for (f in m.files) new TorrentFile(f, storage, this)]; infoHashV2 = m.infoHashV2; infoHashV2Hex = m.infoHashV2Hex; if (infoHash == null && m.infoHash != null) { @@ -151,8 +143,7 @@ class Torrent { if (m.urlList == null && pendingWebSeeds != null) { m.urlList = pendingWebSeeds; } - magnetURI = Magnet.build(m.infoHash, m.name, trackers, - m.urlList, m.infoHashV2); + magnetURI = Magnet.build(m.infoHash, m.name, trackers, m.urlList, m.infoHashV2); ready = true; initWebSeeds(); emit1("metadata", m); @@ -214,6 +205,24 @@ class Torrent { return startTracker(url); } + public function setTrackers(urls:Array):Void { + if (stopped) { + trackers = urls != null ? urls.copy() : []; + return; + } + if (rtcPool != null) { + rtcPool.close(); + rtcPool = null; + } + for (t in httpTrackers) t.stop(); + for (t in udpTrackers) t.stop(); + httpTrackers = []; + udpTrackers = []; + trackerObjects = []; + trackers = urls != null ? urls.copy() : []; + startTrackers(); + } + function startTracker(url:String):Dynamic { var infoHashBin = TorrentInfo.toBinaryString(infoHash); var peerIdBin = TorrentInfo.toBinaryString(peerId); @@ -231,9 +240,7 @@ class Torrent { } #if (sys || kha_krom) else if (url.indexOf("http") == 0) { - var t = new leenkx.network.torrent.tracker.HttpTracker( - url, infoHash, peerId, - announcePort > 0 ? announcePort : 6881); + var t = new leenkx.network.torrent.tracker.HttpTracker(url, infoHash, peerId, announcePort > 0 ? announcePort : 6881); t.statsProvider = getStats; t.onPeers = onClassicPeers; t.onError = function(e) { @@ -246,9 +253,7 @@ class Torrent { #end #if (sys || kha_krom) else if (url.indexOf("udp") == 0) { - var t = new leenkx.network.torrent.tracker.UdpTracker( - url, infoHash, peerId, - announcePort > 0 ? announcePort : 6881); + var t = new leenkx.network.torrent.tracker.UdpTracker(url, infoHash, peerId, announcePort > 0 ? announcePort : 6881); t.statsProvider = getStats; t.onPeers = onClassicPeers; t.onError = function(e) { @@ -272,8 +277,7 @@ class Torrent { for (p in list) { var key = "tcp:" + p.host + ":" + p.port; if (wires.exists(key)) continue; - var ch = new leenkx.network.torrent.peer.TcpPeer( - p.host, p.port); + var ch = new leenkx.network.torrent.peer.TcpPeer(p.host, p.port); addWire(key, ch); } } @@ -308,8 +312,7 @@ class Torrent { requestBlocks(key); }; wire.onHave = function(index) { - if (meta == null || index < 0 - || index >= meta.numPieces) return; + if (meta == null || index < 0 || index >= meta.numPieces) return; var bf = peerBitfields.get(key); if (bf != null && index < bf.length) bf[index] = true; requestBlocks(key); @@ -317,17 +320,13 @@ class Torrent { wire.onPiece = function(index, begin, data) { var n = outstanding.get(key); if (n != null && n > 0) outstanding.set(key, n - 1); - if (pieces != null && index >= 0 - && index < meta.numPieces - && pieces.addBlock(index, begin, data)) { + if (pieces != null && index >= 0 && index < meta.numPieces && pieces.addBlock(index, begin, data)) { broadcastHave(index); } requestBlocks(key); }; wire.onRequest = function(index, begin, length) { - if (wire.amChoking || pieces == null || index < 0 - || index >= meta.numPieces || begin < 0 || length <= 0 - || begin + length > meta.pieceSize(index)) return; + if (wire.amChoking || pieces == null || index < 0 || index >= meta.numPieces || begin < 0 || length <= 0 || begin + length > meta.pieceSize(index)) return; if (!pieces.have[index]) return; var data = pieces.readBlock(index, begin, length); uploadedBytes += data.length; @@ -384,8 +383,7 @@ class Torrent { var out:Array = []; for (i in 0...n) { var byte = i >> 3; - out.push(byte < bf.length - && (bf.get(byte) & (0x80 >> (i & 7))) != 0); + out.push(byte < bf.length && (bf.get(byte) & (0x80 >> (i & 7))) != 0); } return out; } @@ -479,8 +477,7 @@ class Torrent { if (host == "" || port == null) return false; var key = "tcp:" + host + ":" + port; if (wires.exists(key)) return false; - addWire(key, - new leenkx.network.torrent.peer.TcpPeer(host, port)); + addWire(key, new leenkx.network.torrent.peer.TcpPeer(host, port)); return true; #else return false; @@ -509,8 +506,7 @@ class Torrent { var pendingLayers:Map, got:Int}> = []; function pieceLayerBase():Int { - return Std.int(Math.log(meta.pieceLength / 16384) - / Math.log(2)); + return Std.int(Math.log(meta.pieceLength / 16384) / Math.log(2)); } function requestPieceLayer(wire:WireProtocol, fi:Int):Void { @@ -528,8 +524,7 @@ class Torrent { if (chunk > 512) chunk = 512; var len = 2; while (len < chunk) len <<= 1; - wire.sendHashRequest(root, pieceLayerBase(), sent, - len, 0); + wire.sendHashRequest(root, pieceLayerBase(), sent, len, 0); hashReqNext.set(fi, sent + len); hashReqTime.set(fi, haxe.Timer.stamp()); } @@ -547,11 +542,9 @@ class Torrent { if (count <= 0) return; for (fi in 0...meta.files.length) { var f = meta.files[fi]; - if (f.piecesRoot == null - || !bytesEq(root, f.piecesRoot)) continue; + if (f.piecesRoot == null || !bytesEq(root, f.piecesRoot)) continue; if (meta.fileHashes[fi] != null) return; - var np = Std.int(Math.ceil(f.length - / meta.pieceLength)); + var np = Std.int(Math.ceil(f.length / meta.pieceLength)); var pl = pendingLayers.get(fi); if (pl == null) { pl = {arr: [for (i in 0...np) null], got: 0}; @@ -585,8 +578,7 @@ class Torrent { var root = payload.sub(0, 32); for (fi in 0...meta.files.length) { var f = meta.files[fi]; - if (f.piecesRoot != null - && bytesEq(root, f.piecesRoot)) { + if (f.piecesRoot != null && bytesEq(root, f.piecesRoot)) { hashReqNext.remove(fi); pendingLayers.remove(fi); return; @@ -604,10 +596,8 @@ class Torrent { var np = Std.int(Math.ceil(f.length / meta.pieceLength)); l = []; for (p in 0...np) { - var size = Std.int(Math.min(meta.pieceLength, - f.length - p * meta.pieceLength)); - l.push(meta.pieceRootV2(storage.read( - f.offset + p * meta.pieceLength, size))); + var size = Std.int(Math.min(meta.pieceLength, f.length - p * meta.pieceLength)); + l.push(meta.pieceRootV2(storage.read(f.offset + p * meta.pieceLength, size))); } layerCache.set(fi, l); return l; @@ -620,13 +610,11 @@ class Torrent { t = [level]; var depth = 0; while (level.length > 1) { - var zero = MetaInfo.layerZero(pieceLayerBase() - + depth + 1); + var zero = MetaInfo.layerZero(pieceLayerBase() + depth + 1); var next:Array = []; var i = 0; while (i < level.length) { - var b = i + 1 < level.length - ? level[i + 1] : zero; + var b = i + 1 < level.length ? level[i + 1] : zero; next.push(MetaInfo.hashPair(level[i], b)); i += 2; } diff --git a/leenkx/Sources/leenkx/network/torrent/TorrentClient.hx b/leenkx/Sources/leenkx/network/torrent/TorrentClient.hx index 20ae2be5..68d4bb74 100644 --- a/leenkx/Sources/leenkx/network/torrent/TorrentClient.hx +++ b/leenkx/Sources/leenkx/network/torrent/TorrentClient.hx @@ -5,9 +5,7 @@ import leenkx.network.torrent.storage.Storage; import leenkx.network.torrent.storage.Storage.IStorage; class TorrentClient { - public static var defaultTrackers:Array = [ - "wss://ws1.leenkx.com" - ]; + public static var defaultTrackers:Array = []; public static var defaultDownloadDir:String = null; public var downloadDir:String = null; @@ -247,8 +245,7 @@ class TorrentClient { files.push({path: name, data: Bytes.ofString(Std.string(data))}); } - return {meta: MetaInfo.create(name, files, trackers, opts), - files: files}; + return {meta: MetaInfo.create(name, files, trackers, opts), files: files}; } public static function readFolder(path:String, ?base:String):Array<{path:String, data:Bytes}> { @@ -259,8 +256,7 @@ class TorrentClient { root = root.substr(0, root.length - 1); } if (base == null) { - var i = Std.int(Math.max(root.lastIndexOf("/"), - root.lastIndexOf("\\"))); + var i = Std.int(Math.max(root.lastIndexOf("/"), root.lastIndexOf("\\"))); base = i >= 0 ? root.substr(i + 1) : root; } var files:Array<{path:String, data:Bytes}> = []; @@ -317,20 +313,79 @@ class TorrentClient { return t; } - public function remove(infoHashHex:String, ?deleteData:Bool):Void { - var t = torrents.get(infoHashHex); - if (t == null) { - t = torrents.get("v2:" + infoHashHex); - if (t != null) infoHashHex = "v2:" + infoHashHex; + public function get(id:Dynamic):Torrent { + if (Std.isOfType(id, Torrent)) return id; + if (id == null) return null; + var hex = Std.string(id); + if (StringTools.startsWith(hex, "v2:")) { + hex = hex.substr(3); } - if (t != null) { - t.destroy(); - if (deleteData) t.deleteData(); - torrents.remove(infoHashHex); + var t = torrents.get(hex); + if (t == null) t = torrents.get("v2:" + hex); + if (t == null) { + for (f in torrents) { + if (f.infoHashHex == hex || f.infoHashV2Hex == hex || (f.infoHash != null && Crypto.toHex(f.infoHash) == hex)) { + t = f; + break; + } + } + } + return t; + } + + public static function torrentIdFor(v:Dynamic):Dynamic { + if (v == null) return null; + if (Std.isOfType(v, haxe.io.Bytes)) return v; + var s = Std.string(v); + if (s.indexOf("magnet:") == 0) return s; + if (s.indexOf("v2:") == 0) { + var h = s.substr(3); + return h.length == 64 ? "magnet:?xt=urn:btmh:1220" + h : s; + } + switch (s.length) { + case 40 | 32: + return "magnet:?xt=urn:btih:" + s; + case 64: + return "magnet:?xt=urn:btmh:1220" + s; + case 68: + return "magnet:?xt=urn:btmh:" + s; + default: + return s; } } - static function mergeTrackers(trackers:Array, urls:Array):Void { + public static function trackerUrls(v:Dynamic):Array { + if (v == null) return null; + var items:Array = Std.isOfType(v, Array) + ? (v:Array) : [v]; + var out:Array = []; + for (item in items) { + var u:Dynamic = null; + if (!Std.isOfType(item, String)) { + try { + u = Reflect.field(item, "url"); + } catch(e:Dynamic) {} + } + var s = u != null ? Std.string(u) : Std.string(item); + if (s != "" && s != "null") out.push(s); + } + return out.length > 0 ? out : null; + } + + public function remove(infoHashHex:String, ?deleteData:Bool):Void { + var t = get(infoHashHex); + if (t != null) { + t.destroy(); + if (deleteData) t.deleteData(); + var key:String = null; + for (k in torrents.keys()) { + if (torrents.get(k) == t) { key = k; break; } + } + if (key != null) torrents.remove(key); + } + } + + public static function mergeTrackers(trackers:Array, urls:Array):Void { if (urls == null) return; for (u in urls) { if (u != null && Lambda.indexOf(trackers, u) < 0) { diff --git a/leenkx/Sources/leenkx/network/torrent/peer/RtcPeerPool.hx b/leenkx/Sources/leenkx/network/torrent/peer/RtcPeerPool.hx index 11fa0c39..db7c1df7 100644 --- a/leenkx/Sources/leenkx/network/torrent/peer/RtcPeerPool.hx +++ b/leenkx/Sources/leenkx/network/torrent/peer/RtcPeerPool.hx @@ -51,10 +51,12 @@ class RtcPeerPool { var rtcInitialized = false; #end + public static var defaultIceServers:Array = + ["stun:stun.l.google.com:19302", "stun:global.stun.twilio.com:3478"]; + public function new(?iceServers:Array, ?channelLabel:String, unordered:Bool = false, maxRetransmits:Int = -1) { this.iceServers = iceServers != null ? iceServers - : ["stun:stun.l.google.com:19302", - "stun:global.stun.twilio.com:3478"]; + : defaultIceServers.copy(); if (channelLabel != null) this.channelLabel = channelLabel; this.unordered = unordered; this.maxRetransmits = maxRetransmits; @@ -72,8 +74,7 @@ class RtcPeerPool { } #end - var signaling = new RtcSignaling(trackerUrl, infoHashBinary, - peerIdBinary); + var signaling = new RtcSignaling(trackerUrl, infoHashBinary, peerIdBinary); signalings.push(signaling); signaling.statsProvider = statsProvider; signaling.onConnected = function() { @@ -223,8 +224,7 @@ class RtcPeerPool { } }; var dc = pc.createDataChannel(channelLabel, - {ordered: !unordered, - maxRetransmits: unordered ? maxRetransmits : null}); + {ordered: !unordered, maxRetransmits: unordered ? maxRetransmits : null}); attachOfferChannel(offerIdHex, new DataChannelPeer(dc)); var iceComplete = false; var iceTimer:haxe.Timer = null; @@ -271,8 +271,7 @@ class RtcPeerPool { iceComplete = true; if (iceTimer != null) iceTimer.stop(); if (!offerPeers.exists(offerIdHex)) return; - sig.sendAnswer(peerIdHex, offerIdHex, - pc.getLocalDescription()); + sig.sendAnswer(peerIdHex, offerIdHex, pc.getLocalDescription()); }; pc.onGatheringStateChange = function(state) { if (state == RTC_GATHERING_COMPLETE) sendAnswerSdp(); @@ -312,8 +311,7 @@ class RtcPeerPool { iceComplete = true; if (iceTimer != null) iceTimer.stop(); if (!offerPeers.exists(offerIdHex)) return; - sig.sendAnswer(peerIdHex, offerIdHex, - RtcNative.getLocalDescription(pcId)); + sig.sendAnswer(peerIdHex, offerIdHex, RtcNative.getLocalDescription(pcId)); }; RtcNative.setGatheringStateCallback(pcId, function(state:Int) { if (state == RtcNative.RTC_GATHERING_COMPLETE) sendAnswerSdp(); diff --git a/leenkx/Sources/leenkx/network/torrent/transport/DatachannelTransport.hx b/leenkx/Sources/leenkx/network/torrent/transport/DatachannelTransport.hx index e092bcec..4b34c134 100644 --- a/leenkx/Sources/leenkx/network/torrent/transport/DatachannelTransport.hx +++ b/leenkx/Sources/leenkx/network/torrent/transport/DatachannelTransport.hx @@ -36,6 +36,10 @@ class DatachannelTransport implements ITransport { if (opts.announce != null && opts.announce.length > 0) { url = opts.announce[0]; } + if (url == null || url == "") { + if (onError != null) onError("no announce tracker configured"); + return; + } if (opts.iceServers != null) iceServers = opts.iceServers; pk = opts.pk != null ? opts.pk : ""; ek = opts.ek != null ? opts.ek : ""; diff --git a/leenkx/Sources/leenkx/network/torrent/transport/WebTorrentTransport.hx b/leenkx/Sources/leenkx/network/torrent/transport/WebTorrentTransport.hx index baf290c6..d50df5ec 100644 --- a/leenkx/Sources/leenkx/network/torrent/transport/WebTorrentTransport.hx +++ b/leenkx/Sources/leenkx/network/torrent/transport/WebTorrentTransport.hx @@ -48,7 +48,7 @@ class WebTorrentTransport implements ITransport { var blob:Dynamic = createBlob(identifier); var torrentOpts:Dynamic = { name: identifier, - announce: opts.announce != null ? opts.announce : ["wss://ws1.leenkx.com"] + announce: opts.announce != null ? opts.announce : leenkx.network.torrent.TorrentClient.defaultTrackers }; if (opts.torrentOpts != null) { torrentOpts = js.lib.Object.assign( diff --git a/leenkx/Sources/leenkx/network/torrent/transport/WireTransport.hx b/leenkx/Sources/leenkx/network/torrent/transport/WireTransport.hx index 4c6b1aa9..a675e2a9 100644 --- a/leenkx/Sources/leenkx/network/torrent/transport/WireTransport.hx +++ b/leenkx/Sources/leenkx/network/torrent/transport/WireTransport.hx @@ -43,6 +43,10 @@ class WireTransport implements ITransport { if (opts.announce != null && opts.announce.length > 0) { url = opts.announce[0]; } + if (url == null || url == "") { + if (onError != null) onError("no announce tracker configured"); + return; + } if (opts.iceServers != null) iceServers = opts.iceServers; pk = opts.pk != null ? opts.pk : ""; ek = opts.ek != null ? opts.ek : ""; diff --git a/leenkx/blender/lnx/exporter.py b/leenkx/blender/lnx/exporter.py index e3a39407..1de115f0 100644 --- a/leenkx/blender/lnx/exporter.py +++ b/leenkx/blender/lnx/exporter.py @@ -223,6 +223,9 @@ class LeenkxExporter: cls.export_network = False if wrd.lnx_network == 'Enabled': cls.export_network = True + cls.export_torrent = False + if wrd.lnx_torrent == 'Enabled': + cls.export_torrent = True @staticmethod def write_matrix(matrix): diff --git a/leenkx/blender/lnx/logicnode/custom/LN_add_torrent.py b/leenkx/blender/lnx/logicnode/custom/LN_add_torrent.py deleted file mode 100644 index ba743eeb..00000000 --- a/leenkx/blender/lnx/logicnode/custom/LN_add_torrent.py +++ /dev/null @@ -1,30 +0,0 @@ -from lnx.logicnode.lnx_nodes import * - - -class AddTorrentNode(LnxLogicTreeNode): - """Add Torrent""" - bl_idname = 'LNAddTorrentNode' - bl_label = 'Add Torrent' - bl_description = 'Add a torrent file from the network' - lnx_category = 'Leenkx' - lnx_version = 1 - - def __init__(self, *args, **kwargs): - super(AddTorrentNode, self).__init__(*args, **kwargs) - array_nodes[str(id(self))] = self - - - def lnx_init(self, context): - self.add_input('LnxNodeSocketAction', 'In') - self.add_input('LnxDynamicSocket', 'Connection') - self.add_input('LnxStringSocket', 'Torrent') - - self.add_output('LnxNodeSocketAction', 'Out') - self.add_output('LnxDynamicSocket', 'Title') - self.add_output('LnxDynamicSocket', 'Files') - self.add_output('LnxNodeSocketAction', 'Done') - - -def register(): - add_category('Leenkx', icon='ORIENTATION_CURSOR') - AddTorrentNode.on_register() \ No newline at end of file diff --git a/leenkx/blender/lnx/logicnode/custom/LN_create_leenkx.py b/leenkx/blender/lnx/logicnode/custom/LN_create_leenkx.py index f4ebe1bf..049472ee 100644 --- a/leenkx/blender/lnx/logicnode/custom/LN_create_leenkx.py +++ b/leenkx/blender/lnx/logicnode/custom/LN_create_leenkx.py @@ -4,19 +4,71 @@ class CreateLeenkxNode(LnxLogicTreeNode): """Create Leenkx""" bl_idname = 'LNCreateLeenkxNode' bl_label = 'Create Leenkx' - bl_description = 'Create Leenkx' + bl_description = ('Open a Leenkx peer connection on the given swarm identifier') lnx_category = 'Leenkx' lnx_version = 1 + def update_sockets(self, context): + if not self.inputs: + return + links = {} + for socket in self.inputs: + for link in socket.links: + links.setdefault(socket.name, []).append( + (link.from_node, link.from_socket)) + for socket in self.inputs[2:]: + self.inputs.remove(socket) + if (self.property0 == 'datachannel' + and self.property2 != 'inherit'): + self.add_input('LnxNodeSocketArray', 'ICE Servers') + if self.property3 != 'inherit': + self.add_input('LnxNodeSocketArray', 'Trackers') + tree = self.id_data + for socket in self.inputs: + if socket.name in links: + for from_node, from_socket in links[socket.name]: + tree.links.new(from_socket, socket) + + property0: HaxeEnumProperty('property0', + items=[('wire', 'Wire', 'BitTorrent wire protocol transport (default)'), + ('datachannel', 'DataChannel', 'WebRTC data channels for browser/krom peers - shows the ICE Servers input')], + name='Transport', default='wire', update=update_sockets) + + property1: HaxeBoolProperty('property1', + name='Gossip', description='Relay mode - forward messages to all connected peers', default=False) + + property2: HaxeEnumProperty('property2', + items=[('inherit', 'Inherit', 'Use the built-in STUN server list; no ICE Servers input'), + ('add', 'Add', 'Merge the ICE Servers input on top of the built-in STUN servers'), + ('override', 'Override', 'Replace the ICE server list with the ICE Servers input')], + name='ICE Servers', default='inherit', update=update_sockets) + + property3: HaxeEnumProperty('property3', + items=[('inherit', 'Inherit', 'Use the project torrent tracker list; no Trackers input'), + ('add', 'Add', 'Merge the Trackers input on top of the project tracker list'), + ('override', 'Override', 'Replace the announce tracker list with the Trackers input')], + name='Trackers', default='inherit', update=update_sockets) def lnx_init(self, context): self.add_input('LnxNodeSocketAction', 'In') - self.add_input('LnxStringSocket', 'Url', default_value="8001") + self.add_input('LnxStringSocket', 'Channel', default_value='8001') + if (self.property0 == 'datachannel' + and self.property2 != 'inherit'): + self.add_input('LnxNodeSocketArray', 'ICE Servers') + if self.property3 != 'inherit': + self.add_input('LnxNodeSocketArray', 'Trackers') self.add_output('LnxNodeSocketAction', 'Out') self.add_output('LnxDynamicSocket', 'Connection') self.add_output('LnxDynamicSocket', 'ID') + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') + layout.prop(self, 'property1') + if self.property0 == 'datachannel': + layout.prop(self, 'property2') + layout.prop(self, 'property3') + def register(): add_category('Leenkx', icon='ORIENTATION_CURSOR') diff --git a/leenkx/blender/lnx/logicnode/custom/LN_create_torrent.py b/leenkx/blender/lnx/logicnode/custom/LN_create_torrent.py index 28c31a5d..2c785c1a 100644 --- a/leenkx/blender/lnx/logicnode/custom/LN_create_torrent.py +++ b/leenkx/blender/lnx/logicnode/custom/LN_create_torrent.py @@ -5,7 +5,7 @@ class CreateTorrentNode(LnxLogicTreeNode): """Create Torrent""" bl_idname = 'LNCreateTorrentNode' bl_label = 'Create Torrent' - bl_description = 'Create a torrent file from the network' + bl_description = ('Build a .torrent from content (file, files or folder)') lnx_category = 'Leenkx' lnx_version = 1 @@ -13,17 +13,67 @@ class CreateTorrentNode(LnxLogicTreeNode): super(CreateTorrentNode, self).__init__(*args, **kwargs) array_nodes[str(id(self))] = self + def update_sockets(self, context): + if not self.inputs: + return + links = {} + for socket in self.inputs: + for link in socket.links: + links.setdefault(socket.name, []).append( + (link.from_node, link.from_socket)) + for socket in self.inputs[1:]: + self.inputs.remove(socket) + self._add_data_inputs() + tree = self.id_data + for socket in self.inputs: + if socket.name in links: + for from_node, from_socket in links[socket.name]: + tree.links.new(from_socket, socket) + + def _add_data_inputs(self): + mode_sockets = { + 'file': ('LnxDynamicSocket', 'File (Bytes)', 'Content of the single file as Bytes'), + 'files': ('LnxNodeSocketArray', 'Files ({path, data})', 'Array of {path, data} objects'), + 'folder': ('LnxStringSocket', 'Folder Path', 'Folder read recursively for content (native targets only)'), + } + socket_type, label, desc = mode_sockets.get(self.property1, mode_sockets['file']) + self.add_input(socket_type, label) + self.add_input('LnxStringSocket', 'Name') + self.add_input('LnxStringSocket', 'Comment') + self.add_input('LnxStringSocket', 'Created By') + self.add_input('LnxStringSocket', 'Source') + self.add_input('LnxNodeSocketArray', 'Trackers') + self.add_input('LnxBoolSocket', 'Private') + self.add_input('LnxIntSocket', 'Piece Length') + self.add_input('LnxNodeSocketArray', 'Web Seeds') + + property0: HaxeEnumProperty('property0', + items=[('1', 'V1', 'BitTorrent v1 - SHA-1 piece list over one packed stream; works with every client'), + ('2', 'V2', 'BitTorrent v2 (BEP 52) - SHA-256 merkle tree per file, pieces aligned to file boundaries; needs modern peers'), + ('3', 'Hybrid', 'One .torrent with both v1 and v2 metadata - two infohashes (btih + btmh) over the same info dict')], + name='Version', default='1') + + property1: HaxeEnumProperty('property1', + items=[('file', 'File', 'Single file: Data is a Bytes payload'), + ('files', 'Files', 'Multiple files: Data is an array of {path, data}'), + ('folder', 'Folder', 'Read a folder recursively (native targets only): Data is a folder path string')], + name='Data', default='file', update=update_sockets) def lnx_init(self, context): self.add_input('LnxNodeSocketAction', 'In') - self.add_input('LnxDynamicSocket', 'Connection') - self.add_input('LnxDynamicSocket', 'Data') + self._add_data_inputs() self.add_output('LnxNodeSocketAction', 'Out') - self.add_output('LnxDynamicSocket', 'Torrent') - self.add_output('LnxNodeSocketAction', 'Done') + self.add_output('LnxDynamicSocket', 'Meta') + self.add_output('LnxDynamicSocket', 'Torrent Bytes') + self.add_output('LnxStringSocket', 'InfoHash') + self.add_output('LnxStringSocket', 'Magnet') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') + layout.prop(self, 'property1') def register(): add_category('Leenkx', icon='ORIENTATION_CURSOR') - CreateTorrentNode.on_register() \ No newline at end of file + CreateTorrentNode.on_register() diff --git a/leenkx/blender/lnx/logicnode/custom/LN_create_tracker.py b/leenkx/blender/lnx/logicnode/custom/LN_create_tracker.py new file mode 100644 index 00000000..50e075f0 --- /dev/null +++ b/leenkx/blender/lnx/logicnode/custom/LN_create_tracker.py @@ -0,0 +1,61 @@ +from lnx.logicnode.lnx_nodes import * + + +class CreateTrackerNode(LnxLogicTreeNode): + """Create Tracker""" + bl_idname = 'LNCreateTrackerNode' + bl_label = 'Create Tracker Server' + bl_description = ('Hosts a local tracker') + lnx_category = 'Leenkx' + lnx_version = 1 + + def __init__(self, *args, **kwargs): + super(CreateTrackerNode, self).__init__(*args, **kwargs) + array_nodes[str(id(self))] = self + + def update_sockets(self, context): + if not self.inputs: + return + links = {} + for socket in self.inputs: + for link in socket.links: + links.setdefault(socket.name, []).append( + (link.from_node, link.from_socket)) + for socket in self.inputs[1:]: + self.inputs.remove(socket) + self._add_mode_inputs() + tree = self.id_data + for socket in self.inputs: + if socket.name in links: + for from_node, from_socket in links[socket.name]: + tree.links.new(from_socket, socket) + + def _add_mode_inputs(self): + self.add_input('LnxStringSocket', 'Host', default_value='127.0.0.1') + self.add_input('LnxIntSocket', 'Port', default_value=6969) + if self.property0 == 'wss': + self.add_input('LnxStringSocket', 'Cert File') + self.add_input('LnxStringSocket', 'Key File') + + property0: HaxeEnumProperty('property0', + items=[('udp', 'UDP', 'UDP tracker (BEP 15, sys targets only)'), + ('http', 'HTTP', 'HTTP tracker (BEP 3)'), + ('ws', 'WS', 'Plain WebSocket tracker (local relay, WebTorrent/web peers)'), + ('wss', 'WSS', 'Secure WebSocket tracker (WebTorrent/web peers)')], + name='Type', default='udp', update=update_sockets) + + def lnx_init(self, context): + self.add_input('LnxNodeSocketAction', 'In') + self._add_mode_inputs() + + self.add_output('LnxNodeSocketAction', 'Out') + self.add_output('LnxDynamicSocket', 'Tracker') + self.add_output('LnxStringSocket', 'URL') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') + + +def register(): + add_category('Leenkx', icon='ORIENTATION_CURSOR') + CreateTrackerNode.on_register() diff --git a/leenkx/blender/lnx/logicnode/custom/LN_download_torrent.py b/leenkx/blender/lnx/logicnode/custom/LN_download_torrent.py new file mode 100644 index 00000000..3448c3f3 --- /dev/null +++ b/leenkx/blender/lnx/logicnode/custom/LN_download_torrent.py @@ -0,0 +1,76 @@ +from lnx.logicnode.lnx_nodes import * + + +class DownloadTorrentNode(LnxLogicTreeNode): + """Download Torrent""" + bl_idname = 'LNDownloadTorrentNode' + bl_label = 'Download Torrent' + bl_description = ('Add a torrent to a Leenkx connection and start downloading') + lnx_category = 'Leenkx' + lnx_version = 1 + + def __init__(self, *args, **kwargs): + super(DownloadTorrentNode, self).__init__(*args, **kwargs) + array_nodes[str(id(self))] = self + + def update_sockets(self, context): + if not self.inputs: + return + links = {} + for socket in self.inputs: + for link in socket.links: + links.setdefault(socket.name, []).append( + (link.from_node, link.from_socket)) + for socket in self.inputs[2:]: + self.inputs.remove(socket) + self._add_id_inputs() + tree = self.id_data + for socket in self.inputs: + if socket.name in links: + for from_node, from_socket in links[socket.name]: + tree.links.new(from_socket, socket) + + def _add_id_inputs(self): + mode_sockets = { + 'magnet': ('LnxStringSocket', 'Magnet URI', 'Magnet URI (magnet:?xt=...)'), + 'id': ('LnxStringSocket', 'Infohash', '40-char v1 or 64-char v2 infohash hex'), + 'file': ('LnxDynamicSocket', 'Torrent File (Bytes)', '.torrent file bytes'), + } + socket_type, label, desc = mode_sockets.get(self.property0, mode_sockets['magnet']) + self.add_input(socket_type, label) + if self.property1 != 'inherit': + self.add_input('LnxNodeSocketArray', 'Trackers') + + property0: HaxeEnumProperty('property0', + items=[('magnet', 'Magnet', 'Magnet URI (magnet:?xt=...)'), + ('id', 'Infohash', '40-char v1 or 64-char v2 infohash hex'), + ('file', 'Torrent File', '.torrent file bytes')], + name='Input', default='magnet', update=update_sockets) + + property1: HaxeEnumProperty('property1', + items=[('inherit', 'Inherit', 'Use the torrent\'s own trackers plus the connection\'s; no Trackers input'), + ('add', 'Add', 'Merge the Trackers input on top of the torrent\'s own trackers'), + ('override', 'Override', 'Replace all trackers with the Trackers input (torrent\'s own and connection defaults are dropped)')], + name='Trackers', default='inherit', update=update_sockets) + + def lnx_init(self, context): + self.add_input('LnxNodeSocketAction', 'In') + self.add_input('LnxDynamicSocket', 'Connection') + self._add_id_inputs() + + self.add_output('LnxNodeSocketAction', 'Out') + self.add_output('LnxDynamicSocket', 'Torrent') + self.add_output('LnxStringSocket', 'Name') + self.add_output('LnxNodeSocketArray', 'Files') + self.add_output('LnxNodeSocketAction', 'Done') + self.add_output('LnxStringSocket', 'InfoHash') + self.add_output('LnxStringSocket', 'Magnet') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') + layout.prop(self, 'property1') + + +def register(): + add_category('Leenkx', icon='ORIENTATION_CURSOR') + DownloadTorrentNode.on_register() diff --git a/leenkx/blender/lnx/logicnode/custom/LN_get_torrent_file.py b/leenkx/blender/lnx/logicnode/custom/LN_get_torrent_file.py new file mode 100644 index 00000000..2ed28c04 --- /dev/null +++ b/leenkx/blender/lnx/logicnode/custom/LN_get_torrent_file.py @@ -0,0 +1,77 @@ +from lnx.logicnode.lnx_nodes import * + + +class GetTorrentFileNode(LnxLogicTreeNode): + """Get Torrent File""" + bl_idname = 'LNGetTorrentFileNode' + bl_label = 'Get Torrent File' + bl_description = ('Find one file inside a torrent and read its ' + 'stored bytes (webtorrent torrent.files.find + ' + 'file.arrayBuffer). Match By selects how the ' + 'Match input is interpreted: index, exact file ' + 'name, exact path, or extension') + lnx_category = 'Leenkx' + lnx_version = 1 + + def __init__(self, *args, **kwargs): + super(GetTorrentFileNode, self).__init__(*args, **kwargs) + array_nodes[str(id(self))] = self + + def update_sockets(self, context): + if not self.inputs: + return + links = {} + for socket in self.inputs: + for link in socket.links: + links.setdefault(socket.name, []).append( + (link.from_node, link.from_socket)) + for socket in self.inputs[3:]: + self.inputs.remove(socket) + self._add_match_input() + tree = self.id_data + for socket in self.inputs: + if socket.name in links: + for from_node, from_socket in links[socket.name]: + tree.links.new(from_socket, socket) + + def _add_match_input(self): + match_sockets = { + 'index': ('LnxIntSocket', 'Index', 'Position in the torrent file list'), + 'name': ('LnxStringSocket', 'Name', 'Exact file name, e.g. movie.mp4'), + 'path': ('LnxStringSocket', 'Path', 'Exact path inside the torrent, e.g. docs/readme.txt'), + 'ext': ('LnxStringSocket', 'Extension', 'File extension like mp4 or .mp4; first matching file wins'), + } + socket_type, label, desc = match_sockets.get(self.property0, match_sockets['name']) + self.add_input(socket_type, label) + + property0: HaxeEnumProperty('property0', + items=[('index', 'Index', 'Position in the torrent file list; Match is an int'), + ('name', 'Name', 'Exact file name; Match is a string like movie.mp4'), + ('path', 'Path', 'Exact path inside the torrent; Match is a string like docs/readme.txt'), + ('ext', 'Extension', 'First file whose name ends with the extension; Match is mp4 or .mp4')], + name='Match By', default='name', update=update_sockets) + + def lnx_init(self, context): + self.add_input('LnxNodeSocketAction', 'In') + self.add_input('LnxDynamicSocket', 'Connection') + self.add_input('LnxDynamicSocket', 'Torrent') + self._add_match_input() + + self.add_output('LnxNodeSocketAction', 'Out') + self.add_output('LnxDynamicSocket', 'File') + self.add_output('LnxStringSocket', 'Name') + self.add_output('LnxStringSocket', 'Path') + self.add_output('LnxFloatSocket', 'Size') + self.add_output('LnxStringSocket', 'Type') + self.add_output('LnxDynamicSocket', 'Data') + self.add_output('LnxBoolSocket', 'Found') + self.add_output('LnxFloatSocket', 'Downloaded') + self.add_output('LnxFloatSocket', 'Progress') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') + + +def register(): + add_category('Leenkx', icon='ORIENTATION_CURSOR') + GetTorrentFileNode.on_register() diff --git a/leenkx/blender/lnx/logicnode/custom/LN_leenkx_close_connection.py b/leenkx/blender/lnx/logicnode/custom/LN_leenkx_close_connection.py index 6a140ea9..f1e2112b 100644 --- a/leenkx/blender/lnx/logicnode/custom/LN_leenkx_close_connection.py +++ b/leenkx/blender/lnx/logicnode/custom/LN_leenkx_close_connection.py @@ -5,63 +5,40 @@ class LeenkxCloseConnectionNode(LnxLogicTreeNode): """Close connection on the network""" bl_idname = 'LNLeenkxCloseConnectionNode' bl_label = 'Close Leenkx Connection' + bl_description = ('Tear down a Leenkx connection, or drop a single peer from it') lnx_version = 1 lnx_category = 'Leenkx' - ind = 2 - @staticmethod - def get_enum_id_value(obj, prop_name, value): - return obj.bl_rna.properties[prop_name].enum_items[value].identifier + def update_sockets(self, context): + if not self.inputs: + return + links = {} + for socket in self.inputs: + for link in socket.links: + links.setdefault(socket.name, []).append( + (link.from_node, link.from_socket)) + for socket in self.inputs[2:]: + self.inputs.remove(socket) + if self.property0 == 'peer': + self.add_input('LnxStringSocket', 'ID') + tree = self.id_data + for socket in self.inputs: + if socket.name in links: + for from_node, from_socket in links[socket.name]: + tree.links.new(from_socket, socket) - @staticmethod - def get_count_in(type_name): - return { - 'client': 0, - 'peer': 1 - }.get(type_name, 0) - - def get_enum(self): - return self.get('property0', 0) - - def set_enum(self, value): - # Checking the selection of each type - select_current = self.get_enum_id_value(self, 'property0', value) - select_prev = self.property0 - - #Check if type changed - if select_prev != select_current: - - for i in self.inputs: - self.inputs.remove(i) - - # Arguements for type Host - if (self.get_count_in(select_current) == 0): - self.add_input('LnxNodeSocketAction', 'In') - self.add_input('LnxDynamicSocket', 'Connection') - self.add_input('LnxStringSocket', 'Url') - - # Arguements for type Client - if (self.get_count_in(select_current) == 1): - self.add_input('LnxNodeSocketAction', 'In') - self.add_input('LnxDynamicSocket', 'Connection') - self.add_input('LnxStringSocket', 'ID') - - self['property0'] = value - - - property0: HaxeEnumProperty( - 'property1', + property0: HaxeEnumProperty('property0', items = [('client', 'Leenkx Client', 'Close the Leenkx Client connection on the network'), - ('peer', 'Single Peer', 'Close a Peer connection on network')], + ('peer', 'Single Peer', 'Drop a peer from the connection (forgets its address; it can rejoin on a new handshake)')], name='', default='client', - set=set_enum, - get=get_enum) + update=update_sockets) def lnx_init(self, context): self.add_input('LnxNodeSocketAction', 'In') self.add_input('LnxDynamicSocket', 'Connection') - self.add_input('LnxStringSocket', 'Url') + if self.property0 == 'peer': + self.add_input('LnxStringSocket', 'ID') self.add_output('LnxNodeSocketAction', 'Out') diff --git a/leenkx/blender/lnx/logicnode/custom/LN_leenkx_event.py b/leenkx/blender/lnx/logicnode/custom/LN_leenkx_event.py index 3a4e4326..db4308f3 100644 --- a/leenkx/blender/lnx/logicnode/custom/LN_leenkx_event.py +++ b/leenkx/blender/lnx/logicnode/custom/LN_leenkx_event.py @@ -9,84 +9,26 @@ class LeenkxEventNode(LnxLogicTreeNode): lnx_category = 'Leenkx' lnx_version = 1 - @staticmethod - def get_enum_id_value(obj, prop_name, value): - return obj.bl_rna.properties[prop_name].enum_items[value].identifier - - @staticmethod - def get_count_in(type_name): - return { - 'client': 0, - 'host': 1, - 'securehost': 2 - }.get(type_name, 0) - - def get_enum(self): - return self.get('property0', 0) - - def set_enum(self, value): - # Checking the selection of each type - select_current = self.get_enum_id_value(self, 'property0', value) - select_prev = self.property0 - - #Check if type changed - if select_prev != select_current: - - for i in self.inputs: - self.inputs.remove(i) - - # Arguements for type Client - if (self.get_count_in(select_current) == 0): - self.add_input('LnxStringSocket', 'Url', default_value="8001") - - - # Arguements for type Host - if (self.get_count_in(select_current) == 1): - self.add_input('LnxStringSocket', 'Domain', default_value="127.0.0.1") - self.add_input('LnxIntSocket', 'Port', default_value=8001) - - - # Arguements for type Secure Host - if (self.get_count_in(select_current) == 2): - self.add_input('LnxStringSocket', 'Domain', default_value="127.0.0.1") - self.add_input('LnxIntSocket', 'Port', default_value=8001) - - - self['property0'] = value - - - property0: HaxeEnumProperty( - 'property0', - items = [('client', 'Client', 'Network client Event listener'), - ('host', 'Host', 'Network host Event listener'), - ('securehost', 'Secure Host', 'Network secure host Event listener')], - name='', - default='client', - set=set_enum, - get=get_enum) - - - property1: HaxeEnumProperty( - 'property1', - items = [('onopen', 'OnOpen', 'Listens to onOpen event'), - ('onmessage', 'OnMessage', 'Listens to onMessage event'), - ('onerror', 'OnError', 'Listens to onError event'), - ('onclose', 'OnClose', 'Listens to onClose event'), - ('onseen', 'OnSeen', 'Listens to onClose event'), - ('onserver', 'OnServer', 'Listens to onClose event'), - ('onconnections', 'OnConnections', 'Listens to onClose event'), - ('onping', 'OnPing', 'Listens to onClose event'), - ('onleft', 'OnLeft', 'Listens to onClose event'), - ('ontimeout', 'OnTimeout', 'Listens to onClose event'), - ('onrpc', 'OnRpc', 'Listens to onClose event'), - ('onrpcresponse', 'OnRpcResponse', 'Listens to onClose event'), - ('onwireleft', 'OnWireLeft', 'Listens to onClose event'), - ('onwireseen', 'OnWireSeen', 'Listens to onClose event'), - ('ontorrent', 'OnTorrent', 'Listens to onClose event'), - ('ontracker', 'OnTracker', 'Listens to onClose event'), - ('onannounce', 'OnAnnounce', 'Listens to onClose event')], - name='', - default='onopen') + property1: HaxeEnumProperty('property1', + items = [('onopen', 'OnOpen', 'The connection to the server is open'), + ('onmessage', 'OnMessage', 'A message arrived; ID is the sender peer id, Data the payload'), + ('onerror', 'OnError', 'A connection error occurred'), + ('onclose', 'OnClose', 'The connection was closed'), + ('onseen', 'OnSeen', 'A peer was discovered; ID is the peer id'), + ('onserver', 'OnServer', 'The signaling server responded'), + ('onconnections', 'OnConnections', 'The server sent the peer list; Data is the connection list'), + ('onping', 'OnPing', 'Heartbeat ping roundtrip completed'), + ('onleft', 'OnLeft', 'A peer left; ID is the peer id'), + ('ontimeout', 'OnTimeout', 'A handshake timed out'), + ('onwireleft', 'OnWireLeft', 'A BitTorrent wire peer disconnected; ID is the peer id'), + ('onwireseen', 'OnWireSeen', 'A BitTorrent wire peer connected; ID is the peer id'), + ('ontorrent', 'OnTorrent', 'Torrent activity on the connection; Data is the torrent'), + ('ontorrentadded', 'OnTorrentAdded', 'A torrent registered on the connection; Data is the Torrent handle'), + ('ontracker', 'OnTracker', 'A tracker event on the connection'), + ('onannounce', 'OnAnnounce', 'A tracker announce completed'), + ('ontorrentdone', 'OnTorrentDone', 'A torrent finished downloading; Data is the torrent'), + ('onload', 'OnLoad', 'The connection was registered; fires once at startup')], + name='', default='onopen') def __init__(self, *args, **kwargs): super(LeenkxEventNode, self).__init__(*args, **kwargs) @@ -95,7 +37,7 @@ class LeenkxEventNode(LnxLogicTreeNode): def lnx_init(self, context): #self.add_input('LnxNodeSocketAction', 'In') - self.add_input('LnxStringSocket', 'Url', default_value="ws://127.0.0.1:8001") + self.add_input('LnxStringSocket', 'Channel', default_value='8001') self.add_output('LnxNodeSocketAction', 'Out') self.add_output('LnxDynamicSocket', 'ID') @@ -103,7 +45,6 @@ class LeenkxEventNode(LnxLogicTreeNode): def draw_buttons(self, context, layout): - layout.prop(self, 'property0') layout.prop(self, 'property1') def register(): diff --git a/leenkx/blender/lnx/logicnode/custom/LN_leenkx_message_parser.py b/leenkx/blender/lnx/logicnode/custom/LN_leenkx_message_parser.py index dd210dfb..276abeb7 100644 --- a/leenkx/blender/lnx/logicnode/custom/LN_leenkx_message_parser.py +++ b/leenkx/blender/lnx/logicnode/custom/LN_leenkx_message_parser.py @@ -9,16 +9,15 @@ class LeenkxMessageParserNode(LnxLogicTreeNode): lnx_category = 'Leenkx' lnx_version = 1 - property0: HaxeEnumProperty( - 'property0', - items = [('string', 'String', 'Event for a string over the network'), - ('vector', 'Vector', 'Event for a vector over the network'), - ('float', 'Float', 'Event for a float over the network'), - ('integer', 'Integer', 'Event for an integer over the network'), - ('boolean', 'Boolean', 'Event for a boolean over the network'), - ('transform', 'Transform', 'Event for a transform over the network'), - ('rotation', 'Rotation', 'Event for a rotation over the network')], - name='', default='string') + property0: HaxeEnumProperty('property0', items=[ + ('string', 'String', 'Parse the message as a string'), + ('vector', 'Vector', 'Parse the message as a vector (Vec4)'), + ('float', 'Float', 'Parse the message as a float'), + ('integer', 'Integer', 'Parse the message as an integer'), + ('boolean', 'Boolean', 'Parse the message as a boolean'), + ('transform', 'Transform', 'Parse the message as a transform (Mat4)'), + ('rotation', 'Rotation', 'Parse the message as a rotation (Quat)') + ], name='', default='string') def lnx_init(self, context): diff --git a/leenkx/blender/lnx/logicnode/custom/LN_leenkx_send_message.py b/leenkx/blender/lnx/logicnode/custom/LN_leenkx_send_message.py index 9bce429e..93653ad1 100644 --- a/leenkx/blender/lnx/logicnode/custom/LN_leenkx_send_message.py +++ b/leenkx/blender/lnx/logicnode/custom/LN_leenkx_send_message.py @@ -9,77 +9,55 @@ class LeenkxSendMessageNode(LnxLogicTreeNode): lnx_category = 'Leenkx' lnx_version = 1 - @staticmethod - def get_enum_id_value(obj, prop_name, value): - return obj.bl_rna.properties[prop_name].enum_items[value].identifier + def update_sockets(self, context): + if not self.inputs: + return + links = {} + send_all = True + for socket in self.inputs: + for link in socket.links: + links.setdefault(socket.name, []).append( + (link.from_node, link.from_socket)) + if socket.name == 'Send All': + send_all = socket.default_value_raw + for socket in self.inputs[3:]: + self.inputs.remove(socket) + self._add_data_inputs(send_all) + tree = self.id_data + for socket in self.inputs: + if socket.name in links: + for from_node, from_socket in links[socket.name]: + tree.links.new(from_socket, socket) - @staticmethod - def get_count_in(type_name): - return { - 'client': 0, - 'host': 1, - 'securehost': 2 - }.get(type_name, 0) + def _add_data_inputs(self, send_all=True): + data_sockets = { + 'string': ('LnxStringSocket', 'String payload to send'), + 'vector': ('LnxVectorSocket', 'Vector payload to send'), + 'float': ('LnxFloatSocket', 'Float payload to send'), + 'integer': ('LnxIntSocket', 'Integer payload to send'), + 'boolean': ('LnxBoolSocket', 'Boolean payload to send'), + 'transform': ('LnxDynamicSocket', 'Transform (Mat4) payload to send'), + 'rotation': ('LnxRotationSocket', 'Rotation (Quat) payload to send'), + } + socket_type, desc = data_sockets.get(self.property1, data_sockets['string']) + self.add_input(socket_type, 'Data') + self.add_input('LnxBoolSocket', 'Send All', default_value=send_all) + if not send_all: + self._add_id_input() - def get_enum(self): - return self.get('property0', 0) + def _add_id_input(self): + self.add_input('LnxStringSocket', 'ID') - def set_enum(self, value): - # Checking the selection of each type - select_current = self.get_enum_id_value(self, 'property0', value) - select_prev = self.property0 + def on_socket_val_update(self, context, socket): + if socket.name == 'Send All': + if (len(self.inputs) > 5 + and self.inputs[5].name == 'ID'): + self.inputs.remove(self.inputs[5]) + if not socket.default_value_raw: + self._add_id_input() + super().on_socket_val_update(context, socket) - #Check if type changed - if select_prev != select_current: - - for i in self.inputs: - self.inputs.remove(i) - - # Arguements for type Client - if (self.get_count_in(select_current) == 0): - self.add_input('LnxNodeSocketAction', 'In') - self.add_input('LnxDynamicSocket', 'Connection') - self.add_input('LnxStringSocket', 'API') - self.add_input('LnxDynamicSocket', 'Data') - self.add_input('LnxStringSocket', 'ID') - self.add_input('LnxBoolSocket', 'Send All') - - # Arguements for type Host - if (self.get_count_in(select_current) == 1): - self.add_input('LnxNodeSocketAction', 'In') - self.add_input('LnxDynamicSocket', 'Connection') - self.add_input('LnxStringSocket', 'API') - self.add_input('LnxDynamicSocket', 'Data') - self.add_input('LnxStringSocket', 'ID') - self.add_input('LnxBoolSocket', 'Send All') - - - # Arguements for type Secure Host - if (self.get_count_in(select_current) == 2): - self.add_input('LnxNodeSocketAction', 'In') - self.add_input('LnxDynamicSocket', 'Connection') - self.add_input('LnxStringSocket', 'API') - self.add_input('LnxDynamicSocket', 'Data') - self.add_input('LnxStringSocket', 'ID') - self.add_input('LnxBoolSocket', 'Send All') - - - self['property0'] = value - - - property0: HaxeEnumProperty( - 'property0', - items = [('client', 'Client', 'Network client Event listener'), - ('host', 'Host', 'Network host Event listener'), - ('securehost', 'Secure Host', 'Network secure host Event listener')], - name='', - default='client', - set=set_enum, - get=get_enum) - - - property1: HaxeEnumProperty( - 'property1', + property1: HaxeEnumProperty('property1', items = [('string', 'String', 'Send a string over the network to one or all of the clients'), ('vector', 'Vector', 'Send a vector over the network to one or all of the clients'), ('float', 'Float', 'Send a float over the network to one or all of the clients'), @@ -87,8 +65,12 @@ class LeenkxSendMessageNode(LnxLogicTreeNode): ('boolean', 'Boolean', 'Send a boolean over the network to one or all of the clients'), ('transform', 'Transform', 'Send a transform over the network to one or all of the clients'), ('rotation', 'Rotation', 'Send a rotation over the network to one or all of the clients')], - name='', - default='string') + name='', default='string', update=update_sockets) + + property2: HaxeBoolProperty('property2', + name='Encrypted', + description='Use full encryption protocol (signing, bencode, box encryption). Disable for fast raw message mode', + default=False) def __init__(self, *args, **kwargs): @@ -100,17 +82,15 @@ class LeenkxSendMessageNode(LnxLogicTreeNode): self.add_input('LnxNodeSocketAction', 'In') self.add_input('LnxDynamicSocket', 'Connection') self.add_input('LnxStringSocket', 'API') - self.add_input('LnxDynamicSocket', 'Data') - self.add_input('LnxStringSocket', 'ID') - self.add_input('LnxBoolSocket', 'Send All') - + self._add_data_inputs() + self.add_output('LnxNodeSocketAction', 'Out') - + def draw_buttons(self, context, layout): - layout.prop(self, 'property0') layout.prop(self, 'property1') + layout.prop(self, 'property2') def register(): add_category('Leenkx', icon='ORIENTATION_CURSOR') diff --git a/leenkx/blender/lnx/logicnode/custom/LN_parse_torrent.py b/leenkx/blender/lnx/logicnode/custom/LN_parse_torrent.py new file mode 100644 index 00000000..f085b636 --- /dev/null +++ b/leenkx/blender/lnx/logicnode/custom/LN_parse_torrent.py @@ -0,0 +1,68 @@ +from lnx.logicnode.lnx_nodes import * + + +class ParseTorrentNode(LnxLogicTreeNode): + """Parse Torrent""" + bl_idname = 'LNParseTorrentNode' + bl_label = 'Parse Torrent' + bl_description = ('Parse a torrent id without joining a swarm') + lnx_category = 'Leenkx' + lnx_version = 1 + + def __init__(self, *args, **kwargs): + super(ParseTorrentNode, self).__init__(*args, **kwargs) + array_nodes[str(id(self))] = self + + def update_sockets(self, context): + if not self.inputs: + return + links = {} + for socket in self.inputs: + for link in socket.links: + links.setdefault(socket.name, []).append( + (link.from_node, link.from_socket)) + for socket in self.inputs[1:]: + self.inputs.remove(socket) + self._add_id_input() + tree = self.id_data + for socket in self.inputs: + if socket.name in links: + for from_node, from_socket in links[socket.name]: + tree.links.new(from_socket, socket) + + def _add_id_input(self): + mode_sockets = { + 'magnet': ('LnxStringSocket', 'Magnet URI', 'Magnet URI (magnet:?xt=...)'), + 'id': ('LnxStringSocket', 'Infohash', '40-char v1 or 64-char v2 infohash hex'), + 'file': ('LnxDynamicSocket', 'Torrent File (Bytes)', '.torrent file bytes'), + } + socket_type, label, desc = mode_sockets.get(self.property0, mode_sockets['magnet']) + self.add_input(socket_type, label) + + property0: HaxeEnumProperty('property0', + items=[('magnet', 'Magnet', 'Magnet URI (magnet:?xt=...)'), + ('id', 'Infohash', '40-char v1 or 64-char v2 infohash hex'), + ('file', 'Torrent File', '.torrent file bytes')], + name='Input', default='magnet', update=update_sockets) + + def lnx_init(self, context): + self.add_input('LnxNodeSocketAction', 'In') + self._add_id_input() + + self.add_output('LnxNodeSocketAction', 'Out') + self.add_output('LnxDynamicSocket', 'Meta') + self.add_output('LnxStringSocket', 'InfoHash') + self.add_output('LnxStringSocket', 'Name') + self.add_output('LnxNodeSocketArray', 'Trackers') + self.add_output('LnxNodeSocketArray', 'Web Seeds') + self.add_output('LnxNodeSocketArray', 'Files') + self.add_output('LnxFloatSocket', 'Total Size') + self.add_output('LnxDynamicSocket', 'Torrent Bytes') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') + + +def register(): + add_category('Leenkx', icon='ORIENTATION_CURSOR') + ParseTorrentNode.on_register() diff --git a/leenkx/blender/lnx/logicnode/custom/LN_seed_torrent.py b/leenkx/blender/lnx/logicnode/custom/LN_seed_torrent.py index 5a6e18a7..d2201601 100644 --- a/leenkx/blender/lnx/logicnode/custom/LN_seed_torrent.py +++ b/leenkx/blender/lnx/logicnode/custom/LN_seed_torrent.py @@ -17,12 +17,14 @@ class SeedTorrentNode(LnxLogicTreeNode): def lnx_init(self, context): self.add_input('LnxNodeSocketAction', 'In') self.add_input('LnxDynamicSocket', 'Connection') - self.add_input('LnxDynamicSocket', 'Opts [Map]') + self.add_input('LnxDynamicSocket', 'Torrent') self.add_input('LnxDynamicSocket', 'Data') self.add_output('LnxNodeSocketAction', 'Out') self.add_output('LnxDynamicSocket', 'Torrent') self.add_output('LnxNodeSocketAction', 'Done') + self.add_output('LnxStringSocket', 'InfoHash') + self.add_output('LnxStringSocket', 'Magnet') def register(): diff --git a/leenkx/blender/lnx/logicnode/custom/LN_set_trackers.py b/leenkx/blender/lnx/logicnode/custom/LN_set_trackers.py new file mode 100644 index 00000000..222787d0 --- /dev/null +++ b/leenkx/blender/lnx/logicnode/custom/LN_set_trackers.py @@ -0,0 +1,75 @@ +from lnx.logicnode.lnx_nodes import * + + +class SetTrackersNode(LnxLogicTreeNode): + """Set Trackers""" + bl_idname = 'LNSetTrackersNode' + bl_label = 'Set Trackers' + bl_description = ('Add/Override edit the tracker announce list of a live torrent') + lnx_category = 'Leenkx' + lnx_version = 1 + + tracker_props = { + 'interval': ('LnxIntSocket', 'Interval'), + 'port': ('LnxIntSocket', 'Port'), + 'key': ('LnxIntSocket', 'Key'), + 'numwant': ('LnxIntSocket', 'Num Want'), + 'trackerId': ('LnxStringSocket', 'Tracker ID'), + } + + def __init__(self, *args, **kwargs): + super(SetTrackersNode, self).__init__(*args, **kwargs) + array_nodes[str(id(self))] = self + + def update_sockets(self, context): + if not self.inputs: + return + links = {} + for socket in self.inputs: + for link in socket.links: + links.setdefault(socket.name, []).append( + (link.from_node, link.from_socket)) + for socket in self.inputs[1:]: + self.inputs.remove(socket) + self._add_target_inputs() + tree = self.id_data + for socket in self.inputs: + if socket.name in links: + for from_node, from_socket in links[socket.name]: + tree.links.new(from_socket, socket) + + def _add_target_inputs(self): + if self.property0 in ('add', 'override'): + self.add_input('LnxDynamicSocket', 'Connection') + self.add_input('LnxDynamicSocket', 'Torrent') + self.add_input('LnxNodeSocketArray', 'Trackers') + else: + self.add_input('LnxDynamicSocket', 'Tracker') + socket_type, label = self.tracker_props.get( + self.property0, self.tracker_props['interval']) + self.add_input(socket_type, label) + + property0: HaxeEnumProperty('property0', + items=[('add', 'Add', 'Merge the Trackers input on top of the existing announce list'), + ('override', 'Override', 'Replace the announce list entirely; running tracker connections are restarted'), + ('interval', 'Interval', 'Announce interval in seconds'), + ('port', 'Port', 'Listening port reported to the tracker'), + ('key', 'Key', 'Session key sent with announces'), + ('numwant', 'Num Want', 'Number of peers requested per announce'), + ('trackerId', 'Tracker ID', 'Tracker id echoed back by the tracker')], + name='Property', default='add', update=update_sockets) + + def lnx_init(self, context): + self.add_input('LnxNodeSocketAction', 'In') + self._add_target_inputs() + + self.add_output('LnxNodeSocketAction', 'Out') + self.add_output('LnxDynamicSocket', 'Target') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') + + +def register(): + add_category('Leenkx', icon='ORIENTATION_CURSOR') + SetTrackersNode.on_register() diff --git a/leenkx/blender/lnx/logicnode/custom/LN_torrent_client_info.py b/leenkx/blender/lnx/logicnode/custom/LN_torrent_client_info.py new file mode 100644 index 00000000..2b0bf11e --- /dev/null +++ b/leenkx/blender/lnx/logicnode/custom/LN_torrent_client_info.py @@ -0,0 +1,31 @@ +from lnx.logicnode.lnx_nodes import * + + +class TorrentClientInfoNode(LnxLogicTreeNode): + """Torrent Client Info""" + bl_idname = 'LNTorrentClientInfoNode' + bl_label = 'Connection Torrents' + bl_description = ('Aggregate stats for the torrent client attached') + lnx_category = 'Leenkx' + lnx_version = 1 + + def __init__(self, *args, **kwargs): + super(TorrentClientInfoNode, self).__init__(*args, **kwargs) + array_nodes[str(id(self))] = self + + def lnx_init(self, context): + self.add_input('LnxNodeSocketAction', 'In') + self.add_input('LnxDynamicSocket', 'Connection') + + self.add_output('LnxNodeSocketAction', 'Out') + self.add_output('LnxNodeSocketArray', 'Torrents') + self.add_output('LnxIntSocket', 'Count') + self.add_output('LnxFloatSocket', 'Down Rate') + self.add_output('LnxFloatSocket', 'Up Rate') + self.add_output('LnxFloatSocket', 'Progress') + self.add_output('LnxFloatSocket', 'Ratio') + + +def register(): + add_category('Leenkx', icon='ORIENTATION_CURSOR') + TorrentClientInfoNode.on_register() diff --git a/leenkx/blender/lnx/logicnode/custom/LN_torrent_control.py b/leenkx/blender/lnx/logicnode/custom/LN_torrent_control.py new file mode 100644 index 00000000..ee2efb88 --- /dev/null +++ b/leenkx/blender/lnx/logicnode/custom/LN_torrent_control.py @@ -0,0 +1,71 @@ +from lnx.logicnode.lnx_nodes import * + + +class TorrentControlNode(LnxLogicTreeNode): + """Torrent Control""" + bl_idname = 'LNTorrentControlNode' + bl_label = 'Torrent Control' + bl_description = ('Start, stop, remove or prioritize a torrent.') + lnx_category = 'Leenkx' + lnx_version = 1 + + def __init__(self, *args, **kwargs): + super(TorrentControlNode, self).__init__(*args, **kwargs) + array_nodes[str(id(self))] = self + + property0: HaxeEnumProperty('property0', + items=[('start', 'Start', 'Start or resume the torrent session'), + ('stop', 'Stop', 'Pause the torrent (announces stopped to trackers); it stays registered and can be started again'), + ('remove', 'Remove', 'Destroy the torrent and remove it from the client'), + ('remove-data', 'Remove + Data', 'Destroy the torrent and delete the stored content bytes'), + ('select', 'Select', 'Prioritize a piece range: File handle or Start/End bytes, Priority optional (webtorrent torrent.select)'), + ('deselect', 'Deselect', 'Drop a priority selection, back to normal order'), + ('critical', 'Critical', 'Top-priority selection for streaming seeks'), + ('add-peer', 'Add Peer', 'Connect to a peer directly; Address is host:port (sys/Krom targets only)'), + ('add-web-seed', 'Add Web Seed', 'Register a BEP 19 webseed; Address is an http(s) URL, fills pieces when peers can\'t serve them')], + name='Action', default='start', + update=lambda self, context: self._rebuild_inputs()) + + def _rebuild_inputs(self): + if not self.inputs: + return + links = {} + for socket in self.inputs: + for link in socket.links: + links.setdefault(socket.name, []).append( + (link.from_node, link.from_socket)) + for socket in self.inputs[3:]: + self.inputs.remove(socket) + self._add_action_inputs() + tree = self.id_data + for socket in self.inputs: + if socket.name in links: + for from_node, from_socket in links[socket.name]: + tree.links.new(from_socket, socket) + + def _add_action_inputs(self): + if self.property0 in ('select', 'deselect', 'critical'): + self.add_input('LnxDynamicSocket', 'File') + self.add_input('LnxIntSocket', 'Start') + self.add_input('LnxIntSocket', 'End') + self.add_input('LnxIntSocket', 'Priority') + elif self.property0 == 'add-peer': + self.add_input('LnxStringSocket', 'Address') + elif self.property0 == 'add-web-seed': + self.add_input('LnxStringSocket', 'Address') + + def lnx_init(self, context): + self.add_input('LnxNodeSocketAction', 'In') + self.add_input('LnxDynamicSocket', 'Connection') + self.add_input('LnxDynamicSocket', 'Torrent') + self._add_action_inputs() + + self.add_output('LnxNodeSocketAction', 'Out') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') + + +def register(): + add_category('Leenkx', icon='ORIENTATION_CURSOR') + TorrentControlNode.on_register() diff --git a/leenkx/blender/lnx/logicnode/custom/LN_torrent_event.py b/leenkx/blender/lnx/logicnode/custom/LN_torrent_event.py new file mode 100644 index 00000000..d08b597b --- /dev/null +++ b/leenkx/blender/lnx/logicnode/custom/LN_torrent_event.py @@ -0,0 +1,40 @@ +from lnx.logicnode.lnx_nodes import * + + +class OnTorrentEventNode(LnxLogicTreeNode): + """On Torrent Event""" + bl_idname = 'LNOnTorrentEventNode' + bl_label = 'On Torrent Event' + bl_description = ('Fires when the selected torrent event happens ') + lnx_category = 'Leenkx' + lnx_version = 1 + + def __init__(self, *args, **kwargs): + super(OnTorrentEventNode, self).__init__(*args, **kwargs) + array_nodes[str(id(self))] = self + + property0: HaxeEnumProperty('property0', + items=[('ready', 'Ready', 'Metainfo is known; fires immediately if already ready'), + ('piece', 'Piece', 'A piece finished downloading; Value is the piece index'), + ('wire', 'Wire', 'A peer connected; Value is the WireProtocol'), + ('metadata', 'Metadata', 'ut_metadata fetch completed; Value is the MetaInfo'), + ('done', 'Done', 'All pieces are downloaded'), + ('error', 'Error', 'Torrent failed; Value is the error string')], + name='Event', default='done') + + def lnx_init(self, context): + self.add_input('LnxNodeSocketAction', 'In') + self.add_input('LnxDynamicSocket', 'Connection') + self.add_input('LnxDynamicSocket', 'Torrent') + + self.add_output('LnxNodeSocketAction', 'Out') + self.add_output('LnxDynamicSocket', 'Torrent') + self.add_output('LnxDynamicSocket', 'Value') + + def draw_buttons(self, context, layout): + layout.prop(self, 'property0') + + +def register(): + add_category('Leenkx', icon='ORIENTATION_CURSOR') + OnTorrentEventNode.on_register() diff --git a/leenkx/blender/lnx/logicnode/custom/LN_torrent_info.py b/leenkx/blender/lnx/logicnode/custom/LN_torrent_info.py new file mode 100644 index 00000000..d988c60f --- /dev/null +++ b/leenkx/blender/lnx/logicnode/custom/LN_torrent_info.py @@ -0,0 +1,46 @@ +from lnx.logicnode.lnx_nodes import * + + +class TorrentInfoNode(LnxLogicTreeNode): + """Torrent Info""" + bl_idname = 'LNTorrentInfoNode' + bl_label = 'Torrent Info' + bl_description = ('Read the state of a torrent: infohash, name, peer count, progress, transfer totals, magnet URI') + lnx_category = 'Leenkx' + lnx_version = 1 + + def __init__(self, *args, **kwargs): + super(TorrentInfoNode, self).__init__(*args, **kwargs) + array_nodes[str(id(self))] = self + + def lnx_init(self, context): + self.add_input('LnxNodeSocketAction', 'In') + self.add_input('LnxDynamicSocket', 'Connection') + self.add_input('LnxDynamicSocket', 'Torrent') + + self.add_output('LnxNodeSocketAction', 'Out') + self.add_output('LnxStringSocket', 'InfoHash') + self.add_output('LnxStringSocket', 'Name') + self.add_output('LnxIntSocket', 'Peers') + self.add_output('LnxFloatSocket', 'Progress') + self.add_output('LnxFloatSocket', 'Downloaded') + self.add_output('LnxFloatSocket', 'Uploaded') + self.add_output('LnxBoolSocket', 'Done') + self.add_output('LnxStringSocket', 'Magnet') + self.add_output('LnxNodeSocketArray', 'Files') + self.add_output('LnxIntSocket', 'Num Pieces') + self.add_output('LnxIntSocket', 'Piece Length') + self.add_output('LnxFloatSocket', 'Total Size') + self.add_output('LnxStringSocket', 'Comment') + self.add_output('LnxStringSocket', 'Created By') + self.add_output('LnxIntSocket', 'Version') + self.add_output('LnxStringSocket', 'InfoHash2') + self.add_output('LnxFloatSocket', 'Down Rate') + self.add_output('LnxFloatSocket', 'Up Rate') + self.add_output('LnxBoolSocket', 'Stopped') + self.add_output('LnxDynamicSocket', 'Torrent') + + +def register(): + add_category('Leenkx', icon='ORIENTATION_CURSOR') + TorrentInfoNode.on_register() diff --git a/leenkx/blender/lnx/make.py b/leenkx/blender/lnx/make.py index 861df316..5c43a7c1 100644 --- a/leenkx/blender/lnx/make.py +++ b/leenkx/blender/lnx/make.py @@ -248,6 +248,7 @@ def export_data_impl(fp, sdk_path): export_navigation = bpy.data.worlds['Lnx'].lnx_navigation != 'Disabled' export_ui = bpy.data.worlds['Lnx'].lnx_ui != 'Disabled' export_network = bpy.data.worlds['Lnx'].lnx_network != 'Disabled' + export_torrent = bpy.data.worlds['Lnx'].lnx_torrent != 'Disabled' assets.reset() mat_state.material_cache.clear() @@ -264,6 +265,7 @@ def export_data_impl(fp, sdk_path): navigation_found = False ui_found = False network_found = False + torrent_found = False LeenkxExporter.compress_enabled = wrd.lnx_asset_compression LeenkxExporter.optimize_enabled = state.is_publish and wrd.lnx_optimize_data if not os.path.exists(build_dir + '/compiled/Assets'): @@ -326,6 +328,8 @@ def export_data_impl(fp, sdk_path): ui_found = True if LeenkxExporter.export_network: network_found = True + if LeenkxExporter.export_torrent: + torrent_found = True assets.add(asset_path) if physics_found is False: # Disable physics if no rigid body is exported @@ -340,6 +344,9 @@ def export_data_impl(fp, sdk_path): if network_found == False: export_network = False + if torrent_found == False: + export_torrent = False + # Ugly workaround: some logic nodes require Zui code even if no UI is used, # for now enable UI export unless explicitly disabled. export_ui = True @@ -349,6 +356,9 @@ def export_data_impl(fp, sdk_path): if wrd.lnx_network == 'Enabled': export_network = True + if wrd.lnx_torrent == 'Enabled': + export_torrent = True + modules = [] if wrd.lnx_audio == 'Enabled': modules.append('audio') @@ -360,6 +370,8 @@ def export_data_impl(fp, sdk_path): modules.append('ui') if export_network: modules.append('network') + if export_torrent: + modules.append('torrent') defs = lnx.utils.def_strings_to_array(wrd.world_defs) cdefs = lnx.utils.def_strings_to_array(wrd.compo_defs) @@ -451,7 +463,7 @@ def export_data_impl(fp, sdk_path): wrd.lnx_project_version = lnx.utils.change_version_project(wrd.lnx_project_version) # Write khafile.js - write_data.write_khafilejs(state.is_play, export_physics, export_navigation, export_ui, export_network, state.is_publish, LeenkxExporter.import_traits) + write_data.write_khafilejs(state.is_play, export_physics, export_navigation, export_ui, export_network, export_torrent, state.is_publish, LeenkxExporter.import_traits) # Write Main.hx - depends on write_khafilejs for writing number of assets scene_name = lnx.utils.get_project_scene_name() diff --git a/leenkx/blender/lnx/props.py b/leenkx/blender/lnx/props.py index c2ee0b2e..a2752c9f 100644 --- a/leenkx/blender/lnx/props.py +++ b/leenkx/blender/lnx/props.py @@ -34,6 +34,16 @@ else: lnx_version = '2026.9' lnx_commit = '$Id: 6b2644d47db169cedd95593497cc283207d23a74 $' +LNX_TORRENT_DEFAULT_TRACKERS = [ + 'wss://ws1.leenkx.com', + 'wss://video.blender.org/tracker/socket', +] + +LNX_TORRENT_DEFAULT_ICE_SERVERS = [ + 'stun:stun.l.google.com:19302', + 'stun:global.stun.twilio.com:3478', +] + def get_project_html5_copy(self): return self.get('lnx_project_html5_copy', False) @@ -282,6 +292,17 @@ def init_properties(): ('Enabled', 'Enabled', 'Enabled'), ('Auto', 'Auto', 'Auto')], name="Networking", default='Auto', description="Include Network library", update=assets.invalidate_compiler_cache) + bpy.types.World.lnx_torrent = EnumProperty( + items=[('Disabled', 'Disabled', 'Disabled'), + ('Enabled', 'Enabled', 'Enabled')], + name="Torrent", default='Disabled', description="Include Torrent (Leenkx) library", update=assets.invalidate_compiler_cache) + bpy.types.World.lnx_torrent_download_dir = StringProperty(name="Download Path", description="Directory torrent downloads are written to. Relative paths resolve against the project directory at runtime; absolute paths are used as-is", default="Downloads", subtype='DIR_PATH', update=assets.invalidate_compiler_cache) + bpy.types.World.lnx_torrent_trackers = CollectionProperty(name="Torrent Trackers", description="Default torrent tracker announce URLs", type=bpy.types.PropertyGroup) + bpy.types.World.lnx_torrent_trackers_index = IntProperty(name="Torrent Tracker Index", default=0) + bpy.types.World.lnx_torrent_ice_servers = CollectionProperty(name="ICE Servers", description="Default ICE/STUN servers used for WebRTC peer connections", type=bpy.types.PropertyGroup) + bpy.types.World.lnx_torrent_ice_servers_index = IntProperty(name="ICE Server Index", default=0) + bpy.types.World.lnx_torrent_peer_timeout = IntProperty(name="Peer Timeout", description="Milliseconds after which an unresponsive peer is dropped", default=5 * 60 * 1000, min=0, update=assets.invalidate_compiler_cache) + bpy.types.World.lnx_torrent_heartbeat = IntProperty(name="Heartbeat", description="Milliseconds between heartbeat packets sent to peers", default=30000, min=0, update=assets.invalidate_compiler_cache) bpy.types.World.lnx_audio = EnumProperty( items=[('Disabled', 'Disabled', 'Disabled'), ('Enabled', 'Enabled', 'Enabled')], @@ -701,13 +722,23 @@ def create_wrd(): wrd.use_fake_user = True # Store data world object, add fake user to keep it alive wrd.lnx_version = lnx_version wrd.lnx_commit = lnx_commit + seed_torrent_trackers(wrd) + +def seed_torrent_trackers(wrd): + if len(wrd.lnx_torrent_trackers) == 0: + for url in LNX_TORRENT_DEFAULT_TRACKERS: + wrd.lnx_torrent_trackers.add().name = url + if len(wrd.lnx_torrent_ice_servers) == 0: + for url in LNX_TORRENT_DEFAULT_ICE_SERVERS: + wrd.lnx_torrent_ice_servers.add().name = url def init_properties_on_load(): if 'Lnx' not in bpy.data.worlds: init_properties() + wrd = bpy.data.worlds['Lnx'] + seed_torrent_trackers(wrd) # New project? if bpy.data.filepath == '': - wrd = bpy.data.worlds['Lnx'] wrd.lnx_debug_console = lnx.utils.get_debug_console_auto() lnx.utils.fetch_script_names() diff --git a/leenkx/blender/lnx/props_ui.py b/leenkx/blender/lnx/props_ui.py index edf725fe..329c4509 100644 --- a/leenkx/blender/lnx/props_ui.py +++ b/leenkx/blender/lnx/props_ui.py @@ -1358,11 +1358,150 @@ class LNX_PT_ProjectModulesPanel(bpy.types.Panel): layout.prop(wrd, 'lnx_navigation_engine') layout.prop(wrd, 'lnx_ui') layout.prop(wrd, 'lnx_network') + layout.prop(wrd, 'lnx_torrent') layout.prop_search(wrd, 'lnx_khafile', bpy.data, 'texts') layout.prop(wrd, 'lnx_project_root') layout.prop(wrd, 'lnx_external_blends_path') - + +class LNX_UL_TorrentTrackerList(bpy.types.UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): + layout.prop(item, "name", text="", emboss=False, icon='NETWORK_DRIVE') + +class LnxTorrentTrackerListNewItem(bpy.types.Operator): + bl_idname = "lnx_torrent_tracker_list.new_item" + bl_label = "Add a new tracker" + + def execute(self, context): + wrd = bpy.data.worlds['Lnx'] + wrd.lnx_torrent_trackers.add() + wrd.lnx_torrent_trackers_index = len(wrd.lnx_torrent_trackers) - 1 + assets.invalidate_compiler_cache(wrd, context) + return{'FINISHED'} + +class LnxTorrentTrackerListDeleteItem(bpy.types.Operator): + bl_idname = "lnx_torrent_tracker_list.delete_item" + bl_label = "Deletes a tracker" + + @classmethod + def poll(cls, context): + wrd = bpy.data.worlds['Lnx'] + return len(wrd.lnx_torrent_trackers) > 0 + + def execute(self, context): + wrd = bpy.data.worlds['Lnx'] + index = wrd.lnx_torrent_trackers_index + + wrd.lnx_torrent_trackers.remove(index) + wrd.lnx_torrent_trackers_index = min(max(0, index - 1), len(wrd.lnx_torrent_trackers) - 1) + assets.invalidate_compiler_cache(wrd, context) + return{'FINISHED'} + +class LnxTorrentTrackerListReset(bpy.types.Operator): + bl_idname = "lnx_torrent_tracker_list.reset_items" + bl_label = "Reset To Default Trackers" + bl_description = "Remove all trackers and add back the default ones" + + def execute(self, context): + wrd = bpy.data.worlds['Lnx'] + wrd.lnx_torrent_trackers.clear() + for url in props.LNX_TORRENT_DEFAULT_TRACKERS: + wrd.lnx_torrent_trackers.add().name = url + wrd.lnx_torrent_trackers_index = 0 + assets.invalidate_compiler_cache(wrd, context) + return{'FINISHED'} + +class LNX_UL_TorrentIceServerList(bpy.types.UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): + layout.prop(item, "name", text="", emboss=False, icon='URL') + +class LnxTorrentIceServerListNewItem(bpy.types.Operator): + bl_idname = "lnx_torrent_ice_server_list.new_item" + bl_label = "Add a new ICE server" + + def execute(self, context): + wrd = bpy.data.worlds['Lnx'] + wrd.lnx_torrent_ice_servers.add() + wrd.lnx_torrent_ice_servers_index = len(wrd.lnx_torrent_ice_servers) - 1 + assets.invalidate_compiler_cache(wrd, context) + return{'FINISHED'} + +class LnxTorrentIceServerListDeleteItem(bpy.types.Operator): + bl_idname = "lnx_torrent_ice_server_list.delete_item" + bl_label = "Deletes an ICE server" + + @classmethod + def poll(cls, context): + wrd = bpy.data.worlds['Lnx'] + return len(wrd.lnx_torrent_ice_servers) > 0 + + def execute(self, context): + wrd = bpy.data.worlds['Lnx'] + index = wrd.lnx_torrent_ice_servers_index + + wrd.lnx_torrent_ice_servers.remove(index) + wrd.lnx_torrent_ice_servers_index = min(max(0, index - 1), len(wrd.lnx_torrent_ice_servers) - 1) + assets.invalidate_compiler_cache(wrd, context) + return{'FINISHED'} + +class LnxTorrentIceServerListReset(bpy.types.Operator): + bl_idname = "lnx_torrent_ice_server_list.reset_items" + bl_label = "Reset To Default ICE Servers" + bl_description = "Remove all ICE servers and add back the default ones" + + def execute(self, context): + wrd = bpy.data.worlds['Lnx'] + wrd.lnx_torrent_ice_servers.clear() + for url in props.LNX_TORRENT_DEFAULT_ICE_SERVERS: + wrd.lnx_torrent_ice_servers.add().name = url + wrd.lnx_torrent_ice_servers_index = 0 + assets.invalidate_compiler_cache(wrd, context) + return{'FINISHED'} + +class LNX_PT_LeenkxNetworkPanel(bpy.types.Panel): + bl_label = "Leenkx Network" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "render" + bl_options = {'DEFAULT_CLOSED'} + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + wrd = bpy.data.worlds['Lnx'] + + col = layout.column() + col.enabled = wrd.lnx_torrent != 'Disabled' + + col.prop(wrd, "lnx_torrent_download_dir") + col.prop(wrd, "lnx_torrent_peer_timeout") + col.prop(wrd, "lnx_torrent_heartbeat") + + col.label(text="Trackers") + rows = 2 + if len(wrd.lnx_torrent_trackers) > 1: + rows = 4 + row = col.row() + row.template_list("LNX_UL_TorrentTrackerList", "The_List", wrd, "lnx_torrent_trackers", wrd, "lnx_torrent_trackers_index", rows=rows) + btns = row.column(align=True) + btns.operator("lnx_torrent_tracker_list.new_item", icon='ADD', text="") + btns.operator("lnx_torrent_tracker_list.delete_item", icon='REMOVE', text="") + + col.operator("lnx_torrent_tracker_list.reset_items", icon='FILE_REFRESH') + + col.label(text="ICE Servers") + rows = 2 + if len(wrd.lnx_torrent_ice_servers) > 1: + rows = 4 + row = col.row() + row.template_list("LNX_UL_TorrentIceServerList", "The_List", wrd, "lnx_torrent_ice_servers", wrd, "lnx_torrent_ice_servers_index", rows=rows) + btns = row.column(align=True) + btns.operator("lnx_torrent_ice_server_list.new_item", icon='ADD', text="") + btns.operator("lnx_torrent_ice_server_list.delete_item", icon='REMOVE', text="") + + col.operator("lnx_torrent_ice_server_list.reset_items", icon='FILE_REFRESH') + class LnxVirtualInputPanel(bpy.types.Panel): bl_label = "Leenkx Virtual Input" bl_space_type = "PROPERTIES" @@ -3107,6 +3246,15 @@ __REG_CLASSES = ( LNX_PT_ProjectFlagsDebugConsolePanel, LNX_PT_ProjectWindowPanel, LNX_PT_ProjectModulesPanel, + LNX_UL_TorrentTrackerList, + LnxTorrentTrackerListNewItem, + LnxTorrentTrackerListDeleteItem, + LnxTorrentTrackerListReset, + LNX_UL_TorrentIceServerList, + LnxTorrentIceServerListNewItem, + LnxTorrentIceServerListDeleteItem, + LnxTorrentIceServerListReset, + LNX_PT_LeenkxNetworkPanel, LNX_PT_RenderPathPanel, LNX_PT_RenderPathRendererPanel, LNX_PT_RenderPathShadowsPanel, diff --git a/leenkx/blender/lnx/write_data.py b/leenkx/blender/lnx/write_data.py index 8bccdbdf..35455ab3 100644 --- a/leenkx/blender/lnx/write_data.py +++ b/leenkx/blender/lnx/write_data.py @@ -158,7 +158,7 @@ def remove_readonly(func, path, excinfo): func(path) -def write_khafilejs(is_play, export_physics: bool, export_navigation: bool, export_ui: bool, export_network: bool, is_publish: bool, +def write_khafilejs(is_play, export_physics: bool, export_navigation: bool, export_ui: bool, export_network: bool, export_torrent: bool, is_publish: bool, import_traits: List[str]) -> None: wrd = bpy.data.worlds['Lnx'] rpdat = lnx.utils.get_rp() @@ -482,9 +482,16 @@ let project = new Project('""" + lnx.utils.safesrc(wrd.lnx_project_name + '-' + if not os.path.exists('Libraries/network'): khafile.write(add_leenkx_library(sdk_path, 'lib/network', rel_path=do_relpath_sdk)) assets.add_khafile_def('lnx_network') - lnxjs_path = sdk_path + '/lib/leenkx_tools/lnxjs/Leenkx.js' - lnxjs_path = lnxjs_path.replace('\\', '/').replace('//', '/') - khafile.write(add_assets(lnxjs_path, rel_path=do_relpath_sdk)) + #lnxjs_path = sdk_path + '/lib/leenkx_tools/lnxjs/Leenkx.js' + #lnxjs_path = lnxjs_path.replace('\\', '/').replace('//', '/') + #khafile.write(add_assets(lnxjs_path, rel_path=do_relpath_sdk)) + + if export_torrent: + assets.add_khafile_def('lnx_torrent') + if state.target in ('windows-hl', 'linux-hl', 'macos-hl'): + if not os.path.exists('Libraries/datachannel'): + khafile.write(add_leenkx_library(sdk_path + '/lib/', 'datachannel', rel_path=do_relpath_sdk)) + assets.add_khafile_def('lnx_datachannel') if not wrd.lnx_minimize: assets.add_khafile_def('lnx_json') @@ -736,6 +743,19 @@ class Main { elif wrd.lnx_canvas_img_scaling_quality == 'high': f.write("\n" + content_indent + "leenkx.ui.Canvas.imageScaleQuality = kha.graphics2.ImageScaleQuality.High;") + if wrd.lnx_torrent != 'Disabled': + dl_dir = wrd.lnx_torrent_download_dir + if dl_dir.startswith('//'): + dl_dir = bpy.path.abspath(dl_dir) + if dl_dir != '': + f.write("\n" + content_indent + "leenkx.network.torrent.TorrentClient.defaultDownloadDir = " + json.dumps(dl_dir) + ";") + trackers_list = [json.dumps(t.name) for t in wrd.lnx_torrent_trackers if t.name != ''] + f.write("\n" + content_indent + "leenkx.network.torrent.TorrentClient.defaultTrackers = [" + ", ".join(trackers_list) + "];") + ice_list = [json.dumps(t.name) for t in wrd.lnx_torrent_ice_servers if t.name != ''] + f.write("\n" + content_indent + "leenkx.network.torrent.peer.RtcPeerPool.defaultIceServers = [" + ", ".join(ice_list) + "];") + f.write("\n" + content_indent + "leenkx.network.torrent.LeenkxClient.PEERTIMEOUT = " + str(wrd.lnx_torrent_peer_timeout) + ";") + f.write("\n" + content_indent + "leenkx.network.torrent.LeenkxClient.defaultHeartbeat = " + str(wrd.lnx_torrent_heartbeat) + ";") + # Write Starter.main call starter_indent = content_indent + " " f.write("\n" + content_indent + "leenkx.system.Starter.main(")