@ -1,5 +1,6 @@
from . common import InfoExtractor
from . . utils import (
ExtractorError ,
int_or_none ,
remove_end ,
str_or_none ,
@ -10,16 +11,16 @@ from ..utils import (
class GoProIE ( InfoExtractor ) :
_VALID_URL = r ' https?://(www \ .)?gopro \ .com/v /(?P<id>[A-Za-z0-9]+)'
_VALID_URL = r ' https?://(www \ .)?gopro \ .com/v (/(?P<plid>[A-Za-z0-9]+))?( /(?P<id>[A-Za-z0-9]+))? '
_TESTS = [ {
' url ' : ' https://gopro.com/v/ZNVvED8QDzR5V ' ,
' info_dict ' : {
' id ' : ' ZNVvED8QDzR5V ' ,
' id ' : ' LvV9MVbapDqKy ' ,
' title ' : ' My GoPro Adventure - 9/19/21 ' ,
' thumbnail ' : r ' re:https?://.+ ' ,
' ext ' : ' mp4 ' ,
' timestamp ' : 163207 2947 ,
' timestamp ' : 163207 0674 ,
' upload_date ' : ' 20210919 ' ,
' uploader_id ' : ' fireydive30018 ' ,
' duration ' : 396062 ,
@ -27,11 +28,25 @@ class GoProIE(InfoExtractor):
} , {
' url ' : ' https://gopro.com/v/KRm6Vgp2peg4e ' ,
' info_dict ' : {
' id ' : ' KRm6Vgp2peg4e ' ,
' id ' : ' 0rl3J2y2l5N3v ' ,
' title ' : ' じゃがいも カリカリ オーブン焼き ' ,
' thumbnail ' : r ' re:https?://.+ ' ,
' ext ' : ' mp4 ' ,
' timestamp ' : 1607231125 ,
' timestamp ' : 1607231111 ,
' upload_date ' : ' 20201206 ' ,
' uploader_id ' : ' dc9bcb8b-47d2-47c6-afbc-4c48f9a3769e ' ,
' duration ' : 45187 ,
' track ' : ' The Sky Machine ' ,
} ,
} , {
' url ' : ' https://gopro.com/v/KRm6Vgp2peg4e/0rl3J2y2l5N3v ' ,
' note ' : ' Same video as above, but full link (playlist+video) ' ,
' info_dict ' : {
' id ' : ' 0rl3J2y2l5N3v ' ,
' title ' : ' じゃがいも カリカリ オーブン焼き ' ,
' thumbnail ' : r ' re:https?://.+ ' ,
' ext ' : ' mp4 ' ,
' timestamp ' : 1607231111 ,
' upload_date ' : ' 20201206 ' ,
' uploader_id ' : ' dc9bcb8b-47d2-47c6-afbc-4c48f9a3769e ' ,
' duration ' : 45187 ,
@ -40,29 +55,49 @@ class GoProIE(InfoExtractor):
} , {
' url ' : ' https://gopro.com/v/kVrK9wlJvBMwn ' ,
' info_dict ' : {
' id ' : ' kVrK9wlJvBMwn ' ,
' title ' : ' DARKNESS ' ,
' id ' : ' pJzDM128KQ8wG ' ,
' title ' : ' DARKNESS (DARKNESS) ' ,
' thumbnail ' : r ' re:https?://.+ ' ,
' ext ' : ' mp4 ' ,
' timestamp ' : 159418 3735 ,
' timestamp ' : 159418 1304 ,
' upload_date ' : ' 20200708 ' ,
' uploader_id ' : ' 闇夜乃皇帝 ' ,
' duration ' : 313075 ,
' track ' : ' Battery (Live) ' ,
' artist ' : ' Metallica ' ,
' artist s ' : [ ' Metallica ' ] ,
} ,
} ]
def _real_extract ( self , url ) :
video_id = self . _match_id ( url )
webpage = self . _download_webpage ( url , video_id )
playlist_id = self . _match_valid_url ( url ) . group ( ' plid ' )
if playlist_id is None and video_id is None :
raise ExtractorError ( ' Invalid URL: neither video ID nor playlist ID is present ' )
webpage = self . _download_webpage ( url , video_id or playlist_id )
metadata = self . _search_json (
r ' window \ .__reflectData \ s*= ' , webpage , ' metadata ' , video_id )
r ' window \ .__reflectData \ s*= ' , webpage , ' metadata ' , video_id or playlist_id )
collection_title = str_or_none (
try_get ( metadata , lambda x : x [ ' collection ' ] [ ' title ' ] )
or self . _html_search_meta ( [ ' og:title ' , ' twitter:title ' ] , webpage )
or remove_end ( self . _html_search_regex (
r ' <title[^>]*>([^<]+)</title> ' , webpage , ' title ' , fatal = False ) , ' | GoPro ' ) )
if collection_title :
collection_title = collection_title . replace ( ' \n ' , ' ' )
entries = [ ]
for video_info in metadata [ ' collectionMedia ' ] :
# If video_id is none, downloading the whole playlist.
if video_id is None or video_id == video_info [ ' id ' ] :
entries . append ( self . ExtractOneEntry ( webpage , metadata , collection_title , video_info ) )
if not entries :
raise ExtractorError ( f ' { url } : No videos found ' )
return self . playlist_result ( entries , playlist_id = playlist_id ,
playlist_title = collection_title )
video_info = metadata [ ' collectionMedia ' ] [ 0 ]
def ExtractOneEntry ( self , webpage , metadata , collection_title , video_info ) :
video_id = video_info [ ' id ' ]
media_data = self . _download_json (
' https://api.gopro.com/media/ {} /download ' . format ( video_info [ ' id ' ] ) , video_id )
f ' https://api.gopro.com/media/ { video_id } /download ' , video_id )
formats = [ ]
for fmt in try_get ( media_data , lambda x : x [ ' _embedded ' ] [ ' variations ' ] ) or [ ] :
@ -78,13 +113,14 @@ class GoProIE(InfoExtractor):
' height ' : int_or_none ( fmt . get ( ' height ' ) ) ,
} )
title = str_or_none (
try_get ( metadata , lambda x : x [ ' collection ' ] [ ' title ' ] )
or self . _html_search_meta ( [ ' og:title ' , ' twitter:title ' ] , webpage )
or remove_end ( self . _html_search_regex (
r ' <title[^>]*>([^<]+)</title> ' , webpage , ' title ' , fatal = False ) , ' | GoPro ' ) )
if title :
title = title . replace ( ' \n ' , ' ' )
title = collection_title
content_title = video_info . get ( ' content_title ' )
if content_title :
content_title = content_title . replace ( ' \n ' , ' ' )
if title :
title + = f ' ( { content_title } ) '
else :
title = content_title
return {
' id ' : video_id ,
@ -93,7 +129,8 @@ class GoProIE(InfoExtractor):
' thumbnail ' : url_or_none (
self . _html_search_meta ( [ ' og:image ' , ' twitter:image ' ] , webpage ) ) ,
' timestamp ' : unified_timestamp (
try_get ( metadata , lambda x : x [ ' collection ' ] [ ' created_at ' ] ) ) ,
video_info . get ( ' created_at ' )
or try_get ( metadata , lambda x : x [ ' collection ' ] [ ' created_at ' ] ) ) ,
' uploader_id ' : str_or_none (
try_get ( metadata , lambda x : x [ ' account ' ] [ ' nickname ' ] ) ) ,
' duration ' : int_or_none (