mangadex 5.3.3.3 → 5.4.11.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Dockerfile +7 -0
- data/Gemfile.lock +7 -5
- data/README.md +335 -1
- data/bin/console +14 -7
- data/docker-compose.yml +9 -0
- data/docs/authentication.md +1 -4
- data/lib/config.rb +5 -7
- data/lib/mangadex/api/response.rb +22 -0
- data/lib/mangadex/api/user.rb +49 -11
- data/lib/mangadex/api/version_checker.rb +1 -1
- data/lib/mangadex/artist.rb +2 -0
- data/lib/mangadex/at_home.rb +30 -0
- data/lib/mangadex/auth.rb +22 -5
- data/lib/mangadex/author.rb +2 -0
- data/lib/mangadex/chapter.rb +5 -27
- data/lib/mangadex/custom_list.rb +2 -1
- data/lib/mangadex/internal/context.rb +8 -7
- data/lib/mangadex/internal/definition.rb +7 -1
- data/lib/mangadex/internal/request.rb +11 -4
- data/lib/mangadex/internal/with_attributes.rb +5 -2
- data/lib/mangadex/manga.rb +23 -4
- data/lib/mangadex/relationship.rb +7 -2
- data/lib/mangadex/scanlation_group.rb +5 -1
- data/lib/mangadex/types.rb +3 -0
- data/lib/mangadex/version.rb +3 -3
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a85d0e0c75d37210331bb2d0acceed577d4bd6672d16c1b4b8636816d6fdb4ad
|
4
|
+
data.tar.gz: bd32b903f13ee416b776a7342cf63d37cc5d7e5a1a02840e70edfe5d3f497a58
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cd176cc4387606c3d032a7f6411bc77cf7bbc21889502cd3fa60fe08fe6577a3abc788d74f4b0c087f058ea325ea94d097b5aa23f9123b51a9cad0699e7206ac
|
7
|
+
data.tar.gz: d57fe45b272002f80e86e3a8bca7cd9bb97764a08c73e6544323b0fbd49d6d64f8b645af7cbaf0fb408c592a5370bd377fe42122db7a18a7efc6748a64c601e8
|
data/Dockerfile
ADDED
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/docker-compose.yml
ADDED
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
|
@@ -48,6 +48,28 @@ module Mangadex
|
|
48
48
|
errors.select { |error| error.status.to_s == status.to_s }.any?
|
49
49
|
end
|
50
50
|
|
51
|
+
def more_results?
|
52
|
+
return unless data.is_a?(Array)
|
53
|
+
|
54
|
+
total > data.count
|
55
|
+
end
|
56
|
+
|
57
|
+
def count
|
58
|
+
data.is_a?(Array) ? data.count : nil
|
59
|
+
end
|
60
|
+
|
61
|
+
def each(&block)
|
62
|
+
if data.is_a?(Array)
|
63
|
+
data.each(&block)
|
64
|
+
else
|
65
|
+
raise ArgumentError, "Expect data to be Array, but got #{data.class}"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def to_a
|
70
|
+
each.to_a
|
71
|
+
end
|
72
|
+
|
51
73
|
def as_json(*)
|
52
74
|
Hash(raw_data)
|
53
75
|
end
|
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
|
|
data/lib/mangadex/artist.rb
CHANGED
@@ -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/author.rb
CHANGED
data/lib/mangadex/chapter.rb
CHANGED
@@ -10,9 +10,7 @@ module Mangadex
|
|
10
10
|
:volume,
|
11
11
|
:chapter,
|
12
12
|
:translated_language,
|
13
|
-
:
|
14
|
-
:data,
|
15
|
-
:data_saver,
|
13
|
+
:pages,
|
16
14
|
:last_chapter,
|
17
15
|
:uploader,
|
18
16
|
:external_url,
|
@@ -77,29 +75,9 @@ module Mangadex
|
|
77
75
|
attributes&.title.presence || chapter.presence && "Chapter #{chapter}" || "N/A"
|
78
76
|
end
|
79
77
|
|
80
|
-
sig { returns(T.nilable(String)) }
|
81
|
-
def
|
82
|
-
|
83
|
-
return if found_locale.nil?
|
84
|
-
|
85
|
-
ISO_639.find(found_locale)
|
86
|
-
end
|
87
|
-
|
88
|
-
sig { returns(T.nilable(String)) }
|
89
|
-
def locale_name
|
90
|
-
locale&.english_name
|
91
|
-
end
|
92
|
-
|
93
|
-
sig { returns(Integer) }
|
94
|
-
def page_count
|
95
|
-
Array(data).count
|
96
|
-
end
|
97
|
-
|
98
|
-
sig { returns(T.nilable(String)) }
|
99
|
-
def preview_image_url
|
100
|
-
return if data_saver.empty?
|
101
|
-
|
102
|
-
"https://uploads.mangadex.org/data-saver/#{attributes.hash}/#{data_saver.first}"
|
78
|
+
sig { params(data_saver: T::Boolean).returns(T.nilable(T::Array[String])) }
|
79
|
+
def page_urls(data_saver: true)
|
80
|
+
Mangadex::AtHome.page_urls(id, data_saver: data_saver)
|
103
81
|
end
|
104
82
|
|
105
83
|
def as_json(*)
|
@@ -110,7 +88,7 @@ module Mangadex
|
|
110
88
|
end
|
111
89
|
|
112
90
|
def self.attributes_to_inspect
|
113
|
-
[:id, :type, :title, :volume, :chapter, :
|
91
|
+
[:id, :type, :title, :volume, :chapter, :pages, :publish_at]
|
114
92
|
end
|
115
93
|
end
|
116
94
|
end
|
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
|
|
@@ -106,7 +106,13 @@ module Mangadex
|
|
106
106
|
{
|
107
107
|
limit: { accepts: Integer },
|
108
108
|
offset: { accepts: Integer },
|
109
|
-
|
109
|
+
ids: { accepts: [String] },
|
110
|
+
title: { accepts: String },
|
111
|
+
manga: { accepts: String },
|
112
|
+
groups: { accepts: [String] },
|
113
|
+
uploader: { accepts: [String], converts: converts(:to_a) },
|
114
|
+
chapter: { accepts: [String], converts: converts(:to_a) },
|
115
|
+
translated_language: { accepts: [String], converts: converts(:to_a) },
|
110
116
|
original_language: { accepts: [String] },
|
111
117
|
excluded_original_language: { accepts: [String] },
|
112
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
@@ -17,6 +17,7 @@ module Mangadex
|
|
17
17
|
:year,
|
18
18
|
:content_rating,
|
19
19
|
:tags,
|
20
|
+
:state,
|
20
21
|
:version,
|
21
22
|
:created_at,
|
22
23
|
:updated_at
|
@@ -49,6 +50,8 @@ module Mangadex
|
|
49
50
|
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
51
|
order: { accepts: Hash },
|
51
52
|
includes: { accepts: Array, converts: to_a },
|
53
|
+
has_available_chapters: { accepts: ['0', '1', 'true', 'false'] },
|
54
|
+
group: { accepts: String },
|
52
55
|
}),
|
53
56
|
content_rating: true,
|
54
57
|
)
|
@@ -67,12 +70,13 @@ module Mangadex
|
|
67
70
|
|
68
71
|
sig { params(id: String, args: T::Api::Arguments).returns(T::Api::MangaResponse) }
|
69
72
|
def self.view(id, **args)
|
73
|
+
to_a = Mangadex::Internal::Definition.converts(:to_a)
|
70
74
|
Mangadex::Internal::Definition.must(id)
|
71
75
|
|
72
76
|
Mangadex::Internal::Request.get(
|
73
77
|
'/manga/%{id}' % {id: id},
|
74
78
|
Mangadex::Internal::Definition.validate(args, {
|
75
|
-
includes: { accepts: Array },
|
79
|
+
includes: { accepts: Array, converts: to_a },
|
76
80
|
})
|
77
81
|
)
|
78
82
|
end
|
@@ -123,9 +127,9 @@ module Mangadex
|
|
123
127
|
)
|
124
128
|
end
|
125
129
|
|
126
|
-
sig { params(status: String).returns(T::Api::GenericResponse) }
|
127
|
-
def self.all_reading_status(status)
|
128
|
-
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?
|
129
133
|
|
130
134
|
Mangadex::Internal::Request.get(
|
131
135
|
'/manga/status',
|
@@ -162,6 +166,15 @@ module Mangadex
|
|
162
166
|
)
|
163
167
|
end
|
164
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
|
+
|
165
178
|
# Untested API endpoints
|
166
179
|
sig { params(id: String, args: T::Api::Arguments).returns(T::Api::MangaResponse) }
|
167
180
|
def self.update(id, **args)
|
@@ -199,5 +212,11 @@ module Mangadex
|
|
199
212
|
|
200
213
|
ContentRating.new(attributes.content_rating)
|
201
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
|
202
221
|
end
|
203
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
|
|
@@ -10,10 +10,14 @@ module Mangadex
|
|
10
10
|
:discord,
|
11
11
|
:contact_email,
|
12
12
|
:description,
|
13
|
+
:twitter,
|
13
14
|
:locked,
|
14
15
|
:official,
|
15
16
|
:verified,
|
16
|
-
:
|
17
|
+
:focused_languages,
|
18
|
+
:publish_delay,
|
19
|
+
:inactive,
|
20
|
+
:manga_updates,
|
17
21
|
:version,
|
18
22
|
:created_at,
|
19
23
|
:updated_at
|
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
|
+
version: 5.4.11.1
|
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:
|
11
|
+
date: 2022-01-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: psych
|
@@ -163,6 +163,7 @@ files:
|
|
163
163
|
- ".ruby-version"
|
164
164
|
- ".travis.yml"
|
165
165
|
- CODE_OF_CONDUCT.md
|
166
|
+
- Dockerfile
|
166
167
|
- Gemfile
|
167
168
|
- Gemfile.lock
|
168
169
|
- LICENSE.txt
|
@@ -170,6 +171,7 @@ files:
|
|
170
171
|
- Rakefile
|
171
172
|
- bin/console
|
172
173
|
- bin/setup
|
174
|
+
- docker-compose.yml
|
173
175
|
- docs/authentication.md
|
174
176
|
- docs/context.md
|
175
177
|
- lib/config.rb
|
@@ -182,6 +184,7 @@ files:
|
|
182
184
|
- lib/mangadex/api/user.rb
|
183
185
|
- lib/mangadex/api/version_checker.rb
|
184
186
|
- lib/mangadex/artist.rb
|
187
|
+
- lib/mangadex/at_home.rb
|
185
188
|
- lib/mangadex/auth.rb
|
186
189
|
- lib/mangadex/author.rb
|
187
190
|
- lib/mangadex/chapter.rb
|