mangadex 5.4.9 → 5.4.11
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 +4 -4
- data/Gemfile.lock +7 -5
- data/README.md +335 -1
- data/bin/console +14 -7
- data/docs/authentication.md +1 -4
- data/lib/config.rb +5 -7
- data/lib/mangadex/api/user.rb +49 -11
- data/lib/mangadex/api/version_checker.rb +1 -1
- data/lib/mangadex/at_home.rb +30 -0
- data/lib/mangadex/auth.rb +22 -5
- data/lib/mangadex/chapter.rb +3 -5
- data/lib/mangadex/custom_list.rb +2 -1
- data/lib/mangadex/internal/context.rb +8 -7
- data/lib/mangadex/internal/definition.rb +2 -1
- data/lib/mangadex/internal/request.rb +11 -4
- data/lib/mangadex/internal/with_attributes.rb +5 -2
- data/lib/mangadex/manga.rb +20 -4
- data/lib/mangadex/relationship.rb +7 -2
- data/lib/mangadex/scanlation_group.rb +1 -0
- data/lib/mangadex/types.rb +3 -0
- data/lib/mangadex/version.rb +1 -1
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 29160bf116b71505a8b9697dd7c3940054e7f0b7a837c4f34470b5bff896879c
|
|
4
|
+
data.tar.gz: a245fdd04997fbc3ce6db61d2b3ab831d80df24db6431efc8a9a516a55111617
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0234ba9078a3927eecc7bf84acfde344612c4b4ae3ae0b8b30074daf320d874540bf2dfd4a4e82b7b23cfa534ed919271fa398ad22d0bdd8045ed6c2b2c5d1b6
|
|
7
|
+
data.tar.gz: 4ac033f5cfa89f851d2e8842b50b2bf487b5310c7c92391e0466bf6183a0b0fee1be1bb0b97765e6b8cf4597285edf8c84f7ee38f9892d7592d3cd4171180199
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
mangadex (5.
|
|
4
|
+
mangadex (5.4.11)
|
|
5
5
|
activesupport (~> 6.1)
|
|
6
6
|
psych (~> 4.0.1)
|
|
7
7
|
rest-client (~> 2.1)
|
|
@@ -32,9 +32,9 @@ GEM
|
|
|
32
32
|
i18n (1.8.10)
|
|
33
33
|
concurrent-ruby (~> 1.0)
|
|
34
34
|
method_source (1.0.0)
|
|
35
|
-
mime-types (3.
|
|
35
|
+
mime-types (3.4.1)
|
|
36
36
|
mime-types-data (~> 3.2015)
|
|
37
|
-
mime-types-data (3.
|
|
37
|
+
mime-types-data (3.2022.0105)
|
|
38
38
|
minitest (5.14.4)
|
|
39
39
|
mustermann (1.1.1)
|
|
40
40
|
ruby2_keywords (~> 0.0.1)
|
|
@@ -42,7 +42,8 @@ GEM
|
|
|
42
42
|
pry (0.14.1)
|
|
43
43
|
coderay (~> 1.1)
|
|
44
44
|
method_source (~> 1.0)
|
|
45
|
-
psych (4.0.
|
|
45
|
+
psych (4.0.3)
|
|
46
|
+
stringio
|
|
46
47
|
public_suffix (4.0.6)
|
|
47
48
|
rack (2.2.3)
|
|
48
49
|
rack-protection (2.1.0)
|
|
@@ -82,9 +83,10 @@ GEM
|
|
|
82
83
|
smart_properties (1.16.3)
|
|
83
84
|
sorbet (0.5.9152)
|
|
84
85
|
sorbet-static (= 0.5.9152)
|
|
85
|
-
sorbet-runtime (0.5.
|
|
86
|
+
sorbet-runtime (0.5.9542)
|
|
86
87
|
sorbet-static (0.5.9152-universal-darwin-20)
|
|
87
88
|
sorbet-static (0.5.9152-x86_64-linux)
|
|
89
|
+
stringio (3.0.1)
|
|
88
90
|
tilt (2.0.10)
|
|
89
91
|
tzinfo (2.0.4)
|
|
90
92
|
concurrent-ruby (~> 1.0)
|
data/README.md
CHANGED
|
@@ -23,7 +23,341 @@ Or install it yourself as:
|
|
|
23
23
|
## Usage
|
|
24
24
|
|
|
25
25
|
Please note that I tried my best to follow Mangadex's naming conventions for [their documentation](https://api.mangadex.org). Track the progress [here in an issue](https://github.com/thedrummeraki/mangadex/issues/5).
|
|
26
|
-
|
|
26
|
+
Although a work a in progress, feel free to [check this out](lib/mangadex).
|
|
27
|
+
|
|
28
|
+
### Basic Usage
|
|
29
|
+
|
|
30
|
+
Here's a couple of cool things you can do with the gem:
|
|
31
|
+
|
|
32
|
+
#### Get a list of manga
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
response = Mangadex::Manga.list
|
|
36
|
+
manga = response.data # Array of #<Mangadex::Manga>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
#### Get a manga by id, with cover_art
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
manga_id = 'd86cf65b-5f6c-437d-a0af-19a31f94ec55'
|
|
43
|
+
response = Mangadex::Manga.get(manga_id, includes: :cover_art)
|
|
44
|
+
entity = response.data # Object of #<Mangadex::Manga>
|
|
45
|
+
|
|
46
|
+
# Original size
|
|
47
|
+
entity.cover_art.image_url(size: :original)
|
|
48
|
+
entity.cover_art.image_url(size: :medium)
|
|
49
|
+
entity.cover_art.image_url(size: :small) # default size
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
#### Get a manga's chapter list, ordered by volume and chapter number
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
manga_id = 'd86cf65b-5f6c-437d-a0af-19a31f94ec55'
|
|
56
|
+
manga_response = Mangadex::Manga.get(manga_id, includes: :cover_art)
|
|
57
|
+
entity = manga_response.data
|
|
58
|
+
|
|
59
|
+
chapter_response = Mangadex::Chapter.list(
|
|
60
|
+
manga: entity.id,
|
|
61
|
+
order: { volume: 'asc', chapter: 'asc' },
|
|
62
|
+
translated_language: 'en',
|
|
63
|
+
)
|
|
64
|
+
chapters = chapter_response.data # Array of #<Mangadex::Chapter>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
#### Get a chapter's pages
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
chapter_id = 'e7bb1892-7f83-4a89-bccc-0d6d403a85fc'
|
|
71
|
+
chapter = Mangadex::Chapter.get(chapter_id).data
|
|
72
|
+
pages = chapter.page_urls # Data saver true by default
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
#### Search for manga by title
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
response = Mangadex::Manga.list(title: 'Ijiranaide nagatoro')
|
|
79
|
+
found_manga = response.data
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
#### Authenticate
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
Mangadex::Auth.login(username: 'username', password: 'password') do |user|
|
|
86
|
+
# `user` is of type Mangadex::Api::User
|
|
87
|
+
puts(user.mangadex_user_id)
|
|
88
|
+
puts(user.session)
|
|
89
|
+
puts(user.refresh)
|
|
90
|
+
puts(user.session_valid_until)
|
|
91
|
+
end
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
You can access the authenticated user by using context:
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
user = Mangadex.context.user
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
#### Refresh the user's token
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
Mangadex.context.user.refresh_session! do |user|
|
|
104
|
+
# `user` is of type Mangadex::Api::User
|
|
105
|
+
puts(user.mangadex_user_id)
|
|
106
|
+
puts(user.session)
|
|
107
|
+
puts(user.refresh)
|
|
108
|
+
puts(user.session_valid_until)
|
|
109
|
+
end
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
#### Create an public MDList, add then remove a manga
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
Mangadex::Auth.login(...)
|
|
116
|
+
response = Mangadex::CustomList.create(name: 'My awesome list!', visibility: 'public')
|
|
117
|
+
custom_list = response.data
|
|
118
|
+
|
|
119
|
+
manga_id = 'd86cf65b-5f6c-437d-a0af-19a31f94ec55'
|
|
120
|
+
# Add the manga
|
|
121
|
+
custom_list.add_manga(manga_id)
|
|
122
|
+
|
|
123
|
+
# Remove the manga
|
|
124
|
+
custom_list.remove_manga(manga_id)
|
|
125
|
+
|
|
126
|
+
# Get manga list
|
|
127
|
+
manga = custom_list.manga_details.data # Array of #<Mangadex::Manga>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Are you on Rails?
|
|
131
|
+
|
|
132
|
+
This gem tries its best to be agnostic to popular frameworks like Rails. Here's however a couple of things to can do to integrate the gem to your project.
|
|
133
|
+
|
|
134
|
+
First, add the gem to your `Gemfile`.
|
|
135
|
+
|
|
136
|
+
#### Configurating the gem
|
|
137
|
+
|
|
138
|
+
Create a initilizer file to `config/initializers/mangadex.rb`. You can add the following:
|
|
139
|
+
|
|
140
|
+
```ruby
|
|
141
|
+
Mangadex.configure do |config|
|
|
142
|
+
# Override the default content ratings
|
|
143
|
+
config.default_content_ratings = %w(safe suggestive)
|
|
144
|
+
|
|
145
|
+
# Override the Mangadex API URL (ie: proxy)
|
|
146
|
+
config.mangadex_url = 'https://my-proxy-mangadex-url.com'
|
|
147
|
+
end
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
#### Authenticate your users
|
|
151
|
+
|
|
152
|
+
This could be useful if you want to support authentication. You will need to persist your user's session, refresh token and session expiry date.
|
|
153
|
+
|
|
154
|
+
##### Persist the user information
|
|
155
|
+
|
|
156
|
+
If you haven't done so, create your user.
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
class CreateUsers < ActiveRecord::Migration[6.1]
|
|
160
|
+
def change
|
|
161
|
+
create_table :users do |t|
|
|
162
|
+
t.string :mangadex_user_id, null: false
|
|
163
|
+
t.string :username
|
|
164
|
+
t.string :session
|
|
165
|
+
t.string :refresh
|
|
166
|
+
t.datetime :session_valid_until
|
|
167
|
+
|
|
168
|
+
t.timestamps
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Already created your `User` class? Make sure it has all of the following:
|
|
176
|
+
- `mangade_user_id`: ID used to identify your user on Mangadex
|
|
177
|
+
- `username`: Your username
|
|
178
|
+
- `session`: The session token (valid for 15 minutes)
|
|
179
|
+
- `refresh`: The refresh token, used to refresh the session (valid for 1 month)
|
|
180
|
+
- `session_valid_until`: The time `session` session expires at
|
|
181
|
+
|
|
182
|
+
If anything is missing, create a migration.
|
|
183
|
+
|
|
184
|
+
#### Authentication flow on the controller
|
|
185
|
+
|
|
186
|
+
Add these methods to your controller's helper (ie: `ApplicationHelper`):
|
|
187
|
+
|
|
188
|
+
```ruby
|
|
189
|
+
module ApplicationHelper
|
|
190
|
+
def current_user
|
|
191
|
+
@current_user ||= User.find_by(id: session[:id])
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def logged_in?
|
|
195
|
+
current_user.present?
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def log_in(user)
|
|
199
|
+
# `user` is an instance of your `User` class
|
|
200
|
+
session[:id] = user.id6
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def log_out
|
|
204
|
+
# Logout from Mangadex
|
|
205
|
+
Mangadex::Auth.logout
|
|
206
|
+
|
|
207
|
+
# Remove the session
|
|
208
|
+
session.delete(:id)
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
First make sure `ApplicationController` includes the helper above
|
|
214
|
+
|
|
215
|
+
```ruby
|
|
216
|
+
class ApplicationController < ActionController::Base
|
|
217
|
+
include ApplicationHelper
|
|
218
|
+
|
|
219
|
+
# ...
|
|
220
|
+
end
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
We recommend creating a controller for authentication. Here's how you can implement the login and logout actions:
|
|
224
|
+
|
|
225
|
+
```ruby
|
|
226
|
+
# app/controllers/session_controller.rb
|
|
227
|
+
class SessionController < ApplicationController
|
|
228
|
+
# GET /login
|
|
229
|
+
def new
|
|
230
|
+
# render the login form
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# POST /login
|
|
234
|
+
def create
|
|
235
|
+
username = params[:username]
|
|
236
|
+
password = params[:password]
|
|
237
|
+
|
|
238
|
+
# You can also use `email` instead of `username` to log in
|
|
239
|
+
user = Mangadex::Auth.login(username: username, password: password) do |mangadex_user|
|
|
240
|
+
# Find the user by mangadex user id (or initialize if it doesn't exist)
|
|
241
|
+
our_user = User.find_or_initialize_by(mangadex_user_id: mangadex_user.mangadex_user_id) do |new_user|
|
|
242
|
+
new_user.username = mangadex_user.data.username
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Update the session info data
|
|
246
|
+
our_user.session = mangadex_user.session
|
|
247
|
+
our_user.refresh = mangadex_user.refresh
|
|
248
|
+
our_user.session_valid_until = mangadex_user.session_valid_until
|
|
249
|
+
|
|
250
|
+
# ...then save the user
|
|
251
|
+
our_user.save!
|
|
252
|
+
our_user
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# `user` will be an instance of your `User` class
|
|
256
|
+
# Now, we can log in then redirect to the root.
|
|
257
|
+
log_in(user)
|
|
258
|
+
redirect_to(root_path)
|
|
259
|
+
rescue Mangadex::Errors::AuthenticationError => error
|
|
260
|
+
# See https://api.mangadex.org/docs.html to learn more about errors
|
|
261
|
+
Rails.logger.error(error.response.errors)
|
|
262
|
+
|
|
263
|
+
# Handle authentication errors here
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# DELETE /logout
|
|
267
|
+
def destroy
|
|
268
|
+
log_out
|
|
269
|
+
|
|
270
|
+
redirect_to(root_path)
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Finally, add the routes.
|
|
276
|
+
|
|
277
|
+
```ruby
|
|
278
|
+
# config/routes.rb
|
|
279
|
+
Rails.application.routes.draw do
|
|
280
|
+
# ...
|
|
281
|
+
get '/login' => 'session#new'
|
|
282
|
+
post '/login' => 'session#create'
|
|
283
|
+
delete '/logout' => 'session#destroy'
|
|
284
|
+
end
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
#### Protected resources
|
|
288
|
+
|
|
289
|
+
Here's an example of a controller that requires every action to be logged in. This is based on the steps above.
|
|
290
|
+
|
|
291
|
+
```ruby
|
|
292
|
+
class ProtectedController < ApplicationController
|
|
293
|
+
before_action :ensure_logged_in!
|
|
294
|
+
|
|
295
|
+
private
|
|
296
|
+
|
|
297
|
+
def ensure_logged_in!
|
|
298
|
+
return if logged_in?
|
|
299
|
+
|
|
300
|
+
redirect_to(login_path) # go to /login
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
We're going with managing (list, create, show, edit, delete) MDLists (ie: custom lists). __We're not using strong params below to keep things simple, but you should, especially when mutating data (ie: creating and editing)__.
|
|
306
|
+
|
|
307
|
+
```ruby
|
|
308
|
+
class CustomListsController < ProtectedController
|
|
309
|
+
# GET /custom_list
|
|
310
|
+
def index
|
|
311
|
+
@custom_lists = Mangadex::CustomList.list
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# GET /custom_list/new
|
|
315
|
+
def new
|
|
316
|
+
# new custom list form
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# POST /custom_list
|
|
320
|
+
def create
|
|
321
|
+
@custom_list = Mangadex::CustomList.create(
|
|
322
|
+
name: params[:name],
|
|
323
|
+
visibility: params[:visibility],
|
|
324
|
+
manga: params[:manga], # Manga ID
|
|
325
|
+
)
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# GET /custom_list/<id>
|
|
329
|
+
def show
|
|
330
|
+
@custom_list = Mangadex::CustomList.get(params[:id])
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# GET /custom_list/<id>/edit
|
|
334
|
+
def edit
|
|
335
|
+
@custom_list = Mangadex::CustomList.get(params[:id])
|
|
336
|
+
# edit custom list form
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# PUT /custom_list/<id>
|
|
340
|
+
# PATCH /custom_list/<id>
|
|
341
|
+
def update
|
|
342
|
+
# Note: when updating the custom list, be sure to pass in
|
|
343
|
+
# the current version number!
|
|
344
|
+
@custom_list = Mangadex::CustomList.update(
|
|
345
|
+
params[:id],
|
|
346
|
+
{
|
|
347
|
+
name: params[:name],
|
|
348
|
+
visibility: params[:visibility],
|
|
349
|
+
manga: params[:manga],
|
|
350
|
+
version: params[:version].to_i,
|
|
351
|
+
}
|
|
352
|
+
)
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# DELETE /custom_list/<id>
|
|
356
|
+
def destroy
|
|
357
|
+
Mangadex::CustomList.delete(params[:id])
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
```
|
|
27
361
|
|
|
28
362
|
## Development
|
|
29
363
|
|
data/bin/console
CHANGED
|
@@ -3,16 +3,23 @@
|
|
|
3
3
|
require "bundler/setup"
|
|
4
4
|
require "mangadex"
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
]
|
|
6
|
+
def try_logging_in
|
|
7
|
+
username, password, email = [
|
|
8
|
+
ENV['MD_USERNAME'],
|
|
9
|
+
ENV['MD_PASSWORD'],
|
|
10
|
+
ENV['MD_EMAIL'],
|
|
11
|
+
]
|
|
11
12
|
|
|
12
|
-
if (username || email) && password
|
|
13
|
-
|
|
13
|
+
if (username || email) && password
|
|
14
|
+
Mangadex::Auth.login(username: username, email: email, password: password)
|
|
15
|
+
end
|
|
16
|
+
rescue Mangadex::Errors::StandardError => e
|
|
17
|
+
puts e
|
|
18
|
+
false
|
|
14
19
|
end
|
|
15
20
|
|
|
21
|
+
try_logging_in
|
|
22
|
+
|
|
16
23
|
# You can add fixtures and/or initialization code here to make experimenting
|
|
17
24
|
# with your gem easier. You can also use a different console, if you like.
|
|
18
25
|
|
data/docs/authentication.md
CHANGED
|
@@ -61,10 +61,7 @@ user.data
|
|
|
61
61
|
|
|
62
62
|
```ruby
|
|
63
63
|
# Refreshes the tokens now (Boolean)
|
|
64
|
-
user.
|
|
65
|
-
|
|
66
|
-
# Refreshes the tokens if expired, then return user itself (Mangadex::Api::User)
|
|
67
|
-
user.with_valid_session
|
|
64
|
+
user.refresh_session!
|
|
68
65
|
|
|
69
66
|
# Returns if user.session has expired (Boolean)
|
|
70
67
|
user.session_expired?
|
data/lib/config.rb
CHANGED
|
@@ -4,11 +4,6 @@ module Mangadex
|
|
|
4
4
|
class Config
|
|
5
5
|
extend T::Sig
|
|
6
6
|
|
|
7
|
-
# Class used to persist users
|
|
8
|
-
# Must respond to: :session, :refresh, :mangadex_user_id
|
|
9
|
-
sig { returns(Class) }
|
|
10
|
-
attr_accessor :user_class
|
|
11
|
-
|
|
12
7
|
# Persisting strategy. See Mangadex::Storage::Base for more details.
|
|
13
8
|
sig { returns(Class) }
|
|
14
9
|
attr_accessor :storage_class
|
|
@@ -16,16 +11,19 @@ module Mangadex
|
|
|
16
11
|
sig { returns(T::Array[ContentRating]) }
|
|
17
12
|
attr_accessor :default_content_ratings
|
|
18
13
|
|
|
14
|
+
sig { returns(String) }
|
|
15
|
+
attr_accessor :mangadex_url
|
|
16
|
+
|
|
19
17
|
sig { void }
|
|
20
18
|
def initialize
|
|
21
|
-
@user_class = Api::User
|
|
22
19
|
@storage_class = Storage::Memory
|
|
23
20
|
@default_content_ratings = ContentRating.parse(['safe', 'suggestive', 'erotica'])
|
|
21
|
+
@mangadex_url = 'https://api.mangadex.org'
|
|
24
22
|
end
|
|
25
23
|
|
|
26
24
|
sig { params(klass: Class).void }
|
|
27
25
|
def user_class=(klass)
|
|
28
|
-
missing_methods = [:session, :refresh, :mangadex_user_id] - klass.
|
|
26
|
+
missing_methods = [:session, :refresh, :mangadex_user_id] - klass.new.methods
|
|
29
27
|
if missing_methods.empty?
|
|
30
28
|
@user_class = klass
|
|
31
29
|
else
|
data/lib/mangadex/api/user.rb
CHANGED
|
@@ -7,6 +7,13 @@ module Mangadex
|
|
|
7
7
|
attr_accessor :mangadex_user_id, :session, :refresh, :session_valid_until
|
|
8
8
|
attr_reader :data
|
|
9
9
|
|
|
10
|
+
SERIALIZABLE_KEYS = %i(
|
|
11
|
+
mangadex_user_id
|
|
12
|
+
session
|
|
13
|
+
refresh
|
|
14
|
+
session_valid_until
|
|
15
|
+
)
|
|
16
|
+
|
|
10
17
|
sig { params(mangadex_user_id: String, session: T.nilable(String), refresh: T.nilable(String), data: T.untyped, session_valid_until: T.nilable(Time)).void }
|
|
11
18
|
def initialize(mangadex_user_id:, session: nil, refresh: nil, data: nil, session_valid_until: nil)
|
|
12
19
|
raise ArgumentError, 'Missing mangadex_user_id' if mangadex_user_id.to_s.empty?
|
|
@@ -18,10 +25,33 @@ module Mangadex
|
|
|
18
25
|
@data = data
|
|
19
26
|
end
|
|
20
27
|
|
|
28
|
+
# true: The tokens were successfully refreshed if the session expired
|
|
29
|
+
# false: Error: refresh token empty or could not refresh the token on the server
|
|
30
|
+
sig do
|
|
31
|
+
params(
|
|
32
|
+
block: T.nilable(
|
|
33
|
+
T.proc.params(arg0: User).returns(
|
|
34
|
+
T.untyped)
|
|
35
|
+
)
|
|
36
|
+
).returns(T::Boolean)
|
|
37
|
+
end
|
|
38
|
+
def refresh_session(&block)
|
|
39
|
+
return true unless session_expired?
|
|
40
|
+
|
|
41
|
+
refresh_session!(&block)
|
|
42
|
+
end
|
|
43
|
+
|
|
21
44
|
# true: The tokens were successfully refreshed
|
|
22
45
|
# false: Error: refresh token empty or could not refresh the token on the server
|
|
23
|
-
sig
|
|
24
|
-
|
|
46
|
+
sig do
|
|
47
|
+
params(
|
|
48
|
+
block: T.nilable(
|
|
49
|
+
T.proc.params(arg0: User).returns(
|
|
50
|
+
T.untyped)
|
|
51
|
+
)
|
|
52
|
+
).returns(T::Boolean)
|
|
53
|
+
end
|
|
54
|
+
def refresh_session!(&block)
|
|
25
55
|
return false if refresh.nil?
|
|
26
56
|
|
|
27
57
|
response = Mangadex::Internal::Request.post('/auth/refresh', payload: { token: refresh })
|
|
@@ -31,15 +61,11 @@ module Mangadex
|
|
|
31
61
|
@refresh = response.dig('token', 'refresh')
|
|
32
62
|
@session = response.dig('token', 'session')
|
|
33
63
|
|
|
34
|
-
|
|
35
|
-
|
|
64
|
+
if block_given?
|
|
65
|
+
yield(self)
|
|
66
|
+
end
|
|
36
67
|
|
|
37
|
-
|
|
38
|
-
def with_valid_session
|
|
39
|
-
session_expired? && refresh!
|
|
40
|
-
self
|
|
41
|
-
ensure
|
|
42
|
-
self
|
|
68
|
+
true
|
|
43
69
|
end
|
|
44
70
|
|
|
45
71
|
sig { returns(T::Boolean) }
|
|
@@ -65,6 +91,18 @@ module Mangadex
|
|
|
65
91
|
!mangadex_user_id.nil? && !mangadex_user_id.strip.empty?
|
|
66
92
|
end
|
|
67
93
|
|
|
94
|
+
sig { params(except: T.any(Symbol, T::Array[Symbol])).returns(Hash) }
|
|
95
|
+
def to_h(except: [])
|
|
96
|
+
except = Array(except).map(&:to_sym)
|
|
97
|
+
keys = SERIALIZABLE_KEYS.reject do |key|
|
|
98
|
+
except.include?(key)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
keys.map do |key|
|
|
102
|
+
[key, send(key)]
|
|
103
|
+
end.to_h
|
|
104
|
+
end
|
|
105
|
+
|
|
68
106
|
sig { params(mangadex_user_id: T.nilable(String)).returns(T.nilable(User)) }
|
|
69
107
|
def self.from_storage(mangadex_user_id)
|
|
70
108
|
return if mangadex_user_id.nil?
|
|
@@ -81,7 +119,7 @@ module Mangadex
|
|
|
81
119
|
session: session,
|
|
82
120
|
refresh: refresh,
|
|
83
121
|
session_valid_until: session_valid_until,
|
|
84
|
-
)
|
|
122
|
+
)
|
|
85
123
|
else
|
|
86
124
|
nil
|
|
87
125
|
end
|
|
@@ -18,7 +18,7 @@ module Mangadex
|
|
|
18
18
|
if version != Mangadex::Version::STRING
|
|
19
19
|
warn(
|
|
20
20
|
"[Warning] This gem is compatible with #{Mangadex::Version::STRING} but it looks like Mangadex is at #{version}",
|
|
21
|
-
"[Warning] Check out #{Mangadex
|
|
21
|
+
"[Warning] Check out #{Mangadex.configuration.mangadex_url} for more information.",
|
|
22
22
|
)
|
|
23
23
|
end
|
|
24
24
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
|
|
3
|
+
module Mangadex
|
|
4
|
+
class AtHome
|
|
5
|
+
extend T::Sig
|
|
6
|
+
|
|
7
|
+
sig { params(chapter_id: String).returns(T::Api::GenericResponse) }
|
|
8
|
+
def self.server(chapter_id)
|
|
9
|
+
Mangadex::Internal::Request.get(
|
|
10
|
+
"/at-home/server/#{chapter_id}",
|
|
11
|
+
)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
sig { params(chapter_id: String, data_saver: T::Boolean).returns(T.nilable(T::Array[String])) }
|
|
15
|
+
def self.page_urls(chapter_id, data_saver: true)
|
|
16
|
+
response = self.server(chapter_id)
|
|
17
|
+
return if response.is_a?(Mangadex::Api::Response)
|
|
18
|
+
|
|
19
|
+
base_url = response['baseUrl']
|
|
20
|
+
chapter_data = response['chapter']
|
|
21
|
+
hash = chapter_data['hash']
|
|
22
|
+
source = data_saver ? chapter_data['dataSaver'] : chapter_data['data']
|
|
23
|
+
data_source = data_saver ? 'data-saver' : 'data'
|
|
24
|
+
|
|
25
|
+
source.map do |filename|
|
|
26
|
+
[base_url, data_source, hash, filename].join('/')
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
data/lib/mangadex/auth.rb
CHANGED
|
@@ -3,8 +3,17 @@ module Mangadex
|
|
|
3
3
|
class Auth
|
|
4
4
|
extend T::Sig
|
|
5
5
|
|
|
6
|
-
sig
|
|
7
|
-
|
|
6
|
+
sig do
|
|
7
|
+
params(
|
|
8
|
+
username: T.nilable(String),
|
|
9
|
+
email: T.nilable(String),
|
|
10
|
+
password: String,
|
|
11
|
+
block: T.nilable(T.proc.returns(T.untyped))
|
|
12
|
+
).returns(
|
|
13
|
+
T.nilable(T.untyped),
|
|
14
|
+
)
|
|
15
|
+
end
|
|
16
|
+
def self.login(username: nil, email: nil, password: nil, &block)
|
|
8
17
|
args = { password: password }
|
|
9
18
|
args.merge!(email: email) if email
|
|
10
19
|
args.merge!(username: username) if username
|
|
@@ -18,6 +27,8 @@ module Mangadex
|
|
|
18
27
|
}),
|
|
19
28
|
)
|
|
20
29
|
|
|
30
|
+
session_valid_until = Time.now + (15 * 60)
|
|
31
|
+
|
|
21
32
|
session = response.dig('token', 'session')
|
|
22
33
|
refresh = response.dig('token', 'refresh')
|
|
23
34
|
|
|
@@ -28,12 +39,18 @@ module Mangadex
|
|
|
28
39
|
session: session,
|
|
29
40
|
refresh: refresh,
|
|
30
41
|
data: mangadex_user.data,
|
|
42
|
+
session_valid_until: session_valid_until,
|
|
31
43
|
)
|
|
32
|
-
|
|
44
|
+
|
|
45
|
+
user.persist
|
|
46
|
+
user
|
|
33
47
|
|
|
34
48
|
Mangadex.context.user = user
|
|
35
49
|
|
|
36
|
-
|
|
50
|
+
if block_given?
|
|
51
|
+
return yield(user)
|
|
52
|
+
end
|
|
53
|
+
|
|
37
54
|
user
|
|
38
55
|
rescue Errors::UnauthorizedError => error
|
|
39
56
|
raise Errors::AuthenticationError.new(error.response)
|
|
@@ -67,7 +84,7 @@ module Mangadex
|
|
|
67
84
|
|
|
68
85
|
sig { returns(T::Boolean) }
|
|
69
86
|
def self.refresh_token
|
|
70
|
-
!(Mangadex.context.user&.
|
|
87
|
+
!(Mangadex.context.user&.refresh_session!).nil?
|
|
71
88
|
end
|
|
72
89
|
|
|
73
90
|
private
|
data/lib/mangadex/chapter.rb
CHANGED
|
@@ -88,11 +88,9 @@ module Mangadex
|
|
|
88
88
|
locale&.english_name
|
|
89
89
|
end
|
|
90
90
|
|
|
91
|
-
sig { returns(T.nilable(String)) }
|
|
92
|
-
def
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
"https://uploads.mangadex.org/data-saver/#{attributes.hash}/#{data_saver.first}"
|
|
91
|
+
sig { params(data_saver: T::Boolean).returns(T.nilable(T::Array[String])) }
|
|
92
|
+
def page_urls(data_saver: true)
|
|
93
|
+
Mangadex::AtHome.page_urls(id, data_saver: data_saver)
|
|
96
94
|
end
|
|
97
95
|
|
|
98
96
|
def as_json(*)
|
data/lib/mangadex/custom_list.rb
CHANGED
|
@@ -135,7 +135,8 @@ module Mangadex
|
|
|
135
135
|
|
|
136
136
|
sig { params(args: T::Api::Arguments).returns(T.nilable(Mangadex::Api::Response[Manga])) }
|
|
137
137
|
def manga_details(**args)
|
|
138
|
-
|
|
138
|
+
custom_list = Mangadex::CustomList.get(id).data
|
|
139
|
+
ids = custom_list.mangas.map(&:id)
|
|
139
140
|
ids.any? ? Mangadex::Manga.list(**args.merge(ids: ids)) : nil
|
|
140
141
|
end
|
|
141
142
|
|
|
@@ -18,7 +18,7 @@ module Mangadex
|
|
|
18
18
|
|
|
19
19
|
sig { returns(T.nilable(Mangadex::Api::User)) }
|
|
20
20
|
def user
|
|
21
|
-
@ignore_user ? nil : @user
|
|
21
|
+
@ignore_user ? nil : @user
|
|
22
22
|
rescue Mangadex::Errors::UnauthorizedError
|
|
23
23
|
warn("A user is present but not authenticated!")
|
|
24
24
|
nil
|
|
@@ -29,7 +29,7 @@ module Mangadex
|
|
|
29
29
|
@tags ||= Mangadex::Tag.list.data
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
-
sig { params(user: T.nilable(T.
|
|
32
|
+
sig { params(user: T.nilable(T.untyped), block: T.proc.returns(T.untyped)).returns(T.untyped) }
|
|
33
33
|
def with_user(user, &block)
|
|
34
34
|
temp_set_value("user", user) do
|
|
35
35
|
yield
|
|
@@ -64,11 +64,12 @@ module Mangadex
|
|
|
64
64
|
session: user[:session],
|
|
65
65
|
refresh: user[:refresh],
|
|
66
66
|
)
|
|
67
|
-
elsif user_object?(user)
|
|
67
|
+
elsif Mangadex::Internal::Context.user_object?(user)
|
|
68
68
|
@user = Mangadex::Api::User.new(
|
|
69
69
|
mangadex_user_id: user.mangadex_user_id.to_s,
|
|
70
70
|
session: user.session,
|
|
71
71
|
refresh: user.refresh,
|
|
72
|
+
session_valid_until: user.session_valid_until,
|
|
72
73
|
data: user,
|
|
73
74
|
)
|
|
74
75
|
elsif user.nil?
|
|
@@ -113,18 +114,18 @@ module Mangadex
|
|
|
113
114
|
end
|
|
114
115
|
end
|
|
115
116
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
def user_object?(user)
|
|
117
|
+
def self.user_object?(user)
|
|
119
118
|
return false if user.nil?
|
|
120
119
|
|
|
121
|
-
missing_methods = [:session, :refresh, :mangadex_user_id] - user.methods
|
|
120
|
+
missing_methods = [:session, :refresh, :mangadex_user_id, :save] - user.methods
|
|
122
121
|
return true if missing_methods.empty?
|
|
123
122
|
|
|
124
123
|
warn("Potential user object #{user} is missing #{missing_methods}")
|
|
125
124
|
false
|
|
126
125
|
end
|
|
127
126
|
|
|
127
|
+
private
|
|
128
|
+
|
|
128
129
|
def temp_set_value(name, value, &block)
|
|
129
130
|
setter_method_name = "#{name}="
|
|
130
131
|
|
|
@@ -108,10 +108,11 @@ module Mangadex
|
|
|
108
108
|
offset: { accepts: Integer },
|
|
109
109
|
ids: { accepts: [String] },
|
|
110
110
|
title: { accepts: String },
|
|
111
|
+
manga: { accepts: String },
|
|
111
112
|
groups: { accepts: [String] },
|
|
112
113
|
uploader: { accepts: [String], converts: converts(:to_a) },
|
|
113
114
|
chapter: { accepts: [String], converts: converts(:to_a) },
|
|
114
|
-
translated_language: { accepts: String },
|
|
115
|
+
translated_language: { accepts: [String], converts: converts(:to_a) },
|
|
115
116
|
original_language: { accepts: [String] },
|
|
116
117
|
excluded_original_language: { accepts: [String] },
|
|
117
118
|
content_rating: { accepts: %w(safe suggestive erotica pornographic), converts: converts(:to_a) },
|
|
@@ -7,7 +7,6 @@ require 'active_support/core_ext/hash/keys'
|
|
|
7
7
|
module Mangadex
|
|
8
8
|
module Internal
|
|
9
9
|
class Request
|
|
10
|
-
BASE_URI = 'https://api.mangadex.org'
|
|
11
10
|
ALLOWED_METHODS = %i(get post put delete).freeze
|
|
12
11
|
|
|
13
12
|
attr_accessor :path, :headers, :payload, :method, :raw
|
|
@@ -51,7 +50,7 @@ module Mangadex
|
|
|
51
50
|
end
|
|
52
51
|
|
|
53
52
|
def run!(raw: false, auth: false)
|
|
54
|
-
payload_details = request_payload ? "Payload: #{
|
|
53
|
+
payload_details = request_payload ? "Payload: #{sensitive_request_payload}" : "{no-payload}"
|
|
55
54
|
puts("[#{self.class.name}] #{method.to_s.upcase} #{request_url} #{payload_details}")
|
|
56
55
|
|
|
57
56
|
raise Mangadex::Errors::UserNotLoggedIn.new if auth && Mangadex.context.user.nil?
|
|
@@ -100,7 +99,7 @@ module Mangadex
|
|
|
100
99
|
|
|
101
100
|
def request_url
|
|
102
101
|
request_path = path.start_with?('/') ? path : "/#{path}"
|
|
103
|
-
"#{
|
|
102
|
+
"#{Mangadex.configuration.mangadex_url}#{request_path}"
|
|
104
103
|
end
|
|
105
104
|
|
|
106
105
|
def request_payload
|
|
@@ -108,12 +107,20 @@ module Mangadex
|
|
|
108
107
|
|
|
109
108
|
JSON.generate(payload)
|
|
110
109
|
end
|
|
110
|
+
|
|
111
|
+
def sensitive_request_payload(sensitive_fields: %w(password token))
|
|
112
|
+
payload = JSON.parse(request_payload)
|
|
113
|
+
sensitive_fields.map(&:to_s).each do |field|
|
|
114
|
+
payload[field] = '[REDACTED]' if payload.key?(field)
|
|
115
|
+
end
|
|
116
|
+
JSON.generate(payload)
|
|
117
|
+
end
|
|
111
118
|
|
|
112
119
|
def request_headers
|
|
113
120
|
return headers if Mangadex.context.user.nil?
|
|
114
121
|
|
|
115
122
|
headers.merge({
|
|
116
|
-
Authorization: Mangadex.context.user.
|
|
123
|
+
Authorization: Mangadex.context.user.session,
|
|
117
124
|
})
|
|
118
125
|
end
|
|
119
126
|
|
|
@@ -31,7 +31,7 @@ module Mangadex
|
|
|
31
31
|
self.name.split('::').last.underscore
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
-
def from_data(data, related_type: nil)
|
|
34
|
+
def from_data(data, related_type: nil, source_obj: nil)
|
|
35
35
|
base_class_name = self.name.gsub('::', '_')
|
|
36
36
|
klass_name = self.name
|
|
37
37
|
target_attributes_class_name = "#{base_class_name}_Attributes"
|
|
@@ -57,7 +57,7 @@ module Mangadex
|
|
|
57
57
|
data = data.with_indifferent_access
|
|
58
58
|
|
|
59
59
|
relationships = data['relationships']&.map do |relationship_data|
|
|
60
|
-
Relationship.from_data(relationship_data)
|
|
60
|
+
Relationship.from_data(relationship_data, MangadexObject.new(**data))
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
attributes = klass.new(**Hash(data['attributes']))
|
|
@@ -69,8 +69,11 @@ module Mangadex
|
|
|
69
69
|
related_type: related_type,
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
relationships = [source_obj].compact unless relationships.present?
|
|
72
73
|
initialize_hash.merge!({relationships: relationships}) if relationships.present?
|
|
73
74
|
|
|
75
|
+
# binding.pry
|
|
76
|
+
|
|
74
77
|
new(**initialize_hash)
|
|
75
78
|
end
|
|
76
79
|
end
|
data/lib/mangadex/manga.rb
CHANGED
|
@@ -70,12 +70,13 @@ module Mangadex
|
|
|
70
70
|
|
|
71
71
|
sig { params(id: String, args: T::Api::Arguments).returns(T::Api::MangaResponse) }
|
|
72
72
|
def self.view(id, **args)
|
|
73
|
+
to_a = Mangadex::Internal::Definition.converts(:to_a)
|
|
73
74
|
Mangadex::Internal::Definition.must(id)
|
|
74
75
|
|
|
75
76
|
Mangadex::Internal::Request.get(
|
|
76
77
|
'/manga/%{id}' % {id: id},
|
|
77
78
|
Mangadex::Internal::Definition.validate(args, {
|
|
78
|
-
includes: { accepts: Array },
|
|
79
|
+
includes: { accepts: Array, converts: to_a },
|
|
79
80
|
})
|
|
80
81
|
)
|
|
81
82
|
end
|
|
@@ -126,9 +127,9 @@ module Mangadex
|
|
|
126
127
|
)
|
|
127
128
|
end
|
|
128
129
|
|
|
129
|
-
sig { params(status: String).returns(T::Api::GenericResponse) }
|
|
130
|
-
def self.all_reading_status(status)
|
|
131
|
-
args = { status: status }
|
|
130
|
+
sig { params(status: T.nilable(String)).returns(T::Api::GenericResponse) }
|
|
131
|
+
def self.all_reading_status(status = nil)
|
|
132
|
+
args = { status: status } if status.present?
|
|
132
133
|
|
|
133
134
|
Mangadex::Internal::Request.get(
|
|
134
135
|
'/manga/status',
|
|
@@ -165,6 +166,15 @@ module Mangadex
|
|
|
165
166
|
)
|
|
166
167
|
end
|
|
167
168
|
|
|
169
|
+
sig { params(id: T.any(T::Array[String], String), grouped: T::Boolean).returns(T::Api::GenericResponse) }
|
|
170
|
+
def self.read_markers(id, grouped: false)
|
|
171
|
+
Mangadex::Internal::Request.get(
|
|
172
|
+
'/manga/read',
|
|
173
|
+
{ ids: Array(id), grouped: grouped },
|
|
174
|
+
auth: true,
|
|
175
|
+
)
|
|
176
|
+
end
|
|
177
|
+
|
|
168
178
|
# Untested API endpoints
|
|
169
179
|
sig { params(id: String, args: T::Api::Arguments).returns(T::Api::MangaResponse) }
|
|
170
180
|
def self.update(id, **args)
|
|
@@ -202,5 +212,11 @@ module Mangadex
|
|
|
202
212
|
|
|
203
213
|
ContentRating.new(attributes.content_rating)
|
|
204
214
|
end
|
|
215
|
+
|
|
216
|
+
sig { params(args: T::Api::Arguments).returns(Mangadex::Api::Response[Chapter]) }
|
|
217
|
+
def chapters(**args)
|
|
218
|
+
chapter_args = args.merge({manga: id})
|
|
219
|
+
Chapter.list(**chapter_args)
|
|
220
|
+
end
|
|
205
221
|
end
|
|
206
222
|
end
|
|
@@ -22,19 +22,24 @@ module Mangadex
|
|
|
22
22
|
).freeze
|
|
23
23
|
|
|
24
24
|
class << self
|
|
25
|
-
|
|
25
|
+
# data: Relationship data
|
|
26
|
+
# source_obj: The object to witch the object belongs to
|
|
27
|
+
def from_data(data, source_obj = nil)
|
|
26
28
|
data = data.with_indifferent_access
|
|
27
29
|
klass = class_for_relationship_type(data['type'])
|
|
28
30
|
|
|
29
31
|
if klass && data['attributes']&.any?
|
|
30
|
-
return klass.from_data(data, related_type: data['related'])
|
|
32
|
+
return klass.from_data(data, related_type: data['related'], source_obj: source_obj)
|
|
31
33
|
end
|
|
32
34
|
|
|
35
|
+
relationships = [source_obj] if source_obj
|
|
36
|
+
|
|
33
37
|
new(
|
|
34
38
|
id: data['id'],
|
|
35
39
|
type: data['type'],
|
|
36
40
|
attributes: OpenStruct.new(data['attributes']),
|
|
37
41
|
related: data['related'],
|
|
42
|
+
relationships: relationships,
|
|
38
43
|
)
|
|
39
44
|
end
|
|
40
45
|
|
data/lib/mangadex/types.rb
CHANGED
data/lib/mangadex/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mangadex
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 5.4.
|
|
4
|
+
version: 5.4.11
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Akinyele Cafe-Febrissy
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2022-01-
|
|
11
|
+
date: 2022-01-22 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: psych
|
|
@@ -184,6 +184,7 @@ files:
|
|
|
184
184
|
- lib/mangadex/api/user.rb
|
|
185
185
|
- lib/mangadex/api/version_checker.rb
|
|
186
186
|
- lib/mangadex/artist.rb
|
|
187
|
+
- lib/mangadex/at_home.rb
|
|
187
188
|
- lib/mangadex/auth.rb
|
|
188
189
|
- lib/mangadex/author.rb
|
|
189
190
|
- lib/mangadex/chapter.rb
|
|
@@ -261,7 +262,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
261
262
|
- !ruby/object:Gem::Version
|
|
262
263
|
version: '0'
|
|
263
264
|
requirements: []
|
|
264
|
-
rubygems_version: 3.2.
|
|
265
|
+
rubygems_version: 3.2.15
|
|
265
266
|
signing_key:
|
|
266
267
|
specification_version: 4
|
|
267
268
|
summary: Your next favourite Ruby gem for interacting with Mangadex.org
|