Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
98a4c78
Add hourly frequency primitives for v1.5.0
11sid11 Aug 19, 2026
65928a7
Extend canonical frequency model to hourly intervals
11sid11 Aug 19, 2026
d3a6908
Make scheduler adapt between daily and hourly modes
11sid11 Aug 19, 2026
6b66de3
Keep hourly no-op scheduler checks lightweight
11sid11 Aug 19, 2026
b0a7dbb
Extend job automation model to hourly intervals
11sid11 Aug 19, 2026
3baf6be
Add hourly options to the single sidebar automation control
11sid11 Aug 19, 2026
90ed917
Add adaptive scheduler regression coverage
11sid11 Aug 19, 2026
9511e17
Add hourly automation job editor tests
11sid11 Aug 19, 2026
55e5977
Protect manual sync and hourly no-op semantics
11sid11 Aug 19, 2026
b7431b2
Add focused v1.5 frequency and due-time tests
11sid11 Aug 19, 2026
f0995cc
Run v1.5 sub-daily automation regressions in CI
11sid11 Aug 19, 2026
1249572
Document v1.5 hourly automation
11sid11 Aug 19, 2026
093c8d0
Document adaptive v1.5 scheduler architecture
11sid11 Aug 19, 2026
b7f6e15
Show hourly automation on the public product page
11sid11 Aug 19, 2026
2f30832
Publish v1.5.0 release metadata
11sid11 Aug 19, 2026
aab5cbd
Keep docs freshness checks aligned with v1.5 automation
11sid11 Aug 19, 2026
84b93fc
Add v1.5.0 hourly automation changelog
11sid11 Aug 19, 2026
bb204ae
Update storage regression for canonical hour/day frequencies
11sid11 Aug 19, 2026
43afd41
Advance update-checker future-release fixture for v1.5
11sid11 Aug 19, 2026
37137ab
Preserve daily execution window under hourly dispatcher
11sid11 Aug 19, 2026
7194beb
Pass scheduler mode into due-job dispatch
11sid11 Aug 19, 2026
3455d1d
Test mixed hourly and daily scheduling compatibility
11sid11 Aug 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ jobs:
run: node scripts/test.js
- name: Run v1.4 single-surface tests
run: node scripts/test-v14.js
- name: Run v1.5 sub-daily automation tests
run: node scripts/test-v15.js
- name: Run sheet and scheduler checks
run: node scripts/test-scheduler.js
- name: Run migration and storage checks
Expand Down
41 changes: 24 additions & 17 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ User's Google Sheet
+-- job persistence
+-- sync engine
+-- optional playlist heartbeat
+-- one reconciled daily scheduler
+-- one reconciled scheduler
+-- single sidebar application
+-- read-only status + Activity views
|
Expand Down Expand Up @@ -58,14 +58,16 @@ User-facing:

Internal:

- **Jobs** — hidden durable job records, including stable Job IDs, playlist IDs, behavior, automation interval, heartbeat preference and per-job telemetry.
- **Schedule** — legacy v1.3 sheet preserved/hidden on upgrade; normal v1.4 runtime does not render it.
- **Jobs** — hidden durable job records, including stable Job IDs, playlist IDs, behavior, automation frequency, heartbeat preference and per-job telemetry.
- **Schedule** — legacy v1.3 sheet preserved/hidden on upgrade; normal v1.5 runtime does not render it.

The existing `Frequency` cell is the canonical serialized schedule. It stores values such as `Hourly`, `Every 6 hours`, `Daily`, or `Every 7 days`; no additional scheduling column is required for v1.5.

### Document Properties

- scheduler telemetry
- scheduler telemetry and current scheduler mode (`NONE`, `DAILY`, or `HOURLY`)
- update-check cache/status
- latest run summary
- latest real run summary
- playlist display-name cache
- one tiny heartbeat phrase index per stable Job ID

Expand Down Expand Up @@ -111,12 +113,14 @@ Runtime identity uses hidden Spotify playlist IDs, not human-readable display la
Automation maps onto the existing storage model:

```text
Off → enabled=false
Daily → enabled=true, intervalDays=1
Every N days → enabled=true, intervalDays=N
Off → enabled=false, stored Frequency retained
Hourly → enabled=true, unit=HOUR, interval=1
Every N hours → enabled=true, unit=HOUR, interval=N (1–23)
Daily → enabled=true, unit=DAY, interval=1
Every N days → enabled=true, unit=DAY, interval=N (1–3650)
```

The canonical server-side Frequency parser remains responsible for validating the supported 1–3650 day interval.
The canonical server-side Frequency parser remains responsible for validation. Day-based schedules retain the existing calendar-day semantics; hour-based schedules use elapsed hours since the last successful run.

## Runtime model

Expand All @@ -125,13 +129,16 @@ The canonical server-side Frequency parser remains responsible for validating th
`Scheduler.reconcile()` enforces the invariant:

```text
0 automated jobs → 0 Spoti Sync triggers
1+ automated jobs → exactly 1 Spoti Sync daily trigger
0 automated jobs → 0 Spoti Sync triggers
only Daily / Every N days jobs → exactly 1 DAILY Spoti Sync trigger
any Hourly / Every N hours job present → exactly 1 HOURLY Spoti Sync trigger
```

An already-correct single trigger is retained. Missing or duplicate Spoti Sync triggers are normalized. There are no per-job triggers.
There is never a daily and hourly Spoti Sync scheduler trigger at the same time, and there are no per-job triggers.

The scheduler persists the selected dispatcher mode in Document Properties because Apps Script project-trigger objects identify the handler but do not expose enough recurrence metadata for Spoti Sync to reliably infer whether an existing clock trigger is hourly or daily. A single legacy v1.4 trigger with no stored mode is treated as DAILY, so an upgrade with only day-based jobs does not recreate a correct trigger.

The daily trigger invokes `spotiSyncScheduler()`, which runs enabled jobs only when their configured interval is due.
The hourly dispatcher is deliberately cheap. It loads local job state, evaluates due jobs, and exits when none are due. A no-due check does not call Spotify, append an Activity row, overwrite the last real run summary, or repaint the status sheet. Scheduler health telemetry may still be updated.

### Manual runs

Expand Down Expand Up @@ -186,9 +193,9 @@ Normal runtime does not format or add validation to hidden Jobs, and does not re

## Migration

v1.4 upgrades the v1.3.8 Jobs schema by appending `Heartbeat Enabled` and defaults existing configured jobs to `true`.
v1.5 does not add a Jobs schema column. Existing `Daily` and `Every N days` Frequency strings continue to parse exactly as day schedules and retain their existing calendar-day behavior.

Migration preserves stable Job IDs, playlist IDs, names, behavior, frequencies, automation state and telemetry. It remains explicit/bounded: no whole-sheet `clearFormats()` and no OAuth/property reset.
The v1.4 migration protections remain in place: stable Job IDs, playlist IDs, names, behavior, frequencies, automation state, heartbeat state and telemetry are preserved. Migration remains explicit/bounded: no whole-sheet `clearFormats()` and no OAuth/property reset.

The old Dashboard is renamed to Spoti Sync when safe. A conflicting unrelated user sheet named `Spoti Sync` must never be cleared; a safe status fallback is used instead.

Expand All @@ -212,9 +219,9 @@ Strategies remain pure planning logic.

## Safety and resilience

- Script lock prevents overlapping playlist writes.
- Script lock prevents overlapping playlist writes, including manual and scheduled runs.
- Spotify `401` can trigger token refresh/retry.
- Spotify `429` honors practical Retry-After values.
- Spotify `429` honors practical `Retry-After` values and stops rather than sleeping past the Apps Script execution budget when the requested wait is too long.
- transient `5xx` responses use bounded retries.
- one failed job does not stop later jobs.
- heartbeat failure does not replay playlist mutations.
Expand Down
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,35 @@

All notable changes to Spoti Sync are documented here.

## 1.5.0 — 2026-08-20

### Added

- Added **Hourly** and **Every N hours** automation alongside the existing Off, Daily, and Every N days options.
- Hour-based intervals support 1–23 hours; 24 hours remains represented canonically as **Daily**.

### Automation

- Kept the single-trigger architecture. `Scheduler.reconcile()` now selects one of three states: no trigger when no jobs are automated, one daily trigger for day-only jobs, or one hourly dispatcher when any hour-based job exists.
- Mixed schedules still use only one trigger. The hourly dispatcher checks local job state first and calls Spotify only for jobs that are actually due.
- Existing single v1.4 daily triggers are retained for day-only installations instead of being recreated unnecessarily; scheduler mode is persisted in Document Properties so later reconciliations can distinguish daily and hourly cadence safely.

### Compatibility and performance

- Existing `Daily` and `Every N days` values keep their calendar-day semantics and require no Jobs schema migration.
- Hour-based schedules use elapsed hours since the last successful run.
- A no-due hourly scheduler wake does not fetch Spotify data, append Activity noise, overwrite the last real run summary, or repaint the status sheet.
- Preserved manual Sync now for Automation Off jobs, script locking, Exact Mirror / Append Only behavior, playlist heartbeat semantics, OAuth state, playlist IDs and stable Job IDs.

### Tests

- Added regression coverage for hourly parsing/bounds, elapsed-hour eligibility, mixed daily/hourly reconciliation, legacy daily-trigger retention, hourly-to-daily downgrade, duplicate-trigger normalization, no-due execution cost and existing manual/day-based behavior.
- Kept generated-sidebar boot protections, migration guards, no-`clearFormats()` protections, playlist-catalog laziness and installer-manifest checks in CI.

### Upgrade note

- Install the 1.5.0 bundle in the same Apps Script project, save, reload the Sheet, then choose **Spoti Sync → Open Spoti Sync**. Existing Spotify connection, Client ID, jobs, playlist IDs, heartbeat preferences and automation are preserved; no repair or manual trigger recreation is required.

## 1.4.1 — 2026-08-16

### Fixed
Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Spoti Sync has one normal control surface: **Spoti Sync → Open Spoti Sync**. T
- Use **Liked Songs** or a Spotify playlist as the source.
- Sync into an existing playlist or create a new target playlist.
- Choose **Exact Mirror** or **Append Only** behavior.
- Set **Automation: Off, Daily, or Every N days**.
- Set **Automation: Off, Hourly, Every N hours, Daily, or Every N days**.
- Run any job manually with **Sync now**.
- Optionally keep the target playlist description updated with Spoti Sync status.

Expand All @@ -43,11 +43,13 @@ No web-app deployment, local server, Node.js installation, or `clasp` setup is r
| **Source** | Liked Songs or Spotify playlist |
| **Target** | Existing playlist or create a new playlist |
| **Behavior** | Exact Mirror or Append Only |
| **Automation** | Off, Daily, or Every N days |
| **Automation** | Off, Hourly, Every N hours, Daily, or Every N days |

**Exact Mirror** keeps the managed target membership aligned with the source.
**Append Only** adds missing source tracks and never removes existing target tracks.

Spoti Sync still uses one background scheduler. If any job uses an hour-based interval, jobs are checked hourly; Spotify is contacted only for jobs that are actually due.

## 🔐 Privacy

Spoti Sync runs in your own Google Apps Script environment. Spotify access and refresh tokens are stored in Apps Script User Properties, not on a hosted Spoti Sync backend or GitHub Pages.
Expand All @@ -71,6 +73,7 @@ Node.js 22+ is used only for local build and test tooling.
node scripts/build.js
node scripts/test.js
node scripts/test-v14.js
node scripts/test-v15.js
node scripts/test-scheduler.js
node scripts/test-sheet-repair.js
node scripts/test-job-editor.js
Expand Down
4 changes: 2 additions & 2 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,9 @@ <h2>Only the choices that matter.</h2>
<article><span>Source</span><strong>Liked Songs<br>or Spotify playlist</strong></article>
<article><span>Target</span><strong>Existing playlist<br>or create new</strong></article>
<article><span>Behavior</span><strong>Exact Mirror<br>or Append Only</strong></article>
<article><span>Automation</span><strong>Off · Daily<br>Every N days</strong></article>
<article><span>Automation</span><strong>Off · Hourly · Every N hours<br>Daily · Every N days</strong></article>
</div>
<p class="section-intro">Optional per job: keep the target playlist description updated with the latest successful sync time.</p>
<p class="section-intro">Hour-based jobs are checked hourly, but Spotify is contacted only when a job is actually due. Optional per job: keep the target playlist description updated with the latest successful sync time.</p>
</section>

<section id="install" class="section dark-section">
Expand Down
10 changes: 5 additions & 5 deletions docs/version.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"schema": 1,
"version": "1.4.1",
"version": "1.5.0",
"channel": "stable",
"released_at": "2026-08-16",
"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": [
"Fixes the blank v1.4 sidebar caused by a browser-global top identifier collision in generated client JavaScript.",
"Adds a visible loading state and compact runtime boot-error fallback so sidebar failures are no longer silent.",
"Adds regression coverage that parses the generated production sidebar script and guards against redeclaring the browser top global."
"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."
]
}
1 change: 1 addition & 0 deletions scripts/test-docs.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ const requiredCurrentFlow = [
'Spoti Sync → Open Spoti Sync',
'Exact Mirror',
'Append Only',
'Every N hours',
'Every N days'
];

Expand Down
Loading