stageplus.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515
  1. import json
  2. import uuid
  3. from .common import InfoExtractor
  4. from ..utils import (
  5. float_or_none,
  6. traverse_obj,
  7. try_call,
  8. unified_timestamp,
  9. url_or_none,
  10. )
  11. class StagePlusVODConcertIE(InfoExtractor):
  12. _NETRC_MACHINE = 'stageplus'
  13. _VALID_URL = r'https?://(?:www\.)?stage-plus\.com/video/(?P<id>vod_concert_\w+)'
  14. _TESTS = [{
  15. 'url': 'https://www.stage-plus.com/video/vod_concert_APNM8GRFDPHMASJKBSPJACG',
  16. 'playlist_count': 6,
  17. 'info_dict': {
  18. 'id': 'vod_concert_APNM8GRFDPHMASJKBSPJACG',
  19. 'title': 'Yuja Wang plays Rachmaninoff\'s Piano Concerto No. 2 – from Odeonsplatz',
  20. 'description': 'md5:50f78ec180518c9bdb876bac550996fc',
  21. 'artists': ['Yuja Wang', 'Lorenzo Viotti'],
  22. 'upload_date': '20230331',
  23. 'timestamp': 1680249600,
  24. 'release_date': '20210709',
  25. 'release_timestamp': 1625788800,
  26. 'thumbnails': 'count:3',
  27. },
  28. 'playlist': [{
  29. 'info_dict': {
  30. 'id': 'performance_work_A1IN4PJFE9MM2RJ3CLBMUSJBBSOJAD9O',
  31. 'ext': 'mp4',
  32. 'title': 'Piano Concerto No. 2 in C Minor, Op. 18',
  33. 'description': 'md5:50f78ec180518c9bdb876bac550996fc',
  34. 'upload_date': '20230331',
  35. 'timestamp': 1680249600,
  36. 'release_date': '20210709',
  37. 'release_timestamp': 1625788800,
  38. 'duration': 2207,
  39. 'chapters': 'count:5',
  40. 'artists': ['Yuja Wang'],
  41. 'composers': ['Sergei Rachmaninoff'],
  42. 'album': 'Yuja Wang plays Rachmaninoff\'s Piano Concerto No. 2 – from Odeonsplatz',
  43. 'album_artists': ['Yuja Wang', 'Lorenzo Viotti'],
  44. 'track': 'Piano Concerto No. 2 in C Minor, Op. 18',
  45. 'track_number': 1,
  46. 'genre': 'Instrumental Concerto',
  47. },
  48. }],
  49. 'params': {'skip_download': 'm3u8'},
  50. }]
  51. # TODO: Prune this after livestream and/or album extractors are added
  52. _GRAPHQL_QUERY = '''query videoDetailPage($videoId: ID!, $sliderItemsFirst: Int = 24) {
  53. node(id: $videoId) {
  54. __typename
  55. ...LiveConcertFields
  56. ... on LiveConcert {
  57. artists {
  58. edges {
  59. role {
  60. ...RoleFields
  61. }
  62. node {
  63. id
  64. name
  65. sortName
  66. }
  67. }
  68. }
  69. isAtmos
  70. maxResolution
  71. groups {
  72. id
  73. name
  74. typeDisplayName
  75. }
  76. shortDescription
  77. performanceWorks {
  78. ...livePerformanceWorkFields
  79. }
  80. totalDuration
  81. sliders {
  82. ...contentContainerFields
  83. }
  84. vodConcert {
  85. __typename
  86. id
  87. }
  88. }
  89. ...VideoFields
  90. ... on Video {
  91. artists {
  92. edges {
  93. role {
  94. ...RoleFields
  95. }
  96. node {
  97. id
  98. name
  99. sortName
  100. }
  101. }
  102. }
  103. isAtmos
  104. maxResolution
  105. isLossless
  106. description
  107. productionDate
  108. takedownDate
  109. sliders {
  110. ...contentContainerFields
  111. }
  112. }
  113. ...VodConcertFields
  114. ... on VodConcert {
  115. artists {
  116. edges {
  117. role {
  118. ...RoleFields
  119. }
  120. node {
  121. id
  122. name
  123. sortName
  124. }
  125. }
  126. }
  127. isAtmos
  128. maxResolution
  129. groups {
  130. id
  131. name
  132. typeDisplayName
  133. }
  134. performanceWorks {
  135. ...PerformanceWorkFields
  136. }
  137. shortDescription
  138. productionDate
  139. takedownDate
  140. sliders {
  141. ...contentContainerFields
  142. }
  143. }
  144. }
  145. }
  146. fragment LiveConcertFields on LiveConcert {
  147. endTime
  148. id
  149. pictures {
  150. ...PictureFields
  151. }
  152. reruns {
  153. ...liveConcertRerunFields
  154. }
  155. publicationLevel
  156. startTime
  157. streamStartTime
  158. subtitle
  159. title
  160. typeDisplayName
  161. stream {
  162. ...liveStreamFields
  163. }
  164. trailerStream {
  165. ...streamFields
  166. }
  167. geoAccessCountries
  168. geoAccessMode
  169. }
  170. fragment PictureFields on Picture {
  171. id
  172. url
  173. type
  174. }
  175. fragment liveConcertRerunFields on LiveConcertRerun {
  176. streamStartTime
  177. endTime
  178. startTime
  179. stream {
  180. ...rerunStreamFields
  181. }
  182. }
  183. fragment rerunStreamFields on RerunStream {
  184. publicationLevel
  185. streamType
  186. url
  187. }
  188. fragment liveStreamFields on LiveStream {
  189. publicationLevel
  190. streamType
  191. url
  192. }
  193. fragment streamFields on Stream {
  194. publicationLevel
  195. streamType
  196. url
  197. }
  198. fragment RoleFields on Role {
  199. __typename
  200. id
  201. type
  202. displayName
  203. }
  204. fragment livePerformanceWorkFields on LivePerformanceWork {
  205. __typename
  206. id
  207. artists {
  208. ...artistWithRoleFields
  209. }
  210. groups {
  211. edges {
  212. node {
  213. id
  214. name
  215. typeDisplayName
  216. }
  217. }
  218. }
  219. work {
  220. ...workFields
  221. }
  222. }
  223. fragment artistWithRoleFields on ArtistWithRoleConnection {
  224. edges {
  225. role {
  226. ...RoleFields
  227. }
  228. node {
  229. id
  230. name
  231. sortName
  232. }
  233. }
  234. }
  235. fragment workFields on Work {
  236. id
  237. title
  238. movements {
  239. id
  240. title
  241. }
  242. composers {
  243. id
  244. name
  245. }
  246. genre {
  247. id
  248. title
  249. }
  250. }
  251. fragment contentContainerFields on CuratedContentContainer {
  252. __typename
  253. ...SliderFields
  254. ...BannerFields
  255. }
  256. fragment SliderFields on Slider {
  257. id
  258. headline
  259. items(first: $sliderItemsFirst) {
  260. edges {
  261. node {
  262. id
  263. __typename
  264. ...AlbumFields
  265. ...ArtistFields
  266. ...EpochFields
  267. ...GenreFields
  268. ...GroupFields
  269. ...LiveConcertFields
  270. ...PartnerFields
  271. ...PerformanceWorkFields
  272. ...VideoFields
  273. ...VodConcertFields
  274. }
  275. }
  276. }
  277. }
  278. fragment AlbumFields on Album {
  279. artistAndGroupDisplayInfo
  280. id
  281. pictures {
  282. ...PictureFields
  283. }
  284. title
  285. }
  286. fragment ArtistFields on Artist {
  287. id
  288. name
  289. roles {
  290. ...RoleFields
  291. }
  292. pictures {
  293. ...PictureFields
  294. }
  295. }
  296. fragment EpochFields on Epoch {
  297. id
  298. endYear
  299. pictures {
  300. ...PictureFields
  301. }
  302. startYear
  303. title
  304. }
  305. fragment GenreFields on Genre {
  306. id
  307. pictures {
  308. ...PictureFields
  309. }
  310. title
  311. }
  312. fragment GroupFields on Group {
  313. id
  314. name
  315. typeDisplayName
  316. pictures {
  317. ...PictureFields
  318. }
  319. }
  320. fragment PartnerFields on Partner {
  321. id
  322. name
  323. typeDisplayName
  324. subtypeDisplayName
  325. pictures {
  326. ...PictureFields
  327. }
  328. }
  329. fragment PerformanceWorkFields on PerformanceWork {
  330. __typename
  331. id
  332. artists {
  333. ...artistWithRoleFields
  334. }
  335. groups {
  336. edges {
  337. node {
  338. id
  339. name
  340. typeDisplayName
  341. }
  342. }
  343. }
  344. work {
  345. ...workFields
  346. }
  347. stream {
  348. ...streamFields
  349. }
  350. vodConcert {
  351. __typename
  352. id
  353. }
  354. duration
  355. cuePoints {
  356. mark
  357. title
  358. }
  359. }
  360. fragment VideoFields on Video {
  361. id
  362. archiveReleaseDate
  363. title
  364. subtitle
  365. pictures {
  366. ...PictureFields
  367. }
  368. stream {
  369. ...streamFields
  370. }
  371. trailerStream {
  372. ...streamFields
  373. }
  374. duration
  375. typeDisplayName
  376. duration
  377. geoAccessCountries
  378. geoAccessMode
  379. publicationLevel
  380. takedownDate
  381. }
  382. fragment VodConcertFields on VodConcert {
  383. id
  384. archiveReleaseDate
  385. pictures {
  386. ...PictureFields
  387. }
  388. subtitle
  389. title
  390. typeDisplayName
  391. totalDuration
  392. geoAccessCountries
  393. geoAccessMode
  394. trailerStream {
  395. ...streamFields
  396. }
  397. publicationLevel
  398. takedownDate
  399. }
  400. fragment BannerFields on Banner {
  401. description
  402. link
  403. pictures {
  404. ...PictureFields
  405. }
  406. title
  407. }'''
  408. _TOKEN = None
  409. def _perform_login(self, username, password):
  410. auth = self._download_json('https://audience.api.stageplus.io/oauth/token', None, headers={
  411. 'Content-Type': 'application/json',
  412. 'Origin': 'https://www.stage-plus.com',
  413. }, data=json.dumps({
  414. 'grant_type': 'password',
  415. 'username': username,
  416. 'password': password,
  417. 'device_info': 'Chrome (Windows)',
  418. 'client_device_id': str(uuid.uuid4()),
  419. }, separators=(',', ':')).encode(), note='Logging in')
  420. if auth.get('access_token'):
  421. self._TOKEN = auth['access_token']
  422. def _real_initialize(self):
  423. if self._TOKEN:
  424. return
  425. self._TOKEN = try_call(
  426. lambda: self._get_cookies('https://www.stage-plus.com/')['dgplus_access_token'].value)
  427. if not self._TOKEN:
  428. self.raise_login_required()
  429. def _real_extract(self, url):
  430. concert_id = self._match_id(url)
  431. data = self._download_json('https://audience.api.stageplus.io/graphql', concert_id, headers={
  432. 'authorization': f'Bearer {self._TOKEN}',
  433. 'content-type': 'application/json',
  434. 'Origin': 'https://www.stage-plus.com',
  435. }, data=json.dumps({
  436. 'query': self._GRAPHQL_QUERY,
  437. 'variables': {'videoId': concert_id},
  438. 'operationName': 'videoDetailPage',
  439. }, separators=(',', ':')).encode())['data']['node']
  440. metadata = traverse_obj(data, {
  441. 'title': 'title',
  442. 'description': ('shortDescription', {str}),
  443. 'artists': ('artists', 'edges', ..., 'node', 'name'),
  444. 'timestamp': ('archiveReleaseDate', {unified_timestamp}),
  445. 'release_timestamp': ('productionDate', {unified_timestamp}),
  446. })
  447. thumbnails = traverse_obj(data, ('pictures', lambda _, v: url_or_none(v['url']), {
  448. 'id': 'name',
  449. 'url': 'url',
  450. })) or None
  451. entries = []
  452. for idx, video in enumerate(traverse_obj(data, (
  453. 'performanceWorks', lambda _, v: v['id'] and url_or_none(v['stream']['url']))), 1):
  454. formats, subtitles = self._extract_m3u8_formats_and_subtitles(
  455. video['stream']['url'], video['id'], 'mp4', m3u8_id='hls', query={'token': self._TOKEN})
  456. entries.append({
  457. 'id': video['id'],
  458. 'formats': formats,
  459. 'subtitles': subtitles,
  460. 'album': metadata.get('title'),
  461. 'album_artists': metadata.get('artist'),
  462. 'track_number': idx,
  463. **metadata,
  464. **traverse_obj(video, {
  465. 'title': ('work', 'title'),
  466. 'track': ('work', 'title'),
  467. 'duration': ('duration', {float_or_none}),
  468. 'chapters': (
  469. 'cuePoints', lambda _, v: float_or_none(v['mark']) is not None, {
  470. 'title': 'title',
  471. 'start_time': ('mark', {float_or_none}),
  472. }),
  473. 'artists': ('artists', 'edges', ..., 'node', 'name'),
  474. 'composers': ('work', 'composers', ..., 'name'),
  475. 'genre': ('work', 'genre', 'title'),
  476. }),
  477. })
  478. return self.playlist_result(entries, concert_id, thumbnails=thumbnails, **metadata)