niconico 1.7.0 → 1.8.0.beta1

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
  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.