@tomorrowos/sdk 0.5.2 → 0.5.5

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.
@@ -0,0 +1,20 @@
1
+ export interface CloudinaryStorageConfig {
2
+ cloudName: string;
3
+ apiKey: string;
4
+ apiSecret: string;
5
+ folder?: string;
6
+ }
7
+ export interface CloudinaryUploadResult {
8
+ publicId: string;
9
+ secureUrl: string;
10
+ resourceType: string;
11
+ bytes?: number;
12
+ }
13
+ export declare function resolveCloudinaryConfig(env?: NodeJS.ProcessEnv): CloudinaryStorageConfig | null;
14
+ export declare function uploadBufferToCloudinary(body: Buffer, options: {
15
+ config: CloudinaryStorageConfig;
16
+ publicId: string;
17
+ filename: string;
18
+ }): Promise<CloudinaryUploadResult>;
19
+ export declare function deleteCloudinaryAsset(config: CloudinaryStorageConfig, publicId: string, resourceType?: string): Promise<void>;
20
+ //# sourceMappingURL=cloudinary-storage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cloudinary-storage.d.ts","sourceRoot":"","sources":["../src/cloudinary-storage.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,uBAAuB;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAOD,wBAAgB,uBAAuB,CACrC,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,uBAAuB,GAAG,IAAI,CAoBhC;AAWD,wBAAsB,wBAAwB,CAC5C,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE;IACP,MAAM,EAAE,uBAAuB,CAAC;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB,GACA,OAAO,CAAC,sBAAsB,CAAC,CAsCjC;AAED,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,uBAAuB,EAC/B,QAAQ,EAAE,MAAM,EAChB,YAAY,SAAU,GACrB,OAAO,CAAC,IAAI,CAAC,CAKf"}
@@ -0,0 +1,70 @@
1
+ import { v2 as cloudinary } from "cloudinary";
2
+ function cleanOptional(value) {
3
+ const trimmed = String(value || "").trim();
4
+ return trimmed || undefined;
5
+ }
6
+ export function resolveCloudinaryConfig(env = process.env) {
7
+ const cloudName = cleanOptional(env.CLOUDINARY_CLOUD_NAME);
8
+ const apiKey = cleanOptional(env.CLOUDINARY_API_KEY);
9
+ const apiSecret = cleanOptional(env.CLOUDINARY_API_SECRET);
10
+ const hasAny = !!(cloudName || apiKey || apiSecret);
11
+ if (!hasAny)
12
+ return null;
13
+ if (!cloudName || !apiKey || !apiSecret) {
14
+ console.warn("[TomorrowOS] incomplete Cloudinary config; falling back to local uploads.");
15
+ return null;
16
+ }
17
+ return {
18
+ cloudName,
19
+ apiKey,
20
+ apiSecret,
21
+ folder: cleanOptional(env.CLOUDINARY_FOLDER)
22
+ };
23
+ }
24
+ function configureCloudinary(config) {
25
+ cloudinary.config({
26
+ cloud_name: config.cloudName,
27
+ api_key: config.apiKey,
28
+ api_secret: config.apiSecret,
29
+ secure: true
30
+ });
31
+ }
32
+ export async function uploadBufferToCloudinary(body, options) {
33
+ configureCloudinary(options.config);
34
+ const publicId = options.config.folder
35
+ ? `${options.config.folder.replace(/^\/+|\/+$/g, "")}/${options.publicId}`
36
+ : options.publicId;
37
+ const result = await new Promise((resolve, reject) => {
38
+ const stream = cloudinary.uploader.upload_stream({
39
+ resource_type: "auto",
40
+ public_id: publicId,
41
+ overwrite: true,
42
+ unique_filename: false,
43
+ use_filename: false,
44
+ filename_override: options.filename
45
+ }, (err, uploadResult) => {
46
+ if (err) {
47
+ reject(err);
48
+ return;
49
+ }
50
+ if (!uploadResult) {
51
+ reject(new Error("Cloudinary upload returned no result"));
52
+ return;
53
+ }
54
+ resolve(uploadResult);
55
+ });
56
+ stream.end(body);
57
+ });
58
+ return {
59
+ publicId: result.public_id,
60
+ secureUrl: result.secure_url,
61
+ resourceType: result.resource_type,
62
+ bytes: result.bytes
63
+ };
64
+ }
65
+ export async function deleteCloudinaryAsset(config, publicId, resourceType = "image") {
66
+ configureCloudinary(config);
67
+ await cloudinary.uploader.destroy(publicId, {
68
+ resource_type: resourceType
69
+ });
70
+ }
@@ -1,4 +1,4 @@
1
- import type { DevicePlaylistAssignment, DeviceRegistryRecord, PairedDeviceEntry, PairedDeviceRecord, PendingCodeRecord, StoredPlaylist, TomorrowOSStore } from "./types.js";
1
+ import type { DevicePlaylistAssignment, DeviceRegistryRecord, PairedDeviceEntry, PairedDeviceRecord, PendingCodeRecord, StoredPlaylist, TomorrowOSStore, UploadedAssetRecord } from "./types.js";
2
2
  /**
3
3
  * Default store for development / single-node demos.
4
4
  * Data is lost on process restart — not for multi-instance production.
@@ -9,6 +9,7 @@ export declare class MemoryStore implements TomorrowOSStore {
9
9
  private readonly codeToDeviceId;
10
10
  private readonly pairedDevices;
11
11
  private readonly playlists;
12
+ private readonly uploadedAssets;
12
13
  private readonly deviceAssignments;
13
14
  setPendingCode(code: string, record: PendingCodeRecord): Promise<void>;
14
15
  getPendingCode(code: string): Promise<PendingCodeRecord | undefined>;
@@ -27,6 +28,10 @@ export declare class MemoryStore implements TomorrowOSStore {
27
28
  getPlaylist(id: string): Promise<StoredPlaylist | undefined>;
28
29
  setPlaylist(record: StoredPlaylist): Promise<void>;
29
30
  isPlaylistNameTaken(name: string, excludeId?: string): Promise<boolean>;
31
+ getUploadedAsset(id: string): Promise<UploadedAssetRecord | undefined>;
32
+ getUploadedAssetBySha256(sha256: string, storageProvider?: UploadedAssetRecord["storageProvider"]): Promise<UploadedAssetRecord | undefined>;
33
+ setUploadedAsset(record: UploadedAssetRecord): Promise<void>;
34
+ deleteUploadedAsset(id: string): Promise<void>;
30
35
  getDeviceAssignments(deviceId: string): Promise<DevicePlaylistAssignment[]>;
31
36
  setDeviceAssignments(deviceId: string, assignments: DevicePlaylistAssignment[]): Promise<void>;
32
37
  }
@@ -1 +1 @@
1
- {"version":3,"file":"memory-store.d.ts","sourceRoot":"","sources":["../../src/store/memory-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,wBAAwB,EACxB,oBAAoB,EACpB,iBAAiB,EACjB,kBAAkB,EAClB,iBAAiB,EACjB,cAAc,EACd,eAAe,EAChB,MAAM,YAAY,CAAC;AAEpB;;;GAGG;AACH,qBAAa,WAAY,YAAW,eAAe;IACjD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAwC;IACrE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA2C;IAC1E,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA6B;IAC5D,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAyC;IACvE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAqC;IAC/D,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAiD;IAE7E,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAItE,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC;IAIpE,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI9C,iBAAiB,CACrB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,oBAAoB,GAAG,SAAS,CAAC;IAItC,iBAAiB,CACrB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,oBAAoB,GAC3B,OAAO,CAAC,IAAI,CAAC;IASV,uBAAuB,CAC3B,IAAI,EAAE,MAAM,GACX,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,oBAAoB,CAAA;KAAE,GAAG,SAAS,CAAC;IAQpE,eAAe,CACnB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,kBAAkB,GACzB,OAAO,CAAC,IAAI,CAAC;IAIV,eAAe,CACnB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC;IAIpC,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAInD,iBAAiB,IAAI,OAAO,CAAC,iBAAiB,EAAE,CAAC;IAOjD,aAAa,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;IAI1C,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC;IAI5D,WAAW,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAIlD,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAUvE,oBAAoB,CACxB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,wBAAwB,EAAE,CAAC;IAIhC,oBAAoB,CACxB,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,wBAAwB,EAAE,GACtC,OAAO,CAAC,IAAI,CAAC;CASjB"}
1
+ {"version":3,"file":"memory-store.d.ts","sourceRoot":"","sources":["../../src/store/memory-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,wBAAwB,EACxB,oBAAoB,EACpB,iBAAiB,EACjB,kBAAkB,EAClB,iBAAiB,EACjB,cAAc,EACd,eAAe,EACf,mBAAmB,EACpB,MAAM,YAAY,CAAC;AAEpB;;;GAGG;AACH,qBAAa,WAAY,YAAW,eAAe;IACjD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAwC;IACrE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA2C;IAC1E,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA6B;IAC5D,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAyC;IACvE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAqC;IAC/D,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA0C;IACzE,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAiD;IAE7E,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAItE,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC;IAIpE,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI9C,iBAAiB,CACrB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,oBAAoB,GAAG,SAAS,CAAC;IAItC,iBAAiB,CACrB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,oBAAoB,GAC3B,OAAO,CAAC,IAAI,CAAC;IASV,uBAAuB,CAC3B,IAAI,EAAE,MAAM,GACX,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,oBAAoB,CAAA;KAAE,GAAG,SAAS,CAAC;IAQpE,eAAe,CACnB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,kBAAkB,GACzB,OAAO,CAAC,IAAI,CAAC;IAIV,eAAe,CACnB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC;IAIpC,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAInD,iBAAiB,IAAI,OAAO,CAAC,iBAAiB,EAAE,CAAC;IAOjD,aAAa,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;IAI1C,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC;IAI5D,WAAW,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAIlD,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAUvE,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC;IAItE,wBAAwB,CAC5B,MAAM,EAAE,MAAM,EACd,eAAe,CAAC,EAAE,mBAAmB,CAAC,iBAAiB,CAAC,GACvD,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC;IAYrC,gBAAgB,CAAC,MAAM,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAI5D,mBAAmB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI9C,oBAAoB,CACxB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,wBAAwB,EAAE,CAAC;IAIhC,oBAAoB,CACxB,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,wBAAwB,EAAE,GACtC,OAAO,CAAC,IAAI,CAAC;CASjB"}
@@ -8,6 +8,7 @@ export class MemoryStore {
8
8
  codeToDeviceId = new Map();
9
9
  pairedDevices = new Map();
10
10
  playlists = new Map();
11
+ uploadedAssets = new Map();
11
12
  deviceAssignments = new Map();
12
13
  async setPendingCode(code, record) {
13
14
  this.pendingCodes.set(code, record);
@@ -74,6 +75,24 @@ export class MemoryStore {
74
75
  }
75
76
  return false;
76
77
  }
78
+ async getUploadedAsset(id) {
79
+ return this.uploadedAssets.get(id);
80
+ }
81
+ async getUploadedAssetBySha256(sha256, storageProvider) {
82
+ for (const asset of this.uploadedAssets.values()) {
83
+ if (asset.sha256 === sha256 &&
84
+ (!storageProvider || asset.storageProvider === storageProvider)) {
85
+ return asset;
86
+ }
87
+ }
88
+ return undefined;
89
+ }
90
+ async setUploadedAsset(record) {
91
+ this.uploadedAssets.set(record.id, record);
92
+ }
93
+ async deleteUploadedAsset(id) {
94
+ this.uploadedAssets.delete(id);
95
+ }
77
96
  async getDeviceAssignments(deviceId) {
78
97
  return [...(this.deviceAssignments.get(deviceId) ?? [])];
79
98
  }
@@ -4,6 +4,7 @@ export interface MigrationResult {
4
4
  deviceRegistry: number;
5
5
  pairedDevices: number;
6
6
  playlists: number;
7
+ uploadedAssets: number;
7
8
  deviceAssignments: number;
8
9
  }
9
10
  export declare function exportTomorrowOSData(store: TomorrowOSMigratableStore): Promise<TomorrowOSDataSnapshot>;
@@ -1 +1 @@
1
- {"version":3,"file":"migration.d.ts","sourceRoot":"","sources":["../../src/store/migration.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,sBAAsB,EACtB,yBAAyB,EAC1B,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,eAAe;IAC9B,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,yBAAyB,GAC/B,OAAO,CAAC,sBAAsB,CAAC,CA2BjC;AAED,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,yBAAyB,EAChC,QAAQ,EAAE,sBAAsB,GAC/B,OAAO,CAAC,eAAe,CAAC,CA+B1B;AAED,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,yBAAyB,EAC/B,EAAE,EAAE,yBAAyB,GAC5B,OAAO,CAAC,eAAe,CAAC,CAG1B"}
1
+ {"version":3,"file":"migration.d.ts","sourceRoot":"","sources":["../../src/store/migration.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,sBAAsB,EACtB,yBAAyB,EAC1B,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,eAAe;IAC9B,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,yBAAyB,GAC/B,OAAO,CAAC,sBAAsB,CAAC,CA8BjC;AAED,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,yBAAyB,EAChC,QAAQ,EAAE,sBAAsB,GAC/B,OAAO,CAAC,eAAe,CAAC,CAoC1B;AAED,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,yBAAyB,EAC/B,EAAE,EAAE,yBAAyB,GAC5B,OAAO,CAAC,eAAe,CAAC,CAG1B"}
@@ -1,9 +1,10 @@
1
1
  export async function exportTomorrowOSData(store) {
2
- const [pendingCodes, deviceRegistry, pairedDevices, playlists] = await Promise.all([
2
+ const [pendingCodes, deviceRegistry, pairedDevices, playlists, uploadedAssets] = await Promise.all([
3
3
  store.listPendingCodes(),
4
4
  store.listDeviceRegistry(),
5
5
  store.listPairedDevices(),
6
- store.listPlaylists()
6
+ store.listPlaylists(),
7
+ store.listUploadedAssets()
7
8
  ]);
8
9
  const deviceAssignments = await Promise.all(pairedDevices.map(async ({ deviceId }) => ({
9
10
  deviceId,
@@ -14,6 +15,7 @@ export async function exportTomorrowOSData(store) {
14
15
  deviceRegistry,
15
16
  pairedDevices,
16
17
  playlists,
18
+ uploadedAssets,
17
19
  deviceAssignments
18
20
  };
19
21
  }
@@ -27,6 +29,9 @@ export async function importTomorrowOSData(store, snapshot) {
27
29
  for (const playlist of snapshot.playlists) {
28
30
  await store.setPlaylist(playlist);
29
31
  }
32
+ for (const asset of snapshot.uploadedAssets ?? []) {
33
+ await store.setUploadedAsset(asset);
34
+ }
30
35
  for (const { deviceId, assignments } of snapshot.deviceAssignments) {
31
36
  await store.setDeviceAssignments(deviceId, assignments);
32
37
  }
@@ -38,6 +43,7 @@ export async function importTomorrowOSData(store, snapshot) {
38
43
  deviceRegistry: snapshot.deviceRegistry.length,
39
44
  pairedDevices: snapshot.pairedDevices.length,
40
45
  playlists: snapshot.playlists.length,
46
+ uploadedAssets: snapshot.uploadedAssets?.length ?? 0,
41
47
  deviceAssignments: snapshot.deviceAssignments.reduce((total, entry) => total + entry.assignments.length, 0)
42
48
  };
43
49
  }
@@ -1,5 +1,5 @@
1
1
  import { type PoolConfig } from "pg";
2
- import type { DevicePlaylistAssignment, DeviceRegistryEntry, DeviceRegistryRecord, PairedDeviceEntry, PairedDeviceRecord, PendingCodeEntry, PendingCodeRecord, StoredPlaylist, TomorrowOSMigratableStore } from "./types.js";
2
+ import type { DevicePlaylistAssignment, DeviceRegistryEntry, DeviceRegistryRecord, PairedDeviceEntry, PairedDeviceRecord, PendingCodeEntry, PendingCodeRecord, StoredPlaylist, TomorrowOSMigratableStore, UploadedAssetRecord, UploadedAssetStorageProvider } from "./types.js";
3
3
  export interface PostgresStoreOptions {
4
4
  connectionString: string;
5
5
  ssl?: PoolConfig["ssl"];
@@ -34,10 +34,16 @@ export declare class PostgresStore implements TomorrowOSMigratableStore {
34
34
  getPlaylist(id: string): Promise<StoredPlaylist | undefined>;
35
35
  setPlaylist(record: StoredPlaylist): Promise<void>;
36
36
  isPlaylistNameTaken(name: string, excludeId?: string): Promise<boolean>;
37
+ getUploadedAsset(id: string): Promise<UploadedAssetRecord | undefined>;
38
+ getUploadedAssetBySha256(sha256: string, storageProvider?: UploadedAssetStorageProvider): Promise<UploadedAssetRecord | undefined>;
39
+ setUploadedAsset(record: UploadedAssetRecord): Promise<void>;
40
+ deleteUploadedAsset(id: string): Promise<void>;
41
+ listUploadedAssets(): Promise<UploadedAssetRecord[]>;
37
42
  getDeviceAssignments(deviceId: string): Promise<DevicePlaylistAssignment[]>;
38
43
  setDeviceAssignments(deviceId: string, assignments: DevicePlaylistAssignment[]): Promise<void>;
39
44
  private mapDeviceRegistryRow;
40
45
  private mapPairedDeviceRow;
41
46
  private mapPlaylistRow;
47
+ private mapUploadedAssetRow;
42
48
  }
43
49
  //# sourceMappingURL=postgres-store.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"postgres-store.d.ts","sourceRoot":"","sources":["../../src/store/postgres-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,UAAU,EAAE,MAAM,IAAI,CAAC;AAE3C,OAAO,KAAK,EACV,wBAAwB,EACxB,mBAAmB,EACnB,oBAAoB,EACpB,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,EAChB,iBAAiB,EAGjB,cAAc,EACd,yBAAyB,EAC1B,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,oBAAoB;IACnC,gBAAgB,EAAE,MAAM,CAAC;IACzB,GAAG,CAAC,EAAE,UAAU,CAAC,KAAK,CAAC,CAAC;CACzB;AAyDD;;;GAGG;AACH,qBAAa,aAAc,YAAW,yBAAyB;IAC7D,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAO;IAC5B,OAAO,CAAC,KAAK,CAAC,CAAgB;gBAElB,OAAO,EAAE,oBAAoB,GAAG,MAAM;IAY5C,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAsErB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YAId,WAAW;IAKnB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAWtE,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC;IAepE,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAK9C,gBAAgB,IAAI,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAiB/C,iBAAiB,CACrB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,oBAAoB,GAAG,SAAS,CAAC;IAWtC,iBAAiB,CACrB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,oBAAoB,GAC3B,OAAO,CAAC,IAAI,CAAC;IA4BV,uBAAuB,CAC3B,IAAI,EAAE,MAAM,GACX,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,oBAAoB,CAAA;KAAE,GAAG,SAAS,CAAC;IAepE,kBAAkB,IAAI,OAAO,CAAC,mBAAmB,EAAE,CAAC;IAapD,eAAe,CACnB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,kBAAkB,GACzB,OAAO,CAAC,IAAI,CAAC;IAwCV,eAAe,CACnB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC;IAWpC,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAOnD,iBAAiB,IAAI,OAAO,CAAC,iBAAiB,EAAE,CAAC;IAajD,aAAa,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;IAU1C,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC;IAW5D,WAAW,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAkClD,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAevE,oBAAoB,CACxB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,wBAAwB,EAAE,CAAC;IAqBhC,oBAAoB,CACxB,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,wBAAwB,EAAE,GACtC,OAAO,CAAC,IAAI,CAAC;IAuChB,OAAO,CAAC,oBAAoB;IAU5B,OAAO,CAAC,kBAAkB;IAc1B,OAAO,CAAC,cAAc;CAYvB"}
1
+ {"version":3,"file":"postgres-store.d.ts","sourceRoot":"","sources":["../../src/store/postgres-store.ts"],"names":[],"mappings":"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"}
@@ -92,6 +92,24 @@ export class PostgresStore {
92
92
  CREATE INDEX IF NOT EXISTS device_assignments_device_order_idx
93
93
  ON device_assignments (device_id, sort_order);
94
94
 
95
+ CREATE TABLE IF NOT EXISTS uploaded_assets (
96
+ id TEXT PRIMARY KEY,
97
+ sha256 TEXT NOT NULL,
98
+ storage_provider TEXT NOT NULL,
99
+ storage_key TEXT NOT NULL,
100
+ url TEXT NOT NULL,
101
+ original_filename TEXT,
102
+ mime_type TEXT,
103
+ resource_type TEXT,
104
+ bytes BIGINT,
105
+ created_at TEXT NOT NULL,
106
+ updated_at TEXT NOT NULL,
107
+ UNIQUE (storage_provider, sha256)
108
+ );
109
+
110
+ CREATE INDEX IF NOT EXISTS uploaded_assets_sha256_idx
111
+ ON uploaded_assets (sha256);
112
+
95
113
  INSERT INTO schema_migrations (id, name)
96
114
  VALUES (1, 'initial_tomorrowos_store')
97
115
  ON CONFLICT (id) DO NOTHING;
@@ -340,6 +358,82 @@ export class PostgresStore {
340
358
  `, [target, excludeId ?? null]);
341
359
  return result.rows.length > 0;
342
360
  }
361
+ async getUploadedAsset(id) {
362
+ await this.ensureReady();
363
+ const result = await this.pool.query(`
364
+ SELECT *
365
+ FROM uploaded_assets
366
+ WHERE id = $1
367
+ `, [id]);
368
+ const row = result.rows[0];
369
+ return row ? this.mapUploadedAssetRow(row) : undefined;
370
+ }
371
+ async getUploadedAssetBySha256(sha256, storageProvider) {
372
+ await this.ensureReady();
373
+ const result = await this.pool.query(`
374
+ SELECT *
375
+ FROM uploaded_assets
376
+ WHERE sha256 = $1
377
+ AND ($2::text IS NULL OR storage_provider = $2)
378
+ LIMIT 1
379
+ `, [sha256, storageProvider ?? null]);
380
+ const row = result.rows[0];
381
+ return row ? this.mapUploadedAssetRow(row) : undefined;
382
+ }
383
+ async setUploadedAsset(record) {
384
+ await this.ensureReady();
385
+ await this.pool.query(`
386
+ INSERT INTO uploaded_assets (
387
+ id,
388
+ sha256,
389
+ storage_provider,
390
+ storage_key,
391
+ url,
392
+ original_filename,
393
+ mime_type,
394
+ resource_type,
395
+ bytes,
396
+ created_at,
397
+ updated_at
398
+ )
399
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
400
+ ON CONFLICT (id) DO UPDATE SET
401
+ sha256 = EXCLUDED.sha256,
402
+ storage_provider = EXCLUDED.storage_provider,
403
+ storage_key = EXCLUDED.storage_key,
404
+ url = EXCLUDED.url,
405
+ original_filename = EXCLUDED.original_filename,
406
+ mime_type = EXCLUDED.mime_type,
407
+ resource_type = EXCLUDED.resource_type,
408
+ bytes = EXCLUDED.bytes,
409
+ updated_at = EXCLUDED.updated_at
410
+ `, [
411
+ record.id,
412
+ record.sha256,
413
+ record.storageProvider,
414
+ record.storageKey,
415
+ record.url,
416
+ record.originalFilename ?? null,
417
+ record.mimeType ?? null,
418
+ record.resourceType ?? null,
419
+ record.bytes ?? null,
420
+ record.createdAt,
421
+ record.updatedAt
422
+ ]);
423
+ }
424
+ async deleteUploadedAsset(id) {
425
+ await this.ensureReady();
426
+ await this.pool.query("DELETE FROM uploaded_assets WHERE id = $1", [id]);
427
+ }
428
+ async listUploadedAssets() {
429
+ await this.ensureReady();
430
+ const result = await this.pool.query(`
431
+ SELECT *
432
+ FROM uploaded_assets
433
+ ORDER BY created_at ASC
434
+ `);
435
+ return result.rows.map((row) => this.mapUploadedAssetRow(row));
436
+ }
343
437
  async getDeviceAssignments(deviceId) {
344
438
  await this.ensureReady();
345
439
  const result = await this.pool.query(`
@@ -432,4 +526,19 @@ export class PostgresStore {
432
526
  retiredAt: optionalString(row.retired_at)
433
527
  };
434
528
  }
529
+ mapUploadedAssetRow(row) {
530
+ return {
531
+ id: row.id,
532
+ sha256: row.sha256,
533
+ storageProvider: row.storage_provider,
534
+ storageKey: row.storage_key,
535
+ url: row.url,
536
+ originalFilename: optionalString(row.original_filename),
537
+ mimeType: optionalString(row.mime_type),
538
+ resourceType: optionalString(row.resource_type),
539
+ bytes: optionalNumber(row.bytes),
540
+ createdAt: row.created_at,
541
+ updatedAt: row.updated_at
542
+ };
543
+ }
435
544
  }
@@ -1,4 +1,4 @@
1
- import type { DevicePlaylistAssignment, DeviceRegistryEntry, DeviceRegistryRecord, PairedDeviceEntry, PairedDeviceRecord, PendingCodeEntry, PendingCodeRecord, StoredPlaylist, TomorrowOSMigratableStore } from "./types.js";
1
+ import type { DevicePlaylistAssignment, DeviceRegistryEntry, DeviceRegistryRecord, PairedDeviceEntry, PairedDeviceRecord, PendingCodeEntry, PendingCodeRecord, StoredPlaylist, TomorrowOSMigratableStore, UploadedAssetRecord, UploadedAssetStorageProvider } from "./types.js";
2
2
  interface SQLiteStoreOptions {
3
3
  databasePath?: string;
4
4
  }
@@ -31,11 +31,17 @@ export declare class SQLiteStore implements TomorrowOSMigratableStore {
31
31
  getPlaylist(id: string): Promise<StoredPlaylist | undefined>;
32
32
  setPlaylist(record: StoredPlaylist): Promise<void>;
33
33
  isPlaylistNameTaken(name: string, excludeId?: string): Promise<boolean>;
34
+ getUploadedAsset(id: string): Promise<UploadedAssetRecord | undefined>;
35
+ getUploadedAssetBySha256(sha256: string, storageProvider?: UploadedAssetStorageProvider): Promise<UploadedAssetRecord | undefined>;
36
+ setUploadedAsset(record: UploadedAssetRecord): Promise<void>;
37
+ deleteUploadedAsset(id: string): Promise<void>;
38
+ listUploadedAssets(): Promise<UploadedAssetRecord[]>;
34
39
  getDeviceAssignments(deviceId: string): Promise<DevicePlaylistAssignment[]>;
35
40
  setDeviceAssignments(deviceId: string, assignments: DevicePlaylistAssignment[]): Promise<void>;
36
41
  private mapDeviceRegistryRow;
37
42
  private mapPairedDeviceRow;
38
43
  private mapPlaylistRow;
44
+ private mapUploadedAssetRow;
39
45
  }
40
46
  export {};
41
47
  //# sourceMappingURL=sqlite-store.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sqlite-store.d.ts","sourceRoot":"","sources":["../../src/store/sqlite-store.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,wBAAwB,EACxB,mBAAmB,EACnB,oBAAoB,EACpB,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,EAChB,iBAAiB,EAGjB,cAAc,EACd,yBAAyB,EAC1B,MAAM,YAAY,CAAC;AAEpB,UAAU,kBAAkB;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AA+DD;;;GAGG;AACH,qBAAa,WAAY,YAAW,yBAAyB;IAC3D,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAoB;gBAE3B,OAAO,GAAE,kBAAkB,GAAG,MAAW;IAerD,IAAI,IAAI,IAAI;IAwEZ,KAAK,IAAI,IAAI;IAIP,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAUtE,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC;IAUpE,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI9C,gBAAgB,IAAI,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAY/C,iBAAiB,CACrB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,oBAAoB,GAAG,SAAS,CAAC;IAStC,iBAAiB,CACrB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,oBAAoB,GAC3B,OAAO,CAAC,IAAI,CAAC;IA2BV,uBAAuB,CAC3B,IAAI,EAAE,MAAM,GACX,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,oBAAoB,CAAA;KAAE,GAAG,SAAS,CAAC;IAapE,kBAAkB,IAAI,OAAO,CAAC,mBAAmB,EAAE,CAAC;IAYpD,eAAe,CACnB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,kBAAkB,GACzB,OAAO,CAAC,IAAI,CAAC;IAuCV,eAAe,CACnB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC;IASpC,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAInD,iBAAiB,IAAI,OAAO,CAAC,iBAAiB,EAAE,CAAC;IAYjD,aAAa,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;IAS1C,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC;IAS5D,WAAW,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAiClD,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAcvE,oBAAoB,CACxB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,wBAAwB,EAAE,CAAC;IAoBhC,oBAAoB,CACxB,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,wBAAwB,EAAE,GACtC,OAAO,CAAC,IAAI,CAAC;IA8BhB,OAAO,CAAC,oBAAoB;IAU5B,OAAO,CAAC,kBAAkB;IAc1B,OAAO,CAAC,cAAc;CAYvB"}
1
+ {"version":3,"file":"sqlite-store.d.ts","sourceRoot":"","sources":["../../src/store/sqlite-store.ts"],"names":[],"mappings":"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"}
@@ -98,6 +98,24 @@ export class SQLiteStore {
98
98
  CREATE INDEX IF NOT EXISTS device_assignments_device_order_idx
99
99
  ON device_assignments (device_id, sort_order);
100
100
 
101
+ CREATE TABLE IF NOT EXISTS uploaded_assets (
102
+ id TEXT PRIMARY KEY,
103
+ sha256 TEXT NOT NULL,
104
+ storage_provider TEXT NOT NULL,
105
+ storage_key TEXT NOT NULL,
106
+ url TEXT NOT NULL,
107
+ original_filename TEXT,
108
+ mime_type TEXT,
109
+ resource_type TEXT,
110
+ bytes INTEGER,
111
+ created_at TEXT NOT NULL,
112
+ updated_at TEXT NOT NULL,
113
+ UNIQUE (storage_provider, sha256)
114
+ );
115
+
116
+ CREATE INDEX IF NOT EXISTS uploaded_assets_sha256_idx
117
+ ON uploaded_assets (sha256);
118
+
101
119
  INSERT OR IGNORE INTO schema_migrations (id, name)
102
120
  VALUES (1, 'initial_tomorrowos_store');
103
121
  `);
@@ -291,6 +309,63 @@ export class SQLiteStore {
291
309
  `).get(target, excludeId ?? null, excludeId ?? null);
292
310
  return !!row;
293
311
  }
312
+ async getUploadedAsset(id) {
313
+ const row = this.db.prepare(`
314
+ SELECT *
315
+ FROM uploaded_assets
316
+ WHERE id = ?
317
+ `).get(id);
318
+ return row ? this.mapUploadedAssetRow(row) : undefined;
319
+ }
320
+ async getUploadedAssetBySha256(sha256, storageProvider) {
321
+ const row = this.db.prepare(`
322
+ SELECT *
323
+ FROM uploaded_assets
324
+ WHERE sha256 = ?
325
+ AND (? IS NULL OR storage_provider = ?)
326
+ LIMIT 1
327
+ `).get(sha256, storageProvider ?? null, storageProvider ?? null);
328
+ return row ? this.mapUploadedAssetRow(row) : undefined;
329
+ }
330
+ async setUploadedAsset(record) {
331
+ this.db.prepare(`
332
+ INSERT INTO uploaded_assets (
333
+ id,
334
+ sha256,
335
+ storage_provider,
336
+ storage_key,
337
+ url,
338
+ original_filename,
339
+ mime_type,
340
+ resource_type,
341
+ bytes,
342
+ created_at,
343
+ updated_at
344
+ )
345
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
346
+ ON CONFLICT(id) DO UPDATE SET
347
+ sha256 = excluded.sha256,
348
+ storage_provider = excluded.storage_provider,
349
+ storage_key = excluded.storage_key,
350
+ url = excluded.url,
351
+ original_filename = excluded.original_filename,
352
+ mime_type = excluded.mime_type,
353
+ resource_type = excluded.resource_type,
354
+ bytes = excluded.bytes,
355
+ updated_at = excluded.updated_at
356
+ `).run(record.id, record.sha256, record.storageProvider, record.storageKey, record.url, record.originalFilename ?? null, record.mimeType ?? null, record.resourceType ?? null, record.bytes ?? null, record.createdAt, record.updatedAt);
357
+ }
358
+ async deleteUploadedAsset(id) {
359
+ this.db.prepare("DELETE FROM uploaded_assets WHERE id = ?").run(id);
360
+ }
361
+ async listUploadedAssets() {
362
+ const rows = this.db.prepare(`
363
+ SELECT *
364
+ FROM uploaded_assets
365
+ ORDER BY created_at ASC
366
+ `).all();
367
+ return rows.map((row) => this.mapUploadedAssetRow(row));
368
+ }
294
369
  async getDeviceAssignments(deviceId) {
295
370
  const rows = this.db.prepare(`
296
371
  SELECT playlist_id, published_version, published_at, snapshot_json
@@ -364,4 +439,19 @@ export class SQLiteStore {
364
439
  retiredAt: optionalString(row.retired_at)
365
440
  };
366
441
  }
442
+ mapUploadedAssetRow(row) {
443
+ return {
444
+ id: row.id,
445
+ sha256: row.sha256,
446
+ storageProvider: row.storage_provider,
447
+ storageKey: row.storage_key,
448
+ url: row.url,
449
+ originalFilename: optionalString(row.original_filename),
450
+ mimeType: optionalString(row.mime_type),
451
+ resourceType: optionalString(row.resource_type),
452
+ bytes: optionalNumber(row.bytes),
453
+ createdAt: row.created_at,
454
+ updatedAt: row.updated_at
455
+ };
456
+ }
367
457
  }
@@ -39,9 +39,24 @@ export interface DeviceRegistryEntry {
39
39
  }
40
40
  export interface PlaylistItemRecord {
41
41
  url: string;
42
+ assetId?: string;
42
43
  type?: string;
43
44
  durationMs?: number;
44
45
  }
46
+ export type UploadedAssetStorageProvider = "local" | "cloudinary";
47
+ export interface UploadedAssetRecord {
48
+ id: string;
49
+ sha256: string;
50
+ storageProvider: UploadedAssetStorageProvider;
51
+ storageKey: string;
52
+ url: string;
53
+ originalFilename?: string;
54
+ mimeType?: string;
55
+ resourceType?: string;
56
+ bytes?: number;
57
+ createdAt: string;
58
+ updatedAt: string;
59
+ }
45
60
  /** Device-local run window: startDate+start → endDate+end (one continuous period, not daily repeat). */
46
61
  export interface PlaylistSchedule {
47
62
  startDate?: string;
@@ -98,6 +113,10 @@ export interface TomorrowOSStore {
98
113
  getPlaylist(id: string): Promise<StoredPlaylist | undefined>;
99
114
  setPlaylist(record: StoredPlaylist): Promise<void>;
100
115
  isPlaylistNameTaken(name: string, excludeId?: string): Promise<boolean>;
116
+ getUploadedAsset(id: string): Promise<UploadedAssetRecord | undefined>;
117
+ getUploadedAssetBySha256(sha256: string, storageProvider?: UploadedAssetStorageProvider): Promise<UploadedAssetRecord | undefined>;
118
+ setUploadedAsset(record: UploadedAssetRecord): Promise<void>;
119
+ deleteUploadedAsset(id: string): Promise<void>;
101
120
  getDeviceAssignments(deviceId: string): Promise<DevicePlaylistAssignment[]>;
102
121
  setDeviceAssignments(deviceId: string, assignments: DevicePlaylistAssignment[]): Promise<void>;
103
122
  }
@@ -106,6 +125,7 @@ export interface TomorrowOSDataSnapshot {
106
125
  deviceRegistry: DeviceRegistryEntry[];
107
126
  pairedDevices: PairedDeviceEntry[];
108
127
  playlists: StoredPlaylist[];
128
+ uploadedAssets: UploadedAssetRecord[];
109
129
  deviceAssignments: Array<{
110
130
  deviceId: string;
111
131
  assignments: DevicePlaylistAssignment[];
@@ -114,5 +134,6 @@ export interface TomorrowOSDataSnapshot {
114
134
  export interface TomorrowOSMigratableStore extends TomorrowOSStore {
115
135
  listPendingCodes(): Promise<PendingCodeEntry[]>;
116
136
  listDeviceRegistry(): Promise<DeviceRegistryEntry[]>;
137
+ listUploadedAssets(): Promise<UploadedAssetRecord[]>;
117
138
  }
118
139
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/store/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/store/types.ts"],"names":[],"mappings":"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"}
@@ -123,6 +123,11 @@ export declare class TomorrowOS extends EventEmitter {
123
123
  private sendCommandToSocket;
124
124
  listen(options: ListenOptions): http.Server;
125
125
  private handleMediaUpload;
126
+ private storeUploadedMediaAsset;
127
+ private getAssetIdsFromItems;
128
+ private isUploadedAssetReferenced;
129
+ private releaseRemovedPlaylistAssets;
130
+ private deleteUploadedAssetIfUnreferenced;
126
131
  private refreshResolvedBrandLogo;
127
132
  private tryServeStatic;
128
133
  private handleHttp;
@@ -1 +1 @@
1
- {"version":3,"file":"tomorrowos.d.ts","sourceRoot":"","sources":["../src/tomorrowos.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tomorrowos.d.ts","sourceRoot":"","sources":["../src/tomorrowos.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAEtC,OAAO,IAAI,MAAM,MAAM,CAAC;AAUxB,OAAO,EAAE,eAAe,EAAE,KAAK,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAChF,OAAO,KAAK,EAKV,eAAe,EAGhB,MAAM,kBAAkB,CAAC;AAc1B,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,eAAe,CAAC;IACvB,6EAA6E;IAC7E,KAAK,CAAC,EAAE,eAAe,CAAC;CACzB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAsDD,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,2EAA2E;IAC3E,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,kBAAkB,EAAE,OAAO,CAAC;IAC5B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,mEAAmE;IACnE,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,kBAAkB,EAAE,KAAK,CAAC;QACxB,UAAU,EAAE,MAAM,CAAC;QACnB,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,WAAW,EAAE,MAAM,CAAC;KACrB,CAAC,CAAC;IACH,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;AAED,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAwJD,qBAAa,UAAW,SAAQ,YAAY;IAC1C,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAC;IAChC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAkB;IACxC,QAAQ,CAAC,SAAS,EAAE,eAAe,CAAC;IACpC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmC;IAC3D,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAsC;IACxE,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAuC;IAClE,OAAO,CAAC,UAAU,CAA4B;IAC9C,OAAO,CAAC,GAAG,CAAgC;IAC3C,OAAO,CAAC,UAAU,CAAuB;IACzC,OAAO,CAAC,eAAe,CAAgB;gBAE3B,OAAO,EAAE,iBAAiB;IAOtC,yFAAyF;IACnF,wBAAwB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;QACxD,MAAM,EAAE,OAAO,CAAC;QAChB,MAAM,CAAC,EAAE,iBAAiB,CAAC,QAAQ,CAAC,CAAC;KACtC,CAAC;YAkBY,iBAAiB;IAY/B,6EAA6E;IACvE,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAgEhE,6EAA6E;IACvE,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,OAAO,CAAA;KAAE,CAAC;IA4BvF,OAAO;uBACU,MAAM;sBA9FgC,MAAM;;2BA+FxC,MAAM;sBA9BgC,MAAM;sBAAY,OAAO;;MA+BlF;IAEF,kFAAkF;IAC5E,WAAW,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;IAsD9C,OAAO,CAAC,aAAa;IASrB,OAAO,CAAC,aAAa;IAMrB,OAAO,CAAC,iBAAiB;IAKzB,OAAO,CAAC,gBAAgB;YAeV,+BAA+B;YAiC/B,iBAAiB;YASjB,iBAAiB;YA4BjB,kBAAkB;IAUhC,uFAAuF;IACvF,OAAO,CAAC,kBAAkB;YAmBZ,gBAAgB;YAMhB,uBAAuB;IAuCrC,MAAM,CAAC,QAAQ,EAAE,MAAM;oBAGD,CAAC,oBACT,MAAM,WACN,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,OAAO,CAAC;YAAE,MAAM,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YAAC,KAAK,CAAC,EAAE,MAAM,CAAC;YAAC,KAAK,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;;IAU5E,OAAO,CAAC,mBAAmB;IA6D3B,MAAM,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI,CAAC,MAAM;YAmD7B,iBAAiB;YAiDjB,uBAAuB;IAqFrC,OAAO,CAAC,oBAAoB;YASd,yBAAyB;YAQzB,4BAA4B;YAa5B,iCAAiC;YA4BjC,wBAAwB;YAiBxB,cAAc;YAiDd,UAAU;YAyRV,gBAAgB;IAqG9B,OAAO,CAAC,iBAAiB;IAWzB,OAAO,CAAC,gBAAgB;CA4HzB"}
@@ -8,8 +8,9 @@ import { generateRandomPairingCode, isValidPairingCodeFormat, normalizePairingCo
8
8
  import { PlaylistCatalog } from "./playlist-catalog.js";
9
9
  import { MemoryStore } from "./store/memory-store.js";
10
10
  import { resolveBrandLogoPath, syncProjectAssetsToStaticRoot } from "./brand-assets.js";
11
+ import { deleteCloudinaryAsset, resolveCloudinaryConfig, uploadBufferToCloudinary } from "./cloudinary-storage.js";
11
12
  import { probeVideoDurationMs } from "./media-probe.js";
12
- import { storeUploadIfNeeded } from "./upload-storage.js";
13
+ import { contentHashHex, storeUploadIfNeeded } from "./upload-storage.js";
13
14
  function normalizeDevicePlatform(platform, system) {
14
15
  const explicit = typeof platform === "string" ? platform.trim().toLowerCase() : "";
15
16
  if (explicit)
@@ -26,13 +27,13 @@ function formatDeviceSystemForFirmware(platform, firmware, existingSystem) {
26
27
  if (!fw)
27
28
  return existingSystem ?? undefined;
28
29
  if (platform === "brightsign")
29
- return `BrightSign OS (${fw})`;
30
+ return "BrightSignOS";
30
31
  if (platform === "tizen")
31
- return `Tizen (${fw})`;
32
+ return "Tizen";
32
33
  const base = typeof existingSystem === "string"
33
34
  ? existingSystem.replace(/\s*\([^)]*\)\s*$/, "").trim()
34
35
  : "";
35
- return base ? `${base} (${fw})` : fw;
36
+ return base ? `${base}` : fw;
36
37
  }
37
38
  function formatDurationMs(ms) {
38
39
  if (!Number.isFinite(ms) || ms < 0)
@@ -139,6 +140,21 @@ function sanitizeUploadFilename(name) {
139
140
  const base = path.basename(String(name || "upload")).replace(/[^\w.\-()+ ]/g, "_");
140
141
  return base.slice(0, 180) || "upload";
141
142
  }
143
+ function inferResourceType(mimeType, filename) {
144
+ const mime = String(mimeType || "").toLowerCase();
145
+ if (mime.startsWith("image/"))
146
+ return "image";
147
+ if (mime.startsWith("video/"))
148
+ return "video";
149
+ const ext = path.extname(filename).toLowerCase();
150
+ if ([".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg"].includes(ext)) {
151
+ return "image";
152
+ }
153
+ if ([".mp4", ".mov", ".m4v", ".webm", ".avi", ".mkv"].includes(ext)) {
154
+ return "video";
155
+ }
156
+ return "raw";
157
+ }
142
158
  function sendJson(res, status, body) {
143
159
  res.writeHead(status, { "Content-Type": "application/json" });
144
160
  res.end(JSON.stringify(body));
@@ -589,16 +605,19 @@ export class TomorrowOS extends EventEmitter {
589
605
  }
590
606
  const rawName = url.searchParams.get("filename") || "upload";
591
607
  const safeName = sanitizeUploadFilename(rawName);
592
- const uploadsDir = path.join(path.resolve(this.staticRoot), "uploads");
593
- await fs.mkdir(uploadsDir, { recursive: true });
594
- const { storedName, deduplicated, contentHash } = await storeUploadIfNeeded(uploadsDir, body, safeName);
608
+ const mimeType = typeof req.headers["content-type"] === "string"
609
+ ? req.headers["content-type"]
610
+ : undefined;
611
+ const { asset, deduplicated } = await this.storeUploadedMediaAsset(body, safeName, mimeType);
595
612
  const durationMs = probeVideoDurationMs(body, safeName);
596
613
  const payload = {
597
614
  status: "success",
598
- url: `/uploads/${storedName}`,
599
- filename: storedName,
600
- size: body.length,
601
- contentHash,
615
+ url: asset.url,
616
+ assetId: asset.id,
617
+ filename: asset.storageProvider === "local" ? asset.storageKey : safeName,
618
+ size: asset.bytes ?? body.length,
619
+ contentHash: asset.sha256,
620
+ storageProvider: asset.storageProvider,
602
621
  deduplicated
603
622
  };
604
623
  if (durationMs != null)
@@ -610,6 +629,117 @@ export class TomorrowOS extends EventEmitter {
610
629
  sendJson(res, 400, { status: "failed", error: msg });
611
630
  }
612
631
  }
632
+ async storeUploadedMediaAsset(body, safeName, mimeType) {
633
+ const sha256 = contentHashHex(body);
634
+ const cloudinaryConfig = resolveCloudinaryConfig();
635
+ const storageProvider = cloudinaryConfig
636
+ ? "cloudinary"
637
+ : "local";
638
+ const existing = await this.store.getUploadedAssetBySha256(sha256, storageProvider);
639
+ if (existing) {
640
+ if (existing.storageProvider === "cloudinary") {
641
+ return { asset: existing, deduplicated: true };
642
+ }
643
+ const existingLocalPath = path.join(path.resolve(this.staticRoot || "."), "uploads", existing.storageKey);
644
+ try {
645
+ const stat = await fs.stat(existingLocalPath);
646
+ if (stat.size === body.length) {
647
+ return { asset: existing, deduplicated: true };
648
+ }
649
+ }
650
+ catch {
651
+ // Local file is missing; rewrite it below and update this asset record.
652
+ }
653
+ }
654
+ const now = new Date().toISOString();
655
+ const resourceType = inferResourceType(mimeType, safeName);
656
+ if (cloudinaryConfig) {
657
+ const uploaded = await uploadBufferToCloudinary(body, {
658
+ config: cloudinaryConfig,
659
+ publicId: sha256,
660
+ filename: safeName
661
+ });
662
+ const asset = {
663
+ id: randomUUID(),
664
+ sha256,
665
+ storageProvider: "cloudinary",
666
+ storageKey: uploaded.publicId,
667
+ url: uploaded.secureUrl,
668
+ originalFilename: safeName,
669
+ mimeType,
670
+ resourceType: uploaded.resourceType || resourceType,
671
+ bytes: uploaded.bytes ?? body.length,
672
+ createdAt: now,
673
+ updatedAt: now
674
+ };
675
+ await this.store.setUploadedAsset(asset);
676
+ return { asset, deduplicated: false };
677
+ }
678
+ const uploadsDir = path.join(path.resolve(this.staticRoot || "."), "uploads");
679
+ await fs.mkdir(uploadsDir, { recursive: true });
680
+ const { storedName, deduplicated } = await storeUploadIfNeeded(uploadsDir, body, safeName);
681
+ const asset = {
682
+ id: existing?.id ?? randomUUID(),
683
+ sha256,
684
+ storageProvider: "local",
685
+ storageKey: storedName,
686
+ url: `/uploads/${storedName}`,
687
+ originalFilename: safeName,
688
+ mimeType,
689
+ resourceType,
690
+ bytes: body.length,
691
+ createdAt: existing?.createdAt ?? now,
692
+ updatedAt: now
693
+ };
694
+ await this.store.setUploadedAsset(asset);
695
+ return { asset, deduplicated };
696
+ }
697
+ getAssetIdsFromItems(items) {
698
+ const ids = new Set();
699
+ for (const item of items ?? []) {
700
+ const assetId = String(item.assetId || "").trim();
701
+ if (assetId)
702
+ ids.add(assetId);
703
+ }
704
+ return ids;
705
+ }
706
+ async isUploadedAssetReferenced(assetId) {
707
+ const playlists = await this.store.listPlaylists();
708
+ return playlists.some((playlist) => {
709
+ if (playlist.retired)
710
+ return false;
711
+ return playlist.items.some((item) => item.assetId === assetId);
712
+ });
713
+ }
714
+ async releaseRemovedPlaylistAssets(previousItems, nextItems) {
715
+ const previous = this.getAssetIdsFromItems(previousItems);
716
+ const next = this.getAssetIdsFromItems(nextItems);
717
+ const removed = [...previous].filter((assetId) => !next.has(assetId));
718
+ for (const assetId of removed) {
719
+ await this.deleteUploadedAssetIfUnreferenced(assetId);
720
+ }
721
+ }
722
+ async deleteUploadedAssetIfUnreferenced(assetId) {
723
+ try {
724
+ if (await this.isUploadedAssetReferenced(assetId))
725
+ return;
726
+ const asset = await this.store.getUploadedAsset(assetId);
727
+ if (!asset)
728
+ return;
729
+ if (asset.storageProvider === "cloudinary") {
730
+ const cloudinaryConfig = resolveCloudinaryConfig();
731
+ if (!cloudinaryConfig) {
732
+ console.warn(`[TomorrowOS] Cloudinary config missing; cannot delete asset ${asset.id}.`);
733
+ return;
734
+ }
735
+ await deleteCloudinaryAsset(cloudinaryConfig, asset.storageKey, asset.resourceType || "image");
736
+ }
737
+ await this.store.deleteUploadedAsset(asset.id);
738
+ }
739
+ catch (err) {
740
+ console.warn("[TomorrowOS] uploaded asset cleanup failed:", err);
741
+ }
742
+ }
613
743
  async refreshResolvedBrandLogo() {
614
744
  if (!this.staticRoot)
615
745
  return;
@@ -728,14 +858,22 @@ export class TomorrowOS extends EventEmitter {
728
858
  if (req.method === "POST" && pathname === "/playlists") {
729
859
  const body = (await readJsonBody(req));
730
860
  try {
861
+ const playlistId = typeof body.id === "string" ? body.id : undefined;
862
+ const previous = playlistId
863
+ ? await this.store.getPlaylist(playlistId)
864
+ : undefined;
865
+ const items = Array.isArray(body.items)
866
+ ? body.items
867
+ : [];
731
868
  const saved = await this.playlists.savePlaylist({
732
- id: typeof body.id === "string" ? body.id : undefined,
869
+ id: playlistId,
733
870
  name: String(body.name ?? ""),
734
871
  schedule: body.schedule && typeof body.schedule === "object"
735
872
  ? body.schedule
736
873
  : undefined,
737
- items: Array.isArray(body.items) ? body.items : []
874
+ items
738
875
  });
876
+ await this.releaseRemovedPlaylistAssets(previous?.items, saved.items);
739
877
  const paired = await this.store.listPairedDevices();
740
878
  const deployResults = [];
741
879
  for (const entry of paired) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tomorrowos/sdk",
3
- "version": "0.5.2",
3
+ "version": "0.5.5",
4
4
  "description": "TomorrowOS CMS server SDK - WebSocket transport, pairing, device commands, optional static CMS UI. Includes CLI (tomorrowos init / build) and starter templates.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -33,8 +33,9 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "better-sqlite3": "^12.11.1",
36
+ "cloudinary": "^2.10.0",
36
37
  "pg": "^8.22.0",
37
- "ws": "^8.18.0"
38
+ "ws": "^8.21.0"
38
39
  },
39
40
  "devDependencies": {
40
41
  "@types/better-sqlite3": "^7.6.13",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "my-cms",
3
- "version": "0.5.2",
3
+ "version": "0.5.5",
4
4
  "description": "CMS server on @tomorrowos/sdk. Add your UI (React, static files, etc.) alongside this server.",
5
5
  "private": true,
6
6
  "type": "module",
@@ -10,7 +10,7 @@
10
10
  "build-player": "tomorrowos build --platform tizen"
11
11
  },
12
12
  "dependencies": {
13
- "@tomorrowos/sdk": "^0.5.2",
13
+ "@tomorrowos/sdk": "^0.5.5",
14
14
  "dotenv": "^17.2.3"
15
15
  },
16
16
  "devDependencies": {
@@ -12,7 +12,7 @@ let selectedPlaylistId = null;
12
12
  /** True while creating a new playlist locally (assets allowed before Save). */
13
13
  let playlistDraftActive = false;
14
14
 
15
- /** @type {{ id: string, url: string, name: string, type: string, durationMs: number }[]} */
15
+ /** @type {{ id: string, assetId?: string, url: string, name: string, type: string, durationMs: number }[]} */
16
16
  let editorItems = [];
17
17
 
18
18
  /** @type {string|null} */
@@ -429,6 +429,7 @@ function loadEditorFromSelection() {
429
429
  loadScheduleIntoForm(pl.schedule);
430
430
  editorItems = (pl.items || []).map((item) => ({
431
431
  id: crypto.randomUUID(),
432
+ assetId: item.assetId,
432
433
  url: item.url,
433
434
  name: item.url?.split("/").pop() || "asset",
434
435
  type: item.type || "image",
@@ -539,6 +540,7 @@ async function saveCurrentPlaylist() {
539
540
  try {
540
541
  items = editorItems.map((item) => ({
541
542
  url: absoluteMediaUrl(item.url),
543
+ assetId: item.assetId,
542
544
  type: item.type,
543
545
  durationMs: item.durationMs
544
546
  }));
@@ -949,6 +951,7 @@ async function addAssetFromFile(file, queueIndex, queueTotal) {
949
951
  const durationMs = await resolveVideoDurationMs(file, type, data);
950
952
  editorItems.push({
951
953
  id: crypto.randomUUID(),
954
+ assetId: data.assetId,
952
955
  url: data.url,
953
956
  name: file.name,
954
957
  type,