librqbit 9.0.1

The main library used by rqbit torrent client. The binary is just a small wrapper on top of it.
Documentation
import { useRef, useEffect, useContext, useState } from "react";
import { APIContext } from "../../context";
import { customSetInterval } from "../../helper/customSetInterval";
import { STATE_LIVE, STATE_INITIALIZING, TorrentStats } from "../../api-types";

interface PiecesCanvasProps {
  torrentId: number;
  totalPieces: number;
  stats: TorrentStats | null;
}

const CANVAS_HEIGHT = 12;

export const PiecesCanvas: React.FC<PiecesCanvasProps> = ({
  torrentId,
  totalPieces,
  stats,
}) => {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const containerRef = useRef<HTMLDivElement>(null);
  const API = useContext(APIContext);
  const [bitmap, setBitmap] = useState<Uint8Array | null>(null);

  // Fetch bitmap with adaptive polling
  useEffect(() => {
    if (totalPieces === 0) return;

    return customSetInterval(async () => {
      try {
        const buffer = await API.getTorrentHaves(torrentId);
        setBitmap(buffer);
      } catch (e) {
        console.error("Failed to fetch haves:", e);
      }

      // Poll faster when downloading, slower when paused/seeding
      const isActive =
        stats?.state === STATE_LIVE || stats?.state === STATE_INITIALIZING;
      const isFinished = stats?.finished ?? false;

      if (isActive && !isFinished) {
        return 2000; // 2s while downloading
      }
      return 30000; // 30s when paused or seeding
    }, 0);
  }, [torrentId, totalPieces, stats?.state, stats?.finished]);

  // Render bitmap to canvas - single horizontal line
  useEffect(() => {
    const canvas = canvasRef.current;
    const container = containerRef.current;
    if (!canvas || !container || !bitmap || totalPieces === 0) return;

    const ctx = canvas.getContext("2d");
    if (!ctx) return;

    const containerWidth = container.clientWidth;
    const canvasWidth = Math.max(containerWidth, 100);

    canvas.width = canvasWidth;
    canvas.height = CANVAS_HEIGHT;

    // Get colors from CSS custom properties (read from body where .dark class is applied)
    const computedStyle = getComputedStyle(document.body);
    const haveColor =
      computedStyle.getPropertyValue("--color-success-bg").trim() || "#22c55e";
    const missingColor =
      computedStyle.getPropertyValue("--color-divider").trim() || "#374151";

    // Fill background with missing color
    ctx.fillStyle = missingColor;
    ctx.fillRect(0, 0, canvasWidth, CANVAS_HEIGHT);

    // Helper to check if piece is present
    const hasPiece = (pieceIndex: number): boolean => {
      const byteIndex = Math.floor(pieceIndex / 8);
      const bitIndex = 7 - (pieceIndex % 8); // MSB0 ordering
      return (
        byteIndex < bitmap.length && ((bitmap[byteIndex] >> bitIndex) & 1) === 1
      );
    };

    const pieceWidth = canvasWidth / totalPieces;
    ctx.fillStyle = haveColor;

    let runStart = -1;

    for (let i = 0; i < totalPieces; i++) {
      if (hasPiece(i)) {
        if (runStart === -1) runStart = i;
      } else {
        if (runStart !== -1) {
          const x = runStart * pieceWidth;
          const width = (i - runStart) * pieceWidth;
          ctx.fillRect(x, 0, width, CANVAS_HEIGHT);
          runStart = -1;
        }
      }
    }

    if (runStart !== -1) {
      const x = runStart * pieceWidth;
      const width = (totalPieces - runStart) * pieceWidth;
      ctx.fillRect(x, 0, width, CANVAS_HEIGHT);
    }
  }, [bitmap, totalPieces]);

  if (totalPieces === 0) {
    return null;
  }

  return (
    <div ref={containerRef} className="w-full">
      <canvas
        ref={canvasRef}
        className="w-full rounded"
        style={{ height: `${CANVAS_HEIGHT}px`, imageRendering: "pixelated" }}
      />
    </div>
  );
};