#include "photo_queue.h" #include "immich_client.h" #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; Settings s = _settings->get(); std::vector newIds; // Get selected album IDs std::vector albumIds = getSelectedAlbumIds(); if (albumIds.empty()) { // "All photos" mode — fetch from all albums auto albums = _client->fetchAlbums(); for (auto& album : albums) { auto ids = _client->fetchAlbumAssetIds(album.id); for (auto& id : ids) { newIds.push_back(id); } } } else { // Fetch from selected albums only for (auto& albumId : albumIds) { auto ids = _client->fetchAlbumAssetIds(albumId); for (auto& id : ids) { newIds.push_back(id); } } } // Deduplicate std::sort(newIds.begin(), newIds.end()); newIds.erase(std::unique(newIds.begin(), newIds.end()), newIds.end()); if (newIds.empty()) { Serial.println("[queue] No assets found"); 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; } // Clamp cursor if (_cursor >= _queue.size()) { _cursor = 0; } _lastSyncTime = millis(); Serial.printf("[queue] Synced: %d assets, cursor at %d\n", _queue.size(), _cursor); return true; } String PhotoQueue::next() { if (_queue.empty()) return ""; if (_cursor >= _queue.size()) { _cursor = 0; shuffle(); // Reshuffle on wrap for random mode } String id = _queue[_cursor++]; // Persist cursor _settings->saveField("queue_cursor", static_cast(_cursor & 0xFF)); 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() { if (_lastSyncTime == 0) return true; unsigned long elapsed = millis() - _lastSyncTime; return elapsed >= (QUEUE_RESYNC_HOURS * 3600000UL); } String PhotoQueue::findPortraitPair(size_t startIndex) { // Look ahead up to PORTRAIT_LOOKAHEAD items for another portrait for (size_t i = 1; i <= PORTRAIT_LOOKAHEAD && (startIndex + i) < _queue.size(); i++) { // We'd need asset info to determine portrait status // This will be called by the display task which fetches AssetInfo // Return the ID — caller checks isPortrait from AssetInfo return _queue[startIndex + i]; } 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) { // For chronological sort, we'd need timestamps which we don't store in the queue // The IDs from Immich are UUIDs, not sortable by time // For now, keep the order returned by Immich (which is chronological within albums) 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(); // Add favorites 2 more times (total 3× appearance) for (auto& fav : favorites) { // Only add if already in queue 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; }