librqbit 8.1.1

The main library used by rqbit torrent client. The binary is just a small wrapper on top of it.
Documentation
import { GoClock, GoFile, GoPeople } from "react-icons/go";
import {
  TorrentDetails,
  TorrentStats,
  STATE_INITIALIZING,
  ErrorDetails,
} from "../api-types";
import { TorrentActions } from "./buttons/TorrentActions";
import { ProgressBar } from "./ProgressBar";
import { Speed } from "./Speed";
import { formatBytes } from "../helper/formatBytes";
import { torrentDisplayName } from "../helper/getTorrentDisplayName";
import { getCompletionETA } from "../helper/getCompletionETA";
import { StatusIcon } from "./StatusIcon";
import { FileListInput } from "./FileListInput";
import { useContext, useEffect, useState } from "react";
import { APIContext, RefreshTorrentStatsContext } from "../context";
import { useErrorStore } from "../stores/errorStore";

export const TorrentRow: React.FC<{
  id: number;
  detailsResponse: TorrentDetails | null;
  statsResponse: TorrentStats | null;
}> = ({ id, detailsResponse, statsResponse }) => {
  const state = statsResponse?.state ?? "";
  const error = statsResponse?.error ?? null;
  const totalBytes = statsResponse?.total_bytes ?? 1;
  const progressBytes = statsResponse?.progress_bytes ?? 0;
  const finished = statsResponse?.finished || false;
  const progressPercentage = error
    ? 100
    : totalBytes == 0
      ? 100
      : (progressBytes / totalBytes) * 100;

  const formatPeersString = () => {
    let peer_stats = statsResponse?.live?.snapshot.peer_stats;
    if (!peer_stats) {
      return "";
    }
    return `${peer_stats.live} / ${peer_stats.seen}`;
  };

  const statusIcon = (className: string) => {
    return (
      <StatusIcon
        className={className}
        error={!!error}
        live={!!statsResponse?.live}
        finished={finished}
      />
    );
  };

  const [selectedFiles, setSelectedFiles] = useState<Set<number>>(new Set());

  // Update selected files whenever details are updated.
  useEffect(() => {
    setSelectedFiles(
      new Set<number>(
        detailsResponse?.files
          .map((f, id) => ({ f, id }))
          .filter(({ f }) => f.included)
          .map(({ id }) => id) ?? []
      )
    );
  }, [detailsResponse]);

  const API = useContext(APIContext);

  const refreshCtx = useContext(RefreshTorrentStatsContext);

  const [savingSelectedFiles, setSavingSelectedFiles] = useState(false);

  let setCloseableError = useErrorStore((state) => state.setCloseableError);

  const updateSelectedFiles = (selectedFiles: Set<number>) => {
    setSavingSelectedFiles(true);
    API.updateOnlyFiles(id, Array.from(selectedFiles))
      .then(
        () => {
          refreshCtx.refresh();
          setCloseableError(null);
        },
        (e) => {
          setCloseableError({
            text: "Error configuring torrent",
            details: e as ErrorDetails,
          });
        }
      )
      .finally(() => setSavingSelectedFiles(false));
  };

  const [extendedView, setExtendedView] = useState(false);

  return (
    <div className="flex flex-col border p-2 border-gray-200 rounded-xl shadow-xs hover:drop-shadow-sm dark:bg-slate-800 dark:border-slate-900">
      <section className="flex flex-col lg:flex-row items-center gap-2">
        {/* Icon */}
        <div className="hidden md:block">{statusIcon("w-10 h-10")}</div>
        {/* Name, progress, stats */}
        <div className="w-full flex flex-col gap-2">
          {detailsResponse && (
            <div className="flex items-center gap-2">
              <div className="md:hidden">{statusIcon("w-5 h-5")}</div>
              <div className="text-left text-sm lg:text-lg text-gray-900 text-ellipsis break-all dark:text-slate-200">
                {torrentDisplayName(detailsResponse)}
              </div>
            </div>
          )}
          {error ? (
            <p className="text-red-500 text-sm">
              <strong>Error:</strong> {error}
            </p>
          ) : (
            <>
              <div>
                <ProgressBar
                  now={progressPercentage}
                  label={error}
                  variant={
                    state == STATE_INITIALIZING
                      ? "warn"
                      : finished
                        ? "success"
                        : "info"
                  }
                />
              </div>
              <div className="grid grid-cols-2 lg:grid-cols-4 gap-2 sm:flex-wrap items-center text-sm text-nowrap font-medium text-gray-500">
                <div className="flex gap-2 items-center">
                  <GoPeople /> {formatPeersString().toString()}
                </div>
                <div className="flex gap-2 items-center">
                  <GoFile />
                  <div>
                    {formatBytes(progressBytes)}/{formatBytes(totalBytes)}
                  </div>
                </div>
                {statsResponse && (
                  <>
                    <div className="flex gap-2 items-center">
                      <GoClock />
                      {getCompletionETA(statsResponse)}
                    </div>
                    <div className="flex gap-2 items-center">
                      <Speed statsResponse={statsResponse} />
                    </div>
                  </>
                )}
              </div>
            </>
          )}
        </div>
        {/* Actions */}
        {statsResponse && (
          <div className="">
            <TorrentActions
              id={id}
              statsResponse={statsResponse}
              extendedView={extendedView}
              setExtendedView={setExtendedView}
            />
          </div>
        )}
      </section>

      {/* extended view */}
      {detailsResponse && extendedView && (
        <div className="">
          <FileListInput
            torrentId={id}
            torrentDetails={detailsResponse}
            torrentStats={statsResponse}
            selectedFiles={selectedFiles}
            setSelectedFiles={updateSelectedFiles}
            disabled={savingSelectedFiles}
            allowStream
            showProgressBar
          />
        </div>
      )}
    </div>
  );
};