Files
immich-frame/src/photo_queue.cpp

266 lines
7.8 KiB
C++
Raw Normal View History

#include "photo_queue.h"
#include "immich_client.h"
#include <ArduinoJson.h>
#include <algorithm>
#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<String> newIds;
std::vector<String> 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<uint32_t>(_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<String>& 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<size_t> 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<String> sorted;
sorted.reserve(_queue.size());
for (size_t idx : indices) {
sorted.push_back(_queue[idx]);
}
_queue = sorted;
}
void PhotoQueue::applyRecencyWeighting(const std::vector<String>& dates, bool favorRecent) {
if (dates.size() != _queue.size() || _queue.size() < 3) return;
// Sort queue by date (oldest first)
std::vector<size_t> 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<String> 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<String> PhotoQueue::getSelectedAlbumIds() {
std::vector<String> ids;
Settings s = _settings->get();
JsonDocument doc;
DeserializationError err = deserializeJson(doc, s.albums_json);
if (err) return ids;
JsonArray arr = doc.as<JsonArray>();
for (JsonVariant v : arr) {
ids.push_back(v.as<String>());
}
return ids;
}