sdls 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/bin/sdls +4 -0
- data/lib/sdls/cli.rb +89 -0
- data/lib/sdls/client.rb +117 -0
- data/lib/sdls/config.rb +31 -0
- data/lib/sdls/version.rb +5 -0
- data/lib/sdls.rb +9 -0
- metadata +46 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 134c42aa266627ae45c1e085d2b33e36dda4a1a54ff91aac0c7c8af973f55ddd
|
4
|
+
data.tar.gz: 39ebe584b534cc372ec2de8c4c7f486e4b2075734fb8bab14207f1cea2ca9a18
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 0a87ee27110e3e2adcecc1ee5ebdc22208a45edd552dbc2319c918f7120e78bae355fe54ee61e796536cc2e50f6c7661d91edcff9eef099152a8ada29c30f1f5
|
7
|
+
data.tar.gz: fe91dc17db9b7123d927583f3b10cfaec1265f1802ff279880a99123f5a8fe57f920072cafc41429bc2907dedfbcd5507c7c4945388178925ffea8c53870b394
|
data/bin/sdls
ADDED
data/lib/sdls/cli.rb
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "thor"
|
4
|
+
require "tty-prompt"
|
5
|
+
require "clipboard"
|
6
|
+
|
7
|
+
module SDLS
|
8
|
+
class CLI < Thor
|
9
|
+
DEFAULT_CONFIG_PATH = File.expand_path("~/.config/sdls.yml")
|
10
|
+
|
11
|
+
def initialize(*args, config_path: nil, **kwargs)
|
12
|
+
super(*args, **kwargs)
|
13
|
+
@config_path = config_path || ENV["SDLS_CONFIG_PATH"] || DEFAULT_CONFIG_PATH
|
14
|
+
end
|
15
|
+
|
16
|
+
no_commands do
|
17
|
+
def current_config
|
18
|
+
@config ||= SDLS::Config.load(@config_path)
|
19
|
+
end
|
20
|
+
|
21
|
+
def client
|
22
|
+
SDLS::Client.new(
|
23
|
+
host: current_config.host,
|
24
|
+
username: current_config.username,
|
25
|
+
password: current_config.password,
|
26
|
+
op_item_name: current_config.op_item_name
|
27
|
+
)
|
28
|
+
end
|
29
|
+
|
30
|
+
def extract_torrent_name(magnet)
|
31
|
+
uri = URI.parse(magnet)
|
32
|
+
query = URI.decode_www_form(uri.query || "")
|
33
|
+
dn_param = query.find { |key, _| key == "dn" }
|
34
|
+
dn_param&.last
|
35
|
+
rescue URI::InvalidURIError
|
36
|
+
nil
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
desc "version", "Display the SDLS tool version"
|
41
|
+
def version
|
42
|
+
puts SDLS::VERSION
|
43
|
+
end
|
44
|
+
|
45
|
+
desc "config", "Display the current configuration"
|
46
|
+
def config
|
47
|
+
puts "Current config:"
|
48
|
+
puts " host: #{current_config.host}"
|
49
|
+
puts " username: #{current_config.username}"
|
50
|
+
puts " password: [REDACTED]"
|
51
|
+
puts " op_item_name: #{current_config.op_item_name}" if current_config.op_item_name
|
52
|
+
puts " directories: #{current_config.directories.join(", ")}" if current_config.directories.any?
|
53
|
+
end
|
54
|
+
|
55
|
+
desc "connect", "Verify connectivity and authentication with the server"
|
56
|
+
def connect
|
57
|
+
sid = client.authenticate
|
58
|
+
if sid
|
59
|
+
puts "Connection successful. Session ID: #{sid.slice(0, 8)}..."
|
60
|
+
else
|
61
|
+
puts "Connection failed. Please check your credentials or server status."
|
62
|
+
exit 1
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
desc "add [MAGNET]", "Add a magnet link to Synology Download Station"
|
67
|
+
def add(magnet = nil)
|
68
|
+
magnet ||= Clipboard.paste.strip
|
69
|
+
|
70
|
+
unless magnet&.start_with?("magnet:")
|
71
|
+
warn "Invalid or missing magnet link."
|
72
|
+
exit 1
|
73
|
+
end
|
74
|
+
|
75
|
+
name = extract_torrent_name(magnet)
|
76
|
+
puts "Adding torrent: #{name}" if name
|
77
|
+
|
78
|
+
prompt = TTY::Prompt.new
|
79
|
+
destination = prompt.select("Choose download directory", current_config.directories, default: current_config.directories.first)
|
80
|
+
|
81
|
+
success = client.create_download(magnet: magnet, destination: destination)
|
82
|
+
exit 1 unless success
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.exit_on_failure?
|
86
|
+
true
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
data/lib/sdls/client.rb
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
require "net/http"
|
2
|
+
require "uri"
|
3
|
+
require "json"
|
4
|
+
require "open3"
|
5
|
+
|
6
|
+
module SDLS
|
7
|
+
class Client
|
8
|
+
def initialize(host:, username:, password:, op_item_name: nil)
|
9
|
+
@host = host
|
10
|
+
@username = username
|
11
|
+
@password = password
|
12
|
+
@op_item_name = op_item_name
|
13
|
+
end
|
14
|
+
|
15
|
+
def authenticate(otp: nil)
|
16
|
+
uri = build_uri("/webapi/auth.cgi", auth_params(otp))
|
17
|
+
response_body = make_request(uri)
|
18
|
+
|
19
|
+
handle_auth_response(response_body, otp)
|
20
|
+
rescue => e
|
21
|
+
warn "Authentication error: #{e.message}"
|
22
|
+
nil
|
23
|
+
end
|
24
|
+
|
25
|
+
def create_download(magnet:, destination:)
|
26
|
+
sid = authenticate
|
27
|
+
return false unless sid
|
28
|
+
|
29
|
+
uri = build_uri("/webapi/DownloadStation/task.cgi")
|
30
|
+
data = download_params(magnet, destination, sid)
|
31
|
+
|
32
|
+
response = Net::HTTP.post_form(uri, data)
|
33
|
+
body = JSON.parse(response.body)
|
34
|
+
|
35
|
+
if response.is_a?(Net::HTTPSuccess) && body["success"]
|
36
|
+
puts "Download created successfully in #{destination}"
|
37
|
+
true
|
38
|
+
else
|
39
|
+
warn "Download creation failed: #{body.inspect}"
|
40
|
+
false
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def build_uri(endpoint, params = nil)
|
47
|
+
uri = URI.parse("#{@host}#{endpoint}")
|
48
|
+
uri.query = URI.encode_www_form(params) if params
|
49
|
+
uri
|
50
|
+
end
|
51
|
+
|
52
|
+
def make_request(uri)
|
53
|
+
response = Net::HTTP.get_response(uri)
|
54
|
+
raise "HTTP error: #{response.code}" unless response.is_a?(Net::HTTPSuccess)
|
55
|
+
JSON.parse(response.body)
|
56
|
+
end
|
57
|
+
|
58
|
+
def auth_params(otp)
|
59
|
+
params = {
|
60
|
+
api: "SYNO.API.Auth",
|
61
|
+
version: 6,
|
62
|
+
method: "login",
|
63
|
+
account: @username,
|
64
|
+
passwd: @password,
|
65
|
+
session: "FileStation",
|
66
|
+
format: "cookie"
|
67
|
+
}
|
68
|
+
params[:otp_code] = otp if otp
|
69
|
+
params
|
70
|
+
end
|
71
|
+
|
72
|
+
def download_params(magnet, destination, sid)
|
73
|
+
{
|
74
|
+
api: "SYNO.DownloadStation.Task",
|
75
|
+
version: "1",
|
76
|
+
method: "create",
|
77
|
+
session: "DownloadStation",
|
78
|
+
_sid: sid,
|
79
|
+
uri: magnet,
|
80
|
+
destination: destination
|
81
|
+
}
|
82
|
+
end
|
83
|
+
|
84
|
+
def handle_auth_response(body, otp)
|
85
|
+
return body["data"]["sid"] if body["success"]
|
86
|
+
|
87
|
+
if otp_required?(body, otp)
|
88
|
+
retry_with_otp
|
89
|
+
else
|
90
|
+
raise "Authentication failed: #{body.inspect}"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def otp_required?(body, current_otp)
|
95
|
+
current_otp.nil? && body.dig("error", "errors", "types")&.any? { |e| e["type"] == "otp" }
|
96
|
+
end
|
97
|
+
|
98
|
+
def retry_with_otp
|
99
|
+
puts "OTP required for authentication. Fetching from 1Password..."
|
100
|
+
fetched_otp = fetch_otp_from_1password
|
101
|
+
return authenticate(otp: fetched_otp) if fetched_otp
|
102
|
+
raise "Could not retrieve OTP from 1Password"
|
103
|
+
end
|
104
|
+
|
105
|
+
def fetch_otp_from_1password
|
106
|
+
return nil unless @op_item_name
|
107
|
+
|
108
|
+
stdout, stderr, status = Open3.capture3("op item get \"#{@op_item_name}\" --otp")
|
109
|
+
if status.success?
|
110
|
+
stdout.strip
|
111
|
+
else
|
112
|
+
warn "Failed to retrieve OTP from 1Password: #{stderr.strip}"
|
113
|
+
nil
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
data/lib/sdls/config.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "yaml"
|
4
|
+
|
5
|
+
module SDLS
|
6
|
+
REQUIRED_KEYS = %i[host username password]
|
7
|
+
Config = Data.define(:host, :username, :password, :op_item_name, :directories) do
|
8
|
+
def self.load(path)
|
9
|
+
raise "Configuration file not found: #{path}" unless File.exist?(path)
|
10
|
+
|
11
|
+
data = YAML.load_file(path)
|
12
|
+
data = {} unless data.is_a?(Hash)
|
13
|
+
data = symbolize_keys(data)
|
14
|
+
|
15
|
+
missing_keys = REQUIRED_KEYS - data.keys
|
16
|
+
nil_keys = REQUIRED_KEYS.select { |k| data[k].nil? }
|
17
|
+
|
18
|
+
if missing_keys.any? || nil_keys.any?
|
19
|
+
raise "Configuration file (#{path}) is missing required keys or values: #{(missing_keys + nil_keys).uniq.join(", ")}"
|
20
|
+
end
|
21
|
+
|
22
|
+
new(**data)
|
23
|
+
rescue Psych::SyntaxError => e
|
24
|
+
raise "Error parsing configuration file (#{path}): #{e.message}"
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.symbolize_keys(hash)
|
28
|
+
hash.to_h { |k, v| [k.to_sym, v] }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/sdls/version.rb
ADDED
data/lib/sdls.rb
ADDED
metadata
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sdls
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Camillo Visini
|
8
|
+
bindir: bin
|
9
|
+
cert_chain: []
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
11
|
+
dependencies: []
|
12
|
+
description: A command-line interface for managing downloads on Synology Download
|
13
|
+
Station
|
14
|
+
executables:
|
15
|
+
- sdls
|
16
|
+
extensions: []
|
17
|
+
extra_rdoc_files: []
|
18
|
+
files:
|
19
|
+
- bin/sdls
|
20
|
+
- lib/sdls.rb
|
21
|
+
- lib/sdls/cli.rb
|
22
|
+
- lib/sdls/client.rb
|
23
|
+
- lib/sdls/config.rb
|
24
|
+
- lib/sdls/version.rb
|
25
|
+
homepage: https://github.com/visini/sdls
|
26
|
+
licenses:
|
27
|
+
- MIT
|
28
|
+
metadata: {}
|
29
|
+
rdoc_options: []
|
30
|
+
require_paths:
|
31
|
+
- lib
|
32
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
33
|
+
requirements:
|
34
|
+
- - ">="
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: '3.4'
|
37
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0'
|
42
|
+
requirements: []
|
43
|
+
rubygems_version: 3.6.9
|
44
|
+
specification_version: 4
|
45
|
+
summary: Synology Download Station CLI
|
46
|
+
test_files: []
|