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. 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 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." ] } 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.'); 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 }); diff --git a/src/00_Core.gs b/src/00_Core.gs index eb5ada3..b567897 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', 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 = {