"Merci professeur !" is a short linguistic program presented by Bernard CERQUIGLINI on TV5MONDE, the world's leading French-language cultural broadcaster that reaches more than 318 million households and 32 million viewers every week in 200 countries and territories:
Each episode of this program presents, with humor and simplicity, and in less than 2 minutes, linguistic, etymological, orthographic, and grammatical difficulties of the French language. Viewers can ask Bernard CERQUIGLINI questions about the French language's subtleties which answer will be directly broadcast.
"Merci professeur !" is probably the most accessible and interesting program about the French language. There are hundreds of episodes available on the Internet. However, these episodes are not greatly highlighted on TV5MONDE web site, while they could be integrated in a free mobile application that would push a new episode to the subscribers, for instance, every morning, to enjoy with a coffee and croissants. :)
Your mission, should you choose to accept it, is to find a way to hack TV5MONDE Web site's data, to download and rebuild the episode videos. As always, should you be caught, the Secretary will disavow any knowledge of your actions. Good luck.
We have started to hack TV5MONDE Web site's data and we have discovered that it is using a private API to fetch the list of the episodes that are displayed.
The URL of this endpoint is: http://www.tv5monde.com/emissions/episodes/merci-professeur.json.
This endpoint returns a JSON expression that contains an array of dictionaries, each dictionary corresponds to the information of an episode. We can discover the structure of the response returned by this API's endpoint with the following Shell command:
$ curl --silent http://www.tv5monde.com/emissions/episodes/merci-professeur.json | json_pp
For example, this command returns:
{
"episodes":[
{
"title":"Permaculture",
"url":"\/emissions\/episode\/merci-professeur-permaculture",
"image":"https:\/\/vodhdimg.tv5monde.com\/tv5mondeplus\/images\/4927553.jpg",
"date":"Vendredi 2 ao\u00fbt 2019 (redif. du Mercredi 28 f\u00e9vrier 2018)",
"duration":"02:00"
},
{
"title":"On est sur...",
"url":"\/emissions\/episode\/merci-professeur-on-est-sur",
"image":"https:\/\/vodhdimg.tv5monde.com\/tv5mondeplus\/images\/4832469.jpg",
"date":"Vendredi 2 ao\u00fbt 2019",
"duration":"02:32"
},
...
],
"numPages":26
}
We will store the information of an episode in an object.
Write a Python class) Episode
which constructor takes the following parameters in that particular order:
title
: The title of the episodepage_url
: The Uniform Resource Locator (URL) of the Web page dedicated to this episodeimage_url
: The Uniform Resource Locator (URL) of the image (poster) that is shown while the video of the episode is downloading or until the user hits the play button; this is the representative of the episode's videobroadcasting_date
: The date when this episode has been broadcast
Write a static method from_json
of this class that takes an argument payload
(a JSON expression) and that returns an object Episode
.
We provide hereafter an example of the JSON expression that is passed to this static method:
{
"title": "Kilom\u00e8tre par heure",
"url": "/emissions/episode/merci-professeur-kilometre-par-heure",
"image": "https://vodhdimg.tv5monde.com/tv5mondeplus/images/5022428.jpg",
"date": "Vendredi 21 juin 2019 (redif. du Samedi 28 avril 2018)",
"duration": "02:08"
}
The class Episode
's attributes MUST be private. They MUST be accessible through the read-only properties title
, page_url
, image_url
, and broadcasting_date
.
Also, the private attribute page_url
, corresponding to the URL of the episode's Web page, MUST start with the string "http://www.tv5monde.com
".
For example:
# Build the JSON expression of the information of an episode.
>>> payload = json.loads(
... """
... {
... "title": "Kilom\u00e8tre par heure",
... "url": "/emissions/episode/merci-professeur-kilometre-par-heure",
... "image": "https://vodhdimg.tv5monde.com/tv5mondeplus/images/5022428.jpg",
... "date": "Vendredi 21 juin 2019 (redif. du Samedi 28 avril 2018)",
... "duration": "02:08"
... }""")
# Build an object Episode with this JSON expression.
>>> episode = Episode.from_json(payload)
# Read the episode's information using the properties of this object.
>>> episode.title
'Kilomètre par heure'
>>> episode.page_url # This URL has been prefixed with "http://www.tv5monde.com".
'http://www.tv5monde.com/emissions/episode/merci-professeur-kilometre-par-heure'
>>> episode.image_url
'https://vodhdimg.tv5monde.com/tv5mondeplus/images/5022428.jpg'
>>> episode.broadcasting_date
'Vendredi 21 juin 2019 (redif. du Samedi 28 avril 2018)'
>>> episode.duration
'02:08'
# These properties are read-only; they CANNOT be set.
>>> episode.title = 'Something else'
Traceback (most recent call last):
File "<input>", line 1, in <module>
AttributeError: can't set attribute
Each episode is identified with a number.
We have discovered that this identification can be extracted from the URL of the representative image of the episode's video (cf. image_url
). The file of this image is actually named after the identification of the episode.
For example:
https://vodhdimg.tv5monde.com/tv5mondeplus/images/5022428.jpg
The identification of this episode is 5022428
.
You need to:
-
Add a private static method
__parse_episode_id
to the classEpisode
that takes an argumenturl
(a string) representing the Uniform Resource Locator of the image of an episode, and that returns the identification of the episode (a string); -
Update the constructor of the class
Episode
to create an additional private attribute and set its value with the identification of the episode extracted from the URL of the representative image of the episode; -
Add a read-only property
episode_id
to the classEpisode
that returns the identification of the episode.
For example:
>>> payload = json.loads(
... """
... {
... "title": "Kilom\u00e8tre par heure",
... "url": "/emissions/episode/merci-professeur-kilometre-par-heure",
... "image": "https://vodhdimg.tv5monde.com/tv5mondeplus/images/5022428.jpg",
... "date": "Vendredi 21 juin 2019 (redif. du Samedi 28 avril 2018)",
... "duration": "02:08"
... }""")
>>> episode = Episode,from_json(payload)
>>> episode.episode_id
'5022428'
Now that we have a class Episode
, we can easily instantiate objects by providing JSON expressions representing episodes.
You will need to write a function fetch_episodes
that takes an argument url
(a string) that corresponds to the Uniform Resource Locator (URL) of the TV5MONDE's endpoint which allows to get the list of episodes.
The function sends a HTTP GET
request to the specified TV5MONDE's private API, reads the JSON data returned by this endpoint, and returns a list of objects Episode
.
There are a few points you need to consider that we present hereafter.
When connecting to a machine through the Internet, and sending and retrieving data to and from a remote machine, your definitively MUST expect to face a couple of issues:
- network issue: your machine or the remote machine is not currently connected to the Internet, the remote machine is not accessible because of various possible failures between your machine and this remote machine (DNS, router, firewall, switch, etc.);
- machine issue: the remote machine is down, its network interface is down, etc.
- application issue: the Web sever application of this machine is down or it is not responding, the resource specified in your HTTP request does not exist or its access is not allowed, etc.
You MUST distinguish permanent errors (resource is not found, its access is not forbidden, etc.), from temporary errors (connectivity issue, server issues). Temporary errors are recoverable. Permanent errors are not. In case of temporary errors, your code SHOULD try to reattempt the same request some times later.
You SHOULD definitively write the code that handles the HTTP request to the endpoint specified by an in a separate function. This is the Separation of Concerns (SoC) principle: each function addresses on one and only one concern. This principle allows better modularity and maintainability of your code.
You SHOULD write a function read_url
that takes an argument url
(a string), that performs the HTTP request to the specified endpoint, and that returns the data read contained in the HTTP response. This function SHOULD reattempts a certain number of times to connect and read data from the specified URL when temporary errors occur.
For instance:
def read_url(
url,
maximum_attempt_count=3,
sleep_duration_between_attempts=10):
"""
Return data fetched from a HTTP endpoint.
:param url: A Uniform Resource Locator (URL) that references the
endpoint to open and read data from.
:param maximum_attempt_count: Maximal number of failed attempts to
fetch data from the specified URL before the function raises an
exception.
:param sleep_duration_between_attempts: Time in seconds during which
the current thread is suspended after a failed attempt to fetch
data from the specified URL, before a next attempt.
:return: The data read from the specified URL.
:raise HTTPError: If an error occurs when trying unsuccessfully
several times to fetch data from the specified URL, after
"""
...
Your function fetch_episodes
calls this other function read_url
to read the JSON expression representing a list of episodes fetched from the TV5MONDE's private API.
Your application needs to disguise itself, i.e., to fake a browser application that impersonates a real user. Why? Because some Web servers don't allow client applications other than browsers to fetch data from their private API. How do they recognize browsers. They read a special HTTP header, User-Agent
, from the HTTP request they received. If the HTTP header User-Agent
doesn't reference an accepted browser, the Web server may deny the access to the requested resource.
Your function read_data_from_url
needs to add an HTTP header User-Agent
with a real browser identification when your function sends the HTTP request to TV5MONDE private API.
TV5MONDE's private API doesn't return all the episodes available online in only one request. It only returns a page of episodes.
How to fetch all the episodes? TV5MONDE's private API supports pagination. You may have noticed that the JSON expression, returned by TV5MONDE private API, contains an attribute numPages
that indicates the number of pages.
The endpoint of this API supports a query parameter page
that allows the caller to indicate the index of the page to return episodes from. This index starts with 1
. By default, when not defined, the page index value is 1
.
For example:
$ curl --silent http://www.tv5monde.com/emissions/episodes/merci-professeur.json?page=2 | json_pp
{
"episodes":[
{
"title":"Trace",
"url":"\/emissions\/episode\/merci-professeur-trace",
"image":"https:\/\/vodhdimg.tv5monde.com\/tv5mondeplus\/images\/5257520.jpg",
"date":"Lundi 24 juin 2019",
"duration":"02:08"
},
{
"title":"Kilom\u00e8tre par heure",
"url":"\/emissions\/episode\/merci-professeur-kilometre-par-heure",
"image":"https:\/\/vodhdimg.tv5monde.com\/tv5mondeplus\/images\/5022428.jpg",
"date":"Vendredi 21 juin 2019 (redif. du Samedi 28 avril 2018)",
"duration":"02:08"
},
...
],
"numPages":26
}
Update your function fetch_episodes
to return all available episodes.
We need now to understand how the video of an episode is downloaded by your browser and how it is played.
For that you need to open the page of an episode, to click on the High Definition (HD) video option, and to inspect network activity between your browser and TV5MONDE Web site.
You need to access the Developer Tools of your browser. Most of the browsers, such as Chrome and FireFox, support a set of tools that help developers edit pages on-the-fly and diagnose problems quickly.
For example, with Google Chrome, the developer Web Tool provides a tab to access network activity.
You can filter resources that the browser accesses to by entering some keywords. Enter the keyword segment
. You will see a list of TS files such as segment1_3_av.ts?null=0
, segment2_3_av.ts?null=0
, etc.:
Transport Stream (TS) is a standard format specified in MPEG-2 for the transmission and storage of audio, video and data, and commonly used in broadcast systems.
If you click on one particular TS files displayed in the filtered list, you have access to detailed information about this resource, such as its location referenced by the request URL, for example:
https://hlstv5mplus-vh.akamaihd.net/i/hls/61/5022428_,300,700,1400,2100,k.mp4.csmil/segment1_3_av.ts?null=0
We can manually download this file to watch it:
# Download the video file "segment1_3_av.ts".
$ wget --output-document=segment1_3_av.ts "https://hlstv5mplus-vh.akamaihd.net/i/hls/61/5022428_,300,700,1400,2100,k.mp4.csmil/segment1_3_av.ts?null=0"
--2019-08-08 11:22:04-- https://hlstv5mplus-vh.akamaihd.net/i/hls/61/5022428_,300,700,1400,2100,k.mp4.csmil/segment1_3_av.ts?null=0
Resolving hlstv5mplus-vh.akamaihd.net (hlstv5mplus-vh.akamaihd.net)... 113.171.230.8
Connecting to hlstv5mplus-vh.akamaihd.net (hlstv5mplus-vh.akamaihd.net)|113.171.230.8|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 3165732 (3.0M) [video/MP2T]
Saving to: ‘segment1_3_av.ts’
segment1_3_av.ts 100%[==================================>] 3.02M 5.87MB/s in 0.5s
2019-08-08 11:22:05 (5.87 MB/s) - ‘segment1_3_av.ts’ saved [3165732/3165732]
# Display information about this file.
$ ls -la segment1_3_av.ts
-rw-r--r--@ 1 lythanhphu student 3165732 Aug 8 11:22 segment1_3_av.ts
You can watch this file with your favorite video reader, such as VLC media player, a free and open source cross-platform multimedia player. You will then notice that this video is only the first 10 seconds of the episode.
An episode is actually composed of a list of small TS videos (a playlist) to be played sequentially. This technique allows a kind of progressive downloading: the user can start to play the episode while the whole video is not completely downloaded.
You may have noticed that the TS videos are not hosted on TV5MONDE (tv5monde.com
), but on another location (akamaihd.net
). For instance hlstv5mplus-vh.akamaihd.net
. These videos are actually hosted on Akamai, a Content Delivery Network (CDN).
Our scraper application will need to download the videos of an episode from Akamai's servers. But what is the URL of each TS segment?
If you watch several episodes and you inspect the network activity, you will find a common pattern of the request URL:
https://hlstv5mplus-vh.akamaihd.net/i/hls/61/5022428_,300,700,1400,2100,k.mp4.csmil/segment1_3_av.ts?null=0
https://hlstv5mplus-vh.akamaihd.net/i/hls/73/5257520_,300,700,1400,2100,k.mp4.csmil/segment1_3_av.ts?null=0
https://hlstv5mplus-vh.akamaihd.net/i/hls/9e/4927553_,300,700,1400,2100,k.mp4.csmil/segment1_3_av.ts?null=0
https://hlstv5mplus-vh.akamaihd.net/i/hls/2b/5257518_,300,700,1400,2100,k.mp4.csmil/segment1_3_av.ts?null=0
The common pattern between the request URL to download these TS videos is:
- the scheme is
HTTPS
- the hostname is
hlstv5mplus-vh.akamaihd.net
- the path starts with
/i/hls
, followed with a magic number (for instance2b
), followed with the episode identifier, followed with_,300,700,1400,2100,k.mp4.csmil
- the TS file name starts with
segment
, followed with the index of the video segment (starting with1
), followed with_3_av.ts
- the query is
null=0
We have discovered that the magic number is always the same for the video segments of a same episode, however this magic number is different from one episode to another. So how could we determine the magic number for a specific episode? What is the root file where we should expect to find it? More likely the episode HTML source page itself!
For example:
View Page Source | Page HTML Code |
---|---|
You SHOULD search for the hostname hlstv5mplus-vh.akamaihd.net
. You will find it is included in a JSON expression similar to:
{
"files": [
{
"format": "m3u8",
"url": "https://hlstv5mplus-vh.akamaihd.net/i/hls/1b/4832469_,300,700,1400,2100,k.mp4.csmil/master.m3u8"
}
],
"primary": "html5",
"token": false
}
Can you see the magic number there? Can you actually figure out how you can reuse the given URL to retrieve the video segments of this episode? Can you see the light?!
Hint: Nope?! Don't you see the similarity between the URL of the video segments of the episode and the URL that you can extract from the page source of this episode?
https://hlstv5mplus-vh.akamaihd.net/i/hls/1b/4832469_,300,700,1400,2100,k.mp4.csmil/segment1_3_av.ts?null=0
and:
https://hlstv5mplus-vh.akamaihd.net/i/hls/1b/4832469_,300,700,1400,2100,k.mp4.csmil/master.m3u8
Write a function fetch_episode_html_page
that takes an argument episode
(an object Episode
), and that returns the textual HTML content of the episode page (cf. page_url
). This function internally calls the function read_url
to read data (bytes) from the specified URL, and converts these data (encoded in UTF-8) to a string:
Write a function parse_broadcast_data_attribute
that takes an argument html_page
, a string corresponding to the source code of the HTML page of an episode, and that returns a JSON expression corresponding to the string value of the attribute data-broadcast
.
For example:
# Fetch the list of episodes.
>>> episodes = fetch_episodes('http://www.tv5monde.com/emissions/episodes/merci-professeur.json?page={}')
>>> len(episodes)
611
# Fetch the HTML source page of the first episode of this list.
>>> episode = episodes[0]
>>> episode.page_url
'http://www.tv5monde.com/emissions/episode/merci-professeur-trace'
>>> episode_html_page = fetch_episode_html_page(episode)
'<!DOCTYPE html>\n<html lang="fr">\n<head>\n<meta charset="UTF-8" />\n...'
# Parse broadcast information about the episode's video.
>>> parse_broadcast_data_attribute(episode_html_page)
{'files': [{'format': 'm3u8', 'url': 'https://hlstv5mplus-vh.akamaihd.net/i/hls/73/5257520_,300,700,1400,2100,k.mp4.csmil/master.m3u8'}], 'primary': 'html5', 'token': False}
Using the URL provided in the broadcast data that we have extracted in the previous waypoint, we should be able to easily build an URL pattern for accessing the video segments of an episode.
We have inspected the network activity and we have seen that the URLs of the video segments of an episode are almost the same:
https://hlstv5mplus-vh.akamaihd.net/i/hls/1b/4832469_,300,700,1400,2100,k.mp4.csmil/segment1_3_av.ts?null=0
https://hlstv5mplus-vh.akamaihd.net/i/hls/1b/4832469_,300,700,1400,2100,k.mp4.csmil/segment2_3_av.ts?null=0
https://hlstv5mplus-vh.akamaihd.net/i/hls/1b/4832469_,300,700,1400,2100,k.mp4.csmil/segment3_3_av.ts?null=0
https://hlstv5mplus-vh.akamaihd.net/i/hls/1b/4832469_,300,700,1400,2100,k.mp4.csmil/segment4_3_av.ts?null=0
https://hlstv5mplus-vh.akamaihd.net/i/hls/1b/4832469_,300,700,1400,2100,k.mp4.csmil/segment5_3_av.ts?null=0
https://hlstv5mplus-vh.akamaihd.net/i/hls/1b/4832469_,300,700,1400,2100,k.mp4.csmil/segment6_3_av.ts?null=0
https://hlstv5mplus-vh.akamaihd.net/i/hls/1b/4832469_,300,700,1400,2100,k.mp4.csmil/segment7_3_av.ts?null=0
https://hlstv5mplus-vh.akamaihd.net/i/hls/1b/4832469_,300,700,1400,2100,k.mp4.csmil/segment8_3_av.ts?null=0
https://hlstv5mplus-vh.akamaihd.net/i/hls/1b/4832469_,300,700,1400,2100,k.mp4.csmil/segment9_3_av.ts?null=0
https://hlstv5mplus-vh.akamaihd.net/i/hls/1b/4832469_,300,700,1400,2100,k.mp4.csmil/segment10_3_av.ts?null=0
https://hlstv5mplus-vh.akamaihd.net/i/hls/1b/4832469_,300,700,1400,2100,k.mp4.csmil/segment11_3_av.ts?null=0
https://hlstv5mplus-vh.akamaihd.net/i/hls/1b/4832469_,300,700,1400,2100,k.mp4.csmil/segment12_3_av.ts?null=0
https://hlstv5mplus-vh.akamaihd.net/i/hls/1b/4832469_,300,700,1400,2100,k.mp4.csmil/segment13_3_av.ts?null=0
https://hlstv5mplus-vh.akamaihd.net/i/hls/1b/4832469_,300,700,1400,2100,k.mp4.csmil/segment14_3_av.ts?null=0
https://hlstv5mplus-vh.akamaihd.net/i/hls/1b/4832469_,300,700,1400,2100,k.mp4.csmil/segment15_3_av.ts?null=0
https://hlstv5mplus-vh.akamaihd.net/i/hls/1b/4832469_,300,700,1400,2100,k.mp4.csmil/segment16_3_av.ts?null=0
The only difference is that each URL of a video segment contains the index of this video segment, starting from 1. The URL pattern of the video segments of this particular episode is:
https://hlstv5mplus-vh.akamaihd.net/i/hls/1b/4832469_,300,700,1400,2100,k.mp4.csmil/segment{}_3_av.ts?null=0
where {}
could be easily replaced with the index of a video segment using the string function format
.
This URL pattern can be easily built from the broadcast data that our function parse_broadcast_data_attribute
parses from the HTML source code of an episode, such as for example:
https://hlstv5mplus-vh.akamaihd.net/i/hls/73/5257520_,300,700,1400,2100,k.mp4.csmil/master.m3u8
Write a function build_segment_url_pattern
that takes an argument broadcast_data
(a JSON expression), representing the broadcast data of an episode, and that returns a string representing a URL pattern that references the video segments of this episode.
For example:
# Fetch the list of episodes.
>>> episodes = fetch_episodes('http://www.tv5monde.com/emissions/episodes/merci-professeur.json?page={}')
>>> len(episodes)
611
# Fetch the HTML source page of the first episode of this list.
>>> episode = episodes[0]
>>> episode.page_url
'http://www.tv5monde.com/emissions/episode/merci-professeur-trace'
>>> episode_html_page = fetch_episode_html_page(episode)
'<!DOCTYPE html>\n<html lang="fr">\n<head>\n<meta charset="UTF-8" />\n...'
# Parse broadcast information about the episode's video.
>>> broadcast_data = parse_broadcast_data_attribute(episode_html_page)
{'files': [{'format': 'm3u8', 'url': 'https://hlstv5mplus-vh.akamaihd.net/i/hls/73/5257520_,300,700,1400,2100,k.mp4.csmil/master.m3u8'}], 'primary': 'html5', 'token': False}
>>> segment_url_pattern = build_segment_url_pattern(broadcast_data)
>>> print(segment_url_pattern)
https://hlstv5mplus-vh.akamaihd.net/i/hls/73/5257520_,300,700,1400,2100,k.mp4.csmil/segment{}_3_av.ts?null=0
# Display the URL that references the first video segment.
>>> print(segment_url_pattern.format('1'))
https://hlstv5mplus-vh.akamaihd.net/i/hls/73/5257520_,300,700,1400,2100,k.mp4.csmil/segment1_3_av.ts?null=0
Note: you SHOULD use the function urlparse
and the class ParseResult
to parse the URL provided in the broadcast data of the episode and to build the URL pattern.
Write a function download_episode_video_segments
that takes an argument episode
(an object `Episode), that downloads all the TS video segments of this episode, and returns the absolute path and file names of these video segments in the order of the segment indices.
The function download_episode_video_segments
accepts an optional argument path
(a string) that indicates in with directory the video segment files need to be saved into. If not defined, the function saves the video segment files in the current working directory.
The file name of each video segment MUST be composed with the following pattern:
segment_{episode_id}_{segment_index}.ts`
where:
episode_id
: Identification of the episodesegment_index
: Index of the video segment
For example:
# Let's consider the following episode:
>>> episode
<__main__.Episode object at 0x1052b2eb8>
>>> episode.title
'Trace'
>>> episode.episode_id
'5257520'
>>> episode.page_url
'http://www.tv5monde.com/emissions/episode/merci-professeur-trace'
# Download all the video segments of this episode in our directory
# "Movies".
>>> download_episode_video_segments(episode, path='~/Movies')
['/home/lythanhphu/Movies/segment_5257520_1.ts', '/home/lythanhphu/Movies/segment_5257520_2.ts', '/home/lythanhphu/Movies/segment_5257520_3.ts', '/home/lythanhphu/Movies/segment_5257520_4.ts', '/home/lythanhphu/Movies/segment_5257520_5.ts', '/home/lythanhphu/Movies/segment_5257520_6.ts', '/home/lythanhphu/Movies/segment_5257520_7.ts', '/home/lythanhphu/Movies/segment_5257520_8.ts', '/home/lythanhphu/Movies/segment_5257520_9.ts', '/home/lythanhphu/Movies/segment_5257520_10.ts', '/home/lythanhphu/Movies/segment_5257520_11.ts', '/home/lythanhphu/Movies/segment_5257520_12.ts', '/home/lythanhphu/Movies/segment_5257520_13.ts']
There are many techniques you can use to download a file from an HTTP URL:
- The helper function
urlretrieve
to download each individual video segment file. However, this function doesn't support an option to indicate a timeout to the HTTP request that is performed, meaning that if the request is blocked (for various possible reasons), your script could be blocked for ever. You could set the default timeout for new socket objects with the function `, but this should be generally discouraged as it could introduce undesirable side effects in other parts of your application.
-
The function
urlopen
that supports the optiontimeout
, which allows to specify a timeout in seconds for blocking operations like the connection attempt; -
The third-party library
requests
, which functionget
also supports an optiontimeout
.
Note: how do we know how many video segments there are for an episode? We don't initially know this number. We could get this number by reading the M3U8 playlist of the episode; this probably the most generic solution, but it would be longer to implement. We suggest you to simply download video segments, incrementing the index of video segment for ever until your code catches a HTTP 404 error "Not Found", meaning there is no more video segment.
Write a function build_episode_video
that takes two arguments episode
and segment_file_path_names
where:
episode
: An objectEpisode
segment_file_path_names
: a list of strings corresponding to absolute path and file names of TS video segments in the order of their index.
The function accepts an optional parameter path
(a string) that indicates in with directory the episode's video file need to be saved into. If not defined, the function saves the episode video file in the path identified by the first video segment of the list segment_file_path_names
.
The function assembles all these video segments in one video named after the identification of the episode.
The function returns the absolute path and file name of the episode's video.
For example:
# Let's consider the following episode:
>>> episode
<__main__.Episode object at 0x1052b2eb8>
>>> episode.title
'Trace'
>>> episode.episode_id
'5257520'
# Download all the video segments of this episode in our directory
# "Movies".
>>> segment_file_path_names = download_episode_video_segments(episode, path='~/Movies')
# Build the final video.
>>> build_episode_video(episode, segment_file_path_names)
'/home/lythanhphu/Movies/5257520.ts'
You will need to run your script from time to time to download new episodes that TV5MONDE is going to publish. You don't want
However if you run the current version of your script, it downloads the video segments of every episode that it has previously downloaded. This results in a huge waste of time and an amazing useless CPU and network consumption.
You need to update your code to implement a caching mechanism, meaning that your code doesn't download again and again video segments that have been already downloaded.
At this point, you might think you are finished to hack this TV5MONDE video program. Well, not totally.
Your script should work perfectly fine for almost all the episodes that have been published recently, minus some errors from TV5MONDE side. However, the current version of your script may no be able to download the videos of episodes that have been published in 2014.
The reason is that TV5MONDE doesn't use a M38U playlist to stream the video of these old episodes, but a single MPEG-4 video file per episode. If you closely inspect the broadcast data from the source code of an episode's Web page, you will notice that the format is not "m3u8
" but "mp4
", and the URL doesn't refer to a M38U playlist (that ultimately references other TS video segment files) but a MPEG-4 file.
{
"files": [
{
"format": "mp4",
"url": "https://dlhd.tv5monde.com/tv5mondeplus/hq/3842766.mp4"
}
],
"primary": "html5",
"token": false
}
You need to elegantly modify your script to support downloading the videos of these older episodes.
Since the beginning of this mission, we have made the assumption that every episode has a representative image (cf. attribute image
). And we use the URL of this representative image to extract the identification of the corresponding episode. Our code uses the identification of an episode is used to name the video segment files and the final video file of this episode.
Unfortunately, we have discovered that a few episodes don't have representative image.
For example:
{
"episodes": [
...
{
"title": "Tomate (et patate)",
"url": "/emissions/episode/merci-professeur-tomate-et-patate",
"image": "",
"date": "Vendredi 1 ao\u00fbt 2014",
"duration": "01:43"
}
...
],
"numPages": 26
}
The current version of our code badly handles this situation. The static method __parse_episode_id
of the class Episode
that we have developed in the waypoint #2, returns an empty string as the identification of an episode with no representative image (or raises an exception). Without episode identification, our code cannot correctly saves the video segment files and the final video file of these episodes.
We could more surely retrieve the identification of the episode from the broadcast data of an episode, would this episode be split in several video segments:
{
"files": [
{
"format": "m3u8",
"url": "https://hlstv5mplus-vh.akamaihd.net/i/hls/1b/4832469_,300,700,1400,2100,k.mp4.csmil/master.m3u8"
}
],
"primary": "html5",
"token": false
}
or would this episode composed of only one MPEG-4 video file:
{
"files": [
{
"format": "mp4",
"url": "https://dlhd.tv5monde.com/tv5mondeplus/hq/3842766.mp4"
}
],
"primary": "html5",
"token": false
}
The problem with this solution is that it breaks our cache mechanism (cf. waypoint #9). Our cache mechanism allows our code to only fetch the list of episodes from TV5MONDE private API and to immediately detect which episodes has been already downloaded, which new episodes need to be downloaded. It's efficient. It's fast.
If we need to read the Web page of each episode and extract the broadcast data of this episode in order to retrieve the identification of this episode, and to decide whether we need to download or not the video files of this episode, our script would be very slow.
How can we fix this issue?
We can decide to generate a key to uniquely identify episodes. This key has to be generated from the data directly fetched from TV5MONDE private API, so no other request is required to take the decision whether we need to download the video files of an episode, or whether we need to skip this episode as we have already downloaded it.
An episode always has a dedicated Web page (cf. attribute url
). This Web page is unique for each episode:
{
"title": "Tomate (et patate)",
"url": "/emissions/episode/merci-professeur-tomate-et-patate",
"image": "",
"date": "Vendredi 1 ao\u00fbt 2014",
"duration": "01:43"
}
We can use this URL (actually a path) to generate a dedicated unique key for this episode. We will use the MD5 message-digest algorithm to produce a hash value of the episode's URL. We will use the hexadecimal representation of this hash to name the episode's video files.
For example, the path of the dedicated Web page of the episode "Tomate (et patate)" is "/emissions/episode/merci-professeur-tomate-et-patate
". The MD5 hash value of this path is caa8efbaaae3bb32cbd14a9ff6d73c63
.
We will replace in our code the way we name the video files of an episode, from the identification of the episode to this hash value.
You need to:
-
Add a private static method
__generate_key
to the classEpisode
, that takes an arguments
(a string) and that returns a string representing the MD5 hexadecimal hash value of this arguments
; -
Update the constructor of the class
Episode
to create an additional private attribute and set its value with the hexadecimal hash value of the episode built from the URL (path only) of the Web page of the episode; -
Add a read-only property
key
to the classEpisode
that returns the unique key of the episode.
Then you need to refactor your code to name the video files of an episode with the key of this episode.