4 from .common
import InfoExtractor
14 class StagePlusVODConcertIE(InfoExtractor
):
15 _NETRC_MACHINE
= 'stageplus'
16 _VALID_URL
= r
'https?://(?:www\.)?stage-plus\.com/video/(?P<id>vod_concert_\w+)'
18 'url': 'https://www.stage-plus.com/video/vod_concert_APNM8GRFDPHMASJKBSPJACG',
21 'id': 'vod_concert_APNM8GRFDPHMASJKBSPJACG',
22 'title': 'Yuja Wang plays Rachmaninoff\'s Piano Concerto No. 2 – from Odeonsplatz',
23 'description': 'md5:50f78ec180518c9bdb876bac550996fc',
24 'artist': ['Yuja Wang', 'Lorenzo Viotti'],
25 'upload_date': '20230331',
26 'timestamp': 1680249600,
27 'release_date': '20210709',
28 'release_timestamp': 1625788800,
29 'thumbnails': 'count:3',
33 'id': 'performance_work_A1IN4PJFE9MM2RJ3CLBMUSJBBSOJAD9O',
35 'title': 'Piano Concerto No. 2 in C Minor, Op. 18',
36 'description': 'md5:50f78ec180518c9bdb876bac550996fc',
37 'upload_date': '20230331',
38 'timestamp': 1680249600,
39 'release_date': '20210709',
40 'release_timestamp': 1625788800,
42 'chapters': 'count:5',
43 'artist': ['Yuja Wang'],
44 'composer': ['Sergei Rachmaninoff'],
45 'album': 'Yuja Wang plays Rachmaninoff\'s Piano Concerto No. 2 – from Odeonsplatz',
46 'album_artist': ['Yuja Wang', 'Lorenzo Viotti'],
47 'track': 'Piano Concerto No. 2 in C Minor, Op. 18',
49 'genre': 'Instrumental Concerto',
52 'params': {'skip_download': 'm3u8'}
,
55 # TODO: Prune this after livestream and/or album extractors are added
56 _GRAPHQL_QUERY
= '''query videoDetailPage($videoId: ID!, $sliderItemsFirst: Int = 24) {
82 ...livePerformanceWorkFields
86 ...contentContainerFields
114 ...contentContainerFields
139 ...PerformanceWorkFields
145 ...contentContainerFields
151 fragment LiveConcertFields on LiveConcert {
158 ...liveConcertRerunFields
176 fragment PictureFields on Picture {
182 fragment liveConcertRerunFields on LiveConcertRerun {
191 fragment rerunStreamFields on RerunStream {
197 fragment liveStreamFields on LiveStream {
203 fragment streamFields on Stream {
209 fragment RoleFields on Role {
216 fragment livePerformanceWorkFields on LivePerformanceWork {
220 ...artistWithRoleFields
236 fragment artistWithRoleFields on ArtistWithRoleConnection {
249 fragment workFields on Work {
266 fragment contentContainerFields on CuratedContentContainer {
272 fragment SliderFields on Slider {
275 items(first: $sliderItemsFirst) {
287 ...PerformanceWorkFields
295 fragment AlbumFields on Album {
296 artistAndGroupDisplayInfo
304 fragment ArtistFields on Artist {
315 fragment EpochFields on Epoch {
325 fragment GenreFields on Genre {
333 fragment GroupFields on Group {
342 fragment PartnerFields on Partner {
352 fragment PerformanceWorkFields on PerformanceWork {
356 ...artistWithRoleFields
384 fragment VideoFields on Video {
407 fragment VodConcertFields on VodConcert {
426 fragment BannerFields on Banner {
437 def _perform_login(self
, username
, password
):
438 auth
= self
._download
_json
('https://audience.api.stageplus.io/oauth/token', None, headers
={
439 'Content-Type': 'application/json',
440 'Origin': 'https://www.stage-plus.com',
442 'grant_type': 'password',
443 'username': username
,
444 'password': password
,
445 'device_info': 'Chrome (Windows)',
446 'client_device_id': str(uuid
.uuid4()),
447 }, separators
=(',', ':')).encode(), note
='Logging in')
449 if auth
.get('access_token'):
450 self
._TOKEN
= auth
['access_token']
452 def _real_initialize(self
):
456 self
._TOKEN
= try_call(
457 lambda: self
._get
_cookies
('https://www.stage-plus.com/')['dgplus_access_token'].value
)
459 self
.raise_login_required()
461 def _real_extract(self
, url
):
462 concert_id
= self
._match
_id
(url
)
464 data
= self
._download
_json
('https://audience.api.stageplus.io/graphql', concert_id
, headers
={
465 'authorization': f
'Bearer {self._TOKEN}',
466 'content-type': 'application/json',
467 'Origin': 'https://www.stage-plus.com',
469 'query': self
._GRAPHQL
_QUERY
,
470 'variables': {'videoId': concert_id}
,
471 'operationName': 'videoDetailPage'
472 }, separators
=(',', ':')).encode())['data']['node']
474 metadata
= traverse_obj(data
, {
476 'description': ('shortDescription', {str}
),
477 'artist': ('artists', 'edges', ..., 'node', 'name'),
478 'timestamp': ('archiveReleaseDate', {unified_timestamp}
),
479 'release_timestamp': ('productionDate', {unified_timestamp}
),
482 thumbnails
= traverse_obj(data
, ('pictures', lambda _
, v
: url_or_none(v
['url']), {
488 for idx
, video
in enumerate(traverse_obj(data
, (
489 'performanceWorks', lambda _
, v
: v
['id'] and url_or_none(v
['stream']['url']))), 1):
490 formats
, subtitles
= self
._extract
_m
3u8_formats
_and
_subtitles
(
491 video
['stream']['url'], video
['id'], 'mp4', m3u8_id
='hls', query
={'token': self._TOKEN}
)
495 'subtitles': subtitles
,
496 'album': metadata
.get('title'),
497 'album_artist': metadata
.get('artist'),
500 **traverse_obj(video
, {
501 'title': ('work', 'title'),
502 'track': ('work', 'title'),
503 'duration': ('duration', {float_or_none}
),
505 'cuePoints', lambda _
, v
: float_or_none(v
['mark']) is not None, {
507 'start_time': ('mark', {float_or_none}
),
509 'artist': ('artists', 'edges', ..., 'node', 'name'),
510 'composer': ('work', 'composers', ..., 'name'),
511 'genre': ('work', 'genre', 'title'),
515 return self
.playlist_result(entries
, concert_id
, thumbnails
=thumbnails
, **metadata
)