@internetarchive/metadata-service 1.0.6 → 1.2.0-beta.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 +18 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/src/backend/default-metadata-backend.d.ts +8 -0
- package/dist/src/backend/default-metadata-backend.js +32 -0
- package/dist/src/backend/default-metadata-backend.js.map +1 -1
- package/dist/src/backend/metadata-backend-interface.d.ts +14 -0
- package/dist/src/backend/metadata-backend-interface.js.map +1 -1
- package/dist/src/metadata-patch.d.ts +21 -0
- package/dist/src/metadata-patch.js +37 -0
- package/dist/src/metadata-patch.js.map +1 -0
- package/dist/src/metadata-service-error.d.ts +3 -1
- package/dist/src/metadata-service-error.js +2 -0
- package/dist/src/metadata-service-error.js.map +1 -1
- package/dist/src/metadata-service-interface.d.ts +42 -0
- package/dist/src/metadata-service-interface.js.map +1 -1
- package/dist/src/metadata-service.d.ts +6 -1
- package/dist/src/metadata-service.js +28 -0
- package/dist/src/metadata-service.js.map +1 -1
- package/dist/src/responses/metadata-response.d.ts +22 -0
- package/dist/src/responses/metadata-response.js +5 -0
- package/dist/src/responses/metadata-response.js.map +1 -1
- package/dist/test/default-metadata-backend.test.js +52 -0
- package/dist/test/default-metadata-backend.test.js.map +1 -1
- package/dist/test/metadata-patch.test.js +41 -0
- package/dist/test/metadata-patch.test.js.map +1 -0
- package/dist/test/metadata-service-write.test.js +96 -0
- package/dist/test/metadata-service-write.test.js.map +1 -0
- package/dist/test/mock-response-generator.js +11 -0
- package/dist/test/mock-response-generator.js.map +1 -1
- package/dist/test/responses/metadata-response.test.d.ts +1 -0
- package/dist/test/responses/metadata-response.test.js +48 -0
- package/dist/test/responses/metadata-response.test.js.map +1 -0
- package/index.ts +13 -1
- package/package.json +2 -2
- package/src/backend/default-metadata-backend.ts +47 -0
- package/src/backend/metadata-backend-interface.ts +19 -0
- package/src/metadata-patch.ts +53 -0
- package/src/metadata-service-error.ts +2 -0
- package/src/metadata-service-interface.ts +56 -0
- package/src/metadata-service.ts +57 -1
- package/src/responses/metadata-response.ts +34 -0
- package/test/default-metadata-backend.test.ts +66 -0
- package/test/metadata-patch.test.ts +53 -0
- package/test/metadata-service-write.test.ts +138 -0
- package/test/mock-response-generator.ts +11 -0
- package/test/responses/metadata-response.test.ts +55 -0
- package/.claude/settings.local.json +0 -15
- package/dist/src/models/file.d.ts +0 -29
- package/dist/src/models/file.js +0 -104
- package/dist/src/models/file.js.map +0 -1
- package/dist/src/models/review.d.ts +0 -11
- package/dist/src/models/review.js +0 -46
- package/dist/src/models/review.js.map +0 -1
- package/dist/src/models/speech-music-asr-entry.d.ts +0 -10
- package/dist/src/models/speech-music-asr-entry.js +0 -2
- package/dist/src/models/speech-music-asr-entry.js.map +0 -1
- package/dist/src/models/task.d.ts +0 -11
- package/dist/src/models/task.js +0 -2
- package/dist/src/models/task.js.map +0 -1
- package/dist/test/models/file.test.js +0 -57
- package/dist/test/models/file.test.js.map +0 -1
- package/dist/test/models/review.test.js +0 -53
- package/dist/test/models/review.test.js.map +0 -1
- /package/dist/test/{models/file.test.d.ts → metadata-patch.test.d.ts} +0 -0
- /package/dist/test/{models/review.test.d.ts → metadata-service-write.test.d.ts} +0 -0
package/src/metadata-service.ts
CHANGED
|
@@ -5,7 +5,15 @@ import {
|
|
|
5
5
|
MetadataServiceError,
|
|
6
6
|
MetadataServiceErrorType,
|
|
7
7
|
} from './metadata-service-error';
|
|
8
|
-
import type {
|
|
8
|
+
import type {
|
|
9
|
+
MetadataServiceInterface,
|
|
10
|
+
MetadataWriteResult,
|
|
11
|
+
} from './metadata-service-interface';
|
|
12
|
+
import {
|
|
13
|
+
buildMetadataFieldPatch,
|
|
14
|
+
type MetadataFieldValue,
|
|
15
|
+
type MetadataPatchOperation,
|
|
16
|
+
} from './metadata-patch';
|
|
9
17
|
import { MetadataResponse } from './responses/metadata-response';
|
|
10
18
|
|
|
11
19
|
/**
|
|
@@ -60,4 +68,52 @@ export class MetadataService implements MetadataServiceInterface {
|
|
|
60
68
|
|
|
61
69
|
return { success: result.success.result };
|
|
62
70
|
}
|
|
71
|
+
|
|
72
|
+
/** @inheritdoc */
|
|
73
|
+
async patchMetadata(
|
|
74
|
+
identifier: string,
|
|
75
|
+
patch: MetadataPatchOperation[],
|
|
76
|
+
target = 'metadata',
|
|
77
|
+
): Promise<Result<MetadataWriteResult, MetadataServiceError>> {
|
|
78
|
+
if (!this.backend.writeMetadata) {
|
|
79
|
+
return {
|
|
80
|
+
error: new MetadataServiceError(
|
|
81
|
+
MetadataServiceErrorType.writeNotSupported,
|
|
82
|
+
'This metadata backend does not support writes',
|
|
83
|
+
),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const result = await this.backend.writeMetadata(identifier, target, patch);
|
|
88
|
+
if (result.error) {
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { success: { changed: true, taskId: result.success?.task_id } };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** @inheritdoc */
|
|
96
|
+
async updateMetadataField(
|
|
97
|
+
identifier: string,
|
|
98
|
+
field: string,
|
|
99
|
+
value: MetadataFieldValue,
|
|
100
|
+
): Promise<Result<MetadataWriteResult, MetadataServiceError>> {
|
|
101
|
+
const current = await this.fetchMetadataValue<
|
|
102
|
+
Record<string, MetadataFieldValue>
|
|
103
|
+
>(identifier, 'metadata');
|
|
104
|
+
if (current.error) {
|
|
105
|
+
return { error: current.error };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const patch = buildMetadataFieldPatch(
|
|
109
|
+
field,
|
|
110
|
+
value,
|
|
111
|
+
current.success?.[field],
|
|
112
|
+
);
|
|
113
|
+
if (patch.length === 0) {
|
|
114
|
+
return { success: { changed: false } };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return this.patchMetadata(identifier, patch);
|
|
118
|
+
}
|
|
63
119
|
}
|
|
@@ -5,6 +5,25 @@ import {
|
|
|
5
5
|
SpeechMusicASREntry,
|
|
6
6
|
} from '@internetarchive/iaux-item-metadata';
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* A single alternate download location: a server and the directory on it that
|
|
10
|
+
* holds the item's files.
|
|
11
|
+
*/
|
|
12
|
+
export type AlternateLocation = {
|
|
13
|
+
server: string;
|
|
14
|
+
dir: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Alternate download locations for an item, beyond the primary `server`/`dir`.
|
|
19
|
+
* `servers` lists all known mirrors; `workable` is the subset currently
|
|
20
|
+
* reachable for downloads.
|
|
21
|
+
*/
|
|
22
|
+
export type AlternateLocations = {
|
|
23
|
+
servers: AlternateLocation[];
|
|
24
|
+
workable: AlternateLocation[];
|
|
25
|
+
};
|
|
26
|
+
|
|
8
27
|
/**
|
|
9
28
|
* The main top-level reponse when fetching Metadata
|
|
10
29
|
*
|
|
@@ -43,6 +62,16 @@ export class MetadataResponse {
|
|
|
43
62
|
|
|
44
63
|
readonly reviews?: Review[];
|
|
45
64
|
|
|
65
|
+
readonly alternate_locations?: AlternateLocations;
|
|
66
|
+
|
|
67
|
+
readonly clips?: Record<string, unknown>;
|
|
68
|
+
|
|
69
|
+
readonly plays?: Record<string, unknown>;
|
|
70
|
+
|
|
71
|
+
readonly simplelists?: Record<string, unknown>;
|
|
72
|
+
|
|
73
|
+
readonly solo?: boolean;
|
|
74
|
+
|
|
46
75
|
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
|
47
76
|
constructor(json: Record<string, any>) {
|
|
48
77
|
this.rawResponse = json;
|
|
@@ -64,5 +93,10 @@ export class MetadataResponse {
|
|
|
64
93
|
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
|
65
94
|
(entry: Record<string, any>) => new Review(entry),
|
|
66
95
|
);
|
|
96
|
+
this.alternate_locations = json.alternate_locations;
|
|
97
|
+
this.clips = json.clips;
|
|
98
|
+
this.plays = json.plays;
|
|
99
|
+
this.simplelists = json.simplelists;
|
|
100
|
+
this.solo = json.solo;
|
|
67
101
|
}
|
|
68
102
|
}
|
|
@@ -102,3 +102,69 @@ describe('DefaultMetadataBackend', () => {
|
|
|
102
102
|
window.fetch = fetchBackup;
|
|
103
103
|
});
|
|
104
104
|
});
|
|
105
|
+
|
|
106
|
+
describe('DefaultMetadataBackend writeMetadata', () => {
|
|
107
|
+
const fetchBackup = window.fetch;
|
|
108
|
+
|
|
109
|
+
afterEach(() => {
|
|
110
|
+
window.fetch = fetchBackup;
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('posts the patch as a form to the item endpoint', async () => {
|
|
114
|
+
let requestUrl = '';
|
|
115
|
+
let requestInit: RequestInit | undefined;
|
|
116
|
+
window.fetch = async (
|
|
117
|
+
input: RequestInfo | URL,
|
|
118
|
+
init?: RequestInit,
|
|
119
|
+
): Promise<Response> => {
|
|
120
|
+
requestUrl = String(input);
|
|
121
|
+
requestInit = init;
|
|
122
|
+
return new Response('{ "success": true, "task_id": 7, "log": "x" }');
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const backend = new DefaultMetadataBackend({ includeCredentials: true });
|
|
126
|
+
const patch = [{ op: 'replace' as const, path: '/title', value: 'New' }];
|
|
127
|
+
const result = await backend.writeMetadata('foo', 'metadata', patch);
|
|
128
|
+
|
|
129
|
+
expect(result.success?.task_id).to.equal(7);
|
|
130
|
+
expect(requestUrl).to.equal('https://archive.org/metadata/foo');
|
|
131
|
+
expect(requestInit?.method).to.equal('POST');
|
|
132
|
+
expect(requestInit?.credentials).to.equal('include');
|
|
133
|
+
const body = requestInit?.body as URLSearchParams;
|
|
134
|
+
expect(body.get('-target')).to.equal('metadata');
|
|
135
|
+
expect(JSON.parse(body.get('-patch') ?? '')).to.deep.equal(patch);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('returns a writeError with MDAPI’s message when the write is rejected', async () => {
|
|
139
|
+
window.fetch = async (): Promise<Response> =>
|
|
140
|
+
new Response('{ "success": false, "error": "Authorization failed" }', {
|
|
141
|
+
status: 401,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const backend = new DefaultMetadataBackend();
|
|
145
|
+
const result = await backend.writeMetadata('foo', 'metadata', []);
|
|
146
|
+
|
|
147
|
+
expect(result.error?.type).to.equal(MetadataServiceErrorType.writeError);
|
|
148
|
+
expect(result.error?.message).to.equal('Authorization failed');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('returns a writeError when MDAPI does not report success', async () => {
|
|
152
|
+
window.fetch = async (): Promise<Response> => new Response('{}');
|
|
153
|
+
|
|
154
|
+
const backend = new DefaultMetadataBackend();
|
|
155
|
+
const result = await backend.writeMetadata('foo', 'metadata', []);
|
|
156
|
+
|
|
157
|
+
expect(result.error?.type).to.equal(MetadataServiceErrorType.writeError);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('returns a networkError when the request fails', async () => {
|
|
161
|
+
window.fetch = async (): Promise<Response> => {
|
|
162
|
+
throw new Error('offline');
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const backend = new DefaultMetadataBackend();
|
|
166
|
+
const result = await backend.writeMetadata('foo', 'metadata', []);
|
|
167
|
+
|
|
168
|
+
expect(result.error?.type).to.equal(MetadataServiceErrorType.networkError);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { expect } from '@open-wc/testing';
|
|
2
|
+
import { buildMetadataFieldPatch } from '../src/metadata-patch';
|
|
3
|
+
|
|
4
|
+
describe('buildMetadataFieldPatch', () => {
|
|
5
|
+
it('replaces an existing field', () => {
|
|
6
|
+
expect(buildMetadataFieldPatch('title', 'New', 'Old')).to.deep.equal([
|
|
7
|
+
{ op: 'replace', path: '/title', value: 'New' },
|
|
8
|
+
]);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('adds a field that is absent', () => {
|
|
12
|
+
expect(buildMetadataFieldPatch('volume', ['2'], undefined)).to.deep.equal([
|
|
13
|
+
{ op: 'add', path: '/volume', value: '2' },
|
|
14
|
+
]);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('writes a single-entry list as a bare string', () => {
|
|
18
|
+
expect(
|
|
19
|
+
buildMetadataFieldPatch('subject', ['one'], ['one', 'two']),
|
|
20
|
+
).to.deep.equal([{ op: 'replace', path: '/subject', value: 'one' }]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('keeps multiple values as a list', () => {
|
|
24
|
+
expect(buildMetadataFieldPatch('subject', ['a', 'b'], 'a')).to.deep.equal([
|
|
25
|
+
{ op: 'replace', path: '/subject', value: ['a', 'b'] },
|
|
26
|
+
]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('removes a field set to nothing', () => {
|
|
30
|
+
expect(buildMetadataFieldPatch('notes', [], 'old')).to.deep.equal([
|
|
31
|
+
{ op: 'remove', path: '/notes' },
|
|
32
|
+
]);
|
|
33
|
+
expect(buildMetadataFieldPatch('notes', '', ['a', 'b'])).to.deep.equal([
|
|
34
|
+
{ op: 'remove', path: '/notes' },
|
|
35
|
+
]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('returns an empty patch when nothing changes', () => {
|
|
39
|
+
expect(buildMetadataFieldPatch('title', ['Same'], 'Same')).to.deep.equal(
|
|
40
|
+
[],
|
|
41
|
+
);
|
|
42
|
+
expect(buildMetadataFieldPatch('title', 'Same', ['Same'])).to.deep.equal(
|
|
43
|
+
[],
|
|
44
|
+
);
|
|
45
|
+
expect(buildMetadataFieldPatch('notes', [], undefined)).to.deep.equal([]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('escapes the key as a JSON Pointer', () => {
|
|
49
|
+
expect(buildMetadataFieldPatch('a/b~c', 'x', undefined)).to.deep.equal([
|
|
50
|
+
{ op: 'add', path: '/a~1b~0c', value: 'x' },
|
|
51
|
+
]);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { expect } from '@open-wc/testing';
|
|
3
|
+
|
|
4
|
+
import { Result } from '@internetarchive/result-type';
|
|
5
|
+
import { MetadataBackendInterface } from '../src/backend/metadata-backend-interface';
|
|
6
|
+
import { MetadataService } from '../src/metadata-service';
|
|
7
|
+
import {
|
|
8
|
+
MetadataServiceError,
|
|
9
|
+
MetadataServiceErrorType,
|
|
10
|
+
} from '../src/metadata-service-error';
|
|
11
|
+
import type { MetadataPatchOperation } from '../src/metadata-patch';
|
|
12
|
+
|
|
13
|
+
class WritableMockBackend implements MetadataBackendInterface {
|
|
14
|
+
current: Record<string, unknown> = {};
|
|
15
|
+
|
|
16
|
+
readError?: MetadataServiceError;
|
|
17
|
+
|
|
18
|
+
writeError?: MetadataServiceError;
|
|
19
|
+
|
|
20
|
+
writes: { identifier: string; target: string; patch: any[] }[] = [];
|
|
21
|
+
|
|
22
|
+
async fetchMetadata(
|
|
23
|
+
_identifier: string,
|
|
24
|
+
keypath?: string,
|
|
25
|
+
): Promise<Result<any, MetadataServiceError>> {
|
|
26
|
+
if (this.readError) return { error: this.readError };
|
|
27
|
+
if (keypath !== 'metadata') return { success: {} };
|
|
28
|
+
return { success: { result: this.current } };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async writeMetadata(
|
|
32
|
+
identifier: string,
|
|
33
|
+
target: string,
|
|
34
|
+
patch: MetadataPatchOperation[],
|
|
35
|
+
): Promise<Result<any, MetadataServiceError>> {
|
|
36
|
+
if (this.writeError) return { error: this.writeError };
|
|
37
|
+
this.writes.push({ identifier, target, patch });
|
|
38
|
+
return { success: { success: true, task_id: 42 } };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe('MetadataService writes', () => {
|
|
43
|
+
describe('patchMetadata', () => {
|
|
44
|
+
it('sends the patch to the metadata target by default', async () => {
|
|
45
|
+
const backend = new WritableMockBackend();
|
|
46
|
+
const service = new MetadataService(backend);
|
|
47
|
+
const patch = [{ op: 'remove' as const, path: '/notes' }];
|
|
48
|
+
|
|
49
|
+
const result = await service.patchMetadata('foo', patch);
|
|
50
|
+
|
|
51
|
+
expect(result.success).to.deep.equal({ changed: true, taskId: 42 });
|
|
52
|
+
expect(backend.writes).to.deep.equal([
|
|
53
|
+
{ identifier: 'foo', target: 'metadata', patch },
|
|
54
|
+
]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('passes a backend error through', async () => {
|
|
58
|
+
const backend = new WritableMockBackend();
|
|
59
|
+
backend.writeError = new MetadataServiceError(
|
|
60
|
+
MetadataServiceErrorType.writeError,
|
|
61
|
+
'nope',
|
|
62
|
+
);
|
|
63
|
+
const service = new MetadataService(backend);
|
|
64
|
+
|
|
65
|
+
const result = await service.patchMetadata('foo', []);
|
|
66
|
+
|
|
67
|
+
expect(result.error?.message).to.equal('nope');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('reports writeNotSupported for a backend without writes', async () => {
|
|
71
|
+
const readOnlyBackend: MetadataBackendInterface = {
|
|
72
|
+
fetchMetadata: async () => ({ success: {} }),
|
|
73
|
+
};
|
|
74
|
+
const service = new MetadataService(readOnlyBackend);
|
|
75
|
+
|
|
76
|
+
const result = await service.patchMetadata('foo', []);
|
|
77
|
+
|
|
78
|
+
expect(result.error?.type).to.equal(
|
|
79
|
+
MetadataServiceErrorType.writeNotSupported,
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('updateMetadataField', () => {
|
|
85
|
+
it('replaces a field MDAPI has', async () => {
|
|
86
|
+
const backend = new WritableMockBackend();
|
|
87
|
+
backend.current = { title: 'Old' };
|
|
88
|
+
const service = new MetadataService(backend);
|
|
89
|
+
|
|
90
|
+
const result = await service.updateMetadataField('foo', 'title', 'New');
|
|
91
|
+
|
|
92
|
+
expect(result.success).to.deep.equal({ changed: true, taskId: 42 });
|
|
93
|
+
expect(backend.writes[0].patch).to.deep.equal([
|
|
94
|
+
{ op: 'replace', path: '/title', value: 'New' },
|
|
95
|
+
]);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('adds a field MDAPI does not have', async () => {
|
|
99
|
+
const backend = new WritableMockBackend();
|
|
100
|
+
backend.current = { title: 'T' };
|
|
101
|
+
const service = new MetadataService(backend);
|
|
102
|
+
|
|
103
|
+
await service.updateMetadataField('foo', 'volume', ['2']);
|
|
104
|
+
|
|
105
|
+
expect(backend.writes[0].patch).to.deep.equal([
|
|
106
|
+
{ op: 'add', path: '/volume', value: '2' },
|
|
107
|
+
]);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('skips the write when MDAPI already has the value', async () => {
|
|
111
|
+
const backend = new WritableMockBackend();
|
|
112
|
+
backend.current = { subject: ['a', 'b'] };
|
|
113
|
+
const service = new MetadataService(backend);
|
|
114
|
+
|
|
115
|
+
const result = await service.updateMetadataField('foo', 'subject', [
|
|
116
|
+
'a',
|
|
117
|
+
'b',
|
|
118
|
+
]);
|
|
119
|
+
|
|
120
|
+
expect(result.success).to.deep.equal({ changed: false });
|
|
121
|
+
expect(backend.writes).to.be.empty;
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('returns the read error without writing', async () => {
|
|
125
|
+
const backend = new WritableMockBackend();
|
|
126
|
+
backend.readError = new MetadataServiceError(
|
|
127
|
+
MetadataServiceErrorType.searchEngineError,
|
|
128
|
+
"Couldn't locate item 'foo'",
|
|
129
|
+
);
|
|
130
|
+
const service = new MetadataService(backend);
|
|
131
|
+
|
|
132
|
+
const result = await service.updateMetadataField('foo', 'title', 'New');
|
|
133
|
+
|
|
134
|
+
expect(result.error?.message).to.equal("Couldn't locate item 'foo'");
|
|
135
|
+
expect(backend.writes).to.be.empty;
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -57,6 +57,17 @@ export class MockResponseGenerator {
|
|
|
57
57
|
server: 'ia800201.us.archive.org',
|
|
58
58
|
uniq: 162444403,
|
|
59
59
|
workable_servers: ['ia800201.us.archive.org', 'ia600201.us.archive.org'],
|
|
60
|
+
alternate_locations: {
|
|
61
|
+
servers: [
|
|
62
|
+
{ server: 'ia800201.us.archive.org', dir: '/27/items/foo' },
|
|
63
|
+
{ server: 'ia600201.us.archive.org', dir: '/27/items/foo' },
|
|
64
|
+
],
|
|
65
|
+
workable: [{ server: 'ia800201.us.archive.org', dir: '/27/items/foo' }],
|
|
66
|
+
},
|
|
67
|
+
clips: { '60|120': [1, 2, 3] },
|
|
68
|
+
plays: {},
|
|
69
|
+
simplelists: {},
|
|
70
|
+
solo: false,
|
|
60
71
|
};
|
|
61
72
|
}
|
|
62
73
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { expect } from '@open-wc/testing';
|
|
2
|
+
|
|
3
|
+
import { MetadataResponse } from '../../src/responses/metadata-response';
|
|
4
|
+
|
|
5
|
+
describe('MetadataResponse', () => {
|
|
6
|
+
it('parses alternate_locations into servers and workable lists', async () => {
|
|
7
|
+
const response = new MetadataResponse({
|
|
8
|
+
metadata: { identifier: 'foo' },
|
|
9
|
+
alternate_locations: {
|
|
10
|
+
servers: [
|
|
11
|
+
{ server: 'ia801234.us.archive.org', dir: '/12/items/foo' },
|
|
12
|
+
{ server: 'ia601234.us.archive.org', dir: '/12/items/foo' },
|
|
13
|
+
],
|
|
14
|
+
workable: [{ server: 'ia801234.us.archive.org', dir: '/12/items/foo' }],
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
expect(response.alternate_locations?.servers).to.have.lengthOf(2);
|
|
18
|
+
expect(response.alternate_locations?.servers[0].server).to.equal(
|
|
19
|
+
'ia801234.us.archive.org',
|
|
20
|
+
);
|
|
21
|
+
expect(response.alternate_locations?.servers[0].dir).to.equal(
|
|
22
|
+
'/12/items/foo',
|
|
23
|
+
);
|
|
24
|
+
expect(response.alternate_locations?.workable).to.have.lengthOf(1);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('exposes the clips, plays, and simplelists maps', async () => {
|
|
28
|
+
const response = new MetadataResponse({
|
|
29
|
+
metadata: { identifier: 'foo' },
|
|
30
|
+
clips: { '60|120': [1, 2, 3] },
|
|
31
|
+
plays: { total: 5 },
|
|
32
|
+
simplelists: {},
|
|
33
|
+
});
|
|
34
|
+
expect(response.clips).to.deep.equal({ '60|120': [1, 2, 3] });
|
|
35
|
+
expect(response.plays).to.deep.equal({ total: 5 });
|
|
36
|
+
expect(response.simplelists).to.deep.equal({});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('exposes the solo flag', async () => {
|
|
40
|
+
const response = new MetadataResponse({
|
|
41
|
+
metadata: { identifier: 'foo' },
|
|
42
|
+
solo: true,
|
|
43
|
+
});
|
|
44
|
+
expect(response.solo).to.be.true;
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('leaves the top-level fields undefined when absent', async () => {
|
|
48
|
+
const response = new MetadataResponse({ metadata: { identifier: 'foo' } });
|
|
49
|
+
expect(response.alternate_locations).to.be.undefined;
|
|
50
|
+
expect(response.clips).to.be.undefined;
|
|
51
|
+
expect(response.plays).to.be.undefined;
|
|
52
|
+
expect(response.simplelists).to.be.undefined;
|
|
53
|
+
expect(response.solo).to.be.undefined;
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"permissions": {
|
|
3
|
-
"allow": [
|
|
4
|
-
"WebFetch(domain:github.com)",
|
|
5
|
-
"Bash(gh api *)",
|
|
6
|
-
"Bash(git -C /Users/jason/Projects/IA/iaux/iaux-metadata-service status)",
|
|
7
|
-
"Bash(git -C /Users/jason/Projects/IA/iaux/iaux-metadata-service log --oneline -10)",
|
|
8
|
-
"Bash(git -C /Users/jason/Projects/IA/iaux/iaux-metadata-service remote -v)",
|
|
9
|
-
"Bash(git *)",
|
|
10
|
-
"Bash(host local.archive.org)",
|
|
11
|
-
"Bash(dig +short local.archive.org)",
|
|
12
|
-
"Bash(mkcert -CAROOT)"
|
|
13
|
-
]
|
|
14
|
-
}
|
|
15
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { Byte, Duration } from '@internetarchive/field-parsers';
|
|
2
|
-
/**
|
|
3
|
-
* This represents an Internet Archive File
|
|
4
|
-
*
|
|
5
|
-
* @export
|
|
6
|
-
* @class File
|
|
7
|
-
*/
|
|
8
|
-
export declare class File {
|
|
9
|
-
readonly rawValue: Readonly<Record<string, any>>;
|
|
10
|
-
get name(): string;
|
|
11
|
-
get source(): string;
|
|
12
|
-
get btih(): string;
|
|
13
|
-
get md5(): string;
|
|
14
|
-
get format(): string;
|
|
15
|
-
get mtime(): Date | undefined;
|
|
16
|
-
get crc32(): string;
|
|
17
|
-
get sha1(): string;
|
|
18
|
-
get original(): string | undefined;
|
|
19
|
-
get size(): Byte | undefined;
|
|
20
|
-
get title(): string | undefined;
|
|
21
|
-
get length(): Duration | undefined;
|
|
22
|
-
get height(): number | undefined;
|
|
23
|
-
get width(): number | undefined;
|
|
24
|
-
get track(): number | undefined;
|
|
25
|
-
get external_identifier(): string | string[] | undefined;
|
|
26
|
-
get creator(): string | undefined;
|
|
27
|
-
get album(): string | undefined;
|
|
28
|
-
constructor(json?: Record<string, any>);
|
|
29
|
-
}
|
package/dist/src/models/file.js
DELETED
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import { __decorate } from "tslib";
|
|
2
|
-
import { Memoize } from 'typescript-memoize';
|
|
3
|
-
import { ByteParser, DurationParser, NumberParser, } from '@internetarchive/field-parsers';
|
|
4
|
-
/**
|
|
5
|
-
* This represents an Internet Archive File
|
|
6
|
-
*
|
|
7
|
-
* @export
|
|
8
|
-
* @class File
|
|
9
|
-
*/
|
|
10
|
-
export class File {
|
|
11
|
-
get name() {
|
|
12
|
-
return this.rawValue.name;
|
|
13
|
-
}
|
|
14
|
-
get source() {
|
|
15
|
-
return this.rawValue.source;
|
|
16
|
-
}
|
|
17
|
-
get btih() {
|
|
18
|
-
return this.rawValue.btih;
|
|
19
|
-
}
|
|
20
|
-
get md5() {
|
|
21
|
-
return this.rawValue.md5;
|
|
22
|
-
}
|
|
23
|
-
get format() {
|
|
24
|
-
return this.rawValue.format;
|
|
25
|
-
}
|
|
26
|
-
get mtime() {
|
|
27
|
-
if (this.rawValue.mtime == null) {
|
|
28
|
-
return undefined;
|
|
29
|
-
}
|
|
30
|
-
const numberValue = NumberParser.shared.parseValue(this.rawValue.mtime);
|
|
31
|
-
if (numberValue) {
|
|
32
|
-
return new Date(numberValue * 1000);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
get crc32() {
|
|
36
|
-
return this.rawValue.crc32;
|
|
37
|
-
}
|
|
38
|
-
get sha1() {
|
|
39
|
-
return this.rawValue.sha1;
|
|
40
|
-
}
|
|
41
|
-
get original() {
|
|
42
|
-
return this.rawValue.original;
|
|
43
|
-
}
|
|
44
|
-
get size() {
|
|
45
|
-
return this.rawValue.size != null
|
|
46
|
-
? ByteParser.shared.parseValue(this.rawValue.size)
|
|
47
|
-
: undefined;
|
|
48
|
-
}
|
|
49
|
-
get title() {
|
|
50
|
-
return this.rawValue.title;
|
|
51
|
-
}
|
|
52
|
-
get length() {
|
|
53
|
-
return this.rawValue.length != null
|
|
54
|
-
? DurationParser.shared.parseValue(this.rawValue.length)
|
|
55
|
-
: undefined;
|
|
56
|
-
}
|
|
57
|
-
get height() {
|
|
58
|
-
return this.rawValue.height != null
|
|
59
|
-
? NumberParser.shared.parseValue(this.rawValue.height)
|
|
60
|
-
: undefined;
|
|
61
|
-
}
|
|
62
|
-
get width() {
|
|
63
|
-
return this.rawValue.width != null
|
|
64
|
-
? NumberParser.shared.parseValue(this.rawValue.width)
|
|
65
|
-
: undefined;
|
|
66
|
-
}
|
|
67
|
-
get track() {
|
|
68
|
-
return this.rawValue.track != null
|
|
69
|
-
? NumberParser.shared.parseValue(this.rawValue.track)
|
|
70
|
-
: undefined;
|
|
71
|
-
}
|
|
72
|
-
get external_identifier() {
|
|
73
|
-
return this.rawValue.external_identifier;
|
|
74
|
-
}
|
|
75
|
-
get creator() {
|
|
76
|
-
return this.rawValue.creator;
|
|
77
|
-
}
|
|
78
|
-
get album() {
|
|
79
|
-
return this.rawValue.album;
|
|
80
|
-
}
|
|
81
|
-
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
|
82
|
-
constructor(json = {}) {
|
|
83
|
-
this.rawValue = json;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
__decorate([
|
|
87
|
-
Memoize()
|
|
88
|
-
], File.prototype, "mtime", null);
|
|
89
|
-
__decorate([
|
|
90
|
-
Memoize()
|
|
91
|
-
], File.prototype, "size", null);
|
|
92
|
-
__decorate([
|
|
93
|
-
Memoize()
|
|
94
|
-
], File.prototype, "length", null);
|
|
95
|
-
__decorate([
|
|
96
|
-
Memoize()
|
|
97
|
-
], File.prototype, "height", null);
|
|
98
|
-
__decorate([
|
|
99
|
-
Memoize()
|
|
100
|
-
], File.prototype, "width", null);
|
|
101
|
-
__decorate([
|
|
102
|
-
Memoize()
|
|
103
|
-
], File.prototype, "track", null);
|
|
104
|
-
//# sourceMappingURL=file.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"file.js","sourceRoot":"","sources":["../../../src/models/file.ts"],"names":[],"mappings":"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","sourcesContent":["import { Memoize } from 'typescript-memoize';\nimport {\n Byte,\n ByteParser,\n Duration,\n DurationParser,\n NumberParser,\n} from '@internetarchive/field-parsers';\n\n/**\n * This represents an Internet Archive File\n *\n * @export\n * @class File\n */\nexport class File {\n /* eslint-disable-next-line @typescript-eslint/no-explicit-any */\n readonly rawValue: Readonly<Record<string, any>>;\n\n get name(): string {\n return this.rawValue.name;\n }\n\n get source(): string {\n return this.rawValue.source;\n }\n\n get btih(): string {\n return this.rawValue.btih;\n }\n\n get md5(): string {\n return this.rawValue.md5;\n }\n\n get format(): string {\n return this.rawValue.format;\n }\n\n @Memoize() get mtime(): Date | undefined {\n if (this.rawValue.mtime == null) {\n return undefined;\n }\n const numberValue = NumberParser.shared.parseValue(this.rawValue.mtime);\n if (numberValue) {\n return new Date(numberValue * 1000);\n }\n }\n\n get crc32(): string {\n return this.rawValue.crc32;\n }\n\n get sha1(): string {\n return this.rawValue.sha1;\n }\n\n get original(): string | undefined {\n return this.rawValue.original;\n }\n\n @Memoize() get size(): Byte | undefined {\n return this.rawValue.size != null\n ? ByteParser.shared.parseValue(this.rawValue.size)\n : undefined;\n }\n\n get title(): string | undefined {\n return this.rawValue.title;\n }\n\n @Memoize() get length(): Duration | undefined {\n return this.rawValue.length != null\n ? DurationParser.shared.parseValue(this.rawValue.length)\n : undefined;\n }\n\n @Memoize() get height(): number | undefined {\n return this.rawValue.height != null\n ? NumberParser.shared.parseValue(this.rawValue.height)\n : undefined;\n }\n\n @Memoize() get width(): number | undefined {\n return this.rawValue.width != null\n ? NumberParser.shared.parseValue(this.rawValue.width)\n : undefined;\n }\n\n @Memoize() get track(): number | undefined {\n return this.rawValue.track != null\n ? NumberParser.shared.parseValue(this.rawValue.track)\n : undefined;\n }\n\n get external_identifier(): string | string[] | undefined {\n return this.rawValue.external_identifier;\n }\n\n get creator(): string | undefined {\n return this.rawValue.creator;\n }\n\n get album(): string | undefined {\n return this.rawValue.album;\n }\n\n /* eslint-disable-next-line @typescript-eslint/no-explicit-any */\n constructor(json: Record<string, any> = {}) {\n this.rawValue = json;\n }\n}\n"]}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
export declare class Review {
|
|
2
|
-
readonly rawValue: Readonly<Record<string, any>>;
|
|
3
|
-
get reviewbody(): string | undefined;
|
|
4
|
-
get reviewtitle(): string | undefined;
|
|
5
|
-
get reviewer(): string | undefined;
|
|
6
|
-
get reviewer_itemname(): string | undefined;
|
|
7
|
-
get reviewdate(): Date | undefined;
|
|
8
|
-
get createdate(): Date | undefined;
|
|
9
|
-
get stars(): number | undefined;
|
|
10
|
-
constructor(json?: Record<string, any>);
|
|
11
|
-
}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { __decorate } from "tslib";
|
|
2
|
-
import { Memoize } from 'typescript-memoize';
|
|
3
|
-
import { DateParser, NumberParser } from '@internetarchive/field-parsers';
|
|
4
|
-
export class Review {
|
|
5
|
-
get reviewbody() {
|
|
6
|
-
return this.rawValue.reviewbody;
|
|
7
|
-
}
|
|
8
|
-
get reviewtitle() {
|
|
9
|
-
return this.rawValue.reviewtitle;
|
|
10
|
-
}
|
|
11
|
-
get reviewer() {
|
|
12
|
-
return this.rawValue.reviewer;
|
|
13
|
-
}
|
|
14
|
-
get reviewer_itemname() {
|
|
15
|
-
return this.rawValue.reviewer_itemname;
|
|
16
|
-
}
|
|
17
|
-
get reviewdate() {
|
|
18
|
-
return this.rawValue.reviewdate != null
|
|
19
|
-
? DateParser.shared.parseValue(this.rawValue.reviewdate)
|
|
20
|
-
: undefined;
|
|
21
|
-
}
|
|
22
|
-
get createdate() {
|
|
23
|
-
return this.rawValue.createdate != null
|
|
24
|
-
? DateParser.shared.parseValue(this.rawValue.createdate)
|
|
25
|
-
: undefined;
|
|
26
|
-
}
|
|
27
|
-
get stars() {
|
|
28
|
-
return this.rawValue.stars != null
|
|
29
|
-
? NumberParser.shared.parseValue(this.rawValue.stars)
|
|
30
|
-
: undefined;
|
|
31
|
-
}
|
|
32
|
-
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
|
33
|
-
constructor(json = {}) {
|
|
34
|
-
this.rawValue = json;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
__decorate([
|
|
38
|
-
Memoize()
|
|
39
|
-
], Review.prototype, "reviewdate", null);
|
|
40
|
-
__decorate([
|
|
41
|
-
Memoize()
|
|
42
|
-
], Review.prototype, "createdate", null);
|
|
43
|
-
__decorate([
|
|
44
|
-
Memoize()
|
|
45
|
-
], Review.prototype, "stars", null);
|
|
46
|
-
//# sourceMappingURL=review.js.map
|