feat: wire up album filtering to photo queue
Album selection now controls which photos are displayed.
Uses POST /api/search/random with albumIds filter (Immich v3
compatible) instead of the removed GET /api/albums/{id} assets.
- Add albumIds parameter to fetchRandomAssets/fetchRandomAssetIds
- syncFromAlbums: fetch random assets filtered by selected albums
- syncFromAllPhotos: preserve existing global random behavior
- Album mode reshuffles on exhaustion instead of re-fetching
- 24h resync checks album assetCounts for changes before re-fetch
- requestSync + albums_changed action triggers queue rebuild on save
- timerWakeCycle respects album selection
- Discard stale pre-renders on album change
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -16,11 +16,79 @@ bool PhotoQueue::sync() {
|
||||
if (_client == nullptr || _settings == nullptr) return false;
|
||||
|
||||
_lastSyncAttempt = millis();
|
||||
_syncRequested = false;
|
||||
|
||||
Settings s = _settings->get();
|
||||
auto albumIds = getSelectedAlbumIds();
|
||||
|
||||
bool success;
|
||||
if (!albumIds.empty()) {
|
||||
success = syncFromAlbums(albumIds, s);
|
||||
} else {
|
||||
success = syncFromAllPhotos(s);
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
_syncRetryDelay = min(_syncRetryDelay * 2, SYNC_RETRY_MAX);
|
||||
Serial.printf("[queue] Sync failed, retry in %lus\n", _syncRetryDelay / 1000);
|
||||
return false;
|
||||
}
|
||||
|
||||
_cursor = 0;
|
||||
_lastSyncTime = millis();
|
||||
_syncRetryDelay = SYNC_RETRY_MIN;
|
||||
Serial.printf("[queue] Synced: %d assets, album_mode=%d, cursor at %d\n",
|
||||
_queue.size(), _albumMode, _cursor);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PhotoQueue::syncFromAlbums(const std::vector<String>& albumIds, const Settings& s) {
|
||||
Serial.printf("[queue] Syncing from %d selected album(s)\n", albumIds.size());
|
||||
|
||||
// Get current album counts for 24h resync change detection
|
||||
auto albums = _client->fetchAlbums();
|
||||
std::map<String, int> albumCounts;
|
||||
for (auto& selId : albumIds) {
|
||||
for (auto& album : albums) {
|
||||
if (album.id == selId) {
|
||||
albumCounts[selId] = album.assetCount;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use POST /api/search/random with albumIds filter (Immich v3 compatible).
|
||||
// The server returns up to `size` random assets from the specified albums.
|
||||
// For small albums (< TARGET_QUEUE_SIZE), all assets are returned.
|
||||
auto batch = _client->fetchRandomAssets(
|
||||
static_cast<int>(TARGET_QUEUE_SIZE), albumIds);
|
||||
|
||||
if (batch.empty()) {
|
||||
Serial.println("[queue] No assets found in selected albums");
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<String> newIds;
|
||||
std::vector<String> newDates;
|
||||
for (auto& asset : batch) {
|
||||
if (std::find(newIds.begin(), newIds.end(), asset.id) != newIds.end()) continue;
|
||||
newIds.push_back(asset.id);
|
||||
newDates.push_back(asset.dateTime);
|
||||
}
|
||||
|
||||
Serial.printf("[queue] Album sync: %d assets from server\n", newIds.size());
|
||||
|
||||
_queue = newIds;
|
||||
_albumMode = true;
|
||||
_lastAlbumCounts = albumCounts;
|
||||
|
||||
applyCycleMode(s, newDates);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PhotoQueue::syncFromAllPhotos(const Settings& s) {
|
||||
Serial.printf("[queue] Syncing from all photos (global random)\n");
|
||||
|
||||
// Fetch in batches until we have TARGET_QUEUE_SIZE usable, non-duplicate IDs.
|
||||
// Use fetchRandomAssets() to capture dates alongside IDs.
|
||||
std::vector<String> newIds;
|
||||
std::vector<String> newDates;
|
||||
int rounds = 0;
|
||||
@@ -37,10 +105,8 @@ bool PhotoQueue::sync() {
|
||||
|
||||
for (auto& asset : batch) {
|
||||
if (newIds.size() >= TARGET_QUEUE_SIZE) break;
|
||||
|
||||
if (std::find(newIds.begin(), newIds.end(), asset.id) != newIds.end()) continue;
|
||||
if (std::find(_shown.begin(), _shown.end(), asset.id) != _shown.end()) continue;
|
||||
|
||||
newIds.push_back(asset.id);
|
||||
newDates.push_back(asset.dateTime);
|
||||
}
|
||||
@@ -50,58 +116,67 @@ bool PhotoQueue::sync() {
|
||||
rounds, newIds.size(), TARGET_QUEUE_SIZE);
|
||||
}
|
||||
|
||||
if (newIds.empty()) {
|
||||
_syncRetryDelay = min(_syncRetryDelay * 2, SYNC_RETRY_MAX);
|
||||
Serial.printf("[queue] Sync failed after %d rounds, retry in %lus\n",
|
||||
rounds, _syncRetryDelay / 1000);
|
||||
return false;
|
||||
}
|
||||
if (newIds.empty()) return false;
|
||||
|
||||
_queue = newIds;
|
||||
_albumMode = false;
|
||||
_lastAlbumCounts.clear();
|
||||
|
||||
// Apply cycling mode
|
||||
applyCycleMode(s, newDates);
|
||||
Serial.printf("[queue] All-photos sync: %d assets in %d rounds\n",
|
||||
_queue.size(), rounds);
|
||||
return true;
|
||||
}
|
||||
|
||||
void PhotoQueue::applyCycleMode(const Settings& s, const std::vector<String>& dates) {
|
||||
switch (s.cycle_mode) {
|
||||
case CycleMode::Random:
|
||||
applyFavoritesWeighting();
|
||||
shuffle();
|
||||
break;
|
||||
case CycleMode::Chronological:
|
||||
sortChronological(newDates, false);
|
||||
sortChronological(dates, false);
|
||||
break;
|
||||
case CycleMode::ReverseChronological:
|
||||
sortChronological(newDates, true);
|
||||
sortChronological(dates, true);
|
||||
break;
|
||||
case CycleMode::FavoritesWeighted:
|
||||
applyFavoritesWeighting();
|
||||
shuffle();
|
||||
break;
|
||||
case CycleMode::WeightedChronological:
|
||||
applyRecencyWeighting(newDates, true);
|
||||
applyRecencyWeighting(dates, true);
|
||||
shuffle();
|
||||
break;
|
||||
case CycleMode::WeightedReverseChronological:
|
||||
applyRecencyWeighting(newDates, false);
|
||||
applyRecencyWeighting(dates, false);
|
||||
shuffle();
|
||||
break;
|
||||
// No default — compiler warns on unhandled CycleMode via -Wswitch
|
||||
}
|
||||
}
|
||||
|
||||
// Reset cursor for fresh queue
|
||||
_cursor = 0;
|
||||
|
||||
_lastSyncTime = millis();
|
||||
_syncRetryDelay = SYNC_RETRY_MIN;
|
||||
Serial.printf("[queue] Synced: %d assets in %d rounds, cursor at %d\n",
|
||||
_queue.size(), rounds, _cursor);
|
||||
return true;
|
||||
void PhotoQueue::requestSync() {
|
||||
_syncRequested = true;
|
||||
}
|
||||
|
||||
String PhotoQueue::next() {
|
||||
// Auto-refill when queue is exhausted
|
||||
if (_queue.empty() || _cursor >= _queue.size()) {
|
||||
Serial.println("[queue] Queue exhausted, fetching new batch");
|
||||
sync();
|
||||
_cursor = 0;
|
||||
if (_albumMode && !_queue.empty()) {
|
||||
// Album mode: reshuffle existing queue instead of re-fetching
|
||||
Serial.println("[queue] Album queue exhausted, reshuffling in place");
|
||||
Settings s = _settings->get();
|
||||
// Clear shown history since we're replaying the same set
|
||||
_shown.clear();
|
||||
applyCycleMode(s, {});
|
||||
_cursor = 0;
|
||||
} else {
|
||||
// All-photos mode: fetch a new batch
|
||||
Serial.println("[queue] Queue exhausted, fetching new batch");
|
||||
sync();
|
||||
_cursor = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (_queue.empty()) return "";
|
||||
@@ -137,6 +212,8 @@ size_t PhotoQueue::size() {
|
||||
}
|
||||
|
||||
bool PhotoQueue::needsResync() {
|
||||
if (_syncRequested) return true;
|
||||
|
||||
unsigned long now = millis();
|
||||
|
||||
// If never synced successfully, use retry backoff
|
||||
@@ -147,7 +224,44 @@ bool PhotoQueue::needsResync() {
|
||||
|
||||
// Normal resync interval
|
||||
unsigned long elapsed = now - _lastSyncTime;
|
||||
return elapsed >= (QUEUE_RESYNC_HOURS * 3600000UL);
|
||||
if (elapsed < (QUEUE_RESYNC_HOURS * 3600000UL)) return false;
|
||||
|
||||
// In album mode, check if album sizes actually changed before re-fetching
|
||||
if (_albumMode && !_lastAlbumCounts.empty()) {
|
||||
auto albums = _client->fetchAlbums();
|
||||
auto selectedIds = getSelectedAlbumIds();
|
||||
bool changed = false;
|
||||
|
||||
for (auto& selId : selectedIds) {
|
||||
int currentCount = 0;
|
||||
for (auto& album : albums) {
|
||||
if (album.id == selId) {
|
||||
currentCount = album.assetCount;
|
||||
break;
|
||||
}
|
||||
}
|
||||
auto it = _lastAlbumCounts.find(selId);
|
||||
int lastCount = (it != _lastAlbumCounts.end()) ? it->second : -1;
|
||||
if (currentCount != lastCount) {
|
||||
Serial.printf("[queue] Album %s count changed: %d -> %d\n",
|
||||
selId.c_str(), lastCount, currentCount);
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!changed) {
|
||||
Serial.println("[queue] Album counts unchanged, reshuffling instead of re-fetching");
|
||||
Settings s = _settings->get();
|
||||
_shown.clear();
|
||||
applyCycleMode(s, {});
|
||||
_cursor = 0;
|
||||
_lastSyncTime = millis();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
String PhotoQueue::findNextPortrait() {
|
||||
@@ -177,6 +291,11 @@ void PhotoQueue::shuffle() {
|
||||
|
||||
void PhotoQueue::sortChronological(const std::vector<String>& dates, bool reverse) {
|
||||
if (dates.size() != _queue.size()) {
|
||||
// Dates unavailable (e.g. reshuffle after exhaustion) — fall back to shuffle
|
||||
if (dates.empty()) {
|
||||
shuffle();
|
||||
return;
|
||||
}
|
||||
Serial.println("[queue] sortChronological: date/queue size mismatch, skipping");
|
||||
return;
|
||||
}
|
||||
@@ -200,7 +319,11 @@ void PhotoQueue::sortChronological(const std::vector<String>& dates, bool revers
|
||||
}
|
||||
|
||||
void PhotoQueue::applyRecencyWeighting(const std::vector<String>& dates, bool favorRecent) {
|
||||
if (dates.size() != _queue.size() || _queue.size() < 3) return;
|
||||
if (dates.size() != _queue.size() || _queue.size() < 3) {
|
||||
// Dates unavailable — fall back to shuffle
|
||||
if (dates.empty()) return;
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort queue by date (oldest first)
|
||||
std::vector<size_t> indices(dates.size());
|
||||
|
||||
Reference in New Issue
Block a user