123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515 |
- import json
- import uuid
- from .common import InfoExtractor
- from ..utils import (
- float_or_none,
- traverse_obj,
- try_call,
- unified_timestamp,
- url_or_none,
- )
- class StagePlusVODConcertIE(InfoExtractor):
- _NETRC_MACHINE = 'stageplus'
- _VALID_URL = r'https?://(?:www\.)?stage-plus\.com/video/(?P<id>vod_concert_\w+)'
- _TESTS = [{
- 'url': 'https://www.stage-plus.com/video/vod_concert_APNM8GRFDPHMASJKBSPJACG',
- 'playlist_count': 6,
- 'info_dict': {
- 'id': 'vod_concert_APNM8GRFDPHMASJKBSPJACG',
- 'title': 'Yuja Wang plays Rachmaninoff\'s Piano Concerto No. 2 – from Odeonsplatz',
- 'description': 'md5:50f78ec180518c9bdb876bac550996fc',
- 'artists': ['Yuja Wang', 'Lorenzo Viotti'],
- 'upload_date': '20230331',
- 'timestamp': 1680249600,
- 'release_date': '20210709',
- 'release_timestamp': 1625788800,
- 'thumbnails': 'count:3',
- },
- 'playlist': [{
- 'info_dict': {
- 'id': 'performance_work_A1IN4PJFE9MM2RJ3CLBMUSJBBSOJAD9O',
- 'ext': 'mp4',
- 'title': 'Piano Concerto No. 2 in C Minor, Op. 18',
- 'description': 'md5:50f78ec180518c9bdb876bac550996fc',
- 'upload_date': '20230331',
- 'timestamp': 1680249600,
- 'release_date': '20210709',
- 'release_timestamp': 1625788800,
- 'duration': 2207,
- 'chapters': 'count:5',
- 'artists': ['Yuja Wang'],
- 'composers': ['Sergei Rachmaninoff'],
- 'album': 'Yuja Wang plays Rachmaninoff\'s Piano Concerto No. 2 – from Odeonsplatz',
- 'album_artists': ['Yuja Wang', 'Lorenzo Viotti'],
- 'track': 'Piano Concerto No. 2 in C Minor, Op. 18',
- 'track_number': 1,
- 'genre': 'Instrumental Concerto',
- },
- }],
- 'params': {'skip_download': 'm3u8'},
- }]
- # TODO: Prune this after livestream and/or album extractors are added
- _GRAPHQL_QUERY = '''query videoDetailPage($videoId: ID!, $sliderItemsFirst: Int = 24) {
- node(id: $videoId) {
- __typename
- ...LiveConcertFields
- ... on LiveConcert {
- artists {
- edges {
- role {
- ...RoleFields
- }
- node {
- id
- name
- sortName
- }
- }
- }
- isAtmos
- maxResolution
- groups {
- id
- name
- typeDisplayName
- }
- shortDescription
- performanceWorks {
- ...livePerformanceWorkFields
- }
- totalDuration
- sliders {
- ...contentContainerFields
- }
- vodConcert {
- __typename
- id
- }
- }
- ...VideoFields
- ... on Video {
- artists {
- edges {
- role {
- ...RoleFields
- }
- node {
- id
- name
- sortName
- }
- }
- }
- isAtmos
- maxResolution
- isLossless
- description
- productionDate
- takedownDate
- sliders {
- ...contentContainerFields
- }
- }
- ...VodConcertFields
- ... on VodConcert {
- artists {
- edges {
- role {
- ...RoleFields
- }
- node {
- id
- name
- sortName
- }
- }
- }
- isAtmos
- maxResolution
- groups {
- id
- name
- typeDisplayName
- }
- performanceWorks {
- ...PerformanceWorkFields
- }
- shortDescription
- productionDate
- takedownDate
- sliders {
- ...contentContainerFields
- }
- }
- }
- }
- fragment LiveConcertFields on LiveConcert {
- endTime
- id
- pictures {
- ...PictureFields
- }
- reruns {
- ...liveConcertRerunFields
- }
- publicationLevel
- startTime
- streamStartTime
- subtitle
- title
- typeDisplayName
- stream {
- ...liveStreamFields
- }
- trailerStream {
- ...streamFields
- }
- geoAccessCountries
- geoAccessMode
- }
- fragment PictureFields on Picture {
- id
- url
- type
- }
- fragment liveConcertRerunFields on LiveConcertRerun {
- streamStartTime
- endTime
- startTime
- stream {
- ...rerunStreamFields
- }
- }
- fragment rerunStreamFields on RerunStream {
- publicationLevel
- streamType
- url
- }
- fragment liveStreamFields on LiveStream {
- publicationLevel
- streamType
- url
- }
- fragment streamFields on Stream {
- publicationLevel
- streamType
- url
- }
- fragment RoleFields on Role {
- __typename
- id
- type
- displayName
- }
- fragment livePerformanceWorkFields on LivePerformanceWork {
- __typename
- id
- artists {
- ...artistWithRoleFields
- }
- groups {
- edges {
- node {
- id
- name
- typeDisplayName
- }
- }
- }
- work {
- ...workFields
- }
- }
- fragment artistWithRoleFields on ArtistWithRoleConnection {
- edges {
- role {
- ...RoleFields
- }
- node {
- id
- name
- sortName
- }
- }
- }
- fragment workFields on Work {
- id
- title
- movements {
- id
- title
- }
- composers {
- id
- name
- }
- genre {
- id
- title
- }
- }
- fragment contentContainerFields on CuratedContentContainer {
- __typename
- ...SliderFields
- ...BannerFields
- }
- fragment SliderFields on Slider {
- id
- headline
- items(first: $sliderItemsFirst) {
- edges {
- node {
- id
- __typename
- ...AlbumFields
- ...ArtistFields
- ...EpochFields
- ...GenreFields
- ...GroupFields
- ...LiveConcertFields
- ...PartnerFields
- ...PerformanceWorkFields
- ...VideoFields
- ...VodConcertFields
- }
- }
- }
- }
- fragment AlbumFields on Album {
- artistAndGroupDisplayInfo
- id
- pictures {
- ...PictureFields
- }
- title
- }
- fragment ArtistFields on Artist {
- id
- name
- roles {
- ...RoleFields
- }
- pictures {
- ...PictureFields
- }
- }
- fragment EpochFields on Epoch {
- id
- endYear
- pictures {
- ...PictureFields
- }
- startYear
- title
- }
- fragment GenreFields on Genre {
- id
- pictures {
- ...PictureFields
- }
- title
- }
- fragment GroupFields on Group {
- id
- name
- typeDisplayName
- pictures {
- ...PictureFields
- }
- }
- fragment PartnerFields on Partner {
- id
- name
- typeDisplayName
- subtypeDisplayName
- pictures {
- ...PictureFields
- }
- }
- fragment PerformanceWorkFields on PerformanceWork {
- __typename
- id
- artists {
- ...artistWithRoleFields
- }
- groups {
- edges {
- node {
- id
- name
- typeDisplayName
- }
- }
- }
- work {
- ...workFields
- }
- stream {
- ...streamFields
- }
- vodConcert {
- __typename
- id
- }
- duration
- cuePoints {
- mark
- title
- }
- }
- fragment VideoFields on Video {
- id
- archiveReleaseDate
- title
- subtitle
- pictures {
- ...PictureFields
- }
- stream {
- ...streamFields
- }
- trailerStream {
- ...streamFields
- }
- duration
- typeDisplayName
- duration
- geoAccessCountries
- geoAccessMode
- publicationLevel
- takedownDate
- }
- fragment VodConcertFields on VodConcert {
- id
- archiveReleaseDate
- pictures {
- ...PictureFields
- }
- subtitle
- title
- typeDisplayName
- totalDuration
- geoAccessCountries
- geoAccessMode
- trailerStream {
- ...streamFields
- }
- publicationLevel
- takedownDate
- }
- fragment BannerFields on Banner {
- description
- link
- pictures {
- ...PictureFields
- }
- title
- }'''
- _TOKEN = None
- def _perform_login(self, username, password):
- auth = self._download_json('https://audience.api.stageplus.io/oauth/token', None, headers={
- 'Content-Type': 'application/json',
- 'Origin': 'https://www.stage-plus.com',
- }, data=json.dumps({
- 'grant_type': 'password',
- 'username': username,
- 'password': password,
- 'device_info': 'Chrome (Windows)',
- 'client_device_id': str(uuid.uuid4()),
- }, separators=(',', ':')).encode(), note='Logging in')
- if auth.get('access_token'):
- self._TOKEN = auth['access_token']
- def _real_initialize(self):
- if self._TOKEN:
- return
- self._TOKEN = try_call(
- lambda: self._get_cookies('https://www.stage-plus.com/')['dgplus_access_token'].value)
- if not self._TOKEN:
- self.raise_login_required()
- def _real_extract(self, url):
- concert_id = self._match_id(url)
- data = self._download_json('https://audience.api.stageplus.io/graphql', concert_id, headers={
- 'authorization': f'Bearer {self._TOKEN}',
- 'content-type': 'application/json',
- 'Origin': 'https://www.stage-plus.com',
- }, data=json.dumps({
- 'query': self._GRAPHQL_QUERY,
- 'variables': {'videoId': concert_id},
- 'operationName': 'videoDetailPage',
- }, separators=(',', ':')).encode())['data']['node']
- metadata = traverse_obj(data, {
- 'title': 'title',
- 'description': ('shortDescription', {str}),
- 'artists': ('artists', 'edges', ..., 'node', 'name'),
- 'timestamp': ('archiveReleaseDate', {unified_timestamp}),
- 'release_timestamp': ('productionDate', {unified_timestamp}),
- })
- thumbnails = traverse_obj(data, ('pictures', lambda _, v: url_or_none(v['url']), {
- 'id': 'name',
- 'url': 'url',
- })) or None
- entries = []
- for idx, video in enumerate(traverse_obj(data, (
- 'performanceWorks', lambda _, v: v['id'] and url_or_none(v['stream']['url']))), 1):
- formats, subtitles = self._extract_m3u8_formats_and_subtitles(
- video['stream']['url'], video['id'], 'mp4', m3u8_id='hls', query={'token': self._TOKEN})
- entries.append({
- 'id': video['id'],
- 'formats': formats,
- 'subtitles': subtitles,
- 'album': metadata.get('title'),
- 'album_artists': metadata.get('artist'),
- 'track_number': idx,
- **metadata,
- **traverse_obj(video, {
- 'title': ('work', 'title'),
- 'track': ('work', 'title'),
- 'duration': ('duration', {float_or_none}),
- 'chapters': (
- 'cuePoints', lambda _, v: float_or_none(v['mark']) is not None, {
- 'title': 'title',
- 'start_time': ('mark', {float_or_none}),
- }),
- 'artists': ('artists', 'edges', ..., 'node', 'name'),
- 'composers': ('work', 'composers', ..., 'name'),
- 'genre': ('work', 'genre', 'title'),
- }),
- })
- return self.playlist_result(entries, concert_id, thumbnails=thumbnails, **metadata)
|