jnc_api 0.1.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +2 -4
- data/examples/feed/models.rb +18 -0
- data/examples/feed/tracker.rb +86 -36
- data/examples/feed/viewer.rb +12 -2
- data/lib/jnc_api/version.rb +1 -1
- data/lib/jnc_api.rb +46 -4
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3974d29ba88445303cbe19210402f6b3acfb1ad8aa9ca9ee5f662025f04a9ab0
|
4
|
+
data.tar.gz: b50c7ba9e5ff1666565241497a974c35fef74ba957037d476d0a169d317b4ad9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f5849dc5efaf3877966a24d1d399e41b1aaacece2969a9e1dcd05924c38712b1906f24d249b3a11144fb888f3edfaea4de1f6e96180463aab5cf64555a875901
|
7
|
+
data.tar.gz: 0ed4905c22b8ae2ea72d125bab66df8ff544879ecb6f8196afa853457aab45bf5246220c423066566422963d6fbde0bca0fb0775d67f496f757d36f5c25b423a
|
data/README.md
CHANGED
@@ -4,15 +4,13 @@ A thin wrapper for the j-novel.club API
|
|
4
4
|
|
5
5
|
## Installation
|
6
6
|
|
7
|
-
TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
|
8
|
-
|
9
7
|
Install the gem and add to the application's Gemfile by executing:
|
10
8
|
|
11
|
-
$ bundle add
|
9
|
+
$ bundle add jnc_api
|
12
10
|
|
13
11
|
If bundler is not being used to manage dependencies, install the gem by executing:
|
14
12
|
|
15
|
-
$ gem install
|
13
|
+
$ gem install jnc_api
|
16
14
|
|
17
15
|
## Usage
|
18
16
|
|
data/examples/feed/models.rb
CHANGED
@@ -46,8 +46,26 @@ ActiveRecord::Schema.define do
|
|
46
46
|
t.timestamps
|
47
47
|
end
|
48
48
|
end
|
49
|
+
|
50
|
+
unless table_exists?(:manga_parts)
|
51
|
+
create_table :manga_parts, force: :cascade do |t|
|
52
|
+
t.integer :part_id, null: false
|
53
|
+
t.text :uuid
|
54
|
+
t.text :ngtoken
|
55
|
+
t.text :kgtoken
|
56
|
+
t.text :webpub
|
57
|
+
t.timestamps
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
49
61
|
end
|
50
62
|
|
51
63
|
class Part < ActiveRecord::Base
|
64
|
+
has_many :manga_parts
|
52
65
|
serialize :crypt_data, coder: JSON
|
53
66
|
end
|
67
|
+
|
68
|
+
class MangaPart < ActiveRecord::Base
|
69
|
+
belongs_to :part
|
70
|
+
serialize :webpub, coder: JSON
|
71
|
+
end
|
data/examples/feed/tracker.rb
CHANGED
@@ -23,6 +23,7 @@ gemfile do
|
|
23
23
|
gem "activerecord", "~> 7.1", require: "active_record"
|
24
24
|
gem "sqlite3", "~> 1.4", require: "sqlite3"
|
25
25
|
gem "addressable", "~> 2.8", require: "addressable"
|
26
|
+
gem "optparse", require: "optparse"
|
26
27
|
|
27
28
|
gem "jnc_api", path: "../..", require: "jnc_api"
|
28
29
|
end
|
@@ -46,52 +47,40 @@ else
|
|
46
47
|
|
47
48
|
api = JncApi::Api.login(login, password)
|
48
49
|
puts "Your JNC_TOKEN is #{api.token}"
|
50
|
+
puts "Press [Enter] to continue ..."
|
51
|
+
gets
|
49
52
|
end
|
50
53
|
|
51
|
-
|
52
|
-
feed = JSON.parse(api.feed(user_id))
|
53
|
-
|
54
|
-
feed["items"].each do |item|
|
55
|
-
sleep 3
|
56
|
-
|
57
|
-
url = Addressable::URI.parse(item["id"])
|
58
|
-
slug = url.path.split("/").last
|
59
|
-
|
60
|
-
# persist/update part data
|
61
|
-
part = Part.find_or_create_by(slug: slug)
|
62
|
-
|
63
|
-
# skip items if the feed item is already seen
|
64
|
-
if part.date_published &&
|
65
|
-
item["date_published"].to_datetime == part.date_published.to_datetime
|
66
|
-
puts "Skipping part #{part.slug} (feed item already seen)"
|
67
|
-
next
|
68
|
-
end
|
69
|
-
|
70
|
-
p = JSON.parse(api.parts_data(slug))
|
71
|
-
|
54
|
+
def assign_part_from_json(part, p)
|
72
55
|
part.series_type = p["type"]
|
73
56
|
part.legacy_id = p["partLegacyId"]
|
74
57
|
part.title = p["partTitle"]
|
75
|
-
part.updated = p["updated"]
|
76
58
|
part.clear_data = p["clearData"]
|
59
|
+
end
|
77
60
|
|
78
|
-
|
79
|
-
|
61
|
+
options = {}
|
62
|
+
OptionParser
|
63
|
+
.new do |opts|
|
64
|
+
opts.on(
|
65
|
+
"--slug=SLUG",
|
66
|
+
"Only download this part as identified by the slug"
|
67
|
+
) { |s| options[:slug] = s }
|
68
|
+
end
|
69
|
+
.parse!
|
80
70
|
|
81
|
-
|
71
|
+
me = api.me
|
72
|
+
puts me
|
73
|
+
user_id = JSON.parse(me)["legacyId"]
|
82
74
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
next
|
88
|
-
end
|
75
|
+
if options[:slug]
|
76
|
+
opt_slug = options[:slug]
|
77
|
+
p = JSON.parse(api.parts_data(opt_slug))
|
78
|
+
part = Part.find_or_create_by(slug: opt_slug)
|
89
79
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
end
|
80
|
+
assign_part_from_json(part, p)
|
81
|
+
part.updated = p["updated"]
|
82
|
+
|
83
|
+
part.save!
|
95
84
|
|
96
85
|
e = api.embed(p["partLegacyId"])
|
97
86
|
|
@@ -100,4 +89,65 @@ feed["items"].each do |item|
|
|
100
89
|
part.save!
|
101
90
|
|
102
91
|
puts "Updated part #{part.slug}"
|
92
|
+
else
|
93
|
+
feed = JSON.parse(api.feed(user_id))
|
94
|
+
|
95
|
+
feed["items"].each do |item|
|
96
|
+
url = Addressable::URI.parse(item["id"])
|
97
|
+
slug = url.path.split("/").last
|
98
|
+
|
99
|
+
# persist/update part data
|
100
|
+
part = Part.find_or_create_by(slug: slug)
|
101
|
+
|
102
|
+
# skip items if the feed item is already seen
|
103
|
+
if part.date_published &&
|
104
|
+
item["date_published"].to_datetime == part.date_published.to_datetime
|
105
|
+
puts "Skipping part #{part.slug} (feed item already seen)"
|
106
|
+
|
107
|
+
next
|
108
|
+
end
|
109
|
+
|
110
|
+
puts "sleeping for 5s"
|
111
|
+
sleep 5
|
112
|
+
|
113
|
+
p = JSON.parse(api.parts_data(slug))
|
114
|
+
assign_part_from_json(part, p)
|
115
|
+
|
116
|
+
# skip items if the part is already up-to-date
|
117
|
+
if part.data && p["updated"].to_datetime == part.updated.to_datetime
|
118
|
+
puts "Skipping part #{part.slug} (feed item updated)"
|
119
|
+
|
120
|
+
next
|
121
|
+
end
|
122
|
+
|
123
|
+
# note: we currently have no way to descramble the manga parts
|
124
|
+
if p["type"] == "MANGA"
|
125
|
+
puts "part #{part.slug} is manga, downloading webpub"
|
126
|
+
tokens = JSON.parse(api.manga_parts(part.legacy_id))
|
127
|
+
manga_part = part.manga_parts.find_or_create_by(uuid: tokens["uuid"])
|
128
|
+
manga_part.uuid = tokens["uuid"]
|
129
|
+
manga_part.ngtoken = tokens["ngtoken"]
|
130
|
+
manga_part.kgtoken = tokens["kgtoken"]
|
131
|
+
|
132
|
+
puts tokens
|
133
|
+
puts "sleeping for 30s"
|
134
|
+
sleep 5
|
135
|
+
|
136
|
+
manga_part.webpub = api.webpub(tokens["uuid"], tokens["ngtoken"])
|
137
|
+
manga_part.save!
|
138
|
+
else
|
139
|
+
e = api.embed(p["partLegacyId"])
|
140
|
+
|
141
|
+
part.data = e
|
142
|
+
part.crypt_data = p["cryptData"]
|
143
|
+
end
|
144
|
+
|
145
|
+
part.image = item["image"]
|
146
|
+
part.date_published = item["date_published"]
|
147
|
+
part.updated = p["updated"]
|
148
|
+
|
149
|
+
part.save!
|
150
|
+
|
151
|
+
puts "Updated part #{part.slug}"
|
152
|
+
end
|
103
153
|
end
|
data/examples/feed/viewer.rb
CHANGED
@@ -39,7 +39,7 @@ class App < Sinatra::Application
|
|
39
39
|
enable :inline_templates
|
40
40
|
|
41
41
|
get "/" do
|
42
|
-
@parts = Part.order("date_published DESC")
|
42
|
+
@parts = Part.order("date_published DESC").where(series_type: "NOVEL")
|
43
43
|
erb :index
|
44
44
|
end
|
45
45
|
|
@@ -65,6 +65,10 @@ __END__
|
|
65
65
|
@@ layout
|
66
66
|
<!doctype html>
|
67
67
|
<html>
|
68
|
+
<head>
|
69
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
70
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
71
|
+
<link href="https://fonts.googleapis.com/css2?family=Goudy+Bookletter+1911&display=swap" rel="stylesheet">
|
68
72
|
<style>
|
69
73
|
body { background-color: linen; }
|
70
74
|
section { max-width: 80em; }
|
@@ -72,8 +76,14 @@ __END__
|
|
72
76
|
background-color: white;
|
73
77
|
max-width: 50em;
|
74
78
|
padding: 0.25em 1em;
|
79
|
+
font-family: "Goudy Bookletter 1911", serif;
|
80
|
+
font-weight: 600;
|
81
|
+
font-style: normal;
|
82
|
+
font-size: large;
|
83
|
+
line-height: 1.5em;
|
75
84
|
}
|
76
85
|
</style>
|
86
|
+
</head>
|
77
87
|
<body><%= yield %></body>
|
78
88
|
</html>
|
79
89
|
|
@@ -95,7 +105,7 @@ __END__
|
|
95
105
|
<hr /><%= @part.data %><hr />
|
96
106
|
<% if ENV["JNC_TOKEN"] %>
|
97
107
|
<form action="/mark_as_completed/<%= @part.slug %>" method="post" id="mark-button">
|
98
|
-
<p><input type="submit" value="Mark as completed"></p>
|
108
|
+
<p><input style="font-size: 2em;" type="submit" value="Mark as completed"></p>
|
99
109
|
</div>
|
100
110
|
<% end %>
|
101
111
|
<div><p><a href="/">Back to index</a></p></div>
|
data/lib/jnc_api/version.rb
CHANGED
data/lib/jnc_api.rb
CHANGED
@@ -30,7 +30,9 @@ module JncApi
|
|
30
30
|
feed: "https://labs.j-novel.club/feed/user",
|
31
31
|
parts: "https://labs.j-novel.club/app/v1/parts",
|
32
32
|
embed: "https://labs.j-novel.club/embed/",
|
33
|
-
completion: "https://labs.j-novel.club/app/v1/me/completion"
|
33
|
+
completion: "https://labs.j-novel.club/app/v1/me/completion",
|
34
|
+
manga_parts: "https://api.j-novel.club/api/mangaParts",
|
35
|
+
webpub: "https://m11.j-novel.club/nebel/wp"
|
34
36
|
}
|
35
37
|
|
36
38
|
class Api
|
@@ -42,7 +44,7 @@ module JncApi
|
|
42
44
|
routes[:login] + "?format=json",
|
43
45
|
body:
|
44
46
|
JSON.generate(
|
45
|
-
{"login" => login, "password" => password, "slim" => true}
|
47
|
+
{ "login" => login, "password" => password, "slim" => true }
|
46
48
|
),
|
47
49
|
headers: {
|
48
50
|
"Content-Type" => "application/json"
|
@@ -59,8 +61,15 @@ module JncApi
|
|
59
61
|
def initialize(routes: V1_ROUTES, token: nil)
|
60
62
|
@routes = routes
|
61
63
|
@token = token
|
62
|
-
@default_headers = {
|
63
|
-
|
64
|
+
@default_headers = {
|
65
|
+
"User-Agent" =>
|
66
|
+
"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:127.0) Gecko/20100101 Firefox/127.0",
|
67
|
+
"Accept" =>
|
68
|
+
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
|
69
|
+
"Accept-Language" => "en-US,en;q=0.5",
|
70
|
+
"Content-Type" => "application/json"
|
71
|
+
}
|
72
|
+
@bearer = { "Authorization" => "Bearer #{@token}" }
|
64
73
|
end
|
65
74
|
|
66
75
|
def series_list(limit: 500, skip: 0)
|
@@ -157,5 +166,38 @@ module JncApi
|
|
157
166
|
raise Error.new(response.inspect)
|
158
167
|
end
|
159
168
|
end
|
169
|
+
|
170
|
+
# we can't seem to use bearer tokens for the manga endpoints
|
171
|
+
def manga_parts(part_id)
|
172
|
+
response =
|
173
|
+
HTTParty.get(
|
174
|
+
@routes[:manga_parts] + "/#{part_id}/token?access_token=#{@token}",
|
175
|
+
headers: @default_headers
|
176
|
+
)
|
177
|
+
|
178
|
+
if response.success?
|
179
|
+
response.body
|
180
|
+
else
|
181
|
+
raise Error.new(response.inspect)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
# this endpoint seems to be on a CDN without need for authorization
|
186
|
+
# likely for cheaper static hosting (as long as the locations are
|
187
|
+
# difficult to guess such as uuid)
|
188
|
+
def webpub(uuid, ngtoken)
|
189
|
+
response =
|
190
|
+
HTTParty.post(
|
191
|
+
@routes[:webpub] + "/#{uuid}",
|
192
|
+
body: ngtoken,
|
193
|
+
headers: @default_headers
|
194
|
+
)
|
195
|
+
|
196
|
+
if response.success?
|
197
|
+
response.body
|
198
|
+
else
|
199
|
+
raise Error.new(response.inspect)
|
200
|
+
end
|
201
|
+
end
|
160
202
|
end
|
161
203
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: jnc_api
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- parasquid
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-07-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: httparty
|
@@ -71,7 +71,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
71
71
|
- !ruby/object:Gem::Version
|
72
72
|
version: '0'
|
73
73
|
requirements: []
|
74
|
-
rubygems_version: 3.4.
|
74
|
+
rubygems_version: 3.4.20
|
75
75
|
signing_key:
|
76
76
|
specification_version: 4
|
77
77
|
summary: Light wrapper for the J-Novel Club API.
|