bilibili_sunday 0.0.1
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.
- data/.gitignore +18 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +22 -0
- data/README.md +29 -0
- data/Rakefile +1 -0
- data/bilibili_sunday.gemspec +28 -0
- data/bin/bsdownload +10 -0
- data/bin/bsdv +63 -0
- data/bin/bsmonitor +60 -0
- data/bin/bsserver +7 -0
- data/lib/bilibili_sunday/cacher.rb +42 -0
- data/lib/bilibili_sunday/client.rb +81 -0
- data/lib/bilibili_sunday/downloader.rb +312 -0
- data/lib/bilibili_sunday/server.rb +145 -0
- data/lib/bilibili_sunday/version.rb +3 -0
- data/lib/bilibili_sunday.rb +6 -0
- metadata +162 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Jiahao Li
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# BilibiliSunday
|
2
|
+
|
3
|
+
TODO: Write a gem description
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'bilibili_sunday'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install bilibili_sunday
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
TODO: Write usage instructions here
|
22
|
+
|
23
|
+
## Contributing
|
24
|
+
|
25
|
+
1. Fork it
|
26
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
28
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
29
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'bilibili_sunday/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "bilibili_sunday"
|
8
|
+
spec.version = BilibiliSunday::VERSION
|
9
|
+
spec.authors = ["Jiahao Li"]
|
10
|
+
spec.email = ["isundaylee.reg@gmail.com"]
|
11
|
+
spec.description = %q{A Bilibili downloader! }
|
12
|
+
spec.summary = %q{A Bilibili downloader! }
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
22
|
+
spec.add_development_dependency "rake"
|
23
|
+
|
24
|
+
spec.add_dependency "aria2-ruby"
|
25
|
+
spec.add_dependency "nokogiri"
|
26
|
+
spec.add_dependency "xml-simple"
|
27
|
+
spec.add_dependency "webrick"
|
28
|
+
end
|
data/bin/bsdownload
ADDED
data/bin/bsdv
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#!/usr/bin/env ruby
|
3
|
+
|
4
|
+
require 'bilibili_sunday/client'
|
5
|
+
|
6
|
+
url = ARGV[0]
|
7
|
+
host = ARGV[1]
|
8
|
+
client = BilibiliSunday::Client.new(host)
|
9
|
+
|
10
|
+
cid = client.cid_for_video_url(url)
|
11
|
+
title = client.title_for_video_url(url)
|
12
|
+
videos = client.all_videos
|
13
|
+
|
14
|
+
client.request_cache(cid) unless videos.include?(cid)
|
15
|
+
|
16
|
+
def print_bar(progress)
|
17
|
+
width = @width - 20
|
18
|
+
length = (width * progress).to_i
|
19
|
+
|
20
|
+
print ' '
|
21
|
+
print '#' * length
|
22
|
+
print ' ' * (width - length)
|
23
|
+
print ' '
|
24
|
+
print "%3.6f\%" % (100 * progress)
|
25
|
+
puts
|
26
|
+
end
|
27
|
+
|
28
|
+
def print_status(status)
|
29
|
+
|
30
|
+
puts " #{status['cid']}: #{status['status']} - #{status['path']}"
|
31
|
+
|
32
|
+
status['downloads'].each do |download|
|
33
|
+
print_bar(download['status']['progress'].to_f)
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
while true
|
39
|
+
|
40
|
+
@width = `tput cols`.to_i
|
41
|
+
|
42
|
+
divider = '-' * @width
|
43
|
+
status = client.query_status(cid)
|
44
|
+
|
45
|
+
system('clear')
|
46
|
+
|
47
|
+
puts divider
|
48
|
+
|
49
|
+
puts title
|
50
|
+
|
51
|
+
puts divider
|
52
|
+
|
53
|
+
print_status(status)
|
54
|
+
|
55
|
+
puts divider
|
56
|
+
|
57
|
+
puts "Update Time: #{Time.now}"
|
58
|
+
|
59
|
+
puts divider
|
60
|
+
|
61
|
+
sleep 1
|
62
|
+
|
63
|
+
end
|
data/bin/bsmonitor
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#!/usr/bin/env ruby
|
3
|
+
|
4
|
+
require 'bilibili_sunday/client'
|
5
|
+
|
6
|
+
def print_bar(progress)
|
7
|
+
width = @width - 20
|
8
|
+
length = (width * progress).to_i
|
9
|
+
|
10
|
+
print ' '
|
11
|
+
print '#' * length
|
12
|
+
print ' ' * (width - length)
|
13
|
+
print ' '
|
14
|
+
print "%3.6f\%" % (100 * progress)
|
15
|
+
puts
|
16
|
+
end
|
17
|
+
|
18
|
+
def print_status(status)
|
19
|
+
|
20
|
+
puts " #{status['cid']}: #{status['status']} - #{status['path']}"
|
21
|
+
|
22
|
+
status['downloads'].each do |download|
|
23
|
+
print_bar(download['status']['progress'].to_f)
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
host = ARGV[0]
|
29
|
+
client = BilibiliSunday::Client.new(host)
|
30
|
+
|
31
|
+
while true
|
32
|
+
|
33
|
+
@width = `tput cols`.to_i
|
34
|
+
|
35
|
+
divider = '-' * @width
|
36
|
+
|
37
|
+
videos_list = client.all_videos.map { |x| x.to_i }
|
38
|
+
|
39
|
+
statuses = []
|
40
|
+
|
41
|
+
videos_list.each do |cid|
|
42
|
+
statuses << client.query_status(cid)
|
43
|
+
end
|
44
|
+
|
45
|
+
system('clear')
|
46
|
+
|
47
|
+
puts divider
|
48
|
+
|
49
|
+
statuses.each do |status|
|
50
|
+
print_status(status)
|
51
|
+
puts divider
|
52
|
+
end
|
53
|
+
|
54
|
+
puts "Update Time: #{Time.now}"
|
55
|
+
|
56
|
+
puts divider
|
57
|
+
|
58
|
+
sleep 1
|
59
|
+
|
60
|
+
end
|
data/bin/bsserver
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
module BilibiliSunday
|
2
|
+
|
3
|
+
class Cacher
|
4
|
+
|
5
|
+
require 'fileutils'
|
6
|
+
require 'open-uri'
|
7
|
+
require 'digest/md5'
|
8
|
+
|
9
|
+
def initialize(dir)
|
10
|
+
@dir = dir
|
11
|
+
|
12
|
+
FileUtils.mkdir_p(@dir)
|
13
|
+
end
|
14
|
+
|
15
|
+
def read_url(url)
|
16
|
+
cached?(url) ?
|
17
|
+
cached_content(url) :
|
18
|
+
write_cache_for_url(url, open(url).read)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def cached?(url)
|
24
|
+
File.exists?(cache_path_for_url(url))
|
25
|
+
end
|
26
|
+
|
27
|
+
def cached_content(url)
|
28
|
+
File.open(cache_path_for_url(url)) { |f| f.read }
|
29
|
+
end
|
30
|
+
|
31
|
+
def write_cache_for_url(url, content)
|
32
|
+
File.open(cache_path_for_url(url), 'w') { |f| f.write(content) }
|
33
|
+
content
|
34
|
+
end
|
35
|
+
|
36
|
+
def cache_path_for_url(url)
|
37
|
+
File.join(@dir, Digest::MD5.hexdigest(url))
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'bilibili_sunday'
|
2
|
+
|
3
|
+
module BilibiliSunday
|
4
|
+
|
5
|
+
require 'base64'
|
6
|
+
require 'json'
|
7
|
+
|
8
|
+
class Client
|
9
|
+
|
10
|
+
DEFAULT_HOST = '127.0.0.1'
|
11
|
+
DEFAULT_PORT = 10753
|
12
|
+
|
13
|
+
def initialize(host = nil, port = nil, working_dir = nil)
|
14
|
+
@host = host || DEFAULT_HOST
|
15
|
+
@port = port || DEFAULT_PORT
|
16
|
+
end
|
17
|
+
|
18
|
+
def cid_for_video_url(url)
|
19
|
+
rpc_call('cid_for_video_url', [url])
|
20
|
+
end
|
21
|
+
|
22
|
+
def title_for_video_url(url)
|
23
|
+
rpc_call('title_for_video_url', [url])
|
24
|
+
end
|
25
|
+
|
26
|
+
def request_cache(cid)
|
27
|
+
rpc_call('request_cache', [cid])
|
28
|
+
end
|
29
|
+
|
30
|
+
def all_videos
|
31
|
+
rpc_call('all_videos', [])
|
32
|
+
end
|
33
|
+
|
34
|
+
def active_videos
|
35
|
+
rpc_call('active_videos', [])
|
36
|
+
end
|
37
|
+
|
38
|
+
def query_status(cid)
|
39
|
+
rpc_call('query_status', [cid])
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def get(url, params = {})
|
45
|
+
uri = URI.parse(url)
|
46
|
+
|
47
|
+
uri.query = URI.encode_www_form(params)
|
48
|
+
|
49
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
50
|
+
request = Net::HTTP::Get.new(uri.request_uri)
|
51
|
+
|
52
|
+
response = http.request(request)
|
53
|
+
|
54
|
+
{
|
55
|
+
'code' => response.code.to_i,
|
56
|
+
'body' => response.body
|
57
|
+
}
|
58
|
+
end
|
59
|
+
|
60
|
+
def rpc_path
|
61
|
+
"http://#{@host}:#{@port}/jsonrpc"
|
62
|
+
end
|
63
|
+
|
64
|
+
def rpc_call(method, params)
|
65
|
+
method = "bilibili_sunday.#{method}"
|
66
|
+
id = 'bilibili_sunday_client'
|
67
|
+
params_encoded = Base64.encode64(JSON.generate(params))
|
68
|
+
|
69
|
+
response = get("#{rpc_path}", {'method' => method, 'id' => id, 'params' => params_encoded})
|
70
|
+
answer = JSON.parse(response['body'])
|
71
|
+
|
72
|
+
if response['code'] == 200
|
73
|
+
answer['result']
|
74
|
+
else
|
75
|
+
raise "BilibiliSunday Server error #{answer['error']['code'].to_i}: #{answer['error']['message']}"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
@@ -0,0 +1,312 @@
|
|
1
|
+
require 'bilibili_sunday/cacher'
|
2
|
+
|
3
|
+
module BilibiliSunday
|
4
|
+
|
5
|
+
class Downloader
|
6
|
+
|
7
|
+
require 'fileutils'
|
8
|
+
require 'yaml'
|
9
|
+
require 'digest/md5'
|
10
|
+
require 'aria2'
|
11
|
+
require 'nokogiri'
|
12
|
+
require 'xmlsimple'
|
13
|
+
require 'uri'
|
14
|
+
require 'open-uri'
|
15
|
+
require 'logger'
|
16
|
+
|
17
|
+
def initialize(work_path = nil, downloader = nil, logger = nil)
|
18
|
+
@work_path = File.expand_path(work_path || '~/.bilibili_sunday')
|
19
|
+
@downloader = downloader || Aria2::Downloader.new
|
20
|
+
@logger = logger || Logger.new($stdout)
|
21
|
+
@cacher = BilibiliSunday::Cacher.new(cacher_store_path)
|
22
|
+
|
23
|
+
FileUtils.mkdir_p(@work_path)
|
24
|
+
end
|
25
|
+
|
26
|
+
def routine_work
|
27
|
+
@logger.info 'Carrying out routine work. '
|
28
|
+
|
29
|
+
videos = all_videos
|
30
|
+
|
31
|
+
videos.each do |cid|
|
32
|
+
update_status(cid)
|
33
|
+
concat(cid) if (cache_completed?(cid) && (!concat_started?(cid)))
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def query_status(cid)
|
38
|
+
status = :unknown
|
39
|
+
status = :caching if cache_in_progress?(cid)
|
40
|
+
status = :concatenating if concat_in_progress?(cid)
|
41
|
+
status = :complete if concat_completed?(cid)
|
42
|
+
|
43
|
+
{
|
44
|
+
cid: cid,
|
45
|
+
status: status,
|
46
|
+
downloads: load_yaml(status_yaml_path(cid)) || [],
|
47
|
+
path: concat_completed?(cid) ? concat_output_file_path(cid) : nil
|
48
|
+
}
|
49
|
+
end
|
50
|
+
|
51
|
+
def request_cache(cid)
|
52
|
+
cache(cid)
|
53
|
+
end
|
54
|
+
|
55
|
+
def all_videos
|
56
|
+
Dir.glob(File.join(video_store_path, '*')).select {|f| File.directory? f}.map { |f| File.basename(f).to_i }
|
57
|
+
end
|
58
|
+
|
59
|
+
def cid_for_video_url(url)
|
60
|
+
doc = Nokogiri::HTML(gzip_inflate(@cacher.read_url(url)))
|
61
|
+
|
62
|
+
doc.css('.scontent').each do |i|
|
63
|
+
res = /cid=([0-9]*)/.match(i.to_s)
|
64
|
+
|
65
|
+
if res && res[1]
|
66
|
+
return res[1].to_i
|
67
|
+
else
|
68
|
+
raise "Not a valid Bilibili video page URL. "
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
raise "Not a valid Bilibili video page URL. "
|
73
|
+
end
|
74
|
+
|
75
|
+
def title_for_video_url(url)
|
76
|
+
doc = Nokogiri::HTML(gzip_inflate(@cacher.read_url(url)))
|
77
|
+
|
78
|
+
doc.css('meta').each do |i|
|
79
|
+
return i['content'] if i['name'] == 'title'
|
80
|
+
end
|
81
|
+
|
82
|
+
raise "Not a valid Bilibili video page URL. "
|
83
|
+
end
|
84
|
+
|
85
|
+
def active_videos
|
86
|
+
all_videos.select { |i| !concat_completed?(i)}
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def gzip_inflate(string)
|
92
|
+
# TODO Ugly workaround...
|
93
|
+
begin
|
94
|
+
Zlib::GzipReader.new(StringIO.new(string)).read
|
95
|
+
rescue
|
96
|
+
string
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def update_status(cid)
|
101
|
+
# If download has not started yet, skip updating status.
|
102
|
+
return unless cache_started?(cid)
|
103
|
+
|
104
|
+
# If download already completed, skip updating status.
|
105
|
+
return if cache_completed?(cid)
|
106
|
+
|
107
|
+
downloads = load_yaml(downloads_yaml_path(cid))
|
108
|
+
old_status = load_yaml(status_yaml_path(cid))
|
109
|
+
|
110
|
+
status = []
|
111
|
+
incomplete = false
|
112
|
+
|
113
|
+
downloads.each_with_index do |download, order|
|
114
|
+
# If already completed, stop updating status.
|
115
|
+
if old_status
|
116
|
+
last_status = old_status[order][:status]
|
117
|
+
if last_status['status'] == 'complete'
|
118
|
+
download[:status] = last_status
|
119
|
+
status << download
|
120
|
+
next
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
download[:status] = @downloader.query_status(download[:download_id])
|
125
|
+
incomplete = true unless download[:status]['status'] == 'complete'
|
126
|
+
status << download
|
127
|
+
end
|
128
|
+
|
129
|
+
write_yaml(status_yaml_path(cid), status)
|
130
|
+
mark_cache_complete(cid) unless incomplete
|
131
|
+
end
|
132
|
+
|
133
|
+
def load_yaml(path)
|
134
|
+
File.exists?(path) ?
|
135
|
+
YAML.load(File.open(path) { |f| f.read }) :
|
136
|
+
nil
|
137
|
+
end
|
138
|
+
|
139
|
+
def write_yaml(path, obj)
|
140
|
+
FileUtils.mkdir_p(File.dirname(path))
|
141
|
+
File.open(path, 'w') { |f| f.write(YAML.dump(obj))}
|
142
|
+
end
|
143
|
+
|
144
|
+
def yaml_exists?(rel_path)
|
145
|
+
File.exists?(rel_path)
|
146
|
+
end
|
147
|
+
|
148
|
+
def fetch_filelist(cid)
|
149
|
+
url = "http://interface.bilibili.tv/v_cdn_play?cid=#{cid}"
|
150
|
+
xml = XmlSimple::xml_in(@cacher.read_url(url))
|
151
|
+
|
152
|
+
filelist = [''] * xml['durl'].length
|
153
|
+
|
154
|
+
xml['durl'].each do |file|
|
155
|
+
order = file['order'][0].to_i
|
156
|
+
url = file['url'][0]
|
157
|
+
filelist[order - 1] = url
|
158
|
+
end
|
159
|
+
|
160
|
+
filelist
|
161
|
+
end
|
162
|
+
|
163
|
+
def mark_cache_complete(cid)
|
164
|
+
write_yaml(downloaded_yaml_path(cid), true)
|
165
|
+
end
|
166
|
+
|
167
|
+
def mark_concat_complete(cid)
|
168
|
+
write_yaml(concatenated_yaml_path(cid), true)
|
169
|
+
end
|
170
|
+
|
171
|
+
def video_store_path
|
172
|
+
File.join(@work_path, "store")
|
173
|
+
end
|
174
|
+
|
175
|
+
def video_path(cid)
|
176
|
+
File.join(video_store_path, "#{cid}")
|
177
|
+
end
|
178
|
+
|
179
|
+
def cache_started?(cid)
|
180
|
+
yaml_exists?(downloads_yaml_path(cid))
|
181
|
+
end
|
182
|
+
|
183
|
+
def cache_completed?(cid)
|
184
|
+
yaml_exists?(downloaded_yaml_path(cid))
|
185
|
+
end
|
186
|
+
|
187
|
+
def cache_in_progress?(cid)
|
188
|
+
cache_started?(cid) && (!cache_completed?(cid))
|
189
|
+
end
|
190
|
+
|
191
|
+
def concat_started?(cid)
|
192
|
+
yaml_exists?(ffmpeg_concat_input_path(cid))
|
193
|
+
end
|
194
|
+
|
195
|
+
def concat_completed?(cid)
|
196
|
+
yaml_exists?(concatenated_yaml_path(cid))
|
197
|
+
end
|
198
|
+
|
199
|
+
def concat_in_progress?(cid)
|
200
|
+
concat_started?(cid) && (!concat_completed?(cid))
|
201
|
+
end
|
202
|
+
|
203
|
+
def downloads_yaml_path(cid)
|
204
|
+
File.join(video_path(cid), 'downloads.yml')
|
205
|
+
end
|
206
|
+
|
207
|
+
def downloaded_yaml_path(cid)
|
208
|
+
File.join(video_path(cid), 'downloaded.yml')
|
209
|
+
end
|
210
|
+
|
211
|
+
def concatenated_yaml_path(cid)
|
212
|
+
File.join(video_path(cid), 'concatenated.yaml')
|
213
|
+
end
|
214
|
+
|
215
|
+
def cacher_store_path
|
216
|
+
File.join(@work_path, 'cache')
|
217
|
+
end
|
218
|
+
|
219
|
+
def video_ext(cid)
|
220
|
+
# TODO better way of identifying file types
|
221
|
+
downloads = load_yaml(downloads_yaml_path(cid))
|
222
|
+
ext = File.extname(downloads[0][:path])
|
223
|
+
ext && !ext.empty? ?
|
224
|
+
ext :
|
225
|
+
'.flv'
|
226
|
+
end
|
227
|
+
|
228
|
+
def fix_extname(extname)
|
229
|
+
return '.flv' if extname == '.hlv'
|
230
|
+
extname
|
231
|
+
end
|
232
|
+
|
233
|
+
def concat_output_file_path(cid)
|
234
|
+
File.join(video_path(cid), "entirety#{video_ext(cid)}")
|
235
|
+
end
|
236
|
+
|
237
|
+
def status_yaml_path(cid)
|
238
|
+
File.join(video_path(cid), 'status.yaml')
|
239
|
+
end
|
240
|
+
|
241
|
+
def ffmpeg_concat_input_path(cid)
|
242
|
+
File.join(video_path(cid), 'concat_list')
|
243
|
+
end
|
244
|
+
|
245
|
+
def cache(cid)
|
246
|
+
return false if cache_started?(cid)
|
247
|
+
|
248
|
+
FileUtils.mkdir_p(video_path(cid))
|
249
|
+
|
250
|
+
filelist = fetch_filelist(cid)
|
251
|
+
downloads = []
|
252
|
+
|
253
|
+
filelist.each_with_index do |url, order|
|
254
|
+
to_name = "#{order}#{fix_extname(File.extname(URI.parse(url).path))}"
|
255
|
+
to_path = File.join(video_path(cid), to_name)
|
256
|
+
|
257
|
+
downloads << {
|
258
|
+
order: order,
|
259
|
+
url: url,
|
260
|
+
download_id: @downloader.download(url, to_path),
|
261
|
+
path: to_path
|
262
|
+
}
|
263
|
+
end
|
264
|
+
|
265
|
+
write_yaml(downloads_yaml_path(cid), downloads)
|
266
|
+
|
267
|
+
true
|
268
|
+
end
|
269
|
+
|
270
|
+
def write_ffmpeg_concat_input_file(cid)
|
271
|
+
downloads = load_yaml(downloads_yaml_path(cid))
|
272
|
+
|
273
|
+
File.open(ffmpeg_concat_input_path(cid), 'w') do |f|
|
274
|
+
downloads.sort_by! { |f| f[:order] }
|
275
|
+
|
276
|
+
downloads.each do |v|
|
277
|
+
f.puts "file '#{v[:path]}'"
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
def remove_ffmpeg_concat_input_file(cid)
|
283
|
+
FileUtils.rm(ffmpeg_concat_input_path(cid))
|
284
|
+
end
|
285
|
+
|
286
|
+
def concat(cid)
|
287
|
+
return false if concat_started?(cid)
|
288
|
+
|
289
|
+
unless cache_completed?(cid)
|
290
|
+
raise "Cannot concat clips. Downloads not completed. "
|
291
|
+
end
|
292
|
+
|
293
|
+
write_ffmpeg_concat_input_file(cid)
|
294
|
+
|
295
|
+
Thread.start(cid) do |cid|
|
296
|
+
ffmpeg = "ffmpeg"
|
297
|
+
command = "\"#{ffmpeg}\" -f concat -i \"#{ffmpeg_concat_input_path(cid)}\" -c copy \"#{concat_output_file_path(cid)}\""
|
298
|
+
|
299
|
+
system(command)
|
300
|
+
|
301
|
+
if $? == 0
|
302
|
+
mark_concat_complete(cid)
|
303
|
+
else
|
304
|
+
remove_ffmpeg_concat_input_file(cid)
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
end
|
309
|
+
|
310
|
+
end
|
311
|
+
|
312
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
require 'bilibili_sunday/downloader'
|
2
|
+
require 'webrick'
|
3
|
+
|
4
|
+
module BilibiliSunday
|
5
|
+
|
6
|
+
require 'base64'
|
7
|
+
require 'json'
|
8
|
+
|
9
|
+
class RequestHandler
|
10
|
+
|
11
|
+
def initialize(downloader)
|
12
|
+
@downloader = downloader
|
13
|
+
end
|
14
|
+
|
15
|
+
def handle_request(method, params)
|
16
|
+
begin
|
17
|
+
result = /^bilibili_sunday.(.*?)$/.match(method)
|
18
|
+
|
19
|
+
return handle_error(1, 'No matching method. ') unless result
|
20
|
+
|
21
|
+
method = result[1]
|
22
|
+
|
23
|
+
if method == 'cid_for_video_url'
|
24
|
+
handle_cid_for_video_url(params[0])
|
25
|
+
elsif method == 'title_for_video_url'
|
26
|
+
handle_title_for_video_url(params[0])
|
27
|
+
elsif method == 'request_cache'
|
28
|
+
handle_request_cache(params[0].to_i)
|
29
|
+
elsif method == 'query_status'
|
30
|
+
handle_query_status(params[0].to_i)
|
31
|
+
elsif method == 'all_videos'
|
32
|
+
handle_all_videos
|
33
|
+
elsif method == 'active_videos'
|
34
|
+
handle_active_videos
|
35
|
+
else
|
36
|
+
handle_error(1, 'No matching method. ')
|
37
|
+
end
|
38
|
+
rescue
|
39
|
+
return handle_error(2, 'Internal server error. ')
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def handle_cid_for_video_url(url)
|
44
|
+
return 200, {result: @downloader.cid_for_video_url(url)}
|
45
|
+
end
|
46
|
+
|
47
|
+
def handle_title_for_video_url(url)
|
48
|
+
return 200, {result: @downloader.title_for_video_url(url)}
|
49
|
+
end
|
50
|
+
|
51
|
+
def handle_request_cache(cid)
|
52
|
+
return 200, {result: @downloader.request_cache(cid)}
|
53
|
+
end
|
54
|
+
|
55
|
+
def handle_query_status(cid)
|
56
|
+
return 200, {result: @downloader.query_status(cid)}
|
57
|
+
end
|
58
|
+
|
59
|
+
def handle_all_videos
|
60
|
+
return 200, {result: @downloader.all_videos}
|
61
|
+
end
|
62
|
+
|
63
|
+
def handle_active_videos
|
64
|
+
return 200, {result: @downloader.active_videos}
|
65
|
+
end
|
66
|
+
|
67
|
+
def handle_error(error_code, error_message)
|
68
|
+
return 500, {error: {code: error_code, message: error_message}}
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
class Servlet < WEBrick::HTTPServlet::AbstractServlet
|
74
|
+
|
75
|
+
def initialize(server, downloader)
|
76
|
+
super(server)
|
77
|
+
@handler = RequestHandler.new(downloader)
|
78
|
+
end
|
79
|
+
|
80
|
+
def do_GET(request, response)
|
81
|
+
id = request.query["id"]
|
82
|
+
method = request.query["method"]
|
83
|
+
params = JSON.parse(Base64.decode64(request.query["params"]))
|
84
|
+
|
85
|
+
code, result = @handler.handle_request(method, params)
|
86
|
+
result[:id] = id
|
87
|
+
result[:jsonrpc] = '2.0'
|
88
|
+
|
89
|
+
response.status = code
|
90
|
+
response['Content-Type'] = 'application/json'
|
91
|
+
response['Access-Control-Allow-Origin'] = '*'
|
92
|
+
response.body = result.to_json
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
class Server
|
98
|
+
|
99
|
+
require 'json'
|
100
|
+
|
101
|
+
DEFAULT_PORT = 10753
|
102
|
+
|
103
|
+
def initialize(port = DEFAULT_PORT, working_dir = nil)
|
104
|
+
@port = port
|
105
|
+
@downloader = Downloader.new(working_dir || File.expand_path("~/.bilibili_sunday"))
|
106
|
+
end
|
107
|
+
|
108
|
+
def start
|
109
|
+
@running = true
|
110
|
+
|
111
|
+
@downloader_thread = Thread.new do
|
112
|
+
while true
|
113
|
+
@downloader.routine_work
|
114
|
+
sleep 1
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
@rpc_server_thread = Thread.new do
|
119
|
+
begin
|
120
|
+
server = WEBrick::HTTPServer.new(:Port => @port)
|
121
|
+
server.mount "/jsonrpc", Servlet, @downloader
|
122
|
+
server.start
|
123
|
+
rescue
|
124
|
+
ensure
|
125
|
+
server.shutdown
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
while true
|
130
|
+
unless @running
|
131
|
+
@downloader_thread.terminate
|
132
|
+
@rpc_server_thread.terminate
|
133
|
+
break
|
134
|
+
end
|
135
|
+
sleep 1
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def stop
|
140
|
+
@running = false
|
141
|
+
end
|
142
|
+
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
metadata
ADDED
@@ -0,0 +1,162 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: bilibili_sunday
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Jiahao Li
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-11-23 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: bundler
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '1.3'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '1.3'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rake
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: aria2-ruby
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: nokogiri
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :runtime
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: xml-simple
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :runtime
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: webrick
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
type: :runtime
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ! '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
description: ! 'A Bilibili downloader! '
|
111
|
+
email:
|
112
|
+
- isundaylee.reg@gmail.com
|
113
|
+
executables:
|
114
|
+
- bsdownload
|
115
|
+
- bsdv
|
116
|
+
- bsmonitor
|
117
|
+
- bsserver
|
118
|
+
extensions: []
|
119
|
+
extra_rdoc_files: []
|
120
|
+
files:
|
121
|
+
- .gitignore
|
122
|
+
- Gemfile
|
123
|
+
- LICENSE.txt
|
124
|
+
- README.md
|
125
|
+
- Rakefile
|
126
|
+
- bilibili_sunday.gemspec
|
127
|
+
- bin/bsdownload
|
128
|
+
- bin/bsdv
|
129
|
+
- bin/bsmonitor
|
130
|
+
- bin/bsserver
|
131
|
+
- lib/bilibili_sunday.rb
|
132
|
+
- lib/bilibili_sunday/cacher.rb
|
133
|
+
- lib/bilibili_sunday/client.rb
|
134
|
+
- lib/bilibili_sunday/downloader.rb
|
135
|
+
- lib/bilibili_sunday/server.rb
|
136
|
+
- lib/bilibili_sunday/version.rb
|
137
|
+
homepage: ''
|
138
|
+
licenses:
|
139
|
+
- MIT
|
140
|
+
post_install_message:
|
141
|
+
rdoc_options: []
|
142
|
+
require_paths:
|
143
|
+
- lib
|
144
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
145
|
+
none: false
|
146
|
+
requirements:
|
147
|
+
- - ! '>='
|
148
|
+
- !ruby/object:Gem::Version
|
149
|
+
version: '0'
|
150
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
151
|
+
none: false
|
152
|
+
requirements:
|
153
|
+
- - ! '>='
|
154
|
+
- !ruby/object:Gem::Version
|
155
|
+
version: '0'
|
156
|
+
requirements: []
|
157
|
+
rubyforge_project:
|
158
|
+
rubygems_version: 1.8.24
|
159
|
+
signing_key:
|
160
|
+
specification_version: 3
|
161
|
+
summary: A Bilibili downloader!
|
162
|
+
test_files: []
|