gamejolt.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537
  1. import itertools
  2. import json
  3. import math
  4. import urllib.parse
  5. from .common import InfoExtractor
  6. from ..utils import (
  7. determine_ext,
  8. format_field,
  9. int_or_none,
  10. str_or_none,
  11. traverse_obj,
  12. try_get,
  13. )
  14. class GameJoltBaseIE(InfoExtractor):
  15. _API_BASE = 'https://gamejolt.com/site-api/'
  16. def _call_api(self, endpoint, *args, **kwargs):
  17. kwargs.setdefault('headers', {}).update({'Accept': 'image/webp,*/*'})
  18. return self._download_json(self._API_BASE + endpoint, *args, **kwargs)['payload']
  19. def _parse_content_as_text(self, content):
  20. outer_contents, joined_contents = content.get('content') or [], []
  21. for outer_content in outer_contents:
  22. if outer_content.get('type') != 'paragraph':
  23. joined_contents.append(self._parse_content_as_text(outer_content))
  24. continue
  25. inner_contents, inner_content_text = outer_content.get('content') or [], ''
  26. for inner_content in inner_contents:
  27. if inner_content.get('text'):
  28. inner_content_text += inner_content['text']
  29. elif inner_content.get('type') == 'hardBreak':
  30. inner_content_text += '\n'
  31. joined_contents.append(inner_content_text)
  32. return '\n'.join(joined_contents)
  33. def _get_comments(self, post_num_id, post_hash_id):
  34. sort_by, scroll_id = self._configuration_arg('comment_sort', ['hot'], ie_key=GameJoltIE.ie_key())[0], -1
  35. is_scrolled = sort_by in ('new', 'you')
  36. for page in itertools.count(1):
  37. comments_data = self._call_api(
  38. 'comments/Fireside_Post/%s/%s?%s=%d' % (
  39. post_num_id, sort_by,
  40. 'scroll_id' if is_scrolled else 'page', scroll_id if is_scrolled else page),
  41. post_hash_id, note=f'Downloading comments list page {page}')
  42. if not comments_data.get('comments'):
  43. break
  44. for comment in traverse_obj(comments_data, (('comments', 'childComments'), ...), expected_type=dict):
  45. yield {
  46. 'id': comment['id'],
  47. 'text': self._parse_content_as_text(
  48. self._parse_json(comment['comment_content'], post_hash_id)),
  49. 'timestamp': int_or_none(comment.get('posted_on'), scale=1000),
  50. 'like_count': comment.get('votes'),
  51. 'author': traverse_obj(comment, ('user', ('display_name', 'name')), expected_type=str_or_none, get_all=False),
  52. 'author_id': traverse_obj(comment, ('user', 'username'), expected_type=str_or_none),
  53. 'author_thumbnail': traverse_obj(comment, ('user', 'image_avatar'), expected_type=str_or_none),
  54. 'parent': comment.get('parent_id') or None,
  55. }
  56. scroll_id = int_or_none(comments_data['comments'][-1].get('posted_on'))
  57. def _parse_post(self, post_data):
  58. post_id = post_data['hash']
  59. lead_content = self._parse_json(post_data.get('lead_content') or '{}', post_id, fatal=False) or {}
  60. description, full_description = post_data.get('leadStr') or self._parse_content_as_text(
  61. self._parse_json(post_data.get('lead_content'), post_id)), None
  62. if post_data.get('has_article'):
  63. article_content = self._parse_json(
  64. post_data.get('article_content')
  65. or self._call_api(f'web/posts/article/{post_data.get("id", post_id)}', post_id,
  66. note='Downloading article metadata', errnote='Unable to download article metadata', fatal=False).get('article'),
  67. post_id, fatal=False)
  68. full_description = self._parse_content_as_text(article_content)
  69. user_data = post_data.get('user') or {}
  70. info_dict = {
  71. 'extractor_key': GameJoltIE.ie_key(),
  72. 'extractor': 'GameJolt',
  73. 'webpage_url': str_or_none(post_data.get('url')) or f'https://gamejolt.com/p/{post_id}',
  74. 'id': post_id,
  75. 'title': description,
  76. 'description': full_description or description,
  77. 'display_id': post_data.get('slug'),
  78. 'uploader': user_data.get('display_name') or user_data.get('name'),
  79. 'uploader_id': user_data.get('username'),
  80. 'uploader_url': format_field(user_data, 'url', 'https://gamejolt.com%s'),
  81. 'categories': [try_get(category, lambda x: '{} - {}'.format(x['community']['name'], x['channel'].get('display_title') or x['channel']['title']))
  82. for category in post_data.get('communities') or []],
  83. 'tags': traverse_obj(
  84. lead_content, ('content', ..., 'content', ..., 'marks', ..., 'attrs', 'tag'), expected_type=str_or_none),
  85. 'like_count': int_or_none(post_data.get('like_count')),
  86. 'comment_count': int_or_none(post_data.get('comment_count'), default=0),
  87. 'timestamp': int_or_none(post_data.get('added_on'), scale=1000),
  88. 'release_timestamp': int_or_none(post_data.get('published_on'), scale=1000),
  89. '__post_extractor': self.extract_comments(post_data.get('id'), post_id),
  90. }
  91. # TODO: Handle multiple videos/embeds?
  92. video_data = traverse_obj(post_data, ('videos', ...), expected_type=dict, get_all=False) or {}
  93. formats, subtitles, thumbnails = [], {}, []
  94. for media in video_data.get('media') or []:
  95. media_url, mimetype, ext, media_id = media['img_url'], media.get('filetype', ''), determine_ext(media['img_url']), media.get('type')
  96. if mimetype == 'application/vnd.apple.mpegurl' or ext == 'm3u8':
  97. hls_formats, hls_subs = self._extract_m3u8_formats_and_subtitles(media_url, post_id, 'mp4', m3u8_id=media_id)
  98. formats.extend(hls_formats)
  99. subtitles.update(hls_subs)
  100. elif mimetype == 'application/dash+xml' or ext == 'mpd':
  101. dash_formats, dash_subs = self._extract_mpd_formats_and_subtitles(media_url, post_id, mpd_id=media_id)
  102. formats.extend(dash_formats)
  103. subtitles.update(dash_subs)
  104. elif 'image' in mimetype:
  105. thumbnails.append({
  106. 'id': media_id,
  107. 'url': media_url,
  108. 'width': media.get('width'),
  109. 'height': media.get('height'),
  110. 'filesize': media.get('filesize'),
  111. })
  112. else:
  113. formats.append({
  114. 'format_id': media_id,
  115. 'url': media_url,
  116. 'width': media.get('width'),
  117. 'height': media.get('height'),
  118. 'filesize': media.get('filesize'),
  119. 'acodec': 'none' if 'video-card' in media_url else None,
  120. })
  121. if formats:
  122. return {
  123. **info_dict,
  124. 'formats': formats,
  125. 'subtitles': subtitles,
  126. 'thumbnails': thumbnails,
  127. 'view_count': int_or_none(video_data.get('view_count')),
  128. }
  129. gif_entries = []
  130. for media in post_data.get('media', []):
  131. if determine_ext(media['img_url']) != 'gif' or 'gif' not in media.get('filetype', ''):
  132. continue
  133. gif_entries.append({
  134. 'id': media['hash'],
  135. 'title': media['filename'].split('.')[0],
  136. 'formats': [{
  137. 'format_id': url_key,
  138. 'url': media[url_key],
  139. 'width': media.get('width') if url_key == 'img_url' else None,
  140. 'height': media.get('height') if url_key == 'img_url' else None,
  141. 'filesize': media.get('filesize') if url_key == 'img_url' else None,
  142. 'acodec': 'none',
  143. } for url_key in ('img_url', 'mediaserver_url', 'mediaserver_url_mp4', 'mediaserver_url_webm') if media.get(url_key)],
  144. })
  145. if gif_entries:
  146. return {
  147. '_type': 'playlist',
  148. **info_dict,
  149. 'entries': gif_entries,
  150. }
  151. embed_url = traverse_obj(post_data, ('embeds', ..., 'url'), expected_type=str_or_none, get_all=False)
  152. if embed_url:
  153. return self.url_result(embed_url)
  154. return info_dict
  155. class GameJoltIE(GameJoltBaseIE):
  156. _VALID_URL = r'https?://(?:www\.)?gamejolt\.com/p/(?:[\w-]*-)?(?P<id>\w{8})'
  157. _TESTS = [{
  158. # No audio
  159. 'url': 'https://gamejolt.com/p/introducing-ramses-jackson-some-fnf-himbo-i-ve-been-animating-fo-c6achnzu',
  160. 'md5': 'cd5f733258f6678b0ce500dd88166d86',
  161. 'info_dict': {
  162. 'id': 'c6achnzu',
  163. 'ext': 'mp4',
  164. 'display_id': 'introducing-ramses-jackson-some-fnf-himbo-i-ve-been-animating-fo-c6achnzu',
  165. 'title': 'Introducing Ramses Jackson, some FNF himbo I’ve been animating for the past few days, hehe.\n#fnfmod #fridaynightfunkin',
  166. 'description': 'Introducing Ramses Jackson, some FNF himbo I’ve been animating for the past few days, hehe.\n#fnfmod #fridaynightfunkin',
  167. 'uploader': 'Jakeneutron',
  168. 'uploader_id': 'Jakeneutron',
  169. 'uploader_url': 'https://gamejolt.com/@Jakeneutron',
  170. 'categories': ['Friday Night Funkin\' - Videos'],
  171. 'tags': ['fnfmod', 'fridaynightfunkin'],
  172. 'timestamp': 1633499590,
  173. 'upload_date': '20211006',
  174. 'release_timestamp': 1633499655,
  175. 'release_date': '20211006',
  176. 'thumbnail': 're:^https?://.+wgch9mhq.png$',
  177. 'like_count': int,
  178. 'comment_count': int,
  179. 'view_count': int,
  180. },
  181. }, {
  182. # YouTube embed
  183. 'url': 'https://gamejolt.com/p/hey-hey-if-there-s-anyone-who-s-looking-to-get-into-learning-a-n6g4jzpq',
  184. 'md5': '79a931ff500a5c783ef6c3bda3272e32',
  185. 'info_dict': {
  186. 'id': 'XsNA_mzC0q4',
  187. 'title': 'Adobe Animate CC 2021 Tutorial || Part 1 - The Basics',
  188. 'description': 'md5:9d1ab9e2625b3fe1f42b2a44c67fdd13',
  189. 'uploader': 'Jakeneutron',
  190. 'uploader_id': 'Jakeneutron',
  191. 'uploader_url': 'http://www.youtube.com/user/Jakeneutron',
  192. 'ext': 'mp4',
  193. 'duration': 1749,
  194. 'tags': ['Adobe Animate CC', 'Tutorial', 'Animation', 'The Basics', 'For Beginners'],
  195. 'like_count': int,
  196. 'playable_in_embed': True,
  197. 'categories': ['Education'],
  198. 'availability': 'public',
  199. 'thumbnail': 'https://i.ytimg.com/vi_webp/XsNA_mzC0q4/maxresdefault.webp',
  200. 'age_limit': 0,
  201. 'live_status': 'not_live',
  202. 'channel_url': 'https://www.youtube.com/channel/UC6_L7fnczNalFZyBthUE9oA',
  203. 'channel': 'Jakeneutron',
  204. 'channel_id': 'UC6_L7fnczNalFZyBthUE9oA',
  205. 'upload_date': '20211015',
  206. 'view_count': int,
  207. 'chapters': 'count:18',
  208. },
  209. }, {
  210. # Article
  211. 'url': 'https://gamejolt.com/p/i-fuckin-broke-chaos-d56h3eue',
  212. 'md5': '786c1ccf98fde02c03a2768acb4258d0',
  213. 'info_dict': {
  214. 'id': 'd56h3eue',
  215. 'ext': 'mp4',
  216. 'display_id': 'i-fuckin-broke-chaos-d56h3eue',
  217. 'title': 'I fuckin broke Chaos.',
  218. 'description': 'I moved my tab durning the cutscene so now it\'s stuck like this.',
  219. 'uploader': 'Jeff____________',
  220. 'uploader_id': 'The_Nyesh_Man',
  221. 'uploader_url': 'https://gamejolt.com/@The_Nyesh_Man',
  222. 'categories': ['Friday Night Funkin\' - Videos'],
  223. 'timestamp': 1639800264,
  224. 'upload_date': '20211218',
  225. 'release_timestamp': 1639800330,
  226. 'release_date': '20211218',
  227. 'thumbnail': 're:^https?://.+euksy8bd.png$',
  228. 'like_count': int,
  229. 'comment_count': int,
  230. 'view_count': int,
  231. },
  232. }, {
  233. # Single GIF
  234. 'url': 'https://gamejolt.com/p/hello-everyone-i-m-developing-a-pixel-art-style-mod-for-fnf-and-i-vs4gdrd8',
  235. 'info_dict': {
  236. 'id': 'vs4gdrd8',
  237. 'display_id': 'hello-everyone-i-m-developing-a-pixel-art-style-mod-for-fnf-and-i-vs4gdrd8',
  238. 'title': 'md5:cc3d8b031d9bc7ec2ec5a9ffc707e1f9',
  239. 'description': 'md5:cc3d8b031d9bc7ec2ec5a9ffc707e1f9',
  240. 'uploader': 'Quesoguy',
  241. 'uploader_id': 'CheeseguyDev',
  242. 'uploader_url': 'https://gamejolt.com/@CheeseguyDev',
  243. 'categories': ['Game Dev - General', 'Arts n\' Crafts - Creations', 'Pixel Art - showcase',
  244. 'Friday Night Funkin\' - Mods', 'Newgrounds - Friday Night Funkin (13+)'],
  245. 'timestamp': 1639517122,
  246. 'release_timestamp': 1639519966,
  247. 'like_count': int,
  248. 'comment_count': int,
  249. },
  250. 'playlist': [{
  251. 'info_dict': {
  252. 'id': 'dszyjnwi',
  253. 'ext': 'webm',
  254. 'title': 'gif-presentacion-mejorado-dszyjnwi',
  255. },
  256. }],
  257. 'playlist_count': 1,
  258. }, {
  259. # Multiple GIFs
  260. 'url': 'https://gamejolt.com/p/gif-yhsqkumq',
  261. 'playlist_count': 35,
  262. 'info_dict': {
  263. 'id': 'yhsqkumq',
  264. 'display_id': 'gif-yhsqkumq',
  265. 'title': 'GIF',
  266. 'description': 'GIF',
  267. 'uploader': 'DaniilTvman',
  268. 'uploader_id': 'DaniilTvman',
  269. 'uploader_url': 'https://gamejolt.com/@DaniilTvman',
  270. 'categories': ['Five Nights At The AGK Studio Comunity - NEWS game'],
  271. 'timestamp': 1638721559,
  272. 'release_timestamp': 1638722276,
  273. 'like_count': int,
  274. 'comment_count': int,
  275. },
  276. }]
  277. def _real_extract(self, url):
  278. post_id = self._match_id(url)
  279. post_data = self._call_api(
  280. f'web/posts/view/{post_id}', post_id)['post']
  281. return self._parse_post(post_data)
  282. class GameJoltPostListBaseIE(GameJoltBaseIE):
  283. def _entries(self, endpoint, list_id, note='Downloading post list', errnote='Unable to download post list', initial_items=[]):
  284. page_num, scroll_id = 1, None
  285. items = initial_items or self._call_api(endpoint, list_id, note=note, errnote=errnote)['items']
  286. while items:
  287. for item in items:
  288. yield self._parse_post(item['action_resource_model'])
  289. scroll_id = items[-1]['scroll_id']
  290. page_num += 1
  291. items = self._call_api(
  292. endpoint, list_id, note=f'{note} page {page_num}', errnote=errnote, data=json.dumps({
  293. 'scrollDirection': 'from',
  294. 'scrollId': scroll_id,
  295. }).encode()).get('items')
  296. class GameJoltUserIE(GameJoltPostListBaseIE):
  297. _VALID_URL = r'https?://(?:www\.)?gamejolt\.com/@(?P<id>[\w-]+)'
  298. _TESTS = [{
  299. 'url': 'https://gamejolt.com/@BlazikenSuperStar',
  300. 'playlist_mincount': 1,
  301. 'info_dict': {
  302. 'id': '6116784',
  303. 'title': 'S. Blaze',
  304. 'description': 'md5:5ba7fbbb549e8ea2545aafbfe22eb03a',
  305. },
  306. 'params': {
  307. 'ignore_no_formats_error': True,
  308. },
  309. 'expected_warnings': ['skipping format', 'No video formats found', 'Requested format is not available'],
  310. }]
  311. def _real_extract(self, url):
  312. user_id = self._match_id(url)
  313. user_data = self._call_api(
  314. f'web/profile/@{user_id}', user_id, note='Downloading user info', errnote='Unable to download user info')['user']
  315. bio = self._parse_content_as_text(
  316. self._parse_json(user_data.get('bio_content', '{}'), user_id, fatal=False) or {})
  317. return self.playlist_result(
  318. self._entries(f'web/posts/fetch/user/@{user_id}?tab=active', user_id, 'Downloading user posts', 'Unable to download user posts'),
  319. str_or_none(user_data.get('id')), user_data.get('display_name') or user_data.get('name'), bio)
  320. class GameJoltGameIE(GameJoltPostListBaseIE):
  321. _VALID_URL = r'https?://(?:www\.)?gamejolt\.com/games/[\w-]+/(?P<id>\d+)'
  322. _TESTS = [{
  323. 'url': 'https://gamejolt.com/games/Friday4Fun/655124',
  324. 'playlist_mincount': 2,
  325. 'info_dict': {
  326. 'id': '655124',
  327. 'title': 'Friday Night Funkin\': Friday 4 Fun',
  328. 'description': 'md5:576a7dd87912a2dcf33c50d2bd3966d3',
  329. },
  330. 'params': {
  331. 'ignore_no_formats_error': True,
  332. },
  333. 'expected_warnings': ['skipping format', 'No video formats found', 'Requested format is not available'],
  334. }]
  335. def _real_extract(self, url):
  336. game_id = self._match_id(url)
  337. game_data = self._call_api(
  338. f'web/discover/games/{game_id}', game_id, note='Downloading game info', errnote='Unable to download game info')['game']
  339. description = self._parse_content_as_text(
  340. self._parse_json(game_data.get('description_content', '{}'), game_id, fatal=False) or {})
  341. return self.playlist_result(
  342. self._entries(f'web/posts/fetch/game/{game_id}', game_id, 'Downloading game posts', 'Unable to download game posts'),
  343. game_id, game_data.get('title'), description)
  344. class GameJoltGameSoundtrackIE(GameJoltBaseIE):
  345. _VALID_URL = r'https?://(?:www\.)?gamejolt\.com/get/soundtrack(?:\?|\#!?)(?:.*?[&;])??game=(?P<id>(?:\d+)+)'
  346. _TESTS = [{
  347. 'url': 'https://gamejolt.com/get/soundtrack?foo=bar&game=657899',
  348. 'info_dict': {
  349. 'id': '657899',
  350. 'title': 'Friday Night Funkin\': Vs Oswald',
  351. },
  352. 'playlist': [{
  353. 'info_dict': {
  354. 'id': '184434',
  355. 'ext': 'mp3',
  356. 'title': 'Gettin\' Lucky (Menu Music)',
  357. 'url': r're:^https://.+vs-oswald-menu-music\.mp3$',
  358. 'release_timestamp': 1635190816,
  359. 'release_date': '20211025',
  360. },
  361. }, {
  362. 'info_dict': {
  363. 'id': '184435',
  364. 'ext': 'mp3',
  365. 'title': 'Rabbit\'s Luck (Extended Version)',
  366. 'url': r're:^https://.+rabbit-s-luck--full-version-\.mp3$',
  367. 'release_timestamp': 1635190841,
  368. 'release_date': '20211025',
  369. },
  370. }, {
  371. 'info_dict': {
  372. 'id': '185228',
  373. 'ext': 'mp3',
  374. 'title': 'Last Straw',
  375. 'url': r're:^https://.+last-straw\.mp3$',
  376. 'release_timestamp': 1635881104,
  377. 'release_date': '20211102',
  378. },
  379. }],
  380. 'playlist_count': 3,
  381. }]
  382. def _real_extract(self, url):
  383. game_id = self._match_id(url)
  384. game_overview = self._call_api(
  385. f'web/discover/games/overview/{game_id}', game_id, note='Downloading soundtrack info', errnote='Unable to download soundtrack info')
  386. return self.playlist_result([{
  387. 'id': str_or_none(song.get('id')),
  388. 'title': str_or_none(song.get('title')),
  389. 'url': str_or_none(song.get('url')),
  390. 'release_timestamp': int_or_none(song.get('posted_on'), scale=1000),
  391. } for song in game_overview.get('songs') or []], game_id, traverse_obj(
  392. game_overview, ('microdata', 'name'), (('twitter', 'fb'), 'title'), expected_type=str_or_none, get_all=False))
  393. class GameJoltCommunityIE(GameJoltPostListBaseIE):
  394. _VALID_URL = r'https?://(?:www\.)?gamejolt\.com/c/(?P<id>(?P<community>[\w-]+)(?:/(?P<channel>[\w-]+))?)(?:(?:\?|\#!?)(?:.*?[&;])??sort=(?P<sort>\w+))?'
  395. _TESTS = [{
  396. 'url': 'https://gamejolt.com/c/fnf/videos',
  397. 'playlist_mincount': 50,
  398. 'info_dict': {
  399. 'id': 'fnf/videos',
  400. 'title': 'Friday Night Funkin\' - Videos',
  401. 'description': 'md5:6d8c06f27460f7d35c1554757ffe53c8',
  402. },
  403. 'params': {
  404. 'playlistend': 50,
  405. 'ignore_no_formats_error': True,
  406. },
  407. 'expected_warnings': ['skipping format', 'No video formats found', 'Requested format is not available'],
  408. }, {
  409. 'url': 'https://gamejolt.com/c/youtubers',
  410. 'playlist_mincount': 50,
  411. 'info_dict': {
  412. 'id': 'youtubers/featured',
  413. 'title': 'Youtubers - featured',
  414. 'description': 'md5:53e5582c93dcc467ab597bfca4db17d4',
  415. },
  416. 'params': {
  417. 'playlistend': 50,
  418. 'ignore_no_formats_error': True,
  419. },
  420. 'expected_warnings': ['skipping format', 'No video formats found', 'Requested format is not available'],
  421. }]
  422. def _real_extract(self, url):
  423. display_id, community_id, channel_id, sort_by = self._match_valid_url(url).group('id', 'community', 'channel', 'sort')
  424. channel_id, sort_by = channel_id or 'featured', sort_by or 'new'
  425. community_data = self._call_api(
  426. f'web/communities/view/{community_id}', display_id,
  427. note='Downloading community info', errnote='Unable to download community info')['community']
  428. channel_data = traverse_obj(self._call_api(
  429. f'web/communities/view-channel/{community_id}/{channel_id}', display_id,
  430. note='Downloading channel info', errnote='Unable to download channel info', fatal=False), 'channel') or {}
  431. title = f'{community_data.get("name") or community_id} - {channel_data.get("display_title") or channel_id}'
  432. description = self._parse_content_as_text(
  433. self._parse_json(community_data.get('description_content') or '{}', display_id, fatal=False) or {})
  434. return self.playlist_result(
  435. self._entries(
  436. f'web/posts/fetch/community/{community_id}?channels[]={sort_by}&channels[]={channel_id}',
  437. display_id, 'Downloading community posts', 'Unable to download community posts'),
  438. f'{community_id}/{channel_id}', title, description)
  439. class GameJoltSearchIE(GameJoltPostListBaseIE):
  440. _VALID_URL = r'https?://(?:www\.)?gamejolt\.com/search(?:/(?P<filter>communities|users|games))?(?:\?|\#!?)(?:.*?[&;])??q=(?P<id>(?:[^&#]+)+)'
  441. _URL_FORMATS = {
  442. 'users': 'https://gamejolt.com/@{username}',
  443. 'communities': 'https://gamejolt.com/c/{path}',
  444. 'games': 'https://gamejolt.com/games/{slug}/{id}',
  445. }
  446. _TESTS = [{
  447. 'url': 'https://gamejolt.com/search?foo=bar&q=%23fnf',
  448. 'playlist_mincount': 50,
  449. 'info_dict': {
  450. 'id': '#fnf',
  451. 'title': '#fnf',
  452. },
  453. 'params': {
  454. 'playlistend': 50,
  455. 'ignore_no_formats_error': True,
  456. },
  457. 'expected_warnings': ['skipping format', 'No video formats found', 'Requested format is not available'],
  458. }, {
  459. 'url': 'https://gamejolt.com/search/communities?q=cookie%20run',
  460. 'playlist_mincount': 10,
  461. 'info_dict': {
  462. 'id': 'cookie run',
  463. 'title': 'cookie run',
  464. },
  465. }, {
  466. 'url': 'https://gamejolt.com/search/users?q=mlp',
  467. 'playlist_mincount': 278,
  468. 'info_dict': {
  469. 'id': 'mlp',
  470. 'title': 'mlp',
  471. },
  472. }, {
  473. 'url': 'https://gamejolt.com/search/games?q=roblox',
  474. 'playlist_mincount': 688,
  475. 'info_dict': {
  476. 'id': 'roblox',
  477. 'title': 'roblox',
  478. },
  479. }]
  480. def _search_entries(self, query, filter_mode, display_query):
  481. initial_search_data = self._call_api(
  482. f'web/search/{filter_mode}?q={query}', display_query,
  483. note=f'Downloading {filter_mode} list', errnote=f'Unable to download {filter_mode} list')
  484. entries_num = traverse_obj(initial_search_data, 'count', f'{filter_mode}Count')
  485. if not entries_num:
  486. return
  487. for page in range(1, math.ceil(entries_num / initial_search_data['perPage']) + 1):
  488. search_results = self._call_api(
  489. f'web/search/{filter_mode}?q={query}&page={page}', display_query,
  490. note=f'Downloading {filter_mode} list page {page}', errnote=f'Unable to download {filter_mode} list')
  491. for result in search_results[filter_mode]:
  492. yield self.url_result(self._URL_FORMATS[filter_mode].format(**result))
  493. def _real_extract(self, url):
  494. filter_mode, query = self._match_valid_url(url).group('filter', 'id')
  495. display_query = urllib.parse.unquote(query)
  496. return self.playlist_result(
  497. self._search_entries(query, filter_mode, display_query) if filter_mode else self._entries(
  498. f'web/posts/fetch/search/{query}', display_query, initial_items=self._call_api(
  499. f'web/search?q={query}', display_query,
  500. note='Downloading initial post list', errnote='Unable to download initial post list')['posts']),
  501. display_query, display_query)