Commit 9f20e3e8 authored by jan.koester's avatar jan.koester
Browse files

deb



Co-authored-by: default avatarCopilot <copilot@github.com>
parent b309056b
Loading
Loading
Loading
Loading
+18 −0
Original line number Diff line number Diff line
mediadb (20260423+70) unstable; urgency=medium

  * Cluster data consistency: tag media listings with availability flag
    (cluster_available) so the frontend can indicate when media data is
    not yet replicated to the cluster.
  * Increase cluster fetch/fetch_range deadline from 10s to 30s to avoid
    premature timeouts on congested networks.
  * Return HTTP 503 with Retry-After header instead of 422/500 when
    preview rendering or media data fetch fails due to cluster timeouts
    or queue pressure — signals clients to retry.
  * Frontend: throttle preview fetches to 6 concurrent requests, fixing
    NS_BINDING_ABORTED in Firefox. Add AbortController for cancellation
    and automatic 503 retry with 2s backoff.
  * Frontend: dim unavailable media items in admin table with warning
    indicator.

 -- Jan Koester <jan.koester@tuxist.de>  Wed, 23 Apr 2026 00:00:00 +0200

mediadb (20260423+69) unstable; urgency=high

  * Fix tombstone infinite loop: load_tombstones() now REPLACES the local
+32 −9
Original line number Diff line number Diff line
@@ -32,6 +32,8 @@ tr:hover{background:#1e1e1e}
.preview-cell{display:flex;flex-direction:column;align-items:flex-start;gap:.4rem}
.preview-cell img{max-width:120px;max-height:90px;border-radius:6px;border:1px solid #333;cursor:pointer}
.preview-cell img:hover{border-color:#4fc3f7}
tr.media-unavailable{opacity:.5}
tr.media-unavailable td:first-child::after{content:" \u26a0";color:#ff9800;font-size:.7rem}
.preview-controls{display:flex;align-items:center;gap:.4rem;font-size:.75rem;color:#aaa}
.preview-controls input[type=range]{width:100px;accent-color:#4fc3f7}
.preview-controls select{padding:.15rem .3rem;background:#222;color:#eee;border:1px solid #444;border-radius:4px;font-size:.75rem}
@@ -178,18 +180,38 @@ function fmtSize(b) {
  return (b / 1048576).toFixed(1) + ' MB';
}

// ---- Preview loader (synchronous) ----
function loadPreviewAsync(img, mediaId, fmt, t, width) {
  const w = width || 120;
  const url = `/media/${mediaId}/preview?w=${w}&fmt=${encodeURIComponent(fmt)}&t=${t}`;
  fetch(url, {credentials:'same-origin'}).then(r => {
// ---- Preview loader with throttling ----
const previewQueue = [];
let previewActive = 0;
const MAX_CONCURRENT_PREVIEWS = 6;

function drainPreviewQueue() {
  while (previewActive < MAX_CONCURRENT_PREVIEWS && previewQueue.length > 0) {
    const job = previewQueue.shift();
    previewActive++;
    const ctrl = new AbortController();
    job.img._previewAbort = ctrl;
    fetch(job.url, {credentials:'same-origin', signal: ctrl.signal}).then(r => {
      if (r.status === 200) {
        return r.blob().then(blob => {
        img.src = URL.createObjectURL(blob);
        img.style.opacity = '';
          job.img.src = URL.createObjectURL(blob);
          job.img.style.opacity = '';
        });
      } else if (r.status === 503) {
        // Server busy — re-queue with delay
        setTimeout(() => { previewQueue.push(job); drainPreviewQueue(); }, 2000);
      }
    }).catch(() => {}).finally(() => { previewActive--; drainPreviewQueue(); });
  }
}
  }).catch(() => {});

function loadPreviewAsync(img, mediaId, fmt, t, width) {
  // Cancel any previous in-flight request for this image
  if (img._previewAbort) { img._previewAbort.abort(); img._previewAbort = null; }
  const w = width || 120;
  const url = `/media/${mediaId}/preview?w=${w}&fmt=${encodeURIComponent(fmt)}&t=${t}`;
  previewQueue.push({img, url});
  drainPreviewQueue();
}

// ---- Stores ----
@@ -465,6 +487,7 @@ async function showMedia(albumName) {
      }
      tr.innerHTML = `<td style="font-family:monospace;font-size:.78rem;color:#888">${m.id}</td>${previewHtml}<td>${fname}</td><td>${m.kind}</td><td>${fmtSize(m.size_bytes)}</td><td>${m.created_at}</td>
        <td><button class="btn btn-danger btn-sm del-media" data-id="${m.id}">Delete</button></td>`;
      if (m.available === false) tr.className = 'media-unavailable';
      tb.appendChild(tr);
    });
    tb.querySelectorAll('.del-media').forEach(b => b.addEventListener('click', async () => {
+28 −7
Original line number Diff line number Diff line
@@ -50,6 +50,33 @@ nav a{color:#4fc3f7;text-decoration:none;margin-right:1rem}
<script>
const API = '';

// ---- Preview fetch throttling ----
const galQueue = [];
let galActive = 0;
const GAL_MAX_CONCURRENT = 6;

function galDrain() {
  while (galActive < GAL_MAX_CONCURRENT && galQueue.length > 0) {
    const job = galQueue.shift();
    galActive++;
    const ctrl = new AbortController();
    fetch(job.url, {signal: ctrl.signal}).then(r => {
      if (r.ok) return r.blob().then(blob => {
        job.img.src = URL.createObjectURL(blob);
        job.img.style.opacity = '';
      });
      if (r.status === 503) {
        setTimeout(() => { galQueue.push(job); galDrain(); }, 2000);
      }
    }).catch(() => {}).finally(() => { galActive--; galDrain(); });
  }
}

function galLoadPreview(img, url) {
  galQueue.push({img, url});
  galDrain();
}

async function loadPublicAlbums() {
  try {
    const res = await fetch(API + '/public/albums');
@@ -98,13 +125,7 @@ async function openAlbum(albumId, albumName) {
        img.style.opacity = '0.3';
        img.src = '';
        item.appendChild(img);
        // load preview
        fetch(`/media/${m.id}/preview?w=240&fmt=webp`).then(r => {
          if (r.ok) return r.blob().then(blob => {
            img.src = URL.createObjectURL(blob);
            img.style.opacity = '';
          });
        }).catch(() => {});
        galLoadPreview(img, `/media/${m.id}/preview?w=240&fmt=webp`);
      } else {
        const placeholder = document.createElement('div');
        placeholder.style.cssText = 'width:100%;height:140px;display:flex;align-items:center;justify-content:center;background:#222;color:#666;font-size:2rem';
+22 −3
Original line number Diff line number Diff line
@@ -372,6 +372,7 @@ json_object* App::media_to_json(const MediaRecord& m) {
    json_object_object_add(j, "store_id", json_object_new_string(m.store_id.c_str()));
    json_object_object_add(j, "created_at", json_object_new_string(m.created_at.c_str()));
    json_object_object_add(j, "size_bytes", json_object_new_double(static_cast<double>(m.size_bytes)));
    json_object_object_add(j, "available", json_object_new_boolean(m.cluster_available));
    return j;
}

@@ -1608,7 +1609,11 @@ HttpResponse App::handle_get_media_raw(const HttpRequest& req) {
        const std::uint64_t length = range_end - range_start + 1;

        auto data = db_.read_media_data_range(media_id, range_start, length);
        if (data.empty()) return error_json(500, "could not read media data range");
        if (data.empty()) {
            auto resp = error_json(503, "media data temporarily unavailable");
            resp.headers["Retry-After"] = "5";
            return resp;
        }

        // Speculative prefetch: fire-and-forget fetch of the next 2 chunks
        // so sequential playback hits the cache.
@@ -1655,7 +1660,11 @@ HttpResponse App::handle_get_media_raw(const HttpRequest& req) {
    for (std::uint64_t off = 0; off < total_size; off += CHUNK) {
        std::uint64_t len = std::min(CHUNK, total_size - off);
        auto chunk = db_.read_media_data_range(media_id, off, len);
        if (chunk.empty()) return error_json(500, "could not read media data");
        if (chunk.empty()) {
            auto resp = error_json(503, "media data temporarily unavailable");
            resp.headers["Retry-After"] = "5";
            return resp;
        }
        full_data.insert(full_data.end(), chunk.begin(), chunk.end());
    }

@@ -1725,7 +1734,17 @@ HttpResponse App::handle_preview(const HttpRequest& req) {
    } catch (...) {
        return error_json(500, "preview render crashed");
    }
    if (!preview) return error_json(422, error.empty() ? "preview failed" : error);
    if (!preview) {
        // Cluster data temporarily unavailable — signal client to retry
        if (error.find("timed out") != std::string::npos
            || error.find("queue full") != std::string::npos
            || error.find("in-flight") != std::string::npos) {
            auto resp = error_json(503, error);
            resp.headers["Retry-After"] = "5";
            return resp;
        }
        return error_json(422, error.empty() ? "preview failed" : error);
    }

    // If-None-Match for freshly generated preview
    auto inm = req.headers.find("if-none-match");
+16 −1
Original line number Diff line number Diff line
@@ -2823,7 +2823,22 @@ std::optional<MediaRecord> ClusterMediaBackend::get_media(const std::string& id)

std::vector<MediaRecord> ClusterMediaBackend::list_media(const std::string& album_id) const {
    std::shared_lock<std::shared_mutex> cguard(cluster_op_mutex_);
    return local_.list_media(album_id);
    auto items = local_.list_media(album_id);
    if (!cluster_.isRunning()) return items;
    // Tag each media record with cluster availability using cached peer group data.
    auto peer_groups = cluster_.list_peer_groups();
    std::unordered_set<uint64_t> known_gids;
    for (const auto& pg : peer_groups) {
        if (pg.online) {
            known_gids.insert(pg.groups.begin(), pg.groups.end());
        }
    }
    for (auto& m : items) {
        uint64_t gid = cluster_group_id("media:" + m.id);
        m.cluster_available = (known_gids.count(gid) > 0)
                              || !m.pending_data.empty();
    }
    return items;
}

bool ClusterMediaBackend::delete_media(const std::string& id) {
Loading