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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 369eb625aa055dce9f77b637d1c6a6e8251ecdffe14dc2f232568ecbb8495237
4
- data.tar.gz: 439806ffe90c5804bfdc319e4f82789e0e77e66d095e9daf1d326e5f966fb9bd
3
+ metadata.gz: 3974d29ba88445303cbe19210402f6b3acfb1ad8aa9ca9ee5f662025f04a9ab0
4
+ data.tar.gz: b50c7ba9e5ff1666565241497a974c35fef74ba957037d476d0a169d317b4ad9
5
5
  SHA512:
6
- metadata.gz: 25dd6f4b7c9c46a4c841fe6bf3df95dddf8d89aff993d477a53593846b7412c0bd48387de2b817f13310c1f54d0612084ec8e6f2416385f3cf1cbb035d93f11a
7
- data.tar.gz: 79995090f88bf2fddca1cc93688ecb2774aba33c2b01fb56c58193ada886884818eab099ecd77e0d56cd229860fae8ec9ccf5c5adf10eaa76293f2c91064ca38
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 UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
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 UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
13
+ $ gem install jnc_api
16
14
 
17
15
  ## Usage
18
16
 
@@ -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
@@ -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
- user_id = JSON.parse(api.me)["legacyId"]
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
- part.image = item["image"]
79
- part.date_published = item["date_published"]
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
- part.save!
71
+ me = api.me
72
+ puts me
73
+ user_id = JSON.parse(me)["legacyId"]
82
74
 
83
- # skip items that are not novels e.g. manga
84
- # we currently have no way to decode the manga parts
85
- if p["type"] != "NOVEL"
86
- puts "Skipping part #{part.slug} (not NOVEL type unsupported)"
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
- # skip items if the part is already up-to-date
91
- if part.data && p["updated"].to_datetime == part.updated.to_datetime
92
- puts "Skipping part #{part.slug} (feed item updated)"
93
- next
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
@@ -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>
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JncApi
4
- VERSION = "0.1.0"
4
+ VERSION = "0.3.0"
5
5
  end
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 = {"Content-Type" => "application/json"}
63
- @bearer = {"Authorization" => "Bearer #{@token}"}
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.1.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-06-19 00:00:00.000000000 Z
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.6
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.