From f8475ac0482c42c45088dff547ec18c793483bcc Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Tue, 6 Aug 2024 10:36:42 +0200 Subject: [PATCH 01/10] Fixed rrule evaluation to handle local time correctly --- CHANGELOG.md | 2 ++ package.json | 2 +- src/schedule.js | 45 +++++++++++++++++++++++++++++++ src/service/scheduleService.js | 48 ++++++++++++++-------------------- yarn.lock | 24 ++++++++--------- 5 files changed, 78 insertions(+), 43 deletions(-) create mode 100644 src/schedule.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 862111d5..2ff521c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file. ## Unreleased +- Fixed rrule evaluation to handle local time correctly. + ## [2.0.3] - 2024-05-21 - [#125](https://github.com/os2display/display-client/pull/125) diff --git a/package.json b/package.json index 99b07993..83064e68 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "react-dom": "^18.2.0", "react-intl": "^5.20.2", "react-transition-group": "^4.4.2", - "rrule": "^2.6.8", + "rrule": "^2.7.2", "sass": "^1.37.5", "styled-components": "^5.3.1", "suncalc": "^1.9.0", diff --git a/src/schedule.js b/src/schedule.js new file mode 100644 index 00000000..fbd4e1d6 --- /dev/null +++ b/src/schedule.js @@ -0,0 +1,45 @@ +import { RRule } from 'rrule'; + +class ScheduleUtils { + static occursNow(rruleString, durationSeconds) { + const rrule = RRule.fromString(rruleString.replace("\\n", "\n")); + const duration = durationSeconds * 1000; + + const now = new Date(); + // Subtract duration from now to make sure all relevant occurrences are considered. + const from = new Date(now.getTime() - duration); + + let occurs = false; + + rrule.between( + new Date(Date.UTC(from.getFullYear(), from.getMonth(), from.getDate(), from.getHours(), from.getMinutes(), from.getSeconds())), + new Date(Date.UTC(now.getFullYear(), now.getMonth(), now.getDate(), now.getHours(), now.getMinutes(), now.getSeconds())), + true, + function iterator(occurrenceDate) { + const start = new Date( + occurrenceDate.getUTCFullYear(), + occurrenceDate.getUTCMonth(), + occurrenceDate.getUTCDate(), + occurrenceDate.getUTCHours(), + occurrenceDate.getUTCMinutes(), + occurrenceDate.getUTCSeconds(), + ); + + const end = new Date(start.getTime() + duration); + + if (now >= start && now <= end) { + occurs = true; + // break iteration. + return false; + } + + // continue iteration. + return true; + } + ); + + return occurs; + } +} + +export default ScheduleUtils; diff --git a/src/service/scheduleService.js b/src/service/scheduleService.js index 2aed7643..c14cd814 100644 --- a/src/service/scheduleService.js +++ b/src/service/scheduleService.js @@ -1,11 +1,12 @@ import cloneDeep from 'lodash.clonedeep'; import sha256 from 'crypto-js/sha256'; import Md5 from 'crypto-js/md5'; -import RRule from 'rrule'; +import { RRule, datetime } from 'rrule'; import Base64 from 'crypto-js/enc-base64'; import isPublished from '../util/isPublished'; import Logger from '../logger/logger'; import ConfigLoader from '../config-loader'; +import ScheduleUtils from "../schedule"; /** * ScheduleService. @@ -173,8 +174,6 @@ class ScheduleService { */ static findScheduledSlides(playlists, regionId) { const slides = []; - - const now = new Date(); const startOfDay = new Date(); startOfDay.setUTCHours(0, 0, 0, 0); @@ -185,38 +184,29 @@ class ScheduleService { return; } - let occurs = true; + let active = true; // If schedules are set for the playlist, do not show playlist unless a schedule is active. if (schedules.length > 0) { - occurs = false; - - schedules.forEach((schedule) => { - const rrule = RRule.fromString(schedule.rrule.replace('\\n', '\n')); - rrule.between( - // Subtract duration from now to make sure all relevant occurrences are considered. - new Date( - now.getTime() - (schedule.duration ? schedule.duration * 1000 : 0) - ), - now, - true, - function iterator(occurrenceDate) { - const occurrenceEnd = new Date( - occurrenceDate.getTime() + schedule.duration * 1000 - ); - - if (now >= occurrenceDate && now <= occurrenceEnd) { - occurs = true; - // Break the iteration. - return false; - } - return true; - } - ); + active = false; + + // Run through all schedule item and see if it occurs now. If one or more occur now, the playlist is active. + schedules.every((schedule) => { + const scheduleOccurs = ScheduleUtils.occursNow(schedule.rrule, schedule.duration); + + if (scheduleOccurs) { + active = true; + + // Break iteration. + return false; + } + + // Continue iteration. + return true; }); } - if (occurs) { + if (active) { playlist?.slidesData?.forEach((slide) => { if (!isPublished(slide.published)) { return; diff --git a/yarn.lock b/yarn.lock index 580ceafa..c088c629 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7628,11 +7628,6 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" -luxon@^1.21.3: - version "1.28.0" - resolved "https://registry.yarnpkg.com/luxon/-/luxon-1.28.0.tgz#e7f96daad3938c06a62de0fb027115d251251fbf" - integrity sha512-TfTiyvZhwBYM/7QdAVDh+7dBTBA29v4ik0Ce9zda3Mnf8on1S5KJI8P2jKFZ8+5C0jhmr0KwJEO/Wdpm0VeWJQ== - lz-string@^1.4.4: version "1.4.4" resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26" @@ -10366,14 +10361,12 @@ rollup@^1.31.1: "@types/node" "*" acorn "^7.1.0" -rrule@^2.6.8: - version "2.6.8" - resolved "https://registry.yarnpkg.com/rrule/-/rrule-2.6.8.tgz#c61714f246e7676e8efa16c2baabac199f20f6db" - integrity sha512-cUaXuUPrz9d1wdyzHsBfT1hptKlGgABeCINFXFvulEPqh9Np9BnF3C3lrv9uO54IIr8VDb58tsSF3LhsW+4VRw== +rrule@^2.7.2: + version "2.8.1" + resolved "https://registry.yarnpkg.com/rrule/-/rrule-2.8.1.tgz#e8341a9ce3e68ce5b8da4d502e893cd9f286805e" + integrity sha512-hM3dHSBMeaJ0Ktp7W38BJZ7O1zOgaFEsn41PDk+yHoEtfLV+PoJt9E9xAlZiWgf/iqEqionN0ebHFZIDAp+iGw== dependencies: - tslib "^1.10.0" - optionalDependencies: - luxon "^1.21.3" + tslib "^2.4.0" rsvp@^4.8.4: version "4.8.5" @@ -11595,7 +11588,7 @@ tsconfig-paths@^3.9.0: minimist "^1.2.0" strip-bom "^3.0.0" -tslib@^1.10.0, tslib@^1.8.1: +tslib@^1.8.1: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== @@ -11605,6 +11598,11 @@ tslib@^2.0.3, tslib@^2.1.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e" integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg== +tslib@^2.4.0: + version "2.6.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" + integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== + tsutils@^3.17.1, tsutils@^3.21.0: version "3.21.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" From 44357aab626875264cf763da11769c9c609c88bd Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Tue, 6 Aug 2024 10:40:14 +0200 Subject: [PATCH 02/10] 2000: Removed unused code --- src/service/scheduleService.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/service/scheduleService.js b/src/service/scheduleService.js index c14cd814..26c8e35e 100644 --- a/src/service/scheduleService.js +++ b/src/service/scheduleService.js @@ -174,8 +174,6 @@ class ScheduleService { */ static findScheduledSlides(playlists, regionId) { const slides = []; - const startOfDay = new Date(); - startOfDay.setUTCHours(0, 0, 0, 0); playlists.forEach((playlist) => { const { schedules } = playlist; From 1e050eb8cdbf48bc26e7a6902e052beb00ec1e36 Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Tue, 6 Aug 2024 10:45:34 +0200 Subject: [PATCH 03/10] 2000: Added comment --- src/schedule.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/schedule.js b/src/schedule.js index fbd4e1d6..02ceb29f 100644 --- a/src/schedule.js +++ b/src/schedule.js @@ -11,6 +11,7 @@ class ScheduleUtils { let occurs = false; + // @see https://github.com/jkbrzt/rrule#important-use-utc-dates rrule.between( new Date(Date.UTC(from.getFullYear(), from.getMonth(), from.getDate(), from.getHours(), from.getMinutes(), from.getSeconds())), new Date(Date.UTC(now.getFullYear(), now.getMonth(), now.getDate(), now.getHours(), now.getMinutes(), now.getSeconds())), From de3f6e4be5b17eee14ab6d9fce6aaf329f23bcfc Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Tue, 6 Aug 2024 11:42:48 +0200 Subject: [PATCH 04/10] 2000: Simplified code and added comments about rrule library quirks --- src/schedule.js | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/src/schedule.js b/src/schedule.js index 02ceb29f..7010c242 100644 --- a/src/schedule.js +++ b/src/schedule.js @@ -6,29 +6,25 @@ class ScheduleUtils { const duration = durationSeconds * 1000; const now = new Date(); + + // For evaluation with the RRule library we pretend that "now" is in UTC instead of the local timezone. + // That is 9:00 in Europe/Copenhagen time will be evaluated as if it was 9:00 in UTC. + // @see https://github.com/jkbrzt/rrule#important-use-utc-dates + const nowWithoutTimezone = new Date(Date.UTC(now.getFullYear(), now.getMonth(), now.getDate(), now.getHours(), now.getMinutes(), now.getSeconds())); + // Subtract duration from now to make sure all relevant occurrences are considered. - const from = new Date(now.getTime() - duration); + const from = new Date(nowWithoutTimezone.getTime() - duration); let occurs = false; - // @see https://github.com/jkbrzt/rrule#important-use-utc-dates rrule.between( - new Date(Date.UTC(from.getFullYear(), from.getMonth(), from.getDate(), from.getHours(), from.getMinutes(), from.getSeconds())), - new Date(Date.UTC(now.getFullYear(), now.getMonth(), now.getDate(), now.getHours(), now.getMinutes(), now.getSeconds())), + from, + nowWithoutTimezone, true, function iterator(occurrenceDate) { - const start = new Date( - occurrenceDate.getUTCFullYear(), - occurrenceDate.getUTCMonth(), - occurrenceDate.getUTCDate(), - occurrenceDate.getUTCHours(), - occurrenceDate.getUTCMinutes(), - occurrenceDate.getUTCSeconds(), - ); - - const end = new Date(start.getTime() + duration); - - if (now >= start && now <= end) { + const end = new Date(occurrenceDate.getTime() + duration); + + if (nowWithoutTimezone >= occurrenceDate && nowWithoutTimezone <= end) { occurs = true; // break iteration. return false; From 53ad5e106eecaa691ee3f85052444602182fc221 Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Tue, 6 Aug 2024 12:18:51 +0200 Subject: [PATCH 05/10] 2000: Updated changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ff521c3..8fbd295c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,8 @@ All notable changes to this project will be documented in this file. ## Unreleased -- Fixed rrule evaluation to handle local time correctly. +- [#128](https://github.com/os2display/display-client/pull/128) + - Fixed rrule evaluation to handle local time correctly. ## [2.0.3] - 2024-05-21 From 679b675186db322129a81b71dcacecd8aaeff1ef Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Tue, 6 Aug 2024 12:26:53 +0200 Subject: [PATCH 06/10] 2000: Added logger message when playlist is not scheduled for now --- src/service/scheduleService.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/service/scheduleService.js b/src/service/scheduleService.js index 26c8e35e..bcbfbc9b 100644 --- a/src/service/scheduleService.js +++ b/src/service/scheduleService.js @@ -217,6 +217,8 @@ class ScheduleService { newSlide.executionId = `EXE-ID-${executionId}`; slides.push(newSlide); }); + } else { + Logger.log('info', `Playlist ${playlist['@id']} not scheduled for now`); } }); From 597c312f53cc7277060219a9ae67310ad7a936ab Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Tue, 6 Aug 2024 13:12:54 +0200 Subject: [PATCH 07/10] Update src/schedule.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ture Gjørup --- src/schedule.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/schedule.js b/src/schedule.js index 7010c242..8c2babad 100644 --- a/src/schedule.js +++ b/src/schedule.js @@ -7,6 +7,12 @@ class ScheduleUtils { const now = new Date(); + // From the RRULE docs:_ "Returned "UTC" dates are always meant to be + // interpreted as dates in your local timezone. This may mean you have to + // do additional conversion to get the "correct" local time with offset + // applied." + // + // We do the opposite to ensure that datetime comparisons works as expected. // For evaluation with the RRule library we pretend that "now" is in UTC instead of the local timezone. // That is 9:00 in Europe/Copenhagen time will be evaluated as if it was 9:00 in UTC. // @see https://github.com/jkbrzt/rrule#important-use-utc-dates From 0afbbb8a01cb4f1fb673eda8ad4a2eb6c1a9aa20 Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Tue, 6 Aug 2024 13:13:02 +0200 Subject: [PATCH 08/10] Update src/schedule.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ture Gjørup --- src/schedule.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/schedule.js b/src/schedule.js index 8c2babad..fece9e58 100644 --- a/src/schedule.js +++ b/src/schedule.js @@ -28,6 +28,9 @@ class ScheduleUtils { nowWithoutTimezone, true, function iterator(occurrenceDate) { + // The "ccurrenceDate" we are iterating over contains a "pretend UTC" datetime + // object. As above, if the time for "occurrenceDate" is 09:00 UTC it should be + // treated as 09:00 local time regardsless of the actual local timezone const end = new Date(occurrenceDate.getTime() + duration); if (nowWithoutTimezone >= occurrenceDate && nowWithoutTimezone <= end) { From 5eda902e8f7d210d1797680bda8d20e0a47f9e69 Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Tue, 6 Aug 2024 13:13:20 +0200 Subject: [PATCH 09/10] Update src/schedule.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ture Gjørup --- src/schedule.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/schedule.js b/src/schedule.js index fece9e58..dfef377a 100644 --- a/src/schedule.js +++ b/src/schedule.js @@ -23,6 +23,9 @@ class ScheduleUtils { let occurs = false; + // RRule.prototype.between(after, before, inc=false [, iterator]) + // The between() function expects "after" and "before" to be in pretend UTC as + // described above. rrule.between( from, nowWithoutTimezone, From 157997d8d7e27daa6ec85df21be8e753c58288df Mon Sep 17 00:00:00 2001 From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com> Date: Wed, 14 Aug 2024 10:52:48 +0200 Subject: [PATCH 10/10] 1859: Updated changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fbd295c..b16a2b92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file. ## Unreleased +## [2.0.4] - 2024-08-14 + - [#128](https://github.com/os2display/display-client/pull/128) - Fixed rrule evaluation to handle local time correctly.