hktv 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +64 -0
- data/Rakefile +2 -0
- data/bin/hktv +4 -0
- data/hktv.gemspec +31 -0
- data/lib/hktv.rb +241 -0
- data/lib/hktv/command.rb +183 -0
- data/lib/hktv/version.rb +3 -0
- data/spec/cassettes/HKTV/with_authentication/_ott_token/should_retrieve_a_token.yml +140 -0
- data/spec/cassettes/HKTV/with_authentication/_playlist/should_find_playlist_for_a_video.yml +42 -0
- data/spec/cassettes/HKTV/with_no_token_and_auth/_features/should_list_feature.yml +274 -0
- data/spec/cassettes/HKTV/with_no_token_and_auth/_ott_token/should_retrieve_a_token.yml +81 -0
- data/spec/cassettes/HKTV/with_no_token_and_auth/_playlist/should_retrieve_a_playlist.yml +81 -0
- data/spec/cassettes/HKTV/with_no_token_and_auth/_programs/should_list_programs.yml +5698 -0
- data/spec/cassettes/auth.yml +52 -0
- data/spec/hktv_spec.rb +123 -0
- data/spec/spec_helper.rb +30 -0
- metadata +217 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 480088f574c5b62191b13bf05b158af33316b6a0
|
4
|
+
data.tar.gz: b8cde4170b9e46f412663028818720cea5a21c51
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: db2ea07db61ffa58cdd23dd7043072bcf02d41f5c03c943c03a33c5cce3b34bfb6e8424118d893c48b881b290af38b16316fded9d69ecf0e3f065defba45f006
|
7
|
+
data.tar.gz: ded7e980b52a9555b8320c573dd02c7231be9d0360e66fe462f967c0f401d25bd0eb9eaa087b48b534016ad2624310a832e5a16624f52f485b4859026b8edc06
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Francis Chong
|
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,64 @@
|
|
1
|
+
# HKTV
|
2
|
+
|
3
|
+
Command line utilities to find and download HKTV videos.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'hktv', github: "siuying/ruby-hktv"
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it via:
|
18
|
+
|
19
|
+
$ gem install hktv
|
20
|
+
|
21
|
+
|
22
|
+
## Prerequisite
|
23
|
+
|
24
|
+
- Ruby 2.x
|
25
|
+
- Only tested on OS X Yosemite, it may work on Linux.
|
26
|
+
- Requries ``ffmpeg`` for combine video.
|
27
|
+
|
28
|
+
## Usage
|
29
|
+
|
30
|
+
### Login to HKTV
|
31
|
+
|
32
|
+
Login to HKTV. This is required for download video.
|
33
|
+
|
34
|
+
$ hktv login
|
35
|
+
|
36
|
+
### List all video of HKTV
|
37
|
+
|
38
|
+
Print a comma sepeated list of programs.
|
39
|
+
|
40
|
+
$ hktv list "選戰"
|
41
|
+
選戰 第1集 第1節
|
42
|
+
選戰 第1集 第2節
|
43
|
+
...
|
44
|
+
選戰 第7集 第3節
|
45
|
+
選戰 第7集 第4節
|
46
|
+
|
47
|
+
### Download an Episode
|
48
|
+
|
49
|
+
Download all video files of an episode, and merge them into single file.
|
50
|
+
|
51
|
+
$ hktv download "選戰 第7集"
|
52
|
+
Downloading: 選戰 第7集 第1節
|
53
|
+
Downloading: 選戰 第7集 第2節
|
54
|
+
Downloading: 選戰 第7集 第3節
|
55
|
+
Downloading: 選戰 第7集 第4節
|
56
|
+
Merge videos into 選戰_第7集.mp4
|
57
|
+
|
58
|
+
## Contributing
|
59
|
+
|
60
|
+
1. Fork it ( https://github.com/siuying/ruby-hktv/fork )
|
61
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
62
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
63
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
64
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
data/bin/hktv
ADDED
data/hktv.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'hktv/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "hktv"
|
8
|
+
spec.version = HKTV::VERSION
|
9
|
+
spec.authors = ["Francis Chong"]
|
10
|
+
spec.email = ["francis@ignition.hk"]
|
11
|
+
spec.summary = %q{Find and download HKTV videos.}
|
12
|
+
spec.description = %q{Command line utilities to find and download HKTV videos.}
|
13
|
+
spec.homepage = "https://github.com/siuying/ruby-hktv"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
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.7"
|
22
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
23
|
+
spec.add_development_dependency "pry", '~> 0'
|
24
|
+
spec.add_development_dependency "rspec", '~> 0'
|
25
|
+
spec.add_development_dependency "vcr", '~> 0'
|
26
|
+
spec.add_development_dependency "webmock", '~> 0'
|
27
|
+
|
28
|
+
spec.add_dependency "httparty", "~> 0.13", '>= 0.13.1'
|
29
|
+
spec.add_dependency "retriable", "~> 1.4", '>= 1.4.1'
|
30
|
+
spec.add_dependency 'commander', "~> 4.2", '>= 4.2.1'
|
31
|
+
end
|
data/lib/hktv.rb
ADDED
@@ -0,0 +1,241 @@
|
|
1
|
+
require_relative "./hktv/version"
|
2
|
+
|
3
|
+
require "httparty"
|
4
|
+
require 'retriable'
|
5
|
+
|
6
|
+
require "digest/md5"
|
7
|
+
require "json"
|
8
|
+
require 'securerandom'
|
9
|
+
|
10
|
+
class HKTV
|
11
|
+
include HTTParty
|
12
|
+
|
13
|
+
# set by auth
|
14
|
+
attr_accessor :access_token, :expires_date, :refresh_token
|
15
|
+
|
16
|
+
# set by token
|
17
|
+
attr_accessor :user_id, :user_level, :ott_token, :ott_expires_date
|
18
|
+
|
19
|
+
base_uri 'webservices.hktv.com.hk'
|
20
|
+
headers "Content_type" => "application/x-www-form-urlencoded", "Accept" => "*/*"
|
21
|
+
|
22
|
+
API_BASE = "/"
|
23
|
+
API_TOKEN = "account/token"
|
24
|
+
API_FEATURE = "lists/getFeature"
|
25
|
+
API_PLAYLIST = "playlist/request"
|
26
|
+
|
27
|
+
API_SECRET = "43e814b31f8764756672c1cd1217d775"
|
28
|
+
API_KI = "12"
|
29
|
+
API_VID = "1"
|
30
|
+
|
31
|
+
API_MUID = "0"
|
32
|
+
API_DEVICE = "USB Android TV"
|
33
|
+
API_MANUF = "hktv-ruby"
|
34
|
+
API_MODEL = "Ruby"
|
35
|
+
API_OS = "0.1.0"
|
36
|
+
API_MX_RES = "1920"
|
37
|
+
API_NETWORK = "fixed"
|
38
|
+
|
39
|
+
# hardcoded user in HKTV app
|
40
|
+
API_USERNAME = "hktv_ios"
|
41
|
+
API_PASSWORD = "H*aK#)HM248"
|
42
|
+
|
43
|
+
def initialize(uuid: SecureRandom.uuid, access_token: nil, expires_date: nil, refresh_token: nil, user_id: "1", user_level: nil, ott_token: nil, ott_expires_date: nil)
|
44
|
+
@uuid = uuid
|
45
|
+
@access_token = access_token
|
46
|
+
@expires_date = expires_date
|
47
|
+
@refresh_token = refresh_token
|
48
|
+
@ott_token = ott_token
|
49
|
+
@ott_expires_date = ott_expires_date
|
50
|
+
@user_id = user_id
|
51
|
+
@user_level = user_level
|
52
|
+
end
|
53
|
+
|
54
|
+
# return true if client has authenticated, false otherwise
|
55
|
+
def authenticated?
|
56
|
+
if @expires_date && Time.now > @expires_date
|
57
|
+
@access_token = nil
|
58
|
+
@expires_date = nil
|
59
|
+
@refresh_token = nil
|
60
|
+
end
|
61
|
+
|
62
|
+
!@access_token.nil?
|
63
|
+
end
|
64
|
+
|
65
|
+
# return true if client needs ott token
|
66
|
+
def needs_ott_token?
|
67
|
+
return @ott_token.nil? || (@ott_expires_date && Time.now > @ott_expires_date)
|
68
|
+
end
|
69
|
+
|
70
|
+
# Authenticate user with given username/password
|
71
|
+
# return true if success
|
72
|
+
def auth(username, password)
|
73
|
+
options = {
|
74
|
+
"grant_type" => "password",
|
75
|
+
"username" => username,
|
76
|
+
"password" => password
|
77
|
+
}
|
78
|
+
|
79
|
+
auth = post_json("https://www.hktvmall.com:443/hktvwebservices/oauth/token?rand=#{Time.now.to_i}", body: options, basic_auth: {username: API_USERNAME, password: API_PASSWORD})
|
80
|
+
@access_token = auth["access_token"]
|
81
|
+
@expires_date = Time.now + auth["expires_in"]
|
82
|
+
@refresh_token = auth["refresh_token"]
|
83
|
+
!@access_token.nil?
|
84
|
+
end
|
85
|
+
|
86
|
+
# get the OTT token
|
87
|
+
def ott_token
|
88
|
+
result = nil
|
89
|
+
|
90
|
+
# hktv api just fail for unknown reason, retry to workaround it
|
91
|
+
Retriable.retriable(tries: 20, base_interval: 1.0) do
|
92
|
+
if authenticated?
|
93
|
+
result = get_json("https://www.hktvmall.com:443/hktvwebservices/v1/hktv/ott/token?rand=#{Time.now.to_i}", headers: headers)
|
94
|
+
else
|
95
|
+
ts = Time.now.to_i
|
96
|
+
options = {
|
97
|
+
"ki" => API_KI,
|
98
|
+
"ts" => ts.to_s,
|
99
|
+
"s" => sign_request(API_TOKEN, ts, [API_KI, API_MUID]),
|
100
|
+
"muid" => API_MUID
|
101
|
+
}
|
102
|
+
result = self.class.post(API_BASE + API_TOKEN, body: options, headers: headers)
|
103
|
+
end
|
104
|
+
|
105
|
+
if result["errors"]
|
106
|
+
raise result["errors"].first["message"]
|
107
|
+
end
|
108
|
+
|
109
|
+
result
|
110
|
+
end
|
111
|
+
|
112
|
+
@user_id = result["user_id"]
|
113
|
+
@user_level = result["user_level"]
|
114
|
+
@ott_token = result["token"]
|
115
|
+
|
116
|
+
!@user_id.nil?
|
117
|
+
end
|
118
|
+
|
119
|
+
def logout
|
120
|
+
@user_id = nil
|
121
|
+
@user_level = nil
|
122
|
+
@ott_token = nil
|
123
|
+
|
124
|
+
@access_token = nil
|
125
|
+
@expires_in = nil
|
126
|
+
@refresh_token = nil
|
127
|
+
|
128
|
+
if authenticated?
|
129
|
+
self.class.post("https://www.hktvmall.com:443/hktvwebservices/v1/customers/current/logout", headers: headers)["success"]
|
130
|
+
else
|
131
|
+
true
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# get a playlist of given video
|
136
|
+
# @param video_id the video ID
|
137
|
+
# @return URL to the playlist
|
138
|
+
def playlist(video_id="1")
|
139
|
+
self.ott_token if needs_ott_token?
|
140
|
+
|
141
|
+
ts = Time.now.to_i
|
142
|
+
signature = sign_request(API_PLAYLIST, ts, [API_DEVICE, API_KI, API_MODEL, API_MANUF, API_MX_RES, API_NETWORK, API_OS, @ott_token, @uuid, @user_id, video_id])
|
143
|
+
options = {
|
144
|
+
"d" => API_DEVICE,
|
145
|
+
"ki" => API_KI,
|
146
|
+
"mdl" => API_MODEL,
|
147
|
+
"mf" => API_MANUF,
|
148
|
+
"mxres" => API_MX_RES,
|
149
|
+
"net" => API_NETWORK,
|
150
|
+
"os" => API_OS,
|
151
|
+
"t" => @ott_token,
|
152
|
+
"udid" => @uuid,
|
153
|
+
"uid" => @user_id,
|
154
|
+
"vid" => video_id,
|
155
|
+
"ts" => ts.to_s,
|
156
|
+
"s" => signature
|
157
|
+
}
|
158
|
+
self.class.post(API_BASE + API_PLAYLIST, body: options)["m3u8"]
|
159
|
+
end
|
160
|
+
|
161
|
+
# list featured video on HKTV
|
162
|
+
# @return nested array of video
|
163
|
+
def features(lang="zh-Hant", count=999)
|
164
|
+
self.ott_token if needs_ott_token?
|
165
|
+
|
166
|
+
ts = Time.now.to_i
|
167
|
+
options = {
|
168
|
+
"lang" => lang,
|
169
|
+
"lim" => count.to_s,
|
170
|
+
"lut" => "0",
|
171
|
+
"_" => ts.to_s,
|
172
|
+
"ofs" => "0"
|
173
|
+
}
|
174
|
+
self.class.get("http://ott-www.hktvmall.com/api/lists/getFeature", query: options)["videos"]
|
175
|
+
end
|
176
|
+
|
177
|
+
# list programs on HKTV
|
178
|
+
# @return nested array of video
|
179
|
+
def programs(lang="zh-Hant", count=999)
|
180
|
+
ts = Time.now.to_i
|
181
|
+
options = {
|
182
|
+
"lang" => lang,
|
183
|
+
"lim" => count.to_s,
|
184
|
+
"lut" => "0",
|
185
|
+
"_" => ts.to_s,
|
186
|
+
"ofs" => "0"
|
187
|
+
}
|
188
|
+
self.class.get("http://ott-www.hktvmall.com/api/lists/getProgram", query: options)["videos"]
|
189
|
+
end
|
190
|
+
|
191
|
+
def to_hash
|
192
|
+
json = {
|
193
|
+
uuid: @uuid,
|
194
|
+
access_token: @access_token,
|
195
|
+
refresh_token: @refresh_token,
|
196
|
+
user_id: @user_id,
|
197
|
+
user_level: @user_level,
|
198
|
+
ott_token: @ott_token
|
199
|
+
}
|
200
|
+
json[:expires_date] = @expires_date.to_i if @expires_date
|
201
|
+
json[:ott_expires_date] = @ott_expires_date.to_i if @ott_expires_date
|
202
|
+
return json
|
203
|
+
end
|
204
|
+
|
205
|
+
def self.from_hash(json)
|
206
|
+
if json[:"expires_date"]
|
207
|
+
json[:expires_date] = Time.at(json[:expires_date])
|
208
|
+
end
|
209
|
+
if json[:ott_expires_date]
|
210
|
+
json[:ott_expires_date] = Time.at(json[:ott_expires_date])
|
211
|
+
end
|
212
|
+
return HKTV.new(json)
|
213
|
+
end
|
214
|
+
|
215
|
+
private
|
216
|
+
def headers
|
217
|
+
if authenticated?
|
218
|
+
return {
|
219
|
+
"Authorization" => "Bearer #{@access_token}"
|
220
|
+
}
|
221
|
+
else
|
222
|
+
return {}
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
def post_json(path, options={})
|
227
|
+
response = self.class.post(path, options)
|
228
|
+
data = response.body
|
229
|
+
JSON.parse(data)
|
230
|
+
end
|
231
|
+
|
232
|
+
def get_json(path, options={})
|
233
|
+
response = self.class.get(path, options)
|
234
|
+
data = response.body
|
235
|
+
JSON.parse(data)
|
236
|
+
end
|
237
|
+
|
238
|
+
def sign_request(path, timestamp, params=[])
|
239
|
+
return Digest::MD5.hexdigest(path + params.join("") + API_SECRET + timestamp.to_s)
|
240
|
+
end
|
241
|
+
end
|
data/lib/hktv/command.rb
ADDED
@@ -0,0 +1,183 @@
|
|
1
|
+
require 'commander'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'csv'
|
4
|
+
require 'open3'
|
5
|
+
require_relative './version'
|
6
|
+
require_relative '../hktv'
|
7
|
+
|
8
|
+
class HKTV
|
9
|
+
class Command
|
10
|
+
include Commander::Methods
|
11
|
+
|
12
|
+
CONFIG_DIR = "#{Dir.home}/.hktv"
|
13
|
+
CONFIG_FILE = "#{Dir.home}/.hktv/hktv.json"
|
14
|
+
|
15
|
+
def load
|
16
|
+
hktv = nil
|
17
|
+
|
18
|
+
if !File.exists?(CONFIG_DIR)
|
19
|
+
FileUtils.mkdir_p(CONFIG_DIR)
|
20
|
+
end
|
21
|
+
|
22
|
+
if File.exists?(CONFIG_FILE)
|
23
|
+
hktv = HKTV.from_hash(JSON(File.open(CONFIG_FILE).read, symbolize_names: true))
|
24
|
+
else
|
25
|
+
hktv = HKTV.new
|
26
|
+
end
|
27
|
+
|
28
|
+
return hktv
|
29
|
+
end
|
30
|
+
|
31
|
+
def save(hktv)
|
32
|
+
if !File.exists?(CONFIG_DIR)
|
33
|
+
FileUtils.mkdir_p(CONFIG_DIR)
|
34
|
+
end
|
35
|
+
|
36
|
+
File.open(CONFIG_FILE, 'w') do |f|
|
37
|
+
f.write(JSON.generate(hktv.to_hash))
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def run
|
42
|
+
program :name, 'hktv'
|
43
|
+
program :version, HKTV::VERSION
|
44
|
+
program :description, 'Lookup HKTV videos'
|
45
|
+
program :help_formatter, :compact
|
46
|
+
|
47
|
+
command :login do |c|
|
48
|
+
c.syntax = 'hktv login'
|
49
|
+
c.description = 'Login to HKTV'
|
50
|
+
c.action do |args, options|
|
51
|
+
username = ask("username: ")
|
52
|
+
password = ask("password: ") { |q| q.echo = "*" }
|
53
|
+
|
54
|
+
hktv = self.load
|
55
|
+
hktv.logout if hktv.authenticated?
|
56
|
+
|
57
|
+
if hktv.auth(username, password)
|
58
|
+
puts "Logged in"
|
59
|
+
self.save(hktv)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
command :list do |c|
|
65
|
+
c.syntax = 'hktv list [episode-title]'
|
66
|
+
c.description = 'Print a comma sepeated list of programs of HKTV'
|
67
|
+
c.option '--keys keys', String, 'Output data keys, by default "title", available: [title, video_id, category, thumbnail, url, duration]'
|
68
|
+
c.option '--playlist', 'Fetch the playlist URL. By default the URL is not fetched.'
|
69
|
+
|
70
|
+
c.action do |args, options|
|
71
|
+
options.default category: "DRAMA", playlist: false, keys: "title"
|
72
|
+
hktv = self.load
|
73
|
+
programs = extract_root_videos(hktv.programs)
|
74
|
+
keys = options.keys.split(",")
|
75
|
+
title = args[0]
|
76
|
+
|
77
|
+
if options.category
|
78
|
+
programs = programs.select {|program| program["category"] == options.category }
|
79
|
+
end
|
80
|
+
|
81
|
+
if title
|
82
|
+
programs = programs.select {|program| program["title"].include?(title) }
|
83
|
+
end
|
84
|
+
|
85
|
+
if options.playlist
|
86
|
+
programs = programs.select {|program| program["url"] = hktv.playlist(program["video_id"]) }
|
87
|
+
end
|
88
|
+
|
89
|
+
if options.playlist && !keys.include?("url") && options.keys == "title,video_id"
|
90
|
+
keys << "url"
|
91
|
+
end
|
92
|
+
|
93
|
+
rows = programs.map do |program|
|
94
|
+
keys.collect do |key|
|
95
|
+
program[key]
|
96
|
+
end.to_csv
|
97
|
+
end
|
98
|
+
|
99
|
+
puts rows.join("")
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
command :download do |c|
|
104
|
+
c.syntax = 'hktv download [episode-title] (output-filename)'
|
105
|
+
c.description = 'Download an episode of HKTV program.'
|
106
|
+
|
107
|
+
c.action do |args, options|
|
108
|
+
hktv = self.load
|
109
|
+
|
110
|
+
unless hktv.authenticated?
|
111
|
+
puts "You have not login! Try \"hktv login\""
|
112
|
+
raise "Not logged in."
|
113
|
+
end
|
114
|
+
|
115
|
+
title = args[0]
|
116
|
+
filename = args[1]
|
117
|
+
raise "Missing episode title" if title.nil?
|
118
|
+
|
119
|
+
filename = filename_with_title(title, ".mp4") if filename.nil?
|
120
|
+
programs = extract_root_videos(hktv.programs)
|
121
|
+
programs = programs.select {|program| program["title"].include?(title) }
|
122
|
+
|
123
|
+
if programs.size == 0
|
124
|
+
puts "No video matching \"#{title}\""
|
125
|
+
raise "Video not found"
|
126
|
+
end
|
127
|
+
|
128
|
+
# fetch playlist url
|
129
|
+
programs.each {|program| program["url"] = hktv.playlist(program["video_id"]) }
|
130
|
+
|
131
|
+
# download playlist and merge them
|
132
|
+
download_and_merge_programs(programs, filename)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
run!
|
137
|
+
end
|
138
|
+
|
139
|
+
private
|
140
|
+
def extract_root_videos(videos)
|
141
|
+
videos.collect do |video|
|
142
|
+
if video["child_nodes"]
|
143
|
+
extract_root_videos(video["child_nodes"])
|
144
|
+
else
|
145
|
+
video
|
146
|
+
end
|
147
|
+
end.flatten
|
148
|
+
end
|
149
|
+
|
150
|
+
def filename_with_title(title, ext=".ts")
|
151
|
+
title.gsub(/\s/, "_").gsub(/\//, "") + ext
|
152
|
+
end
|
153
|
+
|
154
|
+
def download_and_merge_programs(videos, output)
|
155
|
+
temp_files = videos.map {|video| filename_with_title(video["title"]) }
|
156
|
+
failed = false
|
157
|
+
|
158
|
+
begin
|
159
|
+
# download the video and convert them into ts file
|
160
|
+
# https://trac.ffmpeg.org/wiki/Concatenate
|
161
|
+
videos.each do |video|
|
162
|
+
url = video["url"]
|
163
|
+
title = video["title"]
|
164
|
+
puts "Downloading: #{title}"
|
165
|
+
`ffmpeg -i \"#{url}\" -c copy -bsf:v h264_mp4toannexb -f mpegts \"#{filename_with_title(title)}\" 2> /dev/null`
|
166
|
+
if $?.to_i != 0
|
167
|
+
puts "Failed download file."
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
# losslessly merge these ts
|
172
|
+
puts "Merge videos into #{output}"
|
173
|
+
`ffmpeg -f mpegts -i \"concat:#{temp_files.join("|")}\" -c copy -bsf:a aac_adtstoasc \"#{output}\" 2> /dev/null`
|
174
|
+
if $?.to_i != 0
|
175
|
+
puts "Failed merging file."
|
176
|
+
end
|
177
|
+
ensure
|
178
|
+
puts "Remove temp file: #{temp_files.join(" ")}"
|
179
|
+
`rm \"#{temp_files.join("\" \"")}\"`
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|