niconico 1.7.0 → 1.8.0.beta1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4e974da09ccd8ca51489f33e40cb8cee03492e8c
4
- data.tar.gz: cd9f2c241c322cac652d47c82ba2461e9aae46ce
3
+ metadata.gz: 8f3cecdf7d7a1219ecc232673b957e18b42fcf8f
4
+ data.tar.gz: 4e1e775aa2ef787e937b1ab6381d5bf81d72bd22
5
5
  SHA512:
6
- metadata.gz: 83d692cf58272878c31836fcb8fd22cf76e11e063117a337d42aa7b62e9145bdf986d783b8fe611d4bcbd3e62c7c1cc088c17023ce5f2c0d93e325a59bdf37e8
7
- data.tar.gz: e7527167a1871dbde1c74fe965b90a35456282be8171da7ce5faf1a52ed042d31b4ae028fbc744f84551f850ba32a923941a52fcd006f36e1edfed2bd4bb9837
6
+ metadata.gz: 33f67d8e48081a9ca33c56a9ddfd8b0fa105ac0dbd02570f2482606d6fc2aa68b399e542085a4c1603cdf72e270a936e36ca4eb445bd44ff4ce08ba62291b633
7
+ data.tar.gz: 73773ad3629e2dd89b24618f0464050a39df504ad6c7a42d5355deaf0f92545038aaac0c6fbb7fa68a8284a517a74511e381a5b8b84e6ba55dee6c14d121dd58
data/README.mkd CHANGED
@@ -12,7 +12,7 @@ Wrapper of `Mechanize`, optimized for <http://www.nicovideo.jp/>.
12
12
 
13
13
  ## Requirements
14
14
 
15
- * Ruby 1.9+ (1.9.2+ is supported)
15
+ * Ruby 2.0.0+ (2.1+ is supported)
16
16
 
17
17
  ## Install
18
18
 
@@ -114,4 +114,6 @@ require 'niconico/mylist'
114
114
  require 'niconico/ranking'
115
115
  require 'niconico/channel'
116
116
  require 'niconico/live'
117
+ require 'niconico/live/client'
118
+ require 'niconico/live/mypage'
117
119
  require 'niconico/nico_api'
@@ -16,6 +16,25 @@ class Niconico
16
16
  def deferred_methods
17
17
  @deferred_methods ||= []
18
18
  end
19
+
20
+ def lazy(key, &block)
21
+ define_method(key) do
22
+ case
23
+ when fetched?
24
+ self.instance_eval &block
25
+ when @preload[key]
26
+ @preload[key]
27
+ else
28
+ get()
29
+ self.instance_eval &block
30
+ end
31
+ end
32
+ self.lazy_methods.push key
33
+ end
34
+
35
+ def lazy_methods
36
+ @lazy_methods ||= []
37
+ end
19
38
  end
20
39
 
21
40
  def self.included(klass)
@@ -31,9 +50,14 @@ class Niconico
31
50
  private
32
51
 
33
52
  def preload_deffered_values(vars={})
53
+ @preload ||= {}
34
54
  vars.each do |k,v|
35
- next unless self.class.deferred_methods.include?(k)
36
- instance_variable_set "@#{k}", v
55
+ case
56
+ when self.class.deferred_methods.include?(k)
57
+ instance_variable_set "@#{k}", v
58
+ when self.class.lazy_methods.include?(k)
59
+ @preload[k] = v
60
+ end
37
61
  end
38
62
  end
39
63
  end
@@ -1,4 +1,5 @@
1
1
  # coding: utf-8
2
+ require 'niconico/deferrable'
2
3
  require 'niconico/live/api'
3
4
 
4
5
  class Niconico
@@ -7,6 +8,8 @@ class Niconico
7
8
  end
8
9
 
9
10
  class Live
11
+ include Niconico::Deferrable
12
+
10
13
  class ReservationOutdated < Exception; end
11
14
  class ReservationNotAccepted < Exception; end
12
15
  class TicketRetrievingFailed < Exception; end
@@ -28,13 +31,17 @@ class Niconico
28
31
  end
29
32
  end
30
33
 
31
- def initialize(parent, live_id)
34
+ def initialize(parent, live_id, preload = nil)
32
35
  @parent = parent
33
36
  @agent = parent.agent
34
37
  @id = @live_id = live_id
35
38
  @client = Niconico::Live::API.new(@agent)
36
39
 
37
- get()
40
+ if preload
41
+ preload_deffered_values(preload)
42
+ else
43
+ get
44
+ end
38
45
  end
39
46
 
40
47
  attr_reader :id, :live, :ticket
@@ -79,27 +86,27 @@ class Niconico
79
86
  end
80
87
 
81
88
  def inspect
82
- "#<Niconico::Live: #{id}, #{title}>"
89
+ "#<Niconico::Live: #{id}, #{title}#{fetched? ? '': ' (deferred)'}>"
83
90
  end
84
91
 
85
- def title
86
- get.live[:title]
92
+ lazy :title do
93
+ live[:title]
87
94
  end
88
95
 
89
- def description
90
- get.live[:description]
96
+ lazy :description do
97
+ live[:description]
91
98
  end
92
99
 
93
- def opens_at
94
- get.live[:opens_at]
100
+ lazy :opens_at do
101
+ live[:opens_at]
95
102
  end
96
103
 
97
- def starts_at
98
- get.live[:starts_at]
104
+ lazy :starts_at do
105
+ live[:starts_at]
99
106
  end
100
107
 
101
- def status
102
- get.live[:status]
108
+ lazy :status do
109
+ live[:status]
103
110
  end
104
111
 
105
112
  def scheduled?
@@ -114,32 +121,36 @@ class Niconico
114
121
  status == :closed
115
122
  end
116
123
 
124
+ lazy :reservation do
125
+ live[:reservation]
126
+ end
127
+
117
128
  def reserved?
118
- get.live.key? :reservation
129
+ !!reservation
130
+ end
131
+
132
+ def reservation_available?
133
+ reserved? && reservation[:available]
119
134
  end
120
135
 
121
136
  def reservation_unaccepted?
122
- reserved? && live[:reservation][:status] == :reserved
137
+ reservation_available? && reservation[:status] == :reserved
123
138
  end
124
139
 
125
140
  def reservation_accepted?
126
- reserved? && live[:reservation][:status] == :accepted
141
+ reserved? && reservation[:status] == :accepted
127
142
  end
128
143
 
129
144
  def reservation_outdated?
130
- reserved? && live[:reservation][:status] == :outdated
131
- end
132
-
133
- def reservation_available?
134
- reservation_unaccepted? || reservation_accepted?
145
+ reserved? && reservation[:status] == :outdated
135
146
  end
136
147
 
137
148
  def reservation_expires_at
138
- reserved? ? live[:reservation][:expires_at] : nil
149
+ reserved? ? reservation[:expires_at] : nil
139
150
  end
140
151
 
141
- def channel
142
- get.live[:channel]
152
+ lazy :channel do
153
+ live[:channel]
143
154
  end
144
155
 
145
156
  def premium?
@@ -1,6 +1,7 @@
1
1
  # coding: utf-8
2
2
  require 'time'
3
3
  require 'openssl'
4
+ require 'niconico/live/util'
4
5
 
5
6
  class Niconico
6
7
  class Live
@@ -16,8 +17,43 @@ class Niconico
16
17
 
17
18
  attr_reader :agent
18
19
 
20
+ def self.parse_reservation_message(message)
21
+ valid_until_message = message.match(/(?:使用|利用)?期限: (.+?)まで|(?:期限中、何度でも視聴できます|視聴権(?:利用|使用)期限が切れています|タイムシフト利用期間は終了しました)\s*\[(.+?)まで\]/)
22
+ valid_message = message.match(/\[視聴期限未定\]/)
23
+
24
+ case
25
+ when message.match(/予約中\s*\[/)
26
+ {status: :reserved, available: false}
27
+ when valid_until_message || valid_message
28
+ {}.tap do |reservation|
29
+ if valid_until_message
30
+ reservation[:expires_at] = Time.parse("#{valid_until_message[1] || valid_until_message[2]} +0900")
31
+ end
32
+
33
+ if message.include?('視聴権を使用し、タイムシフト視聴を行いますか?')
34
+ reservation[:status] = :reserved
35
+ reservation[:available] = true
36
+ elsif message.include?('本番組は、タイムシフト視聴を行う事が可能です。') \
37
+ || message.include?('期限中、何度でも視聴できます')
38
+ reservation[:status] = :accepted
39
+ reservation[:available] = true
40
+ elsif message.include?('タイムシフト視聴をこれ以上行う事は出来ません。') \
41
+ || message.include?('視聴権の利用期限が過ぎています。') \
42
+ || message.include?('視聴権利用期限が切れています') \
43
+ || message.include?('視聴権使用期限が切れています') \
44
+ || message.include?('タイムシフト利用期間は終了しました') \
45
+ || message.include?('アーカイブ公開期限は終了しました。')
46
+ reservation[:status] = :outdated
47
+ reservation[:available] = false
48
+ end
49
+ end
50
+ else
51
+ nil
52
+ end
53
+ end
54
+
19
55
  def get(id)
20
- id = normalize_id(id)
56
+ id = Util::normalize_id(id)
21
57
 
22
58
  page = agent.get("http://live.nicovideo.jp/gate/#{id}")
23
59
 
@@ -41,20 +77,9 @@ class Niconico
41
77
  result[:closed_at] = Time.parse("#{close_message[1]} +0900")
42
78
  end
43
79
 
44
- reservation_valid_until_message = comment_area.match(/使用期限: (.+?)まで/)
45
- if reservation_valid_until_message
46
- result[:reservation] = {}
47
- result[:reservation][:expires_at] = Time.parse("#{reservation_valid_until_message[1]} +0900")
48
-
49
- if comment_area.include?('視聴権を使用し、タイムシフト視聴を行いますか?')
50
- result[:reservation][:status] = :reserved
51
- result[:reservation][:available] = true
52
- elsif comment_area.include?('本番組は、タイムシフト視聴を行う事が可能です。')
53
- result[:reservation][:status] = :accepted
54
- result[:reservation][:available] = true
55
- elsif comment_area.include?('タイムシフト視聴をこれ以上行う事は出来ません。') || comment_area.include?('視聴権の利用期限が過ぎています。')
56
- result[:reservation][:status] = :outdated
57
- end
80
+ result[:reservation] = self.class.parse_reservation_message(comment_area)
81
+ if !result[:reservation] && page.search(".watching_reservation_reserved").any? { |_| _['onclick'].include?(id) }
82
+ result[:reservation] = {status: :reserved, available: false}
58
83
  end
59
84
 
60
85
  channel = page.at('div.chan')
@@ -74,7 +99,7 @@ class Niconico
74
99
  end
75
100
 
76
101
  def get_player_status(id, public_key = nil)
77
- id = normalize_id(id)
102
+ id = Util::normalize_id(id)
78
103
  page = agent.get("http://ow.live.nicovideo.jp/api/getplayerstatus?locale=GLOBAL&lang=ja%2Djp&v=#{id}&seat%5Flocale=JP")
79
104
  if page.body[0] == 'c' # encrypted
80
105
  page = Nokogiri::XML(decrypt_encrypted_player_status(page.body, public_key))
@@ -157,18 +182,21 @@ class Niconico
157
182
 
158
183
  def watching_reservations
159
184
  page = agent.get(URL_WATCHINGRESERVATION_LIST)
160
- page.search('vid').map(&:inner_text).map{ |_| normalize_id(_) }
185
+ page.search('vid').map(&:inner_text).map{ |_| Util::normalize_id(_) }
161
186
  end
162
187
 
163
188
  def accept_watching_reservation(id_)
164
- id = normalize_id(id_, with_lv: false)
165
- page = agent.get("http://live.nicovideo.jp/api/watchingreservation?mode=confirm_watch_my&vid=#{id}&next_url&analytic")
166
- token = page.at('#reserve img')['onclick'].scan(/'(.+?)'/)[1][0]
189
+ id = Util::normalize_id(id_, with_lv: false)
167
190
 
191
+ page = agent.get("http://live.nicovideo.jp/api/watchingreservation?mode=watch_num&vid=#{id}&next_url&analytic")
192
+
193
+ token = Util::fetch_token(@agent)
168
194
  page = agent.post("http://live.nicovideo.jp/api/watchingreservation",
169
- accept: 'true', mode: 'use', vid: id, token: token)
195
+ mode: 'auto_register', vid: id, token: token, '_' => '')
170
196
 
171
- page.at('nicolive_video_response')['status'] == 'ok'
197
+ token = Util::fetch_token(@agent)
198
+ page = agent.post("http://live.nicovideo.jp/api/watchingreservation",
199
+ accept: 'true', mode: 'use', vid: id, token: token)
172
200
  end
173
201
 
174
202
  def decrypt_encrypted_player_status(body, public_key)
@@ -195,18 +223,6 @@ class Niconico
195
223
  body = cipher.update(encrypted_body) + cipher.final
196
224
  body.force_encoding('utf-8')
197
225
  end
198
-
199
- private
200
-
201
- def normalize_id(id, with_lv: true)
202
- id = id.to_s
203
-
204
- if with_lv
205
- id.start_with?('lv') ? id : "lv#{id}"
206
- else
207
- id.start_with?('lv') ? id[2..-1] : id
208
- end
209
- end
210
226
  end
211
227
  end
212
228
  end
@@ -0,0 +1,59 @@
1
+ require 'niconico/live/client/search_result'
2
+ require 'niconico/live/client/search_filters'
3
+
4
+ class Niconico
5
+ def live_client
6
+ Live::Client.new(self.agent)
7
+ end
8
+
9
+ class Live
10
+ class Client
11
+
12
+ def initialize(agent)
13
+ @agent = agent
14
+ @api = API.new(agent)
15
+ end
16
+
17
+ def remove_timeshifts(ids)
18
+ post_body = "delete=timeshift&confirm=#{Util::fetch_token(@agent)}"
19
+ if ids.size == 0
20
+ return
21
+ end
22
+ ids.each do |id|
23
+ id = Util::normalize_id(id, with_lv: false)
24
+ # mechanize doesn't support multiple values for the same key in query.
25
+ post_body += "&vid%5B%5D=#{id}"
26
+ end
27
+ @agent.post(
28
+ 'http://live.nicovideo.jp/my.php',
29
+ post_body,
30
+ 'Content-Type' => 'application/x-www-form-urlencoded'
31
+ )
32
+ end
33
+
34
+ def search(keyword, filters = [])
35
+ filter = filters.join('+')
36
+ page = @agent.get(
37
+ 'http://live.nicovideo.jp/search',
38
+ track: '',
39
+ sort: 'recent',
40
+ date: '',
41
+ kind: '',
42
+ keyword: keyword,
43
+ filter: filter
44
+ )
45
+ results_dom = page.at('.result_list')
46
+ items = results_dom.css('.result_item')
47
+ search_results = items.map do |item|
48
+ title_dom = item.at('.search_stream_title a')
49
+ next nil unless title_dom
50
+ id = title_dom.attr(:href).scan(/lv[\d]+/).first
51
+ title = title_dom.text.strip
52
+ description = item.at('.search_stream_description').text.strip
53
+ SearchResult.new(id, title, description)
54
+ end
55
+ search_results.compact
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,21 @@
1
+ class Niconico
2
+ class Live
3
+ class Client
4
+ module SearchFilters
5
+ ONAIR = ':onair:' # 放送中
6
+ RESERVED = ':reserved:' # 放送予定
7
+ CLOSED = ':closed:' # 放送終了
8
+
9
+ # joined by 'OR'
10
+ OFFICIAL = ':official:' # 公式
11
+ CHANNEL = ':channel:' # チャンネル
12
+ COMMUNITY = ':community:' # コミュニティ
13
+
14
+ HIDE_TS_EXPIRED = ':hidetsexpired:' # タイムシフトが視聴できない番組を表示しない
15
+ NO_COMMUNITY_GROUP = ':nocommunitygroup:' # 同一コミュニティをまとめて表示しない
16
+ HIDE_COM_ONLY = ':hidecomonly:' # コミュニティ限定番組を表示しない
17
+ end
18
+ end
19
+ end
20
+ end
21
+
@@ -0,0 +1,8 @@
1
+ class Niconico
2
+ class Live
3
+ class Client
4
+ class SearchResult < Struct.new(:id, :title, :description)
5
+ end
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,67 @@
1
+ # coding: utf-8
2
+ require 'niconico/live'
3
+ require 'niconico/live/api'
4
+
5
+ class Niconico
6
+ def live_mypage
7
+ Niconico::Live::Mypage.new(self)
8
+ end
9
+
10
+ class Live
11
+ class Mypage
12
+ class UnknownStatus < Exception; end
13
+ URL = 'http://live.nicovideo.jp/my'.freeze
14
+
15
+ def initialize(client)
16
+ @client = client
17
+ end
18
+
19
+ attr_reader :client
20
+
21
+ def agent
22
+ client.agent
23
+ end
24
+
25
+ def page
26
+ @page ||= agent.get(URL)
27
+ end
28
+
29
+ def reservations
30
+ return @reservations if @reservations
31
+ lists = page.search("form[name=timeshift_list] .liveItems")
32
+ @reservations = lists.flat_map do |list|
33
+ list.search('.column').map do |column|
34
+ link = column.at('.name a')
35
+ id = link[:href].sub(/\A.*\//,'').sub(/\?.*\z/,'')
36
+ status = column.at('.status').inner_text
37
+ watch_button = column.at('.timeshift_watch a')
38
+
39
+ preload = {}
40
+
41
+ preload[:title] = link[:title]
42
+
43
+ preload[:reservation] = Live::API.parse_reservation_message(status)
44
+ raise UnknownStatus, "BUG, there's unknown message for reservation status: #{status.inspect}" unless preload[:reservation]
45
+
46
+ # (試聴する) [試聴期限未定]
47
+ if watch_button && watch_button[:onclick] && watch_button[:onclick].include?('confirm_watch_my')
48
+ preload[:reservation][:status] = :reserved
49
+ preload[:reservation][:available] = true
50
+ end
51
+
52
+ Niconico::Live.new(client, id, preload)
53
+ end
54
+ end
55
+ end
56
+ alias timeshift_list reservations
57
+
58
+ def available_reservations
59
+ reservations.select { |_| _.reservation_available? }
60
+ end
61
+
62
+ def unaccepted_reservations
63
+ reservations.select { |_| _.reservation_unaccepted? }
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,22 @@
1
+ class Niconico
2
+ class Live
3
+ class Util
4
+ class << self
5
+ def normalize_id(id, with_lv: true)
6
+ id = id.to_s
7
+
8
+ if with_lv
9
+ id.start_with?('lv') ? id : "lv#{id}"
10
+ else
11
+ id.start_with?('lv') ? id[2..-1] : id
12
+ end
13
+ end
14
+
15
+ def fetch_token(agent)
16
+ page = agent.get('http://live.nicovideo.jp/my')
17
+ page.at('#confirm').attr('value')
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -2,8 +2,16 @@ require 'json'
2
2
 
3
3
  class Niconico
4
4
  class NicoAPI
5
- class ApiError < Exception; end
6
5
  class AcquiringTokenError < Exception; end
6
+ class ApiError < Exception
7
+ def initialize(error)
8
+ @description = error['description']
9
+ @code = error['code']
10
+ super "#{@code}: #{@description.inspect}"
11
+ end
12
+
13
+ attr_reader :code, :description
14
+ end
7
15
 
8
16
  MYLIST_ITEM_TYPES = {video: 0, seiga: 5}
9
17
 
@@ -33,7 +41,6 @@ class Niconico
33
41
  item_type: MYLIST_ITEM_TYPES[item_type],
34
42
  item_id: item_id,
35
43
  description: description,
36
- token: token
37
44
  }
38
45
  )
39
46
  end
@@ -41,12 +48,25 @@ class Niconico
41
48
  private
42
49
 
43
50
  def post(path, params)
44
- uri = URI.join(Niconico::URL[:top], path)
45
- page = agent.post(uri, params)
46
- json = JSON.parse(page.body)
47
- raise ApiError, json unless json['status'] == 'ok'
48
- json
49
- end
51
+ retried = false
52
+ begin
53
+ params = params.merge(token: token)
54
+ uri = URI.join(Niconico::URL[:top], path)
55
+ page = agent.post(uri, params)
56
+ json = JSON.parse(page.body)
50
57
 
58
+ raise ApiError.new(json['error']) unless json['status'] == 'ok'
59
+
60
+ json
61
+ rescue ApiError => e
62
+ if (e.code == 'INVALIDTOKEN' || e.code == 'EXPIRETOKEN') && !retried
63
+ retried = true
64
+ @token = nil
65
+ retry
66
+ else
67
+ raise e
68
+ end
69
+ end
70
+ end
51
71
  end
52
72
  end
@@ -1,3 +1,3 @@
1
1
  class Niconico
2
- VERSION = "1.7.0"
2
+ VERSION = "1.8.0.beta1"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: niconico
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.7.0
4
+ version: 1.8.0.beta1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shota Fukumori (sora_h)
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-02-11 00:00:00.000000000 Z
11
+ date: 2015-05-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: mechanize
@@ -55,6 +55,11 @@ files:
55
55
  - lib/niconico/deferrable.rb
56
56
  - lib/niconico/live.rb
57
57
  - lib/niconico/live/api.rb
58
+ - lib/niconico/live/client.rb
59
+ - lib/niconico/live/client/search_filters.rb
60
+ - lib/niconico/live/client/search_result.rb
61
+ - lib/niconico/live/mypage.rb
62
+ - lib/niconico/live/util.rb
58
63
  - lib/niconico/mylist.rb
59
64
  - lib/niconico/nico_api.rb
60
65
  - lib/niconico/ranking.rb
@@ -75,12 +80,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
75
80
  version: '0'
76
81
  required_rubygems_version: !ruby/object:Gem::Requirement
77
82
  requirements:
78
- - - ">="
83
+ - - ">"
79
84
  - !ruby/object:Gem::Version
80
- version: '0'
85
+ version: 1.3.1
81
86
  requirements: []
82
87
  rubyforge_project:
83
- rubygems_version: 2.4.1
88
+ rubygems_version: 2.2.2
84
89
  signing_key:
85
90
  specification_version: 4
86
91
  summary: wrapper of Mechanize, optimized for nicovideo.