@yaebal/media-cache 0.0.1 → 0.0.3
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/README.md +41 -4
- package/lib/index.d.ts +22 -10
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +72 -9
- package/lib/index.js.map +1 -1
- package/package.json +3 -3
- package/src/index.test.ts +1 -1
- package/src/index.ts +110 -14
package/README.md
CHANGED
|
@@ -1,11 +1,48 @@
|
|
|
1
1
|
# @yaebal/media-cache
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
reuse a `file_id` instead of re-uploading the same file.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## manual mode
|
|
6
6
|
|
|
7
|
-
```
|
|
8
|
-
|
|
7
|
+
```ts
|
|
8
|
+
import { mediaCache } from "@yaebal/media-cache"
|
|
9
|
+
import { media } from "yaebal"
|
|
10
|
+
|
|
11
|
+
const cache = mediaCache()
|
|
12
|
+
|
|
13
|
+
bot.command("logo", async (ctx) => {
|
|
14
|
+
await cache.photo(ctx, "logo", media.path("./logo.png"))
|
|
15
|
+
})
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## transparent mode (hook into `ctx.send*`)
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { mediaCache } from "@yaebal/media-cache"
|
|
22
|
+
import { media } from "yaebal"
|
|
23
|
+
|
|
24
|
+
const cache = mediaCache()
|
|
25
|
+
cache.hook(bot.api)
|
|
26
|
+
|
|
27
|
+
// all sendPhoto/sendDocument/sendAudio/... auto-cache by path or URL
|
|
28
|
+
bot.command("logo", async (ctx) => {
|
|
29
|
+
await ctx.sendPhoto(media.path("./logo.png")) // first time: upload
|
|
30
|
+
await ctx.sendPhoto(media.path("./logo.png")) // second time: cached file_id
|
|
31
|
+
})
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
keyed by the source path or URL — if you send the same path twice, it reuses the `file_id` from the first response.
|
|
35
|
+
|
|
36
|
+
## storage
|
|
37
|
+
|
|
38
|
+
defaults to in-memory (`MemoryStorage`, lost on restart). pass a persistent storage:
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { mediaCache } from "@yaebal/media-cache"
|
|
42
|
+
|
|
43
|
+
const cache = mediaCache({
|
|
44
|
+
storage: myRedisStorage, // any StorageAdapter<string>
|
|
45
|
+
})
|
|
9
46
|
```
|
|
10
47
|
|
|
11
48
|
---
|
package/lib/index.d.ts
CHANGED
|
@@ -1,20 +1,32 @@
|
|
|
1
|
-
import { type Context, type MediaSource, type Message } from "@yaebal/core";
|
|
1
|
+
import { type Api, type Context, type MediaSource, type Message } from "@yaebal/core";
|
|
2
2
|
import { type StorageAdapter } from "@yaebal/session";
|
|
3
3
|
export interface MediaCacheOptions {
|
|
4
|
-
/** where to keep `key → file_id`. defaults to in-memory (lost on restart). */
|
|
5
4
|
storage?: StorageAdapter<string>;
|
|
6
5
|
}
|
|
7
6
|
export interface MediaCache {
|
|
8
|
-
/** send a photo, caching its `file_id` under `key` to skip future uploads. */
|
|
9
7
|
photo(ctx: Context, key: string, source: MediaSource | string, extra?: Record<string, unknown>): Promise<Message>;
|
|
10
|
-
/** send a document, caching its `file_id` under `key`. */
|
|
11
8
|
document(ctx: Context, key: string, source: MediaSource | string, extra?: Record<string, unknown>): Promise<Message>;
|
|
9
|
+
hook(api: Api): void;
|
|
10
|
+
/**
|
|
11
|
+
* cached media sources — `.path()` and `.url()` return thenables that
|
|
12
|
+
* transparently resolve to a cached `file_id` string (if available) or
|
|
13
|
+
* the original source. works directly in `ctx.send*()` — no wrapper needed.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* const cache = mediaCache()
|
|
17
|
+
* bot.command("logo", async (ctx) => {
|
|
18
|
+
* await ctx.sendPhoto(cache.media.path("./logo.png"))
|
|
19
|
+
* })
|
|
20
|
+
*/
|
|
21
|
+
media: CachedMedia;
|
|
22
|
+
}
|
|
23
|
+
export interface CachedMedia {
|
|
24
|
+
path(p: string): MediaSource & {
|
|
25
|
+
then(resolve: (value: string | MediaSource) => unknown): unknown;
|
|
26
|
+
};
|
|
27
|
+
url(u: string): MediaSource & {
|
|
28
|
+
then(resolve: (value: string | MediaSource) => unknown): unknown;
|
|
29
|
+
};
|
|
12
30
|
}
|
|
13
|
-
/**
|
|
14
|
-
* the first time you send a local file under a `key`, telegram returns a
|
|
15
|
-
* `file_id`; this caches it and reuses it on later sends — no re-upload.
|
|
16
|
-
* caller-supplied keys, so it's correct under concurrency (unlike a transparent
|
|
17
|
-
* hook that has to guess which upload produced which id).
|
|
18
|
-
*/
|
|
19
31
|
export declare function mediaCache(options?: MediaCacheOptions): MediaCache;
|
|
20
32
|
//# sourceMappingURL=index.d.ts.map
|
package/lib/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,GAAG,EACR,KAAK,OAAO,EAEZ,KAAK,WAAW,EAChB,KAAK,OAAO,EAEZ,MAAM,cAAc,CAAC;AACtB,OAAO,EAAiB,KAAK,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAErE,MAAM,WAAW,iBAAiB;IACjC,OAAO,CAAC,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,UAAU;IAC1B,KAAK,CACJ,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,WAAW,GAAG,MAAM,EAC5B,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,OAAO,CAAC,OAAO,CAAC,CAAC;IACpB,QAAQ,CACP,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,WAAW,GAAG,MAAM,EAC5B,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,OAAO,CAAC,OAAO,CAAC,CAAC;IACpB,IAAI,CAAC,GAAG,EAAE,GAAG,GAAG,IAAI,CAAC;IACrB;;;;;;;;;;OAUG;IACH,KAAK,EAAE,WAAW,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IAC3B,IAAI,CACH,CAAC,EAAE,MAAM,GACP,WAAW,GAAG;QAAE,IAAI,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,KAAK,OAAO,GAAG,OAAO,CAAA;KAAE,CAAC;IACtF,GAAG,CACF,CAAC,EAAE,MAAM,GACP,WAAW,GAAG;QAAE,IAAI,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,KAAK,OAAO,GAAG,OAAO,CAAA;KAAE,CAAC;CACtF;AA0CD,wBAAgB,UAAU,CAAC,OAAO,GAAE,iBAAsB,GAAG,UAAU,CAwEtE"}
|
package/lib/index.js
CHANGED
|
@@ -1,19 +1,43 @@
|
|
|
1
|
-
import { media } from "@yaebal/core";
|
|
1
|
+
import { isMediaSource, media, } from "@yaebal/core";
|
|
2
2
|
import { MemoryStorage } from "@yaebal/session";
|
|
3
3
|
function extractFileId(result, field) {
|
|
4
4
|
const value = result[field];
|
|
5
|
-
const node = Array.isArray(value) ? value
|
|
5
|
+
const node = Array.isArray(value) ? value.at(-1) : value;
|
|
6
6
|
return node?.file_id;
|
|
7
7
|
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
const MEDIA_METHODS = {
|
|
9
|
+
sendPhoto: { param: "photo", field: "photo" },
|
|
10
|
+
sendDocument: { param: "document", field: "document" },
|
|
11
|
+
sendAudio: { param: "audio", field: "audio" },
|
|
12
|
+
sendVideo: { param: "video", field: "video" },
|
|
13
|
+
sendAnimation: { param: "animation", field: "animation" },
|
|
14
|
+
sendVoice: { param: "voice", field: "voice" },
|
|
15
|
+
sendVideoNote: { param: "video_note", field: "video_note" },
|
|
16
|
+
sendSticker: { param: "sticker", field: "sticker" },
|
|
17
|
+
};
|
|
18
|
+
function computeKey(source) {
|
|
19
|
+
if (source.kind === "path")
|
|
20
|
+
return `path:${source.path}`;
|
|
21
|
+
if (source.kind === "url")
|
|
22
|
+
return `url:${source.url}`;
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
function createCachedSource(source, storage_) {
|
|
26
|
+
const key = computeKey(source);
|
|
27
|
+
return {
|
|
28
|
+
...source,
|
|
29
|
+
then(resolve) {
|
|
30
|
+
if (!key)
|
|
31
|
+
return resolve(source);
|
|
32
|
+
return Promise.resolve(storage_.get(key)).then((cached) => {
|
|
33
|
+
resolve(cached ? media.fileId(cached) : source);
|
|
34
|
+
});
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
14
38
|
export function mediaCache(options = {}) {
|
|
15
39
|
const storage = options.storage ?? new MemoryStorage();
|
|
16
|
-
const send = async (
|
|
40
|
+
const send = async (_ctx, key, source, field, sender, extra) => {
|
|
17
41
|
const cached = await storage.get(key);
|
|
18
42
|
const result = await sender(cached ? media.fileId(cached) : source, extra);
|
|
19
43
|
if (!cached) {
|
|
@@ -26,6 +50,45 @@ export function mediaCache(options = {}) {
|
|
|
26
50
|
return {
|
|
27
51
|
photo: (ctx, key, source, extra) => send(ctx, key, source, "photo", (s, e) => ctx.sendPhoto(s, e), extra),
|
|
28
52
|
document: (ctx, key, source, extra) => send(ctx, key, source, "document", (s, e) => ctx.sendDocument(s, e), extra),
|
|
53
|
+
media: {
|
|
54
|
+
path: (p) => createCachedSource(media.path(p), storage),
|
|
55
|
+
url: (u) => createCachedSource(media.url(u), storage),
|
|
56
|
+
},
|
|
57
|
+
hook(api) {
|
|
58
|
+
const cachedBefore = new Map();
|
|
59
|
+
api.before(async (method, params) => {
|
|
60
|
+
const config = MEDIA_METHODS[method];
|
|
61
|
+
if (!config || !params)
|
|
62
|
+
return;
|
|
63
|
+
const src = params[config.param];
|
|
64
|
+
if (!isMediaSource(src))
|
|
65
|
+
return;
|
|
66
|
+
const key = computeKey(src);
|
|
67
|
+
if (!key)
|
|
68
|
+
return;
|
|
69
|
+
const fileId = await storage.get(key);
|
|
70
|
+
if (fileId) {
|
|
71
|
+
params[config.param] = fileId;
|
|
72
|
+
return params;
|
|
73
|
+
}
|
|
74
|
+
cachedBefore.set(params, key);
|
|
75
|
+
return params;
|
|
76
|
+
});
|
|
77
|
+
api.after(async (method, params, result) => {
|
|
78
|
+
if (!params)
|
|
79
|
+
return;
|
|
80
|
+
const config = MEDIA_METHODS[method];
|
|
81
|
+
if (!config)
|
|
82
|
+
return;
|
|
83
|
+
const key = cachedBefore.get(params);
|
|
84
|
+
if (!key)
|
|
85
|
+
return;
|
|
86
|
+
cachedBefore.delete(params);
|
|
87
|
+
const id = extractFileId(result, config.field);
|
|
88
|
+
if (id)
|
|
89
|
+
await storage.set(key, id);
|
|
90
|
+
});
|
|
91
|
+
},
|
|
29
92
|
};
|
|
30
93
|
}
|
|
31
94
|
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yaebal/media-cache",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "yaebal media-cache — reuse a file_id instead of re-uploading the same file.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
"src"
|
|
17
17
|
],
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@yaebal/core": "0.0.
|
|
20
|
-
"@yaebal/session": "0.0.
|
|
19
|
+
"@yaebal/core": "0.0.5",
|
|
20
|
+
"@yaebal/session": "0.0.2"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"@types/node": "latest"
|
package/src/index.test.ts
CHANGED
|
@@ -50,7 +50,7 @@ test("distinct keys cache independently", async () => {
|
|
|
50
50
|
const cache = mediaCache();
|
|
51
51
|
await cache.photo(ctx, "a", media.path("./a.png"));
|
|
52
52
|
await cache.photo(ctx, "b", media.path("./b.png")); // different key → uploads
|
|
53
|
-
|
|
53
|
+
|
|
54
54
|
assert.equal(sent[0]?.kind, "path");
|
|
55
55
|
assert.equal(sent[1]?.kind, "path");
|
|
56
56
|
});
|
package/src/index.ts
CHANGED
|
@@ -1,46 +1,99 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type Api,
|
|
3
|
+
type Context,
|
|
4
|
+
isMediaSource,
|
|
5
|
+
type MediaSource,
|
|
6
|
+
type Message,
|
|
7
|
+
media,
|
|
8
|
+
} from "@yaebal/core";
|
|
2
9
|
import { MemoryStorage, type StorageAdapter } from "@yaebal/session";
|
|
3
10
|
|
|
4
11
|
export interface MediaCacheOptions {
|
|
5
|
-
/** where to keep `key → file_id`. defaults to in-memory (lost on restart). */
|
|
6
12
|
storage?: StorageAdapter<string>;
|
|
7
13
|
}
|
|
8
14
|
|
|
9
15
|
export interface MediaCache {
|
|
10
|
-
/** send a photo, caching its `file_id` under `key` to skip future uploads. */
|
|
11
16
|
photo(
|
|
12
17
|
ctx: Context,
|
|
13
18
|
key: string,
|
|
14
19
|
source: MediaSource | string,
|
|
15
20
|
extra?: Record<string, unknown>,
|
|
16
21
|
): Promise<Message>;
|
|
17
|
-
/** send a document, caching its `file_id` under `key`. */
|
|
18
22
|
document(
|
|
19
23
|
ctx: Context,
|
|
20
24
|
key: string,
|
|
21
25
|
source: MediaSource | string,
|
|
22
26
|
extra?: Record<string, unknown>,
|
|
23
27
|
): Promise<Message>;
|
|
28
|
+
hook(api: Api): void;
|
|
29
|
+
/**
|
|
30
|
+
* cached media sources — `.path()` and `.url()` return thenables that
|
|
31
|
+
* transparently resolve to a cached `file_id` string (if available) or
|
|
32
|
+
* the original source. works directly in `ctx.send*()` — no wrapper needed.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* const cache = mediaCache()
|
|
36
|
+
* bot.command("logo", async (ctx) => {
|
|
37
|
+
* await ctx.sendPhoto(cache.media.path("./logo.png"))
|
|
38
|
+
* })
|
|
39
|
+
*/
|
|
40
|
+
media: CachedMedia;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface CachedMedia {
|
|
44
|
+
path(
|
|
45
|
+
p: string,
|
|
46
|
+
): MediaSource & { then(resolve: (value: string | MediaSource) => unknown): unknown };
|
|
47
|
+
url(
|
|
48
|
+
u: string,
|
|
49
|
+
): MediaSource & { then(resolve: (value: string | MediaSource) => unknown): unknown };
|
|
24
50
|
}
|
|
25
51
|
|
|
26
52
|
function extractFileId(result: Message, field: string): string | undefined {
|
|
27
53
|
const value = (result as unknown as Record<string, unknown>)[field];
|
|
28
|
-
const node = Array.isArray(value) ? value
|
|
29
|
-
|
|
54
|
+
const node = Array.isArray(value) ? value.at(-1) : value;
|
|
30
55
|
return (node as { file_id?: string } | undefined)?.file_id;
|
|
31
56
|
}
|
|
32
57
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
58
|
+
const MEDIA_METHODS: Record<string, { param: string; field: string }> = {
|
|
59
|
+
sendPhoto: { param: "photo", field: "photo" },
|
|
60
|
+
sendDocument: { param: "document", field: "document" },
|
|
61
|
+
sendAudio: { param: "audio", field: "audio" },
|
|
62
|
+
sendVideo: { param: "video", field: "video" },
|
|
63
|
+
sendAnimation: { param: "animation", field: "animation" },
|
|
64
|
+
sendVoice: { param: "voice", field: "voice" },
|
|
65
|
+
sendVideoNote: { param: "video_note", field: "video_note" },
|
|
66
|
+
sendSticker: { param: "sticker", field: "sticker" },
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
function computeKey(source: MediaSource): string | undefined {
|
|
70
|
+
if (source.kind === "path") return `path:${source.path}`;
|
|
71
|
+
if (source.kind === "url") return `url:${source.url}`;
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function createCachedSource(
|
|
76
|
+
source: MediaSource,
|
|
77
|
+
storage_: StorageAdapter<string>,
|
|
78
|
+
): MediaSource & { then(resolve: (value: string | MediaSource) => unknown): unknown } {
|
|
79
|
+
const key = computeKey(source);
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
...source,
|
|
83
|
+
then(resolve: (value: string | MediaSource) => unknown) {
|
|
84
|
+
if (!key) return resolve(source);
|
|
85
|
+
return Promise.resolve(storage_.get(key)).then((cached) => {
|
|
86
|
+
resolve(cached ? media.fileId(cached) : source);
|
|
87
|
+
});
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
39
92
|
export function mediaCache(options: MediaCacheOptions = {}): MediaCache {
|
|
40
93
|
const storage = options.storage ?? new MemoryStorage<string>();
|
|
41
94
|
|
|
42
95
|
const send = async (
|
|
43
|
-
|
|
96
|
+
_ctx: Context,
|
|
44
97
|
key: string,
|
|
45
98
|
source: MediaSource | string,
|
|
46
99
|
field: "photo" | "document",
|
|
@@ -52,7 +105,7 @@ export function mediaCache(options: MediaCacheOptions = {}): MediaCache {
|
|
|
52
105
|
|
|
53
106
|
if (!cached) {
|
|
54
107
|
const id = extractFileId(result, field);
|
|
55
|
-
|
|
108
|
+
|
|
56
109
|
if (id) await storage.set(key, id);
|
|
57
110
|
}
|
|
58
111
|
|
|
@@ -64,5 +117,48 @@ export function mediaCache(options: MediaCacheOptions = {}): MediaCache {
|
|
|
64
117
|
send(ctx, key, source, "photo", (s, e) => ctx.sendPhoto(s, e), extra),
|
|
65
118
|
document: (ctx, key, source, extra) =>
|
|
66
119
|
send(ctx, key, source, "document", (s, e) => ctx.sendDocument(s, e), extra),
|
|
120
|
+
media: {
|
|
121
|
+
path: (p: string) => createCachedSource(media.path(p), storage),
|
|
122
|
+
url: (u: string) => createCachedSource(media.url(u), storage),
|
|
123
|
+
},
|
|
124
|
+
hook(api: Api) {
|
|
125
|
+
const cachedBefore = new Map<object, string>();
|
|
126
|
+
|
|
127
|
+
api.before(async (method, params) => {
|
|
128
|
+
const config = MEDIA_METHODS[method];
|
|
129
|
+
if (!config || !params) return;
|
|
130
|
+
|
|
131
|
+
const src = params[config.param];
|
|
132
|
+
if (!isMediaSource(src)) return;
|
|
133
|
+
|
|
134
|
+
const key = computeKey(src);
|
|
135
|
+
if (!key) return;
|
|
136
|
+
|
|
137
|
+
const fileId = await storage.get(key);
|
|
138
|
+
if (fileId) {
|
|
139
|
+
params[config.param] = fileId;
|
|
140
|
+
return params;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
cachedBefore.set(params, key);
|
|
144
|
+
|
|
145
|
+
return params;
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
api.after(async (method, params, result) => {
|
|
149
|
+
if (!params) return;
|
|
150
|
+
|
|
151
|
+
const config = MEDIA_METHODS[method];
|
|
152
|
+
if (!config) return;
|
|
153
|
+
|
|
154
|
+
const key = cachedBefore.get(params);
|
|
155
|
+
if (!key) return;
|
|
156
|
+
|
|
157
|
+
cachedBefore.delete(params);
|
|
158
|
+
|
|
159
|
+
const id = extractFileId(result as Message, config.field);
|
|
160
|
+
if (id) await storage.set(key, id);
|
|
161
|
+
});
|
|
162
|
+
},
|
|
67
163
|
};
|
|
68
164
|
}
|