@postfetch/core 0.2.3 → 0.3.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/dist/index.js CHANGED
@@ -520,6 +520,714 @@ function randomToken(length = 8) {
520
520
  return crypto.getRandomValues(new Uint8Array(length)).reduce((value, byte) => value + (byte % 36).toString(36), "");
521
521
  }
522
522
 
523
+ // src/isobmff.ts
524
+ function parseBoxes(data, start = 0, end = data.length) {
525
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
526
+ const boxes = [];
527
+ let offset = start;
528
+ while (offset + 8 <= end) {
529
+ let size = view.getUint32(offset);
530
+ const type = String.fromCharCode(data[offset + 4], data[offset + 5], data[offset + 6], data[offset + 7]);
531
+ let dataStart = offset + 8;
532
+ if (size === 1) {
533
+ size = Number(view.getBigUint64(offset + 8));
534
+ dataStart = offset + 16;
535
+ } else if (size === 0) {
536
+ size = end - offset;
537
+ }
538
+ if (size < 8 || offset + size > end) {
539
+ break;
540
+ }
541
+ boxes.push({ type, start: offset, end: offset + size, dataStart });
542
+ offset += size;
543
+ }
544
+ return boxes;
545
+ }
546
+ function findBox(boxes, type) {
547
+ return boxes.find((box) => box.type === type);
548
+ }
549
+ function childBoxes(data, box) {
550
+ return parseBoxes(data, box.dataStart, box.end);
551
+ }
552
+ function sliceBox(data, box) {
553
+ return data.slice(box.start, box.end);
554
+ }
555
+ function buildBox(type, payload) {
556
+ const out = new Uint8Array(8 + payload.length);
557
+ const view = new DataView(out.buffer);
558
+ view.setUint32(0, out.length);
559
+ out[4] = type.charCodeAt(0);
560
+ out[5] = type.charCodeAt(1);
561
+ out[6] = type.charCodeAt(2);
562
+ out[7] = type.charCodeAt(3);
563
+ out.set(payload, 8);
564
+ return out;
565
+ }
566
+ function concat(parts) {
567
+ const total = parts.reduce((sum, part) => sum + part.length, 0);
568
+ const out = new Uint8Array(total);
569
+ let offset = 0;
570
+ for (const part of parts) {
571
+ out.set(part, offset);
572
+ offset += part.length;
573
+ }
574
+ return out;
575
+ }
576
+ function readU32(data, offset) {
577
+ return new DataView(data.buffer, data.byteOffset, data.byteLength).getUint32(offset);
578
+ }
579
+ function writeU32(data, offset, value) {
580
+ new DataView(data.buffer, data.byteOffset, data.byteLength).setUint32(offset, value);
581
+ }
582
+ function boxVersion(data, box) {
583
+ return data[box.dataStart];
584
+ }
585
+
586
+ // src/hls.ts
587
+ function isMasterPlaylist(text) {
588
+ return text.includes("#EXT-X-STREAM-INF");
589
+ }
590
+ function parseMaster(text, baseUrl) {
591
+ const lines = text.split(`
592
+ `).map((line) => line.trim());
593
+ const audio = {};
594
+ const variants = [];
595
+ for (let index = 0;index < lines.length; index += 1) {
596
+ const line = lines[index];
597
+ if (line.startsWith("#EXT-X-MEDIA:") && /TYPE=AUDIO/.test(line)) {
598
+ const group = line.match(/GROUP-ID="([^"]+)"/)?.[1];
599
+ const uri = line.match(/URI="([^"]+)"/)?.[1];
600
+ if (group && uri) {
601
+ audio[group] = new URL(uri, baseUrl).href;
602
+ }
603
+ } else if (line.startsWith("#EXT-X-STREAM-INF:")) {
604
+ const target = lines[index + 1];
605
+ if (target && !target.startsWith("#")) {
606
+ const resolution = line.match(/RESOLUTION=(\d+)x(\d+)/);
607
+ variants.push({
608
+ width: resolution ? Number(resolution[1]) : 0,
609
+ height: resolution ? Number(resolution[2]) : 0,
610
+ bandwidth: Number(line.match(/BANDWIDTH=(\d+)/)?.[1] ?? 0),
611
+ url: new URL(target, baseUrl).href,
612
+ audioGroup: line.match(/AUDIO="([^"]+)"/)?.[1] ?? null
613
+ });
614
+ }
615
+ }
616
+ }
617
+ return { variants, audio };
618
+ }
619
+ async function assembleHls(net, playlistUrl, headers) {
620
+ const response = await net(playlistUrl, { headers });
621
+ if (!response.ok) {
622
+ throw new Error(`HLS playlist failed: ${response.status}`);
623
+ }
624
+ const segments = mediaSegments(await response.text(), playlistUrl);
625
+ if (segments.length === 0) {
626
+ throw new Error("HLS playlist has no segments");
627
+ }
628
+ return concat(await fetchSegments(net, segments, headers));
629
+ }
630
+ function mediaSegments(text, baseUrl) {
631
+ const segments = [];
632
+ let pendingRange = null;
633
+ let nextOffset = 0;
634
+ for (const raw of text.split(`
635
+ `)) {
636
+ const line = raw.trim();
637
+ if (line.startsWith("#EXT-X-MAP:")) {
638
+ const uri = line.match(/URI="([^"]+)"/)?.[1];
639
+ if (uri) {
640
+ const range = byteRange(line.match(/BYTERANGE="([^"]+)"/)?.[1], nextOffset);
641
+ segments.push({ url: new URL(uri, baseUrl).href, range });
642
+ nextOffset = range ? range.offset + range.length : nextOffset;
643
+ }
644
+ } else if (line.startsWith("#EXT-X-BYTERANGE:")) {
645
+ pendingRange = byteRange(line.slice("#EXT-X-BYTERANGE:".length), nextOffset);
646
+ nextOffset = pendingRange ? pendingRange.offset + pendingRange.length : nextOffset;
647
+ } else if (line.length > 0 && !line.startsWith("#")) {
648
+ segments.push({ url: new URL(line, baseUrl).href, range: pendingRange });
649
+ pendingRange = null;
650
+ }
651
+ }
652
+ return segments;
653
+ }
654
+ function byteRange(spec, runningOffset) {
655
+ const match = spec?.trim().match(/^(\d+)(?:@(\d+))?$/);
656
+ if (!match) {
657
+ return null;
658
+ }
659
+ return { length: Number(match[1]), offset: match[2] === undefined ? runningOffset : Number(match[2]) };
660
+ }
661
+ async function fetchSegments(net, segments, headers) {
662
+ const results = new Array(segments.length);
663
+ let cursor = 0;
664
+ async function worker() {
665
+ while (cursor < segments.length) {
666
+ const index = cursor;
667
+ cursor += 1;
668
+ results[index] = await fetchSegment(net, segments[index], headers);
669
+ }
670
+ }
671
+ const workers = Array.from({ length: Math.min(8, segments.length) }, worker);
672
+ await Promise.all(workers);
673
+ return results;
674
+ }
675
+ async function fetchSegment(net, segment, headers) {
676
+ const requestHeaders = new Headers(headers);
677
+ if (segment.range) {
678
+ requestHeaders.set("range", `bytes=${segment.range.offset}-${segment.range.offset + segment.range.length - 1}`);
679
+ }
680
+ const response = await net(segment.url, { headers: requestHeaders });
681
+ if (!response.ok) {
682
+ throw new Error(`HLS segment failed: ${response.status}`);
683
+ }
684
+ return new Uint8Array(await response.arrayBuffer());
685
+ }
686
+
687
+ // src/pinterest.ts
688
+ async function resolvePinterest(input) {
689
+ const id = await pinId(input.net, input.url);
690
+ const pin = await pinResource(input.net, id);
691
+ const items = await mediaItems2(input, pin, id);
692
+ if (items.length === 0) {
693
+ throw new Error("Pinterest media not found");
694
+ }
695
+ return { archiveFilename: filename(`pinterest_${id}.zip`), id, items, platform: "pinterest" };
696
+ }
697
+ async function pinId(net, input) {
698
+ const direct = parsePin(input);
699
+ if (direct) {
700
+ return direct;
701
+ }
702
+ const response = await net(input, { headers: { "user-agent": browserUserAgent() } }, 1);
703
+ const resolved = parsePin(response.url);
704
+ if (!resolved) {
705
+ throw new Error("Pinterest pin id not found");
706
+ }
707
+ return resolved;
708
+ }
709
+ function parsePin(input) {
710
+ return asUrl(input).pathname.match(/\/pin\/(\d+)/)?.[1] ?? null;
711
+ }
712
+ async function pinResource(net, id) {
713
+ const data = JSON.stringify({
714
+ options: { id, field_set_key: "unauth_react_main_pin", fetch_visual_search_objects: false },
715
+ context: {}
716
+ });
717
+ const url = new URL("https://www.pinterest.com/resource/PinResource/get/");
718
+ url.searchParams.set("source_url", `/pin/${id}/`);
719
+ url.searchParams.set("data", data);
720
+ const response = await net(url.href, {
721
+ headers: {
722
+ accept: "application/json",
723
+ "user-agent": browserUserAgent(),
724
+ "x-pinterest-pws-handler": "www/pin/[id].js"
725
+ }
726
+ });
727
+ if (!response.ok) {
728
+ throw new Error(`Pinterest resource failed: ${response.status}`);
729
+ }
730
+ const payload = await response.json().catch(() => null);
731
+ const wrapper = object(payload) && object(payload.resource_response) ? payload.resource_response : null;
732
+ const pin = wrapper && object(wrapper.data) ? wrapper.data : null;
733
+ if (!pin) {
734
+ throw new Error("Pinterest pin not found");
735
+ }
736
+ return pin;
737
+ }
738
+ async function mediaItems2(input, pin, id) {
739
+ const video = await standardVideo(input, pin, id);
740
+ if (video.length > 0) {
741
+ return video;
742
+ }
743
+ const story = await storyItems(input, pin, id);
744
+ if (story.length > 0) {
745
+ return story;
746
+ }
747
+ return imageItems(pin, id);
748
+ }
749
+ async function standardVideo(input, pin, id) {
750
+ const item = await videoFromList(input, id, null, videoList(object(pin.videos) ? pin.videos : null));
751
+ return item ? [item] : [];
752
+ }
753
+ async function storyItems(input, pin, id) {
754
+ const story = object(pin.story_pin_data) ? pin.story_pin_data : null;
755
+ if (!story) {
756
+ return [];
757
+ }
758
+ const pages = Array.isArray(story.pages) ? story.pages.filter(object) : [];
759
+ const blocks = pages.flatMap((page) => Array.isArray(page.blocks) ? page.blocks.filter(object) : []);
760
+ const videoBlocks = blocks.filter((block) => object(block.video));
761
+ const videos = [];
762
+ for (const [index, block] of videoBlocks.entries()) {
763
+ const item = await videoFromList(input, id, index + 1, videoList(object(block.video) ? block.video : null));
764
+ if (item) {
765
+ videos.push(item);
766
+ }
767
+ }
768
+ if (videos.length > 0) {
769
+ return videos;
770
+ }
771
+ if (videoBlocks.length > 0) {
772
+ throw new Error("Pinterest idea pin video has no downloadable rendition");
773
+ }
774
+ return imageItems(pin, id);
775
+ }
776
+ async function videoFromList(input, id, index, renditions) {
777
+ const progressive = progressiveMp4(renditions);
778
+ if (progressive) {
779
+ return videoItem(id, index, progressive);
780
+ }
781
+ const master = hlsUrl(renditions);
782
+ return master ? hlsVideoItem(input.net, id, index, master, input.preferredWidth) : null;
783
+ }
784
+ async function hlsVideoItem(net, id, index, masterUrl, preferredWidth) {
785
+ const headers = { "user-agent": browserUserAgent() };
786
+ const suffix = index === null ? "" : `_${index}`;
787
+ const base = {
788
+ filename: filename(`pinterest_${id}${suffix}.mp4`),
789
+ headers,
790
+ hls: true,
791
+ id,
792
+ kind: "video",
793
+ mime: "video/mp4",
794
+ platform: "pinterest",
795
+ url: masterUrl
796
+ };
797
+ const response = await net(masterUrl, { headers });
798
+ if (!response.ok) {
799
+ throw new Error(`Pinterest HLS failed: ${response.status}`);
800
+ }
801
+ const playlist = await response.text();
802
+ if (!isMasterPlaylist(playlist)) {
803
+ return base;
804
+ }
805
+ const master = parseMaster(playlist, masterUrl);
806
+ const variant = pickVariant(master.variants, preferredWidth);
807
+ if (!variant) {
808
+ throw new Error("Pinterest HLS variant not found");
809
+ }
810
+ const audioUrl = variant.audioGroup ? master.audio[variant.audioGroup] : undefined;
811
+ return { ...base, url: variant.url, ...audioUrl ? { audio: { headers, url: audioUrl } } : {} };
812
+ }
813
+ function pickVariant(variants, preferredWidth) {
814
+ return variants.reduce((current, variant) => {
815
+ if (!current) {
816
+ return variant;
817
+ }
818
+ return Math.abs(variant.width - preferredWidth) < Math.abs(current.width - preferredWidth) ? variant : current;
819
+ }, null);
820
+ }
821
+ function hlsUrl(renditions) {
822
+ return renditions.map((rendition) => string(rendition.url)).find((url) => url !== null && url.toLowerCase().includes(".m3u8")) ?? null;
823
+ }
824
+ function imageItems(pin, id) {
825
+ const images = object(pin.images) ? pin.images : null;
826
+ const orig = images && object(images.orig) ? images.orig : null;
827
+ const url = orig ? string(orig.url) : null;
828
+ if (!url) {
829
+ return [];
830
+ }
831
+ return [
832
+ {
833
+ filename: filename(`pinterest_${id}.${imageExtension(url)}`),
834
+ headers: { "user-agent": browserUserAgent() },
835
+ id,
836
+ kind: "image",
837
+ mime: imageMime(url),
838
+ platform: "pinterest",
839
+ url
840
+ }
841
+ ];
842
+ }
843
+ function videoList(videos) {
844
+ const list = videos && object(videos.video_list) ? videos.video_list : null;
845
+ return list ? Object.values(list).filter(object) : [];
846
+ }
847
+ function progressiveMp4(renditions) {
848
+ const mp4s = renditions.filter((rendition) => string(rendition.url)?.toLowerCase().endsWith(".mp4"));
849
+ const best = mp4s.reduce((current, rendition) => {
850
+ const width = number(rendition.width) ?? 0;
851
+ const currentWidth = current ? number(current.width) ?? 0 : -1;
852
+ return width > currentWidth ? rendition : current;
853
+ }, null);
854
+ return best ? string(best.url) : null;
855
+ }
856
+ function videoItem(id, index, url) {
857
+ const suffix = index === null ? "" : `_${index}`;
858
+ return {
859
+ filename: filename(`pinterest_${id}${suffix}.mp4`),
860
+ headers: { "user-agent": browserUserAgent() },
861
+ id,
862
+ kind: "video",
863
+ mime: "video/mp4",
864
+ platform: "pinterest",
865
+ url
866
+ };
867
+ }
868
+ function imageExtension(url) {
869
+ const extension = asUrl(url).pathname.match(/\.([a-z0-9]+)$/i)?.[1]?.toLowerCase();
870
+ return extension === "png" || extension === "webp" || extension === "gif" ? extension : "jpg";
871
+ }
872
+ function imageMime(url) {
873
+ const extension = imageExtension(url);
874
+ if (extension === "png") {
875
+ return "image/png";
876
+ }
877
+ if (extension === "webp") {
878
+ return "image/webp";
879
+ }
880
+ if (extension === "gif") {
881
+ return "image/gif";
882
+ }
883
+ return "image/jpeg";
884
+ }
885
+
886
+ // src/reddit.ts
887
+ var installedClientId = "ohXpoqrZYub1kg";
888
+ async function resolveReddit(input) {
889
+ const id = await postId(input.net, input.url);
890
+ const token = await appToken(input.net);
891
+ const post = await fetchPost(input.net, token, id);
892
+ const items = await mediaItems3(input, post, id);
893
+ if (items.length === 0) {
894
+ throw new Error("Reddit media not found");
895
+ }
896
+ return { archiveFilename: filename(`reddit_${id}.zip`), id, items, platform: "reddit" };
897
+ }
898
+ async function postId(net, input) {
899
+ const direct = parseId(input);
900
+ if (direct) {
901
+ return direct;
902
+ }
903
+ const response = await net(input, { headers: { "user-agent": browserUserAgent() } }, 1);
904
+ const resolved = parseId(response.url);
905
+ if (!resolved) {
906
+ throw new Error("Reddit post id not found");
907
+ }
908
+ return resolved;
909
+ }
910
+ function parseId(input) {
911
+ const url = asUrl(input);
912
+ if (url.hostname === "redd.it") {
913
+ const slug = url.pathname.split("/").filter(Boolean)[0];
914
+ return slug && /^[a-z0-9]+$/i.test(slug) ? slug : null;
915
+ }
916
+ return url.pathname.match(/\/comments\/([a-z0-9]+)/i)?.[1] ?? null;
917
+ }
918
+ async function appToken(net) {
919
+ const response = await net("https://www.reddit.com/api/v1/access_token", {
920
+ body: "grant_type=https://oauth.reddit.com/grants/installed_client&device_id=DO_NOT_TRACK_THIS_DEVICE",
921
+ headers: {
922
+ authorization: `Basic ${btoa(`${installedClientId}:`)}`,
923
+ "content-type": "application/x-www-form-urlencoded",
924
+ "user-agent": browserUserAgent()
925
+ },
926
+ method: "POST"
927
+ });
928
+ if (!response.ok) {
929
+ throw new Error(`Reddit auth failed: ${response.status}`);
930
+ }
931
+ const payload = await response.json().catch(() => null);
932
+ const token = object(payload) ? string(payload.access_token) : null;
933
+ if (!token) {
934
+ throw new Error("Reddit token not found");
935
+ }
936
+ return token;
937
+ }
938
+ async function fetchPost(net, token, id) {
939
+ const response = await net(`https://oauth.reddit.com/comments/${id}?raw_json=1&limit=1`, {
940
+ headers: { authorization: `Bearer ${token}`, "user-agent": browserUserAgent() }
941
+ });
942
+ if (!response.ok) {
943
+ throw new Error(`Reddit post failed: ${response.status}`);
944
+ }
945
+ const payload = await response.json().catch(() => null);
946
+ const listing = Array.isArray(payload) ? payload[0] : null;
947
+ const children = object(listing) && object(listing.data) && Array.isArray(listing.data.children) ? listing.data.children : [];
948
+ const first = children[0];
949
+ const post = object(first) && object(first.data) ? first.data : null;
950
+ if (!post) {
951
+ throw new Error("Reddit post not found");
952
+ }
953
+ return post;
954
+ }
955
+ async function mediaItems3(input, post, id) {
956
+ const own = await extract(input, post, id);
957
+ if (own.length > 0) {
958
+ return own;
959
+ }
960
+ const parents = Array.isArray(post.crosspost_parent_list) ? post.crosspost_parent_list.filter(object) : [];
961
+ return parents[0] ? extract(input, parents[0], id) : [];
962
+ }
963
+ async function extract(input, post, id) {
964
+ const gallery = galleryItems(post, id);
965
+ if (gallery.length > 0) {
966
+ return gallery;
967
+ }
968
+ const video = await videoItems(input, post, id);
969
+ if (video.length > 0) {
970
+ return video;
971
+ }
972
+ return imageItems2(post, id);
973
+ }
974
+ function galleryItems(post, id) {
975
+ const data = object(post.gallery_data) ? post.gallery_data : null;
976
+ const order = data && Array.isArray(data.items) ? data.items.filter(object) : [];
977
+ const metadata = object(post.media_metadata) ? post.media_metadata : {};
978
+ return order.flatMap((entry, index) => {
979
+ const mediaId2 = string(entry.media_id);
980
+ const meta = mediaId2 && object(metadata[mediaId2]) ? metadata[mediaId2] : null;
981
+ const best = meta && object(meta.s) ? meta.s : null;
982
+ if (!best) {
983
+ return [];
984
+ }
985
+ const video = string(best.mp4);
986
+ if (video) {
987
+ return [mediaItem(id, index + 1, "video", "video/mp4", video)];
988
+ }
989
+ const image = string(best.u) ?? string(best.gif);
990
+ return image ? [mediaItem(id, index + 1, "image", mimeOf(string(meta?.m), image), image)] : [];
991
+ });
992
+ }
993
+ async function videoItems(input, post, id) {
994
+ const media = redditVideo(post);
995
+ const fallback = media ? string(media.fallback_url) : null;
996
+ if (!media || !fallback) {
997
+ return [];
998
+ }
999
+ if (media.has_audio !== true) {
1000
+ return [mediaItem(id, null, "video", "video/mp4", fallback)];
1001
+ }
1002
+ const dashUrl = string(media.dash_url);
1003
+ const streams = dashUrl ? await dashStreams(input.net, dashUrl, input.preferredWidth) : null;
1004
+ if (!streams) {
1005
+ throw new Error("Reddit DASH manifest unavailable for muxing");
1006
+ }
1007
+ return [muxedVideoItem(id, streams.video, streams.audio)];
1008
+ }
1009
+ async function dashStreams(net, dashUrl, preferredWidth) {
1010
+ const response = await net(dashUrl, { headers: { "user-agent": browserUserAgent() } });
1011
+ if (!response.ok) {
1012
+ return null;
1013
+ }
1014
+ const manifest = await response.text();
1015
+ const base = dashUrl.slice(0, dashUrl.lastIndexOf("/") + 1);
1016
+ const query = dashUrl.includes("?") ? dashUrl.slice(dashUrl.indexOf("?")) : "";
1017
+ const video = videoRendition(manifest, preferredWidth);
1018
+ const audio = audioRendition(manifest);
1019
+ if (!video || !audio) {
1020
+ return null;
1021
+ }
1022
+ return { video: `${base}${video}${query}`, audio: `${base}${audio}${query}` };
1023
+ }
1024
+ function videoRendition(manifest, preferredWidth) {
1025
+ const renditions = renditionsOf(manifest, false);
1026
+ const sized = renditions.filter((rendition) => rendition.width !== null);
1027
+ const pool = sized.length > 0 ? sized : renditions;
1028
+ const best = pool.reduce((current, rendition) => {
1029
+ if (!current) {
1030
+ return rendition;
1031
+ }
1032
+ if (rendition.width !== null && current.width !== null) {
1033
+ return Math.abs(rendition.width - preferredWidth) < Math.abs(current.width - preferredWidth) ? rendition : current;
1034
+ }
1035
+ return rendition.bandwidth > current.bandwidth ? rendition : current;
1036
+ }, null);
1037
+ return best ? best.base : null;
1038
+ }
1039
+ function audioRendition(manifest) {
1040
+ const best = renditionsOf(manifest, true).reduce((current, rendition) => !current || rendition.bandwidth > current.bandwidth ? rendition : current, null);
1041
+ return best ? best.base : null;
1042
+ }
1043
+ function renditionsOf(manifest, audio) {
1044
+ return manifest.split("<AdaptationSet").slice(1).filter((chunk) => isAudioSet(chunk) === audio).flatMap((chunk) => [...chunk.matchAll(/<Representation\b([^>]*)>([\s\S]*?)<\/Representation>/g)].flatMap((match) => {
1045
+ const segment = match[2].match(/<BaseURL>([^<]+)<\/BaseURL>/)?.[1];
1046
+ if (!segment) {
1047
+ return [];
1048
+ }
1049
+ const width = match[1].match(/(?:^|[^a-zA-Z])width="(\d+)"/)?.[1];
1050
+ return [{ width: width ? Number(width) : null, bandwidth: Number(match[1].match(/bandwidth="(\d+)"/)?.[1] ?? 0), base: segment }];
1051
+ }));
1052
+ }
1053
+ function isAudioSet(chunk) {
1054
+ const head = chunk.slice(0, chunk.indexOf(">"));
1055
+ return /contentType="audio"/.test(head) || /mimeType="audio/.test(head);
1056
+ }
1057
+ function muxedVideoItem(id, videoUrl, audioUrl) {
1058
+ return {
1059
+ audio: { headers: { "user-agent": browserUserAgent() }, url: audioUrl },
1060
+ filename: filename(`reddit_${id}.mp4`),
1061
+ headers: { "user-agent": browserUserAgent() },
1062
+ id,
1063
+ kind: "video",
1064
+ mime: "video/mp4",
1065
+ platform: "reddit",
1066
+ url: videoUrl
1067
+ };
1068
+ }
1069
+ function redditVideo(post) {
1070
+ const secure = object(post.secure_media) ? post.secure_media : null;
1071
+ const media = object(post.media) ? post.media : null;
1072
+ const fromSecure = secure && object(secure.reddit_video) ? secure.reddit_video : null;
1073
+ const fromMedia = media && object(media.reddit_video) ? media.reddit_video : null;
1074
+ return fromSecure ?? fromMedia;
1075
+ }
1076
+ function imageItems2(post, id) {
1077
+ const url = string(post.url_overridden_by_dest) ?? string(post.url);
1078
+ if (!url) {
1079
+ return [];
1080
+ }
1081
+ const path = asUrl(url).pathname;
1082
+ const isImage = string(post.post_hint) === "image" || asUrl(url).hostname === "i.redd.it" || /\.(jpe?g|png|webp|gif)$/i.test(path);
1083
+ return isImage ? [mediaItem(id, null, "image", mimeOf(null, url), url)] : [];
1084
+ }
1085
+ function mediaItem(id, index, kind, mime, url) {
1086
+ const suffix = index === null ? "" : `_${index}`;
1087
+ return {
1088
+ filename: filename(`reddit_${id}${suffix}.${extensionOf(mime)}`),
1089
+ headers: { "user-agent": browserUserAgent() },
1090
+ id,
1091
+ kind,
1092
+ mime,
1093
+ platform: "reddit",
1094
+ url
1095
+ };
1096
+ }
1097
+ function mimeOf(declared, url) {
1098
+ if (declared && declared.includes("/")) {
1099
+ return declared.replace("image/jpg", "image/jpeg");
1100
+ }
1101
+ const extension = asUrl(url).pathname.match(/\.([a-z0-9]+)$/i)?.[1]?.toLowerCase();
1102
+ if (extension === "png") {
1103
+ return "image/png";
1104
+ }
1105
+ if (extension === "webp") {
1106
+ return "image/webp";
1107
+ }
1108
+ if (extension === "gif") {
1109
+ return "image/gif";
1110
+ }
1111
+ return "image/jpeg";
1112
+ }
1113
+ function extensionOf(mime) {
1114
+ if (mime === "video/mp4") {
1115
+ return "mp4";
1116
+ }
1117
+ if (mime === "image/png") {
1118
+ return "png";
1119
+ }
1120
+ if (mime === "image/webp") {
1121
+ return "webp";
1122
+ }
1123
+ if (mime === "image/gif") {
1124
+ return "gif";
1125
+ }
1126
+ return "jpg";
1127
+ }
1128
+
1129
+ // src/soundcloud.ts
1130
+ async function resolveSoundcloud(input) {
1131
+ const url = await canonicalUrl2(input.net, input.url);
1132
+ const clientId = await appClientId(input.net);
1133
+ const track = await resolveTrack(input.net, url, clientId);
1134
+ const id = trackId(track);
1135
+ const item = await audioItem(input.net, track, id, clientId);
1136
+ return { archiveFilename: filename(`soundcloud_${id}.zip`), id, items: [item], platform: "soundcloud" };
1137
+ }
1138
+ async function canonicalUrl2(net, input) {
1139
+ const host = asUrl(input).hostname;
1140
+ if (host !== "on.soundcloud.com" && host !== "snd.sc") {
1141
+ return input;
1142
+ }
1143
+ const response = await net(input, { headers: { "user-agent": browserUserAgent() } }, 1);
1144
+ return response.url;
1145
+ }
1146
+ async function appClientId(net) {
1147
+ const home = await net("https://soundcloud.com/", { headers: { "user-agent": browserUserAgent() } });
1148
+ if (!home.ok) {
1149
+ throw new Error(`SoundCloud home failed: ${home.status}`);
1150
+ }
1151
+ const html = await home.text();
1152
+ const scripts = [...html.matchAll(/<script[^>]+src="(https:\/\/a-v2\.sndcdn\.com\/assets\/[^"]+\.js)"/g)].map((match) => match[1]);
1153
+ for (const src of scripts.reverse()) {
1154
+ const response = await net(src, { headers: { "user-agent": browserUserAgent() } }, 1);
1155
+ if (!response.ok) {
1156
+ continue;
1157
+ }
1158
+ const code = await response.text();
1159
+ const id = code.match(/client_id\s*[:=]\s*"([A-Za-z0-9]{20,})"/)?.[1];
1160
+ if (id) {
1161
+ return id;
1162
+ }
1163
+ }
1164
+ throw new Error("SoundCloud client id not found");
1165
+ }
1166
+ async function resolveTrack(net, url, clientId) {
1167
+ const api = new URL("https://api-v2.soundcloud.com/resolve");
1168
+ api.searchParams.set("url", url);
1169
+ api.searchParams.set("client_id", clientId);
1170
+ const response = await net(api.href, { headers: { accept: "application/json", "user-agent": browserUserAgent() } });
1171
+ if (!response.ok) {
1172
+ throw new Error(`SoundCloud resolve failed: ${response.status}`);
1173
+ }
1174
+ const payload = await response.json().catch(() => null);
1175
+ if (!object(payload) || string(payload.kind) !== "track") {
1176
+ throw new Error("SoundCloud track not found");
1177
+ }
1178
+ return payload;
1179
+ }
1180
+ async function audioItem(net, track, id, clientId) {
1181
+ const media = object(track.media) ? track.media : null;
1182
+ const transcodings = media && Array.isArray(media.transcodings) ? media.transcodings.filter(object) : [];
1183
+ const progressive = transcodings.find((transcoding) => protocol(transcoding) === "progressive");
1184
+ const hls = transcodings.find((transcoding) => protocol(transcoding) === "hls" && mimeType(transcoding)?.includes("mp4") === true);
1185
+ const chosen = progressive ?? hls;
1186
+ if (!chosen) {
1187
+ throw new Error("SoundCloud track has no downloadable stream");
1188
+ }
1189
+ const stream = await streamUrl(net, chosen, clientId);
1190
+ const title = string(track.title) ?? id;
1191
+ const headers = { "user-agent": browserUserAgent() };
1192
+ if (chosen === progressive) {
1193
+ return { filename: filename(`soundcloud_${title}.mp3`), headers, id, kind: "audio", mime: "audio/mpeg", platform: "soundcloud", url: stream };
1194
+ }
1195
+ return { filename: filename(`soundcloud_${title}.m4a`), headers, hls: true, id, kind: "audio", mime: "audio/mp4", platform: "soundcloud", url: stream };
1196
+ }
1197
+ async function streamUrl(net, transcoding, clientId) {
1198
+ const endpoint = string(transcoding.url);
1199
+ if (!endpoint) {
1200
+ throw new Error("SoundCloud stream endpoint not found");
1201
+ }
1202
+ const url = new URL(endpoint);
1203
+ url.searchParams.set("client_id", clientId);
1204
+ const response = await net(url.href, { headers: { accept: "application/json", "user-agent": browserUserAgent() } });
1205
+ if (!response.ok) {
1206
+ throw new Error(`SoundCloud stream failed: ${response.status}`);
1207
+ }
1208
+ const payload = await response.json().catch(() => null);
1209
+ const stream = object(payload) ? string(payload.url) : null;
1210
+ if (!stream) {
1211
+ throw new Error("SoundCloud stream url not found");
1212
+ }
1213
+ return stream;
1214
+ }
1215
+ function protocol(transcoding) {
1216
+ const format = object(transcoding.format) ? transcoding.format : null;
1217
+ return format ? string(format.protocol) : null;
1218
+ }
1219
+ function mimeType(transcoding) {
1220
+ const format = object(transcoding.format) ? transcoding.format : null;
1221
+ return format ? string(format.mime_type) : null;
1222
+ }
1223
+ function trackId(track) {
1224
+ const numeric = number(track.id);
1225
+ if (numeric !== null) {
1226
+ return String(numeric);
1227
+ }
1228
+ return string(track.permalink) ?? "track";
1229
+ }
1230
+
523
1231
  // src/tiktok.ts
524
1232
  var marker = '<script id="__UNIVERSAL_DATA_FOR_REHYDRATION__" type="application/json">';
525
1233
  async function resolveTiktok(input) {
@@ -543,7 +1251,7 @@ async function resolveTiktok(input) {
543
1251
  if (page.cookie) {
544
1252
  headers.cookie = page.cookie;
545
1253
  }
546
- const items = mediaItems2(page.item, user, id, headers);
1254
+ const items = mediaItems4(page.item, user, id, headers);
547
1255
  if (items.length === 0) {
548
1256
  throw new Error("TikTok media not found");
549
1257
  }
@@ -603,10 +1311,10 @@ function downloadUrl(item) {
603
1311
  const video = object(item.video) ? item.video : null;
604
1312
  return video ? string(video.playAddr) ?? string(video.downloadAddr) : null;
605
1313
  }
606
- function mediaItems2(item, user, id, headers) {
607
- const images = imageItems(item, user, id, headers);
1314
+ function mediaItems4(item, user, id, headers) {
1315
+ const images = imageItems3(item, user, id, headers);
608
1316
  if (images.length > 0) {
609
- const audio = audioItem(item, user, id, headers);
1317
+ const audio = audioItem2(item, user, id, headers);
610
1318
  return audio ? [...images, audio] : images;
611
1319
  }
612
1320
  const url = downloadUrl(item);
@@ -620,7 +1328,7 @@ function mediaItems2(item, user, id, headers) {
620
1328
  url
621
1329
  }] : [];
622
1330
  }
623
- function imageItems(item, user, id, headers) {
1331
+ function imageItems3(item, user, id, headers) {
624
1332
  const imagePost = object(item.imagePost) ? item.imagePost : null;
625
1333
  const images = imagePost && Array.isArray(imagePost.images) ? imagePost.images.filter(object) : [];
626
1334
  return images.flatMap((image, index) => {
@@ -638,7 +1346,7 @@ function imageItems(item, user, id, headers) {
638
1346
  }] : [];
639
1347
  });
640
1348
  }
641
- function audioItem(item, user, id, headers) {
1349
+ function audioItem2(item, user, id, headers) {
642
1350
  const video = object(item.video) ? item.video : null;
643
1351
  const music = object(item.music) ? item.music : null;
644
1352
  const url = video ? string(video.playAddr) : null;
@@ -756,22 +1464,64 @@ async function resolveYoutube(input) {
756
1464
  const reason = object(payload) && object(payload.playabilityStatus) ? string(payload.playabilityStatus.reason) : null;
757
1465
  throw new Error(reason ?? "YouTube video unavailable");
758
1466
  }
759
- const format = selectFormat(payload);
760
- if (!format) {
761
- throw new Error("YouTube progressive mp4 not found");
1467
+ const streams = selectStreams(payload, input.preferredWidth);
1468
+ if (!streams) {
1469
+ throw new Error("YouTube mp4 stream not found");
762
1470
  }
763
1471
  const title = object(payload) && object(payload.videoDetails) ? string(payload.videoDetails.title) : null;
1472
+ const headers = { "user-agent": androidVrClient.userAgent };
764
1473
  const media = {
765
1474
  filename: filename(`youtube_${title ?? id}_${id}.mp4`),
766
- headers: { "user-agent": androidVrClient.userAgent },
1475
+ headers,
767
1476
  id,
768
1477
  kind: "video",
769
1478
  mime: "video/mp4",
770
1479
  platform: "youtube",
771
- url: format
1480
+ url: streams.video,
1481
+ ...streams.audio ? { audio: { headers, url: streams.audio } } : {}
772
1482
  };
773
1483
  return { archiveFilename: filename(`youtube_${id}.zip`), id, items: [media], platform: "youtube" };
774
1484
  }
1485
+ function selectStreams(payload, preferredWidth) {
1486
+ const adaptive = adaptiveFormats(payload);
1487
+ const video = bestVideo(adaptive, preferredWidth);
1488
+ const audio = bestAudio(adaptive);
1489
+ if (video && audio) {
1490
+ return { video, audio };
1491
+ }
1492
+ const progressive = selectFormat(payload);
1493
+ return progressive ? { video: progressive, audio: null } : null;
1494
+ }
1495
+ function adaptiveFormats(payload) {
1496
+ const root = object(payload) ? payload : null;
1497
+ const streaming = root && object(root.streamingData) ? root.streamingData : null;
1498
+ return streaming && Array.isArray(streaming.adaptiveFormats) ? streaming.adaptiveFormats.filter(object) : [];
1499
+ }
1500
+ function bestVideo(formats, preferredWidth) {
1501
+ const candidates = formats.filter((format) => {
1502
+ const mimeType2 = string(format.mimeType);
1503
+ return Boolean(string(format.url)) && mimeType2?.startsWith("video/mp4") === true && mimeType2.includes("avc1");
1504
+ });
1505
+ const best = candidates.reduce((current, format) => {
1506
+ if (!current) {
1507
+ return format;
1508
+ }
1509
+ const width = number(format.width) ?? 0;
1510
+ const currentWidth = number(current.width) ?? 0;
1511
+ return Math.abs(width - preferredWidth) < Math.abs(currentWidth - preferredWidth) ? format : current;
1512
+ }, null);
1513
+ return best ? string(best.url) : null;
1514
+ }
1515
+ function bestAudio(formats) {
1516
+ const candidates = formats.filter((format) => Boolean(string(format.url)) && string(format.mimeType)?.startsWith("audio/mp4") === true);
1517
+ const best = candidates.reduce((current, format) => {
1518
+ if (!current) {
1519
+ return format;
1520
+ }
1521
+ return (number(format.bitrate) ?? 0) > (number(current.bitrate) ?? 0) ? format : current;
1522
+ }, null);
1523
+ return best ? string(best.url) : null;
1524
+ }
775
1525
  function playerBody(id, session) {
776
1526
  return {
777
1527
  contentCheckOk: true,
@@ -904,6 +1654,12 @@ async function postfetch(url, options = {}) {
904
1654
  return resolveFacebook(context);
905
1655
  case "instagram":
906
1656
  return resolveInstagram(context);
1657
+ case "pinterest":
1658
+ return resolvePinterest(context);
1659
+ case "reddit":
1660
+ return resolveReddit(context);
1661
+ case "soundcloud":
1662
+ return resolveSoundcloud(context);
907
1663
  case "tiktok":
908
1664
  return resolveTiktok(context);
909
1665
  case "twitter":
@@ -920,6 +1676,15 @@ function detect(url) {
920
1676
  if (host.includes("instagram.com")) {
921
1677
  return "instagram";
922
1678
  }
1679
+ if (host.includes("pinterest.") || host === "pin.it") {
1680
+ return "pinterest";
1681
+ }
1682
+ if (host === "reddit.com" || host.endsWith(".reddit.com") || host === "redd.it") {
1683
+ return "reddit";
1684
+ }
1685
+ if (host.includes("soundcloud.com") || host === "snd.sc") {
1686
+ return "soundcloud";
1687
+ }
923
1688
  if (host.includes("youtube.com") || host === "youtu.be") {
924
1689
  return "youtube";
925
1690
  }
@@ -929,8 +1694,135 @@ function detect(url) {
929
1694
  if (host === "x.com" || host.endsWith(".x.com") || host.includes("twitter.com")) {
930
1695
  return "twitter";
931
1696
  }
932
- throw new PostfetchError(400, "only Facebook, Instagram, TikTok, X and YouTube URLs are supported");
1697
+ throw new PostfetchError(400, "only Facebook, Instagram, Pinterest, Reddit, SoundCloud, TikTok, X and YouTube URLs are supported");
1698
+ }
1699
+ // src/remux.ts
1700
+ function mergeAudioVideo(video, audio) {
1701
+ const videoSource = analyze(video);
1702
+ const audioSource = analyze(audio);
1703
+ const audioTrackId = videoSource.trackId === 1 ? 2 : 1;
1704
+ const moov = buildMoov(videoSource, audioSource, audioTrackId);
1705
+ const parts = [sliceBox(video, videoSource.ftyp), moov];
1706
+ let sequence = 1;
1707
+ const fragmentCount = Math.max(videoSource.fragments.length, audioSource.fragments.length);
1708
+ for (let index = 0;index < fragmentCount; index += 1) {
1709
+ const videoFragment = videoSource.fragments[index];
1710
+ if (videoFragment) {
1711
+ parts.push(rewriteMoof(video, videoFragment.moof, null, sequence));
1712
+ parts.push(video.subarray(videoFragment.mdat.start, videoFragment.mdat.end));
1713
+ sequence += 1;
1714
+ }
1715
+ const audioFragment = audioSource.fragments[index];
1716
+ if (audioFragment) {
1717
+ parts.push(rewriteMoof(audio, audioFragment.moof, audioTrackId, sequence));
1718
+ parts.push(audio.subarray(audioFragment.mdat.start, audioFragment.mdat.end));
1719
+ sequence += 1;
1720
+ }
1721
+ }
1722
+ return concat(parts);
1723
+ }
1724
+ function analyze(data) {
1725
+ const top = parseBoxes(data);
1726
+ const ftyp = findBox(top, "ftyp");
1727
+ const moov = findBox(top, "moov");
1728
+ if (!ftyp || !moov) {
1729
+ throw new Error("not a fragmented MP4");
1730
+ }
1731
+ const moovChildren = childBoxes(data, moov);
1732
+ const trak = findBox(moovChildren, "trak");
1733
+ const mvex = findBox(moovChildren, "mvex");
1734
+ if (!trak || !mvex) {
1735
+ throw new Error("MP4 is not fragmented (no trak/mvex)");
1736
+ }
1737
+ const tkhd = findBox(childBoxes(data, trak), "tkhd");
1738
+ if (!tkhd) {
1739
+ throw new Error("track header not found");
1740
+ }
1741
+ const fragments = [];
1742
+ for (let index = 0;index < top.length; index += 1) {
1743
+ if (top[index].type === "moof") {
1744
+ const mdat = top[index + 1];
1745
+ if (!mdat || mdat.type !== "mdat") {
1746
+ throw new Error("fragment without mdat");
1747
+ }
1748
+ fragments.push({ moof: top[index], mdat });
1749
+ }
1750
+ }
1751
+ if (fragments.length === 0) {
1752
+ throw new Error("MP4 has no fragments");
1753
+ }
1754
+ return { data, ftyp, moov, trak, mvex, trackId: trackHeaderId(data, tkhd), fragments };
1755
+ }
1756
+ function trackHeaderId(data, tkhd) {
1757
+ return readU32(data, tkhd.dataStart + (boxVersion(data, tkhd) === 1 ? 20 : 12));
1758
+ }
1759
+ function buildMoov(video, audio, audioTrackId) {
1760
+ const moovChildren = childBoxes(video.data, video.moov);
1761
+ const mvhd = patchedMvhd(video, audioTrackId);
1762
+ const meta = findBox(moovChildren, "meta");
1763
+ const videoTrak = sliceBox(video.data, video.trak);
1764
+ const audioTrak = retrackTrak(sliceBox(audio.data, audio.trak), audioTrackId);
1765
+ const mvex = buildMvex(video, audio, audioTrackId);
1766
+ const children = [mvhd];
1767
+ if (meta) {
1768
+ children.push(sliceBox(video.data, meta));
1769
+ }
1770
+ children.push(videoTrak, audioTrak, mvex);
1771
+ return buildBox("moov", concat(children));
1772
+ }
1773
+ function patchedMvhd(video, audioTrackId) {
1774
+ const mvhd = findBox(childBoxes(video.data, video.moov), "mvhd");
1775
+ if (!mvhd) {
1776
+ throw new Error("movie header not found");
1777
+ }
1778
+ const copy = sliceBox(video.data, mvhd);
1779
+ writeU32(copy, copy.length - 4, Math.max(video.trackId, audioTrackId) + 1);
1780
+ return copy;
1781
+ }
1782
+ function buildMvex(video, audio, audioTrackId) {
1783
+ const videoMvex = childBoxes(video.data, video.mvex);
1784
+ const mehd = findBox(videoMvex, "mehd");
1785
+ const videoTrex = findBox(videoMvex, "trex");
1786
+ const audioTrex = findBox(childBoxes(audio.data, audio.mvex), "trex");
1787
+ if (!videoTrex || !audioTrex) {
1788
+ throw new Error("track extends box not found");
1789
+ }
1790
+ const audioTrexCopy = sliceBox(audio.data, audioTrex);
1791
+ writeU32(audioTrexCopy, audioTrex.dataStart - audioTrex.start + 4, audioTrackId);
1792
+ const children = [];
1793
+ if (mehd) {
1794
+ children.push(sliceBox(video.data, mehd));
1795
+ }
1796
+ children.push(sliceBox(video.data, videoTrex), audioTrexCopy);
1797
+ return buildBox("mvex", concat(children));
1798
+ }
1799
+ function retrackTrak(trak, trackId2) {
1800
+ const tkhd = findBox(parseBoxes(trak, 8, trak.length), "tkhd");
1801
+ if (!tkhd) {
1802
+ throw new Error("track header not found");
1803
+ }
1804
+ writeU32(trak, tkhd.dataStart + (boxVersion(trak, tkhd) === 1 ? 20 : 12), trackId2);
1805
+ return trak;
1806
+ }
1807
+ function rewriteMoof(data, moofBox, trackId2, sequence) {
1808
+ const moof = sliceBox(data, moofBox);
1809
+ const moofChildren = parseBoxes(moof, 8, moof.length);
1810
+ const mfhd = findBox(moofChildren, "mfhd");
1811
+ if (!mfhd) {
1812
+ throw new Error("movie fragment header not found");
1813
+ }
1814
+ writeU32(moof, mfhd.dataStart + 4, sequence);
1815
+ if (trackId2 !== null) {
1816
+ const traf = findBox(moofChildren, "traf");
1817
+ const tfhd = traf ? findBox(parseBoxes(moof, traf.dataStart, traf.end), "tfhd") : undefined;
1818
+ if (!tfhd) {
1819
+ throw new Error("track fragment header not found");
1820
+ }
1821
+ writeU32(moof, tfhd.dataStart + 4, trackId2);
1822
+ }
1823
+ return moof;
933
1824
  }
1825
+
934
1826
  // src/zip.ts
935
1827
  var encoder = new TextEncoder;
936
1828
  var table = new Uint32Array(256);
@@ -1024,8 +1916,15 @@ function crc32(data) {
1024
1916
  }
1025
1917
 
1026
1918
  // src/download.ts
1919
+ function buffered(item) {
1920
+ return Boolean(item.audio) || item.hls === true;
1921
+ }
1027
1922
  async function download(item, options = {}) {
1028
1923
  const net = createNet(options.fetch ?? globalThis.fetch);
1924
+ if (buffered(item)) {
1925
+ const bytes = await itemBytes(net, item);
1926
+ return new Response(toArrayBuffer(bytes), { headers: { "content-length": String(bytes.length), "content-type": item.mime } });
1927
+ }
1029
1928
  const response = await net(item.url, { headers: item.headers });
1030
1929
  if (!response.ok || !response.body) {
1031
1930
  throw new PostfetchError(502, `download failed: ${response.status}`);
@@ -1034,13 +1933,7 @@ async function download(item, options = {}) {
1034
1933
  }
1035
1934
  async function archive(result, options = {}) {
1036
1935
  const net = createNet(options.fetch ?? globalThis.fetch);
1037
- const files = await Promise.all(result.items.map(async (item) => {
1038
- const response = await net(item.url, { headers: item.headers });
1039
- if (!response.ok) {
1040
- throw new PostfetchError(502, `download failed: ${response.status}`);
1041
- }
1042
- return { data: new Uint8Array(await response.arrayBuffer()), name: item.filename };
1043
- }));
1936
+ const files = await Promise.all(result.items.map(async (item) => ({ data: await itemBytes(net, item), name: item.filename })));
1044
1937
  return { bytes: zip(files), filename: result.archiveFilename, mime: "application/zip" };
1045
1938
  }
1046
1939
  async function toResponse(result, options = {}) {
@@ -1050,6 +1943,21 @@ async function toResponse(result, options = {}) {
1050
1943
  return archiveResponse(result, options);
1051
1944
  }
1052
1945
  async function singleResponse(item, options) {
1946
+ if (buffered(item)) {
1947
+ const net = createNet(options.fetch ?? globalThis.fetch);
1948
+ const bytes = await itemBytes(net, item);
1949
+ return new Response(toArrayBuffer(bytes), {
1950
+ headers: {
1951
+ "content-disposition": `attachment; filename="${item.filename}"`,
1952
+ "content-length": String(bytes.length),
1953
+ "content-type": item.mime,
1954
+ "x-media-count": "1",
1955
+ "x-media-id": item.id,
1956
+ "x-media-kind": item.kind,
1957
+ "x-media-platform": item.platform
1958
+ }
1959
+ });
1960
+ }
1053
1961
  const media = await download(item, options);
1054
1962
  const headers = new Headers({
1055
1963
  "content-disposition": `attachment; filename="${item.filename}"`,
@@ -1067,8 +1975,7 @@ async function singleResponse(item, options) {
1067
1975
  }
1068
1976
  async function archiveResponse(result, options) {
1069
1977
  const { bytes, filename: filename2 } = await archive(result, options);
1070
- const body = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
1071
- return new Response(body, {
1978
+ return new Response(toArrayBuffer(bytes), {
1072
1979
  headers: {
1073
1980
  "content-disposition": `attachment; filename="${filename2}"`,
1074
1981
  "content-length": String(bytes.length),
@@ -1079,6 +1986,36 @@ async function archiveResponse(result, options) {
1079
1986
  }
1080
1987
  });
1081
1988
  }
1989
+ async function itemBytes(net, item) {
1990
+ if (item.hls) {
1991
+ const video = await assembleHls(net, item.url, item.headers);
1992
+ if (item.audio) {
1993
+ return mergeAudioVideo(video, await assembleHls(net, item.audio.url, item.audio.headers));
1994
+ }
1995
+ return video;
1996
+ }
1997
+ if (item.audio) {
1998
+ return mergedBytes(net, item, item.audio);
1999
+ }
2000
+ return fetchBytes(net, item.url, item.headers);
2001
+ }
2002
+ async function mergedBytes(net, item, audio) {
2003
+ const [video, audioBytes] = await Promise.all([
2004
+ fetchBytes(net, item.url, item.headers),
2005
+ fetchBytes(net, audio.url, audio.headers)
2006
+ ]);
2007
+ return mergeAudioVideo(video, audioBytes);
2008
+ }
2009
+ async function fetchBytes(net, url, headers) {
2010
+ const response = await net(url, { headers });
2011
+ if (!response.ok) {
2012
+ throw new PostfetchError(502, `download failed: ${response.status}`);
2013
+ }
2014
+ return new Uint8Array(await response.arrayBuffer());
2015
+ }
2016
+ function toArrayBuffer(bytes) {
2017
+ return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
2018
+ }
1082
2019
  export {
1083
2020
  toResponse,
1084
2021
  postfetch,