@@ -85,6 +85,20 @@ def _content_url_from_json_ld(html: str) -> str:
8585 return ""
8686
8787
88+ def _ld_dimension (value : Any ) -> int :
89+ """Pixel size from a schema.org width/height (number, "1920", "1920 px",
90+ or a QuantitativeValue). Returns 0 when absent or unparseable."""
91+ if isinstance (value , dict ):
92+ value = value .get ("value" )
93+ if isinstance (value , (int , float )):
94+ return max (0 , int (value ))
95+ if isinstance (value , str ):
96+ match = re .match (r"\s*(\d+)" , value )
97+ if match :
98+ return int (match .group (1 ))
99+ return 0
100+
101+
88102class MixkitSource :
89103 """Mixkit video adapter. Satisfies `StockSource`."""
90104
@@ -193,6 +207,54 @@ def search(self, query: str, filters: SearchFilters) -> list[Candidate]:
193207 )
194208 )
195209
210+ if not out :
211+ out = self ._parse_json_ld (soup , query , filters )
212+ return out
213+
214+ def _parse_json_ld (self , soup , query : str , filters : SearchFilters ) -> list [Candidate ]:
215+ """Read results from the page's schema.org VideoObject entries.
216+
217+ Mixkit's current markup no longer exposes the card classes above, but
218+ every result is still described in JSON-LD with a direct contentUrl.
219+ """
220+ import json
221+
222+ out : list [Candidate ] = []
223+ for script in soup .select ('script[type="application/ld+json"]' ):
224+ try :
225+ data = json .loads (script .string or "" )
226+ except (ValueError , TypeError ):
227+ continue
228+ nodes = data .get ("@graph" , [data ]) if isinstance (data , dict ) else data
229+ for node in nodes if isinstance (nodes , list ) else []:
230+ if not isinstance (node , dict ) or node .get ("@type" ) != "VideoObject" :
231+ continue
232+ content_url = node .get ("contentUrl" ) or ""
233+ if not content_url .lower ().endswith (".mp4" ):
234+ continue
235+ href = (node .get ("@id" ) or "" ).split ("#" , 1 )[0 ]
236+ clip_id = href .rstrip ("/" ).rsplit ("-" , 1 )[- 1 ] if href else content_url
237+ out .append (
238+ Candidate (
239+ source = self .name ,
240+ source_id = f"mixkit_{ clip_id } " ,
241+ source_url = href or content_url ,
242+ download_url = content_url ,
243+ kind = "video" ,
244+ # Only trust declared dimensions; the "-1080" filename suffix
245+ # says nothing about orientation. 0 = unknown until probed.
246+ width = _ld_dimension (node .get ("width" )),
247+ height = _ld_dimension (node .get ("height" )),
248+ duration = 0.0 ,
249+ creator = "Mixkit" ,
250+ license = _LICENSE ,
251+ source_tags = f"{ node .get ('name' , '' )} { node .get ('description' , '' )} { query } " .strip (),
252+ thumbnail_url = node .get ("thumbnailUrl" , "" ),
253+ extra = {"detail_url" : content_url , "preview_url" : node .get ("embedUrl" , "" )},
254+ )
255+ )
256+ if len (out ) >= filters .per_page :
257+ return out
196258 return out
197259
198260 def download (self , candidate : Candidate , out_path : Path ) -> Path :
0 commit comments