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.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +129 -0
  3. package/dist/admin/App-Cd-TZrqC.mjs +1179 -0
  4. package/dist/admin/App-DTOuR7aY.js +1199 -0
  5. package/dist/admin/Settings-B3cZR7VF.mjs +516 -0
  6. package/dist/admin/Settings-DHv9SmRU.js +534 -0
  7. package/dist/admin/en-CTCtUg23.mjs +8 -0
  8. package/dist/admin/en-D_61ojL7.js +8 -0
  9. package/dist/admin/index-BLAtUo_j.js +334 -0
  10. package/dist/admin/index-Dj3e3A79.mjs +318 -0
  11. package/dist/admin/index.js +4 -0
  12. package/dist/admin/index.mjs +4 -0
  13. package/dist/admin/src/api.d.ts +107 -0
  14. package/dist/admin/src/components/Initializer.d.ts +5 -0
  15. package/dist/admin/src/components/Panel.d.ts +18 -0
  16. package/dist/admin/src/components/PluginIcon.d.ts +2 -0
  17. package/dist/admin/src/index.d.ts +3 -0
  18. package/dist/admin/src/pages/App.d.ts +2 -0
  19. package/dist/admin/src/pages/HomePage.d.ts +6 -0
  20. package/dist/admin/src/pages/Settings.d.ts +8 -0
  21. package/dist/admin/src/pages/Uploader.d.ts +10 -0
  22. package/dist/admin/src/pluginId.d.ts +1 -0
  23. package/dist/admin/src/theme/tokens.d.ts +96 -0
  24. package/dist/admin/src/utils/getTranslation.d.ts +2 -0
  25. package/dist/admin/tokens-ACCekCH6.mjs +161 -0
  26. package/dist/admin/tokens-CDeNcvZq.js +178 -0
  27. package/dist/server/index.js +1193 -0
  28. package/dist/server/index.mjs +1165 -0
  29. package/dist/server/src/bootstrap.d.ts +5 -0
  30. package/dist/server/src/config/index.d.ts +17 -0
  31. package/dist/server/src/content-types/index.d.ts +51 -0
  32. package/dist/server/src/content-types/track-record/schema.json.d.ts +29 -0
  33. package/dist/server/src/controllers/index.d.ts +21 -0
  34. package/dist/server/src/controllers/settings.d.ts +16 -0
  35. package/dist/server/src/controllers/uploader.d.ts +10 -0
  36. package/dist/server/src/controllers/utils.d.ts +16 -0
  37. package/dist/server/src/destroy.d.ts +5 -0
  38. package/dist/server/src/index.d.ts +267 -0
  39. package/dist/server/src/middlewares/index.d.ts +2 -0
  40. package/dist/server/src/policies/index.d.ts +2 -0
  41. package/dist/server/src/register.d.ts +5 -0
  42. package/dist/server/src/routes/admin/index.d.ts +22 -0
  43. package/dist/server/src/routes/index.d.ts +19 -0
  44. package/dist/server/src/services/id3.d.ts +18 -0
  45. package/dist/server/src/services/index.d.ts +156 -0
  46. package/dist/server/src/services/mapping.d.ts +47 -0
  47. package/dist/server/src/services/radiocult.d.ts +115 -0
  48. package/dist/server/src/services/records.d.ts +25 -0
  49. package/dist/server/src/services/settings.d.ts +103 -0
  50. package/dist/server/src/utils/errors.d.ts +5 -0
  51. package/dist/server/src/utils/plugin.d.ts +4 -0
  52. package/logo.png +0 -0
  53. package/package.json +98 -0
  54. 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;
@@ -0,0 +1,5 @@
1
+ /** Error with an HTTP status the admin routes pass straight to the client. */
2
+ export declare class RcHttpError extends Error {
3
+ status: number;
4
+ constructor(status: number, message: string);
5
+ }
@@ -0,0 +1,4 @@
1
+ export declare const PLUGIN_ID = "radiocult-uploader";
2
+ export declare const TRACK_RECORD_UID: "plugin::radiocult-uploader.track-record";
3
+ export declare const PLATFORMS: readonly ["mixcloud", "soundcloud"];
4
+ export type Platform = (typeof PLATFORMS)[number];
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
+ }