strapi-plugin-radiocult-uploader 1.0.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/LICENSE +21 -0
- package/README.md +129 -0
- package/dist/admin/App-Cd-TZrqC.mjs +1179 -0
- package/dist/admin/App-DTOuR7aY.js +1199 -0
- package/dist/admin/Settings-B3cZR7VF.mjs +516 -0
- package/dist/admin/Settings-DHv9SmRU.js +534 -0
- package/dist/admin/en-CTCtUg23.mjs +8 -0
- package/dist/admin/en-D_61ojL7.js +8 -0
- package/dist/admin/index-BLAtUo_j.js +334 -0
- package/dist/admin/index-Dj3e3A79.mjs +318 -0
- package/dist/admin/index.js +4 -0
- package/dist/admin/index.mjs +4 -0
- package/dist/admin/src/api.d.ts +107 -0
- package/dist/admin/src/components/Initializer.d.ts +5 -0
- package/dist/admin/src/components/Panel.d.ts +18 -0
- package/dist/admin/src/components/PluginIcon.d.ts +2 -0
- package/dist/admin/src/index.d.ts +3 -0
- package/dist/admin/src/pages/App.d.ts +2 -0
- package/dist/admin/src/pages/HomePage.d.ts +6 -0
- package/dist/admin/src/pages/Settings.d.ts +8 -0
- package/dist/admin/src/pages/Uploader.d.ts +10 -0
- package/dist/admin/src/pluginId.d.ts +1 -0
- package/dist/admin/src/theme/tokens.d.ts +96 -0
- package/dist/admin/src/utils/getTranslation.d.ts +2 -0
- package/dist/admin/tokens-ACCekCH6.mjs +161 -0
- package/dist/admin/tokens-CDeNcvZq.js +178 -0
- package/dist/server/index.js +1193 -0
- package/dist/server/index.mjs +1165 -0
- package/dist/server/src/bootstrap.d.ts +5 -0
- package/dist/server/src/config/index.d.ts +17 -0
- package/dist/server/src/content-types/index.d.ts +51 -0
- package/dist/server/src/content-types/track-record/schema.json.d.ts +29 -0
- package/dist/server/src/controllers/index.d.ts +21 -0
- package/dist/server/src/controllers/settings.d.ts +16 -0
- package/dist/server/src/controllers/uploader.d.ts +10 -0
- package/dist/server/src/controllers/utils.d.ts +16 -0
- package/dist/server/src/destroy.d.ts +5 -0
- package/dist/server/src/index.d.ts +267 -0
- package/dist/server/src/middlewares/index.d.ts +2 -0
- package/dist/server/src/policies/index.d.ts +2 -0
- package/dist/server/src/register.d.ts +5 -0
- package/dist/server/src/routes/admin/index.d.ts +22 -0
- package/dist/server/src/routes/index.d.ts +19 -0
- package/dist/server/src/services/id3.d.ts +18 -0
- package/dist/server/src/services/index.d.ts +156 -0
- package/dist/server/src/services/mapping.d.ts +47 -0
- package/dist/server/src/services/radiocult.d.ts +115 -0
- package/dist/server/src/services/records.d.ts +25 -0
- package/dist/server/src/services/settings.d.ts +103 -0
- package/dist/server/src/utils/errors.d.ts +5 -0
- package/dist/server/src/utils/plugin.d.ts +4 -0
- package/logo.png +0 -0
- package/package.json +98 -0
- package/scripts/migrate-from-fields.mjs +78 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Core } from '@strapi/strapi';
|
|
2
|
+
import { FieldMap } from './settings';
|
|
3
|
+
import { Platform } from '../utils/plugin';
|
|
4
|
+
export interface NormalisedEntry {
|
|
5
|
+
documentId: string;
|
|
6
|
+
title: string;
|
|
7
|
+
image: any | null;
|
|
8
|
+
broadcastDate: string | null;
|
|
9
|
+
tags: string[];
|
|
10
|
+
showName: string;
|
|
11
|
+
links: Record<Platform, string | null>;
|
|
12
|
+
raw: any;
|
|
13
|
+
}
|
|
14
|
+
declare const _default: ({ strapi }: {
|
|
15
|
+
strapi: Core.Strapi;
|
|
16
|
+
}) => {
|
|
17
|
+
settings(): Core.Service;
|
|
18
|
+
mapping(): Promise<FieldMap>;
|
|
19
|
+
/** Throws 503 until a content type + title field have been mapped. */
|
|
20
|
+
requireMapping(): Promise<FieldMap>;
|
|
21
|
+
/** Populate object covering every mapped relation/media field. */
|
|
22
|
+
populateFor(mapping: FieldMap): Record<string, true>;
|
|
23
|
+
normalise(mapping: FieldMap, entry: any): NormalisedEntry;
|
|
24
|
+
/** Loads one mapped entry (with relations populated) or throws 404. */
|
|
25
|
+
loadEntry(documentId: string): Promise<NormalisedEntry>;
|
|
26
|
+
/**
|
|
27
|
+
* Writes platform links back onto the user's entry. Only the mapped link
|
|
28
|
+
* fields are touched. Uses the db layer (like the pre-plugin code) so the
|
|
29
|
+
* write lands directly on the stored row without a draft/publish cycle.
|
|
30
|
+
*/
|
|
31
|
+
writeLinks(documentId: string, links: Partial<Record<Platform, string>>): Promise<string[]>;
|
|
32
|
+
/**
|
|
33
|
+
* Content types + attributes for the settings page's mapping selects.
|
|
34
|
+
* Only user-created (api::) types; attributes reduced to what the UI needs.
|
|
35
|
+
*/
|
|
36
|
+
listContentTypes(): {
|
|
37
|
+
uid: string;
|
|
38
|
+
displayName: any;
|
|
39
|
+
attributes: {
|
|
40
|
+
name: string;
|
|
41
|
+
type: any;
|
|
42
|
+
target: any;
|
|
43
|
+
targetAttributes: string[];
|
|
44
|
+
}[];
|
|
45
|
+
}[];
|
|
46
|
+
};
|
|
47
|
+
export default _default;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { Core } from '@strapi/strapi';
|
|
2
|
+
type PlatformState = 'not_published' | 'publishing' | 'linked' | 'stalled';
|
|
3
|
+
declare const _default: ({ strapi }: {
|
|
4
|
+
strapi: Core.Strapi;
|
|
5
|
+
}) => {
|
|
6
|
+
/**
|
|
7
|
+
* Receives the browser upload (a formidable temp file), embeds artwork
|
|
8
|
+
* for mp3s, forwards the file to Radio Cult and stores the returned track
|
|
9
|
+
* id in the plugin's track-record. `meta` carries the (possibly edited)
|
|
10
|
+
* details from the uploader page; anything missing falls back to the
|
|
11
|
+
* entry's own values.
|
|
12
|
+
*/
|
|
13
|
+
uploadTrack({ documentId, file, force, meta, }: {
|
|
14
|
+
documentId: string;
|
|
15
|
+
file: any;
|
|
16
|
+
force: boolean;
|
|
17
|
+
meta?: {
|
|
18
|
+
title?: string;
|
|
19
|
+
artist?: string;
|
|
20
|
+
album?: string;
|
|
21
|
+
};
|
|
22
|
+
}): Promise<{
|
|
23
|
+
trackId: any;
|
|
24
|
+
warnings: string[];
|
|
25
|
+
}>;
|
|
26
|
+
/**
|
|
27
|
+
* Asks Radio Cult to publish the entry's track to Mixcloud or SoundCloud.
|
|
28
|
+
* Radio Cult transfers asynchronously; syncLinks picks up the URL later.
|
|
29
|
+
* `meta` carries the (possibly edited) details from the uploader page.
|
|
30
|
+
*/
|
|
31
|
+
publish({ documentId, platform, force, meta, }: {
|
|
32
|
+
documentId: string;
|
|
33
|
+
platform: string;
|
|
34
|
+
force: boolean;
|
|
35
|
+
meta?: {
|
|
36
|
+
title?: string;
|
|
37
|
+
tags?: unknown;
|
|
38
|
+
description?: string;
|
|
39
|
+
};
|
|
40
|
+
}): Promise<{
|
|
41
|
+
requestedAt: string;
|
|
42
|
+
}>;
|
|
43
|
+
/**
|
|
44
|
+
* Finds entries whose publish was requested but whose link hasn't
|
|
45
|
+
* arrived, fetches Radio Cult's track list ONCE, and writes any links
|
|
46
|
+
* that appeared back onto the mapped fields. Called by the cron sweep,
|
|
47
|
+
* the status endpoint and the "check now" button.
|
|
48
|
+
*/
|
|
49
|
+
syncLinks({ documentIds, bypassCache, }?: {
|
|
50
|
+
documentIds?: string[];
|
|
51
|
+
bypassCache?: boolean;
|
|
52
|
+
}): Promise<{
|
|
53
|
+
updated: number;
|
|
54
|
+
pending: number;
|
|
55
|
+
}>;
|
|
56
|
+
/**
|
|
57
|
+
* Per-entry state for the panel and the uploader page. Includes the
|
|
58
|
+
* entry details the uploader page prefills its editable form with.
|
|
59
|
+
*/
|
|
60
|
+
getStatus(documentId: string, { refresh }?: {
|
|
61
|
+
refresh?: boolean;
|
|
62
|
+
}): Promise<{
|
|
63
|
+
enabled: boolean;
|
|
64
|
+
trackId: any;
|
|
65
|
+
uploadedAt: any;
|
|
66
|
+
platforms: {
|
|
67
|
+
mixcloud: {
|
|
68
|
+
state: PlatformState;
|
|
69
|
+
requestedAt: string;
|
|
70
|
+
link: string;
|
|
71
|
+
};
|
|
72
|
+
soundcloud: {
|
|
73
|
+
state: PlatformState;
|
|
74
|
+
requestedAt: string;
|
|
75
|
+
link: string;
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
rateLimitMessage: string;
|
|
79
|
+
details: {
|
|
80
|
+
title: any;
|
|
81
|
+
showName: any;
|
|
82
|
+
artist: any;
|
|
83
|
+
album: string;
|
|
84
|
+
description: any;
|
|
85
|
+
genres: any;
|
|
86
|
+
image: {
|
|
87
|
+
name: any;
|
|
88
|
+
previewUrl: any;
|
|
89
|
+
usable: boolean;
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
}>;
|
|
93
|
+
/**
|
|
94
|
+
* Pushes edited title/artist/album to an already-uploaded Radio Cult
|
|
95
|
+
* track (the uploader page's "Save details to Radio Cult" button).
|
|
96
|
+
*/
|
|
97
|
+
updateTrackMetadata({ documentId, meta, }: {
|
|
98
|
+
documentId: string;
|
|
99
|
+
meta: {
|
|
100
|
+
title?: string;
|
|
101
|
+
artist?: string;
|
|
102
|
+
album?: string;
|
|
103
|
+
};
|
|
104
|
+
}): Promise<{
|
|
105
|
+
updated: string[];
|
|
106
|
+
}>;
|
|
107
|
+
/**
|
|
108
|
+
* Lifecycle hook body: pushes a changed title to the Radio Cult track.
|
|
109
|
+
* Only fires when the mapped title field actually changed (beforeUpdate
|
|
110
|
+
* stashes the previous value) — the track title can be customised on the
|
|
111
|
+
* uploader page, and an unrelated save must not overwrite it.
|
|
112
|
+
*/
|
|
113
|
+
maybeSyncTitle(event: any): Promise<void>;
|
|
114
|
+
};
|
|
115
|
+
export default _default;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Core } from '@strapi/strapi';
|
|
2
|
+
import { Platform } from '../utils/plugin';
|
|
3
|
+
export interface TrackRecord {
|
|
4
|
+
id: number;
|
|
5
|
+
documentId: string;
|
|
6
|
+
contentTypeUid: string;
|
|
7
|
+
entryDocumentId: string;
|
|
8
|
+
trackId: string | null;
|
|
9
|
+
uploadedAt: string | null;
|
|
10
|
+
mixcloudRequestedAt: string | null;
|
|
11
|
+
soundcloudRequestedAt: string | null;
|
|
12
|
+
lastSyncedAt: string | null;
|
|
13
|
+
}
|
|
14
|
+
export declare const REQUESTED_AT: Record<Platform, 'mixcloudRequestedAt' | 'soundcloudRequestedAt'>;
|
|
15
|
+
declare const _default: ({ strapi }: {
|
|
16
|
+
strapi: Core.Strapi;
|
|
17
|
+
}) => {
|
|
18
|
+
/** The record for one entry, or null. */
|
|
19
|
+
find(contentTypeUid: string, entryDocumentId: string): Promise<TrackRecord | null>;
|
|
20
|
+
/** Creates or updates the record for one entry (service-level uniqueness). */
|
|
21
|
+
upsert(contentTypeUid: string, entryDocumentId: string, data: Partial<TrackRecord>): Promise<TrackRecord>;
|
|
22
|
+
/** All records with an uploaded track for the given content type. */
|
|
23
|
+
findUploaded(contentTypeUid: string, entryDocumentIds?: string[]): Promise<TrackRecord[]>;
|
|
24
|
+
};
|
|
25
|
+
export default _default;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { Core } from '@strapi/strapi';
|
|
2
|
+
export interface FieldMap {
|
|
3
|
+
/** UID of the content type episodes live in, e.g. 'api::episode.episode'. */
|
|
4
|
+
contentType: string | null;
|
|
5
|
+
/** String field holding the episode title. */
|
|
6
|
+
title: string | null;
|
|
7
|
+
/** Media field holding the episode artwork. */
|
|
8
|
+
image: string | null;
|
|
9
|
+
/** Date/datetime field for the broadcast date (used for the default album name). */
|
|
10
|
+
broadcastDate: string | null;
|
|
11
|
+
/** String field the SoundCloud link is written back to. */
|
|
12
|
+
soundcloudLink: string | null;
|
|
13
|
+
/** String field the Mixcloud link is written back to. */
|
|
14
|
+
mixcloudLink: string | null;
|
|
15
|
+
/** Relation field holding genre/tag entries. */
|
|
16
|
+
tagsRelation: string | null;
|
|
17
|
+
/** String attribute on the tag target type used as the tag text. */
|
|
18
|
+
tagsLabelField: string | null;
|
|
19
|
+
/** Optional relation to a parent show (used for the tab title). */
|
|
20
|
+
showRelation: string | null;
|
|
21
|
+
/** String attribute on the show target type used as the show name. */
|
|
22
|
+
showLabelField: string | null;
|
|
23
|
+
}
|
|
24
|
+
export interface ThemeSettings {
|
|
25
|
+
preset: string;
|
|
26
|
+
/** Per-token colour overrides, e.g. { pageBg: '#C0B7AF' }. */
|
|
27
|
+
overrides: Record<string, string>;
|
|
28
|
+
/** Uploader font: 'system' or a Google font key (see admin FONTS). */
|
|
29
|
+
fontFamily: string;
|
|
30
|
+
/** Logo image URL (media library file) shown in the uploader header. */
|
|
31
|
+
logoUrl: string | null;
|
|
32
|
+
/** Station / site name shown under the uploader heading. */
|
|
33
|
+
stationName: string;
|
|
34
|
+
}
|
|
35
|
+
export interface PluginSettings {
|
|
36
|
+
credentials: {
|
|
37
|
+
apiKey: string;
|
|
38
|
+
stationId: string;
|
|
39
|
+
enabled: boolean;
|
|
40
|
+
};
|
|
41
|
+
mapping: FieldMap;
|
|
42
|
+
defaults: {
|
|
43
|
+
artist: string;
|
|
44
|
+
description: string;
|
|
45
|
+
};
|
|
46
|
+
theme: ThemeSettings;
|
|
47
|
+
}
|
|
48
|
+
export declare const DEFAULT_SETTINGS: PluginSettings;
|
|
49
|
+
declare const _default: ({ strapi }: {
|
|
50
|
+
strapi: Core.Strapi;
|
|
51
|
+
}) => {
|
|
52
|
+
store(): {
|
|
53
|
+
get(params?: Partial<{
|
|
54
|
+
key: string;
|
|
55
|
+
type?: string;
|
|
56
|
+
environment?: string;
|
|
57
|
+
name?: string;
|
|
58
|
+
tag?: string;
|
|
59
|
+
}>): Promise<unknown>;
|
|
60
|
+
set(params?: Partial<{
|
|
61
|
+
key: string;
|
|
62
|
+
value: unknown;
|
|
63
|
+
type?: string;
|
|
64
|
+
environment?: string;
|
|
65
|
+
name?: string;
|
|
66
|
+
tag?: string;
|
|
67
|
+
}>): Promise<void>;
|
|
68
|
+
delete(params?: Partial<{
|
|
69
|
+
key: string;
|
|
70
|
+
type?: string;
|
|
71
|
+
environment?: string;
|
|
72
|
+
name?: string;
|
|
73
|
+
tag?: string;
|
|
74
|
+
}>): Promise<void>;
|
|
75
|
+
};
|
|
76
|
+
/** Full settings object, stored values merged over defaults. */
|
|
77
|
+
get(): Promise<PluginSettings>;
|
|
78
|
+
/**
|
|
79
|
+
* Which credential fields are locked by plugin config / env vars. The
|
|
80
|
+
* settings page greys those inputs out and says where the value comes from.
|
|
81
|
+
*/
|
|
82
|
+
envLocks(): {
|
|
83
|
+
apiKey: boolean;
|
|
84
|
+
stationId: boolean;
|
|
85
|
+
enabled: boolean;
|
|
86
|
+
};
|
|
87
|
+
/** Effective credentials: plugin config (env-backed) wins over the store. */
|
|
88
|
+
credentials(): Promise<{
|
|
89
|
+
apiKey: string;
|
|
90
|
+
stationId: string;
|
|
91
|
+
enabled: boolean;
|
|
92
|
+
}>;
|
|
93
|
+
/** Validates a mapping against the live content types. Throws RcHttpError(400). */
|
|
94
|
+
validateMapping(mapping: FieldMap): void;
|
|
95
|
+
/** Merge-updates the stored settings. Returns the new effective settings. */
|
|
96
|
+
update(patch: Partial<PluginSettings>): Promise<PluginSettings>;
|
|
97
|
+
/** Cheap authenticated Radio Cult call to prove the key + station work. */
|
|
98
|
+
testConnection(): Promise<{
|
|
99
|
+
ok: boolean;
|
|
100
|
+
message: string;
|
|
101
|
+
}>;
|
|
102
|
+
};
|
|
103
|
+
export default _default;
|
package/logo.png
ADDED
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "1.0.0",
|
|
3
|
+
"keywords": [
|
|
4
|
+
"strapi",
|
|
5
|
+
"strapi-plugin",
|
|
6
|
+
"radiocult",
|
|
7
|
+
"radio",
|
|
8
|
+
"soundcloud",
|
|
9
|
+
"mixcloud",
|
|
10
|
+
"audio",
|
|
11
|
+
"podcast"
|
|
12
|
+
],
|
|
13
|
+
"type": "commonjs",
|
|
14
|
+
"exports": {
|
|
15
|
+
"./package.json": "./package.json",
|
|
16
|
+
"./strapi-admin": {
|
|
17
|
+
"types": "./dist/admin/src/index.d.ts",
|
|
18
|
+
"source": "./admin/src/index.ts",
|
|
19
|
+
"import": "./dist/admin/index.mjs",
|
|
20
|
+
"require": "./dist/admin/index.js",
|
|
21
|
+
"default": "./dist/admin/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./strapi-server": {
|
|
24
|
+
"types": "./dist/server/src/index.d.ts",
|
|
25
|
+
"source": "./server/src/index.ts",
|
|
26
|
+
"import": "./dist/server/index.mjs",
|
|
27
|
+
"require": "./dist/server/index.js",
|
|
28
|
+
"default": "./dist/server/index.js"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"scripts",
|
|
34
|
+
"README.md",
|
|
35
|
+
"LICENSE",
|
|
36
|
+
"logo.png"
|
|
37
|
+
],
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "strapi-plugin build",
|
|
40
|
+
"watch": "strapi-plugin watch",
|
|
41
|
+
"watch:link": "strapi-plugin watch:link",
|
|
42
|
+
"verify": "strapi-plugin verify",
|
|
43
|
+
"test:ts:front": "tsc -p admin/tsconfig.json --noEmit",
|
|
44
|
+
"test:ts:back": "tsc -p server/tsconfig.json --noEmit",
|
|
45
|
+
"prepublishOnly": "npm run build && npm run verify"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"node-id3": "^0.2.6"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@strapi/design-system": "^2.2.4",
|
|
52
|
+
"@strapi/icons": "^2.2.4",
|
|
53
|
+
"@strapi/sdk-plugin": "^6.1.1",
|
|
54
|
+
"@strapi/strapi": "^5.52.0",
|
|
55
|
+
"@strapi/typescript-utils": "^5.52.0",
|
|
56
|
+
"@types/react": "^18.3.31",
|
|
57
|
+
"@types/react-dom": "^18.3.7",
|
|
58
|
+
"prettier": "^3.9.6",
|
|
59
|
+
"react": "^18.3.1",
|
|
60
|
+
"react-dom": "^18.3.1",
|
|
61
|
+
"react-intl": "^6.8.9",
|
|
62
|
+
"react-router-dom": "^6.30.4",
|
|
63
|
+
"styled-components": "^6.5.2",
|
|
64
|
+
"typescript": "^5.9.3",
|
|
65
|
+
"yalc": "^1.0.0-pre.53"
|
|
66
|
+
},
|
|
67
|
+
"peerDependencies": {
|
|
68
|
+
"@strapi/design-system": "^2.0.0",
|
|
69
|
+
"@strapi/icons": "^2.0.0",
|
|
70
|
+
"@strapi/strapi": "^5.0.0",
|
|
71
|
+
"react": "^18.3.1",
|
|
72
|
+
"react-dom": "^18.3.1",
|
|
73
|
+
"react-intl": "^6.8.9",
|
|
74
|
+
"react-router-dom": "^6.30.4",
|
|
75
|
+
"styled-components": "^6.5.2"
|
|
76
|
+
},
|
|
77
|
+
"strapi": {
|
|
78
|
+
"kind": "plugin",
|
|
79
|
+
"name": "radiocult-uploader",
|
|
80
|
+
"displayName": "Radio Cult Uploader",
|
|
81
|
+
"description": "Upload radio show episodes to Radio Cult and publish them to SoundCloud and Mixcloud from the Strapi admin."
|
|
82
|
+
},
|
|
83
|
+
"name": "strapi-plugin-radiocult-uploader",
|
|
84
|
+
"description": "Upload radio show episodes to Radio Cult and publish them to SoundCloud and Mixcloud from the Strapi admin.",
|
|
85
|
+
"license": "MIT",
|
|
86
|
+
"author": "Tom Willis <tom.willis123456@gmail.com>",
|
|
87
|
+
"engines": {
|
|
88
|
+
"node": ">=20.0.0"
|
|
89
|
+
},
|
|
90
|
+
"repository": {
|
|
91
|
+
"type": "git",
|
|
92
|
+
"url": "git+https://github.com/SisterMidnightFM/Strapi-to-Radio-Cult-Plugin.git"
|
|
93
|
+
},
|
|
94
|
+
"bugs": {
|
|
95
|
+
"url": "https://github.com/SisterMidnightFM/Strapi-to-Radio-Cult-Plugin/issues"
|
|
96
|
+
},
|
|
97
|
+
"homepage": "https://github.com/SisterMidnightFM/Strapi-to-Radio-Cult-Plugin#readme"
|
|
98
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-off migration for installs that previously tracked Radio Cult uploads
|
|
3
|
+
* in fields on their own content type (RadioCultTrackId & co): copies that
|
|
4
|
+
* bookkeeping into the plugin's track-record content type.
|
|
5
|
+
*
|
|
6
|
+
* Run from your Strapi app's root (after `npm run build` if the app is
|
|
7
|
+
* TypeScript):
|
|
8
|
+
*
|
|
9
|
+
* node node_modules/strapi-plugin-radiocult-uploader/scripts/migrate-from-fields.mjs
|
|
10
|
+
* # or from a checkout: node path/to/scripts/migrate-from-fields.mjs
|
|
11
|
+
*
|
|
12
|
+
* Configuration (env vars, defaults in brackets):
|
|
13
|
+
* RCU_SOURCE_UID [api::episode.episode] content type to read from
|
|
14
|
+
* RCU_FIELD_TRACK_ID [RadioCultTrackId]
|
|
15
|
+
* RCU_FIELD_UPLOADED [RadioCultUploadedAt]
|
|
16
|
+
* RCU_FIELD_MIX_REQ [RadioCultMixcloudRequestedAt]
|
|
17
|
+
* RCU_FIELD_SOUND_REQ [RadioCultSoundcloudRequestedAt]
|
|
18
|
+
* RCU_DRY_RUN [false] set to "true" to only print what would happen
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
// Strapi's ESM build cannot be imported under Node 20+ (@strapi/core pulls in
|
|
22
|
+
// lodash/fp as a directory import), so load the CommonJS build instead.
|
|
23
|
+
import { createRequire } from 'node:module';
|
|
24
|
+
const require = createRequire(import.meta.url);
|
|
25
|
+
const { createStrapi, compileStrapi } = require('@strapi/strapi');
|
|
26
|
+
|
|
27
|
+
const UID = process.env.RCU_SOURCE_UID ?? 'api::episode.episode';
|
|
28
|
+
const F_TRACK = process.env.RCU_FIELD_TRACK_ID ?? 'RadioCultTrackId';
|
|
29
|
+
const F_UPLOADED = process.env.RCU_FIELD_UPLOADED ?? 'RadioCultUploadedAt';
|
|
30
|
+
const F_MIX = process.env.RCU_FIELD_MIX_REQ ?? 'RadioCultMixcloudRequestedAt';
|
|
31
|
+
const F_SOUND = process.env.RCU_FIELD_SOUND_REQ ?? 'RadioCultSoundcloudRequestedAt';
|
|
32
|
+
const DRY = process.env.RCU_DRY_RUN === 'true';
|
|
33
|
+
const RECORD_UID = 'plugin::radiocult-uploader.track-record';
|
|
34
|
+
|
|
35
|
+
const appContext = await compileStrapi();
|
|
36
|
+
const app = await createStrapi(appContext).load();
|
|
37
|
+
app.log.level = 'error';
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const rows = await app.db.query(UID).findMany({
|
|
41
|
+
where: { [F_TRACK]: { $notNull: true } },
|
|
42
|
+
});
|
|
43
|
+
console.log(`${rows.length} ${UID} entries with a ${F_TRACK}`);
|
|
44
|
+
|
|
45
|
+
let created = 0;
|
|
46
|
+
let skipped = 0;
|
|
47
|
+
const seen = new Set();
|
|
48
|
+
for (const row of rows) {
|
|
49
|
+
if (seen.has(row.documentId)) continue; // draft+published share a documentId
|
|
50
|
+
seen.add(row.documentId);
|
|
51
|
+
|
|
52
|
+
const existing = await app.db.query(RECORD_UID).findOne({
|
|
53
|
+
where: { contentTypeUid: UID, entryDocumentId: row.documentId },
|
|
54
|
+
});
|
|
55
|
+
if (existing) {
|
|
56
|
+
skipped += 1;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const data = {
|
|
60
|
+
contentTypeUid: UID,
|
|
61
|
+
entryDocumentId: row.documentId,
|
|
62
|
+
trackId: row[F_TRACK],
|
|
63
|
+
uploadedAt: row[F_UPLOADED] ?? null,
|
|
64
|
+
mixcloudRequestedAt: row[F_MIX] ?? null,
|
|
65
|
+
soundcloudRequestedAt: row[F_SOUND] ?? null,
|
|
66
|
+
};
|
|
67
|
+
if (DRY) {
|
|
68
|
+
console.log('would create', data);
|
|
69
|
+
} else {
|
|
70
|
+
await app.db.query(RECORD_UID).create({ data });
|
|
71
|
+
}
|
|
72
|
+
created += 1;
|
|
73
|
+
}
|
|
74
|
+
console.log(`${DRY ? 'Would create' : 'Created'} ${created} track-records, ${skipped} already existed.`);
|
|
75
|
+
console.log('Done. You can now remove the old RadioCult* fields from your schema.');
|
|
76
|
+
} finally {
|
|
77
|
+
await app.destroy();
|
|
78
|
+
}
|