rewind-mcp-server 0.1.0 → 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/README.md +77 -90
- package/dist/__tests__/helpers.test.d.ts +1 -0
- package/dist/__tests__/helpers.test.js +131 -0
- package/dist/__tests__/helpers.test.js.map +1 -0
- package/dist/__tests__/image-block.live.d.ts +1 -0
- package/dist/__tests__/image-block.live.js +41 -0
- package/dist/__tests__/image-block.live.js.map +1 -0
- package/dist/__tests__/server.test.d.ts +1 -0
- package/dist/__tests__/server.test.js +1308 -0
- package/dist/__tests__/server.test.js.map +1 -0
- package/dist/__tests__/stdio-e2e.live.d.ts +1 -0
- package/dist/__tests__/stdio-e2e.live.js +60 -0
- package/dist/__tests__/stdio-e2e.live.js.map +1 -0
- package/dist/__tests__/ui-resource.live.d.ts +1 -0
- package/dist/__tests__/ui-resource.live.js +71 -0
- package/dist/__tests__/ui-resource.live.js.map +1 -0
- package/dist/client.d.ts +9 -0
- package/dist/client.js +15 -0
- package/dist/client.js.map +1 -1
- package/dist/index.js +0 -0
- package/dist/prompts.js +101 -0
- package/dist/prompts.js.map +1 -1
- package/dist/resources/ui.d.ts +33 -0
- package/dist/resources/ui.js +14 -0
- package/dist/resources/ui.js.map +1 -0
- package/dist/resources.js +128 -0
- package/dist/resources.js.map +1 -1
- package/dist/server.js +55 -1
- package/dist/server.js.map +1 -1
- package/dist/tools/collecting.js +116 -14
- package/dist/tools/collecting.js.map +1 -1
- package/dist/tools/cross-domain.js +74 -17
- package/dist/tools/cross-domain.js.map +1 -1
- package/dist/tools/debug.d.ts +6 -0
- package/dist/tools/debug.js +26 -0
- package/dist/tools/debug.js.map +1 -0
- package/dist/tools/helpers.d.ts +81 -5
- package/dist/tools/helpers.js +138 -4
- package/dist/tools/helpers.js.map +1 -1
- package/dist/tools/listening.js +223 -52
- package/dist/tools/listening.js.map +1 -1
- package/dist/tools/reading.js +74 -22
- package/dist/tools/reading.js.map +1 -1
- package/dist/tools/running.js +87 -43
- package/dist/tools/running.js.map +1 -1
- package/dist/tools/watching.js +199 -32
- package/dist/tools/watching.js.map +1 -1
- package/dist/ui-bundles.d.ts +3 -0
- package/dist/ui-bundles.js +8 -0
- package/dist/ui-bundles.js.map +1 -0
- package/dist/worker.d.ts +2 -1
- package/dist/worker.js +55 -30
- package/dist/worker.js.map +1 -1
- package/package.json +18 -3
package/README.md
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
# rewind-mcp-server
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://github.com/pdugan20/rewind/actions/workflows/mcp-server.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/rewind-mcp-server)
|
|
5
|
+
[](https://docs.rewind.rest/mcp-server)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
MCP server for the [Rewind](https://rewind.rest) personal data API. Gives AI assistants access to your listening, running, watching, collecting, and reading data.
|
|
6
9
|
|
|
7
|
-
|
|
8
|
-
- **4 resources** for sync status and year-in-review data
|
|
9
|
-
- **3 prompt templates** for weekly summaries, year-in-review, and period comparisons
|
|
10
|
-
- Works with Claude Desktop, Claude Code, Claude iOS, and claude.ai
|
|
10
|
+
## Setup
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
### Desktop Apps
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
14
|
+
Add to your MCP client config (Claude Desktop, ChatGPT, Gemini, etc.):
|
|
17
15
|
|
|
18
16
|
```json
|
|
19
17
|
{
|
|
@@ -29,112 +27,101 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
|
29
27
|
}
|
|
30
28
|
```
|
|
31
29
|
|
|
32
|
-
|
|
30
|
+
### Mobile & Web
|
|
31
|
+
|
|
32
|
+
Add as a remote integration in your client's settings:
|
|
33
|
+
|
|
34
|
+
- **URL**: `https://mcp.rewind.rest/mcp`
|
|
35
|
+
- **Authorization**: `Bearer rw_live_your_key_here`
|
|
33
36
|
|
|
34
|
-
|
|
37
|
+
<details>
|
|
38
|
+
<summary>Claude Code</summary>
|
|
35
39
|
|
|
36
40
|
```bash
|
|
37
41
|
claude mcp add rewind -- npx -y rewind-mcp-server
|
|
38
42
|
```
|
|
39
43
|
|
|
40
|
-
|
|
44
|
+
</details>
|
|
41
45
|
|
|
42
|
-
|
|
43
|
-
{
|
|
44
|
-
"mcpServers": {
|
|
45
|
-
"rewind": {
|
|
46
|
-
"command": "npx",
|
|
47
|
-
"args": ["-y", "rewind-mcp-server"],
|
|
48
|
-
"env": {
|
|
49
|
-
"REWIND_API_KEY": "rw_live_your_key_here"
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
```
|
|
46
|
+
Requires a [Rewind API key](https://docs.rewind.rest/authentication). `REWIND_API_URL` defaults to `https://api.rewind.rest`.
|
|
55
47
|
|
|
56
|
-
|
|
48
|
+
## Tools
|
|
57
49
|
|
|
58
|
-
|
|
50
|
+
| Domain | Source | Tools |
|
|
51
|
+
| ---------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
52
|
+
| **Listening** | Last.fm | Now playing, recent scrobbles, stats, top artists/albums/tracks, streaks, artist and album details, genre breakdown over time |
|
|
53
|
+
| **Running** | Strava | Stats, recent runs, personal records, streaks, activity details, per-mile splits, per-year summaries |
|
|
54
|
+
| **Watching** | Plex, Letterboxd | Recent watches, movie details, browse by genre/decade/director, stats, genre/decade/director breakdowns |
|
|
55
|
+
| **Collecting** | Discogs, Trakt | Vinyl collection, physical media (Blu-ray/4K UHD/HD DVD), collection and media stats |
|
|
56
|
+
| **Reading** | Instapaper | Recent articles, highlights, random highlight, stats |
|
|
57
|
+
| **Cross-domain** | All | Full-text search, unified feed, on-this-day |
|
|
59
58
|
|
|
60
|
-
|
|
61
|
-
- **Authorization:** `Bearer rw_live_your_key_here`
|
|
59
|
+
## Rich responses
|
|
62
60
|
|
|
63
|
-
|
|
61
|
+
Tool responses follow the MCP 2025-06-18 content model and include more than plain text:
|
|
64
62
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
| `REWIND_API_URL` | No | `https://api.rewind.rest` | API base URL |
|
|
63
|
+
- **Images.** Detail tools (`get_movie_details`, `get_album_details`, `get_artist_details`) and list tools (`get_recent_watches`, `get_recent_listens`, `get_top_albums`, `get_vinyl_collection`, etc.) return cover art / posters / artist imagery as `image` content blocks (top-N on lists, default N=5). Pass `include_images: false` to skip them when keeping responses compact matters.
|
|
64
|
+
- **Resource links.** External platform URLs come back as `resource_link` content blocks rather than being baked into prose -- Letterboxd reviews, Strava activities, Discogs releases, Apple Music pages, original article URLs, Last.fm pages.
|
|
65
|
+
- **Structured content.** Every tool also returns `structuredContent` with a JSON shape that mirrors the underlying API response, so the model can reason over exact numbers without re-parsing prose.
|
|
69
66
|
|
|
70
|
-
##
|
|
67
|
+
## Entity resources
|
|
71
68
|
|
|
72
|
-
|
|
69
|
+
The server exposes `@`-mentionable resources for fetching full detail on any entity:
|
|
73
70
|
|
|
74
|
-
|
|
|
75
|
-
|
|
|
76
|
-
| `
|
|
77
|
-
| `
|
|
78
|
-
| `
|
|
79
|
-
| `
|
|
80
|
-
| `
|
|
81
|
-
| `
|
|
82
|
-
| `
|
|
83
|
-
| `
|
|
84
|
-
| `
|
|
71
|
+
| Entity | URI | Source endpoint |
|
|
72
|
+
| -------------- | -------------------------------- | --------------------------------- |
|
|
73
|
+
| Movie | `rewind://movie/{id}` | `/v1/watching/movies/{id}` |
|
|
74
|
+
| Show | `rewind://show/{id}` | `/v1/watching/shows/{id}` |
|
|
75
|
+
| Album | `rewind://album/{id}` | `/v1/listening/albums/{id}` |
|
|
76
|
+
| Artist | `rewind://artist/{id}` | `/v1/listening/artists/{id}` |
|
|
77
|
+
| Vinyl | `rewind://vinyl/{id}` | `/v1/collecting/vinyl/{id}` |
|
|
78
|
+
| Physical media | `rewind://physical-media/{id}` | `/v1/collecting/media/{id}` |
|
|
79
|
+
| Article | `rewind://article/{id}` | `/v1/reading/articles/{id}` |
|
|
80
|
+
| Activity | `rewind://activity/{id}` | `/v1/running/activities/{id}` |
|
|
81
|
+
| Sync status | `rewind://sync/status` | `/v1/health/sync` |
|
|
82
|
+
| Year in review | `rewind://{domain}/year/{year}` | `/v1/{domain}/year/{year}` |
|
|
85
83
|
|
|
86
|
-
|
|
84
|
+
`search` returns `resource_link`s pointing at these URIs so clients can drill from a match straight into the full record.
|
|
87
85
|
|
|
88
|
-
|
|
89
|
-
| ---------------------- | --------------------------- |
|
|
90
|
-
| `get_running_stats` | Overall running statistics |
|
|
91
|
-
| `get_recent_runs` | Recent activities |
|
|
92
|
-
| `get_personal_records` | PRs at standard distances |
|
|
93
|
-
| `get_running_streaks` | Current and longest streaks |
|
|
94
|
-
| `get_activity_details` | Single run details by ID |
|
|
86
|
+
## Interactive UI (MCP Apps)
|
|
95
87
|
|
|
96
|
-
|
|
88
|
+
`get_recent_watches` advertises a [MCP Apps](https://blog.modelcontextprotocol.io/posts/2026-01-26-mcp-apps/) UI resource. On clients that render MCP Apps (Claude Desktop, Claude web, VS Code GitHub Copilot, Goose) the tool returns an interactive poster grid inline in the conversation: thumbhash placeholders, theme-aware cards, clickable Letterboxd reviews, hover states. On clients that do not render MCP Apps the existing text + image + resource_link + structuredContent response is unchanged, so no caller regresses.
|
|
97
89
|
|
|
98
|
-
|
|
99
|
-
| -------------------- | --------------------------- |
|
|
100
|
-
| `get_recent_watches` | Recently watched movies/TV |
|
|
101
|
-
| `get_movie_details` | Movie info by ID |
|
|
102
|
-
| `get_watching_stats` | Overall watching statistics |
|
|
90
|
+
## Example Queries
|
|
103
91
|
|
|
104
|
-
|
|
92
|
+
- "What albums have I been listening to the most recently?"
|
|
93
|
+
- "Compare my mile splits from this month vs last month"
|
|
94
|
+
- "When was the last time I watched a film by Wes Anderson?"
|
|
95
|
+
- "What Beastie Boys records are missing from my vinyl collection?"
|
|
96
|
+
- "How many articles did I read last year and stack-rank the top 10 sources"
|
|
97
|
+
- "Can you give me a quick summary of everything I did last week?"
|
|
105
98
|
|
|
106
|
-
|
|
107
|
-
| ---------------------- | --------------------- |
|
|
108
|
-
| `get_vinyl_collection` | Browse vinyl records |
|
|
109
|
-
| `get_collecting_stats` | Collection statistics |
|
|
99
|
+
## Prompts
|
|
110
100
|
|
|
111
|
-
|
|
101
|
+
The server exposes slash-command prompts that orchestrate multiple tools for common asks:
|
|
112
102
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
103
|
+
- `weekly-summary` -- rolls up activity across all domains for the past 7 days
|
|
104
|
+
- `year-in-review` -- comprehensive yearly recap
|
|
105
|
+
- `compare-periods` -- parametric comparison between two time windows in a single domain
|
|
106
|
+
- `letterboxd-review-draft` -- drafts a Letterboxd-style review for your most recent unrated film
|
|
107
|
+
- `training-report` -- coach-style running report for the last 7-14 days
|
|
108
|
+
- `film-diet` -- portrait of your film-watching taste (genre mix, decades, directors)
|
|
119
109
|
|
|
120
|
-
|
|
110
|
+
## Authentication
|
|
121
111
|
|
|
122
|
-
|
|
123
|
-
| ----------------- | ----------------------------------- |
|
|
124
|
-
| `search` | Full-text search across all domains |
|
|
125
|
-
| `get_feed` | Unified activity feed |
|
|
126
|
-
| `get_on_this_day` | Historical "on this day" items |
|
|
127
|
-
| `get_health` | API and sync health status |
|
|
112
|
+
**Desktop apps** use the `REWIND_API_KEY` environment variable passed directly to the server process. The server authenticates with the Rewind API using this key as a Bearer token.
|
|
128
113
|
|
|
129
|
-
|
|
114
|
+
**Mobile and web clients** connect to the remote server at `mcp.rewind.rest`, which uses OAuth 2.1 with GitHub as the identity provider. The flow:
|
|
130
115
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
116
|
+
1. Client discovers endpoints via `/.well-known/oauth-authorization-server`
|
|
117
|
+
2. Client dynamically registers via `/register`
|
|
118
|
+
3. User authenticates with GitHub and approves access
|
|
119
|
+
4. Client receives scoped tokens (1h access, 90d refresh, PKCE S256)
|
|
120
|
+
|
|
121
|
+
The server has **read-only access** to your data. No write or admin operations are exposed.
|
|
137
122
|
|
|
138
|
-
##
|
|
123
|
+
## Privacy & Support
|
|
139
124
|
|
|
140
|
-
|
|
125
|
+
- [Privacy Policy](https://rewind.rest/privacy)
|
|
126
|
+
- [Documentation](https://docs.rewind.rest/mcp-server)
|
|
127
|
+
- [Report an issue](https://github.com/pdugan20/rewind/issues)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { text, resourceLink, imageBlock, withRichResponse, withErrorHandling, } from '../tools/helpers.js';
|
|
3
|
+
import { RewindClient } from '../client.js';
|
|
4
|
+
describe('helpers', () => {
|
|
5
|
+
describe('text()', () => {
|
|
6
|
+
it('wraps a string in a text content block', () => {
|
|
7
|
+
expect(text('hello')).toEqual({ type: 'text', text: 'hello' });
|
|
8
|
+
});
|
|
9
|
+
it('accepts an empty string', () => {
|
|
10
|
+
expect(text('')).toEqual({ type: 'text', text: '' });
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
describe('resourceLink()', () => {
|
|
14
|
+
it('builds a resource_link block for a valid URI', () => {
|
|
15
|
+
const block = resourceLink('https://letterboxd.com/u/film/', 'Letterboxd review');
|
|
16
|
+
expect(block).toEqual({
|
|
17
|
+
type: 'resource_link',
|
|
18
|
+
uri: 'https://letterboxd.com/u/film/',
|
|
19
|
+
name: 'Letterboxd review',
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
it('includes optional mimeType and description', () => {
|
|
23
|
+
const block = resourceLink('https://example.com', 'Example', {
|
|
24
|
+
mimeType: 'text/html',
|
|
25
|
+
description: 'An example site',
|
|
26
|
+
});
|
|
27
|
+
expect(block).toMatchObject({
|
|
28
|
+
type: 'resource_link',
|
|
29
|
+
uri: 'https://example.com',
|
|
30
|
+
name: 'Example',
|
|
31
|
+
mimeType: 'text/html',
|
|
32
|
+
description: 'An example site',
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
it('returns null when uri is null/undefined/empty', () => {
|
|
36
|
+
expect(resourceLink(null, 'x')).toBeNull();
|
|
37
|
+
expect(resourceLink(undefined, 'x')).toBeNull();
|
|
38
|
+
expect(resourceLink('', 'x')).toBeNull();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
describe('imageBlock()', () => {
|
|
42
|
+
const bytes = new Uint8Array([0xff, 0xd8, 0xff, 0xe0]); // JPEG magic
|
|
43
|
+
it('base64-encodes bytes fetched from image.cdn_url', async () => {
|
|
44
|
+
const client = new RewindClient('https://api.test', 'rw_test');
|
|
45
|
+
const spy = vi.spyOn(client, 'getBinaryFromUrl').mockResolvedValue({
|
|
46
|
+
bytes,
|
|
47
|
+
mimeType: 'image/jpeg',
|
|
48
|
+
});
|
|
49
|
+
const block = await imageBlock(client, {
|
|
50
|
+
cdn_url: 'https://cdn.rewind.rest/watching/movies/1/original.jpg',
|
|
51
|
+
});
|
|
52
|
+
expect(block).not.toBeNull();
|
|
53
|
+
expect(block?.type).toBe('image');
|
|
54
|
+
expect(block?.mimeType).toBe('image/jpeg');
|
|
55
|
+
expect(block?.data).toBe('/9j/4A==');
|
|
56
|
+
expect(spy).toHaveBeenCalledWith('https://cdn.rewind.rest/watching/movies/1/original.jpg');
|
|
57
|
+
});
|
|
58
|
+
it('falls back to image.url when cdn_url is absent', async () => {
|
|
59
|
+
const client = new RewindClient('https://api.test', 'rw_test');
|
|
60
|
+
const spy = vi.spyOn(client, 'getBinaryFromUrl').mockResolvedValue({
|
|
61
|
+
bytes,
|
|
62
|
+
mimeType: 'image/jpeg',
|
|
63
|
+
});
|
|
64
|
+
await imageBlock(client, { url: 'https://cdn.example/x.jpg' });
|
|
65
|
+
expect(spy).toHaveBeenCalledWith('https://cdn.example/x.jpg');
|
|
66
|
+
});
|
|
67
|
+
it('returns null when image is null or undefined', async () => {
|
|
68
|
+
const client = new RewindClient('https://api.test', 'rw_test');
|
|
69
|
+
expect(await imageBlock(client, null)).toBeNull();
|
|
70
|
+
expect(await imageBlock(client, undefined)).toBeNull();
|
|
71
|
+
});
|
|
72
|
+
it('returns null when image has no cdn_url or url', async () => {
|
|
73
|
+
const client = new RewindClient('https://api.test', 'rw_test');
|
|
74
|
+
expect(await imageBlock(client, { thumbhash: 'x' })).toBeNull();
|
|
75
|
+
});
|
|
76
|
+
it('returns null on fetch failure instead of throwing', async () => {
|
|
77
|
+
const client = new RewindClient('https://api.test', 'rw_test');
|
|
78
|
+
vi.spyOn(client, 'getBinaryFromUrl').mockRejectedValue(new Error('network down'));
|
|
79
|
+
const block = await imageBlock(client, {
|
|
80
|
+
cdn_url: 'https://cdn.example/x.jpg',
|
|
81
|
+
});
|
|
82
|
+
expect(block).toBeNull();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
describe('withRichResponse()', () => {
|
|
86
|
+
it('returns the handler result on success', async () => {
|
|
87
|
+
const result = await withRichResponse(async () => ({
|
|
88
|
+
content: [text('ok')],
|
|
89
|
+
structuredContent: { value: 42 },
|
|
90
|
+
}));
|
|
91
|
+
expect(result).toEqual({
|
|
92
|
+
content: [{ type: 'text', text: 'ok' }],
|
|
93
|
+
structuredContent: { value: 42 },
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
it('catches exceptions and returns isError=true', async () => {
|
|
97
|
+
const result = await withRichResponse(async () => {
|
|
98
|
+
throw new Error('boom');
|
|
99
|
+
});
|
|
100
|
+
expect(result.isError).toBe(true);
|
|
101
|
+
expect(result.content).toHaveLength(1);
|
|
102
|
+
expect(result.content[0]).toEqual({
|
|
103
|
+
type: 'text',
|
|
104
|
+
text: 'Error: boom',
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
it('handles non-Error throwables', async () => {
|
|
108
|
+
const result = await withRichResponse(async () => {
|
|
109
|
+
throw 'string error';
|
|
110
|
+
});
|
|
111
|
+
expect(result.isError).toBe(true);
|
|
112
|
+
expect(result.content[0].text).toBe('Error: string error');
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
describe('withErrorHandling() (legacy text-only wrapper)', () => {
|
|
116
|
+
it('still works for text-only tool handlers', async () => {
|
|
117
|
+
const result = await withErrorHandling(async () => 'plain text');
|
|
118
|
+
expect(result).toEqual({
|
|
119
|
+
content: [{ type: 'text', text: 'plain text' }],
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
it('catches and reports errors', async () => {
|
|
123
|
+
const result = await withErrorHandling(async () => {
|
|
124
|
+
throw new Error('nope');
|
|
125
|
+
});
|
|
126
|
+
expect(result.isError).toBe(true);
|
|
127
|
+
expect(result.content[0].text).toBe('Error: nope');
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
//# sourceMappingURL=helpers.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"helpers.test.js","sourceRoot":"","sources":["../../src/__tests__/helpers.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live diagnostic: does imageBlock() actually produce a valid image
|
|
3
|
+
* content block when run against the real Rewind CDN? No mocks.
|
|
4
|
+
*
|
|
5
|
+
* Run: npx tsx src/__tests__/image-block.live.ts
|
|
6
|
+
*/
|
|
7
|
+
import { RewindClient } from '../client.js';
|
|
8
|
+
import { imageBlock } from '../tools/helpers.js';
|
|
9
|
+
const FIXTURE = {
|
|
10
|
+
movieId: 710,
|
|
11
|
+
title: "I'm Chevy Chase and You're Not",
|
|
12
|
+
cdn_url: 'https://cdn.rewind.rest/watching/movies/710/original.jpg?width=300&height=300&fit=cover&format=auto&quality=85&v=1',
|
|
13
|
+
};
|
|
14
|
+
async function main() {
|
|
15
|
+
// Base URL + api key are irrelevant here -- imageBlock goes through
|
|
16
|
+
// getBinaryFromUrl which hits the full CDN URL without auth.
|
|
17
|
+
const client = new RewindClient('https://api.rewind.rest', 'rw_unused');
|
|
18
|
+
console.log(`Testing imageBlock against live CDN...`);
|
|
19
|
+
console.log(`Fixture: ${FIXTURE.title} (movie ${FIXTURE.movieId})`);
|
|
20
|
+
console.log(`URL: ${FIXTURE.cdn_url}\n`);
|
|
21
|
+
const start = Date.now();
|
|
22
|
+
const block = await imageBlock(client, { cdn_url: FIXTURE.cdn_url });
|
|
23
|
+
const ms = Date.now() - start;
|
|
24
|
+
if (!block) {
|
|
25
|
+
console.error(`FAIL: imageBlock returned null (fetched in ${ms}ms)`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
console.log(`SUCCESS (${ms}ms)`);
|
|
29
|
+
console.log(`- type: ${block.type}`);
|
|
30
|
+
console.log(`- mimeType: ${block.mimeType}`);
|
|
31
|
+
console.log(`- data size: ${block.data.length} base64 chars`);
|
|
32
|
+
console.log(`- sniff: ${block.data.slice(0, 24)}...`);
|
|
33
|
+
// JPEG base64 starts with /9j/
|
|
34
|
+
const looksLikeJpeg = block.data.startsWith('/9j/');
|
|
35
|
+
console.log(`- JPEG? ${looksLikeJpeg ? 'yes (starts with /9j/)' : 'NO -- unexpected'}`);
|
|
36
|
+
}
|
|
37
|
+
main().catch((err) => {
|
|
38
|
+
console.error('Unexpected error:', err);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
});
|
|
41
|
+
//# sourceMappingURL=image-block.live.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image-block.live.js","sourceRoot":"","sources":["../../src/__tests__/image-block.live.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAEjD,MAAM,OAAO,GAAG;IACd,OAAO,EAAE,GAAG;IACZ,KAAK,EAAE,gCAAgC;IACvC,OAAO,EACL,oHAAoH;CACvH,CAAC;AAEF,KAAK,UAAU,IAAI;IACjB,oEAAoE;IACpE,6DAA6D;IAC7D,MAAM,MAAM,GAAG,IAAI,YAAY,CAAC,yBAAyB,EAAE,WAAW,CAAC,CAAC;IAExE,OAAO,CAAC,GAAG,CAAC,wCAAwC,CAAC,CAAC;IACtD,OAAO,CAAC,GAAG,CAAC,YAAY,OAAO,CAAC,KAAK,WAAW,OAAO,CAAC,OAAO,GAAG,CAAC,CAAC;IACpE,OAAO,CAAC,GAAG,CAAC,QAAQ,OAAO,CAAC,OAAO,IAAI,CAAC,CAAC;IAEzC,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACzB,MAAM,KAAK,GAAG,MAAM,UAAU,CAAC,MAAM,EAAE,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;IACrE,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC;IAE9B,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,CAAC,KAAK,CAAC,8CAA8C,EAAE,KAAK,CAAC,CAAC;QACrE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,YAAY,EAAE,KAAK,CAAC,CAAC;IACjC,OAAO,CAAC,GAAG,CAAC,gBAAgB,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;IAC1C,OAAO,CAAC,GAAG,CAAC,gBAAgB,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC;IAC9C,OAAO,CAAC,GAAG,CAAC,gBAAgB,KAAK,CAAC,IAAI,CAAC,MAAM,eAAe,CAAC,CAAC;IAC9D,OAAO,CAAC,GAAG,CAAC,gBAAgB,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC;IAC1D,+BAA+B;IAC/B,MAAM,aAAa,GAAG,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;IACpD,OAAO,CAAC,GAAG,CACT,gBAAgB,aAAa,CAAC,CAAC,CAAC,wBAAwB,CAAC,CAAC,CAAC,kBAAkB,EAAE,CAChF,CAAC;AACJ,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,OAAO,CAAC,KAAK,CAAC,mBAAmB,EAAE,GAAG,CAAC,CAAC;IACxC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|