furaffinity 0.1.0 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 50e5e2ea8cd319bee7eb1665afe2888c7d763dd63ab1889ff861621d0871c96d
4
- data.tar.gz: 984287cda209a247bda7499bda6b5509cb56ae9bdda37ae1e32b9e17b98b4ce5
3
+ metadata.gz: 486881425319cc8b49b196f58f5e4784462d39baa62aa27e985c91a0606c0f21
4
+ data.tar.gz: 81eef14d9b5b32e2874ae3d1e83e6811086cdc902b47b3e861f0e70d0a1f6810
5
5
  SHA512:
6
- metadata.gz: 961c5c9f67fcc98522a3c09f51f83c299c4016d1987f29d18f8ba516f4d6b3b16f11213b70d76902f00b4174d213a423a94b854943d983bc69044678ffe0c031
7
- data.tar.gz: 9f87aa43971869236f1f7b2d0ad02ba9bfa73a45cba39a103c84488cfbc51c60d6ea021a6dbcba2ccf6f399e2ec87608b4b4b4c16b65e9408d0276269ce785f2
6
+ metadata.gz: 1d4ea145d6886a771017f00189153022947575f5184d8328ec0252748b29d17802188ad307dc9abee02806ff7af2d4bc5552cfa94f5d9e6119ca87369d7c59d6
7
+ data.tar.gz: 8e91c5403692b846cf7078735af81a67f4e462d8e04e99d51d4268fa13d7778e70ecb22c2ef2690690bfc1f25c8186d27903d179ab48c29e652a60f88855caa8
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.0] - 2023-12-21
4
+
5
+ ## Added
6
+ - `fa edit` command to edit submissions
7
+ - `after_upload` hook to queues
8
+
9
+ ## Changed
10
+ - `folder_name` is now called `create_folder_name` as it should
11
+
3
12
  ## [0.1.0] - 2023-11-04
4
13
 
5
14
  - Initial release
data/README.md CHANGED
@@ -30,6 +30,10 @@ fa notifications
30
30
 
31
31
  # upload a new submission
32
32
  fa upload my_image.png --title "test post please ignore" --description "This is an image as you can see" --rating general --scrap
33
+
34
+ # interactively update a submission, this needs your preferred editor in ENV
35
+ export EDITOR=vi
36
+ fa edit 54328944
33
37
  ```
34
38
 
35
39
  There is also a way to upload submissions in bulk: `fa queue`
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "json"
4
+ require "tempfile"
4
5
  require "thor"
6
+ require "yaml"
5
7
 
6
8
  module Furaffinity
7
9
  class Cli < Thor
@@ -63,9 +65,9 @@ module Furaffinity
63
65
  type: :string,
64
66
  desc: "Keywords, separated by spaces.",
65
67
  default: ""
66
- option :folder_name,
68
+ option :create_folder_name,
67
69
  type: :string,
68
- desc: "Place this submission into this folder.",
70
+ desc: "Create a new folder and place this submission into it.",
69
71
  default: ""
70
72
  def upload(file_path)
71
73
  set_log_level(options)
@@ -83,6 +85,55 @@ module Furaffinity
83
85
  say "Submission uploaded! #{url}", :green
84
86
  end
85
87
 
88
+ EDIT_TEMPLATE = <<~YAML
89
+ ---
90
+ # Submission info for %<id>s
91
+
92
+ # Required field:
93
+ title: %<title>s
94
+
95
+ # Required field
96
+ description: |-
97
+ %<description>s
98
+
99
+ # Optional field, keywords separated by spaces:
100
+ keywords: %<keywords>s
101
+
102
+ # Required field, one of: [#{Furaffinity::Client::RATING_MAP.keys.join(", ")}]
103
+ rating: %<rating>s
104
+
105
+ scrap: %<scrap>s
106
+ lock_comments: %<lock_comments>s
107
+
108
+ # vim: syntax=yaml
109
+ YAML
110
+
111
+ desc "edit ID", "Edit submission info"
112
+ def edit_interactive(id)
113
+ set_log_level(options)
114
+ config = config_for(options)
115
+ client = config.new_client
116
+
117
+ submission_info = client.submission_info(id)
118
+ # write template
119
+ file = Tempfile.new("facli_edit_#{id}")
120
+ file.puts format(EDIT_TEMPLATE,
121
+ id:,
122
+ **submission_info.slice(:title, :keywords, :scrap, :lock_comments).transform_values(&:inspect),
123
+ description: submission_info.fetch(:message).gsub(/^/, " "),
124
+ rating: submission_info.fetch(:rating),
125
+ )
126
+ file.close
127
+ CliUtils.open_editor file.path, fatal: true
128
+ params = YAML.safe_load_file(file.path, permitted_classes: [Symbol]).transform_keys(&:to_sym)
129
+
130
+ url = client.update(id:, **params)
131
+ say "Submission updated! #{url}", :green
132
+ ensure
133
+ file.close
134
+ file.unlink
135
+ end
136
+
86
137
  desc "queue SUBCOMMAND ...ARGS", "Manage an upload queue"
87
138
  long_desc <<~LONG_DESC, wrap: false
88
139
  `#{basename} queue` manages an upload queue.
@@ -122,5 +173,10 @@ module Furaffinity
122
173
  #{basename} queue clean
123
174
  LONG_DESC
124
175
  subcommand "queue", CliQueue
176
+
177
+ map %w[--version -v] => :__print_version
178
+
179
+ desc "--version, -v", "Print the version"
180
+ def __print_version = puts "furaffinity-cli/#{VERSION}"
125
181
  end
126
182
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "shellwords"
4
+
5
+ module Furaffinity
6
+ module CliUtils
7
+ module_function
8
+
9
+ include SemanticLogger::Loggable
10
+
11
+ def open_editor(file, fatal: false)
12
+ editor = ENV["FA_EDITOR"] || ENV["VISUAL"] || ENV["EDITOR"]
13
+ unless editor
14
+ logger.warn "could not open editor for #{file.inspect}, set one of FA_EDITOR, VISUAL, or EDITOR in your ENV"
15
+ raise "No suitable editor found to edit #{file.inspect}, set one of FA_EDITOR, VISUAL, or EDITOR in your ENV" if fatal
16
+
17
+ return
18
+ end
19
+
20
+ system(*Shellwords.shellwords(editor), file).tap do
21
+ next if $?.exitstatus == 0
22
+
23
+ logger.error "could not run #{editor} #{file}, exit code: #{$?.exitstatus}"
24
+ end
25
+ end
26
+ end
27
+ end
@@ -73,23 +73,18 @@ module Furaffinity
73
73
  adult: 1,
74
74
  }.freeze
75
75
 
76
- def fake_upload(file, title:, rating:, description:, keywords:, folder_name: "", lock_comments: false, scrap: false, type: :submission)
77
- type = type.to_sym
78
- raise ArgumentError.new("#{type.inspect} is not in #{SUBMISSION_TYPES.inspect}") unless SUBMISSION_TYPES.include?(type)
79
- rating = rating.to_sym
80
- raise ArgumentError.new("#{rating.inspect} is not in #{RATING_MAP.keys.inspect}") unless RATING_MAP.include?(rating)
76
+ def fake_upload(file, title:, rating:, description:, keywords:, create_folder_name: "", lock_comments: false, scrap: false, type: :submission)
77
+ validate_args!(type:, rating:) => { type:, rating: }
78
+
81
79
  raise "not a file" unless file.is_a?(File)
82
80
  params = { MAX_FILE_SIZE: "10485760" }
83
81
  raise ArgumentError.new("file size of #{file.size} is greater than FA limit of #{params[:MAX_FILE_SIZE]}") if file.size > params[:MAX_FILE_SIZE].to_i
84
- "https://..."
82
+ "https://www.furaffinity.net/view/54328944/?upload-successful"
85
83
  end
86
84
 
87
85
  # @param file [File]
88
- def upload(file, title:, rating:, description:, keywords:, folder_name: "", lock_comments: false, scrap: false, type: :submission)
89
- type = type.to_sym
90
- raise ArgumentError.new("#{type.inspect} is not in #{SUBMISSION_TYPES.inspect}") unless SUBMISSION_TYPES.include?(type)
91
- rating = rating.to_sym
92
- raise ArgumentError.new("#{rating.inspect} is not in #{RATING_MAP.keys.inspect}") unless RATING_MAP.include?(rating)
86
+ def upload(file, title:, rating:, description:, keywords:, create_folder_name: "", lock_comments: false, scrap: false, type: :submission)
87
+ validate_args!(type:, rating:) => { type:, rating: }
93
88
 
94
89
  client = http_client
95
90
 
@@ -143,7 +138,7 @@ module Furaffinity
143
138
  message: description,
144
139
  keywords: keywords,
145
140
 
146
- create_folder_name: folder_name,
141
+ create_folder_name:,
147
142
 
148
143
  # finalize button :)
149
144
  finalize: "Finalize ",
@@ -164,8 +159,100 @@ module Furaffinity
164
159
  end
165
160
  end
166
161
 
162
+ # Returns submission information from your own gallery.
163
+ def submission_info(id)
164
+ client = http_client
165
+
166
+ logger.trace { "Retrieving submission information for #{id}" }
167
+ response = get("/controls/submissions/changeinfo/#{id}/", client:).then(&method(:parse_response))
168
+
169
+ {}.tap do |h|
170
+ h[:title] = response.css("form[name=MsgForm] input[name=title]").first.attr(:value)
171
+
172
+ %i[message keywords].each do |field|
173
+ h[field] = response.css("form[name=MsgForm] textarea[name=#{field}]").first.text
174
+ end
175
+
176
+ h[:rating] = RATING_MAP.invert.fetch(response.css("form[name=MsgForm] input[name=rating][checked]").first.attr(:value).to_i)
177
+
178
+ %i[lock_comments scrap].each do |field|
179
+ h[field] = !!response.css("form[name=MsgForm] input[name=#{field}][checked]").first&.attr(:value)
180
+ end
181
+
182
+ %i[cat atype species gender].each do |field|
183
+ h[field] = response.css("form[name=MsgForm] select[name=#{field}] option[selected]").first.attr(:value)
184
+ end
185
+
186
+ h[:folder_ids] = response.css("form[name=MsgForm] input[name=\"folder_ids[]\"][checked]").map { _1.attr(:value) }
187
+ end
188
+ end
189
+
190
+ def fake_update(id:, title:, rating:, description:, keywords:, lock_comments: false, scrap: false)
191
+ validate_args!(rating:) => { rating: }
192
+
193
+ "https://www.furaffinity.net/view/54328944/"
194
+ end
195
+
196
+ # NOTE: only tested with `type=submission`
197
+ def update(id:, title:, rating:, description:, keywords:, lock_comments: false, scrap: false)
198
+ validate_args!(rating:) => { rating: }
199
+
200
+ client = http_client
201
+
202
+ # step 1: get the required keys
203
+ logger.trace { "Extracting keys from submission #{id}" }
204
+ response = get("/controls/submissions/changeinfo/#{id}/", client:).then(&method(:parse_response))
205
+ already_set_params = {
206
+ key: response.css("form[name=MsgForm] input[name=key]").first.attr(:value),
207
+ folder_ids: response.css("form[name=MsgForm] input[name=\"folder_ids[]\"][checked]").map { _1.attr(:value) },
208
+ }.tap do |h|
209
+ %i[cat atype species gender].each do |field|
210
+ h[field] = response.css("form[name=MsgForm] select[name=#{field}] option[selected]").first.attr(:value)
211
+ end
212
+ end
213
+
214
+ params = {
215
+ update: "yes",
216
+
217
+ rating: RATING_MAP.fetch(rating),
218
+ title: title,
219
+ message: description,
220
+ keywords: keywords,
221
+
222
+ **already_set_params,
223
+
224
+ # update button ;-)
225
+ submit: "Update",
226
+ }
227
+ params[:lock_comments] = "1" if lock_comments
228
+ params[:scrap] = "1" if scrap
229
+
230
+ logger.debug { "Updating submission #{id}..." }
231
+ update_response = post("/controls/submissions/changeinfo/#{id}/", form: params, client:)
232
+ if update_response.status == 302
233
+ redirect_location = update_response.headers[:location]
234
+ url = File.join(BASE_URL, redirect_location)
235
+ logger.info "Updated! #{url}"
236
+ return url
237
+ else
238
+ fa_error = parse_response(update_response).css(".redirect-message").text
239
+ raise Error.new("FA returned: #{fa_error}")
240
+ end
241
+ end
242
+
167
243
  private
168
244
 
245
+ def validate_args!(type: nil, rating:)
246
+ if type
247
+ type = type.to_sym
248
+ raise ArgumentError.new("#{type.inspect} is not in #{SUBMISSION_TYPES.inspect}") unless SUBMISSION_TYPES.include?(type)
249
+ end
250
+ rating = rating.to_sym
251
+ raise ArgumentError.new("#{rating.inspect} is not in #{RATING_MAP.keys.inspect}") unless RATING_MAP.include?(rating)
252
+
253
+ { type:, rating: }
254
+ end
255
+
169
256
  def parse_response(httpx_response)
170
257
  logger.measure_trace "Parsing response" do
171
258
  Nokogiri::HTML.parse(httpx_response)
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "fileutils"
4
- require "shellwords"
5
4
  require "yaml"
6
5
 
7
6
  module Furaffinity
@@ -35,8 +34,46 @@ module Furaffinity
35
34
  scrap: false
36
35
  lock_comments: false
37
36
 
38
- # Folder name to place this submission under, leave blank if it should not be in any.
39
- folder_name: ""
37
+ # Create a new folder to place this submission under, leave blank if none should be created.
38
+ create_folder_name: ""
39
+
40
+ # Run this Ruby code after uploading
41
+ after_upload: |-
42
+ # Quick reference
43
+ #
44
+ # Available objects:
45
+ # - `client` (Furaffinity::Client)
46
+ # The client object used to interact with FurAffinity.
47
+ # - `submission_info` (Hash)
48
+ # The current submission information of this YAML file. Keys are symbols.
49
+ # To get the description use e.g. `submission_info[:description]`.
50
+ # This also contains the ID of the uploaded submission, e.g.
51
+ # `submission_info[:id]`.
52
+ # - `file_info` (Hash)
53
+ # A hash of all YAML files. Format is
54
+ # `{ "filename.png" => { submission_info } }`.
55
+ # Like `submission_info` it also contains the submission's ID as the `:id`
56
+ # field if it's been uploaded.
57
+ #
58
+ # Helper functions:
59
+ # - `submission_url(submission_id)`
60
+ # Generates a submission URL, e.g.
61
+ # "https://www.furaffinity.net/view/54328944/"
62
+ # - `link_to(url_or_submission, text)`
63
+ # Generates a link. If the first parameter is a submission info hash it will
64
+ # generate the URL using `submission_url(submission_info[:id])`.
65
+
66
+ # Remove this `return` if you want to run Ruby code
67
+ return
68
+
69
+ # Append a link to this submission to a previously uploaded one
70
+ previous_submission = file_info.fetch("previous_file.png")
71
+ previous_submission[:description] += ("\n\n" + link_to(submission_info, "Alt version 2"))
72
+ client.update(**previous_submission)
73
+
74
+ # Append a link to the previous submission to the current one
75
+ submission_info[:description] += ("\n\n" + link_to(previous_submission, "Alt version 1"))
76
+ client.update(**submission_info)
40
77
  YAML
41
78
 
42
79
  attr_reader :client, :queue_dir, :queue, :upload_status, :file_info
@@ -109,7 +146,7 @@ module Furaffinity
109
146
  end
110
147
 
111
148
  submission_info_path = create_submission_info(file)
112
- open_editor submission_info_path
149
+ CliUtils.open_editor submission_info_path
113
150
 
114
151
  queue << file
115
152
  upload_status[file] = {
@@ -151,12 +188,14 @@ module Furaffinity
151
188
  end
152
189
 
153
190
  def reorder
154
- open_editor fa_info_path("queue.yml")
191
+ CliUtils.open_editor fa_info_path("queue.yml")
155
192
  end
156
193
 
157
194
  def upload(wait_time = 60)
158
195
  raise ArgumentError.new("wait_time must be at least 30") if wait_time < 30
159
196
 
197
+ hook_handler = QueueHook.new(client, file_info)
198
+
160
199
  while file_name = queue.shift
161
200
  info = file_info[file_name]
162
201
  unless info
@@ -164,6 +203,8 @@ module Furaffinity
164
203
  next
165
204
  end
166
205
 
206
+ code = file_info[file_name].delete(:after_upload)
207
+
167
208
  logger.info "Uploading #{info[:title].inspect} (#{file_name.inspect})"
168
209
  url = client.upload(
169
210
  File.new(file_name),
@@ -175,6 +216,11 @@ module Furaffinity
175
216
 
176
217
  save
177
218
 
219
+ if code
220
+ hook_handler.update_ids(upload_status)
221
+ hook_handler.run_hook(file_name, code)
222
+ end
223
+
178
224
  unless queue.empty?
179
225
  logger.info "Waiting #{wait_time} seconds until the next upload"
180
226
  sleep wait_time
@@ -210,19 +256,5 @@ module Furaffinity
210
256
 
211
257
  submission_info_path
212
258
  end
213
-
214
- def open_editor(file)
215
- editor = ENV["FA_EDITOR"] || ENV["VISUAL"] || ENV["EDITOR"]
216
- unless editor
217
- logger.warn "could not open editor for #{file.inspect}, set one of FA_EDITOR, VISUAL, or EDITOR in your ENV"
218
- return
219
- end
220
-
221
- system(*Shellwords.shellwords(editor), file).tap do
222
- next if $?.exitstatus == 0
223
-
224
- logger.error "could not run #{editor} #{file}, exit code: #{$?.exitstatus}"
225
- end
226
- end
227
259
  end
228
260
  end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Furaffinity
4
+ class QueueHook
5
+ include SemanticLogger::Loggable
6
+
7
+ class HookRunner
8
+ include SemanticLogger::Loggable
9
+
10
+ attr_reader :client, :file_info, :submission_info
11
+
12
+ def initialize(client, file_info, file_name)
13
+ @client = client
14
+ @file_info = file_info
15
+ @submission_info = file_info.fetch(file_name)
16
+ end
17
+
18
+ def submission_url(submission_id) = "https://www.furaffinity.net/view/#{submission_id}/"
19
+
20
+ def link_to(url_or_submission, text)
21
+ url = case url_or_submission
22
+ in { id: }
23
+ submission_url(id)
24
+ else
25
+ if url_or_submission.is_a?(Hash)
26
+ logger.warn { "passed hash does not have an ID, probably not uploaded yet? hash keys: #{url_or_submission.keys.inspect}" }
27
+ end
28
+ url_or_submission.to_s
29
+ end
30
+
31
+ "[url=#{url}]#{text}[/url]"
32
+ end
33
+ end
34
+
35
+ attr_reader :client, :file_info
36
+
37
+ def initialize(client, file_info)
38
+ @client = client
39
+ @file_info = file_info.each_with_object({}) do |(file_name, info), h|
40
+ # Hash#except duplicates the hash, which is good here as we don't want
41
+ # to modify the queue.
42
+ # exclude after_upload as it's not needed, and create_folder_name and
43
+ # type is only relevant when initially uploading the submission.
44
+ h[file_name] = info.except(:create_folder_name, :after_upload, :type)
45
+ end
46
+ end
47
+
48
+ def update_ids(upload_status)
49
+ logger.trace { "Updating file info ids" }
50
+ upload_status.each do |file_name, status|
51
+ next unless status[:uploaded]
52
+
53
+ @file_info[file_name][:id] = status[:url].match(%{/view/(?<id>[^/]+)})[:id]
54
+ end
55
+ end
56
+
57
+ def run_hook(file_name, code)
58
+ logger.debug { "Running hook" }
59
+ logger.trace { "Hook code:\n#{code}" }
60
+ HookRunner
61
+ .new(client, file_info, file_name)
62
+ .instance_eval(code, File.join(Dir.pwd, "#{file_name}.info.yml"), 0)
63
+ end
64
+ end
65
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Furaffinity
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: furaffinity
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Georg Gadinger
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-11-06 00:00:00.000000000 Z
11
+ date: 2023-12-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: httpx
@@ -113,9 +113,11 @@ files:
113
113
  - lib/furaffinity/cli.rb
114
114
  - lib/furaffinity/cli_base.rb
115
115
  - lib/furaffinity/cli_queue.rb
116
+ - lib/furaffinity/cli_utils.rb
116
117
  - lib/furaffinity/client.rb
117
118
  - lib/furaffinity/config.rb
118
119
  - lib/furaffinity/queue.rb
120
+ - lib/furaffinity/queue_hook.rb
119
121
  - lib/furaffinity/version.rb
120
122
  homepage: https://github.com/nilsding/furaffinity-cli
121
123
  licenses: