@postfetch/core 0.9.0 → 0.9.1
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/download.d.ts +1 -0
- package/dist/index.js +53 -1
- package/dist/internal.d.ts +2 -2
- package/package.json +1 -1
- package/src/download.ts +1 -0
- package/src/internal.ts +2 -2
- package/src/twitter.ts +63 -1
package/dist/download.d.ts
CHANGED
|
@@ -73,6 +73,7 @@ export declare function download(item: MediaItem, options?: DownloadOptions): Pr
|
|
|
73
73
|
export declare function downloadBlob(url: string, options: DownloadBlobOptions & {
|
|
74
74
|
remux: true;
|
|
75
75
|
}): Promise<RemuxedVideo>;
|
|
76
|
+
/** Download without remuxing and return the upstream media Blob. */
|
|
76
77
|
export declare function downloadBlob(url: string, options?: DownloadBlobOptions & {
|
|
77
78
|
remux?: false;
|
|
78
79
|
}): Promise<Blob>;
|
package/dist/index.js
CHANGED
|
@@ -2009,6 +2009,7 @@ async function resolveTwitter(input) {
|
|
|
2009
2009
|
if (string(tweet.__typename) === "TweetTombstone") {
|
|
2010
2010
|
throw new Error("Tweet is unavailable or age-restricted");
|
|
2011
2011
|
}
|
|
2012
|
+
await expandTwitterText(input.net, tweet, id);
|
|
2012
2013
|
const groups = twitterTweets(tweet, id).flatMap(({ id: postId2, tweet: post }) => {
|
|
2013
2014
|
const media = Array.isArray(post.mediaDetails) ? post.mediaDetails.filter(object) : [];
|
|
2014
2015
|
return media.flatMap((entry, index) => {
|
|
@@ -2032,7 +2033,7 @@ function twitterMetadata(tweet) {
|
|
|
2032
2033
|
extra.quotedTweet = { id: quotedId, metadata: twitterMetadata(quotedTweet) };
|
|
2033
2034
|
}
|
|
2034
2035
|
return {
|
|
2035
|
-
text:
|
|
2036
|
+
text: twitterText(tweet) ?? undefined,
|
|
2036
2037
|
author: user ? {
|
|
2037
2038
|
handle: string(user.screen_name) ?? undefined,
|
|
2038
2039
|
name: string(user.name) ?? undefined,
|
|
@@ -2047,6 +2048,57 @@ function twitterMetadata(tweet) {
|
|
|
2047
2048
|
extra
|
|
2048
2049
|
};
|
|
2049
2050
|
}
|
|
2051
|
+
function noteText(tweet) {
|
|
2052
|
+
const note = object(tweet.note_tweet) ? tweet.note_tweet : null;
|
|
2053
|
+
const results = note && object(note.note_tweet_results) ? note.note_tweet_results : null;
|
|
2054
|
+
const result = results && object(results.result) ? results.result : null;
|
|
2055
|
+
return (result ? string(result.text) : null) ?? (note ? string(note.text) : null);
|
|
2056
|
+
}
|
|
2057
|
+
function twitterText(tweet) {
|
|
2058
|
+
return noteText(tweet) ?? string(tweet.full_text) ?? string(tweet.text);
|
|
2059
|
+
}
|
|
2060
|
+
function needsFullText(tweet) {
|
|
2061
|
+
if (noteText(tweet) || string(tweet.full_text)) {
|
|
2062
|
+
return false;
|
|
2063
|
+
}
|
|
2064
|
+
return object(tweet.note_tweet) || bool(tweet.truncated) === true || (string(tweet.text)?.length ?? 0) >= 270;
|
|
2065
|
+
}
|
|
2066
|
+
async function expandTwitterText(net, tweet, rootId) {
|
|
2067
|
+
const posts = new Map(twitterTweets(tweet, rootId).map(({ id, tweet: post }) => [id, post]));
|
|
2068
|
+
for (const [id, post] of posts) {
|
|
2069
|
+
if (!needsFullText(post)) {
|
|
2070
|
+
continue;
|
|
2071
|
+
}
|
|
2072
|
+
try {
|
|
2073
|
+
const response = await net(`https://api.fxtwitter.com/status/${id}`, {
|
|
2074
|
+
headers: { accept: "application/json", "user-agent": browserUserAgent() }
|
|
2075
|
+
}, 1);
|
|
2076
|
+
if (!response.ok) {
|
|
2077
|
+
continue;
|
|
2078
|
+
}
|
|
2079
|
+
const payload = await response.json();
|
|
2080
|
+
if (!object(payload) || payload.code !== 200 || !object(payload.tweet) || string(payload.tweet.id) !== id) {
|
|
2081
|
+
continue;
|
|
2082
|
+
}
|
|
2083
|
+
let current = payload.tweet;
|
|
2084
|
+
const seen = new Set;
|
|
2085
|
+
while (current) {
|
|
2086
|
+
const currentId = string(current.id);
|
|
2087
|
+
if (!currentId || seen.has(currentId)) {
|
|
2088
|
+
break;
|
|
2089
|
+
}
|
|
2090
|
+
seen.add(currentId);
|
|
2091
|
+
const target = posts.get(currentId);
|
|
2092
|
+
const raw = object(current.raw_text) ? string(current.raw_text.text) : null;
|
|
2093
|
+
const text = raw ?? string(current.text);
|
|
2094
|
+
if (target && needsFullText(target) && text && text.length > (twitterText(target)?.length ?? 0)) {
|
|
2095
|
+
target.full_text = text;
|
|
2096
|
+
}
|
|
2097
|
+
current = object(current.quote) ? current.quote : null;
|
|
2098
|
+
}
|
|
2099
|
+
} catch {}
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2050
2102
|
function twitterTweets(tweet, rootId) {
|
|
2051
2103
|
const tweets = [];
|
|
2052
2104
|
const seen = new Set;
|
package/dist/internal.d.ts
CHANGED
|
@@ -47,8 +47,8 @@ export type PostAuthor = {
|
|
|
47
47
|
};
|
|
48
48
|
/**
|
|
49
49
|
* Post metadata normalized across platforms. Every field is optional: an absent
|
|
50
|
-
* field means the
|
|
51
|
-
*
|
|
50
|
+
* field means the sources consulted did not expose it (never a fabricated
|
|
51
|
+
* default — a missing count is `undefined`,
|
|
52
52
|
* not `0`). All counts are non-negative. Platform-specific fields live in `extra`.
|
|
53
53
|
*/
|
|
54
54
|
export type PostMetadata = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@postfetch/core",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "Zero-dependency typed core that turns Facebook, Instagram, LinkedIn, Pinterest, Reddit, SoundCloud, TikTok, X and YouTube post URLs into media files.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/src/download.ts
CHANGED
|
@@ -101,6 +101,7 @@ export async function downloadBlob(
|
|
|
101
101
|
url: string,
|
|
102
102
|
options: DownloadBlobOptions & { remux: true },
|
|
103
103
|
): Promise<RemuxedVideo>;
|
|
104
|
+
/** Download without remuxing and return the upstream media Blob. */
|
|
104
105
|
export async function downloadBlob(
|
|
105
106
|
url: string,
|
|
106
107
|
options?: DownloadBlobOptions & { remux?: false },
|
package/src/internal.ts
CHANGED
|
@@ -48,8 +48,8 @@ export type PostAuthor = {
|
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
50
|
* Post metadata normalized across platforms. Every field is optional: an absent
|
|
51
|
-
* field means the
|
|
52
|
-
*
|
|
51
|
+
* field means the sources consulted did not expose it (never a fabricated
|
|
52
|
+
* default — a missing count is `undefined`,
|
|
53
53
|
* not `0`). All counts are non-negative. Platform-specific fields live in `extra`.
|
|
54
54
|
*/
|
|
55
55
|
export type PostMetadata = {
|
package/src/twitter.ts
CHANGED
|
@@ -31,6 +31,7 @@ export async function resolveTwitter(input: ResolveContext): Promise<PostfetchRe
|
|
|
31
31
|
if (string(tweet.__typename) === "TweetTombstone") {
|
|
32
32
|
throw new Error("Tweet is unavailable or age-restricted");
|
|
33
33
|
}
|
|
34
|
+
await expandTwitterText(input.net, tweet, id);
|
|
34
35
|
const groups = twitterTweets(tweet, id).flatMap(({ id: postId, tweet: post }) => {
|
|
35
36
|
const media = Array.isArray(post.mediaDetails) ? post.mediaDetails.filter(object) : [];
|
|
36
37
|
return media.flatMap((entry, index) => {
|
|
@@ -58,7 +59,7 @@ export function twitterMetadata(tweet: Json): PostMetadata & { extra?: TwitterEx
|
|
|
58
59
|
extra.quotedTweet = { id: quotedId, metadata: twitterMetadata(quotedTweet) };
|
|
59
60
|
}
|
|
60
61
|
return {
|
|
61
|
-
text: (
|
|
62
|
+
text: twitterText(tweet) ?? undefined,
|
|
62
63
|
author: user
|
|
63
64
|
? {
|
|
64
65
|
handle: string(user.screen_name) ?? undefined,
|
|
@@ -76,6 +77,67 @@ export function twitterMetadata(tweet: Json): PostMetadata & { extra?: TwitterEx
|
|
|
76
77
|
};
|
|
77
78
|
}
|
|
78
79
|
|
|
80
|
+
function noteText(tweet: Json): string | null {
|
|
81
|
+
const note = object(tweet.note_tweet) ? tweet.note_tweet : null;
|
|
82
|
+
const results = note && object(note.note_tweet_results) ? note.note_tweet_results : null;
|
|
83
|
+
const result = results && object(results.result) ? results.result : null;
|
|
84
|
+
return (result ? string(result.text) : null) ?? (note ? string(note.text) : null);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function twitterText(tweet: Json): string | null {
|
|
88
|
+
return noteText(tweet) ?? string(tweet.full_text) ?? string(tweet.text);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function needsFullText(tweet: Json): boolean {
|
|
92
|
+
if (noteText(tweet) || string(tweet.full_text)) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
// Embedded quotes can omit note_tweet entirely. Near-limit text is only a
|
|
96
|
+
// hint: ordinary long tweets may also incur a lookup, but are never shortened.
|
|
97
|
+
return object(tweet.note_tweet) || bool(tweet.truncated) === true || (string(tweet.text)?.length ?? 0) >= 270;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function expandTwitterText(net: Net, tweet: Json, rootId: string): Promise<void> {
|
|
101
|
+
const posts = new Map(twitterTweets(tweet, rootId).map(({ id, tweet: post }) => [id, post]));
|
|
102
|
+
for (const [id, post] of posts) {
|
|
103
|
+
if (!needsFullText(post)) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
// FxTwitter's public v1 API includes full note text and embedded quotes.
|
|
108
|
+
// Keep media and other metadata from X, and don't retry an optional lookup.
|
|
109
|
+
const response = await net(`https://api.fxtwitter.com/status/${id}`, {
|
|
110
|
+
headers: { accept: "application/json", "user-agent": browserUserAgent() },
|
|
111
|
+
}, 1);
|
|
112
|
+
if (!response.ok) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const payload: unknown = await response.json();
|
|
116
|
+
if (!object(payload) || payload.code !== 200 || !object(payload.tweet) || string(payload.tweet.id) !== id) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
let current: Json | null = payload.tweet;
|
|
120
|
+
const seen = new Set<string>();
|
|
121
|
+
while (current) {
|
|
122
|
+
const currentId = string(current.id);
|
|
123
|
+
if (!currentId || seen.has(currentId)) {
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
seen.add(currentId);
|
|
127
|
+
const target = posts.get(currentId);
|
|
128
|
+
const raw = object(current.raw_text) ? string(current.raw_text.text) : null;
|
|
129
|
+
const text = raw ?? string(current.text);
|
|
130
|
+
if (target && needsFullText(target) && text && text.length > (twitterText(target)?.length ?? 0)) {
|
|
131
|
+
target.full_text = text;
|
|
132
|
+
}
|
|
133
|
+
current = object(current.quote) ? current.quote : null;
|
|
134
|
+
}
|
|
135
|
+
} catch {
|
|
136
|
+
// A text enrichment outage must not discard a valid syndication result.
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
79
141
|
function twitterTweets(tweet: Json, rootId: string): Array<{ id: string; tweet: Json }> {
|
|
80
142
|
const tweets: Array<{ id: string; tweet: Json }> = [];
|
|
81
143
|
const seen = new Set<string>();
|