Replace entire codebase with homarr-labs/homarr

This commit is contained in:
Thomas Camlong
2026-01-15 21:54:44 +01:00
parent c5bc3b1559
commit 4fdd1fe351
4666 changed files with 409577 additions and 147434 deletions

View File

@@ -0,0 +1,175 @@
import type { StartedTestContainer } from "testcontainers";
import { GenericContainer, getContainerRuntimeClient, ImageName, Wait } from "testcontainers";
import { beforeAll, describe, expect, test, vi } from "vitest";
import { createDb } from "@homarr/db/test";
import { Aria2Integration } from "../src";
vi.mock("@homarr/db", async (importActual) => {
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
const actual = await importActual<typeof import("@homarr/db")>();
return {
...actual,
db: createDb(),
};
});
vi.mock("@homarr/core/infrastructure/certificates", async (importActual) => {
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
const actual = await importActual<typeof import("@homarr/core/infrastructure/certificates")>();
return {
...actual,
getTrustedCertificateHostnamesAsync: vi.fn().mockImplementation(() => {
return Promise.resolve([]);
}),
};
});
const API_KEY = "ARIA2_API_KEY";
const IMAGE_NAME = "hurlenko/aria2-ariang:latest";
describe("Aria2 integration", () => {
beforeAll(async () => {
const containerRuntimeClient = await getContainerRuntimeClient();
await containerRuntimeClient.image.pull(ImageName.fromString(IMAGE_NAME));
}, 100_000);
test("Test connection should work", async () => {
// Arrange
const startedContainer = await createAria2Container().start();
const aria2Integration = createAria2Intergration(startedContainer, API_KEY);
// Act
const result = await aria2Integration.testConnectionAsync();
// Assert
expect(result.success).toBe(true);
// Cleanup
await startedContainer.stop();
}, 30_000);
test("pauseQueueAsync should work", async () => {
// Arrange
const startedContainer = await createAria2Container().start();
const aria2Integration = createAria2Intergration(startedContainer, API_KEY);
// Acts
const actAsync = async () => await aria2Integration.pauseQueueAsync();
const getAsync = async () => await aria2Integration.getClientJobsAndStatusAsync({ limit: 99 });
// Assert
await expect(actAsync()).resolves.not.toThrow();
await expect(getAsync()).resolves.toMatchObject({ status: { paused: true } });
// Cleanup
await startedContainer.stop();
}, 30_000); // Timeout of 30 seconds
test("Items should be empty", async () => {
// Arrange
const startedContainer = await createAria2Container().start();
const aria2Integration = createAria2Intergration(startedContainer, API_KEY);
// Act
const getAsync = async () => await aria2Integration.getClientJobsAndStatusAsync({ limit: 99 });
// Assert
await expect(getAsync()).resolves.not.toThrow();
await expect(getAsync()).resolves.toMatchObject({
items: [],
});
// Cleanup
await startedContainer.stop();
}, 30_000); // Timeout of 30 seconds
test("1 Items should exist after adding one", async () => {
// Arrange
const startedContainer = await createAria2Container().start();
const aria2Integration = createAria2Intergration(startedContainer, API_KEY);
await aria2AddItemAsync(startedContainer, API_KEY, aria2Integration);
// Act
const getAsync = async () => await aria2Integration.getClientJobsAndStatusAsync({ limit: 99 });
// Assert
await expect(getAsync()).resolves.not.toThrow();
expect((await getAsync()).items).toHaveLength(1);
// Cleanup
await startedContainer.stop();
}, 30_000); // Timeout of 30 seconds
test("Delete item should result in empty items", async () => {
// Arrange
const startedContainer = await createAria2Container().start();
const aria2Integration = createAria2Intergration(startedContainer, API_KEY);
const item = await aria2AddItemAsync(startedContainer, API_KEY, aria2Integration);
// Act
const actAsync = async () => await aria2Integration.deleteItemAsync(item, true);
// Assert
await expect(actAsync()).resolves.not.toThrow();
// NzbGet is slow and we wait for a second before querying the items. Test was flaky without this.
await new Promise((resolve) => setTimeout(resolve, 1000));
const result = await aria2Integration.getClientJobsAndStatusAsync({ limit: 99 });
expect(result.items).toHaveLength(0);
// Cleanup
await startedContainer.stop();
}, 30_000); // Timeout of 30 seconds
});
const createAria2Container = () => {
return new GenericContainer(IMAGE_NAME)
.withExposedPorts(8080)
.withEnvironment({
PUID: "1000",
PGID: "1000",
ARIA2RPCPORT: "443",
RPC_SECRET: API_KEY,
})
.withWaitStrategy(Wait.forLogMessage("listening on TCP port"));
};
const createAria2Intergration = (container: StartedTestContainer, apikey: string) => {
return new Aria2Integration({
id: "1",
decryptedSecrets: [
{
kind: "apiKey",
value: apikey,
},
],
name: "Aria2",
url: `http://${container.getHost()}:${container.getMappedPort(8080)}`,
externalUrl: null,
});
};
const aria2AddItemAsync = async (container: StartedTestContainer, apiKey: string, integration: Aria2Integration) => {
await fetch(`http://${container.getHost()}:${container.getMappedPort(8080)}/jsonrpc`, {
method: "POST",
body: JSON.stringify({
jsonrpc: "2.0",
id: btoa(["Homarr", Date.now().toString(), Math.random()].join(".")), // unique id per request
method: "aria2.addUri",
params: [`token:${apiKey}`, ["https://google.com"]],
}),
});
await delay(3000);
const {
items: [item],
} = await integration.getClientJobsAndStatusAsync({ limit: 99 });
if (!item) {
throw new Error("No item found");
}
return item;
};
const delay = (microseconds: number) => new Promise((resolve) => setTimeout(resolve, microseconds));

View File

@@ -0,0 +1,69 @@
import { describe, expect, test, vi } from "vitest";
import { ResponseError } from "@homarr/common/server";
import { createDb } from "@homarr/db/test";
import type { IntegrationTestingInput } from "../src/base/integration";
import { Integration } from "../src/base/integration";
import type { TestingResult } from "../src/base/test-connection/test-connection-service";
vi.mock("@homarr/db", async (importActual) => {
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
const actual = await importActual<typeof import("@homarr/db")>();
return {
...actual,
db: createDb(),
};
});
vi.mock("@homarr/core/infrastructure/certificates", async (importActual) => {
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
const actual = await importActual<typeof import("@homarr/core/infrastructure/certificates")>();
return {
...actual,
getTrustedCertificateHostnamesAsync: vi.fn().mockImplementation(() => {
return Promise.resolve([]);
}),
};
});
describe("Base integration", () => {
test("testConnectionAsync should handle errors", async () => {
const responseError = new ResponseError({ status: 500, url: "https://example.com" });
const integration = new FakeIntegration(undefined, responseError);
const result = await integration.testConnectionAsync();
expect(result.success).toBe(false);
if (result.success) return;
expect(result.error.type === "statusCode").toBe(true);
if (result.error.type !== "statusCode") return;
expect(result.error.data.statusCode).toBe(500);
expect(result.error.data.url).toContain("https://example.com");
expect(result.error.data.reason).toBe("internalServerError");
});
});
class FakeIntegration extends Integration {
constructor(
private testingResult?: TestingResult,
private error?: Error,
) {
super({
id: "test",
name: "Test",
url: "https://example.com",
decryptedSecrets: [],
externalUrl: null,
});
}
// eslint-disable-next-line no-restricted-syntax
protected testingAsync(_: IntegrationTestingInput): Promise<TestingResult> {
if (this.error) {
return Promise.reject(this.error);
}
return Promise.resolve(this.testingResult ?? { success: true });
}
}

View File

@@ -0,0 +1,113 @@
import { join } from "path";
import type { StartedTestContainer } from "testcontainers";
import { GenericContainer, getContainerRuntimeClient, ImageName, Wait } from "testcontainers";
import { beforeAll, describe, expect, test, vi } from "vitest";
import { createDb } from "@homarr/db/test";
import { HomeAssistantIntegration } from "../src";
import { TestConnectionError } from "../src/base/test-connection/test-connection-error";
vi.mock("@homarr/db", async (importActual) => {
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
const actual = await importActual<typeof import("@homarr/db")>();
return {
...actual,
db: createDb(),
};
});
vi.mock("@homarr/core/infrastructure/certificates", async (importActual) => {
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
const actual = await importActual<typeof import("@homarr/core/infrastructure/certificates")>();
return {
...actual,
getTrustedCertificateHostnamesAsync: vi.fn().mockImplementation(() => {
return Promise.resolve([]);
}),
};
});
const DEFAULT_API_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkNjQwY2VjNDFjOGU0NGM5YmRlNWQ4ZmFjMjUzYWViZiIsImlhdCI6MTcxODQ3MTE1MSwiZXhwIjoyMDMzODMxMTUxfQ.uQCZ5FZTokipa6N27DtFhLHkwYEXU1LZr0fsVTryL2Q";
const IMAGE_NAME = "ghcr.io/home-assistant/home-assistant:stable";
describe("Home Assistant integration", () => {
beforeAll(async () => {
const containerRuntimeClient = await getContainerRuntimeClient();
await containerRuntimeClient.image.pull(ImageName.fromString(IMAGE_NAME));
}, 100_000);
test("Test connection should work", async () => {
// Arrange
const startedContainer = await prepareHomeAssistantContainerAsync();
const homeAssistantIntegration = createHomeAssistantIntegration(startedContainer);
// Act
const result = await homeAssistantIntegration.testConnectionAsync();
// Assert
expect(result.success).toBe(true);
// Cleanup
await startedContainer.stop();
}, 30_000); // Timeout of 30 seconds
test("Test connection should fail with wrong credentials", async () => {
// Arrange
const startedContainer = await prepareHomeAssistantContainerAsync();
const homeAssistantIntegration = createHomeAssistantIntegration(startedContainer, "wrong-api-key");
// Act
const result = await homeAssistantIntegration.testConnectionAsync();
// Assert
expect(result.success).toBe(false);
if (result.success) return;
expect(result.error).toBeInstanceOf(TestConnectionError);
expect(result.error.type).toBe("authorization");
// Cleanup
await startedContainer.stop();
}, 30_000); // Timeout of 30 seconds
});
const prepareHomeAssistantContainerAsync = async () => {
const homeAssistantContainer = createHomeAssistantContainer();
const startedContainer = await homeAssistantContainer.start();
await startedContainer.exec(["unzip", "-o", "/tmp/config.zip", "-d", "/config"]);
await startedContainer.restart();
return startedContainer;
};
const createHomeAssistantContainer = () => {
return (
new GenericContainer(IMAGE_NAME)
.withCopyFilesToContainer([
{
source: join(__dirname, "/volumes/home-assistant-config.zip"),
target: "/tmp/config.zip",
},
])
.withPrivilegedMode()
.withExposedPorts(8123)
// This has to be a page that is not redirected (or a status code has to be defined withStatusCode(statusCode))
.withWaitStrategy(Wait.forHttp("/onboarding.html", 8123))
);
};
const createHomeAssistantIntegration = (container: StartedTestContainer, apiKeyOverride?: string) => {
console.log("Creating Home Assistant integration...");
return new HomeAssistantIntegration({
id: "1",
decryptedSecrets: [
{
kind: "apiKey",
value: apiKeyOverride ?? DEFAULT_API_KEY,
},
],
name: "Home assistant",
url: `http://${container.getHost()}:${container.getMappedPort(8123)}`,
externalUrl: null,
});
};

View File

@@ -0,0 +1,231 @@
import { readFile } from "fs/promises";
import { join } from "path";
import type { StartedTestContainer } from "testcontainers";
import { GenericContainer, getContainerRuntimeClient, ImageName, Wait } from "testcontainers";
import { beforeAll, describe, expect, test, vi } from "vitest";
import { createDb } from "@homarr/db/test";
import { NzbGetIntegration } from "../src";
import { TestConnectionError } from "../src/base/test-connection/test-connection-error";
vi.mock("@homarr/db", async (importActual) => {
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
const actual = await importActual<typeof import("@homarr/db")>();
return {
...actual,
db: createDb(),
};
});
vi.mock("@homarr/core/infrastructure/certificates", async (importActual) => {
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
const actual = await importActual<typeof import("@homarr/core/infrastructure/certificates")>();
return {
...actual,
getTrustedCertificateHostnamesAsync: vi.fn().mockImplementation(() => {
return Promise.resolve([]);
}),
};
});
const username = "nzbget";
const password = "tegbzn6789";
const IMAGE_NAME = "linuxserver/nzbget:latest";
describe("Nzbget integration", () => {
beforeAll(async () => {
const containerRuntimeClient = await getContainerRuntimeClient();
await containerRuntimeClient.image.pull(ImageName.fromString(IMAGE_NAME));
}, 100_000);
test("Test connection should work", async () => {
// Arrange
const startedContainer = await createNzbGetContainer().start();
const nzbGetIntegration = createNzbGetIntegration(startedContainer, username, password);
// Act
const result = await nzbGetIntegration.testConnectionAsync();
// Assert
expect(result.success).toBe(true);
// Cleanup
await startedContainer.stop();
}, 30_000);
test("Test connection should fail with wrong credentials", async () => {
// Arrange
const startedContainer = await createNzbGetContainer().start();
const nzbGetIntegration = createNzbGetIntegration(startedContainer, "wrong-user", "wrong-password");
// Act
const result = await nzbGetIntegration.testConnectionAsync();
// Assert
expect(result.success).toBe(false);
if (result.success) return;
expect(result.error).toBeInstanceOf(TestConnectionError);
expect(result.error.type).toBe("authorization");
// Cleanup
await startedContainer.stop();
}, 30_000); // Timeout of 30 seconds
test("pauseQueueAsync should work", async () => {
// Arrange
const startedContainer = await createNzbGetContainer().start();
const nzbGetIntegration = createNzbGetIntegration(startedContainer, username, password);
// Acts
const actAsync = async () => await nzbGetIntegration.pauseQueueAsync();
const getAsync = async () => await nzbGetIntegration.getClientJobsAndStatusAsync({ limit: 99 });
// Assert
await expect(actAsync()).resolves.not.toThrow();
await expect(getAsync()).resolves.toMatchObject({ status: { paused: true } });
// Cleanup
await startedContainer.stop();
}, 30_000); // Timeout of 30 seconds
test("resumeQueueAsync should work", async () => {
// Arrange
const startedContainer = await createNzbGetContainer().start();
const nzbGetIntegration = createNzbGetIntegration(startedContainer, username, password);
await nzbGetIntegration.pauseQueueAsync();
// Acts
const actAsync = async () => await nzbGetIntegration.resumeQueueAsync();
const getAsync = async () => await nzbGetIntegration.getClientJobsAndStatusAsync({ limit: 99 });
// Assert
await expect(actAsync()).resolves.not.toThrow();
await expect(getAsync()).resolves.toMatchObject({
status: { paused: false },
});
// Cleanup
await startedContainer.stop();
}, 30_000); // Timeout of 30 seconds
test("Items should be empty", async () => {
// Arrange
const startedContainer = await createNzbGetContainer().start();
const nzbGetIntegration = createNzbGetIntegration(startedContainer, username, password);
// Act
const getAsync = async () => await nzbGetIntegration.getClientJobsAndStatusAsync({ limit: 99 });
// Assert
await expect(getAsync()).resolves.not.toThrow();
await expect(getAsync()).resolves.toMatchObject({
items: [],
});
// Cleanup
await startedContainer.stop();
}, 30_000); // Timeout of 30 seconds
test("1 Items should exist after adding one", async () => {
// Arrange
const startedContainer = await createNzbGetContainer().start();
const nzbGetIntegration = createNzbGetIntegration(startedContainer, username, password);
await nzbGetAddItemAsync(startedContainer, username, password, nzbGetIntegration);
// Act
const getAsync = async () => await nzbGetIntegration.getClientJobsAndStatusAsync({ limit: 99 });
// Assert
await expect(getAsync()).resolves.not.toThrow();
expect((await getAsync()).items).toHaveLength(1);
// Cleanup
await startedContainer.stop();
}, 30_000); // Timeout of 30 seconds
test("Delete item should result in empty items", async () => {
// Arrange
const startedContainer = await createNzbGetContainer().start();
const nzbGetIntegration = createNzbGetIntegration(startedContainer, username, password);
const item = await nzbGetAddItemAsync(startedContainer, username, password, nzbGetIntegration);
// Act
const actAsync = async () => await nzbGetIntegration.deleteItemAsync(item, true);
// Assert
await expect(actAsync()).resolves.not.toThrow();
// NzbGet is slow and we wait for a few seconds before querying the items. Test was flaky without this.
await new Promise((resolve) => setTimeout(resolve, 5000));
const result = await nzbGetIntegration.getClientJobsAndStatusAsync({ limit: 99 });
expect(result.items).toHaveLength(0);
// Cleanup
await startedContainer.stop();
}, 30_000); // Timeout of 30 seconds
});
const createNzbGetContainer = () => {
return new GenericContainer(IMAGE_NAME)
.withExposedPorts(6789)
.withEnvironment({ PUID: "0", PGID: "0" })
.withWaitStrategy(Wait.forLogMessage("[ls.io-init] done."));
};
const createNzbGetIntegration = (container: StartedTestContainer, username: string, password: string) => {
return new NzbGetIntegration({
id: "1",
decryptedSecrets: [
{
kind: "username",
value: username,
},
{
kind: "password",
value: password,
},
],
name: "NzbGet",
url: `http://${container.getHost()}:${container.getMappedPort(6789)}`,
externalUrl: null,
});
};
const nzbGetAddItemAsync = async (
container: StartedTestContainer,
username: string,
password: string,
integration: NzbGetIntegration,
) => {
const fileContent = await readFile(join(__dirname, "/volumes/usenet/test_download_100MB.nzb"), "base64");
// Trigger scanning of the watch folder (Only available way to add an item except "append" which is too complex and unnecessary)
await fetch(`http://${container.getHost()}:${container.getMappedPort(6789)}/${username}:${password}/jsonrpc`, {
method: "POST",
body: JSON.stringify({
method: "append",
params: [
"/downloads/nzb/test_download_100MB.nzb", // NZBFilename
fileContent, // Content
"", // Category
0, // Priority
true, // AddToTop
false, // Paused
"random", // DupeKey
1000, // DupeScore
"all", // DupeMode
[], // PPParameters
],
}),
});
const {
items: [item],
} = await integration.getClientJobsAndStatusAsync({ limit: 99 });
if (!item) {
throw new Error("No item found");
}
return item;
};

View File

@@ -0,0 +1,250 @@
import type { StartedTestContainer } from "testcontainers";
import { GenericContainer, Wait } from "testcontainers";
import { describe, expect, test, vi } from "vitest";
import { createDb } from "@homarr/db/test";
import { PiHoleIntegrationV5, PiHoleIntegrationV6 } from "../src";
import type { SessionStore } from "../src/base/session-store";
import { TestConnectionError } from "../src/base/test-connection/test-connection-error";
vi.mock("@homarr/db", async (importActual) => {
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
const actual = await importActual<typeof import("@homarr/db")>();
return {
...actual,
db: createDb(),
};
});
vi.mock("@homarr/core/infrastructure/certificates", async (importActual) => {
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
const actual = await importActual<typeof import("@homarr/core/infrastructure/certificates")>();
return {
...actual,
getTrustedCertificateHostnamesAsync: vi.fn().mockImplementation(() => {
return Promise.resolve([]);
}),
};
});
const DEFAULT_PASSWORD = "12341234";
const DEFAULT_API_KEY = "3b1434980677dcf53fa8c4a611db3b1f0f88478790097515c0abb539102778b9"; // Some hash generated from password
describe("Pi-hole v5 integration", () => {
test("getSummaryAsync should return summary from pi-hole", async () => {
// Arrange
const piholeContainer = await createPiHoleV5Container(DEFAULT_PASSWORD).start();
const piHoleIntegration = createPiHoleIntegrationV5(piholeContainer, DEFAULT_API_KEY);
// Act
const result = await piHoleIntegration.getSummaryAsync();
// Assert
expect(result.adsBlockedToday).toBe(0);
expect(result.adsBlockedTodayPercentage).toBe(0);
expect(result.dnsQueriesToday).toBe(0);
expect(result.domainsBeingBlocked).toBeGreaterThan(1);
// Cleanup
await piholeContainer.stop();
}, 20_000); // Timeout of 20 seconds
test("testConnectionAsync should be successful", async () => {
// Arrange
const piholeContainer = await createPiHoleV5Container(DEFAULT_PASSWORD).start();
const piHoleIntegration = createPiHoleIntegrationV5(piholeContainer, DEFAULT_API_KEY);
// Act
const result = await piHoleIntegration.testConnectionAsync();
// Assert
expect(result.success).toBe(true);
// Cleanup
await piholeContainer.stop();
}, 20_000); // Timeout of 20 seconds
test("testConnectionAsync should fail with unauthorized for wrong credentials", async () => {
// Arrange
const piholeContainer = await createPiHoleV5Container(DEFAULT_PASSWORD).start();
const piHoleIntegration = createPiHoleIntegrationV5(piholeContainer, "wrong-api-key");
// Act
const result = await piHoleIntegration.testConnectionAsync();
// Assert
expect(result.success).toBe(false);
if (result.success) return;
expect(result.error).toBeInstanceOf(TestConnectionError);
expect(result.error.type).toBe("authorization");
// Cleanup
await piholeContainer.stop();
}, 20_000); // Timeout of 20 seconds
});
vi.mock("../src/base/session-store", () => ({
createSessionStore: () =>
({
async getAsync() {
return await Promise.resolve(null);
},
async setAsync() {
return await Promise.resolve();
},
async clearAsync() {
return await Promise.resolve();
},
}) satisfies SessionStore<string>,
}));
describe("Pi-hole v6 integration", () => {
test("getSummaryAsync should return summary from pi-hole", async () => {
// Arrange
const piholeContainer = await createPiHoleV6Container(DEFAULT_PASSWORD).start();
const piHoleIntegration = createPiHoleIntegrationV6(piholeContainer, DEFAULT_PASSWORD);
// Act
const result = await piHoleIntegration.getSummaryAsync();
// Assert
expect(result.status).toBe("enabled");
expect(result.adsBlockedToday).toBe(0);
expect(result.adsBlockedTodayPercentage).toBe(0);
expect(result.dnsQueriesToday).toBe(0);
expect(result.domainsBeingBlocked).toBeGreaterThanOrEqual(0);
// Cleanup
await piholeContainer.stop();
}, 20_000); // Timeout of 20 seconds
test("enableAsync should enable pi-hole", async () => {
// Arrange
const piholeContainer = await createPiHoleV6Container(DEFAULT_PASSWORD).start();
const piHoleIntegration = createPiHoleIntegrationV6(piholeContainer, DEFAULT_PASSWORD);
// Disable pi-hole
await piholeContainer.exec(["pihole", "disable"]);
// Act
await piHoleIntegration.enableAsync();
// Assert
const status = await piHoleIntegration.getDnsBlockingStatusAsync();
expect(status.blocking).toContain("enabled");
}, 20_000); // Timeout of 20 seconds
test("disableAsync should disable pi-hole", async () => {
// Arrange
const piholeContainer = await createPiHoleV6Container(DEFAULT_PASSWORD).start();
const piHoleIntegration = createPiHoleIntegrationV6(piholeContainer, DEFAULT_PASSWORD);
// Act
await piHoleIntegration.disableAsync();
// Assert
const status = await piHoleIntegration.getDnsBlockingStatusAsync();
expect(status.blocking).toBe("disabled");
expect(status.timer).toBe(null);
}, 20_000); // Timeout of 20 seconds
test("disableAsync should disable pi-hole with timer", async () => {
// Arrange
const timer = 10 * 60; // 10 minutes
const piholeContainer = await createPiHoleV6Container(DEFAULT_PASSWORD).start();
const piHoleIntegration = createPiHoleIntegrationV6(piholeContainer, DEFAULT_PASSWORD);
// Act
await piHoleIntegration.disableAsync(timer);
// Assert
const status = await piHoleIntegration.getDnsBlockingStatusAsync();
expect(status.blocking).toBe("disabled");
expect(status.timer).toBeGreaterThan(timer - 10);
}, 20_000); // Timeout of 20 seconds
test("testConnectionAsync should be successful", async () => {
// Arrange
const piholeContainer = await createPiHoleV6Container(DEFAULT_PASSWORD).start();
const piHoleIntegration = createPiHoleIntegrationV6(piholeContainer, DEFAULT_PASSWORD);
// Act
const result = await piHoleIntegration.testConnectionAsync();
// Assert
expect(result.success).toBe(true);
// Cleanup
await piholeContainer.stop();
}, 20_000); // Timeout of 20 seconds
test("testConnectionAsync should fail with unauthorized for wrong credentials", async () => {
// Arrange
const piholeContainer = await createPiHoleV6Container(DEFAULT_PASSWORD).start();
const piHoleIntegration = createPiHoleIntegrationV6(piholeContainer, "wrong-api-key");
// Act
const result = await piHoleIntegration.testConnectionAsync();
// Assert
expect(result.success).toBe(false);
if (result.success) return;
expect(result.error).toBeInstanceOf(TestConnectionError);
expect(result.error.type).toBe("authorization");
// Cleanup
await piholeContainer.stop();
}, 20_000); // Timeout of 20 seconds
});
const createPiHoleV5Container = (password: string) => {
return new GenericContainer("pihole/pihole:2024.07.0") // v5
.withEnvironment({
WEBPASSWORD: password,
})
.withExposedPorts(80)
.withWaitStrategy(Wait.forLogMessage("Pi-hole Enabled"));
};
const createPiHoleIntegrationV5 = (container: StartedTestContainer, apiKey: string) => {
return new PiHoleIntegrationV5({
id: "1",
decryptedSecrets: [
{
kind: "apiKey",
value: apiKey,
},
],
name: "Pi hole",
url: `http://${container.getHost()}:${container.getMappedPort(80)}`,
externalUrl: null,
});
};
const createPiHoleV6Container = (password: string) => {
return (
new GenericContainer("pihole/pihole:latest")
.withEnvironment({
FTLCONF_webserver_api_password: password,
})
.withExposedPorts(80)
// This has to be a page that is not redirected (or a status code has to be defined withStatusCode(statusCode))
.withWaitStrategy(Wait.forHttp("/admin/login", 80))
);
};
const createPiHoleIntegrationV6 = (container: StartedTestContainer, apiKey: string) => {
return new PiHoleIntegrationV6({
id: "1",
decryptedSecrets: [
{
kind: "apiKey",
value: apiKey,
},
],
name: "Pi hole",
url: `http://${container.getHost()}:${container.getMappedPort(80)}`,
externalUrl: null,
});
};

View File

@@ -0,0 +1,265 @@
import { join } from "path";
import { GenericContainer, getContainerRuntimeClient, ImageName, Wait } from "testcontainers";
import type { StartedTestContainer } from "testcontainers";
import { beforeAll, describe, expect, test, vi } from "vitest";
import { createDb } from "@homarr/db/test";
import { SabnzbdIntegration } from "../src";
import { TestConnectionError } from "../src/base/test-connection/test-connection-error";
import type { DownloadClientItem } from "../src/interfaces/downloads/download-client-items";
vi.mock("@homarr/db", async (importActual) => {
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
const actual = await importActual<typeof import("@homarr/db")>();
return {
...actual,
db: createDb(),
};
});
vi.mock("@homarr/core/infrastructure/certificates", async (importActual) => {
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
const actual = await importActual<typeof import("@homarr/core/infrastructure/certificates")>();
return {
...actual,
getTrustedCertificateHostnamesAsync: vi.fn().mockImplementation(() => {
return Promise.resolve([]);
}),
};
});
const DEFAULT_API_KEY = "8r45mfes43s3iw7x3oecto6dl9ilxnf9";
const IMAGE_NAME = "linuxserver/sabnzbd:latest";
describe("Sabnzbd integration", () => {
beforeAll(async () => {
const containerRuntimeClient = await getContainerRuntimeClient();
await containerRuntimeClient.image.pull(ImageName.fromString(IMAGE_NAME));
}, 100_000);
test("Test connection should work", async () => {
// Arrange
const startedContainer = await createSabnzbdContainer().start();
const sabnzbdIntegration = createSabnzbdIntegration(startedContainer, DEFAULT_API_KEY);
// Act
const result = await sabnzbdIntegration.testConnectionAsync();
// Assert
expect(result.success).toBe(true);
// Cleanup
await startedContainer.stop();
}, 30_000); // Timeout of 30 seconds
test("Test connection should fail with wrong ApiKey", async () => {
// Arrange
const startedContainer = await createSabnzbdContainer().start();
const sabnzbdIntegration = createSabnzbdIntegration(startedContainer, "wrong-api-key");
// Act
const result = await sabnzbdIntegration.testConnectionAsync();
// Assert
expect(result.success).toBe(false);
if (result.success) return;
expect(result.error).toBeInstanceOf(TestConnectionError);
expect(result.error.type).toBe("authorization");
// Cleanup
await startedContainer.stop();
}, 30_000); // Timeout of 30 seconds
test("pauseQueueAsync should work", async () => {
// Arrange
const startedContainer = await createSabnzbdContainer().start();
const sabnzbdIntegration = createSabnzbdIntegration(startedContainer, DEFAULT_API_KEY);
// Acts
const actAsync = async () => await sabnzbdIntegration.pauseQueueAsync();
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync({ limit: 99 });
// Assert
await expect(actAsync()).resolves.not.toThrow();
await expect(getAsync()).resolves.toMatchObject({ status: { paused: true } });
// Cleanup
await startedContainer.stop();
}, 30_000); // Timeout of 30 seconds
test("resumeQueueAsync should work", async () => {
// Arrange
const startedContainer = await createSabnzbdContainer().start();
const sabnzbdIntegration = createSabnzbdIntegration(startedContainer, DEFAULT_API_KEY);
await sabnzbdIntegration.pauseQueueAsync();
// Acts
const actAsync = async () => await sabnzbdIntegration.resumeQueueAsync();
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync({ limit: 99 });
// Assert
await expect(actAsync()).resolves.not.toThrow();
await expect(getAsync()).resolves.toMatchObject({
status: { paused: false },
});
// Cleanup
await startedContainer.stop();
}, 30_000); // Timeout of 30 seconds
test("Items should be empty", async () => {
// Arrange
const startedContainer = await createSabnzbdContainer().start();
const sabnzbdIntegration = createSabnzbdIntegration(startedContainer, DEFAULT_API_KEY);
// Act
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync({ limit: 99 });
// Assert
await expect(getAsync()).resolves.not.toThrow();
await expect(getAsync()).resolves.toMatchObject({
items: [],
});
// Cleanup
await startedContainer.stop();
}, 30_000); // Timeout of 30 seconds
test("1 Items should exist after adding one", async () => {
// Arrange
const startedContainer = await createSabnzbdContainer().start();
const sabnzbdIntegration = createSabnzbdIntegration(startedContainer, DEFAULT_API_KEY);
await sabNzbdAddItemAsync(startedContainer, DEFAULT_API_KEY, sabnzbdIntegration);
// Act
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync({ limit: 99 });
// Assert
await expect(getAsync()).resolves.not.toThrow();
expect((await getAsync()).items).toHaveLength(1);
// Cleanup
await startedContainer.stop();
}, 30_000); // Timeout of 30 seconds
test("Pause item should work", async () => {
// Arrange
const startedContainer = await createSabnzbdContainer().start();
const sabnzbdIntegration = createSabnzbdIntegration(startedContainer, DEFAULT_API_KEY);
const item = await sabNzbdAddItemAsync(startedContainer, DEFAULT_API_KEY, sabnzbdIntegration);
// Act
const actAsync = async () => await sabnzbdIntegration.pauseItemAsync(item);
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync({ limit: 99 });
// Assert
await expect(getAsync()).resolves.toMatchObject({ items: [{ ...item, state: "downloading" }] });
await expect(actAsync()).resolves.not.toThrow();
await expect(getAsync()).resolves.toMatchObject({ items: [{ ...item, state: "paused" }] });
// Cleanup
await startedContainer.stop();
}, 30_000); // Timeout of 30 seconds
test("Resume item should work", async () => {
// Arrange
const startedContainer = await createSabnzbdContainer().start();
const sabnzbdIntegration = createSabnzbdIntegration(startedContainer, DEFAULT_API_KEY);
const item = await sabNzbdAddItemAsync(startedContainer, DEFAULT_API_KEY, sabnzbdIntegration);
await sabnzbdIntegration.pauseItemAsync(item);
// Act
const actAsync = async () => await sabnzbdIntegration.resumeItemAsync(item);
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync({ limit: 99 });
// Assert
await expect(getAsync()).resolves.toMatchObject({ items: [{ ...item, state: "paused" }] });
await expect(actAsync()).resolves.not.toThrow();
await expect(getAsync()).resolves.toMatchObject({ items: [{ ...item, state: "downloading" }] });
// Cleanup
await startedContainer.stop();
}, 30_000); // Timeout of 30 seconds
test("Delete item should result in empty items", async () => {
// Arrange
const startedContainer = await createSabnzbdContainer().start();
const sabnzbdIntegration = createSabnzbdIntegration(startedContainer, DEFAULT_API_KEY);
const item = await sabNzbdAddItemAsync(startedContainer, DEFAULT_API_KEY, sabnzbdIntegration);
// Act - fromDisk already doesn't work for sabnzbd, so only test deletion itself.
const actAsync = async () =>
await sabnzbdIntegration.deleteItemAsync({ ...item, progress: 0 } as DownloadClientItem, false);
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync({ limit: 99 });
// Assert
await expect(actAsync()).resolves.not.toThrow();
await expect(getAsync()).resolves.toMatchObject({ items: [] });
// Cleanup
await startedContainer.stop();
}, 30_000); // Timeout of 30 seconds
});
const createSabnzbdContainer = () => {
return (
new GenericContainer(IMAGE_NAME)
.withCopyFilesToContainer([
{
source: join(__dirname, "/volumes/usenet/sabnzbd.ini"),
target: "/config/sabnzbd.ini",
},
])
.withExposedPorts(1212)
.withEnvironment({ PUID: "0", PGID: "0" })
// This has to be a page that is not redirected (or a status code has to be defined withStatusCode(statusCode))
.withWaitStrategy(Wait.forHttp("/sabnzbd/wizard/", 1212))
);
};
const createSabnzbdIntegration = (container: StartedTestContainer, apiKey: string) => {
return new SabnzbdIntegration({
id: "1",
decryptedSecrets: [
{
kind: "apiKey",
value: apiKey,
},
],
name: "Sabnzbd",
url: `http://${container.getHost()}:${container.getMappedPort(1212)}`,
externalUrl: null,
});
};
const sabNzbdAddItemAsync = async (
container: StartedTestContainer,
apiKey: string,
integration: SabnzbdIntegration,
) => {
// Add nzb file in the watch folder
await container.copyFilesToContainer([
{
source: join(__dirname, "/volumes/usenet/test_download_100MB.nzb"),
target: "/nzb/test_download_100MB.nzb",
},
]);
// Adding file is faster than triggering scan of the watch folder
// (local add: 1.4-1.6s, scan trigger: 2.5-2.7s, auto scan: 2.9-3s)
await fetch(
`http://${container.getHost()}:${container.getMappedPort(1212)}/api` +
"?mode=addlocalfile" +
"&name=%2Fnzb%2Ftest_download_100MB.nzb" +
`&apikey=${apiKey}`,
);
// Retries up to 5 times to let SabNzbd scan and process the nzb (1 retry should suffice tbh)
for (let i = 0; i < 5; i++) {
const {
items: [item],
} = await integration.getClientJobsAndStatusAsync({ limit: 99 });
if (item) return item;
}
// Throws if it can't find the item
throw new Error("No item found");
};

View File

@@ -0,0 +1,407 @@
__version__ = 19
__encoding__ = utf-8
[misc]
pre_script = None
queue_complete = ""
queue_complete_pers = 0
bandwidth_perc = 100
refresh_rate = 1
interface_settings = '{"dateFormat":"fromNow","extraQueueColumns":[],"extraHistoryColumns":[],"displayCompact":false,"displayFullWidth":false,"displayTabbed":false,"confirmDeleteQueue":true,"confirmDeleteHistory":true,"keyboardShortcuts":true}'
queue_limit = 20
config_lock = 0
sched_converted = 0
notified_new_skin = 2
direct_unpack_tested = 1
check_new_rel = 0
auto_browser = 0
language = en
enable_https_verification = 1
host = 127.0.0.1
port = 1212
https_port = 1212
username = ""
password = ""
bandwidth_max = 1125M
cache_limit = 128G
web_dir = Glitter
web_color = Auto
https_cert = server.cert
https_key = server.key
https_chain = ""
enable_https = 0
inet_exposure = 4
local_ranges = ,
api_key = 8r45mfes43s3iw7x3oecto6dl9ilxnf9
nzb_key = nc6q489idfb4fmdjh0uuqlsn4fjawrub
permissions = ""
download_dir = /temp
download_free = ""
complete_dir = /downloads
complete_free = ""
fulldisk_autoresume = 0
script_dir = ""
nzb_backup_dir = ""
admin_dir = /admin
dirscan_dir = /nzb
dirscan_speed = 1
password_file = ""
log_dir = logs
max_art_tries = 3
load_balancing = 2
top_only = 0
sfv_check = 1
quick_check_ext_ignore = nfo, sfv, srr
script_can_fail = 0
enable_recursive = 1
flat_unpack = 0
par_option = ""
pre_check = 0
nice = ""
win_process_prio = 3
ionice = ""
fail_hopeless_jobs = 1
fast_fail = 1
auto_disconnect = 1
no_dupes = 0
no_series_dupes = 0
series_propercheck = 1
pause_on_pwrar = 1
ignore_samples = 0
deobfuscate_final_filenames = 0
auto_sort = ""
direct_unpack = 1
direct_unpack_threads = 6
propagation_delay = 0
folder_rename = 1
replace_spaces = 0
replace_dots = 0
safe_postproc = 1
pause_on_post_processing = 0
sanitize_safe = 0
cleanup_list = ,
unwanted_extensions = ,
action_on_unwanted_extensions = 0
new_nzb_on_failure = 0
history_retention = ""
enable_meta = 1
quota_size = ""
quota_day = ""
quota_resume = 0
quota_period = m
rating_enable = 0
rating_host = ""
rating_api_key = ""
rating_filter_enable = 0
rating_filter_abort_audio = 0
rating_filter_abort_video = 0
rating_filter_abort_encrypted = 0
rating_filter_abort_encrypted_confirm = 0
rating_filter_abort_spam = 0
rating_filter_abort_spam_confirm = 0
rating_filter_abort_downvoted = 0
rating_filter_abort_keywords = ""
rating_filter_pause_audio = 0
rating_filter_pause_video = 0
rating_filter_pause_encrypted = 0
rating_filter_pause_encrypted_confirm = 0
rating_filter_pause_spam = 0
rating_filter_pause_spam_confirm = 0
rating_filter_pause_downvoted = 0
rating_filter_pause_keywords = ""
enable_tv_sorting = 0
tv_sort_string = ""
tv_sort_countries = 1
tv_categories = ,
enable_movie_sorting = 0
movie_sort_string = ""
movie_sort_extra = -cd%1
movie_extra_folder = 0
movie_categories = movies,
enable_date_sorting = 0
date_sort_string = ""
date_categories = tv,
schedlines = ,
rss_rate = 60
ampm = 0
replace_illegal = 1
start_paused = 0
enable_all_par = 0
enable_par_cleanup = 1
enable_unrar = 1
enable_unzip = 1
enable_7zip = 1
enable_filejoin = 1
enable_tsjoin = 1
overwrite_files = 0
ignore_unrar_dates = 0
backup_for_duplicates = 1
empty_postproc = 0
wait_for_dfolder = 0
rss_filenames = 0
api_logging = 1
html_login = 1
osx_menu = 1
osx_speed = 1
warn_dupl_jobs = 1
helpfull_warnings = 1
keep_awake = 1
win_menu = 1
allow_incomplete_nzb = 0
enable_broadcast = 1
max_art_opt = 0
ipv6_hosting = 0
fixed_ports = 1
api_warnings = 1
disable_api_key = 0
no_penalties = 0
x_frame_options = 1
require_modern_tls = 0
num_decoders = 3
rss_odd_titles = nzbindex.nl/, nzbindex.com/, nzbclub.com/
req_completion_rate = 100.2
selftest_host = self-test.sabnzbd.org
movie_rename_limit = 100M
size_limit = 0
show_sysload = 2
history_limit = 10
wait_ext_drive = 5
max_foldername_length = 246
nomedia_marker = ""
ipv6_servers = 1
url_base = /sabnzbd
host_whitelist = ,
max_url_retries = 10
downloader_sleep_time = 10
ssdp_broadcast_interval = 15
email_server = ""
email_to = ,
email_from = ""
email_account = ""
email_pwd = ""
email_endjob = 0
email_full = 0
email_dir = ""
email_rss = 0
email_cats = *,
unwanted_extensions_mode = 0
preserve_paused_state = 0
process_unpacked_par2 = 1
helpful_warnings = 1
allow_old_ssl_tls = 0
episode_rename_limit = 20M
socks5_proxy_url = ""
num_simd_decoders = 2
ext_rename_ignore = ,
sorters_converted = 1
backup_dir = ""
replace_underscores = 0
tray_icon = 1
enable_season_sorting = 1
receive_threads = 6
switchinterval = 0.005
enable_multipar = 1
verify_xff_header = 0
end_queue_script = None
no_smart_dupes = 0
dupes_propercheck = 1
history_retention_option = all
history_retention_number = 1
ipv6_staging = 0
[logging]
log_level = 1
max_log_size = 5242880
log_backups = 5
[ncenter]
ncenter_enable = 0
ncenter_cats = *,
ncenter_prio_startup = 1
ncenter_prio_download = 0
ncenter_prio_pause_resume = 0
ncenter_prio_pp = 0
ncenter_prio_complete = 1
ncenter_prio_failed = 1
ncenter_prio_disk_full = 1
ncenter_prio_new_login = 0
ncenter_prio_warning = 0
ncenter_prio_error = 0
ncenter_prio_queue_done = 1
ncenter_prio_other = 1
[acenter]
acenter_enable = 0
acenter_cats = *,
acenter_prio_startup = 0
acenter_prio_download = 0
acenter_prio_pause_resume = 0
acenter_prio_pp = 0
acenter_prio_complete = 1
acenter_prio_failed = 1
acenter_prio_disk_full = 1
acenter_prio_new_login = 0
acenter_prio_warning = 0
acenter_prio_error = 0
acenter_prio_queue_done = 1
acenter_prio_other = 1
[ntfosd]
ntfosd_enable = 1
ntfosd_cats = *,
ntfosd_prio_startup = 1
ntfosd_prio_download = 0
ntfosd_prio_pause_resume = 0
ntfosd_prio_pp = 0
ntfosd_prio_complete = 1
ntfosd_prio_failed = 1
ntfosd_prio_disk_full = 1
ntfosd_prio_new_login = 0
ntfosd_prio_warning = 0
ntfosd_prio_error = 0
ntfosd_prio_queue_done = 1
ntfosd_prio_other = 1
[prowl]
prowl_enable = 0
prowl_cats = *,
prowl_apikey = ""
prowl_prio_startup = -3
prowl_prio_download = -3
prowl_prio_pause_resume = -3
prowl_prio_pp = -3
prowl_prio_complete = 0
prowl_prio_failed = 1
prowl_prio_disk_full = 1
prowl_prio_new_login = -3
prowl_prio_warning = -3
prowl_prio_error = -3
prowl_prio_queue_done = 0
prowl_prio_other = 0
[pushover]
pushover_token = ""
pushover_userkey = ""
pushover_device = ""
pushover_emergency_expire = 3600
pushover_emergency_retry = 60
pushover_enable = 0
pushover_cats = *,
pushover_prio_startup = -3
pushover_prio_download = -2
pushover_prio_pause_resume = -2
pushover_prio_pp = -3
pushover_prio_complete = -1
pushover_prio_failed = -1
pushover_prio_disk_full = 1
pushover_prio_new_login = -3
pushover_prio_warning = 1
pushover_prio_error = 1
pushover_prio_queue_done = -1
pushover_prio_other = -1
[pushbullet]
pushbullet_enable = 0
pushbullet_cats = *,
pushbullet_apikey = ""
pushbullet_device = ""
pushbullet_prio_startup = 0
pushbullet_prio_download = 0
pushbullet_prio_pause_resume = 0
pushbullet_prio_pp = 0
pushbullet_prio_complete = 1
pushbullet_prio_failed = 1
pushbullet_prio_disk_full = 1
pushbullet_prio_new_login = 0
pushbullet_prio_warning = 0
pushbullet_prio_error = 0
pushbullet_prio_queue_done = 0
pushbullet_prio_other = 1
[nscript]
nscript_enable = 0
nscript_cats = *,
nscript_script = ""
nscript_parameters = ""
nscript_prio_startup = 1
nscript_prio_download = 0
nscript_prio_pause_resume = 0
nscript_prio_pp = 0
nscript_prio_complete = 1
nscript_prio_failed = 1
nscript_prio_disk_full = 1
nscript_prio_new_login = 0
nscript_prio_warning = 0
nscript_prio_error = 0
nscript_prio_queue_done = 1
nscript_prio_other = 1
[servers]
[categories]
[[audio]]
name = audio
order = 0
pp = ""
script = Default
dir = ""
newzbin = ""
priority = -100
[[software]]
name = software
order = 0
pp = ""
script = Default
dir = ""
newzbin = ""
priority = -100
[[books]]
name = books
order = 1
pp = ""
script = Default
dir = books
newzbin = ""
priority = -100
[[tv]]
name = tv
order = 0
pp = ""
script = Default
dir = tvshows
newzbin = ""
priority = -100
[[movies]]
name = movies
order = 0
pp = ""
script = Default
dir = movies
newzbin = ""
priority = -100
[[*]]
name = *
order = 0
pp = 3
script = Default
dir = ""
newzbin = ""
priority = 0
[rss]
[apprise]
apprise_enable = 0
apprise_cats = *,
apprise_urls = ""
apprise_target_startup = ""
apprise_target_startup_enable = 0
apprise_target_download = ""
apprise_target_download_enable = 0
apprise_target_pause_resume = ""
apprise_target_pause_resume_enable = 0
apprise_target_pp = ""
apprise_target_pp_enable = 0
apprise_target_complete = ""
apprise_target_complete_enable = 1
apprise_target_failed = ""
apprise_target_failed_enable = 1
apprise_target_disk_full = ""
apprise_target_disk_full_enable = 0
apprise_target_new_login = ""
apprise_target_new_login_enable = 1
apprise_target_warning = ""
apprise_target_warning_enable = 0
apprise_target_error = ""
apprise_target_error_enable = 0
apprise_target_queue_done = ""
apprise_target_queue_done_enable = 0
apprise_target_other = ""
apprise_target_other_enable = 1

View File

@@ -0,0 +1,317 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE nzb PUBLIC "-//newzBin//DTD NZB 1.1//EN" "http://www.newzbin.com/DTD/nzb/nzb-1.1.dtd">
<nzb xmlns="http://www.newzbin.com/DTD/2003/nzb">
<file poster="blablamannetje &lt;blabla@example.com&gt;" date="1658672638" subject="reftestnzb_100MB_a82beff8e340 [01/20] - &quot;sometestfile-100MB.part01.rar&quot; yEnc (1/15) 10485760">
<groups>
<group>alt.binaries.test</group>
</groups>
<segments>
<segment bytes="739737" number="1">EkZuHcMrVxSbBbBaSuFgVbMm-1658672638813@nyuu</segment>
<segment bytes="739573" number="2">HeTrDnAcEoFtWyJhTdPkNhPe-1658672638816@nyuu</segment>
<segment bytes="739672" number="3">OyFbRuRmVxPzXrZoKpWhJsKm-1658672638821@nyuu</segment>
<segment bytes="739807" number="4">NwNaDvOgStUoRfVbXmZxKeQf-1658672638822@nyuu</segment>
<segment bytes="739558" number="5">DpZgLkTwXvIePlNdDiCcEkXu-1658672638853@nyuu</segment>
<segment bytes="739741" number="6">HlTnIiCoXaLbOyOpXyIsMjJo-1658672638855@nyuu</segment>
<segment bytes="739665" number="7">YqLsWoYuZnHbYvCjSuZpJdQx-1658672638856@nyuu</segment>
<segment bytes="739603" number="8">TgRzBeNrGuQhTxIdLbZgGnNv-1658672638857@nyuu</segment>
<segment bytes="739514" number="9">BtUmYfDwAaSdWgRnWjKfRkMl-1658672638862@nyuu</segment>
<segment bytes="739612" number="10">EoUoUdSxYgIhVlQrPpMtHzFg-1658672638883@nyuu</segment>
<segment bytes="739650" number="11">MeEhBmZsBzSqEtZcFzLqUwMr-1658672639406@nyuu</segment>
<segment bytes="739796" number="12">VwBfZmSuHdVuUfJsUnCiKgAl-1658672639428@nyuu</segment>
<segment bytes="739593" number="13">IfJyWnIgPhKkFvEmSqQiQzSd-1658672639461@nyuu</segment>
<segment bytes="739576" number="14">LiRyAkTyOwRkVzMnJpAzJlQr-1658672639469@nyuu</segment>
<segment bytes="464856" number="15">PwMjYlAzKaMbOcWjGrNhLvNc-1658672639479@nyuu</segment>
</segments>
</file>
<file poster="blablamannetje &lt;blabla@example.com&gt;" date="1658672639" subject="reftestnzb_100MB_a82beff8e340 [02/20] - &quot;sometestfile-100MB.part02.rar&quot; yEnc (1/15) 10485760">
<groups>
<group>alt.binaries.test</group>
</groups>
<segments>
<segment bytes="739650" number="1">UtJrLhPwPvHrZjTvUjZrCpKw-1658672639558@nyuu</segment>
<segment bytes="739528" number="2">OzChWnChCwAeBsHrBvKvPcGr-1658672639883@nyuu</segment>
<segment bytes="739692" number="3">VsLoYaHzQfOmDgNdPnTzPzLx-1658672639939@nyuu</segment>
<segment bytes="739648" number="4">FkDiOcGkYxNwOgLrJaWcShKy-1658672640012@nyuu</segment>
<segment bytes="739652" number="5">OyGyQxWoHmMxHzPaRdGeMlXz-1658672640035@nyuu</segment>
<segment bytes="739643" number="6">NiFsKxNsWjCxXxQfPtNmGyOw-1658672640039@nyuu</segment>
<segment bytes="739840" number="7">JqMdEoVhTaRtAdLeYfAeCvRi-1658672640078@nyuu</segment>
<segment bytes="739454" number="8">NkOnBwJtHjBoMlUkHjHrNdGo-1658672640324@nyuu</segment>
<segment bytes="739842" number="9">QwQnXeMrZjKiJoCbPuSbMjPq-1658672640398@nyuu</segment>
<segment bytes="739521" number="10">ZiPvOsXwOjTqIjLnIrKbBdXv-1658672640437@nyuu</segment>
<segment bytes="739598" number="11">DwJyDjVzBhKxRyLxIxXqRfHp-1658672640521@nyuu</segment>
<segment bytes="739624" number="12">ArRbXcZqRaDhAgRsNmMsGeBh-1658672640545@nyuu</segment>
<segment bytes="739597" number="13">UvYkRiCmXfBoIrYxCdEjMtJw-1658672640563@nyuu</segment>
<segment bytes="739709" number="14">TqHaSdLuNlWvAaFqZlHqZzJr-1658672640762@nyuu</segment>
<segment bytes="464981" number="15">SdNiSzMzVdJkFtTtGtJyEcKi-1658672640859@nyuu</segment>
</segments>
</file>
<file poster="blablamannetje &lt;blabla@example.com&gt;" date="1658672640" subject="reftestnzb_100MB_a82beff8e340 [03/20] - &quot;sometestfile-100MB.part03.rar&quot; yEnc (1/15) 10485760">
<groups>
<group>alt.binaries.test</group>
</groups>
<segments>
<segment bytes="739513" number="1">ZpUwNcWlXaVzJeJcBjXmBqCu-1658672640910@nyuu</segment>
<segment bytes="739735" number="2">AtQtJrYjNnYdVbJsXuKvXkSc-1658672640969@nyuu</segment>
<segment bytes="739535" number="3">NzBlVfVlKtTxFaIfZxDgAfHa-1658672641009@nyuu</segment>
<segment bytes="739683" number="4">JmBgCqStEdWlXqCdWgMtRaKh-1658672641040@nyuu</segment>
<segment bytes="739677" number="5">XyLjFiRjRuWmHeVhHmIhIzTg-1658672641200@nyuu</segment>
<segment bytes="739566" number="6">IvKxDaYbZkGhMsJiZcXtMhFk-1658672641311@nyuu</segment>
<segment bytes="739678" number="7">YuGtYkBnWtRiLdMeUrRxIbId-1658672641377@nyuu</segment>
<segment bytes="739671" number="8">GcNoBzAgAhSjJbQkFyKuKbAj-1658672641454@nyuu</segment>
<segment bytes="739500" number="9">RlLtFgVnBfWgJpPwTtPoJjLf-1658672641491@nyuu</segment>
<segment bytes="739776" number="10">BxLxScLaVfDoNmDwRkMbUxPg-1658672641517@nyuu</segment>
<segment bytes="739754" number="11">LhVnUdRnVqFyUiThZyIrNfMt-1658672641649@nyuu</segment>
<segment bytes="739497" number="12">VoVyMbIvAuZnWhZoEqVuRpJd-1658672641769@nyuu</segment>
<segment bytes="739614" number="13">GnMqIqUjMgVlPsZgAuLlAtGs-1658672641851@nyuu</segment>
<segment bytes="739546" number="14">YzPbErKpMvWhVgMiNgJoRmNa-1658672641933@nyuu</segment>
<segment bytes="465012" number="15">FaSoKgLtJxPsOsCyPuWgNzEe-1658672641972@nyuu</segment>
</segments>
</file>
<file poster="blablamannetje &lt;blabla@example.com&gt;" date="1658672642" subject="reftestnzb_100MB_a82beff8e340 [04/20] - &quot;sometestfile-100MB.part04.rar&quot; yEnc (1/15) 10485760">
<groups>
<group>alt.binaries.test</group>
</groups>
<segments>
<segment bytes="739583" number="1">PoGtRtIyThRwHgJbSrJhXuNq-1658672642009@nyuu</segment>
<segment bytes="739617" number="2">CnRbGgFpMxKxKyKtMmZbSuSa-1658672642085@nyuu</segment>
<segment bytes="739565" number="3">NrSeRjXxKaBqWnZaDzMiAmBi-1658672642217@nyuu</segment>
<segment bytes="739533" number="4">WwMyBrUwInZkYjUfIsDkLkEr-1658672642300@nyuu</segment>
<segment bytes="739641" number="5">DxRtSoVyQrChKqSySdPoDvGn-1658672642384@nyuu</segment>
<segment bytes="739722" number="6">SaMlZuQzOiHtNpUcOzRqAkOw-1658672642443@nyuu</segment>
<segment bytes="739655" number="7">RuGaHaWfEsAeGpLwLzQpFdOd-1658672642476@nyuu</segment>
<segment bytes="739807" number="8">CmNcLvFdZfTjIlXcQiWdHuTe-1658672642524@nyuu</segment>
<segment bytes="739636" number="9">UxRnTlCuZjUcIcXhAmYkDdQz-1658672642677@nyuu</segment>
<segment bytes="739545" number="10">PvLmSwSzFzJjLoNiVdCpAqKp-1658672642735@nyuu</segment>
<segment bytes="739607" number="11">VjVmMvDxOzXvNmOrOwGdBbVg-1658672642826@nyuu</segment>
<segment bytes="739816" number="12">ToFnSxDePaBlQcEjViYzSdGo-1658672642903@nyuu</segment>
<segment bytes="739556" number="13">LjHkMlYqRaQcBpVkFjCuXrHc-1658672642938@nyuu</segment>
<segment bytes="739829" number="14">DpAmYrOyHoXkGkCfZcDiBiIn-1658672642972@nyuu</segment>
<segment bytes="465138" number="15">DkGlMfAxEnPcPlAjIbBpNpFg-1658672643110@nyuu</segment>
</segments>
</file>
<file poster="blablamannetje &lt;blabla@example.com&gt;" date="1658672643" subject="reftestnzb_100MB_a82beff8e340 [05/20] - &quot;sometestfile-100MB.part05.rar&quot; yEnc (1/15) 10485760">
<groups>
<group>alt.binaries.test</group>
</groups>
<segments>
<segment bytes="739678" number="1">LkSeRbWaNmKoPaLxRpYcOjWf-1658672643185@nyuu</segment>
<segment bytes="739820" number="2">MoUlMsBmZmWtBkFzJtRuYcJy-1658672643272@nyuu</segment>
<segment bytes="739746" number="3">QmYmNyEiSmVgSqVaQmEvXuBh-1658672643358@nyuu</segment>
<segment bytes="739763" number="4">EfHzOhLcXqKfCwRiKcAmYdCf-1658672643403@nyuu</segment>
<segment bytes="739669" number="5">YpBuJtUmPlDvLiPpKlPoTcZg-1658672643425@nyuu</segment>
<segment bytes="739657" number="6">PmGyAhWxVtBiOcOhGaIwSxRd-1658672643555@nyuu</segment>
<segment bytes="739728" number="7">HaSrHrMhKqHxDdDcYqNkUdEp-1658672643632@nyuu</segment>
<segment bytes="739449" number="8">QcDnQmPkMiHuWqIrHsRqWaYu-1658672643725@nyuu</segment>
<segment bytes="739583" number="9">SjThZtNpPqLlReTpKlQpTwBk-1658672643799@nyuu</segment>
<segment bytes="739761" number="10">KjMnVtRmOgJjYnYeQuLdRkNd-1658672643832@nyuu</segment>
<segment bytes="739545" number="11">UeIdYsQtLaKkDqJpFjTrAjSp-1658672643871@nyuu</segment>
<segment bytes="739505" number="12">LbVrYoMpQzDbNzXbAdNjEvAo-1658672643996@nyuu</segment>
<segment bytes="739686" number="13">QdUwTpUvVhBtDaGvYsTrIuAt-1658672644078@nyuu</segment>
<segment bytes="739756" number="14">FtFgNeYsBoLzImYqDbGfSdQf-1658672644160@nyuu</segment>
<segment bytes="464954" number="15">OsQiRhKjTyYwAwUaGmUpUlLe-1658672644258@nyuu</segment>
</segments>
</file>
<file poster="blablamannetje &lt;blabla@example.com&gt;" date="1658672644" subject="reftestnzb_100MB_a82beff8e340 [06/20] - &quot;sometestfile-100MB.part06.rar&quot; yEnc (1/15) 10485760">
<groups>
<group>alt.binaries.test</group>
</groups>
<segments>
<segment bytes="739712" number="1">HwJpGfDtJuCiCnLgCuTxAgOe-1658672644335@nyuu</segment>
<segment bytes="739540" number="2">DxAdNjPhGcJhNzQeYlJjYaAj-1658672644340@nyuu</segment>
<segment bytes="739778" number="3">UsXkQuGsJuQgSpZzAqYqDhPm-1658672644365@nyuu</segment>
<segment bytes="739697" number="4">OvDlAlUaWpZuIvImBcInGxZm-1658672644530@nyuu</segment>
<segment bytes="739712" number="5">JrGfSbDkHsIrIiZzLpWuJzZj-1658672644599@nyuu</segment>
<segment bytes="739687" number="6">TfYyCsKwKgUfYvXlZwGdLuJx-1658672644713@nyuu</segment>
<segment bytes="739592" number="7">MgQiXjLvFpEqNdIoMxEkPoJb-1658672644783@nyuu</segment>
<segment bytes="739813" number="8">TbBhHaXgToWiTjBkTvPfVjSf-1658672644805@nyuu</segment>
<segment bytes="739419" number="9">NpZaHwXwIrZdCeQfBfJuZhVm-1658672644831@nyuu</segment>
<segment bytes="739735" number="10">EvDjIqRhNmFzYsTqFxUfLmJo-1658672644945@nyuu</segment>
<segment bytes="739727" number="11">YcAnJpKgSmDmTrExKtClJiJw-1658672645044@nyuu</segment>
<segment bytes="739698" number="12">UdPfUkYtQqEfBiYsHeJnBoFv-1658672645173@nyuu</segment>
<segment bytes="739628" number="13">GjXmLsWnXvQuOhZrXuFaQsWt-1658672645264@nyuu</segment>
<segment bytes="739599" number="14">YoEsZaVnNrElAnIoBdZkEsUl-1658672645310@nyuu</segment>
<segment bytes="465020" number="15">FiDqTcLyQjSiMoKwNkJcOpKu-1658672645321@nyuu</segment>
</segments>
</file>
<file poster="blablamannetje &lt;blabla@example.com&gt;" date="1658672645" subject="reftestnzb_100MB_a82beff8e340 [07/20] - &quot;sometestfile-100MB.part07.rar&quot; yEnc (1/15) 10485760">
<groups>
<group>alt.binaries.test</group>
</groups>
<segments>
<segment bytes="739612" number="1">BcCnTfTbAqSiUoOkJyMhOpGx-1658672645395@nyuu</segment>
<segment bytes="739836" number="2">QsClUcDuHqEcDiTpOdGfDgFs-1658672645483@nyuu</segment>
<segment bytes="739582" number="3">EsXdXwNbZnIsFhPxQkKhSaHj-1658672645616@nyuu</segment>
<segment bytes="739722" number="4">XuDkCuXkIkJsIjAdKsTkLqWq-1658672645751@nyuu</segment>
<segment bytes="739828" number="5">PfEjJjTjHiCsXqDxYiBcUxCw-1658672645777@nyuu</segment>
<segment bytes="739674" number="6">IrGgIuRlScWcZwCtDjTrQdKy-1658672645805@nyuu</segment>
<segment bytes="739612" number="7">LwVpWrWgUjSuHyLqXoZrMaKb-1658672645859@nyuu</segment>
<segment bytes="739769" number="8">VxLjOjYzKeEoGxUgBeIwSfFb-1658672645927@nyuu</segment>
<segment bytes="739698" number="9">OaQySkXmXlTnWbSoLkOmExIn-1658672646084@nyuu</segment>
<segment bytes="739756" number="10">JmAsMqGcGaVsPiUcRbWqScYh-1658672646171@nyuu</segment>
<segment bytes="739526" number="11">EcOcFyCdIaJvOnNjYkMgTyLc-1658672646251@nyuu</segment>
<segment bytes="739937" number="12">JdTfZrWbKhYyPpCuOkEuUpWs-1658672646289@nyuu</segment>
<segment bytes="739633" number="13">CtZkDeFoUhJyXeTsOxQcPfEg-1658672646332@nyuu</segment>
<segment bytes="739722" number="14">ArAjLtAfYtVjRxHwPeTfSsPw-1658672646366@nyuu</segment>
<segment bytes="465061" number="15">CmKcCzHnYhEqUxMoOyPwNgUe-1658672646549@nyuu</segment>
</segments>
</file>
<file poster="blablamannetje &lt;blabla@example.com&gt;" date="1658672646" subject="reftestnzb_100MB_a82beff8e340 [08/20] - &quot;sometestfile-100MB.part08.rar&quot; yEnc (1/15) 10485760">
<groups>
<group>alt.binaries.test</group>
</groups>
<segments>
<segment bytes="739536" number="1">ExOtMnHzYeTeXbPrQpMqQcEd-1658672646626@nyuu</segment>
<segment bytes="739547" number="2">FvHhUgEeOuOmZuJrOeOdCmBp-1658672646690@nyuu</segment>
<segment bytes="739691" number="3">OgIaDbSgOmMjGkNgYzAvOzEa-1658672646751@nyuu</segment>
<segment bytes="739690" number="4">LxTeXnRpDpQsMjQxIpRzFfQo-1658672646793@nyuu</segment>
<segment bytes="739819" number="5">VcToYzYiJqAwGvCmMiVsGqNj-1658672646834@nyuu</segment>
<segment bytes="739598" number="6">XhBxFwNzQdHkIoGcHdMeDeSo-1658672646992@nyuu</segment>
<segment bytes="739658" number="7">BmQxAvJfEwEdKzVoMxCoVmIr-1658672647067@nyuu</segment>
<segment bytes="739732" number="8">PxYzIiRmDrSdFmKaSfTtQrWp-1658672647129@nyuu</segment>
<segment bytes="739867" number="9">PnSpIsPeHgAfThOjXyOpNlCo-1658672647192@nyuu</segment>
<segment bytes="739740" number="10">YfHjYuKnUvRlBnKdDqOoGnKo-1658672647224@nyuu</segment>
<segment bytes="739780" number="11">WpPkPhPeXsBiMkAkUcEcZuQg-1658672647285@nyuu</segment>
<segment bytes="739649" number="12">SlOeJlNtDjHqTuEkAeNxMdDk-1658672647439@nyuu</segment>
<segment bytes="739899" number="13">AdCgCfRmEvZkRzXgCoLrHfGa-1658672647508@nyuu</segment>
<segment bytes="739666" number="14">JrQhNuXmRiLjRaBvNlBzMgAd-1658672647568@nyuu</segment>
<segment bytes="464911" number="15">XiYcGuBtZdChJfIeKeYwAsHy-1658672647654@nyuu</segment>
</segments>
</file>
<file poster="blablamannetje &lt;blabla@example.com&gt;" date="1658672647" subject="reftestnzb_100MB_a82beff8e340 [09/20] - &quot;sometestfile-100MB.part09.rar&quot; yEnc (1/15) 10485760">
<groups>
<group>alt.binaries.test</group>
</groups>
<segments>
<segment bytes="739437" number="1">YnIxYaQuEcKvMpSyYnEeAvVn-1658672647688@nyuu</segment>
<segment bytes="739623" number="2">XrFdJwZqRcRtHnIcDnEzCoMc-1658672647743@nyuu</segment>
<segment bytes="739486" number="3">WfRuRmRrPwGgKwZhPaAmNpOn-1658672647970@nyuu</segment>
<segment bytes="739521" number="4">PkCrNtOrEvNvRoPaDuAuGqSf-1658672648003@nyuu</segment>
<segment bytes="739505" number="5">KsFvGlAdBjRfTlZhVxEuWxJm-1658672648029@nyuu</segment>
<segment bytes="739627" number="6">ZnIxXyHkDpGpBkLlPkQnHwWt-1658672648104@nyuu</segment>
<segment bytes="739623" number="7">RhVuUwDpCoRiUoNzUpOpFrWp-1658672648150@nyuu</segment>
<segment bytes="739569" number="8">JpYiPxYmAaClCuXtYwLcTkHb-1658672648199@nyuu</segment>
<segment bytes="739753" number="9">GiSuQeEyMkMeVaJmReDyKgVt-1658672648484@nyuu</segment>
<segment bytes="739515" number="10">LrFxMoDtJaNcEiLgDxQoFgIq-1658672648486@nyuu</segment>
<segment bytes="739602" number="11">BfUcDaUxLsNrZzAvBrLcRdWw-1658672648524@nyuu</segment>
<segment bytes="739622" number="12">MgHqJlXjFsEmGtCuCvTrMfDl-1658672648565@nyuu</segment>
<segment bytes="739802" number="13">DgZiJzKyBoGaWvXrRcRfPbZy-1658672648642@nyuu</segment>
<segment bytes="739798" number="14">SvAwVvXyWaFgPkAwAxLmAoVe-1658672648661@nyuu</segment>
<segment bytes="464943" number="15">UwZyZtBxBlZdQnIbMkAmFrUx-1658672648994@nyuu</segment>
</segments>
</file>
<file poster="blablamannetje &lt;blabla@example.com&gt;" date="1658672650" subject="reftestnzb_100MB_a82beff8e340 [11/20] - &quot;sometestfile-100MB.part11.rar&quot; yEnc (1/1) 1745">
<groups>
<group>alt.binaries.test</group>
</groups>
<segments>
<segment bytes="1965" number="1">MzGbXzBiDkQkCkPjHgRfYgSt-1658672650132@nyuu</segment>
</segments>
</file>
<file poster="blablamannetje &lt;blabla@example.com&gt;" date="1658672649" subject="reftestnzb_100MB_a82beff8e340 [10/20] - &quot;sometestfile-100MB.part10.rar&quot; yEnc (1/15) 10485760">
<groups>
<group>alt.binaries.test</group>
</groups>
<segments>
<segment bytes="739508" number="1">QbGaVgPmAxUeYtGfRrNaCgPi-1658672649037@nyuu</segment>
<segment bytes="739728" number="2">TlYaEwZcLkMxXiVlWnXbBhCo-1658672649043@nyuu</segment>
<segment bytes="739729" number="3">JvFuObAaRgTpRdAaNsBnUjSf-1658672649046@nyuu</segment>
<segment bytes="739717" number="4">ViJqMxYcZuCzRiXqZqPyXhVl-1658672649105@nyuu</segment>
<segment bytes="739773" number="5">QaYyAkGsSmRwGwWlYwOcIdCh-1658672649138@nyuu</segment>
<segment bytes="739801" number="6">KdLcItOyTuDfZlFvDgFoGjLx-1658672649505@nyuu</segment>
<segment bytes="739517" number="7">DwEiPdQdSdKbYjQzSpCtNnBp-1658672649527@nyuu</segment>
<segment bytes="739823" number="8">JjZfPzJoYrSnSqOzLfQdLaJe-1658672649559@nyuu</segment>
<segment bytes="739682" number="9">JpAdAoOiWbLlElNnXyZqUrZk-1658672649591@nyuu</segment>
<segment bytes="739581" number="10">KcXsPhOqSmVlImNiAaBxOeDg-1658672649593@nyuu</segment>
<segment bytes="739466" number="11">OcKeQsZvSvCzZzGkSyYaIwLe-1658672649621@nyuu</segment>
<segment bytes="739695" number="12">TpSgWsQbCxSvRhCpNeVxXpQp-1658672650017@nyuu</segment>
<segment bytes="739642" number="13">FgLlWiOgZsXwZbDiUfRlUbAh-1658672650037@nyuu</segment>
<segment bytes="739797" number="14">DpWgTfSaHsEyDoRwKmMrHlNg-1658672650077@nyuu</segment>
<segment bytes="464958" number="15">ApWcZeAvJfEfArBnTqKaAlTi-1658672650131@nyuu</segment>
</segments>
</file>
<file poster="blablamannetje &lt;blabla@example.com&gt;" date="1658672650" subject="reftestnzb_100MB_a82beff8e340 [12/20] - &quot;sometestfile-100MB.par2&quot; yEnc (1/1) 42740">
<groups>
<group>alt.binaries.test</group>
</groups>
<segments>
<segment bytes="44186" number="1">GiCuNqThWxXhQtBdIpCuSpTu-1658672650163@nyuu</segment>
</segments>
</file>
<file poster="blablamannetje &lt;blabla@example.com&gt;" date="1658672650" subject="reftestnzb_100MB_a82beff8e340 [13/20] - &quot;sometestfile-100MB.vol000+001.par2&quot; yEnc (1/1) 95504">
<groups>
<group>alt.binaries.test</group>
</groups>
<segments>
<segment bytes="98613" number="1">XwSuKgDcYgYbThUtGbFnYlMr-1658672650478@nyuu</segment>
</segments>
</file>
<file poster="blablamannetje &lt;blabla@example.com&gt;" date="1658672650" subject="reftestnzb_100MB_a82beff8e340 [14/20] - &quot;sometestfile-100MB.vol001+002.par2&quot; yEnc (1/1) 148268">
<groups>
<group>alt.binaries.test</group>
</groups>
<segments>
<segment bytes="153068" number="1">XtIpShElZfGwAxEdWcWmMoSg-1658672650533@nyuu</segment>
</segments>
</file>
<file poster="blablamannetje &lt;blabla@example.com&gt;" date="1658672650" subject="reftestnzb_100MB_a82beff8e340 [15/20] - &quot;sometestfile-100MB.vol003+004.par2&quot; yEnc (1/1) 296420">
<groups>
<group>alt.binaries.test</group>
</groups>
<segments>
<segment bytes="305933" number="1">YjBcYcXxFoSmTdKrXbQaVcEc-1658672650555@nyuu</segment>
</segments>
</file>
<file poster="blablamannetje &lt;blabla@example.com&gt;" date="1658672650" subject="reftestnzb_100MB_a82beff8e340 [16/20] - &quot;sometestfile-100MB.vol007+008.par2&quot; yEnc (1/1) 550100">
<groups>
<group>alt.binaries.test</group>
</groups>
<segments>
<segment bytes="567518" number="1">YrBmEjHwHgWrGpWwWvHnZsNr-1658672650628@nyuu</segment>
</segments>
</file>
<file poster="blablamannetje &lt;blabla@example.com&gt;" date="1658672650" subject="reftestnzb_100MB_a82beff8e340 [17/20] - &quot;sometestfile-100MB.vol015+016.par2&quot; yEnc (1/2) 1014836">
<groups>
<group>alt.binaries.test</group>
</groups>
<segments>
<segment bytes="739515" number="1">HmLkJbFaPwGnDeHoAsUeWvOx-1658672650641@nyuu</segment>
<segment bytes="307638" number="2">BgMgXvTfPmQpMeIxMrVbSbWb-1658672650648@nyuu</segment>
</segments>
</file>
<file poster="blablamannetje &lt;blabla@example.com&gt;" date="1658672650" subject="reftestnzb_100MB_a82beff8e340 [18/20] - &quot;sometestfile-100MB.vol031+032.par2&quot; yEnc (1/3) 1901684">
<groups>
<group>alt.binaries.test</group>
</groups>
<segments>
<segment bytes="739626" number="1">InAlBjYtHfRoZbUcLbLjVwGg-1658672650925@nyuu</segment>
<segment bytes="739341" number="2">UeLxHaYaYrGcWoApMiIeUcFc-1658672650940@nyuu</segment>
<segment bytes="482988" number="3">TmTyRsQpWjLvJoZtYvDxKfDk-1658672650960@nyuu</segment>
</segments>
</file>
<file poster="blablamannetje &lt;blabla@example.com&gt;" date="1658672651" subject="reftestnzb_100MB_a82beff8e340 [19/20] - &quot;sometestfile-100MB.vol063+064.par2&quot; yEnc (1/6) 3632756">
<groups>
<group>alt.binaries.test</group>
</groups>
<segments>
<segment bytes="739390" number="1">AoFjKyMxTlDpZtDzHyJtFaVt-1658672651017@nyuu</segment>
<segment bytes="739605" number="2">WbUyNnEyReLnSdNwBsVqVfVc-1658672651029@nyuu</segment>
<segment bytes="739539" number="3">FaXfJfTqWuIiMvTlGdOfGgKi-1658672651035@nyuu</segment>
<segment bytes="739543" number="4">NeEnAwTqVeRoJuStBvPhSsCf-1658672651324@nyuu</segment>
<segment bytes="739399" number="5">FeFfMtWjQyDkIcPaPnFnTvZl-1658672651372@nyuu</segment>
<segment bytes="50461" number="6">JxFgMzBwLqVoRcPuJzHoSgFy-1658672651406@nyuu</segment>
</segments>
</file>
<file poster="blablamannetje &lt;blabla@example.com&gt;" date="1658672651" subject="reftestnzb_100MB_a82beff8e340 [20/20] - &quot;sometestfile-100MB.vol127+072.par2&quot; yEnc (1/6) 4054868">
<groups>
<group>alt.binaries.test</group>
</groups>
<segments>
<segment bytes="739529" number="1">ZrZzDkZqMlGxTlXsOxZzWkFy-1658672651436@nyuu</segment>
<segment bytes="739638" number="2">EkIfIsZtKbFcHyLtEiOvCgUe-1658672651500@nyuu</segment>
<segment bytes="739479" number="3">FdAlCsPqQgToRlEcZxCzHhFu-1658672651528@nyuu</segment>
<segment bytes="739655" number="4">OnYrJuAaClWaDjEdFmYoDaKt-1658672651727@nyuu</segment>
<segment bytes="739624" number="5">TsJbMqVtYcIaGqEvShTyEhWf-1658672651793@nyuu</segment>
<segment bytes="485771" number="6">UbNvVcQoDxAfCiPsEqFfGkDu-1658672651860@nyuu</segment>
</segments>
</file>
</nzb>