#include "photo_queue.h" #include "immich_client.h" #include #include #include "config.h" void PhotoQueue::begin(ImmichClient& client, SettingsManager& settings) { _client = &client; _settings = &settings; Settings s = _settings->get(); _cursor = s.queue_cursor; } bool PhotoQueue::sync() { if (_client == nullptr || _settings == nullptr) return false; _lastSyncAttempt = millis(); Settings s = _settings->get(); // Fetch in batches until we have TARGET_QUEUE_SIZE usable, non-duplicate IDs. // Use fetchRandomAssets() to capture dates alongside IDs. std::vector newIds; std::vector newDates; int rounds = 0; Serial.printf("[queue] Fetching until %d usable assets (excluding %d shown)\n", TARGET_QUEUE_SIZE, _shown.size()); while (newIds.size() < TARGET_QUEUE_SIZE && rounds < MAX_FETCH_ROUNDS) { auto batch = _client->fetchRandomAssets(50); if (batch.empty()) { Serial.printf("[queue] Fetch round %d returned empty\n", rounds + 1); break; } 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); } rounds++; Serial.printf("[queue] Round %d: have %d/%d usable assets\n", 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; } _queue = newIds; // Apply cycling mode switch (s.cycle_mode) { case CycleMode::Random: applyFavoritesWeighting(); shuffle(); break; case CycleMode::Chronological: sortChronological(newDates, false); break; case CycleMode::ReverseChronological: sortChronological(newDates, true); break; case CycleMode::FavoritesWeighted: applyFavoritesWeighting(); shuffle(); break; case CycleMode::WeightedChronological: applyRecencyWeighting(newDates, true); shuffle(); break; case CycleMode::WeightedReverseChronological: applyRecencyWeighting(newDates, 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; } 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 (_queue.empty()) return ""; String id = _queue[_cursor++]; // Track shown IDs for dedup on next refill _shown.push_back(id); if (_shown.size() > MAX_SHOWN_HISTORY) { _shown.erase(_shown.begin()); } // Persist cursor _settings->saveField("queue_cursor", static_cast(_cursor)); return id; } String PhotoQueue::random() { if (_queue.empty()) return ""; size_t idx = ::random(0, _queue.size()); return _queue[idx]; } String PhotoQueue::current() { if (_queue.empty()) return ""; size_t idx = (_cursor > 0) ? _cursor - 1 : 0; return _queue[idx]; } size_t PhotoQueue::size() { return _queue.size(); } bool PhotoQueue::needsResync() { unsigned long now = millis(); // If never synced successfully, use retry backoff if (_lastSyncTime == 0) { if (_lastSyncAttempt == 0) return true; return (now - _lastSyncAttempt) >= _syncRetryDelay; } // Normal resync interval unsigned long elapsed = now - _lastSyncTime; return elapsed >= (QUEUE_RESYNC_HOURS * 3600000UL); } String PhotoQueue::findNextPortrait() { if (_client == nullptr) return ""; size_t scanEnd = min(_cursor + PORTRAIT_LOOKAHEAD, _queue.size()); for (size_t i = _cursor; i < scanEnd; i++) { AssetInfo info = _client->fetchAssetInfo(_queue[i]); if (info.isPortrait) { String id = _queue[i]; _queue.erase(_queue.begin() + i); Serial.printf("[queue] Found portrait pair: %s (scanned %d ahead)\n", id.c_str(), (int)(i - _cursor + 1)); return id; } } Serial.println("[queue] No portrait pair found in lookahead"); return ""; } void PhotoQueue::shuffle() { for (size_t i = _queue.size() - 1; i > 0; i--) { size_t j = ::random(0, i + 1); std::swap(_queue[i], _queue[j]); } } void PhotoQueue::sortChronological(const std::vector& dates, bool reverse) { if (dates.size() != _queue.size()) { Serial.println("[queue] sortChronological: date/queue size mismatch, skipping"); return; } // Build index array sorted by date (ISO 8601 strings compare correctly) std::vector indices(dates.size()); for (size_t i = 0; i < indices.size(); i++) indices[i] = i; std::sort(indices.begin(), indices.end(), [&](size_t a, size_t b) { return dates[a] < dates[b]; }); if (reverse) std::reverse(indices.begin(), indices.end()); std::vector sorted; sorted.reserve(_queue.size()); for (size_t idx : indices) { sorted.push_back(_queue[idx]); } _queue = sorted; } void PhotoQueue::applyRecencyWeighting(const std::vector& dates, bool favorRecent) { if (dates.size() != _queue.size() || _queue.size() < 3) return; // Sort queue by date (oldest first) std::vector indices(dates.size()); for (size_t i = 0; i < indices.size(); i++) indices[i] = i; std::sort(indices.begin(), indices.end(), [&](size_t a, size_t b) { return dates[a] < dates[b]; }); std::vector sorted; sorted.reserve(_queue.size()); for (size_t idx : indices) { sorted.push_back(_queue[idx]); } _queue = sorted; // Duplicate the preferred third (3x copies total) size_t third = _queue.size() / 3; size_t start = favorRecent ? _queue.size() - third : 0; size_t end = favorRecent ? _queue.size() : third; for (size_t i = start; i < end; i++) { _queue.push_back(_queue[i]); _queue.push_back(_queue[i]); } Serial.printf("[queue] Recency weighting: %s third duplicated (%d -> %d entries)\n", favorRecent ? "recent" : "oldest", (int)sorted.size(), (int)_queue.size()); } void PhotoQueue::applyFavoritesWeighting() { if (_client == nullptr) return; Settings s = _settings->get(); if (s.cycle_mode != CycleMode::FavoritesWeighted) return; auto favorites = _client->fetchFavoriteAssetIds(); for (auto& fav : favorites) { bool inQueue = false; for (auto& id : _queue) { if (id == fav) { inQueue = true; break; } } if (inQueue) { _queue.push_back(fav); _queue.push_back(fav); } } } std::vector PhotoQueue::getSelectedAlbumIds() { std::vector ids; Settings s = _settings->get(); JsonDocument doc; DeserializationError err = deserializeJson(doc, s.albums_json); if (err) return ids; JsonArray arr = doc.as(); for (JsonVariant v : arr) { ids.push_back(v.as()); } return ids; }