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