niconico 1.3.0 → 1.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 36a98bd1ac8a923065e75fb31ce853304d878dd6
4
- data.tar.gz: 6117581998277bd370b01516ab7f4f33c5f07c6c
3
+ metadata.gz: df82c4e26df5f6e3fd88b09775a014a65600fb3b
4
+ data.tar.gz: dccb4490003953bcbc1bac19b533f775e506cdde
5
5
  SHA512:
6
- metadata.gz: f597570ac2329a7cd8da0bb401e735aa1242e2da6e35eb4d80d369ada23e1ca1deebe9cc7541afb6b9be4088b62b6aebb006c90251ad4adb910b5d669482ee0d
7
- data.tar.gz: e8b48d6985978f6f00d3fd3d7965c36661b289824d7346066a16beb34cf1787683e1baaf8f89a4a65a036037365cc424162520e9532b9dd93a4e9ff1067971e8
6
+ metadata.gz: c56db982b4c804be9d889360b752396bb321bd2e88c874e709dc5d9bb2853348b330de2421d4aca67888e1fa19688953be8353945ed1ce2b890984c0a7f422d8
7
+ data.tar.gz: 5bea438a9fd9264d97328918663b1287bc5164d320ca2a0237bfc448a6c6915861cf875d4e4f6cbc81dd83d9eca5dbf970ff08f251cc8aa688f77d5542a06de4
data/lib/niconico.rb CHANGED
@@ -1,12 +1,12 @@
1
1
  # -*- coding: utf-8 -*-
2
2
 
3
- require 'rubygems'
4
3
  require 'mechanize'
5
4
  require 'cgi'
6
5
  require 'niconico/version'
7
6
 
8
7
  class Niconico
9
8
  URL = {
9
+ top: 'http://www.nicovideo.jp/',
10
10
  login: 'https://secure.nicovideo.jp/secure/login?site=niconico',
11
11
  watch: 'http://www.nicovideo.jp/watch/',
12
12
  getflv: 'http://flapi.nicovideo.jp/api/getflv'
@@ -16,34 +16,76 @@ class Niconico
16
16
 
17
17
  attr_reader :agent, :logined
18
18
 
19
- def initialize(mail, pass)
20
- @mail = mail
21
- @pass = pass
19
+ def initialize(*args)
20
+ case args.size
21
+ when 2
22
+ @mail, @pass = args
23
+ when 1
24
+ @token = args.first
25
+ else
26
+ raise ArgumentError, "wrong number of arguments (#{args.size} for 1..2)"
27
+ end
22
28
 
23
29
  @logined = false
24
30
 
25
31
  @agent = Mechanize.new.tap do |agent|
26
32
  agent.user_agent = "Niconico.gem (#{Niconico::VERSION}, https://github.com/sorah/niconico)"
27
33
  agent.ssl_version = 'SSLv3'
34
+
35
+ agent.cookie_jar.add(
36
+ HTTP::Cookie.new(
37
+ domain: '.nicovideo.jp', path: '/',
38
+ name: 'lang', value: 'ja-jp',
39
+ )
40
+ )
28
41
  end
29
42
  end
30
43
 
31
44
  def login(force=false)
32
45
  return false if !force && @logined
33
46
 
47
+ if @token
48
+ login_with_token
49
+ elsif @mail && @pass
50
+ login_with_email
51
+ else
52
+ raise 'huh? (may be bug)'
53
+ end
54
+ end
55
+
56
+ def inspect
57
+ "#<Niconico: #{@mail || '(token)'}, #{@logined ? "" : "not "}logined>"
58
+ end
59
+
60
+ class LoginError < StandardError; end
61
+
62
+ private
63
+
64
+ def login_with_email
34
65
  page = @agent.post(URL[:login], 'mail' => @mail, 'password' => @pass)
35
66
 
36
67
  raise LoginError, "Failed to login (x-niconico-authflag is 0)" if page.header["x-niconico-authflag"] == '0'
37
68
  @logined = true
38
69
  end
39
70
 
40
- def inspect
41
- "#<Niconico: #{@mail} (#{@logined ? "" : "not "}logined)>"
71
+ def login_with_token
72
+ @agent.cookie_jar.add(
73
+ HTTP::Cookie.new(
74
+ domain: '.nicovideo.jp', path: '/',
75
+ name: 'user_session', value: @token
76
+ )
77
+ )
78
+
79
+ page = @agent.get(URL[:top])
80
+ raise LoginError, "Failed to login (x-niconico-authflag is 0)" if page.header["x-niconico-authflag"] == '0'
81
+
82
+ @logined = true
42
83
  end
43
- class LoginError < StandardError; end
84
+
44
85
  end
45
86
 
46
- require_relative './niconico/video'
47
- require_relative './niconico/mylist'
48
- require_relative './niconico/ranking'
49
- require_relative './niconico/channel'
87
+ require 'niconico/video'
88
+ require 'niconico/mylist'
89
+ require 'niconico/ranking'
90
+ require 'niconico/channel'
91
+ require 'niconico/live'
@@ -2,7 +2,7 @@
2
2
  require 'json'
3
3
  require 'open-uri'
4
4
  require 'nokogiri'
5
- require_relative './video'
5
+ require 'niconico/video'
6
6
 
7
7
  class Niconico
8
8
  def channel_videos(ch)
@@ -0,0 +1,40 @@
1
+ class Niconico
2
+ module Deferrable
3
+ module ClassMethods
4
+ def deferrable(*keys)
5
+ keys.each do |key|
6
+ binding.eval(<<-EOM, __FILE__, __LINE__.succ)
7
+ define_method(:#{key}) do
8
+ get() unless fetched?
9
+ @#{key}
10
+ end
11
+ EOM
12
+ end
13
+ self.deferred_methods.push *keys
14
+ end
15
+
16
+ def deferred_methods
17
+ @deferred_methods ||= []
18
+ end
19
+ end
20
+
21
+ def self.included(klass)
22
+ klass.extend ClassMethods
23
+ end
24
+
25
+ def fetched?; @fetched; end
26
+
27
+ def get
28
+ @fetched = true
29
+ end
30
+
31
+ private
32
+
33
+ def preload_deffered_values(vars={})
34
+ vars.each do |k,v|
35
+ next unless self.class.deferred_methods.include?(k)
36
+ instance_variable_set "@#{k}", v
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,204 @@
1
+ # coding: utf-8
2
+ require 'niconico/live/api'
3
+
4
+ class Niconico
5
+ def live(live_id)
6
+ Live.new(self, live_id)
7
+ end
8
+
9
+ class Live
10
+ class ReservationOutdated < Exception; end
11
+ class ReservationNotAccepted < Exception; end
12
+ class TicketRetrievingFailed < Exception; end
13
+ class AcceptingReservationFailed < Exception; end
14
+
15
+ class << self
16
+ def public_key
17
+ @public_key ||= begin
18
+ if ENV["NICONICO_LIVE_PUBLIC_KEY"]
19
+ File.read(File.expand_path(ENV["NICONICO_LIVE_PUBLIC_KEY"]))
20
+ else
21
+ nil
22
+ end
23
+ end
24
+ end
25
+
26
+ def public_key=(other)
27
+ @public_key = other
28
+ end
29
+ end
30
+
31
+ def initialize(parent, live_id)
32
+ @parent = parent
33
+ @agent = parent.agent
34
+ @id = @live_id = live_id
35
+ @client = Niconico::Live::API.new(@agent)
36
+
37
+ get()
38
+ end
39
+
40
+ attr_reader :id, :live, :ticket
41
+ attr_writer :public_key
42
+
43
+ def public_key
44
+ @public_key || self.class.public_key
45
+ end
46
+
47
+ def fetched?
48
+ !!@fetched
49
+ end
50
+
51
+ def get(force=false)
52
+ return self if @fetched && !force
53
+ @live = @client.get(@live_id)
54
+ @fetched = true
55
+ self
56
+ end
57
+
58
+ def seat(force=false)
59
+ return @seat if @seat && !force
60
+ raise ReservationNotAccepted if reserved? && !reservation_accepted?
61
+
62
+ @seat = @client.get_player_status(self.id, self.public_key)
63
+
64
+ raise TicketRetrievingFailed, @seat[:error] if @seat[:error]
65
+
66
+ @seat
67
+ end
68
+
69
+ def accept_reservation
70
+ return self if reservation_accepted?
71
+ raise ReservationOutdated if reservation_outdated?
72
+
73
+ result = @client.accept_watching_reservation(self.id)
74
+ raise AcceptingReservationFailed unless result
75
+
76
+ get(:reload)
77
+
78
+ self
79
+ end
80
+
81
+ def inspect
82
+ "#<Niconico::Live: #{id}, #{title}>"
83
+ end
84
+
85
+ def title
86
+ get.live[:title]
87
+ end
88
+
89
+ def description
90
+ get.live[:description]
91
+ end
92
+
93
+ def opens_at
94
+ get.live[:opens_at]
95
+ end
96
+
97
+ def starts_at
98
+ get.live[:starts_at]
99
+ end
100
+
101
+ def status
102
+ get.live[:status]
103
+ end
104
+
105
+ def scheduled?
106
+ status == :scheduled
107
+ end
108
+
109
+ def on_air?
110
+ status == :on_air
111
+ end
112
+
113
+ def closed?
114
+ status == :closed
115
+ end
116
+
117
+ def reserved?
118
+ get.live.key? :reservation
119
+ end
120
+
121
+ def reservation_unaccepted?
122
+ reserved? && live[:reservation][:status] == :reserved
123
+ end
124
+
125
+ def reservation_accepted?
126
+ reserved? && live[:reservation][:status] == :accepted
127
+ end
128
+
129
+ def reservation_outdated?
130
+ reserved? && live[:reservation][:status] == :outdated
131
+ end
132
+
133
+ def reservation_available?
134
+ reservation_unaccepted? || reservation_accepted?
135
+ end
136
+
137
+ def reservation_expires_at
138
+ reserved? ? live[:reservation][:expires_at] : nil
139
+ end
140
+
141
+ def channel
142
+ get.live[:channel]
143
+ end
144
+
145
+ def premium?
146
+ !!seat[:premium?]
147
+ end
148
+
149
+ def rtmp_url
150
+ seat[:rtmp][:url]
151
+ end
152
+
153
+ def ticket
154
+ seat[:rtmp][:ticket]
155
+ end
156
+
157
+ def quesheet
158
+ seat[:quesheet]
159
+ end
160
+
161
+ def rtmpdump_commands(file_base)
162
+ file_base = File.expand_path(file_base)
163
+
164
+ publishes = quesheet.select{ |_| /^\/publish / =~ _[:body] }.map do |publish|
165
+ publish[:body].split(/ /).tap(&:shift)
166
+ end
167
+
168
+ plays = quesheet.select{ |_| /^\/play / =~ _[:body] }
169
+
170
+ plays.map.with_index do |play, i|
171
+ cases = play[:body].sub(/^case:/,'').split(/ /)[1].split(/,/)
172
+ publish_id = nil
173
+
174
+ publish_id = cases.find { |_| _.start_with?('premium:') } if premium?
175
+ publish_id ||= cases.find { |_| _.start_with?('default:') }
176
+ publish_id ||= cases[0]
177
+
178
+ publish_id = publish_id.split(/:/).last
179
+
180
+ contents = publishes.select{ |_| _[0] == publish_id }
181
+
182
+ contents.map.with_index do |content, j|
183
+ content = content[1]
184
+ rtmp = "#{self.rtmp_url}/mp4:#{content}"
185
+
186
+ seq = 0
187
+ begin
188
+ file = "#{file_base}.#{i}.#{j}.#{seq}.flv"
189
+ seq += 1
190
+ end while File.exist?(file)
191
+
192
+ ['rtmpdump',
193
+ '-V',
194
+ '-o', file,
195
+ '-r', rtmp,
196
+ '-C', "S:#{ticket}",
197
+ '--playpath', "mp4:#{content}",
198
+ '--app', URI.parse(self.rtmp_url).path.sub(/^\//,'')
199
+ ]
200
+ end
201
+ end.flatten(1)
202
+ end
203
+ end
204
+ end
@@ -0,0 +1,212 @@
1
+ # coding: utf-8
2
+ require 'time'
3
+ require 'openssl'
4
+
5
+ class Niconico
6
+ class Live
7
+ class API
8
+ class NoPublicKeyProvided < Exception; end
9
+
10
+ URL_GETPLAYERSTATUS = 'http://ow.live.nicovideo.jp/api/getplayerstatus'.freeze
11
+ URL_WATCHINGRESERVATION_LIST = 'http://live.nicovideo.jp/api/watchingreservation?mode=list'
12
+
13
+ def initialize(agent)
14
+ @agent = agent
15
+ end
16
+
17
+ attr_reader :agent
18
+
19
+ def get(id)
20
+ id = normalize_id(id)
21
+
22
+ page = agent.get("http://live.nicovideo.jp/gate/#{id}")
23
+
24
+ comment_area = page.at("#comment_area#{id}").inner_text
25
+
26
+ result = {
27
+ title: page.at('h2 span').inner_text,
28
+ id: id,
29
+ description: page.at('.stream_description .text_area').inner_html,
30
+ }
31
+
32
+ kaijo = page.search('.kaijo strong').map(&:inner_text)
33
+ result[:opens_at] = Time.parse("#{kaijo[0]} #{kaijo[1]} +0900")
34
+ result[:starts_at] = Time.parse("#{kaijo[0]} #{kaijo[2]} +0900")
35
+
36
+ result[:status] = :scheduled if comment_area.include?('開場まで、あと')
37
+ result[:status] = :on_air if comment_area.include?('現在放送中')
38
+ close_message = comment_area.match(/この番組は(.+?)に終了いたしました。/)
39
+ if close_message
40
+ result[:status] = :closed
41
+ result[:closed_at] = Time.parse("#{close_message[1]} +0900")
42
+ end
43
+
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
58
+ end
59
+
60
+ channel = page.at('div.chan')
61
+ if channel
62
+ result[:channel] = {
63
+ name: channel.at('.shosai a').inner_text,
64
+ id: channel.at('.shosai a')['href'].split('/').last,
65
+ link: channel.at('.shosai a')['href'],
66
+ }
67
+ end
68
+
69
+ result
70
+ end
71
+
72
+ def heartbeat
73
+ raise NotImplementedError
74
+ end
75
+
76
+ def get_player_status(id, public_key = nil)
77
+ id = normalize_id(id)
78
+ page = agent.get("http://ow.live.nicovideo.jp/api/getplayerstatus?locale=GLOBAL&lang=ja%2Djp&v=#{id}&seat%5Flocale=JP")
79
+ if page.body[0] == 'c' # encrypted
80
+ page = Nokogiri::XML(decrypt_encrypted_player_status(page.body, public_key))
81
+ end
82
+
83
+ status = page.at('getplayerstatus')
84
+
85
+ if status['status'] == 'fail'
86
+ error = page.at('error code').inner_text
87
+
88
+ case error
89
+ when 'notlogin'
90
+ return {error: :not_logged_in}
91
+ when 'comingsoon'
92
+ return {error: :not_yet_started}
93
+ when 'closed'
94
+ return {error: :closed}
95
+ when 'require_accept_print_timeshift_ticket'
96
+ return {error: :reservation_not_accepted}
97
+ when 'timeshift_ticket_expire'
98
+ return {error: :reservation_expired}
99
+ when 'noauth'
100
+ return {error: :archive_closed}
101
+ when 'notfound'
102
+ return {error: :not_found}
103
+ else
104
+ return {error: error}
105
+ end
106
+ end
107
+
108
+ result = {}
109
+
110
+ # Strings
111
+ %w(id title description provider_type owner_name
112
+ bourbon_url full_video kickout_video).each do |key|
113
+ item = status.at(key)
114
+ result[key.to_sym] = item.inner_text if item
115
+ end
116
+
117
+ # Integers
118
+ %w(watch_count comment_count owner_id watch_count comment_count).each do |key|
119
+ item = status.at(key)
120
+ result[key.to_sym] = item.inner_text.to_i if item
121
+ end
122
+
123
+ # Flags
124
+ %w(is_premium is_reserved is_owner international is_rerun_stream is_archiveplayserver
125
+ archive allow_netduetto
126
+ is_nonarchive_timeshift_enabled is_timeshift_reserved).each do |key|
127
+ item = status.at(key)
128
+ result[key.sub(/^is_/,'').concat('?').to_sym] = item.inner_text == '1' if item
129
+ end
130
+
131
+ # Datetimes
132
+ %w(base_time open_time start_time end_time).each do |key|
133
+ item = status.at(key)
134
+ result[key.to_sym] = Time.at(item.inner_text.to_i) if item
135
+ end
136
+
137
+ rtmp = status.at('rtmp')
138
+ result[:rtmp] = {
139
+ url: rtmp.at('url').inner_text,
140
+ ticket: rtmp.at('ticket').inner_text,
141
+ }
142
+
143
+ ms = status.at('ms')
144
+ result[:ms] = {
145
+ address: ms.at('addr').inner_text,
146
+ port: ms.at('port').inner_text.to_i,
147
+ thread: ms.at('thread').inner_text,
148
+ }
149
+
150
+ quesheet = status.search('quesheet que')
151
+ result[:quesheet] = quesheet.map do |que|
152
+ {vpos: que['vpos'].to_i, mail: que['mail'], name: que['name'], body: que.inner_text}
153
+ end
154
+
155
+ result
156
+ end
157
+
158
+ def watching_reservations
159
+ page = agent.get(URL_WATCHINGRESERVATION_LIST)
160
+ page.search('vid').map(&:inner_text).map{ |_| normalize_id(_) }
161
+ end
162
+
163
+ 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]
167
+
168
+ page = agent.post("http://live.nicovideo.jp/api/watchingreservation",
169
+ accept: 'true', mode: 'use', vid: id, token: token)
170
+
171
+ page.at('nicolive_video_response')['status'] == 'ok'
172
+ end
173
+
174
+ def decrypt_encrypted_player_status(body, public_key)
175
+ unless public_key
176
+ raise NoPublicKeyProvided,
177
+ 'You should provide proper public key to decrypt ' \
178
+ 'encrypted player status'
179
+ end
180
+
181
+ lines = body.lines
182
+ pubkey = OpenSSL::PKey::RSA.new(public_key)
183
+
184
+ encrypted_shared_key = lines[1].unpack('m*')[0]
185
+ shared_key_raw = pubkey.public_decrypt(encrypted_shared_key)
186
+ shared_key = shared_key_raw.unpack('L>*')[0].to_s
187
+
188
+ cipher = OpenSSL::Cipher.new('bf-ecb').decrypt
189
+ cipher.padding = 0
190
+ cipher.key_len = shared_key.size
191
+ cipher.key = shared_key
192
+
193
+ encrypted_body = lines[2].unpack('m*')[0]
194
+
195
+ body = cipher.update(encrypted_body) + cipher.final
196
+ body.force_encoding('utf-8')
197
+ 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
+ end
211
+ end
212
+ end
@@ -1,3 +1,3 @@
1
1
  class Niconico
2
- VERSION = "1.3.0"
2
+ VERSION = "1.4.0"
3
3
  end
@@ -1,5 +1,6 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  require 'json'
3
+ require 'niconico/deferrable'
3
4
 
4
5
  class Niconico
5
6
  def video(video_id)
@@ -8,35 +9,29 @@ class Niconico
8
9
  end
9
10
 
10
11
  class Video
11
- DEFERRABLES = [:id, :title, :url, :video_url, :type, :tags, :mylist_comment, :description, :description_raw]
12
- DEFERRABLES_VAR = DEFERRABLES.map{|k| :"@#{k}" }
12
+ include Niconico::Deferrable
13
13
 
14
- DEFERRABLES.zip(DEFERRABLES_VAR).each do |(k,i)|
15
- define_method(k) do
16
- instance_variable_get(i) || (get && instance_variable_get(i))
17
- end
18
- end
14
+ deferrable :id, :title,
15
+ :description, :description_raw,
16
+ :url, :video_url, :type,
17
+ :tags, :mylist_comment
19
18
 
20
19
  def initialize(parent, video_id, defer=nil)
21
20
  @parent = parent
22
21
  @agent = parent.agent
23
22
  @fetched = false
24
23
  @thread_id = @id = video_id
24
+ @page = nil
25
25
  @url = "#{Niconico::URL[:watch]}#{@id}"
26
26
 
27
27
  if defer
28
- defer.each do |k,v|
29
- next unless DEFERRABLES.include?(k)
30
- instance_variable_set :"@#{k}", v
31
- end
32
- @page = nil
28
+ preload_deffered_values(defer)
33
29
  else
34
- @page = get()
30
+ get()
35
31
  end
36
32
  end
37
33
 
38
34
  def economy?; @eco; end
39
- def fetched?; @fetched; end
40
35
 
41
36
  def get(options = {})
42
37
  begin
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.3.0
4
+ version: 1.4.0
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: 2014-04-07 00:00:00.000000000 Z
11
+ date: 2014-05-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: mechanize
@@ -52,6 +52,9 @@ files:
52
52
  - Rakefile
53
53
  - lib/niconico.rb
54
54
  - lib/niconico/channel.rb
55
+ - lib/niconico/deferrable.rb
56
+ - lib/niconico/live.rb
57
+ - lib/niconico/live/api.rb
55
58
  - lib/niconico/mylist.rb
56
59
  - lib/niconico/ranking.rb
57
60
  - lib/niconico/version.rb
@@ -76,9 +79,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
76
79
  version: '0'
77
80
  requirements: []
78
81
  rubyforge_project:
79
- rubygems_version: 2.2.0
82
+ rubygems_version: 2.2.2
80
83
  signing_key:
81
84
  specification_version: 4
82
85
  summary: wrapper of Mechanize, optimized for nicovideo.
83
86
  test_files: []
84
- has_rdoc: