niconico 1.3.0 → 1.4.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
  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: