mangadex 5.3.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/.gitignore +12 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +81 -0
- data/LICENSE.txt +21 -0
- data/README.md +42 -0
- data/Rakefile +6 -0
- data/bin/console +23 -0
- data/bin/setup +8 -0
- data/lib/extensions.rb +12 -0
- data/lib/mangadex/README.md +93 -0
- data/lib/mangadex/api/context.rb +53 -0
- data/lib/mangadex/api/response.rb +104 -0
- data/lib/mangadex/api/user.rb +48 -0
- data/lib/mangadex/api/version.rb +21 -0
- data/lib/mangadex/api.rb +5 -0
- data/lib/mangadex/artist.rb +13 -0
- data/lib/mangadex/auth.rb +56 -0
- data/lib/mangadex/author.rb +101 -0
- data/lib/mangadex/chapter.rb +105 -0
- data/lib/mangadex/content_rating.rb +75 -0
- data/lib/mangadex/cover_art.rb +93 -0
- data/lib/mangadex/custom_list.rb +127 -0
- data/lib/mangadex/internal/definition.rb +162 -0
- data/lib/mangadex/internal/request.rb +121 -0
- data/lib/mangadex/internal/with_attributes.rb +120 -0
- data/lib/mangadex/internal.rb +3 -0
- data/lib/mangadex/manga.rb +188 -0
- data/lib/mangadex/mangadex_object.rb +62 -0
- data/lib/mangadex/relationship.rb +46 -0
- data/lib/mangadex/report_reason.rb +39 -0
- data/lib/mangadex/scanlation_group.rb +97 -0
- data/lib/mangadex/sorbet.rb +42 -0
- data/lib/mangadex/tag.rb +10 -0
- data/lib/mangadex/types.rb +24 -0
- data/lib/mangadex/upload.rb +78 -0
- data/lib/mangadex/user.rb +103 -0
- data/lib/mangadex/version.rb +4 -0
- data/lib/mangadex.rb +35 -0
- data/mangadex.gemspec +35 -0
- data/sorbet/config +3 -0
- data/sorbet/rbi/gems/activesupport.rbi +1267 -0
- data/sorbet/rbi/gems/coderay.rbi +285 -0
- data/sorbet/rbi/gems/concurrent-ruby.rbi +1662 -0
- data/sorbet/rbi/gems/domain_name.rbi +52 -0
- data/sorbet/rbi/gems/http-accept.rbi +101 -0
- data/sorbet/rbi/gems/http-cookie.rbi +119 -0
- data/sorbet/rbi/gems/i18n.rbi +133 -0
- data/sorbet/rbi/gems/method_source.rbi +64 -0
- data/sorbet/rbi/gems/mime-types-data.rbi +17 -0
- data/sorbet/rbi/gems/mime-types.rbi +218 -0
- data/sorbet/rbi/gems/netrc.rbi +51 -0
- data/sorbet/rbi/gems/pry.rbi +1898 -0
- data/sorbet/rbi/gems/psych.rbi +471 -0
- data/sorbet/rbi/gems/rake.rbi +660 -0
- data/sorbet/rbi/gems/rest-client.rbi +454 -0
- data/sorbet/rbi/gems/rspec-core.rbi +1939 -0
- data/sorbet/rbi/gems/rspec-expectations.rbi +1150 -0
- data/sorbet/rbi/gems/rspec-mocks.rbi +1100 -0
- data/sorbet/rbi/gems/rspec-support.rbi +280 -0
- data/sorbet/rbi/gems/rspec.rbi +15 -0
- data/sorbet/rbi/gems/tzinfo.rbi +586 -0
- data/sorbet/rbi/gems/unf.rbi +19 -0
- data/sorbet/rbi/hidden-definitions/errors.txt +3942 -0
- data/sorbet/rbi/hidden-definitions/hidden.rbi +8210 -0
- data/sorbet/rbi/sorbet-typed/lib/activesupport/>=6/activesupport.rbi +37 -0
- data/sorbet/rbi/sorbet-typed/lib/activesupport/all/activesupport.rbi +1850 -0
- data/sorbet/rbi/sorbet-typed/lib/minitest/all/minitest.rbi +108 -0
- data/sorbet/rbi/sorbet-typed/lib/rake/all/rake.rbi +645 -0
- data/sorbet/rbi/sorbet-typed/lib/rspec-core/all/rspec-core.rbi +1891 -0
- data/sorbet/rbi/todo.rbi +7 -0
- metadata +243 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
module Mangadex
|
|
3
|
+
module Internal
|
|
4
|
+
class Definition
|
|
5
|
+
attr_reader :key, :value, :converts, :accepts, :required
|
|
6
|
+
attr_reader :errors
|
|
7
|
+
|
|
8
|
+
def initialize(key, value, converts: nil, accepts: nil, required: false)
|
|
9
|
+
@converts = converts
|
|
10
|
+
@key = key
|
|
11
|
+
@value = convert_value(value)
|
|
12
|
+
@raw_value = value
|
|
13
|
+
@accepts = accepts
|
|
14
|
+
@required = required ? true : false
|
|
15
|
+
@errors = Array.new
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def empty?
|
|
19
|
+
value.respond_to?(:empty?) ? value.empty? : value.to_s.strip.empty?
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def validate!
|
|
23
|
+
validate_required!
|
|
24
|
+
validate_accepts!
|
|
25
|
+
|
|
26
|
+
true
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def valid?
|
|
30
|
+
validate!
|
|
31
|
+
rescue ArgumentError
|
|
32
|
+
false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def error
|
|
36
|
+
validate! && nil
|
|
37
|
+
rescue ArgumentError => error
|
|
38
|
+
error.message
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def convert_value(value)
|
|
42
|
+
if converts.is_a?(Proc)
|
|
43
|
+
converts.call(value)
|
|
44
|
+
elsif converts.is_a?(String) || converts.is_a?(Symbol)
|
|
45
|
+
value.send(converts)
|
|
46
|
+
else
|
|
47
|
+
value
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def validate_required!
|
|
52
|
+
return unless required
|
|
53
|
+
|
|
54
|
+
if empty?
|
|
55
|
+
raise ArgumentError, "Missing :#{key}"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def validate_accepts!
|
|
60
|
+
return if value.nil? && !required
|
|
61
|
+
return unless accepts
|
|
62
|
+
|
|
63
|
+
if accepts.is_a?(Class) && !value.is_a?(accepts)
|
|
64
|
+
raise ArgumentError, "Expected :#{key} to be a #{accepts}, but got a #{value.class}"
|
|
65
|
+
end
|
|
66
|
+
if accepts.is_a?(Regexp) && !(accepts === value)
|
|
67
|
+
raise ArgumentError, "Expected :#{key} to match /#{accepts}/"
|
|
68
|
+
end
|
|
69
|
+
if accepts.is_a?(Array)
|
|
70
|
+
if accepts.count == 1 && accepts[0].is_a?(Class)
|
|
71
|
+
expected_class = accepts[0]
|
|
72
|
+
if !value.is_a?(Array)
|
|
73
|
+
raise ArgumentError, "Expected :#{key} to be an Array of #{expected_class}, but got #{value}"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
invalid_elements = []
|
|
77
|
+
value.each do |x|
|
|
78
|
+
invalid_elements << x unless x.is_a?(expected_class)
|
|
79
|
+
end
|
|
80
|
+
return if invalid_elements.empty?
|
|
81
|
+
bad = invalid_elements.map { |x| "<#{x}:#{x.class}>" }
|
|
82
|
+
raise ArgumentError, "Expected elements in :#{key} to be an Array of #{expected_class}, but found #{bad}"
|
|
83
|
+
else
|
|
84
|
+
if value.is_a?(Array)
|
|
85
|
+
extra_elements = value - accepts
|
|
86
|
+
return if extra_elements.empty?
|
|
87
|
+
raise ArgumentError, "Expected elements in :#{key} to be one of #{accepts}, but found #{extra_elements}"
|
|
88
|
+
elsif !(value.nil? || (value.respond_to?(:empty?) && value.empty?)) && !accepts.include?(value)
|
|
89
|
+
raise ArgumentError, "Expected :#{key} to be one of #{accepts}, but got #{@raw_value}:#{@raw_value.class}"
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
class << self
|
|
96
|
+
def converts(key=nil)
|
|
97
|
+
procs = { to_a: -> ( x ) { Array(x) } }
|
|
98
|
+
return procs if key.nil?
|
|
99
|
+
|
|
100
|
+
procs[key]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def chapter_list(args)
|
|
104
|
+
validate(
|
|
105
|
+
args,
|
|
106
|
+
{
|
|
107
|
+
limit: { accepts: Integer },
|
|
108
|
+
offset: { accepts: Integer },
|
|
109
|
+
translated_language: { accepts: String },
|
|
110
|
+
original_language: { accepts: [String] },
|
|
111
|
+
excluded_original_language: { accepts: [String] },
|
|
112
|
+
content_rating: { accepts: %w(safe suggestive erotica pornographic), converts: converts(:to_a) },
|
|
113
|
+
include_future_updates: { accepts: %w(0 1) },
|
|
114
|
+
created_at_since: { accepts: %r{^\d{4}-[0-1]\d-([0-2]\d|3[0-1])T([0-1]\d|2[0-3]):[0-5]\d:[0-5]\d$} },
|
|
115
|
+
updated_at_since: { accepts: %r{^\d{4}-[0-1]\d-([0-2]\d|3[0-1])T([0-1]\d|2[0-3]):[0-5]\d:[0-5]\d$} },
|
|
116
|
+
publish_at_since: { accepts: %r{^\d{4}-[0-1]\d-([0-2]\d|3[0-1])T([0-1]\d|2[0-3]):[0-5]\d:[0-5]\d$} },
|
|
117
|
+
order: { accepts: Hash },
|
|
118
|
+
includes: { accepts: [String], converts: converts(:to_a) },
|
|
119
|
+
},
|
|
120
|
+
)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def validate(args, definition)
|
|
124
|
+
args = Hash(args).with_indifferent_access
|
|
125
|
+
definition = Hash(definition).with_indifferent_access
|
|
126
|
+
return args if definition.empty?
|
|
127
|
+
|
|
128
|
+
errors = []
|
|
129
|
+
extra_keys = args.keys - definition.keys
|
|
130
|
+
extra_keys.each do |extra_key|
|
|
131
|
+
errors << { extra: extra_key }
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
definition.each do |key, definition|
|
|
135
|
+
validator = Definition.new(key, args[key], **definition.symbolize_keys)
|
|
136
|
+
validation_error = validator.error
|
|
137
|
+
if validation_error
|
|
138
|
+
errors << { message: validation_error }
|
|
139
|
+
elsif !validator.empty?
|
|
140
|
+
args[key] = validator.value
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
if errors.any?
|
|
145
|
+
error_message = errors.map do |error|
|
|
146
|
+
if error[:extra]
|
|
147
|
+
"params[:#{error[:extra]}] does not exist and cannot be passed to this request"
|
|
148
|
+
elsif error[:message]
|
|
149
|
+
error[:message]
|
|
150
|
+
else
|
|
151
|
+
error.to_s
|
|
152
|
+
end
|
|
153
|
+
end.join(', ')
|
|
154
|
+
raise ArgumentError, "Validation error: #{error_message}"
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
args.symbolize_keys
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# typed: false
|
|
2
|
+
require 'rest-client'
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'active_support/core_ext/object/to_query'
|
|
5
|
+
require 'active_support/core_ext/hash/keys'
|
|
6
|
+
|
|
7
|
+
module Mangadex
|
|
8
|
+
module Internal
|
|
9
|
+
class Request
|
|
10
|
+
BASE_URI = 'https://api.mangadex.org'
|
|
11
|
+
ALLOWED_METHODS = %i(get post put delete).freeze
|
|
12
|
+
|
|
13
|
+
attr_accessor :path, :headers, :payload, :method, :raw
|
|
14
|
+
attr_reader :response
|
|
15
|
+
|
|
16
|
+
def self.get(path, params={}, auth: false, headers: nil, raw: false)
|
|
17
|
+
new(
|
|
18
|
+
path_with_params(path, params),
|
|
19
|
+
method: :get,
|
|
20
|
+
headers: headers,
|
|
21
|
+
payload: nil,
|
|
22
|
+
).run!(raw: raw, auth: auth)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.post(path, headers: nil, auth: false, payload: nil, raw: false)
|
|
26
|
+
new(path, method: :post, headers: headers, payload: payload).run!(raw: raw, auth: auth)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.put(path, headers: nil, auth: false, payload: nil, raw: false)
|
|
30
|
+
new(path, method: :put, headers: headers, payload: payload).run!(raw: raw, auth: auth)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.delete(path, headers: nil, auth: false, payload: nil, raw: false)
|
|
34
|
+
new(path, method: :delete, headers: headers, payload: payload).run!(raw: raw, auth: auth)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def initialize(path, method:, headers: nil, payload: nil)
|
|
38
|
+
@path = path
|
|
39
|
+
@headers = Hash(headers)
|
|
40
|
+
@payload = payload
|
|
41
|
+
@method = ensure_method!(method)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def request
|
|
45
|
+
RestClient::Request.new(
|
|
46
|
+
method: method,
|
|
47
|
+
url: request_url,
|
|
48
|
+
headers: request_headers,
|
|
49
|
+
payload: request_payload,
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def run!(raw: false, auth: false)
|
|
54
|
+
payload_details = request_payload ? "Payload: #{request_payload}" : "{no-payload}"
|
|
55
|
+
puts("[#{self.class.name}] #{method.to_s.upcase} #{request_url} #{payload_details}")
|
|
56
|
+
|
|
57
|
+
raise Mangadex::UserNotLoggedIn.new if auth && Mangadex::Api::Context.user.nil?
|
|
58
|
+
|
|
59
|
+
start_time = Time.now
|
|
60
|
+
|
|
61
|
+
@response = request.execute
|
|
62
|
+
end_time = Time.now
|
|
63
|
+
elapsed_time = ((end_time - start_time) * 1000).to_i
|
|
64
|
+
puts("[#{self.class.name}] took #{elapsed_time} ms")
|
|
65
|
+
|
|
66
|
+
if @response.body
|
|
67
|
+
raw ? @response.body : Mangadex::Api::Response.coerce(JSON.parse(@response.body))
|
|
68
|
+
end
|
|
69
|
+
rescue RestClient::Exception => error
|
|
70
|
+
if error.response.body
|
|
71
|
+
raw ? error.response.body : Mangadex::Api::Response.coerce(JSON.parse(error.response.body))
|
|
72
|
+
else
|
|
73
|
+
raise error
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def self.path_with_params(path, params)
|
|
80
|
+
return path if params.blank?
|
|
81
|
+
|
|
82
|
+
params = params.deep_transform_keys do |key|
|
|
83
|
+
key.to_s.camelize(:lower)
|
|
84
|
+
end
|
|
85
|
+
"#{path}?#{params.to_query}"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def request_url
|
|
89
|
+
request_path = path.start_with?('/') ? path : "/#{path}"
|
|
90
|
+
"#{BASE_URI}#{request_path}"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def request_payload
|
|
94
|
+
return if payload.nil? || payload.empty?
|
|
95
|
+
|
|
96
|
+
JSON.generate(payload)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def request_headers
|
|
100
|
+
return headers if Mangadex::Api::Context.user.nil?
|
|
101
|
+
|
|
102
|
+
headers.merge({
|
|
103
|
+
Authorization: Mangadex::Api::Context.user.with_valid_session.session,
|
|
104
|
+
})
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def missing_method?(method)
|
|
108
|
+
method.nil? || method.to_s.strip.empty?
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def ensure_method!(method)
|
|
112
|
+
raise 'Method must be present' if missing_method?(method)
|
|
113
|
+
|
|
114
|
+
clean_method = method.to_s.downcase.to_sym
|
|
115
|
+
return clean_method if ALLOWED_METHODS.include?(clean_method)
|
|
116
|
+
|
|
117
|
+
raise "Invalid method: #{method}. Must be one of: #{ALLOWED_METHODS}"
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# typed: false
|
|
2
|
+
|
|
3
|
+
require "active_support/hash_with_indifferent_access"
|
|
4
|
+
|
|
5
|
+
module Mangadex
|
|
6
|
+
module Internal
|
|
7
|
+
module WithAttributes
|
|
8
|
+
extend ActiveSupport::Concern
|
|
9
|
+
|
|
10
|
+
attr_accessor \
|
|
11
|
+
:id,
|
|
12
|
+
:type,
|
|
13
|
+
:attributes,
|
|
14
|
+
:relationships
|
|
15
|
+
|
|
16
|
+
class_methods do
|
|
17
|
+
USING_ATTRIBUTES = {}
|
|
18
|
+
|
|
19
|
+
def has_attributes(*attributes)
|
|
20
|
+
USING_ATTRIBUTES[self.name] = Array(USING_ATTRIBUTES[self.name]).concat(
|
|
21
|
+
attributes.map(&:to_sym)
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def attributes
|
|
26
|
+
USING_ATTRIBUTES[self.name] || []
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def type
|
|
30
|
+
self.name.split('::').last.underscore
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def from_data(data)
|
|
34
|
+
base_class_name = self.name.gsub('::', '_')
|
|
35
|
+
klass_name = self.name
|
|
36
|
+
target_attributes_class_name = "#{base_class_name}_Attributes"
|
|
37
|
+
|
|
38
|
+
klass = if const_defined?(target_attributes_class_name)
|
|
39
|
+
target_attributes_class_name.constantize
|
|
40
|
+
else
|
|
41
|
+
class_contents = <<-END
|
|
42
|
+
# typed: true
|
|
43
|
+
class ::#{target_attributes_class_name} < MangadexObject
|
|
44
|
+
#{USING_ATTRIBUTES[klass_name].map {|attribute| "sig { returns(T.untyped) }; attr_accessor(:#{attribute})"}.join(';')}
|
|
45
|
+
|
|
46
|
+
def self.attributes_to_inspect
|
|
47
|
+
#{USING_ATTRIBUTES[klass_name]}
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
END
|
|
51
|
+
|
|
52
|
+
eval class_contents
|
|
53
|
+
Object.const_get(target_attributes_class_name)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
data = data.with_indifferent_access
|
|
57
|
+
|
|
58
|
+
relationships = data['relationships']&.map do |relationship_data|
|
|
59
|
+
Relationship.from_data(relationship_data)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
attributes = klass.new(**Hash(data['attributes']))
|
|
63
|
+
|
|
64
|
+
initialize_hash = {
|
|
65
|
+
id: data['id'],
|
|
66
|
+
type: data['type'] || self.type,
|
|
67
|
+
attributes: attributes,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
initialize_hash.merge!({relationships: relationships}) if relationships.present?
|
|
71
|
+
|
|
72
|
+
new(**initialize_hash)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
included do
|
|
77
|
+
def ==(other)
|
|
78
|
+
if other.is_a?(String)
|
|
79
|
+
id == other
|
|
80
|
+
elsif other.respond_to?(:id)
|
|
81
|
+
id == other.id
|
|
82
|
+
else
|
|
83
|
+
false
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def any_relationships?
|
|
88
|
+
Array(relationships).any?
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def method_missing(method_name, *args, **kwargs)
|
|
92
|
+
if self.class.attributes.include?(method_name.to_sym)
|
|
93
|
+
return if attributes.nil?
|
|
94
|
+
return unless attributes.respond_to?(method_name)
|
|
95
|
+
|
|
96
|
+
attributes.send(method_name)
|
|
97
|
+
elsif any_relationships?
|
|
98
|
+
existing_relationships = relationships.map(&:type)
|
|
99
|
+
original_relationship = method_name.to_s
|
|
100
|
+
looking_for_relationship = original_relationship.singularize
|
|
101
|
+
is_looking_for_many = original_relationship != looking_for_relationship
|
|
102
|
+
|
|
103
|
+
if existing_relationships.include?(looking_for_relationship)
|
|
104
|
+
search_method = is_looking_for_many ? :select : :find
|
|
105
|
+
relationships.send(search_method) do |relationship|
|
|
106
|
+
relationship.type == looking_for_relationship
|
|
107
|
+
end
|
|
108
|
+
elsif is_looking_for_many
|
|
109
|
+
[]
|
|
110
|
+
else
|
|
111
|
+
super
|
|
112
|
+
end
|
|
113
|
+
else
|
|
114
|
+
super
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
require_relative "mangadex_object"
|
|
3
|
+
|
|
4
|
+
module Mangadex
|
|
5
|
+
class Manga < MangadexObject
|
|
6
|
+
has_attributes \
|
|
7
|
+
:title,
|
|
8
|
+
:alt_titles,
|
|
9
|
+
:description,
|
|
10
|
+
:is_locked,
|
|
11
|
+
:links,
|
|
12
|
+
:original_language,
|
|
13
|
+
:last_volume,
|
|
14
|
+
:last_chapter,
|
|
15
|
+
:publication_demographic,
|
|
16
|
+
:status,
|
|
17
|
+
:year,
|
|
18
|
+
:content_rating,
|
|
19
|
+
:tags,
|
|
20
|
+
:version,
|
|
21
|
+
:created_at,
|
|
22
|
+
:updated_at
|
|
23
|
+
|
|
24
|
+
sig { params(args: T::Api::Arguments).returns(T::Api::MangaResponse) }
|
|
25
|
+
def self.list(**args)
|
|
26
|
+
to_a = Mangadex::Internal::Definition.converts(:to_a)
|
|
27
|
+
|
|
28
|
+
Mangadex::Internal::Request.get(
|
|
29
|
+
'/manga',
|
|
30
|
+
Mangadex::Internal::Definition.validate(args, {
|
|
31
|
+
limit: { accepts: Integer },
|
|
32
|
+
offset: { accepts: Integer },
|
|
33
|
+
title: { accepts: String },
|
|
34
|
+
authors: { accepts: [String] },
|
|
35
|
+
artists: { accepts: [String] },
|
|
36
|
+
year: { accepts: Integer },
|
|
37
|
+
included_tags: { accepts: [String] },
|
|
38
|
+
included_tags_mode: { accepts: %w(OR AND), converts: to_a },
|
|
39
|
+
excluded_tags: { accepts: [String] },
|
|
40
|
+
excluded_tags_mode: { accepts: %w(OR AND), converts: to_a },
|
|
41
|
+
status: { accepts: %w(ongoing completed hiatus cancelled), converts: to_a },
|
|
42
|
+
original_language: { accepts: [String] },
|
|
43
|
+
excluded_original_language: { accepts: [String] },
|
|
44
|
+
available_translated_language: { accepts: [String] },
|
|
45
|
+
publication_demographic: { accepts: %w(shounen shoujo josei seinen none), converts: to_a },
|
|
46
|
+
ids: { accepts: Array },
|
|
47
|
+
content_rating: { accepts: %w(safe suggestive erotica pornographic), converts: to_a },
|
|
48
|
+
created_at_since: { accepts: %r{^\d{4}-[0-1]\d-([0-2]\d|3[0-1])T([0-1]\d|2[0-3]):[0-5]\d:[0-5]\d$} },
|
|
49
|
+
updated_at_since: { accepts: %r{^\d{4}-[0-1]\d-([0-2]\d|3[0-1])T([0-1]\d|2[0-3]):[0-5]\d:[0-5]\d$} },
|
|
50
|
+
order: { accepts: Hash },
|
|
51
|
+
includes: { accepts: Array, converts: to_a },
|
|
52
|
+
}),
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
sig { params(id: String, args: T::Api::Arguments).returns(Hash) }
|
|
57
|
+
def self.volumes_and_chapters(id, **args)
|
|
58
|
+
Mangadex::Internal::Request.get(
|
|
59
|
+
'/manga/%{id}/aggregate' % {id: id},
|
|
60
|
+
Mangadex::Internal::Definition.validate(args, {
|
|
61
|
+
translated_language: { accepts: Array },
|
|
62
|
+
groups: { accepts: Array },
|
|
63
|
+
}),
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
sig { params(id: String, args: T::Api::Arguments).returns(T::Api::MangaResponse) }
|
|
68
|
+
def self.view(id, **args)
|
|
69
|
+
Mangadex::Internal::Request.get(
|
|
70
|
+
'/manga/%{id}' % {id: id},
|
|
71
|
+
Mangadex::Internal::Definition.validate(args, {
|
|
72
|
+
includes: { accepts: Array },
|
|
73
|
+
})
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
sig { params(id: String).returns(T.any(Hash, Mangadex::Api::Response)) }
|
|
78
|
+
def self.unfollow(id)
|
|
79
|
+
Mangadex::Internal::Request.delete(
|
|
80
|
+
'/manga/%{id}/follow' % {id: id},
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
sig { params(id: String).returns(T.any(Hash, Mangadex::Api::Response)) }
|
|
85
|
+
def self.follow(id)
|
|
86
|
+
Mangadex::Internal::Request.post(
|
|
87
|
+
'/manga/%{id}/follow' % {id: id},
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
sig { params(id: String, args: T::Api::Arguments).returns(T::Api::ChapterResponse) }
|
|
92
|
+
def self.feed(id, **args)
|
|
93
|
+
Mangadex::Internal::Request.get(
|
|
94
|
+
'/manga/%{id}/feed' % {id: id},
|
|
95
|
+
Mangadex::Internal::Definition.chapter_list(args),
|
|
96
|
+
)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
sig { params(args: T::Api::Arguments).returns(T::Api::MangaResponse) }
|
|
100
|
+
def self.random(**args)
|
|
101
|
+
Mangadex::Internal::Request.get(
|
|
102
|
+
'/manga/random',
|
|
103
|
+
Mangadex::Internal::Definition.validate(args, {
|
|
104
|
+
includes: { accepts: Array },
|
|
105
|
+
})
|
|
106
|
+
)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
sig { returns(Mangadex::Api::Response[Mangadex::Tag]) }
|
|
110
|
+
def self.tag_list
|
|
111
|
+
Mangadex::Internal::Request.get(
|
|
112
|
+
'/manga/tag'
|
|
113
|
+
)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
sig { params(args: T::Api::Arguments).returns(T::Api::GenericResponse) }
|
|
117
|
+
def self.reading_status(**args)
|
|
118
|
+
Mangadex::Internal::Request.get(
|
|
119
|
+
'/manga/status',
|
|
120
|
+
Mangadex::Internal::Definition.validate(args, {
|
|
121
|
+
status: {
|
|
122
|
+
accepts: %w(reading on_hold dropped plan_to_read re_reading completed),
|
|
123
|
+
converts: Mangadex::Internal::Definition.converts(:to_a),
|
|
124
|
+
},
|
|
125
|
+
})
|
|
126
|
+
)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
sig { params(id: String).returns(T::Api::GenericResponse) }
|
|
130
|
+
def self.reading_status(id)
|
|
131
|
+
Mangadex::Internal::Request.get(
|
|
132
|
+
'/manga/%{id}/status' % {id: id},
|
|
133
|
+
)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
sig { returns(T::Api::GenericResponse) }
|
|
137
|
+
def self.all_reading_status
|
|
138
|
+
Mangadex::Internal::Request.get(
|
|
139
|
+
'/manga/status',
|
|
140
|
+
)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
sig { params(id: String, status: String).returns(T::Api::GenericResponse) }
|
|
144
|
+
def self.update_reading_status(id, status)
|
|
145
|
+
Mangadex::Internal::Request.post(
|
|
146
|
+
'/manga/%{id}/status' % {id: id},
|
|
147
|
+
payload: Mangadex::Internal::Definition.validate({status: status}, {
|
|
148
|
+
status: {
|
|
149
|
+
accepts: %w(reading on_hold dropped plan_to_read re_reading completed),
|
|
150
|
+
required: true,
|
|
151
|
+
},
|
|
152
|
+
})
|
|
153
|
+
)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Untested API endpoints
|
|
157
|
+
sig { params(id: String, args: T::Api::Arguments).returns(T::Api::MangaResponse) }
|
|
158
|
+
def self.update(id, **args)
|
|
159
|
+
Mangadex::Internal::Request.put('/manga/%{id}' % {id: id}, payload: args)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
sig { params(id: String).returns(Hash) }
|
|
163
|
+
def self.delete(id)
|
|
164
|
+
Mangadex::Internal::Request.delete(
|
|
165
|
+
'/manga/%{id}' % {id: id},
|
|
166
|
+
)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def self.create(**args)
|
|
170
|
+
Mangadex::Internal::Request.post('/manga', payload: args)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def self.attributes_to_inspect
|
|
174
|
+
[:id, :type, :title, :content_rating, :original_language, :year]
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
class << self
|
|
178
|
+
alias_method :aggregate, :volumes_and_chapters
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
sig { returns(T.nilable(ContentRating)) }
|
|
182
|
+
def content_rating
|
|
183
|
+
return unless attributes&.content_rating.present?
|
|
184
|
+
|
|
185
|
+
ContentRating.new(attributes.content_rating)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# typed: false
|
|
2
|
+
require "active_support/inflector"
|
|
3
|
+
require_relative "internal/with_attributes"
|
|
4
|
+
|
|
5
|
+
module Mangadex
|
|
6
|
+
class MangadexObject
|
|
7
|
+
extend T::Sig
|
|
8
|
+
include Internal::WithAttributes
|
|
9
|
+
|
|
10
|
+
def self.attributes_to_inspect
|
|
11
|
+
to_inspect = [:id, :type]
|
|
12
|
+
if self.respond_to?(:inspect_attributes)
|
|
13
|
+
to_inspect.concat(Array(self.inspect_attributes))
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
to_inspect
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def initialize(**args)
|
|
20
|
+
args.keys.each do |attribute|
|
|
21
|
+
original_attribute = attribute
|
|
22
|
+
attribute = attribute.to_s.underscore
|
|
23
|
+
attribute_to_set = "#{attribute}="
|
|
24
|
+
|
|
25
|
+
if respond_to?(attribute_to_set)
|
|
26
|
+
if %w(created_at updated_at publish_at).include?(attribute)
|
|
27
|
+
args[original_attribute] = DateTime.parse(args[original_attribute])
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
send(attribute_to_set, args[original_attribute])
|
|
31
|
+
else
|
|
32
|
+
warn("Ignoring setter `#{attribute_to_set}` on #{self.class.name}...")
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
self.type = self.class.type if self.type.blank?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def eq?(other)
|
|
40
|
+
return id == other.id if respond_to?(:id) && other.respond_to?(:id)
|
|
41
|
+
|
|
42
|
+
super
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def hash
|
|
46
|
+
id.hash
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def inspect
|
|
50
|
+
string = "#<#{self.class.name}:#{self.object_id} "
|
|
51
|
+
fields = self.class.attributes_to_inspect.map do |field|
|
|
52
|
+
value = self.send(field)
|
|
53
|
+
if !value.nil?
|
|
54
|
+
"@#{field}=\"#{value}\""
|
|
55
|
+
end
|
|
56
|
+
rescue => error
|
|
57
|
+
"@#{field}[!]={#{error.class.name}: #{error.message}}"
|
|
58
|
+
end.compact
|
|
59
|
+
string << fields.join(" ") << ">"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# typed: false
|
|
2
|
+
module Mangadex
|
|
3
|
+
class Relationship < MangadexObject
|
|
4
|
+
attr_accessor :id, :type, :attributes
|
|
5
|
+
|
|
6
|
+
class << self
|
|
7
|
+
def from_data(data)
|
|
8
|
+
data = data.with_indifferent_access
|
|
9
|
+
klass = class_for_relationship_type(data['type'])
|
|
10
|
+
|
|
11
|
+
return klass.from_data(data) if klass && data['attributes']&.any?
|
|
12
|
+
|
|
13
|
+
new(
|
|
14
|
+
id: data['id'],
|
|
15
|
+
type: data['type'],
|
|
16
|
+
attributes: OpenStruct.new(data['attributes']),
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def build_attributes(data)
|
|
23
|
+
klass = class_for_relationship_type(data['type'])
|
|
24
|
+
if klass.present?
|
|
25
|
+
klass.from_data(data)
|
|
26
|
+
else
|
|
27
|
+
OpenStruct.new(data['attributes'])
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def class_for_relationship_type(type)
|
|
32
|
+
module_parts = self.name.split('::')
|
|
33
|
+
module_name = module_parts.take(module_parts.size - 1).join('::')
|
|
34
|
+
klass_name = "#{module_name}::#{type.split('_').collect(&:capitalize).join}"
|
|
35
|
+
|
|
36
|
+
return unless Object.const_defined?(klass_name)
|
|
37
|
+
|
|
38
|
+
Object.const_get(klass_name)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def inspect
|
|
43
|
+
"#<#{self.class} id=#{id.inspect} type=#{type.inspect}>"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|