iobroker.lovelace
Version:
With this adapter you can build visualization for ioBroker with Home Assistant Lovelace UI
251 lines (250 loc) • 8.06 kB
JavaScript
;
const IMAGE_FOLDER = "uploaded_images";
const CARDS_FOLDER = "cards";
const ROOT = "media-source://";
const SRC_IMAGES = "media-source://image_upload";
const SRC_CARDS = "media-source://lovelace_cards";
function mediaClassForFile(name) {
var _a;
const ext = ((_a = name.split(".").pop()) == null ? void 0 : _a.toLowerCase()) || "";
if (["png", "jpg", "jpeg", "gif", "webp", "svg", "bmp", "ico", "avif"].includes(ext)) {
const subtype = ext === "jpg" ? "jpeg" : ext === "svg" ? "svg+xml" : ext;
return { media_class: "image", media_content_type: `image/${subtype}` };
}
if (["mp4", "webm", "ogv", "mov", "mkv"].includes(ext)) {
return { media_class: "video", media_content_type: `video/${ext}` };
}
if (["mp3", "wav", "ogg", "flac", "m4a", "aac"].includes(ext)) {
return { media_class: "music", media_content_type: `audio/${ext}` };
}
return { media_class: "document", media_content_type: "application/octet-stream" };
}
class MediaSourceModule {
adapter;
sendResponse;
/**
* @param options - options
* @param options.adapter - ioBroker adapter instance (for file storage access)
* @param options.sendResponse - function to send a response to a client
*/
constructor(options) {
this.adapter = options.adapter;
this.sendResponse = options.sendResponse;
}
/**
* Handle a media_source message.
*
* @param ws - websocket connection
* @param message - the message
* @returns true if handled
*/
async processMessage(ws, message) {
const type = message.type;
if (type === "media_source/browse_media") {
const id = message.media_content_id || ROOT;
this.sendResponse(ws, message.id, await this._browse(id));
return true;
}
if (type === "media_source/resolve_media") {
this.sendResponse(ws, message.id, await this._resolve(message.media_content_id));
return true;
}
if (type === "image/list") {
this.sendResponse(ws, message.id, await this._listImages());
return true;
}
if (type === "image/delete") {
await this._deleteImage(message.image_id);
this.sendResponse(ws, message.id, null);
return true;
}
return false;
}
/**
* List the metadata of all uploaded images (used by the frontend image manager).
*
* @returns array of { id, filesize, name, uploaded_at, content_type }
*/
async _listImages() {
const result = [];
let files = [];
try {
files = await this.adapter.readDirAsync(this.adapter.namespace, IMAGE_FOLDER);
} catch {
return result;
}
for (const f of files) {
if (f.isDir || !f.file.endsWith(".json")) {
continue;
}
try {
const meta = JSON.parse(
(await this.adapter.readFileAsync(this.adapter.namespace, `${IMAGE_FOLDER}/${f.file}`)).file.toString()
);
result.push(meta);
} catch {
}
}
return result;
}
/**
* Delete an uploaded image (binary + its .json sidecar). Errors are ignored so a missing file
* does not fail the request.
*
* @param imageId - id of the uploaded image
*/
async _deleteImage(imageId) {
const id = String(imageId || "").replace(/[^a-zA-Z0-9-]/g, "");
if (!id) {
return;
}
for (const path of [`${IMAGE_FOLDER}/${id}`, `${IMAGE_FOLDER}/${id}.json`]) {
try {
await this.adapter.delFileAsync(this.adapter.namespace, path);
} catch {
}
}
}
/**
* Build a directory node for the browse tree.
*
* @param title - display title
* @param mediaContentId - media-source id of the directory
* @param childClass - media_class of the directory's children
* @param children - optional already-resolved children
* @returns the directory node
*/
dir(title, mediaContentId, childClass, children) {
return {
title,
media_class: "directory",
media_content_type: "",
media_content_id: mediaContentId,
can_play: false,
can_expand: true,
children_media_class: childClass,
thumbnail: null,
children
};
}
/**
* Browse a media-source id and return its tree node (with children).
*
* @param mediaContentId - the media-source id to browse
* @returns the browse tree node
*/
async _browse(mediaContentId) {
if (mediaContentId === ROOT) {
return this.dir("ioBroker Lovelace", ROOT, "directory", [
this.dir("Uploaded images", SRC_IMAGES, "image"),
this.dir("Cards", SRC_CARDS, "image")
]);
}
if (mediaContentId === SRC_IMAGES) {
return this.dir("Uploaded images", SRC_IMAGES, "image", await this._browseImages());
}
if (mediaContentId === SRC_CARDS) {
return this.dir("Cards", SRC_CARDS, "image", await this._browseCards());
}
return this.dir(mediaContentId, mediaContentId, "directory", []);
}
/**
* List uploaded images as media items (reads each `<id>.json` sidecar for name/content type).
*
* @returns the image media items
*/
async _browseImages() {
const children = [];
let files = [];
try {
files = await this.adapter.readDirAsync(this.adapter.namespace, IMAGE_FOLDER);
} catch {
return children;
}
for (const f of files) {
if (f.isDir || f.file.endsWith(".json")) {
continue;
}
const id = f.file;
let title = id;
let contentType = "image/*";
try {
const meta = JSON.parse(
(await this.adapter.readFileAsync(this.adapter.namespace, `${IMAGE_FOLDER}/${id}.json`)).file.toString()
);
title = meta.name || id;
contentType = meta.content_type || contentType;
} catch {
}
children.push({
title,
media_class: "image",
media_content_type: contentType,
media_content_id: `${SRC_IMAGES}/${id}`,
can_play: true,
can_expand: false,
children_media_class: null,
thumbnail: `/api/image/serve/${id}`
});
}
return children;
}
/**
* List the files in the `cards/` folder as media items (custom cards, fonts, images, …).
*
* @returns the card media items
*/
async _browseCards() {
const children = [];
let files = [];
try {
files = await this.adapter.readDirAsync(this.adapter.namespace, `/${CARDS_FOLDER}/`);
} catch {
return children;
}
for (const f of files) {
if (f.isDir) {
continue;
}
const { media_class, media_content_type } = mediaClassForFile(f.file);
children.push({
title: f.file,
media_class,
media_content_type,
media_content_id: `${SRC_CARDS}/${f.file}`,
can_play: true,
can_expand: false,
children_media_class: null,
thumbnail: media_class === "image" ? `/${CARDS_FOLDER}/${f.file}` : null
});
}
return children;
}
/**
* Resolve a media-source item id to a playable URL + mime type.
*
* @param mediaContentId - the media-source id to resolve
* @returns the resolved url and mime type
*/
async _resolve(mediaContentId) {
if (mediaContentId == null ? void 0 : mediaContentId.startsWith(`${SRC_IMAGES}/`)) {
const id = mediaContentId.substring(SRC_IMAGES.length + 1).replace(/[^a-zA-Z0-9-]/g, "");
let mime = "image/*";
try {
const meta = JSON.parse(
(await this.adapter.readFileAsync(this.adapter.namespace, `${IMAGE_FOLDER}/${id}.json`)).file.toString()
);
mime = meta.content_type || mime;
} catch {
}
return { url: `/api/image/serve/${id}`, mime_type: mime };
}
if (mediaContentId == null ? void 0 : mediaContentId.startsWith(`${SRC_CARDS}/`)) {
const file = mediaContentId.substring(SRC_CARDS.length + 1).replace(/\.\.+/g, "");
return { url: `/${CARDS_FOLDER}/${file}`, mime_type: mediaClassForFile(file).media_content_type };
}
return { url: "", mime_type: "" };
}
}
module.exports = MediaSourceModule;
//# sourceMappingURL=mediaSource.js.map