#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 std::vector newIds; 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->fetchRandomAssetIds(50); if (batch.empty()) { Serial.printf("[queue] Fetch round %d returned empty\n", rounds + 1); break; } for (auto& id : batch) { if (newIds.size() >= TARGET_QUEUE_SIZE) break; // Skip if already in this batch if (std::find(newIds.begin(), newIds.end(), id) != newIds.end()) continue; // Skip if recently shown if (std::find(_shown.begin(), _shown.end(), id) != _shown.end()) continue; newIds.push_back(id); } 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(false); break; case CycleMode::ReverseChronological: sortChronological(true); break; case CycleMode::FavoritesWeighted: applyFavoritesWeighting(); shuffle(); break; } // 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(bool reverse) { if (reverse) { std::reverse(_queue.begin(), _queue.end()); } } 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; }