Compare commits

...

8 Commits

Author SHA1 Message Date
Barbagus 57da060e73 Merge pull request 'support for collections' (#28) from collections into stable
Reviewed-on: #28
2023-01-24 19:26:05 +00:00
Barbagus 6b24b15f57 Update README according to implementation 2023-01-24 20:24:59 +01:00
Barbagus e23cd73664 Implement collections 2023-01-24 19:59:39 +01:00
Barbagus 3ca02e8e42 Include collection www/json samples
TV series that list episodes through many `collection_subcollection_*`
zones (one per season):
 - RC-023217__acquitted.json
 - RC-022923__cry-wolf.json

Other collection that list items in one `collection_videos_*` zone:
 - RC-023013__l-incroyable-periple-de-magellan.json
 - RC-023242__bandes-de-pirates.json
2023-01-24 10:15:50 +01:00
Barbagus 56c1e8468a Split program/rendition/variant/target operations
Significant rewrite after model modification: introducing `*Sources`
objects that encapsulate metadata and fetch information (urls,
protocols). The API (#20) is organized as pipe elements with sources
being what flows through the pipe.
    1. fetch program sources
    2. fetch rendition sources
    3. fetch variant sources
    4. fetch targets
    5. process (download+mux) targets
Some user selection filter or modifiers could then be applied at any
step of the pipe. Our __main__.py is an implementation of that scheme.

Implied modifications include:
 - Later failure on unsupported protocols, used to be in `api`, now in
   `hls`. This offers the possibility to filter and/or support them
   later.
 - Give up honoring the http ranges for mp4 download, stream-download
   them by fixed chunk instead.
 - Cleaning up of the `hls` module moving the main download function to
   __init__ and specific (mp4/vtt) download functions to a new
   `download` module.

On the side modifications include:
 - The progress handler showing downloading rates.
 - The naming utilities providing rendition and variant code insertion.
 - Download parts to working directories and skip unnecessary
   re-downloads on failure.

This was a big change for a single commit... too big of a change maybe.
2023-01-24 08:27:37 +01:00
Barbagus ed5ba06a98 Implement a "schema guard" for `api` module
In order to catch errors related to assumed JSON schema, regroup all
JSON data access under a context manager that catch related errors:
- KeyError
- IndexError
- ValueError
2023-01-16 21:12:55 +01:00
Barbagus fcadd531c4 Reorganize imports in files 2023-01-14 20:46:16 +01:00
Barbagus 639a8063a5 Get program information from page content
Changes the way the program information is figured out. From URL parsing
to page content parsing.
A massive JSON object is shipped within the HTML of the page, that's
were we get what we need from.

Side effects:
 - drop `slug` from the program's info
 - drop `slug` naming option
 - no `Program` / `ProgramMeta` distinction

Includes some JSON samples.
2023-01-14 19:51:02 +01:00
18 changed files with 11118 additions and 622 deletions

View File

@ -74,7 +74,8 @@ Options:
--name-sep=<sep> field separator [default: - ]
--name-seq-pfx=<pfx> sequence counter prefix [default: - ]
--name-seq-no-pad disable sequence zero-padding
--name-add-resolution add resolution tag
--name-add-rendition add rendition code
--name-add-variant add variant code
```
🔧 How it works
@ -82,33 +83,15 @@ Options:
## 🏗️ The streaming infrastructure
Every video program have a _program identifier_ visible in their web page URL:
```
https://www.arte.tv/es/videos/110139-000-A/fromental-halevy-la-tempesta/
https://www.arte.tv/fr/videos/100204-001-A/esprit-d-hiver-1-3/
https://www.arte.tv/en/videos/104001-000-A/clint-eastwood/
```
That _program identifier_ enables us to query an API for the program's information.
We support both _single program pages_ and _program collection pages_. Every page is shipped with some embedded JSON data, example of such data can be found [here](https://git.afpy.org/fcode/delarte/src/branch/stable/samples/www/). From that we extract metadata for each programs. In particular, we extract a _site language_ and a _program ID_. These enables us to query the config API
### The _config_ API
For the last example the API call is as such:
```
https://api.arte.tv/api/player/v2/config/en/104001-000-A
```
The response is a JSON object, a sample of which can be found [here](https://git.afpy.org/fcode/delarte/src/branch/stable/samples/api/config-105612-000-A.json):
Information about the program is detailed in `$.data.attributes.metadata` and a list of available audio/subtitles combinations in `$.data.attributes.streams`. In our code such a combination is referred to as a _rendition_ (or _version_ in the CLI).
Every such _rendition_ has a reference to a _program index_ file in `.streams[i].url`
This API returns a `ConfigPlayer` JSON object, a sample of which can be found [here](https://git.afpy.org/fcode/delarte/src/branch/stable/samples/api/). A list of available audio/subtitles combinations in `$.data.attributes.streams`. In our code such a combination is referred to as a _rendition_. Every such _rendition_ has a reference to a _program index_ file in `.streams[i].url`
### The _program index_ file
As defined in [HTTP Live Streaming](https://www.rfc-editor.org/rfc/rfc8216) (sample file can be found [here](https://git.afpy.org/fcode/delarte/src/branch/stable/samples/hls/program-105612-000-A_VOF-STMF_XQ.m3u8) or [here](https://git.afpy.org/fcode/delarte/src/branch/stable/samples/hls/program-105612-000-A_VA-STA_XQ.m3u8)). This file show the a list of video _variants_ URIs (one per video resolution). Each of them has
As defined in [HTTP Live Streaming](https://www.rfc-editor.org/rfc/rfc8216) (sample files can be found [here](https://git.afpy.org/fcode/delarte/src/branch/stable/samples/hls/)). This file show the a list of video _variants_ URIs (one per video resolution). Each of them has
- exactly one video _track index_ reference
- exactly one audio _track index_ reference
- at most one subtitles _track index_ reference
@ -120,23 +103,21 @@ Audio and subtitles tracks reference also include:
### The video and audio _track index_ file
As defined in [HTTP Live Streaming](https://www.rfc-editor.org/rfc/rfc8216) (a sample file can be found [here](https://git.afpy.org/fcode/delarte/src/branch/stable/samples/hls/audio-105612-000-A_aud_VA.m3u8) or [here](https://git.afpy.org/fcode/delarte/src/branch/stable/samples/hls/video-105612-000-A_v1080.m3u8)). This file is basically a list of _segments_ (http ranges) the client is supposed to download in sequence.
As defined in [HTTP Live Streaming](https://www.rfc-editor.org/rfc/rfc8216) (sample files can be found [here](https://git.afpy.org/fcode/delarte/src/branch/stable/samples/hls/). This file is basically a list of _segments_ (http ranges) the client is supposed to download in sequence.
### The subtitles _track index_ file
As defined in [HTTP Live Streaming](https://www.rfc-editor.org/rfc/rfc8216) (a sample file can be found [here](https://git.afpy.org/fcode/delarte/src/branch/stable/samples/hls/subtitles-105612-000-A_st_VA-ALL.m3u8)). This file references the actual file containing the subtitles [VTT](https://developer.mozilla.org/en-US/docs/Web/API/WebVTT_API) data.
As defined in [HTTP Live Streaming](https://www.rfc-editor.org/rfc/rfc8216) (sample files can be found [here](https://git.afpy.org/fcode/delarte/src/branch/stable/samples/hls/)). This file references the actual file containing the subtitles [VTT](https://developer.mozilla.org/en-US/docs/Web/API/WebVTT_API) data.
## ⚙The process
1. Figure out available _sources_ by:
- fetching the _config_ API object for the _program identifier_
- fetching all referenced _program index_.
2. Select the desired _target_ based on _renditions_ and _variants_ codes.
3. Download video, audio and subtitles tracks content.
- convert `VTT` subtitles to styled `SRT`
4. Feed the all the tracks to `ffmpeg` for multiplexing (or _muxing_)
1. Fetch _program sources_ form the page pointed by the given URL
2. Fetch _rendition sources_ from _config API_
3. Filter _renditions_
4. Fetch _variant sources_ from _HLS_ _program index_ files.
5. Filter _variants_
6. Fetch final target information and figure out output naming
7. Download data streams (convert VTT subtitles to formatted SRT subtitles) and mux them with FFMPEG
## 📽️ FFMPEG

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,893 @@
{
"props": {
"pageProps": {
"geoblocking": null,
"initialPage": {
"tag": "Ok",
"value": {
"code": "RC-022923",
"language": "fr",
"support": "web",
"type": "collection",
"level": 3,
"parent": {
"type": "category",
"page": "SER",
"label": "Séries et fictions",
"url": "/fr/videos/series-et-fictions/",
"deeplink": "arte://emac/SER",
"id": "SER_fr_web",
"slug": "series-et-fictions",
"parent": null
},
"alternativeLanguages": [
{
"code": "fr",
"label": "Français",
"page": "RC-022923",
"url": "/fr/videos/RC-022923/cry-wolf/",
"title": "Cry Wolf"
},
{
"code": "de",
"label": "Deutsch",
"page": "RC-022923",
"url": "/de/videos/RC-022923/cry-wolf/",
"title": "Cry Wolf"
}
],
"url": "/fr/videos/RC-022923/cry-wolf/",
"deeplink": "arte://collection/RC-022923",
"slug": "cry-wolf",
"zones": [
{
"id": "51ba8da1-5389-43c4-af08-cca614192d77_RC-022923",
"code": "collection_content_RC-022923",
"title": "Cry Wolf",
"slug": null,
"description": null,
"displayOptions": {
"template": "single-collectionContent",
"theme": null,
"showZoneTitle": true,
"showItemTitle": true
},
"link": null,
"authenticatedContent": null,
"content": {
"data": [
{
"id": "RC-022923",
"type": "collection",
"kind": {
"code": "TV_SERIES",
"label": "Série",
"isCollection": true
},
"url": "/fr/videos/RC-022923/cry-wolf/",
"deeplink": "arte://collection/RC-022923",
"title": "Cry Wolf",
"subtitle": null,
"shortDescription": "Holly, 14 ans, accuse son beau-père de violences envers elle. Mais ses parents nient laccusation. Un assistant social atypique est chargé de démêler laffaire mais Holly subit les remontrances de sa mère qui laccuse de mentir. Drame familial perturbant, la série danoise \"Cry Wolf\" plonge dans lintimité dune famille apparemment bien sous tous rapports.",
"mainImage": {
"caption": "Série Cry Wolf",
"url": "https://api-cdn.arte.tv/img/v2/image/eFsiE6z6z3LXnBZj9A6SQ9/__SIZE__"
},
"stickers": [
{
"code": "COLLECTION",
"label": "COLLECTION"
}
],
"trackingPixel": "/ct/?abv=B&language=fr&pageid=collection&position=1&support=web&teaserid=RC-022923&teasertitle=Cry%20Wolf&zoneCode=collection_content_RC-022923&zoneIndexInPage=0&zoneTemplate=single_collectionContent&zoneid=collection_content&zonename=Cry%20Wolf",
"trailer": {
"config": "https://api.arte.tv/api/player/v2/trailer/fr/RC-022923"
},
"partners": null,
"description": "Holly, 14 ans, accuse son beau-père de violences envers elle. Mais ses parents nient laccusation. Un assistant social atypique est chargé de démêler laffaire mais Holly subit les remontrances de sa mère qui laccuse de mentir. Drame familial perturbant, la série danoise \"Cry Wolf\" plonge dans lintimité dune famille apparemment bien sous tous rapports.",
"video": null,
"availability": null
}
],
"pagination": null
}
},
{
"id": "d3bad9c8-b1c0-43db-87fb-fb95cc68766d_RC-022923",
"code": "collection_videos_RC-022923",
"title": "Toutes les vidéos",
"slug": null,
"description": null,
"displayOptions": {
"template": "horizontal-landscape",
"theme": null,
"showZoneTitle": true,
"showItemTitle": true
},
"link": null,
"authenticatedContent": null,
"content": {
"data": [],
"pagination": null
}
},
{
"id": "e0dfcff6-67bc-4d83-8055-8edf45687aea_RC-022923_RC-022924",
"code": "collection_subcollection_RC-022923_RC-022924",
"title": "Cry Wolf",
"slug": "cry-wolf",
"description": "Une série choc danoise sur lenfance maltraitée. Un drame familial remuant aux accents de thriller.",
"displayOptions": {
"template": "verticalFirstHighlighted-landscape",
"theme": null,
"showZoneTitle": true,
"showItemTitle": true
},
"link": null,
"authenticatedContent": null,
"content": {
"data": [
{
"id": "100998-001-A_fr",
"type": "teaser",
"kind": {
"code": "SHOW",
"label": "Programme",
"isCollection": false
},
"url": "/fr/videos/100998-001-A/cry-wolf-1-8/",
"deeplink": "arte://program/100998-001-A",
"title": "Cry Wolf (1/8)",
"subtitle": null,
"shortDescription": "Holly, 14 ans, accuse son beau-père de violences envers elle. Quen est-il vraiment ? Un assistant social atypique, Lars Madsen, est chargé de démêler laffaire... Un drame familial remuant aux accents de thriller, tout en tension et en non-dits.",
"mainImage": {
"caption": null,
"url": "https://api-cdn.arte.tv/img/v2/image/NnBniMoe7qfMqSo2r38YFE/__SIZE__"
},
"stickers": [
{
"code": "PLAYABLE",
"label": "PLAYABLE"
},
{
"code": "FULL_VIDEO",
"label": "Voir le programme"
}
],
"trackingPixel": "/ct/?abv=B&em=100998-001-A&language=fr&pageid=collection&position=1&support=web&teaserid=100998-001-A_fr&teasertitle=Cry%20Wolf%20%281%2F8%29&zoneCode=collection_subcollection_RC-022923_RC-022924&zoneIndexInPage=2&zoneTemplate=horizontal_landscape&zoneid=collection_subcollection&zonename=Cry%20Wolf",
"programId": "100998-001-A",
"teaserText": "Un drame familial remuant aux accents de thriller, tout en tension et en non-dits.",
"duration": 3328,
"durationLabel": "56 min",
"geoblocking": {
"code": "DE_FR",
"label": "",
"inclusion": [],
"exclusion": []
},
"genre": null,
"audioVersions": [],
"availability": {
"type": "VOD",
"start": "2023-01-12T04:00:00Z",
"end": "2023-03-27T03:00:00Z",
"upcomingDate": "2023-01-12T04:00:00Z",
"label": "Disponible du 12/01/2023 au 26/03/2023"
},
"ageRating": 16,
"callToAction": "Regarder",
"clip": null,
"trailer": null,
"childrenCount": null
},
{
"id": "100998-002-A_fr",
"type": "teaser",
"kind": {
"code": "SHOW",
"label": "Programme",
"isCollection": false
},
"url": "/fr/videos/100998-002-A/cry-wolf-2-8/",
"deeplink": "arte://program/100998-002-A",
"title": "Cry Wolf (2/8)",
"subtitle": null,
"shortDescription": "Holly, 14 ans, accuse son beau-père de violences envers elle. Quen est-il vraiment ? Un assistant social atypique, Lars Madsen, est chargé de démêler laffaire… Un drame familial remuant aux accents de thriller, tout en tension et en non-dits. Deuxième épisode : lambiance est assommante dans la maison familiale. Dea interroge Simon : a-t-il oui ou non frappé Holly ?",
"mainImage": {
"caption": null,
"url": "https://api-cdn.arte.tv/img/v2/image/VTefZdwQPweXAvxuqaeGcc/__SIZE__"
},
"stickers": [
{
"code": "PLAYABLE",
"label": "PLAYABLE"
},
{
"code": "FULL_VIDEO",
"label": "Voir le programme"
}
],
"trackingPixel": "/ct/?abv=B&em=100998-002-A&language=fr&pageid=collection&position=2&support=web&teaserid=100998-002-A_fr&teasertitle=Cry%20Wolf%20%282%2F8%29&zoneCode=collection_subcollection_RC-022923_RC-022924&zoneIndexInPage=2&zoneTemplate=horizontal_landscape&zoneid=collection_subcollection&zonename=Cry%20Wolf",
"programId": "100998-002-A",
"teaserText": "Lambiance est assommante dans la maison familiale. Dea interroge Simon : a-t-il oui ou non frappé Holly ?",
"duration": 3071,
"durationLabel": "52 min",
"geoblocking": {
"code": "DE_FR",
"label": "",
"inclusion": [],
"exclusion": []
},
"genre": null,
"audioVersions": [],
"availability": {
"type": "VOD",
"start": "2023-01-12T04:00:00Z",
"end": "2023-03-27T03:00:00Z",
"upcomingDate": "2023-01-12T04:00:00Z",
"label": "Disponible du 12/01/2023 au 26/03/2023"
},
"ageRating": 16,
"callToAction": "Regarder",
"clip": null,
"trailer": null,
"childrenCount": null
},
{
"id": "100998-003-A_fr",
"type": "teaser",
"kind": {
"code": "SHOW",
"label": "Programme",
"isCollection": false
},
"url": "/fr/videos/100998-003-A/cry-wolf-3-8/",
"deeplink": "arte://program/100998-003-A",
"title": "Cry Wolf (3/8)",
"subtitle": null,
"shortDescription": "Holly, 14 ans, accuse son beau-père de violences envers elle. Quen est-il vraiment ? Un assistant social atypique, Lars Madsen, est chargé de démêler laffaire… Un drame familial remuant aux accents de thriller, tout en tension et en non-dits. Troisième épisode : Simon se rend compte quil risque de tout perdre et saffole..",
"mainImage": {
"caption": null,
"url": "https://api-cdn.arte.tv/img/v2/image/oyDB5vM2RpNKRCvTrP5bVS/__SIZE__"
},
"stickers": [
{
"code": "PLAYABLE",
"label": "PLAYABLE"
},
{
"code": "FULL_VIDEO",
"label": "Voir le programme"
}
],
"trackingPixel": "/ct/?abv=B&em=100998-003-A&language=fr&pageid=collection&position=3&support=web&teaserid=100998-003-A_fr&teasertitle=Cry%20Wolf%20%283%2F8%29&zoneCode=collection_subcollection_RC-022923_RC-022924&zoneIndexInPage=2&zoneTemplate=horizontal_landscape&zoneid=collection_subcollection&zonename=Cry%20Wolf",
"programId": "100998-003-A",
"teaserText": "Simon se rend compte quil risque de tout perdre et saffole.",
"duration": 3300,
"durationLabel": "55 min",
"geoblocking": {
"code": "DE_FR",
"label": "",
"inclusion": [],
"exclusion": []
},
"genre": null,
"audioVersions": [],
"availability": {
"type": "VOD",
"start": "2023-01-12T04:00:00Z",
"end": "2023-03-27T03:00:00Z",
"upcomingDate": "2023-01-12T04:00:00Z",
"label": "Disponible du 12/01/2023 au 26/03/2023"
},
"ageRating": 16,
"callToAction": "Regarder",
"clip": null,
"trailer": null,
"childrenCount": null
},
{
"id": "100998-008-A_fr",
"type": "teaser",
"kind": {
"code": "SHOW",
"label": "Programme",
"isCollection": false
},
"url": "/fr/videos/100998-008-A/cry-wolf-4-8/",
"deeplink": "arte://program/100998-008-A",
"title": "Cry Wolf (4/8)",
"subtitle": null,
"shortDescription": "Holly, 14 ans, accuse son beau-père de violences envers elle. Quen est-il vraiment ? Un assistant social atypique, Lars Madsen, est chargé de démêler laffaire… Un drame familial remuant aux accents de thriller, tout en tension et en non-dits. Quatrième épisode : Theo a disparu. En réalité, il est parti à la recherche de son père pour son anniversaire.",
"mainImage": {
"caption": null,
"url": "https://api-cdn.arte.tv/img/v2/image/CV3pnaC9yg6bKqArzToEYo/__SIZE__"
},
"stickers": [
{
"code": "PLAYABLE",
"label": "PLAYABLE"
},
{
"code": "FULL_VIDEO",
"label": "Voir le programme"
}
],
"trackingPixel": "/ct/?abv=B&em=100998-008-A&language=fr&pageid=collection&position=4&support=web&teaserid=100998-008-A_fr&teasertitle=Cry%20Wolf%20%284%2F8%29&zoneCode=collection_subcollection_RC-022923_RC-022924&zoneIndexInPage=2&zoneTemplate=horizontal_landscape&zoneid=collection_subcollection&zonename=Cry%20Wolf",
"programId": "100998-008-A",
"teaserText": "Theo a disparu. En réalité, il est parti à la recherche de son père pour son anniversaire...",
"duration": 2861,
"durationLabel": "48 min",
"geoblocking": {
"code": "DE_FR",
"label": "",
"inclusion": [],
"exclusion": []
},
"genre": null,
"audioVersions": [],
"availability": {
"type": "VOD",
"start": "2023-01-12T04:00:00Z",
"end": "2023-03-27T03:00:00Z",
"upcomingDate": "2023-01-12T04:00:00Z",
"label": "Disponible du 12/01/2023 au 26/03/2023"
},
"ageRating": 16,
"callToAction": "Regarder",
"clip": null,
"trailer": null,
"childrenCount": null
},
{
"id": "100998-005-A_fr",
"type": "teaser",
"kind": {
"code": "SHOW",
"label": "Programme",
"isCollection": false
},
"url": "/fr/videos/100998-005-A/cry-wolf-5-8/",
"deeplink": "arte://program/100998-005-A",
"title": "Cry Wolf (5/8)",
"subtitle": null,
"shortDescription": "Holly, 14 ans, accuse son beau-père de violences envers elle. Quen est-il vraiment ? Un assistant social atypique, Lars Madsen, est chargé de démêler laffaire… Cinquième épisode : une vidéo partagée sur les réseaux sociaux fragilise grandement Lars.",
"mainImage": {
"caption": null,
"url": "https://api-cdn.arte.tv/img/v2/image/wHzjfqVZ3PaLC9fGiQyJkF/__SIZE__"
},
"stickers": [
{
"code": "PLAYABLE",
"label": "PLAYABLE"
},
{
"code": "FULL_VIDEO",
"label": "Voir le programme"
}
],
"trackingPixel": "/ct/?abv=B&em=100998-005-A&language=fr&pageid=collection&position=5&support=web&teaserid=100998-005-A_fr&teasertitle=Cry%20Wolf%20%285%2F8%29&zoneCode=collection_subcollection_RC-022923_RC-022924&zoneIndexInPage=2&zoneTemplate=horizontal_landscape&zoneid=collection_subcollection&zonename=Cry%20Wolf",
"programId": "100998-005-A",
"teaserText": "Une vidéo partagée sur les réseaux sociaux fragilise grandement Lars.",
"duration": 2640,
"durationLabel": "44 min",
"geoblocking": {
"code": "DE_FR",
"label": "",
"inclusion": [],
"exclusion": []
},
"genre": null,
"audioVersions": [],
"availability": {
"type": "VOD",
"start": "2023-01-12T04:00:00Z",
"end": "2023-03-27T03:00:00Z",
"upcomingDate": "2023-01-12T04:00:00Z",
"label": "Disponible du 12/01/2023 au 26/03/2023"
},
"ageRating": 16,
"callToAction": "Regarder",
"clip": null,
"trailer": null,
"childrenCount": null
},
{
"id": "100998-006-A_fr",
"type": "teaser",
"kind": {
"code": "SHOW",
"label": "Programme",
"isCollection": false
},
"url": "/fr/videos/100998-006-A/cry-wolf-6-8/",
"deeplink": "arte://program/100998-006-A",
"title": "Cry Wolf (6/8)",
"subtitle": null,
"shortDescription": "Holly, 14 ans, accuse son beau-père de violences envers elle. Quen est-il vraiment ? Un assistant social atypique, Lars Madsen, est chargé de démêler laffaire… Sixième épisode : alors quune nouvelle réunion de la commission de lenfance approche, lavocate de Simon et Dea leur propose une solution radicale.",
"mainImage": {
"caption": null,
"url": "https://api-cdn.arte.tv/img/v2/image/6Zvp8ms34Sqre6sn8k6xL3/__SIZE__"
},
"stickers": [
{
"code": "PLAYABLE",
"label": "PLAYABLE"
},
{
"code": "FULL_VIDEO",
"label": "Voir le programme"
}
],
"trackingPixel": "/ct/?abv=B&em=100998-006-A&language=fr&pageid=collection&position=6&support=web&teaserid=100998-006-A_fr&teasertitle=Cry%20Wolf%20%286%2F8%29&zoneCode=collection_subcollection_RC-022923_RC-022924&zoneIndexInPage=2&zoneTemplate=horizontal_landscape&zoneid=collection_subcollection&zonename=Cry%20Wolf",
"programId": "100998-006-A",
"teaserText": "Alors quune nouvelle réunion de la commission de lenfance approche, lavocate de Simon et Dea leur propose une solution radicale.",
"duration": 3010,
"durationLabel": "51 min",
"geoblocking": {
"code": "DE_FR",
"label": "",
"inclusion": [],
"exclusion": []
},
"genre": null,
"audioVersions": [],
"availability": {
"type": "VOD",
"start": "2023-01-12T04:00:00Z",
"end": "2023-03-27T03:00:00Z",
"upcomingDate": "2023-01-12T04:00:00Z",
"label": "Disponible du 12/01/2023 au 26/03/2023"
},
"ageRating": 16,
"callToAction": "Regarder",
"clip": null,
"trailer": null,
"childrenCount": null
},
{
"id": "100998-007-A_fr",
"type": "teaser",
"kind": {
"code": "SHOW",
"label": "Programme",
"isCollection": false
},
"url": "/fr/videos/100998-007-A/cry-wolf-7-8/",
"deeplink": "arte://program/100998-007-A",
"title": "Cry Wolf (7/8)",
"subtitle": null,
"shortDescription": "Holly, 14 ans, accuse son beau-père de violences envers elle. Quen est-il vraiment ? Un assistant social atypique, Lars Madsen, est chargé de démêler laffaire… Septième épisode : la réunion de la commission de lenfance se prépare. Toujours plus proche de Jonatan, Holly est déstabilisée.",
"mainImage": {
"caption": null,
"url": "https://api-cdn.arte.tv/img/v2/image/HWRodkStBtsVJZZmpA7usX/__SIZE__"
},
"stickers": [
{
"code": "PLAYABLE",
"label": "PLAYABLE"
},
{
"code": "FULL_VIDEO",
"label": "Voir le programme"
}
],
"trackingPixel": "/ct/?abv=B&em=100998-007-A&language=fr&pageid=collection&position=7&support=web&teaserid=100998-007-A_fr&teasertitle=Cry%20Wolf%20%287%2F8%29&zoneCode=collection_subcollection_RC-022923_RC-022924&zoneIndexInPage=2&zoneTemplate=horizontal_landscape&zoneid=collection_subcollection&zonename=Cry%20Wolf",
"programId": "100998-007-A",
"teaserText": "La réunion de la commission de lenfance se prépare. Toujours plus proche de Jonatan, Holly est déstabilisée...",
"duration": 3011,
"durationLabel": "51 min",
"geoblocking": {
"code": "DE_FR",
"label": "",
"inclusion": [],
"exclusion": []
},
"genre": null,
"audioVersions": [],
"availability": {
"type": "VOD",
"start": "2023-01-12T04:00:00Z",
"end": "2023-03-27T03:00:00Z",
"upcomingDate": "2023-01-12T04:00:00Z",
"label": "Disponible du 12/01/2023 au 26/03/2023"
},
"ageRating": 16,
"callToAction": "Regarder",
"clip": null,
"trailer": null,
"childrenCount": null
},
{
"id": "100998-004-A_fr",
"type": "teaser",
"kind": {
"code": "SHOW",
"label": "Programme",
"isCollection": false
},
"url": "/fr/videos/100998-004-A/cry-wolf-8-8/",
"deeplink": "arte://program/100998-004-A",
"title": "Cry Wolf (8/8)",
"subtitle": null,
"shortDescription": "Holly, 14 ans, accuse son beau-père de violences envers elle. Quen est-il vraiment ? Un assistant social atypique, Lars Madsen, est chargé de démêler laffaire… Dernier épisode : un changement radical attend la famille. Latmosphère est plus sombre que jamais ; la tension, permanente.",
"mainImage": {
"caption": null,
"url": "https://api-cdn.arte.tv/img/v2/image/HAEoSZ67qzafC9LFwg8tJX/__SIZE__"
},
"stickers": [
{
"code": "PLAYABLE",
"label": "PLAYABLE"
},
{
"code": "FULL_VIDEO",
"label": "Voir le programme"
}
],
"trackingPixel": "/ct/?abv=B&em=100998-004-A&language=fr&pageid=collection&position=8&support=web&teaserid=100998-004-A_fr&teasertitle=Cry%20Wolf%20%288%2F8%29&zoneCode=collection_subcollection_RC-022923_RC-022924&zoneIndexInPage=2&zoneTemplate=horizontal_landscape&zoneid=collection_subcollection&zonename=Cry%20Wolf",
"programId": "100998-004-A",
"teaserText": "Un changement radical attend la famille. Latmosphère est plus sombre que jamais ; la tension, permanente.",
"duration": 3208,
"durationLabel": "54 min",
"geoblocking": {
"code": "DE_FR",
"label": "",
"inclusion": [],
"exclusion": []
},
"genre": null,
"audioVersions": [],
"availability": {
"type": "VOD",
"start": "2023-01-12T04:00:00Z",
"end": "2023-03-27T03:00:00Z",
"upcomingDate": "2023-01-12T04:00:00Z",
"label": "Disponible du 12/01/2023 au 26/03/2023"
},
"ageRating": 16,
"callToAction": "Regarder",
"clip": null,
"trailer": null,
"childrenCount": null
}
],
"pagination": {
"page": 1,
"pages": 1,
"totalCount": 8,
"links": {
"first": "https://api-internal.arte.tv/api/emac/v4/fr/web/zones/e0dfcff6-67bc-4d83-8055-8edf45687aea/content?abv=B&collectionId=RC-022923&page=1&pageId=collection&subCollectionId=RC-022924&type=collection&zoneIndexInPage=2",
"next": null,
"last": "https://api-internal.arte.tv/api/emac/v4/fr/web/zones/e0dfcff6-67bc-4d83-8055-8edf45687aea/content?abv=B&collectionId=RC-022923&page=1&pageId=collection&subCollectionId=RC-022924&type=collection&zoneIndexInPage=2"
}
}
}
},
{
"id": "5c5ab0fc-a32c-493c-bfa1-7d4eddca8834_RC-022923",
"code": "collection_upcoming_RC-022923",
"title": "Collection Upcomings",
"slug": null,
"description": null,
"displayOptions": {
"template": "horizontal-landscape",
"theme": null,
"showZoneTitle": true,
"showItemTitle": true
},
"link": null,
"authenticatedContent": null,
"content": {
"data": [],
"pagination": null
}
},
{
"id": "3e4b7126-60ee-4740-9aa6-e0a92af2bfe1_RC-022923",
"code": "collection_article_RC-022923",
"title": "Collection Articles",
"slug": null,
"description": null,
"displayOptions": {
"template": "horizontal-landscape",
"theme": null,
"showZoneTitle": true,
"showItemTitle": true
},
"link": null,
"authenticatedContent": null,
"content": {
"data": [],
"pagination": null
}
},
{
"id": "125ee988-ab24-4bdf-a62c-703717d02164_RC-022923",
"code": "collection_associated_RC-022923",
"title": "Sur le même thème",
"slug": null,
"description": null,
"displayOptions": {
"template": "horizontal-landscape",
"theme": null,
"showZoneTitle": true,
"showItemTitle": true
},
"link": null,
"authenticatedContent": null,
"content": {
"data": [],
"pagination": null
}
},
{
"id": "371699f8-dcfb-4abb-933a-83cc956a5d7d_RC-022923",
"code": "collection_partner_RC-022923",
"title": "Collection Partners",
"slug": null,
"description": null,
"displayOptions": {
"template": "horizontal-landscape",
"theme": null,
"showZoneTitle": true,
"showItemTitle": true
},
"link": null,
"authenticatedContent": null,
"content": {
"data": [],
"pagination": null
}
}
],
"stats": {
"xiti": {
"page_name": "Home_RC-022923_cry-wolf",
"chapter1": "SER_series-et-fictions",
"chapter2": "Série",
"chapter3": "RC-022923_cry-wolf",
"x1": "fr",
"x2": "Collection",
"s2": 1,
"siteId": "582046",
"env_work": "prod",
"search_keywords": null
}
},
"metadata": {
"title": "Cry Wolf",
"description": "Holly, 14 ans, accuse son beau-père de violences envers elle. Mais ses parents nient laccusation. Un assistant social atypique est chargé de démêler laffaire mais Holly subit les remontrances de sa mère qui laccuse de mentir. Drame familial perturbant, la série danoise \"Cry Wolf\" plonge dans lintimité dune famille apparemment bien sous tous rapports.",
"seo": {
"title": "Cry Wolf - Séries et fictions | ARTE",
"description": "Holly, 14 ans, accuse son beau-père de violences envers elle. Mais ses parents nient laccusation. Un assistant social atypique est chargé de démêler laffaire mais Holly subit les remontrances de sa mère qui laccuse de mentir. Drame familial perturbant, la série danoise \"Cry Wolf\" plonge dans lintimité dune famille apparemment bien sous tous rapports.",
"canonical": "/fr/videos/RC-022923/cry-wolf/"
},
"og": {
"image": {
"url": "https://api-cdn.arte.tv/img/v2/image/eFsiE6z6z3LXnBZj9A6SQ9/1920x1080?type=TEXT&watermark=true",
"width": 1920,
"height": 1080
}
},
"twitter": {
"image": {
"url": "https://api-cdn.arte.tv/img/v2/image/eFsiE6z6z3LXnBZj9A6SQ9/1920x1080?type=TEXT&watermark=true"
},
"site": "@ARTEfr"
}
},
"base": {
"type": "collections",
"redirect": null
}
}
},
"initialType": "collections",
"mamiBaseUrl": "https://api-cdn.arte.tv/api/mami/v1/",
"locale": "fr",
"tcStartFrom": null,
"abvGroups": "B",
"emacVersion": "v4"
},
"locale": "fr",
"footerProps": {
"main": [
{
"label": "Sites",
"href": null,
"links": [
{
"label": "ARTE VOD/DVD",
"kind": "internal",
"href": "https://boutique.arte.tv/",
"rel": null
},
{
"label": "ARTE Radio",
"kind": "internal",
"href": "https://www.arteradio.com/",
"rel": null
},
{
"label": "Coups de cœur culture",
"kind": "internal",
"href": "https://my.arte.tv/index.php?lang=fr&page=eventsFavorite",
"rel": null
},
{
"label": "Programmes en UHD ",
"kind": "internal",
"href": "https://www.arte.tv/fr/videos/RC-022710/nos-programmes-en-uhd/",
"rel": null
},
{
"label": "ARTE Info Plus - Décryptez l'actualité",
"kind": "internal",
"href": "https://www.arte.tv/fr/videos/RC-022628/arte-info-plus/",
"rel": null
}
]
},
{
"label": "Entreprise",
"href": null,
"links": [
{
"label": "Tout sur ARTE",
"kind": "internal",
"href": "https://www.arte.tv/sites/corporate/",
"rel": "nofollow"
},
{
"label": "Emplois et stages",
"kind": "internal",
"href": "https://www.arte.tv/sites/corporate/particuliers-professionnels/#offres-demploi-et-de-stages",
"rel": "nofollow"
},
{
"label": "Appels d'offres",
"kind": "internal",
"href": "https://www.arte.tv/sites/corporate/appels-doffres/",
"rel": "nofollow"
},
{
"label": "Aide & contact",
"kind": "internal",
"href": "https://www.arte.tv/sites/corporate/nous-repondons-a-vos-questions/",
"rel": "nofollow"
},
{
"label": "Streamer responsable",
"kind": "internal",
"href": "https://www.arte.tv/sites/corporate/streaming-responsable/",
"rel": "nofollow"
}
]
},
{
"label": "Infos légales",
"href": null,
"links": [
{
"label": "Gérer les cookies",
"kind": "cookie",
"href": null,
"rel": null
},
{
"label": "ARTE et vos données personnelles",
"kind": "internal",
"href": "https://www.arte.tv/sites/corporate/donnees-personnelles/",
"rel": "nofollow"
},
{
"label": "Mentions légales et crédits",
"kind": "internal",
"href": "https://www.arte.tv/sites/corporate/credits/",
"rel": "nofollow"
},
{
"label": "CGU",
"kind": "internal",
"href": "https://www.arte.tv/sites/corporate/conditions-generales-dutilisation-cgu/",
"rel": "nofollow"
},
{
"label": "Accessibilité",
"kind": "internal",
"href": "https://www.arte.tv/sites/corporate/accessibilite/",
"rel": "nofollow"
},
{
"label": "Plan du site",
"kind": "internal",
"href": "https://www.arte.tv/fr/sitemap/",
"rel": null
}
]
},
{
"label": "Réseaux sociaux",
"href": null,
"links": [
{
"label": "Facebook",
"kind": "external",
"href": "https://www.facebook.com/artetv",
"rel": "nofollow"
},
{
"label": "Instagram",
"kind": "external",
"href": "https://www.instagram.com/artefr",
"rel": "nofollow"
},
{
"label": "Youtube",
"kind": "external",
"href": "https://www.youtube.com/arteplus7",
"rel": "nofollow"
},
{
"label": "Twitter",
"kind": "external",
"href": "https://www.twitter.com/artefr",
"rel": "nofollow"
}
]
}
]
},
"geoblocking": "DE_FR",
"serverTime": "2023-01-23T21:29:41.265Z",
"__N_SSP": true
},
"page": "/videos/[...identifiers]",
"query": {
"identifiers": [
"RC-022923",
"cry-wolf"
]
},
"buildId": "230117085533",
"assetPrefix": "https://static-cdn.arte.tv/replay",
"runtimeConfig": {
"emacVersion": "v4",
"ebuBoxUrl": "https://reco.ebu.io/news-reco-arte.js",
"ffABTesting": true,
"ffDirectPlayerAutoPlay": "true",
"ffEbuBox": false,
"ffImagePlaceholder": false,
"ffLivePage": true,
"ffMeta": true,
"ffNewGuideTv": true,
"ffNewsletterZoneWithTeaserImage": false,
"ffPlayerAutoPlay": "if_available",
"ffProgramTrailer": false,
"ffProfileMenu": false,
"ffSettingsMenuTargetedComms": false,
"ffSidaction": false,
"ffThemeSwitch": false,
"ffAtInternetSrcForce": true,
"ffSettingsMenuVideoQuality": false,
"ffSliderMetaInfoButtonSeeMore": false,
"newsletterSubscribeUrl": "https://api.arte.tv/api/sso/v3/newsletter/subscribe",
"tagCommanderUrl": "https://cdn.tagcommander.com/3445"
},
"isFallback": false,
"gssp": true,
"customServer": true,
"appGip": true,
"locale": "fr",
"locales": [
"fr",
"de",
"en",
"es",
"pl",
"it"
],
"defaultLocale": "fr",
"scriptLoader": []
}

View File

@ -0,0 +1,703 @@
{
"props": {
"pageProps": {
"geoblocking": null,
"initialPage": {
"tag": "Ok",
"value": {
"code": "RC-023013",
"language": "fr",
"support": "web",
"type": "collection",
"level": 3,
"parent": {
"type": "category",
"page": "HIS",
"label": "Histoire",
"url": "/fr/videos/histoire/",
"deeplink": "arte://emac/HIS",
"id": "HIS_fr_web",
"slug": "histoire",
"parent": null
},
"alternativeLanguages": [
{
"code": "fr",
"label": "Français",
"page": "RC-023013",
"url": "/fr/videos/RC-023013/l-incroyable-periple-de-magellan/",
"title": "L'incroyable périple de Magellan"
},
{
"code": "de",
"label": "Deutsch",
"page": "RC-023013",
"url": "/de/videos/RC-023013/die-abenteuerliche-weltreise-des-magellan/",
"title": "Die abenteuerliche Weltreise des Magellan"
}
],
"url": "/fr/videos/RC-023013/l-incroyable-periple-de-magellan/",
"deeplink": "arte://collection/RC-023013",
"slug": "l-incroyable-periple-de-magellan",
"zones": [
{
"id": "51ba8da1-5389-43c4-af08-cca614192d77_RC-023013",
"code": "collection_content_RC-023013",
"title": "L'incroyable périple de Magellan",
"slug": null,
"description": null,
"displayOptions": {
"template": "single-collectionContent",
"theme": null,
"showZoneTitle": true,
"showItemTitle": true
},
"link": null,
"authenticatedContent": null,
"content": {
"data": [
{
"id": "RC-023013",
"type": "collection",
"kind": {
"code": "TV_SERIES",
"label": "Série",
"isCollection": true
},
"url": "/fr/videos/RC-023013/l-incroyable-periple-de-magellan/",
"deeplink": "arte://collection/RC-023013",
"title": "L'incroyable périple de Magellan",
"subtitle": null,
"shortDescription": "De 1519 à 1522, le navigateur portugais Fernand de Magellan et sa flotte réalisent le tout premier tour du monde par voie maritime. Une épopée hors du commun, empreinte d'erreurs et de trahisons mais aussi de rencontres. Cette série documentaire en quatre volets retrace un exploit maritime digne des plus grands romans daventure.",
"mainImage": {
"caption": "Magellan",
"url": "https://api-cdn.arte.tv/img/v2/image/yKtLtmVraMDiGhCYFv8bnU/__SIZE__"
},
"stickers": [
{
"code": "COLLECTION",
"label": "COLLECTION"
}
],
"trackingPixel": "/ct/?abv=A&language=fr&pageid=collection&position=1&support=web&teaserid=RC-023013&teasertitle=L%27incroyable%20p%C3%A9riple%20de%20Magellan&zoneCode=collection_content_RC-023013&zoneIndexInPage=0&zoneTemplate=single_collectionContent&zoneid=collection_content&zonename=L%27incroyable%20p%C3%A9riple%20de%20Magellan",
"trailer": null,
"partners": null,
"description": "De 1519 à 1522, le navigateur portugais Fernand de Magellan et sa flotte réalisent le tout premier tour du monde par voie maritime. Une épopée hors du commun, empreinte d'erreurs et de trahisons mais aussi de rencontres. Cette série documentaire en quatre volets retrace un exploit maritime digne des plus grands romans daventure.",
"video": null,
"availability": null
}
],
"pagination": null
}
},
{
"id": "d3bad9c8-b1c0-43db-87fb-fb95cc68766d_RC-023013",
"code": "collection_videos_RC-023013",
"title": "Toutes les vidéos",
"slug": null,
"description": null,
"displayOptions": {
"template": "verticalFirstHighlighted-landscape",
"theme": null,
"showZoneTitle": true,
"showItemTitle": true
},
"link": null,
"authenticatedContent": null,
"content": {
"data": [
{
"id": "093644-001-A_fr",
"type": "teaser",
"kind": {
"code": "SHOW",
"label": "Programme",
"isCollection": false
},
"url": "/fr/videos/093644-001-A/l-incroyable-periple-de-magellan-1-4/",
"deeplink": "arte://program/093644-001-A",
"title": "L'incroyable périple de Magellan (1/4)",
"subtitle": "Le partage du monde",
"shortDescription": "En 1519, entré au service du roi dEspagne, le navigateur portugais Fernand de Magellan dirige la flotte qui bouclera, sans lavoir décidé au départ, le premier tour du monde maritime de lhistoire. En quatre volets, le formidable récit dune expédition historique, marquée par les drames.",
"mainImage": {
"caption": null,
"url": "https://api-cdn.arte.tv/img/v2/image/8irEnBpEuNCBCzr3vW5nZT/__SIZE__"
},
"stickers": [
{
"code": "PLAYABLE",
"label": "PLAYABLE"
},
{
"code": "FULL_VIDEO",
"label": "Voir le programme"
}
],
"trackingPixel": "/ct/?abv=A&em=093644-001-A&language=fr&pageid=collection&position=1&support=web&teaserid=093644-001-A_fr&teasertitle=L%27incroyable%20p%C3%A9riple%20de%20Magellan%20%281%2F4%29&zoneCode=collection_videos_RC-023013&zoneIndexInPage=1&zoneTemplate=horizontal_landscape&zoneid=collection_videos&zonename=Toutes%20les%20vid%C3%A9os",
"programId": "093644-001-A",
"teaserText": "Entre 1519 et 1522, la flotte du navigateur portugais Fernand de Magellan réalisa le premier tour du monde de lhistoire.",
"duration": 3153,
"durationLabel": "53 min",
"geoblocking": {
"code": "SAT",
"label": "",
"inclusion": [],
"exclusion": []
},
"genre": null,
"audioVersions": [],
"availability": {
"type": "VOD",
"start": "2022-11-12T04:00:00Z",
"end": "2023-01-28T04:00:00Z",
"upcomingDate": "2022-11-12T04:00:00Z",
"label": "Disponible du 12/11/2022 au 27/01/2023"
},
"ageRating": 0,
"callToAction": "Regarder",
"clip": null,
"trailer": null,
"childrenCount": null
},
{
"id": "093644-002-A_fr",
"type": "teaser",
"kind": {
"code": "SHOW",
"label": "Programme",
"isCollection": false
},
"url": "/fr/videos/093644-002-A/l-incroyable-periple-de-magellan-2-4/",
"deeplink": "arte://program/093644-002-A",
"title": "L'incroyable périple de Magellan (2/4)",
"subtitle": "Voyage au bord du monde",
"shortDescription": "Entre 1519 et 1522, la flotte du navigateur portugais Fernand de Magellan réalisa le premier tour du monde de lhistoire. Deuxième volet du récit de cette expédition : pour rejoindre lOrient par lOccident, Magellan promet quil trouvera passage à travers lAmérique et quil réussira à rejoindre les Indes, là où Christophe Colomb avait échoué.",
"mainImage": {
"caption": null,
"url": "https://api-cdn.arte.tv/img/v2/image/4Ki6dV3KtNtVk624X2qcMK/__SIZE__"
},
"stickers": [
{
"code": "PLAYABLE",
"label": "PLAYABLE"
},
{
"code": "FULL_VIDEO",
"label": "Voir le programme"
}
],
"trackingPixel": "/ct/?abv=A&em=093644-002-A&language=fr&pageid=collection&position=2&support=web&teaserid=093644-002-A_fr&teasertitle=L%27incroyable%20p%C3%A9riple%20de%20Magellan%20%282%2F4%29&zoneCode=collection_videos_RC-023013&zoneIndexInPage=1&zoneTemplate=horizontal_landscape&zoneid=collection_videos&zonename=Toutes%20les%20vid%C3%A9os",
"programId": "093644-002-A",
"teaserText": "Pour rejoindre lOrient par lOccident, Magellan promet quil réussira à rejoindre les Indes, là où Christophe Colomb avait échoué.",
"duration": 3282,
"durationLabel": "55 min",
"geoblocking": {
"code": "SAT",
"label": "",
"inclusion": [],
"exclusion": []
},
"genre": null,
"audioVersions": [],
"availability": {
"type": "VOD",
"start": "2022-11-12T04:00:00Z",
"end": "2023-01-28T04:00:00Z",
"upcomingDate": "2022-11-12T04:00:00Z",
"label": "Disponible du 12/11/2022 au 27/01/2023"
},
"ageRating": 0,
"callToAction": "Regarder",
"clip": null,
"trailer": null,
"childrenCount": null
},
{
"id": "093644-003-A_fr",
"type": "teaser",
"kind": {
"code": "SHOW",
"label": "Programme",
"isCollection": false
},
"url": "/fr/videos/093644-003-A/l-incroyable-periple-de-magellan-3-4/",
"deeplink": "arte://program/093644-003-A",
"title": "L'incroyable périple de Magellan (3/4)",
"subtitle": "Le royaume de Magellan",
"shortDescription": "Entre 1519 et 1522, la flotte du navigateur portugais Fernand de Magellan réalisa le premier tour du monde de lhistoire. Troisième volet du récit de cette expédition : longeant la côte sud-américaine au-delà du Brésil, Magellan découvre au sud de lArgentine un passage qui lui permet de s'enfoncer dans le continent américain.",
"mainImage": {
"caption": null,
"url": "https://api-cdn.arte.tv/img/v2/image/aF8bDFGRTD6pm9ocsR7LRG/__SIZE__"
},
"stickers": [
{
"code": "PLAYABLE",
"label": "PLAYABLE"
},
{
"code": "FULL_VIDEO",
"label": "Voir le programme"
}
],
"trackingPixel": "/ct/?abv=A&em=093644-003-A&language=fr&pageid=collection&position=3&support=web&teaserid=093644-003-A_fr&teasertitle=L%27incroyable%20p%C3%A9riple%20de%20Magellan%20%283%2F4%29&zoneCode=collection_videos_RC-023013&zoneIndexInPage=1&zoneTemplate=horizontal_landscape&zoneid=collection_videos&zonename=Toutes%20les%20vid%C3%A9os",
"programId": "093644-003-A",
"teaserText": "Longeant la côte au-delà du Brésil, Magellan découvre un passage qui lui permet de s'enfoncer dans le continent américain.",
"duration": 3089,
"durationLabel": "52 min",
"geoblocking": {
"code": "SAT",
"label": "",
"inclusion": [],
"exclusion": []
},
"genre": null,
"audioVersions": [],
"availability": {
"type": "VOD",
"start": "2022-11-12T04:00:00Z",
"end": "2023-01-28T04:00:00Z",
"upcomingDate": "2022-11-12T04:00:00Z",
"label": "Disponible du 12/11/2022 au 27/01/2023"
},
"ageRating": 0,
"callToAction": "Regarder",
"clip": null,
"trailer": null,
"childrenCount": null
},
{
"id": "093644-004-A_fr",
"type": "teaser",
"kind": {
"code": "SHOW",
"label": "Programme",
"isCollection": false
},
"url": "/fr/videos/093644-004-A/l-incroyable-periple-de-magellan-4-4/",
"deeplink": "arte://program/093644-004-A",
"title": "L'incroyable périple de Magellan (4/4)",
"subtitle": "Le premier tour du monde",
"shortDescription": "Entre 1519 et 1522, la flotte du navigateur portugais Fernand de Magellan réalisa le premier tour du monde de lhistoire. Dernier volet : privé de Magellan, qui a succombé aux Philippines le 27 avril 1521, ainsi que de leurs meilleurs officiers, assassinés lors du piège tendu par le chef Lapu-Lapu, les équipages des deux navires restants poursuivent leur route.",
"mainImage": {
"caption": null,
"url": "https://api-cdn.arte.tv/img/v2/image/Qc5BebADZ6mrCvdnZaoxE8/__SIZE__"
},
"stickers": [
{
"code": "PLAYABLE",
"label": "PLAYABLE"
},
{
"code": "FULL_VIDEO",
"label": "Voir le programme"
}
],
"trackingPixel": "/ct/?abv=A&em=093644-004-A&language=fr&pageid=collection&position=4&support=web&teaserid=093644-004-A_fr&teasertitle=L%27incroyable%20p%C3%A9riple%20de%20Magellan%20%284%2F4%29&zoneCode=collection_videos_RC-023013&zoneIndexInPage=1&zoneTemplate=horizontal_landscape&zoneid=collection_videos&zonename=Toutes%20les%20vid%C3%A9os",
"programId": "093644-004-A",
"teaserText": "Privé de Magellan, qui a succombé aux Philippines le 27 avril 1521, les équipages des deux navires restants poursuivent leur route.",
"duration": 3197,
"durationLabel": "54 min",
"geoblocking": {
"code": "SAT",
"label": "",
"inclusion": [],
"exclusion": []
},
"genre": null,
"audioVersions": [],
"availability": {
"type": "VOD",
"start": "2022-11-12T04:00:00Z",
"end": "2023-01-28T04:00:00Z",
"upcomingDate": "2022-11-12T04:00:00Z",
"label": "Disponible du 12/11/2022 au 27/01/2023"
},
"ageRating": 0,
"callToAction": "Regarder",
"clip": null,
"trailer": null,
"childrenCount": null
}
],
"pagination": {
"page": 1,
"pages": 1,
"totalCount": 4,
"links": {
"first": "https://api-internal.arte.tv/api/emac/v4/fr/web/zones/d3bad9c8-b1c0-43db-87fb-fb95cc68766d/content?abv=A&collectionId=RC-023013&page=1&pageId=collection&type=collection&zoneIndexInPage=1",
"next": null,
"last": "https://api-internal.arte.tv/api/emac/v4/fr/web/zones/d3bad9c8-b1c0-43db-87fb-fb95cc68766d/content?abv=A&collectionId=RC-023013&page=1&pageId=collection&type=collection&zoneIndexInPage=1"
}
}
}
},
{
"id": "5c5ab0fc-a32c-493c-bfa1-7d4eddca8834_RC-023013",
"code": "collection_upcoming_RC-023013",
"title": "Collection Upcomings",
"slug": null,
"description": null,
"displayOptions": {
"template": "horizontal-landscape",
"theme": null,
"showZoneTitle": true,
"showItemTitle": true
},
"link": null,
"authenticatedContent": null,
"content": {
"data": [],
"pagination": null
}
},
{
"id": "3e4b7126-60ee-4740-9aa6-e0a92af2bfe1_RC-023013",
"code": "collection_article_RC-023013",
"title": "Collection Articles",
"slug": null,
"description": null,
"displayOptions": {
"template": "horizontal-landscape",
"theme": null,
"showZoneTitle": true,
"showItemTitle": true
},
"link": null,
"authenticatedContent": null,
"content": {
"data": [],
"pagination": null
}
},
{
"id": "125ee988-ab24-4bdf-a62c-703717d02164_RC-023013",
"code": "collection_associated_RC-023013",
"title": "Sur le même thème",
"slug": null,
"description": null,
"displayOptions": {
"template": "horizontal-landscape",
"theme": null,
"showZoneTitle": true,
"showItemTitle": true
},
"link": null,
"authenticatedContent": null,
"content": {
"data": [
{
"id": "RC-020554",
"type": "teaser",
"kind": {
"code": "TOPIC",
"label": "Collection",
"isCollection": true
},
"url": "/fr/videos/RC-020554/les-series-documentaires-d-histoire/",
"deeplink": "arte://collection/RC-020554",
"title": "Les séries documentaires d'histoire",
"subtitle": null,
"shortDescription": null,
"mainImage": {
"caption": "Séries Doc",
"url": "https://api-cdn.arte.tv/img/v2/image/nzpxHm9yHWryG3TfPQMnSR/__SIZE__"
},
"stickers": null,
"trackingPixel": "/ct/?abv=A&em=RC-020554&language=fr&pageid=collection&position=1&support=web&teaserid=RC-020554&teasertitle=Les%20s%C3%A9ries%20documentaires%20d%27histoire&zoneCode=collection_associated_RC-023013&zoneIndexInPage=4&zoneTemplate=horizontal_landscape&zoneid=collection_associated&zonename=Sur%20le%20m%C3%AAme%20th%C3%A8me",
"programId": "RC-020554",
"teaserText": null,
"duration": null,
"durationLabel": "Collection",
"geoblocking": null,
"genre": null,
"audioVersions": [],
"availability": null,
"ageRating": null,
"callToAction": "Watch",
"clip": null,
"trailer": null,
"childrenCount": null
}
],
"pagination": {
"page": 1,
"pages": 1,
"totalCount": 1,
"links": {
"first": "https://api-internal.arte.tv/api/emac/v4/fr/web/zones/125ee988-ab24-4bdf-a62c-703717d02164/content?abv=A&collectionId=RC-023013&page=1&pageId=collection&type=collection&zoneIndexInPage=4",
"next": null,
"last": "https://api-internal.arte.tv/api/emac/v4/fr/web/zones/125ee988-ab24-4bdf-a62c-703717d02164/content?abv=A&collectionId=RC-023013&page=1&pageId=collection&type=collection&zoneIndexInPage=4"
}
}
}
},
{
"id": "371699f8-dcfb-4abb-933a-83cc956a5d7d_RC-023013",
"code": "collection_partner_RC-023013",
"title": "Collection Partners",
"slug": null,
"description": null,
"displayOptions": {
"template": "horizontal-landscape",
"theme": null,
"showZoneTitle": true,
"showItemTitle": true
},
"link": null,
"authenticatedContent": null,
"content": {
"data": [],
"pagination": null
}
}
],
"stats": {
"xiti": {
"page_name": "Home_RC-023013_l-incroyable-periple-de-magellan",
"chapter1": "HIS_histoire",
"chapter2": "Série",
"chapter3": "RC-023013_l-incroyable-periple-de-magellan",
"x1": "fr",
"x2": "Collection",
"s2": 1,
"siteId": "582046",
"env_work": "prod",
"search_keywords": null
}
},
"metadata": {
"title": "L'incroyable périple de Magellan",
"description": "De 1519 à 1522, le navigateur portugais Fernand de Magellan et sa flotte réalisent le tout premier tour du monde par voie maritime. Une épopée hors du commun, empreinte d'erreurs et de trahisons mais aussi de rencontres. Cette série documentaire en quatre volets retrace un exploit maritime digne des plus grands romans daventure.",
"seo": {
"title": "L'incroyable périple de Magellan - Histoire | ARTE",
"description": "De 1519 à 1522, le navigateur portugais Fernand de Magellan et sa flotte réalisent le tout premier tour du monde par voie maritime. Une épopée hors du commun, empreinte d'erreurs et de trahisons mais aussi de rencontres. Cette série documentaire en quatre volets retrace un exploit maritime digne des plus grands romans daventure.",
"canonical": "/fr/videos/RC-023013/l-incroyable-periple-de-magellan/"
},
"og": {
"image": {
"url": "https://api-cdn.arte.tv/img/v2/image/yKtLtmVraMDiGhCYFv8bnU/1920x1080?type=TEXT&watermark=true",
"width": 1920,
"height": 1080
}
},
"twitter": {
"image": {
"url": "https://api-cdn.arte.tv/img/v2/image/yKtLtmVraMDiGhCYFv8bnU/1920x1080?type=TEXT&watermark=true"
},
"site": "@ARTEfr"
}
},
"base": {
"type": "collections",
"redirect": null
}
}
},
"initialType": "collections",
"mamiBaseUrl": "https://api-cdn.arte.tv/api/mami/v1/",
"locale": "fr",
"tcStartFrom": null,
"abvGroups": "A",
"emacVersion": "v4"
},
"locale": "fr",
"footerProps": {
"main": [
{
"label": "Sites",
"href": null,
"links": [
{
"label": "ARTE VOD/DVD",
"kind": "internal",
"href": "https://boutique.arte.tv/",
"rel": null
},
{
"label": "ARTE Radio",
"kind": "internal",
"href": "https://www.arteradio.com/",
"rel": null
},
{
"label": "Coups de cœur culture",
"kind": "internal",
"href": "https://my.arte.tv/index.php?lang=fr&page=eventsFavorite",
"rel": null
},
{
"label": "Programmes en UHD ",
"kind": "internal",
"href": "https://www.arte.tv/fr/videos/RC-022710/nos-programmes-en-uhd/",
"rel": null
},
{
"label": "ARTE Info Plus - Décryptez l'actualité",
"kind": "internal",
"href": "https://www.arte.tv/fr/videos/RC-022628/arte-info-plus/",
"rel": null
}
]
},
{
"label": "Entreprise",
"href": null,
"links": [
{
"label": "Tout sur ARTE",
"kind": "internal",
"href": "https://www.arte.tv/sites/corporate/",
"rel": "nofollow"
},
{
"label": "Emplois et stages",
"kind": "internal",
"href": "https://www.arte.tv/sites/corporate/particuliers-professionnels/#offres-demploi-et-de-stages",
"rel": "nofollow"
},
{
"label": "Appels d'offres",
"kind": "internal",
"href": "https://www.arte.tv/sites/corporate/appels-doffres/",
"rel": "nofollow"
},
{
"label": "Aide & contact",
"kind": "internal",
"href": "https://www.arte.tv/sites/corporate/nous-repondons-a-vos-questions/",
"rel": "nofollow"
},
{
"label": "Streamer responsable",
"kind": "internal",
"href": "https://www.arte.tv/sites/corporate/streaming-responsable/",
"rel": "nofollow"
}
]
},
{
"label": "Infos légales",
"href": null,
"links": [
{
"label": "Gérer les cookies",
"kind": "cookie",
"href": null,
"rel": null
},
{
"label": "ARTE et vos données personnelles",
"kind": "internal",
"href": "https://www.arte.tv/sites/corporate/donnees-personnelles/",
"rel": "nofollow"
},
{
"label": "Mentions légales et crédits",
"kind": "internal",
"href": "https://www.arte.tv/sites/corporate/credits/",
"rel": "nofollow"
},
{
"label": "CGU",
"kind": "internal",
"href": "https://www.arte.tv/sites/corporate/conditions-generales-dutilisation-cgu/",
"rel": "nofollow"
},
{
"label": "Accessibilité",
"kind": "internal",
"href": "https://www.arte.tv/sites/corporate/accessibilite/",
"rel": "nofollow"
},
{
"label": "Plan du site",
"kind": "internal",
"href": "https://www.arte.tv/fr/sitemap/",
"rel": null
}
]
},
{
"label": "Réseaux sociaux",
"href": null,
"links": [
{
"label": "Facebook",
"kind": "external",
"href": "https://www.facebook.com/artetv",
"rel": "nofollow"
},
{
"label": "Instagram",
"kind": "external",
"href": "https://www.instagram.com/artefr",
"rel": "nofollow"
},
{
"label": "Youtube",
"kind": "external",
"href": "https://www.youtube.com/arteplus7",
"rel": "nofollow"
},
{
"label": "Twitter",
"kind": "external",
"href": "https://www.twitter.com/artefr",
"rel": "nofollow"
}
]
}
]
},
"geoblocking": "DE_FR",
"serverTime": "2023-01-24T09:06:38.559Z",
"__N_SSP": true
},
"page": "/videos/[...identifiers]",
"query": {
"identifiers": [
"RC-023013",
"l-incroyable-periple-de-magellan"
]
},
"buildId": "230117085533",
"assetPrefix": "https://static-cdn.arte.tv/replay",
"runtimeConfig": {
"emacVersion": "v4",
"ebuBoxUrl": "https://reco.ebu.io/news-reco-arte.js",
"ffABTesting": true,
"ffDirectPlayerAutoPlay": "true",
"ffEbuBox": false,
"ffImagePlaceholder": false,
"ffLivePage": true,
"ffMeta": true,
"ffNewGuideTv": true,
"ffNewsletterZoneWithTeaserImage": false,
"ffPlayerAutoPlay": "if_available",
"ffProgramTrailer": false,
"ffProfileMenu": false,
"ffSettingsMenuTargetedComms": false,
"ffSidaction": false,
"ffThemeSwitch": false,
"ffAtInternetSrcForce": true,
"ffSettingsMenuVideoQuality": false,
"ffSliderMetaInfoButtonSeeMore": false,
"newsletterSubscribeUrl": "https://api.arte.tv/api/sso/v3/newsletter/subscribe",
"tagCommanderUrl": "https://cdn.tagcommander.com/3445"
},
"isFallback": false,
"gssp": true,
"customServer": true,
"appGip": true,
"locale": "fr",
"locales": [
"fr",
"de",
"en",
"es",
"pl",
"it"
],
"defaultLocale": "fr",
"scriptLoader": []
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,948 @@
{
"props": {
"pageProps": {
"geoblocking": null,
"initialPage": {
"tag": "Ok",
"value": {
"code": "RC-023242",
"language": "fr",
"support": "web",
"type": "collection",
"level": 3,
"parent": {
"type": "category",
"page": "HIS",
"label": "Histoire",
"url": "/fr/videos/histoire/",
"deeplink": "arte://emac/HIS",
"id": "HIS_fr_web",
"slug": "histoire",
"parent": null
},
"alternativeLanguages": [
{
"code": "fr",
"label": "Français",
"page": "RC-023242",
"url": "/fr/videos/RC-023242/bandes-de-pirates/",
"title": "Bandes de pirates !"
},
{
"code": "de",
"label": "Deutsch",
"page": "RC-023242",
"url": "/de/videos/RC-023242/die-schrecken-der-meere/",
"title": "Die Schrecken der Meere"
}
],
"url": "/fr/videos/RC-023242/bandes-de-pirates/",
"deeplink": "arte://collection/RC-023242",
"slug": "bandes-de-pirates",
"zones": [
{
"id": "51ba8da1-5389-43c4-af08-cca614192d77_RC-023242",
"code": "collection_content_RC-023242",
"title": "Bandes de pirates !",
"slug": null,
"description": null,
"displayOptions": {
"template": "single-collectionContent",
"theme": null,
"showZoneTitle": true,
"showItemTitle": true
},
"link": null,
"authenticatedContent": null,
"content": {
"data": [
{
"id": "RC-023242",
"type": "collection",
"kind": {
"code": "TOPIC",
"label": "Collection",
"isCollection": true
},
"url": "/fr/videos/RC-023242/bandes-de-pirates/",
"deeplink": "arte://collection/RC-023242",
"title": "Bandes de pirates !",
"subtitle": "Les aventuriers des océans",
"shortDescription": "Aux XVII et XVIIIe siècles, les pirates et les corsaires sillonent les océans en quête d'aventures. Ils sont redoutables et intriguants et, du Capitaine Horatio Hornblower aux corsaires napoléonniens en passant par Francis Drake, ils ont alimenté nos imaginaires et contribuent encore à de fameuses légendes.",
"mainImage": {
"caption": "Pirates & Co.",
"url": "https://api-cdn.arte.tv/img/v2/image/Jhjhuk8piMeNtdBsdM8Tjf/__SIZE__"
},
"stickers": [
{
"code": "COLLECTION",
"label": "COLLECTION"
}
],
"trackingPixel": "/ct/?abv=A&language=fr&pageid=collection&position=1&support=web&teaserid=RC-023242&teasertitle=Bandes%20de%20pirates%20%21&zoneCode=collection_content_RC-023242&zoneIndexInPage=0&zoneTemplate=single_collectionContent&zoneid=collection_content&zonename=Bandes%20de%20pirates%20%21",
"trailer": null,
"partners": null,
"description": "Aux XVII et XVIIIe siècles, les pirates et les corsaires sillonent les océans en quête d'aventures. Ils sont redoutables et intriguants et, du Capitaine Horatio Hornblower aux corsaires napoléonniens en passant par Francis Drake, ils ont alimenté nos imaginaires et contribuent encore à de fameuses légendes.",
"video": null,
"availability": null
}
],
"pagination": null
}
},
{
"id": "d3bad9c8-b1c0-43db-87fb-fb95cc68766d_RC-023242",
"code": "collection_videos_RC-023242",
"title": "Toutes les vidéos",
"slug": null,
"description": null,
"displayOptions": {
"template": "verticalFirstHighlighted-landscape",
"theme": null,
"showZoneTitle": true,
"showItemTitle": true
},
"link": null,
"authenticatedContent": null,
"content": {
"data": [
{
"id": "100814-000-A_fr",
"type": "teaser",
"kind": {
"code": "SHOW",
"label": "Programme",
"isCollection": false
},
"url": "/fr/videos/100814-000-A/la-veritable-histoire-des-pirates/",
"deeplink": "arte://program/100814-000-A",
"title": "La véritable histoire des pirates",
"subtitle": null,
"shortDescription": "Figures populaires de la littérature et du cinéma, les pirates ont écumé les mers aux XVIIe et XVIIIe siècles. Dans le sillage de deux campagnes de fouilles dirigées par larchéologue Jean Soulat, Stéphane Bégoin nous entraîne sur leurs traces à lîle Maurice et à Madagascar.",
"mainImage": {
"caption": null,
"url": "https://api-cdn.arte.tv/img/v2/image/9fzDtM4epMJC4iJ3XoAFdT/__SIZE__"
},
"stickers": [
{
"code": "PLAYABLE",
"label": "PLAYABLE"
},
{
"code": "FULL_VIDEO",
"label": "Voir le programme"
}
],
"trackingPixel": "/ct/?abv=A&em=100814-000-A&language=fr&pageid=collection&position=1&support=web&teaserid=100814-000-A_fr&teasertitle=La%20v%C3%A9ritable%20histoire%20des%20pirates&zoneCode=collection_videos_RC-023242&zoneIndexInPage=1&zoneTemplate=horizontal_landscape&zoneid=collection_videos&zonename=Toutes%20les%20vid%C3%A9os",
"programId": "100814-000-A",
"teaserText": "Qui étaient ces flibustiers qui sillonnaient les mers aux XVIIe et XVIIIe siècles pour semparer des cargaisons des navires marchands ?",
"duration": 5523,
"durationLabel": "93 min",
"geoblocking": {
"code": "SAT",
"label": "",
"inclusion": [],
"exclusion": []
},
"genre": null,
"audioVersions": [],
"availability": {
"type": "VOD",
"start": "2022-12-31T04:00:00Z",
"end": "2023-03-08T04:00:00Z",
"upcomingDate": "2022-12-31T04:00:00Z",
"label": "Disponible du 31/12/2022 au 07/03/2023"
},
"ageRating": 0,
"callToAction": "Regarder",
"clip": null,
"trailer": null,
"childrenCount": null
},
{
"id": "063615-000-A_fr",
"type": "teaser",
"kind": {
"code": "SHOW",
"label": "Programme",
"isCollection": false
},
"url": "/fr/videos/063615-000-A/a-la-decouverte-des-caraibes/",
"deeplink": "arte://program/063615-000-A",
"title": "À la découverte des Caraïbes",
"subtitle": null,
"shortDescription": "La biodiversité des îles caribéennes recèle de nombreux trésors, malheureusement les Caraïbes ne sont épargnées ni par les séismes ni par les cyclones aux effets dévastateurs. Explications.",
"mainImage": {
"caption": null,
"url": "https://api-cdn.arte.tv/img/v2/image/GKijReRUWQmpeMGPRQ6CVR/__SIZE__"
},
"stickers": [
{
"code": "PLAYABLE",
"label": "PLAYABLE"
},
{
"code": "FULL_VIDEO",
"label": "Voir le programme"
}
],
"trackingPixel": "/ct/?abv=A&em=063615-000-A&language=fr&pageid=collection&position=2&support=web&teaserid=063615-000-A_fr&teasertitle=%C3%80%20la%20d%C3%A9couverte%20des%20Cara%C3%AFbes&zoneCode=collection_videos_RC-023242&zoneIndexInPage=1&zoneTemplate=horizontal_landscape&zoneid=collection_videos&zonename=Toutes%20les%20vid%C3%A9os",
"programId": "063615-000-A",
"teaserText": "Une riche biodiversité se maintient dans cette région frappée par les séismes et les cyclones.",
"duration": 3092,
"durationLabel": "52 min",
"geoblocking": {
"code": "EUR_DE_FR",
"label": "",
"inclusion": [],
"exclusion": []
},
"genre": null,
"audioVersions": [],
"availability": {
"type": "VOD",
"start": "2023-01-06T04:00:00Z",
"end": "2023-02-06T04:00:00Z",
"upcomingDate": "2023-01-06T04:00:00Z",
"label": "Disponible du 06/01/2023 au 05/02/2023"
},
"ageRating": 0,
"callToAction": "Regarder",
"clip": null,
"trailer": null,
"childrenCount": null
},
{
"id": "056769-002-A_fr",
"type": "teaser",
"kind": {
"code": "SHOW",
"label": "Programme",
"isCollection": false
},
"url": "/fr/videos/056769-002-A/les-corsaires-barbaresques/",
"deeplink": "arte://program/056769-002-A",
"title": "Les corsaires barbaresques",
"subtitle": null,
"shortDescription": "Au XVIIIe siècle, l'Europe est terrorisée par les corsaires barbaresques. Ces derniers partent en quête de \"l'or blanc\" d'alors, cest-à-dire des Européens des deux sexes, à la peau claire, qui seront vendus comme esclaves en Afrique du Nord et en Orient.",
"mainImage": {
"caption": "Pirates",
"url": "https://api-cdn.arte.tv/img/v2/image/7VkG7xfCTvTwJc4amGZXnZ/__SIZE__"
},
"stickers": [
{
"code": "PLAYABLE",
"label": "PLAYABLE"
},
{
"code": "FULL_VIDEO",
"label": "Voir le programme"
}
],
"trackingPixel": "/ct/?abv=A&em=056769-002-A&language=fr&pageid=collection&position=3&support=web&teaserid=056769-002-A_fr&teasertitle=Les%20corsaires%20barbaresques&zoneCode=collection_videos_RC-023242&zoneIndexInPage=1&zoneTemplate=horizontal_landscape&zoneid=collection_videos&zonename=Toutes%20les%20vid%C3%A9os",
"programId": "056769-002-A",
"teaserText": "Qui sont les corsaires barbaresques qui terrorisèrent l'Europe au XVIIIe siècle ?",
"duration": 3000,
"durationLabel": "50 min",
"geoblocking": {
"code": "DE_FR",
"label": "",
"inclusion": [],
"exclusion": []
},
"genre": null,
"audioVersions": [],
"availability": {
"type": "VOD",
"start": "2023-01-06T04:00:00Z",
"end": "2023-02-06T04:00:00Z",
"upcomingDate": "2023-01-06T04:00:00Z",
"label": "Disponible du 06/01/2023 au 05/02/2023"
},
"ageRating": 0,
"callToAction": "Regarder",
"clip": null,
"trailer": null,
"childrenCount": null
},
{
"id": "056769-001-A_fr",
"type": "teaser",
"kind": {
"code": "SHOW",
"label": "Programme",
"isCollection": false
},
"url": "/fr/videos/056769-001-A/francis-drake-corsaire-de-sa-majeste/",
"deeplink": "arte://program/056769-001-A",
"title": "Francis Drake, corsaire de Sa Majesté",
"subtitle": null,
"shortDescription": "XVIe siècle. Après la découverte de l'Amérique, le monde est dominé par lempire espagnol du très catholique Philippe II. La jeune souveraine anglicane Élisabeth Ire s'en inquiète et veut que son royaume devienne une grande puissance maritime. Pour cela, elle fait appel au fascinant Francis Drake qui avait commencé comme simple mousse.",
"mainImage": {
"caption": null,
"url": "https://api-cdn.arte.tv/img/v2/image/xS2y6wbE52WidzWpAYy6fW/__SIZE__"
},
"stickers": [
{
"code": "PLAYABLE",
"label": "PLAYABLE"
},
{
"code": "FULL_VIDEO",
"label": "Voir le programme"
}
],
"trackingPixel": "/ct/?abv=A&em=056769-001-A&language=fr&pageid=collection&position=4&support=web&teaserid=056769-001-A_fr&teasertitle=Francis%20Drake%2C%20corsaire%20de%20Sa%20Majest%C3%A9&zoneCode=collection_videos_RC-023242&zoneIndexInPage=1&zoneTemplate=horizontal_landscape&zoneid=collection_videos&zonename=Toutes%20les%20vid%C3%A9os",
"programId": "056769-001-A",
"teaserText": "Voulant que son royaume devienne une grande puissance maritime, Élisabeth Ire fait appel au fascinant corsaire Francis Drake.",
"duration": 3136,
"durationLabel": "53 min",
"geoblocking": {
"code": "ALL",
"label": "",
"inclusion": [],
"exclusion": []
},
"genre": null,
"audioVersions": [],
"availability": {
"type": "VOD",
"start": "2023-01-06T04:00:00Z",
"end": "2023-02-06T04:00:00Z",
"upcomingDate": "2023-01-06T04:00:00Z",
"label": "Disponible du 06/01/2023 au 05/02/2023"
},
"ageRating": 0,
"callToAction": "Regarder",
"clip": null,
"trailer": null,
"childrenCount": null
},
{
"id": "094484-002-A_fr",
"type": "teaser",
"kind": {
"code": "SHOW",
"label": "Programme",
"isCollection": false
},
"url": "/fr/videos/094484-002-A/faire-l-histoire/",
"deeplink": "arte://program/094484-002-A",
"title": "Faire l'histoire",
"subtitle": "Le drapeau pirate, contre les nations",
"shortDescription": "Pièce maitresse du déguisement des enfants, le drapeau pirate est pourtant loin d'être un objet anecdotique. Guillaume Calafat nous explique qu'à travers l'histoire de cet étendard familier se joue une partie décisive pour les Etats-nations qui, à partir du XVIe siècle, se forgent autant sur les mers que par leurs frontières terrestres. Le drapeau pirate devient ainsi le révélateur indirect d'une histoire de la souveraineté.",
"mainImage": {
"caption": "Le drapeau pirate",
"url": "https://api-cdn.arte.tv/img/v2/image/tHRt5eBbkC4LEpTjKiRiiU/__SIZE__"
},
"stickers": [
{
"code": "PLAYABLE",
"label": "PLAYABLE"
},
{
"code": "FULL_VIDEO",
"label": "Voir le programme"
}
],
"trackingPixel": "/ct/?abv=A&em=094484-002-A&language=fr&pageid=collection&position=5&support=web&teaserid=094484-002-A_fr&teasertitle=Faire%20l%27histoire&zoneCode=collection_videos_RC-023242&zoneIndexInPage=1&zoneTemplate=horizontal_landscape&zoneid=collection_videos&zonename=Toutes%20les%20vid%C3%A9os",
"programId": "094484-002-A",
"teaserText": "Quand le drapeau pirate devient le révélateur indirect d'une histoire de la souveraineté.",
"duration": 1028,
"durationLabel": "18 min",
"geoblocking": {
"code": "SAT",
"label": "",
"inclusion": [],
"exclusion": []
},
"genre": null,
"audioVersions": [],
"availability": {
"type": "VOD",
"start": "2021-09-16T09:10:00Z",
"end": "2024-08-12T21:59:00Z",
"upcomingDate": "2021-09-16T09:10:00Z",
"label": "Disponible du 16/09/2021 au 12/08/2024"
},
"ageRating": 0,
"callToAction": "Regarder",
"clip": null,
"trailer": null,
"childrenCount": null
},
{
"id": "047323-001-A_fr",
"type": "teaser",
"kind": {
"code": "SHOW",
"label": "Programme",
"isCollection": false
},
"url": "/fr/videos/047323-001-A/iles-de-beautes/",
"deeplink": "arte://program/047323-001-A",
"title": "Îles de beautés",
"subtitle": "Zanzibar",
"shortDescription": "Zanzibar se situe au large des côtes africaines, dans l'océan Indien. La jungle, vierge de présence humaine, reste le domaine de rongeurs géants, de singes mangeurs de charbon et de chauves-souris frugivores.",
"mainImage": {
"caption": null,
"url": "https://api-cdn.arte.tv/img/v2/image/rYc6LmUEYCrPTi5LxhkxQW/__SIZE__"
},
"stickers": [
{
"code": "PLAYABLE",
"label": "PLAYABLE"
},
{
"code": "FULL_VIDEO",
"label": "Voir le programme"
}
],
"trackingPixel": "/ct/?abv=A&em=047323-001-A&language=fr&pageid=collection&position=6&support=web&teaserid=047323-001-A_fr&teasertitle=%C3%8Eles%20de%20beaut%C3%A9s&zoneCode=collection_videos_RC-023242&zoneIndexInPage=1&zoneTemplate=horizontal_landscape&zoneid=collection_videos&zonename=Toutes%20les%20vid%C3%A9os",
"programId": "047323-001-A",
"teaserText": "Au large des côtes africaines, dans l'océan Indien, Zanzibar a gardé un écosystème et une nature vierge.",
"duration": 2583,
"durationLabel": "44 min",
"geoblocking": {
"code": "SAT",
"label": "",
"inclusion": [],
"exclusion": []
},
"genre": null,
"audioVersions": [],
"availability": {
"type": "VOD",
"start": "2023-01-06T04:00:00Z",
"end": "2023-04-07T03:00:00Z",
"upcomingDate": "2023-01-06T04:00:00Z",
"label": "Disponible du 06/01/2023 au 06/04/2023"
},
"ageRating": 0,
"callToAction": "Regarder",
"clip": null,
"trailer": null,
"childrenCount": null
},
{
"id": "047323-003-A_fr",
"type": "teaser",
"kind": {
"code": "SHOW",
"label": "Programme",
"isCollection": false
},
"url": "/fr/videos/047323-003-A/iles-de-beautes/",
"deeplink": "arte://program/047323-003-A",
"title": "Îles de beautés",
"subtitle": "Sri Lanka",
"shortDescription": "À la découverte des faunes étranges qui peuplent le Sri Lanka. Située à quelques degrés de latitude nord, l'île émerge de l'océan Indien. Ses plaines vivent au rythme de la mousson. Inondées deux fois l'an, en mai et en octobre, elles souffrent de la sécheresse le reste de l'année.",
"mainImage": {
"caption": null,
"url": "https://api-cdn.arte.tv/img/v2/image/9xHhLrauwcoq7VGCRb82PV/__SIZE__"
},
"stickers": [
{
"code": "PLAYABLE",
"label": "PLAYABLE"
},
{
"code": "FULL_VIDEO",
"label": "Voir le programme"
}
],
"trackingPixel": "/ct/?abv=A&em=047323-003-A&language=fr&pageid=collection&position=7&support=web&teaserid=047323-003-A_fr&teasertitle=%C3%8Eles%20de%20beaut%C3%A9s&zoneCode=collection_videos_RC-023242&zoneIndexInPage=1&zoneTemplate=horizontal_landscape&zoneid=collection_videos&zonename=Toutes%20les%20vid%C3%A9os",
"programId": "047323-003-A",
"teaserText": "Inondées par la mousson en mai et en octobre, les plaines du Sri Lanka souffrent de la sécheresse le reste de l'année.",
"duration": 2590,
"durationLabel": "44 min",
"geoblocking": {
"code": "SAT",
"label": "",
"inclusion": [],
"exclusion": []
},
"genre": null,
"audioVersions": [],
"availability": {
"type": "VOD",
"start": "2023-01-06T04:00:00Z",
"end": "2023-04-07T03:00:00Z",
"upcomingDate": "2023-01-06T04:00:00Z",
"label": "Disponible du 06/01/2023 au 06/04/2023"
},
"ageRating": 0,
"callToAction": "Regarder",
"clip": null,
"trailer": null,
"childrenCount": null
},
{
"id": "047323-004-A_fr",
"type": "teaser",
"kind": {
"code": "SHOW",
"label": "Programme",
"isCollection": false
},
"url": "/fr/videos/047323-004-A/iles-de-beautes/",
"deeplink": "arte://program/047323-004-A",
"title": "Îles de beautés",
"subtitle": "Les Caraïbes",
"shortDescription": "À la découverte des faunes étranges qui peuplent les Caraïbes, véritables laboratoires de l'évolution. Les plages de Trinidad sont un des lieux de ponte favoris des tortues-luth, qui sillonnaient déjà les mers du temps des dinosaures. Autre emblème des Caraïbes, le minuscule colibri, l'oiseau au métabolisme le plus rapide du monde",
"mainImage": {
"caption": null,
"url": "https://api-cdn.arte.tv/img/v2/image/mG773a698ovUhpEBU2ZBf8/__SIZE__"
},
"stickers": [
{
"code": "PLAYABLE",
"label": "PLAYABLE"
},
{
"code": "FULL_VIDEO",
"label": "Voir le programme"
}
],
"trackingPixel": "/ct/?abv=A&em=047323-004-A&language=fr&pageid=collection&position=8&support=web&teaserid=047323-004-A_fr&teasertitle=%C3%8Eles%20de%20beaut%C3%A9s&zoneCode=collection_videos_RC-023242&zoneIndexInPage=1&zoneTemplate=horizontal_landscape&zoneid=collection_videos&zonename=Toutes%20les%20vid%C3%A9os",
"programId": "047323-004-A",
"teaserText": "Tortues-luth, colibris : les Caraïbes sont peuplées d'une faune étrange, véritable laboratoire de l'évolution.",
"duration": 2572,
"durationLabel": "43 min",
"geoblocking": {
"code": "SAT",
"label": "",
"inclusion": [],
"exclusion": []
},
"genre": null,
"audioVersions": [],
"availability": {
"type": "VOD",
"start": "2023-01-06T04:00:00Z",
"end": "2023-04-07T03:00:00Z",
"upcomingDate": "2023-01-06T04:00:00Z",
"label": "Disponible du 06/01/2023 au 06/04/2023"
},
"ageRating": 0,
"callToAction": "Regarder",
"clip": null,
"trailer": null,
"childrenCount": null
}
],
"pagination": {
"page": 1,
"pages": 1,
"totalCount": 8,
"links": {
"first": "https://api-internal.arte.tv/api/emac/v4/fr/web/zones/d3bad9c8-b1c0-43db-87fb-fb95cc68766d/content?abv=A&collectionId=RC-023242&page=1&pageId=collection&type=collection&zoneIndexInPage=1",
"next": null,
"last": "https://api-internal.arte.tv/api/emac/v4/fr/web/zones/d3bad9c8-b1c0-43db-87fb-fb95cc68766d/content?abv=A&collectionId=RC-023242&page=1&pageId=collection&type=collection&zoneIndexInPage=1"
}
}
}
},
{
"id": "5c5ab0fc-a32c-493c-bfa1-7d4eddca8834_RC-023242",
"code": "collection_upcoming_RC-023242",
"title": "Prochainement",
"slug": null,
"description": null,
"displayOptions": {
"template": "horizontal-landscape",
"theme": null,
"showZoneTitle": true,
"showItemTitle": true
},
"link": null,
"authenticatedContent": null,
"content": {
"data": [],
"pagination": null
}
},
{
"id": "3e4b7126-60ee-4740-9aa6-e0a92af2bfe1_RC-023242",
"code": "collection_article_RC-023242",
"title": "Collection Articles",
"slug": null,
"description": null,
"displayOptions": {
"template": "horizontal-landscape",
"theme": null,
"showZoneTitle": true,
"showItemTitle": true
},
"link": null,
"authenticatedContent": null,
"content": {
"data": [],
"pagination": null
}
},
{
"id": "125ee988-ab24-4bdf-a62c-703717d02164_RC-023242",
"code": "collection_associated_RC-023242",
"title": "Sur le même thème",
"slug": null,
"description": null,
"displayOptions": {
"template": "horizontal-landscape",
"theme": null,
"showZoneTitle": true,
"showItemTitle": true
},
"link": null,
"authenticatedContent": null,
"content": {
"data": [
{
"id": "https://www.arteradio.com/serie/la_derniere_nuit_d_anne_bonny/2393",
"type": "teaser",
"kind": {
"code": "EXTERNAL",
"label": "Lien web",
"isCollection": false
},
"url": "https://www.arteradio.com/serie/la_derniere_nuit_d_anne_bonny/2393",
"deeplink": null,
"title": "La dernière nuit d'Anne Bonny - Un podcast ARTE Radio",
"subtitle": null,
"shortDescription": null,
"mainImage": {
"caption": null,
"url": "https://api-cdn.arte.tv/img/v2/image/Jhjhuk8piMeNtdBsdM8Tjf/__SIZE__?photoId=32ce3f9271c297efb7a5dca15ba210c1603c998f"
},
"stickers": [],
"trackingPixel": "/ct/?abv=A&language=fr&pageid=collection&position=1&support=web&teaserid=https%3A%2F%2Fwww.arteradio.com%2Fserie%2Fla_derniere_nuit_d_anne_bonny%2F2393&teasertitle=La%20derni%C3%A8re%20nuit%20d%27Anne%20Bonny%20-%20Un%20podcast%20ARTE%20Radio&zoneCode=collection_associated_RC-023242&zoneIndexInPage=4&zoneTemplate=horizontal_landscape&zoneid=collection_associated&zonename=Sur%20le%20m%C3%AAme%20th%C3%A8me",
"programId": null,
"teaserText": null,
"duration": null,
"durationLabel": null,
"geoblocking": null,
"genre": null,
"audioVersions": [],
"availability": null,
"ageRating": null,
"callToAction": "Regarder",
"clip": null,
"trailer": null,
"childrenCount": null
},
{
"id": "https://www.arteradio.com/son/61675284/anne_bonny_la_reine_des_pirates_1_6",
"type": "teaser",
"kind": {
"code": "EXTERNAL",
"label": "Lien web",
"isCollection": false
},
"url": "https://www.arteradio.com/son/61675284/anne_bonny_la_reine_des_pirates_1_6",
"deeplink": null,
"title": "Anne Bonny, la reine des pirates - Une fiction jeune public sur ARTE Radio",
"subtitle": null,
"shortDescription": null,
"mainImage": {
"caption": null,
"url": "https://api-cdn.arte.tv/img/v2/image/Jhjhuk8piMeNtdBsdM8Tjf/__SIZE__?photoId=663343f621223cc6d55eed38146aa0162e9875b9"
},
"stickers": [],
"trackingPixel": "/ct/?abv=A&language=fr&pageid=collection&position=2&support=web&teaserid=https%3A%2F%2Fwww.arteradio.com%2Fson%2F61675284%2Fanne_bonny_la_reine_des_pirates_1_6&teasertitle=Anne%20Bonny%2C%20la%20reine%20des%20pirates%20-%20Une%20fiction%20jeune%20public%20sur%20ARTE%20Radio&zoneCode=collection_associated_RC-023242&zoneIndexInPage=4&zoneTemplate=horizontal_landscape&zoneid=collection_associated&zonename=Sur%20le%20m%C3%AAme%20th%C3%A8me",
"programId": null,
"teaserText": null,
"duration": null,
"durationLabel": null,
"geoblocking": null,
"genre": null,
"audioVersions": [],
"availability": null,
"ageRating": null,
"callToAction": "Regarder",
"clip": null,
"trailer": null,
"childrenCount": null
}
],
"pagination": {
"page": 1,
"pages": 1,
"totalCount": 2,
"links": {
"first": "https://api-internal.arte.tv/api/emac/v4/fr/web/zones/125ee988-ab24-4bdf-a62c-703717d02164/content?abv=A&collectionId=RC-023242&page=1&pageId=collection&type=collection&zoneIndexInPage=4",
"next": null,
"last": "https://api-internal.arte.tv/api/emac/v4/fr/web/zones/125ee988-ab24-4bdf-a62c-703717d02164/content?abv=A&collectionId=RC-023242&page=1&pageId=collection&type=collection&zoneIndexInPage=4"
}
}
}
},
{
"id": "371699f8-dcfb-4abb-933a-83cc956a5d7d_RC-023242",
"code": "collection_partner_RC-023242",
"title": "Collection Partners",
"slug": null,
"description": null,
"displayOptions": {
"template": "horizontal-landscape",
"theme": null,
"showZoneTitle": true,
"showItemTitle": true
},
"link": null,
"authenticatedContent": null,
"content": {
"data": [],
"pagination": null
}
}
],
"stats": {
"xiti": {
"page_name": "Home_RC-023242_bandes-de-pirates",
"chapter1": "HIS_histoire",
"chapter2": "Collection",
"chapter3": "RC-023242_bandes-de-pirates",
"x1": "fr",
"x2": "Collection",
"s2": 1,
"siteId": "582046",
"env_work": "prod",
"search_keywords": null
}
},
"metadata": {
"title": "Bandes de pirates !",
"description": "Aux XVII et XVIIIe siècles, les pirates et les corsaires sillonent les océans en quête d'aventures. Ils sont redoutables et intriguants et, du Capitaine Horatio Hornblower aux corsaires napoléonniens en passant par Francis Drake, ils ont alimenté nos imaginaires et contribuent encore à de fameuses légendes.",
"seo": {
"title": "Bandes de pirates ! - Histoire | ARTE",
"description": "Aux XVII et XVIIIe siècles, les pirates et les corsaires sillonent les océans en quête d'aventures. Ils sont redoutables et intriguants et, du Capitaine Horatio Hornblower aux corsaires napoléonniens en passant par Francis Drake, ils ont alimenté nos imaginaires et contribuent encore à de fameuses légendes.",
"canonical": "/fr/videos/RC-023242/bandes-de-pirates/"
},
"og": {
"image": {
"url": "https://api-cdn.arte.tv/img/v2/image/Jhjhuk8piMeNtdBsdM8Tjf/1920x1080?type=TEXT&watermark=true",
"width": 1920,
"height": 1080
}
},
"twitter": {
"image": {
"url": "https://api-cdn.arte.tv/img/v2/image/Jhjhuk8piMeNtdBsdM8Tjf/1920x1080?type=TEXT&watermark=true"
},
"site": "@ARTEfr"
}
},
"base": {
"type": "collections",
"redirect": null
}
}
},
"initialType": "collections",
"mamiBaseUrl": "https://api-cdn.arte.tv/api/mami/v1/",
"locale": "fr",
"tcStartFrom": null,
"abvGroups": "A",
"emacVersion": "v4"
},
"locale": "fr",
"footerProps": {
"main": [
{
"label": "Sites",
"href": null,
"links": [
{
"label": "ARTE VOD/DVD",
"kind": "internal",
"href": "https://boutique.arte.tv/",
"rel": null
},
{
"label": "ARTE Radio",
"kind": "internal",
"href": "https://www.arteradio.com/",
"rel": null
},
{
"label": "Coups de cœur culture",
"kind": "internal",
"href": "https://my.arte.tv/index.php?lang=fr&page=eventsFavorite",
"rel": null
},
{
"label": "Programmes en UHD ",
"kind": "internal",
"href": "https://www.arte.tv/fr/videos/RC-022710/nos-programmes-en-uhd/",
"rel": null
},
{
"label": "ARTE Info Plus - Décryptez l'actualité",
"kind": "internal",
"href": "https://www.arte.tv/fr/videos/RC-022628/arte-info-plus/",
"rel": null
}
]
},
{
"label": "Entreprise",
"href": null,
"links": [
{
"label": "Tout sur ARTE",
"kind": "internal",
"href": "https://www.arte.tv/sites/corporate/",
"rel": "nofollow"
},
{
"label": "Emplois et stages",
"kind": "internal",
"href": "https://www.arte.tv/sites/corporate/particuliers-professionnels/#offres-demploi-et-de-stages",
"rel": "nofollow"
},
{
"label": "Appels d'offres",
"kind": "internal",
"href": "https://www.arte.tv/sites/corporate/appels-doffres/",
"rel": "nofollow"
},
{
"label": "Aide & contact",
"kind": "internal",
"href": "https://www.arte.tv/sites/corporate/nous-repondons-a-vos-questions/",
"rel": "nofollow"
},
{
"label": "Streamer responsable",
"kind": "internal",
"href": "https://www.arte.tv/sites/corporate/streaming-responsable/",
"rel": "nofollow"
}
]
},
{
"label": "Infos légales",
"href": null,
"links": [
{
"label": "Gérer les cookies",
"kind": "cookie",
"href": null,
"rel": null
},
{
"label": "ARTE et vos données personnelles",
"kind": "internal",
"href": "https://www.arte.tv/sites/corporate/donnees-personnelles/",
"rel": "nofollow"
},
{
"label": "Mentions légales et crédits",
"kind": "internal",
"href": "https://www.arte.tv/sites/corporate/credits/",
"rel": "nofollow"
},
{
"label": "CGU",
"kind": "internal",
"href": "https://www.arte.tv/sites/corporate/conditions-generales-dutilisation-cgu/",
"rel": "nofollow"
},
{
"label": "Accessibilité",
"kind": "internal",
"href": "https://www.arte.tv/sites/corporate/accessibilite/",
"rel": "nofollow"
},
{
"label": "Plan du site",
"kind": "internal",
"href": "https://www.arte.tv/fr/sitemap/",
"rel": null
}
]
},
{
"label": "Réseaux sociaux",
"href": null,
"links": [
{
"label": "Facebook",
"kind": "external",
"href": "https://www.facebook.com/artetv",
"rel": "nofollow"
},
{
"label": "Instagram",
"kind": "external",
"href": "https://www.instagram.com/artefr",
"rel": "nofollow"
},
{
"label": "Youtube",
"kind": "external",
"href": "https://www.youtube.com/arteplus7",
"rel": "nofollow"
},
{
"label": "Twitter",
"kind": "external",
"href": "https://www.twitter.com/artefr",
"rel": "nofollow"
}
]
}
]
},
"geoblocking": "DE_FR",
"serverTime": "2023-01-24T08:43:36.718Z",
"__N_SSP": true
},
"page": "/videos/[...identifiers]",
"query": {
"identifiers": [
"RC-023242",
"bandes-de-pirates"
]
},
"buildId": "230117085533",
"assetPrefix": "https://static-cdn.arte.tv/replay",
"runtimeConfig": {
"emacVersion": "v4",
"ebuBoxUrl": "https://reco.ebu.io/news-reco-arte.js",
"ffABTesting": true,
"ffDirectPlayerAutoPlay": "true",
"ffEbuBox": false,
"ffImagePlaceholder": false,
"ffLivePage": true,
"ffMeta": true,
"ffNewGuideTv": true,
"ffNewsletterZoneWithTeaserImage": false,
"ffPlayerAutoPlay": "if_available",
"ffProgramTrailer": false,
"ffProfileMenu": false,
"ffSettingsMenuTargetedComms": false,
"ffSidaction": false,
"ffThemeSwitch": false,
"ffAtInternetSrcForce": true,
"ffSettingsMenuVideoQuality": false,
"ffSliderMetaInfoButtonSeeMore": false,
"newsletterSubscribeUrl": "https://api.arte.tv/api/sso/v3/newsletter/subscribe",
"tagCommanderUrl": "https://cdn.tagcommander.com/3445"
},
"isFallback": false,
"gssp": true,
"customServer": true,
"appGip": true,
"locale": "fr",
"locales": [
"fr",
"de",
"en",
"es",
"pl",
"it"
],
"defaultLocale": "fr",
"scriptLoader": []
}

View File

@ -9,155 +9,165 @@ from .error import *
from .model import *
def fetch_sources(http_session, url):
"""Fetch sources at a given ArteTV page URL."""
from .api import fetch_program_info
from .hls import fetch_program_tracks
from .www import parse_url
def fetch_program_sources(url, http_session):
"""Fetch program sources listed on given ArteTV page."""
from .www import iter_programs
site, program_id, slug = parse_url(url)
variants = dict()
renditions = dict()
p_meta, program_index_urls = fetch_program_info(http_session, site, program_id)
program = Program(program_id, slug, p_meta)
for program_index_url in program_index_urls:
v_tracks, a_track, s_track = fetch_program_tracks(
http_session, program_index_url
return [
ProgramSource(
program,
player_config_url,
)
for v_meta, v_url in v_tracks:
if v_meta not in variants:
variants[v_meta] = v_url
elif variants[v_meta] != v_url:
raise ValueError
for program, player_config_url in iter_programs(url, http_session)
]
a_meta, a_url = a_track
s_meta, s_url = s_track or (None, None)
if (a_meta, s_meta) not in renditions:
renditions[(a_meta, s_meta)] = (a_url, s_url)
elif renditions[(a_meta, s_meta)] != (a_url, s_url):
raise ValueError
def fetch_rendition_sources(program_sources, http_session):
"""Fetch renditions for given programs."""
from itertools import groupby
return Sources(
program,
[Variant(key, source) for key, source in variants.items()],
[Rendition(key, source) for key, source in renditions.items()],
from .api import iter_renditions
sources = [
RenditionSource(
program,
rendition,
protocol,
program_index_url,
)
for program, player_config_url in program_sources
for rendition, protocol, program_index_url in iter_renditions(
program.id,
player_config_url,
http_session,
)
]
descriptors = list({(s.rendition.code, s.rendition.label) for s in sources})
descriptors.sort()
for code, group in groupby(descriptors, key=lambda t: t[0]):
labels_for_code = [t[1] for t in group]
if len(labels_for_code) != 1:
raise UnexpectedError("MULTIPLE_RENDITION_LABELS", code, labels_for_code)
return sources
def fetch_variant_sources(renditions_sources, http_session):
"""Fetch variants for given renditions."""
from itertools import groupby
from .hls import iter_variants
sources = [
VariantSource(
program,
rendition,
variant,
VariantSource.VideoMedia(*video),
VariantSource.AudioMedia(*audio),
VariantSource.SubtitlesMedia(*subtitles) if subtitles else None,
)
for program, rendition, protocol, program_index_url in renditions_sources
for variant, video, audio, subtitles in iter_variants(
protocol, program_index_url, http_session
)
]
descriptors = list(
{(s.variant.code, s.video_media.track.frame_rate) for s in sources}
)
descriptors.sort()
for code, group in groupby(descriptors, key=lambda t: t[0]):
frame_rates_for_code = [t[1] for t in group]
if len(frame_rates_for_code) != 1:
raise UnexpectedError(
"MULTIPLE_RENDITION_FRAME_RATES", code, frame_rates_for_code
)
def iter_renditions(sources):
"""Iterate over renditions (code, key) of the given sources."""
keys = [r.key for r in sources.renditions]
keys.sort(
key=lambda k: (
not k[0].is_original,
k[0].language,
k[0].is_descriptive,
k[1].language if k[1] else "",
k[1].is_descriptive if k[1] else False,
)
)
for (a_meta, s_meta) in keys:
code = a_meta.language
if a_meta.is_descriptive:
code += "[AD]"
if s_meta:
if s_meta.is_descriptive:
code += f"-{s_meta.language}[CC]"
elif s_meta.language != a_meta.language:
code += f"-{s_meta.language}"
yield code, (a_meta, s_meta)
return sources
def select_rendition(sources, key):
"""Reject all other renditions from the given sources."""
renditions = [r for r in sources.renditions if r.key == key]
match len(renditions):
case 0:
raise ValueError("rendition not found")
case 1:
pass
case _:
raise ValueError("non unique rendition")
sources.renditions[:] = renditions
def iter_variants(sources):
"""Iterate over variants (code, key) of the given sources."""
import itertools
keys = [v.key for v in sources.variants]
keys.sort(key=lambda k: (k.height, k.frame_rate), reverse=True)
for height, group in itertools.groupby(keys, lambda m: m.height):
group = list(group)
if len(group) == 1:
yield f"{height}p", group[0]
else:
for m in group:
yield f"{height}p@{m.frame_rate}", m
def select_variant(sources, key):
"""Reject all other variants from the given sources."""
variants = [v for v in sources.variants if v.key == key]
match len(variants):
case 0:
raise ValueError("variant not found")
case 1:
pass
case _:
raise ValueError("non unique variant")
sources.variants[:] = variants
def compile_sources(sources, **naming_options):
"""Return target from the given sources."""
def fetch_targets(variant_sources, http_session, **naming_options):
"""Compile download targets for given variants."""
from .hls import fetch_mp4_media, fetch_vtt_media
from .naming import file_name_builder
match len(sources.variants):
case 0:
raise ValueError("no variants")
case 1:
v_meta, v_url = sources.variants[0]
case _:
raise ValueError("multiple variants")
build_file_name = file_name_builder(**naming_options)
match len(sources.renditions):
case 0:
raise ValueError("no renditions")
case 1:
(a_meta, s_meta), (a_url, s_url) = sources.renditions[0]
case _:
raise ValueError("multiple renditions")
targets = [
Target(
Target.VideoInput(
video_media.track,
fetch_mp4_media(video_media.track_index_url, http_session),
),
Target.AudioInput(
audio_media.track,
fetch_mp4_media(audio_media.track_index_url, http_session),
),
(
Target.SubtitlesInput(
subtitles_media.track,
fetch_vtt_media(subtitles_media.track_index_url, http_session),
)
if subtitles_media
else None
),
(program.title, program.subtitle) if program.subtitle else program.title,
build_file_name(program, rendition, variant),
)
for program, rendition, variant, video_media, audio_media, subtitles_media in variant_sources
]
build_file_name = file_name_builder(v_meta, a_meta, s_meta, **naming_options)
return Target(
sources.program.meta,
VideoTrack(v_meta, v_url),
AudioTrack(a_meta, a_url),
SubtitlesTrack(s_meta, s_url) if s_meta else None,
build_file_name(sources.program),
)
return targets
def download_target(http_session, target, progress):
"""Download the given target."""
from .hls import download_target_tracks
def download_targets(targets, http_session, on_progress):
"""Download given target."""
import os
from .download import download_mp4_media, download_vtt_media
from .muxing import mux_target
with download_target_tracks(http_session, target, progress) as local_target:
mux_target(local_target, progress)
for target in targets:
video_path = target.output + ".video.mp4"
audio_path = target.output + ".audio.mp4"
subtitles_path = target.output + ".srt"
download_mp4_media(
target.video_input.url, video_path, http_session, on_progress
)
download_mp4_media(
target.audio_input.url, audio_path, http_session, on_progress
)
if target.subtitles_input:
download_vtt_media(
target.subtitles_input.url, subtitles_path, http_session, on_progress
)
mux_target(
target._replace(
video_input=target.video_input._replace(url=video_path),
audio_input=target.audio_input._replace(url=audio_path),
subtitles_input=(
target.subtitles_input._replace(url=subtitles_path)
if target.subtitles_input
else None
),
),
on_progress,
)
if os.path.isfile(subtitles_path):
os.unlink(subtitles_path)
if os.path.isfile(audio_path):
os.unlink(audio_path)
if os.path.isfile(video_path):
os.unlink(video_path)

View File

@ -23,13 +23,14 @@ Options:
--version print current version of the program
--debug on error, print debugging information
--name-use-id use the program ID
--name-use-slug use the URL slug
--name-sep=<sep> field separator [default: - ]
--name-seq-pfx=<pfx> sequence counter prefix [default: - ]
--name-seq-no-pad disable sequence zero-padding
--name-add-resolution add resolution tag
--name-add-rendition add rendition code
--name-add-variant add variant code
"""
import itertools
import sys
import time
@ -37,16 +38,15 @@ import docopt
import requests
from . import (
ModuleError,
UnexpectedError,
__version__,
compile_sources,
download_target,
fetch_sources,
iter_renditions,
iter_variants,
select_rendition,
select_variant,
download_targets,
fetch_program_sources,
fetch_rendition_sources,
fetch_targets,
fetch_variant_sources,
)
from .error import ModuleError, UnexpectedError
class Abort(ModuleError):
@ -57,131 +57,104 @@ class Fail(UnexpectedError):
"""Unexpected error."""
_LANGUAGES = {
"de": "German",
"en": "English",
"es": "Spanish",
"fr": "French",
"it": "Italian",
"mul": "multiple language",
"no": "Norwegian",
"pt": "Portuguese",
}
def _create_progress():
# create a progress handler for input downloads
state = {}
def _language_name_for_code(code):
return _LANGUAGES.get(code, f"[{code}]")
def _language_name(meta):
return _language_name_for_code(meta.language)
def _print_renditions(renditions):
has_original = False
for code, (a_meta, s_meta) in renditions:
label = _language_name(a_meta)
if a_meta.is_original:
has_original = True
label = "original " + label
elif a_meta.is_descriptive:
label += " audio description"
elif has_original:
label += " dubbed"
if s_meta:
if s_meta.is_descriptive:
label += f" ({_language_name(s_meta)} closed captions)"
elif s_meta.language != a_meta.language:
label += f" ({_language_name(s_meta)} subtitles)"
print(f"\t{code:>6} - {label}")
def _validate_rendition(renditions, code):
for code_, rendition in renditions:
if code_ == code:
break
else:
print(f"{code!r} is not a valid rendition code, possible values are:")
_print_renditions(renditions)
raise Abort()
return rendition
def _print_variants(variants):
for code, _ in variants:
print(f"\t{code}")
def _validate_variant(variants, code):
for code_, variant in variants:
if code_ == code:
break
else:
print(f"{code!r} is not a valid variant code, possible values are:")
_print_variants(variants)
raise Abort()
return variant
def create_progress():
"""Create a progress handler for input downloads."""
state = {
"last_update_time": 0,
"last_channel": None,
}
def progress(channel, current, total):
def on_progress(file, current, total):
now = time.time()
if current == total:
print(f"\rDownloading {channel}: 100.0%")
state["last_update_time"] = now
elif channel != state["last_channel"]:
print(f"Downloading {channel}: 0.0%", end="")
state["last_update_time"] = now
state["last_channel"] = channel
elif now - state["last_update_time"] > 1:
if current == 0:
print(f"Downloading {file!r}: 0.0%", end="")
state["start_time"] = now
state["last_time"] = now
state["last_count"] = 0
elif current == total:
elapsed_time = now - state["start_time"]
rate = int(total / elapsed_time) if elapsed_time else "NaN"
print(f"\rDownloading {file!r}: 100.0% [{rate}]")
state.clear()
elif now - state["last_time"] > 1:
elapsed_time1 = now - state["start_time"]
elapsed_time2 = now - state["last_time"]
progress = int(1000.0 * current / total) / 10.0
rate1 = int(current / elapsed_time1) if elapsed_time1 else "NaN"
rate2 = (
int((current - state["last_count"]) / elapsed_time2)
if elapsed_time2
else "NaN"
)
print(
f"\rDownloading {channel}: {int(1000.0 * current / total) / 10.0}%",
f"\rDownloading {file!r}: {progress}% [{rate1}, {rate2}]",
end="",
)
state["last_update_time"] = now
state["last_time"] = now
state["last_count"] = current
return progress
return on_progress
def _select_rendition_sources(rendition_code, rendition_sources):
if rendition_code:
filtered = [s for s in rendition_sources if s.rendition.code == rendition_code]
if filtered:
return filtered
print(
f"{rendition_code!r} is not a valid rendition code. Available values are:"
)
else:
print("Available renditions:")
key = lambda s: (s.rendition.label, s.rendition.code)
rendition_sources.sort(key=key)
for (label, code), _ in itertools.groupby(rendition_sources, key=key):
print(f"{code:>12} : {label}")
raise Abort()
def _select_variant_sources(variant_code, variant_sources):
if variant_code:
filtered = [s for s in variant_sources if s.variant.code == variant_code]
if filtered:
return filtered
print(f"{variant_code!r} is not a valid variant code. Available values are:")
else:
print("Available variants:")
variant_sources.sort(key=lambda s: s.video_media.track.height, reverse=True)
for code, _ in itertools.groupby(variant_sources, key=lambda s: s.variant.code):
print(f"{code:>12}")
raise Abort()
def main():
"""CLI command."""
args = docopt.docopt(__doc__, sys.argv[1:], version=__version__)
http_session = requests.sessions.Session()
try:
http_session = requests.sessions.Session()
program_sources = fetch_program_sources(args["URL"], http_session)
sources = fetch_sources(http_session, args["URL"])
rendition_sources = _select_rendition_sources(
args["RENDITION"],
fetch_rendition_sources(program_sources, http_session),
)
renditions = list(iter_renditions(sources))
if not args["RENDITION"]:
print(f"Available renditions:")
_print_renditions(renditions)
return 0
variant_sources = _select_variant_sources(
args["VARIANT"],
fetch_variant_sources(rendition_sources, http_session),
)
select_rendition(sources, _validate_rendition(renditions, args["RENDITION"]))
variants = list(iter_variants(sources))
if not args["VARIANT"]:
print(f"Available variants:")
_print_variants(variants)
return 0
select_variant(sources, _validate_variant(variants, args["VARIANT"]))
target = compile_sources(
sources,
targets = fetch_targets(
variant_sources,
http_session,
**{
k[7:].replace("-", "_"): v
for k, v in args.items()
@ -189,9 +162,7 @@ def main():
},
)
progress = create_progress()
download_target(http_session, target, progress)
download_targets(targets, http_session, _create_progress())
except UnexpectedError as e:
print(str(e))

View File

@ -3,62 +3,67 @@
"""Provide ArteTV JSON API utilities."""
from .error import UnexpectedAPIResponse, UnsupportedHLSProtocol
from .model import ProgramMeta
from .error import UnexpectedAPIResponse
from .model import Rendition
MIME_TYPE = "application/vnd.api+json; charset=utf-8"
def _fetch_api_data(http_session, path, object_type):
def _fetch_api_object(http_session, url, object_type):
# Fetch an API object.
url = "https://api.arte.tv/api/player/v2/" + path
r = http_session.get(url)
r.raise_for_status()
if (_ := r.headers["content-type"]) != MIME_TYPE:
raise UnexpectedAPIResponse("MIME_TYPE", path, MIME_TYPE, _)
mime_type = r.headers["content-type"]
if mime_type != MIME_TYPE:
raise UnexpectedAPIResponse("MIME_TYPE", url, MIME_TYPE, mime_type)
obj = r.json()["data"]
obj = r.json()
if (_ := obj["type"]) != object_type:
raise UnexpectedAPIResponse("OBJECT_TYPE", path, object_type, _)
try:
data_type = obj["data"]["type"]
if data_type != object_type:
raise UnexpectedAPIResponse("OBJECT_TYPE", url, object_type, data_type)
return obj["attributes"]
return obj["data"]["attributes"]
except (KeyError, IndexError, ValueError) as e:
raise UnexpectedAPIResponse("SCHEMA", url) from e
def fetch_program_info(http_session, site, program_id):
"""Fetch the given program metadata and indexes."""
obj = _fetch_api_data(http_session, f"config/{site}/{program_id}", "ConfigPlayer")
def iter_renditions(program_id, player_config_url, http_session):
"""Iterate over renditions for the given program."""
obj = _fetch_api_object(http_session, player_config_url, "ConfigPlayer")
if (_ := obj["metadata"]["providerId"]) != program_id:
raise UnexpectedAPIResponse(
"PROGRAM_ID_MISMATCH",
site,
program_id,
_,
)
program_meta = ProgramMeta(
obj["metadata"]["title"],
obj["metadata"]["subtitle"],
obj["metadata"]["description"],
)
program_index_urls = set()
for s in obj["streams"]:
if (_ := s["protocol"]) != "HLS_NG":
raise UnsupportedHLSProtocol(site, program_id, _)
if (program_index_url := s["url"]) in program_index_urls:
codes = set()
try:
provider_id = obj["metadata"]["providerId"]
if provider_id != program_id:
raise UnexpectedAPIResponse(
"DUPLICATE_PROGRAM_INDEX_URL",
site,
program_id,
program_index_url,
"PROVIDER_ID_MISMATCH", player_config_url, provider_id
)
program_index_urls.add(program_index_url)
for s in obj["streams"]:
code = s["versions"][0]["eStat"]["ml5"]
return program_meta, program_index_urls
if code in codes:
raise UnexpectedAPIResponse(
"DUPLICATE_RENDITION_CODE", player_config_url, code
)
codes.add(code)
yield (
Rendition(
s["versions"][0]["eStat"]["ml5"],
s["versions"][0]["label"],
),
s["protocol"],
s["url"],
)
except (KeyError, IndexError, ValueError) as e:
raise UnexpectedAPIResponse("SCHEMA", player_config_url) from e
if not codes:
raise UnexpectedAPIResponse("NO_RENDITIONS", player_config_url)

52
src/delarte/download.py Normal file
View File

@ -0,0 +1,52 @@
# License: GNU AGPL v3: http://www.gnu.org/licenses/
# This file is part of `delarte` (https://git.afpy.org/fcode/delarte.git)
"""Provide download utilities."""
import os
from . import subtitles
_CHUNK = 64 * 1024
def download_mp4_media(url, file_name, http_session, on_progress):
"""Download a MP4 (video or audio) to given file."""
on_progress(file_name, 0, 0)
if os.path.isfile(file_name):
on_progress(file_name, 1, 1)
return
temp_file = f"{file_name}.tmp"
with open(temp_file, "w+b") as f:
r = http_session.get(url, timeout=5, stream=True)
r.raise_for_status()
total = int(r.headers["content-length"])
for content in r.iter_content(_CHUNK):
f.write(content)
on_progress(file_name, f.tell(), total)
os.rename(temp_file, file_name)
def download_vtt_media(url, file_name, http_session, on_progress):
"""Download a VTT and SRT-convert it to to given file."""
on_progress(file_name, 0, 0)
if os.path.isfile(file_name):
on_progress(file_name, 1, 1)
return
temp_file = f"{file_name}.tmp"
with open(temp_file, "w", encoding="utf-8") as f:
r = http_session.get(url, timeout=5)
r.raise_for_status()
r.encoding = "utf-8"
subtitles.convert(r.text, f)
on_progress(file_name, f.tell(), f.tell())
os.rename(temp_file, file_name)

View File

@ -16,18 +16,39 @@ class ModuleError(Exception):
return f"{self.__class__}{self.args!r}"
class ExpectedError(ModuleError):
"""A feature limitation to submit as an enhancement to developers."""
class UnexpectedError(ModuleError):
"""An error to report to developers."""
class InvalidUrl(ModuleError):
"""Invalid ArteTV URL."""
#
# www
#
class PageNotFound(ModuleError):
"""Page not found at ArteTV."""
class PageNotSupported(ExpectedError):
"""The page you are trying to download from is not (yet) supported."""
class InvalidPage(UnexpectedError):
"""Invalid ArteTV page."""
#
# api
#
class UnexpectedAPIResponse(UnexpectedError):
"""Unexpected response from ArteTV."""
#
# hls
#
class UnexpectedHLSResponse(UnexpectedError):
"""Unexpected response from ArteTV."""
@ -36,5 +57,8 @@ class UnsupportedHLSProtocol(ModuleError):
"""Program type not supported."""
#
# subtitles
#
class WebVTTError(UnexpectedError):
"""Unexpected WebVTT data."""

View File

@ -4,23 +4,10 @@
"""Provide HLS protocol utilities."""
import contextlib
import os
from tempfile import NamedTemporaryFile
import m3u8
from . import subtitles
from .error import UnexpectedHLSResponse
from .model import (
AudioMeta,
AudioTrack,
SubtitlesMeta,
SubtitlesTrack,
VideoMeta,
VideoTrack,
Target,
)
from .error import UnexpectedHLSResponse, UnsupportedHLSProtocol
from .model import AudioTrack, SubtitlesTrack, Variant, VideoTrack
#
# WARNING !
@ -40,7 +27,7 @@ from .model import (
MIME_TYPE = "application/x-mpegURL"
def _fetch_index(http_session, url):
def _fetch_index(url, http_session):
# Fetch a M3U8 playlist
r = http_session.get(url)
r.raise_for_status()
@ -53,9 +40,12 @@ def _fetch_index(http_session, url):
return m3u8.loads(r.text, url)
def fetch_program_tracks(http_session, program_index_url):
"""Fetch video, audio and subtitles tracks for the given program index."""
program_index = _fetch_index(http_session, program_index_url)
def iter_variants(protocol, program_index_url, http_session):
"""Iterate over variants for the given rendition."""
if protocol != "HLS_NG":
raise UnsupportedHLSProtocol(protocol, program_index_url)
program_index = _fetch_index(program_index_url, http_session)
audio_media = None
subtitles_media = None
@ -78,8 +68,9 @@ def fetch_program_tracks(http_session, program_index_url):
if not audio_media:
raise UnexpectedHLSResponse("NO_AUDIO_MEDIA", program_index_url)
audio_track = AudioTrack(
AudioMeta(
audio = (
AudioTrack(
audio_media.name,
audio_media.language,
audio_media.name.startswith("VO"),
(
@ -90,9 +81,10 @@ def fetch_program_tracks(http_session, program_index_url):
audio_media.absolute_uri,
)
subtitles_track = (
SubtitlesTrack(
SubtitlesMeta(
subtitles = (
(
SubtitlesTrack(
subtitles_media.name,
subtitles_media.language,
(
subtitles_media.characteristics is not None
@ -105,7 +97,7 @@ def fetch_program_tracks(http_session, program_index_url):
else None
)
video_tracks = set()
codes = set()
for video_media in program_index.playlists:
stream_info = video_media.stream_info
@ -117,33 +109,39 @@ def fetch_program_tracks(http_session, program_index_url):
if subtitles_media:
if stream_info.subtitles != subtitles_media.group_id:
raise UnexpectedHLSResponse(
"INVALID_SUBTITLES_MEDIA",
program_index_url,
stream_info.subtitles,
"INVALID_SUBTITLES_MEDIA", program_index_url, stream_info.subtitles
)
elif stream_info.subtitles:
raise UnexpectedHLSResponse(
"INVALID_SUBTITLES_MEDIA",
program_index_url,
stream_info.subtitles,
"INVALID_SUBTITLES_MEDIA", program_index_url, stream_info.subtitles
)
video_track = VideoTrack(
VideoMeta(
stream_info.resolution[0],
stream_info.resolution[1],
stream_info.frame_rate,
code = f"{stream_info.resolution[1]}p"
if code in codes:
raise UnexpectedHLSResponse(
"DUPLICATE_STREAM_CODE", program_index_url, code
)
codes.add(code)
yield (
Variant(
code,
stream_info.average_bandwidth,
),
video_media.absolute_uri,
(
VideoTrack(
stream_info.resolution[0],
stream_info.resolution[1],
stream_info.frame_rate,
),
video_media.absolute_uri,
),
audio,
subtitles,
)
if video_track in video_tracks:
raise UnexpectedHLSResponse(
"DUPLICATE_VIDEO_TRACK", program_index_url, video_track
)
video_tracks.add(video_track)
return video_tracks, audio_track, subtitles_track
if not codes:
raise UnexpectedHLSResponse("NO_VARIANTS", program_index_url)
def _convert_byterange(obj):
@ -154,18 +152,16 @@ def _convert_byterange(obj):
return offset, offset + count - 1
def _fetch_av_index(http_session, track_index_url):
# Fetch an audio or video track index.
# Return a tuple:
# - the media file url
# - the media file's ranges
track_index = _fetch_index(http_session, track_index_url)
def fetch_mp4_media(track_index_url, http_session):
"""Fetch an audio or video media."""
track_index = _fetch_index(track_index_url, http_session)
file_name = track_index.segment_map[0].uri
start, end = _convert_byterange(track_index.segment_map[0])
if start != 0:
raise UnexpectedHLSResponse("INVALID_AV_INDEX_FRAGMENT_START", track_index_url)
ranges = [(start, end)]
# ranges = [(start, end)]
next_start = end + 1
for segment in track_index.segments:
@ -178,16 +174,15 @@ def _fetch_av_index(http_session, track_index_url):
"DISCONTINUOUS_AV_INDEX_FRAGMENT", track_index_url
)
ranges.append((start, end))
# ranges.append((start, end))
next_start = end + 1
return track_index.segment_map[0].absolute_uri, ranges
return track_index.segment_map[0].absolute_uri
def _fetch_s_index(http_session, track_index_url):
# Fetch subtitles index.
# Return the subtitle file url.
track_index = _fetch_index(http_session, track_index_url)
def fetch_vtt_media(track_index_url, http_session):
"""Fetch an audio or video media."""
track_index = _fetch_index(track_index_url, http_session)
urls = [s.absolute_uri for s in track_index.segments]
if not urls:
@ -197,112 +192,3 @@ def _fetch_s_index(http_session, track_index_url):
raise UnexpectedHLSResponse("MULTIPLE_S_INDEX_FILES", track_index_url)
return urls[0]
def _download_av_track(http_session, track_index_url, progress):
# Download an audio or video data to temporary file.
# Return the temporary file path.
url, ranges = _fetch_av_index(http_session, track_index_url)
total = ranges[-1][1]
with (
NamedTemporaryFile(
mode="w+b", delete=False, prefix="delarte.", suffix=".mp4"
) as f
):
for range_start, range_end in ranges:
r = http_session.get(
url,
headers={
"Range": f"bytes={range_start}-{range_end}",
},
timeout=5,
)
r.raise_for_status()
if r.status_code != 206:
raise UnexpectedHLSResponse(
"UNEXPECTED_AV_TRACK_HTTP_STATUS",
track_index_url,
r.request.headers,
r.status,
)
if len(r.content) != range_end - range_start + 1:
raise UnexpectedHLSResponse(
"INVALID_AV_TRACK_FRAGMENT_LENGTH", track_index_url
)
f.write(r.content)
progress(range_end, total)
return f.name
def _download_s_track(http_session, track_index_url, progress):
# Download a subtitle file (converted from VTT to SRT format) into a temporary file.
# Return the temporary file path.
url = _fetch_s_index(http_session, track_index_url)
progress(0, 2)
r = http_session.get(url)
r.raise_for_status()
r.encoding = "utf-8"
progress(1, 2)
with NamedTemporaryFile(
"w", delete=False, prefix="delarte.", suffix=".srt", encoding="utf8"
) as f:
subtitles.convert(r.text, f)
progress(2, 2)
return f.name
@contextlib.contextmanager
def download_target_tracks(http_session, target, progress):
"""Download target tracks to temporary files.
Returns a context manager that will delete the temporary files on exit.
The context expression is a local version of the given target.
"""
v_path, (v_meta, v_url) = None, target.video_track
a_path, (a_meta, a_url) = None, target.audio_track
s_path, (s_meta, s_url) = None, target.subtitles_track or (None, None)
try:
s_path = (
_download_s_track(
http_session,
s_url,
lambda i, n: progress("subtitles", i, n),
)
if s_meta
else None
)
a_path = _download_av_track(
http_session, a_url, lambda i, n: progress("audio", i, n)
)
v_path = _download_av_track(
http_session, v_url, lambda i, n: progress("video", i, n)
)
yield Target(
target.program,
VideoTrack(v_meta, v_path),
AudioTrack(a_meta, a_path),
SubtitlesTrack(s_meta, s_path) if s_meta else None,
target.file_name,
)
finally:
if v_path and os.path.isfile(v_path):
os.unlink(v_path)
if a_path and os.path.isfile(a_path):
os.unlink(a_path)
if s_path and os.path.isfile(s_path):
os.unlink(s_path)

View File

@ -7,111 +7,131 @@
from typing import NamedTuple, Optional
class ProgramMeta(NamedTuple):
#
# Metadata objects
#
class Program(NamedTuple):
"""A program metadata."""
id: str
language: str
title: str
"""The title."""
subtitle: str
"""The subtitle or secondary title."""
description: str
"""The description."""
class VideoMeta(NamedTuple):
"""A video track metadata."""
class Rendition(NamedTuple):
"""A program rendition metadata."""
width: int
"""Horizontal part of the resolution."""
height: int
"""Vertical part of the resolution."""
frame_rate: float
"""Frame rate per seconds."""
code: str
label: str
class SubtitlesMeta(NamedTuple):
"""A subtitles track metadata."""
class Variant(NamedTuple):
"""A program variant metadata."""
language: str
"""ISO 639-1 two-letter language codes."""
is_descriptive: bool
"""Whether provides a textual description (closed captions)."""
class AudioMeta(NamedTuple):
"""A audio track metadata."""
language: str
"""ISO 639-1 two-letter language codes, or "mul" for multiple languages."""
is_original: bool
"""Whether audio track is original (no audio description or dubbing)."""
is_descriptive: bool
"""Whether provides an audio description."""
code: str
average_bandwidth: int
#
# Track objects
#
class VideoTrack(NamedTuple):
"""A video track."""
meta: VideoMeta
url: str
width: int
height: int
frame_rate: float
class AudioTrack(NamedTuple):
"""An audio track."""
name: str
language: str
original: bool
visual_impaired: bool
class SubtitlesTrack(NamedTuple):
"""A subtitles track."""
meta: SubtitlesMeta
url: str
name: str
language: str
hearing_impaired: bool
class AudioTrack(NamedTuple):
"""A audio track."""
meta: AudioMeta
url: str
class Variant(NamedTuple):
"""A program variant."""
key: VideoMeta
source: str
class Rendition(NamedTuple):
"""A program rendition."""
key: tuple[AudioMeta, Optional[SubtitlesMeta]]
source: tuple[str, Optional[str]]
class Program(NamedTuple):
"""A program representation."""
id: str
slug: str
meta: ProgramMeta
class Sources(NamedTuple):
"""A program's sources."""
#
# Source objects
#
class ProgramSource(NamedTuple):
"""A program source item."""
program: Program
variants: list[Variant]
renditions: list[Rendition]
player_config_url: str
class RenditionSource(NamedTuple):
"""A rendition source item."""
program: Program
rendition: Rendition
protocol: str
program_index_url: Program
class VariantSource(NamedTuple):
"""A variant source item."""
class VideoMedia(NamedTuple):
"""A video media."""
track: VideoTrack
track_index_url: str
class AudioMedia(NamedTuple):
"""An audio media."""
track: AudioTrack
track_index_url: str
class SubtitlesMedia(NamedTuple):
"""A subtitles media."""
track: SubtitlesTrack
track_index_url: str
program: Program
rendition: Rendition
variant: Variant
video_media: VideoMedia
audio_media: AudioMedia
subtitles_media: Optional[SubtitlesMedia]
class Target(NamedTuple):
"""A download target."""
"""A download target item."""
program: ProgramMeta
video_track: VideoTrack
audio_track: AudioTrack
subtitles_track: Optional[SubtitlesTrack]
file_name: str
class VideoInput(NamedTuple):
"""A video input."""
track: VideoTrack
url: str
class AudioInput(NamedTuple):
"""An audio input."""
track: AudioTrack
url: str
class SubtitlesInput(NamedTuple):
"""A subtitles input."""
track: SubtitlesTrack
url: str
video_input: VideoInput
audio_input: AudioInput
subtitles_input: Optional[SubtitlesInput]
title: str | tuple[str, str]
output: str

View File

@ -1,33 +1,74 @@
# License: GNU AGPL v3: http://www.gnu.org/licenses/
# This file is part of `delarte` (https://git.afpy.org/fcode/delarte.git)
"""Provide tracks muxing utilities."""
"""Provide target muxing utilities."""
import subprocess
def mux_target(target, _progress):
"""Multiplexes tracks into a single file."""
"""Multiplexes target into a single file."""
cmd = ["ffmpeg", "-hide_banner"]
cmd.extend(["-i", target.video_track.url])
cmd.extend(["-i", target.audio_track.url])
if target.subtitles_track:
cmd.extend(["-i", target.subtitles_track.url])
# inputs
cmd.extend(["-i", target.video_input.url])
cmd.extend(["-i", target.audio_input.url])
if target.subtitles_input:
cmd.extend(["-i", target.subtitles_input.url])
# codecs
cmd.extend(["-c:v", "copy"])
cmd.extend(["-c:a", "copy"])
if target.subtitles_track:
if target.subtitles_input:
cmd.extend(["-c:s", "copy"])
cmd.extend(["-bsf:a", "aac_adtstoasc"])
cmd.extend(["-metadata:s:a:0", f"language={target.audio_track.meta.language}"])
if target.subtitles_track:
# stream metadata & disposition
# cmd.extend(["-metadata:s:v:0", f"name={target.video.name!r}"])
# cmd.extend(["-metadata:s:v:0", f"language={target.video.language!r}"])
cmd.extend(["-metadata:s:a:0", f"name={target.audio_input.track.name}"])
cmd.extend(["-metadata:s:a:0", f"language={target.audio_input.track.language}"])
a_disposition = "default"
if target.audio_input.track.original:
a_disposition += "+original"
else:
a_disposition += "-original"
if target.audio_input.track.visual_impaired:
a_disposition += "+visual_impaired"
else:
a_disposition += "-visual_impaired"
cmd.extend(["-disposition:a:0", a_disposition])
if target.subtitles_input:
cmd.extend(["-metadata:s:s:0", f"name={target.subtitles_input.track.name}"])
cmd.extend(
["-metadata:s:s:0", f"language={target.subtitles_track.meta.language}"]
["-metadata:s:s:0", f"language={target.subtitles_input.track.language}"]
)
cmd.extend(["-disposition:s:0", "default"])
cmd.append(f"{target.file_name}.mkv")
s_disposition = "default"
if target.subtitles_input.track.hearing_impaired:
s_disposition += "+hearing_impaired+descriptions"
else:
s_disposition += "-hearing_impaired-descriptions"
cmd.extend(["-disposition:s:0", s_disposition])
# file metadata
if isinstance(target.title, tuple):
cmd.extend(["-metadata", f"title={target.title[0]}"])
cmd.extend(["-metadata", f"subtitle={target.title[1]}"])
else:
cmd.extend(["-metadata", f"title={target.title}"])
# output
cmd.append(f"{target.output}.mkv")
print(cmd)
subprocess.run(cmd)

View File

@ -4,23 +4,17 @@
"""Provide contextualized based file naming utility."""
import re
from typing import Optional
from .model import Program, VideoMeta, AudioMeta, SubtitlesMeta
def file_name_builder(
v_meta: VideoMeta,
a_meta: AudioMeta,
s_meta: Optional[SubtitlesMeta],
*,
use_id=False,
use_slug=False,
sep=" - ",
seq_pfx=" - ",
seq_no_pad=False,
add_resolution=False,
add_rendition=False,
add_variant=False
):
"""Create a file namer from context."""
"""Create a file namer."""
def sub_sequence_counter(match):
index = match[1]
@ -32,20 +26,20 @@ def file_name_builder(
def replace_sequence_counter(s: str) -> str:
return re.sub(r"\s+\((\d+)/(\d+)\)", sub_sequence_counter, s)
def build_file_name(program: Program) -> str:
"""Create a file name for given program."""
def build_file_name(program, rendition, variant):
"""Create a file name."""
if use_id:
return program.id
if use_slug:
return program.slug
fields = [replace_sequence_counter(program.title)]
if program.subtitle:
fields.append(replace_sequence_counter(program.subtitle))
fields = [replace_sequence_counter(program.meta.title)]
if program.meta.subtitle:
fields.add(replace_sequence_counter(program.meta.subtitles))
if add_rendition:
fields.append(rendition.code)
if add_resolution:
fields.append(f"{v_meta.height}p")
if add_variant:
fields.append(variant.code)
name = sep.join(fields)
name = re.sub(r'[/:<>"\\|?*]', "", name)

View File

@ -7,7 +7,6 @@ import re
from .error import WebVTTError
RE_CUE_START = r"^((?:\d\d:)\d\d:\d\d)\.(\d\d\d) --> ((?:\d\d:)\d\d:\d\d)\.(\d\d\d)"
RE_STYLED_CUE = r"^<c\.(\w+)\.bg_(?:\w+)>(.*)</c>$"

View File

@ -3,28 +3,135 @@
"""Provide ArteTV website utilities."""
from .error import InvalidUrl
import json
BASE = "https://www.arte.tv/"
SITES = ["fr", "de", "en", "es", "pl", "it"]
from .error import InvalidPage, PageNotFound, PageNotSupported
from .model import Program
_DATA_MARK = '<script id="__NEXT_DATA__" type="application/json">'
def parse_url(url):
"""Parse ArteTV web URL into target ID and web UI language."""
if not url.startswith(BASE):
raise InvalidUrl("BASE", url)
def _process_programs_page(page_value):
path = url[len(BASE) :].split("/")
language = page_value["language"]
site = path.pop(0)
zone_found = False
program_found = False
if site not in SITES:
raise InvalidUrl("SITE", url, site)
for zone in page_value["zones"]:
if zone["code"].startswith("program_content_"):
if zone_found:
raise InvalidPage("PROGRAMS_CONTENT_ZONES_COUNT")
zone_found = True
else:
continue
if (_ := path.pop(0)) != "videos":
raise InvalidUrl("PATH", url, _)
for data_item in zone["content"]["data"]:
if data_item["type"] == "program":
if program_found:
raise InvalidPage("PROGRAMS_CONTENT_PROGRAM_COUNT")
program_found = True
else:
raise InvalidPage("PROGRAMS_CONTENT_PROGRAM_TYPE")
id = path.pop(0)
slug = path.pop(0)
yield (
Program(
data_item["programId"],
language,
data_item["title"],
data_item["subtitle"],
),
data_item["player"]["config"],
)
return site, id, slug
if not zone_found:
raise InvalidPage("PROGRAMS_CONTENT_ZONES_COUNT")
if not program_found:
raise InvalidPage("PROGRAMS_CONTENT_PROGRAM_COUNT")
def _process_collections_page(page_value):
language = page_value["language"]
main_zone_found = False
sub_zone_found = False
program_found = False
for zone in page_value["zones"]:
if zone["code"].startswith("collection_videos_"):
if main_zone_found:
raise InvalidPage("COLLECTIONS_MAIN_ZONE_COUNT")
if program_found:
raise InvalidPage("COLLECTIONS_MIXED_ZONES")
main_zone_found = True
elif zone["code"].startswith("collection_subcollection_"):
if program_found and not sub_zone_found:
raise InvalidPage("COLLECTIONS_MIXED_ZONES")
sub_zone_found = True
else:
continue
for data_item in zone["content"]["data"]:
if (_ := data_item["type"]) == "teaser":
program_found = True
else:
raise InvalidPage("COLLECTIONS_INVALID_CONTENT_DATA_ITEM", _)
yield (
Program(
data_item["programId"],
language,
data_item["title"],
data_item["subtitle"],
),
f"https://api.arte.tv/api/player/v2/config/{language}/{data_item['programId']}",
)
if not main_zone_found:
raise InvalidPage("COLLECTIONS_MAIN_ZONE_COUNT")
if not program_found:
raise InvalidPage("COLLECTIONS_PROGRAMS_COUNT")
def iter_programs(page_url, http_session):
"""Iterate over programs listed on given ArteTV page."""
r = http_session.get(page_url)
# special handling of 404
if r.status_code == 404:
raise PageNotFound(page_url)
r.raise_for_status()
# no HTML parsing required, whe just find the mark
html = r.text
start = html.find(_DATA_MARK)
if start < 0:
raise InvalidPage("DATA_MARK_NOT_FOUND", page_url)
start += len(_DATA_MARK)
end = html.index("</script>", start)
try:
next_js_data = json.loads(html[start:end].strip())
except json.JSONDecodeError:
raise InvalidPage("INVALID_JSON_DATA", page_url)
try:
initial_page_value = next_js_data["props"]["pageProps"]["initialPage"]["value"]
initial_type = next_js_data["props"]["pageProps"]["initialType"]
match initial_type:
case "programs":
yield from _process_programs_page(initial_page_value)
case "collections":
yield from _process_collections_page(initial_page_value)
case _:
raise PageNotSupported(page_url, initial_type)
except (KeyError, IndexError, ValueError) as e:
raise InvalidPage("SCHEMA", page_url) from e
except InvalidPage as e:
raise InvalidPage(e.args[0], page_url) from e