@totallynotdavid/downloader 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 David Duran
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,33 @@
1
+ type ResolveOptions = {
2
+ timeout?: number;
3
+ headers?: Record<string, string>;
4
+ };
5
+ type MediaItem = {
6
+ type: "image" | "video" | "audio";
7
+ url: string;
8
+ filename: string;
9
+ };
10
+ type MediaResult = {
11
+ urls: MediaItem[];
12
+ headers: Record<string, string>;
13
+ meta: {
14
+ title: string;
15
+ author: string;
16
+ platform: string;
17
+ views?: number;
18
+ likes?: number;
19
+ };
20
+ };
21
+ declare function resolve(url: string, options?: ResolveOptions): Promise<MediaResult>;
22
+ declare class PlatformNotSupportedError extends Error {
23
+ constructor(url: string);
24
+ }
25
+ declare class NetworkError extends Error {
26
+ statusCode?: number | undefined;
27
+ constructor(message: string, statusCode?: number | undefined);
28
+ }
29
+ declare class ParseError extends Error {
30
+ platform: string;
31
+ constructor(message: string, platform: string);
32
+ }
33
+ export { resolve, ResolveOptions, PlatformNotSupportedError, ParseError, NetworkError, MediaResult, MediaItem };
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ class R extends Error{constructor(o){super(`No extractor found for URL: ${o}`);this.name="PlatformNotSupportedError"}}class p extends Error{statusCode;constructor(o,i){super(o);this.statusCode=i;this.name="NetworkError"}}class r extends Error{platform;constructor(o,i){super(`[${i}] ${o}`);this.platform=i;this.name="ParseError"}}var $="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36",L=1e4;async function y(o,i={}){let e=new AbortController,n=setTimeout(()=>e.abort(),i.timeout??L);try{let s=await fetch(o,{headers:{"User-Agent":$,"Accept-Language":"en-US,en;q=0.9",...i.headers},signal:e.signal});if(!s.ok)throw new p(`HTTP ${s.status}: ${s.statusText}`,s.status);return s}catch(s){if(s.name==="AbortError")throw new p("Request timeout",408);if(s instanceof p)throw s;throw new p(s.message)}finally{clearTimeout(n)}}async function k(o,i,e={}){let n=new AbortController,s=setTimeout(()=>n.abort(),e.timeout??L);try{let t=await fetch(o,{method:"POST",headers:{"User-Agent":$,"Content-Type":i instanceof URLSearchParams?"application/x-www-form-urlencoded":"application/json",...e.headers},body:i.toString(),signal:n.signal});if(!t.ok)throw new p(`HTTP ${t.status}: ${t.statusText}`,t.status);return t}catch(t){if(t.name==="AbortError")throw new p("Request timeout",408);if(t instanceof p)throw t;throw new p(t.message)}finally{clearTimeout(s)}}var V="936619743392459",W="8845758582119845",D="Instagram 309.0.0.15.109 Android (31/12; 480dpi; 1080x2228; samsung; SM-G996B; t2s; qcom; en_US; 544099989)",H=/(?:p|reel|tv)\/([A-Za-z0-9_-]+)/;function j(o,i,e){let n=o.is_video,s=n?o.video_url:o.display_url;if(!s)return null;let t=e>0?`-${e}`:"";return{type:n?"video":"image",url:s,filename:`instagram-${i}${t}.${n?"mp4":"jpg"}`}}function z(o,i){let e=o.__typename;if(e==="GraphSidecar"||e==="XDTGraphSidecar")return(o.edge_sidecar_to_children?.edges||[]).map((m,l)=>m?.node&&j(m.node,i,l+1)).filter((m)=>m!==null);let s=j(o,i,0);return s?[s]:[]}async function O(o,i){let e=o.match(H);if(!e?.[1])throw new r("Could not parse post shortcode","instagram");let n=e[1],s=new URLSearchParams({doc_id:W,variables:JSON.stringify({shortcode:n})});try{let l=(await(await k("https://www.instagram.com/graphql/query",s,{headers:{"User-Agent":D,"Content-Type":"application/x-www-form-urlencoded","X-IG-App-ID":V,"X-IG-WWW-Claim":"0","X-Requested-With":"XMLHttpRequest",Accept:"*/*","Accept-Language":"en-US,en;q=0.9",...i.headers},timeout:i.timeout})).json())?.data?.xdt_shortcode_media;if(!l)throw new r("No media data found","instagram");let a=z(l,n);if(a.length===0)throw new r("No media found","instagram");let d=l.edge_media_to_caption?.edges?.[0]?.node?.text,f=l.owner?.username;return{urls:a,headers:{"User-Agent":D,Referer:"https://www.instagram.com/"},meta:{platform:"instagram",title:d||"Instagram post",author:f||"Unknown"}}}catch(t){if(t instanceof p||t instanceof r)throw t;throw new r(t.message,"instagram")}}var B=/<script id="__UNIVERSAL_DATA_FOR_REHYDRATION__" type="application\/json">([^<]+)<\/script>/;async function T(o,i){let e=o.replace("/photo/","/video/");try{let t=(await(await y(e,i)).text()).match(B);if(!t?.[1])throw new r("Could not find hydration data","tiktok");let l=JSON.parse(t[1]).__DEFAULT_SCOPE__?.["webapp.video-detail"],a=l?.itemInfo?.itemStruct;if(!a)throw new r("Metadata not found","tiktok");let d=[];if(a.imagePost?.images){if(a.imagePost.images.forEach((f,c)=>{let w=f.imageURL?.urlList?.[0];if(w)d.push({type:"image",url:w,filename:`tiktok-${a.id}-${c+1}.jpg`})}),a.music?.playUrl)d.push({type:"audio",url:a.music.playUrl,filename:`tiktok-${a.id}-audio.mp3`})}else if(a.video){let f=a.video.bitrateInfo||[],c=f.length>0?f[0].PlayAddr.UrlList[0]:a.video.playAddr;if(c)d.push({type:"video",url:c,filename:`tiktok-${a.id}.mp4`})}if(d.length===0)throw new r("No media content found","tiktok");return{urls:d,headers:{Referer:"https://www.tiktok.com/"},meta:{title:l.shareMeta?.desc||a.desc||"TikTok Post",author:a.author?.nickname||a.author?.uniqueId||"Unknown",platform:"tiktok",likes:a.stats?.diggCount,views:a.stats?.playCount}}}catch(n){if(n instanceof p||n instanceof r)throw n;throw new r(n.message,"tiktok")}}var C="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";function b(o){try{return o.replace(/\\u([\dA-Fa-f]{4})/g,(i,e)=>String.fromCharCode(Number.parseInt(e,16))).replace(/\\\//g,"/")}catch{return o}}function v(o,i,e){let n=o.indexOf(i);if(n===-1)return"";let s=n+i.length,t=o.indexOf(e,s);if(t===-1)return"";return o.slice(s,t)}function Y(o){let i=v(o,"\\\"video_id\\\":\\\"","\\\""),e=b(v(o,'"actors":[{"__typename":"User","name":"','","')),n=v(o,'"permalink_url"',"\\/Period>\\u003C\\/MPD>"),s=v(n,"AudioChannelConfiguration","BaseURL>\\u003C"),t=b(v(s,"BaseURL>","\\u003C\\/")),m={},l=n.split("FBQualityLabel=\\\"");for(let a=1;a<l.length;a++){let d=l[a];if(!d)continue;let f=d.split('"',1)[0];if(!f)continue;let c=d.split("BaseURL>",2)[1];if(!c)continue;let w=c.split("\\u003C\\/BaseURL>",1)[0];if(!w)continue;let u=b(w);if(u)m[f]=u}return{video_urls:m,audio_url:t,video_id:i,username:e}}function K(o){let i=v(o,'"__isNode":"Photo","id":"','"'),e=b(v(o,'"owner":{"__typename":"User","name":"','"'));return{photo_url:b(v(o,',"image":{"uri":"','","')),photo_id:i,username:e}}async function E(o,i){try{let n=await(await y(o,{headers:{"User-Agent":C,Accept:"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8","Accept-Language":"en-US,en;q=0.9","Sec-Fetch-Dest":"document","Sec-Fetch-Mode":"navigate","Sec-Fetch-Site":"none",...i.headers},timeout:i.timeout})).text(),s=n.includes("\\\"video_id\\\":\\\""),t=[];if(s){let{video_urls:d,audio_url:f,video_id:c,username:w}=Y(n);if(Object.keys(d).length===0)throw new r("No video URLs found","facebook");let u=Object.keys(d).map((h)=>Number.parseInt(h.replace(/\D/g,""),10)||0).reduce((h,U)=>Math.max(h,U),0),g=Object.keys(d).find((h)=>h.includes(String(u))),_=g?d[g]:Object.values(d)[0];if(!_)throw new r("Failed to select video URL","facebook");if(t.push({type:"video",url:_,filename:`fb-${c||Date.now()}.mp4`}),f)t.push({type:"audio",url:f,filename:`fb-${c||Date.now()}.m4a`});return{urls:t,headers:{"User-Agent":C},meta:{title:"Facebook Video",author:w||"Unknown",platform:"facebook"}}}let{photo_url:m,photo_id:l,username:a}=K(n);if(!m)throw new r("No photo URL found","facebook");return{urls:[{type:"image",url:m,filename:`fb-${l||Date.now()}.jpg`}],headers:{"User-Agent":C},meta:{title:"Facebook Photo",author:a||"Unknown",platform:"facebook"}}}catch(e){if(e instanceof p||e instanceof r)throw e;throw new r(e.message,"facebook")}}import Z from"node:path";var J="https://api.vxtwitter.com";async function x(o,i){try{let e=new URL(o),n=`${J}${e.pathname}`,t=await(await y(n,i)).json();if(!t?.media_extended||t.media_extended.length===0)throw new r("No media found in tweet","twitter");return{urls:t.media_extended.map((l,a)=>{let d=Z.extname(new URL(l.url).pathname)||".mp4";return{type:l.type==="video"||l.type==="gif"?"video":"image",url:l.url,filename:`twitter-${t.tweetID}-${a}${d}`}}),headers:{},meta:{title:t.text||"Twitter post",author:`${t.user_name} (@${t.user_screen_name})`,platform:"twitter",likes:t.likes,views:t.views}}}catch(e){if(e instanceof p||e instanceof r)throw e;throw new r(e.message,"twitter")}}var Q="https://www.youtube.com/youtubei/v1/player?key=AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39w",F={clientName:"ANDROID",clientVersion:"19.09.37",androidSdkVersion:30,hl:"en",gl:"US",timeZone:"UTC",utcOffsetMinutes:0},tt="com.google.android.youtube/19.09.37 (Linux; U; Android 11) gzip";function et(o){let i=[/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/shorts\/)([a-zA-Z0-9_-]{11})/,/youtube\.com\/embed\/([a-zA-Z0-9_-]{11})/];for(let e of i){let n=o.match(e);if(n?.[1])return n[1]}return null}function ot(o){return o.replace(/[<>:"/\\|?*]/g,"").trim()}async function A(o,i){try{let e=et(o);if(!e)throw new r("Could not extract video ID from URL","youtube");let t=await(await k(Q,JSON.stringify({videoId:e,context:{client:F},contentCheckOk:!0,racyCheckOk:!0}),{headers:{"Content-Type":"application/json","User-Agent":tt,"X-Youtube-Client-Name":"3","X-Youtube-Client-Version":F.clientVersion,...i.headers},timeout:i.timeout??15000})).json();if(t.playabilityStatus?.status!=="OK")throw new r(t.playabilityStatus?.reason||"Video not playable","youtube");if(!t.streamingData)throw new r("No streaming data available","youtube");let m=[...t.streamingData.formats||[],...t.streamingData.adaptiveFormats||[]],l=[],a=m.filter((u)=>u.url&&u.audioChannels&&u.audioChannels>0&&u.width&&u.width>0).sort((u,g)=>(g.width||0)-(u.width||0))[0];if(a?.url)l.push({type:"video",url:a.url,filename:`youtube-${e}.mp4`});let d=m.filter((u)=>u.url&&u.width&&u.width>0&&(!u.audioChannels||u.audioChannels===0)&&u.mimeType.includes("video/mp4")).sort((u,g)=>(g.width||0)-(u.width||0))[0],f=m.filter((u)=>u.url&&u.audioChannels&&u.audioChannels>0&&(!u.width||u.width===0)&&u.mimeType.includes("audio/mp4")).sort((u,g)=>g.bitrate-u.bitrate)[0];if(d?.url&&d.width&&d.width>(a?.width||0)){let u=d.mimeType.includes("webm")?"webm":"mp4";if(l.push({type:"video",url:d.url,filename:`youtube-${e}-video.${u}`}),f?.url)l.push({type:"audio",url:f.url,filename:`youtube-${e}-audio.m4a`})}if(l.length===0)throw new r("No downloadable formats found","youtube");let c=t.videoDetails?.title||"YouTube video",w=t.videoDetails?.author||"Unknown";return{urls:l,headers:{},meta:{title:ot(c),author:w,platform:"youtube",views:t.videoDetails?.viewCount?Number.parseInt(t.videoDetails.viewCount,10):void 0}}}catch(e){if(e instanceof p||e instanceof r)throw e;throw new r(e.message,"youtube")}}var rt=/\.(mp4|mkv|webm)$/i;async function I(o,i){try{let e=`${o.replace(/\/$/,"")}.json`,s=await(await y(e,i)).json();if(!(Array.isArray(s)&&s[0]?.data?.children?.[0]?.data))throw new r("Invalid Reddit API response","reddit");let t=s[0].data.children[0].data,m=[];if(t.is_gallery&&t.media_metadata){let l=t.gallery_data?.items||[];for(let a of l){let d=a.media_id,f=t.media_metadata[d];if(f?.s){let c=f.s.u||f.s.gif;if(c)m.push({type:"image",url:c.replace(/&amp;/g,"&"),filename:`reddit-${d}.jpg`})}}}else if(t.is_video&&t.media?.reddit_video?.fallback_url)m.push({type:"video",url:t.media.reddit_video.fallback_url.replace(/&amp;/g,"&"),filename:`reddit-${t.id}.mp4`});else if(t.url_overridden_by_dest||t.url){let l=(t.url_overridden_by_dest||t.url).replace(/&amp;/g,"&"),a=l.match(rt);m.push({type:a?"video":"image",url:l,filename:`reddit-${t.id}.${a?"mp4":"jpg"}`})}if(m.length===0)throw new r("No media found in post","reddit");return{urls:m,headers:{},meta:{title:t.title,author:t.author,platform:"reddit",likes:t.score,views:t.view_count}}}catch(e){if(e instanceof p||e instanceof r)throw e;throw new r(e.message,"reddit")}}import it from"node:path";var nt="546c25a59c58ad7",st=/^[a-zA-Z0-9]+$/;async function P(o,i){try{let n=new URL(o).pathname.split("/").filter(Boolean),s=n[n.length-1];if(!s)throw new r("Invalid Imgur URL: no path","imgur");let t=s.split(".")[0];if(!t)throw new r("Invalid Imgur URL: no ID found","imgur");let m=n.includes("gallery")||n.includes("a");if(m&&t.includes("-")){let g=t.split("-"),_=g[g.length-1];if(_&&_.length>=5&&st.test(_))t=_}let a=`https://api.imgur.com/3/${m?"album":"image"}/${t}?client_id=${nt}`,c=(await(await y(a,i)).json())?.data;if(!c)throw new r("Imgur API returned no data","imgur");let w=c.images||[c],u=[];for(let g of w)if(g.link){let _=it.extname(g.link)||".jpg";u.push({type:g.type?.startsWith("video")?"video":"image",url:g.link,filename:`imgur-${g.id}${_}`})}if(u.length===0)throw new r("No media links found","imgur");return{urls:u,headers:{},meta:{title:c.title||"Imgur Media",author:c.account_url||"Unknown",platform:"imgur",views:c.views,likes:(c.ups||0)-(c.downs||0)}}}catch(e){if(e instanceof p||e instanceof r)throw e;throw new r(e.message,"imgur")}}var at=/\/pin\/(?:[\w-]+--)?(\d+)/,X=["V_720P","V_EXP7","V_EXP6","V_EXP5","V_EXP4"];async function N(o,i){let e=o.match(at);if(!e?.[1])throw new r("Could not extract pin ID from URL","pinterest");let n=e[1];try{let s="https://www.pinterest.com/resource/PinResource/get/?data="+encodeURIComponent(JSON.stringify({options:{field_set_key:"unauth_react_main_pin",id:n}})),l=(await(await y(s,{headers:{"X-Pinterest-PWS-Handler":"www/[username].js",...i.headers},timeout:i.timeout})).json()).resource_response?.data;if(!l)throw new r("Invalid Pinterest API response","pinterest");let a=l.title||l.grid_title||"Pinterest Pin",d=l.closeup_attribution?.full_name||l.pinner?.full_name||l.pinner?.username||"Unknown",f=l.story_pin_data?.pages;if(f)for(let _ of f){let h=_.blocks;if(!h)continue;for(let U of h){let S=U.video?.video_list;if(!S)continue;for(let q of X){let M=S[q];if(M?.url&&!M.url.endsWith(".m3u8"))return{urls:[{type:"video",url:M.url,filename:`pinterest-${n}.mp4`}],headers:{},meta:{title:a,author:d,platform:"pinterest"}}}}}let c=l.videos?.video_list;if(c)for(let _ of X){let h=c[_];if(h?.url&&!h.url.endsWith(".m3u8"))return{urls:[{type:"video",url:h.url,filename:`pinterest-${n}.mp4`}],headers:{},meta:{title:a,author:d,platform:"pinterest"}}}let w=l.images;if(!w)throw new r("No media found in pin","pinterest");let u=w.orig?.url;if(!u){let _=0;for(let h of Object.values(w))if(h.width>_&&h.url)_=h.width,u=h.url}if(!u)throw new r("No image URL found","pinterest");let g=u.split(".").pop()?.split("?")[0]||"jpg";return{urls:[{type:"image",url:u,filename:`pinterest-${n}.${g}`}],headers:{},meta:{title:a,author:d,platform:"pinterest"}}}catch(s){if(s instanceof p||s instanceof r)throw s;throw new r(s.message,"pinterest")}}var ut=new Map([["instagram.com",O],["tiktok.com",T],["facebook.com",E],["fb.com",E],["twitter.com",x],["x.com",x],["youtube.com",A],["youtu.be",A],["reddit.com",I],["redd.it",I],["imgur.com",P],["i.imgur.com",P],["pinterest.com",N]]);function G(o){let i=new URL(o).hostname.replace(/^www\./,""),e=ut.get(i);if(e)return e;if(i.endsWith(".pinterest.com"))return N;return null}async function lt(o,i={}){let e=G(o);if(!e)throw new R(o);return e(o,i)}export{lt as resolve,R as PlatformNotSupportedError,r as ParseError,p as NetworkError};
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@totallynotdavid/downloader",
3
+ "version": "1.0.0",
4
+ "description": "Get direct download URLs from YouTube, Instagram, TikTok, X, and more.",
5
+ "keywords": [
6
+ "imgur",
7
+ "reddit",
8
+ "instagram",
9
+ "facebook",
10
+ "twitter",
11
+ "pinterest",
12
+ "tiktok",
13
+ "downloader"
14
+ ],
15
+ "license": "MIT",
16
+ "author": "David Duran <dadch1404@gmail.com>",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/totallynotdavid/downloader.git"
20
+ },
21
+ "bugs": {
22
+ "url": "https://github.com/totallynotdavid/downloader/issues"
23
+ },
24
+ "homepage": "https://github.com/totallynotdavid/downloader#readme",
25
+ "type": "module",
26
+ "main": "dist/index.js",
27
+ "types": "dist/index.d.ts",
28
+ "files": [
29
+ "dist",
30
+ "README.md",
31
+ "LICENSE"
32
+ ],
33
+ "scripts": {
34
+ "clean": "shx rm -rf dist",
35
+ "format": "biome format --write . && biome check --write . && bun x prettier \"**/*.{md,yml}\" --write --print-width=80 --prose-wrap=always",
36
+ "build": "bun run format && bun x bunup",
37
+ "test": "bun test",
38
+ "prepublishOnly": "bun run clean && bun run build"
39
+ },
40
+ "dependencies": {
41
+ "cheerio": "1.1.2"
42
+ },
43
+ "devDependencies": {
44
+ "@types/bun": "^1.3.3",
45
+ "shx": "^0.4.0"
46
+ }
47
+ }
package/readme.md ADDED
@@ -0,0 +1,100 @@
1
+ # [pkg]: @totallynotdavid/downloader
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@totallynotdavid/downloader.svg)](https://www.npmjs.com/package/@totallynotdavid/downloader)
4
+ [![CodeQL](https://github.com/totallynotdavid/downloader/actions/workflows/codeql.yml/badge.svg)](https://github.com/totallynotdavid/downloader/actions/workflows/codeql.yml)
5
+ [![Biome CI](https://github.com/totallynotdavid/downloader/actions/workflows/quality.yml/badge.svg)](https://github.com/totallynotdavid/downloader/actions/workflows/quality.yml)
6
+
7
+ Direct URLs from social posts. Skip reverse-engineering and heavy tools.
8
+ Supports Instagram, TikTok, Twitter/X, YouTube, Reddit, Facebook, Imgur, and
9
+ Pinterest.
10
+
11
+ ```sh
12
+ npm install @totallynotdavid/downloader
13
+ ```
14
+
15
+ ```typescript
16
+ import { resolve } from "@totallynotdavid/downloader";
17
+
18
+ const result = await resolve("https://www.instagram.com/p/ABC123/");
19
+ ```
20
+
21
+ `result.urls[0].url` is the direct media URL. `result.urls[0].filename` is a
22
+ suggested filename. `result.meta` contains post metadata like author and title.
23
+
24
+ Some platforms require headers to download. Pass `result.headers` when fetching:
25
+
26
+ ```typescript
27
+ const response = await fetch(result.urls[0].url, {
28
+ headers: result.headers,
29
+ });
30
+ ```
31
+
32
+ ## Reference
33
+
34
+ <details>
35
+ <summary>Options</summary>
36
+
37
+ ```typescript
38
+ await resolve(url, {
39
+ timeout: 15000,
40
+ headers: {
41
+ "User-Agent": "...",
42
+ },
43
+ });
44
+ ```
45
+
46
+ Default timeout is 10 seconds.
47
+
48
+ </details>
49
+
50
+ <details>
51
+ <summary>Errors</summary>
52
+
53
+ - `PlatformNotSupportedError`: URL hostname not recognized
54
+ - `NetworkError`: request failed (timeout, DNS, HTTP error)
55
+ - `ParseError`: platform response changed, extractor needs update
56
+
57
+ ```typescript
58
+ import {
59
+ resolve,
60
+ PlatformNotSupportedError,
61
+ NetworkError,
62
+ ParseError,
63
+ } from "@totallynotdavid/downloader";
64
+ ```
65
+
66
+ </details>
67
+
68
+ <details>
69
+ <summary>Types</summary>
70
+
71
+ ```typescript
72
+ type MediaResult = {
73
+ urls: MediaItem[];
74
+ headers: Record<string, string>;
75
+ meta: {
76
+ title: string;
77
+ author: string;
78
+ platform: string;
79
+ views?: number;
80
+ likes?: number;
81
+ };
82
+ };
83
+
84
+ type MediaItem = {
85
+ type: "image" | "video" | "audio";
86
+ url: string;
87
+ filename: string;
88
+ };
89
+
90
+ type ResolveOptions = {
91
+ timeout?: number;
92
+ headers?: Record<string, string>;
93
+ };
94
+ ```
95
+
96
+ </details>
97
+
98
+ ## License
99
+
100
+ MIT