@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 +21 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +1 -0
- package/package.json +47 -0
- package/readme.md +100 -0
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.
|
package/dist/index.d.ts
ADDED
|
@@ -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(/&/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(/&/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(/&/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
|
+
[](https://www.npmjs.com/package/@totallynotdavid/downloader)
|
|
4
|
+
[](https://github.com/totallynotdavid/downloader/actions/workflows/codeql.yml)
|
|
5
|
+
[](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
|