furaffinity 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/.rubocop.yml +13 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/LICENSE +662 -0
- data/README.md +84 -0
- data/Rakefile +5 -0
- data/exe/fa +11 -0
- data/lib/furaffinity/cli.rb +126 -0
- data/lib/furaffinity/cli_base.rb +17 -0
- data/lib/furaffinity/cli_queue.rb +140 -0
- data/lib/furaffinity/client.rb +175 -0
- data/lib/furaffinity/config.rb +53 -0
- data/lib/furaffinity/queue.rb +228 -0
- data/lib/furaffinity/version.rb +5 -0
- data/lib/furaffinity.rb +10 -0
- metadata +145 -0
data/README.md
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
# furaffinity
|
2
|
+
|
3
|
+
A gem to interface with FurAffinity, along with a neat little CLI.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
```sh
|
8
|
+
gem install furaffinity
|
9
|
+
```
|
10
|
+
|
11
|
+
## Usage
|
12
|
+
|
13
|
+
CLI usage is as follows:
|
14
|
+
|
15
|
+
```sh
|
16
|
+
# replace "cookie-a" and "cookie-b" with the values of the respective cookies
|
17
|
+
# use firefox's web inspector for that, the "Storage" tab displays it nicely
|
18
|
+
fa auth cookie-a cookie-b
|
19
|
+
|
20
|
+
# retrieve your notification counters as JSON
|
21
|
+
fa notifications
|
22
|
+
# {
|
23
|
+
# "submissions": 30944,
|
24
|
+
# "watches": 317,
|
25
|
+
# "comments": 278,
|
26
|
+
# "favourites": 1018,
|
27
|
+
# "journals": 1642,
|
28
|
+
# "trouble_tickets": 9
|
29
|
+
# }
|
30
|
+
|
31
|
+
# upload a new submission
|
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
|
+
|
35
|
+
There is also a way to upload submissions in bulk: `fa queue`
|
36
|
+
|
37
|
+
```sh
|
38
|
+
# set your preferred editor in ENV
|
39
|
+
export EDITOR=vi
|
40
|
+
|
41
|
+
# initialise queue directory
|
42
|
+
fa queue init my_queue
|
43
|
+
cd my_queue
|
44
|
+
|
45
|
+
# copy files to upload into the queue directory
|
46
|
+
cp ~/Pictures/pic*.png .
|
47
|
+
|
48
|
+
# add files, an editor will open up for each file to fill in the details
|
49
|
+
fa queue add pic1.png
|
50
|
+
fa queue add pic2.png pic3.png
|
51
|
+
|
52
|
+
# see the status of the queue
|
53
|
+
fa queue status
|
54
|
+
|
55
|
+
# use your preferred editor to change the submission information afterwards
|
56
|
+
vi pic2.png.info.yml
|
57
|
+
|
58
|
+
# open up an editor to rearrange the queue order
|
59
|
+
fa queue reorder
|
60
|
+
|
61
|
+
# upload the entire queue
|
62
|
+
fa queue upload
|
63
|
+
|
64
|
+
# once everything's uploaded you can remove the already uploaded pics
|
65
|
+
fa queue clean
|
66
|
+
```
|
67
|
+
|
68
|
+
## Development
|
69
|
+
|
70
|
+
After checking out the repo, run `bin/setup` to install dependencies.
|
71
|
+
|
72
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
73
|
+
|
74
|
+
## Contributing
|
75
|
+
|
76
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/nilsding/furaffinity. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/nilsding/furaffinity/blob/main/CODE_OF_CONDUCT.md).
|
77
|
+
|
78
|
+
## License
|
79
|
+
|
80
|
+
The gem is available as open source under the terms of the [AGPLv3 License](https://opensource.org/license/agpl-v3/).
|
81
|
+
|
82
|
+
## Code of Conduct
|
83
|
+
|
84
|
+
Everyone interacting in the Furaffinity project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/nilsding/furaffinity/blob/main/CODE_OF_CONDUCT.md).
|
data/Rakefile
ADDED
data/exe/fa
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
lib_dir = File.expand_path("../lib", __dir__)
|
4
|
+
$LOAD_PATH.unshift(lib_dir) unless $LOAD_PATH.include?(lib_dir)
|
5
|
+
|
6
|
+
require "furaffinity"
|
7
|
+
|
8
|
+
SemanticLogger.sync!
|
9
|
+
SemanticLogger.add_appender(io: $stdout, formatter: :color)
|
10
|
+
|
11
|
+
Furaffinity::Cli.start
|
@@ -0,0 +1,126 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require "thor"
|
5
|
+
|
6
|
+
module Furaffinity
|
7
|
+
class Cli < Thor
|
8
|
+
include CliBase
|
9
|
+
|
10
|
+
class_option :log_level,
|
11
|
+
type: :string,
|
12
|
+
desc: "Log level to use",
|
13
|
+
default: "info"
|
14
|
+
|
15
|
+
class_option :config,
|
16
|
+
type: :string,
|
17
|
+
desc: "Path to the config",
|
18
|
+
default: File.join(Dir.home, ".farc")
|
19
|
+
|
20
|
+
desc "auth A_COOKIE B_COOKIE", "Store authentication info for FurAffinity"
|
21
|
+
def auth(a_cookie, b_cookie)
|
22
|
+
set_log_level(options)
|
23
|
+
config = config_for(options)
|
24
|
+
config.set!(a: a_cookie, b: b_cookie)
|
25
|
+
say "Authentication info stored.", :green
|
26
|
+
end
|
27
|
+
|
28
|
+
desc "notifications", "Get the current notification counters as JSON"
|
29
|
+
def notifications
|
30
|
+
set_log_level(options)
|
31
|
+
config = config_for(options)
|
32
|
+
client = config.new_client
|
33
|
+
|
34
|
+
puts JSON.pretty_generate client.notifications
|
35
|
+
end
|
36
|
+
|
37
|
+
desc "upload FILE_PATH", "Upload a new submission"
|
38
|
+
option :type,
|
39
|
+
type: :string,
|
40
|
+
desc: "Submission type. One of: #{Furaffinity::Client::SUBMISSION_TYPES.join(", ")}",
|
41
|
+
default: "submission"
|
42
|
+
option :title,
|
43
|
+
type: :string,
|
44
|
+
desc: "Submission title.",
|
45
|
+
required: true
|
46
|
+
option :description,
|
47
|
+
type: :string,
|
48
|
+
desc: "Submission description.",
|
49
|
+
required: true
|
50
|
+
option :rating,
|
51
|
+
type: :string,
|
52
|
+
desc: "Submission rating. One of: #{Furaffinity::Client::RATING_MAP.keys.join(", ")}",
|
53
|
+
required: true
|
54
|
+
option :lock_comments,
|
55
|
+
type: :boolean,
|
56
|
+
desc: "Disable comments on this submission.",
|
57
|
+
default: false
|
58
|
+
option :scrap,
|
59
|
+
type: :boolean,
|
60
|
+
desc: "Place this upload to your scraps.",
|
61
|
+
default: false
|
62
|
+
option :keywords,
|
63
|
+
type: :string,
|
64
|
+
desc: "Keywords, separated by spaces.",
|
65
|
+
default: ""
|
66
|
+
option :folder_name,
|
67
|
+
type: :string,
|
68
|
+
desc: "Place this submission into this folder.",
|
69
|
+
default: ""
|
70
|
+
def upload(file_path)
|
71
|
+
set_log_level(options)
|
72
|
+
config = config_for(options)
|
73
|
+
client = config.new_client
|
74
|
+
|
75
|
+
upload_options = options.slice(
|
76
|
+
*client
|
77
|
+
.method(:upload)
|
78
|
+
.parameters
|
79
|
+
.select { |(type, name)| %i[keyreq key].include?(type) }
|
80
|
+
.map(&:last)
|
81
|
+
).transform_keys(&:to_sym)
|
82
|
+
url = client.upload(File.new(file_path), **upload_options)
|
83
|
+
say "Submission uploaded! #{url}", :green
|
84
|
+
end
|
85
|
+
|
86
|
+
desc "queue SUBCOMMAND ...ARGS", "Manage an upload queue"
|
87
|
+
long_desc <<~LONG_DESC, wrap: false
|
88
|
+
`#{basename} queue` manages an upload queue.
|
89
|
+
|
90
|
+
It behaves somewhat like `git`, where you have to initialise a designated
|
91
|
+
directory first for it to use as a queue.
|
92
|
+
|
93
|
+
An example workflow with `#{basename} queue` would look like:
|
94
|
+
|
95
|
+
# set your preferred editor in ENV
|
96
|
+
export EDITOR=vi
|
97
|
+
|
98
|
+
# initialise queue directory
|
99
|
+
#{basename} queue init my_queue
|
100
|
+
cd my_queue
|
101
|
+
|
102
|
+
# copy files to upload into the queue directory
|
103
|
+
cp ~/Pictures/pic*.png .
|
104
|
+
|
105
|
+
# add files, an editor will open up for each file to fill in the details
|
106
|
+
#{basename} queue add pic1.png
|
107
|
+
#{basename} queue add pic2.png pic3.png
|
108
|
+
|
109
|
+
# see the status of the queue
|
110
|
+
#{basename} queue status
|
111
|
+
|
112
|
+
# use your preferred editor to change the submission information afterwards
|
113
|
+
vi pic2.png.info.yml
|
114
|
+
|
115
|
+
# open up an editor to rearrange the queue order
|
116
|
+
#{basename} queue reorder
|
117
|
+
|
118
|
+
# upload the entire queue
|
119
|
+
#{basename} queue upload
|
120
|
+
|
121
|
+
# once everything's uploaded you can remove the already uploaded pics
|
122
|
+
#{basename} queue clean
|
123
|
+
LONG_DESC
|
124
|
+
subcommand "queue", CliQueue
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Furaffinity
|
4
|
+
module CliBase
|
5
|
+
def self.included(base)
|
6
|
+
def base.exit_on_failure? = true
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def set_log_level(options)
|
12
|
+
SemanticLogger.default_level = options[:log_level].to_sym
|
13
|
+
end
|
14
|
+
|
15
|
+
def config_for(options) = Config.new(options[:config])
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "thor"
|
4
|
+
|
5
|
+
module Furaffinity
|
6
|
+
class CliQueue < Thor
|
7
|
+
include CliBase
|
8
|
+
|
9
|
+
desc "init [DIR]", "Initialise a queue directory"
|
10
|
+
def init(dir = Dir.pwd)
|
11
|
+
result = queue(dir).init
|
12
|
+
say "Created new queue dir in #{result.inspect}", :green
|
13
|
+
rescue Furaffinity::Error => e
|
14
|
+
say e.message, :red
|
15
|
+
exit 1
|
16
|
+
end
|
17
|
+
|
18
|
+
desc "add FILE...", "Add a file to the upload queue"
|
19
|
+
def add(*files)
|
20
|
+
if files.empty?
|
21
|
+
say "You need to pass a file.", :red
|
22
|
+
help :add
|
23
|
+
exit 1
|
24
|
+
end
|
25
|
+
|
26
|
+
queue.reload
|
27
|
+
|
28
|
+
files_added = queue.add(*files)
|
29
|
+
files_added.each do |file|
|
30
|
+
say "added #{file.inspect}", :green
|
31
|
+
end
|
32
|
+
rescue Furaffinity::Error => e
|
33
|
+
say e.message, :red
|
34
|
+
exit 1
|
35
|
+
end
|
36
|
+
|
37
|
+
desc "remove FILE...", "Removes a file from the upload queue"
|
38
|
+
def remove(*files)
|
39
|
+
if files.empty?
|
40
|
+
say "You need to pass a file.", :red
|
41
|
+
help :remove
|
42
|
+
exit 1
|
43
|
+
end
|
44
|
+
|
45
|
+
queue.reload
|
46
|
+
|
47
|
+
files_removed = queue.remove(*files)
|
48
|
+
files_removed.each do |file|
|
49
|
+
say "removed #{file.inspect}", :green
|
50
|
+
end
|
51
|
+
rescue Furaffinity::Error => e
|
52
|
+
say e.message, :red
|
53
|
+
exit 1
|
54
|
+
end
|
55
|
+
|
56
|
+
desc "clean", "Remove uploaded files"
|
57
|
+
def clean
|
58
|
+
queue.reload
|
59
|
+
queue.clean
|
60
|
+
say "Removed uploaded files", :green
|
61
|
+
rescue Furaffinity::Error => e
|
62
|
+
say e.message, :red
|
63
|
+
exit 1
|
64
|
+
end
|
65
|
+
|
66
|
+
desc "reorder", "Open an editor to rearrange the queue"
|
67
|
+
def reorder
|
68
|
+
queue.reload
|
69
|
+
queue.reorder unless queue.queue.empty?
|
70
|
+
|
71
|
+
invoke :status
|
72
|
+
end
|
73
|
+
|
74
|
+
desc "status", "Print the current status of the queue"
|
75
|
+
def status
|
76
|
+
queue.reload
|
77
|
+
|
78
|
+
if queue.queue.empty?
|
79
|
+
say "Nothing is in the queue yet, use `#{File.basename $0} queue add ...` to add files.", :yellow
|
80
|
+
|
81
|
+
print_uploaded_files
|
82
|
+
return
|
83
|
+
end
|
84
|
+
|
85
|
+
say "Enqueued files:"
|
86
|
+
queue_table = [["Position", "File name", "Title", "Rating"], :separator]
|
87
|
+
queue.queue.each_with_index do |file_name, idx|
|
88
|
+
file_info = queue.file_info.fetch(file_name)
|
89
|
+
queue_table << [idx + 1, file_name, file_info[:title], file_info[:rating]]
|
90
|
+
end
|
91
|
+
print_table queue_table, borders: true
|
92
|
+
|
93
|
+
print_uploaded_files
|
94
|
+
|
95
|
+
rescue Furaffinity::Error => e
|
96
|
+
say e.message, :red
|
97
|
+
exit 1
|
98
|
+
end
|
99
|
+
|
100
|
+
desc "upload", "Upload all submissions in the queue."
|
101
|
+
option :wait_time, type: :numeric, desc: "Seconds to wait between each upload.", default: 60
|
102
|
+
def upload
|
103
|
+
if options[:wait_time] < 30
|
104
|
+
say "--wait-time must be at least 30", :red
|
105
|
+
exit 1
|
106
|
+
end
|
107
|
+
|
108
|
+
queue.reload
|
109
|
+
queue.upload(options[:wait_time])
|
110
|
+
say "Submissions uploaded.", :green
|
111
|
+
rescue Furaffinity::Error => e
|
112
|
+
say e.message, :red
|
113
|
+
exit 1
|
114
|
+
end
|
115
|
+
|
116
|
+
private
|
117
|
+
|
118
|
+
def queue(dir = Dir.pwd)
|
119
|
+
@queue ||= begin
|
120
|
+
set_log_level(options)
|
121
|
+
config = config_for(options)
|
122
|
+
Queue.new(config.new_client, dir)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def print_uploaded_files
|
127
|
+
uploaded_files = queue.uploaded_files
|
128
|
+
unless uploaded_files.empty?
|
129
|
+
say
|
130
|
+
say "Uploaded files (will be removed when you run `#{File.basename $0} queue clean`):"
|
131
|
+
uploaded_table = [["File name", "Title", "Submission URL"], :separator]
|
132
|
+
uploaded_files.each do |file_name, upload_status|
|
133
|
+
file_info = queue.file_info.fetch(file_name)
|
134
|
+
uploaded_table << [file_name, file_info[:title], upload_status[:url]]
|
135
|
+
end
|
136
|
+
print_table uploaded_table, borders: true
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,175 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "httpx"
|
4
|
+
require "nokogiri"
|
5
|
+
|
6
|
+
module Furaffinity
|
7
|
+
class Client
|
8
|
+
include SemanticLogger::Loggable
|
9
|
+
|
10
|
+
BASE_URL = "https://www.furaffinity.net"
|
11
|
+
|
12
|
+
# @param a [String] value of the `a` cookie
|
13
|
+
# @param b [String] value of the `b` cookie
|
14
|
+
def initialize(a:, b:)
|
15
|
+
raise Error.new("a needs to be a non-zero string") unless a.is_a?(String) || a.empty?
|
16
|
+
raise Error.new("b needs to be a non-zero string") unless b.is_a?(String) || b.empty?
|
17
|
+
|
18
|
+
@auth_cookies = { "a" => a, "b" => b }
|
19
|
+
end
|
20
|
+
|
21
|
+
def http_client
|
22
|
+
HTTPX
|
23
|
+
.plugin(:cookies)
|
24
|
+
.plugin(:stream)
|
25
|
+
# .plugin(:follow_redirects)
|
26
|
+
.with(headers: { "user-agent" => "furaffinity/#{Furaffinity::VERSION} (Ruby)" })
|
27
|
+
.with_cookies(@auth_cookies)
|
28
|
+
end
|
29
|
+
|
30
|
+
def get(path, client: nil)
|
31
|
+
client ||= http_client
|
32
|
+
url = File.join(BASE_URL, path)
|
33
|
+
logger.measure_trace("GET #{url}") do
|
34
|
+
client.get(url)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def post(path, form: {}, client: nil)
|
39
|
+
client ||= http_client
|
40
|
+
url = File.join(BASE_URL, path)
|
41
|
+
logger.measure_trace("POST #{url}", { form: }) do
|
42
|
+
client.post(url, form:)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
NOTIFICATIONS_MAP = {
|
47
|
+
"submission" => :submissions,
|
48
|
+
"watch" => :watches,
|
49
|
+
"comment" => :comments,
|
50
|
+
"favorite" => :favourites,
|
51
|
+
"journal" => :journals,
|
52
|
+
"unread" => :notes,
|
53
|
+
"troubleticket" => :trouble_tickets,
|
54
|
+
}
|
55
|
+
|
56
|
+
def notifications
|
57
|
+
get("/")
|
58
|
+
.then(&method(:parse_response))
|
59
|
+
.css("a.notification-container")
|
60
|
+
.map { _1.attr(:title) }
|
61
|
+
.uniq
|
62
|
+
.each_with_object({}) do |item, h|
|
63
|
+
count, type, *_rest = item.split(" ")
|
64
|
+
count = count.tr(",", "").strip.to_i
|
65
|
+
h[NOTIFICATIONS_MAP.fetch(type.downcase.strip)] = count
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
SUBMISSION_TYPES = %i[submission story poetry music].freeze
|
70
|
+
RATING_MAP = {
|
71
|
+
general: 0,
|
72
|
+
mature: 2,
|
73
|
+
adult: 1,
|
74
|
+
}.freeze
|
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)
|
81
|
+
raise "not a file" unless file.is_a?(File)
|
82
|
+
params = { MAX_FILE_SIZE: "10485760" }
|
83
|
+
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://..."
|
85
|
+
end
|
86
|
+
|
87
|
+
# @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)
|
93
|
+
|
94
|
+
client = http_client
|
95
|
+
|
96
|
+
# step 1: get the required keys
|
97
|
+
logger.trace "Extracting keys from upload form"
|
98
|
+
response = get("/submit/", client:).then(&method(:parse_response))
|
99
|
+
params = {
|
100
|
+
submission_type: type,
|
101
|
+
submission: file,
|
102
|
+
thumbnail: {
|
103
|
+
content_type: "application/octet-stream",
|
104
|
+
filename: "",
|
105
|
+
body: ""
|
106
|
+
},
|
107
|
+
}
|
108
|
+
params.merge!(
|
109
|
+
%w[MAX_FILE_SIZE key]
|
110
|
+
.map { [_1.to_sym, response.css("form#myform input[name=#{_1}]").first.attr(:value)] }
|
111
|
+
.to_h
|
112
|
+
)
|
113
|
+
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
|
114
|
+
|
115
|
+
# step 2: upload the submission file
|
116
|
+
logger.debug "Uploading submission..."
|
117
|
+
upload_response = post("/submit/upload/", form: params, client:)
|
118
|
+
# for some reason HTTPX performs a GET redirect with the params so we
|
119
|
+
# can't use its plugin here
|
120
|
+
# --> follow the redirect ourselves
|
121
|
+
raise Error.new("expected a 302 response, got #{upload_response.status}") unless upload_response.status == 302
|
122
|
+
|
123
|
+
redirect_location = upload_response.headers[:location]
|
124
|
+
unless redirect_location == "/submit/finalize/"
|
125
|
+
logger.warn "unexpected redirect target #{redirect_location.inspect}, expected \"/submit/finalize/\". continuing regardless ..."
|
126
|
+
end
|
127
|
+
response = get(redirect_location, client:).then(&method(:parse_response))
|
128
|
+
|
129
|
+
params = {
|
130
|
+
key: response.css("form#myform input[name=key]").first.attr(:value),
|
131
|
+
|
132
|
+
# category, "1" is "Visual Art -> All"
|
133
|
+
cat: "1",
|
134
|
+
# theme, "1" is "General Things -> All"
|
135
|
+
atype: "1",
|
136
|
+
# species, "1" is "Unspecified / Any"
|
137
|
+
species: "1",
|
138
|
+
# gender, "0" is "Any"
|
139
|
+
gender: "0",
|
140
|
+
|
141
|
+
rating: RATING_MAP.fetch(rating),
|
142
|
+
title: title,
|
143
|
+
message: description,
|
144
|
+
keywords: keywords,
|
145
|
+
|
146
|
+
create_folder_name: folder_name,
|
147
|
+
|
148
|
+
# finalize button :)
|
149
|
+
finalize: "Finalize ",
|
150
|
+
}
|
151
|
+
params[:lock_comments] = "1" if lock_comments
|
152
|
+
params[:scrap] = "1" if scrap
|
153
|
+
|
154
|
+
logger.debug "Finalising submission..."
|
155
|
+
finalize_response = post("/submit/finalize/", form: params, client:)
|
156
|
+
if finalize_response.status == 302
|
157
|
+
redirect_location = finalize_response.headers[:location]
|
158
|
+
url = File.join(BASE_URL, redirect_location)
|
159
|
+
logger.info "Uploaded! #{url}"
|
160
|
+
return url
|
161
|
+
else
|
162
|
+
fa_error = parse_response(finalize_response).css(".redirect-message").text
|
163
|
+
raise Error.new("FA returned: #{fa_error}")
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
private
|
168
|
+
|
169
|
+
def parse_response(httpx_response)
|
170
|
+
logger.measure_trace "Parsing response" do
|
171
|
+
Nokogiri::HTML.parse(httpx_response)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "yaml"
|
4
|
+
|
5
|
+
module Furaffinity
|
6
|
+
class Config
|
7
|
+
include SemanticLogger::Loggable
|
8
|
+
|
9
|
+
def initialize(path)
|
10
|
+
@path = path
|
11
|
+
if File.exist?(path)
|
12
|
+
logger.measure_trace("Loading configuration") do
|
13
|
+
@config_hash = YAML.safe_load_file(path)
|
14
|
+
end
|
15
|
+
else
|
16
|
+
@config_hash = {}
|
17
|
+
end
|
18
|
+
rescue => e
|
19
|
+
logger.fatal("Error while loading configuration:", e)
|
20
|
+
raise
|
21
|
+
end
|
22
|
+
|
23
|
+
# @return [Furaffinity::Client]
|
24
|
+
def new_client = Furaffinity::Client.new(a: self[:a], b: self[:b])
|
25
|
+
|
26
|
+
def [](key) = @config_hash[key.to_s]
|
27
|
+
alias get []
|
28
|
+
|
29
|
+
def []=(key, value)
|
30
|
+
@config_hash[key.to_s] = value
|
31
|
+
end
|
32
|
+
|
33
|
+
def set(**kwargs)
|
34
|
+
kwargs.each do |k, v|
|
35
|
+
self[k] = v
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def set!(**kwargs)
|
40
|
+
set(**kwargs)
|
41
|
+
save
|
42
|
+
end
|
43
|
+
|
44
|
+
def save
|
45
|
+
logger.measure_debug("Saving configuration") do
|
46
|
+
yaml = @config_hash.to_yaml
|
47
|
+
File.open(@path, "w") do |f|
|
48
|
+
f.puts yaml
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|