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 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
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ require_relative "../lib/sdls"
3
+
4
+ SDLS::CLI.start(ARGV)
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
@@ -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
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SDLS
4
+ VERSION = "0.1.0"
5
+ end
data/lib/sdls.rb ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "sdls/cli"
4
+ require_relative "sdls/config"
5
+ require_relative "sdls/version"
6
+ require_relative "sdls/client"
7
+
8
+ module SDLS
9
+ end
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: []