diff --git a/src/renderer/src/pages/game-details/seeders-and-peers/seeders-and-peers-skeleton.tsx b/src/renderer/src/pages/game-details/seeders-and-peers/seeders-and-peers-skeleton.tsx
new file mode 100644
index 00000000..dc0f7381
--- /dev/null
+++ b/src/renderer/src/pages/game-details/seeders-and-peers/seeders-and-peers-skeleton.tsx
@@ -0,0 +1,20 @@
+import Skeleton from "react-loading-skeleton";
+
+export function SeedersAndPeersSkeleton() {
+ return (
+
+
+
+
+ );
+}
diff --git a/src/renderer/src/pages/game-details/seeders-and-peers/seeders-and-peers.tsx b/src/renderer/src/pages/game-details/seeders-and-peers/seeders-and-peers.tsx
new file mode 100644
index 00000000..5244d8e4
--- /dev/null
+++ b/src/renderer/src/pages/game-details/seeders-and-peers/seeders-and-peers.tsx
@@ -0,0 +1,54 @@
+import { GameRepack } from "@types";
+import { Sprout, Users } from "lucide-react";
+
+import { useMagnetData } from "./useMagnetData";
+import { Tooltip } from "@renderer/components/tooltip/tooltip";
+import { SeedersAndPeersSkeleton } from "./seeders-and-peers-skeleton";
+import { vars } from "@renderer/theme.css";
+
+interface SeedersAndPeersProps {
+ repack: GameRepack;
+}
+
+export function SeedersAndPeers({ repack }: Readonly) {
+ const { magnetData, isLoading, error } = useMagnetData(repack.magnet);
+
+ if (isLoading) {
+ return ;
+ }
+
+ if (error) {
+ return null;
+ }
+
+ return (
+
+
+
+
+ {magnetData?.seeders}
+
+
+
+
+
+ {magnetData?.peers}
+
+
+
+ );
+}
diff --git a/src/renderer/src/pages/game-details/seeders-and-peers/types.ts b/src/renderer/src/pages/game-details/seeders-and-peers/types.ts
new file mode 100644
index 00000000..23532807
--- /dev/null
+++ b/src/renderer/src/pages/game-details/seeders-and-peers/types.ts
@@ -0,0 +1,5 @@
+export type TorrentData = {
+ seeders: number;
+ peers: number;
+ lastTracked?: Date;
+};
diff --git a/src/renderer/src/pages/game-details/seeders-and-peers/useMagnetData.tsx b/src/renderer/src/pages/game-details/seeders-and-peers/useMagnetData.tsx
new file mode 100644
index 00000000..b072ac22
--- /dev/null
+++ b/src/renderer/src/pages/game-details/seeders-and-peers/useMagnetData.tsx
@@ -0,0 +1,75 @@
+import { useCallback, useEffect, useState } from "react";
+import { TorrentData } from "./types";
+
+const cache: Record = {};
+
+export function useMagnetData(magnet: string) {
+ const [magnetData, setMagnetData] = useState(cache[magnet] || null);
+ const [isLoading, setIsLoading] = useState(() => {
+ if (cache[magnet]) {
+ return false;
+ }
+
+ return true;
+ });
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ if (!magnet) {
+ return;
+ }
+
+ if (cache[magnet]) {
+ setMagnetData(cache[magnet]);
+ setIsLoading(false);
+ return;
+ }
+
+ window.electron.getMagnetData(magnet).then(
+ (result) => {
+ if (result) {
+ setMagnetData(result);
+ setIsLoading(false);
+
+ cache[magnet] = result;
+ cache[magnet].lastTracked = new Date();
+ }
+ },
+ (error) => {
+ setError(error);
+ setIsLoading(false);
+ }
+ );
+ }, []);
+
+ const invalidateCache = useCallback(() => {
+ const TWO_MINUTES = 2 * 60 * 1000;
+ const cacheExpiresIn = TWO_MINUTES;
+
+ Object.keys(cache).forEach((key) => {
+ const lastTracked = cache[key].lastTracked;
+
+ if (!lastTracked) {
+ return;
+ }
+
+ if (Date.now() - lastTracked.getTime() > cacheExpiresIn) {
+ delete cache[key];
+ }
+ });
+ }, []);
+
+ useEffect(() => {
+ invalidateCache();
+
+ return () => {
+ invalidateCache();
+ }
+ }, []);
+
+ return {
+ magnetData,
+ isLoading,
+ error,
+ };
+}