@anymux/unsplash 0.1.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.d.ts +137 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +158 -0
- package/dist/index.js.map +1 -0
- package/package.json +49 -0
- package/src/UnsplashClient.ts +163 -0
- package/src/UnsplashMediaProvider.ts +115 -0
- package/src/index.ts +3 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { IMediaProvider, MediaItem } from "@anymux/ui-kit/media";
|
|
2
|
+
|
|
3
|
+
//#region src/UnsplashClient.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Low-level Unsplash API client.
|
|
6
|
+
*
|
|
7
|
+
* Uses the public Client-ID authentication for read-only access.
|
|
8
|
+
* All photos are free to use under the Unsplash License.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Low-level Unsplash API client.
|
|
12
|
+
*
|
|
13
|
+
* Uses the public Client-ID authentication for read-only access.
|
|
14
|
+
* All photos are free to use under the Unsplash License.
|
|
15
|
+
*/
|
|
16
|
+
interface UnsplashPhoto {
|
|
17
|
+
id: string;
|
|
18
|
+
created_at: string;
|
|
19
|
+
updated_at: string;
|
|
20
|
+
width: number;
|
|
21
|
+
height: number;
|
|
22
|
+
color: string;
|
|
23
|
+
blur_hash: string | null;
|
|
24
|
+
description: string | null;
|
|
25
|
+
alt_description: string | null;
|
|
26
|
+
urls: {
|
|
27
|
+
raw: string;
|
|
28
|
+
full: string;
|
|
29
|
+
regular: string;
|
|
30
|
+
small: string;
|
|
31
|
+
thumb: string;
|
|
32
|
+
};
|
|
33
|
+
links: {
|
|
34
|
+
self: string;
|
|
35
|
+
html: string;
|
|
36
|
+
download: string;
|
|
37
|
+
download_location: string;
|
|
38
|
+
};
|
|
39
|
+
user: {
|
|
40
|
+
id: string;
|
|
41
|
+
username: string;
|
|
42
|
+
name: string;
|
|
43
|
+
portfolio_url: string | null;
|
|
44
|
+
profile_image: {
|
|
45
|
+
small: string;
|
|
46
|
+
medium: string;
|
|
47
|
+
large: string;
|
|
48
|
+
};
|
|
49
|
+
links: {
|
|
50
|
+
html: string;
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
tags?: Array<{
|
|
54
|
+
title: string;
|
|
55
|
+
}>;
|
|
56
|
+
}
|
|
57
|
+
interface UnsplashCollection {
|
|
58
|
+
id: string;
|
|
59
|
+
title: string;
|
|
60
|
+
description: string | null;
|
|
61
|
+
published_at: string;
|
|
62
|
+
updated_at: string;
|
|
63
|
+
total_photos: number;
|
|
64
|
+
cover_photo: UnsplashPhoto | null;
|
|
65
|
+
user: UnsplashPhoto['user'];
|
|
66
|
+
}
|
|
67
|
+
interface UnsplashSearchResult<T> {
|
|
68
|
+
total: number;
|
|
69
|
+
total_pages: number;
|
|
70
|
+
results: T[];
|
|
71
|
+
}
|
|
72
|
+
interface UnsplashClientConfig {
|
|
73
|
+
/** Unsplash API access key (Client-ID) */
|
|
74
|
+
accessKey: string;
|
|
75
|
+
}
|
|
76
|
+
declare class UnsplashClient {
|
|
77
|
+
private accessKey;
|
|
78
|
+
constructor(config: UnsplashClientConfig);
|
|
79
|
+
/** List editorial photos. */
|
|
80
|
+
listPhotos(options?: {
|
|
81
|
+
page?: number;
|
|
82
|
+
perPage?: number;
|
|
83
|
+
orderBy?: 'latest' | 'oldest' | 'popular';
|
|
84
|
+
}): Promise<UnsplashPhoto[]>;
|
|
85
|
+
/** Get a single photo by ID. */
|
|
86
|
+
getPhoto(id: string): Promise<UnsplashPhoto>;
|
|
87
|
+
/** Search photos by query. */
|
|
88
|
+
searchPhotos(options: {
|
|
89
|
+
query: string;
|
|
90
|
+
page?: number;
|
|
91
|
+
perPage?: number;
|
|
92
|
+
orderBy?: 'relevant' | 'latest';
|
|
93
|
+
orientation?: 'landscape' | 'portrait' | 'squarish';
|
|
94
|
+
color?: string;
|
|
95
|
+
}): Promise<UnsplashSearchResult<UnsplashPhoto>>;
|
|
96
|
+
/** List collections. */
|
|
97
|
+
listCollections(options?: {
|
|
98
|
+
page?: number;
|
|
99
|
+
perPage?: number;
|
|
100
|
+
}): Promise<UnsplashCollection[]>;
|
|
101
|
+
/** Get photos in a collection. */
|
|
102
|
+
getCollectionPhotos(collectionId: string, options?: {
|
|
103
|
+
page?: number;
|
|
104
|
+
perPage?: number;
|
|
105
|
+
}): Promise<UnsplashPhoto[]>;
|
|
106
|
+
/** Search collections by query. */
|
|
107
|
+
searchCollections(options: {
|
|
108
|
+
query: string;
|
|
109
|
+
page?: number;
|
|
110
|
+
perPage?: number;
|
|
111
|
+
}): Promise<UnsplashSearchResult<UnsplashCollection>>;
|
|
112
|
+
private get;
|
|
113
|
+
} //#endregion
|
|
114
|
+
//#region src/UnsplashMediaProvider.d.ts
|
|
115
|
+
|
|
116
|
+
//# sourceMappingURL=UnsplashClient.d.ts.map
|
|
117
|
+
declare class UnsplashMediaProvider implements IMediaProvider {
|
|
118
|
+
private client;
|
|
119
|
+
/** Cache of collection ID -> collection for resolving album names. */
|
|
120
|
+
private collectionCache;
|
|
121
|
+
constructor(config: UnsplashClientConfig);
|
|
122
|
+
listItems(): Promise<MediaItem[]>;
|
|
123
|
+
getItem(id: string): Promise<MediaItem | null>;
|
|
124
|
+
createItem(_item: Omit<MediaItem, 'id' | 'createdAt' | 'updatedAt'>): Promise<MediaItem>;
|
|
125
|
+
updateItem(_id: string, _updates: Partial<MediaItem>): Promise<MediaItem>;
|
|
126
|
+
deleteItem(_id: string): Promise<void>;
|
|
127
|
+
search(query: string): Promise<MediaItem[]>;
|
|
128
|
+
getAlbums(): Promise<string[]>;
|
|
129
|
+
getByAlbum(album: string): Promise<MediaItem[]>;
|
|
130
|
+
getByDateRange(start: Date, end: Date): Promise<MediaItem[]>;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
//#endregion
|
|
134
|
+
//# sourceMappingURL=UnsplashMediaProvider.d.ts.map
|
|
135
|
+
|
|
136
|
+
export { UnsplashClient, UnsplashClientConfig, UnsplashCollection, UnsplashMediaProvider, UnsplashPhoto, UnsplashSearchResult };
|
|
137
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/UnsplashClient.ts","../src/UnsplashMediaProvider.ts"],"sourcesContent":null,"mappings":";;;;;;;;;;;;;;;UAWiB,aAAA;;;EAAA,UAAA,EAAA,MAAa;EAkCb,KAAA,EAAA,MAAA;EAAkB,MAAA,EAAA,MAAA;EAAA,KAOpB,EAAA,MAAA;EAAa,SACpB,EAAA,MAAA,GAAA,IAAA;EAAa,WAAA,EAAA,MAAA,GAAA,IAAA;EAGJ,eAAA,EAAA,MAAoB,GAAA,IAAA;EAMpB,IAAA,EAAA;IAKJ,GAAA,EAAA,MAAA;IAAc,IAAA,EAAA,MAAA;IAGL,OAAA,EAAA,MAAA;IASH,KAAA,EAAA,MAAA;IAAR,KAAA,EAAA,MAAA;EAAO,CAAA;EASiC,KAArB,EAAA;IAYK,IAAA,EAAA,MAAA;IAArB,IAAA,EAAA,MAAA;IAAR,QAAA,EAAA,MAAA;IAea,iBAAA,EAAA,MAAA;EAAkB,CAAA;EAAnB,IAWC,EAAA;IAAR,EAAA,EAAA,MAAA;IAYwB,QAAA,EAAA,MAAA;IAArB,IAAA,EAAA,MAAA;IAAR,aAAA,EAAA,MAAA,GAAA,IAAA;IAAO,aAAA,EAAA;;;;ICvGA,CAAA;IAAsB,KAAA,EAAA;MAKb,IAAA,EAAA,MAAA;IAMO,CAAA;EAAS,CAAA;EAAV,IAKS,CAAA,EDT5B,KCS4B,CAAA;IAAR,KAAA,EAAA,MAAA;EAAO,CAAA,CAAA;;AASkD,UDfrE,kBAAA,CCeqE;EAAS,EAAA,EAAjB,MAAA;EAAO,KAInC,EAAA,MAAA;EAAS,WAAjB,EAAA,MAAA,GAAA,IAAA;EAAO,YAAsB,EAAA,MAAA;EAAS,UAAjB,EAAA,MAAA;EAAO,YAIrC,EAAA,MAAA;EAAO,WAID,EDpBxB,aCoBwB,GAAA,IAAA;EAAS,IAAjB,EDnBvB,aCmBuB,CAAA,MAAA,CAAA;;AAgBY,UDhC1B,oBCgC0B,CAAA,CAAA,CAAA,CAAA;EAAS,KAAjB,EAAA,MAAA;EAAO,WAgBZ,EAAA,MAAA;EAAI,OAAO,ED7C9B,CC6C8B,EAAA;;AAAO,UD1C/B,oBAAA,CC0C+B;EAAO;EArEK,SAAA,EAAA,MAAA;;cDgC/C,cAAA;;sBAGS;;;;;;MASX,QAAQ;;wBASW,QAAQ;;;;;;;;;MAYhC,QAAQ,qBAAqB;;;;;MAexB,QAAQ;;;;;MAWR,QAAQ;;;;;;MAYb,QAAQ,qBAAqB;;;;;;cCvGtB,qBAAA,YAAiC;;;;sBAKxB;eAMD,QAAQ;uBAKA,QAAQ;EDxCpB,UAAA,CAAA,KAAa,ECiDJ,IDjDI,CCiDC,SDlBjB,EAAA,IAAA,GAAA,WAAA,GAAA,WAAA,CAAA,CAAA,ECkBgE,ODlBhE,CCkBwE,SDlBxE,CAAA;EAGG,UAAA,CAAA,GAAA,EAAA,MAAkB,EAAA,QAAA,ECmBO,ODnBP,CCmBe,SDnBf,CAAA,CAAA,ECmB4B,ODnB5B,CCmBoC,SDnBpC,CAAA;EAAA,UAAA,CAAA,GAAA,EAAA,MAAA,CAAA,ECuBF,ODvBE,CAAA,IAAA,CAAA;EAAA,MAOpB,CAAA,KAAA,EAAA,MAAA,CAAA,ECoBgB,ODpBhB,CCoBwB,SDpBxB,EAAA,CAAA;EAAa,SACpB,CAAA,CAAA,EC0Ba,OD1Bb,CAAA,MAAA,EAAA,CAAA;EAAa,UAAA,CAAA,KAAA,EAAA,MAAA,CAAA,ECmCc,ODnCd,CCmCsB,SDnCtB,EAAA,CAAA;EAGJ,cAAA,CAAA,KAAA,ECgDa,IDhDO,EAAA,GAAA,ECgDI,ID7C7B,CAAA,EC6CoC,OD7CpC,CC6C4C,SD7C5C,EAAA,CAAA;AAGZ;;;AAKA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
//#region src/UnsplashClient.ts
|
|
2
|
+
/**
|
|
3
|
+
* Low-level Unsplash API client.
|
|
4
|
+
*
|
|
5
|
+
* Uses the public Client-ID authentication for read-only access.
|
|
6
|
+
* All photos are free to use under the Unsplash License.
|
|
7
|
+
*/
|
|
8
|
+
const API_URL = "https://api.unsplash.com";
|
|
9
|
+
var UnsplashClient = class {
|
|
10
|
+
constructor(config) {
|
|
11
|
+
this.accessKey = config.accessKey;
|
|
12
|
+
}
|
|
13
|
+
/** List editorial photos. */
|
|
14
|
+
async listPhotos(options = {}) {
|
|
15
|
+
const params = new URLSearchParams();
|
|
16
|
+
params.set("page", String(options.page ?? 1));
|
|
17
|
+
params.set("per_page", String(options.perPage ?? 30));
|
|
18
|
+
if (options.orderBy) params.set("order_by", options.orderBy);
|
|
19
|
+
return this.get(`/photos?${params}`);
|
|
20
|
+
}
|
|
21
|
+
/** Get a single photo by ID. */
|
|
22
|
+
async getPhoto(id) {
|
|
23
|
+
return this.get(`/photos/${id}`);
|
|
24
|
+
}
|
|
25
|
+
/** Search photos by query. */
|
|
26
|
+
async searchPhotos(options) {
|
|
27
|
+
const params = new URLSearchParams();
|
|
28
|
+
params.set("query", options.query);
|
|
29
|
+
params.set("page", String(options.page ?? 1));
|
|
30
|
+
params.set("per_page", String(options.perPage ?? 30));
|
|
31
|
+
if (options.orderBy) params.set("order_by", options.orderBy);
|
|
32
|
+
if (options.orientation) params.set("orientation", options.orientation);
|
|
33
|
+
if (options.color) params.set("color", options.color);
|
|
34
|
+
return this.get(`/search/photos?${params}`);
|
|
35
|
+
}
|
|
36
|
+
/** List collections. */
|
|
37
|
+
async listCollections(options = {}) {
|
|
38
|
+
const params = new URLSearchParams();
|
|
39
|
+
params.set("page", String(options.page ?? 1));
|
|
40
|
+
params.set("per_page", String(options.perPage ?? 30));
|
|
41
|
+
return this.get(`/collections?${params}`);
|
|
42
|
+
}
|
|
43
|
+
/** Get photos in a collection. */
|
|
44
|
+
async getCollectionPhotos(collectionId, options = {}) {
|
|
45
|
+
const params = new URLSearchParams();
|
|
46
|
+
params.set("page", String(options.page ?? 1));
|
|
47
|
+
params.set("per_page", String(options.perPage ?? 30));
|
|
48
|
+
return this.get(`/collections/${collectionId}/photos?${params}`);
|
|
49
|
+
}
|
|
50
|
+
/** Search collections by query. */
|
|
51
|
+
async searchCollections(options) {
|
|
52
|
+
const params = new URLSearchParams();
|
|
53
|
+
params.set("query", options.query);
|
|
54
|
+
params.set("page", String(options.page ?? 1));
|
|
55
|
+
params.set("per_page", String(options.perPage ?? 30));
|
|
56
|
+
return this.get(`/search/collections?${params}`);
|
|
57
|
+
}
|
|
58
|
+
async get(path) {
|
|
59
|
+
const url = `${API_URL}${path}`;
|
|
60
|
+
const res = await fetch(url, { headers: {
|
|
61
|
+
Authorization: `Client-ID ${this.accessKey}`,
|
|
62
|
+
"Accept-Version": "v1"
|
|
63
|
+
} });
|
|
64
|
+
if (!res.ok) {
|
|
65
|
+
const text = await res.text();
|
|
66
|
+
throw new Error(`Unsplash API error ${res.status}: ${text}`);
|
|
67
|
+
}
|
|
68
|
+
return res.json();
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region src/UnsplashMediaProvider.ts
|
|
74
|
+
function mapPhoto(photo, albumName) {
|
|
75
|
+
const createdAt = new Date(photo.created_at);
|
|
76
|
+
const item = {
|
|
77
|
+
id: photo.id,
|
|
78
|
+
type: "media",
|
|
79
|
+
title: photo.description || photo.alt_description || `Photo ${photo.id}`,
|
|
80
|
+
mediaType: "photo",
|
|
81
|
+
url: photo.urls.regular,
|
|
82
|
+
thumbnail: photo.urls.small,
|
|
83
|
+
mimeType: "image/jpeg",
|
|
84
|
+
width: photo.width,
|
|
85
|
+
height: photo.height,
|
|
86
|
+
createdAt,
|
|
87
|
+
updatedAt: new Date(photo.updated_at)
|
|
88
|
+
};
|
|
89
|
+
if (photo.description) item.description = photo.description;
|
|
90
|
+
if (photo.tags && photo.tags.length > 0) item.tags = photo.tags.map((t) => t.title);
|
|
91
|
+
if (albumName) item.album = albumName;
|
|
92
|
+
return item;
|
|
93
|
+
}
|
|
94
|
+
var UnsplashMediaProvider = class {
|
|
95
|
+
constructor(config) {
|
|
96
|
+
this.collectionCache = new Map();
|
|
97
|
+
this.client = new UnsplashClient(config);
|
|
98
|
+
}
|
|
99
|
+
async listItems() {
|
|
100
|
+
const photos = await this.client.listPhotos({ perPage: 30 });
|
|
101
|
+
return photos.map((p) => mapPhoto(p));
|
|
102
|
+
}
|
|
103
|
+
async getItem(id) {
|
|
104
|
+
try {
|
|
105
|
+
const photo = await this.client.getPhoto(id);
|
|
106
|
+
return mapPhoto(photo);
|
|
107
|
+
} catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async createItem(_item) {
|
|
112
|
+
throw new Error("Unsplash is a read-only photo library");
|
|
113
|
+
}
|
|
114
|
+
async updateItem(_id, _updates) {
|
|
115
|
+
throw new Error("Unsplash is a read-only photo library");
|
|
116
|
+
}
|
|
117
|
+
async deleteItem(_id) {
|
|
118
|
+
throw new Error("Unsplash is a read-only photo library");
|
|
119
|
+
}
|
|
120
|
+
async search(query) {
|
|
121
|
+
const result = await this.client.searchPhotos({
|
|
122
|
+
query,
|
|
123
|
+
perPage: 30
|
|
124
|
+
});
|
|
125
|
+
return result.results.map((p) => mapPhoto(p));
|
|
126
|
+
}
|
|
127
|
+
async getAlbums() {
|
|
128
|
+
const collections = await this.client.listCollections({ perPage: 30 });
|
|
129
|
+
this.collectionCache.clear();
|
|
130
|
+
for (const c of collections) this.collectionCache.set(c.id, c);
|
|
131
|
+
return collections.map((c) => c.title);
|
|
132
|
+
}
|
|
133
|
+
async getByAlbum(album) {
|
|
134
|
+
if (this.collectionCache.size === 0) await this.getAlbums();
|
|
135
|
+
let collectionId;
|
|
136
|
+
for (const [id, c] of this.collectionCache) if (c.title === album) {
|
|
137
|
+
collectionId = id;
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
if (!collectionId) return [];
|
|
141
|
+
const photos = await this.client.getCollectionPhotos(collectionId, { perPage: 30 });
|
|
142
|
+
return photos.map((p) => mapPhoto(p, album));
|
|
143
|
+
}
|
|
144
|
+
async getByDateRange(start, end) {
|
|
145
|
+
const photos = await this.client.listPhotos({
|
|
146
|
+
perPage: 30,
|
|
147
|
+
orderBy: "latest"
|
|
148
|
+
});
|
|
149
|
+
return photos.filter((p) => {
|
|
150
|
+
const d = new Date(p.created_at);
|
|
151
|
+
return d >= start && d <= end;
|
|
152
|
+
}).map((p) => mapPhoto(p));
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
//#endregion
|
|
157
|
+
export { UnsplashClient, UnsplashMediaProvider };
|
|
158
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["config: UnsplashClientConfig","options: {\n page?: number;\n perPage?: number;\n orderBy?: 'latest' | 'oldest' | 'popular';\n }","id: string","options: {\n query: string;\n page?: number;\n perPage?: number;\n orderBy?: 'relevant' | 'latest';\n orientation?: 'landscape' | 'portrait' | 'squarish';\n color?: string;\n }","options: {\n page?: number;\n perPage?: number;\n }","collectionId: string","options: {\n query: string;\n page?: number;\n perPage?: number;\n }","path: string","photo: UnsplashPhoto","albumName?: string","item: MediaItem","config: UnsplashClientConfig","id: string","_item: Omit<MediaItem, 'id' | 'createdAt' | 'updatedAt'>","_id: string","_updates: Partial<MediaItem>","query: string","album: string","collectionId: string | undefined","start: Date","end: Date"],"sources":["../src/UnsplashClient.ts","../src/UnsplashMediaProvider.ts"],"sourcesContent":["/**\n * Low-level Unsplash API client.\n *\n * Uses the public Client-ID authentication for read-only access.\n * All photos are free to use under the Unsplash License.\n */\n\nconst API_URL = 'https://api.unsplash.com';\n\n// ---------- Response shapes ----------\n\nexport interface UnsplashPhoto {\n id: string;\n created_at: string;\n updated_at: string;\n width: number;\n height: number;\n color: string;\n blur_hash: string | null;\n description: string | null;\n alt_description: string | null;\n urls: {\n raw: string;\n full: string;\n regular: string;\n small: string;\n thumb: string;\n };\n links: {\n self: string;\n html: string;\n download: string;\n download_location: string;\n };\n user: {\n id: string;\n username: string;\n name: string;\n portfolio_url: string | null;\n profile_image: { small: string; medium: string; large: string };\n links: { html: string };\n };\n tags?: Array<{ title: string }>;\n}\n\nexport interface UnsplashCollection {\n id: string;\n title: string;\n description: string | null;\n published_at: string;\n updated_at: string;\n total_photos: number;\n cover_photo: UnsplashPhoto | null;\n user: UnsplashPhoto['user'];\n}\n\nexport interface UnsplashSearchResult<T> {\n total: number;\n total_pages: number;\n results: T[];\n}\n\nexport interface UnsplashClientConfig {\n /** Unsplash API access key (Client-ID) */\n accessKey: string;\n}\n\nexport class UnsplashClient {\n private accessKey: string;\n\n constructor(config: UnsplashClientConfig) {\n this.accessKey = config.accessKey;\n }\n\n /** List editorial photos. */\n async listPhotos(options: {\n page?: number;\n perPage?: number;\n orderBy?: 'latest' | 'oldest' | 'popular';\n } = {}): Promise<UnsplashPhoto[]> {\n const params = new URLSearchParams();\n params.set('page', String(options.page ?? 1));\n params.set('per_page', String(options.perPage ?? 30));\n if (options.orderBy) params.set('order_by', options.orderBy);\n return this.get<UnsplashPhoto[]>(`/photos?${params}`);\n }\n\n /** Get a single photo by ID. */\n async getPhoto(id: string): Promise<UnsplashPhoto> {\n return this.get<UnsplashPhoto>(`/photos/${id}`);\n }\n\n /** Search photos by query. */\n async searchPhotos(options: {\n query: string;\n page?: number;\n perPage?: number;\n orderBy?: 'relevant' | 'latest';\n orientation?: 'landscape' | 'portrait' | 'squarish';\n color?: string;\n }): Promise<UnsplashSearchResult<UnsplashPhoto>> {\n const params = new URLSearchParams();\n params.set('query', options.query);\n params.set('page', String(options.page ?? 1));\n params.set('per_page', String(options.perPage ?? 30));\n if (options.orderBy) params.set('order_by', options.orderBy);\n if (options.orientation) params.set('orientation', options.orientation);\n if (options.color) params.set('color', options.color);\n return this.get<UnsplashSearchResult<UnsplashPhoto>>(`/search/photos?${params}`);\n }\n\n /** List collections. */\n async listCollections(options: {\n page?: number;\n perPage?: number;\n } = {}): Promise<UnsplashCollection[]> {\n const params = new URLSearchParams();\n params.set('page', String(options.page ?? 1));\n params.set('per_page', String(options.perPage ?? 30));\n return this.get<UnsplashCollection[]>(`/collections?${params}`);\n }\n\n /** Get photos in a collection. */\n async getCollectionPhotos(collectionId: string, options: {\n page?: number;\n perPage?: number;\n } = {}): Promise<UnsplashPhoto[]> {\n const params = new URLSearchParams();\n params.set('page', String(options.page ?? 1));\n params.set('per_page', String(options.perPage ?? 30));\n return this.get<UnsplashPhoto[]>(`/collections/${collectionId}/photos?${params}`);\n }\n\n /** Search collections by query. */\n async searchCollections(options: {\n query: string;\n page?: number;\n perPage?: number;\n }): Promise<UnsplashSearchResult<UnsplashCollection>> {\n const params = new URLSearchParams();\n params.set('query', options.query);\n params.set('page', String(options.page ?? 1));\n params.set('per_page', String(options.perPage ?? 30));\n return this.get<UnsplashSearchResult<UnsplashCollection>>(`/search/collections?${params}`);\n }\n\n // ---------- Internal ----------\n\n private async get<T>(path: string): Promise<T> {\n const url = `${API_URL}${path}`;\n const res = await fetch(url, {\n headers: {\n Authorization: `Client-ID ${this.accessKey}`,\n 'Accept-Version': 'v1',\n },\n });\n if (!res.ok) {\n const text = await res.text();\n throw new Error(`Unsplash API error ${res.status}: ${text}`);\n }\n return res.json() as Promise<T>;\n }\n}\n","import type { IMediaProvider, MediaItem } from '@anymux/ui-kit/media';\nimport { UnsplashClient } from './UnsplashClient';\nimport type { UnsplashPhoto, UnsplashCollection, UnsplashClientConfig } from './UnsplashClient';\n\nfunction mapPhoto(photo: UnsplashPhoto, albumName?: string): MediaItem {\n const createdAt = new Date(photo.created_at);\n const item: MediaItem = {\n id: photo.id,\n type: 'media',\n title: photo.description || photo.alt_description || `Photo ${photo.id}`,\n mediaType: 'photo',\n url: photo.urls.regular,\n thumbnail: photo.urls.small,\n mimeType: 'image/jpeg',\n width: photo.width,\n height: photo.height,\n createdAt,\n updatedAt: new Date(photo.updated_at),\n };\n\n if (photo.description) {\n item.description = photo.description;\n }\n\n if (photo.tags && photo.tags.length > 0) {\n item.tags = photo.tags.map(t => t.title);\n }\n\n if (albumName) {\n item.album = albumName;\n }\n\n return item;\n}\n\nexport class UnsplashMediaProvider implements IMediaProvider {\n private client: UnsplashClient;\n /** Cache of collection ID -> collection for resolving album names. */\n private collectionCache = new Map<string, UnsplashCollection>();\n\n constructor(config: UnsplashClientConfig) {\n this.client = new UnsplashClient(config);\n }\n\n // ---------- IObjectProvider<MediaItem> ----------\n\n async listItems(): Promise<MediaItem[]> {\n const photos = await this.client.listPhotos({ perPage: 30 });\n return photos.map(p => mapPhoto(p));\n }\n\n async getItem(id: string): Promise<MediaItem | null> {\n try {\n const photo = await this.client.getPhoto(id);\n return mapPhoto(photo);\n } catch {\n return null;\n }\n }\n\n async createItem(_item: Omit<MediaItem, 'id' | 'createdAt' | 'updatedAt'>): Promise<MediaItem> {\n throw new Error('Unsplash is a read-only photo library');\n }\n\n async updateItem(_id: string, _updates: Partial<MediaItem>): Promise<MediaItem> {\n throw new Error('Unsplash is a read-only photo library');\n }\n\n async deleteItem(_id: string): Promise<void> {\n throw new Error('Unsplash is a read-only photo library');\n }\n\n async search(query: string): Promise<MediaItem[]> {\n const result = await this.client.searchPhotos({ query, perPage: 30 });\n return result.results.map(p => mapPhoto(p));\n }\n\n // ---------- IMediaProvider ----------\n\n async getAlbums(): Promise<string[]> {\n const collections = await this.client.listCollections({ perPage: 30 });\n this.collectionCache.clear();\n for (const c of collections) {\n this.collectionCache.set(c.id, c);\n }\n return collections.map(c => c.title);\n }\n\n async getByAlbum(album: string): Promise<MediaItem[]> {\n if (this.collectionCache.size === 0) await this.getAlbums();\n\n let collectionId: string | undefined;\n for (const [id, c] of this.collectionCache) {\n if (c.title === album) {\n collectionId = id;\n break;\n }\n }\n if (!collectionId) return [];\n\n const photos = await this.client.getCollectionPhotos(collectionId, { perPage: 30 });\n return photos.map(p => mapPhoto(p, album));\n }\n\n async getByDateRange(start: Date, end: Date): Promise<MediaItem[]> {\n // Unsplash doesn't have date range search, so we fetch recent and filter client-side\n const photos = await this.client.listPhotos({ perPage: 30, orderBy: 'latest' });\n return photos\n .filter(p => {\n const d = new Date(p.created_at);\n return d >= start && d <= end;\n })\n .map(p => mapPhoto(p));\n }\n}\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@anymux/unsplash",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"description": "Unsplash media provider adapter for AnyMux",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"anymux",
|
|
9
|
+
"unsplash",
|
|
10
|
+
"media",
|
|
11
|
+
"photos",
|
|
12
|
+
"stock-photos"
|
|
13
|
+
],
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/AnyMux/AnyMuxMonorepo.git",
|
|
17
|
+
"directory": "packages/unsplash"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/AnyMux/AnyMuxMonorepo/tree/main/packages/unsplash#readme",
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=20"
|
|
22
|
+
},
|
|
23
|
+
"main": "./src/index.ts",
|
|
24
|
+
"types": "./src/index.ts",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"import": "./dist/index.js"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"src",
|
|
34
|
+
"README.md"
|
|
35
|
+
],
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@anymux/ui-kit": "0.2.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "^22.10.2",
|
|
41
|
+
"tsdown": "^0.10.2",
|
|
42
|
+
"typescript": "^5.7.3",
|
|
43
|
+
"@anymux/typescript-config": "0.0.0"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsdown",
|
|
47
|
+
"check": "tsc -p tsconfig.json --noEmit"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Low-level Unsplash API client.
|
|
3
|
+
*
|
|
4
|
+
* Uses the public Client-ID authentication for read-only access.
|
|
5
|
+
* All photos are free to use under the Unsplash License.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const API_URL = 'https://api.unsplash.com';
|
|
9
|
+
|
|
10
|
+
// ---------- Response shapes ----------
|
|
11
|
+
|
|
12
|
+
export interface UnsplashPhoto {
|
|
13
|
+
id: string;
|
|
14
|
+
created_at: string;
|
|
15
|
+
updated_at: string;
|
|
16
|
+
width: number;
|
|
17
|
+
height: number;
|
|
18
|
+
color: string;
|
|
19
|
+
blur_hash: string | null;
|
|
20
|
+
description: string | null;
|
|
21
|
+
alt_description: string | null;
|
|
22
|
+
urls: {
|
|
23
|
+
raw: string;
|
|
24
|
+
full: string;
|
|
25
|
+
regular: string;
|
|
26
|
+
small: string;
|
|
27
|
+
thumb: string;
|
|
28
|
+
};
|
|
29
|
+
links: {
|
|
30
|
+
self: string;
|
|
31
|
+
html: string;
|
|
32
|
+
download: string;
|
|
33
|
+
download_location: string;
|
|
34
|
+
};
|
|
35
|
+
user: {
|
|
36
|
+
id: string;
|
|
37
|
+
username: string;
|
|
38
|
+
name: string;
|
|
39
|
+
portfolio_url: string | null;
|
|
40
|
+
profile_image: { small: string; medium: string; large: string };
|
|
41
|
+
links: { html: string };
|
|
42
|
+
};
|
|
43
|
+
tags?: Array<{ title: string }>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface UnsplashCollection {
|
|
47
|
+
id: string;
|
|
48
|
+
title: string;
|
|
49
|
+
description: string | null;
|
|
50
|
+
published_at: string;
|
|
51
|
+
updated_at: string;
|
|
52
|
+
total_photos: number;
|
|
53
|
+
cover_photo: UnsplashPhoto | null;
|
|
54
|
+
user: UnsplashPhoto['user'];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface UnsplashSearchResult<T> {
|
|
58
|
+
total: number;
|
|
59
|
+
total_pages: number;
|
|
60
|
+
results: T[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface UnsplashClientConfig {
|
|
64
|
+
/** Unsplash API access key (Client-ID) */
|
|
65
|
+
accessKey: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export class UnsplashClient {
|
|
69
|
+
private accessKey: string;
|
|
70
|
+
|
|
71
|
+
constructor(config: UnsplashClientConfig) {
|
|
72
|
+
this.accessKey = config.accessKey;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** List editorial photos. */
|
|
76
|
+
async listPhotos(options: {
|
|
77
|
+
page?: number;
|
|
78
|
+
perPage?: number;
|
|
79
|
+
orderBy?: 'latest' | 'oldest' | 'popular';
|
|
80
|
+
} = {}): Promise<UnsplashPhoto[]> {
|
|
81
|
+
const params = new URLSearchParams();
|
|
82
|
+
params.set('page', String(options.page ?? 1));
|
|
83
|
+
params.set('per_page', String(options.perPage ?? 30));
|
|
84
|
+
if (options.orderBy) params.set('order_by', options.orderBy);
|
|
85
|
+
return this.get<UnsplashPhoto[]>(`/photos?${params}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Get a single photo by ID. */
|
|
89
|
+
async getPhoto(id: string): Promise<UnsplashPhoto> {
|
|
90
|
+
return this.get<UnsplashPhoto>(`/photos/${id}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Search photos by query. */
|
|
94
|
+
async searchPhotos(options: {
|
|
95
|
+
query: string;
|
|
96
|
+
page?: number;
|
|
97
|
+
perPage?: number;
|
|
98
|
+
orderBy?: 'relevant' | 'latest';
|
|
99
|
+
orientation?: 'landscape' | 'portrait' | 'squarish';
|
|
100
|
+
color?: string;
|
|
101
|
+
}): Promise<UnsplashSearchResult<UnsplashPhoto>> {
|
|
102
|
+
const params = new URLSearchParams();
|
|
103
|
+
params.set('query', options.query);
|
|
104
|
+
params.set('page', String(options.page ?? 1));
|
|
105
|
+
params.set('per_page', String(options.perPage ?? 30));
|
|
106
|
+
if (options.orderBy) params.set('order_by', options.orderBy);
|
|
107
|
+
if (options.orientation) params.set('orientation', options.orientation);
|
|
108
|
+
if (options.color) params.set('color', options.color);
|
|
109
|
+
return this.get<UnsplashSearchResult<UnsplashPhoto>>(`/search/photos?${params}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** List collections. */
|
|
113
|
+
async listCollections(options: {
|
|
114
|
+
page?: number;
|
|
115
|
+
perPage?: number;
|
|
116
|
+
} = {}): Promise<UnsplashCollection[]> {
|
|
117
|
+
const params = new URLSearchParams();
|
|
118
|
+
params.set('page', String(options.page ?? 1));
|
|
119
|
+
params.set('per_page', String(options.perPage ?? 30));
|
|
120
|
+
return this.get<UnsplashCollection[]>(`/collections?${params}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Get photos in a collection. */
|
|
124
|
+
async getCollectionPhotos(collectionId: string, options: {
|
|
125
|
+
page?: number;
|
|
126
|
+
perPage?: number;
|
|
127
|
+
} = {}): Promise<UnsplashPhoto[]> {
|
|
128
|
+
const params = new URLSearchParams();
|
|
129
|
+
params.set('page', String(options.page ?? 1));
|
|
130
|
+
params.set('per_page', String(options.perPage ?? 30));
|
|
131
|
+
return this.get<UnsplashPhoto[]>(`/collections/${collectionId}/photos?${params}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Search collections by query. */
|
|
135
|
+
async searchCollections(options: {
|
|
136
|
+
query: string;
|
|
137
|
+
page?: number;
|
|
138
|
+
perPage?: number;
|
|
139
|
+
}): Promise<UnsplashSearchResult<UnsplashCollection>> {
|
|
140
|
+
const params = new URLSearchParams();
|
|
141
|
+
params.set('query', options.query);
|
|
142
|
+
params.set('page', String(options.page ?? 1));
|
|
143
|
+
params.set('per_page', String(options.perPage ?? 30));
|
|
144
|
+
return this.get<UnsplashSearchResult<UnsplashCollection>>(`/search/collections?${params}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ---------- Internal ----------
|
|
148
|
+
|
|
149
|
+
private async get<T>(path: string): Promise<T> {
|
|
150
|
+
const url = `${API_URL}${path}`;
|
|
151
|
+
const res = await fetch(url, {
|
|
152
|
+
headers: {
|
|
153
|
+
Authorization: `Client-ID ${this.accessKey}`,
|
|
154
|
+
'Accept-Version': 'v1',
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
if (!res.ok) {
|
|
158
|
+
const text = await res.text();
|
|
159
|
+
throw new Error(`Unsplash API error ${res.status}: ${text}`);
|
|
160
|
+
}
|
|
161
|
+
return res.json() as Promise<T>;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { IMediaProvider, MediaItem } from '@anymux/ui-kit/media';
|
|
2
|
+
import { UnsplashClient } from './UnsplashClient';
|
|
3
|
+
import type { UnsplashPhoto, UnsplashCollection, UnsplashClientConfig } from './UnsplashClient';
|
|
4
|
+
|
|
5
|
+
function mapPhoto(photo: UnsplashPhoto, albumName?: string): MediaItem {
|
|
6
|
+
const createdAt = new Date(photo.created_at);
|
|
7
|
+
const item: MediaItem = {
|
|
8
|
+
id: photo.id,
|
|
9
|
+
type: 'media',
|
|
10
|
+
title: photo.description || photo.alt_description || `Photo ${photo.id}`,
|
|
11
|
+
mediaType: 'photo',
|
|
12
|
+
url: photo.urls.regular,
|
|
13
|
+
thumbnail: photo.urls.small,
|
|
14
|
+
mimeType: 'image/jpeg',
|
|
15
|
+
width: photo.width,
|
|
16
|
+
height: photo.height,
|
|
17
|
+
createdAt,
|
|
18
|
+
updatedAt: new Date(photo.updated_at),
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
if (photo.description) {
|
|
22
|
+
item.description = photo.description;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (photo.tags && photo.tags.length > 0) {
|
|
26
|
+
item.tags = photo.tags.map(t => t.title);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (albumName) {
|
|
30
|
+
item.album = albumName;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return item;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class UnsplashMediaProvider implements IMediaProvider {
|
|
37
|
+
private client: UnsplashClient;
|
|
38
|
+
/** Cache of collection ID -> collection for resolving album names. */
|
|
39
|
+
private collectionCache = new Map<string, UnsplashCollection>();
|
|
40
|
+
|
|
41
|
+
constructor(config: UnsplashClientConfig) {
|
|
42
|
+
this.client = new UnsplashClient(config);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ---------- IObjectProvider<MediaItem> ----------
|
|
46
|
+
|
|
47
|
+
async listItems(): Promise<MediaItem[]> {
|
|
48
|
+
const photos = await this.client.listPhotos({ perPage: 30 });
|
|
49
|
+
return photos.map(p => mapPhoto(p));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async getItem(id: string): Promise<MediaItem | null> {
|
|
53
|
+
try {
|
|
54
|
+
const photo = await this.client.getPhoto(id);
|
|
55
|
+
return mapPhoto(photo);
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async createItem(_item: Omit<MediaItem, 'id' | 'createdAt' | 'updatedAt'>): Promise<MediaItem> {
|
|
62
|
+
throw new Error('Unsplash is a read-only photo library');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async updateItem(_id: string, _updates: Partial<MediaItem>): Promise<MediaItem> {
|
|
66
|
+
throw new Error('Unsplash is a read-only photo library');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async deleteItem(_id: string): Promise<void> {
|
|
70
|
+
throw new Error('Unsplash is a read-only photo library');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async search(query: string): Promise<MediaItem[]> {
|
|
74
|
+
const result = await this.client.searchPhotos({ query, perPage: 30 });
|
|
75
|
+
return result.results.map(p => mapPhoto(p));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---------- IMediaProvider ----------
|
|
79
|
+
|
|
80
|
+
async getAlbums(): Promise<string[]> {
|
|
81
|
+
const collections = await this.client.listCollections({ perPage: 30 });
|
|
82
|
+
this.collectionCache.clear();
|
|
83
|
+
for (const c of collections) {
|
|
84
|
+
this.collectionCache.set(c.id, c);
|
|
85
|
+
}
|
|
86
|
+
return collections.map(c => c.title);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async getByAlbum(album: string): Promise<MediaItem[]> {
|
|
90
|
+
if (this.collectionCache.size === 0) await this.getAlbums();
|
|
91
|
+
|
|
92
|
+
let collectionId: string | undefined;
|
|
93
|
+
for (const [id, c] of this.collectionCache) {
|
|
94
|
+
if (c.title === album) {
|
|
95
|
+
collectionId = id;
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (!collectionId) return [];
|
|
100
|
+
|
|
101
|
+
const photos = await this.client.getCollectionPhotos(collectionId, { perPage: 30 });
|
|
102
|
+
return photos.map(p => mapPhoto(p, album));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async getByDateRange(start: Date, end: Date): Promise<MediaItem[]> {
|
|
106
|
+
// Unsplash doesn't have date range search, so we fetch recent and filter client-side
|
|
107
|
+
const photos = await this.client.listPhotos({ perPage: 30, orderBy: 'latest' });
|
|
108
|
+
return photos
|
|
109
|
+
.filter(p => {
|
|
110
|
+
const d = new Date(p.created_at);
|
|
111
|
+
return d >= start && d <= end;
|
|
112
|
+
})
|
|
113
|
+
.map(p => mapPhoto(p));
|
|
114
|
+
}
|
|
115
|
+
}
|
package/src/index.ts
ADDED