@chilfish/gallery-dl-instagram 0.1.0 → 0.2.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.
@@ -0,0 +1,24 @@
1
+ import { access, mkdir, writeFile } from "node:fs/promises";
2
+ import { dirname } from "node:path";
3
+ //#region src/cli/storage.ts
4
+ function createStorage() {
5
+ return {
6
+ async exists(path) {
7
+ try {
8
+ await access(path);
9
+ return true;
10
+ } catch {
11
+ return false;
12
+ }
13
+ },
14
+ async write(path, data) {
15
+ await mkdir(dirname(path), { recursive: true });
16
+ await writeFile(path, data);
17
+ },
18
+ async mkdir(path) {
19
+ await mkdir(path, { recursive: true });
20
+ }
21
+ };
22
+ }
23
+ //#endregion
24
+ export { createStorage };
@@ -0,0 +1,24 @@
1
+ let node_fs_promises = require("node:fs/promises");
2
+ let node_path = require("node:path");
3
+ //#region src/cli/storage.ts
4
+ function createStorage() {
5
+ return {
6
+ async exists(path) {
7
+ try {
8
+ await (0, node_fs_promises.access)(path);
9
+ return true;
10
+ } catch {
11
+ return false;
12
+ }
13
+ },
14
+ async write(path, data) {
15
+ await (0, node_fs_promises.mkdir)((0, node_path.dirname)(path), { recursive: true });
16
+ await (0, node_fs_promises.writeFile)(path, data);
17
+ },
18
+ async mkdir(path) {
19
+ await (0, node_fs_promises.mkdir)(path, { recursive: true });
20
+ }
21
+ };
22
+ }
23
+ //#endregion
24
+ exports.createStorage = createStorage;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@chilfish/gallery-dl-instagram",
3
3
  "type": "module",
4
- "version": "0.1.0",
4
+ "version": "0.2.0",
5
5
  "description": "Instagram extraction pipeline — platform-agnostic SDK + CLI",
6
6
  "license": "GPL-2.0-only",
7
7
  "keywords": [
@@ -11,28 +11,28 @@
11
11
  "gallery-dl",
12
12
  "cli"
13
13
  ],
14
+ "sideEffects": false,
14
15
  "exports": {
15
- ".": "./dist/index.mjs",
16
- "./cli": "./dist/cli/index.mjs",
17
- "./sdk": "./dist/sdk.mjs",
16
+ ".": {
17
+ "import": "./dist/index.mjs",
18
+ "require": "./dist/index.cjs"
19
+ },
20
+ "./node": {
21
+ "import": "./dist/node.mjs",
22
+ "require": "./dist/node.cjs"
23
+ },
18
24
  "./package.json": "./package.json"
19
25
  },
20
- "types": "./dist/index.d.mts",
26
+ "main": "./dist/index.cjs",
27
+ "module": "./dist/index.mjs",
28
+ "types": "./dist/index.d.cts",
21
29
  "bin": {
22
- "gallery-dl-instagram": "./dist/cli/index.mjs"
30
+ "dl-ins": "./dist/dl-ins.mjs"
23
31
  },
24
32
  "files": [
25
- "!*.log",
26
- "!node_modules",
27
- "cli/",
28
- "config.ts",
29
- "core/",
30
- "dist/",
31
- "index.ts",
32
- "instagram/",
33
- "message.ts",
34
- "types.ts",
35
- "utils/"
33
+ "LICENSE",
34
+ "README.md",
35
+ "dist"
36
36
  ],
37
37
  "engines": {
38
38
  "node": ">=18"
@@ -40,7 +40,7 @@
40
40
  "scripts": {
41
41
  "build": "tsdown",
42
42
  "typecheck": "tsc --noEmit -p tsconfig.json",
43
- "cli": "bun cli/index.ts",
43
+ "cli": "bun src/cli/index.ts",
44
44
  "lint": "eslint . --fix",
45
45
  "test": "vitest run",
46
46
  "test:watch": "vitest",
@@ -48,21 +48,28 @@
48
48
  "test:unit": "vitest run tests/unit",
49
49
  "test:integration": "vitest run tests/integration"
50
50
  },
51
- "dependencies": {
52
- "axios": "^1.16.1"
51
+ "peerDependencies": {
52
+ "axios": "^1.0.0"
53
53
  },
54
+ "peerDependenciesMeta": {
55
+ "axios": {
56
+ "optional": true
57
+ }
58
+ },
59
+ "dependencies": {},
54
60
  "devDependencies": {
55
61
  "@antfu/eslint-config": "^9.0.0",
56
62
  "@types/node": "^25.9.1",
57
- "commander": "^14.0.3",
63
+ "axios": "^1.16.1",
64
+ "commander": "^15.0.0",
58
65
  "dotenv": "^17.4.2",
59
- "eslint": "^10.4.0",
60
- "lefthook": "^2.1.8",
61
- "tsdown": "^0.22.0",
66
+ "eslint": "^10.4.1",
67
+ "lefthook": "^2.1.9",
68
+ "tsdown": "^0.22.1",
62
69
  "typescript": "^6.0.3",
63
70
  "vitest": "^4.1.7"
64
71
  },
65
72
  "inlinedDependencies": {
66
- "commander": "14.0.3"
73
+ "commander": "15.0.0"
67
74
  }
68
75
  }
package/cli/adapter.ts DELETED
@@ -1,284 +0,0 @@
1
- /**
2
- * Node.js adapter — HttpClient + Storage + Logger implementations.
3
- *
4
- * These bind the platform-agnostic SDK interfaces to Node.js primitives:
5
- * axios for HTTP, fs/promises for file I/O, console for logging.
6
- */
7
-
8
- import type { AxiosInstance } from 'axios'
9
- import type { Logger } from '../core/extractor'
10
- import type { HttpClient, HttpResponse, Storage } from '../types'
11
- import { access, mkdir, writeFile } from 'node:fs/promises'
12
- import { dirname } from 'node:path'
13
- import axios from 'axios'
14
- import { createCookieJar } from './cookies'
15
-
16
- /** NodeHttpClient — axios wrapper */
17
-
18
- /**
19
- * Extract csrftoken value from a Cookie header string.
20
- */
21
- export function extractCsrfFromCookies(cookies: string): string {
22
- const m = cookies.match(/(?:^|;\s*)csrftoken=([^;]+)/)
23
- return m?.[1] ?? ''
24
- }
25
-
26
- export function createHttpClient(
27
- sessionId?: string,
28
- fullCookies?: string,
29
- logger?: Logger,
30
- ): HttpClient {
31
- const instance: AxiosInstance = axios.create({
32
- timeout: 30000,
33
- maxRedirects: 20,
34
- validateStatus: () => true, // don't throw on non-2xx
35
- headers: {
36
- 'User-Agent':
37
- 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
38
- },
39
- })
40
-
41
- // Build base Cookie string
42
- // Priority: full cookies string > sessionid-only
43
- const baseCookie = fullCookies
44
- || (sessionId ? `sessionid=${sessionId}` : null)
45
-
46
- return {
47
- async request<T = unknown>(config: {
48
- url: string
49
- method?: string
50
- headers?: Record<string, string>
51
- params?: Record<string, string | number | null | undefined>
52
- data?: unknown
53
- signal?: AbortSignal
54
- timeout?: number
55
- responseType?: 'arraybuffer' | 'text' | 'json'
56
- }): Promise<HttpResponse<T>> {
57
- const method = config.method ?? 'GET'
58
- logger?.debug(`${method} ${config.url}`)
59
-
60
- // Merge base cookie with per-request headers (Cookie values are joined)
61
- const mergedHeaders: Record<string, string> = {}
62
- if (baseCookie) {
63
- mergedHeaders.Cookie = baseCookie
64
- }
65
- if (config.headers) {
66
- for (const [k, v] of Object.entries(config.headers)) {
67
- if (k.toLowerCase() === 'cookie' && mergedHeaders.Cookie) {
68
- // Append instead of overriding: sessionid=xxx + csrftoken=yyy → sessionid=xxx; csrftoken=yyy
69
- mergedHeaders.Cookie = `${mergedHeaders.Cookie}; ${v}`
70
- }
71
- else {
72
- mergedHeaders[k] = v
73
- }
74
- }
75
- }
76
-
77
- if (mergedHeaders.Cookie) {
78
- logger?.debug(` Cookie: ${mergedHeaders.Cookie.slice(0, 200)}`)
79
- }
80
-
81
- try {
82
- const resp = await instance.request<T>({
83
- url: config.url,
84
- method,
85
- headers: mergedHeaders,
86
- params: cleanupParams(config.params),
87
- data: config.data,
88
- signal: config.signal,
89
- timeout: config.timeout,
90
- responseType: config.responseType ?? 'json',
91
- })
92
-
93
- const finalUrl = resp.request?.res?.responseUrl ?? config.url
94
- logger?.debug(` ← ${resp.status} ${resp.status >= 400 ? '⚠️' : ''} (${finalUrl.slice(0, 100)})`)
95
-
96
- return {
97
- status: resp.status,
98
- data: resp.data,
99
- headers: resp.headers as Record<string, string>,
100
- url: finalUrl,
101
- }
102
- }
103
- catch (err) {
104
- const msg = String(err)
105
- if (msg.includes('TOO_MANY_REDIRECTS') || msg.includes('too many redirects')) {
106
- throw new Error(
107
- 'Too many redirects — sessionid may be expired or invalid. '
108
- + 'Export a fresh sessionid from your browser.',
109
- )
110
- }
111
- throw err
112
- }
113
- },
114
- }
115
- }
116
-
117
- /** WebClient — anonymous cookie-jar HTTP client */
118
-
119
- /**
120
- * Create an HTTP client with an in-memory cookie jar.
121
- *
122
- * Use this when you don't have a sessionid — the client first seeds its
123
- * cookie jar by visiting ``instagram.com``, then uses those anonymous
124
- * cookies for subsequent API calls. This is how incognito browsing works.
125
- *
126
- * Returns the client + the initial CSRF token extracted from cookies.
127
- */
128
- export async function createWebClient(
129
- logger?: Logger,
130
- ): Promise<{ http: HttpClient, csrfToken: string }> {
131
- const jar = createCookieJar()
132
-
133
- // Seed the cookie jar by visiting Instagram's homepage
134
- logger?.info('Seeding anonymous session (visiting instagram.com)…')
135
- const seedResp = await axios.get('https://www.instagram.com/', {
136
- headers: {
137
- 'User-Agent':
138
- 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
139
- 'Accept':
140
- 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
141
- },
142
- maxRedirects: 20,
143
- validateStatus: () => true,
144
- })
145
- jar.setFromResponse(seedResp.headers as Record<string, string>)
146
- logger?.debug(` ← ${seedResp.status} — got ${jar.getCookieHeader().split(';').length} cookies`)
147
-
148
- // Extract csrftoken from jar
149
- const cookieStr = jar.getCookieHeader()
150
- const csrfMatch = cookieStr.match(/(?:^|;\s*)csrftoken=([^;]+)/)
151
- const csrfToken = csrfMatch?.[1] ?? ''
152
-
153
- // Build the wrapper
154
- const http: HttpClient = {
155
- async request<T = unknown>(config: {
156
- url: string
157
- method?: string
158
- headers?: Record<string, string>
159
- params?: Record<string, string | number | null | undefined>
160
- data?: unknown
161
- signal?: AbortSignal
162
- timeout?: number
163
- responseType?: 'arraybuffer' | 'text' | 'json'
164
- }): Promise<HttpResponse<T>> {
165
- const method = config.method ?? 'GET'
166
- logger?.debug(`${method} ${config.url}`)
167
-
168
- // Merge jar cookies with per-request headers (Cookie values are joined)
169
- const jarCookie = jar.getCookieHeader()
170
- const mergedHeaders: Record<string, string> = {}
171
- if (jarCookie) {
172
- mergedHeaders.Cookie = jarCookie
173
- }
174
- if (config.headers) {
175
- for (const [k, v] of Object.entries(config.headers)) {
176
- if (k.toLowerCase() === 'cookie' && mergedHeaders.Cookie) {
177
- mergedHeaders.Cookie = `${mergedHeaders.Cookie}; ${v}`
178
- }
179
- else {
180
- mergedHeaders[k] = v
181
- }
182
- }
183
- }
184
-
185
- try {
186
- const resp = await axios.request<T>({
187
- url: config.url,
188
- method,
189
- headers: mergedHeaders,
190
- params: cleanupParams(config.params),
191
- data: config.data,
192
- signal: config.signal,
193
- timeout: config.timeout ?? 30000,
194
- maxRedirects: 20,
195
- validateStatus: () => true,
196
- responseType: config.responseType ?? 'json',
197
- })
198
-
199
- // Update jar with response cookies
200
- jar.setFromResponse(resp.headers as Record<string, string>)
201
-
202
- const finalUrl = resp.request?.res?.responseUrl ?? config.url
203
- logger?.debug(` ← ${resp.status} ${resp.status >= 400 ? '⚠️' : ''} (${finalUrl.slice(0, 100)})`)
204
-
205
- return {
206
- status: resp.status,
207
- data: resp.data,
208
- headers: resp.headers as Record<string, string>,
209
- url: finalUrl,
210
- }
211
- }
212
- catch (err) {
213
- const msg = String(err)
214
- if (msg.includes('TOO_MANY_REDIRECTS') || msg.includes('too many redirects')) {
215
- throw new Error(
216
- 'Too many redirects — Instagram may be blocking the request. Try again later or use --sessionid.',
217
- )
218
- }
219
- throw err
220
- }
221
- },
222
- }
223
-
224
- return { http, csrfToken }
225
- }
226
-
227
- function cleanupParams(
228
- params?: Record<string, string | number | null | undefined>,
229
- ): Record<string, string> | undefined {
230
- if (!params)
231
- return undefined
232
- const cleaned: Record<string, string> = {}
233
- for (const [k, v] of Object.entries(params)) {
234
- if (v != null)
235
- cleaned[k] = String(v)
236
- }
237
- return cleaned
238
- }
239
-
240
- /** NodeStorage — fs/promises wrapper */
241
-
242
- export function createStorage(): Storage {
243
- return {
244
- async exists(path: string): Promise<boolean> {
245
- try {
246
- await access(path)
247
- return true
248
- }
249
- catch {
250
- return false
251
- }
252
- },
253
-
254
- async write(path: string, data: Uint8Array | string): Promise<void> {
255
- // Ensure parent directory
256
- await mkdir(dirname(path), { recursive: true })
257
- await writeFile(path, data)
258
- },
259
-
260
- async mkdir(path: string): Promise<void> {
261
- await mkdir(path, { recursive: true })
262
- },
263
- }
264
- }
265
-
266
- /** NodeLogger — console wrapper */
267
-
268
- export function createLogger(verbose: boolean): Logger {
269
- return {
270
- debug(message: string, ...args: unknown[]): void {
271
- if (verbose)
272
- console.debug(`[debug] ${message}`, ...args)
273
- },
274
- info(message: string, ...args: unknown[]): void {
275
- console.info(`[info] ${message}`, ...args)
276
- },
277
- warn(message: string, ...args: unknown[]): void {
278
- console.warn(`[warn] ${message}`, ...args)
279
- },
280
- error(message: string, ...args: unknown[]): void {
281
- console.error(`[error] ${message}`, ...args)
282
- },
283
- }
284
- }
package/cli/cookies.ts DELETED
@@ -1,59 +0,0 @@
1
- /**
2
- * Simple in-memory cookie jar for Node.js HTTP clients.
3
- *
4
- * Tracks Set-Cookie headers across requests and injects stored cookies
5
- * into subsequent requests to the same domain. This is enough for
6
- * Instagram's anonymous session flow: visit homepage → get cookies →
7
- * use those cookies for API calls.
8
- */
9
-
10
- export interface CookieJar {
11
- /** Record cookies from a response's Set-Cookie header(s). */
12
- setFromResponse: (headers: Record<string, string>) => void
13
- /** Get the Cookie header value for the next request. */
14
- getCookieHeader: () => string
15
- }
16
-
17
- export function createCookieJar(): CookieJar {
18
- const cookies = new Map<string, string>()
19
- let cookieString = ''
20
-
21
- return {
22
- setFromResponse(headers: Record<string, string>): void {
23
- const raw = headers['set-cookie']
24
- if (!raw)
25
- return
26
-
27
- const cookieHeaders = Array.isArray(raw) ? raw : [raw]
28
- for (const header of cookieHeaders) {
29
- const parts = header.split(';')
30
- if (parts.length === 0)
31
- continue
32
-
33
- const [nameValue] = parts
34
- const eqIdx = nameValue!.indexOf('=')
35
- if (eqIdx <= 0)
36
- continue
37
-
38
- const name = nameValue!.slice(0, eqIdx).trim()
39
- const value = nameValue!.slice(eqIdx + 1).trim()
40
-
41
- // Skip expiration by checking Max-Age=0 or Expires=epoch
42
- const rest = parts.slice(1).map((s: string) => s.trim().toLowerCase())
43
- if (rest.includes('max-age=0'))
44
- continue
45
-
46
- cookies.set(name, value)
47
- }
48
-
49
- // Rebuild cookie string
50
- cookieString = Array.from(cookies.entries())
51
- .map(([k, v]) => `${k}=${v}`)
52
- .join('; ')
53
- },
54
-
55
- getCookieHeader(): string {
56
- return cookieString
57
- },
58
- }
59
- }