nicorepo 0.0.8 → 1.0.0
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/README.md +58 -28
- data/bin/nicorepo +3 -1
- data/lib/nicorepo.rb +5 -51
- data/lib/nicorepo/{cli/cli.rb → cli.rb} +36 -44
- data/lib/nicorepo/cli/{config.rb → configuration.rb} +3 -3
- data/lib/nicorepo/client.rb +64 -0
- data/lib/nicorepo/filter.rb +66 -0
- data/lib/nicorepo/page.rb +60 -0
- data/lib/nicorepo/report.rb +101 -10
- data/lib/nicorepo/request.rb +81 -0
- data/lib/nicorepo/version.rb +2 -2
- data/nicorepo.gemspec +6 -5
- metadata +43 -27
- data/lib/nicorepo/parser.rb +0 -87
- data/lib/nicorepo/reports.rb +0 -78
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bae618f5d97184adc26e5376e237e195903e70f0
|
4
|
+
data.tar.gz: 82a97cc15b7d6b3c2931d27108c22f9d0d9a5d08
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e92bf7c4e0f7916158d5cb8fc6d21ba0dc9ea3ce1d48a0a64775563eb1d0e5391cff14148d80c3c9d8606f0f9fe0ca5419d295290e79697ed93d07f7c6e2223b
|
7
|
+
data.tar.gz: a6343d1bb79878a5fe79b025db64b897fbb00e1f79dac1f5fe9f41319ce14e4f738dfd1cbbb3fd02b531ba69a3beb70bacf91b6dafdd8bf2f3b35bd706a66e02
|
data/README.md
CHANGED
@@ -1,12 +1,14 @@
|
|
1
1
|
# Nicorepo
|
2
2
|
|
3
|
-
Nicorepo is
|
3
|
+
Nicorepo is an (unofficial) API client for Nicorepo on nicovideo.jp
|
4
4
|
|
5
|
-
-
|
6
|
-
-
|
7
|
-
-
|
5
|
+
- Fetch nicorepo logs
|
6
|
+
- Filter them by topics and a period
|
7
|
+
- Provides a built-in CLI and commands
|
8
8
|
|
9
|
-
|
9
|
+
## Requirements
|
10
|
+
|
11
|
+
- ruby 2.3.0
|
10
12
|
|
11
13
|
## Installation
|
12
14
|
|
@@ -24,11 +26,27 @@ Or install it yourself as:
|
|
24
26
|
|
25
27
|
## Usage
|
26
28
|
|
27
|
-
|
29
|
+
```rb
|
30
|
+
client = Nicorepo::Client.new
|
31
|
+
|
32
|
+
client.all
|
33
|
+
# => Returns raw logs wrapped in Nicorepo::Report
|
34
|
+
|
35
|
+
client.lives(10)
|
36
|
+
# => Returns 10 live logs
|
37
|
+
|
38
|
+
client.videos(10, from: Time.now - (3600 * 24), to: Time.now - (3600 * 48))
|
39
|
+
# => Returns 10 uploaded video logs (at most) within yesterday
|
28
40
|
|
29
|
-
|
41
|
+
client.all.format
|
42
|
+
# => Returns an array of logs formatted by Nicorepo::Report::DefaultFormatter
|
43
|
+
```
|
44
|
+
|
45
|
+
### Authentication for nicovideo.jp
|
30
46
|
|
31
|
-
|
47
|
+
Nicorepo requires your nicovideo account and reads them from the `~/.netrc` file.
|
48
|
+
|
49
|
+
Add following lines to your `~/.netrc`.
|
32
50
|
|
33
51
|
```
|
34
52
|
machine nicovideo.jp
|
@@ -42,48 +60,60 @@ And set the permission if not yet.
|
|
42
60
|
$ chmod 0600 ~/.netrc
|
43
61
|
```
|
44
62
|
|
45
|
-
### Start
|
63
|
+
### Start Nicorepo CLI
|
46
64
|
|
47
|
-
|
65
|
+
```console
|
66
|
+
$ nicorepo
|
67
|
+
```
|
48
68
|
|
49
|
-
You can use following commands in interactive cli
|
69
|
+
You can use following commands in interactive cli.
|
50
70
|
|
51
71
|
command | alias | description
|
52
72
|
----------------------|-------|---------------------------------------------------------
|
53
|
-
all | a | fetch all
|
54
|
-
videos | v | fetch only video
|
55
|
-
lives | li | fetch only live
|
56
|
-
show | s | show current
|
73
|
+
all | a | fetch all logs
|
74
|
+
videos | v | fetch only video logs
|
75
|
+
lives | li | fetch only live logs
|
76
|
+
show | s | show current logs
|
57
77
|
open REPORT-NUMBER | o | open the report url specified by number in your browser
|
58
78
|
help [COMMAND] | h | Describe available commands or one specific command
|
59
79
|
login | lo | re-login if your session is expired
|
60
80
|
exit | e | exit interactive prompt
|
61
81
|
|
62
|
-
|
63
|
-
For example, if you want 20 video reports by searcing over 5 pages, the command will be,
|
82
|
+
If you want only video logs, type `video` command.
|
64
83
|
|
65
|
-
> video
|
84
|
+
> video
|
66
85
|
|
67
86
|
Or you can also use aliases.
|
68
87
|
|
69
|
-
> v
|
88
|
+
> v
|
89
|
+
|
90
|
+
Some commands have options of `-n, --request-num=N` and `-p, --limit-page=N`.
|
91
|
+
For example:
|
92
|
+
|
93
|
+
> lives -n20 -p5
|
70
94
|
|
71
|
-
|
72
|
-
|
95
|
+
It fetches 20 live logs at a most, with limitation of max 5 pages.
|
96
|
+
|
97
|
+
If you omit them the default values are used for that. (the defaults are configurable)
|
73
98
|
|
74
99
|
> v
|
75
|
-
# => `video -n10 -
|
100
|
+
# => `video -n10 -p10`
|
76
101
|
> v -n20
|
77
|
-
# => `video -n20 -
|
102
|
+
# => `video -n20 -p10`
|
78
103
|
> v -p5
|
79
|
-
# => `video -n20 -
|
104
|
+
# => `video -n20 -p10`
|
80
105
|
|
81
|
-
And also, you can use `-l, --latest`, `-h, --hours
|
106
|
+
And also, you can use `-l, --latest`, `-h, --hours` and `-d, --days` options to fetch logs in the specific period.
|
82
107
|
|
83
108
|
> all -l
|
84
|
-
> lives -h 1
|
85
109
|
|
86
|
-
|
110
|
+
Collect all logs until reach the last fetched log.
|
111
|
+
|
112
|
+
> lives -h1
|
113
|
+
|
114
|
+
Collect live logs until 1 hour ago.
|
115
|
+
|
116
|
+
#### Configuration for CLI
|
87
117
|
|
88
118
|
You can configure default `request_num` and `limit_page` by adding `~/.nicorepo.yaml` if you want.
|
89
119
|
Please refer the sample `nicorepo/.nicorepo.yaml.sample` or copy it to your home directory.
|
@@ -100,7 +130,7 @@ limit_page:
|
|
100
130
|
```
|
101
131
|
|
102
132
|
- `general`: used in all commands
|
103
|
-
- `all`, `videos`, `lives`: used in each command, has priority than `general`
|
133
|
+
- `all`, `videos`, `lives`: used in each command, has higher priority than `general`
|
104
134
|
|
105
135
|
## Contributing
|
106
136
|
|
data/bin/nicorepo
CHANGED
data/lib/nicorepo.rb
CHANGED
@@ -1,52 +1,6 @@
|
|
1
|
-
require 'mechanize'
|
2
|
-
require 'nicorepo/reports'
|
3
|
-
require 'nicorepo/parser'
|
4
|
-
|
5
|
-
class Nicorepo
|
6
|
-
class LoginError < StandardError; end
|
7
|
-
|
8
|
-
PER_PAGE = 20
|
9
|
-
LOGIN_URL = 'https://secure.nicovideo.jp/secure/login?site=niconico'
|
10
|
-
|
11
|
-
attr_reader :agent
|
12
|
-
|
13
|
-
def initialize
|
14
|
-
@agent = Mechanize.new
|
15
|
-
@agent.ssl_version = 'TLSv1'
|
16
|
-
@agent.request_headers = { 'accept-language' => 'ja-JP', 'content-language' => 'ja-JP' }
|
17
|
-
@parser = Parser.new(@agent)
|
18
|
-
@logined = false
|
19
|
-
end
|
20
|
-
|
21
|
-
def login(mail, pass)
|
22
|
-
page = @agent.post(LOGIN_URL, mail: mail, password: pass)
|
23
|
-
if page.header["x-niconico-authflag"] == '0'
|
24
|
-
raise LoginError, "Failed to login"
|
25
|
-
else
|
26
|
-
@logined = true
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
def logined?
|
31
|
-
# TODO: page.header["x-niconico-auth-flag"] をチェックする?
|
32
|
-
# 現状一度ログインしたらfalseにならない
|
33
|
-
@logined
|
34
|
-
end
|
35
|
-
|
36
|
-
def all(request_num = PER_PAGE, since: nil)
|
37
|
-
limit_page = request_num / PER_PAGE + 1
|
38
|
-
Reports.new(@parser).fetch(request_num, limit_page, since: since)
|
39
|
-
end
|
40
|
-
|
41
|
-
def videos(request_num = 3, limit_page = 5, since: nil)
|
42
|
-
VideoReports.new(@parser).fetch(request_num, limit_page, since: since)
|
43
|
-
end
|
44
|
-
|
45
|
-
def lives(request_num = 3, limit_page = 5, since: nil)
|
46
|
-
LiveReports.new(@parser).fetch(request_num, limit_page, since: since)
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
require 'nicorepo/cli/cli'
|
51
1
|
require "nicorepo/version"
|
52
|
-
|
2
|
+
require 'nicorepo/client'
|
3
|
+
require 'nicorepo/filter'
|
4
|
+
require 'nicorepo/page'
|
5
|
+
require 'nicorepo/report'
|
6
|
+
require 'nicorepo/request'
|
@@ -1,10 +1,11 @@
|
|
1
|
+
require 'nicorepo'
|
2
|
+
require 'nicorepo/cli/configuration'
|
3
|
+
|
1
4
|
require 'thor'
|
2
|
-
require 'netrc'
|
3
5
|
require 'launchy'
|
4
6
|
require 'readline'
|
5
|
-
require 'nicorepo/cli/config'
|
6
7
|
|
7
|
-
|
8
|
+
module Nicorepo
|
8
9
|
module Cli
|
9
10
|
class Runner
|
10
11
|
def self.run
|
@@ -17,32 +18,14 @@ class Nicorepo
|
|
17
18
|
end
|
18
19
|
|
19
20
|
class Interactor < Thor
|
20
|
-
class LoginAccountError < StandardError; end
|
21
|
-
class ReportExistenceError < StandardError; end
|
22
|
-
|
23
21
|
class << self
|
24
|
-
def start(*)
|
25
|
-
login unless repo.logined?
|
26
|
-
super
|
27
|
-
end
|
28
|
-
|
29
22
|
# replaces by a blank for help because Interactor dosen't require the basename
|
30
23
|
def basename
|
31
24
|
''
|
32
25
|
end
|
33
26
|
|
34
|
-
def login
|
35
|
-
mail, pass = Netrc.read["nicovideo.jp"]
|
36
|
-
raise LoginAccountError, "machine nicovideo.jp is not defined in .netrc" if mail.nil? || pass.nil?
|
37
|
-
begin
|
38
|
-
repo.login(mail, pass)
|
39
|
-
rescue
|
40
|
-
raise LoginAccountError, "invalid mail or pass: mail = #{mail}"
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
27
|
def repo
|
45
|
-
@repo ||= Nicorepo.new
|
28
|
+
@repo ||= Nicorepo::Client.new
|
46
29
|
end
|
47
30
|
|
48
31
|
def conf
|
@@ -54,9 +37,9 @@ class Nicorepo
|
|
54
37
|
end
|
55
38
|
end
|
56
39
|
|
57
|
-
desc "
|
58
|
-
def
|
59
|
-
|
40
|
+
desc "reset_session", "reset current session and try re-login at the next access. It is useful if your session is expired"
|
41
|
+
def reset_session
|
42
|
+
@repo.reset_session
|
60
43
|
end
|
61
44
|
|
62
45
|
desc "all", "fetch all reports"
|
@@ -66,7 +49,7 @@ class Nicorepo
|
|
66
49
|
option :"hours", type: :numeric, aliases: :h
|
67
50
|
def all
|
68
51
|
request_num = options[:"request-num"] || conf.request_num("all")
|
69
|
-
request_options = {
|
52
|
+
request_options = { to: parse_time_options(options) }
|
70
53
|
cache(repo.all(request_num, request_options))
|
71
54
|
show
|
72
55
|
end
|
@@ -80,8 +63,8 @@ class Nicorepo
|
|
80
63
|
def videos
|
81
64
|
request_num = options[:"request-num"] || conf.request_num("videos")
|
82
65
|
limit_page = options[:"limit-page"] || conf.limit_page("videos")
|
83
|
-
request_options = {
|
84
|
-
cache(repo.videos(request_num,
|
66
|
+
request_options = { limit_page: limit_page, to: parse_time_options(options) }
|
67
|
+
cache(repo.videos(request_num, request_options))
|
85
68
|
show
|
86
69
|
end
|
87
70
|
|
@@ -94,24 +77,29 @@ class Nicorepo
|
|
94
77
|
def lives
|
95
78
|
request_num = options[:"request-num"] || conf.request_num("lives")
|
96
79
|
limit_page = options[:"limit-page"] || conf.limit_page("lives")
|
97
|
-
request_options = {
|
98
|
-
cache(repo.lives(request_num,
|
80
|
+
request_options = { limit_page: limit_page, to: parse_time_options(options) }
|
81
|
+
cache(repo.lives(request_num, request_options))
|
99
82
|
show
|
100
83
|
end
|
101
84
|
|
102
85
|
desc "show", "show current reports"
|
103
86
|
option :more, type: :boolean, aliases: :m
|
104
87
|
def show
|
105
|
-
|
106
|
-
|
88
|
+
if cached_reports.size == 0
|
89
|
+
say "* No reports", :red
|
90
|
+
return
|
91
|
+
end
|
92
|
+
|
93
|
+
reports = options[:more] ? cached_reports : cached_reports[0, recent_tail]
|
94
|
+
reports.each.with_index(1) do |report, i|
|
107
95
|
say "--- MORE ---", :blue if i == recent_tail + 1
|
108
|
-
say "[#{i}] #{report
|
109
|
-
say "
|
96
|
+
say "[#{i}] #{report[:topic]} from: #{report[:sender]} at: #{report[:created_at]}"
|
97
|
+
say " #{report[:title]} (#{report[:url]})", :green
|
110
98
|
end
|
111
99
|
say "* last fetch time: #{cached_at}", :blue
|
112
100
|
end
|
113
101
|
|
114
|
-
desc "open REPORT-NUMBER", "open the
|
102
|
+
desc "open REPORT-NUMBER", "open the url in your browser. REPORT-NUMBER is shown in left of each report"
|
115
103
|
def open(report_number)
|
116
104
|
open_numbered_link(report_number.to_i)
|
117
105
|
end
|
@@ -132,7 +120,7 @@ class Nicorepo
|
|
132
120
|
end
|
133
121
|
|
134
122
|
def cached_reports
|
135
|
-
self.class.cache[:
|
123
|
+
self.class.cache[:report] ||= []
|
136
124
|
end
|
137
125
|
|
138
126
|
def recent_tail
|
@@ -143,13 +131,13 @@ class Nicorepo
|
|
143
131
|
self.class.cache[:cached_at]
|
144
132
|
end
|
145
133
|
|
146
|
-
def cache(
|
147
|
-
cached_reports.unshift(
|
148
|
-
self.class.cache[:recent_tail] =
|
134
|
+
def cache(new_report)
|
135
|
+
cached_reports.unshift(*new_report.format)
|
136
|
+
self.class.cache[:recent_tail] = new_report.size
|
149
137
|
self.class.cache[:cached_at] = Time.now
|
150
138
|
end
|
151
139
|
|
152
|
-
def
|
140
|
+
def parse_time_options(options)
|
153
141
|
case
|
154
142
|
when options[:latest]
|
155
143
|
cached_at
|
@@ -162,12 +150,16 @@ class Nicorepo
|
|
162
150
|
end
|
163
151
|
end
|
164
152
|
|
165
|
-
def open_numbered_link(
|
166
|
-
|
167
|
-
|
153
|
+
def open_numbered_link(num)
|
154
|
+
if num > cached_reports.size || num < 1
|
155
|
+
say "Unavailable report number", :red
|
156
|
+
return
|
157
|
+
end
|
158
|
+
|
159
|
+
url = cached_reports[num - 1][:url]
|
168
160
|
|
169
161
|
Launchy.open(url, options) do |exception|
|
170
|
-
puts "Attempted to open #{url}
|
162
|
+
puts "Attempted to open #{url} but failed because of #{exception}"
|
171
163
|
raise exception
|
172
164
|
end
|
173
165
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
require 'yaml'
|
2
2
|
|
3
|
-
|
3
|
+
module Nicorepo
|
4
4
|
module Cli
|
5
5
|
class Configuration
|
6
6
|
|
@@ -8,7 +8,7 @@ class Nicorepo
|
|
8
8
|
|
9
9
|
def initialize
|
10
10
|
params = defaults.merge(load_config)
|
11
|
-
@request_nums
|
11
|
+
@request_nums = params["request_num"]
|
12
12
|
@limit_pages = params["limit_page"]
|
13
13
|
end
|
14
14
|
|
@@ -35,7 +35,7 @@ class Nicorepo
|
|
35
35
|
"general" => 10
|
36
36
|
},
|
37
37
|
"limit_page" => {
|
38
|
-
"general" =>
|
38
|
+
"general" => 10
|
39
39
|
}
|
40
40
|
}
|
41
41
|
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'netrc'
|
2
|
+
|
3
|
+
module Nicorepo
|
4
|
+
class Client
|
5
|
+
class AccountLackError < StandardError; end
|
6
|
+
|
7
|
+
PER_PAGE = 25
|
8
|
+
MAX_PAGES_DEFAULT = 20
|
9
|
+
|
10
|
+
def session
|
11
|
+
# TODO: handle expiration
|
12
|
+
@session ||= (
|
13
|
+
mail, pass = Netrc.read["nicovideo.jp"]
|
14
|
+
raise AccountLackError, "mail and password is not defined in .netrc as a machine nicovideo.jp" if mail.nil? || pass.nil?
|
15
|
+
Request::Auth.new.login(mail, pass)
|
16
|
+
)
|
17
|
+
end
|
18
|
+
|
19
|
+
def reset_session
|
20
|
+
@session = nil
|
21
|
+
end
|
22
|
+
|
23
|
+
def all(request_num = PER_PAGE, options = {})
|
24
|
+
options = options.merge(limit_page: request_num / PER_PAGE + 1)
|
25
|
+
|
26
|
+
fetch(:all, request_num, options)
|
27
|
+
end
|
28
|
+
|
29
|
+
def videos(request_num, options = {})
|
30
|
+
fetch(:videos, request_num, options)
|
31
|
+
end
|
32
|
+
|
33
|
+
def lives(request_num, options = {})
|
34
|
+
fetch(:lives, request_num, options)
|
35
|
+
end
|
36
|
+
|
37
|
+
# === Params
|
38
|
+
#
|
39
|
+
# * limit_page - Integer
|
40
|
+
# * request_num - Integer
|
41
|
+
# * options - Hash
|
42
|
+
#
|
43
|
+
# === Options
|
44
|
+
#
|
45
|
+
# * from - Time or String
|
46
|
+
# * to - Time or String
|
47
|
+
#
|
48
|
+
# fetch reports `:from` newer `:to` older
|
49
|
+
#
|
50
|
+
def fetch(filter_type, request_num, options = {})
|
51
|
+
limit_page = options[:limit_page] || MAX_PAGES_DEFAULT
|
52
|
+
filter = Filter.new(filter_type, min_time: options[:to])
|
53
|
+
page = Page.new(session, filter, options[:from], options[:to])
|
54
|
+
|
55
|
+
# NOTE: filter は report に渡したほうがシンプルじゃない?
|
56
|
+
limit_page.times.each_with_object(Report.new(request_num)) do |_, report|
|
57
|
+
report.push(page)
|
58
|
+
break report if page.last_page? || report.reach_request_num?
|
59
|
+
|
60
|
+
page = page.next
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Nicorepo
|
2
|
+
class Filter
|
3
|
+
def initialize(type_or_topic, options = {})
|
4
|
+
@rules = [].tap do |r|
|
5
|
+
r << (
|
6
|
+
case type_or_topic
|
7
|
+
when :all
|
8
|
+
All.new
|
9
|
+
when :videos
|
10
|
+
Equal.new('nicovideo.user.video.upload')
|
11
|
+
when :lives
|
12
|
+
Match.new(/^live/)
|
13
|
+
when String
|
14
|
+
Equal.new(type_or_topic)
|
15
|
+
when Regex
|
16
|
+
Match.new(type_or_topic)
|
17
|
+
else
|
18
|
+
fail "Given type or topic #{type_or_topic} is not supported"
|
19
|
+
end
|
20
|
+
)
|
21
|
+
|
22
|
+
r << MinTime.new(options[:min_time]) if options[:min_time]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def accepts?(data)
|
27
|
+
@rules.all? { |r| r.accepts?(data) }
|
28
|
+
end
|
29
|
+
|
30
|
+
class MinTime
|
31
|
+
def initialize(min_time)
|
32
|
+
@min_time = min_time
|
33
|
+
end
|
34
|
+
|
35
|
+
def accepts?(data)
|
36
|
+
Time.parse(data['createdAt']) > @min_time
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class All
|
41
|
+
def accepts?(_)
|
42
|
+
true
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class Equal
|
47
|
+
def initialize(topic)
|
48
|
+
@topic = topic
|
49
|
+
end
|
50
|
+
|
51
|
+
def accepts?(data)
|
52
|
+
data['topic'] == @topic
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class Match
|
57
|
+
def initialize(topic_regexp)
|
58
|
+
@topic_regexp = topic_regexp
|
59
|
+
end
|
60
|
+
|
61
|
+
def accepts?(data)
|
62
|
+
data['topic'] =~ @topic_regexp
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Nicorepo
|
2
|
+
class Page
|
3
|
+
attr_reader :raw
|
4
|
+
|
5
|
+
def initialize(session, filter, from = nil, to = nil)
|
6
|
+
@session = session
|
7
|
+
@filter = filter
|
8
|
+
@cursor = Cursor.new(from).value
|
9
|
+
@min_cursor = Cursor.new(to).value
|
10
|
+
|
11
|
+
fetch
|
12
|
+
end
|
13
|
+
|
14
|
+
def next
|
15
|
+
fail 'Next page not found' if last_page?
|
16
|
+
|
17
|
+
self.class.new(@session, @filter, @next_cursor, @min_cursor)
|
18
|
+
end
|
19
|
+
|
20
|
+
def last_page?
|
21
|
+
return false unless @min_cursor && @next_cursor
|
22
|
+
|
23
|
+
@min_cursor > @next_cursor
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def fetch
|
29
|
+
json = Request::Timeline.fetch(@session, cursor: @cursor)
|
30
|
+
|
31
|
+
@next_cursor = json['meta']['minId']
|
32
|
+
@raw = json['data'].select { |data| @filter.accepts?(data) }
|
33
|
+
end
|
34
|
+
|
35
|
+
class Cursor
|
36
|
+
attr_reader :value
|
37
|
+
|
38
|
+
def initialize(time_or_cursor)
|
39
|
+
@value = cursor?(time_or_cursor) ? time_or_cursor : value_from_time(time_or_cursor)
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_s
|
43
|
+
@value
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def value_from_time(time)
|
49
|
+
return unless time
|
50
|
+
|
51
|
+
time = Time.parse(time) if time === String
|
52
|
+
(time.to_f * 1000).floor.to_s
|
53
|
+
end
|
54
|
+
|
55
|
+
def cursor?(value)
|
56
|
+
value === String && cursor.match?(/\A\d{13}\z/)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
data/lib/nicorepo/report.rb
CHANGED
@@ -1,16 +1,107 @@
|
|
1
|
-
|
1
|
+
module Nicorepo
|
2
2
|
class Report
|
3
|
-
|
3
|
+
attr_reader :pages
|
4
4
|
|
5
|
-
|
5
|
+
def initialize(request_num)
|
6
|
+
@pages = []
|
7
|
+
@request_num = request_num
|
8
|
+
end
|
9
|
+
|
10
|
+
def push(page_or_pages)
|
11
|
+
pages = (page_or_pages.is_a?(Array) ? page_or_pages : [page_or_pages]).compact
|
12
|
+
@pages.push(*pages)
|
13
|
+
self
|
14
|
+
end
|
15
|
+
|
16
|
+
def size
|
17
|
+
raw.size
|
18
|
+
end
|
19
|
+
|
20
|
+
def raw
|
21
|
+
@pages.map(&:raw).flatten[0, @request_num]
|
22
|
+
end
|
23
|
+
|
24
|
+
def format(formatter = DefaultFormatter)
|
25
|
+
formatter.process_all(raw)
|
26
|
+
end
|
27
|
+
|
28
|
+
def reach_request_num?
|
29
|
+
size >= @request_num
|
30
|
+
end
|
31
|
+
|
32
|
+
class DefaultFormatter
|
33
|
+
require 'time'
|
34
|
+
|
35
|
+
class << self
|
36
|
+
def process_all(raw)
|
37
|
+
raw.map { |h| process(h) }.compact
|
38
|
+
end
|
39
|
+
|
40
|
+
def process(h)
|
41
|
+
return unless h
|
42
|
+
|
43
|
+
title, url = infer_body(h)
|
44
|
+
{
|
45
|
+
sender: nickname(h),
|
46
|
+
topic: topic(h),
|
47
|
+
title: title,
|
48
|
+
url: url,
|
49
|
+
created_at: created_at(h)
|
50
|
+
}
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def nickname(h)
|
56
|
+
h.dig('senderNiconicoUser', 'nickname')
|
57
|
+
end
|
58
|
+
|
59
|
+
def topic(h)
|
60
|
+
case h['topic']
|
61
|
+
when 'nicovideo.user.video.upload'
|
62
|
+
'動画投稿'
|
63
|
+
when 'nicovideo.user.video.kiriban.play'
|
64
|
+
"#{h.dig('actionLog', 'viewCount')}再生達成"
|
65
|
+
when 'nicovideo.user.video.advertise'
|
66
|
+
'動画広告'
|
67
|
+
when 'nicovideo.user.mylist.add.video'
|
68
|
+
'マイリスト登録'
|
69
|
+
when 'live.user.program.onairs'
|
70
|
+
'生放送開始'
|
71
|
+
else
|
72
|
+
h['topic']
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def created_at(h)
|
77
|
+
Time.parse(h['createdAt'])
|
78
|
+
end
|
79
|
+
|
80
|
+
# @return [String, String] title, url
|
81
|
+
def infer_body(h)
|
82
|
+
candidate_keys = %w(video program)
|
83
|
+
candidate_keys.each do |k|
|
84
|
+
if h[k]
|
85
|
+
id, title = h[k].fetch_values('id', 'title') if h[k]
|
86
|
+
return title, watch_url(id, k)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
['', '']
|
91
|
+
end
|
92
|
+
|
93
|
+
def watch_url(id, key)
|
94
|
+
base =
|
95
|
+
case key
|
96
|
+
when 'video'
|
97
|
+
"http://www.nicovideo.jp/watch/"
|
98
|
+
when 'program'
|
99
|
+
"http://live.nicovideo.jp/watch/"
|
100
|
+
end
|
6
101
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
@url = attrs[:url]
|
11
|
-
@author = attrs[:author]
|
12
|
-
@kind = attrs[:kind]
|
13
|
-
@date = attrs[:date]
|
102
|
+
base + id
|
103
|
+
end
|
104
|
+
end
|
14
105
|
end
|
15
106
|
end
|
16
107
|
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'cgi'
|
3
|
+
require 'openssl'
|
4
|
+
|
5
|
+
module Nicorepo
|
6
|
+
module Request
|
7
|
+
class Auth
|
8
|
+
class LoginError < StandardError; end
|
9
|
+
|
10
|
+
attr_reader :session
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@session = ''
|
14
|
+
end
|
15
|
+
|
16
|
+
def login(mail, pass)
|
17
|
+
res = Net::HTTP.start(url.host, url.port, use_ssl: true, verify_mode: OpenSSL::SSL::VERIFY_NONE) do |client|
|
18
|
+
client.post(url.path, body(mail, pass))
|
19
|
+
end
|
20
|
+
|
21
|
+
cookies = res.get_fields('set-cookie').map { |str| CGI::Cookie.parse(str) }
|
22
|
+
|
23
|
+
session_cookie = cookies.detect { |c|
|
24
|
+
c['user_session']&.size > 0 && c['user_session'] != ['deleted']
|
25
|
+
}
|
26
|
+
fail LoginError, "invalid mail or password: mail => #{mail}" unless session_cookie
|
27
|
+
|
28
|
+
@session = session_cookie.fetch('user_session').first
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def url
|
34
|
+
URI('https://secure.nicovideo.jp/secure/login?site=niconico')
|
35
|
+
end
|
36
|
+
|
37
|
+
def body(mail, pass)
|
38
|
+
"mail=#{mail}&password=#{pass}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class Timeline
|
43
|
+
require 'json'
|
44
|
+
|
45
|
+
def self.fetch(session, cursor: nil)
|
46
|
+
new.fetch(session, cursor)
|
47
|
+
end
|
48
|
+
|
49
|
+
def fetch(session, cursor)
|
50
|
+
url = url(cursor)
|
51
|
+
res = Net::HTTP.start(url.host, url.port) do |client|
|
52
|
+
client.get(url.request_uri, header(session))
|
53
|
+
end
|
54
|
+
|
55
|
+
fail "Request timeline failed !! #{res.inspect}: #{res.to_hash}" if res.code != '200'
|
56
|
+
|
57
|
+
JSON.parse(res.body)
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def header(session)
|
63
|
+
{
|
64
|
+
'Cookie' => CGI::Cookie.new('user_session', session).to_s,
|
65
|
+
'Connection' => 'keep-alive',
|
66
|
+
}
|
67
|
+
end
|
68
|
+
|
69
|
+
def url(cursor)
|
70
|
+
params = {
|
71
|
+
client_app: 'pc_myrepo',
|
72
|
+
cursor: cursor,
|
73
|
+
}.compact
|
74
|
+
|
75
|
+
URI('http://www.nicovideo.jp/api/nicorepo/timeline/my/all').tap do |url|
|
76
|
+
url.query = params.map { |k, v| "#{CGI.escape(k.to_s)}=#{v}" }.join('&')
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
data/lib/nicorepo/version.rb
CHANGED
@@ -1,3 +1,3 @@
|
|
1
|
-
|
2
|
-
VERSION = "0.0
|
1
|
+
module Nicorepo
|
2
|
+
VERSION = "1.0.0"
|
3
3
|
end
|
data/nicorepo.gemspec
CHANGED
@@ -7,8 +7,8 @@ Gem::Specification.new do |spec|
|
|
7
7
|
spec.version = Nicorepo::VERSION
|
8
8
|
spec.authors = ["upinetree"]
|
9
9
|
spec.email = ["upinetree@gmail.com"]
|
10
|
-
spec.summary = %q{Simple nicorepo
|
11
|
-
spec.description = %q{
|
10
|
+
spec.summary = %q{Simple nicorepo client}
|
11
|
+
spec.description = %q{Provides a simple API client for Nicorepo (on nicovideo.jp).}
|
12
12
|
spec.homepage = "https://github.com/upinetree/nicorepo"
|
13
13
|
spec.license = "MIT"
|
14
14
|
|
@@ -20,16 +20,17 @@ Gem::Specification.new do |spec|
|
|
20
20
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
21
21
|
spec.require_paths = ["lib"]
|
22
22
|
|
23
|
-
spec.required_ruby_version = ">= 2.
|
23
|
+
spec.required_ruby_version = ">= 2.3.0"
|
24
24
|
|
25
|
-
spec.add_dependency "mechanize", "~> 2.7"
|
26
25
|
spec.add_dependency "launchy", "~> 2.4"
|
27
26
|
spec.add_dependency "netrc", "~> 0.7"
|
28
27
|
spec.add_dependency "thor", "~> 0.19"
|
29
28
|
|
30
|
-
spec.add_development_dependency "bundler"
|
29
|
+
spec.add_development_dependency "bundler"
|
31
30
|
spec.add_development_dependency "pry"
|
32
31
|
spec.add_development_dependency "rake"
|
33
32
|
spec.add_development_dependency "rspec"
|
33
|
+
spec.add_development_dependency "webmock"
|
34
|
+
spec.add_development_dependency "vcr"
|
34
35
|
end
|
35
36
|
|
metadata
CHANGED
@@ -1,29 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: nicorepo
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- upinetree
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-07-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
-
- !ruby/object:Gem::Dependency
|
14
|
-
name: mechanize
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
16
|
-
requirements:
|
17
|
-
- - "~>"
|
18
|
-
- !ruby/object:Gem::Version
|
19
|
-
version: '2.7'
|
20
|
-
type: :runtime
|
21
|
-
prerelease: false
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
23
|
-
requirements:
|
24
|
-
- - "~>"
|
25
|
-
- !ruby/object:Gem::Version
|
26
|
-
version: '2.7'
|
27
13
|
- !ruby/object:Gem::Dependency
|
28
14
|
name: launchy
|
29
15
|
requirement: !ruby/object:Gem::Requirement
|
@@ -70,16 +56,16 @@ dependencies:
|
|
70
56
|
name: bundler
|
71
57
|
requirement: !ruby/object:Gem::Requirement
|
72
58
|
requirements:
|
73
|
-
- - "
|
59
|
+
- - ">="
|
74
60
|
- !ruby/object:Gem::Version
|
75
|
-
version: '
|
61
|
+
version: '0'
|
76
62
|
type: :development
|
77
63
|
prerelease: false
|
78
64
|
version_requirements: !ruby/object:Gem::Requirement
|
79
65
|
requirements:
|
80
|
-
- - "
|
66
|
+
- - ">="
|
81
67
|
- !ruby/object:Gem::Version
|
82
|
-
version: '
|
68
|
+
version: '0'
|
83
69
|
- !ruby/object:Gem::Dependency
|
84
70
|
name: pry
|
85
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -122,7 +108,35 @@ dependencies:
|
|
122
108
|
- - ">="
|
123
109
|
- !ruby/object:Gem::Version
|
124
110
|
version: '0'
|
125
|
-
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: webmock
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: vcr
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
description: Provides a simple API client for Nicorepo (on nicovideo.jp).
|
126
140
|
email:
|
127
141
|
- upinetree@gmail.com
|
128
142
|
executables:
|
@@ -141,11 +155,13 @@ files:
|
|
141
155
|
- bin/nicorepo
|
142
156
|
- exe/nicorepo
|
143
157
|
- lib/nicorepo.rb
|
144
|
-
- lib/nicorepo/cli
|
145
|
-
- lib/nicorepo/cli/
|
146
|
-
- lib/nicorepo/
|
158
|
+
- lib/nicorepo/cli.rb
|
159
|
+
- lib/nicorepo/cli/configuration.rb
|
160
|
+
- lib/nicorepo/client.rb
|
161
|
+
- lib/nicorepo/filter.rb
|
162
|
+
- lib/nicorepo/page.rb
|
147
163
|
- lib/nicorepo/report.rb
|
148
|
-
- lib/nicorepo/
|
164
|
+
- lib/nicorepo/request.rb
|
149
165
|
- lib/nicorepo/version.rb
|
150
166
|
- nicorepo.gemspec
|
151
167
|
homepage: https://github.com/upinetree/nicorepo
|
@@ -160,7 +176,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
160
176
|
requirements:
|
161
177
|
- - ">="
|
162
178
|
- !ruby/object:Gem::Version
|
163
|
-
version: 2.
|
179
|
+
version: 2.3.0
|
164
180
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
165
181
|
requirements:
|
166
182
|
- - ">="
|
@@ -171,5 +187,5 @@ rubyforge_project:
|
|
171
187
|
rubygems_version: 2.6.11
|
172
188
|
signing_key:
|
173
189
|
specification_version: 4
|
174
|
-
summary: Simple nicorepo
|
190
|
+
summary: Simple nicorepo client
|
175
191
|
test_files: []
|
data/lib/nicorepo/parser.rb
DELETED
@@ -1,87 +0,0 @@
|
|
1
|
-
class Nicorepo
|
2
|
-
class Parser
|
3
|
-
def initialize(agent)
|
4
|
-
@agent = agent
|
5
|
-
end
|
6
|
-
|
7
|
-
def parse_page(url)
|
8
|
-
page = @agent.get(url)
|
9
|
-
{
|
10
|
-
reports_attrs: parse_reports(page),
|
11
|
-
next_url: next_url(page)
|
12
|
-
}
|
13
|
-
end
|
14
|
-
|
15
|
-
private
|
16
|
-
|
17
|
-
def parse_reports(page)
|
18
|
-
nodes = report_nodes(page)
|
19
|
-
nodes.map { |node| report_attrs(node) }
|
20
|
-
end
|
21
|
-
|
22
|
-
def report_attrs(node)
|
23
|
-
{
|
24
|
-
body: parse_body(node),
|
25
|
-
title: parse_title(node),
|
26
|
-
url: parse_url(node),
|
27
|
-
author: parse_author(node),
|
28
|
-
kind: parse_kind(node),
|
29
|
-
date: parse_date(node)
|
30
|
-
}
|
31
|
-
rescue => e
|
32
|
-
error_report(node, e)
|
33
|
-
end
|
34
|
-
|
35
|
-
def report_nodes(page)
|
36
|
-
page.search('.timeline/.log')
|
37
|
-
end
|
38
|
-
|
39
|
-
def next_url(page)
|
40
|
-
page.search('.next-page-link').first['href']
|
41
|
-
end
|
42
|
-
|
43
|
-
def parse_body(node)
|
44
|
-
node.search('.log-body').first.inner_text.gsub(/(\t|\r|\n)/, "")
|
45
|
-
end
|
46
|
-
|
47
|
-
def parse_title(node)
|
48
|
-
node.search('.log-target-info/a').first.inner_text
|
49
|
-
end
|
50
|
-
|
51
|
-
def parse_url(node)
|
52
|
-
node.search('.log-target-info/a').first['href']
|
53
|
-
end
|
54
|
-
|
55
|
-
def parse_author(node)
|
56
|
-
node.search('.log-body/a').first.inner_text
|
57
|
-
end
|
58
|
-
|
59
|
-
# example: 'log.log-community-video-upload' -> 'community-video-upload'
|
60
|
-
def parse_kind(node)
|
61
|
-
cls = node['class']
|
62
|
-
trim = 'log-'
|
63
|
-
|
64
|
-
index = cls.index(/#{trim}/)
|
65
|
-
return cls if index.nil?
|
66
|
-
|
67
|
-
index += trim.size
|
68
|
-
cls[index..cls.size]
|
69
|
-
end
|
70
|
-
|
71
|
-
def parse_date(node)
|
72
|
-
d = node.search('.log-footer-date/time').first['datetime']
|
73
|
-
Time.xmlschema(d).localtime
|
74
|
-
end
|
75
|
-
|
76
|
-
def error_report(node, e)
|
77
|
-
{
|
78
|
-
body: node.inner_html.gsub(/\r|\n|\t/, ''),
|
79
|
-
title: "An exception occured: #{e.message}\n#{e.backtrace}",
|
80
|
-
url: '',
|
81
|
-
author: '',
|
82
|
-
kind: Report::ERROR_KIND,
|
83
|
-
date: ''
|
84
|
-
}
|
85
|
-
end
|
86
|
-
end
|
87
|
-
end
|
data/lib/nicorepo/reports.rb
DELETED
@@ -1,78 +0,0 @@
|
|
1
|
-
require 'nicorepo/report'
|
2
|
-
require 'forwardable'
|
3
|
-
|
4
|
-
class Nicorepo
|
5
|
-
class Reports
|
6
|
-
extend Forwardable
|
7
|
-
|
8
|
-
TOP_URL = 'http://www.nicovideo.jp/my/top'
|
9
|
-
|
10
|
-
attr_reader :reports
|
11
|
-
def_delegators :@reports, :size
|
12
|
-
|
13
|
-
def initialize(parser)
|
14
|
-
@parser = parser
|
15
|
-
@reports = []
|
16
|
-
end
|
17
|
-
|
18
|
-
def fetch(request_num, limit_page, since: nil)
|
19
|
-
filter = {}
|
20
|
-
filter[:kind] = selected_kind
|
21
|
-
filter[:since] =
|
22
|
-
case since
|
23
|
-
when String
|
24
|
-
Time.parse since
|
25
|
-
when Time
|
26
|
-
since
|
27
|
-
else
|
28
|
-
nil
|
29
|
-
end
|
30
|
-
@reports = fetch_recursively(request_num, limit_page, filter)
|
31
|
-
end
|
32
|
-
|
33
|
-
private
|
34
|
-
|
35
|
-
def selected_kind
|
36
|
-
nil
|
37
|
-
end
|
38
|
-
|
39
|
-
def fetch_recursively(request_num, limit_page, filter = {}, url = TOP_URL)
|
40
|
-
return [] unless limit_page > 0
|
41
|
-
|
42
|
-
# fetch current page reports
|
43
|
-
page = @parser.parse_page(url)
|
44
|
-
reports = page[:reports_attrs].map { |attrs| Report.new(attrs) }
|
45
|
-
|
46
|
-
if filter[:since]
|
47
|
-
reach_oldest_page = (reports.last.date < filter[:since])
|
48
|
-
reports.reject! { |report| report.date < filter[:since] }
|
49
|
-
end
|
50
|
-
reports.select! { |report| report.kind =~ /#{filter[:kind]}|#{Report::ERROR_KIND}/ } if filter[:kind]
|
51
|
-
|
52
|
-
return reports[0, request_num] if reports.size >= request_num
|
53
|
-
return reports if filter[:since] && reach_oldest_page
|
54
|
-
|
55
|
-
# recursively fetch next reports
|
56
|
-
next_reports = fetch_recursively(request_num - reports.size, limit_page - 1, filter, page[:next_url])
|
57
|
-
reports += next_reports unless next_reports.nil?
|
58
|
-
reports
|
59
|
-
end
|
60
|
-
end
|
61
|
-
|
62
|
-
class VideoReports < Reports
|
63
|
-
private
|
64
|
-
|
65
|
-
def selected_kind
|
66
|
-
'video-upload'
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
class LiveReports < Reports
|
71
|
-
private
|
72
|
-
|
73
|
-
def selected_kind
|
74
|
-
'live'
|
75
|
-
end
|
76
|
-
end
|
77
|
-
end
|
78
|
-
|