]> code.delx.au - webdl/blob - brightcove.py
support grabbing brightcove widevine videos - for channel 9
[webdl] / brightcove.py
1 import logging
2 import re
3 import sys
4
5 from common import grab_json, download_hls, download_http, Node, append_to_qs
6
7 CH9_TOKEN = "ogxhPgSphIVa2hhxbi9oqtYwtg032io4B4-ImwddYliFWHqS0UfMEw.."
8 CH10_TOKEN = "lWCaZyhokufjqe7H4TLpXwHSTnNXtqHxyMvoNOsmYA_GRaZ4zcwysw.."
9
10 BRIGHTCOVE_API = "http://api.brightcove.com/services/library?"
11
12
13 class BrightcoveVideoNode(Node):
14 def __init__(self, title, parent, token, video_id):
15 Node.__init__(self, title, parent)
16 self.can_download = True
17 self.token = token
18 self.video_id = video_id
19
20 def download(self):
21 f = None
22 try_streams = [self.try_widevine, self.try_hls]
23
24 while f is None and try_streams:
25 f = try_streams.pop()()
26
27 if f is None:
28 logging.error("No HLS or Widevine stream available for: %s", self.title)
29 return False
30
31 return f()
32
33 def try_hls(self):
34 desc_url = append_to_qs(BRIGHTCOVE_API, {
35 "token": self.token,
36 "command": "find_video_by_id",
37 "video_fields": "HLSURL",
38 "video_id": self.video_id,
39 })
40
41 doc = grab_json(desc_url, 3600)
42 video_url = doc["HLSURL"]
43 if not video_url:
44 return
45
46 filename = self.title + ".ts"
47 return lambda: download_hls(filename, video_url)
48
49 def try_widevine(self):
50 desc_url = append_to_qs(BRIGHTCOVE_API, {
51 "token": self.token,
52 "command": "find_video_by_id",
53 "video_fields": "WVMRenditions",
54 "video_id": self.video_id,
55 })
56
57 doc = grab_json(desc_url, 3600)
58
59 renditions = doc["WVMRenditions"]
60 if not renditions:
61 return
62
63 best_rendition = sorted(renditions, key=lambda r: r["frameHeight"])[-1]
64 video_url = best_rendition["url"]
65 filename = self.title + ".ts"
66
67 return lambda: download_http(filename, video_url)
68
69
70 class BrightcoveRootNode(Node):
71 def __init__(self, title, parent, token):
72 Node.__init__(self, title, parent)
73 self.token = token
74 self.series_nodes = {}
75
76 def get_series_node(self, series_name):
77 series_name = series_name.split("-")[0].strip()
78 key = series_name.lower()
79 node = self.series_nodes.get(key, None)
80 if node is None:
81 node = Node(series_name, self)
82 self.series_nodes[key] = node
83 return node
84
85 def fill_children(self):
86 page_number = 0
87 while page_number < 100:
88 url = self.get_all_videos_url(page_number)
89 page_number += 1
90
91 page = grab_json(url, 3600)
92 items = page["items"]
93 if len(items) == 0:
94 break
95
96 for video_desc in items:
97 self.process_video(video_desc)
98
99 def process_video(self, video_desc):
100 if not video_desc["customFields"]:
101 return
102
103 video_id = video_desc["id"]
104 title = self.get_video_title(video_desc)
105 series_name = self.get_series_name(video_desc)
106
107 parent_node = self.get_series_node(series_name)
108 BrightcoveVideoNode(title, parent_node, self.token, video_id)
109
110 def get_video_title(self, video_desc):
111 raise NotImplementedError()
112
113 def get_series_name(self, video_desc):
114 raise NotImplementedError()
115
116 def get_all_videos_url(self, page_number):
117 raise NotImplementedError()
118
119
120 class Ch9RootNode(BrightcoveRootNode):
121 def __init__(self, root_node):
122 BrightcoveRootNode.__init__(self, "Nine", root_node, CH9_TOKEN)
123
124 def get_video_title(self, video_desc):
125 title = video_desc["name"]
126 season = video_desc["customFields"].get("season", "")
127 episode = video_desc["customFields"].get("episode", "1").rjust(2, "0")
128 series = self.get_series_name(video_desc)
129
130 if re.match(R"ep(isode)?\s*[0-9]+\s*:", title.lower()):
131 title = title.split(":", 1)[1].strip()
132
133 title = "%s - %sx%s - %s" % (
134 series,
135 season,
136 episode,
137 title
138 )
139 return title
140
141 def get_series_name(self, video_desc):
142 series = video_desc["customFields"].get("series", None)
143 if not series:
144 series = video_desc["name"]
145 return series
146
147 def get_all_videos_url(self, page_number):
148 return append_to_qs(BRIGHTCOVE_API, {
149 "token": self.token,
150 "command": "search_videos",
151 "video_fields": "id,name,customFields",
152 "custom_fields": "series,season,episode",
153 "sort_by": "PUBLISH_DATE",
154 "page_number": str(page_number),
155 })
156
157
158 class Ch10RootNode(BrightcoveRootNode):
159 def __init__(self, root_node):
160 BrightcoveRootNode.__init__(self, "Ten", root_node, CH10_TOKEN)
161
162 def get_video_title(self, video_desc):
163 return video_desc["name"]
164
165 def get_series_name(self, video_desc):
166 return video_desc["customFields"]["tv_show"]
167
168 def get_all_videos_url(self, page_number):
169 return append_to_qs(BRIGHTCOVE_API, {
170 "token": self.token,
171 "command": "search_videos",
172 "video_fields": "id,name,customFields",
173 "custom_fields": "tv_show",
174 "sort_by": "PUBLISH_DATE",
175 "any": "video_type_long_form:Full Episode",
176 "page_number": str(page_number),
177 })
178
179
180 def fill_nodes(root_node):
181 Ch9RootNode(root_node)
182 Ch10RootNode(root_node)
183