From 59e7cf0148262b226c5f6fb8586c3972b3d3d4c6 Mon Sep 17 00:00:00 2001 From: Siddharth Sahoo <67169975+11sid11@users.noreply.github.com> Date: Thu, 20 Aug 2026 14:34:37 +0530 Subject: [PATCH 1/8] Polish playlist heartbeat timestamp format --- src/75_PlaylistHeartbeat.gs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/75_PlaylistHeartbeat.gs b/src/75_PlaylistHeartbeat.gs index 8329b69..3e0e4e1 100644 --- a/src/75_PlaylistHeartbeat.gs +++ b/src/75_PlaylistHeartbeat.gs @@ -42,8 +42,8 @@ var SpotiSync = SpotiSync || {}; throw new Error('Cannot build playlist heartbeat with an invalid timestamp.'); } - timestamp = Utilities.formatDate(when, timezone, "EEEE 'at' h:mm a"); - return PHRASES[index] + ' · ' + ns.Constants.HEARTBEAT_SIGNATURE + ' · Synced ' + timestamp; + timestamp = Utilities.formatDate(when, timezone, 'EEE, MMM d · h:mm a'); + return PHRASES[index] + ' · ' + ns.Constants.HEARTBEAT_SIGNATURE + ' · 🔄 ' + timestamp; } ns.PlaylistHeartbeat = { From d441977a178763d15f91d78ffdf6c77e8d88cf5c Mon Sep 17 00:00:00 2001 From: Siddharth Sahoo <67169975+11sid11@users.noreply.github.com> Date: Thu, 20 Aug 2026 14:35:06 +0530 Subject: [PATCH 2/8] Cover compact heartbeat timestamp format --- scripts/test-heartbeat.js | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/scripts/test-heartbeat.js b/scripts/test-heartbeat.js index d9039ea..3cfcf55 100644 --- a/scripts/test-heartbeat.js +++ b/scripts/test-heartbeat.js @@ -9,14 +9,16 @@ const vm = require('vm'); const root = path.resolve(__dirname, '..'); const properties = new Map(); const calls = []; +const heartbeatDate = new Date('2026-08-19T21:46:00Z'); const context = vm.createContext({ console, Date, Object, Array, String, Number, Boolean, Math, JSON, RegExp, Error, encodeURIComponent, Utilities: { formatDate(date, timezone, pattern) { assert.strictEqual(timezone, 'Asia/Kolkata'); - assert.strictEqual(pattern, "EEEE 'at' h:mm a"); - return 'Saturday at 2:22 AM'; + assert.strictEqual(pattern, 'EEE, MMM d · h:mm a'); + assert.strictEqual(date.toISOString(), heartbeatDate.toISOString()); + return 'Thu, Aug 20 · 3:16 AM'; } }, LockService: { @@ -45,6 +47,9 @@ context.SpotiSync.SheetStore = { context.SpotiSync.SpotifyApi = { updatePlaylistDescription(playlistId, description) { calls.push(['description', playlistId, description]); + }, + updatePlaylistName() { + calls.push(['name']); } }; load('75_PlaylistHeartbeat.gs'); @@ -53,17 +58,20 @@ const job = { jobId: 'job_abc', targetPlaylist: '1234567890AB' }; const H = context.SpotiSync.PlaylistHeartbeat; (function testDescriptionTemplate() { - const text = H._buildDescription(job, new Date('2026-08-15T00:00:00Z'), 'Asia/Kolkata', 0); - assert.strictEqual(text, 'Kept fresh with Spoti Sync ✨ · sid.is-a.dev · Synced Saturday at 2:22 AM'); + const text = H._buildDescription(job, heartbeatDate, 'Asia/Kolkata', 0); + assert.strictEqual(text, 'Kept fresh with Spoti Sync ✨ · sid.is-a.dev · 🔄 Thu, Aug 20 · 3:16 AM'); })(); (function testPhraseRotationOnlyAfterSuccessfulUpdate() { - const first = H.update(job, new Date('2026-08-15T00:00:00Z')); - const second = H.update(job, new Date('2026-08-15T00:00:00Z')); + calls.length = 0; + const first = H.update(job, heartbeatDate); + const second = H.update(job, heartbeatDate); assert.strictEqual(first.ok, true); assert.strictEqual(first.phraseIndex, 0); assert.strictEqual(second.phraseIndex, 1); assert(second.description.startsWith('Kept in sync with Spoti Sync 🔄')); + assert.strictEqual(calls.filter((entry) => entry[0] === 'description').length, 2); + assert.strictEqual(calls.some((entry) => entry[0] === 'name'), false, 'Heartbeat must never rename the playlist.'); })(); (function testFailureIsReturnedWithoutAdvancingRotation() { @@ -71,7 +79,7 @@ const H = context.SpotiSync.PlaylistHeartbeat; context.SpotiSync.SpotifyApi.updatePlaylistDescription = function () { throw new Error('temporary description failure'); }; - const result = H.update(job, new Date('2026-08-15T00:00:00Z')); + const result = H.update(job, heartbeatDate); assert.strictEqual(result.ok, false); assert.match(result.error, /temporary description failure/); assert.strictEqual(H._readIndex(job), currentIndex); @@ -144,4 +152,4 @@ load('70_SyncEngine.gs'); assert.match(summary.warning, /description API unavailable/); })(); -console.log('Playlist heartbeat opt-in/out and ordering tests passed.'); +console.log('Playlist heartbeat timestamp, opt-in/out, and ordering tests passed.'); From 9ee04d868a9983c408cc3447c4f73722c763c3dd Mon Sep 17 00:00:00 2001 From: Siddharth Sahoo <67169975+11sid11@users.noreply.github.com> Date: Thu, 20 Aug 2026 14:35:33 +0530 Subject: [PATCH 3/8] Bump runtime version to 1.5.1 --- src/00_Core.gs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/00_Core.gs b/src/00_Core.gs index eb5ada3..2d4c27e 100644 --- a/src/00_Core.gs +++ b/src/00_Core.gs @@ -5,7 +5,7 @@ var SpotiSync = SpotiSync || {}; (function (ns) { 'use strict'; - ns.VERSION = '1.5.0'; + ns.VERSION = '1.5.1'; ns.Constants = Object.freeze({ APP_NAME: 'Spoti Sync', @@ -71,7 +71,7 @@ var SpotiSync = SpotiSync || {}; .replace(/&/g, '&') .replace(//g, '>') - .replace(/"/g, '"') + .replace(/\"/g, '"') .replace(/'/g, '''); }, From 79873f463b0b878087468b901963d523ffa18818 Mon Sep 17 00:00:00 2001 From: Siddharth Sahoo <67169975+11sid11@users.noreply.github.com> Date: Thu, 20 Aug 2026 14:35:47 +0530 Subject: [PATCH 4/8] Publish v1.5.1 release metadata --- docs/version.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/version.json b/docs/version.json index 73f83f2..d651646 100644 --- a/docs/version.json +++ b/docs/version.json @@ -1,13 +1,13 @@ { "schema": 1, - "version": "1.5.0", + "version": "1.5.1", "channel": "stable", "released_at": "2026-08-20", "installer_url": "https://sid.is-a.dev/Spoti-sync/#update", "changelog_url": "https://github.com/11sid11/Spoti-sync/blob/main/CHANGELOG.md", "notes": [ - "Adds Hourly and Every N hours automation while keeping the existing Off, Daily, and Every N days options.", - "Keeps a single adaptive Apps Script scheduler: daily for day-only jobs, hourly when any sub-daily job exists, with Spotify contacted only for due jobs.", - "Preserves existing jobs, playlist IDs, OAuth state, day-based scheduling semantics, manual Sync now behavior, and heartbeat settings." + "Polishes playlist heartbeat timestamps to the compact format 🔄 Thu, Aug 20 · 3:16 AM.", + "Preserves rotating heartbeat phrases, the sid.is-a.dev signature, spreadsheet-timezone formatting, and Success-with-warning behavior.", + "Presentation-only patch: scheduler, OAuth, jobs, playlist IDs, sync behavior, and Spotify API call count are unchanged." ] } From 97183d4090f43647b22c6faad266f5b3d186f58c Mon Sep 17 00:00:00 2001 From: Siddharth Sahoo <67169975+11sid11@users.noreply.github.com> Date: Thu, 20 Aug 2026 14:36:05 +0530 Subject: [PATCH 5/8] Advance update-checker future-release fixture for v1.5.1 --- scripts/test-update-checker.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/scripts/test-update-checker.js b/scripts/test-update-checker.js index 2cb82ec..db3f36a 100644 --- a/scripts/test-update-checker.js +++ b/scripts/test-update-checker.js @@ -12,7 +12,7 @@ let fetchCount = 0; let responseStatus = 200; let responseBody = { schema: 1, - version: '1.5.1', + version: '1.5.2', channel: 'stable', released_at: '2026-09-01', installer_url: 'https://example.com/update', @@ -71,7 +71,7 @@ const { UpdateChecker, VERSION } = context.SpotiSync; const status = UpdateChecker.check({ force: true }); assert.strictEqual(fetchCount, 1); assert.strictEqual(status.currentVersion, VERSION); - assert.strictEqual(status.latestVersion, '1.5.1'); + assert.strictEqual(status.latestVersion, '1.5.2'); assert.strictEqual(status.updateAvailable, true); assert.strictEqual(status.checkStatus, 'Update available'); assert.strictEqual(status.installerUrl, 'https://example.com/update'); @@ -80,15 +80,15 @@ const { UpdateChecker, VERSION } = context.SpotiSync; (function testNormalCheckUsesDailyCache() { const status = UpdateChecker.check({ force: false }); assert.strictEqual(fetchCount, 1, 'Fresh cached status must avoid another GitHub request.'); - assert.strictEqual(status.latestVersion, '1.5.1'); + assert.strictEqual(status.latestVersion, '1.5.2'); })(); (function testCachedAvailabilityRecomputesAfterCodeUpgrade() { const installedVersion = context.SpotiSync.VERSION; - context.SpotiSync.VERSION = '1.5.1'; + context.SpotiSync.VERSION = '1.5.2'; const status = UpdateChecker.getCachedStatus(); - assert.strictEqual(status.currentVersion, '1.5.1'); - assert.strictEqual(status.latestVersion, '1.5.1'); + assert.strictEqual(status.currentVersion, '1.5.2'); + assert.strictEqual(status.latestVersion, '1.5.2'); assert.strictEqual(status.updateAvailable, false); assert.strictEqual(status.checkStatus, 'Up to date'); context.SpotiSync.VERSION = installedVersion; @@ -126,7 +126,7 @@ const { UpdateChecker, VERSION } = context.SpotiSync; responseStatus = 200; responseBody = { ...responseBody, - version: '1.5.1' + version: '1.5.2' }; documentStatus.UPDATE_LAST_CHECK_AT = new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(); const status = UpdateChecker.check({ force: false }); From 56e2a4cb722d096aa8706f1f0e4bf2f5c90a1321 Mon Sep 17 00:00:00 2001 From: Siddharth Sahoo <67169975+11sid11@users.noreply.github.com> Date: Thu, 20 Aug 2026 14:36:29 +0530 Subject: [PATCH 6/8] Document compact heartbeat timestamp --- ARCHITECTURE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index e13de5c..72763c1 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -170,10 +170,10 @@ A description-only failure is non-fatal and produces `Success with warning`. A f `75_PlaylistHeartbeat.gs` owns description formatting and phrase rotation: ```text -[rotating Spoti Sync phrase] · sid.is-a.dev · Synced [weekday] at [time] +[rotating Spoti Sync phrase] · sid.is-a.dev · 🔄 Thu, Aug 20 · 3:16 AM ``` -The job-owned `Heartbeat Enabled` field controls whether SyncEngine invokes that module. Disabling it does not clear or replace the current Spotify description. +The timestamp uses the spreadsheet timezone. The job-owned `Heartbeat Enabled` field controls whether SyncEngine invokes that module. Disabling it does not clear or replace the current Spotify description. Phrase rotation state remains in Document Properties and advances only after Spotify accepts the description update. From 3fd0c6bf7936d4b8ae9a509948544907896c8535 Mon Sep 17 00:00:00 2001 From: Siddharth Sahoo <67169975+11sid11@users.noreply.github.com> Date: Thu, 20 Aug 2026 14:37:22 +0530 Subject: [PATCH 7/8] Document v1.5.1 heartbeat timestamp polish --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25fd05c..9a7b7e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ All notable changes to Spoti Sync are documented here. +## 1.5.1 — 2026-08-20 + +### Changed + +- Updated playlist heartbeat timestamps to the compact format `🔄 Thu, Aug 20 · 3:16 AM` while keeping the existing rotating Spoti Sync phrase and `sid.is-a.dev` signature. +- Preserved spreadsheet-timezone formatting, heartbeat opt-in/out behavior, phrase rotation, playlist-write ordering, and `Success with warning` semantics for description-only failures. + +### Upgrade note + +- Install the 1.5.1 bundle in the same Apps Script project, save, and reload the Sheet. No Spotify reconnection, job recreation, scheduler recreation, repair step, or playlist-ID re-entry is required. + ## 1.5.0 — 2026-08-20 ### Added From 5627031298a96dbda21cb4b74fdfcb173d224b12 Mon Sep 17 00:00:00 2001 From: Siddharth Sahoo <67169975+11sid11@users.noreply.github.com> Date: Thu, 20 Aug 2026 14:40:02 +0530 Subject: [PATCH 8/8] Keep version bump isolated to release metadata --- src/00_Core.gs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/00_Core.gs b/src/00_Core.gs index 2d4c27e..b567897 100644 --- a/src/00_Core.gs +++ b/src/00_Core.gs @@ -71,7 +71,7 @@ var SpotiSync = SpotiSync || {}; .replace(/&/g, '&') .replace(//g, '>') - .replace(/\"/g, '"') + .replace(/"/g, '"') .replace(/'/g, '''); },