mangadex 5.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|