@chilfish/gallery-dl-instagram 0.2.3 → 0.2.4

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/dist/dl-ins.mjs CHANGED
@@ -2968,7 +2968,7 @@ function useColor() {
2968
2968
  new Command();
2969
2969
  //#endregion
2970
2970
  //#region package.json
2971
- var version = "0.2.3";
2971
+ var version = "0.2.4";
2972
2972
  //#endregion
2973
2973
  //#region src/utils/id-codec.ts
2974
2974
  /**
@@ -3643,9 +3643,38 @@ function parseStoryRest(post, cfg) {
3643
3643
  const item = items[num];
3644
3644
  const media = parseMediaItem(item, post, cfg, num + 1);
3645
3645
  if (!media) continue;
3646
- extractTaggedUsers(item, media);
3646
+ const itemRec = item;
3647
+ extractTaggedUsers(itemRec, media);
3648
+ const musicStickers = itemRec.story_music_stickers;
3649
+ if (musicStickers?.[0]) {
3650
+ const audio = extractAudio(itemRec, data, musicStickers[0], cfg);
3651
+ if (audio) {
3652
+ audio.num = num + 1;
3653
+ if (musicStickers[0].attribution) audio.music_attribution = musicStickers[0].attribution;
3654
+ if (musicStickers[0].display_type) audio.music_display_type = musicStickers[0].display_type;
3655
+ data._files.push(audio);
3656
+ }
3657
+ }
3658
+ const linkStickers = itemRec.story_link_stickers;
3659
+ if (linkStickers?.[0]) {
3660
+ const sl = linkStickers[0].story_link;
3661
+ media.story_link_url = sl.url;
3662
+ media.story_link_display = sl.display_url;
3663
+ media.story_link_title = sl.link_title;
3664
+ media.story_link_type = sl.link_type;
3665
+ }
3647
3666
  data._files.push(media);
3648
3667
  }
3668
+ if (post.music_metadata) {
3669
+ const info = post.music_metadata.music_info;
3670
+ if (info) {
3671
+ const audio = extractAudio(post, data, info, cfg);
3672
+ if (audio) {
3673
+ audio.num = items.length;
3674
+ data._files.push(audio);
3675
+ }
3676
+ }
3677
+ }
3649
3678
  return data;
3650
3679
  }
3651
3680
  /** Parse a single media item (image/video) from a carousel or story. */
@@ -3988,7 +4017,14 @@ var InstagramExtractor = class extends Extractor {
3988
4017
  yield url(file.audio_url, combined);
3989
4018
  }
3990
4019
  if (previewsAud) combined.media_id = `${combined.media_id}p`;
3991
- else continue;
4020
+ if (!audio && !previewsAud) {
4021
+ const coverUrl = file.display_url;
4022
+ if (coverUrl) {
4023
+ nameExtFromURL(coverUrl, combined);
4024
+ yield url(coverUrl, combined);
4025
+ }
4026
+ continue;
4027
+ }
3992
4028
  }
3993
4029
  if (file.video_url) {
3994
4030
  if (shouldDownloadVideos) {
@@ -4512,9 +4548,9 @@ var DownloadJob = class DownloadJob extends Job {
4512
4548
  ...msg.metadata
4513
4549
  };
4514
4550
  const extrClass = meta._extractor;
4515
- if (!extrClass || typeof extrClass !== "object") return;
4551
+ if (!extrClass || typeof extrClass !== "function") return;
4516
4552
  const cls = extrClass;
4517
- const match = cls.pattern.exec(msg.url);
4553
+ const match = cls.pattern.exec(msg.url) ?? cls.pattern.exec(msg.url.replace(/\/$/, ""));
4518
4554
  if (!match) return;
4519
4555
  const parentExtr = this.extractor;
4520
4556
  const childJob = new DownloadJob(Reflect.construct(cls, [{
@@ -4589,7 +4625,12 @@ var PrintJob = class PrintJob extends Job {
4589
4625
  audioDuration: meta.audio_duration ?? void 0,
4590
4626
  audioHasLyrics: meta.audio_has_lyrics ?? void 0,
4591
4627
  audioIsExplicit: meta.audio_is_explicit ?? void 0,
4592
- coverArtworkUri: meta.audio_cover_artwork_uri ?? meta.audio_cover_artwork_thumbnail_uri ?? void 0
4628
+ coverArtworkUri: meta.audio_cover_artwork_uri ?? meta.audio_cover_artwork_thumbnail_uri ?? void 0,
4629
+ storyLinkUrl: meta.story_link_url ?? void 0,
4630
+ storyLinkDisplay: meta.story_link_display ?? void 0,
4631
+ storyLinkTitle: meta.story_link_title ?? void 0,
4632
+ storyLinkType: meta.story_link_type ?? void 0,
4633
+ musicAttribution: meta.music_attribution ?? void 0
4593
4634
  });
4594
4635
  }
4595
4636
  async handleQueue(msg) {
@@ -4600,9 +4641,9 @@ var PrintJob = class PrintJob extends Job {
4600
4641
  ...this._currentDir,
4601
4642
  ...msg.metadata
4602
4643
  }._extractor;
4603
- if (!extrClass || typeof extrClass !== "object") return;
4644
+ if (!extrClass || typeof extrClass !== "function") return;
4604
4645
  const cls = extrClass;
4605
- const match = cls.pattern.exec(msg.url);
4646
+ const match = cls.pattern.exec(msg.url) ?? cls.pattern.exec(msg.url.replace(/\/$/, ""));
4606
4647
  if (!match) return;
4607
4648
  const parentExtr = this.extractor;
4608
4649
  const childJob = new PrintJob(Reflect.construct(cls, [{
@@ -4641,6 +4682,28 @@ var PrintJob = class PrintJob extends Job {
4641
4682
  row("Type:", `${m.type ?? "?"} (${this._files.length} files)`);
4642
4683
  row("URL:", m.post_url ?? "?");
4643
4684
  const desc = m.description ?? "";
4685
+ const isPrivate = m.is_private;
4686
+ const isVerified = m.is_verified;
4687
+ const followerCount = m.follower_count;
4688
+ const followingCount = m.following_count;
4689
+ const mediaCount = m.media_count;
4690
+ const bio = m.biography ?? "";
4691
+ const externalUrl = m.external_url;
4692
+ if (isPrivate !== void 0 || isVerified !== void 0 || followerCount !== void 0) {
4693
+ const badges = [];
4694
+ if (isPrivate) badges.push("🔒 Private");
4695
+ if (isVerified) badges.push("✅ Verified");
4696
+ if (followerCount !== void 0) badges.push(`${followerCount.toLocaleString()} followers`);
4697
+ if (followingCount !== void 0) badges.push(`${followingCount.toLocaleString()} following`);
4698
+ if (mediaCount !== void 0) badges.push(`${mediaCount.toLocaleString()} posts`);
4699
+ row("Profile:", badges.join(" · "));
4700
+ }
4701
+ if (bio) {
4702
+ process.stdout.write(` ${dim("│")}\n`);
4703
+ process.stdout.write(` ${dim("│")} ${b("Bio:")}\n`);
4704
+ for (const line of bio.split("\n")) for (const wl of this._wrap(line, w - 8)) process.stdout.write(` ${dim("│")} ${dim(wl)}\n`);
4705
+ }
4706
+ if (externalUrl) row("Website:", externalUrl);
4644
4707
  if (desc) {
4645
4708
  process.stdout.write(` ${dim("│")}\n`);
4646
4709
  process.stdout.write(` ${dim("│")} ${b("Description:")}\n`);
@@ -4685,6 +4748,16 @@ var PrintJob = class PrintJob extends Job {
4685
4748
  process.stdout.write(`${line}\n`);
4686
4749
  }
4687
4750
  }
4751
+ const linkFiles = this._files.filter((f) => f.storyLinkUrl);
4752
+ if (linkFiles.length > 0) {
4753
+ process.stdout.write(` ${dim("│")}\n`);
4754
+ process.stdout.write(` ${dim("│")} ${b("Link:")}\n`);
4755
+ for (const lf of linkFiles) {
4756
+ if (lf.storyLinkDisplay) process.stdout.write(` ${dim("│")} ${g("🔗")} ${lf.storyLinkDisplay}\n`);
4757
+ if (lf.storyLinkTitle && lf.storyLinkTitle !== "Visit Link") process.stdout.write(` ${dim("│")} ${dim("Title:")} ${lf.storyLinkTitle}\n`);
4758
+ if (lf.storyLinkType) process.stdout.write(` ${dim("│")} ${dim("Type:")} ${lf.storyLinkType}\n`);
4759
+ }
4760
+ }
4688
4761
  const audioFiles = this._files.filter((f) => f.audioUrl);
4689
4762
  if (audioFiles.length > 0) {
4690
4763
  process.stdout.write(` ${dim("│")}\n`);
@@ -4694,6 +4767,7 @@ var PrintJob = class PrintJob extends Job {
4694
4767
  const title = af.audioArtist ? `${af.audioTitle} — ${af.audioArtist}` : af.audioTitle;
4695
4768
  process.stdout.write(` ${dim("│")} ${g("♪")} ${title}\n`);
4696
4769
  }
4770
+ if (af.musicAttribution) process.stdout.write(` ${dim("│")} ${dim("via:")} ${af.musicAttribution}\n`);
4697
4771
  if (af.audioDuration) {
4698
4772
  const mins = Math.floor(af.audioDuration / 60);
4699
4773
  const secs = Math.round(af.audioDuration % 60);
@@ -4975,6 +5049,11 @@ function resolveExtractor(url) {
4975
5049
  throw new Error(`No extractor matched URL: ${url}. Supported: /p/, /reel/, /{user}/, /stories/, /highlights/, /explore/tags/, /saved/`);
4976
5050
  }
4977
5051
  async function runExtractor(url, extrClass, opts) {
5052
+ if (opts.info && extrClass === InstagramUserExtractor) {
5053
+ const parts = (opts.include ?? "info").split(",").map((s) => s.trim());
5054
+ if (!parts.includes("info")) parts.push("info");
5055
+ opts.include = parts.join(",");
5056
+ }
4978
5057
  const config = buildConfig(opts);
4979
5058
  const log = createLogger(opts.verbose ?? false);
4980
5059
  let http;
package/dist/index.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_sdk = require("./sdk-D8q2Rjw2.cjs");
2
+ const require_sdk = require("./sdk-B9mRv7JE.cjs");
3
3
  //#region src/core/print-job.ts
4
4
  var PrintJob = class PrintJob extends require_sdk.Job {
5
5
  _currentDir = {};
@@ -37,7 +37,12 @@ var PrintJob = class PrintJob extends require_sdk.Job {
37
37
  audioDuration: meta.audio_duration ?? void 0,
38
38
  audioHasLyrics: meta.audio_has_lyrics ?? void 0,
39
39
  audioIsExplicit: meta.audio_is_explicit ?? void 0,
40
- coverArtworkUri: meta.audio_cover_artwork_uri ?? meta.audio_cover_artwork_thumbnail_uri ?? void 0
40
+ coverArtworkUri: meta.audio_cover_artwork_uri ?? meta.audio_cover_artwork_thumbnail_uri ?? void 0,
41
+ storyLinkUrl: meta.story_link_url ?? void 0,
42
+ storyLinkDisplay: meta.story_link_display ?? void 0,
43
+ storyLinkTitle: meta.story_link_title ?? void 0,
44
+ storyLinkType: meta.story_link_type ?? void 0,
45
+ musicAttribution: meta.music_attribution ?? void 0
41
46
  });
42
47
  }
43
48
  async handleQueue(msg) {
@@ -48,9 +53,9 @@ var PrintJob = class PrintJob extends require_sdk.Job {
48
53
  ...this._currentDir,
49
54
  ...msg.metadata
50
55
  }._extractor;
51
- if (!extrClass || typeof extrClass !== "object") return;
56
+ if (!extrClass || typeof extrClass !== "function") return;
52
57
  const cls = extrClass;
53
- const match = cls.pattern.exec(msg.url);
58
+ const match = cls.pattern.exec(msg.url) ?? cls.pattern.exec(msg.url.replace(/\/$/, ""));
54
59
  if (!match) return;
55
60
  const parentExtr = this.extractor;
56
61
  const childJob = new PrintJob(Reflect.construct(cls, [{
@@ -89,6 +94,28 @@ var PrintJob = class PrintJob extends require_sdk.Job {
89
94
  row("Type:", `${m.type ?? "?"} (${this._files.length} files)`);
90
95
  row("URL:", m.post_url ?? "?");
91
96
  const desc = m.description ?? "";
97
+ const isPrivate = m.is_private;
98
+ const isVerified = m.is_verified;
99
+ const followerCount = m.follower_count;
100
+ const followingCount = m.following_count;
101
+ const mediaCount = m.media_count;
102
+ const bio = m.biography ?? "";
103
+ const externalUrl = m.external_url;
104
+ if (isPrivate !== void 0 || isVerified !== void 0 || followerCount !== void 0) {
105
+ const badges = [];
106
+ if (isPrivate) badges.push("🔒 Private");
107
+ if (isVerified) badges.push("✅ Verified");
108
+ if (followerCount !== void 0) badges.push(`${followerCount.toLocaleString()} followers`);
109
+ if (followingCount !== void 0) badges.push(`${followingCount.toLocaleString()} following`);
110
+ if (mediaCount !== void 0) badges.push(`${mediaCount.toLocaleString()} posts`);
111
+ row("Profile:", badges.join(" · "));
112
+ }
113
+ if (bio) {
114
+ process.stdout.write(` ${require_sdk.dim("│")}\n`);
115
+ process.stdout.write(` ${require_sdk.dim("│")} ${require_sdk.b("Bio:")}\n`);
116
+ for (const line of bio.split("\n")) for (const wl of this._wrap(line, w - 8)) process.stdout.write(` ${require_sdk.dim("│")} ${require_sdk.dim(wl)}\n`);
117
+ }
118
+ if (externalUrl) row("Website:", externalUrl);
92
119
  if (desc) {
93
120
  process.stdout.write(` ${require_sdk.dim("│")}\n`);
94
121
  process.stdout.write(` ${require_sdk.dim("│")} ${require_sdk.b("Description:")}\n`);
@@ -133,6 +160,16 @@ var PrintJob = class PrintJob extends require_sdk.Job {
133
160
  process.stdout.write(`${line}\n`);
134
161
  }
135
162
  }
163
+ const linkFiles = this._files.filter((f) => f.storyLinkUrl);
164
+ if (linkFiles.length > 0) {
165
+ process.stdout.write(` ${require_sdk.dim("│")}\n`);
166
+ process.stdout.write(` ${require_sdk.dim("│")} ${require_sdk.b("Link:")}\n`);
167
+ for (const lf of linkFiles) {
168
+ if (lf.storyLinkDisplay) process.stdout.write(` ${require_sdk.dim("│")} ${require_sdk.g("🔗")} ${lf.storyLinkDisplay}\n`);
169
+ if (lf.storyLinkTitle && lf.storyLinkTitle !== "Visit Link") process.stdout.write(` ${require_sdk.dim("│")} ${require_sdk.dim("Title:")} ${lf.storyLinkTitle}\n`);
170
+ if (lf.storyLinkType) process.stdout.write(` ${require_sdk.dim("│")} ${require_sdk.dim("Type:")} ${lf.storyLinkType}\n`);
171
+ }
172
+ }
136
173
  const audioFiles = this._files.filter((f) => f.audioUrl);
137
174
  if (audioFiles.length > 0) {
138
175
  process.stdout.write(` ${require_sdk.dim("│")}\n`);
@@ -142,6 +179,7 @@ var PrintJob = class PrintJob extends require_sdk.Job {
142
179
  const title = af.audioArtist ? `${af.audioTitle} — ${af.audioArtist}` : af.audioTitle;
143
180
  process.stdout.write(` ${require_sdk.dim("│")} ${require_sdk.g("♪")} ${title}\n`);
144
181
  }
182
+ if (af.musicAttribution) process.stdout.write(` ${require_sdk.dim("│")} ${require_sdk.dim("via:")} ${af.musicAttribution}\n`);
145
183
  if (af.audioDuration) {
146
184
  const mins = Math.floor(af.audioDuration / 60);
147
185
  const secs = Math.round(af.audioDuration % 60);
package/dist/index.d.cts CHANGED
@@ -138,6 +138,36 @@ interface BloksSticker {
138
138
  interface MusicSticker {
139
139
  music_asset_info?: MusicAssetInfo;
140
140
  music_consumption_info?: MusicConsumptionInfo;
141
+ /** Text attribution (e.g. "Sticker by 前島亜美"). */
142
+ attribution?: string;
143
+ /** Display type (e.g. "music_hidden", "music_with_lyrics"). */
144
+ display_type?: string;
145
+ /** Placement on story canvas (0-1 fractional). */
146
+ x?: number;
147
+ y?: number;
148
+ width?: number;
149
+ height?: number;
150
+ rotation?: number;
151
+ start_time_ms?: number;
152
+ end_time_ms?: number;
153
+ }
154
+ /** A link sticker on a story (swipe-up / CTA link). */
155
+ interface StoryLink {
156
+ display_url: string;
157
+ link_title: string;
158
+ link_type: string;
159
+ url: string;
160
+ }
161
+ interface StoryLinkSticker {
162
+ id: string;
163
+ x: number;
164
+ y: number;
165
+ width: number;
166
+ height: number;
167
+ rotation: number;
168
+ start_time_ms: number;
169
+ end_time_ms: number;
170
+ story_link: StoryLink;
141
171
  }
142
172
  interface MusicAssetInfo {
143
173
  id: string;
@@ -150,10 +180,82 @@ interface MusicAssetInfo {
150
180
  duration_in_ms?: number;
151
181
  highlight_start_times_in_ms?: number[];
152
182
  progressive_download_url: string;
183
+ /** Lower-bitrate alternative download URL. */
184
+ fast_start_progressive_download_url?: string;
153
185
  cover_artwork_uri?: string;
154
186
  cover_artwork_thumbnail_uri?: string;
155
187
  has_lyrics?: boolean;
156
188
  is_explicit?: boolean;
189
+ /** Profile picture placeholder for the artist. */
190
+ placeholder_profile_pic_url?: string;
191
+ /** Audio asset ID (differs from ``id`` which is the track ID). */
192
+ audio_asset_id?: string;
193
+ /** Where in the track the playback starts (ms). */
194
+ audio_asset_start_time_in_ms?: number;
195
+ /** How long the music overlaps with the video. */
196
+ overlap_duration_in_ms?: number;
197
+ /** Music cluster ID for trending / recommendations. */
198
+ audio_cluster_id?: string;
199
+ /** Artist ID in Instagram's music catalog. */
200
+ artist_id?: string;
201
+ /** Whether the track is trending in Reels. */
202
+ is_trending_in_clips?: boolean;
203
+ /** Whether the user has bookmarked this track. */
204
+ is_bookmarked?: boolean;
205
+ /** Licensing subtype (e.g. "DEFAULT"). */
206
+ licensed_music_subtype?: string;
207
+ /** Whether media creation with this music is allowed. */
208
+ allow_media_creation_with_music?: boolean;
209
+ /** Whether saving/downloading the audio is allowed. */
210
+ allows_saving?: boolean;
211
+ /** Audio muting configuration. */
212
+ audio_muting_info?: {
213
+ mute_audio: boolean;
214
+ mute_reason_str: string;
215
+ allow_audio_editing: boolean;
216
+ show_muted_audio_toast: boolean;
217
+ };
218
+ /** Whether audio should be muted and why. */
219
+ should_mute_audio?: boolean;
220
+ should_mute_audio_reason?: string;
221
+ should_mute_audio_reason_type?: string | null;
222
+ /** Whether to render the soundwave visual. */
223
+ should_render_soundwave?: boolean;
224
+ /** Reactive/streaming audio download URL (may be null). */
225
+ reactive_audio_download_url?: string | null;
226
+ /** 30-second web preview download URL. */
227
+ web_30s_preview_download_url?: string | null;
228
+ /** Monetization info for the song. */
229
+ song_monetization_info?: unknown;
230
+ /** Trend rank info (current and previous). */
231
+ trend_rank?: unknown;
232
+ previous_trend_rank?: unknown;
233
+ /** Display labels for the track. */
234
+ display_labels?: unknown;
235
+ /** Related derived content. */
236
+ derived_content_id?: string | null;
237
+ derived_content_start_time_in_composition_in_ms?: number | null;
238
+ /** Number of clips that use this music. */
239
+ formatted_clips_media_count?: unknown;
240
+ /** Eligibility for vinyl sticker / audio effects. */
241
+ is_eligible_for_audio_effects?: unknown;
242
+ is_eligible_for_vinyl_sticker?: boolean;
243
+ /** Audio filter configurations. */
244
+ audio_filter_infos?: unknown[];
245
+ /** Lyrics data (if available). */
246
+ lyrics?: unknown;
247
+ /** Spotify track metadata (if linked). */
248
+ spotify_track_metadata?: unknown;
249
+ /** User notes on the track. */
250
+ user_notes?: unknown;
251
+ /** Related audio recommendations. */
252
+ related_audios?: unknown;
253
+ /** Dark mode message override. */
254
+ dark_message?: unknown;
255
+ /** DASH manifest for audio. */
256
+ dash_manifest?: unknown;
257
+ /** Music creation restriction reason. */
258
+ music_creation_restriction_reason?: unknown;
157
259
  }
158
260
  interface MusicConsumptionInfo {
159
261
  display_artist?: string;
@@ -190,7 +292,14 @@ interface InstagramPost {
190
292
  reel_mentions?: ReelMention[];
191
293
  story_bloks_stickers?: BloksSticker[];
192
294
  story_music_stickers?: MusicSticker[];
295
+ story_link_stickers?: StoryLinkSticker[];
193
296
  music_metadata?: MusicMetadata;
297
+ highlights_info?: {
298
+ added_to: Array<{
299
+ reel_id: string;
300
+ title: string;
301
+ }>;
302
+ };
194
303
  expiring_at?: number;
195
304
  seen?: number;
196
305
  items?: InstagramCarouselItem[];
@@ -221,13 +330,13 @@ interface InstagramCarouselItem {
221
330
  subscription_media_visibility?: string;
222
331
  audience?: string;
223
332
  story_music_stickers?: MusicSticker[];
333
+ story_link_stickers?: StoryLinkSticker[];
224
334
  usertags?: {
225
335
  in: UserTag[];
226
336
  };
227
337
  reel_mentions?: ReelMention[];
228
338
  story_bloks_stickers?: BloksSticker[];
229
339
  }
230
- /** Parsed post (normalized output) */
231
340
  interface ParsedPost {
232
341
  post_id: string;
233
342
  post_shortcode: string;
@@ -292,6 +401,15 @@ interface ParsedMedia {
292
401
  _ytdl_manifest_data?: string;
293
402
  sidecar_media_id?: string;
294
403
  sidecar_shortcode?: string;
404
+ /** Story link sticker data. */
405
+ story_link_url?: string;
406
+ story_link_display?: string;
407
+ story_link_title?: string;
408
+ story_link_type?: string;
409
+ /** Music sticker attribution (e.g. "Sticker by 前島亜美"). */
410
+ music_attribution?: string;
411
+ /** Music sticker display type (e.g. "music_hidden"). */
412
+ music_display_type?: string;
295
413
  }
296
414
  interface Coauthor {
297
415
  id: string;
package/dist/index.d.mts CHANGED
@@ -138,6 +138,36 @@ interface BloksSticker {
138
138
  interface MusicSticker {
139
139
  music_asset_info?: MusicAssetInfo;
140
140
  music_consumption_info?: MusicConsumptionInfo;
141
+ /** Text attribution (e.g. "Sticker by 前島亜美"). */
142
+ attribution?: string;
143
+ /** Display type (e.g. "music_hidden", "music_with_lyrics"). */
144
+ display_type?: string;
145
+ /** Placement on story canvas (0-1 fractional). */
146
+ x?: number;
147
+ y?: number;
148
+ width?: number;
149
+ height?: number;
150
+ rotation?: number;
151
+ start_time_ms?: number;
152
+ end_time_ms?: number;
153
+ }
154
+ /** A link sticker on a story (swipe-up / CTA link). */
155
+ interface StoryLink {
156
+ display_url: string;
157
+ link_title: string;
158
+ link_type: string;
159
+ url: string;
160
+ }
161
+ interface StoryLinkSticker {
162
+ id: string;
163
+ x: number;
164
+ y: number;
165
+ width: number;
166
+ height: number;
167
+ rotation: number;
168
+ start_time_ms: number;
169
+ end_time_ms: number;
170
+ story_link: StoryLink;
141
171
  }
142
172
  interface MusicAssetInfo {
143
173
  id: string;
@@ -150,10 +180,82 @@ interface MusicAssetInfo {
150
180
  duration_in_ms?: number;
151
181
  highlight_start_times_in_ms?: number[];
152
182
  progressive_download_url: string;
183
+ /** Lower-bitrate alternative download URL. */
184
+ fast_start_progressive_download_url?: string;
153
185
  cover_artwork_uri?: string;
154
186
  cover_artwork_thumbnail_uri?: string;
155
187
  has_lyrics?: boolean;
156
188
  is_explicit?: boolean;
189
+ /** Profile picture placeholder for the artist. */
190
+ placeholder_profile_pic_url?: string;
191
+ /** Audio asset ID (differs from ``id`` which is the track ID). */
192
+ audio_asset_id?: string;
193
+ /** Where in the track the playback starts (ms). */
194
+ audio_asset_start_time_in_ms?: number;
195
+ /** How long the music overlaps with the video. */
196
+ overlap_duration_in_ms?: number;
197
+ /** Music cluster ID for trending / recommendations. */
198
+ audio_cluster_id?: string;
199
+ /** Artist ID in Instagram's music catalog. */
200
+ artist_id?: string;
201
+ /** Whether the track is trending in Reels. */
202
+ is_trending_in_clips?: boolean;
203
+ /** Whether the user has bookmarked this track. */
204
+ is_bookmarked?: boolean;
205
+ /** Licensing subtype (e.g. "DEFAULT"). */
206
+ licensed_music_subtype?: string;
207
+ /** Whether media creation with this music is allowed. */
208
+ allow_media_creation_with_music?: boolean;
209
+ /** Whether saving/downloading the audio is allowed. */
210
+ allows_saving?: boolean;
211
+ /** Audio muting configuration. */
212
+ audio_muting_info?: {
213
+ mute_audio: boolean;
214
+ mute_reason_str: string;
215
+ allow_audio_editing: boolean;
216
+ show_muted_audio_toast: boolean;
217
+ };
218
+ /** Whether audio should be muted and why. */
219
+ should_mute_audio?: boolean;
220
+ should_mute_audio_reason?: string;
221
+ should_mute_audio_reason_type?: string | null;
222
+ /** Whether to render the soundwave visual. */
223
+ should_render_soundwave?: boolean;
224
+ /** Reactive/streaming audio download URL (may be null). */
225
+ reactive_audio_download_url?: string | null;
226
+ /** 30-second web preview download URL. */
227
+ web_30s_preview_download_url?: string | null;
228
+ /** Monetization info for the song. */
229
+ song_monetization_info?: unknown;
230
+ /** Trend rank info (current and previous). */
231
+ trend_rank?: unknown;
232
+ previous_trend_rank?: unknown;
233
+ /** Display labels for the track. */
234
+ display_labels?: unknown;
235
+ /** Related derived content. */
236
+ derived_content_id?: string | null;
237
+ derived_content_start_time_in_composition_in_ms?: number | null;
238
+ /** Number of clips that use this music. */
239
+ formatted_clips_media_count?: unknown;
240
+ /** Eligibility for vinyl sticker / audio effects. */
241
+ is_eligible_for_audio_effects?: unknown;
242
+ is_eligible_for_vinyl_sticker?: boolean;
243
+ /** Audio filter configurations. */
244
+ audio_filter_infos?: unknown[];
245
+ /** Lyrics data (if available). */
246
+ lyrics?: unknown;
247
+ /** Spotify track metadata (if linked). */
248
+ spotify_track_metadata?: unknown;
249
+ /** User notes on the track. */
250
+ user_notes?: unknown;
251
+ /** Related audio recommendations. */
252
+ related_audios?: unknown;
253
+ /** Dark mode message override. */
254
+ dark_message?: unknown;
255
+ /** DASH manifest for audio. */
256
+ dash_manifest?: unknown;
257
+ /** Music creation restriction reason. */
258
+ music_creation_restriction_reason?: unknown;
157
259
  }
158
260
  interface MusicConsumptionInfo {
159
261
  display_artist?: string;
@@ -190,7 +292,14 @@ interface InstagramPost {
190
292
  reel_mentions?: ReelMention[];
191
293
  story_bloks_stickers?: BloksSticker[];
192
294
  story_music_stickers?: MusicSticker[];
295
+ story_link_stickers?: StoryLinkSticker[];
193
296
  music_metadata?: MusicMetadata;
297
+ highlights_info?: {
298
+ added_to: Array<{
299
+ reel_id: string;
300
+ title: string;
301
+ }>;
302
+ };
194
303
  expiring_at?: number;
195
304
  seen?: number;
196
305
  items?: InstagramCarouselItem[];
@@ -221,13 +330,13 @@ interface InstagramCarouselItem {
221
330
  subscription_media_visibility?: string;
222
331
  audience?: string;
223
332
  story_music_stickers?: MusicSticker[];
333
+ story_link_stickers?: StoryLinkSticker[];
224
334
  usertags?: {
225
335
  in: UserTag[];
226
336
  };
227
337
  reel_mentions?: ReelMention[];
228
338
  story_bloks_stickers?: BloksSticker[];
229
339
  }
230
- /** Parsed post (normalized output) */
231
340
  interface ParsedPost {
232
341
  post_id: string;
233
342
  post_shortcode: string;
@@ -292,6 +401,15 @@ interface ParsedMedia {
292
401
  _ytdl_manifest_data?: string;
293
402
  sidecar_media_id?: string;
294
403
  sidecar_shortcode?: string;
404
+ /** Story link sticker data. */
405
+ story_link_url?: string;
406
+ story_link_display?: string;
407
+ story_link_title?: string;
408
+ story_link_type?: string;
409
+ /** Music sticker attribution (e.g. "Sticker by 前島亜美"). */
410
+ music_attribution?: string;
411
+ /** Music sticker display type (e.g. "music_hidden"). */
412
+ music_display_type?: string;
295
413
  }
296
414
  interface Coauthor {
297
415
  id: string;
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { A as directory, B as _YELLOW, C as extract, D as parseUnicodeEscapes, E as parseInt, F as Extractor, G as pad, H as c, I as noopLogger, K as ConfigManager, L as DownloadJob, M as url, N as idFromShortcode, O as unescape, P as shortcodeFromId, R as Job, S as extr, T as nameExtFromURL, U as dim, V as b, W as g, _ as extractAudio, a as InstagramTaggedExtractor, b as InstagramRestAPI, c as InstagramSavedExtractor, d as InstagramPostExtractor, f as InstagramInfoExtractor, g as parsePostGraphql, h as InstagramExtractor, i as InstagramUserExtractor, j as queue, k as unquote, l as InstagramReelsExtractor, m as InstagramAvatarExtractor, o as InstagramTagExtractor, p as InstagramHighlightsExtractor, s as InstagramStoriesExtractor, t as InstagramSDK, u as InstagramPostsExtractor, v as extractTaggedUsers, w as findTags, x as ensureHttpScheme, y as parsePostRest, z as _RESET } from "./sdk-E0L5ISZC.mjs";
1
+ import { A as directory, B as _YELLOW, C as extract, D as parseUnicodeEscapes, E as parseInt, F as Extractor, G as pad, H as c, I as noopLogger, K as ConfigManager, L as DownloadJob, M as url, N as idFromShortcode, O as unescape, P as shortcodeFromId, R as Job, S as extr, T as nameExtFromURL, U as dim, V as b, W as g, _ as extractAudio, a as InstagramTaggedExtractor, b as InstagramRestAPI, c as InstagramSavedExtractor, d as InstagramPostExtractor, f as InstagramInfoExtractor, g as parsePostGraphql, h as InstagramExtractor, i as InstagramUserExtractor, j as queue, k as unquote, l as InstagramReelsExtractor, m as InstagramAvatarExtractor, o as InstagramTagExtractor, p as InstagramHighlightsExtractor, s as InstagramStoriesExtractor, t as InstagramSDK, u as InstagramPostsExtractor, v as extractTaggedUsers, w as findTags, x as ensureHttpScheme, y as parsePostRest, z as _RESET } from "./sdk-Dr4PJwiS.mjs";
2
2
  //#region src/core/print-job.ts
3
3
  var PrintJob = class PrintJob extends Job {
4
4
  _currentDir = {};
@@ -36,7 +36,12 @@ var PrintJob = class PrintJob extends Job {
36
36
  audioDuration: meta.audio_duration ?? void 0,
37
37
  audioHasLyrics: meta.audio_has_lyrics ?? void 0,
38
38
  audioIsExplicit: meta.audio_is_explicit ?? void 0,
39
- coverArtworkUri: meta.audio_cover_artwork_uri ?? meta.audio_cover_artwork_thumbnail_uri ?? void 0
39
+ coverArtworkUri: meta.audio_cover_artwork_uri ?? meta.audio_cover_artwork_thumbnail_uri ?? void 0,
40
+ storyLinkUrl: meta.story_link_url ?? void 0,
41
+ storyLinkDisplay: meta.story_link_display ?? void 0,
42
+ storyLinkTitle: meta.story_link_title ?? void 0,
43
+ storyLinkType: meta.story_link_type ?? void 0,
44
+ musicAttribution: meta.music_attribution ?? void 0
40
45
  });
41
46
  }
42
47
  async handleQueue(msg) {
@@ -47,9 +52,9 @@ var PrintJob = class PrintJob extends Job {
47
52
  ...this._currentDir,
48
53
  ...msg.metadata
49
54
  }._extractor;
50
- if (!extrClass || typeof extrClass !== "object") return;
55
+ if (!extrClass || typeof extrClass !== "function") return;
51
56
  const cls = extrClass;
52
- const match = cls.pattern.exec(msg.url);
57
+ const match = cls.pattern.exec(msg.url) ?? cls.pattern.exec(msg.url.replace(/\/$/, ""));
53
58
  if (!match) return;
54
59
  const parentExtr = this.extractor;
55
60
  const childJob = new PrintJob(Reflect.construct(cls, [{
@@ -88,6 +93,28 @@ var PrintJob = class PrintJob extends Job {
88
93
  row("Type:", `${m.type ?? "?"} (${this._files.length} files)`);
89
94
  row("URL:", m.post_url ?? "?");
90
95
  const desc = m.description ?? "";
96
+ const isPrivate = m.is_private;
97
+ const isVerified = m.is_verified;
98
+ const followerCount = m.follower_count;
99
+ const followingCount = m.following_count;
100
+ const mediaCount = m.media_count;
101
+ const bio = m.biography ?? "";
102
+ const externalUrl = m.external_url;
103
+ if (isPrivate !== void 0 || isVerified !== void 0 || followerCount !== void 0) {
104
+ const badges = [];
105
+ if (isPrivate) badges.push("🔒 Private");
106
+ if (isVerified) badges.push("✅ Verified");
107
+ if (followerCount !== void 0) badges.push(`${followerCount.toLocaleString()} followers`);
108
+ if (followingCount !== void 0) badges.push(`${followingCount.toLocaleString()} following`);
109
+ if (mediaCount !== void 0) badges.push(`${mediaCount.toLocaleString()} posts`);
110
+ row("Profile:", badges.join(" · "));
111
+ }
112
+ if (bio) {
113
+ process.stdout.write(` ${dim("│")}\n`);
114
+ process.stdout.write(` ${dim("│")} ${b("Bio:")}\n`);
115
+ for (const line of bio.split("\n")) for (const wl of this._wrap(line, w - 8)) process.stdout.write(` ${dim("│")} ${dim(wl)}\n`);
116
+ }
117
+ if (externalUrl) row("Website:", externalUrl);
91
118
  if (desc) {
92
119
  process.stdout.write(` ${dim("│")}\n`);
93
120
  process.stdout.write(` ${dim("│")} ${b("Description:")}\n`);
@@ -132,6 +159,16 @@ var PrintJob = class PrintJob extends Job {
132
159
  process.stdout.write(`${line}\n`);
133
160
  }
134
161
  }
162
+ const linkFiles = this._files.filter((f) => f.storyLinkUrl);
163
+ if (linkFiles.length > 0) {
164
+ process.stdout.write(` ${dim("│")}\n`);
165
+ process.stdout.write(` ${dim("│")} ${b("Link:")}\n`);
166
+ for (const lf of linkFiles) {
167
+ if (lf.storyLinkDisplay) process.stdout.write(` ${dim("│")} ${g("🔗")} ${lf.storyLinkDisplay}\n`);
168
+ if (lf.storyLinkTitle && lf.storyLinkTitle !== "Visit Link") process.stdout.write(` ${dim("│")} ${dim("Title:")} ${lf.storyLinkTitle}\n`);
169
+ if (lf.storyLinkType) process.stdout.write(` ${dim("│")} ${dim("Type:")} ${lf.storyLinkType}\n`);
170
+ }
171
+ }
135
172
  const audioFiles = this._files.filter((f) => f.audioUrl);
136
173
  if (audioFiles.length > 0) {
137
174
  process.stdout.write(` ${dim("│")}\n`);
@@ -141,6 +178,7 @@ var PrintJob = class PrintJob extends Job {
141
178
  const title = af.audioArtist ? `${af.audioTitle} — ${af.audioArtist}` : af.audioTitle;
142
179
  process.stdout.write(` ${dim("│")} ${g("♪")} ${title}\n`);
143
180
  }
181
+ if (af.musicAttribution) process.stdout.write(` ${dim("│")} ${dim("via:")} ${af.musicAttribution}\n`);
144
182
  if (af.audioDuration) {
145
183
  const mins = Math.floor(af.audioDuration / 60);
146
184
  const secs = Math.round(af.audioDuration % 60);
package/dist/node.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_sdk = require("./sdk-D8q2Rjw2.cjs");
2
+ const require_sdk = require("./sdk-B9mRv7JE.cjs");
3
3
  //#region src/node-factory.ts
4
4
  /**
5
5
  * Create an SDK instance with Node.js defaults.
package/dist/node.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { I as noopLogger, n as createFetchHttpClient, r as extractCsrf, t as InstagramSDK } from "./sdk-E0L5ISZC.mjs";
1
+ import { I as noopLogger, n as createFetchHttpClient, r as extractCsrf, t as InstagramSDK } from "./sdk-Dr4PJwiS.mjs";
2
2
  //#region src/node-factory.ts
3
3
  /**
4
4
  * Create an SDK instance with Node.js defaults.
@@ -203,9 +203,9 @@ var DownloadJob = class DownloadJob extends Job {
203
203
  ...msg.metadata
204
204
  };
205
205
  const extrClass = meta._extractor;
206
- if (!extrClass || typeof extrClass !== "object") return;
206
+ if (!extrClass || typeof extrClass !== "function") return;
207
207
  const cls = extrClass;
208
- const match = cls.pattern.exec(msg.url);
208
+ const match = cls.pattern.exec(msg.url) ?? cls.pattern.exec(msg.url.replace(/\/$/, ""));
209
209
  if (!match) return;
210
210
  const parentExtr = this.extractor;
211
211
  const childJob = new DownloadJob(Reflect.construct(cls, [{
@@ -1000,9 +1000,38 @@ function parseStoryRest(post, cfg) {
1000
1000
  const item = items[num];
1001
1001
  const media = parseMediaItem(item, post, cfg, num + 1);
1002
1002
  if (!media) continue;
1003
- extractTaggedUsers(item, media);
1003
+ const itemRec = item;
1004
+ extractTaggedUsers(itemRec, media);
1005
+ const musicStickers = itemRec.story_music_stickers;
1006
+ if (musicStickers?.[0]) {
1007
+ const audio = extractAudio(itemRec, data, musicStickers[0], cfg);
1008
+ if (audio) {
1009
+ audio.num = num + 1;
1010
+ if (musicStickers[0].attribution) audio.music_attribution = musicStickers[0].attribution;
1011
+ if (musicStickers[0].display_type) audio.music_display_type = musicStickers[0].display_type;
1012
+ data._files.push(audio);
1013
+ }
1014
+ }
1015
+ const linkStickers = itemRec.story_link_stickers;
1016
+ if (linkStickers?.[0]) {
1017
+ const sl = linkStickers[0].story_link;
1018
+ media.story_link_url = sl.url;
1019
+ media.story_link_display = sl.display_url;
1020
+ media.story_link_title = sl.link_title;
1021
+ media.story_link_type = sl.link_type;
1022
+ }
1004
1023
  data._files.push(media);
1005
1024
  }
1025
+ if (post.music_metadata) {
1026
+ const info = post.music_metadata.music_info;
1027
+ if (info) {
1028
+ const audio = extractAudio(post, data, info, cfg);
1029
+ if (audio) {
1030
+ audio.num = items.length;
1031
+ data._files.push(audio);
1032
+ }
1033
+ }
1034
+ }
1006
1035
  return data;
1007
1036
  }
1008
1037
  /** Parse a single media item (image/video) from a carousel or story. */
@@ -1345,7 +1374,14 @@ var InstagramExtractor = class extends Extractor {
1345
1374
  yield url(file.audio_url, combined);
1346
1375
  }
1347
1376
  if (previewsAud) combined.media_id = `${combined.media_id}p`;
1348
- else continue;
1377
+ if (!audio && !previewsAud) {
1378
+ const coverUrl = file.display_url;
1379
+ if (coverUrl) {
1380
+ nameExtFromURL(coverUrl, combined);
1381
+ yield url(coverUrl, combined);
1382
+ }
1383
+ continue;
1384
+ }
1349
1385
  }
1350
1386
  if (file.video_url) {
1351
1387
  if (shouldDownloadVideos) {
@@ -1509,10 +1545,37 @@ var InstagramInfoExtractor = class InstagramInfoExtractor extends InstagramExtra
1509
1545
  }
1510
1546
  async *items() {
1511
1547
  const screenName = (this.groups[0] ?? "").replace(/^\//, "");
1512
- let user;
1513
- if (screenName.startsWith("id:")) user = await this.api.userById(screenName.slice(3));
1514
- else user = await this.api.userByScreenName(screenName);
1515
- yield directory(user);
1548
+ const userId = screenName.startsWith("id:") ? screenName.slice(3) : await this.api.userId(screenName, false);
1549
+ const user = await this.api.userById(userId);
1550
+ const ur = user;
1551
+ const num = (key) => ur[key] ?? ur[key]?.count ?? 0;
1552
+ yield directory({
1553
+ post_id: user.pk,
1554
+ post_shortcode: user.username,
1555
+ post_url: `${this.root}/${user.username}/`,
1556
+ owner_id: user.pk,
1557
+ username: user.username,
1558
+ fullname: user.full_name ?? "",
1559
+ post_date: "",
1560
+ date: "",
1561
+ description: "",
1562
+ likes: 0,
1563
+ liked: false,
1564
+ pinned: [],
1565
+ type: "post",
1566
+ count: 0,
1567
+ _files: [],
1568
+ user,
1569
+ is_private: user.is_private ?? false,
1570
+ is_verified: user.is_verified ?? false,
1571
+ profile_pic_url: user.profile_pic_url ?? "",
1572
+ profile_pic_url_hd: user.hd_profile_pic_url_info?.url ?? user.profile_pic_url_hd ?? "",
1573
+ follower_count: num("follower_count"),
1574
+ following_count: num("following_count"),
1575
+ media_count: num("media_count"),
1576
+ biography: ur.biography ?? "",
1577
+ external_url: ur.external_url ?? ""
1578
+ });
1516
1579
  }
1517
1580
  async *posts() {}
1518
1581
  };
@@ -203,9 +203,9 @@ var DownloadJob = class DownloadJob extends Job {
203
203
  ...msg.metadata
204
204
  };
205
205
  const extrClass = meta._extractor;
206
- if (!extrClass || typeof extrClass !== "object") return;
206
+ if (!extrClass || typeof extrClass !== "function") return;
207
207
  const cls = extrClass;
208
- const match = cls.pattern.exec(msg.url);
208
+ const match = cls.pattern.exec(msg.url) ?? cls.pattern.exec(msg.url.replace(/\/$/, ""));
209
209
  if (!match) return;
210
210
  const parentExtr = this.extractor;
211
211
  const childJob = new DownloadJob(Reflect.construct(cls, [{
@@ -1000,9 +1000,38 @@ function parseStoryRest(post, cfg) {
1000
1000
  const item = items[num];
1001
1001
  const media = parseMediaItem(item, post, cfg, num + 1);
1002
1002
  if (!media) continue;
1003
- extractTaggedUsers(item, media);
1003
+ const itemRec = item;
1004
+ extractTaggedUsers(itemRec, media);
1005
+ const musicStickers = itemRec.story_music_stickers;
1006
+ if (musicStickers?.[0]) {
1007
+ const audio = extractAudio(itemRec, data, musicStickers[0], cfg);
1008
+ if (audio) {
1009
+ audio.num = num + 1;
1010
+ if (musicStickers[0].attribution) audio.music_attribution = musicStickers[0].attribution;
1011
+ if (musicStickers[0].display_type) audio.music_display_type = musicStickers[0].display_type;
1012
+ data._files.push(audio);
1013
+ }
1014
+ }
1015
+ const linkStickers = itemRec.story_link_stickers;
1016
+ if (linkStickers?.[0]) {
1017
+ const sl = linkStickers[0].story_link;
1018
+ media.story_link_url = sl.url;
1019
+ media.story_link_display = sl.display_url;
1020
+ media.story_link_title = sl.link_title;
1021
+ media.story_link_type = sl.link_type;
1022
+ }
1004
1023
  data._files.push(media);
1005
1024
  }
1025
+ if (post.music_metadata) {
1026
+ const info = post.music_metadata.music_info;
1027
+ if (info) {
1028
+ const audio = extractAudio(post, data, info, cfg);
1029
+ if (audio) {
1030
+ audio.num = items.length;
1031
+ data._files.push(audio);
1032
+ }
1033
+ }
1034
+ }
1006
1035
  return data;
1007
1036
  }
1008
1037
  /** Parse a single media item (image/video) from a carousel or story. */
@@ -1345,7 +1374,14 @@ var InstagramExtractor = class extends Extractor {
1345
1374
  yield url(file.audio_url, combined);
1346
1375
  }
1347
1376
  if (previewsAud) combined.media_id = `${combined.media_id}p`;
1348
- else continue;
1377
+ if (!audio && !previewsAud) {
1378
+ const coverUrl = file.display_url;
1379
+ if (coverUrl) {
1380
+ nameExtFromURL(coverUrl, combined);
1381
+ yield url(coverUrl, combined);
1382
+ }
1383
+ continue;
1384
+ }
1349
1385
  }
1350
1386
  if (file.video_url) {
1351
1387
  if (shouldDownloadVideos) {
@@ -1509,10 +1545,37 @@ var InstagramInfoExtractor = class InstagramInfoExtractor extends InstagramExtra
1509
1545
  }
1510
1546
  async *items() {
1511
1547
  const screenName = (this.groups[0] ?? "").replace(/^\//, "");
1512
- let user;
1513
- if (screenName.startsWith("id:")) user = await this.api.userById(screenName.slice(3));
1514
- else user = await this.api.userByScreenName(screenName);
1515
- yield directory(user);
1548
+ const userId = screenName.startsWith("id:") ? screenName.slice(3) : await this.api.userId(screenName, false);
1549
+ const user = await this.api.userById(userId);
1550
+ const ur = user;
1551
+ const num = (key) => ur[key] ?? ur[key]?.count ?? 0;
1552
+ yield directory({
1553
+ post_id: user.pk,
1554
+ post_shortcode: user.username,
1555
+ post_url: `${this.root}/${user.username}/`,
1556
+ owner_id: user.pk,
1557
+ username: user.username,
1558
+ fullname: user.full_name ?? "",
1559
+ post_date: "",
1560
+ date: "",
1561
+ description: "",
1562
+ likes: 0,
1563
+ liked: false,
1564
+ pinned: [],
1565
+ type: "post",
1566
+ count: 0,
1567
+ _files: [],
1568
+ user,
1569
+ is_private: user.is_private ?? false,
1570
+ is_verified: user.is_verified ?? false,
1571
+ profile_pic_url: user.profile_pic_url ?? "",
1572
+ profile_pic_url_hd: user.hd_profile_pic_url_info?.url ?? user.profile_pic_url_hd ?? "",
1573
+ follower_count: num("follower_count"),
1574
+ following_count: num("following_count"),
1575
+ media_count: num("media_count"),
1576
+ biography: ur.biography ?? "",
1577
+ external_url: ur.external_url ?? ""
1578
+ });
1516
1579
  }
1517
1580
  async *posts() {}
1518
1581
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@chilfish/gallery-dl-instagram",
3
3
  "type": "module",
4
- "version": "0.2.3",
4
+ "version": "0.2.4",
5
5
  "description": "Instagram extraction pipeline — platform-agnostic SDK + CLI",
6
6
  "license": "GPL-2.0-only",
7
7
  "keywords": [