diff --git a/.gitignore b/.gitignore index 3c3629e..d5f19d8 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ node_modules +package-lock.json diff --git a/MMM-RandomPhoto.css b/MMM-RandomPhoto.css new file mode 100644 index 0000000..b53ec0e --- /dev/null +++ b/MMM-RandomPhoto.css @@ -0,0 +1,66 @@ +:root { + --randomphoto-blur-value: 0px; +} + +#randomPhoto img { + opacity: 0; + position: absolute;; + top: 0; + left: 0; + height: 100%; + width: 100%; + object-fit: cover; +} + +#randomPhoto img.grayscale { + filter: grayscale(100%); +} + +#randomPhoto img.blur { + filter: blur(var(--randomphoto-blur-value)); +} + +#randomPhotoIcon { + position: absolute; +} + +#randomPhotoIcon.rpitop { + top: 5px; +} + +#randomPhotoIcon.rpibottom { + bottom: 5px; +} + +#randomPhotoIcon.rpiright { + right: 10px; +} + +#randomPhotoIcon.rpileft { + left: 10px; +} + +#randomPhotoIcon i { + opacity: 1; +} + +#randomPhotoIcon i.rpihidden { + opacity: 0; +} + +@keyframes fadeOut { + 0% { opacity: 1; } + 100% { opacity: 0; } +} + +@keyframes fadeIn { + 0% { opacity: 0; } + 100% { opacity: 1; } +} + +@keyframes fadeInAndOut { + 0% { opacity: 0; } + 50% { opacity: 1; } + 100% { opacity: 0; } +} + diff --git a/MMM-RandomPhoto.js b/MMM-RandomPhoto.js index 89e2cb8..a403906 100644 --- a/MMM-RandomPhoto.js +++ b/MMM-RandomPhoto.js @@ -1,58 +1,415 @@ /* global Module */ -/* Magic Mirror +/* MagicMirror² * Module: MMM-RandomPhoto * * By Diego Vieira + * and skuethe * ICS Licensed. */ Module.register("MMM-RandomPhoto",{ - defaults: { - opacity: 0.3, - animationSpeed: 500, - updateInterval: 60, - url: 'https://unsplash.it/1920/1080/?random' - }, - - start: function() { - this.load(); - }, - - load: function() { - var self = this; - - var url = self.config.url + (self.config.url.indexOf('?') > -1 ? '&' : '?') + (new Date().getTime()); - var img = $('').attr('src', url); - - img.on('load', function() { - $('#mmm-photos-placeholder1').attr('src', url).animate({ - opacity: self.config.opacity - }, self.config.animationSpeed, function() { - $(this).attr('id', 'mmm-photos-placeholder2'); - }); - - $('#mmm-photos-placeholder2').animate({ - opacity: 0 - }, self.config.animationSpeed, function() { - $(this).attr('id', 'mmm-photos-placeholder1'); - }); - }); - - setTimeout(function() { - self.load(); - }, (self.config.updateInterval * 1000)); - - }, - - getDom: function() { - var wrapper = document.createElement("div"); - wrapper.innerHTML = ''; - return wrapper; - }, - getScripts: function() { - return [ - this.file('node_modules/jquery/dist/jquery.min.js') - ] - } + defaults: { + opacity: 0.3, + animationSpeed: 500, + updateInterval: 60, + imageRepository: "picsum", // Select the image repository source. One of "picsum" (default / fallback), "localdirectory" or "nextcloud" (currently broken because of CORS bug in nextcloud) + repositoryConfig: { + // if imageRepository = "picsum" -> "path", "username" and "password" are ignored and can be left empty + // if imageRepository = "nextcloud" + // -> "path" will point to your image directory URL, f.e.: "https://YOUR.NEXTCLOUD.HOST/remote.php/dav/files/USERNAME/PATH/TO/DIRECTORY/" + // -> if the share is private / internally shared only, add "username" and "password" for basic authentication. See documentation on how to create an "device" password: + // https://docs.nextcloud.com/server/latest/user_manual/en/session_management.html#managing-devices + // if imageRepository = "localdirectory" + // -> "path" will point to your local directory, f.e.: "~/someReallyCoolPictures", "username" and "password" are ignored + path: "https://picsum.photos/", + username: "", + password: "", + recursive: false, + exclude: [], + }, + width: 1920, + height: 1080, + random: true, // Show random images? Has no effect if you select "picsum" as imageRepository - there it is always random + grayscale: false, + blur: false, + blurAmount: 1, // between 1 and 10 + startHidden: false, // helpful if you use it as a screensaver and only want to show it f.e. after a specific time or with a specific touch gesture + startPaused: false, // start in "paused" mode -> automatic image loading is paused + showStatusIcon: true, + statusIconMode: "show", // one of: "show" (default / fallback) or "fade" + statusIconPosition: "top_right", // one of: "top_right" (default / fallback), "top_left", "bottom_right" or "bottom_left" + }, + + start: function() { + this.updateTimer = null; + this.imageList = null; // Used for nextcloud and localdirectory image list + this.currentImageIndex = -1; // Used for nextcloud and localdirectory image list + this.running = false; + + this.nextcloud = false; + this.localdirectory = false; + + this.config.imageRepository = this.config.imageRepository.toLowerCase(); + if (this.config.imageRepository === "nextcloud") { + this.nextcloud = true; + } else if (this.config.imageRepository === "localdirectory") { + this.localdirectory = true; + } + + // Set blur amount to a max of 10px + if(this.config.blurAmount > 10) { this.config.blurAmount = 10; } + + if (this.nextcloud || this.localdirectory) { + this.sendSocketNotification('SET_CONFIG', this.config); + this.fetchImageList(); + } else { + // picsum -> force URL + Log.log(this.name + " --- DEBUG ---: using picsum"); + this.config.repositoryConfig.path = "https://picsum.photos/"; + this.sendSocketNotification('SET_CONFIG', this.config); + } + }, + + fetchImageList: function() { + if (typeof this.config.repositoryConfig.path !== "undefined" && this.config.repositoryConfig.path !== null) { + this.sendSocketNotification('FETCH_IMAGE_LIST'); + } else { + Log.error("[" + this.name + "] Trying to use 'nextcloud' or 'localdirectory' but did not specify any 'config.repositoryConfig.path'."); + } + }, + + pauseImageLoading: function() { + clearTimeout(this.updateTimer); + this.running = false; + if (this.config.showStatusIcon) { + this.loadIcon(); + } + }, + + resumeImageLoading: function(respectPausedState) { + if (!this.running) { + if (respectPausedState && this.config.startPaused) { + this.running = false; + } else { + this.running = true; + } + this.load(); + if (this.config.showStatusIcon) { + this.loadIcon(); + } + } + }, + + load: function(mode="next") { + var self = this; + var url = ""; + + if (self.localdirectory || self.nextcloud) { + if (self.imageList && self.imageList.length > 0) { + url = "/" + this.name + "/images/" + this.returnImageFromList(mode); + + jQuery.ajax({ + method: "GET", + url: url, + }) + .done(function (data) { + self.smoothImageChange(data); + }) + .fail(function( jqXHR, textStatus ) { + Log.error("[" + self.name + "] Request failed: " + textStatus); + console.dir(jqXHR); + return false; + }); + + } else { + Log.error("[" + self.name + "] No images to display. 'this.imageList': " + self.imageList); + return false; + } + } else { + // picsum default / fallback + url = self.config.repositoryConfig.path + self.config.width + "/" + self.config.height + "/" + if(self.config.grayscale) { + url = url + (url.indexOf('?') > -1 ? '&' : '?') + "grayscale"; + } + if(self.config.blur) { + url = url + (url.indexOf('?') > -1 ? '&' : '?') + "blur"; + if(self.config.blurAmount > 1) { + if(self.config.blurAmount > 10) { self.config.blurAmount = 10; } + url = url + "=" + self.config.blurAmount; + } + } + url = url + (url.indexOf('?') > -1 ? '&' : '?') + (new Date(Date.now()).getTime()); + self.smoothImageChange(url); + } + + // Only activate re-loading itself, if we are not in "pause" state + if (this.running) { + this.updateTimer = setTimeout(function() { + self.load(); + }, (this.config.updateInterval * 1000)); + } + }, + + smoothImageChange: function(url) { + var self = this; + var img = $('').attr('src', url); + img.on('load', function() { + $('#randomPhoto-placeholder1').attr('src', url).animate({ + opacity: self.config.opacity + }, self.config.animationSpeed, function() { + $(this).attr('id', 'randomPhoto-placeholder2'); + }); + + $('#randomPhoto-placeholder2').animate({ + opacity: 0 + }, self.config.animationSpeed, function() { + $(this).attr('id', 'randomPhoto-placeholder1'); + }); + }); + }, + + returnImageFromList: function(mode) { + var indexToFetch = this.currentImageIndex; + const imageList = this.imageList; + + if (this.config.random) { + //Log.info("[" + this.name + "] -- DEBUG -- will fetch a random image"); + do { + indexToFetch = Math.floor(Math.random() * imageList.length); + } while (imageList.length > 1 && indexToFetch === this.currentImageIndex); + } else { + if (mode === "previous") { + indexToFetch--; + if (indexToFetch < 0) { + indexToFetch = (imageList.length - 1); + } + } else { + indexToFetch++; + if (indexToFetch >= imageList.length) { + indexToFetch = 0; + } + } + } + var imageSource = imageList[indexToFetch]; + Log.info(indexToFetch, imageSource); + this.currentImageIndex = indexToFetch; + + return imageSource; + }, + + loadIcon: function(navigate="none") { + var self = this; + const statusIcon = document.getElementById("randomPhotoStatusIcon"); + + let currentIndex = -1; + let iconloadInProgress = false; + + // Animation stuff + const animationSteps = []; + const animateToNextState = () => { + requestAnimationFrame( () => { + currentIndex++; + if (currentIndex < animationSteps.length) { + animationSteps[currentIndex](); + //console.log("animateToNextState(): " + animationSteps[currentIndex].toString()); + } + }); + }; + const cleanupAnimation = () => { + statusIcon.style.animation = null; + iconloadInProgress = false; + } + + // MutationObserver to listen to class change events + const attrObserver = new MutationObserver((mutations) => { + mutations.forEach(mu => { + if (mu.attributeName === "class" && iconloadInProgress) { + animateToNextState(); + } + }); + }); + attrObserver.observe(statusIcon, { attributes: true }); + + // Eventlistener to listen to animation end events + statusIcon.addEventListener("animationend", () => { + animateToNextState(); + }); + + // Some helper strings for fontawsome icons + var translateStatus = ""; + if (self.running) { + translateStatus = "play" + } else { + translateStatus = "pause" + } + + // If we used the "next" / "previous" notifications + if (navigate != "none") { + if (!statusIcon.classList.contains("rpihidden")) { + animationSteps.push( + () => statusIcon.style.animation = "fadeOut 1s", + ); + } + animationSteps.push( + () => statusIcon.className = "far fa-arrow-alt-circle-" + navigate + " rpihidden", + () => statusIcon.style.animation = "fadeInAndOut 2s", + () => statusIcon.className = "far fa-" + translateStatus + "-circle rpihidden", + ); + if (self.config.statusIconMode != "fade") { + animationSteps.push( + () => statusIcon.style.animation = "fadeIn 1s", + () => statusIcon.classList.remove("rpihidden"), + ); + } + animationSteps.push( + () => cleanupAnimation() + ); + iconloadInProgress = true; + animateToNextState(); + } else { + if (!statusIcon.classList.contains("rpihidden")) { + animationSteps.push( + () => statusIcon.style.animation = "fadeOut 1s", + ); + } + animationSteps.push( + () => statusIcon.className = "far fa-" + translateStatus + "-circle rpihidden", + () => statusIcon.style.animation = "fadeIn 1s", + () => statusIcon.classList.remove("rpihidden"), + ); + if (self.config.statusIconMode === "fade") { + animationSteps.push( + () => statusIcon.style.animation = "fadeOut 4s", + () => statusIcon.classList.add("rpihidden"), + ); + } + animationSteps.push( + () => cleanupAnimation() + ); + iconloadInProgress = true; + animateToNextState(); + } + }, + + getDom: function() { + var wrapper = document.createElement("div"); + wrapper.id = "randomPhoto"; + + var img1 = document.createElement("img"); + img1.id = "randomPhoto-placeholder1"; + var img2 = document.createElement("img"); + img2.id = "randomPhoto-placeholder2"; + + // Only apply grayscale / blur css classes if we are NOT using picsum, as picsum is doing it via URL parameters + if (this.nextcloud || this.localdirectory) { + if (this.config.grayscale) { + img1.classList.add("grayscale"); + img2.classList.add("grayscale"); + } + if (this.config.blur) { + img1.classList.add("blur"); + img2.classList.add("blur"); + img1.style.setProperty("--randomphoto-blur-value", this.config.blurAmount + "px"); + img2.style.setProperty("--randomphoto-blur-value", this.config.blurAmount + "px"); + } + } + + wrapper.appendChild(img1); + wrapper.appendChild(img2); + //wrapper.innerHTML = ''; + if (this.config.showStatusIcon) { + var validatePosition = ['top_right', 'top_left', 'bottom_right', 'bottom_left']; + if (validatePosition.indexOf(this.config.statusIconPosition) === -1) { + this.config.statusIconPosition = 'top_right'; + } + var statusIconObject = document.createElement("span"); + statusIconObject.id = "randomPhotoIcon"; + statusIconObject.classList.add("dimmed"); + this.config.statusIconPosition.split("_").forEach(function(extractedName) { + statusIconObject.classList.add("rpi" + extractedName); + }); + statusIconObject.innerHTML = ''; + wrapper.appendChild(statusIconObject); + } + return wrapper; + }, + + getScripts: function() { + return [ + this.file('node_modules/jquery/dist/jquery.min.js') + ] + }, + + getStyles: function() { + return [ + "MMM-RandomPhoto.css", + "font-awesome.css" + ]; + }, + + notificationReceived: function(notification, payload, sender) { + if (notification === "MODULE_DOM_CREATED") { + if (this.config.startHidden) { + this.hide(); + } else { + if (!this.nextcloud && !this.localdirectory) { + // only start "right away" if we display "picsum" images. Otherwise wait until we receive the "IMAGE_LIST" socketNotification + this.resumeImageLoading(true); + } + } + } + if (notification === "RANDOMPHOTO_NEXT") { + // Don't call the pause or resume functions here, so we can actually work with both states ("paused" and "active"), so independent of what "this.running" is set to + clearTimeout(this.updateTimer); + this.load(); + if (this.config.showStatusIcon) { + this.loadIcon("right"); + } + } + if (notification === "RANDOMPHOTO_PREVIOUS") { + // Only allow this if we are NOT in random mode and NOT use picsum as a source + if (!this.config.random && (this.nextcloud || this.localdirectory)) { + clearTimeout(this.updateTimer); + this.load("previous"); + if (this.config.showStatusIcon) { + this.loadIcon("left"); + } + } + } + if (notification === "RANDOMPHOTO_TOGGLE") { + if (this.running) { + this.pauseImageLoading(); + } else { + this.resumeImageLoading(false); + } + } + if (notification === "RANDOMPHOTO_PAUSE") { + this.pauseImageLoading(); + } + if (notification === "RANDOMPHOTO_RESUME") { + this.resumeImageLoading(false); + } + }, + + socketNotificationReceived: function(notification, payload) { + //Log.log("["+ this.name + "] received a '" + notification + "' with payload: " + payload); + //console.dir(payload); + if (notification === "IMAGE_LIST") { + this.imageList = payload; + // After we now received the image list, go ahead and display them (only when not starting as hidden) + if(!this.config.startHidden) { + this.resumeImageLoading(true); + } + } + }, + + suspend: function() { + this.pauseImageLoading(); + }, + + resume: function() { + this.resumeImageLoading(true); + } + }); diff --git a/README.md b/README.md index c18e7db..3a13c11 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,145 @@ -# No support -I don't support this module anymore. Few free to fork and modify it if you want to use it or fork another fork. - # MMM-RandomPhoto -This a module for the [MagicMirror](https://github.com/MichMich/MagicMirror). It will show a random photo from an url. + +This a module for the [MagicMirror²](https://github.com/MagicMirrorOrg/MagicMirror). It will show a (random) photo from any of these sources: + +- [picsum.photos](https://picsum.photos) +- a share of any [nextcloud](https://nextcloud.com/) instance +- a local directory on the Raspberry Pi ## Installation -1. Navigate into your MagicMirror's `modules` folder and execute `git clone https://github.com/diego-vieira/MMM-RandomPhoto.git`. -2. cd `cd MMM-RandomPhoto` -3. Execute `npm install` to install the node dependencies. + +1. Navigate into your MagicMirror's `modules` folder and execute: + +```bash +git clone https://github.com/skuethe/MMM-RandomPhoto.git && cd MMM-RandomPhoto +``` + +2. Install the node dependencies: + +```bash +npm install +``` ## Config + The entry in `config.js` can include the following options: +| Option | Description +|-----------------------|------------ +| `imageRepository` | *Optional* - The image source.

**Type:** `string`
**Allowed:** `picsum`, `nextcloud`, `localdirectory`
**Default:** `picsum` +| `repositoryConfig` | *Optional* - The configuration block for the selected image repository. See below.

**Type:** `Object` +| `random` | *Optional* - Should the images be shown at random? Has **NO** effect when `imageRepository` is set to `picsum`, as it is forced there.

**Type:** `boolean`
**Default:** `true` +| `width` | *Optional* - The width of the image in px. Only used when `imageRepository` is set to `picsum`

**Type:** `int`
**Default:** `1920` +| `height` | *Optional* - The height of the image in px. Only used when `imageRepository` is set to `picsum`

**Type:** `int`
**Default:** `1080` +| `grayscale` | *Optional* - Should the image be grayscaled?

**Type:** `boolean`
**Default:** `false` +| `blur` | *Optional* - Should the image be blurred?

**Type:** `boolean`
**Default:** `false` +| `blurAmount` | *Optional* - If you want to blur it, how much?

**Type:** `int`
**Allowed:** minimum: `0`, maximum: `10`
Default `1` +| `opacity` | *Optional* - The opacity of the image.

**Type:** `double`
**Default:** `0.3` +| `animationSpeed` | *Optional* - How long the fade out and fade in of photos should take.

**Type:** `int`
**Default:** `500` +| `updateInterval` | *Optional* - How long before getting a new image.

**Type:** `int`
**Default:** `60` seconds +| `startHidden` | *Optional* - Should the module start hidden? Useful if you use it as a "screensaver"

**Type:** `boolean`
**Default:** `false` +| `startPaused` | *Optional* - Should the module start in "paused" (automatic image loading will be paused) mode?

**Type:** `boolean`
**Default:** `false` +| `showStatusIcon` | *Optional* - Do you want to see the current status of automatic image loading ("play" / "paused" mode)?

**Type:** `boolean`
**Default:** `true` +| `statusIconMode` | *Optional* - Do you want to display the icon all the time or just fade in and out on status change?

**Type:** `string`
**Allowed:** `show` or `fade`
**Default:** `show` +| `statusIconPosition` | *Optional* - Where do you want to display the status icon?

**Type:** `string`
**Allowed:** `top_right`, `top_left`, `bottom_right` or `bottom_left`
**Default:** `top_right` + +Options for `repositoryConfig` - [more information](https://github.com/skuethe/MMM-RandomPhoto/blob/master/MMM-RandomPhoto.js#L18-L24): + +| Option | Description +|-----------------------|------------ +| `path` | *Required for nextcloud and localdirectory* - Path / URL to fetch images from.
- if `imageRepository` is set to `picsum` it is **ignored**
- if `imageRepository` is set to `nextcloud` it has to point to your nextcloud instance's specific share path
- if `imageRepository` is set to `localdirectory` it has to point to a local Path

**Type:** `string`
**Default:** `https://picsum.photos/` +| `username` | *Required for nextcloud with basic auth* - The username if images require basic authentication.

**Type:** `string`
**Default:** `` +| `password` | *Required for nextcloud with basic auth* - The password if images require basic authentication.

**Type:** `string`
**Default:** `` +| `recursive` | *Optional for localdirectory* - Search recursive for images in path.

**Type:** `boolean`
**Default:** `false` +| `exclude` | *Optional for localdirectory* - Exclude matching regex files.

**Type:** `list`
**Default:** `[]` -|Option|Description| -|---|---| -|`opacity`|The opacity of the image.

**Type:** `double`
Default 0.3| -|`animationSpeed`|How long the fade out and fade in of photos should take.

**Type:** `int`
Default 500| -|`updateInterval`|How long before getting a new image.

**Type:** `int`
Default 60 seconds| -|`url`|URL to pull a new image from.

**Type:** `string`
Default https://unsplash.it/1920/1080/?random| +Here are some examples for entries in `config.js` -Here is an example of an entry in `config.js` +**picsum**: + +```js +{ + module: 'MMM-RandomPhoto', + position: 'fullscreen_below', + config: { + imageRepository: "picsum", + repositoryConfig: { + }, + width: 1920, + height: 1080, + grayscale: true, + startHidden: true, + showStatusIcon: true, + statusIconMode: "show", + statusIconPosition: "top_right", + } +}, ``` + +**NextCloud**: + +*Hint*: [Create a "device secret"](https://docs.nextcloud.com/server/latest/user_manual/en/session_management.html#managing-devices) for accessing a share behind basic authentication. + +```js +{ + module: 'MMM-RandomPhoto', + position: 'fullscreen_below', + config: { + imageRepository: "nextcloud", + repositoryConfig: { + path: "https://YOUR.NEXTCLOUD.HOST/remote.php/dav/files/USERNAME/PATH/TO/DIRECTORY/", + username: "USERNAME", + password: "YOURDEVICESECRET", + }, + grayscale: true, + startPaused: true, + showStatusIcon: true, + } +}, +``` + +**local directory**: + +```js { - module: 'MMM-RandomPhoto', - position: 'fullscreen_below', - config: { - opacity: 0.3, - animationSpeed: 500, - updateInterval: 60, - url: 'https://unsplash.it/1920/1080/?random' - } + module: 'MMM-RandomPhoto', + position: 'fullscreen_below', + config: { + imageRepository: "localdirectory", + repositoryConfig: { + path: "/home/USER/pictures/background/", + recursive: true, + exclude: ["tmp", "#recycle"], + }, + } }, ``` +## Notifications + +You can control this module by sending specific notifications. +See the following list: + +| Option | Description +|-----------------------|------------ +| `RANDOMPHOTO_NEXT` | Don't wait for `updateInterval` to trigger and immidiately show the next image
Respects the current state of automatic image loading +| `RANDOMPHOTO_PREVIOUS`| Show the previous image.
Only works if `config.random` is set to `false` and `imageRepository` is **NOT** set to `picsum` +| `RANDOMPHOTO_TOGGLE` | Toggle the state of automatic image loading ("play" / "pause" mode) +| `RANDOMPHOTO_PAUSE` | Pause the loading of new images +| `RANDOMPHOTO_RESUME` | Resume the loading of new images + +## Ideas + +Thinking about implementing the following things: + +- possibility to show the EXIF comment from each image on screen (target selectable) +- ... + ## Dependencies -- [jquery](https://www.npmjs.com/package/jquery) (installed via `npm install`) + +- [jQuery](https://www.npmjs.com/package/jquery) (installed via `npm install`) ## Special Thanks -- [Michael Teeuw](https://github.com/MichMich) for creating the awesome [MagicMirror2](https://github.com/MichMich/MagicMirror) project that made this module possible. + +- [Michael Teeuw](https://github.com/MichMich) for creating the awesome [MagicMirror²](https://github.com/MagicMirrorOrg/MagicMirror) project that made this module possible. +- [Diego Vieira](https://github.com/diego-vieira) for [initially](https://github.com/diego-vieira/MMM-RandomPhoto) creating this module. diff --git a/example1.png b/example1.png new file mode 100644 index 0000000..9161d4f Binary files /dev/null and b/example1.png differ diff --git a/node_helper.js b/node_helper.js new file mode 100644 index 0000000..bc9c32d --- /dev/null +++ b/node_helper.js @@ -0,0 +1,175 @@ +require("url"); // for nextcloud +const https = require("node:https"); // for nextcloud +const fs = require("fs"); // for localdirectory +const Log = require("logger"); + +const NodeHelper = require("node_helper"); + +module.exports = NodeHelper.create({ + + start: function() { + var self = this; + + this.nextcloud = false; + this.localdirectory = false; + + this.imageList = []; + this.expressApp.get("/" + this.name + "/images/:randomImageName", async function(request, response) { + var imageBase64Encoded = await self.fetchEncodedImage(decodeURIComponent(request.params.randomImageName)); + response.send(imageBase64Encoded); + }); + }, + + + socketNotificationReceived: function(notification, payload) { + //console.log("["+ this.name + "] received a '" + notification + "' with payload: " + payload); + if (notification === "SET_CONFIG") { + this.config = payload; + if (this.config.imageRepository === "nextcloud") { + this.nextcloud = true; + } else if (this.config.imageRepository === "localdirectory") { + this.localdirectory = true; + } + } + if (notification === "FETCH_IMAGE_LIST") { + if (this.config.imageRepository === "nextcloud") { + this.fetchNextcloudImageList(); + } + if (this.config.imageRepository === "localdirectory") { + this.fetchLocalImageList(); + } + } + }, + + fetchLocalImageDirectory: function(path) { + var self = this; + + // Validate path + if (!fs.existsSync(path)) { + console.log("["+ self.name + "] ERROR - specified path does not exist: " + path); + return false; + } + + const excludePattern = self.config.repositoryConfig.exclude?.map(pattern => new RegExp(pattern)); + + var fileList = fs.readdirSync(path, { withFileTypes: true }); + if (fileList.length > 0) { + for (var f = 0; f < fileList.length; f++) { + if (excludePattern?.some(regex => regex.test(fileList[f].name))) continue; + + if (fileList[f].isFile()) { + //TODO: add mime type check here + self.imageList.push(encodeURIComponent(path + "/" + fileList[f].name)); + } + if ((self.config.repositoryConfig.recursive === true) && fileList[f].isDirectory()) { + self.fetchLocalImageDirectory(path + "/" + fileList[f].name); + } + } + return; + } + }, + + fetchLocalImageList: function() { + var self = this; + var path = self.config.repositoryConfig.path; + + self.imageList = []; + self.fetchLocalImageDirectory(path); + + self.sendSocketNotification("IMAGE_LIST", self.imageList); + return false; + }, + + + fetchNextcloudImageList: function() { + var self = this; + var imageList = []; + var path = self.config.repositoryConfig.path; + + const urlParts = new URL(path); + const requestOptions = { + method: "PROPFIND", + headers: { + "Authorization": "Basic " + new Buffer.from(this.config.repositoryConfig.username + ":" + this.config.repositoryConfig.password).toString("base64") + } + }; + https.get(path, requestOptions, function(response) { + var body = ""; + response.on("data", function(data) { + body += data; + }); + response.on("end", function() { + imageList = body.match(/href>\/[^<]+/g); + imageList.shift(); // delete first array entry, because it contains the link to the current folder + if (imageList && imageList.length > 0) { + imageList.forEach(function(item, index) { + // remove clutter and the pathing from the entry -> only save file name + imageList[index] = encodeURIComponent(item.replace("href>" + urlParts.pathname, "")); + //console.log("[" + self.name + "] Found entry: " + imageList[index]); + }); + self.sendSocketNotification("IMAGE_LIST", imageList); + return; + } else { + console.log("[" + this.name + "] WARNING: did not get any images from nextcloud url"); + return false; + } + }); + }) + .on("error", function(err) { + console.log("[" + this.name + "] ERROR: " + err); + return false; + }); + }, + + + fetchEncodedImage: async function(passedImageName) { + var self = this; + return new Promise(function(resolve, reject) { + var fullImagePath = passedImageName; + + // Local files + if (self.localdirectory) { + var fileEncoded = "data:image/jpeg;base64," + fs.readFileSync(fullImagePath, { encoding: 'base64' }); + resolve(fileEncoded); + } + + // Nextcloud + else if (self.nextcloud) { + const requestOptions = { + method: "GET", + headers: { + "Authorization": "Basic " + new Buffer.from(self.config.repositoryConfig.username + ":" + self.config.repositoryConfig.password).toString("base64") + } + }; + https.get(self.config.repositoryConfig.path + fullImagePath, requestOptions, (response) => { + response.setEncoding('base64'); + var fileEncoded = "data:" + response.headers["content-type"] + ";base64,"; + response.on("data", (data) => { fileEncoded += data; }); + response.on("end", () => { + resolve(fileEncoded); + }); + }) + .on("error", function(err) { + console.log("[" + this.name + "] ERROR: " + err); + return false; + }); + } + }) + + + /** + var getMimeObject = spawn("file", ["-b", "--mime-type", "-0", "-0", file]); + getMimeObject.stdout.on('data', (data) => { + var mimeType = data.toString().replace("\0", ""); + //console.log("mime type is: '" + mimeType + "'"); + var fileEncoded = "data:" + mimeType + ";base64,"; + fileEncoded += fs.readFileSync(file, { encoding: 'base64' }); + //console.log("base64:"); + console.log(""); + //return fileEncoded; + }); + **/ + }, + + +}); diff --git a/package.json b/package.json index ce8a646..e31fa9e 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,21 @@ { "name": "magic-mirror-module-random-photo", - "version": "1.0.0", + "version": "1.1.2", "description": "Show a random photo from an url", + "repository": { + "type": "git", + "url": "git+https://github.com/skuethe/MMM-RandomPhoto.git" + }, + "keywords": [ + "MagicMirror", + "Module", + "Photo", + "Image", + "Picsum", + "Nextcloud" + ], "main": "MMM-RandomPhoto.js", - "author": "Diego Vieira", + "author": "skuethe", "license": "ISC", "dependencies": { "jquery": "^3.1.0"